ESP32のULPアセンブリ言語入門 その6 ADC、入力、復帰

概要

前回はビット演算を行いましたが、今回は残った命令をすべて紹介したいと思います。

今回は命令より、仕組み解説が大変なのでスケッチを紹介してから、命令の解説を行います。

ADC(アナログ入力)

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "driver/adc.h"
#include "UlpDebug.h"
// Slow memory variable assignment
enum {
  SLOW_ADC,
  SLOW_PROG_ADDR        // Program start address
};
void ULP(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);
  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM);
  uint8_t adc_pin = GPIO_NUM_36;
  uint8_t adc_adc;
  uint32_t adc_channel;
  if ( esp32_gpioMux[adc_pin].adc < 10 ) {
    // ADC1
    adc_adc = 0;
    adc_channel = esp32_gpioMux[adc_pin].adc;
    adc1_config_channel_atten((adc1_channel_t)adc_channel, ADC_ATTEN_DB_11);
    adc1_config_width(ADC_WIDTH_BIT_12);
  } else {
    // ADC2
    adc_adc = 1;
    adc_channel = esp32_gpioMux[adc_pin].adc % 10;
    adc2_config_channel_atten((adc2_channel_t)adc_channel, ADC_ATTEN_DB_11);
  }
  Serial.printf("adc_adc : %d\n", adc_adc);
  Serial.printf("adc_channel : %d\n", adc_channel);
  // ULP Program
  const ulp_insn_t  ulp_prog[] = {
    I_ADC(R0, adc_adc, adc_channel),
    I_MOVI(R2, SLOW_ADC),                   // R2 = SLOW_COUNT
    I_ST(R0, R2, 0),                        // MEM[R2(SLOW_COUNT)] = R1
    I_HALT()                                // Stop the program
  };
  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);
}
void setup() {
  Serial.begin(115200);
  delay(50);
  // Execute ULP program at 300ms intervals
  ULP(300000);
}
void loop() {
  // For debug output
  ulpDump();
  // Wait
  delay(1000);
}

ULPからADCでアナログ入力を行うためには、ADCの初期化処理が必要になります。

GPIOADCチャンネル
36ADC1(0)0
37ADC1(0)1
38ADC1(0)2
39ADC1(0)3
32ADC1(0)4
33ADC1(0)5
34ADC1(0)6
35ADC1(0)7
4ADC2(1)0
0ADC2(1)1
2ADC2(1)2
15ADC2(1)3
13ADC2(1)4
12ADC2(1)5
14ADC2(1)6
27ADC2(1)7
25ADC2(1)8
26ADC2(1)9

上記の対応になるのですが、さすがに覚えることは難しいのでテーブルがないか確認したところesp32_gpioMuxのadcが使えそうでした。

  if ( esp32_gpioMux[adc_pin].adc < 10 ) {
    // ADC1
    adc_adc = 0;
    adc_channel = esp32_gpioMux[adc_pin].adc;
    adc1_config_channel_atten((adc1_channel_t)adc_channel, ADC_ATTEN_DB_11);
    adc1_config_width(ADC_WIDTH_BIT_12);
  } else {
    // ADC2
    adc_adc = 1;
    adc_channel = esp32_gpioMux[adc_pin].adc % 10;
    adc2_config_channel_atten((adc2_channel_t)adc_channel, ADC_ATTEN_DB_11);
  }

adcが10未満の場合にはADC1で、数値がそのままチャンネルになります。ADC1とADC2で初期化方法も異なります。

ADC2の場合には10以上の数値になるので、10のあまりを取得してチャンネル番号に変換しています。

ADC_ATTEN_DB_11は11dbで、通常のanalogRead()と同じ設定値です。adc1_config_width()関数で標準値の12ビットを設定しています。ADC2は全体設定する項目がなく、取得時に指定するのですが未設定の場合おそらく12ビットになっている気がします。。。

この辺は情報がかなり少ないので追いきれませんでした。そして取得した数値もかなりぶれているので、個人的にはADCをULPから取得するのはおすすめしません。

ADC1のGPIO36でためしたところ、5V入力で約1000の数値、3.3Vで約500の数値が戻ってきました。ADC2のGPIO26でためしたところ、0Vで約4000、3.3Vで約0になりました。

戻ってくる数値が違っているので、どこか初期化がおかしい気がします。

GPIO入力(デジタル入力)とスリープ復帰

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "UlpDebug.h"
// Slow memory variable assignment
enum {
  SLOW_BTN_STATE,       // Btn status
  SLOW_PROG_ADDR        // Program start address
};
void ULP(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);
  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM);
  // Blink status initialization
  RTC_SLOW_MEM[SLOW_BTN_STATE] = 1;
  // PIN to btnA (specify by +14)
  const int pin_btn_bit = RTCIO_GPIO37_CHANNEL + 14;
  const gpio_num_t pin_btn = GPIO_NUM_37;
  // GPIO37 initialization
  rtc_gpio_init(pin_btn);
  rtc_gpio_set_direction(pin_btn, RTC_GPIO_MODE_INPUT_ONLY);
  // ULP Program
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_BTN_STATE),             // R3 = SLOW_BTN_STATE
    I_RD_REG(RTC_GPIO_IN_REG, pin_btn_bit, pin_btn_bit),
                                            // R0 = RTC_GPIO_IN_REG[pin_btn_bit:pin_btn_bit]
    I_ST(R0, R3, 0),                        // MEM[R3(SLOW_BTN_STATE)] = R0
    M_BGE(0, 1),                            // IF R0 >= 1 THAN GOTO M_LABEL(0)
    I_WAKE(),                               // WAKE
    M_LABEL(0),
    I_HALT()                                // Stop the program
  };
  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);
}
void setup() {
  Serial.begin(115200);
  delay(50);
  Serial.println("Boot");
  
  // ULPをウェイクアップソースとして有効にする
  esp_sleep_enable_ulp_wakeup();
  // Execute ULP program at 300ms intervals
  ULP(300000);
 
  // ディープスリープ
  esp_deep_sleep_start();
}
void loop() {
}

ディープスリープから、ボタン入力で復帰するスケッチです。

GPIO37を入力用の初期化をして、スリープにはいってGPIO37がLOWになるとI_WAKE()でスリープ復帰しています。

命令紹介

アナログ入力 – ADC

I_ADC(reg_dest, adc_idx, pad_idx)

regにadc_idx(0:ADC1, 1:ADC2)のpad_idx(チャンネル番号)の電圧を代入します。

I_ADC(R0, ADC1, 0),

レジスタ読込 – REG_RD

I_RD_REG(reg, low_bit, high_bit)

R0にアドレスの内容を、low_bitとhigh_bitで範囲指定して読み込みます。

I_RD_REG(RTC_GPIO_IN_REG, pin_btn_bit, pin_btn_bit),
                               // R0 = RTC_GPIO_IN_REG[pin_btn_bit:pin_btn_bit]

スリープ復帰 – WAKE

I_WAKE()

スリープから復帰します。

I_WAKE(),

命令一覧

命令アセンブリCマクロ処理
ノーオペレーションNOP
代入MOVEI_MOVI(reg_dest, imm_)
I_MOVR(reg_dest, reg_src)
reg_dest = imm_
reg_dest = reg_src
メモリロードLDI_LD(reg_dest, reg_addr, offset_)reg_dest = MEM[reg_addr + offset_]
メモリストアSTI_ST(reg_val, reg_addr, offset_)MEM[reg_addr + offset_] = reg_val
足し算ADDI_ADDI(reg_dest, reg_src, imm_)
I_ADDR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src + imm_
reg_dest = reg_src1 + reg_src2
引き算SUBI_SUBI(reg_dest, reg_src, imm_)
I_SUBR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src – imm_
reg_dest = reg_src1 – reg_src2
アンドANDI_ADDI(reg_dest, reg_src, imm_)
I_ADDR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src & imm_
reg_dest = reg_src1 & reg_src2
オアORI_ORI(reg_dest, reg_src, imm_)
I_ORR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src | imm_
reg_dest = reg_src1 | reg_src2
左シフトLSHI_LSHI(reg_dest, reg_src, imm_)
I_LSHR(reg_dest, reg_src, reg_shift)
reg_dest = reg_src << imm_
reg_dest = reg_src1 << reg_src2
右シフトRSHI_RSHI(reg_dest, reg_src, imm_)
I_RSHR(reg_dest, reg_src, reg_shift)
reg_dest = reg_src >> imm_
reg_dest = reg_src1 >> reg_src2
ラベルM_LABEL(label_num)
ジャンプJUMPM_BX(label_num)
M_BXZ(label_num)
M_BXF(label_num)
GOTO label_num
IF zero_flag THAN GOTO label_num
IF overflow_flag THAN GOTO label_num
R0比較ジャンプJUMPRM_BL(label_num, imm_value)
M_BGE(label_num, imm_value)
IF R0 < imm_value THAN GOTO label_num
IF R0 >= imm_value THAN GOTO label_num
アナログ入力ADCI_ADC(reg_dest, adc_idx, pad_idx)
レジスタ読込REG_RDI_RD_REG(reg, low_bit, high_bit)reg = REG_RD[reg][high_bit:low_bit]
レジスタ書込REG_WRI_WR_REG(reg, low_bit, high_bit, val)REG_WR[reg][high_bit:low_bit] = val
ウエイトWAITI_DELAY(cycles_)Delay cycles_
停止HALTI_HALT()
スリープ復帰WAKEI_WAKE()
ULPタイマー停止I_END()

資料集

ULP命令集

上がオフィシャルの資料で、下が私が翻訳した資料です。ULPがわかっていないときに翻訳したので、誤りがあると思います。

Cマクロ

Cマクロのオフィシャルの資料です。ここの内容を解説しましたが、ここにはI_ADC()が書かれていません。定義ファイルから拾ってきて解説しています。

ESP32の内部温度を取得するI_TSENS()も定義はされているのですが、未実装と書いてある資料もあるので、今回は省略しました。

ULP機械語

詳細情報はテクニカルマニュアルのULPのページを参考にしてください。命令集にあったTSENS命令やNOP命令がテクニカルマニュアルにはなぜか載っていません、、、

まとめ

ここまでで、Cマクロで通常使う命令はすべて解説が終わりました。ここまで命令の説明だけやってきたので、どのようにプログラミングするのかはあまり触れてきませんでした。

また、マシン語の構造などについても次回から解説したいと思います。

続編

コメント