Files
templates/python/set_devices/qt_ports/f407_usb_bootloader.py

184 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)