Portfolio Обо мне Блог

Сегодня мы рассмотрим как сделать систему сборки на основе с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:

  1. Выбираем плату STM32F3348-DISCO.
  2. Соглашаемся на дефолтную пред настройку.
  3. Далее в Project manager -> Project задаем название проекта и выбираем генерировать makefile.
  4. В Project manager -> Code Generator выбираем только необходимые файлы и раздельную генерацию .c и .h файлов для разной периферии.
  5. Генерируем проект.

В итоге получаем минимально рабочий проект с 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 при этом имея возможность с легкостью использовать как С, так и С++.

Полезные ссылки: