4б. STM32CubeIDE: проект, сборка, отладка и разбор сбоев
STM32CubeIDE — бесплатная среда ST: Eclipse с модулем C/C++ (CDT),
компилятором GCC для Arm и отладчиком GDB; конфигуратор CubeMX в версиях 1.x
встроен в неё, а с 2.0 — отдельная программа. Кнопки у неё
удобные, но за каждой стоит вызов обычной программы: компилятора,
компоновщика, отладчика. Раздел разбирает, что это за вызовы. Тогда проект
понятен без снимков экрана, собирается и отлаживается и без окна среды, а
сбой вида HardFault разбирается по регистрам, а не угадывается.
Всё показанное — настоящие файлы проекта CubeIDE из пакета примеров ST для
Discovery F407 (GPIO_EXTI): стартовый файл, скрипт компоновщика,
syscalls.c, sysmem.c, .cproject и .project — без единой правки. К ним
добавлена маленькая программа main.c. Сборка идёт arm-none-eabi-gcc 12.2
с ключами из .cproject, запуск — в эмуляторе QEMU (плата netduinoplus2
с STM32F405 — тем же ядром и той же памятью), отладка — GDB 13.1, теми же
командами, которые CubeIDE отдаёт отладчику за кнопками
(_verify/cubeide4b/).
Как это называется в документации
Оригинал Перевод workspace, project рабочее пространство (папка с настройками среды), проект build configuration: Debug, Release конфигурация сборки: отладочная, выпускная managed build сборка, которой управляет среда: Makefile пишет она сама linker script ( .ld)скрипт компоновщика startup file стартовый файл: таблица векторов и Reset_Handlersystem calls ( syscalls.c,sysmem.c)заглушки системных вызовов для библиотеки C newlib, newlib-nano библиотека C для микроконтроллеров и её урезанный вариант debug configuration, GDB server настройка запуска отладки, сервер отладчика (ST-LINK GDB server, OpenOCD) breakpoint, watchpoint точка останова, точка наблюдения за переменной Live Expressions переменные, обновляемые на ходу без остановки SFRs (special function registers) регистры периферии по файлу SVD Fault Analyzer разбор причины HardFaultпо регистрамSWV (Serial Wire Viewer), ITM трасса по выводу SWO, канал вывода ядра Build Analyzer, Static Stack Analyzer карта памяти после сборки, стек по функциям
4б.1 Что внутри среды
Среда складывается из четырёх частей, и у каждой есть обычный заменитель из командной строки:
| В CubeIDE | Что делает | Без среды |
|---|---|---|
проект CDT, .project и .cproject |
список файлов и ключи сборки | Makefile или CMake |
| GNU Tools for STM32 | компилятор, ассемблер, компоновщик | arm-none-eabi-gcc |
CubeMX (файл .ioc; с версии 2.0 не встроен) |
выводы, такты, код инициализации | отдельный STM32CubeMX |
| отладка: GDB и сервер ST-LINK или OpenOCD | загрузка, остановки, регистры | gdb + openocd или st-util |
Рабочее пространство (workspace) — папка с настройками самой среды, в git
её не кладут. Проект — папка с .project, .cproject, исходниками и
скриптом компоновщика; её кладут целиком, кроме папок Debug/ и Release/
с результатами сборки.
4б.2 Файлы проекта
Шесть файлов делают папку проектом CubeIDE:
| Файл | Зачем |
|---|---|
.project |
имя проекта и связанные файлы — ссылки на исходники вне папки |
.cproject |
конфигурации Debug и Release: камень, FPU, ключи, пути, скрипт компоновщика |
STM32F407VGTX_FLASH.ld |
скрипт компоновщика: память и секции |
Startup/startup_stm32f407vgtx.s |
таблица векторов и Reset_Handler на ассемблере |
syscalls.c |
_write, _read и другие заглушки для printf и scanf |
sysmem.c |
_sbrk — откуда malloc берёт память |
В примере ST исходники HAL и BSP не скопированы в проект, а подключены
ссылками: в .project их 18. Выглядит аккуратно, но проект перестаёт
собираться, как только его переносят в другую папку. Для своих проектов
надёжнее класть драйверы внутрь.
.cproject — XML, и всё, что задаётся галочками в свойствах проекта, лежит в
нём текстом. Выписка проверки:
2 option.floatabi.value.hard
2 option.fpu.value.fpv4-sp-d16
2 option.target_mcu" value="STM32F407VGTx
3 debuglevel.value.g3
2 optimization.level.value.os
2 USE_STM32F4_DISCO | STM32F407xx | USE_HAL_DRIVER
Двойки — потому что конфигураций две: FPU fpv4-sp-d16 с аппаратной
передачей чисел с плавающей точкой, камень STM32F407VGTx и определения
STM32F407xx, USE_HAL_DRIVER — в обеих. Оптимизация -Os записана
только у Release, у Debug значение по умолчанию — -O0.
4б.3 Debug и Release: ключи в командной строке
Всё, что CubeIDE передаёт компилятору для этого проекта, умещается в несколько строк:
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
-std=gnu11 -g3 -O0 -DDEBUG -DSTM32F407xx -ffunction-sections -fdata-sections -Wall -fstack-usage -c main.c
arm-none-eabi-gcc ... startup.o main.o syscalls.o sysmem.o -T STM32F407VGTX_FLASH.ld \
--specs=nosys.specs --specs=nano.specs -Wl,--gc-sections -static -Wl,-Map=app.map -Wl,--print-memory-usage
Стартовый файл собирается тем же gcc с -x assembler-with-cpp, чтобы в
нём работал препроцессор. Ключи --specs=nano.specs (урезанная newlib) и
--specs=nosys.specs (заглушки системных вызовов) в .cproject не видны:
это умолчания среды для «Reduced C». Отчёт компоновщика и размеры:
== build Debug
Memory region Used Size Region Size %age Used
CCMRAM: 0 GB 64 KB 0.00%
RAM: 1688 B 128 KB 1.29%
ROM: 8816 B 1 MB 0.84%
size: 8708 100 1588
== build Release
ROM: 8500 B 1 MB 0.81%
size: 8392 100 1588
Три числа size — код с константами, .data и .bss. Release с -Os
меньше Debug на 316 байт; на такой крошечной программе почти всё место
занимает newlib, а не наш код. Флаг -DDEBUG в Release не передаётся,
поэтому код под #ifdef DEBUG в выпуск не попадает.
4б.4 Скрипт компоновщика CubeIDE
Скрипт среды длиннее самодельного из раздела 4в, но основа та же. Память —
три области, флеш-память здесь названа ROM:
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
ROM (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
Начальные значения .data лежат во флеш-памяти, а работают в ОЗУ:
_sidata = LOADADDR(.data);
} >RAM AT> ROM
Карта после сборки подтверждает каждую строку:
.isr_vector 0x08000000 0x188
.text 0x08000190 0x1f14
.rodata 0x080020a4 0x160
.data 0x20000000 0x64 load address 0x0800220c
.bss 0x20000064 0x30
0800220c A _sidata
20000000 D _sdata
20000064 D _edata
20000094 B _ebss
20000098 B _end
20020000 R _estack
.data работает с 0x20000000, а хранится по 0x0800220c — сразу за
.rodata. Таблица векторов — 0x188 = 392 байта: 98 слов .word, по одному
на вектор. _estack = 0x20000000 + 128 КБ = 0x20020000.
Две вещи в скрипте CubeIDE не очевидны. Секция ._user_heap_stack ничего
не резервирует в образе — это проверка: если после .bss не помещаются
_Min_Heap_Size + _Min_Stack_Size, компоновщик сообщит о переполнении.
Сами стек и куча не ограничены: стек растёт вниз от _estack, куча — вверх
от _end. А CCMRAM (64 КБ у F407, быстрая память без DMA) описана, но
стартовый код её не заполняет: не копирует начальные значения и не
обнуляет. Мусор там будет даже у переменной без инициализатора, которую C
обещает обнулить, — годятся только буферы, которые программа заполнит
сама, а переменную с начальным значением туда класть
нельзя без своего цикла копирования.
4б.5 printf и malloc: syscalls.c и sysmem.c
newlib не знает, куда выводить текст и где свободная память. Ей нужны
функции с именами _write и _sbrk, и CubeIDE кладёт их в проект.
_write из syscalls.c вызывает для каждого символа слабую функцию
__io_putchar — её программа и определяет:
int __io_putchar(int ch) /* syscalls.c зовёт её из _write для каждого символа */
{
while (!(USART1_SR & (1u << 7))) {
}
USART1_DR = (uint32_t)ch;
return ch;
}
USART1_SR и USART1_DR объявлены в main.c через volatile (раздел
3.4). Без него сборка Release (-Os) читает SR один раз и, если TXE
в этот миг 0, уходит в вечный переход на себя; в Debug (-O0) ошибки
не видно.
_sbrk из sysmem.c выдаёт память от символа _end до
_estack − _Min_Stack_Size. Функция короткая — вот она целиком, вместе
с переменной файла, которая помнит, докуда память уже выдана:
static uint8_t *__sbrk_heap_end = NULL;
void *_sbrk(ptrdiff_t incr)
{
extern uint8_t _end; /* Symbol defined in the linker script */
extern uint8_t _estack; /* Symbol defined in the linker script */
extern uint32_t _Min_Stack_Size; /* Symbol defined in the linker script */
const uint32_t stack_limit = (uint32_t)&_estack - (uint32_t)&_Min_Stack_Size;
const uint8_t *max_heap = (uint8_t *)stack_limit;
uint8_t *prev_heap_end;
/* Initialize heap end at first call */
if (NULL == __sbrk_heap_end)
{
__sbrk_heap_end = &_end;
}
/* Protect heap from growing into the reserved MSP stack */
if (__sbrk_heap_end + incr > max_heap)
{
errno = ENOMEM;
return (void *)-1;
}
prev_heap_end = __sbrk_heap_end;
__sbrk_heap_end += incr;
return (void *)prev_heap_end;
}
Вывод программы в эмуляторе:
printf через _write: square(7) = 49
malloc(100) = 0x20000250, _end = 0x20000098
malloc(200000) = 0
calls = 1, float:
сейчас будет HardFault
Куча началась от _end, а первый блок оказался дальше: часть памяти уже
забрала сама библиотека под буфер вывода. Запрос на 200 000 байт больше
всего ОЗУ, _sbrk вернул отказ, malloc — ноль.
И главная ловушка: printf("%.2f") напечатал пустоту. В newlib-nano вывод
чисел с плавающей точкой по умолчанию выключен ради размера. Включается он
ключом компоновщика -u _printf_float — в CubeIDE это галочка «Use float
with printf»:
app.elf: text 8708
float.elf: text 22004
calls = 1, float: 4.50
Одна галочка — плюс 13 296 байт кода. На F103 с 64 КБ это пятая часть флеш-памяти. Часто проще печатать число целыми частями.
4б.6 Сборка без окна
Проект CubeIDE собирается и без окна среды, например на сервере сборки: в CubeIDE есть headless-режим Eclipse:
stm32cubeide --launcher.suppressErrors -nosplash \
-application org.eclipse.cdt.managedbuilder.core.headlessbuild \
-data /tmp/ws -import ./GPIO_EXTI -build GPIO_EXTI/Debug
На Windows вместо stm32cubeide вызывают консольный stm32cubeidec.exe:
оконный сразу отпускает консоль, а этот ждёт конца сборки и возвращает код
ошибки.
-data — временное рабочее пространство, -import — папка проекта,
-build — проект и конфигурация. Среда сама пишет makefile в папку
Debug/, и после одной сборки в окне проект можно собирать просто make
в этой папке. Но такой Makefile содержит абсолютные пути вашей машины;
для сервера надёжнее один раз переписать ключи из 4б.3 в свой Makefile
или CMake — как в разделе 4в.
4б.7 Отладка: что делает кнопка «Debug»
Кнопка «Debug» делает три вещи: собирает проект, запускает сервер отладчика
(ST-LINK GDB server или OpenOCD) и подключает к нему arm-none-eabi-gdb.
Точки останова, шаги, переменные в окнах среды — это команды GDB. Их
можно дать и руками, к плате через сервер или к эмулятору. Сеанс ниже —
QEMU с остановкой на первой команде:
target remote :1234
Reset_Handler () at Startup/startup_stm32f407vgtx.s:61
pc 0x8001da4 0x8001da4 <Reset_Handler>
sp 0x20020000 0x20020000
Ядро само загрузило SP из первого слова таблицы векторов (_estack) и PC
из второго. Точка останова на функции и точка наблюдения за переменной:
break square
watch calls
Breakpoint 1, square (x=7) at main.c:32
#0 square (x=7) at main.c:32
#1 0x08001eaa in main () at main.c:43
Hardware watchpoint 2: calls
Old value = 0
New value = 1
bt показал, кто вызвал square. Точка наблюдения остановила программу
на той строке, что изменила calls, и показала старое и новое значения.
Такие точки — аппаратные: у Cortex-M4 блок DWT держит четыре компаратора,
поэтому следить сразу больше чем за четырьмя адресами нельзя. В CubeIDE
то же ставится в окне Expressions. Точки останова в коде во Flash тоже
аппаратные: блок FPB у Cortex-M3/M4 держит шесть (раздел 35д.6), седьмую
GDB поставить не сможет.
Окна среды соответствуют командам GDB:
| Окно CubeIDE | Команда GDB |
|---|---|
| Variables, Expressions | print x, display, watch |
| Registers | info registers |
| Memory | x/16xw 0x20000000 |
| SFRs | x по адресу регистра из файла SVD |
| Debug (стек вызовов) | bt, frame |
4б.8 HardFault: разбор по регистрам
В CubeIDE есть окно Fault Analyzer, и всё, что оно показывает, читается из регистров системного блока. Программа нарочно переходит по чётному адресу:
void (*jump)(void) = (void (*)(void))0x08000200; /* чётный адрес: у Cortex-M нет режима ARM */
jump();
Обработчик HardFault_Handler в стартовом файле — слабый псевдоним
бесконечного цикла Default_Handler, и отладчик останавливается в нём:
Breakpoint 3, WWDG_IRQHandler () at Startup/startup_stm32f407vgtx.s:113
113 b Infinite_Loop
CFSR = 0x00020000
HFSR = 0x40000000
lr 0xfffffff9 -7
кадр: R0 0x0000000a LR 0x08001ef9 PC 0x08000200 xPSR 0x00000000
main + 133 in section .text
Разбор по шагам:
- Где стоим. GDB назвал точку
WWDG_IRQHandler: у всех слабых обработчиков один адрес, и отладчик берёт первое имя. Сбой ли это, видно по регистрам, а не по имени. - CFSR (
0xE000ED28) =0x00020000— бит 17, INVSTATE: попытка перейти в режим ARM. У Cortex-M есть только Thumb, младший бит адреса перехода обязан быть единицей. - HFSR (
0xE000ED2C) =0x40000000— бит FORCED: UsageFault выключен по умолчанию и поднялся до HardFault. - LR =
0xFFFFFFF9— это не адрес, а код возврата из исключения: кадр лежит на основном стеке MSP. - Кадр исключения — восемь слов на стеке: R0–R3, R12, LR, PC, xPSR.
Сохранённый PC —
0x08000200, тот самый чётный адрес. Сохранённый LR0x08001ef9—main + 133, строка сразу за вызовом.
Команды, которыми это читается:
printf "CFSR = 0x%08x\n", *(unsigned int *)0xE000ED28
printf "HFSR = 0x%08x\n", *(unsigned int *)0xE000ED2C
info symbol ((unsigned int *)$sp)[5]
При реальном сбое по памяти рядом смотрят ещё MMFAR (0xE000ED34) и
BFAR (0xE000ED38): там адрес, к которому обратилась программа, если в
CFSR стоят биты MMARVALID или BFARVALID.
4б.9 Память и стек: Build Analyzer и .su
Окно Build Analyzer — это разобранный map-файл: области памяти с процентом
заполнения (та же строка, что печатает --print-memory-usage) и размер
каждого символа. Static Stack Analyzer берёт кадры из файлов .su, которые
пишет ключ -fstack-usage:
main.c:22:5:__io_putchar 16 static
main.c:30:12:square 16 static
main.c:36:5:main 24 static
sysmem.c:53:7:_sbrk 32 static
syscalls.c:80:27:_write 32 static
Это кадр одной функции без вызываемых: чтобы оценить стек, складывают
кадры по самой глубокой цепочке вызовов. Внутрь newlib .su не
заглядывает, а printf в ней съедает сотни байт стека. Поэтому
_Min_Stack_Size = 0x400 — нижняя оценка, а не гарантия.
4б.10 SWV, ITM и живые переменные — только на плате
Часть отладки CubeIDE требует настоящего ядра и отладчика, в эмуляторе её не показать:
- Live Expressions — переменные обновляются, пока программа работает: отладчик читает память через SWD на ходу, не останавливая ядро.
- SWV и ITM — ядро шлёт данные по отдельному выводу SWO:
printfбез UART (порт ITM 0), метки времени прерываний, значения переменных по таймеру. Нужны вывод SWO на разъёме, ST-LINK с SWO и частота ядра в настройках отладки; у Cortex-M0 и M0+ SWO нет вовсе. - Сохранение при сбое — CubeIDE умеет не сбрасывать ядро при подключении, чтобы разобрать зависшую программу, как в 4б.8.
4б.11 Грабли
| Симптом | Причина |
|---|---|
printf("%f") печатает пусто |
в newlib-nano вывод float выключен; -u _printf_float, +13 КБ кода |
printf не печатает ничего |
нет __io_putchar или буфер stdout не сброшен — нужен \n или setvbuf |
| проект не собирается после переноса | исходники подключены ссылками из .project, а не лежат в папке |
malloc возвращает 0 при свободном ОЗУ |
_sbrk не отдаёт память ниже _estack − _Min_Stack_Size |
переменная в .ccmram содержит мусор |
стартовый код CubeIDE секцию .ccmram не копирует и не обнуляет; класть туда только то, что программа заполнит сама, или дописать в Reset_Handler копирование от _siccmram |
| отладчик остановился в «чужом» обработчике | все слабые обработчики — один адрес; смотреть CFSR и HFSR |
HardFault сразу после перехода по указателю |
чётный адрес функции — INVSTATE |
| Release ведёт себя иначе, чем Debug | -Os вместо -O0: всплыли неопределённое поведение и забытый volatile |
| точка наблюдения не ставится | в DWT четыре компаратора — лишние снять |
| SWV молчит | нет SWO на разъёме, неверная частота ядра в настройках или ядро M0 |
| кнопка «Open Project» в CubeMX ничего не открывает | расширения .cproject и .project не связаны с CubeIDE — связать один раз вручную |
4б.12 Итоги
- CubeIDE = Eclipse CDT + GCC + GDB, в версиях 1.x ещё и CubeMX (с 2.0 он отдельно); всё настраиваемое лежит текстом в
.cproject. - Проект:
.project,.cproject, скрипт.ld, стартовый файл,syscalls.c,sysmem.c. - Debug:
-g3 -O0 -DDEBUG; Release:-Os; обаfpv4-sp-d16, hard float. - Пример: Debug 8708 байт кода, Release 8392; ОЗУ 1688 байт из 128 КБ.
.dataработает с 0x20000000, хранится за.rodata; векторов 98 слов = 392 байта.- newlib-nano: float в
printfвыключен, с ним код 22 004 байта вместо 8708. - HardFault по чётному адресу: CFSR 0x00020000 (INVSTATE), HFSR 0x40000000 (FORCED), PC в кадре 0x08000200.
- Окна отладчика — это команды GDB, их можно дать и без среды.
Полные исходники
main.c — программа поверх файлов проекта CubeIDE: printf в USART1, malloc и нарочная ошибка для разбора HardFault.
/* Раздел 4б: программа поверх файлов проекта STM32CubeIDE — стартовый код, скрипт компоновщика,
* syscalls.c и sysmem.c взяты из проекта ST без правок. printf идёт через _write → __io_putchar в USART1,
* malloc — через _sbrk; в конце нарочная ошибка, чтобы разобрать HardFault в отладчике. */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR (*(volatile uint32_t *)0x40011000)
#define USART1_DR (*(volatile uint32_t *)0x40011004)
#define USART1_BRR (*(volatile uint32_t *)0x40011008)
#define USART1_CR1 (*(volatile uint32_t *)0x4001100C)
extern uint8_t _end; /* конец .bss — начало кучи, из скрипта компоновщика */
int calls; /* .bss — за ней следит точка наблюдения */
void SystemInit(void) /* в настоящем проекте — system_stm32f4xx.c: такты, FPU */
{
*(uint32_t *)0xE000ED88 |= 0xFu << 20; /* SCB->CPACR: доступ к FPU, иначе первая же операция с float — сбой */
}
int __io_putchar(int ch) /* syscalls.c зовёт её из _write для каждого символа */
{
while (!(USART1_SR & (1u << 7))) {
}
USART1_DR = (uint32_t)ch;
return ch;
}
static int square(int x)
{
calls++;
return x * x;
}
int main(void)
{
RCC_APB2ENR |= 1u << 4;
USART1_BRR = 0x008B;
USART1_CR1 = (1u << 13) | (1u << 3);
setvbuf(stdout, NULL, _IONBF, 0); /* без буфера: каждый printf сразу уходит в _write */
printf("printf через _write: square(7) = %d\n", square(7));
char *p = malloc(100);
printf("malloc(100) = %p, _end = %p\n", (void *)p, (void *)&_end);
char *big = malloc(200000); /* больше всего ОЗУ: _sbrk вернёт отказ */
printf("malloc(200000) = %p\n", (void *)big);
printf("calls = %d, float: %.2f\n", calls, 1.5f * 3);
printf("сейчас будет HardFault\n");
void (*jump)(void) = (void (*)(void))0x08000200; /* чётный адрес: у Cortex-M нет режима ARM */
jump();
printf("сюда не дойдём\n");
return 0;
}
Дальше: раздел 4в «Сборка без среды» — свой Makefile, стартовый код на C и скрипт компоновщика с нуля.