A breakpoint that shows every register as the program had them, waits for a key, and carries on. NOTHING IS OVERWRITTEN, and that is the design rather than a shortcut. A breakpoint poked into a running program has to replace an instruction, and putting that instruction back in order to continue is the same act as disarming the breakpoint; firing a second time would mean stepping over the restored instruction and putting the breakpoint back behind it, and this machine cannot step a single instruction. SWI is two bytes, dispatches through a vector, and its frame already holds the address after it, so RETI resumes at the next instruction with nothing to restore and nothing to re-arm. It fires every time it is reached. The price is that a breakpoint is part of the program: a build with them in has different addresses from a build without. That is the bargain every machine with a break instruction makes. Every value shown comes out of the frame rather than the registers, because by the time the handler runs the registers are the handler's. Apps/Break.asm stops twice so that the second stop is checked as well as the first. Also here, found by the test that came with it: the monitor's s wrote into whichever bank was selected, and bank 2 is the controller's own table, published read only. Writing to it was refused, and a refusal nobody catches stops the machine - so selecting the bank table to look at it and then typing s killed the session. bankPresent now keeps the whole flags byte and s declines. The recorded output of cosmosMonitor had contained that crash, having been blessed without being read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2174 lines
46 KiB
NASM
2174 lines
46 KiB
NASM
; cosmos.asm
|
|
; CosmOS, and the shell that is most of it.
|
|
;
|
|
; The machine boots into this. It registers what the hardware brought, mounts whatever
|
|
; disk is attached, and then reads lines and does what they say until there is no more
|
|
; typing to be had.
|
|
;
|
|
; ---- Where things live ----
|
|
;
|
|
; The system keeps to the bottom of both memories, and everything above is for whatever
|
|
; it is running:
|
|
;
|
|
; Program Memory 0x0000 - 0x1FFF the system
|
|
; 0x2000 - a loaded program's code
|
|
; Data Memory 0x0000 - 0x0FFF the system
|
|
; 0x1000 - a loaded program's data
|
|
;
|
|
; Nothing enforces that. Nothing can: the fence guards a range, and this is a convention
|
|
; about which range belongs to whom rather than a rule about what may be touched. The
|
|
; assembler prints both segment sizes, and they are what to watch.
|
|
;
|
|
; A program is staged at 0x8000 while it is being loaded, which is inside the region a
|
|
; loaded program will own. That is safe because nothing is running during a load, and it
|
|
; is where a big program can be read without the system reserving the room for good.
|
|
;
|
|
; ---- What it can do ----
|
|
;
|
|
; dir List what is on the disk.
|
|
; load Read a program off the disk and put it where it asks to go.
|
|
; run Start the program that was loaded.
|
|
; help Say what these are.
|
|
; exit Stop.
|
|
;
|
|
; dump is next. The dispatch below is a chain of comparisons, which is the right shape for
|
|
; five commands and the wrong shape for twenty; when it grows, the table that
|
|
; dispatchTest.asm demonstrates is where it should go.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Include console.asm
|
|
#Include text.asm
|
|
#Include sbfs.asm
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
boot:
|
|
SETD.0 Banner
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; Find out whether there is a filesystem to talk to. Doing this once at boot rather than
|
|
; once per command means a disk swapped underneath us is not noticed, which is honest
|
|
; for a machine whose disk is a file named on the command line.
|
|
CALL sbfsMount
|
|
SETD.0 DiskReady
|
|
BNQ bootNoDisk
|
|
INIA 0x01
|
|
STA.0
|
|
BRI prompt
|
|
bootNoDisk:
|
|
RSTA
|
|
STA.0
|
|
SETD.0 NoDisk
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; ---- The loop ----
|
|
|
|
; ---- The loop ----
|
|
;
|
|
; The shell has two modes and one prompt that says which. Ordinary mode runs programs;
|
|
; monitor mode also looks at memory, changes it, and jumps into it.
|
|
;
|
|
; THE MODE IS A VARIABLE RATHER THAN A SECOND LOOP, and that is what makes it persistent
|
|
; without anything having to remember it. Every way back here goes through this one place,
|
|
; INCLUDING A PROGRAM GIVING THE MACHINE BACK - so jumping to an address, letting it run,
|
|
; and having it exit puts you back at the monitor prompt you left from, rather than at the
|
|
; shell. Only saying so leaves the monitor, or a program breaking the machine badly enough
|
|
; to need starting again.
|
|
prompt:
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA promptPlain
|
|
SETD.0 MonitorPrompt
|
|
BRI promptSay
|
|
promptPlain:
|
|
SETD.0 PromptText
|
|
promptSay:
|
|
CALL printString
|
|
|
|
SETD.0 CommandLine
|
|
INIB 0d63
|
|
CALL readLine
|
|
|
|
; Running out of typing is how this ends. It is not the same as an empty line, which is
|
|
; just somebody pressing return, and the shell should sit there when that happens.
|
|
SETD.0 ConsoleEndOfInput
|
|
LDA.0
|
|
BNA quitRanOut
|
|
|
|
SETD.0 CommandLine
|
|
CALL textSplit
|
|
|
|
; An empty line asks for nothing.
|
|
SETD.0 CommandLine
|
|
LDA.0
|
|
BRA prompt
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DirName
|
|
CALL textSame
|
|
BRQ doDir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 LoadName
|
|
CALL textSame
|
|
BRQ doLoad
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RunName
|
|
CALL textSame
|
|
BRQ doRun
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DeleteName
|
|
CALL textSame
|
|
BRQ doDelete
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RenameName
|
|
CALL textSame
|
|
BRQ doRename
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 HelpName
|
|
CALL textSame
|
|
BRQ doHelp
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ExitName
|
|
CALL textSame
|
|
BRQ doExit
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 MonitorName
|
|
CALL textSame
|
|
BRQ doMonitor
|
|
|
|
; The monitor's own commands, which only answer when the monitor is on. They are single
|
|
; letters because they are typed constantly and because the prompt has already said which
|
|
; mode you are in; the plain shell keeps its words and stays plain.
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA promptUnknown
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ExamineName
|
|
CALL textSame
|
|
BRQ doExamine
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DisName
|
|
CALL textSame
|
|
BRQ doDisassemble
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 SetName
|
|
CALL textSame
|
|
BRQ doSet
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 BankName
|
|
CALL textSame
|
|
BRQ doBank
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 GoName
|
|
CALL textSame
|
|
BRQ doGo
|
|
|
|
promptUnknown:
|
|
; Nothing matched. Saying which word was not understood is worth the four instructions:
|
|
; it tells somebody who mistyped what they actually typed.
|
|
SETD.0 Unknown
|
|
CALL printString
|
|
SETD.0 CommandLine
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; Running out of console leaves the cursor part way along a line, because there was no
|
|
; return at the end to move it on. Somebody who typed "exit" has already pressed one, and
|
|
; a second would only leave a blank line behind.
|
|
; Leaving whatever you are in: the monitor if you are in it, and the machine if you are
|
|
; not. Two exits to stop from the monitor, which is what every nested prompt has ever asked
|
|
; for and reads the right way round.
|
|
doExit:
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA quit
|
|
RSTA
|
|
STA.0
|
|
BRI prompt
|
|
|
|
doMonitor:
|
|
INIA 0x01
|
|
SETD.0 Mode
|
|
STA.0
|
|
SETD.0 MonitorHelp
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
quitRanOut:
|
|
CALL newLine
|
|
quit:
|
|
SETD.0 Farewell
|
|
CALL printString
|
|
CALL newLine
|
|
HALT
|
|
|
|
; ---- dir ----
|
|
;
|
|
; Walks the directory and prints what is in it. A free entry in the middle of a directory
|
|
; is stepped over by the walk, so what comes out is the files and nothing else.
|
|
|
|
doDir:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA dirNoDisk
|
|
|
|
RSTA
|
|
SETD.0 DirSeen
|
|
STA.0
|
|
|
|
CALL sbfsFirst
|
|
BRI dirCheck
|
|
dirStep:
|
|
CALL sbfsNext
|
|
dirCheck:
|
|
BNQ dirDone
|
|
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
|
|
SETD.0 SbfsName
|
|
CALL printString
|
|
|
|
SETD.0 SbfsName
|
|
CALL nameWidth
|
|
MVQA
|
|
CALL printSpaces
|
|
|
|
; A file's length is its block count times 256 plus its tail, which is the block count
|
|
; in the high byte and the tail in the low one. Nothing has to multiply anything.
|
|
SETD.0 SbfsFileBlocks
|
|
DPUP.0 0d01
|
|
LDA.0
|
|
SETD.1 DirSize
|
|
STA.1
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
SETD.1 DirSize
|
|
INCD.1
|
|
STA.1
|
|
|
|
SETD.0 DirSize
|
|
CALL printWordDecimal
|
|
CALL newLine
|
|
BRI dirStep
|
|
|
|
dirDone:
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
CALL printByteDecimal
|
|
; One file is not one files. Cheap to get right and it reads as carelessness otherwise.
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
DECA
|
|
BRA dirOne
|
|
SETD.0 FilesText
|
|
BRI dirCount
|
|
dirOne:
|
|
SETD.0 FileText
|
|
dirCount:
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
dirNoDisk:
|
|
SETD.0 NoDisk
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; DP0 names a string. Q is how many spaces pad it out to twenty four columns. A name
|
|
; already that long gets one space, so that it cannot run into the number after it.
|
|
nameWidth:
|
|
INIA 0d24
|
|
SETD.1 WidthLeft
|
|
STA.1
|
|
widthLoop:
|
|
LDA.0
|
|
BRA widthDone
|
|
SETD.1 WidthLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRA widthFloor
|
|
INCD.0
|
|
BRI widthLoop
|
|
widthFloor:
|
|
INIA 0d1
|
|
SETD.1 WidthLeft
|
|
STA.1
|
|
widthDone:
|
|
SETD.1 WidthLeft
|
|
LDA.1
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- load ----
|
|
;
|
|
; Reads a program off the disk and puts it where its header asks to go. Nothing relocates
|
|
; anything: the addresses in the header are the ones the program was built for, and it
|
|
; would not work anywhere else.
|
|
;
|
|
; The whole file is staged at 0x8000 first and then blitted into place, because where the
|
|
; pieces belong is not known until the header has been read, and the header is in the file.
|
|
|
|
doLoad:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA loadNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA loadNothingNamed
|
|
|
|
CALL sbfsFind
|
|
BNQ loadMissing
|
|
|
|
SETD.1 0x80 0x00
|
|
CALL sbfsRead
|
|
BNQ loadUnreadable
|
|
|
|
; "SBEX", or this is not a program. Without this, loading a text file would put nonsense
|
|
; into Program Memory and then jump into the middle of it.
|
|
SETD.0 0x80 0x00
|
|
SETD.2 ExecMagic
|
|
INIA 0d4
|
|
SETD.1 LoadCount
|
|
STA.1
|
|
loadMagicLoop:
|
|
LDA.0
|
|
LDB.2
|
|
XOR
|
|
BNQ loadNotProgram
|
|
INCD.0
|
|
INCD.2
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA loadMagicLoop
|
|
|
|
; Version one is code and data. Version two also brings vectors, which is a thing a
|
|
; loader has to know how to do rather than a detail it can skip: a program whose handlers
|
|
; were quietly dropped would run and then go wrong somewhere with nothing to connect it
|
|
; back to here. Anything else is refused.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d04
|
|
LDA.0
|
|
SETD.1 LoadVersion
|
|
STA.1
|
|
INIB 0d1
|
|
XOR
|
|
BRQ loadVersionKnown
|
|
SETD.1 LoadVersion
|
|
LDA.1
|
|
INIB 0d2
|
|
XOR
|
|
BNQ loadWrongVersion
|
|
loadVersionKnown:
|
|
|
|
; The code. It comes from the staging area just past the sixteen byte header, and goes
|
|
; wherever the header says, in Program Memory, which the instruction set cannot write
|
|
; and the controller can.
|
|
INIA 0d1
|
|
OUTA 0xE0 ; SourceBank: Data Memory, where the file was staged.
|
|
INIA 0x80
|
|
OUTA 0xE1
|
|
INIA 0d16
|
|
OUTA 0xE2 ; 0x8010, the first byte after the header.
|
|
|
|
RSTA
|
|
OUTA 0xE3 ; DestBank: Program Memory.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d06
|
|
LDA.0
|
|
OUTA 0xE4
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE5
|
|
|
|
SETD.0 0x80 0x00
|
|
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 0x80 0x00
|
|
DPUP.0 0d12
|
|
LDA.0
|
|
OUTA 0xE4
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE5
|
|
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d14
|
|
LDA.0
|
|
OUTA 0xE6
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE7
|
|
|
|
INIA 0x01
|
|
OUTA 0xE8 ; Blit.
|
|
|
|
; ---- The vectors it brought ----
|
|
;
|
|
; Kept here rather than installed. A vector points into a program, so it has no business
|
|
; being in the table while that program is only loaded and not running: run puts them in
|
|
; and exit takes them out again, so the window they are live in is exactly the run.
|
|
; Keeping our own copy is also what lets a program be run more than once, since the
|
|
; staging area it came in on is fair game for the program's own use.
|
|
;
|
|
; Where to read them from is not worked out. The data blit left the controller's source
|
|
; address on the first byte after the data, which is where they are, so it is read back.
|
|
SETD.0 VectorSource
|
|
INA 0xE1
|
|
STA.0
|
|
INCD.0
|
|
INA 0xE2
|
|
STA.0
|
|
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d05
|
|
LDA.0
|
|
SETD.1 LoadedVectorCount
|
|
STA.1
|
|
BRA loadVectorsCopied
|
|
|
|
; More than there is room for is refused rather than half taken. Half a program's
|
|
; handlers is not a smaller version of that program.
|
|
INIB 0d17
|
|
CCF
|
|
SUB
|
|
BNC loadTooManyVectors
|
|
|
|
SETD.0 VectorSource
|
|
LDD.2.0 ; DP2 walks the entries where they are staged.
|
|
SETD.3 LoadedVectors ; DP3 walks our own copy of them.
|
|
SETD.1 LoadedVectorCount
|
|
LDA.1
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
|
|
loadVectorCopy:
|
|
; Four bytes: where it goes, then what goes there. The two bytes for what was there
|
|
; before are left alone until something is actually put in.
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
INCD.3
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA loadVectorCopy
|
|
|
|
loadVectorsCopied:
|
|
|
|
; Where it starts. Written out by hand rather than through a routine, because a routine
|
|
; could not hand two bytes back: CALL puts A, B and the first three pointers back the
|
|
; way it found them.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d08
|
|
LDA.0
|
|
SETD.1 LoadedEntry
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.0
|
|
STA.1
|
|
|
|
INIA 0x01
|
|
SETD.0 LoadedOk
|
|
STA.0
|
|
|
|
SETD.0 LoadedText
|
|
CALL printString
|
|
SETD.0 LoadedEntry
|
|
CALL printWordHex
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
loadNoDisk:
|
|
SETD.0 NoDisk
|
|
BRI loadComplain
|
|
loadNothingNamed:
|
|
SETD.0 LoadWhat
|
|
BRI loadComplain
|
|
loadTooManyVectors:
|
|
SETD.0 TooManyVectors
|
|
BRI loadComplain
|
|
loadMissing:
|
|
SETD.0 NoSuchFile
|
|
BRI loadComplain
|
|
loadUnreadable:
|
|
SETD.0 Unreadable
|
|
BRI loadComplain
|
|
loadNotProgram:
|
|
SETD.0 NotProgram
|
|
BRI loadComplain
|
|
loadWrongVersion:
|
|
SETD.0 WrongVersion
|
|
loadComplain:
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; ---- delete and rename ----
|
|
;
|
|
; The two things a disk needs that reading and writing do not provide, and the two that
|
|
; anything editing a document will want from the shell as well as from a program. Deleting
|
|
; frees an entry and its blocks; renaming changes twenty two bytes and moves nothing.
|
|
|
|
doDelete:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA deleteWhat
|
|
|
|
CALL sbfsDelete
|
|
BNQ deleteFailed
|
|
SETD.0 Deleted
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
deleteWhat:
|
|
SETD.0 DeleteWhat
|
|
BRI fileComplain
|
|
deleteFailed:
|
|
SETD.0 NoSuchFile
|
|
BRI fileComplain
|
|
|
|
doRename:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA renameWhat
|
|
|
|
; Two names, so the rest of the line is split again. textSplit writes a zero over the
|
|
; space it cuts at, so what was one string becomes two without anything being copied.
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 RenameFrom
|
|
STD.0.1
|
|
CALL textSplit
|
|
|
|
SETD.1 TextRest
|
|
LDD.1.1
|
|
LDA.1
|
|
BRA renameWhat ; Only one name was given, and this needs both.
|
|
|
|
SETD.2 RenameFrom
|
|
LDD.0.2
|
|
CALL sbfsRename
|
|
BNQ renameFailed
|
|
SETD.0 Renamed
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
renameWhat:
|
|
SETD.0 RenameWhat
|
|
BRI fileComplain
|
|
renameFailed:
|
|
; Either there is no such file or the new name is already taken. Which of the two is not
|
|
; worth another message: both mean the disk does not have room for that name to move.
|
|
SETD.0 RenameNo
|
|
BRI fileComplain
|
|
|
|
fileNoDisk:
|
|
SETD.0 NoDisk
|
|
fileComplain:
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; ---- run ----
|
|
;
|
|
; Hands the machine to whatever was loaded. Where the Stack is now is written down first,
|
|
; because the program is not going to unwind anything it pushes and the exit handler has
|
|
; to be able to put the Stack back.
|
|
|
|
doRun:
|
|
SETD.0 LoadedOk
|
|
LDA.0
|
|
BRA runNothing
|
|
|
|
MVSD.0
|
|
SETD.1 SystemStack
|
|
STD.0.1
|
|
|
|
; Whatever followed the word "run" is kept where the program can ask for it. Copied
|
|
; rather than pointed at, because what it is pointing at is the line the shell typed
|
|
; into, and a program is entitled to outlive the shell's opinion of that.
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 RunArgument
|
|
INIB 0d64
|
|
CALL copyText
|
|
|
|
CALL installVectors
|
|
|
|
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
|
|
; in, because it is the pointer nothing puts back.
|
|
SETD.1 LoadedEntry
|
|
LDD.3.1
|
|
BRD.3
|
|
|
|
runNothing:
|
|
SETD.0 NothingLoaded
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; DP0 is a string, DP1 is where it should go, and B is how much room there is counting
|
|
; the zero on the end. What does not fit is left behind, and what is written is a string
|
|
; either way.
|
|
copyText:
|
|
BRB copyTextDone ; No room at all, so nothing is written, not even the zero.
|
|
copyTextLoop:
|
|
DECB
|
|
BRB copyTextEnd ; Only room for the terminator now.
|
|
LDA.0
|
|
STA.1
|
|
BRA copyTextDone
|
|
INCD.0
|
|
INCD.1
|
|
BRI copyTextLoop
|
|
copyTextEnd:
|
|
RSTA
|
|
STA.1
|
|
copyTextDone:
|
|
RET
|
|
|
|
; ---- Putting a program's vectors in, and taking them out again ----
|
|
;
|
|
; The vector table lives in Program Memory, which no instruction can write, so both of
|
|
; these go through the memory controller. Port 0xE9 reads a byte from the source and writes
|
|
; a byte to the destination, stepping the address on either way, so a two byte entry is two
|
|
; reads or two writes and no address arithmetic in between.
|
|
;
|
|
; What was in the slot is kept before anything replaces it, and put back afterwards, rather
|
|
; than the slot being cleared. Clearing would be wrong wherever a program has installed a
|
|
; handler over one the system was already using: the program is allowed to do that, and
|
|
; when it goes, what it covered up has to come back rather than becoming a hole.
|
|
|
|
installVectors:
|
|
SETD.0 LoadedVectorCount
|
|
LDA.0
|
|
BRA installDone
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
SETD.3 LoadedVectors
|
|
|
|
installOne:
|
|
; DP3 walks one six byte entry: where it goes, what goes there, and room for what was
|
|
; there before. Reading and writing the same slot, so the controller is pointed at it
|
|
; from both ends at once and the address is only worked out once.
|
|
RSTA
|
|
OUTA 0xE0 ; SourceBank: Program Memory.
|
|
OUTA 0xE3 ; DestBank: the same.
|
|
LDA.3
|
|
OUTA 0xE1
|
|
OUTA 0xE4
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE2
|
|
OUTA 0xE5
|
|
INCD.3 ; On the handler.
|
|
|
|
; What is there now, before anything replaces it.
|
|
INA 0xE9
|
|
PSHA
|
|
INA 0xE9
|
|
PSHA
|
|
|
|
; And the handler in its place.
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3 ; On the two bytes kept for what was there before.
|
|
|
|
; The Stack gives them back in the reverse of the order they went on, so the low byte
|
|
; arrives first and is written to the second of the two. Getting this the natural way
|
|
; round instead put the low byte where the high one goes and the high byte over the
|
|
; handler, which the first run of a program survives - the table is already written by
|
|
; then - and the second run does not.
|
|
POPA
|
|
INCD.3
|
|
STA.3
|
|
DECD.3
|
|
POPA
|
|
STA.3
|
|
INCD.3
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA installOne
|
|
installDone:
|
|
RET
|
|
|
|
removeVectors:
|
|
SETD.0 LoadedVectorCount
|
|
LDA.0
|
|
BRA removeDone
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
SETD.3 LoadedVectors
|
|
|
|
removeOne:
|
|
RSTA
|
|
OUTA 0xE3 ; DestBank: Program Memory.
|
|
LDA.3
|
|
OUTA 0xE4
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE5
|
|
INCD.3
|
|
INCD.3
|
|
INCD.3 ; Past the handler, to what was underneath it.
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA removeOne
|
|
removeDone:
|
|
RET
|
|
|
|
; ---- The services ----
|
|
;
|
|
; These are what a loaded program is allowed to ask for. The names and their numbers come
|
|
; from services.asm, which the programs include as well, so neither side writes a number
|
|
; down and the two cannot disagree about them.
|
|
;
|
|
; A handler arrives with the caller's registers exactly as they were: an interrupt frame
|
|
; is pushed, not cleared. So the pointer a program put in DP0 is still there to be used.
|
|
|
|
; The disk finishing, acknowledged and ignored.
|
|
;
|
|
; The system drives the disk by asking its status port and waiting, so it has no use for
|
|
; the line. But the disk raises one after every operation whether anybody wants it or not,
|
|
; and a line goes on waiting while the Interrupt Flag is down rather than being lost. The
|
|
; shell keeps the flag down, so the line from the last disk read was still standing when
|
|
; the first program to enable interrupts ran, and it arrived there - a fault, in a program
|
|
; that had never heard of the disk, blamed on the innocent instruction that let it through.
|
|
;
|
|
; Answering a line is what takes it down, so this is one instruction and that is the point.
|
|
diskDone:
|
|
RETI
|
|
|
|
handlePrintString:
|
|
CALL printString
|
|
RETI
|
|
|
|
handleReadLine:
|
|
CALL readLine
|
|
|
|
; readLine works out how long the line was, and RETI would throw that away: it restores
|
|
; every register from the frame, which is exactly what makes an interrupt safe to arrive
|
|
; unannounced and exactly what stops a service answering. So the answer is written into
|
|
; the frame, over the saved Q, and RETI puts it back as though the caller had computed it.
|
|
;
|
|
; This has to be here rather than in a routine, because the offset is from where the
|
|
; Stack Pointer is now and a CALL moves it by ten.
|
|
MVQA
|
|
MVSD.1
|
|
DPUP.1 0d02
|
|
STA.1
|
|
RETI
|
|
|
|
; What the program was asked to work on. DP0 says where to put it and B how much room
|
|
; there is, counting the zero on the end, which is the same bargain readLine offers.
|
|
;
|
|
; Being asked for rather than left at an agreed address is deliberate. The two sides of
|
|
; this already have to agree on a vector number and nothing else, and that number is
|
|
; written down once in services.asm; an address would be a second thing to agree about, in
|
|
; a memory map that is a convention rather than anything enforced.
|
|
handleArgument:
|
|
PSHD.0
|
|
POPD.1
|
|
SETD.0 RunArgument
|
|
CALL copyText
|
|
RETI
|
|
|
|
; ---- The disk, on a program's behalf ----
|
|
;
|
|
; A loaded program that wanted a file used to include the whole filesystem, so it carried a
|
|
; private copy of code the system already has running, and mounted a disk that was already
|
|
; mounted. These are that code, reachable through a number instead.
|
|
;
|
|
; Every one of them answers in Q, and the answer is written into the frame over the saved
|
|
; register, because RETI puts every register back and would otherwise throw it away. That
|
|
; has to be done here rather than in a routine of its own: the offsets are from where the
|
|
; Stack Pointer is, and a CALL moves it by ten.
|
|
;
|
|
; A machine with no disk answers no to all of them rather than going ahead and finding out,
|
|
; because sbfs on a disk that was never mounted is reading whatever bank 3 happens to be.
|
|
|
|
; DP0 names the file, DP1 says where to put it. Q is zero if it read, and DP3 comes back
|
|
; holding how many bytes there were.
|
|
handleFileRead:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA fileReadNo
|
|
|
|
CALL sbfsFind
|
|
BNQ fileReadNo
|
|
|
|
; A file of 256 blocks is 64K, which will not fit in Data Memory and will not fit in the
|
|
; pointer that says how long it is either. Refused, rather than read as much of as fits:
|
|
; a length that lies is worse than a file that will not open.
|
|
SETD.2 SbfsFileBlocks
|
|
LDA.2
|
|
BNA fileReadNo
|
|
|
|
CALL sbfsRead
|
|
BNQ fileReadNo
|
|
|
|
; How long it is: the block count is the high byte of that and the tail is the low one,
|
|
; which is how a size is put together everywhere on this disk.
|
|
SETD.2 SbfsFileBlocks
|
|
INCD.2
|
|
LDA.2
|
|
SETD.2 SbfsFileTail
|
|
LDB.2
|
|
|
|
MVSD.2
|
|
DPUP.2 0d05 ; The saved DP3, high byte first.
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
|
|
MVSD.2
|
|
DPUP.2 0d02 ; And the saved Q.
|
|
RSTA
|
|
STA.2
|
|
RETI
|
|
|
|
fileReadNo:
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
INIA 0d1
|
|
STA.2
|
|
RETI
|
|
|
|
; DP0 names the file, DP1 is the bytes, and A and B together are how many. Q is zero if it
|
|
; saved. Whether it was there before makes no difference, which is what saving means.
|
|
handleFileSave:
|
|
SETD.2 DiskReady
|
|
PSHA
|
|
LDA.2
|
|
BRA fileSaveNoDisk
|
|
POPA
|
|
|
|
; Blocks are the high half of the count and the tail is the low half.
|
|
SETD.2 SbfsFileBlocks
|
|
PSHA
|
|
RSTA
|
|
STA.2 ; A whole file's block count fits in a byte, so this is zero.
|
|
INCD.2
|
|
POPA
|
|
STA.2
|
|
SETD.2 SbfsFileTail
|
|
STB.2
|
|
|
|
CALL sbfsSaveFile
|
|
MVQA
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
STA.2
|
|
RETI
|
|
|
|
fileSaveNoDisk:
|
|
POPA
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
INIA 0d1
|
|
STA.2
|
|
RETI
|
|
|
|
; DP0 names it. Q is zero if it went.
|
|
handleFileDelete:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA serviceNoDisk
|
|
CALL sbfsDelete
|
|
MVQA
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
STA.2
|
|
RETI
|
|
|
|
; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
|
|
handleFileRename:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA serviceNoDisk
|
|
CALL sbfsRename
|
|
MVQA
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
STA.2
|
|
RETI
|
|
|
|
serviceNoDisk:
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
INIA 0d1
|
|
STA.2
|
|
RETI
|
|
|
|
; A breakpoint. Shows every register as the interrupted program had them, waits for a key,
|
|
; and returns as though nothing happened.
|
|
;
|
|
; EVERY VALUE COMES OUT OF THE FRAME, not out of the registers, because by the time this
|
|
; runs the registers belong to the handler. The frame is what the program had, and RETI is
|
|
; going to give it all back, so what is shown is what will be resumed with.
|
|
;
|
|
; DP3 holds the frame throughout. It survives a CALL, and console.asm promises not to
|
|
; disturb it, which is what lets the printing routines be used between one field and the
|
|
; next. The Stack Pointer comes back to the same place after a balanced call, so the frame
|
|
; stays where it was found.
|
|
;
|
|
; +1 Status +2 Q +3 A +4 B +5 DP3 +7 DP2 +9 DP1 +11 DP0 +13 where it resumes
|
|
handleBreak:
|
|
MVSD.3
|
|
|
|
; Where it broke, which is two before where it resumes: the SWI and the vector it names.
|
|
SETD.0 BreakText
|
|
CALL printString
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d13
|
|
LDA.0
|
|
PSHA ; The high half, while the low one is worked on.
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d2
|
|
CCF
|
|
SUB ; Two back from where it resumes: the SWI and the vector it names.
|
|
MVQA
|
|
POPB
|
|
BRC breakBorrowed ; It borrowed, so the high half comes down by one.
|
|
BRI breakAddress
|
|
breakBorrowed:
|
|
DECB
|
|
breakAddress:
|
|
PSHA ; low
|
|
PSHB ; high
|
|
POPA
|
|
CALL printByteHex
|
|
POPA
|
|
CALL printByteHex
|
|
CALL newLine
|
|
|
|
SETD.0 ARegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d3
|
|
CALL breakByte
|
|
SETD.0 BRegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d4
|
|
CALL breakByte
|
|
SETD.0 QRegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d2
|
|
CALL breakByte
|
|
SETD.0 SRegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d1
|
|
CALL breakByte
|
|
CALL newLine
|
|
|
|
SETD.0 DP0Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d11
|
|
CALL breakWord
|
|
SETD.0 DP1Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d9
|
|
CALL breakWord
|
|
SETD.0 DP2Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d7
|
|
CALL breakWord
|
|
SETD.0 DP3Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d5
|
|
CALL breakWord
|
|
CALL newLine
|
|
|
|
; Anything typed carries on. Reading the data port waits however the console is set, which
|
|
; is one key if the program asked for key mode and a whole line if it did not - and either
|
|
; way it is the program's own console being borrowed for a moment.
|
|
SETD.0 ResumeText
|
|
CALL printString
|
|
INA 0x00
|
|
CALL newLine
|
|
RETI
|
|
|
|
; DP0 names a field and DP1 points at it in the frame. The caller does the stepping, with
|
|
; DPUP and a number written into the program, because a routine cannot hand a pointer back:
|
|
; CALL saves DP0 to DP2 and RET puts them back, so a walk done in here would be undone on
|
|
; the way out. Written that way first, and every field showed the frame's first byte.
|
|
breakByte:
|
|
CALL printString
|
|
LDA.1
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
RET
|
|
|
|
; The same for the two byte fields, most significant first the way the frame holds them.
|
|
breakWord:
|
|
CALL printString
|
|
LDA.1
|
|
CALL printByteHex
|
|
INCD.1
|
|
LDA.1
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
RET
|
|
|
|
; A and B together are a number. Prints it in decimal without leading zeroes, which covers
|
|
; a line number and a byte count both, so there is no need for one service each.
|
|
handlePrintNumber:
|
|
SETD.0 PrintNumber
|
|
STA.0
|
|
INCD.0
|
|
STB.0
|
|
SETD.0 PrintNumber
|
|
CALL printWordDecimal
|
|
RETI
|
|
|
|
; Giving the machine back. This is the one place MVDS earns its keep. The program's Stack,
|
|
; and the frame this very interrupt arrived on, are both abandoned where they lie, because
|
|
; nothing is going to return through either of them.
|
|
;
|
|
; Which is exactly why this cannot RETI. Its return address is on the Stack it just walked
|
|
; away from, so it branches to the prompt instead.
|
|
handleExit:
|
|
SETD.1 SystemStack
|
|
LDD.0.1
|
|
MVDS.0
|
|
|
|
; Whatever the program put in the vector table comes out again. A vector points into the
|
|
; program that supplied it, and the program is gone, so anything left installed would aim
|
|
; an interrupt at whatever those addresses hold next.
|
|
CALL removeVectors
|
|
|
|
; The console goes back to how the shell wants it, whatever the program left it in: line
|
|
; mode, and not interrupting. A program that wanted either is expected to put it back
|
|
; itself, but one that stopped early, or forgot, would otherwise hand back a shell with
|
|
; no echo and no backspace, or one being interrupted about keys it is reading anyway.
|
|
; Zero is both bits, so this undoes everything the control port can be asked for, and
|
|
; asking for what is already the case costs a byte out of a port and does nothing. That
|
|
; is the right price for not having to know.
|
|
RSTA
|
|
OUTA 0x02
|
|
|
|
SETD.0 Finished
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; ---- dump ----
|
|
;
|
|
; dump Sixty four more bytes, carrying on from the last one.
|
|
; dump <where> From the start of that bank.
|
|
; dump <where> <addr> From there.
|
|
;
|
|
; <where> is program, data, or a bank number in hexadecimal. That the CPU cannot read
|
|
; Program Memory and this can is the whole point: the instruction set has no way to look
|
|
; at itself, and the controller does, so a monitor is possible at all only through it.
|
|
|
|
; b <program|data|number>
|
|
;
|
|
; The two banks that always exist have names, because "program" is what somebody means and
|
|
; 0 is only what the machine calls it. Anything else is a number, and has to be one that is
|
|
; really there.
|
|
doBank:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA bankWhat
|
|
|
|
SETD.1 ProgramWord
|
|
CALL textSame
|
|
BRQ bankProgram
|
|
SETD.1 DataWord
|
|
CALL textSame
|
|
BRQ bankData
|
|
|
|
CALL textHexWord
|
|
BNQ bankWhat
|
|
SETD.0 TextValue
|
|
INCD.0
|
|
LDA.0
|
|
BRI bankSet
|
|
bankProgram:
|
|
RSTA
|
|
BRI bankSet
|
|
bankData:
|
|
INIA 0d1
|
|
bankSet:
|
|
; The old one is kept, because asking about a bank means writing it down first - and if
|
|
; it turns out not to exist, being left pointed at it would fault on the very next look.
|
|
SETD.0 DumpBank
|
|
LDB.0
|
|
SETD.1 BankWas
|
|
STB.1
|
|
SETD.0 DumpBank
|
|
STA.0
|
|
|
|
; Naming a bank puts the cursor at the start of it, which is the only answer that does
|
|
; not depend on what was asked for last time.
|
|
CALL bankPresent
|
|
BRQ bankNotThere
|
|
|
|
RSTA
|
|
SETD.0 DumpAt
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
|
|
SETD.0 BankIs
|
|
CALL printString
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
bankNotThere:
|
|
SETD.0 BankWas
|
|
LDA.0
|
|
SETD.1 DumpBank
|
|
STA.1 ; Back where it was, which is somewhere that exists.
|
|
SETD.0 NoSuchBank
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
bankWhat:
|
|
SETD.0 BankUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; x [address] - sixty four bytes. d [address] - eight instructions. Without an address
|
|
; either carries on from where the last one stopped, so reading through memory is one
|
|
; letter at a time and the two share a place in it.
|
|
doExamine:
|
|
RSTA
|
|
SETD.0 ShowAsCode
|
|
STA.0
|
|
BRI showAt
|
|
|
|
doDisassemble:
|
|
INIA 0x01
|
|
SETD.0 ShowAsCode
|
|
STA.0
|
|
|
|
showAt:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA dumpGo ; Nothing said, so carry on from where the last one stopped.
|
|
CALL textHexWord
|
|
BNQ dumpBadWhere
|
|
SETD.0 TextValue
|
|
LDA.0
|
|
SETD.1 DumpAt
|
|
STA.1
|
|
SETD.0 TextValue
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 DumpAt
|
|
INCD.1
|
|
STA.1
|
|
|
|
dumpCheckBank:
|
|
CALL bankPresent
|
|
BRQ dumpNoBank
|
|
|
|
dumpGo:
|
|
SETD.0 ShowAsCode
|
|
LDA.0
|
|
BNA disassembleGo
|
|
|
|
INIA 0d4
|
|
SETD.0 DumpRows
|
|
STA.0
|
|
|
|
dumpRow:
|
|
SETD.0 DumpAt
|
|
CALL printWordHex
|
|
INIA 0d2
|
|
CALL printSpaces
|
|
|
|
; Point the controller at the row. Reading the Data port takes a byte and steps the
|
|
; source on, so the whole row is one instruction repeated.
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
OUTA 0xE0
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
OUTA 0xE1
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE2
|
|
|
|
; Sixteen bytes, kept as they go past so that they can be shown twice.
|
|
INIA 0d16
|
|
SETD.0 DumpCount
|
|
STA.0
|
|
SETD.1 DumpBytes
|
|
dumpByte:
|
|
INA 0xE9
|
|
STA.1
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INCD.1
|
|
SETD.0 DumpCount
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA dumpByte
|
|
|
|
; The same sixteen again, as characters. Anything that is not printable shows as a dot,
|
|
; because a control character sent to the console would move the cursor and ruin the
|
|
; shape of the dump.
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INIA 0d16
|
|
SETD.0 DumpCount
|
|
STA.0
|
|
SETD.1 DumpBytes
|
|
dumpChar:
|
|
LDA.1
|
|
INIB 0x20
|
|
CCF
|
|
SUB
|
|
BRC dumpDot ; Below a space.
|
|
INIB 0x7F
|
|
CCF
|
|
SUB
|
|
BNC dumpDot ; Delete, or above it.
|
|
OUTA 0x00
|
|
BRI dumpCharNext
|
|
dumpDot:
|
|
INIA 0x2E
|
|
OUTA 0x00
|
|
dumpCharNext:
|
|
INCD.1
|
|
SETD.0 DumpCount
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA dumpChar
|
|
CALL newLine
|
|
|
|
; Sixteen further along, carrying into the high byte if the low one wrapped.
|
|
SETD.0 DumpAt
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d16
|
|
CCF
|
|
ADD
|
|
STQ.0
|
|
BNC dumpRowNext
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
dumpRowNext:
|
|
SETD.0 DumpRows
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA dumpRow
|
|
BRI prompt
|
|
|
|
disassembleGo:
|
|
INIA 0d8
|
|
SETD.0 DumpRows
|
|
STA.0
|
|
disassembleOne:
|
|
CALL showInstruction
|
|
SETD.0 DumpRows
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA disassembleOne
|
|
BRI prompt
|
|
|
|
dumpBadWhere:
|
|
SETD.0 ExamineUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
dumpNoBank:
|
|
SETD.0 NoSuchBank
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; s <address> <byte> <byte> ...
|
|
;
|
|
; Writes into whichever bank is being looked at, THROUGH THE CONTROLLER, so Program Memory
|
|
; can be changed as easily as Data - which no instruction on this machine can do, and which
|
|
; is most of the reason for having a monitor at all.
|
|
;
|
|
; The cursor is left alone. Somebody poking a byte is usually looking at something else, and
|
|
; having the address they were reading move underneath them would be a poor reward.
|
|
doSet:
|
|
; A bank can be present and still refuse to be written: the controller's own table is
|
|
; published read only, and writing to it is refused. A refusal nobody catches stops the
|
|
; machine, which is a poor answer to somebody looking around with b and then typing s.
|
|
CALL bankPresent
|
|
SETD.0 BankFlags
|
|
LDA.0
|
|
INIB 0x02 ; The read only bit of that bank's record.
|
|
AND
|
|
BNQ setReadOnly
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ setWhat
|
|
|
|
SETD.1 DumpBank
|
|
LDA.1
|
|
OUTA 0xE3
|
|
SETD.1 TextValue
|
|
LDA.1
|
|
OUTA 0xE4
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE5
|
|
|
|
CALL stepPastNumber
|
|
PSHD.3
|
|
POPD.0
|
|
setByte:
|
|
CALL textHexWord
|
|
BNQ prompt ; Nothing more on the line, so that was all of them.
|
|
SETD.1 TextValue
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE9 ; The destination steps on by itself, so a run of bytes is a loop.
|
|
CALL stepPastNumber
|
|
PSHD.3
|
|
POPD.0
|
|
BRI setByte
|
|
|
|
setReadOnly:
|
|
SETD.0 ReadOnlyText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
setWhat:
|
|
SETD.0 SetUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; g <address>
|
|
;
|
|
; Somewhere to go. It does not come back by itself - that would want a breakpoint, which is
|
|
; a byte written over an instruction and a handler waiting for it, and neither exists yet.
|
|
; But a program that gives the machine back the ordinary way lands at the prompt it was
|
|
; started from, which is this one, still in the monitor.
|
|
doGo:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ goWhat
|
|
|
|
; Where the Stack is now, written down before leaving, exactly as run does it. Whatever is
|
|
; jumped to may give the machine back through osExit, and osExit puts the Stack back to
|
|
; what is written here - so without this it would restore the one the LAST run left, or
|
|
; none at all, and the shell would come back with its Stack pointing at nothing.
|
|
MVSD.0
|
|
SETD.1 SystemStack
|
|
STD.0.1
|
|
|
|
SETD.1 TextValue
|
|
LDD.3.1
|
|
BRD.3
|
|
|
|
goWhat:
|
|
SETD.0 GoUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; DP0 names text that textHexWord has just read a number off the front of. LEAVES DP3 past
|
|
; the digits and any spaces after them, ready for the next one.
|
|
;
|
|
; DP3 rather than DP0, because a subroutine cannot hand a pointer back in DP0: CALL saves it
|
|
; and RET puts it back, so stepping it here would be undone on the way out.
|
|
stepPastNumber:
|
|
PSHD.0
|
|
POPD.3
|
|
SETD.1 TextDigits
|
|
LDB.1
|
|
stepPastDigits:
|
|
BRB stepPastSpaces
|
|
INCD.3
|
|
DECB
|
|
BRI stepPastDigits
|
|
stepPastSpaces:
|
|
LDA.3
|
|
BRA stepPastDone
|
|
INIB 0x20
|
|
XOR
|
|
BNQ stepPastDone
|
|
INCD.3
|
|
BRI stepPastSpaces
|
|
stepPastDone:
|
|
RET
|
|
|
|
|
|
; Points the controller's source at the cursor, so that reading port 0xE9 walks forwards.
|
|
aimAtDumpAt:
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
OUTA 0xE0
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
OUTA 0xE1
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE2
|
|
RET
|
|
|
|
; One byte from the cursor, and the cursor moves on.
|
|
;
|
|
; THE BYTE COMES BACK IN Q, not in A, because a subroutine cannot hand anything back in A:
|
|
; CALL saves it and RET puts it back, so an assignment here would be undone by the return.
|
|
takeByte:
|
|
INA 0xE9
|
|
PSHA
|
|
SETD.0 DumpAt
|
|
INCD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
BNC takeByteDone
|
|
DECD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
takeByteDone:
|
|
POPA
|
|
RSTB
|
|
OR
|
|
RET
|
|
|
|
; ---- Showing instructions ----
|
|
;
|
|
; The half of a monitor that a byte dump cannot do. What it needs and a dump does not
|
|
; is to know how LONG each instruction is, because getting that wrong does not print one
|
|
; line wrong - it loses the place and prints everything after it wrong.
|
|
|
|
; One instruction: where it is, the bytes it is made of, and what it says.
|
|
showInstruction:
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
|
|
CALL aimAtDumpAt
|
|
CALL takeByte
|
|
MVQA
|
|
SETD.0 Opcode
|
|
STA.0
|
|
|
|
CALL findInstruction
|
|
BNQ showUnknown
|
|
|
|
; What shape it is, and from that how many bytes it runs to.
|
|
PSHD.3
|
|
POPD.0
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 Shape
|
|
STA.1
|
|
|
|
SETD.0 ShapeLength
|
|
LDB.1
|
|
shapeStep:
|
|
BRB shapeGot
|
|
INCD.0
|
|
DECB
|
|
BRI shapeStep
|
|
shapeGot:
|
|
LDA.0
|
|
SETD.1 Length
|
|
STA.1
|
|
|
|
; The rest of its bytes. The first one is already read.
|
|
SETD.0 InstrBytes
|
|
SETD.1 Opcode
|
|
LDA.1
|
|
STA.0
|
|
INCD.0
|
|
SETD.1 Length
|
|
LDB.1
|
|
DECB
|
|
readRest:
|
|
BRB readRestDone
|
|
PSHB
|
|
CALL takeByte
|
|
POPB
|
|
MVQA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BRI readRest
|
|
readRestDone:
|
|
|
|
; Show them, padded out so that what follows lines up however long the instruction was.
|
|
SETD.0 InstrBytes
|
|
SETD.1 Length
|
|
LDB.1
|
|
showBytes:
|
|
PSHB
|
|
LDA.0
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
POPB
|
|
INCD.0
|
|
DECB
|
|
BNB showBytes
|
|
|
|
INIB 0d4
|
|
SETD.0 Length
|
|
LDA.0
|
|
padBytes:
|
|
CCF
|
|
SUB
|
|
BRQ padDone ; As many as there are, so nothing to pad.
|
|
PSHA
|
|
PSHB
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
OUTA 0x00
|
|
OUTA 0x00
|
|
POPB
|
|
POPA
|
|
DECB
|
|
BRI padBytes
|
|
padDone:
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
|
|
; Its name, without the spaces it is padded to four with.
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d02
|
|
INIB 0d4
|
|
showName:
|
|
LDA.0
|
|
BRA showNameDone
|
|
PSHB
|
|
INIB 0x20
|
|
XOR
|
|
POPB
|
|
BRQ showNameDone
|
|
OUTA 0x00
|
|
INCD.0
|
|
DECB
|
|
BNB showName
|
|
showNameDone:
|
|
|
|
; And whatever follows it, which depends only on the shape.
|
|
SETD.0 Shape
|
|
LDA.0
|
|
BRA showOperandNone ; 0, nothing at all
|
|
|
|
DECA
|
|
BRA showAddress ; 1, an address
|
|
DECA
|
|
BRA showByte ; 2, one byte
|
|
DECA
|
|
BRA showSelector ; 3, a Data Pointer
|
|
DECA
|
|
BRA showSelectorByte ; 4, a Data Pointer and a byte
|
|
DECA
|
|
BRA showSelectorAddress ; 5, a Data Pointer and an address
|
|
BRI showTwoSelectors ; 6
|
|
|
|
showOperandNone:
|
|
CALL newLine
|
|
RET
|
|
|
|
showAddress:
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showByte:
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showSelector:
|
|
CALL putSelectorOne
|
|
CALL newLine
|
|
RET
|
|
|
|
showSelectorByte:
|
|
CALL putSelectorOne
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
DPUP.0 0d02
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showSelectorAddress:
|
|
CALL putSelectorOne
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
DPUP.0 0d02
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showTwoSelectors:
|
|
CALL putSelectorOne
|
|
INIA 0d46 ; .
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
DPUP.0 0d02
|
|
LDA.0
|
|
CALL printDecimalDigit
|
|
CALL newLine
|
|
RET
|
|
|
|
; ".n" for the selector that follows the opcode.
|
|
putSelectorOne:
|
|
INIA 0d46
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
INCD.0
|
|
LDA.0
|
|
CALL printDecimalDigit
|
|
RET
|
|
|
|
; A byte that decodes as nothing. Shown as it is, and the cursor moves on by one, because
|
|
; the only honest thing to do with a byte that is not an instruction is say so and carry on.
|
|
showUnknown:
|
|
SETD.0 Opcode
|
|
LDA.0
|
|
CALL printByteHex
|
|
SETD.0 UnknownText
|
|
SWI osPrintString
|
|
RET
|
|
|
|
; Looks the opcode up. DP3 lands on its entry and Q is zero, or Q is not zero and it is not
|
|
; an instruction at all.
|
|
findInstruction:
|
|
SETD.3 Instructions
|
|
SETD.0 InstructionCount
|
|
LDB.0
|
|
findStep:
|
|
LDA.3
|
|
SETD.0 Opcode
|
|
PSHB
|
|
LDB.0
|
|
XOR
|
|
POPB
|
|
BRQ findFound
|
|
DPUP.3 0d07
|
|
DECB
|
|
BNB findStep
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
findFound:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Is there such a bank? Q is zero if there is not.
|
|
;
|
|
; ASKING THE CONTROLLER FOR A BANK THAT IS NOT THERE IS REFUSED, and a refusal nobody
|
|
; catches stops the machine, which is a wretched answer to a mistyped number. The bank table
|
|
; says what exists, and it lives in bank 2.
|
|
;
|
|
; Bank n's record starts at n times eight. A and B are a shift register sixteen bits wide,
|
|
; so putting the number in the low half and rotating left three times multiplies it by
|
|
; eight without anything falling off the top: the most it can reach is 2040.
|
|
bankPresent:
|
|
RSTA
|
|
SETD.0 DumpBank
|
|
LDB.0
|
|
SHL SHL SHL
|
|
SETD.0 DumpRecord
|
|
STA.0
|
|
INCD.0
|
|
STB.0
|
|
|
|
INIA 0d2
|
|
OUTA 0xE0 ; SourceBank: the controller's own memory.
|
|
SETD.0 DumpRecord
|
|
LDA.0
|
|
OUTA 0xE1
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE2
|
|
INA 0xE9 ; The flags byte of that bank's record.
|
|
SETD.0 BankFlags
|
|
STA.0 ; Kept whole, since present is not the only thing it says.
|
|
INIB 0x01
|
|
AND
|
|
RET
|
|
|
|
; ---- help ----
|
|
|
|
doHelp:
|
|
SETD.0 HelpText
|
|
CALL printString
|
|
CALL newLine
|
|
SETD.0 HelpMoreText
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; And what the monitor adds, but only when it is on. Listing commands that would not
|
|
; answer is a way of teaching somebody something untrue.
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA prompt
|
|
SETD.0 MonitorHelp
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
#Data
|
|
|
|
Banner:
|
|
"CosmOS"
|
|
PromptText:
|
|
"> "
|
|
NoDisk:
|
|
"no filesystem on the disk"
|
|
Unknown:
|
|
"I do not know: "
|
|
Farewell:
|
|
"halted"
|
|
FilesText:
|
|
" files"
|
|
FileText:
|
|
" file"
|
|
|
|
; Two strings rather than one, because a string literal stops at 255 characters and each
|
|
; one carries its own zero byte, so they are printed in turn rather than joined.
|
|
HelpText:
|
|
"dir list what is on the disk
|
|
load <file> read a program off the disk
|
|
run [words] start what was loaded, and tell it those words
|
|
delete <file> take it off the disk
|
|
rename <file> <to> call it something else"
|
|
HelpMoreText:
|
|
"monitor look at memory, change it, and jump into it
|
|
help this
|
|
exit stop, or leave the monitor if you are in it"
|
|
|
|
ExamineUsage:
|
|
"x <address>, or x on its own to carry on"
|
|
NoSuchBank:
|
|
"there is no such bank"
|
|
ProgramWord:
|
|
"program"
|
|
DataWord:
|
|
"data"
|
|
|
|
ExecMagic:
|
|
"SBEX"
|
|
LoadWhat:
|
|
"load what?"
|
|
NoSuchFile:
|
|
"no such file"
|
|
Deleted:
|
|
"gone"
|
|
Renamed:
|
|
"renamed"
|
|
DeleteWhat:
|
|
"delete what?"
|
|
RenameWhat:
|
|
"rename what to what?"
|
|
RenameNo:
|
|
"there is no such file, or that name is taken"
|
|
TooManyVectors:
|
|
"that program wants more vectors than there is room for"
|
|
Unreadable:
|
|
"could not read it"
|
|
NotProgram:
|
|
"not a program"
|
|
WrongVersion:
|
|
"a version I do not know"
|
|
LoadedText:
|
|
"loaded, starting at "
|
|
NothingLoaded:
|
|
"nothing is loaded"
|
|
Finished:
|
|
"finished"
|
|
|
|
BreakText:
|
|
"break at "
|
|
ARegText:
|
|
"A "
|
|
BRegText:
|
|
"B "
|
|
QRegText:
|
|
"Q "
|
|
SRegText:
|
|
"S "
|
|
DP0Text:
|
|
"DP0 "
|
|
DP1Text:
|
|
"DP1 "
|
|
DP2Text:
|
|
"DP2 "
|
|
DP3Text:
|
|
"DP3 "
|
|
ResumeText:
|
|
"press a key "
|
|
MonitorPrompt:
|
|
"* "
|
|
UnknownText:
|
|
" ?
|
|
"
|
|
MonitorName:
|
|
"monitor"
|
|
MonitorHelp:
|
|
"x examine, d disassemble, s set, b bank, g go, exit leaves"
|
|
ExamineName:
|
|
"x"
|
|
DisName:
|
|
"d"
|
|
SetName:
|
|
"s"
|
|
BankName:
|
|
"b"
|
|
GoName:
|
|
"g"
|
|
BankUsage:
|
|
"b <program|data|number>"
|
|
BankIs:
|
|
"bank "
|
|
SetUsage:
|
|
"s <address> <byte> <byte> ..."
|
|
ReadOnlyText:
|
|
"that bank will not be written"
|
|
GoUsage:
|
|
"g <address>"
|
|
DirName:
|
|
"dir"
|
|
LoadName:
|
|
"load"
|
|
RunName:
|
|
"run"
|
|
DeleteName:
|
|
"delete"
|
|
RenameName:
|
|
"rename"
|
|
HelpName:
|
|
"help"
|
|
ExitName:
|
|
"exit"
|
|
|
|
DiskReady:
|
|
0x00
|
|
LoadedOk:
|
|
0x00
|
|
LoadedEntry:
|
|
0x00 0x00
|
|
LoadCount:
|
|
0x00
|
|
LoadVersion:
|
|
0x00
|
|
|
|
; Where the first of rename's two names is, kept while the second is picked out of the
|
|
; line, since finding that needs the pointers for itself.
|
|
RenameFrom:
|
|
0x00 0x00
|
|
|
|
; What followed "run", kept for the program to ask for.
|
|
RunArgument:
|
|
#Reserve 0d64
|
|
|
|
; A number on its way to being printed, since the routine that prints one wants it in
|
|
; memory and a service is handed it in registers.
|
|
PrintNumber:
|
|
0x00 0x00
|
|
|
|
; ---- The vectors a loaded program brought with it ----
|
|
;
|
|
; Six bytes each: where it goes, what goes there, and what was there before. The last two
|
|
; are filled in when the program runs and read back when it exits, so what a program covers
|
|
; up comes back rather than becoming a hole.
|
|
;
|
|
; Sixteen is a limit rather than a considered number. It is far more than anything written
|
|
; so far wants, and a program asking for more is refused at load rather than having some of
|
|
; its handlers installed and the rest dropped.
|
|
VectorSource:
|
|
0x00 0x00
|
|
VectorsLeft:
|
|
0x00
|
|
LoadedVectorCount:
|
|
0x00
|
|
LoadedVectors:
|
|
#Reserve 0d96
|
|
|
|
; Where the monitor is looking, so that a bare 'dump' can carry on from it.
|
|
DumpBank:
|
|
0x00
|
|
DumpAt:
|
|
0x00 0x00
|
|
DumpRows:
|
|
0x00
|
|
Mode:
|
|
0x00
|
|
DumpCount:
|
|
0x00
|
|
BankWas:
|
|
0x00
|
|
BankFlags:
|
|
0x00
|
|
ShowAsCode:
|
|
0x00
|
|
DumpRecord:
|
|
0x00 0x00
|
|
Opcode:
|
|
0x00
|
|
Shape:
|
|
0x00
|
|
Length:
|
|
0x00
|
|
InstrBytes:
|
|
#Reserve 0d4
|
|
|
|
; How many bytes an instruction of each shape runs to, the opcode included.
|
|
ShapeLength:
|
|
0d1 0d3 0d2 0d2 0d3 0d4 0d3
|
|
|
|
InstructionCount:
|
|
0d64
|
|
|
|
; ---- The instruction table ----
|
|
;
|
|
; Generated by Tests/instructiontable.py from the assembler's own list, and checked against
|
|
; it by Tests/docs.sh. Seven bytes each: the opcode, the shape, and four characters of name
|
|
; with the zero the assembler puts after a string.
|
|
Instructions:
|
|
0x00 0d0 "ADD "
|
|
0x01 0d0 "SUB "
|
|
0x02 0d0 "AND "
|
|
0x03 0d0 "OR "
|
|
0x04 0d0 "XOR "
|
|
0x05 0d0 "NOTA"
|
|
0x06 0d0 "NOTB"
|
|
0x07 0d0 "SHL "
|
|
0x08 0d0 "SHR "
|
|
0x10 0d1 "BRI "
|
|
0x11 0d1 "BRQ "
|
|
0x12 0d1 "BRA "
|
|
0x13 0d1 "BRB "
|
|
0x14 0d1 "BRC "
|
|
0x15 0d3 "BRD "
|
|
0x1A 0d1 "BNQ "
|
|
0x1B 0d1 "BNA "
|
|
0x1C 0d1 "BNB "
|
|
0x1D 0d1 "BNC "
|
|
0x17 0d1 "CALL"
|
|
0x18 0d2 "SWI "
|
|
0x19 0d0 "RETI"
|
|
0x1F 0d0 "RET "
|
|
0x20 0d0 "RSTA"
|
|
0x21 0d0 "RSTB"
|
|
0x22 0d0 "INCA"
|
|
0x23 0d0 "INCB"
|
|
0x24 0d0 "DECA"
|
|
0x25 0d0 "DECB"
|
|
0x26 0d2 "INIA"
|
|
0x27 0d2 "INIB"
|
|
0x28 0d0 "CCF "
|
|
0x29 0d0 "MVQA"
|
|
0x2A 0d0 "MVQB"
|
|
0x2B 0d0 "SIF "
|
|
0x2C 0d0 "CIF "
|
|
0x30 0d0 "PSHQ"
|
|
0x31 0d0 "PSHA"
|
|
0x32 0d0 "PSHB"
|
|
0x33 0d3 "PSHD"
|
|
0x34 0d0 "POPA"
|
|
0x35 0d0 "POPB"
|
|
0x36 0d3 "POPD"
|
|
0x40 0d3 "INCD"
|
|
0x41 0d3 "DECD"
|
|
0x42 0d3 "LDA "
|
|
0x43 0d3 "LDB "
|
|
0x44 0d3 "STQ "
|
|
0x45 0d3 "STA "
|
|
0x46 0d3 "STB "
|
|
0x47 0d5 "SETD"
|
|
0x48 0d4 "DPUP"
|
|
0x49 0d4 "DPDN"
|
|
0x4A 0d6 "LDD "
|
|
0x4B 0d6 "STD "
|
|
0x4C 0d3 "MVSD"
|
|
0x4D 0d3 "MVDS"
|
|
0xD0 0d2 "OUTQ"
|
|
0xD1 0d2 "OUTA"
|
|
0xD2 0d2 "OUTB"
|
|
0xE0 0d2 "INA "
|
|
0xE1 0d2 "INB "
|
|
0xF0 0d0 "NOP "
|
|
0xFF 0d0 "HALT"
|
|
DumpBytes:
|
|
#Reserve 0d16
|
|
|
|
; Where the system's Stack was when it handed the machine to a program. Kept below the
|
|
; region a program owns, so that a program has to go looking to break it.
|
|
SystemStack:
|
|
0x00 0x00
|
|
DirSeen:
|
|
0x00
|
|
DirSize:
|
|
0x00 0x00
|
|
WidthLeft:
|
|
0x00
|
|
|
|
; Sixty three characters and the zero byte that ends them.
|
|
CommandLine:
|
|
#Reserve 0d64
|
|
|
|
#Vectors
|
|
|
|
Boot boot
|
|
osPrintString handlePrintString
|
|
osReadLine handleReadLine
|
|
osExit handleExit
|
|
osArgument handleArgument
|
|
osFileRead handleFileRead
|
|
osFileSave handleFileSave
|
|
osFileDelete handleFileDelete
|
|
osFileRename handleFileRename
|
|
osPrintNumber handlePrintNumber
|
|
osBreak handleBreak
|
|
Device 0x20 diskDone
|