Give the memory controller to a bus rather than to the machine

The third and largest piece of the peripheral core, and like the two before it nothing
changes: 169 tests, and the two that would notice a misroute pass loudest of all.

A CONTROLLER IS THE MOST STATEFUL THING ON THIS MACHINE - a source bank and address, a
destination, a length, five guard registers and a table describing every bank it can reach.
Two processors sharing one would interleave into nonsense: one sets a source, the other sets
a destination, the first issues a blit and moves the wrong bytes somewhere else again. No
arbitration fixes that, because there is nothing to arbitrate - both writes were legal and
the result belongs to neither of them.

So it is a struct threaded through all seventeen functions that touch it, rather than a
pointer to a current one set on the way in. The smaller change was tempting and is the shape
that produced the stale reset flag and the editor's IsNew surviving a second run, both this
week: state that has to be set on the right path before anything reads it. A controller is
where that goes wrong quietly rather than loudly.

THE STATICS WERE DELETED RATHER THAN LEFT, which is what made this safe to do mechanically.
A missed reference is a compile error rather than a variable that still exists and serves the
wrong core, so "did I catch all 165?" stopped being a judgement and became a question the
compiler answered.

Two things the transformation nearly got wrong, both caught by reading rather than by
building. guardStart and guardEnd are fields of Bank as well as registers of the controller,
so banks[n].guardStart had to keep its name while a bare guardStart changed - the difference
between a fence and the register about to be written into one. And a definition and a call
look alike enough that the first attempt turned publishBank(number) into
publishBank(Controller *c, number); definitions start at column zero here and calls never do.

Tests/agree.sh is what says this is right, more than the count does. It builds the same disk
with SplitDisk and with CosmOS and compares byte for byte, and every filesystem operation on
the machine goes through the controller - so a blit that went to the wrong place would corrupt
a disk that is checked against one built by different code entirely. Tests/cycles.sh covers
the other half, since what a transfer costs depends on pendingCycles landing in the right one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-29 17:32:10 -04:00
co-authored by Claude Opus 5
parent 5be5bea994
commit 33afc20abc
5 changed files with 219 additions and 180 deletions
+150 -172
View File
@@ -14,152 +14,130 @@
#include "../Assembler/assembly.h" // For the fault vector numbers. #include "../Assembler/assembly.h" // For the fault vector numbers.
#include <string.h> #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 // 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. // anything about a bank changes, so the published table and the real one cannot drift.
static void publishBank(int number) { static void publishBank(Controller *c, int number) {
uint8_t *record = bankTable + number * BANK_RECORD_BYTES; uint8_t *record = c->bankTable + number * BANK_RECORD_BYTES;
record[0] = banks[number].flags; record[0] = c->banks[number].flags;
record[1] = banks[number].ownerPort; record[1] = c->banks[number].ownerPort;
// Zero means the whole 64K, the same convention Length uses, because a capacity of // Zero means the whole 64K, the same convention Length uses, because a capacity of
// nothing is never what anyone meant. // nothing is never what anyone meant.
record[2] = (uint8_t)((banks[number].capacity >> 8) & 0xFF); record[2] = (uint8_t)((c->banks[number].capacity >> 8) & 0xFF);
record[3] = (uint8_t)(banks[number].capacity & 0xFF); record[3] = (uint8_t)(c->banks[number].capacity & 0xFF);
record[4] = (uint8_t)(banks[number].guardStart >> 8); record[4] = (uint8_t)(c->banks[number].guardStart >> 8);
record[5] = (uint8_t)(banks[number].guardStart & 0xFF); record[5] = (uint8_t)(c->banks[number].guardStart & 0xFF);
record[6] = (uint8_t)(banks[number].guardEnd >> 8); record[6] = (uint8_t)(c->banks[number].guardEnd >> 8);
record[7] = (uint8_t)(banks[number].guardEnd & 0xFF); record[7] = (uint8_t)(c->banks[number].guardEnd & 0xFF);
} }
static void defineBank(int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) { static void defineBank(Controller *c, int number, uint8_t *memory, uint32_t capacity, uint8_t flags, uint8_t owner) {
banks[number].memory = memory; c->banks[number].memory = memory;
banks[number].capacity = capacity; c->banks[number].capacity = capacity;
banks[number].flags = flags | BANK_FLAG_PRESENT; c->banks[number].flags = flags | BANK_FLAG_PRESENT;
banks[number].ownerPort = owner; c->banks[number].ownerPort = owner;
banks[number].guardStart = 0; c->banks[number].guardStart = 0;
banks[number].guardEnd = 0; c->banks[number].guardEnd = 0;
publishBank(number); publishBank(c, number);
} }
void initializeController(uint8_t *programMemory, uint8_t *dataMemory) { void initializeController(Controller *c, uint8_t *programMemory, uint8_t *dataMemory) {
memset(banks, 0, sizeof(banks)); memset(c->banks, 0, sizeof(c->banks));
memset(bankTable, 0, sizeof(bankTable)); memset(c->bankTable, 0, sizeof(c->bankTable));
for (int i = 0; i < BANK_COUNT; i++) { for (int i = 0; i < BANK_COUNT; i++) {
banks[i].ownerPort = BANK_OWNER_MACHINE; c->banks[i].ownerPort = BANK_OWNER_MACHINE;
publishBank(i); publishBank(c, i);
} }
defineBank(BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE); defineBank(c, BANK_PROGRAM, programMemory, 0x10000, 0, BANK_OWNER_MACHINE);
defineBank(BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE); defineBank(c, BANK_DATA, dataMemory, 0x10000, 0, BANK_OWNER_MACHINE);
// The table describes itself, so a program that walks it finds bank 2 in there along // 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 // with everything else. It is read only, which is what keeps RegisterBank the only
// way to change what the controller routes through. // way to change what the controller routes through.
defineBank(BANK_TABLE, bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE); defineBank(c, BANK_TABLE, c->bankTable, BANK_TABLE_BYTES, BANK_FLAG_READ_ONLY, BANK_OWNER_MACHINE);
sourceBank = destBank = guardBank = 0; c->sourceBank = c->destBank = c->guardBank = 0;
sourceAddress = destAddress = length = 0; c->sourceAddress = c->destAddress = c->length = 0;
guardStart = guardEnd = 0; c->guardStart = c->guardEnd = 0;
status = 0; c->status = 0;
} }
// Refuses, remembering why so that Status can be read afterwards. // Refuses, remembering why so that Status can be read afterwards.
static void refuse(uint8_t faultVector) { static void refuse(Controller *c, uint8_t faultVector) {
status = faultVector; c->status = faultVector;
refuseAccess(faultVector); refuseAccess(faultVector);
} }
// Is this somewhere the controller can read? A bank has to be there, and the address has // Is this somewhere the controller can read? A bank has to be there, and the address has
// to be inside it. // to be inside it.
static int canRead(uint8_t bank, uint16_t address) { static int canRead(Controller *c, uint8_t bank, uint16_t address) {
if (!(banks[bank].flags & BANK_FLAG_PRESENT) || address >= banks[bank].capacity) { if (!(c->banks[bank].flags & BANK_FLAG_PRESENT) || address >= c->banks[bank].capacity) {
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return 0; return 0;
} }
return 1; return 1;
} }
// The same, and then the two reasons a write in particular gets turned away. // The same, and then the two reasons a write in particular gets turned away.
static int canWrite(uint8_t bank, uint16_t address) { static int canWrite(Controller *c, uint8_t bank, uint16_t address) {
if (!canRead(bank, address)) { if (!canRead(c, bank, address)) {
return 0; return 0;
} }
if (banks[bank].flags & BANK_FLAG_READ_ONLY) { if (c->banks[bank].flags & BANK_FLAG_READ_ONLY) {
refuse(VECTOR_GUARD_VIOLATION); refuse(c, VECTOR_GUARD_VIOLATION);
return 0; return 0;
} }
if ((banks[bank].flags & BANK_FLAG_GUARDED) if ((c->banks[bank].flags & BANK_FLAG_GUARDED)
&& address >= banks[bank].guardStart && address <= banks[bank].guardEnd) { && address >= c->banks[bank].guardStart && address <= c->banks[bank].guardEnd) {
refuse(VECTOR_GUARD_VIOLATION); refuse(c, VECTOR_GUARD_VIOLATION);
return 0; return 0;
} }
return 1; return 1;
} }
// A length of zero means the whole 64K, because a transfer of no bytes is never what // A c->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. // anyone meant, and 65536 does not fit in the two bytes that carry it.
static uint32_t transferLength(void) { static uint32_t transferLength(Controller *c) {
return (length == 0) ? 0x10000u : (uint32_t)length; return (c->length == 0) ? 0x10000u : (uint32_t)c->length;
} }
// Everything a transfer will touch is checked before any of it moves. A blit that ran // 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 // 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 // diagnostic would arrive after the damage rather than instead of it. So these answer
// for the whole range or refuse the whole thing. // for the whole range or refuse the whole thing.
static int rangeReadable(uint8_t bank, uint16_t address, uint32_t count) { static int rangeReadable(Controller *c, uint8_t bank, uint16_t address, uint32_t count) {
if (!(banks[bank].flags & BANK_FLAG_PRESENT) if (!(c->banks[bank].flags & BANK_FLAG_PRESENT)
|| (uint32_t)address + count > banks[bank].capacity) { || (uint32_t)address + count > c->banks[bank].capacity) {
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return 0; return 0;
} }
return 1; return 1;
} }
static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) { static int rangeWritable(Controller *c, uint8_t bank, uint16_t address, uint32_t count) {
if (!rangeReadable(bank, address, count)) { if (!rangeReadable(c, bank, address, count)) {
return 0; return 0;
} }
if (banks[bank].flags & BANK_FLAG_READ_ONLY) { if (c->banks[bank].flags & BANK_FLAG_READ_ONLY) {
refuse(VECTOR_GUARD_VIOLATION); refuse(c, VECTOR_GUARD_VIOLATION);
return 0; return 0;
} }
if (banks[bank].flags & BANK_FLAG_GUARDED) { if (c->banks[bank].flags & BANK_FLAG_GUARDED) {
uint32_t last = (uint32_t)address + count - 1; uint32_t last = (uint32_t)address + count - 1;
// Any overlap at all with the fence, not just a write that starts inside it. // Any overlap at all with the fence, not just a write that starts inside it.
if (!(last < banks[bank].guardStart || address > banks[bank].guardEnd)) { if (!(last < c->banks[bank].guardStart || address > c->banks[bank].guardEnd)) {
refuse(VECTOR_GUARD_VIOLATION); refuse(c, VECTOR_GUARD_VIOLATION);
return 0; return 0;
} }
} }
return 1; return 1;
} }
// What the moves below have cost since anybody last asked.
static unsigned long pendingCycles = 0;
// ---- Sixteen bits wide, when the addresses let it be ---- // ---- Sixteen bits wide, when the addresses let it be ----
// //
// The controller reaches bank memory two bytes at a time, so an aligned transfer moves two // The controller reaches bank memory two bytes at a time, so an aligned transfer moves two
// bytes in the time a misaligned one moves one. A word is read at an even address and // bytes in the time a misaligned one moves one. A word is read at an even address and
// written at an even address, which is why the source, the destination AND the length must // written at an even address, which is why the source, the destination AND the c->length must
// all be even: an odd anything would have the controller shifting bytes across word // all be even: an odd anything would have the controller shifting bytes across word
// boundaries to line them up, and that is a second design rather than this one. // boundaries to line them up, and that is a second design rather than this one.
// //
@@ -173,140 +151,140 @@ static int wideRun(uint32_t addressesAndLength) {
return (addressesAndLength & 1u) == 0; return (addressesAndLength & 1u) == 0;
} }
unsigned long controllerTakeCycles(void) { unsigned long controllerTakeCycles(Controller *c) {
unsigned long taken = pendingCycles; unsigned long taken = c->pendingCycles;
pendingCycles = 0; c->pendingCycles = 0;
return taken; return taken;
} }
static void doBlit(void) { static void doBlit(Controller *c) {
uint32_t count = transferLength(); uint32_t count = transferLength(c);
if (!rangeReadable(sourceBank, sourceAddress, count)) { if (!rangeReadable(c, c->sourceBank, c->sourceAddress, count)) {
return; return;
} }
if (!rangeWritable(destBank, destAddress, count)) { if (!rangeWritable(c, c->destBank, c->destAddress, count)) {
return; return;
} }
// memmove rather than memcpy, because source and destination may be the same bank // 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 // 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 // getting it silently wrong is exactly the sort of failure this machine keeps
// designing against. // designing against.
memmove(banks[destBank].memory + destAddress, memmove(c->banks[c->destBank].memory + c->destAddress,
banks[sourceBank].memory + sourceAddress, count); c->banks[c->sourceBank].memory + c->sourceAddress, count);
// A word read and a word written. Two banks are two memories and the pair overlaps; // A word read and a word written. Two c->banks are two memories and the pair overlaps;
// one bank is one memory and they do not. The odd cycle is the pipeline filling. // one bank is one memory and they do not. The odd cycle is the pipeline filling.
// //
// Wide when everything is even, so an aligned move between banks settles at two bytes a // Wide when everything is even, so an aligned move between c->banks settles at two bytes a
// cycle and an aligned move within one at a byte a cycle - each twice what it was. // cycle and an aligned move within one at a byte a cycle - each twice what it was.
unsigned long moves = wideRun(sourceAddress | destAddress | count) ? count / 2 : count; unsigned long moves = wideRun(c->sourceAddress | c->destAddress | count) ? count / 2 : count;
pendingCycles += (sourceBank == destBank) ? 2 * moves + 1 : moves + 1; c->pendingCycles += (c->sourceBank == c->destBank) ? 2 * moves + 1 : moves + 1;
sourceAddress = (uint16_t)(sourceAddress + count); c->sourceAddress = (uint16_t)(c->sourceAddress + count);
destAddress = (uint16_t)(destAddress + count); c->destAddress = (uint16_t)(c->destAddress + count);
status = 0; c->status = 0;
} }
static void doFill(void) { static void doFill(Controller *c) {
uint32_t count = transferLength(); uint32_t count = transferLength(c);
if (!rangeWritable(destBank, destAddress, count)) { if (!rangeWritable(c, c->destBank, c->destAddress, count)) {
return; return;
} }
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and the // 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 - including for the alignment, which // rest of the source registers mean nothing here - including for the alignment, which
// asks only about where the bytes are going and how many there are. // asks only about where the bytes are going and how many there are.
pendingCycles += (wideRun(destAddress | count) ? count / 2 : count) + 1; c->pendingCycles += (wideRun(c->destAddress | count) ? count / 2 : count) + 1;
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count); memset(c->banks[c->destBank].memory + c->destAddress, (int)(c->sourceAddress & 0xFF), count);
destAddress = (uint16_t)(destAddress + count); c->destAddress = (uint16_t)(c->destAddress + count);
status = 0; c->status = 0;
} }
// DestBank is the number being given out, and SourceLow says which port owns the memory. // 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 // The capacity is asked of the device rather than supplied, because how big a bank is
// was settled when the machine was built. // was settled when the machine was built.
static void doRegisterBank(void) { static void doRegisterBank(Controller *c) {
if (destBank <= BANK_TABLE) { if (c->destBank <= BANK_TABLE) {
// Banks 0 to 2 are the machine's own and are not anybody's to hand out. // Banks 0 to 2 are the machine's own and are not anybody's to hand out.
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return; return;
} }
uint8_t port = (uint8_t)(sourceAddress & 0xFF); uint8_t port = (uint8_t)(c->sourceAddress & 0xFF);
uint32_t capacity = 0; uint32_t capacity = 0;
uint8_t *memory = deviceMemory(port, &capacity); uint8_t *memory = deviceMemory(port, &capacity);
if (memory == NULL) { if (memory == NULL) {
// Either nothing is on that port or what is there brings no memory. Registering // 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. // it would put a bank in the table that leads nowhere.
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return; return;
} }
// Registering over a bank that already has something in it is allowed. Which number // 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 // a device's memory answers to is the OS's business, and nothing was allocated that
// could be lost by changing its mind. // could be lost by changing its mind.
defineBank(destBank, memory, capacity, 0, port); defineBank(c, c->destBank, memory, capacity, 0, port);
status = 0; c->status = 0;
} }
// The guard registers stage a range; this is what commits it. Raising a fence over a // 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 // bank that is not there would protect nothing while looking like it protected
// something, so it is refused rather than quietly accepted. // something, so it is refused rather than quietly accepted.
static void doGuardOn(void) { static void doGuardOn(Controller *c) {
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) { if (!(c->banks[c->guardBank].flags & BANK_FLAG_PRESENT)) {
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return; return;
} }
if (guardStart > guardEnd) { if (c->guardStart > c->guardEnd) {
// No address can be inside a range that ends before it starts, so this fence // 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 // 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. // and would not be, which is worse than having no fence at all.
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return; return;
} }
banks[guardBank].guardStart = guardStart; c->banks[c->guardBank].guardStart = c->guardStart;
banks[guardBank].guardEnd = guardEnd; c->banks[c->guardBank].guardEnd = c->guardEnd;
banks[guardBank].flags |= BANK_FLAG_GUARDED; c->banks[c->guardBank].flags |= BANK_FLAG_GUARDED;
publishBank(guardBank); publishBank(c, c->guardBank);
status = 0; c->status = 0;
} }
static void doGuardOff(void) { static void doGuardOff(Controller *c) {
if (!(banks[guardBank].flags & BANK_FLAG_PRESENT)) { if (!(c->banks[c->guardBank].flags & BANK_FLAG_PRESENT)) {
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
return; return;
} }
banks[guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED; c->banks[c->guardBank].flags &= (uint8_t)~BANK_FLAG_GUARDED;
publishBank(guardBank); publishBank(c, c->guardBank);
status = 0; c->status = 0;
} }
uint8_t controllerWrite(uint8_t value, uint8_t port) { uint8_t controllerWrite(Controller *c, uint8_t value, uint8_t port) {
switch (port) { switch (port) {
case CTRL_SOURCE_BANK: sourceBank = value; break; case CTRL_SOURCE_BANK: c->sourceBank = value; break;
case CTRL_SOURCE_HIGH: sourceAddress = (uint16_t)(value << 8) | (sourceAddress & 0x00FF); break; case CTRL_SOURCE_HIGH: c->sourceAddress = (uint16_t)(value << 8) | (c->sourceAddress & 0x00FF); break;
case CTRL_SOURCE_LOW: sourceAddress = (sourceAddress & 0xFF00) | value; break; case CTRL_SOURCE_LOW: c->sourceAddress = (c->sourceAddress & 0xFF00) | value; break;
case CTRL_DEST_BANK: destBank = value; break; case CTRL_DEST_BANK: c->destBank = value; break;
case CTRL_DEST_HIGH: destAddress = (uint16_t)(value << 8) | (destAddress & 0x00FF); break; case CTRL_DEST_HIGH: c->destAddress = (uint16_t)(value << 8) | (c->destAddress & 0x00FF); break;
case CTRL_DEST_LOW: destAddress = (destAddress & 0xFF00) | value; break; case CTRL_DEST_LOW: c->destAddress = (c->destAddress & 0xFF00) | value; break;
case CTRL_LENGTH_HIGH: length = (uint16_t)(value << 8) | (length & 0x00FF); break; case CTRL_LENGTH_HIGH: c->length = (uint16_t)(value << 8) | (c->length & 0x00FF); break;
case CTRL_LENGTH_LOW: length = (length & 0xFF00) | value; break; case CTRL_LENGTH_LOW: c->length = (c->length & 0xFF00) | value; break;
case CTRL_GUARD_BANK: guardBank = value; break; case CTRL_GUARD_BANK: c->guardBank = value; break;
case CTRL_GUARD_START_HIGH: guardStart = (uint16_t)(value << 8) | (guardStart & 0x00FF); break; case CTRL_GUARD_START_HIGH: c->guardStart = (uint16_t)(value << 8) | (c->guardStart & 0x00FF); break;
case CTRL_GUARD_START_LOW: guardStart = (guardStart & 0xFF00) | value; break; case CTRL_GUARD_START_LOW: c->guardStart = (c->guardStart & 0xFF00) | value; break;
case CTRL_GUARD_END_HIGH: guardEnd = (uint16_t)(value << 8) | (guardEnd & 0x00FF); break; case CTRL_GUARD_END_HIGH: c->guardEnd = (uint16_t)(value << 8) | (c->guardEnd & 0x00FF); break;
case CTRL_GUARD_END_LOW: guardEnd = (guardEnd & 0xFF00) | value; break; case CTRL_GUARD_END_LOW: c->guardEnd = (c->guardEnd & 0xFF00) | value; break;
case CTRL_DATA: case CTRL_DATA:
// A byte into the destination, and the address steps on so that writing a // 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. // run of bytes is a loop over one instruction rather than four.
if (canWrite(destBank, destAddress)) { if (canWrite(c, c->destBank, c->destAddress)) {
pendingCycles++; // The byte itself, beyond reaching the port. c->pendingCycles++; // The byte itself, beyond reaching the port.
banks[destBank].memory[destAddress] = value; c->banks[c->destBank].memory[c->destAddress] = value;
if (destBank == BANK_TABLE) { if (c->destBank == BANK_TABLE) {
// Unreachable while the table is read only, and here so that it stays // Unreachable while the table is read only, and here so that it stays
// true if that ever changes: the published bytes are a description, // true if that ever changes: the published bytes are a description,
// and nothing may write through them into a real bank. // and nothing may write through them into a real bank.
publishBank(BANK_TABLE); publishBank(c, BANK_TABLE);
} }
destAddress++; c->destAddress++;
status = 0; c->status = 0;
} }
break; break;
@@ -314,16 +292,16 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
// Both leave the addresses past whatever they touched and Length as it was, // 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. // so asking again carries straight on from where the last one stopped.
switch (value) { switch (value) {
case COMMAND_BLIT: doBlit(); break; case COMMAND_BLIT: doBlit(c); break;
case COMMAND_FILL: doFill(); break; case COMMAND_FILL: doFill(c); break;
case COMMAND_REGISTER_BANK: doRegisterBank(); break; case COMMAND_REGISTER_BANK: doRegisterBank(c); break;
case COMMAND_GUARD_ON: doGuardOn(); break; case COMMAND_GUARD_ON: doGuardOn(c); break;
case COMMAND_GUARD_OFF: doGuardOff(); break; case COMMAND_GUARD_OFF: doGuardOff(c); break;
default: default:
// Refusing an unknown command is better than ignoring it, since a // Refusing an unknown command is better than ignoring it, since a
// program that asked for something is entitled to find out that it // program that asked for something is entitled to find out that it
// did not happen. // did not happen.
refuse(VECTOR_BANK_FAULT); refuse(c, VECTOR_BANK_FAULT);
break; break;
} }
break; break;
@@ -335,33 +313,33 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
return 0; return 0;
} }
uint8_t controllerRead(uint8_t port) { uint8_t controllerRead(Controller *c, uint8_t port) {
switch (port) { switch (port) {
case CTRL_SOURCE_BANK: return sourceBank; case CTRL_SOURCE_BANK: return c->sourceBank;
case CTRL_SOURCE_HIGH: return (uint8_t)(sourceAddress >> 8); case CTRL_SOURCE_HIGH: return (uint8_t)(c->sourceAddress >> 8);
case CTRL_SOURCE_LOW: return (uint8_t)(sourceAddress & 0xFF); case CTRL_SOURCE_LOW: return (uint8_t)(c->sourceAddress & 0xFF);
case CTRL_DEST_BANK: return destBank; case CTRL_DEST_BANK: return c->destBank;
case CTRL_DEST_HIGH: return (uint8_t)(destAddress >> 8); case CTRL_DEST_HIGH: return (uint8_t)(c->destAddress >> 8);
case CTRL_DEST_LOW: return (uint8_t)(destAddress & 0xFF); case CTRL_DEST_LOW: return (uint8_t)(c->destAddress & 0xFF);
case CTRL_LENGTH_HIGH: return (uint8_t)(length >> 8); case CTRL_LENGTH_HIGH: return (uint8_t)(c->length >> 8);
case CTRL_LENGTH_LOW: return (uint8_t)(length & 0xFF); case CTRL_LENGTH_LOW: return (uint8_t)(c->length & 0xFF);
case CTRL_STATUS: return status; case CTRL_STATUS: return c->status;
case CTRL_GUARD_BANK: return guardBank; case CTRL_GUARD_BANK: return c->guardBank;
case CTRL_GUARD_START_HIGH: return (uint8_t)(guardStart >> 8); case CTRL_GUARD_START_HIGH: return (uint8_t)(c->guardStart >> 8);
case CTRL_GUARD_START_LOW: return (uint8_t)(guardStart & 0xFF); case CTRL_GUARD_START_LOW: return (uint8_t)(c->guardStart & 0xFF);
case CTRL_GUARD_END_HIGH: return (uint8_t)(guardEnd >> 8); case CTRL_GUARD_END_HIGH: return (uint8_t)(c->guardEnd >> 8);
case CTRL_GUARD_END_LOW: return (uint8_t)(guardEnd & 0xFF); case CTRL_GUARD_END_LOW: return (uint8_t)(c->guardEnd & 0xFF);
case CTRL_DATA: { case CTRL_DATA: {
// A byte out of the source, stepping on the same way a write does. // A byte out of the source, stepping on the same way a write does.
if (!canRead(sourceBank, sourceAddress)) { if (!canRead(c, c->sourceBank, c->sourceAddress)) {
return 0; return 0;
} }
pendingCycles++; // As above, the other way round. c->pendingCycles++; // As above, the other way round.
uint8_t value = banks[sourceBank].memory[sourceAddress]; uint8_t value = c->banks[c->sourceBank].memory[c->sourceAddress];
sourceAddress++; c->sourceAddress++;
status = 0; c->status = 0;
return value; return value;
} }
} }
+40 -4
View File
@@ -76,6 +76,17 @@
// a program that could write one would be setting a host address, which means nothing on // a program that could write one would be setting a host address, which means nothing on
// hardware and everything to the emulator running it. // hardware and everything to the emulator running it.
#define BANK_RECORD_BYTES 8 #define BANK_RECORD_BYTES 8
// What the controller knows about one bank. The memory pointer is never published - see the
// note above the record layout.
typedef struct {
uint8_t *memory;
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;
#define BANK_TABLE_BYTES (BANK_COUNT * BANK_RECORD_BYTES) #define BANK_TABLE_BYTES (BANK_COUNT * BANK_RECORD_BYTES)
#define BANK_FLAG_PRESENT 0x01 #define BANK_FLAG_PRESENT 0x01
@@ -107,12 +118,37 @@
// and waits for it. Whether real hardware would let the two run at once is a live question - // and waits for it. Whether real hardware would let the two run at once is a live question -
// the memories are separate, so it plausibly could - and the answer wants measuring before // the memories are separate, so it plausibly could - and the answer wants measuring before
// it is designed. // it is designed.
unsigned long controllerTakeCycles(void); // ---- One of these to a bus ----
//
// A controller is the most stateful thing on this machine: a source bank and address, a
// destination, a length, a command, five guard registers and a table describing every bank
// it can reach. TWO PROCESSORS SHARING ONE WOULD INTERLEAVE INTO NONSENSE - one sets a
// source, the other sets a destination, the first issues a blit and moves the wrong bytes
// somewhere else again. No amount of arbitration fixes that, because there is nothing to
// arbitrate: both writes were legal and the result belongs to neither of them.
//
// So a peripheral core gets its own, describing its own memories. The fields are here rather
// than hidden in the source file because a device that contains a core has to be able to hold
// one; nothing outside reaches into them.
typedef struct {
Bank banks[BANK_COUNT];
// Bank 2's contents: the description of every bank, for anything that wants to read it.
uint8_t bankTable[BANK_TABLE_BYTES];
// The registers, exactly as the ports name them.
uint8_t sourceBank, destBank, guardBank;
uint16_t sourceAddress, destAddress, length;
uint16_t guardStart, guardEnd;
uint8_t status;
// What the moves have cost since anybody last asked.
unsigned long pendingCycles;
} Controller;
void initializeController(uint8_t *programMemory, uint8_t *dataMemory); unsigned long controllerTakeCycles(Controller *c);
uint8_t controllerWrite(uint8_t value, uint8_t port); void initializeController(Controller *c, uint8_t *programMemory, uint8_t *dataMemory);
uint8_t controllerRead(uint8_t port); uint8_t controllerWrite(Controller *c, uint8_t value, uint8_t port);
uint8_t controllerRead(Controller *c, uint8_t port);
#endif // CONTROLLER_H #endif // CONTROLLER_H
+23 -3
View File
@@ -626,6 +626,26 @@ static uint8_t consoleStatus(void) {
// entire reason these are a struct rather than an array sitting here. // entire reason these are a struct rather than an array sitting here.
static InterruptLines machineLines; static InterruptLines machineLines;
// And its own controller, for the same reason. Everything on this bus that moves memory means
// this one.
static Controller theMachinesController;
Controller *machineController(void) {
return &theMachinesController;
}
static uint8_t machineControllerWrite(uint8_t value, uint8_t port) {
return controllerWrite(&theMachinesController, value, port);
}
static uint8_t machineControllerRead(uint8_t port) {
return controllerRead(&theMachinesController, port);
}
static unsigned long machineControllerCycles(void) {
return controllerTakeCycles(&theMachinesController);
}
// Whether somebody has asked the machine to start over, and taking that request away. // Whether somebody has asked the machine to start over, and taking that request away.
static int resetWanted = 0; static int resetWanted = 0;
@@ -659,7 +679,7 @@ int takeResetRequest(void) {
static const Bus theMachinesBus = { static const Bus theMachinesBus = {
OutputHandler, OutputHandler,
InputHandler, InputHandler,
controllerTakeCycles, machineControllerCycles,
takeIdleCycles, takeIdleCycles,
nextPendingInterrupt, nextPendingInterrupt,
clearInterrupt, clearInterrupt,
@@ -1001,7 +1021,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
refusedPort = Address; refusedPort = Address;
// The controller answers on a block of ports, which is a range rather than a list. // 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) { if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
return controllerWrite(DataByte, Address); return machineControllerWrite(DataByte, Address);
} }
// And so does the screen. // And so does the screen.
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) { if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
@@ -1096,7 +1116,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
uint8_t InputHandler(uint8_t Address) { uint8_t InputHandler(uint8_t Address) {
refusedPort = Address; refusedPort = Address;
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) { if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
return controllerRead(Address); return machineControllerRead(Address);
} }
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) { if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
return videoRead(Address); return videoRead(Address);
+5
View File
@@ -8,6 +8,7 @@
#include <stdint.h> #include <stdint.h>
#include "cpu.h" #include "cpu.h"
#include "controller.h"
// ---- Ports ---- // ---- Ports ----
// //
@@ -347,6 +348,10 @@ void linesClear(InterruptLines *lines, uint8_t port);
// what happens next by reading the port numbers. // what happens next by reading the port numbers.
int linesNext(const InterruptLines *lines); int linesNext(const InterruptLines *lines);
// The machine's own controller, which is what every device on this bus means when it moves
// memory. A peripheral core's device holds its own.
Controller *machineController(void);
// The machine's own lines, which is what every device on this bus means. // The machine's own lines, which is what every device on this bus means.
void raiseInterrupt(uint8_t port); void raiseInterrupt(uint8_t port);
+1 -1
View File
@@ -187,7 +187,7 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
consoleHome(); consoleHome();
// The controller has to know where the memories are before anything can reach // The controller has to know where the memories are before anything can reach
// them through it. Banks 0 and 1 are those two arrays. // them through it. Banks 0 and 1 are those two arrays.
initializeController(Program, Data); initializeController(machineController(), Program, Data);
initializeCPU(&m->cpu, Program, Data); initializeCPU(&m->cpu, Program, Data);
if (m->options.debug) { if (m->options.debug) {
printRegisters(&m->cpu, Program, Data); printRegisters(&m->cpu, Program, Data);