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

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_Handler
system 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: файлы проекта собирает arm-none-eabi-gcc в app.elf и карту памяти, отладку ведёт GDB через сервер ST-LINK или OpenOCD, а сбой разбирается по регистрам SCB

Среда складывается из четырёх частей, и у каждой есть обычный заменитель из командной строки:

В 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 обещает обнулить, — годятся только буферы, которые программа заполнит сама, а переменную с начальным значением туда класть нельзя без своего цикла копирования.

Карта памяти после сборки Debug: секции и копия .data во флеш-памяти, в ОЗУ .data и .bss, куча вверх от _end до предела _sbrk, стек вниз от _estack 0x20020000


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

Разбор по шагам:

  1. Где стоим. GDB назвал точку WWDG_IRQHandler: у всех слабых обработчиков один адрес, и отладчик берёт первое имя. Сбой ли это, видно по регистрам, а не по имени.
  2. CFSR (0xE000ED28) = 0x00020000 — бит 17, INVSTATE: попытка перейти в режим ARM. У Cortex-M есть только Thumb, младший бит адреса перехода обязан быть единицей.
  3. HFSR (0xE000ED2C) = 0x40000000 — бит FORCED: UsageFault выключен по умолчанию и поднялся до HardFault.
  4. LR = 0xFFFFFFF9 — это не адрес, а код возврата из исключения: кадр лежит на основном стеке MSP.
  5. Кадр исключения — восемь слов на стеке: R0–R3, R12, LR, PC, xPSR. Сохранённый PC — 0x08000200, тот самый чётный адрес. Сохранённый LR 0x08001ef9 — main + 133, строка сразу за вызовом.

Разбор HardFault: переход по чётному адресу 0x08000200 вызывает INVSTATE, ядро кладёт на основной стек кадр из восьми слов с сохранёнными R0, LR, PC и xPSR, причина читается из CFSR по адресу 0xE000ED28 со значением 0x00020000 и HFSR по адресу 0xE000ED2C со значением 0x40000000, а код возврата LR 0xFFFFFFF9 говорит, что кадр лежит на MSP

Команды, которыми это читается:

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 требует настоящего ядра и отладчика, в эмуляторе её не показать:


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 Итоги

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

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 и скрипт компоновщика с нуля.

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