MicroPython的午睡(31) ラズパイPico、PWMの周波数カウント機能を使う

Joseph Halfmoon

前回、MicroPythonからのラズパイPicoのPWMカウンタへの入力機能の使い方が分かりませんでした。代案でPIOを使ってパルス幅測定をいたしました。今回こそは、PWMカウンタへの入力を使用してみたいと思います。題材は周波数測定であります。これまたタイマカウンタ使った測定の定番?

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

(末尾にPWMカウンタを使った周波数測定用のサンプルコード全文を掲げました。)

最初にお断りですが、この投稿、Raspberry Pi Pico用のMicroPythonのv1.14というバージョンの上で実験して書いております。本日、久しぶりにラズパイPicoのMicroPythonのダウンロードページを確認したらバージョン上がってました。

v1.15

次回からはバージョンUPいたします。今回はv1.14のままにて失礼。

さて、ちょっとクセのあるラズパイPicoのTimer/Counter機能です。メインのTimerハードウエアはタイムキーパーとして内に籠ってしまっていて、IO端子からは切り離されている感じです。IO端子の入出力操作のために使えるタイマ・カウンタは、

    • PWMカウンタ
    • PIOステートマシンでカウント

のどちらかとなると思います。そのうちPWMカウンタはPWM出力に使えるだけでなく、入力信号でPWMカウンタのカウントを制御できるので、

    • 周波数測定
    • デューティ測定

などができる、ということになっています。

しかし、ラズパイPicoのMicroPython(繰り返しますがv1.14です)では、PythonからPWM出力をするのは容易なのですが、PWMカウンタの入力使いについては「それらしい関数が machineモジュールのPWMクラスには見当たらない」です。それで、前回はパルス幅測定をするのに、PIOステートマシンでカウントしたわけであります。まあこちらでも32ビットのカウントはできます(が、SMのプログラミング必須。ちょっとマイクロコード風で私は好きです。)しかし、折角のPWMの入力機能です。

直接ハードウエアレジスタを操作してやればPWM入力使えるんじゃね

ということで今回はやってみました。

ハードウエアレジスタの直接制御

MicroPythonインタプリタからのハードウエアレジスタの直接制御で「危ない」点をまず挙げときます。「MicroPython側の操作と自分の操作が意図せずバッティングする」という可能性です。MicroPythonのmachine関係の関数を呼び出せば、ほぼ必ず何等かのハードウエアレジスタの操作が行われているはずです。しかし、PythonのSDK文書を読んでもどことどこのレジスタを触っているよ、みたいなことは書いてません。基本、バッティングしないように気をつけて(Python側の操作を想像?して)コードを書くことになります。いっそのこと、自分が操作したいハードウエアレジスタに関して「触りそうなPython関数を積極的に使ってみて、どこをどう触っているのか」事前に調べておく、という方が安全かもしれません。いずれにせよ、ハードウエアレジスタを知らねば話になりません。以下の文書を熟読(といって関係しそうなところだけ)。

RP2040 Datasheet

まあ、これ読むとPWMカウンタの入力使いはそれほど難しくありません。DIVモード、というものを変更すれば通常カウンタ1チャンネルにAとBの2本あるPWM出力端子のうち、B端子の方が入力になってカウンタへのクロック制御に使えるようになるようです。カウンタ前段の分周器(8.4という小数点以下まであるカッコイイ分周器です)もカウンタそのものも動作は変わらないので、モードの設定ができれば外部入力によるカウントが可能になるように読めます。しかし、先に書いておくと

PWM関係のレジスタを操作しただけではカウンタはピクリとも動きませぬ

バカなことに、PWMのレジスタを設定しても全然カウント動作しないと暫く悩みました。しかし気付きました。PWMブロックを操作するだけでなく、GPIOブロックの該当端子をPWM入力用に切り替えておかないなりません。いくら端子に信号を印加してもGPIOのところでPWM機能に接続していなければカウンタが動作する筈はありませんでした。MicroPythonでPWMクラスの関数を使うときには、関数の中でPWMの設定だけでなく、GPIOの設定なども同時に行ってくれているので、それに慣れていて失念しておりました。1か所、信号を印加しているGPIO端子をPWM機能に切り替えるオマジナイを挿入したら、嘘のように動作しはじめました。

Cやアセンブラで書くときのようにちゃんと設定良く読めよ

です。まあクロックとGPIOはあちこち関係するので必読でしょう。

動作実験

PWMチャネル0を使いました。チャネル0のB端子としてはGP1端子を用い、これに外部から方形波を与えてその周波数を測定してみました。波形の生成と観察には例のごとくDigilent Analog Discovery2を使用しました。波形の生成は以下のような感じで、1kHz, 2kHz, 5kHzと周波数を変化させてみました。

input Wave form末尾に実験用コードを掲げましたが、以下のような設定です。

    1. 測定前に、まずPWMチャネル0のレジスタの値を念のためダンプ
    2. TIMERを使って100ms後にPWMチャネル0のカウンタを読み出すようにワンショットタイマをセット
    3. PWMチャネル0をGP1端子からの信号の立ち上がりでカウントするように設定(カウンタはゼロクリア、分周比は1.0)
    4. TIMERコールバックが動作してカウンタ読み出しが完了するまで暇つぶし(念のため110ms待った)
    5. 読み出したカウンタ値(100ms分)を10倍すれば周波数[Hz]となる

これにて 5kHzの入力波形を与えた場合の結果はこんな感じ。

CSR=0x00 DIVMODE=0 EN=0
DIV=1.0
CTR=0x01f1
CCA=0x0000
CCB=0x0000
TOP=0xffff
Frequency measured: 4970 [Hz]

入力5kHzに対して4970Hzと30Hzほど小さ目な値が出ています。0.6%くらいの誤差ですかね。末尾のコードはPWMをスタートするときにいろいろレジスタを一度に操作して時間を使っているので、出だしのカウントをミスっている可能性が高いです。事前に設定してタイマを仕掛けた後はEnableするだけに改めた方が良いと思います。また、絶対精度を上げるためには100msの測定期間を長くしたほうが良いでしょう。しかし、PWMカウンタのビット幅は16ビットしかないという制限があります。末尾コードの、1.0分周(入力信号直結)、100ms測定期間であると10Hzから約655kHzまで10Hzレゾリューションで測定可能です。これを1s測定にすれば1Hzから約65.5kHzまで1Hzレゾリューションということになります。また、分周比を操作すればもっと高い周波数の入力信号に対応させることも可能だと思います。しかし高い方を測定しようとすると、下限の測定範囲は上がるし、レゾリューションも荒くなります。測定する対象物により、ハードを勘案しながら塩梅するしかありますまい。

次回は周波数カウンタにセンサでもつなげて実動作を見てみますか。おっとPythonのバージョンUPも確認しないと。バージョンUPした版では簡単にPWM入力使えるようになっていたりして。。。

MicroPython的午睡(30) ラズパイPico、HC-SR04超音波センサを接続 へ戻る

MicroPython的午睡(32) ラズパイPicoの処理系をようやくv1.15に更新 へ進む

MicroPythonからRP2040のPWMの周波数カウント機能を使うスクリプト

from machine import Pin, Timer, PWM, mem32
import time

counter0 = 0

def getPWMsettings(ch):
    PWM_BASE = 0x40050000
    if (ch < 0) or (ch > 7):
        return None
    ch_adr = ch * 0x14 + PWM_BASE
    csr = mem32[ch_adr + 0]
    div = mem32[ch_adr + 4]
    ctr = mem32[ch_adr + 8]
    cc  = mem32[ch_adr + 0xC]
    top = mem32[ch_adr + 0x10]
    return (csr, div, ctr, cc, top)

def readModifyWrite(adr, mask, dat):
    temp = mem32[adr] & mask
    mem32[adr] = temp | dat

def startPWMCtr(ch):
    PWM_BASE = 0x40050000
    if (ch < 0) or (ch > 7):
        return None
    ch_adr = ch * 0x14 + PWM_BASE
    readModifyWrite(ch_adr + 0x10, 0xFFFF0000, 0xFFFF)
    mem32[ch_adr + 0xC] = 0
    readModifyWrite(ch_adr + 8, 0xFFFF0000, 0)
    readModifyWrite(ch_adr + 4, 0xFFFFF000, 0x10)
    readModifyWrite(ch_adr + 0, 0xFFFFFF00, 0x21)
    
def stopPWMCtr(ch):
    PWM_BASE = 0x40050000
    if (ch < 0) or (ch > 7):
        return None
    ch_adr = ch * 0x14 + PWM_BASE
    work = mem32[ch_adr + 8]
    readModifyWrite(ch_adr + 0, 0xFFFFFF00, 0)
    return work & 0xFFFF

def timCB(timer):
    global counter0
    counter0 = stopPWMCtr(0)

def setGPIO1CTRL():
    IO_BANK0_BASE = 0x40014000
    gpio1_ctrl = IO_BANK0_BASE + 0x00c
    temp = mem32[gpio1_ctrl]
    mem32[gpio1_ctrl] = (temp & 0xFFFFFFE0) | 0x4
    
setGPIO1CTRL()

dumpSetting = getPWMsettings(0)
if dumpSetting is not None:
    print("CSR=0x{0:02x} DIVMODE={1} EN={2}".format(dumpSetting[0] & 0xFF, (dumpSetting[0] & 0x30) >> 4, dumpSetting[0] & 1))
    print("DIV={0}.{1}".format((dumpSetting[1] & 0xFF0) >> 4, (dumpSetting[1] & 0xF)))
    print("CTR=0x{0:04x}".format(dumpSetting[2] & 0xFFFF))
    print("CCA=0x{0:04x}".format(dumpSetting[3] & 0xFFFF))
    print("CCB=0x{0:04x}".format((dumpSetting[3] >> 16) & 0xFFFF))
    print("TOP=0x{0:04x}".format(dumpSetting[4] & 0xFFFF))

tim = Timer()
tim.init(mode=Timer.ONE_SHOT, period=100, callback=timCB) # measurement period = 100ms, *10 for Hz
startPWMCtr(0)
time.sleep(0.11) # wait 110ms
tim.deinit()
if counter0 is not None:
    print("Frequency measured: {0} [Hz]".format(counter0 * 10))
else:
    print("ERROR: Measurement.")