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
This commit is contained in:
Anachronaut
2026-08-28 21:30:12 -04:00
co-authored by Claude Opus 5
parent c3c2451afe
commit 4c3eac8d9c
7 changed files with 234 additions and 17 deletions
+28 -5
View File
@@ -155,6 +155,24 @@ static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) {
// What the moves below have cost since anybody last asked.
static unsigned long pendingCycles = 0;
// ---- 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
// 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
// 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.
//
// Misaligned falls back to a byte a cycle, which is exactly what the machine did before it
// was widened, so nothing already written got slower.
//
// THE RULE IS VISIBLE ON PURPOSE. A program that cares can align what it moves, and a cost
// a program cannot see is a cost it cannot avoid. It is also the honest thing to model:
// hardware this shape really does behave this way.
static int wideRun(uint32_t addressesAndLength) {
return (addressesAndLength & 1u) == 0;
}
unsigned long controllerTakeCycles(void) {
unsigned long taken = pendingCycles;
pendingCycles = 0;
@@ -175,9 +193,13 @@ static void doBlit(void) {
// designing against.
memmove(banks[destBank].memory + destAddress,
banks[sourceBank].memory + sourceAddress, count);
// A byte read and a byte written. Two banks are two memories and the pair overlaps;
// A word read and a word written. Two banks are two memories and the pair overlaps;
// one bank is one memory and they do not. The odd cycle is the pipeline filling.
pendingCycles += (sourceBank == destBank) ? 2 * count + 1 : count + 1;
//
// Wide when everything is even, so an aligned move between banks settles at two bytes a
// 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;
pendingCycles += (sourceBank == destBank) ? 2 * moves + 1 : moves + 1;
sourceAddress = (uint16_t)(sourceAddress + count);
destAddress = (uint16_t)(destAddress + count);
status = 0;
@@ -188,9 +210,10 @@ static void doFill(void) {
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.
pendingCycles += count + 1;
// 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
// asks only about where the bytes are going and how many there are.
pendingCycles += (wideRun(destAddress | count) ? count / 2 : count) + 1;
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
destAddress = (uint16_t)(destAddress + count);
status = 0;