Building and Testing Pico C Firmware¶
Run these software checks from the repository root. Native tests compile Pico logic for the host with hardware replacements; the instruction emulator runs Z80 code, not ARM firmware. Cross-builds use the real Pico SDK. Physical board testing remains a separate qualification step.
1. Understand the four validation layers¶
| Layer | What runs | What it establishes |
|---|---|---|
| Native C/C++ host tests | Maintained firmware logic compiled for the Mac or other host, with test SDK headers and functions | Software state transitions, bounds, error handling and scripted fault/interleaving cases |
| Z80 execution | Assembled Z80 programs, BIOS and CP/M in an instruction emulator, with modeled peripherals | Actual Z80 instruction behavior, console/disk protocol use, boot and warm-boot behavior |
| Pico cross-build | ARM compiler and linker with the real Pico SDK | Firmware compiles/links for Pico 2 W, SDK APIs are valid and artifacts fit the protected flash layout |
| Physical bench qualification | Deployed firmware, real Pico, Z80, SRAM, buffers and flash | Actual USB behavior, multicore/interrupt operation, bus ownership, timing, electrical margins and sustained reliability |
2. Prerequisites¶
Follow Toolchain for checkout and installation. Native tests
need host C/C++ compilers, CMake, Ninja and Python 3.11+. On macOS their
Z80 assembly runs in Apple Container; other hosts need z80asm. Set PYTHON
to your installed interpreter:
export PYTHON=python3
"$PYTHON" --version
Node.js optionally enables terminal_keyboard; no npm installation is needed
for native tests. The Pico SDK and Arm toolchain are needed only for SDK/TLS
checks and firmware cross-builds.
3. Configure, build and run the native C tests¶
Use a separate build directory from the Pico cross-build. Explicitly select
the host compilers, not arm-none-eabi-gcc:
cmake -S src/common/tests -B build/host-control -G Ninja \
-DCMAKE_C_COMPILER=cc \
-DCMAKE_CXX_COMPILER=c++
cmake --build build/host-control
ctest --test-dir build/host-control --output-on-failure --no-tests=error
The offline suite covers control/loader state, flash recovery, terminal
queues, services, PIO banking, and HTTP/weather/chat parsing and transport.
Warnings are errors and assertions remain enabled. The optional
npm run test:control runs the same configure/build/test sequence.
On macOS/Linux, build the optional live runner with the firmware’s real Mbed TLS
configuration and trust bundle, configure with PICO_SDK_PATH:
cmake -S src/common/tests -B build/host-control -G Ninja \
-DCMAKE_C_COMPILER=cc -DCMAKE_CXX_COMPILER=c++ \
-DPICO_SDK_PATH=/absolute/path/to/pico-sdk
cmake --build build/host-control --target http_live_test
python3 src/common/tests/http_transfer_integration.py \
--runner build/host-control/http_live_test --https
The live runner needs openssl; --https also needs internet access. It tests
local HTTP binary/text transfers, empty files, redirects, chunking, 404 errors,
untrusted TLS rejection, and a verified GitHub HTTPS download. The production
FT/parser/crypto code runs through a native socket adapter; Pico lwIP behavior
is covered separately by offline transport tests.
What replaces the hardware?¶
Test SDK replacements model flash as a 4 MiB array, SRAM as arrays or injected cycles, GPIO/clock state, controlled time, USB queues, reboot, and scripted core interleavings. They enforce software contracts without reproducing physical IRQs, dual-core scheduling, or electrical timing.
Phase 9 coverage and its boundaries¶
| CTest name | Maintained code exercised | Important boundary |
|---|---|---|
flash_backend_recovery |
Actual flash_backend.c: manifest validation, byte loading/readback controller, cache, journal programming and recovery |
Flash, SRAM byte access, bus/clock calls and reboot are modeled |
stage09_usb_cpm |
Actual Phase 9 application functions and shared terminal_queue.c |
Boot/storage and SDK calls are substituted; command/stop interleavings are driven by the harness |
usb_core1_console |
Actual usb_console.c routing and bounded diagnostic ring |
Modeled SDK endpoints assert core-1 affinity; checks concurrent producer/drain, CRLF vs raw console bytes, backpressure, wrap, overflow, disconnect, init failure, batched input and the lock-free idle pass, not physical IRQ delivery |
stage09_autoboot_cpm |
The same application compiled with automatic startup, including its actual main initialization path |
SDK/storage calls are substituted; tests cover boot before USB connection, retained CP/M output, late failure reporting and no automatic retries |
disk_protocol |
Actual disk_device.c: Z80-facing commands, single-slot mailbox, BUSY ownership, reads, read-ahead hits/misses, seqlock fallback, write invalidation and fatal error latch |
The flash backend is stubbed; this is not the journal test |
assisted_loader, control_faults |
Shared injected-loader and control code | Cycle/pin models do not establish physical setup/hold timing |
Fault cases include invalid packages, cancelled loads, SRAM mismatches, interrupted journal writes, queue overflow, disconnects, and initialization failures. Harnesses check core ownership, disk-first scheduling, safe stops, and automatic boot before USB connection. A requested 20 MHz clock is checked as a software value, not measured as a signal.
For a focused native Phase 9 run:
cmake --build build/host-control \
--target flash_backend_test stage09_test stage09_autoboot_test disk_test
ctest --test-dir build/host-control \
-R '^(flash_backend_recovery|stage09_usb_cpm|stage09_autoboot_cpm|disk_protocol)$' \
--output-on-failure --no-tests=error
The authoritative target/test list is src/common/tests/CMakeLists.txt.
4. Z80 execution tests¶
The shared Apple container runs the C Z80 emulator.
Install Apple Container, Python and CMake on macOS and initialize the
submodules. For the full suite, install DCC on the host PATH or set DCC_DIR
to its tool directory. Set Z80_FT_SOURCE if the FT assembly source is not
in the sibling ESP32 Altair checkout.
python3 scripts/z80_container.py test
The suite uses an isolated workspace and runs the emulator
instruction and bank regressions first. The test container image is reused
until its build inputs change. To rebuild it explicitly, add --rebuild.
Required example and test programs are compiled with host DCC in the temporary
workspace. Focused BIOS and emulator tests do not require DCC. Select a test
with, for example:
python3 scripts/z80_container.py test --test banked_allocator_z80_test.py
Add an independently checked regression for each emulator bug.
The math/scope and bank-transfer programs execute on the emulator. CP/M tests cover cold/warm boot, directories A–D, transients, AUTORUN, expanded disk writes, and the read-only disk diagnostic. Modeled peripherals establish instruction/protocol behavior, not Pico latency or hardware timing.
5. Validate CP/M image construction¶
On Apple silicon macOS, run against this checkout inside the shared container:
python3 scripts/z80_container.py image
container run --rm --mount "type=bind,source=$PWD,target=/workspace,readonly" \
--entrypoint python3 z80-cpm:local \
-m unittest discover -s /workspace/src/cpm -p 'test_*.py' -v
With Python 3.11+, z80asm, and cpmtools installed on another host:
"$PYTHON" -m unittest discover -s src/cpm -p 'test_*.py' -v
These image tests check packaging, CRCs, exact memory/disk geometry, BIOS jump-table compatibility and CP/M translation. They are not the instruction-execution tests above.
The container's CP/M host also tests multi-extent programs and cross-drive writes; see the end-to-end CP/M procedure. Neither host model replaces the Pico/physical-board gate.
6. Cross-compile the real Pico firmware¶
Install the SDK and complete Arm toolchain using Toolchain. Keep the firmware build separate from host tests.
Then configure the separate firmware build:
cmake -S . -B build/pico -G Ninja \
-DPICO_BOARD=pico2_w \
-DPICO_SDK_PATH="$HOME/GitHub/pico/pico-sdk" \
-DPython3_EXECUTABLE="$(command -v "$PYTHON")"
cmake --build build/pico \
--target z80_stage09_cpm_images z80_stage09_cpm_loader_images z80_cpm_images
This builds both Phase 9 variants and Phase 10 against the real SDK, without flashing. Phase 10 Wi-Fi credentials are configured through USB/ENV at runtime; they are not build arguments.
Output under build/pico/ |
Purpose |
|---|---|
src/stage09_flash_storage/z80_stage09_flash_storage.uf2 |
Deployable Phase 9 Pico firmware |
src/stage09_flash_storage/z80_stage09_cpm_loader.uf2 |
Automatic Phase 9 loader, independent of USB connection |
src/stage10_websocket_terminal/z80_stage10_websocket_terminal.uf2 |
Deployable Phase 10 Pico firmware |
cpm-stage09/ |
Matching Phase 9 boot package, disks, manifest and complete flash image |
cpm-stage09-loader/ |
Matching automatic-loader provisioning set |
cpm/ |
Matching Phase 10 provisioning set |
Both phases run the Z80 at 20 MHz after SRAM loading/verification. Phase 9
starts on command or automatically with its loader variant; Phase 10 starts
automatically. The linker reserves flash above 0x1D0000 for storage.
Use VS Code deployment tasks when ready to flash. BIOS changes require matching boot-package and Drive A images; full provisioning replaces disks and ENV.
7. Record software results, then qualify the board¶
Record the revision/local changes, tool versions, configuration, test names, results, and artifact hashes. Distinguish host tests, Z80 execution, SDK builds, and physical bench results. Software checks do not establish electrical margins, real interrupt/multicore timing, USB behavior, or power-cut recovery. They need no attached Pico; leave active hardware soaks and their serial owners undisturbed.
After software validation, follow the Phase 9 controlled USB workflow and its bench checklist. Observe the CP/M prompt and filesystem behavior, verify safe stop/recovery, and collect the required frequency/timing evidence. The Z80 clock has been scope-measured at 20 MHz on the project breadboard. Integrated CP/M, disk-I/O and receiving-pin timing checks remain separate qualification evidence.