Skip to content

I/O Ports and Services

The Pico provides virtual peripherals through Z80 IN and OUT instructions. This page owns the port allocation and the ROMLESS-SVC/1 extended-service protocol, compatible with the ESP32 Altair io_ports.h and dcc DXSVC.H client. Extended services are implemented in both Phase 9 variants and Phase 10. Phase 8 retains its original bring-up diagnostics.

Maintained sources: transport and timers, service worker, clock and utilities, persistent environment store and public constants. See operational boundaries for the terminal, disk and clock-stop trap contracts.

Port allocation

The four monitored address bits, A0/A1/A2/A4, provide 16 distinct ports: 0x00-0x07 and 0x10-0x17, using address & 0x17. Other addresses are aliases, not additional ports. For example, 0x08 aliases 0x00 and 0x18 aliases 0x10. No additional wiring is required.

Port (hex) Decimal Function Allocation
0x00 0 Terminal data, IN/OUT Fixed
0x01 1 Terminal status Fixed
0x02 2 Shared service command (OUT) / status (IN) Implemented
0x03 3 Shared service length-prefixed request bytes (OUT) Implemented
0x04 4 Shared service length-prefixed response bytes (IN) Implemented
0x05 5 BIOS cold-entry/CCP-handoff reports; Phase 9 disk-test and Phase 10 qualification results Reserved for diagnostics
0x06 6 Phase 9 disk-test bytes and Phase 10 qualification data; Phase 8 test results Reserved for diagnostics
0x07 7 SRAM bank select (OUT bit 0) / capability and selected bank (IN) PIO-only bank switching at 20 MHz; IN A,(07h) uses the trap to read availability and selected bank
0x10 16 Disk command (OUT) / status (IN) Fixed
0x11 17 Disk drive selection Fixed
0x12 18 Disk sector address, low byte Fixed
0x13 19 Disk sector address, high byte Fixed
0x14 20 Disk data, IN/OUT Fixed
0x15 21 Speaker sound engine packets (OUT) / status (IN) Stage 10 and host emulator
0x16 22 Available Unallocated
0x17 23 Independent countdown timers and stopwatches, IN/OUT Implemented

This assigns or reserves 15 ports, leaving one available. A port supports both IN and OUT, so a command and its status can share one number.

Bank select returns B4h/B5h when the loader verified banked SRAM, or 00h when unavailable (including when PIO is inactive). GP8 drives A16 through a spare AHCT244 channel. Use the banked-memory transfer library and wiring for C programs: a raw bank select also switches the executing code and stack.

The BIOS emits B0h on port 0x05 at cold entry and C0h immediately before handoff to CCP (including warm boot). Phase 9 observes these as execution milestones, not proof of a working CP/M prompt. Its read-only disk test uses port 0x06 for compared bytes and 5Ah/E1h on port 0x05 for completion/ failure. These diagnostic meanings are mode-specific and do not change the fixed terminal or disk ABI.

HTTP and HTTPS file transfer

Phase 10 supports the FT.COM protocol through shared-service group 0x70 on ports 0x02/0x03/0x04. Phase 9 returns HTTP_TRANSFER for downloads.

Set HTTP_BASE_URL using interactive ENV to preserve URL case: run ENV, choose A to add or an entry number to edit, then Q to quit. For example:

https://raw.githubusercontent.com/AzureSphereCloudEnabledAltair8800/RetroGames/refs/heads/main/

Download text with FT -G LOVE.BAS, then run MBASIC LOVE. Use FT -GB PROGRAM.COM for binary files. Text mode normalizes CR LF and appends ^Z; binary mode preserves bytes. Files use uppercase 8.3 basenames and 128-byte record padding. Omit the filename (FT -G or FT -GB) to enter a case-preserving path at the prompt.

Full HTTP/HTTPS URLs need no base setting. Relative paths extend the base; a leading / starts at the server root. For a local server, run python3 -m http.server 8000 --bind 0.0.0.0 and set the base to its LAN URL, for example http://192.168.1.10:8000/.

HTTPS requires Wi-Fi, synchronized SNTP time, and a trusted certificate chain and hostname. The CA bundle can be replaced with -DZ80_HTTP_CA_FILE=/absolute/path/to/bundle.pem. Plain HTTP needs no synchronized clock.

Each request uses the shared framing, including its 16-bit little-endian lengths. The payload includes the operation byte:

Request payload Result
[1, filename/URL bytes] Open/replace the session; COMPLETE, then ACK
[3] BUSY until a chunk or EOF; RESPONSE_READY with [state, file bytes...]
[4] Cancel/close; COMPLETE, then ACK

Names contain 1-767 bytes without a NUL terminator. Read responses contain one state byte (0 more data, 2 EOF) followed by 0-256 file bytes, so their framed length is 1-257. EOF can accompany the final data bytes; an empty file returns [2]. HTTP_BASE_URL (0x20) and HTTP_TRANSFER (0x21) are shared errors, not response states. Invalid operations, lengths and embedded NULs use the standard shared errors.

Consume each response fully before another shared transaction. Other groups can run between chunks without changing the open session. Explicit shared RESET cancels a pending HTTP read; close or open replaces the session. Transfers stream on core 1 without loading the whole file into RAM. HTTP/1.0 and 1.1, chunked bodies, and up to five redirects are supported; HTTPS downgrades, compressed or malformed responses fail. Network stalls time out after 30 seconds, excluding consumer backpressure. See http_io.c and http_stream.c for implementation limits.

Why A4 is monitored but A3 is not

Only A0/A1/A2/A4 are decoded (address & 0x17). A3, A5–A7, and the high address byte are ignored: 0x09 aliases terminal status 0x01, 0x30 aliases disk command 0x10, and 0xFF aliases timer port 0x17. A4 keeps console and disk registers distinct. This applies only to I/O; A3 still connects Z80 pin 33 to SRAM pin 9, not U7.

Each register has 16 low-byte aliases. Scanning the seven registers finds 111 active addresses (112 aliases, excluding 0x00), not 111 peripherals; the count changes as services are added. Scanning response ports can cause UNDERFLOW; RESET the shared channel afterward. Service group 0x30 is a command byte written to 0x02, not a port address.

Shared service interface

Framing and recovery

There is one outstanding shared transaction at a time:

  1. Write RESET (0x01) to port 0x02 to recover an unknown parser state.
  2. Write the service group to 0x02.
  3. Write a 16-bit little-endian payload length to 0x03, then that many payload bytes. Length includes the operation byte and must be 1-2048.
  4. Poll status on 0x02 with a bounded client-side polling limit.
  5. For RESPONSE_READY, read the 16-bit little-endian response length from 0x04, then exactly that many bytes. The last byte returns the channel to READY. An empty response still has its two length bytes.
  6. For COMPLETE, write ACK (0x00) to 0x02.
  7. For ERROR, read the length prefix and one-byte error code from 0x04, then ACK. If an overlapping transaction poisoned the parser, RESET is required instead.
Status Value Meaning
READY 0x00 Can accept a new command
COMPLETE 0x01 Successful operation with no response; ACK required
RESPONSE_READY 0x02 Length-prefixed response can be read
COLLECTING 0x03 Awaiting the length prefix or remaining payload
BUSY 0x04 Worker executing the request
ERROR 0xFF Length-prefixed error can be read

Special commands on 0x02 are ACK (0x00), RESET (0x01) and IDENTIFY (0x02). IDENTIFY needs no request and returns the text ROMLESS-SVC/1 with the usual response length prefix. Text replies have no trailing NUL; binary replies are not strings.

RESET invalidates pending replies through a generation identifier, but does not undo side effects or cancel a worker already writing flash. A new worker request can return BUSY_ERROR until that job finishes. RESET does not reset running timers or stopwatches. There is no automatic server timeout for an incomplete request: the client must use bounded polling, report failure and RESET. Reading status never acknowledges COMPLETE.

Worker ownership

Core 0 handles traps and timers; core 1 runs slow services after disk work. The trap never performs flash/network work or waits for a service response. Worker replies are read in place and remain valid until consumed or discarded. Generation tags reject stale replies after RESET. One shared transaction may be outstanding at a time.

Error codes match the ESP32 client:

Code Error
0x01 / 0x02 UNKNOWN_GROUP / UNKNOWN_OPERATION
0x03 / 0x04 INVALID_LENGTH / INVALID_PARAMETER
0x05 / 0x06 UNAVAILABLE / BUSY_ERROR
0x07 / 0x08 / 0x09 TIMEOUT (reserved) / INVALID_STATE / UNDERFLOW
0x10 / 0x11 / 0x12 STORAGE_OPEN / STORAGE_READ / STORAGE_WRITE
0x13 / 0x14 CAPACITY / NOT_FOUND
0x15 / 0x16 CLOCK_UNSYNCED / REBOOT_CONFIRMATION
0x20 / 0x21 HTTP_BASE_URL (missing/empty) / HTTP_TRANSFER

Groups and operations

All integer request fields and binary results are little-endian.

Group Operations and payloads Result
Timers 0x10 [0, id, duration_u32] starts/replaces; [1, id] queries COMPLETE or one byte, 1 running / 0 expired
Stopwatches 0x11 [0, id] resets; [1, id] reads seconds; [2, id] reads milliseconds COMPLETE or unsigned 32-bit elapsed count
Clock 0x20 [0] uptime seconds; [1] UTC ISO 8601; [2] local ISO 8601; [3] long local date; [4] DATE-style local date/time Text
Utility 0x21 [0] random; [1, selector] system info; [2, 0xA5] confirmed reboot; [3] version; [4, state] set onboard LED; [5] read LED state Two random bytes, text, COMPLETE, or one LED-state byte
Environment 0x30 Operations below Text or COMPLETE
Weather 0x40 [0, field_id] reads text; [1] reads status Phase 10 cached weather; Phase 9 UNAVAILABLE
Chat 0x50 Reset/append/submit/status/chunk operations below Phase 10 streaming OpenAI/Ollama; Phase 9 UNAVAILABLE
Sense HAT 0x60 Recognized group without a Pico driver UNAVAILABLE
HTTP(S) 0x70 [1, name...] open; [3] read; [4] close COMPLETE or [state, up to 256 file bytes]; details above

There are four countdown timers: IDs 0-2 take 0-65535 milliseconds; ID 3 takes 0-255 whole seconds. The shared duration field is four bytes for client compatibility, but out-of-range values are rejected. Timer 3 uses the ESP32 whole-second boundary rule: (current_ms / 1000 + duration) * 1000. A zero duration expires immediately.

There are three stopwatches, IDs 0-2. Reads do not reset them. A latched 32-bit millisecond count wraps after about 49.7 days; seconds wrap after about 136 years. Internal deadlines and uptime use a 64-bit monotonic clock.

Phase 10 starts SNTP against pool.ntp.org after Wi-Fi association. Calendar operations return CLOCK_UNSYNCED until the first valid synchronization. Phase 9 has no network time source and returns CLOCK_UNSYNCED for calendar queries. Uptime remains available in both. Local formatting uses persistent TZ in POSIX timezone syntax (for example, AEST-10); an absent TZ means UTC.

CLOCK application time compatibility

The ESP32 Altair dcc Apps/CLOCK time interface is already provided by these services; no extra physical ports or separate time driver are needed:

CLOCK request Pico interface Result
Local time Clock group 0x20, payload [2] on shared ports 2/3/4 YYYY-MM-DDTHH:MM:SS (19 bytes)
Local date Clock group 0x20, payload [3] Monday, 01 January 2024
Uptime Clock group 0x20, payload [0] Decimal seconds since Pico startup
Start refresh timer OUT 23: 0x12, 50, 0 Timer 2, 50 ms, low duration byte first
Query refresh timer OUT 23: 0x22, then IN 23 1 while running, 0 when expired

The timer port works while a clock request is BUSY. Phase 10 uses SNTP at pool.ntp.org; calendar calls return CLOCK_UNSYNCED (0x15) until synchronized. Time then advances through Wi-Fi outages. Uptime and timers need no network.

Set local time with POSIX TZ, for example ENV TZ=AEST-10 (UTC+10) or ENV TZ=ACST-9:30 (UTC+09:30). POSIX signs are reversed; daylight-saving rules are supported. Changes apply on the next request. UTC_OFFSET is unused. CLOCK’s weather display requires OWM_KEY and OWM_LOCATION in Phase 10; Phase 9 reports weather UNAVAILABLE.

CHAT application: OpenAI and Ollama

Phase 10 implements the framed ABI used by the ESP32 Altair dcc Apps/CHAT/CHATC11.C application. Use the existing CP/M application and its CHAT.SYS system-prompt file; no BIOS, pin or disk-layout change is needed. CHAT 1.7-c11 adds application-side thinking control as described below; earlier versions still work but leave thinking at the server default. Firmware builds do not install the application on a disk. Do not use physical ports 120-124 for chat: this board's address decoding aliases them onto unrelated peripherals.

All requests use group 0x50 through shared ports 2/3/4:

Payload Result
[0] Cancel any previous request and clear request assembly; COMPLETE
[1, json_bytes...] Append 1-2047 bytes; COMPLETE, or explicit CAPACITY/BUSY/invalid-input error
[2] Cancel the connection and clear queued response/error text; COMPLETE; assembled request is retained
[3] Submit the assembled request asynchronously; COMPLETE, or BUSY/empty-request error
[4] One status byte: 0 EOF, 1 WAITING, 2 DATA_READY
[5, maximum_u16] [status, count_u16, text_bytes...], with status after removing the returned bytes

maximum is 1-2045, bounded by the 2048-byte service payload. CHATC11 requests 256 bytes at a time and appends JSON in 2047-byte pieces. The assembled request limit is 8191 bytes, excluding its local NUL terminator. Overflow never silently truncates a request. Appending or resubmitting while active returns BUSY. Operation 0 cancels before reusing the request buffer, so a new turn recovers safely after CHATC11 fills its local response buffer or times out. Operation 2 also cancels immediately. Shared transport RESET only invalidates the mailbox reply; as with ENV, it does not undo service side effects.

Provider configuration

For a local Ollama server with its OpenAI-compatible API:

ENV CHAT_PROVIDER=compatible
ENV CHAT_ENDPOINT=http://192.168.1.26:11434/v1/chat/completions
ENV -I CHAT_MODEL=phi4-mini:3.8b
ENV CHAT_MAX_TOKENS=1024
ENV CHAT_TEMPERATURE=0.7

CHAT 1.7-c11 reads CHAT_THINK each turn for compatible/Ollama providers: false sends reasoning_effort=none, true sends medium, and default omits the field. Model/server support determines the effect. Missing/empty values default to false; invalid values fail submission. OpenAI requests omit this setting. CHAT binaries that do not send it retain server defaults.

The USB shell also offers Ctrl-] c → 3 Chat → 7 configure thinking. CHAT_PROVIDER=ollama aliases compatible. Use an installed model name; the example address/model is configuration, not a required endpoint.

For OpenAI:

ENV CHAT_PROVIDER=openai
ENV -I CHAT_OPENAI_KEY=<your-api-key>
ENV -I CHAT_MODEL=gpt-4o-mini
ENV CHAT_MAX_TOKENS=1024
ENV CHAT_TEMPERATURE=0.7

The ENV CLI evaluates +/- inside ordinary assignments as arithmetic. Use literal -I for API keys and model names; it only inserts absent keys. To replace an existing key, first use ENV -D CHAT_MODEL (or ENV -D CHAT_OPENAI_KEY), then its literal insert command. Do not paste real keys into logs or source control. ENV is plaintext onboard flash, not encrypted secret storage.

An absent provider defaults to openai. OpenAI always uses https://api.openai.com/v1/chat/completions, ignoring CHAT_ENDPOINT. Only OpenAI requests carry CHAT_OPENAI_KEY; a saved OpenAI key is never forwarded to an Ollama/compatible endpoint. Authenticated third-party compatible providers are not supported. URLs require an explicit path and accept HTTP or HTTPS, a DNS name/IPv4 address, and an optional port 1-65535. User-info, fragments, whitespace and control characters are rejected. Limits are 95 host bytes, 103 authority bytes, 159 path bytes, and 255 key bytes.

Core 1 reads provider/endpoint/key from ENV on each submission. CHATC11 owns the JSON model, history, system prompt, token count, temperature and thinking parameter; restart CHAT after changing model/token/temperature settings. Thinking/provider changes are read at the next turn by CHAT 1.7-c11. HTTPS requires synchronized SNTP time and verifies certificate dates, chain and hostname using the bundled GTS Root R4. There is no insecure TLS fallback. Compatible HTTPS endpoints need a chain trusted by that root; arbitrary private/self-signed CAs are not supported. Plain HTTP Ollama works without SNTP but must remain on a trusted LAN.

Streaming and limits

Core 1 owns the 4096-byte response ring and async client. Bounded network polls and backpressure keep the trap and other services responsive; generation tokens reject stale callbacks after cancellation.

The decoder requires HTTP 200, uncompressed text/event-stream, and a final [DONE]. It accepts content-length, connection-close, and chunked bodies. Only choices[0].delta.content is displayed; other fields are ignored. JSON/UTF-8 are validated and converted to CP/M-safe 7-bit text. Limits are 4096 header bytes, 4095 bytes per HTTP/SSE line or decoded event, and 16 JSON nesting levels.

The firmware deadline is 120 seconds, including backpressure. Failures publish Chat: ... after queued text, then EOF; requests are not automatically replayed. CHATC11 also has a roughly 30-second no-content timeout and an 8191-byte answer limit, so it may stop first. Diagnostics omit credentials and conversation text.

OpenWeatherMap weather

Phase 10 supports the ESP32 Altair weather ABI through shared service group 0x40 on ports 2/3/4. Do not use the old physical ports 46/47/200: these alias other peripherals on this board.

  • Payload [1] returns one status byte: 0 NONE, 1 FETCHING, 2 READY, or 3 ERROR. Status reads never wait for an HTTP request.
  • Payload [0, field_id] returns length-prefixed text, without a trailing NUL. Units and error text are available before the first successful fetch; other fields return UNAVAILABLE until data exists.
  • Unknown operations, incorrect lengths and unknown fields return the usual explicit service errors. An HTTP failure is exposed by status ERROR and field 12, not by inventing weather data or failing every status query.
Field Text
0 City
1 / 2 Current condition / description
3 / 13 Current temperature / feels-like temperature
4 / 5 Humidity percent / wind speed
6 / 7 Forecast condition / description
8 / 14 Forecast temperature / feels-like temperature
9 First forecast block time, as supplied by OWM in UTC
10 Seconds since the last successful current-data refresh
11 Temperature units: C, F, or K
12 Last error or forecast warning; empty after a fully successful refresh

Temperatures and wind speed are rounded to integer strings as in the reference driver; wind is m/s for metric/standard and mph for imperial. Only the first forecast block is requested (cnt=1).

Configure the existing durable ENV store:

ENV OWM_KEY=<your-api-key>
ENV OWM_LOCATION=Sydney,AU
ENV OWM_UNITS=metric

OWM_KEY is limited to 40 bytes, location to 64 bytes. Location accepts a city/country query or a validated lat=-33.8&lon=151.2 coordinate pair. The ENV CLI treats + and - inside assignments as arithmetic; use its literal insert mode for coordinates or names containing these characters: ENV -D OWM_LOCATION, then ENV -I OWM_LOCATION=lat=-33.8&lon=151.2. Units are metric (default), imperial, or standard. Missing keys or invalid settings produce an explanatory error; no request is sent. Settings are re-read after each ENV commit. Changing them cancels an in-flight request, clears data belonging to the old configuration, and starts a new fetch without rebooting. ENV remains plaintext onboard flash; do not publish its contents. Firmware diagnostics never print the key, request URL or provider response body.

Core 1 waits for Wi-Fi/SNTP, then 5 seconds before fetching. Refreshes repeat every 20 minutes; initial failures retry every 2 minutes for five retries. Reconnection triggers a fresh fetch. Cached data survives fetch failures with an increasing age and ERROR status. A forecast-only failure keeps current conditions READY and reports a warning in field 12.

HTTPS requires certificate-chain, hostname, and SNTP-based date verification, using weather_ca.pem. There is no insecure fallback. Requests have a 15-second deadline, 16 KiB wire/8 KiB decoded limits, and at most 2 KiB of headers. Content-length, connection-close, and chunked responses are supported; malformed, oversized, or compressed bodies fail. TLS/parsing run on core 1, outside Z80 traps.

System information and reboot

System-info selectors are 0 = hostname z80-nexus-xxxxxx (the final six hexadecimal characters of the board ID, in lowercase), 1 = current Wi-Fi IPv4 address, and 2 = Pico unique board ID. The IPv4 selector returns UNAVAILABLE without a connection, including Phase 9. Reboot requires the exact confirmation byte 0xA5; it flushes queued/cached disk writes, then core 0 disables traps, holds reset, isolates the data paths and stops the clock before watchdog reboot. New disk commands are gated before the reboot job is published, with BUSY+ERROR reported on disk status, so writes cannot race the final flush. Failed flushes reopen the gate and report STORAGE_WRITE instead.

Onboard LED device

Utility group 0x21 controls the Pico 2 W LED in Phases 9/10:

  • [4, 0] turns it off; [4, 1] turns it on. Success returns COMPLETE; ACK it.
  • [5] returns the last successfully commanded state (0/1), initially off.
  • Invalid state/length returns INVALID_PARAMETER/INVALID_LENGTH; driver failure returns UNAVAILABLE without changing the reported state.

The worker initializes the wireless-chip driver lazily on core 1. Phase 9 uses its LED-only backend; no Wi-Fi connection is required. State is not persisted, and transport RESET leaves it unchanged. This is asynchronous LED control.

For example, write 21h to port 02h, then 02h,00h,04h,01h to port 03h (group, length prefix, set operation, on). Poll for COMPLETE/ERROR, then ACK success or drain and acknowledge the error.

Persistent environment API

ENV is a core-1 system service. Flash is read and CRC-checked once, on first use; every lookup and command then uses the in-memory entries, which each commit keeps identical to flash. A change counter advances on every commit, so services that cache a setting, such as weather and the parsed TZ, refresh it only then. Operation 1 reports whether that first read succeeded.

Keys are case-insensitive, stored uppercase, and contain 1-16 non-NUL bytes. Values contain 0-255 non-NUL bytes. Empty and absent values are distinct: GET of an empty value returns a zero-length response; an absent key returns NOT_FOUND.

Operation Payload Result
0 reset / 1 initialize [operation] COMPLETE
2 get [2, key_length_u8, key_bytes] Value text
3 set [3, key_length_u8, key_bytes, value_length_u16, value_bytes] COMPLETE, durable before success
4 delete [4, key_length_u8, key_bytes] COMPLETE or NOT_FOUND
5 list [5] KEY=VALUE lines with CRLF
6 count [6] Decimal count text
7 clear [7] COMPLETE, durable before success
8 exists [8, key_length_u8, key_bytes] COMPLETE or NOT_FOUND
9 execute ENV command [9, command_text], at most 511 command bytes Text

The ENV command parser is shared with the maintained host driver. It supports listing, NAME, NAME=VALUE, copying another variable, numeric addition/ subtraction, NAME+N, NAME-N, -I NAME=VALUE, -D NAME, -N, NOW (uptime seconds), and -H/HELP/?. Numeric overflow is an explicit error. No additional physical ports expose the host's private selectors.

The store supports at most 128 entries, subject to a 3,840-byte serialized snapshot limit. Each entry consumes key length + value length + 2 bytes. There is room for at least 14 maximum-length key/value pairs. Requests that exceed storage or the 2,048-byte reply capacity fail explicitly; LIST never silently truncates. After a failed write, the in-memory state is reloaded from the newest valid durable snapshot; an error does not undo a commit that already reached flash.

Two 4 KiB sectors at 0x1FE000 and 0x1FF000 occupy unused space in the boot partition, beyond the boot payload ending at 0x1F1000 (exclusive). Firmware, the disk journal, boot package and all four disk offsets remain unchanged. Each snapshot has a version, sequence number, payload CRC32 and header CRC32. Updates erase/program the inactive sector, verify the payload, and commit the header last. The previous committed snapshot remains recoverable after an interrupted replacement. A durable empty baseline is established before the first mutation. Repeated unchanged writes do not erase flash.

Flash erase/program uses the disk backend's BUSREQ/BUSACK and flash-safe multicore handshake. A non-success flash-safe result fails closed through watchdog reboot. Both sectors erased means a new empty store; neither erased nor CRC-valid means STORAGE_READ, not a fabricated empty environment. Flash has finite endurance: use countdown timers and stopwatches, not repeated persistent writes, for high-frequency counters.

Preserving environment variables during updates

Firmware-only and exact boot-package-only updates preserve variables. Complete 4 MiB provisioning, mass erase and flash_nuke.uf2 destroy them. Do not write a padded 128 KiB boot-partition image: it would overwrite ENV. Full generated images deliberately contain erased environment sectors.

Worked request bytes

These examples show the actual wire framing; prefer the existing dcc svc_operation_send(), svc_get_text() and timer helpers in DXSVC.H. Use bounded client polling, drain error frames, and RESET after failure.

Read uptime:
  OUT 02,20
  OUT 03,01  OUT 03,00       ; payload length = 1
  OUT 03,00                 ; operation = uptime seconds
  IN 02 until 02 or FF       ; RESPONSE_READY or ERROR
  IN 04 twice, then length bytes

Set TZ=UTC0:
  OUT 02,30
  OUT 03,0A  OUT 03,00       ; payload length = 10
  OUT 03,03  OUT 03,02       ; SET, key length
  OUT 03,54  OUT 03,5A       ; "TZ"
  OUT 03,04  OUT 03,00       ; value length = 4
  OUT 03,55  OUT 03,54  OUT 03,43  OUT 03,30  ; "UTC0"
  IN 02 until 01 or FF       ; COMPLETE or ERROR
  OUT 02,00                 ; ACK only after success

Independent timer port

Port 0x17 uses no shared-service framing and remains usable while the service worker is BUSY. Its timers/stopwatches are the same instances as groups 0x10/0x11.

OUT command Subsequent transfers
0x00 Reset only this parser; running timers remain unchanged
0x10-0x12 Two OUT bytes, low then high: start millisecond timer 0-2
0x13 One OUT byte: start whole-second timer 3
0x20-0x23 IN returns selected timer's running flag
0x30 IN returns running bitmask, bits 0-3
0x40-0x42 Reset stopwatch 0-2
0x50-0x52 Four IN bytes: latched stopwatch seconds, little-endian
0x60-0x62 Four IN bytes: latched stopwatch milliseconds, little-endian

The parser initially selects the timer bitmask. An invalid command, an IN during parameter collection, or stopwatch over-read returns 0xFF and requires recovery. To recover an unknown state, perform IN 0x17, then OUT 0x17, 0x00. Multi-byte port-23 operations must not be interleaved with other users of that port.

Speaker port

Port 0x15 carries the Stage 11 ATtiny85 sound-engine I2C command packets byte for byte. Stage 10 forwards complete packets to the ATtiny85 on the shared OLED I2C bus. The engine owns all timing: a game sends one packet per sound (tone, sweep, siren, sequence, segments, pulses, vibrato, arpeggio, gate or one of 63 presets) and keeps running.

Transfer Meaning
OUT packet bytes Submit the complete packet; invalid packets are dropped
OUT 0x00 between packets Latch the 19-byte status
IN Next latched status byte, then 0xFF; also aborts a partial packet

An unknown command or bad count discards OUT bytes until the next IN, so every request starts with IN 0x15. Programs use the snd_* API in DXSVC; examples/SOUND exercises every effect and preset.

Stage 10 collects packets in the PIO IRQ handler without I2C work, allocation or waiting. The core 0 main loop sends them at 400 kHz using the shared PIO/DMA transport, as the OLED on the same bus does. Commands are fire and forget: written once, paced 500 µs apart, with no read-back. An IN waits for prior commands or a requested snapshot by deferring the PIO reply; the IRQ handler returns immediately. Subsequent snapshot bytes are RAM reads. OUT 0x00 reads fresh status from the device, not a stale background cache, and waits for a command still executing so the status is current.

The fixed bridge queue holds four packets, separate from the ATtiny85's two-sound playback queue. The SDK's initial IN serializes requests, so it does not fill the bridge queue. Raw clients must do the same. A command that the ATtiny85 refuses, or that arrives with the queue full, is dropped and counted in dropped on the Ctrl-] s [speaker] line; it is not retried. I2C failure, protocol mismatch or a stalled bridge reports [speaker] FAIL and disables sound until reboot. Waiting reads are released with version 0, idle playback and result 9 (invalid); snd_init() fails. There is no background polling when sound is unused. Phase 9 leaves this port unimplemented.

The host emulator compiles the real ATtiny85 engine and plays its Timer1 output through miniaudio on macOS, Linux and Windows; set Z80SBC_SPEAKER=off for silent wall-clock timing.

Availability and validation

Phase 10 implements file transfer, weather, and chat. Phase 9 reports download errors and weather/chat UNAVAILABLE. Sense HAT is unavailable in both. The ESP32 interrupt-timer ABI is unsupported: 0x07 selects SRAM banks, and this board has no Pico-to-INT# driver or interrupt-vector path.

Host tests cover framing, aliases, recovery, timers, ENV persistence/capacity, calendar formatting, HTTP/SSE parsing, cancellation, TLS configuration, and phase routing. They do not establish physical timing, live provider behavior, or power-cut recovery.