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
https://github.com/raspberrypi/pico-sdk/ - UF2 ドキュメント
https://github.com/microsoft/uf2
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 を用意します。取りあえず次のように書きました。
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 つ。
|
19, 35~37, 59~63 行目: | プリプロセッサの確認用。gmake preproc を実行すると、プリプロセッサを通ったあとのファイルが build/preproc/ に出力される (拡張子 .p)。 |
21, 33, 68 行目: | ヘッダファイルが更新されたらリビルドするためのしくみ。コンパイラに-MMD オプションを付けることで、ヘッダファイルの依存情報が build/obj/ に出力される (拡張子 .d)。 |
24 行目: | 標準ライブラリの使用を抑制するオプション。 |
25 行目: | リンカに渡すオプション。
|
26 行目: | リンクする静的ライブラリを ※ 静的ライブラリをリンクする場合は、 |
29~33 行目: | ソースファイル、オブジェクトファイル、ヘッダ依存情報ファイルのリストをそれぞれSRCS 、OBJS 、DEPS に格納する。39~42 行目のコメントを外すと、それぞれの変数の中身をデバッグ表示することができる。 |
44 行目: | all 、preproc 、clean は通常のターゲットではなく、疑似ターゲットとして実行する。 |
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 を次に示す差分のように修正します。
% 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 とパソコンとをつなぐだけです。
実行プログラムを書き込む
それではいよいよ、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 と名づけておきましょう。
#!/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 が点滅を開始します。写真だとわかりづらいかもしれませんが、チカチカしてます。
ちなみに、実行プログラムはフラッシュメモリに書き込まれているので、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 で公開します。
広告