SNSへはこちら

Raspberry Pi Pico(RP2040)のPIOについて備忘録(MicroPython)

追記および訂正

  • 2021/02/05: inout のピンのベース指定で、間違ったことを記述しておりました。
  • 2021/02/07: PIO はペリフェラルではありませんでした。
  • 2021/03/05: pushpull に於いて、FIFO 名の記述に誤りがありました。詳しくはコメントを参照。

ここから記事開始です。

色々いじっていて気付きましたが、ラズパイ Pico の PIO って最強の機能ですね。
実は早々と「Python はクソ!」と言い放って C/C++ でプログラミングをしたかったのですが、PIO が楽しすぎて一向に進む気配を見せません。環境構築すらしていないという。

この PIO ですが、ハッキリ言って初見だとデータシート類が難解だと思います。僕の読解力が著しく低い可能性はありますが。
そこで、本記事では自分がサラッとは分からなかった PIO の仕様(For MicroPython)に関して備忘録的に書いていきたいと思います。記述方法以外のところは pioasm でも共通だと思いますのでご参考にしていただければと。情報量と文章量が多いので、辞書的に使っていただくと良いと思います。

間違っていたりしたらコメントでこっそり教えて下さい。また、随時追記する可能性があります。また、動作させている MicroPython のバージョンは 20210121(v1.3) です。

参考

あと、次の出力を見ておくと幸せになれるかも(適宜改行しています)。

# インタプリタ上で実行しよう
>>> import rp2
>>> help(rp2)

object <module 'rp2'> is of type module
  Flash -- <class 'Flash'>
  PIOASMEmit -- <class 'PIOASMEmit'>
  const -- <function>
  asm_pio -- <function asm_pio at 0x20006c40>
  PIOASMError -- <class 'PIOASMError'>
  _pio_funcs -- {'in_': None, 'y_dec': 4, 'pin': 6, 'iffull': 64, 
'gpio': 0, 'not_osre': 7, 'clear': 64, 'rel': <function <lambda> at 0x200069d0>, 
'wrap': None, 'x_not_y': 5, 'word': None, 'out': None, 'push': None, 'noblock': 1, 'pull': None, 
'wrap_target': None, 'x_dec': 2, 'mov': None, 'irq': None, 'set': None, 'y': 2, 'x': 1, 
'null': 3, 'pc': 5, 'invert': <function <lambda> at 0x200066e0>, 'pins': 0, 'not_x': 1, 'not_y': 3, 
'ifempty': 64, 'isr': 6, 'pindirs': 4, 'exec': 8, 'label': None, 'status': 5, 'nop': None, 'osr': 7, 
'block': 33, 'reverse': <function <lambda> at 0x200066f0>, 'jmp': None, 'wait': None}
  __name__ -- rp2
  StateMachine -- <class 'StateMachine'>
  asm_pio_encode -- <function asm_pio_encode at 0x20006ac0>
  PIO -- <class 'PIO'>

...レジスタ名の実態はただの数だった!なるほどね。

PIOの構造(ざっくり)

PIO は CPU を介さずに BitBang が出来る機能。1つの「ステートマシン」にはレジスタと独自のプロセッサ(?)が搭載されているため、CPU と独立できるメリットがある。
このステートマシンは PIO ブロック1つに対して4つ存在する。このブロックが2つ存在するため、マイコン上には8つのステートマシンがあると考える。

PIO では GPIO のピン方向と、ピンの値が操作可能。

  • 原則1クロック1命令
    • WAIT 中など、ストールする命令は当然この限りではない。
  • 1つの PIO ステートマシンに対して、使えるメモリは 32 命令ぶん(0 ~ 31)。
    • pc == 31 になったらオーバーフローし(特にエラーも吐かずに)、0 からスタートする。
  • アクセス対象は PIO 内部の 32bit レジスタ、バッファと GPIO の値。
    • レジスタ:x, y, isr(Input Shift Register), osr(Output Shift, Register), pc, irq(IRQ フラグ)
    • バッファ:4バイト分の深さがある FIFO。isr, osr でアクセスする。
    • GPIO: pin, pins, pindir, pindirs
    • s が付くか付かないかなので、打ち間違えには要注意
  • wrap 機能
    • wrap_target()wrap() でコードを挟むと、この部分が無限ループになる(この wrap 機能自体ではプログラムメモリを消費しない。すなわち wrap を実現するためのレジスタが他にあるということ)。
    • もしこれを書かない場合は、書いたコード全体で無限ループになる。
    • 例えば命令を 10 個だけ書いて wrap しなかった場合は、0~9 の命令をループする。
  • sideset, delay
    • 命令の直後に .side(n) と置くと、ついでに sideset として指定したピンを操作できる。
    • 各命令の最後に [n] と置くと、命令実行後指定クロックだけ nop を行う。
    • いずれも限りある PIO のメモリ空間を有効に使うため。
    • ってかこの sideset はどこでも使えるので、寧ろこっちが(サイドと言うよりも)メインなのでは
  • TX FIFO, RX FIFO あり。
    • CPU とデータをやり取りするためにあると考えられる。FIFO は直接操作できず、前述の isr, osr を介する。
    • この isr, osr はそれぞれ後述の push, pull を用いることで、FIFO のデータを扱える(それ以外の方法もある。後述)。
    • 命名は恐らく CPU サイドから見たもの。PIO ステートマシンがデータを取り込む元は TX FIFO (レジスタは osr) だし、データを送り込む先は RX FIFO(レジスタは isr) です。

基本的な書き方

まず、書く場所は2箇所ある。それぞれについて説明。

  • 関数部:プログラム本体。@ から始まる独自のデコレータを付けて、引数空の関数として定義する。
    • まず Pico 特有のディレクティブでピンのモード・初期値を指定(sideset に関しては必ず指定しないと怒られる)。これはアセンブリプログラムが実行される直前に初期化される。
    • 関数内部ではアセンブリを行う。Python 仕様の関数が定義されているっぽいので、それを使う。
  • インスタンス部:上が関数定義だけだったのに対し、こちらは main 部。ここでインスタンス化を行う。
    • activate 関数で動作を司る。

例えばこんな感じで書く。

import rp2
from machine import Pin
import time

# ここでアセンブリ。直下の行がデコレータ。
@rp2.asm_pio(set_init=rp2.PIO.OUT_HIGH)
def blink():
    set(pins, 1)
    nop()
    set(pins, 0)
    nop()

# ここでインスタンス化
sm = rp2.StateMachine(0, blink, freq=2000, set_base=Pin(0))
sm.activate(1) # 実行!
time.sleep(3)
sm.activate(0) # 停止

詳しい解説・注意すべき点など

ここから、特にわかりにくいところを述べていきます(既に情報量が多いですが...)。

ステートマシンの初期化・デコレータについて

設定できる動作周波数の下限

CPU クロックは 125MHz。これを分周して PIO のステートマシンクロックとしている。どれだけ分周するかは内部の CLKDIV レジスタが担っており、どれだけ分周可能かはこのレジスタ仕様に依存している。

CLKDIV固定小数点で表されていて、16bit が整数部、8bit が小数部である。精度は 1/256。
よって約 65536 分周が最大。この時クロックは 約1,907Hz になっているので、これ以上周波数を下げることは不可。

仮に freq=1000 と指定しても、この 1,907Hz で動くよう。なので原則 2000Hz 以上で動作させるものと考えたほうが良い。

なんとか_baseの設定

まず、この base というのはベースアドレスを指定していることに注意が必要(データシートでは IO mapping と記載)。
例えば set_base=Pin(5) としたとき、set 命令の pins に関して、以下の対応関係がある。

bit pin
0 GP5(Pin 5) のピンの値
1 GP6
2 GP7
... ...
31 GP36(実在せず)

そういった点で、pins 等で参照する場合のベースとなるピン番号を与えていると考えるべき。

引数として以下を設定可能。inout は全く同じ名前の命令があるからこんがらがりやすいよね。

  • set_base: set 命令で pins, pindirs として参照可能なベース。
    • デコレータで初期設定を行わないと使えない。
  • sideset_base: sideset として使う。.side(n) と書く記法で。
    • デコレータで初期設定を行わないと実行時エラーになり、使えない。
  • in_base: mov 命令でソースオペランドとして pins を使う際に参照可能なベース。
    • in 命令のオペランドも該当。
    • デコレータでの初期設定は出来ない(入力端子として使うのが当たり前だから?)。
  • out_base: mov 命令で Dest オペランドとして pins を使う際に参照可能なベース。
    • out 命令のオペランドも該当。
    • デコレータで初期設定を行わないと使えない(出力として使うに決まっているが、初期値の設定が必要)。

      これらはオーバーラップして指定することも可能。注意すべきは使う命令によって pins あるいは pindirs の参照する先が異なる、ということ。また、wait 命令はマッピングに関係なく、GPIO 関連は GP0 から始まる絶対的な参照となる。

デコレータでの複数ピン設定

ここでは、例えば set の話をする。他も同様。また、ステートマシンのインスタンス部で set_base=Pin(5) としていると仮定する。

単純に1つのピンのみ初期値設定をしたければ

@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def f:
    ...

とそのまま書けば良い。これで GP5 が出力・初期値 Low となった。

複数ピンを設定したければ、

@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW, rp2.PIO.OUT_HIGH, rp2.PIO.IN_LOW))
def f:
    ...

と、タプルで渡せば良い。こうした場合は、

  • GP5 が出力・初期値 Low
  • GP6 が出力・初期値 Hi
  • GP7 が入力・(Low に関しては不明)

として設定される。

sidesetとdelay

命令セット表によると、この sideset の値を書き込むフィールドと delay を書き込むフィールドは共有されており、sideset にピンをいくつ使うかによって、delay の書ける範囲が変わってくる。
MicoPython ではデコレータ内で sideset のピン方向を初期化する際に、この sideset 数を判定しているよう。ただこれが若干妙だったので、メモとして載せておく。

sideset 数 delay 最大値 備考
0 31
1 7 ここで何故かいきなり減る
2 3
3 1
4 不可能
5 不可能
6 不可能 ビットフィールド幅の関係上、ここから先は正常動作しないと考える
7 不可能
8 不可能
9 不可能 実行時エラー

in命令

in は Python の予約語としてバッティングしているので、本命令は in_ という名前になっている。

jmp, movで使う特殊な記法

まずは jmp。データシートでは !X, X-- 等が用いられるが、MicroPython では以下の対応である。

Referenceでの記述 MicroPython
!X not_x
X-- x_dec
!Y not_y
Y-- y_dec
X!=Y x_not_y
!OSRE not_osre

続いて mov

Referenceでの記述 MicroPython
Invert 例: mov(pins, invert(x))
Bit Reverse 例: mov(pins, reverse(x))

FIFOとISR, OSRについて

FIFO はそのままでは使えない。データを持ってくるときには pull, データを送るときには push をせねばならない。isr, osr はこの命令を実行するためのレジスタで、(ほぼ)必ずこれらを操作する必要がある。

なお、ここでは autopush/autopull の説明は省略する。

メインのプログラム側でのデータのやり取り

FIFO はいわばメインのプログラムとデータをやり取りするのに必要なバッファである。メイン側ではステートマシン変数(インスタンスを sm とする)の関数を呼ぶことでデータを通信する。

データを送りたいときは sm.put(data) あるいは sm.put(data_array, length) とすると、PIO の RX FIFO に送られる。
データを受け取りたいときは sm.put(data) あるいは sm.put(data_array, length) とする。ただしブロッキング動作であり、PIO 側が osrpull しない限りメインの処理は止まる(多分ノンブロックの方法もある?)。

PIO側でのデータ送受信

データを受け取るときは pull() をすれば TX FIFO から1ワード(32bit)分が osr にコピーされる(このとき osr はゼロ初期化される)。これを mov 等を使って処理すれば良い。なお pull(noblock) と書けばノンブロッキングになる。

データを送るときは、予め mov 等で isr にデータをコピーした上で、 push() をすれば isr から RX FIFO にコピーされる(同様にこのとき isr はゼロ初期化される)。

To Be Described

まだ謎なことを書いておきます。調査でき次第記入します。

  • IN_HIGHIN_LOW の違いとは?
  • expression を書くことで加減算の実現は出来るのか?
  • IRQ が全般的に分かっていない
  • sm.put のノンブロックな方法

コメント

  1. […] Raspberry Pi Pico(RP2040)のPIOについて備忘録(MicroPython) […]

  2. より:

    初めまして。PIOの情報をまとめていただきありがとうございます。
    PIOを理解する上で大変参考になりました。
    余計なお世話でありますが、、記述が間違っているのでは?と思う点が
    ありコメントに書き込ませていただきます。
    ーーーー記事の内容ーーーーーーーーーーーーーーーーーーーーー
    データを受け取るときは pull() をすれば RX FIFO から1ワード(32bit)分が
    osr にコピーされる。
    データを送るときは、予め mov 等で isr にデータをコピーした上で、
    push() をすれば isr から TX FIFO にコピーされる
    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

    仕様書では以下となっており、レジスタ名とか違っているのでは?と
    思われます。

    仕様書(RP2040 Datasheet) P328 より
    ---------------------------------------------
    (PUT) PULL OUT
    -->[TxFIFO]--->[OSR]--->[Pin] 出力

    (GET) PUSH IN
    <--[RxFIFO]<---[ISR]<---[Pin]   入力

    (送信操作)
    [PULL]... remove a 32-bit word from the TX FIFO and
    place into the OSR.
    [OUT].... instructions shift data from the OSR to other destinations
    1,..,32 bits at a time.
    (受信操作)
    [IN].... shift 1,..,32 bits at a time into the register.
    [PUSH].... write the ISR contents to the RX FIFO.
    ---------------------------------------------
    自分の勘違いでしたらすみません。以上ご参考まで

    • shima-529 より:

      ご指摘ありがとうございます!

      仰せの通り、こちらの記述(TX/RX)が逆になっていますね...書いている時も、時折逆に考えることがあったのですがここは間違ったまま記事にしてしまいました。

      そもそも記事本文中に「(TX FIFO/RX FIFOの)命名は恐らく CPU サイドから見たもの。」と自分で書いたのですが、ここだけそのとおりになっていませんね。訂正いたします。