Cmake jest fajną alternatywą dla pisania własnych skryptów makefile, czy korzystania z wyklikanej konfiguracji projektu w naszym IDE. Jednak początkowo może być trudno zmusić go do działania z mikrokontrolerami. Dlatego w tym artykule pokażę jak stworzyć plik konfiguracyjny dla naszego toolchaina umożliwiający budowanie projektów na STM32.
Do czego służy cmake?
Cmake to narzędzie służące do budowania projektów. W skryptach cmake umieszczamy informacje o plikach źródłowych, ścieżkach includów, flagach kompilacji, definicjach preprocesora itp. Z tych skryptów cmake potrafi wygenerować na przykład skrypty makefile, czy projekty do IDE takich jak Visual Studio, czy Eclipse.
Główną zaletą cmake jest niezależność od platformy. Ten sam skrypt cmake powinien wygenerować poprawne makefile na Windowsie, Linuxie i MacOSie. Używając cmake nie musimy też się martwić, czy nasz projekt wspiera kompilację inkrementalną. Czasami makefile pisane ręcznie zawsze kompilują cały program, albo rozpatrują tylko proste zależności od plików .c
. Używając cmake mamy kompilację inkrementalną sprawdzającą również używane headery, czy zmianę flag kompilacji w skrypcie. Dodatkowo dostajemy bajery takie jak np. progres kompilacji w procentach drukowany na konsolę.
Jednak żeby używać cmake na mikrokontrolerach musmy jeszcze skonfigurować odpowiedni toolchain. Inaczej cmake wygeneruje nam skrypty do kompilacji dla zwykłego gcc na x86.
Konfiguracja cross kompilacji
Aby cmake nie korzystał z natywnego kompilatora, musimy podać mu odpowiedni plik z konfiguracją toolchaina. Możemy to zrobić z linii komend:
cmake .. -DCMAKE_TOOLCHAIN_FILE=../Toolchain-arm-gcc-cmake
Albo za pomocą GUI:
Sam plik toolchain dla naszego procesora możemy łatwo znaleźć w internecie:
Możemy również napisać swój od zera. Może to nam się przydać także kiedy będziemy korzystać z mniej popularnego kompilatora. Instrukcje na ten temat znajdziesz w dokumentacji cmake.
Minimalny plik toolchain dla arm-none-eabi-gcc
wygląda tak:
# System Generic - no OS bare-metal application set(CMAKE_SYSTEM_NAME Generic) # Setup arm processor and gcc toolchain set(CMAKE_SYSTEM_PROCESSOR arm) 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(CMAKE_NM arm-none-eabi-nm) set(CMAKE_STRIP arm-none-eabi-strip) set(CMAKE_RANLIB arm-none-eabi-ranlib) # When trying to link cross compiled test program, error occurs, so setting test compilation to static library set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
W podanym wyżej kodzie najpierw informujemy cmake, że to aplikacja bare metal. Następnie ustawiamy nazwy dla poszczególnych narzędzi z toolchaina takich jak kompilator c, c++, archiver, objcopy itd.
Bardzo ważna jest ostatnia linijka, bez której konfiguracja nam się nie powiedzie:
The C compiler "C:/ARM/arm-none-eabi-gcc-8.2.0-180726/bin/arm-none-eabi-gcc.exe" is not able to compile a simple test program. It fails with the following output: ...
Cmake podczas konfiguracji kompiluje sobie przykładowy program w C i C++, żeby sprawdzić, czy podany toolchain w ogóle działa. Jednak cross kompilacja tego testowego programu nie może się powieźć. Zamiast tego musimy skompilować go jako bibliotekę statyczną, co umożliwia dodana komenda.
Kiedy konfiguracja się powiedzie, możemy wykonać kompilację używając komendy make
albo ninja
w zależności od wybranej przez nas opcji.
ninja -v all [1/7] C:\ARM\arm-none-eabi-gcc-8.2.0-180726\bin\arm-none-eabi-gcc.exe -DSTM32F40_41xxx -I../code -I../hw -I../utils -I../external/stm32 -I../external/cmsis -x assembler-with-cpp -mcpu=cortex-m4 -mthumb -g -mfloat-abi=hard -mfpu=fpv4-sp-d16 -ffast-math -Wall -Wextra -MD -MT CMakeFiles/template_stm32f4.dir/hw/startup/startup.S.obj -MF CMakeFiles\template_stm32f4.dir\hw\startup\startup.S.obj.d -o CMakeFiles/template_stm32f4.dir/hw/startup/startup.S.obj -c ../hw/startup/startup.S [2/7] C:\ARM\arm-none-eabi-gcc-8.2.0-180726\bin\arm-none-eabi-gcc.exe -DSTM32F40_41xxx -I../code -I../hw -I../utils -I../external/stm32 -I../external/cmsis -mcpu=cortex-m4 -mthumb -g -mfloat-abi=hard -mfpu=fpv4-sp-d16 -ffast-math -std=gnu89 -O0 -ffunction-sections -fdata-sections -fverbose-asm -MMD -Wall -Wextra -Wstrict-prototypes -MD -MT CMakeFiles/template_stm32f4.dir/main.c.obj -MF CMakeFiles\template_stm32f4.dir\main.c.obj.d -o CMakeFiles/template_stm32f4.dir/main.c.obj -c ../main.c
Teraz kompilacja się kończy sukcesem i zostaje utworzony plik .elf
. Ale co jeżeli chcemy mieć na przykład hexa? Musimy wtedy dodać do cmake własne komendy.
Tworzenie własnych komend w cmake
W skrypcie budowania chcielibyśmy mieć opcje tworzenia plików .bin
, .hex
, .elf
. Do tego przydałby się też listing asemblerowy. Aby je uzyskać, potrzebujemy utworzyć własne komendy cmake wywołujące pod spodem odpowiednie narzędzia toolchaina z odpowiednimi flagami.
Komenda tworząca plik hex wygląda tak:
add_custom_command( OUTPUT ${hex_file} COMMAND ${CMAKE_OBJCOPY} -O ihex ${elf_file} ${hex_file} DEPENDS ${elf_file} )
Definiujemy w niej nazwę pliku, który zostaje utworzony w sekcji OUTPUT
. Sekcja COMMAND
zawiera komendę konsolową służącą do utworzenia tego pliku. Z kolei sekcja DEPENDS
określa pliki, które muszą istnieć wcześniej. Zmienna ${CMAKE_OBJCOPY}
została przez nas zdefiniowana w poprzednim akapicie i zawiera odpowiednią komendę z naszego toolchaina. Więcej o tworzeniu własnych komend w dokumentacji cmake.
Z kolei zmienne ${hex_file}
i ${elf_file}
definiujemy sobie samodzielnie jako nazwy odpowiednich plików.
Podobne komendy musimy utworzyć dla wszystkich innych plików, które chcemy generować podczas kompilacji. Możemy wszystkie te komendy zawrzeć w macrze add_arm_executable
:
macro(add_arm_executable target_name) # Output files set(elf_file ${target_name}.elf) set(map_file ${target_name}.map) set(hex_file ${target_name}.hex) set(bin_file ${target_name}.bin) set(lss_file ${target_name}.lss) set(dmp_file ${target_name}.dmp) add_executable(${elf_file} ${ARGN}) #generate hex file add_custom_command( OUTPUT ${hex_file} COMMAND ${CMAKE_OBJCOPY} -O ihex ${elf_file} ${hex_file} DEPENDS ${elf_file} ) # #generate bin file add_custom_command( OUTPUT ${bin_file} COMMAND ${CMAKE_OBJCOPY} -O binary ${elf_file} ${bin_file} DEPENDS ${elf_file} ) # #generate extended listing add_custom_command( OUTPUT ${lss_file} COMMAND ${CMAKE_OBJDUMP} -h -S ${elf_file} > ${lss_file} DEPENDS ${elf_file} ) # #generate memory dump add_custom_command( OUTPUT ${dmp_file} COMMAND ${CMAKE_OBJDUMP} -x --syms ${elf_file} > ${dmp_file} DEPENDS ${elf_file} ) #postprocessing from elf file - generate hex bin etc. add_custom_target( ${CMAKE_PROJECT_NAME} ALL DEPENDS ${hex_file} ${bin_file} ${lss_file} ${dmp_file} ) set_target_properties( ${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${elf_file} ) endmacro(add_arm_executable)
W tym macrze dodatkowo definiujemy nowy target generujący wszystkie pliki wynikowe kompilacji, których potrzebujemy:
add_custom_target( ${CMAKE_PROJECT_NAME} ALL DEPENDS ${hex_file} ${bin_file} ${lss_file} ${dmp_file} )
I konfigurujemy plik .elf
jako główny plik wynikowy kompilacji:
set_target_properties( ${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${elf_file} )
I w ten sposób udało nam się utworzyć plik toolchain dla GCC na ARM i własną komendę add_arm_executable
tworzącą pliki wynikowe w różnych formatach. Plik toolchain zawierający efekt znajdziecie tutaj.
Co dodać w głównym pliku cmake?
Mając ten plik możemy dodać go w pliku CMakeLists.txt
:
set(CMAKE_TOOLCHAIN_FILE Toolchain-arm-gcc.cmake)
Możemy go też podać jako argument w linii komend dla cmake albo w GUI. Naszą dodatkową komendę możemy wywołać w taki sposób:
add_arm_executable(${CMAKE_PROJECT_NAME} ${CPP_SRCS} ${C_SRCS} ${ASM_SRCS})
Podsumowanie
W tym artykule pokazałem jak zrobić minimalistyczny plik do obsługi toolchaina w cmake. Inne pliki tego typu, które znajdziecie w internecie mogą być dużo bardziej rozbudowane. Mogą na przykład wyszukiwać zainstalowanych toolchainów, czy wykonywać jakieś komendy warunkowo. Musimy tylko uważać, bo takie skrypty często bazują np. na komendach linuksowych psując w ten sposób portowalność. Możecie zobaczyć ten artykuł z konfiguracją cmake na AVR, gdzie autor tworzy również komendy do flashowania pod linuxem.
Jeżeli mamy do czynienia z innym kompilatorem będziemy pewnie musieli napisać inne komendy do tworzenia plików wynikowych. Poza tym dla każdego kompilatora są inne flagi kompilacji. Moim zdaniem najlepiej jest obsługiwać te flagi w osobnym pliku cmake zawierającym flagi dla danego projektu. Jednak często możecie natknąć się na flagi również w pliku toolchaina. Tym bardziej, że bez flagi określającej rodzinę procesorów często kompilacja się nie powiedzie.
No i to już wszystko, mam nadzieję, że ten artykuł ułatwi Ci korzystanie z cmake w swoich projektach. Tym bardziej, że cmake ma wiele innych ciekawych opcji, o których możesz poczytać na przykład w tym artykule.
14 grudnia 2020 at 14:05
Dobry artykuł, tylko brakuje mi większej ilości szczegółów. Na przykład informacji gdzie dodać ścieżki plików źródłowych oraz ściezki do nagłówków.
Pozdrawiam!
14 grudnia 2020 at 15:28
Świetny artykuł! Fajnie że takie tematy publikujesz po polsku – brakuje tego w naszej blogosferze. Sam kiedyś dużo męczyłem się z cross-kompilacją w CMake’u dopóki nie odkryłem toolchain file’ów i nie zacząłem tworzyć projekty, które są niezależnie zupełnie od targetu a procesor wybierałem tylko toolchain file’em.
Przy okazji dzięki za wzmiankę o moim artykule. Dla dociekliwych w temacie CMake’a dołączam link do swoich postów o CMake’u.
Powodzenia i czekamy na więcej!
14 grudnia 2020 at 16:40
Cześć Kuba!
Twoje artykuły o cmake to prawdziwe złoto. Z tego co widzę masz wpis na ten sam temat, tylko bardziej szczegółowo:
https://kubasejdak.com/how-to-cross-compile-for-embedded-with-cmake-like-a-champ
I masz jeszcze artykuły o includach PRIVATE i PUBLIC, czy o integracji cmake z gitem do ściągania zależności. Bardzo mi pomogą bo to są dwie rzeczy z cmake, które od dawna chciałem sprawdzić.
https://kubasejdak.com/modern-cmake-is-like-inheritance
https://kubasejdak.com/how-to-join-repositories-in-cmake
14 grudnia 2020 at 16:16
Faktycznie, brakuje tych informacji. W takim razie zrobię całą serię o cmake. Tutaj się skupiłem tylko i wyłącznie na pliku do konfiguracji toolchaina, a include i pliki źródłowe dodajesz w pliku CMakeLists.txt
21 grudnia 2021 at 18:03
W konfiguracji CMakeList z github-a widzę ze nie dodajesz pliku syscalls.c ze swojego przykładu. NIe wiedze też podpiecia pliku .ld i wygenerowany hex zaczyna się od adresu 0x00000000
21 grudnia 2021 at 18:12
Skrypt .ld jest OK brakuje tylko tego syscalls.c