MicroPython的午睡(15) ラズパイPico、プログラマブルIOの威力

Joseph Halfmoon

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出力」の実行結果

PIO_WAVE0

黄色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波形が得られました。

PIO_WAVE1

ステートマシンの周波数を上げてみた結果

次にステートマシンの動作周波数を100kHzにあげてみます。修正するのは以下の箇所です。なお、1命令実行にかかるサイクル数は最初の16サイクルに戻してあります。

sm = rp2.StateMachine(0, wave, freq=100000, sideset_base=Pin(21), set_base=Pin(22))

下の実行結果を見ると、予定どおり、最初の結果の10倍の周波数になっています。

PIO_WAVE2

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)