188 lines
8.5 KiB
Python
188 lines
8.5 KiB
Python
# Асинхронный обновитель односекционного STM32F407 через SET v2 USB. Проверка образа
|
|
# предшествует передаче, а продвижение по этапам зависит от ответов загрузчика; интерфейс не
|
|
# должен блокироваться ожиданием Flash.
|
|
|
|
"""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)
|