Add shared waveform processing, generation and firmware image support

This commit is contained in:
2026-09-27 01:45:08 +03:00
parent 2ad29e7ffd
commit cc22c803d1
36 changed files with 2591 additions and 61 deletions

View 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)