From 2db2ca51fc79eac8a6c1ddc6535c32ef869e80c0 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 14:31:22 -0500 Subject: [PATCH 001/100] firmware: share the REU's RAM bank allocation, preload and save Moves the RAM1/RAM2 8K bank allocator, the image preload and the numbered button save out of IOH_REU.c into RAMBanks.c, so the GeoRAM handler can use the same code. The REU keeps its own bank array and count: SerUSBIO's busy check reads NumREU_Banks to mean the REU's DMA is live, which a GeoRAM must not trip. Behavior is unchanged except that the save writes whole banks instead of a byte at a time. --- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 1 + .../MinimalBoot/Common/IO_Handlers/IOH_REU.c | 182 ++--------------- .../MinimalBoot/Common/IO_Handlers/RAMBanks.c | 184 ++++++++++++++++++ 3 files changed, 196 insertions(+), 171 deletions(-) create mode 100644 Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index 88bc75b7..c3f060ef 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -109,6 +109,7 @@ struct stcIOHandlers #ifndef MinimumBuild #include "IO_Handlers/IOH_MIDI.c" #include "IO_Handlers/IOH_Debug.c" + #include "IO_Handlers/RAMBanks.c" #ifdef Fab04_REU #include "IO_Handlers/IOH_REU.c" #endif diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index fe43dcac..b602bf91 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -29,8 +29,7 @@ #ifdef USE_RAM12 - #define REU_RAM_Bank_Size 0x2000 - #define REU_Size (NumREU_Banks * REU_RAM_Bank_Size) //Dynamically allocated based on NumREU_Banks, each bank is 8k (0x2000) + #define REU_Size (NumREU_Banks * RAM_Bank_Size) //Dynamically allocated based on NumREU_Banks, each bank is 8k (0x2000) #define REU_RAM_READ(a,d) d=REU_Bank[a>>13][a&0x1fff] #define REU_RAM_WRITE(a,d) REU_Bank[a>>13][a&0x1fff]=d #define MIN_REU_BANKS 16 // 128K, smallest real REU (1700) @@ -76,11 +75,6 @@ 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 void EEPreadStr(uint16_t addr, char* buf); -extern RegMenuTypes RegMenuTypeFromFileName(char** ptrptrFileName); -extern bool SDFullInit(); -extern bool USBFileSystemWait(); -extern FS *FSfromSourceID(RegMenuTypes SourceID); //ref: https://codebase64.net/doku.php?id=base:reu_registers //tests: @@ -145,18 +139,12 @@ extern bool DMA_RnW, DMA_FixC64Addr; extern uint32_t DMA_Length, DMA_Count, DMA_StartAddr; extern bool DMAByte(uint8_t *Data); -extern void FreeDriveDirMenu(); -extern uint8_t RAM_Image[]; extern StructCrtChip CrtChips[]; // Removal breaks downstream code, so keep it here for now. -extern uint8_t NumCrtChips; extern StructMenuItem *DriveDirMenu; -extern uint8_t *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer void FreeREU() -{ //free banks allocated in RAM2 and reset NumREUBanks - for(uint16_t cnt=0; cnt < NumREU_Banks; cnt++) - if((uint32_t)REU_Bank[cnt] >= 0x20200000) free(REU_Bank[cnt]); - NumREU_Banks = 0; +{ + FreeRAMBanks(REU_Bank, &NumREU_Banks); } void DMAByte_BASkip(uint8_t *Data1) @@ -456,83 +444,12 @@ FLASHMEM void ReadWriteREU(bool RnW, uint32_t REUAddr, uint8_t *REUBuf, uint16_t FLASHMEM void Save_REU() { - if (NumREU_Banks == 0) return; // nothing to save: probably due to failure to allocate 128K - - //Create filename based on current. reu000.reu becomes reu001.reu, etc +#ifndef USE_RAM12 + #error Save_REU saves RAM1/2 banks only +#endif char Filename[MaxPathLength]; EEPreadStr(eepAdREUFilename, Filename); - char *ptrFileName = Filename; //pointer to move past SD/USB/TR: - RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(&ptrFileName); - - if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected - if (MenuSourceID == rmtUSBDrive) USBFileSystemWait(); //wait up to 1.5 sec in case USB drive just changed or powered up - //rmtTeensy not allowed, no reu files in Teensy Mem - - FS *sourceFS = FSfromSourceID(MenuSourceID); - - char Extension[20]; - char *pDot = strrchr(ptrFileName, '.'); //find last dot - if (pDot == NULL) pDot = ptrFileName + strlen(ptrFileName); //if no extension, set to end of filename - strcpy(Extension, pDot); //copy extension - *pDot = 0; //terminate at dot/extension - - bool IsNum = true; //are last 3digits of filename all numbers? - if (strlen(ptrFileName)>=3) - { - for(char *charloc=pDot-3; charloc'9') IsNum = false; - //Serial.printf("%08x: %c %02x\n", (uint32_t)charloc, *charloc, *charloc); - } - } - else IsNum = false; - - - uint16_t NewNum = 0; - if (IsNum) - { //find current number and then terminate it - NewNum = atoi(pDot-3); - *(pDot-3) = 0; //terminate number - } - - //now see if the file exists, or itterate until it doesn't - char NewFilename[MaxPathLength]; - - do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); - while (sourceFS->exists(NewFilename)); - - Serial.printf("Saving REU: %s\n", NewFilename); - File SaveFile = sourceFS->open(NewFilename, FILE_WRITE); - if (!SaveFile) - { - Serial.println("Unable to open!"); - return; - } - - //uint32_t StartmS = millis(); - uint8_t NextByte; - - for (uint32_t CharNum=0; CharNum= 0x20200000) - free(REU_Bank[unused_bank]); - NumREU_Banks = NewNumREU_Banks; - } - - Printf_dbg_reu("After resizing to power of 2, %luK bytes allocated\n", (uint32_t)NumREU_Banks * REU_RAM_Bank_Size / 1024); - - if (NumREU_Banks < MIN_REU_BANKS) // If REU is not 128k or larger + NumREU_Banks = AllocRAMBanks(REU_Bank, MAX_REU_BANKS, MIN_REU_BANKS); + if (NumREU_Banks == 0) { - Serial.printf("Unable to allocate minimum REU size of 128K\n" - "REU is disabled\n"); - FreeREU(); + Serial.printf("REU is disabled\n"); return; } @@ -623,37 +491,9 @@ uint8_t *pRAM_Image = ptrRAM_ImageEnd; //start at end of any CRT-used RAM1 fSpecialBtnChange = &SpecialBtn_REU; //REU RAM is ready; enable button now so a failed/missing preload below doesn't skip it Serial.printf("REU Size: %luK bytes\n", (uint32_t)REU_Size / 1024); - //pre-load REU into RAM here: char Filename[MaxPathLength]; EEPreadStr(eepAdREUFilename, Filename); - char *ptrFileName = Filename; //pointer to move past SD/USB/TR: - Serial.printf("Loading REU: %s\n", Filename); - RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(&ptrFileName); - - if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected - if (MenuSourceID == rmtUSBDrive) USBFileSystemWait(); //wait up to 1.5 sec in case USB drive just changed or powered up - //rmtTeensy not allowed, no reu files in Teensy Mem - - FS *sourceFS = FSfromSourceID(MenuSourceID); - File LoadFile = sourceFS->open(ptrFileName, FILE_READ); - if (!LoadFile) - { - Serial.println("Not found!"); - return; - } - - if (LoadFile.size() > REU_Size) - Serial.printf("Too Large! Loading first %luK\n", (uint32_t)REU_Size/1024); - - //uint32_t StartmS = millis(); - uint32_t CharNum = 0; - for (uint8_t Bank = 0; Bank < NumREU_Banks && LoadFile.available(); Bank++) - CharNum += LoadFile.read(REU_Bank[Bank], REU_RAM_Bank_Size); - //Serial.printf("Read %lu Bytes in %lumS\n", CharNum, millis()-StartmS); - Serial.printf("Read %lu Bytes into REU\n", CharNum); - LoadFile.close(); - //delay(250); - + LoadRAMBanks(REU_Bank, NumREU_Banks, Filename); #elif defined(USE_PSRAM) //higher clock speed and shorter CS setup/hold do help, but still not enough for NUVIEs w/ sound diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c new file mode 100644 index 00000000..7f2f01b5 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -0,0 +1,184 @@ +// MIT License +// +// Copyright (c) 2026 Travis Smith +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +// the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +// RAM expansion memory (REU, GeoRAM): 8k banks taken from the free end of RAM1's RAM_Image, +// then from RAM2, preloaded from and saved to an image file. + +#define RAM_Bank_Size 0x2000 + +extern void EEPreadStr(uint16_t addr, char* buf); +extern RegMenuTypes RegMenuTypeFromFileName(char** ptrptrFileName); +extern bool SDFullInit(); +extern bool USBFileSystemWait(); +extern FS *FSfromSourceID(RegMenuTypes SourceID); +extern void FreeDriveDirMenu(); +extern uint8_t RAM_Image[]; +extern uint8_t NumCrtChips; +extern uint8_t *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer + +FLASHMEM void FreeRAMBanks(uint8_t *Bank[], uint8_t *NumBanks) +{ //free banks allocated in RAM2 + for(uint16_t cnt=0; cnt < *NumBanks; cnt++) + if((uint32_t)Bank[cnt] >= 0x20200000) free(Bank[cnt]); + *NumBanks = 0; +} + +// Returns the number of banks allocated: a power of 2 up to MaxBanks, or 0 if fewer than MinBanks fit. +FLASHMEM uint8_t AllocRAMBanks(uint8_t *Bank[], uint8_t MaxBanks, uint8_t MinBanks) +{ + uint8_t NumBanks = 0; + FreeDriveDirMenu(); //free/clear prev loaded directory to make space. Doing it regardless to preserve continuity + + if (NumCrtChips == 0) ptrRAM_ImageEnd = RAM_Image; //If no CRT loaded, RAM1 is entirely free + uint8_t *pRAM_Image = ptrRAM_ImageEnd; //start at end of any CRT-used RAM1 + + //First use RAM_Image from RAM1: + while (((uint32_t)pRAM_Image - (uint32_t)RAM_Image + RAM_Bank_Size <= RAM_ImageSize) + && (NumBanks < MaxBanks)) + { + Bank[NumBanks] = pRAM_Image; + pRAM_Image += RAM_Bank_Size; + NumBanks++; + } + + //allocate the rest from RAM2 + while(NumBanks < MaxBanks && (NULL != (Bank[NumBanks] = (uint8_t*)malloc(RAM_Bank_Size)))) + NumBanks++; + + //resize to a power of 2 if needed and free unused banks + if (NumBanks & (NumBanks-1)) //not a power of 2 + { + // largest power less than or equal to NumBanks + uint8_t NewNumBanks = 1 << (7 - (__builtin_clz(NumBanks) - 24)); + Serial.printf("Resizing from %d banks to %d banks\n", NumBanks, NewNumBanks); + + for (uint8_t unused_bank = NewNumBanks; unused_bank < NumBanks; unused_bank++) + if((uint32_t)Bank[unused_bank] >= 0x20200000) + free(Bank[unused_bank]); + NumBanks = NewNumBanks; + } + + if (NumBanks < MinBanks) + { + Serial.printf("Unable to allocate minimum size of %dK\n", MinBanks * RAM_Bank_Size / 1024); + FreeRAMBanks(Bank, &NumBanks); + } + return NumBanks; +} + +// Path is source-prefixed ("SD:/reu.reu") +FLASHMEM static FS *RAMBanksFS(char *Path, char **ptrFileName) +{ + *ptrFileName = Path; //pointer to move past SD/USB/TR: + RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(ptrFileName); + + if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected + if (MenuSourceID == rmtUSBDrive) USBFileSystemWait(); //wait up to 1.5 sec in case USB drive just changed or powered up + //rmtTeensy not allowed, no RAM image files in Teensy Mem + + return FSfromSourceID(MenuSourceID); +} + +FLASHMEM void LoadRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) +{ + char Filename[MaxPathLength]; + strlcpy(Filename, SourcePath, MaxPathLength); + Serial.printf("Loading: %s\n", Filename); + char *ptrFileName; + FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); + File LoadFile = sourceFS->open(ptrFileName, FILE_READ); + if (!LoadFile) + { + Serial.println("Not found!"); + return; + } + + if (LoadFile.size() > (uint32_t)NumBanks * RAM_Bank_Size) + Serial.printf("Too Large! Loading first %luK\n", (uint32_t)NumBanks * RAM_Bank_Size / 1024); + + uint32_t CharNum = 0; + for (uint8_t BankNum = 0; BankNum < NumBanks && LoadFile.available(); BankNum++) + CharNum += LoadFile.read(Bank[BankNum], RAM_Bank_Size); + Serial.printf("Read %lu Bytes\n", CharNum); + LoadFile.close(); +} + +// Saves to the first free numbered name: reu.reu becomes reu000.reu, reu000.reu becomes reu001.reu, etc +FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) +{ + if (NumBanks == 0) return; // nothing to save: probably due to failure to allocate + + char Filename[MaxPathLength]; + strlcpy(Filename, SourcePath, MaxPathLength); + char *ptrFileName; + FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); + + char Extension[20]; + char *pDot = strrchr(ptrFileName, '.'); //find last dot + if (pDot == NULL) pDot = ptrFileName + strlen(ptrFileName); //if no extension, set to end of filename + strlcpy(Extension, pDot, sizeof(Extension)); //copy extension + *pDot = 0; //terminate at dot/extension + + bool IsNum = true; //are last 3digits of filename all numbers? + if (strlen(ptrFileName)>=3) + { + for(char *charloc=pDot-3; charloc'9') IsNum = false; + } + else IsNum = false; + + uint16_t NewNum = 0; + if (IsNum) + { //find current number and then terminate it + NewNum = atoi(pDot-3); + *(pDot-3) = 0; //terminate number + } + + //now see if the file exists, or itterate until it doesn't + char NewFilename[MaxPathLength]; + + do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); + while (sourceFS->exists(NewFilename)); + + Serial.printf("Saving: %s\n", NewFilename); + File SaveFile = sourceFS->open(NewFilename, FILE_WRITE); + if (!SaveFile) + { + Serial.println("Unable to open!"); + return; + } + + for (uint8_t BankNum = 0; BankNum < NumBanks; BankNum++) + SaveFile.write(Bank[BankNum], RAM_Bank_Size); + Serial.printf("Saved %dKBytes\n", NumBanks * RAM_Bank_Size / 1024); + SaveFile.close(); + + //Flash LED to ack save + uint32_t LastmS = millis(); + bool LEDState = true; + + for (uint32_t FlashCount=0; FlashCount<20; FlashCount++) + { + while(millis()-LastmS < 75); + LastmS = millis(); + if ((LEDState = !LEDState)) SetLEDOn; + else SetLEDOff; + } +} From 2759010b488e3c5798ffd0d71b88b7bb5a2139c6 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 14:47:01 -0500 Subject: [PATCH 002/100] firmware: GeoRAM IO handler A Berkeley Softworks geoRAM: the $DE00 page window onto RAM banks from RAMBanks.c, with the page ($DFFE) and 16K block ($DFFF) registers decoded across $DF80-$DFFF the way VICE does, leaving $DF00-$DF7F free. No DMA, so it is in every full build, TR and TR+, and serves C64 and C128 mode alike. Up to 512K, as much as fits beside a loaded crt; preloads SD:/georam.ram, and the special button saves the next free georamNNN.ram. Freed with the REU wherever the menu reclaims RAM. --- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 2 + .../Common/IO_Handlers/IOH_GeoRAM.c | 97 +++++++++++++++++++ Source/Teensy/MinimalBoot/Common/Menu_Regs.h | 1 + Source/Teensy/RemoteControl.ino | 3 +- Source/Teensy/Teensy.ino | 3 +- 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index c3f060ef..7aaa3a8d 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -110,6 +110,7 @@ struct stcIOHandlers #include "IO_Handlers/IOH_MIDI.c" #include "IO_Handlers/IOH_Debug.c" #include "IO_Handlers/RAMBanks.c" + #include "IO_Handlers/IOH_GeoRAM.c" #ifdef Fab04_REU #include "IO_Handlers/IOH_REU.c" #endif @@ -158,6 +159,7 @@ stcIOHandlers* IOHandler[] = //Synch order/qty with enum enumIOHandlers #ifdef Fab04_KernalReplace &IOHndlr_KernalReplace, //IOH_KernalReplace, #endif + &IOHndlr_GeoRAM, //IOH_GeoRAM, &IOHndlr_Debug, //IOH_Debug, //*not* manually selectable, see LastSelectableIOH diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c new file mode 100644 index 00000000..845f2006 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +// Berkeley Softworks geoRAM: a 256-byte window at $DE00 onto the page selected by two +// write-only registers, $DFFE (page 0-63 within a 16K block) and $DFFF (16K block). +// No DMA, so it runs on every TR, in C64 and C128 mode. +// ref: VICE src/c64/cart/georam.c + +void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn); +void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn); +void InitHndlr_GeoRAM(); +extern void (*fSpecialBtnChange)(bool Up_nDn); + +stcIOHandlers IOHndlr_GeoRAM = +{ + "GeoRAM", //Name of handler + &InitHndlr_GeoRAM, //Called once at handler startup + &IO1Hndlr_GeoRAM, //IO1 R/W handler + &IO2Hndlr_GeoRAM, //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 + NULL, //Polled in main routine + NULL, //called at the end of EVERY c64 cycle +}; + +#define GeoRAM_Filename "SD:/georam.ram" +#define MAX_GEORAM_BANKS 64 // 512K, the original geoRAM +#define MIN_GEORAM_BANKS 16 // 128K +#define GeoRAM_PagesPerBlock 64 +#define GeoRAM_BanksPerBlock (16384 / RAM_Bank_Size) + +// VICE decodes the registers across $DF80-$DFFF (A0 picks which); $DF00-$DF7F is left free. +#define GeoRAM_RegsBase 0x80 +#define GeoRAM_RegBlockBit 0x01 + +uint8_t NumGeoRAM_Banks = 0; +uint8_t *GeoRAM_Bank[MAX_GEORAM_BANKS]; +uint8_t GeoRAM_Block, GeoRAM_PageNum; +static uint8_t GeoRAM_NoRAM[256]; // the window while no RAM is allocated +uint8_t *GeoRAM_Page = GeoRAM_NoRAM; + +static void GeoRAM_SelectPage() +{ + if (NumGeoRAM_Banks == 0) return; + // the unused high register bits wrap, as on real hardware + uint32_t Offset = ((GeoRAM_Block & (NumGeoRAM_Banks / GeoRAM_BanksPerBlock - 1)) * GeoRAM_PagesPerBlock + + (GeoRAM_PageNum & (GeoRAM_PagesPerBlock - 1))) * 256; + GeoRAM_Page = GeoRAM_Bank[Offset / RAM_Bank_Size] + (Offset % RAM_Bank_Size); +} + +FLASHMEM void FreeGeoRAM() +{ + FreeRAMBanks(GeoRAM_Bank, &NumGeoRAM_Banks); + GeoRAM_Page = GeoRAM_NoRAM; +} + +FLASHMEM void SpecialBtn_GeoRAM(bool Up_nDn) +{ + // on button release save GeoRAM contents + if(Up_nDn) SaveRAMBanks(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); +} + +FLASHMEM void InitHndlr_GeoRAM() +{ + FreeGeoRAM(); + GeoRAM_Block = GeoRAM_PageNum = 0; + NumGeoRAM_Banks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); + if (NumGeoRAM_Banks == 0) + { + Serial.printf("GeoRAM is disabled\n"); + return; + } + GeoRAM_SelectPage(); + fSpecialBtnChange = &SpecialBtn_GeoRAM; + Serial.printf("GeoRAM Size: %dK bytes\n", NumGeoRAM_Banks * RAM_Bank_Size / 1024); + LoadRAMBanks(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); +} + +void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) +{ + if (R_Wn) DataPortWriteWaitLog(GeoRAM_Page[Address]); + else + { + uint8_t Data = DataPortWaitRead(); + GeoRAM_Page[Address] = Data; + TraceLogAddValidData(Data); + } +} + +void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn) +{ + if (R_Wn || Address < GeoRAM_RegsBase) return; // write-only registers + uint8_t Data = DataPortWaitRead(); + if (Address & GeoRAM_RegBlockBit) GeoRAM_Block = Data; + else GeoRAM_PageNum = Data; + GeoRAM_SelectPage(); + TraceLogAddValidData(Data); +} diff --git a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h index 166ecfe0..63aaa8ba 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -437,6 +437,7 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) #ifdef Fab04_KernalReplace IOH_KernalReplace, #endif + IOH_GeoRAM, IOH_Debug, //last manually selectable, see LastSelectableIOH //*not* manually selectable, see LastSelectableIOH diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index e3076195..0fc6d965 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -230,7 +230,8 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa FreeCrtChips(); #ifdef Fab04_REU FreeREU(); -#endif +#endif + FreeGeoRAM(); FreeSwiftlinkBuffs(); InitDriveDirMenu(); diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 99d05145..7cc1cf90 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -420,7 +420,8 @@ FLASHMEM void SetUpMainMenuROM() FreeCrtChips(); #ifdef Fab04_REU FreeREU(); -#endif +#endif + FreeGeoRAM(); FreeSwiftlinkBuffs(); RedirectEmptyDriveDirMenu(); free((void*)MIDIRxBuf); MIDIRxBuf=NULL; From 3bfb688cbd80545637f4875bc4a0a9b281636c4e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 14:49:06 -0500 Subject: [PATCH 003/100] firmware: RAM bank save reports a failed write instead of acking it A short write (card full, drive pulled) printed "Saved" and flashed the save LED. Load stops at a read error rather than adding -1 to its count. --- .../MinimalBoot/Common/IO_Handlers/RAMBanks.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c index 7f2f01b5..856581d0 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -115,7 +115,11 @@ FLASHMEM void LoadRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source uint32_t CharNum = 0; for (uint8_t BankNum = 0; BankNum < NumBanks && LoadFile.available(); BankNum++) - CharNum += LoadFile.read(Bank[BankNum], RAM_Bank_Size); + { + int BytesRead = LoadFile.read(Bank[BankNum], RAM_Bank_Size); + if (BytesRead <= 0) break; + CharNum += BytesRead; + } Serial.printf("Read %lu Bytes\n", CharNum); LoadFile.close(); } @@ -166,7 +170,14 @@ FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source } for (uint8_t BankNum = 0; BankNum < NumBanks; BankNum++) - SaveFile.write(Bank[BankNum], RAM_Bank_Size); + { + if (SaveFile.write(Bank[BankNum], RAM_Bank_Size) != RAM_Bank_Size) + { + Serial.printf("Write failed after %dKBytes!\n", BankNum * RAM_Bank_Size / 1024); + SaveFile.close(); + return; + } + } Serial.printf("Saved %dKBytes\n", NumBanks * RAM_Bank_Size / 1024); SaveFile.close(); From 6ecc7db082f8d1e836eea9c957a2454025c61efa Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 14:49:17 -0500 Subject: [PATCH 004/100] firmware: drop RAMBanks.c comments that restate the code --- .../MinimalBoot/Common/IO_Handlers/RAMBanks.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c index 856581d0..e15d37e2 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -34,7 +34,7 @@ extern uint8_t NumCrtChips; extern uint8_t *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer FLASHMEM void FreeRAMBanks(uint8_t *Bank[], uint8_t *NumBanks) -{ //free banks allocated in RAM2 +{ for(uint16_t cnt=0; cnt < *NumBanks; cnt++) if((uint32_t)Bank[cnt] >= 0x20200000) free(Bank[cnt]); *NumBanks = 0; @@ -44,12 +44,11 @@ FLASHMEM void FreeRAMBanks(uint8_t *Bank[], uint8_t *NumBanks) FLASHMEM uint8_t AllocRAMBanks(uint8_t *Bank[], uint8_t MaxBanks, uint8_t MinBanks) { uint8_t NumBanks = 0; - FreeDriveDirMenu(); //free/clear prev loaded directory to make space. Doing it regardless to preserve continuity + FreeDriveDirMenu(); if (NumCrtChips == 0) ptrRAM_ImageEnd = RAM_Image; //If no CRT loaded, RAM1 is entirely free uint8_t *pRAM_Image = ptrRAM_ImageEnd; //start at end of any CRT-used RAM1 - //First use RAM_Image from RAM1: while (((uint32_t)pRAM_Image - (uint32_t)RAM_Image + RAM_Bank_Size <= RAM_ImageSize) && (NumBanks < MaxBanks)) { @@ -58,11 +57,9 @@ FLASHMEM uint8_t AllocRAMBanks(uint8_t *Bank[], uint8_t MaxBanks, uint8_t MinBan NumBanks++; } - //allocate the rest from RAM2 while(NumBanks < MaxBanks && (NULL != (Bank[NumBanks] = (uint8_t*)malloc(RAM_Bank_Size)))) NumBanks++; - //resize to a power of 2 if needed and free unused banks if (NumBanks & (NumBanks-1)) //not a power of 2 { // largest power less than or equal to NumBanks @@ -86,7 +83,7 @@ FLASHMEM uint8_t AllocRAMBanks(uint8_t *Bank[], uint8_t MaxBanks, uint8_t MinBan // Path is source-prefixed ("SD:/reu.reu") FLASHMEM static FS *RAMBanksFS(char *Path, char **ptrFileName) { - *ptrFileName = Path; //pointer to move past SD/USB/TR: + *ptrFileName = Path; RegMenuTypes MenuSourceID = RegMenuTypeFromFileName(ptrFileName); if (MenuSourceID == rmtSD) SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected @@ -135,10 +132,10 @@ FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); char Extension[20]; - char *pDot = strrchr(ptrFileName, '.'); //find last dot + char *pDot = strrchr(ptrFileName, '.'); if (pDot == NULL) pDot = ptrFileName + strlen(ptrFileName); //if no extension, set to end of filename - strlcpy(Extension, pDot, sizeof(Extension)); //copy extension - *pDot = 0; //terminate at dot/extension + strlcpy(Extension, pDot, sizeof(Extension)); + *pDot = 0; bool IsNum = true; //are last 3digits of filename all numbers? if (strlen(ptrFileName)>=3) @@ -155,7 +152,6 @@ FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source *(pDot-3) = 0; //terminate number } - //now see if the file exists, or itterate until it doesn't char NewFilename[MaxPathLength]; do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); @@ -181,7 +177,6 @@ FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source Serial.printf("Saved %dKBytes\n", NumBanks * RAM_Bank_Size / 1024); SaveFile.close(); - //Flash LED to ack save uint32_t LastmS = millis(); bool LEDState = true; From 89c578cfcbceaf946b7e3dbd91dbda29c8e7b3d7 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 15:41:45 -0500 Subject: [PATCH 005/100] firmware: a disabled GeoRAM leaves $DE00 open instead of answering from one shared page --- Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index 845f2006..90d0ad35 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -77,6 +77,7 @@ FLASHMEM void InitHndlr_GeoRAM() void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) { + if (NumGeoRAM_Banks == 0) return; // disabled: $DE00 stays open bus, so software sees no geoRAM if (R_Wn) DataPortWriteWaitLog(GeoRAM_Page[Address]); else { From 8686274b1d3c0e6e72bc0f286279bcaa544c1130 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 15:41:55 -0500 Subject: [PATCH 006/100] firmware: FreeGeoRAM stops the IO handlers using the banks before freeing them --- Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index 90d0ad35..9f9fed67 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -35,7 +35,7 @@ stcIOHandlers IOHndlr_GeoRAM = uint8_t NumGeoRAM_Banks = 0; uint8_t *GeoRAM_Bank[MAX_GEORAM_BANKS]; uint8_t GeoRAM_Block, GeoRAM_PageNum; -static uint8_t GeoRAM_NoRAM[256]; // the window while no RAM is allocated +static uint8_t GeoRAM_NoRAM[256]; // the window until a page of allocated RAM is selected uint8_t *GeoRAM_Page = GeoRAM_NoRAM; static void GeoRAM_SelectPage() @@ -49,8 +49,12 @@ static void GeoRAM_SelectPage() FLASHMEM void FreeGeoRAM() { - FreeRAMBanks(GeoRAM_Bank, &NumGeoRAM_Banks); + // the handler is still installed and the C64 may be running: the IO ISRs must stop + // touching the banks before the first one is freed + uint8_t NumBanks = NumGeoRAM_Banks; + NumGeoRAM_Banks = 0; GeoRAM_Page = GeoRAM_NoRAM; + FreeRAMBanks(GeoRAM_Bank, &NumBanks); } FLASHMEM void SpecialBtn_GeoRAM(bool Up_nDn) From 1c8cf3217c31093398f217b62eaaa9be270ac00f Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 15:42:06 -0500 Subject: [PATCH 007/100] firmware: GeoRAM state is file-local --- Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index 9f9fed67..787b86b3 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -32,11 +32,11 @@ stcIOHandlers IOHndlr_GeoRAM = #define GeoRAM_RegsBase 0x80 #define GeoRAM_RegBlockBit 0x01 -uint8_t NumGeoRAM_Banks = 0; -uint8_t *GeoRAM_Bank[MAX_GEORAM_BANKS]; -uint8_t GeoRAM_Block, GeoRAM_PageNum; +static uint8_t NumGeoRAM_Banks = 0; +static uint8_t *GeoRAM_Bank[MAX_GEORAM_BANKS]; +static uint8_t GeoRAM_Block, GeoRAM_PageNum; static uint8_t GeoRAM_NoRAM[256]; // the window until a page of allocated RAM is selected -uint8_t *GeoRAM_Page = GeoRAM_NoRAM; +static uint8_t *GeoRAM_Page = GeoRAM_NoRAM; static void GeoRAM_SelectPage() { From b27c082c2ce4c24525f049459359bac6bb03c447 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 15:42:06 -0500 Subject: [PATCH 008/100] firmware: drop trailing spaces after the FreeGeoRAM calls --- Source/Teensy/RemoteControl.ino | 2 +- Source/Teensy/Teensy.ino | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index 0fc6d965..cc3dfd46 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -231,7 +231,7 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa #ifdef Fab04_REU FreeREU(); #endif - FreeGeoRAM(); + FreeGeoRAM(); FreeSwiftlinkBuffs(); InitDriveDirMenu(); diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 7cc1cf90..0dd28899 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -421,7 +421,7 @@ FLASHMEM void SetUpMainMenuROM() #ifdef Fab04_REU FreeREU(); #endif - FreeGeoRAM(); + FreeGeoRAM(); FreeSwiftlinkBuffs(); RedirectEmptyDriveDirMenu(); free((void*)MIDIRxBuf); MIDIRxBuf=NULL; From 9b91191b2a9f15be1661734253897128d6c84eed Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 16:23:12 -0500 Subject: [PATCH 009/100] firmware: GeoRAM is the last selectable handler, after Debug Placed before Debug it moved Debug's number, and that number is what eepAdNextIOHndlr stores, so a board saved with Debug as its default came up on GeoRAM. After Debug, with LastSelectableIOH moved to it, every selectable handler keeps the number it had. --- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 2 +- Source/Teensy/MinimalBoot/Common/Menu_Regs.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index 7aaa3a8d..722267c2 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -159,8 +159,8 @@ stcIOHandlers* IOHandler[] = //Synch order/qty with enum enumIOHandlers #ifdef Fab04_KernalReplace &IOHndlr_KernalReplace, //IOH_KernalReplace, #endif - &IOHndlr_GeoRAM, //IOH_GeoRAM, &IOHndlr_Debug, //IOH_Debug, + &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 63aaa8ba..a3c616da 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -437,8 +437,8 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) #ifdef Fab04_KernalReplace IOH_KernalReplace, #endif - IOH_GeoRAM, - IOH_Debug, //last manually selectable, see LastSelectableIOH + IOH_Debug, + IOH_GeoRAM, //last manually selectable, see LastSelectableIOH //*not* manually selectable, see LastSelectableIOH @@ -472,4 +472,4 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) IOH_Num_Handlers //always last }; -#define LastSelectableIOH IOH_Debug //127 max. Used in IOH_TeensyROM.c for Special IO display/select +#define LastSelectableIOH IOH_GeoRAM //127 max. Used in IOH_TeensyROM.c for Special IO display/select From 1330587868f25f9206ebac5e6677583b2e399b46 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 12:15:29 -0500 Subject: [PATCH 010/100] firmware: a USB mouse read through registers beside GeoRAM and the REU The TR's USB host port takes a mouse; the main loop adds up its movement and the IO handlers hand it to the C64 as signed steps, at $DF00 beside GeoRAM and $DE00 beside the REU, in the halves of the IO area each one leaves free. A 1351 can't be emulated: the SID's POT lines are not on the cartridge port, so this is for software with a TR driver. Each read of X or Y returns the movement since the last read of it, at most 127 steps, and keeps the rest for the next one, so a fast mouse is not lost between polls. Only the main loop writes the USB totals and only the IO ISR writes what it has handed out, so neither needs a lock. --- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 2 + .../Common/IO_Handlers/IOH_GeoRAM.c | 10 +++- .../MinimalBoot/Common/IO_Handlers/IOH_REU.c | 9 ++- .../MinimalBoot/Common/IO_Handlers/USBMouse.c | 56 +++++++++++++++++++ Source/Teensy/Teensy.ino | 1 + 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index 722267c2..0d6da33c 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -36,6 +36,7 @@ USBHIDParser hid1(myusbHost); USBHIDParser hid2(myusbHost); USBHIDParser hid3(myusbHost); //need all 3? + MouseController usbHostMouse(myusbHost); USBSerial USBHostSerial(myusbHost); //update HostSerialType to match, defined in PN532_UHSU.h //USBSerial USBHostSerial(myusbHost); // only for those Serial devices who transfer <=64 bytes (like T3.x, FTDI...) @@ -110,6 +111,7 @@ struct stcIOHandlers #include "IO_Handlers/IOH_MIDI.c" #include "IO_Handlers/IOH_Debug.c" #include "IO_Handlers/RAMBanks.c" + #include "IO_Handlers/USBMouse.c" #include "IO_Handlers/IOH_GeoRAM.c" #ifdef Fab04_REU #include "IO_Handlers/IOH_REU.c" diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index 787b86b3..afaeeb31 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -28,7 +28,7 @@ stcIOHandlers IOHndlr_GeoRAM = #define GeoRAM_PagesPerBlock 64 #define GeoRAM_BanksPerBlock (16384 / RAM_Bank_Size) -// VICE decodes the registers across $DF80-$DFFF (A0 picks which); $DF00-$DF7F is left free. +// VICE decodes the registers across $DF80-$DFFF (A0 picks which); $DF00-$DF7F is the USB mouse's. #define GeoRAM_RegsBase 0x80 #define GeoRAM_RegBlockBit 0x01 @@ -67,6 +67,7 @@ FLASHMEM void InitHndlr_GeoRAM() { FreeGeoRAM(); GeoRAM_Block = GeoRAM_PageNum = 0; + InitUSBMouse(); NumGeoRAM_Banks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); if (NumGeoRAM_Banks == 0) { @@ -93,7 +94,12 @@ void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn) { - if (R_Wn || Address < GeoRAM_RegsBase) return; // write-only registers + if (Address < GeoRAM_RegsBase) + { + if (R_Wn) USBMouseIORead(Address); + return; + } + if (R_Wn) return; // write-only registers uint8_t Data = DataPortWaitRead(); if (Address & GeoRAM_RegBlockBit) GeoRAM_Block = Data; else GeoRAM_PageNum = Data; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index b602bf91..9a76752c 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -51,6 +51,7 @@ __attribute__((always_inline)) inline void Printf_dbg_reu(...) {}; #endif +void IO1Hndlr_REU(uint8_t Address, bool R_Wn); void IO2Hndlr_REU(uint8_t Address, bool R_Wn); void PollingHndlr_REU(); void InitHndlr_REU(); @@ -59,7 +60,7 @@ stcIOHandlers IOHndlr_REU = { "512KB REU", //Name of handler &InitHndlr_REU, //Called once at handler startup - NULL, //IO1 R/W handler + &IO1Hndlr_REU, //IO1 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 @@ -469,6 +470,7 @@ FLASHMEM void InitHndlr_REU() //set reg defaults: uint8_t REURegsInit[REUReg_NumRegs]={0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x1f, 0x3f}; memcpy(REURegs, REURegsInit, REUReg_NumRegs); + InitUSBMouse(); #ifdef Direct_REU Printf_dbg_reu("Direct REU mode\n"); @@ -550,6 +552,11 @@ FLASHMEM void InitHndlr_REU() } //InitHndlr_REU +void IO1Hndlr_REU(uint8_t Address, bool R_Wn) +{ + if (R_Wn) USBMouseIORead(Address); +} + void IO2Hndlr_REU(uint8_t Address, bool R_Wn) { #ifdef DbgIOTraceLog diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c new file mode 100644 index 00000000..c8cd84be --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +// A USB mouse on the TR's USB host port, read through registers that a RAM expansion handler +// serves from the IO page it leaves free: $DF00 beside GeoRAM, $DE00 beside the REU. +// 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). +// +// +0 X movement since the last read of +0, signed, right is positive +// +1 Y movement since the last read of +1, signed, down is positive +// +2 buttons, 1 = pressed: bit 0 left, bit 1 right, bit 2 middle +// +3..5 "TRM", for a driver to find the base it is at + +#define USBMouse_RegButtons 2 +#define USBMouse_RegID 3 +#define USBMouse_StepShift 2 // USB mice count ~1000 per inch; GEOS's screen is 320 wide +#define USBMouse_MaxStep 127 + +static const char USBMouse_ID[] = "TRM"; +static volatile int32_t USBMouse_Counts[2]; // USB counts, written only by the main loop +static int32_t USBMouse_StepsRead[2]; // written only by the IO ISR, after InitUSBMouse +static volatile uint8_t USBMouse_Buttons; + +static int32_t USBMouse_Steps(uint8_t Axis) +{ + return USBMouse_Counts[Axis] >> USBMouse_StepShift; // floors, so no wider dead zone at 0 +} + +void PollUSBMouse() +{ + if (!usbHostMouse.available()) return; + USBMouse_Counts[0] += usbHostMouse.getMouseX(); + USBMouse_Counts[1] += usbHostMouse.getMouseY(); + USBMouse_Buttons = usbHostMouse.getButtons(); + usbHostMouse.mouseDataClear(); +} + +// Movement from before the handler started (at the TR menu) is not the C64's to see +FLASHMEM void InitUSBMouse() +{ + for (uint8_t Axis = 0; Axis < 2; Axis++) USBMouse_StepsRead[Axis] = USBMouse_Steps(Axis); +} + +void USBMouseIORead(uint8_t Address) +{ + if (Address < 2) + { + int32_t Step = USBMouse_Steps(Address) - USBMouse_StepsRead[Address]; + if (Step > USBMouse_MaxStep) Step = USBMouse_MaxStep; + else if (Step < -USBMouse_MaxStep) Step = -USBMouse_MaxStep; + USBMouse_StepsRead[Address] += Step; + DataPortWriteWaitLog((uint8_t)Step); + } + else if (Address == USBMouse_RegButtons) DataPortWriteWaitLog(USBMouse_Buttons); + else if (Address < USBMouse_RegID + sizeof(USBMouse_ID) - 1) + DataPortWriteWaitLog(USBMouse_ID[Address - USBMouse_RegID]); +} diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 0dd28899..ffe7b2df 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -370,6 +370,7 @@ void loop() if (Serial.available()) ServiceSerial(&Serial); myusbHost.Task(); + PollUSBMouse(); if (nfcState == nfcStateEnabled) nfcCheck(); From d631ef34fc64c944cf7de10f2a263d183f99c926 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 12:15:30 -0500 Subject: [PATCH 011/100] tools: a GEOS 128 input driver for the TR's USB mouse trmouse.py assembles trmouse128.asm and writes it to a GEOS 128 boot D81 as TR USB MOUSE, in the directory entry ahead of the other input drivers, because GEOS loads the first one it finds. The driver finds the TR's registers by their ID at $DF03, else uses $DE00, doubles X in 80 columns as the stock 1351 driver does, and leaves clamping to the kernal, which flags a push past an edge for apps that scroll on it. --- tools/geos/trmouse.py | 191 ++++++++++++++++++++++++++++++++++++++ tools/geos/trmouse128.asm | 149 +++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 tools/geos/trmouse.py create mode 100644 tools/geos/trmouse128.asm diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py new file mode 100644 index 00000000..29fa8981 --- /dev/null +++ b/tools/geos/trmouse.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Put the TR USB mouse driver on a GEOS 128 boot disk. + + python3 tools/geos/trmouse.py boot.d81 out.d81 + +GEOS loads the first input driver in the boot disk's directory, so the driver +goes into the entry ahead of every other one, and the input driver that was +first moves to the slot the new file would have taken. Needs acme on PATH. +""" + +import argparse +import datetime +import pathlib +import subprocess +import sys +import tempfile + +SOURCE = pathlib.Path(__file__).with_name('trmouse128.asm') +NAME = b'TR USB MOUSE' +LOAD = 0xfd00 +MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F + +D81_SIZE = 819200 +SECTORS = 40 +DIR_TRACK = 40 +BAM_SECTORS = (1, 2) # tracks 1-40, 41-80 +FIRST_DIR_SECTOR = 3 +BAM_ENTRIES = 0x10 +BAM_ENTRY_SIZE = 6 + +USR_CLOSED = 0x83 +GEOS_SEQUENTIAL = 0 +INPUT_128 = 15 +INFO_ICON_BITMAP = 0xbf # an uncompressed 63-byte icon follows + +ICON = """ +........................ +.........######......... +.......##..#...##....... +......#....#.....#...... +.....#.....#......#..... +.....#.....#......#..... +.....#.....#......#..... +.....##############..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +......#..........#...... +.......##......##....... +.........######......... +........................ +...####.####.####....... +.....#..#..#.#..#....... +.....#..###..####....... +""" + + +def assemble(): + with tempfile.TemporaryDirectory() as tmp: + out = pathlib.Path(tmp) / 'trmouse128.bin' + subprocess.run(['acme', '-f', 'plain', '-o', str(out), str(SOURCE)], check=True) + code = out.read_bytes() + if len(code) > MAX_SIZE: + sys.exit(f'the driver is {len(code)} bytes; GEOS 128 has room for {MAX_SIZE}') + return code + + +def icon_bits(): + rows = ICON.split() + assert len(rows) == 21 and all(len(r) == 24 for r in rows) + out = bytearray() + for row in rows: + bits = int(row.replace('#', '1').replace('.', '0'), 2) + out += bits.to_bytes(3, 'big') + return bytes(out) + + +def padded(text, size, pad=b'\0'): + assert len(text) <= size + return text + pad * (size - len(text)) + + +class D81: + def __init__(self, image): + if len(image) != D81_SIZE: + sys.exit(f'not a D81 without error bytes ({len(image)} bytes, expected {D81_SIZE})') + self.image = bytearray(image) + + def offset(self, track, sector): + return ((track - 1) * SECTORS + sector) * 256 + + def bam_entry(self, track): + sector = BAM_SECTORS[(track - 1) // SECTORS] + return self.offset(DIR_TRACK, sector) + BAM_ENTRIES + ((track - 1) % SECTORS) * BAM_ENTRY_SIZE + + def allocate(self): + for track in range(1, 81): + if track == DIR_TRACK: + continue + entry = self.bam_entry(track) + for sector in range(SECTORS): + byte, bit = entry + 1 + sector // 8, 1 << (sector % 8) + if self.image[byte] & bit: + self.image[byte] &= ~bit + self.image[entry] -= 1 + return track, sector + sys.exit('the disk is full') + + def dir_slots(self): + track, sector = DIR_TRACK, FIRST_DIR_SECTOR + while track: + base = self.offset(track, sector) + for i in range(8): + yield base + i * 32 + track, sector = self.image[base], self.image[base + 1] + + def write_file(self, data, info): + """Write a GEOS sequential file; returns its first data sector.""" + sectors = [self.allocate() for _ in range((len(data) + 253) // 254)] + for i, (track, sector) in enumerate(sectors): + chunk = data[i * 254:(i + 1) * 254] + if i + 1 < len(sectors): + link = bytes(sectors[i + 1]) + else: + link = bytes((0, len(chunk) + 1)) + o = self.offset(track, sector) + self.image[o:o + 256] = padded(link + chunk, 256) + info_ts = self.allocate() + o = self.offset(*info_ts) + self.image[o:o + 256] = info + return sectors[0], info_ts, len(sectors) + 1 + + +def info_block(size): + info = bytearray(256) + info[0:2] = b'\x00\xff' + info[2:5] = bytes((3, 21, INFO_ICON_BITMAP)) + info[5:0x44] = icon_bits() + info[0x44:0x47] = bytes((USR_CLOSED, INPUT_128, GEOS_SEQUENTIAL)) + info[0x47:0x49] = LOAD.to_bytes(2, 'little') + info[0x49:0x4b] = (LOAD + size).to_bytes(2, 'little') + info[0x4d:0x61] = padded(b'TR USB Mouse V1.0', 20) + info[0x61:0x75] = padded(b'TeensyROM', 20) + info[0xa0:0x100] = padded(b'A USB mouse plugged into a TeensyROM.', 0x60) + return bytes(info) + + +def install(image, code): + disk = D81(image) + slots = list(disk.dir_slots()) + entries = [s for s in slots if disk.image[s + 2] & 7] + if any(disk.image[s + 5:s + 21].rstrip(b'\xa0') == NAME for s in entries): + sys.exit(f'{NAME.decode()} is already on this disk') + free = next((s for s in slots if disk.image[s + 2] == 0), None) + if free is None: + sys.exit('the directory is full') + + first, info_ts, blocks = disk.write_file(code, info_block(len(code))) + now = datetime.datetime.now() + entry = bytearray(30) + entry[0] = USR_CLOSED + entry[1:3] = bytes(first) + entry[3:19] = padded(NAME, 16, b'\xa0') + entry[19:21] = bytes(info_ts) + entry[21:23] = bytes((GEOS_SEQUENTIAL, INPUT_128)) + entry[23:28] = bytes((now.year % 100, now.month, now.day, now.hour, now.minute)) + entry[28:30] = blocks.to_bytes(2, 'little') + disk.image[free + 2:free + 32] = entry + + drivers = [s for s in entries if disk.image[s + 24] == INPUT_128] + if drivers and slots.index(drivers[0]) < slots.index(free): + a, b = drivers[0], free + disk.image[a + 2:a + 32], disk.image[b + 2:b + 32] = \ + disk.image[b + 2:b + 32], disk.image[a + 2:a + 32] + return bytes(disk.image) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('disk', type=pathlib.Path) + parser.add_argument('out', type=pathlib.Path) + args = parser.parse_args() + args.out.write_bytes(install(args.disk.read_bytes(), assemble())) + + +if __name__ == '__main__': + main() diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm new file mode 100644 index 00000000..d816bb39 --- /dev/null +++ b/tools/geos/trmouse128.asm @@ -0,0 +1,149 @@ +; SPDX-License-Identifier: MIT +; +; GEOS 128 input driver for a USB mouse on a TeensyROM (USBMouse.c has the registers). + +!cpu 6502 + +mouseOn = $30 ; bit 7: the pointer is on +pressFlag = $39 +mouseXPos = $3a ; 16 bits +mouseYPos = $3c +graphMode = $3f ; bit 7: 80 columns +mouseData = $8505 ; bit 7: button up +inputData = $8506 ; a joystick direction, or $ff + +MOUSE_BIT = $20 ; in pressFlag: mouseData changed +BUTTON_UP = $80 +NO_DIRECTION = $ff +OFF_TOP = 228 ; the kernal reads a mouseYPos this high as above the top + +regX = 0 +regY = 1 +regButtons = 2 +regID = 3 +LEFT_BUTTON = $01 + + * = $fd00 ; where GEOS 128 loads an input driver + + jmp MouseInit + jmp SlowMouse + jmp UpdateMouse + jmp SetMouse + +lastButton !byte BUTTON_UP +deltaX !word 0 +deltaY !byte 0 + +MouseInit + lda #0 + sta mouseXPos+1 + lda #8 + sta mouseXPos + sta mouseYPos +SlowMouse +SetMouse + rts + +; The registers are at $DF00 beside GeoRAM and $DE00 beside the REU; the ID tells which. +; $DF03-$DF05 are REU address registers, which have no side effects when read. +FindRegs + ldx #2 +- lda $df00+regID,x + cmp trID,x + bne + + dex + bpl - + lda #$df + bne ++ ++ lda #$de +++ sta readX+2 + sta readY+2 + sta readButtons+2 + rts + +trID !text "TRM" + +UpdateMouse + bit mouseOn + bmi + + rts ++ jsr FindRegs + +readButtons + lda $df00+regButtons + and #LEFT_BUTTON + beq + + lda #0 ; pressed + beq ++ ++ lda #BUTTON_UP +++ cmp lastButton + beq ++ + sta lastButton + sta mouseData + lda pressFlag + ora #MOUSE_BIT + sta pressFlag +++ + +readX + lda $df00+regX + sta deltaX + ldx #0 + cmp #0 + bpl + + dex ++ stx deltaX+1 + bit graphMode + bpl + + asl deltaX ; 640 pixels across in 80 columns + rol deltaX+1 ++ clc + lda mouseXPos + adc deltaX + sta mouseXPos + lda mouseXPos+1 + adc deltaX+1 + sta mouseXPos+1 ; the kernal clamps it to the screen + +readY + lda $df00+regY + sta deltaY + clc + adc mouseYPos + bit deltaY + bmi up + bcs bottom + cmp #OFF_TOP + bcc storeY +bottom lda #OFF_TOP-1 ; below the bottom, for the kernal to clamp + bne storeY +up bcs storeY + lda #$ff ; above the top +storeY sta mouseYPos + +; inputData: 0 right, 1 up-right, 2 up ... 7 down-right, as a joystick driver gives + ldx #1 ; column: 0 left, 1 none, 2 right + lda deltaX+1 + bmi + + ora deltaX + beq ++ + inx + bne ++ ++ dex +++ lda deltaY ; row: 0 up, 1 none, 2 down + beq + + bmi up1 + inx + inx + inx ++ inx + inx + inx +up1 lda directions,x + sta inputData + rts + +directions + !byte 3, 2, 1 ; up + !byte 4, NO_DIRECTION, 0 + !byte 5, 6, 7 ; down From 78cbc0523759ae34d17f101e5cfa58d0891f25c0 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 12:21:34 -0500 Subject: [PATCH 012/100] firmware: the USB mouse poll keeps reports the USB ISR lands mid-read, and drops buttons on unplug --- .../MinimalBoot/Common/IO_Handlers/USBMouse.c | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index c8cd84be..41ce13c9 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -27,11 +27,25 @@ static int32_t USBMouse_Steps(uint8_t Axis) void PollUSBMouse() { + // MouseController is also a BTHIDInput, so its own bool conversion is ambiguous + if (!static_cast(usbHostMouse)) + { + USBMouse_Buttons = 0; // unplugging leaves the library's last report in place + return; + } if (!usbHostMouse.available()) return; - USBMouse_Counts[0] += usbHostMouse.getMouseX(); - USBMouse_Counts[1] += usbHostMouse.getMouseY(); - USBMouse_Buttons = usbHostMouse.getButtons(); + // the USB ISR writes each report straight into the controller, and mouseDataClear() + // would drop one that lands between these reads and the clear. IRQ_USB2 is the host + // port's, which USBHost_t36 calls IRQ_USBHS in a header of its own + NVIC_DISABLE_IRQ(IRQ_USB2); + int32_t X = usbHostMouse.getMouseX(); + int32_t Y = usbHostMouse.getMouseY(); + uint8_t Buttons = usbHostMouse.getButtons(); usbHostMouse.mouseDataClear(); + NVIC_ENABLE_IRQ(IRQ_USB2); + USBMouse_Counts[0] += X; + USBMouse_Counts[1] += Y; + USBMouse_Buttons = Buttons; } // Movement from before the handler started (at the TR menu) is not the C64's to see From d3990d304f61384bc51028261d2d5bb71409714c Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 12:28:34 -0500 Subject: [PATCH 013/100] tools: the GEOS 128 mouse driver ignores a page without the TR's ID, and drains movement while the pointer is off Without a TR handler serving either page, the driver read open bus at $DE00 as motion and a held button, so the pointer jumped and clicked on its own. Movement made while mouseOn was clear stayed in the TR's counters and swept the pointer across the screen once it came back on. --- tools/geos/trmouse128.asm | 43 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index d816bb39..bc2a226f 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -46,28 +46,40 @@ SetMouse ; The registers are at $DF00 beside GeoRAM and $DE00 beside the REU; the ID tells which. ; $DF03-$DF05 are REU address registers, which have no side effects when read. +; Carry set: neither page has the ID, so what reads there is not a mouse. FindRegs + ldy #$df +nextPage + sty readID+2 ldx #2 -- lda $df00+regID,x +readID lda $df00+regID,x cmp trID,x bne + dex - bpl - - lda #$df - bne ++ -+ lda #$de -++ sta readX+2 - sta readY+2 - sta readButtons+2 + bpl readID + sty readX+2 + sty readY+2 + sty readButtons+2 + clc + rts ++ dey + cpy #$dd + bne nextPage + sec rts trID !text "TRM" UpdateMouse + jsr FindRegs + bcs noMouse +readX lda $df00+regX + sta deltaX +readY lda $df00+regY + sta deltaY bit mouseOn - bmi + - rts -+ jsr FindRegs + bmi readButtons +noMouse rts ; movement while the pointer is off is dropped, as a 1351's is readButtons lda $df00+regButtons @@ -85,11 +97,8 @@ readButtons sta pressFlag ++ -readX - lda $df00+regX - sta deltaX ldx #0 - cmp #0 + lda deltaX bpl + dex + stx deltaX+1 @@ -105,9 +114,7 @@ readX adc deltaX+1 sta mouseXPos+1 ; the kernal clamps it to the screen -readY - lda $df00+regY - sta deltaY + lda deltaY clc adc mouseYPos bit deltaY From 50a74e61a1d11d4361052d3999f55ae0db6d1ecf Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 12:28:34 -0500 Subject: [PATCH 014/100] tools: trmouse.py puts the class version at +12, and stops on a looped directory chain GEOS reads the permanent name's version from offset 12, where the class string had a space. A directory sector that links back to an earlier one made dir_slots loop forever when no slot was free. --- tools/geos/trmouse.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index 29fa8981..7894dfdc 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -18,6 +18,7 @@ SOURCE = pathlib.Path(__file__).with_name('trmouse128.asm') NAME = b'TR USB MOUSE' +CLASS_NAME = b'TR USB Mouse' LOAD = 0xfd00 MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F @@ -112,7 +113,11 @@ def allocate(self): def dir_slots(self): track, sector = DIR_TRACK, FIRST_DIR_SECTOR + seen = set() while track: + if (track, sector) in seen or not 1 <= track <= 80 or sector >= SECTORS: + sys.exit(f'the directory chain is broken at {track}/{sector}') + seen.add((track, sector)) base = self.offset(track, sector) for i in range(8): yield base + i * 32 @@ -143,7 +148,7 @@ def info_block(size): info[0x44:0x47] = bytes((USR_CLOSED, INPUT_128, GEOS_SEQUENTIAL)) info[0x47:0x49] = LOAD.to_bytes(2, 'little') info[0x49:0x4b] = (LOAD + size).to_bytes(2, 'little') - info[0x4d:0x61] = padded(b'TR USB Mouse V1.0', 20) + info[0x4d:0x61] = padded(CLASS_NAME.ljust(12) + b'V1.0', 20) # GEOS reads the version at +12 info[0x61:0x75] = padded(b'TeensyROM', 20) info[0xa0:0x100] = padded(b'A USB mouse plugged into a TeensyROM.', 0x60) return bytes(info) From 2564b7846a5d8147766c59b0228c513e86f64eb0 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:05:32 -0500 Subject: [PATCH 015/100] firmware: the USB mouse poll fences the IRQ disable, and clears a report an unplug strands On the Cortex-M7 an NVIC_ICER write is not guaranteed to block the IRQ until a DSB/ISB, and without a compiler barrier the inlined getMouseX/Y loads may be hoisted above the disable, so the window 78cbc052 closes could stay open. A report landing just before an unplug was left pending and delivered (buttons included) on the next plug. IRQ_USBHS is in scope through USBHost_t36.h, so use the library's name. --- .../MinimalBoot/Common/IO_Handlers/USBMouse.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index 41ce13c9..5b0bacc5 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -30,19 +30,22 @@ void PollUSBMouse() // MouseController is also a BTHIDInput, so its own bool conversion is ambiguous if (!static_cast(usbHostMouse)) { - USBMouse_Buttons = 0; // unplugging leaves the library's last report in place + // unplugging leaves the library's last report in place, for a replug to deliver + usbHostMouse.mouseDataClear(); + USBMouse_Buttons = 0; return; } if (!usbHostMouse.available()) return; // the USB ISR writes each report straight into the controller, and mouseDataClear() - // would drop one that lands between these reads and the clear. IRQ_USB2 is the host - // port's, which USBHost_t36 calls IRQ_USBHS in a header of its own - NVIC_DISABLE_IRQ(IRQ_USB2); + // would drop one that lands between these reads and the clear + NVIC_DISABLE_IRQ(IRQ_USBHS); + // the Cortex-M7 can still take the IRQ after the NVIC write until a DSB and ISB + asm volatile("dsb\n\tisb" ::: "memory"); int32_t X = usbHostMouse.getMouseX(); int32_t Y = usbHostMouse.getMouseY(); uint8_t Buttons = usbHostMouse.getButtons(); usbHostMouse.mouseDataClear(); - NVIC_ENABLE_IRQ(IRQ_USB2); + NVIC_ENABLE_IRQ(IRQ_USBHS); USBMouse_Counts[0] += X; USBMouse_Counts[1] += Y; USBMouse_Buttons = Buttons; From b89f417b33fbb700be7c3e67c427e6138beea56e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:07:57 -0500 Subject: [PATCH 016/100] tools: the mouse driver's off-pointer comment drops a 1351 comparison that does not hold --- tools/geos/trmouse128.asm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index bc2a226f..d18bcd93 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -79,7 +79,7 @@ readY lda $df00+regY sta deltaY bit mouseOn bmi readButtons -noMouse rts ; movement while the pointer is off is dropped, as a 1351's is +noMouse rts ; movement while the pointer is off is dropped readButtons lda $df00+regButtons From 20aa9558781029d5037a49636204dd2b9a8699b4 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:09:21 -0500 Subject: [PATCH 017/100] tools: trmouse.py stops on a directory chain into the header or BAM, and holds the class name to its 12 bytes --- tools/geos/trmouse.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index 7894dfdc..ce318959 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -19,10 +19,12 @@ SOURCE = pathlib.Path(__file__).with_name('trmouse128.asm') NAME = b'TR USB MOUSE' CLASS_NAME = b'TR USB Mouse' +CLASS_NAME_SIZE = 12 # GEOS reads the version right after it LOAD = 0xfd00 MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F D81_SIZE = 819200 +TRACKS = 80 SECTORS = 40 DIR_TRACK = 40 BAM_SECTORS = (1, 2) # tracks 1-40, 41-80 @@ -99,7 +101,7 @@ def bam_entry(self, track): return self.offset(DIR_TRACK, sector) + BAM_ENTRIES + ((track - 1) % SECTORS) * BAM_ENTRY_SIZE def allocate(self): - for track in range(1, 81): + for track in range(1, TRACKS + 1): if track == DIR_TRACK: continue entry = self.bam_entry(track) @@ -115,7 +117,8 @@ def dir_slots(self): track, sector = DIR_TRACK, FIRST_DIR_SECTOR seen = set() while track: - if (track, sector) in seen or not 1 <= track <= 80 or sector >= SECTORS: + in_header = track == DIR_TRACK and sector < FIRST_DIR_SECTOR + if (track, sector) in seen or in_header or not 1 <= track <= TRACKS or sector >= SECTORS: sys.exit(f'the directory chain is broken at {track}/{sector}') seen.add((track, sector)) base = self.offset(track, sector) @@ -148,7 +151,7 @@ def info_block(size): info[0x44:0x47] = bytes((USR_CLOSED, INPUT_128, GEOS_SEQUENTIAL)) info[0x47:0x49] = LOAD.to_bytes(2, 'little') info[0x49:0x4b] = (LOAD + size).to_bytes(2, 'little') - info[0x4d:0x61] = padded(CLASS_NAME.ljust(12) + b'V1.0', 20) # GEOS reads the version at +12 + info[0x4d:0x61] = padded(padded(CLASS_NAME, CLASS_NAME_SIZE, b' ') + b'V1.0', 20) info[0x61:0x75] = padded(b'TeensyROM', 20) info[0xa0:0x100] = padded(b'A USB mouse plugged into a TeensyROM.', 0x60) return bytes(info) From caedc75b2ad28ec1dde64b4e654e2e08369a888b Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:14:41 -0500 Subject: [PATCH 018/100] firmware: the USB mouse poll fences the IRQ re-enable against the compiler too mouseDataClear() keeps its stores ahead of NVIC_ENABLE_IRQ only because it lives in another translation unit; with LTO (opt=o2lto) it inlines, and its plain stores may sink past the volatile NVIC write, reopening the race on the way out. --- Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index 5b0bacc5..adbe925e 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -45,6 +45,7 @@ void PollUSBMouse() int32_t Y = usbHostMouse.getMouseY(); uint8_t Buttons = usbHostMouse.getButtons(); usbHostMouse.mouseDataClear(); + asm volatile("" ::: "memory"); NVIC_ENABLE_IRQ(IRQ_USBHS); USBMouse_Counts[0] += X; USBMouse_Counts[1] += Y; From 04de5c8560bf333e1e7d80b46003f4649d188fc8 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:17:18 -0500 Subject: [PATCH 019/100] tools: trmouse.py stops on a directory chain that leaves track 40 A 1581 directory lives only on track 40, sectors 3-39. A chain into a free data sector on any other track let allocate() hand that sector to the driver while the new directory entry was written over it. D81_SIZE is derived from the geometry it stands for. --- tools/geos/trmouse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index ce318959..d05a9288 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -23,9 +23,9 @@ LOAD = 0xfd00 MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F -D81_SIZE = 819200 TRACKS = 80 SECTORS = 40 +D81_SIZE = TRACKS * SECTORS * 256 DIR_TRACK = 40 BAM_SECTORS = (1, 2) # tracks 1-40, 41-80 FIRST_DIR_SECTOR = 3 @@ -117,8 +117,8 @@ def dir_slots(self): track, sector = DIR_TRACK, FIRST_DIR_SECTOR seen = set() while track: - in_header = track == DIR_TRACK and sector < FIRST_DIR_SECTOR - if (track, sector) in seen or in_header or not 1 <= track <= TRACKS or sector >= SECTORS: + in_directory = track == DIR_TRACK and FIRST_DIR_SECTOR <= sector < SECTORS + if (track, sector) in seen or not in_directory: sys.exit(f'the directory chain is broken at {track}/{sector}') seen.add((track, sector)) base = self.offset(track, sector) From f18ee292f815c933c49117d5108acf514a5b927d Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:56:05 -0500 Subject: [PATCH 020/100] tools: trmouse.py takes D64 and D71 boot disks too, follows the header's directory link, and grows a full directory --- tools/geos/trmouse.py | 137 +++++++++++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 35 deletions(-) diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index d05a9288..2d5ad915 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT """Put the TR USB mouse driver on a GEOS 128 boot disk. - python3 tools/geos/trmouse.py boot.d81 out.d81 + python3 tools/geos/trmouse.py boot.d81 out.d81 (or a .d64 or .d71) GEOS loads the first input driver in the boot disk's directory, so the driver goes into the entry ahead of every other one, and the input driver that was @@ -23,14 +23,8 @@ LOAD = 0xfd00 MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F -TRACKS = 80 -SECTORS = 40 -D81_SIZE = TRACKS * SECTORS * 256 -DIR_TRACK = 40 -BAM_SECTORS = (1, 2) # tracks 1-40, 41-80 -FIRST_DIR_SECTOR = 3 -BAM_ENTRIES = 0x10 -BAM_ENTRY_SIZE = 6 +SECTOR_SIZE = 256 +DIR_ENTRY_SIZE = 32 USR_CLOSED = 0x83 GEOS_SEQUENTIAL = 0 @@ -87,45 +81,116 @@ def padded(text, size, pad=b'\0'): return text + pad * (size - len(text)) -class D81: +def zone_sectors(track): + """A 1541's sectors per track, which a 1571 repeats on its second side.""" + track = (track - 1) % 35 + 1 + return 21 if track <= 17 else 19 if track <= 24 else 18 if track <= 30 else 17 + + +class Format: + def __init__(self, name, tracks, sectors, dir_track, dir_sectors, reserved, bam): + self.name, self.tracks, self.sectors = name, tracks, sectors + self.dir_track, self.dir_sectors = dir_track, dir_sectors + self.reserved = reserved # tracks DOS never hands out for file data + self.bam = bam # track -> (count, bitmap) as (track, sector, byte) + self.starts = [0] + for track in range(1, tracks + 1): + self.starts.append(self.starts[-1] + sectors(track)) + + @property + def size(self): + return self.starts[-1] * SECTOR_SIZE + + +def d64_bam(track): + return (18, 0, 4 + (track - 1) * 4), (18, 0, 5 + (track - 1) * 4) + + +def d71_bam(track): + if track <= 35: + return d64_bam(track) + return (18, 0, 0xdd + track - 36), (53, 0, (track - 36) * 3) + + +def d81_bam(track): + sector, entry = 1 + (track - 1) // 40, 0x10 + (track - 1) % 40 * 6 + return (40, sector, entry), (40, sector, entry + 1) + + +FORMATS = {f.size: f for f in ( + Format('D64', 35, zone_sectors, 18, range(1, 19), (18,), d64_bam), + Format('D71', 70, zone_sectors, 18, range(1, 19), (18, 53), d71_bam), + Format('D81', 80, lambda track: 40, 40, range(3, 40), (40,), d81_bam), +)} + + +class Disk: def __init__(self, image): - if len(image) != D81_SIZE: - sys.exit(f'not a D81 without error bytes ({len(image)} bytes, expected {D81_SIZE})') + self.format = FORMATS.get(len(image)) + if self.format is None: + sizes = ', '.join(f'{f.name} {f.size}' for f in FORMATS.values()) + sys.exit(f'not a D64, D71 or D81 without error bytes ({len(image)} bytes; expected {sizes})') self.image = bytearray(image) + if self.format.name == 'D71' and not self.image[self.offset(18, 0) + 3] & 0x80: + sys.exit('this D71 is formatted single-sided') def offset(self, track, sector): - return ((track - 1) * SECTORS + sector) * 256 + return (self.format.starts[track - 1] + sector) * SECTOR_SIZE - def bam_entry(self, track): - sector = BAM_SECTORS[(track - 1) // SECTORS] - return self.offset(DIR_TRACK, sector) + BAM_ENTRIES + ((track - 1) % SECTORS) * BAM_ENTRY_SIZE + def at(self, place): + track, sector, byte = place + return self.offset(track, sector) + byte + + def take(self, track): + """Mark the first free sector of a track in use; None if it has none.""" + count, bitmap = (self.at(p) for p in self.format.bam(track)) + for sector in range(self.format.sectors(track)): + byte, bit = bitmap + sector // 8, 1 << (sector % 8) + if self.image[byte] & bit: + self.image[byte] &= ~bit + self.image[count] -= 1 + return track, sector + return None def allocate(self): - for track in range(1, TRACKS + 1): - if track == DIR_TRACK: - continue - entry = self.bam_entry(track) - for sector in range(SECTORS): - byte, bit = entry + 1 + sector // 8, 1 << (sector % 8) - if self.image[byte] & bit: - self.image[byte] &= ~bit - self.image[entry] -= 1 - return track, sector + for track in range(1, self.format.tracks + 1): + if track not in self.format.reserved: + found = self.take(track) + if found: + return found sys.exit('the disk is full') - def dir_slots(self): - track, sector = DIR_TRACK, FIRST_DIR_SECTOR + def dir_sectors(self): + header = self.offset(self.format.dir_track, 0) + track, sector = self.image[header], self.image[header + 1] seen = set() while track: - in_directory = track == DIR_TRACK and FIRST_DIR_SECTOR <= sector < SECTORS + in_directory = track == self.format.dir_track and sector in self.format.dir_sectors if (track, sector) in seen or not in_directory: sys.exit(f'the directory chain is broken at {track}/{sector}') seen.add((track, sector)) + yield track, sector base = self.offset(track, sector) - for i in range(8): - yield base + i * 32 track, sector = self.image[base], self.image[base + 1] + def dir_slots(self): + for track, sector in self.dir_sectors(): + base = self.offset(track, sector) + for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE): + yield base + i * DIR_ENTRY_SIZE + + def grow_directory(self): + """Chain one more directory sector, as DOS does when a directory fills; returns its slots.""" + last = list(self.dir_sectors())[-1] + new = self.take(self.format.dir_track) + if new is None or new[1] not in self.format.dir_sectors: + sys.exit('the directory is full') + o = self.offset(*last) + self.image[o:o + 2] = bytes(new) + o = self.offset(*new) + self.image[o:o + SECTOR_SIZE] = padded(b'\x00\xff', SECTOR_SIZE) + return [o + i * DIR_ENTRY_SIZE for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE)] + def write_file(self, data, info): """Write a GEOS sequential file; returns its first data sector.""" sectors = [self.allocate() for _ in range((len(data) + 253) // 254)] @@ -136,10 +201,10 @@ def write_file(self, data, info): else: link = bytes((0, len(chunk) + 1)) o = self.offset(track, sector) - self.image[o:o + 256] = padded(link + chunk, 256) + self.image[o:o + SECTOR_SIZE] = padded(link + chunk, SECTOR_SIZE) info_ts = self.allocate() o = self.offset(*info_ts) - self.image[o:o + 256] = info + self.image[o:o + SECTOR_SIZE] = info return sectors[0], info_ts, len(sectors) + 1 @@ -158,14 +223,16 @@ def info_block(size): def install(image, code): - disk = D81(image) + disk = Disk(image) slots = list(disk.dir_slots()) entries = [s for s in slots if disk.image[s + 2] & 7] if any(disk.image[s + 5:s + 21].rstrip(b'\xa0') == NAME for s in entries): sys.exit(f'{NAME.decode()} is already on this disk') free = next((s for s in slots if disk.image[s + 2] == 0), None) if free is None: - sys.exit('the directory is full') + grown = disk.grow_directory() + slots += grown + free = grown[0] first, info_ts, blocks = disk.write_file(code, info_block(len(code))) now = datetime.datetime.now() From b17b18b0452ba2d947c3d2b34a3b206477d53388 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 13:58:50 -0500 Subject: [PATCH 021/100] tools: trmouse.py refuses a BAM that frees a live directory sector or undercounts a track, and a header with no directory link --- tools/geos/trmouse.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index 2d5ad915..5a36093c 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -147,6 +147,8 @@ def take(self, track): for sector in range(self.format.sectors(track)): byte, bit = bitmap + sector // 8, 1 << (sector % 8) if self.image[byte] & bit: + if not self.image[count]: + sys.exit(f'the BAM count for track {track} disagrees with its bitmap') self.image[byte] &= ~bit self.image[count] -= 1 return track, sector @@ -163,6 +165,8 @@ def allocate(self): def dir_sectors(self): header = self.offset(self.format.dir_track, 0) track, sector = self.image[header], self.image[header + 1] + if not track: + sys.exit('the disk header links to no directory') seen = set() while track: in_directory = track == self.format.dir_track and sector in self.format.dir_sectors @@ -173,23 +177,27 @@ def dir_sectors(self): base = self.offset(track, sector) track, sector = self.image[base], self.image[base + 1] + def slots(self, track, sector): + base = self.offset(track, sector) + return [base + i * DIR_ENTRY_SIZE for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE)] + def dir_slots(self): for track, sector in self.dir_sectors(): - base = self.offset(track, sector) - for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE): - yield base + i * DIR_ENTRY_SIZE + yield from self.slots(track, sector) def grow_directory(self): """Chain one more directory sector, as DOS does when a directory fills; returns its slots.""" - last = list(self.dir_sectors())[-1] + chain = list(self.dir_sectors()) new = self.take(self.format.dir_track) if new is None or new[1] not in self.format.dir_sectors: sys.exit('the directory is full') - o = self.offset(*last) + if new in chain: + sys.exit(f'the BAM shows directory sector {new[0]}/{new[1]} free') + o = self.offset(*chain[-1]) self.image[o:o + 2] = bytes(new) o = self.offset(*new) self.image[o:o + SECTOR_SIZE] = padded(b'\x00\xff', SECTOR_SIZE) - return [o + i * DIR_ENTRY_SIZE for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE)] + return self.slots(*new) def write_file(self, data, info): """Write a GEOS sequential file; returns its first data sector.""" From ea12ae50b78dcf1ea0d58b9a17a1c1f5f498f981 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 14:01:14 -0500 Subject: [PATCH 022/100] tools: trmouse.py refuses any undercounted track and any live directory sector the BAM frees, not only the first free one --- tools/geos/trmouse.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index 5a36093c..d8a72f61 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -141,18 +141,23 @@ def at(self, place): track, sector, byte = place return self.offset(track, sector) + byte + def free_sectors(self, track): + _, bitmap = (self.at(p) for p in self.format.bam(track)) + return [s for s in range(self.format.sectors(track)) + if self.image[bitmap + s // 8] & 1 << (s % 8)] + def take(self, track): """Mark the first free sector of a track in use; None if it has none.""" count, bitmap = (self.at(p) for p in self.format.bam(track)) - for sector in range(self.format.sectors(track)): - byte, bit = bitmap + sector // 8, 1 << (sector % 8) - if self.image[byte] & bit: - if not self.image[count]: - sys.exit(f'the BAM count for track {track} disagrees with its bitmap') - self.image[byte] &= ~bit - self.image[count] -= 1 - return track, sector - return None + free = self.free_sectors(track) + if self.image[count] < len(free): + sys.exit(f'the BAM count for track {track} disagrees with its bitmap') + if not free: + return None + sector = free[0] + self.image[bitmap + sector // 8] &= ~(1 << (sector % 8)) + self.image[count] -= 1 + return track, sector def allocate(self): for track in range(1, self.format.tracks + 1): @@ -188,11 +193,13 @@ def dir_slots(self): def grow_directory(self): """Chain one more directory sector, as DOS does when a directory fills; returns its slots.""" chain = list(self.dir_sectors()) + free = self.free_sectors(self.format.dir_track) + for track, sector in chain: + if sector in free: + sys.exit(f'the BAM shows directory sector {track}/{sector} free') new = self.take(self.format.dir_track) if new is None or new[1] not in self.format.dir_sectors: sys.exit('the directory is full') - if new in chain: - sys.exit(f'the BAM shows directory sector {new[0]}/{new[1]} free') o = self.offset(*chain[-1]) self.image[o:o + 2] = bytes(new) o = self.offset(*new) From be36f993b99661bc116ea65983368222bd76449e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:14:05 -0500 Subject: [PATCH 023/100] tools: trmouse.py writes the driver file as a C header for the firmware The firmware will put the same file on a boot disk that trmouse.py does, so the bytes come from one place. A test regenerates the header and fails when the committed copy drifts from trmouse128.asm. --- Source/Teensy/GEOS/TRMouse128.h | 41 +++++++++++++++++++++++++++++++++ tools/geos/trmouse.py | 30 ++++++++++++++++++++++-- tools/geos/trmouse.test.mjs | 31 +++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 Source/Teensy/GEOS/TRMouse128.h create mode 100644 tools/geos/trmouse.test.mjs diff --git a/Source/Teensy/GEOS/TRMouse128.h b/Source/Teensy/GEOS/TRMouse128.h new file mode 100644 index 00000000..0e5916f9 --- /dev/null +++ b/Source/Teensy/GEOS/TRMouse128.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit. +#pragma once +#include + +static const char TRMouse128Name[] = "TR USB MOUSE"; +static const uint8_t TRMouse128Code[226] = { + 0x4c, 0x10, 0xfd, 0x4c, 0x1a, 0xfd, 0x4c, 0x42, 0xfd, 0x4c, 0x1a, 0xfd, 0x80, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa0, 0xdf, 0x8c, 0x24, 0xfd, + 0xa2, 0x02, 0xbd, 0x03, 0xdf, 0xdd, 0x3f, 0xfd, 0xd0, 0x0e, 0xca, 0x10, 0xf5, 0x8c, 0x49, 0xfd, + 0x8c, 0x4f, 0xfd, 0x8c, 0x5a, 0xfd, 0x18, 0x60, 0x88, 0xc0, 0xdd, 0xd0, 0xe0, 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, 0xd9, 0xfd, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, + 0x06, 0x07 +}; +static const uint8_t TRMouse128Info[256] = { + 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, 0xe2, 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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x41, 0x20, 0x55, 0x53, 0x42, 0x20, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x20, 0x70, 0x6c, 0x75, 0x67, + 0x67, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, 0x65, 0x65, 0x6e, 0x73, + 0x79, 0x52, 0x4f, 0x4d, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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.py b/tools/geos/trmouse.py index d8a72f61..7a5bfb5f 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -3,6 +3,7 @@ """Put the TR USB mouse driver on a GEOS 128 boot disk. python3 tools/geos/trmouse.py boot.d81 out.d81 (or a .d64 or .d71) + python3 tools/geos/trmouse.py --c-header out.h (the driver file, for the firmware) GEOS loads the first input driver in the boot disk's directory, so the driver goes into the entry ahead of every other one, and the input driver that was @@ -269,11 +270,36 @@ def install(image, code): return bytes(disk.image) +def c_array(name, data): + rows = (', '.join(f'0x{b:02x}' for b in data[i:i + 16]) for i in range(0, len(data), 16)) + body = ',\n'.join(f' {row}' for row in rows) + return f'static const uint8_t {name}[{len(data)}] = {{\n{body}\n}};\n' + + +def c_header(code): + return ( + '// SPDX-License-Identifier: MIT\n' + '// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit.\n' + '#pragma once\n' + '#include \n\n' + f'static const char TRMouse128Name[] = "{NAME.decode()}";\n' + + c_array('TRMouse128Code', code) + + c_array('TRMouse128Info', info_block(len(code)))) + + def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - parser.add_argument('disk', type=pathlib.Path) - parser.add_argument('out', type=pathlib.Path) + parser.add_argument('disk', type=pathlib.Path, nargs='?') + parser.add_argument('out', type=pathlib.Path, nargs='?') + parser.add_argument('--c-header', type=pathlib.Path, metavar='OUT') args = parser.parse_args() + if args.c_header: + if args.disk: + parser.error('--c-header takes no disk images') + args.c_header.write_text(c_header(assemble())) + return + if not args.out: + parser.error('needs a boot disk and an output image') args.out.write_bytes(install(args.disk.read_bytes(), assemble())) diff --git a/tools/geos/trmouse.test.mjs b/tools/geos/trmouse.test.mjs new file mode 100644 index 00000000..ed13d1ca --- /dev/null +++ b/tools/geos/trmouse.test.mjs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +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 { fileURLToPath } from 'node:url'; +import { commandExists } from '../lib/toolchain.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const trmouse = path.join(root, 'tools/geos/trmouse.py'); +const committedHeader = path.join(root, 'Source/Teensy/GEOS/TRMouse128.h'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['acme', 'python3'].find((tool) => !commandExists(tool)); + +test('the committed driver header matches the driver source', { skip: missingTool && !inCi && `${missingTool} is not on PATH` }, () => { + assert.equal(missingTool, undefined, 'CI installs ACME and Python, so this test must not skip there'); + const out = fs.mkdtempSync(path.join(os.tmpdir(), 'trmouse-test-')); + try { + const header = path.join(out, 'TRMouse128.h'); + const r = spawnSync('python3', [trmouse, '--c-header', header], { encoding: 'utf8' }); + assert.equal(r.status, 0, r.stdout + r.stderr); + const committed = fs.readFileSync(committedHeader, 'utf8').replaceAll('\r\n', '\n'); + assert.equal(fs.readFileSync(header, 'utf8'), committed, 'rerun tools/geos/trmouse.py --c-header'); + } finally { + fs.rmSync(out, { recursive: true, force: true }); + } +}); From 29dc4448f4fc14fcf102d1424f8eae065aeb7943 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:15:54 -0500 Subject: [PATCH 024/100] tools: trmouse.py writes the driver header with LF endings and keeps its arrays in flash Python's write_text writes CRLF on Windows, which the drift test compared against an LF checkout. A Teensy 4 places const data without PROGMEM in RAM1. --- .gitattributes | 3 +++ Source/Teensy/GEOS/TRMouse128.h | 7 ++++--- tools/geos/trmouse.py | 9 +++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index ebeba5c5..f884fa2b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,6 @@ # Generated by tools/build-c64.mjs with LF endings; check them out as LF too so a # rebuild on Windows (core.autocrlf=true) doesn't flag unchanged headers as modified Source/Teensy/TRMenuFiles/ROMs/*.h text eol=lf + +# Generated by tools/geos/trmouse.py --c-header with LF endings, kept LF for the same reason +Source/Teensy/GEOS/*.h text eol=lf diff --git a/Source/Teensy/GEOS/TRMouse128.h b/Source/Teensy/GEOS/TRMouse128.h index 0e5916f9..75f2bc5b 100644 --- a/Source/Teensy/GEOS/TRMouse128.h +++ b/Source/Teensy/GEOS/TRMouse128.h @@ -2,9 +2,10 @@ // Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit. #pragma once #include +#include -static const char TRMouse128Name[] = "TR USB MOUSE"; -static const uint8_t TRMouse128Code[226] = { +static const char TRMouse128Name[] PROGMEM = "TR USB MOUSE"; +static const uint8_t TRMouse128Code[226] PROGMEM = { 0x4c, 0x10, 0xfd, 0x4c, 0x1a, 0xfd, 0x4c, 0x42, 0xfd, 0x4c, 0x1a, 0xfd, 0x80, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa0, 0xdf, 0x8c, 0x24, 0xfd, 0xa2, 0x02, 0xbd, 0x03, 0xdf, 0xdd, 0x3f, 0xfd, 0xd0, 0x0e, 0xca, 0x10, 0xf5, 0x8c, 0x49, 0xfd, @@ -21,7 +22,7 @@ static const uint8_t TRMouse128Code[226] = { 0xe8, 0xe8, 0xbd, 0xd9, 0xfd, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, 0x06, 0x07 }; -static const uint8_t TRMouse128Info[256] = { +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, diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index 7a5bfb5f..e5b4d890 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -273,7 +273,7 @@ def install(image, code): def c_array(name, data): rows = (', '.join(f'0x{b:02x}' for b in data[i:i + 16]) for i in range(0, len(data), 16)) body = ',\n'.join(f' {row}' for row in rows) - return f'static const uint8_t {name}[{len(data)}] = {{\n{body}\n}};\n' + return f'static const uint8_t {name}[{len(data)}] PROGMEM = {{\n{body}\n}};\n' def c_header(code): @@ -281,8 +281,9 @@ def c_header(code): '// SPDX-License-Identifier: MIT\n' '// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit.\n' '#pragma once\n' - '#include \n\n' - f'static const char TRMouse128Name[] = "{NAME.decode()}";\n' + '#include \n' + '#include \n\n' + f'static const char TRMouse128Name[] PROGMEM = "{NAME.decode()}";\n' + c_array('TRMouse128Code', code) + c_array('TRMouse128Info', info_block(len(code)))) @@ -296,7 +297,7 @@ def main(): if args.c_header: if args.disk: parser.error('--c-header takes no disk images') - args.c_header.write_text(c_header(assemble())) + args.c_header.write_bytes(c_header(assemble()).encode()) return if not args.out: parser.error('needs a boot disk and an output image') From d73741c8323170444301d4ee1716a3cf3318be8c Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:18:49 -0500 Subject: [PATCH 025/100] firmware: a GEOS 128 boot-disk patch that adds the TR USB mouse driver block by block It reads and writes 256-byte blocks through callbacks, so it can patch an image on SD that is too large for RAM. A host test compiles it and holds it byte for byte to trmouse.py on D64, D71 and D81 boot disks, refusals included. --- Source/Teensy/GEOS/GeosDiskPatch.h | 445 ++++++++++++++++++ Source/Teensy/GEOS/TRMouse128.h | 4 + Source/Teensy/tests/geos-disk-patch.test.js | 216 +++++++++ Source/Teensy/tests/geos_disk_patch_harness.c | 51 ++ tools/geos/trmouse.py | 6 +- 5 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 Source/Teensy/GEOS/GeosDiskPatch.h create mode 100644 Source/Teensy/tests/geos-disk-patch.test.js create mode 100644 Source/Teensy/tests/geos_disk_patch_harness.c diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h new file mode 100644 index 00000000..471c43ea --- /dev/null +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: MIT +// Puts the TR USB mouse driver on a GEOS 128 boot disk image, block by block. +// Mirrors tools/geos/trmouse.py install(), which the host test holds it to byte for byte. +#pragma once +#include +#include +#include +#include + +#include "TRMouse128.h" + +#define GEOS_SECTOR_SIZE 256 +#define GEOS_DIR_ENTRY_SIZE 32 +#define GEOS_ENTRIES_PER_DIR (GEOS_SECTOR_SIZE / GEOS_DIR_ENTRY_SIZE) +#define GEOS_NAME_SIZE 16 +#define GEOS_MAX_DIR_SECTORS 40 +#define GEOS_ERROR_SIZE 80 + +#define CBM_NAME_PAD 0xa0 +#define CBM_TYPE_MASK 0x07 +#define CBM_USR_CLOSED 0x83 +#define GEOS_SEQUENTIAL 0 +#define GEOS_INPUT_128 15 +#define D71_DOUBLE_SIDED 0x80 + +// Byte offsets inside a 32-byte directory slot (the first two hold the sector link) +enum GeosSlot +{ + SlotType = 2, + SlotFirst = 3, + SlotName = 5, + SlotInfo = 21, + SlotStructure = 23, + SlotGeosType = 24, + SlotDate = 25, + SlotBlocks = 30, +}; + +struct GeosPlace { uint8_t Track, Sector, Byte; }; +struct GeosTS { uint8_t Track, Sector; }; + +struct GeosFormat +{ + const char *Name; + uint8_t Tracks, DirTrack, FirstDirSector, LastDirSector; + uint8_t Reserved[2]; + uint32_t Size; + bool FixedSectors; // D81: 40 per track; else the 1541's zones, repeated on a 1571's second side +}; + +struct GeosDate { uint8_t Year, Month, Day, Hour, Minute; }; + +// A boot disk image reached through 256-byte block reads and writes at byte offsets. +struct GeosDisk +{ + const struct GeosFormat *Format; + void *Context; + bool (*Read)(void *Context, uint32_t Offset, uint8_t *Block); + bool (*Write)(void *Context, uint32_t Offset, const uint8_t *Block); + char Error[GEOS_ERROR_SIZE]; +}; + +static const struct GeosFormat GeosFormats[] = +{ + { "D64", 35, 18, 1, 18, { 18, 0 }, 174848, false }, + { "D71", 70, 18, 1, 18, { 18, 53 }, 349696, false }, + { "D81", 80, 40, 3, 39, { 40, 0 }, 819200, true }, +}; + +static bool GeosFail(struct GeosDisk *Disk, const char *Message) +{ + snprintf(Disk->Error, sizeof Disk->Error, "%s", Message); + return false; +} + +static uint8_t GeosSectorsOnTrack(const struct GeosFormat *Format, uint8_t Track) +{ + uint8_t ZoneTrack = (Track - 1) % 35 + 1; + + if (Format->FixedSectors) return 40; + return ZoneTrack <= 17 ? 21 : ZoneTrack <= 24 ? 19 : ZoneTrack <= 30 ? 18 : 17; +} + +static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +{ + uint32_t Blocks = Sector; + + for (uint8_t t = 1; t < Track; t++) Blocks += GeosSectorsOnTrack(Format, t); + return Blocks * GEOS_SECTOR_SIZE; +} + +static bool GeosReadTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, uint8_t *Block) +{ + if (Disk->Read(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; + return GeosFail(Disk, "the image could not be read"); +} + +static bool GeosWriteTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Block) +{ + if (Disk->Write(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; + return GeosFail(Disk, "the image could not be written"); +} + +static bool GeosIsReserved(const struct GeosFormat *Format, uint8_t Track) +{ + return Track == Format->Reserved[0] || Track == Format->Reserved[1]; +} + +static bool GeosIsDirSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +{ + return Track == Format->DirTrack && Sector >= Format->FirstDirSector && Sector <= Format->LastDirSector; +} + +// Where a track's free count and free-sector bitmap live +static void GeosBamPlaces(const struct GeosFormat *Format, uint8_t Track, struct GeosPlace *Count, struct GeosPlace *Bitmap) +{ + if (Format->FixedSectors) + { + uint8_t Sector = 1 + (Track - 1) / 40, Entry = 0x10 + (Track - 1) % 40 * 6; + *Count = (struct GeosPlace){ 40, Sector, Entry }; + *Bitmap = (struct GeosPlace){ 40, Sector, (uint8_t)(Entry + 1) }; + } + else if (Track <= 35) + { + *Count = (struct GeosPlace){ 18, 0, (uint8_t)(4 + (Track - 1) * 4) }; + *Bitmap = (struct GeosPlace){ 18, 0, (uint8_t)(5 + (Track - 1) * 4) }; + } + else + { + *Count = (struct GeosPlace){ 18, 0, (uint8_t)(0xdd + Track - 36) }; + *Bitmap = (struct GeosPlace){ 53, 0, (uint8_t)((Track - 36) * 3) }; + } +} + +static bool GeosSectorIsFree(const uint8_t *Bitmap, uint8_t Sector) +{ + return Bitmap[Sector / 8] & (1 << (Sector % 8)); +} + +static uint8_t GeosCountFree(const struct GeosFormat *Format, uint8_t Track, const uint8_t *Bitmap, int *FirstFree) +{ + uint8_t Free = 0; + + *FirstFree = -1; + for (uint8_t s = 0; s < GeosSectorsOnTrack(Format, Track); s++) + { + if (!GeosSectorIsFree(Bitmap, s)) continue; + if (*FirstFree < 0) *FirstFree = s; + Free++; + } + return Free; +} + +// Marks the first free sector of a track in use; *Sector is -1 when the track has none. +static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) +{ + struct GeosPlace Count, Bitmap; + uint8_t CountBlock[GEOS_SECTOR_SIZE], BitmapBlock[GEOS_SECTOR_SIZE]; + bool Shared; + + GeosBamPlaces(Disk->Format, Track, &Count, &Bitmap); + Shared = Count.Track == Bitmap.Track && Count.Sector == Bitmap.Sector; + if (!GeosReadTS(Disk, Count.Track, Count.Sector, CountBlock)) return false; + if (!Shared && !GeosReadTS(Disk, Bitmap.Track, Bitmap.Sector, BitmapBlock)) return false; + uint8_t *Bits = (Shared ? CountBlock : BitmapBlock) + Bitmap.Byte; + + uint8_t Free = GeosCountFree(Disk->Format, Track, Bits, Sector); + if (CountBlock[Count.Byte] < Free) + { + snprintf(Disk->Error, sizeof Disk->Error, "the BAM count for track %u disagrees with its bitmap", Track); + return false; + } + if (*Sector < 0) return true; + + Bits[*Sector / 8] &= ~(1 << (*Sector % 8)); + CountBlock[Count.Byte]--; + if (!Shared && !GeosWriteTS(Disk, Bitmap.Track, Bitmap.Sector, BitmapBlock)) return false; + return GeosWriteTS(Disk, Count.Track, Count.Sector, CountBlock); +} + +static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) +{ + for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) + { + int Sector; + if (GeosIsReserved(Disk->Format, Track)) continue; + if (!GeosTake(Disk, Track, &Sector)) return false; + if (Sector < 0) continue; + *Where = (struct GeosTS){ Track, (uint8_t)Sector }; + return true; + } + return GeosFail(Disk, "the disk is full"); +} + +static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, uint8_t Sector) +{ + for (int i = 0; i < Length; i++) + { + if (Chain[i].Track == Track && Chain[i].Sector == Sector) return true; + } + return false; +} + +static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +{ + uint8_t Block[GEOS_SECTOR_SIZE]; + uint8_t Track, Sector; + + *Length = 0; + if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Block)) return false; + Track = Block[0]; + Sector = Block[1]; + if (!Track) return GeosFail(Disk, "the disk header links to no directory"); + + while (Track) + { + if (GeosChainHas(Chain, *Length, Track, Sector) || !GeosIsDirSector(Disk->Format, Track, Sector) || + *Length == GEOS_MAX_DIR_SECTORS) + { + snprintf(Disk->Error, sizeof Disk->Error, "the directory chain is broken at %u/%u", Track, Sector); + return false; + } + Chain[(*Length)++] = (struct GeosTS){ Track, Sector }; + if (!GeosReadTS(Disk, Track, Sector, Block)) return false; + Track = Block[0]; + Sector = Block[1]; + } + return true; +} + +// Chains one more directory sector, as DOS does when a directory fills. +static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +{ + const struct GeosFormat *Format = Disk->Format; + struct GeosPlace Count, Bitmap; + uint8_t Block[GEOS_SECTOR_SIZE]; + int New; + + GeosBamPlaces(Format, Format->DirTrack, &Count, &Bitmap); + if (!GeosReadTS(Disk, Bitmap.Track, Bitmap.Sector, Block)) return false; + for (int i = 0; i < *Length; i++) + { + if (!GeosSectorIsFree(Block + Bitmap.Byte, Chain[i].Sector)) continue; + snprintf(Disk->Error, sizeof Disk->Error, "the BAM shows directory sector %u/%u free", Chain[i].Track, Chain[i].Sector); + return false; + } + + if (!GeosTake(Disk, Format->DirTrack, &New)) return false; + if (New < 0 || !GeosIsDirSector(Format, Format->DirTrack, New) || *Length == GEOS_MAX_DIR_SECTORS) + { + return GeosFail(Disk, "the directory is full"); + } + + struct GeosTS Last = Chain[*Length - 1]; + if (!GeosReadTS(Disk, Last.Track, Last.Sector, Block)) return false; + Block[0] = Format->DirTrack; + Block[1] = New; + if (!GeosWriteTS(Disk, Last.Track, Last.Sector, Block)) return false; + + memset(Block, 0, sizeof Block); + Block[1] = 0xff; + Chain[(*Length)++] = (struct GeosTS){ Format->DirTrack, (uint8_t)New }; + return GeosWriteTS(Disk, Format->DirTrack, New, Block); +} + +// Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. +static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) +{ + const uint16_t DataPerBlock = GEOS_SECTOR_SIZE - 2; + uint16_t DataBlocks = (sizeof TRMouse128Code + DataPerBlock - 1) / DataPerBlock; + struct GeosTS Here, Next; + uint8_t Block[GEOS_SECTOR_SIZE]; + + if (!GeosAllocate(Disk, &Here)) return false; + *First = Here; + for (uint16_t i = 0; i < DataBlocks; i++) + { + uint16_t Start = i * DataPerBlock, Length = sizeof TRMouse128Code - Start; + if (Length > DataPerBlock) Length = DataPerBlock; + 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, TRMouse128Code + Start, Length); + if (!GeosWriteTS(Disk, Here.Track, Here.Sector, Block)) return false; + Here = Next; + } + + if (!GeosAllocate(Disk, Info)) return false; + *Blocks = DataBlocks + 1; + return GeosWriteTS(Disk, Info->Track, Info->Sector, TRMouse128Info); +} + +static void GeosBuildEntry(uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) +{ + memset(Slot + SlotType, 0, GEOS_DIR_ENTRY_SIZE - SlotType); + Slot[SlotType] = CBM_USR_CLOSED; + Slot[SlotFirst] = First.Track; + Slot[SlotFirst + 1] = First.Sector; + memset(Slot + SlotName, CBM_NAME_PAD, GEOS_NAME_SIZE); + memcpy(Slot + SlotName, TRMouse128Name, strlen(TRMouse128Name)); + Slot[SlotInfo] = Info.Track; + Slot[SlotInfo + 1] = Info.Sector; + Slot[SlotStructure] = GEOS_SEQUENTIAL; + Slot[SlotGeosType] = GEOS_INPUT_128; + Slot[SlotDate] = Date->Year % 100; + Slot[SlotDate + 1] = Date->Month; + Slot[SlotDate + 2] = Date->Day; + Slot[SlotDate + 3] = Date->Hour; + Slot[SlotDate + 4] = Date->Minute; + Slot[SlotBlocks] = Blocks & 0xff; + Slot[SlotBlocks + 1] = Blocks >> 8; +} + +static bool GeosSlotNamed(const uint8_t *Slot, const char *Name) +{ + size_t Length = strlen(Name); + + if (memcmp(Slot + SlotName, Name, Length)) return false; + for (size_t i = Length; i < GEOS_NAME_SIZE; i++) + { + if (Slot[SlotName + i] != CBM_NAME_PAD) return false; + } + return true; +} + +// A slot's place in directory order, as an index over the chain's sectors and their slots +struct GeosSlotRef { int Sector, Slot; }; + +struct GeosDirScan +{ + struct GeosSlotRef Free, FirstDriver; + bool HasFree, HasDriver, HasMouse; +}; + +static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) +{ + uint8_t Block[GEOS_SECTOR_SIZE]; + + memset(Scan, 0, sizeof *Scan); + for (int i = 0; i < Length; i++) + { + if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; + for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) + { + const uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; + struct GeosSlotRef Here = { i, e }; + bool InUse = Slot[SlotType] & CBM_TYPE_MASK; + + if (!Slot[SlotType] && !Scan->HasFree) { Scan->Free = Here; Scan->HasFree = true; } + if (!InUse) continue; + if (GeosSlotNamed(Slot, TRMouse128Name)) Scan->HasMouse = true; + if (Slot[SlotGeosType] == GEOS_INPUT_128 && !Scan->HasDriver) { Scan->FirstDriver = Here; Scan->HasDriver = true; } + } + } + return true; +} + +static bool GeosSlotBefore(struct GeosSlotRef a, struct GeosSlotRef b) +{ + return a.Sector < b.Sector || (a.Sector == b.Sector && a.Slot < b.Slot); +} + +static bool GeosSwapEntries(struct GeosDisk *Disk, const struct GeosTS *Chain, struct GeosSlotRef a, struct GeosSlotRef b) +{ + uint8_t BlockA[GEOS_SECTOR_SIZE], BlockB[GEOS_SECTOR_SIZE], Saved[GEOS_DIR_ENTRY_SIZE]; + bool Shared = a.Sector == b.Sector; + uint8_t *SlotA = BlockA + a.Slot * GEOS_DIR_ENTRY_SIZE + SlotType; + uint8_t *SlotB = (Shared ? BlockA : BlockB) + b.Slot * GEOS_DIR_ENTRY_SIZE + SlotType; + const size_t EntryBytes = GEOS_DIR_ENTRY_SIZE - SlotType; + + if (!GeosReadTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA)) return false; + if (!Shared && !GeosReadTS(Disk, Chain[b.Sector].Track, Chain[b.Sector].Sector, BlockB)) return false; + memcpy(Saved, SlotA, EntryBytes); + memcpy(SlotA, SlotB, EntryBytes); + memcpy(SlotB, Saved, EntryBytes); + if (!Shared && !GeosWriteTS(Disk, Chain[b.Sector].Track, Chain[b.Sector].Sector, BlockB)) return false; + return GeosWriteTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA); +} + +static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, const struct GeosDate *Date) +{ + struct GeosTS First, Info; + uint16_t Blocks; + uint8_t Block[GEOS_SECTOR_SIZE]; + + if (!GeosWriteDriver(Disk, &First, &Info, &Blocks)) return false; + if (!GeosReadTS(Disk, Where.Track, Where.Sector, Block)) return false; + GeosBuildEntry(Block + Slot * GEOS_DIR_ENTRY_SIZE, First, Info, Blocks, Date); + return GeosWriteTS(Disk, Where.Track, Where.Sector, Block); +} + +const struct GeosFormat *GeosFormatForSize(uint32_t Size) +{ + for (size_t i = 0; i < sizeof GeosFormats / sizeof GeosFormats[0]; i++) + { + if (GeosFormats[i].Size == Size) return &GeosFormats[i]; + } + return NULL; +} + +// Checks the image and returns whether it already holds the driver. +bool GeosHasMouseDriver(struct GeosDisk *Disk, bool *HasMouse) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; + struct GeosDirScan Scan; + uint8_t Header[GEOS_SECTOR_SIZE]; + int Length; + + if (!Disk->Format) return GeosFail(Disk, "not a D64, D71 or D81 without error bytes"); + if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Header)) return false; + if (Disk->Format->Size == 349696 && !(Header[3] & D71_DOUBLE_SIDED)) + { + return GeosFail(Disk, "this D71 is formatted single-sided"); + } + if (!GeosDirChain(Disk, Chain, &Length)) return false; + if (!GeosScanDirectory(Disk, Chain, Length, &Scan)) return false; + *HasMouse = Scan.HasMouse; + return true; +} + +// Writes the driver ahead of every other input driver, since GEOS 128 loads the first one in the directory. +bool GeosInstallMouseDriver(struct GeosDisk *Disk, const struct GeosDate *Date) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; + struct GeosDirScan Scan; + bool HasMouse; + int Length; + + if (!GeosHasMouseDriver(Disk, &HasMouse)) return false; + if (HasMouse) return GeosFail(Disk, "TR USB MOUSE is already on this disk"); + if (!GeosDirChain(Disk, Chain, &Length)) return false; + if (!GeosScanDirectory(Disk, Chain, Length, &Scan)) return false; + if (!Scan.HasFree) + { + if (!GeosGrowDirectory(Disk, Chain, &Length)) return false; + Scan.Free = (struct GeosSlotRef){ Length - 1, 0 }; + } + + if (!GeosWriteEntry(Disk, Chain[Scan.Free.Sector], Scan.Free.Slot, Date)) return false; + if (!Scan.HasDriver || !GeosSlotBefore(Scan.FirstDriver, Scan.Free)) return true; + return GeosSwapEntries(Disk, Chain, Scan.FirstDriver, Scan.Free); +} diff --git a/Source/Teensy/GEOS/TRMouse128.h b/Source/Teensy/GEOS/TRMouse128.h index 75f2bc5b..1658658c 100644 --- a/Source/Teensy/GEOS/TRMouse128.h +++ b/Source/Teensy/GEOS/TRMouse128.h @@ -2,7 +2,11 @@ // Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit. #pragma once #include +#ifdef ARDUINO #include +#else +#define PROGMEM +#endif static const char TRMouse128Name[] PROGMEM = "TR USB MOUSE"; static const uint8_t TRMouse128Code[226] PROGMEM = { diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js new file mode 100644 index 00000000..f1a79222 --- /dev/null +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +// The firmware's GEOS boot-disk patch, held byte for byte to tools/geos/trmouse.py on synthetic boot disks. +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 FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); +const TRMOUSE = path.resolve(FIRMWARE_ROOT, '../../tools/geos/trmouse.py'); +const HARNESS_SOURCE = path.join(FIRMWARE_ROOT, 'tests/geos_disk_patch_harness.c'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['acme', 'python3', 'cc', 'c++'].find((tool) => !commandExists(tool)); +const skip = missingTool && !inCi && `${missingTool} is not on PATH`; + +const SECTOR = 256; +const PAD = 0xa0; +const USR_CLOSED = 0x83; +const INPUT_128 = 15; +const MOUSE_NAME = 'TR USB MOUSE'; +const DATE_IN_SLOT = 25; +const DATE_BYTES = 5; + +const zoneSectors = (track) => { + const zone = (track - 1) % 35 + 1; + return zone <= 17 ? 21 : zone <= 24 ? 19 : zone <= 30 ? 18 : 17; +}; + +const FORMATS = { + d64: { tracks: 35, sectors: zoneSectors, dirTrack: 18, firstDir: 1 }, + d71: { tracks: 70, sectors: zoneSectors, dirTrack: 18, firstDir: 1 }, + d81: { tracks: 80, sectors: () => 40, dirTrack: 40, firstDir: 3 }, +}; + +function offset(format, track, sector) { + let blocks = sector; + for (let t = 1; t < track; t++) blocks += format.sectors(t); + return blocks * SECTOR; +} + +function imageSize(format) { + return offset(format, format.tracks + 1, 0); +} + +function bamPlaces(kind, track) { + if (kind === 'd81') { + const entry = 0x10 + ((track - 1) % 40) * 6; + const sector = 1 + Math.floor((track - 1) / 40); + return { count: [40, sector, entry], bitmap: [40, sector, entry + 1] }; + } + if (track <= 35) return { count: [18, 0, 4 + (track - 1) * 4], bitmap: [18, 0, 5 + (track - 1) * 4] }; + return { count: [18, 0, 0xdd + track - 36], bitmap: [53, 0, (track - 36) * 3] }; +} + +function at(disk, [track, sector, byte]) { + return offset(disk.format, track, sector) + byte; +} + +function setTrackFree(disk, track, freeSectors) { + const { count, bitmap } = bamPlaces(disk.kind, track); + const bits = at(disk, bitmap); + for (let s = 0; s < disk.format.sectors(track); s++) { + const mask = 1 << (s % 8); + if (freeSectors.includes(s)) disk.image[bits + (s >> 3)] |= mask; + else disk.image[bits + (s >> 3)] &= ~mask; + } + disk.image[at(disk, count)] = freeSectors.length; +} + +function allSectors(disk, track) { + return Array.from({ length: disk.format.sectors(track) }, (_, s) => s); +} + +function writeHeader(disk) { + const { image, format, kind } = disk; + const header = offset(format, format.dirTrack, 0); + image.set([format.dirTrack, format.firstDir], header); + if (kind === 'd81') { + image.set([40, 2, 0x44, 0xbb], offset(format, 40, 1)); + image.set([0, 0xff, 0x44, 0xbb], offset(format, 40, 2)); + } + if (kind === 'd71') image[header + 3] = 0x80; + image[offset(format, format.dirTrack, format.firstDir) + 1] = 0xff; +} + +function blankDisk(kind) { + const format = FORMATS[kind]; + const disk = { kind, format, image: new Uint8Array(imageSize(format)) }; + writeHeader(disk); + for (let track = 1; track <= format.tracks; track++) setTrackFree(disk, track, allSectors(disk, track)); + const systemSectors = kind === 'd81' ? [0, 1, 2, 3] : [0, 1]; + setTrackFree(disk, format.dirTrack, allSectors(disk, format.dirTrack).filter((s) => !systemSectors.includes(s))); + if (kind === 'd71') setTrackFree(disk, 53, []); + return disk; +} + +function putEntry(disk, slot, { name, type = USR_CLOSED, geosType = 0 }) { + const base = offset(disk.format, disk.format.dirTrack, disk.format.firstDir) + slot * 32; + disk.image.fill(PAD, base + 5, base + 21); + disk.image.set(Buffer.from(name, 'latin1'), base + 5); + disk.image[base + 2] = type; + disk.image[base + 24] = geosType; +} + +function withDriver(kind) { + const disk = blankDisk(kind); + putEntry(disk, 0, { name: '128 JOYSTICK', geosType: INPUT_128 }); + putEntry(disk, 1, { name: '128 DESKTOP', geosType: 6 }); + return disk; +} + +function fullDirectory(kind) { + const disk = blankDisk(kind); + for (let slot = 0; slot < 8; slot++) { + putEntry(disk, slot, slot === 3 ? { name: '128 JOYSTICK', geosType: INPUT_128 } : { name: `FILE ${slot}` }); + } + return disk; +} + +const PATCHED_CASES = { + 'a blank boot disk': blankDisk, + 'a disk whose first input driver comes earlier': withDriver, + 'a full first directory sector': fullDirectory, +}; + +const REFUSED_CASES = { + 'a disk that already has the driver': (kind) => { + const disk = withDriver(kind); + putEntry(disk, 2, { name: MOUSE_NAME, geosType: INPUT_128 }); + return disk; + }, + 'a BAM count below its bitmap': (kind) => { + const disk = blankDisk(kind); + disk.image[at(disk, bamPlaces(kind, 1).count)] -= 1; + return disk; + }, + 'a header linking outside the directory': (kind) => { + const disk = blankDisk(kind); + disk.image[offset(disk.format, disk.format.dirTrack, 0)] = 1; + return disk; + }, + 'a full directory whose BAM frees its sector': (kind) => { + const disk = fullDirectory(kind); + const free = allSectors(disk, disk.format.dirTrack).filter((s) => s >= disk.format.firstDir); + setTrackFree(disk, disk.format.dirTrack, free); + return disk; + }, +}; + +function maskMouseDate(image) { + const name = Buffer.concat([Buffer.from(MOUSE_NAME, 'latin1'), Buffer.alloc(16 - MOUSE_NAME.length, PAD)]); + const found = Buffer.from(image).indexOf(name); + assert.notEqual(found, -1, 'the patched image has no TR USB MOUSE entry'); + const date = found - 5 + DATE_IN_SLOT; + const masked = Buffer.from(image); + masked.fill(0, date, date + DATE_BYTES); + return masked; +} + +function run(command, args) { + return spawnSync(command, args, { encoding: 'utf8' }); +} + +function patchBoth(dir, label, image) { + const input = path.join(dir, `${label}.in`); + const viaPython = path.join(dir, `${label}.py.out`); + const viaFirmware = path.join(dir, `${label}.c.out`); + fs.writeFileSync(input, image); + return { + python: { ...run('python3', [TRMOUSE, input, viaPython]), out: viaPython }, + firmware: { ...run(path.join(dir, 'harness'), [input, viaFirmware]), out: viaFirmware }, + }; +} + +test('the firmware patch matches trmouse.py', { skip }, async (t) => { + assert.equal(missingTool, undefined, 'CI installs every tool this test needs, so it must not skip there'); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'geos-disk-patch-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const built = run('cc', ['-std=c99', '-Wall', '-Wextra', '-Werror', '-o', path.join(dir, 'harness'), HARNESS_SOURCE]); + assert.equal(built.status, 0, built.stderr); + const asCpp = run('c++', ['-x', 'c++', '-std=gnu++17', '-Wall', '-Werror', '-fsyntax-only', HARNESS_SOURCE]); + assert.equal(asCpp.status, 0, `the firmware compiles this as C++: ${asCpp.stderr}`); + + for (const kind of Object.keys(FORMATS)) { + for (const [label, make] of Object.entries(PATCHED_CASES)) { + await t.test(`${kind}: ${label}`, () => { + const { python, firmware } = patchBoth(dir, `${kind}-${label}`, make(kind).image); + assert.equal(python.status, 0, python.stderr); + assert.equal(firmware.status, 0, firmware.stderr); + assert.deepEqual(maskMouseDate(fs.readFileSync(firmware.out)), maskMouseDate(fs.readFileSync(python.out))); + }); + } + for (const [label, make] of Object.entries(REFUSED_CASES)) { + await t.test(`${kind}: refuses ${label}`, () => { + const { python, firmware } = patchBoth(dir, `${kind}-${label}`, make(kind).image); + assert.equal(firmware.status, 1, `the firmware patched it: ${firmware.stderr}`); + assert.notEqual(python.status, 0, 'trmouse.py patched it'); + assert.equal(firmware.stderr.trim(), python.stderr.trim()); + }); + } + } + + await t.test('d71: refuses a single-sided disk', () => { + const disk = blankDisk('d71'); + disk.image[offset(disk.format, 18, 0) + 3] = 0; + const { python, firmware } = patchBoth(dir, 'd71-single', disk.image); + assert.equal(firmware.status, 1); + assert.equal(firmware.stderr.trim(), python.stderr.trim()); + }); +}); diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c new file mode 100644 index 00000000..a6d04303 --- /dev/null +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// geos_disk_patch_harness IN OUT: the firmware's GEOS boot-disk patch, run on an image file. +// Exits 0 after writing OUT, 1 with the patcher's refusal on stderr. +#include + +#include "../GEOS/GeosDiskPatch.h" + +#define MAX_IMAGE 819200 + +static uint8_t Image[MAX_IMAGE]; + +static bool ReadBlock(void *Context, uint32_t Offset, uint8_t *Block) +{ + memcpy(Block, (uint8_t *)Context + Offset, GEOS_SECTOR_SIZE); + return true; +} + +static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) +{ + memcpy((uint8_t *)Context + Offset, Block, GEOS_SECTOR_SIZE); + return true; +} + +int main(int argc, char **argv) +{ + const struct GeosDate Date = { 26, 10, 16, 12, 0 }; + struct GeosDisk Disk = { 0 }; + FILE *In, *Out; + size_t Size; + + if (argc != 3) return 2; + In = fopen(argv[1], "rb"); + if (!In) return 2; + Size = fread(Image, 1, sizeof Image, In); + fclose(In); + + Disk.Format = GeosFormatForSize(Size); + Disk.Context = Image; + Disk.Read = ReadBlock; + Disk.Write = WriteBlock; + if (!GeosInstallMouseDriver(&Disk, &Date)) + { + fprintf(stderr, "%s\n", Disk.Error); + return 1; + } + + Out = fopen(argv[2], "wb"); + if (!Out || fwrite(Image, 1, Size, Out) != Size) return 2; + fclose(Out); + return 0; +} diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index e5b4d890..178d51d0 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -282,7 +282,11 @@ def c_header(code): '// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit.\n' '#pragma once\n' '#include \n' - '#include \n\n' + '#ifdef ARDUINO\n' + '#include \n' + '#else\n' + '#define PROGMEM\n' + '#endif\n\n' f'static const char TRMouse128Name[] PROGMEM = "{NAME.decode()}";\n' + c_array('TRMouse128Code', code) + c_array('TRMouse128Info', info_block(len(code)))) From 4be1f11ad9b38090b42302421494e71813aca559 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:19:37 -0500 Subject: [PATCH 026/100] gitattributes: pin LF only on the generated GEOS mouse driver header, not the hand-written patch header --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index f884fa2b..84883f63 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,4 +6,4 @@ Source/Teensy/TRMenuFiles/ROMs/*.h text eol=lf # Generated by tools/geos/trmouse.py --c-header with LF endings, kept LF for the same reason -Source/Teensy/GEOS/*.h text eol=lf +Source/Teensy/GEOS/TRMouse128.h text eol=lf From 5825d1644118a39202126e6dc9db377b3227a375 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:25:41 -0500 Subject: [PATCH 027/100] firmware: the GEOS disk patch refuses a full disk or directory before its first write, and links from more than one file A refusal used to come after blocks were already written, which on an image patched in place on SD leaves it half patched. The harness now fails a refusal that changed the image, and refuses an image too big for any format rather than reading its first 800K. --- Source/Teensy/GEOS/GeosDiskPatch.h | 161 +++++++++++------- Source/Teensy/tests/geos-disk-patch.test.js | 13 ++ Source/Teensy/tests/geos_disk_patch_harness.c | 8 +- 3 files changed, 122 insertions(+), 60 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 471c43ea..e3c29292 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -15,6 +15,9 @@ #define GEOS_NAME_SIZE 16 #define GEOS_MAX_DIR_SECTORS 40 #define GEOS_ERROR_SIZE 80 +#define GEOS_DATA_PER_BLOCK (GEOS_SECTOR_SIZE - 2) +#define GEOS_DRIVER_DATA_BLOCKS ((sizeof TRMouse128Code + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK) +#define GEOS_DRIVER_BLOCKS (GEOS_DRIVER_DATA_BLOCKS + 1) #define CBM_NAME_PAD 0xa0 #define CBM_TYPE_MASK 0x07 @@ -46,6 +49,7 @@ struct GeosFormat uint8_t Reserved[2]; uint32_t Size; bool FixedSectors; // D81: 40 per track; else the 1541's zones, repeated on a 1571's second side + bool TwoSided; }; struct GeosDate { uint8_t Year, Month, Day, Hour, Minute; }; @@ -62,9 +66,9 @@ struct GeosDisk static const struct GeosFormat GeosFormats[] = { - { "D64", 35, 18, 1, 18, { 18, 0 }, 174848, false }, - { "D71", 70, 18, 1, 18, { 18, 53 }, 349696, false }, - { "D81", 80, 40, 3, 39, { 40, 0 }, 819200, true }, + { "D64", 35, 18, 1, 18, { 18, 0 }, 174848, false, false }, + { "D71", 70, 18, 1, 18, { 18, 53 }, 349696, false, true }, + { "D81", 80, 40, 3, 39, { 40, 0 }, 819200, true, false }, }; static bool GeosFail(struct GeosDisk *Disk, const char *Message) @@ -151,31 +155,66 @@ static uint8_t GeosCountFree(const struct GeosFormat *Format, uint8_t Track, con return Free; } +// A track's BAM blocks as read; Count and Bits point into them. +struct GeosTrackBam +{ + struct GeosPlace CountAt, BitmapAt; + bool Shared; + uint8_t CountBlock[GEOS_SECTOR_SIZE], BitmapBlock[GEOS_SECTOR_SIZE]; + uint8_t *Count, *Bits, Free; + int FirstFree; +}; + +static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, struct GeosTrackBam *Bam) +{ + GeosBamPlaces(Disk->Format, Track, &Bam->CountAt, &Bam->BitmapAt); + Bam->Shared = Bam->CountAt.Track == Bam->BitmapAt.Track && Bam->CountAt.Sector == Bam->BitmapAt.Sector; + if (!GeosReadTS(Disk, Bam->CountAt.Track, Bam->CountAt.Sector, Bam->CountBlock)) return false; + if (!Bam->Shared && !GeosReadTS(Disk, Bam->BitmapAt.Track, Bam->BitmapAt.Sector, Bam->BitmapBlock)) return false; + Bam->Count = Bam->CountBlock + Bam->CountAt.Byte; + Bam->Bits = (Bam->Shared ? Bam->CountBlock : Bam->BitmapBlock) + Bam->BitmapAt.Byte; + + Bam->Free = GeosCountFree(Disk->Format, Track, Bam->Bits, &Bam->FirstFree); + return true; +} + +static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct GeosTrackBam *Bam) +{ + if (*Bam->Count >= Bam->Free) return true; + snprintf(Disk->Error, sizeof Disk->Error, "the BAM count for track %u disagrees with its bitmap", Track); + return false; +} + // Marks the first free sector of a track in use; *Sector is -1 when the track has none. static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) { - struct GeosPlace Count, Bitmap; - uint8_t CountBlock[GEOS_SECTOR_SIZE], BitmapBlock[GEOS_SECTOR_SIZE]; - bool Shared; + struct GeosTrackBam Bam; + + if (!GeosReadTrackBam(Disk, Track, &Bam) || !GeosCheckCount(Disk, Track, &Bam)) return false; + *Sector = Bam.FirstFree; + if (*Sector < 0) return true; - GeosBamPlaces(Disk->Format, Track, &Count, &Bitmap); - Shared = Count.Track == Bitmap.Track && Count.Sector == Bitmap.Sector; - if (!GeosReadTS(Disk, Count.Track, Count.Sector, CountBlock)) return false; - if (!Shared && !GeosReadTS(Disk, Bitmap.Track, Bitmap.Sector, BitmapBlock)) return false; - uint8_t *Bits = (Shared ? CountBlock : BitmapBlock) + Bitmap.Byte; + Bam.Bits[*Sector / 8] &= ~(1 << (*Sector % 8)); + (*Bam.Count)--; + if (!Bam.Shared && !GeosWriteTS(Disk, Bam.BitmapAt.Track, Bam.BitmapAt.Sector, Bam.BitmapBlock)) return false; + return GeosWriteTS(Disk, Bam.CountAt.Track, Bam.CountAt.Sector, Bam.CountBlock); +} + +// Refuses, before anything is written, a disk that GeosAllocate would run out on partway. +// Reads the tracks GeosAllocate would visit, in its order, so the refusals match trmouse.py's. +static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) +{ + struct GeosTrackBam Bam; + unsigned Found = 0; - uint8_t Free = GeosCountFree(Disk->Format, Track, Bits, Sector); - if (CountBlock[Count.Byte] < Free) + for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) { - snprintf(Disk->Error, sizeof Disk->Error, "the BAM count for track %u disagrees with its bitmap", Track); - return false; + if (GeosIsReserved(Disk->Format, Track)) continue; + if (!GeosReadTrackBam(Disk, Track, &Bam) || !GeosCheckCount(Disk, Track, &Bam)) return false; + Found += Bam.Free; + if (Found >= Needed) return true; } - if (*Sector < 0) return true; - - Bits[*Sector / 8] &= ~(1 << (*Sector % 8)); - CountBlock[Count.Byte]--; - if (!Shared && !GeosWriteTS(Disk, Bitmap.Track, Bitmap.Sector, BitmapBlock)) return false; - return GeosWriteTS(Disk, Count.Track, Count.Sector, CountBlock); + return GeosFail(Disk, "the disk is full"); } static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) @@ -228,28 +267,32 @@ static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Lengt return true; } -// Chains one more directory sector, as DOS does when a directory fills. -static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +// Refuses, before anything is written, a directory that GeosGrowDirectory could not grow. +static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length) { const struct GeosFormat *Format = Disk->Format; - struct GeosPlace Count, Bitmap; - uint8_t Block[GEOS_SECTOR_SIZE]; - int New; + struct GeosTrackBam Bam; - GeosBamPlaces(Format, Format->DirTrack, &Count, &Bitmap); - if (!GeosReadTS(Disk, Bitmap.Track, Bitmap.Sector, Block)) return false; - for (int i = 0; i < *Length; i++) + if (!GeosReadTrackBam(Disk, Format->DirTrack, &Bam)) return false; + for (int i = 0; i < Length; i++) { - if (!GeosSectorIsFree(Block + Bitmap.Byte, Chain[i].Sector)) continue; + if (!GeosSectorIsFree(Bam.Bits, Chain[i].Sector)) continue; snprintf(Disk->Error, sizeof Disk->Error, "the BAM shows directory sector %u/%u free", Chain[i].Track, Chain[i].Sector); return false; } + if (!GeosCheckCount(Disk, Format->DirTrack, &Bam)) return false; + if (Bam.FirstFree >= 0 && GeosIsDirSector(Format, Format->DirTrack, Bam.FirstFree)) return true; + return GeosFail(Disk, "the directory is full"); +} + +// Chains one more directory sector, as DOS does when a directory fills, once GeosCheckDirectoryRoom has passed. +static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +{ + const struct GeosFormat *Format = Disk->Format; + uint8_t Block[GEOS_SECTOR_SIZE]; + int New; if (!GeosTake(Disk, Format->DirTrack, &New)) return false; - if (New < 0 || !GeosIsDirSector(Format, Format->DirTrack, New) || *Length == GEOS_MAX_DIR_SECTORS) - { - return GeosFail(Disk, "the directory is full"); - } struct GeosTS Last = Chain[*Length - 1]; if (!GeosReadTS(Disk, Last.Track, Last.Sector, Block)) return false; @@ -266,18 +309,16 @@ static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * // Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) { - const uint16_t DataPerBlock = GEOS_SECTOR_SIZE - 2; - uint16_t DataBlocks = (sizeof TRMouse128Code + DataPerBlock - 1) / DataPerBlock; - struct GeosTS Here, Next; + struct GeosTS Here, Next = { 0, 0 }; uint8_t Block[GEOS_SECTOR_SIZE]; if (!GeosAllocate(Disk, &Here)) return false; *First = Here; - for (uint16_t i = 0; i < DataBlocks; i++) + for (uint16_t i = 0; i < GEOS_DRIVER_DATA_BLOCKS; i++) { - uint16_t Start = i * DataPerBlock, Length = sizeof TRMouse128Code - Start; - if (Length > DataPerBlock) Length = DataPerBlock; - bool Last = i + 1 == DataBlocks; + uint16_t Start = i * GEOS_DATA_PER_BLOCK, Length = sizeof TRMouse128Code - Start; + if (Length > GEOS_DATA_PER_BLOCK) Length = GEOS_DATA_PER_BLOCK; + bool Last = i + 1 == GEOS_DRIVER_DATA_BLOCKS; if (!Last && !GeosAllocate(Disk, &Next)) return false; memset(Block, 0, sizeof Block); @@ -289,7 +330,7 @@ static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct } if (!GeosAllocate(Disk, Info)) return false; - *Blocks = DataBlocks + 1; + *Blocks = GEOS_DRIVER_BLOCKS; return GeosWriteTS(Disk, Info->Track, Info->Sector, TRMouse128Info); } @@ -392,7 +433,7 @@ static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, return GeosWriteTS(Disk, Where.Track, Where.Sector, Block); } -const struct GeosFormat *GeosFormatForSize(uint32_t Size) +static inline const struct GeosFormat *GeosFormatForSize(uint32_t Size) { for (size_t i = 0; i < sizeof GeosFormats / sizeof GeosFormats[0]; i++) { @@ -401,38 +442,44 @@ const struct GeosFormat *GeosFormatForSize(uint32_t Size) return NULL; } -// Checks the image and returns whether it already holds the driver. -bool GeosHasMouseDriver(struct GeosDisk *Disk, bool *HasMouse) +static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length, struct GeosDirScan *Scan) { - struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; - struct GeosDirScan Scan; uint8_t Header[GEOS_SECTOR_SIZE]; - int Length; if (!Disk->Format) return GeosFail(Disk, "not a D64, D71 or D81 without error bytes"); if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Header)) return false; - if (Disk->Format->Size == 349696 && !(Header[3] & D71_DOUBLE_SIDED)) + if (Disk->Format->TwoSided && !(Header[3] & D71_DOUBLE_SIDED)) { return GeosFail(Disk, "this D71 is formatted single-sided"); } - if (!GeosDirChain(Disk, Chain, &Length)) return false; - if (!GeosScanDirectory(Disk, Chain, Length, &Scan)) return false; + if (!GeosDirChain(Disk, Chain, Length)) return false; + return GeosScanDirectory(Disk, Chain, *Length, Scan); +} + +// Checks the image and returns whether it already holds the driver. +static inline bool GeosHasMouseDriver(struct GeosDisk *Disk, bool *HasMouse) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; + struct GeosDirScan Scan; + int Length; + + if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; *HasMouse = Scan.HasMouse; return true; } // Writes the driver ahead of every other input driver, since GEOS 128 loads the first one in the directory. -bool GeosInstallMouseDriver(struct GeosDisk *Disk, const struct GeosDate *Date) +// Every refusal but a failed block read or write comes before the first write, leaving the image as it was. +static inline bool GeosInstallMouseDriver(struct GeosDisk *Disk, const struct GeosDate *Date) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; struct GeosDirScan Scan; - bool HasMouse; int Length; - if (!GeosHasMouseDriver(Disk, &HasMouse)) return false; - if (HasMouse) return GeosFail(Disk, "TR USB MOUSE is already on this disk"); - if (!GeosDirChain(Disk, Chain, &Length)) return false; - if (!GeosScanDirectory(Disk, Chain, Length, &Scan)) return false; + if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; + if (Scan.HasMouse) return GeosFail(Disk, "TR USB MOUSE is already on this disk"); + if (!Scan.HasFree && !GeosCheckDirectoryRoom(Disk, Chain, Length)) return false; + if (!GeosCheckRoom(Disk, GEOS_DRIVER_BLOCKS)) return false; if (!Scan.HasFree) { if (!GeosGrowDirectory(Disk, Chain, &Length)) return false; diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js index f1a79222..9148c6e3 100644 --- a/Source/Teensy/tests/geos-disk-patch.test.js +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -122,6 +122,14 @@ function fullDirectory(kind) { return disk; } +function withOneDataSector(disk) { + const reserved = disk.kind === 'd71' ? [18, 53] : [disk.format.dirTrack]; + for (let track = 1; track <= disk.format.tracks; track++) { + if (!reserved.includes(track)) setTrackFree(disk, track, track === 1 ? [0] : []); + } + return disk; +} + const PATCHED_CASES = { 'a blank boot disk': blankDisk, 'a disk whose first input driver comes earlier': withDriver, @@ -144,6 +152,9 @@ const REFUSED_CASES = { disk.image[offset(disk.format, disk.format.dirTrack, 0)] = 1; return disk; }, + 'a disk with room for the data block but not the info block': (kind) => withOneDataSector(blankDisk(kind)), + 'a full directory on a disk with room for the data block but not the info block': (kind) => + withOneDataSector(fullDirectory(kind)), 'a full directory whose BAM frees its sector': (kind) => { const disk = fullDirectory(kind); const free = allSectors(disk, disk.format.dirTrack).filter((s) => s >= disk.format.firstDir); @@ -199,6 +210,8 @@ test('the firmware patch matches trmouse.py', { skip }, async (t) => { for (const [label, make] of Object.entries(REFUSED_CASES)) { await t.test(`${kind}: refuses ${label}`, () => { const { python, firmware } = patchBoth(dir, `${kind}-${label}`, make(kind).image); + const REFUSED_AFTER_WRITING = 3; + assert.notEqual(firmware.status, REFUSED_AFTER_WRITING, `the refusal changed the image: ${firmware.stderr}`); assert.equal(firmware.status, 1, `the firmware patched it: ${firmware.stderr}`); assert.notEqual(python.status, 0, 'trmouse.py patched it'); assert.equal(firmware.stderr.trim(), python.stderr.trim()); diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c index a6d04303..2b91c373 100644 --- a/Source/Teensy/tests/geos_disk_patch_harness.c +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT // geos_disk_patch_harness IN OUT: the firmware's GEOS boot-disk patch, run on an image file. -// Exits 0 after writing OUT, 1 with the patcher's refusal on stderr. +// Exits 0 after writing OUT, 1 with the patcher's refusal on stderr, 3 if a refusal changed the image. #include #include "../GEOS/GeosDiskPatch.h" #define MAX_IMAGE 819200 -static uint8_t Image[MAX_IMAGE]; +// One byte past the largest image, so an oversized file reads as a size no format has +static uint8_t Image[MAX_IMAGE + 1], Original[MAX_IMAGE + 1]; static bool ReadBlock(void *Context, uint32_t Offset, uint8_t *Block) { @@ -33,6 +34,7 @@ int main(int argc, char **argv) if (!In) return 2; Size = fread(Image, 1, sizeof Image, In); fclose(In); + memcpy(Original, Image, Size); Disk.Format = GeosFormatForSize(Size); Disk.Context = Image; @@ -41,7 +43,7 @@ int main(int argc, char **argv) if (!GeosInstallMouseDriver(&Disk, &Date)) { fprintf(stderr, "%s\n", Disk.Error); - return 1; + return memcmp(Original, Image, Size) ? 3 : 1; } Out = fopen(argv[2], "wb"); From a3e2cf0e98f3a44dfd8142ee8bc08f4dbd469c61 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:29:59 -0500 Subject: [PATCH 028/100] firmware: the GEOS directory grow refuses a sector outside the directory again, and a test holds the full-directory refusal to before the first write --- Source/Teensy/GEOS/GeosDiskPatch.h | 4 ++++ Source/Teensy/tests/geos-disk-patch.test.js | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index e3c29292..787e9bdd 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -293,6 +293,10 @@ static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * int New; if (!GeosTake(Disk, Format->DirTrack, &New)) return false; + if (New < 0 || !GeosIsDirSector(Format, Format->DirTrack, New) || *Length == GEOS_MAX_DIR_SECTORS) + { + return GeosFail(Disk, "the directory is full"); + } struct GeosTS Last = Chain[*Length - 1]; if (!GeosReadTS(Disk, Last.Track, Last.Sector, Block)) return false; diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js index 9148c6e3..389ba44c 100644 --- a/Source/Teensy/tests/geos-disk-patch.test.js +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -25,6 +25,7 @@ const INPUT_128 = 15; const MOUSE_NAME = 'TR USB MOUSE'; const DATE_IN_SLOT = 25; const DATE_BYTES = 5; +const REFUSED_AFTER_WRITING = 3; const zoneSectors = (track) => { const zone = (track - 1) % 35 + 1; @@ -155,6 +156,11 @@ const REFUSED_CASES = { 'a disk with room for the data block but not the info block': (kind) => withOneDataSector(blankDisk(kind)), 'a full directory on a disk with room for the data block but not the info block': (kind) => withOneDataSector(fullDirectory(kind)), + 'a full directory whose track has only a system sector free': (kind) => { + const disk = fullDirectory(kind); + setTrackFree(disk, disk.format.dirTrack, [0]); + return disk; + }, 'a full directory whose BAM frees its sector': (kind) => { const disk = fullDirectory(kind); const free = allSectors(disk, disk.format.dirTrack).filter((s) => s >= disk.format.firstDir); @@ -210,7 +216,6 @@ test('the firmware patch matches trmouse.py', { skip }, async (t) => { for (const [label, make] of Object.entries(REFUSED_CASES)) { await t.test(`${kind}: refuses ${label}`, () => { const { python, firmware } = patchBoth(dir, `${kind}-${label}`, make(kind).image); - const REFUSED_AFTER_WRITING = 3; assert.notEqual(firmware.status, REFUSED_AFTER_WRITING, `the refusal changed the image: ${firmware.stderr}`); assert.equal(firmware.status, 1, `the firmware patched it: ${firmware.stderr}`); assert.notEqual(python.status, 0, 'trmouse.py patched it'); From f35c3be7c763ad6fcc77c1fed9ab8fb23a5ada51 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:32:07 -0500 Subject: [PATCH 029/100] firmware: the GEOS directory grow checks for room before taking a sector, so its own refusal leaves the image as it was --- Source/Teensy/GEOS/GeosDiskPatch.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 787e9bdd..56773d6e 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -285,18 +285,15 @@ static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *C return GeosFail(Disk, "the directory is full"); } -// Chains one more directory sector, as DOS does when a directory fills, once GeosCheckDirectoryRoom has passed. +// Chains one more directory sector, as DOS does when a directory fills. static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) { const struct GeosFormat *Format = Disk->Format; uint8_t Block[GEOS_SECTOR_SIZE]; int New; - if (!GeosTake(Disk, Format->DirTrack, &New)) return false; - if (New < 0 || !GeosIsDirSector(Format, Format->DirTrack, New) || *Length == GEOS_MAX_DIR_SECTORS) - { - return GeosFail(Disk, "the directory is full"); - } + if (*Length == GEOS_MAX_DIR_SECTORS) return GeosFail(Disk, "the directory is full"); + if (!GeosCheckDirectoryRoom(Disk, Chain, *Length) || !GeosTake(Disk, Format->DirTrack, &New)) return false; struct GeosTS Last = Chain[*Length - 1]; if (!GeosReadTS(Disk, Last.Track, Last.Sector, Block)) return false; From 5ef62c981ef2d51373381752ed2ab6d0cf4cc60c Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:35:49 -0500 Subject: [PATCH 030/100] firmware: the GEOS disk patch takes its DeskTop and driver from a target, and tells a boot disk from a data disk GEOS 64 needs only its own driver and a second target to use the same patch. --- Source/Teensy/GEOS/GeosDiskPatch.h | 73 ++++++++++++------- Source/Teensy/GEOS/GeosTargets.h | 19 +++++ Source/Teensy/tests/geos-disk-patch.test.js | 18 +++++ Source/Teensy/tests/geos_disk_patch_harness.c | 24 +++++- 4 files changed, 105 insertions(+), 29 deletions(-) create mode 100644 Source/Teensy/GEOS/GeosTargets.h diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 56773d6e..42fc868f 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Puts the TR USB mouse driver on a GEOS 128 boot disk image, block by block. +// Puts a TR input driver on a GEOS boot disk image, block by block. // Mirrors tools/geos/trmouse.py install(), which the host test holds it to byte for byte. #pragma once #include @@ -7,8 +7,6 @@ #include #include -#include "TRMouse128.h" - #define GEOS_SECTOR_SIZE 256 #define GEOS_DIR_ENTRY_SIZE 32 #define GEOS_ENTRIES_PER_DIR (GEOS_SECTOR_SIZE / GEOS_DIR_ENTRY_SIZE) @@ -16,14 +14,11 @@ #define GEOS_MAX_DIR_SECTORS 40 #define GEOS_ERROR_SIZE 80 #define GEOS_DATA_PER_BLOCK (GEOS_SECTOR_SIZE - 2) -#define GEOS_DRIVER_DATA_BLOCKS ((sizeof TRMouse128Code + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK) -#define GEOS_DRIVER_BLOCKS (GEOS_DRIVER_DATA_BLOCKS + 1) #define CBM_NAME_PAD 0xa0 #define CBM_TYPE_MASK 0x07 #define CBM_USR_CLOSED 0x83 #define GEOS_SEQUENTIAL 0 -#define GEOS_INPUT_128 15 #define D71_DOUBLE_SIDED 0x80 // Byte offsets inside a 32-byte directory slot (the first two hold the sector link) @@ -54,10 +49,22 @@ struct GeosFormat struct GeosDate { uint8_t Year, Month, Day, Hour, Minute; }; +// A GEOS version the TR sets up: its boot disk's DeskTop and the input driver the TR adds +struct GeosTarget +{ + const char *DeskTopName, *DriverName; + const uint8_t *DriverCode, *DriverInfo; + uint16_t DriverSize; + uint8_t InputType; +}; + +struct GeosBootDisk { bool HasDeskTop, HasDriver; }; + // A boot disk image reached through 256-byte block reads and writes at byte offsets. struct GeosDisk { const struct GeosFormat *Format; + const struct GeosTarget *Target; void *Context; bool (*Read)(void *Context, uint32_t Offset, uint8_t *Block); bool (*Write)(void *Context, uint32_t Offset, const uint8_t *Block); @@ -307,46 +314,53 @@ static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * return GeosWriteTS(Disk, Format->DirTrack, New, Block); } +static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) +{ + return (Target->DriverSize + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK; +} + // Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) { + const struct GeosTarget *Target = Disk->Target; + const uint16_t DataBlocks = GeosDriverDataBlocks(Target); struct GeosTS Here, Next = { 0, 0 }; uint8_t Block[GEOS_SECTOR_SIZE]; if (!GeosAllocate(Disk, &Here)) return false; *First = Here; - for (uint16_t i = 0; i < GEOS_DRIVER_DATA_BLOCKS; i++) + for (uint16_t i = 0; i < DataBlocks; i++) { - uint16_t Start = i * GEOS_DATA_PER_BLOCK, Length = sizeof TRMouse128Code - Start; + 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 == GEOS_DRIVER_DATA_BLOCKS; + 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, TRMouse128Code + Start, Length); + memcpy(Block + 2, Target->DriverCode + Start, Length); if (!GeosWriteTS(Disk, Here.Track, Here.Sector, Block)) return false; Here = Next; } if (!GeosAllocate(Disk, Info)) return false; - *Blocks = GEOS_DRIVER_BLOCKS; - return GeosWriteTS(Disk, Info->Track, Info->Sector, TRMouse128Info); + *Blocks = DataBlocks + 1; + return GeosWriteTS(Disk, Info->Track, Info->Sector, Target->DriverInfo); } -static void GeosBuildEntry(uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) +static void GeosBuildEntry(const struct GeosTarget *Target, uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) { memset(Slot + SlotType, 0, GEOS_DIR_ENTRY_SIZE - SlotType); Slot[SlotType] = CBM_USR_CLOSED; Slot[SlotFirst] = First.Track; Slot[SlotFirst + 1] = First.Sector; memset(Slot + SlotName, CBM_NAME_PAD, GEOS_NAME_SIZE); - memcpy(Slot + SlotName, TRMouse128Name, strlen(TRMouse128Name)); + memcpy(Slot + SlotName, Target->DriverName, strlen(Target->DriverName)); Slot[SlotInfo] = Info.Track; Slot[SlotInfo + 1] = Info.Sector; Slot[SlotStructure] = GEOS_SEQUENTIAL; - Slot[SlotGeosType] = GEOS_INPUT_128; + Slot[SlotGeosType] = Target->InputType; Slot[SlotDate] = Date->Year % 100; Slot[SlotDate + 1] = Date->Month; Slot[SlotDate + 2] = Date->Day; @@ -374,11 +388,12 @@ struct GeosSlotRef { int Sector, Slot; }; struct GeosDirScan { struct GeosSlotRef Free, FirstDriver; - bool HasFree, HasDriver, HasMouse; + bool HasFree, HasDriver, HasDeskTop, HasOurs; }; static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) { + const struct GeosTarget *Target = Disk->Target; uint8_t Block[GEOS_SECTOR_SIZE]; memset(Scan, 0, sizeof *Scan); @@ -393,8 +408,9 @@ static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, if (!Slot[SlotType] && !Scan->HasFree) { Scan->Free = Here; Scan->HasFree = true; } if (!InUse) continue; - if (GeosSlotNamed(Slot, TRMouse128Name)) Scan->HasMouse = true; - if (Slot[SlotGeosType] == GEOS_INPUT_128 && !Scan->HasDriver) { Scan->FirstDriver = Here; Scan->HasDriver = true; } + if (GeosSlotNamed(Slot, Target->DriverName)) Scan->HasOurs = true; + if (GeosSlotNamed(Slot, Target->DeskTopName)) Scan->HasDeskTop = true; + if (Slot[SlotGeosType] == Target->InputType && !Scan->HasDriver) { Scan->FirstDriver = Here; Scan->HasDriver = true; } } } return true; @@ -430,7 +446,7 @@ static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, if (!GeosWriteDriver(Disk, &First, &Info, &Blocks)) return false; if (!GeosReadTS(Disk, Where.Track, Where.Sector, Block)) return false; - GeosBuildEntry(Block + Slot * GEOS_DIR_ENTRY_SIZE, First, Info, Blocks, Date); + GeosBuildEntry(Disk->Target, Block + Slot * GEOS_DIR_ENTRY_SIZE, First, Info, Blocks, Date); return GeosWriteTS(Disk, Where.Track, Where.Sector, Block); } @@ -457,30 +473,35 @@ static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * return GeosScanDirectory(Disk, Chain, *Length, Scan); } -// Checks the image and returns whether it already holds the driver. -static inline bool GeosHasMouseDriver(struct GeosDisk *Disk, bool *HasMouse) +// Checks the image and says whether it is the target's boot disk and already holds the TR driver. +static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; struct GeosDirScan Scan; int Length; if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; - *HasMouse = Scan.HasMouse; + Found->HasDeskTop = Scan.HasDeskTop; + Found->HasDriver = Scan.HasOurs; return true; } -// Writes the driver ahead of every other input driver, since GEOS 128 loads the first one in the directory. +// Writes the driver ahead of every other input driver of its type: GEOS 128 boots with the first one in the directory. // Every refusal but a failed block read or write comes before the first write, leaving the image as it was. -static inline bool GeosInstallMouseDriver(struct GeosDisk *Disk, const struct GeosDate *Date) +static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDate *Date) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; struct GeosDirScan Scan; int Length; if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; - if (Scan.HasMouse) return GeosFail(Disk, "TR USB MOUSE is already on this disk"); + if (Scan.HasOurs) + { + snprintf(Disk->Error, sizeof Disk->Error, "%s is already on this disk", Disk->Target->DriverName); + return false; + } if (!Scan.HasFree && !GeosCheckDirectoryRoom(Disk, Chain, Length)) return false; - if (!GeosCheckRoom(Disk, GEOS_DRIVER_BLOCKS)) return false; + if (!GeosCheckRoom(Disk, GeosDriverDataBlocks(Disk->Target) + 1u)) return false; if (!Scan.HasFree) { if (!GeosGrowDirectory(Disk, Chain, &Length)) return false; diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h new file mode 100644 index 00000000..144985eb --- /dev/null +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// The GEOS versions the TR sets up from a boot disk image the user puts on the SD card. +#pragma once +#include "GeosDiskPatch.h" +#include "TRMouse128.h" + +#define GEOS_INPUT_128 15 + +struct GeosLauncher +{ + const char *Title, *Folder; + struct GeosTarget Target; +}; + +static const struct GeosLauncher Geos128Launcher = +{ + "GEOS 128", "/GEOS128", + { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128 }, +}; diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js index 389ba44c..77d2c93b 100644 --- a/Source/Teensy/tests/geos-disk-patch.test.js +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -224,6 +224,24 @@ test('the firmware patch matches trmouse.py', { skip }, async (t) => { } } + for (const kind of Object.keys(FORMATS)) { + await t.test(`${kind}: inspect tells a boot disk from a data disk, and sees the driver once added`, () => { + const inspect = (label, image) => { + const file = path.join(dir, `${kind}-inspect-${label}`); + fs.writeFileSync(file, image); + const result = run(path.join(dir, 'harness'), ['--inspect', file]); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + const bootDisk = withDriver(kind); + const { firmware } = patchBoth(dir, `${kind}-inspect-patched`, bootDisk.image); + + assert.equal(inspect('data', blankDisk(kind).image), 'desktop=0 driver=0'); + assert.equal(inspect('boot', bootDisk.image), 'desktop=1 driver=0'); + assert.equal(inspect('patched', fs.readFileSync(firmware.out)), 'desktop=1 driver=1'); + }); + } + await t.test('d71: refuses a single-sided disk', () => { const disk = blankDisk('d71'); disk.image[offset(disk.format, 18, 0) + 3] = 0; diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c index 2b91c373..6d369821 100644 --- a/Source/Teensy/tests/geos_disk_patch_harness.c +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT // geos_disk_patch_harness IN OUT: the firmware's GEOS boot-disk patch, run on an image file. // Exits 0 after writing OUT, 1 with the patcher's refusal on stderr, 3 if a refusal changed the image. +// geos_disk_patch_harness --inspect IN: prints what GeosInspect found, or exits 1 with its refusal. #include -#include "../GEOS/GeosDiskPatch.h" +#include "../GEOS/GeosTargets.h" #define MAX_IMAGE 819200 @@ -22,25 +23,42 @@ static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) return true; } +static int Inspect(struct GeosDisk *Disk) +{ + struct GeosBootDisk Found; + + if (!GeosInspect(Disk, &Found)) + { + fprintf(stderr, "%s\n", Disk->Error); + return 1; + } + printf("desktop=%d driver=%d\n", Found.HasDeskTop, Found.HasDriver); + return 0; +} + int main(int argc, char **argv) { const struct GeosDate Date = { 26, 10, 16, 12, 0 }; struct GeosDisk Disk = { 0 }; FILE *In, *Out; size_t Size; + bool Inspecting; if (argc != 3) return 2; - In = fopen(argv[1], "rb"); + Inspecting = !strcmp(argv[1], "--inspect"); + In = fopen(Inspecting ? argv[2] : argv[1], "rb"); if (!In) return 2; Size = fread(Image, 1, sizeof Image, In); fclose(In); memcpy(Original, Image, Size); Disk.Format = GeosFormatForSize(Size); + Disk.Target = &Geos128Launcher.Target; Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; - if (!GeosInstallMouseDriver(&Disk, &Date)) + if (Inspecting) return Inspect(&Disk); + if (!GeosInstallDriver(&Disk, &Date)) { fprintf(stderr, "%s\n", Disk.Error); return memcmp(Original, Image, Size) ? 3 : 1; From 2bdf7faba6cb9a3ba9403d09f0fa16c87ac27f8b Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:38:10 -0500 Subject: [PATCH 031/100] firmware: the GEOS disk patch refuses a target whose names or driver cannot fit, and counts the driver's blocks in one place --- Source/Teensy/GEOS/GeosDiskPatch.h | 25 +++++++++++++++------ Source/Teensy/tests/geos-disk-patch.test.js | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 42fc868f..181dff72 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -319,6 +319,11 @@ static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) return (Target->DriverSize + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK; } +static uint16_t GeosDriverBlocks(const struct GeosTarget *Target) +{ + return GeosDriverDataBlocks(Target) + 1; +} + // Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) { @@ -345,7 +350,7 @@ static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct } if (!GeosAllocate(Disk, Info)) return false; - *Blocks = DataBlocks + 1; + *Blocks = GeosDriverBlocks(Target); return GeosWriteTS(Disk, Info->Track, Info->Sector, Target->DriverInfo); } @@ -387,8 +392,8 @@ struct GeosSlotRef { int Sector, Slot; }; struct GeosDirScan { - struct GeosSlotRef Free, FirstDriver; - bool HasFree, HasDriver, HasDeskTop, HasOurs; + struct GeosSlotRef Free, FirstInputDriver; + bool HasFree, HasInputDriver, HasDeskTop, HasOurs; }; static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) @@ -410,7 +415,7 @@ static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, if (!InUse) continue; if (GeosSlotNamed(Slot, Target->DriverName)) Scan->HasOurs = true; if (GeosSlotNamed(Slot, Target->DeskTopName)) Scan->HasDeskTop = true; - if (Slot[SlotGeosType] == Target->InputType && !Scan->HasDriver) { Scan->FirstDriver = Here; Scan->HasDriver = true; } + if (Slot[SlotGeosType] == Target->InputType && !Scan->HasInputDriver) { Scan->FirstInputDriver = Here; Scan->HasInputDriver = true; } } } return true; @@ -463,6 +468,12 @@ static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * { uint8_t Header[GEOS_SECTOR_SIZE]; + const struct GeosTarget *Target = Disk->Target; + + if (strlen(Target->DeskTopName) > GEOS_NAME_SIZE || strlen(Target->DriverName) > GEOS_NAME_SIZE || !Target->DriverSize) + { + return GeosFail(Disk, "the GEOS target's names or driver do not fit a directory entry"); + } if (!Disk->Format) return GeosFail(Disk, "not a D64, D71 or D81 without error bytes"); if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Header)) return false; if (Disk->Format->TwoSided && !(Header[3] & D71_DOUBLE_SIDED)) @@ -501,7 +512,7 @@ static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDat return false; } if (!Scan.HasFree && !GeosCheckDirectoryRoom(Disk, Chain, Length)) return false; - if (!GeosCheckRoom(Disk, GeosDriverDataBlocks(Disk->Target) + 1u)) return false; + if (!GeosCheckRoom(Disk, GeosDriverBlocks(Disk->Target))) return false; if (!Scan.HasFree) { if (!GeosGrowDirectory(Disk, Chain, &Length)) return false; @@ -509,6 +520,6 @@ static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDat } if (!GeosWriteEntry(Disk, Chain[Scan.Free.Sector], Scan.Free.Slot, Date)) return false; - if (!Scan.HasDriver || !GeosSlotBefore(Scan.FirstDriver, Scan.Free)) return true; - return GeosSwapEntries(Disk, Chain, Scan.FirstDriver, Scan.Free); + if (!Scan.HasInputDriver || !GeosSlotBefore(Scan.FirstInputDriver, Scan.Free)) return true; + return GeosSwapEntries(Disk, Chain, Scan.FirstInputDriver, Scan.Free); } diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js index 77d2c93b..e2a90ab5 100644 --- a/Source/Teensy/tests/geos-disk-patch.test.js +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -235,6 +235,7 @@ test('the firmware patch matches trmouse.py', { skip }, async (t) => { }; const bootDisk = withDriver(kind); const { firmware } = patchBoth(dir, `${kind}-inspect-patched`, bootDisk.image); + assert.equal(firmware.status, 0, firmware.stderr); assert.equal(inspect('data', blankDisk(kind).image), 'desktop=0 driver=0'); assert.equal(inspect('boot', bootDisk.image), 'desktop=1 driver=0'); From 95ca21e88134b6744b2c2472834f9448c5196462 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:39:53 -0500 Subject: [PATCH 032/100] firmware: a GEOS 128 menu item that finds the user's boot disk image in /GEOS128 and adds the mouse driver to a copy With no image there, it tells the user where to copy one. The original is left as it was. --- Source/C64/MainMenuCRT/source/Menu_Regs.i | 1 + Source/C64/MainMenuCRT/source/StringsMsgs.s | 1 + Source/Teensy/DriveDirLoad.ino | 6 + Source/Teensy/GeosLauncher.ino | 193 ++++++++++++++++++ Source/Teensy/MainMenuItems.h | 1 + Source/Teensy/MinimalBoot/Common/Menu_Regs.h | 1 + Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h | 2 +- Source/Teensy/Teensy.ino | 1 + 8 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 Source/Teensy/GeosLauncher.ino diff --git a/Source/C64/MainMenuCRT/source/Menu_Regs.i b/Source/C64/MainMenuCRT/source/Menu_Regs.i index 57d5b5df..fecb5b32 100644 --- a/Source/C64/MainMenuCRT/source/Menu_Regs.i +++ b/Source/C64/MainMenuCRT/source/Menu_Regs.i @@ -394,6 +394,7 @@ rtBinC128 = 18 rtFileREU = 19 rtFileTRH = 20 + rtGEOS = 21 ;63 max: rRegItemTypePlusIOH uses bit 7 (assigned IOH) and bit 6 (/VMS claim), ;and TblItemType's mult by 4 limits it to 63 as well diff --git a/Source/C64/MainMenuCRT/source/StringsMsgs.s b/Source/C64/MainMenuCRT/source/StringsMsgs.s index 44956125..3d4895d0 100644 --- a/Source/C64/MainMenuCRT/source/StringsMsgs.s +++ b/Source/C64/MainMenuCRT/source/StringsMsgs.s @@ -177,3 +177,4 @@ TblItemType: ;must match regItemTypes (rtNone, rtBin16k, etc) order! !tx 0, "128" ; rtBinC128 = 18 !tx 0, "REU" ; rtFileREU = 19 !tx 0, "TRH" ; rtFileTRH = 20 + !tx 0, "GEO" ; rtGEOS = 21 diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index 253a32ee..a60aaf63 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -248,6 +248,12 @@ FLASHMEM void HandleExecution() return; } + if (MenuSelCpy.ItemType == rtGEOS) + { + LaunchGeos((const struct GeosLauncher*)MenuSelCpy.Code_Image); + return; + } + SendMsgPrintfln(MenuSelCpy.Name); if (MenuSelCpy.ItemType == rtFileCrt) { //load the CRT into RAM diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino new file mode 100644 index 00000000..26e99ea8 --- /dev/null +++ b/Source/Teensy/GeosLauncher.ino @@ -0,0 +1,193 @@ +// A GEOS menu item: finds the user's boot disk image in the launcher's folder on the SD card and puts +// the TR input driver on a copy of it, leaving the original untouched. + +#include "GEOS/GeosTargets.h" + +#define GEOS_TEMP_NAME "PATCHING.TMP" +#define GEOS_COPY_TAG "-TR" +#define GEOS_EXT_LENGTH 4 +#define GEOS_MAX_IMAGES 8 + +static const char *const GeosImageExtensions[] = { ".d64", ".d71", ".d81" }; + +FLASHMEM static bool GeosFileRead(void *Context, uint32_t Offset, uint8_t *Block) +{ + File *Image = (File *)Context; + return Image->seek(Offset) && Image->read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; +} + +FLASHMEM static bool GeosFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) +{ + File *Image = (File *)Context; + return Image->seek(Offset) && Image->write(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; +} + +FLASHMEM static struct GeosDisk GeosDiskOn(const struct GeosLauncher *Launcher, File *Image) +{ + struct GeosDisk Disk = {}; + uint64_t Size = Image->size(); + + Disk.Format = Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL; + Disk.Target = &Launcher->Target; + Disk.Context = Image; + Disk.Read = GeosFileRead; + Disk.Write = GeosFileWrite; + return Disk; +} + +FLASHMEM static bool GeosHasImageExtension(const char *Name) +{ + size_t Length = strlen(Name); + + if (Length <= GEOS_EXT_LENGTH) return false; + for (const char *Extension : GeosImageExtensions) + { + if (!strcasecmp(Name + Length - GEOS_EXT_LENGTH, Extension)) return true; + } + return false; +} + +FLASHMEM static bool GeosIsPatchedCopy(const char *Name) +{ + size_t Base = strlen(Name) - GEOS_EXT_LENGTH, TagLength = strlen(GEOS_COPY_TAG); + return Base > TagLength && !strncasecmp(Name + Base - TagLength, GEOS_COPY_TAG, TagLength); +} + +FLASHMEM static void GeosPath(char *Path, size_t Size, const struct GeosLauncher *Launcher, const char *Name) +{ + snprintf(Path, Size, "%s/%s", Launcher->Folder, Name); +} + +FLASHMEM static void GeosCopyPath(char *Path, size_t Size, const struct GeosLauncher *Launcher, const char *Name) +{ + int Base = strlen(Name) - GEOS_EXT_LENGTH; + snprintf(Path, Size, "%s/%.*s" GEOS_COPY_TAG "%s", Launcher->Folder, Base, Name, Name + Base); +} + +// The originals in the folder, listed before anything is written to it +FLASHMEM static uint8_t GeosListImages(const struct GeosLauncher *Launcher, char Names[][MaxItemNameLength]) +{ + File Dir = SD.open(Launcher->Folder); + uint8_t Count = 0; + + if (!Dir) return 0; + for (File Entry = Dir.openNextFile(); Entry && Count < GEOS_MAX_IMAGES; Entry = Dir.openNextFile()) + { + const char *Name = Entry.name(); + if (Entry.isDirectory() || !GeosHasImageExtension(Name) || GeosIsPatchedCopy(Name)) continue; + if (strlen(Name) >= MaxItemNameLength) continue; + strcpy(Names[Count++], Name); + } + Dir.close(); + return Count; +} + +FLASHMEM static bool GeosInspectFile(const struct GeosLauncher *Launcher, const char *Path, struct GeosBootDisk *Found, + char *Error, size_t ErrorSize) +{ + File Image = SD.open(Path, FILE_READ); + struct GeosDisk Disk; + bool Inspected; + + if (!Image) return false; + Disk = GeosDiskOn(Launcher, &Image); + Inspected = GeosInspect(&Disk, Found); + if (!Inspected && Error) snprintf(Error, ErrorSize, "%s", Disk.Error); + Image.close(); + return Inspected; +} + +FLASHMEM static bool GeosCopyFile(const char *From, const char *To) +{ + File Source = SD.open(From, FILE_READ), Target; + uint8_t Buffer[GEOS_SECTOR_SIZE * 4]; + bool Copied = true; + int Length; + + SD.remove(To); + Target = SD.open(To, FILE_WRITE); + if (!Source || !Target) Copied = false; + while (Copied && (Length = Source.read(Buffer, sizeof Buffer)) > 0) + { + Copied = Target.write(Buffer, Length) == (size_t)Length; + } + Source.close(); + Target.close(); + return Copied; +} + +FLASHMEM static struct GeosDate GeosToday() +{ + return (struct GeosDate){ (uint8_t)(year() % 100), (uint8_t)month(), (uint8_t)day(), (uint8_t)hour(), (uint8_t)minute() }; +} + +FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const char *Path, char *Error, size_t ErrorSize) +{ + File Image = SD.open(Path, FILE_WRITE_BEGIN); + struct GeosDisk Disk; + struct GeosDate Today = GeosToday(); + bool Patched; + + if (!Image) return false; + Disk = GeosDiskOn(Launcher, &Image); + Patched = GeosInstallDriver(&Disk, &Today); + if (!Patched) snprintf(Error, ErrorSize, "%s", Disk.Error); + Image.close(); + return Patched; +} + +// Patches a temporary copy and renames it into place, so a failure never leaves a half-patched copy behind +FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Source, const char *Copy) +{ + char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; + + GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); + if (!GeosCopyFile(Source, Temp) || !GeosPatchFile(Launcher, Temp, Error, sizeof Error) || + (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) + { + SD.remove(Temp); + SendMsgPrintfln("Could not add the mouse driver:\r\n%s", Error); + return false; + } + return true; +} + +// Whether the image is a boot disk the TR can now start, saying what it found +FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name) +{ + char Source[MaxNamePathLength], Copy[MaxNamePathLength], Error[GEOS_ERROR_SIZE]; + struct GeosBootDisk Found; + + GeosPath(Source, sizeof Source, Launcher, Name); + GeosCopyPath(Copy, sizeof Copy, Launcher, Name); + if (!GeosInspectFile(Launcher, Source, &Found, Error, sizeof Error)) + { + SendMsgPrintfln("%s:\r\n%s", Name, Error); + return false; + } + if (!Found.HasDeskTop) return false; + + SendMsgPrintfln("%s boot disk:\r\n%s", Launcher->Title, Name); + if (Found.HasDriver) return true; + if (GeosInspectFile(Launcher, Copy, &Found, NULL, 0) && Found.HasDriver) return true; + if (!GeosPatchCopy(Launcher, Source, Copy)) return false; + SendMsgPrintfln("Added the TR USB mouse driver to\r\n%s", Copy); + return true; +} + +FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) +{ + char Names[GEOS_MAX_IMAGES][MaxItemNameLength]; + uint8_t Count, Ready = 0; + + if (!SDFullInit()) + { + SendMsgPrintfln("%s starts from your boot disk\r\nimage on the SD card. Insert the card.", Launcher->Title); + return; + } + Count = GeosListImages(Launcher, Names); + for (uint8_t i = 0; i < Count; i++) Ready += GeosSetUpImage(Launcher, Names[i]); + if (Ready) return; + SendMsgPrintfln("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again.", + Launcher->Title, Launcher->Folder); +} diff --git a/Source/Teensy/MainMenuItems.h b/Source/Teensy/MainMenuItems.h index 8d0ea642..f638ee5f 100644 --- a/Source/Teensy/MainMenuItems.h +++ b/Source/Teensy/MainMenuItems.h @@ -324,5 +324,6 @@ StructMenuItem TeensyROMMenu[] = /**7**/ rtDirectory, IOH_None , (char*)"/Utilities" , (uint8_t*)dirUtilities , sizeof(dirUtilities) , /* 8 */ rtDirectory, IOH_None , (char*)"/Text + PETSCII + Docs" , (uint8_t*)dirTEXT_PETSCII , sizeof(dirTEXT_PETSCII) , /**9**/ rtDirectory, IOH_None , (char*)"/TeensyROM Specific" , (uint8_t*)dirTeensyROMSpecific , sizeof(dirTeensyROMSpecific) , + /* 10*/ rtGEOS , IOH_None , (char*)"GEOS 128" , (uint8_t*)&Geos128Launcher , sizeof(Geos128Launcher) , }; diff --git a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h index a3c616da..9798d633 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -403,6 +403,7 @@ enum regItemTypes //synch with TblItemType rtBinC128 = 18, rtFileREU = 19, rtFileTRH = 20, + rtGEOS = 21, //63 max: rRegItemTypePlusIOH uses bit 7 (assigned IOH) and bit 6 (/VMS claim), //and TblItemType's mult by 4 limits it to 63 as well diff --git a/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h b/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h index 84078f17..a1de882c 100644 --- a/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h +++ b/Source/Teensy/TRMenuFiles/ROMs/TeensyROMC64.h @@ -388,7 +388,7 @@ static const unsigned char TeensyROMC64_bin[] = { 0xc9, 0xc4, 0x00, 0xcb, 0x4c, 0x41, 0x00, 0xc1, 0x52, 0x54, 0x00, 0xd4, 0x58, 0x54, 0x00, 0xd3, 0x45, 0x51, 0x00, 0x31, 0x36, 0x4b, 0x00, 0x38, 0xc8, 0x49, 0x00, 0x38, 0xcc, 0x4f, 0x00, 0x31, 0x32, 0x38, 0x00, 0xd2, - 0xc5, 0xd5, 0x00, 0xd4, 0xd2, 0xc8, 0x00, 0x00 + 0xc5, 0xd5, 0x00, 0xd4, 0xd2, 0xc8, 0x00, 0xc7, 0xc5, 0xcf, 0x00, 0x00 }; #endif /* TEENSYROMC64_BIN_H */ diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index ffe7b2df..a585279d 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -36,6 +36,7 @@ #include "MinimalBoot/Common/DMAControl.c" #include "MinimalBoot/Common/DriveDirLoad.h" #include "LoadedListing.h" +#include "GEOS/GeosTargets.h" #include "MainMenuItems.h" #ifdef VM_EXTENSIONS_ENABLED // Before IOHandlers.h: PollingHndlr_TeensyROM is where the record is shown. From 52799a52a93e0051b989f8613dc4b62b20c531e9 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:49:25 -0500 Subject: [PATCH 033/100] firmware: the GEOS launcher stamps the driver with the RTC's local date, keeps an unreadable -TR copy, skips ._ twins, and stops telling a user with a boot disk to copy one --- Source/Teensy/GeosLauncher.ino | 41 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 26e99ea8..a7d98d13 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -74,7 +74,7 @@ FLASHMEM static uint8_t GeosListImages(const struct GeosLauncher *Launcher, char for (File Entry = Dir.openNextFile(); Entry && Count < GEOS_MAX_IMAGES; Entry = Dir.openNextFile()) { const char *Name = Entry.name(); - if (Entry.isDirectory() || !GeosHasImageExtension(Name) || GeosIsPatchedCopy(Name)) continue; + if (Entry.isDirectory() || IsDotfile(Name) || !GeosHasImageExtension(Name) || GeosIsPatchedCopy(Name)) continue; if (strlen(Name) >= MaxItemNameLength) continue; strcpy(Names[Count++], Name); } @@ -89,7 +89,11 @@ FLASHMEM static bool GeosInspectFile(const struct GeosLauncher *Launcher, const struct GeosDisk Disk; bool Inspected; - if (!Image) return false; + if (!Image) + { + if (Error) snprintf(Error, ErrorSize, "the image could not be opened"); + return false; + } Disk = GeosDiskOn(Launcher, &Image); Inspected = GeosInspect(&Disk, Found); if (!Inspected && Error) snprintf(Error, ErrorSize, "%s", Disk.Error); @@ -101,6 +105,7 @@ FLASHMEM static bool GeosCopyFile(const char *From, const char *To) { File Source = SD.open(From, FILE_READ), Target; uint8_t Buffer[GEOS_SECTOR_SIZE * 4]; + uint64_t Total = 0; bool Copied = true; int Length; @@ -110,15 +115,22 @@ FLASHMEM static bool GeosCopyFile(const char *From, const char *To) while (Copied && (Length = Source.read(Buffer, sizeof Buffer)) > 0) { Copied = Target.write(Buffer, Length) == (size_t)Length; + Total += Length; } + Copied = Copied && Total == Source.size(); Source.close(); Target.close(); return Copied; } +// TimeLib's year() and friends run from boot with no sync provider set, so the RTC is read directly FLASHMEM static struct GeosDate GeosToday() { - return (struct GeosDate){ (uint8_t)(year() % 100), (uint8_t)month(), (uint8_t)day(), (uint8_t)hour(), (uint8_t)minute() }; + int8_t Timezone = (int8_t)IO1[rwRegTimezone]; + tmElements_t Now; + + breakTime(Teensy3Clock.get() + (int16_t)Timezone * 30 * 60, Now); + return (struct GeosDate){ (uint8_t)(tmYearToCalendar(Now.Year) % 100), Now.Month, Now.Day, Now.Hour, Now.Minute }; } FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const char *Path, char *Error, size_t ErrorSize) @@ -152,7 +164,7 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch return true; } -// Whether the image is a boot disk the TR can now start, saying what it found +// Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name) { char Source[MaxNamePathLength], Copy[MaxNamePathLength], Error[GEOS_ERROR_SIZE]; @@ -169,16 +181,25 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c SendMsgPrintfln("%s boot disk:\r\n%s", Launcher->Title, Name); if (Found.HasDriver) return true; - if (GeosInspectFile(Launcher, Copy, &Found, NULL, 0) && Found.HasDriver) return true; - if (!GeosPatchCopy(Launcher, Source, Copy)) return false; - SendMsgPrintfln("Added the TR USB mouse driver to\r\n%s", Copy); + if (SD.exists(Copy)) + { + // The copy is the disk GEOS saves the user's work to: one that cannot be read is left alone, not replaced + if (!GeosInspectFile(Launcher, Copy, &Found, Error, sizeof Error)) + { + SendMsgPrintfln("%s:\r\n%s", Copy, Error); + return true; + } + if (Found.HasDriver) return true; + } + if (GeosPatchCopy(Launcher, Source, Copy)) SendMsgPrintfln("Added the TR USB mouse driver to\r\n%s", Copy); return true; } FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) { char Names[GEOS_MAX_IMAGES][MaxItemNameLength]; - uint8_t Count, Ready = 0; + uint8_t Count; + bool BootDiskFound = false; if (!SDFullInit()) { @@ -186,8 +207,8 @@ FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) return; } Count = GeosListImages(Launcher, Names); - for (uint8_t i = 0; i < Count; i++) Ready += GeosSetUpImage(Launcher, Names[i]); - if (Ready) return; + for (uint8_t i = 0; i < Count; i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i]); + if (BootDiskFound) return; SendMsgPrintfln("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again.", Launcher->Title, Launcher->Folder); } From 42680427aa8c1af1e1ff0d5b9693cd2a41a5f4e6 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 18:50:27 -0500 Subject: [PATCH 034/100] firmware: the GEOS launcher names the disk that will boot, the -TR copy rather than the original --- Source/Teensy/GeosLauncher.ino | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index a7d98d13..08e5c115 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -164,11 +164,18 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch return true; } +FLASHMEM static bool GeosReady(const struct GeosLauncher *Launcher, const char *Name) +{ + SendMsgPrintfln("%s boot disk:\r\n%s", Launcher->Title, Name); + return true; +} + // Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name) { char Source[MaxNamePathLength], Copy[MaxNamePathLength], Error[GEOS_ERROR_SIZE]; struct GeosBootDisk Found; + const char *CopyName = Copy + strlen(Launcher->Folder) + 1; GeosPath(Source, sizeof Source, Launcher, Name); GeosCopyPath(Copy, sizeof Copy, Launcher, Name); @@ -178,21 +185,21 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return false; } if (!Found.HasDeskTop) return false; + if (Found.HasDriver) return GeosReady(Launcher, Name); - SendMsgPrintfln("%s boot disk:\r\n%s", Launcher->Title, Name); - if (Found.HasDriver) return true; if (SD.exists(Copy)) { // The copy is the disk GEOS saves the user's work to: one that cannot be read is left alone, not replaced if (!GeosInspectFile(Launcher, Copy, &Found, Error, sizeof Error)) { - SendMsgPrintfln("%s:\r\n%s", Copy, Error); + SendMsgPrintfln("%s:\r\n%s", CopyName, Error); return true; } - if (Found.HasDriver) return true; + if (Found.HasDriver) return GeosReady(Launcher, CopyName); } - if (GeosPatchCopy(Launcher, Source, Copy)) SendMsgPrintfln("Added the TR USB mouse driver to\r\n%s", Copy); - return true; + if (!GeosPatchCopy(Launcher, Source, Copy)) return true; + SendMsgPrintfln("Added the TR USB mouse driver to\r\na copy of %s", Name); + return GeosReady(Launcher, CopyName); } FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) From 03a63889c48344b4f3e39d23f3fcd19d38d2fdb6 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 19:02:33 -0500 Subject: [PATCH 035/100] firmware: the GEOS launcher names the image a failed patch was for, and builds the -TR name once instead of slicing it out of the path --- Source/Teensy/GeosLauncher.ino | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 08e5c115..9ca7726e 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -58,10 +58,10 @@ FLASHMEM static void GeosPath(char *Path, size_t Size, const struct GeosLauncher snprintf(Path, Size, "%s/%s", Launcher->Folder, Name); } -FLASHMEM static void GeosCopyPath(char *Path, size_t Size, const struct GeosLauncher *Launcher, const char *Name) +FLASHMEM static void GeosCopyName(char *CopyName, size_t Size, const char *Name) { int Base = strlen(Name) - GEOS_EXT_LENGTH; - snprintf(Path, Size, "%s/%.*s" GEOS_COPY_TAG "%s", Launcher->Folder, Base, Name, Name + Base); + snprintf(CopyName, Size, "%.*s" GEOS_COPY_TAG "%s", Base, Name, Name + Base); } // The originals in the folder, listed before anything is written to it @@ -149,7 +149,8 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch } // Patches a temporary copy and renames it into place, so a failure never leaves a half-patched copy behind -FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Source, const char *Copy) +FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, + const char *Copy) { char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; @@ -158,7 +159,7 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) { SD.remove(Temp); - SendMsgPrintfln("Could not add the mouse driver:\r\n%s", Error); + SendMsgPrintfln("Could not add the mouse driver to\r\na copy of %s:\r\n%s", Name, Error); return false; } return true; @@ -173,12 +174,13 @@ FLASHMEM static bool GeosReady(const struct GeosLauncher *Launcher, const char * // Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name) { - char Source[MaxNamePathLength], Copy[MaxNamePathLength], Error[GEOS_ERROR_SIZE]; + char Source[MaxNamePathLength], CopyName[MaxItemNameLength + sizeof GEOS_COPY_TAG], Copy[MaxNamePathLength]; + char Error[GEOS_ERROR_SIZE]; struct GeosBootDisk Found; - const char *CopyName = Copy + strlen(Launcher->Folder) + 1; GeosPath(Source, sizeof Source, Launcher, Name); - GeosCopyPath(Copy, sizeof Copy, Launcher, Name); + GeosCopyName(CopyName, sizeof CopyName, Name); + GeosPath(Copy, sizeof Copy, Launcher, CopyName); if (!GeosInspectFile(Launcher, Source, &Found, Error, sizeof Error)) { SendMsgPrintfln("%s:\r\n%s", Name, Error); @@ -197,7 +199,7 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c } if (Found.HasDriver) return GeosReady(Launcher, CopyName); } - if (!GeosPatchCopy(Launcher, Source, Copy)) return true; + if (!GeosPatchCopy(Launcher, Name, Source, Copy)) return true; SendMsgPrintfln("Added the TR USB mouse driver to\r\na copy of %s", Name); return GeosReady(Launcher, CopyName); } From b7d1933b8b36fe318ff8a424f2bc96894f5bc12f Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 12:42:02 -0500 Subject: [PATCH 036/100] firmware: GEOS 128 boot disk setup moves from the root menu to TeensyROM Specific Preparing the boot disk is a TeensyROM utility, not a program to launch. --- Source/Teensy/MainMenuItems.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/MainMenuItems.h b/Source/Teensy/MainMenuItems.h index f638ee5f..9f63c511 100644 --- a/Source/Teensy/MainMenuItems.h +++ b/Source/Teensy/MainMenuItems.h @@ -296,6 +296,7 @@ StructMenuItem dirTeensyROMSpecific[] = rtFilePrg , IOH_TR_BASIC , (char*)"BASIC TR Command IO access only" , (uint8_t*)empty_prg , sizeof(empty_prg) , rtBin8kLo , IOH_TeensyROM , (char*)"(This) TeensyROM Menu Cart" , (uint8_t*)TeensyROMC64_bin , sizeof(TeensyROMC64_bin) , rtFilePrg , IOH_TeensyROM , (char*)"TeensyROM External Ports Test" , (uint8_t*)TRExtPortCheck_prg , sizeof(TRExtPortCheck_prg) , + rtGEOS , IOH_None , (char*)"GEOS 128 Boot Disk Setup" , (uint8_t*)&Geos128Launcher , sizeof(Geos128Launcher) , #ifdef Fab04_REU rtFilePrg , IOH_TeensyROM , (char*)"TR+ C64 Expansion Port Test" , (uint8_t*)ExpansionPortTest_prg , sizeof(ExpansionPortTest_prg) , #endif @@ -324,6 +325,5 @@ StructMenuItem TeensyROMMenu[] = /**7**/ rtDirectory, IOH_None , (char*)"/Utilities" , (uint8_t*)dirUtilities , sizeof(dirUtilities) , /* 8 */ rtDirectory, IOH_None , (char*)"/Text + PETSCII + Docs" , (uint8_t*)dirTEXT_PETSCII , sizeof(dirTEXT_PETSCII) , /**9**/ rtDirectory, IOH_None , (char*)"/TeensyROM Specific" , (uint8_t*)dirTeensyROMSpecific , sizeof(dirTeensyROMSpecific) , - /* 10*/ rtGEOS , IOH_None , (char*)"GEOS 128" , (uint8_t*)&Geos128Launcher , sizeof(Geos128Launcher) , }; From ae2841f39b14577a512ddec2f34233c7bb778cbc Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 18:42:27 -0500 Subject: [PATCH 037/100] firmware: the TR block device contract, register core and disk image backend A register interface for reading and writing 256-byte blocks of a disk image on the SD card with no drive on the IEC bus, for the GEOS disk driver and any other software that finds it. The core is free of Arduino so a host test drives it. --- Source/Teensy/BlockDevice/TRBlock.h | 159 ++++++++++++++++++ Source/Teensy/BlockDevice/TRBlockImage.h | 60 +++++++ Source/Teensy/tests/trblock.test.js | 203 +++++++++++++++++++++++ Source/Teensy/tests/trblock_harness.c | 76 +++++++++ docs/TR_Block_Device.md | 124 ++++++++++++++ 5 files changed, 622 insertions(+) create mode 100644 Source/Teensy/BlockDevice/TRBlock.h create mode 100644 Source/Teensy/BlockDevice/TRBlockImage.h create mode 100644 Source/Teensy/tests/trblock.test.js create mode 100644 Source/Teensy/tests/trblock_harness.c create mode 100644 docs/TR_Block_Device.md diff --git a/Source/Teensy/BlockDevice/TRBlock.h b/Source/Teensy/BlockDevice/TRBlock.h new file mode 100644 index 00000000..06cb59fe --- /dev/null +++ b/Source/Teensy/BlockDevice/TRBlock.h @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +// The register side of the TR block device, free of Arduino so a host test can drive it. +// The contract is docs/TR_Block_Device.md; offsets here are from the base that doc names. +#pragma once +#include +#include +#include + +#define TRBLOCK_SIZE 256 +#define TRBLOCK_WINDOW_SIZE 64 +#define TRBLOCK_INFO_SIZE 5 + +enum TRBlockOffset +{ + TRBlockCmd = 0x08, + TRBlockAddr0 = 0x09, + TRBlockAddr1 = 0x0a, + TRBlockPage = 0x0b, + TRBlockToken = 0x0c, + TRBlockStatus = 0x0d, + TRBlockEcho = 0x0e, + TRBlockId = 0x10, + TRBlockWindow = 0x40, +}; + +enum TRBlockCommand +{ + TRBlockRead = 1, + TRBlockWrite = 2, + TRBlockFlush = 3, + TRBlockInfo = 4, +}; + +enum TRBlockStatusValue +{ + TRBlockNeverRun = 0x00, + TRBlockBusy = 0x80, + TRBlockDone = 0x5a, + TRBlockNoImage = 0xe1, + TRBlockBadAddress = 0xe2, + TRBlockReadFailed = 0xe3, + TRBlockWriteFailed = 0xe4, + TRBlockReadOnly = 0xe5, + TRBlockBadCommand = 0xe6, +}; + +static const char TRBlockIdText[] = "TRB"; +#define TRBLOCK_ID_SIZE (sizeof TRBlockIdText - 1) + +// Where the ISR and the main loop meet: the ISR only fills registers and the window and starts a +// job, the main loop does the card's work. +struct TRBlock +{ + uint8_t Buffer[TRBLOCK_SIZE]; + uint8_t Addr0, Addr1, Page, Token; + uint8_t JobCmd, JobAddr0, JobAddr1, JobToken; + volatile uint8_t Status, Echo; + volatile bool Pending; +}; + +struct TRBlockBackend +{ + void *Context; + uint8_t (*Transfer)(void *Context, bool Write, uint8_t Addr0, uint8_t Addr1, uint8_t *Block); + uint8_t (*Info)(void *Context, uint8_t *Out); // TRBLOCK_INFO_SIZE bytes + uint8_t (*Flush)(void *Context); +}; + +// ISR and main loop run on one core, so only the compiler's ordering matters +#define TRBLOCK_ORDER() __atomic_signal_fence(__ATOMIC_SEQ_CST) + +static inline void TRBlockReset(struct TRBlock *Dev) +{ + memset((void *)Dev, 0, sizeof *Dev); +} + +// The IO ISR's side: reads answer from registers or the window... +static inline bool TRBlockServes(uint8_t Offset) +{ + return Offset == TRBlockStatus || Offset == TRBlockEcho || + (Offset >= TRBlockId && Offset < TRBlockId + TRBLOCK_ID_SIZE) || + (Offset >= TRBlockWindow && Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE); +} + +static inline uint8_t TRBlockReadAt(const struct TRBlock *Dev, uint8_t Offset) +{ + if (Offset == TRBlockStatus) return Dev->Status; + if (Offset == TRBlockEcho) return Dev->Echo; + if (Offset >= TRBlockId && Offset < TRBlockId + TRBLOCK_ID_SIZE) return (uint8_t)TRBlockIdText[Offset - TRBlockId]; + return Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow]; +} + +// ...and writes go to registers or the window. The caller reads the data byte only when this says +// the offset takes one. +static inline bool TRBlockTakes(uint8_t Offset) +{ + return (Offset >= TRBlockCmd && Offset <= TRBlockToken) || + (Offset >= TRBlockWindow && Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE); +} + +static inline void TRBlockWriteAt(struct TRBlock *Dev, uint8_t Offset, uint8_t Data) +{ + bool Busy = Dev->Status == TRBlockBusy; + + if (Offset >= TRBlockWindow) + { + if (!Busy) Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow] = Data; + return; + } + if (Offset == TRBlockAddr0) Dev->Addr0 = Data; + else if (Offset == TRBlockAddr1) Dev->Addr1 = Data; + else if (Offset == TRBlockPage) Dev->Page = Data; + else if (Offset == TRBlockToken) Dev->Token = Data; + else if (Offset == TRBlockCmd && !Busy) + { + Dev->JobCmd = Data; + Dev->JobAddr0 = Dev->Addr0; + Dev->JobAddr1 = Dev->Addr1; + Dev->JobToken = Dev->Token; + TRBLOCK_ORDER(); + Dev->Status = TRBlockBusy; + TRBLOCK_ORDER(); + Dev->Pending = true; + } +} + +static uint8_t TRBlockRun(struct TRBlock *Dev, const struct TRBlockBackend *Backend) +{ + if (!Backend || !Backend->Context) return TRBlockNoImage; + switch (Dev->JobCmd) + { + case TRBlockRead: + return Backend->Transfer(Backend->Context, false, Dev->JobAddr0, Dev->JobAddr1, Dev->Buffer); + case TRBlockWrite: + return Backend->Transfer(Backend->Context, true, Dev->JobAddr0, Dev->JobAddr1, Dev->Buffer); + case TRBlockFlush: + return Backend->Flush(Backend->Context); + case TRBlockInfo: + return Backend->Info(Backend->Context, Dev->Buffer); + } + return TRBlockBadCommand; +} + +// The main loop's side: runs a started command, true if there was one. The echo is published +// before the status, so a status that is not busy always comes with its token. +static inline bool TRBlockService(struct TRBlock *Dev, const struct TRBlockBackend *Backend) +{ + uint8_t Result; + + if (!Dev->Pending) return false; + TRBLOCK_ORDER(); + Result = TRBlockRun(Dev, Backend); + Dev->Pending = false; + TRBLOCK_ORDER(); + Dev->Echo = Dev->JobToken; + TRBLOCK_ORDER(); + Dev->Status = Result; + return true; +} diff --git a/Source/Teensy/BlockDevice/TRBlockImage.h b/Source/Teensy/BlockDevice/TRBlockImage.h new file mode 100644 index 00000000..e48e7fe1 --- /dev/null +++ b/Source/Teensy/BlockDevice/TRBlockImage.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// A CBM disk image (D64, D71, D81) as the block device's backend: the track and sector the driver +// sends become an offset in the image file. +#pragma once +#include "../GEOS/GeosDiskPatch.h" +#include "TRBlock.h" + +#define TRBLOCK_IMAGE_WRITABLE 0x01 + +struct TRBlockImage +{ + const struct GeosFormat *Format; + bool Writable; + void *Context; + bool (*Read)(void *Context, uint32_t Offset, uint8_t *Block); + bool (*Write)(void *Context, uint32_t Offset, const uint8_t *Block); + bool (*Flush)(void *Context); +}; + +static inline bool TRBlockImageHasSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +{ + return Track >= 1 && Track <= Format->Tracks && Sector < GeosSectorsOnTrack(Format, Track); +} + +static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, uint8_t Sector, uint8_t *Block) +{ + struct TRBlockImage *Image = (struct TRBlockImage *)Context; + uint32_t Offset; + + if (!TRBlockImageHasSector(Image->Format, Track, Sector)) return TRBlockBadAddress; + Offset = GeosOffset(Image->Format, Track, Sector); + if (!Write) return Image->Read(Image->Context, Offset, Block) ? TRBlockDone : TRBlockReadFailed; + if (!Image->Writable) return TRBlockReadOnly; + return Image->Write(Image->Context, Offset, Block) ? TRBlockDone : TRBlockWriteFailed; +} + +static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) +{ + struct TRBlockImage *Image = (struct TRBlockImage *)Context; + uint32_t Blocks = Image->Format->Size / GEOS_SECTOR_SIZE; + + Out[0] = (uint8_t)(Image->Format - GeosFormats + 1); + Out[1] = Image->Writable ? TRBLOCK_IMAGE_WRITABLE : 0; + Out[2] = (uint8_t)Blocks; + Out[3] = (uint8_t)(Blocks >> 8); + Out[4] = (uint8_t)(Blocks >> 16); + return TRBlockDone; +} + +static uint8_t TRBlockImageFlush(void *Context) +{ + struct TRBlockImage *Image = (struct TRBlockImage *)Context; + + return Image->Flush(Image->Context) ? TRBlockDone : TRBlockWriteFailed; +} + +static inline struct TRBlockBackend TRBlockImageBackend(struct TRBlockImage *Image) +{ + return (struct TRBlockBackend){ Image, TRBlockImageTransfer, TRBlockImageInfo, TRBlockImageFlush }; +} diff --git a/Source/Teensy/tests/trblock.test.js b/Source/Teensy/tests/trblock.test.js new file mode 100644 index 00000000..8a0451a4 --- /dev/null +++ b/Source/Teensy/tests/trblock.test.js @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +// The TR block device's registers and disk image backend, held to docs/TR_Block_Device.md. +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 FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); +const HARNESS_SOURCE = path.join(FIRMWARE_ROOT, 'tests/trblock_harness.c'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const skip = !commandExists('cc') && !inCi && 'cc is not on PATH'; + +const SECTOR = 256; +const WINDOW = 64; +const PAGES = SECTOR / WINDOW; +const CMD = 0x08, ADDR0 = 0x09, ADDR1 = 0x0a, PAGE = 0x0b, TOKEN = 0x0c, STATUS = 0x0d, ECHO = 0x0e, ID = 0x10; +const WINDOW_AT = 0x40; +const READ = 1, WRITE = 2, FLUSH = 3, INFO = 4; +const BUSY = 0x80, DONE = 0x5a, NO_IMAGE = 0xe1, BAD_ADDRESS = 0xe2, READ_ONLY = 0xe5, BAD_COMMAND = 0xe6; + +const zoneSectors = (track) => { + const zone = (track - 1) % 35 + 1; + return zone <= 17 ? 21 : zone <= 24 ? 19 : zone <= 30 ? 18 : 17; +}; + +const FORMATS = { + d64: { kind: 1, tracks: 35, sectors: zoneSectors }, + d71: { kind: 2, tracks: 70, sectors: zoneSectors }, + d81: { kind: 3, tracks: 80, sectors: () => 40 }, +}; + +const blockNumber = (format, track, sector) => { + let blocks = sector; + for (let t = 1; t < track; t++) blocks += format.sectors(t); + return blocks; +}; + +const totalBlocks = (format) => blockNumber(format, format.tracks + 1, 0); + +// Every byte says which block it is in: the block number's two bytes, then a counter +const sectorBytes = (number) => Buffer.from(Array.from({ length: SECTOR }, (_, i) => + i === 0 ? number & 0xff : i === 1 ? (number >> 8) & 0xff : (i + number) & 0xff)); + +function makeImage(format) { + return Buffer.concat(Array.from({ length: totalBlocks(format) }, (_, n) => sectorBytes(n))); +} + +const hex = (n) => n.toString(16); +const write = (offset, value) => `w ${hex(offset)} ${hex(value)}\n`; +const read = (offset) => `r ${hex(offset)}\n`; + +// A command as the driver sends it: operands, token, then CMD +const command = (cmd, { addr0 = 0, addr1 = 0, page = 0, token = 1 } = {}) => + write(ADDR0, addr0) + write(ADDR1, addr1) + write(PAGE, page) + write(TOKEN, token) + write(CMD, cmd); + +const readBlock = (addr0, addr1, token = 1) => + command(READ, { addr0, addr1, token }) + 'svc\n' + + Array.from({ length: PAGES }, (_, p) => write(PAGE, p) + Array.from({ length: WINDOW }, (_, i) => read(WINDOW_AT + i)).join('')).join(''); + +const fillWindow = (bytes) => Array.from({ length: PAGES }, (_, p) => + write(PAGE, p) + Array.from({ length: WINDOW }, (_, i) => write(WINDOW_AT + i, bytes[p * WINDOW + i])).join('')).join(''); + +let harnessPath; +function harness() { + if (harnessPath) return harnessPath; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trblock-')); + harnessPath = path.join(dir, 'harness'); + const built = spawnSync('cc', ['-std=c99', '-Wall', '-Wextra', '-Werror', '-o', harnessPath, HARNESS_SOURCE], { encoding: 'utf8' }); + assert.equal(built.status, 0, built.stderr); + return harnessPath; +} + +function run(kind, script, mode) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trblock-run-')); + const image = path.join(dir, 'in.img'), out = path.join(dir, 'out.img'); + fs.writeFileSync(image, makeImage(FORMATS[kind])); + 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); + return { lines, image: fs.readFileSync(out), original: fs.readFileSync(image) }; +} + +const bytesOf = (lines) => Buffer.from(lines.map((l) => parseInt(l, 16))); +const statusOf = (script, kind = 'd81', mode) => run(kind, script + read(STATUS), mode).lines.at(-1); + +test('the id is TRB at 0x10, and nothing else answers outside the registers and window', { skip }, () => { + const { lines } = run('d81', [0x10, 0x11, 0x12, 0x13, 0x0f, 0x08, 0x09, 0x0b, 0x0c, 0x3f, 0x80].map(read).join('')); + assert.deepEqual(lines, ['54', '52', '42', 'open', 'open', 'open', 'open', 'open', 'open', 'open', 'open']); +}); + +test('status reads 00 before any command', { skip }, () => { + assert.equal(run('d81', read(STATUS) + read(ECHO)).lines.join(), '00,00'); +}); + +test('status is busy from the CMD write until the main loop services it, then done with the token', { skip }, () => { + const { lines } = run('d81', command(READ, { addr0: 40, addr1: 0, token: 0x37 }) + read(STATUS) + read(ECHO) + 'svc\n' + read(STATUS) + read(ECHO) + 'svc\n'); + assert.deepEqual(lines, ['80', '00', '1', '5a', '37', '0']); +}); + +for (const [kind, format] of Object.entries(FORMATS)) { + test(`${kind}: every track's first and last sector read back as that block, in four pages`, { skip }, () => { + for (let track = 1; track <= format.tracks; track++) { + for (const sector of [0, format.sectors(track) - 1]) { + const { lines } = run(kind, readBlock(track, sector)); + const got = bytesOf(lines.slice(1)); + assert.deepEqual(got, sectorBytes(blockNumber(format, track, sector)), `${kind} ${track}/${sector}`); + } + } + }); + + test(`${kind}: the sector after a track's last, track 0 and the track past the end fail with bad address`, { skip }, () => { + const cases = [[0, 0], [format.tracks + 1, 0], [1, format.sectors(1)], [format.tracks, format.sectors(format.tracks)], [255, 255]]; + for (const [track, sector] of cases) { + assert.equal(statusOf(command(READ, { addr0: track, addr1: sector }) + 'svc\n', kind), hex(BAD_ADDRESS), `${track}/${sector}`); + } + }); + + test(`${kind}: info says the kind, that it is writable and the block count`, { skip }, () => { + const blocks = totalBlocks(format); + const { lines } = run(kind, command(INFO) + 'svc\n' + Array.from({ length: 5 }, (_, i) => read(WINDOW_AT + i)).join('')); + assert.deepEqual(bytesOf(lines.slice(1)), Buffer.from([format.kind, 1, blocks & 0xff, (blocks >> 8) & 0xff, blocks >> 16])); + }); +} + +test('a write reaches exactly its block in the image and leaves every other byte alone', { skip }, () => { + const format = FORMATS.d81; + const payload = Buffer.from(Array.from({ length: SECTOR }, (_, i) => (i * 7 + 3) & 0xff)); + const { image, original } = run('d81', fillWindow(payload) + command(WRITE, { addr0: 79, addr1: 5 }) + 'svc\n'); + const at = blockNumber(format, 79, 5) * SECTOR; + assert.deepEqual(image.subarray(at, at + SECTOR), payload); + assert.deepEqual(Buffer.concat([image.subarray(0, at), image.subarray(at + SECTOR)]), + Buffer.concat([original.subarray(0, at), original.subarray(at + SECTOR)])); +}); + +test('a write then a read of the same block returns what was written, whatever came before', { skip }, () => { + const payload = Buffer.from(Array.from({ length: SECTOR }, (_, i) => 255 - i)); + const { lines } = run('d64', fillWindow(payload) + command(WRITE, { addr0: 18, addr1: 1 }) + 'svc\n' + readBlock(18, 1, 2)); + assert.deepEqual(bytesOf(lines.slice(2)), payload); +}); + +test('a failed write changes nothing in the image', { skip }, () => { + const payload = Buffer.alloc(SECTOR, 0xee); + const bad = run('d64', fillWindow(payload) + command(WRITE, { addr0: 36, addr1: 0 }) + 'svc\n'); + assert.deepEqual(bad.image, bad.original); + const readOnly = run('d81', fillWindow(payload) + command(WRITE, { addr0: 1, addr1: 0 }) + 'svc\n' + read(STATUS), 'ro'); + assert.equal(readOnly.lines.at(-1), hex(READ_ONLY)); + assert.deepEqual(readOnly.image, readOnly.original); +}); + +test('a read-only image still reads, and info clears the writable flag', { skip }, () => { + const { lines } = run('d81', command(INFO) + 'svc\n' + read(WINDOW_AT + 1) + readBlock(1, 0, 2).split('\n').slice(0, 2).join('\n') + '\n' + read(STATUS), 'ro'); + assert.equal(lines[1], '00'); + assert.equal(lines.at(-1), '5a'); +}); + +test('with no image mounted every command fails with no image, and status never claims done', { skip }, () => { + for (const cmd of [READ, WRITE, FLUSH, INFO]) { + assert.equal(statusOf(command(cmd, { addr0: 1 }) + 'svc\n', 'd81', 'nobackend'), hex(NO_IMAGE), `cmd ${cmd}`); + } +}); + +test('a command that is not one fails with bad command, and so does zero', { skip }, () => { + for (const cmd of [0, 5, 0x80, 0xff]) assert.equal(statusOf(command(cmd) + 'svc\n'), hex(BAD_COMMAND), `cmd ${cmd}`); +}); + +test('flush reaches the backend once per command and reports done', { skip }, () => { + const { lines } = run('d81', command(FLUSH) + 'svc\n' + read(STATUS) + 'flushes\n' + command(FLUSH, { token: 2 }) + 'svc\nflushes\n'); + assert.deepEqual(lines, ['1', '5a', '1', '1', '2']); +}); + +test('operands are latched when CMD is written: changing them while the command waits changes nothing', { skip }, () => { + const start = command(READ, { addr0: 40, addr1: 3, token: 9 }); + const { lines } = run('d81', start + write(ADDR0, 1) + write(ADDR1, 1) + write(TOKEN, 0x44) + 'svc\n' + read(ECHO) + read(WINDOW_AT)); + assert.equal(lines[1], '09'); + assert.equal(parseInt(lines[2], 16), blockNumber(FORMATS.d81, 40, 3) & 0xff); +}); + +test('a second CMD while one is busy is ignored, and window writes while busy are ignored', { skip }, () => { + const { lines } = run('d81', command(READ, { addr0: 40, addr1: 3, token: 5 }) + command(READ, { addr0: 1, addr1: 0, token: 6 }) + + write(WINDOW_AT, 0x99) + 'svc\nsvc\n' + read(ECHO) + read(WINDOW_AT)); + assert.deepEqual(lines.slice(0, 2), ['1', '0']); + assert.equal(lines[2], '05'); + assert.equal(parseInt(lines[3], 16), blockNumber(FORMATS.d81, 40, 3) & 0xff); +}); + +test('a window write while a write command is busy does not change what that command writes', { skip }, () => { + const payload = Buffer.alloc(SECTOR, 0x11); + const { image } = run('d81', fillWindow(payload) + command(WRITE, { addr0: 2, addr1: 0 }) + write(PAGE, 0) + write(WINDOW_AT, 0x99) + 'svc\n'); + const at = blockNumber(FORMATS.d81, 2, 0) * SECTOR; + assert.equal(image[at], 0x11); +}); + +test('the page register uses only its low two bits', { skip }, () => { + const { lines } = run('d81', command(READ, { addr0: 1, addr1: 0 }) + 'svc\n' + write(PAGE, 0) + read(WINDOW_AT) + write(PAGE, 4) + read(WINDOW_AT) + write(PAGE, 5) + read(WINDOW_AT)); + assert.equal(lines[1], lines[2]); + assert.notEqual(lines[1], lines[3]); +}); diff --git a/Source/Teensy/tests/trblock_harness.c b/Source/Teensy/tests/trblock_harness.c new file mode 100644 index 00000000..8477b5a9 --- /dev/null +++ b/Source/Teensy/tests/trblock_harness.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +// trblock_harness IMAGE OUT [ro|nobackend]: the block device's register side and disk image backend, +// driven from stdin one command a line, as the IO ISR and the main loop would drive them: +// w OFFSET HEX an IO write r OFFSET an IO read, printing the byte or "open" +// svc the main loop's service, printing 1 if a command ran +// flushes how many times the backend was asked to flush +// OUT receives the image when stdin ends. +#include +#include + +#include "../BlockDevice/TRBlockImage.h" + +#define MAX_IMAGE 819200 + +static uint8_t Bytes[MAX_IMAGE]; +static uint32_t Size, Flushes; + +static bool ReadAt(void *Context, uint32_t Offset, uint8_t *Block) +{ + (void)Context; + if (Offset + GEOS_SECTOR_SIZE > Size) return false; + memcpy(Block, Bytes + Offset, GEOS_SECTOR_SIZE); + return true; +} + +static bool WriteAt(void *Context, uint32_t Offset, const uint8_t *Block) +{ + (void)Context; + if (Offset + GEOS_SECTOR_SIZE > Size) return false; + memcpy(Bytes + Offset, Block, GEOS_SECTOR_SIZE); + return true; +} + +static bool FlushAll(void *Context) +{ + (void)Context; + Flushes++; + return true; +} + +int main(int argc, char **argv) +{ + FILE *In = fopen(argv[1], "rb"); + struct TRBlock Dev; + struct TRBlockImage Image = { NULL, argc < 4 || strcmp(argv[3], "ro"), NULL, ReadAt, WriteAt, FlushAll }; + struct TRBlockBackend Backend; + char Line[64]; + + Size = fread(Bytes, 1, MAX_IMAGE, In); + fclose(In); + Image.Format = GeosFormatForSize(Size); + Backend = TRBlockImageBackend(&Image); + TRBlockReset(&Dev); + const struct TRBlockBackend *Use = argc >= 4 && !strcmp(argv[3], "nobackend") ? NULL : &Backend; + + while (fgets(Line, sizeof Line, stdin)) + { + unsigned Offset, Data; + + if (sscanf(Line, "w %x %x", &Offset, &Data) == 2) + { + if (TRBlockTakes(Offset)) TRBlockWriteAt(&Dev, Offset, Data); + } + else if (sscanf(Line, "r %x", &Offset) == 1) + { + if (TRBlockServes(Offset)) printf("%02x\n", TRBlockReadAt(&Dev, Offset)); + else printf("open\n"); + } + else if (!strncmp(Line, "svc", 3)) printf("%d\n", TRBlockService(&Dev, Use)); + else if (!strncmp(Line, "flushes", 7)) printf("%u\n", Flushes); + } + FILE *Out = fopen(argv[2], "wb"); + fwrite(Bytes, 1, Size, Out); + fclose(Out); + return 0; +} diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md new file mode 100644 index 00000000..ce8be6e7 --- /dev/null +++ b/docs/TR_Block_Device.md @@ -0,0 +1,124 @@ +# TeensyROM block device + +A small register interface that lets software on the C64 or C128 read and write 256-byte blocks of a disk image file on the TeensyROM's SD card, with no drive on the IEC bus. The first user is the GEOS disk driver in [`tools/geos`](../tools/geos), but nothing here is specific to GEOS: any program that finds the registers can use them. + +This document is the whole contract. The firmware side is [`TRBlock.h`](../Source/Teensy/BlockDevice/TRBlock.h); the registers sit beside the USB mouse's, served from [`TRBlockIO.c`](../Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c). + +## Table of contents + * [What it is and is not](#what-it-is-and-is-not) + * [Where the registers are](#where-the-registers-are) + * [Register map](#register-map) + * [Running a command](#running-a-command) + * [Commands](#commands) + * [Status and errors](#status-and-errors) + * [Disk addresses](#disk-addresses) + * [Writes reach the card later](#writes-reach-the-card-later) + * [What the firmware does not promise](#what-the-firmware-does-not-promise) + * [A reader's checklist](#a-readers-checklist) + +## 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 a RAM expansion handler is active (GeoRAM or REU, which is also where the USB mouse registers are). It is not available under other handlers, 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 USB mouse registers already occupy the start of the free IO page left by the RAM expansion handler, and identify it: + +| Handler | Free page | Base (`B`) | +|---------|-----------|-----------:| +| GeoRAM | IO2 | `$DF00` | +| REU | IO1 | `$DE00` | + +Find the base the way the mouse driver does: look for `TRM` at `$DF03..$DF05`, then at `$DE03..$DE05`. The block device is present when `TRB` is also 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. + +Everything below is an offset from `B`. + +## Register map + +| Offset | Dir | Name | Meaning | +|-------:|:---:|------|---------| +| `$08` | write | CMD | command; writing it starts the command | +| `$09` | write | ADDR0 | block address, first byte | +| `$0A` | write | ADDR1 | block address, second byte | +| `$0B` | write | PAGE | which 64-byte quarter of the block the window shows, 0 to 3 (higher bits ignored) | +| `$0C` | write | TOKEN | a non-zero byte the driver chooses for the next command | +| `$0D` | read | STATUS | `$80` busy, `$5A` done, `$E0` and up failed, `$00` no command run yet | +| `$0E` | read | ECHO | the token of the command STATUS reports on | +| `$10..$12` | read | ID | `TRB` | +| `$40..$7F` | read/write | WINDOW | the quarter of the block PAGE selects | + +Reading a register marked write is not answered. Offsets not listed are not answered. + +## Running a command +1. Write ADDR0, ADDR1 and PAGE as the command needs, and TOKEN, a non-zero byte that differs from the last command's. +2. For a write, fill the window, one PAGE at a time (see [`WRITE`](#commands)). +3. Write CMD. STATUS reads `$80` from the moment this write completes, so a read of STATUS after it never sees the previous command's result. +4. Read STATUS until it is not `$80`, then check ECHO equals your TOKEN. A result with another token is not yours: wait, or run the command again. +5. `$5A` is success. `$E0` and up is a failure ([Status and errors](#status-and-errors)). + +Two rules that Wes Biggs' [teensyrom-vdrive](https://github.com/wesbiggs/teensyrom-vdrive) learned on hardware with a similar register interface, and which hold here too: +* **Read STATUS until two consecutive reads agree** before believing a value other than `$80`. A read can land while the firmware is updating it. +* **Read back what you write into the window** when the data matters, and compare, before sending CMD. + +Do not write ADDR0, ADDR1, PAGE or TOKEN, or the window, while STATUS is `$80`. The firmware latches the command's operands when CMD is written, so a stray register write cannot change a command already running, but writes to the window during a command are ignored. + +## Commands + +| CMD | Name | What it does | +|----:|------|--------------| +| `$01` | `READ` | reads the block at ADDR0, ADDR1 into the firmware's 256-byte buffer. Afterwards the window shows the PAGE quarter of it, and each write to PAGE shows another | +| `$02` | `WRITE` | writes the 256-byte buffer to the block at ADDR0, ADDR1. The buffer is what the window wrote, one PAGE at a time, since the last command | +| `$03` | `FLUSH` | forces the card to hold every write accepted so far | +| `$04` | `INFO` | describes the image: five bytes at the start of the window (see below) | + +`READ` and `INFO` fill the buffer; a `WRITE` after either writes what they left unless the window changed it. A `WRITE` for a block the driver has not filled is the driver's mistake, not the firmware's to guard. + +`INFO` answers, in window bytes 0 to 4: + +| Byte | Meaning | +|-----:|---------| +| 0 | image kind: `1` D64, `2` D71, `3` D81 | +| 1 | flags; bit 0 set when the image can be written | +| 2..4 | blocks in the image, 24 bits, low byte first | + +Any other CMD fails with `$E6`. + +## Status and errors + +| STATUS | Meaning | +|-------:|---------| +| `$00` | no command has been run since the handler started | +| `$80` | a command is running | +| `$5A` | the command with ECHO's token succeeded | +| `$E1` | no image is mounted | +| `$E2` | the address is outside the image | +| `$E3` | the card could not be read | +| `$E4` | the card could not be written | +| `$E5` | the image is read-only | +| `$E6` | not a command | + +A failure changes nothing on the card. A failed `READ` leaves the buffer unspecified. + +## Disk addresses +For the disk image kinds (D64, D71, D81) ADDR0 is the track and ADDR1 the sector, as the 1541, 1571 and 1581 number them: tracks start at 1, sectors at 0. The 1541 and 1571 use the usual four speed zones (21, 19, 18 and 17 sectors a track, the 1571's second side repeating them); the 1581 has 40 sectors on every track. An address off the end of the image or past a track's last sector fails with `$E2`. + +The firmware does this arithmetic so that a 6502 driver does not have to. A driver that wants raw sector numbers instead is a different image kind, which this version does not define. + +## Writes reach the card later +`WRITE` returns `$5A` when the block is accepted, not when the card holds it. The firmware writes the block to the file at once and flushes the file when no command has arrived for a quarter of a second, or on `FLUSH`. A program that must know a write survived power loss sends `FLUSH` and waits for `$5A`. GEOS does not, so a disk saved and then powered off within that quarter second can be torn. + +## What the firmware does not promise +* **Speed.** A command takes as long as the card takes. Poll; do not time it. +* **C128 2 MHz.** The registers have not been exercised at 2 MHz. A driver on the C128 should run at 1 MHz around a transfer until that is measured. +* **Concurrent users.** One command at a time, one driver at a time. Two programs polling the same STATUS will read each other's results; the token tells them apart but does not queue them. +* **The image.** It must not be changed on the card while it is mounted. + +## A reader's checklist +A driver that follows this document: +* finds `B` from `TRM`, then confirms `TRB`; +* 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; +* sends `FLUSH` where a write must survive power loss; +* works whichever of `$DF00` and `$DE00` the base is. From e09f7e1d7818b8aa3169363077ce048983f68909 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 18:43:24 -0500 Subject: [PATCH 038/100] firmware: TRBlock bounds the window access itself and tolerates an unrecognized image size Found reviewing ae2841f: ReadAt/WriteAt indexed the buffer for any offset past the window, and the image backend dereferenced a null Format. --- Source/Teensy/BlockDevice/TRBlock.h | 8 ++++++-- Source/Teensy/BlockDevice/TRBlockImage.h | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/BlockDevice/TRBlock.h b/Source/Teensy/BlockDevice/TRBlock.h index 06cb59fe..08469765 100644 --- a/Source/Teensy/BlockDevice/TRBlock.h +++ b/Source/Teensy/BlockDevice/TRBlock.h @@ -87,7 +87,11 @@ static inline uint8_t TRBlockReadAt(const struct TRBlock *Dev, uint8_t Offset) if (Offset == TRBlockStatus) return Dev->Status; if (Offset == TRBlockEcho) return Dev->Echo; if (Offset >= TRBlockId && Offset < TRBlockId + TRBLOCK_ID_SIZE) return (uint8_t)TRBlockIdText[Offset - TRBlockId]; - return Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow]; + if (Offset >= TRBlockWindow && Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE) + { + return Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow]; + } + return 0xff; } // ...and writes go to registers or the window. The caller reads the data byte only when this says @@ -104,7 +108,7 @@ static inline void TRBlockWriteAt(struct TRBlock *Dev, uint8_t Offset, uint8_t D if (Offset >= TRBlockWindow) { - if (!Busy) Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow] = Data; + if (Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE && !Busy) Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow] = Data; return; } if (Offset == TRBlockAddr0) Dev->Addr0 = Data; diff --git a/Source/Teensy/BlockDevice/TRBlockImage.h b/Source/Teensy/BlockDevice/TRBlockImage.h index e48e7fe1..308a1326 100644 --- a/Source/Teensy/BlockDevice/TRBlockImage.h +++ b/Source/Teensy/BlockDevice/TRBlockImage.h @@ -27,6 +27,7 @@ static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, ui struct TRBlockImage *Image = (struct TRBlockImage *)Context; uint32_t Offset; + if (!Image->Format) return TRBlockNoImage; if (!TRBlockImageHasSector(Image->Format, Track, Sector)) return TRBlockBadAddress; Offset = GeosOffset(Image->Format, Track, Sector); if (!Write) return Image->Read(Image->Context, Offset, Block) ? TRBlockDone : TRBlockReadFailed; @@ -37,7 +38,10 @@ static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, ui static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; - uint32_t Blocks = Image->Format->Size / GEOS_SECTOR_SIZE; + uint32_t Blocks; + + if (!Image->Format) return TRBlockNoImage; + Blocks = Image->Format->Size / GEOS_SECTOR_SIZE; Out[0] = (uint8_t)(Image->Format - GeosFormats + 1); Out[1] = Image->Writable ? TRBLOCK_IMAGE_WRITABLE : 0; From c86f8ce4f7af81b60096d86eec1db0f069fd3073 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 18:44:05 -0500 Subject: [PATCH 039/100] tests: cover the unknown-size image, and state which failed writes can leave a block changed --- Source/Teensy/tests/trblock.test.js | 10 ++++++++++ Source/Teensy/tests/trblock_harness.c | 7 ++++++- docs/TR_Block_Device.md | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/tests/trblock.test.js b/Source/Teensy/tests/trblock.test.js index 8a0451a4..addca66a 100644 --- a/Source/Teensy/tests/trblock.test.js +++ b/Source/Teensy/tests/trblock.test.js @@ -165,6 +165,16 @@ test('with no image mounted every command fails with no image, and status never } }); +test('an image that is no known size mounts as no image rather than being guessed at', { skip }, () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trblock-odd-')); + const image = path.join(dir, 'in.img'), out = path.join(dir, 'out.img'); + fs.writeFileSync(image, makeImage(FORMATS.d64).subarray(0, SECTOR * 100)); + for (const cmd of [READ, WRITE, INFO]) { + const ran = spawnSync(harness(), [image, out], { input: command(cmd, { addr0: 1 }) + 'svc\n' + read(STATUS), encoding: 'utf8' }); + assert.equal(ran.stdout.trim().split('\n').at(-1), hex(NO_IMAGE), `cmd ${cmd}`); + } +}); + test('a command that is not one fails with bad command, and so does zero', { skip }, () => { for (const cmd of [0, 5, 0x80, 0xff]) assert.equal(statusOf(command(cmd) + 'svc\n'), hex(BAD_COMMAND), `cmd ${cmd}`); }); diff --git a/Source/Teensy/tests/trblock_harness.c b/Source/Teensy/tests/trblock_harness.c index 8477b5a9..6b190363 100644 --- a/Source/Teensy/tests/trblock_harness.c +++ b/Source/Teensy/tests/trblock_harness.c @@ -40,12 +40,17 @@ static bool FlushAll(void *Context) int main(int argc, char **argv) { - FILE *In = fopen(argv[1], "rb"); + FILE *In = argc >= 3 ? fopen(argv[1], "rb") : NULL; struct TRBlock Dev; struct TRBlockImage Image = { NULL, argc < 4 || strcmp(argv[3], "ro"), NULL, ReadAt, WriteAt, FlushAll }; struct TRBlockBackend Backend; char Line[64]; + if (!In) + { + fprintf(stderr, "usage: trblock_harness IMAGE OUT [ro|nobackend]\n"); + return 2; + } Size = fread(Bytes, 1, MAX_IMAGE, In); fclose(In); Image.Format = GeosFormatForSize(Size); diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index ce8be6e7..96ad5fe9 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -98,7 +98,7 @@ Any other CMD fails with `$E6`. | `$E5` | the image is read-only | | `$E6` | not a command | -A failure changes nothing on the card. A failed `READ` leaves the buffer unspecified. +A failed `READ` leaves the buffer unspecified. A failed `WRITE` that was rejected before the card was touched (`$E1`, `$E2`, `$E5`) changes nothing; one the card failed (`$E4`) may have changed part of the block. ## Disk addresses For the disk image kinds (D64, D71, D81) ADDR0 is the track and ADDR1 the sector, as the 1541, 1571 and 1581 number them: tracks start at 1, sectors at 0. The 1541 and 1571 use the usual four speed zones (21, 19, 18 and 17 sectors a track, the 1571's second side repeating them); the 1581 has 40 sectors on every track. An address off the end of the image or past a track's last sector fails with `$E2`. From 08d99d59ede1911c533689201dee6492b30e55f9 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 18:44:56 -0500 Subject: [PATCH 040/100] firmware: serve the block device beside the USB mouse registers, and mount an image for it GeoRAM and the REU hand the free IO page to one dispatch that answers the mouse and the block device, and the main loop runs the card's side of each command. A launcher mounts the image; nothing mounts one yet. --- Source/Teensy/MinimalBoot/Common/IOHandlers.h | 1 + .../Common/IO_Handlers/IOH_GeoRAM.c | 3 +- .../MinimalBoot/Common/IO_Handlers/IOH_REU.c | 3 +- .../Common/IO_Handlers/TRBlockIO.c | 90 +++++++++++++++++++ Source/Teensy/Teensy.ino | 1 + 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index 0d6da33c..ae1d60b8 100644 --- a/Source/Teensy/MinimalBoot/Common/IOHandlers.h +++ b/Source/Teensy/MinimalBoot/Common/IOHandlers.h @@ -112,6 +112,7 @@ struct stcIOHandlers #include "IO_Handlers/IOH_Debug.c" #include "IO_Handlers/RAMBanks.c" #include "IO_Handlers/USBMouse.c" + #include "IO_Handlers/TRBlockIO.c" #include "IO_Handlers/IOH_GeoRAM.c" #ifdef Fab04_REU #include "IO_Handlers/IOH_REU.c" diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index afaeeb31..e13a0652 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -68,6 +68,7 @@ FLASHMEM void InitHndlr_GeoRAM() FreeGeoRAM(); GeoRAM_Block = GeoRAM_PageNum = 0; InitUSBMouse(); + InitTRBlock(); NumGeoRAM_Banks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); if (NumGeoRAM_Banks == 0) { @@ -96,7 +97,7 @@ void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn) { if (Address < GeoRAM_RegsBase) { - if (R_Wn) USBMouseIORead(Address); + TRPageIO(Address, R_Wn); return; } if (R_Wn) return; // write-only registers diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index 9a76752c..5bfbe97a 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -471,6 +471,7 @@ FLASHMEM void InitHndlr_REU() uint8_t REURegsInit[REUReg_NumRegs]={0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x1f, 0x3f}; memcpy(REURegs, REURegsInit, REUReg_NumRegs); InitUSBMouse(); + InitTRBlock(); #ifdef Direct_REU Printf_dbg_reu("Direct REU mode\n"); @@ -554,7 +555,7 @@ FLASHMEM void InitHndlr_REU() void IO1Hndlr_REU(uint8_t Address, bool R_Wn) { - if (R_Wn) USBMouseIORead(Address); + TRPageIO(Address, R_Wn); } void IO2Hndlr_REU(uint8_t Address, bool R_Wn) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c new file mode 100644 index 00000000..c11e667e --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +// The TR block device (docs/TR_Block_Device.md) on the IO page a RAM expansion handler leaves free, beside +// the USB mouse's registers. The ISR side only fills registers and the window; the SD card is touched +// only from PollTRBlock, in the main loop. + +#include "../../../BlockDevice/TRBlockImage.h" + +#define TRBlock_FlushIdlemS 250 + +static struct TRBlock TRBlockDev; +static struct TRBlockImage TRBlockImg; +static struct TRBlockBackend TRBlockBack; +static File TRBlockFile; +static bool TRBlockDirty; +static uint32_t TRBlockLastWritemS; + +static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) +{ + return TRBlockFile.seek(Offset) && TRBlockFile.read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; +} + +static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) +{ + if (!TRBlockFile.seek(Offset) || TRBlockFile.write(Block, GEOS_SECTOR_SIZE) != GEOS_SECTOR_SIZE) return false; + TRBlockDirty = true; + TRBlockLastWritemS = millis(); + return true; +} + +static bool TRBlockFileFlush(void *Context) +{ + TRBlockFile.flush(); + TRBlockDirty = false; + return true; +} + +FLASHMEM void TRBlockUnmount() +{ + TRBlockBack.Context = NULL; + if (TRBlockFile) TRBlockFile.close(); + TRBlockDirty = false; +} + +// The image stays mounted across handler restarts, so a launcher can mount it and then start the handler +FLASHMEM bool TRBlockMount(const char *Path) +{ + uint64_t Size; + + TRBlockUnmount(); + TRBlockFile = SD.open(Path, FILE_WRITE_BEGIN); + if (!TRBlockFile) return false; + Size = TRBlockFile.size(); + TRBlockImg = (struct TRBlockImage){ Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL, true, NULL, + TRBlockFileRead, TRBlockFileWrite, TRBlockFileFlush }; + if (!TRBlockImg.Format) + { + TRBlockFile.close(); + return false; + } + TRBlockBack = TRBlockImageBackend(&TRBlockImg); + return true; +} + +FLASHMEM void InitTRBlock() +{ + TRBlockReset(&TRBlockDev); +} + +void PollTRBlock() +{ + TRBlockService(&TRBlockDev, &TRBlockBack); + if (TRBlockDirty && millis() - TRBlockLastWritemS > TRBlock_FlushIdlemS) TRBlockFileFlush(NULL); +} + +// One IO access to the page the mouse registers start: the mouse's and the block device's offsets do not overlap +void TRPageIO(uint8_t Address, bool R_Wn) +{ + if (R_Wn) + { + USBMouseIORead(Address); + if (TRBlockServes(Address)) DataPortWriteWaitLog(TRBlockReadAt(&TRBlockDev, Address)); + } + else if (TRBlockTakes(Address)) + { + uint8_t Data = DataPortWaitRead(); + TRBlockWriteAt(&TRBlockDev, Address, Data); + TraceLogAddValidData(Data); + } +} diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index a585279d..c318ab9f 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -372,6 +372,7 @@ void loop() if (Serial.available()) ServiceSerial(&Serial); myusbHost.Task(); PollUSBMouse(); + PollTRBlock(); if (nfcState == nfcStateEnabled) nfcCheck(); From ae93ace37613f3127e636f17aa16afaf2a9e94bf Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:08:38 -0500 Subject: [PATCH 041/100] tools: a GEOS disk driver transport served by the TR block device The firmware places the code over a stock driver's turbo routines, so the generator marks every address in it for relocation. The 1581 header swap stays with the driver, since the stock drivers do it there. --- Source/Teensy/GEOS/TRDriver.h | 53 ++++++ tools/geos/trdrive.asm | 347 ++++++++++++++++++++++++++++++++++ tools/geos/trdrive.py | 75 ++++++++ tools/geos/trdrive.test.mjs | 31 +++ 4 files changed, 506 insertions(+) create mode 100644 Source/Teensy/GEOS/TRDriver.h create mode 100644 tools/geos/trdrive.asm create mode 100644 tools/geos/trdrive.py create mode 100644 tools/geos/trdrive.test.mjs diff --git a/Source/Teensy/GEOS/TRDriver.h b/Source/Teensy/GEOS/TRDriver.h new file mode 100644 index 00000000..00e7938d --- /dev/null +++ b/Source/Teensy/GEOS/TRDriver.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// Generated by tools/geos/trdrive.py from tools/geos/trdrive.asm; do not edit. +#pragma once +#include +#ifdef ARDUINO +#include +#else +#define PROGMEM +#endif + +#define TR_DRIVER_BASE 0x9000 +static const uint8_t TRDriverCode[481] PROGMEM = { + 0x08, 0x68, 0x8d, 0xd7, 0x91, 0x78, 0xa5, 0x01, 0x8d, 0xd8, 0x91, 0xa9, 0x36, 0x85, 0x01, 0x60, + 0x78, 0xad, 0xd8, 0x91, 0x85, 0x01, 0xad, 0xd7, 0x91, 0x48, 0x28, 0x60, 0xa2, 0x00, 0x8a, 0x60, + 0xa2, 0x00, 0xa0, 0x00, 0x60, 0x8d, 0x89, 0x84, 0x85, 0xba, 0xa2, 0x00, 0x60, 0x20, 0x7a, 0x90, + 0xe0, 0x00, 0xd0, 0x03, 0x20, 0x5c, 0x91, 0xa0, 0x00, 0x60, 0x20, 0x5c, 0x91, 0x20, 0xb1, 0x90, + 0x8a, 0x48, 0x20, 0x5c, 0x91, 0x68, 0xaa, 0xa0, 0x00, 0x60, 0x20, 0x8f, 0x91, 0xb0, 0x26, 0xa9, + 0x01, 0x20, 0xe9, 0x90, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, 0xa2, 0x0b, 0x20, 0xd0, 0x91, 0xa2, + 0x40, 0x20, 0xcc, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0xcc, 0x91, 0xc8, 0x91, 0x0a, + 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, 0x20, 0x8f, 0x91, 0xb0, 0xf6, 0xa9, + 0x01, 0x20, 0xe9, 0x90, 0xe0, 0x00, 0xd0, 0x28, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xdc, 0x91, 0xad, + 0xdc, 0x91, 0xa2, 0x0b, 0x20, 0xd0, 0x91, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0x91, 0x0a, 0xc8, 0xe8, + 0xe0, 0x40, 0xd0, 0xf5, 0xee, 0xdc, 0x91, 0xad, 0xdc, 0x91, 0xc9, 0x04, 0xd0, 0xe1, 0xa2, 0x00, + 0x60, 0x20, 0x8f, 0x91, 0xb0, 0xbf, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xdc, 0x91, 0xad, 0xdc, 0x91, + 0xa2, 0x0b, 0x20, 0xd0, 0x91, 0xa2, 0x00, 0xb1, 0x0a, 0x9d, 0x40, 0xdf, 0xdd, 0x40, 0xdf, 0xd0, + 0x15, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0xdc, 0x91, 0xad, 0xdc, 0x91, 0xc9, 0x04, 0xd0, + 0xdc, 0xa9, 0x02, 0x4c, 0xe9, 0x90, 0xa2, 0x0d, 0x60, 0x8d, 0xda, 0x91, 0xee, 0xdd, 0x91, 0xd0, + 0x03, 0xee, 0xdd, 0x91, 0xad, 0xdd, 0x91, 0xa2, 0x0c, 0x20, 0xd0, 0x91, 0xa5, 0x04, 0xa2, 0x09, + 0x20, 0xd0, 0x91, 0xa5, 0x05, 0xa2, 0x0a, 0x20, 0xd0, 0x91, 0xad, 0xda, 0x91, 0xa2, 0x08, 0x20, + 0xd0, 0x91, 0xa9, 0x00, 0x8d, 0xdf, 0x91, 0x8d, 0xe0, 0x91, 0xa2, 0x0d, 0x20, 0xcc, 0x91, 0x8d, + 0xdb, 0x91, 0x20, 0xcc, 0x91, 0xcd, 0xdb, 0x91, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0xdf, + 0x91, 0xd0, 0xe7, 0xee, 0xe0, 0x91, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0xcc, + 0x91, 0xcd, 0xdd, 0x91, 0xf0, 0x04, 0x68, 0x4c, 0x2e, 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, 0xd9, 0x91, 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0xd9, 0x91, 0x91, + 0x0a, 0x98, 0x38, 0xe9, 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xad, + 0xde, 0x91, 0xd0, 0x20, 0xa2, 0xdf, 0x20, 0xb8, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, 0xb8, 0x91, + 0xb0, 0x14, 0x8e, 0xde, 0x91, 0x8e, 0xce, 0x91, 0x8e, 0xd2, 0x91, 0x8e, 0x9b, 0x90, 0x8e, 0xcb, + 0x90, 0x8e, 0xce, 0x90, 0x18, 0x60, 0x38, 0x60, 0x8e, 0xbf, 0x91, 0xa0, 0x02, 0xb9, 0x10, 0xdf, + 0xd9, 0xd4, 0x91, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, 0xdf, 0x60, + 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00 +}; +static const uint16_t TRDriverRelocations[61] PROGMEM = { + 0x0003, 0x0009, 0x0012, 0x0017, 0x002e, 0x0035, 0x003b, 0x003e, 0x0043, 0x004b, 0x0052, 0x005d, 0x0062, 0x006b, 0x007b, 0x0082, + 0x008d, 0x0090, 0x0095, 0x00a5, 0x00a8, 0x00b2, 0x00bb, 0x00be, 0x00c3, 0x00d8, 0x00db, 0x00e4, 0x00ea, 0x00ed, 0x00f2, 0x00f5, + 0x00fa, 0x0101, 0x0108, 0x010b, 0x0110, 0x0115, 0x0118, 0x011d, 0x0120, 0x0123, 0x0126, 0x012f, 0x0134, 0x013f, 0x0142, 0x0148, + 0x0172, 0x017d, 0x0190, 0x0197, 0x019e, 0x01a3, 0x01a6, 0x01a9, 0x01ac, 0x01af, 0x01b2, 0x01b9, 0x01c1 +}; +static const uint16_t TRDriverEntries[13] PROGMEM = { + 0x0000, 0x0010, 0x001f, 0x001f, 0x001c, 0x0025, 0x0020, 0x002d, 0x003a, 0x0020, 0x0020, 0x0020, 0x004a +}; diff --git a/tools/geos/trdrive.asm b/tools/geos/trdrive.asm new file mode 100644 index 00000000..3cecdb8e --- /dev/null +++ b/tools/geos/trdrive.asm @@ -0,0 +1,347 @@ +; SPDX-License-Identifier: MIT +; The transport of a GEOS disk driver served by the TR block device (docs/TR_Block_Device.md). The firmware +; puts this over the stock driver's turbo code and points the driver's vectors here; build with acme. + +!ifndef ORG { ORG = $9000 } + +r1L = $04 +r1H = $05 +r4 = $0a +curDevice = $ba +curDrive = $8489 +driveType = $904e + +TYPE_1581 = 3 +DIR_1581_TRACK = 40 +HEADER_SWAP = $8c +HEADER_FIRST = 4 +HEADER_END = $1d + +; offsets from the base the TRM and TRB ids reveal +REG_CMD = $08 +REG_ADDR0 = $09 +REG_ADDR1 = $0a +REG_PAGE = $0b +REG_TOKEN = $0c +REG_STATUS = $0d +REG_ECHO = $0e +WINDOW = $40 +WINDOW_PAGES = 4 +WINDOW_SIZE = 64 + +CMD_READ = 1 +CMD_WRITE = 2 +STATUS_BUSY = $80 +STATUS_DONE = $5a +STATUS_BAD_ADDRESS = $e2 + +ERR_INV_TRACK = 2 +ERR_DEV_NOT_FOUND = 13 + +BASE_GEOS_RAM = $df +BASE_REU = $de + +* = ORG + +InitForIO + php + pla + sta savedP + sei + lda $01 + sta saved01 + lda #$36 + sta $01 + rts + +DoneWithIO + sei + lda saved01 + sta $01 + lda savedP + pha + plp + rts + +EnterTurbo + ldx #0 + txa +ExitTurbo +PurgeTurbo + rts + +NewDisk +VerWriteBlock +SendTSBytes +GetDOSError + ldx #0 + ldy #0 + rts + +ChangeDiskDevice + sta curDrive + sta curDevice + ldx #0 + rts + +ReadBlock + jsr readSector + cpx #0 + bne finished + jsr swapHeader +finished + ldy #0 + rts + +WriteBlock + jsr swapHeader + jsr writeSector + txa + pha + jsr swapHeader + pla + tax + ldy #0 + rts + +ReadLink + jsr findBase + bcs missing + lda #CMD_READ + jsr runCommand + cpx #0 + bne finished + lda #0 + ldx #REG_PAGE + jsr putReg + ldx #WINDOW + jsr getReg + ldy #0 + sta (r4),y + ldx #WINDOW + 1 + jsr getReg + iny + sta (r4),y + ldx #0 + ldy #0 + rts + +missing + ldx #ERR_DEV_NOT_FOUND + ldy #0 + rts + +readSector + jsr findBase + bcs missing + lda #CMD_READ + jsr runCommand + cpx #0 + bne readDone + ldy #0 + lda #0 + sta page +readPage + lda page + ldx #REG_PAGE + jsr putReg + ldx #0 +windowRead + lda BASE_GEOS_RAM * 256 + WINDOW,x + sta (r4),y + iny + inx + cpx #WINDOW_SIZE + bne windowRead + inc page + lda page + cmp #WINDOW_PAGES + bne readPage + ldx #0 +readDone + rts + +writeSector + jsr findBase + bcs missing + ldy #0 + lda #0 + sta page +writePage + lda page + ldx #REG_PAGE + jsr putReg + ldx #0 +windowWrite + lda (r4),y +windowStore + sta BASE_GEOS_RAM * 256 + WINDOW,x +windowCheck + cmp BASE_GEOS_RAM * 256 + WINDOW,x + bne windowFault + iny + inx + cpx #WINDOW_SIZE + bne windowWrite + inc page + lda page + cmp #WINDOW_PAGES + bne writePage + lda #CMD_WRITE + jmp runCommand +windowFault + ldx #ERR_DEV_NOT_FOUND + rts + +; A = command; returns the GEOS error in X +runCommand + sta command + inc token + bne tokenReady + inc token +tokenReady + lda token + ldx #REG_TOKEN + jsr putReg + lda r1L + ldx #REG_ADDR0 + jsr putReg + lda r1H + ldx #REG_ADDR1 + jsr putReg + lda command + ldx #REG_CMD + jsr putReg + lda #0 + sta patience + sta patience + 1 +poll + ldx #REG_STATUS + jsr getReg + sta seen + jsr getReg + cmp seen + bne again + cmp #STATUS_BUSY + bne decided +again + inc patience + bne poll + inc patience + 1 + bne poll + ldx #ERR_DEV_NOT_FOUND + rts +decided + pha + ldx #REG_ECHO + jsr getReg + cmp token + beq ours + pla + jmp again +ours + pla + cmp #STATUS_DONE + beq succeeded + cmp #STATUS_BAD_ADDRESS + beq badTrack + ldx #ERR_DEV_NOT_FOUND + rts +succeeded + ldx #0 + rts +badTrack + ldx #ERR_INV_TRACK + rts + +; the 1581 header sector keeps its fields where the 1541's are +swapHeader + lda driveType + cmp #TYPE_1581 + bne swapped + lda r1L + cmp #DIR_1581_TRACK + bne swapped + lda r1H + bne swapped + ldy #HEADER_FIRST +swapField + lda (r4),y + sta scratch + tya + clc + adc #HEADER_SWAP + tay + lda (r4),y + pha + lda scratch + sta (r4),y + tya + sec + sbc #HEADER_SWAP + tay + pla + sta (r4),y + iny + cpy #HEADER_END + bne swapField +swapped + rts + +; carry clear when the TRB id answers at a base: first the page GeoRAM leaves free, then the REU's +findBase + lda baseHi + bne located + ldx #BASE_GEOS_RAM + jsr tryBase + bcc settle + ldx #BASE_REU + jsr tryBase + bcs absent +settle + stx baseHi + stx getReg + 2 + stx putReg + 2 + stx windowRead + 2 + stx windowStore + 2 + stx windowCheck + 2 +located + clc + rts +absent + sec + rts + +tryBase + stx probe + 2 + ldy #2 +probe + lda BASE_GEOS_RAM * 256 + $10,y + cmp trbId,y + bne notHere + dey + bpl probe + clc + rts +notHere + sec + rts + +getReg + lda BASE_GEOS_RAM * 256,x + rts + +putReg + sta BASE_GEOS_RAM * 256,x + rts + +trbId !byte $54, $52, $42 +savedP !byte 0 +saved01 !byte 0 +scratch !byte 0 +command !byte 0 +seen !byte 0 +page !byte 0 +token !byte 0 +baseHi !byte 0 +patience !word 0 diff --git a/tools/geos/trdrive.py b/tools/geos/trdrive.py new file mode 100644 index 00000000..d2f0c2e4 --- /dev/null +++ b/tools/geos/trdrive.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Emit the TR block device driver for the firmware. + + python3 tools/geos/trdrive.py out.h + +The driver is assembled twice, a page and a byte apart, and every 16-bit word that moved is an +address of its own, so the firmware can place the code wherever the stock driver has room. Needs acme. +""" + +import pathlib +import re +import subprocess +import sys +import tempfile + +SOURCE = pathlib.Path(__file__).with_name('trdrive.asm') +BASE = 0x9000 +SHIFTED = 0x0101 +ENTRIES = ('InitForIO', 'DoneWithIO', 'ExitTurbo', 'PurgeTurbo', 'EnterTurbo', 'ChangeDiskDevice', 'NewDisk', + 'ReadBlock', 'WriteBlock', 'VerWriteBlock', 'SendTSBytes', 'GetDOSError', 'ReadLink') + + +def assemble(org, tmp): + out, labels = tmp / f'{org:x}.bin', tmp / f'{org:x}.lbl' + subprocess.run(['acme', f'-DORG={org}', '-f', 'plain', '-o', str(out), '-l', str(labels), str(SOURCE)], + check=True) + return out.read_bytes(), labels.read_text() + + +def words(code): + return [code[i] | code[i + 1] << 8 for i in range(len(code) - 1)] + + +def relocations(low, high): + moved = [i for i, (a, b) in enumerate(zip(words(low), words(high))) if (b - a) & 0xffff == SHIFTED] + return [i for n, i in enumerate(moved) if n == 0 or i != moved[n - 1] + 1] + + +def entry_offsets(labels): + found = {m.group(1): int(m.group(2), 16) - BASE for m in re.finditer(r'^\s*(\w+)\s*=\s*\$([0-9a-f]+)', labels, re.M)} + return [found[name] for name in ENTRIES] + + +def c_array(name, values, width): + rows = [values[i:i + 16] for i in range(0, len(values), 16)] + body = ',\n'.join(' ' + ', '.join(f'0x{v:0{width}x}' for v in row) for row in rows) + return f'static const uint{width * 4}_t {name}[{len(values)}] PROGMEM = {{\n{body}\n}};\n' + + +def header(): + with tempfile.TemporaryDirectory() as tmp: + low, labels = assemble(BASE, pathlib.Path(tmp)) + high, _ = assemble(BASE + SHIFTED, pathlib.Path(tmp)) + relocs = relocations(low, high) + return ( + '// SPDX-License-Identifier: MIT\n' + '// Generated by tools/geos/trdrive.py from tools/geos/trdrive.asm; do not edit.\n' + '#pragma once\n' + '#include \n' + '#ifdef ARDUINO\n' + '#include \n' + '#else\n' + '#define PROGMEM\n' + '#endif\n\n' + f'#define TR_DRIVER_BASE 0x{BASE:04x}\n' + + c_array('TRDriverCode', list(low), 2) + + c_array('TRDriverRelocations', relocs, 4) + + c_array('TRDriverEntries', entry_offsets(labels), 4)) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + sys.exit(__doc__) + pathlib.Path(sys.argv[1]).write_text(header()) diff --git a/tools/geos/trdrive.test.mjs b/tools/geos/trdrive.test.mjs new file mode 100644 index 00000000..88ec892c --- /dev/null +++ b/tools/geos/trdrive.test.mjs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +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 { fileURLToPath } from 'node:url'; +import { commandExists } from '../lib/toolchain.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const trdrive = path.join(root, 'tools/geos/trdrive.py'); +const committedHeader = path.join(root, 'Source/Teensy/GEOS/TRDriver.h'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['acme', 'python3'].find((tool) => !commandExists(tool)); + +test('the committed disk driver header matches the driver source', { skip: missingTool && !inCi && `${missingTool} is not on PATH` }, () => { + assert.equal(missingTool, undefined, 'CI installs ACME and Python, so this test must not skip there'); + const out = fs.mkdtempSync(path.join(os.tmpdir(), 'trdrive-test-')); + try { + const header = path.join(out, 'TRDriver.h'); + const r = spawnSync('python3', [trdrive, header], { encoding: 'utf8' }); + assert.equal(r.status, 0, r.stdout + r.stderr); + const committed = fs.readFileSync(committedHeader, 'utf8').replaceAll('\r\n', '\n'); + assert.equal(fs.readFileSync(header, 'utf8'), committed, 'rerun tools/geos/trdrive.py'); + } finally { + fs.rmSync(out, { recursive: true, force: true }); + } +}); From f46a67d324aeb92de34d41692624c2e6b0078ead Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:13:33 -0500 Subject: [PATCH 042/100] tools: build the TR USB mouse driver for GEOS 64 It is the 128 driver loaded at the 64's input driver address, with the 64's input driver type, so one source serves both. --- Source/Teensy/GEOS/TRMouse64.h | 46 ++++++++++++++++++++++++++++++++++ tools/geos/trmouse.py | 33 +++++++++++++----------- tools/geos/trmouse.test.mjs | 30 +++++++++++----------- tools/geos/trmouse128.asm | 5 ++-- 4 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 Source/Teensy/GEOS/TRMouse64.h diff --git a/Source/Teensy/GEOS/TRMouse64.h b/Source/Teensy/GEOS/TRMouse64.h new file mode 100644 index 00000000..3f211beb --- /dev/null +++ b/Source/Teensy/GEOS/TRMouse64.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit. +#pragma once +#include +#ifdef ARDUINO +#include +#else +#define PROGMEM +#endif + +static const char TRMouse64Name[] PROGMEM = "TR USB MOUSE"; +static const uint8_t TRMouse64Code[226] PROGMEM = { + 0x4c, 0x90, 0xfe, 0x4c, 0x9a, 0xfe, 0x4c, 0xc2, 0xfe, 0x4c, 0x9a, 0xfe, 0x80, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa0, 0xdf, 0x8c, 0xa4, 0xfe, + 0xa2, 0x02, 0xbd, 0x03, 0xdf, 0xdd, 0xbf, 0xfe, 0xd0, 0x0e, 0xca, 0x10, 0xf5, 0x8c, 0xc9, 0xfe, + 0x8c, 0xcf, 0xfe, 0x8c, 0xda, 0xfe, 0x18, 0x60, 0x88, 0xc0, 0xdd, 0xd0, 0xe0, 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, 0x24, 0x3f, 0x10, 0x06, 0x0e, 0x8d, 0xfe, 0x2e, 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, 0x59, 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, 0x62, 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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x41, 0x20, 0x55, 0x53, 0x42, 0x20, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x20, 0x70, 0x6c, 0x75, 0x67, + 0x67, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x54, 0x65, 0x65, 0x6e, 0x73, + 0x79, 0x52, 0x4f, 0x4d, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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.py b/tools/geos/trmouse.py index 178d51d0..a4b06402 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: MIT -"""Put the TR USB mouse driver on a GEOS 128 boot disk. +"""Put the TR USB mouse driver on a GEOS 128 (or, with --target 64, GEOS 64) boot disk. python3 tools/geos/trmouse.py boot.d81 out.d81 (or a .d64 or .d71) python3 tools/geos/trmouse.py --c-header out.h (the driver file, for the firmware) +Add --target 64 to either form for GEOS 64. + GEOS loads the first input driver in the boot disk's directory, so the driver goes into the entry ahead of every other one, and the input driver that was first moves to the slot the new file would have taken. Needs acme on PATH. @@ -21,15 +23,15 @@ NAME = b'TR USB MOUSE' CLASS_NAME = b'TR USB Mouse' CLASS_NAME_SIZE = 12 # GEOS reads the version right after it -LOAD = 0xfd00 -MAX_SIZE = 0x180 # GEOS 128's input driver area, $FD00-$FE7F +TARGETS = {'128': (0xfd00, 15), '64': (0xfe80, 10)} # where GEOS loads an input driver, and its type +LOAD, INPUT_TYPE = TARGETS['128'] +MAX_SIZE = 0x180 # the input driver area, $FD00-$FE7F on GEOS 128 and $FE80-$FFFF on GEOS 64 SECTOR_SIZE = 256 DIR_ENTRY_SIZE = 32 USR_CLOSED = 0x83 GEOS_SEQUENTIAL = 0 -INPUT_128 = 15 INFO_ICON_BITMAP = 0xbf # an uncompressed 63-byte icon follows ICON = """ @@ -60,10 +62,10 @@ def assemble(): with tempfile.TemporaryDirectory() as tmp: out = pathlib.Path(tmp) / 'trmouse128.bin' - subprocess.run(['acme', '-f', 'plain', '-o', str(out), str(SOURCE)], check=True) + subprocess.run(['acme', f'-DMOUSE_BASE={LOAD}', '-f', 'plain', '-o', str(out), str(SOURCE)], check=True) code = out.read_bytes() if len(code) > MAX_SIZE: - sys.exit(f'the driver is {len(code)} bytes; GEOS 128 has room for {MAX_SIZE}') + sys.exit(f'the driver is {len(code)} bytes; GEOS has room for {MAX_SIZE}') return code @@ -229,7 +231,7 @@ def info_block(size): info[0:2] = b'\x00\xff' info[2:5] = bytes((3, 21, INFO_ICON_BITMAP)) info[5:0x44] = icon_bits() - info[0x44:0x47] = bytes((USR_CLOSED, INPUT_128, GEOS_SEQUENTIAL)) + info[0x44:0x47] = bytes((USR_CLOSED, INPUT_TYPE, GEOS_SEQUENTIAL)) info[0x47:0x49] = LOAD.to_bytes(2, 'little') info[0x49:0x4b] = (LOAD + size).to_bytes(2, 'little') info[0x4d:0x61] = padded(padded(CLASS_NAME, CLASS_NAME_SIZE, b' ') + b'V1.0', 20) @@ -257,12 +259,12 @@ def install(image, code): entry[1:3] = bytes(first) entry[3:19] = padded(NAME, 16, b'\xa0') entry[19:21] = bytes(info_ts) - entry[21:23] = bytes((GEOS_SEQUENTIAL, INPUT_128)) + entry[21:23] = bytes((GEOS_SEQUENTIAL, INPUT_TYPE)) entry[23:28] = bytes((now.year % 100, now.month, now.day, now.hour, now.minute)) entry[28:30] = blocks.to_bytes(2, 'little') disk.image[free + 2:free + 32] = entry - drivers = [s for s in entries if disk.image[s + 24] == INPUT_128] + drivers = [s for s in entries if disk.image[s + 24] == INPUT_TYPE] if drivers and slots.index(drivers[0]) < slots.index(free): a, b = drivers[0], free disk.image[a + 2:a + 32], disk.image[b + 2:b + 32] = \ @@ -276,7 +278,7 @@ def c_array(name, data): return f'static const uint8_t {name}[{len(data)}] PROGMEM = {{\n{body}\n}};\n' -def c_header(code): +def c_header(code, target): return ( '// SPDX-License-Identifier: MIT\n' '// Generated by tools/geos/trmouse.py --c-header from tools/geos/trmouse128.asm; do not edit.\n' @@ -287,9 +289,9 @@ def c_header(code): '#else\n' '#define PROGMEM\n' '#endif\n\n' - f'static const char TRMouse128Name[] PROGMEM = "{NAME.decode()}";\n' - + c_array('TRMouse128Code', code) - + c_array('TRMouse128Info', info_block(len(code)))) + f'static const char TRMouse{target}Name[] PROGMEM = "{NAME.decode()}";\n' + + c_array(f'TRMouse{target}Code', code) + + c_array(f'TRMouse{target}Info', info_block(len(code)))) def main(): @@ -297,11 +299,14 @@ def main(): parser.add_argument('disk', type=pathlib.Path, nargs='?') parser.add_argument('out', type=pathlib.Path, nargs='?') parser.add_argument('--c-header', type=pathlib.Path, metavar='OUT') + parser.add_argument('--target', choices=TARGETS, default='128') args = parser.parse_args() + global LOAD, INPUT_TYPE + LOAD, INPUT_TYPE = TARGETS[args.target] if args.c_header: if args.disk: parser.error('--c-header takes no disk images') - args.c_header.write_bytes(c_header(assemble()).encode()) + args.c_header.write_bytes(c_header(assemble(), args.target).encode()) return if not args.out: parser.error('needs a boot disk and an output image') diff --git a/tools/geos/trmouse.test.mjs b/tools/geos/trmouse.test.mjs index ed13d1ca..64e006b4 100644 --- a/tools/geos/trmouse.test.mjs +++ b/tools/geos/trmouse.test.mjs @@ -10,22 +10,24 @@ import { commandExists } from '../lib/toolchain.mjs'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const trmouse = path.join(root, 'tools/geos/trmouse.py'); -const committedHeader = path.join(root, 'Source/Teensy/GEOS/TRMouse128.h'); +const committedHeaders = { 128: 'Source/Teensy/GEOS/TRMouse128.h', 64: 'Source/Teensy/GEOS/TRMouse64.h' }; const NOT_CI = ['', '0', 'false']; const inCi = !NOT_CI.includes(process.env.CI ?? ''); const missingTool = ['acme', 'python3'].find((tool) => !commandExists(tool)); -test('the committed driver header matches the driver source', { skip: missingTool && !inCi && `${missingTool} is not on PATH` }, () => { - assert.equal(missingTool, undefined, 'CI installs ACME and Python, so this test must not skip there'); - const out = fs.mkdtempSync(path.join(os.tmpdir(), 'trmouse-test-')); - try { - const header = path.join(out, 'TRMouse128.h'); - const r = spawnSync('python3', [trmouse, '--c-header', header], { encoding: 'utf8' }); - assert.equal(r.status, 0, r.stdout + r.stderr); - const committed = fs.readFileSync(committedHeader, 'utf8').replaceAll('\r\n', '\n'); - assert.equal(fs.readFileSync(header, 'utf8'), committed, 'rerun tools/geos/trmouse.py --c-header'); - } finally { - fs.rmSync(out, { recursive: true, force: true }); - } -}); +for (const [target, committedPath] of Object.entries(committedHeaders)) { + test(`the committed GEOS ${target} driver header matches the driver source`, { skip: missingTool && !inCi && `${missingTool} is not on PATH` }, () => { + assert.equal(missingTool, undefined, 'CI installs ACME and Python, so this test must not skip there'); + const out = fs.mkdtempSync(path.join(os.tmpdir(), 'trmouse-test-')); + try { + const header = path.join(out, 'TRMouse.h'); + const r = spawnSync('python3', [trmouse, '--target', target, '--c-header', header], { encoding: 'utf8' }); + assert.equal(r.status, 0, r.stdout + r.stderr); + const committed = fs.readFileSync(path.join(root, committedPath), 'utf8').replaceAll('\r\n', '\n'); + assert.equal(fs.readFileSync(header, 'utf8'), committed, `rerun tools/geos/trmouse.py --target ${target} --c-header`); + } finally { + fs.rmSync(out, { recursive: true, force: true }); + } + }); +} diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index d18bcd93..56423f17 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -1,6 +1,6 @@ ; SPDX-License-Identifier: MIT ; -; GEOS 128 input driver for a USB mouse on a TeensyROM (USBMouse.c has the registers). +; GEOS input driver (128, or 64 assembled at its own address) for a USB mouse on a TeensyROM (USBMouse.c has the registers). !cpu 6502 @@ -23,7 +23,8 @@ regButtons = 2 regID = 3 LEFT_BUTTON = $01 - * = $fd00 ; where GEOS 128 loads an input driver +!ifndef MOUSE_BASE { MOUSE_BASE = $fd00 } + * = MOUSE_BASE ; where GEOS loads an input driver jmp MouseInit jmp SlowMouse From f881e79634fa25d51d104ba6a83616829905fa5f Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:13:33 -0500 Subject: [PATCH 043/100] firmware: build a PRG that starts GEOS from a boot disk image The stock start-up file LOADs the boot file from a drive, so the PRG carries the boot file itself with the disk driver's transport replaced by the TR's. CONFIGURE files are retyped so GEOS does not probe an empty IEC bus and leave drive A without a disk. --- Source/Teensy/GEOS/GeosBoot.h | 196 +++++++++++++++++++++ Source/Teensy/GEOS/GeosDiskPatch.h | 2 + Source/Teensy/GEOS/GeosTargets.h | 15 +- Source/Teensy/tests/geos-boot.test.js | 216 ++++++++++++++++++++++++ Source/Teensy/tests/geos_boot_harness.c | 56 ++++++ 5 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 Source/Teensy/GEOS/GeosBoot.h create mode 100644 Source/Teensy/tests/geos-boot.test.js create mode 100644 Source/Teensy/tests/geos_boot_harness.c diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h new file mode 100644 index 00000000..50921f7b --- /dev/null +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +// What launching a GEOS boot disk from the TR needs of its image: the boot file with the disk driver swapped for +// the TR's, and the disk's own auto-configuration kept from probing a drive that is not there. +#pragma once +#include "GeosDiskPatch.h" +#include "TRDriver.h" + +#define GEOS_DRIVER_AREA 0x9000 +#define GEOS_DRIVER_SIZE 0xd80 +#define GEOS_VECTOR_WORDS 24 +#define GEOS_JUMP_SLOTS 0x30 +#define GEOS_JUMP_COUNT 10 +#define GEOS_TYPE_BYTE 0x4e +#define GEOS_JUMP_OPCODE 0x4c +#define GEOS_AUTO_EXEC 14 +#define GEOS_SYSTEM_FILE 5 +#define GEOS_CONFIG_PREFIX "CONFIG" +#define GEOS_BASIC_START 0x0801 +#define GEOS_MAX_FILE_BLOCKS 3200 +#define GEOS_DEVICE_8 8 + +enum GeosDriverVector { VecInitForIO, VecDoneWithIO, VecExitTurbo, VecPurgeTurbo, VecEnterTurbo, VecChangeDiskDevice, + VecNewDisk, VecReadBlock, VecWriteBlock, VecVerWriteBlock, VecCount = 10 }; +enum GeosDriverJump { JumpSendTSBytes = 6, JumpGetDOSError = 7, JumpReadLink = 9 }; +enum TRDriverEntry { EntrySendTSBytes = 10, EntryGetDOSError, EntryReadLink }; + +static uint16_t GeosWord(const uint8_t *At) { return At[0] | At[1] << 8; } + +static void GeosPutWord(uint8_t *At, uint16_t Value) +{ + At[0] = Value & 0xff; + At[1] = Value >> 8; +} + +static bool GeosInDriverArea(uint16_t Address) { return Address >= GEOS_DRIVER_AREA && Address < GEOS_DRIVER_AREA + GEOS_DRIVER_SIZE; } + +// 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) +{ + uint8_t Type = At[GEOS_TYPE_BYTE]; + + for (int i = 0; i < GEOS_VECTOR_WORDS; i++) + { + if (!GeosInDriverArea(GeosWord(At + 2 * i))) return false; + } + for (int i = 0; i < GEOS_JUMP_COUNT; i++) + { + const uint8_t *Slot = At + GEOS_JUMP_SLOTS + 3 * i; + if (Slot[0] != GEOS_JUMP_OPCODE || !GeosInDriverArea(GeosWord(Slot + 1))) return false; + } + return Type >= 1 && Type <= 3; +} + +// Offset of the stock driver's image in a boot file, or -1 +static int32_t GeosFindDriver(const uint8_t *Data, uint32_t Size) +{ + for (uint32_t At = 0; At + GEOS_DRIVER_SIZE <= Size; At++) + { + if (GeosIsDriverAt(Data + At)) return At; + } + return -1; +} + +// The stock driver's turbo routines occupy one run of its code, from InitForIO up to ReadBlock; the TR's +// driver replaces them and points every vector it implements at its own routines +static bool GeosReplaceTransport(uint8_t *Driver) +{ + uint16_t Start = GeosWord(Driver + 2 * VecInitForIO), End = GeosWord(Driver + 2 * VecReadBlock); + uint8_t Code[sizeof TRDriverCode]; + + if (End <= Start || End - Start < sizeof Code) return false; + for (int i = VecDoneWithIO; i <= VecNewDisk; i++) + { + uint16_t Routine = GeosWord(Driver + 2 * i); + if (Routine < Start || Routine >= End) return false; + } + memcpy(Code, TRDriverCode, sizeof Code); + for (size_t i = 0; i < sizeof TRDriverRelocations / sizeof TRDriverRelocations[0]; i++) + { + uint8_t *Word = Code + TRDriverRelocations[i]; + GeosPutWord(Word, GeosWord(Word) + Start - TR_DRIVER_BASE); + } + memcpy(Driver + (Start - GEOS_DRIVER_AREA), Code, sizeof Code); + for (int i = 0; i < VecCount; i++) GeosPutWord(Driver + 2 * i, Start + TRDriverEntries[i]); + GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpSendTSBytes + 1, Start + TRDriverEntries[EntrySendTSBytes]); + GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpGetDOSError + 1, Start + TRDriverEntries[EntryGetDOSError]); + GeosPutWord(Driver + GEOS_JUMP_SLOTS + 3 * JumpReadLink + 1, Start + TRDriverEntries[EntryReadLink]); + return true; +} + +// A file's bytes, found by name in the directory and followed along its sector chain +static bool GeosReadFile(struct GeosDisk *Disk, const char *Name, uint8_t *Out, uint32_t Capacity, uint32_t *Length) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS], At = { 0, 0 }; + uint8_t Block[GEOS_SECTOR_SIZE]; + struct GeosDirScan Scan; + int Count; + + if (!GeosReadDirectory(Disk, Chain, &Count, &Scan)) return false; + for (int i = 0; i < Count && !At.Track; i++) + { + if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; + for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) + { + const uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; + if (!(Slot[SlotType] & CBM_TYPE_MASK) || !GeosSlotNamed(Slot, Name)) continue; + At = (struct GeosTS){ Slot[SlotFirst], Slot[SlotFirst + 1] }; + break; + } + } + if (!At.Track) return GeosFail(Disk, "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, "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, "the boot file is too large"); + memcpy(Out + *Length, Block + 2, Used); + *Length += Used; + At = (struct GeosTS){ Block[0], Block[1] }; + } + return true; +} + +// 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 bool GeosDisableConfigure(struct GeosDisk *Disk, bool *Changed) +{ + struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; + uint8_t Block[GEOS_SECTOR_SIZE]; + struct GeosDirScan Scan; + int Count; + + *Changed = false; + if (!GeosReadDirectory(Disk, Chain, &Count, &Scan)) return false; + for (int i = 0; i < Count; i++) + { + bool Retyped = false; + 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; + if (strncmp((char *)Slot + SlotName, GEOS_CONFIG_PREFIX, strlen(GEOS_CONFIG_PREFIX))) continue; + Slot[SlotGeosType] = GEOS_SYSTEM_FILE; + Retyped = true; + } + if (!Retyped) continue; + if (!GeosWriteTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; + *Changed = true; + } + return true; +} + +// BASIC "10 SYS 2061" and the stub it runs: the screen colors and drive 8 the stock GEOS start-up file sets, then +// the boot file's entry. The stock file would LOAD the boot file from the drive; here it arrives in the same PRG. +static const uint8_t GeosBasicLine[] = { 0x0b, 0x08, 0x0a, 0x00, 0x9e, '2', '0', '6', '1', 0x00, 0x00, 0x00 }; +static const uint8_t GeosBootStub[] = +{ + 0xa9, 0x0e, 0x8d, 0x20, 0xd0, // lda #$0e : sta $d020 + 0xa0, 0x06, 0x8c, 0x21, 0xd0, // ldy #$06 : sty $d021 + 0xa9, GEOS_DEVICE_8, 0x85, 0xba, // lda #8 : sta $ba + 0x4c, 0x00, 0x00, // jmp entry +}; + +// One PRG that starts GEOS: the boot file with the disk driver's transport replaced, placed where its load +// address puts it, behind a BASIC line that jumps to the boot entry +static bool GeosBuildBootPrg(struct GeosDisk *Disk, uint8_t *Out, uint32_t Capacity, uint32_t *Length) +{ + const uint32_t Prologue = 2 + sizeof GeosBasicLine + sizeof GeosBootStub; + uint32_t FileSize, BodySize, Load, Place; + int32_t Driver; + uint8_t *Body = Out + 2; + + if (!GeosReadFile(Disk, Disk->Target->BootFile, Out, Capacity, &FileSize)) return false; + if (FileSize < 2 + GEOS_DRIVER_SIZE) return GeosFail(Disk, "the boot file is too short"); + BodySize = FileSize - 2; + Load = GeosWord(Out); + Driver = GeosFindDriver(Body, BodySize); + if (Driver < 0 || !GeosReplaceTransport(Body + Driver)) return GeosFail(Disk, "the boot file's disk driver is not one the TR can replace"); + Place = 2 + Load - GEOS_BASIC_START; + if (Load < GEOS_BASIC_START + Prologue - 2 || Place + BodySize > Capacity) return GeosFail(Disk, "the boot file does not fit in memory"); + + memmove(Out + Place, Body, BodySize); + memset(Out, 0, Place); + GeosPutWord(Out, GEOS_BASIC_START); + memcpy(Out + 2, GeosBasicLine, sizeof GeosBasicLine); + memcpy(Out + 2 + sizeof GeosBasicLine, GeosBootStub, sizeof GeosBootStub); + GeosPutWord(Out + Prologue - 2, Disk->Target->BootEntry); + *Length = Place + BodySize; + return true; +} diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 181dff72..f0d8b23f 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -56,6 +56,8 @@ struct GeosTarget const uint8_t *DriverCode, *DriverInfo; uint16_t DriverSize; uint8_t InputType; + const char *BootFile; // what the TR starts the disk from; none for a version it only sets up + uint16_t BootEntry; }; struct GeosBootDisk { bool HasDeskTop, HasDriver; }; diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h index 144985eb..7017e22c 100644 --- a/Source/Teensy/GEOS/GeosTargets.h +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -3,8 +3,12 @@ #pragma once #include "GeosDiskPatch.h" #include "TRMouse128.h" +#include "TRMouse64.h" #define GEOS_INPUT_128 15 +#define GEOS_INPUT_64 10 +#define GEOS64_BOOT_FILE "GEOBOOT" +#define GEOS64_BOOT_ENTRY 0x2f3c struct GeosLauncher { @@ -12,8 +16,17 @@ struct GeosLauncher struct GeosTarget Target; }; +static inline bool GeosBoots(const struct GeosLauncher *Launcher) { return Launcher->Target.BootFile != NULL; } + static const struct GeosLauncher Geos128Launcher = { "GEOS 128", "/GEOS128", - { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128 }, + { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, NULL, 0 }, +}; + +static const struct GeosLauncher Geos64Launcher = +{ + "GEOS 64", "/GEOS64", + { "DESK TOP", TRMouse64Name, TRMouse64Code, TRMouse64Info, sizeof TRMouse64Code, GEOS_INPUT_64, + GEOS64_BOOT_FILE, GEOS64_BOOT_ENTRY }, }; diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js new file mode 100644 index 00000000..b208186c --- /dev/null +++ b/Source/Teensy/tests/geos-boot.test.js @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +// The firmware's GEOS 64 boot PRG and CONFIGURE handling, run on synthetic D81 boot disks. +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 FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); +const HARNESS_SOURCE = path.join(FIRMWARE_ROOT, 'tests/geos_boot_harness.c'); +const DRIVER_HEADER = path.join(FIRMWARE_ROOT, 'GEOS/TRDriver.h'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['cc', 'c++'].find((tool) => !commandExists(tool)); +const skip = missingTool && !inCi && `${missingTool} is not on PATH`; + +const SECTOR = 256; +const SECTORS_PER_TRACK = 40; +const TRACKS = 80; +const DIR_TRACK = 40; +const FIRST_DIR_SECTOR = 3; +const FIRST_FILE_TRACK = 1; +const DIR_SLOT = 32; +const SLOT_TYPE = 2; +const SLOT_FIRST = 3; +const SLOT_NAME = 5; +const SLOT_GEOS_TYPE = 24; +const PAD = 0xa0; +const USR_CLOSED = 0x83; +const AUTO_EXEC = 14; +const SYSTEM_FILE = 5; + +const DRIVER_AREA = 0x9000; +const DRIVER_SIZE = 0xd80; +const TURBO_START = 0x9100; +const READ_BLOCK = 0x9400; +const TYPE_BYTE = 0x4e; +const JUMP_SLOTS = 0x30; +const JUMP_COUNT = 10; +const LOAD_ADDRESS = 0x2c98; +const DRIVER_AT_IN_BODY = 0x4400; +const BODY_SIZE = 0x6000; +const BASIC_START = 0x0801; + +const header = fs.readFileSync(DRIVER_HEADER, 'utf8'); +const arrayOf = (name) => [...header.match(new RegExp(`${name}\\[\\d+\\] PROGMEM = \\{([^}]*)\\}`))[1].matchAll(/0x[0-9a-f]+/g)].map((m) => parseInt(m[0], 16)); +const CODE = arrayOf('TRDriverCode'); +const RELOCATIONS = arrayOf('TRDriverRelocations'); +const ENTRIES = arrayOf('TRDriverEntries'); + +const offsetOf = (track, sector) => ((track - 1) * SECTORS_PER_TRACK + sector) * SECTOR; + +function stockDriver() { + const driver = Buffer.alloc(DRIVER_SIZE, 0xea); + const vectors = [TURBO_START, 0x9140, 0x9180, 0x91c0, 0x9200, 0x9240, 0x9280, READ_BLOCK, 0x9500, 0x9540]; + for (let i = 0; i < 24; i++) driver.writeUInt16LE(vectors[i] ?? 0x9600 + i, 2 * i); + for (let i = 0; i < JUMP_COUNT; i++) { + driver[JUMP_SLOTS + 3 * i] = 0x4c; + driver.writeUInt16LE(0x9700 + i, JUMP_SLOTS + 3 * i + 1); + } + driver[TYPE_BYTE] = 3; + return driver; +} + +function bootFile(driver = stockDriver()) { + const body = Buffer.alloc(BODY_SIZE); + for (let i = 0; i < BODY_SIZE; i++) body[i] = (i * 31 + 7) & 0xff; + driver.copy(body, DRIVER_AT_IN_BODY); + const file = Buffer.alloc(2 + BODY_SIZE); + file.writeUInt16LE(LOAD_ADDRESS, 0); + body.copy(file, 2); + return { file, body }; +} + +class Disk { + constructor() { + this.image = Buffer.alloc(TRACKS * SECTORS_PER_TRACK * SECTOR); + this.nextSector = 0; + this.slots = []; + const header = offsetOf(DIR_TRACK, 0); + this.image[header] = DIR_TRACK; + this.image[header + 1] = FIRST_DIR_SECTOR; + this.image[offsetOf(DIR_TRACK, FIRST_DIR_SECTOR) + 1] = 0xff; + } + + addFile(name, data, geosType = 0) { + const sectors = []; + for (let at = 0; at < data.length || !sectors.length; at += SECTOR - 2) sectors.push(this.nextSector++); + sectors.forEach((n, i) => { + const track = FIRST_FILE_TRACK + Math.floor(n / SECTORS_PER_TRACK); + const o = offsetOf(track, n % SECTORS_PER_TRACK); + const chunk = data.subarray(i * (SECTOR - 2), (i + 1) * (SECTOR - 2)); + const last = i === sectors.length - 1; + const next = sectors[i + 1]; + this.image[o] = last ? 0 : FIRST_FILE_TRACK + Math.floor(next / SECTORS_PER_TRACK); + this.image[o + 1] = last ? chunk.length + 1 : next % SECTORS_PER_TRACK; + chunk.copy(this.image, o + 2); + }); + 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.fill(PAD, slot + SLOT_NAME, slot + SLOT_NAME + 16); + this.image.write(name, slot + SLOT_NAME, 'latin1'); + this.image[slot + SLOT_GEOS_TYPE] = geosType; + this.slots.push(slot); + return slot; + } +} + +function run(args) { + const r = spawnSync(args[0], args.slice(1), { encoding: 'utf8' }); + return { status: r.status, out: r.stdout, err: r.stderr }; +} + +function withHarness(body) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'geos-boot-')); + try { + const built = run(['cc', '-std=c99', '-Wall', '-Wextra', '-Werror', '-o', path.join(dir, 'harness'), HARNESS_SOURCE]); + 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) => { + fs.writeFileSync(path.join(dir, 'in.d81'), disk.image); + const r = run([path.join(dir, 'harness'), path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81')]); + return { ...r, prg: r.status === 0 ? fs.readFileSync(path.join(dir, 'out.prg')) : null, image: r.status === 0 ? fs.readFileSync(path.join(dir, 'out.d81')) : null }; + }; + body(launch); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +function standardDisk(driver) { + const disk = new Disk(); + disk.addFile('GEOS', Buffer.from([1, 8, 0])); + disk.addFile('GEOBOOT', bootFile(driver).file); + return disk; +} + +test('the boot PRG carries the boot file, with the TR disk driver in place of the stock transport', { skip }, () => { + withHarness((launch) => { + const r = launch(standardDisk()); + assert.equal(r.status, 0, r.err); + const { body } = bootFile(); + const place = 2 + LOAD_ADDRESS - BASIC_START; + + assert.equal(r.prg.readUInt16LE(0), BASIC_START); + assert.equal(r.prg.length, place + BODY_SIZE); + assert.equal(r.prg.subarray(2, 14).toString('hex'), '0b080a009e323036310000' + '00'); + assert.equal(r.prg.subarray(31, place).every((b) => b === 0), true, 'zero padding up to the load address'); + assert.deepEqual([...r.prg.subarray(2 + 12 + 14, 2 + 12 + 17)], [0x4c, 0x3c, 0x2f], 'jumps to the boot entry'); + + const patched = r.prg.subarray(place + DRIVER_AT_IN_BODY, place + DRIVER_AT_IN_BODY + DRIVER_SIZE); + const expected = Buffer.from(CODE); + for (const at of RELOCATIONS) expected.writeUInt16LE(expected.readUInt16LE(at) + TURBO_START - DRIVER_AREA, at); + const start = TURBO_START - DRIVER_AREA; + assert.equal(patched.subarray(start, start + CODE.length).equals(expected), true, 'relocated driver at InitForIO'); + assert.equal(patched.subarray(start + CODE.length).equals(stockDriver().subarray(start + CODE.length)), true, 'the rest of the stock driver stays'); + for (let i = 0; i < 10; i++) assert.equal(patched.readUInt16LE(2 * i), TURBO_START + ENTRIES[i], `vector ${i}`); + for (const [slot, entry] of [[6, 10], [7, 11], [9, 12]]) assert.equal(patched.readUInt16LE(JUMP_SLOTS + 3 * slot + 1), TURBO_START + ENTRIES[entry], `jump slot ${slot}`); + assert.equal(patched[TYPE_BYTE], 3); + assert.equal(r.prg.subarray(place, place + DRIVER_AT_IN_BODY).equals(body.subarray(0, DRIVER_AT_IN_BODY)), true, 'bytes before the driver'); + assert.equal(r.prg.subarray(place + DRIVER_AT_IN_BODY + DRIVER_SIZE).equals(body.subarray(DRIVER_AT_IN_BODY + DRIVER_SIZE)), true, 'bytes after the driver'); + }); +}); + +test('a boot file whose driver is not recognized is refused', { skip }, () => { + withHarness((launch) => { + const odd = stockDriver(); + odd[TYPE_BYTE] = 9; + const r = launch(standardDisk(odd)); + assert.equal(r.status, 1); + assert.match(r.err, /disk driver is not one the TR can replace/); + + const crowded = stockDriver(); + crowded.writeUInt16LE(0x9110, 2 * 7); + assert.equal(launch(standardDisk(crowded)).status, 1, 'no room for the TR driver before ReadBlock'); + }); +}); + +test('a disk with no boot file is refused', { skip }, () => { + withHarness((launch) => { + const disk = new Disk(); + disk.addFile('GEOS', Buffer.from([1, 8, 0])); + const r = launch(disk); + assert.equal(r.status, 1); + assert.match(r.err, /boot file is not on the disk/); + }); +}); + +test('CONFIGURE files 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 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); + assert.match(first.out, /changed=1/); + assert.equal(first.image[configure + SLOT_GEOS_TYPE], SYSTEM_FILE); + assert.equal(first.image[second + SLOT_GEOS_TYPE], SYSTEM_FILE); + 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 again = launch({ image: first.image }); + assert.match(again.out, /changed=0/); + assert.equal(again.image.equals(first.image), true); + }); +}); diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c new file mode 100644 index 00000000..334d1758 --- /dev/null +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// geos_boot_harness IN.d81 OUT.prg OUT.d81: retypes the disk's CONFIGURE files, builds the GEOS 64 boot PRG from +// it, and writes both. Prints "changed=<0|1>"; exits 1 with the refusal on stderr. +#include + +#include "../GEOS/GeosBoot.h" +#include "../GEOS/GeosTargets.h" + +#define MAX_IMAGE 819200 +#define MAX_PRG (128 * 1024) + +static uint8_t Image[MAX_IMAGE + 1], Prg[MAX_PRG]; + +static bool ReadBlock(void *Context, uint32_t Offset, uint8_t *Block) +{ + memcpy(Block, (uint8_t *)Context + Offset, GEOS_SECTOR_SIZE); + return true; +} + +static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) +{ + memcpy((uint8_t *)Context + Offset, Block, GEOS_SECTOR_SIZE); + return true; +} + +int main(int argc, char **argv) +{ + struct GeosDisk Disk = { 0 }; + uint32_t Length; + bool Changed; + FILE *File; + size_t Size; + + if (argc != 4 || !(File = fopen(argv[1], "rb"))) return 2; + Size = fread(Image, 1, sizeof Image, File); + fclose(File); + + Disk.Format = GeosFormatForSize(Size); + Disk.Target = &Geos64Launcher.Target; + Disk.Context = Image; + Disk.Read = ReadBlock; + Disk.Write = WriteBlock; + if (!GeosDisableConfigure(&Disk, &Changed) || !GeosBuildBootPrg(&Disk, Prg, sizeof Prg, &Length)) + { + fprintf(stderr, "%s\n", Disk.Error); + return 1; + } + printf("changed=%d\n", Changed); + File = fopen(argv[2], "wb"); + fwrite(Prg, 1, Length, File); + fclose(File); + File = fopen(argv[3], "wb"); + fwrite(Image, 1, Size, File); + fclose(File); + return 0; +} From b7e76912595f91d57433b389642f7091a5703eea Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:16:21 -0500 Subject: [PATCH 044/100] firmware: launch GEOS 64 from the TeensyROM Specific menu Selecting it sets up a copy of the user's boot disk, builds the start-up PRG from it and mounts the copy for the block device, then lets the usual PRG launch run it under the GeoRAM handler. The copy is made even when the original already has the mouse driver, since GEOS saves to the disk it starts from. --- Source/Teensy/DriveDirLoad.ino | 5 +-- Source/Teensy/GeosLauncher.ino | 80 +++++++++++++++++++++++++++------- Source/Teensy/MainMenuItems.h | 1 + 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index a60aaf63..481034c0 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -250,8 +250,7 @@ FLASHMEM void HandleExecution() if (MenuSelCpy.ItemType == rtGEOS) { - LaunchGeos((const struct GeosLauncher*)MenuSelCpy.Code_Image); - return; + if (!LaunchGeos((const struct GeosLauncher*)MenuSelCpy.Code_Image, &MenuSelCpy)) return; } SendMsgPrintfln(MenuSelCpy.Name); @@ -292,7 +291,7 @@ FLASHMEM void HandleExecution() else { //non-CRT: copy the whole thing from flash into the RAM1 buffer - memcpy(RAM_Image, MenuSelCpy.Code_Image, MenuSelCpy.Size); + if (MenuSelCpy.Code_Image != RAM_Image) memcpy(RAM_Image, MenuSelCpy.Code_Image, MenuSelCpy.Size); MenuSelCpy.Code_Image = RAM_Image; } SendMsgPrintfln("Copied to RAM"); diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 9ca7726e..693492a9 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -1,6 +1,7 @@ // A GEOS menu item: finds the user's boot disk image in the launcher's folder on the SD card and puts // the TR input driver on a copy of it, leaving the original untouched. +#include "GEOS/GeosBoot.h" #include "GEOS/GeosTargets.h" #define GEOS_TEMP_NAME "PATCHING.TMP" @@ -150,12 +151,12 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch // Patches a temporary copy and renames it into place, so a failure never leaves a half-patched copy behind FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, - const char *Copy) + const char *Copy, bool Patch) { char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); - if (!GeosCopyFile(Source, Temp) || !GeosPatchFile(Launcher, Temp, Error, sizeof Error) || + if (!GeosCopyFile(Source, Temp) || (Patch && !GeosPatchFile(Launcher, Temp, Error, sizeof Error)) || (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) { SD.remove(Temp); @@ -171,12 +172,21 @@ FLASHMEM static bool GeosReady(const struct GeosLauncher *Launcher, const char * return true; } -// Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one -FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name) +FLASHMEM static bool GeosUse(const struct GeosLauncher *Launcher, const char *Name, char *BootName) +{ + strcpy(BootName, Name); + return GeosReady(Launcher, Name); +} + +// Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one. +// BootName gets the disk to start from: the original when it is set up and only the setup is wanted, else a copy, +// since GEOS saves to the disk it starts from +FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name, char *BootName) { char Source[MaxNamePathLength], CopyName[MaxItemNameLength + sizeof GEOS_COPY_TAG], Copy[MaxNamePathLength]; char Error[GEOS_ERROR_SIZE]; struct GeosBootDisk Found; + bool NeedsDriver; GeosPath(Source, sizeof Source, Launcher, Name); GeosCopyName(CopyName, sizeof CopyName, Name); @@ -187,7 +197,8 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return false; } if (!Found.HasDeskTop) return false; - if (Found.HasDriver) return GeosReady(Launcher, Name); + if (Found.HasDriver && !GeosBoots(Launcher)) return GeosUse(Launcher, Name, BootName); + NeedsDriver = !Found.HasDriver; if (SD.exists(Copy)) { @@ -197,27 +208,64 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c SendMsgPrintfln("%s:\r\n%s", CopyName, Error); return true; } - if (Found.HasDriver) return GeosReady(Launcher, CopyName); + if (Found.HasDriver) return GeosUse(Launcher, CopyName, BootName); + } + if (!GeosPatchCopy(Launcher, Name, Source, Copy, NeedsDriver)) return true; + SendMsgPrintfln(NeedsDriver ? "Added the TR USB mouse driver to\r\na copy of %s" : "Made a copy of %s\r\nto start from", Name); + return GeosUse(Launcher, CopyName, BootName); +} + +// Turns the menu item into the PRG that starts GEOS from the disk, which the block device serves from the SD card +FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char *Name, StructMenuItem *Item) +{ + char Path[MaxNamePathLength]; + struct GeosDisk Disk; + uint32_t Length = 0; + bool Changed, Built; + File Image; + + GeosPath(Path, sizeof Path, Launcher, Name); + Image = SD.open(Path, FILE_WRITE_BEGIN); + if (!Image) + { + SendMsgPrintfln("%s\r\ncould not be opened", Name); + return false; + } + Disk = GeosDiskOn(Launcher, &Image); + Built = GeosDisableConfigure(&Disk, &Changed) && GeosBuildBootPrg(&Disk, RAM_Image, RAM_ImageSize, &Length); + if (!Built) SendMsgPrintfln("%s:\r\n%s", Name, Disk.Error); + Image.close(); + if (!Built) return false; + if (!TRBlockMount(Path)) + { + SendMsgPrintfln("%s\r\ncould not be mounted", Name); + return false; } - if (!GeosPatchCopy(Launcher, Name, Source, Copy)) return true; - SendMsgPrintfln("Added the TR USB mouse driver to\r\na copy of %s", Name); - return GeosReady(Launcher, CopyName); + Item->ItemType = rtFilePrg; + Item->Code_Image = RAM_Image; + Item->Size = Length; + return true; } -FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) +// True when the item has become a PRG to run: the version starts from the disk it set up +FLASHMEM bool LaunchGeos(const struct GeosLauncher *Launcher, StructMenuItem *Item) { - char Names[GEOS_MAX_IMAGES][MaxItemNameLength]; + char Names[GEOS_MAX_IMAGES][MaxItemNameLength], BootName[MaxItemNameLength + sizeof GEOS_COPY_TAG] = ""; uint8_t Count; bool BootDiskFound = false; if (!SDFullInit()) { SendMsgPrintfln("%s starts from your boot disk\r\nimage on the SD card. Insert the card.", Launcher->Title); - return; + return false; } Count = GeosListImages(Launcher, Names); - for (uint8_t i = 0; i < Count; i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i]); - if (BootDiskFound) return; - SendMsgPrintfln("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again.", - Launcher->Title, Launcher->Folder); + for (uint8_t i = 0; i < Count && !(GeosBoots(Launcher) && *BootName); i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i], BootName); + if (!BootDiskFound) + { + SendMsgPrintfln("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again.", + Launcher->Title, Launcher->Folder); + return false; + } + return GeosBoots(Launcher) && *BootName && GeosStart(Launcher, BootName, Item); } diff --git a/Source/Teensy/MainMenuItems.h b/Source/Teensy/MainMenuItems.h index 9f63c511..5e6a56a6 100644 --- a/Source/Teensy/MainMenuItems.h +++ b/Source/Teensy/MainMenuItems.h @@ -297,6 +297,7 @@ StructMenuItem dirTeensyROMSpecific[] = rtBin8kLo , IOH_TeensyROM , (char*)"(This) TeensyROM Menu Cart" , (uint8_t*)TeensyROMC64_bin , sizeof(TeensyROMC64_bin) , rtFilePrg , IOH_TeensyROM , (char*)"TeensyROM External Ports Test" , (uint8_t*)TRExtPortCheck_prg , sizeof(TRExtPortCheck_prg) , rtGEOS , IOH_None , (char*)"GEOS 128 Boot Disk Setup" , (uint8_t*)&Geos128Launcher , sizeof(Geos128Launcher) , + rtGEOS , IOH_GeoRAM , (char*)"GEOS 64" , (uint8_t*)&Geos64Launcher , sizeof(Geos64Launcher) , #ifdef Fab04_REU rtFilePrg , IOH_TeensyROM , (char*)"TR+ C64 Expansion Port Test" , (uint8_t*)ExpansionPortTest_prg , sizeof(ExpansionPortTest_prg) , #endif From 0facad9c91f97f68fda699a5c254a2737f0e7ef6 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:21:52 -0500 Subject: [PATCH 045/100] tools: write the generated driver header with LF endings on every host --- tools/geos/trdrive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/geos/trdrive.py b/tools/geos/trdrive.py index d2f0c2e4..b0552d94 100644 --- a/tools/geos/trdrive.py +++ b/tools/geos/trdrive.py @@ -72,4 +72,4 @@ def header(): if __name__ == '__main__': if len(sys.argv) != 2: sys.exit(__doc__) - pathlib.Path(sys.argv[1]).write_text(header()) + pathlib.Path(sys.argv[1]).write_bytes(header().encode()) From 4aafa1e542d758cf31234980524308f55fa6303e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:23:18 -0500 Subject: [PATCH 046/100] tools: GEOS 64 mouse skips the 80-column scaling and stops at its vectors The 80-column flag at $3f is a GEOS 128 variable, so the 64 build leaves the scaling out. Each target gets its own driver area limit, and the test checks that a header's code starts in its target's area. --- Source/Teensy/GEOS/TRMouse64.h | 17 ++++++++--------- tools/geos/trmouse.py | 13 ++++++++----- tools/geos/trmouse.test.mjs | 2 ++ tools/geos/trmouse128.asm | 5 ++++- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Source/Teensy/GEOS/TRMouse64.h b/Source/Teensy/GEOS/TRMouse64.h index 3f211beb..60b3edcd 100644 --- a/Source/Teensy/GEOS/TRMouse64.h +++ b/Source/Teensy/GEOS/TRMouse64.h @@ -9,7 +9,7 @@ #endif static const char TRMouse64Name[] PROGMEM = "TR USB MOUSE"; -static const uint8_t TRMouse64Code[226] PROGMEM = { +static const uint8_t TRMouse64Code[216] PROGMEM = { 0x4c, 0x90, 0xfe, 0x4c, 0x9a, 0xfe, 0x4c, 0xc2, 0xfe, 0x4c, 0x9a, 0xfe, 0x80, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa0, 0xdf, 0x8c, 0xa4, 0xfe, 0xa2, 0x02, 0xbd, 0x03, 0xdf, 0xdd, 0xbf, 0xfe, 0xd0, 0x0e, 0xca, 0x10, 0xf5, 0x8c, 0xc9, 0xfe, @@ -18,20 +18,19 @@ static const uint8_t TRMouse64Code[226] PROGMEM = { 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, 0x24, 0x3f, 0x10, 0x06, 0x0e, 0x8d, 0xfe, 0x2e, 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, 0x59, 0xff, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, - 0x06, 0x07 + 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, 0x4f, 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, 0x62, 0xff, 0x00, 0x00, 0x54, 0x52, 0x20, + 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0a, 0x00, 0x80, 0xfe, 0x58, 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.py b/tools/geos/trmouse.py index a4b06402..7293ad4d 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -24,8 +24,9 @@ CLASS_NAME = b'TR USB Mouse' CLASS_NAME_SIZE = 12 # GEOS reads the version right after it TARGETS = {'128': (0xfd00, 15), '64': (0xfe80, 10)} # where GEOS loads an input driver, and its type +AREA_END = {'128': 0xfe80, '64': 0xfffa} # the 64's area stops at the hardware vectors LOAD, INPUT_TYPE = TARGETS['128'] -MAX_SIZE = 0x180 # the input driver area, $FD00-$FE7F on GEOS 128 and $FE80-$FFFF on GEOS 64 +TARGET = '128' SECTOR_SIZE = 256 DIR_ENTRY_SIZE = 32 @@ -62,10 +63,11 @@ def assemble(): with tempfile.TemporaryDirectory() as tmp: out = pathlib.Path(tmp) / 'trmouse128.bin' - subprocess.run(['acme', f'-DMOUSE_BASE={LOAD}', '-f', 'plain', '-o', str(out), str(SOURCE)], check=True) + subprocess.run(['acme', f'-DMOUSE_BASE={LOAD}', *(['-DGEOS64=1'] if TARGET == '64' else []), '-f', 'plain', '-o', str(out), str(SOURCE)], check=True) code = out.read_bytes() - if len(code) > MAX_SIZE: - sys.exit(f'the driver is {len(code)} bytes; GEOS has room for {MAX_SIZE}') + room = AREA_END[TARGET] - LOAD + if len(code) > room: + sys.exit(f'the driver is {len(code)} bytes; GEOS {TARGET} has room for {room}') return code @@ -301,7 +303,8 @@ def main(): parser.add_argument('--c-header', type=pathlib.Path, metavar='OUT') parser.add_argument('--target', choices=TARGETS, default='128') args = parser.parse_args() - global LOAD, INPUT_TYPE + global LOAD, INPUT_TYPE, TARGET + TARGET = args.target LOAD, INPUT_TYPE = TARGETS[args.target] if args.c_header: if args.disk: diff --git a/tools/geos/trmouse.test.mjs b/tools/geos/trmouse.test.mjs index 64e006b4..ec8206c9 100644 --- a/tools/geos/trmouse.test.mjs +++ b/tools/geos/trmouse.test.mjs @@ -25,6 +25,8 @@ for (const [target, committedPath] of Object.entries(committedHeaders)) { const r = spawnSync('python3', [trmouse, '--target', target, '--c-header', header], { encoding: 'utf8' }); assert.equal(r.status, 0, r.stdout + r.stderr); const committed = fs.readFileSync(path.join(root, committedPath), 'utf8').replaceAll('\r\n', '\n'); + const first = committed.match(/Code\[\d+\] PROGMEM = \{\s*0x4c, 0x([0-9a-f]{2}), 0x([0-9a-f]{2})/); + assert.equal(parseInt(first[2] + first[1], 16) >> 8, target === '64' ? 0xfe : 0xfd, 'the first jump lands in the target\'s own driver area'); assert.equal(fs.readFileSync(header, 'utf8'), committed, `rerun tools/geos/trmouse.py --target ${target} --c-header`); } finally { fs.rmSync(out, { recursive: true, force: true }); diff --git a/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index 56423f17..d1ec4ecb 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -103,11 +103,14 @@ readButtons bpl + dex + stx deltaX+1 +!ifndef GEOS64 { bit graphMode bpl + asl deltaX ; 640 pixels across in 80 columns rol deltaX+1 -+ clc ++ +} + clc lda mouseXPos adc deltaX sta mouseXPos From c8b056354b04d33acb461e60ff30fbe467527202 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:24:28 -0500 Subject: [PATCH 047/100] firmware: keep the GEOS boot PRG inside 64K and refuse a target with no boot file --- 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 50921f7b..7145733d 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -16,6 +16,7 @@ #define GEOS_SYSTEM_FILE 5 #define GEOS_CONFIG_PREFIX "CONFIG" #define GEOS_BASIC_START 0x0801 +#define GEOS_MEMORY_END 0x10000 #define GEOS_MAX_FILE_BLOCKS 3200 #define GEOS_DEVICE_8 8 @@ -176,6 +177,7 @@ static bool GeosBuildBootPrg(struct GeosDisk *Disk, uint8_t *Out, uint32_t Capac int32_t Driver; uint8_t *Body = Out + 2; + if (!Disk->Target->BootFile) return GeosFail(Disk, "this version has no boot file to start from"); if (!GeosReadFile(Disk, Disk->Target->BootFile, Out, Capacity, &FileSize)) return false; if (FileSize < 2 + GEOS_DRIVER_SIZE) return GeosFail(Disk, "the boot file is too short"); BodySize = FileSize - 2; @@ -183,7 +185,7 @@ static bool GeosBuildBootPrg(struct GeosDisk *Disk, uint8_t *Out, uint32_t Capac Driver = GeosFindDriver(Body, BodySize); if (Driver < 0 || !GeosReplaceTransport(Body + Driver)) return GeosFail(Disk, "the boot file's disk driver is not one the TR can replace"); Place = 2 + Load - GEOS_BASIC_START; - if (Load < GEOS_BASIC_START + Prologue - 2 || Place + BodySize > Capacity) return GeosFail(Disk, "the boot file does not fit in memory"); + if (Load < GEOS_BASIC_START + Prologue - 2 || Place + BodySize > Capacity || Load + BodySize > GEOS_MEMORY_END) return GeosFail(Disk, "the boot file does not fit in memory"); memmove(Out + Place, Body, BodySize); memset(Out, 0, Place); From 3611895fe4637fa2bc8f549c92864b6511c3875d Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:25:02 -0500 Subject: [PATCH 048/100] firmware: refuse a stock driver with other entries in the code the TR replaces A vector or jump slot the TR driver does not repoint would land in the middle of its code. The disk's CONFIGURE files are retyped only once the boot PRG has built, so a failed launch leaves the disk as it was. --- Source/Teensy/GEOS/GeosBoot.h | 11 +++++++++++ Source/Teensy/GeosLauncher.ino | 2 +- Source/Teensy/tests/geos-boot.test.js | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 7145733d..b99e40b6 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -76,6 +76,17 @@ static bool GeosReplaceTransport(uint8_t *Driver) uint16_t Routine = GeosWord(Driver + 2 * i); if (Routine < Start || Routine >= End) return false; } + for (int i = VecCount; i < GEOS_VECTOR_WORDS; i++) + { + uint16_t Other = GeosWord(Driver + 2 * i); + if (Other >= Start && Other < End) return false; + } + for (int i = 0; i < GEOS_JUMP_COUNT; i++) + { + uint16_t Target = GeosWord(Driver + GEOS_JUMP_SLOTS + 3 * i + 1); + bool Repointed = i == JumpSendTSBytes || i == JumpGetDOSError || i == JumpReadLink; + if (!Repointed && Target >= Start && Target < End) return false; + } memcpy(Code, TRDriverCode, sizeof Code); for (size_t i = 0; i < sizeof TRDriverRelocations / sizeof TRDriverRelocations[0]; i++) { diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 693492a9..07ba78d5 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -232,7 +232,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * return false; } Disk = GeosDiskOn(Launcher, &Image); - Built = GeosDisableConfigure(&Disk, &Changed) && GeosBuildBootPrg(&Disk, RAM_Image, RAM_ImageSize, &Length); + Built = GeosBuildBootPrg(&Disk, RAM_Image, RAM_ImageSize, &Length) && GeosDisableConfigure(&Disk, &Changed); if (!Built) SendMsgPrintfln("%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 b208186c..ac0c740c 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -180,6 +180,14 @@ test('a boot file whose driver is not recognized is refused', { skip }, () => { const crowded = stockDriver(); crowded.writeUInt16LE(0x9110, 2 * 7); assert.equal(launch(standardDisk(crowded)).status, 1, 'no room for the TR driver before ReadBlock'); + + const shared = stockDriver(); + shared.writeUInt16LE(TURBO_START + 0x20, 2 * 12); + assert.equal(launch(standardDisk(shared)).status, 1, 'a vector the TR driver does not replace points into the replaced run'); + + const jumped = stockDriver(); + jumped.writeUInt16LE(TURBO_START + 0x20, JUMP_SLOTS + 3 * 2 + 1); + assert.equal(launch(standardDisk(jumped)).status, 1, 'a jump slot the TR driver does not replace points into the replaced run'); }); }); From ee97b3fb19a76856b35ca80320134e8e54c0cc77 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:25:57 -0500 Subject: [PATCH 049/100] firmware: drop two comments that restate the GEOS launch functions --- Source/Teensy/GeosLauncher.ino | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 07ba78d5..6ce0bf8a 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -215,7 +215,6 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return GeosUse(Launcher, CopyName, BootName); } -// Turns the menu item into the PRG that starts GEOS from the disk, which the block device serves from the SD card FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char *Name, StructMenuItem *Item) { char Path[MaxNamePathLength]; @@ -247,7 +246,6 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * return true; } -// True when the item has become a PRG to run: the version starts from the disk it set up FLASHMEM bool LaunchGeos(const struct GeosLauncher *Launcher, StructMenuItem *Item) { char Names[GEOS_MAX_IMAGES][MaxItemNameLength], BootName[MaxItemNameLength + sizeof GEOS_COPY_TAG] = ""; From 935483200d95f3dd49205825e64bc4972294bde7 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 19:49:49 -0500 Subject: [PATCH 050/100] tests: geos boot harness builds the boot PRG before retyping CONFIGURE, as the launcher does --- Source/Teensy/tests/geos_boot_harness.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 334d1758..4e7c8619 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -40,7 +40,7 @@ int main(int argc, char **argv) Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; - if (!GeosDisableConfigure(&Disk, &Changed) || !GeosBuildBootPrg(&Disk, Prg, sizeof Prg, &Length)) + if (!GeosBuildBootPrg(&Disk, Prg, sizeof Prg, &Length) || !GeosDisableConfigure(&Disk, &Changed)) { fprintf(stderr, "%s\n", Disk.Error); return 1; From 1823fb886a376f1fe718dc8302f365a2baabc531 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:21:58 -0500 Subject: [PATCH 051/100] tools: the GEOS disk driver masks interrupts around IO as the stock drivers do Stock InitForIO silences the VIC and CIA interrupt sources and DoneWithIO clears what latched, so a source GEOS never acknowledges cannot hold the IRQ line low once interrupts return. Without that, GEOS 64 on a C128 reached the DeskTop and then spent its time in the interrupt handler. --- Source/Teensy/GEOS/TRDriver.h | 78 ++++++++++++++++++----------------- tools/geos/trdrive.asm | 19 +++++++++ 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/Source/Teensy/GEOS/TRDriver.h b/Source/Teensy/GEOS/TRDriver.h index 00e7938d..43424b05 100644 --- a/Source/Teensy/GEOS/TRDriver.h +++ b/Source/Teensy/GEOS/TRDriver.h @@ -9,45 +9,47 @@ #endif #define TR_DRIVER_BASE 0x9000 -static const uint8_t TRDriverCode[481] PROGMEM = { - 0x08, 0x68, 0x8d, 0xd7, 0x91, 0x78, 0xa5, 0x01, 0x8d, 0xd8, 0x91, 0xa9, 0x36, 0x85, 0x01, 0x60, - 0x78, 0xad, 0xd8, 0x91, 0x85, 0x01, 0xad, 0xd7, 0x91, 0x48, 0x28, 0x60, 0xa2, 0x00, 0x8a, 0x60, - 0xa2, 0x00, 0xa0, 0x00, 0x60, 0x8d, 0x89, 0x84, 0x85, 0xba, 0xa2, 0x00, 0x60, 0x20, 0x7a, 0x90, - 0xe0, 0x00, 0xd0, 0x03, 0x20, 0x5c, 0x91, 0xa0, 0x00, 0x60, 0x20, 0x5c, 0x91, 0x20, 0xb1, 0x90, - 0x8a, 0x48, 0x20, 0x5c, 0x91, 0x68, 0xaa, 0xa0, 0x00, 0x60, 0x20, 0x8f, 0x91, 0xb0, 0x26, 0xa9, - 0x01, 0x20, 0xe9, 0x90, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, 0xa2, 0x0b, 0x20, 0xd0, 0x91, 0xa2, - 0x40, 0x20, 0xcc, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0xcc, 0x91, 0xc8, 0x91, 0x0a, - 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, 0x20, 0x8f, 0x91, 0xb0, 0xf6, 0xa9, - 0x01, 0x20, 0xe9, 0x90, 0xe0, 0x00, 0xd0, 0x28, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xdc, 0x91, 0xad, - 0xdc, 0x91, 0xa2, 0x0b, 0x20, 0xd0, 0x91, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0x91, 0x0a, 0xc8, 0xe8, - 0xe0, 0x40, 0xd0, 0xf5, 0xee, 0xdc, 0x91, 0xad, 0xdc, 0x91, 0xc9, 0x04, 0xd0, 0xe1, 0xa2, 0x00, - 0x60, 0x20, 0x8f, 0x91, 0xb0, 0xbf, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xdc, 0x91, 0xad, 0xdc, 0x91, - 0xa2, 0x0b, 0x20, 0xd0, 0x91, 0xa2, 0x00, 0xb1, 0x0a, 0x9d, 0x40, 0xdf, 0xdd, 0x40, 0xdf, 0xd0, - 0x15, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0xdc, 0x91, 0xad, 0xdc, 0x91, 0xc9, 0x04, 0xd0, - 0xdc, 0xa9, 0x02, 0x4c, 0xe9, 0x90, 0xa2, 0x0d, 0x60, 0x8d, 0xda, 0x91, 0xee, 0xdd, 0x91, 0xd0, - 0x03, 0xee, 0xdd, 0x91, 0xad, 0xdd, 0x91, 0xa2, 0x0c, 0x20, 0xd0, 0x91, 0xa5, 0x04, 0xa2, 0x09, - 0x20, 0xd0, 0x91, 0xa5, 0x05, 0xa2, 0x0a, 0x20, 0xd0, 0x91, 0xad, 0xda, 0x91, 0xa2, 0x08, 0x20, - 0xd0, 0x91, 0xa9, 0x00, 0x8d, 0xdf, 0x91, 0x8d, 0xe0, 0x91, 0xa2, 0x0d, 0x20, 0xcc, 0x91, 0x8d, - 0xdb, 0x91, 0x20, 0xcc, 0x91, 0xcd, 0xdb, 0x91, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0xdf, - 0x91, 0xd0, 0xe7, 0xee, 0xe0, 0x91, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0xcc, - 0x91, 0xcd, 0xdd, 0x91, 0xf0, 0x04, 0x68, 0x4c, 0x2e, 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, 0xd9, 0x91, 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0xd9, 0x91, 0x91, - 0x0a, 0x98, 0x38, 0xe9, 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xad, - 0xde, 0x91, 0xd0, 0x20, 0xa2, 0xdf, 0x20, 0xb8, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, 0xb8, 0x91, - 0xb0, 0x14, 0x8e, 0xde, 0x91, 0x8e, 0xce, 0x91, 0x8e, 0xd2, 0x91, 0x8e, 0x9b, 0x90, 0x8e, 0xcb, - 0x90, 0x8e, 0xce, 0x90, 0x18, 0x60, 0x38, 0x60, 0x8e, 0xbf, 0x91, 0xa0, 0x02, 0xb9, 0x10, 0xdf, - 0xd9, 0xd4, 0x91, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, 0xdf, 0x60, - 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 +static const uint8_t TRDriverCode[516] PROGMEM = { + 0x08, 0x68, 0x8d, 0xf9, 0x91, 0x78, 0xa5, 0x01, 0x8d, 0xfa, 0x91, 0xa9, 0x36, 0x85, 0x01, 0xad, + 0x1a, 0xd0, 0x8d, 0x03, 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, 0x03, 0x92, + 0x8d, 0x1a, 0xd0, 0xad, 0xfa, 0x91, 0x85, 0x01, 0xad, 0xf9, 0x91, 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, 0x7e, 0x91, 0xa0, 0x00, 0x60, 0x20, 0x7e, 0x91, 0x20, + 0xd3, 0x90, 0x8a, 0x48, 0x20, 0x7e, 0x91, 0x68, 0xaa, 0xa0, 0x00, 0x60, 0x20, 0xb1, 0x91, 0xb0, + 0x26, 0xa9, 0x01, 0x20, 0x0b, 0x91, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, 0xa2, 0x0b, 0x20, 0xf2, + 0x91, 0xa2, 0x40, 0x20, 0xee, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0xee, 0x91, 0xc8, + 0x91, 0x0a, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, 0x20, 0xb1, 0x91, 0xb0, + 0xf6, 0xa9, 0x01, 0x20, 0x0b, 0x91, 0xe0, 0x00, 0xd0, 0x28, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xfe, + 0x91, 0xad, 0xfe, 0x91, 0xa2, 0x0b, 0x20, 0xf2, 0x91, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0x91, 0x0a, + 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf5, 0xee, 0xfe, 0x91, 0xad, 0xfe, 0x91, 0xc9, 0x04, 0xd0, 0xe1, + 0xa2, 0x00, 0x60, 0x20, 0xb1, 0x91, 0xb0, 0xbf, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xfe, 0x91, 0xad, + 0xfe, 0x91, 0xa2, 0x0b, 0x20, 0xf2, 0x91, 0xa2, 0x00, 0xb1, 0x0a, 0x9d, 0x40, 0xdf, 0xdd, 0x40, + 0xdf, 0xd0, 0x15, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0xfe, 0x91, 0xad, 0xfe, 0x91, 0xc9, + 0x04, 0xd0, 0xdc, 0xa9, 0x02, 0x4c, 0x0b, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0xfc, 0x91, 0xee, 0xff, + 0x91, 0xd0, 0x03, 0xee, 0xff, 0x91, 0xad, 0xff, 0x91, 0xa2, 0x0c, 0x20, 0xf2, 0x91, 0xa5, 0x04, + 0xa2, 0x09, 0x20, 0xf2, 0x91, 0xa5, 0x05, 0xa2, 0x0a, 0x20, 0xf2, 0x91, 0xad, 0xfc, 0x91, 0xa2, + 0x08, 0x20, 0xf2, 0x91, 0xa9, 0x00, 0x8d, 0x01, 0x92, 0x8d, 0x02, 0x92, 0xa2, 0x0d, 0x20, 0xee, + 0x91, 0x8d, 0xfd, 0x91, 0x20, 0xee, 0x91, 0xcd, 0xfd, 0x91, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, + 0xee, 0x01, 0x92, 0xd0, 0xe7, 0xee, 0x02, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, + 0x20, 0xee, 0x91, 0xcd, 0xff, 0x91, 0xf0, 0x04, 0x68, 0x4c, 0x50, 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, 0xfb, 0x91, 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0xfb, + 0x91, 0x91, 0x0a, 0x98, 0x38, 0xe9, 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, + 0x60, 0xad, 0x00, 0x92, 0xd0, 0x20, 0xa2, 0xdf, 0x20, 0xda, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, + 0xda, 0x91, 0xb0, 0x14, 0x8e, 0x00, 0x92, 0x8e, 0xf0, 0x91, 0x8e, 0xf4, 0x91, 0x8e, 0xbd, 0x90, + 0x8e, 0xed, 0x90, 0x8e, 0xf0, 0x90, 0x18, 0x60, 0x38, 0x60, 0x8e, 0xe1, 0x91, 0xa0, 0x02, 0xb9, + 0x10, 0xdf, 0xd9, 0xf6, 0x91, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, + 0xdf, 0x60, 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; -static const uint16_t TRDriverRelocations[61] PROGMEM = { - 0x0003, 0x0009, 0x0012, 0x0017, 0x002e, 0x0035, 0x003b, 0x003e, 0x0043, 0x004b, 0x0052, 0x005d, 0x0062, 0x006b, 0x007b, 0x0082, - 0x008d, 0x0090, 0x0095, 0x00a5, 0x00a8, 0x00b2, 0x00bb, 0x00be, 0x00c3, 0x00d8, 0x00db, 0x00e4, 0x00ea, 0x00ed, 0x00f2, 0x00f5, - 0x00fa, 0x0101, 0x0108, 0x010b, 0x0110, 0x0115, 0x0118, 0x011d, 0x0120, 0x0123, 0x0126, 0x012f, 0x0134, 0x013f, 0x0142, 0x0148, - 0x0172, 0x017d, 0x0190, 0x0197, 0x019e, 0x01a3, 0x01a6, 0x01a9, 0x01ac, 0x01af, 0x01b2, 0x01b9, 0x01c1 +static const uint16_t TRDriverRelocations[63] PROGMEM = { + 0x0003, 0x0009, 0x0013, 0x002e, 0x0034, 0x0039, 0x0050, 0x0057, 0x005d, 0x0060, 0x0065, 0x006d, 0x0074, 0x007f, 0x0084, 0x008d, + 0x009d, 0x00a4, 0x00af, 0x00b2, 0x00b7, 0x00c7, 0x00ca, 0x00d4, 0x00dd, 0x00e0, 0x00e5, 0x00fa, 0x00fd, 0x0106, 0x010c, 0x010f, + 0x0114, 0x0117, 0x011c, 0x0123, 0x012a, 0x012d, 0x0132, 0x0137, 0x013a, 0x013f, 0x0142, 0x0145, 0x0148, 0x0151, 0x0156, 0x0161, + 0x0164, 0x016a, 0x0194, 0x019f, 0x01b2, 0x01b9, 0x01c0, 0x01c5, 0x01c8, 0x01cb, 0x01ce, 0x01d1, 0x01d4, 0x01db, 0x01e3 }; static const uint16_t TRDriverEntries[13] PROGMEM = { - 0x0000, 0x0010, 0x001f, 0x001f, 0x001c, 0x0025, 0x0020, 0x002d, 0x003a, 0x0020, 0x0020, 0x0020, 0x004a + 0x0000, 0x0026, 0x0041, 0x0041, 0x003e, 0x0047, 0x0042, 0x004f, 0x005c, 0x0042, 0x0042, 0x0042, 0x006c }; diff --git a/tools/geos/trdrive.asm b/tools/geos/trdrive.asm index 3cecdb8e..22e56fef 100644 --- a/tools/geos/trdrive.asm +++ b/tools/geos/trdrive.asm @@ -38,6 +38,12 @@ STATUS_BAD_ADDRESS = $e2 ERR_INV_TRACK = 2 ERR_DEV_NOT_FOUND = 13 +VIC_IRQ_FLAGS = $d019 +VIC_IRQ_ENABLE = $d01a +CIA1_ICR = $dc0d +CIA2_ICR = $dd0d +MASK_ALL = $7f + BASE_GEOS_RAM = $df BASE_REU = $de @@ -52,10 +58,22 @@ InitForIO sta saved01 lda #$36 sta $01 + lda VIC_IRQ_ENABLE + sta savedIrqEnable + lda #0 + sta VIC_IRQ_ENABLE + lda #MASK_ALL + sta VIC_IRQ_FLAGS + sta CIA1_ICR + sta CIA2_ICR rts DoneWithIO sei + lda CIA1_ICR + lda CIA2_ICR + lda savedIrqEnable + sta VIC_IRQ_ENABLE lda saved01 sta $01 lda savedP @@ -345,3 +363,4 @@ page !byte 0 token !byte 0 baseHi !byte 0 patience !word 0 +savedIrqEnable !byte 0 From ba2e6b139ed2793e9771d96a97b7faa4f23573ed Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:37:30 -0500 Subject: [PATCH 052/100] firmware: start GEOS 64 with the TeensyROM's date and time The boot file's start-up code sets a fixed date and a fixed 1 PM; the launch patches those with the TeensyROM's local time. A boot file whose start-up code is not the one it knows keeps what it has. --- Source/Teensy/GEOS/GeosBoot.h | 55 +++++++++++++++++++++++++ Source/Teensy/GeosLauncher.ino | 2 + Source/Teensy/tests/geos-boot.test.js | 33 +++++++++++++-- Source/Teensy/tests/geos_boot_harness.c | 5 ++- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index b99e40b6..16de454d 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -179,6 +179,61 @@ static const uint8_t GeosBootStub[] = 0x4c, 0x00, 0x00, // jmp entry }; +// The kernal's start-up code sets the date with three immediate stores and the clock in CIA 1 with another run; +// both are found in the boot file by their bytes, since the stock file fixes the date and time they set +static const uint8_t GeosStartDate[] = { 0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85 }; +static const uint8_t GeosStartClock[] = +{ + 0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, // clear the alarm-set bit + 0xa9, 0x81, 0x8d, 0x0b, 0xdc, // hour 1 PM + 0xa9, 0x00, 0x8d, 0x0a, 0xdc, // minute 0 + 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, // second and tenths 0; the tenths start the clock +}; +#define GEOS_PM_BIT 0x80 +#define GEOS_NOP 0xea + +static uint8_t GeosBcd(uint8_t Value) +{ + return (Value / 10) << 4 | Value % 10; +} + +// Finds Pattern once in Prg; NULL when it is absent or ambiguous +static uint8_t *GeosFindOnce(uint8_t *Prg, 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(Prg + i, Pattern, Size)) continue; + if (Found) return NULL; + Found = Prg + i; + } + return Found; +} + +// Sets the date and time GEOS starts with. Leaves the PRG as it is, and returns false, for a boot file whose +// start-up code is not the one this knows +static bool GeosSetClock(uint8_t *Prg, uint32_t Length, const struct GeosDate *Now) +{ + uint8_t *Date = GeosFindOnce(Prg, Length, GeosStartDate, sizeof GeosStartDate); + uint8_t *Clock = GeosFindOnce(Prg, Length, GeosStartClock, sizeof GeosStartClock); + uint8_t Hour12 = Now->Hour % 12 ? Now->Hour % 12 : 12; + uint8_t Time[] = + { + 0xa9, (uint8_t)(GeosBcd(Hour12) | (Now->Hour >= 12 ? GEOS_PM_BIT : 0)), 0x8d, 0x0b, 0xdc, + 0xa2, GeosBcd(Now->Minute), 0x8e, 0x0a, 0xdc, + 0xa9, 0x00, 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, + }; + + if (!Date || !Clock) return false; + Date[1] = Now->Day; + Date[6] = Now->Month; + Date[11] = Now->Year; + memset(Clock, GEOS_NOP, sizeof GeosStartClock); + memcpy(Clock + sizeof GeosStartClock - sizeof Time, Time, sizeof Time); + return true; +} + // One PRG that starts GEOS: the boot file with the disk driver's transport replaced, placed where its load // address puts it, behind a BASIC line that jumps to the boot entry static bool GeosBuildBootPrg(struct GeosDisk *Disk, uint8_t *Out, uint32_t Capacity, uint32_t *Length) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 6ce0bf8a..31675eb6 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -220,6 +220,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * char Path[MaxNamePathLength]; struct GeosDisk Disk; uint32_t Length = 0; + struct GeosDate Now = GeosToday(); bool Changed, Built; File Image; @@ -232,6 +233,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * } Disk = GeosDiskOn(Launcher, &Image); Built = GeosBuildBootPrg(&Disk, RAM_Image, RAM_ImageSize, &Length) && GeosDisableConfigure(&Disk, &Changed); + if (Built) GeosSetClock(RAM_Image, Length, &Now); if (!Built) SendMsgPrintfln("%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 ac0c740c..61bf6c6b 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -66,10 +66,15 @@ function stockDriver() { return driver; } -function bootFile(driver = stockDriver()) { +const START_AT_IN_BODY = 0x1000; +const START_DATE = [0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85]; +const START_CLOCK = [0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, 0xa9, 0x81, 0x8d, 0x0b, 0xdc, 0xa9, 0x00, 0x8d, 0x0a, 0xdc, 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc]; + +function bootFile(driver = stockDriver(), startUp = false) { const body = Buffer.alloc(BODY_SIZE); for (let i = 0; i < BODY_SIZE; i++) body[i] = (i * 31 + 7) & 0xff; driver.copy(body, DRIVER_AT_IN_BODY); + if (startUp) Buffer.from([...START_DATE, ...START_CLOCK]).copy(body, START_AT_IN_BODY); const file = Buffer.alloc(2 + BODY_SIZE); file.writeUInt16LE(LOAD_ADDRESS, 0); body.copy(file, 2); @@ -135,10 +140,10 @@ function withHarness(body) { } } -function standardDisk(driver) { +function standardDisk(driver, startUp = false) { const disk = new Disk(); disk.addFile('GEOS', Buffer.from([1, 8, 0])); - disk.addFile('GEOBOOT', bootFile(driver).file); + disk.addFile('GEOBOOT', bootFile(driver, startUp).file); return disk; } @@ -169,6 +174,28 @@ test('the boot PRG carries the boot file, with the TR disk driver in place of th }); }); +test('the boot PRG starts GEOS with the given date and time', { skip }, () => { + withHarness((launch) => { + const r = launch(standardDisk(undefined, true)); + const place = 2 + LOAD_ADDRESS - BASIC_START + START_AT_IN_BODY; + const at = r.prg.subarray(place, place + START_DATE.length + START_CLOCK.length); + + assert.match(r.out, /clock=1/); + assert.deepEqual([at[1], at[6], at[11]], [21, 10, 26], 'day, month, year'); + assert.deepEqual([...at.subarray(15 + 6, 15 + 24)], [0xa9, 0x83, 0x8d, 0x0b, 0xdc, 0xa2, 0x07, 0x8e, 0x0a, 0xdc, 0xa9, 0x00, 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc], '3 PM and 7 minutes'); + assert.equal(at.subarray(15, 15 + 6).every((b) => b === 0xea), true, 'padding'); + }); +}); + +test('a boot file without the known start-up code keeps the date and time it has', { skip }, () => { + withHarness((launch) => { + const r = launch(standardDisk()); + assert.match(r.out, /clock=0/); + const place = 2 + LOAD_ADDRESS - BASIC_START; + assert.equal(r.prg.subarray(place, place + DRIVER_AT_IN_BODY).equals(bootFile().body.subarray(0, DRIVER_AT_IN_BODY)), true); + }); +}); + test('a boot file whose driver is not recognized is refused', { skip }, () => { withHarness((launch) => { const odd = stockDriver(); diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 4e7c8619..edaef20f 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 IN.d81 OUT.prg OUT.d81: retypes the disk's CONFIGURE files, builds the GEOS 64 boot PRG from -// it, and writes both. Prints "changed=<0|1>"; exits 1 with the refusal on stderr. +// it, sets the clock to 2026-10-21 15:07, and writes both. Prints "changed=<0|1> clock=<0|1>"; exits 1 with the refusal on stderr. #include #include "../GEOS/GeosBoot.h" @@ -25,6 +25,7 @@ static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) int main(int argc, char **argv) { + const struct GeosDate Now = { 26, 10, 21, 15, 7 }; struct GeosDisk Disk = { 0 }; uint32_t Length; bool Changed; @@ -45,7 +46,7 @@ int main(int argc, char **argv) fprintf(stderr, "%s\n", Disk.Error); return 1; } - printf("changed=%d\n", Changed); + printf("changed=%d clock=%d\n", Changed, GeosSetClock(Prg, Length, &Now)); File = fopen(argv[2], "wb"); fwrite(Prg, 1, Length, File); fclose(File); From 3a2d2d3bd4610c565373c54e6c8c252815bd6632 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:44:32 -0500 Subject: [PATCH 053/100] tests: geos boot harness header states the build-then-retype order --- Source/Teensy/tests/geos_boot_harness.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index edaef20f..63389e3f 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 IN.d81 OUT.prg OUT.d81: retypes the disk's CONFIGURE files, builds the GEOS 64 boot PRG from -// it, sets the clock to 2026-10-21 15:07, and writes both. Prints "changed=<0|1> clock=<0|1>"; exits 1 with the refusal on stderr. +// geos_boot_harness IN.d81 OUT.prg OUT.d81: builds the GEOS 64 boot PRG from the disk, retypes its CONFIGURE +// files, sets the clock to 2026-10-21 15:07, and writes both. Prints "changed=<0|1> clock=<0|1>"; exits 1 with the refusal on stderr. #include #include "../GEOS/GeosBoot.h" From 9f096097c32aaab110e4368b68839ba9d985fd0e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:45:57 -0500 Subject: [PATCH 054/100] firmware: a flush with no usable image reports no image READ, WRITE and INFO already did; FLUSH reported done for an image the device treats as unmounted. The test also removes its temporary directory. --- Source/Teensy/BlockDevice/TRBlockImage.h | 1 + Source/Teensy/tests/trblock.test.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/Teensy/BlockDevice/TRBlockImage.h b/Source/Teensy/BlockDevice/TRBlockImage.h index 308a1326..bac42a00 100644 --- a/Source/Teensy/BlockDevice/TRBlockImage.h +++ b/Source/Teensy/BlockDevice/TRBlockImage.h @@ -55,6 +55,7 @@ static uint8_t TRBlockImageFlush(void *Context) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; + if (!Image->Format) return TRBlockNoImage; return Image->Flush(Image->Context) ? TRBlockDone : TRBlockWriteFailed; } diff --git a/Source/Teensy/tests/trblock.test.js b/Source/Teensy/tests/trblock.test.js index addca66a..a375f7ae 100644 --- a/Source/Teensy/tests/trblock.test.js +++ b/Source/Teensy/tests/trblock.test.js @@ -168,10 +168,14 @@ test('with no image mounted every command fails with no image, and status never test('an image that is no known size mounts as no image rather than being guessed at', { skip }, () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trblock-odd-')); const image = path.join(dir, 'in.img'), out = path.join(dir, 'out.img'); - fs.writeFileSync(image, makeImage(FORMATS.d64).subarray(0, SECTOR * 100)); - for (const cmd of [READ, WRITE, INFO]) { - const ran = spawnSync(harness(), [image, out], { input: command(cmd, { addr0: 1 }) + 'svc\n' + read(STATUS), encoding: 'utf8' }); - assert.equal(ran.stdout.trim().split('\n').at(-1), hex(NO_IMAGE), `cmd ${cmd}`); + try { + fs.writeFileSync(image, makeImage(FORMATS.d64).subarray(0, SECTOR * 100)); + for (const cmd of [READ, WRITE, FLUSH, INFO]) { + const ran = spawnSync(harness(), [image, out], { input: command(cmd, { addr0: 1 }) + 'svc\n' + read(STATUS), encoding: 'utf8' }); + assert.equal(ran.stdout.trim().split('\n').at(-1), hex(NO_IMAGE), `cmd ${cmd}`); + } + } finally { + fs.rmSync(dir, { recursive: true, force: true }); } }); From 3febf0dd70d5a39eeafb9d6db0599dc00376ee20 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:47:07 -0500 Subject: [PATCH 055/100] firmware: add the mouse driver to an existing GEOS copy in place A copy the user already has, with a desktop but without the driver, was replaced by a fresh copy and lost what GEOS had saved to it. --- Source/Teensy/GeosLauncher.ino | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 31675eb6..0bf60ad5 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -209,6 +209,15 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return true; } if (Found.HasDriver) return GeosUse(Launcher, CopyName, BootName); + if (Found.HasDeskTop) + { + if (!GeosPatchFile(Launcher, Copy, Error, sizeof Error)) + { + SendMsgPrintfln("Could not add the mouse driver to\r\n%s:\r\n%s", CopyName, Error); + return true; + } + return GeosUse(Launcher, CopyName, BootName); + } } if (!GeosPatchCopy(Launcher, Name, Source, Copy, NeedsDriver)) return true; SendMsgPrintfln(NeedsDriver ? "Added the TR USB mouse driver to\r\na copy of %s" : "Made a copy of %s\r\nto start from", Name); From 26d9329a12d24c94800e0d101fc44a118892e4b7 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:49:41 -0500 Subject: [PATCH 056/100] firmware: report why GeosPatchFile could not open the image --- Source/Teensy/GeosLauncher.ino | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 0bf60ad5..f732cf2d 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -141,7 +141,11 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch struct GeosDate Today = GeosToday(); bool Patched; - if (!Image) return false; + if (!Image) + { + snprintf(Error, ErrorSize, "the image could not be opened"); + return false; + } Disk = GeosDiskOn(Launcher, &Image); Patched = GeosInstallDriver(&Disk, &Today); if (!Patched) snprintf(Error, ErrorSize, "%s", Disk.Error); From c86eabc30bcc9619087627d56d4db55a9f5a60f1 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:50:13 -0500 Subject: [PATCH 057/100] firmware: patch an existing GEOS copy through a temporary file, never replace it A copy that is readable but has no desktop used to be deleted and recopied, and one needing the driver was patched in place. Both can lose what GEOS saved to it. The patch now goes through GeosPatchCopy, and a copy with no desktop is left for the user to remove. --- Source/Teensy/GeosLauncher.ino | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index f732cf2d..ed5b744c 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -213,15 +213,13 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return true; } if (Found.HasDriver) return GeosUse(Launcher, CopyName, BootName); - if (Found.HasDeskTop) + if (!Found.HasDeskTop) { - if (!GeosPatchFile(Launcher, Copy, Error, sizeof Error)) - { - SendMsgPrintfln("Could not add the mouse driver to\r\n%s:\r\n%s", CopyName, Error); - return true; - } - return GeosUse(Launcher, CopyName, BootName); + SendMsgPrintfln("%s\r\nhas no desktop; remove it to start over", CopyName); + return true; } + if (!GeosPatchCopy(Launcher, CopyName, Copy, Copy, true)) return true; + return GeosUse(Launcher, CopyName, BootName); } if (!GeosPatchCopy(Launcher, Name, Source, Copy, NeedsDriver)) return true; SendMsgPrintfln(NeedsDriver ? "Added the TR USB mouse driver to\r\na copy of %s" : "Made a copy of %s\r\nto start from", Name); From c5d21eb7c4a57487a58716b5cd0259cabcc8a49f Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:52:27 -0500 Subject: [PATCH 058/100] firmware: GeosPatchCopy names the failure for a plain copy correctly --- Source/Teensy/GeosLauncher.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index ed5b744c..9aa3358b 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -164,7 +164,7 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) { SD.remove(Temp); - SendMsgPrintfln("Could not add the mouse driver to\r\na copy of %s:\r\n%s", Name, Error); + SendMsgPrintfln(Patch ? "Could not add the mouse driver to\r\na copy of %s:\r\n%s" : "Could not make a copy of\r\n%s:\r\n%s", Name, Error); return false; } return true; From ed1cd28b1758665fdaca2120f38fe1ad98cd308c Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:52:51 -0500 Subject: [PATCH 059/100] firmware: keep the replaced GEOS copy until its successor is in place Removing the copy and then renaming the new one over it lost the copy when the rename failed or power dropped between the two. The old copy is renamed aside and put back if the new one cannot take its place. --- Source/Teensy/GeosLauncher.ino | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 9aa3358b..3dc28d79 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -5,6 +5,7 @@ #include "GEOS/GeosTargets.h" #define GEOS_TEMP_NAME "PATCHING.TMP" +#define GEOS_BACKUP_NAME "REPLACED.BAK" #define GEOS_COPY_TAG "-TR" #define GEOS_EXT_LENGTH 4 #define GEOS_MAX_IMAGES 8 @@ -153,21 +154,30 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch return Patched; } -// Patches a temporary copy and renames it into place, so a failure never leaves a half-patched copy behind +// Patches a temporary copy and renames it into place, keeping the copy it replaces until the new one is in, so a +// failure never leaves a half-patched copy behind or loses the one GEOS saved to FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, const char *Copy, bool Patch) { - char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; + char Temp[MaxNamePathLength], Backup[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; + bool Replacing = SD.exists(Copy); GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); - if (!GeosCopyFile(Source, Temp) || (Patch && !GeosPatchFile(Launcher, Temp, Error, sizeof Error)) || - (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) + GeosPath(Backup, sizeof Backup, Launcher, GEOS_BACKUP_NAME); + if (Replacing && SD.exists(Backup)) snprintf(Error, sizeof Error, "%s is left from an earlier try: remove or restore it", GEOS_BACKUP_NAME); + else if (GeosCopyFile(Source, Temp) && (!Patch || GeosPatchFile(Launcher, Temp, Error, sizeof Error)) && + (!Replacing || SD.rename(Copy, Backup))) { - SD.remove(Temp); - SendMsgPrintfln(Patch ? "Could not add the mouse driver to\r\na copy of %s:\r\n%s" : "Could not make a copy of\r\n%s:\r\n%s", Name, Error); - return false; + if (SD.rename(Temp, Copy)) + { + if (Replacing) SD.remove(Backup); + return true; + } + if (Replacing) SD.rename(Backup, Copy); } - return true; + SD.remove(Temp); + SendMsgPrintfln(Patch ? "Could not add the mouse driver to\r\na copy of %s:\r\n%s" : "Could not make a copy of\r\n%s:\r\n%s", Name, Error); + return false; } FLASHMEM static bool GeosReady(const struct GeosLauncher *Launcher, const char *Name) From b14faef8db341dd68ddc5bf2cca740adfa100984 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:57:17 -0500 Subject: [PATCH 060/100] firmware: put back a GEOS copy that power loss left renamed aside The backup took one fixed name for every copy in a folder, so a leftover for one copy blocked the others and nothing said which copy it held. It now takes its copy's name, and a launch that finds the copy missing and its backup present renames the backup back. --- Source/Teensy/GeosLauncher.ino | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 3dc28d79..bdee8402 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -5,7 +5,7 @@ #include "GEOS/GeosTargets.h" #define GEOS_TEMP_NAME "PATCHING.TMP" -#define GEOS_BACKUP_NAME "REPLACED.BAK" +#define GEOS_BACKUP_TAG ".BAK" #define GEOS_COPY_TAG "-TR" #define GEOS_EXT_LENGTH 4 #define GEOS_MAX_IMAGES 8 @@ -154,6 +154,20 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch return Patched; } +FLASHMEM static void GeosBackupPath(char *Backup, size_t Size, const char *Copy) +{ + snprintf(Backup, Size, "%s" GEOS_BACKUP_TAG, Copy); +} + +// A copy renamed aside and never put back means power dropped between the two renames of GeosPatchCopy +FLASHMEM static void GeosRestoreCopy(const char *Copy) +{ + char Backup[MaxNamePathLength]; + + GeosBackupPath(Backup, sizeof Backup, Copy); + if (!SD.exists(Copy) && SD.exists(Backup)) SD.rename(Backup, Copy); +} + // Patches a temporary copy and renames it into place, keeping the copy it replaces until the new one is in, so a // failure never leaves a half-patched copy behind or loses the one GEOS saved to FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, @@ -163,10 +177,10 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch bool Replacing = SD.exists(Copy); GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); - GeosPath(Backup, sizeof Backup, Launcher, GEOS_BACKUP_NAME); - if (Replacing && SD.exists(Backup)) snprintf(Error, sizeof Error, "%s is left from an earlier try: remove or restore it", GEOS_BACKUP_NAME); - else if (GeosCopyFile(Source, Temp) && (!Patch || GeosPatchFile(Launcher, Temp, Error, sizeof Error)) && - (!Replacing || SD.rename(Copy, Backup))) + GeosBackupPath(Backup, sizeof Backup, Copy); + if (Replacing) SD.remove(Backup); + if (GeosCopyFile(Source, Temp) && (!Patch || GeosPatchFile(Launcher, Temp, Error, sizeof Error)) && + (!Replacing || SD.rename(Copy, Backup))) { if (SD.rename(Temp, Copy)) { @@ -214,6 +228,7 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c if (Found.HasDriver && !GeosBoots(Launcher)) return GeosUse(Launcher, Name, BootName); NeedsDriver = !Found.HasDriver; + GeosRestoreCopy(Copy); if (SD.exists(Copy)) { // The copy is the disk GEOS saves the user's work to: one that cannot be read is left alone, not replaced From 7266e1300986cf048c3303ed1fffdab21b8e4a5d Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 20:59:49 -0500 Subject: [PATCH 061/100] firmware: stop a launch that could not put a set-aside GEOS copy back Without this a failed restore fell through to making a fresh copy, leaving the user's saved work stranded in the .BAK file. --- Source/Teensy/GeosLauncher.ino | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index bdee8402..c7ae4368 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -160,12 +160,12 @@ FLASHMEM static void GeosBackupPath(char *Backup, size_t Size, const char *Copy) } // A copy renamed aside and never put back means power dropped between the two renames of GeosPatchCopy -FLASHMEM static void GeosRestoreCopy(const char *Copy) +FLASHMEM static bool GeosRestoreCopy(const char *Copy) { char Backup[MaxNamePathLength]; GeosBackupPath(Backup, sizeof Backup, Copy); - if (!SD.exists(Copy) && SD.exists(Backup)) SD.rename(Backup, Copy); + return SD.exists(Copy) || !SD.exists(Backup) || SD.rename(Backup, Copy); } // Patches a temporary copy and renames it into place, keeping the copy it replaces until the new one is in, so a @@ -228,7 +228,11 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c if (Found.HasDriver && !GeosBoots(Launcher)) return GeosUse(Launcher, Name, BootName); NeedsDriver = !Found.HasDriver; - GeosRestoreCopy(Copy); + if (!GeosRestoreCopy(Copy)) + { + SendMsgPrintfln("%s\r\nwas set aside and could not be put back", CopyName); + return true; + } if (SD.exists(Copy)) { // The copy is the disk GEOS saves the user's work to: one that cannot be read is left alone, not replaced From 8d129a2a2b22ceaafda43ed1ea1289c5f32a9f98 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 21:06:13 -0500 Subject: [PATCH 062/100] firmware: say where the GEOS disk is when a failed copy cannot be rolled back A failed swap whose rollback rename also failed left the user's disk in the .BAK file with only a generic copy error shown. The message now names the .BAK path. --- Source/Teensy/GeosLauncher.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index c7ae4368..3655d74a 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -187,7 +187,7 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch if (Replacing) SD.remove(Backup); return true; } - if (Replacing) SD.rename(Backup, Copy); + if (Replacing && !SD.rename(Backup, Copy)) snprintf(Error, sizeof Error, "your disk is now %s", Backup); } SD.remove(Temp); SendMsgPrintfln(Patch ? "Could not add the mouse driver to\r\na copy of %s:\r\n%s" : "Could not make a copy of\r\n%s:\r\n%s", Name, Error); From dc8b09e207be224c65136cbebf344ad16d257aaf Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 21:06:13 -0500 Subject: [PATCH 063/100] firmware: drop the stale GEOS backup when the copy it preceded is in place Power loss after the swap but before the backup was removed left both files. The copy is the complete newer one, so the backup is stale and is removed. --- Source/Teensy/GeosLauncher.ino | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 3655d74a..35252df3 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -165,7 +165,12 @@ FLASHMEM static bool GeosRestoreCopy(const char *Copy) char Backup[MaxNamePathLength]; GeosBackupPath(Backup, sizeof Backup, Copy); - return SD.exists(Copy) || !SD.exists(Backup) || SD.rename(Backup, Copy); + if (SD.exists(Copy)) + { + SD.remove(Backup); + return true; + } + return !SD.exists(Backup) || SD.rename(Backup, Copy); } // Patches a temporary copy and renames it into place, keeping the copy it replaces until the new one is in, so a From 04011cb4b38d101c1af7b1a8a3271c79a644bf7c Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 21:06:52 -0500 Subject: [PATCH 064/100] firmware: size the copy error buffer to hold a full backup path --- Source/Teensy/GeosLauncher.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 35252df3..b092b4a1 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -178,7 +178,7 @@ FLASHMEM static bool GeosRestoreCopy(const char *Copy) FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, const char *Copy, bool Patch) { - char Temp[MaxNamePathLength], Backup[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; + char Temp[MaxNamePathLength], Backup[MaxNamePathLength], Error[MaxNamePathLength + GEOS_ERROR_SIZE] = "the SD card could not be written"; bool Replacing = SD.exists(Copy); GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); From 6e890a203b677254c35ebdac32a6e3434ed4a189 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 21:08:15 -0500 Subject: [PATCH 065/100] firmware: bound SendMsgPrintfln to its buffer The wider GeosPatchCopy error text can exceed the 256-byte line buffer. --- Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino b/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino index 7b54cca1..5a03a0ea 100644 --- a/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino +++ b/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino @@ -415,7 +415,7 @@ void SendMsgPrintfln(const char *Fmt, ...) va_list ap; va_start(ap,Fmt); - vsprintf(SerialStringBuf, Fmt, ap); + vsnprintf(SerialStringBuf, sizeof SerialStringBuf, Fmt, ap); va_end(ap); Serial.printf("%s\n", SerialStringBuf); From ef59e5e79eb06b1eee2bee6e0e5ed621af798cd4 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Tue, 20 Oct 2026 21:51:02 -0500 Subject: [PATCH 066/100] firmware: compare the transport span as unsigned so gcc's -Werror accepts it --- Source/Teensy/GEOS/GeosBoot.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 16de454d..9d2b3231 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -70,7 +70,7 @@ static bool GeosReplaceTransport(uint8_t *Driver) uint16_t Start = GeosWord(Driver + 2 * VecInitForIO), End = GeosWord(Driver + 2 * VecReadBlock); uint8_t Code[sizeof TRDriverCode]; - if (End <= Start || End - Start < sizeof Code) return false; + if (End <= Start || (size_t)(End - Start) < sizeof Code) return false; for (int i = VecDoneWithIO; i <= VecNewDisk; i++) { uint16_t Routine = GeosWord(Driver + 2 * i); From 2aa0d57e14b6772ee3c4db3c8748b53e26abd91d Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Wed, 21 Oct 2026 09:12:24 -0500 Subject: [PATCH 067/100] firmware: drop the GEOS 128 boot disk setup item until GEOS 128 launches --- Source/Teensy/GEOS/GeosDiskPatch.h | 2 +- Source/Teensy/GEOS/GeosTargets.h | 10 ---------- Source/Teensy/GeosLauncher.ino | 8 +++----- Source/Teensy/MainMenuItems.h | 1 - Source/Teensy/tests/geos_disk_patch_harness.c | 8 +++++++- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index f0d8b23f..e9aee1e0 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -56,7 +56,7 @@ struct GeosTarget const uint8_t *DriverCode, *DriverInfo; uint16_t DriverSize; uint8_t InputType; - const char *BootFile; // what the TR starts the disk from; none for a version it only sets up + const char *BootFile; // what the TR starts the disk from uint16_t BootEntry; }; diff --git a/Source/Teensy/GEOS/GeosTargets.h b/Source/Teensy/GEOS/GeosTargets.h index 7017e22c..c96b9f46 100644 --- a/Source/Teensy/GEOS/GeosTargets.h +++ b/Source/Teensy/GEOS/GeosTargets.h @@ -2,10 +2,8 @@ // The GEOS versions the TR sets up from a boot disk image the user puts on the SD card. #pragma once #include "GeosDiskPatch.h" -#include "TRMouse128.h" #include "TRMouse64.h" -#define GEOS_INPUT_128 15 #define GEOS_INPUT_64 10 #define GEOS64_BOOT_FILE "GEOBOOT" #define GEOS64_BOOT_ENTRY 0x2f3c @@ -16,14 +14,6 @@ struct GeosLauncher struct GeosTarget Target; }; -static inline bool GeosBoots(const struct GeosLauncher *Launcher) { return Launcher->Target.BootFile != NULL; } - -static const struct GeosLauncher Geos128Launcher = -{ - "GEOS 128", "/GEOS128", - { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, NULL, 0 }, -}; - static const struct GeosLauncher Geos64Launcher = { "GEOS 64", "/GEOS64", diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index b092b4a1..b3eae932 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -212,8 +212,7 @@ FLASHMEM static bool GeosUse(const struct GeosLauncher *Launcher, const char *Na } // Whether the image is a boot disk, set up or not: only with none does the user need telling where to copy one. -// BootName gets the disk to start from: the original when it is set up and only the setup is wanted, else a copy, -// since GEOS saves to the disk it starts from +// BootName gets the disk to start from: always a copy, since GEOS saves to the disk it starts from FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const char *Name, char *BootName) { char Source[MaxNamePathLength], CopyName[MaxItemNameLength + sizeof GEOS_COPY_TAG], Copy[MaxNamePathLength]; @@ -230,7 +229,6 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return false; } if (!Found.HasDeskTop) return false; - if (Found.HasDriver && !GeosBoots(Launcher)) return GeosUse(Launcher, Name, BootName); NeedsDriver = !Found.HasDriver; if (!GeosRestoreCopy(Copy)) @@ -305,12 +303,12 @@ FLASHMEM bool LaunchGeos(const struct GeosLauncher *Launcher, StructMenuItem *It return false; } Count = GeosListImages(Launcher, Names); - for (uint8_t i = 0; i < Count && !(GeosBoots(Launcher) && *BootName); i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i], BootName); + for (uint8_t i = 0; i < Count && !*BootName; i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i], BootName); if (!BootDiskFound) { SendMsgPrintfln("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again.", Launcher->Title, Launcher->Folder); return false; } - return GeosBoots(Launcher) && *BootName && GeosStart(Launcher, BootName, Item); + return *BootName && GeosStart(Launcher, BootName, Item); } diff --git a/Source/Teensy/MainMenuItems.h b/Source/Teensy/MainMenuItems.h index b18ddacd..be3e65cc 100644 --- a/Source/Teensy/MainMenuItems.h +++ b/Source/Teensy/MainMenuItems.h @@ -296,7 +296,6 @@ StructMenuItem dirTeensyROMSpecific[] = rtFilePrg , IOH_TR_BASIC , (char*)"BASIC TR Command IO access only" , (uint8_t*)empty_prg , sizeof(empty_prg) , rtBin8kLo , IOH_TeensyROM , (char*)"(This) TeensyROM Menu Cart" , (uint8_t*)TeensyROMC64_bin , sizeof(TeensyROMC64_bin) , rtFilePrg , IOH_TeensyROM , (char*)"TeensyROM External Ports Test" , (uint8_t*)TRExtPortCheck_prg , sizeof(TRExtPortCheck_prg) , - rtGEOS , IOH_None , (char*)"GEOS 128 Boot Disk Setup" , (uint8_t*)&Geos128Launcher , sizeof(Geos128Launcher) , rtGEOS , IOH_GeoRAM , (char*)"GEOS 64" , (uint8_t*)&Geos64Launcher , sizeof(Geos64Launcher) , #ifdef Fab04_REU rtFilePrg , IOH_TeensyROM , (char*)"TR+ C64 Expansion Port Test" , (uint8_t*)ExpansionPortTest_prg , sizeof(ExpansionPortTest_prg) , diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c index 6d369821..88b5f30e 100644 --- a/Source/Teensy/tests/geos_disk_patch_harness.c +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -5,6 +5,9 @@ #include #include "../GEOS/GeosTargets.h" +#include "../GEOS/TRMouse128.h" + +#define GEOS_INPUT_128 15 #define MAX_IMAGE 819200 @@ -23,6 +26,9 @@ static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) return true; } +static const struct GeosTarget Geos128Target = + { "128 DESKTOP", TRMouse128Name, TRMouse128Code, TRMouse128Info, sizeof TRMouse128Code, GEOS_INPUT_128, NULL, 0 }; + static int Inspect(struct GeosDisk *Disk) { struct GeosBootDisk Found; @@ -53,7 +59,7 @@ int main(int argc, char **argv) memcpy(Original, Image, Size); Disk.Format = GeosFormatForSize(Size); - Disk.Target = &Geos128Launcher.Target; + Disk.Target = &Geos128Target; Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; From cb011546fd62e6fc18a1008ca28b4c8d615da403 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Wed, 21 Oct 2026 12:39:21 -0500 Subject: [PATCH 068/100] firmware: answer the TR block window first so its bytes reach the bus in time On an NTSC C64 with a Kawari VIC-II, the block device's ID came back wrong in every read and window bytes now and then, so GEOS found no disk. A read now splits the page once, answers the window from a pointer kept by PAGE writes, and answers the registers in one pass. --- Source/Teensy/BlockDevice/TRBlock.h | 32 +++++++++---------- .../Common/IO_Handlers/TRBlockIO.c | 23 ++++++++++--- Source/Teensy/tests/trblock_harness.c | 3 +- docs/TR_Block_Device.md | 2 ++ 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Source/Teensy/BlockDevice/TRBlock.h b/Source/Teensy/BlockDevice/TRBlock.h index 08469765..345c3021 100644 --- a/Source/Teensy/BlockDevice/TRBlock.h +++ b/Source/Teensy/BlockDevice/TRBlock.h @@ -52,7 +52,8 @@ static const char TRBlockIdText[] = "TRB"; struct TRBlock { uint8_t Buffer[TRBLOCK_SIZE]; - uint8_t Addr0, Addr1, Page, Token; + uint8_t *Window; // the page of Buffer the window shows, kept by page writes so a read does no arithmetic + uint8_t Addr0, Addr1, Token; uint8_t JobCmd, JobAddr0, JobAddr1, JobToken; volatile uint8_t Status, Echo; volatile bool Pending; @@ -72,26 +73,23 @@ struct TRBlockBackend static inline void TRBlockReset(struct TRBlock *Dev) { memset((void *)Dev, 0, sizeof *Dev); + Dev->Window = Dev->Buffer; } -// The IO ISR's side: reads answer from registers or the window... -static inline bool TRBlockServes(uint8_t Offset) -{ - return Offset == TRBlockStatus || Offset == TRBlockEcho || - (Offset >= TRBlockId && Offset < TRBlockId + TRBLOCK_ID_SIZE) || - (Offset >= TRBlockWindow && Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE); -} +#define TRBLOCK_UNSERVED (-1) -static inline uint8_t TRBlockReadAt(const struct TRBlock *Dev, uint8_t Offset) +// The IO ISR's side: a read answers from the window or a register, else TRBLOCK_UNSERVED +static inline int TRBlockReadAt(const struct TRBlock *Dev, uint8_t Offset) { - if (Offset == TRBlockStatus) return Dev->Status; - if (Offset == TRBlockEcho) return Dev->Echo; - if (Offset >= TRBlockId && Offset < TRBlockId + TRBLOCK_ID_SIZE) return (uint8_t)TRBlockIdText[Offset - TRBlockId]; - if (Offset >= TRBlockWindow && Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE) + if (Offset >= TRBlockWindow) { - return Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow]; + if (Offset >= TRBlockWindow + TRBLOCK_WINDOW_SIZE) return TRBLOCK_UNSERVED; + return Dev->Window[Offset - TRBlockWindow]; } - return 0xff; + if (Offset == TRBlockStatus) return Dev->Status; + if (Offset == TRBlockEcho) return Dev->Echo; + if ((uint8_t)(Offset - TRBlockId) < TRBLOCK_ID_SIZE) return (uint8_t)TRBlockIdText[Offset - TRBlockId]; + return TRBLOCK_UNSERVED; } // ...and writes go to registers or the window. The caller reads the data byte only when this says @@ -108,12 +106,12 @@ static inline void TRBlockWriteAt(struct TRBlock *Dev, uint8_t Offset, uint8_t D if (Offset >= TRBlockWindow) { - if (Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE && !Busy) Dev->Buffer[(Dev->Page & 3) * TRBLOCK_WINDOW_SIZE + Offset - TRBlockWindow] = Data; + if (Offset < TRBlockWindow + TRBLOCK_WINDOW_SIZE && !Busy) Dev->Window[Offset - TRBlockWindow] = Data; return; } if (Offset == TRBlockAddr0) Dev->Addr0 = Data; else if (Offset == TRBlockAddr1) Dev->Addr1 = Data; - else if (Offset == TRBlockPage) Dev->Page = Data; + else if (Offset == TRBlockPage) Dev->Window = Dev->Buffer + (Data & 3) * TRBLOCK_WINDOW_SIZE; else if (Offset == TRBlockToken) Dev->Token = Data; else if (Offset == TRBlockCmd && !Busy) { diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index c11e667e..2d0cbabb 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -73,13 +73,28 @@ void PollTRBlock() if (TRBlockDirty && millis() - TRBlockLastWritemS > TRBlock_FlushIdlemS) TRBlockFileFlush(NULL); } -// One IO access to the page the mouse registers start: the mouse's and the block device's offsets do not overlap -void TRPageIO(uint8_t Address, bool R_Wn) +// One IO access to the page the mouse registers start: the mouse owns the offsets below the block device's. +// A read that reaches DataPortWriteWait late is latched wrong on some C64s, so the window is answered first. +__attribute__((always_inline)) inline void TRPageIO(uint8_t Address, bool R_Wn) { + uint8_t WindowOffset = Address - TRBlockWindow; + if (R_Wn) { - USBMouseIORead(Address); - if (TRBlockServes(Address)) DataPortWriteWaitLog(TRBlockReadAt(&TRBlockDev, Address)); + int Data; + + if (WindowOffset < TRBLOCK_WINDOW_SIZE) + { + DataPortWriteWaitLog(TRBlockDev.Window[WindowOffset]); + return; + } + if (Address < TRBlockCmd) + { + USBMouseIORead(Address); + return; + } + Data = TRBlockReadAt(&TRBlockDev, Address); + if (Data != TRBLOCK_UNSERVED) DataPortWriteWaitLog(Data); } else if (TRBlockTakes(Address)) { diff --git a/Source/Teensy/tests/trblock_harness.c b/Source/Teensy/tests/trblock_harness.c index 6b190363..9b988cab 100644 --- a/Source/Teensy/tests/trblock_harness.c +++ b/Source/Teensy/tests/trblock_harness.c @@ -68,7 +68,8 @@ int main(int argc, char **argv) } else if (sscanf(Line, "r %x", &Offset) == 1) { - if (TRBlockServes(Offset)) printf("%02x\n", TRBlockReadAt(&Dev, Offset)); + int Read = TRBlockReadAt(&Dev, Offset); + if (Read != TRBLOCK_UNSERVED) printf("%02x\n", Read); else printf("open\n"); } else if (!strncmp(Line, "svc", 3)) printf("%d\n", TRBlockService(&Dev, Use)); diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index 96ad5fe9..ed3dc822 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -61,6 +61,8 @@ Two rules that Wes Biggs' [teensyrom-vdrive](https://github.com/wesbiggs/teensyr * **Read STATUS until two consecutive reads agree** before believing a value other than `$80`. A read can land while the firmware is updating it. * **Read back what you write into the window** when the data matters, and compare, before sending CMD. +On an NTSC C64 with a Kawari VIC-II, a TR+ read now and then comes back with the previous read's bits ORed in, so **read every byte until two consecutive reads agree**, the window and ID included, and compare a read-back again before calling it a fault. + Do not write ADDR0, ADDR1, PAGE or TOKEN, or the window, while STATUS is `$80`. The firmware latches the command's operands when CMD is written, so a stray register write cannot change a command already running, but writes to the window during a command are ignored. ## Commands From 8aa89396a4e7ae44fe7240fa1f39ac14d990074b Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Wed, 21 Oct 2026 12:39:54 -0500 Subject: [PATCH 069/100] tools: the GEOS disk driver reads each TR byte until two reads agree On an NTSC C64 with a Kawari VIC-II, a TR+ read now and then returns the previous read's bits ORed in. With the double read patched out of the driver in GEOS memory, 3 of 200 blocks read wrong; with it back, the same blocks read clean. --- Source/Teensy/GEOS/TRDriver.h | 77 ++++++++++++++++++----------------- tools/geos/trdrive.asm | 26 +++++++++++- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/Source/Teensy/GEOS/TRDriver.h b/Source/Teensy/GEOS/TRDriver.h index 43424b05..07a38bb9 100644 --- a/Source/Teensy/GEOS/TRDriver.h +++ b/Source/Teensy/GEOS/TRDriver.h @@ -9,46 +9,49 @@ #endif #define TR_DRIVER_BASE 0x9000 -static const uint8_t TRDriverCode[516] PROGMEM = { - 0x08, 0x68, 0x8d, 0xf9, 0x91, 0x78, 0xa5, 0x01, 0x8d, 0xfa, 0x91, 0xa9, 0x36, 0x85, 0x01, 0xad, - 0x1a, 0xd0, 0x8d, 0x03, 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, 0x03, 0x92, - 0x8d, 0x1a, 0xd0, 0xad, 0xfa, 0x91, 0x85, 0x01, 0xad, 0xf9, 0x91, 0x48, 0x28, 0x60, 0xa2, 0x00, +static const uint8_t TRDriverCode[555] PROGMEM = { + 0x08, 0x68, 0x8d, 0x20, 0x92, 0x78, 0xa5, 0x01, 0x8d, 0x21, 0x92, 0xa9, 0x36, 0x85, 0x01, 0xad, + 0x1a, 0xd0, 0x8d, 0x2a, 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, 0x2a, 0x92, + 0x8d, 0x1a, 0xd0, 0xad, 0x21, 0x92, 0x85, 0x01, 0xad, 0x20, 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, 0x7e, 0x91, 0xa0, 0x00, 0x60, 0x20, 0x7e, 0x91, 0x20, - 0xd3, 0x90, 0x8a, 0x48, 0x20, 0x7e, 0x91, 0x68, 0xaa, 0xa0, 0x00, 0x60, 0x20, 0xb1, 0x91, 0xb0, - 0x26, 0xa9, 0x01, 0x20, 0x0b, 0x91, 0xe0, 0x00, 0xd0, 0xdf, 0xa9, 0x00, 0xa2, 0x0b, 0x20, 0xf2, - 0x91, 0xa2, 0x40, 0x20, 0xee, 0x91, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0xee, 0x91, 0xc8, - 0x91, 0x0a, 0xa2, 0x00, 0xa0, 0x00, 0x60, 0xa2, 0x0d, 0xa0, 0x00, 0x60, 0x20, 0xb1, 0x91, 0xb0, - 0xf6, 0xa9, 0x01, 0x20, 0x0b, 0x91, 0xe0, 0x00, 0xd0, 0x28, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xfe, - 0x91, 0xad, 0xfe, 0x91, 0xa2, 0x0b, 0x20, 0xf2, 0x91, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0x91, 0x0a, - 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf5, 0xee, 0xfe, 0x91, 0xad, 0xfe, 0x91, 0xc9, 0x04, 0xd0, 0xe1, - 0xa2, 0x00, 0x60, 0x20, 0xb1, 0x91, 0xb0, 0xbf, 0xa0, 0x00, 0xa9, 0x00, 0x8d, 0xfe, 0x91, 0xad, - 0xfe, 0x91, 0xa2, 0x0b, 0x20, 0xf2, 0x91, 0xa2, 0x00, 0xb1, 0x0a, 0x9d, 0x40, 0xdf, 0xdd, 0x40, - 0xdf, 0xd0, 0x15, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0xfe, 0x91, 0xad, 0xfe, 0x91, 0xc9, - 0x04, 0xd0, 0xdc, 0xa9, 0x02, 0x4c, 0x0b, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0xfc, 0x91, 0xee, 0xff, - 0x91, 0xd0, 0x03, 0xee, 0xff, 0x91, 0xad, 0xff, 0x91, 0xa2, 0x0c, 0x20, 0xf2, 0x91, 0xa5, 0x04, - 0xa2, 0x09, 0x20, 0xf2, 0x91, 0xa5, 0x05, 0xa2, 0x0a, 0x20, 0xf2, 0x91, 0xad, 0xfc, 0x91, 0xa2, - 0x08, 0x20, 0xf2, 0x91, 0xa9, 0x00, 0x8d, 0x01, 0x92, 0x8d, 0x02, 0x92, 0xa2, 0x0d, 0x20, 0xee, - 0x91, 0x8d, 0xfd, 0x91, 0x20, 0xee, 0x91, 0xcd, 0xfd, 0x91, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, - 0xee, 0x01, 0x92, 0xd0, 0xe7, 0xee, 0x02, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, - 0x20, 0xee, 0x91, 0xcd, 0xff, 0x91, 0xf0, 0x04, 0x68, 0x4c, 0x50, 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, 0xfb, 0x91, 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0xfb, - 0x91, 0x91, 0x0a, 0x98, 0x38, 0xe9, 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, - 0x60, 0xad, 0x00, 0x92, 0xd0, 0x20, 0xa2, 0xdf, 0x20, 0xda, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, - 0xda, 0x91, 0xb0, 0x14, 0x8e, 0x00, 0x92, 0x8e, 0xf0, 0x91, 0x8e, 0xf4, 0x91, 0x8e, 0xbd, 0x90, - 0x8e, 0xed, 0x90, 0x8e, 0xf0, 0x90, 0x18, 0x60, 0x38, 0x60, 0x8e, 0xe1, 0x91, 0xa0, 0x02, 0xb9, - 0x10, 0xdf, 0xd9, 0xf6, 0x91, 0xd0, 0x05, 0x88, 0x10, 0xf5, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, - 0xdf, 0x60, 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 + 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, 0x19, + 0x92, 0xa2, 0x40, 0x20, 0x0a, 0x92, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0x0a, 0x92, 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, 0x25, + 0x92, 0xad, 0x25, 0x92, 0xa2, 0x0b, 0x20, 0x19, 0x92, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0xdd, 0x40, + 0xdf, 0xd0, 0xf8, 0x91, 0x0a, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0x25, 0x92, 0xad, 0x25, + 0x92, 0xc9, 0x04, 0xd0, 0xdc, 0xa2, 0x00, 0x60, 0x20, 0xbb, 0x91, 0xb0, 0xba, 0xa0, 0x00, 0xa9, + 0x00, 0x8d, 0x25, 0x92, 0xad, 0x25, 0x92, 0xa2, 0x0b, 0x20, 0x19, 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, 0x25, 0x92, 0xad, 0x25, 0x92, 0xc9, 0x04, 0xd0, 0xd7, 0xa9, 0x02, 0x4c, + 0x15, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0x23, 0x92, 0xee, 0x26, 0x92, 0xd0, 0x03, 0xee, 0x26, 0x92, + 0xad, 0x26, 0x92, 0xa2, 0x0c, 0x20, 0x19, 0x92, 0xa5, 0x04, 0xa2, 0x09, 0x20, 0x19, 0x92, 0xa5, + 0x05, 0xa2, 0x0a, 0x20, 0x19, 0x92, 0xad, 0x23, 0x92, 0xa2, 0x08, 0x20, 0x19, 0x92, 0xa9, 0x00, + 0x8d, 0x28, 0x92, 0x8d, 0x29, 0x92, 0xa2, 0x0d, 0x20, 0x06, 0x92, 0x8d, 0x24, 0x92, 0x20, 0x06, + 0x92, 0xcd, 0x24, 0x92, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0x28, 0x92, 0xd0, 0xe7, 0xee, + 0x29, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0x06, 0x92, 0xcd, 0x26, 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, 0x22, 0x92, + 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0x22, 0x92, 0x91, 0x0a, 0x98, 0x38, 0xe9, + 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xad, 0x27, 0x92, 0xd0, 0x26, + 0xa2, 0xdf, 0x20, 0xea, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, 0xea, 0x91, 0xb0, 0x1a, 0x8e, 0x27, + 0x92, 0x8e, 0x08, 0x92, 0x8e, 0x1b, 0x92, 0x8e, 0xbd, 0x90, 0x8e, 0xc0, 0x90, 0x8e, 0xf2, 0x90, + 0x8e, 0xf5, 0x90, 0x8e, 0xfa, 0x90, 0x18, 0x60, 0x38, 0x60, 0x8e, 0xf4, 0x91, 0x8e, 0xf7, 0x91, + 0xa0, 0x02, 0xb9, 0x10, 0xdf, 0xd9, 0x10, 0xdf, 0xd0, 0xf8, 0xd9, 0x1d, 0x92, 0xd0, 0x05, 0x88, + 0x10, 0xf0, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, 0xdf, 0x60, 0x20, 0x06, 0x92, 0x8d, 0x24, 0x92, + 0x20, 0x06, 0x92, 0xcd, 0x24, 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 TRDriverRelocations[63] PROGMEM = { +static const uint16_t TRDriverRelocations[70] PROGMEM = { 0x0003, 0x0009, 0x0013, 0x002e, 0x0034, 0x0039, 0x0050, 0x0057, 0x005d, 0x0060, 0x0065, 0x006d, 0x0074, 0x007f, 0x0084, 0x008d, - 0x009d, 0x00a4, 0x00af, 0x00b2, 0x00b7, 0x00c7, 0x00ca, 0x00d4, 0x00dd, 0x00e0, 0x00e5, 0x00fa, 0x00fd, 0x0106, 0x010c, 0x010f, - 0x0114, 0x0117, 0x011c, 0x0123, 0x012a, 0x012d, 0x0132, 0x0137, 0x013a, 0x013f, 0x0142, 0x0145, 0x0148, 0x0151, 0x0156, 0x0161, - 0x0164, 0x016a, 0x0194, 0x019f, 0x01b2, 0x01b9, 0x01c0, 0x01c5, 0x01c8, 0x01cb, 0x01ce, 0x01d1, 0x01d4, 0x01db, 0x01e3 + 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, 0x01bc, 0x01c3, 0x01ca, 0x01cf, 0x01d2, 0x01d5, 0x01d8, 0x01db, 0x01de, 0x01e1, 0x01e4, 0x01eb, + 0x01ee, 0x01fb, 0x020b, 0x020e, 0x0211, 0x0214 }; static const uint16_t TRDriverEntries[13] PROGMEM = { 0x0000, 0x0026, 0x0041, 0x0041, 0x003e, 0x0047, 0x0042, 0x004f, 0x005c, 0x0042, 0x0042, 0x0042, 0x006c diff --git a/tools/geos/trdrive.asm b/tools/geos/trdrive.asm index 22e56fef..ab2e4bcb 100644 --- a/tools/geos/trdrive.asm +++ b/tools/geos/trdrive.asm @@ -133,11 +133,11 @@ ReadLink ldx #REG_PAGE jsr putReg ldx #WINDOW - jsr getReg + jsr getStable ldy #0 sta (r4),y ldx #WINDOW + 1 - jsr getReg + jsr getStable iny sta (r4),y ldx #0 @@ -166,6 +166,9 @@ readPage ldx #0 windowRead lda BASE_GEOS_RAM * 256 + WINDOW,x +windowReread + cmp BASE_GEOS_RAM * 256 + WINDOW,x + bne windowRead sta (r4),y iny inx @@ -195,8 +198,12 @@ windowWrite windowStore sta BASE_GEOS_RAM * 256 + WINDOW,x windowCheck + cmp BASE_GEOS_RAM * 256 + WINDOW,x + beq windowStored +windowRecheck cmp BASE_GEOS_RAM * 256 + WINDOW,x bne windowFault +windowStored iny inx cpx #WINDOW_SIZE @@ -321,8 +328,10 @@ settle stx getReg + 2 stx putReg + 2 stx windowRead + 2 + stx windowReread + 2 stx windowStore + 2 stx windowCheck + 2 + stx windowRecheck + 2 located clc rts @@ -332,9 +341,13 @@ absent tryBase stx probe + 2 + stx probeAgain + 2 ldy #2 probe lda BASE_GEOS_RAM * 256 + $10,y +probeAgain + cmp BASE_GEOS_RAM * 256 + $10,y + bne probe cmp trbId,y bne notHere dey @@ -349,6 +362,15 @@ getReg lda BASE_GEOS_RAM * 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 +getStable + jsr getReg + sta seen + jsr getReg + cmp seen + bne getStable + rts + putReg sta BASE_GEOS_RAM * 256,x rts From 0300249d4f5b7a02ceb9524b9156819427b1a62f Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Wed, 21 Oct 2026 14:26:16 -0500 Subject: [PATCH 070/100] firmware: list GEOS 64 last in TeensyROM Specific --- Source/Teensy/MainMenuItems.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/MainMenuItems.h b/Source/Teensy/MainMenuItems.h index be3e65cc..e52bfdd0 100644 --- a/Source/Teensy/MainMenuItems.h +++ b/Source/Teensy/MainMenuItems.h @@ -296,10 +296,10 @@ StructMenuItem dirTeensyROMSpecific[] = rtFilePrg , IOH_TR_BASIC , (char*)"BASIC TR Command IO access only" , (uint8_t*)empty_prg , sizeof(empty_prg) , rtBin8kLo , IOH_TeensyROM , (char*)"(This) TeensyROM Menu Cart" , (uint8_t*)TeensyROMC64_bin , sizeof(TeensyROMC64_bin) , rtFilePrg , IOH_TeensyROM , (char*)"TeensyROM External Ports Test" , (uint8_t*)TRExtPortCheck_prg , sizeof(TRExtPortCheck_prg) , - rtGEOS , IOH_GeoRAM , (char*)"GEOS 64" , (uint8_t*)&Geos64Launcher , sizeof(Geos64Launcher) , #ifdef Fab04_REU rtFilePrg , IOH_TeensyROM , (char*)"TR+ C64 Expansion Port Test" , (uint8_t*)ExpansionPortTest_prg , sizeof(ExpansionPortTest_prg) , #endif + rtGEOS , IOH_GeoRAM , (char*)"GEOS 64" , (uint8_t*)&Geos64Launcher , sizeof(Geos64Launcher) , //could repeat these here... //rtFilePrg , IOH_ASID , (char*)"TeensyROM ASID Player +TR ASID", (uint8_t*)ASIDPlayer_prg , sizeof(ASIDPlayer_prg) , //rtFilePrg , IOH_TeensyROM , (char*)"MIDI2SID +TeensyROM MIDI", (uint8_t*)MIDI2SID_prg , sizeof(MIDI2SID_prg) , From ce6ffef9fd36fb6701b0e828cb3440d7863146d3 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:09:16 -0500 Subject: [PATCH 071/100] firmware: the USB mouse registers live only beside GeoRAM, and plain REU mode leaves IO1 alone again Serving them at $DE00 beside the REU made every read of $DE00/$DE01 consume mouse movement, and IO1 probes for SwiftLink, RR-Net or MIDI saw changing values. The GEOS 128 driver now probes only $DF03, so it stops reading $DE03-$DE05 every frame. --- .../MinimalBoot/Common/IO_Handlers/IOH_REU.c | 9 +------ .../MinimalBoot/Common/IO_Handlers/USBMouse.c | 4 +-- tools/geos/trmouse128.asm | 26 +++++++------------ 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index 9a76752c..b602bf91 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c @@ -51,7 +51,6 @@ __attribute__((always_inline)) inline void Printf_dbg_reu(...) {}; #endif -void IO1Hndlr_REU(uint8_t Address, bool R_Wn); void IO2Hndlr_REU(uint8_t Address, bool R_Wn); void PollingHndlr_REU(); void InitHndlr_REU(); @@ -60,7 +59,7 @@ stcIOHandlers IOHndlr_REU = { "512KB REU", //Name of handler &InitHndlr_REU, //Called once at handler startup - &IO1Hndlr_REU, //IO1 R/W handler + NULL, //IO1 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 @@ -470,7 +469,6 @@ FLASHMEM void InitHndlr_REU() //set reg defaults: uint8_t REURegsInit[REUReg_NumRegs]={0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x1f, 0x3f}; memcpy(REURegs, REURegsInit, REUReg_NumRegs); - InitUSBMouse(); #ifdef Direct_REU Printf_dbg_reu("Direct REU mode\n"); @@ -552,11 +550,6 @@ FLASHMEM void InitHndlr_REU() } //InitHndlr_REU -void IO1Hndlr_REU(uint8_t Address, bool R_Wn) -{ - if (R_Wn) USBMouseIORead(Address); -} - void IO2Hndlr_REU(uint8_t Address, bool R_Wn) { #ifdef DbgIOTraceLog diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index adbe925e..07095e6d 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 that a RAM expansion handler -// serves from the IO page it leaves free: $DF00 beside GeoRAM, $DE00 beside the REU. +// 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. // 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/tools/geos/trmouse128.asm b/tools/geos/trmouse128.asm index d18bcd93..cc6f1d3e 100644 --- a/tools/geos/trmouse128.asm +++ b/tools/geos/trmouse128.asm @@ -17,6 +17,7 @@ BUTTON_UP = $80 NO_DIRECTION = $ff OFF_TOP = 228 ; the kernal reads a mouseYPos this high as above the top +regs = $df00 regX = 0 regY = 1 regButtons = 2 @@ -44,28 +45,19 @@ SlowMouse SetMouse rts -; The registers are at $DF00 beside GeoRAM and $DE00 beside the REU; the ID tells which. +; 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: neither page has the ID, so what reads there is not a mouse. +; Carry set: what reads there is not a mouse. FindRegs - ldy #$df -nextPage - sty readID+2 ldx #2 -readID lda $df00+regID,x +- lda regs+regID,x cmp trID,x bne + dex - bpl readID - sty readX+2 - sty readY+2 - sty readButtons+2 + bpl - clc rts -+ dey - cpy #$dd - bne nextPage - sec ++ sec rts trID !text "TRM" @@ -73,16 +65,16 @@ trID !text "TRM" UpdateMouse jsr FindRegs bcs noMouse -readX lda $df00+regX + lda regs+regX sta deltaX -readY lda $df00+regY + lda regs+regY sta deltaY bit mouseOn bmi readButtons noMouse rts ; movement while the pointer is off is dropped readButtons - lda $df00+regButtons + lda regs+regButtons and #LEFT_BUTTON beq + lda #0 ; pressed From 54a7f9acd2bbd44b56ec4694c7f7ebfe07daffd6 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:09:41 -0500 Subject: [PATCH 072/100] firmware: GeoRAM polls the USB mouse from its handler instead of every main loop pass Other handlers no longer pay for the poll. While GeoRAM is not running the library's report accumulates unread, so starting GeoRAM clears it rather than delivering it to the C64 as one jump. --- Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c | 8 +++++++- Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c | 3 ++- Source/Teensy/Teensy.ino | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index afaeeb31..98b745c1 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -8,6 +8,7 @@ void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn); void IO2Hndlr_GeoRAM(uint8_t Address, bool R_Wn); void InitHndlr_GeoRAM(); +void PollingHndlr_GeoRAM(); extern void (*fSpecialBtnChange)(bool Up_nDn); stcIOHandlers IOHndlr_GeoRAM = @@ -18,7 +19,7 @@ stcIOHandlers IOHndlr_GeoRAM = &IO2Hndlr_GeoRAM, //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 - NULL, //Polled in main routine + &PollingHndlr_GeoRAM, //Polled in main routine NULL, //called at the end of EVERY c64 cycle }; @@ -80,6 +81,11 @@ FLASHMEM void InitHndlr_GeoRAM() LoadRAMBanks(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); } +FLASHMEM void PollingHndlr_GeoRAM() +{ + PollUSBMouse(); +} + void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) { if (NumGeoRAM_Banks == 0) return; // disabled: $DE00 stays open bus, so software sees no geoRAM diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index 07095e6d..98d113c9 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -25,7 +25,7 @@ static int32_t USBMouse_Steps(uint8_t Axis) return USBMouse_Counts[Axis] >> USBMouse_StepShift; // floors, so no wider dead zone at 0 } -void PollUSBMouse() +FLASHMEM void PollUSBMouse() { // MouseController is also a BTHIDInput, so its own bool conversion is ambiguous if (!static_cast(usbHostMouse)) @@ -55,6 +55,7 @@ void PollUSBMouse() // Movement from before the handler started (at the TR menu) is not the C64's to see FLASHMEM void InitUSBMouse() { + usbHostMouse.mouseDataClear(); for (uint8_t Axis = 0; Axis < 2; Axis++) USBMouse_StepsRead[Axis] = USBMouse_Steps(Axis); } diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index ffe7b2df..0dd28899 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -370,7 +370,6 @@ void loop() if (Serial.available()) ServiceSerial(&Serial); myusbHost.Task(); - PollUSBMouse(); if (nfcState == nfcStateEnabled) nfcCheck(); From bb4e2e168de280e70b0f762c517e09d335123818 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:12:16 -0500 Subject: [PATCH 073/100] firmware: the USB mouse's X and Y reads drive a byte kept ready, and do the arithmetic after A read that reaches DataPortWriteWait late is latched wrong on some C64s, so the step and clamp now run after the byte is on the bus. The main loop's poll updates the ready byte with a compare-exchange that fails if a read landed meanwhile, so a step is never driven twice. The bytes a read sequence returns are unchanged. --- .../MinimalBoot/Common/IO_Handlers/USBMouse.c | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index 98d113c9..99494423 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -14,17 +14,41 @@ #define USBMouse_RegID 3 #define USBMouse_StepShift 2 // USB mice count ~1000 per inch; GEOS's screen is 320 wide #define USBMouse_MaxStep 127 +#define USBMouse_ReadyByte 0xff +#define USBMouse_ReadyNext 0x100 static const char USBMouse_ID[] = "TRM"; static volatile int32_t USBMouse_Counts[2]; // USB counts, written only by the main loop -static int32_t USBMouse_StepsRead[2]; // written only by the IO ISR, after InitUSBMouse +static volatile int32_t USBMouse_StepsRead[2]; // written only by the IO ISR, after InitUSBMouse static volatile uint8_t USBMouse_Buttons; +// The next read's byte, kept ready so a read drives the bus before any arithmetic. The bits +// above it count reads, so the main loop can tell that a read slipped in while it computed. +static volatile uint32_t USBMouse_Ready[2]; static int32_t USBMouse_Steps(uint8_t Axis) { return USBMouse_Counts[Axis] >> USBMouse_StepShift; // floors, so no wider dead zone at 0 } +static uint8_t USBMouse_NextStep(uint8_t Axis) +{ + int32_t Step = USBMouse_Steps(Axis) - USBMouse_StepsRead[Axis]; + if (Step > USBMouse_MaxStep) Step = USBMouse_MaxStep; + else if (Step < -USBMouse_MaxStep) Step = -USBMouse_MaxStep; + return (uint8_t)Step; +} + +static void USBMouse_UpdateReady() +{ + for (uint8_t Axis = 0; Axis < 2; Axis++) + { + uint32_t Ready = __atomic_load_n(&USBMouse_Ready[Axis], __ATOMIC_RELAXED); + uint32_t Next; + do Next = (Ready & ~USBMouse_ReadyByte) | USBMouse_NextStep(Axis); + while (!__atomic_compare_exchange_n(&USBMouse_Ready[Axis], &Ready, Next, true, __ATOMIC_RELAXED, __ATOMIC_RELAXED)); + } +} + FLASHMEM void PollUSBMouse() { // MouseController is also a BTHIDInput, so its own bool conversion is ambiguous @@ -50,24 +74,28 @@ FLASHMEM void PollUSBMouse() USBMouse_Counts[0] += X; USBMouse_Counts[1] += Y; USBMouse_Buttons = Buttons; + USBMouse_UpdateReady(); } // Movement from before the handler started (at the TR menu) is not the C64's to see FLASHMEM void InitUSBMouse() { usbHostMouse.mouseDataClear(); - for (uint8_t Axis = 0; Axis < 2; Axis++) USBMouse_StepsRead[Axis] = USBMouse_Steps(Axis); + for (uint8_t Axis = 0; Axis < 2; Axis++) + { + USBMouse_StepsRead[Axis] = USBMouse_Steps(Axis); + USBMouse_Ready[Axis] = 0; + } } void USBMouseIORead(uint8_t Address) { if (Address < 2) { - int32_t Step = USBMouse_Steps(Address) - USBMouse_StepsRead[Address]; - if (Step > USBMouse_MaxStep) Step = USBMouse_MaxStep; - else if (Step < -USBMouse_MaxStep) Step = -USBMouse_MaxStep; - USBMouse_StepsRead[Address] += Step; - DataPortWriteWaitLog((uint8_t)Step); + uint32_t Ready = USBMouse_Ready[Address]; + DataPortWriteWaitLog((uint8_t)Ready); + USBMouse_StepsRead[Address] += (int8_t)Ready; + USBMouse_Ready[Address] = (Ready & ~USBMouse_ReadyByte) + USBMouse_ReadyNext + USBMouse_NextStep(Address); } else if (Address == USBMouse_RegButtons) DataPortWriteWaitLog(USBMouse_Buttons); else if (Address < USBMouse_RegID + sizeof(USBMouse_ID) - 1) From 12d176d2b60ab753506afad8cc134e2129e963aa Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:17:49 -0500 Subject: [PATCH 074/100] firmware: GeoRAM drops its 256-byte no-RAM page GeoRAM_NoRAM was never read: the IO handlers return while there are no banks. Init now points the window at the first page before it publishes the bank count, so that stays true without it; the two are volatile so the stores keep that order. --- .../Common/IO_Handlers/IOH_GeoRAM.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index 787b86b3..19f60399 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -32,11 +32,12 @@ stcIOHandlers IOHndlr_GeoRAM = #define GeoRAM_RegsBase 0x80 #define GeoRAM_RegBlockBit 0x01 -static uint8_t NumGeoRAM_Banks = 0; +// volatile: the IO ISRs read the page only while there are banks, so the two stores +// must reach memory in the order written +static volatile uint8_t NumGeoRAM_Banks = 0; static uint8_t *GeoRAM_Bank[MAX_GEORAM_BANKS]; static uint8_t GeoRAM_Block, GeoRAM_PageNum; -static uint8_t GeoRAM_NoRAM[256]; // the window until a page of allocated RAM is selected -static uint8_t *GeoRAM_Page = GeoRAM_NoRAM; +static uint8_t * volatile GeoRAM_Page; static void GeoRAM_SelectPage() { @@ -53,7 +54,6 @@ FLASHMEM void FreeGeoRAM() // touching the banks before the first one is freed uint8_t NumBanks = NumGeoRAM_Banks; NumGeoRAM_Banks = 0; - GeoRAM_Page = GeoRAM_NoRAM; FreeRAMBanks(GeoRAM_Bank, &NumBanks); } @@ -67,16 +67,17 @@ FLASHMEM void InitHndlr_GeoRAM() { FreeGeoRAM(); GeoRAM_Block = GeoRAM_PageNum = 0; - NumGeoRAM_Banks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); - if (NumGeoRAM_Banks == 0) + uint8_t NumBanks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); + if (NumBanks == 0) { Serial.printf("GeoRAM is disabled\n"); return; } - GeoRAM_SelectPage(); + GeoRAM_Page = GeoRAM_Bank[0]; + NumGeoRAM_Banks = NumBanks; fSpecialBtnChange = &SpecialBtn_GeoRAM; - Serial.printf("GeoRAM Size: %dK bytes\n", NumGeoRAM_Banks * RAM_Bank_Size / 1024); - LoadRAMBanks(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); + Serial.printf("GeoRAM Size: %dK bytes\n", NumBanks * RAM_Bank_Size / 1024); + LoadRAMBanks(GeoRAM_Bank, NumBanks, GeoRAM_Filename); } void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) From ce7cf6c0edcc16dc137b773a9df9a2aa12e6786a Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:19:11 -0500 Subject: [PATCH 075/100] firmware: keep the GEOS disk patcher out of ITCM The patcher's helpers had no FLASHMEM, so the ones GCC kept out of line (GeosReadDirectory, GeosReadTrackBam, GeosTake and five more, 1,770 B) were placed in RAM1 code. They run only from the GEOS menu item, never from an interrupt, so flash is fast enough. The host harness gets an empty FLASHMEM, as TRMouse128.h does for PROGMEM. --- Source/Teensy/GEOS/GeosDiskPatch.h | 69 ++++++++++++++++-------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 181dff72..3220738e 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -6,6 +6,11 @@ #include #include #include +#ifdef ARDUINO +#include +#else +#define FLASHMEM +#endif #define GEOS_SECTOR_SIZE 256 #define GEOS_DIR_ENTRY_SIZE 32 @@ -78,13 +83,13 @@ static const struct GeosFormat GeosFormats[] = { "D81", 80, 40, 3, 39, { 40, 0 }, 819200, true, false }, }; -static bool GeosFail(struct GeosDisk *Disk, const char *Message) +FLASHMEM static bool GeosFail(struct GeosDisk *Disk, const char *Message) { snprintf(Disk->Error, sizeof Disk->Error, "%s", Message); return false; } -static uint8_t GeosSectorsOnTrack(const struct GeosFormat *Format, uint8_t Track) +FLASHMEM static uint8_t GeosSectorsOnTrack(const struct GeosFormat *Format, uint8_t Track) { uint8_t ZoneTrack = (Track - 1) % 35 + 1; @@ -92,7 +97,7 @@ static uint8_t GeosSectorsOnTrack(const struct GeosFormat *Format, uint8_t Track return ZoneTrack <= 17 ? 21 : ZoneTrack <= 24 ? 19 : ZoneTrack <= 30 ? 18 : 17; } -static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +FLASHMEM static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) { uint32_t Blocks = Sector; @@ -100,30 +105,30 @@ static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Track, uint8 return Blocks * GEOS_SECTOR_SIZE; } -static bool GeosReadTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, uint8_t *Block) +FLASHMEM static bool GeosReadTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, uint8_t *Block) { if (Disk->Read(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; return GeosFail(Disk, "the image could not be read"); } -static bool GeosWriteTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Block) +FLASHMEM static bool GeosWriteTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Block) { if (Disk->Write(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; return GeosFail(Disk, "the image could not be written"); } -static bool GeosIsReserved(const struct GeosFormat *Format, uint8_t Track) +FLASHMEM static bool GeosIsReserved(const struct GeosFormat *Format, uint8_t Track) { return Track == Format->Reserved[0] || Track == Format->Reserved[1]; } -static bool GeosIsDirSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +FLASHMEM static bool GeosIsDirSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) { return Track == Format->DirTrack && Sector >= Format->FirstDirSector && Sector <= Format->LastDirSector; } // Where a track's free count and free-sector bitmap live -static void GeosBamPlaces(const struct GeosFormat *Format, uint8_t Track, struct GeosPlace *Count, struct GeosPlace *Bitmap) +FLASHMEM static void GeosBamPlaces(const struct GeosFormat *Format, uint8_t Track, struct GeosPlace *Count, struct GeosPlace *Bitmap) { if (Format->FixedSectors) { @@ -143,12 +148,12 @@ static void GeosBamPlaces(const struct GeosFormat *Format, uint8_t Track, struct } } -static bool GeosSectorIsFree(const uint8_t *Bitmap, uint8_t Sector) +FLASHMEM static bool GeosSectorIsFree(const uint8_t *Bitmap, uint8_t Sector) { return Bitmap[Sector / 8] & (1 << (Sector % 8)); } -static uint8_t GeosCountFree(const struct GeosFormat *Format, uint8_t Track, const uint8_t *Bitmap, int *FirstFree) +FLASHMEM static uint8_t GeosCountFree(const struct GeosFormat *Format, uint8_t Track, const uint8_t *Bitmap, int *FirstFree) { uint8_t Free = 0; @@ -172,7 +177,7 @@ struct GeosTrackBam int FirstFree; }; -static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, struct GeosTrackBam *Bam) +FLASHMEM static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, struct GeosTrackBam *Bam) { GeosBamPlaces(Disk->Format, Track, &Bam->CountAt, &Bam->BitmapAt); Bam->Shared = Bam->CountAt.Track == Bam->BitmapAt.Track && Bam->CountAt.Sector == Bam->BitmapAt.Sector; @@ -185,7 +190,7 @@ static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, struct GeosTr return true; } -static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct GeosTrackBam *Bam) +FLASHMEM static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct GeosTrackBam *Bam) { if (*Bam->Count >= Bam->Free) return true; snprintf(Disk->Error, sizeof Disk->Error, "the BAM count for track %u disagrees with its bitmap", Track); @@ -193,7 +198,7 @@ static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct Ge } // Marks the first free sector of a track in use; *Sector is -1 when the track has none. -static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) +FLASHMEM static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) { struct GeosTrackBam Bam; @@ -209,7 +214,7 @@ static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) // Refuses, before anything is written, a disk that GeosAllocate would run out on partway. // Reads the tracks GeosAllocate would visit, in its order, so the refusals match trmouse.py's. -static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) +FLASHMEM static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) { struct GeosTrackBam Bam; unsigned Found = 0; @@ -224,7 +229,7 @@ static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) return GeosFail(Disk, "the disk is full"); } -static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) +FLASHMEM static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) { for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) { @@ -238,7 +243,7 @@ static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) return GeosFail(Disk, "the disk is full"); } -static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, uint8_t Sector) +FLASHMEM static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, uint8_t Sector) { for (int i = 0; i < Length; i++) { @@ -247,7 +252,7 @@ static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, return false; } -static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +FLASHMEM static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) { uint8_t Block[GEOS_SECTOR_SIZE]; uint8_t Track, Sector; @@ -275,7 +280,7 @@ static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Lengt } // Refuses, before anything is written, a directory that GeosGrowDirectory could not grow. -static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length) +FLASHMEM static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length) { const struct GeosFormat *Format = Disk->Format; struct GeosTrackBam Bam; @@ -293,7 +298,7 @@ static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *C } // Chains one more directory sector, as DOS does when a directory fills. -static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +FLASHMEM static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) { const struct GeosFormat *Format = Disk->Format; uint8_t Block[GEOS_SECTOR_SIZE]; @@ -314,18 +319,18 @@ static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * return GeosWriteTS(Disk, Format->DirTrack, New, Block); } -static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) +FLASHMEM static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) { return (Target->DriverSize + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK; } -static uint16_t GeosDriverBlocks(const struct GeosTarget *Target) +FLASHMEM static uint16_t GeosDriverBlocks(const struct GeosTarget *Target) { return GeosDriverDataBlocks(Target) + 1; } // Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. -static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) +FLASHMEM static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) { const struct GeosTarget *Target = Disk->Target; const uint16_t DataBlocks = GeosDriverDataBlocks(Target); @@ -354,7 +359,7 @@ static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct return GeosWriteTS(Disk, Info->Track, Info->Sector, Target->DriverInfo); } -static void GeosBuildEntry(const struct GeosTarget *Target, uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) +FLASHMEM static void GeosBuildEntry(const struct GeosTarget *Target, uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) { memset(Slot + SlotType, 0, GEOS_DIR_ENTRY_SIZE - SlotType); Slot[SlotType] = CBM_USR_CLOSED; @@ -375,7 +380,7 @@ static void GeosBuildEntry(const struct GeosTarget *Target, uint8_t *Slot, struc Slot[SlotBlocks + 1] = Blocks >> 8; } -static bool GeosSlotNamed(const uint8_t *Slot, const char *Name) +FLASHMEM static bool GeosSlotNamed(const uint8_t *Slot, const char *Name) { size_t Length = strlen(Name); @@ -396,7 +401,7 @@ struct GeosDirScan bool HasFree, HasInputDriver, HasDeskTop, HasOurs; }; -static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) +FLASHMEM static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) { const struct GeosTarget *Target = Disk->Target; uint8_t Block[GEOS_SECTOR_SIZE]; @@ -421,12 +426,12 @@ static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, return true; } -static bool GeosSlotBefore(struct GeosSlotRef a, struct GeosSlotRef b) +FLASHMEM static bool GeosSlotBefore(struct GeosSlotRef a, struct GeosSlotRef b) { return a.Sector < b.Sector || (a.Sector == b.Sector && a.Slot < b.Slot); } -static bool GeosSwapEntries(struct GeosDisk *Disk, const struct GeosTS *Chain, struct GeosSlotRef a, struct GeosSlotRef b) +FLASHMEM static bool GeosSwapEntries(struct GeosDisk *Disk, const struct GeosTS *Chain, struct GeosSlotRef a, struct GeosSlotRef b) { uint8_t BlockA[GEOS_SECTOR_SIZE], BlockB[GEOS_SECTOR_SIZE], Saved[GEOS_DIR_ENTRY_SIZE]; bool Shared = a.Sector == b.Sector; @@ -443,7 +448,7 @@ static bool GeosSwapEntries(struct GeosDisk *Disk, const struct GeosTS *Chain, s return GeosWriteTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA); } -static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, const struct GeosDate *Date) +FLASHMEM static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, const struct GeosDate *Date) { struct GeosTS First, Info; uint16_t Blocks; @@ -455,7 +460,7 @@ static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, return GeosWriteTS(Disk, Where.Track, Where.Sector, Block); } -static inline const struct GeosFormat *GeosFormatForSize(uint32_t Size) +FLASHMEM static inline const struct GeosFormat *GeosFormatForSize(uint32_t Size) { for (size_t i = 0; i < sizeof GeosFormats / sizeof GeosFormats[0]; i++) { @@ -464,7 +469,7 @@ static inline const struct GeosFormat *GeosFormatForSize(uint32_t Size) return NULL; } -static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length, struct GeosDirScan *Scan) +FLASHMEM static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length, struct GeosDirScan *Scan) { uint8_t Header[GEOS_SECTOR_SIZE]; @@ -485,7 +490,7 @@ static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int * } // Checks the image and says whether it is the target's boot disk and already holds the TR driver. -static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found) +FLASHMEM static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; struct GeosDirScan Scan; @@ -499,7 +504,7 @@ static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found // Writes the driver ahead of every other input driver of its type: GEOS 128 boots with the first one in the directory. // Every refusal but a failed block read or write comes before the first write, leaving the image as it was. -static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDate *Date) +FLASHMEM static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDate *Date) { struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; struct GeosDirScan Scan; From e984c6dc344c6ccbeb20bd228e27a7c5d8212f4d Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:25:30 -0500 Subject: [PATCH 076/100] firmware: start GEOS 64 at the right hour at noon and midnight A 6526 flips AM/PM when hour 12 is written to its TOD, so writing 12 PM left the clock at midnight and 12 AM at noon; GEOS then ran 12 hours off and turned the date over at noon. The hour byte is now the one that lands on the right value after the flip. The patch also keeps the stock clear of CRB's alarm-select bit, which it had dropped to make room: with that bit set, the time would go to the alarm. The time fits in the stock run by storing the seconds and tenths from X, which the stock file's preceding loop leaves zero; the matched bytes include that loop's end so another boot file is left alone. --- Source/Teensy/GEOS/GeosBoot.h | 27 ++++++---- Source/Teensy/tests/geos-boot.test.js | 72 ++++++++++++++++++++----- Source/Teensy/tests/geos_boot_harness.c | 13 +++-- 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 9d2b3231..0534c5ad 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -180,23 +180,34 @@ static const uint8_t GeosBootStub[] = }; // The kernal's start-up code sets the date with three immediate stores and the clock in CIA 1 with another run; -// both are found in the boot file by their bytes, since the stock file fixes the date and time they set +// both are found in the boot file by their bytes, since the stock file fixes the date and time they set. The +// clock's run follows a loop that leaves X zero, which the patched run uses for the seconds and tenths. static const uint8_t GeosStartDate[] = { 0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85 }; static const uint8_t GeosStartClock[] = { + 0xca, 0xd0, 0xe4, // dex : bne, the loop before it 0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, // clear the alarm-set bit 0xa9, 0x81, 0x8d, 0x0b, 0xdc, // hour 1 PM 0xa9, 0x00, 0x8d, 0x0a, 0xdc, // minute 0 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, // second and tenths 0; the tenths start the clock }; -#define GEOS_PM_BIT 0x80 -#define GEOS_NOP 0xea +#define GEOS_CLOCK_TIME 11 +#define GEOS_PM_BIT 0x80 static uint8_t GeosBcd(uint8_t Value) { return (Value / 10) << 4 | Value % 10; } +// The hour byte that leaves CIA 1's TOD holding Hour: a 6526 flips AM/PM when hour 12 is written to it +static uint8_t GeosTodHour(uint8_t Hour) +{ + uint8_t Hour12 = Hour % 12 ? Hour % 12 : 12; + bool Pm = (Hour >= 12) != (Hour12 == 12); + + return GeosBcd(Hour12) | (Pm ? GEOS_PM_BIT : 0); +} + // Finds Pattern once in Prg; NULL when it is absent or ambiguous static uint8_t *GeosFindOnce(uint8_t *Prg, uint32_t Length, const uint8_t *Pattern, uint32_t Size) { @@ -217,20 +228,18 @@ static bool GeosSetClock(uint8_t *Prg, uint32_t Length, const struct GeosDate *N { uint8_t *Date = GeosFindOnce(Prg, Length, GeosStartDate, sizeof GeosStartDate); uint8_t *Clock = GeosFindOnce(Prg, Length, GeosStartClock, sizeof GeosStartClock); - uint8_t Hour12 = Now->Hour % 12 ? Now->Hour % 12 : 12; uint8_t Time[] = { - 0xa9, (uint8_t)(GeosBcd(Hour12) | (Now->Hour >= 12 ? GEOS_PM_BIT : 0)), 0x8d, 0x0b, 0xdc, - 0xa2, GeosBcd(Now->Minute), 0x8e, 0x0a, 0xdc, - 0xa9, 0x00, 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc, + 0xa9, GeosTodHour(Now->Hour), 0x8d, 0x0b, 0xdc, + 0xa9, GeosBcd(Now->Minute), 0x8d, 0x0a, 0xdc, + 0x8e, 0x09, 0xdc, 0x8e, 0x08, 0xdc, }; if (!Date || !Clock) return false; Date[1] = Now->Day; Date[6] = Now->Month; Date[11] = Now->Year; - memset(Clock, GEOS_NOP, sizeof GeosStartClock); - memcpy(Clock + sizeof GeosStartClock - sizeof Time, Time, sizeof Time); + memcpy(Clock + GEOS_CLOCK_TIME, Time, sizeof Time); return true; } diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index 61bf6c6b..f4a96233 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -68,13 +68,48 @@ function stockDriver() { const START_AT_IN_BODY = 0x1000; const START_DATE = [0xa9, 0x17, 0x8d, 0x18, 0x85, 0xa9, 0x03, 0x8d, 0x17, 0x85, 0xa9, 0x5c, 0x8d, 0x16, 0x85]; +const LOOP_END = [0xca, 0xd0, 0xe4]; const START_CLOCK = [0xad, 0x0f, 0xdc, 0x29, 0x7f, 0x8d, 0x0f, 0xdc, 0xa9, 0x81, 0x8d, 0x0b, 0xdc, 0xa9, 0x00, 0x8d, 0x0a, 0xdc, 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc]; +const START_UP = [...START_DATE, ...LOOP_END, ...START_CLOCK]; +const CLOCK_AT = START_DATE.length + LOOP_END.length; -function bootFile(driver = stockDriver(), startUp = false) { +const CRB = 0xdc0f; +const ALARM_SELECT = 0x80; +const TOD_TENTHS = 0xdc08; +const TOD_HOURS = 0xdc0b; +const PM = 0x80; + +// Runs the start-up clock code the way a 6526 takes it: X is zero from the loop before it, writes go to the alarm +// while CRB selects it, and writing hour 12 flips AM/PM. Returns the TOD registers it sets, tenths first. +function runClock(code) { + let a = 0; + let x = 0; + let crb = ALARM_SELECT; + const tod = []; + const write = (address, value) => { + if (address === CRB) crb = value; + else if (crb & ALARM_SELECT) assert.fail(`$${address.toString(16)} written while CRB selects the alarm`); + else tod[address - TOD_TENTHS] = address === TOD_HOURS && (value & 0x1f) === 0x12 ? value ^ PM : value; + }; + for (let pc = 0; pc < code.length; ) { + const op = code[pc]; + const word = code[pc + 1] | (code[pc + 2] << 8); + if (op === 0xa9) (a = code[pc + 1]), (pc += 2); + else if (op === 0xa2) (x = code[pc + 1]), (pc += 2); + else if (op === 0x29) (a &= code[pc + 1]), (pc += 2); + else if (op === 0xad && word === CRB) (a = crb), (pc += 3); + else if (op === 0x8d) write(word, a), (pc += 3); + else if (op === 0x8e) write(word, x), (pc += 3); + else assert.fail(`unexpected opcode $${op.toString(16)}`); + } + return tod; +} + +function bootFile(driver = stockDriver(), startUp = []) { const body = Buffer.alloc(BODY_SIZE); for (let i = 0; i < BODY_SIZE; i++) body[i] = (i * 31 + 7) & 0xff; driver.copy(body, DRIVER_AT_IN_BODY); - if (startUp) Buffer.from([...START_DATE, ...START_CLOCK]).copy(body, START_AT_IN_BODY); + Buffer.from(startUp).copy(body, START_AT_IN_BODY); const file = Buffer.alloc(2 + BODY_SIZE); file.writeUInt16LE(LOAD_ADDRESS, 0); body.copy(file, 2); @@ -129,9 +164,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) => { + const launch = (disk, time = []) => { fs.writeFileSync(path.join(dir, 'in.d81'), disk.image); - const r = run([path.join(dir, 'harness'), path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81')]); + const r = run([path.join(dir, 'harness'), path.join(dir, 'in.d81'), path.join(dir, 'out.prg'), path.join(dir, 'out.d81'), ...time.map(String)]); return { ...r, prg: r.status === 0 ? fs.readFileSync(path.join(dir, 'out.prg')) : null, image: r.status === 0 ? fs.readFileSync(path.join(dir, 'out.d81')) : null }; }; body(launch); @@ -140,7 +175,7 @@ function withHarness(body) { } } -function standardDisk(driver, startUp = false) { +function standardDisk(driver, startUp = []) { const disk = new Disk(); disk.addFile('GEOS', Buffer.from([1, 8, 0])); disk.addFile('GEOBOOT', bootFile(driver, startUp).file); @@ -176,23 +211,36 @@ test('the boot PRG carries the boot file, with the TR disk driver in place of th test('the boot PRG starts GEOS with the given date and time', { skip }, () => { withHarness((launch) => { - const r = launch(standardDisk(undefined, true)); + const r = launch(standardDisk(undefined, START_UP)); const place = 2 + LOAD_ADDRESS - BASIC_START + START_AT_IN_BODY; - const at = r.prg.subarray(place, place + START_DATE.length + START_CLOCK.length); + const at = r.prg.subarray(place, place + START_UP.length); assert.match(r.out, /clock=1/); assert.deepEqual([at[1], at[6], at[11]], [21, 10, 26], 'day, month, year'); - assert.deepEqual([...at.subarray(15 + 6, 15 + 24)], [0xa9, 0x83, 0x8d, 0x0b, 0xdc, 0xa2, 0x07, 0x8e, 0x0a, 0xdc, 0xa9, 0x00, 0x8d, 0x09, 0xdc, 0x8d, 0x08, 0xdc], '3 PM and 7 minutes'); - assert.equal(at.subarray(15, 15 + 6).every((b) => b === 0xea), true, 'padding'); + assert.deepEqual([...at.subarray(START_DATE.length, CLOCK_AT)], LOOP_END); + assert.deepEqual(runClock(at.subarray(CLOCK_AT)), [0x00, 0x00, 0x07, PM | 0x03], '3:07 PM'); + }); +}); + +test('the TOD holds noon and midnight as 12 PM and 12 AM', { skip }, () => { + withHarness((launch) => { + const place = 2 + LOAD_ADDRESS - BASIC_START + START_AT_IN_BODY + CLOCK_AT; + for (const [hour, held] of [[12, PM | 0x12], [0, 0x12], [11, 0x11], [23, PM | 0x11]]) { + const r = launch(standardDisk(undefined, START_UP), [hour, 30]); + assert.equal(r.status, 0, r.err); + assert.deepEqual(runClock(r.prg.subarray(place, place + START_CLOCK.length)), [0x00, 0x00, 0x30, held], `${hour}:30`); + } }); }); test('a boot file without the known start-up code keeps the date and time it has', { skip }, () => { withHarness((launch) => { - const r = launch(standardDisk()); - assert.match(r.out, /clock=0/); const place = 2 + LOAD_ADDRESS - BASIC_START; - assert.equal(r.prg.subarray(place, place + DRIVER_AT_IN_BODY).equals(bootFile().body.subarray(0, DRIVER_AT_IN_BODY)), true); + for (const startUp of [[], [...START_DATE, ...START_CLOCK]]) { + const r = launch(standardDisk(undefined, startUp)); + assert.match(r.out, /clock=0/); + assert.equal(r.prg.subarray(place, place + DRIVER_AT_IN_BODY).equals(bootFile(undefined, startUp).body.subarray(0, DRIVER_AT_IN_BODY)), true); + } }); }); diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 63389e3f..24b75d57 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 IN.d81 OUT.prg OUT.d81: builds the GEOS 64 boot PRG from the disk, retypes its CONFIGURE -// files, sets the clock to 2026-10-21 15:07, and writes both. Prints "changed=<0|1> clock=<0|1>"; exits 1 with the refusal on stderr. +// geos_boot_harness IN.d81 OUT.prg OUT.d81 [HOUR MINUTE]: builds the GEOS 64 boot PRG 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>"; exits 1 with the refusal on stderr. #include #include "../GEOS/GeosBoot.h" @@ -25,14 +25,19 @@ static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) int main(int argc, char **argv) { - const struct GeosDate Now = { 26, 10, 21, 15, 7 }; + struct GeosDate Now = { 26, 10, 21, 15, 7 }; struct GeosDisk Disk = { 0 }; uint32_t Length; bool Changed; FILE *File; size_t Size; - if (argc != 4 || !(File = fopen(argv[1], "rb"))) return 2; + if ((argc != 4 && argc != 6) || !(File = fopen(argv[1], "rb"))) return 2; + if (argc == 6) + { + Now.Hour = atoi(argv[4]); + Now.Minute = atoi(argv[5]); + } Size = fread(Image, 1, sizeof Image, File); fclose(File); From 9d1c7b98468ace632b43b2b0ebd8a4396e841e14 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:28:16 -0500 Subject: [PATCH 077/100] firmware: GeoRAM saves back to georam.ram, keeping the last one as georam.bak The button saved to the REU's numbered names (georam000.ram, ...), but preload reads only georam.ram, so a saved GeoRAM never came back. The new image is written to georam.tmp first and renamed into place only after the write succeeds. The REU keeps its numbered saves. --- .../Common/IO_Handlers/IOH_GeoRAM.c | 2 +- .../MinimalBoot/Common/IO_Handlers/RAMBanks.c | 93 ++++++++++++++----- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index 19f60399..49997e10 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -60,7 +60,7 @@ FLASHMEM void FreeGeoRAM() FLASHMEM void SpecialBtn_GeoRAM(bool Up_nDn) { // on button release save GeoRAM contents - if(Up_nDn) SaveRAMBanks(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); + if(Up_nDn) SaveRAMBanksReplacing(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); } FLASHMEM void InitHndlr_GeoRAM() diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c index e15d37e2..3c4caa79 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -121,6 +121,45 @@ FLASHMEM void LoadRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source LoadFile.close(); } +FLASHMEM static bool WriteRAMBanksFile(FS *sourceFS, const char *FileName, uint8_t *Bank[], uint8_t NumBanks) +{ + Serial.printf("Saving: %s\n", FileName); + sourceFS->remove(FileName); // FILE_WRITE appends to an existing file + File SaveFile = sourceFS->open(FileName, FILE_WRITE); + if (!SaveFile) + { + Serial.println("Unable to open!"); + return false; + } + + for (uint8_t BankNum = 0; BankNum < NumBanks; BankNum++) + { + if (SaveFile.write(Bank[BankNum], RAM_Bank_Size) != RAM_Bank_Size) + { + Serial.printf("Write failed after %dKBytes!\n", BankNum * RAM_Bank_Size / 1024); + SaveFile.close(); + return false; + } + } + Serial.printf("Saved %dKBytes\n", NumBanks * RAM_Bank_Size / 1024); + SaveFile.close(); + return true; +} + +FLASHMEM static void FlashSavedLED() +{ + uint32_t LastmS = millis(); + bool LEDState = true; + + for (uint32_t FlashCount=0; FlashCount<20; FlashCount++) + { + while(millis()-LastmS < 75); + LastmS = millis(); + if ((LEDState = !LEDState)) SetLEDOn; + else SetLEDOff; + } +} + // Saves to the first free numbered name: reu.reu becomes reu000.reu, reu000.reu becomes reu001.reu, etc FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) { @@ -157,34 +196,38 @@ FLASHMEM void SaveRAMBanks(uint8_t *Bank[], uint8_t NumBanks, const char *Source do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); while (sourceFS->exists(NewFilename)); - Serial.printf("Saving: %s\n", NewFilename); - File SaveFile = sourceFS->open(NewFilename, FILE_WRITE); - if (!SaveFile) - { - Serial.println("Unable to open!"); - return; - } + if (WriteRAMBanksFile(sourceFS, NewFilename, Bank, NumBanks)) FlashSavedLED(); +} - for (uint8_t BankNum = 0; BankNum < NumBanks; BankNum++) - { - if (SaveFile.write(Bank[BankNum], RAM_Bank_Size) != RAM_Bank_Size) - { - Serial.printf("Write failed after %dKBytes!\n", BankNum * RAM_Bank_Size / 1024); - SaveFile.close(); - return; - } - } - Serial.printf("Saved %dKBytes\n", NumBanks * RAM_Bank_Size / 1024); - SaveFile.close(); +FLASHMEM static void WithExtension(char *Dest, const char *FileName, const char *Extension) +{ + strlcpy(Dest, FileName, MaxPathLength); + char *pDot = strrchr(Dest, '.'); + if (pDot != NULL) *pDot = 0; + strlcat(Dest, Extension, MaxPathLength); +} - uint32_t LastmS = millis(); - bool LEDState = true; +// Overwrites the file, keeping the one it replaces as .bak: georam.ram becomes georam.bak +FLASHMEM void SaveRAMBanksReplacing(uint8_t *Bank[], uint8_t NumBanks, const char *SourcePath) +{ + if (NumBanks == 0) return; // nothing to save: probably due to failure to allocate - for (uint32_t FlashCount=0; FlashCount<20; FlashCount++) + char Filename[MaxPathLength]; + strlcpy(Filename, SourcePath, MaxPathLength); + char *ptrFileName; + FS *sourceFS = RAMBanksFS(Filename, &ptrFileName); + + char TempFilename[MaxPathLength], BackupFilename[MaxPathLength]; + WithExtension(TempFilename, ptrFileName, ".tmp"); + WithExtension(BackupFilename, ptrFileName, ".bak"); + + if (!WriteRAMBanksFile(sourceFS, TempFilename, Bank, NumBanks)) return; + sourceFS->remove(BackupFilename); + sourceFS->rename(ptrFileName, BackupFilename); // fails harmlessly when there is no previous save + if (!sourceFS->rename(TempFilename, ptrFileName)) { - while(millis()-LastmS < 75); - LastmS = millis(); - if ((LEDState = !LEDState)) SetLEDOn; - else SetLEDOff; + Serial.printf("Unable to rename %s to %s!\n", TempFilename, ptrFileName); + return; } + FlashSavedLED(); } From 2c864364f5fc20d784b4033d8a818ca22df130c5 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:36:46 -0500 Subject: [PATCH 078/100] firmware: keep the GEOS patcher's messages in flash On Teensy 4 string literals land in .rodata, which the linker places in DTCM. PSTR() puts each one in flash instead, where snprintf and SendMsgPrintfln read it directly. The host harness gets PSTR(s) as (s). --- Source/Teensy/GEOS/GeosDiskPatch.h | 31 +++++++++++++++--------------- Source/Teensy/GeosLauncher.ino | 27 +++++++++++++------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index 3220738e..13375a03 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -10,6 +10,7 @@ #include #else #define FLASHMEM +#define PSTR(s) (s) #endif #define GEOS_SECTOR_SIZE 256 @@ -85,7 +86,7 @@ static const struct GeosFormat GeosFormats[] = FLASHMEM static bool GeosFail(struct GeosDisk *Disk, const char *Message) { - snprintf(Disk->Error, sizeof Disk->Error, "%s", Message); + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s"), Message); return false; } @@ -108,13 +109,13 @@ FLASHMEM static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Tra FLASHMEM static bool GeosReadTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, uint8_t *Block) { if (Disk->Read(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; - return GeosFail(Disk, "the image could not be read"); + return GeosFail(Disk, PSTR("the image could not be read")); } FLASHMEM static bool GeosWriteTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Block) { if (Disk->Write(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; - return GeosFail(Disk, "the image could not be written"); + return GeosFail(Disk, PSTR("the image could not be written")); } FLASHMEM static bool GeosIsReserved(const struct GeosFormat *Format, uint8_t Track) @@ -193,7 +194,7 @@ FLASHMEM static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, stru FLASHMEM static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct GeosTrackBam *Bam) { if (*Bam->Count >= Bam->Free) return true; - snprintf(Disk->Error, sizeof Disk->Error, "the BAM count for track %u disagrees with its bitmap", Track); + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM count for track %u disagrees with its bitmap"), Track); return false; } @@ -226,7 +227,7 @@ FLASHMEM static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) Found += Bam.Free; if (Found >= Needed) return true; } - return GeosFail(Disk, "the disk is full"); + return GeosFail(Disk, PSTR("the disk is full")); } FLASHMEM static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) @@ -240,7 +241,7 @@ FLASHMEM static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) *Where = (struct GeosTS){ Track, (uint8_t)Sector }; return true; } - return GeosFail(Disk, "the disk is full"); + return GeosFail(Disk, PSTR("the disk is full")); } FLASHMEM static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, uint8_t Sector) @@ -261,14 +262,14 @@ FLASHMEM static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, i if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Block)) return false; Track = Block[0]; Sector = Block[1]; - if (!Track) return GeosFail(Disk, "the disk header links to no directory"); + if (!Track) return GeosFail(Disk, PSTR("the disk header links to no directory")); while (Track) { if (GeosChainHas(Chain, *Length, Track, Sector) || !GeosIsDirSector(Disk->Format, Track, Sector) || *Length == GEOS_MAX_DIR_SECTORS) { - snprintf(Disk->Error, sizeof Disk->Error, "the directory chain is broken at %u/%u", Track, Sector); + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the directory chain is broken at %u/%u"), Track, Sector); return false; } Chain[(*Length)++] = (struct GeosTS){ Track, Sector }; @@ -289,12 +290,12 @@ FLASHMEM static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct for (int i = 0; i < Length; i++) { if (!GeosSectorIsFree(Bam.Bits, Chain[i].Sector)) continue; - snprintf(Disk->Error, sizeof Disk->Error, "the BAM shows directory sector %u/%u free", Chain[i].Track, Chain[i].Sector); + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM shows directory sector %u/%u free"), Chain[i].Track, Chain[i].Sector); return false; } if (!GeosCheckCount(Disk, Format->DirTrack, &Bam)) return false; if (Bam.FirstFree >= 0 && GeosIsDirSector(Format, Format->DirTrack, Bam.FirstFree)) return true; - return GeosFail(Disk, "the directory is full"); + return GeosFail(Disk, PSTR("the directory is full")); } // Chains one more directory sector, as DOS does when a directory fills. @@ -304,7 +305,7 @@ FLASHMEM static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Cha uint8_t Block[GEOS_SECTOR_SIZE]; int New; - if (*Length == GEOS_MAX_DIR_SECTORS) return GeosFail(Disk, "the directory is full"); + if (*Length == GEOS_MAX_DIR_SECTORS) return GeosFail(Disk, PSTR("the directory is full")); if (!GeosCheckDirectoryRoom(Disk, Chain, *Length) || !GeosTake(Disk, Format->DirTrack, &New)) return false; struct GeosTS Last = Chain[*Length - 1]; @@ -477,13 +478,13 @@ FLASHMEM static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Cha if (strlen(Target->DeskTopName) > GEOS_NAME_SIZE || strlen(Target->DriverName) > GEOS_NAME_SIZE || !Target->DriverSize) { - return GeosFail(Disk, "the GEOS target's names or driver do not fit a directory entry"); + return GeosFail(Disk, PSTR("the GEOS target's names or driver do not fit a directory entry")); } - if (!Disk->Format) return GeosFail(Disk, "not a D64, D71 or D81 without error bytes"); + if (!Disk->Format) return GeosFail(Disk, PSTR("not a D64, D71 or D81 without error bytes")); if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Header)) return false; if (Disk->Format->TwoSided && !(Header[3] & D71_DOUBLE_SIDED)) { - return GeosFail(Disk, "this D71 is formatted single-sided"); + return GeosFail(Disk, PSTR("this D71 is formatted single-sided")); } if (!GeosDirChain(Disk, Chain, Length)) return false; return GeosScanDirectory(Disk, Chain, *Length, Scan); @@ -513,7 +514,7 @@ FLASHMEM static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struc if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; if (Scan.HasOurs) { - snprintf(Disk->Error, sizeof Disk->Error, "%s is already on this disk", Disk->Target->DriverName); + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is already on this disk"), Disk->Target->DriverName); return false; } if (!Scan.HasFree && !GeosCheckDirectoryRoom(Disk, Chain, Length)) return false; diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 9ca7726e..f9de7ae1 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -55,13 +55,13 @@ FLASHMEM static bool GeosIsPatchedCopy(const char *Name) FLASHMEM static void GeosPath(char *Path, size_t Size, const struct GeosLauncher *Launcher, const char *Name) { - snprintf(Path, Size, "%s/%s", Launcher->Folder, Name); + snprintf(Path, Size, PSTR("%s/%s"), Launcher->Folder, Name); } FLASHMEM static void GeosCopyName(char *CopyName, size_t Size, const char *Name) { int Base = strlen(Name) - GEOS_EXT_LENGTH; - snprintf(CopyName, Size, "%.*s" GEOS_COPY_TAG "%s", Base, Name, Name + Base); + snprintf(CopyName, Size, PSTR("%.*s" GEOS_COPY_TAG "%s"), Base, Name, Name + Base); } // The originals in the folder, listed before anything is written to it @@ -91,12 +91,12 @@ FLASHMEM static bool GeosInspectFile(const struct GeosLauncher *Launcher, const if (!Image) { - if (Error) snprintf(Error, ErrorSize, "the image could not be opened"); + if (Error) snprintf(Error, ErrorSize, PSTR("the image could not be opened")); return false; } Disk = GeosDiskOn(Launcher, &Image); Inspected = GeosInspect(&Disk, Found); - if (!Inspected && Error) snprintf(Error, ErrorSize, "%s", Disk.Error); + if (!Inspected && Error) snprintf(Error, ErrorSize, PSTR("%s"), Disk.Error); Image.close(); return Inspected; } @@ -143,7 +143,7 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch if (!Image) return false; Disk = GeosDiskOn(Launcher, &Image); Patched = GeosInstallDriver(&Disk, &Today); - if (!Patched) snprintf(Error, ErrorSize, "%s", Disk.Error); + if (!Patched) snprintf(Error, ErrorSize, PSTR("%s"), Disk.Error); Image.close(); return Patched; } @@ -152,14 +152,15 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const char *Name, const char *Source, const char *Copy) { - char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE] = "the SD card could not be written"; + char Temp[MaxNamePathLength], Error[GEOS_ERROR_SIZE]; + strcpy(Error, PSTR("the SD card could not be written")); GeosPath(Temp, sizeof Temp, Launcher, GEOS_TEMP_NAME); if (!GeosCopyFile(Source, Temp) || !GeosPatchFile(Launcher, Temp, Error, sizeof Error) || (SD.exists(Copy) && !SD.remove(Copy)) || !SD.rename(Temp, Copy)) { SD.remove(Temp); - SendMsgPrintfln("Could not add the mouse driver to\r\na copy of %s:\r\n%s", Name, Error); + SendMsgPrintfln(PSTR("Could not add the mouse driver to\r\na copy of %s:\r\n%s"), Name, Error); return false; } return true; @@ -167,7 +168,7 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch FLASHMEM static bool GeosReady(const struct GeosLauncher *Launcher, const char *Name) { - SendMsgPrintfln("%s boot disk:\r\n%s", Launcher->Title, Name); + SendMsgPrintfln(PSTR("%s boot disk:\r\n%s"), Launcher->Title, Name); return true; } @@ -183,7 +184,7 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c GeosPath(Copy, sizeof Copy, Launcher, CopyName); if (!GeosInspectFile(Launcher, Source, &Found, Error, sizeof Error)) { - SendMsgPrintfln("%s:\r\n%s", Name, Error); + SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Error); return false; } if (!Found.HasDeskTop) return false; @@ -194,13 +195,13 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c // The copy is the disk GEOS saves the user's work to: one that cannot be read is left alone, not replaced if (!GeosInspectFile(Launcher, Copy, &Found, Error, sizeof Error)) { - SendMsgPrintfln("%s:\r\n%s", CopyName, Error); + SendMsgPrintfln(PSTR("%s:\r\n%s"), CopyName, Error); return true; } if (Found.HasDriver) return GeosReady(Launcher, CopyName); } if (!GeosPatchCopy(Launcher, Name, Source, Copy)) return true; - SendMsgPrintfln("Added the TR USB mouse driver to\r\na copy of %s", Name); + SendMsgPrintfln(PSTR("Added the TR USB mouse driver to\r\na copy of %s"), Name); return GeosReady(Launcher, CopyName); } @@ -212,12 +213,12 @@ FLASHMEM void LaunchGeos(const struct GeosLauncher *Launcher) if (!SDFullInit()) { - SendMsgPrintfln("%s starts from your boot disk\r\nimage on the SD card. Insert the card.", Launcher->Title); + SendMsgPrintfln(PSTR("%s starts from your boot disk\r\nimage on the SD card. Insert the card."), Launcher->Title); return; } Count = GeosListImages(Launcher, Names); for (uint8_t i = 0; i < Count; i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i]); if (BootDiskFound) return; - SendMsgPrintfln("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again.", + SendMsgPrintfln(PSTR("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again."), Launcher->Title, Launcher->Folder); } From dbc1ba8c833004e25de57847156968a28ddc11c0 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:33:07 -0500 Subject: [PATCH 079/100] firmware: the USB mouse register map says the ID marks the registers, not where they are --- Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c index 99494423..c3612801 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -8,7 +8,7 @@ // +0 X movement since the last read of +0, signed, right is positive // +1 Y movement since the last read of +1, signed, down is positive // +2 buttons, 1 = pressed: bit 0 left, bit 1 right, bit 2 middle -// +3..5 "TRM", for a driver to find the base it is at +// +3..5 "TRM", for a driver to tell that the registers are there #define USBMouse_RegButtons 2 #define USBMouse_RegID 3 From e7f06e25d263bd9294bdf3ab13d8d764d33dd846 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:50:47 -0500 Subject: [PATCH 080/100] firmware: regenerate the GEOS 128 mouse driver header for the driver's single $DF00 probe --- Source/Teensy/GEOS/TRMouse128.h | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Source/Teensy/GEOS/TRMouse128.h b/Source/Teensy/GEOS/TRMouse128.h index 1658658c..52c27ba1 100644 --- a/Source/Teensy/GEOS/TRMouse128.h +++ b/Source/Teensy/GEOS/TRMouse128.h @@ -9,29 +9,27 @@ #endif static const char TRMouse128Name[] PROGMEM = "TR USB MOUSE"; -static const uint8_t TRMouse128Code[226] PROGMEM = { - 0x4c, 0x10, 0xfd, 0x4c, 0x1a, 0xfd, 0x4c, 0x42, 0xfd, 0x4c, 0x1a, 0xfd, 0x80, 0x00, 0x00, 0x00, - 0xa9, 0x00, 0x85, 0x3b, 0xa9, 0x08, 0x85, 0x3a, 0x85, 0x3c, 0x60, 0xa0, 0xdf, 0x8c, 0x24, 0xfd, - 0xa2, 0x02, 0xbd, 0x03, 0xdf, 0xdd, 0x3f, 0xfd, 0xd0, 0x0e, 0xca, 0x10, 0xf5, 0x8c, 0x49, 0xfd, - 0x8c, 0x4f, 0xfd, 0x8c, 0x5a, 0xfd, 0x18, 0x60, 0x88, 0xc0, 0xdd, 0xd0, 0xe0, 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, 0xd9, 0xfd, 0x8d, 0x06, 0x85, 0x60, 0x03, 0x02, 0x01, 0x04, 0xff, 0x00, 0x05, - 0x06, 0x07 +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 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, 0xe2, 0xfd, 0x00, 0x00, 0x54, 0x52, 0x20, + 0x80, 0x04, 0xe7, 0x80, 0x83, 0x0f, 0x00, 0x00, 0xfd, 0xcf, 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, From 0d20dc11e59a5c0ca051cd8cfa62ac42aec76ef3 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:58:29 -0500 Subject: [PATCH 081/100] firmware: a TR block FLUSH the card fails reports $E4 instead of done SD.h's File::flush() is void and drops FsFile::sync()'s result, and getWriteError() reads a Print flag the SD library never sets, so the image is held as an FsFile opened on SD.sdfs and FLUSH returns sync()'s result. Closing at unmount reports a failed final write on the serial console instead of dropping it. The image opens O_RDWR: a missing path no longer creates an empty file. --- .../MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 15 +++++++-------- Source/Teensy/tests/trblock.test.js | 6 +++++- Source/Teensy/tests/trblock_harness.c | 8 +++++--- docs/TR_Block_Device.md | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 2d0cbabb..f027ad01 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -11,18 +11,19 @@ static struct TRBlock TRBlockDev; static struct TRBlockImage TRBlockImg; static struct TRBlockBackend TRBlockBack; -static File TRBlockFile; +// An FsFile, not an SD.h File: File::flush() discards whether the sync reached the card +static FsFile TRBlockFile; static bool TRBlockDirty; static uint32_t TRBlockLastWritemS; static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) { - return TRBlockFile.seek(Offset) && TRBlockFile.read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; + return TRBlockFile.seekSet(Offset) && TRBlockFile.read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; } static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) { - if (!TRBlockFile.seek(Offset) || TRBlockFile.write(Block, GEOS_SECTOR_SIZE) != GEOS_SECTOR_SIZE) return false; + if (!TRBlockFile.seekSet(Offset) || TRBlockFile.write(Block, GEOS_SECTOR_SIZE) != GEOS_SECTOR_SIZE) return false; TRBlockDirty = true; TRBlockLastWritemS = millis(); return true; @@ -30,15 +31,14 @@ static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Bloc static bool TRBlockFileFlush(void *Context) { - TRBlockFile.flush(); TRBlockDirty = false; - return true; + return TRBlockFile.sync(); } FLASHMEM void TRBlockUnmount() { TRBlockBack.Context = NULL; - if (TRBlockFile) TRBlockFile.close(); + if (TRBlockFile.isOpen() && !TRBlockFile.close()) Serial.printf("TR block image: last writes did not reach the card\n"); TRBlockDirty = false; } @@ -48,8 +48,7 @@ FLASHMEM bool TRBlockMount(const char *Path) uint64_t Size; TRBlockUnmount(); - TRBlockFile = SD.open(Path, FILE_WRITE_BEGIN); - if (!TRBlockFile) return false; + if (!TRBlockFile.open(&SD.sdfs, Path, O_RDWR)) return false; Size = TRBlockFile.size(); TRBlockImg = (struct TRBlockImage){ Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL, true, NULL, TRBlockFileRead, TRBlockFileWrite, TRBlockFileFlush }; diff --git a/Source/Teensy/tests/trblock.test.js b/Source/Teensy/tests/trblock.test.js index a375f7ae..e73e331b 100644 --- a/Source/Teensy/tests/trblock.test.js +++ b/Source/Teensy/tests/trblock.test.js @@ -22,7 +22,7 @@ const PAGES = SECTOR / WINDOW; const CMD = 0x08, ADDR0 = 0x09, ADDR1 = 0x0a, PAGE = 0x0b, TOKEN = 0x0c, STATUS = 0x0d, ECHO = 0x0e, ID = 0x10; const WINDOW_AT = 0x40; const READ = 1, WRITE = 2, FLUSH = 3, INFO = 4; -const BUSY = 0x80, DONE = 0x5a, NO_IMAGE = 0xe1, BAD_ADDRESS = 0xe2, READ_ONLY = 0xe5, BAD_COMMAND = 0xe6; +const BUSY = 0x80, DONE = 0x5a, NO_IMAGE = 0xe1, BAD_ADDRESS = 0xe2, WRITE_FAILED = 0xe4, READ_ONLY = 0xe5, BAD_COMMAND = 0xe6; const zoneSectors = (track) => { const zone = (track - 1) % 35 + 1; @@ -188,6 +188,10 @@ test('flush reaches the backend once per command and reports done', { skip }, () assert.deepEqual(lines, ['1', '5a', '1', '1', '2']); }); +test('a flush the card fails reports write failed, not done', { skip }, () => { + assert.equal(statusOf(command(FLUSH) + 'svc\n', 'd81', 'flushfails'), hex(WRITE_FAILED)); +}); + test('operands are latched when CMD is written: changing them while the command waits changes nothing', { skip }, () => { const start = command(READ, { addr0: 40, addr1: 3, token: 9 }); const { lines } = run('d81', start + write(ADDR0, 1) + write(ADDR1, 1) + write(TOKEN, 0x44) + 'svc\n' + read(ECHO) + read(WINDOW_AT)); diff --git a/Source/Teensy/tests/trblock_harness.c b/Source/Teensy/tests/trblock_harness.c index 9b988cab..b65c88c4 100644 --- a/Source/Teensy/tests/trblock_harness.c +++ b/Source/Teensy/tests/trblock_harness.c @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// trblock_harness IMAGE OUT [ro|nobackend]: the block device's register side and disk image backend, +// trblock_harness IMAGE OUT [ro|nobackend|flushfails]: the block device's register side and disk image backend, // driven from stdin one command a line, as the IO ISR and the main loop would drive them: // w OFFSET HEX an IO write r OFFSET an IO read, printing the byte or "open" // svc the main loop's service, printing 1 if a command ran @@ -14,6 +14,7 @@ static uint8_t Bytes[MAX_IMAGE]; static uint32_t Size, Flushes; +static bool FlushFails; static bool ReadAt(void *Context, uint32_t Offset, uint8_t *Block) { @@ -35,7 +36,7 @@ static bool FlushAll(void *Context) { (void)Context; Flushes++; - return true; + return !FlushFails; } int main(int argc, char **argv) @@ -48,12 +49,13 @@ int main(int argc, char **argv) if (!In) { - fprintf(stderr, "usage: trblock_harness IMAGE OUT [ro|nobackend]\n"); + fprintf(stderr, "usage: trblock_harness IMAGE OUT [ro|nobackend|flushfails]\n"); return 2; } Size = fread(Bytes, 1, MAX_IMAGE, In); fclose(In); Image.Format = GeosFormatForSize(Size); + FlushFails = argc >= 4 && !strcmp(argv[3], "flushfails"); Backend = TRBlockImageBackend(&Image); TRBlockReset(&Dev); const struct TRBlockBackend *Use = argc >= 4 && !strcmp(argv[3], "nobackend") ? NULL : &Backend; diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index ed3dc822..e72fb226 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -108,7 +108,7 @@ For the disk image kinds (D64, D71, D81) ADDR0 is the track and ADDR1 the sector The firmware does this arithmetic so that a 6502 driver does not have to. A driver that wants raw sector numbers instead is a different image kind, which this version does not define. ## Writes reach the card later -`WRITE` returns `$5A` when the block is accepted, not when the card holds it. The firmware writes the block to the file at once and flushes the file when no command has arrived for a quarter of a second, or on `FLUSH`. A program that must know a write survived power loss sends `FLUSH` and waits for `$5A`. GEOS does not, so a disk saved and then powered off within that quarter second can be torn. +`WRITE` returns `$5A` when the block is accepted, not when the card holds it. The firmware writes the block to the file at once and flushes the file a quarter of a second after the last `WRITE`, or on `FLUSH`. A `FLUSH` the card fails returns `$E4`. A program that must know a write survived power loss sends `FLUSH` and waits for `$5A`. GEOS does not, so a disk saved and then powered off within that quarter second can be torn. ## What the firmware does not promise * **Speed.** A command takes as long as the card takes. Poll; do not time it. From fbfaad431b87269c804d8628d05e1343fdb8d027 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:59:22 -0500 Subject: [PATCH 082/100] firmware: every SD.begin closes the TR block image around it and reopens it SD.begin rebuilds the SdFat volume in place: the image's dirty cached sector is dropped rather than written, and the open FsFile is left pointing into the rebuilt volume. Host file commands (GetStorageDevice), a GeoRAM save from the special button (SDFullInit and FSfromSourceID) and a remote launch can all begin the card while GEOS has the image mounted. Patching each caller would leave the next one to forget, so the main image's SD.begin calls all go through SDBegin(), which syncs and closes the image first and reopens it from its saved path after, keeping a running GEOS session's disk. --- Source/Teensy/DriveDirLoad.ino | 4 +- Source/Teensy/FileTransfer.ino | 2 +- .../Common/IO_Handlers/IOH_TR_BASIC.c | 2 +- .../Common/IO_Handlers/Swift_Browser.c | 2 +- .../Common/IO_Handlers/TRBlockIO.c | 50 +++++++++++++++---- Source/Teensy/Teensy.ino | 2 +- Source/Teensy/nfcScan.ino | 2 +- 7 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index 350ff1b6..48accd62 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -109,7 +109,7 @@ FLASHMEM void ShowVmPackages() //Begun here, with one retry while the slot still reads filled: the retry //SDFullInit makes, because a card's first start can fail. Nothing is remembered //between draws, so a card that will not start costs both attempts on each one. - Card = SD.begin(BUILTIN_SDCARD) || (SDSlotFilled() && SD.begin(BUILTIN_SDCARD)); + Card = SDBegin() || (SDSlotFilled() && SDBegin()); //A card started here in place of one that had been begun: whatever the menu holds //from that one is stale, its listing and the extension table built from it. The //flags remote file commands raise for the same reason make the menu's next @@ -443,7 +443,7 @@ void MenuChange() SetNumItems(sizeof(TeensyROMMenu)/sizeof(TeensyROMMenu[0])); break; case rmtSD: - SD.begin(BUILTIN_SDCARD); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + SDBegin(); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated LoadDirectory(&SD); //do this regardless of SD.begin result to populate one entry w/ message MenuSource = DriveDirMenu; break; diff --git a/Source/Teensy/FileTransfer.ino b/Source/Teensy/FileTransfer.ino index 2f2bb03a..96195bf7 100644 --- a/Source/Teensy/FileTransfer.ino +++ b/Source/Teensy/FileTransfer.ino @@ -37,7 +37,7 @@ FLASHMEM FS* GetStorageDevice(uint32_t storageType) { if (!storageType) return &firstPartition; - if (!SD.begin(BUILTIN_SDCARD)) + if (!SDBegin()) { SendU16(FailToken); CmdChannel->printf("Specified storage device was not found: %u\n", storageType); diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c index 0a29c5bf..77db2d01 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c @@ -125,7 +125,7 @@ FLASHMEM FS *FSfromFileName(char** ptrptrLSFileName) ptrLSFileName += 3; sourceFS = &SD; Printf_dbg("SD:*\n"); - if(!SD.begin(BUILTIN_SDCARD)) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + if(!SDBegin()) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated { Printf_dbg("SD Init Fail\n"); return NULL; //return BAS_ERROR_DEVICE_NOT_PRESENT; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c index 32ab2531..a2f74937 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c @@ -645,7 +645,7 @@ FLASHMEM void DoSearch(const char *Term) FLASHMEM bool InitCheckSD() { - if (!SD.begin(BUILTIN_SDCARD)) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + if (!SDBegin()) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated { SendASCIIErrorStrImmediate("No SD card"); return false; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index f027ad01..95fe0dee 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -15,6 +15,7 @@ static struct TRBlockBackend TRBlockBack; static FsFile TRBlockFile; static bool TRBlockDirty; static uint32_t TRBlockLastWritemS; +static char *TRBlockPath; static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) { @@ -35,20 +36,11 @@ static bool TRBlockFileFlush(void *Context) return TRBlockFile.sync(); } -FLASHMEM void TRBlockUnmount() -{ - TRBlockBack.Context = NULL; - if (TRBlockFile.isOpen() && !TRBlockFile.close()) Serial.printf("TR block image: last writes did not reach the card\n"); - TRBlockDirty = false; -} - -// The image stays mounted across handler restarts, so a launcher can mount it and then start the handler -FLASHMEM bool TRBlockMount(const char *Path) +static FLASHMEM bool TRBlockOpen() { uint64_t Size; - TRBlockUnmount(); - if (!TRBlockFile.open(&SD.sdfs, Path, O_RDWR)) return false; + if (!TRBlockFile.open(&SD.sdfs, TRBlockPath, O_RDWR)) return false; Size = TRBlockFile.size(); TRBlockImg = (struct TRBlockImage){ Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL, true, NULL, TRBlockFileRead, TRBlockFileWrite, TRBlockFileFlush }; @@ -61,6 +53,42 @@ FLASHMEM bool TRBlockMount(const char *Path) return true; } +static FLASHMEM void TRBlockClose() +{ + TRBlockBack.Context = NULL; + if (TRBlockFile.isOpen() && !TRBlockFile.close()) Serial.printf("TR block image: last writes did not reach the card\n"); + TRBlockDirty = false; +} + +FLASHMEM void TRBlockUnmount() +{ + TRBlockClose(); + free(TRBlockPath); + TRBlockPath = NULL; +} + +// The image stays mounted across handler restarts, so a launcher can mount it and then start the handler +FLASHMEM bool TRBlockMount(const char *Path) +{ + TRBlockUnmount(); + TRBlockPath = strdup(Path); + if (TRBlockPath && TRBlockOpen()) return true; + TRBlockUnmount(); + return false; +} + +// Every SD.begin in the main image goes through here. SD.begin rebuilds the volume in place under +// any open file and drops its cached, unwritten sectors, so the image is closed around it. +FLASHMEM bool SDBegin() +{ + bool Began; + + TRBlockClose(); + Began = SD.begin(BUILTIN_SDCARD); + if (TRBlockPath) TRBlockOpen(); + return Began; +} + FLASHMEM void InitTRBlock() { TRBlockReset(&TRBlockDev); diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 78c1e121..d75cb207 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -559,7 +559,7 @@ bool SDFullInit() Printf_dbg("[=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=]\n"); Printf_dbg("Start mediaPresent %d\n", SD.mediaPresent()); //This indicates zero regardless of actual prior to begin() - while (!SD.begin(BUILTIN_SDCARD)) + while (!SDBegin()) { Count--; Printf_dbg("SD Init fail, %d tries left. Time: %lu mS\n", Count, millis()-Startms); diff --git a/Source/Teensy/nfcScan.ino b/Source/Teensy/nfcScan.ino index edca2934..b852473d 100644 --- a/Source/Teensy/nfcScan.ino +++ b/Source/Teensy/nfcScan.ino @@ -173,7 +173,7 @@ FS *FSfromSourceID(RegMenuTypes SourceID) { if(SourceID == rmtSD) { - SD.begin(BUILTIN_SDCARD); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + SDBegin(); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated return &SD; } else if(SourceID == rmtUSBDrive) return &firstPartition; From b0d365eb6f78d0f01956c2ae27e1e46047999103 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:59:41 -0500 Subject: [PATCH 083/100] firmware: GeoRAM polls the TR block device, and unmounts its image when it ends The block device only answers under GeoRAM, so its polling moves from every main loop pass to GeoRAM's polling handler, beside the USB mouse. Nothing would then flush or close the image after GeoRAM ends, so leaving it (SetUpMainMenuROM, RemoteLaunch) unmounts the image beside FreeGeoRAM: close() syncs it. RemoteLaunch's SDFullInit before that point is safe since SDBegin closes and reopens the image. --- Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c | 1 + Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 2 +- Source/Teensy/RemoteControl.ino | 1 + Source/Teensy/Teensy.ino | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c index b4588d5c..2fa7bb93 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -86,6 +86,7 @@ FLASHMEM void InitHndlr_GeoRAM() FLASHMEM void PollingHndlr_GeoRAM() { PollUSBMouse(); + PollTRBlock(); } void IO1Hndlr_GeoRAM(uint8_t Address, bool R_Wn) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 95fe0dee..8a722b7a 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -2,7 +2,7 @@ // The TR block device (docs/TR_Block_Device.md) on the IO page a RAM expansion handler leaves free, beside // the USB mouse's registers. The ISR side only fills registers and the window; the SD card is touched -// only from PollTRBlock, in the main loop. +// only from the main loop: PollTRBlock from GeoRAM's polling handler, and SDBegin. #include "../../../BlockDevice/TRBlockImage.h" diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index cc3dfd46..5b18bd38 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -232,6 +232,7 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa FreeREU(); #endif FreeGeoRAM(); + TRBlockUnmount(); FreeSwiftlinkBuffs(); InitDriveDirMenu(); diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index d75cb207..3365d627 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -371,7 +371,6 @@ void loop() if (Serial.available()) ServiceSerial(&Serial); myusbHost.Task(); - PollTRBlock(); if (nfcState == nfcStateEnabled) nfcCheck(); @@ -424,6 +423,7 @@ FLASHMEM void SetUpMainMenuROM() FreeREU(); #endif FreeGeoRAM(); + TRBlockUnmount(); FreeSwiftlinkBuffs(); RedirectEmptyDriveDirMenu(); free((void*)MIDIRxBuf); MIDIRxBuf=NULL; From f3fe5f165a244c0a73533ea68a1e4087b50e9fab Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:03:29 -0500 Subject: [PATCH 084/100] firmware: keep the TR block device's main-loop side out of ITCM Only TRPageIO and the inline register helpers in TRBlock.h serve the IO ISR. PollTRBlock and the file and image backends run from the main loop at SD card speed, so they move to flash; TRBlockImage.h gets FLASHMEM's host fallback through GeosDiskPatch.h. --- Source/Teensy/BlockDevice/TRBlockImage.h | 6 +++--- .../MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Teensy/BlockDevice/TRBlockImage.h b/Source/Teensy/BlockDevice/TRBlockImage.h index bac42a00..b9c788d7 100644 --- a/Source/Teensy/BlockDevice/TRBlockImage.h +++ b/Source/Teensy/BlockDevice/TRBlockImage.h @@ -22,7 +22,7 @@ static inline bool TRBlockImageHasSector(const struct GeosFormat *Format, uint8_ return Track >= 1 && Track <= Format->Tracks && Sector < GeosSectorsOnTrack(Format, Track); } -static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, uint8_t Sector, uint8_t *Block) +FLASHMEM static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, uint8_t Sector, uint8_t *Block) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; uint32_t Offset; @@ -35,7 +35,7 @@ static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, ui return Image->Write(Image->Context, Offset, Block) ? TRBlockDone : TRBlockWriteFailed; } -static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) +FLASHMEM static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; uint32_t Blocks; @@ -51,7 +51,7 @@ static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) return TRBlockDone; } -static uint8_t TRBlockImageFlush(void *Context) +FLASHMEM static uint8_t TRBlockImageFlush(void *Context) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 8a722b7a..16689ae5 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -17,12 +17,12 @@ static bool TRBlockDirty; static uint32_t TRBlockLastWritemS; static char *TRBlockPath; -static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) +FLASHMEM static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) { return TRBlockFile.seekSet(Offset) && TRBlockFile.read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; } -static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) +FLASHMEM static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) { if (!TRBlockFile.seekSet(Offset) || TRBlockFile.write(Block, GEOS_SECTOR_SIZE) != GEOS_SECTOR_SIZE) return false; TRBlockDirty = true; @@ -30,13 +30,13 @@ static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Bloc return true; } -static bool TRBlockFileFlush(void *Context) +FLASHMEM static bool TRBlockFileFlush(void *Context) { TRBlockDirty = false; return TRBlockFile.sync(); } -static FLASHMEM bool TRBlockOpen() +FLASHMEM static bool TRBlockOpen() { uint64_t Size; @@ -53,7 +53,7 @@ static FLASHMEM bool TRBlockOpen() return true; } -static FLASHMEM void TRBlockClose() +FLASHMEM static void TRBlockClose() { TRBlockBack.Context = NULL; if (TRBlockFile.isOpen() && !TRBlockFile.close()) Serial.printf("TR block image: last writes did not reach the card\n"); @@ -94,7 +94,7 @@ FLASHMEM void InitTRBlock() TRBlockReset(&TRBlockDev); } -void PollTRBlock() +FLASHMEM void PollTRBlock() { TRBlockService(&TRBlockDev, &TRBlockBack); if (TRBlockDirty && millis() - TRBlockLastWritemS > TRBlock_FlushIdlemS) TRBlockFileFlush(NULL); From 745cdf71dd8e0063142a488f6db117846e152d5a Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:03:59 -0500 Subject: [PATCH 085/100] docs: the TR block device is GeoRAM's only, at $DF00 The REU no longer serves IO1, so the REU row, the $DE00 probe and the checklist's either-base item go. --- .../MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 4 ++-- docs/TR_Block_Device.md | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 16689ae5..cd5ffbf9 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// The TR block device (docs/TR_Block_Device.md) on the IO page a RAM expansion handler leaves free, beside -// the USB mouse's registers. The ISR side only fills registers and the window; the SD card is touched +// 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 SDBegin. #include "../../../BlockDevice/TRBlockImage.h" diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index e72fb226..7a605447 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -19,18 +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 a RAM expansion handler is active (GeoRAM or REU, which is also where the USB mouse registers are). It is not available under other handlers, and not under a Host Extension. +* 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 not an IEC drive. Software that talks to the IEC bus cannot see it. ## Where the registers are -The USB mouse registers already occupy the start of the free IO page left by the RAM expansion handler, and identify it: +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`. -| Handler | Free page | Base (`B`) | -|---------|-----------|-----------:| -| GeoRAM | IO2 | `$DF00` | -| REU | IO1 | `$DE00` | - -Find the base the way the mouse driver does: look for `TRM` at `$DF03..$DF05`, then at `$DE03..$DE05`. The block device is present when `TRB` is also 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. +Find it the way the mouse driver does: look for `TRM` at `$DF03..$DF05`. The block device is present when `TRB` is also 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. Everything below is an offset from `B`. @@ -118,9 +113,8 @@ 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: -* finds `B` from `TRM`, then confirms `TRB`; +* checks `TRM` at `$DF03`, then confirms `TRB`; * 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; -* sends `FLUSH` where a write must survive power loss; -* works whichever of `$DF00` and `$DE00` the base is. +* sends `FLUSH` where a write must survive power loss. From 0f600891b87ae15fa5166bc7ec4e34379132b696 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:25:59 -0500 Subject: [PATCH 086/100] firmware: keep the GEOS 64 launcher's messages in flash PSTR on the launcher's and boot builder's own messages, as the patcher's already are, so their literals go to .progmem instead of DTCM .data. --- Source/Teensy/GEOS/GeosBoot.h | 14 +++++++------- Source/Teensy/GeosLauncher.ino | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 0534c5ad..edf33e73 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -121,16 +121,16 @@ static bool GeosReadFile(struct GeosDisk *Disk, const char *Name, uint8_t *Out, break; } } - if (!At.Track) return GeosFail(Disk, "the boot file is not on the disk"); + 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, "the boot file's chain is broken"); + 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, "the boot file is too large"); + 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] }; @@ -252,15 +252,15 @@ static bool GeosBuildBootPrg(struct GeosDisk *Disk, uint8_t *Out, uint32_t Capac int32_t Driver; uint8_t *Body = Out + 2; - if (!Disk->Target->BootFile) return GeosFail(Disk, "this version has no boot file to start from"); + if (!Disk->Target->BootFile) return GeosFail(Disk, PSTR("this version has no boot file to start from")); if (!GeosReadFile(Disk, Disk->Target->BootFile, Out, Capacity, &FileSize)) return false; - if (FileSize < 2 + GEOS_DRIVER_SIZE) return GeosFail(Disk, "the boot file is too short"); + if (FileSize < 2 + GEOS_DRIVER_SIZE) return GeosFail(Disk, PSTR("the boot file is too short")); BodySize = FileSize - 2; Load = GeosWord(Out); Driver = GeosFindDriver(Body, BodySize); - if (Driver < 0 || !GeosReplaceTransport(Body + Driver)) return GeosFail(Disk, "the boot file's disk driver is not one the TR can replace"); + if (Driver < 0 || !GeosReplaceTransport(Body + Driver)) return GeosFail(Disk, PSTR("the boot file's disk driver is not one the TR can replace")); Place = 2 + Load - GEOS_BASIC_START; - if (Load < GEOS_BASIC_START + Prologue - 2 || Place + BodySize > Capacity || Load + BodySize > GEOS_MEMORY_END) return GeosFail(Disk, "the boot file does not fit in memory"); + if (Load < GEOS_BASIC_START + Prologue - 2 || Place + BodySize > Capacity || Load + BodySize > GEOS_MEMORY_END) return GeosFail(Disk, PSTR("the boot file does not fit in memory")); memmove(Out + Place, Body, BodySize); memset(Out, 0, Place); diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index fd3b10c3..15b92b6f 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -144,7 +144,7 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch if (!Image) { - snprintf(Error, ErrorSize, "the image could not be opened"); + snprintf(Error, ErrorSize, PSTR("the image could not be opened")); return false; } Disk = GeosDiskOn(Launcher, &Image); @@ -235,7 +235,7 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c if (!GeosRestoreCopy(Copy)) { - SendMsgPrintfln("%s\r\nwas set aside and could not be put back", CopyName); + SendMsgPrintfln(PSTR("%s\r\nwas set aside and could not be put back"), CopyName); return true; } if (SD.exists(Copy)) @@ -249,7 +249,7 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c if (Found.HasDriver) return GeosUse(Launcher, CopyName, BootName); if (!Found.HasDeskTop) { - SendMsgPrintfln("%s\r\nhas no desktop; remove it to start over", CopyName); + SendMsgPrintfln(PSTR("%s\r\nhas no desktop; remove it to start over"), CopyName); return true; } if (!GeosPatchCopy(Launcher, CopyName, Copy, Copy, true)) return true; @@ -273,18 +273,18 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * Image = SD.open(Path, FILE_WRITE_BEGIN); if (!Image) { - SendMsgPrintfln("%s\r\ncould not be opened", Name); + SendMsgPrintfln(PSTR("%s\r\ncould not be opened"), Name); return false; } Disk = GeosDiskOn(Launcher, &Image); Built = GeosBuildBootPrg(&Disk, RAM_Image, RAM_ImageSize, &Length) && GeosDisableConfigure(&Disk, &Changed); if (Built) GeosSetClock(RAM_Image, Length, &Now); - if (!Built) SendMsgPrintfln("%s:\r\n%s", Name, Disk.Error); + if (!Built) SendMsgPrintfln(PSTR("%s:\r\n%s"), Name, Disk.Error); Image.close(); if (!Built) return false; if (!TRBlockMount(Path)) { - SendMsgPrintfln("%s\r\ncould not be mounted", Name); + SendMsgPrintfln(PSTR("%s\r\ncould not be mounted"), Name); return false; } Item->ItemType = rtFilePrg; From 9d413205c2e131d8bf9d1f68231311f6f5af06a4 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:36:06 -0500 Subject: [PATCH 087/100] firmware: GeoRAM rotates georam.ram to .bak only when there is one, and puts it back if the save cannot take its place If the last rename failed, georam.ram was gone: the next session preloaded nothing, and saving that blank RAM removed georam.bak, the last good image. --- Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c index 3c4caa79..e7d75094 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -222,11 +222,16 @@ FLASHMEM void SaveRAMBanksReplacing(uint8_t *Bank[], uint8_t NumBanks, const cha WithExtension(BackupFilename, ptrFileName, ".bak"); if (!WriteRAMBanksFile(sourceFS, TempFilename, Bank, NumBanks)) return; - sourceFS->remove(BackupFilename); - sourceFS->rename(ptrFileName, BackupFilename); // fails harmlessly when there is no previous save + bool HadSave = sourceFS->exists(ptrFileName); + if (HadSave) + { // rename refuses an existing target + sourceFS->remove(BackupFilename); + sourceFS->rename(ptrFileName, BackupFilename); + } if (!sourceFS->rename(TempFilename, ptrFileName)) { Serial.printf("Unable to rename %s to %s!\n", TempFilename, ptrFileName); + if (HadSave) sourceFS->rename(BackupFilename, ptrFileName); return; } FlashSavedLED(); From 76c13e52cd688b46616a69e91725ae80ae8579b8 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:36:07 -0500 Subject: [PATCH 088/100] firmware: a remote launch finds its TR menu target before freeing the running handler's memory A launch is accepted while a handler is busy. One naming a TR menu path that does not exist returned after the CRT chips, REU, GeoRAM and SwiftLink buffers were freed, without resetting to the menu, leaving the running program on freed memory. --- Source/Teensy/RemoteControl.ino | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index cc3dfd46..9ae54f1f 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -226,22 +226,15 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa } //DriveDirPath's buffer can't hold both "/" and the filename starting at index 1 } - //free mem for DriveDirMenu in case current (non-tr) handler is using it all - FreeCrtChips(); -#ifdef Fab04_REU - FreeREU(); -#endif - FreeGeoRAM(); - FreeSwiftlinkBuffs(); - InitDriveDirMenu(); + int16_t MenuNum = -1; + StructMenuItem* DefTRMenu = TeensyROMMenu; //default to root menu + uint16_t NumMenuItems = sizeof(TeensyROMMenu)/sizeof(StructMenuItem); + // A launch can arrive while a handler is running the C64: one that finds no + // target must return before anything that handler uses is freed. if (MenuSourceID == rmtTeensy) { //point MenuSource to directory - int16_t MenuNum = -1; - StructMenuItem* DefTRMenu = TeensyROMMenu; //default to root menu - uint16_t NumMenuItems = sizeof(TeensyROMMenu)/sizeof(StructMenuItem); - if(strcmp(DriveDirPath, "/") !=0 ) {//find dir menu MenuNum = FindTRMenuItem(DefTRMenu, NumMenuItems, DriveDirPath); @@ -265,7 +258,19 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa return; } Printf_dbg("TR File Num = %d\n", MenuNum); + } + //free mem for DriveDirMenu in case current (non-tr) handler is using it all + FreeCrtChips(); +#ifdef Fab04_REU + FreeREU(); +#endif + FreeGeoRAM(); + FreeSwiftlinkBuffs(); + InitDriveDirMenu(); + + if (MenuSourceID == rmtTeensy) + { //point to item # matching filename MenuSource = DefTRMenu; SetNumItems(NumMenuItems); From 9f0c8f8be3f5c5b0081773bdce37720f524acf2e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:41:49 -0500 Subject: [PATCH 089/100] tests: drive the USB mouse registers on the host A C++ harness includes USBMouse.c with the USB controller, the NVIC and the data port stubbed, and runs it from a script of USB reports, polls, inits and IO reads. The tests hold the register map: a large movement arrives as 127s and then the remainder, X and Y apart, sub-step counts carry, init discards movement from before the handler, and the buttons follow the report and clear on unplug. An IO read can be landed at the poll's atomic write of the ready word, and a weak compare-exchange can be made to fail, to show neither repeats nor drops movement. The source is C++ (static_cast on the controller), so the harness is built as gnu++17 rather than C99. The Cortex-M barrier in PollUSBMouse does not assemble on the host, so the harness turns asm volatile(...) into asm(""). --- Source/Teensy/tests/usb-mouse.test.js | 92 +++++++++++++++ Source/Teensy/tests/usb_mouse_harness.cpp | 132 ++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 Source/Teensy/tests/usb-mouse.test.js create mode 100644 Source/Teensy/tests/usb_mouse_harness.cpp diff --git a/Source/Teensy/tests/usb-mouse.test.js b/Source/Teensy/tests/usb-mouse.test.js new file mode 100644 index 00000000..c84f17bc --- /dev/null +++ b/Source/Teensy/tests/usb-mouse.test.js @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// The USB mouse registers GeoRAM serves at $DF00, held to the register map in USBMouse.c. +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, 'usb_mouse_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 X = 0, Y = 1, BUTTONS = 2; +const COUNTS_PER_STEP = 4; +const MAX_STEP = 127; + +let harnessPath; +function harness() { + if (harnessPath) return harnessPath; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'usb-mouse-')); + harnessPath = path.join(dir, 'harness'); + const built = spawnSync('c++', ['-std=gnu++17', '-Wall', '-Wextra', '-Werror', '-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 report = (dx, dy, buttons = 0) => [`report ${dx} ${dy} ${buttons.toString(16)}`, 'poll']; +const read = (offset) => `r ${offset}`; +const signed = (byte) => (parseInt(byte, 16) << 24) >> 24; + +test('the id is TRM at +3, and nothing answers after it', { skip }, () => { + assert.deepEqual(run([3, 4, 5, 6, 7].map(read)), ['54', '52', '4d', 'open', 'open']); +}); + +test('a large movement arrives over several reads, 127 at most each, then the remainder', { skip }, () => { + const steps = 300; + const got = run([...report(steps * COUNTS_PER_STEP, 0), ...Array(5).fill(read(X))]).map(signed); + assert.deepEqual(got, [MAX_STEP, MAX_STEP, steps - 2 * MAX_STEP, 0, 0]); +}); + +test('X and Y keep their own movement, whatever order they are read in', { skip }, () => { + const got = run([...report(200 * COUNTS_PER_STEP, -150 * COUNTS_PER_STEP), ...[X, Y, Y, X, Y, X, X].map(read)]).map(signed); + assert.deepEqual(got, [127, -127, -23, 73, 0, 0, 0]); +}); + +test('counts below a step carry over to the next report, rounding toward negative', { skip }, () => { + assert.deepEqual(run([...report(3, -1), read(X), read(Y), ...report(1, 0), read(X), read(Y)]).map(signed), [0, -1, 1, 0]); +}); + +// A driver reads +0 and +1 from its interrupt, so any read can land while PollUSBMouse +// is part way through updating the byte the next read will drive. +for (const axis of [X, Y]) { + test(`axis ${axis}: a read landing during the poll's update neither repeats nor loses movement`, { skip }, () => { + const unread = 100, more = 20; + const move = (steps) => report(axis === X ? steps * COUNTS_PER_STEP : 0, axis === Y ? steps * COUNTS_PER_STEP : 0); + const lines = run([...move(unread), `race ${axis}`, ...move(more), ...Array(5).fill(read(axis))]); + assert.equal(lines[0], `race ${unread.toString(16)}`); + assert.deepEqual(lines.slice(1).map(signed), [more, 0, 0, 0, 0]); + }); +} + +test('a weak compare-exchange failing for no reason still leaves the new movement ready', { skip }, () => { + assert.deepEqual(run(['spurious 0', ...report(20 * COUNTS_PER_STEP, 0), read(X)]).map(signed), [20]); +}); + +test('the handler starting discards movement from before it, both polled and still in the controller', { skip }, () => { + const polled = report(40 * COUNTS_PER_STEP, 40 * COUNTS_PER_STEP); + const pending = `report ${30 * COUNTS_PER_STEP} ${30 * COUNTS_PER_STEP} 0`; + const got = run([...polled, pending, 'init', 'poll', read(X), read(Y), ...report(COUNTS_PER_STEP, -COUNTS_PER_STEP), read(X), read(Y)]).map(signed); + assert.deepEqual(got, [0, 0, 1, -1]); +}); + +test('the buttons register follows the latest report, and clears when the mouse is unplugged', { skip }, () => { + const got = run([...report(0, 0, 0b101), read(BUTTONS), ...report(0, 0, 0b010), read(BUTTONS), 'unplug', 'poll', read(BUTTONS)]); + assert.deepEqual(got, ['05', '02', '00']); +}); + +test('a report the controller held when the mouse was unplugged is not delivered on replug', { skip }, () => { + const got = run(['unplug', `report ${10 * COUNTS_PER_STEP} 0 1`, 'poll', 'plug', 'poll', read(X), read(BUTTONS)]); + assert.deepEqual(got, ['00', '00']); +}); diff --git a/Source/Teensy/tests/usb_mouse_harness.cpp b/Source/Teensy/tests/usb_mouse_harness.cpp new file mode 100644 index 00000000..16e00c4f --- /dev/null +++ b/Source/Teensy/tests/usb_mouse_harness.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +// usb_mouse_harness: the USB mouse registers, driven from stdin one command a line, +// as the USB host, the main loop and the IO ISR would drive them: +// report X Y B the USB host delivers a report into the mouse controller +// unplug, plug the mouse leaves or returns +// poll the main loop's PollUSBMouse +// init InitUSBMouse, as the handler starts +// r OFFSET an IO read, printing the byte driven or "open" +// race AXIS at the next poll's atomic write of AXIS's ready word, an IO read of +// that axis lands first and prints "race BYTE" +// spurious AXIS the next compare-exchange on AXIS's ready word fails without a reason, +// as a weak one may +#include +#include +#include + +#define FLASHMEM +#define NVIC_DISABLE_IRQ(Irq) +#define NVIC_ENABLE_IRQ(Irq) +#define IRQ_USBHS 0 +// Cortex-M barriers do not assemble for the host: asm volatile(...) becomes asm("") +#define volatile(...) ("") + +struct USBHIDInput +{ + bool Plugged = true; + explicit operator bool() { return Plugged; } +}; + +static struct MouseController : USBHIDInput +{ + int X = 0, Y = 0; + uint8_t Buttons = 0; + bool Available = false; + bool available() { return Available; } + int getMouseX() { return X; } + int getMouseY() { return Y; } + uint8_t getButtons() { return Buttons; } + void mouseDataClear() { X = Y = 0; Buttons = 0; Available = false; } +} usbHostMouse; + +static int Driven; +static void DataPortWriteWaitLog(uint8_t Data) { Driven = Data; } + +static int RaceAxis = -1, SpuriousAxis = -1; +static void LandRace(volatile uint32_t *Word); +static bool FailSpuriously(volatile uint32_t *Word, uint32_t *Expected); + +template +static bool RacedCompareExchange(volatile uint32_t *Word, uint32_t *Expected, Rest... rest) +{ + LandRace(Word); + if (FailSpuriously(Word, Expected)) return false; + return __atomic_compare_exchange_n(Word, Expected, rest...); +} + +template +static void RacedStore(volatile uint32_t *Word, Rest... rest) +{ + LandRace(Word); + __atomic_store_n(Word, rest...); +} + +#define __atomic_compare_exchange_n RacedCompareExchange +#define __atomic_store_n RacedStore +#include "../MinimalBoot/Common/IO_Handlers/USBMouse.c" +#undef __atomic_compare_exchange_n +#undef __atomic_store_n +#undef volatile + +static int Read(unsigned Offset) +{ + Driven = -1; + USBMouseIORead(Offset); + return Driven; +} + +static void LandRace(volatile uint32_t *Word) +{ + if (RaceAxis < 0 || Word != &USBMouse_Ready[RaceAxis]) return; + unsigned Axis = RaceAxis; + RaceAxis = -1; + printf("race %02x\n", Read(Axis)); +} + +static bool FailSpuriously(volatile uint32_t *Word, uint32_t *Expected) +{ + if (SpuriousAxis < 0 || Word != &USBMouse_Ready[SpuriousAxis]) return false; + SpuriousAxis = -1; + *Expected = *Word; + return true; +} + +int main() +{ + char Line[64]; + while (fgets(Line, sizeof Line, stdin)) + { + int X, Y; + unsigned Value; + if (sscanf(Line, "report %d %d %x", &X, &Y, &Value) == 3) + { + usbHostMouse.X = X; + usbHostMouse.Y = Y; + usbHostMouse.Buttons = Value; + usbHostMouse.Available = true; + } + else if (!strncmp(Line, "unplug", 6)) usbHostMouse.Plugged = false; + else if (!strncmp(Line, "plug", 4)) usbHostMouse.Plugged = true; + else if (!strncmp(Line, "poll", 4)) PollUSBMouse(); + else if (!strncmp(Line, "init", 4)) InitUSBMouse(); + else if (sscanf(Line, "race %u", &Value) == 1) RaceAxis = Value; + else if (sscanf(Line, "spurious %u", &Value) == 1) SpuriousAxis = Value; + else if (sscanf(Line, "r %x", &Value) == 1) + { + int Byte = Read(Value); + if (Byte < 0) printf("open\n"); + else printf("%02x\n", Byte); + } + else + { + fprintf(stderr, "unknown command: %s", Line); + return 2; + } + } + if (RaceAxis >= 0 || SpuriousAxis >= 0) + { + fprintf(stderr, "an armed race or spurious failure never happened\n"); + return 1; + } + return 0; +} From e2664224b004f2a3f733e4712dc527db3e50a78a Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:44:04 -0500 Subject: [PATCH 090/100] tools: the GEOS disk driver looks for the TR block device at $DF00 only The REU no longer serves IO1, so the block device lives only beside GeoRAM. Probing $DE as a fallback read GeoRAM's page RAM under GeoRAM and open bus under the REU. With the base fixed, the driver no longer patches its own register addresses. The checklist now says what the driver checks: TRB, not TRM. --- Source/Teensy/GEOS/TRDriver.h | 60 ++++++++++++++++------------------- docs/TR_Block_Device.md | 4 +-- tools/geos/trdrive.asm | 50 +++++------------------------ 3 files changed, 38 insertions(+), 76 deletions(-) diff --git a/Source/Teensy/GEOS/TRDriver.h b/Source/Teensy/GEOS/TRDriver.h index 07a38bb9..ce4e66b3 100644 --- a/Source/Teensy/GEOS/TRDriver.h +++ b/Source/Teensy/GEOS/TRDriver.h @@ -9,49 +9,45 @@ #endif #define TR_DRIVER_BASE 0x9000 -static const uint8_t TRDriverCode[555] PROGMEM = { - 0x08, 0x68, 0x8d, 0x20, 0x92, 0x78, 0xa5, 0x01, 0x8d, 0x21, 0x92, 0xa9, 0x36, 0x85, 0x01, 0xad, - 0x1a, 0xd0, 0x8d, 0x2a, 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, 0x2a, 0x92, - 0x8d, 0x1a, 0xd0, 0xad, 0x21, 0x92, 0x85, 0x01, 0xad, 0x20, 0x92, 0x48, 0x28, 0x60, 0xa2, 0x00, +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, 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, 0x19, - 0x92, 0xa2, 0x40, 0x20, 0x0a, 0x92, 0xa0, 0x00, 0x91, 0x0a, 0xa2, 0x41, 0x20, 0x0a, 0x92, 0xc8, + 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, 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, 0x25, - 0x92, 0xad, 0x25, 0x92, 0xa2, 0x0b, 0x20, 0x19, 0x92, 0xa2, 0x00, 0xbd, 0x40, 0xdf, 0xdd, 0x40, - 0xdf, 0xd0, 0xf8, 0x91, 0x0a, 0xc8, 0xe8, 0xe0, 0x40, 0xd0, 0xf0, 0xee, 0x25, 0x92, 0xad, 0x25, - 0x92, 0xc9, 0x04, 0xd0, 0xdc, 0xa2, 0x00, 0x60, 0x20, 0xbb, 0x91, 0xb0, 0xba, 0xa0, 0x00, 0xa9, - 0x00, 0x8d, 0x25, 0x92, 0xad, 0x25, 0x92, 0xa2, 0x0b, 0x20, 0x19, 0x92, 0xa2, 0x00, 0xb1, 0x0a, + 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, 0x9d, 0x40, 0xdf, 0xdd, 0x40, 0xdf, 0xf0, 0x05, 0xdd, 0x40, 0xdf, 0xd0, 0x15, 0xc8, 0xe8, 0xe0, - 0x40, 0xd0, 0xeb, 0xee, 0x25, 0x92, 0xad, 0x25, 0x92, 0xc9, 0x04, 0xd0, 0xd7, 0xa9, 0x02, 0x4c, - 0x15, 0x91, 0xa2, 0x0d, 0x60, 0x8d, 0x23, 0x92, 0xee, 0x26, 0x92, 0xd0, 0x03, 0xee, 0x26, 0x92, - 0xad, 0x26, 0x92, 0xa2, 0x0c, 0x20, 0x19, 0x92, 0xa5, 0x04, 0xa2, 0x09, 0x20, 0x19, 0x92, 0xa5, - 0x05, 0xa2, 0x0a, 0x20, 0x19, 0x92, 0xad, 0x23, 0x92, 0xa2, 0x08, 0x20, 0x19, 0x92, 0xa9, 0x00, - 0x8d, 0x28, 0x92, 0x8d, 0x29, 0x92, 0xa2, 0x0d, 0x20, 0x06, 0x92, 0x8d, 0x24, 0x92, 0x20, 0x06, - 0x92, 0xcd, 0x24, 0x92, 0xd0, 0x04, 0xc9, 0x80, 0xd0, 0x0d, 0xee, 0x28, 0x92, 0xd0, 0xe7, 0xee, - 0x29, 0x92, 0xd0, 0xe2, 0xa2, 0x0d, 0x60, 0x48, 0xa2, 0x0e, 0x20, 0x06, 0x92, 0xcd, 0x26, 0x92, + 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, 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, 0x22, 0x92, - 0x98, 0x18, 0x69, 0x8c, 0xa8, 0xb1, 0x0a, 0x48, 0xad, 0x22, 0x92, 0x91, 0x0a, 0x98, 0x38, 0xe9, - 0x8c, 0xa8, 0x68, 0x91, 0x0a, 0xc8, 0xc0, 0x1d, 0xd0, 0xe1, 0x60, 0xad, 0x27, 0x92, 0xd0, 0x26, - 0xa2, 0xdf, 0x20, 0xea, 0x91, 0x90, 0x07, 0xa2, 0xde, 0x20, 0xea, 0x91, 0xb0, 0x1a, 0x8e, 0x27, - 0x92, 0x8e, 0x08, 0x92, 0x8e, 0x1b, 0x92, 0x8e, 0xbd, 0x90, 0x8e, 0xc0, 0x90, 0x8e, 0xf2, 0x90, - 0x8e, 0xf5, 0x90, 0x8e, 0xfa, 0x90, 0x18, 0x60, 0x38, 0x60, 0x8e, 0xf4, 0x91, 0x8e, 0xf7, 0x91, - 0xa0, 0x02, 0xb9, 0x10, 0xdf, 0xd9, 0x10, 0xdf, 0xd0, 0xf8, 0xd9, 0x1d, 0x92, 0xd0, 0x05, 0x88, - 0x10, 0xf0, 0x18, 0x60, 0x38, 0x60, 0xbd, 0x00, 0xdf, 0x60, 0x20, 0x06, 0x92, 0x8d, 0x24, 0x92, - 0x20, 0x06, 0x92, 0xcd, 0x24, 0x92, 0xd0, 0xf2, 0x60, 0x9d, 0x00, 0xdf, 0x60, 0x54, 0x52, 0x42, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + 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 }; -static const uint16_t TRDriverRelocations[70] PROGMEM = { +static const uint16_t TRDriverRelocations[57] 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, 0x01bc, 0x01c3, 0x01ca, 0x01cf, 0x01d2, 0x01d5, 0x01d8, 0x01db, 0x01de, 0x01e1, 0x01e4, 0x01eb, - 0x01ee, 0x01fb, 0x020b, 0x020e, 0x0211, 0x0214 + 0x016e, 0x0174, 0x019e, 0x01a9, 0x01c6, 0x01d6, 0x01d9, 0x01dc, 0x01df }; static const uint16_t TRDriverEntries[13] PROGMEM = { 0x0000, 0x0026, 0x0041, 0x0041, 0x003e, 0x0047, 0x0042, 0x004f, 0x005c, 0x0042, 0x0042, 0x0042, 0x006c diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index 7a605447..5c5b3481 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -25,7 +25,7 @@ This document is the whole contract. The firmware side is [`TRBlock.h`](../Sourc ## 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`. -Find it the way the mouse driver does: look for `TRM` at `$DF03..$DF05`. The block device is present when `TRB` is also 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. Everything below is an offset from `B`. @@ -113,7 +113,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 `TRM` at `$DF03`, then confirms `TRB`; +* checks `TRB` at `$DF10`; * 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; diff --git a/tools/geos/trdrive.asm b/tools/geos/trdrive.asm index ab2e4bcb..b08cc7d2 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 the base the TRM and TRB ids reveal +; offsets from BASE_GEOS_RAM's page REG_CMD = $08 REG_ADDR0 = $09 REG_ADDR1 = $0a @@ -45,7 +45,6 @@ CIA2_ICR = $dd0d MASK_ALL = $7f BASE_GEOS_RAM = $df -BASE_REU = $de * = ORG @@ -123,7 +122,7 @@ WriteBlock rts ReadLink - jsr findBase + jsr findDevice bcs missing lda #CMD_READ jsr runCommand @@ -150,7 +149,7 @@ missing rts readSector - jsr findBase + jsr findDevice bcs missing lda #CMD_READ jsr runCommand @@ -166,7 +165,6 @@ readPage ldx #0 windowRead lda BASE_GEOS_RAM * 256 + WINDOW,x -windowReread cmp BASE_GEOS_RAM * 256 + WINDOW,x bne windowRead sta (r4),y @@ -183,7 +181,7 @@ readDone rts writeSector - jsr findBase + jsr findDevice bcs missing ldy #0 lda #0 @@ -195,12 +193,9 @@ writePage ldx #0 windowWrite lda (r4),y -windowStore sta BASE_GEOS_RAM * 256 + WINDOW,x -windowCheck cmp BASE_GEOS_RAM * 256 + WINDOW,x beq windowStored -windowRecheck cmp BASE_GEOS_RAM * 256 + WINDOW,x bne windowFault windowStored @@ -313,48 +308,20 @@ swapField swapped rts -; carry clear when the TRB id answers at a base: first the page GeoRAM leaves free, then the REU's -findBase - lda baseHi - bne located - ldx #BASE_GEOS_RAM - jsr tryBase - bcc settle - ldx #BASE_REU - jsr tryBase - bcs absent -settle - stx baseHi - stx getReg + 2 - stx putReg + 2 - stx windowRead + 2 - stx windowReread + 2 - stx windowStore + 2 - stx windowCheck + 2 - stx windowRecheck + 2 -located - clc - rts -absent - sec - rts - -tryBase - stx probe + 2 - stx probeAgain + 2 +; carry clear when the TRB id answers +findDevice ldy #2 probe lda BASE_GEOS_RAM * 256 + $10,y -probeAgain cmp BASE_GEOS_RAM * 256 + $10,y bne probe cmp trbId,y - bne notHere + bne absent dey bpl probe clc rts -notHere +absent sec rts @@ -383,6 +350,5 @@ command !byte 0 seen !byte 0 page !byte 0 token !byte 0 -baseHi !byte 0 patience !word 0 savedIrqEnable !byte 0 From e7d08660adc8ff375a66465b7c82cafd4a604162 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:44:14 -0500 Subject: [PATCH 091/100] fix: review findings for 9f0c8f8 --- Source/Teensy/tests/usb-mouse.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Teensy/tests/usb-mouse.test.js b/Source/Teensy/tests/usb-mouse.test.js index c84f17bc..cd791b71 100644 --- a/Source/Teensy/tests/usb-mouse.test.js +++ b/Source/Teensy/tests/usb-mouse.test.js @@ -68,6 +68,14 @@ for (const axis of [X, Y]) { assert.equal(lines[0], `race ${unread.toString(16)}`); assert.deepEqual(lines.slice(1).map(signed), [more, 0, 0, 0, 0]); }); + + test(`axis ${axis}: a read landing during the poll's update that leaves the same byte ready does not repeat movement`, { skip }, () => { + const steps = 10; + const move = (n) => report(axis === X ? n * COUNTS_PER_STEP : 0, axis === Y ? n * COUNTS_PER_STEP : 0); + const lines = run([...move(steps), `race ${axis}`, ...move(steps), ...Array(3).fill(read(axis))]); + assert.equal(lines[0], `race ${steps.toString(16).padStart(2, '0')}`); + assert.deepEqual(lines.slice(1).map(signed), [steps, 0, 0]); + }); } test('a weak compare-exchange failing for no reason still leaves the new movement ready', { skip }, () => { From f43c8b4044bb372c8a45229090115754a914d208 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:49:48 -0500 Subject: [PATCH 092/100] firmware: SDFullInit alone closes the TR block image around SD.begin The image is mounted only while GeoRAM is the handler, and under GeoRAM the card is begun only through SDFullInit (the special button's save, a host or NFC launch) and FSfromSourceID (the same save, an NFC tap). FSfromSourceID now begins through SDFullInit, which only adds its retry for a card that is present but fails to start. Every other SD.begin goes back to calling SD.begin. --- Source/Teensy/DriveDirLoad.ino | 4 ++-- Source/Teensy/FileTransfer.ino | 2 +- .../Common/IO_Handlers/IOH_TR_BASIC.c | 2 +- .../Common/IO_Handlers/Swift_Browser.c | 2 +- .../Common/IO_Handlers/TRBlockIO.c | 23 ++++++++----------- Source/Teensy/Teensy.ino | 14 +++++++++-- Source/Teensy/nfcScan.ino | 2 +- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index 48accd62..350ff1b6 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -109,7 +109,7 @@ FLASHMEM void ShowVmPackages() //Begun here, with one retry while the slot still reads filled: the retry //SDFullInit makes, because a card's first start can fail. Nothing is remembered //between draws, so a card that will not start costs both attempts on each one. - Card = SDBegin() || (SDSlotFilled() && SDBegin()); + Card = SD.begin(BUILTIN_SDCARD) || (SDSlotFilled() && SD.begin(BUILTIN_SDCARD)); //A card started here in place of one that had been begun: whatever the menu holds //from that one is stale, its listing and the extension table built from it. The //flags remote file commands raise for the same reason make the menu's next @@ -443,7 +443,7 @@ void MenuChange() SetNumItems(sizeof(TeensyROMMenu)/sizeof(TeensyROMMenu[0])); break; case rmtSD: - SDBegin(); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + SD.begin(BUILTIN_SDCARD); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated LoadDirectory(&SD); //do this regardless of SD.begin result to populate one entry w/ message MenuSource = DriveDirMenu; break; diff --git a/Source/Teensy/FileTransfer.ino b/Source/Teensy/FileTransfer.ino index 96195bf7..2f2bb03a 100644 --- a/Source/Teensy/FileTransfer.ino +++ b/Source/Teensy/FileTransfer.ino @@ -37,7 +37,7 @@ FLASHMEM FS* GetStorageDevice(uint32_t storageType) { if (!storageType) return &firstPartition; - if (!SDBegin()) + if (!SD.begin(BUILTIN_SDCARD)) { SendU16(FailToken); CmdChannel->printf("Specified storage device was not found: %u\n", storageType); diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c index 77db2d01..0a29c5bf 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TR_BASIC.c @@ -125,7 +125,7 @@ FLASHMEM FS *FSfromFileName(char** ptrptrLSFileName) ptrLSFileName += 3; sourceFS = &SD; Printf_dbg("SD:*\n"); - if(!SDBegin()) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + if(!SD.begin(BUILTIN_SDCARD)) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated { Printf_dbg("SD Init Fail\n"); return NULL; //return BAS_ERROR_DEVICE_NOT_PRESENT; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c index a2f74937..32ab2531 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_Browser.c @@ -645,7 +645,7 @@ FLASHMEM void DoSearch(const char *Term) FLASHMEM bool InitCheckSD() { - if (!SDBegin()) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + if (!SD.begin(BUILTIN_SDCARD)) // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated { SendASCIIErrorStrImmediate("No SD card"); return false; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index cd5ffbf9..93357046 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -2,7 +2,7 @@ // 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 SDBegin. +// only from the main loop: PollTRBlock from GeoRAM's polling handler, and SDFullInit. #include "../../../BlockDevice/TRBlockImage.h" @@ -53,13 +53,20 @@ FLASHMEM static bool TRBlockOpen() return true; } -FLASHMEM static void TRBlockClose() +// Close and reopen around an SD.begin that can run while GeoRAM has an image mounted, as SDFullInit does. +// SD.begin rebuilds the volume in place under any open file and drops its cached, unwritten sectors. +FLASHMEM void TRBlockClose() { TRBlockBack.Context = NULL; if (TRBlockFile.isOpen() && !TRBlockFile.close()) Serial.printf("TR block image: last writes did not reach the card\n"); TRBlockDirty = false; } +FLASHMEM void TRBlockReopen() +{ + if (TRBlockPath) TRBlockOpen(); +} + FLASHMEM void TRBlockUnmount() { TRBlockClose(); @@ -77,18 +84,6 @@ FLASHMEM bool TRBlockMount(const char *Path) return false; } -// Every SD.begin in the main image goes through here. SD.begin rebuilds the volume in place under -// any open file and drops its cached, unwritten sectors, so the image is closed around it. -FLASHMEM bool SDBegin() -{ - bool Began; - - TRBlockClose(); - Began = SD.begin(BUILTIN_SDCARD); - if (TRBlockPath) TRBlockOpen(); - return Began; -} - FLASHMEM void InitTRBlock() { TRBlockReset(&TRBlockDev); diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 3365d627..7b0e4ff1 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -548,7 +548,7 @@ void SetNumItems(uint16_t NumItems) (NumItems==0 ? 1 : 0); } -bool SDFullInit() +bool SDBeginRetrying() { // begin() takes 3 seconds for fail, 20-200mS for pass, 2 seconds for unpopulated @@ -559,7 +559,7 @@ bool SDFullInit() Printf_dbg("[=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=][=]\n"); Printf_dbg("Start mediaPresent %d\n", SD.mediaPresent()); //This indicates zero regardless of actual prior to begin() - while (!SDBegin()) + while (!SD.begin(BUILTIN_SDCARD)) { Count--; Printf_dbg("SD Init fail, %d tries left. Time: %lu mS\n", Count, millis()-Startms); @@ -579,6 +579,16 @@ bool SDFullInit() return true; } +bool SDFullInit() +{ + bool Began; + + TRBlockClose(); + Began = SDBeginRetrying(); + TRBlockReopen(); + return Began; +} + bool USBFileSystemWait() { //wait for USB file system to init in case it's needed by startup SID or auto-launched diff --git a/Source/Teensy/nfcScan.ino b/Source/Teensy/nfcScan.ino index b852473d..8cbc926d 100644 --- a/Source/Teensy/nfcScan.ino +++ b/Source/Teensy/nfcScan.ino @@ -173,7 +173,7 @@ FS *FSfromSourceID(RegMenuTypes SourceID) { if(SourceID == rmtSD) { - SDBegin(); // refresh, takes 3 seconds for fail/unpopulated, 20-200mS populated + SDFullInit(); // SD.begin(BUILTIN_SDCARD); with retry if presence detected return &SD; } else if(SourceID == rmtUSBDrive) return &firstPartition; From 18042b732f8a19b3351f359692bf1119c608f577 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:52:20 -0500 Subject: [PATCH 093/100] firmware: a TR block reopen refuses an image that is not the one mounted Every D64 is the same size, so after a card swap the reopen took the new card's file at the same path and GEOS's cached BAM and directory overwrote it. The mount records the file's first sector and the card's serial number; a reopen that finds either changed leaves the image unmounted and logs it. --- .../Common/IO_Handlers/TRBlockIO.c | 24 +++++++++++++++++-- docs/TR_Block_Device.md | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 93357046..21e6cbe4 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -16,6 +16,7 @@ static FsFile TRBlockFile; static bool TRBlockDirty; static uint32_t TRBlockLastWritemS; static char *TRBlockPath; +static struct { uint32_t FirstSector, CardSerial; } TRBlockMounted; FLASHMEM static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) { @@ -62,9 +63,24 @@ FLASHMEM void TRBlockClose() TRBlockDirty = false; } +FLASHMEM static uint32_t TRBlockCardSerial() +{ + cid_t CID; + + return SD.sdfs.card()->readCID(&CID) ? CID.psn : 0; +} + +FLASHMEM static bool TRBlockIsMountedFile() +{ + return TRBlockFile.firstSector() == TRBlockMounted.FirstSector && TRBlockCardSerial() == TRBlockMounted.CardSerial; +} + +// Every D64 is the same size, so a swapped card's image at the same path would take GEOS's cached BAM and directory FLASHMEM void TRBlockReopen() { - if (TRBlockPath) TRBlockOpen(); + if (!TRBlockPath || !TRBlockOpen() || TRBlockIsMountedFile()) return; + TRBlockClose(); + Serial.printf("TR block image: %s on the card is not the one mounted; left unmounted\n", TRBlockPath); } FLASHMEM void TRBlockUnmount() @@ -79,7 +95,11 @@ FLASHMEM bool TRBlockMount(const char *Path) { TRBlockUnmount(); TRBlockPath = strdup(Path); - if (TRBlockPath && TRBlockOpen()) return true; + if (TRBlockPath && TRBlockOpen()) + { + TRBlockMounted = { TRBlockFile.firstSector(), TRBlockCardSerial() }; + return true; + } TRBlockUnmount(); return false; } diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index 7a605447..36fe20c2 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -109,7 +109,7 @@ The firmware does this arithmetic so that a 6502 driver does not have to. A driv * **Speed.** A command takes as long as the card takes. Poll; do not time it. * **C128 2 MHz.** The registers have not been exercised at 2 MHz. A driver on the C128 should run at 1 MHz around a transfer until that is measured. * **Concurrent users.** One command at a time, one driver at a time. Two programs polling the same STATUS will read each other's results; the token tells them apart but does not queue them. -* **The image.** It must not be changed on the card while it is mounted. +* **The image.** It must not be changed on the card while it is mounted. A card swapped while it is mounted is noticed when the firmware next starts the card, as a GeoRAM save does; commands then fail with `$E1` rather than write to the other card. ## A reader's checklist A driver that follows this document: From 51f5d34a964a6fed440d527de5382453015cf8a9 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:57:30 -0500 Subject: [PATCH 094/100] firmware: a TR block flush the card fails stays dirty and is retried GEOS never sends FLUSH, so the idle flush is its only way to the card, and it cleared the dirty flag whether or not the sync succeeded. A failed sync now leaves the image dirty, retries a quarter second later and logs once. --- .../MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 15 +++++++++++++-- docs/TR_Block_Device.md | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 21e6cbe4..436a58f1 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -31,10 +31,21 @@ FLASHMEM static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint return true; } +// GEOS never sends FLUSH, so a failed sync stays dirty for the idle flush to retry FLASHMEM static bool TRBlockFileFlush(void *Context) { - TRBlockDirty = false; - return TRBlockFile.sync(); + static bool FailureLogged; + + if (TRBlockFile.sync()) + { + TRBlockDirty = FailureLogged = false; + return true; + } + TRBlockDirty = true; + TRBlockLastWritemS = millis(); + if (!FailureLogged) Serial.printf("TR block image: writes are not reaching the card; retrying\n"); + FailureLogged = true; + return false; } FLASHMEM static bool TRBlockOpen() diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index 36fe20c2..31692689 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -103,7 +103,7 @@ For the disk image kinds (D64, D71, D81) ADDR0 is the track and ADDR1 the sector The firmware does this arithmetic so that a 6502 driver does not have to. A driver that wants raw sector numbers instead is a different image kind, which this version does not define. ## Writes reach the card later -`WRITE` returns `$5A` when the block is accepted, not when the card holds it. The firmware writes the block to the file at once and flushes the file a quarter of a second after the last `WRITE`, or on `FLUSH`. A `FLUSH` the card fails returns `$E4`. A program that must know a write survived power loss sends `FLUSH` and waits for `$5A`. GEOS does not, so a disk saved and then powered off within that quarter second can be torn. +`WRITE` returns `$5A` when the block is accepted, not when the card holds it. The firmware writes the block to the file at once and flushes the file a quarter of a second after the last `WRITE`, or on `FLUSH`. A `FLUSH` the card fails returns `$E4`. A flush the card fails is tried again every quarter second until the card takes it. A program that must know a write survived power loss sends `FLUSH` and waits for `$5A`. GEOS does not, so a disk saved and then powered off within that quarter second can be torn. ## What the firmware does not promise * **Speed.** A command takes as long as the card takes. Poll; do not time it. From 45ceadbbac3e2a385a37a16230449e7d4128de37 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 21:08:50 -0500 Subject: [PATCH 095/100] firmware: a TR block flush that keeps failing is logged again after the image is reopened The flush-failure log flag was a function static that only a successful sync cleared, so after an image was closed with its sync still failing, a later mount's failures went unlogged. It is file scope now and TRBlockClose clears it with the dirty flag. --- .../Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index 436a58f1..5f4903e3 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -16,6 +16,7 @@ static FsFile TRBlockFile; static bool TRBlockDirty; static uint32_t TRBlockLastWritemS; static char *TRBlockPath; +static bool TRBlockFlushFailureLogged; static struct { uint32_t FirstSector, CardSerial; } TRBlockMounted; FLASHMEM static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) @@ -34,17 +35,15 @@ FLASHMEM static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint // GEOS never sends FLUSH, so a failed sync stays dirty for the idle flush to retry FLASHMEM static bool TRBlockFileFlush(void *Context) { - static bool FailureLogged; - if (TRBlockFile.sync()) { - TRBlockDirty = FailureLogged = false; + TRBlockDirty = TRBlockFlushFailureLogged = false; return true; } TRBlockDirty = true; TRBlockLastWritemS = millis(); - if (!FailureLogged) Serial.printf("TR block image: writes are not reaching the card; retrying\n"); - FailureLogged = true; + if (!TRBlockFlushFailureLogged) Serial.printf("TR block image: writes are not reaching the card; retrying\n"); + TRBlockFlushFailureLogged = true; return false; } @@ -71,7 +70,7 @@ FLASHMEM void TRBlockClose() { TRBlockBack.Context = NULL; if (TRBlockFile.isOpen() && !TRBlockFile.close()) Serial.printf("TR block image: last writes did not reach the card\n"); - TRBlockDirty = false; + TRBlockDirty = TRBlockFlushFailureLogged = false; } FLASHMEM static uint32_t TRBlockCardSerial() From 8b09ed7cf15015384f90aaad311cfe49ce4c4758 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Sat, 24 Oct 2026 21:40:53 -0500 Subject: [PATCH 096/100] firmware: the GEOS launcher retypes CONFIGURE as a system file, not a desk accessory GEOS file type 5 is DESK_ACC; SYSTEM is 4. As a desk accessory, a double-click on CONFIGURE made DeskTop write a swap file and run it, and its IEC drive probe hung the C64 with interrupts off. --- Source/Teensy/GEOS/GeosBoot.h | 2 +- Source/Teensy/tests/geos-boot.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index edf33e73..9d596509 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -13,7 +13,7 @@ #define GEOS_TYPE_BYTE 0x4e #define GEOS_JUMP_OPCODE 0x4c #define GEOS_AUTO_EXEC 14 -#define GEOS_SYSTEM_FILE 5 +#define GEOS_SYSTEM_FILE 4 #define GEOS_CONFIG_PREFIX "CONFIG" #define GEOS_BASIC_START 0x0801 #define GEOS_MEMORY_END 0x10000 diff --git a/Source/Teensy/tests/geos-boot.test.js b/Source/Teensy/tests/geos-boot.test.js index f4a96233..f919f1ea 100644 --- a/Source/Teensy/tests/geos-boot.test.js +++ b/Source/Teensy/tests/geos-boot.test.js @@ -32,7 +32,7 @@ const SLOT_GEOS_TYPE = 24; const PAD = 0xa0; const USR_CLOSED = 0x83; const AUTO_EXEC = 14; -const SYSTEM_FILE = 5; +const SYSTEM_FILE = 4; const DRIVER_AREA = 0x9000; const DRIVER_SIZE = 0xd80; From 3bc93c6014cef13a189b28bbbfefcb0f0d6b6127 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Sun, 25 Oct 2026 17:31:03 -0500 Subject: [PATCH 097/100] firmware: the GEOS 64 launcher looks only for a D81 boot disk image --- Source/Teensy/GeosLauncher.ino | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 15b92b6f..9b5e6f44 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -10,7 +10,8 @@ #define GEOS_EXT_LENGTH 4 #define GEOS_MAX_IMAGES 8 -static const char *const GeosImageExtensions[] = { ".d64", ".d71", ".d81" }; +// Only GEOS 64's D81 image, with the deskTop and the applications on one disk, is supported +#define GEOS_IMAGE_EXTENSION ".d81" FLASHMEM static bool GeosFileRead(void *Context, uint32_t Offset, uint8_t *Block) { @@ -41,12 +42,7 @@ FLASHMEM static bool GeosHasImageExtension(const char *Name) { size_t Length = strlen(Name); - if (Length <= GEOS_EXT_LENGTH) return false; - for (const char *Extension : GeosImageExtensions) - { - if (!strcasecmp(Name + Length - GEOS_EXT_LENGTH, Extension)) return true; - } - return false; + return Length > GEOS_EXT_LENGTH && !strcasecmp(Name + Length - GEOS_EXT_LENGTH, GEOS_IMAGE_EXTENSION); } FLASHMEM static bool GeosIsPatchedCopy(const char *Name) @@ -308,7 +304,7 @@ FLASHMEM bool LaunchGeos(const struct GeosLauncher *Launcher, StructMenuItem *It for (uint8_t i = 0; i < Count && !*BootName; i++) BootDiskFound |= GeosSetUpImage(Launcher, Names[i], BootName); if (!BootDiskFound) { - SendMsgPrintfln(PSTR("Copy your %s boot disk image\r\n(.D64, .D71 or .D81) to %s\r\non the SD card, then select this again."), + SendMsgPrintfln(PSTR("Copy your %s boot disk image\r\n(.D81) to %s\r\non the SD card, then select this again."), Launcher->Title, Launcher->Folder); return false; } From 4210842589247c3f2361adcb942a20778818f5e1 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Sun, 25 Oct 2026 19:32:07 -0500 Subject: [PATCH 098/100] fix: review findings for 5ec90dd --- Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c index e7d75094..684290d0 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -30,7 +30,6 @@ extern bool USBFileSystemWait(); extern FS *FSfromSourceID(RegMenuTypes SourceID); extern void FreeDriveDirMenu(); extern uint8_t RAM_Image[]; -extern uint8_t NumCrtChips; extern uint8_t *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer FLASHMEM void FreeRAMBanks(uint8_t *Bank[], uint8_t *NumBanks) From 156f4a4c5e854191f7308ffcb400bf9f3dcf901f Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Sun, 25 Oct 2026 19:36:57 -0500 Subject: [PATCH 099/100] test: a PSTR("...") message format counts as a literal --- Source/Teensy/tests/c64-message-buffer.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/tests/c64-message-buffer.test.js b/Source/Teensy/tests/c64-message-buffer.test.js index 65b0767e..c86bbccc 100644 --- a/Source/Teensy/tests/c64-message-buffer.test.js +++ b/Source/Teensy/tests/c64-message-buffer.test.js @@ -9,7 +9,7 @@ const FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); const SOURCE_EXTENSIONS = new Set(['.ino', '.c', '.cpp', '.h']); const SKIPPED_DIRECTORIES = new Set(['tests', 'TRMenuFiles']); -const MESSAGE_FORMATTERS = /\b(SendMsgPrintfln|SendMsgPrintf|SendStrPrintfln)\s*\(\s*(\S)/g; +const MESSAGE_FORMATTERS = /\b(SendMsgPrintfln|SendMsgPrintf|SendStrPrintfln)\s*\(\s*(?:PSTR\s*\(\s*)?(\S)/g; const FORMATTER_DECLARATION = /^\s*(?:FLASHMEM\s+|extern\s+)*(?:void|bool)\s+Send(?:Msg|Str)/; function firmwareSources(dir = FIRMWARE_ROOT) { @@ -45,6 +45,9 @@ test('the scan still sees a call wrapped after its open paren', () => { ['x.ino:1 SendMsgPrintfln']); assert.deepEqual(nonLiteralFormats('SendMsgPrintfln(\n "%s", MenuSelCpy.Name);\n', 'x.ino'), []); assert.deepEqual(nonLiteralFormats('/* SendMsgPrintfln(Name); */\n', 'x.ino'), []); + assert.deepEqual(nonLiteralFormats('SendMsgPrintfln(PSTR("%s"), Name);\n', 'x.ino'), []); + assert.deepEqual(nonLiteralFormats('SendMsgPrintfln(PSTR(Name));\n', 'x.ino'), + ['x.ino:1 SendMsgPrintfln']); }); test('no caller throws away whether the current path fit', () => { From 852acec374db4d3694e024b445f5f45a649a8cf7 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Sun, 25 Oct 2026 19:48:49 -0500 Subject: [PATCH 100/100] firmware: each GEOS launcher message passes its format as a literal --- Source/Teensy/GeosLauncher.ino | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 9b5e6f44..b2278fb7 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -192,8 +192,8 @@ FLASHMEM static bool GeosPatchCopy(const struct GeosLauncher *Launcher, const ch if (Replacing && !SD.rename(Backup, Copy)) snprintf(Error, sizeof Error, PSTR("your disk is now %s"), Backup); } SD.remove(Temp); - SendMsgPrintfln(Patch ? PSTR("Could not add the mouse driver to\r\na copy of %s:\r\n%s") : PSTR("Could not make a copy of\r\n%s:\r\n%s"), - Name, Error); + if (Patch) SendMsgPrintfln(PSTR("Could not add the mouse driver to\r\na copy of %s:\r\n%s"), Name, Error); + else SendMsgPrintfln(PSTR("Could not make a copy of\r\n%s:\r\n%s"), Name, Error); return false; } @@ -252,7 +252,8 @@ FLASHMEM static bool GeosSetUpImage(const struct GeosLauncher *Launcher, const c return GeosUse(Launcher, CopyName, BootName); } if (!GeosPatchCopy(Launcher, Name, Source, Copy, NeedsDriver)) return true; - SendMsgPrintfln(NeedsDriver ? PSTR("Added the TR USB mouse driver to\r\na copy of %s") : PSTR("Made a copy of %s\r\nto start from"), Name); + if (NeedsDriver) SendMsgPrintfln(PSTR("Added the TR USB mouse driver to\r\na copy of %s"), Name); + else SendMsgPrintfln(PSTR("Made a copy of %s\r\nto start from"), Name); return GeosUse(Launcher, CopyName, BootName); }