Skip to content

UART to replace semihosting#223

Open
JesseMelon wants to merge 4 commits into
upstream-taskfrom
semihosting-to-UART
Open

UART to replace semihosting#223
JesseMelon wants to merge 4 commits into
upstream-taskfrom
semihosting-to-UART

Conversation

@JesseMelon
Copy link
Copy Markdown

AST10x0 Test Infrastructure

Overview

Tests for the AST10x0 target are firmware images executed either under QEMU
(--config=virt_ast10x0) or on physical hardware via UART upload
(*_uart_upload_test targets). Both execution paths use the same firmware
binary ΓÇö pass/fail is signalled by writing a sentinel string to UART rather
than by any emulator-specific mechanism.

bazel test --config=virt_ast10x0 //target/ast10x0/...

How Pass/Fail Signalling Works

When a test completes, the firmware writes one of two sentinel strings directly
to UART via console_backend_write_all:

TEST_RESULT:PASS\n
TEST_RESULT:FAIL\n

The QEMU test runner (target/ast10x0/harness/qemu_runner.py) watches the raw
serial stream and kills QEMU as soon as it sees either sentinel, then exits with
code 0 or 1 accordingly. On physical hardware the same sentinel appears on the
serial console, where it can be read by automated test fixture tooling.

Semihosting Migration

This infrastructure previously used ARM semihosting
(cortex-m-semihosting::debug::exit) to signal pass/fail. Semihosting works
by triggering a BKPT 0xAB trap that QEMU intercepts and translates into a
process exit code. It has two hard constraints:

  1. QEMU only. On real hardware with no attached debugger the trap causes a
    HardFault, so the firmware never produces a result.
  2. Exit code only. There is no in-band UART output to observe ΓÇö the result
    is invisible to anything other than QEMU itself.

Replacing semihosting with a UART sentinel removes both constraints. The same
binary now runs identically under QEMU and on a physical board, which is a
prerequisite for the physical-device SSH test fixture.

Note: semihosting has no relationship to stepping through code in a debugger.
Line-by-line debugging uses GDB over JTAG/SWD, which is unaffected by this
change.


Changed Files

.bazelrc

Before:

run:virt_ast10x0  --run_under="@pigweed//pw_kernel/tooling:qemu \
  --cpu cortex-m4 --machine ast1030-evb --semihosting --image "
test:virt_ast10x0 --run_under="@pigweed//pw_kernel/tooling:qemu \
  --cpu cortex-m4 --machine ast1030-evb --semihosting --image "

After:

run:virt_ast10x0  --run_under="//target/ast10x0/harness:qemu_runner \
  --cpu cortex-m4 --machine ast1030-evb --image "
test:virt_ast10x0 --run_under="//target/ast10x0/harness:qemu_runner \
  --cpu cortex-m4 --machine ast1030-evb --image "

Switched --run_under from Pigweed's upstream QEMU runner (which terminates
only via semihosting exit) to our local runner, which terminates on UART
sentinel detection. --semihosting is removed; QEMU no longer enables the
semihosting config.


target/ast10x0/harness/qemu_runner.py (new)

Local QEMU runner that replaces @pigweed//pw_kernel/tooling:qemu_runner for
the virt_ast10x0 config. It is structurally identical to Pigweed's runner
(same QEMU invocation, same detokenizer thread for display) with two additions:

  • A sentinel watcher thread reads the raw serial stream from the same temp
    file and scans for TEST_RESULT:PASS or TEST_RESULT:FAIL. When found it
    kills QEMU and records the result.
  • A 30-second timeout kills QEMU if no sentinel is detected (prevents the
    runner from hanging indefinitely if firmware crashes before completing).

The process exit code (0 or 1) is driven by the sentinel, not by QEMU's exit
code. This is the core difference from the upstream runner.

The qemu-system-arm binary is located using qemu.qemu_system_arm.RLOCATION
from the qemu-system-arm-runfiles Bazel target (canonical label
@@pigweed++_repo_rules5+qemu). If this label breaks after a Pigweed upgrade,
run ls $(bazel info output_base)/external/ | grep qemu to find the new
canonical name.


target/ast10x0/harness/BUILD.bazel

Added a py_binary target (qemu_runner_bin) and a platform_data wrapper
(qemu_runner). The platform_data wrapper is required so that the Python
binary executes on the host platform even when the Bazel target platform is
ARM ΓÇö identical to how Pigweed exposes its own runner.

Dependencies:

  • @@pigweed++_repo_rules5+qemu//:qemu-system-arm-runfiles ΓÇö provides the
    qemu.qemu_system_arm Python importable used to locate the QEMU binary via
    Bazel runfiles.
  • @pigweed//pw_tokenizer/py:detokenize ΓÇö same detokenizer used by the
    upstream runner for display of pw_log output.
  • @rules_python//python/runfiles ΓÇö standard Bazel runfiles library.

tests/unittest_runner/target.rs

Before: imported and called cortex_m_semihosting::debug::exit.

After: calls console_backend::console_backend_write_all with the
appropriate sentinel byte string, then enters loop {}.

The console_backend crate was already linked into this binary (previously
imported as console_backend as _ for side effects). The import is now
explicit so the public console_backend_write_all function is accessible.


tests/interrupts/kernel/target.rs

Same pattern as unittest_runner. The test result comes from
test_interrupts::main::<Arch>(TEST_IRQ) returning Ok(()) or Err(_),
which maps directly to the pass/fail sentinel.


tests/interrupts/user/target.rs

Uses the shutdown(code: u32) hook rather than returning from main. The
code value (0 = pass, non-zero = fail) maps to the sentinel. Semihosting
exit() is replaced with console_backend_write_all + loop {}.


tests/ipc/user/target.rs

Same shutdown(code: u32) pattern as interrupts/user. The existing
pw_log::info!("Shutting down with code {}", code) line is retained for
visibility in the detokenized log output before the sentinel is written.


tests/threads/kernel/target.rs

Same pattern as unittest_runner ΓÇö result from threads::main(...) drives
sentinel selection directly in main().


tests/usart/target.rs

Same shutdown(code: u32) pattern as interrupts/user.


tests/*/BUILD.bazel (6 files)

Removed "@rust_crates//:cortex-m-semihosting" from the deps list of each
rust_binary (or rust_test) target. No other dependency changes were needed
because console_backend was already present in every target's dep graph.

Affected files:

  • tests/unittest_runner/BUILD.bazel
  • tests/interrupts/kernel/BUILD.bazel
  • tests/interrupts/user/BUILD.bazel
  • tests/ipc/user/BUILD.bazel
  • tests/threads/kernel/BUILD.bazel
  • tests/usart/BUILD.bazel

Test Results

The result of bazel test --config=virt_ast10x0 //target/ast10x0/... is
unchanged from before the migration:

Test Before After
interrupts_test (kernel) PASSED PASSED
interrupts_test (user) PASSED PASSED
ipc_test PASSED PASSED
threads_test PASSED PASSED
unittest_runner PASSED PASSED
usart_test PASSED PASSED
*_uart_upload_test (×5) FAILED (no board) FAILED (no board)
*_no_panics_test (×5) SKIPPED SKIPPED

The uart_upload_test failures previously exited with code 250 (the upload
script bailing out immediately). They now exit with code 1 after the 30-second
runner timeout, since our runner starts QEMU and waits for a sentinel that
never arrives. The semantic result ΓÇö no physical board, test fails ΓÇö is the
same.

Next Steps

The physical-device test fixture (ast1060-test-fixture-automation branch)
reads the same TEST_RESULT:PASS / TEST_RESULT:FAIL sentinel from the
board's serial console over SSH, completing the full hardware test loop.

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented May 11, 2026

CLA Not Signed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant