Сохранить совместимый переход RAM-ядра, постоянный поток Qt, точную диагностику обмена и журнал без повторов процентов. Добавить проверку транспортной точки входа официальным TI compiler.
164 lines
28 KiB
Python
164 lines
28 KiB
Python
"""Встроенная прошивка F28335 через штатный SCI Boot без внешнего C2Prog.""" # Qt отвечает за COM и потоки; протокол реализован общим C99-ядром templates.
|
||
import hashlib # Проверяет точные байты RAM-ядра, сохранённого в комплекте postbuild.
|
||
from pathlib import Path # Разрешает имя RAM-ядра относительно выбранного BIN.
|
||
import threading # Передаёт отмену из GUI в рабочий поток без принудительного завершения потока.
|
||
import time # Системные часы используются только транспортом для конечных тайм-аутов ожидания.
|
||
try: # Поддерживает основные сборки Qt 6 и Windows 7/Qt 5.
|
||
from PySide6.QtCore import QThread, Signal, QIODevice, Qt, Slot # Рабочий поток не блокирует GUI ожиданием UART или Flash API.
|
||
from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QCheckBox, QSpinBox, QFileDialog, QLabel # Панель использует обычные контролы SETGUI.
|
||
from PySide6.QtSerialPort import QSerialPort # Транспорт создаёт отдельный системный COM-сеанс на время программирования.
|
||
except ImportError: # Для Python 3.8/Windows 7 использует те же операции Qt 5.
|
||
from PySide2.QtCore import QThread, Signal, QIODevice, Qt, Slot # Сигналы безопасно возвращают результаты в главный поток.
|
||
from PySide2.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QCheckBox, QSpinBox, QFileDialog, QLabel # Интерфейс не требует компонентов, доступных только Qt 6.
|
||
from PySide2.QtSerialPort import QSerialPort # COM-порт остаётся локальной зависимостью адаптера.
|
||
from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Сборка, разбор, checksum и автомат обмена не повторяются в порту.
|
||
|
||
|
||
class SciWorker(QThread): # Один сеанс владеет COM-портом и неизменяемыми буферами до завершения.
|
||
progress = Signal(int, str) # Штатная полоса GUI получает только подтверждённый прогресс C-автомата.
|
||
result = Signal(bool, str) # Ошибка транспорта или checksum не выдаётся за успешную запись.
|
||
|
||
def __init__(self, kernel, image, port, baud, enter=False, timeout_ms=3000, parent=None): # Все параметры снимаются с GUI до запуска потока.
|
||
super().__init__(parent) # Время жизни потока связано с существующей панелью прошивки.
|
||
self.kernel, self.image, self.port, self.baud = kernel, image, port, baud # Буферы не перечитываются с диска после аппаратного старта.
|
||
self._stage, self._last_tx = "Подготовка SCI", b"" # Начальное состояние нужно также для самостоятельного создания SciWorker в проверках транспорта.
|
||
self.enter, self.timeout_ms, self.stop = enter, timeout_ms, threading.Event() # Отмена проверяется во всех конечных ожиданиях.
|
||
|
||
def configure(self, kernel, image, port, baud, enter=False, timeout_ms=3000): # Повторно использует готовый Qt-поток только после полного завершения предыдущего сеанса.
|
||
if self.isRunning(): raise ValueError('Прошивка SCI ещё выполняется') # Не меняем буферы, настройки UART и событие отмены во время аппаратного обмена.
|
||
self.kernel, self.image, self.port, self.baud = kernel, image, port, baud # Фиксируем новый неизменяемый комплект и скорость UART в бодах до запуска run.
|
||
self.enter, self.timeout_ms = enter, timeout_ms # Сохраняем явный вход Monitor и тайм-аут ответа в миллисекундах для нового сеанса.
|
||
self.stop.clear() # Отмена предыдущей операции не должна автоматически отменять следующую явную попытку.
|
||
self._stage, self._last_tx = 'Подготовка SCI', b'' # Диагностика нового сеанса не использует этап или последние байты предыдущей передачи.
|
||
|
||
def cancel(self): # GUI не вызывает методы QSerialPort чужого потока.
|
||
self.stop.set() # Рабочий поток сам завершит чтение и закроет COM-порт.
|
||
|
||
def _cancelled(self): # Единая проверка отмены используется в задержках, чтении и записи.
|
||
if self.stop.is_set(): raise ValueError("Прошивка остановлена оператором") # Отмена не запускает повторную передачу или попытку erase.
|
||
|
||
def _pause(self, milliseconds): # Обязательные паузы TI остаются прерываемыми оператором.
|
||
end = time.monotonic() + milliseconds / 1000 # Миллисекунды C-автомата преобразуются только в системное ожидание порта.
|
||
while time.monotonic() < end: self._cancelled(); self.msleep(min(50, max(1, int((end - time.monotonic()) * 1000)))) # Проверяет отмену не реже одного раза за 50 мс.
|
||
|
||
def _write(self, serial, data): # Передаёт готовые байты и дожидается фактической отправки UART.
|
||
self._cancelled() # Не добавляет новые данные после запроса остановки.
|
||
self._last_tx = bytes(data[:16]) # При ошибке сохраняем первые 16 октетов последней передачи, чтобы отличить команду №93, autobaud и данные ядра.
|
||
if not data: return # Отдельное подтверждение пустого остатка TI не требует передачи.
|
||
if serial.write(data) != len(data): raise OSError("Не удалось поставить полный пакет SCI в очередь COM") # Частичная запись не должна сдвинуть автомат протокола.
|
||
end = time.monotonic() + max(5, len(data) * 12 / self.baud + 2) # Бюджет включает время передачи октетов UART и запас драйвера.
|
||
while serial.bytesToWrite(): # Очередь обязана опустеть до начала ожидания ответа.
|
||
self._cancelled() # Отмена сохраняет конечность длительной отправки на низкой скорости.
|
||
serial.waitForBytesWritten(100) # Короткое ожидание позволяет проверять отмену и ошибку драйвера.
|
||
if time.monotonic() > end: raise TimeoutError("Истёк тайм-аут передачи SCI") # Не ожидает бесконечно отключённый преобразователь.
|
||
|
||
def _read(self, serial, count, timeout): # Собирает ровно требуемое число октетов, не разбирая их поля.
|
||
result, end = bytearray(), time.monotonic() + timeout / 1000 # Единственный дедлайн сохраняется при частичных UART-порциях.
|
||
while len(result) < count: # Фрагментация USB-UART не меняет смысл ответа C-автомата.
|
||
self._cancelled() # Оператор может прекратить ожидание недоступного MCU.
|
||
if serial.bytesAvailable(): result.extend(bytes(serial.read(count - len(result)))) # Следующий ответ не поглощается текущим чтением.
|
||
else: serial.waitForReadyRead(100) # Ожидание остаётся внутри рабочего потока, без зависания GUI.
|
||
if time.monotonic() > end: raise TimeoutError(f"{self._stage}: {self.port}, {self.baud} бод; получено {len(result)}/{count} байт, TX {self._last_tx.hex(' ').upper() or 'нет'}") # Отсутствие эха или успешного Flash API не трактуется как продолжение.
|
||
return bytes(result) # Проверку эха и checksum выполнит C-ядро.
|
||
|
||
def run(self): # Qt вызывает метод в отдельном потоке, где создаётся и закрывается COM-порт.
|
||
serial, started = None, time.monotonic() # COM открывается после проверки образа; монотонные часы измеряют полную длительность текущего сеанса, включая загрузку ядра и обязательные паузы.
|
||
try: # Все исключения превращаются в конечный результат GUI.
|
||
session = Session(self.kernel, self.image) # Проверяет RAM-ядро и Flash-таблицу до любой управляющей команды.
|
||
self._stage = f"Открытие {self.port}" # Ошибка доступа к COM возникает до начала синхронизации с микроконтроллером.
|
||
serial = QSerialPort(); serial.setPortName(self.port); serial.setBaudRate(468750 if self.enter else self.baud) # Штатный Monitor ИБП работает на 468750 бит/с (MONITOR_BRR=9, LSPCLK=37,5 МГц); при ручном входе сразу используется скорость загрузчика из manifest.
|
||
serial.setDataBits(QSerialPort.DataBits.Data8); serial.setParity(QSerialPort.Parity.NoParity); serial.setStopBits(QSerialPort.StopBits.OneStop); serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # TI SCI Boot и первичный загрузчик UlitCo используют 8N1 без аппаратного управления потоком.
|
||
if not serial.open(QIODevice.OpenModeFlag.ReadWrite): raise OSError(f"{self._stage}: {serial.errorString()}") # Занятый терминалом порт не открывается вторым владельцем.
|
||
serial.clear() # Старые данные терминала не должны стать ложным эхо autobaud.
|
||
if self.enter: # Приложение UlitCo принимает переход в загрузчик лишь в разрешённом остановленном состоянии.
|
||
self._stage = "Вход через Monitor №93" # До смены UART контроллер принимает штатный запрос на скорости приложения.
|
||
self.progress.emit(0, f"Monitor {self.port}, 468750 бод; TX {ulitco_enter().hex(' ').upper()}") # Показывает отдельный этап до передачи RAM-ядра.
|
||
self._write(serial, ulitco_enter()); self._pause(500) # Отправляет проверенную команду Monitor №93 на скорости приложения и даёт штатному SCI-приёмнику 500 мс на подготовку autobaud.
|
||
if not serial.setBaudRate(self.baud): raise OSError("Не удалось переключить COM на скорость загрузчика SCI") # Только после выхода из приложения включает скорость RAM-ядра из manifest; отказ запрещает дальнейшую передачу.
|
||
serial.clear() # Остатки ответов Monitor не должны стать ложным эхом нового autobaud.
|
||
exchanges, last_percent = 0, None # Номер обмена уточняет тайм-аут, а последний показанный процент ограничивает журнал одним сообщением на изменение прогресса текущего сеанса.
|
||
while True: # Порядок обменов полностью определяется общим C-автоматом.
|
||
self._cancelled(); state, tx, expected, delay = session.next() # Порт не вычисляет пакетные поля или checksum.
|
||
if state == 0: break # Подтверждённые порции и отправленный терминатор завершили поток.
|
||
if state == 2: self._pause(delay); serial.clear(); continue # TI требует паузу после RAM-ядра и его autobaud.
|
||
exchanges += 1 # Нумеруем только фактические передачи, не включая обязательные паузы C-автомата.
|
||
self._stage = "Первичная синхронизация SCI" if exchanges == 1 else f"Обмен SCI №{exchanges}" # Первая ошибка отделяется от последующей передачи ядра и программирования Flash.
|
||
if exchanges == 1: self.progress.emit(0, f"{self._stage}: {self.port}, {self.baud} бод, TX {tx.hex(' ').upper()}") # До ожидания первого ответа журнал фиксирует скорость и настоящий символ синхронизации.
|
||
self._write(serial, tx) # Отправляет готовый октет ROM либо порцию Flash Kernel A.
|
||
if expected: session.accept(self._read(serial, expected, 60000 if expected == 2 else self.timeout_ms)) # Erase/program получают минуту; эхо ROM использует точный тайм-аут из профиля JSON в миллисекундах.
|
||
percent = session.progress # Подтверждённый процент по-прежнему вычисляется единственным C-автоматом после проверки ответа.
|
||
if percent != last_percent: # Тысячи побайтовых подтверждений RAM-ядра не должны создавать одинаковые строки и queued-сигналы GUI.
|
||
self.progress.emit(percent, "Ядро TI / запись Flash…") # Публикуем каждое изменение процента, включая конечные 100 процентов, после успешного обмена.
|
||
last_percent = percent # Следующий байт с тем же процентом продолжит аппаратный сеанс без повторной перерисовки и записи в журнал.
|
||
self.result.emit(True, f"Передача завершена за {time.monotonic() - started:.1f} с; ядро TI подтвердило порции Flash. Проверка чтением памяти отдельно не выполнялась.") # Сообщаем полное измеренное время сеанса и подтверждение ядра, сохраняя точное указание об отсутствии отдельной проверки чтением.
|
||
except (OSError, ValueError, RuntimeError) as error: self.result.emit(False, str(error)) # Ошибка остановит GUI и сохранится в журнале текущей прошивки.
|
||
finally: # Владение портом освобождается также после отмены или тайм-аута.
|
||
if serial is not None: serial.close() # Закрывает только созданный этим потоком COM-сеанс.
|
||
|
||
|
||
class C2000SciPanel(QWidget): # Общая панель используется SETGUI без зависимости от отдельного приложения C2Prog.
|
||
progress = Signal(int, str) # Согласует рабочий поток со штатной полосой прошивки.
|
||
finished = Signal(bool, str) # Завершение освобождает форму одинаково после успеха, отказа или отмены.
|
||
|
||
def __init__(self, parent=None): # Создаёт только параметры маршрута, оставляя COM и скорость основной форме.
|
||
super().__init__(parent); self.worker = None; self.metadata = {}; self.path = None # Состояние соответствует одному выбранному Flash BIN.
|
||
self._result = None # Конечный результат относится к одному активному сеансу и сбрасывается при следующем явном старте.
|
||
self._thread = SciWorker(b'', b'', '', 115200, parent=self) # Создаём Qt-метаобъект потока один раз при создании панели, а не после ручного №93 и удаления элементов GUI.
|
||
self._thread.progress.connect(self._relay_progress, Qt.QueuedConnection) # Доставка прогресса идёт в слот панели главного потока вместо прямого вызова Signal.emit из аппаратного потока.
|
||
self._thread.result.connect(self._capture_result, Qt.QueuedConnection) # Обработка результата не изменяет состояние QWidget из рабочего потока.
|
||
self._thread.finished.connect(self._released, Qt.QueuedConnection) # Освобождение активного сеанса начинается только после завершения run и закрытия собственного COM.
|
||
form = QFormLayout(self) # Контролы не требуют дополнительного окна приложения.
|
||
self.kernel = QLineEdit(); form.addRow("RAM-ядро TI", self.kernel) # Готовый комплект заполняет путь автоматически.
|
||
button = QPushButton("Выбрать ядро…"); button.clicked.connect(self._choose); form.addRow(button) # Старые образы допускают явный выбор соответствующего ядра.
|
||
self.enter_monitor = QCheckBox("Перевести остановленный UlitCo в загрузчик через Monitor №93"); form.addRow(self.enter_monitor) # Управляющая команда отправляется только при явном выбранном режиме.
|
||
self.oscillator_hz = QSpinBox(); self.oscillator_hz.setRange(20000000, 30000000); self.oscillator_hz.setSingleStep(5000000); self.oscillator_hz.setValue(30000000); self.oscillator_hz.setReadOnly(True); form.addRow("Кварц ядра, Гц", self.oscillator_hz) # Частота зафиксирована при сборке ядра и не меняется настройкой COM.
|
||
self.timeout_ms = QSpinBox(); self.timeout_ms.setRange(50, 10000); self.timeout_ms.setValue(3000); form.addRow("Ожидание связи, мс", self.timeout_ms) # Отдельный erase/program получает обязательный бюджет до 60 секунд.
|
||
label = QLabel("Штатный SCI Boot F28335: приложение UlitCo → RAM-ядро TI → Flash. Выберите порт SCI-A; при ручном входе предварительно включите загрузчик."); label.setWordWrap(True); form.addRow(label) # Оператор видит физический интерфейс и последовательность до начала записи.
|
||
|
||
def _choose(self): # Выбор RAM-ядра сам по себе ничего не отправляет в устройство.
|
||
path, _ = QFileDialog.getOpenFileName(self, "RAM-ядро F28335 SCI", self.kernel.text(), "SCI8 (*.bin *.txt)") # Принимает штатные бинарное и ASCII-представления TI.
|
||
if path: self.kernel.setText(path) # Отмена сохраняет текущий комплект.
|
||
|
||
def set_manifest(self, image, metadata): # Подтягивает ядро и параметры из выбранного постсборочного комплекта.
|
||
self.path, self.metadata = Path(image), metadata # Сохраняет принадлежность ядра конкретному Flash BIN.
|
||
info = metadata.get("flash_kernel", {}); info = info if isinstance(info, dict) else {} # Не относящийся к SCI manifest не должен нарушать визуальную загрузку других режимов.
|
||
name = info.get("file", "") # Manifest хранит только исходное имя соседнего RAM-бинарника.
|
||
if isinstance(name, str) and name and Path(name).name == name: self.kernel.setText(str(self.path.parent / name)) # Не разрешает metadata-путь за пределы папки образа.
|
||
else: self.kernel.clear() # Новый образ не должен наследовать RAM-ядро предыдущей прошивки.
|
||
frequency = info.get("oscillator_hz", 30000000); self.oscillator_hz.setValue(frequency if type(frequency) is int and frequency in (20000000, 25000000, 30000000) else 30000000) # Показывает допустимую частоту без исключения setter на чужих metadata.
|
||
|
||
def prepare(self, image): # Фиксирует и проверяет все байты до подтверждения аппаратной операции.
|
||
with Path(image).open("rb") as stream: data = stream.read(1024 * 1024 + 1) # Ограничивает чтение Flash BIN до одного МиБ и одного контрольного октета.
|
||
with Path(self.kernel.text()).open("rb") as stream: kernel_raw = stream.read(1024 * 1024 + 1) # Worker получает ограниченный неизменяемый снимок RAM-ядра.
|
||
if len(data) > 1024 * 1024 or len(kernel_raw) > 1024 * 1024: raise ValueError("Слишком большой файл SCI8") # Ограничивает отдельные Flash и RAM-образы одним МиБ.
|
||
if self.metadata and (len(data) != self.metadata.get("size_bytes") or hashlib.sha256(data).hexdigest() != self.metadata.get("sha256")): raise ValueError("Flash BIN изменён после выбора или не соответствует manifest") # Новый корректный SCI8 не должен незаметно заменить ранее подтверждённый образ.
|
||
info = self.metadata.get("flash_kernel", {}) # При наличии паспорта зависимость должна совпадать побайтно.
|
||
if not isinstance(info, dict): raise ValueError("В manifest требуется объект flash_kernel") # Неверный тип метаданных возвращается обычной ошибкой комплекта до доступа к COM.
|
||
if info and (len(kernel_raw) != info.get("size_bytes") or hashlib.sha256(kernel_raw).hexdigest() != info.get("sha256")): raise ValueError("RAM-ядро TI не соответствует manifest Flash BIN") # Чужая частота или маска erase не должны незаметно попасть в сеанс.
|
||
kernel, flash = decode(kernel_raw), decode(data) # Оба представления разбираются только общим C-декодером.
|
||
validate(kernel, False); _, sectors = validate(flash, True) # Не открывает COM для повреждённых таблиц или адресов CSM; маску вычисляет единственный C-декодер.
|
||
if info and (info.get("sector_mask") != sectors or info.get("oscillator_hz") != self.oscillator_hz.value()): raise ValueError("RAM-ядро собрано для другой частоты или набора секторов Flash") # Комплект обязан соответствовать фактическим адресам приложения и выбранному при сборке кварцу.
|
||
return kernel, flash # Возвращает готовые байты для неизменяемой аппаратной операции.
|
||
|
||
def start(self, job, port, baud): # Основная форма вызывает метод после штатного подтверждения пользователя.
|
||
if self.worker is not None: raise ValueError("Прошивка SCI уже выполняется") # Один виджет не может владеть двумя одновременными COM-сеансами.
|
||
self._thread.configure(*job, port, baud, self.enter_monitor.isChecked(), self.timeout_ms.value()) # Обновляем параметры уже существующего остановленного Qt-потока, не создавая новый метаобъект при каждой прошивке.
|
||
self.worker, self._result = self._thread, None # Публичная ссылка обозначает только активный сеанс и сохраняет существующие проверки закрытия главного окна.
|
||
self.worker.start() # Возобновляем готовый QThread; COM создаётся и закрывается только внутри его run.
|
||
|
||
@Slot(int, str) # Явная сигнатура позволяет Qt доставить прогресс именно в GUI-поток панели.
|
||
def _relay_progress(self, percent, text): # Принимает межпоточный сигнал до уведомления остальных элементов интерфейса.
|
||
self.progress.emit(percent, text) # Обновление полосы и терминала прошивки начинается из главного потока.
|
||
|
||
@Slot(bool, str) # Результат рабочего потока обрабатывается зарегистрированным слотом QWidget.
|
||
def _capture_result(self, success, text): # Промежуточный сигнал не означает, что QThread уже закончил закрытие порта.
|
||
self._result = (success, text) # Конечное уведомление будет отправлено после штатного QThread.finished.
|
||
|
||
@Slot() # Завершение потока доставляется главному потоку через явное queued-соединение.
|
||
def _released(self): # Сохраняет готовый Qt-объект для следующего сеанса вместо удаления и повторного создания.
|
||
self.worker = None # Следующая попытка разрешена только после полного завершения предыдущего run.
|
||
self.finished.emit(*(self._result or (False, 'Поток SCI завершился без результата'))) # Форма разблокируется после закрытия COM, а постоянный QThread остаётся дочерним объектом панели.
|
||
|
||
def cancel(self): # Отмена не вызывает terminate и не убивает чужие процессы.
|
||
if self.worker is not None: self.worker.cancel() # Рабочий поток завершит своё ближайшее конечное ожидание.
|