From 2db2ca51fc79eac8a6c1ddc6535c32ef869e80c0 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 15 Oct 2026 14:31:22 -0500 Subject: [PATCH 01/33] 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 02/33] 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 03/33] 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 04/33] 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 05/33] 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 06/33] 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 07/33] 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 08/33] 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 09/33] 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 10/33] 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 11/33] 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 12/33] 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 13/33] 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 14/33] 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 15/33] 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 16/33] 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 17/33] 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 18/33] 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 19/33] 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 20/33] 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 21/33] 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 22/33] 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 ce6ffef9fd36fb6701b0e828cb3440d7863146d3 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:09:16 -0500 Subject: [PATCH 23/33] 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 24/33] 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 25/33] 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 26/33] 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 9d1c7b98468ace632b43b2b0ebd8a4396e841e14 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:28:16 -0500 Subject: [PATCH 27/33] 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 dbc1ba8c833004e25de57847156968a28ddc11c0 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:33:07 -0500 Subject: [PATCH 28/33] 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 9d413205c2e131d8bf9d1f68231311f6f5af06a4 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:36:06 -0500 Subject: [PATCH 29/33] 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 30/33] 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 31/33] 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 e7d08660adc8ff375a66465b7c82cafd4a604162 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 20:44:14 -0500 Subject: [PATCH 32/33] 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 4210842589247c3f2361adcb942a20778818f5e1 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Sun, 25 Oct 2026 19:32:07 -0500 Subject: [PATCH 33/33] 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)