Raspberry Pi Picoの上でMicroPythonを走らせていて「どうしたら良いのか」ちょっと迷ったことにパルス幅の測定があります。RP2040のPWM機能の中に周波数測定、デューティ測定などはありますが、単発のパルス1発の幅を拾いたいです。こういう時こそPIO(Programable IO)ですかね。
※「MicroPython的午睡」投稿順 Indexはこちら
(末尾に実験用のMicroPythonプログラム全文を掲げました)
単発のパルス幅を測定したかったのは、あるセンサの接続のためです。例えばArduino環境では、そのものずばりのpluseIn()関数などというものが存在するので、これ1発で測定完了です。そういうことができるのは、多機能なタイマ・カウンタを搭載している「普通の」マイコンの場合、パルス幅の測定はそれらのハードウエアでサポートされていることが多いためだと思います。ところが、いろいろよく考えられているRP2040マイコンですが、タイマ・カウンタに関しては普通のマイコンとはちょっと機能の構成が異なっています。タイマは周期あるいはワンショットの時間管理専用という感じでIOのハードウエア操作向きではないです。一方IO操作のためには、8チャンネル、16端子に接続できるPWM機能のためのカウンタを使用するようになっています。PWM出力についてはMicroPythonからも使用できます。しかし、PWMのカウンタで端子から入力された信号の周波数やデューティを測定する機能についてはMicroPythonからどう制御したもんだか現在のところ不明です。MicroPythonからでも直接ハードウエアにアクセスすれば使えるでしょうが、醜いです。また、そもそもの測定対象も周波数とかデューティであって、単発のパルスにどの程度使えるのかは不明。単発のインプット・キャプチャとか、アウトプット・コンペアみたいなタイマ・カウンタ機能が欲しいところなのですが。
そこで例によってPIOをプログラムしてパルス幅を測定してみようと思い立ちました。まず、どのようなパルスを測定対象として考えているかを書きます。
- パルス幅は数μ秒~数百μ秒くらい
- 2μ秒単位くらいで測定できれば良い。
- とりあえずHighパルスのみ
上記のパルスに対してPIOをプログラムします。アウトラインはこんな感じ。
- 測定をトリガするまでPIOは何もせず待っている(Queue空でブロック)
- 測定上限幅に相当する値xをQueueに書き込まれたら(put)測定開始
- まず端子がLow(0)であることを確認する(Highであれば、既にパルスの途中にいるということ。現実装では次回パルスのためにLowに落ちるまで待つ。)
- 端子がHigh(1)であれば、値xからカウントダウンすることをHighの間繰り返す。
- 端子がLow(0) になったら、カウントダウンを止め、残カウントをQueueに書き込む(メインプログラムは適当なところでQueueからgetすれば値が得られる)
カウンタの初期値と残カウントの差がパルス幅に比例することになります。PIOをプログラムしていて気付いたTipsがいくつかあります。(例によって、RP2040 Datasheetの Chapter 3. PIOを読みながら書きましたが、具体的な例が無い場合も多く、思った機能をどう実現するべきか迷っています。)
- PIO内ではカウントダウンは簡単にできるが、カウントアップは簡単でないので、パルス幅の測定は想定上限値をカウントダウンすることで行った。(0でループから抜けることになるのでタイムアウト的な動作にもなる)
- 通常、PIOのSM用の「プログラム」では、入出力端子は pins などと抽象的に表現し、SMを生成するときに外側から具体的なPin番号をマップして与える。ところが、ある端子が所定の状態になるまで「wait」する機能については、マップでなく、直接GPIOの端子番号で与える必要がある。(物理端子番号依存)
- 直接、入力端子の状態(0/1)を判定して分岐することは可能。しかし命令的には端子がHighのときにJMP成立のケースしかないので判定の書き方には注意。
- JMP alwaysはPIO ASM的にはあるが、MicroPython上では always定数が見当たらなかったので 定数0 を即値で書いてみた。
サンプルプログラムの実行結果
パルスを入力する端子としてはGP0を用いました。この端子に Digilent Analog Discovery 2 のパターン生成器から5kHzから50kHzまでの何通りかの方形波(デューティ50%)信号を加えます。測定結果はMicroPythonのREPLに接続しているモニタに垂れ流しです。
PIOの動作周波数はきりのよいところで1MHz、1クロック=1μ秒に設定しました。後に掲げたサンプルプログラムのとおり、パルス幅を測定するループは2クロックで回っている(1ループ=2μ秒)ので、パルス幅は
(ステートマシンにputした値 – ステートマシンからgetした値) * 2 [μ秒]
ということになります。以下のサンプルコードで約10μ秒幅から約100μ秒幅までほぼほぼ正確な値が取れたので、まあいいか、ですね。
MicroPython的午睡(28) ラズパイPico、lightとdeepなsleep へ戻る
MicroPython的午睡(30) ラズパイPico、HC-SR04超音波センサを接続 へ進む
PIO利用のパルス幅測定サンプルコード
from rp2 import PIO, asm_pio from machine import Pin, Timer import time @asm_pio() def measureX(): pull(block) mov(x, osr) wait(0, gpio, 0) wait(1, gpio, 0) label("highloop") jmp(pin, "count") jmp(0, "exitloop") label("count") jmp(x_dec, "highloop") label("exitloop") mov(isr, x) push(block) sm0 = rp2.StateMachine(0, measureX, freq=1000000, in_base=Pin(0)) while True: sm0.active(1) sm0.put(100) dat = sm0.get() sm0.active(0) print("DATA=0x{0:08x}".format(dat)) time.sleep_ms(500)