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 и
отпускает ядро дальше.
Операции, проверенные в опыте:
| Код | Операция | Аргумент | В опыте |
|---|---|---|---|
| 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 — всё прошло.
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+
программа этот регистр не читает. И бит, как сказано выше, остаётся взведённым
после отсоединения отладчика до сброса по питанию — так что надёжнее другое.
Проще всего — макрос вывода, который в выпускной сборке пуст.
Вторая цена — время. На каждый вызов ядро останавливается, отладчик читает аргументы из памяти, выполняет запрос и только потом отпускает программу. Пока он работает, стоит всё, включая прерывания: 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 что-то напечатал, нужны четыре вещи — в опыте без каждой из трёх
последних строка не появилась, хотя программа доходила до конца:
- semihosting включён в отладчике: в OpenOCD —
monitor arm semihosting enableпосле подключения, в QEMU — ключ-semihosting-config enable=on,target=native; - в проекте со своим стартовым файлом — так устроен проект CubeIDE — стартовый код rdimon не
работает, и первой строкой
mainвызываютinitialise_monitor_handles();(объявление —extern void initialise_monitor_handles(void);); syscalls.cпроекта исключён из сборки: его_writeперехватывает вывод;- строка кончается на
\nили за ней стоитfflush(stdout): иначе она так и лежит в буфереstdout.
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 Итоги
- Semihosting: номер операции в
r0, аргументы вr1,BKPT 0xAB; отладчик выполняет запрос на компьютере. - Вывод, файлы на компьютере, время, код выхода — в опыте QEMU записал и вернул файл, вышел с кодом 3.
- Без отладчика первый же вызов — HardFault (на кристалле — с битом
DEBUGEVT), а если отладчик отсоединили без сброса по питанию — остановка ядра; только для отладочных сборок. --specs=rdimon.specsдаётprintfи файлы из newlib: 31 756 байт, с nano — 10 252.
Полные исходники
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 без единой среды.