プログラマブル IO

RP2040 は I2C、SPI、UART など標準的な通信プロトコルをハードウェアでサポートしています。ハードウェアでサポートされていないプロトコルや、カスタム化した I/O 動作が必要な場合は、プログラマブル IO (Programmable Input Output: PIO)が活躍します。また、MicroPython のアプリケーションの中には、データを送信するためにピンを高速でオン/オフするビットバンギングと呼ばれる技術を使用しているものがあります。これは、プロセッサが他のロジックを実行するよりもビットバンギングに集中するため、処理全体が遅くなる可能性があります。しかし、PIO は、CPU がメインの作業を実行している間に、ビットバンギングをバックグラウンドで行えます。

RP2040 には、2つの Cortex-M0+ プロセッシングコアに加えて、2つの PIO ブロックがあり、それぞれが4つの独立したステートマシンを持っています。これらのステートマシンは、先入れ先出し (FIFO) バッファを使って、他のエンティティとの間でデータを転送できます。これにより、ステートマシンとメインプロセッサは独立して動作しながら、データを同期させることができます。各 FIFO には4ワード(各32ビット)があり、これを DMA にリンクすることで大容量のデータを転送できます。

すべての 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 + 7 + 32 * (30 + 1) = 1000
    set(pins, 0)
    set(x, 31)                  [6]
    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命令のサイクルもかかります)。
  • set(pins, 0) はピン 25 をローに設定することにより、LED をオフにします。
  • 次に nop() [29]jmp(...) の32回のループを実行します。
  • wrap_target()wrap() が指定されていないため、デフォルトが使用され、プログラムの実行は最下から最上に折り返されます。このラッピングに実行サイクルはかかりません。

ルーチン全体は、正確にステートマシンの 2000 サイクルかかります。ステートマシンの周波数を 2000Hz に設定すると LED が 1Hz で点滅します。