Files
AnachronautandClaude Opus 5 33afc20abc 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
2026-08-29 17:32:10 -04:00

155 lines
6.2 KiB
C

// 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
// 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
#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
// ---- What the controller's work costs ----
//
// The controller moves memory, and memory takes time to move: a byte has to be read from
// somewhere and written somewhere else. A blit is not free just because the machine issues
// it with one instruction, and pretending otherwise made a quarter of a millisecond of
// work look like ten cycles.
//
// Banks are separate memories, which is what decides the rate. A move between two of them
// can overlap its read and its write - fetch the next word while the last one is stored -
// while a move WITHIN one bank cannot and costs twice as much. A fill has nothing to read
// and costs the same as a move between banks.
//
// AND THE PATH IS SIXTEEN BITS WIDE, so a transfer whose source, destination and length are
// all even moves two bytes a cycle between banks and one within a bank. Anything odd falls
// back to the byte a cycle this had before it was widened: lining up bytes across word
// boundaries is a second design, and this is not it. See wideRun in controller.c.
//
// Returned and cleared, so the caller adds it to whatever it is charging for. The CPU picks
// it up after each port access, which makes the transfer a stall: the machine issues a blit
// 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.
// ---- 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;
unsigned long controllerTakeCycles(Controller *c);
void initializeController(Controller *c, uint8_t *programMemory, uint8_t *dataMemory);
uint8_t controllerWrite(Controller *c, uint8_t value, uint8_t port);
uint8_t controllerRead(Controller *c, uint8_t port);
#endif // CONTROLLER_H