Block device peripheral and SBFS file system implemented.

This commit is contained in:
Anachronaut
2026-08-16 14:03:29 -04:00
parent 04dfcd707b
commit eff6902bcf
36 changed files with 3251 additions and 31 deletions
+871
View File
@@ -0,0 +1,871 @@
; sbfs.asm
; Reading the SplitBit Filesystem.
;
; The other implementation of this format is SplitDisk on the host. Nothing is shared
; between them but the written specification, so anything that changes here has to change
; there in the same breath.
;
; The disk's buffer is registered as bank 3, and every block read lands there and is then
; blitted where it is wanted. The CPU never touches the buffer directly, because nothing
; can: memory a device brings is reachable only through the controller.
;
; Written by Anachronaut
#Program
; Registers the disk's buffer, reads the superblock, and checks that the disk really is
; one of ours. Q is zero if the disk is ready to be read.
sbfsMount:
INIA 0d3
OUTA 0xE3
INIA 0x20
OUTA 0xE2
INIA 0x03
OUTA 0xE8 ; RegisterBank: bank 3 is the disk's buffer.
SETD.0 SbfsBlock
RSTA
STA.0
INCD.0
STA.0 ; Block 0, the superblock.
CALL sbfsReadBlock
BRQ sbfsMountRead
RET ; The read failed, and Q says so.
sbfsMountRead:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
; "SBFS", or this is not a disk of ours. A blank image is all zeroes, so without this
; an unformatted disk would look like a formatted one with no files on it.
SETD.0 SbfsBuffer
SETD.2 SbfsMagic
INIA 0d4
SETD.1 SbfsCount
STA.1
sbfsMagicLoop:
CALL sbfsSameByte
BRQ sbfsMagicSame
RET ; Q is the difference, which is not zero.
sbfsMagicSame:
INCD.0
INCD.2
LDA.1
DECA
STA.1
BNA sbfsMagicLoop
sbfsMagicDone:
; The version has to be one we understand.
SETD.0 SbfsBuffer
DPUP.0 0d04
LDA.0
INIB 0d1
XOR
BRQ sbfsGeometry
RET ; A version we do not know.
sbfsGeometry:
; Where the directory is and how big it is. The superblock carries both so that disks
; of different sizes can have different directories without a new format.
SETD.0 SbfsBuffer
DPUP.0 0d08
SETD.1 SbfsDirStart
CALL sbfsCopyWord
SETD.0 SbfsBuffer
DPUP.0 0d10
SETD.1 SbfsDirBlocks
CALL sbfsCopyWord
SETD.0 SbfsBuffer
DPUP.0 0d06
SETD.1 SbfsDiskBlocks
CALL sbfsCopyWord
RSTA
RSTB
CCF
ADD ; Q is zero: mounted.
RET
; Finds a file by name. DP0 points at a name, ending in a zero byte. Q is zero if it was
; found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it.
sbfsFind:
SETD.1 SbfsWanted
CALL sbfsKeepName
; Start at the first directory block and work through them all.
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
DPUP.0 0d01
LDA.0
SETD.1 SbfsLeft
STA.1 ; Only the low byte: a directory of 256 blocks is 2048 files.
sbfsFindBlock:
CALL sbfsReadBlock
BRQ sbfsFindLoaded
RET ; The read failed.
sbfsFindLoaded:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
; Eight entries to a block, thirty two bytes each.
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsFindEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsFindNext ; The in use bit is down, so this entry is free.
CALL sbfsMatchEntry
BRQ sbfsFindFound
sbfsFindNext:
DPUP.2 0d32
LDA.1
DECA
STA.1
BNA sbfsFindEntry
sbfsFindNextBlock:
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BRA sbfsFindMissing
BRI sbfsFindBlock
sbfsFindMissing:
; Nothing of that name. Q has to be something other than zero to say so.
RSTA
INIB 0d1
CCF
ADD
RET
sbfsFindFound:
; DP2 is on the entry. Keep it in DP3 while the pieces are taken out of it: DP3 is the
; pointer a CALL does not put back, so it is the only one that survives a subroutine.
; Copying it into DP0 has to be done here rather than in a routine of its own, for the
; same reason: a routine that set DP0 would have the assignment undone by its own RET.
PSHD.2
POPD.3
PSHD.3
POPD.0
DPUP.0 0d01
SETD.1 SbfsFileStart
CALL sbfsCopyWord
PSHD.3
POPD.0
DPUP.0 0d03
SETD.1 SbfsFileBlocks
CALL sbfsCopyWord
PSHD.3
POPD.0
DPUP.0 0d05
LDA.0
SETD.1 SbfsFileTail
STA.1
RSTA
RSTB
CCF
ADD ; Q is zero: found.
RET
; Compares the name in the entry at DP2 with the one kept in SbfsWanted. Q is zero if
; they are the same. Names are padded with zeroes rather than terminated, so a name that
; fills the field has no terminator to look for, which is why the count is what stops it.
sbfsMatchEntry:
PSHD.2
POPD.0
DPUP.0 0d06 ; The name inside the entry.
SETD.2 SbfsWanted
INIA 0d22
SETD.1 SbfsCount
STA.1
sbfsMatchLoop:
CALL sbfsSameByte
BRQ sbfsMatchSame
RET ; They differ, and Q is the difference.
sbfsMatchSame:
LDA.0
BRA sbfsMatchYes ; Both ended in the same place.
INCD.0
INCD.2
LDA.1
DECA
STA.1
BNA sbfsMatchLoop ; Twenty two the same is the same name, so falling out is a match.
sbfsMatchYes:
RSTA
RSTB
CCF
ADD
RET
; Copies the name at DP0 into DP1, twenty two bytes of it, padding with zeroes the way an
; entry is padded so that the two can be compared as they stand.
sbfsKeepName:
INIA 0d22
SETD.2 SbfsCount
STA.2
RSTB ; B stays zero once the name has ended.
sbfsKeepLoop:
LDA.0
BRA sbfsKeepPad
STA.1
INCD.0
BRI sbfsKeepStep
sbfsKeepPad:
STB.1
sbfsKeepStep:
INCD.1
LDA.2
DECA
STA.2
BNA sbfsKeepLoop
sbfsKeepDone:
RET
; Reads the file that sbfsFind found into Data Memory at DP1. Q is zero if it worked.
;
; Whole blocks are copied whole, including the last one, so the bytes after the end of a
; file are whatever else was in that block on the disk. SbfsFileTail says where the file
; actually stops, and it is the caller's business to respect it.
;
; A file of more than 255 blocks is not handled here. That is 64K, which is the whole of
; Data Memory, so it could not be read into it anyway.
sbfsRead:
PSHD.1
POPD.3 ; Where it goes. DP3 survives a CALL, and nothing called here
; touches it.
SETD.0 SbfsFileStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsFileBlocks
DPUP.0 0d01
LDA.0
SETD.1 SbfsLeft
STA.1
SETD.0 SbfsFileTail
LDA.0
BRA sbfsReadCounted ; No part block on the end.
SETD.0 SbfsLeft
LDA.0
INCA
STA.0 ; One more block for the tail.
sbfsReadCounted:
SETD.0 SbfsLeft
LDA.0
BRA sbfsReadDone ; An empty file has nothing to read.
sbfsReadLoop:
CALL sbfsReadBlock
BRQ sbfsReadGot
RET ; The read failed, and Q says so.
sbfsReadGot:
PSHD.3
POPD.1
CALL sbfsBufferOut
; On by a whole block. DPUP carries one byte at a time, so 256 takes two of them.
DPUP.3 0xFF
DPUP.3 0x01
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.0 SbfsLeft
LDA.0
DECA
STA.0
BNA sbfsReadLoop
sbfsReadDone:
RSTA
RSTB
CCF
ADD ; Q is zero: read.
RET
; ---- Writing ----
; Where the first block that can hold a file is: past the superblock and the directory.
sbfsFirstData:
SETD.0 SbfsCandidate
SETD.2 SbfsDirStart
CALL sbfsSetWord
SETD.0 SbfsCandidate
SETD.2 SbfsDirBlocks
CALL sbfsAddWord
RET
; SbfsEntryStart and SbfsEntryEnd describe the file in the entry at DP2. The end is one
; past the last block it holds, and a file of nothing holds none, so its end is its start.
sbfsEntryBounds:
PSHD.2
POPD.3
PSHD.3
POPD.0
DPUP.0 0d01
SETD.1 SbfsEntryStart
CALL sbfsCopyWord
SETD.0 SbfsEntryEnd
SETD.2 SbfsEntryStart
CALL sbfsSetWord
PSHD.3
POPD.0
DPUP.0 0d03
SETD.1 SbfsScratch
CALL sbfsCopyWord
SETD.0 SbfsEntryEnd
SETD.2 SbfsScratch
CALL sbfsAddWord
PSHD.3
POPD.0
DPUP.0 0d05
LDA.0
BRA sbfsBoundsDone ; No part block on the end.
SETD.0 SbfsEntryEnd
SETD.2 SbfsOne
CALL sbfsAddWord
sbfsBoundsDone:
RET
; Finds a run of SbfsWantBlocks free blocks and puts where it begins in SbfsFileStart.
; Q is zero if there was room.
;
; First fit, walking the directory, because with files laid down contiguously the
; directory already says which blocks are spoken for. There is no allocation table to
; consult and none to keep right.
sbfsAllocate:
CALL sbfsFirstData
sbfsAllocTry:
SETD.0 SbfsCandEnd
SETD.2 SbfsCandidate
CALL sbfsSetWord
SETD.0 SbfsCandEnd
SETD.2 SbfsWantBlocks
CALL sbfsAddWord
; Past the end of the disk means there is nowhere left to look.
SETD.0 SbfsDiskBlocks
SETD.2 SbfsCandEnd
CALL sbfsCompareWord
BRC sbfsAllocNoRoom ; The disk is smaller than where this run would end.
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
DPUP.0 0d01
LDA.0
SETD.1 SbfsLeft
STA.1
sbfsAllocBlock:
CALL sbfsReadBlock
BNQ sbfsAllocFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsAllocEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsAllocNext ; A free entry holds nothing, so it is in nobody's way.
CALL sbfsEntryBounds
; The comparisons need DP2 for their own purposes, so where this entry is goes on the
; Stack for the duration. Working it out again afterwards was the first way this was
; written, and it was both longer and wrong.
PSHD.2
; Two runs overlap when each begins before the other ends.
SETD.0 SbfsCandidate
SETD.2 SbfsEntryEnd
CALL sbfsCompareWord
BNC sbfsAllocClear
SETD.0 SbfsEntryStart
SETD.2 SbfsCandEnd
CALL sbfsCompareWord
BNC sbfsAllocClear
; They do overlap, so try again from the far end of whatever is in the way.
POPD.2
SETD.0 SbfsCandidate
SETD.2 SbfsEntryEnd
CALL sbfsSetWord
BRI sbfsAllocTry
sbfsAllocClear:
POPD.2
sbfsAllocNext:
DPUP.2 0d32
SETD.1 SbfsCount
LDA.1
DECA
STA.1
BNA sbfsAllocEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BNA sbfsAllocBlock
; Nothing was in the way, so this is where it goes.
SETD.0 SbfsFileStart
SETD.2 SbfsCandidate
CALL sbfsSetWord
RSTA
RSTB
CCF
ADD
RET
sbfsAllocNoRoom:
sbfsAllocFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; The two byte number at DP0 becomes the one at DP2. Written destination first, so that a
; call reads the way an assignment does. sbfsCopyWord goes the other way, source first,
; which is a difference worth keeping in mind: getting it backwards here quietly destroyed
; a value that had just been worked out.
sbfsSetWord:
LDA.2
STA.0
INCD.2
INCD.0
LDA.2
STA.0
RET
; The two byte number at DP0 becomes itself less the one at DP2.
sbfsSubWord:
DPUP.0 0d01
DPUP.2 0d01
LDA.0
LDB.2
CCF
SUB
MVQA
STA.0
DPDN.0 0d01
DPDN.2 0d01
LDA.0
LDB.2
SUB ; Borrows in from the low half.
MVQA
STA.0
RET
; How many blocks a file of SbfsFileBlocks and SbfsFileTail actually occupies, into
; SbfsWantBlocks. A file of nothing occupies none.
sbfsFileExtent:
SETD.0 SbfsWantBlocks
SETD.2 SbfsFileBlocks
CALL sbfsSetWord
SETD.0 SbfsFileTail
LDA.0
BRA sbfsExtentDone
SETD.0 SbfsWantBlocks
SETD.2 SbfsOne
CALL sbfsAddWord
sbfsExtentDone:
RET
; Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is,
; which has to be settled before it is made because nothing here can grow one afterwards.
; Q is zero if it was made, and then SbfsFileStart says where its blocks are.
sbfsCreate:
SETD.1 SbfsWanted
CALL sbfsKeepName
CALL sbfsFileExtent
CALL sbfsAllocate
BNQ sbfsCreateFailed
; Walk the directory for an entry nobody is using.
SETD.0 SbfsDirStart
SETD.1 SbfsBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
DPUP.0 0d01
LDA.0
SETD.1 SbfsLeft
STA.1
sbfsCreateBlock:
CALL sbfsReadBlock
BNQ sbfsCreateFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.2 SbfsBuffer
INIA 0d8
SETD.1 SbfsCount
STA.1
sbfsCreateEntry:
LDA.2
INIB 0x01
AND
BRQ sbfsCreateFill ; This one is free.
DPUP.2 0d32
SETD.1 SbfsCount
LDA.1
DECA
STA.1
BNA sbfsCreateEntry
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.1 SbfsLeft
LDA.1
DECA
STA.1
BNA sbfsCreateBlock
sbfsCreateFull:
RSTA
INIB 0d1
CCF
ADD ; Every entry is taken.
RET
sbfsCreateFill:
; DP2 is on the entry. Fill it in, then put the whole block back on the disk.
PSHD.2
POPD.3
INIA 0x01
STA.3 ; In use.
PSHD.3
POPD.1
DPUP.1 0d01
SETD.0 SbfsFileStart
CALL sbfsCopyWord
PSHD.3
POPD.1
DPUP.1 0d03
SETD.0 SbfsFileBlocks
CALL sbfsCopyWord
PSHD.3
POPD.1
DPUP.1 0d05
SETD.0 SbfsFileTail
LDA.0
STA.1
PSHD.3
POPD.1
DPUP.1 0d06
SETD.0 SbfsWanted
INIA 0d22
SETD.2 SbfsCount
STA.2
sbfsCreateName:
LDA.0
STA.1
INCD.0
INCD.1
LDA.2
DECA
STA.2
BNA sbfsCreateName
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
BNQ sbfsCreateFailed
; The free count is a note rather than the truth, but it should still be kept right.
RSTA
SETD.0 SbfsBlock
STA.0
INCD.0
STA.0
CALL sbfsReadBlock
BNQ sbfsCreateFailed
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.0 SbfsBuffer
DPUP.0 0d12
SETD.2 SbfsWantBlocks
CALL sbfsSubWord
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
RET
sbfsCreateFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; Writes a file that sbfsCreate made, from Data Memory at DP1. Q is zero if it worked.
;
; Whole blocks go out whole, so whatever follows the file in Data Memory ends up in the
; unused tail of its last block. That is harmless, since the tail in the entry says where
; the file stops, but it does mean a block can hold a little of whatever was next to it.
sbfsWriteFile:
PSHD.1
POPD.3
SETD.0 SbfsBlock
SETD.2 SbfsFileStart
CALL sbfsSetWord
CALL sbfsFileExtent
SETD.0 SbfsWantBlocks
DPUP.0 0d01
LDA.0
SETD.1 SbfsLeft
STA.1
BRA sbfsWriteDone ; A file of nothing has nothing to write.
sbfsWriteLoop:
PSHD.3
POPD.1
CALL sbfsBufferIn
CALL sbfsWriteBlock
BNQ sbfsWriteFailed
DPUP.3 0xFF
DPUP.3 0x01
SETD.0 SbfsBlock
CALL sbfsStepWord
SETD.0 SbfsLeft
LDA.0
DECA
STA.0
BNA sbfsWriteLoop
sbfsWriteDone:
RSTA
RSTB
CCF
ADD
RET
sbfsWriteFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; Copies a whole block from Data Memory at DP1 into the disk's buffer, which is the other
; way round from sbfsBufferOut.
sbfsBufferIn:
INIA 0d1
OUTA 0xE0
PSHD.1
POPA
POPB
OUTB 0xE1
OUTA 0xE2
INIA 0d3
OUTA 0xE3
RSTA
OUTA 0xE4
OUTA 0xE5
INIA 0x01
OUTA 0xE6
RSTA
OUTA 0xE7
INIA 0x01
OUTA 0xE8
RET
; Puts the disk's buffer down as the block named by SbfsBlock. Q is zero if it worked.
sbfsWriteBlock:
SETD.0 SbfsBlock
LDA.0
OUTA 0x20
INCD.0
LDA.0
OUTA 0x21
INIA 0x02
OUTA 0x22
INA 0x23
INIB 0x02
AND
RET
; Reads the block named by SbfsBlock into the disk's buffer. Q is zero if it worked.
sbfsReadBlock:
SETD.0 SbfsBlock
LDA.0
OUTA 0x20
INCD.0
LDA.0
OUTA 0x21
INIA 0x01
OUTA 0x22
INA 0x23
INIB 0x02
AND ; Q is the error bit, so zero means it worked.
RET
; Copies the disk's buffer, a whole block of it, to Data Memory at DP1.
sbfsBufferOut:
INIA 0d3
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
INIA 0d1
OUTA 0xE3
PSHD.1
POPA
POPB
OUTB 0xE4
OUTA 0xE5
INIA 0x01
OUTA 0xE6
RSTA
OUTA 0xE7 ; A whole block.
INIA 0x01
OUTA 0xE8
RET
; Two bytes from DP0 to DP1, most significant first, the way every number on a SplitBit
; disk is stored.
sbfsCopyWord:
LDA.0
STA.1
INCD.0
INCD.1
LDA.0
STA.1
RET
; Adds one to the two byte number at DP0.
sbfsStepWord:
DPUP.0 0d01
LDA.0
INCA
STA.0
BNC sbfsStepDone ; It did not wrap, so the high byte is untouched.
DPDN.0 0d01
LDA.0
INCA
STA.0
sbfsStepDone:
RET
; The two byte number at DP0 becomes itself plus the one at DP2. Only memory changes, so
; it survives the return: a routine cannot hand back a pointer or a register.
sbfsAddWord:
DPUP.0 0d01
DPUP.2 0d01
LDA.0
LDB.2
CCF
ADD
MVQA
STA.0
DPDN.0 0d01
DPDN.2 0d01
LDA.0
LDB.2
ADD ; Carries in from the low half. Nothing between the two touches it.
MVQA
STA.0
RET
; Compares the two byte number at DP0 with the one at DP2. Q is zero if they are equal,
; and the Carry Flag is set if the one at DP0 is the smaller. Both come back, because
; neither Q nor the Status register is put back by a return.
sbfsCompareWord:
LDA.0
LDB.2
CCF
SUB ; The high bytes settle it unless they are the same.
BNQ sbfsCompareDone
INCD.0
INCD.2
LDA.0
LDB.2
CCF
SUB
sbfsCompareDone:
RET
; The byte at DP0 against the byte at DP2. Q is zero if they are the same.
sbfsSameByte:
LDA.0
LDB.2
XOR
RET
#Data
SbfsMagic:
"SBFS"
SbfsDirStart:
0x00 0x00
SbfsDirBlocks:
0x00 0x00
SbfsFileStart:
0x00 0x00
SbfsFileBlocks:
0x00 0x00
SbfsFileTail:
0x00
SbfsBlock:
0x00 0x00
SbfsDiskBlocks:
0x00 0x00
SbfsWantBlocks:
0x00 0x00
SbfsCandidate:
0x00 0x00
SbfsCandEnd:
0x00 0x00
SbfsEntryStart:
0x00 0x00
SbfsEntryEnd:
0x00 0x00
SbfsScratch:
0x00 0x00
SbfsOne:
0x00 0x01
SbfsCount:
0x00
SbfsLeft:
0x00
SbfsWanted:
#Reserve 0d22
SbfsBuffer:
#Reserve 0d256
+33
View File
@@ -0,0 +1,33 @@
; A program meant to be loaded off a disk rather than booted from.
;
; It is assembled for where it will live. Reserving the front of each segment puts the
; first thing in it at a known address, and everything after that follows, so the labels
; inside are already right for the place the loader will put it. Nothing relocates
; anything: this works because the addresses here and the addresses in its header say the
; same thing.
;
; The loader keeps below both of these, which is the whole of the arrangement.
#Program
#Reserve 0x2000 ; This program's code lives from 0x2000.
hello:
SETD.0 HelloText
helloLoop:
LDA.0
BRA helloDone
OUTA 0x00
INCD.0
BRI helloLoop
helloDone:
INIA 0x0A
OUTA 0x00
HALT
#Data
#Reserve 0x1000 ; And its data from 0x1000.
HelloText:
"loaded off a disk, with a string and a loop of its own"
+184
View File
@@ -0,0 +1,184 @@
; Loads a program off a disk and runs it.
;
; Everything this needs already existed: the filesystem reads the file, the controller
; writes Program Memory, and BRD jumps to an address worked out at run time. The only new
; part is the sixteen bytes on the front of a loadable program that say where it goes.
;
; Nothing relocates anything. The program is put exactly where its header asks, and that
; has to be where it was assembled for, or every branch inside it points somewhere wrong.
; This loader therefore keeps out of the way: its own code and data sit above the
; addresses the loaded program claims, which is an arrangement rather than a mechanism.
;
; Correct output is:
; loader
; loaded off a disk, with a string and a loop of its own
#Include print.asm
#Include sbfs.asm
#Program
; print.asm branches here at the top of the Program Segment, so this is where the machine
; arrives whether or not the Boot Vector is obeyed. Naming it in the Vector Segment as
; well says plainly where the program begins.
start:
SETD.0 LoaderName
CALL printString
CALL lineFeed
CALL sbfsMount
BNQ loaderNoDisk
SETD.0 ProgramName
CALL sbfsFind
BNQ loaderMissing
SETD.1 Staging
CALL sbfsRead
BNQ loaderUnreadable
; "SBEX", or it is not a program at all. Loading a text file would otherwise put
; nonsense into Program Memory and jump into it.
SETD.0 Staging
SETD.2 ExecMagic
INIA 0d4
SETD.1 LoaderCount
STA.1
loaderMagicLoop:
LDA.0
LDB.2
XOR
BNQ loaderNotProgram
INCD.0
INCD.2
LDA.1
DECA
STA.1
BNA loaderMagicLoop
SETD.0 Staging
DPUP.0 0d04
LDA.0
INIB 0d1
XOR
BNQ loaderWrongVersion
; The code first. Source is the bytes just past the header; destination is wherever the
; header says the code lives.
INIA 0d1
OUTA 0xE0 ; SourceBank: Data Memory, where the file was read to.
SETD.0 Staging
DPUP.0 0d16
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
RSTA
OUTA 0xE3 ; DestBank: Program Memory.
; Where the code goes. Read and written out a byte at a time rather than through a
; subroutine, because a subroutine could not hand the two bytes back: CALL preserves
; A and B along with the first three Data Pointers, so anything a routine puts in them
; is undone by its own return.
SETD.0 Staging
DPUP.0 0d06
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
; How much of it there is.
SETD.0 Staging
DPUP.0 0d10
LDA.0
OUTA 0xE6
INCD.0
LDA.0
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit.
; Then the data. A blit leaves its addresses past whatever it touched, so the source is
; already sitting on the first byte of the data and only the destination changes.
INIA 0d1
OUTA 0xE3 ; DestBank: Data Memory.
SETD.0 Staging
DPUP.0 0d12
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
SETD.0 Staging
DPUP.0 0d14
LDA.0
OUTA 0xE6
INCD.0
LDA.0
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit.
; And go. The entry address is a number until BRD makes it a place. DP3 is built from
; the two bytes by hand, since it is the pointer nothing puts back.
SETD.0 Staging
DPUP.0 0d08
LDA.0
INCD.0
LDB.0
PSHA
PSHB
POPD.3
BRD.3
loaderNoDisk:
SETD.0 NoDisk
BRI loaderComplain
loaderMissing:
SETD.0 NotThere
BRI loaderComplain
loaderUnreadable:
SETD.0 Unreadable
BRI loaderComplain
loaderNotProgram:
SETD.0 NotProgram
BRI loaderComplain
loaderWrongVersion:
SETD.0 WrongVersion
loaderComplain:
CALL printString
CALL lineFeed
HALT
#Data
ExecMagic:
"SBEX"
LoaderName:
"loader"
ProgramName:
"hello.sbx"
NoDisk:
"no disk"
NotThere:
"no such program"
Unreadable:
"could not read it"
NotProgram:
"not a program"
WrongVersion:
"a version I do not know"
LoaderCount:
0x00
Staging:
#Reserve 0d512
#Vectors
Boot start
+117
View File
@@ -0,0 +1,117 @@
; The negative sense branches.
;
; A quarter of every conditional branch in the corpus used to be a branch over an
; unconditional one, because only "branch if zero" existed. Each of those needed a label
; invented purely to be jumped past, which is a cost in names as much as in bytes.
;
; These test the register directly, the way the positive ones do. A branch on this machine
; never depends on which instruction ran last, except for the two that read the Carry Flag
; and say so in their names.
;
; Each case is checked both ways round, so a branch that always went the same way would
; be caught rather than looking correct half the time.
;
; Correct output is:
; QAB C qab c
; 9876543210
#Include print.asm
#Program
start:
; ---- Each one taken when it should be. ----
RSTA
INIB 0d1
CCF
ADD ; Q is 1, so not zero.
BNQ qTaken
BRI wrong
qTaken:
INIA 0d81 ; 'Q'
OUTA 0x00
INIA 0d5
BNA aTaken
BRI wrong
aTaken:
INIA 0d65 ; 'A'
OUTA 0x00
INIB 0d5
BNB bTaken
BRI wrong
bTaken:
INIA 0d66 ; 'B'
OUTA 0x00
CCF
BNC cTaken
BRI wrong
cTaken:
CALL blankSpace
INIA 0d67 ; 'C'
OUTA 0x00
CALL blankSpace
; ---- And each one not taken when it should not be. ----
RSTA
RSTB
CCF
ADD ; Q is zero.
BNQ wrong
INIA 0d113 ; 'q'
OUTA 0x00
RSTA
BNA wrong
INIA 0d97 ; 'a'
OUTA 0x00
RSTB
BNB wrong
INIA 0d98 ; 'b'
OUTA 0x00
; Set the carry by overflowing, then check BNC does not take it.
INIA 0xFF
INIB 0x01
CCF
ADD
BNC wrong
CALL blankSpace
INIA 0d99 ; 'c'
OUTA 0x00
CALL lineFeed
; ---- A countdown, which is what the missing sense was mostly wanted for. ----
; Before these existed this loop needed a label to jump over, and now it does not.
INIA 0d10
SETD.0 Count
STA.0
countLoop:
LDA.0
DECA ; Ten down to one becomes nine down to zero.
INIB 0d48 ; '0'
CCF
ADD
MVQA
OUTA 0x00
LDA.0
DECA
STA.0
BNA countLoop ; One instruction where it used to take two and a label.
CALL lineFeed
HALT
wrong:
SETD.0 Wrong
CALL printString
CALL lineFeed
HALT
#Data
Wrong:
"a branch went the wrong way"
Count:
0x00
+42
View File
@@ -0,0 +1,42 @@
; A write protected disk.
;
; The bar is in the device, not in the filesystem. A flag in a superblock can be got
; around by writing blocks directly; this cannot be got around at all. It is the tab on
; the side of a floppy rather than a note asking politely.
;
; A disk is read only if it was attached that way, or if the host will not let its image
; be written. The machine cannot tell the two apart and does not need to.
;
; Status bit 2 says the disk is protected. Unlike the busy and error bits it describes the
; medium rather than the last operation, so it reads true before anything has been asked
; of the disk at all.
;
; Correct output is:
; 04 protected, before anything has been attempted
; 04 a read is allowed and does not disturb the bit
; 06 a write is barred: protected, and the operation failed
#Include print.asm
#Program
start:
INA 0x23
CALL printByteHex
CALL lineFeed
RSTA
OUTA 0x20
OUTA 0x21 ; Block 0.
INIA 0x01
OUTA 0x22 ; Read. Reading a protected disk is ordinary.
INA 0x23
CALL printByteHex
CALL lineFeed
INIA 0x02
OUTA 0x22 ; Write. Barred by the device.
INA 0x23
CALL printByteHex
CALL lineFeed
HALT
+143
View File
@@ -0,0 +1,143 @@
; Storage.
;
; The disk is a block device and nothing more. It knows numbered blocks of 256 bytes and
; has never heard of a file, which is deliberate: a filesystem is software this machine
; will run rather than something the host does on its behalf. A disk that understood
; filenames would be the emulator doing the work while the machine pretended it had.
;
; Its buffer is one block, and like any memory a device brings, it is unreachable until
; it is registered as a bank and gone through with the controller.
;
; Between writing and reading the buffer is scrubbed, so a stale copy of the message
; sitting in it cannot make a broken read look like a working one.
;
; The image is made fresh for every run of the suite, so block 3 is zeroes before this
; program touches it.
;
; Correct output is:
; 00 the write worked
; from the disk block 3 came back
; 02 a block past the end of the image is an error, not a fault
#Include print.asm
#Program
start:
; The disk's buffer becomes bank 5.
INIA 0d5
OUTA 0xE3
INIA 0x20
OUTA 0xE2
INIA 0x03
OUTA 0xE8
; Put the message into the buffer and write it to block 3.
SETD.0 Message
CALL aimSourceData
CALL aimDestBuffer
INIA 0d16
CALL setLength
INIA 0x01
OUTA 0xE8
CALL selectBlockThree
INIA 0x02
OUTA 0x22 ; Write.
INA 0x23
CALL printByteHex
CALL lineFeed
; Scrub the buffer, so what comes back has to have come off the disk.
CALL aimDestBuffer
INIA 0x2D ; '-' as the fill byte.
OUTA 0xE2
INIA 0d16
CALL setLength
INIA 0x02
OUTA 0xE8 ; Fill.
CALL selectBlockThree
INIA 0x01
OUTA 0x22 ; Read.
; Bring the block into Data Memory and print it.
INIA 0d5
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
SETD.0 Landing
CALL aimDestData
INIA 0d16
CALL setLength
INIA 0x01
OUTA 0xE8
SETD.0 Landing
CALL printString
CALL lineFeed
; A block past the end of the image says so in Status rather than stopping the machine.
; A disk that cannot read a block is an ordinary thing that happens to working programs.
INIA 0xFF
OUTA 0x20
INIA 0xFF
OUTA 0x21
INIA 0x01
OUTA 0x22
INA 0x23
CALL printByteHex
CALL lineFeed
HALT
selectBlockThree:
RSTA
OUTA 0x20
INIA 0d3
OUTA 0x21
RET
aimSourceData:
INIA 0d1
OUTA 0xE0
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
RET
aimDestData:
INIA 0d1
OUTA 0xE3
PSHD.0
POPA
POPB
OUTB 0xE4
OUTA 0xE5
RET
aimDestBuffer:
INIA 0d5
OUTA 0xE3
RSTA
OUTA 0xE4
OUTA 0xE5
RET
setLength:
PSHA
RSTA
OUTA 0xE6
POPA
OUTA 0xE7
RET
#Data
Message:
"from the disk"
Landing:
#Reserve 0d20
+156
View File
@@ -0,0 +1,156 @@
; Reading the SplitBit Filesystem.
;
; The disk this reads was made by SplitDisk, which is the other implementation of the same
; format. That is what makes this worth running: the library is being checked against
; something written by different code working from the same written specification, rather
; than against itself. If the two ever drift, this is where it shows.
;
; The cases here are the ones most likely to be wrong. across.txt is longer than a block,
; so reading it has to carry on from one to the next. Its entry, and the two after it, are
; in the second directory block, so finding it has to walk past the end of the first.
; aName22CharactersLong! fills the name field exactly, so there is no zero on the end of
; it to stop a comparison. empty.txt has no blocks at all.
;
; Correct output is:
; greeting.txt 0000 11 hello from a file
; across.txt 0002 BC ABCDEFGH...
; aName22CharactersLong! 0000 16 exactly twenty two!!!!
; empty.txt 0000 00
; absent.txt missing
#Include print.asm
#Include sbfs.asm
#Program
start:
CALL sbfsMount
BRQ mounted
SETD.0 NoMount
CALL printString
CALL lineFeed
HALT
mounted:
SETD.0 WantGreeting
CALL showFile
SETD.0 WantAcross
CALL showFile
SETD.0 WantLongName
CALL showFile
SETD.0 WantEmpty
CALL showFile
SETD.0 WantAbsent
CALL showFile
HALT
; DP0 names a file. Prints its name, what the directory says about it, and then the file
; itself. DP3 keeps the name across the calls, because it is the pointer a CALL does not
; put back.
showFile:
PSHD.0
POPD.3
CALL printString
CALL blankSpace
PSHD.3
POPD.0
CALL sbfsFind
BRQ showFound
SETD.0 Missing
CALL printString
CALL lineFeed
RET
showFound:
SETD.0 SbfsFileBlocks
LDA.0
CALL printByteHex
INCD.0
LDA.0
CALL printByteHex
CALL blankSpace
SETD.0 SbfsFileTail
LDA.0
CALL printByteHex
CALL blankSpace
SETD.1 Landing
CALL sbfsRead
BRQ showContents
SETD.0 Failed
CALL printString
CALL lineFeed
RET
; Writes out exactly as many bytes as the file has, and no more. The length is the block
; count and the tail side by side: blocks times 256 plus the tail is the same as putting
; the count in the high byte and the tail in the low one.
showContents:
SETD.0 Landing
SETD.1 SbfsFileBlocks
DPUP.1 0d01
LDA.1
SETD.1 LeftHigh
STA.1
SETD.1 SbfsFileTail
LDA.1
SETD.1 LeftLow
STA.1
showLoop:
SETD.1 LeftHigh
LDA.1
SETD.2 LeftLow
LDB.2
OR
BRQ showEnd ; Nothing left of it.
LDA.0
OUTA 0x00
INCD.0
SETD.1 LeftLow
LDA.1
DECA
STA.1
BRC showBorrow ; It went under, so the high byte owes one.
BRI showLoop
showBorrow:
SETD.1 LeftHigh
LDA.1
DECA
STA.1
BRI showLoop
showEnd:
CALL lineFeed
RET
#Data
WantGreeting:
"greeting.txt"
WantAcross:
"across.txt"
WantLongName:
"aName22CharactersLong!"
WantEmpty:
"empty.txt"
WantAbsent:
"absent.txt"
NoMount:
"no mount"
Missing:
"missing"
Failed:
"failed"
LeftHigh:
0x00
LeftLow:
0x00
Landing:
#Reserve 0d1024
+157
View File
@@ -0,0 +1,157 @@
; Writing the SplitBit Filesystem.
;
; A file's size is settled when it is made, because nothing here can grow one afterwards:
; files are laid down contiguously, so the block after a file usually belongs to somebody
; else. That is the bargain the format makes, and it is why the size comes first.
;
; The disk already has a file on it, so the second one has to be put somewhere that does
; not tread on it. There is no allocation table to consult: with contiguous files the
; directory already says which blocks are spoken for, so finding room is a walk through
; the entries rather than a lookup.
;
; What is written is read straight back, through the same directory, so a file that went
; down in the wrong place would come back wrong rather than looking plausible.
;
; Correct output is:
; here.txt 0002
; first.txt 0003 written by SplitBit itself
; second.txt 0004 and a second one after it
#Include print.asm
#Include sbfs.asm
#Program
start:
CALL sbfsMount
BNQ failed
; What was already there, and where.
SETD.0 Existing
CALL report
BNQ failed
SETD.0 FirstName
SETD.1 FirstText
INIA 0d26
CALL makeAndWrite
BNQ failed
SETD.0 SecondName
SETD.1 SecondText
INIA 0d25
CALL makeAndWrite
BNQ failed
; And read both of them back off the disk.
SETD.0 FirstName
CALL report
BNQ failed
SETD.0 SecondName
CALL report
BNQ failed
HALT
failed:
SETD.0 Failed
CALL printString
CALL lineFeed
HALT
; DP0 names the file, DP1 is its text, and A is how many bytes of it there are. Nothing
; here is longer than a block, so the whole size is the tail.
makeAndWrite:
; The text goes on the Stack, not into DP3. DP3 is the pointer a call does not put back,
; which cuts both ways: it is how a routine hands one out, and it is therefore not a
; safe place to leave anything across a call to a routine that might use it. sbfsCreate
; does use it.
PSHD.1
SETD.1 SbfsFileTail
STA.1
RSTA
SETD.1 SbfsFileBlocks
STA.1
INCD.1
STA.1
CALL sbfsCreate
POPD.1 ; Back off the Stack whether it worked or not.
BNQ makeFailed
CALL sbfsWriteFile
RET
makeFailed:
RET
; Prints a file's name, where it begins, and what is in it.
report:
PSHD.0
POPD.3
CALL printString
CALL blankSpace
PSHD.3
POPD.0
CALL sbfsFind
BNQ reportFailed
SETD.0 SbfsFileStart
LDA.0
CALL printByteHex
INCD.0
LDA.0
CALL printByteHex
CALL blankSpace
SETD.1 Landing
CALL sbfsRead
BNQ reportFailed
; Everything here fits in one block, so the tail is the whole length.
SETD.0 Landing
SETD.1 SbfsFileTail
LDA.1
SETD.1 LeftOver
STA.1
BRA reportEnd
reportLoop:
LDA.0
OUTA 0x00
INCD.0
SETD.1 LeftOver
LDA.1
DECA
STA.1
BNA reportLoop
reportEnd:
CALL lineFeed
RSTA
RSTB
CCF
ADD
RET
reportFailed:
RSTA
INIB 0d1
CCF
ADD
RET
#Data
Existing:
"here.txt"
FirstName:
"first.txt"
SecondName:
"second.txt"
FirstText:
"written by SplitBit itself"
SecondText:
"and a second one after it"
Failed:
"failed"
LeftOver:
0x00
Landing:
#Reserve 0d512