From 5b004979d534212ba3e416f9570d3c7b1c0010fc Mon Sep 17 00:00:00 2001 From: Philip Smart Date: Fri, 3 Apr 2026 14:20:43 +0100 Subject: [PATCH] SFD700 Version: Updated to have MZ700 and MZ80A drivers selected based on jumper pin --- asm/include/macros.asm | 3 +- asm/include/rfs_definitions.asm | 1 - asm/rfs.asm | 31 +- asm/rfs_bank1.asm | 641 ++++++++++++++++++++++++++++++-- asm/rfs_bank11.asm | 10 +- asm/rfs_bank2.asm | 22 ++ asm/rfs_bank3.asm | 113 +++++- asm/rfs_bank8.asm | 352 +++++++++++++++++- roms/rfs.rom | Bin 73728 -> 73728 bytes tools/MZFD/MZFDTool | Bin 0 -> 21752 bytes tools/MZFD/MZFDTool.c | 618 ++++++++++++++++++++++++++++++ tools/MZFD/MZFDTool.c.original | 630 +++++++++++++++++++++++++++++++ tools/MZFD/Makefile | 19 + tools/MZFDTool | Bin 0 -> 21752 bytes tools/make_roms.sh | 4 +- 15 files changed, 2373 insertions(+), 71 deletions(-) create mode 100755 tools/MZFD/MZFDTool create mode 100755 tools/MZFD/MZFDTool.c create mode 100755 tools/MZFD/MZFDTool.c.original create mode 100644 tools/MZFD/Makefile create mode 100755 tools/MZFDTool diff --git a/asm/include/macros.asm b/asm/include/macros.asm index 631e021..50e71d4 100644 --- a/asm/include/macros.asm +++ b/asm/include/macros.asm @@ -102,7 +102,7 @@ HWSEL2: LD A,(BNKCTRLRST) LD A,BNKDEFMROM_MZ700 ; Setup default MROM for an MZ700, this is a 4K Window into the UROM at F000. HWSEL21: OUT (REG_FXXX),A LD A,BNKDEFUROM ; Setup default UROM, this is a 2K Window into the UROM at E800 and contains the RFS. - OUT (REG_EXXX),A + OUT (REG_EXXX),A NOP ; Nops to allocate space to match RomDisk block. NOP NOP @@ -112,7 +112,6 @@ HWSEL21: OUT (REG_FXXX),A NOP NOP NOP - NOP ENDIF ENDM diff --git a/asm/include/rfs_definitions.asm b/asm/include/rfs_definitions.asm index 94c0ef7..ed53982 100644 --- a/asm/include/rfs_definitions.asm +++ b/asm/include/rfs_definitions.asm @@ -284,7 +284,6 @@ MMIO7 EQU 0E7H ; MZ-70 ; REG_EXXX EQU 060H ; A write copies D6:0 into the EXXX page address register to set a uniform 4K block in the region E300:EFFF window. REG_FXXX EQU 061H ; A write copies D6:0 into the FXXX page address register to set a uniform 4k block in the region F000:FFFF. -REG_MEMMODE EQU 062H ; A write with D0 = low enables FlashROM, D0 = high enables RAM. SFD700_MODE EQU 063H ; FDC Interface card configured target mode. FDC_CMD EQU 0D8H ; WD1773 Command Register. FDC_STATUS EQU 0D8H ; WD1773 Status Register. diff --git a/asm/rfs.asm b/asm/rfs.asm index c10647b..5bd2950 100644 --- a/asm/rfs.asm +++ b/asm/rfs.asm @@ -93,9 +93,15 @@ CMDTABLE2: IF BUILD_SFD700 = 1 DB 000H | 018H | 001H DB 'D' ; Dump Memory. DW DUMPX + DB 000H | 018H | 002H + DB "FC" ; Save memory to Floppy. + DW SAVEFDCARD DB 000H | 008H | 002H DB "FL" ; 'FL' Floppy disk boot (built-in WD1773 FDC). DW FLOPPY + DB 000H | 018H | 004H + DB "FD2T" ; Copy Floppy to Tape. + DW FD2TAPE DB 000H | 008H | 002H DB "FD" ; 'FD' Floppy disk directory listing. DW FDDIR @@ -144,6 +150,9 @@ CMDTABLE2: IF BUILD_SFD700 = 1 DB 000H | 020H | 001H DB 'S' ; Save to CMT DW SAVEX + DB 000H | 018H | 004H + DB "T2FD" ; Copy Tape to Floppy. + DW TAPE2FD DB 000H | 000H | 004H DB "TEST" ; A test function used in debugging. DW LOCALTEST @@ -571,15 +580,19 @@ CMDTABLE: IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 DB 000H | 010H | 002H DB "EC" ; Erase file. DW ERASESD + IF BUILD_MZ700 = 1 DB 000H | 008H | 002H - DB "FL" ; 'FL' RFS Floppy load/boot. + DB "FL" ; 'FL' Floppy boot (built-in MZ-700 WD1773). DW FLOPPY DB 000H | 008H | 002H DB "FD" ; 'FD' Floppy directory. DW FDDIR - DB 000H | 008H | 001H - DB 0AAH ; 'f' Original Floppy boot code. + ENDIF + IF BUILD_MZ80A = 1 + DB 000H | 008H | 002H + DB "FL" ; 'FL' Floppy boot (external FI ROM at F000H). DW FDCK + ENDIF DB 000H | 058H | 001H DB 'H' ; Help screen (bank 11). DW HELP @@ -1316,9 +1329,11 @@ LOADBASIC: LD DE,BASICFILENM ENDIF ; BUILD_SFD700 + IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 LOADPROG: LD HL,LOADSDCARD CALL BKSW0to2 RET + ENDIF ;------------------------------------------------------------------------------- ; END OF RFS COMMAND FUNCTIONS. @@ -1575,10 +1590,14 @@ DEFAULTFNE: EQU $ INCLUDE "rfs_utilities.asm" ; ; Ensure we fill the entire 2K by padding with FF's. + ; RomDisk/picoZ80: EFF8-EFFF are coded latch control registers. + ; SFD700: no control registers, bank switching via I/O ports. ; - ALIGN 0EFF8h - ORG 0EFF8h - DB 0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0AAh + IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 + ALIGN 0EFF8h + ORG 0EFF8h + DB 0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0AAh + ENDIF IF BUILD_SFD700 = 1 ALIGN 0F000H diff --git a/asm/rfs_bank1.asm b/asm/rfs_bank1.asm index bad7faa..2a551e7 100644 --- a/asm/rfs_bank1.asm +++ b/asm/rfs_bank1.asm @@ -145,6 +145,14 @@ TRK0FD3 EQU 01004H TRK0FD4 EQU 01005H RETRIES EQU 01006H BPARA EQU 01008H + ; SAVEFDX work variables (after BPARA's 11 bytes at 1008H-1012H). +FREEDSEC EQU 01014H ; Logical sector of free dir entry. +FREEDDIX EQU 01016H ; Index (0-7) of free entry in sector. +NXTDATASEC EQU 01017H ; Next free data sector (2 bytes LE). +SAVEDRVNO EQU 01019H ; Saved drive number for save operations. + ; Temporary variables for TMPSTACKP save/restore (MUST be in RAM, not ROM). +FD7RS_TSP EQU 0101AH ; FD7READSAFE saved TMPSTACKP (2 bytes). +FD7ERR_TSP EQU 0101CH ; FD7ERR saved TMPSTACKP (2 bytes). ;------------------------------------------------------------------------------- ; START OF FLOPPY DISK CONTROLLER FUNCTIONALITY @@ -160,7 +168,12 @@ FDCKROM: LD A,(0F000h) RET FLOPPY: IF BUILD_ROMDISK+BUILD_PICOZ80+BUILD_SFD700 = 1 - IF BUILD_MZ80A = 1 + ; + ; NOTE: MZ-80A FDC code removed from bank 1. + ; - SFD700+MZ80A: handled by bank 8 (FD80A_BOOT) with HW acceleration. + ; - ROMDISK/picoZ80+MZ80A: uses physical MZ-80A FI ROM at F000H via FDCK. + ; + IF 0 = 1 ; MZ-80A section removed. PUSH DE ; Preserve pointer to input buffer. LD DE,BPARA ; Copy disk parameter block into RAM work area. (From) LD HL,PRMBLK ; (To) @@ -545,8 +558,24 @@ L0300: IN A,(0D8H) ; State ; RETRIES (1006H): Retry counter ;------------------------------------------------------------------------------- - ; Method to boot from MZ-700 floppy disk. - ; + ; Boot entry. MZ-80A mode dispatch temporarily disabled for testing. + ; Initialize timer (as MZ-1E05 does) and reset FDC. + PUSH DE ; Save command line pointer. + XOR A + LD DE,0 + CALL ?TMST ; Timer init. + POP DE ; Restore command line pointer. + XOR A + OUT (FDC_DRIVE),A ; Deselect all drives, motor off. + LD A,0D8H ; Force interrupt. + CPL + OUT (FDC_CMD),A + LD B,0 ; ~1.7ms delay for FDC to settle. +FLDLY1: DJNZ FLDLY1 + XOR A + LD (MOTON),A + LD (TRK0FD1),A + LD (FDCCMD),A PUSH DE ; Preserve pointer to input buffer. LD DE,BPARA ; Copy disk parameter block into RAM work area. LD HL,PRMBLK @@ -556,7 +585,6 @@ L0300: IN A,(0D8H) ; State LD A,(DE) ; Check if drive number given on command line. CP 00DH JR NZ,FD7BOOT - CALL FD7INIT ; Initialise disk and flags. FD7PRMPT: LDDE MSGBOOTDRV LD HL,PRINTMSG CALL BKSW1to6 @@ -602,6 +630,7 @@ FD7CHK: LD C,(HL) CALL BKSW1to6 ; ; Extract load parameters from boot sector. + LD IX,BPARA ; Restore IX (PRINTMSG may corrupt it). LD HL,(0CF16H) ; Load/target address. LD A,H OR L @@ -649,12 +678,18 @@ FD7NOTMST: CALL FD7INIT LDDE MSGDSKNOTMST JR FD7ERR FD7LOADERR: LDDE MSGLOADERR -FD7ERR: LD HL,PRINTMSG +FD7ERR: LD HL,(TMPSTACKP) ; Save TMPSTACKP before BKSW overwrites it. + LD (FD7ERR_TSP),HL + LD HL,PRINTMSG CALL BKSW1to6 LD DE,ERRTONE CALL MELDY - LD SP,(TMPSTACKP) ; Recover stack pointer. + LD HL,(FD7ERR_TSP) ; Restore saved TMPSTACKP. + LD SP,HL ; Recover stack pointer. RET + ; FD7ERR_TSP now in RAM (EQU 0101CH). + + ;------------------------------------------------------------------------------- ; MZ-700 FDC LOW-LEVEL ROUTINES @@ -687,6 +722,7 @@ FD7INIT: PUSH AF ; Motor on: enable motor and wait for spinup. FD7MOTON: LD A,080H OUT (FDC_DRIVE),A + CALL FD7DLY80U ; Settle delay after motor on. LD B,16 FD7MTD1: CALL FD7DLY60M DJNZ FD7MTD1 @@ -701,6 +737,7 @@ FD7READY: LD A,(MOTON) LD A,(IX+0) ; Drive number. OR 084H OUT (FDC_DRIVE),A ; Drive select + motor on. + CALL FD7DLY80U ; Settle delay after drive select. XOR A LD (FDCCMD),A CALL FD7DLY60M @@ -798,6 +835,33 @@ FD7BSF3: DEC E POP DE JP FD7DSKERR + ;------------------------------------------------------------------------------- + ; FD7READSAFE - Safe read wrapper. Catches FD7ERR longjmp and returns + ; with carry set instead of corrupting the caller's stack. + ; IX = parameter block (BPARA). Returns: carry clear=OK, carry set=error. + ; Mechanism: pushes a recovery address on the stack, then sets TMPSTACKP + ; to SP. FD7ERR's "LD SP,(TMPSTACKP) / RET" pops the recovery address, + ; landing at FD7RS_REC instead of unwinding the entire call chain. + ;------------------------------------------------------------------------------- +FD7READSAFE: LD HL,(TMPSTACKP) + LD (FD7RS_TSP),HL ; Save original TMPSTACKP. + LD HL,FD7RS_REC ; Recovery address. + PUSH HL ; Push onto stack. + LD (TMPSTACKP),SP ; FD7ERR longjmp lands here → pops FD7RS_REC. + CALL FD7READ ; Normal return if success. + POP HL ; Remove recovery address. + LD HL,(FD7RS_TSP) + LD (TMPSTACKP),HL ; Restore TMPSTACKP. + OR A ; Clear carry = success. + RET + ; FD7ERR longjmp caught: SP restored, RET popped FD7RS_REC → here. + ; Error message and tone already displayed by FD7ERR. +FD7RS_REC: LD HL,(FD7RS_TSP) + LD (TMPSTACKP),HL ; Restore TMPSTACKP. + SCF ; Set carry = error. + RET + ; FD7RS_TSP now in RAM (EQU 0101AH). + ; Sequential read from disk. ; IX = parameter block (BPARA). FD7READ: CALL FD7CNVRT ; Convert logical sector to track/sector. @@ -952,7 +1016,22 @@ FD7DLYT: DEC DE ; Input: DE = pointer to optional drive number on command line. ;------------------------------------------------------------------------------- -FDDIR: ; Parse drive number (default = drive 1 = index 0). +FDDIR: ; Initialize timer and reset FDC. + XOR A + LD DE,0 + CALL ?TMST ; Timer init. + XOR A + OUT (FDC_DRIVE),A ; Deselect all drives, motor off. + LD A,0D8H ; Force interrupt. + CPL + OUT (FDC_CMD),A + LD B,0 ; ~1.7ms delay. +FDDLY1: DJNZ FDDLY1 + XOR A + LD (MOTON),A + LD (TRK0FD1),A + LD (FDCCMD),A + ; Parse drive number (default = drive 1 = index 0). LD A,(DE) CP 00DH JR Z,FDDIR_DEF @@ -990,6 +1069,7 @@ FDDIR_GO: LD (BPARA),A ; Store ; Read boot sector (sector 0 = IPL) and verify signature. LD IX,BPARA CALL FD7READ + JP C,FDDIR_END ; Read error. LD HL,0CF00H LD DE,DSKID LD B,7 @@ -1003,11 +1083,11 @@ FDDIR_IPL: LD A,(DE) LD DE,0CF07H LD HL,PRTFN CALL BKSW1to6 - LD HL,(0CF16H) - PUSH HL - LD HL,(0CF18H) - PUSH HL - LD BC,(0CF14H) + LD HL,(0CF18H) ; Exec addr. + PUSH HL ; Stack bottom: exec. + LD HL,(0CF16H) ; Load addr. + PUSH HL ; Stack top: load. + LD BC,(0CF14H) ; Size. LDDE MSGFDINFO LD HL,PRINTMSG CALL BKSW1to6 @@ -1022,17 +1102,19 @@ FDDIR_IPL: LD A,(DE) ; Get number of directory sectors from IPL offset +1EH. LD A,(0CF1EH) ; Start data sector (low byte). SUB 16 ; Directory sectors = startDataSec - 16. - JR Z,FDDIR_END ; No directory sectors. - JR C,FDDIR_END + JP Z,FDDIR_END ; No directory sectors. + JP C,FDDIR_END LD C,A ; C = number of directory sectors. LD A,16 ; First directory sector. ; FDDIR_SEC: ; Read next directory sector. PUSH AF ; Save current sector number. PUSH BC ; Save remaining sector count. + LD IX,BPARA ; Restore IX (may be corrupted by PRINTMSG). LD (IX+1),A LD (IX+2),0 - CALL FD7READ + CALL FD7READ + JR C,FDDIR_RERR ; Read error → clean stack and exit. ; ; First sector (16): skip entry 0 (FAT header), scan entries 1-7. ; Subsequent sectors: scan all 8 entries. @@ -1048,12 +1130,25 @@ FDDIR_SEC: ; Read next directory sector. FDDIR_AL8: LD HL,0CF00H ; Start from entry 0. LD B,8 ; 8 entries per sector. ; -FDDIR_ENT: ; Check type byte at (HL). 00 = empty/deleted, FF = unused → skip. +FDDIR_ENT: ; Check type byte at (HL). Only 01-7F are valid file entries. LD A,(HL) OR A - JR Z,FDDIR_NXE + JR Z,FDDIR_NXE ; 00 = deleted/empty. + CP 080H + JR NC,FDDIR_NXE ; >= 80H = header/system/uninit. + ; Validate: check SectorAddress is not 0000 or FFFF (uninit garbage). + PUSH HL + LD DE,01EH + ADD HL,DE + LD A,(HL) + INC HL + LD D,(HL) ; D=high, A=low of SectorAddress. + POP HL + OR D + JR Z,FDDIR_NXE ; SectorAddress=0000 → skip. + AND D CP 0FFH - JR Z,FDDIR_NXE + JR Z,FDDIR_NXE ; SectorAddress=FFFF → skip. ; ; Valid entry: print filename and load/exec/size. LD (TMPADR),HL ; Save entry base. @@ -1063,23 +1158,27 @@ FDDIR_ENT: ; Check type byte at (HL). 00 = empty/deleted, FF = unused → skip EX DE,HL LD HL,PRTFN CALL BKSW1to6 - ; Read size from base+12H, load from base+14H, exec from base+16H. + ; Read size from +14H, load from +16H, exec from +18H. + ; (Dir entry: +12=LockFlag, +13=DummyFlag, +14=FileSize, + ; +16=LoadAddress, +18=ExecAddress, +1E=SectorAddress.) LD HL,(TMPADR) - LD DE,012H + LD DE,014H ADD HL,DE LD C,(HL) INC HL - LD B,(HL) ; BC = size. - INC HL ; +14H = load addr. + LD B,(HL) ; BC = file size. + INC HL ; +16H = load addr. LD E,(HL) INC HL LD D,(HL) - PUSH DE ; Stack: load addr. - INC HL ; +16H = exec addr. + PUSH DE ; Save load addr. + INC HL ; +18H = exec addr. LD E,(HL) INC HL - LD D,(HL) - PUSH DE ; Stack: exec addr. + LD D,(HL) ; DE = exec addr. + POP HL ; HL = load addr. + PUSH DE ; Stack bottom: exec. + PUSH HL ; Stack top: load (MSGFDINFO pops load first). LDDE MSGFDINFO LD HL,PRINTMSG CALL BKSW1to6 @@ -1098,10 +1197,474 @@ FDDIR_NXE: LD DE,32 ; Next 3 INC A ; Next sector. DEC C JR NZ,FDDIR_SEC ; More directory sectors. + JP FDDIR_END ; Done — skip error recovery POPs. ; +FDDIR_RERR: POP BC ; Clean stack from FDDIR_SEC PUSH. + POP AF FDDIR_END: CALL FD7INIT RET + ;------------------------------------------------------------------------------- + ; FD7WRITE - Sequential write to disk. + ; Mirror of FD7READ but uses OUTI for data output. + ; IX = parameter block (BPARA): drive, logical sector, size, source addr. + ;------------------------------------------------------------------------------- + ; Sequential write to disk. + ; Returns: carry clear = success, carry set = error. + ; Does NOT call FD7DSKERR — caller handles errors via carry flag. +FD7WRITE: CALL FD7CNVRT ; Convert logical sector to track/sector. + CALL FD7PRST1 ; Setup params, HL = source address. +FD7WE8: CALL FD7SIDST ; Set side/head. + CALL FD7SEEK + JR NZ,FD7WERR ; Seek failed. + CALL FD7PRST2 ; Set track & sector registers. + DI + LD A,0A4H ; Write sector (multiple records). + CALL FD7CMD2 +FD7WE6: LD B,0 ; 256 bytes per sector. +FD7WE4: IN A,(FDC_CMD) + RRCA + JR C,FD7WE3 ; Busy dropped = done. + RRCA + JR C,FD7WE4 ; DRQ not set, keep waiting. + OUTI ; Write byte: OUT(C)=(HL), B--, HL++. + JR NZ,FD7WE4 + ; + INC (IX+8) ; Next sector. + LD A,(IX+8) + CP 17 ; Past last sector on track? + JR Z,FD7WESEC + DEC D ; Decrement sector count. + JR NZ,FD7WE6 ; More sectors to write. + JR FD7WEDONE +FD7WESEC: DEC D +FD7WEDONE: LD A,0D8H ; Force interrupt. + CPL + OUT (FDC_CMD),A + CALL FD7BSYON +FD7WE3: EI + IN A,(FDC_CMD) + CPL + AND 07CH ; Check error bits (WP, WF, RNF, CRC, lost data). + JR NZ,FD7WERR ; Write error. + CALL FD7ADJ ; Adjust sector/track. + JR Z,FD7WEND ; All sectors written. + LD A,(IX+7) ; Next track. + JR FD7WE8 +FD7WEND: LD A,080H + OUT (FDC_DRIVE),A ; Keep motor on. + OR A ; Clear carry = success. + RET +FD7WERR: EI + SCF ; Set carry = error. + RET + + ;------------------------------------------------------------------------------- + ; LOADFDCP - Load file from floppy into memory, populating CMT header. + ; Reads boot sector, extracts file info, reads program data to 0x1200. + ; Sets RESULT=0 on success, non-zero on error. + ; Called from bank 3 for FD2T command. + ;------------------------------------------------------------------------------- + ;------------------------------------------------------------------------------- + ; LOADFDCP - Load FIRST file from floppy directory into memory at 0x1200. + ; Scans directory for first valid entry, populates CMT header, loads data. + ; Output: RESULT = 0 success, 0xFE FD7ERR handled, 0xFF caller shows error. + ;------------------------------------------------------------------------------- +LOADFDCP: LD A,0FEH + LD (RESULT),A ; 0xFE = FD7ERR will handle errors. + ; Full FDC init: timer, deselect, force interrupt, clear flags. + PUSH DE + XOR A + LD DE,0 + CALL ?TMST + POP DE + XOR A + OUT (FDC_DRIVE),A + LD A,0D8H + CPL + OUT (FDC_CMD),A + LD B,0 +LFDCP_DLY: DJNZ LFDCP_DLY + XOR A + LD (MOTON),A + LD (TRK0FD1),A + ; Init BPARA. + LD HL,PRMBLK + LD DE,BPARA + LD BC,11 + LDIR + LD IX,BPARA + ; + ; Scan directory for first valid file entry. + LD A,16 ; First directory sector. + LD C,16 ; 16 sectors. +LFDCP_DSC: PUSH AF + PUSH BC + LD (IX+1),A + LD (IX+2),0 + LD HL,0CF00H + LD (IX+5),L + LD (IX+6),H + LD (IX+3),0 + LD (IX+4),1 + CALL FD7READ ; (FD7ERR longjumps on error.) + POP BC + POP AF + ; + LD HL,0CF00H + LD B,8 +LFDCP_ENT: LD A,(HL) + OR A + JR Z,LFDCP_NXE ; 00 = empty. + CP 080H + JR NC,LFDCP_NXE ; >=80 = header/system. + ; Valid entry — check if filename matches NAME. + PUSH HL + PUSH BC + INC HL ; +01 = filename in entry. + LD DE,NAME + LD B,17 ; Compare 17 bytes. +LFDCP_CMP: LD A,(DE) + CP (HL) + JR NZ,LFDCP_NOM ; Mismatch. + CP 00DH ; Both end with CR? + JR Z,LFDCP_MAT ; Match! + INC HL + INC DE + DJNZ LFDCP_CMP +LFDCP_MAT: POP BC + POP HL + JP LFDCP_GOT ; Found matching file. +LFDCP_NOM: POP BC + POP HL +LFDCP_NXE: LD DE,32 + ADD HL,DE + DJNZ LFDCP_ENT + ; No entry in this sector, try next. + POP AF ; Wait, stack issue... + ; Actually the POP AF/BC was already done above. Just continue. + INC A + DEC C + JP NZ,LFDCP_DSC + ; No file found on disk. + JP LFDCP_ERR + ; +LFDCP_GOT: ; HL = entry base of first valid file. + ; Populate CMT header from directory entry. + LD A,(HL) ; +00 = FileType. + LD (ATRB),A + PUSH HL + INC HL ; +01 = Filename. + LD DE,NAME + LD BC,17 + LDIR + POP HL + PUSH HL + LD DE,014H + ADD HL,DE ; +14 = FileSize. + LD E,(HL) + INC HL + LD D,(HL) + LD (SIZE),DE + INC HL ; +16 = LoadAddress. + LD E,(HL) + INC HL + LD D,(HL) + LD (DTADR),DE + INC HL ; +18 = ExecAddress. + LD E,(HL) + INC HL + LD D,(HL) + LD (EXADR),DE + POP HL + PUSH HL + LD DE,01EH + ADD HL,DE ; +1E = SectorAddress. + LD E,(HL) + INC HL + LD D,(HL) + POP HL + ; DE = start sector, load data to 0x1200. + LD (IX+1),E + LD (IX+2),D + LD HL,(SIZE) + LD (IX+3),L + LD (IX+4),H + LD HL,01200H + LD (IX+5),L + LD (IX+6),H + LD (DTADR),HL ; Override load addr for copy. + CALL FD7READ + CALL FD7INIT + XOR A + LD (RESULT),A ; Success. + RET +LFDCP_ERR: CALL FD7INIT + LD A,0FFH + LD (RESULT),A + RET + + ;------------------------------------------------------------------------------- + ; SAVEFDX - Save memory to floppy disk with IPL boot header. + ; Writes an IPL boot sector at sector 0 and program data from sector 1+. + ; Uses CMT header fields: NAME, SIZE, DTADR, EXADR. + ; Called from bank 3 for T2FD and FC commands. + ;------------------------------------------------------------------------------- + ;------------------------------------------------------------------------------- + ; SAVEFDX - Save file to floppy disk directory. + ; Scans directory for a free entry, calculates next free data sector, + ; writes program data, then updates the directory entry. + ; Input: NAME, SIZE, DTADR, EXADR, ATRB set by caller. + ; Output: RESULT = 0 on success, 0xFF on error. + ;------------------------------------------------------------------------------- +SAVEFDX: LD A,0FFH + LD (RESULT),A ; Default = error. + ; Full FDC init: timer, deselect, force interrupt, clear flags. + PUSH DE + XOR A + LD DE,0 + CALL ?TMST ; Timer init (required for FDC). + POP DE + XOR A + OUT (FDC_DRIVE),A ; Deselect drives. + LD A,0D8H + CPL + OUT (FDC_CMD),A ; Force interrupt. + LD B,0 +SFDX_DLY: DJNZ SFDX_DLY ; Settle delay. + XOR A + LD (MOTON),A + LD (TRK0FD1),A + ; Init BPARA from PRMBLK and set drive 0. + LD HL,PRMBLK + LD DE,BPARA + LD BC,11 + LDIR + LD IX,BPARA + ; Init scan variables. + LD A,0FFH + LD (FREEDSEC),A ; FF = no free entry found yet. + LD HL,32 ; First data sector after directory. + LD (NXTDATASEC),HL + ; + ; --- Scan directory: logical sectors 16-31 (Track 0, Head 1). --- + LD A,16 ; First directory sector. + LD C,16 ; 16 sectors to scan. +SFDX_DSEC: LD (SAVEDRVNO),A ; Save current sector for SFDX_FREE. + PUSH AF + PUSH BC + LD IX,BPARA + LD (IX+1),A ; Logical sector. + LD (IX+2),0 + LD HL,0CF00H ; Buffer. + LD (IX+5),L + LD (IX+6),H + LD (IX+3),0 + LD (IX+4),1 ; 1 sector = 256 bytes. + CALL FD7READ + POP BC + POP AF + JP C,SFDX_ERR ; Read error. + PUSH AF + PUSH BC + ; + ; Scan 8 entries in this sector. + LD HL,0CF00H + LD B,8 +SFDX_DENT: PUSH HL + PUSH BC + LD A,(HL) ; FileType. + OR A + JR Z,SFDX_FREE ; 00 = deleted/free entry. + CP 0FFH + JR Z,SFDX_FREE ; FF = uninitialized/free entry. + CP 080H + JR NC,SFDX_NXTE ; >=80H = header/system, skip. + ; Check SectorAddress — if 0000 or FFFF, treat as free (uninit garbage). + PUSH HL + LD DE,01EH + ADD HL,DE + LD E,(HL) + INC HL + LD D,(HL) ; DE = SectorAddress. + POP HL + LD A,E + OR D + JR Z,SFDX_FREE ; SectorAddress=0000 → free. + LD A,E + AND D + CP 0FFH + JR Z,SFDX_FREE ; SectorAddress=FFFF → free. + PUSH HL + LD BC,014H + ADD HL,BC + LD C,(HL) + INC HL + LD B,(HL) ; BC = FileSize. + POP HL + ; Sectors used = (FileSize + 255) / 256 = (FileSize >> 8) + ((FileSize & FF) ? 1 : 0). + LD A,C + OR A + JR Z,SFDX_NRU + INC B ; Round up. +SFDX_NRU: LD C,B + LD B,0 ; BC = sectors used. + EX DE,HL ; HL = SectorAddress. + ADD HL,BC ; HL = end sector. + EX DE,HL ; DE = end sector. + ; Update NXTDATASEC if this file ends later. + PUSH HL + LD HL,(NXTDATASEC) + OR A + SBC HL,DE ; HL = NXTDATASEC - endSec. + POP HL + JR NC,SFDX_NXTE ; NXTDATASEC >= endSec, no update. + LD (NXTDATASEC),DE ; Update with higher value. + JR SFDX_NXTE + ; +SFDX_FREE: ; Found a free entry. Record if first one found. + LD A,(FREEDSEC) + CP 0FFH + JR NZ,SFDX_NXTE ; Already have one. + ; Record: FREEDSEC = current logical sector, FREEDDIX = entry index. + ; Entry index = 8 - B (since B counts down from 8). + POP BC ; B = entries remaining. + POP HL + PUSH HL + PUSH BC + LD A,8 + SUB B + LD (FREEDDIX),A + LD A,(SAVEDRVNO) ; Sector number saved at loop start. + LD (FREEDSEC),A + ; +SFDX_NXTE: POP BC + POP HL + LD DE,32 ; Next 32-byte entry. + ADD HL,DE + DEC B + JR NZ,SFDX_DENT + ; + POP BC + POP AF + INC A ; Next directory sector. + DEC C + JP NZ,SFDX_DSEC + ; + ; --- Check we found a free entry. --- + LD A,(FREEDSEC) + CP 0FFH + RET Z ; No free entry → error. + ; + ; --- Write file data at NXTDATASEC. --- + LD IX,BPARA + LD HL,(NXTDATASEC) + LD (IX+1),L + LD (IX+2),H + LD HL,(SIZE) + LD (IX+3),L + LD (IX+4),H + LD HL,(DTADR) + LD (IX+5),L + LD (IX+6),H + CALL FD7WRITE + JP C,SFDX_ERR + ; + ; --- Re-read directory sector with free entry. --- + LD IX,BPARA + LD A,(FREEDSEC) + LD (IX+1),A + LD (IX+2),0 + LD HL,0CF00H + LD (IX+5),L + LD (IX+6),H + LD (IX+3),0 + LD (IX+4),1 + CALL FD7READ + JP C,SFDX_ERR ; Read error. + ; + ; --- Fill directory entry. --- + LD A,(FREEDDIX) + LD HL,0CF00H + OR A + JR Z,SFDX_FILL ; Index 0 → HL already at CF00. + LD DE,32 +SFDX_FMUL: ADD HL,DE + DEC A + JR NZ,SFDX_FMUL +SFDX_FILL: ; HL = entry base. Clear 32 bytes. + PUSH HL + LD D,H + LD E,L + INC DE + LD (HL),0 + LD BC,31 + LDIR + POP HL + ; +00: FileType from ATRB (with CMT→FD type conversion). + LD A,(ATRB) + CP 005H ; CMT type 05 (RB) → FD type 02 (BTX). + JR NZ,SFDX_FT1 + LD A,002H +SFDX_FT1: LD (HL),A + ; +01: FileName from NAME (17 bytes). + PUSH HL + INC HL + EX DE,HL + LD HL,NAME + LD BC,17 + LDIR + POP HL + ; +14: FileSize from SIZE. + PUSH HL + LD DE,014H + ADD HL,DE + LD DE,(SIZE) + LD (HL),E + INC HL + LD (HL),D + ; +16: LoadAddress from DTADR. + INC HL + LD DE,(DTADR) + LD (HL),E + INC HL + LD (HL),D + ; +18: ExecAddress from EXADR. + INC HL + LD DE,(EXADR) + LD (HL),E + INC HL + LD (HL),D + POP HL + ; +1E: SectorAddress = NXTDATASEC. + PUSH HL + LD DE,01EH + ADD HL,DE + LD DE,(NXTDATASEC) + LD (HL),E + INC HL + LD (HL),D + POP HL + ; + ; --- Write directory sector back. --- + LD A,(FREEDSEC) + LD (IX+1),A + LD (IX+2),0 + LD HL,0CF00H + LD (IX+5),L + LD (IX+6),H + LD (IX+3),0 + LD (IX+4),1 + CALL FD7WRITE + JP C,SFDX_ERR + ; + CALL FD7INIT + XOR A + LD (RESULT),A ; Success. + RET +SFDX_ERR: CALL FD7INIT + RET ; RESULT still = 0xFF. + ENDIF ENDIF @@ -1119,19 +1682,27 @@ FDDIR_END: CALL FD7INIT ; Error tone. ERRTONE: DB "A0", 0D7H, "ARA", 0D7H, "AR", 00DH - ; Boot disk identifier - 002H prefix for MZ-80A, 003H prefix for MZ-700. -DSKID: IF BUILD_MZ80A = 1 - DB 002H, "IPLPRO" + ; Boot disk identifier - prefix set at runtime for SFD700, compile-time otherwise. + ; (Glass: label must be outside IF for global visibility.) +DSKID: IF BUILD_SFD700 = 1 + DB 003H, "IPLPRO" ; Default MZ-700; compile-time value for BUILD_MZ700. ELSE - DB 003H, "IPLPRO" + IF BUILD_MZ80A = 1 + DB 002H, "IPLPRO" + ELSE + DB 003H, "IPLPRO" + ENDIF ENDIF - ; Parameter block: drive 0, sector 0, 256 bytes, load address differs. - ; MZ-80A loads boot sector to CE00H, MZ-700 loads to CF00H. -PRMBLK: IF BUILD_MZ80A = 1 - DB 000H, 000H, 000H, 000H, 001H, 000H, 0CEH, 000H, 000H, 000H, 000H + ; Parameter block: drive 0, sector 0, 256 bytes, load address set at runtime for SFD700. +PRMBLK: IF BUILD_SFD700 = 1 + DB 000H, 000H, 000H, 000H, 001H, 000H, 0CFH, 000H, 000H, 000H, 000H ; Default MZ-700; patched at runtime. ELSE - DB 000H, 000H, 000H, 000H, 001H, 000H, 0CFH, 000H, 000H, 000H, 000H + IF BUILD_MZ80A = 1 + DB 000H, 000H, 000H, 000H, 001H, 000H, 0CEH, 000H, 000H, 000H, 000H + ELSE + DB 000H, 000H, 000H, 000H, 001H, 000H, 0CFH, 000H, 000H, 000H, 000H + ENDIF ENDIF IF BUILD_MZ80A = 1 diff --git a/asm/rfs_bank11.asm b/asm/rfs_bank11.asm index 2d5c0c9..706a5cd 100644 --- a/asm/rfs_bank11.asm +++ b/asm/rfs_bank11.asm @@ -389,7 +389,10 @@ HELPSCR: IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 DB "DXXXX[YYYY] - dump mem X to Y.", 00DH DB "DASMXXXX[YYYY]", 00DH DB " disassemble X to Y", 00DH + DB "FC[XXXXYYYYZZZZ] - save mem to floppy.", 00DH + DB " X=start,Y=end,Z=exec", 00DH DB "FD/FL - fd dir/boot", 00DH + DB "FD2T - copy floppy to tape.", 00DH DB "H - this help screen.", 00DH DB "IR - rfs rom dir listing.", 00DH DB "JXXXX - jump to location X.", 00DH @@ -400,12 +403,13 @@ HELPSCR: IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 DB "P - test printer.", 00DH ;DB "QD/QL - QD dir/boot", 00DH DB "R - test dram memory.", 00DH - DB "SD2T - copy sd card to tape.", 00DH + ;DB "SD2T - copy sd card to tape.", 00DH DB "ST[XXXXYYYYZZZZ] - save mem to tape.", 00DH - DB "SC[XXXXYYYYZZZZ] - save mem to card.", 00DH + ;DB "SC[XXXXYYYYZZZZ] - save mem to card.", 00DH DB " X=start,Y=end,Z=exec", 00DH DB "T - test timer.", 00DH - DB "T2SD - copy tape to sd card.", 00DH + ;DB "T2SD - copy tape to sd card.", 00DH + DB "T2FD - copy tape to floppy.", 00DH DB "V - verify tape save.", 00DH DB 000H ENDIF diff --git a/asm/rfs_bank2.asm b/asm/rfs_bank2.asm index 4bbf691..016bc02 100644 --- a/asm/rfs_bank2.asm +++ b/asm/rfs_bank2.asm @@ -218,7 +218,28 @@ TDELAYB1: RRA ;------------------------------------------------------------------------------- ; START OF SD CONTROLLER FUNCTIONALITY + ; SD card hardware not present on SFD700 — exclude for that build. ;------------------------------------------------------------------------------- + ; Dummy EQUs for SD functions — not used on SFD700 (no SD hardware). + ; Bank 0 CMT intercept handlers reference these. Must be outside IF for Glass visibility. +SDINIT EQU BUILD_SFD700 * 0 +LOADSDCARD EQU BUILD_SFD700 * 0 +LOADSDCARDX EQU BUILD_SFD700 * 0 +LOADSDCP EQU BUILD_SFD700 * 0 +LOADSDINF EQU BUILD_SFD700 * 0 +LOADSDDATA EQU BUILD_SFD700 * 0 +LOADSD3 EQU BUILD_SFD700 * 0 +SAVESDCARD EQU BUILD_SFD700 * 0 +SAVESDCARDX EQU BUILD_SFD700 * 0 +SAVESDDATA EQU BUILD_SFD700 * 0 +DIRSDCARD EQU BUILD_SFD700 * 0 +FINDSDX EQU BUILD_SFD700 * 0 +SDPRINT EQU BUILD_SFD700 * 0 +LOADSD9BC EQU BUILD_SFD700 * 0 +LOADSD9 EQU BUILD_SFD700 * 0 +LOADSD11 EQU BUILD_SFD700 * 0 + + IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 ;------------------------------------------------------------------------------- ; Hardware SPI SD Controller (HW_SPI_ENA = 1) @@ -1612,6 +1633,7 @@ LOADSD9BC: LD H,B ;------------------------------------------------------------------------------- ; END OF SD CONTROLLER FUNCTIONALITY ;------------------------------------------------------------------------------- + ENDIF ; BUILD_ROMDISK+BUILD_PICOZ80 (SD controller) ; RomDisk, top 8 bytes are used by the control registers when enabled so dont use the space. IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 diff --git a/asm/rfs_bank3.asm b/asm/rfs_bank3.asm index f164cc2..5234c41 100644 --- a/asm/rfs_bank3.asm +++ b/asm/rfs_bank3.asm @@ -143,21 +143,16 @@ BKSWRET3: POP AF ; G RET ;------------------------------------------------------------------------------- - ; START OF TAPE/SD CMDLINE TOOLS FUNCTIONALITY + ; START OF TAPE/SD/FD CMDLINE TOOLS FUNCTIONALITY ;------------------------------------------------------------------------------- - ; Method to copy an application on a tape to an SD stored application. The tape drive is read and the first - ; encountered program is loaded into memory at 0x1200. The CMT header is populated with the correct details (even if - ; the load address isnt 0x1200, the CMT Header contains the correct value). - ; A call is then made to write the application to the SD card. - ; -TAPE2SD: ; Load from tape into memory, filling the tape CMT header and loading data into location 0x1200. - LD HL,LOADTAPECP ; Call the Loadtape command, non execute version to get the tape contents into memory. + IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 + ; Method to copy an application on a tape to an SD stored application. +TAPE2SD: LD HL,LOADTAPECP CALL BKSW3to4 LD A,(RESULT) OR A JR NZ,TAPE2SDERR - ; Save to SD Card. LD HL,SAVESDCARDX CALL BKSW3to2 LD A,(RESULT) @@ -171,12 +166,7 @@ TAPE2SDERR2:LD HL,PRINTMSG RET ; Method to copy an SD stored application to a Cassette tape in the CMT. - ; The directory entry number or filename is passed to the command and the entry is located within the SD - ; directory structure. The file is then loaded into memory and the CMT header populated. A call is then made - ; to write out the data to tap. - ; -SD2TAPE: ; Load from SD, fill the CMT header then call CMT save. - LD HL,LOADSDCP +SD2TAPE: LD HL,LOADSDCP CALL BKSW3to2 LD A,(RESULT) OR A @@ -190,8 +180,97 @@ SD2TAPE: ; Load from SD, fill the CMT header then call CMT save. SD2TAPEERR: LDDE MSGSD2TERR JR TAPE2SDERR2 RET + ENDIF + + IF BUILD_SFD700 = 1 ;------------------------------------------------------------------------------- - ; END OF TAPE/SD CMDLINE TOOLS FUNCTIONALITY + ; FD2TAPE - Copy floppy disk file to cassette tape. + ; Reads boot sector + file from floppy, populates CMT header, writes to tape. + ;------------------------------------------------------------------------------- +FD2TAPE: ; Prompt for filename to extract from floppy. + CALL NL + LDDE MSGFDFNAME + CALL MSG ; "FILENAME? " + LD DE,BUFER + CALL GETL ; Get user input. + LD HL,BUFER+10 ; Skip past prompt echo. + LD DE,NAME + LD BC,FNSIZE + LDIR ; Copy filename to NAME. + ; + LD HL,LOADFDCP ; Load named file from floppy (bank 1). + CALL BKSW3to1 + LD A,(RESULT) + OR A + JR Z,FD2T_SAV ; Success → save to tape. + CP 0FEH + RET Z ; 0xFE = FD7ERR already showed error. + JR FD2TERR ; 0xFF = show our error message. +FD2T_SAV: LD HL,SAVECMT ; Write to tape (bank 4). + CALL BKSW3to4 + LD A,(RESULT) + OR A + JR NZ,FD2TERR + LDDE MSGFD2TOK + JR FDTMSG +FD2TERR: LDDE MSGFD2TERR + ; Print local message: DE = string in THIS bank (CR-terminated). + ; Uses monitor ROM MSG function directly (no bank switch needed). +FDTMSG: CALL NL + CALL MSG ; Print CR-terminated string at DE. + RET + + ;------------------------------------------------------------------------------- + ; TAPE2FD - Copy cassette tape file to floppy disk. + ; Loads from tape, then writes IPL header + data to floppy. + ;------------------------------------------------------------------------------- +TAPE2FD: LD HL,LOADTAPECP ; Load from tape (bank 4). + CALL BKSW3to4 + LD A,(RESULT) + OR A + JR NZ,T2FDERR + LD HL,SAVEFDX ; Write to floppy (bank 1). + CALL BKSW3to1 + LD A,(RESULT) + OR A + JR NZ,T2FDERR + LDDE MSGT2FDOK + JR FDTMSG +T2FDERR: LDDE MSGT2FDERR + JR FDTMSG + + ;------------------------------------------------------------------------------- + ; SAVEFDCARD - Save memory region to floppy disk (FC command). + ; Gets CMT parameters (name, start, size, exec) then writes to floppy. + ;------------------------------------------------------------------------------- +SAVEFDCARD: CALL GETCMTPARM ; Get parameters (same bank). + LD A,C + OR A + JR NZ,SFDC_ERR ; C=1 means input error. + LD A,001H ; Default file type = OBJ. + LD (ATRB),A + LD HL,SAVEFDX ; Write to floppy (bank 1). + CALL BKSW3to1 + LD A,(RESULT) + OR A + JR NZ,SFDC_ERR + LDDE MSGFCSAVEOK + JR FDTMSG +SFDC_ERR: LDDE MSGFCSAVEERR + JR FDTMSG + + ; Messages for floppy/tape transfer commands (local to bank 3). +MSGFDFNAME: DB "FILENAME? ", 00DH +MSGFD2TOK: DB "FLOPPY -> TAPE OK.", 00DH +MSGFD2TERR: DB "FLOPPY -> TAPE ERROR.", 00DH +MSGT2FDOK: DB "TAPE -> FLOPPY OK.", 00DH +MSGT2FDERR: DB "TAPE -> FLOPPY ERROR.", 00DH +MSGFCSAVEOK:DB "SAVED TO FLOPPY OK.", 00DH +MSGFCSAVEERR:DB "SAVE TO FLOPPY ERROR.", 00DH + ENDIF + + ;------------------------------------------------------------------------------- + ; END OF TAPE/SD/FD CMDLINE TOOLS FUNCTIONALITY ;------------------------------------------------------------------------------- @@ -402,7 +481,7 @@ GETCMTPARM: CALL READ4HEX ; Start CALL NL LDDE MSGSAVE ; 'FILENAME? ' LD HL,PRINTMSG - CALL BKSW2to6 ; Print out the filename. + CALL BKSW3to6 ; Print out the filename prompt. LD DE,BUFER CALL GETL LD HL,BUFER+10 diff --git a/asm/rfs_bank8.asm b/asm/rfs_bank8.asm index 73adc61..58b7240 100644 --- a/asm/rfs_bank8.asm +++ b/asm/rfs_bank8.asm @@ -586,13 +586,355 @@ CPY2SP_EX8: RET ; END OF METHODS ;------------------------------------------------------------------------------- + ;=============================================================================== + ; MZ-80A FLOPPY DISK CONTROLLER (SFD700) — Bank 8 (8K: E300-FFFF). + ; + ; Hardware-accelerated read using A10 address toggle: + ; F3FE: DD E9 = JP (IX) — spin loop (A10=0, DRQ low). + ; F7FE: FD E9 = JP (IY) — data handler (A10=1, DRQ high). + ; These are the ORIGINAL MZ-80A FI ROM addresses that BASIC expects. + ; + ; Bank 8 = ROMBANK8 = 12 (>= ROMBANK6), so E-page + F-page are both mapped. + ;=============================================================================== + IF BUILD_SFD700 = 1 + + ; MZ-80A specific data. +DSKID80A: DB 002H, "IPLPRO" +PRMBLK80A: DB 000H, 000H, 000H, 000H, 001H, 000H, 0CEH, 000H, 000H, 000H, 000H +ERRTONE8: DB "A0", 0D7H, "ARA", 0D7H, "AR", 00DH +FD80A_TSP EQU 0101EH ; In RAM, not ROM. + + ENDIF + ; Glass: label outside IF for global visibility. +FD80A_BOOT: + IF BUILD_SFD700 = 1 + PUSH DE + LD DE,BPARA + LD HL,PRMBLK80A + LD BC,11 + LDIR + POP DE + LD A,(DE) + CP 00DH + JR NZ,FD80A_GDSK + CALL FD80A_INIT +FD80A_PRM: LDDE MSGBOOTDRV + LD HL,PRINTMSG + CALL BKSW8to6 + LD DE,011A3H + CALL GETL + LD A,(DE) + CP 01BH + JP Z,FD80A_BRK + LD HL,19 + ADD HL,DE + LD A,(HL) + CP 00DH + JR Z,FD80A_RD1 +FD80A_GDSK: CALL HEX + JR C,FD80A_PRM + DEC A + CP 004H + JR NC,FD80A_PRM + LD (BPARA),A +FD80A_RD1: LD IX,BPARA + CALL FD80A_READ + LD HL,0CE00H + LD DE,DSKID80A + LD B,007H +FD80A_CHK: LD C,(HL) + LD A,(DE) + CP C + JP NZ,FD80A_NMS + INC HL + INC DE + DJNZ FD80A_CHK + LDDE MSGIPLLOAD + LD HL,PRINTMSG + CALL BKSW8to6 + LD DE,0CE07H + LD HL,PRTFN + CALL BKSW8to6 + LD IX,BPARA + LD HL,(0CE16H) + LD (IX+5),L + LD (IX+6),H + LD HL,(0CE14H) + LD (IX+3),L + LD (IX+4),H + LD HL,(0CE1EH) + LD (IX+1),L + LD (IX+2),H + CALL FD80A_READ + CALL FD80A_INIT + LD HL,(0CE18H) + JP (HL) + +FD80A_LERR: LDDE MSGLOADERR + JR FD80A_ERR +FD80A_NMS: LDDE MSGDSKNOTMST +FD80A_ERR: LD HL,(TMPSTACKP) + LD (FD80A_TSP),HL + LD HL,PRINTMSG + CALL BKSW8to6 + LD DE,ERRTONE8 + CALL MELDY + LD HL,(FD80A_TSP) + LD SP,HL + RET +FD80A_BRK: LD SP,(TMPSTACKP) + RET + + ; --- Low-level FDC routines --- +FD80A_RDY: LD A,(MOTON) + RRCA + CALL NC,FD80A_MON + LD A,(IX+0) + OR 084H + OUT (FDC_DRIVE),A + XOR A + LD (FDCCMD),A + LD HL,0 +FD80A_RDW: DEC HL + LD A,H + OR L + JP Z,FD80A_DSKE + IN A,(FDC_CMD) + CPL + RLCA + JR C,FD80A_RDW + LD C,(IX+0) + LD HL,TRK0FD1 + LD B,0 + ADD HL,BC + BIT 0,(HL) + RET NZ + CALL FD80A_SK0 + SET 0,(HL) + RET +FD80A_MON: LD A,080H + OUT (FDC_DRIVE),A + LD B,16 +FD80A_MNW: CALL FD80A_DL60 + DJNZ FD80A_MNW + LD A,1 + LD (MOTON),A + RET +FD80A_SEEK: LD A,01BH + CALL FD80A_CMD + AND 099H + RET +FD80A_INIT: XOR A + OUT (FDC_DRIVE),A + LD (TRK0FD1),A + LD (TRK0FD2),A + LD (TRK0FD3),A + LD (TRK0FD4),A + LD (MOTON),A + RET +FD80A_SK0: LD A,00BH + CALL FD80A_CMD + AND 085H + XOR 004H + RET Z + JP FD80A_DSKE +FD80A_CMD: LD (FDCCMD),A + CPL + OUT (FDC_CMD),A + CALL FD80A_WBR + IN A,(FDC_CMD) + CPL + RET +FD80A_WBR: PUSH DE + PUSH HL + CALL FD80A_DL7 + LD E,7 +FD80A_WB1: LD HL,0 +FD80A_WB2: DEC HL + LD A,H + OR L + JR Z,FD80A_WB3 + IN A,(FDC_CMD) + CPL + RRCA + JR C,FD80A_WB2 + POP HL + POP DE + RET +FD80A_WB3: DEC E + JR NZ,FD80A_WB1 + POP HL + POP DE + JP FD80A_DSKE +FD80A_WBA: PUSH DE + PUSH HL + CALL FD80A_DL7 + LD E,7 +FD80A_WA1: LD HL,0 +FD80A_WA2: DEC HL + LD A,H + OR L + JR Z,FD80A_WA3 + IN A,(FDC_CMD) + CPL + RRCA + JR NC,FD80A_WA2 + POP HL + POP DE + RET +FD80A_WA3: DEC E + JR NZ,FD80A_WA1 + POP HL + POP DE + JP FD80A_DSKE + + ; --- Hardware-accelerated sector read --- + ; Uses A10 toggle: F3FE=JP(IX) spin, F7FE=JP(IY) data. +FD80A_READ: CALL FD80A_CNV +FD80A_R1: CALL FD80A_PRS +FD80A_R2: CALL FD80A_SID + CALL FD80A_SEEK + JR NZ,FD80A_RET + CALL FD80A_STR + PUSH IX + LD IX,0F3FEH ; Spin loop at F3FE (original FI ROM addr). + LD IY,FD80A_INI ; Data handler. + LD A,094H + CALL FD80A_CM2 +FD80A_R3: LD B,0 + JP (IX) ; Enter spin loop. +FD80A_INI: INI + JP NZ,0F3FEH ; Back to spin loop. + POP IX + INC (IX+8) + LD A,(IX+8) + PUSH IX + LD IX,0F3FEH + CP 17 + JR Z,FD80A_SEC + DEC D + JR NZ,FD80A_R3 + JR FD80A_RDN +FD80A_SEC: DEC D +FD80A_RDN: CALL FD80A_FI + POP IX + IN A,(FDC_CMD) + CPL + AND 0FFH + JR NZ,FD80A_RET + CALL FD80A_ADJ + JP Z,FD80A_END + LD A,(IX+7) + JR FD80A_R2 +FD80A_RET: LD A,(RETRIES) + DEC A + LD (RETRIES),A + JP Z,FD80A_DSKE + CALL FD80A_SK0 + JR FD80A_R1 +FD80A_END: LD A,080H + OUT (FDC_DRIVE),A + RET + + ; --- Helpers --- +FD80A_PRS: CALL FD80A_RDY + LD D,(IX+4) + LD A,(IX+3) + OR A + JR Z,FD80A_PS1 + INC D +FD80A_PS1: LD A,(IX+10) + LD (IX+8),A + LD A,(IX+9) + LD (IX+7),A + LD L,(IX+5) + LD H,(IX+6) + RET +FD80A_SID: SRL A + CPL + OUT (FDC_DATA),A + JR NC,FD80A_S0 + LD A,1 + JR FD80A_S1 +FD80A_S0: XOR A +FD80A_S1: CPL + OUT (FDC_SIDE),A + RET +FD80A_STR: LD C,FDC_DATA + LD A,(IX+7) + SRL A + CPL + OUT (FDC_TRACK),A + LD A,(IX+8) + CPL + OUT (FDC_SECTOR),A + RET +FD80A_ADJ: LD A,(IX+8) + CP 17 + JR NZ,FD80A_AJ1 + LD A,1 + LD (IX+8),A + INC (IX+7) +FD80A_AJ1: LD A,D + OR A + RET +FD80A_CNV: LD B,0 + LD DE,16 + LD L,(IX+1) + LD H,(IX+2) + XOR A +FD80A_CN1: SBC HL,DE + JR C,FD80A_CN2 + INC B + JR FD80A_CN1 +FD80A_CN2: ADD HL,DE + LD H,B + INC L + LD (IX+9),H + LD (IX+10),L + LD A,10 + LD (RETRIES),A + RET +FD80A_CM2: LD (FDCCMD),A + CPL + OUT (FDC_CMD),A + CALL FD80A_WBA + RET +FD80A_FI: LD A,0D8H + CPL + OUT (FDC_CMD),A + CALL FD80A_WBR + RET +FD80A_DSKE: CALL FD80A_INIT + JP FD80A_LERR +FD80A_DL7: PUSH DE + LD DE,7 + JR FD80A_DLP +FD80A_DL60: PUSH DE + LD DE,01013H +FD80A_DLP: DEC DE + LD A,E + OR D + JR NZ,FD80A_DLP + POP DE + RET + + ; --- Hardware acceleration alignment points (F-page) --- + ; JP (IX) at F3FE: A10=0, DRQ low spin loop. + ALIGN_NOPS 0F3FEH + JP (IX) ; DD E9 at F3FE/F3FF. + ; JP (IY) at F7FE: A10=1, DRQ high data handler. + ALIGN_NOPS 0F7FEH + JP (IY) ; FD E9 at F7FE/F7FF. + + ; SFD700 - Pad to end of 8K bank (F-page through FFFFH). + ALIGN 10000H + + ENDIF ; BUILD_SFD700 + ; RomDisk - Pad to EFFF boundary. IF BUILD_ROMDISK+BUILD_PICOZ80 = 1 ALIGN 0EFF8h ORG 0EFF8h - DB 0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh - ENDIF - ; SFD700 - Pad to 10000H - IF BUILD_SFD700 = 1 - ALIGN 10000H + DB 0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh,0FFh ENDIF diff --git a/roms/rfs.rom b/roms/rfs.rom index 15e07382e4a3d02d3b48b6406cdd5330ddaab41e..b7cbc8c19456ed9377ab37d9929c53ef7ae14aa8 100644 GIT binary patch delta 6543 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 diff --git a/tools/MZFD/MZFDTool b/tools/MZFD/MZFDTool new file mode 100755 index 0000000000000000000000000000000000000000..8c9b9970d7fcc764451062df17981c0d4ab25c56 GIT binary patch literal 21752 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 + * +01: FileName (17 bytes, CR-terminated, space-padded) + * +12: LockFlag (1 byte) + * +13: DummyFlag (1 byte) + * +14: FileSize (2 bytes LE, in bytes) + * +16: LoadAddress (2 bytes LE) + * +18: ExecAddress (2 bytes LE) + * +1A: Reserved (4 bytes) + * +1E: SectorAddress (2 bytes LE, logical) + * + * Usage: + * MZFDTool format [-o disk.img] Format empty disk image + * MZFDTool dir [-o disk.img] List directory + * MZFDTool add [-o disk.img] Add MZF file to directory + * MZFDTool extract [-o disk.img] Extract file to MZF + * MZFDTool boot [-o disk.img] Set boot program + * ========================================================================= */ + +#define VERSION "2.00" + +#include +#include +#include +#include +#include + +#define CYLS 40 +#define HEADS 2 +#define SECTORS 16 +#define SECSIZE 256 +#define TOTAL_SECTORS (CYLS * HEADS * SECTORS) +#define IMGSIZE (TOTAL_SECTORS * SECSIZE) +#define DIR_FIRST_SEC 16 /* First directory logical sector */ +#define DIR_SECTORS 16 /* Number of directory sectors */ +#define DIR_ENTRIES 8 /* Entries per sector */ +#define DATA_FIRST_SEC 32 /* First data logical sector */ +#define MAXFILETYPES 12 +#define DEFAULTIMAGE "MZ700.img" +#define CMTHDRSIZE 128 /* MZF/CMT file header size */ +#define BOOT_SIG_TYPE 3 /* MZ-700 boot type */ +#define BOOT_SIG "IPLPRO" +#define BOOT_SIG_LEN 6 + +#pragma pack(push, 1) + +/* Boot sector structure (first 32 bytes of logical sector 0) */ +struct BootSector { + uint8_t type; /* +00: 03 = MZ-700 bootable */ + char signature[6]; /* +01: "IPLPRO" */ + char name[11]; /* +07: Program name (CR-terminated) */ + uint16_t loadAddress; /* +12: Load address */ + uint16_t fileSize; /* +14: File size in bytes */ + uint8_t reserved[8]; /* +16: Reserved */ + uint16_t sectorAddress; /* +1E: Start data sector */ +}; + +/* Directory entry (32 bytes) */ +struct DirEntry { + uint8_t fileType; /* +00: 01=OBJ, 02=BTX, 00=deleted */ + char fileName[17]; /* +01: CR-terminated, space-padded */ + uint8_t lockFlag; /* +12: Lock flag */ + uint8_t dummyFlag; /* +13: Reserved */ + uint16_t fileSize; /* +14: File size in bytes */ + uint16_t loadAddress; /* +16: Load address */ + uint16_t execAddress; /* +18: Exec address */ + uint8_t dummy[4]; /* +1A: Reserved */ + uint16_t sectorAddress; /* +1E: Start sector (logical) */ +}; + +/* MZF/CMT tape file header (128 bytes) */ +struct CMTHeader { + uint8_t attribute; /* File type: 01=OBJ, 02=BTX, etc. */ + char name[17]; /* Filename (CR-terminated) */ + uint16_t size; /* Data size */ + uint16_t loadAddress; /* Load address */ + uint16_t execAddress; /* Execution address */ + char comment[104]; /* Comment area */ +}; + +#pragma pack(pop) + +static const char *FileTypes[MAXFILETYPES] = { + "???", "OBJ", "BTX", "BSD", "BRD", "RB ", + "???", "LIB", "???", "???", "SYS", "GR " +}; + +static uint8_t diskImage[IMGSIZE]; +static char imgFileName[256] = DEFAULTIMAGE; + +/* ------- Sector I/O helpers ------- */ + +/* Convert logical sector to byte offset in raw image */ +static int sec_offset(int logical_sec) +{ + /* Logical sector mapping: + * phys_sector = logical % 16 + 1 (but stored 0-based in image) + * head = (logical / 16) % 2 + * cylinder = logical / 16 / 2 + * Raw image is sequential: cyl0/head0/sec1..16, cyl0/head1/sec1..16, ... */ + return logical_sec * SECSIZE; +} + +/* Invert a buffer (XOR 0xFF) — MZ-700 data bus inversion */ +static void invert_buf(uint8_t *buf, int len) +{ + for (int i = 0; i < len; i++) + buf[i] ^= 0xFF; +} + +/* Read a logical sector from image into buffer (un-inverted) */ +static void read_sector(int logical_sec, uint8_t *buf) +{ + memcpy(buf, &diskImage[sec_offset(logical_sec)], SECSIZE); + invert_buf(buf, SECSIZE); +} + +/* Write a buffer to a logical sector in image (inverts before writing) */ +static void write_sector(int logical_sec, const uint8_t *buf) +{ + memcpy(&diskImage[sec_offset(logical_sec)], buf, SECSIZE); + invert_buf(&diskImage[sec_offset(logical_sec)], SECSIZE); +} + +/* Load disk image from file */ +static int load_image(void) +{ + FILE *f = fopen(imgFileName, "rb"); + if (!f) { + fprintf(stderr, "ERROR: Cannot open '%s'\n", imgFileName); + return 0; + } + size_t n = fread(diskImage, 1, IMGSIZE, f); + fclose(f); + if (n != IMGSIZE) { + fprintf(stderr, "ERROR: '%s' is not a valid disk image (%zu bytes, expected %d)\n", + imgFileName, n, IMGSIZE); + return 0; + } + return 1; +} + +/* Save disk image to file */ +static int save_image(void) +{ + FILE *f = fopen(imgFileName, "wb"); + if (!f) { + fprintf(stderr, "ERROR: Cannot write '%s'\n", imgFileName); + return 0; + } + fwrite(diskImage, IMGSIZE, 1, f); + fclose(f); + return 1; +} + +/* Format a filename for display: stop at CR, null-terminate */ +static void format_name(char *dst, const char *src, int maxlen) +{ + memcpy(dst, src, maxlen); + dst[maxlen] = '\0'; + for (int i = 0; i < maxlen; i++) { + if (dst[i] == '\r' || dst[i] == '\0') { dst[i] = '\0'; break; } + } +} + +/* ------- Commands ------- */ + +/* Format: create an empty formatted disk image */ +static void cmd_format(void) +{ + uint8_t sec[SECSIZE]; + + printf("Formatting '%s' (%d bytes, %d sectors)\n", imgFileName, IMGSIZE, TOTAL_SECTORS); + + /* Fill entire image with 0xFF (which inverts to 0x00 = empty) */ + memset(diskImage, 0xFF, IMGSIZE); + + /* Write volume header in directory sector 16, entry 0 */ + memset(sec, 0, SECSIZE); + sec[0] = 0x81; /* Volume header marker */ + sec[1] = 'O'; /* Volume type */ + /* +14: capacity hint = 100 (not used by RFS) */ + sec[0x14] = 100; + /* +1E: next free sector (first data sector) */ + sec[0x1E] = DATA_FIRST_SEC & 0xFF; + sec[0x1F] = DATA_FIRST_SEC >> 8; + write_sector(DIR_FIRST_SEC, sec); + + if (!save_image()) exit(1); + puts("Done."); +} + +/* Dir: list directory contents */ +static void cmd_dir(void) +{ + uint8_t sec[SECSIZE]; + char name[18]; + int files = 0; + + if (!load_image()) exit(1); + + /* Check boot sector */ + read_sector(0, sec); + struct BootSector *boot = (struct BootSector *)sec; + if (boot->type == BOOT_SIG_TYPE && memcmp(boot->signature, BOOT_SIG, BOOT_SIG_LEN) == 0) { + format_name(name, boot->name, 11); + printf("\nBoot program: %-17s Size=%u Load=0x%04X Sector=%u\n", + name, boot->fileSize, boot->loadAddress, boot->sectorAddress); + } else { + printf("\nDisk is not bootable.\n"); + } + + printf("\nDirectory of '%s':\n\n", imgFileName); + printf(" %-17s %-3s %5s %s %4s %4s %6s (C H S)\n", + "Name", "Typ", "Size", "L", "Load", "Exec", "Sector"); + printf(" %-17s %-3s %5s %s %4s %4s %6s %s\n", + "-----------------", "---", "-----", "-", "----", "----", "------", "-------"); + + for (int ds = 0; ds < DIR_SECTORS; ds++) { + read_sector(DIR_FIRST_SEC + ds, sec); + + for (int e = 0; e < DIR_ENTRIES; e++) { + /* Skip entry 0 of first directory sector (volume header) */ + if (ds == 0 && e == 0) continue; + + struct DirEntry *ent = (struct DirEntry *)&sec[e * sizeof(struct DirEntry)]; + if (ent->fileType == 0x00 || ent->fileType >= 0x80) + continue; + + uint8_t ft = ent->fileType; + if (ft >= MAXFILETYPES) ft = 0; + format_name(name, ent->fileName, 17); + + int ls = ent->sectorAddress; + int ps = ls % 16 + 1; + int hd = (ls / 16) % 2; + int cy = ls / 16 / 2; + + printf(" %-17s %s %5u %c %04X %04X %5u (%2d %d %2d)\n", + name, FileTypes[ft], ent->fileSize, + ent->lockFlag ? 'X' : ' ', + ent->loadAddress, ent->execAddress, + ent->sectorAddress, cy, hd, ps); + files++; + } + } + + /* Calculate free space */ + int highSec = DATA_FIRST_SEC; + for (int ds = 0; ds < DIR_SECTORS; ds++) { + read_sector(DIR_FIRST_SEC + ds, sec); + for (int e = 0; e < DIR_ENTRIES; e++) { + struct DirEntry *ent = (struct DirEntry *)&sec[e * sizeof(struct DirEntry)]; + if (ent->fileType > 0 && ent->fileType < 0x80 && ent->sectorAddress > 0) { + int endSec = ent->sectorAddress + (ent->fileSize + SECSIZE - 1) / SECSIZE; + if (endSec > highSec) highSec = endSec; + } + } + } + printf("\n%d file(s), %d sectors used, %d sectors free\n", + files, highSec, TOTAL_SECTORS - highSec); +} + +/* Add: add an MZF file to the disk directory */ +static void cmd_add(const char *mzfFileName) +{ + FILE *mzfFile; + uint8_t sec[SECSIZE]; + struct CMTHeader cmtHdr; + char name[18]; + + if (!load_image()) exit(1); + + /* Open and read MZF file */ + mzfFile = fopen(mzfFileName, "rb"); + if (!mzfFile) { + fprintf(stderr, "ERROR: Cannot open '%s'\n", mzfFileName); + exit(1); + } + fseek(mzfFile, 0, SEEK_END); + long mzfSize = ftell(mzfFile); + fseek(mzfFile, 0, SEEK_SET); + + if (mzfSize < CMTHDRSIZE) { + fprintf(stderr, "ERROR: '%s' too small for MZF format (%ld bytes)\n", mzfFileName, mzfSize); + fclose(mzfFile); + exit(1); + } + fread(&cmtHdr, CMTHDRSIZE, 1, mzfFile); + uint16_t dataSize = cmtHdr.size; + if (dataSize == 0) dataSize = (uint16_t)(mzfSize - CMTHDRSIZE); + + /* Find free directory entry and highest used sector */ + int freeDirSec = -1, freeDirIdx = -1; + int highSec = DATA_FIRST_SEC; + + for (int ds = 0; ds < DIR_SECTORS; ds++) { + read_sector(DIR_FIRST_SEC + ds, sec); + for (int e = 0; e < DIR_ENTRIES; e++) { + if (ds == 0 && e == 0) continue; /* Skip volume header */ + struct DirEntry *ent = (struct DirEntry *)&sec[e * sizeof(struct DirEntry)]; + + if (ent->fileType == 0x00 && freeDirSec < 0) { + freeDirSec = ds; + freeDirIdx = e; + } + if (ent->fileType > 0 && ent->fileType < 0x80 && ent->sectorAddress > 0) { + int endSec = ent->sectorAddress + (ent->fileSize + SECSIZE - 1) / SECSIZE; + if (endSec > highSec) highSec = endSec; + } + } + } + + if (freeDirSec < 0) { + fprintf(stderr, "ERROR: Directory full\n"); + fclose(mzfFile); + exit(1); + } + + int dataSectors = (dataSize + SECSIZE - 1) / SECSIZE; + if (highSec + dataSectors > TOTAL_SECTORS) { + fprintf(stderr, "ERROR: Not enough space (need %d sectors, have %d)\n", + dataSectors, TOTAL_SECTORS - highSec); + fclose(mzfFile); + exit(1); + } + + /* Write file data sectors */ + for (int s = 0; s < dataSectors; s++) { + memset(sec, 0, SECSIZE); + fread(sec, 1, SECSIZE, mzfFile); + write_sector(highSec + s, sec); + } + fclose(mzfFile); + + /* Update directory entry */ + read_sector(DIR_FIRST_SEC + freeDirSec, sec); + struct DirEntry *ent = (struct DirEntry *)&sec[freeDirIdx * sizeof(struct DirEntry)]; + memset(ent, 0, sizeof(struct DirEntry)); + + ent->fileType = cmtHdr.attribute; + if (ent->fileType == 0x05) ent->fileType = 0x02; /* CMT type 05 → FD type 02 (BTX) */ + memcpy(ent->fileName, cmtHdr.name, 17); + ent->fileSize = dataSize; + ent->loadAddress = cmtHdr.loadAddress; + ent->execAddress = cmtHdr.execAddress; + ent->sectorAddress = highSec; + write_sector(DIR_FIRST_SEC + freeDirSec, sec); + + format_name(name, cmtHdr.name, 17); + printf(" Added: \"%s\" type=%s size=%u load=0x%04X exec=0x%04X sector=%d\n", + name, FileTypes[ent->fileType < MAXFILETYPES ? ent->fileType : 0], + dataSize, cmtHdr.loadAddress, cmtHdr.execAddress, highSec); + printf(" %d sectors free\n", TOTAL_SECTORS - highSec - dataSectors); + + if (!save_image()) exit(1); +} + +/* Extract: extract a file from disk to MZF format */ +static void cmd_extract(const char *fileName) +{ + uint8_t sec[SECSIZE]; + char entName[18], outName[256]; + + if (!load_image()) exit(1); + + /* Search directory for matching filename */ + static struct DirEntry foundEntry; + int foundIt = 0; + + for (int ds = 0; ds < DIR_SECTORS && !foundIt; ds++) { + read_sector(DIR_FIRST_SEC + ds, sec); + for (int e = 0; e < DIR_ENTRIES && !foundIt; e++) { + if (ds == 0 && e == 0) continue; + struct DirEntry *ent = (struct DirEntry *)&sec[e * sizeof(struct DirEntry)]; + if (ent->fileType > 0 && ent->fileType < 0x80) { + format_name(entName, ent->fileName, 17); + if (strcmp(fileName, entName) == 0) { + memcpy(&foundEntry, ent, sizeof(foundEntry)); + foundIt = 1; + } + } + } + } + + if (!foundIt) { + /* Also check boot sector */ + read_sector(0, sec); + struct BootSector *boot = (struct BootSector *)sec; + if (boot->type == BOOT_SIG_TYPE && memcmp(boot->signature, BOOT_SIG, BOOT_SIG_LEN) == 0) { + format_name(entName, boot->name, 11); + if (strcmp(fileName, entName) == 0) { + memset(&foundEntry, 0, sizeof(foundEntry)); + foundEntry.fileType = 0x01; + memcpy(foundEntry.fileName, boot->name, 11); + foundEntry.fileSize = boot->fileSize; + foundEntry.loadAddress = boot->loadAddress; + foundEntry.sectorAddress = boot->sectorAddress; + foundIt = 1; + } + } + } + + if (!foundIt) { + fprintf(stderr, "ERROR: '%s' not found on disk\n", fileName); + exit(1); + } + + struct DirEntry *found = &foundEntry; + + /* Build output filename */ + snprintf(outName, sizeof(outName), "%s.mzf", fileName); + for (char *p = outName; *p; p++) { + if (*p == ' ') *p = '_'; + } + + /* Write MZF file */ + FILE *out = fopen(outName, "wb"); + if (!out) { + fprintf(stderr, "ERROR: Cannot create '%s'\n", outName); + exit(1); + } + + struct CMTHeader hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.attribute = (found->fileType == 0x02) ? 0x05 : found->fileType; + memcpy(hdr.name, found->fileName, 17); + hdr.size = found->fileSize; + hdr.loadAddress = found->loadAddress; + hdr.execAddress = found->execAddress; + snprintf(hdr.comment, sizeof(hdr.comment), "Extracted by MZFDTool V%s", VERSION); + fwrite(&hdr, sizeof(hdr), 1, out); + + /* Read and write data sectors */ + uint16_t remaining = found->fileSize; + int curSec = found->sectorAddress; + while (remaining > 0) { + read_sector(curSec, sec); + uint16_t chunk = (remaining > SECSIZE) ? SECSIZE : remaining; + fwrite(sec, 1, chunk, out); + remaining -= chunk; + curSec++; + } + + fclose(out); + format_name(entName, found->fileName, 17); + printf(" Extracted: \"%s\" → %s (%u bytes)\n", entName, outName, found->fileSize); +} + +/* Boot: set boot program from MZF file */ +static void cmd_boot(const char *mzfFileName) +{ + FILE *mzfFile; + uint8_t sec[SECSIZE]; + struct CMTHeader cmtHdr; + char name[18]; + + if (!load_image()) exit(1); + + mzfFile = fopen(mzfFileName, "rb"); + if (!mzfFile) { + fprintf(stderr, "ERROR: Cannot open '%s'\n", mzfFileName); + exit(1); + } + fseek(mzfFile, 0, SEEK_END); + long mzfSize = ftell(mzfFile); + fseek(mzfFile, 0, SEEK_SET); + + if (mzfSize < CMTHDRSIZE) { + fprintf(stderr, "ERROR: '%s' too small for MZF format\n", mzfFileName); + fclose(mzfFile); + exit(1); + } + fread(&cmtHdr, CMTHDRSIZE, 1, mzfFile); + uint16_t dataSize = cmtHdr.size; + if (dataSize == 0) dataSize = (uint16_t)(mzfSize - CMTHDRSIZE); + + /* Find space for boot data — use sectors 1-15 (logical), before directory */ + int dataSectors = (dataSize + SECSIZE - 1) / SECSIZE; + int bootDataStart = 1; /* Sector 1 (sector 0 is the boot sector itself) */ + if (dataSectors > 15) { + fprintf(stderr, "ERROR: Boot program too large (%d sectors, max 15)\n", dataSectors); + fclose(mzfFile); + exit(1); + } + + /* Write boot data to sectors 1-15 */ + for (int s = 0; s < dataSectors; s++) { + memset(sec, 0, SECSIZE); + fread(sec, 1, SECSIZE, mzfFile); + write_sector(bootDataStart + s, sec); + } + fclose(mzfFile); + + /* Write boot sector (sector 0) */ + memset(sec, 0, SECSIZE); + struct BootSector *boot = (struct BootSector *)sec; + boot->type = BOOT_SIG_TYPE; + memcpy(boot->signature, BOOT_SIG, BOOT_SIG_LEN); + format_name(name, cmtHdr.name, 11); + memcpy(boot->name, cmtHdr.name, 11); + boot->loadAddress = cmtHdr.loadAddress; + boot->fileSize = dataSize; + boot->sectorAddress = bootDataStart; + write_sector(0, sec); + + format_name(name, cmtHdr.name, 11); + printf(" Boot set: \"%s\" size=%u load=0x%04X exec=0x%04X sector=%d\n", + name, dataSize, cmtHdr.loadAddress, cmtHdr.execAddress, bootDataStart); + + if (!save_image()) exit(1); +} + +/* ------- Usage and main ------- */ + +static void usage(void) +{ + printf("Usage:\n"); + printf(" MZFDTool format [-o disk.img] Format empty disk image\n"); + printf(" MZFDTool dir [-o disk.img] List directory\n"); + printf(" MZFDTool add [-o disk.img] Add MZF file to disk\n"); + printf(" MZFDTool extract [-o disk.img] Extract file to MZF\n"); + printf(" MZFDTool boot [-o disk.img] Set boot program\n"); + printf("\n"); + printf("Options:\n"); + printf(" -o Disk image file (default: %s)\n", DEFAULTIMAGE); + printf("\n"); + printf("Disk geometry: %d cyls, %d heads, %d sectors, %d bytes/sector = %d bytes\n", + CYLS, HEADS, SECTORS, SECSIZE, IMGSIZE); + printf("\n"); +} + +int main(int argc, char *argv[]) +{ + printf("\nMZFDTool V%s (c) 2002 BKK, 2026 Philip Smart\n\n", VERSION); + + if (argc < 2) { + usage(); + return 1; + } + + /* Parse -o option from any position */ + for (int i = 1; i < argc - 1; i++) { + if (strcmp("-o", argv[i]) == 0) { + strncpy(imgFileName, argv[i + 1], sizeof(imgFileName) - 1); + imgFileName[sizeof(imgFileName) - 1] = '\0'; + for (int j = i; j < argc - 2; j++) + argv[j] = argv[j + 2]; + argc -= 2; + break; + } + } + + if (strcmp("format", argv[1]) == 0) { + cmd_format(); + } else if (strcmp("dir", argv[1]) == 0) { + cmd_dir(); + } else if (strcmp("add", argv[1]) == 0) { + if (argc < 3) { + fprintf(stderr, "ERROR: No MZF file specified\n"); + return 2; + } + cmd_add(argv[2]); + } else if (strcmp("extract", argv[1]) == 0) { + if (argc < 3) { + fprintf(stderr, "ERROR: No filename specified\n"); + return 2; + } + cmd_extract(argv[2]); + } else if (strcmp("boot", argv[1]) == 0) { + if (argc < 3) { + fprintf(stderr, "ERROR: No MZF file specified\n"); + return 2; + } + cmd_boot(argv[2]); + } else { + fprintf(stderr, "ERROR: Unknown command '%s'\n", argv[1]); + usage(); + return 1; + } + + return 0; +} diff --git a/tools/MZFD/MZFDTool.c.original b/tools/MZFD/MZFDTool.c.original new file mode 100755 index 0000000..8aa168f --- /dev/null +++ b/tools/MZFD/MZFDTool.c.original @@ -0,0 +1,630 @@ +#define VERSION "1.01" + +#include +#include +#include +#include +#include +#include + + + +union REGS regs; +struct SREGS sregs; + + +struct MZ700BootStruct +{ + unsigned char Type; + char Signature[6]; + char Name[11]; + unsigned short StartAddress; + unsigned short FileSize; + unsigned char Dummy[8]; + unsigned short SectorAddress; +}; + + +struct MZ700DirectoryStruct +{ + unsigned char FileType; // 0x00 -> 1 + char FileName[17]; // 0x01 ... 0x11 -> 17 + unsigned char LockFlag; // 0x12 -> 1 + unsigned char DummyFlag; // 0x13 -> 1 + unsigned short FileSize; // 0x14 ... 0x15 -> 2 + unsigned short LoadAddress; // 0x16 ... 0x17 -> 2 + unsigned short ExecAddress; // 0x18 ... 0x19 -> 2 + char Dummy[4]; // 0x1A ... 0x1D -> 4 + unsigned short SectorAddress; // 0x1E ... 0x1F -> 2 +}; + + +struct CMTHeader +{ + unsigned char Attribute; + char Name[17]; + unsigned short Size; + unsigned short LoadAddress; + unsigned short ExecAddress; + char Comment[104]; +}; + + + + +char FileTypes[][4] = { + "???", + "OBJ", + "BTX", + "BSD", + "BRD", + "RB ", + "???", + "LIB", + "???", + "???", + "SYS", + "GR " + }; + + + +unsigned char DiskParameters[11] = + {0xDF, 0x02, 0x25, 0x01, 16, 0x4E, 0xFF, 0x6C, 0xE5, 100, 8}; +// {0xDF, 0x02, 0x25, 0x02, 18, 0x1B, 0xFF, 0x6C, 0xF6, 100, 8}; /* 1.44MB */ + + +char *ErrorMsg[] = +{ + "success", + "invalid function", + "address mark not found", + "disk write-protected", + "sector not found / read error", + "reset faild", + "data did not verify correctly", + "disk changed", + "drive parameter activity failed", + "DMA overrun", + "data boundary error", + "bad sector detected", + "bad track detected", + "unsupported track or invalid media", + "invalid number of sectors", + "control data address mark detected", + "DMA arbitration level out of range", + "uncorrectable CRC or ECCerror", + "data ECC corrected" +}; + + +unsigned char SectorBuffer[1024]; + +unsigned char Drive; +unsigned char Cylinder; +unsigned char Head; +unsigned char Sector; + + +unsigned int OrgInt1EOffset; +unsigned int OrgInt1ESegment; + + + + +void Error(unsigned char Index) +{ + printf("Error: %02X - ", Index); + if(Index < 0x12) printf("%s\n", ErrorMsg[Index]); + if(Index == 0x80) puts("No disc in drive!"); + + exit(1); +} + + + + +void ShowSectorBuffer(void) +{ + unsigned char ByteCounter; + unsigned char LineCounter; + int Counter; + + + Counter = 0; + + for(LineCounter = 0; LineCounter < 16; LineCounter++) + { + printf("\n%04X : ", Counter); + for(ByteCounter = 0; ByteCounter < 16; ByteCounter++) + { + printf("%02X ", SectorBuffer[Counter + ByteCounter]); + } + printf(" "); + for(ByteCounter = 0; ByteCounter < 16; ByteCounter++) + { + printf("%c", iscntrl(SectorBuffer[Counter]) ? ' ' : SectorBuffer[Counter]); + Counter++; + } + } + puts(""); +} + + + + +int ResetFloppy(char Drive) +{ + regs.h.ah = 0; + regs.h.dl = Drive; + + int86(0x13, ®s, ®s); + + return(regs.h.ah); +} + + + + +int ReadSector(char Drive, char Cylinder, char Head, char Sector) +{ + int Counter; + + + if(Head == 0) Head = 1; + else Head = 0; + + + regs.h.ah = 0x02; + Counter = 3; + + while((regs.h.ah != 0) && (Counter != 0)) + { + regs.h.ah = 0x02; + regs.h.al = 1; + regs.h.ch = Cylinder; + regs.h.cl = Sector; + regs.h.dh = Head; + regs.h.dl = Drive; + + sregs.es = FP_SEG(SectorBuffer); + regs.x.bx = FP_OFF(SectorBuffer); + + int86x(0x13, ®s, ®s, &sregs); + Counter--; + } + + for(Counter = 0; Counter < sizeof(SectorBuffer); Counter++) + SectorBuffer[Counter] = ~SectorBuffer[Counter]; + + return(regs.x.ax); +} + + + +void SaveInt1E(void) +{ + OrgInt1EOffset = peek(0x0000, 0x1E * 4); + OrgInt1ESegment = peek(0x0000, 0x1E * 4 + 2); +} + + + + +void SetInt1E(unsigned char Type) +{ + Type++; + asm cli; + poke(0x0000, 0x1E * 4, FP_OFF(DiskParameters)); + poke(0x0000, 0x1E * 4 + 2, FP_SEG(DiskParameters)); + asm sti; +} + + + + +void ResetInt1E(void) +{ + asm cli; + poke(0x0000, 0x1E * 4, OrgInt1EOffset); + poke(0x0000, 0x1E * 4 + 2, OrgInt1ESegment); + asm sti; +} + + + + +void ShowDirectory(void) +{ + unsigned char SectorCounter; + unsigned char Counter; + unsigned char C, H, S; + struct MZ700BootStruct *Boot; + struct MZ700DirectoryStruct *Entry; + char FileName[18]; + unsigned int Result; +// unsigned short LastUsedSector; + + + puts(""); + + Result = ReadSector(Drive, 0, 0, 1); + Result = Result >> 8; + if(Result != 0) Error(Result); + Boot = (struct MZ700BootStruct *) SectorBuffer; + memcpy(FileName, Boot->Signature, 6); + FileName[6] = '\0'; + if((Boot->Type == 3) && (strcmp(FileName, "IPLPRO") == 0)) + { + puts("Floppy is bootable:"); + puts(""); + + memcpy(FileName, Boot->Name, 11); + if(strchr(FileName, 0x0D) != NULL) *strchr(FileName, 0x0D) = '\0'; + else FileName[11] = '\0'; + + printf(" %-17s OBJ %5u ", FileName, Boot->FileSize); + S = Boot->SectorAddress % 16 + 1; + H = (Boot->SectorAddress / 16) % 2; + C = Boot->SectorAddress / 16 / 2; + printf(" %4u (%2u %u %2u)\n", Boot->SectorAddress, C, H, S); + } + else + { + puts("Floppy is not bootable."); + } + + puts(""); + puts("Directory:"); + puts(""); + puts(" Name Type Size Lock Load Exec Pos ( C H S)"); + puts(""); + + for(SectorCounter = 1; SectorCounter < 17; SectorCounter++) + { + ReadSector(Drive, 0, 1, SectorCounter); + + + + for(Counter = 0; Counter < 256 / 32; Counter++) + { + Entry = (struct MZ700DirectoryStruct *) &SectorBuffer[Counter * sizeof(struct MZ700DirectoryStruct)]; + +// if((SectorCounter == 1) && (Counter == 0)) LastUsedSector = Entry->SectorAddress; + + if((Entry->FileType > 0) && (Entry->FileType < 0x80)) + { + memcpy(FileName, Entry->FileName, 17); + if(strchr(FileName, 0x0D) != NULL) *strchr(FileName, 0x0D) = '\0'; + else FileName[17] = '\0'; + + printf(" %-17s %s %5u %c %04X %04X", FileName, FileTypes[Entry->FileType], Entry->FileSize, Entry->LockFlag ? 'X' : ' ', Entry->LoadAddress, Entry->ExecAddress); + S = Entry->SectorAddress % 16 + 1; + H = (Entry->SectorAddress / 16) % 2; + C = Entry->SectorAddress / 16 / 2; + printf(" %4u (%2u %u %2u)\n", Entry->SectorAddress, C, H, S); + } + } + } +// printf("\nLast used Sector: %u\n\n", LastUsedSector); +} + + + + +void CopyFDToMZF(char *FileName) +{ + char MZFFileName[13]; + char EntryFileName[18]; + unsigned char SectorCounter, Counter; + FILE *MZFFile; + struct MZ700DirectoryStruct *Entry; + struct CMTHeader MZFHeader; + unsigned char C, H, S; + unsigned short WriteSize; + unsigned int Result; + + + S = 0; + + Result = ReadSector(Drive, 0, 0, 1); // BootSector + Result = Result >> 8; + if(Result != 0) Error(Result); + SectorBuffer[33] = '\0'; // emergency stop + if(strstr(SectorBuffer, FileName) != NULL) + { + Entry = (struct MZ700DirectoryStruct *) SectorBuffer; + S = Entry->SectorAddress % 16 + 1; + H = (Entry->SectorAddress / 16) % 2; + C = Entry->SectorAddress / 16 / 2; + Entry->FileType = 1; + } + else + { + for(SectorCounter = 1; (SectorCounter < 17) && (S == 0); SectorCounter++) + { + ReadSector(Drive, 0, 1, SectorCounter); + + for(Counter = 0; (Counter < 256 / 32) && (S == 0); Counter++) + { + Entry = (struct MZ700DirectoryStruct *) &SectorBuffer[Counter * sizeof(struct MZ700DirectoryStruct)]; + if((Entry->FileType > 0) && (Entry->FileType < 0x80)) + { + memcpy(EntryFileName, Entry->FileName, 17); + if(strchr(EntryFileName, 0x0D) != NULL) *strchr(EntryFileName, 0x0D) = '\0'; + else EntryFileName[17] = '\0'; + if(strcmp(FileName, EntryFileName) == 0) + { + S = Entry->SectorAddress % 16 + 1; + H = (Entry->SectorAddress / 16) % 2; + C = Entry->SectorAddress / 16 / 2; + } + } + } + } + } + + if(S != 0) + { + memset(MZFFileName, 0, sizeof(MZFFileName)); + strncpy(MZFFileName, FileName, 8); + strcat(MZFFileName, ".MZF"); + printf("%s found save it as %s\n", FileName, MZFFileName); + MZFFile = fopen(MZFFileName , "wb"); + memset(&MZFHeader, 0, sizeof(MZFHeader)); + if(Entry->FileType == 0x02) MZFHeader.Attribute = 0x05; + else MZFHeader.Attribute = Entry->FileType; + memcpy(MZFHeader.Name, Entry->FileName, 17); + MZFHeader.Size = Entry->FileSize; + strcat(MZFHeader.Comment, "by MZFDTool"); + + if((Entry->FileType == 0x01) && (Entry->LoadAddress == 0x0000)) + { + MZFHeader.Comment[24] = 0x21; + MZFHeader.Comment[25] = 0x30; + MZFHeader.Comment[26] = 0x11; + + MZFHeader.Comment[27] = 0x01; + MZFHeader.Comment[28] = 0x10; + MZFHeader.Comment[29] = 0x00; + + MZFHeader.Comment[30] = 0x11; + MZFHeader.Comment[31] = 0xF0; + MZFHeader.Comment[32] = 0xCF; + + MZFHeader.Comment[33] = 0xED; + MZFHeader.Comment[34] = 0xB0; + + MZFHeader.Comment[35] = 0xC3; + MZFHeader.Comment[36] = 0xF0; + MZFHeader.Comment[37] = 0xCF; + + + + MZFHeader.Comment[40] = 0x21; + MZFHeader.Comment[41] = 0x00; + MZFHeader.Comment[42] = 0x12; + + MZFHeader.Comment[43] = 0x01; + MZFHeader.Comment[44] = (unsigned char) Entry->FileSize & 0xFF; + MZFHeader.Comment[45] = Entry->FileSize >> 8; + + MZFHeader.Comment[46] = 0x11; + MZFHeader.Comment[47] = 0x00; + MZFHeader.Comment[48] = 0x00; + + MZFHeader.Comment[49] = 0xD3; + MZFHeader.Comment[50] = 0xE0; + + MZFHeader.Comment[51] = 0xED; + MZFHeader.Comment[52] = 0xB0; + + MZFHeader.Comment[53] = 0xC3; + MZFHeader.Comment[54] = 0x00; + MZFHeader.Comment[55] = 0x00; + + MZFHeader.LoadAddress = 0x1200; + MZFHeader.ExecAddress = 0x1120; + } + else + { + MZFHeader.LoadAddress = Entry->LoadAddress; + MZFHeader.ExecAddress = Entry->ExecAddress; + } + fwrite(&MZFHeader, sizeof(MZFHeader), 1, MZFFile); + + while(MZFHeader.Size != 0) + { + WriteSize = 256; + if(MZFHeader.Size < WriteSize) WriteSize = MZFHeader.Size; + MZFHeader.Size = MZFHeader.Size - WriteSize; + ReadSector(Drive, C, H, S); + fwrite(SectorBuffer, WriteSize, 1, MZFFile); + S++; + if(S == 17) + { + S = 1; + if(H == 0) H = 1; + else + { + C++; + H = 0; + } + } + } + + fclose(MZFFile); + } + else + { + puts(""); + printf("%s not found!\n", FileName); + puts(""); + puts("Remember that capital letters are differenced!"); + } +} + + + + +void CopyMZFToFD(char *FileName) +{ + printf("Not implemented yet (%s)\n", FileName); +} + + + + +void Copy(char *FileName) +{ + if((strstr(FileName, ".MZF") == NULL) && (strstr(FileName, ".mzf") == NULL)) CopyFDToMZF(FileName); + else CopyMZFToFD(FileName); +} + + + + +void ShowMap(void) +{ + unsigned short UsedSectors; + unsigned short Counter; + unsigned char BitCounter; +// unsigned char BitMask; + unsigned short BitMask; + unsigned short *SectorsPerTrack; + unsigned char StartTrack; + unsigned int Result; + + + puts(""); + + Result = ReadSector(Drive, 0, 0, 16); + Result = Result >> 8; + if(Result != 0) Error(Result); + UsedSectors = SectorBuffer[3] * 256 + SectorBuffer[4]; + StartTrack = SectorBuffer[5]; + + printf("Volume: %c%c%c Used Sectors: %4u\n\n", SectorBuffer[0], SectorBuffer[1], SectorBuffer[2], UsedSectors); + puts(""); + + puts(" 1111111"); + puts("Track ( C H S) 1234567890123456"); + puts(""); + + for(Counter = StartTrack; Counter <= 80; Counter++) + { + printf(" %3d (%2d %s 1) ", Counter, (Counter - 1) / 2, (Counter - 1) % 2 ? "1" : "0"); + BitMask = 0x0001; + SectorsPerTrack = (unsigned short *) &SectorBuffer[6 + 2 * (Counter - StartTrack)]; + + for(BitCounter = 0; BitCounter < 16; BitCounter++) + { + if(*SectorsPerTrack & BitMask) printf("X"); + else printf("-"); + BitMask = BitMask << 1; + } + puts(""); + } + + puts(""); +} + + + + +void Init(void) +{ + memset(SectorBuffer, 0, sizeof(SectorBuffer)); + Drive = 0; + Cylinder = 0; + Head = 0; + Sector = 1; +} + + + + + + + + +void Usage(void) +{ + puts("usage: mzfdtool dir [DRIVE:]"); + puts(" map [DRIVE:]"); + puts(" copy [DRIVE:] FILENAME"); + puts(" DRIVE: C H S"); + puts(""); + exit(1); +} + + + + +int main(int argc, char *argv[]) +{ + unsigned int Result; + + + printf("\nMZFDTool V %s (c) 2002 by BKK\n\n", VERSION); + + if(argc == 1) Usage(); + + Init(); + + SetInt1E(0); + + ResetFloppy(Drive); + + if(strcmp(argv[1], "dir") == 0) + { + if(argc == 3) + if(toupper(argv[2][0]) == 'B') Drive = 1; + ShowDirectory(); + } + else + { + if(strcmp(argv[1], "copy") == 0) + { + if(argc == 4) + { + if(toupper(argv[2][0]) == 'B') Drive = 1; + Copy(argv[3]); + } + else Copy(argv[2]); + } + else + { + if(strcmp(argv[1], "map") == 0) + { + if(argc == 3) + if(toupper(argv[2][0]) == 'B') Drive = 1; + ShowMap(); + } + else + { + if(argc < 3) exit(1); + + if(toupper(argv[1][0]) == 'B') Drive = 1; + Cylinder = atoi(argv[2]); + Head = atoi(argv[3]); + Sector = atoi(argv[4]); + + printf("Drive: %c CHS: %u %u %u\n", Drive ? 'B' : 'A', Cylinder, Head, Sector); + +// Set_Floppy_Medis_Type(); + + Result = ReadSector(Drive, Cylinder, Head, Sector); + Result = Result >> 8; + + if(Result == 0) ShowSectorBuffer(); + else Error(Result); + } + } + } + + ResetInt1E(); + ResetFloppy(Drive); + + return(0); +} \ No newline at end of file diff --git a/tools/MZFD/Makefile b/tools/MZFD/Makefile new file mode 100644 index 0000000..82f4dd5 --- /dev/null +++ b/tools/MZFD/Makefile @@ -0,0 +1,19 @@ +# MZFDTool - Sharp MZ-700 Floppy Disk image tool +# (c) 2002 BKK, 2026 Philip Smart +# +# Usage: +# make Build MZFDTool +# make clean Remove build artifacts + +CC = gcc +CFLAGS = -Wall -Wno-unused-result -O2 +TARGET = MZFDTool +SRC = MZFDTool.c + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $< + +clean: + rm -f $(TARGET) + +.PHONY: clean diff --git a/tools/MZFDTool b/tools/MZFDTool new file mode 100755 index 0000000000000000000000000000000000000000..8c9b9970d7fcc764451062df17981c0d4ab25c56 GIT binary patch literal 21752 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 ${SFD700_ROM}