Files
SplitBit-Emulator/Source/Emulator/controller.h
T
AnachronautandClaude Opus 5 4c3eac8d9c Widen the memory controller's path to sixteen bits
The controller now reaches bank memory two bytes at a time, so a transfer whose source,
destination and length are all even moves two bytes a cycle between banks and one within
a bank - twice what each was. A 256 byte block between banks falls from 257 cycles to 129.

Alignment is required all three ways because a word is read at an even address and written
at an even address; an odd anything would mean shifting bytes across word boundaries to
line them up, which is a different design. A misaligned transfer falls back to the byte a
cycle it cost before, so nothing already written got slower.

THE CPU DOES NOT CHANGE. It still sees eight bits, a Data Pointer still addresses a byte,
and no instruction means anything different. This is a peripheral getting faster, which is
why it is worth doing now rather than after more is built on top of it.

The rule is deliberately visible rather than smoothed over: aligning a buffer costs nothing
and halves what moving it costs, and a cost a program cannot see is a cost it cannot avoid.

Tests/cycles.sh is new, and is the test the Test Manual has always said this kind of change
would need - run.sh strips the cycle count from every recorded result, so nothing else in
the suite can see any of this. It pins the RATE rather than a total: each case runs twice
from programs whose instructions are identical but for the byte written to the Command
port, once asking for the transfer and once for GuardOff, which costs nothing beyond the
port write. The difference is the transfer and nothing else. Verified by disabling the
widening, which failed exactly the three aligned cases and left the five misaligned ones
passing.

The Programming Manual gains a section saying what a transfer costs, which it never said at
all - it only promised a transfer does not wait, which is a different claim and could be
read as promising it is free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-28 21:30:12 -04:00

119 lines
4.6 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
#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.
unsigned long controllerTakeCycles(void);
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