Memory controller implemented.
This commit is contained in:
@@ -55,7 +55,9 @@ The software vectors are given out like this:
|
||||
| 0 | The Boot Vector. Where the machine begins at power on. |
|
||||
| 1 | The Soft Reset Vector. A warm restart. |
|
||||
| 2 | A byte that is not an instruction. |
|
||||
| 3 to 15 | Held back for faults not yet defined. |
|
||||
| 3 | A device refused a write, because it landed inside a raised fence. GuardViolation. |
|
||||
| 4 | A bank was named that has nothing in it, or an access ran past its end. BankFault. |
|
||||
| 5 to 15 | Held back for faults not yet defined. |
|
||||
| 16 and up | A program's own, given out by the assembler in the order they are named. |
|
||||
|
||||
A programmer does not write vector numbers. Handlers are named in the Vector Segment of an assembly file and used by name, the same way every other address in SplitBit is worked out by the assembler rather than typed. See the SplitBit Assembler Manual.
|
||||
@@ -126,6 +128,9 @@ 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 0xFF | The bus registry. See Asking What Is There below. | 0x01 |
|
||||
|
||||
## Asking What Is There:
|
||||
@@ -162,9 +167,180 @@ One thing to be careful of: the registry remembers which port it was asked about
|
||||
| 0x00 | Nothing. |
|
||||
| 0x01 | Bus registry. |
|
||||
| 0x02 | Console. |
|
||||
| 0x03 - 0x0F | Reserved for the machine itself. |
|
||||
| 0x10 | Test device. |
|
||||
| 0x11 - 0xFF | Peripherals. |
|
||||
| 0x03 | Memory controller. |
|
||||
| 0x04 - 0x0F | Reserved for the machine itself. |
|
||||
| 0x10 | Test device, which raises its own line. |
|
||||
| 0x11 | Test device, which refuses everything. |
|
||||
| 0x12 | Test device, which owns memory. |
|
||||
| 0x13 - 0xFF | Peripherals. |
|
||||
|
||||
## The Memory Controller:
|
||||
|
||||
SplitBit's instruction set cannot write Program Memory. That is what a Harvard machine is, and it is worth keeping true of the instructions: a machine where any instruction stream can rewrite its own code has given away the separation it was built for. The memory controller can, so writing code is a capability reached deliberately through a port rather than something every program has by accident.
|
||||
|
||||
It answers on ports 0xE0 to 0xEF, one register to a port.
|
||||
|
||||
| Port | Register |
|
||||
| --- | --- |
|
||||
| 0xE0 | SourceBank |
|
||||
| 0xE1 | SourceHigh |
|
||||
| 0xE2 | SourceLow |
|
||||
| 0xE3 | DestBank |
|
||||
| 0xE4 | DestHigh |
|
||||
| 0xE5 | DestLow |
|
||||
| 0xE6 | LengthHigh |
|
||||
| 0xE7 | LengthLow |
|
||||
| 0xE8 | Command |
|
||||
| 0xE9 | Data |
|
||||
| 0xEA | Status |
|
||||
| 0xEB | GuardBank |
|
||||
| 0xEC | GuardStartHigh |
|
||||
| 0xED | GuardStartLow |
|
||||
| 0xEE | GuardEndHigh |
|
||||
| 0xEF | GuardEndLow |
|
||||
|
||||
Reading the Data port takes a byte from the source and steps the source address on. Writing to it puts a byte at the destination and steps the destination address on. Reading a run of bytes is therefore a loop over one instruction rather than four.
|
||||
|
||||
Status says how the last thing the controller was asked to do went. Zero means it worked; anything else is the vector it refused with.
|
||||
|
||||
### Commands:
|
||||
|
||||
Writing to the Command port performs it at once. A transfer is instantaneous as far as the CPU is concerned: waiting belongs to a peripheral that has something to wait for, not to the moving of bytes.
|
||||
|
||||
| Value | Command | Does |
|
||||
| --- | --- | --- |
|
||||
| 0x01 | Blit | Moves Length bytes from Source to Dest. Either end may be any bank, including the same one. |
|
||||
| 0x02 | Fill | Writes the byte in SourceLow across Dest, Length times. SourceBank and SourceHigh mean nothing here, because a fill has nowhere to read from. |
|
||||
| 0x10 | GuardOn | Raises a fence over the bank named by GuardBank, across the range in the guard registers. |
|
||||
| 0x11 | GuardOff | Lowers that bank's fence. |
|
||||
| 0x03 | RegisterBank | Gives the bank number in DestBank to the memory owned by the device on port SourceLow. |
|
||||
|
||||
Length is how many bytes, and zero means the whole 64K, since a transfer of nothing is never what anyone meant. Both commands leave the addresses past whatever they touched and leave Length as it was, so asking again carries straight on from where the last one stopped.
|
||||
|
||||
A blit may overlap itself. Sliding a run of bytes along inside its own bank works, rather than repeating the first byte the way a plain forward copy would.
|
||||
|
||||
Everything a transfer would touch is checked before any of it moves. A transfer that would run out of bank, or write somewhere it may not, is refused whole: nothing moves at all. A transfer that stopped halfway would leave memory in a state no program asked for, and the diagnostic would arrive after the damage rather than instead of it.
|
||||
|
||||
Filling is worth reaching for. Clearing a page with one Fill instead of a store and a loop takes about a tenth off the running time of the segmented sieve, which spends most of its life zeroing its window.
|
||||
|
||||
### Banks:
|
||||
|
||||
Memory the controller can reach is divided into banks of up to 64K each, numbered 0 to 255. Program and Data are banks like any other; being 0 and 1 is the only thing special about them.
|
||||
|
||||
| Bank | Holds |
|
||||
| --- | --- |
|
||||
| 0 | Program Memory. |
|
||||
| 1 | Data Memory. |
|
||||
| 2 | The controller's own memory, which is where the bank table lives. |
|
||||
| 3 and up | Registered by software, for devices that bring memory of their own. |
|
||||
|
||||
Banks 0 to 2 belong to the machine and cannot be handed out. Everything above them is registered by whoever enumerated the hardware, so which number a device's memory answers to is the operating system's business rather than the machine's. That is what lets a device own no banks, one, or several.
|
||||
|
||||
Registering asks the bus registry which ports bring memory, then names one:
|
||||
|
||||
```
|
||||
INIA 0d5
|
||||
OUTA 0xE3 ; The bank number being handed out.
|
||||
INIA 0x12
|
||||
OUTA 0xE2 ; The port that owns the memory.
|
||||
INIA 0x03
|
||||
OUTA 0xE8 ; RegisterBank.
|
||||
```
|
||||
|
||||
How big the bank is comes from the device, not from the program. Capacity was settled when the machine was built, so a program asserting it could only ever be wrong. Registering a bank over one that already holds something is allowed: nothing was allocated that could be lost by changing your mind.
|
||||
|
||||
Registering fails if the number is one of the machine's own, or if the port named brings no memory. Either would put a bank in the table that leads nowhere.
|
||||
|
||||
A reset clears the table back to banks 0, 1 and 2. Banks are soft state, so a program that wants a device's memory registers it during setup, which is a handful of writes and needs no operating system.
|
||||
|
||||
Banks are reachable only through the controller. There is no bank register that changes what the CPU sees, and there never will be: a Data Pointer addresses bank 1, always, so an instruction never means something different depending on state you cannot see in the listing.
|
||||
|
||||
### The Bank Table:
|
||||
|
||||
Bank 2 holds a description of every bank, eight bytes each, so bank n's record begins at n times eight.
|
||||
|
||||
| Byte | Holds |
|
||||
| --- | --- |
|
||||
| 0 | Flags: bit 0 present, bit 1 read only, bit 2 fenced. |
|
||||
| 1 | The port that owns it, or 0xFF for the machine itself. |
|
||||
| 2 and 3 | Capacity, where zero means the whole 64K. |
|
||||
| 4 and 5 | The first guarded address. |
|
||||
| 6 and 7 | The last guarded address. |
|
||||
|
||||
A program reads it the way it reads anything else, by pointing the controller at bank 2. There is no separate query for it, and nothing to interfere with an enumeration already in progress.
|
||||
|
||||
Bank 2 is read only. That is what keeps the table something only the controller changes, and the table is what every transfer routes through: corrupt it and everything afterwards goes somewhere arbitrary. What the table holds is a description rather than the machinery, so writing to it could never redirect a bank even if it were allowed.
|
||||
|
||||
### The Fence:
|
||||
|
||||
Every bank may have one guarded range. A write that lands inside it is refused, and so is any transfer that so much as overlaps it: a blit that clipped the edge would otherwise do the part that fitted and leave the rest undone.
|
||||
|
||||
Set GuardBank to say which bank, put the first and last guarded addresses in the guard registers, and write GuardOn. GuardOff lowers it again.
|
||||
|
||||
**Any program may lower any fence.** This is a fence rather than a wall, and nobody is ever told no. What it stops is walking into something by accident. A program that genuinely means to write there lowers the fence first, which costs one instruction and says plainly in the listing what it intended.
|
||||
|
||||
A fence guards writing, not reading. Whatever is behind it can still be read, because a debugger has to be able to see what it is protecting.
|
||||
|
||||
A range that ends before it starts is refused. No address could be inside it, so it would catch nothing while looking like it caught something, and a program that raised one would believe it was protected when it was not.
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
The sequence is short. Put the bytes of a routine somewhere, blit them into Program Memory, write their address into a vector, and call it.
|
||||
|
||||
```
|
||||
; Vector 20 lives at 0xFC00 plus twice twenty, which is 0xFC28.
|
||||
RSTA
|
||||
OUTA 0xE3 ; DestBank = 0, Program Memory.
|
||||
INIA 0xFC
|
||||
OUTA 0xE4
|
||||
INIA 0x28
|
||||
OUTA 0xE5
|
||||
OUTB 0xE9 ; The handler's address, high byte then low.
|
||||
OUTA 0xE9
|
||||
SWI 0d20
|
||||
```
|
||||
|
||||
A vector that is empty when a program starts and holds a working handler by the time it is called is the whole reason this device exists. A vector table nothing can write is not really a vector table.
|
||||
|
||||
### What Loading Does Not Solve:
|
||||
|
||||
A routine that has been moved works at its new address only if it does not contain any addresses of its own.
|
||||
|
||||
INIA, OUTA and RETI name no places, so a routine built only from those runs correctly wherever it is put. A branch, a CALL, or a SETD is different: each of them carries an address that was decided when it was assembled, and that address is wrong everywhere except where it was assembled for. Nothing in SplitBit adjusts them.
|
||||
|
||||
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:
|
||||
|
||||
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 write the controller will not perform is a GuardViolation. There are two reasons for one: the bank is read only, or the write touches a fence that has been raised over it.
|
||||
|
||||
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.
|
||||
|
||||
## 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 refusal names a software vector, so a handler knows what happened from the entry it arrived through, the same as every other fault. The frame carries the address of the instruction that was refused, so a handler can see which one it was.
|
||||
|
||||
That means a handler returning with a bare RETI will meet the same refused instruction again. 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.
|
||||
|
||||
If nothing is installed for the vector a device refused with, the machine stops and the emulator says which port refused and where.
|
||||
|
||||
## Faults:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user