Raspberry Pi Pico 向け TOPPERS/FMP3 を CMake でビルドできるようにしてみた
VSCode で開発を行っていると、組み込み開発でも CMake を使ったプロジェクトをよく目にするようになりました。
VSCode の拡張機能には Renesas Build Utilities や STM32 VS Code Extension があり、今回の Raspberry Pi Pico でも CMake が採用されています。
今回は、 Raspberry Pi Pico 向け TOPPERS/FMP3 を CMake でビルドできるようにしたので、元の Makefile がどのような流れでビルドを行っているかの説明と、CMake でどのように実装したかを紹介したいと思います。
https://toppers.jp/fmp3-e-download.html#raspi_pico
今回作成したものは以下のリポジトリに公開しています。ターゲット非依存部のバージョンを「3.3.3」に更新しました。
https://github.com/h7ga40/fmp3_raspberrypi_pico_cmake
付属の Makefile のビルド手順
構成
まず、Makefile を用いたビルド手順を確認します。ビルドの前に次の手順を行い、ターゲットに応じた Makefile を生成します。それを用いてプロジェクトをビルドします。以下では、作業フォルダとしてbuildを使用します。
「Raspberry Pi Pico 簡易パッケージ」の「fmp3_raspberrypi_pico_gcc-20211016.tar.gz」をダウンロードして展開してできたfmp3
フォルダの中で下記のコマンドを実行します。
mkdir build
cd build
ruby ../configure.rb -T raspberrypi_pico_gcc -w -S "syslog.o banner.o serial.o logtask.o chip_serial.o"
これにより、実行したフォルダに、FMP3 の使用例である sample1 をビルドする Makefile が生成されます。続いてmake
コマンドでビルドします。これが付属のビルド方法です。
FMP3 のビルドプロセスには、静的 API を処理するための前処理が含まれています。また、組み込みソフトウェア特有の、ROM に書き込むためのファイル出力が後処理として実行されます。
ビルド前処理
FMP3 は主に C 言語で記述されていますが、一部の低レベル処理(割り込み処理やディスパッチャ)はアセンブラで実装されています。
このようなケースでは、アセンブラからも C 言語で定義された構造体のメンバオフセットを利用する必要があります。しかし、アセンブラから直接 C 言語の構造体を参照することはできないため、必要な値を得るためのコードを C 言語で出力し、それをビルドして実行ファイルのバイナリダンプから値を取得する手順を取っています。
また、アプリケーションで使用するカーネルオブジェクトなどを*.cfg
ファイルに静的 API で定義します。この*.cfg
ファイルから情報を読み取って、C 言語で出力する工程があります。
この処理は、cfg.rb
という Ruby スクリプトを用いて実行されます。
はじめの段階pass 1
では、アプリケーションの*.cfg
ファイルを起点にターゲットやカーネルの定義を読み取り、カーネルのビルドに必要な情報を取得します。その情報を元にcfg1_out.c
を出力、ビルドし、実行ファイルから、シンボル情報出力とバイナリダンプをします。
続くpass 2
では、アセンブラで使用する値をoffset.h
に出力し、アプリケーションで使用するカーネルオブジェクトなどの情報をkernel_cfg.c
およびkernel_cfg.h
に出力します。
ビルド
FMP3 のカーネルとターゲット依存部のソースコードをビルドしてlibkernel.a
を生成します。このlibkernel.a
にはアプリケーション固有の情報は含まれていないので、別のアプリケーションにも使用することができます。
次に、アプリケーションをビルドします。FMP3にはアプリケーションの例としてsample1
が付属していますのでこれを使います。sample1
はシリアルドライバとログ出力処理を使用していますので、それぞれのフォルダsample1
やsyssvc
、library
の配下のソースファイルをビルドし、libkernel.a
とリンクすることで、実行バイナリが生成されます。
ビルド後処理
ビルドで出力される実行ファイルを、ターゲットに書き込むためのバイナリデータにします。
また、FMP3 ではpass 3
として収集した情報を元に、正しくメモリに配置されているかをチェックします。
CMake で実装してみる
ファイル構成
次に、CMake を用いたビルド手順について説明します。CMake では、Makefile と同様にターゲット依存部を起点としてビルド情報を収集する仕組みを取り入れています。
ソースコードやコンパイルオプションなどのビルド情報は、ターゲット依存部を参照することで、そこからアーキテクチャが参照され、ツールチェインが参照され、カーネルをビルドする情報をすべて得る仕組みとなっています。
CMake 版では、下記のコマンド手順でビルドすることを目標にしました。メインのCMakeLists.txt
をルートに置く、ファイル構成になっています。
mkdir build
cd build
cmake -G Ninja ..
cmake --build .
CMake では、まずcmake -G Ninja ..
で構成を行います。..
の部分は、ルートのCMakeLists.txt
が置いてあるパスになるので、フォルダ構成を変えた場合はこのパスを変更します。
ターゲット依存部のフォルダ名は下記のように指定しています。FMP3_TARGET
変数を使ってターゲットフォルダのパスを編集し、そのフォルダにあるtarget.cmake
を参照します。
set(FMP3_TARGET raspberrypi_pico_gcc)
CMake のファイル参照方法にはinclude
とadd_subdirectory
があります。このうち、ビルド情報を集める目的でinclude
を使用しています。
一方、カーネルは別途CMakeLists.txt
を用意し、アプリケーションからadd_subdirectory
で読み込む構成にしました。
この方法により、カーネルをライブラリとして独立させ、コード変更がなければアプリケーション部分のみをビルドできるようにしています。また、複数のアプリケーションでカーネルライブラリを共有することも可能になります。
プロジェクトを宣言
FMP3 のフォルダのCMakeLists.txt
には、下記のようにproject
でfmp3
というプロジェクトであること、add_library
でスタティックライブラリを出力するとことを宣言し、ビルドするソースコードを指定します。
FMP3_TARGET
変数で指定された、ターゲット依存部のフォルダにあるtarget.cmake
をinclude
で読み出して、ターゲットの情報を取得します。
project(fmp3 C ASM)
include(${PROJECT_SOURCE_DIR}/target/${FMP3_TARGET}/target.cmake)
add_library(fmp3 STATIC
:
kernel/task.c
:
)
ルートフォルダのCMakeLists.txt
には、下記のようにproject
でsample1
というプロジェクトであること、add_executable
で実行ファイルを出力するとことを宣言し、ビルドするソースコードを指定します。
また、add_subdirectory
でfmp3
フォルダの FMP3 プロジェクトをビルドして、target_link_libraries
でsample1
プロジェクトにfmp3
ライブラリをリンクすることを指定します。
project(sample1 C)
add_executable(sample1
${FMP3_ROOT_DIR}/sample/sample1.c
)
add_subdirectory(fmp3)
target_link_libraries(sample1 fmp3)
include(${FMP3_ROOT_DIR}/library/library.cmake)
include(${FMP3_ROOT_DIR}/syssvc/syssvc.cmake)
CMakeによるビルド前処理の定義
CMake を使用してビルド前処理を実装する方法について説明します。
FMP3 ではpass 1
とpass 2
のビルド前処理がありますが、これらは Ruby スクリプトを通じて実行されます。
コンパイルやリンク以外のコマンドを書き出すためにはadd_custom_command
を使用します。
pass 1
の記述内容は下記です。
add_custom_command(
OUTPUT cfg1_out.c
WORKING_DIRECTORY ${CFG1_OUT_DIR}
COMMAND ${CMAKE_COMMAND} -E echo "Generating cfg1_out.c"
COMMAND ruby ${PROJECT_SOURCE_DIR}/cfg/cfg.rb --pass 1 --kernel fmp ${CFG_INCLUDE_DIRS} ${CFG_API_TABLES} ${CFG_SYMVAL_TABLES} -M ${CFG1_OUT_DIR}/cfg1_out_c.d ${FMP3_CFG_FILES}
DEPENDS ${PROJECT_SOURCE_DIR}/cfg/cfg.rb ${FMP3_SYMVAL_TABLES} ${CFG_API_TABLE} ${FMP3_CFG_FILES}
COMMENT "Running cfg pass 1 to generate cfg1_out.c"
)
-
OUTPUT
は、カスタムコマンドで出力されるファイルを指定します -
WORKING_DIRECTORY
は、コマンドが実行されるディレクトリを指定します -
COMMAND
は、実行するコマンドを順番に記述します -
CMAKE_COMMAND
は、cmake
コマンド自身を指し-E echo
はメッセージを表示する指定です -
DEPENDS
は、出力ファイルを生成するために必要な依存ファイルを指定します -
COMMENT
は、コマンド実行時に表示されるメッセージを指定します
COMMAND
で${CFG_INCLUDE_DIRS}
など変数を使って複数のコマンド引数を指定できますが、変数が文字列になっていると一つのコマンド引数として扱われるので注意が必要です。
このようにしてpass 1
の処理を CMake に組み込むことができます。他のビルド処理やpass 2
についても同様の方法で定義を行います。
次に、pass 1
で出力したcfg1_out.c
をビルドして実行ファイルcfg1_out
を出力する手順を行います。これはアプリケーションの実行ファイルと同じように記述します。
add_executable(cfg1_out cfg1_out.c)
第一引数がプロジェクト名ではないので、プロジェクトの最終生成物とは扱われないようです。たぶん。
ビルドしたcfg1_out
から情報を取り出すために、nm
やobjdump
を実行しますが、これはcfg.rb
の実行と同様にadd_custom_command
を使用します。
CMakeによるビルド工程の定義
ビルドに必要な情報は、以下の関数を使用して指定します。
-
target_include_directories
:インクルードファイルの検索パスを指定します -
target_compile_definitions
:プリプロセッサシンボルの定義を指定します -
target_compile_options
:コンパイルオプションを指定します -
target_link_options
:リンクオプションを指定します
FMP3プロジェクトでは、以下のように記述されています。
target_include_directories(fmp3
PUBLIC ${FMP3_INCLUDE_DIRS}
PUBLIC ${FMP3_APP_INCLUDE_DIRS}
PUBLIC ${KERNEL_CFG_DIR}
PRIVATE ${PROJECT_SOURCE_DIR}/kernel
)
target_compile_definitions(fmp3
PRIVATE ALLFUNC
PUBLIC ${FMP3_COMPILE_DEFS}
)
target_xxx
の第一引数で対象となるプロジェクトを指定します。
PUBLIC
が付けられた項目は、add_subdirectory
で取り込まれたプロジェクト(例: sample1
)にも適用されます。
一方、PRIVATE
が付けられた項目は、他のプロジェクトには影響を与えません。
CMakeによるビルド後処理の定義
pass 3
は、アプリケーションのビルド後に実行する処理ですが、add_subdirectory
で取り込まれたプロジェクトから、元のアプリケーションのビルドプロセスを定義することはできません。
そこで、FMP3のCMakeLists.txt
に関数を定義し、アプリケーション側からその関数を呼び出すことで、ビルド後処理を実行するようにしました。
以下は、fmp3_cfg_check
関数の定義例です。
set(FMP3_PASS3_ARGS ${PASS3_ARGS} PARENT_SCOPE)
set(FMP3_KERNEL_CFG_DIR ${KERNEL_CFG_DIR} PARENT_SCOPE)
function(fmp3_cfg_check TARGET)
add_custom_command(TARGET ${TARGET} POST_BUILD
WORKING_DIRECTORY ${FMP3_KERNEL_CFG_DIR}
COMMAND ${CMAKE_COMMAND} -E echo "Running cfg pass 3 to check configuration"
COMMAND ruby ${FMP3_PASS3_ARGS} --rom-symbol ${TARGET}.syms --rom-image ${TARGET}.srec
DEPENDS ${TARGET}.syms ${TARGET}.srec
COMMENT "Running cfg pass 3 to check configuration"
)
endfunction()
-
function
の引数TARGET
は、呼び出し元のプロジェクト名を受け取ります。例:fmp3_cfg_check(sample1)
-
add_custom_command
のTARGET ${TARGET} POST_BUILD
は、プロジェクト${TARGET}
のビルド後に実行する処理を指定します。
TARGET
指定はOUTPUT
指定と排他利用となります。
この設計には工夫が必要でした。
特に、この関数はアプリケーションのスコープで実行されるため、FMP3側で定義された変数が利用できません。
そのため、set
コマンドにPARENT_SCOPE
オプションを指定して変数を定義することで、アプリケーション側からもこれらの変数を参照できるようにしています。
注意点
コマンドの記述
COMMAND
でコマンドを記述する際は、以下の点に注意してください。
コマンド引数はスペースで区切られますが、変数が文字列の場合、一体化して引数として渡されてしまいます。
以下はエラーが発生する例です。
set(OPTION "script.rb --version")
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND ruby ${OPTION}
)
この場合、OPTION
変数が1つの引数として扱われるため、RubyがNo such file or directory
エラーを出力します。
特にリスト形式の変数を加工して文字列化した場合、こうした問題が発生しやすいです。
構成とビルド
CMakeは構成時にすべてのビルドコマンドを記述します。
add_custom_command
はコマンドの定義を行うものであり、構成時に実行されるわけではありません。
構成時にコマンドを実行したい場合は、execute_process
を使用してください。
最後に
CMakeを用いた本格的なビルドプロセス記述は今回が初めてだったため、試行錯誤を重ねつつ実装しました。
何か誤解や認識の誤りがあれば、ご指摘いただけると幸いです。