From 19666dfadc88e7b0dcbf5495e3cd0d3e4117d8f9 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 20:39:04 -0500 Subject: [PATCH 1/2] tools: regsweep.py measures a Special IO handler's read margin with regread.a --- tools/bench/README.md | 1 + tools/bench/regread.a | 119 ++++++++++++++++++++++++++++++++++++++++ tools/bench/regsweep.py | 73 ++++++++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 tools/bench/regread.a create mode 100644 tools/bench/regsweep.py diff --git a/tools/bench/README.md b/tools/bench/README.md index c2e9785d..46f04c11 100644 --- a/tools/bench/README.md +++ b/tools/bench/README.md @@ -48,6 +48,7 @@ whatever node appeared beside it. | `hostenter.py [phrase]` | The other shape from `exttest.py`, for a host that hands the machine back: launch, the port drops, the host runs and resets, and the main image prints its `VmFail` record on the way back up. With a `phrase` that record is the assertion; without one the run reports what it saw and asserts nothing, because a launch that failed after the jump reboots the same way a good one does. A host that stays resident never gives the main image's port back, and what that looks like here depends on whether minimal's orphan node stayed behind: with no node, `REBOOT_TIMEOUT` seconds and then `the board did not come back`; with one -- the usual case -- the screen read that follows fails instead, `no reply -- DMA read is not compiled into this image, or this is not the main image`, which names the wrong diagnosis for a host that is running exactly as intended. Use `exttest.py` for those; it treats no image answering as the success it is. | | `peek.py ` | Hex dump C64 memory. | | `spans.py [NMI period]` | `WriteC64Spans` against a C64 running `spanclient.a`: read-back across slice boundaries, the 1,024-slice refusal, what one slice costs with the C64 loop writing, only reading, and only reading with the screen blanked, and timer NMIs lost per job against slice size. Needs `acme` on `PATH`, and resets the C64 when done. | +| `regsweep.py ...` | The read margin of the Special IO handler chosen in Settings, per `docs/Architecture/Constraints.md`. Runs `regread.a`, which reads the REU's address and length registers back, checks that `$DF10-$DF1F` read `$FF` beside a TR page at `$DE00`, and checks the TR page's `TRM`/`TRB` IDs. Then, for each value, sets `nS_PLAprop` (`tp###`) and prints the wrong answers the C64 counted in `dwell` seconds, and restores the defaults (`td`) at the end. Needs `acme` on `PATH` and an image built with `Dbg_SerTimChg` in `TeensyROM.h`. | | `grants.py` | Service bit 20 end to end: packages `grants/module.cpp` and `grants/client.a` as `/VMS/GRANTS`, then launches one content file per slice size. The client grants a slice from a CIA2 timer NMI every 200 cycles, when it will end before the next tick, and counts the NMIs it loses; the module writes 4 KiB to `$4000` for 1.5 s and exits with its job count and that loss, which the main image prints on the way back. Each row also checks a sum the client takes over what it holds after the last job, and a slice that never fits, like a client that never grants (the first row), ends the job `VM_C64_NO_GRANT`. `grants.py 120 140` runs only those rows. Needs `acme` on `PATH` and a host serving bit 20 installed. | | `screen.py` | The C64 text screen. | | `keypress.py [code]` | Put a key in the keyboard buffer (default `Y`; F1/F3/F5/F7 are `0x85`..`0x88`). | diff --git a/tools/bench/regread.a b/tools/bench/regread.a new file mode 100644 index 00000000..6b65ee04 --- /dev/null +++ b/tools/bench/regread.a @@ -0,0 +1,119 @@ +; C64 side of regsweep.py: reads the cartridge's registers in a loop and counts +; wrong answers at $C000, where regsweep.py reads them by DMA. +; $C000/1 passes (256 rounds each) $C002/3 REU register errors $C004/5 $FF-page errors +; $C006/7 TR page ID errors $C008 layout: 0 plain REU, 1 REU+TR page at $DE, 2 GeoRAM (TR page at $DF) +!cpu 6502 +res = $c000 +val = $fb +page = $fc ; pointer $fc/$fd to the TR page +save = $02 + * = $0801 + !word +, 10 + !byte $9e + !text "2061" + !byte 0 ++ !word 0 + lda #0 + ldx #8 +- sta res,x + dex + bpl - + sta page + lda #$df + jsr hasTRB + bcc geo + lda #$de + jsr hasTRB + bcc beside + lda #0 + beq setmode +geo lda #2 + bne setmode +beside lda #1 +setmode sta res+8 + +loop lda res+8 + cmp #2 + beq trpage + ldx #6 ; REU address and length registers read back as written +- txa + clc + adc val + sta $df02,x + dex + bpl - + ldx #6 +- txa + clc + adc val + cmp $df02,x + beq + + inc res+2 + bne + + inc res+3 ++ dex + bpl - + lda res+8 + beq next + ldx #$0f ; beside the TR page, $DF10-$DF1F read $FF +- lda $df10,x + cmp #$ff + beq + + inc res+4 + bne + + inc res+5 ++ dex + bpl - +trpage jsr checkIDs +next inc val + bne loop + inc res + bne loop + inc res+1 + jmp loop + +checkIDs ; page+3..5 = "TRM", page+$10..$12 = "TRB" + ldy #2 +- sty save + tya + clc + adc #3 + tay + lda (page),y + ldy save + cmp trm,y + beq + + jsr idErr ++ tya + clc + adc #$10 + tay + lda (page),y + ldy save + cmp trb,y + beq + + jsr idErr ++ dey + bpl - + rts + +idErr inc res+6 + bne + + inc res+7 ++ rts + +hasTRB sta page+1 ; carry clear when "TRB" is at A*256+$10 + ldy #$12 +- lda (page),y + cmp trb-$10,y + bne + + dey + cpy #$10 + bcs - + clc + rts ++ sec + rts + +trm !text "TRM" +trb !text "TRB" diff --git a/tools/bench/regsweep.py b/tools/bench/regsweep.py new file mode 100644 index 00000000..c54aef52 --- /dev/null +++ b/tools/bench/regsweep.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Read margin of the Special IO handler in use: raise nS_PLAprop while regread.a +reads the cartridge's registers, and count the wrong answers at each step. + regsweep.py DWELL VALUE... + +Needs acme on PATH, an image built with Dbg_SerTimChg, the Special IO handler +under test chosen in Settings, and the C64 at the menu. Restores the default +timing when done, and leaves regread running. +""" +import os, subprocess, sys, tempfile, time +from protocol import DRIVE_SD +from trlink import Link + +RESULTS = 0xC000 +LAYOUTS = {0: 'plain REU', 1: 'REU with the TR page at $DE00', 2: 'GeoRAM with the TR page at $DF00'} + + +def counters(tr): + b = tr.peek(RESULTS, 9) + return dict(passes=b[0] | b[1] << 8, reu=b[2] | b[3] << 8, ff=b[4] | b[5] << 8, + ids=b[6] | b[7] << 8, layout=b[8]) + + +def start_regread(tr): + here = os.path.dirname(os.path.abspath(__file__)) + with tempfile.TemporaryDirectory() as work: + subprocess.run(['acme', '-f', 'cbm', '-o', os.path.join(work, 'regread.prg'), + os.path.join(here, 'regread.a')], check=True, cwd=work) + tr.post(os.path.join(work, 'regread.prg'), '/REGREAD.PRG') + tr.launch('/REGREAD.PRG', DRIVE_SD) + time.sleep(3) + first = counters(tr)['passes'] + time.sleep(1) + if counters(tr)['passes'] == first: + raise SystemExit('regread is not running') + + +def timing(tr, command): + tr.drain(0.2) + tr.wr(command) + return tr.text() + + +def main(): + if len(sys.argv) < 3: + raise SystemExit(__doc__) + dwell = float(sys.argv[1]) + values = [int(v) for v in sys.argv[2:]] + with Link() as tr: + if 'nS_PLAprop' not in timing(tr, b't'): + raise SystemExit('no timing commands: this image was built without Dbg_SerTimChg') + start_regread(tr) + print(f'layout: {LAYOUTS.get(counters(tr)["layout"], "unknown")}') + try: + for value in values: + shown = timing(tr, b'tp%03d' % value) + if 'nS_PLAprop %03d' % value not in shown: + raise SystemExit(f'tp{value:03d} not taken:\n{shown}') + time.sleep(0.5) + before = counters(tr) + time.sleep(dwell) + after = counters(tr) + added = {k: after[k] - before[k] & 0xFFFF for k in ('passes', 'reu', 'ff', 'ids')} + print(f'nS_PLAprop {value:3d} passes +{added["passes"]:5d} REU registers +{added["reu"]:5d} ' + f'$FF page +{added["ff"]:5d} TR page IDs +{added["ids"]:5d}', flush=True) + finally: + if 'Defaults set' not in timing(tr, b'td'): + raise SystemExit('td not confirmed: the board is still running the last value') + + +if __name__ == '__main__': + main() From 33cf5e35d793c795b7af0295b6f37a1b4f0dfb1e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 29 Oct 2026 20:44:40 -0500 Subject: [PATCH 2/2] fix: review findings for 19666df regread.a counted errors in 16 bits, which wrap within a 4-second dwell once most reads fail: at nS_PLAprop 400 the $FF page row printed +1248 for about 66,800 wrong reads. Error counters are now 24-bit. --- tools/bench/regread.a | 27 +++++++++++++++++---------- tools/bench/regsweep.py | 13 +++++++------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/tools/bench/regread.a b/tools/bench/regread.a index 6b65ee04..305451e9 100644 --- a/tools/bench/regread.a +++ b/tools/bench/regread.a @@ -1,7 +1,8 @@ ; C64 side of regsweep.py: reads the cartridge's registers in a loop and counts ; wrong answers at $C000, where regsweep.py reads them by DMA. -; $C000/1 passes (256 rounds each) $C002/3 REU register errors $C004/5 $FF-page errors -; $C006/7 TR page ID errors $C008 layout: 0 plain REU, 1 REU+TR page at $DE, 2 GeoRAM (TR page at $DF) +; $C000/1 passes (256 rounds each) $C002-4 REU register errors $C005-7 $FF-page errors +; $C008-A TR page ID errors $C00B layout: 0 plain REU, 1 REU+TR page at $DE, 2 GeoRAM (TR page at $DF) +; Error counts are 24-bit: with every read failing they pass 65535 within 4 seconds. !cpu 6502 res = $c000 val = $fb @@ -14,7 +15,7 @@ save = $02 !byte 0 + !word 0 lda #0 - ldx #8 + ldx #$0b - sta res,x dex bpl - @@ -30,9 +31,9 @@ save = $02 geo lda #2 bne setmode beside lda #1 -setmode sta res+8 +setmode sta res+$0b -loop lda res+8 +loop lda res+$0b cmp #2 beq trpage ldx #6 ; REU address and length registers read back as written @@ -51,17 +52,21 @@ loop lda res+8 inc res+2 bne + inc res+3 + bne + + inc res+4 + dex bpl - - lda res+8 + lda res+$0b beq next ldx #$0f ; beside the TR page, $DF10-$DF1F read $FF - lda $df10,x cmp #$ff beq + - inc res+4 - bne + inc res+5 + bne + + inc res+6 + bne + + inc res+7 + dex bpl - trpage jsr checkIDs @@ -97,9 +102,11 @@ checkIDs ; page+3..5 = "TRM", page+$10..$12 = "TRB" bpl - rts -idErr inc res+6 +idErr inc res+8 bne + - inc res+7 + inc res+9 + bne + + inc res+$0a + rts hasTRB sta page+1 ; carry clear when "TRB" is at A*256+$10 diff --git a/tools/bench/regsweep.py b/tools/bench/regsweep.py index c54aef52..abf372da 100644 --- a/tools/bench/regsweep.py +++ b/tools/bench/regsweep.py @@ -17,9 +17,9 @@ def counters(tr): - b = tr.peek(RESULTS, 9) - return dict(passes=b[0] | b[1] << 8, reu=b[2] | b[3] << 8, ff=b[4] | b[5] << 8, - ids=b[6] | b[7] << 8, layout=b[8]) + b = tr.peek(RESULTS, 12) + return dict(passes=b[0] | b[1] << 8, reu=int.from_bytes(b[2:5], 'little'), + ff=int.from_bytes(b[5:8], 'little'), ids=int.from_bytes(b[8:11], 'little'), layout=b[11]) def start_regread(tr): @@ -61,9 +61,10 @@ def main(): before = counters(tr) time.sleep(dwell) after = counters(tr) - added = {k: after[k] - before[k] & 0xFFFF for k in ('passes', 'reu', 'ff', 'ids')} - print(f'nS_PLAprop {value:3d} passes +{added["passes"]:5d} REU registers +{added["reu"]:5d} ' - f'$FF page +{added["ff"]:5d} TR page IDs +{added["ids"]:5d}', flush=True) + added = {k: after[k] - before[k] & 0xFFFFFF for k in ('reu', 'ff', 'ids')} + added['passes'] = after['passes'] - before['passes'] & 0xFFFF + print(f'nS_PLAprop {value:3d} passes +{added["passes"]:5d} REU registers +{added["reu"]:6d} ' + f'$FF page +{added["ff"]:6d} TR page IDs +{added["ids"]:6d}', flush=True) finally: if 'Defaults set' not in timing(tr, b'td'): raise SystemExit('td not confirmed: the board is still running the last value')