From 943c4018bed3b9e398737f24586c89c9ec1d441a Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:29:55 -0500 Subject: [PATCH 01/19] tools: the GEOS disk driver finds the TR's registers beside GeoRAM or beside an REU Under an REU the registers move to $DE00, since the REU's own repeat across $DF00-$DFFF. A copy of the driver lives on the user's disk, in CONFIGURE's 1581 driver, and outlives a change of Special IO, so it looks for the TRB id at $DF10 and then $DE10 on every transfer instead of being told the page when it is installed. $DF10 comes first because $DE10 is GeoRAM's window, which can hold any bytes; an REU's unused registers read $FF there. --- Source/Teensy/GEOS/TRDriver.h | 59 ++++++------ Source/Teensy/GEOS/TRDriver128.h | 58 ++++++------ tools/geos/trdrive-run.test.mjs | 151 +++++++++++++++++++++++++++++++ tools/geos/trdrive.asm | 59 +++++++++--- 4 files changed, 258 insertions(+), 69 deletions(-) create mode 100644 tools/geos/trdrive-run.test.mjs diff --git a/Source/Teensy/GEOS/TRDriver.h b/Source/Teensy/GEOS/TRDriver.h index ce4e66b3..fc5b13cb 100644 --- a/Source/Teensy/GEOS/TRDriver.h +++ b/Source/Teensy/GEOS/TRDriver.h @@ -9,45 +9,48 @@ #endif #define TR_DRIVER_BASE 0x9000 -static const uint8_t TRDriverCode[501] PROGMEM = { - 0x08, 0x68, 0x8d, 0xeb, 0x91, 0x78, 0xa5, 0x01, 0x8d, 0xec, 0x91, 0xa9, 0x36, 0x85, 0x01, 0xad, - 0x1a, 0xd0, 0x8d, 0xf4, 0x91, 0xa9, 0x00, 0x8d, 0x1a, 0xd0, 0xa9, 0x7f, 0x8d, 0x19, 0xd0, 0x8d, - 0x0d, 0xdc, 0x8d, 0x0d, 0xdd, 0x60, 0x78, 0xad, 0x0d, 0xdc, 0xad, 0x0d, 0xdd, 0xad, 0xf4, 0x91, - 0x8d, 0x1a, 0xd0, 0xad, 0xec, 0x91, 0x85, 0x01, 0xad, 0xeb, 0x91, 0x48, 0x28, 0x60, 0xa2, 0x00, +static const uint8_t TRDriverCode[543] PROGMEM = { + 0x08, 0x68, 0x8d, 0x15, 0x92, 0x78, 0xa5, 0x01, 0x8d, 0x16, 0x92, 0xa9, 0x36, 0x85, 0x01, 0xad, + 0x1a, 0xd0, 0x8d, 0x1e, 0x92, 0xa9, 0x00, 0x8d, 0x1a, 0xd0, 0xa9, 0x7f, 0x8d, 0x19, 0xd0, 0x8d, + 0x0d, 0xdc, 0x8d, 0x0d, 0xdd, 0x60, 0x78, 0xad, 0x0d, 0xdc, 0xad, 0x0d, 0xdd, 0xad, 0x1e, 0x92, + 0x8d, 0x1a, 0xd0, 0xad, 0x16, 0x92, 0x85, 0x01, 0xad, 0x15, 0x92, 0x48, 0x28, 0x60, 0xa2, 0x00, 0x8a, 0x60, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0x8d, 0x89, 0x84, 0x85, 0xba, 0xa2, 0x00, 0x60, 0x20, 0x9c, 0x90, 0xe0, 0x00, 0xd0, 0x03, 0x20, 0x88, 0x91, 0xa0, 0x00, 0x60, 0x20, 0x88, 0x91, 0x20, 0xd8, 0x90, 0x8a, 0x48, 0x20, 0x88, 0x91, 0x68, 0xaa, 0xa0, 0x00, 0x60, 0x20, 0xbb, 0x91, 0xb0, - 0x26, 0xa9, 0x01, 0x20, 0x15, 0x91, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, 0xa2, 0x0b, 0x20, 0xe4, - 0x91, 0xa2, 0x40, 0x20, 0xd5, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0xd5, 0x91, 0xc8, + 0x26, 0xa9, 0x01, 0x20, 0x15, 0x91, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, 0xa2, 0x0b, 0x20, 0x0e, + 0x92, 0xa2, 0x40, 0x20, 0xff, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0xff, 0x91, 0xc8, 0x91, 0x0a, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, 0x20, 0xbb, 0x91, 0xb0, - 0xf6, 0xa9, 0x01, 0x20, 0x15, 0x91, 0xe0, 0x00, 0xd0, 0x2d, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xf0, - 0x91, 0xad, 0xf0, 0x91, 0xa2, 0x0b, 0x20, 0xe4, 0x91, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0xdd, 0x40, - 0xdf, 0xd0, 0xf8, 0x91, 0x0a, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0xf0, 0x91, 0xad, 0xf0, - 0x91, 0xc9, 0x04, 0xd0, 0xdc, 0xa2, 0x00, 0x60, 0x20, 0xbb, 0x91, 0xb0, 0xba, 0xa0, 0x00, 0xa9, - 0x00, 0x8d, 0xf0, 0x91, 0xad, 0xf0, 0x91, 0xa2, 0x0b, 0x20, 0xe4, 0x91, 0xa2, 0x00, 0xb1, 0x0a, + 0xf6, 0xa9, 0x01, 0x20, 0x15, 0x91, 0xe0, 0x00, 0xd0, 0x2d, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0x1a, + 0x92, 0xad, 0x1a, 0x92, 0xa2, 0x0b, 0x20, 0x0e, 0x92, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0xdd, 0x40, + 0xdf, 0xd0, 0xf8, 0x91, 0x0a, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0x1a, 0x92, 0xad, 0x1a, + 0x92, 0xc9, 0x04, 0xd0, 0xdc, 0xa2, 0x00, 0x60, 0x20, 0xbb, 0x91, 0xb0, 0xba, 0xa0, 0x00, 0xa9, + 0x00, 0x8d, 0x1a, 0x92, 0xad, 0x1a, 0x92, 0xa2, 0x0b, 0x20, 0x0e, 0x92, 0xa2, 0x00, 0xb1, 0x0a, 0x9d, 0x40, 0xdf, 0xdd, 0x40, 0xdf, 0xf0, 0x05, 0xdd, 0x40, 0xdf, 0xd0, 0x15, 0xc8, 0xe8, 0xe0, - 0x40, 0xd0, 0xeb, 0xee, 0xf0, 0x91, 0xad, 0xf0, 0x91, 0xc9, 0x04, 0xd0, 0xd7, 0xa9, 0x02, 0x4c, - 0x15, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0xee, 0x91, 0xee, 0xf1, 0x91, 0xd0, 0x03, 0xee, 0xf1, 0x91, - 0xad, 0xf1, 0x91, 0xa2, 0x0c, 0x20, 0xe4, 0x91, 0xa5, 0x04, 0xa2, 0x09, 0x20, 0xe4, 0x91, 0xa5, - 0x05, 0xa2, 0x0a, 0x20, 0xe4, 0x91, 0xad, 0xee, 0x91, 0xa2, 0x08, 0x20, 0xe4, 0x91, 0xa9, 0x00, - 0x8d, 0xf2, 0x91, 0x8d, 0xf3, 0x91, 0xa2, 0x0d, 0x20, 0xd1, 0x91, 0x8d, 0xef, 0x91, 0x20, 0xd1, - 0x91, 0xcd, 0xef, 0x91, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0xf2, 0x91, 0xd0, 0xe7, 0xee, - 0xf3, 0x91, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0xd1, 0x91, 0xcd, 0xf1, 0x91, + 0x40, 0xd0, 0xeb, 0xee, 0x1a, 0x92, 0xad, 0x1a, 0x92, 0xc9, 0x04, 0xd0, 0xd7, 0xa9, 0x02, 0x4c, + 0x15, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0x18, 0x92, 0xee, 0x1b, 0x92, 0xd0, 0x03, 0xee, 0x1b, 0x92, + 0xad, 0x1b, 0x92, 0xa2, 0x0c, 0x20, 0x0e, 0x92, 0xa5, 0x04, 0xa2, 0x09, 0x20, 0x0e, 0x92, 0xa5, + 0x05, 0xa2, 0x0a, 0x20, 0x0e, 0x92, 0xad, 0x18, 0x92, 0xa2, 0x08, 0x20, 0x0e, 0x92, 0xa9, 0x00, + 0x8d, 0x1c, 0x92, 0x8d, 0x1d, 0x92, 0xa2, 0x0d, 0x20, 0xfb, 0x91, 0x8d, 0x19, 0x92, 0x20, 0xfb, + 0x91, 0xcd, 0x19, 0x92, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0x1c, 0x92, 0xd0, 0xe7, 0xee, + 0x1d, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0xfb, 0x91, 0xcd, 0x1b, 0x92, 0xf0, 0x04, 0x68, 0x4c, 0x5a, 0x91, 0x68, 0xc9, 0x5a, 0xf0, 0x07, 0xc9, 0xe2, 0xf0, 0x06, 0xa2, 0x0d, 0x60, 0xa2, 0x00, 0x60, 0xa2, 0x02, 0x60, 0xad, 0x4e, 0x90, 0xc9, 0x03, 0xd0, 0x2b, 0xa5, - 0x04, 0xc9, 0x28, 0xd0, 0x25, 0xa5, 0x05, 0xd0, 0x21, 0xa0, 0x04, 0xb1, 0x0a, 0x8d, 0xed, 0x91, - 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0xed, 0x91, 0x91, 0x0a, 0x98, 0x38, 0xe9, - 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xa0, 0x02, 0xb9, 0x10, 0xdf, - 0xd9, 0x10, 0xdf, 0xd0, 0xf8, 0xd9, 0xe8, 0x91, 0xd0, 0x05, 0x88, 0x10, 0xf0, 0x18, 0x60, 0x38, - 0x60, 0xbd, 0x00, 0xdf, 0x60, 0x20, 0xd1, 0x91, 0x8d, 0xef, 0x91, 0x20, 0xd1, 0x91, 0xcd, 0xef, - 0x91, 0xd0, 0xf2, 0x60, 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00 + 0x04, 0xc9, 0x28, 0xd0, 0x25, 0xa5, 0x05, 0xd0, 0x21, 0xa0, 0x04, 0xb1, 0x0a, 0x8d, 0x17, 0x92, + 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0x17, 0x92, 0x91, 0x0a, 0x98, 0x38, 0xe9, + 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xa2, 0xdf, 0x20, 0xdf, 0x91, + 0x90, 0x07, 0xa2, 0xde, 0x20, 0xdf, 0x91, 0xb0, 0x15, 0x8e, 0xfd, 0x91, 0x8e, 0x10, 0x92, 0x8e, + 0xbd, 0x90, 0x8e, 0xc0, 0x90, 0x8e, 0xf2, 0x90, 0x8e, 0xf5, 0x90, 0x8e, 0xfa, 0x90, 0x60, 0x8e, + 0xe9, 0x91, 0x8e, 0xec, 0x91, 0xa0, 0x02, 0xb9, 0x10, 0xdf, 0xd9, 0x10, 0xdf, 0xd0, 0xf8, 0xd9, + 0x12, 0x92, 0xd0, 0x05, 0x88, 0x10, 0xf0, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, 0xdf, 0x60, 0x20, + 0xfb, 0x91, 0x8d, 0x19, 0x92, 0x20, 0xfb, 0x91, 0xcd, 0x19, 0x92, 0xd0, 0xf2, 0x60, 0x9d, 0x00, + 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; -static const uint16_t TRDriverRelocations[57] PROGMEM = { +static const uint16_t TRDriverRelocations[68] PROGMEM = { 0x0003, 0x0009, 0x0013, 0x002e, 0x0034, 0x0039, 0x0050, 0x0057, 0x005d, 0x0060, 0x0065, 0x006d, 0x0074, 0x007f, 0x0084, 0x008d, 0x009d, 0x00a4, 0x00af, 0x00b2, 0x00b7, 0x00cc, 0x00cf, 0x00d9, 0x00e2, 0x00e5, 0x00ea, 0x0104, 0x0107, 0x0110, 0x0116, 0x0119, 0x011e, 0x0121, 0x0126, 0x012d, 0x0134, 0x0137, 0x013c, 0x0141, 0x0144, 0x0149, 0x014c, 0x014f, 0x0152, 0x015b, 0x0160, 0x016b, - 0x016e, 0x0174, 0x019e, 0x01a9, 0x01c6, 0x01d6, 0x01d9, 0x01dc, 0x01df + 0x016e, 0x0174, 0x019e, 0x01a9, 0x01be, 0x01c5, 0x01ca, 0x01cd, 0x01d0, 0x01d3, 0x01d6, 0x01d9, 0x01dc, 0x01e0, 0x01e3, 0x01f0, + 0x0200, 0x0203, 0x0206, 0x0209 }; static const uint16_t TRDriverEntries[13] PROGMEM = { 0x0000, 0x0026, 0x0041, 0x0041, 0x003e, 0x0047, 0x0042, 0x004f, 0x005c, 0x0042, 0x0042, 0x0042, 0x006c diff --git a/Source/Teensy/GEOS/TRDriver128.h b/Source/Teensy/GEOS/TRDriver128.h index 1d90f956..2b9bfdf9 100644 --- a/Source/Teensy/GEOS/TRDriver128.h +++ b/Source/Teensy/GEOS/TRDriver128.h @@ -9,46 +9,50 @@ #endif #define TR_DRIVER128_BASE 0x9000 -static const uint8_t TRDriver128Code[522] PROGMEM = { - 0x08, 0x68, 0x8d, 0xff, 0x91, 0x78, 0xad, 0x00, 0xff, 0x8d, 0x00, 0x92, 0xa9, 0x7e, 0x8d, 0x00, - 0xff, 0xad, 0x30, 0xd0, 0x8d, 0x01, 0x92, 0xa9, 0x00, 0x8d, 0x30, 0xd0, 0xad, 0x1a, 0xd0, 0x8d, - 0x09, 0x92, 0xa9, 0x00, 0x8d, 0x1a, 0xd0, 0xa9, 0x7f, 0x8d, 0x19, 0xd0, 0x8d, 0x0d, 0xdc, 0x8d, - 0x0d, 0xdd, 0x60, 0x78, 0xad, 0x0d, 0xdc, 0xad, 0x0d, 0xdd, 0xad, 0x09, 0x92, 0x8d, 0x1a, 0xd0, - 0xad, 0x01, 0x92, 0x8d, 0x30, 0xd0, 0xad, 0x00, 0x92, 0x8d, 0x00, 0xff, 0xad, 0xff, 0x91, 0x48, +static const uint8_t TRDriver128Code[564] PROGMEM = { + 0x08, 0x68, 0x8d, 0x29, 0x92, 0x78, 0xad, 0x00, 0xff, 0x8d, 0x2a, 0x92, 0xa9, 0x7e, 0x8d, 0x00, + 0xff, 0xad, 0x30, 0xd0, 0x8d, 0x2b, 0x92, 0xa9, 0x00, 0x8d, 0x30, 0xd0, 0xad, 0x1a, 0xd0, 0x8d, + 0x33, 0x92, 0xa9, 0x00, 0x8d, 0x1a, 0xd0, 0xa9, 0x7f, 0x8d, 0x19, 0xd0, 0x8d, 0x0d, 0xdc, 0x8d, + 0x0d, 0xdd, 0x60, 0x78, 0xad, 0x0d, 0xdc, 0xad, 0x0d, 0xdd, 0xad, 0x33, 0x92, 0x8d, 0x1a, 0xd0, + 0xad, 0x2b, 0x92, 0x8d, 0x30, 0xd0, 0xad, 0x2a, 0x92, 0x8d, 0x00, 0xff, 0xad, 0x29, 0x92, 0x48, 0x28, 0x60, 0xa2, 0x00, 0x8a, 0x60, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0x8d, 0x89, 0x84, 0x85, 0xba, 0xa2, 0x00, 0x60, 0x20, 0xb0, 0x90, 0xe0, 0x00, 0xd0, 0x03, 0x20, 0x9c, 0x91, 0xa0, 0x00, 0x60, 0x20, 0x9c, 0x91, 0x20, 0xec, 0x90, 0x8a, 0x48, 0x20, 0x9c, 0x91, 0x68, 0xaa, 0xa0, 0x00, 0x60, 0x20, 0xcf, 0x91, 0xb0, 0x26, 0xa9, 0x01, 0x20, 0x29, 0x91, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, - 0xa2, 0x0b, 0x20, 0xf8, 0x91, 0xa2, 0x40, 0x20, 0xe9, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, - 0x20, 0xe9, 0x91, 0xc8, 0x91, 0x0a, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, + 0xa2, 0x0b, 0x20, 0x22, 0x92, 0xa2, 0x40, 0x20, 0x13, 0x92, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, + 0x20, 0x13, 0x92, 0xc8, 0x91, 0x0a, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, 0x20, 0xcf, 0x91, 0xb0, 0xf6, 0xa9, 0x01, 0x20, 0x29, 0x91, 0xe0, 0x00, 0xd0, 0x2d, 0xa0, 0x00, - 0xa9, 0x00, 0x8d, 0x05, 0x92, 0xad, 0x05, 0x92, 0xa2, 0x0b, 0x20, 0xf8, 0x91, 0xa2, 0x00, 0xbd, + 0xa9, 0x00, 0x8d, 0x2f, 0x92, 0xad, 0x2f, 0x92, 0xa2, 0x0b, 0x20, 0x22, 0x92, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0xdd, 0x40, 0xdf, 0xd0, 0xf8, 0x91, 0x0a, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, - 0x05, 0x92, 0xad, 0x05, 0x92, 0xc9, 0x04, 0xd0, 0xdc, 0xa2, 0x00, 0x60, 0x20, 0xcf, 0x91, 0xb0, - 0xba, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0x05, 0x92, 0xad, 0x05, 0x92, 0xa2, 0x0b, 0x20, 0xf8, 0x91, + 0x2f, 0x92, 0xad, 0x2f, 0x92, 0xc9, 0x04, 0xd0, 0xdc, 0xa2, 0x00, 0x60, 0x20, 0xcf, 0x91, 0xb0, + 0xba, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0x2f, 0x92, 0xad, 0x2f, 0x92, 0xa2, 0x0b, 0x20, 0x22, 0x92, 0xa2, 0x00, 0xb1, 0x0a, 0x9d, 0x40, 0xdf, 0xdd, 0x40, 0xdf, 0xf0, 0x05, 0xdd, 0x40, 0xdf, 0xd0, - 0x15, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xeb, 0xee, 0x05, 0x92, 0xad, 0x05, 0x92, 0xc9, 0x04, 0xd0, - 0xd7, 0xa9, 0x02, 0x4c, 0x29, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0x03, 0x92, 0xee, 0x06, 0x92, 0xd0, - 0x03, 0xee, 0x06, 0x92, 0xad, 0x06, 0x92, 0xa2, 0x0c, 0x20, 0xf8, 0x91, 0xa5, 0x04, 0xa2, 0x09, - 0x20, 0xf8, 0x91, 0xa5, 0x05, 0xa2, 0x0a, 0x20, 0xf8, 0x91, 0xad, 0x03, 0x92, 0xa2, 0x08, 0x20, - 0xf8, 0x91, 0xa9, 0x00, 0x8d, 0x07, 0x92, 0x8d, 0x08, 0x92, 0xa2, 0x0d, 0x20, 0xe5, 0x91, 0x8d, - 0x04, 0x92, 0x20, 0xe5, 0x91, 0xcd, 0x04, 0x92, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0x07, - 0x92, 0xd0, 0xe7, 0xee, 0x08, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0xe5, - 0x91, 0xcd, 0x06, 0x92, 0xf0, 0x04, 0x68, 0x4c, 0x6e, 0x91, 0x68, 0xc9, 0x5a, 0xf0, 0x07, 0xc9, + 0x15, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xeb, 0xee, 0x2f, 0x92, 0xad, 0x2f, 0x92, 0xc9, 0x04, 0xd0, + 0xd7, 0xa9, 0x02, 0x4c, 0x29, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0x2d, 0x92, 0xee, 0x30, 0x92, 0xd0, + 0x03, 0xee, 0x30, 0x92, 0xad, 0x30, 0x92, 0xa2, 0x0c, 0x20, 0x22, 0x92, 0xa5, 0x04, 0xa2, 0x09, + 0x20, 0x22, 0x92, 0xa5, 0x05, 0xa2, 0x0a, 0x20, 0x22, 0x92, 0xad, 0x2d, 0x92, 0xa2, 0x08, 0x20, + 0x22, 0x92, 0xa9, 0x00, 0x8d, 0x31, 0x92, 0x8d, 0x32, 0x92, 0xa2, 0x0d, 0x20, 0x0f, 0x92, 0x8d, + 0x2e, 0x92, 0x20, 0x0f, 0x92, 0xcd, 0x2e, 0x92, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0x31, + 0x92, 0xd0, 0xe7, 0xee, 0x32, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0x0f, + 0x92, 0xcd, 0x30, 0x92, 0xf0, 0x04, 0x68, 0x4c, 0x6e, 0x91, 0x68, 0xc9, 0x5a, 0xf0, 0x07, 0xc9, 0xe2, 0xf0, 0x06, 0xa2, 0x0d, 0x60, 0xa2, 0x00, 0x60, 0xa2, 0x02, 0x60, 0xad, 0x4e, 0x90, 0xc9, 0x03, 0xd0, 0x2b, 0xa5, 0x04, 0xc9, 0x28, 0xd0, 0x25, 0xa5, 0x05, 0xd0, 0x21, 0xa0, 0x04, 0xb1, - 0x0a, 0x8d, 0x02, 0x92, 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0x02, 0x92, 0x91, - 0x0a, 0x98, 0x38, 0xe9, 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xa0, - 0x02, 0xb9, 0x10, 0xdf, 0xd9, 0x10, 0xdf, 0xd0, 0xf8, 0xd9, 0xfc, 0x91, 0xd0, 0x05, 0x88, 0x10, - 0xf0, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, 0xdf, 0x60, 0x20, 0xe5, 0x91, 0x8d, 0x04, 0x92, 0x20, - 0xe5, 0x91, 0xcd, 0x04, 0x92, 0xd0, 0xf2, 0x60, 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + 0x0a, 0x8d, 0x2c, 0x92, 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0x2c, 0x92, 0x91, + 0x0a, 0x98, 0x38, 0xe9, 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xa2, + 0xdf, 0x20, 0xf3, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, 0xf3, 0x91, 0xb0, 0x15, 0x8e, 0x11, 0x92, + 0x8e, 0x24, 0x92, 0x8e, 0xd1, 0x90, 0x8e, 0xd4, 0x90, 0x8e, 0x06, 0x91, 0x8e, 0x09, 0x91, 0x8e, + 0x0e, 0x91, 0x60, 0x8e, 0xfd, 0x91, 0x8e, 0x00, 0x92, 0xa0, 0x02, 0xb9, 0x10, 0xdf, 0xd9, 0x10, + 0xdf, 0xd0, 0xf8, 0xd9, 0x26, 0x92, 0xd0, 0x05, 0x88, 0x10, 0xf0, 0x18, 0x60, 0x38, 0x60, 0xbd, + 0x00, 0xdf, 0x60, 0x20, 0x0f, 0x92, 0x8d, 0x2e, 0x92, 0x20, 0x0f, 0x92, 0xcd, 0x2e, 0x92, 0xd0, + 0xf2, 0x60, 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; -static const uint16_t TRDriver128Relocations[59] PROGMEM = { +static const uint16_t TRDriver128Relocations[70] PROGMEM = { 0x0003, 0x000a, 0x0015, 0x0020, 0x003b, 0x0041, 0x0047, 0x004d, 0x0064, 0x006b, 0x0071, 0x0074, 0x0079, 0x0081, 0x0088, 0x0093, 0x0098, 0x00a1, 0x00b1, 0x00b8, 0x00c3, 0x00c6, 0x00cb, 0x00e0, 0x00e3, 0x00ed, 0x00f6, 0x00f9, 0x00fe, 0x0118, 0x011b, 0x0124, 0x012a, 0x012d, 0x0132, 0x0135, 0x013a, 0x0141, 0x0148, 0x014b, 0x0150, 0x0155, 0x0158, 0x015d, 0x0160, 0x0163, 0x0166, 0x016f, - 0x0174, 0x017f, 0x0182, 0x0188, 0x01b2, 0x01bd, 0x01da, 0x01ea, 0x01ed, 0x01f0, 0x01f3 + 0x0174, 0x017f, 0x0182, 0x0188, 0x01b2, 0x01bd, 0x01d2, 0x01d9, 0x01de, 0x01e1, 0x01e4, 0x01e7, 0x01ea, 0x01ed, 0x01f0, 0x01f4, + 0x01f7, 0x0204, 0x0214, 0x0217, 0x021a, 0x021d }; static const uint16_t TRDriver128Entries[13] PROGMEM = { 0x0000, 0x0033, 0x0055, 0x0055, 0x0052, 0x005b, 0x0056, 0x0063, 0x0070, 0x0056, 0x0056, 0x0056, 0x0080 diff --git a/tools/geos/trdrive-run.test.mjs b/tools/geos/trdrive-run.test.mjs new file mode 100644 index 00000000..d8b19042 --- /dev/null +++ b/tools/geos/trdrive-run.test.mjs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Mos6502 } from '../lib/mos6502.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const GEOS_DIR = path.join(root, 'Source/Teensy/GEOS'); + +const ENTRY = { ReadBlock: 7, WriteBlock: 8 }; +const R1 = 0x04; +const R4 = 0x0a; +const BUFFER = 0x8000; +const CALLER = 0x0200; +const ERR_DEV_NOT_FOUND = 13; +const BESIDE_GEORAM = 0xdf00; +const BESIDE_REU = 0xde00; +const REU_REGISTERS = 0x0b; +const REU_GHOSTS = 0x1f; +const PAGE = 256; +const QUARTER = 64; + +function readDriver(header, prefix) { + const text = fs.readFileSync(path.join(GEOS_DIR, header), 'utf8'); + const array = (name) => [...text.match(new RegExp(`${name}\\[\\d+\\] PROGMEM = \\{([^}]*)\\}`))[1].matchAll(/0x[0-9a-f]+/g)].map((m) => parseInt(m[0], 16)); + const base = parseInt(text.match(/#define TR_DRIVER\w*_BASE (0x[0-9a-f]+)/)[1], 16); + return { base, code: array(`${prefix}Code`), entries: array(`${prefix}Entries`) }; +} + +const DRIVERS = [ + ['GEOS 64', readDriver('TRDriver.h', 'TRDriver')], + ['GEOS 128', readDriver('TRDriver128.h', 'TRDriver128')], +]; + +// The TR block device (docs/TR_Block_Device.md) answering READ and WRITE at once, its blocks keyed by ADDR1:ADDR0 +function blockDevice(blocks) { + const regs = { addr0: 0, addr1: 0, page: 0, token: 0 }; + let buffer = new Uint8Array(PAGE), status = 0, echo = 0; + return { + write(offset, value) { + if (offset >= 0x40 && offset < 0x80) buffer[regs.page * QUARTER + offset - 0x40] = value; + else if (offset === 0x09) regs.addr0 = value; + else if (offset === 0x0a) regs.addr1 = value; + else if (offset === 0x0b) regs.page = value & 3; + else if (offset === 0x0c) regs.token = value; + else if (offset === 0x08) { + const key = (regs.addr1 << 8) | regs.addr0; + if (value === 1) buffer = Uint8Array.from(blocks.get(key) ?? new Uint8Array(PAGE)); + if (value === 2) blocks.set(key, Uint8Array.from(buffer)); + status = 0x5a; + echo = regs.token; + } + }, + read(offset) { + if (offset >= 0x40 && offset < 0x80) return buffer[regs.page * QUARTER + offset - 0x40]; + if (offset === 0x0d) return status; + if (offset === 0x0e) return echo; + if (offset >= 0x10 && offset < 0x13) return 'TRB'.charCodeAt(offset - 0x10); + return 0; + }, + }; +} + +// What answers at $DF00 beside an REU: its registers, holding what a program left in them, ghosted every 32 bytes, +// the unused ones reading $FF +function reuRegisters(regs) { + return { + write() {}, + read: (offset) => ((offset & REU_GHOSTS) < REU_REGISTERS ? regs[offset & REU_GHOSTS] : 0xff), + }; +} + +// GeoRAM's window: RAM, holding whatever the page it shows holds +function geoRamWindow(bytes) { + return { write: (offset, value) => (bytes[offset] = value), read: (offset) => bytes[offset] }; +} + +function machine(driver, pages) { + const ram = new Uint8Array(0x10000); + const ioWrites = []; + ram.set(driver.code, driver.base); + const bus = { + read: (address) => (pages[address & 0xff00] ? pages[address & 0xff00].read(address & 0xff) : ram[address]), + write(address, value) { + const page = pages[address & 0xff00]; + if (!page) ram[address] = value; + else { + ioWrites.push(address); + page.write(address & 0xff, value); + } + }, + }; + return { ram, ioWrites, pages, driver, bus }; +} + +function call(m, entry, track, sector) { + const cpu = new Mos6502(m.bus); + const at = m.driver.base + m.driver.entries[entry]; + m.ram.set([0x20, at & 0xff, at >> 8], CALLER); + m.ram.set([track, sector], R1); + m.ram.set([BUFFER & 0xff, BUFFER >> 8], R4); + cpu.pc = CALLER; + cpu.run((c) => c.pc === CALLER + 3); + return cpu.x; +} + +const sectorOf = (seed) => Uint8Array.from({ length: PAGE }, (_, i) => (i * 13 + seed) & 0xff); +const key = (track, sector) => (sector << 8) | track; + +for (const [name, driver] of DRIVERS) { + test(`${name}: the driver reads through the registers beside GeoRAM, not the GeoRAM window that shows "TRB" too`, () => { + const window = new Uint8Array(PAGE); + window.set([...'TRB'].map((c) => c.charCodeAt(0)), 0x10); + const m = machine(driver, {}); + m.pages[BESIDE_GEORAM] = blockDevice(new Map([[key(1, 2), sectorOf(1)]])); + m.pages[BESIDE_REU] = geoRamWindow(window); + assert.equal(call(m, ENTRY.ReadBlock, 1, 2), 0); + assert.deepEqual(m.ram.subarray(BUFFER, BUFFER + PAGE), sectorOf(1)); + assert.deepEqual(m.ioWrites.filter((a) => a < BESIDE_GEORAM), [], 'nothing written to the GeoRAM window'); + }); + + test(`${name}: the driver reads and writes through the registers beside an REU, whatever the REU's own registers hold`, () => { + const blocks = new Map([[key(3, 4), sectorOf(2)]]); + const m = machine(driver, {}); + m.pages[BESIDE_REU] = blockDevice(blocks); + m.pages[BESIDE_GEORAM] = reuRegisters([0x10, 0x00, 0x54, 0x52, 0x42, 0x54, 0x52, 0x42, 0, 0, 0]); + assert.equal(call(m, ENTRY.ReadBlock, 3, 4), 0); + assert.deepEqual(m.ram.subarray(BUFFER, BUFFER + PAGE), sectorOf(2)); + + m.ram.set(sectorOf(3), BUFFER); + assert.equal(call(m, ENTRY.WriteBlock, 5, 6), 0); + assert.deepEqual(blocks.get(key(5, 6)), sectorOf(3)); + assert.deepEqual(m.ioWrites.filter((a) => a >= BESIDE_GEORAM), [], 'nothing written to the REU'); + }); + + test(`${name}: the driver finds the registers again when they move, and reports a missing device`, () => { + const m = machine(driver, {}); + m.pages[BESIDE_GEORAM] = blockDevice(new Map([[key(1, 0), sectorOf(4)]])); + assert.equal(call(m, ENTRY.ReadBlock, 1, 0), 0); + + m.pages[BESIDE_GEORAM] = reuRegisters(new Array(REU_REGISTERS).fill(0)); + m.pages[BESIDE_REU] = blockDevice(new Map([[key(1, 0), sectorOf(5)]])); + assert.equal(call(m, ENTRY.ReadBlock, 1, 0), 0); + assert.deepEqual(m.ram.subarray(BUFFER, BUFFER + PAGE), sectorOf(5)); + + m.pages[BESIDE_REU] = reuRegisters(new Array(REU_REGISTERS).fill(0)); + assert.equal(call(m, ENTRY.ReadBlock, 1, 0), ERR_DEV_NOT_FOUND); + }); +} diff --git a/tools/geos/trdrive.asm b/tools/geos/trdrive.asm index a2d74e03..32017c2d 100644 --- a/tools/geos/trdrive.asm +++ b/tools/geos/trdrive.asm @@ -17,7 +17,7 @@ HEADER_SWAP = $8c HEADER_FIRST = 4 HEADER_END = $1d -; offsets from BASE_GEOS_RAM's page +; offsets from the registers' page REG_CMD = $08 REG_ADDR0 = $09 REG_ADDR1 = $0a @@ -51,7 +51,10 @@ IO_IN_BANK_1 = $7e VIC_CLOCK = $d030 CLOCK_1MHZ = 0 -BASE_GEOS_RAM = $df +; beside GeoRAM, and beside the REU, whose unused registers read $FF across $DF0B-$DF1F +PAGE_BESIDE_GEORAM = $df +PAGE_BESIDE_REU = $de +REG_ID = $10 * = ORG @@ -189,8 +192,10 @@ readPage jsr putReg ldx #0 windowRead - lda BASE_GEOS_RAM * 256 + WINDOW,x - cmp BASE_GEOS_RAM * 256 + WINDOW,x +windowLoad + lda PAGE_BESIDE_GEORAM * 256 + WINDOW,x +windowCheck + cmp PAGE_BESIDE_GEORAM * 256 + WINDOW,x bne windowRead sta (r4),y iny @@ -218,10 +223,13 @@ writePage ldx #0 windowWrite lda (r4),y - sta BASE_GEOS_RAM * 256 + WINDOW,x - cmp BASE_GEOS_RAM * 256 + WINDOW,x +windowStore + sta PAGE_BESIDE_GEORAM * 256 + WINDOW,x +windowVerify + cmp PAGE_BESIDE_GEORAM * 256 + WINDOW,x beq windowStored - cmp BASE_GEOS_RAM * 256 + WINDOW,x +windowRetry + cmp PAGE_BESIDE_GEORAM * 256 + WINDOW,x bne windowFault windowStored iny @@ -333,25 +341,48 @@ swapField swapped rts -; carry clear when the TRB id answers +; carry clear when the TRB id answers on either page, with every register access pointed at that page findDevice + ldx #PAGE_BESIDE_GEORAM + jsr hasId + bcc usePage + ldx #PAGE_BESIDE_REU + jsr hasId + bcs absent +usePage + stx getReg + 2 + stx putReg + 2 + stx windowLoad + 2 + stx windowCheck + 2 + stx windowStore + 2 + stx windowVerify + 2 + stx windowRetry + 2 +absent + rts + +; X = page; carry clear when the TRB id reads there +hasId + stx idLoad + 2 + stx idCheck + 2 ldy #2 probe - lda BASE_GEOS_RAM * 256 + $10,y - cmp BASE_GEOS_RAM * 256 + $10,y +idLoad + lda PAGE_BESIDE_GEORAM * 256 + REG_ID,y +idCheck + cmp PAGE_BESIDE_GEORAM * 256 + REG_ID,y bne probe cmp trbId,y - bne absent + bne notHere dey bpl probe clc rts -absent +notHere sec rts getReg - lda BASE_GEOS_RAM * 256,x + lda PAGE_BESIDE_GEORAM * 256,x rts ; a read that comes back the same twice; on some machines the TR's byte arrives with the last one's bits ORed in @@ -364,7 +395,7 @@ getStable rts putReg - sta BASE_GEOS_RAM * 256,x + sta PAGE_BESIDE_GEORAM * 256,x rts trbId !byte $54, $52, $42 From d1dceec28a8ecb60553cb1787defc76b944237c3 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:31:21 -0500 Subject: [PATCH 02/19] tools: the GEOS mouse driver finds the TR's registers beside GeoRAM or beside an REU Beside an REU, $DF03-$DF05 are its address registers, which hold whatever the last transfer left and so can read "TRM". The block device's TRB id at $DF10 picks the page instead: an REU's unused registers read $FF there, and GeoRAM's window at $DE10 is never asked. --- Source/Teensy/GEOS/TRMouse128.h | 33 +++++++------ Source/Teensy/GEOS/TRMouse64.h | 32 +++++++------ tools/geos/trmouse-run.test.mjs | 83 +++++++++++++++++++++++++++++++++ tools/geos/trmouse128.asm | 42 ++++++++++++----- 4 files changed, 148 insertions(+), 42 deletions(-) create mode 100644 tools/geos/trmouse-run.test.mjs diff --git a/Source/Teensy/GEOS/TRMouse128.h b/Source/Teensy/GEOS/TRMouse128.h index 52c27ba1..a457c517 100644 --- a/Source/Teensy/GEOS/TRMouse128.h +++ b/Source/Teensy/GEOS/TRMouse128.h @@ -9,27 +9,30 @@ #endif static const char TRMouse128Name[] PROGMEM = "TR USB MOUSE"; -static const uint8_t TRMouse128Code[207] PROGMEM = { - 0x4c, 0x10, 0xfd, 0x4c, 0x1a, 0xfd, 0x4c, 0x2f, 0xfd, 0x4c, 0x1a, 0xfd, 0x80, 0x00, 0x00, 0x00, - 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa2, 0x02, 0xbd, 0x03, 0xdf, - 0xdd, 0x2c, 0xfd, 0xd0, 0x05, 0xca, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0x54, 0x52, 0x4d, 0x20, - 0x1b, 0xfd, 0xb0, 0x10, 0xad, 0x00, 0xdf, 0x8d, 0x0d, 0xfd, 0xad, 0x01, 0xdf, 0x8d, 0x0f, 0xfd, - 0x24, 0x30, 0x30, 0x01, 0x60, 0xad, 0x02, 0xdf, 0x29, 0x01, 0xf0, 0x04, 0xa9, 0x00, 0xf0, 0x02, - 0xa9, 0x80, 0xcd, 0x0c, 0xfd, 0xf0, 0x0c, 0x8d, 0x0c, 0xfd, 0x8d, 0x05, 0x85, 0xa5, 0x39, 0x09, - 0x20, 0x85, 0x39, 0xa2, 0x00, 0xad, 0x0d, 0xfd, 0x10, 0x01, 0xca, 0x8e, 0x0e, 0xfd, 0x24, 0x3f, - 0x10, 0x06, 0x0e, 0x0d, 0xfd, 0x2e, 0x0e, 0xfd, 0x18, 0xa5, 0x3a, 0x6d, 0x0d, 0xfd, 0x85, 0x3a, - 0xa5, 0x3b, 0x6d, 0x0e, 0xfd, 0x85, 0x3b, 0xad, 0x0f, 0xfd, 0x18, 0x65, 0x3c, 0x2c, 0x0f, 0xfd, - 0x30, 0x0a, 0xb0, 0x04, 0xc9, 0xe4, 0x90, 0x08, 0xa9, 0xe3, 0xd0, 0x04, 0xb0, 0x02, 0xa9, 0xff, - 0x85, 0x3c, 0xa2, 0x01, 0xad, 0x0e, 0xfd, 0x30, 0x08, 0x0d, 0x0d, 0xfd, 0xf0, 0x04, 0xe8, 0xd0, - 0x01, 0xca, 0xad, 0x0f, 0xfd, 0xf0, 0x05, 0x30, 0x06, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xbd, - 0xc6, 0xfd, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, 0x06, 0x07 +static const uint8_t TRMouse128Code[241] PROGMEM = { + 0x4c, 0x10, 0xfd, 0x4c, 0x1a, 0xfd, 0x4c, 0x51, 0xfd, 0x4c, 0x1a, 0xfd, 0x80, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa2, 0xdf, 0xa0, 0x02, 0xb9, + 0x10, 0xdf, 0xd9, 0x4e, 0xfd, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x30, 0x02, 0xa2, 0xde, 0x8e, 0x3e, + 0xfd, 0x8e, 0x58, 0xfd, 0x8e, 0x5e, 0xfd, 0x8e, 0x69, 0xfd, 0xa0, 0x02, 0xb9, 0x03, 0xdf, 0xd9, + 0x4b, 0xfd, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0x54, 0x52, 0x4d, 0x54, 0x52, + 0x42, 0x20, 0x1b, 0xfd, 0xb0, 0x10, 0xad, 0x00, 0xdf, 0x8d, 0x0d, 0xfd, 0xad, 0x01, 0xdf, 0x8d, + 0x0f, 0xfd, 0x24, 0x30, 0x30, 0x01, 0x60, 0xad, 0x02, 0xdf, 0x29, 0x01, 0xf0, 0x04, 0xa9, 0x00, + 0xf0, 0x02, 0xa9, 0x80, 0xcd, 0x0c, 0xfd, 0xf0, 0x0c, 0x8d, 0x0c, 0xfd, 0x8d, 0x05, 0x85, 0xa5, + 0x39, 0x09, 0x20, 0x85, 0x39, 0xa2, 0x00, 0xad, 0x0d, 0xfd, 0x10, 0x01, 0xca, 0x8e, 0x0e, 0xfd, + 0x24, 0x3f, 0x10, 0x06, 0x0e, 0x0d, 0xfd, 0x2e, 0x0e, 0xfd, 0x18, 0xa5, 0x3a, 0x6d, 0x0d, 0xfd, + 0x85, 0x3a, 0xa5, 0x3b, 0x6d, 0x0e, 0xfd, 0x85, 0x3b, 0xad, 0x0f, 0xfd, 0x18, 0x65, 0x3c, 0x2c, + 0x0f, 0xfd, 0x30, 0x0a, 0xb0, 0x04, 0xc9, 0xe4, 0x90, 0x08, 0xa9, 0xe3, 0xd0, 0x04, 0xb0, 0x02, + 0xa9, 0xff, 0x85, 0x3c, 0xa2, 0x01, 0xad, 0x0e, 0xfd, 0x30, 0x08, 0x0d, 0x0d, 0xfd, 0xf0, 0x04, + 0xe8, 0xd0, 0x01, 0xca, 0xad, 0x0f, 0xfd, 0xf0, 0x05, 0x30, 0x06, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, + 0xe8, 0xbd, 0xe8, 0xfd, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, 0x06, + 0x07 }; static const uint8_t TRMouse128Info[256] PROGMEM = { 0x00, 0xff, 0x03, 0x15, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x01, 0x91, 0x80, 0x02, 0x10, 0x40, 0x04, 0x10, 0x20, 0x04, 0x10, 0x20, 0x04, 0x10, 0x20, 0x07, 0xff, 0xe0, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x02, 0x00, 0x40, 0x01, 0x81, 0x80, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xf7, 0x80, 0x04, 0x94, - 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0f, 0x00, 0x00, 0xfd, 0xcf, 0xfd, 0x00, 0x00, 0x54, 0x52, 0x20, + 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0f, 0x00, 0x00, 0xfd, 0xf1, 0xfd, 0x00, 0x00, 0x54, 0x52, 0x20, 0x55, 0x53, 0x42, 0x20, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x56, 0x31, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x00, 0x54, 0x65, 0x65, 0x6e, 0x73, 0x79, 0x52, 0x4f, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/Source/Teensy/GEOS/TRMouse64.h b/Source/Teensy/GEOS/TRMouse64.h index b68cc617..12844049 100644 --- a/Source/Teensy/GEOS/TRMouse64.h +++ b/Source/Teensy/GEOS/TRMouse64.h @@ -9,27 +9,29 @@ #endif static const char TRMouse64Name[] PROGMEM = "TR USB MOUSE"; -static const uint8_t TRMouse64Code[197] PROGMEM = { - 0x4c, 0x90, 0xfe, 0x4c, 0x9a, 0xfe, 0x4c, 0xaf, 0xfe, 0x4c, 0x9a, 0xfe, 0x80, 0x00, 0x00, 0x00, - 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa2, 0x02, 0xbd, 0x03, 0xdf, - 0xdd, 0xac, 0xfe, 0xd0, 0x05, 0xca, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0x54, 0x52, 0x4d, 0x20, - 0x9b, 0xfe, 0xb0, 0x10, 0xad, 0x00, 0xdf, 0x8d, 0x8d, 0xfe, 0xad, 0x01, 0xdf, 0x8d, 0x8f, 0xfe, - 0x24, 0x30, 0x30, 0x01, 0x60, 0xad, 0x02, 0xdf, 0x29, 0x01, 0xf0, 0x04, 0xa9, 0x00, 0xf0, 0x02, - 0xa9, 0x80, 0xcd, 0x8c, 0xfe, 0xf0, 0x0c, 0x8d, 0x8c, 0xfe, 0x8d, 0x05, 0x85, 0xa5, 0x39, 0x09, - 0x20, 0x85, 0x39, 0xa2, 0x00, 0xad, 0x8d, 0xfe, 0x10, 0x01, 0xca, 0x8e, 0x8e, 0xfe, 0x18, 0xa5, - 0x3a, 0x6d, 0x8d, 0xfe, 0x85, 0x3a, 0xa5, 0x3b, 0x6d, 0x8e, 0xfe, 0x85, 0x3b, 0xad, 0x8f, 0xfe, - 0x18, 0x65, 0x3c, 0x2c, 0x8f, 0xfe, 0x30, 0x0a, 0xb0, 0x04, 0xc9, 0xe4, 0x90, 0x08, 0xa9, 0xe3, - 0xd0, 0x04, 0xb0, 0x02, 0xa9, 0xff, 0x85, 0x3c, 0xa2, 0x01, 0xad, 0x8e, 0xfe, 0x30, 0x08, 0x0d, - 0x8d, 0xfe, 0xf0, 0x04, 0xe8, 0xd0, 0x01, 0xca, 0xad, 0x8f, 0xfe, 0xf0, 0x05, 0x30, 0x06, 0xe8, - 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xbd, 0x3c, 0xff, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, - 0xff, 0x00, 0x05, 0x06, 0x07 +static const uint8_t TRMouse64Code[231] PROGMEM = { + 0x4c, 0x90, 0xfe, 0x4c, 0x9a, 0xfe, 0x4c, 0xd1, 0xfe, 0x4c, 0x9a, 0xfe, 0x80, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa2, 0xdf, 0xa0, 0x02, 0xb9, + 0x10, 0xdf, 0xd9, 0xce, 0xfe, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x30, 0x02, 0xa2, 0xde, 0x8e, 0xbe, + 0xfe, 0x8e, 0xd8, 0xfe, 0x8e, 0xde, 0xfe, 0x8e, 0xe9, 0xfe, 0xa0, 0x02, 0xb9, 0x03, 0xdf, 0xd9, + 0xcb, 0xfe, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0x54, 0x52, 0x4d, 0x54, 0x52, + 0x42, 0x20, 0x9b, 0xfe, 0xb0, 0x10, 0xad, 0x00, 0xdf, 0x8d, 0x8d, 0xfe, 0xad, 0x01, 0xdf, 0x8d, + 0x8f, 0xfe, 0x24, 0x30, 0x30, 0x01, 0x60, 0xad, 0x02, 0xdf, 0x29, 0x01, 0xf0, 0x04, 0xa9, 0x00, + 0xf0, 0x02, 0xa9, 0x80, 0xcd, 0x8c, 0xfe, 0xf0, 0x0c, 0x8d, 0x8c, 0xfe, 0x8d, 0x05, 0x85, 0xa5, + 0x39, 0x09, 0x20, 0x85, 0x39, 0xa2, 0x00, 0xad, 0x8d, 0xfe, 0x10, 0x01, 0xca, 0x8e, 0x8e, 0xfe, + 0x18, 0xa5, 0x3a, 0x6d, 0x8d, 0xfe, 0x85, 0x3a, 0xa5, 0x3b, 0x6d, 0x8e, 0xfe, 0x85, 0x3b, 0xad, + 0x8f, 0xfe, 0x18, 0x65, 0x3c, 0x2c, 0x8f, 0xfe, 0x30, 0x0a, 0xb0, 0x04, 0xc9, 0xe4, 0x90, 0x08, + 0xa9, 0xe3, 0xd0, 0x04, 0xb0, 0x02, 0xa9, 0xff, 0x85, 0x3c, 0xa2, 0x01, 0xad, 0x8e, 0xfe, 0x30, + 0x08, 0x0d, 0x8d, 0xfe, 0xf0, 0x04, 0xe8, 0xd0, 0x01, 0xca, 0xad, 0x8f, 0xfe, 0xf0, 0x05, 0x30, + 0x06, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xbd, 0x5e, 0xff, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, + 0x01, 0x04, 0xff, 0x00, 0x05, 0x06, 0x07 }; static const uint8_t TRMouse64Info[256] PROGMEM = { 0x00, 0xff, 0x03, 0x15, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x01, 0x91, 0x80, 0x02, 0x10, 0x40, 0x04, 0x10, 0x20, 0x04, 0x10, 0x20, 0x04, 0x10, 0x20, 0x07, 0xff, 0xe0, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x04, 0x00, 0x20, 0x02, 0x00, 0x40, 0x01, 0x81, 0x80, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xf7, 0x80, 0x04, 0x94, - 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0a, 0x00, 0x80, 0xfe, 0x45, 0xff, 0x00, 0x00, 0x54, 0x52, 0x20, + 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0a, 0x00, 0x80, 0xfe, 0x67, 0xff, 0x00, 0x00, 0x54, 0x52, 0x20, 0x55, 0x53, 0x42, 0x20, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x56, 0x31, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x00, 0x54, 0x65, 0x65, 0x6e, 0x73, 0x79, 0x52, 0x4f, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/tools/geos/trmouse-run.test.mjs b/tools/geos/trmouse-run.test.mjs new file mode 100644 index 00000000..b0357359 --- /dev/null +++ b/tools/geos/trmouse-run.test.mjs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Mos6502 } from '../lib/mos6502.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +const UPDATE_MOUSE = 6; +const MOUSE_ON = 0x30; +const POINTER_ON = 0x80; +const MOUSE_X = 0x3a; +const MOUSE_Y = 0x3c; +const CALLER = 0x0200; +const BESIDE_GEORAM = 0xdf00; +const BESIDE_REU = 0xde00; +const REU_REGISTERS = 0x0b; +const REU_GHOSTS = 0x1f; +const START = 100; + +const id = (text) => [...text].map((c) => c.charCodeAt(0)); + +function readDriver(header, prefix, base) { + const text = fs.readFileSync(path.join(root, 'Source/Teensy/GEOS', header), 'utf8'); + const code = [...text.match(new RegExp(`${prefix}Code\\[\\d+\\] PROGMEM = \\{([^}]*)\\}`))[1].matchAll(/0x[0-9a-f]+/g)].map((m) => parseInt(m[0], 16)); + return { base, code }; +} + +const DRIVERS = [ + ['GEOS 64', readDriver('TRMouse64.h', 'TRMouse64', 0xfe80)], + ['GEOS 128', readDriver('TRMouse128.h', 'TRMouse128', 0xfd00)], +]; + +// The TR's page as USBMouse.c and the block device serve it: X and Y movement, the buttons, "TRM", and "TRB" at $10 +function trPage(dx, dy) { + const page = new Uint8Array(256); + page.set([dx & 0xff, dy & 0xff, 0, ...id('TRM')]); + page.set(id('TRB'), 0x10); + return (offset) => page[offset]; +} + +// An REU's registers at $DF00, holding what a program left in them, ghosted every 32 bytes, the unused ones reading $FF +const reuRegisters = (regs) => (offset) => ((offset & REU_GHOSTS) < REU_REGISTERS ? regs[offset & REU_GHOSTS] : 0xff); + +function update(driver, pages) { + const ram = new Uint8Array(0x10000); + ram.set(driver.code, driver.base); + ram.set([0x20, (driver.base + UPDATE_MOUSE) & 0xff, (driver.base + UPDATE_MOUSE) >> 8], CALLER); + ram[MOUSE_ON] = POINTER_ON; + ram[MOUSE_X] = START; + ram[MOUSE_Y] = START; + const cpu = new Mos6502({ + read: (a) => (pages[a & 0xff00] ? pages[a & 0xff00](a & 0xff) : ram[a]), + write: (a, v) => { + if (pages[a & 0xff00]) throw new Error(`the driver wrote $${a.toString(16)}`); + ram[a] = v; + }, + }); + cpu.pc = CALLER; + cpu.run((c) => c.pc === CALLER + 3); + return { x: ram[MOUSE_X] - START, y: ram[MOUSE_Y] - START }; +} + +for (const [name, driver] of DRIVERS) { + test(`${name}: the mouse moves by the registers beside GeoRAM, not by a GeoRAM window that shows the TR's IDs`, () => { + const window = new Uint8Array(256); + window.set([9, 9, 0, ...id('TRM')]); + window.set(id('TRB'), 0x10); + assert.deepEqual(update(driver, { [BESIDE_GEORAM]: trPage(3, 4), [BESIDE_REU]: (o) => window[o] }), { x: 3, y: 4 }); + }); + + test(`${name}: the mouse moves by the registers beside an REU, even when the REU's address registers hold "TRM"`, () => { + const reu = reuRegisters([0x10, 0, 9, ...id('TRM'), 0, 0, 0, 0, 0]); + assert.deepEqual(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: trPage(5, 6) }), { x: 5, y: 6 }); + }); + + test(`${name}: with no TR registers on either page the mouse stays put`, () => { + const reu = reuRegisters([0x10, 0, 9, ...id('TRM'), 0, 0, 0, 0, 0]); + assert.deepEqual(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: () => 7 }), { x: 0, y: 0 }); + }); +} diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index 04e3a0e1..24fd07ce 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -17,11 +17,14 @@ BUTTON_UP = $80 NO_DIRECTION = $ff OFF_TOP = 228 ; the kernal reads a mouseYPos this high as above the top -regs = $df00 +; beside GeoRAM, and beside the REU, whose unused registers read $FF across $DF0B-$DF1F +PAGE_BESIDE_GEORAM = $df +PAGE_BESIDE_REU = $de regX = 0 regY = 1 regButtons = 2 regID = 3 +regBlockID = $10 ; the TR block device's, which shares the page and tells it from an REU's LEFT_BUTTON = $01 !ifndef MOUSE_BASE { MOUSE_BASE = $fd00 } @@ -46,36 +49,51 @@ SlowMouse SetMouse rts -; The ID tells whether the registers are there, beside GeoRAM. -; $DF03-$DF05 are REU address registers, which have no side effects when read. -; Carry set: what reads there is not a mouse. +; The block device's ID picks the page: beside an REU, $DF03-$DF05 are its address registers and can hold anything. +; Carry set: what reads there is not a mouse; otherwise every register read points at the page. FindRegs - ldx #2 -- lda regs+regID,x - cmp trID,x + ldx #PAGE_BESIDE_GEORAM + ldy #2 +- lda PAGE_BESIDE_GEORAM*256+regBlockID,y + cmp trbID,y bne + - dex + dey + bpl - + bmi ++ ++ ldx #PAGE_BESIDE_REU +++ stx mouseID+2 + stx readX+2 + stx readY+2 + stx readButtons+2 + ldy #2 +- +mouseID lda PAGE_BESIDE_GEORAM*256+regID,y + cmp trmID,y + bne + + dey bpl - clc rts + sec rts -trID !text "TRM" +trmID !text "TRM" +trbID !text "TRB" UpdateMouse jsr FindRegs bcs noMouse - lda regs+regX +readX lda PAGE_BESIDE_GEORAM*256+regX sta deltaX - lda regs+regY +readY lda PAGE_BESIDE_GEORAM*256+regY sta deltaY bit mouseOn bmi readButtons noMouse rts ; movement while the pointer is off is dropped readButtons - lda regs+regButtons +readButtons + lda PAGE_BESIDE_GEORAM*256+regButtons and #LEFT_BUTTON beq + lda #0 ; pressed From e42a3509b57ce374617c71c1c5f89d898e510eae Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:33:12 -0500 Subject: [PATCH 03/19] tools: the GEOS 128 boot ROM reaches the block device on the page the launch gives it The boot ROM is built at every launch, which knows which handler it starts, so the firmware writes the page into the ROM instead of the ROM searching for it: a glitch-safe search does not fit in the one page the cold start copies. geos128boot.mjs assembles the ROM at two pages and lists the bytes that move, as trdrive.py does for relocations. The launch still asks for the page beside GeoRAM. --- Source/Teensy/GEOS/Geos128Boot.h | 4 +++ Source/Teensy/GEOS/GeosBoot.h | 11 +++++-- Source/Teensy/GeosLauncher.ino | 2 +- Source/Teensy/tests/geos-boot.test.js | 25 +++++++++++++--- Source/Teensy/tests/geos_boot_harness.c | 7 +++-- tools/geos/geos128boot-run.test.mjs | 38 ++++++++++++++++++------- tools/geos/geos128boot.asm | 5 ++-- tools/geos/geos128boot.mjs | 33 ++++++++++++++++++--- 8 files changed, 98 insertions(+), 27 deletions(-) diff --git a/Source/Teensy/GEOS/Geos128Boot.h b/Source/Teensy/GEOS/Geos128Boot.h index 7c87ac4c..27d86070 100644 --- a/Source/Teensy/GEOS/Geos128Boot.h +++ b/Source/Teensy/GEOS/Geos128Boot.h @@ -32,3 +32,7 @@ static const uint8_t Geos128BootRoml[245] PROGMEM = { static const uint8_t Geos128BootVectors[7] PROGMEM = { 0x40, 0xf9, 0xff, 0xf9, 0xff, 0x0a, 0x80 }; +static const uint16_t Geos128BootPageAt[11] PROGMEM = { + 0x0077, 0x007c, 0x0081, 0x008e, 0x00a1, 0x00a4, 0x00b1, 0x00b4, + 0x00bf, 0x00c4, 0x00c7 +}; diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index abb4d6ce..3b323ba0 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -21,6 +21,9 @@ #define GEOS_DEVICE_8 8 #define GEOS128_SHARED_RAM_END 0x2000 // the C128 boot ROM's loader runs in the RAM below, which both banks share #define GEOS128_IO_START 0xd000 // and writes bank 1 with I/O mapped in from here up +// The block device's page: beside GeoRAM, or beside the REU, whose registers repeat across $DF00-$DFFF +#define GEOS_REGS_BESIDE_GEORAM 0xdf +#define GEOS_REGS_BESIDE_REU 0xde enum GeosDriverVector { VecInitForIO, VecDoneWithIO, VecExitTurbo, VecPurgeTurbo, VecEnterTurbo, VecChangeDiskDevice, VecNewDisk, VecReadBlock, VecWriteBlock, VecVerWriteBlock, VecCount = 10 }; @@ -389,9 +392,10 @@ static inline bool GeosBuildBootPrg(struct GeosDisk *Disk, const struct GeosDate } // The C128 boot ROM, its ROML chip then its ROMH chip, followed by the boot image it pulls through BOOT a page at a -// time. Image and ImageSize say where that image starts, without its load address, for TRBlockMount. -static inline bool GeosBuildC128Boot(struct GeosDisk *Disk, const struct GeosDate *Now, uint8_t *Out, uint32_t Capacity, - const uint8_t **Image, uint32_t *ImageSize, bool *ClockSet) +// time from the block device at RegsPage. Image and ImageSize say where that image starts, without its load address, +// for TRBlockMount. +static inline bool GeosBuildC128Boot(struct GeosDisk *Disk, const struct GeosDate *Now, uint8_t RegsPage, uint8_t *Out, + uint32_t Capacity, const uint8_t **Image, uint32_t *ImageSize, bool *ClockSet) { uint8_t *Roml = Out, *Romh = Out + GEOS128_BOOT_CHIP_SIZE, *File = Out + 2 * GEOS128_BOOT_CHIP_SIZE; uint32_t Length, Pages, Load; @@ -410,6 +414,7 @@ static inline bool GeosBuildC128Boot(struct GeosDisk *Disk, const struct GeosDat GeosPutWord(Roml + GEOS128_BOOT_PARAMS, Load); GeosPutWord(Roml + GEOS128_BOOT_PARAMS + 2, Pages); GeosPutWord(Roml + GEOS128_BOOT_PARAMS + 4, Disk->Target->BootEntry); + for (size_t i = 0; i < sizeof Geos128BootPageAt / sizeof Geos128BootPageAt[0]; i++) Roml[Geos128BootPageAt[i]] = RegsPage; *Image = File + 2; *ImageSize = Length - 2; return true; diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 2954eb9b..f35b905c 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -288,7 +288,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * } Disk = GeosDiskOn(Launcher, &Image); Built = Launcher->Machine == GeosC128 - ? GeosBuildC128Boot(&Disk, &Now, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) + ? GeosBuildC128Boot(&Disk, &Now, GEOS_REGS_BESIDE_GEORAM, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) : GeosBuildBootPrg(&Disk, &Now, RAM_Image, RAM_ImageSize, &Length, &ClockSet); Built = Built && GeosDisableConfigure(&Disk, &Changed); if (!Built) SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Disk.Error); diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 995c6b4b..4ef13277 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -56,6 +56,9 @@ const ENTRIES = arrayOf('TRDriverEntries'); const ENTRIES_128 = arrayIn(DRIVER128_HEADER)('TRDriver128Entries'); const BOOT_ROML = arrayIn(BOOT_ROM_HEADER)('Geos128BootRoml'); const BOOT_VECTORS = arrayIn(BOOT_ROM_HEADER)('Geos128BootVectors'); +const BOOT_PAGE_AT = arrayIn(BOOT_ROM_HEADER)('Geos128BootPageAt'); +const REGS_BESIDE_GEORAM = 0xdf; +const REGS_BESIDE_REU = 0xde; const BOOT_CHIP = 0x2000; const BOOT_PARAMS = 0x21; @@ -172,8 +175,8 @@ class Disk { } } -function run(args) { - const r = spawnSync(args[0], args.slice(1), { encoding: 'utf8' }); +function run(args, env = {}) { + const r = spawnSync(args[0], args.slice(1), { encoding: 'utf8', env: { ...process.env, ...env } }); return { status: r.status, out: r.stdout, err: r.stderr }; } @@ -184,9 +187,9 @@ function withHarness(body) { assert.equal(built.status, 0, built.err); const asCpp = run(['c++', '-x', 'c++', '-std=gnu++17', '-Wall', '-Werror', '-fsyntax-only', HARNESS_SOURCE]); assert.equal(asCpp.status, 0, asCpp.err); - const launch = (disk, time = [], version = '64') => { + const launch = (disk, time = [], version = '64', env = {}) => { fs.writeFileSync(path.join(dir, 'in.d81'), disk.image); - const r = run([path.join(dir, 'harness'), version, path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81'), ...time.map(String)]); + const r = run([path.join(dir, 'harness'), version, path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81'), ...time.map(String)], env); const out = r.status === 0 ? fs.readFileSync(path.join(dir, 'out.prg')) : null; const image = r.status === 0 ? fs.readFileSync(path.join(dir, 'out.d81')) : null; const boot = r.out.match(/ boot=(\d+),(\d+)/); @@ -432,6 +435,7 @@ test('the GEOS 128 boot ROM is told where the boot image goes, how many pages it assert.equal(roml.readUInt16LE(BOOT_PARAMS + 2), Math.ceil(size / SECTOR), 'a short last page counts'); assert.equal(roml.readUInt16LE(BOOT_PARAMS + 4), 0x2f3c); assert.deepEqual([...roml.subarray(BOOT_PARAMS + 6, BOOT_ROML.length)], BOOT_ROML.slice(BOOT_PARAMS + 6)); + for (const at of BOOT_PAGE_AT) assert.equal(roml[at], REGS_BESIDE_GEORAM, `the block device's page at ROML ${at}`); assert.equal(roml.subarray(BOOT_ROML.length).every((b) => b === 0), true); assert.deepEqual([...romh.subarray(BOOT_CHIP - BOOT_VECTORS.length)], BOOT_VECTORS, 'the CPU vectors end ROMH'); assert.equal(romh.subarray(0, BOOT_CHIP - BOOT_VECTORS.length).every((b) => b === 0), true, 'ROMH is not a function ROM of its own'); @@ -452,3 +456,16 @@ test('a GEOS 128 boot file that would reach I/O or the shared RAM is refused', { assert.equal(fits(0x2000, BODY_SIZE).status, 0, 'loaded right above the shared RAM'); }); }); + +test('the GEOS 128 boot ROM reaches the block device on the page the launch puts it', { skip }, () => { + withHarness((launch) => { + const r = launch(geos128Disk(), [], '128', { REGS_PAGE: REGS_BESIDE_REU.toString(16) }); + assert.equal(r.status, 0, r.err); + const roml = [...r.rom.subarray(0, BOOT_ROML.length)]; + assert.ok(BOOT_PAGE_AT.length > 0); + for (const at of BOOT_PAGE_AT) assert.equal(roml[at], REGS_BESIDE_REU, `ROML ${at}`); + const beside = launch(geos128Disk(), [], '128'); + const moved = roml.flatMap((byte, at) => (byte === beside.rom[at] ? [] : [at])); + assert.deepEqual(moved, BOOT_PAGE_AT, 'nothing else in the ROM changes with the page'); + }); +}); diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 291232f1..0e59999c 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -2,7 +2,8 @@ // geos_boot_harness 64|128 IN.d81 OUT OUT.d81 [HOUR MINUTE]: builds the GEOS 64 boot PRG, or the GEOS 128 boot ROM // and the boot image behind it, from the disk, retypes its CONFIGURE files, sets the clock to 2026-10-21 at // HOUR:MINUTE (15:07 if not given), and writes both. Prints "changed=<0|1> clock=<0|1>", and for GEOS 128 -// " boot=," where BOOT's image lies in OUT; exits 1 with the refusal on stderr. +// " boot=," where BOOT's image lies in OUT; exits 1 with the refusal on stderr. REGS_PAGE (hex) is the +// block device's page the GEOS 128 boot ROM is built for, beside GeoRAM if not set. #include #include "../GEOS/GeosTargets.h" @@ -30,6 +31,7 @@ int main(int argc, char **argv) struct GeosDisk Disk = { 0 }; uint32_t Length, BootSize = 0; const uint8_t *Boot = NULL; + const char *RegsPage = getenv("REGS_PAGE"); bool Is128, Changed, ClockSet, Built; FILE *File; size_t Size; @@ -49,7 +51,8 @@ int main(int argc, char **argv) Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; - Built = Is128 ? GeosBuildC128Boot(&Disk, &Now, Prg, sizeof Prg, &Boot, &BootSize, &ClockSet) + Built = Is128 ? GeosBuildC128Boot(&Disk, &Now, RegsPage ? (uint8_t)strtol(RegsPage, NULL, 16) : GEOS_REGS_BESIDE_GEORAM, + Prg, sizeof Prg, &Boot, &BootSize, &ClockSet) : GeosBuildBootPrg(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet); if (!Built || !GeosDisableConfigure(&Disk, &Changed)) { diff --git a/tools/geos/geos128boot-run.test.mjs b/tools/geos/geos128boot-run.test.mjs index ce68d4df..64a9b389 100644 --- a/tools/geos/geos128boot-run.test.mjs +++ b/tools/geos/geos128boot-run.test.mjs @@ -14,7 +14,10 @@ const MMU_RAM_SHARE = 0xd506; const KERNAL_RESET_SHARE = 0x04; const FUNCTION_ROM_AT_8000 = 0x08; const COLD_START = 0x8000; -const BLOCK = 0xdf00; +const BESIDE_GEORAM = 0xdf; +const BESIDE_REU = 0xde; +const REU_REGISTERS = 0x0b; +const REU_GHOSTS = 0x1f; const STATUS = { BUSY: 0x80, DONE: 0x5a, PAST_END: 0xe2 }; const CMD_BOOT = 5; const PAGE = 256; @@ -30,12 +33,13 @@ function readHeader() { vectors: define('GEOS128_BOOT_VECTORS'), roml: bytes('Geos128BootRoml'), romVectors: bytes('Geos128BootVectors'), + pageAt: [...text.match(/Geos128BootPageAt\[\d+\] PROGMEM = \{([^}]*)\}/)[1].matchAll(/0x[0-9a-f]+/g)].map((m) => parseInt(m[0], 16)), }; } -// The cartridge as GeosBuildC128Boot fills it: ROML with the load address, page count and entry patched in, ROMH -// holding only the vectors. -function cartridge({ load, pages, entry }) { +// The cartridge as GeosBuildC128Boot fills it: ROML with the load address, page count, entry and the block device's +// page patched in, ROMH holding only the vectors. +function cartridge({ load, pages, entry, page }) { const h = readHeader(); const roml = new Uint8Array(h.chipSize); const romh = new Uint8Array(h.chipSize); @@ -45,10 +49,11 @@ function cartridge({ load, pages, entry }) { roml[h.params + 2 * i] = word & 0xff; roml[h.params + 2 * i + 1] = word >> 8; } + for (const at of h.pageAt) roml[at] = page; return { roml, romh }; } -// The block device's registers at $DF00 as docs/TR_Block_Device.md describes them, serving BOOT from image. +// The block device's registers as docs/TR_Block_Device.md describes them, serving BOOT from image. // busyReads: STATUS reads $80 this many times after each CMD. staleEcho: for this many ECHO reads after STATUS turns // done, the device still shows the previous command's result, its token and its page. glitchEvery: every nth read // comes back with bits ORed in, as the Kawari does. echoGlitch: ORed into the first stale ECHO read. @@ -114,8 +119,11 @@ function blockDevice(image, { busyReads = 0, staleEcho = 0, glitchEvery = 0, ech } // A C128 as the boot ROM sees it: two 64K RAM banks behind the MMU's configuration register, the shared RAM at the -// bottom, I/O at $D000 (the block device at $DF00) and the cartridge's function ROM at $8000 and $C000. -function c128(cart, device) { +// bottom, I/O at $D000 (the block device on its page, and beside an REU, the REU's registers at $DF00) and the +// cartridge's function ROM at $8000 and $C000. +function c128(cart, device, page) { + const block = page << 8; + const reu = (offset) => ((offset & REU_GHOSTS) < REU_REGISTERS ? 0 : 0xff); const ram = [new Uint8Array(0x10000), new Uint8Array(0x10000)]; const io = new Map([[MMU_RAM_SHARE, KERNAL_RESET_SHARE]]); const ioWrites = []; @@ -143,7 +151,8 @@ function c128(cart, device) { read(address) { if (address === MMU_CR) return machine.cr; if (ioMapped(address)) { - if (address >= BLOCK) return device.read(address - BLOCK); + if ((address & 0xff00) === block) return device.read(address - block); + if (page === BESIDE_REU && address >= BESIDE_GEORAM << 8) return reu(address & 0xff); return io.get(address) ?? 0xff; } const rom = romAt(address); @@ -158,7 +167,7 @@ function c128(cart, device) { } if (ioMapped(address)) { ioWrites.push([address, value]); - if (address >= BLOCK) device.write(address - BLOCK, value); + if ((address & 0xff00) === block) device.write(address - block, value); else io.set(address, value); return; } @@ -177,9 +186,9 @@ function bootImage(pages, last = PAGE) { } // The kernal's cold start calls the function ROM with IRQs on and the ROM mapped at $8000. -function boot({ load = 0x2b19, pages = 3, entry = 0x2f3c, image = bootImage(pages), device: options, limit } = {}) { +function boot({ load = 0x2b19, pages = 3, entry = 0x2f3c, image = bootImage(pages), device: options, limit, page = BESIDE_GEORAM } = {}) { const device = blockDevice(image, options); - const machine = c128(cartridge({ load, pages, entry }), device); + const machine = c128(cartridge({ load, pages, entry, page }), device, page); const cpu = new Mos6502(machine.bus); cpu.pc = COLD_START; cpu.p &= ~0x04; @@ -208,6 +217,13 @@ test('the boot ROM pulls every page into bank 1 and enters GEOS with I/O in, the assert.equal(b.device.commands, 150); }); +test('the boot ROM pulls the image from the block device beside an REU, and writes nothing to the REU', () => { + const b = boot({ pages: 5, page: BESIDE_REU }); + run(b, entered); + assertLoaded(b); + assert.deepEqual(b.machine.ioWrites.filter(([a]) => a >= BESIDE_GEORAM << 8), []); +}); + test('the boot ROM writes no bank 0 RAM outside the shared bottom 8K, and nothing in bank 1 but the image', () => { const b = boot(); run(b, entered); diff --git a/tools/geos/geos128boot.asm b/tools/geos/geos128boot.asm index 4aa43a5e..c3857430 100644 --- a/tools/geos/geos128boot.asm +++ b/tools/geos/geos128boot.asm @@ -2,7 +2,7 @@ ; A C128 function ROM that starts GEOS 128 with no drive: it pulls the boot image the firmware built through the ; TR block device's BOOT command (docs/TR_Block_Device.md) into RAM bank 1, then enters it as GEOS's own ROM ; loader does. geos128boot.mjs turns it into Source/Teensy/GEOS/Geos128Boot.h; build with acme. -; The firmware fills in bootLoad, bootPages and bootEntry. +; The firmware fills in bootLoad, bootPages and bootEntry, and puts the block device's page in every register address. MMU_CR = $ff00 MMU_RAM_SHARE = $d506 @@ -22,7 +22,8 @@ DEVICE_8 = 8 ; in the common RAM the loader shares between the banks, where #63's loader ran LOADER = $1300 -BLOCK = $df00 +!ifndef BLOCK_PAGE { BLOCK_PAGE = $df } +BLOCK = BLOCK_PAGE * 256 REG_CMD = BLOCK + $08 REG_ADDR0 = BLOCK + $09 REG_ADDR1 = BLOCK + $0a diff --git a/tools/geos/geos128boot.mjs b/tools/geos/geos128boot.mjs index eb00e7d8..b62d261b 100644 --- a/tools/geos/geos128boot.mjs +++ b/tools/geos/geos128boot.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node // SPDX-License-Identifier: MIT // node tools/geos/geos128boot.mjs [OUT.h]: assembles the GEOS 128 boot ROM (geos128boot.asm) and emits it for the -// firmware, by default to Source/Teensy/GEOS/Geos128Boot.h. Needs ACME (found as the C64 build finds it). +// firmware, by default to Source/Teensy/GEOS/Geos128Boot.h, with where in ROML the block device's page goes. Needs +// ACME (found as the C64 build finds it). import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -17,12 +18,17 @@ const ROML = 0x8000; const ROMH = 0xc000; const CHIP_MASK = 0x1fff; const BYTES_PER_LINE = 16; +const WORDS_PER_LINE = 8; +// the block device's page beside GeoRAM, as the source assembles it, and one it can move to +const PAGE = 0xdf; +const OTHER_PAGE = 0xde; -function assemble() { +function assemble(page = PAGE) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'geos128boot-')); try { const out = path.join(dir, 'rom.bin'), labels = path.join(dir, 'rom.lbl'); - const r = spawnSync(resolveAcme(path.join(ROOT, 'tools/.cache')), ['-f', 'plain', '-o', out, '-l', labels, SOURCE], { encoding: 'utf8' }); + const args = [`-DBLOCK_PAGE=${page}`, '-f', 'plain', '-o', out, '-l', labels, SOURCE]; + const r = spawnSync(resolveAcme(path.join(ROOT, 'tools/.cache')), args, { encoding: 'utf8' }); if (r.status !== 0 || r.stderr) throw new Error(`acme: ${r.stdout}${r.stderr}`); const found = Object.fromEntries([...fs.readFileSync(labels, 'utf8').matchAll(/^\s*(\w+)\s*=\s*\$([0-9a-f]+)/gm)] .map((m) => [m[1], parseInt(m[2], 16)])); @@ -42,6 +48,24 @@ function cArray(name, bytes) { const hex4 = (n) => `0x${n.toString(16).padStart(4, '0')}`; +function cWords(name, words) { + const rows = []; + for (let i = 0; i < words.length; i += WORDS_PER_LINE) rows.push(' ' + words.slice(i, i + WORDS_PER_LINE).map(hex4).join(', ')); + return `static const uint16_t ${name}[${words.length}] PROGMEM = {\n${rows.join(',\n')}\n};\n`; +} + +// The ROML offsets that hold the block device's page: every byte that moves with it, and moves by exactly that +function pageOffsets(code) { + const moved = assemble(OTHER_PAGE).rom.subarray(0, code.length); + const offsets = []; + code.forEach((byte, i) => { + if (byte === moved[i]) return; + if (byte !== PAGE || moved[i] !== OTHER_PAGE) throw new Error(`ROML byte ${hex4(i)} changes with the page but is not it`); + offsets.push(i); + }); + return offsets; +} + export function header() { const { rom, labels } = assemble(); const code = rom.subarray(0, labels.loaderEnd - ROML); @@ -59,7 +83,8 @@ export function header() { + `#define GEOS128_BOOT_PARAMS ${hex4(labels.loaderRom - ROML)} // load address, page count, entry: words in ROML\n` + `#define GEOS128_BOOT_VECTORS ${hex4((labels.return - ROMH) & CHIP_MASK)} // in ROMH\n` + cArray('Geos128BootRoml', code) - + cArray('Geos128BootVectors', vectors); + + cArray('Geos128BootVectors', vectors) + + cWords('Geos128BootPageAt', pageOffsets(code)); } if (process.argv[1] === fileURLToPath(import.meta.url)) { From d371eb7f91a095a19f49e2fa9f235a55a71d31c7 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:38:23 -0500 Subject: [PATCH 04/19] fix: review findings for 3fe2dd5 --- tools/geos/trdrive-run.test.mjs | 22 +++++++++++++++++++--- tools/geos/trmouse-run.test.mjs | 20 ++++++++++++++------ tools/geos/trmouse128.asm | 1 - 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/tools/geos/trdrive-run.test.mjs b/tools/geos/trdrive-run.test.mjs index d8b19042..b57b21d6 100644 --- a/tools/geos/trdrive-run.test.mjs +++ b/tools/geos/trdrive-run.test.mjs @@ -34,13 +34,18 @@ const DRIVERS = [ ['GEOS 128', readDriver('TRDriver128.h', 'TRDriver128')], ]; -// The TR block device (docs/TR_Block_Device.md) answering READ and WRITE at once, its blocks keyed by ADDR1:ADDR0 -function blockDevice(blocks) { +// The TR block device (docs/TR_Block_Device.md) answering READ and WRITE at once, its blocks keyed by ADDR1:ADDR0. +// A garbled device reads each window byte back wrong once after it is written, as some machines latch it. +function blockDevice(blocks, { garbled = false } = {}) { const regs = { addr0: 0, addr1: 0, page: 0, token: 0 }; + const unsettled = new Set(); let buffer = new Uint8Array(PAGE), status = 0, echo = 0; return { write(offset, value) { - if (offset >= 0x40 && offset < 0x80) buffer[regs.page * QUARTER + offset - 0x40] = value; + if (offset >= 0x40 && offset < 0x80) { + buffer[regs.page * QUARTER + offset - 0x40] = value; + if (garbled) unsettled.add(offset); + } else if (offset === 0x09) regs.addr0 = value; else if (offset === 0x0a) regs.addr1 = value; else if (offset === 0x0b) regs.page = value & 3; @@ -54,6 +59,7 @@ function blockDevice(blocks) { } }, read(offset) { + if (unsettled.delete(offset)) return buffer[regs.page * QUARTER + offset - 0x40] ^ 0xff; if (offset >= 0x40 && offset < 0x80) return buffer[regs.page * QUARTER + offset - 0x40]; if (offset === 0x0d) return status; if (offset === 0x0e) return echo; @@ -135,6 +141,16 @@ for (const [name, driver] of DRIVERS) { assert.deepEqual(m.ioWrites.filter((a) => a >= BESIDE_GEORAM), [], 'nothing written to the REU'); }); + test(`${name}: a window byte beside an REU that reads back wrong once is checked again on that page`, () => { + const blocks = new Map(); + const m = machine(driver, {}); + m.pages[BESIDE_REU] = blockDevice(blocks, { garbled: true }); + m.pages[BESIDE_GEORAM] = reuRegisters(new Array(REU_REGISTERS).fill(0)); + m.ram.set(sectorOf(6), BUFFER); + assert.equal(call(m, ENTRY.WriteBlock, 7, 8), 0); + assert.deepEqual(blocks.get(key(7, 8)), sectorOf(6)); + }); + test(`${name}: the driver finds the registers again when they move, and reports a missing device`, () => { const m = machine(driver, {}); m.pages[BESIDE_GEORAM] = blockDevice(new Map([[key(1, 0), sectorOf(4)]])); diff --git a/tools/geos/trmouse-run.test.mjs b/tools/geos/trmouse-run.test.mjs index b0357359..7b35ba31 100644 --- a/tools/geos/trmouse-run.test.mjs +++ b/tools/geos/trmouse-run.test.mjs @@ -13,6 +13,9 @@ const MOUSE_ON = 0x30; const POINTER_ON = 0x80; const MOUSE_X = 0x3a; const MOUSE_Y = 0x3c; +const PRESS_FLAG = 0x39; +const MOUSE_BIT = 0x20; +const LEFT_BUTTON = 0x01; const CALLER = 0x0200; const BESIDE_GEORAM = 0xdf00; const BESIDE_REU = 0xde00; @@ -34,9 +37,9 @@ const DRIVERS = [ ]; // The TR's page as USBMouse.c and the block device serve it: X and Y movement, the buttons, "TRM", and "TRB" at $10 -function trPage(dx, dy) { +function trPage(dx, dy, buttons = 0) { const page = new Uint8Array(256); - page.set([dx & 0xff, dy & 0xff, 0, ...id('TRM')]); + page.set([dx & 0xff, dy & 0xff, buttons, ...id('TRM')]); page.set(id('TRB'), 0x10); return (offset) => page[offset]; } @@ -60,7 +63,7 @@ function update(driver, pages) { }); cpu.pc = CALLER; cpu.run((c) => c.pc === CALLER + 3); - return { x: ram[MOUSE_X] - START, y: ram[MOUSE_Y] - START }; + return { x: ram[MOUSE_X] - START, y: ram[MOUSE_Y] - START, pressed: (ram[PRESS_FLAG] & MOUSE_BIT) !== 0 }; } for (const [name, driver] of DRIVERS) { @@ -68,16 +71,21 @@ for (const [name, driver] of DRIVERS) { const window = new Uint8Array(256); window.set([9, 9, 0, ...id('TRM')]); window.set(id('TRB'), 0x10); - assert.deepEqual(update(driver, { [BESIDE_GEORAM]: trPage(3, 4), [BESIDE_REU]: (o) => window[o] }), { x: 3, y: 4 }); + assert.deepEqual(update(driver, { [BESIDE_GEORAM]: trPage(3, 4), [BESIDE_REU]: (o) => window[o] }), { x: 3, y: 4, pressed: false }); }); test(`${name}: the mouse moves by the registers beside an REU, even when the REU's address registers hold "TRM"`, () => { const reu = reuRegisters([0x10, 0, 9, ...id('TRM'), 0, 0, 0, 0, 0]); - assert.deepEqual(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: trPage(5, 6) }), { x: 5, y: 6 }); + assert.deepEqual(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: trPage(5, 6) }), { x: 5, y: 6, pressed: false }); + }); + + test(`${name}: the button reads from the registers beside an REU, not from the REU's own register at the same offset`, () => { + const reu = reuRegisters([0x10, 0, 0, ...id('TRM'), 0, 0, 0, 0, 0]); + assert.equal(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: trPage(0, 0, LEFT_BUTTON) }).pressed, true); }); test(`${name}: with no TR registers on either page the mouse stays put`, () => { const reu = reuRegisters([0x10, 0, 9, ...id('TRM'), 0, 0, 0, 0, 0]); - assert.deepEqual(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: () => 7 }), { x: 0, y: 0 }); + assert.deepEqual(update(driver, { [BESIDE_GEORAM]: reu, [BESIDE_REU]: () => 7 }), { x: 0, y: 0, pressed: false }); }); } diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index 24fd07ce..0d35995a 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -91,7 +91,6 @@ readY lda PAGE_BESIDE_GEORAM*256+regY bmi readButtons noMouse rts ; movement while the pointer is off is dropped -readButtons readButtons lda PAGE_BESIDE_GEORAM*256+regButtons and #LEFT_BUTTON From d32c261d957006b537082f71dc17ec077a387a62 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:42:33 -0500 Subject: [PATCH 05/19] firmware: the -TR copy runs the stock CONFIGURE, set up to find the TR as drive A CONFIGURE is what finds an REU and sets GEOS up to use it, so it has to run. Its drive-type probe now answers a 1581 at device 8 and nothing elsewhere, and the 1581 driver it installs for drive A, its record 4, gets the TR transport the boot image already has. The stock file is known by the target's name for it and by the probe in record 0, so a copy made before this, where CONFIGURE was retyped, is set up the same way. Anything else named CONFIG is still kept from running, including a CONFIGURE whose records are not laid out as the stock one. The synthetic test disk has one directory sector; the old CONFIGURE test put a ninth file past it, so its check that CONFIGURE.DOC stays as it was looked at a slot no directory held. --- Source/Teensy/GEOS/GeosBoot.h | 146 ++++++++++++++++++------ Source/Teensy/GEOS/GeosDiskPatch.h | 1 + Source/Teensy/GEOS/GeosTargets.h | 5 +- Source/Teensy/GeosLauncher.ino | 5 +- Source/Teensy/tests/geos-boot.test.js | 139 +++++++++++++++++++++- Source/Teensy/tests/geos_boot_harness.c | 8 +- docs/GEOS64_Usage.md | 3 +- 7 files changed, 258 insertions(+), 49 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 3b323ba0..46431e74 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -15,6 +15,9 @@ #define GEOS_AUTO_EXEC 14 #define GEOS_SYSTEM_FILE 4 #define GEOS_CONFIG_NAME "CONFIG" +#define GEOS_VLIR 1 +#define GEOS_PROBE_RECORD 0 +#define GEOS_DRIVER_RECORD 4 #define GEOS_BASIC_START 0x0801 #define GEOS_MEMORY_END 0x10000 #define GEOS_MAX_FILE_BLOCKS 3200 @@ -112,6 +115,69 @@ static bool GeosReplaceTransport(uint8_t *Driver, const struct GeosDiskDriver *W return true; } +// CONFIGURE's drive-type probe, which asks each IEC device what drive it is +static const uint8_t GeosStockProbe[] = { 0xa9, 0xe5, 0x85, 0x03, 0xa9, 0x80, 0x85, 0x02, 0x20 }; +// What the TR puts over it: drive A, at device 8, is a 1581, and no other drive is there +static const uint8_t GeosTRProbe[] = { 0xa9, 0x00, 0xae, 0x89, 0x84, 0xe0, 0x08, 0xd0, 0x02, 0xa9, 0x03, 0x60 }; + +// Finds Pattern once in Body; NULL when it is absent or ambiguous +static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Pattern, uint32_t Size) +{ + uint8_t *Found = NULL; + + for (uint32_t i = 0; i + Size <= Length; i++) + { + if (memcmp(Body + i, Pattern, Size)) continue; + if (Found) return NULL; + Found = Body + i; + } + return Found; +} + +// A file's bytes along its sector chain from At; What names it in a refusal +FLASHMEM static bool GeosReadChain(struct GeosDisk *Disk, struct GeosTS At, const char *What, uint8_t *Out, + uint32_t Capacity, uint32_t *Length) +{ + uint8_t Block[GEOS_SECTOR_SIZE]; + + *Length = 0; + for (int Blocks = 0; At.Track; Blocks++) + { + uint32_t Used; + if (Blocks > GEOS_MAX_FILE_BLOCKS || At.Track > Disk->Format->Tracks) + { + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s's chain is broken"), What); + return false; + } + if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; + Used = Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); + if (*Length + Used > Capacity) + { + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is too large"), What); + return false; + } + memcpy(Out + *Length, Block + 2, Used); + *Length += Used; + At = (struct GeosTS){ Block[0], Block[1] }; + } + return true; +} + +// Writes Data back over the chain GeosReadChain read it from +FLASHMEM static bool GeosRewriteChain(struct GeosDisk *Disk, struct GeosTS At, const uint8_t *Data) +{ + uint8_t Block[GEOS_SECTOR_SIZE]; + + for (uint32_t Done = 0; At.Track; Done += GEOS_DATA_PER_BLOCK) + { + if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; + memcpy(Block + 2, Data + Done, Block[0] ? GEOS_DATA_PER_BLOCK : Block[1] - 1); + if (!GeosWriteTS(Disk, At.Track, At.Sector, Block)) return false; + At = (struct GeosTS){ Block[0], Block[1] }; + } + return true; +} + // A file's bytes, found by name in the directory and followed along its sector chain FLASHMEM static bool GeosReadFile(struct GeosDisk *Disk, const char *Name, uint8_t *Out, uint32_t Capacity, uint32_t *Length) { @@ -133,20 +199,7 @@ FLASHMEM static bool GeosReadFile(struct GeosDisk *Disk, const char *Name, uint8 } } if (!At.Track) return GeosFail(Disk, PSTR("the boot file is not on the disk")); - - *Length = 0; - for (int Blocks = 0; At.Track; Blocks++) - { - uint32_t Used; - if (Blocks > GEOS_MAX_FILE_BLOCKS || At.Track > Disk->Format->Tracks) return GeosFail(Disk, PSTR("the boot file's chain is broken")); - if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; - Used = Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); - if (*Length + Used > Capacity) return GeosFail(Disk, PSTR("the boot file is too large")); - memcpy(Out + *Length, Block + 2, Used); - *Length += Used; - At = (struct GeosTS){ Block[0], Block[1] }; - } - return true; + return GeosReadChain(Disk, At, PSTR("the boot file"), Out, Capacity, Length); } static bool GeosNameContains(const uint8_t *Name, const char *Part) @@ -160,9 +213,43 @@ static bool GeosNameContains(const uint8_t *Name, const char *Part) return false; } -// Gives AUTO_EXEC configuration files the type of a file GEOS leaves alone, the way the stock disk's own -// configuration probes the IEC bus and empties drive A; returns whether anything was retyped -static inline bool GeosDisableConfigure(struct GeosDisk *Disk, bool *Changed) +// Whether Slot is the target's stock CONFIGURE, set up for the TR now or before: its probe answers what the TR has +// and the 1581 driver it installs for drive A is the TR's. The driver goes first, so a write cut short leaves the +// stock probe, and next time the driver no longer looks stock and the file is put aside instead. +FLASHMEM static bool GeosSetUpConfigure(struct GeosDisk *Disk, const uint8_t *Slot, uint8_t *Scratch, uint32_t Capacity, + bool *Ours, bool *Changed) +{ + const char *Name = Disk->Target->ConfigureName; + uint8_t Index[GEOS_SECTOR_SIZE], *Driver, *At; + struct GeosTS Probe, Driven; + uint32_t ProbeLength, DriverLength; + + *Ours = false; + if (Slot[SlotStructure] != GEOS_VLIR || !GeosSlotNamed(Slot, Name)) return true; + if (!GeosReadTS(Disk, Slot[SlotFirst], Slot[SlotFirst + 1], Index)) return false; + Probe = (struct GeosTS){ Index[2 + 2 * GEOS_PROBE_RECORD], Index[3 + 2 * GEOS_PROBE_RECORD] }; + Driven = (struct GeosTS){ Index[2 + 2 * GEOS_DRIVER_RECORD], Index[3 + 2 * GEOS_DRIVER_RECORD] }; + if (!Probe.Track || !Driven.Track) return true; + if (!GeosReadChain(Disk, Probe, Name, Scratch, Capacity, &ProbeLength)) return false; + if (GeosFindOnce(Scratch, ProbeLength, GeosTRProbe, sizeof GeosTRProbe)) return *Ours = true; + At = GeosFindOnce(Scratch, ProbeLength, GeosStockProbe, sizeof GeosStockProbe); + if (!At || At + sizeof GeosTRProbe > Scratch + ProbeLength) return true; + + Driver = Scratch + ProbeLength; + if (!GeosReadChain(Disk, Driven, Name, Driver, Capacity - ProbeLength, &DriverLength)) return false; + if (DriverLength <= GEOS_TYPE_BYTE || !GeosIsDriverAt(Driver)) return true; + if (GeosWord(Driver + 2 * VecReadBlock) - GEOS_DRIVER_AREA > DriverLength) return true; + if (!GeosReplaceTransport(Driver, Disk->Target->DiskDriver)) return true; + memcpy(At, GeosTRProbe, sizeof GeosTRProbe); + if (!GeosRewriteChain(Disk, Driven, Driver) || !GeosRewriteChain(Disk, Probe, Scratch)) return false; + *Changed = true; + return *Ours = true; +} + +// Keeps GEOS's configuration files from probing a drive that is not there: the stock CONFIGURE is set up to find +// the TR and run, and every other one stops being auto-executed. Scratch holds CONFIGURE's records meanwhile. +// Returns whether anything changed. +static inline bool GeosPrepareConfigure(struct GeosDisk *Disk, uint8_t *Scratch, uint32_t Capacity, bool *Changed) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; uint8_t Block[GEOS_SECTOR_SIZE]; @@ -177,10 +264,15 @@ static inline bool GeosDisableConfigure(struct GeosDisk *Disk, bool *Changed) if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) { - uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; - if (!(Slot[SlotType] & CBM_TYPE_MASK) || Slot[SlotGeosType] != GEOS_AUTO_EXEC) continue; + uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE, Type = Slot[SlotGeosType]; + bool Ours; + if (!(Slot[SlotType] & CBM_TYPE_MASK)) continue; + if (Type != GEOS_AUTO_EXEC && Type != GEOS_SYSTEM_FILE) continue; if (!GeosNameContains(Slot + SlotName, GEOS_CONFIG_NAME)) continue; - Slot[SlotGeosType] = GEOS_SYSTEM_FILE; + if (!GeosSetUpConfigure(Disk, Slot, Scratch, Capacity, &Ours, Changed)) return false; + Type = Ours ? GEOS_AUTO_EXEC : GEOS_SYSTEM_FILE; + if (Slot[SlotGeosType] == Type) continue; + Slot[SlotGeosType] = Type; Retyped = true; } if (!Retyped) continue; @@ -297,20 +389,6 @@ FLASHMEM static void Geos128SetClock(uint8_t *Run, const struct GeosDate *Now) static const struct GeosClockRun Geos128ClockRuns[] = { { Geos128StartUp, sizeof Geos128StartUp, Geos128SetClock } }; -// Finds Pattern once in Body; NULL when it is absent or ambiguous -static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Pattern, uint32_t Size) -{ - uint8_t *Found = NULL; - - for (uint32_t i = 0; i + Size <= Length; i++) - { - if (memcmp(Body + i, Pattern, Size)) continue; - if (Found) return NULL; - Found = Body + i; - } - return Found; -} - // Sets the date and time GEOS starts with. Leaves the body as it is, and returns false, for a boot file whose // start-up code is not the one the target knows static bool GeosSetClock(uint8_t *Body, uint32_t Length, const struct GeosTarget *Target, const struct GeosDate *Now) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index ad827138..9a1b8a4f 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -68,6 +68,7 @@ struct GeosTarget const struct GeosClockRun *ClockRuns; uint8_t ClockRunCount; uint16_t ResetKey; // where the boot file holds the C128's reset key, or 0 + const char *ConfigureName; }; struct GeosBootDisk { bool HasDeskTop, HasDriver; }; diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h index 953a3210..cc882e10 100644 --- a/Source/Teensy/GEOS/GeosTargets.h +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -44,7 +44,7 @@ static const struct GeosLauncher Geos64Launcher = "GEOS 64", "/GEOS64", GeosC64, { "DESK TOP", TRMouse64Name, TRMouse64Code, TRMouse64Info, sizeof TRMouse64Code, GEOS_INPUT_64, GEOS64_BOOT_FILE, GEOS64_BOOT_ENTRY, &TRDiskDriver64, - Geos64ClockRuns, sizeof Geos64ClockRuns / sizeof Geos64ClockRuns[0], 0 }, + Geos64ClockRuns, sizeof Geos64ClockRuns / sizeof Geos64ClockRuns[0], 0, "CONFIGURE" }, }; static const struct GeosLauncher Geos128Launcher = @@ -52,5 +52,6 @@ static const struct GeosLauncher Geos128Launcher = "GEOS 128", "/GEOS128", GeosC128, { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, GEOS128_BOOT_FILE, GEOS128_BOOT_ENTRY, &TRDiskDriver128, - Geos128ClockRuns, sizeof Geos128ClockRuns / sizeof Geos128ClockRuns[0], GEOS128_RESET_KEY }, + Geos128ClockRuns, sizeof Geos128ClockRuns / sizeof Geos128ClockRuns[0], GEOS128_RESET_KEY, + "128 CONFIGURE" }, }; diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index f35b905c..845aad53 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -287,10 +287,9 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * return false; } Disk = GeosDiskOn(Launcher, &Image); - Built = Launcher->Machine == GeosC128 + Built = GeosPrepareConfigure(&Disk, RAM_Image, RAM_ImageSize, &Changed) && (Launcher->Machine == GeosC128 ? GeosBuildC128Boot(&Disk, &Now, GEOS_REGS_BESIDE_GEORAM, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) - : GeosBuildBootPrg(&Disk, &Now, RAM_Image, RAM_ImageSize, &Length, &ClockSet); - Built = Built && GeosDisableConfigure(&Disk, &Changed); + : GeosBuildBootPrg(&Disk, &Now, RAM_Image, RAM_ImageSize, &Length, &ClockSet)); if (!Built) SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Disk.Error); Image.close(); if (!Built) return false; diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 4ef13277..29212d73 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -30,6 +30,8 @@ const DIR_SLOT = 32; const SLOT_TYPE = 2; const SLOT_FIRST = 3; const SLOT_NAME = 5; +const SLOT_STRUCTURE = 23; +const VLIR = 1; const SLOT_GEOS_TYPE = 24; const PAD = 0xa0; const USR_CLOSED = 0x83; @@ -147,10 +149,40 @@ class Disk { const header = offsetOf(DIR_TRACK, 0); this.image[header] = DIR_TRACK; this.image[header + 1] = FIRST_DIR_SECTOR; + this.slotsPerSector = SECTOR / DIR_SLOT; this.image[offsetOf(DIR_TRACK, FIRST_DIR_SECTOR) + 1] = 0xff; } addFile(name, data, geosType = 0) { + return this.addSlot(name, this.writeChain(data), geosType); + } + + // A VLIR file: an index block naming each record's chain + addVlir(name, records, geosType = 0) { + const index = Buffer.alloc(SECTOR - 2); + records.forEach((data, i) => { + const first = this.writeChain(data); + index[2 * i] = first.track; + index[2 * i + 1] = first.sector; + }); + const slot = this.addSlot(name, this.writeChain(index), geosType); + this.image[slot + SLOT_STRUCTURE] = VLIR; + return slot; + } + + // A record's bytes, read back along its chain from the index block + record(slot, i, image = this.image) { + let o = offsetOf(image[slot + SLOT_FIRST], image[slot + SLOT_FIRST + 1]); + const chunks = []; + for (let at = offsetOf(image[o + 2 + 2 * i], image[o + 3 + 2 * i]); ; ) { + const last = !image[at]; + chunks.push(image.subarray(at + 2, last ? at + image[at + 1] + 1 : at + SECTOR)); + if (last) return Buffer.concat(chunks); + at = offsetOf(image[at], image[at + 1]); + } + } + + writeChain(data) { const sectors = []; for (let at = 0; at < data.length || !sectors.length; at += SECTOR - 2) sectors.push(this.nextSector++); sectors.forEach((n, i) => { @@ -163,10 +195,15 @@ class Disk { this.image[o + 1] = last ? chunk.length + 1 : next % SECTORS_PER_TRACK; chunk.copy(this.image, o + 2); }); + return { track: FIRST_FILE_TRACK + Math.floor(sectors[0] / SECTORS_PER_TRACK), sector: sectors[0] % SECTORS_PER_TRACK }; + } + + addSlot(name, first, geosType) { + assert.ok(this.slots.length < this.slotsPerSector, 'the directory has one sector'); const slot = offsetOf(DIR_TRACK, FIRST_DIR_SECTOR) + this.slots.length * DIR_SLOT; this.image[slot + SLOT_TYPE] = USR_CLOSED; - this.image[slot + SLOT_FIRST] = FIRST_FILE_TRACK + Math.floor(sectors[0] / SECTORS_PER_TRACK); - this.image[slot + SLOT_FIRST + 1] = sectors[0] % SECTORS_PER_TRACK; + this.image[slot + SLOT_FIRST] = first.track; + this.image[slot + SLOT_FIRST + 1] = first.sector; this.image.fill(PAD, slot + SLOT_NAME, slot + SLOT_NAME + 16); this.image.write(name, slot + SLOT_NAME, 'latin1'); this.image[slot + SLOT_GEOS_TYPE] = geosType; @@ -331,14 +368,13 @@ test('a disk with no boot file is refused', { skip }, () => { }); }); -test('CONFIGURE files stop being auto-executed, and nothing else changes', { skip }, () => { +test('configuration files other than the stock CONFIGURE stop being auto-executed, and nothing else changes', { skip }, () => { withHarness((launch) => { const disk = standardDisk(); const configure = disk.addFile('CONFIGURE', Buffer.from([1, 2, 3]), AUTO_EXEC); const second = disk.addFile('CONFIG 2.0r', Buffer.from([1, 2, 3]), AUTO_EXEC); const geos128 = ['128 CONFIGURE', '128 CONFIG 2.0r', '64 CONFIG 2.0r'].map((name) => disk.addFile(name, Buffer.from([1, 2, 3]), AUTO_EXEC)); const other = disk.addFile('pad color mgr', Buffer.from([1, 2, 3]), AUTO_EXEC); - const plain = disk.addFile('CONFIGURE.DOC', Buffer.from([1, 2, 3]), 0); const first = launch(disk); assert.equal(first.status, 0, first.err); @@ -347,7 +383,12 @@ test('CONFIGURE files stop being auto-executed, and nothing else changes', { ski assert.equal(first.image[second + SLOT_GEOS_TYPE], SYSTEM_FILE); for (const slot of geos128) assert.equal(first.image[slot + SLOT_GEOS_TYPE], SYSTEM_FILE, 'GEOS 128 names CONFIG after a prefix'); assert.equal(first.image[other + SLOT_GEOS_TYPE], AUTO_EXEC, 'other auto-exec files stay'); - assert.equal(first.image[plain + SLOT_GEOS_TYPE], 0, 'a CONFIG file that is not auto-exec stays'); + + const documented = standardDisk(); + const plain = documented.addFile('CONFIGURE.DOC', Buffer.from([1, 2, 3]), 0); + const left = launch(documented); + assert.match(left.out, /changed=0/); + assert.equal(left.image[plain + SLOT_GEOS_TYPE], 0, 'a CONFIG file that is not auto-exec stays'); const again = launch({ image: first.image }); assert.match(again.out, /changed=0/); @@ -469,3 +510,91 @@ test('the GEOS 128 boot ROM reaches the block device on the page the launch puts assert.deepEqual(moved, BOOT_PAGE_AT, 'nothing else in the ROM changes with the page'); }); }); + +// CONFIGURE as the stock disks have it: record 0 its code from $0406, with the drive-type probe in it, and record 4 +// the 1581 driver it installs for drive A, shorter than the driver area +const STOCK_PROBE = [0xa9, 0xe5, 0x85, 0x03, 0xa9, 0x80, 0x85, 0x02, 0x20, 0x1e, 0x0c]; +const TR_PROBE = [0xa9, 0x00, 0xae, 0x89, 0x84, 0xe0, 0x08, 0xd0, 0x02, 0xa9, 0x03, 0x60]; +const PROBE_AT = { 64: 0x0c33 - 0x0406, 128: 0x0b7f - 0x0406 }; +const RECORD_0_SIZE = 3504; +const RECORD_4_SIZE = { 64: 3179, 128: 2997 }; +const PROBE_RECORD = 0; +const DRIVER_RECORD = 4; + +function configure(version, { probes = [PROBE_AT[version]], driver } = {}) { + const code = Buffer.alloc(RECORD_0_SIZE); + for (let i = 0; i < code.length; i++) code[i] = (i * 7 + 3) & 0xff; + for (const at of probes) Buffer.from(STOCK_PROBE).copy(code, at); + const stock = (version === '128' ? stockDriver128() : stockDriver()).subarray(0, RECORD_4_SIZE[version]); + return [code, ...[1, 2, 3].map((n) => Buffer.alloc(300 * n, n)), driver ?? stock, Buffer.alloc(1782, 5)]; +} + +const STOCK_CONFIGURE = { 64: 'CONFIGURE', 128: '128 CONFIGURE' }; +const diskFor = (version) => (version === '128' ? geos128Disk() : standardDisk()); +const bootDriver = (version, r) => r.prg.subarray(version === '128' ? 2 + GEOS128.driverAt : 2 + LOAD_ADDRESS - BASIC_START + DRIVER_AT_IN_BODY); + +for (const version of ['64', '128']) { + test(`GEOS ${version}: the stock CONFIGURE is set up to find the TR's drive and runs, installing the TR's driver for drive A`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + const records = configure(version); + const slot = disk.addVlir(STOCK_CONFIGURE[version], records, AUTO_EXEC); + + const first = launch(disk, [], version); + assert.equal(first.status, 0, first.err); + assert.match(first.out, /changed=1/); + assert.equal(first.image[slot + SLOT_GEOS_TYPE], AUTO_EXEC); + const code = disk.record(slot, PROBE_RECORD, first.image); + const probe = Buffer.from(records[PROBE_RECORD]); + Buffer.from(TR_PROBE).copy(probe, PROBE_AT[version]); + assert.equal(code.equals(probe), true, 'the probe answers for the TR, and nothing else in record 0 changes'); + const driver = disk.record(slot, DRIVER_RECORD, first.image); + assert.equal(driver.length, RECORD_4_SIZE[version]); + assert.equal(driver.equals(bootDriver(version, first).subarray(0, driver.length)), true, 'the TR transport, as the boot image has it'); + for (const i of [1, 2, 3, 5]) assert.equal(disk.record(slot, i, first.image).equals(records[i]), true, `record ${i}`); + + const again = launch({ image: first.image }, [], version); + assert.match(again.out, /changed=0/); + assert.equal(again.image.equals(first.image), true); + }); + }); + + test(`GEOS ${version}: a copy whose stock CONFIGURE was put aside gets it set up and running again`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + const slot = disk.addVlir(STOCK_CONFIGURE[version], configure(version), SYSTEM_FILE); + const r = launch(disk, [], version); + assert.equal(r.status, 0, r.err); + assert.equal(r.image[slot + SLOT_GEOS_TYPE], AUTO_EXEC); + assert.equal(disk.record(slot, PROBE_RECORD, r.image).subarray(PROBE_AT[version], PROBE_AT[version] + TR_PROBE.length).equals(Buffer.from(TR_PROBE)), true); + }); + }); + + test(`GEOS ${version}: a CONFIGURE that is not the stock one, or not as the stock one is laid out, is put aside untouched`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + const other = version === '128' ? '64' : '128'; + const shortDriver = (version === '128' ? stockDriver128() : stockDriver()).subarray(0, READ_BLOCK - DRIVER_AREA - 1); + const notADriver = Buffer.from(configure(version)[DRIVER_RECORD]); + notADriver[TYPE_BYTE] = 0; + const odd = [ + ['the other version', STOCK_CONFIGURE[other], configure(version)], + ['another configuration program', 'CONFIG 2.0r', configure(version)], + ['no probe', STOCK_CONFIGURE[version], configure(version, { probes: [] })], + ['the probe twice', STOCK_CONFIGURE[version], configure(version, { probes: [PROBE_AT[version], 0x100] })], + ['record 4 not a driver', STOCK_CONFIGURE[version], configure(version, { driver: notADriver })], + ['record 4 ending before ReadBlock', STOCK_CONFIGURE[version], configure(version, { driver: shortDriver })], + ].map(([why, name, records]) => ({ why, records, slot: disk.addVlir(name, records, AUTO_EXEC) })); + const r = launch(disk, [], version); + assert.equal(r.status, 0, r.err); + for (const { why, records, slot } of odd) { + assert.equal(r.image[slot + SLOT_GEOS_TYPE], SYSTEM_FILE, why); + records.forEach((data, i) => assert.equal(disk.record(slot, i, r.image).equals(data), true, `${why}: record ${i}`)); + } + + const plain = diskFor(version); + const sequential = plain.addFile(STOCK_CONFIGURE[version], configure(version)[PROBE_RECORD], AUTO_EXEC); + assert.equal(launch(plain, [], version).image[sequential + SLOT_GEOS_TYPE], SYSTEM_FILE, 'a sequential file'); + }); + }); +} diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 0e59999c..819a42fb 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // geos_boot_harness 64|128 IN.d81 OUT OUT.d81 [HOUR MINUTE]: builds the GEOS 64 boot PRG, or the GEOS 128 boot ROM -// and the boot image behind it, from the disk, retypes its CONFIGURE files, sets the clock to 2026-10-21 at +// and the boot image behind it, from the disk, sets up its CONFIGURE files, sets the clock to 2026-10-21 at // HOUR:MINUTE (15:07 if not given), and writes both. Prints "changed=<0|1> clock=<0|1>", and for GEOS 128 // " boot=," where BOOT's image lies in OUT; exits 1 with the refusal on stderr. REGS_PAGE (hex) is the // block device's page the GEOS 128 boot ROM is built for, beside GeoRAM if not set. @@ -51,10 +51,10 @@ int main(int argc, char **argv) Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; - Built = Is128 ? GeosBuildC128Boot(&Disk, &Now, RegsPage ? (uint8_t)strtol(RegsPage, NULL, 16) : GEOS_REGS_BESIDE_GEORAM, + Built = GeosPrepareConfigure(&Disk, Prg, sizeof Prg, &Changed) && (Is128 ? GeosBuildC128Boot(&Disk, &Now, RegsPage ? (uint8_t)strtol(RegsPage, NULL, 16) : GEOS_REGS_BESIDE_GEORAM, Prg, sizeof Prg, &Boot, &BootSize, &ClockSet) - : GeosBuildBootPrg(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet); - if (!Built || !GeosDisableConfigure(&Disk, &Changed)) + : GeosBuildBootPrg(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet)); + if (!Built) { fprintf(stderr, "%s\n", Disk.Error); return 1; diff --git a/docs/GEOS64_Usage.md b/docs/GEOS64_Usage.md index 166d1342..c50885ec 100644 --- a/docs/GEOS64_Usage.md +++ b/docs/GEOS64_Usage.md @@ -30,7 +30,8 @@ * The contents are written to `georam.ram` on the SD card, and the LED flashes for about 1.5 seconds once the save is done. * The previous `georam.ram` is kept as `georam.bak`. * If the power goes off during a save, the next launch loads the newest complete save: the one just made if it was fully written, otherwise the previous one. -* GEOS's `CONFIGURE` does not run at start-up, since it looks for a disk drive, so GEOS does not set GeoRAM up as a RAM disk. +* GEOS's own `CONFIGURE` runs at start-up, set up in the `-TR` copy to find the TeensyROM as drive A. It does not find GeoRAM, so GEOS does not set GeoRAM up as a RAM disk. + * Other configuration programs on the disk (such as `CONFIG 2.0r`) are kept from running at start-up, since they look for a disk drive. ## Date and time * GEOS starts with the date and time of the TeensyROM's real-time clock, adjusted by the Time Zone setting. From dc94269c6110b9e1c842bbbf04e9e248d002d3bf Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:47:18 -0500 Subject: [PATCH 06/19] fix: review findings for c59cfe9 --- Source/Teensy/GEOS/GeosBoot.h | 6 ++- Source/Teensy/tests/geos-boot.test.js | 59 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 46431e74..b4438dc0 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -134,6 +134,8 @@ static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Patt return Found; } +static uint32_t GeosBytesInBlock(const uint8_t *Block) { return Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); } + // A file's bytes along its sector chain from At; What names it in a refusal FLASHMEM static bool GeosReadChain(struct GeosDisk *Disk, struct GeosTS At, const char *What, uint8_t *Out, uint32_t Capacity, uint32_t *Length) @@ -150,7 +152,7 @@ FLASHMEM static bool GeosReadChain(struct GeosDisk *Disk, struct GeosTS At, cons return false; } if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; - Used = Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); + Used = GeosBytesInBlock(Block); if (*Length + Used > Capacity) { snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is too large"), What); @@ -171,7 +173,7 @@ FLASHMEM static bool GeosRewriteChain(struct GeosDisk *Disk, struct GeosTS At, c for (uint32_t Done = 0; At.Track; Done += GEOS_DATA_PER_BLOCK) { if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; - memcpy(Block + 2, Data + Done, Block[0] ? GEOS_DATA_PER_BLOCK : Block[1] - 1); + memcpy(Block + 2, Data + Done, GeosBytesInBlock(Block)); if (!GeosWriteTS(Disk, At.Track, At.Sector, Block)) return false; At = (struct GeosTS){ Block[0], Block[1] }; } diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 29212d73..75c193f3 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -595,6 +595,65 @@ for (const version of ['64', '128']) { const plain = diskFor(version); const sequential = plain.addFile(STOCK_CONFIGURE[version], configure(version)[PROBE_RECORD], AUTO_EXEC); assert.equal(launch(plain, [], version).image[sequential + SLOT_GEOS_TYPE], SYSTEM_FILE, 'a sequential file'); + + const more = diskFor(version); + const pinned = Buffer.from(configure(version)[DRIVER_RECORD]); + pinned.writeUInt16LE(READ_BLOCK - 1, 2 * 10); + const stillOdd = [ + ['the probe too near the end of record 0 for the TR probe', configure(version, { probes: [RECORD_0_SIZE - STOCK_PROBE.length + 2] })], + ['a vector the transport leaves alone pointing into it', configure(version, { driver: pinned })], + ['laid out as the stock one but sequential', configure(version)], + ].map(([why, records]) => ({ why, records, slot: more.addVlir(STOCK_CONFIGURE[version], records, AUTO_EXEC) })); + more.image[stillOdd[2].slot + SLOT_STRUCTURE] = 0; + const m = launch(more, [], version); + assert.equal(m.status, 0, m.err); + for (const { why, records, slot } of stillOdd) { + assert.equal(m.image[slot + SLOT_GEOS_TYPE], SYSTEM_FILE, why); + records.forEach((data, i) => assert.equal(more.record(slot, i, m.image).equals(data), true, `${why}: record ${i}`)); + } + }); + }); + + test(`GEOS ${version}: a set-up cut short after the driver was written is put aside next time`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + const records = configure(version); + const slot = disk.addVlir(STOCK_CONFIGURE[version], records, AUTO_EXEC); + const first = launch(disk, [], version); + assert.equal(first.status, 0, first.err); + + const cut = { image: Buffer.from(first.image) }; + const index = offsetOf(cut.image[slot + SLOT_FIRST], cut.image[slot + SLOT_FIRST + 1]); + let at = offsetOf(cut.image[index + 2 + 2 * PROBE_RECORD], cut.image[index + 3 + 2 * PROBE_RECORD]); + for (let done = 0; ; done += SECTOR - 2) { + records[PROBE_RECORD].copy(cut.image, at + 2, done, done + (cut.image[at] ? SECTOR - 2 : cut.image[at + 1] - 1)); + if (!cut.image[at]) break; + at = offsetOf(cut.image[at], cut.image[at + 1]); + } + const driver = disk.record(slot, DRIVER_RECORD, cut.image); + const r = launch(cut, [], version); + assert.equal(r.status, 0, r.err); + assert.equal(r.image[slot + SLOT_GEOS_TYPE], SYSTEM_FILE); + assert.equal(disk.record(slot, PROBE_RECORD, r.image).equals(records[PROBE_RECORD]), true); + assert.equal(disk.record(slot, DRIVER_RECORD, r.image).equals(driver), true); + }); + }); + + test(`GEOS ${version}: a record whose last block says it holds no bytes is set up without overrunning it`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + const records = configure(version); + const slot = disk.addVlir(STOCK_CONFIGURE[version], records, AUTO_EXEC); + const index = offsetOf(disk.image[slot + SLOT_FIRST], disk.image[slot + SLOT_FIRST + 1]); + let at = offsetOf(disk.image[index + 2 + 2 * PROBE_RECORD], disk.image[index + 3 + 2 * PROBE_RECORD]); + while (disk.image[at]) at = offsetOf(disk.image[at], disk.image[at + 1]); + disk.image[at + 1] = 0; + + const r = launch(disk, [], version); + assert.equal(r.status, 0, r.err); + assert.equal(r.image[slot + SLOT_GEOS_TYPE], AUTO_EXEC); + assert.equal(r.image[at + 1], 0); + assert.equal(r.image.subarray(at + 2, at + SECTOR).equals(disk.image.subarray(at + 2, at + SECTOR)), true, 'the empty last block keeps its bytes'); }); }); } From ed7d550780426a88ade2e4d8b42ca19caf7f57da Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:51:00 -0500 Subject: [PATCH 07/19] firmware: a -TR copy's mouse driver is brought up to date at each launch The copy outlives the firmware that made it, and a mouse driver from before the REU route reads $DF03 for its ID, which beside an REU is an address register. The driver is rewritten in place over its own blocks; one taking another number of blocks is refused with a message to delete the copy, since growing it means allocating on the user's disk. --- Source/Teensy/GEOS/GeosDiskPatch.h | 80 +++++++++++++++++++++---- Source/Teensy/GeosLauncher.ino | 3 +- Source/Teensy/tests/geos-boot.test.js | 67 +++++++++++++++++++++ Source/Teensy/tests/geos_boot_harness.c | 9 +-- docs/GEOS64_Usage.md | 2 + 5 files changed, 145 insertions(+), 16 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 9a1b8a4f..565727a0 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -337,6 +337,20 @@ FLASHMEM static uint16_t GeosDriverBlocks(const struct GeosTarget *Target) return GeosDriverDataBlocks(Target) + 1; } +// Block i of the driver's data chain, linked to Next unless it is the last; returns how many of its bytes are used +FLASHMEM static uint16_t GeosDriverBlock(const struct GeosTarget *Target, uint16_t i, struct GeosTS Next, uint8_t *Block) +{ + uint16_t Start = i * GEOS_DATA_PER_BLOCK, Length = Target->DriverSize - Start; + bool Last = i + 1 == GeosDriverDataBlocks(Target); + + if (Length > GEOS_DATA_PER_BLOCK) Length = GEOS_DATA_PER_BLOCK; + memset(Block, 0, GEOS_SECTOR_SIZE); + Block[0] = Last ? 0 : Next.Track; + Block[1] = Last ? Length + 1 : Next.Sector; + memcpy(Block + 2, Target->DriverCode + Start, Length); + return Length + 2; +} + // Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. FLASHMEM static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) { @@ -349,15 +363,8 @@ FLASHMEM static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First *First = Here; for (uint16_t i = 0; i < DataBlocks; i++) { - uint16_t Start = i * GEOS_DATA_PER_BLOCK, Length = Target->DriverSize - Start; - if (Length > GEOS_DATA_PER_BLOCK) Length = GEOS_DATA_PER_BLOCK; - bool Last = i + 1 == DataBlocks; - - if (!Last && !GeosAllocate(Disk, &Next)) return false; - memset(Block, 0, sizeof Block); - Block[0] = Last ? 0 : Next.Track; - Block[1] = Last ? Length + 1 : Next.Sector; - memcpy(Block + 2, Target->DriverCode + Start, Length); + if (i + 1 < DataBlocks && !GeosAllocate(Disk, &Next)) return false; + GeosDriverBlock(Target, i, Next, Block); if (!GeosWriteTS(Disk, Here.Track, Here.Sector, Block)) return false; Here = Next; } @@ -405,7 +412,7 @@ struct GeosSlotRef { int Sector, Slot; }; struct GeosDirScan { - struct GeosSlotRef Free, FirstInputDriver; + struct GeosSlotRef Free, FirstInputDriver, Ours; bool HasFree, HasInputDriver, HasDeskTop, HasOurs; }; @@ -426,7 +433,7 @@ FLASHMEM static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosT if (!Slot[SlotType] && !Scan->HasFree) { Scan->Free = Here; Scan->HasFree = true; } if (!InUse) continue; - if (GeosSlotNamed(Slot, Target->DriverName)) Scan->HasOurs = true; + if (GeosSlotNamed(Slot, Target->DriverName)) { Scan->Ours = Here; Scan->HasOurs = true; } if (GeosSlotNamed(Slot, Target->DeskTopName)) Scan->HasDeskTop = true; if (Slot[SlotGeosType] == Target->InputType && !Scan->HasInputDriver) { Scan->FirstInputDriver = Here; Scan->HasInputDriver = true; } } @@ -536,3 +543,54 @@ FLASHMEM static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struc if (!Scan.HasInputDriver || !GeosSlotBefore(Scan.FirstInputDriver, Scan.Free)) return true; return GeosSwapEntries(Disk, Chain, Scan.FirstInputDriver, Scan.Free); } + +// Makes the copy's TR mouse driver the firmware's, in place: a copy outlives the firmware that set it up, and an older +// driver may not find the registers where this one puts them. One of another length is refused, the image unchanged. +FLASHMEM static inline bool GeosRefreshDriver(struct GeosDisk *Disk, bool *Changed) +{ + const struct GeosTarget *Target = Disk->Target; + const uint16_t DataBlocks = GeosDriverDataBlocks(Target); + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS], First, Info, At; + uint8_t Block[GEOS_SECTOR_SIZE], Wanted[GEOS_SECTOR_SIZE]; + const uint8_t *Slot; + struct GeosDirScan Scan; + uint16_t Blocks = 0; + int Length; + + *Changed = false; + if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; + if (!Scan.HasOurs) return true; + if (!GeosReadTS(Disk, Chain[Scan.Ours.Sector].Track, Chain[Scan.Ours.Sector].Sector, Block)) return false; + Slot = Block + Scan.Ours.Slot * GEOS_DIR_ENTRY_SIZE; + First = (struct GeosTS){ Slot[SlotFirst], Slot[SlotFirst + 1] }; + Info = (struct GeosTS){ Slot[SlotInfo], Slot[SlotInfo + 1] }; + for (At = First; At.Track && Blocks <= DataBlocks; Blocks++) + { + if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) break; + At = (struct GeosTS){ Block[0], Block[1] }; + } + if (Blocks != DataBlocks || !Info.Track || Info.Track > Disk->Format->Tracks) + { + snprintf(Disk->Error, sizeof Disk->Error, PSTR("its %s is an old one; delete this copy"), Target->DriverName); + return false; + } + + for (uint16_t i = 0; First.Track; i++) + { + struct GeosTS Next; + uint16_t Used; + if (!GeosReadTS(Disk, First.Track, First.Sector, Block)) return false; + Next = (struct GeosTS){ Block[0], Block[1] }; + Used = GeosDriverBlock(Target, i, Next, Wanted); + if (memcmp(Block, Wanted, Used)) + { + if (!GeosWriteTS(Disk, First.Track, First.Sector, Wanted)) return false; + *Changed = true; + } + First = Block[0] ? Next : (struct GeosTS){ 0, 0 }; + } + if (!GeosReadTS(Disk, Info.Track, Info.Sector, Block)) return false; + if (!memcmp(Block, Target->DriverInfo, GEOS_SECTOR_SIZE)) return true; + *Changed = true; + return GeosWriteTS(Disk, Info.Track, Info.Sector, Target->DriverInfo); +} diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 845aad53..fc89389b 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -287,7 +287,8 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * return false; } Disk = GeosDiskOn(Launcher, &Image); - Built = GeosPrepareConfigure(&Disk, RAM_Image, RAM_ImageSize, &Changed) && (Launcher->Machine == GeosC128 + Built = GeosRefreshDriver(&Disk, &Changed) && GeosPrepareConfigure(&Disk, RAM_Image, RAM_ImageSize, &Changed) && + (Launcher->Machine == GeosC128 ? GeosBuildC128Boot(&Disk, &Now, GEOS_REGS_BESIDE_GEORAM, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) : GeosBuildBootPrg(&Disk, &Now, RAM_Image, RAM_ImageSize, &Length, &ClockSet)); if (!Built) SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Disk.Error); diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 75c193f3..3430e5f1 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -657,3 +657,70 @@ for (const version of ['64', '128']) { }); }); } + +const MOUSE_NAME = 'TR USB MOUSE'; +const SLOT_INFO = 21; +const INPUT_TYPE = { 64: 10, 128: 15 }; +const MOUSE = Object.fromEntries(['64', '128'].map((version) => { + const array = arrayIn(path.join(FIRMWARE_ROOT, `GEOS/TRMouse${version}.h`)); + return [version, { code: Buffer.from(array(`TRMouse${version}Code`)), info: Buffer.from(array(`TRMouse${version}Info`)) }]; +})); + +// The TR mouse driver as a copy holds it: its code in a chain, its info block in a sector of its own +function addMouse(disk, version, code, info) { + const slot = disk.addFile(MOUSE_NAME, code, INPUT_TYPE[version]); + const at = disk.writeChain(Buffer.alloc(SECTOR - 2)); + info.copy(disk.image, offsetOf(at.track, at.sector)); + disk.image[slot + SLOT_INFO] = at.track; + disk.image[slot + SLOT_INFO + 1] = at.sector; + return { slot, info: offsetOf(at.track, at.sector) }; +} + +for (const version of ['64', '128']) { + test(`GEOS ${version}: a copy's older TR mouse driver is replaced in place by the firmware's`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + const old = Buffer.from(MOUSE[version].code.subarray(0, MOUSE[version].code.length - 30)).fill(0x60, 0, 5); + const oldInfo = Buffer.from(MOUSE[version].info).fill(0, 0x49, 0x4b); + const { slot, info } = addMouse(disk, version, old, oldInfo); + + const first = launch(disk, [], version); + assert.equal(first.status, 0, first.err); + assert.match(first.out, /mouse=1/); + const data = offsetOf(first.image[slot + SLOT_FIRST], first.image[slot + SLOT_FIRST + 1]); + assert.equal(first.image[data], 0, 'still one data block'); + assert.equal(first.image.subarray(data + 2, data + first.image[data + 1] + 1).equals(MOUSE[version].code), true, 'the firmware driver'); + assert.equal(first.image.subarray(info, info + SECTOR).equals(MOUSE[version].info), true, 'the firmware info block'); + + const again = launch({ image: first.image }, [], version); + assert.match(again.out, /mouse=0/); + assert.equal(again.image.equals(first.image), true); + }); + }); + + test(`GEOS ${version}: a copy whose TR mouse driver takes another number of blocks is refused, to be deleted`, { skip }, () => { + withHarness((launch) => { + const disk = diskFor(version); + addMouse(disk, version, Buffer.alloc(SECTOR + 44, 0x60), MOUSE[version].info); + const r = launch(disk, [], version); + assert.equal(r.status, 1); + assert.match(r.err, /TR USB MOUSE is an old one; delete this copy/); + }); + }); + + test(`GEOS ${version}: a copy whose TR mouse driver points off the disk is refused, the disk unchanged`, { skip }, () => { + withHarness((launch) => { + for (const [why, damage] of [ + ['no info block', (image, { slot }) => (image[slot + SLOT_INFO] = 0)], + ['an info block past the last track', (image, { slot }) => (image[slot + SLOT_INFO] = TRACKS + 1)], + ]) { + const disk = diskFor(version); + const mouse = addMouse(disk, version, Buffer.from(MOUSE[version].code).fill(0x60, 0, 5), MOUSE[version].info); + damage(disk.image, mouse); + const r = launch(disk, [], version); + assert.equal(r.status, 1, why); + assert.match(r.err, /an old one; delete this copy/, why); + } + }); + }); +} diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 819a42fb..89a86369 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT // geos_boot_harness 64|128 IN.d81 OUT OUT.d81 [HOUR MINUTE]: builds the GEOS 64 boot PRG, or the GEOS 128 boot ROM // and the boot image behind it, from the disk, sets up its CONFIGURE files, sets the clock to 2026-10-21 at -// HOUR:MINUTE (15:07 if not given), and writes both. Prints "changed=<0|1> clock=<0|1>", and for GEOS 128 +// HOUR:MINUTE (15:07 if not given), and writes both. Prints "changed=<0|1> clock=<0|1> mouse=<0|1>", the last whether +// the TR mouse driver on the disk was brought up to date, and for GEOS 128 // " boot=," where BOOT's image lies in OUT; exits 1 with the refusal on stderr. REGS_PAGE (hex) is the // block device's page the GEOS 128 boot ROM is built for, beside GeoRAM if not set. #include @@ -32,7 +33,7 @@ int main(int argc, char **argv) uint32_t Length, BootSize = 0; const uint8_t *Boot = NULL; const char *RegsPage = getenv("REGS_PAGE"); - bool Is128, Changed, ClockSet, Built; + bool Is128, Changed, Refreshed, ClockSet, Built; FILE *File; size_t Size; @@ -51,7 +52,7 @@ int main(int argc, char **argv) Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; - Built = GeosPrepareConfigure(&Disk, Prg, sizeof Prg, &Changed) && (Is128 ? GeosBuildC128Boot(&Disk, &Now, RegsPage ? (uint8_t)strtol(RegsPage, NULL, 16) : GEOS_REGS_BESIDE_GEORAM, + Built = GeosRefreshDriver(&Disk, &Refreshed) && GeosPrepareConfigure(&Disk, Prg, sizeof Prg, &Changed) && (Is128 ? GeosBuildC128Boot(&Disk, &Now, RegsPage ? (uint8_t)strtol(RegsPage, NULL, 16) : GEOS_REGS_BESIDE_GEORAM, Prg, sizeof Prg, &Boot, &BootSize, &ClockSet) : GeosBuildBootPrg(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet)); if (!Built) @@ -59,7 +60,7 @@ int main(int argc, char **argv) fprintf(stderr, "%s\n", Disk.Error); return 1; } - printf("changed=%d clock=%d", Changed, ClockSet); + printf("changed=%d clock=%d mouse=%d", Changed, ClockSet, Refreshed); if (Is128) { Length = (uint32_t)(Boot - Prg) + BootSize; diff --git a/docs/GEOS64_Usage.md b/docs/GEOS64_Usage.md index c50885ec..72a82e49 100644 --- a/docs/GEOS64_Usage.md +++ b/docs/GEOS64_Usage.md @@ -21,6 +21,7 @@ * GEOS saves to the disk it starts from, so the TeensyROM never starts from your original image. * The first time you select GEOS 64, it makes a copy next to the original with `-TR` added to the name (`GEOS64.D81` becomes `GEOS64-TR.D81`) and adds the TR USB mouse driver to the copy. Your original is not changed. * Later launches reuse the `-TR` copy, so your saved work is still there. + * Each launch brings the copy's TR USB mouse driver up to date with the firmware's. * To start over from your original, delete the `-TR` copy. The next launch makes a new one. ## GeoRAM @@ -51,6 +52,7 @@ If GEOS 64 can't start, the TeensyROM stays at the menu and shows one of these: | Could not add the mouse driver to a copy of ``: the SD card could not be written | The copy could not be written to the SD card. | | `-TR.D81` has no desktop; remove it to start over | The `-TR` copy has lost `DESK TOP`. Delete it. | | `-TR.D81` was set aside and could not be put back | Power was lost while the copy was being updated, and the TeensyROM could not restore it. Look for `-TR.D81.BAK` in `/GEOS64`. | +| `-TR.D81`: its TR USB MOUSE is an old one; delete this copy | The copy's mouse driver is from a TeensyROM firmware whose driver cannot be updated in place. Delete the `-TR` copy; the next launch makes a new one. | | `-TR.D81`: the boot file is not on the disk | The image has no `GEOBOOT`. | | `-TR.D81`: the boot file's disk driver is not one the TR can replace | This `GEOBOOT` is not a version the TeensyROM can start. | From 20bdc15adb006493f414582a0894efb41c2bc988 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:53:54 -0500 Subject: [PATCH 08/19] fix: review findings for 51da552 --- Source/Teensy/GEOS/GeosDiskPatch.h | 6 +++--- Source/Teensy/tests/geos-boot.test.js | 5 +++++ Source/Teensy/tests/geos_boot_harness.c | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 565727a0..bd840f35 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -564,12 +564,12 @@ FLASHMEM static inline bool GeosRefreshDriver(struct GeosDisk *Disk, bool *Chang Slot = Block + Scan.Ours.Slot * GEOS_DIR_ENTRY_SIZE; First = (struct GeosTS){ Slot[SlotFirst], Slot[SlotFirst + 1] }; Info = (struct GeosTS){ Slot[SlotInfo], Slot[SlotInfo + 1] }; - for (At = First; At.Track && Blocks <= DataBlocks; Blocks++) + for (At = First; At.Track && At.Track <= Disk->Format->Tracks && Blocks <= DataBlocks; Blocks++) { - if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) break; + if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; At = (struct GeosTS){ Block[0], Block[1] }; } - if (Blocks != DataBlocks || !Info.Track || Info.Track > Disk->Format->Tracks) + if (At.Track || Blocks != DataBlocks || !Info.Track || Info.Track > Disk->Format->Tracks) { snprintf(Disk->Error, sizeof Disk->Error, PSTR("its %s is an old one; delete this copy"), Target->DriverName); return false; diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 3430e5f1..bb7b232e 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -713,6 +713,11 @@ for (const version of ['64', '128']) { for (const [why, damage] of [ ['no info block', (image, { slot }) => (image[slot + SLOT_INFO] = 0)], ['an info block past the last track', (image, { slot }) => (image[slot + SLOT_INFO] = TRACKS + 1)], + ['a data block linked past the last track', (image, { slot }) => { + const data = offsetOf(image[slot + SLOT_FIRST], image[slot + SLOT_FIRST + 1]); + image[data] = TRACKS + 1; + image[data + 1] = 0; + }], ]) { const disk = diskFor(version); const mouse = addMouse(disk, version, Buffer.from(MOUSE[version].code).fill(0x60, 0, 5), MOUSE[version].info); diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 89a86369..0c32b997 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -13,15 +13,19 @@ #define MAX_PRG (128 * 1024) static uint8_t Image[MAX_IMAGE + 1], Prg[MAX_PRG]; +static size_t Size; +// Like the SD card's file, a block past the image's end can be neither read nor written static bool ReadBlock(void *Context, uint32_t Offset, uint8_t *Block) { + if (Offset + GEOS_SECTOR_SIZE > Size) return false; memcpy(Block, (uint8_t *)Context + Offset, GEOS_SECTOR_SIZE); return true; } static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) { + if (Offset + GEOS_SECTOR_SIZE > Size) return false; memcpy((uint8_t *)Context + Offset, Block, GEOS_SECTOR_SIZE); return true; } @@ -35,7 +39,6 @@ int main(int argc, char **argv) const char *RegsPage = getenv("REGS_PAGE"); bool Is128, Changed, Refreshed, ClockSet, Built; FILE *File; - size_t Size; if ((argc != 5 && argc != 7) || !(File = fopen(argv[2], "rb"))) return 2; Is128 = !strcmp(argv[1], "128"); From 55b01476cc9d593150b48a8d166a59755335ab2c Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:55:27 -0500 Subject: [PATCH 09/19] firmware: Special IO "REU+USB Mouse/Disk" runs GEOS with the TR+'s REU The REU answers at $DF00 as before, and the USB mouse and block device move to IO1, the page an REU leaves free. A GEOS launch takes this handler when it is the Special IO setting, and GeoRAM otherwise, and builds the GEOS 128 boot ROM for the page that handler puts the registers on. Beside the REU, $DF0B-$DF1F read $FF, as a 1764's unused registers do, so the TRB id the GEOS drivers look for at $DF10 cannot appear there. --- Source/Teensy/GeosLauncher.ino | 12 +++++- Source/Teensy/IOHandlers.ino | 3 +- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 3 ++ .../MinimalBoot/Common/IO_Handlers/IOH_REU.c | 40 +++++++++++++++++++ Source/Teensy/MinimalBoot/Common/Menu_Regs.h | 11 ++++- docs/GEOS64_Usage.md | 6 +++ docs/TR_Block_Device.md | 6 +-- 7 files changed, 75 insertions(+), 6 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index fc89389b..4bba1049 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -268,6 +268,16 @@ FLASHMEM static bool GeosUseC128Boot(const uint8_t *ImageEnd, StructMenuItem *It return SetC128Type(Item); } +// GEOS runs beside GeoRAM unless Special IO is set to the REU with the TR's registers beside it, which GEOS's own +// CONFIGURE then finds +uint8_t GeosIOHandler() +{ +#ifdef Fab04_REU + if (IO1[rwRegNextIOHndlr] == IOH_REU_TRPage) return IOH_REU_TRPage; +#endif + return IOH_GeoRAM; +} + FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char *Name, StructMenuItem *Item) { char Path[MaxNamePathLength]; @@ -289,7 +299,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * Disk = GeosDiskOn(Launcher, &Image); Built = GeosRefreshDriver(&Disk, &Changed) && GeosPrepareConfigure(&Disk, RAM_Image, RAM_ImageSize, &Changed) && (Launcher->Machine == GeosC128 - ? GeosBuildC128Boot(&Disk, &Now, GEOS_REGS_BESIDE_GEORAM, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) + ? GeosBuildC128Boot(&Disk, &Now, GeosIOHandler() == IOH_GeoRAM ? GEOS_REGS_BESIDE_GEORAM : GEOS_REGS_BESIDE_REU, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) : GeosBuildBootPrg(&Disk, &Now, RAM_Image, RAM_ImageSize, &Length, &ClockSet)); if (!Built) SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Disk.Error); Image.close(); diff --git a/Source/Teensy/IOHandlers.ino b/Source/Teensy/IOHandlers.ino index b71843f3..2055a5d4 100644 --- a/Source/Teensy/IOHandlers.ino +++ b/Source/Teensy/IOHandlers.ino @@ -44,7 +44,8 @@ void IOHandlerNextInit() void IOHandlerSelectInit() { //called after cart loaded, PRG x-fer finished, or exit to basic (rsIOHWSelInit) PendingfBusSnoop = NULL; //clean slate for whatever InitHndlr below is about to stage - if (IO1[rWRegCurrMenuWAIT] == rmtTeensy && MenuSource[SelItemFullIdx].IOHndlrAssoc != IOH_None) + if (IO1[rWRegCurrMenuWAIT] == rmtTeensy && MenuSource[SelItemFullIdx].ItemType == rtGEOS) IOHandlerInit(GeosIOHandler()); + else if (IO1[rWRegCurrMenuWAIT] == rmtTeensy && MenuSource[SelItemFullIdx].IOHndlrAssoc != IOH_None) { Printf_dbg("IO Handler set by Teensy Menu\n"); IOHandlerInit(MenuSource[SelItemFullIdx].IOHndlrAssoc); diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index ae1d60b8..b6c61a44 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -164,6 +164,9 @@ stcIOHandlers* IOHandler[] = //Synch order/qty with enum enumIOHandlers #endif &IOHndlr_Debug, //IOH_Debug, &IOHndlr_GeoRAM, //IOH_GeoRAM, + #ifdef Fab04_REU + &IOHndlr_REU_TRPage, //IOH_REU_TRPage, + #endif //*not* manually selectable, see LastSelectableIOH diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index 36e46597..6b7110f5 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -749,3 +749,43 @@ FLASHMEM void PollingHndlr_REU() #endif } +//______________________________________________________________________________________________ +// The REU with the TR's USB mouse and block device beside it in IO1, where a GEOS that found the REU looks for them + +void IO2Hndlr_REU_TRPage(uint8_t Address, bool R_Wn); +void PollingHndlr_REU_TRPage(); +void InitHndlr_REU_TRPage(); + +stcIOHandlers IOHndlr_REU_TRPage = +{ + "REU+USB Mouse/Disk", //Name of handler + &InitHndlr_REU_TRPage, //Called once at handler startup + &TRPageIO, //IO1 R/W handler + &IO2Hndlr_REU_TRPage, //IO2 R/W handler + NULL, //ROML Read handler, in addition to any ROM data sent + NULL, //ROMH Read handler, in addition to any ROM data sent + &PollingHndlr_REU_TRPage, //Polled in main routine + NULL, //called at the end of EVERY c64 cycle +}; + +FLASHMEM void InitHndlr_REU_TRPage() +{ + InitUSBMouse(); + InitTRBlock(); + InitHndlr_REU(); +} + +FLASHMEM void PollingHndlr_REU_TRPage() +{ + PollUSBMouse(); + PollTRBlock(); + PollingHndlr_REU(); +} + +// A 1764's unused registers read $FF. The TR's GEOS drivers look for the block device's ID at $DF10 first, which +// must not answer there beside the REU. +void IO2Hndlr_REU_TRPage(uint8_t Address, bool R_Wn) +{ + if (R_Wn && NumREU_Banks && (Address & 0x1f) >= REUReg_NumRegs) DataPortWriteWaitLog(0xff); + else IO2Hndlr_REU(Address, R_Wn); +} diff --git a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h index b1923116..0e8fc4ae 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -443,7 +443,12 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) IOH_KernalReplace, #endif IOH_Debug, + #ifdef Fab04_REU + IOH_GeoRAM, + IOH_REU_TRPage, //last manually selectable, see LastSelectableIOH + #else IOH_GeoRAM, //last manually selectable, see LastSelectableIOH + #endif //*not* manually selectable, see LastSelectableIOH @@ -477,4 +482,8 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) IOH_Num_Handlers //always last }; -#define LastSelectableIOH IOH_GeoRAM //127 max. Used in IOH_TeensyROM.c for Special IO display/select +#ifdef Fab04_REU + #define LastSelectableIOH IOH_REU_TRPage //127 max. Used in IOH_TeensyROM.c for Special IO display/select +#else + #define LastSelectableIOH IOH_GeoRAM +#endif diff --git a/docs/GEOS64_Usage.md b/docs/GEOS64_Usage.md index 72a82e49..dab112f7 100644 --- a/docs/GEOS64_Usage.md +++ b/docs/GEOS64_Usage.md @@ -34,6 +34,12 @@ * GEOS's own `CONFIGURE` runs at start-up, set up in the `-TR` copy to find the TeensyROM as drive A. It does not find GeoRAM, so GEOS does not set GeoRAM up as a RAM disk. * Other configuration programs on the disk (such as `CONFIG 2.0r`) are kept from running at start-up, since they look for a disk drive. +## REU (TR+ only) +* To run GEOS with the TR+'s 512K REU instead of GeoRAM, set Special IO to `REU+USB Mouse/Disk` in the Settings menu (`F8`), then select GEOS 64 or GEOS 128. + * GEOS's own `CONFIGURE` finds the REU at start-up and sets it up as GEOS does a 1764 or 1750. + * At launch, the TeensyROM loads the REU pre-load/save image shown in the Settings menu into the REU. To save the REU to that file, press and release the Alt button. +* With any other Special IO setting, GEOS runs with GeoRAM. + ## Date and time * GEOS starts with the date and time of the TeensyROM's real-time clock, adjusted by the Time Zone setting. * Set the clock and time zone in the Settings menu (`F8`, page 6). See [General Usage](General_Usage.md#the-settings-menu). diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index 9f88596c..f4c53f5a 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -19,13 +19,13 @@ This document is the whole contract. The firmware side is [`TRBlock.h`](../Sourc ## What it is and is not * One image at a time, chosen by the firmware (for GEOS, when the menu launches it). The C64 cannot choose or replace the image. * Blocks are 256 bytes. A block is addressed by two bytes whose meaning depends on the image kind ([Disk addresses](#disk-addresses)). -* It is available while the GeoRAM handler is active, which is also where the USB mouse registers are. It is not available under other handlers, the REU included, and not under a Host Extension. +* It is available while the GeoRAM handler or the `REU+USB Mouse/Disk` handler (TR+ only) is active, which is also where the USB mouse registers are. It is not available under other handlers, the plain REU included, and not under a Host Extension. * It is not an IEC drive. Software that talks to the IEC bus cannot see it. ## Where the registers are -The registers share IO2, the page GeoRAM leaves free, with the USB mouse registers, which start the page and identify it. The base `B` is `$DF00`. +The registers share a page with the USB mouse registers, which start it and identify it. The base `B` is `$DF00` beside GeoRAM, in the IO2 page GeoRAM leaves free, and `$DE00` beside the REU, whose registers repeat across IO2. -The block device is present when `TRB` is at `B+$10..B+$12`. A handler that does not carry it leaves those bytes unanswered, which reads as whatever the bus last held, so check all three bytes. +The block device is present when `TRB` is at `B+$10..B+$12`. A handler that does not carry it leaves those bytes unanswered, which reads as whatever the bus last held, so check all three bytes. Look at `$DF10` before `$DE10`: beside the REU, `$DF10..$DF12` read `$FF`, as a 1764's unused registers do, while `$DE10` beside GeoRAM is its window, which can hold any bytes. Everything below is an offset from `B`. From 2c213b31b5a225e39baf746f94ca6f926a917c9b Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 16:58:55 -0500 Subject: [PATCH 10/19] fix: review findings for d9c2551 --- Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 8 ++++---- Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c | 4 ++-- docs/GEOS64_Usage.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index c9dfb6d6..af7253f4 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -// The TR block device (docs/TR_Block_Device.md) in IO2, the page GeoRAM leaves free, beside the USB -// mouse's registers. The ISR side only fills registers and the window; the SD card is touched -// only from the main loop: PollTRBlock from GeoRAM's polling handler, and SDFullInit. +// The TR block device (docs/TR_Block_Device.md) beside the USB mouse's registers, in IO2 beside GeoRAM +// and in IO1 beside the REU. The ISR side only fills registers and the window; the SD card is touched +// only from the main loop: PollTRBlock from those two handlers' polling handlers, and SDFullInit. #include "../../../BlockDevice/TRBlockImage.h" @@ -68,7 +68,7 @@ FLASHMEM static bool TRBlockOpen() return true; } -// Close and reopen around an SD.begin that can run while GeoRAM has an image mounted, as SDFullInit does. +// Close and reopen around an SD.begin that can run while an image is mounted, as SDFullInit does. // SD.begin rebuilds the volume in place under any open file and drops its cached, unwritten sectors. FLASHMEM void TRBlockClose() { diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index c3612801..175f947e 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// A USB mouse on the TR's USB host port, read through registers the GeoRAM handler serves -// at $DF00, in the part of the IO2 page that GeoRAM leaves free. +// A USB mouse on the TR's USB host port, read through registers at $DF00 beside GeoRAM, in the +// part of IO2 that GeoRAM leaves free, and at $DE00 beside the REU (IOH_REU_TRPage). // It cannot be a 1351: the SID's POT lines are not on the cartridge port, so only software // with a TR driver sees it (tools/geos). // diff --git a/docs/GEOS64_Usage.md b/docs/GEOS64_Usage.md index dab112f7..5fdb0895 100644 --- a/docs/GEOS64_Usage.md +++ b/docs/GEOS64_Usage.md @@ -35,8 +35,8 @@ * Other configuration programs on the disk (such as `CONFIG 2.0r`) are kept from running at start-up, since they look for a disk drive. ## REU (TR+ only) -* To run GEOS with the TR+'s 512K REU instead of GeoRAM, set Special IO to `REU+USB Mouse/Disk` in the Settings menu (`F8`), then select GEOS 64 or GEOS 128. - * GEOS's own `CONFIGURE` finds the REU at start-up and sets it up as GEOS does a 1764 or 1750. +* To run GEOS with the TR+'s REU (128K to 512K, as much as fits) instead of GeoRAM, set Special IO to `REU+USB Mouse/Disk` in the Settings menu (`F8`), then select GEOS 64 or GEOS 128. + * GEOS's own `CONFIGURE` finds the REU at start-up and sets it up as GEOS does a Commodore REU of that size. * At launch, the TeensyROM loads the REU pre-load/save image shown in the Settings menu into the REU. To save the REU to that file, press and release the Alt button. * With any other Special IO setting, GEOS runs with GeoRAM. From 9861fc8e7e020dfc2de91548fd3a029b602a9b31 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 19:13:44 -0500 Subject: [PATCH 11/19] fix: review findings for bd7676a --- docs/TR_Block_Device.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index f4c53f5a..e29479c5 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -116,7 +116,7 @@ The firmware does this arithmetic so that a 6502 driver does not have to. A driv ## A reader's checklist A driver that follows this document: -* checks `TRB` at `$DF10`; +* checks `TRB` at `$DF10`, then at `$DE10`; * writes TOKEN, the address, PAGE, then CMD; * reads STATUS until two reads agree and the value is not `$80`, then checks ECHO; * moves a block as four PAGEs of the window; From b957e8eb4349988f84db1889c4f27019971d7c2e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 19:32:09 -0500 Subject: [PATCH 12/19] firmware: CONFIGURE's ReadBlock range check builds under GCC's -Wsign-compare --- Source/Teensy/GEOS/GeosBoot.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index b4438dc0..76b263d6 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -225,6 +225,7 @@ FLASHMEM static bool GeosSetUpConfigure(struct GeosDisk *Disk, const uint8_t *Sl uint8_t Index[GEOS_SECTOR_SIZE], *Driver, *At; struct GeosTS Probe, Driven; uint32_t ProbeLength, DriverLength; + uint16_t ReadBlock; *Ours = false; if (Slot[SlotStructure] != GEOS_VLIR || !GeosSlotNamed(Slot, Name)) return true; @@ -240,7 +241,8 @@ FLASHMEM static bool GeosSetUpConfigure(struct GeosDisk *Disk, const uint8_t *Sl Driver = Scratch + ProbeLength; if (!GeosReadChain(Disk, Driven, Name, Driver, Capacity - ProbeLength, &DriverLength)) return false; if (DriverLength <= GEOS_TYPE_BYTE || !GeosIsDriverAt(Driver)) return true; - if (GeosWord(Driver + 2 * VecReadBlock) - GEOS_DRIVER_AREA > DriverLength) return true; + ReadBlock = GeosWord(Driver + 2 * VecReadBlock); + if (ReadBlock < GEOS_DRIVER_AREA || ReadBlock > GEOS_DRIVER_AREA + DriverLength) return true; if (!GeosReplaceTransport(Driver, Disk->Target->DiskDriver)) return true; memcpy(At, GeosTRProbe, sizeof GeosTRProbe); if (!GeosRewriteChain(Disk, Driven, Driver) || !GeosRewriteChain(Disk, Probe, Scratch)) return false; From 4a8f31e9dcf116462fcff9f966ebd59494831fb2 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 19:38:04 -0500 Subject: [PATCH 13/19] test: the REU+USB Mouse/Disk handler's IO2 page reads as a 1764's, and not at all without an REU The TR's GEOS drivers decide which page holds the block device by what $DF10 reads, and no test ran IO2Hndlr_REU_TRPage itself: dropping its $FF answer, its bank check or its 32-byte mirror left the suite green. --- Source/Teensy/tests/reu-trpage.test.js | 49 ++++++++++++ Source/Teensy/tests/reu_trpage_harness.cpp | 87 ++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 Source/Teensy/tests/reu-trpage.test.js create mode 100644 Source/Teensy/tests/reu_trpage_harness.cpp diff --git a/Source/Teensy/tests/reu-trpage.test.js b/Source/Teensy/tests/reu-trpage.test.js new file mode 100644 index 00000000..ebd2f409 --- /dev/null +++ b/Source/Teensy/tests/reu-trpage.test.js @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// The REU's IO2 page under "REU+USB Mouse/Disk", where the TR's GEOS drivers look for the block device first. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +import { commandExists } from '../../../tools/lib/toolchain.mjs'; + +const HARNESS_SOURCE = path.join(import.meta.dirname, 'reu_trpage_harness.cpp'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const skip = !commandExists('c++') && !inCi && 'c++ is not on PATH'; + +const BANKS = 16; +const BLOCK_ID = 0x10; + +let harnessPath; +function harness() { + if (harnessPath) return harnessPath; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reu-trpage-')); + harnessPath = path.join(dir, 'harness'); + // the REU's DMA loops spin on an empty body, which the firmware's own build allows + const built = spawnSync('c++', ['-std=gnu++17', '-Wall', '-Wextra', '-Werror', '-Wno-empty-body', '-o', harnessPath, + HARNESS_SOURCE], { encoding: 'utf8' }); + assert.equal(built.status, 0, built.stderr); + return harnessPath; +} + +function run(lines) { + const ran = spawnSync(harness(), { input: lines.join('\n') + '\n', encoding: 'utf8' }); + assert.equal(ran.status, 0, ran.stderr); + return ran.stdout.split('\n').filter(Boolean); +} + +const read = (offset) => `r ${offset.toString(16)}`; + +test('beside an REU, its registers answer and the rest of each 32 bytes reads $FF, as a 1764\'s do', { skip }, () => { + const offsets = [0x09, 0x0a, 0x0b, BLOCK_ID, BLOCK_ID + 2, 0x1f, 0x2a, 0x20 + BLOCK_ID, 0xff]; + assert.deepEqual(run(['init 0', `banks ${BANKS}`, ...offsets.map(read)]), + ['1f', '3f', 'ff', 'ff', 'ff', 'ff', '3f', 'ff', 'ff']); +}); + +test('with no RAM for the REU, nothing in IO2 answers, so software finds no REU there', { skip }, () => { + assert.deepEqual(run(['init 0', 'banks 0', ...[0x00, 0x0a, BLOCK_ID].map(read)]), ['open', 'open', 'open']); +}); diff --git a/Source/Teensy/tests/reu_trpage_harness.cpp b/Source/Teensy/tests/reu_trpage_harness.cpp new file mode 100644 index 00000000..a0b37992 --- /dev/null +++ b/Source/Teensy/tests/reu_trpage_harness.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// reu_trpage_harness: the REU's IO2 page as IOH_REU_TRPage serves it, driven from stdin one command a line: +// init 0 the handler starts, with no RAM to give the REU +// banks N the REU has N banks (0: none could be allocated) +// r OFFSET an IO2 read, printing the byte driven or "open" +#include +#include +#include + +#define FLASHMEM +#define RAM_Bank_Size 0x2000 +#define MaxPathLength 256 +#define SetIRQAssert +#define SetIRQDeassert +#define SetDMAAssert +#define SetDMADeassert +#define ReadGPIO6 0 +#define GP6_Phi2(Port) ((Port) & 0) +#define ARM_DWT_CYCCNT 0u +#define WaitUntil_nS(nS) +#define GPIO6_ISR Discard +#define CORE_PIN1_BITMASK 0 +#define NVIC_CLEAR_PENDING(Irq) +#define IRQ_GPIO6789 0 + +struct StructMenuItem; +enum DMA_Trans_RnW { DMA_READ, DMA_WRITE }; +enum DMA_Addr_Mode { DMA_ADDR_INCREMENT, DMA_ADDR_FIXED }; +struct stcIOHandlers +{ + char Name[20]; + void (*InitHndlr)(); + void (*IO1Hndlr)(uint8_t Address, bool R_Wn); + void (*IO2Hndlr)(uint8_t Address, bool R_Wn); + void (*ROMLHndlr)(uint32_t Address); + void (*ROMHHndlr)(uint32_t Address); + void (*PollingHndlr)(); + void (*CycleHndlr)(); +}; +static struct { template void printf(const char *, Args...) {} } Serial; + +static uint32_t Discard, StartCycCnt, LastCycCnt; +static uint32_t micros() { return 0; } +static const int eepAdREUFilename = 0; +static void EEPreadStr(int, char *Out) { *Out = 0; } +static bool (*fBusSnoop)(uint16_t Address, bool R_Wn), (*PendingfBusSnoop)(uint16_t Address, bool R_Wn); +void (*fSpecialBtnChange)(bool Up_nDn); +bool DMA_RnW, DMA_FixC64Addr; +uint32_t DMA_Length, DMA_Count, DMA_StartAddr; +StructMenuItem *DriveDirMenu; +bool DMAByte(uint8_t *) { return true; } +static uint8_t AllocRAMBanks(uint8_t **, uint8_t, uint8_t) { return 0; } +static void FreeRAMBanks(uint8_t **, uint8_t *Count) { *Count = 0; } +static void LoadRAMBanks(uint8_t **, uint8_t, const char *) {} +static void SaveRAMBanks(uint8_t **, uint8_t, const char *) {} +static void InitUSBMouse() {} +static void InitTRBlock() {} +static void PollUSBMouse() {} +static void PollTRBlock() {} +static void TRPageIO(uint8_t, bool) {} + +static int Driven; +static void DataPortWriteWaitLog(uint8_t Data) { Driven = Data; } +static uint8_t DataPortWaitRead() { return 0; } + +#include "../MinimalBoot/Common/IO_Handlers/IOH_REU.c" + +int main() +{ + char Command[16]; + unsigned Value; + + while (scanf("%15s %x", Command, &Value) == 2) + { + if (!strcmp(Command, "init")) IOHndlr_REU_TRPage.InitHndlr(); + else if (!strcmp(Command, "banks")) NumREU_Banks = Value; + else if (!strcmp(Command, "r")) + { + Driven = -1; + IOHndlr_REU_TRPage.IO2Hndlr(Value, true); + if (Driven < 0) printf("open\n"); + else printf("%02x\n", Driven); + } + else return 1; + } + return 0; +} From 3f7348441501fabfb0f03f35b42f23528f89d630 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 19:39:24 -0500 Subject: [PATCH 14/19] test: a GEOS launch runs under the handler its boot ROM's page was chosen for Both the C128 boot ROM's register page and the handler come from GeosIOHandler(), and no test held either: dropping IOHandlerSelectInit's GEOS branch puts GEOS under the item's GeoRAM while the boot ROM looks beside the REU, and the suite stayed green. --- Source/Teensy/tests/menu-selection.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Source/Teensy/tests/menu-selection.test.js b/Source/Teensy/tests/menu-selection.test.js index c5b3aefe..29353842 100644 --- a/Source/Teensy/tests/menu-selection.test.js +++ b/Source/Teensy/tests/menu-selection.test.js @@ -32,3 +32,15 @@ test('GEOS 128, listed on every machine, starts only once the menu has reported assert.match(functionBody(source, 'LaunchGeos'), /^\{[^{}]*?\bif\s*\(\s*Launcher->Machine\s*==\s*GeosC128\s*&&\s*!\s*HostIsC128\s*\(\s*\)\s*\)\s*\{[^}]*\breturn\s+false\s*;/); }); + +test('GEOS starts under the handler the launch pointed its boot ROM at, not the item\'s own GeoRAM', () => { + // A GEOS 128 boot ROM pointed at the page beside the REU finds no block device under GeoRAM + const launcher = blankComments(fs.readFileSync(path.join(FIRMWARE_ROOT, 'GeosLauncher.ino'), 'utf8')); + assert.match(functionBody(launcher, 'GeosStart'), + /\bGeosIOHandler\s*\(\s*\)\s*==\s*IOH_GeoRAM\s*\?\s*GEOS_REGS_BESIDE_GEORAM\s*:\s*GEOS_REGS_BESIDE_REU\b/); + const handlers = blankComments(fs.readFileSync(path.join(FIRMWARE_ROOT, 'IOHandlers.ino'), 'utf8')); + const select = functionBody(handlers, 'IOHandlerSelectInit'); + const geos = select.search(/\bItemType\s*==\s*rtGEOS\s*\)\s*IOHandlerInit\s*\(\s*GeosIOHandler\s*\(\s*\)\s*\)/); + assert.notEqual(geos, -1, 'a GEOS item does not take its handler from GeosIOHandler'); + assert.ok(geos < select.indexOf('IOHndlrAssoc'), 'the item\'s own handler is chosen before GeosIOHandler is asked'); +}); From c9fceda39f28d0073ea702752a2d043c9e4af646 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 19:47:03 -0500 Subject: [PATCH 15/19] firmware: CONFIGURE's set-up keeps its helpers and tables out of RAM1 Giving GeosIsDriverAt and GeosReplaceTransport a second caller took them out of their inlined callers and into ITCM, 334 bytes on both TR and TR+, and the probe patterns and CONFIGURE names landed in DTCM. The build's memory report showed it; marking them FLASHMEM and PROGMEM, with GeosFindOnce and GeosBytesInBlock beside them, returns 320 bytes of RAM1 code on each target and leaves the TR's below where it was before the REU change. --- Source/Teensy/GEOS/GeosBoot.h | 12 ++++++------ Source/Teensy/GEOS/GeosTargets.h | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 76b263d6..5450c1a2 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -56,7 +56,7 @@ static bool GeosInDriverArea(uint16_t Address) { return Address >= GEOS_DRIVER_A // Whether a stock disk driver's tables start here: the vectors and the jump slots all point into the driver, and // the type byte names a 1541, 1571 or 1581 -static bool GeosIsDriverAt(const uint8_t *At) +FLASHMEM static bool GeosIsDriverAt(const uint8_t *At) { uint8_t Type = At[GEOS_TYPE_BYTE]; @@ -85,7 +85,7 @@ static int32_t GeosFindDriver(const uint8_t *Data, uint32_t Size) // The TR's driver goes over the stock driver's code from InitForIO up to ReadBlock, provided no vector or jump slot // it leaves alone points there, and every vector it implements is pointed at its own routines. Replaced routines // outside that run stay where they are, unused: GEOS 128 keeps NewDisk after ReadBlock. -static bool GeosReplaceTransport(uint8_t *Driver, const struct GeosDiskDriver *With) +FLASHMEM static bool GeosReplaceTransport(uint8_t *Driver, const struct GeosDiskDriver *With) { uint16_t Start = GeosWord(Driver + 2 * VecInitForIO), End = GeosWord(Driver + 2 * VecReadBlock); uint8_t *Code = Driver + (Start - GEOS_DRIVER_AREA); @@ -116,12 +116,12 @@ static bool GeosReplaceTransport(uint8_t *Driver, const struct GeosDiskDriver *W } // CONFIGURE's drive-type probe, which asks each IEC device what drive it is -static const uint8_t GeosStockProbe[] = { 0xa9, 0xe5, 0x85, 0x03, 0xa9, 0x80, 0x85, 0x02, 0x20 }; +static const uint8_t GeosStockProbe[] PROGMEM = { 0xa9, 0xe5, 0x85, 0x03, 0xa9, 0x80, 0x85, 0x02, 0x20 }; // What the TR puts over it: drive A, at device 8, is a 1581, and no other drive is there -static const uint8_t GeosTRProbe[] = { 0xa9, 0x00, 0xae, 0x89, 0x84, 0xe0, 0x08, 0xd0, 0x02, 0xa9, 0x03, 0x60 }; +static const uint8_t GeosTRProbe[] PROGMEM = { 0xa9, 0x00, 0xae, 0x89, 0x84, 0xe0, 0x08, 0xd0, 0x02, 0xa9, 0x03, 0x60 }; // Finds Pattern once in Body; NULL when it is absent or ambiguous -static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Pattern, uint32_t Size) +FLASHMEM static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Pattern, uint32_t Size) { uint8_t *Found = NULL; @@ -134,7 +134,7 @@ static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint8_t *Patt return Found; } -static uint32_t GeosBytesInBlock(const uint8_t *Block) { return Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); } +FLASHMEM static uint32_t GeosBytesInBlock(const uint8_t *Block) { return Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); } // A file's bytes along its sector chain from At; What names it in a refusal FLASHMEM static bool GeosReadChain(struct GeosDisk *Disk, struct GeosTS At, const char *What, uint8_t *Out, diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h index cc882e10..cce12510 100644 --- a/Source/Teensy/GEOS/GeosTargets.h +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -39,12 +39,15 @@ static const struct GeosDiskDriver TRDiskDriver128 = TRDriver128Relocations, sizeof TRDriver128Relocations / sizeof TRDriver128Relocations[0], TRDriver128Entries, }; +static const char Geos64ConfigureName[] PROGMEM = "CONFIGURE"; +static const char Geos128ConfigureName[] PROGMEM = "128 CONFIGURE"; + static const struct GeosLauncher Geos64Launcher = { "GEOS 64", "/GEOS64", GeosC64, { "DESK TOP", TRMouse64Name, TRMouse64Code, TRMouse64Info, sizeof TRMouse64Code, GEOS_INPUT_64, GEOS64_BOOT_FILE, GEOS64_BOOT_ENTRY, &TRDiskDriver64, - Geos64ClockRuns, sizeof Geos64ClockRuns / sizeof Geos64ClockRuns[0], 0, "CONFIGURE" }, + Geos64ClockRuns, sizeof Geos64ClockRuns / sizeof Geos64ClockRuns[0], 0, Geos64ConfigureName }, }; static const struct GeosLauncher Geos128Launcher = @@ -53,5 +56,5 @@ static const struct GeosLauncher Geos128Launcher = { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, GEOS128_BOOT_FILE, GEOS128_BOOT_ENTRY, &TRDiskDriver128, Geos128ClockRuns, sizeof Geos128ClockRuns / sizeof Geos128ClockRuns[0], GEOS128_RESET_KEY, - "128 CONFIGURE" }, + Geos128ConfigureName }, }; From 5c8de5b7dd2323bfbedf351987e670721bc58d38 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 23:00:57 -0500 Subject: [PATCH 16/19] docs: GEOS 128 usage, and Special IO's GeoRAM and REU+USB Mouse/Disk --- README.md | 1 + docs/GEOS128_Usage.md | 83 +++++++++++++++++++++++++++++++++++++++++++ docs/GEOS64_Usage.md | 32 +++++++++++------ docs/General_Usage.md | 9 ++++- 4 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 docs/GEOS128_Usage.md diff --git a/README.md b/README.md index bac04eb2..e922cdb2 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Compatible with C64 and C128 machines/variants, NTSC and PAL supported. * **[NFC Loading System](docs/NFC_Loader.md)** * **[TeensyROM Web Browser](docs/Browser_Usage.md)** * **[GEOS 64](docs/GEOS64_Usage.md)** + * **[GEOS 128](docs/GEOS128_Usage.md)** * **[Custom BASIC Commands](docs/Custom_BASIC_Commands.md)** * **[TR Host Extensions (TR+ only)](docs/TR_Host_Extensions.md)** * **[USB/Ethernet Remote Command Protocol](docs/ControlComms.md)** diff --git a/docs/GEOS128_Usage.md b/docs/GEOS128_Usage.md new file mode 100644 index 00000000..74ebea92 --- /dev/null +++ b/docs/GEOS128_Usage.md @@ -0,0 +1,83 @@ +# GEOS 128 on the TeensyROM + +## What it is +* Starts GEOS 128 from your own boot disk image on the SD card, with a USB mouse, and no disk drive. + * GEOS itself is not included: you supply the disk image. +* Needs a C128. Runs on the TR and the TR+. +* Plug a USB mouse into the TeensyROM's USB host port. + * It is not a 1351 mouse: only GEOS, with the TR mouse driver the TeensyROM adds, sees it. +* Find it in the main menu: `F1` (Teensy Memory), then **Utilities**, then **GEOS 128 +C128**. + * The C128 restarts in 128 mode and GEOS 128 starts from your boot disk. + * GEOS 128 starts in 40 or 80 columns according to the 40/80 DISPLAY key, as it does from a disk drive, and can switch from its own menu. +* For GEOS 64, see [GEOS 64 on the TeensyROM](GEOS64_Usage.md). The two work the same way, each from its own folder. + +## SD card setup +* Make a folder named `/GEOS128` on the SD card. +* Copy your GEOS 128 boot disk image into it, under any file name ending in `.D81` (for example `GEOS128.D81`). + * The TeensyROM finds the boot disk by what is on it, not by its name: the image must hold `128 DESKTOP` and the boot file `GEOBOOT128`. + * D81 images only: the image with the deskTop and the applications on one disk. + * Don't end the name in `-TR` (`GEOS-TR.D81`): that name is kept for the copy described below, and is skipped. + * Versions that name their deskTop differently (Gateway, Wheels) are not found. +* If the folder holds more than one boot disk, the first one found is used. Only the first 8 `.D81` files in the folder are checked. + +## The -TR copy +* GEOS saves to the disk it starts from, so the TeensyROM never starts from your original image. +* The first time you select GEOS 128, it makes a copy next to the original with `-TR` added to the name (`GEOS128.D81` becomes `GEOS128-TR.D81`) and adds the TR USB mouse driver to the copy. Your original is not changed. +* Later launches reuse the `-TR` copy, so your saved work is still there. + * Each launch brings the copy's TR USB mouse driver up to date with the firmware's. +* To start over from your original, delete the `-TR` copy. The next launch makes a new one. + +## RAM expansion: GeoRAM or REU +GEOS 128 runs beside one of two RAM expansions, chosen by the **Special IO** setting in the Settings menu (`F8`). Either one also carries the TeensyROM's USB mouse and disk, so no other Special IO is needed. See [Selecting and associating Special IO](General_Usage.md#selecting-and-associating-special-io). + +| Special IO | RAM expansion | Boards | +|--|--|--| +| `REU+USB Mouse/Disk` | REU | TR+ only | +| anything else | GeoRAM | TR and TR+ | + +### REU (recommended on a TR+) +* Set Special IO to `REU+USB Mouse/Disk`, then select GEOS 128. +* GEOS's own `128 CONFIGURE` runs at start-up, finds the REU (128K to 512K, as much as fits) and sets it up as GEOS does a Commodore REU of that size. GEOS knows only the REU as a RAM expansion, so this is the way to give it one. +* At launch, the TeensyROM loads the REU pre-load/save image into the REU. Choose that file with `` in the file browser; the Settings menu shows the current one. If the file is not there, the REU starts empty. +* To save the REU to that file, press and release the Alt button (to the left of the menu button). + +### GeoRAM +* With any other Special IO setting, GEOS 128 runs with GeoRAM (128K to 512K, as much as fits). +* At launch, the TeensyROM loads `SD:/georam.ram` into GeoRAM. +* **(TR+ only)** To save GeoRAM, press and release the Alt button. + * The contents are written to `georam.ram` on the SD card, and the LED flashes for about 1.5 seconds once the save is done. + * The previous `georam.ram` is kept as `georam.bak`. + * If the power goes off during a save, the next launch loads the newest complete save: the one just made if it was fully written, otherwise the previous one. +* `128 CONFIGURE` runs at start-up as with the REU, but it does not find GeoRAM, so GEOS runs as it does with no RAM expansion. GeoRAM is there for programs that use it themselves. + +### Either way +* `128 CONFIGURE` is set up in the `-TR` copy to find the TeensyROM as drive A. +* Other configuration programs on the disk are kept from running at start-up, since they look for a disk drive. + +## Date and time +* GEOS starts with the date and time of the TeensyROM's real-time clock, adjusted by the Time Zone setting. + * Set the clock and time zone in the Settings menu (`F8`, page 6). See [General Usage](General_Usage.md#the-settings-menu). + * Without Ethernet, the clock can also be set over USB with the `SetRTCToken` command. See [Remote Command Protocol](ControlComms.md). + +## Messages +If GEOS 128 can't start, the TeensyROM stays at the menu and shows one of these: + +| Message | Meaning | +|--|--| +| GEOS 128 needs a C128 | The TeensyROM is in a C64. | +| GEOS 128 starts from your boot disk image on the SD card. Insert the card. | No SD card was found. | +| Copy your GEOS 128 boot disk image (.D81) to /GEOS128 on the SD card, then select this again. | There is no `/GEOS128` folder, or no D81 in it holds `128 DESKTOP`. | +| ``: not a D64, D71 or D81 without error bytes | The file's size is not that of a disk image (a D81 is 819,200 bytes). | +| ``: (other reason) | The image could not be read, or its directory is damaged. | +| Could not add the mouse driver to a copy of ``: the disk is full | No room for the driver. Free some space on your original image, or use another one. "the directory is full" is the same for directory entries. | +| Could not add the mouse driver to a copy of ``: the SD card could not be written | The copy could not be written to the SD card. | +| `-TR.D81` has no desktop; remove it to start over | The `-TR` copy has lost `128 DESKTOP`. Delete it. | +| `-TR.D81` was set aside and could not be put back | Power was lost while the copy was being updated, and the TeensyROM could not restore it. Look for `-TR.D81.BAK` in `/GEOS128`. | +| `-TR.D81`: its TR USB MOUSE is an old one; delete this copy | The copy's mouse driver is from a TeensyROM firmware whose driver cannot be updated in place. Delete the `-TR` copy; the next launch makes a new one. | +| `-TR.D81`: the boot file is not on the disk | The image has no `GEOBOOT128`. | +| `-TR.D81`: the boot file's disk driver is not one the TR can replace | This `GEOBOOT128` is not a version the TeensyROM can start. | +| `-TR.D81`: the boot file's reset key is not where this version keeps it | This `GEOBOOT128` is not a version the TeensyROM can start. | + +
+ +[Back to main ReadMe](/README.md) diff --git a/docs/GEOS64_Usage.md b/docs/GEOS64_Usage.md index 5fdb0895..249e1b1e 100644 --- a/docs/GEOS64_Usage.md +++ b/docs/GEOS64_Usage.md @@ -7,6 +7,7 @@ * Plug a USB mouse into the TeensyROM's USB host port. * It is not a 1351 mouse: only GEOS, with the TR mouse driver the TeensyROM adds, sees it. * Find it in the main menu: `F1` (Teensy Memory), then **Utilities**, then **GEOS 64**. +* On a C128, GEOS 128 starts the same way: see [GEOS 128 on the TeensyROM](GEOS128_Usage.md). ## SD card setup * Make a folder named `/GEOS64` on the SD card. @@ -24,21 +25,32 @@ * Each launch brings the copy's TR USB mouse driver up to date with the firmware's. * To start over from your original, delete the `-TR` copy. The next launch makes a new one. -## GeoRAM -* GEOS 64 runs with GeoRAM (128K to 512K, as much as fits) as its Special IO, selected automatically. +## RAM expansion: GeoRAM or REU +GEOS 64 runs beside one of two RAM expansions, chosen by the **Special IO** setting in the Settings menu (`F8`). Either one also carries the TeensyROM's USB mouse and disk, so no other Special IO is needed. See [Selecting and associating Special IO](General_Usage.md#selecting-and-associating-special-io). + +| Special IO | RAM expansion | Boards | +|--|--|--| +| `REU+USB Mouse/Disk` | REU | TR+ only | +| anything else | GeoRAM | TR and TR+ | + +### REU (recommended on a TR+) +* Set Special IO to `REU+USB Mouse/Disk`, then select GEOS 64. +* GEOS's own `CONFIGURE` runs at start-up, finds the REU (128K to 512K, as much as fits) and sets it up as GEOS does a Commodore REU of that size. GEOS knows only the REU as a RAM expansion, so this is the way to give it one. +* At launch, the TeensyROM loads the REU pre-load/save image into the REU. Choose that file with `` in the file browser; the Settings menu shows the current one. If the file is not there, the REU starts empty. +* To save the REU to that file, press and release the Alt button (to the left of the menu button). + +### GeoRAM +* With any other Special IO setting, GEOS 64 runs with GeoRAM (128K to 512K, as much as fits). * At launch, the TeensyROM loads `SD:/georam.ram` into GeoRAM. -* **(TR+ only)** To save GeoRAM, press and release the Alt button (to the left of the menu button). +* **(TR+ only)** To save GeoRAM, press and release the Alt button. * The contents are written to `georam.ram` on the SD card, and the LED flashes for about 1.5 seconds once the save is done. * The previous `georam.ram` is kept as `georam.bak`. * If the power goes off during a save, the next launch loads the newest complete save: the one just made if it was fully written, otherwise the previous one. -* GEOS's own `CONFIGURE` runs at start-up, set up in the `-TR` copy to find the TeensyROM as drive A. It does not find GeoRAM, so GEOS does not set GeoRAM up as a RAM disk. - * Other configuration programs on the disk (such as `CONFIG 2.0r`) are kept from running at start-up, since they look for a disk drive. +* `CONFIGURE` runs at start-up as with the REU, but it does not find GeoRAM, so GEOS runs as it does with no RAM expansion. GeoRAM is there for programs that use it themselves. -## REU (TR+ only) -* To run GEOS with the TR+'s REU (128K to 512K, as much as fits) instead of GeoRAM, set Special IO to `REU+USB Mouse/Disk` in the Settings menu (`F8`), then select GEOS 64 or GEOS 128. - * GEOS's own `CONFIGURE` finds the REU at start-up and sets it up as GEOS does a Commodore REU of that size. - * At launch, the TeensyROM loads the REU pre-load/save image shown in the Settings menu into the REU. To save the REU to that file, press and release the Alt button. -* With any other Special IO setting, GEOS runs with GeoRAM. +### Either way +* `CONFIGURE` is set up in the `-TR` copy to find the TeensyROM as drive A. +* Other configuration programs on the disk (such as `CONFIG 2.0r`) are kept from running at start-up, since they look for a disk drive. ## Date and time * GEOS starts with the date and time of the TeensyROM's real-time clock, adjusted by the Time Zone setting. diff --git a/docs/General_Usage.md b/docs/General_Usage.md index ce7fd584..e56be4f3 100644 --- a/docs/General_Usage.md +++ b/docs/General_Usage.md @@ -124,7 +124,7 @@ * See viewer usage/navigation doc via TR on your C64 * **.D64/.D71/.D81 files:** * Single File load/exec only (no multi-file/writebacks) - * Exception (FW 0.9.1 and later): GEOS 64 boots from a .D81 boot disk in `/GEOS64` with full disk read/write. See [GEOS 64 on the TeensyROM](GEOS64_Usage.md). + * Exception (FW 0.9.1 and later): GEOS 64 and GEOS 128 boot from a .D81 boot disk in `/GEOS64` and `/GEOS128` with full disk read/write. See [GEOS 64 on the TeensyROM](GEOS64_Usage.md) and [GEOS 128 on the TeensyROM](GEOS128_Usage.md). * **.HEX files:** * Used for TeensyROM firmware updates (see below) @@ -261,6 +261,13 @@ * The setting stays in memory and will be re-loaded for any app until changed * We recommend disabling NFC if Special IO is selected to avoid interference. * If a selected CRT file is associated with different Special IO (i.e. Epyx, EasyFlash etc), that Special IO will be loaded instead + * RAM expansions: + * **GeoRAM**: Berkeley Softworks geoRAM, 128K to 512K (as much as fits). Its window is at `$DE00` and its registers at `$DFFE`/`$DFFF`. The TeensyROM's USB mouse and [block device](TR_Block_Device.md) registers sit beside it at `$DF00`. + * Loads `SD:/georam.ram` at launch. **(TR+ only)** Press and release the Alt button to save it there; the previous save is kept as `georam.bak`. + * **512KB REU** **(TR+ only)**: a Commodore REU (1700/1764/1750) at `$DF00`, 128K to 512K (as much as fits). + * Loads the REU pre-load/save image at launch (choose it with `` in the file browser). Press and release the Alt button to save it there. + * **REU+USB Mouse/Disk** **(TR+ only)**: the same REU at `$DF00`, with the USB mouse and block device registers moved to `$DE00`. The REU's unused registers read `$FF`, as a 1764's do. + * Made for GEOS: with it selected, GEOS 64 and GEOS 128 run with the REU, which GEOS's own CONFIGURE finds. With any other setting they run with GeoRAM. See [GEOS 64](GEOS64_Usage.md#ram-expansion-georam-or-reu) and [GEOS 128](GEOS128_Usage.md#ram-expansion-georam-or-reu). ## Firmware updates TR and TR+ have separate firmware files, so first pick the one for your hardware: From 96d5141a134ee31baca7dc537ab968a72745b5c5 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Mon, 2 Nov 2026 17:14:29 -0600 Subject: [PATCH 17/19] tests: the REU+USB and block-device harness output parses on Windows --- Source/Teensy/tests/reu-trpage.test.js | 2 +- Source/Teensy/tests/trblock.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/tests/reu-trpage.test.js b/Source/Teensy/tests/reu-trpage.test.js index ebd2f409..cea32e9e 100644 --- a/Source/Teensy/tests/reu-trpage.test.js +++ b/Source/Teensy/tests/reu-trpage.test.js @@ -33,7 +33,7 @@ function harness() { function run(lines) { const ran = spawnSync(harness(), { input: lines.join('\n') + '\n', encoding: 'utf8' }); assert.equal(ran.status, 0, ran.stderr); - return ran.stdout.split('\n').filter(Boolean); + return ran.stdout.split(/\r?\n/).filter(Boolean); } const read = (offset) => `r ${offset.toString(16)}`; diff --git a/Source/Teensy/tests/trblock.test.js b/Source/Teensy/tests/trblock.test.js index 40992fca..e24559d8 100644 --- a/Source/Teensy/tests/trblock.test.js +++ b/Source/Teensy/tests/trblock.test.js @@ -86,7 +86,7 @@ function run(kind, script, mode, boot) { } const ran = spawnSync(harness(), [image, out, ...(mode ? [mode] : [])], { input: script, encoding: 'utf8' }); assert.equal(ran.status, 0, ran.stderr); - const lines = ran.stdout.split('\n').filter(Boolean); + const lines = ran.stdout.split(/\r?\n/).filter(Boolean); return { lines, image: fs.readFileSync(out), original: fs.readFileSync(image) }; } From 39c6244deaef664738b503a982be476dff833442 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Mon, 2 Nov 2026 17:14:45 -0600 Subject: [PATCH 18/19] firmware: GeoRAM stays the last selectable Special IO; REU+USB sits above it --- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 2 +- Source/Teensy/MinimalBoot/Common/Menu_Regs.h | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index b6c61a44..5e83b3e7 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -163,10 +163,10 @@ stcIOHandlers* IOHandler[] = //Synch order/qty with enum enumIOHandlers &IOHndlr_KernalReplace, //IOH_KernalReplace, #endif &IOHndlr_Debug, //IOH_Debug, - &IOHndlr_GeoRAM, //IOH_GeoRAM, #ifdef Fab04_REU &IOHndlr_REU_TRPage, //IOH_REU_TRPage, #endif + &IOHndlr_GeoRAM, //IOH_GeoRAM, //*not* manually selectable, see LastSelectableIOH diff --git a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h index 0e8fc4ae..fc021e6e 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -444,11 +444,9 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) #endif IOH_Debug, #ifdef Fab04_REU - IOH_GeoRAM, - IOH_REU_TRPage, //last manually selectable, see LastSelectableIOH - #else - IOH_GeoRAM, //last manually selectable, see LastSelectableIOH + IOH_REU_TRPage, #endif + IOH_GeoRAM, //last manually selectable, see LastSelectableIOH //*not* manually selectable, see LastSelectableIOH @@ -482,8 +480,4 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) IOH_Num_Handlers //always last }; -#ifdef Fab04_REU - #define LastSelectableIOH IOH_REU_TRPage //127 max. Used in IOH_TeensyROM.c for Special IO display/select -#else - #define LastSelectableIOH IOH_GeoRAM -#endif +#define LastSelectableIOH IOH_GeoRAM //127 max. Used in IOH_TeensyROM.c for Special IO display/select From f0ceb186b92240d9d6a0af68597f12cc8e82b189 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Mon, 2 Nov 2026 17:15:53 -0600 Subject: [PATCH 19/19] firmware: REU+USB reads the REU's registers through the stock REU path --- .../Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c | 13 +++---------- Source/Teensy/tests/reu-trpage.test.js | 4 ++++ Source/Teensy/tests/reu_trpage_harness.cpp | 5 +++++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index 6b7110f5..72ba6d3d 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -75,6 +75,7 @@ stcIOHandlers IOHndlr_REU = extern bool PerformDMA(DMA_Trans_RnW RnW, uint16_t StartAddr, uint8_t *Buffer, uint32_t Length, DMA_Addr_Mode FixC64Addr); extern bool CloseDMA(); extern void (*fSpecialBtnChange)(bool Up_nDn); //Pointer to function called when Special Button Changes +extern uint8_t CurrentIOHandler; //ref: https://codebase64.net/doku.php?id=base:reu_registers //tests: @@ -561,6 +562,7 @@ void IO2Hndlr_REU(uint8_t Address, bool R_Wn) if (R_Wn) //High (IO2 Read) { if (Address < REUReg_NumRegs) DataPortWriteWaitLog(REURegs[Address]); + else if (CurrentIOHandler == IOH_REU_TRPage) DataPortWriteWaitLog(0xff); // a 1764's unused registers; the TR's GEOS drivers look for the block device at $DF10 first if (Address == REUReg_Status) { @@ -752,7 +754,6 @@ FLASHMEM void PollingHndlr_REU() //______________________________________________________________________________________________ // The REU with the TR's USB mouse and block device beside it in IO1, where a GEOS that found the REU looks for them -void IO2Hndlr_REU_TRPage(uint8_t Address, bool R_Wn); void PollingHndlr_REU_TRPage(); void InitHndlr_REU_TRPage(); @@ -761,7 +762,7 @@ stcIOHandlers IOHndlr_REU_TRPage = "REU+USB Mouse/Disk", //Name of handler &InitHndlr_REU_TRPage, //Called once at handler startup &TRPageIO, //IO1 R/W handler - &IO2Hndlr_REU_TRPage, //IO2 R/W handler + &IO2Hndlr_REU, //IO2 R/W handler NULL, //ROML Read handler, in addition to any ROM data sent NULL, //ROMH Read handler, in addition to any ROM data sent &PollingHndlr_REU_TRPage, //Polled in main routine @@ -781,11 +782,3 @@ FLASHMEM void PollingHndlr_REU_TRPage() PollTRBlock(); PollingHndlr_REU(); } - -// A 1764's unused registers read $FF. The TR's GEOS drivers look for the block device's ID at $DF10 first, which -// must not answer there beside the REU. -void IO2Hndlr_REU_TRPage(uint8_t Address, bool R_Wn) -{ - if (R_Wn && NumREU_Banks && (Address & 0x1f) >= REUReg_NumRegs) DataPortWriteWaitLog(0xff); - else IO2Hndlr_REU(Address, R_Wn); -} diff --git a/Source/Teensy/tests/reu-trpage.test.js b/Source/Teensy/tests/reu-trpage.test.js index cea32e9e..33758e84 100644 --- a/Source/Teensy/tests/reu-trpage.test.js +++ b/Source/Teensy/tests/reu-trpage.test.js @@ -47,3 +47,7 @@ test('beside an REU, its registers answer and the rest of each 32 bytes reads $F test('with no RAM for the REU, nothing in IO2 answers, so software finds no REU there', { skip }, () => { assert.deepEqual(run(['init 0', 'banks 0', ...[0x00, 0x0a, BLOCK_ID].map(read)]), ['open', 'open', 'open']); }); + +test('under the stock REU, the unused registers stay open bus', { skip }, () => { + assert.deepEqual(run(['init 0', `banks ${BANKS}`, 'stock 0', ...[0x0a, BLOCK_ID, 0x1f].map(read)]), ['3f', 'open', 'open']); +}); diff --git a/Source/Teensy/tests/reu_trpage_harness.cpp b/Source/Teensy/tests/reu_trpage_harness.cpp index a0b37992..2f3b8810 100644 --- a/Source/Teensy/tests/reu_trpage_harness.cpp +++ b/Source/Teensy/tests/reu_trpage_harness.cpp @@ -2,6 +2,7 @@ // reu_trpage_harness: the REU's IO2 page as IOH_REU_TRPage serves it, driven from stdin one command a line: // init 0 the handler starts, with no RAM to give the REU // banks N the REU has N banks (0: none could be allocated) +// stock 0 the stock 512KB REU is the handler instead // r OFFSET an IO2 read, printing the byte driven or "open" #include #include @@ -58,6 +59,9 @@ static void InitTRBlock() {} static void PollUSBMouse() {} static void PollTRBlock() {} static void TRPageIO(uint8_t, bool) {} +#define Fab04_REU +#include "../MinimalBoot/Common/Menu_Regs.h" +uint8_t CurrentIOHandler = IOH_REU_TRPage; static int Driven; static void DataPortWriteWaitLog(uint8_t Data) { Driven = Data; } @@ -74,6 +78,7 @@ int main() { if (!strcmp(Command, "init")) IOHndlr_REU_TRPage.InitHndlr(); else if (!strcmp(Command, "banks")) NumREU_Banks = Value; + else if (!strcmp(Command, "stock")) CurrentIOHandler = IOH_REU; else if (!strcmp(Command, "r")) { Driven = -1;