Events¶
The event is what a rule waits for. Each one has a filter (which addresses, ports, registers, lines…), a delivery point (the instant the rule runs), and a payload: upper-case names that are valid only inside that rule and describe this occurrence of the event.
| Event | Fires on |
|---|---|
execute addr |
an instruction about to execute |
read addr, write addr |
a memory read or write |
io_read port, io_write port |
a port read or write |
nextreg reg |
a NextREG write, by the CPU or the Copper |
frame [n] |
the end of a frame |
scanline n |
the start of a line |
cycle n |
the first instruction boundary at or after master cycle n |
interrupt |
an accepted maskable interrupt |
nmi |
an NMI |
reset |
a hard or soft reset |
hostkey n |
host key n, 1 to 8 |
copper move / wait / halt |
a Copper instruction |
dma start / byte / end |
a DMA transfer |
stop |
any pause of the machine |
When a rule runs¶
Everything a script does happens with the machine stopped at an instruction boundary. No rule ever runs in the middle of a memory write or a port access; that is why a script can look at anything without disturbing it.
| Event | The rule runs |
|---|---|
execute, cycle, hostkey |
before the instruction at PC |
read, write, io_read, io_write, DMA, Copper |
after the instruction that caused it |
nextreg by the Copper or the DMA |
after the instruction during which it happened |
nextreg by the CPU |
one instruction later: a CPU NextREG write commits after its instruction's events are delivered. PC still names the instruction that wrote it |
interrupt, nmi, reset |
at the boundary where it happened |
frame |
at the frame edge, before the next frame's first instruction |
scanline |
at the first instruction boundary after the line began — at most one instruction late |
stop |
when the machine has paused |
PC in a rule is the event's PC: the instruction that caused it, not where
the CPU is now. For read/write the CPU has already moved on; for frame it
is the instruction during which the frame ended. dump_regs shows the live
registers, so the two can differ:
[debugger] [info] [jds F:20 C:11741208] caught my own stop: boom, PC 11DC, loaded at FRAME 0
CYCLE in a rule is the event's cycle too, and so is the C: of every log
line the rule writes. The other state names (FRAME, the raster counters,
TFRAME, the registers, memory) read the machine as it is when the rule runs.
Order. Several events at one boundary are delivered in the order they
happened, and the rules on one event in file order. Up to 512 events can be
delivered at one boundary; if more happen (a Copper program busy through a
long LDIR, say) the first 512 are delivered and the run logs SCRIPT: event
ring overflowed at CYCLE …, N events dropped — a script cannot see the
dropped ones, and it is told so rather than left to guess.
Address filters: execute, read, write¶
on execute 0x0038 do … ; one address
on write 0x4000..0x57FF do … ; an inclusive range
on write @buffer..(@buffer + 255) do … ; symbols and expressions
on write 0x5C00..0x5CFF page 0x0A do … ; …and only when the slot holds page 0x0A
on write page 0x0A..0x0B do … ; any address, while in pages 0x0A..0x0B
on execute page 0x00 do … ; any instruction in page 0x00
on execute main.c:19 do … ; a source line (needs a source map)
on execute "my dir/main.c:19" do … ; the same, for a name with spaces
An execute rule on a source line fires at every place the line's code
starts, each in the memory it was built for (a line the compiler placed twice
is two places). It takes no range and no page, and a line that is not in the
loaded source map, or that has no code, is a load error that names it. It is
the same line the debugger's Breakpoints panel takes.
An address is a 16-bit CPU address (0 to 0xFFFF). page is a physical 8K
page — the one the MMU has mapped behind the address at the time of the access
— so a filter can follow one piece of memory wherever it is paged. With an
address range, page narrows it ("this range, while it holds this page");
alone, it means every address of that page. An execute page range may name
up to 16 pages.
Payload:
| Name | execute |
read |
write |
Meaning |
|---|---|---|---|---|
ADDR |
✓ | ✓ | ✓ | the address |
PAGE |
✓ | ✓ | ✓ | the physical 8K page behind ADDR |
VALUE |
✓ | ✓ | the byte read or written | |
PREV |
✓ | the byte before the write | ||
SOURCE |
✓ | ✓ | who did it: CPU, DMA or COPPER |
|
PC |
✓ | ✓ | ✓ | the instruction |
One caveat on PAGE: the DivMMC, Multiface and Layer 2 overlays sit in front
of the MMU, and a write one of them captures is still seen (which is what you
want when guarding memory) but PAGE names the MMU slot's page, not the
overlay's.
Port filters: io_read, io_write¶
on io_write 0xFE do … ; low byte 0xFE: any port xxFE
on io_read 0x7FFE do … ; above 0xFF: exactly this port
on io_write 0x10..0x20 do … ; a range of low bytes
on io_read mask 0x00FF value 0xFE do … ; mask and value, as written
A port written 0x00 to 0xFF is matched on its low byte, like the hardware decodes it, whatever the high byte is; a port above 0xFF is matched exactly. A range decodes like a single port, so it must lie wholly in 0x00..0xFF or wholly above it:
[debugger] [error] SCRIPT ERROR errs2.jds:1:13: a port range lies wholly in 0x00..0xFF (decoded on the low byte) or wholly above it
With mask M value V a port matches when port & M == V.
Payload: PORT (the full 16-bit port), VALUE, SOURCE (a DMA byte to a
port is an io_write with SOURCE == DMA), PC.
on io_read mask 0x00FF value 0xFE when FRAME == 120 do
log "mask/value form: ${PORT:x4} -> ${VALUE:x2}"
end
on io_read 0x7FFE when FRAME == 120 do
log "a port above 0xFF is matched exactly: ${PORT:x4}"
end
[debugger] [info] [jds F:120 C:67094464] mask/value form: FEFE -> BF
[debugger] [info] [jds F:120 C:67094936] mask/value form: FDFE -> BF
…
[debugger] [info] [jds F:120 C:67097768] mask/value form: 7FFE -> BF
[debugger] [info] [jds F:120 C:67097768] a port above 0xFF is matched exactly: 7FFE
That is the 48K ROM scanning the eight keyboard half-rows.
nextreg¶
on nextreg 0x51 do …
on nextreg 0x50..0x57 do …
A write to a NextREG, by the CPU (NEXTREG or port 0x253B) or by the Copper.
When the rule runs, the write has happened: nextreg[REG] already reads
VALUE.
Payload: REG, VALUE, PREV (the register before the write), SOURCE,
PC.
on nextreg 0x07 once do
log "nextreg: NR ${REG:x2} ${PREV:x2} -> ${VALUE:x2} by PC ${PC:x4} (SOURCE ${SOURCE})"
end
[debugger] [info] [jds F:23 C:13427480] nextreg: NR 07 00 -> 03 by PC 6B07 (SOURCE 0)
(jnext --headless --machine next: the boot firmware setting 28 MHz.)
frame, scanline, cycle¶
on frame do … ; every frame
on frame 100 do … ; when FRAME is 100
on scanline 96 do … ; when line 96 begins, every frame
on cycle 50000000 do … ; once, at the first boundary at or after that cycle
frameruns at the frame edge.FRAMEcounts frames since power-on, the first being 0, and a hard reset starts it again at 0, so anon frame Nrule fires again after one.scanline Nruns once per frame as line N begins, N being the line number a Next programmer uses:CVC, the counter NextREGs 0x1E/0x1F read, the line interrupt (NR 0x22/0x23) and the Copper'sWAITcompare against. Line 0 is the first line of the 256×192 paper. N is 0 to 1023.cycle Nruns once, at the first instruction boundary at whichCYCLEis N or more.CYCLEis the 28 MHz master clock since power-on (8 cycles per T-state at 3.5 MHz).
Payload: PC.
on scanline 100 when FRAME == 50 do
log "scanline 100: CVC=${CVC} RAW_VC=${RAW_VC} RAW_HC=${RAW_HC} TFRAME=${TFRAME}"
end
on cycle 50000000 do
log "cycle: CYCLE=${CYCLE} FRAME=${FRAME} TFRAME=${TFRAME}"
end
on frame 99 do
log "frame: FRAME=${FRAME}, PC ${PC:x4}"
end
[debugger] [info] [jds F:50 C:28249088] scanline 100: CVC=99 RAW_VC=164 RAW_HC=20 TFRAME=293968
[debugger] [info] [jds F:89 C:50000056] cycle: CYCLE=50000056 FRAME=89 TFRAME=239800
[debugger] [info] [jds F:99 C:55910456] frame: FRAME=99, PC 15ED
Note CVC=99 in the scanline 100 rule. The rule runs as the raw line begins
(raw line 164 on the 48K: 64 lines of border and blanking come first), and
CVC — exactly as a program reading NR 0x1E/0x1F would see it — steps to 100 a
few pixels into that line. RAW_HC shows how far in the beam is. See
Time and the raster.
interrupt, nmi, reset¶
on interrupt do … ; a maskable interrupt accepted (IM 0, 1 or 2)
on nmi do … ; an NMI: the Multiface or DivMMC button, or NR 0x02
on reset do … ; a hard or a soft reset
Payload: PC — for interrupt and nmi, the instruction that was
interrupted.
on interrupt once do
log "interrupt: accepted at PC ${PC:x4}, IM ${IM}, I=${I:x2}"
end
on execute 0x0038 once do
log "execute: IM 1 handler entered, ADDR=${ADDR:x4} PAGE=${PAGE:x2} SP=${SP:x4} return to ${stack[0]:x4}"
end
[debugger] [info] [jds F:83 C:46406264] interrupt: accepted at PC 0E5C, IM 1, I=3F
[debugger] [info] [jds F:83 C:46406368] execute: IM 1 handler entered, ADDR=0038 PAGE=00 SP=FF48 return to 0E5C
An NMI from the Multiface button (--delayed-nmi-frames 600 nmi, on the Next):
[debugger] [info] MUTATE nmi button MF pressed by 0
[debugger] [info] [jds F:600 C:340358408] nmi: taken at PC 0C8F
A reset: the Next's firmware resets the machine itself at frame 310 of a cold boot, and a script can cause a hard reset by writing NR 0x02 (see Changing the machine):
on reset do
log "reset at PC ${PC:x4}, FRAME ${FRAME}"
end
on frame 650 once do
set nextreg[0x02] = 0x02 ; NR 0x02 bit 1: a hard reset
end
[debugger] [info] [jds F:310 C:175891965] reset at PC 6D2F, FRAME 310
[debugger] [info] MUTATE nextreg 0x02 0x2 -> 0x2 by 2
[debugger] [info] RESET hard (guest-initiated cold boot, reconstruct re-applied)
[debugger] [info] [jds F:0 C:0] reset at PC 0000, FRAME 0
After a hard reset FRAME is 0 again. Without once, the frame 650 rule
would reset the machine every time it reached frame 650.
hostkey¶
on hostkey 1 do …
Host key N, 1 to 8: Alt+1 … Alt+8 in the emulator or debugger window,
or --script-key FRAME N headless. The rule runs before the next instruction
(headless: at the edge of frame FRAME). See Keys, joysticks and host
keys.
Payload: KEY (1 to 8), PC.
Copper: copper move, copper wait, copper halt¶
on copper move do … ; every MOVE
on copper move 0x40..0x44 do … ; a MOVE to NextREGs 0x40..0x44
on copper wait do … ; every WAIT, when it is satisfied
on copper halt do … ; the HALT
on copper move at 0..15 do … ; …only at Copper addresses 0..15
The Copper's own view of its program: one event per instruction, at its own
raster position. at A..B filters on the Copper's program counter (0 to
1023). A Copper MOVE to a NextREG raises both copper move and nextreg
(with SOURCE == COPPER).
| Name | move |
wait |
halt |
Meaning |
|---|---|---|---|---|
CPC |
✓ | ✓ | ✓ | the Copper's program counter |
HC_ULA, CVC |
✓ | ✓ | ✓ | the raster position of this Copper step (see below) |
REG, VALUE |
✓ | the NextREG and the value | ||
WAIT_V, WAIT_H |
✓ | the WAIT's line, and its horizontal threshold (hpos << 3) + 12 |
||
PC |
✓ | ✓ | ✓ | the CPU instruction during which it happened |
Inside a copper rule, HC_ULA and CVC are the Copper step's position,
not the live beam's — the rule runs at most one CPU instruction later, so the
two can differ. Use RAW_HC/RAW_VC for the live beam. A WAIT is satisfied
on the Copper's own line equal to its WAIT_V, at or past WAIT_H:
on copper move 0x43 when armed == 1 do
log "Copper MOVE NR43=${VALUE:x2} at copper PC ${CPC} on cvc ${CVC} hc_ula ${HC_ULA}"
end
on copper halt once when armed == 1 do
log "copper HALT at ${CPC}"
end
[debugger] [info] [jds F:501 C:284316507] Copper MOVE NR43=00 at copper PC 0 on cvc 0 hc_ula 1
[debugger] [info] [jds F:501 C:284489832] Copper MOVE NR43=02 at copper PC 2 on cvc 95 hc_ula 12
[debugger] [info] [jds F:501 C:284489832] copper HALT at 3
(From copper.jds.)
DMA: dma start, dma byte, dma end¶
on dma start do … ; a transfer begins
on dma byte do … ; every byte it moves
on dma byte 0x0000..0x3FFF do … ; a byte whose DESTINATION is in 0x0000..0x3FFF
on dma end do … ; it finished
The address range of dma byte is the destination; there is no page filter
for DMA (dma byte page 3 is an error).
| Name | start |
byte |
end |
Meaning |
|---|---|---|---|---|
SRC, DST |
✓ | ✓ | ✓ | the block's source and destination; on byte, this byte's |
VALUE |
✓ | the byte | ||
LEN |
✓ | ✓ | the programmed length on start; the bytes moved on end |
|
DMA_MODE |
✓ | ✓ | 0 byte, 1 continuous, 2 burst | |
IO_SRC, IO_DST |
✓ | ✓ | 1 when that side is a port, not memory | |
PC |
✓ | ✓ | ✓ | the CPU instruction during which it happened |
on dma start when armed == 1 do
log "DMA ${SRC:x4} -> ${DST:x4} len ${LEN} mode ${DMA_MODE} (io dst ${IO_DST})"
end
[debugger] [info] [jds F:516 C:292711711] DMA 8217 -> 005B len 256 mode 1 (io dst 1)
(From dma.jds: a sprite-pattern
upload to port 0x5B.)
stop¶
on stop do …
on stop when REASON == "boom" do …
Runs whenever the machine pauses — a script's stop or failed assert, a
breakpoint, the Pause button, a step that finished — after every rule of the
event that caused it has run. Headless, the run then ends; the on stop rules
run first, so they can log the state the run ended in.
Payload: REASON, a string — a script stop's own message, otherwise the kind
of pause (user, breakpoint, …) — and PC, where the machine is paused.
A stop inside an on stop rule only logs: the machine is already stopped.
And an exit there never changes the status — the pause that ran the rule has
already decided it:
on frame 20 do stop "x" end
on stop do
log "on stop runs, then exit 7"
exit 7
end
[debugger] [warning] SCRIPT STOP: x at PC=11E0 FRAME=20 CYCLE=11741208
[debugger] [info] [jds F:20 C:11741208] on stop runs, then exit 7
[debugger] [info] [jds F:20 C:11741208] SCRIPT EXIT 7
[platform] [info] script requested exit 3
on frame 20 do stop "boom" end
on stop when REASON == "boom" do
log "caught my own stop: ${REASON}, PC ${PC:x4}, loaded at FRAME ${loaded_at}"
stop "this only logs"
end
[debugger] [warning] SCRIPT STOP: boom at PC=11E0 FRAME=20 CYCLE=11741208
[debugger] [info] [jds F:20 C:11741208] caught my own stop: boom, PC 11DC, loaded at FRAME 0
[debugger] [warning] SCRIPT STOP: this only logs at PC=11DC FRAME=20 CYCLE=11741208
[platform] [info] script requested exit 3
(loaded_at is var loaded_at = FRAME.) The first SCRIPT STOP names the
frame event's PC, 0x11E0; the on stop rule sees the paused PC, 0x11DC.
What a payload name outside its event does¶
Using a payload name the event does not carry is an error when the script loads, never a silent 0:
[debugger] [error] SCRIPT ERROR err2.jds:4:31: `PREV` is event payload and is not available in a `read` event
[debugger] [error] SCRIPT ERROR err2.jds:5:22: `ADDR` is event payload and is not available in a `frame` event
The same goes for a var initial value and a filter bound: they are computed
when the script loads, outside any event, so a payload name there is an error.