diff --git a/tools/bench/README.md b/tools/bench/README.md index 2e5844ec5..928e0ecbd 100644 --- a/tools/bench/README.md +++ b/tools/bench/README.md @@ -51,6 +51,7 @@ whatever node appeared beside it. | `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`). | +| `type.py [--c128] ''` | Type a line into the keyboard buffer, ten keys at a time; `\n` is RETURN. `--c128` for a C128 in 128 mode. | | `colors.py` | Top row, its colour RAM and the VIC colour registers. | | `listen.py [secs] [hex]` | Echo serial output, optionally sending bytes first. | | `probe.py` | Which image is running -- main, minimal or silent -- and its build banner. | diff --git a/tools/bench/c64.py b/tools/bench/c64.py index 2ca423ed2..1db07c2e6 100644 --- a/tools/bench/c64.py +++ b/tools/bench/c64.py @@ -13,6 +13,8 @@ VIC_DEN = 0x10 KEYBUF, KEYCOUNT = 0x0277, 0x00C6 # KERNAL keyboard buffer, and the count GETIN reads +KEYBUF_128, KEYCOUNT_128 = 0x034A, 0x00D0 # the same pair in C128 mode +KEYBUF_SIZE = 10 PETSCII_U, PETSCII_Y = 0x55, 0x59 F1, F3, F5, F7, F8 = 0x85, 0x86, 0x87, 0x88, 0x8C diff --git a/tools/bench/type.py b/tools/bench/type.py new file mode 100755 index 000000000..e8c212fee --- /dev/null +++ b/tools/bench/type.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Type a line into the KERNAL keyboard buffer by DMA, a buffer-full at a time. + + type.py [--c128] '' + +`\\n` is RETURN and `\\xNN` is any key code. Letters go in unshifted (uppercase on the +power-on character set). +--c128 uses the C128-mode buffer; C64 mode, including a C128 in 64 mode, is the default. +""" +import sys +import time +from c64 import KEYBUF, KEYBUF_128, KEYBUF_SIZE, KEYCOUNT, KEYCOUNT_128 +from trlink import Link + +DRAIN_TIMEOUT = 30 + + +def main(): + args = sys.argv[1:] + c128 = '--c128' in args + args = [a for a in args if a != '--c128'] + if len(args) != 1: + sys.exit(__doc__.strip()) + keybuf, keycount = (KEYBUF_128, KEYCOUNT_128) if c128 else (KEYBUF, KEYCOUNT) + if not args[0].isascii(): + sys.exit('ASCII only: write any other key code as \\xNN') + try: + data = args[0].encode().decode('unicode_escape').encode('latin1') + except UnicodeEncodeError: + sys.exit('key codes go up to \\xFF') + data = data.upper().replace(b'\n', b'\r') + with Link() as tr: + for i in range(0, len(data), KEYBUF_SIZE): + wait_until_read(tr, keycount, i) + chunk = data[i:i + KEYBUF_SIZE] + if not (tr.poke(keybuf, chunk) and tr.poke(keycount, [len(chunk)])): + sys.exit(f'poke refused at character {i}') + wait_until_read(tr, keycount, len(data)) + + +def wait_until_read(tr, keycount, at): + start = time.time() + while tr.peek(keycount, 1)[0]: + if time.time() - start > DRAIN_TIMEOUT: + sys.exit(f'keyboard buffer not read for {DRAIN_TIMEOUT} s, at character {at}') + time.sleep(0.1) + + +main() diff --git a/tools/c128/README.md b/tools/c128/README.md new file mode 100644 index 000000000..05f746edc --- /dev/null +++ b/tools/c128/README.md @@ -0,0 +1,67 @@ +# C128 tools + +Scripts for running C128 cartridges on the TR and for finding out why one does not. +Python 3, standard library only. + +| Script | Does | +|--------|------| +| `geos128crt.py [--clock T] [name]` | Build a C128 `.crt` that boots GEOS 128 v2 from its boot disk's GEOBOOT128 (below). Also writes the ROM as `.bin`. | +| `iecdecode.py [prefix]` | Decode the drive commands in a VICE trace of the KERNAL serial entry points: each M-W, M-E and command string, with sd2iec's fast-loader CRC. With a prefix, dumps the drive RAM each M-E ran. | +| `vicemon.py "" ""` | Run VICE remote-monitor commands, resume, and run more once a breakpoint stops it, on one connection. | + +The C128 needs TR firmware that maps a 16 KB + 16 KB C128 `.crt` +(`c128-crt-load-address`). GEOS is copyrighted, so it is not here; build the `.crt` from +your own boot disk and launch it from SD. + +## The GEOS 128 crt + +The cart does what a disk boot does, without the disk's boot loader: it unpacks GEOBOOT128 +into RAM bank 1 at `$2B19`, where the boot loader would have loaded it, and enters it at +`$2F3C` with the setup GEOS's own ROM loader uses. GEOS then reads the rest of itself from drive 8, so the boot disk still has to be +there, and its configuration decides the RAM expansion: the stock GEOS 128 2.0 GEOBOOT for an +REU, or the GEOBOOT and `128 CONFIG 2.0R` from GEOS 2.0r's GeoRAM disk (`GR81128`) with the +TR's Special IO set to GeoRAM. Take the file off the disk with VICE's `c1541`: + + c1541 boot.d81 -read geoboot128 geoboot128.prg + python3 tools/c128/geos128crt.py geoboot128.prg geos128.crt + +GEOBOOT128 is packed (an LZ format of the tool's own, unpacked by the stub) into the cart's +32 KB from `$8300`. The C128 calls a function ROM's cold start with IRQs enabled and the +ROM's top page mapped over the CPU vectors, so the cold start masks IRQs first, and `$FFF9` +holds an `RTI` that the NMI vector points to; the IRQ vector points at the cold start. The stub runs from `$1300` with the bottom 8 KB +common to both banks, reading the ROM with `$FF00 = $2B` and writing bank 1 with `$7F`. + +GEOS keeps a RAM reset key in bank 1: `CBM` at `$FFF5` and a vector to `$03E4`. The C128 +KERNAL jumps through it on every reset into C128 mode, before it initializes anything, and +it survives every reset. The TR menu runs in C64 mode, which reuses +`$03E4`, so after GEOS the next C128 crt of any kind crashed at reset. The tool breaks the +`C` in GEOBOOT128 (`$6FF5`), as GEOS's own ROM build does. + +## The boot clock + +GEOS 128 handles years past 1999 itself: it keeps a two-digit year, rolls 12/31/99 over to +01/01/00, and its leap-year test holds through 2099. What it lacks is a clock source. A cold +boot sets CIA1's time-of-day clock to 1:00 PM and the date to 3/23/92, as immediate operands +in GEOBOOT128 (`$2C29`). + +Once GEOBOOT128 is unpacked, the stub maps the cartridge in (`$FF00 = $6A`) and runs a +routine at `$8200`. It reads a clock record, sets the TOD from it, writes its year, month and +day over GEOS's defaults, and jumps GEOS past its own TOD set. If GEOS's defaults are not the +expected bytes, it leaves them alone. + +The record sits at `$81F0`: the tag `TRCLOCK`, a version byte `$01`, then century, year +(0–99), month, day, hour (0–23), minute and second. The tool fills it with the time the +`.crt` was built, or `--clock`. TR firmware with C128 crt clock support (`c128-crt-clock`) +finds the record by its tag and keeps it at local time while the cart runs, so GEOS boots +with the TR's clock. The routine reads the record until two reads agree, since the TR can +rewrite it mid-read. + +## Tracing drive commands in VICE + +The headless VICE build (`--enable-headlessui`) runs without a window. It cannot save +screenshots, so read results from memory through the monitor instead (`bsave`, or `m`). + + x128 -default -80col -drive8type 1581 -8 disk.d81 -cartcrt geos.crt \ + -remotemonitor -monlog -monlogname iec.log -limitcycles 2000000000 & + printf 'trace exec ffb1\ntrace exec ff93\ntrace exec ffa8\ntrace exec ffae\nx\n' | nc 127.0.0.1 6510 + python3 tools/c128/iecdecode.py iec.log diff --git a/tools/c128/geos128crt.py b/tools/c128/geos128crt.py new file mode 100755 index 000000000..8baa8371a --- /dev/null +++ b/tools/c128/geos128crt.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Build a C128 .crt the TR can launch that boots GEOS 128 v2 the way its boot disk does. + + geos128crt.py [--clock YYYY-MM-DDTHH:MM[:SS]] [name] + + is the GEOBOOT128 file from a GEOS 128 v2 boot disk, e.g. +`c1541 boot.d81 -read geoboot128 geoboot128.prg`. The cart unpacks it into RAM bank 1 and +starts it, and GEOS loads the rest from drive 8 as a disk boot does. GEOS boots with the date +and time in the crt's clock record: the time it was built, or --clock. TR firmware with C128 +crt clock support keeps it at local time while the cart runs. +Also writes the raw ROM next to the .crt as .bin. +""" +import argparse +import datetime +import os +import struct +import sys + +LOAD, ENTRY = 0x2B19, 0x2F3C # GEOBOOT128's load address, and where GEOS's ROM loader enters it +SIZE = 38226 +DEFAULTS = 0x2C29 # GEOS's first-boot TOD and date, operands of BOOT4 below +RESET_KEY = 0x6FF5 # unpacks to bank 1 $FFF5 with the rest of GEOS's top page +STUB_RAM, STUB_ROM = 0x1300, 0x8021 +RECORD, CLOCK_ADDR, DATA = 0x81F0, 0x8200, 0x8300 # baked into the stub and CLOCK operands +CLOCK_SCRATCH = 0x1B00 +VECTORS, COLD = 0xFFF9, 0x800A + +# The TR finds this record by its tag and keeps the seven bytes after it at local time. +CLOCK_TAG = b'TRCLOCK\x01' + +# GEOS's first-boot defaults at DEFAULTS: CIA1 TOD 1:00 PM, then 3/23/92. +BOOT4 = bytes.fromhex('ad0fdc297f8d0fdc' 'a9818d0bdc' 'a9008d0adc8d09dc8d08dc' + 'a95c8d1685' 'a9038d1785' 'a9178d1885') + +STUB = bytes.fromhex( # $8000, a C128 function ROM; copies $8021 on to $1300 + 'ea' # 8000 nop + 'ea' # 8001 nop + 'ea' # 8002 nop + '4c0a80' # 8003 jmp cold + 'ff' # 8006 !byte $ff + '43424d' # 8007 !text "CBM" + '78' # 800a cold sei IRQs are on, and this ROM's top page is the CPU vectors + 'ad00ff' # 800b lda $ff00 + '293f' # 800e and #$3f + '8d00ff' # 8010 sta $ff00 + 'a200' # 8013 ldx #0 + 'bd2180' # 8015 - lda stubrom,x + '9d0013' # 8018 sta $1300,x + 'e8' # 801b inx + 'd0f7' # 801c bne - + '4c0013' # 801e jmp $1300 + '78' # 1300 sei + 'a93e' # 1301 lda #$3e + '8d00ff' # 1303 sta $ff00 + 'a906' # 1306 lda #$06 + '8d06d5' # 1308 sta $d506 + 'a900' # 130b lda #0 + '8d11d0' # 130d sta $d011 + 'a900' # 1310 lda #DATA + '8503' # 1316 sta src+1 + 'a919' # 1318 lda #$19 + '8504' # 131a sta dst + 'a92b' # 131c lda #$2b + '8505' # 131e sta dst+1 + 'a97f' # 1320 lda #$7f + '8d00ff' # 1322 sta $ff00 + '208513' # 1325 next jsr get + 'f03d' # 1328 beq done + '300c' # 132a bmi copy + 'aa' # 132c tax + '208513' # 132d - jsr get + '209c13' # 1330 jsr put + 'ca' # 1333 dex + 'd0f7' # 1334 bne - + 'f0ed' # 1336 beq next + '297f' # 1338 copy and #$7f + '18' # 133a clc + '6903' # 133b adc #3 + 'aa' # 133d tax + '208513' # 133e jsr get + '8508' # 1341 sta tmp + '208513' # 1343 jsr get + '8509' # 1346 sta tmp+1 + '38' # 1348 sec + 'a504' # 1349 lda dst + 'e508' # 134b sbc tmp + '8506' # 134d sta ref + 'a505' # 134f lda dst+1 + 'e509' # 1351 sbc tmp+1 + '8507' # 1353 sta ref+1 + 'a000' # 1355 - ldy #0 + 'b106' # 1357 lda (ref),y + '209c13' # 1359 jsr put + 'e606' # 135c inc ref + 'd002' # 135e bne + + 'e607' # 1360 inc ref+1 + 'ca' # 1362 + dex + 'd0f0' # 1363 bne - + 'f0be' # 1365 beq next + 'a96a' # 1367 done lda #$6a + '8d00ff' # 1369 sta $ff00 + '200082' # 136c jsr CLOCK + 'a97e' # 136f lda #$7e + '8d00ff' # 1371 sta $ff00 + 'a91b' # 1374 lda #$1b + '8d11d0' # 1376 sta $d011 + 'a900' # 1379 lda #0 + '8d30d0' # 137b sta $d030 + 'a908' # 137e lda #8 + '85ba' # 1380 sta $ba + '4c3c2f' # 1382 jmp $2f3c + 'a92b' # 1385 get lda #$2b + '8d00ff' # 1387 sta $ff00 + 'a000' # 138a ldy #0 + 'b102' # 138c lda (src),y + 'a07f' # 138e ldy #$7f + '8c00ff' # 1390 sty $ff00 + 'e602' # 1393 inc src + 'd002' # 1395 bne + + 'e603' # 1397 inc src+1 + '48' # 1399 + pha + '68' # 139a pla + '60' # 139b rts + 'a000' # 139c put ldy #0 + '9104' # 139e sta (dst),y + 'e604' # 13a0 inc dst + 'd002' # 13a2 bne + + 'e605' # 13a4 inc dst+1 + '60') # 13a6 + rts + +CLOCK = bytes.fromhex( # $8200, entered with $FF00 = $6A + 'a206' # 8200 CLOCK ldx #6 + 'bdf881' # 8202 - lda REC,x + '9d001b' # 8205 sta SCR,x + 'ca' # 8208 dex + '10f7' # 8209 bpl - + 'a206' # 820b ldx #6 + 'bdf881' # 820d - lda REC,x + 'dd001b' # 8210 cmp SCR,x + 'd0eb' # 8213 bne CLOCK + 'ca' # 8215 dex + '10f5' # 8216 bpl - + 'a226' # 8218 ldx #38 + 'bd292c' # 821a - lda DEF,x + 'dda282' # 821d cmp BOOT4,x + 'd069' # 8220 bne out + 'ca' # 8222 dex + '10f5' # 8223 bpl - + 'ad011b' # 8225 lda SCR+1 + '8d422c' # 8228 sta DEF+$19 + 'ad021b' # 822b lda SCR+2 + '8d472c' # 822e sta DEF+$1e + 'ad031b' # 8231 lda SCR+3 + '8d4c2c' # 8234 sta DEF+$23 + 'a94c' # 8237 lda #$4c + '8d292c' # 8239 sta DEF + 'a941' # 823c lda #<(DEF+$18) + '8d2a2c' # 823e sta DEF+1 + 'a92c' # 8241 lda #>(DEF+$18) + '8d2b2c' # 8243 sta DEF+2 + 'ad0fdc' # 8246 lda $dc0f + '297f' # 8249 and #$7f + '8d0fdc' # 824b sta $dc0f + 'a000' # 824e ldy #0 + 'ad041b' # 8250 lda SCR+4 + 'c90c' # 8253 cmp #12 + '9004' # 8255 bcc + + 'e90c' # 8257 sbc #12 + 'a080' # 8259 ldy #$80 + '8c071b' # 825b + sty SCR+7 + 'aa' # 825e tax + 'd00a' # 825f bne + + 'ad071b' # 8261 lda SCR+7 + '4980' # 8264 eor #$80 + '8d071b' # 8266 sta SCR+7 + 'a90c' # 8269 lda #12 + '208c82' # 826b + jsr bcd + '0d071b' # 826e ora SCR+7 + '8d0bdc' # 8271 sta $dc0b + 'ad051b' # 8274 lda SCR+5 + '208c82' # 8277 jsr bcd + '8d0adc' # 827a sta $dc0a + 'ad061b' # 827d lda SCR+6 + '208c82' # 8280 jsr bcd + '8d09dc' # 8283 sta $dc09 + 'a900' # 8286 lda #0 + '8d08dc' # 8288 sta $dc08 + '60' # 828b out rts + 'a2ff' # 828c bcd ldx #$ff + '38' # 828e sec + 'e8' # 828f - inx + 'e90a' # 8290 sbc #10 + 'b0fb' # 8292 bcs - + '690a' # 8294 adc #10 + '8d081b' # 8296 sta SCR+8 + '8a' # 8299 txa + '0a' # 829a asl + '0a' # 829b asl + '0a' # 829c asl + '0a' # 829d asl + '0d081b' # 829e ora SCR+8 + '60' # 82a1 rts +) + BOOT4 # 82a2 + +# Addresses the code above has baked in, as (code, offset of the operand, what it must be). +GET, PUT, BCD = STUB_RAM + 0x85, STUB_RAM + 0x9C, CLOCK_ADDR + 0x8C +OPERANDS = ((STUB, 0x04, COLD), (STUB, 0x16, STUB_ROM), (STUB, 0x19, STUB_RAM), (STUB, 0x1F, STUB_RAM), + (STUB, 0x47, GET), (STUB, 0x4F, GET), (STUB, 0x52, PUT), (STUB, 0x60, GET), + (STUB, 0x65, GET), (STUB, 0x7B, PUT), (STUB, 0x8E, CLOCK_ADDR), (STUB, 0xA4, ENTRY), + (CLOCK, 0x03, RECORD + len(CLOCK_TAG)), (CLOCK, 0x06, CLOCK_SCRATCH), + (CLOCK, 0x0E, RECORD + len(CLOCK_TAG)), (CLOCK, 0x11, CLOCK_SCRATCH), + (CLOCK, 0x1B, DEFAULTS), (CLOCK, 0x1E, CLOCK_ADDR + len(CLOCK) - len(BOOT4)), + (CLOCK, 0x26, CLOCK_SCRATCH + 1), (CLOCK, 0x29, DEFAULTS + 0x19), + (CLOCK, 0x2C, CLOCK_SCRATCH + 2), (CLOCK, 0x2F, DEFAULTS + 0x1E), + (CLOCK, 0x32, CLOCK_SCRATCH + 3), (CLOCK, 0x35, DEFAULTS + 0x23), + (CLOCK, 0x3A, DEFAULTS), (CLOCK, 0x3F, DEFAULTS + 1), (CLOCK, 0x44, DEFAULTS + 2), + (CLOCK, 0x51, CLOCK_SCRATCH + 4), (CLOCK, 0x5C, CLOCK_SCRATCH + 7), + (CLOCK, 0x62, CLOCK_SCRATCH + 7), (CLOCK, 0x67, CLOCK_SCRATCH + 7), + (CLOCK, 0x6C, BCD), (CLOCK, 0x6F, CLOCK_SCRATCH + 7), (CLOCK, 0x75, CLOCK_SCRATCH + 5), + (CLOCK, 0x78, BCD), (CLOCK, 0x7E, CLOCK_SCRATCH + 6), (CLOCK, 0x81, BCD), + (CLOCK, 0x97, CLOCK_SCRATCH + 8), (CLOCK, 0x9F, CLOCK_SCRATCH + 8)) +LZ_MIN, LZ_MAX_COPY, LZ_MAX_RUN = 3, 127 + 3, 127 + + +def lz_pack(data): + """The stub's format. A control byte of 0 ends; 1-127 is a run of that many literal bytes; + $80 + n copies n + 3 bytes from the distance in the two bytes that follow (lo, hi).""" + out, lit, seen = bytearray(), bytearray(), {} + + def flush(): + while lit: + n = min(len(lit), LZ_MAX_RUN) + out.append(n) + out.extend(lit[:n]) + del lit[:n] + + i = 0 + while i < len(data): + best, dist = 0, 0 + for j in reversed(seen.get(data[i:i + LZ_MIN], [])[-256:]): + n = 0 + while i + n < len(data) and n < LZ_MAX_COPY and data[j + n] == data[i + n]: + n += 1 + if n > best: + best, dist = n, i - j + if n == LZ_MAX_COPY: + break + if best >= 4 or (best == LZ_MIN and not lit): + flush() + out += bytes([0x80 | (best - LZ_MIN), dist & 0xFF, dist >> 8]) + for k in range(i, i + best): + seen.setdefault(data[k:k + LZ_MIN], []).append(k) + i += best + else: + seen.setdefault(data[i:i + LZ_MIN], []).append(i) + lit.append(data[i]) + i += 1 + flush() + out.append(0) + return bytes(out) + + +def lz_unpack(packed): + out, i = bytearray(), 0 + while packed[i]: + n = packed[i] + if n < 0x80: + out += packed[i + 1:i + 1 + n] + i += 1 + n + else: + dist = packed[i + 1] | packed[i + 2] << 8 + for _ in range((n & 0x7F) + LZ_MIN): + out.append(out[-dist]) + i += 3 + return bytes(out) + + +def expect(image, addr, want, what): + at = addr - LOAD + if image[at:at + len(want)] != want: + sys.exit(f'{what}: expected {want.hex()} at ${addr:04X}, found ' + f'{image[at:at + len(want)].hex()} -- not a GEOS 128 v2 GEOBOOT128?') + + +def break_reset_key(image): + # GEOS puts "CBM" and $03E4 at bank 1 $FFF5, and the C128 KERNAL jumps through that on + # every C128-mode reset until the power goes off. The TR menu boots in C64 mode, which + # overwrites $03E4, so the next C128 crt crashes. GEOS's own ROM build breaks it too. + expect(image, RESET_KEY, b'CBM' + struct.pack(' 0xFF00: # the MMU registers at $FF00 hide the ROM from there up + sys.exit(f'GEOBOOT128 packs to {len(packed)} bytes, {DATA + len(packed) - 0xFF00} too many') + # An IRQ that arrives before the cold start's SEI is still asserted when an RTI returns, + # and the 6502 takes it again before the next instruction, so IRQ enters the cold start. + rom = bytearray(b'\xff' * 0x8000) + for addr, data in ((0x8000, STUB), (RECORD, clock_record(t)), (CLOCK_ADDR, CLOCK), + (DATA, packed), (VECTORS, b'\x40' + struct.pack('IHHBBB', 0x40, 0x0100, 0, 1, 1, 0) + + b'\0' * 5 + name.encode()[:32].ljust(32, b'\0')) + chips = b'' + for i, load in enumerate((0x8000, 0xC000)): + chips += (b'CHIP' + struct.pack('>IHHHH', 0x4010, 0, 0, load, 0x4000) + + rom[i * 0x4000:(i + 1) * 0x4000]) + return header + chips + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.strip().splitlines()[0]) + ap.add_argument('--clock', type=datetime.datetime.fromisoformat, + help='date and time GEOS boots with when the TR does not set it ' + '(default: now)') + ap.add_argument('geoboot') + ap.add_argument('out') + ap.add_argument('name', nargs='?', default='GEOS 128') + args = ap.parse_args() + raw = os.path.splitext(args.out)[0] + '.bin' + if os.path.abspath(raw) == os.path.abspath(args.out): + sys.exit(f'{args.out}: the ROM is written as .bin, so the .crt cannot be a .bin') + prg = open(args.geoboot, 'rb').read() + if len(prg) != 2 + SIZE or prg[:2] != struct.pack(' [image-prefix] + +Prints each command channel string (M-W, M-E and the rest) with the cycle it was sent on. +For M-W it also prints sd2iec's running CRC, which is what sd2iec matches against its table +of known fast loaders (doscmd.c, fl_crc_table) when the M-E arrives. With an image prefix, +writes the $0300-$04FF drive RAM image each M-E ran as _.bin. + +Make the log with VICE's monitor: `trace exec ffb1`, `trace exec ff93`, `trace exec ffa8` +and `trace exec ffae` (LISTEN, SECOND, CIOUT, UNLSN), then `x`, with -monlog / -monlogname +set. Only bytes sent after a SECOND to channel 15 are decoded; file data is skipped. +""" +import re +import sys + +LISTEN, SECOND, CIOUT, UNLSN = 'ffb1', 'ff93', 'ffa8', 'ffae' +COMMAND_CHANNEL = 15 +SA_DATA, SA_OPEN = 0x60, 0xF0 +TRACE_HIT = re.compile(r'\.C:(ff\w\w)\s+4C .*A:(\w\w).*\s(\d+)$') +UPLOAD_BASE, UPLOAD_SIZE = 0x0300, 0x200 + + +def crc16(crc, byte): + # avr-libc _crc16_update, which sd2iec uses: poly 0xA001, reflected + crc ^= byte + for _ in range(8): + crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 + return crc + + +def commands(path): + cmds, cur = [], None + for line in open(path, errors='replace'): + m = TRACE_HIT.match(line.strip()) + if not m: + continue + entry, acc, cycle = m.group(1), int(m.group(2), 16), int(m.group(3)) + if entry == LISTEN: + cur = None + elif entry == SECOND: + to_command_channel = (acc & 0x0F == COMMAND_CHANNEL + and acc & 0xF0 in (SA_DATA, SA_OPEN)) + cur = (cycle, bytearray()) if to_command_channel else None + elif entry == CIOUT and cur is not None: + cur[1].append(acc) + elif entry == UNLSN and cur is not None: + cmds.append((cur[0], bytes(cur[1]))) + cur = None + return cmds + + +def main(): + if len(sys.argv) not in (2, 3): + sys.exit(__doc__.strip()) + crc, ram, images = 0xFFFF, bytearray(UPLOAD_SIZE), [] + for cycle, cmd in commands(sys.argv[1]): + if cmd[:3] == b'M-W' and len(cmd) >= 6: + addr, n = cmd[3] | cmd[4] << 8, cmd[5] + payload = cmd[6:6 + n] + for b in payload: + crc = crc16(crc, b) + if UPLOAD_BASE <= addr and addr + len(payload) <= UPLOAD_BASE + UPLOAD_SIZE: + ram[addr - UPLOAD_BASE:addr - UPLOAD_BASE + len(payload)] = payload + short = f' (only {len(payload)} sent)' if len(payload) < n else '' + print(f'{cycle:>10} M-W ${addr:04x} len {n:3}{short} crc-so-far {crc:04x}') + elif cmd[:3] == b'M-E' and len(cmd) >= 5: + print(f'{cycle:>10} M-E ${cmd[3] | cmd[4] << 8:04x} (crc {crc:04x})') + images.append(bytes(ram)) + crc = 0xFFFF + else: + print(f'{cycle:>10} {cmd[:40]!r}') + if len(sys.argv) == 3: + for i, image in enumerate(images): + open(f'{sys.argv[2]}_{i}.bin', 'wb').write(image) + + +main() diff --git a/tools/c128/vicemon.py b/tools/c128/vicemon.py new file mode 100755 index 000000000..ae718a002 --- /dev/null +++ b/tools/c128/vicemon.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Run VICE remote-monitor commands across a breakpoint, on one connection. + + vicemon.py "" "" + +Commands are separated by ';'. The first set runs, then `x` resumes the emulator; once the +monitor prompt comes back (a breakpoint or watchpoint hit), the second set runs. Start +VICE with -remotemonitor (127.0.0.1:6510, or $VICE_MONITOR as host or host:port). +""" +import os +import re +import socket +import sys + +PROMPT = re.compile(rb'\(C:\$[0-9a-fA-F]{4}\) $') +COMMAND_TIMEOUT = 30 +GREETING_WAIT = 0.5 +DEFAULT_HOST, DEFAULT_PORT = '127.0.0.1', 6510 + + +def main(): + if len(sys.argv) != 3: + sys.exit(__doc__.strip()) + host, _, port = os.environ.get('VICE_MONITOR', DEFAULT_HOST).partition(':') + s = socket.create_connection((host or DEFAULT_HOST, int(port) if port else DEFAULT_PORT)) + + def send(cmd): + s.sendall((cmd + '\n').encode()) + + def until_prompt(timeout, what): + s.settimeout(timeout) + out = b'' + try: + while not PROMPT.search(out): + data = s.recv(65536) + if not data: + sys.exit(f'VICE closed the monitor connection {what}') + out += data + except socket.timeout: + sys.exit(f'no monitor prompt within {timeout} s {what}') + return out.decode('latin1') + + def run(cmds, show): + for cmd in commands(cmds): + send(cmd) + out = until_prompt(COMMAND_TIMEOUT, f'after {cmd!r}') + if show: + print(out) + + s.settimeout(GREETING_WAIT) + try: + s.recv(65536) + except socket.timeout: + pass + run(sys.argv[1], show=False) + send('x') + print(until_prompt(None, 'before stopping')) + run(sys.argv[2], show=True) + + +def commands(cmds): + return [c.strip() for c in cmds.split(';') if c.strip()] + + +main()