STM32用のC/C++のコードをCMakeでビルドする話です。

はじめに

前半ではマイコン開発でCMakeが役に立った事例を紹介し、 後半ではTM32 + CubeMX + CMakeでLチカをする事例を紹介します。

後半の例のソースコードはGithubにおいてあります。

Github:idt12312/STM32_CMake

CMakeとは

CMakeとは、クロスプラットホームで動くビルドを自動化するためのツールです。

同じようにビルドを自動化するツールとしてmakeが広く使われていますが、CMakeはもっと抽象度の高いものです。 CMakeはCMake専用の設定ファイルを元にmakeのためのmakfileを出力することができます。 他にもEclipse, Visual Studio, XcodeといったIDEに組み込まれているビルドシステムのための設定ファイル(プロジェクトファイル)を生成することもできます1。 つまり、何を使ってビルドするかということを抽象化できます。 その結果、簡単にクロスプラットホームでのビルドを実現できるのです。

なぜCMakeを使いたいのか

自分がCMakeを使い始めた最初のきっかけはマイクロマウスでのソフトの開発でした。 マイクロマウスの開発においてCMakeは大いに活躍したので、そこでの事例を紹介します。

マイクロマウスの開発では、マイコン(STM32)で動く実機とは切り離しにくいコードと、 PCでも検証可能な探索や制御関連のアルゴリズムのためのコードを分離していました。 開発の段階に応じて、以下の図のように共通の探索アルゴリズム部分を様々な対象とリンクすることで 様々な実行ファイルを生成して開発を行っていました。

以降ではこのような背景の下、自分がCMakeを使おうと思った理由をいくらか挙げます。

makefileを書きたくない

やりたいことはmakeでできないことはないでしょう。ただ、CMakeを使うとより簡単にできることが多いです。 上の図のように色んなtargetをビルドしようとすると、makefileもそれなりに複雑になり書くのが大変です。 私はmakefileをバグなく書く自信がなかったので、CMakeに頼ることにしました。 CMakeを使うと「共通のコードを静的ライブラリとして一つ生成しておき、様々対象とリンクして様々な実行ファイルを作る」ことが簡単にできます。

EclipseなどのIDEは独自のビルドシステムを持っているため、それらを利用するということも考えられます。 しかし、そういったIDEのビルド設定を含むプロジェクトファイルは人間が触ることを想定していないファイル形式になっています。 私はビルド環境をテキストベースで管理し、gitによる差分管理や他の環境に引っ越したときにも簡単にビルドできるようにしたかったので、 IDE組み込みのビルドシステムを使うのはやりたくなかったです。

クロスコンパイルが楽

私のマイクロマウス開発では、アルゴリズムの検証をする際はPC(x86/x64)用の実行ファイルを作り、 実機でマシンを走らせるときはマイコン(ARM Cortex-M4)用の実行ファイルを作る必要がありました。 CMakeではtoolchainファイルというものを使うことで簡単にクロスコンパイルを実現することができます。

どのファイルをビルドするかの設定ファイルと、どのコンパイラを使うかの設定ファイルを分けられる点も個人的に嬉しい点です。 CMakeに渡す設定ファイルを変えるだけで、異なった環境で動くバイナリを生成することができるのです。

Qtのプロジェクトのビルドが楽

Qtのプロジェクトのビルドは一般的にqmakeを使います。qmakeを使わずに直接makefileを書くのは大変です。 CMakeではQtのビルドをサポートしているのでライブラリのリンクやQtのmocの色々をケアする必要がなくなり、 qmakeを使ったときと同じくらい簡単にQtのプロジェクトをビルドをすることができます。 Qtのプロジェクトのビルドだけならqmakeを使えばいいのですが、 Qt用のビルドだけではなく、時にはマイコン用のビルドもするならばCMakeが便利です。

テスト用のビルド・実行も簡単にできる

CMakeにはCTestというtestの実行環境があります。 これを使うと、CMakeの設定ファイルをいじるだけでテストのビルド&実行をいい感じにできます。

私はテストを行うテストライブラリとしてはGooleTestを使っていました。

Github:google/googletest

GoogleTestのドキュメントにはCMakeからGoogleTestのライブラリを自動でダウンロード&ビルドする方法も載っています。 CMakeの機能としてGoogleTestをうまく取り込んでCTestと協調して動かすこともできます。

CMakeでつくるSTM32のビルド環境

いよいよ本題です。 ここではCMakeでのSTM32のビルド環境の構築方法を紹介します。 例題として STM32F4DiscoveryでLチカをするためのコードをビルドすることを目標とします。 おまけとしてこんなこともしています。

  • 他のライブラリを追加する方法
  • C++ファイルもビルドに含める方法
  • CMSIS DSPの静的ライブラリをリンクする方法

今回は以下のような環境を前提としてやっていきます。

  • マイコンボード: STM32F4Dicovery
  • コンパイラ: arm-none-eabi-gcc ver 7.2.1
  • CMake: CMake ver 3.9.6

コンパイラとCMakeのバージョンは違っても多分大丈夫です。

1. CubeMXからコードを生成する

STM32F4DiscoveryでLチカをするためのコードを生成します。 とはいえCubeMXでプロジェクトを作成するときに、BoardとしてSTM32F4Discoveryを選択するだけです。 デフォルトっぽい設定がされます。

その後、Makefileプロジェクトとしてコードを生成します。

コード生成をすると以下のような構造でライブラリやら何やらが生成されます。

.
├── Drivers
│   ├── CMSIS
│   └── STM32F4xx_HAL_Driver
├── Inc
├── Makefile
├── Src
├── startup_stm32f407xx.s
├── stm32_cmake.ioc
└── STM32F407VGTx_FLASH.ld

この時点でmakeを実行するとビルドできることを確認してください。 もしできなかったらコンパイラのインストールなどの環境構築がちゃんとできていない可能性があります (この環境構築については本題とは離れるので省略します)。 この時点でmakeでビルドできるようになっていればひとまずOKです。

2. CMakeのためのファイルを追加する

CMakeのためのファイル(CMakeLists.txt, arm-none-eabi-gcc_toolchain.cmake)と、 例を示すための簡単なソースコードを追加します。追加したものはGithubにもおいてあります。

Github:idt12312/STM32_CMake

以下のようなディレクトリになります。 太字になっているファイルが追加したものです。

.
├── arm-none-eabi-gcc_toolchain.cmake
├── CMakeLists.txt
├── Drivers
│   ├── CMSIS
│   └── STM32F4xx_HAL_Driver
│   ├── CMakeLists.txt
│   ├── Inc
│   └── Src
├── Inc
│   ├── main.h
│   ├── stm32f4xx_hal_conf.h
│   ├── stm32f4xx_it.h
│   └── test_cpp.h
├── Src
│   ├── main.c
│   ├── stm32f4xx_hal_msp.c
│   ├── stm32f4xx_it.c
│   ├── system_stm32f4xx.c
│   └── test_cpp.cpp
├── startup_stm32f407xx.s
├── stm32_cmake.ioc
├── STM32F407VGTx_FLASH.ld
└── TestLibrary
├── CMakeLists.txt
├── test_library.c
└── test_library.h

TestLibraryは新たにライブラリを追加したいときにどうすればいいかの例を示すために追加しました。 同様にC++のファイルを追加する例を示すためにtest_cpp.h/.cppも追加しています。

今回は以下の図のように全体を分割してビルドをしていきます。

基本的にはCMakeLists.txtでどのファイルをコンパイルするを設定し、 ここではarm-none-eabi-gcc_toolchain.cmakeという名前のtoolchainファイルで クロスコンパイルのための設定をします。 toolchainファイルはcmakeの実行時に-DCMAKE_TOOLCHAIN_FILE=arm-none-eabi-gcc_toolchain.cmakeという オプションを加えることでCMakeに取り込まれます。

以降では、上で追加したCMakeのための設定ファイルの解説をします。

最上位にあるCMakeLists.txt

先程の図を実現するコマンド達を書き連ねています。

cmake_minimum_required(VERSION 3.1.0)

project(stm32_cmake)

# to compile startup file
enable_language(ASM)

set(CMAKE_C_FLAGS_DEBUG "-O0 -DDEBUG")
set(CMAKE_C_FLAGS_RELEASE "-O2")

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O2")

add_subdirectory(Drivers/STM32F4xx_HAL_Driver)
add_subdirectory(TestLibrary)

include_directories(
		Inc
		Drivers/CMSIS/Include
		Drivers/CMSIS/Device/ST/STM32F4xx/Include
		Drivers/STM32F4xx_HAL_Driver/Inc
		TestLibrary)

file(GLOB SRCS 
	Src/*.c
	Src/*.cpp
	startup_stm32f407xx.s)

# to link cmsis dsp libray, add a path to the static library
link_directories(${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Lib/GCC)

set(EXECTABLE_NAME ${CMAKE_PROJECT_NAME}.elf)
add_executable(${EXECTABLE_NAME} ${SRCS})

target_link_libraries(${EXECTABLE_NAME}
		hal
		testlibrary
		libarm_cortexM4lf_math.a)

# display the size of the output binary after a build is finished
add_custom_command(TARGET ${EXECTABLE_NAME} POST_BUILD
    COMMAND arm-none-eabi-size --format=berkeley "${EXECTABLE_NAME}")

基本的なことはよくあるCMakeの使い方なので省略します。

以下の点はよくあるCMake入門情報と違っています。

  • アセンブリを有効にしている
  • CMSIS DSPのための静的ライブラリをリンクしている
  • ビルド後にバイナリサイズを表示する設定をしている

アセンブリを有効にするのは、アセンブリで書かれているスタートアップファイルをコンパイルするためです。

CMSIS DSPの静的ライブラリをリンクするために、 その静的ライブラリが配置されている場所をlink_directoriesで指定し、 target_link_librariesでその静的ライブラリの名前を直接追加しています。 target_link_librariesに名前を書くだけではCMakeが静的ライブラリを見つけられないので、 link_directoriesで検索すべき場所を教えてあげます。

CMakeLists.txtの最後の部分では、ビルド後にバイナリのサイズを表示するコマンドを設定しています。 このように書くとビルド後に任意のコマンドが自動実行できるようになるので、 そこでarm-none-eabi-sizeに登録してバイナリサイズを表示するようにしています (ここにtoolchain固有のコマンドを書いてしまうのはかっこ悪くも思えます)。 他にもビルド前に何かを実行するようにしたり、ビルドするターゲットごとに実行するコマンドを変えることもできます。

arm-none-eabi-gcc_toolchain.cmake

このtoolchainファイル2では、クロスコンパイルのために必要なtoolchainの設定をしています。 どのtoolchainを使うかと、マイコン固有のコンパイル・リンクオプションを指定しています。 ちなみにファイル名は任意です。

set(CMAKE_SYSTEM_NAME Generic)

set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_EXE_LINKER arm-none-eabi-g++)

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

unset(CMAKE_C_FLAGS CACHE)
set(CMAKE_C_FLAGS "-mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 \
                    -fsingle-precision-constant -ffunction-sections -fdata-sections -mslow-flash-data \
                    -g3 -Wall -Wextra \
                    -DSTM32F407xx -DUSE_HAL_DRIVER -DARM_MATH_CM4 " CACHE STRING "" FORCE)

unset(CMAKE_AS_FLAGS CACHE)
set(CMAKE_AS_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp" CACHE STRING "" FORCE)

unset(CMAKE_CXX_FLAGS CACHE)
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-use-cxa-atexit -fno-exceptions -fno-rtti" CACHE STRING "" FORCE)

# If you implement systemcall manually, delete "--specs=nosys.specs" option
unset(CMAKE_EXE_LINKER_FLAGS CACHE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -L ${CMAKE_SOURCE_DIR} -T STM32F407VGTx_FLASH.ld \
                        -lc -lm --specs=nosys.specs -Xlinker --gc-sections -Wl,-Map=${CMAKE_PROJECT_NAME}.map" CACHE STRING "" FORCE)

もし違うマイコンを使う場合はtoolchainやコンパイルオプションを適宜変更してください。 CubeMXが生成したmakefileに書いてあるコンパイルオプションを使っておくと大体OKです。 コンパイラやリンクのオプションの話は今回は本質ではないので省略します。

先頭のset(CMAKE_SYSTEM_NAME Generic)はでは、生成されたバイナリの実行環境を設定しています。 今回のようなbaremetalで動かすバイナリのビルドはGenericを設定します3。 もしLinux用のバイナリを作る場合はLinuxを指定します。

次のset(CMAKE_C_COMPILER arm-none-eabi-gcc)ではクロスコンパイルのためにどのツールを使うかを設定しています。

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)はbaremetal用のクロスコンパイラを使うときに必要です。 CMakeは環境スキャン時にコンパイラの機能をテストする動作をします。 クロスコンパイラで生成したバイナリはホストPCで実行できなかったり、 baremetal用のクロスコンパイラは動的ライブラリをうまく扱えないので、 コンパイラのテスト時にエラーが出てしまいます。そのテストを通すためにこの設定をします。

後の部分ではコンパイラやリンカに渡すオプションを設定しています。 unset(CMAKE_C_FLAGS CACHE)CACHE STRING "" FORCEがついているのが奇妙な点です。 CMakelists.txtでコンパイルオプションを指定するときはこのような記述は必要ありません。 toolchainファイルにコンパイルオプションを書くためにしょうがなくつけています。 なぜtoolchainファイルにコンパイルオプションを書きたかったかというと、 マイコンのためのコンパイルオプションはtoolchainとデバイス固有のものなので、 toolchainファイルに分離しておきたかったのです(CMakeのプロはそう考えないのかもしれない)。

toolchainファイルはあくまでtoolchainの指定が目的であるためか、 toolchainファイルにコンパイルオプションを書いてもうまくビルドに反映できませんでした。 そこでCMakeのキャッシュ変数を使ってコンパイルオプションをビルド時に渡しています。 そのためにこういう記述を追加しています4

ライブラリごとに配置するCMakeLists.txt

静的ライブラリとしてまとめられる単位ごとにCMakeLists.txtを配置します。 そこにはその静的ライブラリにまとめるソースファイルを追加していきます。 最上位のCmakelists.txtではadd_subdirectoryコマンドを使ってこのライブラリを登録します。

例えばTestLibraryディレクトリにはこんなファイルをおいています。

file(GLOB SRCS *.c)

add_library(testlibrary STATIC ${SRCS})

こうするとTestLibrary直下の*.cにマッチするファイル名のファイルが、libtestlibrary.aという静的ライブラリの構成ソースファイルとして追加されます。

Drivers/STM32F4xx_HAL_Driverにはこんなファイルを置いています。

# suppress the warning "unused parameter" in HAL Driver
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-parameter")

include_directories(Inc)
include_directories(${CMAKE_SOURCE_DIR}/Inc)
include_directories(${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Include)
include_directories(${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F4xx/Include)

set(SRCS 
	Src/stm32f4xx_hal_tim.c
	Src/stm32f4xx_hal_tim_ex.c
	Src/stm32f4xx_hal_rcc.c
	Src/stm32f4xx_hal_rcc_ex.c
	Src/stm32f4xx_hal_flash.c
	Src/stm32f4xx_hal_flash_ex.c
	Src/stm32f4xx_hal_flash_ramfunc.c
	Src/stm32f4xx_hal_gpio.c
	Src/stm32f4xx_hal_dma_ex.c
	Src/stm32f4xx_hal_dma.c
	Src/stm32f4xx_hal_pwr.c
	Src/stm32f4xx_hal_pwr_ex.c
	Src/stm32f4xx_hal_cortex.c
	Src/stm32f4xx_hal.c)

add_library(hal STATIC ${SRCS})

先程とは違ってファイル名を指定して登録しています。 必要でないファイルの方が多く、ビルドに時間がかかるので追加していません。

これらのファイルをコンパイルするときに別のディレクトリ階層にあるヘッダファイルも必要となるので、 それらのためのinclude pathを設定しています。

他にライブラリを追加するときは

  1. 上記のようにライブラリごとにCMakelists.txtを書く
  2. 最上位のCMakelists.txtでadd_subdirectoryコマンドを使ってライブラリを登録する という作業をするだけで簡単にビルドに含めることができます。

3. ビルドする

ビルド用に、arm-none-eabi-gcc_toolchain.cmakeと同じ階層にbuildという名前のディレクトリを作成し、 その中に移動します。ディレクトリ名はbuildじゃなくてもなんでもいいです。

mkdir build
cd build

そこで以下のコマンドを実行します。

cmake  -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi-gcc_toolchain.cmake ..  

するとCMakeが環境をスキャンし、makefileが生成されます。

うまくmakefileが生成されたら、以下のようにmakeを実行するとビルドが始まります。 CMakeから生成されたデフォルトのmakefileを使うと、 こんな感じで進捗状況を綺麗に表示してくれます。

DebugビルドとReleaseビルドを使い分けるには

最上位にあるCMakeLists.txtではDebugビルドとReleaseビルドでコンパイルオプションが変わるように設定していました。

CMakeを実行するときに、-DCMAKE_BUILD_TYPE=Debugを付けるとDebugビルド用のコンパイルオプションが 適用され、-DCMAKE_BUILD_TYPE=Releaseを付けるとReleaseビルド用のコンパイルオプションが適用されます。

makeが実行したコマンドを見たい

ビルドにおいて何か問題が起こったときに、CMakeの問題なのか、makeの問題なのか、コンパイラの問題なのかを切り分ける必要がある ときがあるかも知れません。 CMakeで生成したmakefileを使うと先のスクリーンショットのようにmakeが実行したコマンドの詳細は隠されてしまい、 makeがどういうコマンドを実行したのかがわからなくなってしまいます。

こういうときには、以下のようにmakeを実行すると、綺麗な表示がなくなる代わりに、 makeが実行したコマンドが全て表示されます。

make VERBOSE=1 

おわりに

STM32のプログラムをコマンドラインでコンパイルするだけなら、CubeMXが生成するmakefileを使うだけで十分でしょう。 ただ、この記事の前半に挙げたマイクロマウスの例のように、 複数環境で複数のターゲットをビルドしようとするとCMakeを使う意味が出てくると思いました。

最近qiitaに組み込み用のCMake記事があることを発見しました。 こちらはもっと詳しく書いてあるので、非常に参考になると思います。

Qiita: たのしい組み込みCMake

参考

  1. https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html 

  2. https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html 

  3. https://github.com/Kitware/CMake/blob/master/Modules/Platform/Generic.cmake 

  4. https://stackoverflow.com/questions/11423313/cmake-cross-compiling-c-flags-from-toolchain-file-ignored