公開日: 2023年10月20日 / 最終更新日: 2023年10月26日

Raspberry Pi Pico の L チカプログラムをビルドして動かす

前回の記事で Raspberry Pi Pico (以下 Pico) のクロス開発環境を FreeBSD 上に構築してみました。今回はこの環境で本当にビルドできるのか?ビルドしたものはちゃんと動くのか?を確認します。確認には『インターフェース』誌 2023 年 7 月号の特集記事に掲載されている L チカプログラムを使わせていただきました。

必要な機材を購入する

最低限必要なのは、Pico 本体および Pico をパソコンとつなぐ USB ケーブルです。これだけあれば取りあえず開発は始められます。とはいえデバッガ抜きでの開発は現実的ではないので、Raspberry Pi Pico の公式デバッガである Debug Probe もそろえておきたいところです。それから必須ではないものの、ブレッドボードもあれば重宝します。回路を組まずにただ Pico を挿しておくだけでも据わりがよくなりますし、UART による通信を行う際には USB-UART 変換器をもつ Debug Probe と簡単に接続することができます (次の次くらいの記事で UART を試そうと思うので、我もと思われる方はついでに購入しておくことをおすすめします)。なお、Pico には W、H、WH などいくつかのバリエーションがありますが、本稿では Debug Probe との接続が容易な Raspberry Pi Pico H を使うことにします。

まとめると次の 4 点です。ブレッドボードは参考までに私が購入したものを挙げておきました。

品名・型番メーカー参考単価 (円)備考
Raspberry Pi Pico H

Raspberry Pi 財団

940

Debug Probe

Raspberry Pi 財団

2,380

(*1)

ブレッドボード EIC-801

E-CALL ENTERPRISE

370

USB ケーブル

1,000 程度

(*2)

(*1)付属品: USB ケーブル 1 本、Pico と接続するための 3-pin to 3-pin ケーブル 1 本、UART 通信用ジャンパケーブル オス / メス 各 1 本
(*2)コネクタ形状が A - USB 2.0 micro-B タイプで、給電可能なもの

作業ディレクトリを作る

これからいくつかのリポジトリを GitHub から clone してきます。それらをまとめてつっこんでおくディレクトリを作成しておきましょう。本稿では ~/pico/repo/ とします (なんか韻を踏んでる)。

% mkdir -p ~/pico/repo/

サンプルプログラムについて

本稿で取り上げるサンプルプログラムは『インターフェース』誌 2023 年 7 月号 pp.51-57 に掲載されている Try Kernel の起動処理サンプルプログラムです。Pico の起動処理がほぼスクラッチから書かれており、最終的に main 関数を呼んで LED をチカチカさせるというものです。ソースコードは GitHub で公開されています。

これを clone して中をのぞくと、part_2/sect_3 というディレクトリがあります。ここに今回使わせていただくプログラムが収められています。

% git clone https://github.com/ytoyoyama/interface_trykernel.git
% ls interface_trykernel/part_2/sect_3/
application/    boot/       include/    linker/

ビルドの準備

ビルドに使う私的なツールや、ビルドの過程で生成されるオブジェクトファイル、最終生成物である ELF ファイルや UF2 ファイルを収めるための作業ディレクトリを作っておきましょう。ここでは、~/pico/repo/interface_trykernel/part_2/ と同じ階層に build_part2 ディレクトリを作り、さらにその下に sect_3 用、sect_4 用、ライブラリ格納用、ツール格納用の各サブディレクトリを作ることにします。

% cd ~/pico/repo/interface_trykernel/
% mkdir build_part2/
% cd build_part2/
% mkdir sect_3/ sect_4/ tools/ libs/

ディレクトリの階層は次のようになります (tree コマンドの出力を抜粋。ちなみに tree コマンドは標準ではインストールされていないので、ports/packages からインストールしてください)。

% tree ~/pico/repo/interface_trykernel/
~/pico/repo/interface_trykernel/
├── build_part2
│      ├── libs
│      ├── sect_3
│      ├── sect_4
│      └── tools
├── part_2
│      ├── sect_3
│      └── sect_4
...

今回は build_part2/sect_3 で作業をします。build_part2/sect_4 は次かその次の記事で UART を試すときまで放置します。

elf2uf2 の準備

実行プログラムを Pico に書き込むには、リンカが出力した ELF ファイルを UF2 という形式に変換する必要があります。これを行ってくれるのが elf2uf2 です。elf2uf2 は Raspberry Pi 公式の pico-sdk にソースコードが含まれているので、これをコンパイルして使います。

pico-sdk を GitHub から clone して clone 先のディレクトリに移動し、

% cd ~/pico/repo/
% git clone https://github.com/raspberrypi/pico-sdk.git --branch master
% cd pico-sdk/tools/elf2uf2/

手動でコンパイルします。

% c++ -o elf2uf2 -I../../src/common/boot_uf2/include main.cpp

あるいは cmake が使える環境なら cmake を利用することもできます。

% cmake -S ./ -B build/
% cmake --build build/

どちらのやり方にしても elf2uf2 という名前で実行ファイルが出来上がります。これを必要に応じて適当なディレクトリにコピーしてください。ここでは先ほど作った tools ディレクトリに置くことにします。

% cp elf2uf2 ~/pico/repo/interface_trykernel/build_part2/tools/

Makefile を書く

build_part2/sect_3 ディレクトリに Makefile を用意します。取りあえず次のように書きました。

~/pico/repo/interface_trykernel/build_part2/sect_3/Makefile
PRGNAME := blink
ARCH = arm-none-eabi

SRCROOTDIR := ../../part_2/sect_3
vpath %.c $(SRCROOTDIR)/application:$(SRCROOTDIR)/boot
TOOLDIR := ../tools
BLDDIR := ./build
EXEFILE = $(BLDDIR)/$(PRGNAME)

CC = $(ARCH)-gcc
LD = $(ARCH)-ld
SIZE = $(ARCH)-size
E2U = $(TOOLDIR)/elf2uf2

CFLAGS = -Wall -march=armv6-m -mthumb -ffreestanding
CFLAGS += -I$(SRCROOTDIR)/include
CFLAGS += -g3 -O0
ifeq ($(MAKECMDGOALS),preproc)
	CFLAGS += -E
else
	CFLAGS += -MMD -MP
endif

LFLAGS = -nostartfiles -nostdlib
LFLAGS += -Wl,-Map,$(EXEFILE).map,--gc-sections,-T,$(SRCROOTDIR)/linker/pico_memmap.ld
LLIBS = -lgcc

OBJDIR = $(BLDDIR)/obj
SRCDIRS = $(shell find $(SRCROOTDIR) -type d)
SRCS = $(foreach dir,$(SRCDIRS),$(wildcard $(dir)/*.c))
OBJS = $(addprefix $(OBJDIR)/,$(notdir $(SRCS)))
OBJS := $(patsubst %.c,%.o,$(OBJS))
DEPS = $(OBJS:.o=.d)

PPDIR = $(BLDDIR)/preproc
PPS = $(addprefix $(PPDIR)/,$(notdir $(SRCS)))
PPS := $(patsubst %.c,%.p,$(PPS))

#$(info SRCS = $(SRCS))
#$(info OBJS = $(OBJS))
#$(info DEPS = $(DEPS))
#$(info PPS = $(PPS))

.PHONY: all preproc clean

all: $(EXEFILE).uf2

$(EXEFILE).uf2: $(OBJS)
	$(CC) -o $(EXEFILE).elf $(LFLAGS) $^ $(LLIBS)
	$(SIZE) $(EXEFILE).elf
ifeq ($(wildcard $(E2U)), $(E2U))
	$(E2U) $(EXEFILE).elf $(EXEFILE).uf2
endif

$(OBJDIR)/%.o: %.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -o $@ -c $<

preproc: $(PPS)

$(PPDIR)/%.p: %.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -o $@ -c $<

clean:
	rm -rf $(BLDDIR)

-include $(DEPS)
4 行目:ソースコードが置かれているディレクトリのトップを指定する。
5 行目:C 言語のソースファイルが置かれているパスを make に伝える。
15 行目:コンパイラに渡すオプション。Pico に関係するのは次の 3 つ。
  • -march=armv6-m

    ターゲットアーキテクチャを Cortex-M0+ (および M0、M1) とする。

  • -mthumb

    命令セットの選択。-marmとの 2 択。

  • -ffreestanding

    利用できるヘッダファイルを float.h、iso646.h、limits.h、stdarg.h、stdbool.h、stddef.h、stdint.h (C99 標準 4.6) に限定する。

    参考 URL: GCC and Bare Metal Programming
    http://cs107e.github.io/guides/gcc/
19, 35~37, 59~63 行目:プリプロセッサの確認用。gmake preproc を実行すると、プリプロセッサを通ったあとのファイルが build/preproc/ に出力される (拡張子 .p)。
21, 33, 68 行目:ヘッダファイルが更新されたらリビルドするためのしくみ。コンパイラに-MMDオプションを付けることで、ヘッダファイルの依存情報が build/obj/ に出力される (拡張子 .d)。
24 行目:標準ライブラリの使用を抑制するオプション。
25 行目:リンカに渡すオプション。
  • -Wl

    GCC 経由でリンカにオプションを渡す場合、-Map mapfile のように引数を取るオプションは、そのままでは正しく渡せない。それらを正しく渡すためにこのオプションを使う。例えば

    -Wl,-Map,hoge.map,--gc-sections,-T,fuga.ld
    

    と書くと、リンカには次のようになって渡される。

    -Map hoge.map --gc-sections -T fuga.ld
    

    (…ということのようですが、いい情報源が見つからなかったので、まちがっているかもしれません)

  • -Map,(EXEFILE).map

    マップファイルを build/blink.map に出力する。

  • --gc-sections

    リンク時に入力セクションのガベージコレクションを行う。

  • -T,./linker/pico_memmap.ld

    リンカスクリプトを指定する。

26 行目:

リンクする静的ライブラリを-lオプションで指定する。

※ 静的ライブラリをリンクする場合は、-lオプションをリンカに渡す順番も重要です。49 行目のように-lオプションがオブジェクトファイル ($^) のあとに来るように書きましょう。こうしておかないとリンクの際に undefined reference エラーが発生します。

29~33 行目:ソースファイル、オブジェクトファイル、ヘッダ依存情報ファイルのリストをそれぞれSRCSOBJSDEPSに格納する。39~42 行目のコメントを外すと、それぞれの変数の中身をデバッグ表示することができる。
44 行目:allpreproccleanは通常のターゲットではなく、疑似ターゲットとして実行する。
48~50 行目:オブジェクトファイルをリンクして ELF ファイルを生成する。その際、arm-none-eabi-size コマンドで各セクションのサイズも出力する。
51~53 行目:もし elf2uf2 が存在すれば ELF を UF2 に変換する。

ここまでの作業でサンプルディレクトリのツリー構造は次のようになっています。★印は私が追加したディレクトリとファイルです。

% tree ~/pico/repo/interface_trykernel/
~/pico/repo/interface_trykernel/
├── build_part2               ★
│      ├── libs               ★
│      ├── sect_3             ★
│      │      └── Makefile    ★
│      ├── sect_4             ★
│      └── tools              ★
│              └── elf2uf2    ★
├── part_2
│      ├── sect_3
│      └── sect_4
...

ビルドする

サンプルプログラムをビルドするには build_part2/sect_3 ディレクトリで gmake コマンドを実行します。成功すればその下の build ディレクトリに blink.uf2 というファイル名で実行プログラムが得られます。

% cd ~/pico/repo/interface_trykernel/part_2/sect_3/
% gmake
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/main.o -c ../../part_2/sect_3/application/main.c
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/boot2.o -c ../../part_2/sect_3/boot/boot2.c
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/reset_hdr.o -c ../../part_2/sect_3/boot/reset_hdr.c
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/vector_tbl.o -c ../../part_2/sect_3/boot/vector_tbl.c
arm-none-eabi-gcc -o ./build/blink.elf -nostartfiles -nostdlib -Wl,-Map,./build/blink.map,--gc-sections,-T,../../part_2/sect_3/linker/pico_memmap.ld build/obj/main.o build/obj/boot2.o build/obj/reset_hdr.o build/obj/vector_tbl.o -lgcc
arm-none-eabi-size ./build/blink.elf
   text        data         bss         dec         hex     filename
   3056           0           0        3056         bf0     ./build/blink.elf
../tools/elf2uf2 ./build/blink.elf ./build/blink.uf2

libaeabi-cortexm0 を使う場合

前回の記事で紹介した libaeabi-cortexm0 を libgcc の代わりに利用する方法についても触れておきます。前回作った libaeabi-cortexm0.a を build_part2/libs ディレクトリに置き、先ほど書いた Makefile を次に示す差分のように修正します。

~/pico/repo/interface_trykernel/build_part2/sect_3/Makefile (差分)
% diff -u Makefile.old Makefile
--- Makefile.old    2023-10-19 22:49:51.486486000 +0900
+++ Makefile    2023-10-18 10:00:12.436376000 +0900
@@ -1,6 +1,8 @@
 PRGNAME := blink
 ARCH = arm-none-eabi

+LIBAEABI := 0
+
 SRCROOTDIR := ../../part_2/sect_3
 vpath %.c $(SRCROOTDIR)/application:$(SRCROOTDIR)/boot
 TOOLDIR := ../tools
@@ -23,7 +25,18 @@

 LFLAGS = -nostartfiles -nostdlib
 LFLAGS += -Wl,-Map,$(EXEFILE).map,--gc-sections,-T,$(SRCROOTDIR)/linker/pico_memmap.ld
-LLIBS = -lgcc
+LLIBS =
+
+ifneq ($(LIBAEABI),0)
+	LIBAEABI_DIR := ../libs
+	LIBAEABI_A = $(LIBAEABI_DIR)/libaeabi-cortexm0.a
+	ifeq ($(wildcard $(LIBAEABI_A)), $(LIBAEABI_A))
+		LFLAGS += -L$(LIBAEABI_DIR)
+		LLIBS += -laeabi-cortexm0
+	endif
+else
+	LLIBS += -lgcc
+endif

 OBJDIR = $(BLDDIR)/obj
 SRCDIRS = $(shell find $(SRCROOTDIR) -type d)

変数 LIBAEABI が 0 以外かつ libaeabi-cortexm0.a が存在するときは libaeabi-cortexm0.a をリンクし、そうでないとき (こっちがデフォルト) は libgcc.a をリンクします。では LIBAEABI=1 を指定して make してみます。

% gmake LIBAEABI=1
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/main.o -c ../../part_2/sect_3/application/main.c
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/boot2.o -c ../../part_2/sect_3/boot/boot2.c
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/reset_hdr.o -c ../../part_2/sect_3/boot/reset_hdr.c
arm-none-eabi-gcc -Wall -march=armv6-m -mthumb -ffreestanding -I../../part_2/sect_3/include -MMD -MP -o build/obj/vector_tbl.o -c ../../part_2/sect_3/boot/vector_tbl.c
arm-none-eabi-gcc -o ./build/blink.elf -nostartfiles -nostdlib -Wl,-Map,./build/blink.map,--gc-sections,-T,../../part_2/sect_3/linker/pico_memmap.ld -L../libs build/obj/main.o build/obj/boot2.o build/obj/reset_hdr.o build/obj/vector_tbl.o -laeabi-cortexm0
arm-none-eabi-size ./build/blink.elf
   text        data         bss         dec         hex     filename
   2560           0           0        2560         a00     ./build/blink.elf
../tools/elf2uf2 ./build/blink.elf ./build/blink.uf2

こちらも無事にビルドすることができました。arm-none-eabi-size コマンドの出力を見ると各セクションの合計が 2560 バイトになっており、libgcc 版 (3056 バイト) に比べて 500 バイトほど節約できたことがわかります。通常は libgcc を使っておけば問題ないでしょうが、こういう選択肢もあることを頭の片隅にとどめておくといいかもしれません。

プログラムの書き込みと実行

パソコンとの接続

UF2 ファイルを書き込むだけならパソコンとの接続は簡単です。図 1 のように USB ケーブルで Pico とパソコンとをつなぐだけです。

図 1: Pico とパソコンの接続
図 1: Pico とパソコンの接続

実行プログラムを書き込む

それではいよいよ、L チカプログラム blink.uf2 を Pico に書き込んでみましょう。Pico の BOOTSEL ボタンを押しながらパソコンに USB 接続すると、マスストレージデバイスとして認識されます。これをマウントしたいので、まずそのデバイスノードを dmesg コマンドと geom コマンドで探ります。

% dmesg | tail
umass0 on uhub2
umass0: <Raspberry Pi RP2 Boot, class 0/0, rev 1.10/1.00, addr 4> on usbus0
umass0:  SCSI over Bulk-Only; quirks = 0x0100
umass0:3:0: Attached to scbus3
da1 at umass-sim0 bus 0 scbus3 target 0 lun 0
da1: <PI RP2 3> Removable Direct Access SCSI-2 device
da1: Serial Number E0C9125B0D9B
da1: 1.000MB/s transfers
da1: 128MB (262144 512 byte sectors)
da1: quirks=0x2<NO_6_BYTE>

% geom part list | grep da1
Geom name: da1
1. Name: da1s1
1. Name: da1

この出力から、デバイスノードは /dev/da1s1 っぽいなということがわかります。あとはこれを適当なマウントポイントにマウントし、UF2 ファイルをコピーするだけで、フラッシュメモリに実行プログラムが書き込まれます。書き込みが終わったらアンマウントしておきましょう (ちなみに当サイトでは、プロンプトが '#' のときは root 権限か sudo でコマンドを実行することを表します)。

# mount_msdosfs /dev/da1s1 /media
# cp blink.uf2 /media
# umount /media

これを毎回やるのは大変なので、ここまでの手順をシェルスクリプトにしてみました。その名も picowrite.sh と名づけておきましょう。

picowrite.sh
#!/bin/sh

if [ $# -lt 1 ]; then
    echo "usage: picowrite uf2file" 1>&2
    exit 1
fi

DISKLIST=`geom disk list`
RE_GEOM="Geom name: "
RE_DESC="descr: "
RE_PICO="RPI RP2"
DISKNAME=""
DESC=""

# ディスクを特定する
while read line; do
    line=`echo $line`    # 前後の空白を削除する
    if [ `expr "$line" : "$RE_GEOM"` -ge ${#RE_GEOM} ]; then
        DISKNAME=`echo $line | sed -e "s/$RE_GEOM//"`
        continue
    fi

    if [ `expr "$line" : "$RE_DESC"` -ge ${#RE_DESC} ]; then
        DESC=`echo $line | sed -e "s/$RE_DESC//"`
        if [ `expr "$DESC" : "$RE_PICO"` -ge ${#RE_PICO} ]; then
            break
        else
            DESC=""
        fi
    fi
done <<EOF
$DISKLIST
EOF

if [ "$DESC" = "" ]; then
    echo "device not found."
    exit 1
fi

PARTLIST=`geom part list "$DISKNAME"`
RE_PROV="Providers:"
RE_NAME="1. Name: "
NODE=""

# パーティションを特定する
while read line; do
    line=`echo $line`    # 前後の空白を削除する
    if [ `expr "$line" : "$RE_PROV"` -ge ${#RE_PROV} ]; then
        read line
        NODE=`echo $line | sed -e "s/$RE_NAME//"`
        NODE="/dev/$NODE"
        break;
    fi
done <<EOF
$PARTLIST
EOF

if [ "$NODE" = "" ]; then
    echo "device not found."
    exit 1
fi

echo -n "copy $1 to $NODE ($DESC)... "

mount_msdosfs $NODE /media
cp $1 /media
umount /media

echo "done."

基本的には上記の手順をそのまま実行しますが、 /dev/da1s1 のところは geom コマンドの出力を地道に解析して自動で判断する作りになっています (もっとスマートなやり方がありそうだけど…)。引数として書き込みたい UF2 ファイル名を指定します。実行には root 権限が必要です。

# picowrite.sh blink.uf2

このスクリプトも tools ディレクトリに入れておきましょう。

L チカを実行する

blink.uf2 を書き込んでアンマウントすると Pico の LED が点滅を開始します。写真だとわかりづらいかもしれませんが、チカチカしてます。

図 2: チカチカしてます
図 2: チカチカしてます

ちなみに、実行プログラムはフラッシュメモリに書き込まれているので、Pico の電源を落としても (USB 接続を断っても) 保持され、次回電源オンすればまたチカチカしだします。

本日のおさらい

今回の成果物と実行手順のおさらいです。

% cd ~/pico/repo/interface_trykernel/
% tree
.
├── build_part2                        ★
│      ├── libs                        ★
│      │      └── libaeabi-cortexm0    ★
│      ├── sect_3                      ★
│      │      └── Makefile             ★
│      ├── sect_4                      ★
│      └── tools                       ★
│              ├── elf2uf2             ★
│              └── picowrite.sh        ★
├── part_2
│      ├── sect_3
│      └── sect_4
...
% cd build_part2/sect_3/
% gmake
# ../tools/picowrite.sh build/blink.uf2

今回はここまでです。作成した Makefile と picowrite.sh は GitHub で公開します。

Raspberry Pi Pico 実験室