From 20572078bae9bee16babf11f938ad7a156e5185a Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Wed, 9 Sep 2026 11:46:59 -0500 Subject: [PATCH 1/4] Fix DMA write timing on NTSC; add bit-pattern DMA self-test DataPortWriteWaitDMA() released the data bus a hardcoded 430nS for both video standards. Split the setup/hold constants PAL/NTSC, make them tunable like every other timing constant, and drop the NTSC hold to 410. Measured on a flat NTSC C128 + TR+ (9/9/26), two distinct failure modes: - Deterministic, reproducible on demand: at 455+ the wait runs past Phi2 falling, so the port/buffer teardown and the next DMAByte() lose cycle alignment. Whole- byte corruption (the readback keeps the page's previous contents), collapsing completely by 460 - even the same-value control fails. - Intermittent, varying over hours: partial-byte errors at 430-450 (30 over ~1.5MB) while 360-425 stayed clean (~2.9MB). Pooled over every run: <=425 gave 2 bad bytes in 8.5MB (2.3e-7/byte), 430-450 gave 31 in 5.3MB (5.9e-6/byte), 455+ gave 1326 in 164KB (8.1e-3/byte). That 25x separation is pooled across time windows, not measured under matched conditions, and the intermittent mode went quiescent partway through: a same-conditions interleaved A/B of 410 vs 430, 1.28MB per arm, found zero on both, with 460 still collapsing as a positive control. 410 is not error-free either (2 bad in 4.5MB). So it is chosen on the pooled rate plus margin - it sits 45nS from the deterministic cliff where 430 sits 25nS from it - and not on a matched-conditions measurement, which this rig would not yield once the fault went quiet. PAL stays at 430. Its collapse point was located at 482 on a C64 Ultimate in PAL mode (9/10/26), exactly where the model predicts - both machines fail once the bus is released less than ~90-95nS before the next Phi2 falling edge, on different silicon and different video standards, which is independent confirmation of the mechanism rather than a fit to one rig. So 430 has 52nS of mechanical margin, more than NTSC's 410 has. But that rig is an FPGA with its own bus buffers and never showed the partial-byte mode at any value, which is the mode that actually drove the NTSC change - so PAL's analog margin remains deliberately unverified. Note the constants are measured from StartCycCnt, which DMAByte() re-latches nS_DMASetup after Phi2 *falling*, not from Phi2 rising; the swept windows are only valid at that constant's default. Changes: - Common_Defs.h: Def_nS_DMADataSetup/Hold PAL+NTSC and their runtime variables, following the existing nS_DMASetup pattern. - DMAControl.ino: use the variables instead of bare literals. The nS->cycles conversion is hoisted out of the spin - F_CPU_ACTUAL is volatile, so WaitUntil_nS() re-runs a load/multiply/divide every pass, and leaving that in would coarsen the very wait being tuned (DataPortWriteWait() already hoists it). This does make every DMA data wait end slightly earlier than before. Measured from the generated code: the old spin was 9 instructions with 2 volatile loads and a reciprocal-multiply divide, the new one is 4 with a single load, so at 600MHz the exit granularity drops from ~17-23nS to ~8-10nS. A nominal hold therefore overshoots by ~4nS instead of ~10nS. The NTSC values here were swept under the new behaviour; PAL's were not, so PAL keeps its nominal value but holds ~5nS shorter on average than shipped builds. That is negligible against its 52nS of margin to the collapse point (see Common_Defs.h) but is unmeasured against the partial-byte mode. - IOH_TeensyROM.c: carry both new constants through the PAL/NTSC discovery. - SerUSBIO.ino: tw###/ty### to tune them live and 'z' to run the pattern test, both under the existing debug gates. 'z' echoes its pass count, since GetDigits needs all three digits and silently falls back to 64 when given fewer. - StatusFunctions.c: TestDMAPattern() writes ValA/ValB alternating byte-by-byte over a controlled prior value, and reports the whole error distribution - bad bytes per page, XOR mask, per-bit direction, and how many bytes kept their previous contents. Uniform fills (ValA==ValB) cannot see this bug at all: a page already holding the value verifies clean whether or not the write landed, which is why TestDMAPage() never caught it. Alternating values additionally make the bus swing between consecutive DMA cycles, and that is the only pattern that trips at all near the chosen default. The pre-fill is read back rather than trusted, since it is subject to the same faults, and its miscompares count as a failure. Wired into ExpPortDMA() with a same-value control. --- Source/Teensy/DMAControl.ino | 8 +- .../Teensy/MinimalBoot/Common/Common_Defs.h | 11 +++ .../Common/IO_Handlers/IOH_TeensyROM.c | 16 +++- .../Common/IO_Handlers/StatusFunctions.c | 92 +++++++++++++++++++ Source/Teensy/SerUSBIO.ino | 30 ++++++ 5 files changed, 152 insertions(+), 5 deletions(-) diff --git a/Source/Teensy/DMAControl.ino b/Source/Teensy/DMAControl.ino index 6966aebd..c0e320bb 100644 --- a/Source/Teensy/DMAControl.ino +++ b/Source/Teensy/DMAControl.ino @@ -10,8 +10,9 @@ uint8_t *DMA_Buffer; //These assume Fab04_DataBufAlwaysEnabled __attribute__((always_inline)) inline uint8_t DataPortWaitReadDMA() { // for "normal" (non-VIC) C64 write cycles - WaitUntil_nS(390); // nS_DataSetup=220 //takes a little longer for read data DMA - //too soon = bad reads + //takes a little longer than the nS_DataSetup=220 non-DMA case, too soon = bad reads + uint32_t Cyc_DMADataSetup = nSToCyc(nS_DMADataSetup); //convert outside the spin, nSToCyc() on a variable is a divide per pass + while((ARM_DWT_CYCCNT-StartCycCnt) < Cyc_DMADataSetup); uint32_t DataIn = ReadGPIO7; return ((DataIn & 0x0F) | ((DataIn >> 12) & 0xF0)); } @@ -25,7 +26,8 @@ __attribute__((always_inline)) inline void DataPortWriteWaitDMA(uint8_t Data) CORE_PIN10_PORTSET = RegBits; CORE_PIN10_PORTCLEAR = ~RegBits & GP7_DataMask; - WaitUntil_nS(430); // nS_DataHold = 390 (err), 470 OK, 430 OK(?) + uint32_t Cyc_DMADataHold = nSToCyc(nS_DMADataHold); //convert outside the spin, see DataPortWaitReadDMA + while((ARM_DWT_CYCCNT-StartCycCnt) < Cyc_DMADataHold); //not checking Phi2 state due to tight timing and early in cycle call can cause early exit SetDataPortDirIn; //set data ports back to inputs/default diff --git a/Source/Teensy/MinimalBoot/Common/Common_Defs.h b/Source/Teensy/MinimalBoot/Common/Common_Defs.h index 0f6fc817..6ba3db0c 100644 --- a/Source/Teensy/MinimalBoot/Common/Common_Defs.h +++ b/Source/Teensy/MinimalBoot/Common/Common_Defs.h @@ -346,6 +346,15 @@ const uint8_t OutputPins[] = { #define Def_nS_DMASetupPAL 440 //400 delay from Phi2 falling to RW/Addr setup (just before rising edge) #define Def_nS_DMASetupNTSC 430 //380 too early will mess up VIC cycle (screen noise), too late will not set up R/W & addr lines fast enough (Write error) // 5/18/26: 440 not working for Rat NTSC for remote mem, reduced to 430 + +//Both are measured from StartCycCnt, which DMAByte() re-latches nS_DMASetup after Phi2 falling, not from Phi2 rising +#define Def_nS_DMADataSetupPAL 390 //delay to latching the data bus on a DMA read, too soon = bad reads +#define Def_nS_DMADataSetupNTSC 390 // 9/9/26: swept 310-450 on a flat NTSC C128 + TR+, clean at 350+, left at 390 +#define Def_nS_DMADataHoldPAL 430 //delay to releasing the data bus on a DMA write. 390 (err), 470 OK, 430 OK(?) +#define Def_nS_DMADataHoldNTSC 410 // 9/9/26: same rig, 455+ overruns Phi2 falling and collapses, 430-450 gives + // intermittent partial-byte errors, <=425 clean. PAL's own collapse is at 482 + // (9/10/26, C64 Ultimate in PAL mode) so 430 clears it, but that rig is an FPGA + // and can't show the partial-byte mode - PAL's analog margin stays unverified. //Other critical Timing #define Def_Cyc_KernProp 35 // Propagation delay for Kernal replace to sample ROMH to determine if HIRAM is asserted //C64 long bd/PAL: 10 fails (occasional misdetect of ram on rom cycle) 11 passes @@ -361,6 +370,8 @@ uint32_t nS_VICStart = Def_nS_VICStart; uint32_t nS_VICDHold = Def_nS_VICDHold; uint32_t nS_DMAAssert = Def_nS_DMAAssert; uint32_t nS_DMASetup = Def_nS_DMASetupPAL; //default to PAL, updated on main menu load (wRegVid_TOD_Clks write) +uint32_t nS_DMADataSetup = Def_nS_DMADataSetupPAL; +uint32_t nS_DMADataHold = Def_nS_DMADataHoldPAL; uint32_t Cyc_KernProp = Def_Cyc_KernProp; __attribute__((always_inline)) inline void DataPortWriteWait(uint8_t Data) diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TeensyROM.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TeensyROM.c index d2ce9de1..07d7c291 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TeensyROM.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_TeensyROM.c @@ -602,8 +602,20 @@ void IO1Hndlr_TeensyROM(uint8_t Address, bool R_Wn) case wRegVid_TOD_Clks: IO1[wRegVid_TOD_Clks]=Data; //make NTSC/PAL specific timing tweaks upon discovery - if (Data & 1) { nS_DMASetup = Def_nS_DMASetupNTSC; nS_MaxAdj = Def_nS_MaxAdjNTSC; } - else { nS_DMASetup = Def_nS_DMASetupPAL; nS_MaxAdj = Def_nS_MaxAdjPAL; } + if (Data & 1) + { + nS_DMASetup = Def_nS_DMASetupNTSC; + nS_MaxAdj = Def_nS_MaxAdjNTSC; + nS_DMADataSetup = Def_nS_DMADataSetupNTSC; + nS_DMADataHold = Def_nS_DMADataHoldNTSC; + } + else + { + nS_DMASetup = Def_nS_DMASetupPAL; + nS_MaxAdj = Def_nS_MaxAdjPAL; + nS_DMADataSetup = Def_nS_DMADataSetupPAL; + nS_DMADataHold = Def_nS_DMADataHoldPAL; + } break; case rwRegPageNumber: IO1[rwRegPageNumber]=Data; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c index d116e9da..81c46678 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c @@ -732,6 +732,83 @@ FLASHMEM bool TestDMAPage(uint16_t Address, uint8_t BytePat) //SendMsgPrintf(" OK"); return true; } + +FLASHMEM bool TestDMAPattern(uint16_t Address, uint8_t PriorVal, uint8_t ValA, uint8_t ValB, uint16_t Passes) +{ + //Alternating ValA/ValB swings the data bus between DMA cycles, which a uniform fill never does. + // Pre-filling with PriorVal is what makes a dropped write visible - TestDMAPage() writing $ff + // over a page already holding $ff verifies clean either way. PriorVal==ValA==ValB is a control. + uint8_t PageBuf[TestPageSize], PriorBuf[TestPageSize]; + uint32_t BadBytes = 0, WorstPass = 0, Unchanged = 0, PrefillBad = 0; + uint32_t BitFell[8] = {0}, BitRose[8] = {0}; + uint8_t XorMask = 0; + + for(uint16_t Pass = 0; Pass < Passes; Pass++) + { + memset(PageBuf, PriorVal, TestPageSize); + PerformDMA(false, Address, PageBuf, TestPageSize, false); + CloseDMA(); + PerformDMA(true, Address, PriorBuf, TestPageSize, false); //what the page really holds now + CloseDMA(); + for(uint16_t ByteNum = 0; ByteNum < TestPageSize; ByteNum++) + if(PriorBuf[ByteNum] != PriorVal) PrefillBad++; + + for(uint16_t ByteNum = 0; ByteNum < TestPageSize; ByteNum++) + PageBuf[ByteNum] = (ByteNum & 1) ? ValB : ValA; + PerformDMA(false, Address, PageBuf, TestPageSize, false); + CloseDMA(); + PerformDMA(true, Address, PageBuf, TestPageSize, false); + CloseDMA(); + + uint32_t PassBad = 0; + for(uint16_t ByteNum = 0; ByteNum < TestPageSize; ByteNum++) + { + uint8_t Expected = (ByteNum & 1) ? ValB : ValA; + uint8_t Diff = PageBuf[ByteNum] ^ Expected; + if(Diff == 0) continue; + PassBad++; + XorMask |= Diff; + if(PageBuf[ByteNum] == PriorBuf[ByteNum]) Unchanged++; //write never landed, vs a partial byte that landed mid-settle + for(uint8_t Bit = 0; Bit < 8; Bit++) + { + if(!(Diff & (1< WorstPass) WorstPass = PassBad; + } + + uint32_t TotalBytes = (uint32_t)Passes * TestPageSize; + uint32_t Fell = 0, Rose = 0; + for(uint8_t Bit = 0; Bit < 8; Bit++) { Fell += BitFell[Bit]; Rose += BitRose[Bit]; } + + if(ValA == ValB) Serial.printf("$%02x", ValA); + else Serial.printf("$%02x/$%02x alt", ValA, ValB); + Serial.printf(" over $%02x @ $%04x: %lu bad of %lu bytes", PriorVal, Address, BadBytes, TotalBytes); + if(PrefillBad) Serial.printf(", pre-fill %lu bad", PrefillBad); + if(BadBytes == 0 && PrefillBad == 0) + { + Serial.printf(" CLEAN\n"); + return true; + } + Serial.printf("\n"); + if(BadBytes) + { + Serial.printf(" worst page: %lu, unchanged: %lu, xor mask $%02x, 1->0: %lu, 0->1: %lu\n", + WorstPass, Unchanged, XorMask, Fell, Rose); + //Not a drive-strength measure - a uniform pattern forces every failing bit into one column + Serial.printf(" bit: "); + for(int8_t Bit = 7; Bit >= 0; Bit--) Serial.printf("%8d", Bit); + Serial.printf("\n 1->0: "); + for(int8_t Bit = 7; Bit >= 0; Bit--) Serial.printf("%8lu", BitFell[Bit]); + Serial.printf("\n 0->1: "); + for(int8_t Bit = 7; Bit >= 0; Bit--) Serial.printf("%8lu", BitRose[Bit]); + Serial.printf("\n"); + } + return false; +} #endif FLASHMEM void ExpPortDMA() @@ -862,6 +939,21 @@ FLASHMEM void ExpPortDMA() SendMsgPrintf(" OK"); +//DMA bit transitions + SendMsgPrintfln("DMA Bit Transition Tests"); + //Unlike the uniform fills above, these set the prior page contents so the data lines actually move + if (!TestDMAPattern(0xc000, 0xff, 0xff, 0xff, 16) || //control: nothing has to change + !TestDMAPattern(0xc000, 0x00, 0xff, 0xff, 64) || //uniform, over the opposite value + !TestDMAPattern(0xc000, 0xff, 0x00, 0x00, 64) || + !TestDMAPattern(0xc000, 0x00, 0x00, 0xff, 64) || //alternating: bus swings each cycle + !TestDMAPattern(0xc000, 0xff, 0x55, 0xaa, 64)) + { + SendMsgPrintfln(" Failed, details on serial"); + return; + } + SendMsgPrintf(" OK"); + + //IRQ SendMsgPrintfln("IRQ Test"); IO1[wRegIRQNMITest] = 0; diff --git a/Source/Teensy/SerUSBIO.ino b/Source/Teensy/SerUSBIO.ino index 09571bf4..673f2ea1 100644 --- a/Source/Teensy/SerUSBIO.ino +++ b/Source/Teensy/SerUSBIO.ino @@ -179,6 +179,26 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) CloseDMA(); } break; + case 'z': //DMA bit-transition test, for sweeping the DMA data timing constants + { + const uint16_t Addr = 0xc000; + uint32_t Passes = 0; + GetDigits(3, &Passes); //z### : 256 byte pages written per pattern + if(Passes == 0) Passes = 64; + NVIC_DISABLE_IRQ(IRQ_ENET); //keep the bus quiet while testing, as ExpPortDMA does + NVIC_DISABLE_IRQ(IRQ_PIT); + Serial.printf("\nDMA pattern test: DMADataHold=%lu DMADataSetup=%lu DMASetup=%lu Passes=%lu" + "\n (overwrites C64 $%04x-$%04x)\n", + nS_DMADataHold, nS_DMADataSetup, nS_DMASetup, Passes, Addr, Addr+255); + TestDMAPattern(Addr, 0xff, 0xff, 0xff, Passes); //control: nothing has to change + TestDMAPattern(Addr, 0x00, 0xff, 0xff, Passes); //uniform, over the opposite value + TestDMAPattern(Addr, 0xff, 0x00, 0x00, Passes); + TestDMAPattern(Addr, 0x00, 0x00, 0xff, Passes); //alternating: bus swings each cycle + TestDMAPattern(Addr, 0xff, 0x55, 0xaa, Passes); + NVIC_ENABLE_IRQ(IRQ_PIT); + NVIC_ENABLE_IRQ(IRQ_ENET); + } + break; #ifdef USE_PSRAM case 'y': //Load REU PSRAM from file { // example: y/reu/nuvies/speed.reu @@ -448,6 +468,12 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) case 'e': //nS_DMASetup change GetDigits(3, &nS_DMASetup); break; + case 'w': //nS_DMADataHold change + GetDigits(3, &nS_DMADataHold); + break; + case 'y': //nS_DMADataSetup change + GetDigits(3, &nS_DMADataSetup); + break; case 'k': //Cyc_KernProp change GetDigits(2, &Cyc_KernProp); break; @@ -461,6 +487,8 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) nS_RWnReady = Def_nS_RWnReady; nS_DMAAssert = Def_nS_DMAAssert; nS_DMASetup = Def_nS_DMASetupPAL; + nS_DMADataHold = Def_nS_DMADataHoldPAL; + nS_DMADataSetup = Def_nS_DMADataSetupPAL; Cyc_KernProp = Def_Cyc_KernProp; CmdChannel->printf("Defaults set\n"); break; @@ -478,6 +506,8 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) CmdChannel->printf("\t nS_VICDHold %03d (ti###)\n", nS_VICDHold); CmdChannel->printf("\t nS_DMAAssert %03d (ta###)\n", nS_DMAAssert); CmdChannel->printf("\t nS_DMASetup %03d (te###)\n", nS_DMASetup); + CmdChannel->printf("\t nS_DMADataHold %03d (tw###)\n", nS_DMADataHold); + CmdChannel->printf("\t nS_DMADataSetup %03d (ty###)\n", nS_DMADataSetup); CmdChannel->printf("\t Cyc_KernProp %02d (tk##)\n", Cyc_KernProp); CmdChannel->printf("\tSet Defaults (td)\n"); From 43ac1d4807db244edde4aeb340a0a1762449c127 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 10 Sep 2026 11:20:59 -0500 Subject: [PATCH 2/4] Record PAL DMA write-hold validation on real silicon The prior note said PAL's collapse point came from a C64 Ultimate and that PAL's partial-byte margin was unverified, because an FPGA has no real DRAM to be marginal. Re-ran the sweep on an original C64 with a VIC-II Kawari configured for PAL, TR+ installed. PAL detection was confirmed to actually fire rather than being read back from the identical power-on defaults: poisoning nS_MaxAdj to 900 and nS_DMASetup to 400, then resetting the C64, restored 1030/440, which only the PAL branch writes. Results, ~15.6MB of DMA writes total: - hold 430 baseline, 1.28MB, clean across all five bit patterns - collapse between 480 and 485, whole-byte, xor=$ff - the same boundary the Ultimate gave, so the mechanical model now holds on two PAL rigs and one NTSC rig, on different silicon and different video standards - swept 385-475 at 1.28MB each, all clean - no partial-byte band - interleaved A/B of 430 vs 410, 768000 bytes per arm, both 0 bad No constant changed. 430 sits mid-window with ~50nS of mechanical margin and there is no evidence for moving it. The clean 385-475 sweep is not a general acquittal of the partial-byte mode on PAL: this board also does not reproduce the 390 error recorded in the existing note, so it is a forgiving board. The comment now says the mode is untested on PAL rather than absent. --- Source/Teensy/MinimalBoot/Common/Common_Defs.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/Common_Defs.h b/Source/Teensy/MinimalBoot/Common/Common_Defs.h index 6ba3db0c..8d6b1b9b 100644 --- a/Source/Teensy/MinimalBoot/Common/Common_Defs.h +++ b/Source/Teensy/MinimalBoot/Common/Common_Defs.h @@ -352,9 +352,10 @@ const uint8_t OutputPins[] = { #define Def_nS_DMADataSetupNTSC 390 // 9/9/26: swept 310-450 on a flat NTSC C128 + TR+, clean at 350+, left at 390 #define Def_nS_DMADataHoldPAL 430 //delay to releasing the data bus on a DMA write. 390 (err), 470 OK, 430 OK(?) #define Def_nS_DMADataHoldNTSC 410 // 9/9/26: same rig, 455+ overruns Phi2 falling and collapses, 430-450 gives - // intermittent partial-byte errors, <=425 clean. PAL's own collapse is at 482 - // (9/10/26, C64 Ultimate in PAL mode) so 430 clears it, but that rig is an FPGA - // and can't show the partial-byte mode - PAL's analog margin stays unverified. + // intermittent partial-byte errors, <=425 clean. PAL collapses at 485 (9/10/26, + // C64+Kawari in PAL, Ultimate agrees) and swept 385-475 clean over 12MB, but that + // board won't repro the 390 err above - it's forgiving, so PAL's partial-byte + // mode is untested rather than absent. //Other critical Timing #define Def_Cyc_KernProp 35 // Propagation delay for Kernal replace to sample ROMH to determine if HIRAM is asserted //C64 long bd/PAL: 10 fails (occasional misdetect of ram on rom cycle) 11 passes From 70c5be4b18ed2f7f75e088105d4ad01ff40f4406 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 10 Sep 2026 11:36:13 -0500 Subject: [PATCH 3/4] Document the pre-detection timing window The initializers read as "PAL is the default" when what they actually mean is "this window runs before detection and something had to be picked". Two of the four carried a trailing note saying so; the hold and read constants did not. Records the part that is not obvious: the PAL bias is deliberate rather than arbitrary. NTSC's MaxAdj of 993 sits under PAL's 1015nS cycle, so initializing to the NTSC set would make a PAL machine re-adjust on every interrupt - the asymmetry only degrades safely in the direction it is already pointing. Also records why the 430 hold in that window is not a live defect on NTSC despite sitting in NTSC's bad band: nothing DMAs before detection. Every DMA caller is either a serial host command, ExpPortDMA (reached through a C64-side rCtlExpPortDMAWAIT write from the menu), or REU emulation, and the menu writes wRegVid_TOD_Clks in its first ~100 instructions. A C64 reset does not restore these, so the window is Teensy boot to first menu load only. No values changed. Lowering the hold initializer to a both-standards-safe 410 stays blocked on a PAL rig that can actually fail: 410 is clean on the Kawari board, but that board will not reproduce the 390 error in the note above, so it cannot certify the value. --- Source/Teensy/MinimalBoot/Common/Common_Defs.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/Common_Defs.h b/Source/Teensy/MinimalBoot/Common/Common_Defs.h index 8d6b1b9b..85c98443 100644 --- a/Source/Teensy/MinimalBoot/Common/Common_Defs.h +++ b/Source/Teensy/MinimalBoot/Common/Common_Defs.h @@ -362,7 +362,11 @@ const uint8_t OutputPins[] = { //C64c/PAL: 19 fails (occasional misdetect of ram on rom cycle) 20 passes //was set to 21, but testing on another C64c NTSC (short) was marginal after warmup. -uint32_t nS_MaxAdj = Def_nS_MaxAdjPAL; //default to PAL, updated on main menu load (wRegVid_TOD_Clks write) +//PAL-biased until the main menu's first wRegVid_TOD_Clks write. Deliberate: NTSC's MaxAdj of 993 is +// under PAL's 1015nS cycle, so a PAL machine would re-adjust every interrupt. The 430 hold is in +// NTSC's bad band for that window, but nothing DMAs before detection - the menu writes the register +// before ExpPortDMA or the REU are reachable, and a C64 reset leaves these already detected. +uint32_t nS_MaxAdj = Def_nS_MaxAdjPAL; uint32_t nS_RWnReady = Def_nS_RWnReady; uint32_t nS_PLAprop = Def_nS_PLAprop; uint32_t nS_DataSetup = Def_nS_DataSetup; @@ -370,7 +374,7 @@ uint32_t nS_DataHold = Def_nS_DataHold; uint32_t nS_VICStart = Def_nS_VICStart; uint32_t nS_VICDHold = Def_nS_VICDHold; uint32_t nS_DMAAssert = Def_nS_DMAAssert; -uint32_t nS_DMASetup = Def_nS_DMASetupPAL; //default to PAL, updated on main menu load (wRegVid_TOD_Clks write) +uint32_t nS_DMASetup = Def_nS_DMASetupPAL; uint32_t nS_DMADataSetup = Def_nS_DMADataSetupPAL; uint32_t nS_DMADataHold = Def_nS_DMADataHoldPAL; uint32_t Cyc_KernProp = Def_Cyc_KernProp; From 0fea31dd2cd6837f8be63828090f00334b44aa26 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 10 Sep 2026 12:17:13 -0500 Subject: [PATCH 4/4] Address code review of the DMA write-timing branch Common_Defs.h: the pre-discovery comment claimed nothing DMAs before the menu writes wRegVid_TOD_Clks. That is false. Autolaunch reaches RemoteLaunch() with DoCartDirect and starts a CRT without ever loading the menu, so an NTSC machine keeps the PAL 430 hold - inside NTSC's own 430-450 partial-byte band - while the REU handler and Write/ReadC64MemToken are both reachable. Corrected to describe the real exposure. Not fixing it by dropping the default to 410: that still needs a PAL board that reproduces the 390 (err), and the one PAL rig measured so far does not. TestDMAPattern(): the bit-level detail block was gated on BadBytes, so a run where only the pre-fill write dropped bytes printed a bare count. That is the max-transition case the test exists to catch ($00 over $ff), and the partial-byte vs whole-byte discrimination was lost exactly there. Pre-fill mismatches now carry their own xor mask and 1->0/0->1 totals. td: restored PAL constants unconditionally, so on an NTSC rig it silently re-armed the 430 hold mid-sweep with nothing to re-latch 410 until the C64 was reset. Now selects by the detected standard and prints which. z: forced PIT/ENET back on when it finished, overriding ExpPortDMA and the handlers that disable them deliberately. Saves and restores instead. --- .../Teensy/MinimalBoot/Common/Common_Defs.h | 13 ++++++++---- .../Common/IO_Handlers/StatusFunctions.c | 21 +++++++++++++++++-- Source/Teensy/SerUSBIO.ino | 21 ++++++++++++------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Source/Teensy/MinimalBoot/Common/Common_Defs.h b/Source/Teensy/MinimalBoot/Common/Common_Defs.h index 85c98443..e4d64d12 100644 --- a/Source/Teensy/MinimalBoot/Common/Common_Defs.h +++ b/Source/Teensy/MinimalBoot/Common/Common_Defs.h @@ -362,10 +362,15 @@ const uint8_t OutputPins[] = { //C64c/PAL: 19 fails (occasional misdetect of ram on rom cycle) 20 passes //was set to 21, but testing on another C64c NTSC (short) was marginal after warmup. -//PAL-biased until the main menu's first wRegVid_TOD_Clks write. Deliberate: NTSC's MaxAdj of 993 is -// under PAL's 1015nS cycle, so a PAL machine would re-adjust every interrupt. The 430 hold is in -// NTSC's bad band for that window, but nothing DMAs before detection - the menu writes the register -// before ExpPortDMA or the REU are reachable, and a C64 reset leaves these already detected. +//PAL-biased until the main menu's first wRegVid_TOD_Clks write. MaxAdj has to stay PAL here: NTSC's +// 993 is under PAL's 1015nS cycle, so a PAL machine would re-adjust on every interrupt. +//The hold is the problem. Autolaunch (Teensy.ino -> RemoteLaunch w/ DoCartDirect) starts a CRT without +// ever loading the C64 menu, so MainMenu.asm never writes the register and an NTSC machine keeps the +// PAL 430 - inside NTSC's own 430-450 partial-byte band. DMA is reachable there: the REU handler, +// and Write/ReadC64MemToken, which ProcessCommand() answers ahead of the busy check. +//Not fixed by dropping this to 410: that needs a PAL board that reproduces the 390 (err) below, and the +// one PAL rig measured so far does not. The real fix is timing Phi2 on the Teensy rather than +// waiting for the C64 to report it, which would retire this whole bootstrap. uint32_t nS_MaxAdj = Def_nS_MaxAdjPAL; uint32_t nS_RWnReady = Def_nS_RWnReady; uint32_t nS_PLAprop = Def_nS_PLAprop; diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c index 81c46678..144f7a1b 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/StatusFunctions.c @@ -741,7 +741,8 @@ FLASHMEM bool TestDMAPattern(uint16_t Address, uint8_t PriorVal, uint8_t ValA, u uint8_t PageBuf[TestPageSize], PriorBuf[TestPageSize]; uint32_t BadBytes = 0, WorstPass = 0, Unchanged = 0, PrefillBad = 0; uint32_t BitFell[8] = {0}, BitRose[8] = {0}; - uint8_t XorMask = 0; + uint32_t PrefillFell = 0, PrefillRose = 0; + uint8_t XorMask = 0, PrefillXor = 0; for(uint16_t Pass = 0; Pass < Passes; Pass++) { @@ -750,8 +751,21 @@ FLASHMEM bool TestDMAPattern(uint16_t Address, uint8_t PriorVal, uint8_t ValA, u CloseDMA(); PerformDMA(true, Address, PriorBuf, TestPageSize, false); //what the page really holds now CloseDMA(); + //the prefill is a full-swing write too - $00 over $ff and back - so it needs the same + // partial-byte vs whole-byte detail as the pattern write, not just a count for(uint16_t ByteNum = 0; ByteNum < TestPageSize; ByteNum++) - if(PriorBuf[ByteNum] != PriorVal) PrefillBad++; + { + uint8_t Diff = PriorBuf[ByteNum] ^ PriorVal; + if(Diff == 0) continue; + PrefillBad++; + PrefillXor |= Diff; + for(uint8_t Bit = 0; Bit < 8; Bit++) + { + if(!(Diff & (1<0: %lu, 0->1: %lu\n", + PrefillXor, PrefillFell, PrefillRose); if(BadBytes) { Serial.printf(" worst page: %lu, unchanged: %lu, xor mask $%02x, 1->0: %lu, 0->1: %lu\n", diff --git a/Source/Teensy/SerUSBIO.ino b/Source/Teensy/SerUSBIO.ino index 673f2ea1..b322365f 100644 --- a/Source/Teensy/SerUSBIO.ino +++ b/Source/Teensy/SerUSBIO.ino @@ -92,7 +92,7 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) // break; // *** The rest of these cases are used for debug/testing only - // u,v,w,y + // u,v,w,y,z #ifdef Dbg_SerDMA #ifdef Fab04_FullDMACapable case 'u': //Perform DMA Write @@ -185,6 +185,8 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) uint32_t Passes = 0; GetDigits(3, &Passes); //z### : 256 byte pages written per pattern if(Passes == 0) Passes = 64; + //restore rather than enable on exit - ExpPortDMA and some handlers leave these off on purpose + bool ENETWasOn = NVIC_IS_ENABLED(IRQ_ENET), PITWasOn = NVIC_IS_ENABLED(IRQ_PIT); NVIC_DISABLE_IRQ(IRQ_ENET); //keep the bus quiet while testing, as ExpPortDMA does NVIC_DISABLE_IRQ(IRQ_PIT); Serial.printf("\nDMA pattern test: DMADataHold=%lu DMADataSetup=%lu DMASetup=%lu Passes=%lu" @@ -195,8 +197,8 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) TestDMAPattern(Addr, 0xff, 0x00, 0x00, Passes); TestDMAPattern(Addr, 0x00, 0x00, 0xff, Passes); //alternating: bus swings each cycle TestDMAPattern(Addr, 0xff, 0x55, 0xaa, Passes); - NVIC_ENABLE_IRQ(IRQ_PIT); - NVIC_ENABLE_IRQ(IRQ_ENET); + if(PITWasOn) NVIC_ENABLE_IRQ(IRQ_PIT); + if(ENETWasOn) NVIC_ENABLE_IRQ(IRQ_ENET); } break; #ifdef USE_PSRAM @@ -478,7 +480,9 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) GetDigits(2, &Cyc_KernProp); break; case 'd': //Set Defaults - nS_MaxAdj = Def_nS_MaxAdjPAL; + { //match what detection would have set, or a sweep resumes from the wrong standard's values + bool IsNTSC = (IO1[wRegVid_TOD_Clks] & 1); + nS_MaxAdj = IsNTSC ? Def_nS_MaxAdjNTSC : Def_nS_MaxAdjPAL; nS_PLAprop = Def_nS_PLAprop; nS_DataSetup = Def_nS_DataSetup; nS_DataHold = Def_nS_DataHold; @@ -486,12 +490,13 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) nS_VICDHold = Def_nS_VICDHold; nS_RWnReady = Def_nS_RWnReady; nS_DMAAssert = Def_nS_DMAAssert; - nS_DMASetup = Def_nS_DMASetupPAL; - nS_DMADataHold = Def_nS_DMADataHoldPAL; - nS_DMADataSetup = Def_nS_DMADataSetupPAL; + nS_DMASetup = IsNTSC ? Def_nS_DMASetupNTSC : Def_nS_DMASetupPAL; + nS_DMADataHold = IsNTSC ? Def_nS_DMADataHoldNTSC : Def_nS_DMADataHoldPAL; + nS_DMADataSetup = IsNTSC ? Def_nS_DMADataSetupNTSC : Def_nS_DMADataSetupPAL; Cyc_KernProp = Def_Cyc_KernProp; - CmdChannel->printf("Defaults set\n"); + CmdChannel->printf("Defaults set (%s)\n", IsNTSC ? "NTSC" : "PAL"); break; + } default: CmdChannel->printf("No changes\n"); break;