ここ数回、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読み取りとは別なタイミングで読み取った値なので微妙に異なりますが、まあいい線かと。
念のため、ポテンショメータを少し回して0.55V入力にした場合の読み取り結果が以下に。
MicroPython、ULPともに読み取れてるみたいだし。アナログ値のポーリング的な監視には良いかもしれんね、ULP。