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
+331
View File
@@ -0,0 +1,331 @@
// controller.c
// The SplitBit Memory Controller.
//
// This is the only thing on the machine that can write Program Memory. That is the whole
// reason it exists: SplitBit is a Harvard machine and its instruction set cannot reach
// its own code, which is worth keeping true of the instructions. Routing it through a
// device instead makes writing code a capability, reached deliberately through a port,
// rather than something every instruction stream can do by accident.
//
// Written by Anachronaut
#include "controller.h"
#include "io.h"
#include "../Assembler/assembly.h" // For the fault vector numbers.
#include <string.h>
typedef struct {
uint8_t *memory; // Never published. See the note in controller.h.
uint32_t capacity; // In bytes. A full bank is 65536, which is why this is not 16 bit.
uint8_t flags;
uint8_t ownerPort;
uint16_t guardStart;
uint16_t guardEnd;
} Bank;
static Bank banks[BANK_COUNT];
// Bank 2's contents: the description of every bank, for anything that wants to read it.
static uint8_t bankTable[BANK_TABLE_BYTES];
// The registers, exactly as the ports name them.
static uint8_t sourceBank, destBank, guardBank;
static uint16_t sourceAddress, destAddress, length;
static uint16_t guardStart, guardEnd;
static uint8_t status;
// Writes a bank's description into the table that bank 2 publishes. Called whenever
// anything about a bank changes, so the published table and the real one cannot drift.
static void publishBank(int number) {
uint8_t *record = bankTable + number * BANK_RECORD_BYTES;
record[0] = banks[number].flags;
record[1] = banks[number].ownerPort;
// Zero means the whole 64K, the same convention Length uses, because a capacity of
// nothing is never what anyone meant.
record[2] = (uint8_t)((banks[number].capacity >> 8) & 0xFF);
record[3] = (uint8_t)(banks[number].capacity & 0xFF);
record[4] = (uint8_t)(banks[number].guardStart >> 8);
record[5] = (uint8_t)(banks[number].guardStart & 0xFF);
record[6] = (uint8_t)(banks[number].guardEnd >> 8);
record[7] = (uint8_t)(banks[number].guardEnd & 0xFF);
}
static void defineBank(int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) {
banks[number].memory = memory;
banks[number].capacity = capacity;
banks[number].flags = flags | BANK_FLAG_PRESENT;
banks[number].ownerPort = owner;
banks[number].guardStart = 0;
banks[number].guardEnd = 0;
publishBank(number);
}
void initializeController(uint8_t *programMemory, uint8_t *dataMemory) {
memset(banks, 0, sizeof(banks));
memset(bankTable, 0, sizeof(bankTable));
for (int i = 0; i < BANK_COUNT; i++) {
banks[i].ownerPort = BANK_OWNER_MACHINE;
publishBank(i);
}
defineBank(BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE);
defineBank(BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE);
// The table describes itself, so a program that walks it finds bank 2 in there along
// with everything else. It is read only, which is what keeps RegisterBank the only
// way to change what the controller routes through.
defineBank(BANK_TABLE, bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE);
sourceBank = destBank = guardBank = 0;
sourceAddress = destAddress = length = 0;
guardStart = guardEnd = 0;
status = 0;
}
// Refuses, remembering why so that Status can be read afterwards.
static void refuse(uint8_t faultVector) {
status = faultVector;
refuseAccess(faultVector);
}
// Is this somewhere the controller can read? A bank has to be there, and the address has
// to be inside it.
static int canRead(uint8_t bank, uint16_t address) {
if (!(banks[bank].flags & BANK_FLAG_PRESENT) || address >= banks[bank].capacity) {
refuse(VECTOR_BANK_FAULT);
return 0;
}
return 1;
}
// The same, and then the two reasons a write in particular gets turned away.
static int canWrite(uint8_t bank, uint16_t address) {
if (!canRead(bank, address)) {
return 0;
}
if (banks[bank].flags & BANK_FLAG_READ_ONLY) {
refuse(VECTOR_GUARD_VIOLATION);
return 0;
}
if ((banks[bank].flags & BANK_FLAG_GUARDED)
&& address >= banks[bank].guardStart && address <= banks[bank].guardEnd) {
refuse(VECTOR_GUARD_VIOLATION);
return 0;
}
return 1;
}
// A length of zero means the whole 64K, because a transfer of no bytes is never what
// anyone meant, and 65536 does not fit in the two bytes that carry it.
static uint32_t transferLength(void) {
return (length == 0) ? 0x10000u : (uint32_t)length;
}
// Everything a transfer will touch is checked before any of it moves. A blit that ran
// out of bank halfway would leave memory in a state no program asked for, and the
// diagnostic would arrive after the damage rather than instead of it. So these answer
// for the whole range or refuse the whole thing.
static int rangeReadable(uint8_t bank, uint16_t address, uint32_t count) {
if (!(banks[bank].flags & BANK_FLAG_PRESENT)
|| (uint32_t)address + count > banks[bank].capacity) {
refuse(VECTOR_BANK_FAULT);
return 0;
}
return 1;
}
static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) {
if (!rangeReadable(bank, address, count)) {
return 0;
}
if (banks[bank].flags & BANK_FLAG_READ_ONLY) {
refuse(VECTOR_GUARD_VIOLATION);
return 0;
}
if (banks[bank].flags & BANK_FLAG_GUARDED) {
uint32_t last = (uint32_t)address + count - 1;
// Any overlap at all with the fence, not just a write that starts inside it.
if (!(last < banks[bank].guardStart || address > banks[bank].guardEnd)) {
refuse(VECTOR_GUARD_VIOLATION);
return 0;
}
}
return 1;
}
static void doBlit(void) {
uint32_t count = transferLength();
if (!rangeReadable(sourceBank, sourceAddress, count)) {
return;
}
if (!rangeWritable(destBank, destAddress, count)) {
return;
}
// memmove rather than memcpy, because source and destination may be the same bank
// and may overlap. Sliding a buffer along itself is an ordinary thing to want, and
// getting it silently wrong is exactly the sort of failure this machine keeps
// designing against.
memmove(banks[destBank].memory + destAddress,
banks[sourceBank].memory + sourceAddress, count);
sourceAddress = (uint16_t)(sourceAddress + count);
destAddress = (uint16_t)(destAddress + count);
status = 0;
}
static void doFill(void) {
uint32_t count = transferLength();
if (!rangeWritable(destBank, destAddress, count)) {
return;
}
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and
// the rest of the source registers mean nothing here.
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
destAddress = (uint16_t)(destAddress + count);
status = 0;
}
// DestBank is the number being given out, and SourceLow says which port owns the memory.
// The capacity is asked of the device rather than supplied, because how big a bank is
// was settled when the machine was built.
static void doRegisterBank(void) {
if (destBank <= BANK_TABLE) {
// Banks 0 to 2 are the machine's own and are not anybody's to hand out.
refuse(VECTOR_BANK_FAULT);
return;
}
uint8_t port = (uint8_t)(sourceAddress & 0xFF);
uint32_t capacity = 0;
uint8_t *memory = deviceMemory(port, &capacity);
if (memory == NULL) {
// Either nothing is on that port or what is there brings no memory. Registering
// it would put a bank in the table that leads nowhere.
refuse(VECTOR_BANK_FAULT);
return;
}
// Registering over a bank that already has something in it is allowed. Which number
// a device's memory answers to is the OS's business, and nothing was allocated that
// could be lost by changing its mind.
defineBank(destBank, memory, capacity, 0, port);
status = 0;
}
// The guard registers stage a range; this is what commits it. Raising a fence over a
// bank that is not there would protect nothing while looking like it protected
// something, so it is refused rather than quietly accepted.
static void doGuardOn(void) {
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) {
refuse(VECTOR_BANK_FAULT);
return;
}
if (guardStart > guardEnd) {
// No address can be inside a range that ends before it starts, so this fence
// would catch nothing. A program that raised one would believe it was protected
// and would not be, which is worse than having no fence at all.
refuse(VECTOR_BANK_FAULT);
return;
}
banks[guardBank].guardStart = guardStart;
banks[guardBank].guardEnd = guardEnd;
banks[guardBank].flags |= BANK_FLAG_GUARDED;
publishBank(guardBank);
status = 0;
}
static void doGuardOff(void) {
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) {
refuse(VECTOR_BANK_FAULT);
return;
}
banks[guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED;
publishBank(guardBank);
status = 0;
}
uint8_t controllerWrite(uint8_t value, uint8_t port) {
switch (port) {
case CTRL_SOURCE_BANK: sourceBank = value; break;
case CTRL_SOURCE_HIGH: sourceAddress = (uint16_t)(value << 8) | (sourceAddress & 0x00FF); break;
case CTRL_SOURCE_LOW: sourceAddress = (sourceAddress & 0xFF00) | value; break;
case CTRL_DEST_BANK: destBank = value; break;
case CTRL_DEST_HIGH: destAddress = (uint16_t)(value << 8) | (destAddress & 0x00FF); break;
case CTRL_DEST_LOW: destAddress = (destAddress & 0xFF00) | value; break;
case CTRL_LENGTH_HIGH: length = (uint16_t)(value << 8) | (length & 0x00FF); break;
case CTRL_LENGTH_LOW: length = (length & 0xFF00) | value; break;
case CTRL_GUARD_BANK: guardBank = value; break;
case CTRL_GUARD_START_HIGH: guardStart = (uint16_t)(value << 8) | (guardStart & 0x00FF); break;
case CTRL_GUARD_START_LOW: guardStart = (guardStart & 0xFF00) | value; break;
case CTRL_GUARD_END_HIGH: guardEnd = (uint16_t)(value << 8) | (guardEnd & 0x00FF); break;
case CTRL_GUARD_END_LOW: guardEnd = (guardEnd & 0xFF00) | value; break;
case CTRL_DATA:
// A byte into the destination, and the address steps on so that writing a
// run of bytes is a loop over one instruction rather than four.
if (canWrite(destBank, destAddress)) {
banks[destBank].memory[destAddress] = value;
if (destBank == BANK_TABLE) {
// Unreachable while the table is read only, and here so that it stays
// true if that ever changes: the published bytes are a description,
// and nothing may write through them into a real bank.
publishBank(BANK_TABLE);
}
destAddress++;
status = 0;
}
break;
case CTRL_COMMAND:
// Both leave the addresses past whatever they touched and Length as it was,
// so asking again carries straight on from where the last one stopped.
switch (value) {
case COMMAND_BLIT: doBlit(); break;
case COMMAND_FILL: doFill(); break;
case COMMAND_REGISTER_BANK: doRegisterBank(); break;
case COMMAND_GUARD_ON: doGuardOn(); break;
case COMMAND_GUARD_OFF: doGuardOff(); break;
default:
// Refusing an unknown command is better than ignoring it, since a
// program that asked for something is entitled to find out that it
// did not happen.
refuse(VECTOR_BANK_FAULT);
break;
}
break;
default:
// Status is read only.
break;
}
return 0;
}
uint8_t controllerRead(uint8_t port) {
switch (port) {
case CTRL_SOURCE_BANK: return sourceBank;
case CTRL_SOURCE_HIGH: return (uint8_t)(sourceAddress >> 8);
case CTRL_SOURCE_LOW: return (uint8_t)(sourceAddress & 0xFF);
case CTRL_DEST_BANK: return destBank;
case CTRL_DEST_HIGH: return (uint8_t)(destAddress >> 8);
case CTRL_DEST_LOW: return (uint8_t)(destAddress & 0xFF);
case CTRL_LENGTH_HIGH: return (uint8_t)(length >> 8);
case CTRL_LENGTH_LOW: return (uint8_t)(length & 0xFF);
case CTRL_STATUS: return status;
case CTRL_GUARD_BANK: return guardBank;
case CTRL_GUARD_START_HIGH: return (uint8_t)(guardStart >> 8);
case CTRL_GUARD_START_LOW: return (uint8_t)(guardStart & 0xFF);
case CTRL_GUARD_END_HIGH: return (uint8_t)(guardEnd >> 8);
case CTRL_GUARD_END_LOW: return (uint8_t)(guardEnd & 0xFF);
case CTRL_DATA: {
// A byte out of the source, stepping on the same way a write does.
if (!canRead(sourceBank, sourceAddress)) {
return 0;
}
uint8_t value = banks[sourceBank].memory[sourceAddress];
sourceAddress++;
status = 0;
return value;
}
}
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
// controller.h
// The SplitBit Memory Controller.
// Written by Anachronaut
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <stdint.h>
// ---- Ports ----
//
// Sixteen registers, one to a port, written and read directly. A register file behind a
// single port would be smaller but stateful, and losing your place in a device that
// moves memory corrupts memory rather than an answer.
#define CONTROLLER_PORT_BASE 0xE0
#define CONTROLLER_PORT_TOP 0xEF
#define CTRL_SOURCE_BANK 0xE0
#define CTRL_SOURCE_HIGH 0xE1
#define CTRL_SOURCE_LOW 0xE2
#define CTRL_DEST_BANK 0xE3
#define CTRL_DEST_HIGH 0xE4
#define CTRL_DEST_LOW 0xE5
#define CTRL_LENGTH_HIGH 0xE6
#define CTRL_LENGTH_LOW 0xE7
#define CTRL_COMMAND 0xE8
#define CTRL_DATA 0xE9
#define CTRL_STATUS 0xEA
#define CTRL_GUARD_BANK 0xEB
#define CTRL_GUARD_START_HIGH 0xEC
#define CTRL_GUARD_START_LOW 0xED
#define CTRL_GUARD_END_HIGH 0xEE
#define CTRL_GUARD_END_LOW 0xEF
// ---- Commands ----
//
// Written to the Command port, which performs them at once. A blit is instantaneous from
// the CPU's point of view: waiting belongs to the peripheral that has something to wait
// for, not to the moving of bytes.
#define COMMAND_BLIT 0x01
#define COMMAND_FILL 0x02
// Gives a bank number to the memory owned by a device. How big it is comes from the
// device, not from software: a program asserting a hardware fact could only ever be
// wrong about it.
#define COMMAND_REGISTER_BANK 0x03
// Raises and lowers the fence over the bank named by GuardBank. Any program may do
// either: this is a fence rather than a wall, and nobody is ever told no. What it stops
// is walking into something by accident, not walking into it on purpose.
#define COMMAND_GUARD_ON 0x10
#define COMMAND_GUARD_OFF 0x11
// ---- Banks ----
//
// Program and Data are banks like any other; being banks 0 and 1 is the only thing
// special about them. Bank 2 is the controller's own memory, and the bank table lives
// in it, which is how anything finds out what banks exist without a second protocol.
#define BANK_PROGRAM 0
#define BANK_DATA 1
#define BANK_TABLE 2
#define BANK_COUNT 256
// Eight bytes each, so bank n's record begins at n * 8.
//
// 0 Flags
// 1 The port that owns it, or the machine itself for banks 0 to 2
// 2 - 3 Capacity, where zero means the whole 64K
// 4 - 5 First guarded address
// 6 - 7 Last guarded address
//
// What is published is a description. The pointer a bank really holds is never in here:
// a program that could write one would be setting a host address, which means nothing on
// hardware and everything to the emulator running it.
#define BANK_RECORD_BYTES 8
#define BANK_TABLE_BYTES (BANK_COUNT * BANK_RECORD_BYTES)
#define BANK_FLAG_PRESENT 0x01
#define BANK_FLAG_READ_ONLY 0x02
#define BANK_FLAG_GUARDED 0x04
// Banks 0 to 2 belong to the machine rather than to any device.
#define BANK_OWNER_MACHINE 0xFF
void initializeController(uint8_t *programMemory, uint8_t *dataMemory);
uint8_t controllerWrite(uint8_t value, uint8_t port);
uint8_t controllerRead(uint8_t port);
#endif // CONTROLLER_H
+39 -5
View File
@@ -67,6 +67,25 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u
return 0;
}
// A device that refused what it was asked stops the machine where it stands, rather than
// raising a line and letting execution carry on past the mistake. The frame carries the
// address of the instruction that asked, so a handler can see which one it was, and so a
// bare RETI meets it again the way every other fault on this machine does.
//
// Returns 1 if the machine has stopped because nothing was installed to catch it.
static uint8_t answerRefusal(CPURegisters *cpu, uint16_t site) {
uint8_t refusal = takeRefusal();
if (refusal == 0) {
return 0;
}
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, refusal, site)) {
cpu->Fault = FAULT_DEVICE_REFUSED;
cpu->FaultVector = refusingPort();
cpu->ProgramCounter = site - 1;
}
return 0;
}
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
cpu->A = 0;
cpu->B = 0;
@@ -544,33 +563,48 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
//
// Dx - Output Operations:
//
case 0xD0:
case 0xD0: {
// OUTQ - Write the value of Q to an output port.
uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++;
OutputHandler(cpu->Q, cpu->Program[cpu->ProgramCounter]);
answerRefusal(cpu, site);
}
break;
case 0xD1:
case 0xD1: {
// OUTA - Write the value of A to an output port.
uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++;
OutputHandler(cpu->A, cpu->Program[cpu->ProgramCounter]);
answerRefusal(cpu, site);
}
break;
case 0xD2:
case 0xD2: {
// OUTB - Write the value of B to an output port.
uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++;
OutputHandler(cpu->B, cpu->Program[cpu->ProgramCounter]);
answerRefusal(cpu, site);
}
break;
//
// Ex - Input Operations:
//
case 0xE0:
case 0xE0: {
// INA - Read an Input to A.
uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++;
cpu->A = InputHandler(cpu->Program[cpu->ProgramCounter]);
answerRefusal(cpu, site);
}
break;
case 0xE1:
case 0xE1: {
// INB - Read an Input to B.
uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++;
cpu->B = InputHandler(cpu->Program[cpu->ProgramCounter]);
answerRefusal(cpu, site);
}
break;
//
// Fx - Special Operations:
+2 -1
View File
@@ -47,7 +47,8 @@ typedef enum {
FAULT_NONE = 0,
FAULT_BAD_OPCODE, // A byte that does not decode to an instruction.
FAULT_NO_HANDLER, // Dispatched through a software vector with nothing in it.
FAULT_NO_DEVICE_HANDLER // A device interrupted, and its vector was empty.
FAULT_NO_DEVICE_HANDLER, // A device interrupted, and its vector was empty.
FAULT_DEVICE_REFUSED // A device refused, and nothing was installed to catch it.
} FaultCause;
// The struct containing the CPU registers.
+7
View File
@@ -9,6 +9,7 @@
#include <stdint.h>
#include <stdlib.h>
#include "cpu.h"
#include "controller.h"
#include "utility.h"
#include <string.h>
#include <getopt.h>
@@ -90,6 +91,9 @@ int main (int argc, char *argv[]) {
return 1;
}
CPURegisters cpu;
// The controller has to know where the memories are before anything can reach
// them through it. Banks 0 and 1 are those two arrays.
initializeController(Program, Data);
initializeCPU(&cpu, Program, Data);
if(options.debug) {
printRegisters(&cpu, Program, Data);
@@ -140,6 +144,9 @@ int main (int argc, char *argv[]) {
if (cpu.Fault == FAULT_NO_HANDLER) {
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
cpu.FaultVector, cpu.ProgramCounter);
} else if (cpu.Fault == FAULT_DEVICE_REFUSED) {
fprintf(stderr, "Fault: The device on port %u refused the access at Program Address 0x%04X, and nothing is installed to deal with it.\n",
cpu.FaultVector, cpu.ProgramCounter);
} else if (cpu.Fault == FAULT_NO_DEVICE_HANDLER) {
fprintf(stderr, "Fault: The device on port %u interrupted at Program Address 0x%04X, and hardware vector %u has no handler installed.\n",
cpu.FaultVector, cpu.ProgramCounter, cpu.FaultVector);
+82
View File
@@ -4,7 +4,10 @@
// 10/16/2024
#include "io.h"
#include "../Assembler/assembly.h" // For the fault vector numbers.
#include "controller.h"
#include <stdio.h>
#include <string.h>
// One bit per port, so a device can ask for attention without anything having to poll
// it. Eight ports to the byte, low bit first.
@@ -37,6 +40,48 @@ int nextPendingInterrupt(void) {
return -1;
}
// ---- Refusing ----
//
// Set when a device will not do what it was asked, and read by the CPU immediately
// after the instruction that asked. It is not a queue: an instruction does one thing to
// one port, so there is only ever one refusal outstanding.
static uint8_t refusedVector = 0;
static uint8_t refusedPort = 0;
void refuseAccess(uint8_t faultVector) {
refusedVector = faultVector;
}
uint8_t takeRefusal(void) {
uint8_t vector = refusedVector;
refusedVector = 0;
return vector;
}
uint8_t refusingPort(void) {
return refusedPort;
}
// ---- A device that brings memory ----
//
// The simplest thing that owns a bank. Writing to its port fills its memory with the
// byte written, which stands in for a disk controller reading a sector: the CPU asks for
// something and the memory it owns then holds the answer. The waiting is taken out so a
// test runs the same way every time.
#define DEVICE_MEMORY_BYTES 256
static uint8_t deviceMemoryBlock[DEVICE_MEMORY_BYTES];
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
if (port != PORT_MEMORY) {
return NULL;
}
*capacity = DEVICE_MEMORY_BYTES;
return deviceMemoryBlock;
}
// ---- The bus registry ----
//
// What is plugged into this machine. The table is fixed when the machine is built: a
@@ -57,6 +102,8 @@ typedef struct {
static const DeviceRecord deviceTable[] = {
{ PORT_CONSOLE, DEVICE_CONSOLE, 0 },
{ PORT_TEST, DEVICE_TEST, 0 },
{ PORT_REFUSE, DEVICE_REFUSE, 0 },
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
};
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
@@ -66,7 +113,15 @@ static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]
static uint8_t registrySelected = 0;
static uint8_t registryCursor = 0;
static const DeviceRecord controllerRecord = { CONTROLLER_PORT_BASE, DEVICE_CONTROLLER, 0 };
static const DeviceRecord *deviceOnPort(uint8_t port) {
// The controller answers on a block of ports rather than one, so every port in the
// block reports it. Its own memory is bank 2, which is already registered, so it
// does not set the flag that means "this brings memory somebody has to register".
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
return &controllerRecord;
}
for (int i = 0; i < deviceCount; i++) {
if (deviceTable[i].port == port) {
return &deviceTable[i];
@@ -90,6 +145,12 @@ static uint8_t readRegistry(void) {
}
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Whichever port is being talked to is the one that would be doing any refusing.
refusedPort = Address;
// The controller answers on a block of ports, which is a range rather than a list.
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
return controllerWrite(DataByte, Address);
}
// This function sends the DataByte to the appropriate place based on the Port Address.
switch(Address) {
case PORT_CONSOLE:
@@ -98,6 +159,18 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte);
break;
case PORT_MEMORY:
// 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
// the controller, which is the only thing that can reach a device's memory.
memset(deviceMemoryBlock, DataByte, DEVICE_MEMORY_BYTES);
break;
case PORT_REFUSE:
// A device that refuses everything. It exists so that a device's ability to
// stop the CPU can be tested before anything depends on it, and so that the
// path stays tested once the memory controller is the only real user.
refuseAccess(VECTOR_GUARD_VIOLATION);
break;
case PORT_REGISTRY:
// Names the port the registry is being asked about. This is the only thing
// that can be written to the registry, and it changes nothing about the
@@ -122,11 +195,20 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
}
uint8_t InputHandler(uint8_t Address) {
refusedPort = Address;
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
return controllerRead(Address);
}
switch(Address) {
case PORT_CONSOLE:
// If data is sent here, it should be read from STDIN.
return getchar();
break;
case PORT_REFUSE:
// Refuses reads as well, so both directions are covered.
refuseAccess(VECTOR_GUARD_VIOLATION);
return 0;
break;
case PORT_REGISTRY:
// One byte of the selected port's record, then the next, and zero once the
// record has run out.
+38 -2
View File
@@ -16,6 +16,8 @@
#define PORT_CONSOLE 0x00
#define PORT_TEST 0x10
#define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12
#define PORT_REGISTRY 0xFF
// ---- Device classes ----
@@ -29,10 +31,13 @@
#define DEVICE_NONE 0x00
#define DEVICE_REGISTRY 0x01
#define DEVICE_CONSOLE 0x02
#define DEVICE_CONTROLLER 0x03
#define DEVICE_TEST 0x10
#define DEVICE_REFUSE 0x11
#define DEVICE_MEMORY 0x12
// What a device brings besides itself. The memory controller will want the first of
// these to find out which ports own memory it can reach.
// 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.
#define DEVICE_FLAG_HAS_MEMORY 0x01
// How many bytes a device's entry in the registry runs to. Reading past the end gives
@@ -59,4 +64,35 @@ void clearInterrupt(uint8_t port);
// The lowest numbered port with its line up, or -1 if none of them are.
int nextPendingInterrupt(void);
// ---- Refusing ----
//
// A device can refuse 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 rather than raise a line and let execution
// carry on past the mistake.
//
// The refusal names a software vector, so the cause is known from the entry it arrives
// through, the same way every other fault on this machine works.
void refuseAccess(uint8_t faultVector);
// ---- Memory a device brings ----
//
// Returns the memory owned by the device on this port, or NULL if it owns none, and
// fills in how much of it there is. This is how the controller finds out what a device
// brings when it is told to register a bank.
//
// It is a direct look at the machine's device table rather than a conversation through
// the registry's port. The port protocol remembers which port it was asked about, so a
// controller that used it would silently lose the place of any enumeration a program had
// in progress. Same table, two consumers, and only one of them needs the protocol.
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity);
// The vector a device refused with, or 0 if none did. Reading it clears it, because a
// refusal is answered once.
uint8_t takeRefusal(void);
// Which port did the refusing. Only meaningful alongside a refusal.
uint8_t refusingPort(void);
#endif // IO_H