basic loading and saving

This commit is contained in:
Joppe Blondel
2026-10-10 12:34:48 +02:00
parent 6374a070ea
commit 7d7ab196c3
2 changed files with 340 additions and 169 deletions
+339 -173
View File
@@ -4616,6 +4616,14 @@ FULLSETTOP:
FULLMSG: FULLMSG:
.BYTE "[FULL MEMORY]",CR,LF,0,0 .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" CLOAD: ld a,(hl) ; get byte after "CLOAD"
cp ZTIMES ; "*" token? ("CLOAD*") cp ZTIMES ; "*" token? ("CLOAD*")
jp z,FCERR ; yes - array load (ARRLD1) jp z,FCERR ; yes - array load (ARRLD1)
@@ -4624,8 +4632,54 @@ CLOAD: ld a,(hl) ; get byte after "CLOAD"
cp '#' ; 'cload#'? cp '#' ; 'cload#'?
jp z,CLOADB ; yes - load binary jp z,CLOADB ; yes - load binary
; just cload - load basic ; 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 # CLOADB: inc hl ; skip #
call GETNUM ; evaluate numeric expression call GETNUM ; evaluate numeric expression
call DEINT ; convert FPREG to de call DEINT ; convert FPREG to de
@@ -4634,71 +4688,124 @@ CLOADB: inc hl ; skip #
push hl push hl
push bc push bc
push de push de
ld hl,CLSTARTMSG ld hl,CLSTARTMSG
call PRS call PRS
ld b,TAPE_TYPE_BINARY
; Load block call CLREADHEADER ; bc = blocks, de = length, hl = filename
call CLREADBLOCK call CLPRINTHEADER
; hl contains block, check block type ld b,d
ld a,(hl) ld c,e ; bc = exact file length
cp 1 ; first block needs to be a header pop de ; destination address
jp nz,CLNOHDR ; First block is not a header call CLLOADBLOCKS
; first block is a header, get nr of blocks
inc hl
ld de,(hl)
; print number of blocks to load
push de
push hl
ex de,hl
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
pop de
pop bc pop bc
pop hl pop hl
ret ret
; -------------------------------------------------------
; 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
ld a,(hl)
pop bc
cp b
jp nz,CLNOHDR
inc hl
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
ld h,b
ld l,c
call PRNTHL
ld hl,CLBLOCKSMSG
call PRS
pop hl
call PRS
pop de
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: CLREADBLOCK:
call BIOS_CAS_READ_BLOCK call BIOS_CAS_READ_BLOCK
jr c,3$ jr c,3$
push hl ; preserve the first copy
ld a,'.' ; block read correctly ld a,'.' ; block read correctly
call OUTC 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 3$: ld a,'x' ; block failed, try redundant block
call OUTC call OUTC
call BIOS_CAS_WAIT_SILENCE call BIOS_CAS_WAIT_SILENCE
call BIOS_CAS_READ_BLOCK call BIOS_CAS_READ_BLOCK
push hl
jr c,4$ jr c,4$
push hl ; preserve the redundant copy
ld a,'.' ; redundant block read correctly ld a,'.' ; redundant block read correctly
call OUTC call OUTC
jr 2$ jr 2$
@@ -4710,12 +4817,7 @@ CLREADBLOCK:
call BIOS_CAS_WAIT_SILENCE call BIOS_CAS_WAIT_SILENCE
pop bc pop bc
pop af pop af
jr CLERR jp 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
2$: ; but make sure it is silent again 2$: ; but make sure it is silent again
call BIOS_CAS_WAIT_SILENCE call BIOS_CAS_WAIT_SILENCE
pop hl pop hl
@@ -4731,20 +4833,17 @@ CLERR: cp 1
cp 4 cp 4
jr z,CLECHS jr z,CLECHS
jp FCERR ; Unknown error jp FCERR ; Unknown error
CLECAL: ld h,0 CLECAL: ld hl,CLECALMSG
ld l,b ; get specific error code jr CLDETAIL
call PRNTHL ; print specific error code CLESYN: ld hl,CLESYNMSG
ld hl, CLECALMSG jr CLDETAIL
jr CLEPRINT CLERED: ld hl,CLEREDMSG
CLESYN: ld h,0 CLDETAIL:
ld l,b ; get specific error code push hl
call PRNTHL ; print specific error code ld h,0
ld hl, CLESYNMSG ld l,b ; print specific error code
jr CLEPRINT call PRNTHL
CLERED: ld h,0 pop hl
ld l,b ; get specific error code
call PRNTHL ; print specific error code
ld hl, CLEREDMSG
jr CLEPRINT jr CLEPRINT
CLECHS: ld hl,CLECHSMSG CLECHS: ld hl,CLECHSMSG
jr CLEPRINT jr CLEPRINT
@@ -4752,9 +4851,6 @@ CLNOHDR:
ld hl,CLNOHDRMSG ld hl,CLNOHDRMSG
CLEPRINT: CLEPRINT:
call PRS call PRS
pop de
pop bc
pop hl
jp FCERR jp FCERR
CLSTARTMSG: .db "Play tape...",0ah,0dh,00h CLSTARTMSG: .db "Play tape...",0ah,0dh,00h
@@ -4768,123 +4864,193 @@ CLCLRF: .db 0ah,0dh,00h
CSAVE: ld a,(hl) ; get byte after "CSAVE" CSAVE: ld a,(hl) ; get byte after "CSAVE"
cp ZTIMES ; "*" token? ("CSAVE*") cp ZTIMES ; "*" token? ("CSAVE*")
jp z,FCERR ; yes - array load (ARRLD1) jp z,FCERR ; yes - array save
cp ZPRINT ; "?" ("PRINT token") verify cp ZPRINT ; "?" ("PRINT token") verify
jp z,FCERR ; yes - flag "verify" jp z,FCERR ; yes - flag "verify"
cp '#' ; 'csave#'? cp '#' ; 'csave#'?
jp z,CSAVEB ; yes - save binary jp z,CSAVEB ; yes - save binary
; just cload - load basic ; just csave - save basic program
jp FCERR CSAVE1: call EVAL ; evaluate filename expression
push bc
CSAVEB: inc hl ; skip # push de ; preserve bc and de
push bc ; save bc push hl ; preserve command buffer position
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
exx exx
call GETSTR call GETSTR
call LOADFP ; bc' = addr, e' = length call LOADFP ; bc' = filename, e'=length
exx exx
pop hl ; get back command buffer pos ; get the program length
pop bc ; get length ld de,(BASTXT) ; program source address
pop de ; get start address ld hl,(PROGND) ; address after prog mem
push hl 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 push de
call BIOS_CAS_GET_BUFFER call GETNUM ; get start address
; hl=buffer, bc=lenght, de=start address call DEINT ; convert FPREG to de
; Write header block push de ; save start address
ld a,1 ; block type header call CHKSYN ; require a comma
ld (hl),a .BYTE ','
inc hl 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 call BYTESTOBLOCKS
push bc pop hl ; exact byte length for the header
ld a,c ; nr of blocks low byte pop af
ld (hl),a call CSMAKEHEADER
inc hl push bc ; preserve block count over delay
ld a,b ; nr of blocks high byte call CSWRITEPAIR
ld (hl),a call CSHEADERGAP
inc hl pop bc
push hl pop de
exx ret
1$: push bc
pop hl ; get str addr to hl ; -------------------------------------------------------
ld a,(hl) ; Write consecutive raw data blocks
inc hl ; needs bc (number of blocks), de (source address)
push hl ; returns bc (zero), de (first address after written data)
pop bc ; get str addr+1 back into bc ; uses a,h,l
pop hl ; get hl to hl' ; -------------------------------------------------------
ld (hl),a ; save character to buffer CSWRITEBLOCKS:
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
ld a,b ld a,b
or c or c
jr z,3$ ; no data blocks for a zero length ret z
2$: 1$: push bc
; load data into buffer ld a,TAPE_TYPE_RAW
push bc call CSWRITEDATA
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
pop bc pop bc
dec bc dec bc
ld a,b ld a,b
or c or c
jr nz,2$ ; repeat if not done jr nz,1$
3$: ret
; All blocks are written
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 push bc ; preserve block count
pop de 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 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 ret
; ------------------------------------------------------- ; -------------------------------------------------------
+9 -4
View File
@@ -10,7 +10,9 @@ from pathlib import Path
BLOCK_SIZE = 128 BLOCK_SIZE = 128
TYPE_RAW = 0 TYPE_RAW = 0
TYPE_HEADER = 1 TYPE_BINARY = 1
TYPE_BAS_PROG = 2
TYPE_BAS_ARR = 3
def crc16(data: bytes) -> int: def crc16(data: bytes) -> int:
"""Calculate CRC-16/CCITT-FALSE.""" """Calculate CRC-16/CCITT-FALSE."""
@@ -107,9 +109,12 @@ class PulseWriter:
def header(pulses: PulseWriter, path:Path) -> None: def header(pulses: PulseWriter, path:Path) -> None:
fname = path.name fname = path.name
fsize = os.path.getsize(path) fsize = os.path.getsize(path)
data = struct.pack("<H", math.ceil(fsize/BLOCK_SIZE)) + fname.encode()[:32].ljust(32, b' ') data = (
pulses.block(TYPE_HEADER, data) struct.pack("<HH", math.ceil(fsize/BLOCK_SIZE), fsize)
pulses.block(TYPE_HEADER, data) + fname.encode()[:32].ljust(32, b' ')
)
pulses.block(TYPE_BINARY, data)
pulses.block(TYPE_BINARY, data)
def raw(pulses: PulseWriter, data:bytes) -> None: def raw(pulses: PulseWriter, data:bytes) -> None:
pulses.block(TYPE_RAW, data) pulses.block(TYPE_RAW, data)