510 lines
31 KiB
Markdown
510 lines
31 KiB
Markdown
# The probe hardening and the 38400 retirement, verified
|
|
|
|
> **Superseded in part.** This report records the build that retired 38400 by blanking its hot-key
|
|
> code, and quotes that build's addresses and its "461 of 4096, in 32 region(s)" figure. The
|
|
> retirement was later reversed: `wedgeDiagnosis.md` found the real cause (NMI re-entrancy
|
|
> exhausting the stack) and `portCompatReport.md` measured 38400 as healthy once it was fixed, so
|
|
> the hot key is back. The probe hardening described below is still current. See
|
|
> `ADDRESS_CHANGES.md` for the addresses that have moved since.
|
|
|
|
|
|
> **Note: the 38400 retirement has been reversed.** Blanking that entry's hot-key code hid a defect
|
|
> rather than fixing one. `wedgeDiagnosis.md` established the defect - unbounded re-entrancy in
|
|
> `commNmiHandler`, not the rate - and with the re-entrancy guard in place C= + 8 selects 38400 again
|
|
> (`portCompatReport.md` sections 3 and 4, `testReport.md` section 11; `testAdopt.py` measured the key
|
|
> putting `baudIndex` = 18 and the control register = `$1F`). The page-latch half of this report
|
|
> stands, and section 5's "C= + 8 does nothing" is the *before* for that later measurement. Nothing
|
|
> below has been rewritten: it is the record of the build of the day.
|
|
|
|
Two changes went into the driver since `highRateReport.md` was written. The first had been looked
|
|
at once - a re-run of `testStrap.py` on the `$DF00` strap, covering two link opens and no hot keys
|
|
at all, quoted in `README.md` section 5 - and the second had not been checked at all:
|
|
|
|
* **the page latch.** `aciaDetect` `$E644` used to probe `$DE00` on every call, and
|
|
`loadBaudParameters` calls it on every link open **and** every baud hot key - so the old build
|
|
wrote its probe pattern into `$DE03` once per open and once per rate change, on a live link, on a
|
|
page that is very often some other cartridge's registers. `aciaPageLatch` `$E5BF` now records the
|
|
answer, `aciaDetect` returns in five bytes while it is non-zero, and only
|
|
`selectFramedLinkVectors` `$E3F3` - which `initCommModule` calls once, right after the overlay
|
|
loads - puts it back to zero. The probe also became a two-pattern probe, because an empty `$DE00`
|
|
reads back open bus.
|
|
* **the 38400 retirement.** `baudEntryTable`'s last entry keeps its control byte `$1F`, but its
|
|
hot-key code is now `$00`, which the search at `$E344` can never match because that search only
|
|
runs on key codes with bit 7 set. 38400 wedged four boots out of four (`highRateReport.md`
|
|
sections 5 and 6).
|
|
|
|
Everything below is VICE 3.7.1 (`x64sc`), NTSC, `-acia1mode 1 -acia1irq 1`, base `$DE00` or `$DF00`,
|
|
on **2026-08-22**, against `../Modem_Wars_SwiftLink.d64` built from the current source with
|
|
`./build.sh`. Every emulator pair runs at **true C64 speed**: warp is used for the loader only and
|
|
switched off through the monitor's own `warp` command before the options menu. As in every earlier
|
|
session each run picked its own X display, its own TCP ports and its own private VICE config file,
|
|
and only processes these scripts started were killed.
|
|
|
|
Short answer:
|
|
|
|
* **Cold detect still works on both straps.** On `$DE00` the probe writes `$1E`, reads `$1E` back,
|
|
writes `$15`, reads `$15` back, and latches `$DE`. On `$DF00` it writes `$1E` to `$DE03`, reads
|
|
back `$F6` - open bus - never writes the second pattern, and aims everything at `$DF00`. All six
|
|
accessors at `$E551-$E56E` and `aciaPageLatch` `$E5BF` agreed on the right page on all four
|
|
machines, immediately after the cold detect and again at the end of each run.
|
|
* **The probe does not run again.** Across the two runs the link was opened twice per machine and
|
|
six speed hot keys landed per machine - eight calls to `loadBaudParameters`, and so eight calls to
|
|
`aciaDetect` - and the probe **ran once**. During the whole hot-key sweep the
|
|
control register received exactly **six** writes, `$17 $18 $1A $1C $1E $15`: one per landed key, in
|
|
the order the keys were pressed, with no probe pattern in front of any of them.
|
|
* **Nothing goes to the other page.** On the `$DF00` strap each machine touched `$DE00-$DE03`
|
|
exactly **twice in the whole session** - the single probe write and its single failed read-back,
|
|
both at cold start - and **zero** times during the hot-key sweep or the 20-second census that
|
|
followed. On the `$DE00` strap, `$DF00-$DF03` was touched **zero** times, ever.
|
|
* **38400 is unreachable from the keyboard.** C= + 8 on a live phase-3 link, on both straps,
|
|
changed neither the control register nor `baudIndex`, and the screen was pixel-identical before and
|
|
after. A breakpoint on the swallow at `$E38A` caught the keystroke with `A` = `$B8` and
|
|
`lastHotkeyCode` `$E046` = `$B8`, so the key does reach the hot-key layer and is answered with
|
|
`$FF` = "no key". The other six rates all selected first press: `$15`, `$17`, `$18`, `$1A`, `$1C`,
|
|
`$1E`.
|
|
* **No regression at 1200 baud.** Two boots, four 75-second windows, 303.0 seconds of measured link
|
|
time at true C64 speed; both machines reached connection phase 3 on both boots, **none of 592
|
|
one-second sampling intervals carried zero bytes**, `linkErrorCount` `$E047` read `$00` at all 24
|
|
window edges, and the transmitter-deadlock shape did not occur once.
|
|
* **Two faults were found in the test rig, not in the driver**, and both are fixed. One of them was
|
|
hiding every receive error: `testRealtime.py`'s tracepoint was still on `countLinkError`'s old
|
|
address. Section 7.
|
|
|
|
---
|
|
|
|
## 1. The build under test
|
|
|
|
```
|
|
$ ./build.sh
|
|
wrote ../Modem_Wars_SwiftLink.d64: module installed in track 18 sector 7 and track 34 sectors 1-15
|
|
$ python3 checkAbi.py build/swiftlinkDriverE000.bin ../disassembly/build/game_modemDriverE000.orig.bin
|
|
bytes changed: 461 of 4096, in 32 region(s)
|
|
...
|
|
ABI CHECK PASSED: every address the rest of the game depends on is unchanged
|
|
```
|
|
|
|
The bytes that carry the two changes are in the image:
|
|
|
|
| Address | Bytes | What it is |
|
|
|---|---|---|
|
|
| `$E644` | `AD BF E5 D0 0B A9 DE 20 F2 E6 F0 04 A9 DF D0 0F` | `aciaDetect`: `LDA aciaPageLatch` / `BNE done` first, then the probe |
|
|
| `$E6F2` | `20 63 E6 A9 1E 20 67 E5 49 1E D0 07 A9 15 20 67 E5 49 15 60` | `aciaProbe`: aim, write `$1E`, read back, write `$15`, read back |
|
|
| `$E402` | `A9 00 8D BF E5 60` | the cold-start latch clear inside `selectFramedLinkVectors` |
|
|
| `$E718` | `00 1F 00` | `baudEntryTable` index 18: control `$1F`, hot-key code **`$00`** |
|
|
|
|
`scanIoAccess.py` re-run against this image still finds **exactly the six accessors and nothing
|
|
else** naming an address in `$DE00-$DFFF`, in both its alignment-aware and its superset pass, and
|
|
still finds the only pass-1 hits elsewhere to be the three instructions in the *stock* driver this
|
|
build replaces.
|
|
|
|
Three static facts about reachability, taken by scanning the whole 4096-byte image for `JSR`/`JMP` to
|
|
each address:
|
|
|
|
* `$E3F3` (`selectFramedLinkVectors`, the only thing that clears the latch) is reached from **one**
|
|
instruction: `JSR $E3F3` at `$E2BA`, inside `initCommModule` - the `$E003` X=1 case, which the game
|
|
calls once, from `$0F46`, immediately after the overlay is loaded.
|
|
* `$E644` (`aciaDetect`) is reached from **one** instruction: `JSR $E644` at `$E353`, the first thing
|
|
`loadBaudParameters` does.
|
|
* `$E6F2` (`aciaProbe`) is reached from **one** instruction: `JSR $E6F2` at `$E64B`, inside
|
|
`aciaDetect`, on the far side of the latch test.
|
|
|
|
So the only way to probe the hardware at all is through the latch test, and the only way to clear the
|
|
latch is a cold start. The rest of this report is that argument measured rather than read.
|
|
|
|
### How it was measured
|
|
|
|
`testHarden.py` (new) boots **two** machines cross-connected through the socket null modem, so every
|
|
measurement below is taken on a link that reached connection phase 3 - the game's own packet protocol
|
|
- rather than on a machine talking to itself. It runs in four phases: the cold detect with both
|
|
cartridge pages watched; the seven speed hot keys with the other page and the control register
|
|
watched; a 20-second census of every register access; and the C= + 8 breakpoint.
|
|
|
|
Unlike `testStrap.py`, which used **watchpoints** and stopped the emulator 800 times, this uses
|
|
VICE **tracepoints**, which print the accessing instruction and its `A` register and let the machine
|
|
run on. That matters: a watchpoint stopping the emulator on every ACIA access makes it impossible to
|
|
press a hot key at true C64 speed, and it is also what let the census run for 20 seconds of live link
|
|
without disturbing anything.
|
|
|
|
```
|
|
python3 testHarden.py ../Modem_Wars_SwiftLink.d64 de00
|
|
SWIFTLINK_ACIA_BASE=0xDF00 python3 testHarden.py ../Modem_Wars_SwiftLink.d64 df00
|
|
python3 testRealtime.py ../Modem_Wars_SwiftLink.d64 75 1200 hard1200 2 0
|
|
python3 testRealtime.py ../Modem_Wars_SwiftLink.d64 75 1200 hard1200b 2 0
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Cold detect, on both straps
|
|
|
|
Both machines of both pairs reached `connectionPhase` 3. These are the first accesses each machine
|
|
made to either cartridge page, in order, decoded from the raw tracepoint logs.
|
|
|
|
### `$DE00` strap (`testLogs/harden.de00.A.cold.txt`; machine B is identical)
|
|
|
|
```
|
|
store $DE03 STA $DE03 at $E567 A=$1E aciaProbe pattern 1
|
|
load $DE03 LDA $DE03 at $E56B A=$1E read back through the fall-through - it matches
|
|
store $DE03 STA $DE03 at $E567 A=$15 aciaProbe pattern 2
|
|
load $DE03 LDA $DE03 at $E56B A=$15 it matches too: a 6551 really is here, latch = $DE
|
|
store $DE03 STA $DE03 at $E567 A=$15 loadBaudParameters: $10 OR 300-baud bits
|
|
load $DE03 LDA $DE03 at $E56B A=$15 the read-back aciaSetControl falls into
|
|
load $DE00 LDA $DE00 at $E55F A=$00 drain the receiver
|
|
load $DE01 LDA $DE01 at $E558 A=$50 status
|
|
store $DE02 STA $DE02 at $E554 A=$09 command: DTR on, RTS low, receiver IRQ on, transmit IRQ off
|
|
```
|
|
|
|
That is exactly the sequence `testReport.md` section 3 recorded, with the second probe pattern now in
|
|
it. **Both probe patterns were needed and both came back**, which is the first time the second
|
|
pattern has been seen to run at all.
|
|
|
|
### `$DF00` strap (`testLogs/harden.df00.A.cold.txt`; machine B is identical)
|
|
|
|
```
|
|
store $DE03 STA $DE03 at $E567 A=$1E aciaProbe pattern 1, on the default page
|
|
load $DE03 LDA $DE03 at $E56B A=$F6 open bus - not $1E, so this page is not a 6551
|
|
store $DF03 STA $DF03 at $E567 A=$15 the fallback is taken: loadBaudParameters at $DF00
|
|
load $DF03 LDA $DF03 at $E56B A=$15
|
|
load $DF00 LDA $DF00 at $E55F A=$00 drain the receiver
|
|
load $DF01 LDA $DF01 at $E558 A=$50 status
|
|
store $DF02 STA $DF02 at $E554 A=$09 command
|
|
```
|
|
|
|
**The second pattern is never written on this strap**, because the first one already decided the
|
|
question - which is the whole point of writing the cheap one first. The `$F6` the read returned is
|
|
the value `probeOpenBus.py` saw in 51.7% of its 120 samples of an empty `$DE03`; nothing about it is
|
|
`$1E`, and the probe fell through correctly.
|
|
|
|
### The six accessors and the latch
|
|
|
|
Read back through the monitor after the cold detect, and again at the end of each run - all four
|
|
machines, all eight readings:
|
|
|
|
| Accessor | Instruction | Operand high byte at | `$DE00` run | `$DF00` run |
|
|
|---|---|---|---|---|
|
|
| `aciaCmdWrite` `$E554` | `sta $xx02` | `$E556` | `$DE` | `$DF` |
|
|
| `aciaStatusRead` `$E558` | `lda $xx01` | `$E55A` | `$DE` | `$DF` |
|
|
| `aciaDataRead` `$E55F` | `lda $xx00` | `$E561` | `$DE` | `$DF` |
|
|
| `aciaDataWrite` `$E563` | `sta $xx00` | `$E565` | `$DE` | `$DF` |
|
|
| `aciaCtrlWrite` `$E567` | `sta $xx03` | `$E569` | `$DE` | `$DF` |
|
|
| `aciaCtrlRead` `$E56B` | `lda $xx03` | `$E56D` | `$DE` | `$DF` |
|
|
| **`aciaPageLatch`** | - | **`$E5BF`** | **`$DE`** | **`$DF`** |
|
|
|
|
The latch and the six accessors were in step in every reading, which is what makes the latch an
|
|
honest stand-in for them.
|
|
|
|
---
|
|
|
|
## 3. The probe does not run again
|
|
|
|
Two independent measurements say so, and neither depends on the other.
|
|
|
|
### 3.1 The link was opened twice, and the probe ran once
|
|
|
|
`testHarden.openLink` offers A/O and space until an ACIA register is touched, and on both runs it
|
|
took two rounds, so `openCommLink` - and therefore `loadBaudParameters`, and therefore `aciaDetect` -
|
|
ran twice on every machine before the hot keys were even reached. What the control register received
|
|
in that phase:
|
|
|
|
| Run | Machine | Control-register writes, in order | Probe writes | Link opens |
|
|
|---|---|---|---:|---:|
|
|
| `$DE00` | A | `$1E $15` then `$15 $15` | 2 (one probe) | 2 |
|
|
| `$DE00` | B | `$1E $15` then `$15 $15` | 2 (one probe) | 2 |
|
|
| `$DF00` | A | `$1E` to `$DE03`, then `$15 $15` to `$DF03` | 1 (one probe) | 2 |
|
|
| `$DF00` | B | `$1E` to `$DE03`, then `$15 $15` to `$DF03` | 1 (one probe) | 2 |
|
|
|
|
The old build would have written the probe pattern once per open. This one wrote it once per cold
|
|
start.
|
|
|
|
### 3.2 Seven hot keys on a live link, six control-register writes
|
|
|
|
For this phase the tracepoints were narrowed to the two things the question turns on: **all four
|
|
registers of the other page**, and **the control register of the strapped page**. (Data and command
|
|
register traffic on the strapped page is the ordinary business of a running link - the census in
|
|
section 4 counts it - and it would have buried the six accesses that matter.)
|
|
|
|
The whole hot-key phase - 182 seconds of live phase-3 link between the end of the cold-detect log and
|
|
the end of the sweep, seven keys pressed on each machine, C= + 1, C= + 2, C= + 4, **C= + 8**, C= + 9,
|
|
C= + 0, C= + 3 - produced **twelve** accesses to that traced set on every one of the four machines,
|
|
and they are the same twelve everywhere:
|
|
|
|
```
|
|
store $xx03 A=$17 load $xx03 A=$17 C= + 1 1200
|
|
store $xx03 A=$18 load $xx03 A=$18 C= + 2 2400
|
|
store $xx03 A=$1A load $xx03 A=$1A C= + 4 4800
|
|
store $xx03 A=$1C load $xx03 A=$1C C= + 9 9600
|
|
store $xx03 A=$1E load $xx03 A=$1E C= + 0 19200
|
|
store $xx03 A=$15 load $xx03 A=$15 C= + 3 300
|
|
```
|
|
|
|
**Six writes for six landed keys, each one the rate that was asked for, in the order it was asked
|
|
for, and nothing else at all** - no probe pattern in front of any of them, not one access to the
|
|
other page, and nothing whatever attributable to C= + 8. The matching `load` after each `store` is
|
|
not a second access to look at: `aciaSetControl` falls through a `NOP` into `aciaGetControl` by
|
|
design, so every control write reads itself back, probe or not.
|
|
|
|
Had the probe re-run on every hot key, as it did before the latch, this sweep would have looked quite
|
|
different. On the `$DE00` strap each key would have written `$1E`, `$15` and then the rate:
|
|
**eighteen** control-register writes instead of six. On the `$DF00` strap each key would have
|
|
written `$1E` into `$DE03`, failed to read it back, and then written the rate into `$DF03`: six
|
|
writes to a page this driver has no business touching, instead of none.
|
|
|
|
**One thing in the script's own output should be ignored.** `testHarden.py` counts back-to-back
|
|
`$1E`,`$15` pairs as "the probe's signature" and reported **1** on the `$DF00` run. That is a false
|
|
positive and the heuristic is worthless: `$1E` is 19200's control byte and `$15` is 300's, and the
|
|
last two keys in the sweep are C= + 0 followed by C= + 3. The pair it found is those two hot keys.
|
|
The measurement that carries the claim is the one above - six writes, matching six keys - not that
|
|
counter.
|
|
|
|
---
|
|
|
|
## 4. Nothing is written to the other page
|
|
|
|
Counted the way `strapReport.md` counted it, per page, but over three separate phases of the same
|
|
session rather than one 800-stop log. A tracepoint does not stop the emulator, so these are complete
|
|
counts of a running machine rather than a sampled subset.
|
|
|
|
### `$DF00` strap - the case that matters
|
|
|
|
| Phase | Machine | `$DE00-$DE03` | `$DF00-$DF03` |
|
|
|---|---|---:|---:|
|
|
| cold detect + two link opens + the Hayes string + byte sync | A | **2** | 530 |
|
|
| | B | **2** | 433 |
|
|
| the 182-second hot-key sweep, seven keys | A | **0** | 12 |
|
|
| | B | **0** | 12 |
|
|
| a 20-second census of a live phase-3 link | A | **0** | 2739 |
|
|
| | B | **0** | 2444 |
|
|
| **whole session** | A | **2** | 3281 |
|
|
| | B | **2** | 2889 |
|
|
|
|
The two accesses are the single probe write of `$1E` to `$DE03` and the single read that returned
|
|
`$F6`. There is no third. The census breaks down as `$DF00` 591 loads / 662 stores (received and
|
|
transmitted characters), `$DF01` 1246 loads (status reads, which are also the NMI acknowledge) and
|
|
`$DF02` 240 stores (command `$09`/`$05` as the transmitter is armed and disarmed) on machine A - the
|
|
ordinary traffic of a link carrying ARQ frames, and none of it on the wrong page.
|
|
|
|
### `$DE00` strap - the mirror-image check
|
|
|
|
| Phase | Machine | `$DE00-$DE03` | `$DF00-$DF03` |
|
|
|---|---|---:|---:|
|
|
| cold detect + two link opens | A | 553 | **0** |
|
|
| | B | 451 | **0** |
|
|
| hot-key sweep | A | 12 | **0** |
|
|
| | B | 12 | **0** |
|
|
| 20-second census | A | 2721 | **0** |
|
|
| | B | 2418 | **0** |
|
|
|
|
Nothing on this build ever named `$DF00` when the cartridge answered at `$DE00`, which is what the
|
|
six self-modified accessors being the module's only knowledge of the hardware predicts.
|
|
|
|
---
|
|
|
|
## 5. 38400 is unreachable, and the other six rates are not
|
|
|
|
### C= + 8 on a live link
|
|
|
|
Pressed once on each machine, Commodore key (Tab) and `8` held down together for 600 ms - about 36 of
|
|
the game's once-a-frame keyboard scans - with both machines in phase 3. State read immediately
|
|
before and immediately after:
|
|
|
|
| Run | Machine | before | after |
|
|
|---|---|---|---|
|
|
| `$DE00` | A | `baudIndex` 9, `$DE03` = `$1A`, phase 3 | `baudIndex` **9**, `$DE03` = **`$1A`**, phase 3 |
|
|
| `$DE00` | B | `baudIndex` 9, `$DE03` = `$1A`, phase 3 | `baudIndex` **9**, `$DE03` = **`$1A`**, phase 3 |
|
|
| `$DF00` | A | `baudIndex` 9, `$DF03` = `$1A`, phase 3 | `baudIndex` **9**, `$DF03` = **`$1A`**, phase 3 |
|
|
| `$DF00` | B | `baudIndex` 9, `$DF03` = `$1A`, phase 3 | `baudIndex` **9**, `$DF03` = **`$1A`**, phase 3 |
|
|
|
|
`$1F` was never written to any control register in any phase of either run, and the tracepoint on the
|
|
control register recorded no access at all between the C= + 4 pair and the C= + 9 pair. The link
|
|
survived it: the next hot key, C= + 9, landed normally on both machines in both runs.
|
|
|
|
### The keystroke reaches the driver and is swallowed
|
|
|
|
"Nothing changed" on its own would also be what a key that never arrived looks like, so the swallow
|
|
itself was caught. `$E38A` is the `LDA #$FF` that answers the game "no key" for any Commodore-key
|
|
combination the hot-key layer does not recognise, and it is the only way out of that layer for
|
|
C= + 8 now that the baud walk cannot match it. With a breakpoint armed there **before** the key went
|
|
down (entering the monitor clears VICE's emulated key matrix, so the order matters):
|
|
|
|
```
|
|
#1 (Stop on exec e38a) 34/$022, 61/$3d
|
|
.C:e38a A9 FF LDA #$FF - A:B8 X:00 Y:00 SP:e9 N.-..... 374331486
|
|
|
|
lastHotkeyCode $E046 = $B8, baudIndex $E055 = 0, bitPeriodLo $E056 = $15, $DE03 = $15
|
|
```
|
|
|
|
`A` = `$B8` is the C= + 8 key code itself, arriving at the swallow; `$E046` = `$B8` says the hot-key
|
|
layer logged it as a fresh Commodore-key code; and the rate bytes are untouched. The same
|
|
breakpoint fired the same way on the `$DF00` run. So the key is seen, walked past every hot key in
|
|
the chain including the baud table, and answered with `$FF` - which is what stops the game seeing it.
|
|
|
|
Corroboration from the screen: `shots/hdde0003aBeforeC8.png` against `shots/hdde0004aAfterC8.png`,
|
|
and `shots/hddf0003aBeforeC8.png` against `shots/hddf0004aAfterC8.png`, are **pixel-identical**
|
|
(0 of 384x247 pixels differ in either pair). The game was on "OPPONENT PICKING GAME/MAP." and did
|
|
not act on an `8`.
|
|
|
|
### The other six rates
|
|
|
|
Every one landed on both machines, on both straps, at the first press:
|
|
|
|
| Hot key | Rate | `baudIndex` `$E055` | Control register | `bitPeriodLo` `$E056` | Landed |
|
|
|---|---:|---:|---|---|---|
|
|
| C= + 3 | 300 | 0 | `$15` | `$15` | yes |
|
|
| C= + 1 | 1200 | 3 | `$17` | `$17` | yes |
|
|
| C= + 2 | 2400 | 6 | `$18` | `$18` | yes |
|
|
| C= + 4 | 4800 | 9 | `$1A` | `$1A` | yes |
|
|
| C= + 9 | 9600 | 12 | `$1C` | `$1C` | yes |
|
|
| C= + 0 | 19200 | 15 | `$1E` | `$1E` | yes |
|
|
| **C= + 8** | **38400** | **unchanged** | **unchanged** | **unchanged** | **no** |
|
|
|
|
`$15`, `$17`, `$18`, `$1A`, `$1C` and `$1E` were each seen in the register with the matching
|
|
`baudIndex` four times over - two machines, two straps.
|
|
|
|
A note on what happened *around* the sweep, recorded because it is the honest picture: driving a pair
|
|
up to 19200 and back down inside three minutes is not a healthy way to run a link, and it showed. On
|
|
the `$DF00` run, one reading at 19200 caught machine A in phase 2 with `linkErrorCount` = 13 and
|
|
`uartPendingCount` = `$C9`; by the next reading, before the following hot key, it was back to
|
|
`linkErrorCount` = 0, `uartPendingCount` = `$00`, `txCharActive` = `$00` and a self-consistent
|
|
command shadow, and the run finished with both machines linked at 300 baud and a 20-second census
|
|
recording 2739 and 2444 register accesses. That is the "degrades and recovers" behaviour
|
|
`highRateReport.md` section 3 measured at 9600 and 19200, not something new. Over all 56 state
|
|
readings of the two runs, `linkErrorCount` was `$00` in 55 of them and the transmitter-deadlock shape
|
|
- `txCharActive` set while command bit 2 is clear - occurred **zero** times.
|
|
|
|
---
|
|
|
|
## 6. No regression: 1200 baud, two boots, four windows
|
|
|
|
`testRealtime.py`, 1200 baud selected with C= + 1 at the "PRESS A OR O" prompt on both machines
|
|
before the link was opened, two consecutive 75-second windows per boot, nothing sent to either
|
|
monitor inside a window.
|
|
|
|
| Boot | Window | Wall | A speed | B speed | A -> B | B -> A | `$55` lead bytes |
|
|
|---|---|---:|---:|---:|---|---|---|
|
|
| 1 | 1 | 75.99 s | 100.9% | 100.5% | 3793 B = **49.91 B/s** | 3151 B = **41.46 B/s** | 697 / 714 |
|
|
| 1 | 2 | 75.69 s | 101.3% | 100.9% | 3133 B = **41.39 B/s** | 3545 B = **46.84 B/s** | 778 / 727 |
|
|
| 2 | 1 | 75.72 s | 101.2% | 100.8% | 2193 B = **28.96 B/s** | 2032 B = **26.84 B/s** | 631 / 705 |
|
|
| 2 | 2 | 75.62 s | 101.2% | 100.8% | 3034 B = **40.12 B/s** | 2574 B = **34.04 B/s** | 530 / 494 |
|
|
|
|
**303.0 seconds of measured link time, 12153 bytes one way and 11302 the other.** Every machine in
|
|
every window ran at 100.5% of real time or better against its own free-running cycle counter, so this
|
|
is true C64 speed and not warp.
|
|
|
|
The five things this run had to answer, one at a time:
|
|
|
|
* **Both machines reached phase 3, on both boots.** `waitForPhase` returned
|
|
`{'A': 3, 'B': 3}` before the first window of each boot, and the `$55` counts - 494 to 778 ARQ
|
|
frame lead bytes per window - say frames kept crossing for the whole of every window, which only
|
|
happens in the packet phase. At the individual window edges `connectionPhase` was found at 2 as
|
|
well as 3, which is the ordinary 1200-baud picture: a receive-ring overflow forces a byte-sync
|
|
round, the round completes, the packet layer carries on.
|
|
* **Traffic flowed both ways every second.** The relay's byte counts are sampled once a second
|
|
inside each window: **592 one-second sampling intervals over the two boots - 148 per window,
|
|
74 in each direction - and not one of them carried zero bytes.** Per-interval rates ran from
|
|
11.9 B/s to 109.3 B/s, and 109.3 is 91% of the 120 B/s an 8N1 line carries at 1200 baud, so the
|
|
line does still get saturated in bursts.
|
|
* **`linkErrorCount` `$E047` stayed at `$00`.** All 24 state readings taken with the link up read
|
|
`$00`. (The two readings taken *before* the link opened read 72 = `$48`, which is the leftover
|
|
ASCII `H` the module ships with at `$E047`; `clearLinkVars` zeroes it when the link opens.)
|
|
* **The transmitter never parked.** In all 24 live readings `txCharActive` `$E5BB` and
|
|
`aciaCommandShadow` `$E5BE` were only ever the two self-consistent pairs `$00`/`$09` (idle) and
|
|
`$01`/`$05` (a byte of ours in the chip). The fault shape - `$E5BB` set while `$E5BE` bit 2 is
|
|
clear - occurred zero times, and `probeTxDeadlock` never fired, so nothing in these numbers is the
|
|
test repairing the driver. `uartPendingCount` `$E0A5` read `$00`, `$01` or `$03` at every edge and
|
|
was never once out of range.
|
|
* **The errors that did occur were the documented ones.** The `countLinkError` tracepoint counted
|
|
**22** rejected characters across the four windows (4, 4, 7, 7), and the conditional tracepoint on
|
|
the `CPX #$14` inside `pushUartRxRing` returned **identical** counts (4, 4, 7, 7). Every single
|
|
one was therefore a **receive-ring overflow, not a bad character** - the same fingerprint
|
|
`rate1200Report.md` found for all 32 of its overflows. That the byte-sync rounds they caused
|
|
completed is an inference here rather than a measurement: the `$E805`/`$E855` sync tracepoints were
|
|
not armed in these windows, so what says the link recovered is that frames kept crossing, that no
|
|
sampling interval carried nothing, and that `connectionPhase` was still being found at 3 at the
|
|
window edges. Neither emulator logged a CPU JAM.
|
|
|
|
That is the same overflow rate `rate1200Report.md` measured on the pre-hardening build, to within
|
|
the noise: 32 in 452 s there, 0.071 a second, against 22 in 303 s here, 0.073 a second. Two boots is
|
|
not enough to call a 3% difference anything at all; what the two runs agree on is the shape - every
|
|
error a ring overflow, every overflow followed by a recovery, and the link never stopping.
|
|
|
|
---
|
|
|
|
## 7. Two faults in the test rig, found and fixed
|
|
|
|
Both are in the rig, not the driver, and both would have made this report wrong.
|
|
|
|
* **`testRealtime.py` was tracing an address that no longer exists.** `countLinkError` moved from
|
|
`$E403` to `$E73F` when `selectFramedLinkVectors` had to grow the five bytes that clear the page
|
|
latch - that move is part of the hardening being verified here. `testRealtime.py` and
|
|
`testHighRates.py` still said `trace exec $e403`, and `$E403` is now the operand byte of an
|
|
`LDA #$00`: the program counter never equals it, so the tracepoint can never fire. Left alone, this
|
|
report would have claimed **zero** receive errors at 1200 baud with a straight face. Both scripts
|
|
now trace `$e73f`, which is where the 22 overflows above came from.
|
|
* **The tracepoint parsers dropped every access whose value contained a hex letter.** VICE prints
|
|
the program counter in lowercase (`.C:e567`) and the register values in uppercase (`- A:1E`), and
|
|
the `DISASM` pattern in `testStrap.py` - which `testHarden.py` inherited - matched `A:` only against
|
|
`[0-9a-f]`. So `A:1E` did not match, the header was paired with a later line, and 58 of 553
|
|
accesses in the first log were mis-parsed, including the probe's own first write. The pattern now
|
|
accepts both cases in `testStrap.py` and `testHarden.py`, and `parseTrace.py` (new) re-parses a
|
|
saved log offline, which is where every number in sections 2 to 4 comes from. The raw logs were
|
|
never affected - only the parsing of them.
|
|
|
|
---
|
|
|
|
## 8. What this does not say
|
|
|
|
* **No real hardware, and no C64 Ultimate.** Everything here is VICE 3.7.1. Nothing has been run on
|
|
a physical SwiftLink, a real cable, a real modem, or the device this exercise is aimed at.
|
|
* **VICE's open bus is not the C64's.** The `$DF00` strap's probe read returned `$F6` because that
|
|
is what VICE puts on the bus for an unmapped I/O read at that moment. The case the second pattern
|
|
exists for - a page that answers the first pattern and not the second - has still **never been
|
|
produced**, on this rig or any other, and cannot be: on this rig the first pattern decides every
|
|
case. The two-pattern probe is therefore *installed and exercised on the path where both patterns
|
|
match*, and unexercised on the path it was written for.
|
|
* **VICE does not emulate the register mirrors**, so "nothing touched the other page" is a statement
|
|
about the four bytes VICE maps. `testHarden.py` did not arm the mirror tripwire that `testStrap.py`
|
|
uses on `$DE04-$DEFF` / `$DF04-$DFFF`; that tripwire recorded 0 stops in every run of
|
|
`strapReport.md` section 7.3, and the static scan in section 1 above still finds no instruction in
|
|
this build that could reach it.
|
|
* **The C= + 8 evidence is in two halves and each is partial.** During the rate sweep the proof is
|
|
negative - nothing changed - because `lastHotkeyCode` is overwritten with `$FF` by the next
|
|
keyboard scan, so the snapshot cannot show that the key arrived. The breakpoint run proves the key
|
|
arrives and is swallowed, but it stops the emulator to do it. Neither run watched what the *game*
|
|
does with a plain `8` in a context where `8` means something, because the game was on
|
|
"OPPONENT PICKING GAME/MAP." both times.
|
|
* **`aciaDetect` still does not verify `$DF00`.** When `$DE00` does not answer, the accessors are
|
|
aimed at `$DF00` on trust and the latch is set - so a machine with no cartridge on either page
|
|
latches `$DF` and never probes again until the module is reloaded. That is deliberate (the source
|
|
says so), it is unchanged by this work, and it was not tested.
|
|
* **The 20-second census is 20 seconds.** It says nothing about a machine that has been linked for
|
|
an hour, and the whole live-link portion of each `testHarden.py` run is about four minutes.
|
|
* **Four of the twelve tracepoint logs end with one header whose disassembly line was cut off** by
|
|
the end of the capture window; `parseTrace.py` warns about it. They are the `$DE00` run's two cold
|
|
logs and its machine-A census, and the `$DF00` run's machine-B cold log - one access out of 553,
|
|
451, 2721 and 435 respectively. Each is counted in the per-address totals, because the header
|
|
carries the address and the direction, but contributes no value byte.
|
|
* **The transmit-overwrite counts are noise at this sample size.** VICE logs
|
|
`ACIA: data register written although data has not been sent yet.` when the driver writes a data
|
|
register that has not emptied - one transmitted character lost, recovered by the ARQ layer. The
|
|
four `testHarden.py` machines logged 27, 21, 108 and 87 of them, and the four 1200-baud machines
|
|
logged 32, 100, 12 and 28. Two runs of the *identical* script differed by a factor of four, and no
|
|
pre-hardening run ever counted them at 1200 baud, so these numbers cannot be read as a regression
|
|
or as its absence. What can be said is that no window lost the link and every receive error
|
|
counted at 1200 was a ring overflow rather than a bad character.
|
|
* **One boot per strap for the hardening runs, two for 1200 baud.** Nothing here is a soak test.
|
|
* **No complete game was played**, at either strap or either rate.
|
|
|
|
---
|
|
|
|
## Files
|
|
|
|
| File | What it is |
|
|
|---|---|
|
|
| `testHarden.py` | new: two machines, either strap, tracepoints on both cartridge pages; the cold detect, the seven hot keys on a live link, a 20-second access census, and the C= + 8 breakpoint |
|
|
| `parseTrace.py` | new: re-parses a saved tracepoint log into an ordered access list and a per-address / per-page census |
|
|
| `testRealtime.py`, `testHighRates.py` | changed: the `countLinkError` tracepoint moved from `$E403` to `$E73F` |
|
|
| `testStrap.py` | changed: the register-value pattern now accepts uppercase hex |
|
|
| `testLogs/harden.de00.txt`, `harden.df00.txt` | the two `testHarden.py` transcripts |
|
|
| `testLogs/harden.{de00,df00}.{A,B}.{cold,hotkeys,census}.txt` | the twelve raw tracepoint logs every count above is taken from |
|
|
| `testLogs/harden1200.txt`, `harden1200b.txt` | the two 1200-baud transcripts |
|
|
| `testLogs/realtime.hard1200*.w{1,2}.samples.txt` / `.trace.txt` | per-second byte counts and raw tracepoint output for the four windows |
|
|
| `shots/hdde00*.png`, `shots/hddf00*.png`, `shots/rthard1200*.png` | the screenshots cited above |
|