🛠️ CMake
В одній із попередніх статей ми порівнювали 3 найпопулярніші IDE, з якими стикаються початківці при програмуванні мікроконтролерів STM32. Ці IDE окрім того, що дозволяють писати код програми в текстовому вікні ще і збирають програму з нашого коду простим натисканням кнопки на панелі інструментів. Під капотом IDE сама знаходить всі хедери і решту файлів, сама задає параметри компіляції і ми отримуємо готовий файл прошивки і навіть одразу можемо завантажити на мікроконтролер. Цього разу давайте познайомимося з тим як в індустрії компілюються і збираються проєкти насправді.
Системи безперервної інтеграції (CI/CD)
В будь-якій ІТ компанії збірка ПЗ виконується не в IDE в кращого програміста місяця на комп'ютері, а в спеціальній системі "безперервної інтеграції", запущеної або десь в хмарі або на серверах самої компанії. Ця система може збирати ("білдити") нові версії ПЗ або вручну або автоматично (коли побачила нові зміни в коді). Додатково вона ще може виконувати багато чого до і після самої збірки, але збірка це її основна задача. Часто ці системи мають веб-інтерфейс для зручної роботи, але під капотом вони всього лиш запускають інші програми, які вже безпосередньо збирають прошивку.
CMake + Ninja🥷
Ці програми — CMake (генератор сценаріїв збірки) та, наприклад, Ninja (власне білдер). Програми на C/C++ спочатку компілюються, а потім отримані файли ще й компонуються лінкером (компонувальник). Якщо ви коли-небудь робили це вручну на проєкті з кількістю файлів більше 10-ти, то бачили як монструозно виглядають команди для компілятора і лінкера (часто просто називають 'toolchain'). Ще й при внесенні змін в один із файлів проєкту доводиться довго очікувати збірку програми з нуля. З цими двома проблемами якраз покликані нам допомогти CMake та Ninja. CMake це інструмент, який дозволяє описати структуру вашого проєкту, а також як і чим його збирати. Адже самих лише файлів коду недостатньо, щоб зрозуміти, що ви хочете отримати в результаті. Погодьтесь, що можна написати просту програму, яка може виконуватися і на Windows і на якомусь мікроконтролері, але збирати їх треба по різному оскільки як мінімум це різні апаратні архітектури. CMake якраз дозволяє описати, яку програму ми хочемо отримати в результаті збірки, з якими параметрами та інструментами її треба збирати. Ninja за цим сценарієм уже буде займатися збіркою. Існує ще багато інших білдерів окрім Ninja, для яких CMake теж вміє створювати інструкції, але Ninja зарекомендував себе як найшвидший, тому я обрав його. Наступна блок-схема показує весь шлях збірки в системах CI/CD:
На перший погляд процес вище виглядає невиправдано ускладненим, але натомість надає гнучкості та автоматизації до процесу збірки. Кожен інструмент на блок-схемі виконує окрему функцію:
- Git вміє автоматично сповіщати CI/CD систему про нові зміни в коді
- CI/CD запускає потужності (віртуальні машини), на яких далі буде відбуватися процес збірки
- CMake генерує файл-сценарій, за яким далі білдер буде виконувати збірку
- Білдер Ninja запускає компілятор і компонувальник за цим сценарієм
Приклад
Перші 2 етапи ми не будемо розглядати в цій статті, але якщо захочете спробувати, то безкоштовний CI/CD GitHub Action надає таку можливість. Розглянемо лише 2 останні етапи, які можна спробувати в себе локально на комп'ютері. Візьмемо для прикладу мікроконтролер STM32G030F6P6 та просту blink програму. Отже, проєкт має 3 основні файли:
- CMakeLists.txt - основний файл CMake
- main.cpp - власне сама програма
- STM32G0_toolchain.cmake - допоміжний файл CMake
- CMakeLists.txt
- main.cpp
- STM32G0_toolchain.cmake
cmake_minimum_required(VERSION 3.15)
set(CMAKE_TOOLCHAIN_FILE "STM32G0_toolchain.cmake")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
project(blink LANGUAGES C CXX ASM)
# Source files
add_executable(${PROJECT_NAME}
src/main.cpp
stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Src/stm32g0xx_hal.c
stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Src/stm32g0xx_hal_gpio.c
stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Src/stm32g0xx_hal_cortex.c
stm32_g0_hal/Drivers/CMSIS/Device/ST/STM32G0xx/Source/Templates/system_stm32g0xx.c
stm32_g0_hal/Drivers/CMSIS/Device/ST/STM32G0xx/Source/Templates/gcc/startup_stm32g030xx.s
)
# Directories with headers
target_include_directories(${PROJECT_NAME} PRIVATE
include
stm32_g0_hal/Drivers/CMSIS/Include
stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Inc
stm32_g0_hal/Drivers/CMSIS/Device/ST/STM32G0xx/Include)
# Linker options
target_link_options(${PROJECT_NAME} PRIVATE
-Wl,--start-group
-Wl,--end-group
-mcpu=cortex-m0
-mthumb
-static)
# Define compile options
target_compile_options(${PROJECT_NAME} PRIVATE
-mcpu=cortex-m0
-mthumb
-Os
-ffunction-sections # Place functions in separate sections
-fdata-sections # Place data in separate sections
-fno-exceptions # Disable exceptions for bare-metal code
)
# Define defines here
target_compile_definitions(${PROJECT_NAME} PRIVATE
STM32G030xx # Define your G0 series MCU
)
# Convert .elf firmware to .bin after build
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME} ${PROJECT_NAME}.bin
)
# Print firmware Flash and RAM usage
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_SIZE} ${PROJECT_NAME}
)
#include "stm32g0xx_hal.h"
extern "C" void SysTick_Handler(void);
void GPIO_PA4_Init();
int main() {
HAL_Init();
GPIO_PA4_Init();
while (1) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);
HAL_Delay(100);
}
return 0;
}
void SysTick_Handler(void) { HAL_IncTick(); }
void GPIO_PA4_Init() {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
# Set the C and C++ standards
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_SYSTEM arm-cortex-m0plus)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR cortex-m0plus)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_EXE_LINKER_FLAGS
"-T ${CMAKE_CURRENT_SOURCE_DIR}/STM32G030F6PX_FLASH.ld -Wl,--gc-sections --specs=nano.specs"
CACHE INTERNAL ""
)
Давайте коротко розглянемо деякі інструкції, використані в CMakeLists.txt файлі вище:
- project - поверхнево описує проєкт - назва проєкту, використані мови програмування
- set - просто записує якесь значення в змінну, яка потім буде використана пізніше
- add_executable - оголошує назву бінарного файлу, який ми хочемо отримати і його c/cpp файлів
- target_include_directories - тут перечислюємо теки з хедерами
- target_link_options - перечислюємо потрібні нам параметри для лінкера
- target_compile_options - перечислюємо параметри для компілятора, наприклад для якої платформи компілювати
- target_compile_definitions - можна задати дефайни прямо тут, які будуть видимі в коді
- add_custom_command - можна викликати будь-яку іншу утиліту на різних етапах збірки
Тестовий проєкт опублікований на GitHub. Структура файлів проєкту має такий вигляд:
blink - рут-тека проєкту
├── include - тека з власними хедерами
│ └── stm32g0xx_hal_conf.h - хедер скопійований з stm32_g0_hal з налаштуваннями МК
├── src - основні сорс-файли тут
│ └── main.cpp - сама блінк програма
├── stm32_g0_hal - CMSIS та HAL драйвера
├── CMakeLists.txt - власне головний файл CMake
├── STM32G030F6PX_FLASH.ld - лінкер скрипт
└── STM32G0_toolchain.cmake - додатковий файл CMake з описом тулчейну
Тека stm32_g0_hal містить CMSIS та HAL, підключена як гіт підмодуль і веде на репозиторій компанії STMicroelectronics. Опис використаного тулчейну винесений в окремий файл STM32G0_toolchain.cmake замість CMakeLists.txt для зручності.
🎬 Відео демонстрація збірки програми мигання світлодіоду
Як видно з відео, можна власноруч зібрати програму і завантажити її на мікроконтролер, не використовуючи графічну IDE. Причому для прикладу я спеціально обрав nvim в якості IDE, щоб продемонструвати як можна розробляти повністю в терміналі: редагувати код, збирати і завантажувати прошивку на МК.
Трохи про Neovim (nvim)
Neovim це форк знаменитого консольного текстового редактора vim, але з потужною системою підтримки плагінів, яких уже дуже багато написано спільнотою. За допомогою цих плагінів простий редактор перетворюється в повноцінну потужну IDE. Для підсвітки синтаксису та навігації по коду використовується той самий принцип як і в VS Code - LSP (Language Server Protocol) - протокол комунікації між IDE та сервером аналізу коду, запущеним як окрема програма. Протокол був розроблений компанією Microsoft спеціально для своєї VS Code, але через дуже вдалу ідею став популярним і за межами VS Code. Поріг входження в розробку на nvim може бути навіть нижчий, ніж в просто vim, адже існують готові збірки плагінів, серед яких популярними є LazyVim та LunarVim, які пропонують багато функцій, до яких розробники звикли з класичних графічних IDE.
🔚 Висновки
Отже, ми розібралися, що існують інструменти, які дозволяють нам збирати прошивку зручніше ніж напряму через тулчейн. Так, вони вимагають певних знань і глибшого розуміння як саме відбувається збірка програми, зате винагороджують гнучкістю і швидкістю. Саме ці інструменти і використовуються білд системами в ІТ компаніях. Насправді ж IDE під капотом теж можуть використовувати такий спосіб збірки, а якщо за замовчуванням цього не роблять, то дозволяють це увімкнути. Наприклад, STM32CubeIDE дозволяє замість звичного проєкту створити CMake проєкт.
Дата публікації
2024.12.11