Embedded HandbookПриложения
Все разделы › V. Отладка и качество
STM32F407, STM32F405 · уровень средний · 9 мин чтения
Открыть в интерактивном справочнике

35а. Semihosting: вывод и файлы через отладчик

Прошивке нужен вывод, а свободного UART нет. SWO (раздел 21) требует вывода на разъёме отладчика. Есть третий путь, самый старый: semihosting — программа на микроконтроллере просит отладчик выполнить за неё действие на компьютере: напечатать строку, открыть файл, прочитать время, завершиться с кодом выхода.

Всё показанное — прогон в QEMU (машина netduinoplus2, Cortex-M4) с включённым semihosting и без него; программа _verify/semi35a/sh35.c обходится без библиотек. Сверяет _verify/semi35a_check.py.

Как это называется в документации

Оригинал Перевод
semihosting выполнение запросов программы на компьютере через отладчик
BKPT 0xAB точка останова с номером, по которому отладчик узнаёт запрос
SYS_WRITE0, SYS_OPEN, SYS_EXIT операции semihosting
rdimon (--specs=rdimon.specs) вариант newlib, чей ввод-вывод идёт через semihosting
Debug state состояние остановки ядра для отладчика

35а.1 Как это работает

Вызов — три регистра и одна команда:

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 — номер операции, в r1 — адрес блока аргументов (или сам аргумент), результат — снова в r0. По руководству ядра команда BKPT переводит процессор в состояние отладки. Отладчик видит остановку, видит число 0xAB в команде, читает r0/r1 и память, выполняет запрос на компьютере, пишет ответ в r0 и отпускает ядро дальше.

Путь одного запроса semihosting: программа, отладчик и компьютер; ядро стоит, пока запрос выполняется

Операции, проверенные в опыте:

Код Операция Аргумент В опыте
0x04 SYS_WRITE0 — напечатать строку адрес строки «semihosting: hello from the target»
0x03 SYS_WRITEC — один символ адрес символа «!»
0x01 SYS_OPEN — открыть файл имя, режим, длина имени файл sh35_out.txt на компьютере
0x05 SYS_WRITE — записать дескриптор, буфер, длина не записано 0 байт
0x06 SYS_READ — прочитать дескриптор, буфер, длина прочитана та же строка
0x10 SYS_CLOCK — время с запуска — ответ получен
0x18 SYS_EXIT — завершить причина прямо в r1 QEMU завершился с кодом 0
0x20 SYS_EXIT_EXTENDED причина и код QEMU завершился с кодом 3

SYS_WRITE и SYS_READ возвращают сколько не удалось записать или прочитать: 0 — всё прошло.

Вызов SYS_OPEN глазами отладчика: r0 = 0x01, r1 указывает на три слова в стеке — адрес имени файла, режим 4 и длину 12; ответ в r0 — дескриптор; режимы 0, 1, 4 и 5


35а.2 Что даёт: файлы и код выхода

Прошивка записала строку в файл на компьютере, закрыла, открыла заново и прочитала — «written by the target via SYS_WRITE». Файл лежит в рабочей папке отладчика. Так сбрасывают дампы памяти, журналы, результаты измерений без всякого интерфейса на плате.

Код выхода превращает прошивку в обычную программу для сценариев: тест под отладчиком (или в эмуляторе) печатает, что проверил, и завершается с кодом — а система сборки (раздел 110) смотрит на код, как у любой программы на компьютере. В опыте SYS_EXIT_EXTENDED с кодом 3 — QEMU вышел с кодом 3.


35а.3 Что стоит: без отладчика — HardFault

Та же прошивка без semihosting в эмуляторе (на плате — без подключённого отладчика):

start
HardFault: HFSR=0x40000000 DFSR=0x00000000

Первая же команда BKPT — и HardFault: остановить ядро некому. Файл не создан. HFSR = 0x40000000 (бит FORCED) — так отказ оформляет эмулятор. На кристалле такой HardFault придёт с битом 31 DEBUGEVT (HFSR = 0x80000000) — по нему его и узнают. А если отладчик отсоединили, не снимая питания, ядро на первом BKPT просто остановится: отладка (C_DEBUGEN в DHCSR) остаётся включённой до сброса по питанию, и плата выглядит зависшей. Отсюда главное правило: semihosting — только в отладочной сборке; в прошивке, которая уходит на плату без отладчика, ни одного вызова остаться не должно. Проверить во время работы, подключён ли отладчик, можно по биту C_DEBUGEN (бит 0) в CoreDebug->DHCSR, но только на Cortex-M3, M4 и M7: у M0/M0+ программа этот регистр не читает. И бит, как сказано выше, остаётся взведённым после отсоединения отладчика до сброса по питанию — так что надёжнее другое. Проще всего — макрос вывода, который в выпускной сборке пуст.

Одна прошивка в двух запусках: с отладчиком каждая операция выполнена на компьютере, без отладчика первый же BKPT 0xAB — HardFault

Вторая цена — время. На каждый вызов ядро останавливается, отладчик читает аргументы из памяти, выполняет запрос и только потом отпускает программу. Пока он работает, стоит всё, включая прерывания: SWO (раздел 21) и UART так не делают — данные уходят, а программа идёт дальше. Для вывода из обработчиков и циклов управления semihosting непригоден.


35а.4 printf через semihosting: библиотека rdimon

Newlib (стандартная библиотека C из набора arm-none-eabi-gcc) умеет направлять printf, fopen, exit в semihosting сама — вариант rdimon:

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 ...

Размер программы из одной строки printf("x=%d y=%s\n", 42, "ok"):

Библиотека Код
полная newlib + rdimon 31 756 байт
newlib-nano + rdimon 10 252 байта

Разница — в самом printf и буферизации: nano-вариант урезан. Для отладочной сборки годится любой; для прошивки — ни один: ввод-вывод rdimon идёт через semihosting.

Чтобы printf через rdimon что-то напечатал, нужны четыре вещи — в опыте без каждой из трёх последних строка не появилась, хотя программа доходила до конца:


35а.5 Грабли

Плата виснет или падает в HardFault, как только отключили отладчик. В сборке остался вызов semihosting — BKPT без отладчика.

Программа «замирает» на первом printf под отладчиком. Semihosting в отладчике не включён: ядро остановлено на BKPT и ждёт. В OpenOCD — monitor arm semihosting enable.

printf под отладчиком молчит, а программа идёт. Не вызван initialise_monitor_handles(), _write из syscalls.c перехватил вывод или строка без \n ждёт в буфере (35а.4).

Прерывания опаздывают, пока идёт вывод. Каждый вызов останавливает ядро целиком.

Файл не появился. Путь — относительно рабочей папки отладчика, а не проекта.

SYS_EXIT вернул «не тот» код. В 32-битном режиме SYS_EXIT получает в r1 причину, а не код; код выхода передаёт SYS_EXIT_EXTENDED.


35а.6 Итоги

Полные исходники

sh35.c — semihosting без библиотек: вызовы через 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 (;;) {
    }
}

Дальше: раздел 35б «Прошивка и отладка из командной строки» — OpenOCD, GDB и STM32_Programmer_CLI без единой среды.

Открыть в интерактивном справочнике