184 lines
8.1 KiB
Python
184 lines
8.1 KiB
Python
"""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)
|