Добавить прошивку F28335 через SCI и подготовку Python
Подключить сборку Flash BIN и RAM-ядра TI через templates. Передавать настройки CAN и RS из JSON и проверять окружение CCS/Keil. Добавить общий C99-протокол и проверки обмена SCI.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Встроенная прошивка 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 # Рабочий поток не блокирует 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 # Сигналы безопасно возвращают результаты в главный поток.
|
||||
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.enter, self.timeout_ms, self.stop = enter, timeout_ms, threading.Event() # Отмена проверяется во всех конечных ожиданиях.
|
||||
|
||||
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() # Не добавляет новые данные после запроса остановки.
|
||||
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("Загрузчик F28335 не подтвердил обмен SCI") # Отсутствие эха или успешного Flash API не трактуется как продолжение.
|
||||
return bytes(result) # Проверку эха и checksum выполнит C-ядро.
|
||||
|
||||
def run(self): # Qt вызывает метод в отдельном потоке, где создаётся и закрывается COM-порт.
|
||||
serial = None # Ошибка проверки образа не должна даже открывать аппаратный интерфейс.
|
||||
try: # Все исключения превращаются в конечный результат GUI.
|
||||
session = Session(self.kernel, self.image) # Проверяет RAM-ядро и Flash-таблицу до любой управляющей команды.
|
||||
serial = QSerialPort(); serial.setPortName(self.port); serial.setBaudRate(self.baud) # Использует точный COM-порт и скорость из формы/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(serial.errorString()) # Занятый терминалом порт не открывается вторым владельцем.
|
||||
serial.clear() # Старые данные терминала не должны стать ложным эхо autobaud.
|
||||
if self.enter: # Приложение UlitCo принимает переход в загрузчик лишь в разрешённом остановленном состоянии.
|
||||
self.progress.emit(0, "Переход UlitCo в первичный загрузчик…") # Показывает отдельный этап до передачи RAM-ядра.
|
||||
self._write(serial, ulitco_enter()); self._pause(500); serial.clear() # Штатная команда Monitor №93 не подменяется BIOS ПМ35.
|
||||
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.
|
||||
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 в миллисекундах.
|
||||
self.progress.emit(session.progress, "Ядро TI / запись Flash…") # Продвижение следует только успешно проверенным ответам.
|
||||
self.result.emit(True, "Передача завершена; ядро TI подтвердило порции Flash. Проверка чтением памяти отдельно не выполнялась.") # Не заявляет недоступную в Kernel A полную readback-проверку.
|
||||
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.
|
||||
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.worker = SciWorker(*job, port, baud, self.enter_monitor.isChecked(), self.timeout_ms.value(), self) # Worker получает только снимок параметров главного потока.
|
||||
self._result = None; self.worker.progress.connect(self.progress.emit); self.worker.result.connect(self._capture_result); self.worker.finished.connect(self._released) # Разблокирует форму только после finally рабочего потока и фактического закрытия COM.
|
||||
self.worker.start() # До этого места никакие байты MCU не передавались.
|
||||
|
||||
def _capture_result(self, success, text): # Промежуточный сигнал не означает, что QThread уже закончил закрытие порта.
|
||||
self._result = (success, text) # Конечное уведомление будет отправлено после штатного QThread.finished.
|
||||
|
||||
def _released(self): # Ссылка на поток сохраняется до завершения его finally и закрытия COM.
|
||||
worker, self.worker = self.worker, None # После окончания разрешает следующую явную прошивку.
|
||||
if worker is not None: worker.deleteLater() # Освобождает Qt-объект в главном потоке без уничтожения работающего QThread.
|
||||
self.finished.emit(*(self._result or (False, "Поток SCI завершился без результата"))) # Форма снова доступна, когда собственный COM-сеанс гарантированно закрыт.
|
||||
|
||||
def cancel(self): # Отмена не вызывает terminate и не убивает чужие процессы.
|
||||
if self.worker is not None: self.worker.cancel() # Рабочий поток завершит своё ближайшее конечное ожидание.
|
||||
Reference in New Issue
Block a user