Files
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

145 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Checks what the memory controller charges for moving memory.
#
# EVERY OTHER TEST HERE IS BLIND TO THIS. run.sh strips the cycle count out of every
# recorded result on purpose, because a number that moves whenever anything changes turns
# real differences into a crowd of meaningless ones. The Test Manual says the other half of
# that bargain out loud: anything that genuinely wants to measure cycles has to say so in a
# test of its own. This is that test.
#
# What it pins is the RATE rather than a total. Each case runs twice, from programs whose
# instructions are identical except for the byte written to the Command port: once asking
# for the transfer, and once asking for GuardOff, which lowers a fence that was never raised
# and costs nothing beyond the port write. The difference between the two runs is therefore
# the transfer and nothing else - no instruction count, no setup, no startup.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUILD="$ROOT/Tests/build/cycles"
ASM="$ROOT/Assembler"
EMU="$ROOT/SplitBit"
for tool in "$ASM" "$EMU"; do
[ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; }
done
rm -rf "$BUILD"; mkdir -p "$BUILD"
PASS=0
FAIL=0
FAILED_NAMES=()
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
# A program that sets the controller up and then writes one byte to the Command port.
# The eight registers are written the same way every time, so two programs built from this
# differ by exactly one immediate.
program() {
# program <sourceBank> <srcHigh> <srcLow> <destBank> <dstHigh> <dstLow> <lenHigh> <lenLow> <command>
cat <<ASM
#Program
start:
INIA $1
OUTA 0xE0
INIA $2
OUTA 0xE1
INIA $3
OUTA 0xE2
INIA $4
OUTA 0xE3
INIA $5
OUTA 0xE4
INIA $6
OUTA 0xE5
INIA $7
OUTA 0xE6
INIA $8
OUTA 0xE7
INIA $9
OUTA 0xE8
HALT
#Vectors
Boot start
ASM
}
# Runs one program and says how many cycles the machine used.
cycles() {
local name="$1"; shift
program "$@" > "$BUILD/$name.asm"
"$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >/dev/null 2>&1 || {
echo " could not assemble $name"; return 1; }
"$EMU" --fast "$BUILD/$name.bin" 2>&1 | grep -oE 'after [0-9]+ cycles' | grep -oE '[0-9]+'
}
# The cost of one transfer: the same program asking for it, less the same program asking
# for GuardOff instead. 0x01 is Blit, 0x02 is Fill, 0x11 is GuardOff.
charged() {
# charged <name> <command> <sourceBank..lenLow>
local name="$1" command="$2"; shift 2
local withIt withoutIt
withIt="$(cycles "$name" "$@" "$command")" || return 1
withoutIt="$(cycles "$name-idle" "$@" 0x11)" || return 1
[ -n "$withIt" ] && [ -n "$withoutIt" ] || { echo " no cycle count for $name"; return 1; }
echo $((withIt - withoutIt))
}
check() {
# check <name> <expected> <command> <registers...>
local name="$1" expected="$2"; shift 2
local got
got="$(charged "$name" "$@")" || {
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %-34s could not measure it\n" "$RED" "$RESET" "$name"
return
}
if [ "$got" = "$expected" ]; then
PASS=$((PASS + 1))
printf " [%sok %s] %-34s %s cycles\n" "$GREEN" "$RESET" "$name" "$got"
else
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %-34s %s cycles, expected %s\n" "$RED" "$RESET" "$name" "$got" "$expected"
fi
}
echo "Checking what the memory controller charges."
# ---- Between two banks ----
#
# Data Memory to Program Memory, well above where the program itself sits and well below
# the vector table. 256 bytes: 128 words and the cycle the pipeline takes to fill.
check "256 bytes between banks" 129 0x01 0d1 0x10 0x00 0d0 0x80 0x00 0x01 0x00
# The same move with an odd source. Nothing about it can be paired, so it runs at the byte
# a cycle the machine had before the path was widened.
check "and again from an odd address" 257 0x01 0d1 0x10 0x01 0d0 0x80 0x00 0x01 0x00
# An odd destination is just as disqualifying, and so is an odd length: all three have to
# line up or none of it does.
check "an odd destination is the same" 257 0x01 0d1 0x10 0x00 0d0 0x80 0x01 0x01 0x00
check "so is an odd length" 256 0x01 0d1 0x10 0x00 0d0 0x80 0x00 0x00 0xFF
# ---- Within one bank ----
#
# One memory cannot overlap its own read and its own write, so it costs twice what the same
# move between two banks costs - widened or not.
check "256 bytes within one bank" 257 0x01 0d1 0x10 0x00 0d1 0x20 0x00 0x01 0x00
check "and misaligned, within one" 513 0x01 0d1 0x10 0x01 0d1 0x20 0x00 0x01 0x00
# ---- Filling ----
#
# Nothing to read, so it goes at the between-banks rate whatever the banks are. SourceLow
# carries the byte rather than an address, which is why only the destination and the length
# decide whether it can be paired.
check "256 bytes filled" 129 0x02 0d0 0x00 0xAA 0d1 0x30 0x00 0x01 0x00
check "and filled at an odd address" 257 0x02 0d0 0x00 0xAA 0d1 0x30 0x01 0x01 0x00
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS controller cost checks passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1