diff --git a/Source/Teensy/MinimalBoot/Common/IOHandlers.h b/Source/Teensy/MinimalBoot/Common/IOHandlers.h index 88bc75b7..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...) @@ -109,6 +110,9 @@ struct stcIOHandlers #ifndef MinimumBuild #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" #endif @@ -158,6 +162,7 @@ stcIOHandlers* IOHandler[] = //Synch order/qty with enum enumIOHandlers &IOHndlr_KernalReplace, //IOH_KernalReplace, #endif &IOHndlr_Debug, //IOH_Debug, + &IOHndlr_GeoRAM, //IOH_GeoRAM, //*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..3f873fb9 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_GeoRAM.c @@ -0,0 +1,115 @@ +// 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(); +void PollingHndlr_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 + &PollingHndlr_GeoRAM, //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 the USB mouse's. +#define GeoRAM_RegsBase 0x80 +#define GeoRAM_RegBlockBit 0x01 + +// 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 * volatile GeoRAM_Page; + +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() +{ + // 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; + FreeRAMBanks(GeoRAM_Bank, &NumBanks); +} + +FLASHMEM void SpecialBtn_GeoRAM(bool Up_nDn) +{ + // on button release save GeoRAM contents + if(Up_nDn) SaveRAMBanksReplacing(GeoRAM_Bank, NumGeoRAM_Banks, GeoRAM_Filename); +} + +FLASHMEM void InitHndlr_GeoRAM() +{ + FreeGeoRAM(); + GeoRAM_Block = GeoRAM_PageNum = 0; + InitUSBMouse(); + uint8_t NumBanks = AllocRAMBanks(GeoRAM_Bank, MAX_GEORAM_BANKS, MIN_GEORAM_BANKS); + if (NumBanks == 0) + { + Serial.printf("GeoRAM is disabled\n"); + return; + } + GeoRAM_Page = GeoRAM_Bank[0]; + NumGeoRAM_Banks = NumBanks; + fSpecialBtnChange = &SpecialBtn_GeoRAM; + Serial.printf("GeoRAM Size: %dK bytes\n", NumBanks * RAM_Bank_Size / 1024); + LoadRAMBanks(GeoRAM_Bank, NumBanks, 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 + 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 (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; + GeoRAM_SelectPage(); + TraceLogAddValidData(Data); +} diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_REU.c index 269499dc..36e46597 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,17 +139,11 @@ 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[]; -// CrtChips.h declares the main image's static table and count. 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) @@ -455,83 +443,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; } @@ -622,37 +490,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..684290d0 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/RAMBanks.c @@ -0,0 +1,237 @@ +// 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 *ptrRAM_ImageEnd; // points to end of CRT-used RAM1 buffer + +FLASHMEM void FreeRAMBanks(uint8_t *Bank[], uint8_t *NumBanks) +{ + 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(); + + 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 + + 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++; + } + + while(NumBanks < MaxBanks && (NULL != (Bank[NumBanks] = (uint8_t*)malloc(RAM_Bank_Size)))) + NumBanks++; + + 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; + 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++) + { + int BytesRead = LoadFile.read(Bank[BankNum], RAM_Bank_Size); + if (BytesRead <= 0) break; + CharNum += BytesRead; + } + Serial.printf("Read %lu Bytes\n", CharNum); + 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) +{ + 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, '.'); + if (pDot == NULL) pDot = ptrFileName + strlen(ptrFileName); //if no extension, set to end of filename + strlcpy(Extension, pDot, sizeof(Extension)); + *pDot = 0; + + 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 + } + + char NewFilename[MaxPathLength]; + + do sprintf(NewFilename, "%s%03d%s", ptrFileName, NewNum++, Extension); + while (sourceFS->exists(NewFilename)); + + if (WriteRAMBanksFile(sourceFS, NewFilename, Bank, NumBanks)) FlashSavedLED(); +} + +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); +} + +// 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 + + 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; + 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(); +} 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..c3612801 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/USBMouse.c @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT + +// A USB mouse on the TR's USB host port, read through registers the GeoRAM handler serves +// at $DF00, in the part of the IO2 page that GeoRAM leaves free. +// 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 tell that the registers are there + +#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 +#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 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 + if (!static_cast(usbHostMouse)) + { + // 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 + 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(); + asm volatile("" ::: "memory"); + NVIC_ENABLE_IRQ(IRQ_USBHS); + 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); + USBMouse_Ready[Axis] = 0; + } +} + +void USBMouseIORead(uint8_t Address) +{ + if (Address < 2) + { + 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) + DataPortWriteWaitLog(USBMouse_ID[Address - USBMouse_RegID]); +} diff --git a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h index 166ecfe0..a3c616da 100644 --- a/Source/Teensy/MinimalBoot/Common/Menu_Regs.h +++ b/Source/Teensy/MinimalBoot/Common/Menu_Regs.h @@ -437,7 +437,8 @@ enum enumIOHandlers //Synch order/qty with IOHandler[] (IOHandlers.h) #ifdef Fab04_KernalReplace IOH_KernalReplace, #endif - IOH_Debug, //last manually selectable, see LastSelectableIOH + IOH_Debug, + IOH_GeoRAM, //last manually selectable, see LastSelectableIOH //*not* manually selectable, see LastSelectableIOH @@ -471,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 diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index cfa10e5e..351577da 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -273,6 +273,7 @@ void RemoteLaunch(RegMenuTypes MenuSourceID, const char *FileNamePath, bool DoCa #ifdef Fab04_REU FreeREU(); #endif + FreeGeoRAM(); FreeSwiftlinkBuffs(); if (MenuSourceID == rmtTeensy) diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index ff171ca6..569254f0 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; diff --git a/Source/Teensy/tests/usb-mouse.test.js b/Source/Teensy/tests/usb-mouse.test.js new file mode 100644 index 00000000..cd791b71 --- /dev/null +++ b/Source/Teensy/tests/usb-mouse.test.js @@ -0,0 +1,100 @@ +// 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(`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 }, () => { + 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; +} diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py new file mode 100644 index 00000000..d8a72f61 --- /dev/null +++ b/tools/geos/trmouse.py @@ -0,0 +1,281 @@ +#!/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 (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 +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' +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 + +SECTOR_SIZE = 256 +DIR_ENTRY_SIZE = 32 + +USR_CLOSED = 0x83 +GEOS_SEQUENTIAL = 0 +INPUT_128 = 15 +INFO_ICON_BITMAP = 0xbf # an uncompressed 63-byte icon follows + +ICON = """ +........................ +.........######......... +.......##..#...##....... +......#....#.....#...... +.....#.....#......#..... +.....#.....#......#..... +.....#.....#......#..... +.....##############..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +.....#............#..... +......#..........#...... +.......##......##....... +.........######......... +........................ +...####.####.####....... +.....#..#..#.#..#....... +.....#..###..####....... +""" + + +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)) + + +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): + 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 (self.format.starts[track - 1] + sector) * SECTOR_SIZE + + 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)) + 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): + if track not in self.format.reserved: + found = self.take(track) + if found: + return found + sys.exit('the disk is full') + + 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 + 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) + 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(): + 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.""" + 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') + 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 self.slots(*new) + + 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 + SECTOR_SIZE] = padded(link + chunk, SECTOR_SIZE) + info_ts = self.allocate() + o = self.offset(*info_ts) + self.image[o:o + SECTOR_SIZE] = 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(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) + + +def install(image, code): + 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: + 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() + 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..cc6f1d3e --- /dev/null +++ b/tools/geos/trmouse128.asm @@ -0,0 +1,148 @@ +; 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 + +regs = $df00 +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 ID tells whether the registers are there, beside GeoRAM. +; $DF03-$DF05 are REU address registers, which have no side effects when read. +; Carry set: what reads there is not a mouse. +FindRegs + ldx #2 +- lda regs+regID,x + cmp trID,x + bne + + dex + bpl - + clc + rts ++ sec + rts + +trID !text "TRM" + +UpdateMouse + jsr FindRegs + bcs noMouse + lda regs+regX + sta deltaX + lda regs+regY + sta deltaY + bit mouseOn + bmi readButtons +noMouse rts ; movement while the pointer is off is dropped + +readButtons + lda regs+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 +++ + + ldx #0 + lda deltaX + 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 + + lda 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