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
+331 -165
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
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 call CLREADBLOCK
; hl contains block, check block type
ld a,(hl) ld a,(hl)
cp 1 ; first block needs to be a header pop bc
jp nz,CLNOHDR ; First block is not a header cp b
; first block is a header, get nr of blocks jp nz,CLNOHDR
inc hl inc hl
ld de,(hl) ld c,(hl) ; block count is little-endian
; print number of blocks to load 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 de
push hl push hl
ex de,hl ld h,b
ld l,c
call PRNTHL call PRNTHL
ld hl,CLBLOCKSMSG ld hl,CLBLOCKSMSG
call PRS call PRS
pop hl pop hl
inc hl call PRS
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 de
pop bc pop bc
pop hl
ret 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,30 +4833,24 @@ 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
CLERED: ld hl,CLEREDMSG
CLDETAIL:
push hl
ld h,0
ld l,b ; print specific error code
call PRNTHL
pop hl
jr CLEPRINT jr CLEPRINT
CLESYN: ld h,0 CLECHS: ld hl,CLECHSMSG
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
jr CLEPRINT jr CLEPRINT
CLNOHDR: 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)