Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt. Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor. Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов. Обновить справочники устройств, проекты CCS и регрессионные проверки.
257 lines
18 KiB
Python
257 lines
18 KiB
Python
# Клиент A/B-загрузчика ProtoCAN с оконной передачей блоков по восемь байт. Ответы принимаются
|
|
# только для целевого адреса и текущей сессии; продвижение по этапам зависит от подтверждения,
|
|
# а не от факта отправки.
|
|
|
|
"""Legacy ProtoCAN Boot client ported from Gui_Android CanFirmwareProtocol.kt."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import struct
|
|
|
|
from .protocan import ProtoCanId
|
|
from .transport import build_frame
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CanBootTarget:
|
|
device_type: int = 0
|
|
device: int = 13
|
|
product_type: int = 0x166
|
|
hardware_min: int = 0
|
|
hardware_max: int = 255
|
|
session_id: int = 1
|
|
memory: str = "eeprom" # Назначение выбирается явно; старые вызовы сохраняют EEPROM A/B и прежний wire-протокол.
|
|
|
|
def __post_init__(self):
|
|
if not 0 <= self.device_type <= 7 or not 0 <= self.device <= 15:
|
|
raise ValueError("Адрес ProtoCAN: Device Type 0…7, Device 0…15")
|
|
if not 0 <= self.product_type <= 65535:
|
|
raise ValueError("Product Type вне диапазона u16")
|
|
if not 0 <= self.hardware_min <= self.hardware_max <= 255:
|
|
raise ValueError("Неверный диапазон аппаратных ревизий")
|
|
if not 1 <= self.session_id <= 255:
|
|
raise ValueError("Session ID должен быть 1…255")
|
|
if self.memory not in ("eeprom", "flash", "stm32") or self.memory == "flash" and self.product_type != 0x2833: # F28335 требует отдельного подтверждения выбора Flash; STM32 использует штатный односекционный поток.
|
|
raise ValueError("Внутренняя Flash по CAN доступна только для F28335 / Product Type 0x2833") # Ошибка назначения обнаруживается до первого CAN-кадра.
|
|
if self.memory == "stm32" and self.product_type != 0: # BOOT CANdle G474VET6 публикует Product Type 0 в своём аппаратном профиле.
|
|
raise ValueError("CANdle G474VET6 требует Product Type 0") # Не допускает случайную передачу образа с ранее выбранным типом платы Бальзам.
|
|
|
|
|
|
class CanBootTransfer:
|
|
"""Windowed 8-byte blocks; control 0x9, slots 0xA/0xB, status 0xC."""
|
|
def __init__(self, image, target: CanBootTarget):
|
|
if not 0 < len(image.data) <= 512 * 1024:
|
|
raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ")
|
|
if target.product_type == 0x2833 and len(image.data) > 0x1FE00:
|
|
raise ValueError("F28335 M95M02 A/B: образ не должен превышать 130560 байт")
|
|
if not 0 <= image.version <= 0xFFFFFFFF:
|
|
raise ValueError("Версия образа вне диапазона u32")
|
|
if target.memory == "stm32": # Односекционная Flash G474 проверяется до первой команды ENTER_BOOT.
|
|
from set_devices.mcu_profiles import CANDLE_G474_BOOT # Размер приложения и адрес векторов берутся из общего профиля платы.
|
|
profile = CANDLE_G474_BOOT # Границы соответствуют 32 КиБ BOOT и 480 КиБ приложения STM32G474VET6.
|
|
if not 8 <= len(image.data) <= profile["app_size"]: # Первые восемь байтов содержат SP и Reset_Handler; запись не должна выходить за Flash.
|
|
raise ValueError("CANdle G474VET6: приложение должно занимать 8…491520 байт") # Ошибка обнаруживается до стирания целевого приложения.
|
|
sp, pc = struct.unpack_from("<II", image.data) # Таблица векторов ARM Cortex-M хранит два 32-битных little-endian адреса.
|
|
if sp & 7 or not profile["stack_base"] < sp <= profile["stack_end"]: # MSP выровнен на восемь байтов и входит в те же 80 КиБ SRAM, которые разрешает валидатор штатного BOOT CANdle.
|
|
raise ValueError("CANdle G474VET6: неверный начальный указатель стека") # Случайный BIN не рассматривается как приложение ARM.
|
|
if not pc & 1 or not profile["app_base"] <= (pc & ~1) < profile["app_base"] + len(image.data): # Thumb-вектор должен указывать внутрь приложения, слинкованного для 0x08008000.
|
|
raise ValueError("CANdle G474VET6: нужен образ приложения для 0x08008000") # BOOT-образ 0x08000000 и приложение USB BOOT RB не принимаются.
|
|
if target.memory == "flash": # Flash принимает тот же подписанный SCI8, что существующий RS Flash, с защищённым сектором A.
|
|
from .f28335_flash import parse_sci8 # Повторно использует общий валидатор FAB1, адресов и CRC без отдельной реализации в клиенте.
|
|
parse_sci8(image.data) # Обычный RAM BIN и образ с аппаратным boot отвергаются до входа в загрузчик.
|
|
self.image, self.target = image, target
|
|
self.stage = "enter"
|
|
self.next_block = 0
|
|
self.slot = None
|
|
self.finished = False
|
|
self._window_end = 0
|
|
|
|
@property
|
|
def response_timeout_ms(self):
|
|
if self.target.memory == "flash" and self.stage == "commit": # TI erase/program/verify выполняются синхронно после полной проверки staging.
|
|
return 180000 # Даёт до 180 секунд на штатную запись и итоговую CRC, как действующий RS Flash-клиент.
|
|
return 15000 if self.stage in ("enter", "verify", "commit") else 1500
|
|
|
|
@property
|
|
def percent(self):
|
|
return min(100, self.next_block * 8 * 100 // len(self.image.data))
|
|
|
|
def _frame(self, kind, body, data=b""):
|
|
t = self.target
|
|
return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, kind, body), data, to_can=True)
|
|
|
|
def _control(self, command, data=b""):
|
|
return [self._frame(9, self.target.session_id << 8 | command, data)]
|
|
|
|
def start(self):
|
|
return self._control(2)
|
|
|
|
def abort(self):
|
|
return self._control(10)
|
|
|
|
def accepts(self, frame):
|
|
p = ProtoCanId.parse(frame.can_id)
|
|
t = self.target
|
|
return (not frame.to_can and frame.ide and not frame.rtr and not frame.is_error
|
|
and p.msg_type == 12 and p.pm == 1 and p.device_type == t.device_type
|
|
and p.device == t.device and p.body >> 8 == t.session_id and len(frame.data) == 8)
|
|
|
|
# Окно содержит до 16 блоков. Последний неполный блок заполняется 0xFF,
|
|
# но метаданные образа сохраняют исходную длину без этого дополнения.
|
|
def _window(self):
|
|
total = (len(self.image.data) + 7) // 8
|
|
self._window_end = min(total, (self.next_block // 16 + 1) * 16) # Ядро подтверждает абсолютные границы по 16 блоков; после потери повторяется только хвост текущего окна, чтобы его ACK не застал в очереди кадры следующего окна.
|
|
return [self._frame(10 + self.slot, i, self.image.data[i * 8:(i + 1) * 8].ljust(8, b"\xff"))
|
|
for i in range(self.next_block, self._window_end)]
|
|
|
|
def handle_status(self, frame):
|
|
if not self.accepts(frame):
|
|
return [], ""
|
|
command = ProtoCanId.parse(frame.can_id).body & 255
|
|
expected_command = {"enter": 2, "memory": 16, "image": 3, "compat": 4, "erase": 5, # Ответ выбора назначения принимается только на собственном этапе.
|
|
"data": 0, "verify": 6, "commit": 7, "reboot": 9}.get(self.stage)
|
|
# Позднее подтверждение предыдущего этапа нельзя применять к новому:
|
|
# оно не продвигает автомат и не считается ошибкой текущего запроса.
|
|
if command != expected_command:
|
|
return [], ""
|
|
status, slot, expected = struct.unpack_from("<BBH", frame.data)
|
|
if status == 1:
|
|
return [], "Загрузчик занят"
|
|
if status not in (0, 8) or (status == 8 and self.stage != "data"):
|
|
raise RuntimeError(f"ProtoCAN Boot: ошибка 0x{status:02X}")
|
|
if self.stage == "enter" and self.target.memory == "flash": # Старый загрузчик должен явно подтвердить поддержку новой цели до передачи образа.
|
|
self.stage = "memory" # Сначала требует ACK SELECT_MEMORY, затем начнёт обычные метаданные.
|
|
return self._control(16, b"\x01"), "Выбор внутренней Flash F28335" # DLC=1 и значение 1 означают внутреннюю Flash; значение 0 зарезервировано за штатной EEPROM.
|
|
if self.stage in ("enter", "memory"): # EEPROM сохраняет прежний переход, Flash переходит только после подтверждённого выбора памяти.
|
|
self.stage = "image"
|
|
return self._control(3, struct.pack("<II", len(self.image.data), self.image.crc32)), "Передача метаданных"
|
|
if self.stage == "image":
|
|
self.stage = "compat"
|
|
t = self.target
|
|
return self._control(4, struct.pack("<HBBI", t.product_type, t.hardware_min, t.hardware_max, self.image.version)), "Проверка совместимости"
|
|
if self.stage == "compat":
|
|
if slot not in (0, 1):
|
|
raise RuntimeError("ProtoCAN Boot: неверный слот")
|
|
self.slot, self.stage = slot, "erase"
|
|
return self._control(5), ("Подготовка временного слота" if self.target.memory == "flash" else "Стирание приложения STM32" if self.target.memory == "stm32" else "Стирание неактивного слота") # F28335 готовит staging, STM32 стирает единственный слот, EEPROM сохраняет штатное поведение A/B.
|
|
if self.stage in ("erase", "data"):
|
|
total = (len(self.image.data) + 7) // 8
|
|
# The final index of a 512 KiB image wraps the u16 status field.
|
|
if self.stage == "data" and expected == 0 and self._window_end == 65536 and status == 0:
|
|
expected = 65536
|
|
limit = total if self.stage == "erase" else self._window_end
|
|
if expected > limit:
|
|
raise RuntimeError("ProtoCAN Boot: подтверждён непереданный блок")
|
|
# Один и тот же ACK или SEQUENCE_ERROR может прийти от нескольких оставшихся кадров старого окна.
|
|
# Повторная отправка на каждый такой ответ переполняет очередь MCU; при отсутствии продвижения
|
|
# последнее окно повторяет транспорт по тайм-ауту, а подтверждённые ранее блоки не отправляются заново.
|
|
if self.stage == "data" and expected <= self.next_block: # Сравнивает next_block для обоих статусов: OK и SEQUENCE_ERROR не должны повторно открывать одно окно.
|
|
return [], "" # Оставляет текущие кадры и срок ожидания транспорта без изменений до нового прогресса или тайм-аута.
|
|
self.next_block = expected
|
|
if expected == total:
|
|
self.stage = "verify"
|
|
return self._control(6), "Проверка CRC32"
|
|
self.stage = "data"
|
|
return self._window(), "Передача блоков ProtoCAN Boot"
|
|
if self.stage == "verify":
|
|
self.stage = "commit"
|
|
return self._control(7), "Запись и проверка внутренней Flash" if self.target.memory == "flash" else "Активация образа" # Для Flash COMMIT означает реальный вызов TI API и публикацию FAB1 последней стадией.
|
|
if self.stage == "commit":
|
|
self.stage = "reboot"
|
|
return self._control(9), "Перезапуск прибора"
|
|
self.stage, self.finished = "finished", True
|
|
if self.target.memory == "stm32": # У односекционного STM32 нет EEPROM-кандидата и механизма пробного запуска A/B.
|
|
return [], "Flash STM32 записана и проверена; прибор перезапускается" # Успех сообщается после ACK VERIFY, COMMIT и REBOOT обычного протокола.
|
|
if self.target.memory == "flash": # Flash не выдаётся за EEPROM-кандидата, которому требуется trial/CONFIRM.
|
|
return [], "Внутренняя Flash записана и проверена; прибор перезапускается" # Успех возможен только после итогового ACK COMMIT и ACK REBOOT.
|
|
if self.target.product_type == 0x2833:
|
|
return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск"
|
|
return [], "Прошивка ProtoCAN Boot завершена"
|
|
|
|
|
|
class CanBootServices:
|
|
"""Read-only passport queries and explicit session service commands.
|
|
|
|
Replies use message D, body=(page<<8)|command. IDENTIFY/discovery reply body=1.
|
|
All multibyte values are little endian; version is major:8/minor:8/patch:16.
|
|
"""
|
|
def __init__(self, target: CanBootTarget):
|
|
self.target = target
|
|
|
|
def request(self, command: int, page: int = 0):
|
|
if command not in (1, 8, 11, 12, 13, 14, 15):
|
|
raise ValueError("Unsupported service command")
|
|
if not 0 <= page <= 255 or (page and command not in (14, 15)):
|
|
raise ValueError("Invalid service page")
|
|
t = self.target
|
|
payload = bytes([page]) if command in (14, 15) else b""
|
|
return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, 9,
|
|
t.session_id << 8 | command), payload, to_can=True)
|
|
|
|
@staticmethod
|
|
def discover():
|
|
return build_frame(ProtoCanId.build(1, 0, 7, 15, 13, 0), b"", to_can=True)
|
|
|
|
def decode(self, frame, *, discovery=False):
|
|
p = ProtoCanId.parse(frame.can_id)
|
|
if (frame.to_can or not frame.ide or frame.rtr or frame.is_error or p.pm != 1
|
|
or p.msg_type not in (12, 13) or len(frame.data) != 8):
|
|
return None
|
|
if discovery:
|
|
if p.msg_type != 13 or p.body != 1:
|
|
return None
|
|
elif (p.device_type, p.device) != (self.target.device_type, self.target.device):
|
|
return None
|
|
if p.msg_type == 12:
|
|
if p.body >> 8 != self.target.session_id:
|
|
return None
|
|
status, slot, next_block, crc32 = struct.unpack("<BBHI", frame.data)
|
|
return {"kind": "status", "device_type": p.device_type, "device": p.device,
|
|
"command": p.body & 255, "status": status, "target_slot": slot,
|
|
"next_block": next_block, "crc32": crc32}
|
|
command, page = p.body & 255, p.body >> 8
|
|
d = frame.data
|
|
result = {"kind": "passport", "device_type": p.device_type, "device": p.device, "command": command, "page": page}
|
|
if command == 1 and page == 0:
|
|
product, hardware, protocol, version = struct.unpack("<HBBI", d)
|
|
result.update(product_type=product, hardware_revision=hardware, protocol_version=protocol,
|
|
firmware_version=version, version=(version >> 24, (version >> 16) & 255, version & 65535))
|
|
elif command == 12 and page == 0:
|
|
result.update(protocol_version=d[0], state=d[1], active_slot=d[2], target_slot=d[3],
|
|
session_id=d[4], slot_count=d[5], last_status=d[6])
|
|
elif command == 13 and page == 0:
|
|
flags, capacity = struct.unpack("<II", d)
|
|
result.update(capabilities=flags, slot_capacity=capacity)
|
|
elif command == 14:
|
|
result["name_fragment"] = d.split(b"\0", 1)[0].decode("ascii", errors="replace")
|
|
elif command == 15:
|
|
if page == 0:
|
|
size, page_size, mode, running = struct.unpack("<IHBB", d)
|
|
result.update(memory_bytes=size, page_bytes=page_size, bootloader=bool(mode), running_slot=running)
|
|
elif page == 1:
|
|
result["slot_offsets"] = struct.unpack("<II", d)
|
|
elif page == 2:
|
|
active, pending, attempted, healthy, timeout = struct.unpack("<BBBBI", d)
|
|
result.update(active_slot=active, pending_slot=pending, trial_attempted=bool(attempted),
|
|
storage_healthy=bool(healthy), session_timeout_ms=timeout)
|
|
elif page == 3:
|
|
result["parameters_offset"], result["parameters_bytes"] = struct.unpack("<II", d)
|
|
else:
|
|
return None
|
|
else:
|
|
return None
|
|
return result
|
|
|
|
|
|
class CanBootPingExchange:
|
|
"""PING adapter with the same request/feed shape as the SETProtocol client."""
|
|
def __init__(self, target: CanBootTarget):
|
|
self.services = CanBootServices(target)
|
|
|
|
def request(self):
|
|
return [self.services.request(12)]
|
|
|
|
def feed(self, frame):
|
|
reply = self.services.decode(frame)
|
|
return reply if reply and reply["kind"] == "passport" and reply["command"] == 12 else None
|