プログラマブル IO
RP2xxx ファミリのマイクロコントローラは I2C、SPI、UART など標準的な通信プロトコルをハードウェアでサポートしています。ハードウェアでサポートされていないプロトコルや、カスタム化した I/O 動作が必要な場合は、プログラマブル IO (Programmable Input Output: PIO)が活躍します。また、MicroPython のアプリケーションの中には、データを送信するためにピンを高速でオン/オフするビットバンギングと呼ばれる技術を使用しているものがあります。これは、プロセッサが他のロジックを実行するよりもビットバンギングに集中するため、処理全体が遅くなる可能性があります。しかし、PIO を使えば、CPU がメインの作業を実行している間に、ビットバンギングをバックグラウンドで実行できます。
PIO ステートマシン
ステートマシンは、先入れ先出し (FIFO) バッファを使って他のエンティティとの間でデータを転送します。これにより、ステートマシンとメインプロセッサは独立して動作しながら、データを同期させることができます。FIFO を DMA にリンクすれば、CPU をあまり使わずに大量のデータを転送できます。
RP2040
RP2040 には2つの PIO ブロックがあり、それぞれが4つの独立したステートマシンを持っています(合計8つ)。
各 FIFO は、ステートマシンごと(TX と RX)に4ワード(各32ビット)を持ちます。
RP2350
RP2350 には3つの PIO ブロックがあり、それぞれが4つの独立したステートマシンを持っています(合計12個)。
RP2040 と同様に、各ステートマシンはステートマシンごと(TX と RX)に4ワードの FIFO を持ちますが、いくつか追加の機能があります
各ステートマシンについて:
TX または RX の FIFO を1つの8ワード FIFO に結合して、帯域幅を高められます。
RX FIFO は、そのレジスタへのランダムな読み書きアクセスをサポートします。
Compatibility
RP2350 の PIO は RP2040 と後方互換性があり、いくつかの機能強化が加えられています。
現時点では、RP2350 の PIO 拡張機能のネイティブサポートは非常に限られていますが、いくつかの機能については作業が進行中です
命令
各 PIO ブロックは、4つのステートマシンで共有される 32 個の命令メモリを持っています。
すべての PIO 命令は共通のパターンにしたがいます:
<instruction> .side(<side_set_value>) [<delay_value>]
サイドセット .side(...) と遅延 [...] の部分はいずれもオプションで、指定するとその命令で複数の処理を行えます。これにより、PIO のプログラムは小さく、効率的になります。
タスクを処理する命令には、以下の9つのものがあります:
jmp()は、コードの違う場所に制御を移しますwait()は、特定のアクションが発生するまで一時停止しますin_()は、ビット列をソース(スクラッチレジスタまたはピンのセット)から入力シフトレジスタにシフトしますout()は、ビット列を出力シフトレジスタからデスティネーションにシフトしますpush()は、RX FIFO にデータを送信しますpull()は、TX FIFO からデータを受信しますmov()は、データをソースからデスティネーションに移動しますirq()は、IRQ フラグを設定またはクリアしますset()は、リテラル値をデスティネーションに書き込みます
命令修飾子は次のものがあります:
.side()は、命令の開始時にサイドセットピンを設定します[]は、命令の実行後に特定のサイクル数だけ遅延します
ディレクティブもあります:
wrap_target()は、プログラムの実行をどこから続行するかを指定しますwrap()は、プログラムの制御フローをラップする命令を指定しますlabel()は、jmp()命令で使うラベルを設定しますword()は、プログラムの命令として機能する生の16ビット値を出力します
サンプルコード
PIO とステートマシンの使い方を簡単に理解するには、ソースに含まれるサンプルルコード pio_1hz.py を参照してください。以下に引用します。
# LED を点滅し、1Hz の IRQ を起こす PIO の使用例。
import time
from machine import Pin
import rp2
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink_1hz():
# サイクル: 1 + 1 + 6 + 32 * (30 + 1) = 1000
irq(rel(0))
set(pins, 1)
set(x, 31) [5]
label("delay_high")
nop() [29]
jmp(x_dec, "delay_high")
# サイクル: 1 + 1 + 6 + 32 * (30 + 1) = 1000
nop()
set(pins, 0)
set(x, 31) [5]
label("delay_low")
nop() [29]
jmp(x_dec, "delay_low")
# blink_1hz プログラムを持ち、出力に Pin(25) を使うステートマシンの作成。
sm = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(25))
# ミリ秒単位のタイムスタンプをプリントする IRQ ハンドラの設定。
sm.irq(lambda p: print(time.ticks_ms()))
# ステートマシンを始動。
sm.active(1)
これは、2000Hz で blink_1hz プログラムを実行し、ピン 25 に接続する、クラス rp2.StateMachine インスタンスを作成します。 blink_1hz プログラムは PIO を使って、このピンに接続されたLEDを 1Hz で点滅させ、LED がオンになると IRQ を起こします。このIRQ はミリ秒単位のタイムスタンプを出力する lambda 関数を呼び出します。
blink_1hz プログラムは PIO のアセンブラルーチンです。これは、出力として構成され、ローから始まる単一のピンに接続します。命令は次のとおりです:
irq(rel(0))は、ステートマシンに関連付けられた IRQ を起こします。LED は
set(pins, 1)命令により点灯します。値 31 をレジスタ X に入れ、その後に
[5]で指定したように、5サイクルだけ遅延します。nop() [29]命令は 30 サイクルだけ遅延します。jmp(x_dec, "delay_high")により、レジスタ X が 0 になるまでdelay_highラベルとの間をループします。この命令が実行された後、X は 1 だけ減ります。X は値31から始まるのでので、このジャンプは31回発生します。したがってnop() [29]が合計32回実行されます(32回のループのそれぞれでjmpによる1命令のサイクルもかかります)。単一の
nop()は IRQ の立ち上げに使われるサイクルと相関があり、LED の点灯と消灯に同じ数のサイクルが使われることを保証します。set(pins, 0)はピン 25 をローに設定することにより、LED をオフにします。次に
nop() [29]とjmp(...)の32回のループを実行します。wrap_target()とwrap()が指定されていないため、デフォルトが使用され、プログラムの実行は最下から最上に折り返されます。このラッピングに実行サイクルはかかりません。
ルーチン全体は、正確にステートマシンの 2000 サイクルかかります。ステートマシンの周波数を 2000Hz に設定すると LED が 1Hz で点滅します。
この例は RP2040 と RP2350 のどちらでも同じように動作します(ステートマシン ID 0 を使用)。RP2350 ではシステムクロックが速いため、より高い freq の値を指定できます。
互換性に関する注意
ここで示した基本命令を使った PIO プログラムは、RP2040 と RP2350 の間で完全な互換性があります。MicroPython の rp2 モジュール API ( @rp2.asm_pio や StateMachine などを含む) も同じですが、RP2350 ではステートマシン ID として 0〜11 が使えます。
RP2350 には、この入門では扱っていない高度な機能(新しい命令、FIFO モード、セキュリティ、GPIO の再配置)が追加されています。詳細は RP2350 のデータシートを参照してください。2つのチップではデフォルトの GPIO リセット状態が異なるため、プログラムによっては軽微な調整が必要になる場合があります。