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
+40 -4
View File
@@ -76,6 +76,17 @@
// 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
// 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_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 -
// the memories are separate, so it plausibly could - and the answer wants measuring before
// 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