Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] '<text>'` | 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. |
Expand Down
2 changes: 2 additions & 0 deletions tools/bench/c64.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 50 additions & 0 deletions tools/bench/type.py
Original file line number Diff line number Diff line change
@@ -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] '<text>'

`\\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()
67 changes: 67 additions & 0 deletions tools/c128/README.md
Original file line number Diff line number Diff line change
@@ -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] <geoboot128> <out.crt> [name]` | Build a C128 `.crt` that boots GEOS 128 v2 from its boot disk's GEOBOOT128 (below). Also writes the ROM as `<out>.bin`. |
| `iecdecode.py <log> [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 "<before>" "<after>"` | 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
Loading
Loading