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..305451e9 --- /dev/null +++ b/tools/bench/regread.a @@ -0,0 +1,126 @@ +; 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-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 +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 #$0b +- 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+$0b + +loop lda res+$0b + 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 + bne + + inc res+4 ++ dex + bpl - + lda res+$0b + beq next + ldx #$0f ; beside the TR page, $DF10-$DF1F read $FF +- lda $df10,x + cmp #$ff + beq + + inc res+5 + bne + + inc res+6 + bne + + inc res+7 ++ 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+8 + bne + + inc res+9 + bne + + inc res+$0a ++ 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..abf372da --- /dev/null +++ b/tools/bench/regsweep.py @@ -0,0 +1,74 @@ +#!/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, 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): + 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] & 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') + + +if __name__ == '__main__': + main()