From 7d7ab196c33de72000b1f3926e85d8eb8a00994c Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sat, 10 Oct 2026 12:34:48 +0200 Subject: [PATCH] basic loading and saving --- apps/basic/basic.asm | 496 +++++++++++++++++++++++++++++-------------- scripts/encode.py | 13 +- 2 files changed, 340 insertions(+), 169 deletions(-) diff --git a/apps/basic/basic.asm b/apps/basic/basic.asm index ddf7a5c..6649bd1 100644 --- a/apps/basic/basic.asm +++ b/apps/basic/basic.asm @@ -4616,6 +4616,14 @@ FULLSETTOP: FULLMSG: .BYTE "[FULL MEMORY]",CR,LF,0,0 +; Every tape block starts with a type byte followed by 128 data bytes. +; Header data contains a little-endian block count, byte length and filename. +TAPE_TYPE_RAW .equ 0 +TAPE_TYPE_BINARY .equ 1 +TAPE_TYPE_BAS_PROG .equ 2 +TAPE_TYPE_BAS_ARR .equ 3 +TAPE_DATA_SIZE .equ 128 + CLOAD: ld a,(hl) ; get byte after "CLOAD" cp ZTIMES ; "*" token? ("CLOAD*") jp z,FCERR ; yes - array load (ARRLD1) @@ -4624,8 +4632,54 @@ CLOAD: ld a,(hl) ; get byte after "CLOAD" cp '#' ; 'cload#'? jp z,CLOADB ; yes - load binary ; just cload - load basic - jp FCERR - + or a ; no arguments allowed + jp nz,FCERR + ld hl,CLSTARTMSG ; ask the user to start the tape + call PRS ; print the prompt + ld b,TAPE_TYPE_BAS_PROG ; expect a BASIC program header + call CLREADHEADER ; bc = blocks, de = length, hl = filename + call CLPRINTHEADER ; print block count and filename + ld b,d ; move exact length to bc + ld c,e ; bc = exact program length + ld a,b ; check the high byte of the length + or a ; 256 bytes or more is always valid here + jr nz,1$ ; skip the minimum-length check + ld a,c ; check a short program's low byte + cp 2 ; an empty program still has its end marker + jp c,FCERR ; less than two bytes is not a program +1$: ld hl,(BASTXT) ; get the program load address + add hl,bc ; calculate the exact program end + jp c,OMERR ; reject a wrapped end address + ld de,40 ; keep BASIC's minimum working space free + add hl,de ; include the working-space margin + jp c,OMERR ; reject a wrapped limit address + ld de,(STRSPC) ; get the bottom of string/stack space + call CPDEHL ; compare required memory with available RAM + jp nc,OMERR ; program would enter BASIC's working area + ld hl,(BASTXT) ; calculate the exact end once more + add hl,bc ; hl = address following the program + push hl ; preserve exact program end + ld de,(BASTXT) ; load the program at the BASIC text area + call CLLOADBLOCKS ; copy only the exact number of bytes + pop hl ; restore the exact program end + dec hl ; point to the last program byte + ld a,(hl) ; get the last end-marker byte + or a ; it must be zero + jp nz,FCERR ; reject an invalid program terminator + dec hl ; point to the first end-marker byte + ld a,(hl) ; get the first end-marker byte + or a ; it must also be zero + jp nz,FCERR ; reject an invalid program terminator + push hl + ld hl,CLCLRF ; print newline + call PRS + ld hl,OKMSG + call PRS ; print ok message + pop hl + inc hl ; restore the address after the program + inc hl + ld (PROGND),hl ; install the new program end + jp SETPTR ; clear variables, relink and return to prompt CLOADB: inc hl ; skip # call GETNUM ; evaluate numeric expression call DEINT ; convert FPREG to de @@ -4634,71 +4688,124 @@ CLOADB: inc hl ; skip # push hl push bc push de - ld hl,CLSTARTMSG call PRS + ld b,TAPE_TYPE_BINARY + call CLREADHEADER ; bc = blocks, de = length, hl = filename + call CLPRINTHEADER + ld b,d + ld c,e ; bc = exact file length + pop de ; destination address + call CLLOADBLOCKS + pop bc + pop hl + ret - ; Load block +; ------------------------------------------------------- +; Read and decode a tape header +; needs b (type of header to expect) +; returns bc (number of following blocks), de (byte length), hl (filename) +; errors do not return +; uses a +; ------------------------------------------------------- +CLREADHEADER: + push bc call CLREADBLOCK - ; hl contains block, check block type ld a,(hl) - cp 1 ; first block needs to be a header - jp nz,CLNOHDR ; First block is not a header - ; first block is a header, get nr of blocks + pop bc + cp b + jp nz,CLNOHDR inc hl - ld de,(hl) - ; print number of blocks to load + ld c,(hl) ; block count is little-endian + inc hl + ld b,(hl) + inc hl + ld e,(hl) ; byte length is little-endian + inc hl + ld d,(hl) + inc hl ; point to filename + ret + +; ------------------------------------------------------- +; Print the block count and filename from a tape header +; needs bc (number of blocks), hl (filename) +; preserves bc,de +; uses a,h,l +; ------------------------------------------------------- +CLPRINTHEADER: + push bc push de push hl - ex de,hl + ld h,b + ld l,c call PRNTHL ld hl,CLBLOCKSMSG call PRS pop hl - inc hl - inc hl ; get filename - ; TODO filename always null terminated? - call PRS ; print filename - ; de contains the nr of blocks to load - ; TODO check de>0 - pop de -1$: push de ; save repeat counter - call CLREADBLOCK ; hl = new data - ; TODO check block type - inc hl ; goto start of data - pop de ; get repeat counter - push hl ; save stored data - exx - pop hl ; get stored data back - pop de ; get destination - ld bc,128 ; block size - ldir ; copy data to memory - push de ; store destination address - exx - dec de - ld a,e - cp 0 - jr nz,1$ ; repeat if not done yet - ld a,d - cp 0 - jr nz,1$ ; repeat if not done yet + call PRS pop de - pop bc - pop hl + pop bc ret +; ------------------------------------------------------- +; Load tape data into memory, including a partial final block +; needs bc (file length in bytes), de (destination address) +; returns de (first address after loaded data) +; returns bc (zero) +; uses a,h,l +; ------------------------------------------------------- +CLLOADBLOCKS: + ld a,b + or c + ret z +1$: push bc + push de + call CLREADBLOCK + inc hl ; skip block type + pop de + pop bc ; remaining byte count + ld a,b + or a + jr nz,2$ ; at least 256 bytes remain + ld a,c + cp TAPE_DATA_SIZE + jr nc,2$ + ldir ; copy the partial final block + ret +2$: push bc + ld bc,TAPE_DATA_SIZE + ldir + pop bc + ld a,c ; subtract one full block + sub TAPE_DATA_SIZE + ld c,a + ld a,b + sbc a,0 + ld b,a + jr 1$ + +; ------------------------------------------------------- +; Read one of two redundant copies of a tape block +; returns hl (block buffer) +; errors do not return +; uses a,b,c,d,e,h,l +; ------------------------------------------------------- CLREADBLOCK: call BIOS_CAS_READ_BLOCK jr c,3$ + push hl ; preserve the first copy ld a,'.' ; block read correctly call OUTC - jr 1$ + ld a,'o' ; skip redundant copy + call OUTC + call BIOS_CAS_READ_BLOCK_DRY + jr 2$ 3$: ld a,'x' ; block failed, try redundant block call OUTC call BIOS_CAS_WAIT_SILENCE call BIOS_CAS_READ_BLOCK - push hl jr c,4$ + push hl ; preserve the redundant copy ld a,'.' ; redundant block read correctly call OUTC jr 2$ @@ -4710,12 +4817,7 @@ CLREADBLOCK: call BIOS_CAS_WAIT_SILENCE pop bc pop af - jr CLERR -1$: push hl ; save hl - ld a,'o' ; skip redundant block - call OUTC - call BIOS_CAS_READ_BLOCK_DRY - ; Ignore errors on this block + jp CLERR 2$: ; but make sure it is silent again call BIOS_CAS_WAIT_SILENCE pop hl @@ -4731,30 +4833,24 @@ CLERR: cp 1 cp 4 jr z,CLECHS jp FCERR ; Unknown error -CLECAL: ld h,0 - ld l,b ; get specific error code - call PRNTHL ; print specific error code - ld hl, CLECALMSG +CLECAL: ld hl,CLECALMSG + jr CLDETAIL +CLESYN: ld hl,CLESYNMSG + jr CLDETAIL +CLERED: ld hl,CLEREDMSG +CLDETAIL: + push hl + ld h,0 + ld l,b ; print specific error code + call PRNTHL + pop hl jr CLEPRINT -CLESYN: ld h,0 - ld l,b ; get specific error code - call PRNTHL ; print specific error code - ld hl, CLESYNMSG - jr CLEPRINT -CLERED: ld h,0 - ld l,b ; get specific error code - call PRNTHL ; print specific error code - ld hl, CLEREDMSG - jr CLEPRINT -CLECHS: ld hl, CLECHSMSG +CLECHS: ld hl,CLECHSMSG jr CLEPRINT CLNOHDR: ld hl,CLNOHDRMSG CLEPRINT: call PRS - pop de - pop bc - pop hl jp FCERR CLSTARTMSG: .db "Play tape...",0ah,0dh,00h @@ -4768,123 +4864,193 @@ CLCLRF: .db 0ah,0dh,00h CSAVE: ld a,(hl) ; get byte after "CSAVE" cp ZTIMES ; "*" token? ("CSAVE*") - jp z,FCERR ; yes - array load (ARRLD1) + jp z,FCERR ; yes - array save cp ZPRINT ; "?" ("PRINT token") verify jp z,FCERR ; yes - flag "verify" cp '#' ; 'csave#'? jp z,CSAVEB ; yes - save binary - ; just cload - load basic - jp FCERR - -CSAVEB: inc hl ; skip # - push bc ; save bc - push de ; save de - CALL GETNUM ; Get start address - CALL DEINT ; Convert it to 16 bits - PUSH DE ; Save start address - CALL CHKSYN ; Make sure "," follows - .BYTE ',' - CALL GETNUM ; Get length - CALL DEINT ; Convert it to 16 bits - PUSH DE ; Save length - CALL CHKSYN ; Make sure "," follows - .BYTE ',' - ; hl points to the first character of the string expression - call EVAL - push hl ; save command buffer pos + ; just csave - save basic program +CSAVE1: call EVAL ; evaluate filename expression + push bc + push de ; preserve bc and de + push hl ; preserve command buffer position exx call GETSTR - call LOADFP ; bc' = addr, e' = length + call LOADFP ; bc' = filename, e'=length exx - pop hl ; get back command buffer pos - pop bc ; get length - pop de ; get start address - push hl + ; get the program length + ld de,(BASTXT) ; program source address + ld hl,(PROGND) ; address after prog mem + or a ; clear carry + sbc hl,de ; hl = prog length + ld b,h + ld c,l ; prog length in bc + ld a,TAPE_TYPE_BAS_PROG + jp CSOUT ; Write the file to tape +CSAVEB: inc hl ; skip # + push bc ; save BASIC registers push de - call BIOS_CAS_GET_BUFFER - ; hl=buffer, bc=lenght, de=start address - ; Write header block - ld a,1 ; block type header - ld (hl),a - inc hl + call GETNUM ; get start address + call DEINT ; convert FPREG to de + push de ; save start address + call CHKSYN ; require a comma + .BYTE ',' + call GETNUM ; get length + call DEINT ; convert FPREG to de + push de ; save length + call CHKSYN ; require a comma + .BYTE ',' + call EVAL ; evaluate filename expression + push hl ; preserve command buffer position + exx + call GETSTR + call LOADFP ; bc' = filename, e' = length + exx + pop hl + pop bc ; length in bytes + pop de ; start address + push hl + ld a,TAPE_TYPE_BINARY +CSOUT: call CSWRITEHEADER ; bc = block count, preserves de + call CSWRITEBLOCKS + ld hl,CLCLRF + call PRS + pop hl + pop de + pop bc + ret + +; ------------------------------------------------------- +; Build and write a header and its redundant copy +; needs bc (file length in bytes) +; needs bc' (filename address), e' (filename length) +; returns bc (number of data blocks) +; preserves de +; uses a,h,l and the alternate register set +; ------------------------------------------------------- +CSWRITEHEADER: + push de + push af ; preserve header type + push bc ; preserve exact byte length call BYTESTOBLOCKS - push bc - ld a,c ; nr of blocks low byte - ld (hl),a - inc hl - ld a,b ; nr of blocks high byte - ld (hl),a - inc hl - push hl - exx -1$: push bc - pop hl ; get str addr to hl - ld a,(hl) - inc hl - push hl - pop bc ; get str addr+1 back into bc - pop hl ; get hl to hl' - ld (hl),a ; save character to buffer - inc hl - push hl - dec e ; count down str len - ld a,e - cp 0 - jr nz,1$ ; repeat for the whole string - exx - pop hl ; get back buffer addr - ld a,0 ; NULL terminate string - ld (hl),a - inc hl - ; Header block complete (rest of block is accepted garbage) - call BIOS_CAS_WRITE_BLOCK - ld a,'.' - call OUTC - call BIOS_CAS_WRITE_BLOCK - ld a,'.' - call OUTC - ld a,250 - call BIOS_DELAY_MS - ld a,250 - call BIOS_DELAY_MS - ld a,250 - call BIOS_DELAY_MS ; add a 750ms delay - pop bc ; get back block count - pop de ; get back start address + pop hl ; exact byte length for the header + pop af + call CSMAKEHEADER + push bc ; preserve block count over delay + call CSWRITEPAIR + call CSHEADERGAP + pop bc + pop de + ret + +; ------------------------------------------------------- +; Write consecutive raw data blocks +; needs bc (number of blocks), de (source address) +; returns bc (zero), de (first address after written data) +; uses a,h,l +; ------------------------------------------------------- +CSWRITEBLOCKS: ld a,b or c - jr z,3$ ; no data blocks for a zero length -2$: - ; load data into buffer - push bc - call BIOS_CAS_GET_BUFFER - ld bc,128 ; block size - ld a,0 ; raw type - ld (hl),a - inc hl - ex de,hl ; hl=src,de=dest - ldir ; copy the bytes - ex de,hl ; de back to being the src addr - call BIOS_CAS_WRITE_BLOCK - ld a,'.' - call OUTC - call BIOS_CAS_WRITE_BLOCK - ld a,'.' - call OUTC + ret z +1$: push bc + ld a,TAPE_TYPE_RAW + call CSWRITEDATA pop bc dec bc ld a,b or c - jr nz,2$ ; repeat if not done -3$: - ; All blocks are written + jr nz,1$ + ret - ld hl,CLCLRF - call PRS +; ------------------------------------------------------- +; Build a header in the tape block buffer +; needs bc (number of data blocks) +; needs hl (file length in bytes) +; needs bc' (filename address), e' (filename length) +; needs a (type of header) +; preserves bc,de +; uses a,h,l and the alternate register set +; ------------------------------------------------------- +CSMAKEHEADER: + push de ; preserve data source address + push hl ; preserve byte length + push af + call BIOS_CAS_GET_BUFFER + pop af + ld (hl),a ; store header type + inc hl + ld (hl),c ; block count is little-endian + inc hl + ld (hl),b + inc hl + pop de ; byte length + ld (hl),e ; byte length is little-endian + inc hl + ld (hl),d + inc hl ; filename destination - pop hl - pop de + push bc ; preserve block count + push hl + exx + ld h,b ; hl = filename source + ld l,c + ld c,e ; bc = filename length + ld b,0 + pop de ; de = filename destination + ld a,b + or c + jr z,1$ + ldir +1$: xor a + ld (de),a ; terminate filename + exx pop bc + pop de ; restore data source address + ret + +; ------------------------------------------------------- +; Copy and write one 128-byte data block +; needs a (block type), de (source address) +; returns de (source address advanced by 128 bytes) +; uses a,b,c,h,l +; ------------------------------------------------------- +CSWRITEDATA: + call BIOS_CAS_GET_BUFFER + ld (hl),a + inc hl + ld bc,TAPE_DATA_SIZE + ex de,hl ; hl = source, de = buffer + ldir + ex de,hl ; de = next source address + call CSWRITEPAIR + ret + +; ------------------------------------------------------- +; Write the current tape buffer twice and show progress +; preserves bc,de,hl +; uses a +; ------------------------------------------------------- +CSWRITEPAIR: + call BIOS_CAS_WRITE_BLOCK + ld a,'.' + call OUTC + call BIOS_CAS_WRITE_BLOCK + ld a,'.' + call OUTC + ret + +; ------------------------------------------------------- +; Leave 750 ms between a header pair and its data +; uses a,b,c +; ------------------------------------------------------- +CSHEADERGAP: + ld a,250 + call BIOS_DELAY_MS + ld a,250 + call BIOS_DELAY_MS + ld a,250 + call BIOS_DELAY_MS ret ; ------------------------------------------------------- diff --git a/scripts/encode.py b/scripts/encode.py index 5a59e15..be1cbe7 100644 --- a/scripts/encode.py +++ b/scripts/encode.py @@ -10,7 +10,9 @@ from pathlib import Path BLOCK_SIZE = 128 TYPE_RAW = 0 -TYPE_HEADER = 1 +TYPE_BINARY = 1 +TYPE_BAS_PROG = 2 +TYPE_BAS_ARR = 3 def crc16(data: bytes) -> int: """Calculate CRC-16/CCITT-FALSE.""" @@ -107,9 +109,12 @@ class PulseWriter: def header(pulses: PulseWriter, path:Path) -> None: fname = path.name fsize = os.path.getsize(path) - data = struct.pack(" None: pulses.block(TYPE_RAW, data)