Make the cursor blink while the machine is waiting, and show how the palette works

THE CURSOR DID NOT BLINK, and the reason is worth stating: it blinks on the machine's clock,
and the machine's clock had stopped. A console waiting on a key stops the CPU, so no cycles
passed, so the phase never moved - and the one moment somebody is looking at a cursor is the
moment they are being asked to type.

Waiting is now charged as IDLE CYCLES, which is what they were built for: a machine stopped
on a device is not using memory, the same distinction WAIT makes, arrived at from the other
direction. And the devices are told as it happens rather than when the instruction finally
finishes, because a display controller does not stop blinking because the processor is
waiting on a keyboard, any more than a disk stops turning.

A keyboard file can now say NOTHING happened. A zero is a byte no keyboard sends, so it is
free to mean "a moment went by with nobody typing" - which is the commonest thing behind a
window and the only thing a file otherwise could not express. That unlocked the whole waiting
path: three checks that the cursor is lit, then dark half a second later, then lit again,
which is what blinking is.

And Programs/Examples/colours.asm, because the palette had nowhere a newcomer could read it.
It prints the sixteen pairs, prints each one again turned inside out, and then CHANGES ONE by
writing three bytes into the palette - so the difference between using the colours a machine
wakes up with and choosing your own is visible in one program. Its header explains what a
cell is, what the attribute nibble does, why palette entries are four bytes rather than
three, and why video memory has to be reached through the controller.

The manual now says where the palette lives and points at it.

SplitLint found a redundant RSTA in the example, which was worth acting on rather than
suppressing: the zero was already in A from the mode write two lines up, and saying so in a
comment teaches that SETD does not touch A, which is a thing worth knowing.

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 08:26:15 -04:00
co-authored by Claude Opus 5
parent d6feddd1b6
commit ff4b025058
11 changed files with 268 additions and 5 deletions
+142
View File
@@ -0,0 +1,142 @@
; colours.asm
; Every colour the machine wakes up with, and how to change one.
; Written by Anachronaut
;
; ---- What a colour is on this machine ----
;
; The screen draws CELLS, and a cell is two bytes: which tile, and an attribute. A tile is
; eight by eight pixels and every pixel is a byte - a number, not a colour. What colour that
; number means is looked up in the PALETTE, which is 256 entries of four bytes: red, green,
; blue, and one spare. Four rather than three so that entry n begins at n times four, which
; is a shift; three would need a multiply and this machine has none.
;
; A cell's attribute nibble is ADDED to every number in its tile, sixteen at a time. So the
; same tile drawn with attribute 0 reads palette entries 0 and 1, with attribute 1 it reads
; 16 and 17, and so on. Sixteen banks of sixteen.
;
; The console's glyphs are drawn in numbers 0 and 1 - paper and ink - so for text those
; sixteen banks are sixteen INK AND PAPER PAIRS. Writing the attribute register at port 0x06
; says which one to use.
;
; The palette a machine wakes up with is laid out so one bit inverts a pair:
;
; banks 0 to 7 a colour on black
; banks 8 to 15 the same colour AS the background, with black text on it
;
; So attribute XOR 8 highlights, which is all a cursor is.
;
; ---- Reaching the palette ----
;
; Video memory belongs to the screen, not to the program, so the CPU cannot write it with a
; store. It is reached the way every device's memory is reached: registered as a bank, and
; written through the memory controller. That is what the last part of this program does.
#Program
start:
; The console wants sixteen columns for the name and a bit more, so the wide screen is
; not needed. This is the mode the machine wakes up in and is here to be seen.
RSTA
OUTA 0x31
; ---- Sixteen pairs, one line each ----
;
; Counting in memory rather than in a register, because the loop below uses A and B for
; the arithmetic and there is nowhere else to keep it.
SETD.0 Bank
STA.0 ; Still the zero from the mode write above: SETD does not touch A
nextBank:
LDA.0
OUTA 0x06 ; Draw in this pair from now on
SETD.1 SampleText
RCAL say
; The same bank with bit 3 set, which is the same colour inside out.
LDA.0
INIB 0x08
XOR
MVQA
OUTA 0x06
SETD.1 HighlightText
RCAL say
RSTA
OUTA 0x06 ; Back to plain for the newline
INIA 0x0A
OUTA 0x00
LDA.0
INCA
STA.0
INIB 0d8 ; Eight banks; the other eight are their reverses
CCF
SUB
BNQ nextBank
; ---- And one written by hand ----
;
; Bank 2 is green when the machine starts. This makes its ink orange instead, by writing
; three bytes into the palette - which means reaching video memory, which means the
; controller.
; Give the screen's memory a bank number. The screen answers on port 0x30, and bank 3 is
; the first number software is allowed to hand out: 0, 1 and 2 belong to the machine.
INIA 0d3
OUTA 0xE3 ; DestBank: the number being given
INIA 0x30
OUTA 0xE2 ; SourceLow: the port that owns the memory
INIA 0x03
OUTA 0xE8 ; Command: RegisterBank
; The palette starts at 0xC000, and entry n is at n times four. Bank 2's ink is entry
; 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xC084.
INIA 0xC0
OUTA 0xE4 ; DestHigh
INIA 0x84
OUTA 0xE5 ; DestLow
; Writing the controller's Data port puts a byte at the destination and steps it on, so
; three writes are red, green and blue in order.
INIA 0xF0
OUTA 0xE9 ; red
INIA 0x80
OUTA 0xE9 ; green
INIA 0x20
OUTA 0xE9 ; blue
INIA 0x02
OUTA 0x06 ; That pair again, now that it has been changed
SETD.1 ChangedText
RCAL say
RSTA
OUTA 0x06
HALT
; DP1 names a string. Printing is one byte at a time out of port 0x00, which is the oldest
; thing on this machine and has never changed.
say:
LDA.1
BRA sayDone
OUTA 0x00
INCD.1
BRI say
sayDone:
RRET
#Data
Bank:
0x00
SampleText:
" ordinary "
HighlightText:
" highlighted "
ChangedText:
"
bank 2's ink is orange now, because this program said so
"
#Vectors
Boot start
+1 -1
View File
@@ -24,7 +24,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host | | [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem | | [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms | | [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life | | [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker | | [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of | | [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler | | [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
+4
View File
@@ -48,6 +48,10 @@ static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
cpu->busCycles++; cpu->busCycles++;
uint8_t value = InputHandler(port); uint8_t value = InputHandler(port);
cpu->busCycles += controllerTakeCycles(); cpu->busCycles += controllerTakeCycles();
// And whatever time went by while the device kept the machine waiting. Idle rather than
// bus, because a machine stopped on a port is not using memory - the same distinction
// WAIT makes, arrived at from the other direction.
cpu->idleCycles += takeIdleCycles();
return value; return value;
} }
+33 -2
View File
@@ -341,6 +341,34 @@ static void consoleDraw(uint8_t byte) {
// when one is typed, or -1 to say the window has gone. // when one is typed, or -1 to say the window has gone.
static int (*inputHook)(int mayWait) = NULL; static int (*inputHook)(int mayWait) = NULL;
// About one frame, which is how long a hook that presents takes to come back. It does not
// have to be exact - nothing is being measured, and the only thing downstream of it is a
// cursor blinking at somebody who is thinking about what to type.
#define CONSOLE_WAIT_CYCLES 16667
static unsigned long idleCycles = 0;
// The machine's clock as the devices last heard it. Kept here rather than passed about,
// because a device that has to know how long it has been waiting has to know what time it
// is now.
static unsigned long deviceNow = 0;
// A frame has gone by with nobody typing. TWO THINGS FOLLOW FROM THAT, and only doing the
// first is what left the cursor frozen: the CPU is told afterwards that it was stopped for a
// while, which is what idle cycles are - and the DEVICES are told now, because they are
// still running. A display controller blinking a cursor does not stop because the processor
// is waiting on a key, and neither does a disk finishing a read.
static void consoleWaited(void) {
idleCycles += CONSOLE_WAIT_CYCLES;
deviceTick(deviceNow + CONSOLE_WAIT_CYCLES);
}
unsigned long takeIdleCycles(void) {
const unsigned long taken = idleCycles;
idleCycles = 0;
return taken;
}
// ---- Line editing, which the terminal used to do ---- // ---- Line editing, which the terminal used to do ----
// //
// A TERMINAL IN LINE MODE DOES NOT HAND A PROGRAM EVERY KEYSTROKE. It collects a line, // A TERMINAL IN LINE MODE DOES NOT HAND A PROGRAM EVERY KEYSTROKE. It collects a line,
@@ -369,6 +397,8 @@ static int consoleGatherLine(void) {
return 0; return 0;
} }
if (got < 0) { if (got < 0) {
// Nobody has typed yet, and the hook spent a frame keeping the window alive.
consoleWaited();
continue; continue;
} }
if (got == 0x08) { if (got == 0x08) {
@@ -436,7 +466,9 @@ uint8_t consoleReadByte(void) {
consoleEnded = 1; consoleEnded = 1;
return 0xFF; return 0xFF;
} }
// Nothing typed yet. The hook kept the window alive; ask it again. // Nothing typed yet. The hook kept the window alive, which took a frame, and a
// frame of waiting is a frame of time passing.
consoleWaited();
} }
} }
unsigned char byte; unsigned char byte;
@@ -660,7 +692,6 @@ static uint8_t diskBuffer[DISK_BLOCK_BYTES];
// what a program that ignores the busy bit deserves to read. // what a program that ignores the busy bit deserves to read.
// The machine's clock as devices see it, which the emulator advances as the CPU spends // The machine's clock as devices see it, which the emulator advances as the CPU spends
// cycles. A device says when it will be finished in these, and is believed. // cycles. A device says when it will be finished in these, and is believed.
static unsigned long deviceNow = 0;
static void diskTransfer(uint8_t command); static void diskTransfer(uint8_t command);
static unsigned long diskLatency = 0; static unsigned long diskLatency = 0;
+12
View File
@@ -250,6 +250,18 @@ void consoleSetInputHook(int (*hook)(int mayWait));
// display that refreshes, a port that waits on the host - wants exactly this shape. // display that refreshes, a port that waits on the host - wants exactly this shape.
void deviceTick(unsigned long now); void deviceTick(unsigned long now);
// ---- Time that passed while the machine was stopped ----
//
// A console waiting on a key it has not been given has stopped the machine, and time is
// still going by: the cursor still blinks, a disk still turns. That is exactly what idle
// cycles are for, and without them the machine's clock froze the moment somebody was asked
// a question - so the cursor stopped blinking precisely when there was a person looking at
// it and waiting to type.
//
// Returned and cleared, the way the controller's cycles are, and picked up in the same
// place: after a port access, by the CPU that was stopped.
unsigned long takeIdleCycles(void);
// How many cycles a block read or write takes. Zero means the answer is there before the // How many cycles a block read or write takes. Zero means the answer is there before the
// next instruction is, which is what this machine has always done and what every recorded // next instruction is, which is what this machine has always done and what every recorded
// test assumes. // test assumes.
+10
View File
@@ -95,6 +95,16 @@ static int keyboardHook(int mayWait) {
if (byte == EOF) { if (byte == EOF) {
return CONSOLE_GONE; return CONSOLE_GONE;
} }
// ---- A zero is a moment of nobody typing ----
//
// The commonest thing that happens behind a window is NOTHING: sixty times a second the
// console asks and is told to come back later, and everything that goes on while that is
// true - the clock advancing, a cursor blinking, a disk finishing - was unreachable from
// here, because a file always has another byte. A zero is a byte no keyboard sends, so it
// is free to mean the one thing a file otherwise cannot say.
if (byte == 0x00) {
return CONSOLE_NOTHING_YET;
}
return byte & 0xFF; return byte & 0xFF;
} }
+4
View File
@@ -607,6 +607,10 @@ So `attribute XOR 8` turns any pair inside out, which is what a highlighted line
**That arrangement is a convention rather than a rule of the machine.** A program that wants different colours writes its own palette, and one that wants thirty-two of something rather than sixteen pairs can have that too - the device only ever adds the nibble and looks the answer up. **That arrangement is a convention rather than a rule of the machine.** A program that wants different colours writes its own palette, and one that wants thirty-two of something rather than sixteen pairs can have that too - the device only ever adds the nibble and looks the answer up.
The palette lives at 0xC000 in video memory, four bytes an entry - red, green, blue, and one spare - so entry *n* begins at 0xC000 plus *n* times four. Video memory belongs to the screen rather than to the program, so it is written the way every device's memory is written: registered as a bank, and reached through the memory controller.
`Programs/Examples/colours.asm` does all of that in eighty lines and prints the result. It shows the sixteen pairs, shows what XOR 8 does to each, and then changes one of them by writing three bytes into the palette, so that the difference between using the colours a machine wakes up with and choosing your own is visible in one program.
### Moving The Cursor: ### Moving The Cursor:
Three more registers, because that is how this machine talks to everything else. Three more registers, because that is how this machine talks to everything else.
+1 -1
View File
@@ -78,7 +78,7 @@ from `make`, not from here.
### 1. Recorded output ### 1. Recorded output
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
everything it printed against a file in `Tests/expected`. 166 tests, of which 104 run, 35 everything it printed against a file in `Tests/expected`. 167 tests, of which 105 run, 35
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
given at all. given at all.
+12
View File
@@ -0,0 +1,12 @@
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
ordinary highlighted
bank 2's ink is orange now, because this program said so
Execution halted.
[exit 0]
+1
View File
@@ -44,6 +44,7 @@ printHello | Examples/printHello.asm | run | -
8bitFibonacci | Examples/Fibonacci/8bitFibonacci.asm | run | - | - 8bitFibonacci | Examples/Fibonacci/8bitFibonacci.asm | run | - | -
16bitFibonacci | Examples/Fibonacci/16bitFibonacci.asm | run | - | - 16bitFibonacci | Examples/Fibonacci/16bitFibonacci.asm | run | - | -
32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | - 32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | -
colours | Examples/colours.asm | run | - | -
8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | - 8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | -
16bitSegmentedSieve | Examples/primeSieve/16bitSegmentedSieve.asm | run | - | - 16bitSegmentedSieve | Examples/primeSieve/16bitSegmentedSieve.asm | run | - | -
# The four pointer rewrite. It emits exactly the same primes as the line above, which # The four pointer rewrite. It emits exactly the same primes as the line above, which
+47
View File
@@ -109,13 +109,27 @@ say() {
done done
} }
# Waits, as a keyboard file: a zero is a moment of nobody typing, which is the commonest
# thing that happens behind a window and the only thing a file otherwise cannot say.
waiting() {
python3 -c "import sys; sys.stdout.buffer.write(b'\\x00' * int(sys.argv[1]) + b'x')" "$1" \
> "$BUILD/waits.keys"
echo "$BUILD/waits.keys"
}
# Assembles what is on standard input, runs it, and leaves the picture in $BUILD/<name>.ppm. # Assembles what is on standard input, runs it, and leaves the picture in $BUILD/<name>.ppm.
# A second argument names a keyboard file to feed it.
run() { run() {
local name="$1" local name="$1"
cat > "$BUILD/$name.asm" cat > "$BUILD/$name.asm"
"$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || { "$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || {
echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; } echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; }
if [ -n "${2:-}" ]; then
"$EMU" --fast --keyboard "$2" --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" \
> "$BUILD/$name.out" 2>&1
else
"$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1 "$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1
fi
} }
# One pixel out of a PPM, as "r,g,b". # One pixel out of a PPM, as "r,g,b".
@@ -478,6 +492,39 @@ coloured cursorblink 0 0 "0,0,0" \
&& result ok "the cursor blinks off again" "half a second later, dark" \ && result ok "the cursor blinks off again" "half a second later, dark" \
|| result no "the cursor blinks off again" "got $(pixel cursorblink 0 0)" || result no "the cursor blinks off again" "got $(pixel cursorblink 0 0)"
# ---- Blinking while the machine is stopped ----
#
# THE MACHINE IS NOT RUNNING while it waits for a key, and that is exactly when somebody is
# looking at the cursor. Time still has to reach the devices: a display controller does not
# stop blinking because the processor is waiting on a keyboard, any more than a disk stops
# turning. Waiting is charged as idle cycles and the devices are told as it happens, so the
# phase below is a pure function of how long nobody typed for.
#
# Key mode, so nothing is echoed and the cursor stays in the corner where it can be seen.
BLINKER='#Program
start:
INIA 0x05
OUTA 0x02
INA 0x00
HALT
#Vectors
Boot start'
echo "$BLINKER" | run blinkon "$(waiting 4)" || exit 1
coloured blinkon 0 0 "216,216,216" \
&& result ok "the cursor is lit while waiting" "sixty thousand cycles in" \
|| result no "the cursor is lit while waiting" "got $(pixel blinkon 0 0)"
echo "$BLINKER" | run blinkoff "$(waiting 40)" || exit 1
coloured blinkoff 0 0 "0,0,0" \
&& result ok "and dark half a second later" "the machine's clock, not the host's" \
|| result no "and dark half a second later" "got $(pixel blinkoff 0 0)"
echo "$BLINKER" | run blinkagain "$(waiting 70)" || exit 1
coloured blinkagain 0 0 "216,216,216" \
&& result ok "and lit again after that" "which is what blinking is" \
|| result no "and lit again after that" "got $(pixel blinkagain 0 0)"
# And what scrolled off the top is still in the map, which is scrollback nothing had to keep. # And what scrolled off the top is still in the map, which is scrollback nothing had to keep.
{ printf '#Program\nstart:\n' { printf '#Program\nstart:\n'
say "A" say "A"