Embedded HandbookПриложения
Все разделы › XVIII. ESP32: второй микроконтроллер
ESP32 · уровень средний · 15 мин чтения
Открыть в интерактивном справочнике

157. Сон и малопотребляющий домен ESP32: режимы питания, память RTC, пробуждение, сопроцессор ULP

В разделе 10 STM32 засыпал в Stop и Standby, а резервный домен хранил часы и пару регистров. У ESP32 то же устроено богаче и жёстче. Радио само по себе съедает сотни миллиампер, поэтому экономят не на ядре, а на времени бодрствования. Из глубокого сна чип выходит сбросом, как после включения. А пока основные ядра спят, может работать отдельный крошечный процессор — сопроцессор ULP — со своей программой на своём ассемблере.

Эмулятор здесь помогает меньше всего: ток и сам ULP он не моделирует. Поэтому раздел опирается на три вещи и каждый раз говорит, на какую. Программа в QEMU (_verify/esp157/) проверяет память RTC, загрузку программы ULP и обмен с ней переменными. Сборка показывает саму программу ULP: размер, адреса, символы. Токи — из даташита ESP32 версии 5.3 (выписка datasheet_power.txt), а расчёт батареи — по методике раздела 10.

Как это называется в документации

Оригинал Перевод
Modem-sleep, Light-sleep, Deep-sleep, Hibernation сон радио, лёгкий сон, глубокий сон, гибернация
RTC domain малопотребляющий домен: таймер, память и выводы RTC
RTC fast / slow memory быстрая и медленная память RTC, по 8 КБ
RTC_DATA_ATTR, RTC_NOINIT_ATTR, RTC_FAST_ATTR атрибуты: переменная в памяти RTC
wakeup source, wakeup cause источник пробуждения, причина пробуждения
ext0 / ext1 wakeup пробуждение по одному выводу RTC / по нескольким
ULP coprocessor (FSM) сопроцессор сверхнизкого потребления, автомат
stage counter счётчик стадий ULP для циклов
ulp_embed_binary сборка программы ULP и вшивание её в приложение
wake stub код, выполняемый сразу после пробуждения до загрузчика

157.1 Режимы питания: цифры даташита

Режимы питания ESP32 от передачи Wi-Fi 240 мА до выключенного чипа 1 мкА: что остаётся включённым в каждом режиме; из глубокого сна выход через сброс

Режим Что работает Ток
передача Wi-Fi 802.11b, +19,5 дБм всё 240 мА
передача 802.11g / 802.11n всё 190 / 180 мА
приём Wi-Fi всё 95–100 мА
сон радио (Modem-sleep), 240 МГц ядра, радио выключено 30–68 мА
сон радио, 160 МГц / 80 МГц то же 27–44 / 20–31 мА
лёгкий сон (Light-sleep) ядра на паузе, ОЗУ цело 0,8 мА
глубокий сон, ULP работает домен RTC и сопроцессор 150 мкА
глубокий сон, ULP раз в 1 % времени то же, редкие запуски 100 мкА
глубокий сон: таймер и память RTC только домен RTC 10 мкА
гибернация только таймер RTC 5 мкА
выключен (CHIP_PU в нуле) ничего 1 мкА

Между верхней и нижней строкой — пять порядков. Сон радио включается сам, когда Wi-Fi подключён и программа не передаёт: ESP-IDF выключает приёмник между маяками роутера. Лёгкий сон — аналог Stop у STM32: ядра стоят, ОЗУ и регистры целы, программа продолжает с той же строки. Глубокий сон — аналог Standby: ОЗУ пропадает, выход через сброс, живёт только домен RTC. Гибернация отключает даже память RTC.


157.2 Память RTC: что переживает сброс

У домена RTC своя память: 8 КБ медленной (её видят оба ядра и ULP) и 8 КБ быстрой (только ядро PRO). Переменную кладут туда атрибутом:

RTC_DATA_ATTR static int data_count;                /* медленная память RTC, заливается загрузчиком */
RTC_NOINIT_ATTR static int noinit_count;            /* медленная память RTC, никто не трогает */
RTC_FAST_ATTR static int fast_count;                /* быстрая память RTC, только ядро PRO */

Программа увеличивает все три счётчика, печатает их и перезапускает чип вызовом esp_restart():

сброс 1: RTC_DATA = 1 по 0x50000200, RTC_NOINIT = 1 по 0x50000204, RTC_FAST = 1 по 0x3ff80080
сброс 3: RTC_DATA = 1 по 0x50000200, RTC_NOINIT = 2 по 0x50000204, RTC_FAST = 1 по 0x3ff80080

Код 1 — включение питания, 3 — программный сброс. После сброса дожил только RTC_NOINIT_ATTR: переменные RTC_DATA_ATTR и RTC_FAST_ATTR загрузчик залил заново. Их сохранность обещана только при выходе из глубокого сна — ради него они и существуют. Нужно пережить и программный сброс, и сторожевой таймер — берут RTC_NOINIT_ATTR и сами обнуляют его при включении питания:

    if (esp_reset_reason() == ESP_RST_POWERON)
        noinit_count = 0;

Адреса тоже говорят. В разделе 150 переменная RTC лежала в самом начале медленной памяти, по 0x50000000. Здесь — по 0x50000200: первые 512 байт отданы программе ULP (CONFIG_ULP_COPROC_RESERVE_MEM=512).


157.3 Источники пробуждения

Пробуждение из сна ESP32 умеет по таймеру RTC, по уровню одного вывода RTC (ext0), по сочетанию нескольких выводов (ext1), по касанию сенсорного входа, по сигналу программы ULP, а из лёгкого сна — ещё и по UART и любому GPIO. В ESP-IDF каждый источник включается своим вызовом esp_sleep_enable_…, сон начинается esp_deep_sleep_start() или esp_light_sleep_start(), а после пробуждения esp_sleep_get_wakeup_cause() говорит, кто разбудил. Все три источника программа включила без ошибок:

ulp_set_wakeup_period 20 мс: ESP_OK
esp_sleep_enable_ulp_wakeup: ESP_OK
esp_sleep_enable_timer_wakeup 5 с: ESP_OK

Сам сон начинается так:

        esp_deep_sleep_start();                     /* отсюда не возвращаются: выход из сна — сброс */

Возврата из этого вызова нет: на чипе питание ядер и ОЗУ отключается, и через 5 секунд — или раньше, по сигналу ULP, — чип стартует с загрузчика. Программа узнаёт об этом по причине сброса и по причине пробуждения; в RTC_DATA_ATTR к этому моменту лежит всё, что она сохранила перед сном.


157.4 Сопроцессор ULP: что это

ULP в ESP32 — не второе ядро, а простой автомат: четыре 16-битных регистра R0–R3, счётчик стадий для циклов и 8 КБ медленной памяти RTC, где лежат и его команды, и данные. Команд два десятка: пересылка, сложение и вычитание, сдвиги, переходы, чтение и запись регистров периферии RTC, чтение АЦП, работа с I²C, задержка, wake и halt. Таймер RTC запускает программу с начала раз в заданный период; она делает своё и останавливается командой halt до следующего запуска.

Смысл в токе: основные ядра спят, а ULP раз в 20 мс опрашивает датчик и будит ядра, только если значение вышло за порог. Цена по даташиту — 150 мкА вместо 10, пока сопроцессор включён постоянно, и 100 мкА, если он работает 1 % времени: в 10–15 раз больше глубокого сна, но в 5–8 раз меньше лёгкого. Программа из 24 команд раз в 20 мс работает ещё меньшую долю времени, и ток у неё ниже; насколько — показывает только замер на плате (раздел 10.8).


157.5 Программа для ULP

Программа пишется на ассемблере ULP и лежит в main/ulp/. Эта считает свои запуски и будит ядра на каждом пятом:

entry:
    move r3, counter            /* r3 = адрес слова counter (в словах от начала памяти RTC) */
    ld   r0, r3, 0              /* r0 = counter */
    add  r0, r0, 1
    st   r0, r3, 0              /* counter = counter + 1 */
    move r3, threshold
    ld   r1, r3, 0              /* r1 = threshold */
    sub  r2, r0, r1             /* counter − threshold: заём ставит флаг переполнения */
    jump still_early, ov        /* counter < threshold — ещё рано */

Ядра могут ещё не уснуть к моменту wake. Поэтому перед пробуждением программа читает поле RTC_CNTL_RDY_FOR_WAKEUP и ждёт его в цикле на счётчике стадий, до 20 попыток:

try_wake:
    stage_inc 1
    jumps still_early, 20, gt   /* ядра не готовы 20 попыток подряд — до следующего запуска */
    READ_RTC_FIELD(RTC_CNTL_LOW_POWER_ST_REG, RTC_CNTL_RDY_FOR_WAKEUP)
    and  r0, r0, 1
    jump try_wake, eq           /* ядра ещё засыпают — подождать */
    wake                        /* разбудить основные ядра */

Сборка проекта сама собирает и программу ULP. Это делает одна строка в main/CMakeLists.txt — вызов ulp_embed_binary:

ulp_main.bin 108
магия b'ulp\x00', код с 12, код 96 байт = 24 команд, данные 0, bss 12; файл 108 байт
0000000 75 6c 70 00 0c 00 60 00 00 00 0c 00

Файл — 108 байт: 12 байт заголовка (ulp\0, смещение кода 12, размер кода 0x60 = 96, данных 0, bss 12) и 96 байт кода — 24 команды по 4 байта. Слова counter, threshold и wakeups в файле не лежат: это bss, их место в памяти RTC сразу за кодом.


157.6 Обмен с программой на C

Сборка пишет заголовок ulp_main.h и сценарий компоновщика ulp_main.ld: каждый глобальный символ программы ULP становится переменной C с приставкой ulp_:

ulp_main.h:extern uint32_t ulp_counter;
ulp_main.ld:PROVIDE ( ulp_counter = 0x50000060 );
ulp_main.ld:PROVIDE ( ulp_threshold = 0x50000064 );
ulp_main.ld:PROVIDE ( ulp_wakeups = 0x50000068 );

Адрес 0x50000060 — начало медленной памяти плюс 96 байт кода. Программа на C загружает ULP, пишет порог и запускает:

    esp_err_t e = ulp_load_binary(0, ulp_main_bin_start, bytes / sizeof(uint32_t));
    ulp_threshold = 5;                              /* порог — из C, программа ULP читает его сама */
    printf("ulp_set_wakeup_period 20 мс: %s\n", esp_err_to_name(ulp_set_wakeup_period(0, 20000)));
    printf("esp_sleep_enable_ulp_wakeup: %s\n", esp_err_to_name(esp_sleep_enable_ulp_wakeup()));
    printf("ulp_run: %s\n", esp_err_to_name(ulp_run(&ulp_entry - RTC_SLOW_MEM)));
ulp_load_binary: ESP_OK, файл программы 108 байт
переменные ULP: entry 0x50000000, counter 0x50000060, threshold 0x50000064, wakeups 0x50000068
ulp_threshold = 5, в слове памяти RTC 0x00000005
ulp_set_wakeup_period 20 мс: ESP_OK
esp_sleep_enable_ulp_wakeup: ESP_OK
ulp_run: ESP_OK

Переменные — 32-битные слова, а регистры ULP — 16-битные. Программа ULP видит только младшие 16 бит, поэтому значения от ULP читают как ulp_counter & 0xFFFF. ulp_run принимает не адрес, а номер слова от начала памяти RTC — отсюда &ulp_entry - RTC_SLOW_MEM.

Медленная память RTC: код ULP 96 байт с 0x50000000, слова ulp_counter, ulp_threshold и ulp_wakeups с 0x50000060, резерв 512 байт и переменные RTC_DATA_ATTR и RTC_NOINIT_ATTR с 0x50000200


157.7 Батарея: считать время бодрствования

Датчик на ESP32 раз в 10 минут просыпается, подключается к Wi-Fi и отправляет замер; всё вместе — 2 секунды при среднем токе 100 мА. Остальное время — глубокий сон. Батарея 2000 мА·ч, расчёт как в разделе 10:

Вариант Средний ток Срок батареи
раз в 10 минут, сон с таймером RTC (10 мкА) 0,343 мА 243 дня
раз в 10 минут, между замерами работает ULP (150 мкА) 0,483 мА 173 дня
раз в минуту, сон 10 мкА 3,34 мА 25 дней
всё время лёгкий сон 0,8 мА 104 дня
без сна, сон радио на 80 МГц 20 мА 4 дня

В первой строке сон даёт только 3 % расхода: 97 % съедают две секунды работы радио. Поэтому у ESP32 выигрывают прежде всего реже и короче просыпаясь: запоминают канал и адрес роутера в памяти RTC, берут статический IP вместо DHCP, отправляют пачкой. ULP окупается, когда будит ядра заметно реже, чем они проснулись бы сами. Строка с ULP в таблице — верхняя оценка: 150 мкА — сопроцессор включён всё время; с цифрой даташита для работы 1 % времени, 100 мкА, выходит 0,433 мА и 192 дня.

Средний ток и срок батареи 2000 мА·ч в пяти режимах: доля работы радио и доля сна


157.8 Чего эмулятор не умеет

Глубокий сон эмулятор не выдерживает. На esp_deep_sleep_start() программа падает:

глубокий сон: esp_deep_sleep_start()
Guru Meditation Error: Core  0 panic'ed (LoadStorePIFAddrError). Exception was unhandled.

Код сна пишет в регистры домена RTC, которых в QEMU нет, — отсюда исключение при записи по адресу периферии. После перезапуска программа печатает:

после сна: причина пробуждения 0, ulp_counter 0, ulp_wakeups 0

Причина 0 — ESP_SLEEP_WAKEUP_UNDEFINED: это был не выход из сна, а сброс после паники. Отсюда правила для опытов со сном:


157.9 Грабли

Симптом Причина
после пробуждения программа начинает с app_main глубокий сон — это сброс; состояние хранить в памяти RTC
переменная RTC_DATA_ATTR обнулилась после esp_restart она переживает только глубокий сон; для сброса — RTC_NOINIT_ATTR
RTC_NOINIT_ATTR при включении содержит мусор обнулять самому по ESP_RST_POWERON
программа ULP «не видит» переменную переменная не объявлена .global — в ulp_main.h её нет
ULP читает не то значение регистры 16-битные: писать и читать младшие 16 бит
ulp_run запускает не с начала передан адрес, а нужен номер слова: &ulp_entry - RTC_SLOW_MEM
ток глубокого сна сотни микроампер подтяжки и выводы RTC, светодиод питания, преобразователь на плате
пробуждение по выводу не работает ext0 и ext1 — только выводы RTC, а не любые GPIO
ULP будит ядра, а те не просыпаются нет esp_sleep_enable_ulp_wakeup() или ядра ещё не уснули — ждать RDY_FOR_WAKEUP
батарея садится за неделю долгое подключение к Wi-Fi: DHCP и поиск сети на каждом пробуждении

157.10 Итоги

Полные исходники

main.c — память RTC и обмен переменными с программой ULP.

/* Раздел 157: память RTC и сопроцессор ULP на ESP32.
 * Что переживает сброс, где лежат переменные RTC, как программа ULP попадает в память RTC
 * и как C обменивается с ней переменными. Сон и сам ULP эмулятор не исполняет — это сказано в тексте. */
#include <inttypes.h>
#include <stdio.h>
#include "esp_attr.h"
#include "esp_err.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ulp.h"
#include "ulp_main.h"

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");

RTC_DATA_ATTR static int data_count;                /* медленная память RTC, заливается загрузчиком */
RTC_NOINIT_ATTR static int noinit_count;            /* медленная память RTC, никто не трогает */
RTC_FAST_ATTR static int fast_count;                /* быстрая память RTC, только ядро PRO */

void app_main(void)
{
    if (esp_reset_reason() == ESP_RST_POWERON)
        noinit_count = 0;
    data_count++;
    noinit_count++;
    fast_count++;
    printf("сброс %d: RTC_DATA = %d по %p, RTC_NOINIT = %d по %p, RTC_FAST = %d по %p\n", (int)esp_reset_reason(),
           data_count, (void *)&data_count, noinit_count, (void *)&noinit_count, fast_count, (void *)&fast_count);

    size_t bytes = ulp_main_bin_end - ulp_main_bin_start;
    esp_err_t e = ulp_load_binary(0, ulp_main_bin_start, bytes / sizeof(uint32_t));
    printf("ulp_load_binary: %s, файл программы %u байт\n", esp_err_to_name(e), (unsigned)bytes);
    printf("переменные ULP: entry %p, counter %p, threshold %p, wakeups %p\n",
           (void *)&ulp_entry, (void *)&ulp_counter, (void *)&ulp_threshold, (void *)&ulp_wakeups);

    ulp_threshold = 5;                              /* порог — из C, программа ULP читает его сама */
    printf("ulp_threshold = %" PRIu32 ", в слове памяти RTC 0x%08" PRIx32 "\n", ulp_threshold & 0xFFFF, ulp_threshold);
    printf("ulp_set_wakeup_period 20 мс: %s\n", esp_err_to_name(ulp_set_wakeup_period(0, 20000)));
    printf("esp_sleep_enable_ulp_wakeup: %s\n", esp_err_to_name(esp_sleep_enable_ulp_wakeup()));
    printf("esp_sleep_enable_timer_wakeup 5 с: %s\n", esp_err_to_name(esp_sleep_enable_timer_wakeup(5ULL * 1000 * 1000)));

    if (noinit_count < 2) {                         /* второй запуск — проверить, что пережило сброс */
        fflush(stdout);
        vTaskDelay(pdMS_TO_TICKS(100));
        esp_restart();
    }
    if (noinit_count == 2) {                        /* второй запуск — ULP и глубокий сон */
        printf("ulp_run: %s\n", esp_err_to_name(ulp_run(&ulp_entry - RTC_SLOW_MEM)));
        printf("глубокий сон: esp_deep_sleep_start()\n");
        fflush(stdout);
        esp_deep_sleep_start();                     /* отсюда не возвращаются: выход из сна — сброс */
    }
    printf("после сна: причина пробуждения %d, ulp_counter %" PRIu32 ", ulp_wakeups %" PRIu32 "\n",
           (int)esp_sleep_get_wakeup_cause(), ulp_counter & 0xFFFF, ulp_wakeups & 0xFFFF);
    printf("done\n");
}

main.S — программа сопроцессора ULP: считает запуски и будит ядра по порогу.

/* Раздел 157: программа сопроцессора ULP (ESP32, автомат FSM).
 * Запускается таймером раз в период, пока основные ядра спят: считает запуски
 * и, когда счётчик дошёл до порога, заданного из C, будит ядра. Регистры R0–R3
 * 16-битные, память — 32-битные слова в медленной памяти RTC. */
#include "soc/rtc_cntl_reg.h"
#include "soc/soc_ulp.h"

    .bss
    .global counter
counter:
    .long 0
    .global threshold
threshold:
    .long 0
    .global wakeups
wakeups:
    .long 0

    .text
    .global entry
entry:
    move r3, counter            /* r3 = адрес слова counter (в словах от начала памяти RTC) */
    ld   r0, r3, 0              /* r0 = counter */
    add  r0, r0, 1
    st   r0, r3, 0              /* counter = counter + 1 */
    move r3, threshold
    ld   r1, r3, 0              /* r1 = threshold */
    sub  r2, r0, r1             /* counter − threshold: заём ставит флаг переполнения */
    jump still_early, ov        /* counter < threshold — ещё рано */
    move r3, counter
    move r0, 0
    st   r0, r3, 0              /* counter = 0 */
    move r3, wakeups
    ld   r0, r3, 0
    add  r0, r0, 1
    st   r0, r3, 0              /* wakeups = wakeups + 1 */
    stage_rst
try_wake:
    stage_inc 1
    jumps still_early, 20, gt   /* ядра не готовы 20 попыток подряд — до следующего запуска */
    READ_RTC_FIELD(RTC_CNTL_LOW_POWER_ST_REG, RTC_CNTL_RDY_FOR_WAKEUP)
    and  r0, r0, 1
    jump try_wake, eq           /* ядра ещё засыпают — подождать */
    wake                        /* разбудить основные ядра */
still_early:
    halt                        /* до следующего запуска по таймеру */

Дальше: раздел 158 — Bluetooth LE.

Открыть в интерактивном справочнике