154 lines
5.7 KiB
Python
154 lines
5.7 KiB
Python
"""Run assembled driver routines with an in-memory block device.
|
|
|
|
Requires Python package z80 and a fresh body build with --sym=body_syms_new.txt.
|
|
Run: python tests/test_fat_offsets.py
|
|
SPI/DMA sector I/O is intercepted; FAT traversal and LOAD512 execute as Z80 code.
|
|
"""
|
|
from pathlib import Path
|
|
import re
|
|
import z80
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SYMS = dict((name, int(value, 16)) for name, value in re.findall(
|
|
r"^(\w+): EQU 0x([0-9A-Fa-f]+)$",
|
|
(ROOT / 'body_syms_new.txt').read_text(), re.M))
|
|
BODY = (ROOT / 'zc_sd_driver_body.bin').read_bytes()
|
|
|
|
|
|
class Driver:
|
|
def __init__(self):
|
|
self.m = z80.Z80Machine()
|
|
self.m.set_memory_block(0, BODY)
|
|
self.disk = {}
|
|
self.ram = bytearray(256 * 16384)
|
|
self.hooks = {SYMS[n]: n for n in
|
|
('sdread_lba', 'sdread_lba_to', 'sdwrite_lba', 'write_fsinfo_hint')}
|
|
for address in [0xff00, *self.hooks]:
|
|
self.m.set_breakpoint(address)
|
|
self.put('reserved_sectors', 32)
|
|
self.put('fatsz32_lo', 64)
|
|
self.put('addtop_lo', 17)
|
|
self.put('data_start_lo', 1000)
|
|
self.put('sectors_per_cluster', 2, 1)
|
|
self.put('num_fats', 2, 1)
|
|
|
|
def put(self, name, value, size=2):
|
|
self.m.set_memory_block(SYMS[name], value.to_bytes(size, 'little'))
|
|
|
|
def get(self, name, size=2):
|
|
return int.from_bytes(self.m.memory[SYMS[name]:SYMS[name]+size], 'little')
|
|
|
|
def cluster(self, value):
|
|
self.put('cur_cluster_lo', value & 65535)
|
|
self.put('cur_cluster_hi', value >> 16)
|
|
|
|
def fat(self, cluster, value):
|
|
sector, offset = divmod(cluster * 4, 512)
|
|
block = self.disk.setdefault(49 + sector, bytearray(512))
|
|
block[offset:offset+4] = value.to_bytes(4, 'little')
|
|
|
|
def call(self, name):
|
|
self.m.sp = 0xfef0
|
|
self.m.set_memory_block(self.m.sp, b'\x00\xff')
|
|
self.m.pc = SYMS[name]
|
|
for _ in range(10000):
|
|
self.m.ticks_to_stop = 100000
|
|
self.m.run()
|
|
if self.m.pc == 0xff00:
|
|
return self.m.a
|
|
hook = self.hooks.get(self.m.pc)
|
|
if not hook:
|
|
continue
|
|
if hook != 'write_fsinfo_hint':
|
|
lba = int.from_bytes(self.m.memory[SYMS['lba_arg']:SYMS['lba_arg']+4], 'big')
|
|
if hook == 'sdwrite_lba':
|
|
p = SYMS['sdbuf']
|
|
self.disk[lba] = bytearray(self.m.memory[p:p+512])
|
|
elif hook == 'sdread_lba':
|
|
self.m.set_memory_block(SYMS['sdbuf'], self.disk[lba])
|
|
# Real sdread_lba resets the DMA destination to sdbuf.
|
|
self.put('dma_xfer_page', 15, 1)
|
|
self.put('dma_xfer_off', SYMS['sdbuf'])
|
|
else:
|
|
dest = self.get('dma_xfer_page', 1)*16384 + (self.get('dma_xfer_off') & 16383)
|
|
self.ram[dest:dest+512] = self.disk[lba]
|
|
self.m.af = 0x0040 # A=0, Z=1 (successful sector I/O)
|
|
self.m.pc = int.from_bytes(self.m.memory[self.m.sp:self.m.sp+2], 'little')
|
|
self.m.sp += 2
|
|
raise AssertionError(f'{name}: execution did not return')
|
|
|
|
|
|
def test_offsets_and_links():
|
|
d = Driver()
|
|
for n in range(128):
|
|
d.fat(256+n, 2000+n)
|
|
for n in range(128):
|
|
d.cluster(256+n)
|
|
assert d.call('cluster_to_fatpos') == 0
|
|
assert d.get('gnc_fatsec_lo') == 34
|
|
assert d.get('gnc_inoff') == n*4, n
|
|
d.call('get_next_cluster')
|
|
assert d.get('chain_ended', 1) == 0
|
|
assert d.get('cur_cluster_lo') == 2000+n, n
|
|
for n in (65535, 65536, 0x123456):
|
|
d.put('fatsz32_lo', 65535)
|
|
d.cluster(n)
|
|
assert d.call('cluster_to_fatpos') == 0
|
|
sector = d.get('gnc_fatsec_lo') | d.get('gnc_fatsec_hi') << 16
|
|
assert sector == 32 + n//128
|
|
assert d.get('gnc_inoff') == (n % 128)*4
|
|
|
|
|
|
def test_write_and_allocation():
|
|
for n in range(128):
|
|
d = Driver()
|
|
original = bytearray(b'\x55\x55\x55\xa5'*128)
|
|
d.disk[51] = original[:]
|
|
d.cluster(256+n)
|
|
d.put('new_val_lo', 0x3456)
|
|
d.put('new_val_hi', 0x0123)
|
|
assert d.call('write_fat_entry') == 0
|
|
expected = original[:]
|
|
expected[n*4:n*4+4] = b'\x56\x34\x23\xa1'
|
|
assert d.disk[51] == expected, n
|
|
assert d.disk[115] == expected, n
|
|
for start, free in ((319, 320), (320, 321), (356, 357), (383, 384)):
|
|
d = Driver()
|
|
for n in range(256, 512):
|
|
d.fat(n, 0x0fffffff)
|
|
d.fat(free, 0)
|
|
d.put('next_search_valid', 1, 1)
|
|
d.put('next_search_lo', start)
|
|
assert d.call('find_free_cluster') == 0
|
|
assert d.get('free_cluster_lo') == free, (start, free)
|
|
|
|
|
|
def test_fragmented_load():
|
|
d = Driver()
|
|
chain = [319, 500, 320, 1023, 384, 383, 356, 256] * 1
|
|
# 40 non-adjacent clusters, 40 KiB: crosses FAT sectors and RAM pages.
|
|
chain += [1300 + n*137 for n in range(32)]
|
|
expected = bytearray()
|
|
for index, cluster in enumerate(chain):
|
|
d.fat(cluster, chain[index+1] if index+1 < len(chain) else 0x0fffffff)
|
|
for sector in range(2):
|
|
data = bytearray((index*19 + sector*71 + i) % 256 for i in range(512))
|
|
d.disk[1017+(cluster-2)*2+sector] = data
|
|
expected.extend(data)
|
|
d.cluster(chain[0])
|
|
d.call('stream_open')
|
|
d.m.c, d.m.hl = 32, 0x4000
|
|
for count in (13, 27, 40):
|
|
d.m.b = count
|
|
assert d.call('core_load512') == 0
|
|
assert d.ram[32*16384:32*16384+len(expected)] == expected
|
|
assert (d.m.c, d.m.hl) == (34, 0x6000)
|
|
d.m.b = 1
|
|
assert d.call('core_load512') == 15
|
|
|
|
|
|
if __name__ == '__main__':
|
|
for test in (test_offsets_and_links, test_write_and_allocation, test_fragmented_load):
|
|
test()
|
|
print(f'PASS: {test.__name__}')
|