Document FAT32 short-name collision bug in README; add raw SD driver diagnostics

The closed WDFCVBI2.COD driver's short-name generator can assign the
same 8.3 name to two different long-name directory entries, corrupting
the FAT directory structure (confirmed live on hardware via chkdsk:
2026_09_01 and 2026_0~1.--0 are the same day, split across two colliding
entries). Since the driver ships as a binary with no source, documented
the issue in README and started writing an open replacement SD/FAT32
driver from scratch.

This commit adds the in-progress diagnostic scaffolding for that new
driver (test_sd_raw/test_sd_raw2 in zifi.asm, marked with ;DIAG:
comments): a standalone SD-card SPI implementation (ports CONF=#77/
DATA=#57) that inits the card, reads sectors via CMD18+CMD12, and
parses the FAT32 boot sector/root directory independently of the
closed driver. Still under active debugging - the command handshake
(R1/token) checks out correct but the bulk data payload comes back
wrong, root cause not yet found. Not wired into the real boot path
beyond the diagnostic hook, and not ready to replace the closed driver.
This commit is contained in:
2026-09-02 16:59:27 +03:00
parent 08dc6ab6a4
commit aa3247a1dc
3 changed files with 793 additions and 0 deletions
+2
View File
@@ -51,6 +51,8 @@ AT+CIPSNTPTIME? — получить текущее врем
- теперь DMA-очистка под индикатор громкости дожидается завершения перед тем, как CPU начинает рисовать поверх той же области (была гонка записи);
- исправил переполнение буфера имени файла (`create_filename`): при скачивании имя файла из ссылки копировалось в 48-байтный буфер без проверки длины, и если оно было длиннее — переполнение затирало соседние переменные (`zifi.ini`, имена служебных папок) прямо в памяти. Теперь копирование корректно обрезается на 48 символах. Проверено на железе: создание папки и файла с длинными именами (в т.ч. ровно 48 символов) и последующее чтение содержимого — всё работает штатно, никакой драйверной обрезки до 8 символов нет.
**Известная серьёзная проблема (пока не исправлена): порча имён папок на FAT32.** Штатный драйвер SD-карты (`_sd/WDFCVBI2.COD`, закрытый бинарник без исходников, автор — "Hacker VBI") при создании папки/файла с длинным именем иногда генерирует короткое имя 8.3, совпадающее с уже существующей записью в каталоге — получаются две разные папки с одинаковым коротким именем на диске. Windows chkdsk потом это обнаруживает и переименовывает вторую запись в `<имя>.--N`, а по факту это два разных физических каталога, которые должны были быть одним (например, файлы за один день оказываются раскиданы между `2026_09_01` и `2026_0~1.--0`). Нашёл и подтвердил дизассемблированием конкретный баг в алгоритме генерации числового суффикса `~N` внутри драйвера, но исправить сам драйвер нельзя — исходников нет, только бинарник. Сейчас пишу отдельный, открытый SD/FAT32-драйвер на замену (реализует только те ~15 функций, которые реально использует ZiFi) — работа в процессе, ещё не готово к использованию.
**Известная нерешённая мелочь:** курсор мыши (аппаратный спрайт TSU) обрывается по фиксированной строке экрана прямо над нижней панелью управления — похоже на границу, завязанную на тайминг переключения видеорежима или на аппаратный бордюр, а не на что-то, что двигается вместе с содержимым панели. Я не нашёл способ поправить это без риска сломать что-то ещё, оставил как есть.
Binary file not shown.
+791
View File
@@ -61,6 +61,8 @@ start
ld (#beff),hl
call gfx_init
call set_text_colors
call test_sd_raw ;DIAG: standalone SD read test, remove once verified
call test_sd_raw2 ;DIAG: standalone SD FAT32 file read test, remove once verified
call load_ini
IF cable_zifi
call psb_start
@@ -3789,6 +3791,795 @@ parsed_link ds #080
modem_command ds 256+128
current_paging_url ds 150
;DIAG: standalone raw SD-card hardware test - bypasses WDFCVBI2 entirely,
;DIAG: talks to the SD controller directly via ports CONF=#77/DATA=#57.
;DIAG: Goal: verify our own init+read code can talk to the hardware at all,
;DIAG: before writing more of a replacement driver. Reads LBA 0 and checks
;DIAG: for the standard 55 AA boot-sector signature. Remove after testing.
test_sd_raw
call set_Textpage
;DIAG: pre-fill the buffer with a recognizable marker before touching
;DIAG: the card at all -- if this marker survives a "successful" read,
;DIAG: the read loop isn't actually writing to sdraw_buf
ld hl,sdraw_buf
ld de,sdraw_buf+1
ld (hl),#ee
ld bc,511
ldir
ld a,%00000001
ld (sdraw_csl_val),a ;DIAG: interface 1 only -- interface 2 (#0B) hung
;DIAG: the whole machine on real hardware, do not retry it
di ;DIAG: off_int_dma alone didn't help -- try a real interrupt
;DIAG: mask, in case int_main pages away our resident slot
;DIAG: mid-read (not just an SPI timing issue)
call sdraw_init
or a
jp nz,test_sd_raw_noc
call sdraw_read0
or a
jp nz,test_sd_raw_rderr
;DIAG: keep DI held through the prints too now -- ei was previously
;DIAG: right here, leaving a window for int_main to run between the
;DIAG: read and the dump; test whether that window is the culprit
;DIAG: show the raw command handshake bytes -- R1 response after
;DIAG: CMD18, and the token byte WTDO settled on (should be #FE)
ld a,(sdraw_last_r1)
ld de,sdraw_hexbuf3
call sdraw_hexch_pair
ld a,(sdraw_last_token)
ld de,sdraw_hexbuf3+2
call sdraw_hexch_pair
xor a
ld (sdraw_hexbuf3+4),a
ld (sdraw_hexbuf3+5),a
ld hl,sdraw_r1tok_msg
ld b,1
call zifi_echo
ld hl,sdraw_hexbuf3
ld b,1
call zifi_echo
;DIAG: always dump [0..15] and [508..511] before judging the signature,
;DIAG: so a mismatch shows real vs. garbage/shifted bytes instead of guessing
ld hl,sdraw_buf
ld de,sdraw_hexbuf
ld b,16
call sdraw_hexdump
ld hl,sdraw_head_msg
ld b,1
call zifi_echo
ld hl,sdraw_hexbuf
ld b,1
call zifi_echo
ld hl,sdraw_buf+508
ld de,sdraw_hexbuf2
ld b,4
call sdraw_hexdump
ld hl,sdraw_tail_msg
ld b,1
call zifi_echo
ld hl,sdraw_hexbuf2
ld b,1
call zifi_echo
;DIAG: read LBA0 again, right now, and dump it too -- same bytes as
;DIAG: above would mean a deterministic bug in our code, not a flaky card
call sdraw_read0
or a
jr nz,test_sd_raw_rderr
ld hl,sdraw_buf
ld de,sdraw_hexbuf
ld b,16
call sdraw_hexdump
ld hl,sdraw_head2_msg
ld b,1
call zifi_echo
ld hl,sdraw_hexbuf
ld b,1
call zifi_echo
ld hl,sdraw_buf+510
ld a,(hl)
cp #55
jr nz,test_sd_raw_sigerr
inc hl
ld a,(hl)
cp #aa
jr nz,test_sd_raw_sigerr
;DIAG: now read a totally different LBA (1000, not 0) -- does the
;DIAG: garbage pattern shift/change, or stay pinned to the same values?
ld hl,0
ld de,1000
call sdraw_readlba
or a
jr nz,test_sd_raw_rderr
ld hl,sdraw_buf
ld de,sdraw_hexbuf
ld b,16
call sdraw_hexdump
ld hl,sdraw_lba1000_msg
ld b,1
call zifi_echo
ld hl,sdraw_hexbuf
ld b,1
call zifi_echo
ei ;DIAG: only now, after every print, re-enable interrupts
ld hl,sdraw_ok_msg
ld b,1
call zifi_echo
ret
test_sd_raw_noc
ei
ld hl,sdraw_noc_msg
ld b,1
call zifi_echo
ret
test_sd_raw_rderr
ei
ld hl,sdraw_rderr_msg
ld b,1
call zifi_echo
ret
test_sd_raw_sigerr
ei
ld hl,sdraw_sigerr_msg
ld b,1
call zifi_echo
ret
sdraw_noc_msg db 'SD raw test: FAIL, no card found',0,0
sdraw_rderr_msg db 'SD raw test: FAIL, read error',0,0
sdraw_sigerr_msg db 'SD raw test: no 55 AA signature (see above)',0,0
sdraw_ok_msg db 'SD raw test: OK, signature valid',0,0
sdraw_head_msg db 'sector0[0..15]=',0,0
sdraw_head2_msg db '2nd read[0..15]=',0,0
sdraw_lba1000_msg db 'LBA1000[0..15]=',0,0
sdraw_tail_msg db 'sector0[508..511]=',0,0
sdraw_r1tok_msg db 'R1,token=',0,0
sdraw_iface_msg db 'interface CS_ON=',0,0
sdraw_hexbuf ds 40
sdraw_hexbuf2 ds 16
sdraw_hexbuf3 ds 8
sdraw_last_r1 ds 1
sdraw_last_token ds 1
sdraw_CONF equ #77
sdraw_DATA equ #57
;--- convert A into 2 hex ASCII chars at (DE) ---
sdraw_hexch_pair
push af
rra
rra
rra
rra
call sdraw_hexch
ld (de),a
inc de
pop af
call sdraw_hexch
ld (de),a
ret
;--- hex dump helper: HL=src, DE=dest ascii buffer, B=byte count ---
sdraw_hexdump
ld a,(hl)
push af
rra
rra
rra
rra
call sdraw_hexch
ld (de),a
inc de
pop af
call sdraw_hexch
ld (de),a
inc de
inc hl
djnz sdraw_hexdump
xor a
ld (de),a
inc de
ld (de),a
ret
sdraw_hexch
and #0f
cp 10
jr c,1f
add a,7
1 add a,'0'
ret
;--- low-level SD-over-SPI primitives, ported from source/DSDZC.ASM (Wild
;--- Commander Improved) which targets the same ZX Evolution SD controller.
sdraw_csh
push bc
push af
ld bc,sdraw_CONF
ld a,%00000011
out (c),a
ld bc,sdraw_DATA
ld a,#ff
out (c),a
pop af
pop bc
ret
sdraw_csl_val db %00000001 ;DIAG: interface 1 by default; test_sd_raw
;DIAG: flips this to %00001011 (interface 2)
;DIAG: if interface 1's signature check fails
sdraw_csl
push bc
push af
ld bc,sdraw_CONF
ld a,(sdraw_csl_val)
out (c),a
ld bc,sdraw_DATA
ld a,#ff
out (c),a
pop af
pop bc
jp sdraw_wait
sdraw_wait
push bc
push af
ld bc,sdraw_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 af
pop bc
ret
sdraw_wtdo
push bc
push de
ld bc,sdraw_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
sdraw_resp
push de
push bc
ld bc,sdraw_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
sdraw_cycl ; DE = number of #FF clock bytes to send
ld bc,sdraw_DATA
1 ld a,#ff
out (c),a
dec de
ld a,d
or e
jr nz,1b
ret
;--- send a pre-built 6-byte command frame pointed to by HL (real CRC).
;--- Matches D:\ST\ZP\PLAY60.ASM's actual shipped timing (not the blog
;--- article, which the article's own author -- who wrote that player --
;--- says has bugs): two dummy IN (C) reads before the frame ("; savelij"
;--- in the source), and a NOP between each byte sent. Without this, the
;--- handshake (R1/token) still looks valid but the payload comes back as
;--- deterministic garbage -- exactly what we were seeing.
sdraw_cmd
call sdraw_csh
call sdraw_csl
ld bc,sdraw_DATA
in (c)
in (c)
outi
nop
outi
nop
outi
nop
outi
nop
outi
nop
outi
jp sdraw_resp
;--- send a simple command: A=opcode, all-zero args, CRC=#FF ---
sdraw_cmdo
call sdraw_csh
call sdraw_csl
sdraw_cmdx
ld bc,sdraw_DATA
in (c)
in (c)
out (c),a
xor a
out (c),a
nop
out (c),a
nop
out (c),a
nop
out (c),a
ld a,#ff
out (c),a
ret
sdraw_cmd00 db %01000000+0,0,0,0,0,#95
sdraw_cmd08 db %01000000+8,0,0,1,#aa,#87
sdraw_cmd16 db %01000000+16,0,0,2,0,#ff
sdraw_do_cmd0 ld hl,sdraw_cmd00
jp sdraw_cmd
sdraw_do_cmd8 ld hl,sdraw_cmd08
jp sdraw_cmd
sdraw_do_cmd16 ld hl,sdraw_cmd16
jp sdraw_cmd
sdraw_do_cmd1 ld a,%01000000+1
call sdraw_cmdo
jp sdraw_resp
sdraw_do_cmd55 ld a,%01000000+55
call sdraw_cmdo
jp sdraw_resp
;--- ACMD41: H = argument high byte (0, or #40 for HCS), rest zero ---
sdraw_do_acmd41
call sdraw_do_cmd55
call sdraw_csh
call sdraw_csl
ld bc,sdraw_DATA
in (c)
in (c)
ld a,%01000000+41
out (c),a
ld l,0
out (c),h
nop
out (c),l
nop
out (c),l
nop
out (c),l
dec l
out (c),l
jp sdraw_resp
;--- card init: CMD0/CMD8/ACMD41 (SDv2), falls back to ACMD41/CMD1 (SDv1/MMC)
; A=0 on success, A=1 on failure
sdraw_init
call sdraw_csh
ld de,512+10
call sdraw_cycl
ld de,8000
1 dec de
ld a,d
or e
jp z,sdraw_init_fail
call sdraw_do_cmd0
jr nz,1b
dec a
jp nz,1b
call sdraw_do_cmd8
push af
ld bc,sdraw_DATA
in e,(c)
nop
in e,(c)
nop
in h,(c)
nop
in l,(c)
pop af
jr nz,1b
bit 2,a
jr z,sdraw_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 sdraw_do_acmd41
jr nz,2b
cp 1
jr z,2b
or a
jr nz,3f
jr sdraw_fbs
3 ld de,8000
4 dec de
ld a,d
or e
jr z,sdraw_init_fail
call sdraw_do_cmd1
jr nz,4b
cp 1
jr z,4b
or a
jr nz,sdraw_init_fail
sdraw_fbs call sdraw_do_cmd16
jr nz,sdraw_init_fail
or a
jr nz,sdraw_init_fail
jr sdraw_init_ok
;--- SDv2 path: verify the 0x1AA echo pattern from CMD8, then ACMD41+HCS ---
sdraw_v2 ld de,#01aa
or a
sbc hl,de
jr nz,sdraw_init_fail
ld de,8000
5 dec de
ld a,d
or e
jr z,sdraw_init_fail
ld h,#40
call sdraw_do_acmd41
jr nz,5b
cp 1
jr z,5b
or a
jr nz,sdraw_init_fail
; not distinguishing byte/block addressing (CMD58/CCS) here --
; irrelevant for reading LBA 0, which is address 0 either way.
sdraw_init_ok
call sdraw_csh
xor a
ret
sdraw_init_fail
call sdraw_csh
xor a
inc a
ret
;--- read LBA 0 (single 512-byte sector) into sdraw_buf ---
; A=0 on success, A=1 on failure
sdraw_read0
ld hl,0
ld de,0
jp sdraw_readlba
;--- read an arbitrary LBA (single 512-byte sector) via CMD18+CMD12 into
;--- sdraw_buf. Matches the proven reference implementation in D:\ST\ZP's
;--- Z-Player source (SD-CARD2.TXT / PLAY60.ASM) byte-for-byte in approach:
;--- CMD18 (not CMD17!) for the read, and INI (not manual IN+LD(HL),A) for
;--- the bulk 512-byte transfer.
; HL = LBA bits31-16, DE = LBA bits15-0
; A=0 on success, A=1 on failure
sdraw_readlba
call sdraw_csh
call sdraw_csl
ld bc,sdraw_DATA
in (c) ;DIAG: settling reads + inter-byte NOPs, matching
in (c) ;DIAG: PLAY60.ASM's CMzz exactly (see sdraw_cmd)
ld a,%01000000+18
out (c),a
nop
out (c),h
nop
out (c),l
nop
out (c),d
nop
out (c),e
ld a,#ff
out (c),a
call sdraw_resp
ld (sdraw_last_r1),a ;DIAG: record for display, whatever the outcome
or a
jr nz,sdraw_readlba_fail
call sdraw_wtdo
ld (sdraw_last_token),a ;DIAG: record for display, whatever the outcome
cp #fe
jr nz,sdraw_readlba_fail
;DIAG: manual IN+store loop, reloading BC (so B is pinned at 0) before
;DIAG: every single byte -- ruling out B-register drift across INI's
;DIAG: as a hardware port-decode issue, however unlikely
ld hl,sdraw_buf
ld e,32
1 ld b,16
2 ld c,sdraw_DATA
in a,(c)
ld (hl),a
inc hl
djnz 2b
dec e
jr nz,1b
ld bc,sdraw_DATA
in a,(c) ; discard 2 CRC bytes
in a,(c)
call sdraw_do_cmd12
call sdraw_snb
call sdraw_wtby
call sdraw_csh
xor a
ret
sdraw_readlba_fail
call sdraw_csh
xor a
inc a
ret
;--- stop a multi-block transfer (CMD12). Sent without a fresh CSH/CSL --
;--- the card is still selected from the CMD18 that started the transfer.
sdraw_do_cmd12
ld bc,sdraw_DATA
ld a,%01000000+12
out (c),a
xor a
out (c),a
out (c),a
out (c),a
out (c),a
ld a,#ff
out (c),a
xor a
in a,(sdraw_DATA) ; one stuff byte before the R1 response, per spec
jp sdraw_resp
sdraw_snb
push bc
ld b,16
1 xor a
in a,(sdraw_DATA)
djnz 1b
pop bc
ret
sdraw_wtby
push bc
ld bc,sdraw_DATA
1 in a,(c)
or a
jr z,1b
pop bc
ret
;DIAG: step 2 - parse the FAT32 BPB (already validated in sdraw_buf's first
;DIAG: 16 bytes), locate the root directory, find ZIFI.INI's short-name
;DIAG: entry there, and read+print its first line. Proves we can navigate
;DIAG: the filesystem and read a real file, not just a raw sector.
test_sd_raw2
di ;DIAG: real interrupt mask, see test_sd_raw
call sdraw_read0
or a
jp nz,t2_fail
ld a,(sdraw_buf+13)
ld (sdraw_secpclus),a
; acc(32-bit) = reservedSectors
ld hl,(sdraw_buf+14)
ld (sdraw_acc_lo),hl
ld hl,0
ld (sdraw_acc_hi),hl
; acc += FATSz32 * numFATs
ld hl,(sdraw_buf+36)
ld (sdraw_fatsz_lo),hl
ld hl,(sdraw_buf+38)
ld (sdraw_fatsz_hi),hl
ld a,(sdraw_buf+16)
ld b,a
or a
jr z,t2_fatdone
t2_fatloop
push bc
ld hl,(sdraw_fatsz_lo)
ld de,(sdraw_fatsz_hi)
call sdraw_acc_add32
pop bc
djnz t2_fatloop
t2_fatdone
; acc now = firstDataSector -- save it, needed again for the file itself
ld hl,(sdraw_acc_lo)
ld (sdraw_firstdata_lo),hl
ld hl,(sdraw_acc_hi)
ld (sdraw_firstdata_hi),hl
; acc += (rootCluster-2) * sectorsPerCluster
ld hl,(sdraw_buf+44)
ld de,2
or a
sbc hl,de
ld b,h
ld c,l
t2_clusloop
ld a,b
or c
jr z,t2_clusdone
push bc
ld a,(sdraw_secpclus)
ld l,a
ld h,0
ld de,0
call sdraw_acc_add32
pop bc
dec bc
jr t2_clusloop
t2_clusdone
ld hl,(sdraw_acc_hi)
ld de,(sdraw_acc_lo)
call sdraw_readlba
or a
jp nz,t2_fail
ld hl,sdraw_buf
t2_scan
ld a,h
cp high (sdraw_buf+512)
jr nz,t2_chk
ld a,l
cp low (sdraw_buf+512)
jr z,t2_notfound
t2_chk
push hl
ld de,sdraw_zifiini_name
ld b,11
1 ld a,(de)
cp (hl)
jr nz,2f
inc hl
inc de
djnz 1b
pop hl
jr t2_found
2 pop hl
ld de,32
add hl,de
jr t2_scan
t2_found
; extract starting cluster: low word @ entry+26, high word @ entry+20
push hl
ld de,26
add hl,de
ld a,(hl)
inc hl
ld h,(hl)
ld l,a
ld (sdraw_filecl_lo),hl
pop hl
push hl
ld de,20
add hl,de
ld a,(hl)
inc hl
ld h,(hl)
ld l,a
ld (sdraw_filecl_hi),hl
pop hl
; acc = firstDataSector + (fileCluster-2)*sectorsPerCluster
ld hl,(sdraw_firstdata_lo)
ld (sdraw_acc_lo),hl
ld hl,(sdraw_firstdata_hi)
ld (sdraw_acc_hi),hl
ld hl,(sdraw_filecl_lo)
ld de,2
or a
sbc hl,de
ld b,h
ld c,l
t2_fclusloop
ld a,b
or c
jr z,t2_fclusdone
push bc
ld a,(sdraw_secpclus)
ld l,a
ld h,0
ld de,0
call sdraw_acc_add32
pop bc
dec bc
jr t2_fclusloop
t2_fclusdone
ld hl,(sdraw_acc_hi)
ld de,(sdraw_acc_lo)
call sdraw_readlba
or a
jr nz,t2_fail
ei
ld hl,sdraw_found_msg
ld b,1
call zifi_echo
ld hl,sdraw_buf
ld b,1
call zifi_echo
ret
t2_notfound
ei
ld hl,sdraw_notfound_msg
ld b,1
call zifi_echo
ret
t2_fail
ei
ld hl,sdraw_fail2_msg
ld b,1
call zifi_echo
ret
; adds a 32-bit value (DE=high word, HL=low word) into the 32-bit
; accumulator sdraw_acc_hi:sdraw_acc_lo
sdraw_acc_add32
push de
ld de,(sdraw_acc_lo)
add hl,de
ld (sdraw_acc_lo),hl
pop hl
ld de,(sdraw_acc_hi)
adc hl,de
ld (sdraw_acc_hi),hl
ret
sdraw_found_msg db 'ZIFI.INI found, first line:',0,0
sdraw_notfound_msg db 'ZIFI.INI not found in root dir sector0',0,0
sdraw_fail2_msg db 'SD raw test2: FAIL',0,0
sdraw_zifiini_name db "ZIFI INI"
sdraw_secpclus ds 1
sdraw_acc_lo ds 2
sdraw_acc_hi ds 2
sdraw_fatsz_lo ds 2
sdraw_fatsz_hi ds 2
sdraw_firstdata_lo ds 2
sdraw_firstdata_hi ds 2
sdraw_filecl_lo ds 2
sdraw_filecl_hi ds 2
align 256
sdraw_buf ds 512
;DIAG: end of standalone raw SD-card test block
sd_init
call init_sd_card