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を用いた本格的なビルドプロセス記述は今回が初めてだったため、試行錯誤を重ねつつ実装しました。
何か誤解や認識の誤りがあれば、ご指摘いただけると幸いです。
