164 lines
5.5 KiB
Python
164 lines
5.5 KiB
Python
"""Unit tests for the ESP32 SD-over-SPI slave (esp32_sd_slave.SdSpiSlave).
|
|
|
|
Mirrors the browser part's tests (protocol-parts.test.ts) — same protocol that
|
|
is validated end-to-end against real Arduino SD.h firmware in
|
|
frontend/src/__tests__/microsd-real-firmware.test.ts. Confirms the Python port
|
|
is faithful: reply-first 1-byte latency, SDSC byte addressing, write capture.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_here = Path(__file__).resolve().parent.parent / "app" / "services" / "esp32_sd_slave.py"
|
|
_spec = importlib.util.spec_from_file_location("esp32_sd_slave", _here)
|
|
_mod = importlib.util.module_from_spec(_spec)
|
|
sys.modules["esp32_sd_slave"] = _mod
|
|
_spec.loader.exec_module(_mod)
|
|
SdSpiSlave = _mod.SdSpiSlave
|
|
|
|
|
|
def xfer(s, data):
|
|
return [s.transfer(b) for b in data]
|
|
|
|
|
|
def cmd(idx, arg=0):
|
|
return [0x40 | idx, (arg >> 24) & 0xFF, (arg >> 16) & 0xFF, (arg >> 8) & 0xFF, arg & 0xFF, 0x95]
|
|
|
|
|
|
def FF(n):
|
|
return [0xFF] * n
|
|
|
|
|
|
def at(block):
|
|
return block * 512
|
|
|
|
|
|
def test_init_handshake():
|
|
s = SdSpiSlave()
|
|
# 1-byte Ncr latency: R1 on the first 0xFF clock after the 6 command bytes.
|
|
r = xfer(s, cmd(0) + FF(1))
|
|
assert r[6] == 0x01 # idle
|
|
r = xfer(s, cmd(8, 0x1AA) + FF(5))
|
|
assert r[6:11] == [0x01, 0x00, 0x00, 0x01, 0xAA] # R7
|
|
r = xfer(s, cmd(55) + FF(1))
|
|
assert r[6] == 0x01
|
|
r = xfer(s, cmd(41) + FF(1))
|
|
assert r[6] == 0x00 # ACMD41 ready
|
|
r = xfer(s, cmd(58) + FF(5))
|
|
assert r[6:11] == [0x00, 0x80, 0xFF, 0x80, 0x00] # OCR, SDSC (CCS=0)
|
|
|
|
|
|
def test_write_then_read_roundtrip():
|
|
s = SdSpiSlave()
|
|
data = [(i * 7 + 3) & 0xFF for i in range(512)]
|
|
xfer(s, cmd(24, at(5)))
|
|
r = xfer(s, [0xFF, 0xFE] + data + [0xFF, 0xFF, 0xFF])
|
|
assert 0x05 in r # data accepted
|
|
r = xfer(s, cmd(17, at(5)) + FF(520))
|
|
t = r.index(0xFE)
|
|
assert r[t + 1 : t + 1 + 512] == data
|
|
|
|
|
|
def test_unwritten_block_is_zeros():
|
|
s = SdSpiSlave()
|
|
r = xfer(s, cmd(17, at(123)) + FF(520))
|
|
t = r.index(0xFE)
|
|
assert r[t + 1 : t + 1 + 512] == [0] * 512
|
|
|
|
|
|
def test_byte_addressing_translates_to_block():
|
|
# Two different byte offsets that map to distinct blocks must not collide.
|
|
s = SdSpiSlave()
|
|
a = [0xAA] * 512
|
|
b = [0xBB] * 512
|
|
xfer(s, cmd(24, at(2)))
|
|
xfer(s, [0xFF, 0xFE] + a + [0xFF, 0xFF, 0xFF])
|
|
xfer(s, cmd(24, at(3)))
|
|
xfer(s, [0xFF, 0xFE] + b + [0xFF, 0xFF, 0xFF])
|
|
r2 = xfer(s, cmd(17, at(2)) + FF(520))
|
|
r3 = xfer(s, cmd(17, at(3)) + FF(520))
|
|
assert r2[r2.index(0xFE) + 1 : r2.index(0xFE) + 513] == a
|
|
assert r3[r3.index(0xFE) + 1 : r3.index(0xFE) + 513] == b
|
|
|
|
|
|
def test_multi_block_write():
|
|
s = SdSpiSlave()
|
|
a = [(i + 1) & 0xFF for i in range(512)]
|
|
b = [(i + 2) & 0xFF for i in range(512)]
|
|
xfer(s, cmd(25, at(10)))
|
|
xfer(s, [0xFC] + a + [0xFF, 0xFF])
|
|
xfer(s, [0xFC] + b + [0xFF, 0xFF])
|
|
xfer(s, [0xFD]) # stop token
|
|
r10 = xfer(s, cmd(17, at(10)) + FF(520))
|
|
r11 = xfer(s, cmd(17, at(11)) + FF(520))
|
|
assert r10[r10.index(0xFE) + 1 : r10.index(0xFE) + 513] == a
|
|
assert r11[r11.index(0xFE) + 1 : r11.index(0xFE) + 513] == b
|
|
|
|
|
|
def test_csd_reflects_capacity():
|
|
s = SdSpiSlave()
|
|
r = xfer(s, cmd(9) + FF(20))
|
|
t = r.index(0xFE)
|
|
csd = r[t + 1 : t + 1 + 16]
|
|
assert len(csd) == 16
|
|
assert csd[0] & 0xC0 == 0x40 # CSD structure v2
|
|
|
|
|
|
def _crc16(data):
|
|
crc = 0
|
|
for b in data:
|
|
crc ^= b << 8
|
|
for _ in range(8):
|
|
crc = ((crc << 1) ^ 0x1021) if (crc & 0x8000) else (crc << 1)
|
|
crc &= 0xFFFF
|
|
return crc
|
|
|
|
|
|
def test_espidf_init_sequence_idle_bit():
|
|
# Mirrors what ESP-IDF sdspi sends: CMD0, CMD59 (CRC on), CMD8, ACMD41,
|
|
# CMD58. Every R1 before ACMD41 must carry the idle bit (0x01); the
|
|
# hardcoded-0x00 bug stalled real ESP-IDF init right after CMD59.
|
|
s = SdSpiSlave()
|
|
assert xfer(s, cmd(0) + FF(1))[6] == 0x01 # idle
|
|
assert xfer(s, [0x40 | 59, 0, 0, 0, 1, 0x83] + FF(1))[6] == 0x01 # CMD59 still idle
|
|
assert xfer(s, cmd(8, 0x1AA) + FF(5))[6] == 0x01 # CMD8 R7, idle bit set
|
|
assert xfer(s, cmd(55) + FF(1))[6] == 0x01
|
|
assert xfer(s, cmd(41) + FF(1))[6] == 0x00 # ACMD41 -> ready, leaves idle
|
|
assert xfer(s, cmd(58) + FF(5))[6] == 0x00 # CMD58 R1 now 0x00
|
|
|
|
|
|
def test_data_block_crc16_valid_when_enabled():
|
|
# With CRC enabled (CMD59 arg=1), the 2 bytes trailing a read block must be
|
|
# the real CRC16 of the block — ESP-IDF rejects the read otherwise.
|
|
s = SdSpiSlave()
|
|
payload = [(i * 13 + 7) & 0xFF for i in range(512)]
|
|
xfer(s, cmd(24, at(8)))
|
|
xfer(s, [0xFF, 0xFE] + payload + [0xFF, 0xFF, 0xFF])
|
|
xfer(s, [0x40 | 59, 0, 0, 0, 1, 0x83] + FF(1)) # enable CRC
|
|
r = xfer(s, cmd(17, at(8)) + FF(520))
|
|
t = r.index(0xFE)
|
|
block = r[t + 1 : t + 1 + 512]
|
|
crc = (r[t + 513] << 8) | r[t + 514]
|
|
assert block == payload
|
|
assert crc == _crc16(bytes(block))
|
|
|
|
|
|
def test_data_block_crc_is_ff_when_disabled():
|
|
# Default (no CMD59): CRC bytes are 0xFFFF — Arduino AVR SD.h ignores them.
|
|
s = SdSpiSlave()
|
|
r = xfer(s, cmd(17, at(0)) + FF(520))
|
|
t = r.index(0xFE)
|
|
assert r[t + 513] == 0xFF and r[t + 514] == 0xFF
|
|
|
|
|
|
def test_loads_a_prebuilt_image():
|
|
block0 = bytes((i ^ 0x5A) & 0xFF for i in range(512))
|
|
block1 = bytes((i + 200) & 0xFF for i in range(512))
|
|
s = SdSpiSlave(block0 + block1)
|
|
r0 = xfer(s, cmd(17, at(0)) + FF(520))
|
|
r1 = xfer(s, cmd(17, at(1)) + FF(520))
|
|
assert bytes(r0[r0.index(0xFE) + 1 : r0.index(0xFE) + 513]) == block0
|
|
assert bytes(r1[r1.index(0xFE) + 1 : r1.index(0xFE) + 513]) == block1
|