
Главное преимущество перехода с GNU Make на CMake это получение кроссплатформенности. То есть вы сможете одними и теми же скриптами собирать прошивки как в окружении Windows, так и в окружении Linux. Буква С в слове CMake как раз это сокращение от Cross Platform. Если вы работаете только в Windows (или только на Mac), то смысла переходить на CMake просто нет.
Дело в том, что самостоятельно написанные скрипты Make как правило имеют специфические для конкретной ОС строки. Это пути, которые начинаются с диска С или название утилит с суффиксом exe. Скрипты написанные для Win вероятно не будут отрабатывать в Linux и наоборот. Хотя можно постараться написать Make максимально переносимым образом.
Поэтому для разработки в условиях кроcсплатформенности OS нужна какая-то надсистема. Этой надсистемой и является утилита CMake (с 1999 года).
Постановка задачи:
Написать на Си проект прошивки для микроконтроллера STM32F407VE. В качестве кросс компилятора выбрать ARM-GCC. Алогритм работы прошивки пока не так важен. Главное то, что в качестве системы сборки следует использовать CMake генерирующий GNU Make скрипты. В качестве HAL использовать фирменный HAL от STM и CMSIS от ARM. Собирать проект в Windows 10.
Компилятору следует передать пучок опций
-MMD -MP -O0 -std=c11 -Wall -Werror -mcpu=cortex-m4 -march=armv7e-m -Werror=address -Werror=address-of-packed-member -Werror=all -Werror=array-bounds=1 -Werror=bool-compare -Werror=bool-operation -Werror=char-subscripts -Werror=clobbered -Werror=comment -Werror=div-by-zero -Werror=duplicate-decl-specifier -Werror=duplicated-cond -Werror=empty-body -Werror=enum-compare -Werror=extra -Werror=float-equal -Werror=ignored-qualifiers -Werror=implicit -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=init-self -Werror=int-in-bool-context -Werror=int-to-pointer-cast -Werror=logical-not-parentheses -Werror=logical-op -Werror=maybe-uninitialized -Werror=memset-elt-size -Werror=memset-transposed-args -Werror=misleading-indentation -Werror=missing -Werror=missing-braces -Werror=missing-parameter-type -Werror=multistatement-macros -Werror=old-style-declaration -Werror=overflow -Werror=override-init -Werror=parentheses -Werror=pointer-arith -Werror=pointer-sign -Werror=pointer-to-int-cast -Werror=return-local-addr -Werror=return-type -Werror=sequence-point -Werror=shadow -Werror=shift-count-overflow -Werror=shift-negative-value -Werror=sign-compare -Werror=sizeof-pointer-div -Werror=sizeof-pointer-memaccess -Werror=strict-aliasing -Werror=strict-overflow=1 -Werror=switch -Werror=switch-default -Werror=tautological-compare -Werror=trigraphs -Werror=type-limits -Werror=uninitialized -Werror=unused -Werror=unused -Werror=unused-but-set-parameter -Werror=unused-but-set-variable -Werror=unused-value -Werror=unused-variable -Werror=unused-variable -g3 -Wextra -Wno -Wno-conversion -Wno-cpp -Wno-discarded-qualifiers -Wno-discarded-qualifiers -Wno-implicit -Wno-int-conversion -Wno-nonnull-compare -Wno-redundant-decls -Wno-restrict -Wno-sign-compare -Wno-stringop-truncation -Wno-switch-bool -Wno-unused-parameter -fallthrough -fdata-sections -fdce -fdse -ffreestanding -ffunction-sections -field-initializers -finline -finline-small -fmax-errors=70 -fmessage-length=0 -fno-common -fno-move-loop-invariants -fno-printf-return-value -fomit -format -format-truncation -frame-pointer -fshort-enums -fsigned-char -fstack-usage -function -function-declaration -functions -functions -g3 -fzero-initialized-in-bss -mfloat-abi=hard -mfpu=fpv4-sp-d16 -mthumb
Компоновщику же следует передать вот эти ключи:
-specs=nosys.specs -Xlinker --gc-sections -Xlinker --nmagic -Wl,--gc-sections -Xlinker --print-memory-usage -t -Wl,--cref -Wl,--gc-sections --verbose -mcpu=cortex-m4 -march=armv7e-m -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -lm
В чем проблема?
Трудность в том, что STMcubeMX не может сгенерировать CMake проект. В лучшем случае можно выбрать только GNU Make сборку.

Теория
Переменная окружения - текстовая переменная операционной системы, хранящая какую-либо информацию — например, данные о настройках системы, пути к исходникам, ключи для компилятора, названия утилит и прочее. Переменные окружения видны всем утилитам. Через ПЕ можно передавать в программы аргументы.
Система сборки (build system) — это набор инструментов и процессов, которые автоматизируют преобразование исходного кода в исполняемые приложения или артефакты для развертывания. Такие системы обрабатывают различные задачи: компиляцию кода, разрешение зависимостей. Примерами систем сборки является GNU Make, Ninjia, Xcode, Visual Studio (Microsoft Windows), Android Studio , IAR Workbench.
Система сборки нужна потому, что построение программы - это не простой процесс. Сборка прошивки похоже на конвейер автозавода, которым надо управлять. Этим и занимается утилита системы сборки.

Генераторы систем сборки (build system generator) - утилиты, которые генерируют код системы сборки. Это Autotools, Cmake, Meson.

в случае с СMake + GNU Make схема сборки выглядит как-то так:

Реализация
Скорее всего Cmake уже установлен на вашем PC. Это можно проверить набрав команду
where cmake
C:\Users\User>where cmake C:\cygwin64\bin\cmake.exe C:\Program Files\CMake\bin\cmake.exe
Скорее всего у вас уже есть какой-то проект прошивки собираемый через GNU Make скрипты и надо лишь на основе готовых make скриптов синтезировать аналогичные Cmake скрипты. Для этого можно применить вот эту подсказку.
Миграция с GNU Make на CMake
При наличии уже готовых GNU Make скриптов, миграция на CMake сводится к простой механической замене текстовых шаблонов кода согласно таблице подсказке ниже. С этой задачей справляется даже DeepSeek.
GNU Make | CMake | Пояснение |
.mk | .cmake | Расширение файла |
VAR += value | string(APPEND VAR " value") | пополнение переменной окружения |
CRC=Y | set(CRC Y) | Определение переменной окружения |
$( ) | ${ } | Вставка переменной окружения |
endif | endif() | конец if |
$(info text) | message(STATUS "text") | printf - отладка |
$(error text) | message(FATAL_ERROR "text") | printf - отладка |
ifeq ($(IAR),Y) | if(IAR STREQUAL Y) | Условный оператор |
ifeq ($(A),Y) | if(A STREQUAL Y) | Условный оператор |
ifeq ($( | if ( | Условный оператор |
COMPILE_GCC_OPT += -Wall | add_compile_options( -Wall) | пополнение переменной окружения |
MCAL_OPT += -DHAS_ARM | set(MCAL_OPT “${MCAL_OPT} -DHAS_ARM”) | добавить в конец переменной окружения текст |
ifneq ($( | if( NOT ( | if not |
@ECHO ${error Hello) | message( SEND_ERROR "Hello" ) | printf - отладка |
OPT += -D | target_compile_definitions(app PUBLIC | добавление ключей |
OPT += -DHAS_CORTEX_M4 | add_compile_definitions(-DHAS_CORTEX_M4) | добавление ключей |
INCDIR += -I xxx/xxxx/xxx | target_include_directories(app PUBLIC xxx/xxx/xxx) | добавление путей |
SOURCES_C += src/main.c | target_sources(app PRIVATE src/main.c) | добавление исходников |
SOURCES_C += src/main.c | add_executable(Application src/main.c) | добавление исходников |
LINKER_GCC_FLAGS += -u printffloat | add_link_options( -u_printf_float ) | Добавление ключей компоновщику |
Синтаксис GNU Make существенно лаконичнее и проще чем CMake.
было LINKER_FLAGS += -u _printf_float стало target_link_options(app PRIVATE -u _printf_float) ----------------------------------------------------- было include $(WORKSPACE_LOC)/connectivity/connectivity.mk стало include(${WORKSPACE_LOC}/connectivity/connectivity.cmake) ------------------------------------------ было SOURCES_THIRD_PARTY_C += $(STM32F4X_HAL_DRIVER_DIR)/stm32f4xx_hal_cortex.c стало string(APPEND SOURCES_THIRD_PARTY_C " ${STM32F4X_HAL_DRIVER_DIR}/stm32f4xx_hal_cortex.c") ------------------------------ было MCAL_OPT += -DFLAG1 стало string(APPEND MCAL_OPT " -DFLAG1") -------------------- было ifneq ($(CONNECTIVITY_MK_INC),Y) endif стало if( NOT (CONNECTIVITY_MK_INC STREQUAL Y)) endif() ------------------ было ifeq ($(AT_START_F413),Y) endif стало if(AT_START_F413 STREQUAL Y) endif() -------------------- было MCAL_OPT += -DUSE_HAL_DRIVER стало add_compile_definitions(USE_HAL_DRIVER)
Как видите, CMake заметно более многословный язык в сравнении с GNU Make.
Утилита CMake работает с файлом CMakeLists.txt. Его надо подвергать версионному контролю. Корневой CMakeLists.txt выглядеть может так:
cmake_minimum_required(VERSION 3.16) project(jz_f407vet6_mbr_gcc_cmake) set(PROJECT_NAME jz_f407vet6_mbr_gcc_cmake) enable_language(C ASM) set(CURRENT_CMAKELISTS_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(PROJECT_LOC ${CMAKE_CURRENT_SOURCE_DIR}) include_directories( ${PROJECT_LOC}) set(BUILD_DIR ${PROJECT_LOC}/build) set(TARGET ${PROJECT_NAME}) set(EXECUTABLE ${TARGET}) get_filename_component(WORKSPACE_LOC "${PROJECT_LOC}/../.." ABSOLUTE) include_directories(${WORKSPACE_LOC}) include(${PROJECT_LOC}/config.cmake) include(${WORKSPACE_LOC}/cmake_scripts/code_base.cmake) include(${WORKSPACE_LOC}/cmake_scripts/rules.cmake)
Каждая сборка уникальная. Уникальность программы формируется набором тех программных компонентов из которых она состоит. Выбрать набор программных компонентов, которые будут участвовать в построении можно при помощи переменных окружения. Достаточно просто определить эти переменные окружения в файле config.cmake. projects\jz_f407vet6_mbr_gcc_cmake\config.cmake
set(CONTROL Y) set(DWT Y) set(ARM_GCC Y) set(CORTEX_M4 Y) set(FLASH Y) set(FPU Y) set(GPIO Y) set(INTERRUPT Y) set(JZ_F407VET6 Y) set(LED_MONO Y) set(MBR Y) set(MCAL_STM32 Y) set(MICROCONTROLLER Y) set(NVIC Y) set(RCC Y) set(SCHEDULER Y) set(STM32F407VE Y) set(STM32F4X_HAL_DRIVER Y) set(SUPER_CYCLE Y) set(SYSTEM Y) set(SYSTICK Y) set(SYS_INIT Y) set(TIME Y)
Все программные компоненты надо добавить внутри code_base.cmake. Один за другим. Согласно активированным переменным окружения. Вот так может выглядеть CMake скрипт для программного компонента GPIO. Тут важно заметить, что к каждой папке, в которой лежат исходники, надо указать путь при помощи CMake функции include_directories
if(NOT (GPIO_DRV_MK_INC STREQUAL Y)) set(GPIO_DRV_MK_INC Y) set(GPIO_DIR ${MCAL_CUSTOM_DIR}/gpio) include_directories( ${GPIO_DIR}) string(APPEND MCAL_OPT " -DHAS_GPIO_CUSTOM") string(APPEND SOURCES_C " ${GPIO_DIR}/gpio_mcal.c") string(APPEND SOURCES_C " ${GPIO_DIR}/gpio_custom_isr.c") if(CLI STREQUAL Y) if(GPIO_COMMANDS STREQUAL Y) string(APPEND MCAL_OPT " -DHAS_GPIO_COMMANDS") string(APPEND SOURCES_C " ${GPIO_DIR}/gpio_custom_commands.c") endif() endif() if(DIAG STREQUAL Y) if(GPIO_DIAG STREQUAL Y) string(APPEND MCAL_OPT " -DHAS_GPIO_DIAG") string(APPEND SOURCES_DIAG_C " ${GPIO_DIR}/gpio_custom_diag.c") endif() endif() endif()
CMake очень гибок в вопросах настройки того, что именно мы хотим собрать. Чтобы подключить файл к сборке надо прописать его путь в переменную окружения SOURCES_C_TOTAL и передать эту переменную в CMake функцию add_executable. Ключи компилятору тоже передаются через переменную окружения MCAL_OPT, которая передается в CMake функцию add_definitions.
set(CMAKE_C_STANDARD 11) add_definitions(${MCAL_OPT}) string(APPEND SOURCES_C_TOTAL "${SOURCES_ASM}") string(APPEND SOURCES_C_TOTAL "${SOURCES_C}") string(APPEND SOURCES_C_TOTAL "${SOURCES_DIAG_C}") string(APPEND SOURCES_C_TOTAL "${SOURCES_THIRD_PARTY_C}") string(APPEND SOURCES_C_TOTAL "${SOURCES_CONFIGURATION_C}") message(STATUS "SOURCES_C_TOTAL=${SOURCES_C_TOTAL}") separate_arguments(SOURCES_LIST UNIX_COMMAND ${SOURCES_C_TOTAL}) add_executable(${EXECUTABLE} ${SOURCES_LIST}) include(${WORKSPACE_LOC}/cmake_scripts/toolchain.cmake) include(${WORKSPACE_LOC}/cmake_scripts/compiler_gcc_options.cmake) include(${WORKSPACE_LOC}/cmake_scripts/linker.cmake) # Print executable size add_custom_command(TARGET ${EXECUTABLE} POST_BUILD COMMAND arm-none-eabi-size ${EXECUTABLE}.elf) # Create hex file add_custom_command(TARGET ${EXECUTABLE} POST_BUILD COMMAND arm-none-eabi-objcopy -O ihex ${EXECUTABLE}.elf ${PROJECT_NAME}.hex COMMAND arm-none-eabi-objcopy -O binary ${EXECUTABLE}.elf ${PROJECT_NAME}.bin) set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX .elf ) set_target_properties(${EXECUTABLE} PROPERTIES LINK_FLAGS "-Wl,--no-undefined" WINDOWS_EXPORT_ALL_SYMBOLS OFF ) set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf") set(CMAKE_EXECUTABLE_SUFFIX_C ".elf") set(CMAKE_EXECUTABLE_SUFFIX_CXX ".elf") #set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
Сборка прошивки это кросс компиляция. Это значит, что надо определить каким именно компилятором мы будем собирать Си код. Выбор компилятора происходит в файле скрипте toolchain.cmake
message(STATUS "WORKSPACE_LOC=${WORKSPACE_LOC}") if(ARM_GCC STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_arm_gcc.cmake) endif() if(RISC_V_GCC STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_risc_v_gcc.cmake) endif() if(GHS STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_ghs.cmake) endif() if(CLANG STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_clang.cmake) endif() if(TCC STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_tcc.cmake) endif() if(MINGW STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_mingw.cmake) endif() if(IAR STREQUAL Y) include( ${WORKSPACE_LOC}/cmake_scripts/toolchain_iar.cmake) endif()
в моем случае выбирается компилятор ARM GCC.
set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g++) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) set(CMAKE_AR arm-none-eabi-ar) set(CMAKE_OBJCOPY arm-none-eabi-objcopy) set(CMAKE_OBJDUMP arm-none-eabi-objdump) set(SIZE arm-none-eabi-size) set(CMAKE_SIZE arm-none-eabi-size)
Компилятору можно передавать сотни разнообразных ключей, которые оказывают существенное влияние на результирующую программу. CMake позволяет передавать ключи компилятору через функцию target_compile_options()
target_compile_options(${EXECUTABLE} PRIVATE -Wall) target_compile_options(${EXECUTABLE} PRIVATE -MD ) target_compile_options(${EXECUTABLE} PRIVATE -ffreestanding ) target_compile_options(${EXECUTABLE} PRIVATE -ffunction-sections ) target_compile_options(${EXECUTABLE} PRIVATE -fdata-sections ) target_compile_options(${EXECUTABLE} PRIVATE -fno-common ) target_compile_options(${EXECUTABLE} PRIVATE -fno-printf-return-value ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=shadow ) target_compile_options(${EXECUTABLE} PRIVATE -fshort-enums ) target_compile_options(${EXECUTABLE} PRIVATE -fomit-frame-pointer ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=return-local-addr ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=missing-declarations ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=missing-prototypes ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=redundant-decls ) target_compile_options(${EXECUTABLE} PRIVATE -Wno-nonnull-compare ) target_compile_options(${EXECUTABLE} PRIVATE -Wall ) target_compile_options(${EXECUTABLE} PRIVATE -fdse ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=address-of-packed-member ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=missing-field-initializers ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=unused-but-set-variable ) target_compile_options(${EXECUTABLE} PRIVATE -Werror=unused-variable ) if(DEBUG STREQUAL Y) target_compile_options(${EXECUTABLE} PRIVATE -O0 ) target_compile_options(${EXECUTABLE} PRIVATE -g3 ) #target_compile_options(${EXECUTABLE} PRIVATE -gdwarf-2 ) else() target_compile_options(${EXECUTABLE} PRIVATE -Os ) endif() if(HI_PERF STREQUAL Y) target_compile_options(${EXECUTABLE} PRIVATE -Ofast ) endif() if(PACK_PROGRAM STREQUAL Y) target_compile_options(${EXECUTABLE} PRIVATE -Os ) target_compile_options(${EXECUTABLE} PRIVATE -flto ) endif() target_compile_options(${EXECUTABLE} PRIVATE -Wno-cpp) target_compile_options(${EXECUTABLE} PRIVATE ${OPTIMIZATION} ) target_compile_options(${EXECUTABLE} PRIVATE ${CSTANDARD} ) add_compile_options( ${MCAL_OPT} ) add_compile_options( ${MICROPROCESSOR} )
Скрипты сборки хороши тем, что вы можете очень гибко собирать пучок опций для компоновщика. Вот так может выглядеть файл linker.cmake для STM32F407x
target_link_options(${EXECUTABLE} PRIVATE -specs=nano.specs) target_link_options(${EXECUTABLE} PRIVATE -specs=nosys.specs) target_link_options(${EXECUTABLE} PRIVATE -u_scanf_float) target_link_options(${EXECUTABLE} PRIVATE -u_printf_float) target_link_options(${EXECUTABLE} PRIVATE -Wl,--gc-sections) target_link_options(${EXECUTABLE} PRIVATE -T ${LDSCRIPT}) target_link_options(${EXECUTABLE} PRIVATE -Wl,--print-memory-usage) target_link_options(${EXECUTABLE} PRIVATE -Wl,-Map=${BUILD_DIR}/${PROJECT_NAME}.map,--cref) set(CMAKE_C_LINK_EXECUTABLE "<CMAKE_C_COMPILER> <FLAGS> <CMAKE_C_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET>" )
Инициировать процесс генерации и сборки можно всего двумя строками из bat файла.
echo off cls cmake -S . -B build --warn-uninitialized -G "Unix Makefiles" cmake --build build/ -- VERBOSE=1
Вот с такими скриптами мне и удалось собрать свой проект первичного загрузчика MBR. И вот я собрал артефакты. Бинари оказались в папке build

Можно брать elf файл и прошивать его по интерфейсу JTAG программой Segger Ozone.
Недостатки сборки прошивок из-под CMake
--CMake подмешивает компоновщику ненужные ключи
--Бинарь разрастается в размере по непонятным причинам
--Финальный Makefile получается грязный. С душком
Достоинства сборки прошивок из-под CMake
++Можно выбирать систему сборки: Ninja, Make и пр.
++Если вы не понимаете язык программирования GNU Make, то вы можете попробовать выучить более простой язык - CMake, который сгенерирует вам в общем-то работоспособный скрипт GNU Make для сборки микроконтроллерной прошивки.
++CMake делает progress bar сборки проекта, который отображается в логе сборки на фазе отработки GNU Make скриптов.

Итоги
Удалось собрать прошивку для STM32 из-под самостоятельно написанных CMake скриптов. Это открывает дорогу для кросcплатформенной разработки прошивок для микроконтроллеров STM32 и интеграции проектов на сервера сборки.
Благодаря скриптам сборки вы можете не просто запрограммировать микроконтроллер, а можете также запрограммировать ещё и сам процесс сборки программы этой прошивки.
Источники
ссылка | URL |
CMake Reference Documentation | |
CMake Reference Documentation | |
CMake Справочная документация | |
cmake(1) - Linux man page | |
STM32 + Cmake + CubeMX | |
CMake on STM32 | Episode 1: the beginning | |
Почему важно собирать код из скриптов | |
Настройка IDE Clion и Cmake для работы с STM32 и C++ | |
CMake project for an ARM Cortex-M cross compilation project | |
How to use CMake in STM32CubeIDE | |
Сборка под stm32duino с помощью CMake | |
Сборка и отладка прошивки IoT-модуля: Python, make, апельсины и чёрная магия | |
Why We Need Build Systems URL | |
CMake Part 1 – The Dark Arts URL | https://blog.feabhas.com/2021/07/cmake-part-1-the-dark-arts/ |
CMake Part 2 – Release and Debug builds URL | https://blog.feabhas.com/2021/07/cmake-part-2-release-and-debug-builds/ |
CMake Part 3 – Source File Organisation URL | https://blog.feabhas.com/2021/08/cmake-part-3-source-file-organisation/ |
CMake Part 4 – Windows 10 Host | https://feabhasblog.wpengine.com/2021/09/cmake-part-4-windows-10-host/ |
STM32 CMake Template | |
Используем CMake и GCC для программирования uC STM32 в линуксе. | |
STM32 without CubeIDE (Part 4): CMake, FPU and STM32 libraries | https://kleinembedded.com/stm32-without-cubeide-part-4-cmake-fpu-and-stm32-libraries/ |
CMake on STM32 | Episode 1: the beginning |
Вопросы
Что такое система сборки?
Назовите как можно больше способов передавать аргументы в утилиту при старте.
Зачем использовать какую бы то ни было систему сборки (хоть GNU Make) если можно просто написать .bat или .sh скрипт который скармливает комилятору исходники? В cmd или bash скриптах же тоже есть переменные и условные операторы.
Как на языке CMake добавить в конец переменной окружения MCU_FLAGS строку -mcpu=cortex-m4 ? set(MCU_FLAGS "${MCU_FLAGS} -mcpu=cortex-m4")
