はじめに
Raspberry Pi Picoに搭載されているPIO(Programable I/O)は、精密なGPIOの制御ができる専用のハードウェアモジュール。CPUの処理負荷を使わずに、複雑なタイミングの信号生成や、特殊なプロトコルの実装をできるため、柔軟な I/O を必要とする場面で非常に便利な機能。PIOはC/C++でもMicroPythonどちらでも利用可能。本記事では、MicroPythonを中心にまとめる。
-
C/C++の場合
*.pioにPIO制御用のアセンブラを記述すると、Build時に*.pio.hのCファイルに変換され、Cから関数としてコールすることができるようになる。公式ドキュメントは下記の「Chapter 3. Using programmable I/O (PIO)」参照。
Raspberry Pi Pico-series C/C++ SDK
-
MicroPythonの場合
import rp2でライブラリをインポートすると、*.pyファイル内に直接アセンブラ記述が可能。公式ドキュメントは下記の「3.9. PIO Support」参照。
目次
PIOの仕組み
PIOの構成をまとめる。詳細はデータシート参照。RP2040がPico1でPR2350がPico2。
RP-008371-DS-1-rp2040-datasheet.pdf
RP-008373-DS-2-rp2350-datasheet.pdf
-
HW構造
- PIOが2ブロック(Pico2 は3ブロック)
- 4つのステートマシーンと各ステートマシーンごとに32bitのTx/RxのFIFO
- 4つのステートマシン共有の32命令分のメモリ
- FIFOはCPUやDMAでデータのやり取りができるバッファ
-
機能
- クロック分周
クロック分周器でステートマシーンの処理速度を調整する。 - OSR(Output Shift Register)
Tx FIFO→OSR→Pinへ1bitずつH/L出力 - ISR(Input Shift Register)
Pinの1bitずつH/L入力→ISR→RX FIFO
- クロック分周
アセンブラ命令
アセンブラの命令は9つ準備され、pin/pins/x/y/[delay]と組み合わせて使う。
- pin/pins
ステートマシーンで設定するin_base,out_baseで決まる。pinはin_baseで設定したpinで、pinsはそこから連続するpin数を指定する。
- x/y
32bitの汎用レジスタでPIOの中で値を保持や比較、ループカウンタに使うためにある。2種類存在する。
- [delay]
各命令の後のdelayを指定する。例えば set(pins,1) [5] とすると、setのあとに5delayかけるという意味。delayは5bit表現で最大[31]まで設定できる。
| 命令 | MicroPython 記法 | 概要 |
|---|---|---|
| JMP | jmp("label") jmp(x_dec, "label") jmp(not_x, "label") |
条件付きジャンプ。x_dec は X-- して 0 でなければジャンプ。not_x は x==0 ならジャンプ。 |
| WAIT | wait(1, pin, 0) wait(0, irq, 3) |
Pin または IRQ の状態待ち。wait(1, pin, 0) は GPIO0 が High になるまで待つ。 |
| IN | in_(pins, 1) | Pin やレジスタの値を ISR に取り込む。in_(pins, N) は Nbit の GPIO を読む。 |
| OUT | out(pins, 1) out(x, 1) |
OSR のビットを Pin やレジスタへ出力。out(pins, N) は Nbit を GPIO に出す。 |
| PUSH | push() push(block) |
ISR → RX FIFO へ転送。block 付きは FIFO が空くまで待つ。 |
| PULL | pull() pull(block) |
TX FIFO → OSR へ転送。block 付きは FIFO が空なら停止。 |
| MOV | mov(x, y) mov(pins, x) mov(x, invert(x)) |
レジスタ間コピー、反転、ビット操作。invert(x) は ~x と同じ。 |
| IRQ | irq(0) irq(block, 1) |
IRQ フラグのセット/クリア/待ち。irq(block, n) は IRQ n がセットされるまで待つ。 |
| SET | set(pins, 1) set(x, 0) set(pindirs, 1) |
即値を Pin やレジスタへ書き込み。set(pindirs, 1) は GPIO を出力に設定。 |
PIOの設定
PIOの制御はデコレータで記載し、StateMachine() → .active(1) で設定駆動させる。デコレータについてはPythonのデコレーター #Python - Qiitaを参照。
ざっくり下記のような感じになる。
from machine import Pin
import rp2
from rp2 import StateMachine
# デコレータで記述
@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW))
def pio_test():
#-------------------
#ここにアセンブラを書く
#-------------------
# StateMachine0 を 2MHzでGPIO0を起点に pio_test()で設定
sm = StateMachine(0, pio_test,freq=2_000_000,set_base=Pin(0))
sm.active(1)
PIOを使った例
GPIO0/1/2/3/4をPIOで制御する例を示す。GPIO0/1/2/3 をH/L出力させる。GPIO4は入力で値を32bit単位でRxFIFOに詰める例。
from machine import Pin
import rp2
from rp2 import StateMachine
import time
# GPIO 0,1,2,3 を設定 set_initはtapleで使うポート分列挙する
@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW))
def pio_toggle():
# wrap_target()→wrap()でループし続ける。
wrap_target()
# GPIO 0,1,2,3の値を同時に設定できる。
set(pins, 0b0001)
set(pins, 0b0010) [1]
set(pins, 0b0100) [2]
set(pins, 0b1000) [3]
set(pins, 0b0000) [4]
set(pins, 0b1111) [6] # GPIO0,1,2,3をHにして6 delayする
set(pins, 0b0000)
wrap()
# GPIO 4に32bit ISRにたまったら、自動的に RxFIFOにpushするという設定
@rp2.asm_pio(in_shiftdir=rp2.PIO.SHIFT_LEFT, autopush=True, push_thresh=32)
def pio_read():
wrap_target()
in_(pins, 1)
wrap()
# --- 出力側 SM0 --- 2MHz GPIO 0基点
sm0 = StateMachine(0, pio_toggle,freq=2_000_000,set_base=Pin(0))
# --- 入力側 SM1 --- 2MHz GPIO 4基点
sm1 = StateMachine(1, pio_read,freq=2_000_000,in_base=Pin(4))
sm0.active(1)
sm1.active(1)
# --- 読み取りループ ---
while True:
if sm1.rx_fifo():
v = sm1.get()
print(f'{v:08X}')
この例の結果をロジアナでとると👇OUT方向のGPIO0,1,2,3は下記の波形となる。H/Lの区間がぴったり制御できる。
PIOを使えばどんな複雑な波形でも記述できるので、いろいろな用途に使えそう。またIN方向については、今回DMAを使っていないので、CPUの取りこぼしが出ると思うが、DMAと組み合わせてあげれば取りこぼしなくRAMにポートの状態をコピーし続けることができるので、Picoでロジックアナライザを作ることもできると思う。


