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 библиотека из объектных файлов без архива .agenerator 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
В настройках проекта 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 Итоги
- CubeMX 6.18.1 для CMake кладёт верхний
CMakeLists.txt, пресеты Debug и Release, файл тулчейна и списокcmake/stm32cubemx/CMakeLists.txt. Повторная генерация переписывает список и код внеUSER CODE; верхний файл, пресеты, тулчейн,.ldи стартовый файл не трогает. - Свой проект подключает список CubeMX как есть, ключи ядра и скрипт — от
платы, строгие предупреждения — только своей библиотеке: на весь проект
CubeMX
-Wextra -Wconversionдают 4 предупреждения у F103 и 13 у F407, в своёмblink.c— ни одного. LINK_DEPENDS: после правки.ldстек вырос на 0x400, ОЗУ 1584 → 2608 байт; без него —no work to do.- F103 Release: флеш-память 3116 байт из 64 КБ (4,75 %),
fw.bin3116 байт,fw.hex8857; при лишних 65 536 байтах —overflowed by 3116 bytes. - Четыре пресета: 4868 и 3116 байт у F103, 6056 и 4148 у F407; образы байт в байт как у проектов CubeMX.
compile_commands.json— 19 команд уf103-debug, ключи те же, что у компилятора.
Дальше: раздел 115б «Последовательный порт со стороны компьютера: мосты, драйверы, терминалы, задержки» — как прошивка говорит с компьютером по UART и что стоит между ними.