この記事でやること
- ArduinoIDEでPIO開発の始め方の解説
- コピペしたらPIOでLチカするコードの紹介
はじめに
RP2040(Raspberry Pi Pico)にはチップ内にPIO機能(プログラマブルIO機能)があります
PIOはポート操作を中心としたごく限られた単純な処理がメインプロセッサの処理とは独立して実行でき、かつクロックサイクルに忠実で高速に処理できるため強力だ、というのは発売当初からよく言われてきましたが、残念なことに ArduinoIDEからRP2040を使っているユーザーからするとやや使いづらい 側面があります
- アセンブリで記述しなければいけない(最低限文法を知らないと始められない)
- アセンブリをアセンブルしなければいけない
- CやMicroPythonのやり方は調べれば出てくるがArduinoIDEでのやり方に言及した記事がない
とはいえやる気の問題なので根気よく調べれば解決できるんですが、自分はしばらく重い腰が上がらなかったところ、今更になってPIOに入門したのでその経験をもとに本記事を書きます
本記事の対象者
本記事は次のような人に向けたものです
- Lチカ感覚のコピペで動かしたい (Arduinoのサンプルコードの感覚で)
- 動かしてからイジりながら勉強したいので、最低限のコードが欲しい
- ArduinoIDEでPIOをやる方法が知りたい
ただし、本記事を読んだ後、Lチカの次のステップに進むには
- RP2040向けのアセンブリの理解は必要
- プログラムの規模が中程度以上なら本記事の邪道な方法ではなく正攻法にヘッダファイルをinludeする方法が望ましい
ので、ターゲット層としてはArduinoIDEでそこそこ趣味開発はやってきたけどPIO未経験で、ただ最初にLチカをしたいというかなり狭い層に向けている記事になります
動作を確認した環境
- Windows11
- ArduinoIDE 1.8.19
- earlephilhower版RP2040ボードライブラリ
- Raspberry Pi Pico
- Wokwi pioasm Online(アセンブリのアセンブルサイト)
コード
以下のコードをArduinoIDEにコピペして、ボード選択をRaspberry Pi Picoにして書き込むとボード上のLEDが点滅すると思います
(ただし、体感10%ぐらいの明るさで点灯しているように見えるはずです)
アセンブリ(Arduinoコード内に埋め込み済み)
.program el_chika
.wrap_target
set pins, 1
set pins, 0
.wrap
Arduinoコード
#include <hardware/pio.h>
#define el_chika_wrap_target 0
#define el_chika_wrap 1
static const uint16_t el_chika_program_instructions[] = {
// .wrap_target
0xe001, // 0: set pins, 1
0xe000, // 1: set pins, 0
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program el_chika_program = {
.instructions = el_chika_program_instructions,
.length = 2,
.origin = -1,
};
static inline pio_sm_config el_chika_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + el_chika_wrap_target, offset + el_chika_wrap);
return c;
}
static inline void el_chika_program_init(PIO pio, uint sm, uint offset, uint pin) {
pio_sm_config c = el_chika_program_get_default_config(offset);
sm_config_set_set_pins(&c, pin, 1);
pio_gpio_init(pio, pin);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
#endif
void setup() {
pinMode(16, OUTPUT);
delay(1000);
el_chika_program_init(
pio0, //pio: pio0 or pio1
0, //sm: state-machine 0,1,2,3
pio_add_program(pio0, &el_chika_program), //offset: program address
PICO_DEFAULT_LED_PIN //pin: head GPIO pin number
);
el_chika_program_init(
pio0, //pio: pio0 or pio1
1, //sm: state-machine 0,1,2,3
pio_add_program(pio0, &el_chika_program), //offset: program address
15 //pin: head GPIO pin number
);
}
void loop() {
digitalWrite(16, HIGH);
digitalWrite(16, LOW);
}
わかる人からするとなんとなく奇妙な記述があると思いますが、本来ヘッダに記述すべきものをそのままワンファイルで実行可能にすべく.inoファイルに移植する形にしているため、動作に影響のない範囲でそのままにしてあります
最初に理解してほしいのは、PIOの動作はアセンブリで記述すると言いつつも、結局は アセンブリの実体部分が static const uint16_t el_chika_program_instructions[]
の配列形式で Cのコードの一部として組み込まれ 、一緒にコンパイルされているという点です
しかも、マイコンが起動してからのsetup()
関数内の処理でアセンブリコードを読み込ませることと、割り当てるピンの指定、実行指示を順に実行していく形なので実行の機序についても、 あくまでCのコードが支配している ということが理解できると思います
コードの解説
static const uint16_t el_chika_program_instructions[]
アセンブリの内容を16bit符号なし整数の配列で記述します
コメントの// 0: set pins, 1
はアセンブル前のアセンブリの記述です
0行目は pins
を1にする という処理で、今回set
命令に割り当てるポートを1つにしているので1bit分の記述になっており、ポートが複数ある場合は2進数で 0b101
のような指定をします
ポート番号はアセンブリ側では記述せず、C側でPIOを初期化する際にポート番号を渡します
処理は .wrap
に達すると自動的に .wrap_target
に戻るようにアセンブリを記述しますが、アセンブルされた配列にはコメントが残るだけです
static inline void el_chika_program_init(PIO pio, uint sm, uint offset, uint pin)
実行したいアセンブリコードの内容と、それを動作させるステートマシン、割り当てるGPIO番号を指定します
PIO関連の初期化をまとめた関数になるので、紹介する記事によって引数や処理内容が異なる場合があります
今回は説明のため動作に不要な処理(複数のポートを想定した分岐処理など)は記述していませんので、このコードを流用する場合は高確率で書き換える必要があります
これをsetup()
関数で2回呼び出していますが、異なるステートマシン番号に動作させ、異なるGPIO番号に出力するように指示しています
ここでは詳細な説明は割愛し、公式ドキュメントや雑誌Interface2021年8月号第2部第1章/第2章、後述するアセンブルサービスのサイトなどを利用してください
動作の確認
上記コードでは3つのポートが操作されています
- PIOでLED(ポート25)が2サイクル周期でON/OFF(点滅)する
- PIOでポート15が2サイクル周期でON/OFFする
- loop()関数でポート16がON/OFFする
LEDは書き込みが正常に成功しているかどうかを視覚的に確認するためのものです
15番と16番の端子をオシロスコープで測定します
ただし、動作クロックが標準の133MHzの場合、LEDの点滅は2サイクルで1周期となるので66MHzで点滅する動作となります
Duty比の理論値は50%ですが、マイコンの電気的能力が間に合わないのでLEDは10%ぐらいの明るさで点灯しているように見えると思います
オシロスコープで確認
意図したとおりに動作しているのか、端子の電位をオシロスコープで確認します
- イエロー:
digitalWrite(16, HIGH/LOW)
をloop()
内で実行している様子 - マゼンタ:PIOで
set pins, 1/0
を実行している様子
イエロー:digitalWrite() | マゼンタ:PIO | |
---|---|---|
周期 | 1620ns | 15ns |
HIGH時間 | 610ns | 7ns |
LOW時間 | 1010ns | 7ns |
周波数 | 0.613MHz | 66.5MHz |
まずマゼンタの PIOが66MHzの速度が出ています
設定クロックが133MHzで、2サイクルでHIGH/LOWこなすので 設定クロックの半分の周波数 で動作することがわかります
電位については本来0V~3.3Vとなってほしいところ、LOWで1V、HIGHで2V程度と狭いレンジでの振動になってます
これはマイコンのGPIOの駆動能力(電気的性能)に対してボードに発生する容量や繋いだオシロスコープのプローブの容量などが大きく、定常状態になる前の過渡応答の初期段階で出力が切り替わってしまっていると理解していいでしょう
GPIOポート15で測定していますが、全く同じ処理を基板上のLEDでも実行しているため、LEDは制御上理想的なデューティー比50%であっても、実態として中途半端に振動している電位によって光っている状態であると推測されます
次にイエローのdigitalWrite()
制御については、矩形波の形としてははっきりとしていますが PIOと比べると1周期に100倍の時間がかかって おり、 プロセッサとPIOではポート制御に要する時間の差が歴然 です
大きな矩形波とは別で細かく電位が上下しているのはPIOで制御されたポートの影響を受けて上下しているものと推測されます
HIGHよりもLOWの時間が長くなっているのは主にloop()
の最後まで到達して先頭に戻る処理に時間を要していると推測されます
アセンブリをアセンブルする方法
ArduinoIDEでコンパイル・書き込みをする場合、PIOのアセンブリを同時にアセンブルする方法がありません(厳密には不可能ではないと思いますが・・・)し、そもそもIDEとして.pio拡張子のファイルをタブに入れることができません
ここでは正攻法のpioasm.exeを使う方法と、ブラウザアプリの2つを紹介しますが、個人的には後者のブラウザの方をおすすめします
pioasm.exeを使う方法
C環境でのPIO開発環境に関する多くの記事では公式SDKに含まれるpioasmを使ってアセンブルする方法が紹介されてますが、それだけのためにC環境を構築して pioasm.exe
を用意するのも面倒だなと思ってしまいます
ただしArduino環境を構築し、(earlephilhower版の)RP2040ボードライブラリを導入していれば、ライブラリの格納されているフォルダ内に pioasm.exe
が実行ファイル形式で用意されています
Windowsの場合(AppDataは隠しフォルダです)
C:\Users\{ユーザー名}\AppData\Local\Arduino15\packages\rp2040\tools\pqt-pioasm\2.1.0-a-d3d2e6\pioasm.exe
GUIアプリではないのでダブルクリックしても.pioファイルの指定ができないので、ターミナルで実行します
実行ファイルは単体でコピーして任意のディレクトリに配置しても動くので、.pioファイルの近くに配置しても構いません
その上で
>pioasm.exe el_chika.pio el_chika.pio.h
(アセンブルしたい.pioファイル名と新規に作成したい.hファイル名を渡す)
とすると、.pio.hファイルが生成されます(エクスプローラに変化がない場合はF5で更新すると出ます)
ファイルに出力される内容は下の方法と同じです
ブラウザアプリを使う方法
一方でブラウザで動作して逐次アセンブルしながらエラーなどを出してくれるサイトがあるので、こちらを使ったほうが試行錯誤しやすいかと思います
Wokwi pioasm Online(アセンブリのアセンブルサイト)
(逐次アセンブル処理はブラウザ上で実行されるサービスです)
左ペインのエディタでアセンブリを記述・変更すると逐次右ペインにアセンブル結果が出力され、エラーも瞬時に出ます
処理の有無を比較したい場合などはアセンブリの行頭に ;(セミコロン)
を書いたり消したりすることで右ペインの変化を観察することができ、文法を理解しやすくなります
本来、右ペインの内容をそのままヘッダファイルにしてコンパイル内容に組み入れるものですが、本記事では #define
~ #endif
の部分をそのまま.inoファイルに突っ込んで動かしています
サンプル左ペインの % c-sdk {~}
内容はアセンブリとして解釈せずそのままヘッダファイルに移植されることになっているので、ヘッダファイルに書いておきたいPIOの初期化処理をCの文法で記述する意図で用意されているものであり、本記事の方法では使用しません
本記事のアセンブリを左ペインに貼り付けると下画像のようになり、右ペインの #define
以降をプログラム本体に組み入れています
そのほか
PIO開発にオシロスコープは必要なのか
特に初心者はオシロスコープを使うべきだと思います
メインプログラムのようにシリアル通信へデバッグメッセージを流すことは簡単ではないですし、特にポートの高速制御に威力を発揮する処理を作る場合や、動作保証をしたい場合は必須と言えるでしょう
とはいえ、オシロスコープを持っていないからといってせっかく興味を持ったPIOに手を出さないのはもったいないですので、ほかの方法で工夫して開発することもできると思います
PIOのシミュレータもあるので、ある程度はそれで代用することもできます(私は動作未確認です)
参考記事
何よりも公式ドキュメントを読みましょう、と言いたいところですが自分はPIOに関して公式を読んでないのでちょっと強くは言えないです
- Interface 2021年8月号
- 第2部第1章(PIOの命令などが解説されているがサンプルコードがやや難しい)
- 第2部第2章(サンプルコードが比較的シンプル)
- Interface 2022年10月号
- 特設第3章(PIOエミュレータの解説を通してPIOアセンブリの説明がある)
最後に
自分自身、PIOについては勉強しながら記事を書いていますので勘違いや誤りがある可能性があります
気づいた点はご指摘いただければと思います