Ideal Reality

興味の赴くままに

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のバイナリを生成するライブラリを作ってみた話。

Contents
スポンサーリンク

ライブラリ本体

GitHub - Hiroki-Kawakami/cpp-pioasm: Write RP2040 Programmable I/O assembly with C++

Write RP2040 Programmable I/O assembly with C++. Contribute to Hiroki-Kawakami/cpp-pioasm development by creating an account on GitHub.

使い方

  1. pioasm::builderを継承したクラスを作る
  2. void code() を override してその中に pio のコードを書く
  3. 必要に応じて init などの関数を追加する
  4. 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コンパイラがおすすめ

pioasm Online | Wokwi

Compiled Raspberry Pi Pico PIO programs in your web-browser (pioasm for rp2040)

エラーチェックも特にない

  • エラーメッセージは特に出さない。組み込みは常に標準出力が使えるわけじゃないし、その考慮が面倒だった。
  • side_set offsetcode() の途中にあってもエラーにならない。途中からコードの生成方法が変わっておかしくなるだけ。
  • GPIO番号とかsideの値とかdelayの値とか、範囲を超えていてもエラーにならない。使えるビットの範囲を超えて値を書き込んでおかしくなるだけ。
  • side_setopt じゃないときに 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 対応してないと辛いと感じるようになっているので、とても使いづらかった。

スポンサーリンク

コメント

投稿されたコメントはありません

名前

メールアドレス(任意)

コメント

関連する投稿

JNIでKotlinからC++のクラスを利用する

NO IMAGE

[C++]文字列を任意の文字列で分割する