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も定義してあげないと、低レベルが見えちゃってる実装が通らなかったので、信ぴょう性は少しはあるかもしれません。

