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

115а. CMake-проект прошивки: от .ioc до VS Code

Раздел 115 в подразделе 115.4 собрал прошивку CMake с файлом тулчейна и пресетами — со своим стартовым кодом, без HAL. Раздел 4в собирал программу Makefile-ом и своим скриптом компоновщика, раздел 145 (145.4) — утилиту для компьютера через CMake и Ninja. Здесь — проект, который создаёт STM32CubeMX, если выбрать в нём CMake: что лежит в каждом файле, что перепишет следующая генерация и как вырастить из него проект на две платы с HAL отдельной библиотекой, картой памяти, .hex и .bin.

Опыт — мигание светодиодом на HAL для BluePill (STM32F103C8, вывод PC13) и STM32F4 Discovery (STM32F407VG, вывод PD12). Проекты созданы STM32CubeMX 6.18.1 из командной строки и собраны CMake 3.25.1, Ninja 1.11.1 и arm-none-eabi-gcc 12.2.1 в образе stm-arm:1. HAL для F103 — из пакета STM32Cube FW_F1 V1.8.7 (HAL 1.1.10), для F407 — из пака Keil STM32F4xx_DFP 2.17.1 (HAL 1.8.1), заголовки ядра core_cm4.h — из пака ARM CMSIS 5.9.0.

Опоры: руководство UM1718 (Rev 50) — командная строка CubeMX, выбор CMake и компилятора, сохранение кода пользователя; шаблоны CMake внутри STM32CubeMX 6.18.1; прогон сборки и повторной генерации.

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

Оригинал Перевод
toolchain file файл тулчейна: чем собирать для чужой машины
presets (CMakePresets.json) готовые наборы настроек: генератор, каталог, тип сборки
target цель CMake: программа или библиотека со своими ключами
INTERFACE, PUBLIC, PRIVATE кому передаются ключи цели: только тем, кто её подключает, всем или только ей
object library библиотека из объектных файлов без архива .a
generator expression ($<CONFIG:Debug>) выражение, которое CMake вычисляет при генерации сборки
LINK_DEPENDS файлы, от которых зависит компоновка
POST_BUILD команда после сборки цели
compilation database (compile_commands.json) список команд компиляции для редактора
user code section (USER CODE BEGIN/END) место в файле CubeMX, которое генерация не трогает

115а.1 Что генерирует CubeMX для CMake

CMake-проект из CubeMX: файл .ioc и повторная генерация; не переписываются CMakeLists.txt, пресеты, файл тулчейна, скрипт компоновщика и стартовый файл; переписываются список cmake/stm32cubemx и код вне USER CODE; свой код и свой проект CubeMX не трогает; сборка пресетом через Ninja в fw.elf и затем в hex и bin

В настройках проекта CubeMX пункт Toolchain/IDE — CMake; рядом появляется выбор компилятора: GCC или Starm-Clang. Из командной строки то же делает сценарий:

config load blink103.ioc
project toolchain CMake
project compiler GCC
project generate
exit

Сценарий передают ключом -q — тогда CubeMX работает без окна: STM32CubeMX.exe -q сценарий.txt. Проект F103 из такого сценария получил:

Файл Что в нём
CMakeLists.txt верхний файл: стандарт C11, тип сборки по умолчанию Debug, цель blink103, места для своих файлов
CMakePresets.json пресеты Debug и Release, генератор Ninja, каталог build/<пресет>
cmake/gcc-arm-none-eabi.cmake файл тулчейна: -mcpu=cortex-m3, ключи предупреждений и оптимизации, путь к скрипту компоновщика
cmake/starm-clang.cmake то же для Starm-Clang
cmake/stm32cubemx/CMakeLists.txt списки CubeMX: макросы, пути, исходники HAL и Core
STM32F103xx_FLASH.ld, startup_stm32f103xb.s скрипт компоновщика и стартовый файл
Core/, Drivers/ код CubeMX и копия HAL с CMSIS

Первые строки верхнего CMakeLists.txt говорят: файл создаётся один раз и дальше не переписывается. Среди шаблонов CubeMX есть ещё mx-generated.cmake с пометкой «Do not modify», но у F103 и F407 его нет: его подключают шаблоны проектов с несколькими контекстами (два ядра или TrustZone). У одноядерного камня всё, что CubeMX переписывает, собрано в cmake/stm32cubemx/CMakeLists.txt.

Что именно переписывает генерация, проверено опытом: в копию проекта в каждый файл дописана метка, CubeMX заново выполнил config load и project generate, и после этого видно, какая метка уцелела:

Файл Метка после генерации
CMakeLists.txt, CMakePresets.json осталась
cmake/gcc-arm-none-eabi.cmake, cmake/starm-clang.cmake осталась
cmake/stm32cubemx/CMakeLists.txt стёрта
STM32F103xx_FLASH.ld, startup_stm32f103xb.s осталась
syscalls.c, sysmem.c, system_stm32f1xx.c осталась
stm32f1xx_it.c, stm32f1xx_hal_conf.h (в конце файла) стёрта
main.c внутри USER CODE осталась
main.c вне USER CODE стёрта
свой файл Core/Src/extra.c остался

Каталога Drivers/ в копии не было — генерация скопировала его снова. UM1718 пишет о том же: сохранение кода касается только мест USER CODE в файлах CubeMX, а свои файлы, добавленные руками, генерация не трогает. Отсюда правило: свой код — в своих файлах или внутри USER CODE, свои строки CMake — в верхнем CMakeLists.txt, но не в cmake/stm32cubemx/CMakeLists.txt. Так в проекте CubeMX вызов мигания стоит в цикле main.c:

    /* USER CODE BEGIN 3 */
    blink_step();
  }
  /* USER CODE END 3 */

115а.2 Цели вместо глобальных переменных

Файл тулчейна CubeMX делает больше, чем говорит его имя: пишет ключи ядра в общие переменные и путь к скрипту компоновщика — в ключи компоновки:

set(TARGET_FLAGS "-mcpu=cortex-m3 ")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T \"${CMAKE_SOURCE_DIR}/STM32F103xx_FLASH.ld\"")

Такой файл годится только для одного камня, а общие переменные получают все цели сразу. Список cmake/stm32cubemx/CMakeLists.txt устроен уже по-целевому: библиотека stm32cubemx без кода (INTERFACE) несёт макросы и пути, HAL собран в объектную библиотеку STM32_Drivers, файлы Core и стартовый файл добавлены в цель с именем проекта.

Свой проект на две платы (каталог fw) берёт этот список как есть, а всё, что зависит от платы, выносит из файла тулчейна:

cmake_minimum_required(VERSION 3.22)
project(fw C ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)        # compile_commands.json для редактора

set(BOARD f103 CACHE STRING "плата: f103 или f407")
include(${CMAKE_SOURCE_DIR}/boards/${BOARD}.cmake)   # MX_DIR, LD_SCRIPT, MCU_FLAGS

add_executable(fw)
add_subdirectory(${MX_DIR}/cmake/stm32cubemx stm32cubemx)   # список CubeMX — как сгенерирован

# ядро — всем, кто подключает stm32cubemx: HAL, коду CubeMX, своим модулям
target_compile_options(stm32cubemx INTERFACE ${MCU_FLAGS})

# свой модуль — отдельной библиотекой, со строгими предупреждениями только для себя
add_library(blink STATIC app/blink.c)
target_include_directories(blink PUBLIC app)
target_link_libraries(blink PRIVATE stm32cubemx)
target_compile_options(blink PRIVATE -Wextra -Wconversion -Werror)

target_link_libraries(fw blink)              # без PRIVATE: CubeMX подключает без ключевых слов
target_link_options(fw PRIVATE ${MCU_FLAGS} -T${LD_SCRIPT} --specs=nano.specs
    -Wl,--gc-sections -Wl,-Map=fw.map -Wl,--print-memory-usage)
set_target_properties(fw PROPERTIES LINK_DEPENDS ${LD_SCRIPT})   # правка .ld — перекомпоновка

add_custom_command(TARGET fw POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O ihex   $<TARGET_FILE:fw> fw.hex
    COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:fw> fw.bin
    COMMAND ${CMAKE_SIZE} $<TARGET_FILE:fw>)

Плата — три строки:

# BluePill: STM32F103C8, 64 КБ флеш-памяти, 20 КБ ОЗУ
set(MX_DIR    ${CMAKE_SOURCE_DIR}/mx/blink103)
set(LD_SCRIPT ${MX_DIR}/STM32F103xx_FLASH.ld)
set(MCU_FLAGS -mcpu=cortex-m3 -mthumb)

Файл тулчейна cmake/arm-gcc.cmake остался общим: компилятор, objcopy, size, ключи оптимизации и TOOLCHAIN_LINK_LIBRARIES m — эту переменную ждёт список CubeMX. Список CubeMX добавляет файлы в цель с именем ${CMAKE_PROJECT_NAME}, поэтому цель называется так же, как проект, — fw, и объявлена до add_subdirectory.

Зачем строгие ключи только своей библиотеке, показал опыт: с -Wextra -Wconversion на весь проект CubeMX у F103 вышло 4 предупреждения — 2 в stm32f1xx_hal_flash_ex.c и 2 в syscalls.c, у F407 — 13: 11 в HAL (8 в stm32f4xx_hal_flash_ex.c, 2 в stm32f4xx_hal_exti.c, 1 в stm32f4xx_hal_pwr.c) и 2 в syscalls.c. В blink.c — ни одного. С -Werror на весь проект сборка падает на чужом коде HAL; на цели blink — проверяет только свой.

После правки blink.c Ninja делает три шага: компиляция blink.c, архив libblink.a, компоновка fw.elf. HAL не пересобирается.


115а.3 Скрипт компоновщика и карта

Скрипт компоновщика для CMake — не исходник, а ключ -T, и о нём CMake не знает. Опыт: в STM32F103xx_FLASH.ld стек увеличен с 0x400 до 0x800. Проект CubeMX на это ответил ninja: no work to do. — прошивка осталась со старым стеком. Свой проект со свойством LINK_DEPENDS перекомпоновал fw.elf одним шагом, и ОЗУ выросло с 1584 до 2608 байт — ровно на 0x400.

Ключ -Wl,--print-memory-usage печатает заполнение областей при каждой сборке (F103, Release):

             RAM:        1584 B        20 KB      7.73%
           FLASH:        3116 B        64 KB      4.75%

Ключ -Wl,-Map=fw.map пишет карту. В ней области памяти из скрипта — флеш-память с 0x08000000 длиной 0x00010000 и ОЗУ с 0x20000000 длиной 0x00005000 — и происхождение каждой секции:

 .text.blink_step
                0x08000bc0       0x1c libblink.a(blink.c.obj)

Функция blink_step — 28 байт по адресу 0x08000bc0, из архива своей библиотеки. arm-none-eabi-size -A раскладывает образ по секциям: .isr_vector 268 байт, .text 2792, .rodata 36, .data 12, .bss 32, ._user_heap_stack 1540. Последняя — место под кучу 0x200 и стек 0x400 из скрипта, 1536 байт, плюс 4 байта выравнивания на 8; она ничего не занимает в образе, но не даёт компоновщику молча отдать это ОЗУ переменным. ОЗУ 1584 = 12 + 32 + 1540.

Куда ушла флеш-память, показывает arm-none-eabi-nm --size-sort -S: самая большая функция — HAL_RCC_OscConfig, 824 байта, дальше HAL_GPIO_Init — 432, HAL_RCC_ClockConfig — 312. Для сравнения: вся программа раздела 4в без HAL и без библиотеки C — 480 байт. Ключ -fstack-usage из файла тулчейна кладёт рядом с каждым объектным файлом .su: blink_step берёт 8 байт стека, пометка static — размер известен при компиляции.


115а.4 После сборки: .hex, .bin, size и предел флеш-памяти

Проект CubeMX .hex и .bin не делает: в его списке cmake/stm32cubemx/CMakeLists.txt нет ни objcopy, ни команды после сборки. Свой проект делает их командой POST_BUILD (листинг в 115а.2). У F103 в Release:

Файл Размер Что это
fw.bin 3116 байт байты флеш-памяти подряд, с 0x08000000
fw.hex 8857 байт Intel HEX: те же байты текстом, с адресами

Первая строка fw.hex — :020000040800F2: старшая половина адреса 0x0800. Последняя — :00000001FF, конец файла. Первые два слова fw.bin — 0x20005000, вершина стека (конец 20 КБ ОЗУ), и 0x08000b75 — адрес Reset_Handler с единицей в младшем бите: код Thumb.

size без ключей печатает text 3104, data 12, bss 1572; во флеш-память идут text и data — 3116 байт, ровно размер fw.bin.

Предел флеш-памяти задаёт область FLASH в скрипте, а следит за ним компоновщик. Опыт: в проект F103 добавлена таблица на 65 536 байт (а чтобы --gc-sections её не выбросил — ключ -Wl,--undefined). Сборка упала:

ld: region `FLASH' overflowed by 3116 bytes
           FLASH:       68652 B        64 KB    104.75%
collect2: error: ld returned 1 exit status

Перебор — ровно размер остальной прошивки: 65 536 + 3116 = 68 652. Отдельный порог в CMake не нужен: при переполнении цель не собирается, и в CI (раздел 110) сборка краснеет.


115а.5 Две платы и Debug/Release пресетами

Плата и тип сборки — переменные кэша, пресеты дают им имена:

        { "name": "f103-debug",   "inherits": "base", "cacheVariables": { "BOARD": "f103", "CMAKE_BUILD_TYPE": "Debug" } },
        { "name": "f103-release", "inherits": "base", "cacheVariables": { "BOARD": "f103", "CMAKE_BUILD_TYPE": "Release" } },

cmake --list-presets показывает четыре: f103-debug, f103-release, f407-debug, f407-release; каждый собирается в свой каталог build/<пресет>, и сборки не мешают друг другу. Debug — -O0 -g3 и макрос DEBUG (его добавляет выражение $<$<CONFIG:Debug>:DEBUG> в списке CubeMX), Release — -Os -g0:

Пресет Флеш-память ОЗУ Шагов Ninja
f103-debug 4868 байт 1584 байта 21
f103-release 3116 байт 1584 байта 21
f407-debug 6056 байт 1584 байта 23
f407-release 4148 байт 1584 байта 23

У F407 отчёт показывает и третью область скрипта: CCMRAM: 0 GB — так ld пишет ноль. Проекты CubeMX как есть (пресеты Debug и Release) собрались в 20 и 22 шага — на один меньше: у них нет архива libblink.a. Образы .bin из своего проекта и из проектов CubeMX совпали байт в байт во всех четырёх сочетаниях — хотя CubeMX не пишет -mthumb, а свой проект пишет: для ядер Cortex-M GCC и так выдаёт только Thumb.


115а.6 compile_commands.json и редактор

CMAKE_EXPORT_COMPILE_COMMANDS включён и в шаблоне CubeMX, с пояснением «для индексации, например, clangd». В каталоге сборки появляется compile_commands.json — для каждого файла полная команда компиляции. У f103-debug в нём 19 записей: 11 файлов HAL из Drivers, стартовый файл на ассемблере и остальные — Core и blink.c. Для blink.c там ровно те ключи, что собрали цель:

-DDEBUG -DSTM32F103xB -DUSE_HAL_DRIVER -Wall -O0 -Wextra -Wconversion -Werror -mcpu=cortex-m3 -mthumb -std=gnu11

По этому файлу редактор знает макрос камня, пути к HAL и стандарт C — переходы к определениям и подсказки работают так же, как собирает компилятор. Файл свой у каждого пресета: у f407-debug для того же blink.c — -DSTM32F407xx и -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard.

VS Code с расширением CMake Tools читает CMakePresets.json и предлагает те же пресеты; языковой сервер clangd берёт compile_commands.json из каталога сборки выбранного пресета. Прошивка и отладка из редактора — те же OpenOCD и GDB, что в разделе 35б, запущенные расширением. Повторные сборки на CI ускоряет ccache: его подставляют переменной CMAKE_C_COMPILER_LAUNCHER. Ни VS Code, ни ccache здесь не проверялись — в образе их нет.


115а.7 Грабли

Правка скрипта компоновщика ничего не меняет. ninja: no work to do. — CMake не знает, что .ld участвует в компоновке. Свойство LINK_DEPENDS у цели.

Своя строка пропала после генерации. Она стояла в cmake/stm32cubemx/CMakeLists.txt или вне USER CODE. Свои файлы и строки — в верхний CMakeLists.txt и в свои каталоги.

The plain signature for target_link_libraries has already been used. Список CubeMX подключает библиотеки без PRIVATE/PUBLIC, а CMake не даёт смешивать два вида у одной цели. У цели проекта — тоже без ключевого слова.

-Werror на весь проект — сборка падает в HAL. У F103 не собираются stm32f1xx_hal_flash_ex.c и syscalls.c, у F407 — stm32f4xx_hal_exti.c, stm32f4xx_hal_flash_ex.c, stm32f4xx_hal_pwr.c и syscalls.c. Строгие ключи — только своим целям.

ERRORGenerated code requires C11 or higher, а сборка идёт. При CMAKE_C_STANDARD 99 список CubeMX печатает это, но message(ERROR …) — не режим остановки CMake, и конфигурация доходит до Generating done. Стандарт — не ниже 11.

fatal error: core_cm4.h: No such file or directory. CubeMX взял библиотеку не из своего хранилища, а из пака Keil (project SetCustomFwPath): в паке Drivers/CMSIS есть только Device, без заголовков ядра. Их берут из пака ARM CMSIS.

**regionFLASH' overflowed.** Прошивка не влезла в область скрипта; число послеby` — на сколько.

CCMRAM: 0 GB. Не ошибка: так --print-memory-usage пишет пустую область.


115а.8 Итоги

Дальше: раздел 115б «Последовательный порт со стороны компьютера: мосты, драйверы, терминалы, задержки» — как прошивка говорит с компьютером по UART и что стоит между ними.

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