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

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