Jak skonfigurować cmake na mikrokontrolery?

Konfiguracja CMake - wszystkie wpisy

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.

Konfiguracja CMake - Nawigacja

6 Comments

  1. 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!

  2. 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

  3. Skrypt .ld jest OK brakuje tylko tego syscalls.c

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *