Raspberry Pi PicoのMCU RP2040の大きな特徴がPIO(Programable IO)とよぶ、IO制御の仕組みだと思います。プログラム可能なステートマシンでIO端子を直接制御するもの。可能性無限大。そしてMicroPythonからでも「プログラム」可能でした。まずは今回はお試し。ちょっとした波形を作って観察してみます。
※「MicroPython的午睡」投稿順 Indexはこちら
普通、SIOといったらシリアルIO、PIOといったらパラレルIOだと思います。ところが Raspberry PiのMCU, RP2040は普通でありません。
- SIO=Single-cycle IO
- PIO=Programable IO
です。どちらもクリティカルなタイミングでIOを制御するための仕組みです。その中でもユニークなのがPIOであります。IO密着のステートマシンが4つ。これらは専用の9種類の命令を実行できる一種のCPUといっても良いでしょう(勿論Armではありません。)専用の「プログラムメモリ」を持ち、この中に格納された命令を基本1命令1サイクルで実行できます。クリティカルなタイミング制御のため、この命令はxサイクルで実行、という具合に実行サイクル数を調整することも可能。わずか9種類の命令といいつつ、IO制御のために必要な機能を盛り込んだ強力な装置です。詳しくは、
RP2040 Datasheet のChapter 3. PIO
をご参照ください。
PIOは基本、pioasm と呼ぶPIO専用のアセンブラでプログラムを書きます。アセンブラ好きの私としましては燃える(萌える?)シチュエーションであります。しかしま、Pythonからすると、アセンブラモジュールをわざわざ別ツールで作成して呼び出すのでは大変面倒です。しかし、ラズパイPicoにMicroPythonを移植した人はそんなこと解決済でした。モジュール rp2 内のオブジェクトなどを使って MicroPythonからでもPIOを制御できるようにしてありました。
まずは例によって、モジュール内でどのようなものが定義されているのか調べます。
rp2モジュールの中のオプジェクトなど
object <module 'rp2'> is of type module Flash -- <class 'Flash'> PIOASMEmit -- <class 'PIOASMEmit'> const -- <function> asm_pio -- <function asm_pio at 0x20006960> 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 0x200066f0>, '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 0x20006400>, '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 0x20006410>, 'jmp': None, 'wait': None} __name__ -- rp2 StateMachine -- <class 'StateMachine'> asm_pio_encode -- <function asm_pio_encode at 0x200067e0> PIO -- <class 'PIO'>
rp2モジュールをインポートすると上のような者共が見えてきます。
asm_pio
というのは「デコレータ」だと思います。このデコレータでPythonの関数を「包む」ことで、PIO用アセンブラ的なプログラムを _pio_funcs内に定義されているお名前を使ってPythonコード内で書けるようになります。
一番下のPIOというのが実際にPIOを構築するときに使うクラスだと思います。また、実行を制御するのに使うのは、StateMachineというクラスです。
rp2.PIO内の関数、定数
PIO内の定義を見ると以下のようです。ちゃんと調べてないのですが、関数はデコレータの中で処理されているのだと思います。「ユーザー」のPythonプログラムでお世話になるのはもっぱら定数です。
object <class 'PIO'> is of type type add_program -- <function> remove_program -- <function> state_machine -- <function> irq -- <function> IN_LOW -- 0 IN_HIGH -- 1 OUT_LOW -- 2 OUT_HIGH -- 3 SHIFT_LEFT -- 0 SHIFT_RIGHT -- 1 IRQ_SM0 -- 256 IRQ_SM1 -- 512 IRQ_SM2 -- 1024 IRQ_SM3 -- 2048
rp2.SateMachine内の関数
実際にPIOの動作にトリガを掛けたり、値を渡したりするために使う関数群が定義されています。こちらはあからさまにMicroPythonから呼び出す必要があります。
object <class 'StateMachine'> is of type type init -- <function> active -- <function> exec -- <function> get -- <function> put -- <function> irq -- <function>
さて、実際にMicroPythonからPIO使ったプログラムを走らせて、その波形をDigilent AnalogDicovery2で観察してみます。最初のプログラムは末尾に「反転波形PIO出力」という名で全文を掲げたものです。
- GP22に 0/1波形を出力する。
- GP21に 上の波形を0/1反転した波形を同タイミングで出力する。
単純なsetだけではサンプルとして面白くないので、sidesetというPIOの機能を使って同時に複数端子の状態を変更しています。また、ステートマシンの「クロック」は10kHzとし、ON/OFFセットの各命令は1命令あたり1+15サイクルの時間を消費するようにプログラムしてあります。
「反転波形PIO出力」の実行結果
黄色GP22,青がGP21です。逆相の信号が期待通りに出力されています。また観察された周波数は312.51Hzです。
10000Hz(10kHz) / 312.51Hz ≒ 32.0
です。ONとOFF2命令に32サイクルを使うようにプログラムしてあるので、期待通りかと思います。なおwrapという名の指示は実ステートマシン上ではサイクル数を消費しないようです(条件jmpはちゃんと1サイクルを消費するので実体がある命令です。)
遅延を小さくしてみた場合の実行結果
PIOのプログラムの中の [ 数字] は、通常の命令実行に加えて何サイクル待つかという指示の為です。上記の例では[15]としてあったので、実行の1サイクル+15サイクルで16サイクル=1命令でした。ここを[1]とすれば、1命令=2サイクルとなり高速な波形が観察できる筈。変更は以下の部分です。
set(pins, 1).side(0) [1] set(pins, 0).side(1) [1]じ
実際に観察した波形が下に。2+2で4サイクル。10kHzクロックなので期待どおり、2.5kHz波形が得られました。
ステートマシンの周波数を上げてみた結果
次にステートマシンの動作周波数を100kHzにあげてみます。修正するのは以下の箇所です。なお、1命令実行にかかるサイクル数は最初の16サイクルに戻してあります。
sm = rp2.StateMachine(0, wave, freq=100000, sideset_base=Pin(21), set_base=Pin(22))
下の実行結果を見ると、予定どおり、最初の結果の10倍の周波数になっています。
8ビットデータをシフトアウトするのに合わせてクロックON/OFF
続いて、データをシフトアウトしながら、それに合わせて「クロック信号」を作ってみます。前回利用した74HC595のようなシフトレジスタへのインタフェースを念頭においています。プログラム全文は、末尾の「同期式シリアルもどき」です。ここで
- GP22 データのシリアルアウト
- GP21 クロック
というイメージです。データ out 命令に sideでクロックを付加する感じ。なお、データは 0x81 の繰り返しにしてしまったので、前のフレームのLSBと後のフレームのMSBがつながっているように見えます。青のクロックの立ち上がりで「サンプル」すれば、10000001(0x81)という値が読めると思います。
いやあ、MicroPythonからでも結構お楽に使えるのね、PIO。
MicroPython的午睡(14) ラズパイPico、7セグ4桁、とりあえずタイマ駆動 に戻る
MicroPython的午睡(16) ラズパイPico、PIOで74HC595制御、簡単 へ進む
反転波形PIO出力
import time from rp2 import PIO, asm_pio from machine import Pin @asm_pio(sideset_init=rp2.PIO.OUT_HIGH, set_init=rp2.PIO.OUT_LOW) def wave(): wrap_target() set(pins, 1).side(0) [15] set(pins, 0).side(1) [15] wrap() sm = rp2.StateMachine(0, wave, freq=10000, sideset_base=Pin(21), set_base=Pin(22)) sm.active(1) time.sleep(10) sm.active(0)
同期式シリアル出力もどき
import time from rp2 import PIO, asm_pio from machine import Pin @asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=PIO.SHIFT_RIGHT) def shiftOut(): pull() set(x, 7).side(0) [7] label("loop") out(pins, 1) [7] nop().side(1) [7] nop().side(0) [7] jmp(x_dec, "loop") sm = rp2.StateMachine(0, shiftOut, freq=10000, sideset_base=Pin(21), out_base=Pin(22)) sm.active(1) for i in range(10000): sm.put(0x81) sm.active(0)