diff --git a/Source/Teensy/DriveDirLoad.ino b/Source/Teensy/DriveDirLoad.ino index 253a32ee..218dbe47 100644 --- a/Source/Teensy/DriveDirLoad.ino +++ b/Source/Teensy/DriveDirLoad.ino @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "MinimalBoot/Common/C128Crt.h" #ifdef VM_EXTENSIONS_ENABLED #include "MinimalBoot/Common/VMLaunch.h" #include "MinimalBoot/Common/VMPackageList.h" @@ -303,6 +304,8 @@ FLASHMEM void HandleExecution() //has to be distilled down to one of these by this point, only ones supported so far. //Emulate ROM or prep for transfer uint8_t CartLoaded = false; + //a C128 cart widens these, and a direct remote launch skips SetUpMainMenuROM, which resets them + LOROM_Mask = HIROM_Mask = 0x1fff; switch(MenuSelCpy.ItemType) { @@ -392,10 +395,7 @@ FLASHMEM void HandleExecution() CartLoaded=true; break; case rtBinC128: - SetGameDeassert; - SetExROMDeassert; - LOROM_Image = MenuSelCpy.Code_Image; - HIROM_Image = NULL; + LaunchC128Cart(); CartLoaded=true; break; case rtFilePrg: @@ -696,6 +696,7 @@ void FreeCrtChips() for(uint16_t cnt=0; cnt < NumCrtChips; cnt++) if((uint32_t)CrtChips[cnt].ChipROM >= 0x20200000) free(CrtChips[cnt].ChipROM); NumCrtChips = 0; + C128Clock = NULL; } uint8_t Assoc_Ext_ItemType(char * FileName) diff --git a/Source/Teensy/FileParsers.ino b/Source/Teensy/FileParsers.ino index ca18e0d2..082585e7 100644 --- a/Source/Teensy/FileParsers.ino +++ b/Source/Teensy/FileParsers.ino @@ -57,7 +57,7 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) SendMsgPrintfln("Parsing CRT File"); SendMsgPrintfln("CRT image size: %luK $%08x", MyMenuItem->Size/1024, MyMenuItem->Size); - if (memcmp(CRT_Image, "C128 CARTRIDGE", 14)==0) SendMsgPrintfln("C128 crt"); + if (DetectC128Crt(CRT_Image)) SendMsgPrintfln("C128 crt"); else if (memcmp(CRT_Image, "C64 CARTRIDGE", 13)==0) SendMsgPrintfln("C64 crt"); else { @@ -76,6 +76,8 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) uint16_t HWType = toU16(CRT_Image+0x16); SendMsgPrintfln("HW Type: %d ($%04x)", (int16_t)HWType, HWType); + if (!C128HWTypeSupported(HWType)) return false; + #ifdef Fab04_Freezers // Create pseudo-CRT number (real HWType + 200) to select dual REU & CRT handler @@ -465,6 +467,8 @@ bool SetTypeFromCRT(StructMenuItem* MyMenuItem, uint8_t EXROM, uint8_t GAME) //check configuration + if (CrtIsC128) return SetC128Type(MyMenuItem); + #ifdef Fab04_Freezers if (IO1[rwRegNextIOHndlr] == IOH_SuperSnapshotV5 || IO1[rwRegNextIOHndlr] == IOH_FinalCartridgeIII || @@ -501,13 +505,6 @@ bool SetTypeFromCRT(StructMenuItem* MyMenuItem, uint8_t EXROM, uint8_t GAME) return true; } - if(CrtChips[0].LoadAddress == 0x0000 && CrtChips[0].ROMSize == 0x2000 && EXROM==0 && GAME==0) - { - MyMenuItem->ItemType = rtBinC128; - SendMsgPrintfln(" C128 config"); - return true; - } - SendMsgPrintfln("HW config unknown"); return false; } diff --git a/Source/Teensy/MinimalBoot/Common/C128Crt.h b/Source/Teensy/MinimalBoot/Common/C128Crt.h new file mode 100644 index 00000000..774b9077 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/C128Crt.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include "CrtChips.h" + +// Included after IOHandlers.h and Common_Defs.h, ahead of the .ino code that calls it: it sets the +// cart lines and ROM pointers those define. + +extern StructCrtChip CrtChips[]; +extern uint8_t NumCrtChips; + +bool CrtIsC128 = false; +static C128CartMap C128Map; + +// Record layout: tools/c128/README.md, "The boot clock". +static const uint8_t C128ClockTag[8] = {'T', 'R', 'C', 'L', 'O', 'C', 'K', 1}; +static const uint8_t C128ClockLen = 7; +static uint8_t *C128Clock = NULL; +static const int32_t TimezoneStepSecs = 30 * 60; +static int32_t C128ClockLocalOffset; + +static bool DetectC128Crt(const uint8_t *CrtHeader) +{ + CrtIsC128 = (memcmp(CrtHeader, "C128 CARTRIDGE", 14) == 0); + return CrtIsC128; +} + +static bool C128HWTypeSupported(uint16_t HWType) +{ //C128 crts number their HW types separately: 1 is Warpspeed128, not Action Replay + if (!CrtIsC128 || HWType == Cart_Generic) return true; + SendMsgPrintfln("Unsupported C128 HW Type"); + return false; +} + +// The C128 half of SetTypeFromCRT; keeps the map for LaunchC128Cart. +static bool SetC128Type(StructMenuItem *MyMenuItem) +{ + if (!MapC128Cart(CrtChips, NumCrtChips, &C128Map)) + { + SendMsgPrintfln("C128 chip layout unsupported"); + return false; + } + MyMenuItem->ItemType = rtBinC128; + SendMsgPrintfln(" C128 config"); + return true; +} + +static uint8_t *FindC128Clock(const uint8_t *Image, uint16_t Mask) +{ + if (Image == NULL || ((uint32_t)Image & SwapSeekAddrMask) == SwapSeekAddrMask) return NULL; + for (uint32_t Offset = 0; Offset + sizeof(C128ClockTag) + C128ClockLen <= (uint32_t)Mask + 1; Offset++) + if (memcmp(Image + Offset, C128ClockTag, sizeof(C128ClockTag)) == 0) + return (uint8_t *)Image + Offset + sizeof(C128ClockTag); + return NULL; +} + +static void WriteC128Clock() +{ + tmElements_t tm; + breakTime(Teensy3Clock.get() + C128ClockLocalOffset, tm); + const uint16_t Year = tmYearToCalendar(tm.Year); + const uint8_t Clock[C128ClockLen] = {(uint8_t)(Year / 100), (uint8_t)(Year % 100), + tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second}; + memcpy(C128Clock, Clock, C128ClockLen); +} + +void UpdateC128Clock() +{ + static uint32_t LastWrite; + if (C128Clock == NULL || millis() - LastWrite < 100) return; + LastWrite = millis(); + WriteC128Clock(); +} + +static void LaunchC128Cart() +{ + SetGameDeassert; + SetExROMDeassert; + LOROM_Image = C128Map.ROML; + HIROM_Image = C128Map.ROMH; + LOROM_Mask = C128Map.ROMLMask; + HIROM_Mask = C128Map.ROMHMask; + + C128Clock = FindC128Clock(C128Map.ROML, C128Map.ROMLMask); + if (C128Clock == NULL) C128Clock = FindC128Clock(C128Map.ROMH, C128Map.ROMHMask); + if (C128Clock == NULL) return; + C128ClockLocalOffset = (int8_t)EEPROM.read(eepAdTimezone) * TimezoneStepSecs; + WriteC128Clock(); +} diff --git a/Source/Teensy/MinimalBoot/Common/CrtChips.h b/Source/Teensy/MinimalBoot/Common/CrtChips.h new file mode 100644 index 00000000..66591334 --- /dev/null +++ b/Source/Teensy/MinimalBoot/Common/CrtChips.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +#pragma once +#include +#include + +// One CHIP packet of a loaded .crt. Pure, so the native tests can hold the layout rules below. +struct StructCrtChip +{ + uint8_t *ChipROM; + uint16_t LoadAddress; + uint16_t ROMSize; + uint16_t BankNum; +}; + +struct C128CartMap +{ + const uint8_t *ROML, *ROMH; + uint16_t ROMLMask, ROMHMask; +}; + +// Generic C128 cart (VICE CRT spec, C128 type 0): ROML is $8000-$BFFF and ROMH $C000-$FFFF, each +// one 8k (mirrored) or 16k chip. +static inline bool MapC128Cart(const StructCrtChip *Chips, uint8_t NumChips, C128CartMap *Map) +{ + const StructCrtChip *ROML = NULL, *ROMH = NULL; + for (uint8_t ChipNum = 0; ChipNum < NumChips; ChipNum++) + { + const StructCrtChip *Chip = &Chips[ChipNum]; + if (Chip->BankNum != 0 || (Chip->ROMSize != 0x2000 && Chip->ROMSize != 0x4000)) return false; + + const StructCrtChip **Slot; + if (Chip->LoadAddress == 0x8000) Slot = &ROML; + else if (Chip->LoadAddress == 0xC000) Slot = &ROMH; + else return false; + + if (*Slot != NULL) return false; + *Slot = Chip; + } + if (ROML == NULL && ROMH == NULL) return false; + + Map->ROML = ROML ? ROML->ChipROM : NULL; + Map->ROMH = ROMH ? ROMH->ChipROM : NULL; + Map->ROMLMask = ROML ? ROML->ROMSize - 1 : 0x1fff; + Map->ROMHMask = ROMH ? ROMH->ROMSize - 1 : 0x1fff; + return true; +} diff --git a/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h b/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h index 59360f59..99ddf0ba 100644 --- a/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h +++ b/Source/Teensy/MinimalBoot/Common/DriveDirLoad.h @@ -184,13 +184,7 @@ StructHWID_IOH_Assoc HWID_IOH_Assoc[]= #define SwapSeekAddrMask 0xef000000 //virtual address mask/marker for swap-out blocks, or-ed with file seek address (minimal build only) -struct StructCrtChip -{ - uint8_t *ChipROM; - uint16_t LoadAddress; - uint16_t ROMSize; - uint16_t BankNum; -}; +#include "CrtChips.h" #ifndef MinimumBuild //only needed in full build: diff --git a/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino b/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino index 7b54cca1..2e48b572 100644 --- a/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino +++ b/Source/Teensy/MinimalBoot/Min_DriveDirLoad.ino @@ -20,6 +20,7 @@ uint8_t NumCrtChips = 0; StructCrtChip CrtChips[MAX_CRT_CHIPS]; +#include "Common/C128Crt.h" File myFile = NULL; void HandleExecution() @@ -62,10 +63,7 @@ void HandleExecution() CartLoaded=true; break; case rtBinC128: - SetGameDeassert; - SetExROMDeassert; - LOROM_Image = MenuSelCpy.Code_Image; - HIROM_Image = NULL; + LaunchC128Cart(); CartLoaded=true; break; case rtUnknown: //had to have been marked unknown after check at start @@ -237,7 +235,7 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) SendMsgPrintfln("Parsing CRT File"); SendMsgPrintfln("CRT image size: %luK $%08x", MyMenuItem->Size/1024, MyMenuItem->Size); - if (memcmp(CRT_Image, "C128 CARTRIDGE", 14)==0) SendMsgPrintfln("C128 crt"); + if (DetectC128Crt(CRT_Image)) SendMsgPrintfln("C128 crt"); else if (memcmp(CRT_Image, "C64 CARTRIDGE", 13)==0) SendMsgPrintfln("C64 crt"); else { @@ -256,6 +254,8 @@ bool ParseCRTHeader(StructMenuItem* MyMenuItem, uint8_t *EXROM, uint8_t *GAME) uint16_t HWType = toU16(CRT_Image+0x16); SendMsgPrintfln("HW Type: %d ($%04x)", (int16_t)HWType, HWType); + if (!C128HWTypeSupported(HWType)) return false; + if (HWType != Cart_Generic) //leave IOH as default/user set for generic { if (!AssocHWID_IOH(HWType)) @@ -326,6 +326,7 @@ void FreeCrtChips() for(uint16_t cnt=0; cnt < NumCrtChips; cnt++) if((uint32_t)CrtChips[cnt].ChipROM >= 0x20200000 && (uint32_t)CrtChips[cnt].ChipROM < SwapSeekAddrMask) free(CrtChips[cnt].ChipROM); NumCrtChips = 0; + C128Clock = NULL; } bool PathIsRoot() @@ -339,6 +340,8 @@ bool SetTypeFromCRT(StructMenuItem* MyMenuItem, uint8_t EXROM, uint8_t GAME) MyMenuItem->Code_Image = CrtChips[0].ChipROM; //check configuration + + if (CrtIsC128) return SetC128Type(MyMenuItem); if(CrtChips[0].LoadAddress == 0x8000 && CrtChips[0].ROMSize == 0x2000) //Usually GAME ==1 and EXROM==0 @@ -364,13 +367,6 @@ bool SetTypeFromCRT(StructMenuItem* MyMenuItem, uint8_t EXROM, uint8_t GAME) return true; } - if(CrtChips[0].LoadAddress == 0x0000 && CrtChips[0].ROMSize == 0x2000 && EXROM==0 && GAME==0) - { - MyMenuItem->ItemType = rtBinC128; - SendMsgPrintfln(" C128 config"); - return true; - } - SendMsgPrintfln("HW config unknown"); return false; } diff --git a/Source/Teensy/MinimalBoot/MinimalBoot.ino b/Source/Teensy/MinimalBoot/MinimalBoot.ino index c70e85b1..9642fad8 100644 --- a/Source/Teensy/MinimalBoot/MinimalBoot.ino +++ b/Source/Teensy/MinimalBoot/MinimalBoot.ino @@ -44,6 +44,7 @@ StructMenuItem DriveDirMenu; char DriveDirPath[MaxPathLength]; uint16_t LOROM_Mask, HIROM_Mask; Stream *CmdChannel = &Serial; +void UpdateC128Clock(); //C128Crt.h, which needs the globals here and comes in with the .ino code #ifdef FeatTCPListen #include @@ -246,6 +247,8 @@ void loop() runMainTRApp_FromMin(); #endif } + + UpdateC128Clock(); if (doReset) { diff --git a/Source/Teensy/TRMenuFiles/ROMs/C128_789010.crt.h b/Source/Teensy/TRMenuFiles/ROMs/C128_789010.crt.h index 922fb8b7..da9943b4 100644 --- a/Source/Teensy/TRMenuFiles/ROMs/C128_789010.crt.h +++ b/Source/Teensy/TRMenuFiles/ROMs/C128_789010.crt.h @@ -8,7 +8,7 @@ PROGMEM static const unsigned char C128_789010_crt[] = { 0x72, 0x69, 0x63, 0x20, 0x43, 0x31, 0x32, 0x38, 0x20, 0x43, 0x61, 0x72, 0x74, 0x72, 0x69, 0x64, 0x67, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x48, 0x49, 0x50, 0x00, 0x00, 0x20, 0x10, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x4c, 0x0a, 0x80, 0x4c, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x20, 0x00, 0x4c, 0x0a, 0x80, 0x4c, 0x0a, 0x80, 0x02, 0x43, 0x42, 0x4d, 0x78, 0xa2, 0xff, 0x9a, 0xd8, 0xa9, 0x73, 0x85, 0x01, 0xa9, 0x37, 0x85, 0x00, 0xa9, 0x3a, 0x8d, 0x00, 0xff, 0xa9, 0x06, 0x8d, 0x06, 0xd5, 0xa2, 0x04, 0xbd, 0xf1, 0x98, 0x9d, 0x07, diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 99d05145..8e464624 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -67,6 +67,7 @@ uint8_t nfcState = nfcStateBitDisabled; //default disabled unless set in eeprom Stream *CmdChannel = &Serial; bool isFrozen = false; void (*fSpecialBtnChange)(bool Up_nDn); //Pointer to function called when Special Button Changes +void UpdateC128Clock(); //C128Crt.h, which needs the globals here and comes in with the .ino code #ifdef Fab04_SpecialButton Bounce SpecialBtnBounce = Bounce(Special_Btn_In_PIN, 35); //35mS debounce time @@ -285,6 +286,8 @@ void loop() if (IO1[rwRegPwrUpDefaults2] & rpud2NFCEnabled) nfcInit(); //connect to nfc scanner SetUpMainMenuROM(); //back to main menu, also sets doReset } + + UpdateC128Clock(); if (doReset) { diff --git a/tools/verify-extensions.mjs b/tools/verify-extensions.mjs index 36e498fa..e745f690 100644 --- a/tools/verify-extensions.mjs +++ b/tools/verify-extensions.mjs @@ -503,8 +503,9 @@ native('fail_test'); native('hello_module_test', [sandbox('hello-sandbox-')]); native('host_install_test', [hostPackageFixture(sandbox('host-package-'))]); native('c64spans_test'); +native('c128cart_test'); native('transfer_test'); if (keep) console.log(`Artifacts kept in ${output}`); else fs.rmSync(output, { recursive: true, force: true }); -console.log('PASS: extension loader conformance (published host contract, package format, file services, image validation, registry, settings package list, launch routing, listing invalidation, packet scheduler, failure reporting, host-package installation, reference module, C64 span rules)'); +console.log('PASS: extension loader conformance (published host contract, package format, file services, image validation, registry, settings package list, launch routing, listing invalidation, packet scheduler, failure reporting, host-package installation, reference module, C64 span rules, C128 cart layouts)'); diff --git a/vm/tests/c128cart_test.cpp b/vm/tests/c128cart_test.cpp new file mode 100644 index 00000000..a264386c --- /dev/null +++ b/vm/tests/c128cart_test.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// Which chip layouts a "C128 CARTRIDGE" .crt may have, and where each chip lands. +#include +#include "../../Source/Teensy/MinimalBoot/Common/CrtChips.h" + +static uint8_t RomA[1], RomB[1]; + +static StructCrtChip chip(uint8_t *Rom, uint16_t Load, uint16_t Size, uint16_t Bank = 0) +{ + return StructCrtChip{Rom, Load, Size, Bank}; +} + +static bool map(const StructCrtChip *Chips, uint8_t Num, C128CartMap *Out) +{ + *Out = C128CartMap{RomA, RomA, 0, 0}; + return MapC128Cart(Chips, Num, Out); +} + +int main() { + C128CartMap Map; + + const StructCrtChip Lo8k[] = {chip(RomA, 0x8000, 0x2000)}; + assert(map(Lo8k, 1, &Map)); + assert(Map.ROML == RomA && Map.ROMLMask == 0x1fff && Map.ROMH == NULL); + + const StructCrtChip Lo16k[] = {chip(RomA, 0x8000, 0x4000)}; + assert(map(Lo16k, 1, &Map)); + assert(Map.ROML == RomA && Map.ROMLMask == 0x3fff && Map.ROMH == NULL); + + const StructCrtChip Both[] = {chip(RomB, 0xC000, 0x4000), chip(RomA, 0x8000, 0x2000)}; + assert(map(Both, 2, &Map)); + assert(Map.ROML == RomA && Map.ROMLMask == 0x1fff); + assert(Map.ROMH == RomB && Map.ROMHMask == 0x3fff); + + const StructCrtChip HiOnly[] = {chip(RomB, 0xC000, 0x2000)}; + assert(map(HiOnly, 1, &Map)); + assert(Map.ROML == NULL && Map.ROMH == RomB && Map.ROMHMask == 0x1fff); + + // The load address TeensyROM once required, and nowhere else in the spec. + const StructCrtChip Old0000[] = {chip(RomA, 0x0000, 0x2000)}; + assert(!map(Old0000, 1, &Map)); + + const StructCrtChip TwoLo[] = {chip(RomA, 0x8000, 0x2000), chip(RomB, 0x8000, 0x2000)}; + assert(!map(TwoLo, 2, &Map)); + const StructCrtChip TwoHi[] = {chip(RomA, 0xC000, 0x2000), chip(RomB, 0xC000, 0x2000)}; + assert(!map(TwoHi, 2, &Map)); + + const StructCrtChip Banked[] = {chip(RomA, 0x8000, 0x4000, 1)}; + assert(!map(Banked, 1, &Map)); + + const StructCrtChip Small[] = {chip(RomA, 0x8000, 0x1000)}; + assert(!map(Small, 1, &Map)); + const StructCrtChip Big[] = {chip(RomA, 0x8000, 0x8000)}; + assert(!map(Big, 1, &Map)); + + const StructCrtChip A000[] = {chip(RomA, 0xA000, 0x2000)}; + assert(!map(A000, 1, &Map)); + const StructCrtChip E000[] = {chip(RomA, 0xE000, 0x2000)}; + assert(!map(E000, 1, &Map)); + + assert(!map(Lo8k, 0, &Map)); + return 0; +}