Сегодня мы рассмотрим как сделать систему сборки на основе сmake
, которая объединит в себе все С
и С++
, CubeMX
, VScode
и по сути будет логическим развитием этой и этой статей. По моему мнению это самый гибкий способ (кроме ситуации когда вы осознанно отказываетесь от CubeMX
, но об этом в другой раз) вести разработку под stm32 в 2к22 году. Само собой только под Linux
.
Для иллюстрации данного способа настройки среды я буду использовать STM32F3348-DISCO
с чипом STM32f334C8T6
со встроенным программатором STLink
. В принципе программатор может быть любым, который поддерживается в openocd
, а контроллер любой STM32.
Установить необходимого софта
Для работы нам будет нужен:
- CubeMX
- VSCode
- gcc-arm-none-eabi
- OpenOCD
Для VSCode
используются следующие расширения:
ms-vscode.cpptools-extension-pack
- пак с расширениями для C/C++ и Cmake.marus25.cortex-debug
- отладка кортекс контроллеров.
Про особенности установки данных пакетов можно прочесть тут, далее считаем что они установлены в системе. Так же для работы также могут пригодиться следующие программы:
sudo apt install cmake make git gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib binutils-multiarch gdb-multiarch openocd
Создание минимального проекта
Для начала нужно сделать обычный проект в CubeMX. Статья не про использование куба поэтому совсем вкратце, напомню что у нас STM32F3348-DISCO
:
- Выбираем плату
STM32F3348-DISCO
. - Соглашаемся на дефолтную пред настройку.
- Далее в
Project manager -> Project
задаем название проекта и выбираем генерироватьmakefile
. - В
Project manager -> Code Generator
выбираем только необходимые файлы и раздельную генерацию.c
и.h
файлов для разной периферии. - Генерируем проект.
В итоге получаем минимально рабочий проект с makefile
, который можно собрать и загрузить в плату. Мы же сейчас создадим и добавим в проект CMakeLists.txt
. Тут есть 2 принципиально разных подхода, первый это когда мы берем огромный готовый Cmake файл который учитывает все варианты генеренного кода в кубе, и автоматом собирает код, например так делают тут. В теории мы просто добавляем папку со скриптами в проект и наш код собирается какой-то магией. Имхо, такой метод только создаст нам новых проблем, например если в кубе что-то поменяю, а автор этого репозитория нет. Мы же пойдем по другому пути, а именно - не будем писать универсальное решение, будем затачивать Cmake под конкретный проект, однако некоторые автоматизацию мы все таки применим, но об этом позже.
Для этого вы можете взять мой CMakeLists.txt
и вписать свои данные в местах которые помечены # Add
, а также раскомментировать что-то из того что отмечено как # Uncomment
:
cmake_minimum_required(VERSION 3.17)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_VERSION 1)
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_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Add you project name
project(stm32f3348_disco_cmake C CXX ASM)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_STANDARD 11)
# Add carnel name
set(MCPU cortex-m4)
# Uncomment for hardware floating point
#add_compile_definitions(ARM_MATH_CM4;ARM_MATH_MATRIX_CHECK;ARM_MATH_ROUNDING)
#add_compile_options(-mfloat-abi=hard -mfpu=fpv4-sp-d16)
#add_link_options(-mfloat-abi=hard -mfpu=fpv4-sp-d16)
# Uncomment for software floating point
add_compile_options(-mfloat-abi=soft)
add_compile_options(-mcpu=${MCPU} -mthumb -mthumb-interwork)
add_compile_options(-ffunction-sections -fdata-sections -fno-common -fmessage-length=0)
# Uncomment to mitigate c++17 absolute addresses warnings
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-register")
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
message(STATUS "Maximum optimization for speed")
add_compile_options(-Ofast)
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
message(STATUS "Maximum optimization for speed, debug info included")
add_compile_options(-Ofast -g)
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "MinSizeRel")
message(STATUS "Maximum optimization for size")
add_compile_options(-Os)
else ()
message(STATUS "Minimal optimization, debug info included")
add_compile_options(-Og -g)
endif ()
# Add Include directories
include_directories(
${CMAKE_SOURCE_DIR}/Core/Inc
${CMAKE_SOURCE_DIR}/Drivers/STM32F3xx_HAL_Driver/Inc
${CMAKE_SOURCE_DIR}/Drivers/STM32F3xx_HAL_Driver/Inc/Legacy
${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F3xx/Include
${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Include
)
# Add C defines
add_definitions(-DUSE_HAL_DRIVER -DSTM32F334x8)
# Add you source file
file(GLOB_RECURSE SOURCES
"Core/Src/*.c"
"Drivers/STM32F3xx_HAL_Driver/Src/*.c"
"startup_stm32f334x8.s"
)
# Add lincer file
set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/STM32F334C8Tx_FLASH.ld)
# this options for C++
add_link_options(-specs=nosys.specs -lstdc++)
add_link_options(-Wl,-gc-sections,--print-memory-usage,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map)
add_link_options(-mcpu=${MCPU} -mthumb -mthumb-interwork)
add_link_options(-T ${LINKER_SCRIPT})
add_executable(${PROJECT_NAME}.elf ${SOURCES} ${LINKER_SCRIPT})
set(HEX_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.hex)
set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin)
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -Oihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${HEX_FILE}
COMMAND ${CMAKE_OBJCOPY} -Obinary $<TARGET_FILE:${PROJECT_NAME}.elf> ${BIN_FILE}
COMMENT "Building ${HEX_FILE}
Building ${BIN_FILE}")
Здесь есть несколько интересных моментов:
- Если не поставить галочку о копировании только необходимых файлов библитек. то
"Drivers/STM32F3xx_HAL_Driver/Src/*.c"
не сработает, и нужно будет перечислять исходники вручную. - Название проекта должно совподать с названием выбранном в кубе (так будет называтся папка с проектом и он указан в make файле).
Этот файл в тупую содран отсюда. Однако я не хочу на этом остановливаться. Можно подметить что MakeFile
имеет строгую постоянную структуру, которая не меняется что-бы мы не выбрали в кубе, что если распарсить его? Это позволило бы нам использовать один cmake ко всем проектам собраным в кубе, и мы описывали только файлы добавленные нами:
cmake_minimum_required(VERSION 3.17)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_VERSION 1)
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_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# project settings, use floader name (name in CubeMX) as progect name
string(REGEX MATCH "[^\/]+$" BUFF_FOR_PROJECT_NAME ${CMAKE_CURRENT_SOURCE_DIR})
project(${BUFF_FOR_PROJECT_NAME} C CXX ASM)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_STANDARD 11)
# read Makefile for parsing
file(READ ${CMAKE_SOURCE_DIR}/Makefile MAKEFILE_BUFF)
# parsing for cortex carnel name
string(REGEX MATCH "-mcpu=[^\n]*" MCPU ${MAKEFILE_BUFF})
string(REGEX MATCH "[^=]*$" MCPU ${MCPU})
# Uncomment for hardware floating point
#add_compile_definitions(ARM_MATH_CM4;ARM_MATH_MATRIX_CHECK;ARM_MATH_ROUNDING)
#add_compile_options(-mfloat-abi=hard -mfpu=fpv4-sp-d16)
#add_link_options(-mfloat-abi=hard -mfpu=fpv4-sp-d16)
# Uncomment for software floating point
add_compile_options(-mfloat-abi=soft)
add_compile_options(-mcpu=${MCPU} -mthumb -mthumb-interwork)
add_compile_options(-ffunction-sections -fdata-sections -fno-common -fmessage-length=0)
# Uncomment to mitigate c++17 absolute addresses warnings
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-register")
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
message(STATUS "Maximum optimization for speed")
add_compile_options(-Ofast)
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
message(STATUS "Maximum optimization for speed, debug info included")
add_compile_options(-Ofast -g)
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "MinSizeRel")
message(STATUS "Maximum optimization for size")
add_compile_options(-Os)
else ()
message(STATUS "Minimal optimization, debug info included")
add_compile_options(-Og -g)
endif ()
# parsing for include dir
string(REGEX MATCHALL "-I[^\ \n]*" INCLUDE_DIRS_RAW ${MAKEFILE_BUFF})
foreach(INCLUDE_DIR ${INCLUDE_DIRS_RAW})
string(REPLACE "-I" "${CMAKE_SOURCE_DIR}/" INCLUDE_DIR ${INCLUDE_DIR})
string(REPLACE ";" "\n" INCLUDE_DIR ${INCLUDE_DIR})
list(APPEND INCLUDE_DIRS ${INCLUDE_DIR})
endforeach()
include_directories(
${INCLUDE_DIRS}
# You can add your's dir with heders
)
# parsing for definitions
string(REGEX MATCHALL "-D[^\ \n]*" DEFINITIONS_PARSE ${MAKEFILE_BUFF})
add_definitions(${DEFINITIONS_PARSE})
# parsing for source dir
file(STRINGS ${CMAKE_SOURCE_DIR}/Makefile MAKEFILE_BUFF)
foreach(A ${MAKEFILE_BUFF})
if (A MATCHES "C_SOURCES =[^\n]*")
string(REGEX MATCH " .+$" A ${A})
string(REGEX MATCHALL "[^ ]+" A ${A})
set(SOURCE_FILES_BUF ${A})
endif()
endforeach()
# find startap file
file(GLOB LINKER "startup_*.s")
message(${LINKER})
file(GLOB SOURCES
${SOURCE_FILES_BUF}
${LINKER}
# You can add your's sorce file
)
# find lincker script
file(GLOB LINKER_SCRIPT "STM32*.ld")
# this options for C++
add_link_options(-specs=nosys.specs -lstdc++)
add_link_options(-Wl,-gc-sections,--print-memory-usage,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map)
add_link_options(-mcpu=${MCPU} -mthumb -mthumb-interwork)
add_link_options(-T ${LINKER_SCRIPT})
add_executable(${PROJECT_NAME}.elf ${SOURCES} ${LINKER_SCRIPT})
set(HEX_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.hex)
set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin)
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -Oihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${HEX_FILE}
COMMAND ${CMAKE_OBJCOPY} -Obinary $<TARGET_FILE:${PROJECT_NAME}.elf> ${BIN_FILE}
COMMENT "Building ${HEX_FILE}
Building ${BIN_FILE}")
Тут необходимо указать только soft или hard flot point. Так-же никто вам не мешает добавить ваши флаги компиляции, как и дополнительные папки с кодом. Далее нужно создать tasks.json
, и launch.json
для этого:
mkdir .vscode
cd .vscode
touch launch.json
touch tasks.json
В launch.json
прописываются параметры для расширения cortex debag
, нам же нужно указать только путь для SVD файла с описанием регистров переферии, который можно скачать на сайте st в разделе CAD resources -> System View Description
, а также программатор и контроллер:
{
"version": "0.2.0",
"configurations": [
{
"name": "STM32",
"cwd": "${workspaceRoot}",
"executable": "build/${workspaceFolderBasename}.elf",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"preLaunchTask": "cmake",
// Add you svd file
"svdFile": "${workspaceRoot}/Docs/STM32F3x4.svd",
// Set you programmer and trget controller
"configFiles": [
"interface/stlink.cfg",
"target/stm32f3x.cfg"
],
"swoConfig": {
"enabled": true,
"cpuFrequency": 8000000,
"swoFrequency": 2000000,
"source": "probe",
"decoders": [
{ "type": "console", "label": "ITM", "port": 0 }
]
}
}
]
}
В tasks.json
нужно создать задачу на сборку которая будет исполняться при старте отладки, он указан в launch.json
в "preLaunchTask": "cmake"
:
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "cmake",
"command": "cmake --build .",
"options": {
"cwd": "${workspaceFolder}/build"
},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": {
"base": "$gcc",
"fileLocation": ["relative", "${workspaceFolder}/build"]
}
}
]
}
Это собственно все, теперь можно добавить в main.c
код мигалки диодом и включить отладку (в актуальной версии cortex debag
при вкючении отладки исполнение кода не прирывается если не ставить брейкпоинт):
/* USER CODE BEGIN WHILE */
while (1) {
HAL_GPIO_TogglePin(LD_L_GPIO_Port, LD_L_Pin);
HAL_Delay(100);
/* USER CODE END WHILE */
И если все сделанно правильно диод начнет мигать.
Организация проекта на C
Далее я предлагаю рассмотреть структуру типичного проекта а также как использовать с++ в этом вот всем.
Для начала нужно определится с файловой структурой проекта, я предлогаю к тому что создал куб добавить еще 3 папки:
- App - Бизнес логику нашего приложения
- Bsp - Библиотеки для конкретных устройств распологающихся на вашей конкретной плате.
- Docs - Документация на контроллер, перефирию, микросхемы на плате, а также архитектурные схемы и SVD файл.
В каждой из этих папок (не Docs) создаем 2 папки Src
и Inc
для единообразия с кодом сгенерированным кубом. Папка Bsp
не пригодится т.к. в нашем примере мы будем мигать диодами на уровне HAL
, для примера пойдет и так, но для продовых решений лучше обарачивать подобные веши в функции уровня BSP
.
Создадим приложение бизнес логика которого будет заключится в том то диоды загараются по кругу сначала в одном направлении потом в другом. Для этого создадим 2 файла App/Src/app.c
и App/Inc/app.h
:
mkdir App App/Src App/Inc && touch App/Src/app.c App/Inc/app.h
app.h
должен содержать в себе инклуд main.h
, это нужно чтобы определения наших функций могли содержать дополнительные типы, такие как int8_t
, uint32_t
и подобные. Также в этот файл нужно добавить определение void app(void)
, эта функцию мы будем использовать как main()
:
#ifndef __APP_H__
#define __APP_H__
#include "main.h"
void app();
#endif /* __APP_H__ */
В app.c
реализуем логику работы:
#include "app.h"
#include "gpio.h"
void app()
{
while (1)
{
HAL_GPIO_TogglePin(LD_L_GPIO_Port, LD_L_Pin);
HAL_GPIO_TogglePin(LD_R_GPIO_Port, LD_R_Pin);
HAL_Delay(100);
}
}
Далее необходимо добавить вызов app()
в main.c
и добавить #include "app.h"
, а также удалить код тестовой мигалки из while(1)
, т.к. он все равно никогда не будет исполнен:
/* USER CODE BEGIN Includes */
#include "app.h"
/* USER CODE END Includes */
// ....
/* USER CODE BEGIN WHILE */
app();
while (1) {
/* USER CODE END WHILE */
После этого мы получаем возможность писать код обстрагированный от куба, и больше не нужно следить за тем чтобы писать только между BEGIN
и END
. Далее для того чтобы наш новый код собирался и линтер его корректно понимал, информацию о новых файлах нужно добавить в списоки папок с хедерами и с исходниками внутри cmake, по образцу:
# ....
include_directories(
${INCLUDE_DIRS}
# You can add your's dir with heders
${CMAKE_SOURCE_DIR}/App/Inc
)
# .... тут какой-то код ....
file(GLOB SOURCES
${SOURCE_FILES_BUF}
${LINKER}
# You can add your's sorce file
"App/Src/*.*"
)
# ....
После этого будут загоратся и тухнкть сразу 2 светодиода на плате.
Организация проекта на C++
Необходимо создать те же файлы (прошлые удалить) только вместо .c
будет .cpp
, а вместо .h
.hpp
.
mkdir App App/Src App/Inc && touch App/Src/app.cpp App/Inc/app.hpp
В app.hpp
пихаем также определение функции app()
, только теперь еще необходимо добавить обертку для сишного кода app_c()
:
#ifndef __APP_H__
#define __APP_H__
#include "main.h"
#ifdef __cplusplus
extern "C"
{
#endif
void app_c(void);
#ifdef __cplusplus
}
#endif
void app(void);
#endif /* __APP_H__ */
В файле app.cpp
опишем функцию app()
которая станет нашим новым мейном, а также добавим класс BLINK
для примера. Естественно в нормальном коде данный клас нужно будет вынести в отбельный файл, но для иллюстрации примера пойдет и так.
app.cpp
:
#include "app.hpp"
#include "gpio.h"
extern "C" void app_c(void)
{
app();
}
class BLINK {
public:
void toggle();
};
void app(void)
{
BLINK blink;
while (1)
{
blink.toggle();
HAL_Delay(100);
}
}
void BLINK::toggle()
{
HAL_GPIO_TogglePin(LD_D_GPIO_Port, LD_D_Pin);
HAL_GPIO_TogglePin(LD_U_GPIO_Port, LD_U_Pin);
HAL_GPIO_TogglePin(LD_R_GPIO_Port, LD_R_Pin);
HAL_GPIO_TogglePin(LD_L_GPIO_Port, LD_L_Pin);
}
Также нужно в main.c
поменять функцию с app();
на app_c();
, а также добавить как библиотеку не app.h
а app.hpp
. Тут необходимо заметить что app.hpp
добавляется в main.c
, а значит в нем не может быть никаких class
и других специфичных для С++ слов, в принципе лучше ничего больше не добавлять в этот файл, а дополнительную логику программы реализовывать в других файлах. Более подробно про использование C++ вместе с C можно глянуть в прошлой статье.
Если подводить итог, то мы получаем очень гибкий скрипт позволяющий собирать любые проекты сгенерированные CubeMX
при этом имея возможность с легкостью использовать как С, так и С++.
Полезные ссылки:
- Man по симейку.
- Документация по симейку.
- Документация по Cmake на русском.
- CGold - The Hitchhiker’s Guide to the CMake.
- Статья учебник по регуляркам + интерактивный тестер.
- OpenOCD Quick Reference Card.