低消費電力の WiFi モジュール ESP32 をさらに少ない電力で使う方法を紹介します.
はじめに
ESP32 は消費電力が少ない IC ですが,CPU 周波数を 80MHz ほどに落としても少なくとも 20mA 程度は流れます.単三アルカリ乾電池だと約 2000mAh なので,2個使いだとざっくり 2000 * (1.5*2 / 3.3) / 20 / 24 = 3.8日で空になってしまいます.
一方,ESP32 には,低消費電力で動作する ULP (Ultra Low Power) コプロセッサが内蔵されています(下図赤丸).
ULP は 8MHz で動作するシンプルなプロセッサで,4 つの 16bit レジスタと 30 程度の命令を備えています.CPU を deep sleep にした状態で動かすことができます.
そのため,消費電力に対する要求が非常に厳しい条件でも ESP32 を活用できるようになります.
今回は,ULP を使って,高性能な温湿度センサである SHT-3x を I2C 通信で制御する例を紹介します.
動作概要
今回作成するプログラムは下図のような動きをします.
CPU プログラムは ULP プログラムを走らせたらすぐに deep sleep に遷移します.ULP プログラムは定期的に起きて計測を行い,一定数計測データが溜まったら CPU プログラムを起こします.測定データは,deep sleep モード中にも値を保持できる,RTC SLOW memory に保存します.
準備
ソフトをビルドするために,ESP-IDF と binutils-esp32ulp をセットアップします.binutils-esp32ulp のダウンロードはこちらから.
ただし,現時点の binutils-esp32ulp の最新リリース(esp32ulp-elf-binutils-*-7b4f341)はアセンブラにバグがあるので,リポジトリからソースを採ってきて次のようにビルドする必要があります.
[18年6月24日追記: この作業はもはや不要です]
1 2 3 4 5 6 |
git clone git@github.com:espressif/binutils-esp32ulp.git cd binutils-esp32ulp ./configure --target=esp32ulp-elf make # リリースビルドの as を上書き cp -f gas/as-new /path/to/esp32ulp-elf-binutils/bin/esp32ulp-elf-as |
ULP 側コード
ULP プログラム全体は,こちらのリポジトリにあります.
ULP は I2C 通信専用の命令も持っているのですが,SHT-3x で必要な通信シーケンスを発行できないので,今回は I2C 通信も含めてソフト(Bit Bang)で実現しています.
ULP の各ソースファイルについて,簡単に説明していきます.
main/ulp/i2c_sht3x.S
SHT-3x 固有の処理を記載しています.sense_sht3x が計測処理本体で,実行すると計測結果が sht3x_sense_value から始まる 4 word に保存されます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
#include "soc/rtc_cntl_reg.h" #include "soc/rtc_io_reg.h" #include "soc/soc_ulp.h" #include "util_macro.S" .set SHT31_DEV_ADDR,0x88 // 8bit representation (0x44 in 7bit) .set SHT31_CMD_MSB,0x24 .set SHT31_CMD_LSB,0x00 //////////////////////////////////////////////////////////// .bss .global sht3x_sense_value sht3x_sense_value: .long 0 .skip 16 //////////////////////////////////////////////////////////// .text .global sense_sht3x .global sht3x_read_value //////////////////////////////////////////////////////////// sense_sht3x: move r1,SHT31_DEV_ADDR push r1 move r1,SHT31_CMD_MSB push r1 move r1,SHT31_CMD_LSB push r1 psr jump i2c_write_reg8 add r3,r3,3 jumpr sht3x_sense_fail,1,ge wait (8*8000) // 8ms (RTC_FAST_CLK = 8Mhz) wait (8*8000) // 8ms (RTC_FAST_CLK = 8Mhz) wait (8*8000) // 8ms (RTC_FAST_CLK = 8Mhz) move r1,SHT31_DEV_ADDR push r1 move r1,sht3x_sense_value push r1 psr jump sht3x_read_value add r3,r3,2 ret //////////////////////////////////////////////////////////// sht3x_read_value: psr jump i2c_start ld r2,r3,12 // device addeess or r2,r2,1 // read psr jump i2c_write_byte jumpr sht3x_sense_fail,1,ge move r2,0 // ACK psr jump i2c_read_byte push r0 move r2,0 // ACK psr jump i2c_read_byte pop r1 lsh r1,r1,8 or r0,r0,r1 ld r1,r3,8 st r0,r1,0 // sensor value[0] move r2,0 // ack psr jump i2c_read_byte ld r1,r3,8 st r0,r1,4 // sensor value[1] move r2,0 // ACK psr jump i2c_read_byte push r0 move r2,0 // ACK psr jump i2c_read_byte pop r1 lsh r1,r1,8 or r0,r0,r1 ld r1,r3,8 st r0,r1,8 // sensor value[2] move r2,1 // NACK psr jump i2c_read_byte ld r2,r3,8 st r0,r2,12 // sensor value[3] jump i2c_return_success //////////////////////////////////////////////////////////// sht3x_sense_fail: ld r1,r3,8 move r0,0xFFFF st r0,r1,0 // sensor value[0] st r0,r1,4 // sensor value[1] st r0,r1,8 // sensor value[2] st r0,r1,12 // sensor value[3] jump i2c_return_fail |
main/ulp/i2c.S
I2C 関連の処理を記載しています.SDA は GPIO 26,SCL として GPIO25 を使用します.クロックは 200kHz となります.
GPIO 端子については,同じ端子に対する CPU 側と ULP 側の番号の割り振りが異なるので注意.詳細はリファレンスマニュアルの「RTC_MUX Pin Summary」参照.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
#include "soc/rtc_cntl_reg.h" #include "soc/rtc_io_reg.h" #include "soc/soc_ulp.h" #include "util_macro.S" .set I2C_SCL_REG_OFFSET,7 // GPIO_NUM_26 .set I2C_SDA_REG_OFFSET,6 // GPIO_NUM_25 .macro i2c_wait_quarter_clock wait 10 // 8Mhz /10 = 800kHz, clock = 800kHz /4 = 200kHz .endm .macro i2c_scl_L WRITE_RTC_REG(RTC_GPIO_ENABLE_W1TS_REG, RTC_GPIO_ENABLE_W1TS_S + I2C_SCL_REG_OFFSET, 1, 1) .endm .macro i2c_scl_H WRITE_RTC_REG(RTC_GPIO_ENABLE_W1TC_REG, RTC_GPIO_ENABLE_W1TC_S + I2C_SCL_REG_OFFSET, 1, 1) .endm .macro i2c_sda_L WRITE_RTC_REG(RTC_GPIO_ENABLE_W1TS_REG, RTC_GPIO_ENABLE_W1TS_S + I2C_SDA_REG_OFFSET, 1, 1) .endm .macro i2c_sda_H WRITE_RTC_REG(RTC_GPIO_ENABLE_W1TC_REG, RTC_GPIO_ENABLE_W1TC_S + I2C_SDA_REG_OFFSET, 1, 1) .endm .macro i2c_sda_read READ_RTC_REG(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT_S + I2C_SDA_REG_OFFSET, 1) .endm .text .global i2c_write_reg8 .global i2c_return_fail .global i2c_return_success .global i2c_stop .global i2c_start .global i2c_write_byte .global i2c_read_byte .global i2c_write_bit .global i2c_read_bit //////////////////////////////////////////////////////////// // usage: // move r1,"DEVICE ADDRESS" // push r1 // move r1,"REGISTER ADDRESS" // push r1 // move r1,"VALUE" // push r1 // psr // jump i2c_write_reg8 // add r3,r3,3 i2c_write_reg8: psr jump i2c_start ld r2,r3,16 // device addeess psr jump i2c_write_byte jumpr i2c_return_fail,1,ge ld r2,r3,12 // register address psr jump i2c_write_byte jumpr i2c_return_fail,1,ge ld r2,r3,8 // value psr jump i2c_write_byte psr jump i2c_stop jumpr i2c_return_fail,1,ge jump i2c_return_success //////////////////////////////////////////////////////////// i2c_return_fail: move r0,1 ret //////////////////////////////////////////////////////////// i2c_return_success: move r0,0 ret //////////////////////////////////////////////////////////// i2c_start: WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + I2C_SCL_REG_OFFSET, 1, 0) WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + I2C_SDA_REG_OFFSET, 1, 0) i2c_sda_L i2c_wait_quarter_clock ret //////////////////////////////////////////////////////////// i2c_stop: i2c_wait_quarter_clock i2c_scl_L i2c_wait_quarter_clock i2c_wait_quarter_clock i2c_scl_H i2c_wait_quarter_clock ret //////////////////////////////////////////////////////////// // r2: the byte to write i2c_write_byte: stage_rst _i2c_write_byte_next_bit: and r0,r2,0x80 psr jump i2c_write_bit lsh r2,r2,1 stage_inc 1 jumps _i2c_write_byte_next_bit,8,lt psr jump i2c_read_bit ret //////////////////////////////////////////////////////////// // r2: send ack (0: ACK, 1: NACK) i2c_read_byte: push r2 move r2,0 stage_rst _i2c_read_byte_next_bit: psr jump i2c_read_bit lsh r2,r2,1 or r2,r2,r0 stage_inc 1 jumps _i2c_read_byte_next_bit,8,lt pop r0 psr jump i2c_write_bit move r0,r2 ret //////////////////////////////////////////////////////////// // r0: the bit to write (0 or 1) i2c_write_bit: i2c_wait_quarter_clock i2c_scl_L i2c_wait_quarter_clock jumpr _i2c_write_bit_0,1,lt _i2c_write_bit_1: i2c_sda_H jump _i2c_write_tick _i2c_write_bit_0: i2c_sda_L _i2c_write_tick: i2c_wait_quarter_clock i2c_scl_H i2c_wait_quarter_clock ret //////////////////////////////////////////////////////////// i2c_read_bit: i2c_wait_quarter_clock i2c_scl_L i2c_wait_quarter_clock i2c_sda_H i2c_wait_quarter_clock i2c_scl_H i2c_sda_read i2c_wait_quarter_clock ret |
main/ulp/main.S
ULP のプログラム本体です.sense_count で指定された回数だけ SHT-3x による計測を行った後,CPU を起こします.
ULP 側のプログラムで定義した変数は,CPU 側からは接頭辞「ulp_」をつけてアクセスできますので,CPU 側からは ulp_sense_count という変数に対して書き込みを行う形になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
#include "soc/rtc_cntl_reg.h" #include "soc/rtc_io_reg.h" #include "soc/soc_ulp.h" #include "util_macro.S" .set SENSE_SIZE,4 .set SENSE_BUF_ENTRY,10 .set SENSE_BUF_SIZE,(4*SENSE_BUF_ENTRY*SENSE_SIZE) .bss .global sense_count sense_count: .long 0 .balign 4 .global sense_data sense_data: .long 0 .skip SENSE_BUF_SIZE .global stack stack: .skip 100 .global stackEnd stackEnd: .long 0 .text .global entry entry: move r3,stackEnd // Check whether CPU is in deep sleep or not // // https://github.com/espressif/esp-idf/issues/484 // > This bit indicates that the SoC is sleeping and RTC is ready to receive wakeup command. READ_RTC_REG(RTC_CNTL_DIAG0_REG, 19, 1) and r0, r0, 1 jump exit, eq sleep 0 psr jump sense_sht3x move r2,sense_count ld r0,r2,0 move r1,sense_data _sense_data_pos: sub r0,r0,1 jumpr _copy_sense_data,1,lt add r1,r1,SENSE_SIZE jump _sense_data_pos _copy_sense_data: move r2,sht3x_sense_value ld r0,r2,0 st r0,r1,0 ld r0,r2,4 st r0,r1,4 ld r0,r2,8 st r0,r1,8 ld r0,r2,12 st r0,r1,12 // decrement sense_count move r2,sense_count ld r0,r2,0 sub r0,r0,1 jumpr wake_cpu,1,lt st r0,r2,0 halt wake_cpu: // reset sense_count move r0,0 st r0,r2,0 wake // stop wake up timer WRITE_RTC_FIELD(RTC_CNTL_STATE0_REG, RTC_CNTL_ULP_CP_SLP_TIMER_EN, 0) exit: halt |
main/ulp/util_macro.S
スタック操作のマクロです.レジスタ r3 はスタックポインタとして占有する前提になっています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
.macro push rx st \rx,r3,0 sub r3,r3,1 .endm .macro pop rx add r3,r3,1 ld \rx,r3,0 .endm .macro psr .set Lret_addr,(.+16) move r1,Lret_addr push r1 .endm .macro ret pop r1 jump r1 .endm |
CPU 側コード
最初の起動時に ULP のプロフラムのロードと GPIO の初期化を行い,それ以降は起きる度に計測結果に対する処理を行う形になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#define SENSE_INTERVAL 30 // sensing interval #define SENSE_COUNT 10 // buffering count extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start"); extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end"); const gpio_num_t gpio_scl = GPIO_NUM_26; const gpio_num_t gpio_sda = GPIO_NUM_25; static void init_ulp_program() { rtc_gpio_init(gpio_scl); rtc_gpio_set_direction(gpio_scl, RTC_GPIO_MODE_INPUT_ONLY); rtc_gpio_init(gpio_sda); rtc_gpio_set_direction(gpio_sda, RTC_GPIO_MODE_INPUT_ONLY); ESP_ERROR_CHECK( ulp_load_binary( 0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t) ) ); REG_SET_FIELD(SENS_ULP_CP_SLEEP_CYC0_REG, SENS_SLEEP_CYCLES_S0, rtc_clk_slow_freq_get_hz()*SENSE_INTERVAL); } void app_main() { if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_ULP) { init_ulp_program(); } else { // ここに来たときは,計測結果が ulp_sense_data に入っている } ulp_sense_count = SENSE_COUNT; ESP_ERROR_CHECK(esp_sleep_enable_ulp_wakeup()); ESP_ERROR_CHECK(ulp_run((&ulp_entry - RTC_SLOW_MEM) / sizeof(uint32_t))); esp_deep_sleep_start(); } |
消費電流
20秒ごとに SHT-3x で計測する設定で動かした場合,消費電流は下図のようになりました.
SHT-3x のデータシートによると測定中の消費電流は typ 0.8mA なので,定期的に増えている消費電流はほぼ SHT-3x によるものと推測されます.
測定時以外の電流値は 5uA 程度でした.
平均消費電流は 0.024 mA となり,乾電池 2個使いでも 2000 * (1.5*2 / 3.3) / 0.024 / 24 = 3156日持つ計算になります.実際の使用条件での電池寿命は WiFi 通信(150mA程度) の頻度が規定する形になりそうです.
まとめ
ESP32 の ULP を使うことで,CPU を使う場合に比べて 100分の1以下の電力で I2C による温湿度計測ができることが確認できました.
コメント
[…] . 参考: bitluni:ULPSoundESP32:今回のスケッチがあります。 espressif:espressif/esp-iot-solution:ULPコプロセッサおよびアセンブリ環境設定の概要 espressif:esp32 ulp programming:esp32 ulpプログラミング docs.espressif:ULP coprocessor instruction set:ULPコプロセッサ命令セット Rabbit Note:ESP32 の ULP コプロセッサを使って超低消費電力 I2C 通信: […]