01 / WHO OWNS THE LOOP?
An endless ML loop would leave BASIC waiting.
In a fully machine-language application, the foreground loop could decode messages, update the display and repeat. IRQs and NMIs would interrupt that work, service the hardware, then return to the interrupted code. The application would carry on around its own loop.
Our interface is written in BASIC. If BASIC enters an ML routine that loops forever, execution never returns to the next BASIC statement. The interrupts still have their jobs, but the BASIC menus, drawing and event handlers no longer advance.
A straightforward alternative would be a polling loop in BASIC: check flags with PEEK and IF, call an ML service routine with SYS, then jump back and repeat. Even when there is no useful work, the interpreter would keep executing that loop’s tests, calls and branches. Repeating those interpreted statements is the overhead we want to avoid.
Our aim is to minimize interpreted BASIC work between useful events. This is where WAIT changes the design: BASIC evaluates its address and masks once, then the ROM performs the repeated flag test in machine language. Enabled interrupt handlers can update the flags and release the wait. The CPU still polls, but it avoids repeatedly interpreting a BASIC polling loop.
We then use IGONE to give the ML runtime its execution turns. By redirecting BASIC’s statement-dispatch vector, we run an ML service pass whenever the interpreter reaches that hook, then let BASIC continue. Servicing ML becomes part of the interpreter’s existing execution path, including while BASIC is working through its UI handlers.
A WAIT-then-SYS design could also avoid busy polling in BASIC. The additional benefit of IGONE is automatic servicing as BASIC advances, without adding explicit service calls throughout the UI. While WAIT itself is polling, IGONE pauses too; interrupt-driven wake-ups let statement execution, and therefore the ML hook, resume.
02 / BORROW THE INTERPRETER’S LOOP
IGONE is the handoff point.
The C128’s BASIC statement-dispatch path jumps through IGONE, a two-byte vector at $0308–$0309. Normally it leads to $4AA2 in BASIC ROM. AIS4CBM saves the current target and redirects the vector to mainLoop.
Despite its name, this mainLoop does one service pass per invocation. It calls ais4cbmManager.main, restores the interpreter’s registers and memory configuration as shown below, then chains to the saved IGONE target. BASIC continues dispatching its next statement.
This is cooperative scheduling at statement-dispatch points. It has no fixed time slice: a long BASIC statement delays the next ML turn, and a long ML operation delays BASIC. IRQ/NMI work remains interrupt-driven.
References: Mapping the Commodore 128: IGONE, printed page 92 · ROM instruction JMP ($0308).
03 / INSTALL, SERVICE, CHAIN
Keep a route back to BASIC.
The installation path saves the previous vector before publishing our hook. IGONE_VEC is defined as $0308:
!install:
lda IGONE_VEC
sta oldGoneLo
lda IGONE_VEC+1
sta oldGoneHi
lda #<mainLoop
sta IGONE_VEC
lda #>mainLoop
sta IGONE_VEC+1
!done:
rts
The full installer also detects an already-installed hook and repairs an empty or recursive saved target. That prevents a repeat installation from making the hook chain back into itself. Startup calls the installer with IRQs masked.
Full installMainLoop routine · Startup caller
The hook’s complete control flow
This is the current bridge with comments omitted and spacing simplified. oldGoneLo and oldGoneHi are adjacent bytes holding the previous target:
mainLoop: {
pha
txa
pha
tya
pha
lda $ff00
sta originalBank+1
lda #BANK0
and #%11001110
sta $FF00
jsr ais4cbmManager.main
lda #BANK15
sta $ff00
lda #0
sta $d507
originalBank:
lda #$00
sta $FF00
pla
tay
pla
tax
pla
jmp (oldGoneLo)
}
The stack preserves A, X and Y. The bridge saves the caller’s MMU configuration, selects the ML mapping and makes the service call. Afterwards it makes I/O visible, restores real zero page for BASIC, restores the saved MMU setting and reloads the three registers.
The final JMP (oldGoneLo) continues the interpreter’s existing dispatch chain. IGONE was entered by an indirect jump, so the hook follows that chain; the JSR inside it is the call that returns from the ML manager.
A service pass has an end
The manager checks pending queries, keyboard work and delivery conditions, then services serial data when appropriate. Its serial path swaps the IRQ-filled buffer into a work buffer under a short PHP / SEI / PLP section. It decodes that captured workLen and returns through the hook. Newly arriving bytes can go into the other buffer.
Manager service entry · Buffer handoff and finite decode pass
04 / CLOSE THE LOOP WITH EVENTS
The apparent paradox: WAIT holds IGONE too.
If BASIC is held in WAIT, how can it reach IGONE to run our ML code? While the wait condition is unsatisfied, it cannot. Execution stays inside the ROM’s polling loop, with no new BASIC statement dispatch. The IGONE-based ML service is paused along with the BASIC interface.
The way out is a hardware interrupt. IRQ/NMI entry is independent of BASIC’s statement dispatch, and WAIT leaves interrupts enabled if they were enabled on entry. An interrupt handler can therefore change the notification byte while BASIC remains inside WAIT.
- WAIT polls the shared byte. BASIC stays in that statement; the IGONE service is not running.
- An IRQ publishes a selected notification. For example, it sets the wake-up bit because buffered serial data needs processing.
- The IRQ returns to the interrupted WAIT routine. WAIT reads the byte again, finds its condition satisfied and finishes the statement.
- BASIC resumes statement dispatch. The interpreter reaches IGONE, our ML service gets its turn, and control then continues back into BASIC.
In AIS4CBM’s serial path, the SwiftLink NMI receives bytes. The IRQ drains them into the runtime’s buffer and handles time and mouse work. When serial pressure calls for foreground processing, it sets WAKE_UP, bit 0 of the shared notification byte:
lda notifyFlag
ora #WAKE_UP
sta notifyFlag
NMI receive path · IRQ buffer drain · Pressure wake-up and tick publication
The wake-up bit satisfies the main loop’s WAIT condition. Statement dispatch can resume, giving the ML hook another turn. The UI acknowledges this wake-only bit and loops; it does not need to decode a message itself. These are the statements of BASIC line 113, split for readability and with its comment omitted:
MK=PEEK(MP)
WAIT WF,MK,MS
WM=PEEK(WF) AND MK
IF (WM AND 1) THEN POKE WF,(PEEK(WF) AND 254):GOTO 113
Once ML has a deliverable vessel update, it publishes the payload and sets MESSAGE_FLAG (32). BASIC then runs the corresponding UI handler. Clock and mouse notifications provide other reasons to leave WAIT. The first tutorial explains the masks and acknowledgement protocol.
Keyboard input shows the two stages
The IRQ captures the key and modifier state, then sets WAKE_UP (1). Once BASIC advances into IGONE, the ML manager processes the key. It handles its own shortcuts directly; for a key passed on to BASIC, it publishes KEYPRESS_FLAG (128). The wake-up and the application notification are separate steps, performed in different execution contexts.
IRQ captures the key and wakes BASIC · The foreground manager publishes the BASIC key event
05 / THREE ROLES, ONE APPLICATION
Execution turns and notifications solve different problems.
| Part | What gives it control? | Its role here |
|---|---|---|
| IRQ / NMI handlers | Hardware interrupts | Receive bytes, service timing/input and publish wake-ups. |
| ML manager | The IGONE statement hook | Process buffered data and publish application notifications. |
| BASIC interface | The interpreter’s own loop | Wait for relevant events, update the UI and continue. |
WAIT handles idle polling inside ROM. IGONE gives the ML runtime execution opportunities as BASIC advances. Shared flags tell BASIC what happened and when to resume. Together, these let each part keep its own logic without continuously interpreting a BASIC polling loop.
Read the code
Excerpts assume the initialized AIS4CBM runtime and its existing memory layout. GitLab links point to main; source line numbers may move.
- installMainLoop — save, replace and repair the IGONE chain.
- irqbridges.asm — separate IRQ, NMI, USR and IGONE bridges.
- ais4cbmManager.asm — foreground service and decoding.
- aiscbm8.bas — the BASIC event loop and handlers.