Add shared waveform processing, generation and firmware image support
This commit is contained in:
183
python/set_devices/qt_ports/f407_usb_bootloader.py
Normal file
183
python/set_devices/qt_ports/f407_usb_bootloader.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Asynchronous SET v2 USB updater for the STM32F407VE single-slot port."""
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from .qt_compat import QObject, QTimer, Signal, QSerialPort
|
||||
from setprotocol.core import (Frame, FrameFlag, FrameParser, build_frame,
|
||||
decode_response, DeviceInfo, FirmwareBegin, FirmwareFlag, encode_firmware_data)
|
||||
|
||||
APP_BASE = 0x08010000
|
||||
APP_SIZE = 0x70000
|
||||
BOOT_MODEL = "STM32F407-EMU-USB-BOOT"
|
||||
|
||||
|
||||
def validate_image(image):
|
||||
if image.base_address != APP_BASE or not 8 <= len(image.data) <= APP_SIZE:
|
||||
raise ValueError("Нужен образ приложения для 0x08010000, размером до 448 КБ")
|
||||
sp, pc = struct.unpack_from("<II", image.data)
|
||||
if sp & 7 or not (0x20000000 < sp <= 0x20020000 or 0x10000000 < sp <= 0x10010000):
|
||||
raise ValueError("Неверный начальный указатель стека")
|
||||
if not pc & 1 or not APP_BASE <= (pc & ~1) < APP_BASE + len(image.data):
|
||||
raise ValueError("Образ собран не для USB-загрузчика (нужен адрес 0x08010000)")
|
||||
|
||||
|
||||
class F407UsbBootloader(QObject):
|
||||
progress = Signal(int, str)
|
||||
finished = Signal(bool, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._serial = QSerialPort(self)
|
||||
self._serial.readyRead.connect(self._read)
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.timeout.connect(self._timeout)
|
||||
self._reconnect = QTimer(self)
|
||||
self._reconnect.setSingleShot(True)
|
||||
self._reconnect.timeout.connect(self._open_boot)
|
||||
self._active = False
|
||||
self._sequence = 0
|
||||
self._pending = None
|
||||
self._parser = FrameParser()
|
||||
self._attempts = 0
|
||||
|
||||
def start(self, image, port_name, baud_rate=115200):
|
||||
if self._active:
|
||||
return
|
||||
try:
|
||||
validate_image(image)
|
||||
if not port_name:
|
||||
raise ValueError("Выберите COM-порт USB платы")
|
||||
except ValueError as error:
|
||||
self.finished.emit(False, str(error))
|
||||
return
|
||||
self._active = True
|
||||
self._image = image
|
||||
self._port_name = port_name
|
||||
self._offset = 0
|
||||
self._attempts = 0
|
||||
self._parser = FrameParser()
|
||||
if not self._open():
|
||||
self._finish(False, "Не удалось открыть USB COM-порт: " + self._serial.errorString())
|
||||
return
|
||||
self.progress.emit(0, "Определение USB-загрузчика")
|
||||
self._request(2, b"", "identify", 2500)
|
||||
|
||||
def _open(self):
|
||||
self._serial.close()
|
||||
self._serial.setPortName(self._port_name)
|
||||
self._serial.setBaudRate(115200)
|
||||
self._serial.setDataBits(QSerialPort.DataBits.Data8)
|
||||
self._serial.setParity(QSerialPort.Parity.NoParity)
|
||||
self._serial.setStopBits(QSerialPort.StopBits.OneStop)
|
||||
self._serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
|
||||
self._parser = FrameParser()
|
||||
return self._serial.open(QSerialPort.OpenModeFlag.ReadWrite)
|
||||
|
||||
def _request(self, kind, payload, phase, timeout=20000):
|
||||
self._sequence = (self._sequence + 1) & 0xffff
|
||||
self._pending = (kind, self._sequence, phase)
|
||||
packet = build_frame(Frame(kind, self._sequence, payload, FrameFlag.ACK_REQUIRED, 42, 0))
|
||||
if self._serial.write(packet) != len(packet):
|
||||
self._finish(False, "Ошибка передачи USB")
|
||||
return
|
||||
self._timer.start(timeout)
|
||||
|
||||
def _read(self):
|
||||
for frame in self._parser.feed(bytes(self._serial.readAll())):
|
||||
if not self._active or self._pending is None:
|
||||
continue
|
||||
kind, sequence, phase = self._pending
|
||||
if (frame.message_type != kind or frame.sequence != sequence or
|
||||
frame.source != 16 or frame.destination != 42 or
|
||||
not frame.flags & FrameFlag.RESPONSE):
|
||||
continue
|
||||
self._timer.stop()
|
||||
self._pending = None
|
||||
try:
|
||||
status, body = decode_response(frame)
|
||||
except ValueError as error:
|
||||
self._finish(False, "Неверный ответ загрузчика: " + str(error))
|
||||
return
|
||||
if status:
|
||||
self._finish(False, f"Загрузчик отклонил {phase}: код {status}. Повторите загрузку целиком.")
|
||||
return
|
||||
try:
|
||||
self._advance(phase, body)
|
||||
except (ValueError, struct.error) as error:
|
||||
self._finish(False, "Неверный ответ загрузчика: " + str(error))
|
||||
return
|
||||
|
||||
def _advance(self, phase, body):
|
||||
if phase in ("identify", "reconnect"):
|
||||
model = DeviceInfo.decode(body).model
|
||||
if model == BOOT_MODEL:
|
||||
self._attempts = 0
|
||||
image = self._image
|
||||
begin = FirmwareBegin(len(image.data), image.crc32, image.version,
|
||||
APP_BASE, 0, 256, bytes(32), FirmwareFlag.ERASE_SLOT)
|
||||
self.progress.emit(1, "Стирание области приложения")
|
||||
self._request(0x100, begin.encode(), "begin")
|
||||
elif model == "STM32F407-EMU-EXPERIMENT" and phase == "identify":
|
||||
self._request(0x1003, b"", "enter")
|
||||
elif phase == "reconnect" and model == "STM32F407-EMU-EXPERIMENT":
|
||||
self._retry_open()
|
||||
else:
|
||||
self._finish(False, "Выбрано другое устройство: " + model)
|
||||
elif phase == "enter":
|
||||
self.progress.emit(0, "Ожидание переподключения USB")
|
||||
self._serial.close()
|
||||
self._attempts = 40
|
||||
self._reconnect.start(1000)
|
||||
elif phase in ("begin", "data"):
|
||||
if phase == "data":
|
||||
self._offset += self._sent
|
||||
if self._offset == len(self._image.data):
|
||||
self.progress.emit(98, "Проверка CRC содержимого Flash")
|
||||
self._request(0x102, struct.pack("<II32s", len(self._image.data),
|
||||
self._image.crc32, bytes(32)), "end")
|
||||
else:
|
||||
block = self._image.data[self._offset:self._offset+256]
|
||||
self._sent = len(block)
|
||||
self.progress.emit(2 + 95*self._offset//len(self._image.data),
|
||||
f"Запись {self._offset}/{len(self._image.data)} байт")
|
||||
self._request(0x101, encode_firmware_data(self._offset, block), "data")
|
||||
elif phase == "end":
|
||||
self._request(0x105, b"", "activate")
|
||||
elif phase == "activate":
|
||||
self.progress.emit(100, "CRC проверена, команда запуска подтверждена")
|
||||
self._finish(True, "Прошивка записана. Плата перезапускается; подключитесь к ней снова.")
|
||||
|
||||
def _retry_open(self):
|
||||
self._serial.close()
|
||||
if self._attempts <= 0:
|
||||
self._finish(False, "USB не появился. Выберите COM-порт загрузчика и повторите загрузку.")
|
||||
else:
|
||||
self._reconnect.start(500)
|
||||
|
||||
def _open_boot(self):
|
||||
if not self._active:
|
||||
return
|
||||
self._attempts -= 1
|
||||
if self._open():
|
||||
self._request(2, b"", "reconnect", 500)
|
||||
else:
|
||||
self._retry_open()
|
||||
|
||||
def _timeout(self):
|
||||
if self._pending and self._pending[2] == "reconnect":
|
||||
self._pending = None
|
||||
self._retry_open()
|
||||
else:
|
||||
self._finish(False, "Нет ответа USB. Проверьте порт и повторите полную загрузку.")
|
||||
|
||||
def cancel(self):
|
||||
if self._active:
|
||||
self._finish(False, "Загрузка отменена. Незавершённый образ не запустится; повторите загрузку.")
|
||||
|
||||
def _finish(self, ok, text):
|
||||
self._active = False
|
||||
self._pending = None
|
||||
self._timer.stop()
|
||||
self._reconnect.stop()
|
||||
self._serial.close()
|
||||
self.finished.emit(ok, text)
|
||||
Reference in New Issue
Block a user