Installing VIOS puts files in two places:
- The ESP32 firmware (
firmware.bin) goes into one of the ESP32's two application slots,app0orapp1. It contains the VDP, the display server, the audio engine, the USB multiplexer, the boot server and a copy of the eZ80 resident. - The SD card gets a small boot stub,
/mos/viosboot.bin, plus a fallback copy of the resident,/vios/vios.binand/vios/vios.man, and aviosbootline in the MOS boot file.
You never write the eZ80's flash for a normal install. It holds Agon MOS (on the reference board MOS 3.0.2, "Arthur"), which starts the boot stub. The ESP32 bootloader and partition table are never touched either.
Before you start, build both halves (see Building): build/vios-resident.bin with its .baud and .gui sidecars, build/viosboot.bin, and firmware/esp32-vios/.pio/build/esp32-vios/firmware.bin.
Installing a release
If you don't want to build VIOS yourself, download a release from the
repository's Releases page. vios-X.Y.Z.zip unpacks to vios-X.Y.Z/:
| Path | Contents |
|---|---|
esp32/firmware.bin |
the ESP32 application, with the eZ80 resident embedded |
esp32/build-manifest.json |
the build's identity: version, image CRCs, baud rate, GUI |
sd/ |
the SD card files: mos/viosboot.bin, vios/vios.bin, vios/vios.man, autoexec.txt |
tools/ |
vios_firmware_ab.py and vios_sd_install.py |
CHANGELOG.md |
this version's notes |
SHA256SUMS |
a checksum for every file |
Check the download, then install the two halves with the release's own tools,
as described in the sections below (use the esp32/firmware.bin and sd/
files instead of your build outputs):
cd vios-0.8.0 && shasum -a 256 -c SHA256SUMS
python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 install --backup-dir backups esp32/firmware.bin
python tools/vios_sd_install.py install /Volumes/AGON --resident sd/vios/vios.bin --stub sd/mos/viosboot.bin
The SD installer keeps your other autoexec.txt lines and your saved
modules. After a reset the console shows the release's banner, for example
VIOS v0.8 and (c) 2026 Alessandro Iob.
Find the port, free the port
ls /dev/cu.usbserial-* # e.g. /dev/cu.usbserial-210
lsof /dev/cu.usbserial-210 # nothing may hold it
The suffix depends on the USB socket or hub, so check it every time. Several tools still default to an older suffix (/dev/cu.usbserial-224130), so always pass --port. If the harness is running, take a snapshot of anything you care about (:snapshot FILE in the CLI), then stop it. The installer needs exclusive use of the port. It also resets the board several times, which loses unsaved RAM work.
ESP32 firmware: A/B install
tools/vios_firmware_ab.py is the only supported way to write ESP firmware. It uses esptool (pinned in tools/requirements.txt) and never writes anything except one application slot and one 4 KB sector of the otadata partition.
How the slots work. The ESP32 flash has two application slots and an otadata partition with two 4 KB entries. The bootloader runs the slot named by the valid entry with the highest sequence number. VIOS uses this for A/B updates:
installwrites the new image into the slot that is not running. The running slot is left alone and stays the fallback.- Before writing, it saves the
otadatapartition and the old contents of the target slot into the backup directory you name. - It writes the image, requires esptool's flash hash check, and reads the slot back to compare. A failed write is never retried.
- It adds a new
otadataentry for the new slot in the sector that does not hold the active entry, so the old entry survives. The new entry isVALIDbut carries aVIOS-TRIALmarker. - It then opens the port, which boots the board, and listens on the event channel for the firmware's verdict (180 s by default).
Trial and confirm. A firmware that boots with the trial marker is on trial until the eZ80 it started reports healthy (HEALTH flag 1) and the boot stub says it ran this firmware's own embedded resident. It then clears the marker itself and reports FIRMWARE confirmed. It marks itself invalid and the old slot boots again (FIRMWARE rollback: ...) in any of these cases:
| Reason in the event | What happened |
|---|---|
CPU boot deadline |
The resident did not report healthy within 30 s (counted from the stub's manifest request, or from the ZDI load in dev mode). |
no CPU health |
No healthy CPU within 180 s of the ESP starting. |
trial restarted 3 times by crashes |
The new firmware crashed (panic, watchdog, brownout) three times before confirming. |
the stub refused this image's resident and ran the SD copy |
The embedded resident failed its CRC check, so the board only came up from the SD fallback. |
host request |
The host sent the control command FR. |
Install a new build:
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 status
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 \
install firmware/esp32-vios/.pio/build/esp32-vios/firmware.bin \
--backup-dir backups/ab-$(date +%Y-%m-%d)
--port belongs before the subcommand. --backup-dir is required. --wait N changes how long to wait for the verdict, and --wait 0 skips the wait. The baud rate stays at 115200 (--baud), because faster esptool rates are unreliable on this board's USB link. Reads are done in 64 KB chunks and retried on a digest error. Expect the whole install to take several minutes: it backs up a 1.25 MB slot, then writes and reads back the new image.
A good install prints the boot events, then a JSON report, and exits with status 0 only when the verdict is FIRMWARE confirmed. On an untethered board the events look like this:
event: BOOT VIOS v0.8 (c) 2026 Alessandro Iob
event: BOOT VIOS VDP 1.04 mux-v1 zdi-edge 1us
event: BOOT retained failures 0
event: FIRMWARE app0 valid trial
event: BOOT user mode: no ZDI; MOS and its stub load the resident
event: BOOT link: MOS answered at the internal rate
event: MOS: Agon Platform MOS Version 3.0.2 Arthur
event: BOOT link: stub switched to 1152000
event: BOOT stub loading the resident over the UART
event: BOOT image sent: 1042 blocks in 2291 ms (build 90 ms, send 1871 ms, slowest send 2517 us)
event: BOOT stub runs this firmware's image
event: HEALTH flag 0
event: HEALTH flag 1
event: FIRMWARE confirmed
This is VIOS 0.8.0 installed into app0. The FIRMWARE ... valid trial and
FIRMWARE confirmed lines appear only on the first boot of a newly installed
slot.
The backup directory now holds otadata-<stamp>.bin and app0-<stamp>.bin or app1-<stamp>.bin (whichever slot was overwritten). Keep them. Then restart the harness.
Status and rollback:
# Which slot boots, and the decoded otadata entries
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 status
# Boot the other slot again (it still holds the previous firmware)
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 rollback
# Select a named slot
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 rollback --slot app0
# Put back an exact otadata backup (8192 bytes)
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 \
rollback --restore backups/ab-2026-10-21/otadata-20261021-000647.bin
rollback selects a slot without a trial marker. It does not copy any images. To bring back an image you backed up earlier, install that file like any other build. It goes into whichever slot is not running, on trial, and backs up what it replaces:
.venv/bin/python tools/vios_firmware_ab.py --port /dev/cu.usbserial-210 \
install backups/ab-2026-10-21/app1-20261021-000647.bin --backup-dir backups/ab-restore
Every esptool call and every port open resets the board. status and rollback therefore reboot it too.
Prepare the SD card
The eZ80 owns the microSD card. MOS boots from it, and VIOS keeps its modules, boot bundles and your files on it. Use a FAT32 card (MBR partition table). Cards up to 32 GB are the conservative choice.
With the card in your Mac (first install, or a card you can remove), use tools/vios_sd_install.py:
make viosboot # builds build/viosboot.bin
.venv/bin/python tools/vios_sd_install.py install /Volumes/AGON --dry-run
.venv/bin/python tools/vios_sd_install.py install /Volumes/AGON
.venv/bin/python tools/vios_sd_install.py check /Volumes/AGON --resident build/vios-resident.bin
install CARD writes these files:
| File | Content |
|---|---|
/mos/viosboot.bin |
The boot stub (a MOS "moslet", at most 32 KB). MOS runs it as the command viosboot. |
/vios/vios.bin |
The resident, the stub's fallback copy. |
/vios/vios.man |
Its 16-byte manifest: size, CRC-32, GUI flag and internal baud rate (the same bytes the ESP sends). |
/autoexec.txt |
Gains one viosboot line. Every other line is kept, and the original is saved once as autoexec.txt.vios-backup. |
/!boot.obey or /autoexec.obey |
If the card already has one, the first of them gains the viosboot line too. MOS 3 runs only the first of !boot.obey, autoexec.obey, autoexec.txt that exists. The installer never creates an obey file, because that would hide autoexec.txt. |
The installer also removes /vios/boot.cnt, the counter of unfinished boots, so a fresh install starts clean. Files are written under temporary names and renamed, the image before its manifest. An interrupted install therefore leaves a manifest that does not match the image, and the stub refuses to run it. Module bundles in /VIOS are not touched.
The options are as follows:
--resident BINand--stub BINselect other files (defaultsbuild/vios-resident.binandbuild/viosboot.bin).- The manifest's GUI flag and baud rate come from the
.guiand.baudsidecar files next to the resident, so keep them together. A missing.guisidecar is recorded as 0. --dry-runprints the report without writing.check CARD [--resident BIN]verifies a card. It checks the stub header, that the image matches its manifest, that the boot file MOS will run containsviosboot, and optionally that the card carries exactly that resident build. It exits non-zero on a problem.
With the card in the board (no need to take it out), use tools/vios_sd_refresh.py. It writes the same files through the running resident's SD handles:
# stop the harness first: this tool opens the port itself
.venv/bin/python tools/vios_sd_refresh.py build/sd-refresh.json --port /dev/cu.usbserial-210
.venv/bin/python tools/vios_sd_refresh.py build/sd-stub.json --port /dev/cu.usbserial-210 \
--stub build/viosboot.bin --stub-only
- It always writes
/vios/vios.binand/vios/vios.man.--stub BINalso writes/mos/viosboot.bin, and--stub-onlywrites only the stub. - It does not edit
autoexec.txt, so the card must already have been set up once withvios_sd_install.py. - The resident has no rename. The tool therefore first overwrites
vios.manwith zeros (a manifest the stub refuses), then writes the image, then writes the real manifest. Afterwards it reads back the manifest and the first 64 KiB of the image. - Writing goes 100 bytes per command, so a 195 KB resident takes about 5 minutes. The report goes to the JSON file you name, next to a raw serial log.
- Opening the port resets the board, which must then boot to
BOOT READYon its own. The tool never writes flash.
After every confirmed ESP install, refresh the SD copy too. Then the fallback image matches the firmware.
Untethered boot
With the firmware installed and the card prepared, the board boots VIOS on its own. No Mac and no ZDI wire are needed:
- The ESP32 starts, brings up VGA and the display server, and tries the ZDI debug link. With no wire present it reports
BOOT user modeand lets the eZ80 run normally. - MOS starts from the eZ80 flash and runs the boot file on the card, which runs
viosboot. - The stub puts the CPU-to-ESP link on the internal rate (1,152,000 baud). It asks the ESP for the manifest of its embedded resident, then streams the image to
0x040000in 192-byte blocks with a CRC each, 32 blocks per request. - It checks the CRC-32 of the whole image and the MOS header, tells the ESP where the image came from, and jumps to it.
- The resident starts, loads the confirmed boot bundle of modules from SD, and reports healthy.
The ESP image comes first because it is guaranteed to match the firmware: same baud rate, GUI pairing and protocol. The SD copy is the fallback. MOS's command line lets you choose differently, for example if you edit the boot file:
| Command | Effect |
|---|---|
viosboot |
ESP image first, then /vios/vios.bin. |
viosboot sd |
SD copy first, then the ESP image (useful for trying a CPU-only build without reflashing the ESP). |
viosboot force |
Ignore the unfinished-boot counter (also viosboot force sd). |
The stub counts boots in /vios/boot.cnt, and the resident clears the count once it is healthy. After three boots that never finished, the stub stays in MOS with VIOSBOOT: 3 boots did not finish; staying in MOS (viosboot force). If neither image is usable it prints VIOSBOOT: no bootable VIOS image; staying in MOS. If no card or no stub is found, the console window shows "VIOS: no boot SD card, or no viosboot on it. Insert the VIOS SD card and reset the board." See First Boot and Setup for the timings.
Recovery
Recovery goes in levels, from least to most invasive. Use the first one that works.
- In VIOS itself. Kill a stuck task (
TASKS, thenn TASK-KILL), inspect the error withLAST-ERROR, revert a module withMODULE-ROLLBACK, or select the previous boot bundle withBOOT-REVERT. See Modules and Persistence and Troubleshooting. - Restart. Press the reset button, power-cycle the board, or stop and start the harness (opening the port resets the board). In the CLI,
:rebootrestarts from an idle prompt and:recoverforces it. On an untethered board, though, these restart only the ESP32 and the resident carries on (see The Host Harness and MCP Tools). A full restart loses RAM work; saved modules come back from SD. - Boot from the other image. At the MOS prompt (shown when the stub gives up), run
viosboot forceorviosboot sd. Or fix the card from the Mac withvios_sd_install.py install. - Go back to the previous firmware. Run
vios_firmware_ab.py ... rollback, or install a backed-up slot image as shown above. A firmware on trial does this automatically when it fails. - The ZDI supervisor (wire attached). With the two ZDI links fitted (see Hardware and Wiring), the ESP boots in dev mode. It halts the eZ80, verifies a 46-byte native bootstrap in on-chip RAM, sets up external RAM, writes and CRC-checks the embedded resident, and starts it. This works whatever is on the SD card or in MOS. In this mode an independent USB control task can always halt the CPU, read registers and memory, and restart everything, even if a graphics command or Forth program has wedged the VDP. The harness exposes this through
debug_registers,memory_read,memory_write,resume,deployandreboot(force=true). A 30-second boot watchdog restarts the ESP after each of the first two failed boots and leaves the CPU halted, with USB debug available, after the third.
The eZ80 flash is never written by a normal install, update or recovery. tools/vios_ez80_flash.py exists. It can stage a MOS.bin in eZ80 RAM and, with write ... --approved, erase and program the flash over the ZDI wire, verified by readback. It was used once, to put MOS 3.0.2 on the reference board. A failed flash write leaves the board unable to boot MOS until it is reprogrammed over ZDI. Treat this tool as a last resort for a board whose MOS is damaged or too old (untethered boot was qualified with MOS 3.0.2), and keep the original flash backup (backups/ez80/) at hand. tools/zdi_backup.py and tools/zdi_monitor.py are bring-up tools from the first day. They load a temporary helper into ESP32 RAM (make esp32-probes) to read the eZ80 flash or run a raw monitor. You do not need them.