Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
115
python/set_devices/tms_flash.py
Normal file
115
python/set_devices/tms_flash.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Legacy BALZAM/TMS parallel-memory dump protocol.
|
||||
|
||||
The TMS320F2812 addresses external XINTF memory in 16-bit words, while
|
||||
``CMD_UPLOAD`` transfers a byte count. This module keeps those units explicit
|
||||
so a complete dump cannot silently become half or twice the requested size.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from protocan import tms2812 as shared_tms2812
|
||||
|
||||
CMD_UPLOAD = 52
|
||||
DEFAULT_CONTROLLER_ADDRESS = 5
|
||||
PARALLEL_NVM_START_WORD = 0x100000
|
||||
PARALLEL_NVM_WORDS = 0x80000
|
||||
PARALLEL_NVM_BYTES = PARALLEL_NVM_WORDS * 2
|
||||
MT29F1G08_WORDS = 0x4000000
|
||||
MT29F1G08_BYTES = MT29F1G08_WORDS * 2
|
||||
DEFAULT_CHUNK_BYTES = 4096
|
||||
NAND_CHUNK_BYTES = 2048
|
||||
UPLOAD_TRAILER_BYTES = 6 # CRC16 + four legacy padding bytes
|
||||
|
||||
|
||||
class TmsFlashError(ValueError):
|
||||
"""Malformed command, reply, or dump range."""
|
||||
|
||||
|
||||
def build_upload_request(address: int, word_address: int, byte_count: int) -> bytes:
|
||||
"""Build the 12-byte legacy ``CMD_UPLOAD`` request."""
|
||||
if not 0 <= address <= 0xFF:
|
||||
raise TmsFlashError("адрес контроллера вне диапазона 0..255")
|
||||
if not 0 <= word_address <= 0xFFFFFFFF:
|
||||
raise TmsFlashError("адрес слова вне диапазона uint32")
|
||||
if not 1 <= byte_count <= 0xFFFFFFFF:
|
||||
raise TmsFlashError("размер блока вне диапазона 1..0xFFFFFFFF")
|
||||
try:
|
||||
return shared_tms2812.build_upload_request(
|
||||
address, word_address, byte_count
|
||||
)
|
||||
except ValueError as error:
|
||||
raise TmsFlashError(str(error)) from error
|
||||
|
||||
|
||||
def decode_upload_reply(raw: bytes, address: int, byte_count: int) -> bytes:
|
||||
"""Validate one complete upload reply and return its memory bytes."""
|
||||
expected = 2 + byte_count + UPLOAD_TRAILER_BYTES
|
||||
if len(raw) != expected:
|
||||
raise TmsFlashError(
|
||||
f"ответ CMD_UPLOAD: ожидалось {expected} байт, получено {len(raw)}"
|
||||
)
|
||||
try:
|
||||
return shared_tms2812.decode_upload_reply(raw, address, byte_count)
|
||||
except ValueError as error:
|
||||
raise TmsFlashError(str(error)) from error
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DumpChunk:
|
||||
"""One request in a word-addressed, byte-transferred dump."""
|
||||
|
||||
word_address: int
|
||||
byte_count: int
|
||||
|
||||
|
||||
class FlashDumpPlan:
|
||||
"""Deterministic chunk plan used by the GUI and host tests."""
|
||||
|
||||
def __init__(self, start_word: int, word_count: int, chunk_bytes: int) -> None:
|
||||
if start_word < 0 or start_word > 0xFFFFFFFF:
|
||||
raise TmsFlashError("начальный адрес слова вне диапазона uint32")
|
||||
if word_count <= 0:
|
||||
raise TmsFlashError("число слов должно быть положительным")
|
||||
if start_word + word_count > 0x100000000:
|
||||
raise TmsFlashError("конец диапазона выходит за uint32")
|
||||
if chunk_bytes < 2 or chunk_bytes > 0xFFFF or chunk_bytes % 2:
|
||||
raise TmsFlashError("блок должен быть чётным числом байт 2..65534")
|
||||
self.start_word = start_word
|
||||
self.word_count = word_count
|
||||
self.chunk_bytes = chunk_bytes
|
||||
self.total_bytes = word_count * 2
|
||||
|
||||
def chunks(self) -> list[DumpChunk]:
|
||||
result = []
|
||||
offset = 0
|
||||
while offset < self.total_bytes:
|
||||
size = min(self.chunk_bytes, self.total_bytes - offset)
|
||||
result.append(DumpChunk(self.start_word + offset // 2, size))
|
||||
offset += size
|
||||
return result
|
||||
|
||||
|
||||
class UploadReplyBuffer:
|
||||
"""Collect arbitrary serial fragments for exactly one upload reply."""
|
||||
|
||||
def __init__(self, address: int, byte_count: int) -> None:
|
||||
self.address = address
|
||||
self.byte_count = byte_count
|
||||
self.expected_size = 2 + byte_count + UPLOAD_TRAILER_BYTES
|
||||
self._buffer = bytearray()
|
||||
|
||||
@property
|
||||
def received_size(self) -> int:
|
||||
return len(self._buffer)
|
||||
|
||||
def feed(self, data: bytes) -> tuple[bytes | None, bytes]:
|
||||
"""Return ``(payload, trailing_data)`` when the reply is complete."""
|
||||
self._buffer.extend(data)
|
||||
if len(self._buffer) < self.expected_size:
|
||||
return None, b""
|
||||
raw = bytes(self._buffer[: self.expected_size])
|
||||
trailing = bytes(self._buffer[self.expected_size :])
|
||||
self._buffer.clear()
|
||||
return decode_upload_reply(raw, self.address, self.byte_count), trailing
|
||||
Reference in New Issue
Block a user