Memory controller implemented.

This commit is contained in:
Anachronaut
2026-08-15 20:43:53 -04:00
parent c4b2c27a2d
commit 04dfcd707b
32 changed files with 1842 additions and 21 deletions
+40
View File
@@ -0,0 +1,40 @@
; Running off the end of a bank.
;
; Bank 2 is the bank table: 256 banks at eight bytes each, so 2048 bytes exactly. The
; last byte is at 0x07FF and there is nothing at 0x0800. A bank knowing how big it is
; means the difference shows up as a fault at the instruction that overran, instead of
; quietly reading whatever happened to be next in the emulator's memory.
;
; Nothing is installed for BankFault here, so the machine stops.
;
; Correct output is:
; O the last byte of the bank was read without complaint
; a fault naming port 233, which is the controller's Data port, and a non zero exit.
#Program
start:
INIA 0d2
OUTA 0xE0 ; SourceBank = 2
INIA 0x07
OUTA 0xE1
INIA 0xFF
OUTA 0xE2 ; 0x07FF, the last byte there is
INA 0xE9 ; Fine.
INIA 0d79 ; 'O'
OUTA 0x00
INIA 0x0A
OUTA 0x00
INIA 0x08
OUTA 0xE1
RSTA
OUTA 0xE2 ; 0x0800, one past the end
INA 0xE9 ; Refused: BankFault.
; Never reached.
INIA 0d88 ; 'X'
OUTA 0x00
HALT
+210
View File
@@ -0,0 +1,210 @@
; Blits and fills.
;
; A blit moves a run of bytes between any two banks, including from one place in a bank
; to another. A fill writes one byte across a run. Both are instantaneous: waiting
; belongs to a peripheral that has something to wait for, not to the moving of bytes.
;
; Everything a transfer will touch is checked before any of it moves, so a transfer that
; would run out of bank refuses outright rather than stopping halfway and leaving memory
; in a state nobody asked for. The last case here proves that: the blit is refused, and
; the bytes it would have written are still what they were.
;
; Correct output is:
; blitted copied from one place in Data Memory to another
; .... filled with a byte of our choosing
; DE AD blitted from Data Memory into Program Memory
; untouched a refused blit moved nothing at all
; ABABCDEFGH an overlapping blit slid the bytes rather than repeating one
#Include print.asm
#Program
start:
; ---- A blit inside Data Memory, which is what a memcpy is here. ----
SETD.0 Message
CALL aimSource
SETD.0 Landing
CALL aimDest
INIA 0d8
CALL setLength ; "blitted" and its terminator.
INIA 0x01
OUTA 0xE8 ; Blit.
SETD.0 Landing
CALL printString
CALL lineFeed
; ---- A fill, with a byte of our choosing rather than only zero. ----
SETD.0 Landing
CALL aimDest
INIA 0x2E ; '.', into SourceLow, which is where a fill takes its byte.
OUTA 0xE2
INIA 0d4
CALL setLength
INIA 0x02
OUTA 0xE8 ; Fill.
; Terminate it so printString knows where to stop.
SETD.0 Landing
DPUP.0 0d04
RSTA
STA.0
SETD.0 Landing
CALL printString
CALL lineFeed
; ---- Data Memory into Program Memory. This is what a loader does. ----
SETD.0 Code
CALL aimSource
SETD.0 Target
CALL aimDestProgram
INIA 0d2
CALL setLength
INIA 0x01
OUTA 0xE8 ; Blit.
; Read it back out of Program Memory to prove it landed.
SETD.0 Target
CALL aimSourceProgram
CALL readAndPrint
CALL readAndPrint
CALL lineFeed
; ---- A blit that would run past the end of bank 2. ----
; Bank 2 is 2048 bytes. Starting at 2040 and asking for 16 does not fit, so the whole
; thing is refused and Landing keeps what it already had.
SETD.0 Untouched
CALL aimSource
SETD.0 Landing
CALL aimDest
INIA 0d10
CALL setLength
INIA 0x01
OUTA 0xE8 ; This one works, so Landing says "untouched".
INIA 0d2
OUTA 0xE0 ; SourceBank = 2, the bank table.
INIA 0x07
OUTA 0xE1
INIA 0xF8
OUTA 0xE2 ; 0x07F8, eight bytes from the end.
SETD.0 Landing
CALL aimDest
INIA 0d16
CALL setLength
INIA 0x01
OUTA 0xE8 ; Refused: it would read past the end of bank 2.
SETD.0 Landing
CALL printString
CALL lineFeed
; ---- A blit whose source and destination overlap. ----
; Sliding a run of bytes along inside its own bank is an ordinary thing to want, so it
; works rather than quietly repeating the first byte the way a naive forward copy does.
SETD.0 Slide
CALL aimSource
SETD.0 Slide
DPUP.0 0d02
CALL aimDest
INIA 0d8
CALL setLength
INIA 0x01
OUTA 0xE8
SETD.0 Slide
CALL printString
CALL lineFeed
HALT
; ---- Helpers. DP0 holds the address to aim at. ----
aimSource:
INIA 0d1 ; Bank 1, Data Memory.
OUTA 0xE0
BRI aimSourceAddress
aimSourceProgram:
RSTA ; Bank 0, Program Memory.
OUTA 0xE0
aimSourceAddress:
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
RET
aimDest:
INIA 0d1 ; Bank 1, Data Memory.
OUTA 0xE3
BRI aimDestAddress
aimDestProgram:
RSTA ; Bank 0, Program Memory.
OUTA 0xE3
aimDestAddress:
PSHD.0
POPA
POPB
OUTB 0xE4
OUTA 0xE5
RET
; A is the length, which is never more than a byte in this program.
setLength:
PSHA
RSTA
OUTA 0xE6
POPA
OUTA 0xE7
RET
readAndPrint:
INA 0xE9
CALL printByteHex
CALL blankSpace
RET
; The refused blit is caught so the program can carry on and show that nothing moved.
; The command write is two bytes, an opcode and a port, so stepping by two gets past it.
refusedHandler:
MVSD.0
DPUP.0 0d14
LDA.0
CCF
INIB 0d2
ADD
MVQA
STA.0
BRC refusedCarried
RETI
refusedCarried:
DPDN.0 0d01
LDA.0
INCA
STA.0
RETI
; Never executed, only written over.
Target:
0x00 0x00
#Data
Message:
"blitted"
Untouched:
"untouched"
Code:
0xDE 0xAD
Landing:
#Reserve 0d16
Slide:
"ABCDEFGH"
#Reserve 0d4
#Vectors
BankFault refusedHandler
@@ -0,0 +1,66 @@
; Reads Program Memory through the memory controller.
;
; Nothing on SplitBit could do this before. The CPU cannot reach its own code, which is
; what a Harvard machine is, and this is the device that can.
;
; The bytes read back are a marker written into the Program Segment after the HALT, where
; nothing executes them. The controller is aimed at it using the label's own address, so
; this checks the controller against what the source says rather than against a guess
; about where the assembler put things.
;
; The address steps on by itself, so reading a run of bytes is a loop over one
; instruction rather than four.
;
; Correct output is:
; DE AD BE EF the marker, read out of Program Memory
; 01 FF bank 0's record in bank 2: present, owned by the machine
; 00 00 and its capacity, where zero means the whole 64K
#Include print.asm
#Program
start:
; Aim the controller at the marker, using the marker's own address.
SETD.0 Marker
PSHD.0 ; High byte, then low, so the low byte comes off first.
POPA
POPB
PSHA ; Keep the low byte while A is needed for something else.
RSTA
OUTA 0xE0 ; SourceBank = 0, which is Program Memory.
OUTB 0xE1 ; SourceHigh
POPA
OUTA 0xE2 ; SourceLow
CALL readAndPrint
CALL readAndPrint
CALL readAndPrint
CALL readAndPrint
CALL lineFeed
; Now the bank table, which is bank 2. Bank 0's record comes first.
INIA 0d2
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
CALL readAndPrint ; Flags: present.
CALL readAndPrint ; Owner: the machine itself.
CALL lineFeed
CALL readAndPrint ; Capacity, high byte.
CALL readAndPrint ; Capacity, low byte.
CALL lineFeed
HALT
readAndPrint:
INA 0xE9
CALL printByteHex
CALL blankSpace
RET
; Never executed. It is here to be read rather than run.
Marker:
0xDE 0xAD 0xBE 0xEF
@@ -0,0 +1,127 @@
; Writes Program Memory through the controller, and gets turned away from what it may
; not touch.
;
; Writing code is the thing SplitBit's instruction set deliberately cannot do. The
; controller can, which is what makes a loader possible. The marker below is written and
; then read back through the controller, so the change is observed rather than assumed.
;
; The two refusals are the other half. Bank 2 is the bank table and is read only, so
; writing to it is a GuardViolation. Bank 200 has nothing registered in it, so naming it
; is a BankFault. Both handlers step the saved address past the instruction that was
; refused, the way any handler carrying on past a fault has to.
;
; Correct output is:
; AA BB the marker after being written through the controller
; readonly
; nobank
; done
#Include print.asm
#Program
start:
; Aim both ends of the controller at the marker.
SETD.0 Marker
PSHD.0
POPA
POPB
PSHA
PSHB
RSTA
OUTA 0xE0 ; SourceBank = 0
OUTA 0xE3 ; DestBank = 0
POPB
OUTB 0xE1 ; SourceHigh
OUTB 0xE4 ; DestHigh
POPA
OUTA 0xE2 ; SourceLow
OUTA 0xE5 ; DestLow
; Write two bytes over the marker. This is Program Memory being changed at run time.
INIA 0xAA
OUTA 0xE9
INIA 0xBB
OUTA 0xE9
; Read them back the same way, to prove they landed.
CALL readAndPrint
CALL readAndPrint
CALL lineFeed
; Bank 2 is the bank table, and it is read only.
INIA 0d2
OUTA 0xE3
RSTA
OUTA 0xE4
OUTA 0xE5
INIA 0d1
OUTA 0xE9 ; Refused: GuardViolation.
; Bank 200 has nothing in it.
INIA 0d200
OUTA 0xE3
INIA 0d1
OUTA 0xE9 ; Refused: BankFault.
SETD.0 Done
CALL printString
CALL lineFeed
HALT
readAndPrint:
INA 0xE9
CALL printByteHex
CALL blankSpace
RET
readOnlyHandler:
SETD.0 ReadOnly
CALL printString
CALL lineFeed
BRI stepPastRefusal
noBankHandler:
SETD.0 NoBank
CALL printString
CALL lineFeed
BRI stepPastRefusal
; Steps the saved address past the refused instruction. OUT is two bytes, an opcode and
; a port, so two is what it takes.
stepPastRefusal:
MVSD.0
DPUP.0 0d14
LDA.0
CCF
INIB 0d2
ADD
MVQA
STA.0
BRC carried
RETI
carried:
DPDN.0 0d01
LDA.0
INCA
STA.0
RETI
; Never executed.
Marker:
0x00 0x00
#Data
ReadOnly:
"readonly"
NoBank:
"nobank"
Done:
"done"
#Vectors
GuardViolation readOnlyHandler
BankFault noBankHandler
+172
View File
@@ -0,0 +1,172 @@
; The fence.
;
; A guarded range is a fence rather than a wall. Any program may raise one and any
; program may lower it, so nobody is ever told no. What it stops is walking into
; something by accident, which is the failure that costs an afternoon: without it, code
; that gets walked over is found much later, when the wreckage is finally executed and
; the evidence is long gone.
;
; The handler reads the controller's destination registers to say where the refused write
; was aimed. That is the whole point of catching it at the instruction that did it: the
; address is still sitting there to be read.
;
; The addresses here are absolute rather than labels, so that this says the same thing no
; matter what else ends up in the Data Segment. Data Memory is 64K and this program uses
; a hundred bytes of it, so 0x0200 is empty ground.
;
; Correct output is:
; outside ok a write just below the fence went through
; caught 0200 a write inside it was refused, and the handler says where
; caught 01FC a blit that merely clipped it was refused whole
; read ok reading inside the fence is allowed: it guards writing
; lowered ok after GuardOff the same write goes through
; fenced 05 the bank table shows bank 1 present and fenced
#Include print.asm
#Program
start:
; Fence off 0x0200 to 0x020F in Data Memory.
INIA 0d1
OUTA 0xEB ; GuardBank = 1
INIA 0x02
OUTA 0xEC
RSTA
OUTA 0xED ; GuardStart = 0x0200
INIA 0x02
OUTA 0xEE
INIA 0x0F
OUTA 0xEF ; GuardEnd = 0x020F
INIA 0x10
OUTA 0xE8 ; GuardOn
; Just below the fence is ordinary ground.
INIA 0x01
OUTA 0xE4
INIA 0xFF
OUTA 0xE5 ; Dest = 0x01FF
INIA 0d1
OUTA 0xE3 ; DestBank = 1
INIA 0x41
OUTA 0xE9
SETD.0 OutsideOk
CALL say
; Inside it is not.
INIA 0x02
OUTA 0xE4
RSTA
OUTA 0xE5 ; Dest = 0x0200
INIA 0x42
OUTA 0xE9 ; Refused.
; A blit from 0x01FC for eight bytes runs to 0x0203, so it clips the fence. It is
; refused whole rather than doing the four bytes that would have fitted.
INIA 0x01
OUTA 0xE4
INIA 0xFC
OUTA 0xE5 ; Dest = 0x01FC
INIA 0d1
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2 ; Source = bank 1, 0x0000
OUTA 0xE6
INIA 0d8
OUTA 0xE7 ; Length = 8
INIA 0x01
OUTA 0xE8 ; Refused.
; Reading inside a fence is allowed. A debugger has to be able to see what is being
; protected.
INIA 0d1
OUTA 0xE0
INIA 0x02
OUTA 0xE1
RSTA
OUTA 0xE2 ; Source = 0x0200
INA 0xE9
SETD.0 ReadOk
CALL say
; Lower it, and the write that was refused goes through.
INIA 0x11
OUTA 0xE8 ; GuardOff
INIA 0x02
OUTA 0xE4
RSTA
OUTA 0xE5
INIA 0x43
OUTA 0xE9
SETD.0 LoweredOk
CALL say
; Raise it again, and read the flags back out of the bank table, where a fence is
; published along with everything else about a bank.
INIA 0x10
OUTA 0xE8
INIA 0d2
OUTA 0xE0
RSTA
OUTA 0xE1
INIA 0d8
OUTA 0xE2 ; Bank 1's record begins at 8.
SETD.0 Fenced
CALL printString
CALL blankSpace
INA 0xE9
CALL printByteHex
CALL lineFeed
HALT
; Says where the refused write was aimed, by reading the controller's own registers.
guardHandler:
SETD.0 Caught
CALL printString
CALL blankSpace
INA 0xE4 ; DestHigh
CALL printByteHex
INA 0xE5 ; DestLow
CALL printByteHex
CALL lineFeed
; Step past the refused instruction. Every write here is an opcode and a port.
MVSD.0
DPUP.0 0d14
LDA.0
CCF
INIB 0d2
ADD
MVQA
STA.0
BRC stepCarried
RETI
stepCarried:
DPDN.0 0d01
LDA.0
INCA
STA.0
RETI
say:
CALL printString
CALL lineFeed
RET
#Data
OutsideOk:
"outside ok"
Caught:
"caught"
ReadOk:
"read ok"
LoweredOk:
"lowered ok"
Fenced:
"fenced"
#Vectors
GuardViolation guardHandler
+125
View File
@@ -0,0 +1,125 @@
; A program that loads a program.
;
; This is what the memory controller was built for. The bytes of a small routine sit in
; the Data Segment the way a program read off a disk would. The loader blits them into
; Program Memory, writes their address into a software vector, and calls it. Nothing
; about that routine was known to the assembler as code: to the loader it is data, and it
; becomes code only because it was put somewhere the CPU fetches from.
;
; Installing the vector at run time is the part worth watching. The argument that started
; the whole controller design was that a vector table nothing can write is not really a
; vector table. Vector 20 is empty when this program starts and holds a real handler by
; the time it is called, and nothing but the controller could have put it there.
;
; WHAT THIS DOES NOT SOLVE:
;
; The payload works at whatever address it lands at only because it contains no
; addresses. It is INIA and OUTA and a RETI, and none of those name a place. The moment a
; loaded routine contains a branch, a CALL, or a SETD, it holds an address that was fixed
; when it was assembled, and it will be wrong everywhere except where it was assembled
; for. SplitBit has no answer to that yet. Loading works; relocating does not exist.
;
; Correct output is:
; before the vector is still empty here
; loaded printed by code that was data a moment ago
; back and RETI came home
#Include print.asm
#Program
start:
SETD.0 Before
CALL say
; ---- Put the payload into Program Memory. ----
SETD.0 Payload
CALL aimSourceData
SETD.0 LandingZone
CALL aimDestProgram
INIA 0d29
CALL setLength
INIA 0x01
OUTA 0xE8 ; Blit: Data Memory into Program Memory.
; ---- Write its address into software vector 20. ----
; The table is at 0xFC00 and entries are two bytes, so vector 20 is at 0xFC28.
RSTA
OUTA 0xE3 ; DestBank = 0, Program Memory.
INIA 0xFC
OUTA 0xE4
INIA 0x28
OUTA 0xE5
SETD.0 LandingZone
PSHD.0
POPA
POPB
OUTB 0xE9 ; High byte of the handler's address.
OUTA 0xE9 ; Low byte. The Data port steps on by itself.
; ---- Call it. ----
SWI 0d20
SETD.0 Back
CALL say
HALT
; DP0 holds the address to aim at.
aimSourceData:
INIA 0d1
OUTA 0xE0
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
RET
aimDestProgram:
RSTA
OUTA 0xE3
PSHD.0
POPA
POPB
OUTB 0xE4
OUTA 0xE5
RET
setLength:
PSHA
RSTA
OUTA 0xE6
POPA
OUTA 0xE7
RET
say:
CALL printString
CALL lineFeed
RET
; Where the payload lands. Reserving it means the assembler knows the room is spoken for,
; so nothing else is ever placed here.
LandingZone:
#Reserve 0d48
#Data
Before:
"before"
Back:
"back"
; The payload, as bytes rather than as code. It prints "loaded" and returns. Written out
; by hand because SplitBit has no linker: to this program it is just so much data.
;
; INIA 'l' OUTA 0 INIA 'o' OUTA 0 ... and so on, then RETI
Payload:
0x26 0x6C 0xD1 0x00
0x26 0x6F 0xD1 0x00
0x26 0x61 0xD1 0x00
0x26 0x64 0xD1 0x00
0x26 0x65 0xD1 0x00
0x26 0x64 0xD1 0x00
0x26 0x0A 0xD1 0x00
0x19
@@ -0,0 +1,25 @@
; A device refuses, and nothing is installed to deal with it.
;
; The machine has to stop where it stands and say which port refused and where. Letting
; execution carry on would mean the program continues as though the access had worked,
; which is the failure the fault trap exists to prevent.
;
; Correct output is:
; O
; a fault naming port 17, and a non zero exit.
#Program
start:
INIA 0d79 ; 'O', so it is clear how far it got.
OUTA 0x00
INIA 0x0A
OUTA 0x00
INIA 0d1
OUTA 0x11 ; The device refuses, and no handler is installed.
; Never reached.
INIA 0d88 ; 'X'
OUTA 0x00
HALT
+74
View File
@@ -0,0 +1,74 @@
; Tests a device refusing what it was asked to do.
;
; Interrupting is a device asking for attention later. Refusing is a device saying no to
; the instruction happening now, so it has to stop the machine where it stands instead of
; letting execution carry on past the mistake.
;
; The device on port 0x11 refuses everything, in both directions, so that this path can
; be exercised without the memory controller. A refusal names a software vector, so the
; handler knows what happened from the entry it arrived through.
;
; The handler steps the saved address past the instruction that was refused, the same way
; the fault handler steps past a byte that would not decode. Without that a bare RETI
; would meet the same refused instruction again forever.
;
; Correct output is:
; caught the handler caught a refused write
; caught and a refused read
; done
#Include print.asm
#Program
start:
INIA 0d1
OUTA 0x11 ; Refused. The handler runs and steps us past this.
INA 0x11 ; Refused as well, and reads are caught the same way.
SETD.0 Done
CALL printString
CALL lineFeed
HALT
; A refusal arrives with a full frame, so the handler may use whatever it likes.
refusalHandler:
CALL reportRefusal
; Step the saved address past the instruction that was refused. Both OUT and IN are
; two bytes, an opcode and a port, so two is what it takes.
MVSD.0
DPUP.0 0d14
LDA.0
CCF
INIB 0d2
ADD
MVQA
STA.0
BRC carried
RETI
carried:
DPDN.0 0d01
LDA.0
INCA
STA.0
RETI
reportRefusal:
SETD.0 Caught
CALL printString
CALL lineFeed
RET
#Data
Caught:
"caught"
Done:
"done"
#Vectors
GuardViolation refusalHandler
+132
View File
@@ -0,0 +1,132 @@
; Giving a bank number to a device's memory.
;
; This is the whole sequence an OS goes through: ask the bus registry which ports bring
; memory, give one of them a bank number, and then reach it with the controller like any
; other bank. A device's memory is unreachable until it has a number, and the controller
; is the only thing that can reach it even then.
;
; How big the bank is comes from the device rather than from this program. Capacity was
; settled when the machine was built, so software asserting it could only ever be wrong.
;
; The device on port 0x12 owns 256 bytes and fills them with whatever byte is written to
; its port, which stands in for a disk controller reading a sector.
;
; Correct output is:
; flags 01 the registry says port 0x12 brings memory
; 01 12 01 00 bank 5's record: present, owned by port 0x12, 256 bytes
; 5A 5A the device's memory, reached through the controller
; refused registering over one of the machine's own banks
; refused and registering memory from a port that brings none
#Include print.asm
#Program
start:
; Ask the registry whether port 0x12 brings memory.
INIA 0x12
OUTA 0xFF
INA 0xFF ; Class, which we do not need here.
INA 0xFF ; Flags: bit 0 says it brings memory.
PSHA
SETD.0 Flags
CALL printString
CALL blankSpace
POPA
CALL printByteHex
CALL lineFeed
; Tell the device to fill its memory, the way a disk controller would be told to read.
INIA 0x5A
OUTA 0x12
; Give that memory bank number 5.
INIA 0d5
OUTA 0xE3 ; DestBank = 5, the number being handed out.
INIA 0x12
OUTA 0xE2 ; SourceLow = the port that owns it.
INIA 0x03
OUTA 0xE8 ; RegisterBank.
; Read bank 5's record out of the bank table. Eight bytes a bank, so bank 5 is at 40.
INIA 0d2
OUTA 0xE0
RSTA
OUTA 0xE1
INIA 0d40
OUTA 0xE2
CALL readAndPrint ; Flags: present.
CALL readAndPrint ; Owner: port 0x12.
CALL readAndPrint ; Capacity, high byte.
CALL readAndPrint ; Capacity, low byte.
CALL lineFeed
; Now reach the device's memory the way any other bank is reached.
INIA 0d5
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
CALL readAndPrint
CALL readAndPrint
CALL lineFeed
; Banks 0 to 2 are the machine's own and cannot be handed out.
INIA 0d1
OUTA 0xE3
INIA 0x12
OUTA 0xE2
INIA 0x03
OUTA 0xE8 ; Refused.
; Neither can memory from a port that brings none. Port 0x10 is the test device.
INIA 0d6
OUTA 0xE3
INIA 0x10
OUTA 0xE2
INIA 0x03
OUTA 0xE8 ; Refused.
HALT
readAndPrint:
INA 0xE9
CALL printByteHex
CALL blankSpace
RET
; Both refusals arrive here, and they really are the same fault: a bank that cannot be
; registered, for two different reasons. Saying so twice is more honest than inventing a
; distinction the machine does not draw.
bankHandler:
SETD.0 Refused
CALL printString
CALL lineFeed
; Step past the refused command write, which is an opcode and a port.
MVSD.0
DPUP.0 0d14
LDA.0
CCF
INIB 0d2
ADD
MVQA
STA.0
BRC bankCarried
RETI
bankCarried:
DPDN.0 0d01
LDA.0
INCA
STA.0
RETI
#Data
Flags:
"flags"
Refused:
"refused"
#Vectors
BankFault bankHandler