Raspberry Pi PicoのPIOのコードをC++で実行時に動的に生成する
Raspberry Pi PicoのProgrammable I/Oを使うには、.pio
ファイルに pioasm のアセンブリを書き、コンパイル時にCMakeのpico_generate_pio_header
を使ってCから読み込めるバイナリを生成します。
これ、出力されるのは uint16_t
の配列と、各種設定用の関数/構造体だけなので、実行時に同様のデータを出力できれば動的にpioのコードを変更できます。
MicroPythonでpio使うときとかそんな感じの挙動してますね。
今回は、動的にpioのバイナリを生成するライブラリを作ってみた話。
ライブラリ本体
Write RP2040 Programmable I/O assembly with C++. Contribute to Hiroki-Kawakami/cpp-pioasm development by creating an account on GitHub.
使い方
pioasm::builder
を継承したクラスを作るvoid code()
を override してその中に pio のコードを書く- 必要に応じて
init
などの関数を追加する pioasm::builder::program()
でプログラムを取得し、pio_add_program
で読み込む
あとは通常のpioasmの操作方法と同じです。
struct uart_tx_program_builder : pioasm::builder {
void code() {
side_set(1, opt) ;
PULL() .side(1) [7] ;
SET(x, 7) .side(0) [7] ;
label("bitloop") ;
OUT(pins, 1) ;
JMP(x--, "bitloop") [6] ;
}
void init(PIO pio, uint sm, uint offset, uint pin_tx, uint baud) {
// % c-sdk 内に書くようなコードは、メンバ関数として追加しておくといいと思う
// 通常pioasmで生成される `pio_sm_config` を取得する関数は、`get_default_config` メンバ関数を代わりに使う
pio_sm_config c = get_default_config(offset);
// 以下残りの初期化処理
}
};
int main() {
uart_tx_program_builder uart_tx_program;
PIO pio = pio0;
int sm = 0;
// uart_tx_program.program() で取得したポインタを `pio_add_program` に渡してプログラムを読み込む
// uart_tx_program をローカル変数に定義している場合は、変数の生存期間に注意
// uart_tx_program が解放されると、uart_tx_program.program()で取得したポインタも使えなくなる
uint offset = pio_add_program(pio, uart_tx_program.program());
uart_tx_program.init(pio, sm, offset, PIN_TX, SERIAL_BAUD);
while (true) {
// pioにデータを送ったりする処理
}
}
void code()
内は、オリジナルのpioasm
の見た目ほぼ同じまま書けるように頑張った(つもり)。
詳しくは README を見て欲しいのだけど、簡単に説明すると、
side_set
offset
ディレクティブはcode()
内の先頭に記述- 命令は全部大文字で、関数を呼び出す。
NOP()
JMP(...)
など - 関数の引数にシンボル
x
y
x!=y
!osre
を渡す際は全部小文字 MOV
命令の Bit-reverse::
は、C++で::
演算子をオーバーロードできない関係上、代わりに*
を使うside
は、命令の関数を呼び出した戻り値に対してside(uint8_t data)を呼ぶ
- delayは、命令の関数 or
side(uint8_t data)
の呼び出しの戻り値に対して[delay]
の添字演算子を適応する - ラベルは
label("ラベル名");
を使用。変数の値を引数に渡すことももちろん可能。 - JMPで使うときは、JMPの引数にそのまま文字列を渡せばいい。
wrap_target
wrap
は同名の関数を呼ぶ
こんな感じ。
exampleとかも用意しているので、あとは勘でなんとかしてもらえれば。
cpp-pioasm/examples at main · Hiroki-Kawakami/cpp-pioasm
色々な補足事項
動作確認やテストはあまりしていない
出力結果がおかしくなるかも。トラブったら普通のpioasmコンパイラで同じコードを吐かせてみて、uint16_t *data()
でとれるバイト列と一致してるか確認するのがいいかも。
その際、CMakeプロジェクトを用意するのは面倒なので、以下のようなWebブラウザで動くpioasmコンパイラがおすすめ
Compiled Raspberry Pi Pico PIO programs in your web-browser (pioasm for rp2040)
エラーチェックも特にない
- エラーメッセージは特に出さない。組み込みは常に標準出力が使えるわけじゃないし、その考慮が面倒だった。
side_set
offset
がcode()
の途中にあってもエラーにならない。途中からコードの生成方法が変わっておかしくなるだけ。- GPIO番号とかsideの値とかdelayの値とか、範囲を超えていてもエラーにならない。使えるビットの範囲を超えて値を書き込んでおかしくなるだけ。
side_set
がopt
じゃないときにside
の呼び出しがなくてもエラーにならない。0が指定されたことになる。wrap_target
wrap
を複数回呼んでもエラーは出さない。後勝ち。
この辺り考えるのが面倒だった。 リソースを節約するためにこうした。
なんで命令の関数だけ大文字?
当初、全部小文字で作ろうとしたんだけど、irq
というシンボルが命令として使われたのかWAIT命令の引数として使われたのか区別させるのがものすごく大変だったので、大文字/小文字分けて区別できるようにした。
命令じゃなくてシンボルの方を大文字にしようとも考えたんだけど、NULL
という名前が衝突して無理だった。
constexpr
でコンパイル時にバイナリを生成はできない?
無理。そもそも動的にpioasmをビルドすることが目的なので、constexpr で扱えるようにすることは考えてない。
余談
自分はArduino Frameworkを使わない派。ESP32を使うときはIDFだし、Raspberry Pi Picoを使うときも公式のC/C++ SDKを使う。
ちょっと凝ったことをしようとした時に、Arduinoだと思い通りにならないことが多いから。
だけど、ユーザー数で言うと Arduino 使ってる人が多いだろうから、今回 example を提示するにあたって Arduino で書いてみたんだけど、C++17 すら使えないのが驚き。
最近は C++20 対応してないと辛いと感じるようになっているので、とても使いづらかった。
コメント