End a run with semihosting

Call SYS_EXIT_EXTENDED so OnMCU stops as soon as the firmware finishes.

A bare-metal test cannot return a result to an operating system. Instead, it can use an ARM semihosting trap to tell OnMCU that it passed or failed.

Use SYS_EXIT_EXTENDED, operation 0x20, with a two-word parameter block:

word 0: 0x00020026  ADP_Stopped_ApplicationExit
word 1: exit code   0 for success, non-zero for failure

On 32-bit Arm targets, put the operation in r0 and a pointer to the block in r1, then execute bkpt 0xAB. OnMCU reads the subcode and ends the job.

Why use the extended operation

The older SYS_EXIT operation cannot carry a separate status code on 32-bit targets. SYS_EXIT_EXTENDED carries both the stop reason and the application's exit code.

Result propagation

A zero device code completes the OnMCU job, and onmcu run returns 0. A non-zero device code fails the job, and the CLI returns 10.

C

This code comes from the shared library in onmcu/c-examples:

semihosting.h
#ifndef SEMIHOSTING_H
#define SEMIHOSTING_H

void sys_exit(int code) __attribute__((noreturn));

#endif
semihosting.c
#include "semihosting.h"
#include <stdint.h>

#define SYS_EXIT_EXTENDED 0x20
#define ADP_STOPPED_APPLICATION_EXIT 0x20026u

static int semihost_call(int op, void *arg)
{
    register int r0 __asm("r0") = op;
    register void *r1 __asm("r1") = arg;
    __asm volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory");
    return r0;
}

void sys_exit(int code)
{
    uint32_t block[2] = {
        ADP_STOPPED_APPLICATION_EXIT,
        (uint32_t)code,
    };
    semihost_call(SYS_EXIT_EXTENDED, block);

    // The host normally tears down the session above.
    for (;;) {
    }
}

Call it after the final result is known:

int main(void)
{
    int result = run_tests();

    if (result == 0) {
        rtt_write_str("all tests passed\n");
        sys_exit(0);
    }

    rtt_write_str("tests failed\n");
    sys_exit(1);
}

Compile semihosting.c into the linked firmware and include its header. It uses no host-side standard library.

Rust

The semihosting crate provides the same operation:

Cargo.toml
[dependencies]
semihosting = "0.1"
fn finish(success: bool) -> ! {
    if success {
        defmt::info!("all tests passed");
        defmt::flush();
        semihosting::process::exit(0);
    } else {
        defmt::error!("tests failed");
        defmt::flush();
        semihosting::process::exit(1);
    }
}

defmt-test already uses semihosting to halt after a test suite. Follow Rust and defmt-test to use OnMCU as its Cargo runner.

Prevent the fallback from returning

The semihosting call is expected to end the session. Keep a non-returning loop after the trap in hand-written C or assembly in case the firmware is run without a semihosting-capable host. Do not let execution fall into unrelated memory.

Preserve the last log line

The debugger can stop immediately after the exit trap. Flush RTT or defmt before exiting. For a hardware UART, wait until its transmit path has drained; a logger flush that covers only a memory buffer may not wait for the serial shift register or USB bridge.

Do not rely on the timeout

If the firmware finishes and enters an infinite loop, OnMCU still sees a running job. It stops only at the configured timeout, and the CLI returns 12. A semihosting exit ends successful tests at once and leaves the timeout for real hangs.

On this page