"""Встроенная прошивка 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() # Рабочий поток завершит своё ближайшее конечное ожидание.