178 lines
7.3 KiB
Python
178 lines
7.3 KiB
Python
"""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)
|