Architecture

The Agon Light 2 has two processors, and VIOS uses both:

  • the eZ80 runs the operating system itself: a resident Forth with its compiler, tasks, files, modules and the logic of every application;
  • the ESP32 runs the firmware that draws the screen, reads the keyboard, plays sound, talks to your Mac over USB, and loads and supervises the eZ80.

This chapter shows how the pieces fit together. You need none of it to use VIOS, but it explains why some things behave the way they do. Examples are why the screen keeps working while a Forth program hangs, why firmware and resident must be built as a pair, and why the board can boot without the Mac.

The big picture

  Mac (optional)
  +--------------------------+
  | vios_cli.py   MCP client |
  |        \        /        |
  |     vios_harness.py      |  one serial owner, TCP 127.0.0.1:8765
  +------------+-------------+
               |  USB-C -> CH340, 115200 8N1, COBS/CRC frames, 5 channels
  Agon Light 2 |
  +------------v---------------------------------------------------+
  | ESP32-PICO-D4: VIOS VDP firmware (in app0 or app1)             |
  |                                                                |
  |  control task (core 0)        VDP task and frame task          |
  |  - USB reader, link resend    - VDU parser                     |
  |  - ZDI supervisor             - display server, compositor ----+--> VGA
  |  - boot watchdog              - console, line editor <---------+--- PS/2 keyboard
  |  - firmware A/B trial         - audio engine ------------------+--> audio / buzzer
  |                               - boot server, embedded image    |
  +------+-----------------------------------+---------------------+
         | ZDI debug port                    | UART 1,152,000 baud, RTS/CTS
         | (optional: 2 jumper wires)        | VDU stream + VDU 23,0,E0..E9
  +------v-----------------------------------v---------------------+
  | eZ80F92, 18.432 MHz, 512 KB external RAM                       |
  |  flash: Agon MOS (runs the boot stub at power-on)              |
  |  RAM:   VIOS resident Forth, from 0x040000 --------------------+--> microSD (SPI)
  +----------------------------------------------------------------+

The eZ80: the resident Forth

Everything you type at the vios> prompt runs on the eZ80. The resident is one binary, build/vios-resident.bin (about 195 KB in VIOS 0.8.0), loaded into external RAM at 0x040000. It is bare metal. Once running, it makes no calls into MOS: it drives its own UART, timer, SPI bus, SD card and FAT32 file system.

Its main parts:

  • The Forth system (forth/): dictionary, outer interpreter and compiler. Cells are 24 bits wide, the eZ80's native word size. Every definition keeps its source text, so SEE shows exactly what you typed.
  • Three engines run the same compiled words. DIRECT is native direct-threaded eZ80 code and the default. TOKEN is native token-threaded code. C is a portable reference interpreter in standard C. You switch with ENGINE DIRECT, ENGINE TOKEN or ENGINE C. Direct threading measured 11–14% faster than token threading. The C engine is the yardstick the tests compare against, and module transactions run on it (see Modules and Persistence).
  • The kernel services (kernel/): console, clock, display, audio, handles and pipes, devices, SD files, modules and boot slots, the GUI client words (UI-*), the workbench editor and the GUI tools. Many services are deferred words such as CONSOLE-EMIT, FS-READ and DISPLAY-WRITE, which your own modules can rebind.
  • Tasks: four background Forth tasks next to the REPL, time-sliced and preempted. See Tasks and Preemption.

There is no MMU and no memory protection. All code shares one address space. Errors inside Forth are caught and reported (see LAST-ERROR), but a native routine that writes to the wrong address can corrupt anything.

Memory map while VIOS runs:

Address Size Contents
0x000000..0x01FFFF 128 KB eZ80 on-chip flash: Agon MOS. VIOS never writes it.
0x040000.. about 195 KB The resident: code, constants, initialized data.
..0x0AD815 about 243 KB BSS: dictionary, names, compiled code, data space, stacks, module source pool, buffers.
0x0AD815..0x0AEFFF about 6 KB Free (0.8.0).
0x0AF000..0x0AFFFF 4 KB Native stack (top at 0x0B0000).
0x0B0000..0x0B7FFF 32 KB MOS moslet area. The boot stub runs here during boot.
0x0B8000..0x0BFFFF 32 KB MOS reserved area and MOS heap/stack. Not used by VIOS.
0xB7E000..0xB7FFFF 8 KB On-chip SRAM, where MOS maps it.

The linker refuses an image whose BSS would run into the stack. Exact BSS figures change with every build; see build/vios-resident.map. Fixed capacities such as dictionary entries, stack depths and module limits are listed in Appendix: Limits and Numbers.

The ESP32: the VIOS VDP firmware

On a stock Agon the ESP32 runs the VDP ("video display processor") firmware, which turns a stream of VDU bytes from the eZ80 into pictures and sound. VIOS keeps that role, built from a pinned copy of the upstream VDP 1.04 source and vdp-gl 1.0.3 with VIOS changes on top. It adds several subsystems:

  • The display server (lib/vios-vds). At power-on the ESP sets a 640×480, 16-colour mode and starts a window compositor before the eZ80 has even loaded. A frame task composes at every vertical blank. Windows, menus, widgets, text views and sprites live on the ESP. The eZ80 drives them with compact batches (VDU 23,0,E4) and pulls events back. Two looks are built in: a Smalltalk-like style and a Turbo Vision-like style. Large repaints are composed off-screen in PSRAM and shown at once, so you never see a half-drawn screen. See The GUI.
  • The console terminal. The eZ80's console output passes through the ESP on its way to USB. The ESP draws the same bytes into the Console window, or full screen in text mode (Ctrl+Alt+F1 switches). The eZ80 never repaints its own console.
  • The line editor. While the REPL waits for a line, the ESP edits it locally: cursor keys, Backspace and Delete, Esc, and a four-line history. Typing therefore stays responsive whatever the eZ80 is doing. The finished line is handed over whole. See The Console and REPL.
  • The audio engine. Three voices are synthesized in the sound interrupt, with tunes stored as text and played without the CPU. See Audio.
  • The keyboard. PS/2 only, on the USB-A-shaped socket. The window manager sees every key first and passes unused keys to the eZ80. The PS/2 mouse is disabled in this firmware.
  • The USB multiplexer. USB serial to the Mac runs at 115200 8N1 through the board's CH340 chip. Instead of a raw terminal, the ESP speaks zero-delimited, COBS-encoded frames with a CRC-32 each, on five channels: 1 console, 2 control commands, 3 status events, 4 host-file requests, 5 link requests. A Forth line from the Mac arrives as one CRC-checked transaction, and only then is it handed to the eZ80. The eZ80 checks the CRC again before running it.
  • Link retransmission. The CH340 link occasionally drops a few bytes, about one frame in 1,500 to 4,000. The ESP therefore numbers every frame it sends and keeps the last 24 in a ring. When the host sees a gap or a damaged frame, it asks for the missing frame on channel 5. The ESP resends the kept frames byte for byte, or answers "gone" when they have left the ring, and the host delivers everything in order. You see this only in counters: link_requests, link_resent and link_gone in esp_stats. Only a "gone" frame, or ten requests without progress, makes the harness report a lost wire.
  • The ZDI supervisor. ZDI is the eZ80's debug port. With two jumper wires fitted (see Hardware and Wiring), a separate control task on the ESP can halt the eZ80, read and write its registers and memory, and load code into it. This task owns the USB reader and runs on its own core, independent of the VDP task. It stays reachable even if a graphics command or a Forth program wedges the screen.
  • The boot server. The firmware contains a copy of the resident it was built with, and serves it to the boot stub on request (VDU 23,0,E8).
  • Firmware A/B. The firmware supervises its own trial after an update. It confirms itself once the eZ80 reports healthy, and falls back to the previous slot otherwise. See Installing.

On the ESP, the control task (USB reader, ZDI, watchdog, firmware trial) runs on core 0 at its own priority. The VDU parser and the frame task run on the other core, where the frame task preempts the parser at every vertical blank. Only the VDP task writes to the eZ80's UART, and every USB frame is written whole under a lock. Console, control and event traffic therefore never interleave.

The eZ80 and ESP talk over a UART at 1,152,000 baud with hardware flow control in both directions. The bytes are the ordinary VDU protocol plus private VDU 23,0,E0..E9 commands, such as console records, the clock, boot health, GUI batches and audio. They are recognized only at a command boundary, so bitmap or audio payload bytes can never be mistaken for commands.

The boot chain

VIOS boots in one of two ways. The ESP decides at every power-on by probing the ZDI port.

Untethered boot (no ZDI wire, the normal case):

 power on
   |
   +-- ESP32: VGA, display server, Console window      (about 1.9 s after power-on)
   |          ZDI probe fails -> "user mode"; answers MOS as a normal VDP
   |
   +-- eZ80: Agon MOS from flash -> boot file on SD -> viosboot (boot stub)
   |          stub: link to 1,152,000 baud, ask ESP for the manifest   (about 2.5 s)
   |          stream the resident into 0x040000, 192-byte CRC'd blocks
   |          whole-image CRC-32 + MOS header check, jump              (about 4.4 s)
   |             (ESP image bad or missing -> /vios/vios.bin from SD)
   |
   +-- resident: start, HEALTH 0                                       (about 5.1 s)
              load confirmed boot bundle (or its compiled cache) from SD
              health check -> HEALTH 1, "BOOT READY"                   (about 7.7 s)
              Console: /VIOS/USER.FTH, AUTORUN, vios> prompt

MOS is involved only up to the jump. It initializes the hardware the way the Agon expects, reads the SD card and runs the stub. After that the resident owns the machine. The image normally comes from the ESP firmware, so the resident and the firmware are guaranteed to be a matched pair. The SD copy is the fallback, and a firmware on trial does not count an SD-sourced boot as a success.

Dev boot (ZDI wire fitted): the ESP halts the eZ80 and finds its on-chip RAM. It writes and verifies a 46-byte native bootstrap there, which sets up the external RAM. It then writes the embedded resident to 0x040000 over ZDI, has the eZ80 compute the image's CRC-32 itself, and starts it. MOS and the SD card play no part in loading. This path keeps working when the card or MOS is broken. It is also the only mode in which the Mac can inspect the halted CPU. A 30-second boot watchdog restarts the ESP after a failed boot and gives up after three.

Above both paths sits a second layer of A/B on the SD card: two boot bundles of source modules with trial, confirm and revert. A module update that fails its boot-time checks falls back to the last confirmed bundle on the next boot. See Modules and Persistence.

The host side

On the Mac, tools/vios_harness.py owns the serial port and serves the board to everything else. It offers 72 MCP tools for LLM clients, a newline-delimited JSON-RPC socket on 127.0.0.1:8765 for the CLI and scripts, and a sandboxed directory the board sees as /host/. JSON, schemas and networking all stay on the Mac; the board only sees binary frames. The board has no network stack of its own. See The Host Harness and MCP Tools.