👀

RPi PicoをD言語でLチカする

2024/12/02に公開

はじめに

この記事はD言語 Advent Calendar 2024の2日目の記事です。

https://qiita.com/advent-calendar/2024/dlang

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で公開されています。

https://github.com/raspberrypi/pico-sdk

こちらの手順に従って必要なツールのインストールなどを行っていきます。

前提とする環境

今回私は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も用意します。

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を参考にします。

https://github.com/raspberrypi/pico-examples

GPIOのOn/Offを繰り返すだけの単純なソースですね。

blink.c
#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.cCMakeLists.txtでコンパイルされるようにします。

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がうまくいかずに終了してしまいます。

https://github.com/dcarp/cmake-d

cmakeはそもそも公式に対応しているプログラミング言語(C/C++・アセンブラ・C#など)向けに作られており、独自に言語を追加するのは割とハックな感じのようです。以下にそのあたりの事が書いてありました。

https://github.com/Kitware/CMake/blob/master/Modules/CMakeAddNewLanguage.txt

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_dirgpio_putは実はインライン関数です。

その2つは、blink.cでC言語のラッパー関数を書いてinlineでは無くしてしまい、そちらをD言語から使うようにしようと思います。多少オーバーヘッドが生じますが……。

(TODO: たぶん頑張ればImportCの機能でinlineのまま取り込める)

そんなわけで、blink.cでラッパー関数を作るようにします。mainはD言語に移植するので消します。

blink.c
#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を使うと良い気がします。

https://zenn.dev/outlandkarasu/articles/341f4d3fc9add9

D言語でArm向けライブラリをビルド

方針が決まったので、実家のように安心感のあるD言語ビルドツールdub向けの設定ファイルを書きます。

(なお、dub.jsonはきちんとしたJSON形式なので、実際はコメントを書けない点に注意してください)

dub.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を書きます。

source/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に組み込みます。

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チカするはずです!

結果

見事Lチカ

やった、光った!

なお、鋭い人は、このボードがRPi PicoではなくWifi搭載版のRPi Pico Wであることに気付くはずです……。呼ぶ関数を多少変えればRPi Pico Wでも大丈夫なわけですね。
(むしろGPIO周りの関数がinlineではなかったので.c無しでいけて簡単でした)

というわけで、D言語でもRPi Picoで遊べることが分かりました。

他のSDKの関数も同じようにD言語にポーティングして色々試せそうです。今月残りのカレンダー枠でもう少し何かできたら良いと思います!

なお、今回やった件のソースコードはGithubに挙げてあります。(Pico W版ですが)

https://github.com/outlandkarasu-sandbox/rpi-pico-blink-d/

Discussion