半導体不足がおさまり、以前から気になっていたRaspberry Pi Picoの入手が容易になったので購入しました。Raspberry Pi Pico Wを使って遠隔スイッチを作成する予定ですが、そのほかに実験用、布教用と合計3つRaspberry Pi Pico Wを購入しました。布教用は会社の忘年会のプレゼント交換で旅立って行きました。
Raspberry Pi Picoとは
Raspberry Pi Pico(以下、Picoと略記します)は、ARM系のCortex-M0+というCPUを搭載したマイコンボードです。Pico以外のRaspberry PiシリーズはPCを小型化したような構成で、LinuxなどのマルチタスクOSをインストールして使用するのが一般的です。それに対して、Picoは基本的にはOSはインストールしません。
ブートローダがいきなりユーザプログラムを動作させ、ユーザが作成したプログラムがCPUを含めたハードウェア全てを支配します。まさに組み込みシステムですが、開発環境が整っているため、初心者でも組み込み開発の世界を垣間見ることができます。OSというある意味邪魔な存在がないため、ソフトウェアとハードウェアの関係性を学習するにはうってつけだと思います。
必要であればFreeRTOSのようなRTOSを併用することも可能なようですし、AWS IoT Coreと接続してあれこれさせることも可能なようです。
こんな素晴らしい環境を無線LAN機能付き、ピンヘッダ付きのRaspberry Pi Pico WHでも約1,000円で買える(ただし記事投稿時は国内ベンダーから入手すると高いため、イギリスのPimoroni社から輸入した場合の価格)とは、良い時代になったものです。
今回の流れ
ハードウェアの動作チェックを兼ねて、定番のLED点滅(通称Lチカ)を行います。「CPUと向き合う」という趣旨ですので、以下のように複数の手段でLチカを行います。
- インタプリタ言語(MicroPython)でLチカ
- コンパイラ言語(C言語)でLチカ
- 機械語でLチカ(← CPUと直接対峙する)
開発環境構築
親切なサイトが複数あり、その通りに行えば簡単に開発環境を構築できますのでここでは手順を省略します。私の開発環境の概要は以下のとおりです。5分程度で開発環境は構築できました。
- ホスト環境 M2 Mac
- 統合開発環境Thonnyを使用
- MicroPythonファームウェアはv1.21.0 (2023-10-05)を使用
MicroPythonでLチカ
BOOTSELスイッチを押しながらホストPCにUSBで接続すると、リムーバブルドライブとしてPicoが見えるようになりますので、ここにMicroPythonファームウェアをコピーします。コピーが完了すると自動的にPicoが再起動するようで、ドライブの接続が自動的に切断されます。ここでOSから下の画像のような「ディスクの不正な取り出し」と警告メッセージが出るためビビりますが、これは正常動作ということで問題ありません。
Thonnyに以下のようなPythonプログラムを入力し、実行するとボードのLEDが点滅します。簡単すぎてつまらないですね… MicroPythonのファームウェアがいろいろ裏でやってくれるので、あまり組み込みっぽくないです。
無線LAN付きのPico Wでは注意点があります。無線LANなしのPicoのLEDはGPIO 25ピンに接続されているところ、Pico WのLEDは無線LANチップ(CYW43439)にぶら下がっているGPIO 0に接続が変更になっています。そのため、色々なところに転がっているPico向けのLチカスクリプト(“LED”の部分が25となっている)を実行するとLEDが点滅しないため、初期不良品か? と焦ることになります。Pico Wの場合は25ではなく”LED”を指定しましょう。
from machine import Pin
import time
led = Pin("LED", Pin.OUT)
while True:
led.value(1)
time.sleep(0.5)
led.value(0)
time.sleep(0.5)
C言語でLチカ
次はC言語でLEDを点滅させてみます。C/C++のSDKが必要となりますので、The C/C++ SDKの手順でセットアップします。ただし、ここで書かれている手順は通常のRaspberry Piを開発用ホストにする場合の手順で書かれていますので、今回のようにM2 Macでホストする場合やWindowsをホストにする場合は手順が変わります。
私は公式ドキュメントのGetting started with Raspberry PicoのPDF文書を参考にセットアップしました。「9.1.1. Installing the Toolchain」を実施後、「2.1. Get the SDK and examples」、「2.3. Updating the SDK」を実施しました。
しかしM2 Macの場合はこの手順ではハマりました。Cのプログラムコンパイル時に「arm-none-eabi-gcc: fatal error: cannot read spec file ‘nosys.specs’: No such file or directory」のようなエラーが出てコンパイルが成功しません。https://github.com/raspberrypi/pico-feedback/issues/355で議論されているように、クロスコンパイラはarm-none-eabi-gccではなくgcc-arm-embeddedをインストールする必要があるようです。
C言語のプログラムを作成しようと思いましたが、公式のExample集にありましたのでそれをそのまま使用します。
% cd ~/pico/
% git clone https://github.com/raspberrypi/pico-examples
pico-examples/pico_w/wifi/blink/picow_blink.cが以下のようなLチカのプログラムです。
/**
* Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
int main() {
stdio_init_all();
if (cyw43_arch_init()) {
printf("Wi-Fi init failed");
return -1;
}
while (true) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
sleep_ms(250);
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
sleep_ms(250);
}
}
先ほども書きましたが、Pico Wの場合はLEDが無線LANチップのGPIOにぶら下がっています。LEDを点滅させるだけなのですが、無線LANチップの初期化(cyw43_arch_init())が実行されています。
コンパイルは以下の手順で行います。Pico SDKは~/pico/pico-sdk/に配置されていると仮定します。
% setenv PICO_SDK_PATH ~/pico/pico-sdk (csh系の場合)
% setenv PICO_BOARD pico_w (csh系の場合)
% cd ~/pico/pico-examples
% mkdir build
% cd build
% cmake ..
% cd pico_w/wifi/blink
% make
(省略...)
Linking CXX executable picow_blink.elf
Built target picow_blink
picow_blink.uf2というファイル名でLチカを行うファームウェアが作成されました。Lチカするだけなのに、ファイルサイズは526,848 bytesと意外に大きいです。リンクされているSDKのライブラリがサイズのほとんどを占めていると思われます。
このuf2ファイルをBOOTSELボタンを押しながらPicoをホストにUSB接続し、リムーバブルドライブとして現れたPicoに書き込めば、自動的にハードウェアリセットされ、LEDが点滅し始めます。
ここでコンパイル時に作成されたpicow_blink.disを眺めてみます。これはC言語をコンパイルしてCPUが直接解釈可能な機械語に変換した結果を、アセンブリ言語で書いたもの(ディスアセンブルコード)となります。長いファイルですが、その中でmainと書かれているところに着目すると、C言語がどのように機械語に変換されたかが分かります。
10000308 <main>:
10000308: b510 push {r4, lr}
1000030a: f004 f871 bl 100043f0 <stdio_init_all>
1000030e: f004 f977 bl 10004600 <cyw43_arch_init>
10000312: 2800 cmp r0, #0
10000314: d10e bne.n 10000334 <main+0x2c>
10000316: 2101 movs r1, #1
10000318: 2000 movs r0, #0
1000031a: f004 f961 bl 100045e0 <cyw43_arch_gpio_put>
1000031e: 20fa movs r0, #250 @ 0xfa
10000320: f000 ffac bl 1000127c <sleep_ms>
10000324: 2100 movs r1, #0
10000326: 2000 movs r0, #0
10000328: f004 f95a bl 100045e0 <cyw43_arch_gpio_put>
1000032c: 20fa movs r0, #250 @ 0xfa
1000032e: f000 ffa5 bl 1000127c <sleep_ms>
10000332: e7f0 b.n 10000316 <main+0xe>
10000334: 4802 ldr r0, [pc, #8] @ (10000340 <main+0x38>)
10000336: f004 f84f bl 100043d8 <__wrap_printf>
1000033a: 2001 movs r0, #1
1000033c: 4240 negs r0, r0
1000033e: bd10 pop {r4, pc}
10000340: 10008348 .word 0x10008348
左側の8桁の16進数がメモリ上のアドレス、その右の4桁または4桁 x 2の16進数が機械語、その右側は機械語を人間が読みやすく書いたアセンブラ言語による記述になります。「b510」とか書かれてもなんのことか分からないですが、「push {r4, lr}」と書かれれば命令セットを知っていれば意味がわかります。命令セットについてはRP2040 Datasheetを参照すると、「2.4.3.3. Instruction set summary」にPicoのCPU(Cortex-M0+)がサポートしている命令セットの一覧とその説明が簡単に書かれています。この説明では簡単すぎて分かりにくいので、さらに詳しく知りたい場合はArmv6-M Architecture Reference ManualなどのARM本家のドキュメントを参照する必要があります。
Cortex-M0+の命令セットの種類はかなり少ない印象です。さすがRISCのCPUですね。CISCのIntel IA-32アーキテクチャのマニュアルを見ると5072ページもあり、命令セットも複雑です。
機械語のリストに話を戻しますと、C言語で書かれた順番にSDKで用意された関数を呼び出し(blでジャンプ)ています。関数の引数は第一引数をr0レジスタ、第二引数をr1レジスタに入れているようです(関数呼び出しの前に、movsで代入している)。関数からの返り値はr0レジスタに入っているようです(cyw43_arch_initの呼び出しの後、cmpでr0と#0を比較している)。
機械語でLチカ
次に、機械語を直接書いてLEDを点滅させてみます。
pico-exampleに新しいディレクトリを作成し、その下に機械語のプログラムなどを配置します。先ほどのC言語のプログラムpico_w/wifi/blinkの隣にasm_blinkを作成します。また、pico_w/wifi/blink/CMakeLists.txtをコピーして、内容を修正します。
% cd ~/pico/pico-example/pico_w/wifi
% mkdir asm_blink
% cd asm_blink
% cp ../blink/CMakeLists.txt .
% vi CMakeLists.txt
add_executable(asm_blink
main.S
)
target_link_libraries(asm_blink
pico_stdlib # for core functionality
pico_cyw43_arch_none # we need Wifi to access the GPIO, but we don't need anything else
)
# create map/bin/hex file etc.
pico_add_extra_outputs(asm_blink)
# add url via pico_set_program_url
example_auto_set_url(asm_blink)
上のように、CMakeLists.txtは”picow_blink”を”asm_blink”に置換し、”picow_blink.c”を”main.S”に置換します。
さらに機械語のプログラムmain.Sを作成します。
.equ SLEEP_TIME, 250
.syntax unified
.thumb_func
.global main
main: bl cyw43_arch_init
loop: movs r0, #0 // GPIO 0
movs r1, #1 // LED On
bl cyw43_arch_gpio_put
movs r0, #SLEEP_TIME
bl sleep_ms
movs r0, #0 // GPIO 0
movs r1, #0 // LED Off
bl cyw43_arch_gpio_put
movs r0, #SLEEP_TIME
bl sleep_ms
b loop
無線LANチップCYW43439を初期化後、GPIO 0のOn/Offを250msのsleepを挟みながら繰り返すというシンプルなプログラムにしました。
ファイルの準備はできましたので、ここからファームウェアの作成を行います。
% cd ~/pico/pico-examples
% rm -rf build (念のため、一旦buildディレクトリを削除)
% mkdir build; cd build
% cmake ..
% cd pico_w/wifi/asm_blink
% make
(省略...)
Building ASM object pico_w/wifi/asm_blink/CMakeFiles/asm_blink.dir/main.S.obj
Linking CXX executable asm_blink.elf
Built target asm_blink
出来上がったasm_blink.uf2をPicoに転送すると、LEDの点滅が始まります。
まとめ
ハードウェアの動作検証を兼ねて、MicroPythonによる開発、C言語による開発、機械語による開発の手順を確認しました。次は実用的なものの作成などに着手しようと思います。