Embedded HandbookApps
All sections › V. Debugging and quality
STM32F407, STM32F405 · level intermediate · 9 min read
Open in the interactive handbook

35а. Semihosting: output and files through the debugger

The firmware needs output, but there is no free UART. SWO (section 21) needs an output pin on the debug connector. There is a third way, the oldest one: semihosting — the program on the microcontroller asks the debugger to carry out an action on the computer for it: print a string, open a file, read the time, exit with an exit code.

Everything shown here is a run in QEMU (the netduinoplus2 machine, Cortex-M4) with semihosting enabled and without it; the program _verify/semi35a/sh35.c does without any libraries. The check is _verify/semi35a_check.py.

Terms used in the documentation

Term Meaning
semihosting the debugger carries out the program's requests on the computer
BKPT 0xAB a breakpoint with a number by which the debugger recognises a request
SYS_WRITE0, SYS_OPEN, SYS_EXIT semihosting operations
rdimon (--specs=rdimon.specs) a newlib variant whose input and output go through semihosting
Debug state the core halted for the debugger

35а.1 How it works

A call is three registers and one instruction:

static int semi(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;
}

r0 holds the operation number, r1 the address of the argument block (or the argument itself), and the result comes back in r0. According to the core manual, the BKPT instruction puts the processor into the debug state. The debugger sees the halt, sees the number 0xAB in the instruction, reads r0/r1 and the memory, carries out the request on the computer, writes the answer into r0 and lets the core run on.

The path of one semihosting request: the program, the debugger and the computer; the core stands still while the request is being carried out

The operations tested in the experiment:

Code Operation Argument In the experiment
0x04 SYS_WRITE0 — print a string address of the string "semihosting: hello from the target"
0x03 SYS_WRITEC — one character address of the character "!"
0x01 SYS_OPEN — open a file name, mode, name length the file sh35_out.txt on the computer
0x05 SYS_WRITE — write handle, buffer, length 0 bytes not written
0x06 SYS_READ — read handle, buffer, length the same string read back
0x10 SYS_CLOCK — time since start — answer received
0x18 SYS_EXIT — exit reason directly in r1 QEMU exited with code 0
0x20 SYS_EXIT_EXTENDED reason and code QEMU exited with code 3

SYS_WRITE and SYS_READ return how many bytes could not be written or read: 0 means everything went through.

The SYS_OPEN call as the debugger sees it: r0 = 0x01, r1 points to three words on the stack — the address of the file name, mode 4 and length 12; the answer in r0 is the handle; modes 0, 1, 4 and 5


35а.2 What you get: files and an exit code

The firmware wrote a string into a file on the computer, closed it, opened it again and read it back — "written by the target via SYS_WRITE". The file sits in the debugger's working folder. This is how memory dumps, logs and measurement results are dumped without any interface on the board.

The exit code turns the firmware into an ordinary program for scripts: a test running under the debugger (or in an emulator) prints what it checked and exits with a code — and the build system (section 110) looks at the code just as it does for any program on the computer. In the experiment SYS_EXIT_EXTENDED with code 3 — QEMU exited with code 3.


35а.3 What it costs: without a debugger it is a HardFault

The same firmware without semihosting in the emulator (on a board — with no debugger connected):

start
HardFault: HFSR=0x40000000 DFSR=0x00000000

The very first BKPT instruction — and a HardFault: there is no one to halt the core. The file is not created. HFSR = 0x40000000 (the FORCED bit) is how the emulator reports the fault. On the chip such a HardFault arrives with bit 31 DEBUGEVT (HFSR = 0x80000000) — that is how you recognise it. And if the debugger was disconnected without removing power, the core simply halts on the first BKPT: debugging (C_DEBUGEN in DHCSR) stays enabled until a power reset, and the board looks hung. Hence the main rule: semihosting belongs only in a debug build; in firmware that goes onto a board without a debugger, not a single call may remain. You can check at run time whether a debugger is connected by the C_DEBUGEN bit (bit 0) in CoreDebug->DHCSR, but only on Cortex-M3, M4 and M7: on M0/M0+ the program cannot read this register. And the bit, as said above, stays set after the debugger is disconnected until a power reset — so something else is more reliable. The simplest is an output macro that is empty in the release build.

One firmware in two runs: with a debugger every operation is carried out on the computer, without a debugger the very first BKPT 0xAB is a HardFault

The second cost is time. On every call the core halts, the debugger reads the arguments from memory, carries out the request and only then lets the program go. While it works, everything stands still, interrupts included: SWO (section 21) and UART do not behave like this — the data goes out and the program runs on. For output from handlers and control loops semihosting is unsuitable.


35а.4 printf through semihosting: the rdimon library

Newlib (the standard C library from the arm-none-eabi-gcc toolchain) can route printf, fopen and exit into semihosting by itself — the rdimon variant:

arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Os --specs=rdimon.specs ...
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Os --specs=nano.specs --specs=rdimon.specs ...

Program size for the single line printf("x=%d y=%s\n", 42, "ok"):

Library Code
full newlib + rdimon 31756 bytes
newlib-nano + rdimon 10252 bytes

The difference is in printf itself and in buffering: the nano variant is cut down. For a debug build either will do; for firmware neither: rdimon input and output go through semihosting.

For printf through rdimon to print anything, four things are needed — in the experiment, without each of the last three the line did not appear, even though the program ran to the end:


35а.5 Pitfalls

The board hangs or falls into HardFault as soon as the debugger is disconnected. A semihosting call was left in the build — BKPT without a debugger.

The program "freezes" on the first printf under the debugger. Semihosting is not enabled in the debugger: the core is halted on BKPT and waiting. In OpenOCD — monitor arm semihosting enable.

printf under the debugger is silent, but the program runs. initialise_monitor_handles() was not called, _write from syscalls.c intercepted the output, or a string without \n is waiting in the buffer (35а.4).

Interrupts are late while output is running. Every call halts the whole core.

The file did not appear. The path is relative to the debugger's working folder, not the project.

SYS_EXIT returned the "wrong" code. In 32-bit mode SYS_EXIT gets the reason in r1, not the code; the exit code is passed by SYS_EXIT_EXTENDED.


35а.6 Summary

Full source code

sh35.c — semihosting without libraries: calls through BKPT 0xAB.

/* Раздел 35а: semihosting без библиотек — прогон в QEMU netduinoplus2.
 * Вызов: номер операции в r0, адрес блока аргументов в r1, команда BKPT 0xAB; результат — в r0.
 * Отладчик (здесь — QEMU с -semihosting) видит остановку на BKPT, выполняет запрос на компьютере
 * и продолжает программу. Без отладчика BKPT поднимает HardFault — обработчик пишет об этом в USART1. */
#include <stdint.h>

#define SYS_OPEN 0x01
#define SYS_CLOSE 0x02
#define SYS_WRITEC 0x03
#define SYS_WRITE0 0x04
#define SYS_WRITE 0x05
#define SYS_READ 0x06
#define SYS_CLOCK 0x10
#define SYS_EXIT 0x18
#define SYS_EXIT_EXTENDED 0x20

static int semi(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;
}

/* USART1 — чтобы HardFault было чем сообщить без отладчика */
#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR (*(volatile uint32_t *)0x40011000)
#define USART1_DR (*(volatile uint32_t *)0x40011004)
#define USART1_CR1 (*(volatile uint32_t *)0x4001100C)
#define SCB_HFSR (*(volatile uint32_t *)0xE000ED2C)
#define SCB_DFSR (*(volatile uint32_t *)0xE000ED30)

static void uputs(const char *s)
{
    for (; *s; s++) {
        while (!(USART1_SR & (1u << 7))) {
        }
        USART1_DR = (uint32_t)*s;
    }
}

static void uhex(uint32_t v)
{
    static const char h[] = "0123456789abcdef";
    char b[11] = "0x";
    for (int i = 0; i < 8; i++)
        b[2 + i] = h[(v >> (28 - 4 * i)) & 0xF];
    b[10] = 0;
    uputs(b);
}

void HardFault_Handler(void)
{
    uputs("HardFault: HFSR=");
    uhex(SCB_HFSR);
    uputs(" DFSR=");
    uhex(SCB_DFSR);
    uputs("\n");
    for (;;) {
    }
}

static unsigned slen(const char *s)
{
    unsigned n = 0;
    while (s[n])
        n++;
    return n;
}

int main(void)
{
    RCC_APB2ENR |= 1u << 4;
    USART1_CR1 = (1u << 13) | (1u << 3);
    uputs("start\n");

    semi(SYS_WRITE0, (void *)"semihosting: hello from the target\n");
    char c = '!';
    semi(SYS_WRITEC, &c);
    c = '\n';
    semi(SYS_WRITEC, &c);

    /* файл на компьютере: открыть на запись (режим 4 = "w"), записать, закрыть; открыть на чтение и прочитать */
    static const char name[] = "sh35_out.txt";
    static const char text[] = "written by the target via SYS_WRITE\n";
    uint32_t a_open[3] = {(uint32_t)name, 4, slen(name)};
    int fd = semi(SYS_OPEN, a_open);
    uint32_t a_wr[3] = {(uint32_t)fd, (uint32_t)text, slen(text)};
    int notw = semi(SYS_WRITE, a_wr);
    uint32_t a_cl[1] = {(uint32_t)fd};
    semi(SYS_CLOSE, a_cl);
    uint32_t a_open_r[3] = {(uint32_t)name, 0, slen(name)};
    int fr = semi(SYS_OPEN, a_open_r);
    static char back[64];
    uint32_t a_rd[3] = {(uint32_t)fr, (uint32_t)back, sizeof back - 1};
    int notr = semi(SYS_READ, a_rd);
    semi(SYS_CLOSE, (uint32_t[1]){(uint32_t)fr});
    back[sizeof back - 1 - notr] = 0;

    char line[96];
    const char *p = "fd=";
    unsigned n = 0;
    while (*p)
        line[n++] = *p++;
    line[n++] = (char)('0' + (fd >= 0));
    p = " not_written=";
    while (*p)
        line[n++] = *p++;
    line[n++] = (char)('0' + notw);
    p = " read_back=";
    while (*p)
        line[n++] = *p++;
    for (const char *q = back; *q && *q != '\n'; q++)
        line[n++] = *q;
    line[n++] = '\n';
    line[n] = 0;
    semi(SYS_WRITE0, line);

    int cs = semi(SYS_CLOCK, 0);                    /* время с запуска программы; −1 — ошибка */
    semi(SYS_WRITE0, cs >= 0 ? (void *)"clock ok\n" : (void *)"clock fail\n");

#ifdef EXIT_EXTENDED
    uint32_t a_exit[2] = {0x20026, 3};              /* SYS_EXIT_EXTENDED: причина и код выхода 3 */
    semi(SYS_EXIT_EXTENDED, a_exit);
#else
    semi(SYS_EXIT, (void *)0x20026);                /* 32-битный SYS_EXIT: причина прямо в r1, кода нет */
#endif
    uputs("not reached\n");
    for (;;) {
    }
}

Next: section 35б "Flashing and debugging from the command line: OpenOCD, GDB, CubeProgrammer CLI" — OpenOCD, GDB and STM32_Programmer_CLI without any IDE.

Open in the interactive handbook