追記および訂正
- 2021/02/05:
in
とout
のピンのベース指定で、間違ったことを記述しておりました。 - 2021/02/07: PIO はペリフェラルではありませんでした。
- 2021/03/05:
push
とpull
に於いて、FIFO 名の記述に誤りがありました。詳しくはコメントを参照。
ここから記事開始です。
色々いじっていて気付きましたが、ラズパイ Pico の PIO って最強の機能ですね。
実は早々と「Python はクソ!」と言い放って C/C++ でプログラミングをしたかったのですが、PIO が楽しすぎて一向に進む気配を見せません。環境構築すらしていないという。
この PIO ですが、ハッキリ言って初見だとデータシート類が難解だと思います。僕の読解力が著しく低い可能性はありますが。
そこで、本記事では自分がサラッとは分からなかった PIO の仕様(For MicroPython)に関して備忘録的に書いていきたいと思います。記述方法以外のところは pioasm でも共通だと思いますのでご参考にしていただければと。情報量と文章量が多いので、辞書的に使っていただくと良いと思います。
間違っていたりしたらコメントでこっそり教えて下さい。また、随時追記する可能性があります。また、動作させている MicroPython のバージョンは 20210121(v1.3) です。
参考
- Raspberry Pico Python SDK Documentation
- MicroPython での記述法が(一応)書いてある。
- でもあんまり説明ないからわからんね。
- RP2040 Datasheet
- PIO の命令セットとか一通りある。仕様とか理解するのに大いに役立つ。
- でも MicroPython の書き方は書いてないね。
- Raspberry Pi Picoの仕様書を読んでみる(3) – 楽しくやろう。
- めちゃめちゃわかりやすいです。感謝。
- PicoのNeoPixel(WS2812)のサンプルを動かしてみた – 楽しくやろう。
- 理解を深めるために、実例を見たほうが良いと思います。僕も最初分からなかったときはこの記事にお世話になりました。
あと、次の出力を見ておくと幸せになれるかも(適宜改行しています)。
# インタプリタ上で実行しよう
>>> 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
) です。
- CPU とデータをやり取りするためにあると考えられる。FIFO は直接操作できず、前述の
基本的な書き方
まず、書く場所は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
等で参照する場合のベースとなるピン番号を与えていると考えるべき。
引数として以下を設定可能。in
と out
は全く同じ名前の命令があるからこんがらがりやすいよね。
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 側が osr
に pull
しない限りメインの処理は止まる(多分ノンブロックの方法もある?)。
PIO側でのデータ送受信
データを受け取るときは pull()
をすれば TX FIFO から1ワード(32bit)分が osr
にコピーされる(このとき osr
はゼロ初期化される)。これを mov
等を使って処理すれば良い。なお pull(noblock)
と書けばノンブロッキングになる。
データを送るときは、予め mov
等で isr
にデータをコピーした上で、 push()
をすれば isr
から RX FIFO にコピーされる(同様にこのとき isr
はゼロ初期化される)。
To Be Described
まだ謎なことを書いておきます。調査でき次第記入します。
IN_HIGH
とIN_LOW
の違いとは?expression を書くことで加減算の実現は出来るのか?- IRQ が全般的に分かっていない
sm.put
のノンブロックな方法
コメント
[…] Raspberry Pi Pico(RP2040)のPIOについて備忘録(MicroPython) […]
初めまして。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.
---------------------------------------------
自分の勘違いでしたらすみません。以上ご参考まで
ご指摘ありがとうございます!
仰せの通り、こちらの記述(TX/RX)が逆になっていますね...書いている時も、時折逆に考えることがあったのですがここは間違ったまま記事にしてしまいました。
そもそも記事本文中に「(TX FIFO/RX FIFOの)命名は恐らく CPU サイドから見たもの。」と自分で書いたのですが、ここだけそのとおりになっていませんね。訂正いたします。