RPi PicoをD言語でLチカする
はじめに
この記事はD言語 Advent Calendar 2024の2日目の記事です。
Raspberry Pi Picoは、シングルボードコンピューターRaspberry Piを作っているRaspberry Pi財団が作ったマイクロコントローラー(のRP2040を使った製品)です。
立ち位置としてはArduinoやESP32に近く、Raspberry PiがLinuxの動くSBCであるのに対して、基本的にOS等のないハードウェア制御用のマイクロコントローラーとなっています。
特徴としては、
- 結構安い。RPi Picoで1000円未満。
- デュアルコアでSRAMも264KBと結構ある。
- 豊富なペリフェラル。PIOとかもある。
- Wifi搭載版(RPi Pico W)もある。
- データシート、ドキュメント、SDKが充実している。
といったところです。機能や性能のわりに安いしドキュメントも揃っているという点が嬉しいなと思います。
で、RPi Picoが出た当初は1枚550円くらいで買えたので勢い余って数枚購入してしまいましたが、それから罪ボード化してしまっていました。年末のこの機会にD言語でLチカくらいやってみようと思います。
まずは普通にC言語でLチカ
RPi PicoはSDKが整備されていて、ソースコードも含めてGithubで公開されています。
こちらの手順に従って必要なツールのインストールなどを行っていきます。
前提とする環境
今回私はWindows(x86_64)のWSL2上のUbuntu 22.04で作業を行いました。他のLinux環境でもほぼ同様の手順でいけると思います。Windows上ではどうなるかわかりません……。
ちなみに、VSCodeの拡張機能としてpico-vscodeが用意されており、こちらを利用すると関連ツールのインストールやSDKのダウンロードまで一式行ってくれるようです。手軽に使う分にはこちらも良いと思います。
使うRPi Picoのボードは公式の通常のRaspberry Pi Pico(RP2040)を想定します。
必要なツールのインストール
さて、まずはArm開発用のクロスコンパイラ等をインストールします。
$ sudo apt install \
cmake \
python3 \
build-essential \
gcc-arm-none-eabi \
libnewlib-arm-none-eabi \
libstdc++-arm-none-eabi-newlib
build-essential
くらいまでは開発やっている人であればなんとなく入っているかなと思います。gcc-arm-none-eabi
以降がArm向け(つまり今回ではRPi Pico向け)クロスコンパイラになります。
Lチカプロジェクトの用意
次にLチカ用ディレクトリを掘ってLチカプロジェクトを作っていきましょう。
今回はとりあえずお試しという事で、pico-sdkが用意してくれているcmakeソースファイルを使って自動でpico-sdkをダウンロードしてもらうようにします。
本格的にRPi Picoを使っていく場合は、きちんとどこかにpico-sdkをダウンロードしておいて、そちらを参照する形にしたほうが良いと思います。
# どこか適当な場所にディレクトリを掘る。
$ mkdir blink
$ cd blink
# pico-sdkを自動で取ってきてくれたりするcmakeファイルの取得
$ curl -O -s https://raw.githubusercontent.com/raspberrypi/pico-sdk/refs/heads/master/external/pico_sdk_import.cmake
また、プロジェクト本体をビルドするためのCMakeLists.txt
も用意します。
cmake_minimum_required(VERSION 3.13)
# Githubからpico-sdkを取得するよ
set(PICO_SDK_FETCH_FROM_GIT on)
# pico-sdkを取得するためのcmakeを呼び出す
# projectより前にやってね
include(pico_sdk_import.cmake)
# Lチカプロジェクト宣言
project(blink)
# pico-sdk初期化
pico_sdk_init()
# 続きは後ほど……
ここまでできた段階で、一旦cmake
します。なお、ビルド時に大量に中間ファイルができるので、build
ディレクトリを掘ってそちらで実行します。
$ mkdir build
$ cd build
$ cmake ..
# 色々インストールとかやってくれるはず
build
は今後中間ファイル置き場になっていくので、Gitで管理する場合は.gitignore
しておくと良いと思います。
Lチカの実装
さてpico-sdkのドキュメントに従いつつ、Lチカをblink.c
として実装していきます。
こちらのリポジトリーにあるblink
を参考にします。
GPIOのOn/Offを繰り返すだけの単純なソースですね。
#include "pico/stdlib.h"
// Lチカ速度 250ミリ秒ごとにOn/Off切り替え
#define LED_DELAY_MS 250
int main() {
// GPIO初期設定。LEDピンを出力に設定。
gpio_init(PICO_DEFAULT_LED_PIN);
gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
// 永久にLチカ
while (true) {
gpio_put(PICO_DEFAULT_LED_PIN, true);
sleep_ms(LED_DELAY_MS);
gpio_put(PICO_DEFAULT_LED_PIN, false);
sleep_ms(LED_DELAY_MS);
}
}
pico-sdkのおかげでArduinoみたいな簡単さになっていますね。
難しいのはビルドです……。(C言語のうちはまだ簡単だけど)
Lチカのビルド
先ほど作成したblink.c
をCMakeLists.txt
でコンパイルされるようにします。
cmake_minimum_required(VERSION 3.13)
# Githubからpico-sdkを取得するよ
set(PICO_SDK_FETCH_FROM_GIT on)
# pico-sdkを取得するためのcmakeを呼び出す
# projectより前にやってね
include(pico_sdk_import.cmake)
# Lチカプロジェクト宣言
project(blink C CXX ASM) # 使用する言語を追加
# C/C++バージョン指定
# これちゃんと書いてないとLチカされない場合があった
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# pico-sdk初期化
pico_sdk_init()
# 実行ファイル生成追加
add_executable(blink
blink.c
)
# ライブラリのリンク追加
target_link_libraries(blink pico_stdlib)
# RPi Picoに焼くための各種ファイル生成追加
pico_add_extra_outputs(blink)
project
周辺とadd_executable
以降が追加箇所になります。私は現代C/C++エアプ勢なのでこのあたりのcmakeの使い方は全然わかりません。雰囲気でやっています。
とにかく上記のような修正でcmake
でき、さらにMakefile
もできてビルドできるようになります。
# 先ほどと同様にcmake実行
$ cd build
$ cmake ..
# 色々出る。
# 成功していればMakefileができているはず。makeする。
$ make blink
# 色々出る。
# 成功していればボード焼き込み用uf2ファイルができる。
$ ls -al blink.uf2
あとは、RPi PicoをBOOTSELボタン押しっぱなしのままUSBでPCに繋いで、出てきたUSBストレージにこのblink.uf2
ファイルをドラッグ&ドロップすれば、自動的にリセットされてLチカされます。やったね。
D言語でLチカ
で、ここからがようやく本記事の本題です。
D言語は、公式のDMDはまだx86_64しか対応していませんが、clangなどで使われているLLVMをバックエンドとするLDCやGCCをバックエンドとするGDCがあり、それらでArm等のプラットフォーム向けのバイナリも生成できます。
今回はLDCを使ってArm向けバイナリを生成し、RPi Picoを動かしてみます。
pico-sdkとの連携方法
ところで、pico-sdkはC言語のライブラリですが、通常のバイナリのライブラリとは異なる、cmakeでの分類によればINTERFACEライブラリ……つまりヘッダ(とソースコード)からなるライブラリの扱いのようです。
おそらく、ボードごとに細かいマクロ等の設定が必要で、通常のバイナリのライブラリとするのが困難なのかなと思います。組み込み系は大体自前ビルド基本なんだろうか……。
で、何が面倒かというと、D言語のコンパイルとpico-sdkのビルドプロセスをどうにか連携させなければなりません。
pico-sdkは前述の通りcmakeでビルドされるようになっています。そこで一番理想的には、D言語のビルドもcmakeで行えると良いのですが、現状cmakeでD言語はビルドできないようです。
探してみたところ過去に対応しようとした人たちの痕跡はあり、いくつか使ってみたのですが、最新版では残念ながら動作しないようです……。
下記のようなcmake用モジュールがあるのですが、cmakeのバージョンが2.8.1
のころのものらしく、なぜかfind_file
がうまくいかずに終了してしまいます。
cmakeはそもそも公式に対応しているプログラミング言語(C/C++・アセンブラ・C#など)向けに作られており、独自に言語を追加するのは割とハックな感じのようです。以下にそのあたりの事が書いてありました。
The implementation behind the scenes of project/enable_language,
including the compiler/platform modules, is an internal API that
does not make any compatibility guarantees. It is not covered in the
official reference documentation that is versioned with the source code.
(雑要約: 言語の追加については内部APIなので互換性に何も保証はないよ。公式リファレンスでもカバーしないよ)
というわけでcmakeで直接D言語を扱うのは諦めて、D言語ではライブラリを生成し、それをcmakeでリンクするという安直な方法でいこうと思います。
たとえライブラリでも、main関数さえ定義してしまえば、リンク時にそのmain関数から起動されるバイナリができることになります。
inline関数への対応
しかし、上記の方針ではまだC言語のinline関数の対応を考える必要があります。
inline関数はオブジェクトファイルに通常の関数としては出力されないため、リンク時に該当の関数が見つからないエラーが起きることになります。
今回blink.c
で使っているgpio_set_dir
とgpio_put
は実はインライン関数です。
その2つは、blink.c
でC言語のラッパー関数を書いてinlineでは無くしてしまい、そちらをD言語から使うようにしようと思います。多少オーバーヘッドが生じますが……。
(TODO: たぶん頑張ればImportCの機能でinlineのまま取り込める)
そんなわけで、blink.c
でラッパー関数を作るようにします。main
はD言語に移植するので消します。
#include "pico/stdlib.h"
// D言語向け関数のポーティング
void gpio_set_dir_d(uint gpio, bool output)
{
gpio_set_dir(gpio, output);
}
void gpio_put_d(uint gpio, bool value)
{
gpio_put(gpio, value);
}
LDCのインストール等
LDCのインストールについては、去年も書いたこの記事等を参考にしてみてください。
D言語のインストールは人生最良の行動です。
基本的にinstall.sh
を使うと良い気がします。
D言語でArm向けライブラリをビルド
方針が決まったので、実家のように安心感のあるD言語ビルドツールdub向けの設定ファイルを書きます。
(なお、dub.json
はきちんとしたJSON形式なので、実際はコメントを書けない点に注意してください)
{
"authors": [
"outlandkarasu"
],
"description": "A minimal D application.",
"license": "BSL-1.0",
"name": "blink",
"targetType": "staticLibrary", // 静的ライブラリをビルドするよう指定
// LDCのツールチェインを使用する
"toolchainRequirements": {
"dmd": "no",
"gdc": "no",
"ldc": ">=1.39.0"
},
// BetterCモードにする。これを指定すると、D言語のGCや例外や動的配列等のランタイムライブラリが必要な機能を使えなくできる。その代わり不便にはなります……。
"buildOptions": [
"betterC"
],
// LDC向けコンパイラーフラグ設定
"dflags": [
"--mtriple=arm-none-eabi", // いわゆるmtriple。ARMを指定。
"--mcpu=cortex-m0plus", // RP2040に合わせてCortex-M0+に
"--defaultlib=", // この辺の余計なライブラリがリンクされないようにする。不要かも……。
"--debuglib=",
"--platformlib=",
"--fno-exceptions" // 例外の機能も確実に無しにする
]
}
そしてD言語のソースapp.d
を書きます。
// GCも例外も使わないのでここで乱暴に指定
@nogc nothrow:
// pico-sdkで定義されていたマクロを移植、関数宣言をポーティング
extern(C) @system
{
// LEDピン定義はRPi Pico向け。他のボードだとたぶん違う。
enum PICO_DEFAULT_LED_PIN = 25;
enum LED_DELAY_MS = 250;
enum GPIO_OUT = 1u;
// pico-sdkの関数。通常の関数であればextern(C)で宣言するだけでOK
void gpio_init(uint gpio);
void sleep_ms(uint ms);
// こちらはinline関数のため、ポーティングした関数を取り込む
// 関数名としては元の名称を使えるようにする
pragma(mangle, "gpio_set_dir_d")
void gpio_set_dir(uint gpio, bool output);
pragma(mangle, "gpio_put_d")
void gpio_put(uint gpio, bool value);
}
/**
* D言語のmain関数。extern(C)しているのでC言語のmain関数でもある。
*/
extern(C) void main()
{
// GPIO初期設定。LEDピンを出力に設定。
gpio_init(PICO_DEFAULT_LED_PIN);
gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
// 永久にLチカ
while (true) {
gpio_put(PICO_DEFAULT_LED_PIN, true);
sleep_ms(LED_DELAY_MS);
gpio_put(PICO_DEFAULT_LED_PIN, false);
sleep_ms(LED_DELAY_MS);
}
}
内容はほとんどblink.c
と同じですね……。C言語使える人なら余裕で使っていける雰囲気でしょう。
さて、上記をビルドします。どうせデバッグ情報等も使えないと思うので、リリースビルドしてしまいます。
$ source ~/dlang/ldc-1.39.0/activate # install.shを使った人はこれでLDCを使えるようにする
$ dub build --build=release-nobounds
# 上記が成功するとライブラリファイル libblink.a が生成される
D言語ライブラリとpico-sdkのリンク
D言語ライブラリのビルドが成功したら、CMakeLists.txt
に組み込みます。
#
# ~前略~
#
# 実行ファイル生成追加
add_executable(blink
blink.c
)
# ライブラリのリンク追加
target_link_libraries(blink pico_stdlib)
# D言語で作ったライブラリを追加
target_link_libraries(blink ${CMAKE_CURRENT_SOURCE_DIR}/libblink.a)
#
# ~後略~
#
そしてcmakeします。
$ cd build
$ cmake ..
$ make blink
$ ls -al blink.uf2
C言語の時と同様にblink.uf2
ができているはずです。こちらをRPi Picoに転送しましょう。
うまくいけば、前と同じようにLチカするはずです!
結果
やった、光った!
なお、鋭い人は、このボードがRPi PicoではなくWifi搭載版のRPi Pico Wであることに気付くはずです……。呼ぶ関数を多少変えればRPi Pico Wでも大丈夫なわけですね。
(むしろGPIO周りの関数がinlineではなかったので.c
無しでいけて簡単でした)
というわけで、D言語でもRPi Picoで遊べることが分かりました。
他のSDKの関数も同じようにD言語にポーティングして色々試せそうです。今月残りのカレンダー枠でもう少し何かできたら良いと思います!
なお、今回やった件のソースコードはGithubに挙げてあります。(Pico W版ですが)
Discussion