📑

Rust で RP2350 からロータリーエンコーダーを使うために Embassy の PIO 関連を調べる

に公開

Embassy は、組み込み Rust で非同期処理を扱うためのフレームワークです。似たようなフレームワークとして RTIC がありますが、Embassy が RTIC と違うのは HAL(ハードウェア抽象化レイヤー)まで提供しているという点です。といってもすべてを独自実装しているわけではないですが、いろいろなものがラップされていてフレームワークと親和性があるかたちになっています。

たとえば、RP2350 関連は rp-rs が提供する crate を使っていて、PIO は以下の pio という名前の crate を使っています。

https://github.com/rp-rs/pio-rs

で、先週 Embassy を使ったときに行きがかり上 PIO も使ったんですが、「あれ、そういえば PIO のコードまったく書いてないけどなんで PIO を使えてるんだ?」と気になりました。

https://zenn.dev/yutannihilation/articles/812c1ed1f2357d

調べてみると、前回使った SPI 通信や PWM など、PIO を使うよくあるパターンはライブラリ側で用意してくれていることが分かりました。ロータリーエンコーダーもそのひとつです。

https://docs.embassy.dev/embassy-rp/git/rp235xa/pio_programs/index.html

実装を覗いてみる

ロータリーエンコーダーを使う前に、Embassy がこの機能をどう実装しているのか少し覗いてみましょう。実際のコードはここです。

https://github.com/embassy-rs/embassy/blob/main/embassy-rp/src/pio_programs/rotary_encoder.rs

具体的な PIO のコードだけ取り出すと、この4行です。

wait 1 pin 1
wait 0 pin 1
in pins, 2
push

そして、RX に書き出された値を読んでいます。

https://github.com/embassy-rs/embassy/blob/6034b17728b3528e42c8499a2893dc35d51d5590/embassy-rp/src/pio_programs/rotary_encoder.rs#L61-L65

これを読んで、「ラズパイピコ PIO ロータリーエンコーダー」とかで検索して出てくる実装と違いすぎてちょっと混乱していました。例えば、Raspberry Pi 公式の実装例はこうです。明らかに行数が違いますよね。なぜ...?

https://github.com/raspberrypi/pico-examples/blob/84e8d489ca321a4be90ee49e36dc29e5c645da08/pio/quadrature_encoder/quadrature_encoder.pio#L29-L86

これはなぜなのかというと、値のカウントまで PIO の中でやるのかどうかの違いでした。Embassy のは、「ロータリーエンコーダーが回った」ということを検知するためだけのもので、それに応じてカウンタの値を増やしたり減らしたりという処理は Rust のコード側でやるようになっています。例えば、Embassy のレポジトリにあるコード例ではこんな感じで使っています。

https://github.com/embassy-rs/embassy/blob/6034b17728b3528e42c8499a2893dc35d51d5590/examples/rp235x/src/bin/pio_rotary_encoder.rs#L18-L28

PIO のコードの意味

2相のロータリーエンコーダーは、回転させると位相が1/4周期ずれたパルス波が2つのピンから出るようになっていて、それがどちらにずれているかによって回転方向がわかります。例えば、秋月電子で売っているロータリーエンコーダー のデータシートには以下のような図が載っています。

これを踏まえて、上の PIO のコードをもう一度読んでみましょう(PIO の命令については詳しく説明しませんが、仕様はRP2350 のデータシートの 11.4 Instruction Set にあります)。最初の2行は、それぞれ以下のような意味です。つまり、矩形波の立ち下がりを検知しています。

wait 1 pin 1    // pin 1 が HIGH になるまで待機する
wait 0 pin 1    // pin 1 が LOW になるまで待機する

この時、pin 1 は必ず LOW です(なぜなら pin 1 が LOW になるのを待っているわけなので)。一方、pin 2 は位相がずれているので、回転方向によって HIGH か LOW かが異なります。それを調べるために、ピンの値を ISR に読み込み、RX FIFO に出力します。

in pins, 2   // pin の値を 2 つ ISR(input shift register)に書き込む
push         // ISR の値を RX FIFO に書き込む

知りたいのは pin 2 だけですが、たぶん in 命令の仕様的に一気に読むしかできないので pin 1 も読んでいます。ただ、上に書いたように pin 1 はかならず LOW なので、影響しません。わかりやすく書くと、ISR は以下のいずれかの値になります。

  • 01
  • 00

余談:RP2350 の PIO の新機能

ちなみに、RP2350 の PIO は、RP2040 より少しだけ機能強化されているみたいです。詳しくはデータシートの 11.1.1 Changes from RP2040 に書かれていますが、ひとつは、どの RX/TX FIFO を使うかを PIO のプログラムの中で決められるようになったことです。

https://github.com/rp-rs/pio-rs/pull/66

https://github.com/embassy-rs/embassy/pull/3369

上のコードで言うと、RX FIFO に書き込んでいたのはこの行でした。どの RX FIFO に書き込むかは PIO のプログラムを読み込むときに設定します。

push

これが、RP2350 だと、以下のような書き方で任意の RX FIFO に書き出すことができるようになったらしいです。添え字に Y レジスタを使って動的に変えることもできるみたいです。

mov rxfifo[0], isr

ただ、使いどころがまだピンと来ていません。いちおう Embassy では以下のようなコード例が示されているのですが、この場合はひとつの RX FIFO しか使ってなくてあまり意味がない例のような気がしています。たぶん、もっと複雑なケースだと便利なことがある気がするんですが、ネット上を検索しても使用例が見当たらずわかりません...

https://github.com/embassy-rs/embassy/blob/main/examples/rp235x/src/bin/pio_rotary_encoder_rxf.rs

embassy_rp::pio_programs::rotary_encoder の使い方

たぶんコードを読めばだいたいわかるので特に説明することはないのですが、LED の点滅間隔をロータリーエンコーダーでコントロールする例を書いてみました。GPIO 4ピン、GPIO 5ピンがロータリーエンコーダーとつながっている想定です。

https://github.com/yutannihilation/raspi-pico-2-practice/blob/0c354de55cd64f4db051b38a41341bc1279ff592/src/bin/rotary_encoder.rs#L33-L78

この例では、エンコーダーの処理は非同期のタスクとして立ち上げていて、メインタスクとは AtomicU32 で値を共有しています。このようにシンプルな値であれば Atomic* は no_std でも使えるので便利です。

もっと複雑なオブジェクトを共有する必要があれば、embassy-sync crate が ChannelMutex など様々なツールを提供しています。

https://crates.io/crates/embassy-sync

Discussion