# Why 38400 wedges > Folded into `testReport.md` as **section 10**, which carries the headline numbers; this file is the > full record. It was written against the build that still had the fault, so its addresses and its > present tense are that build's; `portCompatReport.md` and `testReport.md` section 11 measure the > build with the re-entrancy guard, in which the failure below does not occur. `highRateReport.md` characterised the 38400-baud wedge and said plainly that it had not been explained: four boots, four wedges, one ending in `*** Main CPU: JAM at $0007`, and no path traced from any measured byte to any of it. This document explains it, from measurements taken for that purpose. **The cause is NMI re-entrancy, and it is established, not inferred.** A machine was stopped by a checkpoint at the instant it entered `commNmiHandler` with `SP = $12`, and thirty-four complete nested handler frames were decoded off page 1. The descent from a healthy stack to an exhausted one was recorded three separate times, with the emulator's own cycle counter, and takes **8 to 13 milliseconds**. The arithmetic behind it was measured too: one pass of the handler costs 175 to 266 cycles, and at 38400 baud a character time is 266.3 cycles with **two** interrupts inside it. The handler cannot finish before the next one arrives, so it never finishes at all. Everything below is VICE 3.7.1 (`x64sc`), NTSC, `-acia1mode 1 -acia1base 0xDE00 -acia1irq 1`, on **2026-08-22**, against a disk built from the current source with `./build.sh` (`checkAbi.py` prints `ABI CHECK PASSED`). **No driver source was changed.** The only write either script makes to a machine is `baudIndex $E055` before the link is opened - 38400's hot key was removed from `baudEntryTable`, so the rate is selected by setting the index and letting the driver's own `loadBaudParameters $E353` program the control register. `$DE03` read `$1F` on every machine in every run. --- ## 1. The answer 1. The 6502 does not mask NMI. Entering an NMI sets `I`, which stops IRQs, and nothing at all stops another NMI. 2. `commNmiHandler $E685` reaches `jsr aciaGetStatus` at `$E68F` eight instructions in, and that routine's `lda $DE01` is the ninth. On a 6551 that read *is* the acknowledge: it clears the IRQ flag and releases `/IRQ`. 3. From that read to the end of the pass - up to about 220 more cycles - the chip is free to raise a fresh event. `/IRQ` falls again, the 6502 latches the edge, and the handler is re-entered at the very next instruction boundary. Each re-entry costs **6 bytes of stack** (the CPU's `PCH`/`PCL`/`P` plus the handler's `A`, `X` and saved `$01`), or **8** when the edge lands while the interrupted pass was inside one of its own `JSR`s. 4. At 38400 baud with traffic in both directions there are **two** interrupt-raising events per character time - one `RDRF`, one `TDRE` - so an edge arrives roughly every 133 cycles, while one pass of the handler costs 175 to 266. **Service time exceeds inter-arrival time.** The queue is not a queue: it is a stack, and it grows without bound. 5. About thirty frames is the whole of the 170-odd bytes of page 1 the game leaves free. Measured: **8.4 ms, 12.5 ms and 11.5 ms** in three independent bursts. 6. Once `SP` wraps past `$00` the frames overwrite one another. Every `RTI` from then on pops a `P` and a `PC` that belong to a different frame, and the `PLA` / `STA $01` at `$E6E3`/`$E6E4` writes a foreign byte into the CPU port register. The machine leaves the handler with a corrupted program counter, a corrupted flag byte, or a corrupted memory map - and that is the wedge. The driver's own comment at `commNmiHandler` names this race and prices it at "a checksum failure and one retransmit, which the ARQ layer above already deals with". That price is right for one re-entry. What the comment does not say is that above about 19200 baud it stops being one re-entry. --- ## 2. What one pass of the handler costs - measured `testCost.py` traces six addresses inside the handler - the entry `$E685`, `sta aciaRxByte $E699`, `dec uartPendingCount $E6C9`, `jsr aciaPutData $E6D6`, and both exits (`rti $E6E9`, `jmp (nmiChainVector) $E67F`) - and pairs every entry with its exit, so each pass can be priced *and* classified by what it actually did. It was run at **19200**, where the link is healthy and busy, and where nothing nests: the handler's cost is the same code at any rate. Four windows, two machines, 34.8 emulated seconds, 5694 complete passes: | what the pass did | passes | trace-to-trace, median | whole interrupt | |---|---:|---:|---:| | received a character | 2983 | 165 cycles | **178 cycles** | | transmitted a character | 1835 | 165 | **178** | | transmit interrupt found the ring dry, disarmed it | 677 | 162 | 175 | | **received and transmitted in the same pass** | 188 | 243-253 | **256-266** | | neither half had work (a DCD/DSR change) | 11 | 151 | 164 | "Whole interrupt" adds the 7 cycles the CPU spends taking the NMI and the 6 the `RTI` costs, neither of which lies between the two tracepoints. A hand count of the same paths off the source agrees to within a cycle, which is worth saying because it means the figure is a property of the code and not of the emulator. Now put that against the line. An NTSC C64 runs at 1,022,727 Hz and 8N1 is ten bits per character: | rate | characters/s | one character time | one pass | two passes | share of the CPU | |---|---:|---:|---:|---:|---| | 300 | 30 | 34090.9 cycles | 178 | 356 | 1% | | 1200 | 120 | 8522.7 | 178 | 356 | 4% | | 2400 | 240 | 4261.4 | 178 | 356 | 8% | | 4800 | 480 | 2130.7 | 178 | 356 | 17% | | 9600 | 960 | 1065.3 | 178 | 356 | 33% | | 19200 | 1920 | 532.7 | 178 | 356 | **67%** | | 38400 | 3840 | **266.3** | 178 | 356 | **134% - cannot keep up** | The "one pass" column is a single-half pass; a pass that services both halves costs 256-266. Two numbers in that table decide everything. * **A full-duplex pass costs 266 cycles and a character time at 38400 is 266.3 cycles.** One interrupt that services both halves consumes an entire character time on its own, leaving the game nothing. * When the receive and transmit events land in *separate* passes - which is the normal case, because they are separate events at separate instants - the cost is **356 cycles of work offered every 266 cycles**. That is the whole failure in one line. This is also the ladder every earlier report measured without knowing why: healthy at 4800 (17%), degrading at 9600 and 19200 (33% and 67%), gone at 38400. --- ## 3. The re-entrancy itself - measured three times `testStack.py` arms one tracepoint before the link is ever opened: ``` trace exec e685 if sp < $b4 ``` A tracepoint prints and lets the machine run on, so this does not disturb anything, and it prints **nothing at all** while nesting is shallow: the main line runs with `SP` between `$BD` and `$EB`, so an entry below `$B4` is already one frame deeper than the deepest healthy entry seen on any machine in any run. Every VICE trace line carries `SP` and the free-running cycle counter, so the output is a timestamped staircase. Three runaways were caught, on freely running machines, with no breakpoint involved: | run | machine | entries | duration | `SP` fell | frames | rate of descent | |---|---|---:|---:|---|---:|---:| | `st384d` | B | 44 | 8611 cycles = **8.42 ms** | `$B0` -> `$02` | 174 bytes = 29 | 20666 stack bytes/s | | `st384d` | B | 91 | 12807 cycles = **12.52 ms** | `$B3` -> `$08` | 171 bytes = 28.5 | 13656 bytes/s | | `st384c` | B | 84 | 11780 cycles = **11.52 ms** | `$B3` -> `$00` | 179 bytes = 30 | 15541 bytes/s | The first twenty-four entries `st384d`'s machine B recorded below `$B4`, as `(SP, cycle)`: ``` ($B3,153671517) ($B0,+264) ($B2,+152) ($AA,+115) ($AF,+147) ($A7,+117) ($A9,+152) ($A1,+115) ($A9,+153) ($A1,+115) ($A1,+148) ($99,+115) ($A1,+153) ($99,+115) ($99,+147) ($91,+117) ($99,+151) ($91,+115) ($93,+153) ($8B,+115) ($90,+147) ($88,+163) ($8A,+107) ($84,+113) ``` Read the gaps: they alternate **115 and about 150 cycles, and 115 + 150 = 265** - one character time at 38400, with two interrupts inside it. That is the "two events per character" claim measured directly rather than argued. Read the stack pointer: it saws up and down as inner passes complete, and every saw tooth is lower than the last. In `st384d` the in-burst gaps were min 55, 25th percentile 117, median 158, 75th percentile 265 cycles; in `st384c`, min 55, 25th percentile 60, median 171, max 269 - against a handler that needs 175 to 266. **Control.** The same script, the same instrumentation, 200 seconds at **19200** with two machines carrying 16729 and 16856 bytes - six times the traffic the 38400 runs managed - produced **no entry below `SP $B4` at all, on either machine**, and neither stack was ever pushed below `$01C9`. At 19200 the handler fits inside the character time, so it never nests. --- ## 4. The stack, decoded `st384b` carried a second checkpoint, `break exec e685 if sp < $20`, which stops the emulator once the stack is nearly gone. It fired. Machine A was stopped **at `$E685`, the first instruction of `commNmiHandler`, with `SP = $12`, `X = $14` and `I` set** - about to push three more bytes onto a stack with room for two more nested passes. This is the one machine whose stack can still be *walked*: it was stopped part-way down, before the pointer wrapped and the frames began overwriting one another. Its page 1, walked upward from `$0113` (the newest pushed byte) to `$01FF`, decodes without a gap or an ambiguity into **34 complete nested handler frames plus 16 `JSR` return addresses**: | what | value | count | |---|---|---:| | interrupted PC | `$E6C4` | 10 | | | `$E6C7` | 9 | | | `$E551` | 8 | | | `$E6C2` | 7 | | saved `X` | `$14` | 33 of 33 | | saved `$01` | `$35` | 33 of 33 | | saved `P` | `$25` / `$27` | 25 / 9 - `I` set in every one | | saved `A` | `$10` / `$09` / `$00` | 17 / 8 / 8 | | `JSR` return addresses under the frames | `$E40A`, `$E6E2` | 8 each | Every one of those addresses is inside the handler's **transmit half**, in the window between the status read at `$E68F` and the two writes that would remove the interrupt source: * `$E6C2` - `beq L_E6EA`, the `TDRE` test * `$E6C4` - `lda txCharActive` * `$E6C7` - `beq L_E6D1` * `$E551` - `sta aciaCommandShadow` inside `aciaSetCommand`, reached from `aciaSetCommandIdle $E408` at `$E6E0`; the two `JSR` return addresses `$E6E2` and `$E40A` sitting under those frames are that call chain The saved `$01` is `$35` in all 33 frames, which by itself proves nesting: `$35` is the value the handler *forces* at `$E68D`, so a frame that saved `$35` was interrupted by a handler that had already run. The saved `X` is `$14` = 20 in all 33: `pushUartRxRing` leaves `X` at `uartRxCount`, and the receive ring was pinned at its 20-byte limit because the game had stopped draining it. Three things fall out of that table that earlier reports could not explain. * **`$E6C4` and `$E6C7` straddle `dec uartPendingCount $E6C9`.** A pass interrupted at `$E6C7` has already read `txCharActive` and decided to count a byte off; the inner pass then does its own `dec`, clears `txCharActive`, pops the ring and writes the data register; when the outer pass resumes it decrements *again* for the same byte and writes the data register *again*. That is where `highRateReport.md` section 4's lost transmit characters come from - VICE's "data register written although data has not been sent yet", 0.8% at 4800 climbing to 2.2% at 19200, the same curve as the duty cycle in section 2 - and it is how `uartPendingCount` gets below zero to the `$FF` that section 6.1 found. *The double decrement is shown here as a frame stopped one instruction short of the `DEC`; a `DEC`/`INC` imbalance was not separately counted during a storm.* * **`$E551` explains boot 4's impossible command register.** `aciaSetCommand` writes the shadow at `$E551` and the chip at `$E554`. A machine thrown off the rails between those two instructions ends up exactly as `highRateReport.md` section 6.2 found it: `$DE02` = `$05`, transmit interrupt still armed, while `aciaCommandShadow` says `$09`. * **The two stacks that finished the job cannot be walked, and that is the point.** `st384c`'s and `st384d`'s machine B both finished with page 1 covered end to end in the same material - `$35`/`$14` pairs, `P` bytes of `$65`/`$67` with `I` set, and the return addresses `$E6B5` (`jsr reportLinkError`), `$E6E2` (`jsr aciaSetCommandIdle`), `$E40A` (`jsr aciaSetCommand`), `$E551`/`$E554`, `$E6B9`/`$E6BB` and `$E6E7`/`$E6E8`, the last two being `tax` and `pla` in the handler's own exit sequence. Those two stacks cannot be walked frame by frame the way `st384b`'s can, because the pointer had wrapped and the frames had overwritten each other - which is itself the point. --- ## 5. The aftermath, and why the four boots looked different Once `SP` has wrapped, an `RTI` pops three bytes that were written by three different interrupts. What the machine does next depends on which bytes. Five 38400 boots were run in this session and four machines died; with the one `highRateReport.md` caught, they are the *same* failure photographed at different moments: | machine | where it ended up | |---|---| | `st384b` A | caught mid-runaway: `PC $E685`, `SP $12`, 34 frames still on the stack | | `wg384a` A | running game code at `$44EB-$4502` with `I` **set**, `SP` frozen at `$BD` | | `st384d` B | `PC $C631`, `SP $67`, `I` set, page 1 covered in handler frames | | `st384c` B | `PC $A34E`, `SP $B2`, `I` **and `B`** set, and **`$01 = $31`** - `CHAREN` clear, so `$D000-$DFFF` is the character ROM and the ACIA is not in the address space at all | | `highRateReport.md` boot 4 | `JAM at $0007`, `$00`/`$01` = `$5E`/`$15` - the same corrupt `PC` and the same smashed CPU port, landing on an illegal opcode instead of on live code | `st384c`'s `$01 = $31` is the mechanism caught in the open: `$E6E3` is `pla` and `$E6E4` is `sta $01`, so a wrecked stack hands the memory-map register whatever byte happens to be there. Boot 4's `$01 = $15` is the same instruction with different rubbish. ### Why it never recovers `wg384a`'s machine A was measured for 154 seconds after it died, and three readings say why nothing comes back: * **`pollCarrierState $E3CB` - the `$E00C` entry the raster IRQ calls once a frame - executed ZERO times** in a 12.15-second traced window and **zero** again in an 8.19-second one. Its healthy peer executed it 479 times in 8.51 seconds, 56.3/s. Across the whole run the monitor read machine A's registers **124 times, and the interrupt flag was set in all 124, with `SP` at `$BD` in all 124**; the same 124 reads on its healthy peer found `I` set in only 15 - the ones that caught it inside its raster IRQ - and `SP` ranging over `$DF`-`$E5`. A 6502 with `I` set takes no IRQ, so there is no `$E000`/X=0 and no `$E00C`, and **nothing in the driver runs except the NMI**. * **`sta uartPendingCount $E611`, `startNextTxChar`'s once-a-frame repair of exactly the byte `highRateReport.md` section 6.1 found stuck at `$FF`, also executed zero times.** Its peer ran it 478 times in the same 8.51 seconds. The self-heal cannot heal anything, because the self-heal is on the frame tick and the frame tick is gone. * **`serviceLock $E0A7` read `$00`.** It rests at 1; `serviceCommTick` decrements it on entry and restores it at `$E149`. The runaway threw the CPU out of the middle of a service tick, so the lock is held for ever - a second latch that would refuse to run the driver even if the raster IRQ came back. Its remaining state, for the record: `connectionPhase $E040` = 2, `isLinkActive $E03B` = 1, `uartRxCount $E42D` = `$14` with both indices frozen at `$02`, `uartTxCount $E414` = 0, `uartPendingCount $E0A5` = `$00`, `txCharActive $E5BB` = `$00`, `aciaCommandShadow $E5BE` = `$09`, `$DE01`/`$DE02`/`$DE03` = `$10`/`$09`/`$1F`. The NMI itself was still working perfectly - 18 to 21 interrupts a second, 0.3% duty cycle, every one of them 164 to 207 cycles - and it had nobody left to deliver to. **That is why the 2400-baud repair probe could never have worked: by the time a wedged machine can be examined, the driver is fine and the machine above it is dead.** --- ## 6. What was ruled out | hypothesis | verdict | on what evidence | |---|---|---| | NMI re-entrancy | **ESTABLISHED** | 34 nested frames decoded off a live stack; three timed descents; the cost/character-time budget | | Steady-state CPU starvation by the NMI | **ruled out as the wedge** | after the wedge the NMI duty cycle is 0.3% and the game gets essentially the whole CPU - it just has `I` set. Overload is the *trigger*, not the resting state | | The 20-byte receive ring being too small | **not the cause** | it overflows at 9600 and 19200 too, and those recover. In the wedges the ring is full because the game stopped draining it - a consequence, and `X = $14` in every stack frame is that consequence in the act | | `uartPendingCount` reaching `$FF` (`spec_uart.md` hazard 1) | **a consequence, not the cause** | `wg384a`'s machine A wedged with `uartPendingCount` = `$00`. The `$FF` shape needs both the double decrement (section 4) and a dead frame tick (section 5) | | The control byte `$1F` itself, or the rate table | **ruled out** | `$DE03` read `$1F` on every machine in every run and characters crossed the wire correctly; `highRateReport.md` section 7 ran a machine at `$1F` against a 300-baud peer for 245 s with no wedge, no overflow and no bad character. The chip setting is innocent; the *traffic* is what kills | | A spurious interrupt storm from the chip (a VICE artefact) | **ruled out** | in normal operation the median gap between handler entries is 267-268 cycles, exactly one character time, and the interrupt rate tracks the character rate. The chip raises one interrupt per event, as it should | | A protocol deadlock above the driver | **ruled out** | the wedged machines' CPUs were thrown out of their own control flow with a corrupt `P`, `PC` or `$01`; a healthy peer's page 1 in the same run is untouched below `$01C9` | | The 2400-baud transmitter deadlock | **not this** | `txCharActive`/`aciaCommandShadow` were the self-consistent pair `$00`/`$09` on every wedged machine, as `highRateReport.md` section 6 already found | | Word length / parity / stop bits | **irrelevant here** | every run was 8N1 throughout. Nothing in this failure depends on the framing | **38400 does not always kill inside a minute.** Run `st384a` reached phase 3 on both machines and neither wedged in 200 seconds, on the same build, with the same instrumentation armed. The failure needs both directions bursting at once - `sendPacket $E909` queues a whole 20-odd byte frame into the 22-byte ring in one loop and the NMI shifts it out back to back - so it is a race that a quiet link can lose for a while. Five 38400 boots in this session, four machines dead. --- ## 7. What this does not show * **VICE, not hardware.** No physical SwiftLink and no C64 Ultimate. Two things carry to hardware regardless, because they are properties of the driver and of the 6502: the handler's cost in cycles (measured here, and confirmed by a hand count off the source) and the fact that reading the status register releases `/IRQ` while the handler still has 100 to 240 cycles of work left. What VICE cannot settle is the precise instant a real 6551 re-asserts `/IRQ`, so the *speed* of the collapse on a real cartridge may differ from 8-13 ms. The direction cannot: 356 cycles of work per 266 cycles of line time is arithmetic. * **VICE's ACIA is byte level**, so no framing or parity error can be produced on this rig. On a real line those errors add `reportLinkError $E3BC` -> `countLinkError $E73F` to the receive path, which makes the pass *longer*, not shorter. * **The double decrement of `uartPendingCount` is shown structurally, not counted.** Section 4 has a stack frame stopped one instruction before the `DEC`; it does not have a `DEC`/`INC` imbalance counted during a storm. Instrumenting `$E6C9` and `$E71E` unconditionally at 38400 prints thousands of lines a second, and the storm lasts eight milliseconds. * **Nothing was fixed and nothing was changed.** No driver source file was touched; `checkAbi.py` passes on the image every run above was made against. --- ## 8. What any fix has to satisfy (not implemented) Stated here only because the measurements pin the constraints, and because two of them rule out repairs that look obvious: 1. **The handler must be safe to re-enter, or must not be re-enterable.** Re-entry is not rare at high rates and it is not free: it costs 6 to 8 bytes of stack and it corrupts `uartPendingCount`, `txCharActive` and the byte in the transmitter. A re-entrancy counter alone is not enough - the source's own comment is right that naively refusing a nested pass can lose the interrupt that would have restarted the transmitter, and a lost `TDRE` stalls the transmitter for ever. 2. **Total interrupt work per character time must stay below one character time at the rate the user has configured.** At 38400 that means the two halves must be serviced by *one* interrupt of well under 266 cycles - i.e. the handler has to get materially cheaper, or the transmitter has to stop asking for an interrupt per character. 3. **Whatever is done must hold when the user's hardware is set to a rate the driver did not choose**, which is the point of the current work: the rate is not the driver's to pick. 4. **Eight data bits are mandatory** - the frame layer sends `$55` length codes, sequence bytes and rotate-and-add checksums, so it is not 7-bit clean. Parity and stop bits are free. Nothing in this diagnosis depends on framing either way. --- ## 9. Files | File | What it is | |---|---| | `testWedge.py` | pairs the handler's entry with its exits, counts re-entries, measures the duty cycle, counts the `$E00C` frame service, and profiles PC/SP with monitor pauses | | `testStack.py` | the deep-nesting tracepoint and the stack-page checkpoints; `traceonly` mode records the staircase without ever stopping the machine | | `testCost.py` | classifies and prices every pass of the handler, and prints the per-character-time budget | | `testLogs/wedge.wg384a.txt` | the 38400 boot whose machine A died with `I` set and its raster IRQ gone | | `testLogs/wedge.wg384a.A.nmi.trace.txt`, `.A.after.trace.txt`, `.B.after.trace.txt` | the traces behind the "`$E3CB` executed zero times" measurement | | `testLogs/stack.st384a.txt` | a 38400 boot in which neither machine wedged in 200 s | | `testLogs/stack.st384b.txt` | the boot stopped at `SP = $12`; the stack page decoded in section 4 | | `testLogs/stack.st384c.txt`, `stack.st384d.txt` | the trace-only boots; the three timed descents | | `testLogs/stack.st384c.B.deep.txt`, `stack.st384d.B.deep.txt` | the raw staircases, with `SP` and cycle counter per line | | `testLogs/stack.st192a.txt` | the 19200 control: heavier traffic, no nesting, no wedge | | `testLogs/cost.c192a.txt`, `testLogs/cost.c192a.[AB].[01].trace.txt` | the pass-cost measurement of section 2 | | `shots/wgwg384a*.png`, `shots/stst384*.png`, `shots/stst192a*.png` | both machines at the end of each run |