Skip to content

Commit fa50942

Browse files
authored
firmware: Kernal Replace senses HIRAM by DMA on a C128 (#78)
On a C128 in C64 mode, Kernal Replace never serves the image: the Skoe sense drives A14 low over the CPU's address, but the expansion port's address lines come through a transceiver that drives towards the port outside DMA, so the PLA never sees it. ROMH never answers (measured at every Cyc_KernProp up to 99), every sense reads "RAM", and the one read after each $01 write is left undriven under /GAME. The KERNAL IRQ writes $01 right before its keyboard scan, so the keyboard stops working. With the menu reporting a C128 (rvtcC128), HIRAM is now sensed by DMA: right after a CPU write to $00/$01, /DMA holds the CPU for one cycle (the next cycle is always the opcode fetch, a read) and the TR reads $A000 as bus master with /GAME+/EXROM asserted; ROMH there means HIRAM is set. One CPU cycle per write to $00/$01, more when the VIC holds the bus. The common cycle stays at two tests ahead of the IO1/IO2 answer. A C64 keeps the Skoe sense. Serial tx# (Dbg_SerTimChg) picks the method (0 Skoe, 2 DMA, 9 auto) and tn### the ROMH sample point. KernalCheck now also returns before asserting /GAME when HIRAM is known to be RAM: Ultimax took those reads away from the RAM, which the DMA sense's RAM verdicts need, and which also corrupted RAM reads under the KERNAL on a C64 (Quest for the King). Review fixes: nS_KSenseSample's comment and a DMA-Phi2-Timing.md row give its time base (the nS_DMASetup re-latch), KDS_Skip names the late interrupt it relies on, and the sense-mode message's strings stay in flash.
1 parent 5e9d90a commit fa50942

4 files changed

Lines changed: 190 additions & 0 deletions

File tree

‎Source/Teensy/MinimalBoot/Common/Common_Defs.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ const uint8_t OutputPins[] = {
422422
#define Def_nS_DMADataHoldNTSC128 395
423423
//Other critical Timing
424424
#define Def_Cyc_KernProp 35 // Propagation delay for Kernal replace to sample ROMH to determine if HIRAM is asserted
425+
#define Def_nS_KSenseSample 250 // Kernal replace DMA HIRAM sense (C128): when ROMH is read, from the nS_DMASetup re-latch of the
426+
// DMA read (like nS_DMADataSetup), about 44nS before Phi2 rises on an NTSC C128.
427+
// 10/28/26 flat NTSC C128 (Teensy cycle counter, same time base): ROMH seen low at 98-136nS
428+
// over 180 senses, 98-116nS in 174. Must stay below nS_DMADataSetup.
425429
//C64 long bd/PAL: 10 fails (occasional misdetect of ram on rom cycle) 11 passes
426430
//C64c/PAL: 19 fails (occasional misdetect of ram on rom cycle) 20 passes
427431
//was set to 21, but testing on another C64c NTSC (short) was marginal after warmup.
@@ -445,6 +449,7 @@ uint32_t nS_DMASetup = Def_nS_DMASetupPAL;
445449
uint32_t nS_DMADataSetup = Def_nS_DMADataSetupPAL;
446450
uint32_t nS_DMADataHold = Def_nS_DMADataHoldPAL;
447451
uint32_t Cyc_KernProp = Def_Cyc_KernProp;
452+
uint32_t nS_KSenseSample = Def_nS_KSenseSample;
448453

449454
__attribute__((always_inline)) inline void DataPortWriteWait(uint8_t Data)
450455
{ // for "normal" (non-VIC) C64 read cycles only

‎Source/Teensy/MinimalBoot/Common/IO_Handlers/IOH_KernalReplace.c‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,176 @@ enum enumHIRAM_States
5858

5959
extern volatile uint8_t doReset;
6060

61+
//HIRAM sense: how KernalCheck finds out, after a write to $00/$01, whether the KERNAL is banked in.
62+
// (The 6510 doesn't put the value of a $00/$01 write on the external data bus, so it can't be tracked from the writes.)
63+
// Skoe: on the first $E000+ read, drive A14 low with /EXROM+/GAME asserted (16K mode) and look at ROMH.
64+
// This needs the PLA to see the address the TR drives over the CPU's. On a C128 in C64 mode it doesn't: the
65+
// expansion port's address lines come through a transceiver that drives towards the port outside DMA, so ROMH never
66+
// answers, every sense reads "RAM", and the replacement image is never served.
67+
// DMA: right after the write, hold the CPU with /DMA for one cycle and, as bus master, read $A000 with /EXROM+/GAME
68+
// asserted: the PLA selects ROMH there only with HIRAM set. The cycle after a store to $00/$01 is always the next
69+
// opcode fetch, a read, so the CPU halts on it. Costs the CPU one cycle per write to $00/$01 (more when the VIC
70+
// holds the bus). Used on a C128.
71+
enum enumHIRAMSenseModes
72+
{
73+
KSense_Skoe = 0,
74+
KSense_DMA = 2,
75+
KSense_Auto = 9, //DMA on a C128 (the menu's machine report), Skoe otherwise
76+
};
77+
uint32_t KSenseMode = KSense_Auto; //serial tx# (Dbg_SerTimChg)
78+
uint8_t KSenseEff = KSense_Skoe; //the method in effect
79+
80+
FLASHMEM void KSenseApplyMode()
81+
{
82+
#ifdef Fab04_FullDMACapable
83+
KSenseEff = (KSenseMode == KSense_Auto) ? ((IO1[wRegVid_TOD_Clks] & rvtcC128) ? KSense_DMA : KSense_Skoe) :
84+
(KSenseMode == KSense_DMA) ? KSense_DMA : KSense_Skoe;
85+
#else
86+
KSenseEff = KSense_Skoe;
87+
#endif
88+
if (KSenseEff == KSense_DMA) Serial.println(PSTR("Kernal HIRAM sense: DMA"));
89+
else Serial.println(PSTR("Kernal HIRAM sense: Skoe"));
90+
}
91+
92+
#ifdef Fab04_FullDMACapable
93+
enum enumKDS_States
94+
{
95+
KDS_Idle,
96+
KDS_Skip, //the sense cycle's rising-edge interrupt arrives late (in Phi2 low): drop it. It always arrives: the
97+
// Teensy core clears the GPIO interrupt flag before it calls isrPHI2, so the edge during the sense sets it again.
98+
KDS_WaitBus, //DMA asserted, BA was low: try the sense read on the next cycle
99+
KDS_AfterRead01, //a write to $00/$01 right after a read of $00/$01: the first of a read-modify-write's two
100+
// writes, or a store after an indexed dummy read. The next cycle tells which.
101+
};
102+
volatile uint8_t KDS_State = KDS_Idle;
103+
uint8_t KDS_Guess = HIRAM_State_ROM; //last verdict, for a KERNAL read no sense could precede
104+
uint32_t KDS_Read01Cyc = 0; //ARM_DWT_CYCCNT of the last CPU read of $00/$01
105+
bool KDS_Read01Valid = false;
106+
107+
//Phi2 low (StartCycCnt = falling), /DMA asserted: one DMA read of $A000 with /GAME+/EXROM, address timing as DMAByte's.
108+
//false: bus not available (badline, sprite fetch), try again on the next cycle
109+
FASTRUN bool KDS_SenseCycle()
110+
{
111+
WaitUntil_nS_fine(nS_DMABAWait);
112+
if (!GP9_BA(ReadGPIO9)) return false;
113+
114+
uint32_t RegAddrBits = (0xA000 << 16);
115+
CORE_PIN19_PORTSET = RegAddrBits; //set address port value to be ready for output drive
116+
CORE_PIN19_PORTCLEAR = ~RegAddrBits & GP6_AddrMask;
117+
WaitUntil_nS(nS_DMASetup); //just ahead of Phi2 rising
118+
StartCycCnt = ARM_DWT_CYCCNT;
119+
SetAddrBufsOut;
120+
SetAddrPortDirOut;
121+
SetExROMAssert;
122+
SetGameAssert;
123+
124+
WaitUntil_nS_fine(nS_KSenseSample);
125+
HIRAM_State = KDS_Guess = (GP9_ROMH(ReadGPIO9)==0 ? HIRAM_State_ROM : HIRAM_State_RAM);
126+
127+
WaitUntil_nS_fine(nS_DMADataSetup); //keep the address valid as long as a DMA read does
128+
SetGameDeassert;
129+
SetExROMDeassert;
130+
SetAddrPortDirIn;
131+
SetAddrBufsIn;
132+
return true;
133+
}
134+
135+
//Phi2 low, /DMA asserted, StartCycCnt = falling
136+
FASTRUN void KDS_Run()
137+
{
138+
if (!KDS_SenseCycle())
139+
{
140+
KDS_State = KDS_WaitBus;
141+
return;
142+
}
143+
while (GP6_Phi2(ReadGPIO6)); //Phi2 falling after the sense cycle
144+
SetDMADeassert; //release in Phi2 low, as isrPHI2's DMA_S_StartDisable does
145+
KDS_State = KDS_Skip;
146+
}
147+
148+
//Phi2 high of the CPU's write to $00/$01
149+
FASTRUN void KDS_Begin()
150+
{
151+
if (DMA_State != DMA_S_DisableReady)
152+
{ //another DMA user (REU, remote memory access) owns the line
153+
HIRAM_State = KDS_Guess;
154+
return;
155+
}
156+
while (GP6_Phi2(ReadGPIO6)); //Phi2 falling
157+
StartCycCnt = ARM_DWT_CYCCNT;
158+
WaitUntil_nS(nS_DMAAssert);
159+
SetDMAAssert;
160+
KDS_Run();
161+
}
162+
163+
//The DMA sense's work for a cycle off the common path: a sense pending, or a $00/$01 access.
164+
//true ends the cycle: nothing else may answer it
165+
FASTRUN bool KDS_Slow(uint16_t Address, bool R_Wn)
166+
{
167+
switch (KDS_State)
168+
{
169+
case KDS_Skip:
170+
KDS_State = KDS_Idle;
171+
return true;
172+
case KDS_WaitBus:
173+
while (!GP6_Phi2(ReadGPIO6)); //a late call (in Phi2 low) waits for the next cycle
174+
while (GP6_Phi2(ReadGPIO6));
175+
StartCycCnt = ARM_DWT_CYCCNT;
176+
KDS_Run();
177+
return true;
178+
}
179+
if ((Address & 0xfffe) == 0)
180+
{
181+
if (R_Wn)
182+
{
183+
KDS_Read01Cyc = ARM_DWT_CYCCNT;
184+
KDS_Read01Valid = true;
185+
}
186+
else
187+
{ //write to $00/$01
188+
HIRAM_State = HIRAM_State_Unknown;
189+
bool AfterRead = KDS_Read01Valid && (ARM_DWT_CYCCNT - KDS_Read01Cyc) < nSToCyc(1500); //the previous cycle
190+
KDS_Read01Valid = false;
191+
if (AfterRead && KDS_State != KDS_AfterRead01)
192+
{
193+
KDS_State = KDS_AfterRead01;
194+
return true;
195+
}
196+
KDS_State = KDS_Idle;
197+
KDS_Begin();
198+
return true;
199+
}
200+
}
201+
if (KDS_State == KDS_AfterRead01) KDS_State = KDS_Idle; //it was a store after a dummy read: this read is served on KDS_Guess
202+
return false;
203+
}
204+
#endif
205+
61206
//______________________________________________________________________________________________
62207

63208
FASTRUN bool KernalCheck(uint16_t Address, bool R_Wn)
64209
{
210+
#ifdef Fab04_FullDMACapable
211+
if (KSenseEff == KSense_DMA)
212+
{ //this runs ahead of every IO1/IO2 answer, so the common cycle costs only these tests: work added here answers IO late
213+
if ((KDS_State != KDS_Idle || (Address & 0xfffe) == 0) && KDS_Slow(Address, R_Wn)) return true;
214+
}
215+
else
216+
#endif
65217
//monitor port $0001 for changes
66218
if (!R_Wn && (Address & 0xfffe) == 0) HIRAM_State = HIRAM_State_Unknown; //write to addr 0 or 1 causes re-evaluation of HIRAM
67219

68220
if (!R_Wn || (Address & 0xe000) != 0xe000 || !GP9_BA(ReadGPIO9)) return false; //continue with cycle processing
69221

70222
// CPU read access from address $E000..$FFFF
223+
#ifdef Fab04_FullDMACapable
224+
if (HIRAM_State == HIRAM_State_Unknown && KSenseEff == KSense_DMA) HIRAM_State = KDS_Guess; //no sense could precede this read
225+
#endif
226+
#ifndef KERNAL_MONITOR_ONLY
227+
//HIRAM known to be RAM (no write to $00/$01 since it was sensed): the C64's own RAM answers, so leave /GAME and the
228+
//data bus alone. Asserting /GAME (Ultimax) here takes the read away from the RAM.
229+
if (HIRAM_State == HIRAM_State_RAM) return true;
230+
#endif
71231

72232
//prepare modified Address for Skoe HIRAM check (if needed)
73233
uint32_t RegAddrBits = ((Address & 0xbfff) << 16); //drive A14 low
@@ -206,6 +366,12 @@ FLASHMEM void InitHndlr_KERNALReplace_PreStart() //not called automatically via
206366
//Serial.printf("Read %lu Bytes in %lumS\n", CharNum, millis()-StartmS);
207367
Serial.printf("Read %lu Bytes\nKernal replace Enabled\n", CharNum);
208368
LoadFile.close();
369+
KSenseApplyMode();
370+
#ifdef Fab04_FullDMACapable
371+
KDS_State = KDS_Idle;
372+
KDS_Read01Valid = false;
373+
KDS_Guess = HIRAM_State_ROM;
374+
#endif
209375
delay(250);
210376

211377
//hook kernal checks: runs before BASIC init, well before any PRG-load handshake (see
@@ -229,6 +395,10 @@ FLASHMEM void InitHndlr_KernalReplace()
229395
//hooked there, KernalCheck reads an unloaded KernalBin and drives GAME/EXROM over the cart's own.
230396
if (fBusSnoop != &HandshakeSnoop || KernalBin == NULL) return;
231397
HIRAM_State = HIRAM_State_Unknown;
398+
#ifdef Fab04_FullDMACapable
399+
KDS_State = KDS_Idle; //KernalCheck is unhooked during the handshake: no sense can be in flight
400+
KDS_Read01Valid = false;
401+
#endif
232402
PendingfBusSnoop = &KernalCheck; //not fBusSnoop directly -- may still be mid PRG-load handshake, see HandshakeSnoop
233403
#endif
234404
}

‎Source/Teensy/SerUSBIO.ino‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,14 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel)
513513
case 'k': //Cyc_KernProp change
514514
GetDigits(2, &Cyc_KernProp);
515515
break;
516+
#ifdef Fab04_KernalReplace
517+
case 'x': //Kernal replace HIRAM sense: tx0 Skoe, tx2 DMA, tx9 auto (DMA on a C128)
518+
{ extern uint32_t KSenseMode; extern void KSenseApplyMode(); GetDigits(1, &KSenseMode); KSenseApplyMode(); }
519+
break;
520+
case 'n': //Kernal replace DMA HIRAM sense: ROMH sample point
521+
GetDigits(3, &nS_KSenseSample);
522+
break;
523+
#endif
516524
case 'd': //Set Defaults
517525
{ //match what detection would have set, or a sweep resumes from the wrong standard's values
518526
SetVideoStdTiming(); //MaxAdj and the DMA setup/hold set
@@ -525,6 +533,7 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel)
525533
nS_DMAAssert = Def_nS_DMAAssert;
526534
nS_DMABAWait = Def_nS_DMABAWait;
527535
Cyc_KernProp = Def_Cyc_KernProp;
536+
nS_KSenseSample = Def_nS_KSenseSample;
528537
CmdChannel->printf("Defaults set (%s %s)\n", (IO1[wRegVid_TOD_Clks] & rvtcNTSC) ? "NTSC" : "PAL",
529538
(IO1[wRegVid_TOD_Clks] & rvtcC128) ? "C128" : "C64");
530539
break;
@@ -547,6 +556,11 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel)
547556
CmdChannel->printf("\t nS_DMADataHold %03d (tw###)\n", nS_DMADataHold);
548557
CmdChannel->printf("\t nS_DMADataSetup %03d (ty###)\n", nS_DMADataSetup);
549558
CmdChannel->printf("\t Cyc_KernProp %02d (tk##)\n", Cyc_KernProp);
559+
#ifdef Fab04_KernalReplace
560+
{ extern uint32_t KSenseMode; extern uint8_t KSenseEff;
561+
CmdChannel->printf("\t KSenseMode %lu, in effect %u (tx#: 0 Skoe, 2 DMA, 9 auto)\n", KSenseMode, KSenseEff); }
562+
CmdChannel->printf("\t nS_KSenseSample %03lu (tn###)\n", nS_KSenseSample);
563+
#endif
550564

551565
CmdChannel->printf("\tSet Defaults (td)\n");
552566
CmdChannel->printf("\tList current vals (t)\n-----\n");

‎docs/Architecture/DMA-Phi2-Timing.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ All times in nS, from the `Def_nS_*` constants in [Common_Defs.h](../../Source/T
1616
| nS_DMASetup | Phi2 fall | 440 | 430 | 445 | Drive R/W and address, just before Phi2 rises |
1717
| nS_DMADataSetup | Re-latch at setup | 390 | 390 | 375 | Latch the data bus on a DMA read |
1818
| nS_DMADataHold | Re-latch at setup | 430 | 410 | 395 | Release the data bus on a DMA write |
19+
| nS_KSenseSample | Re-latch at setup | 250 | 250 | 250 | Kernal Replace HIRAM sense (C128): sample ROMH in the stolen cycle |
1920
| Phi2 period (nominal) | | 1015 | 978 | 978 | Reference |
2021

2122
Derived positions, from Phi2 fall in the same cycle:

0 commit comments

Comments
 (0)