Small tutorials

03 / SHARING THE CPU

Let BASIC run.
Give ML a turn.

An all-machine-language application can own an endless loop. AIS4CBM also needs its BASIC interface to run. The solution is to borrow BASIC’s statement loop through the IGONE vector.

Commodore 128 · BASIC 7.0 + 8502 assembly · The scheduling behind the WAIT tutorial

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.

A BASIC statement completes, the interpreter reaches IGONE, the hook runs one ML service pass, and the saved dispatcher executes the next BASIC statement. IRQ and NMI work can interrupt execution while enabled.
The interpreter supplies the repetition. The ML hook supplies a service pass and hands control back. Open full size ↗

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.

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:

Basic8Config.asm · vector replacement excerpt
!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.

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:

irqbridges.asm · mainLoop
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.

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.

  1. WAIT polls the shared byte. BASIC stays in that statement; the IGONE service is not running.
  2. An IRQ publishes a selected notification. For example, it sets the wake-up bit because buffered serial data needs processing.
  3. The IRQ returns to the interrupted WAIT routine. WAIT reads the byte again, finds its condition satisfied and finishes the statement.
  4. 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:

irqhandlers.asm · publish a foreground wake-up
lda notifyFlag
ora #WAKE_UP
sta notifyFlag

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:

aiscbm8.bas · BASIC line 113
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
Serial NMI receives bytes. IRQ buffering raises the wake-up flag. WAIT returns, statement dispatch runs the IGONE ML service, and a ready vessel notification later directs BASIC to its UI handler.
A serial-pressure example, read across the top and back along the bottom. The IRQ returns to WAIT; WAIT’s flag test lets BASIC advance to IGONE. A batch may need more passes before it produces a vessel event. Open full size ↗

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.

05 / THREE ROLES, ONE APPLICATION

Execution turns and notifications solve different problems.

How the responsibilities fit together
PartWhat gives it control?Its role here
IRQ / NMI handlersHardware interruptsReceive bytes, service timing/input and publish wake-ups.
ML managerThe IGONE statement hookProcess buffered data and publish application notifications.
BASIC interfaceThe interpreter’s own loopWait 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.