← Back to the project

DEVELOPER REFERENCE / C128 MACHINE LANGUAGE

Your application.
Our AIS engine.

You may want a different display, your own controls, or an application written in another language. The AIS4CBM API provides a route into the machine-language runtime, so receiving, decoding and storing AIS data can remain the engine’s job.

Based on the repository README · Exports checked against source and build symbols, 18 September 2026

THE PURPOSE

Reuse the engine. Build your own interface.

AIS4CBM’s BASIC 7 text viewer and BASIC 8 graphics viewer already share the same machine-language foundation. Serial reception, AIS decoding, the vessel registry, REU storage and map filtering live below the presentation layer. The API makes that separation available to other applications.

Here, an application programming interface means a set of entry addresses with defined inputs and results. BASIC reaches them through SYS; assembly can use JSR; another language can use a small register-and-memory bridge. The caller does not have to be written in BASIC.

BASIC 7, BASIC 8 and a custom application connect through calls, events and record IDs to the AIS4CBM runtime. The runtime handles serial input, decoding, filtering, the Bank 1 registry and REU storage.
The interface can change while the AIS work stays in ML. A custom frontend supplies the calling bridge and execution schedule. Open full size ↗

The current interface grew out of the BASIC viewers. Some entries are direct register calls; others prepare a BASIC USR() operation or manage BASIC 8 graphics state. Reusing the binaries from another language is possible, but a new frontend must respect those dependencies and the C128 memory layout.

THE CALLING CONTRACT

An address is only half an API.

The public entries occupy the BasicApi segment beginning at $8000. These small wrappers give callers known entry points while the implementation behind them can move. Use the symbols and modules from the same build: out/buildBASIC8.sym is the address authority.

The current standard build uses SwiftLink-compatible serial input and the REU layout for 512 KiB or larger units. A replacement frontend must preserve the runtime’s reserved memory and hardware contracts as well as its call addresses.

  • Registers carry arguments. Each call defines its own use of A, X, Y and carry. Do not assume registers or flags survive unless the routine promises it.
  • Pointer getters return A/X. A is the low byte and X the high byte. The result is an address, not the counter or record value stored there.
  • Memory mapping is part of the contract. A native caller must make the runtime code and its working memory visible, with the expected MMU and zero-page setup. A bare JSR from an arbitrary bank is insufficient.
  • Calls belong in foreground code. The runtime shares scratch buffers, delegates and banking helpers. Treat these interfaces as non-reentrant, and keep one owner of the vessel-access transaction.
8502 assembly · discover the notification address
// Called after runtime initialization, in its valid memory mapping.
jsr $8004             // getNotifyFlagPointer
sta eventAddress      // low byte
stx eventAddress+1    // high byte
// Continue in the caller; eventAddress is its own two-byte variable.

This is the same entry used by BASIC. The caller’s bridge changes; the meaning of the API result stays the same. Prefer a getter to copying a current internal address into your application.

THE MAIN DATA TRANSACTION

Lock. Read. Release.

The application identifies a vessel by a 16-bit record ID, currently 0–999. This is a registry handle, not a Bank 0 pointer and not an MMSI. Records live in Bank 1; each owns a fixed 128-byte REU slot. Reclaimed IDs can be reused, so a long-lived ID alone does not guarantee the same vessel identity.

getVesselData at $8022 — X = record ID low, Y = record ID high for every action
A / actionMeaning
1Lock the record and fetch its metadata and available payloads into the host scratch area. Carry clear means success; carry set means unavailable. This also selects the vessel field reader for BASIC’s USR delegate.
0Persist consumed visual metadata and unlock the same record. This may promote its latest cell to the drawn cell and acknowledges the pending vessel-message notification.
4Unlock without writing the scratch metadata back or changing the drawn cell. Use for a read-only inspection. It does not acknowledge a pending vessel-message notification.

After a successful lock, BASIC reads fields through USR(field). Numeric results use the C128 floating-point accumulator; string results use BASIC string services. The $807A getter exposes the fetched scratch buffer, and $800E exposes the nine-byte registry scratch record. A native frontend can wrap the field reader or interpret the fetched data using the matching build’s layout.

Keep the record locked while copying the fields you need, then release it. The scratch space belongs to the runtime and is reused by the next transaction; it is not a permanent per-vessel buffer. Unlock with the same ID, and only after a successful lock.

For a guided example of the BASIC side, see Getting vessel data with USR, including field selectors and numeric/string results.

CALLS IN / NOTIFICATIONS OUT

The runtime tells your application what changed.

The notification pointer returned by $8004 leads to a shared flag byte and its event context. BASIC uses WAIT to wait for selected flags. Another frontend can read the same state from its own event loop.

The notification byte, from least significant bit to most significant bit
MaskSignalInterpretation
$01Wake-upLet the foreground runtime and application make progress.
$02MousePublished mouse notification; details are at offset +8.
$04TickA toggling state: compare with the last handled tick, rather than clearing it as a pending event.
$08ConnectionSerial connection state.
$10Data flowRecent incoming activity; this is a state indicator.
$20Vessel messageA published vessel update for the frontend.
$40Status / errorA status, warning or error code; successful QR completion also uses this signal.
$80KeypressA key passed on by the foreground manager.
Offsets relative to the notification pointer
OffsetContents
+0Notification flags.
+1Vessel action and AIS message type packed into one byte.
+2, +3Vessel-message record ID, low then high.
+4Keypress code.
+5Status / error / warning code.
+6, +7Context for that status code. For QR-ready code 14, the record ID low/high.
+8Mouse details.
+9Current frontend wait-mask mirror, also discoverable through $8045.

These flags are a shared handshake, not an event queue. Repeated signals can coalesce. Read the relevant context before acknowledging an event, preserve unrelated bits, and coordinate acknowledgements with interrupt updates. Keep the wait-mask mirror consistent with the events your frontend can currently handle: the runtime uses it when deciding what to deliver.

IRQ supplies the bits needed to get BASIC going; once IGONE runs, foreground ML can publish further events. A native loop replaces that foreground scheduling role. It still needs to service the runtime and consume its notifications.

CURRENT EXPORTED ENTRIES

Find the service you need.

The tables retain the source names, including legacy names. Addresses describe the checked build. Detailed payload layouts and implementation helpers should always be taken from its source and symbols.

Runtime and lifecycle

Runtime and lifecycle — current public entry points
AddressEntryContract
$8000nmiRuntime initialization wrapper (legacy name, not a hardware NMI entry). Initializes decoder and storage state; use during startup, not as a service tick.
$80BBrestartRuntimeIrqApiStartup hook installation: resets serial work buffers, installs IRQ and IGONE, starts the clock and enables IRQs. See the integration notes before using outside BASIC.
$80A1rearmRuntimeIrqTimerApiRearm only the runtime CIA1 timer after disk I/O. Preserves caller flags/MMU; does not reset serial buffers or reinstall hooks.
$804EresumeDecoderCompatibility entry; currently returns immediately. It does not resume serial flow.
$804FstartIdleCleanupApiRequest a bounded cleanup pass. Foreground servicing must continue for cleanup to progress.
$8052getIdleCleanupStatusApiNormal build: A bit 6 indicates active; X/Y = reclaimed count low/high. Debug builds also encode fault details; consult idleCleanup.asm.
$8073setDataDriveA = data drive, X = raw-logging flag. Stores those settings.

Vessel access and scratch

Vessel access and scratch — current public entry points
AddressEntryContract
$8022getVesselDataA = action; X/Y = record ID low/high. See the transaction contract.
$8035getVesselAtLocationA = column, X = row. Returns Y = matching drawn, unlocked vessel count; A/X = first record ID low/high. Y = 0 means no match (A/X also zero); ID zero itself is valid.
$800EgetVesselBufferAddressPointerReturns A/X pointing to the nine-byte Bank 0 registry scratch record for BASIC access. This is not the canonical Bank 1 registry.
$8013getVesselRegistryMemoryLocationPointerReturns A/X pointing to the 16-bit bufferTail high-water mark (next unused ID), despite the legacy name. It is not the registry base address or a live vessel count.
$807AgetBASICScratchPointerReturns A/X pointing to the fetched metadata and payload staging area. Read within a successful vessel lock.

Notifications and pointer state

Notifications and pointer state — current public entry points
AddressEntryContract
$8004getNotifyFlagPointerReturns A/X pointing to the notification block.
$8045getBasicWaitMaskPointerReturns A/X pointing to the one-byte frontend wait-mask mirror.
$807FgetMouseQueryPointerReturns A/X pointing to the current query column byte, followed by the row byte.

Runtime counters

Runtime counters — current public entry points
AddressEntryContract
$8009getTotalVesselsTrackedCounterPointerReturns A/X pointing to a 16-bit remaining-slot count, despite the name. Tracked vessels = configured capacity minus this value (capacity is currently 1,000).
$8018getTotalMessagesReceivedCounterPointerReturns A/X pointing to the 32-bit total-message counter.
$801DgetTotalFailedUpdatesCounterPointerReturns A/X pointing to the 16-bit failed-update counter.
$8084getCRCFailuresCounterPointerReturns A/X pointing to the 16-bit NMEA checksum-failure counter.
$8089getMessageTypeCountersPointerReturns A/X pointing to nine 16-bit counters, ordered by AIS type: 1, 2, 3, 4, 5, 9, 18, 21, 24.
$808EgetSkippedNotificationsCounterPointerReturns A/X pointing to the 16-bit skipped-notification counter.
$80F5getRawDataDropCounterReturns A/X pointing to the one-byte raw-log drop counter.

Filtering and chart extent

Filtering and chart extent — current public entry points
AddressEntryContract
$8069setAISMaskBlockReturns A/X with the address of a second routine. Call that returned routine with A = mask and X = block 0–7. Block 0 represents AIS types 1–8; block 1 represents 9–16, and so on.
$806ElistenToAISMessageTypeReturns A/X with the address of a second routine. Call it with A = AIS type 1–63, X = 0 to disable or nonzero to enable. That routine returns carry set for a valid type, clear otherwise.
$8038setChartExtendSelects the BASIC USR extent setter. Follow with USR(north), USR(east), USR(south), USR(west), in that order.
$8057getChartExtendSelects a USR getter whose current implementation returns zero. It does not currently return the chart bounds.

BASIC and viewer helpers

BASIC and viewer helpers — current public entry points
AddressEntryContract
$8025getTimeStringRefreshes the time snapshot and selects Time.getDateTimeBasic as the USR delegate. The call itself does not return an A/X string pointer.
$8093setDelegateUSRInstalls the runtime USR trampoline in BASIC's USR vector. It does not select a new field operation by itself.
$804ArequestCurrentCellQueryApiStarts the graphics pointer-cell vessel query / QR path. Successful QR completion arrives asynchronously as status code 14 on the status/error flag.
$8064getWindowRoutineReturns A/X with the callable address of windowPreparePrintAtRowLine, the text-window cursor restoration helper.
$809EenableBasic8MouseBridgeA = 0 holds/disables the bridge; A = 1 releases a hold and enables when holds reach zero; A = 2 clears holds and resynchronizes from the BASIC 8 pointer.
$80BEcopyBackgroundToBank1Copies the locked vessel's saved REU background into the fixed BASIC 8 Bank 1 stash.
$80D2copyBackgroundFromBank1Copies the fixed BASIC 8 Bank 1 stash into the locked vessel's saved REU background.
$80E6clearOwnerMatrixApiClears the Bank 1 display-cell ownership matrix.
$80E9claimOwnerCellApiUses the locked record ID; X = column, Y = row. Returns A = 0 for success, 1 for another owner, 2 for an invalid cell.
$80ECreleaseOwnerCellApiUses the locked record ID; X = column, Y = row. Returns A = 0 for success, 1 if not the owner, 2 for an invalid cell.
$80EFstartScreenRefreshApiA = display-class mask (bits 0–4: Class A, Class B, base station, aid to navigation, SAR). Clears visual state and starts staged notifications; carry set means active.
$80F2getScreenRefreshStatusApiAdvances the refresh pump and returns carry set while active, clear when done. This status call performs work.

Multi-byte counters listed here are little-endian. Take a consistent snapshot before converting or displaying them; their bytes are shared runtime state. Flags and field/string representations have their own contracts.

A FRONTEND OF YOUR OWN

BASIC is one caller. Yours can be another.

An application written in another language can provide its own foreground loop: service the AIS runtime, inspect notifications, update its interface, and repeat. IRQ and NMI handlers still handle their interrupt work. The integration needs a bridge that supplies registers, captures results and manages memory mapping around calls.

  1. Load a matched runtime. Use the module set and placement in the current build and loader. The disk separates serial code, bridges, registry, decoder/API, runtime support, AIS logic, file operations and REU support. Loading DECODER alone does not provide the complete engine.
  2. Respect the memory contract. Reserve the Bank 1 registry and hash, REU slots, shared scratch, low-memory bridges and zero-page/FAC areas. Use the existing banking and REU helpers; REU DMA must go through reu.runDma, which forces 1 MHz for the transfer.
  3. Supply the execution schedule. Initialize in the expected environment, arrange IRQ/NMI and foreground servicing, and select the events your frontend will consume. Do not install the BASIC IGONE hook and assume it will run in a native-only loop.
  4. Choose your field-access bridge. Register calls are straightforward to wrap. USR/FAC/string helpers still need their C128 ROM and BASIC workspace contracts, even when the application itself is written in another language. A native typed-field interface would be an additional adapter.

The same division of responsibilities can guide a future implementation on another platform. Reusing these particular binaries, however, requires the C128 execution environment or emulation of it.

Keep the implementation beside the reference

GitLab links follow main. For an integration, keep a specific source revision and its generated symbols alongside the binaries you use.