Files
ZiFi/_sd/zc_sd_driver_body.asm
T

4393 lines
113 KiB
NASM

DEVICE ZXSPECTRUM48
; zc_sd_driver_body.asm -- the "packed body" half of zc_sd_driver.asm,
; split out so it can be compressed (ZX0, backward mode) and depacked
; at runtime by drv_dos_swp in the main driver file, freeing enough
; room in the resident #2000-#3FFF window for DELFL/RENAM.
; See [[project-zifi-custom-sd-driver]].
;
; Assembled standalone to compute this code's TRUE final runtime
; addresses (needed both for the compressor's input and so the main
; driver file can reference these labels via body_syms.inc, generated
; from this file's own --sym dump). NOT block-loaded directly by
; spgbld -- only zc_sd_driver.bin (built from the main file) is.
;
; ORG #0000: nothing here needs to avoid the RST vectors (#0000-#0038)
; -- this driver never executes an RST instruction (confirmed via
; grep), and real interrupts are held off (di) for the entire window
; page #0F is mapped into slot 0, including through drv_dos_swp's own
; unpack call, so there's no IM1/IM2 vector-table conflict either.
;
; Historical note: zifi.asm's own load_ini (reading zifi.ini at boot)
; used to call the real LOAD512 with HL=#0000/PAGE3, directly
; clobbering this body's own code at address 0 (confirmed empirically,
; and root-caused to load_ini sharing a "quick scratch = address 0"
; assumption inherited from the ORIGINAL closed WDFCVBI2.COD, whose
; own depacked body lived up near #3C00, never touching #0000 at all
; -- confirmed via a real disassembly of WDFCVBI2.COD's own DOS_SWP).
; Tried fixing this from THIS side first (a sacrifice_buf filler, then
; an ORG shift to #0200) -- both reliably broke BOTH Z80 Hrust decoders
; tried (root cause never found, not a transcription bug either time).
; Fixed instead on the zifi.asm side: load_ini/parse_ini now use
; LOAD512's SAME slot-1/#4000/PAGE1 convention as SAVE512 (see
; core_load512's own comment below), so nothing ever touches address 0
; here anymore -- ORG #0000 is safe again, no shift/buffer needed.
ORG #0000
CONF EQU #77
DATA EQU #57
sdinit xor a
ld (sd_block_addressed),a
call csh
ld de,512+10
call cycl
ld de,8000
1 dec de
ld a,d
or e
jp z,sdinit_fail
call do_cmd0
jr nz,1b
dec a
jp nz,1b
call do_cmd8
push af
ld bc,DATA
in e,(c)
in e,(c)
in h,(c)
in l,(c)
pop af
jr nz,1b
bit 2,a
jr z,sd_v2
;--- legacy path: SDv1 (ACMD41) then MMC (CMD1) ---
ld de,8000
2 dec de
ld a,d
or e
jr z,3f
ld h,0
call do_acmd41
jr nz,2b
cp 1
jr z,2b
or a
jr nz,3f
jr sd_fbs
3 ld de,8000
4 dec de
ld a,d
or e
jr z,sdinit_fail
call do_cmd1
jr nz,4b
cp 1
jr z,4b
or a
jr nz,sdinit_fail
sd_fbs call do_cmd16
jr nz,sdinit_fail
or a
jr nz,sdinit_fail
jr sdinit_ok
sd_v2 ld de,#01aa
or a
sbc hl,de
jr nz,sdinit_fail
ld de,8000
5 dec de
ld a,d
or e
jr z,sdinit_fail
ld h,#40
call do_acmd41
jr nz,5b
cp 1
jr z,5b
or a
jr nz,sdinit_fail
; ACMD41 just succeeded (card left the busy state). CMD58 is
; mandatory here: without it the following four reads only sample
; idle SPI bytes (#ff) and falsely turn every v2 card into SDHC.
; Read its OCR (4 bytes, MSB-first) for the REAL CCS bit
; (bit 30 = bit 6 of the first byte) instead of assuming
; every card that speaks the v2 protocol is block-addressed.
; A genuine SDv2 card can still be an SDSC (<=2GB) card. See
; [[project-zifi-custom-sd-driver]].
call do_cmd58
or a
jr nz,sdinit_fail
ld bc,DATA
in a,(c)
bit 6,a
jr z,sd_v2_ocr_rest
ld a,1
ld (sd_block_addressed),a
sd_v2_ocr_rest in a,(c) ; discard the remaining 3 OCR bytes
in a,(c)
in a,(c)
ld a,(sd_block_addressed)
or a
jr z,sd_fbs ; SDv2 SDSC also requires a 512-byte block length
sdinit_ok call csh
xor a
ret
sdinit_fail call csh
xor a
inc a
ret
;--- set_lba32: HL=bits31-16, DE=bits15-0 -> stores into lba_arg (MSB
;--- first). Store the LBA before SD I/O: transfer routines use registers
;--- for command framing and DMA setup. ---
set_lba32 ld a,h
ld (lba_arg),a
ld a,l
ld (lba_arg+1),a
ld a,d
ld (lba_arg+2),a
ld a,e
ld (lba_arg+3),a
ret
lba_arg ds 4
;--- lba_guard_check: the just-set lba_arg (4 bytes, H,L,D,E big-endian
;--- order per set_lba32) against sd_lba_max_hi/lo. Carry SET = in
;--- range (OK), carry CLEAR = out of range (refuse) -- same 32-bit-
;--- compare shape as cluster_to_fatpos's own bounds check. This is the
;--- LBA-range guard from the Zuma article's §34.2: a byte-addressing-
;--- vs-block-addressing mixup sending a CMD17 argument 512x too large
;--- has bricked a card until power-cycle on this exact hardware family
;--- before (needing a real power cycle to recover, same symptom this
;--- project's own interface-2 CS_ON experiment hit) -- refusing
;--- locally, before the request ever reaches the card, is the safety
;--- net for that whole class of bug. Checked inside sdread_lba/
;--- sdwrite_multi_start themselves (not every set_lba32 call site) so
;--- it can't be bypassed by a future caller that forgets to check it. ---
lba_guard_check
ld a,(lba_arg+2)
ld h,a
ld a,(lba_arg+3)
ld l,a
ld de,(sd_lba_max_lo)
or a
sbc hl,de
ld a,(lba_arg+0)
ld h,a
ld a,(lba_arg+1)
ld l,a
ld de,(sd_lba_max_hi)
sbc hl,de
ret
;=====================================================================
; Card addressing mode (SDSC byte-addressed vs SDHC/SDXC block-
; addressed) -- see [[project-zifi-custom-sd-driver]]. Every card this
; project has hardware-tested on is block-addressed (>=4GB); this only
; matters for a genuine old/small SDSC (<=2GB) card, which interprets
; the same CMD17/18/24/25 argument as a byte offset, not a sector
; number -- sending a raw sector number to one would silently read/
; write from the wrong place (off by a factor of 512).
;=====================================================================
sd_block_addressed db 0 ; 0=byte-addressed (SDSC/legacy default),
; 1=block-addressed (SDHC/SDXC, set only
; after a confirmed CCS=1 in sdinit)
card_arg ds 4
;--- compute_card_arg: converts lba_arg (32-bit sector number, MSB-
;--- first) into card_arg (the actual bytes to send as the CMD17/18/24/
;--- 25 argument) -- identical to lba_arg when sd_block_addressed=1,
;--- or lba_arg<<9 (sector number -> byte offset, 512=2^9) when 0. Any
;--- legitimate SDSC-range sector number's top byte is always 0 (max
;--- ~4,194,304 sectors for a 2GB card fits in 23 bits), so shifting
;--- left by a full byte first (dropping the original top byte) then
;--- one more bit is exact, not lossy, for every value this path can
;--- actually be reached with. ---
compute_card_arg
ld a,(sd_block_addressed)
or a
jr z,cca_shift
ld hl,lba_arg
ld de,card_arg
ld bc,4
ldir
ret
cca_shift ld a,(lba_arg+3) ; e = bits7-0
ld e,a
ld a,(lba_arg+2) ; d = bits15-8
ld d,a
ld a,(lba_arg+1) ; c = bits23-16
ld c,a
; (lba_arg+0, bits31-24, is always 0 here -- dropped by the
; byte-shift below, see header comment)
ld b,c ; shift left by one whole byte:
ld c,d ; b=old c, c=old d, d=old e, e=0
ld d,e
ld e,0
sla e ; then one more bit, 32-bit-wide,
rl d ; LSB-first chain (e -> d -> c -> b)
rl c
rl b
ld a,b
ld (card_arg+0),a
ld a,c
ld (card_arg+1),a
ld a,d
ld (card_arg+2),a
ld a,e
ld (card_arg+3),a
ret
;=====================================================================
; DMA-accelerated 512-byte SPI<->sdbuf transfer, replacing the original
; CPU IN/OUT byte loops (each ~40+ T-states/byte, vs. DMA moving the
; whole 256-word block in hardware). Register map and calling
; convention confirmed against a REAL working example in this exact
; SDK, per the user's own pointer: E:\zx-evo-master\pentevo\sdk\
; ft812sdk\lib\esp32\esp32.c's esp_send_dma/esp_recv_dma -- and that
; file's own SPI_CTRL/SPI_DATA ports (confirmed via its tsconf.h:
; __sfr __at 0x77 SPI_CTRL, __at 0x57 SPI_DATA) are the EXACT SAME
; ports this driver's CONF/DATA already use -- same shared SPI/DMA
; hardware, just talking to whichever device is currently CS-selected
; (SD here, ESP32 there). No address-alignment requirement appears
; anywhere in that reference code (plain 16-bit address + a separate
; page byte) -- the "must be 512-aligned" note from WC's own SD_ZC.ASM
; investigated earlier in this project looks like WC's own internal
; simplification, not a real hardware constraint, so sdbuf's existing
; (unaligned) address is used as-is.
;
; DMASADDR/DMADADDR = 16-bit address (L/H) + a separate page byte.
; DMALEN = (length_in_bytes/2)-1 (per-unit length in WORDS: 512/2-1=
; 255). DMANUM = (unit_count)-1 -- always 0 here (one 512-byte sector
; per call), matching this driver's existing per-sector cluster-chain-
; following structure; DMANUM>0 would need physically contiguous
; sectors, not guaranteed across a cluster boundary.
;
; Caller contract: send the command/argument bytes and consume the
; response/data token via the CPU as before (di/ei-protected CORE entry
; already covers this, see the wrap_* functions) -- these two helpers
; ONLY replace the raw byte-shuffling loop, nothing else about the SD
; protocol. BC is left pointing at dma_ctr on return, NOT at DATA --
; callers that go on to bang more bytes on DATA must reload BC.
;=====================================================================
own_page equ #0f ; this driver's own physical page, matching
; zifi.asm's own "sd_driver_page equ #0f"
; (which we can't reference directly -- that
; constant lives in zifi.asm, not this file)
dma_daddrl equ #1daf
dma_daddrh equ #1eaf
dma_daddrx equ #1faf
dma_saddrl equ #1aaf
dma_saddrh equ #1baf
dma_saddrx equ #1caf
dma_len equ #26af
dma_num equ #28af
dma_ctr equ #27af
;--- dma_xfer_off/dma_xfer_page: the shared DMA target address, set by
;--- the caller right before calling dma_recv_ext/dma_send_ext below --
;--- sdread_lba/sdwrite_multi_block point it at the fixed sdbuf/own_page
;--- pair for the general-purpose SD<->sdbuf path; core_save512/
;--- core_load512 (via stream_write_sector_from/stream_read_sector_to)
;--- point it at the CALLER's own page+offset instead, DMAing the
;--- 512-byte payload DIRECTLY between the SD card and that memory,
;--- bypassing sdbuf AND the page1_port/LDIR dance entirely. That LDIR
;--- needed PAGE1 mapped to the caller's page for the whole copy -- but
;--- PAGE1 is also one of the pages set_music_pages_lite repages on
;--- every interrupt during playback (zifi.asm's pt_play), and there's
;--- no way to restore a write-only page port after an interrupt
;--- clobbers it. DMA addresses RAM by physical page number directly,
;--- never touching the CPU's slot mapping at all, so it's immune to
;--- this regardless of what any interrupt does to PAGE0/1/3 in the
;--- meantime -- matching how the original closed WDFCVBI2.COD driver's
;--- own LOAD512/SAVE512 worked (confirmed via disassembly to use this
;--- same DMA mechanism, and to need zero di/ei protection because of
;--- it). See [[project-zifi-custom-sd-driver]] "noise mixed with
;--- melody".
;---
;--- The high byte IS masked to the low 6 bits (AND #3F) here -- the
;--- earlier assumption that the DMA hardware itself truncates a raw
;--- address to 14 bits turned out to be unverified for an address with
;--- H>=#40: sdbuf's own address never exercised that case (this whole
;--- compressed body sits under #2100, H always <#40 there already, so
;--- masking or not made no observable difference for the sdbuf/
;--- own_page path) -- it only ever LOOKED confirmed for that reason. A
;--- slot-1 address like #4000-#7FFF (exactly what ZiFi's own SAVE512/
;--- LOAD512 callers pass) has H in #40-#7F, where an unmasked write
;--- puts the WRONG high offset byte on the DMA port (off by a whole
;--- #4000) -- caught immediately on real testing via "Error parsing
;--- ini file" (load_ini's own LOAD512 call, HL=#4000). WC's own
;--- DSDZC.ASM computes the equivalent value via repeated SUB #40 (H mod
;--- #40); AND #3F is arithmetically identical and much cheaper here
;--- since only a single 16KB-aligned page ever matters. ---
dma_xfer_off dw 0
dma_xfer_page db 0
;--- set_dma_sdbuf: points dma_xfer_off/page at the fixed sdbuf/own_page
;--- pair -- shared by sdread_lba/sdwrite_multi_block/sdwrite_lba
;--- instead of each inlining the same 4 instructions. ---
set_dma_sdbuf ld hl,sdbuf
ld (dma_xfer_off),hl
ld a,own_page
ld (dma_xfer_page),a
ret
dma_set_addr_ext
ld b,h
ld c,l
ld a,(dma_xfer_off)
out (c),a
inc b
ld a,(dma_xfer_off+1)
and #3f
out (c),a
inc b
ld a,(dma_xfer_page)
out (c),a
ret
dma_recv_ext ld hl,dma_daddrl
call dma_set_addr_ext
ld bc,dma_len
ld a,255
out (c),a
ld bc,dma_num
xor a
out (c),a
ld bc,dma_ctr
ld a,#42
out (c),a
jr dma_wait
dma_send_ext ld hl,dma_saddrl
call dma_set_addr_ext
ld bc,dma_len
ld a,255
out (c),a
ld bc,dma_num
xor a
out (c),a
ld bc,dma_ctr
ld a,#c2
out (c),a
; falls into dma_wait
dma_wait in a,(c) ; dma_ctr/dma_status share one port
add a,a ; bit7 (busy) into carry
jr c,dma_wait
ret
;--- sdread_lba: read the sector set via set_lba32 into sdbuf (sets the
;--- DMA target to the fixed sdbuf/own_page pair, then falls into
;--- sdread_lba_to's shared command-sending+DMA logic). ---
sdread_lba call set_dma_sdbuf
; falls into sdread_lba_to
;--- sdread_lba_to: same read, but DMAs the 512-byte payload directly to
;--- whatever (dma_xfer_page,dma_xfer_off) is CURRENTLY set to --
;--- sdread_lba (above) points it at sdbuf/own_page first; a caller that
;--- wants it to land elsewhere (core_load512's path, via
;--- stream_read_sector_to) sets those two variables itself and calls
;--- this entry point directly, skipping the sdbuf default. Same
;--- instruction shape as the original hardcoded-immediate version that
;--- was first proven to work (out (c),a for every byte) -- only the
;--- source of each A load differs (memory instead of an immediate). ---
sdread_lba_to ld a,%01000000+17
ld (cmdlba_op),a
call send_cmd_lba
jp nc,sdread_lba_oob
; falls into sdread_common
sdread_common call resp
or a
jr nz,sdread_fail
call wtdo
cp #fe
jr nz,sdread_fail
call dma_recv_ext
in a,(DATA)
in a,(DATA)
call csh
xor a
ret
sdread_fail call csh
xor a
inc a
ret
sdread_lba_oob ; out-of-range LBA refused before ever touching the SPI
; bus (no csh needed -- nothing was selected)
xor a
inc a
ret
;--- CMD25 (WRITE_MULTIPLE_BLOCK) write, split into three pieces so a
;--- run of CONSECUTIVE sectors (e.g. zero-filling a whole cluster) can
;--- share ONE open session instead of paying the full CMD25-open +
;--- R1-wait + stop-token + settling-reads + busy-wait overhead on
;--- EVERY sector -- a real problem hit in practice: zero-filling a
;--- 64-sector cluster one full session per sector was slow enough to
;--- feel like a hang. NOT CMD24 (single-block write), which this used
;--- at first and failed on real hardware. Ported from WC core32's own
;--- SDDSE/SAVDS (SD_ZC.ASM), the proven, shipped write path on this
;--- hardware: it never uses CMD24 at all, always CMD25 even for a
;--- single sector, with data token 0xFC (not CMD24's 0xFE) and a
;--- trailing stop token 0xFD + settling reads. Mirrors the read side's
;--- own lesson (CMD18 was proven unstable, CMD17 wasn't) -- for writes
;--- the roles are reversed: don't assume CMD24 by analogy, port what's
;--- actually shipped.
;---
;--- sdwrite_multi_start: opens a session at the sector set via
;--- set_lba32. Follow with 1+ calls to sdwrite_multi_block (each
;--- writing sdbuf as the NEXT consecutive sector) and then exactly one
;--- call to sdwrite_multi_stop. A=0 ok, A=1 fail. ---
sdwrite_multi_start
ld a,%01000000+25 ; CMD25 = WRITE_MULTIPLE_BLOCK
ld (cmdlba_op),a
call send_cmd_lba
jp nc,sdread_lba_oob ; shared with sdread's own OOB path --
; both just refuse before touching SPI
call resp
or a
jp nz,sdread_fail
call wait
xor a
ret
;--- send_cmd_lba: shared CMD-framing sequence for sdread_lba_to's CMD17
;--- and sdwrite_multi_start's CMD25 -- identical apart from the opcode
;--- byte (set by the caller into cmdlba_op first). Checks the LBA
;--- range, computes card_arg, selects the card, and sends the 6-byte
;--- command frame (opcode + card_arg's 4 bytes + trailing 0xFF CRC
;--- placeholder). Carry SET = sent, NC = out of range (nothing was
;--- selected, caller should bail to sdread_lba_oob). ---
cmdlba_op db 0
send_cmd_lba call lba_guard_check
ret nc
call compute_card_arg
call csh
call csl
ld bc,DATA
ld a,(cmdlba_op)
out (c),a
ld hl,card_arg
ld a,(hl)
out (c),a
inc hl
ld a,(hl)
out (c),a
inc hl
ld a,(hl)
out (c),a
inc hl
ld a,(hl)
out (c),a
ld a,#ff
out (c),a
scf
ret
;--- sdwrite_multi_block: writes sdbuf as the next block of an open
;--- session (sets the DMA source to the fixed sdbuf/own_page pair, then
;--- falls into sdwrite_multi_block_from's shared token+DMA+response
;--- logic). Does NOT send CMD25 or the stop token. A=0 ok, A=1 fail. ---
sdwrite_multi_block
call set_dma_sdbuf
; falls into sdwrite_multi_block_from
;--- sdwrite_multi_block_from: same write, but DMAs the 512-byte payload
;--- directly FROM whatever (dma_xfer_page,dma_xfer_off) is CURRENTLY
;--- set to -- sdwrite_multi_block (above) points it at sdbuf/own_page
;--- first; a caller that wants it sourced elsewhere (core_save512's
;--- path, via stream_write_sector_from) sets those two variables itself
;--- and calls this entry point directly. Caller must have already
;--- opened a session via sdwrite_multi_start. Token 0xFC + 512 bytes +
;--- CRC placeholder + response token + busy-wait. Does NOT send CMD25
;--- or the stop token. A=0 ok, A=1 fail. ---
sdwrite_multi_block_from
ld bc,DATA
ld a,#fc ; data token for a CMD25 block (0xFC, not 0xFE)
out (c),a
call dma_send_ext
ld bc,DATA ; dma_send_ext left BC pointing at the DMA
; control port, not DATA -- restore it before
; the CRC bytes/response-token read below
ld a,#ff ; 2 dummy CRC bytes (CRC checking is off)
out (c),a
out (c),a
2 in a,(c) ; data response token: skip leading idle 0xFF
cp #ff ; bytes first (matches WC's own DRESP)
jr z,2b
and #1f ; xxx0AAA1, AAA=010 accepted
cp #05
jp nz,sdread_fail
call wait ; busy while this block programs
xor a
ret
;--- sdwrite_multi_stop: sends the stop-transmission token, settles,
;--- waits for busy to clear, and deselects. Closes a session opened by
;--- sdwrite_multi_start. A=0 always. ---
sdwrite_multi_stop
ld bc,DATA
ld a,#fd ; stop transmission token
out (c),a
ld b,16 ; settling reads (matches WC's SNB)
1 in a,(c)
djnz 1b
call wait
call csh
xor a
ret
;--- sdwrite_lba: write the sector set via set_lba32 from sdbuf, as a
;--- lone single-sector session (start+block+stop). For a run of
;--- consecutive sectors, use sdwrite_multi_start/_block/_stop directly
;--- instead to share one session. A=0 ok, A=1 fail. ---
sdwrite_lba call set_dma_sdbuf
; falls into sdwrite_lba_from
;--- sdwrite_lba_from: same write, but via sdwrite_multi_block_from --
;--- the DMA-direct-to-caller-memory path used by
;--- stream_write_sector_from. sdwrite_lba (above) points the DMA source
;--- at sdbuf/own_page first; falling straight through here reuses the
;--- exact same session-open/block/stop sequence either way. ---
sdwrite_lba_from
call sdwrite_multi_start
or a
jp nz,sdread_fail
call sdwrite_multi_block_from
or a
jp nz,sdread_fail
jp sdwrite_multi_stop
;--- low-level primitives ---
csh push bc
push af
ld bc,CONF
ld a,%00000011
out (c),a
ld bc,DATA
ld a,#ff
out (c),a
pop af
pop bc
ret
csl push bc
push af
ld bc,CONF
ld a,%00000001
out (c),a
ld bc,DATA
ld a,#ff
out (c),a
pop af
pop bc
jp wait
wait push bc
push af
push de ; preserve sdinit's retry counter across csl
ld bc,DATA
ld de,60000
1 in a,(c)
inc a
jr z,2f
dec de
ld a,d
or e
jr nz,1b
2 pop de
pop af
pop bc
ret
wtdo push bc
push de
ld bc,DATA
ld de,60000
1 in a,(c)
cp #ff
jr nz,2f
dec de
ld a,d
or e
jr nz,1b
2 pop de
pop bc
ret
resp push de
push bc
ld bc,DATA
ld d,10
1 in a,(c)
bit 7,a
jr z,2f
dec d
jr nz,1b
inc d
2 pop bc
pop de
ret
cycl ; DE = number of #FF clock bytes to send
ld bc,DATA
1 ld a,#ff
out (c),a
dec de
ld a,d
or e
jr nz,1b
ret
cmd00 db %01000000+0,0,0,0,0,#95
cmd08 db %01000000+8,0,0,1,#aa,#87
cmd16 db %01000000+16,0,0,2,0,#ff
do_cmd0 ld hl,cmd00
jp send_frame
do_cmd8 ld hl,cmd08
jp send_frame
do_cmd16 ld hl,cmd16
jp send_frame
do_cmd58 ld a,%01000000+58
call cmdo
jp resp
do_cmd1 ld a,%01000000+1
call cmdo
jp resp
do_cmd55 ld a,%01000000+55
call cmdo
jp resp
;--- send a pre-built 6-byte command frame pointed to by HL ---
send_frame call csh
call csl
ld bc,DATA
outi
outi
outi
outi
outi
outi
jp resp
;--- send a simple command: A=opcode, all-zero args, CRC=#FF ---
cmdo call csh
call csl
cmdx ld bc,DATA
out (c),a
xor a
out (c),a
out (c),a
out (c),a
out (c),a
ld a,#ff
out (c),a
ret
;--- ACMD41: H = argument high byte (0, or #40 for HCS), rest zero ---
do_acmd41 call do_cmd55
call csh
call csl
ld bc,DATA
ld a,%01000000+41
out (c),a
out (c),h
xor a
out (c),a
out (c),a
out (c),a
ld a,#ff
out (c),a
jp resp
;=====================================================================
; FAT32 geometry: parse the BPB (sdbuf must hold sector 0) and locate
; the root directory sector, then scan it for a "ZIFI" 8.3 entry.
;=====================================================================
;--- addtop_hi/lo: LBA offset of the FAT32 volume within the whole
;--- device (0 unless a partition table was found). WC core32's HDD
;--- calls this ADDTOP and adds it via XSPOZ to every sector number it
;--- reads (FAT sectors, data-cluster sectors, the BPB itself) -- SD
;--- cards vary on whether they even have a partition table at all
;--- (this project's own card doesn't -- BPB sits directly at LBA0). ---
addtop_hi dw 0
addtop_lo dw 0
;--- add_addtop: HL:DE (32-bit sector, hi:lo) += (addtop_hi:addtop_lo).
;--- Call this right before set_lba32 for any FAT-area or data-cluster
;--- sector -- NOT for the values stored in root_dir_hi/lo, data_start_
;--- hi/lo etc, which stay partition-relative (matching WC's own SDFAT/
;--- BFTSZ convention: ADDTOP is added only at actual read time). ---
add_addtop push bc
ld bc,(addtop_lo)
ex de,hl
add hl,bc
ex de,hl
ld bc,(addtop_hi)
adc hl,bc
pop bc
ret
;--- detect_partition: sdbuf must already hold sector 0. Scans up to 4
;--- MBR partition entries (offset 446, 16 bytes each) for a FAT32 type
;--- byte (0x0B or 0x0C). If found, sets addtop_hi/lo to that
;--- partition's start LBA and re-reads its first sector into sdbuf (the
;--- real BPB). If the 55 AA signature is missing or no entry matches,
;--- addtop_hi/lo stays 0 and sdbuf is left untouched -- the original
;--- sector 0 is then treated as the BPB directly, matching a card
;--- formatted without any partition table (confirmed: what this
;--- project's own card does). Doesn't follow extended-partition chains
;--- (type 0x05/0x0F) -- rare enough on an SD card that it's an accepted
;--- simplification for now, not full MBR support.
;---
;--- A raw FAT32 BPB (no partition table at all) also ends in 55 AA --
;--- every valid boot sector does -- so a "partition type" match here
;--- could be a false positive: the reserved bytes at offset 446-509 of
;--- a real BPB could coincidentally contain 0x0B/0x0C at one of the 4
;--- checked spots. So a tentative match is only trusted if re-reading
;--- that sector ALSO passes verify_fat32; otherwise this falls back to
;--- the original sector 0 (kept in sec0_copy) with ADDTOP=0. A=0 unless
;--- even the fallback read failed outright (shouldn't happen -- sdbuf
;--- already held sector 0 successfully before this was ever called). ---
sec0_copy ds 512
detect_partition
xor a
ld (addtop_hi),a
ld (addtop_hi+1),a
ld (addtop_lo),a
ld (addtop_lo+1),a
ld hl,sdbuf
ld de,sec0_copy
ld bc,512
ldir
ld a,(sdbuf+510)
cp #55
jr nz,dp_none
ld a,(sdbuf+511)
cp #aa
jr nz,dp_none
ld hl,sdbuf+446
ld b,4
dp_loop ld (dp_entry),hl
push hl
ld de,4
add hl,de
ld a,(hl)
pop hl
cp #0b
jr z,dp_found
cp #0c
jr z,dp_found
ld de,16
add hl,de
djnz dp_loop
dp_none xor a
ret
dp_found ld hl,(dp_entry)
ld de,8
add hl,de ; hl -> LBA-start field (entry+8, 4 bytes LE)
ld a,(hl)
ld (addtop_lo),a
inc hl
ld a,(hl)
ld (addtop_lo+1),a
inc hl
ld a,(hl)
ld (addtop_hi),a
inc hl
ld a,(hl)
ld (addtop_hi+1),a
ld hl,(addtop_hi)
ld de,(addtop_lo)
call set_lba32
call sdread_lba
or a
jr nz,dp_fallback
call verify_fat32
or a
ret z ; genuinely looks like a BPB -- keep it
dp_fallback xor a
ld (addtop_hi),a
ld (addtop_hi+1),a
ld (addtop_lo),a
ld (addtop_lo+1),a
ld hl,sec0_copy
ld de,sdbuf
ld bc,512
ldir
xor a
ret
dp_entry dw 0
;--- verify_fat32: sdbuf must hold sector 0. A=0 if this really looks like
;--- a FAT32 volume, A=1 if not (don't trust the rest of the parsed BPB
;--- fields, and don't attempt the FAT32-only root-dir-by-cluster walk).
;--- SD cards vary a lot card to card -- checks the boot sector 55 AA
;--- signature, that BytesPerSector is actually 512 (not every device
;--- uses that), and that FATSz16 (offset 22-23) is 0 -- FAT32 always
;--- routes the FAT size through FATSz32 (offset 36) instead and zeroes
;--- the legacy FAT16-only field, so a nonzero FATSz16 means this is
;--- FAT12/FAT16, not FAT32, and the rest of this driver doesn't apply.
;--- Also checks RootEntryCount==0 (another FAT16-only field FAT32
;--- always zeroes), SectorsPerCluster is a nonzero power of 2 (as the
;--- spec requires), and FATSz32 itself is nonzero -- all cross-checked
;--- against WC core32's own HDD routine, which performs exactly these
;--- same checks before trusting a BPB. ---
verify_fat32 ld a,(sdbuf+510)
cp #55
jr nz,vf_bad
ld a,(sdbuf+511)
cp #aa
jr nz,vf_bad
ld hl,(sdbuf+11) ; BytesPerSector
ld de,512
or a
sbc hl,de
jr nz,vf_bad
ld hl,(sdbuf+22) ; FATSz16 -- must be 0 on FAT32
ld a,h
or l
jr nz,vf_bad
ld hl,(sdbuf+17) ; RootEntryCount -- must be 0 on FAT32
ld a,h ; (a leftover FAT12/16 field FAT32 zeroes)
or l
jr nz,vf_bad
ld a,(sdbuf+13) ; SectorsPerCluster must be a nonzero
or a ; power of 2 (1,2,4,...,128) -- test via
jr z,vf_bad ; V & (V-1) == 0
ld b,a
dec a
and b
jr nz,vf_bad
ld hl,(sdbuf+36) ; FATSz32 must be nonzero (32-bit)
ld a,h
or l
jr nz,vf_ok
ld hl,(sdbuf+38)
ld a,h
or l
jr z,vf_bad
vf_ok xor a
ret
vf_bad xor a
inc a
ret
;--- parse_bpb: reads BPB fields out of sdbuf (offsets per the standard
;--- FAT32 BPB layout), computes DataStartSector and RootDirSector as
;--- true 32-bit values.
;--- FATSz32 alone can be tens of thousands of sectors on a real card
;--- (30MB+ FAT tables are normal) -- ReservedSectorCount + NumFATs*
;--- FATSz32 overflowed 16 bits on the first try (32 + 2*59430 =
;--- 118892 > 65535), silently wrapping to a wrong-but-in-range sector
;--- that just happened to read back as all zeroes. All the sector-
;--- number math below uses ADC HL,DE across (hi,lo) 16-bit halves. ---
parse_bpb ld hl,(sdbuf+32) ; TotalSectors32 low16 (u32 @ offset 32)
ld (total_sectors_lo),hl
ld hl,(sdbuf+34) ; TotalSectors32 high16
ld (total_sectors_hi),hl
ld hl,(sdbuf+14) ; ReservedSectorCount (u16)
ld (reserved_sectors),hl
ld hl,(sdbuf+48) ; FSInfoSector (u16, sector # within volume)
ld (fsinfo_sector),hl
ld a,(sdbuf+16) ; NumFATs (u8)
ld (num_fats),a
ld a,(sdbuf+13) ; SectorsPerCluster (u8)
ld (sectors_per_cluster),a
ld hl,(sdbuf+38) ; FATSz32 high16
ld (fatsz32_hi),hl
ld hl,(sdbuf+36) ; FATSz32 low16
ld (fatsz32_lo),hl
ld hl,(sdbuf+46) ; RootCluster high16
ld (root_cluster_hi),hl
ld hl,(sdbuf+44) ; RootCluster low16
ld (root_cluster_lo),hl
; NumFATs * FATSz32 (32-bit product via repeated 32-bit add --
; NumFATs is always tiny, 1 or 2, so this is at most 2 iterations)
ld hl,0
ld (prod_lo),hl
ld (prod_hi),hl
ld a,(num_fats)
or a
jr z,mul1_done
ld b,a
mul1_loop ld hl,(prod_lo)
ld de,(fatsz32_lo)
add hl,de
ld (prod_lo),hl
ld hl,(prod_hi)
ld de,(fatsz32_hi)
adc hl,de
ld (prod_hi),hl
djnz mul1_loop
mul1_done
; DataStartSector = ReservedSectorCount + (NumFATs*FATSz32)
ld hl,(reserved_sectors)
ld de,(prod_lo)
add hl,de
ld (data_start_lo),hl
ld hl,0
ld de,(prod_hi)
adc hl,de
ld (data_start_hi),hl
; RootCluster - 2 (32-bit)
ld hl,(root_cluster_lo)
ld de,2
or a
sbc hl,de
ld (rc2_lo),hl
ld hl,(root_cluster_hi)
ld de,0
sbc hl,de
ld (rc2_hi),hl
; (RootCluster-2) * SectorsPerCluster (32-bit product, repeated
; 32-bit add -- SectorsPerCluster is a small power of 2, <=128)
ld hl,0
ld (prod2_lo),hl
ld (prod2_hi),hl
ld a,(sectors_per_cluster)
or a
jr z,mul2_done
ld b,a
mul2_loop ld hl,(prod2_lo)
ld de,(rc2_lo)
add hl,de
ld (prod2_lo),hl
ld hl,(prod2_hi)
ld de,(rc2_hi)
adc hl,de
ld (prod2_hi),hl
djnz mul2_loop
mul2_done
; RootDirSector = DataStartSector + (RootCluster-2)*SectorsPerCluster
ld hl,(data_start_lo)
ld de,(prod2_lo)
add hl,de
ld (root_dir_lo),hl
ld hl,(data_start_hi)
ld de,(prod2_hi)
adc hl,de
ld (root_dir_hi),hl
; sd_lba_max = ADDTOP + TotalSectors32 -- one past the last
; valid absolute LBA on this card/partition. Computed here
; (not in detect_partition) because it needs TotalSectors32,
; which only exists once the real BPB has been parsed --
; ADDTOP itself is already finalized by the time parse_bpb
; runs (detect_partition runs first, see start:).
ld hl,(addtop_lo)
ld de,(total_sectors_lo)
add hl,de
ld (sd_lba_max_lo),hl
ld hl,(addtop_hi)
ld de,(total_sectors_hi)
adc hl,de
ld (sd_lba_max_hi),hl
ret
total_sectors_lo dw 0
total_sectors_hi dw 0
; default #FFFFFFFF ("unbounded") until parse_bpb computes the real
; value -- CRITICAL: sector-0 and detect_partition's own reads happen
; BEFORE parse_bpb ever runs, so a 0 default here would refuse even
; the very first boot read (0 >= 0), bricking the driver from the
; first sector on. #FFFFFFFF is effectively "no restriction yet".
sd_lba_max_lo dw #ffff
sd_lba_max_hi dw #ffff
reserved_sectors dw 0
num_fats db 0
sectors_per_cluster db 0
fatsz32_lo dw 0
fatsz32_hi dw 0
root_cluster_lo dw 0
root_cluster_hi dw 0
prod_lo dw 0
prod_hi dw 0
data_start_lo dw 0
data_start_hi dw 0
rc2_lo dw 0
rc2_hi dw 0
prod2_lo dw 0
prod2_hi dw 0
root_dir_lo dw 0
root_dir_hi dw 0
;--- find_zifi: scans sdbuf (one already-read 512-byte dir sector, 16 x
;--- 32-byte entries) for an 8.3 short-name entry matching target_name
;--- (caller fills this 11-byte buffer before calling find_zifi_all --
;--- despite the name, it's not hardcoded to "ZIFI" any more). A=1 (plus
;--- zifi_cluster_hi/zifi_cluster/zifi_size_hi/zifi_size) if found, A=2
;--- if this sector hit the true 0x00 end-of-directory marker (caller
;--- should stop entirely, not just this sector), A=0 if this sector's
;--- 16 entries didn't match (caller should read the next sector and
;--- try again -- a directory cluster can be many sectors; 0xE5 =
;--- deleted entry, skipped). ---
zifi_cluster_hi dw 0
zifi_cluster dw 0
zifi_size_hi dw 0
zifi_size dw 0
target_name db "ZIFI " ; 11 bytes: caller-set 8.3 name to search for
find_zifi ld hl,sdbuf
ld b,16
fz_loop ld a,(hl)
or a
jr z,fz_end
cp #e5
jr z,fz_skip
push hl
push bc
ld de,target_name
ld b,11
fz_cmp ld a,(de)
cp (hl)
jr nz,fz_cmpfail
inc hl
inc de
djnz fz_cmp
pop bc
pop hl
jr fz_found
fz_cmpfail pop bc
pop hl
fz_skip ld de,32
add hl,de
djnz fz_loop
xor a
ret
fz_end ld a,2
ret
fz_found push hl
ld de,20 ; cluster high16 at entry+20
add hl,de
ld a,(hl)
ld (zifi_cluster_hi),a
inc hl
ld a,(hl)
ld (zifi_cluster_hi+1),a
pop hl
push hl
ld de,26 ; cluster low16 (+26/27), then size (+28..31)
add hl,de
ld a,(hl)
ld (zifi_cluster),a
inc hl
ld a,(hl)
ld (zifi_cluster+1),a
inc hl
ld a,(hl)
ld (zifi_size),a
inc hl
ld a,(hl)
ld (zifi_size+1),a
inc hl
ld a,(hl)
ld (zifi_size_hi),a
inc hl
ld a,(hl)
ld (zifi_size_hi+1),a
pop hl
ld a,1
ret
;--- find_zifi_all: scans the WHOLE directory cluster chain starting at
;--- whatever cluster the caller has already put in (cur_cluster_hi,
;--- cur_cluster_lo) -- e.g. (root_cluster_hi,root_cluster_lo) to search
;--- the root, or a found subdirectory's own cluster to search inside
;--- it -- for an 8.3 entry matching target_name. A cluster can hold far
;--- more than one sector's worth of entries (e.g. 64 sectors = 1024
;--- entries here), and the directory itself can span MORE than one
;--- cluster once it grows -- a directory is just a cluster chain like
;--- any other file, so this follows the FAT chain (via get_next_cluster)
;--- whenever the current cluster runs out without finding it or hitting
;--- the true end-of-directory marker. A=1+zifi_cluster_hi/zifi_cluster/
;--- zifi_size_hi/zifi_size if found, A=0 if not (read error, true
;--- end-of-directory, or the chain ends with no match). ---
fza_remaining db 0
find_zifi_all
fza_cluster call cluster_to_sector
ld hl,(fza_sector_hi)
ld de,(fza_sector_lo)
call set_lba32
ld a,(sectors_per_cluster)
ld (fza_remaining),a
fza_loop call sdread_lba
or a
jr nz,fza_stop
call find_zifi
cp 1
jr z,fza_done
cp 2
jr z,fza_stop
; advance lba_arg by 1 sector (32-bit increment, MSB-first bytes,
; LSB-to-MSB ripple carry via a small loop instead of unrolling)
ld hl,lba_arg+3
ld b,4
fza_inc_loop ld a,(hl)
inc a
ld (hl),a
jr nz,fza_next
dec hl
djnz fza_inc_loop
fza_next ld hl,fza_remaining
dec (hl)
jr nz,fza_loop
; this cluster is exhausted with no match -- follow the FAT
; chain to the next cluster of the directory, if there is one
call get_next_cluster
ld a,(chain_ended)
or a
jr nz,fza_stop
jr fza_cluster
fza_stop xor a
ret
fza_done ld a,1
ret
;--- cluster_to_sector: (cur_cluster_hi,cur_cluster_lo) -> fza_sector_hi/
;--- fza_sector_lo = DataStartSector + (cluster-2)*SectorsPerCluster.
;--- Same formula as parse_bpb's RootDirSector computation, generalized
;--- to any cluster number so the FAT-chain walk above can locate each
;--- cluster's data as it follows the chain. ---
c2s_lo dw 0
c2s_hi dw 0
c2s_prod_lo dw 0
c2s_prod_hi dw 0
fza_sector_lo dw 0
fza_sector_hi dw 0
cluster_to_sector
ld hl,(cur_cluster_lo)
ld de,2
or a
sbc hl,de
ld (c2s_lo),hl
ld hl,(cur_cluster_hi)
ld de,0
sbc hl,de
ld (c2s_hi),hl
ld hl,0
ld (c2s_prod_lo),hl
ld (c2s_prod_hi),hl
ld a,(sectors_per_cluster)
or a
jr z,c2s_done
ld b,a
c2s_loop ld hl,(c2s_prod_lo)
ld de,(c2s_lo)
add hl,de
ld (c2s_prod_lo),hl
ld hl,(c2s_prod_hi)
ld de,(c2s_hi)
adc hl,de
ld (c2s_prod_hi),hl
djnz c2s_loop
c2s_done ld hl,(data_start_lo)
ld de,(c2s_prod_lo)
add hl,de
ex de,hl
ld hl,(data_start_hi)
ld bc,(c2s_prod_hi)
adc hl,bc
call add_addtop ; offset by ADDTOP here, at read time
ld (fza_sector_hi),hl
ld (fza_sector_lo),de
ret
;--- cluster_to_fatpos: (cur_cluster_hi,cur_cluster_lo) -> gnc_fatsec_hi/
;--- gnc_fatsec_lo (the FAT sector, partition-relative -- caller adds
;--- ADDTOP) and gnc_inoff (byte offset within that sector). Shared by
;--- get_next_cluster (reading a chain link) and write_fat_entry
;--- (writing one, for MKDIR/MKFILE). FAT32 entries are 4 bytes; the
;--- entry for cluster N is at byte N*4 of the FAT, i.e. FAT sector =
;--- ReservedSectorCount + (N*4)/BytesPerSector, offset (N*4) mod
;--- BytesPerSector within it -- since BytesPerSector is always 512
;--- (checked by verify_fat32), that's cluster>>7 / (cluster&0x7F)*4.
;--- Bounds-checks the FAT-sector offset against FATSz32 first (matches
;--- WC core32's CURIT) so a corrupted/out-of-range cluster number is
;--- rejected instead of blindly read from or written to. A=0 ok, A=1
;--- out of range. ---
cur_cluster_hi dw 0
cur_cluster_lo dw 0
gnc_hi dw 0
gnc_lo dw 0
gnc_fatsec_hi dw 0
gnc_fatsec_lo dw 0
gnc_inoff dw 0 ; FAT32 entry offset: 0..508, needs 9 bits
cluster_to_fatpos
ld hl,(cur_cluster_hi)
ld (gnc_hi),hl
ld hl,(cur_cluster_lo)
ld (gnc_lo),hl
ld b,7
ctf_shr ld hl,(gnc_hi)
srl h
rr l
ld (gnc_hi),hl
ld hl,(gnc_lo)
rr h
rr l
ld (gnc_lo),hl
djnz ctf_shr
ld hl,(gnc_lo)
ld de,(fatsz32_lo)
or a
sbc hl,de
ld hl,(gnc_hi)
ld de,(fatsz32_hi)
sbc hl,de
jr nc,ctf_bad
ld hl,(reserved_sectors)
ld de,(gnc_lo)
add hl,de
ld (gnc_fatsec_lo),hl
ld hl,0
ld de,(gnc_hi)
adc hl,de
ld (gnc_fatsec_hi),hl
ld hl,(cur_cluster_lo)
ld a,l
and #7f
ld l,a
ld h,0
add hl,hl
add hl,hl
ld (gnc_inoff),hl
xor a
ret
ctf_bad xor a
inc a
ret
;--- get_next_cluster: reads the FAT32 table entry for the cluster in
;--- (cur_cluster_hi,cur_cluster_lo) and, if it isn't an end-of-chain
;--- marker, replaces cur_cluster_hi/lo with the next cluster. Sets
;--- chain_ended=1 on EOC (masked entry >=0x0FFFFFF8), 2 on an
;--- invalid link or read error, 0 on a valid next cluster. ---
chain_ended db 0
nxt_lo dw 0
nxt_hi dw 0
get_next_cluster
call cluster_to_fatpos
or a
jr nz,gnc_bad
ld hl,(gnc_fatsec_hi)
ld de,(gnc_fatsec_lo)
call add_addtop ; offset by ADDTOP here, at read time
call set_lba32
call sdread_lba
or a
jr nz,gnc_bad
ld hl,(gnc_inoff)
ld de,sdbuf
add hl,de
ld a,(hl)
ld (nxt_lo),a
inc hl
ld a,(hl)
ld (nxt_lo+1),a
inc hl
ld a,(hl)
ld (nxt_hi),a
inc hl
ld a,(hl)
and #0f
ld (nxt_hi+1),a
ld hl,(nxt_hi)
ld a,h
cp #0f
jr nz,gnc_have_next
ld a,l
cp #ff
jr c,gnc_have_next
ld hl,(nxt_lo)
ld de,#fff0
or a
sbc hl,de
jr c,gnc_have_next
ld a,l ; #0FFFFFF0..7 are reserved/bad, not EOC
cp 8
jr c,gnc_bad
gnc_end ld a,1
jr gnc_stop
gnc_bad ld a,2
gnc_stop
ld (chain_ended),a
ret
gnc_have_next ld hl,(nxt_hi)
ld a,h
or l
jr nz,gnc_valid
ld hl,(nxt_lo)
ld de,2
or a
sbc hl,de
jr c,gnc_bad ; free/reserved links 0 and 1
gnc_valid ld hl,(nxt_lo)
ld (cur_cluster_lo),hl
ld hl,(nxt_hi)
ld (cur_cluster_hi),hl
xor a
ld (chain_ended),a
ret
;=====================================================================
; Write path: free-cluster search, FAT entry write (mirrored to every
; FAT copy), cluster zero-fill, and directory-entry write -- the pieces
; MKDIR (and later MKFILE) need. Ported in spirit from WC core32's
; SRHFCL/GENBU (free-cluster search), the FAT-write half of BUtoFAT/
; SVFATM (mirror to every FAT copy), and MKDIR/SVHDFL/SRHDRN's free-
; slot search, simplified where this test doesn't need the general
; case (e.g. always exactly one cluster, no directory-growth-on-full).
;=====================================================================
;--- write_fat_entry: writes (new_val_hi,new_val_lo) into the FAT entry
;--- for the cluster in (cur_cluster_hi,cur_cluster_lo), mirroring the
;--- write to every one of NumFATs copies -- WC core32 calls this
;--- SVFATM. This is the exact bug class already found and fixed once
;--- before in WC Improved's stock driver (FAT copy #2+ silently going
;--- stale because only copy #1 was ever written) -- chkdsk flags
;--- exactly this kind of desync, so it's not optional here. Reads+
;--- patches the primary copy's sector once, then writes that same
;--- patched buffer to every copy's corresponding sector (FATsector +
;--- i*FATSz32) rather than re-reading each copy -- correct as long as
;--- all copies started in sync, which they should have. A=0 ok, A=1
;--- fail (out of range, or a read/write error). ---
new_val_hi dw 0
new_val_lo dw 0
wfe_remaining db 0
wfe_sector_hi dw 0
wfe_sector_lo dw 0
write_fat_entry
call cluster_to_fatpos
or a
jr nz,wfe_fail
ld hl,(gnc_fatsec_hi)
ld (wfe_sector_hi),hl
ld hl,(gnc_fatsec_lo)
ld (wfe_sector_lo),hl
ld hl,(wfe_sector_hi)
ld de,(wfe_sector_lo)
call add_addtop
call set_lba32
call sdread_lba
or a
jr nz,wfe_fail
ld hl,(gnc_inoff)
ld de,sdbuf
add hl,de
ld a,(new_val_lo)
ld (hl),a
inc hl
ld a,(new_val_lo+1)
ld (hl),a
inc hl
ld a,(new_val_hi)
ld (hl),a
inc hl
ld a,(hl) ; top nibble of the 4th byte is reserved --
and #f0 ; preserve whatever's already on disk there
ld b,a
ld a,(new_val_hi+1)
and #0f
or b
ld (hl),a
ld a,(num_fats)
ld (wfe_remaining),a
wfe_loop ld hl,(wfe_sector_hi)
ld de,(wfe_sector_lo)
call add_addtop
call set_lba32
call sdwrite_lba ; sdbuf (already patched) reused as-is
or a
jr nz,wfe_fail
ld hl,(wfe_sector_lo)
ld de,(fatsz32_lo)
add hl,de
ld (wfe_sector_lo),hl
ld hl,(wfe_sector_hi)
ld de,(fatsz32_hi)
adc hl,de
ld (wfe_sector_hi),hl
ld hl,wfe_remaining
dec (hl)
jr nz,wfe_loop
xor a
ret
wfe_fail xor a
inc a
ret
;--- read_fsinfo_hint: reads the FSInfo sector (BPB offset 48) and, if
;--- its two signatures check out, extracts FSI_Nxt_Free (offset 492) as
;--- a starting hint for find_free_cluster -- FAT32's whole reason for
;--- having this field is to avoid scanning the FAT from cluster 2 every
;--- time, which on a card that already has real files can mean
;--- scanning past thousands of used entries first (a real problem hit
;--- in practice, even with the sector-batched scan below). Falls back
;--- to "no hint" if the signatures don't check out or the value is the
;--- documented "unknown" sentinel (all 4 bytes 0xFF). This is a
;--- starting GUESS only, not gospel -- find_free_cluster still verifies
;--- an entry is really free before trusting it, and falls back to a
;--- from-cluster-2 scan if starting from the hint finds nothing (worth
;--- remembering: WC core32's own shipped build doesn't trust this hint
;--- at all, always rescanning from scratch -- noted earlier this
;--- session as a real-world reliability concern with the field). ---
fsinfo_sector dw 0
fsinfo_hint_hi dw 0
fsinfo_hint_lo dw 0
fsinfo_valid db 0
read_fsinfo_hint
xor a
ld (fsinfo_valid),a
ld hl,0
ld de,(fsinfo_sector)
call add_addtop
call set_lba32
call sdread_lba
or a
ret nz
ld hl,(sdbuf+0) ; lead signature "RRaA" (0x41615252 LE)
ld de,#5252
or a
sbc hl,de
ret nz
ld hl,(sdbuf+2)
ld de,#4161
or a
sbc hl,de
ret nz
ld hl,(sdbuf+484) ; struct signature "rrAa" (0x61417272 LE)
ld de,#7272
or a
sbc hl,de
ret nz
ld hl,(sdbuf+486)
ld de,#6141
or a
sbc hl,de
ret nz
ld hl,(sdbuf+492) ; FSI_Nxt_Free (u32 LE); all-0xFF = unknown
ld a,h
and l
ld b,a
ld hl,(sdbuf+494)
ld a,h
and l
and b
cp #ff
ret z
ld hl,(sdbuf+492)
ld (fsinfo_hint_lo),hl
ld hl,(sdbuf+494)
ld (fsinfo_hint_hi),hl
ld a,1
ld (fsinfo_valid),a
ret
;--- write_fsinfo_hint: writes (next_search_hi,next_search_lo) -- already
;--- computed as the just-allocated cluster+1 -- into FSI_Nxt_Free
;--- (offset 492) of the FSInfo sector, and decrements FSI_Free_Count
;--- (offset 488) by 1 unless it's the "unknown" sentinel (all 4 bytes
;--- 0xFF). Keeps the hint honest for whoever reads it next, including a
;--- future run of THIS driver -- unlike WC core32's own shipped build,
;--- which never touches this field at all (confirmed on hardware to be
;--- why the hint was stale: it pointed near the ZIFI folder's own
;--- cluster, i.e. frozen since roughly when the card was first set up,
;--- while everything written since -- by WC -- never advanced it).
;--- Called right after a successful allocation; best-effort only --
;--- the allocation itself already succeeded regardless of whether this
;--- does. A=0 ok, A=1 fail (non-fatal to the caller). ---
write_fsinfo_hint
ld hl,0
ld de,(fsinfo_sector)
call add_addtop
call set_lba32
call sdread_lba
or a
ret nz
ld hl,(sdbuf+488) ; FSI_Free_Count -- skip if "unknown" (0xFFFFFFFF)
ld a,h
and l
ld b,a
ld hl,(sdbuf+490)
ld a,h
and l
and b
cp #ff
jr z,wfi_skip_count
ld hl,(sdbuf+488)
ld de,1
or a
sbc hl,de
ld (sdbuf+488),hl
jr nc,wfi_skip_count
ld hl,(sdbuf+490)
ld de,0 ; low-word subtraction already supplied the borrow
sbc hl,de
ld (sdbuf+490),hl
wfi_skip_count ld hl,(next_search_lo)
ld (sdbuf+492),hl
ld hl,(next_search_hi)
ld (sdbuf+494),hl
ld hl,0
ld de,(fsinfo_sector)
call add_addtop
call set_lba32
jp sdwrite_lba
;--- find_free_cluster: scans the FAT for a free entry (4-byte value ==
;--- 0). Sets free_cluster_hi/lo if found. A=0 ok, A=1 not found
;--- (scanned the whole FAT from cluster 2 onward -- volume genuinely
;--- full). Reads each FAT sector ONCE and checks all of its remaining
;--- entries from memory before moving to the next sector, instead of
;--- issuing a fresh disk read per cluster checked -- a real problem hit
;--- in practice on a card that already has real files on it.
;---
;--- Where the scan STARTS: prefers next_search_hi/lo, an in-memory
;--- cache of "one past the last cluster this session actually
;--- allocated", over the on-disk FSInfo hint -- real hardware test
;--- confirmed the FSInfo hint (FSI_Nxt_Free) is genuinely STALE on a
;--- heavily-used card (30000+ files): it pointed to cluster 0x1B91,
;--- deep inside a ~530000-cluster contiguous used region that had to
;--- be scanned past anyway, costing ~4100 sector reads. That's exactly
;--- why WC core32's own shipped build doesn't trust this field either
;--- (noted earlier this session, now empirically confirmed). The
;--- in-memory cache only helps for the 2nd+ allocation THIS boot --
;--- the very first one still has to pay the full scan once, same as
;--- WC's own FSTFRC starting cold at boot. Uses (and leaves modified)
;--- cur_cluster_hi/lo, same as get_next_cluster. ---
free_cluster_hi dw 0
free_cluster_lo dw 0
ffc_ptr dw 0
ffc_count db 0
ffc_sectors_read dw 0
next_search_valid db 0
next_search_hi dw 0
next_search_lo dw 0
find_free_cluster
ld hl,0
ld (ffc_sectors_read),hl
ld a,(next_search_valid)
or a
jr z,ffc_use_hint
ld hl,(next_search_hi)
ld (cur_cluster_hi),hl
ld hl,(next_search_lo)
ld (cur_cluster_lo),hl
jp ffc_scan
ffc_use_hint call read_fsinfo_hint
ld a,(fsinfo_valid)
or a
jr z,ffc_from2
ld hl,(fsinfo_hint_hi)
ld (cur_cluster_hi),hl
ld hl,(fsinfo_hint_lo)
ld (cur_cluster_lo),hl
call ffc_scan
or a
ret z ; found starting from the hint
ffc_from2 ld hl,0
ld (cur_cluster_hi),hl
ld hl,2
ld (cur_cluster_lo),hl
jp ffc_scan
ffc_scan call cluster_to_fatpos
or a
jp nz,ffc_fail
ld hl,(gnc_fatsec_hi)
ld de,(gnc_fatsec_lo)
call add_addtop
call set_lba32
call sdread_lba
or a
jp nz,ffc_fail
ld hl,(ffc_sectors_read)
inc hl
ld (ffc_sectors_read),hl
ld hl,(gnc_inoff)
ld de,sdbuf
add hl,de
ld (ffc_ptr),hl
; how many of this sector's 128 entries are left from here on
; (the first sector checked may not start at entry 0, since
; cluster 2 isn't necessarily sector-aligned)
ld a,(cur_cluster_lo)
and #7f
ld b,a
ld a,128
sub b
ld (ffc_count),a
ffc_entloop ld hl,(ffc_ptr)
ld a,(hl)
or a
jr nz,ffc_entnext
inc hl
ld a,(hl)
or a
jr nz,ffc_entnext
inc hl
ld a,(hl)
or a
jr nz,ffc_entnext
inc hl
ld a,(hl)
and #0f
jr nz,ffc_entnext
ld hl,(cur_cluster_hi)
ld (free_cluster_hi),hl
ld hl,(cur_cluster_lo)
ld (free_cluster_lo),hl
; remember cluster+1 as the starting point for the NEXT search
; this boot -- avoids re-scanning the same used region on every
; later allocation this session (matches WC core32's own
; FSTFRC in-memory caching pattern)
ld hl,(cur_cluster_lo)
inc hl
ld (next_search_lo),hl
ld a,h
or l
jr nz,ffc_nsv
ld hl,(cur_cluster_hi)
inc hl
ld (next_search_hi),hl
jr ffc_nsv2
ffc_nsv ld hl,(cur_cluster_hi)
ld (next_search_hi),hl
ffc_nsv2 ld a,1
ld (next_search_valid),a
; persist the updated hint to disk too -- unlike WC core32,
; which never touches this field at all (confirmed the real
; cause of the stale hint found on hardware), so the NEXT
; session (not just this one) starts from an honest position
call write_fsinfo_hint
xor a
ret
ffc_entnext ld hl,(ffc_ptr)
ld de,4
add hl,de
ld (ffc_ptr),hl
ld hl,(cur_cluster_lo)
inc hl
ld (cur_cluster_lo),hl
ld a,h
or l
jr nz,ffc_nowrap
ld hl,(cur_cluster_hi)
inc hl
ld (cur_cluster_hi),hl
ffc_nowrap ld a,(ffc_count)
dec a
ld (ffc_count),a
jr nz,ffc_entloop
jp ffc_scan
ffc_fail xor a
inc a
ret
;--- zero_fill_cluster: writes SectorsPerCluster sectors of all-zero
;--- data to the cluster in (cur_cluster_hi,cur_cluster_lo) -- a fresh
;--- cluster on disk can hold anything left over from whatever used it
;--- last, and FAT32 doesn't guarantee zeroed free space. Uses ONE
;--- sdwrite_multi_start/_block/_stop session for the whole cluster
;--- (consecutive sectors) instead of a separate session per sector --
;--- with SectorsPerCluster=64 on this card, that's 63 fewer CMD25-open/
;--- R1-wait/stop-token/busy-wait round trips, which is most of what
;--- made this feel like a hang before. A=0 ok, A=1 fail (a write error
;--- partway through). ---
zfc_remaining db 0
zero_fill_cluster
call cluster_to_sector ; already ADDTOP-offset
ld hl,sdbuf
ld de,sdbuf+1
ld bc,511
ld (hl),0
ldir
ld hl,(fza_sector_hi)
ld de,(fza_sector_lo)
call set_lba32
call sdwrite_multi_start
or a
jr nz,zfc_fail
ld a,(sectors_per_cluster)
ld (zfc_remaining),a
zfc_loop call sdwrite_multi_block
or a
jr nz,zfc_fail
ld hl,zfc_remaining
dec (hl)
jr nz,zfc_loop
jp sdwrite_multi_stop
zfc_fail xor a
inc a
ret
;--- write_dir_entry: writes the 32-byte entry at dir_entry_buf into the
;--- first free slot (0x00 = end of directory / 0xE5 = deleted) found
;--- while scanning the directory cluster chain starting at
;--- (cur_cluster_hi,cur_cluster_lo) -- follows the FAT chain via
;--- get_next_cluster if a whole cluster has no free slot. Does NOT grow
;--- the directory with a new cluster if the entire existing chain is
;--- full (accepted limitation here -- ZiFi's own root/zifi dirs are
;--- nowhere near that). A=0 ok, A=1 fail (read/write error, or no free
;--- slot anywhere in the existing chain). ---
dir_entry_buf ds 32
wde_remaining db 0
wde_sector_hi dw 0
wde_sector_lo dw 0
wde_slotptr dw 0
write_dir_entry
wde_cluster call cluster_to_sector
ld hl,(fza_sector_hi)
ld (wde_sector_hi),hl
ld hl,(fza_sector_lo)
ld (wde_sector_lo),hl
ld a,(sectors_per_cluster)
ld (wde_remaining),a
wde_sec_loop ld hl,(wde_sector_hi)
ld de,(wde_sector_lo)
call set_lba32
call sdread_lba
or a
jr nz,wde_fail
ld hl,sdbuf
ld b,16
wde_entloop ld a,(hl)
or a
jr z,wde_slot
cp #e5
jr z,wde_slot
ld de,32
add hl,de
djnz wde_entloop
ld hl,(wde_sector_lo)
inc hl
ld (wde_sector_lo),hl
ld a,h
or l
jr nz,wde_next
ld hl,(wde_sector_hi)
inc hl
ld (wde_sector_hi),hl
wde_next ld hl,wde_remaining
dec (hl)
jr nz,wde_sec_loop
call get_next_cluster
ld a,(chain_ended)
or a
jr nz,wde_fail
jr wde_cluster
wde_slot ld (wde_slotptr),hl
ld hl,dir_entry_buf
ld de,(wde_slotptr)
ld bc,32
ldir
ld hl,(wde_sector_hi)
ld de,(wde_sector_lo)
call set_lba32
call sdwrite_lba
ret
wde_fail xor a
inc a
ret
;=====================================================================
; Cluster-chain streaming: generic sector-at-a-time read/write that
; follows a file's/directory's FAT chain automatically, allocating a
; new cluster on demand when writing past the current end -- the
; building block a real LOAD512/SAVE512 (and any multi-cluster file,
; which every previous MKFILE test here deliberately avoided by staying
; under one 32KB cluster) actually need. Caller sets (cur_cluster_hi,
; cur_cluster_lo) to the file's first cluster, calls stream_open once,
; then stream_read_sector/stream_write_sector repeatedly (one 512-byte
; sector at a time, via sdbuf).
;=====================================================================
stream_cluster_hi dw 0
stream_cluster_lo dw 0
stream_sec_in_cluster db 0
stream_eoc db 0
strm_sector_hi dw 0
strm_sector_lo dw 0
;--- stream_open: (cur_cluster_hi,cur_cluster_lo) -> starts a stream at
;--- that cluster's first sector. Cluster 0 means "root" (matches the
;--- convention already used for a subdirectory's ".." entry). ---
stream_open ld hl,(cur_cluster_hi)
ld a,h
or l
jr nz,strm_open_nz
ld hl,(cur_cluster_lo)
ld a,h
or l
jr nz,strm_open_nz
ld hl,(root_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(root_cluster_lo)
ld (cur_cluster_lo),hl
strm_open_nz ld hl,(cur_cluster_hi)
ld (stream_cluster_hi),hl
ld hl,(cur_cluster_lo)
ld (stream_cluster_lo),hl
xor a
ld (stream_sec_in_cluster),a
ld (stream_eoc),a
ret
;--- strm_calc_sector: (stream_cluster_hi,stream_cluster_lo,
;--- stream_sec_in_cluster) -> strm_sector_hi/lo (absolute sector,
;--- ADDTOP-offset already applied via cluster_to_sector). Clobbers
;--- cur_cluster_hi/lo (sets them to the stream's current cluster). ---
strm_calc_sector
ld hl,(stream_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(stream_cluster_lo)
ld (cur_cluster_lo),hl
call cluster_to_sector
ld hl,(fza_sector_lo)
ld a,(stream_sec_in_cluster)
ld d,0
ld e,a
add hl,de
ld (strm_sector_lo),hl
ld hl,(fza_sector_hi)
ld de,0
adc hl,de
ld (strm_sector_hi),hl
ret
;--- strm_advance: moves the stream position forward by one sector,
;--- crossing into the next cluster (via get_next_cluster) if that was
;--- the last sector of the current one. Sets stream_eoc=1 if the chain
;--- ends there, or stream_eoc=2 on a broken link / FAT read error.
;--- Returns A=0: the sector just transferred remains valid; the NEXT
;--- read/write reports the stopped stream. Only EOC=1 permits growth. ---
strm_advance ld a,(stream_sec_in_cluster)
inc a
ld b,a ; b = candidate new sec_in_cluster
ld a,(sectors_per_cluster)
cp b ; NC+NZ (sectors_per_cluster > b) -> still
jr nc,strm_adv_test_z ; within this cluster; Z or C -> cross over
jr strm_adv_cross
strm_adv_test_z jr z,strm_adv_cross
ld a,b
ld (stream_sec_in_cluster),a
xor a
ret
strm_adv_cross xor a
ld (stream_sec_in_cluster),a
ld hl,(stream_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(stream_cluster_lo)
ld (cur_cluster_lo),hl
call get_next_cluster
ld hl,(cur_cluster_hi)
ld (stream_cluster_hi),hl
ld hl,(cur_cluster_lo)
ld (stream_cluster_lo),hl
ld a,(chain_ended)
ld (stream_eoc),a
xor a
ret
strm_adv_same ld a,b
ld (stream_sec_in_cluster),a
xor a
ret
;--- stream_read_sector: reads the sector at the current stream
;--- position into sdbuf, then advances. A=0 ok, A=1 read error, A=2
;--- already at end of chain (nothing read). ---
stream_read_sector
ld a,(stream_eoc)
or a
jr nz,strm_read_eoc
call strm_calc_sector
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdread_lba
or a
ret nz
; save the just-read sector before strm_advance's cluster-
; crossing path (get_next_cluster) clobbers sdbuf with its own
; FAT-sector scratch -- same class of bug as write_fat_entry's
; sdbuf reuse (see stream_write_sector/stream_data_save), just
; on the read side: reading the LAST sector of a cluster read
; the right data from disk, but by the time the caller checked
; sdbuf, the chain-follow that runs immediately afterward (to
; find the next cluster, so the FOLLOWING read knows where to
; go) had already overwritten it
ld hl,sdbuf
ld de,stream_data_save
ld bc,512
ldir
call strm_advance
ld hl,stream_data_save
ld de,sdbuf
ld bc,512
ldir
xor a
ret
strm_read_eoc ld a,2
ret
;--- stream_read_sector_to: identical to stream_read_sector, except the
;--- sector is DMA'd directly to (dma_xfer_page,dma_xfer_off) -- caller
;--- must set those two variables first -- instead of sdbuf. No
;--- sdbuf-save/restore dance needed here (unlike stream_read_sector's
;--- own stream_data_save shuffle): the caller's data never touches
;--- sdbuf at all in this path, so get_next_cluster's own FAT-sector
;--- scratch use of sdbuf (inside strm_advance, on a cluster-boundary
;--- crossing) can't clobber it. Used by core_load512, see
;--- dma_xfer_off's own comment above. ---
stream_read_sector_to
ld a,(stream_eoc)
or a
jr nz,strm_read_eoc
call strm_calc_sector
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdread_lba_to
or a
ret nz
jp strm_advance
;--- stream_write_sector: writes sdbuf to the current stream position,
;--- extending the cluster chain (allocating a new cluster and linking
;--- it via write_fat_entry, replacing the previous cluster's EOC
;--- marker) if the stream has run past the end. A=0 ok, A=1 fail
;--- (alloc/link/write error). ---
stream_data_save ds 512 ; scratch: the caller's pending sector data,
; saved off while find_free_cluster/write_fat_entry
; below reuse sdbuf as their OWN FAT-sector
; read/patch/write scratch (found on hardware: without
; this save/restore, a cluster-boundary-crossing write
; got the just-written FAT sector's bytes into the
; new cluster's data instead of the real file data --
; both our own readback and Windows chkdsk caught this
; as real on-disk corruption, not just a readback bug)
stream_write_sector
ld a,(stream_eoc)
cp 2
ret nc ; a broken chain must never be extended
or a
jr z,strm_write_have
ld hl,sdbuf
ld de,stream_data_save
ld bc,512
ldir
call find_free_cluster
or a
ret nz
ld hl,(stream_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(stream_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,(free_cluster_hi)
ld (new_val_hi),hl
ld hl,(free_cluster_lo)
ld (new_val_lo),hl
call write_fat_entry ; old last cluster -> new cluster
or a
ret nz
ld hl,(free_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,#0fff
ld (new_val_hi),hl
ld hl,#ffff
ld (new_val_lo),hl
call write_fat_entry ; new cluster -> EOC
or a
ret nz
ld hl,(free_cluster_hi)
ld (stream_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (stream_cluster_lo),hl
xor a
ld (stream_sec_in_cluster),a
ld (stream_eoc),a
ld hl,stream_data_save
ld de,sdbuf
ld bc,512
ldir
strm_write_have call strm_calc_sector
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdwrite_lba
or a
ret nz
jp strm_advance
;--- stream_write_sector_from: identical to stream_write_sector, except
;--- the sector is DMA'd directly FROM (dma_xfer_page,dma_xfer_off) --
;--- caller must set those two variables first -- instead of sdbuf. On
;--- a cluster-boundary crossing (new cluster needed), find_free_cluster/
;--- write_fat_entry still freely reuse sdbuf as their own FAT-sector
;--- scratch, same as before -- that part is fine, sdbuf's raw bytes
;--- were never the caller's data in this path. BUT find_free_cluster/
;--- write_fat_entry also call sdread_lba/sdwrite_lba internally, which
;--- (via set_dma_sdbuf) overwrite the SHARED dma_xfer_page/dma_xfer_off
;--- pair with own_page/sdbuf -- so by the time strm_write_have2 below
;--- calls sdwrite_lba_from, it was DMAing from sdbuf/own_page (whatever
;--- FAT-sector bytes write_fat_entry last touched) instead of the
;--- caller's real page, on every single new-cluster allocation -- i.e.
;--- the first block of every brand-new file. This is exactly the same
;--- clobbering hazard stream_write_sector's own stream_data_save guards
;--- against, just against dma_xfer_page/off instead of sdbuf's bytes --
;--- confirmed as the real cause of "every download saves as identical
;--- garbage" (2026-09-09), see [[project-zifi-custom-sd-driver]]. Fix:
;--- reload the pair from sv512_page/sv512_off (core_save512's own
;--- persistent copy, untouched by find_free_cluster/write_fat_entry --
;--- they only touch dma_xfer_page/off and their own gnc_*/wfe_*/
;--- cur_cluster_* state) right before the real write below, instead of
;--- adding new scratch bytes to save/restore -- costs 12 bytes instead
;--- of ~27, fits the body's tight #2000 budget. Used by core_save512,
;--- see dma_xfer_off's own comment above. ---
stream_write_sector_from
ld a,(stream_eoc)
cp 2
ret nc
or a
jr z,strm_write_have2
call find_free_cluster
or a
ret nz
ld hl,(stream_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(stream_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,(free_cluster_hi)
ld (new_val_hi),hl
ld hl,(free_cluster_lo)
ld (new_val_lo),hl
call write_fat_entry ; old last cluster -> new cluster
or a
ret nz
ld hl,(free_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,#0fff
ld (new_val_hi),hl
ld hl,#ffff
ld (new_val_lo),hl
call write_fat_entry ; new cluster -> EOC
or a
ret nz
ld hl,(free_cluster_hi)
ld (stream_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (stream_cluster_lo),hl
xor a
ld (stream_sec_in_cluster),a
ld (stream_eoc),a
ld a,(sv512_page)
ld (dma_xfer_page),a
ld hl,(sv512_off)
ld (dma_xfer_off),hl
strm_write_have2
call strm_calc_sector
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdwrite_lba_from
or a
ret nz
jp strm_advance
;=====================================================================
; RTC read + FAT32 directory-entry date/time stamping.
;
; Ports/protocol ported from zifi.asm's own write_rtc (verified working
; on this exact hardware, see [[project-zifi-rtc-time-sync]]): #EFF7 is
; a SHARED system register (video mode/turbo/cache bits, not a
; dedicated CMOS-enable bit) -- must read-modify-write it, a literal
; write stomps unrelated bits. #DFF7 selects a DS1685-style RTC
; register index, #BFF7 is the data port for whichever register is
; selected. Register map (from zifi.asm's own write_rtc): 0x00=seconds,
; 0x02=minutes, 0x04=hours, 0x07=date, 0x08=month, 0x09=year (last 2
; digits only), 0x0B=register B (0x82=SET+24hr while reading, freezes
; the clock for a consistent snapshot; 0x02=24hr, running -- zifi.asm's
; own idle/teardown value, restored here the same way). All fields are
; BCD (confirmed via zifi.asm's code_time_rtc, which packs two ASCII
; digits directly as BCD nibbles, no decimal-to-binary step) -- this
; reads the RTC's raw registers and does its OWN correct BCD->binary
; conversion, since we need real binary values to pack into FAT's
; date/time bit fields, not just to redisplay as digits.
;
; NOT yet tested on hardware -- first attempt, ported carefully from a
; write path proven to work, but reading has never been exercised here.
;=====================================================================
rtc_sec db 0
rtc_min db 0
rtc_hour db 0
rtc_date db 0
rtc_month db 0
rtc_year db 0
read_rtc_datetime
; READ-ONLY -- never writes any RTC chip register (0x00-0x0D).
; The earlier version froze/restored register B (SET bit) for
; a torn-read-proof snapshot, but that means two real WRITES
; to the RTC's own register B every call, x8 calls/boot in
; this test suite. The user's own previously-proven-safe
; reference (D:\ST\DivMmc\RTC-master\rtc_gluk.asm, an ESXDOS
; RTC.SYS actually used without RTC data glitches) reads
; seconds/minutes/hours/date/month/year/register-B via plain
; INI, with NO write to register B at all, ever. Matching that
; exactly here: a torn read (a field rolling over mid-read) is
; rare and merely produces an off-by-a-few-seconds date/time
; stamp, self-correcting next call -- utterly harmless next to
; the real risk of a write leaving register B (or the DM/SET
; bits) in a bad state on real hardware.
;
; No di/ei here anymore -- this only ever really runs once,
; from inside drv_hdd, which is now itself di/ei-wrapped (see
; the jump-table wrappers below); a local ei here would have
; re-enabled interrupts before that OUTER di's critical
; section was actually done.
ld bc,#eff7
in a,(c)
or #80
out (c),a
; register B: read-only, for the DM bit (bit 2: 0=BCD,
; nonzero=binary) -- zifi.asm's own write_rtc always forces
; BCD when IT writes, but something else could leave the chip
; in binary mode, so this checks rather than assumes. No SET/
; freeze write -- see comment above.
ld a,#0b
ld b,#df
out (c),a
ld b,#bf
in a,(c)
and #04
ld (rtc_dm),a ; 0=BCD, nonzero=binary
ld a,#00 ; seconds
ld b,#df
out (c),a
ld b,#bf
in a,(c)
ld (rtc_sec),a
ld a,#02 ; minutes
ld b,#df
out (c),a
ld b,#bf
in a,(c)
ld (rtc_min),a
ld a,#04 ; hours
ld b,#df
out (c),a
ld b,#bf
in a,(c)
ld (rtc_hour),a
ld a,#07 ; date
ld b,#df
out (c),a
ld b,#bf
in a,(c)
ld (rtc_date),a
ld a,#08 ; month
ld b,#df
out (c),a
ld b,#bf
in a,(c)
ld (rtc_month),a
ld a,#09 ; year (last 2 digits)
ld b,#df
out (c),a
ld b,#bf
in a,(c)
ld (rtc_year),a
; nothing to restore -- register B was only ever read (see
; header comment), never written
ld bc,#eff7
in a,(c)
and #7f
out (c),a
ret
rtc_dm db 0
;--- rtc_field_to_bin: A=raw RTC register value -> A=binary, honoring
;--- the RTC's own DM (data-mode) bit captured in rtc_dm by
;--- read_rtc_datetime (0=BCD -> convert, nonzero=already binary ->
;--- pass through). Checking this instead of assuming BCD matters
;--- because something other than zifi.asm's own write_rtc (which
;--- always forces BCD when it writes) could leave the chip in binary
;--- mode. ---
rtc_field_to_bin
push bc
ld b,a
ld a,(rtc_dm)
or a
ld a,b
pop bc
ret nz
jp bcd_to_bin
;--- bcd_to_bin: A=BCD byte (0x00-0x99) -> A=binary (0-99) ---
bcd_to_bin push bc
ld c,a
and #0f
ld b,a ; b = units digit
ld a,c
and #f0
rrca
rrca
rrca
rrca ; a = tens digit
ld c,a
add a,a
add a,a
add a,a
add a,c
add a,c ; a = tens*10
add a,b ; + units
pop bc
ret
;--- rtc_to_fat_datetime: reads the RTC and packs it into fat_date/
;--- fat_time (standard FAT32 directory-entry format). Assumes a
;--- 2000+YY year, matching how zifi.asm's own write_rtc stores just
;--- the last 2 digits of a 4-digit SNTP-sourced year into the RTC. ---
fat_date dw 0
fat_time dw 0
rtc_cached db 0
rtc_to_fat_datetime
; touch the real RTC chip at most ONCE per boot -- this test
; suite calls rtc_to_fat_datetime 8 times (once per MKDIR/
; MKFILE-shaped test), and unlike zifi.asm's own write_rtc
; (called exactly once, at one deliberate point), doing the
; full di/select-register/read sequence 8 times back-to-back
; is real, repeated stress on hardware this project has
; already found to be timing-sensitive in not-fully-understood
; ways (see [[project-zifi-rtc-time-sync]]). All 8 call sites
; just want "the current stamp for this test run" -- a single
; real read, reused, is both safer and more correct than 8
; independent (and needlessly different-by-a-few-seconds)
; reads of the same boot.
ld a,(rtc_cached)
or a
ret nz
call read_rtc_datetime
ld a,(rtc_year)
call rtc_field_to_bin
add a,20 ; (2000+YY)-1980 = YY+20
ld h,a
ld l,0
add hl,hl ; hl = (year-1980) << 9
ld (fat_date),hl
ld a,(rtc_month)
call rtc_field_to_bin
ld h,0
ld l,a
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl ; hl = month << 5
ld de,(fat_date)
add hl,de
ld (fat_date),hl
ld a,(rtc_date)
call rtc_field_to_bin
ld h,0
ld l,a ; day, no shift (bits 4-0)
ld de,(fat_date)
add hl,de
ld (fat_date),hl
ld a,(rtc_hour)
call rtc_field_to_bin
ld h,a
ld l,0
add hl,hl
add hl,hl
add hl,hl ; hl = hour << 11 (== (hour<<8)*8)
ld (fat_time),hl
ld a,(rtc_min)
call rtc_field_to_bin
ld h,0
ld l,a
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl ; hl = minute << 5
ld de,(fat_time)
add hl,de
ld (fat_time),hl
ld a,(rtc_sec)
call rtc_field_to_bin
srl a ; 2-second resolution
ld h,0
ld l,a
ld de,(fat_time)
add hl,de
ld (fat_time),hl
ld a,1
ld (rtc_cached),a
ret
;--- stamp_dir_entry_datetime: writes fat_time/fat_date (already
;--- computed by rtc_to_fat_datetime) into the CURRENT dir_entry_buf's
;--- CrtTime/CrtDate/LstAccDate/WrtTime/WrtDate fields. Leaves
;--- CrtTimeTenth (offset 13) at 0 -- hundredths-of-a-second precision
;--- the RTC doesn't provide anyway. Call after clear_dir_entry (which
;--- already zeroed these), before the cluster/size fields. ---
stamp_dir_entry_datetime
ld hl,(fat_time)
ld (dir_entry_buf+14),hl ; CrtTime
ld (dir_entry_buf+22),hl ; WrtTime
ld hl,(fat_date)
ld (dir_entry_buf+16),hl ; CrtDate
ld (dir_entry_buf+18),hl ; LstAccDate
ld (dir_entry_buf+24),hl ; WrtDate
ret
;--- test_mkdir: creates a directory named "TESTDIR" in the root, to
;--- exercise the whole write path end to end: free-cluster search, FAT
;--- entry write (mirrored to every FAT copy), cluster zero-fill, the
;--- new directory's own "." and ".." entries, and its entry written
;--- into the parent (the root). A=0 ok, A=1 fail. ---
;--- sdbuf MUST be word-aligned (even address) -- the TS-Conf DMA
;--- controller masks the low bit off BOTH DMASAL and DMADAL (silently
;--- rounding down to the nearest even address, since DMA moves 16-bit
;--- words) -- confirmed against the emulator's own io.cpp
;--- (TSW_DMASAL/TSW_DMADAL: "val & 0xFE"). sdbuf had no explicit
;--- alignment before and just happened to land on an even address in
;--- every earlier build of this driver -- today's edits shifted the
;--- preceding code size by an odd amount, landing it on an ODD address
;--- for the first time, which silently shifted every DMA transfer
;--- to/from sdbuf one byte early (byte-for-byte otherwise correct --
;--- confirmed via a full 512-byte diff against the real card image).
;--- This explains "zifi.ini not found" surviving even after the
;--- #2000-overlap fix, and is likely what actually happened on real
;--- hardware too (this masking is real DMA hardware behavior, not an
;--- emulator quirk) -- ALIGN 2 makes this impossible to regress by
;--- accident again. See [[project-zifi-custom-sd-driver]]. ---
ALIGN 2
sdbuf ds 512
;=====================================================================
; VFAT long-filename support: real ZiFi calls FENTRY/MKDIR/MKFILE with
; free-form ASCII names (e.g. "downloads" = 9 chars, a date folder like
; "2026_09_05" = 10 chars) that don't fit an 8.3 short name (8-char
; base limit) -- WDFCVBI2.COD's real, confirmed short-name-collision
; bug (the whole reason this project exists) lives in exactly this
; machinery, so this driver needs a correct, from-scratch
; implementation, not a shortcut. Short name generation follows the
; standard Windows "basis name" scheme: uppercase, drop disallowed
; chars/spaces, truncate to 6+"~N"+ext when needed, N chosen to avoid a
; collision with what's already in the target directory. LFN entries
; (attr 0x0F) store the long name in reverse sequence order immediately
; before the short entry, each carrying the short entry's checksum so a
; reader can detect an orphaned/corrupt LFN run (this is the actual
; mechanism the original \downloads corruption happened in).
;=====================================================================
;--- lfn_checksum: HL -> 11-byte short name -> A = standard FAT LFN
;--- checksum (RRCA is a circular rotate-right, exactly the "wrap low
;--- bit into bit 7" step the algorithm needs -- no separate rotate-
;--- through-carry dance required). ---
lfn_checksum ld b,11
xor a
lfnck_loop rrca
add a,(hl)
inc hl
djnz lfnck_loop
ret
;--- classify_char: A = input byte -> A = filtered/uppercased byte,
;--- carry SET if this byte is allowed in an 8.3 short name (A-Z after
;--- uppercasing, 0-9, or one of a small punctuation allowlist), carry
;--- CLEAR if not (space, dot, or anything else -- caller drops it and
;--- must remember a long name is needed). ---
cc_symbols db "!#$%&'()-@^_`{}~",0
classify_char cp 'a'
jr c,cc_up
cp 'z'+1
jr nc,cc_up
sub 'a'-'A'
cc_up cp 'A'
jr c,cc_dig
cp 'Z'+1
jr c,cc_yes
cc_dig cp '0'
jr c,cc_sym
cp '9'+1
jr c,cc_yes
cc_sym ld b,a
ld hl,cc_symbols
cc_symloop ld a,(hl)
or a
jr z,cc_no
cp b
jr z,cc_symyes
inc hl
jr cc_symloop
cc_symyes ld a,b
cc_yes scf
ret
cc_no xor a
ret
;--- filter_name_to_83: HL -> NUL-terminated ASCII name (caller-owned,
;--- not modified) -> sn_base (8 bytes, space-padded), sn_ext (3 bytes,
;--- space-padded), sn_needs_lfn (1 if the short name had to drop/
;--- truncate/case-fold anything -- i.e. a real long name is needed),
;--- sn_name_len (name length excluding the NUL, for the LFN
;--- entry-count calculation later). The extension is whatever follows
;--- the LAST '.' in the name (an earlier '.' is just an invalid
;--- character in the base, same as any other disallowed byte). ---
sn_base ds 8
sn_ext ds 3
sn_needs_lfn db 0
sn_name_len dw 0
sn_dot_pos dw 0 ; 0xFFFF = no dot in the name
sn_base_len dw 0 ; length of the base substring (before ext)
fn83_charbuf db 0 ; scratch: classify_char's filtered byte, held
; across the flag-clobbering slot-count compare
filter_name_to_83
push hl
ld bc,0
ld de,#ffff
ld (sn_dot_pos),de
fn83_scan ld a,(hl)
or a
jr z,fn83_scandone
cp '.'
jr nz,fn83_scan_next
ld (sn_dot_pos),bc
fn83_scan_next inc hl
inc bc
jr fn83_scan
fn83_scandone ld (sn_name_len),bc
pop hl
ld a,' '
ld (sn_base+0),a
ld (sn_base+1),a
ld (sn_base+2),a
ld (sn_base+3),a
ld (sn_base+4),a
ld (sn_base+5),a
ld (sn_base+6),a
ld (sn_base+7),a
ld (sn_ext+0),a
ld (sn_ext+1),a
ld (sn_ext+2),a
xor a
ld (sn_needs_lfn),a
; base substring length = (no dot) ? name_len : dot_pos
ld de,(sn_dot_pos)
ld a,d
cp #ff
jr nz,fn83_have_baselen
ld a,e
cp #ff
jr nz,fn83_have_baselen
ld de,(sn_name_len)
fn83_have_baselen
ld (sn_base_len),de
; walk the base substring, filtering into sn_base (up to 8).
; hl still points at the start of the original name (from the
; pop above); de = remaining base chars to consume; c = how
; many of the 8 sn_base slots are filled so far; ix -> next
; free slot. classify_char returns the filtered char in A
; *and* sets/clears carry in the same instruction, so the char
; is stashed to a scratch byte before any flag-clobbering
; count compare, then reloaded once the compare is done.
ld bc,0
ld ix,sn_base
fn83_base_loop ld a,e
or d
jr z,fn83_ext ; de==0 -- consumed the whole base substring
dec de
ld a,(hl)
inc hl
call classify_char
jr nc,fn83_base_bad
ld (fn83_charbuf),a
ld a,c
cp 8
jr nc,fn83_base_full
ld a,(fn83_charbuf)
ld (ix+0),a
inc ix
inc c
jr fn83_base_loop
fn83_base_full ld a,1
ld (sn_needs_lfn),a
jr fn83_base_loop
fn83_base_bad ld a,1
ld (sn_needs_lfn),a
jr fn83_base_loop
; hl now points exactly at the '.' (if the name had one) or at
; the terminating NUL (if it didn't) -- the base loop above
; consumed exactly sn_base_len chars, which by construction
; equals sn_dot_pos when a dot exists.
fn83_ext ld de,(sn_dot_pos)
ld a,d
cp #ff
jr nz,fn83_ext_go
ld a,e
cp #ff
ret z ; no dot at all -- sn_ext stays blank, done
fn83_ext_go inc hl ; hl = ext start pointer
push hl
ld hl,(sn_name_len)
ld bc,(sn_dot_pos)
or a
sbc hl,bc
dec hl ; hl = ext_len (name_len - dot_pos - 1)
push hl
pop de ; de = ext_len (counter)
pop hl ; hl = ext start pointer, restored
ld bc,0
ld ix,sn_ext
fn83_ext_loop ld a,e
or d
ret z ; consumed the whole ext substring
dec de
ld a,(hl)
inc hl
call classify_char
jr nc,fn83_ext_bad
ld (fn83_charbuf),a
ld a,c
cp 3
jr nc,fn83_ext_full
ld a,(fn83_charbuf)
ld (ix+0),a
inc ix
inc c
jr fn83_ext_loop
fn83_ext_full ld a,1
ld (sn_needs_lfn),a
jr fn83_ext_loop
fn83_ext_bad ld a,1
ld (sn_needs_lfn),a
jr fn83_ext_loop
;--- generate_unique_shortname: (cur_cluster_hi,cur_cluster_lo already
;--- set by caller to the target directory) + HL -> the same NUL-
;--- terminated ASCII name filter_name_to_83 was just run on -> builds
;--- the final 11-byte short name into target_name (ready for
;--- find_zifi_all / write_dir_entry), trying "~1".."~9" (6-char
;--- truncated base) then "~10".."~99" (5-char truncated base) whenever
;--- sn_needs_lfn is set, re-checking find_zifi_all each try so the
;--- result never collides with an existing entry in that directory.
;--- A=0 ok, A=1 fail (exhausted ~99, directory has too many
;--- collisions -- not expected in practice here). ---
gus_suffix db 0 ; numeric tail, 1-99
generate_unique_shortname
ld a,(sn_needs_lfn)
or a
jr nz,gus_needs_suffix
; short name fits as-is (already validated by the caller's
; classify pass into sn_base/sn_ext) -- just check it doesn't
; happen to already exist under this exact 8.3 name
ld hl,sn_base
ld de,target_name
ld bc,8
ldir
ld hl,sn_ext
ld bc,3
ldir
call find_zifi_all
or a
jr z,gus_ok ; not found -- this exact name is free
; collides with an existing exact-8.3 entry -- fall back to
; the ~N scheme too, same as if truncation had been needed
gus_needs_suffix
ld a,1
ld (gus_suffix),a
gus_try ld a,(gus_suffix)
cp 10
jr c,gus_try_1digit
; 2-digit suffix: 5 base chars + "~" + 2 digits = 8
ld hl,sn_base
ld de,target_name
ld bc,5
ldir
ld de,target_name+5
ld a,'~'
ld (de),a
inc de
ld a,(gus_suffix)
call bin_to_2dig
ld (de),a
inc de
ld a,b ; second digit, set by bin_to_2dig
ld (de),a
jr gus_have_candidate
gus_try_1digit ; 1-digit suffix: 6 base chars + "~" + 1 digit = 8
ld hl,sn_base
ld de,target_name
ld bc,6
ldir
ld de,target_name+6
ld a,'~'
ld (de),a
inc de
ld a,(gus_suffix)
add a,'0'
ld (de),a
gus_have_candidate
ld hl,sn_ext
ld de,target_name+8
ld bc,3
ldir
call find_zifi_all
or a
jr z,gus_ok ; not found -- this candidate is free
ld hl,gus_suffix
inc (hl)
ld a,(hl)
cp 100
jr c,gus_try
xor a
inc a
ret ; exhausted 1..99
gus_ok xor a
ret
;--- bin_to_2dig: A = 10..99 -> A = tens digit ('1'-'9'), B = ones
;--- digit ('0'-'9'). Small helper, only ever called with 2-digit
;--- input here. ---
bin_to_2dig ld b,0
btd_loop cp 10
jr c,btd_done
sub 10
inc b
jr btd_loop
btd_done ; b = tens count (0-9), a = ones remainder (0-9)
ld c,a
ld a,b
add a,'0' ; tens digit char
ld b,a
ld a,c
add a,'0' ; ones digit char
ld c,a
ld a,b ; final A = tens char
ld b,c ; final B = ones char
ret
;--- test_shortname_gen: exercises filter_name_to_83 + generate_unique_
;--- shortname against a few real ZiFi-shaped names (some short enough
;--- to fit 8.3 as-is, some needing truncation) and prints each result
;--- (generated 11-byte short name + whether an LFN was judged
;--- necessary) -- checked here in isolation, on real hardware, before
;--- building the LFN-entry writer and long-name-aware FENTRY on top of
wne_max_lfn equ 8
wne_entries_buf ds (wne_max_lfn+1)*32
wne_slot_count db 0
lfn_char_dest db 1,3,5,7,9,14,16,18,20,22,24,28,30
ble_name_ptr dw 0
ble_lfn_count db 0
ble_checksum db 0
ble_j db 0
ble_charpos dw 0
ble_entry_ptr dw 0
build_lfn_entries
ld (ble_name_ptr),hl
ld a,(sn_needs_lfn)
or a
jr nz,ble_multi
; no LFN needed -- just the short entry
ld hl,dir_entry_buf
ld de,wne_entries_buf
ld bc,32
ldir
ld hl,target_name
ld de,wne_entries_buf
ld bc,11
ldir
ld a,1
ld (wne_slot_count),a
ret
ble_multi ; lfn_count = ceil((name_len+1)/13) = (name_len+13)/13
ld hl,(sn_name_len)
ld de,13
add hl,de
ld b,0
ble_divloop ld de,13
or a
sbc hl,de
jr c,ble_divdone
inc b
jr ble_divloop
ble_divdone ld a,b
ld (ble_lfn_count),a
inc a
ld (wne_slot_count),a
ld hl,target_name
call lfn_checksum
ld (ble_checksum),a
ld a,(ble_lfn_count)
ld (ble_j),a
ble_lfn_loop ld a,(ble_j)
or a
jp z,ble_lfn_done
; storage index i = lfn_count - j; entry_ptr = wne_entries_buf
; + i*32
ld b,a
ld a,(ble_lfn_count)
sub b
ld l,a
ld h,0
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl
ld de,wne_entries_buf
add hl,de
ld (ble_entry_ptr),hl
; zero the whole 32-byte entry first -- simplifies padding,
; only the bytes that need something other than 0x00 (the
; 0xFF pad bytes) are patched explicitly below
ld d,h
ld e,l
inc de
ld bc,31
ld (hl),0
ldir
; seq byte = j | (0x40 "last" bit, only on the highest j)
ld hl,(ble_entry_ptr)
ld a,(ble_j)
ld b,a
ld a,(ble_lfn_count)
cp b
jr nz,ble_notlast
ld a,b
or #40
jr ble_seqset
ble_notlast ld a,b
ble_seqset ld (hl),a
ld hl,(ble_entry_ptr)
ld de,11
add hl,de
ld a,#0f
ld (hl),a ; attr = ATTR_LONG_NAME
ld hl,(ble_entry_ptr)
ld de,13
add hl,de
ld a,(ble_checksum)
ld (hl),a
; char_offset (0-based index into the name) = (j-1)*13
ld a,(ble_j)
dec a
ld b,a
ld hl,0
ble_mul13 ld a,b
or a
jr z,ble_mul13done
ld de,13
add hl,de
dec b
jr ble_mul13
ble_mul13done ld (ble_charpos),hl
ld b,0 ; b = k, which of the 13 char slots
ble_charloop ld a,b
cp 13
jr nc,ble_lfn_next
ld hl,(ble_charpos)
push bc
ld c,b
ld b,0
add hl,bc
pop bc ; hl = charpos = ble_charpos + k
push hl
ld de,(sn_name_len)
or a
sbc hl,de
pop hl ; flags from the compare survive the
; pop (only POP AF touches flags);
; hl restored to charpos itself
jr z,ble_char_isnull
jr c,ble_char_isreal
ld a,#ff
jr ble_char_have
ble_char_isreal push hl
ld hl,(ble_name_ptr)
pop de
add hl,de
ld a,(hl)
jr ble_char_have
ble_char_isnull xor a
ble_char_have push af
ld hl,lfn_char_dest
ld d,0
ld e,b
add hl,de
ld e,(hl)
ld hl,(ble_entry_ptr)
ld d,0
add hl,de
pop af
ld (hl),a
inc hl
cp #ff
jr nz,ble_char_hi0
ld (hl),#ff
jr ble_char_nextk
ble_char_hi0 ld (hl),0
ble_char_nextk inc b
jr ble_charloop
ble_lfn_next ld hl,ble_j
dec (hl)
jp ble_lfn_loop
ble_lfn_done ; short entry goes right after the last LFN entry, at
; wne_entries_buf + lfn_count*32
ld a,(ble_lfn_count)
ld l,a
ld h,0
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl
ld de,wne_entries_buf
add hl,de
ld (ble_entry_ptr),hl
push hl
pop de
ld hl,dir_entry_buf
ld bc,32
ldir
ld hl,target_name
ld de,(ble_entry_ptr)
ld bc,11
ldir
ret
;--- find_free_run: (cur_cluster_hi,cur_cluster_lo already set by
;--- caller to the target directory's first cluster) + wne_slot_count
;--- (already set, e.g. by build_lfn_entries) -> wne_free_index_hi/lo,
;--- the logical 32-byte-entry index (counted from the directory's own
;--- start) where a run of wne_slot_count consecutive free (0x00 or
;--- 0xE5) slots begins. Grows the directory with one freshly
;--- allocated, ZERO-FILLED cluster (via the same find_free_cluster/
;--- write_fat_entry pair stream_write_sector's own on-demand
;--- allocation uses) whenever the existing chain runs out before
;--- enough free slots are found -- zero-filled specifically because a
;--- directory (unlike a file) has no SIZE field bounding valid
;--- entries, so any non-zero leftover byte past the true end could be
;--- mistaken for a real entry by a scan. A=0 ok, A=1 fail (disk
;--- error only -- running out of chain always grows instead of
;--- failing). ---
wne_run_len db 0
wne_run_start_hi dw 0
wne_run_start_lo dw 0
wne_cur_index_hi dw 0
wne_cur_index_lo dw 0
wne_free_index_hi dw 0
wne_free_index_lo dw 0
find_free_run call stream_open
xor a
ld (wne_run_len),a
ld hl,0
ld (wne_cur_index_hi),hl
ld (wne_cur_index_lo),hl
ffr_sector_loop ld a,(stream_eoc)
cp 2
jp nc,ffr_fail
or a
jr z,ffr_have_sector
call ffr_grow_dir
or a
jp nz,ffr_fail
jr ffr_sector_loop
ffr_have_sector call strm_calc_sector
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdread_lba
or a
jr nz,ffr_fail
ld hl,sdbuf
ld b,16
ffr_entloop push bc
ld a,(hl)
or a
jr z,ffr_free
cp #e5
jr z,ffr_free
xor a
ld (wne_run_len),a
jr ffr_advance_index
ffr_free ld a,(wne_run_len)
or a
jr nz,ffr_free_cont
ld de,(wne_cur_index_lo)
ld (wne_run_start_lo),de
ld de,(wne_cur_index_hi)
ld (wne_run_start_hi),de
ffr_free_cont ld a,(wne_run_len)
inc a
ld (wne_run_len),a
ld c,a
ld a,(wne_slot_count)
cp c
jr nz,ffr_advance_index
ld de,(wne_run_start_lo)
ld (wne_free_index_lo),de
ld de,(wne_run_start_hi)
ld (wne_free_index_hi),de
pop bc
xor a
ret
ffr_advance_index
ld de,(wne_cur_index_lo)
inc de
ld (wne_cur_index_lo),de
ld a,d
or e
jr nz,ffr_no_hi_carry
ld de,(wne_cur_index_hi)
inc de
ld (wne_cur_index_hi),de
ffr_no_hi_carry
pop bc
ld de,32
add hl,de
djnz ffr_entloop
call strm_advance
jp ffr_sector_loop
ffr_fail xor a
inc a
ret
;--- ffr_grow_dir: links a freshly allocated, zero-filled cluster onto
;--- the end of the directory chain stream_cluster_hi/lo currently
;--- points at (the true last cluster, since find_free_run only calls
;--- this once stream_eoc says the chain has ended there), then resets
;--- the stream to continue scanning into it. Mirrors stream_write_
;--- sector's own on-demand allocation exactly, plus an explicit
;--- zero_fill_cluster (stream_write_sector doesn't need that for a
;--- FILE, since slack space past its SIZE field is legitimately
;--- ignored -- a directory has no such bound). ---
ffr_grow_dir ld hl,(stream_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(stream_cluster_lo)
ld (cur_cluster_lo),hl
call find_free_cluster
or a
ret nz
ld hl,(stream_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(stream_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,(free_cluster_hi)
ld (new_val_hi),hl
ld hl,(free_cluster_lo)
ld (new_val_lo),hl
call write_fat_entry
or a
ret nz
ld hl,(free_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,#0fff
ld (new_val_hi),hl
ld hl,#ffff
ld (new_val_lo),hl
call write_fat_entry
or a
ret nz
ld hl,(free_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cur_cluster_lo),hl
call zero_fill_cluster
or a
ret nz
ld hl,(free_cluster_hi)
ld (stream_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (stream_cluster_lo),hl
xor a
ld (stream_sec_in_cluster),a
ld (stream_eoc),a
ret
;--- write_name_entries: (cur_cluster_hi,cur_cluster_lo already set by
;--- caller to the target directory's first cluster) + wne_free_index_
;--- hi/lo + wne_slot_count (both already set by find_free_run) +
;--- wne_entries_buf (already filled by build_lfn_entries) -> writes
;--- those wne_slot_count consecutive 32-byte entries to disk at the
;--- located run. Walks the directory stream from its very start again
;--- (simpler and safe -- these directories are small -- than trying to
;--- seek directly to an arbitrary mid-stream sector), patching and
;--- writing back only the sectors that actually contain one or more of
;--- the target slots. A=0 ok, A=1 fail (disk error). ---
wne_w_entptr dw 0
wne_w_entcount db 0
wne_w_dirty db 0
wne_w_this_sec_hi dw 0
wne_w_this_sec_lo dw 0
write_name_entries
call stream_open
ld hl,0
ld (wne_cur_index_lo),hl
wne_w_sector_loop
; stop once cur_index has reached free_index+slot_count (all
; slots placed) -- 16-bit compare only, matching the same
; simplification find_free_run's index tracking already makes
; (this project's directories never approach 65536 entries)
ld hl,(wne_free_index_lo)
ld a,(wne_slot_count)
ld d,0
ld e,a
add hl,de
ld de,(wne_cur_index_lo)
or a
sbc hl,de
jr z,wne_w_done
jr nc,wne_w_continue
wne_w_done xor a
ret
wne_w_continue ld a,(stream_eoc)
or a
jp nz,wne_w_fail ; shouldn't happen -- find_free_run
; already grew the chain if needed
call strm_calc_sector
ld hl,(strm_sector_hi)
ld (wne_w_this_sec_hi),hl
ld hl,(strm_sector_lo)
ld (wne_w_this_sec_lo),hl
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdread_lba
or a
jr nz,wne_w_fail
xor a
ld (wne_w_dirty),a
ld hl,sdbuf
ld (wne_w_entptr),hl
ld a,16
ld (wne_w_entcount),a
wne_w_entloop ld hl,(wne_cur_index_lo)
ld de,(wne_free_index_lo)
or a
sbc hl,de ; hl = cur_index - free_index
jr c,wne_w_notinrange ; cur_index < free_index -- too early
ld a,h
or a
jr nz,wne_w_notinrange ; delta > 255 -- past the run
ld a,l ; a = delta -- the entries_buf slot
; number, IF it's < slot_count
ld b,a
ld a,(wne_slot_count)
cp b
jr c,wne_w_notinrange ; slot_count < delta+1 -- past the run
jr z,wne_w_notinrange ; slot_count == delta -- also past
; (valid range is delta 0..slot_count-1)
ld l,b
ld h,0
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl
ld de,wne_entries_buf
add hl,de ; hl = source (wne_entries_buf+delta*32)
ld de,(wne_w_entptr) ; de = destination (this sdbuf entry)
push de
ld bc,32
ldir
pop de
ld a,1
ld (wne_w_dirty),a
wne_w_notinrange
ld hl,(wne_cur_index_lo)
inc hl
ld (wne_cur_index_lo),hl
ld hl,(wne_w_entptr)
ld de,32
add hl,de
ld (wne_w_entptr),hl
ld hl,wne_w_entcount
dec (hl)
jr nz,wne_w_entloop
ld a,(wne_w_dirty)
or a
jr z,wne_w_skip_write
ld hl,(wne_w_this_sec_hi)
ld de,(wne_w_this_sec_lo)
call set_lba32
call sdwrite_lba
or a
jp nz,wne_w_fail
wne_w_skip_write
call strm_advance
jp wne_w_sector_loop
wne_w_fail xor a
inc a
ret
;--- test_lfn_write: creates a SUBDIRECTORY named "long_name_test_dir"
;--- (18 chars, needs LFN) inside the already-existing TESTDIRA -- the
;--- first real disk write exercising build_lfn_entries/find_free_run/
;--- write_name_entries together. Deliberately scoped to TESTDIRA (a
;--- test-only directory from earlier in this file), not root, so noth-
;--- ing near the card's real ZiFi/downloads data is touched. Verified
;--- externally (Windows Explorer/dir over the card reader showing the
;--- long name correctly, plus chkdsk finding no new corruption) rather
;--- than by a from-scratch long-name search, since that (FENTRY-style
;--- LFN reconstruction) hasn't been built yet -- this test's whole
;--- point is proving the entries this driver just wrote are valid
;--- enough for an independent, already-correct FAT32 reader (Windows'
;--- own) to parse. A=0 ok, A=1 fail (tlw_stage: 0=TESTDIRA not found,
;--- 1=find_free_cluster, 2=write_fat_entry, 3=zero_fill_cluster,
;--- 4=generate_unique_shortname, 5=find_free_run, 6=write_name_entries). ---
;=====================================================================
; Long-name-aware directory search (real FENTRY's job): reconstructs a
; long name from any valid LFN entries immediately preceding a short
; entry (checksum-verified against that short entry -- an orphaned/
; corrupt LFN run, exactly the class of bug that started this whole
; project, is detected and falls back to the short name instead of
; trusting garbage), case-insensitively, matching FAT's own semantics.
;=====================================================================
;--- to_upper: A -> A, uppercased if a-z, unchanged otherwise. Plain
;--- case-fold only, unlike classify_char (which also REJECTS bytes
;--- like '.' that a general string compare must still accept). ---
to_upper cp 'a'
ret c
cp 'z'+1
ret nc
sub 'a'-'A'
ret
;--- str_eq_ci: hl,de -> two NUL-terminated strings -> Z if equal
;--- (case-insensitive), NZ otherwise. ---
str_eq_ci ld a,(hl)
call to_upper
ld b,a
ld a,(de)
call to_upper
cp b
jr nz,seci_ne
or a
jr z,seci_eq
inc hl
inc de
jr str_eq_ci
seci_ne xor a
inc a
ret
seci_eq xor a
ret
;--- render_shortname: hl -> 11-byte short name -> de -> dest buffer,
;--- writes "BASE.EXT" (or just "BASE" if the extension is blank),
;--- trimming trailing spaces, NUL-terminated. hl/de both clobbered. ---
render_shortname
push de
ld b,8
rsn_base ld a,(hl)
cp ' '
jr z,rsn_base_skip
ld (de),a
inc hl
inc de
djnz rsn_base
jr rsn_ext_prep
rsn_base_skip inc hl
djnz rsn_base_skip
rsn_ext_prep ld a,(hl)
cp ' '
jr z,rsn_done
ld a,'.'
ld (de),a
inc de
ld b,3
rsn_ext ld a,(hl)
cp ' '
jr z,rsn_done
ld (de),a
inc hl
inc de
djnz rsn_ext
rsn_done xor a
ld (de),a
pop de
ret
;--- find_by_name: (cur_cluster_hi,cur_cluster_lo already set by caller
;--- to the directory to search) + HL -> NUL-terminated ASCII name ->
;--- A=1 + fbn_cluster_hi/fbn_cluster_lo/fbn_size_hi/fbn_size_lo/
;--- fbn_attr if found, A=0 if not. Also tracks fbn_match_index_lo (the
;--- matched short entry's own logical 32-byte-entry index, counted
;--- from the directory's start) and fbn_match_run_start_lo (the index
;--- where its preceding LFN run started, or the same as fbn_match_
;--- index_lo if no LFN was involved) -- used by core_delfl/core_renam
;--- to know exactly which entries to mark deleted. ---
fbn_name_ptr dw 0
fbn_lfn_buf ds (wne_max_lfn*13)+1
fbn_lfn_checksum db 0
fbn_lfn_seen db 0
fbn_acc_dest dw 0
fbn_cluster_hi dw 0
fbn_cluster_lo dw 0
fbn_size_hi dw 0
fbn_size_lo dw 0
fbn_attr db 0
fbn_render_buf ds 13
fbn_cur_index_lo dw 0
fbn_run_start_lo dw 0
fbn_match_index_lo dw 0
fbn_match_run_start_lo dw 0
find_by_name ld (fbn_name_ptr),hl
call stream_open
xor a
ld (fbn_lfn_seen),a
ld hl,0
ld (fbn_cur_index_lo),hl
fbn_sector_loop ld a,(stream_eoc)
or a
jp nz,fbn_notfound
call strm_calc_sector
ld hl,(strm_sector_hi)
ld de,(strm_sector_lo)
call set_lba32
call sdread_lba
or a
jp nz,fbn_notfound
ld hl,sdbuf
ld b,16
fbn_entloop push bc
ld a,(hl)
or a
jp z,fbn_notfound_pop
cp #e5
jr z,fbn_deleted
push hl
ld de,11
add hl,de
ld a,(hl)
pop hl
cp #0f
jr z,fbn_is_lfn
call fbn_try_match
jp z,fbn_matched
xor a
ld (fbn_lfn_seen),a
jr fbn_next
fbn_is_lfn ld a,(fbn_lfn_seen)
or a
jr nz,fbn_lfn_cont
ld de,(fbn_cur_index_lo)
ld (fbn_run_start_lo),de
fbn_lfn_cont call fbn_accumulate_lfn
jr fbn_next
fbn_deleted xor a
ld (fbn_lfn_seen),a
jr fbn_next
fbn_next pop bc
ld de,32
add hl,de
push hl
ld hl,(fbn_cur_index_lo)
inc hl
ld (fbn_cur_index_lo),hl
pop hl
djnz fbn_entloop
call strm_advance
jp fbn_sector_loop
fbn_notfound_pop
pop bc
fbn_notfound xor a
ret
fbn_matched pop bc
ld hl,(fbn_cur_index_lo)
ld (fbn_match_index_lo),hl
ld a,(fbn_lfn_seen)
or a
jr z,fbn_matched_norun
ld hl,(fbn_run_start_lo)
jr fbn_matched_setrun
fbn_matched_norun
ld hl,(fbn_cur_index_lo)
fbn_matched_setrun
ld (fbn_match_run_start_lo),hl
xor a
inc a
ret
;--- fbn_accumulate_lfn: hl -> a 32-byte LFN entry in sdbuf -> extracts
;--- its 13 characters into fbn_lfn_buf at (seq-1)*13, remembers the
;--- checksum from the highest-sequence ("last"/0x40-flagged) entry. hl
;--- preserved on return. ---
fbn_accumulate_lfn
ld a,1
ld (fbn_lfn_seen),a
ld a,(hl)
and #40
jr z,fbn_acc_nocs
push hl
ld de,13
add hl,de
ld a,(hl)
ld (fbn_lfn_checksum),a
pop hl
fbn_acc_nocs ld a,(hl)
and #1f
or a
ret z
cp wne_max_lfn+1
ret nc
dec a
ld d,0
ld e,a
push hl
ld hl,0
ld b,e
ld a,b
or a
jr z,fbn_acc_mul_done
fbn_acc_mul ld de,13
add hl,de
djnz fbn_acc_mul
fbn_acc_mul_done
ld de,fbn_lfn_buf
add hl,de
ld (fbn_acc_dest),hl
pop hl
ld b,0
fbn_acc_charloop
ld a,b
cp 13
ret nc
push hl
push bc
ld hl,lfn_char_dest
ld d,0
ld e,b
add hl,de
ld e,(hl)
pop bc
pop hl
push hl
ld d,0
add hl,de
ld a,(hl)
ld c,a
inc hl
ld a,(hl)
pop hl
or a
jr nz,fbn_acc_pad
ld a,c
or a
jr z,fbn_acc_null
push hl
ld hl,(fbn_acc_dest)
ld d,0
ld e,b
add hl,de
ld (hl),c
pop hl
jr fbn_acc_pad
fbn_acc_null push hl
ld hl,(fbn_acc_dest)
ld d,0
ld e,b
add hl,de
ld (hl),0
pop hl
fbn_acc_pad inc b
jr fbn_acc_charloop
;--- fbn_try_match: hl -> a 32-byte short entry in sdbuf -> Z if it
;--- matches fbn_name_ptr's target (via the accumulated LFN, checksum-
;--- verified, or the rendered short name otherwise), NZ if not. On a
;--- match, also fills fbn_cluster_hi/lo/fbn_size_hi/lo. hl preserved
;--- on return either way. ---
fbn_try_match ld a,(fbn_lfn_seen)
or a
jr z,fbn_tm_short
push hl
call lfn_checksum
ld b,a
pop hl
ld a,(fbn_lfn_checksum)
cp b
jr nz,fbn_tm_short
push hl
ld de,fbn_lfn_buf
ld hl,(fbn_name_ptr)
call str_eq_ci
pop hl
jr z,fbn_tm_yes
ret
fbn_tm_short push hl
ld de,fbn_render_buf
call render_shortname
ld de,fbn_render_buf
ld hl,(fbn_name_ptr)
call str_eq_ci
pop hl
ret nz
fbn_tm_yes push hl
ld de,11
add hl,de
ld a,(hl)
ld (fbn_attr),a
pop hl
push hl
ld de,20
add hl,de
ld a,(hl)
ld (fbn_cluster_hi),a
inc hl
ld a,(hl)
ld (fbn_cluster_hi+1),a
pop hl
push hl
ld de,26
add hl,de
ld a,(hl)
ld (fbn_cluster_lo),a
inc hl
ld a,(hl)
ld (fbn_cluster_lo+1),a
inc hl
ld a,(hl)
ld (fbn_size_lo),a
inc hl
ld a,(hl)
ld (fbn_size_lo+1),a
inc hl
ld a,(hl)
ld (fbn_size_hi),a
inc hl
ld a,(hl)
ld (fbn_size_hi+1),a
pop hl
xor a
ret
;=====================================================================
; CORE ABI: persistent "active directory" state, matching real ZiFi's
; actual usage pattern confirmed by reading zifi.asm's own call sites
; (load_ini/set_download_dir): FENTRY("zifi") -> SETDIR -> FENTRY
; ("zifi.ini") -> LOAD512 -> SETROOT. Everything above this point
; (find_by_name etc.) takes an explicit cur_cluster_hi/lo per call;
; these wrap that with the persistent state ZiFi's own calling
; convention needs.
;=====================================================================
active_dir_hi dw 0
active_dir_lo dw 0
;--- core_setroot: makes the root directory the active directory. ---
core_setroot ld hl,(root_cluster_hi)
ld (active_dir_hi),hl
ld hl,(root_cluster_lo)
ld (active_dir_lo),hl
ret
;--- core_setdir: makes the directory the last core_fentry call found
;--- (fbn_cluster_hi/lo) the active directory -- matches real SETDIR's
;--- doc, "Set DIR found by ENTRY active". ---
core_setdir ld hl,(fbn_cluster_hi)
ld (active_dir_hi),hl
ld hl,(fbn_cluster_lo)
ld (active_dir_lo),hl
ret
;--- core_fentry: HL -> NUL-terminated ASCII name -> searches the
;--- ACTIVE directory (active_dir_hi/lo, not a caller-supplied cluster)
;--- via find_by_name. A=1+found (fbn_cluster_hi/lo/fbn_size_hi/lo
;--- set), A=0 not found. ---
core_fentry push hl
ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
pop hl
jp find_by_name
;--- test_setdir_flow: exercises the real ZiFi call pattern end to end
;--- -- SETROOT, FENTRY("TESTDIRA"), SETDIR, FENTRY("long_name_test_
;--- dir3") -- using ONLY the persistent active_dir state (no explicit
;--- cur_cluster pokes from the caller), matching how zifi.asm's own
;--- load_ini/set_download_dir actually call these. A=0 ok, A=1 fail
;--- (tsdf_stage: 0=TESTDIRA not found, 1=long_name_test_dir3 not found
;--- inside it). ---
;=====================================================================
; CORE ABI: LOAD512/SAVE512 -- paged-memory bulk transfer.
;
; SAVE512: i: C=page number, HL=a READY slot-1 CPU address
; (0x4000-0x7FFF, used directly, no offset). Confirmed via zifi.asm's
; real call site (zifi_get, ~line 306-315: "call set_page1" then
; thread.adress = get_buffer = #4000, passed as-is to SAVE512) --
; confirmed as the real cause of "noise mixed with melody" in
; downloaded tracks (see [[project-zifi-custom-sd-driver]]) when this
; used to be assumed as a 0-based slot-3 offset instead.
;
; LOAD512: i: C=page number, HL=a 0-based OFFSET (this driver adds
; 0xC000 -- slot 3 -- internally). NOT the same slot/convention as
; SAVE512 -- confirmed via zifi.asm's ONLY real LOAD512 call site
; (load_ini, ~line 3970: C=download_page, HL=0x0000) cross-checked
; against parse_ini (~line 3978: reads the result back via "call
; set_page3 / ld hl,#c000") -- i.e. LOAD512's destination really is
; PAGE3/0xC000, unlike SAVE512's PAGE1/0x4000. Sharing SAVE512's
; convention here was an unverified assumption-by-symmetry that broke
; zifi.ini parsing ("Error parsing ini file") once SAVE512 itself
; started working correctly and load_ini could actually run.
;
; Both: B=block count (512B each), always page-boundary-aligned (a
; page is exactly 32 blocks). o: C,HL=new position (advanced by
; B*512 bytes, wrapping to the next page every 32 blocks); A=#0F on
; EndOfChain. Caller must have already positioned the stream via
; stream_open on the target file/directory's first cluster.
;=====================================================================
page1_port equ #11af
sv512_page db 0
sv512_off dw 0
sv512_count db 0
;--- core_save512: writes B blocks FROM (page C, offset HL) TO the
;--- current stream position, advancing the stream automatically
;--- (allocating new clusters on demand, same as stream_write_sector_from
;--- always has). A=0 ok, A=#0F if a block's disk write failed (can't
;--- grow the chain -- e.g. card full). ---
;--- No di/ei needed here anymore (matches the original closed
;--- WDFCVBI2.COD driver's own zero-di/ei SAVE512/LOAD512): the
;--- page1_port+LDIR dance that needed PAGE1 held stable across the
;--- whole copy is GONE -- stream_write_sector_from DMAs each block
;--- straight from (sv512_page,sv512_off) to the SD card, addressed by
;--- physical page number, never touching the CPU's slot mapping at
;--- all. That mapping was the actual hazard: set_music_pages_lite
;--- repages PAGE0/1/3 on every interrupt during playback, and a
;--- write-only page port can't be restored afterward -- so any
;--- interrupt firing mid-LDIR could silently redirect part of the
;--- 512-byte copy to whatever page music had just switched to. DMA
;--- sidesteps that mechanism entirely. See
;--- [[project-zifi-custom-sd-driver]] "noise mixed with melody". ---
core_save512 ld a,c
ld (sv512_page),a
ld (sv512_off),hl
ld a,b
ld (sv512_count),a
sv512_loop ld a,(sv512_count)
or a
jr z,sv512_done
ld a,(sv512_page)
ld (dma_xfer_page),a
ld hl,(sv512_off)
ld (dma_xfer_off),hl
call stream_write_sector_from
or a
jr nz,sv512_eoc
call sv512_advance
ld hl,sv512_count
dec (hl)
jr sv512_loop
sv512_done ld a,(sv512_page)
ld c,a
ld hl,(sv512_off)
xor a
ret
sv512_eoc ld a,(sv512_page)
ld c,a
ld hl,(sv512_off)
ld a,#0f
ret
;--- core_load512: reads B blocks FROM the current stream position
;--- INTO (page C, offset HL), advancing the stream. A=0 ok, A=#0F if
;--- the chain ended (a real read error is also reported as #0F here --
;--- ZiFi's own real call sites don't actually branch on this return
;--- value, they track position via a separately precomputed block
;--- count instead, so exact error-code fidelity beyond "did it stop
;--- early" isn't load-bearing).
;---
;--- SAME slot/offset convention as SAVE512 (PAGE1/direct address),
;--- but its OWN SEPARATE lv512_page/off/count/advance state -- NOT
;--- shared with sv512_* anymore. An earlier version shared state
;--- (kept this byte-for-byte identical to core_save512, to sidestep a
;--- since-abandoned Hrust compression mystery -- see
;--- [[project-zifi-custom-sd-driver]]), but that's a real hazard: a
;--- download's SAVE512 can be mid-transfer (multiple blocks, DI-
;--- protected only for ONE block at a time between di/ei pairs) while
;--- something else (music/playlist code) calls LOAD512 in between --
;--- confirmed on hardware as the cause of renewed "noise mixed with
;--- melody" plus playlist-switch hangs after the shared-state version
;--- shipped. Separate state costs ~15 bytes now that the body is
;--- compressed with a byte-for-byte self-verified PackBits RLE
;--- instead of Hrust (see zc_sd_driver.asm's drv_dos_swp) -- no more
;--- mystery size-dependent decoder bug to worry about. ---
lv512_page db 0
lv512_off dw 0
lv512_count db 0
core_load512 ld a,c
ld (lv512_page),a
ld (lv512_off),hl
ld a,b
ld (lv512_count),a
lv512_loop ld a,(lv512_count)
or a
jr z,lv512_done
ld a,(lv512_page)
ld (dma_xfer_page),a
ld hl,(lv512_off)
ld (dma_xfer_off),hl
call stream_read_sector_to
or a
jr nz,lv512_eoc
call lv512_advance
ld hl,lv512_count
dec (hl)
jr lv512_loop
;--- lv512_advance: same wrap logic as sv512_advance (slot-1,
;--- 0x4000-0x7FFF, one page = 32 blocks), just against lv512_*'s own
;--- separate state. ---
lv512_advance ld hl,(lv512_off)
ld de,512
add hl,de
ld a,h
cp #80
jr c,lv512_nowrap
sub #40 ; preserve the overflow remainder instead
ld h,a ; of resetting to exactly #4000 -- see
ld a,(lv512_page) ; sv512_advance's own comment below
inc a
ld (lv512_page),a
lv512_nowrap ld (lv512_off),hl
ret
lv512_done ld a,(lv512_page)
ld c,a
ld hl,(lv512_off)
xor a
ret
lv512_eoc ld a,(lv512_page)
ld c,a
ld hl,(lv512_off)
ld a,#0f
ret
;--- sv512_advance: sv512_off += 512, wrapping to sv512_page+1 (back to
;--- slot-1 offset 0x4000, NOT 0 -- see header comment on the real
;--- 0x4000-0x7FFF convention) once it would leave the 0x4000-0x7FFF
;--- range, i.e. reach 0x8000 (one page = exactly 32 blocks).
;---
;--- The wrap subtracts #40 from H (preserving whatever this add
;--- overshot #8000 by) instead of hardcoding H:=#40 -- CP doesn't
;--- touch A, so A still holds the pre-wrap H here, free to reuse.
;--- create_filename's ".EXT"-marker handling can start thread.adress
;--- (and so sv512_off, on the very first call) a few bytes PAST
;--- #4000 (skipping a leading ".TRD"/".SCL"/etc marker some servers
;--- prepend to the real payload) -- a hardcoded reset to exactly
;--- #4000 silently drops that remainder on the FIRST page wrap only
;--- (every wrap after that starts page-aligned, so the bug is
;--- invisible again), splicing 4 stray bytes from the tail of the
;--- block just written into the next one and losing the file's real
;--- last 4 bytes. Confirmed via a live memory-vs-disk diff on a real
;--- multi-page download (2026-09-09): RAM was correct throughout,
;--- only the on-disk copy had the extra bytes -- see
;--- [[project-zifi-custom-sd-driver]]. Same fix applied to
;--- lv512_advance above (LOAD512's own copy of this logic). ---
sv512_advance ld hl,(sv512_off)
ld de,512
add hl,de
ld a,h
cp #80
jr c,sv512_nowrap
sub #40
ld h,a
ld a,(sv512_page)
inc a
ld (sv512_page),a
sv512_nowrap ld (sv512_off),hl
ret
;--- test_load512_save512: creates "P512TEST.BIN" inside TESTDIRA (via
;--- SETROOT/FENTRY/SETDIR -- dogfooding that machinery, not just the
;--- LFN pipeline directly) if it doesn't already exist, fills page 0
;--- offset 0 (0xC000) with a known byte, SAVE512s one block into the
;--- file, corrupts page 0 with a DIFFERENT byte (so a stale-memory
;--- false pass is impossible), LOAD512s the block back, and checks the
;--- original byte reappeared. A=0 ok, A=1 fail (tls_stage: 0=TESTDIRA
;--- not found, 1=find_free_cluster, 2=write_fat_entry, 3=zero_fill_
;--- cluster, 4=generate_unique_shortname, 5=find_free_run, 6=write_
;--- name_entries, 7=SAVE512 unexpected EOC, 8=LOAD512 unexpected EOC,
;--- 9=readback mismatch). ---
;=====================================================================
; CORE ABI: MKDIR/MKFILE -- create a directory/file with a free-form
; name in the ACTIVE directory (active_dir_hi/lo -- caller must have
; already SETROOT/SETDIR'd there), using the VFAT machinery (duplicate-
; guard via find_by_name, short-name generation, LFN entries). Z=
; success, NZ=error (A: 1=name not valid, 2=index fatality [unused --
; nothing in this driver currently produces it], 3=already exists,
; 255=unknown error) -- matches real MKDIR/MKFILE's documented ABI.
; Both are generalized versions of test_lfn_write's own (hardware-
; confirmed) logic, parameterized by name and active_dir instead of a
; fixed test name/TESTDIRA.
;=====================================================================
cmk_name_ptr dw 0
cmk_cluster_hi dw 0
cmk_cluster_lo dw 0
cmk_sec_hi dw 0
cmk_sec_lo dw 0
;--- core_mkdir: HL -> {name(1-255), 0} -> creates a subdirectory (with
;--- its own "."/".." entries) in the active directory. ---
core_mkdir ld (cmk_name_ptr),hl
ld b,0
cmkd_lenloop ld a,(hl)
or a
jr z,cmkd_lendone
inc hl
inc b
jr nz,cmkd_lenloop
cmkd_toolong ld a,1
or a
ret
cmkd_lendone ld a,b
or a
jr z,cmkd_toolong
ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
ld hl,(cmk_name_ptr)
call find_by_name
or a
jr z,cmkd_alloc
ld a,3
or a
ret
cmkd_alloc ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
call find_free_cluster
or a
jr z,cmkd_have_cl
ld a,255
or a
ret
cmkd_have_cl ld hl,(free_cluster_hi)
ld (cmk_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cmk_cluster_lo),hl
ld hl,(free_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,#0fff
ld (new_val_hi),hl
ld hl,#ffff
ld (new_val_lo),hl
call write_fat_entry
or a
jr z,cmkd_zf
ld a,255
or a
ret
cmkd_zf ld hl,(cmk_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(cmk_cluster_lo)
ld (cur_cluster_lo),hl
call zero_fill_cluster
or a
jr z,cmkd_dots
ld a,255
or a
ret
cmkd_dots call rtc_to_fat_datetime
call clear_dir_entry
call stamp_dir_entry_datetime
ld hl,dot_name
ld de,dir_entry_buf
ld bc,11
ldir
ld a,#10
ld (dir_entry_buf+11),a
ld hl,(cmk_cluster_hi)
ld (dir_entry_buf+20),hl
ld hl,(cmk_cluster_lo)
ld (dir_entry_buf+26),hl
ld hl,(cmk_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(cmk_cluster_lo)
ld (cur_cluster_lo),hl
call cluster_to_sector
ld hl,(fza_sector_hi)
ld (cmk_sec_hi),hl
ld hl,(fza_sector_lo)
ld (cmk_sec_lo),hl
ld hl,(cmk_sec_hi)
ld de,(cmk_sec_lo)
call set_lba32
call sdread_lba
or a
jr z,cmkd_dot_ok
ld a,255
or a
ret
cmkd_dot_ok ld hl,dir_entry_buf
ld de,sdbuf
ld bc,32
ldir
call clear_dir_entry
call stamp_dir_entry_datetime
ld hl,dotdot_name
ld de,dir_entry_buf
ld bc,11
ldir
ld a,#10
ld (dir_entry_buf+11),a
ld hl,(active_dir_hi)
ld (dir_entry_buf+20),hl
ld hl,(active_dir_lo)
ld (dir_entry_buf+26),hl
ld hl,dir_entry_buf
ld de,sdbuf+32
ld bc,32
ldir
ld hl,(cmk_sec_hi)
ld de,(cmk_sec_lo)
call set_lba32
call sdwrite_lba
or a
jr z,cmkd_ownentry
ld a,255
or a
ret
cmkd_ownentry call clear_dir_entry
call stamp_dir_entry_datetime
ld a,#10
ld (dir_entry_buf+11),a
ld hl,(cmk_cluster_hi)
ld (dir_entry_buf+20),hl
ld hl,(cmk_cluster_lo)
ld (dir_entry_buf+26),hl
ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
ld hl,(cmk_name_ptr)
call filter_name_to_83
call generate_unique_shortname
or a
jr z,cmkd_buildlfn
ld a,255
or a
ret
cmkd_buildlfn ld hl,(cmk_name_ptr)
call build_lfn_entries
ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
call find_free_run
or a
jr z,cmkd_final
ld a,255
or a
ret
cmkd_final ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
call write_name_entries
or a
ret z
ld a,255
or a
ret
;--- core_mkfile: HL -> {flag(1), length(4,LE32), name(1-255), 0} ->
;--- creates a file with that name and declared size in the active
;--- directory, allocating+EOC-marking its first cluster. Does NOT
;--- write any data or zero-fill the cluster (matching plain FAT
;--- practice for files -- unlike a directory, slack space beyond the
;--- declared SIZE is legitimately ignorable) -- the caller writes real
;--- data afterward via SAVE512, same as real ZiFi's own MKFILE-then-
;--- SAVE512 pattern (save_downloaded_file). ---
cmkf_len_hi dw 0
cmkf_len_lo dw 0
core_mkfile inc hl ; skip the flag byte (unused here --
; this driver doesn't yet distinguish
; anything by it)
ld a,(hl)
ld (cmkf_len_lo),a
inc hl
ld a,(hl)
ld (cmkf_len_lo+1),a
inc hl
ld a,(hl)
ld (cmkf_len_hi),a
inc hl
ld a,(hl)
ld (cmkf_len_hi+1),a
inc hl
ld (cmk_name_ptr),hl ; hl now -> the name itself
push hl
ld b,0
cmkf_lenloop ld a,(hl)
or a
jr z,cmkf_lendone
inc hl
inc b
jr nz,cmkf_lenloop
pop hl
ld a,1
or a
ret
cmkf_lendone pop hl
ld a,b
or a
jr nz,cmkf_havelen
ld a,1
or a
ret
cmkf_havelen ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
ld hl,(cmk_name_ptr)
call find_by_name
or a
jr z,cmkf_alloc
ld a,3
or a
ret
cmkf_alloc ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
call find_free_cluster
or a
jr z,cmkf_havecl
ld a,255
or a
ret
cmkf_havecl ld hl,(free_cluster_hi)
ld (cmk_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cmk_cluster_lo),hl
ld hl,(free_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(free_cluster_lo)
ld (cur_cluster_lo),hl
ld hl,#0fff
ld (new_val_hi),hl
ld hl,#ffff
ld (new_val_lo),hl
call write_fat_entry
or a
jr z,cmkf_entry
ld a,255
or a
ret
cmkf_entry call rtc_to_fat_datetime
call clear_dir_entry
call stamp_dir_entry_datetime
ld a,#20 ; ATTR_ARCHIVE
ld (dir_entry_buf+11),a
ld hl,(cmk_cluster_hi)
ld (dir_entry_buf+20),hl
ld hl,(cmk_cluster_lo)
ld (dir_entry_buf+26),hl
ld hl,(cmkf_len_lo)
ld (dir_entry_buf+28),hl
ld hl,(cmkf_len_hi)
ld (dir_entry_buf+30),hl
ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
ld hl,(cmk_name_ptr)
call filter_name_to_83
call generate_unique_shortname
or a
jr z,cmkf_buildlfn
ld a,255
or a
ret
cmkf_buildlfn ld hl,(cmk_name_ptr)
call build_lfn_entries
ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
call find_free_run
or a
jr z,cmkf_final
ld a,255
or a
ret
cmkf_final ld hl,(active_dir_hi)
ld (cur_cluster_hi),hl
ld hl,(active_dir_lo)
ld (cur_cluster_lo),hl
call write_name_entries
or a
jr z,cmkf_open_stream
ld a,255
or a
ret
; Real zifi.asm calls SAVE512 immediately after MKFILE with no FENTRY/
; SEEK0 in between (matching FENTRY's own "SEEK0 is automatically
; called" convention, which drv_fentry implements by opening a stream
; on its match) -- MKFILE must do the same on the file it just created,
; or SAVE512 writes into whatever stream was left open by the PREVIOUS
; operation (e.g. the parent directory itself) instead of the new
; file's own cluster. Confirmed on hardware as a real bug: downloaded
; files read back as stale leftover disk content (an old test string in
; one, raw directory-entry bytes in another) because their own cluster
; was never actually written.
cmkf_open_stream
ld hl,(cmk_cluster_hi)
ld (cur_cluster_hi),hl
ld hl,(cmk_cluster_lo)
ld (cur_cluster_lo),hl
call stream_open
xor a
ret
;--- test_core_mkdir_mkfile: exercises the real ABI end to end --
;--- SETROOT/FENTRY("TESTDIRA")/SETDIR, MKDIR("core_test_dir") (or
;--- find it if a prior run already made it -- A=3 is treated as
;--- success here, not a test failure), FENTRY+SETDIR into it, then
;--- MKFILE via a real {flag,length(4),name,0} buffer matching zifi.
;--- asm's own FILE buffer layout. Does NOT write any file data (that's
;--- SAVE512's job, already proven separately) -- this only proves
;--- MKDIR/MKFILE themselves produce valid, correctly-placed entries.
;--- A=0 ok, A=1 fail (tcmm_stage: 0=TESTDIRA not found, 1=MKDIR, 2=
;--- FENTRY into the new dir, 3=MKFILE). ---
dot_name db ". " ; 11 bytes
dotdot_name db ".. " ; 11 bytes
clear_dir_entry
ld hl,dir_entry_buf
ld de,dir_entry_buf+1
ld bc,31
ld (hl),0
ldir
ret
body_end:
SAVEBIN "zc_sd_driver_body.bin",#0000,body_end-#0000