4393 lines
113 KiB
NASM
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
|