MicroPython的午睡(120) ESP32版、ULPでパルス幅を測ってみる

Joseph Halfmoon

ESP32(Xtansaコア機)が搭載する「3つめの」プロセッサ・コアULPをMicroPythonから制御するための環境をESP32 DevKitC機にインストール済です。これは使ってみるしかないです。ということでまずはハイパルスのパルス幅を測定してみることにいたしました。まあね、できるっちゃできるけれども。

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

ULPはメモリとIOを傘下に従えており、メインのプロセッサとは独立な処理を行うことができます。メインとは違って何か一つの仕事に「専念」させる事もできるし、ソフト制御でハード的な仕事もできるのではないかしらん、と考えました。

実験してみるにあたり念頭にあったのは、Raspberry Pi PicoのPIOというステートマシンです。Raspberry Pi PicoのPIOはプログラマブルなIOステートマシンで、ソフトウエア的なステートマシン記述で入出力と条件判断を含めた操作を記述可能です。ある意味アセンブラよりも直接的にハードを操作できるのでほぼほぼハードウエアのような感じで多少複雑なIO制御を行うことが可能デス。

一方、ULPもステートマシン的なものを実現可能です。ただし、ラズパイPicoのPIOと比べるとハードというよりは専用の小規模マイコンという感じです。今回は外部から与えた波形のハイ期間を測るという目的で使ってみました。アイディアは簡単、傘下の入力端子がローからハイに遷移したらカウントを始め、ハイからローに遷移したらそのときのカウント値をホストのMicroPythonから読める場所に格納しておく、というだけのもの。

まあね、できるんだけれども、やってみて痛感したのが速度っすね。ラズパイPicoのPIOがマシンの限度いっぱいのクロック周波数でも走らせられるのに対して、ULPは8MHz動作です。あんまり速度的に多くを望んではダメみたいです。

今回実験に使用したMicroPythonソースコード

前回前々回と使わせていただいた、以下のページにあるExamplesを切ったはったでつなぎ合わせて自前のコードを混ぜ合わせたフランケン風です。

micropython-esp32-ulp/examples/

MicroPythonのコードの中に、突如、ULPのアセンブラソースが登場、そのくせ、アセンブラソースの中にMicroPython関数がプリプロセッサ・マクロ風に呼び出されているという独特のスタイルです。慣れればどうってことない?ホントか。

from esp32 import ULP
from machine import mem32
from esp32_ulp import src_to_binary

source = """\
#define DR_REG_RTCIO_BASE            0x3ff48400
#define RTC_IO_TOUCH_PAD0_REG        (DR_REG_RTCIO_BASE + 0x94)
#define RTC_IO_TOUCH_PAD0_MUX_SEL_M  (BIT(19))
#define RTC_IO_TOUCH_PAD0_FUN_IE_M   (BIT(13))
#define RTC_GPIO_IN_REG              (DR_REG_RTCIO_BASE + 0x24)
#define RTC_GPIO_IN_NEXT_S           14

#define RTC_IO_TOUCH_PAD2_REG        (DR_REG_RTCIO_BASE + 0x9c)
#define RTC_IO_TOUCH_PAD2_MUX_SEL_M  (BIT(19))
#define RTC_GPIO_OUT_REG             (DR_REG_RTCIO_BASE + 0x0)
#define RTC_GPIO_ENABLE_REG          (DR_REG_RTCIO_BASE + 0xc)
#define RTC_GPIO_ENABLE_S            14
#define RTC_GPIO_OUT_DATA_S          14
#define RTCIO_GPIO2_CHANNEL          12
.set gpio, RTCIO_GPIO2_CHANNEL
.set channel, 10

.text
cnt:        .long 0

.global entry
entry:
# GPIO2 as a monitor output pin
            WRITE_RTC_REG(RTC_IO_TOUCH_PAD2_REG, RTC_IO_TOUCH_PAD2_MUX_SEL_M, 1, 1);
            WRITE_RTC_REG(RTC_GPIO_ENABLE_REG, RTC_GPIO_ENABLE_S + gpio, 1, 1)
# GPIO4 as a input 
            WRITE_RTC_REG(RTC_IO_TOUCH_PAD0_REG, RTC_IO_TOUCH_PAD0_MUX_SEL_M, 1, 1)
            WRITE_RTC_REG(RTC_IO_TOUCH_PAD0_REG, RTC_IO_TOUCH_PAD0_FUN_IE_M, 1, 1)
            move r3, cnt      # r3, base pointer
            move r2, 0        # r2, count register
            move r1, 0        # r1, old value register
rewind:
            READ_RTC_REG(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT_S + channel, 0)
            and r0, r0, 1     # Get Bit 0 only
            jumpr rewind, 1, eq  # if read value == 1 then rewind            
readr0:
            READ_RTC_REG(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT_S + channel, 0)
            and r0, r0, 1     # Get Bit 0 only
            jumpr lblzero, 0, eq # if read value == 0 then lblzero
# case read value == 1
            move r0, r1       # r0 <- old value(r1)
            jumpr lblclear, 0, eq # if old value == 0 (&&read value==1) then lblclear
# case old value == 1 && read value ==1
            add  r2, r2, 1    # r2(counter)++
            jump readr0       # goto next read
lblclear:
            WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + gpio, 1, 1)
            move r2, 0        # clear r2(counter)
            move r1, 1        # r1(old value) <- 1
            jump readr0       # goto next read
lblzero:
# case read value == 0
            move r0, r1       # r0 <- old value(r1)
            jumpr readr0, 0, eq   # if old value == 0 (&& read value==0) then readr0
# case old value == 1 && read value == 0
            WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + gpio, 1, 0)
            st r2, r3, 0      # [r3+0] <- r2(counter)
            move r1, 0        # r1(old value) <- 0
exit:
            halt
"""
binary = src_to_binary(source)

cnt_addr, entry_addr = 0, 4

ULP_MEM_BASE = 0x50000000
ULP_DATA_MASK = 0xffff

ulp = ULP()
ulp.set_wakeup_period(0, 50000)
ulp.load_binary(cnt_addr, binary)

mem32[ULP_MEM_BASE + cnt_addr] = 0x0
ulp.run(entry_addr)

while True:
    print(hex(mem32[ULP_MEM_BASE + cnt_addr] & ULP_DATA_MASK))

なお、最初、ESP32のTechnical Referenceマニュアルを見ながらアセンブラ書いてましたが、Web上の以下のページの方がずっと分かり易いことに気づきました。最初からそう言ってくれよ。。。

ESP32 ULP Coprocessor Instruction Set

実機で実行

上記のプログラムは、GPIO4番に入力されたパルスのハイ幅を指折り数えたカウンタ値としてMicroPythonで読み出すようになっています。1カウントがxx秒と定数決められればハイパルスをMicroPythonで計算できると。

今回は外から規定の信号(50%デューティ)を与えてそのとき何カウントかという測定としてます。また、ちゃんとやってるぞ、というオシルシに、ハイパルスのカウント中、GPIO2番にハイを出力するようにしています。GPIO2番に(微妙に遅れるけど)GPIO4番の波形のコピーが出力されていたらそこで測定が行われているという感じっす。

以下、黄色が外部からGPIO4番に与えた100Hzの波形。青がULPがパルス幅測定をするときにコピーした波形GPIO2番出力です。ULPは1回測定するとHaltしてしまい。後でTimerで起こされてまた測定というゆるゆるしたループで実験しているので、お休み時間がかなりあります。100Hz_waveformt

 

さて、まずは100Hzの波形を入力した場合の出力。100Hz_result

100Hzで50%デューティなので、ハイパルス幅は額面5msとなる筈。上記の0x514は10進1300なので、1カウントは3.85μsec という感じかと。

つづいて1kHz。1kHz_result同様に計算すると1カウントは3.88μsec。

最後は10kHz.

10kHz10kHzだとちょっと辛そうです。4.17μsecとな。

まあMicroPythonのソフトでポーリングするよりは微妙にレゾリューション細かくて、かつ、MicroPython側の処理に負荷がかからないです。でも、タイマのインプットキャプチャ機能とかとは比べちゃいけないな。多分ラズパイPicoのPIOであればこれより一桁細かいレゾリューション達成できる筈。知らんけど。

もともとRTCの低消費電力な番人的立ち位置のULPだから、こういうのは目的外使用?どうだか。

MicroPython的午睡(119) ESP32版、ULPとULP開発環境のおまとめ へ戻る

MicroPython的午睡(121) ESP32版、ULPで電圧を測ってみる へ進む