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:
co-authored by
Claude Opus 5
parent
d6feddd1b6
commit
ff4b025058
@@ -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
|
||||
@@ -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/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
||||
| [`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/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
|
||||
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
|
||||
|
||||
@@ -48,6 +48,10 @@ static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
|
||||
cpu->busCycles++;
|
||||
uint8_t value = InputHandler(port);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+33
-2
@@ -341,6 +341,34 @@ static void consoleDraw(uint8_t byte) {
|
||||
// when one is typed, or -1 to say the window has gone.
|
||||
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 ----
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
if (got < 0) {
|
||||
// Nobody has typed yet, and the hook spent a frame keeping the window alive.
|
||||
consoleWaited();
|
||||
continue;
|
||||
}
|
||||
if (got == 0x08) {
|
||||
@@ -436,7 +466,9 @@ uint8_t consoleReadByte(void) {
|
||||
consoleEnded = 1;
|
||||
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;
|
||||
@@ -660,7 +692,6 @@ static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
||||
// 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
|
||||
// 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 unsigned long diskLatency = 0;
|
||||
|
||||
@@ -250,6 +250,18 @@ void consoleSetInputHook(int (*hook)(int mayWait));
|
||||
// display that refreshes, a port that waits on the host - wants exactly this shape.
|
||||
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
|
||||
// next instruction is, which is what this machine has always done and what every recorded
|
||||
// test assumes.
|
||||
|
||||
@@ -95,6 +95,16 @@ static int keyboardHook(int mayWait) {
|
||||
if (byte == EOF) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
|
||||
Three more registers, because that is how this machine talks to everything else.
|
||||
|
||||
@@ -78,7 +78,7 @@ from `make`, not from here.
|
||||
### 1. Recorded output
|
||||
|
||||
`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
|
||||
given at all.
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -44,6 +44,7 @@ printHello | Examples/printHello.asm | run | -
|
||||
8bitFibonacci | Examples/Fibonacci/8bitFibonacci.asm | run | - | -
|
||||
16bitFibonacci | Examples/Fibonacci/16bitFibonacci.asm | run | - | -
|
||||
32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | -
|
||||
colours | Examples/colours.asm | run | - | -
|
||||
8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | -
|
||||
16bitSegmentedSieve | Examples/primeSieve/16bitSegmentedSieve.asm | run | - | -
|
||||
# The four pointer rewrite. It emits exactly the same primes as the line above, which
|
||||
|
||||
+48
-1
@@ -109,13 +109,27 @@ say() {
|
||||
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.
|
||||
# A second argument names a keyboard file to feed it.
|
||||
run() {
|
||||
local name="$1"
|
||||
cat > "$BUILD/$name.asm"
|
||||
"$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; }
|
||||
"$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&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
|
||||
fi
|
||||
}
|
||||
|
||||
# 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 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.
|
||||
{ printf '#Program\nstart:\n'
|
||||
say "A"
|
||||
|
||||
Reference in New Issue
Block a user