120 lines
4.7 KiB
Python
120 lines
4.7 KiB
Python
# Планирование дампа внешней памяти TMS и сборка ответов CMD_UPLOAD. Адреса XINTF считаются
|
||
# 16-битными словами, размер передачи — байтами; прогресс и смещение рассчитываются с явным
|
||
# переводом единиц.
|
||
|
||
"""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
|