mbed-cliプロジェクトをVSCodeで快適に開発したい
はじめに
VSCodeいいですよね。仕事でも家でも95%以上の時間画面にVSCodeが表示されているような気がします。
VSCodeの一番の良いところは、やっぱりIntelliSenseじゃないでしょうか?逆にIntelliSenseがないのなら、VSCodeである必要はないというか・・・・
家では今のところmbed-cliで開発している(個人的にはあまり好きじゃない・・・)のですが、便利スクリプトの、mbed compile
でビルドできちゃうので、そんなに難しい設定もせずに、なんとなくVSCodeでも開発できてしまいます。
ところが、対応しているターゲットのソースがすべて(ホントのところすべてかどうかは知りません)プロジェクトに存在しているため、IntelliSenseさんの機嫌が悪いと、赤波線の嵐に見舞われることが多々あります。
そして、良くある対応の、IntelliSense EngineをTag Parserに変えてしまうと・・・・・ほとんどIntelliSenseが効かない、残念なVSCodeになってしまいます。
この頃、やっとC/C++拡張の気持ちがわかってきたような気がするので、備忘録として書き残しておきます。
前提とする環境
ターゲット | nucleo L476RG |
mbed-cliバージョン | 5.15.2 |
Visual Studio Codeバージョン | 1.74.3 |
C/C++拡張機能バージョン | 1.14.1 |
compilerPathなどの、基本的な設定は済ませてある前提です。
問題点
デフォルトのc_cpp_properties.jsonでmbedのプロジェクトを開くと、こうなります。
I2C
が赤波線なのとD14
,D15
が赤波線なのは、理由が少し違います。
なぜこうなる?
I2CやSPIなどのmbed-osが提供する便利クラスが見えない
マクロで無効化されている
I2C
でF12
を押して、定義に飛んでみると・・・・・
class I2C
は、無効化されていることがわかります。
無効化しているのは、ここですね。
DEVICE_I2C
に!0
な値が定義されているか、DOXYGEN_ONLY
が定義されていないと、このクラスは有効かされません。
mbed compile
するときは、コンパイラにオプションで渡しているのでしょう。
同じように、SPI
も赤波線です。
マウスをホバーしてみると・・・・
定義されていない。
こちらも、F12
で飛んでみると、
やっぱり、無効化されています。
無効化している部分も、やっぱり同じで、
この部分です。
IntelliSenseにマクロを教えてあげれば解決
mbedのビルドスクリプトがうまいことやってくれているおかげで、この辺の条件コンパイルが成立しているようなので、IntelliSenseにもこれらの情報を教えてあげれば良さそうです。
c_cpp_properties.json
に、defines
というエレメントがあって、これがコンパイラに渡される事前定義マクロの設定になっているので、ここに追加していきます。
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"DEVICE_I2C",
],
"DEVICE_I2C"
を追加して、先ほどのI2C
のとこに戻ってみると、
赤波線が消えて、クラスのドキュメントまで出てくるではありませんか!
同じように、"DEVICE_SPI"
を追加すると、
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"DEVICE_I2C",
"DEVICE_SPI"
],
各デバイスごとに、どんなマクロを定義すれば良いかは、今のところ都度調べるしかありませんが、F12
で定義に飛んで調べれば、簡単に見つけることができると思います。
PA?などのGPIOピンのエイリアスが見えない
原因
もうひとつ問題点がありました。先ほどのI2C
の部分では、便利クラスI2C
は無事見えるようになりましたが、コンストラクタに渡しているGPIOピンのエイリアスがまだ赤波線です。
とりあえず、定義に飛んでみようと、F12
を押してみると・・・・・・
なんだか、PinNames.hに定義されているっぽいのですが、同じ名前のファイルが大量にあるようです。
恐らく、同じ名前で違う定義が大量にあるために、IntelliSenseとしては、どれかわからんぞゴルァとでも言いたいのでしょう。
IntelliSenseが切れ気味にサジェスチョンしているPinNames.h
は、mbed-os\targets\TARGET_Cypress\TARGET_PSOC6_FUTURE\TARGET_FUTURE_SEQUANA\PinNames.h
と、まったく無関係のターゲットのものになっています。
ここも、mbed
のビルドスクリプトが、適切なインクルードパスをコンパイラに引数で渡しているのでしょう。原因がわかれば、対策もできるということで、このmbed-os/targets
以下がどうなっているのかを調べてみます。
ちょっとでかいですが、エクスプローラーでTARGET_NUCLEO_L476RG
までを展開して表示したのが、こちらです。
これだとわかりにくいので、書き直してみると、
targets -- TARGET_<CPUMaker>
+- TARGET_STM -- メーカ固有のソースファイル
-- TARGET_<CPUSeries>
+- TARGET_STM32L4 -- CPUSeries固有のソースファイル
+- device(HALのソースファイル)
+- TARGET_<CPUType>
+- TARGET_STM32L476xG -- CPUType固有のソースファイル
+- device(処理系依存のソースファイル)
+- TARGET_<Board>
+- TARGET_NUCLEO_L476RG -- ボード固有のソースファイル
という階層構造でもってDRY原則をなるべく守って実装されているようです。個人的に、こういうロジカルな構造は大好きです。
ちなみに、c_cpp_properties.json
のincludePath
での指定方法は二つあります
* | 指定ディレクトリだけをinclude指定する |
** | 指定ディレクトリ以下を再帰的にinclude指定する |
デフォルトでは、${workspaceFolder/**
となっているので、ワークスペースというか、mbed-os
の1段上のディレクトリから再帰的にすべてのディレクトリをinclude指定していることになります。
言い換えれば、すべてのターゲットの実装をincludeしていることになるので、IntelliSenseも混乱して当然です。
解決
解決策は、簡単ですが少々メンドクサイ記述をincludePath
にする必要があります。
問題は、
- 各階層には、各階層でincludeしなくてはならないファイル群と次の階層を示すディレクトリが混在している
- 「再帰的」か「そうでないか」しか、includePathに指定できない
例えば、TARGET_NUCLEO_L476RGだけにファイルが置かれていて、それより浅い階層であるTARGET_STMなどにファイルが置かれていないのであれば、includePath
に、targets/TARGET_STM/TARGET_STM32L4/TARGET_STM32L476xG/TARGET_NUCLEO_L476RG/**
と書くだけで良かったのですが、各階層に置かれているファイルも適切にincludeしなければなりません。
適切にincludeするためには、
- TARGET_STMにあるファイル群
- TARGET_STM32L4にあるファイル群
- TARGET_STM32L4\deviceにあるファイル群
- TARGET_STM32L476xGにあるファイル群
- TARGET_STM32L476xG\device\TOOLCHAIN_GCC_ARMにあるファイル群(処理系に適合したもの)
- TARGET_NUCLEO_L476RGにあるファイル群
を、それぞれincludeしなくてはならない。
それから、デフォルトの記述は強力すぎてすべての努力をなかったことにしてしまうので、デフォルトの記述も変更します。すると、mbed-os
以下のディレクトリも、自分で掘ったディレクトリもすべてincludeされなくなってしまうので、
すべて個別に指定していかなくてはなりません。
includePath
にこれだけ記述してあげると、先ほど赤波線だったI2Cのピン指定も、このようにちゃんと表示されます。
F12
を押すと・・・・
ちゃんと適切にNUCLEO_L476RGのPinNames.hを認識していることがわかります。
最後に、ここまでの修正を行ったc_cpp_properties.json
を置いておきます。compilerPathなどはお使いの環境に合わせてください。
{
"configurations": [
{
"name": "Win32",
"includePath": [
// アプリケーション固有のincludePath
"${workspaceFolder}/*", // プロジェクトルートにあるファイル全部
"${workspaceFolder}/DebugSys/*", // 自分で掘ったディレクトリ
// 自分で掘ったディレクトリは、ここに追加していく
// mbed-os内部のディレクトリ
"${workspaceFolder}/mbed-os/*",
"${workspaceFolder}/mbed-os/cmsis/**",
"${workspaceFolder}/mbed-os/components/**",
"${workspaceFolder}/mbed-os/drivers/**",
"${workspaceFolder}/mbed-os/events/**",
"${workspaceFolder}/mbed-os/features/**",
"${workspaceFolder}/mbed-os/hal/**",
"${workspaceFolder}/mbed-os/platform/**",
"${workspaceFolder}/mbed-os/rtos/**",
// ターゲット固有のディレクトリ
"${workspaceFolder}/mbed-os/targets/TARGET_STM/*",
"${workspaceFolder}/mbed-os/targets/TARGET_STM/TARGET_STM32L4/*",
"${workspaceFolder}/mbed-os/targets/TARGET_STM/TARGET_STM32L4/device/**",
"${workspaceFolder}/mbed-os/targets/TARGET_STM/TARGET_STM32L4/TARGET_STM32L476xG/*",
"${workspaceFolder}/mbed-os/targets/TARGET_STM/TARGET_STM32L4/TARGET_STM32L476xG/device/TOOLCHAIN_GCC_ARM/**",
"${workspaceFolder}/mbed-os/targets/TARGET_STM/TARGET_STM32L4/TARGET_STM32L476xG/TARGET_NUCLEO_L476RG/*"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"DEVICE_I2C",
"DEVICE_SPI"
],
"windowsSdkVersion": "10.0.19041.0",
//"compilerPath": "C:\\Program Files (x86)\\GNU Tools ARM Embedded\\6 2017-q2-update\\bin\\arm-none-eabi-gcc.exe",
"compilerPath": "C:\\Program Files (x86)\\GNU Tools ARM Embedded\\7 2017-q4-major\\bin\\arm-none-eabi-gcc.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-gcc-arm"
}
],
"version": 4
}
デフォルトから、なるべくいじっていません。
Windows用の設定も残っていたりしますが、ツッコミはご容赦を
おまけ
完全なDEVICE_*指定はできるか?
targets
ディレクトリを見ていたら、targets.json
といういかにもな名前のファイルを見つけました。
このファイルで、FAMILY_STM32
の部分を見ると、macros
とか、device_has
には、どこかで見たような文字列が並んでいます。
さらに、NUCLEO_L476RGのところにいくと、ここにも、macros_add
とか書いてある。
mbed.pyを読んでもまったく意味がわからなかったので、ここからは推測ですが・・・・
-
FAMILY_STM32
の、macros
に書かれているマクロが渡されている(ハズ) -
FAMILY_STM32
の、device_has
に書かれている文字列の前にDEVICE_
を付けたマクロも渡されている(ハズ) - NUCLEO_L476RGの、
macros_add
も追加して渡されている(ハズ) - NUCLEO_L476RGの、
device_has_add
に書かれている文字列の前にDEVICE_
を付けたマクロも追加して渡されている(ハズ)
*clock_source
なども、CPUの初期設定の切り替えマクロになっているのは知っているが、いまいち仕組みがわからない(とりあえず、こんな低レベルのことは当面しないだろう)から、からスルー
といえる(ハズ)
この仮定が正しいとすると、mbed compile
の時にコンパイラに渡されている(ハズ)のマクロをすべて再現するための記述は、こんな感じだろうか
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
// FAMILY_STM32.macrosから
"USE_HAL_DRIVER",
"USE_FULL_LL_DRIVER",
"TRANSACTION_QUEUE_SIZE_SPI=2",
// FAMILY_STM32.device_hasから
"DEVICE_USTICKER",
"DEVICE_LPTICKER",
"DEVICE_RTC",
"DEVICE_ANALOGIN",
"DEVICE_I2C",
"DEVICE_I2CSLAVE",
"DEVICE_I2C_ASYNCH",
"DEVICE_INTERRUPTIN",
"DEVICE_PORTIN",
"DEVICE_PORTINOUT",
"DEVICE_PORTOUT",
"DEVICE_PWMOUT",
"DEVICE_SERIAL",
"DEVICE_SERIAL_FC",
"DEVICE_SLEEP",
"DEVICE_SPI",
"DEVICE_SPISLAVE",
"DEVICE_SPI_ASYNCH",
"DEVICE_STDIO_MESSAGES",
"DEVICE_WATCHDOG",
"DEVICE_RESET_REASON",
// NUCLEO_L476RG.macros_addから
"STM32L476xx",
"MBED_TICKLESS",
"EXTRA_IDLE_STACK_REQUIRED",
"USBHOST_OTHER",
"MBED_SPLIT_HEAP",
// NUCLEO_L476RG.device_has_addから
"DEVICE_ANALOGOUT",
"DEVICE_CAN",
"DEVICE_CRC",
"DEVICE_SERIAL_ASYNCH",
"DEVICE_TRNG",
"DEVICE_FLASH",
"DEVICE_MPU"
],
ちなみに、ここには書かなかったけど、DEVICE_ANALOGIN
とかUSE_HAL_DRIVER
も定義してあげないと、低レベルが見えちゃってる実装が通らなかったので、信ぴょう性は少しはあるかもしれません。