From 6ec3c1e7277fe6420897d25d4ea0f0b415352740 Mon Sep 17 00:00:00 2001 From: Wes Biggs Date: Fri, 30 Oct 2026 22:58:47 -0500 Subject: [PATCH 1/2] Kernal Replace: poll the Ethernet stack from loop(), not the PIT ISR Once Ethernet is initialized (no cable needed), NativeEthernet runs the whole FNET stack from a 1 ms IntervalTimer (PIT) ISR. That made about 39 phi2 interrupts per second late (>1.1 us), even with the GPIO interrupt at the higher priority; masking IRQ_ENET did not help, masking IRQ_PIT did. Late interrupts hurt most on the first read after a $01 write, where Kernal Replace senses HIRAM before it drives the bus: Quest for the King (which switches $01 constantly) got corrupt reads, NMIs or froze, and so did CCGMS with SwiftLink over TCP under a replaced KERNAL. While Kernal Replace serves the KERNAL and the stack is up, mask IRQ_PIT and call fnet_poll() from loop() every ms (FnetLoopPollService, FLASHMEM). Late phi2 interrupts drop to about one every 2 s. PreStart starts it, because loop() only sees the new handler ~0.4 s after a launch and ~15 late interrupts in that window still crashed Quest for the King; the main menu ends it. Under Fab04_GlobalKernalReplace SwiftLink can run meanwhile, and its AT commands block waiting for the stack (connect, DHCP, DNS), so ProcessATCommand() unmasks IRQ_PIT while one runs. Co-Authored-By: Claude Opus 5.5 --- .../Common/IO_Handlers/IOH_KernalReplace.c | 3 ++ .../Common/IO_Handlers/Swift_ATcommands.c | 11 +++++- Source/Teensy/Teensy.ino | 38 +++++++++++++++++++ docs/Architecture/Constraints.md | 4 ++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_KernalReplace.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_KernalReplace.c index 3ba276c8d..d3e657ce6 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_KernalReplace.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_KernalReplace.c @@ -334,6 +334,8 @@ FLASHMEM void FreeKernalBin() #endif } +void FnetLoopPollStart(); //Teensy.ino + FLASHMEM void InitHndlr_KERNALReplace_PreStart() //not called automatically via init; called earlier, manually, from KERNALPreStart() { #ifdef Fab04_GlobalKernalReplace @@ -400,6 +402,7 @@ FLASHMEM void InitHndlr_KERNALReplace_PreStart() //not called automatically via #else fBusSnoop = KernalCheckArm(); #endif + FnetLoopPollStart(); //Ethernet poll off the PIT timer from now on (see FnetLoopPollService in Teensy.ino) } FLASHMEM void InitHndlr_KernalReplace() diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c index 24be470f7..40b06ddfe 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c @@ -410,7 +410,7 @@ FLASHMEM ATRespCode AT_Hook(char* CmdArg) } -FLASHMEM ATRespCode ProcessATCommand() +FLASHMEM ATRespCode ProcessATCommandInner() { char* CmdMsg = TxMsg; //local pointer for manipulation stcATCommand ATCommands[] = @@ -466,3 +466,12 @@ FLASHMEM ATRespCode ProcessATCommand() AddToPETSCIIStrToRxQueueLN(TxMsg); return ATRC_ERROR; } + +extern volatile bool FnetLoopPoll; +FLASHMEM ATRespCode ProcessATCommand() +{ //connect/DHCP/DNS block in here waiting for the stack, which then needs its PIT poll timer (masked while Fab04_GlobalKernalReplace serves the KERNAL, see FnetLoopPollService) + if (FnetLoopPoll) NVIC_ENABLE_IRQ(IRQ_PIT); + ATRespCode r = ProcessATCommandInner(); + if (FnetLoopPoll) NVIC_DISABLE_IRQ(IRQ_PIT); + return r; +} diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 3530c17f6..13d6d4c96 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -55,6 +55,14 @@ uint8_t RAM_Image[RAM_ImageSize]; //Main RAM1 file storage buffer volatile uint8_t BtnPressed = false; volatile uint8_t EmulateVicCycles = false; uint8_t CurrentIOHandler = IOH_None; +volatile bool FnetLoopPoll = false; //Ethernet stack polled from loop(), PIT timer masked (see FnetLoopPollService) +volatile bool FnetPollHold = false; //set by Kernal Replace PreStart, before CurrentIOHandler changes; cleared with the main menu +void FnetLoopPollService(); +FLASHMEM void FnetLoopPollStart() +{ //start polling from loop() right away: loop() only sees the new handler ~0.4 s after the launch, and late phi2 interrupts in that time crashed programs + FnetPollHold = true; + if (fnet_netif_get_default() != 0 && !FnetLoopPoll) { NVIC_DISABLE_IRQ(IRQ_PIT); FnetLoopPoll = true; } +} StructMenuItem *DriveDirMenu = NULL; uint16_t NumDrvDirMenuItems = 0; char DriveDirPath[MaxPathLength]; @@ -377,6 +385,8 @@ void loop() if (IO1[rwRegPwrUpDefaults2] & rpud2TRContEnabled) if (USBHostSerial.available()) ServiceSerial(&USBHostSerial); + FnetLoopPollService(); + #ifdef FeatTCPListen if (NetListenEnable) { @@ -399,12 +409,40 @@ void loop() if (IOHandler[CurrentIOHandler]->PollingHndlr != NULL) IOHandler[CurrentIOHandler]->PollingHndlr(); } +FLASHMEM void FnetLoopPollService() +{ //While Kernal Replace serves the KERNAL, run the Ethernet stack poll from here instead of NativeEthernet's 1ms PIT timer ISR: + //with the poll in the PIT interrupt, ~39 phi2 interrupts/s were late (>1.1us) even with the GPIO interrupt at a higher priority, + //and a late one on the HIRAM sense after a $01 write corrupts the read; called from loop() they are down to about one every 2s. + //(The PIT is only used by that timer, except for ASID, which masks it itself.) + static uint32_t lastPollUs = 0; + bool want = FnetPollHold +#ifdef Fab04_GlobalKernalReplace + || (fKernRepl != NULL) +#endif +#ifdef Fab04_KernalReplace + || (CurrentIOHandler == IOH_KernalReplace) +#endif + ; + want = want && (fnet_netif_get_default() != 0); //stack initialized + if (want != FnetLoopPoll) + { + FnetLoopPoll = want; + if (want) NVIC_DISABLE_IRQ(IRQ_PIT); else NVIC_ENABLE_IRQ(IRQ_PIT); + } + if (FnetLoopPoll && micros() - lastPollUs >= 1000) + { + lastPollUs = micros(); + fnet_poll(); + } +} + FLASHMEM void SetUpMainMenuROM() { PendingfBusSnoop = fBusSnoop = NULL; #ifdef Fab04_GlobalKernalReplace fKernRepl = NULL; #endif + FnetPollHold = false; SetDMADeassert; SetIRQDeassert; SetNMIDeassert; diff --git a/docs/Architecture/Constraints.md b/docs/Architecture/Constraints.md index d9423bd5b..978176d06 100644 --- a/docs/Architecture/Constraints.md +++ b/docs/Architecture/Constraints.md @@ -81,6 +81,10 @@ Beyond priority, Ethernet/PIT interrupts are fully **disabled** (not just deprio - **Firmware self-flash** — `Flash/FXUtil.cpp:175-176` disables both, and goes further by fully `detachInterrupt()`-ing the button and PHI2 pins entirely just above (lines 173-174) — there's no valid handler to service PHI2 while live firmware is being overwritten. - **ASID (MIDI SID) playback** — `IOH_ASID.c:579-580` disables both during ASID streaming, which has its own tight audio-rate timing requirement. +The priority alone does not keep the Ethernet stack out of `isrPHI2`'s way. NativeEthernet runs the whole FNET stack (`fnet_poll()`) from a 1 ms `IntervalTimer` (PIT) ISR, and with the stack initialized (no cable needed) about 39 PHI2 interrupts per second came in late (>1.1 µs), with the GPIO interrupt at the higher priority and with `IRQ_ENET` masked. A handler that serves bytes on reads it has to sense first (Kernal Replace's HIRAM sense) gets corrupt reads, NMIs or freezes from those. So while **Kernal Replace** serves the KERNAL (its Special IO, or `fKernRepl` with `Fab04_GlobalKernalReplace`), `IRQ_PIT` is masked and `FnetLoopPollService()` (`Teensy.ino`) calls `fnet_poll()` from `loop()` every ms instead: late PHI2 interrupts drop to about one every 2 s. Kernal Replace starts this in `InitHndlr_KERNALReplace_PreStart()` (`FnetLoopPollStart()`), since `loop()` sees the new handler only ~0.4 s after a launch; `SetUpMainMenuROM()` ends it. Two consequences: +- TCP is serviced only as often as `loop()` comes round meanwhile, so a `PollingHndlr` that blocks also stalls the network. +- Code that blocks waiting for the stack (`connect`, DHCP, DNS) needs the PIT poll back: `ProcessATCommand()` (`Swift_ATcommands.c`) unmasks `IRQ_PIT` while an AT command runs (SwiftLink under `Fab04_GlobalKernalReplace`). New blocking network calls that can run while the KERNAL is replaced need the same. + ## The main `loop()` is single-threaded — a blocking call anywhere in it stalls every other main-loop-driven task `Teensy.ino`'s `loop()` dispatches `ServiceSerial()`/`ProcessCommand()` (incoming serial/network From aaa2b8c6a423975c8fa9996a31d7ca67327d4bd1 Mon Sep 17 00:00:00 2001 From: Wes Biggs Date: Sat, 31 Oct 2026 13:07:16 -0500 Subject: [PATCH 2/2] Kernal Replace loop poll: compile it only where Kernal Replace exists A plain TR has no Kernal Replace (Fab04_KernalReplace is TR+ only), so the poll there never started but still ran its check on every loop() and cost flash and RAM1. Guard it with Fab04_KernalReplace: the TR image is now byte-for-byte the size of main's. The ProcessATCommand() wrapper matters only when SwiftLink can run while the KERNAL is replaced, i.e. under Fab04_GlobalKernalReplace, so it is guarded by that. Co-Authored-By: Claude Opus 5.5 --- .../Common/IO_Handlers/Swift_ATcommands.c | 11 +++++++++-- Source/Teensy/Teensy.ino | 13 +++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c index 40b06ddfe..e35f6be66 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/Swift_ATcommands.c @@ -410,7 +410,11 @@ FLASHMEM ATRespCode AT_Hook(char* CmdArg) } -FLASHMEM ATRespCode ProcessATCommandInner() +#ifdef Fab04_GlobalKernalReplace +FLASHMEM ATRespCode ProcessATCommandInner() //wrapped by ProcessATCommand() below +#else +FLASHMEM ATRespCode ProcessATCommand() +#endif { char* CmdMsg = TxMsg; //local pointer for manipulation stcATCommand ATCommands[] = @@ -467,11 +471,14 @@ FLASHMEM ATRespCode ProcessATCommandInner() return ATRC_ERROR; } +#ifdef Fab04_GlobalKernalReplace extern volatile bool FnetLoopPoll; FLASHMEM ATRespCode ProcessATCommand() -{ //connect/DHCP/DNS block in here waiting for the stack, which then needs its PIT poll timer (masked while Fab04_GlobalKernalReplace serves the KERNAL, see FnetLoopPollService) +{ //SwiftLink can run while the KERNAL is replaced: connect/DHCP/DNS block in here waiting for the stack, which then needs + //its PIT poll timer (masked while the poll runs from loop(), see FnetLoopPollService in Teensy.ino) if (FnetLoopPoll) NVIC_ENABLE_IRQ(IRQ_PIT); ATRespCode r = ProcessATCommandInner(); if (FnetLoopPoll) NVIC_DISABLE_IRQ(IRQ_PIT); return r; } +#endif diff --git a/Source/Teensy/Teensy.ino b/Source/Teensy/Teensy.ino index 13d6d4c96..8c250d300 100644 --- a/Source/Teensy/Teensy.ino +++ b/Source/Teensy/Teensy.ino @@ -55,6 +55,7 @@ uint8_t RAM_Image[RAM_ImageSize]; //Main RAM1 file storage buffer volatile uint8_t BtnPressed = false; volatile uint8_t EmulateVicCycles = false; uint8_t CurrentIOHandler = IOH_None; +#ifdef Fab04_KernalReplace volatile bool FnetLoopPoll = false; //Ethernet stack polled from loop(), PIT timer masked (see FnetLoopPollService) volatile bool FnetPollHold = false; //set by Kernal Replace PreStart, before CurrentIOHandler changes; cleared with the main menu void FnetLoopPollService(); @@ -63,6 +64,7 @@ FLASHMEM void FnetLoopPollStart() FnetPollHold = true; if (fnet_netif_get_default() != 0 && !FnetLoopPoll) { NVIC_DISABLE_IRQ(IRQ_PIT); FnetLoopPoll = true; } } +#endif StructMenuItem *DriveDirMenu = NULL; uint16_t NumDrvDirMenuItems = 0; char DriveDirPath[MaxPathLength]; @@ -385,7 +387,9 @@ void loop() if (IO1[rwRegPwrUpDefaults2] & rpud2TRContEnabled) if (USBHostSerial.available()) ServiceSerial(&USBHostSerial); +#ifdef Fab04_KernalReplace FnetLoopPollService(); +#endif #ifdef FeatTCPListen if (NetListenEnable) @@ -409,18 +413,16 @@ void loop() if (IOHandler[CurrentIOHandler]->PollingHndlr != NULL) IOHandler[CurrentIOHandler]->PollingHndlr(); } +#ifdef Fab04_KernalReplace FLASHMEM void FnetLoopPollService() { //While Kernal Replace serves the KERNAL, run the Ethernet stack poll from here instead of NativeEthernet's 1ms PIT timer ISR: //with the poll in the PIT interrupt, ~39 phi2 interrupts/s were late (>1.1us) even with the GPIO interrupt at a higher priority, //and a late one on the HIRAM sense after a $01 write corrupts the read; called from loop() they are down to about one every 2s. //(The PIT is only used by that timer, except for ASID, which masks it itself.) static uint32_t lastPollUs = 0; - bool want = FnetPollHold + bool want = FnetPollHold || (CurrentIOHandler == IOH_KernalReplace) #ifdef Fab04_GlobalKernalReplace || (fKernRepl != NULL) -#endif -#ifdef Fab04_KernalReplace - || (CurrentIOHandler == IOH_KernalReplace) #endif ; want = want && (fnet_netif_get_default() != 0); //stack initialized @@ -435,6 +437,7 @@ FLASHMEM void FnetLoopPollService() fnet_poll(); } } +#endif FLASHMEM void SetUpMainMenuROM() { @@ -442,7 +445,9 @@ FLASHMEM void SetUpMainMenuROM() #ifdef Fab04_GlobalKernalReplace fKernRepl = NULL; #endif +#ifdef Fab04_KernalReplace FnetPollHold = false; +#endif SetDMADeassert; SetIRQDeassert; SetNMIDeassert;