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
+1
View File
@@ -3,6 +3,7 @@ Programs/build/
Object/ Object/
Assembler Assembler
SplitBit SplitBit
SplitDisk
CLAUDE.md CLAUDE.md
resume resume
codexResume.sh codexResume.sh
+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
+27 -1
View File
@@ -12,6 +12,9 @@ SplitBit is a custom 8 bit system designed for hobbyist projects and experimenta
- Modular Codebase: Mostly clean separation of CPU, I/O, and utility functions for easy modification. - Modular Codebase: Mostly clean separation of CPU, I/O, and utility functions for easy modification.
- Interrupts: Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save. - Interrupts: Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save.
- Devices: A bus registry that says what a machine is made of, so a program can ask rather than being told. - Devices: A bus registry that says what a machine is made of, so a program can ask rather than being told.
- Filesystem: SBFS, read and written by SplitBit itself, and by a host tool that speaks the same format so an image can be moved either way.
- Loadable Programs: A program that was not booted from carries a header saying where it belongs, and Programs/loader.asm reads one off a disk, puts it there, and runs it.
- Storage: A block device with 256 byte blocks and 16 megabytes of them, backed by an image file on the host. It knows blocks and not files, because a filesystem is meant to be software SplitBit runs.
- Memory Controller: Reads and writes Program Memory, moves blocks between memory banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program. - Memory Controller: Reads and writes Program Memory, moves blocks between memory banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program.
- Assembler: Assemble human readable assembly language files directly into SplitBit compatible binary files. Supports including external files, handling labels, alignment and reservation, and defining Program, Data and Vector segments. - Assembler: Assemble human readable assembly language files directly into SplitBit compatible binary files. Supports including external files, handling labels, alignment and reservation, and defining Program, Data and Vector segments.
@@ -21,7 +24,7 @@ SplitBit is a custom 8 bit system designed for hobbyist projects and experimenta
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
cd SplitBit-Emulator cd SplitBit-Emulator
``` ```
2) Build the Emulator and the Assembler: You'll need gcc and make or similar. 2) Build the Emulator, the Assembler and the disk tool: You'll need gcc and make or similar.
``` ```
make make
``` ```
@@ -44,11 +47,28 @@ The sources are ISO C, and build clean under -std=c11 -pedantic with -Wall -Wext
- -d, --debug: Enable debug mode to single step through cycles. Each key press advances one instruction. - -d, --debug: Enable debug mode to single step through cycles. Each key press advances one instruction.
- -c, --cycles N: Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. - -c, --cycles N: Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time.
- -f, --fast: Run as fast as the host machine allows, ignoring the emulated cycle rate. - -f, --fast: Run as fast as the host machine allows, ignoring the emulated cycle rate.
- -D, --disk \<file\>: Attach a disk image, creating a 128K one if the file is not there.
- -W, --write-protect: Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not.
- -h, --help: Show help and usage information. - -h, --help: Show help and usage information.
#### Notes: #### Notes:
- If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed. - If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed.
### Usage:
```
./SplitDisk <command> <image> [arguments]
```
#### Commands:
- format \<image\> [blocks] [dirblocks]: Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 files.
- list \<image\>: Show what is on the disk.
- put \<image\> \<file\> [name]: Put a host file onto it. Without a name it uses the file's own, which is often longer than the 22 characters a name may be.
- get \<image\> \<name\> [file]: Take one off it.
- delete \<image\> \<name\>: Remove one.
#### Notes:
- SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. Until SplitBit can write its own filesystem this is the only way to get a program onto a disk.
- Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens put says so rather than putting part of a file on.
### Usage: ### Usage:
``` ```
./Assembler [options] [assembly file] ./Assembler [options] [assembly file]
@@ -86,6 +106,12 @@ The test suite assembles and runs every program in Programs/ and compares the re
``` ```
make test make test
``` ```
Disk images that tests read from are built by Tests/makedisks.sh before the run, using SplitDisk. A test that reads one is therefore checked against a filesystem written by different code from the same written format, rather than against itself.
The disk tool is checked separately by Tests/disk.sh, which make test runs afterwards: it puts files of every awkward size onto an image and takes them off again, and checks that the things the format says cannot happen are refused.
Tests/docs.sh then checks the manuals against the code: that every instruction has a row and every row is an instruction, that the counts in the headings are right, that every directive is written down, that every routine the manuals promise exists, and that the worked examples still assemble to the bytes printed beside them. Documentation goes stale quietly, and this is what stops it.
Tests are defined in Tests/manifest, one line per program. To record the current output as the expected result, after you have checked that it is correct: Tests are defined in Tests/manifest, one line per program. To record the current output as the expected result, after you have checked that it is correct:
``` ```
make bless make bless
+7
View File
@@ -29,6 +29,13 @@ Instruction instruction_set[] = {
{0x13, "BRB"}, {0x13, "BRB"},
{0x14, "BRC"}, {0x14, "BRC"},
{0x15, "BRD"}, {0x15, "BRD"},
// The same four conditions the other way round. A quarter of the conditional
// branches in the corpus were a branch over an unconditional one before these
// existed, each of them needing a label invented only to be jumped past.
{0x1A, "BNQ"},
{0x1B, "BNA"},
{0x1C, "BNB"},
{0x1D, "BNC"},
{0x17, "CALL"}, {0x17, "CALL"},
{0x18, "SWI"}, {0x18, "SWI"},
{0x19, "RETI"}, {0x19, "RETI"},
+2 -1
View File
@@ -335,7 +335,8 @@ static void checkOperands(intermediateElement *intermediateArray, int arraySize,
// the branch block takes an address: RET has none, and BRD gets its destination // the branch block takes an address: RET has none, and BRD gets its destination
// from a Data Pointer instead of from the program. // from a Data Pointer instead of from the program.
if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 || if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 ||
opcode == 0x13 || opcode == 0x14 || opcode == 0x17) { opcode == 0x13 || opcode == 0x14 || opcode == 0x17 ||
opcode == 0x1A || opcode == 0x1B || opcode == 0x1C || opcode == 0x1D) {
// Branches and CALL take a two byte address, which only a label can supply. // Branches and CALL take a two byte address, which only a label can supply.
if (nextType != LABEL) problem = "Branch without label."; if (nextType != LABEL) problem = "Branch without label.";
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) { } else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
+555
View File
@@ -0,0 +1,555 @@
// SplitDisk.c
// Makes and edits SplitBit disk images from the host.
//
// Until SplitBit can write its own filesystem there has to be some way to get a program
// onto a disk, and this is it. It is not a shortcut around the machine: it speaks exactly
// the format SplitBit will speak, so an image this makes is one the machine can read and
// an image the machine writes is one this can read back.
//
// Written by Anachronaut
#include "sbfs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Numbers on a SplitBit disk are most significant byte first, the same as everywhere
// else on the machine.
static uint16_t readWord(const uint8_t *at) {
return (uint16_t)((at[0] << 8) | at[1]);
}
static void writeWord(uint8_t *at, uint16_t value) {
at[0] = (uint8_t)(value >> 8);
at[1] = (uint8_t)(value & 0xFF);
}
static FILE *openImage(const char *path, const char *mode) {
FILE *image = fopen(path, mode);
if (image == NULL) {
fprintf(stderr, "Error: Couldn't open the disk image \"%s\".\n", path);
}
return image;
}
static int readBlock(FILE *image, uint16_t block, uint8_t *into) {
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|| fread(into, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
fprintf(stderr, "Error: Couldn't read block %u.\n", block);
return 1;
}
return 0;
}
static int writeBlock(FILE *image, uint16_t block, const uint8_t *from) {
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|| fwrite(from, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
fprintf(stderr, "Error: Couldn't write block %u.\n", block);
return 1;
}
return 0;
}
typedef struct {
uint16_t diskBlocks;
uint16_t directoryStart;
uint16_t directoryBlocks;
uint16_t freeBlocks;
} Superblock;
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
// a formatted one with no files would be the same thing.
static int readSuperblock(FILE *image, Superblock *super) {
uint8_t block[SBFS_BLOCK_BYTES];
if (readBlock(image, 0, block)) {
return 1;
}
if (memcmp(block, SBFS_MAGIC, SBFS_MAGIC_BYTES) != 0) {
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
return 1;
}
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION) {
fprintf(stderr, "Error: That disk is version %u, and this understands version %u.\n",
block[SBFS_SUPER_VERSION], SBFS_VERSION);
return 1;
}
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
return 0;
}
static int writeSuperblock(FILE *image, const Superblock *super) {
uint8_t block[SBFS_BLOCK_BYTES];
memset(block, 0, sizeof(block));
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
block[SBFS_SUPER_VERSION] = SBFS_VERSION;
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
return writeBlock(image, 0, block);
}
// The whole directory, held in memory while a command works on it. It is small enough
// that reading it once and writing it back once is simpler than picking at blocks, and it
// is what the machine will do too once it has the Data Memory to spare.
typedef struct {
uint8_t *bytes;
int entries;
} Directory;
static int readDirectory(FILE *image, const Superblock *super, Directory *directory) {
directory->entries = super->directoryBlocks * SBFS_ENTRIES_PER_BLOCK;
directory->bytes = malloc((size_t)super->directoryBlocks * SBFS_BLOCK_BYTES);
if (directory->bytes == NULL) {
fprintf(stderr, "Error: Out of memory reading the directory.\n");
return 1;
}
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
if (readBlock(image, (uint16_t)(super->directoryStart + i),
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
free(directory->bytes);
directory->bytes = NULL;
return 1;
}
}
return 0;
}
static int writeDirectory(FILE *image, const Superblock *super, const Directory *directory) {
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
if (writeBlock(image, (uint16_t)(super->directoryStart + i),
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
return 1;
}
}
return 0;
}
static uint8_t *entryAt(const Directory *directory, int index) {
return directory->bytes + (size_t)index * SBFS_ENTRY_BYTES;
}
static int entryInUse(const uint8_t *entry) {
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
}
static uint32_t entrySize(const uint8_t *entry) {
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
+ entry[SBFS_ENTRY_TAIL];
}
static uint16_t entryBlocksUsed(const uint8_t *entry) {
return (uint16_t)SBFS_BLOCKS_USED(readWord(entry + SBFS_ENTRY_BLOCKS),
entry[SBFS_ENTRY_TAIL]);
}
// Names are compared as written, the way labels are, and are padded rather than
// terminated, so a name that fills the field has no terminator to find.
static void entryName(const uint8_t *entry, char *into) {
memcpy(into, entry + SBFS_ENTRY_NAME, SBFS_NAME_BYTES);
into[SBFS_NAME_BYTES] = '\0';
}
static int findByName(const Directory *directory, const char *name) {
char held[SBFS_NAME_BYTES + 1];
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry)) {
continue;
}
entryName(entry, held);
if (strcmp(held, name) == 0) {
return i;
}
}
return -1;
}
// Where the first free run of the wanted length begins, or -1 if there is not one.
//
// There is no allocation bitmap, and that is the design rather than an omission: with
// files laid down contiguously, every block is either inside some entry's range or it is
// not, so the directory already is the allocation map. A bitmap would be a second copy of
// a fact that is already written down, and a second copy is a thing that can disagree.
static long findFreeRun(const Directory *directory, const Superblock *super, uint16_t wanted) {
if (wanted == 0) {
// An empty file occupies nothing, so it has no start to speak of. Block 0 is the
// superblock and can never hold file data, which makes it the honest way to say
// "nowhere" without inventing a place that belongs to somebody else.
(void)directory;
return 0;
}
uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
for (uint32_t candidate = firstData; candidate + wanted <= super->diskBlocks; candidate++) {
uint32_t clash = 0;
for (int i = 0; i < directory->entries && !clash; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry)) {
continue;
}
uint32_t start = readWord(entry + SBFS_ENTRY_START);
uint32_t used = entryBlocksUsed(entry);
if (used == 0) {
continue;
}
if (candidate < start + used && start < candidate + wanted) {
// Overlaps this file, so start looking again past the end of it.
clash = start + used;
}
}
if (clash) {
candidate = clash - 1; // The loop's increment takes it to clash.
continue;
}
return (long)candidate;
}
return -1;
}
static uint16_t countFree(const Directory *directory, const Superblock *super) {
uint32_t used = 0;
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (entryInUse(entry)) {
used += entryBlocksUsed(entry);
}
}
// Block 0 and everything up to the end of the directory is not available.
uint32_t overhead = (uint32_t)super->directoryStart + super->directoryBlocks;
return (uint16_t)(super->diskBlocks - overhead - used);
}
// ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
if (blocks <= 1u + directoryBlocks) {
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
" directory of %u.\n", blocks, directoryBlocks);
return 1;
}
// Quietly, because a disk that is not there yet is the ordinary case for format and
// not something to complain about on the way past.
FILE *image = fopen(path, "r+b");
if (image == NULL) {
image = openImage(path, "w+b");
if (image == NULL) {
return 1;
}
}
uint8_t empty[SBFS_BLOCK_BYTES];
memset(empty, 0, sizeof(empty));
for (uint16_t i = 0; i < blocks; i++) {
if (writeBlock(image, i, empty)) {
fclose(image);
return 1;
}
}
Superblock super;
super.diskBlocks = blocks;
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
super.directoryBlocks = directoryBlocks;
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
}
fclose(image);
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
path, blocks, directoryBlocks, super.freeBlocks);
return 0;
}
static int commandList(const char *path) {
FILE *image = openImage(path, "rb");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
printf("%s: %u blocks, %u of directory, %u entries.\n",
path, super.diskBlocks, super.directoryBlocks, directory.entries);
printf("%-22s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
char name[SBFS_NAME_BYTES + 1];
int shown = 0;
for (int i = 0; i < directory.entries; i++) {
const uint8_t *entry = entryAt(&directory, i);
if (!entryInUse(entry)) {
continue;
}
entryName(entry, name);
printf("%-22s %8u %7u %7u\n", name, entrySize(entry),
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry));
shown++;
}
// The count in the superblock is a cache, so say what the directory actually adds up
// to as well. If the two ever disagree, the directory is the one to believe.
uint16_t counted = countFree(&directory, &super);
printf("%d file%s, %u blocks free", shown, shown == 1 ? "" : "s", counted);
if (counted != super.freeBlocks) {
printf(" (the superblock says %u, which is stale)", super.freeBlocks);
}
printf(".\n");
free(directory.bytes);
fclose(image);
return 0;
}
static int commandPut(const char *path, const char *hostFile, const char *asName) {
FILE *source = fopen(hostFile, "rb");
if (source == NULL) {
fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile);
return 1;
}
fseek(source, 0, SEEK_END);
long size = ftell(source);
rewind(source);
if (size < 0) {
fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile);
fclose(source);
return 1;
}
if (strlen(asName) > SBFS_NAME_BYTES) {
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name may be %d.\n"
" Give a shorter one as the last argument.\n",
asName, strlen(asName), SBFS_NAME_BYTES);
fclose(source);
return 1;
}
FILE *image = openImage(path, "r+b");
if (image == NULL) {
fclose(source);
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(source);
fclose(image);
return 1;
}
if (findByName(&directory, asName) >= 0) {
fprintf(stderr, "Error: \"%s\" is already on the disk. Delete it first.\n", asName);
goto failed;
}
int slot = -1;
for (int i = 0; i < directory.entries && slot < 0; i++) {
if (!entryInUse(entryAt(&directory, i))) {
slot = i;
}
}
if (slot < 0) {
fprintf(stderr, "Error: The directory is full: %d entries, all taken.\n", directory.entries);
goto failed;
}
uint16_t whole = (uint16_t)SBFS_WHOLE_BLOCKS(size);
uint8_t tail = (uint8_t)SBFS_TAIL_BYTES(size);
uint16_t needed = (uint16_t)SBFS_BLOCKS_USED(whole, tail);
long start = findFreeRun(&directory, &super, needed);
if (start < 0) {
fprintf(stderr, "Error: No run of %u free blocks. There may be room on the disk"
" without there being room in one piece.\n", needed);
goto failed;
}
uint8_t block[SBFS_BLOCK_BYTES];
for (uint16_t i = 0; i < needed; i++) {
memset(block, 0, sizeof(block));
size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source);
if (got == 0 && i < needed) {
fprintf(stderr, "Error: \"%s\" ended sooner than its size said.\n", hostFile);
goto failed;
}
if (writeBlock(image, (uint16_t)(start + i), block)) {
goto failed;
}
}
uint8_t *entry = entryAt(&directory, slot);
memset(entry, 0, SBFS_ENTRY_BYTES);
entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE;
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
entry[SBFS_ENTRY_TAIL] = tail;
memcpy(entry + SBFS_ENTRY_NAME, asName, strlen(asName));
super.freeBlocks = countFree(&directory, &super);
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
goto failed;
}
printf("Put %s on as \"%s\": %ld bytes at block %ld.\n", hostFile, asName, size, start);
free(directory.bytes);
fclose(source);
fclose(image);
return 0;
failed:
free(directory.bytes);
fclose(source);
fclose(image);
return 1;
}
static int commandGet(const char *path, const char *name, const char *hostFile) {
FILE *image = openImage(path, "rb");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
int slot = findByName(&directory, name);
if (slot < 0) {
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
free(directory.bytes);
fclose(image);
return 1;
}
const uint8_t *entry = entryAt(&directory, slot);
uint32_t size = entrySize(entry);
uint16_t start = readWord(entry + SBFS_ENTRY_START);
uint16_t used = entryBlocksUsed(entry);
FILE *out = fopen(hostFile, "wb");
if (out == NULL) {
fprintf(stderr, "Error: Couldn't write \"%s\".\n", hostFile);
free(directory.bytes);
fclose(image);
return 1;
}
uint8_t block[SBFS_BLOCK_BYTES];
uint32_t left = size;
for (uint16_t i = 0; i < used; i++) {
if (readBlock(image, (uint16_t)(start + i), block)) {
fclose(out);
free(directory.bytes);
fclose(image);
return 1;
}
uint32_t take = (left < SBFS_BLOCK_BYTES) ? left : SBFS_BLOCK_BYTES;
fwrite(block, 1, take, out);
left -= take;
}
fclose(out);
printf("Got \"%s\" off as %s: %u bytes.\n", name, hostFile, size);
free(directory.bytes);
fclose(image);
return 0;
}
static int commandDelete(const char *path, const char *name) {
FILE *image = openImage(path, "r+b");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
int slot = findByName(&directory, name);
if (slot < 0) {
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
free(directory.bytes);
fclose(image);
return 1;
}
// A deleted entry and a never used one are the same thing: the in use bit goes down
// and its blocks are free again. The blocks themselves are left as they were, which
// is worth knowing if anything is ever meant to be private.
memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES);
super.freeBlocks = countFree(&directory, &super);
int failed = writeDirectory(image, &super, &directory) || writeSuperblock(image, &super);
if (!failed) {
printf("Deleted \"%s\". %u blocks free.\n", name, super.freeBlocks);
}
free(directory.bytes);
fclose(image);
return failed;
}
static void printUsage(const char *program) {
printf("Usage: %s <command> <image> [arguments]\n", program);
printf("\n");
printf("Commands:\n");
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
printf(" list <image> Show what is on the disk.\n");
printf(" put <image> <file> [name] Put a host file onto it.\n");
printf(" get <image> <name> [file] Take one off it.\n");
printf(" delete <image> <name> Remove one.\n");
printf("\n");
printf("Blocks are %d bytes. A name may be %d characters. Without one, put uses the\n",
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
printf("file's own name, which is often too long, and it will say so.\n");
}
// The part of a path after the last separator, so that put can default to a file's own
// name rather than the whole path it was found at.
static const char *baseName(const char *path) {
const char *slash = strrchr(path, '/');
return slash ? slash + 1 : path;
}
int main(int argc, char *argv[]) {
if (argc < 2 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
printUsage(argv[0]);
return argc < 2 ? 1 : 0;
}
const char *command = argv[1];
if (argc < 3) {
fprintf(stderr, "Error: %s needs a disk image.\n", command);
return 1;
}
const char *path = argv[2];
if (strcmp(command, "format") == 0) {
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
" one of directory.\n");
return 1;
}
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks);
}
if (strcmp(command, "list") == 0) {
return commandList(path);
}
if (strcmp(command, "put") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: put needs a file to put on.\n");
return 1;
}
return commandPut(path, argv[3], (argc > 4) ? argv[4] : baseName(argv[3]));
}
if (strcmp(command, "get") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: get needs the name of a file on the disk.\n");
return 1;
}
return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]);
}
if (strcmp(command, "delete") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: delete needs the name of a file on the disk.\n");
return 1;
}
return commandDelete(path, argv[3]);
}
fprintf(stderr, "Error: There is no \"%s\" command.\n", command);
printUsage(argv[0]);
return 1;
}
+46
View File
@@ -0,0 +1,46 @@
// sbex.h
// The SplitBit loadable program format, version one.
//
// A program that is not the one the machine booted from has to say where it wants to
// live, because nothing relocates it. This is a header saying that, in front of the
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
// with room for the machine to ask a few more questions later.
//
// Two things read this: whatever builds one on the host, and the loader running on
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
//
// All multi byte numbers are most significant byte first.
//
// 0 4 "SBEX"
// 4 1 Version
// 5 1 Reserved
// 6 2 Where the code goes in Program Memory
// 8 2 Where to start running, an address in Program Memory
// 10 2 How many bytes of code there are
// 12 2 Where the data goes in Data Memory
// 14 2 How many bytes of data there are
// 16 The code, then the data
//
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
// than an arithmetic. Nothing here relocates anything: the addresses are where the
// program was built to live, and putting it anywhere else would leave every branch and
// every SETD inside it pointing at the wrong place.
//
// Written by Anachronaut
#ifndef SBEX_H
#define SBEX_H
#define SBEX_MAGIC "SBEX"
#define SBEX_MAGIC_BYTES 4
#define SBEX_VERSION 1
#define SBEX_HEADER_BYTES 16
#define SBEX_VERSION_AT 4
#define SBEX_CODE_AT 6
#define SBEX_ENTRY_AT 8
#define SBEX_CODE_LEN_AT 10
#define SBEX_DATA_AT 12
#define SBEX_DATA_LEN_AT 14
#endif // SBEX_H
+79
View File
@@ -0,0 +1,79 @@
// sbfs.h
// The SplitBit Filesystem, version one.
//
// This is the host side's copy of the format. The other implementation is SplitBit
// assembly running on the machine itself, so nothing can be shared between them except
// the specification: the two have to be kept honest by a document rather than by a
// header. Everything here follows that document exactly, and anything that changes here
// has to change there in the same breath.
//
// All multi byte numbers are most significant byte first, the same as every other number
// SplitBit stores: addresses, the SPBT binary header, and the vector table.
//
// Written by Anachronaut
#ifndef SBFS_H
#define SBFS_H
#include <stdint.h>
#define SBFS_MAGIC "SBFS"
#define SBFS_MAGIC_BYTES 4
#define SBFS_VERSION 1
#define SBFS_BLOCK_BYTES 256
// ---- Block 0, the superblock ----
//
// 0 4 "SBFS"
// 4 1 Version
// 5 1 Reserved
// 6 2 Blocks on the disk
// 8 2 First directory block
// 10 2 Blocks the directory occupies
// 12 2 Free blocks, a cache rather than the authority
// 14 Reserved to the end of the block
#define SBFS_SUPER_VERSION 4
#define SBFS_SUPER_DISK 6
#define SBFS_SUPER_DIRSTART 8
#define SBFS_SUPER_DIRBLOCKS 10
#define SBFS_SUPER_FREE 12
// ---- Directory entries ----
//
// 0 1 Flags
// 1 2 First block of the file's data
// 3 2 Whole blocks the file occupies
// 5 1 Bytes in the trailing part block, or zero if there is not one
// 6 22 Name, padded with zeroes
// 28 4 Reserved
//
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
// reading one never means handling a split.
#define SBFS_ENTRY_BYTES 32
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
#define SBFS_ENTRY_FLAGS 0
#define SBFS_ENTRY_START 1
#define SBFS_ENTRY_BLOCKS 3
#define SBFS_ENTRY_TAIL 5
#define SBFS_ENTRY_NAME 6
#define SBFS_NAME_BYTES 22
#define SBFS_FLAG_IN_USE 0x01
// The directory begins at block 1 and is this many blocks unless told otherwise, which
// is sixty four files. The superblock carries the real number, so this is only what a
// freshly formatted disk gets.
#define SBFS_FIRST_DIRECTORY_BLOCK 1
#define SBFS_DEFAULT_DIRECTORY_BLOCKS 8
// A file of n bytes occupies n / 256 whole blocks and, if anything is left over, one more
// for the tail. Zero means zero in both, so an empty file occupies nothing at all.
#define SBFS_WHOLE_BLOCKS(bytes) ((bytes) / SBFS_BLOCK_BYTES)
#define SBFS_TAIL_BYTES(bytes) ((bytes) % SBFS_BLOCK_BYTES)
#define SBFS_BLOCKS_USED(blocks, tail) ((blocks) + ((tail) ? 1 : 0))
#endif // SBFS_H
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Wraps an assembled SplitBit binary into a loadable program.
The assembler emits a boot image: a Program Segment that loads at zero and a Data Segment
that does the same. A program meant to be loaded somewhere else has to say where it goes,
which is what the SBEX header in front of it is for.
A program says where it lives by reserving the front of each segment, so the addresses
given here have to match the reserves in its source. Nothing checks that for you, and
nothing relocates anything if you get it wrong.
"""
import struct
import sys
def segments(raw):
at = 4 + 1 + 4 # magic, version, feature flags
assert raw[:4] == b"SPBT", "not a SplitBit binary"
assert raw[at:at + 3] == b"PRG"
plen = struct.unpack(">H", raw[at + 3:at + 5])[0]
program = raw[at + 5:at + 5 + plen]
at = at + 5 + plen
assert raw[at:at + 3] == b"DAT"
dlen = struct.unpack(">H", raw[at + 3:at + 5])[0]
return program, raw[at + 5:at + 5 + dlen]
def main():
if len(sys.argv) != 6:
sys.exit("usage: wrap.py <binary> <output> <code address> <data address> <entry>")
binary, output = sys.argv[1], sys.argv[2]
codeAt, dataAt, entry = (int(a, 0) for a in sys.argv[3:6])
program, data = segments(open(binary, "rb").read())
# Everything below the address a segment is placed at is the padding the reserve put
# there, and is not part of the program.
code = program[codeAt:]
values = data[dataAt:]
header = bytearray(16)
header[0:4] = b"SBEX"
header[4] = 1
struct.pack_into(">H", header, 6, codeAt)
struct.pack_into(">H", header, 8, entry)
struct.pack_into(">H", header, 10, len(code))
struct.pack_into(">H", header, 12, dataAt)
struct.pack_into(">H", header, 14, len(values))
with open(output, "wb") as out:
out.write(header)
out.write(code)
out.write(values)
print("%s: %d bytes of code at 0x%04X, %d of data at 0x%04X, entry 0x%04X"
% (output, len(code), codeAt, len(values), dataAt, entry))
main()
+32
View File
@@ -270,6 +270,38 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// CALL - Push the Program Counter to the Stack, and perform an immediate branch. // CALL - Push the Program Counter to the Stack, and perform an immediate branch.
genericCall(cpu); genericCall(cpu);
break; break;
case 0x1A:
// BNQ - Branch if Q is not 0.
if(cpu->Q != 0) {
genericBranch(cpu);
} else {
cpu->ProgramCounter+=2;
}
break;
case 0x1B:
// BNA - Branch if A is not 0.
if(cpu->A != 0) {
genericBranch(cpu);
} else {
cpu->ProgramCounter+=2;
}
break;
case 0x1C:
// BNB - Branch if B is not 0.
if(cpu->B != 0) {
genericBranch(cpu);
} else {
cpu->ProgramCounter+=2;
}
break;
case 0x1D:
// BNC - Branch if the Carry Flag is clear.
if (!(cpu->Status & STATUS_CARRY)) {
genericBranch(cpu);
} else {
cpu->ProgramCounter+=2;
}
break;
case 0x18: { case 0x18: {
// SWI - Software Interrupt. The byte after the opcode names the vector. // SWI - Software Interrupt. The byte after the opcode names the vector.
// Never masked: this is an instruction the program deliberately ran, not // Never masked: this is an instruction the program deliberately ran, not
+5
View File
@@ -10,6 +10,7 @@
#include <stdlib.h> #include <stdlib.h>
#include "cpu.h" #include "cpu.h"
#include "controller.h" #include "controller.h"
#include "io.h"
#include "utility.h" #include "utility.h"
#include <string.h> #include <string.h>
#include <getopt.h> #include <getopt.h>
@@ -90,6 +91,9 @@ int main (int argc, char *argv[]) {
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile); fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
return 1; return 1;
} }
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
return 1;
}
CPURegisters cpu; CPURegisters cpu;
// The controller has to know where the memories are before anything can reach // The controller has to know where the memories are before anything can reach
// them through it. Banks 0 and 1 are those two arrays. // them through it. Banks 0 and 1 are those two arrays.
@@ -136,6 +140,7 @@ int main (int argc, char *argv[]) {
printf("Cycle: %lu\n", cycleCount); printf("Cycle: %lu\n", cycleCount);
} }
} }
detachDisk();
if (limitReached) { if (limitReached) {
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount); printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
} else if (cpu.Status & STATUS_FAULT) { } else if (cpu.Status & STATUS_FAULT) {
+125 -3
View File
@@ -63,6 +63,109 @@ uint8_t refusingPort(void) {
return refusedPort; return refusedPort;
} }
// ---- The disk ----
//
// A block device and nothing more. It knows numbered blocks and has never heard of a
// file, which is the whole point: a filesystem is software this machine will run, not
// something the host does on its behalf. A disk that understood filenames would be the
// emulator doing the work and the machine pretending it had.
static FILE *diskImage = NULL;
static uint32_t diskBlockCount = 0;
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
static uint16_t diskBlock = 0;
static uint8_t diskStatus = 0;
static uint8_t diskProtected = 0;
uint8_t attachDisk(const char *path, uint8_t writeProtect) {
diskProtected = writeProtect ? 1 : 0;
diskImage = fopen(path, "r+b");
if (diskImage == NULL) {
// It may be there and simply not writable, which is a read only disk rather than
// a missing one. Try that before deciding to make a new one.
diskImage = fopen(path, "rb");
if (diskImage != NULL) {
diskProtected = 1;
}
}
if (diskImage == NULL) {
// Nothing there, so make one. A fresh image is zeroes, which is what an unwritten
// block should read as.
diskImage = fopen(path, "w+b");
if (diskImage == NULL) {
fprintf(stderr, "Error: Couldn't open or create the disk image: %s\n", path);
return 1;
}
static const uint8_t empty[DISK_BLOCK_BYTES] = {0};
for (uint32_t i = 0; i < DISK_DEFAULT_BLOCKS; i++) {
if (fwrite(empty, 1, DISK_BLOCK_BYTES, diskImage) != DISK_BLOCK_BYTES) {
fprintf(stderr, "Error: Couldn't write the disk image: %s\n", path);
fclose(diskImage);
diskImage = NULL;
return 1;
}
}
}
if (fseek(diskImage, 0, SEEK_END) != 0) {
fprintf(stderr, "Error: Couldn't measure the disk image: %s\n", path);
fclose(diskImage);
diskImage = NULL;
return 1;
}
long size = ftell(diskImage);
// A part written block at the end is not a block, so it is not counted.
diskBlockCount = (size > 0) ? (uint32_t)(size / DISK_BLOCK_BYTES) : 0;
// The protect bit is a standing property, so it reads true before anything has been
// asked of the disk rather than only after a write has been turned away.
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
return 0;
}
void detachDisk(void) {
if (diskImage != NULL) {
fclose(diskImage);
diskImage = NULL;
}
}
// Reads or writes the block the block registers name. The line goes up either way: the
// operation finished, and whether it worked is what Status is for.
static void diskCommand(uint8_t command) {
// The protect bit describes the disk rather than the operation, so it survives.
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
if (command == DISK_COMMAND_WRITE && diskProtected) {
diskStatus |= DISK_STATUS_ERROR;
raiseInterrupt(PORT_DISK);
return;
}
if (diskImage == NULL || diskBlock >= diskBlockCount) {
diskStatus |= DISK_STATUS_ERROR;
raiseInterrupt(PORT_DISK);
return;
}
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
if (fseek(diskImage, offset, SEEK_SET) != 0) {
diskStatus |= DISK_STATUS_ERROR;
raiseInterrupt(PORT_DISK);
return;
}
size_t moved = 0;
if (command == DISK_COMMAND_READ) {
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
} else if (command == DISK_COMMAND_WRITE) {
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
fflush(diskImage);
} else {
diskStatus |= DISK_STATUS_ERROR;
raiseInterrupt(PORT_DISK);
return;
}
if (moved != DISK_BLOCK_BYTES) {
diskStatus |= DISK_STATUS_ERROR;
}
raiseInterrupt(PORT_DISK);
}
// ---- A device that brings memory ---- // ---- A device that brings memory ----
// //
// The simplest thing that owns a bank. Writing to its port fills its memory with the // The simplest thing that owns a bank. Writing to its port fills its memory with the
@@ -75,12 +178,19 @@ uint8_t refusingPort(void) {
static uint8_t deviceMemoryBlock[DEVICE_MEMORY_BYTES]; static uint8_t deviceMemoryBlock[DEVICE_MEMORY_BYTES];
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) { uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
if (port != PORT_MEMORY) { if (port == PORT_MEMORY) {
return NULL;
}
*capacity = DEVICE_MEMORY_BYTES; *capacity = DEVICE_MEMORY_BYTES;
return deviceMemoryBlock; return deviceMemoryBlock;
} }
if (port == PORT_DISK) {
// The disk's buffer is one block. Reading fills it and writing takes what is in
// it, and the only way to reach it is to register it as a bank and go through the
// controller.
*capacity = DISK_BLOCK_BYTES;
return diskBuffer;
}
return NULL;
}
// ---- The bus registry ---- // ---- The bus registry ----
// //
@@ -104,6 +214,7 @@ static const DeviceRecord deviceTable[] = {
{ PORT_TEST, DEVICE_TEST, 0 }, { PORT_TEST, DEVICE_TEST, 0 },
{ PORT_REFUSE, DEVICE_REFUSE, 0 }, { PORT_REFUSE, DEVICE_REFUSE, 0 },
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY }, { PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 }, { PORT_REGISTRY, DEVICE_REGISTRY, 0 },
}; };
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0])); static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
@@ -122,6 +233,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) { if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
return &controllerRecord; return &controllerRecord;
} }
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
// The base port is in the table proper, since that is the one that owns the
// memory and raises the line. The rest of the block reports the same device.
return deviceOnPort(PORT_DISK);
}
for (int i = 0; i < deviceCount; i++) { for (int i = 0; i < deviceCount; i++) {
if (deviceTable[i].port == port) { if (deviceTable[i].port == port) {
return &deviceTable[i]; return &deviceTable[i];
@@ -159,6 +275,9 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Later, I'll want to use a buffer for this for performance, probably. // Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte); putchar(DataByte);
break; break;
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
case DISK_COMMAND: diskCommand(DataByte); break;
case PORT_MEMORY: case PORT_MEMORY:
// Fills the memory this device owns with the byte written. Nothing is // Fills the memory this device owns with the byte written. Nothing is
// reachable from here: to get at it, register it as a bank and go through // reachable from here: to get at it, register it as a bank and go through
@@ -204,6 +323,9 @@ uint8_t InputHandler(uint8_t Address) {
// If data is sent here, it should be read from STDIN. // If data is sent here, it should be read from STDIN.
return getchar(); return getchar();
break; break;
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
case DISK_STATUS: return diskStatus;
case PORT_REFUSE: case PORT_REFUSE:
// Refuses reads as well, so both directions are covered. // Refuses reads as well, so both directions are covered.
refuseAccess(VECTOR_GUARD_VIOLATION); refuseAccess(VECTOR_GUARD_VIOLATION);
+48
View File
@@ -18,6 +18,16 @@
#define PORT_TEST 0x10 #define PORT_TEST 0x10
#define PORT_REFUSE 0x11 #define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12 #define PORT_MEMORY 0x12
// The disk answers on a block of four ports and interrupts on the first of them. A device
// that spans more than one port raises its line on its base, which is the rule the
// machine has not needed until now: the controller spans sixteen and never interrupts.
#define PORT_DISK 0x20
#define PORT_DISK_TOP 0x23
#define DISK_BLOCK_HIGH 0x20
#define DISK_BLOCK_LOW 0x21
#define DISK_COMMAND 0x22
#define DISK_STATUS 0x23
#define PORT_REGISTRY 0xFF #define PORT_REGISTRY 0xFF
// ---- Device classes ---- // ---- Device classes ----
@@ -35,11 +45,49 @@
#define DEVICE_TEST 0x10 #define DEVICE_TEST 0x10
#define DEVICE_REFUSE 0x11 #define DEVICE_REFUSE 0x11
#define DEVICE_MEMORY 0x12 #define DEVICE_MEMORY 0x12
#define DEVICE_DISK 0x13
// What a device brings besides itself. This means memory that somebody has to register // What a device brings besides itself. This means memory that somebody has to register
// with the controller, so the controller's own bank 2 does not count: it is already there. // with the controller, so the controller's own bank 2 does not count: it is already there.
#define DEVICE_FLAG_HAS_MEMORY 0x01 #define DEVICE_FLAG_HAS_MEMORY 0x01
// ---- The disk ----
//
// Blocks are a page each, so a block number is the whole of a 16 bit address and the
// arithmetic never needs a multiply. Sixteen megabytes is absurd for this machine, which
// is the point: there is room for anything a filesystem might want to grow into later.
#define DISK_BLOCK_BYTES 256
// A fresh image is made the size of Program and Data together, which is a round number
// for this machine and small enough to read in a hex editor while it is being built.
#define DISK_DEFAULT_BLOCKS 512
#define DISK_COMMAND_READ 0x01
#define DISK_COMMAND_WRITE 0x02
// Set while an operation is still going. It always reads clear here, because the host
// finishes before the next instruction does, but a machine with a slower disk would set
// it and a program that ignores it would break there. Honour it anyway.
#define DISK_STATUS_BUSY 0x01
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not
// about the last operation: it is a standing property of the medium, readable before
// anything is attempted. A write protected disk is barred here, in the device, rather
// than by anything in the filesystem, so writing blocks directly cannot get around it.
#define DISK_STATUS_PROTECTED 0x04
// Set when the last operation did not work: no image, or a block that is not on it.
// A disk that cannot read a block is an ordinary thing that happens to working programs,
// so it says so rather than stopping the machine.
#define DISK_STATUS_ERROR 0x02
// Attaches an image, making one if it is not there. A disk is read only if the host will
// not let the file be written, or if writeProtect asks for it, which is the emulated
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
uint8_t attachDisk(const char *path, uint8_t writeProtect);
void detachDisk(void);
// How many bytes a device's entry in the registry runs to. Reading past the end gives // How many bytes a device's entry in the registry runs to. Reading past the end gives
// zero, so the record can grow later without anything already written having to change. // zero, so the record can grow later without anything already written having to change.
#define DEVICE_RECORD_BYTES 2 #define DEVICE_RECORD_BYTES 2
+14 -1
View File
@@ -17,6 +17,9 @@ void printHelp(const char *programName) {
printf(" -d, --debug Enable debug mode.\n"); printf(" -d, --debug Enable debug mode.\n");
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n"); printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n"); printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
printf(" you write is read only whether you ask for this or not.\n");
printf(" -h, --help Display this help message.\n"); printf(" -h, --help Display this help message.\n");
} }
@@ -25,6 +28,8 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
{"debug", no_argument, 0, 'd'}, {"debug", no_argument, 0, 'd'},
{"cycles", required_argument, 0, 'c'}, {"cycles", required_argument, 0, 'c'},
{"fast", no_argument, 0, 'f'}, {"fast", no_argument, 0, 'f'},
{"disk", required_argument, 0, 'D'},
{"write-protect", no_argument, 0, 'W'},
{"help", no_argument, 0, 'h'}, {"help", no_argument, 0, 'h'},
{0, 0, 0, 0 } {0, 0, 0, 0 }
}; };
@@ -34,9 +39,11 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
options->debug = 0; options->debug = 0;
options->fast = 0; options->fast = 0;
options->cycles = 0; options->cycles = 0;
options->disk = NULL;
options->writeProtect = 0;
// Parse options // Parse options
while ((opt = getopt_long(argc, argv, "dc:fh", long_options, &option_index)) != -1) { while ((opt = getopt_long(argc, argv, "dc:fhD:W", long_options, &option_index)) != -1) {
switch (opt) { switch (opt) {
case 'd': case 'd':
options->debug = 1; options->debug = 1;
@@ -57,6 +64,12 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
case 'f': case 'f':
options->fast = 1; options->fast = 1;
break; break;
case 'D':
options->disk = optarg;
break;
case 'W':
options->writeProtect = 1;
break;
case 'h': case 'h':
printHelp(argv[0]); printHelp(argv[0]);
return OPTIONS_HELP; return OPTIONS_HELP;
+2
View File
@@ -19,6 +19,8 @@ typedef struct {
uint8_t debug; // Step one instruction at a time, printing the registers. uint8_t debug; // Step one instruction at a time, printing the registers.
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows. uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts. unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
} EmulatorOptions; } EmulatorOptions;
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options); uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
+4
View File
@@ -22,6 +22,10 @@ The assembler will accept:
Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'. Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'.
A string may be up to 255 characters. Each one is written down with a zero byte on the end, which is what lets a program find where it stops, and it means two strings written one after the other are not one longer string: there is a zero between them. A run of bytes longer than a string can hold has to be written as literals, or put there by the program itself while it runs.
The one exception to the single byte rule is #Align and #Reserve, whose numbers are never emitted as bytes and may go up to 0xFFFF. See Moving The Cursor Along.
## Labels: ## Labels:
Labels may be a string of up to 32 alphanumeric characters that must end with a colon, ':'. Labels may be a string of up to 32 alphanumeric characters that must end with a colon, ':'.
+142 -16
View File
@@ -37,6 +37,10 @@ It has ten registers:
- Bit 2 is the Interrupt Flag. It is set by SIF and cleared by CIF. While it is set the CPU answers devices asking for attention; while it is clear they wait. Arriving at a handler clears it, and RETI restores it along with the rest of the Status register. See Hardware Interrupts below. - Bit 2 is the Interrupt Flag. It is set by SIF and cleared by CIF. While it is set the CPU answers devices asking for attention; while it is clear they wait. Arriving at a handler clears it, and RETI restores it along with the rest of the Status register. See Hardware Interrupts below.
- Bit 7 is the Halt Flag. It is set by the HALT instruction, and by a fault. - Bit 7 is the Halt Flag. It is set by the HALT instruction, and by a fault.
Each condition has a branch both ways round, so a loop that carries on while something is not zero is one instruction rather than a branch over an unconditional one. Before these existed a quarter of every conditional branch in the corpus was written backwards and padded out, and each of those needed a label invented only to be jumped past.
A conditional branch reads the thing it names at the moment it runs. BRA looks at A, and it does not matter which instruction set A or how long ago. The two that read the Carry Flag are the exception, and they say so in their names. That is worth knowing when reading somebody else's code: nothing else on this machine leaves a hidden condition behind for a later branch to find.
## The Vector Table: ## The Vector Table:
The top kilobyte of Program Memory is reserved for vectors. Each entry is two bytes, most significant byte first, and holds a Program Memory address. The top kilobyte of Program Memory is reserved for vectors. Each entry is two bytes, most significant byte first, and holds a Program Memory address.
@@ -129,6 +133,7 @@ If a device interrupts and its vector is empty, that is a fault: the machine sto
| 0x00 | The console. Writing sends a byte to standard output, reading takes one from standard input. | 0x02 | | 0x00 | The console. Writing sends a byte to standard output, reading takes one from standard input. | 0x02 |
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 | | 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 | | 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
| 0x20 - 0x23 | The disk. See Storage below. It interrupts on 0x20, its base port. | 0x13 |
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 | | 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
| 0xE0 - 0xEF | The memory controller. See The Memory Controller below. | 0x03 | | 0xE0 - 0xEF | The memory controller. See The Memory Controller below. | 0x03 |
| 0xFF | The bus registry. See Asking What Is There below. | 0x01 | | 0xFF | The bus registry. See Asking What Is There below. | 0x01 |
@@ -172,7 +177,8 @@ One thing to be careful of: the registry remembers which port it was asked about
| 0x10 | Test device, which raises its own line. | | 0x10 | Test device, which raises its own line. |
| 0x11 | Test device, which refuses everything. | | 0x11 | Test device, which refuses everything. |
| 0x12 | Test device, which owns memory. | | 0x12 | Test device, which owns memory. |
| 0x13 - 0xFF | Peripherals. | | 0x13 | Disk. |
| 0x14 - 0xFF | Peripherals. |
## The Memory Controller: ## The Memory Controller:
@@ -285,6 +291,16 @@ A range that ends before it starts is refused. No address could be inside it, so
A raised fence is published in the bank table along with everything else about the bank, so a program can see what is guarded without having to remember. A raised fence is published in the bank table along with everything else about the bank, so a program can see what is guarded without having to remember.
### What The Fence Does Not Do:
It is worth being plain about the limit, because the name suggests more than it delivers.
Every write to Program Memory goes through the controller, so a fence over bank 0 catches all of them. That is what it is for: code that gets walked over is otherwise discovered much later, when the wreckage is finally executed, thousands of cycles from the mistake and with the evidence gone. A fence turns that into a fault at the instruction responsible, with the address still in the controller's registers to be read.
Data Memory is different. STA, STB, STQ and STD write bank 1 directly and never go near the controller, so a runaway Data Pointer walking over a program's variables is **not** caught and cannot be. Catching it would mean putting the check inside the CPU's store path, which would make an instruction behave differently depending on state that does not appear in the listing. That is the thing this machine does not do.
So the fence protects code from a mistaken loader. It is not general memory protection, and a program should not be written as though it were.
### Loading A Program: ### Loading A Program:
Everything the controller does adds up to one thing a SplitBit machine could not do before: run code that was not in the binary it started from. Everything the controller does adds up to one thing a SplitBit machine could not do before: run code that was not in the binary it started from.
@@ -314,16 +330,6 @@ INIA, OUTA and RETI name no places, so a routine built only from those runs corr
So loading works and relocating does not exist. A program can be put into memory at the address it was built for, and it will run. Putting it anywhere else is an unsolved problem, and a real one, because a machine that can only ever load a program to one place cannot load two programs at once. So loading works and relocating does not exist. A program can be put into memory at the address it was built for, and it will run. Putting it anywhere else is an unsolved problem, and a real one, because a machine that can only ever load a program to one place cannot load two programs at once.
### What The Fence Does Not Do:
It is worth being plain about the limit, because the name suggests more than it delivers.
Every write to Program Memory goes through the controller, so a fence over bank 0 catches all of them. That is what it is for: code that gets walked over is otherwise discovered much later, when the wreckage is finally executed, thousands of cycles from the mistake and with the evidence gone. A fence turns that into a fault at the instruction responsible, with the address still in the controller's registers to be read.
Data Memory is different. STA, STB, STQ and STD write bank 1 directly and never go near the controller, so a runaway Data Pointer walking over a program's variables is **not** caught and cannot be. Catching it would mean putting the check inside the CPU's store path, which would make an instruction behave differently depending on state that does not appear in the listing. That is the thing this machine does not do.
So the fence protects code from a mistaken loader. It is not general memory protection, and a program should not be written as though it were.
### Being Turned Away: ### Being Turned Away:
A bank knows how big it is, so an access past the end of one is a BankFault rather than a read of whatever happens to be next. Naming a bank with nothing registered in it is the same fault. A bank knows how big it is, so an access past the end of one is a BankFault rather than a read of whatever happens to be next. Naming a bank with nothing registered in it is the same fault.
@@ -332,6 +338,120 @@ A write the controller will not perform is a GuardViolation. There are two reaso
Both arrive at the instruction that asked, so a handler sees which one it was. A handler that means to carry on past it steps the saved address on by two, since the input and output instructions are an opcode and a port. Both arrive at the instruction that asked, so a handler sees which one it was. A handler that means to carry on past it steps the saved address on by two, since the input and output instructions are an opcode and a port.
## Storage:
The disk is a block device. It knows numbered blocks of 256 bytes and has never heard of a file. A filesystem is software this machine runs, not something done on its behalf: a disk that understood filenames would be the emulator doing the work while the machine pretended it had.
A block is a page, so a block number is a whole 16 bit address and the arithmetic never needs a multiply. Sixteen megabytes of them is absurd for this machine, which is the point: there is room for whatever a filesystem grows into.
| Port | Register |
| --- | --- |
| 0x20 | BlockHigh |
| 0x21 | BlockLow |
| 0x22 | Command. 0x01 reads, 0x02 writes. |
| 0x23 | Status. Bit 0 busy, bit 1 the last operation failed, bit 2 the disk is write protected. |
The disk owns one block of memory, its buffer. Reading fills it and writing takes what is in it. Like any memory a device brings, it is unreachable until it has been registered as a bank, and reachable only through the memory controller even then. The CPU never touches it directly.
A device that answers on more than one port raises its line on the first of them, so the disk interrupts on 0x20.
### Waiting:
A command returns at once and the line goes up when the block has moved. Status bit 0 says the disk is still working.
That bit always reads clear here, because the host finishes before the next instruction does. **Honour it anyway.** A machine with a slower disk would set it, and a program written to ignore it would work on this one and fail on that one. Waiting for the line is the other way, and the right one once vectors are installed; the bit is what a bootstrap polls before there are any.
### Write Protection:
A disk may be read only, and the bar is in the device. Status bit 2 says so.
That bit is not like the two below it. Busy and error describe the last operation; **protection describes the medium**, so it reads true before anything has been asked of the disk at all. A program can find out whether it can write without having to try and be refused.
The bar being in the device is the point of it. A flag in a superblock can be got around by writing blocks directly, and 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 those two apart, and has no reason to.
Reading a protected disk is ordinary and does not disturb the bit.
### When It Does Not Work:
Status bit 1 says the last operation failed: there is no disk, or the block asked for is not on it.
A disk error is not a fault. Faults on this machine mean it cannot continue, and a read that fails is an ordinary thing that happens to working programs on failing media. It is reported so that a program can cope with it, rather than stopping the machine and taking the choice away.
## Reading The Filesystem:
The disk knows blocks and nothing else, so a filesystem is software. Programs/Libraries/sbfs.asm reads one.
| Routine | Does |
| --- | --- |
| sbfsMount | Registers the disk's buffer as bank 3, reads the superblock, and checks the disk is one of ours. Q is zero if it is. |
| sbfsFind | DP0 names a file, ending in a zero byte. Q is zero if it was found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it. |
| sbfsRead | Reads the file that was found into Data Memory at DP1. Q is zero if it worked. |
| sbfsCreate | Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is. Q is zero if it was made, and then SbfsFileStart says where it went. |
| sbfsWriteFile | Writes the file that was made, from Data Memory at DP1. |
A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known.
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.
A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says.
The other implementation of this format is SplitDisk, on the host. Nothing is shared between the two but the specification, so a change to either has to be a change to both.
### What A Subroutine Can And Cannot Hand Back:
This is the thing that catches people, including whoever wrote the last three pieces of system code, so it is worth stating once and plainly.
CALL saves **A, B, and Data Pointers 0, 1 and 2**, and RET puts all five back. So a subroutine cannot return anything in any of them: whatever it puts there is undone by its own return, silently, and the caller carries on with its old values as though the subroutine had never run.
What comes back is **Q**, which is one byte, and **Data Pointer 3**, which is two. That is the whole of it, and it is why DP3 is not preserved.
The same rule catches a loop that steps a pointer inside a subroutine. The step is thrown away every time round, so the loop reads the same byte forever and the fault is a wrong answer rather than a crash.
If two bytes have to come back and DP3 is spoken for, the honest answers are to write them into Data Memory, or to do the work in the caller rather than in a routine. A short sequence written out twice is better than a subroutine that quietly does nothing.
The same rule cuts the other way, which is easier to miss. Because DP3 is not put back, **a routine you call may leave something of its own in it**. It is where a routine hands a pointer out, so it is not a safe place to leave one of your own across a call to anything that might use it. The Stack is: push it before the call and pop it after, and it will be exactly as it was.
A label may only be defined once across a program and everything it includes, so a routine in one library cannot use a name that another has already taken.
## Loading A Program From A Disk:
A program that was not the one the machine booted from carries sixteen bytes in front of it saying where it belongs.
| Offset | Size | Holds |
| --- | --- | --- |
| 0 | 4 | SBEX |
| 4 | 1 | Version. One. |
| 5 | 1 | Reserved. |
| 6 | 2 | Where the code goes in Program Memory. |
| 8 | 2 | Where to start running. |
| 10 | 2 | How many bytes of code there are. |
| 12 | 2 | Where the data goes in Data Memory. |
| 14 | 2 | How many bytes of data there are. |
| 16 | | The code, and then the data. |
Programs/loader.asm reads one off a disk, puts the two pieces where the header asks, and jumps to the entry with BRD. Every part of that already existed: the filesystem finds the file, the memory controller writes Program Memory, and BRD turns an address worked out at run time into somewhere to go. The header is the only new thing.
The magic matters for the same reason it does everywhere else on this machine. Without it, loading a text file would put nonsense into Program Memory and then jump into it.
### Where A Program Says It Lives:
**Nothing relocates anything.** A program is put exactly where its header asks, and that has to be the address it was assembled for, or every branch and every SETD inside it points somewhere wrong.
A program is assembled for its address by reserving the front of each segment. #Reserve at the top of the Program Segment puts the first instruction after it at a known address, and the same in the Data Segment does it for the data, so every label inside is already right.
```
#Program
#Reserve 0x2000 ; This program's code lives from 0x2000.
hello:
...
```
That is not general placement, since only the first thing in a segment can be put anywhere. It is exactly enough for a program that wants to live at one address, which is what a loadable program is.
The loader keeps its own code and data below the addresses the loaded program claims. That is an arrangement between the two of them rather than anything the machine enforces, and it is the part that a real operating system would have to do properly.
## Refusing: ## Refusing:
A device can refuse what it was asked to do. This is not the same as interrupting. An interrupt is a device asking for attention later, answered between instructions once the CPU is ready. A refusal is a device saying no to the instruction happening now, so the machine stops where it stands rather than carrying on as though the access had worked. A device can refuse what it was asked to do. This is not the same as interrupting. An interrupt is a device asking for attention later, answered between instructions once the CPU is ready. A refusal is a device saying no to the instruction happening now, so the machine stops where it stands rather than carrying on as though the access had worked.
@@ -379,7 +499,7 @@ The Bytes column is the total length of the instruction, counting its opcode, an
| 07 | SHL | 1 | A and B form a circular shift register. Rotate this register left. | | 07 | SHL | 1 | A and B form a circular shift register. Rotate this register left. |
| 08 | SHR | 1 | A and B form a circular shift register. Rotate this register right. | | 08 | SHR | 1 | A and B form a circular shift register. Rotate this register right. |
### Branch and Subroutine Operations: 8 Instructions ### Branch and Subroutine Operations: 14 Instructions
| Hex Code | Mnemonic | Bytes | Description | | Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- | | -- | ---- | -- | -- |
| 10 | BRI | 3 | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. | | 10 | BRI | 3 | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
@@ -388,12 +508,16 @@ The Bytes column is the total length of the instruction, counting its opcode, an
| 13 | BRB | 3 | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. | | 13 | BRB | 3 | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 14 | BRC | 3 | Branch if Carry is set. | | 14 | BRC | 3 | Branch if Carry is set. |
| 15 | BRD | 2 | Branch to the address held in the named Data Pointer. | | 15 | BRD | 2 | Branch to the address held in the named Data Pointer. |
| 1A | BNQ | 3 | Branch if Q is not zero. |
| 1B | BNA | 3 | Branch if A is not zero. |
| 1C | BNB | 3 | Branch if B is not zero. |
| 1D | BNC | 3 | Branch if the Carry Flag is clear. |
| 17 | CALL | 3 | Call subroutine. Pushes the Program Counter, Data Pointers 0 through 2, B and A to the Stack, then performs an immediate branch. This costs ten bytes of Stack. | | 17 | CALL | 3 | Call subroutine. Pushes the Program Counter, Data Pointers 0 through 2, B and A to the Stack, then performs an immediate branch. This costs ten bytes of Stack. |
| 18 | SWI | 2 | Software Interrupt. The next byte names a software vector. Pushes an interrupt frame and dispatches through it. Never masked. | | 18 | SWI | 2 | Software Interrupt. The next byte names a software vector. Pushes an interrupt frame and dispatches through it. Never masked. |
| 19 | RETI | 1 | Return from an interrupt. Restores everything the frame holds and carries on from where the interrupt arrived. | | 19 | RETI | 1 | Return from an interrupt. Restores everything the frame holds and carries on from where the interrupt arrived. |
| 1F | RET | 1 | Return from subroutine. Restores A, B, and Data Pointers 0 through 2 from the Stack, then sets the Program Counter to the instruction after the CALL. Data Pointer 3 and Q are left as the subroutine leaves them. | | 1F | RET | 1 | Return from subroutine. Restores A, B, and Data Pointers 0 through 2 from the Stack, then sets the Program Counter to the instruction after the CALL. Data Pointer 3 and Q are left as the subroutine leaves them. |
### Register Operations: 11 Instructions ### Register Operations: 13 Instructions
| Hex Code | Mnemonic | Bytes | Description | | Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- | | -- | ---- | -- | -- |
| 20 | RSTA | 1 | Resets A to 0. | | 20 | RSTA | 1 | Resets A to 0. |
@@ -423,7 +547,7 @@ Q is where every ALU result lands, and Q is not itself an ALU operand, so MVQA a
| 35 | POPB | 1 | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. | | 35 | POPB | 1 | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. |
| 36 | POPD | 2 | Restores the named Data Pointer from the stack, increments the Stack Pointer by two. | | 36 | POPD | 2 | Restores the named Data Pointer from the stack, increments the Stack Pointer by two. |
### Data Operations: 12 Instructions ### Data Operations: 13 Instructions
| Hex Code | Mnemonic | Bytes | Description | | Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- | | -- | ---- | -- | -- |
| 40 | INCD | 2 | Increments the named Data Pointer. | | 40 | INCD | 2 | Increments the named Data Pointer. |
@@ -465,7 +589,7 @@ LDD and STD are how a program follows an address it has stored, rather than one
## Input and Output In the Emulator: ## Input and Output In the Emulator:
The current implementation has Input 0 and Output 0 hooked to stdin and stdout respectively, allowing programs to read to and from the console. Port 0 is the console: writing sends a byte to standard output and reading takes one from standard input. Everything else this machine has is listed under Devices above, and a program that wants to know what is actually there asks the bus registry rather than assuming.
### Example Program: Hello World ### Example Program: Hello World
``` ```
@@ -507,7 +631,9 @@ A file begins with a nine byte header:
The feature flags are how a binary states that it needs something the base machine does not provide. An emulator that cannot provide everything a binary asks for refuses to run it, rather than running it and going quietly wrong. No feature bits are defined yet, so the field is currently zero in every binary. The feature flags are how a binary states that it needs something the base machine does not provide. An emulator that cannot provide everything a binary asks for refuses to run it, rather than running it and going quietly wrong. No feature bits are defined yet, so the field is currently zero in every binary.
After the header come the two segments, the Program Segment first and then the Data Segment. Each begins with a three character marker, `PRG` or `DAT`, followed by a two byte length. The system loads each memory with the bytes that follow, in sequence, starting from address 0x0000. After the header come the segments. The Program Segment comes first and then the Data Segment, each beginning with a three character marker, `PRG` or `DAT`, followed by a two byte length. The system loads each memory with the bytes that follow, in sequence, starting from address 0x0000.
A third segment may follow them, marked `VEC`, holding the vectors a program named in its Vector Segment. It is four bytes an entry: two saying where in Program Memory the vector sits, and two saying where its handler is. A program that named no vectors has no such segment, and a file that simply ends after its Data Segment is one written before vectors existed. Either way the reader treats the end of the file as an empty table, which is why adding this cost no format version and left every binary already written still loadable.
Here is the hello world program above, assembled and dumped as hex: Here is the hello world program above, assembled and dumped as hex:
Executable
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# Checks SplitDisk against the SBFS format.
#
# The tool and the SplitBit side are two implementations of one written specification,
# and nothing but that document keeps them the same. This checks the host half on its
# own: that a file put onto a disk comes back off it byte for byte, that the sizes which
# exercise the block and tail arithmetic all survive, and that the things the format
# says cannot happen are refused rather than half done.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TOOL="$ROOT/SplitDisk"
WORK="$ROOT/Tests/build/disk"
PASS=0
FAIL=0
FAILED_NAMES=()
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
check() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
else
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "$name"
fi
}
# The opposite: the command is supposed to fail, and passing would be the bug.
refuses() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %s (it was allowed)\n" "$RED" "$RESET" "$name"
else
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
fi
}
if [ ! -x "$TOOL" ]; then
echo "SplitDisk is not built."
exit 1
fi
rm -rf "$WORK"; mkdir -p "$WORK"
cd "$WORK" || exit 1
echo "Checking SplitDisk against the SBFS format."
check "format a disk" "$TOOL" format work.img 64 2
refuses "refuse a disk with no room" "$TOOL" format tiny.img 2 4
refuses "refuse an unformatted disk" "$TOOL" list /dev/null
# The sizes that exercise every corner of blocks-plus-tail: nothing at all, less than a
# block, exactly a block, a part block, and an exact multiple.
: > empty.bin
printf 'x' > one.bin
head -c 256 /dev/urandom > exact.bin
head -c 700 /dev/urandom > part.bin
head -c 768 /dev/urandom > whole.bin
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
check "put $f" "$TOOL" put work.img "$f"
done
roundTrip() {
"$TOOL" get work.img "$1" "got_$1" >/dev/null 2>&1 || return 1
cmp -s "$1" "got_$1"
}
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
check "$f comes back byte for byte" roundTrip "$f"
done
refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin 16bitSegmentedSieveModern.asm
refuses "refuse a duplicate name" "$TOOL" put work.img one.bin
refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin
check "delete" "$TOOL" delete work.img one.bin
refuses "the deleted file is gone" "$TOOL" get work.img one.bin out.bin
check "the name can be used again" "$TOOL" put work.img one.bin
# Contiguous files mean a disk can have room without having room in one piece. That is a
# consequence of the format rather than a bug, so it is checked rather than worked around.
"$TOOL" format frag.img 16 1 >/dev/null 2>&1
head -c 1024 /dev/urandom > a.bin; cp a.bin b.bin; cp a.bin c.bin
"$TOOL" put frag.img a.bin >/dev/null 2>&1
"$TOOL" put frag.img b.bin >/dev/null 2>&1
"$TOOL" put frag.img c.bin >/dev/null 2>&1
"$TOOL" delete frag.img a.bin >/dev/null 2>&1
"$TOOL" delete frag.img c.bin >/dev/null 2>&1
head -c 2048 /dev/urandom > big.bin
refuses "refuse a file with no run long enough" "$TOOL" put frag.img big.bin
head -c 512 /dev/urandom > fits.bin
check "but one that fits the gap goes on" "$TOOL" put frag.img fits.bin
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS disk tool checks passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1
Executable
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# Checks the manuals against the code.
#
# Documentation goes stale quietly. An instruction added without a table row, or a count
# in a heading that nobody updated, is wrong in a way nothing notices until somebody
# trusts it. Everything here is a claim the manuals make that can be settled by looking
# at the source, so it is settled every time the tests run.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT" || exit 1
python3 - <<'PY'
import re
import sys
problems = []
def read(path):
return open(path).read()
pm = read("SplitBit Programming Manual.md")
am = read("SplitBit Assembler Manual.md")
asmc = read("Source/Assembler/assembly.c")
util = read("Source/Assembler/Assm-util.c")
# ---- Every instruction has a row, and every row is an instruction ----
#
# A mnemonic begins with a letter, which is what keeps the offset and size columns of the
# other tables in these manuals out of it.
documented = {(int(m.group(1), 16), m.group(2))
for m in re.finditer(r'^\|\s*([0-9A-F]{2})\s*\|\s*([A-Z][A-Z0-9]*)\s*\|', pm, re.M)}
implemented = {(int(m.group(1), 16), m.group(2))
for m in re.finditer(r'\{0x([0-9A-Fa-f]{2}),\s*"([A-Z0-9]+)"\}', asmc)}
for opcode, name in sorted(implemented - documented):
problems.append("%s (0x%02X) is implemented and not in the manual" % (name, opcode))
for opcode, name in sorted(documented - implemented):
problems.append("%s (0x%02X) is in the manual and not implemented" % (name, opcode))
# ---- The counts in the group headings ----
body = asmc[asmc.index("Instruction instruction_set[]"):asmc.index("int num_instructions")]
actual = {}
group = None
for line in body.split("\n"):
heading = re.match(r'\s*// (.+?) Operations:', line)
if heading:
group = heading.group(1)
actual.setdefault(group, 0)
if re.search(r'\{0x[0-9A-Fa-f]{2},', line) and group:
actual[group] += 1
for m in re.finditer(r'^### (.+?) Operations: (\d+) Instructions?$', pm, re.M):
name, claimed = m.group(1), int(m.group(2))
# The manual's headings are wordier than the source's comments, so match on the start.
match = [v for k, v in actual.items() if name.startswith(k)]
if not match:
problems.append("the manual has a group called \"%s\" that the source does not" % name)
elif match[0] != claimed:
problems.append("the manual says %s has %d instructions, and it has %d"
% (name, claimed, match[0]))
# ---- Every directive the assembler knows is written down ----
for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))):
if directive not in am:
problems.append("%s is a directive and is not in the Assembler Manual" % directive)
# ---- Every routine the manual promises exists ----
for library, names in [("Programs/Libraries/sbfs.asm", re.findall(r'\| (sbfs[A-Za-z]+) \|', pm))]:
defined = set(re.findall(r'^([a-zA-Z][A-Za-z0-9]*):', read(library), re.M))
for name in names:
if name not in defined:
problems.append("the manual lists %s, which %s does not define" % (name, library))
# ---- The worked example still assembles to the bytes the manual prints ----
#
# The hello world program and the hex dump beside it are two claims about the same thing,
# and nothing but this keeps them agreeing.
import os
import subprocess
import tempfile
source = pm.split("### Example Program: Hello World")[1].split("```")[1]
claimed = pm.split("assembled and dumped as hex:")[1].split("```")[1].split()
with tempfile.TemporaryDirectory() as work:
asm = os.path.join(work, "hello.asm")
binary = os.path.join(work, "hello.bin")
open(asm, "w").write(source)
built = subprocess.run(["./Assembler", asm, "-o", binary],
capture_output=True)
if built.returncode != 0:
problems.append("the hello world program in the manual no longer assembles")
else:
actual = ["%02x" % b for b in open(binary, "rb").read()]
if [c.lower() for c in claimed] != actual:
problems.append("the hex dump in the manual is not what that program assembles to"
" now: it prints %d bytes and the assembler makes %d"
% (len(claimed), len(actual)))
# ---- The Assembler Manual's worked programs still assemble ----
for heading in ["## An Example SplitBit Assembly Program:",
"## An Example Using More Than One Data Pointer:",
"## An Example Using Interrupts:"]:
if heading not in am:
problems.append("the Assembler Manual has lost its \"%s\" section" % heading.strip("# :"))
continue
example = am.split(heading)[1].split("```")[1]
with tempfile.TemporaryDirectory() as work:
asm = os.path.join(work, "example.asm")
open(asm, "w").write(example)
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries", asm,
"-o", os.path.join(work, "example.bin")],
capture_output=True)
if built.returncode != 0:
problems.append("the example under \"%s\" no longer assembles"
% heading.strip("# :"))
if problems:
print("The manuals and the code disagree:")
for p in problems:
print(" " + p)
sys.exit(1)
print("The manuals agree with the code.")
PY
+4
View File
@@ -0,0 +1,4 @@
QAB C qab c
9876543210
Execution halted after 176 cycles.
[exit 0]
+5
View File
@@ -0,0 +1,5 @@
04
04
06
Execution halted after 114 cycles.
[exit 0]
+5
View File
@@ -0,0 +1,5 @@
00
from the disk
02
Execution halted after 245 cycles.
[exit 0]
+4
View File
@@ -0,0 +1,4 @@
loader
loaded off a disk, with a string and a loop of its own
Execution halted after 1098 cycles.
[exit 0]
+7
View File
@@ -0,0 +1,7 @@
greeting.txt 0000 11 hello from a file
across.txt 0002 BC ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX
aName22CharactersLong! 0000 16 exactly twenty two!!!!
empty.txt 0000 00
absent.txt missing
Execution halted after 19230 cycles.
[exit 0]
+5
View File
@@ -0,0 +1,5 @@
here.txt 0002 already here
first.txt 0003 written by SplitBit itself
second.txt 0004 and a second one after it
Execution halted after 5888 cycles.
[exit 0]
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Builds the disk images the tests read from.
#
# These are made with SplitDisk, which is the other implementation of the same format.
# That is the point of them: a SplitBit program reading one of these is being checked
# against something written by different code from a written specification, rather than
# against itself.
#
# Written by Anachronaut
set -eu
BUILD="$1"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TOOL="$ROOT/SplitDisk"
DISKS="$BUILD/disks"
WORK="$BUILD/.diskwork"
[ -x "$TOOL" ] || { echo "SplitDisk is not built."; exit 1; }
mkdir -p "$DISKS" "$WORK"
# Two directory blocks, so that a file can be put beyond the first one and the walk from
# block to block gets exercised rather than assumed.
"$TOOL" format "$DISKS/sbfs.img" 64 2 >/dev/null
cd "$WORK"
printf 'hello from a file' > greeting.txt
# Eight files fill the first directory block exactly, so everything after this lands in
# the second one.
for i in 1 2 3 4 5 6 7 8; do printf 'filler %d' "$i" > "filler$i.txt"; done
# Longer than a block, so reading it has to cross from one to the next. The pattern
# repeats every twenty six bytes, which makes a misplaced block obvious to read.
awk 'BEGIN { for (i = 0; i < 700; i++) printf "%c", 65 + (i % 26) }' > across.txt
: > empty.txt
printf 'exactly twenty two!!!!' > longname.txt
"$TOOL" put "$DISKS/sbfs.img" greeting.txt >/dev/null
for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/null; done
"$TOOL" put "$DISKS/sbfs.img" across.txt >/dev/null
"$TOOL" put "$DISKS/sbfs.img" empty.txt >/dev/null
"$TOOL" put "$DISKS/sbfs.img" longname.txt aName22CharactersLong! >/dev/null
# A disk with a loadable program on it. The program is assembled here rather than kept as
# bytes, so that what gets loaded is always built from the source beside it.
"$TOOL" format "$DISKS/load.img" 64 2 >/dev/null
"$ROOT/Assembler" "$ROOT/Programs/loadable/hello.asm" -o "$WORK/hello.bin" >/dev/null
python3 "$ROOT/Source/DiskTool/wrap.py" "$WORK/hello.bin" "$WORK/hello.sbx" 0x2000 0x1000 0x2000 >/dev/null
"$TOOL" put "$DISKS/load.img" "$WORK/hello.sbx" >/dev/null
# A disk of its own for the writing test, with one file already on it so that what it
# writes has to be placed somewhere that does not tread on what is there.
"$TOOL" format "$DISKS/write.img" 32 1 >/dev/null
printf 'already here' > here.txt
"$TOOL" put "$DISKS/write.img" here.txt >/dev/null
+32 -1
View File
@@ -4,7 +4,7 @@
# One test per line, fields separated by '|'. Blank lines and lines starting # One test per line, fields separated by '|'. Blank lines and lines starting
# with '#' are ignored. # with '#' are ignored.
# #
# name | source | mode | stdin | limit # name | source | mode | stdin | limit | disk
# #
# source is relative to Programs/. Everything assembles from there with # source is relative to Programs/. Everything assembles from there with
# Libraries/ on the include path, and the binary is written into Tests/build. # Libraries/ on the include path, and the binary is written into Tests/build.
@@ -18,6 +18,14 @@
# limit is a cycle count, for programs that never halt on their own. It is passed # limit is a cycle count, for programs that never halt on their own. It is passed
# to the emulator as --cycles, which bounds the run by cycles rather than by wall # to the emulator as --cycles, which bounds the run by cycles rather than by wall
# clock time and so keeps the recorded output identical from one run to the next. # clock time and so keeps the recorded output identical from one run to the next.
#
# disk names a disk image to attach, made fresh inside Tests/build for every run so that
# nothing a test writes can be seen by the next one. Leave it off for a machine with no
# disk, which is most of them. A trailing :ro attaches it write protected.
#
# A disk name with a directory in it, such as disks/sbfs.img, is one of the images that
# Tests/makedisks.sh builds with SplitDisk before the run. Those are used as they stand,
# so a test can read a filesystem written by the other implementation of the format.
# Every program is run with --fast, since the emulated cycle rate has no bearing # Every program is run with --fast, since the emulated cycle rate has no bearing
# on what a program prints. # on what a program prints.
@@ -43,6 +51,11 @@ pointerTableTest | testPrograms/pointerTableTest.asm | run | -
staticTableTest | testPrograms/staticTableTest.asm | run | - | - staticTableTest | testPrograms/staticTableTest.asm | run | - | -
dispatchTest | testPrograms/dispatchTest.asm | run | - | - dispatchTest | testPrograms/dispatchTest.asm | run | - | -
# ---- Branching both ways round ----
# Each of the four conditions is checked taken and not taken, so a branch that always
# went the same way would be caught rather than looking right half the time.
branchTest | testPrograms/branchTest.asm | run | - | -
# ---- Moving an ALU result back into an operand register ---- # ---- Moving an ALU result back into an operand register ----
moveQTest | testPrograms/moveQTest.asm | run | - | - moveQTest | testPrograms/moveQTest.asm | run | - | -
@@ -53,6 +66,24 @@ controllerReadTest | testPrograms/controllerReadTest.asm | run | -
controllerWriteTest | testPrograms/controllerWriteTest.asm | run | - | - controllerWriteTest | testPrograms/controllerWriteTest.asm | run | - | -
# Block transfers: between banks, within one, overlapping, and one that is refused. # Block transfers: between banks, within one, overlapping, and one that is refused.
blitTest | testPrograms/blitTest.asm | run | - | - blitTest | testPrograms/blitTest.asm | run | - | -
# Reading a filesystem that the host tool wrote. The two are separate implementations of
# one written format, so this is where any drift between them would show.
sbfsReadTest | testPrograms/sbfsReadTest.asm | run | - | - | disks/sbfs.img
# Writing a filesystem, then reading back what was written. The disk starts with a file
# on it, so allocation has to find room rather than start at the beginning.
sbfsWriteTest | testPrograms/sbfsWriteTest.asm | run | - | - | disks/write.img
# Loading a program off a disk and running it. Everything below this line existed before
# the loader did; the only new part is the sixteen bytes on the front of a loadable
# program saying where it goes.
loader | loader.asm | run | - | - | disks/load.img
# Storage. The image is made fresh for each run, so block 3 starts as zeroes.
diskTest | testPrograms/diskTest.asm | run | - | - | disk.img
# The same disk attached write protected, so the device bars writes rather than software.
diskProtectTest | testPrograms/diskProtectTest.asm | run | - | - | protected.img:ro
# A program that loads a program: blits code into Program Memory, installs a vector at # A program that loads a program: blits code into Program Memory, installs a vector at
# run time, and calls it. If the vector install ever silently failed, the SWI would fault # run time, and calls it. If the vector install ever silently failed, the SWI would fault
# with "no handler" rather than printing, so this test cannot pass by accident. # with "no handler" rather than printing, so this test cannot pass by accident.
+27 -2
View File
@@ -54,6 +54,12 @@ PROGRAMS="$ROOT/Programs"
rm -rf "$BUILD" rm -rf "$BUILD"
mkdir -p "$BUILD" "$EXPECTED" mkdir -p "$BUILD" "$EXPECTED"
# Disk images the tests read from are built here, with the host tool, before anything
# runs. The build directory is thrown away above, so they are always freshly made.
if [ -x "$TESTS/makedisks.sh" ]; then
"$TESTS/makedisks.sh" "$BUILD" || { echo "Couldn't build the test disks."; exit 1; }
fi
PASS=0 PASS=0
FAIL=0 FAIL=0
BLESSED=0 BLESSED=0
@@ -116,13 +122,14 @@ assemble() {
return 1 return 1
} }
while IFS='|' read -r name src mode stdin limit; do while IFS='|' read -r name src mode stdin limit disk; do
name="$(trim "$name")" name="$(trim "$name")"
[ -z "$name" ] && continue [ -z "$name" ] && continue
case "$name" in \#*) continue ;; esac case "$name" in \#*) continue ;; esac
src="$(trim "$src")" src="$(trim "$src")"
mode="$(trim "$mode")"; stdin="$(trim "$stdin")" mode="$(trim "$mode")"; stdin="$(trim "$stdin")"
limit="$(trim "$limit")" limit="$(trim "$limit")"; disk="$(trim "$disk")"
[ -z "$disk" ] && disk="-"
wanted "$name" || continue wanted "$name" || continue
@@ -166,6 +173,24 @@ while IFS='|' read -r name src mode stdin limit; do
# bounds them by cycle count rather than by wall clock. # bounds them by cycle count rather than by wall clock.
EMUARGS=(--fast) EMUARGS=(--fast)
[ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit") [ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit")
# A disk starts fresh for every run, so a test cannot pass because of what a
# previous one left lying on it. The emulator makes the image if it is
# missing, which is what removing it first arranges for.
if [ "$disk" != "-" ]; then
# A trailing :ro attaches the image write protected, so that a test can
# check the device bars writes rather than the filesystem asking nicely.
DISKFILE="${disk%:ro}"
# A name with a directory in it is one of the images makedisks.sh built,
# and is used as it stands. A bare name is scratch: it is removed first so
# that nothing a test writes can be seen by the next one, and the emulator
# makes a blank image in its place.
case "$DISKFILE" in
*/*) ;;
*) rm -f "$BUILD/$DISKFILE" ;;
esac
EMUARGS+=(--disk "$BUILD/$DISKFILE")
[ "$disk" != "$DISKFILE" ] && EMUARGS+=(--write-protect)
fi
timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1 timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1
STATUS=$? STATUS=$?
if [ "$STATUS" -eq 124 ]; then if [ "$STATUS" -eq 124 ]; then
+22 -5
View File
@@ -20,21 +20,25 @@ POSIXFLAGS = -D_POSIX_C_SOURCE=200809L
# Directories # Directories
SRC_DIR_EMU = Source/Emulator SRC_DIR_EMU = Source/Emulator
SRC_DIR_ASM = Source/Assembler SRC_DIR_ASM = Source/Assembler
SRC_DIR_DSK = Source/DiskTool
OBJ_DIR = Object OBJ_DIR = Object
# Source files # Source files
EMU_SRCS = emulator.c io.c controller.c utility.c cpu.c bootstrap.c assembly.c EMU_SRCS = emulator.c io.c controller.c utility.c cpu.c bootstrap.c assembly.c
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
DSK_SRCS = SplitDisk.c
EMU_OBJS = $(EMU_SRCS:%.c=$(OBJ_DIR)/%.o) EMU_OBJS = $(EMU_SRCS:%.c=$(OBJ_DIR)/%.o)
ASM_OBJS = $(ASM_SRCS:%.c=$(OBJ_DIR)/%.o) ASM_OBJS = $(ASM_SRCS:%.c=$(OBJ_DIR)/%.o)
DSK_OBJS = $(DSK_SRCS:%.c=$(OBJ_DIR)/%.o)
# Output binary names # Output binary names
EMU_TARGET = SplitBit EMU_TARGET = SplitBit
ASM_TARGET = Assembler ASM_TARGET = Assembler
DSK_TARGET = SplitDisk
# Default target: build both emulator and assembler # Default target: build both emulator and assembler
all: $(EMU_TARGET) $(ASM_TARGET) all: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
# Emulator binary # Emulator binary
$(EMU_TARGET): $(EMU_OBJS) $(EMU_TARGET): $(EMU_OBJS)
@@ -49,17 +53,30 @@ $(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c
mkdir -p $(OBJ_DIR) mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@ $(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Disk tool binary
$(DSK_TARGET): $(DSK_OBJS)
$(CC) $(CFLAGS) -o $(DSK_TARGET) $(DSK_OBJS)
# Compile disk tool source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_DSK)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Compile assembler source files to object files # Compile assembler source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c $(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
mkdir -p $(OBJ_DIR) mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@ $(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Pull in the header dependencies written out by the compiler above. # Pull in the header dependencies written out by the compiler above.
-include $(EMU_OBJS:.o=.d) $(ASM_OBJS:.o=.d) -include $(EMU_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d)
# Run the test suite against the programs in Programs/ # Run the test suite against the programs in Programs/
test: $(EMU_TARGET) $(ASM_TARGET) test: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
@./Tests/run.sh @./Tests/run.sh
@echo
@./Tests/disk.sh
@echo
@./Tests/docs.sh
# Rebuild both tools with the address and undefined behaviour sanitizers and run # Rebuild both tools with the address and undefined behaviour sanitizers and run
# the test suite under them. Slower than 'make test', and worth running before a # the test suite under them. Slower than 'make test', and worth running before a
@@ -92,10 +109,10 @@ bless: $(EMU_TARGET) $(ASM_TARGET)
clean: clean:
rm -rf $(OBJ_DIR) rm -rf $(OBJ_DIR)
rm -rf Tests/build rm -rf Tests/build
rm -f $(EMU_TARGET) $(ASM_TARGET) rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
# Install compiled binaries # Install compiled binaries
install: $(EMU_TARGET) $(ASM_TARGET) install: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
mkdir -p "$(PREFIX)/bin" mkdir -p "$(PREFIX)/bin"
install -m 755 $^ "$(PREFIX)/bin/" install -m 755 $^ "$(PREFIX)/bin/"