MicroPython的午睡(25) ラズパイPico、Timer周波数設定の上限?

Joseph Halfmoon

MicroPythonが用意してくれているTimerクラスは便利なものです。面倒な設定もなく、指定した周波数でタイマハンドラを呼び出してくれます。ただ、周波数の上限あるはずだよね、どのくらいなの?ちょっと別件でマイクロ秒単位の制御をしたかったのです。どこまで対応できるものだか調べてみることにいたしました。

※「MicroPython的午睡」投稿順 Indexはこちら

(末尾にタイマ周期設定実験使用コード全文掲げました)

ラズパイPicoのMicroPythonの拠り所、以下の文書の 3.1節にはTimerクラスの例文が掲載されています。

raspberry-pi-pico-python-sdk

設定は簡単、以下のような感じで周波数を設定し、PERIODICかONE_SHOTか指定してcallback関数を登録すれば、即座にタイマを使えます。

tim.init(freq=1000, mode=Timer.PERIODIC, callback=tick)

ただし、上記の文書にはfreqの指定にどこまで許されるのか、という記述を見つけることはできませんでした(見落としている?)軽いハンドラであれば、もしや1MHz(1μ秒毎)みたいな周期にも対応できる?実は即座にやってみて「動作しない」ことを確認いたしました。実際133MHzで動作しているCPUに133命令毎に割り込みかけると考えると、負荷的に無理そうなことはなんとなく想像がつきます。以下のMicroPythonの日本語ドキュメントでは、

クラス Timer — ハードウェアタイマーの制御

直接的に制約は書かれていなかったですが、「Timer のコールバックに関する重要な制約」を見よと書かれています。以下1文を引用させていただきます。

これらのコールバックはすべて、割り込みコンテキストでの実行とみなされるべきです。

割り込み周期とハンドラの負荷は考える(「伝統の」現物合わせ?で実測して決める)必要がありそうです。実測のために以下のように設定しました。

  • GP2端子をタイマコールバック関数でトグルさせる。
  • その周波数をDigilent Analog Dicovery2で測定する。

トグルなので、Timerに設定した周波数の半分が観測できる筈。なお、末尾のソースでGP2はオープンドレインでプルアップ付き(外部にも4.7kΩのプルアップ抵抗あり)に指定していますが、後で取り付けるつもりのデバイスのための設定なので単純なOUTでも変わらないと思います。実際Analog Discovery2の測定時の様子はこんな感じ。

TimerWave測定結果

末尾のソースでは1行以外コメントアウトされているTimerクラスの設定を変更しながら測定してみた結果を以下の表にまとめました。
Timer Frequency欄はソース上の設定周波数、Toggle Frequency Expectedは設定から期待されるトグル周波数です。その横に3回測定した周波数の生の値を置き、その平均をとって、期待値と平均値の誤差を右端にERROR%に表示しています。(この結果はCallbackとしてピンをトグルする関数を呼び出した場合に特有のものです。Callbackが変われば結果は異なるでしょう。)

measurementData

これを見ると10kHzくらいまではまあまあな時間精度で使えそうな気がします(用途しだいですが。)しかし10kHzを超えたあたりで誤差が大きくなりはじめ、15kHz以降ではあろうことか急激に周波数が落ちていきます。想像するにハンドラの処理が割り込み周期に追いつかなくなっているのではありますまいか。まあこの結果をみるとmsオーダの制御にTimerクラスを使うのはOKそうですが 100μsは気をつけないと不味そう。ましてや1μsなどは絶対無理、ってな感じでしょうか。

まあ、別件のμsオーダの制御(実際には20μとか50μなんだけれども)にはTimerクラスは向かない、別な方法を考えよ、ということであります。

MicroPython的午睡(24) ラズパイPico、CDSセンサをADCに接続 へ戻る

MicroPython的午睡(26) ラズパイPico、入力端子からの割り込みの反応時間 へ進む

タイマ周期設定実験使用コード全文
from machine import Pin, Timer 
import time

tim = Timer()
testPin = Pin(2, Pin.OPEN_DRAIN, Pin.PULL_UP)
testPin.value(1)

def tick(timer):
    global testPin
    testPin.toggle()

#tim.init(freq=100, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=1000, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=10000, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=11000, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=12000, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=13000, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=14000, mode=Timer.PERIODIC, callback=tick) #OK
#tim.init(freq=15000, mode=Timer.PERIODIC, callback=tick) #NG
#tim.init(freq=20000, mode=Timer.PERIODIC, callback=tick) #NG
#tim.init(freq=30000, mode=Timer.PERIODIC, callback=tick) #NG
#tim.init(freq=50000, mode=Timer.PERIODIC, callback=tick) #NG
tim.init(freq=100000, mode=Timer.PERIODIC, callback=tick) #NG
        
while True:
    time.sleep(10.0)