Memory controller implemented.
This commit is contained in:
@@ -62,7 +62,11 @@
|
||||
#define VECTOR_BOOT 0
|
||||
#define VECTOR_SOFT_RESET 1
|
||||
#define VECTOR_INVALID_OPCODE 2
|
||||
// Vectors 3 to 15 are held back for faults that do not exist yet, so that each cause
|
||||
// A device refused a write, because it landed inside a raised fence.
|
||||
#define VECTOR_GUARD_VIOLATION 3
|
||||
// A bank was named that has nothing registered in it, or an access ran past its end.
|
||||
#define VECTOR_BANK_FAULT 4
|
||||
// Vectors 5 to 15 are held back for faults that do not exist yet, so that each cause
|
||||
// can have an entry of its own rather than sharing one and needing a cause register to
|
||||
// tell them apart. Everything from 16 up belongs to programs.
|
||||
#define VECTOR_FIRST_FREE 16
|
||||
|
||||
@@ -144,9 +144,11 @@ static const struct {
|
||||
const char *name;
|
||||
uint8_t index;
|
||||
} reservedVectors[] = {
|
||||
{ "Boot", VECTOR_BOOT },
|
||||
{ "SoftReset", VECTOR_SOFT_RESET },
|
||||
{ "BadOpcode", VECTOR_INVALID_OPCODE },
|
||||
{ "Boot", VECTOR_BOOT },
|
||||
{ "SoftReset", VECTOR_SOFT_RESET },
|
||||
{ "BadOpcode", VECTOR_INVALID_OPCODE },
|
||||
{ "GuardViolation", VECTOR_GUARD_VIOLATION },
|
||||
{ "BankFault", VECTOR_BANK_FAULT },
|
||||
};
|
||||
static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0]));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user