Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
177
python/set_devices/qt_ports/stm32_bootloader.py
Normal file
177
python/set_devices/qt_ports/stm32_bootloader.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Non-blocking client for the STM32 ROM UART bootloader described by AN3155."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from typing import Callable
|
||||
|
||||
from .qt_compat import QObject, QTimer, Signal
|
||||
from .qt_compat import QSerialPort
|
||||
|
||||
from set_devices.firmware import FirmwareImage
|
||||
from protocan.stm32_boot import (
|
||||
ACK, NACK, SYNC, address, command, erase_pages_payload, write_payload,
|
||||
)
|
||||
|
||||
|
||||
class Stm32Bootloader(QObject):
|
||||
"""Programs STM32 system Flash over the factory UART bootloader (8E1)."""
|
||||
|
||||
progress = Signal(int, str)
|
||||
finished = Signal(bool, str)
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._serial = QSerialPort(self)
|
||||
self._serial.readyRead.connect(self._read_available)
|
||||
self._serial.errorOccurred.connect(self._serial_error)
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.timeout.connect(lambda: self._fail("Нет ответа STM32 bootloader"))
|
||||
self._rx = bytearray()
|
||||
self._steps: deque[tuple[bytes, int, str, Callable[[], None]]] = deque()
|
||||
self._waiting: tuple[int, str, Callable[[], None]] | None = None
|
||||
self._image: FirmwareImage | None = None
|
||||
self._offset = 0
|
||||
self._cancelled = False
|
||||
|
||||
def start(self, image: FirmwareImage, port_name: str, baud_rate: int) -> None:
|
||||
if self._serial.isOpen() or self._waiting is not None:
|
||||
self.finished.emit(False, "Прошивка STM32 уже выполняется")
|
||||
return
|
||||
if not port_name or baud_rate <= 0:
|
||||
self.finished.emit(False, "Неверные настройки UART")
|
||||
return
|
||||
if not 0x08000000 <= image.base_address <= 0x080FFFFF:
|
||||
self.finished.emit(False, "Для STM32 укажите адрес Flash, например 0x08000000")
|
||||
return
|
||||
if image.base_address + len(image.data) > 0x08100000:
|
||||
self.finished.emit(False, "Образ выходит за допустимый диапазон Flash STM32")
|
||||
return
|
||||
self._serial.setPortName(port_name)
|
||||
self._serial.setBaudRate(baud_rate)
|
||||
self._serial.setDataBits(QSerialPort.DataBits.Data8)
|
||||
self._serial.setParity(QSerialPort.Parity.EvenParity)
|
||||
self._serial.setStopBits(QSerialPort.StopBits.OneStop)
|
||||
self._serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
|
||||
if not self._serial.open(QSerialPort.OpenModeFlag.ReadWrite):
|
||||
self.finished.emit(False, f"Не удалось открыть {port_name}: {self._serial.errorString()}")
|
||||
return
|
||||
self._image = image
|
||||
self._offset = 0
|
||||
self._cancelled = False
|
||||
self._rx.clear()
|
||||
self._steps.clear()
|
||||
self.progress.emit(0, "Синхронизация с STM32 (BOOT0=1)")
|
||||
self._queue(SYNC, 1500, "синхронизации", self._erase)
|
||||
self._next()
|
||||
|
||||
def cancel(self) -> None:
|
||||
if not self._serial.isOpen():
|
||||
return
|
||||
self._cancelled = True
|
||||
self._finish(False, "Операция отменена")
|
||||
|
||||
def _queue(self, packet: bytes, timeout_ms: int, stage: str,
|
||||
callback: Callable[[], None]) -> None:
|
||||
self._steps.append((packet, timeout_ms, stage, callback))
|
||||
|
||||
def _next(self) -> None:
|
||||
if self._waiting is not None or not self._steps or self._cancelled:
|
||||
return
|
||||
packet, timeout_ms, stage, callback = self._steps.popleft()
|
||||
if self._serial.write(packet) != len(packet):
|
||||
self._fail(f"Ошибка передачи на этапе {stage}")
|
||||
return
|
||||
self._waiting = (timeout_ms, stage, callback)
|
||||
self._timer.start(timeout_ms)
|
||||
|
||||
def _erase(self) -> None:
|
||||
self.progress.emit(1, "Стирание Flash")
|
||||
self._queue(command(0x43), 1500, "команды Erase", self._erase_payload)
|
||||
self._next()
|
||||
|
||||
def _erase_payload(self) -> None:
|
||||
assert self._image is not None
|
||||
first = (self._image.base_address - 0x08000000) // 1024
|
||||
last = (self._image.base_address + len(self._image.data) - 1 - 0x08000000) // 1024
|
||||
pages = list(range(first, last + 1))
|
||||
self._queue(erase_pages_payload(pages), 20000, "стирания страниц Flash",
|
||||
self._write_next)
|
||||
self._next()
|
||||
|
||||
def _write_next(self) -> None:
|
||||
image = self._image
|
||||
if image is None:
|
||||
return
|
||||
if self._offset >= len(image.data):
|
||||
self.progress.emit(100, "Запуск приложения")
|
||||
self._queue(command(0x21), 1500, "команды Go", self._go_address)
|
||||
self._next()
|
||||
return
|
||||
chunk = image.data[self._offset:self._offset + 256]
|
||||
# STM32 Flash is programmed by words; erased bytes safely pad the tail.
|
||||
if len(chunk) & 3:
|
||||
chunk += b"\xff" * (4 - (len(chunk) & 3))
|
||||
self._queue(command(0x31), 1500, "команды Write Memory", self._write_address)
|
||||
self._next()
|
||||
|
||||
def _write_address(self) -> None:
|
||||
assert self._image is not None
|
||||
self._queue(address(self._image.base_address + self._offset), 1500,
|
||||
"адреса блока", self._write_data)
|
||||
self._next()
|
||||
|
||||
def _write_data(self) -> None:
|
||||
assert self._image is not None
|
||||
chunk = self._image.data[self._offset:self._offset + 256]
|
||||
actual = len(chunk)
|
||||
if len(chunk) & 3:
|
||||
chunk += b"\xff" * (4 - (len(chunk) & 3))
|
||||
|
||||
def accepted() -> None:
|
||||
self._offset += actual
|
||||
percent = int(self._offset * 100 / len(self._image.data))
|
||||
self.progress.emit(percent, f"Записано {self._offset} байт")
|
||||
self._write_next()
|
||||
|
||||
self._queue(write_payload(chunk), 2500, "записи блока", accepted)
|
||||
self._next()
|
||||
|
||||
def _go_address(self) -> None:
|
||||
assert self._image is not None
|
||||
self._queue(address(self._image.base_address), 1500, "адреса запуска",
|
||||
lambda: self._finish(True, "Прошивка STM32 завершена, приложение запущено"))
|
||||
self._next()
|
||||
|
||||
def _read_available(self) -> None:
|
||||
self._rx.extend(bytes(self._serial.readAll()))
|
||||
while self._waiting is not None and self._rx:
|
||||
response = self._rx.pop(0)
|
||||
timeout_ms, stage, callback = self._waiting
|
||||
if response not in (ACK, NACK):
|
||||
continue
|
||||
self._timer.stop()
|
||||
self._waiting = None
|
||||
if response == NACK:
|
||||
self._fail(f"STM32 отклонил операцию на этапе {stage}")
|
||||
return
|
||||
callback()
|
||||
|
||||
def _serial_error(self, error: QSerialPort.SerialPortError) -> None:
|
||||
if error not in (QSerialPort.SerialPortError.NoError,
|
||||
QSerialPort.SerialPortError.TimeoutError):
|
||||
self._fail(self._serial.errorString())
|
||||
|
||||
def _fail(self, message: str) -> None:
|
||||
self._finish(False, message)
|
||||
|
||||
def _finish(self, success: bool, message: str) -> None:
|
||||
self._timer.stop()
|
||||
self._waiting = None
|
||||
self._steps.clear()
|
||||
self._image = None
|
||||
if self._serial.isOpen():
|
||||
self._serial.close()
|
||||
self.progress.emit(100 if success else 0, message)
|
||||
self.finished.emit(success, message)
|
||||
Reference in New Issue
Block a user