Block device peripheral and SBFS file system implemented.
This commit is contained in:
@@ -3,6 +3,7 @@ Programs/build/
|
|||||||
Object/
|
Object/
|
||||||
Assembler
|
Assembler
|
||||||
SplitBit
|
SplitBit
|
||||||
|
SplitDisk
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
resume
|
resume
|
||||||
codexResume.sh
|
codexResume.sh
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+58
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,11 +178,18 @@ 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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
QAB C qab c
|
||||||
|
9876543210
|
||||||
|
Execution halted after 176 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
04
|
||||||
|
04
|
||||||
|
06
|
||||||
|
Execution halted after 114 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
00
|
||||||
|
from the disk
|
||||||
|
02
|
||||||
|
Execution halted after 245 cycles.
|
||||||
|
[exit 0]
|
||||||
@@ -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]
|
||||||
@@ -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]
|
||||||
@@ -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]
|
||||||
Executable
+56
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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/"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user