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

Joseph Halfmoon

ここ数回、ESP32(Xtansaコア機)が搭載する「3つめの」プロセッサ・コア、ULPをMicroPythonから試用中です。前回はデジタル信号の入力を試行。今回はアナログ信号の監視に使えないか実験してみます。なかなかいい感じでないかい。メインが寝ている裏でひっそりと監視するのにULPはピッタリだな。やっぱり。

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

前回はULPでデジタル信号のパルス幅を測るという暴挙に出ました。まあ、測れるんだけれども。もともとLow Powerが売りのULPは速度があまり速くないです。そのためラズパイPicoのPIOステートマシンみたいな高速な信号への対応は望むべくもありません。ゆるゆるした信号向けね。

そうであれば、ゆるゆるしたアナログ信号を監視するのには打ってつけでないかい。当然ESPの中の人もそう考えたらしく、ULPの命令セットにはADCを読み取るための専用命令まで用意されてます。お楽。

ESP32のADC

ESP32(Xtansaコア機)は2個のホストCPU(WiFiをつかさどるPROとMicroPython処理系などアプリを動かすAPP)が仕事を分担しています。ADコンバータも2セット搭載されています。それぞれ8チャネルのアナログマルチプレクサを搭載し8個の異なる対象の電圧を測定可能です。

2個のADCに対してそれぞれADCのコントローラ回路が存在し、両ホストCPUおよびDMAコントローラからどちらへもアクセス可能です。ただし、PRO側のCPUがWiFiを使用しているとき、2番目のADCはPRO占有となるようです。

込み入ったことに我らがULPは、ADC本体(SAR型、逐次比較)とホスト用のADCコントローラの中間で独自の回路を使って両ADCを監視できるようになっているみたいです。勿論、WiFi使用時にはADC2には勝手な事はできませぬ。

今回は、MicroPython(ホストのAPP‐CPU)から1番目のADCをセットアップして値を読み取るとともに、その裏でULPからも同じADCの変換値を読み取ってみたいと思います。これがうまく動けば、ホストのお手を煩わせなくてもゆるゆるとしたアナログ信号の監視などはULPにお任せ、ということだな。

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

実験に使用したコードのうち、ホスト側でADCを読み取る関数などは、別記事で実験したときに作成したものです。ESP32のADCは、読み取る電圧レンジによってアッテネータを制御する必要があります(恐ろしいことに予定外の高い電圧を加えると壊れると警告があります。まあどのADCも定格以上の電圧加えたら壊れるケド)アッテネータの制御は、そちらに任せるとともに、実験用の外部電圧を生成する回路に手心を加えて?高い電圧が加わらないようにしてます。

一方ULP側では、ホスト側で設定済のADCに勝手に相乗りするつもりなので、何も初期設定などしてません。ULPのソースコードは簡単。たった4命令。ULPのインストラクションセットは以下の御本家ページに解説があります。

ESP32 ULP Coprocessor Instruction Set

なおアナログ入力に使用した端子はGPIO32番端子です。

from esp32 import ULP
from machine import mem32, Pin, ADC
from esp32_ulp import src_to_binary
import time

source = """\
.set sar_sel, 0                 # select ADC0 
.set mux_sel, 7                 # select VDET_1(GPIO34)

.text
adcval:     .long 0

.global entry
entry:      move r3, adcval    # load address of adcval into r3
            adc  r0, sar_sel, mux_sel # read ADC value
            st   r0, r3, 0     # store r0 contents into adcval ([r3+0])
            halt
"""

def readADC(opt, apin):
    if   opt == 0:
        print("ATTN_0DB, 100mV - 950mV")
        atn = ADC.ATTN_0DB
    elif opt == 1:
        print("ATTN_2_5DB, 100mV - 1250mV")
        atn = ADC.ATTN_2_5DB
    elif opt == 2:
        print("ATTN_6DB, 150mV - 1750mV")
        atn = ADC.ATTN_6DB
    else:
        print("ATTN_11DB, 150mV - 2450mV")
        atn = ADC.ATTN_11DB
    time.sleep_ms(500)
    adc1 = ADC(Pin(apin),atten=atn)
    time.sleep_ms(500)    
    valu16 = adc1.read_u16()
    time.sleep_ms(500)
    valuv  = adc1.read_uv()
    print("U16:   ", valu16)
    print("valuv: ", valuv/1000, "[mV]")

def main():
    opt = 0
    apin = 34
    readADC(opt, apin)

    binary = src_to_binary(source)
    base_addr, entry_addr = 0, 4
    ULP_MEM_BASE = 0x50000000
    ULP_DATA_MASK = 0xffff
    ulp = ULP()
    ulp.set_wakeup_period(0, 50000)
    ulp.load_binary(base_addr, binary)
    mem32[ULP_MEM_BASE + base_addr] = 0x0
    ulp.run(entry_addr)

    while True:
        readADC(opt, apin)
        u12 = mem32[ULP_MEM_BASE + base_addr] & ULP_DATA_MASK
        print("ULP u12: {0} u16: {1}".format(u12, u12<<4))

if __name__ == "__main__":
    main()
実機で実行

上記のプログラムをターゲット機のESP32 DevkitC上(ESP32版MicroPythonおよびMicroPython上のULP開発環境であるesp32_ulpモジュールをインストール済のもの)で実行してみます。

まずは外部から0.35Vの電圧を加えたときの様子。U16はホスト側で読み取ったADCの生の測定値(ADCは12ビットなので、左4ビットシフトしてバイナリ下4桁0000のもの)、valuvという[mV]表示の方は、MicroPython内蔵関数で補正を加えた電圧値です。

一方ULP u12は、ULPで読み取った生の12ビット値、u16は上記との比較のため4ビット左シフトしたものです。MicroPython側のADC読み取りとは別なタイミングで読み取った値なので微妙に異なりますが、まあいい線かと。ADC_read_3_5

念のため、ポテンショメータを少し回して0.55V入力にした場合の読み取り結果が以下に。ADC_read_5_5

MicroPython、ULPともに読み取れてるみたいだし。アナログ値のポーリング的な監視には良いかもしれんね、ULP。

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

MicroPython的午睡(122) Thonny IDE、シリアルプロッタを使ってみる へ進む