Run Arm’s CMSIS-Zephyr CI example in LabWired
Category: Tutorial
Arm’s CMSIS-Zephyr tutorial shows a complete hardware testing workflow. CMSIS-Toolbox and Zephyr build the application. A Raspberry Pi 5 runner uses ST-Link and pyOCD to load it onto a NUCLEO-H563ZI. RTT carries the console output, and the job saves a SystemView trace.
We tried that same build artifact in LabWired. The original ELF boots on the simulated STM32H563 and prints both LED states over RTT. Three assertions pass. This tutorial adds a simulation job to the original workflow, using a normal GitHub-hosted runner.
There is one model addition for LabWired v0.25.0: a DCACHE1 control-register stub. The firmware is unchanged. We show the addition below, along with what this smoke test proves.
Keep the CMSIS build
Start with a fork of Arm-Examples/CMSIS-Zephyr and follow its setup instructions. The solution file selects the application, target, packs, and build configuration. Zephyr’s west still performs the firmware build.
The original HIL build command is:
cbuild zephyr.csolution.yml --active NUCLEO-H563ZI@HIL --packs
The HIL target set selects blinky.Debug-RTT. That configuration enables the RTT console, disables the UART console, and selects RTT channel 1 for SystemView. The application’s console uses channel 0.
The generated CMSIS run file, zephyr+NUCLEO-H563ZI.cbuild-run.yml, identifies the ELF under out/:
out/blinky/NUCLEO-H563ZI/Debug-RTT/zephyr/zephyr.elf
That is the file LabWired runs. It is also the image the physical workflow loads through pyOCD. LabWired takes the ELF and its own board manifest directly; it does not execute the CMSIS debugger configuration.
Add the board manifest
Create .github/labwired/system.yaml:
name: "cmsis-zephyr-nucleo-h563zi"
chip: "stm32h563"
peripherals:
- id: "dcache1"
type: "dcache"
base_address: 0x40031400
size: "1KB"
The released CLI contains the STM32H563 chip model. This manifest adds the DCACHE1 register window that this Zephyr startup accesses. ST’s CMSIS device header defines DCACHE1 at this address.
Without that entry, the ELF raises a simulated bus fault at LL_DCACHE_Enable before reaching the console. With it, startup continues. The dcache type is an existing control stub: reads return zero and writes are discarded. It lets the enable sequence run; it does not validate cache contents, coherency, hit rates, or cache-dependent timing.
Assert the RTT output
Create .github/labwired/rtt-smoke.yaml:
schema_version: "1.0"
inputs:
firmware: "../../out/blinky/NUCLEO-H563ZI/Debug-RTT/zephyr/zephyr.elf"
system: "./system.yaml"
limits:
max_steps: 30000000
stop_when_assertions_pass: true
assertions:
- rtt_contains: "Booting Zephyr OS"
- rtt_contains: "LED state: OFF"
- rtt_contains: "LED state: ON"
The paths are relative to the script. The test stops after all three assertions pass, with a finite instruction budget if they do not. The assertions check RTT directly. UART text cannot satisfy an RTT assertion.
The original application configures led0, toggles the GPIO, prints its state, and sleeps for one second. Checking both messages takes the test beyond the first line of startup. The GPIO capture below records the actual simulated pad transitions as additional evidence.
Run the same ELF locally
Install LabWired v0.25.0. With Arm’s build output under out/ and the two files under .github/labwired/, run:
labwired test --script .github/labwired/rtt-smoke.yaml \
--rtt --watch-gpio gpiob:0 \
--output-dir out/labwired \
--junit out/labwired/junit.xml
cat out/labwired/rtt.log
The RTT output from our run was:
*** Booting Zephyr OS build v4.4.0-17827-g294c03cd64e6 ***
LED state: OFF
LED state: ON
The runner reported:
PASS 3/3 checks · rtt-smoke · 140697 steps · 0.08s
That duration is one local measurement of the simulation, excluding the firmware build and downloads. It is not a GitHub Actions benchmark. The runner’s event scheduler advances idle time while retaining peripheral events. The GPIO capture records toggles across the application’s sleep interval.
Device time in this model derives from a fixed 250 MHz clock. PLL reconfiguration is not tracked. The firmware configures 240 MHz, so this test does not validate the physical one-second interval.
| GPIO PB0 observation | Device cycle | Level |
|---|---|---|
| Configured as active output | 19,601 | 1 |
| First toggle, OFF | 19,623 | 0 |
| Next toggle, ON | 240,034,350 | 1 |
The capture reports zero dropped edges. These are virtual GPIO observations, not measurements from the NUCLEO board.
Add the GitHub Actions job
Leave Arm’s build workflow in place. Add Run_LabWired.yaml to .github/workflows/. The simulation job runs after Build NUCLEO-H563ZI Debug HIL completes successfully on a push.
It checks out the commit that produced the firmware and downloads the artifact from that specific run. This keeps the test files and ELF tied to the same commit. The job runs on ubuntu-latest, installs the pinned LabWired release, and executes:
- name: Assert the RTT output and capture PB0
run: |
labwired --version
labwired test --script .github/labwired/rtt-smoke.yaml \
--rtt --watch-gpio gpiob:0 \
--output-dir out/labwired \
--junit out/labwired/junit.xml
- name: Show the RTT log
if: always()
run: cat out/labwired/rtt.log
- name: Upload simulation evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: cmsis-zephyr-labwired
path: out/labwired/
The downloadable workflow includes a guard for a missing RTT log and read-only repository permissions. The test command fails the job when an assertion fails. Log display and artifact upload run even after a test failure. Commit all three files to the fork’s default branch before triggering the build; GitHub discovers workflow_run workflows there.
The download contains a complete simulation workflow, not the Zephyr toolchain setup. The original build workflow still installs those dependencies and builds the CMSIS solution. To run these assertions on pull requests too, add the simulator steps to a trusted build job that produces the same ELF.
Inspect the evidence
Each simulation run writes:
| File | What to inspect |
|---|---|
rtt.log | Console bytes read from the SEGGER RTT control block in simulated RAM. |
result.json | Verdict, each assertion, firmware hash, and the PB0 edge capture from --watch-gpio. |
junit.xml | The run and individual assertion results for CI tools. |
snapshot.json | Final CPU state and run limits for diagnosis. |
uart.log | Empty in this example because the build uses RTT instead of UART console output. |
We validated the commands locally on 30 September 2026 using the original ELF from Arm build run 36699848082. The source commit and ELF SHA-256 are:
Commit: ef8713c6c20da47b1f292630f7d662e912af91cb
ELF SHA256: f2a5d5121b70ed145b48fe542d6b320777cf6a685a9d031ae8c708c2df8fef61
We also replaced the ON expectation with a string the application never prints. The simulator returned exit code 1, with the assertion marked failed. Loading an ELF or running out of steps does not make this test green.
Download the observed result.json, RTT log, JUnit report, and verification record. This evidence is from a local simulator run. The downloadable workflow is provided for running the same checks in your fork.
Keep physical HIL for physical evidence
The useful CMSIS connection is that both jobs consume the same build. CMSIS-Toolbox and west produce it once. LabWired checks its modeled execution on a hosted runner. The existing Raspberry Pi job loads it onto the physical board.
This smoke test establishes boot, RTT output, and observed simulated GPIO changes for this particular blinky.Debug-RTT build. It does not establish full support for every Zephyr configuration or every device in Arm’s solution.
Arm’s physical workflow also captures channel 1 as a SystemView .SVDat file. This LabWired tutorial checks the RTT console on channel 0; it does not export or validate that SystemView trace. Use the original HIL job for that capture, and for electrical behavior, probe operation, cache behavior, and timing measurements on real silicon.
Run the CMSIS-Zephyr example in simulation for repeatable CI checks. Use the existing physical HIL bench when you need measurements from the board.