Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESP32: support dynamic freq scaling and wifi power save #5473

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions .gohci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 1
workers:
- name: gohci-u3
checks:
- cmd:
- /home/gohci/repo-info.sh
- cmd:
- /home/gohci/esp32-build.sh
- 4.0
- cmd:
- /home/gohci/esp32-full.sh
- 4.0
- cmd:
- /home/gohci/stm32-build.sh
- cmd:
- /home/gohci/stm32-full.sh
- cmd:
- /home/gohci/esp32-build.sh
- 3.3
- cmd:
- /home/gohci/esp32-full.sh
- 3.3
- cmd:
- /home/gohci/repo-save.sh
11 changes: 11 additions & 0 deletions docs/esp32/quickref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,17 @@ Notes:

p1 = Pin(4, Pin.OUT, None)

Low-power operation
-------------------

See :ref:`esp32-lowpower <esp32-lowpower>` ::

import machine, network
machine.freq(80000000, min_freq=10000000)
wifi = network.WLAN(network.STA_IF)
...
wifi.connect('SSID', 'PASSWD', listen_interval=3)

RMT
---

Expand Down
157 changes: 157 additions & 0 deletions docs/library/esp32-lowpower.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
.. currentmodule:: esp32-lowpower

.. _esp32-lowpower:

:mod:`esp32-lowpower` --- low power options for the ESP32
=========================================================

The ``machine`` and ``network`` modules have a number of options that enable
low power operation on the ESP32. Overall, these options offer approximately a 2x reduction in
operating power while being connected to a WiFi access point by slowing the CPU when
it is idle and by listening to the access point's beacons less frequently. These low
power options are not without drawbacks, in particular, the ESP32 will respond
with more delay to incoming packets or connections as well as potentially to I/O
events.

Note that in addition to the options described below the ESP32 offers light-sleep
and deep-sleep modes
as part of the ``machine`` module: both modes consume less power than the
options presented here but they suspend normal operation of WiFi and I/O.

.. module:: machine
:synopsis: functions related to the hardware

ESP32-specific low-power options in ``machine``
-----------------------------------------------

.. function:: freq(max_freq, [key=None, \*, ...])

The ``machine.freq`` function may be used to set the frequency of the esp32's processor
cores. *max_freq* sets the maximum frequency in Hz and accepts the values
20000000, 40000000, 80000000, 160000000, and 240000000. The optional keyword parameter
*min_freq* sets the minimum frequency, which causes FreeRTOS to reduce the
clock rate when the processor is idle. It accepts the value 10000000 in addition
to those accepted for *max_freq*.

Note that allowing FreeRTOS to change the processor frequency dynamically by setting different
max/min frequencies can affect some I/O peripherals:
- UART, LEDC (``machine.PWM``): not affected
- RMT: frequency varies, need to be fixed (TODO item)
- SPI, I2C, I2S, SDMMC: not affected, they lock the frequency while active
Please consult the ESP-IDF
section on `Dynamic Frequency Scaling
<https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/power_management.html#dynamic-frequency-scaling-and-peripheral-drivers>` for more details.

The optional and highly experimental *auto_light_sleep* keyword parameter allows automatic
light sleep to be enabled in which case the system enters light-sleep mode
automatically when idle (see the power management section of the ESP-IDF documentation).
This setting is currently difficult to use because it causes most
I/O peripherals to stop functioning, including the console UART and many GPIO
pins. It is only provided for completeness and to enable further experimentation
with low-power modes.

.. module:: network
:synopsis: network configuration

ESP32-specific low-power options in ``network``
-----------------------------------------------

.. function:: AbstractNIC.connect([service_id, key=None, \*, ...])

For the WLAN ``STA_IF`` the *connect* function supports an optional
*listen_interval* keyword parameter which causes the WiFi driver to
use the 802.11 power-save-mode (PSM) with the specified beacon-skip interval.

The effect of the listen interval is that the ESP32 tells its access point to queue packets
that are destined for it and to flag the presence of such packets in the standard
WiFi beacon (typ. every 100ms). The ESP32 then enables the radio just in time to receive a
beacon, check the flag, explicitly retrieve queued packets if there are any, and then
it turns the radio off again.

A *listen_interval* value of N >0 causes the ESP32 to wake up and listen every
N beacons (e.g. a value of 5 can cause packets to be queued for up to about 500ms
assuming the standard beacon interval of 100ms).
A value of 0 enables PSM and uses the DTIM value broadcast by the access point as
listen interval. A DTIM setting is available in many access points, but not all.
A value of -1 disables PSM and causes the ESP32 to keep the radio on at all times.
The default value is 1.

Low-power examples
------------------

The following scope capture shows the power consumption in default mode while connected
to Wifi that has DTIM=1 and being pinged once a second. This mode is equivalent to calling
``machine.freq(160000000)`` and ``connect(..., listen_interval=0)``.
(Because the AP's DTIM setting is 1 the same behavior could be observed by setting
*listen_interval=1*.)

.. image:: img/ESP32-micropython-160Mhz.png
:alt: Scope capture of power consumption in default mode
:width: 638px

The blue trace at the bottom shows power consumption in mA at 50mA per vertical division
and a time resolution of 100ms per horizontal division.
For the majority of the time the consumption hovers around 35mA to 60mA but every 100ms
it spikes up to about 120-140mA when the WiFi radio is turned on to receive the
access point's beacon. At the trigger point (500ms into the trace) the beacons indicates
that a packet is queued (presumably due to the pings) and the ESP32 picks-up the packet
and responds to the ping (the first thicker spike up to about 190mA), delays for approx
60ms, and then transmits to the AP that it is re-entering power-save-mode (the thinner
spike to ~190mA).

Sample output from the ping (running on a Linux box on the same network) shows delays
up to about 100ms::
64 bytes from 192.168.0.124: icmp_seq=1 ttl=255 time=49.2 ms
64 bytes from 192.168.0.124: icmp_seq=2 ttl=255 time=67.5 ms
64 bytes from 192.168.0.124: icmp_seq=3 ttl=255 time=95.1 ms
64 bytes from 192.168.0.124: icmp_seq=4 ttl=255 time=114 ms
64 bytes from 192.168.0.124: icmp_seq=5 ttl=255 time=35.4 ms
64 bytes from 192.168.0.124: icmp_seq=6 ttl=255 time=57.5 ms

The cyan trace at the top shows when the micropython interpreter sleeps, i.e. yields the
application processor core: while high the processor is yielded and while low micropython
runs. In this capture the interpreter is idle and just wakes up every 400ms to check
events and yield again.

The next scope capture shows the same situation but with
``machine.freq(80000000, min_freq=10000000)`` and ``connect(..., listen_interval=5)```.

.. image:: img/ESP32-micropython-10-80Mhz-li5.png
:alt: Scope capture of power consumption in low-power mode
:width: 637px

In this capture the scope settings are identical to above. The idle power consumption is now reduced
to approx 12mA and when the radio is on the consumption is around 115mA. The trigger point
again shows an incoming ping and response with about the same timing as previously.
However the frequency at which the ESP32 listens to the access point's beacons is now
500ms as requested with the ``listen_interval`` parameter. One such listen period can be seen
20ms before the end of the trace.

Sample output from the ping shows irregular and sometimes long delays::
64 bytes from 192.168.0.124: icmp_seq=44 ttl=255 time=86.9 ms
64 bytes from 192.168.0.124: icmp_seq=45 ttl=255 time=110 ms
64 bytes from 192.168.0.124: icmp_seq=46 ttl=255 time=136 ms
64 bytes from 192.168.0.124: icmp_seq=47 ttl=255 time=399 ms
64 bytes from 192.168.0.124: icmp_seq=48 ttl=255 time=76.5 ms
64 bytes from 192.168.0.124: icmp_seq=49 ttl=255 time=97.8 ms

Averaged out these scope traces show a reduction of power consumption from around 63mA to
around 25mA, but this should be taken as a rough estimate only because the processor utilitzation
and WiFi traffic have a big impact on the average consumption.

For completeness, the following cropped capture shows power consumption with
PSM turned off, i.e., ``machine.freq(160000000)`` and ``connect(..., listen_interval=-1)``.

.. image:: img/ESP32-micropython-160Mhz-noli.png
:alt: Scope capture of power consumption with power-save off
:width: 636px

While the average power consumption is around 125mA the ping response times are better than with
power-save enabled::
64 bytes from 192.168.0.124: icmp_seq=64 ttl=255 time=2.59 ms
64 bytes from 192.168.0.124: icmp_seq=65 ttl=255 time=2.42 ms
64 bytes from 192.168.0.124: icmp_seq=66 ttl=255 time=1.68 ms
64 bytes from 192.168.0.124: icmp_seq=67 ttl=255 time=1.36 ms
64 bytes from 192.168.0.124: icmp_seq=68 ttl=255 time=1.62 ms
64 bytes from 192.168.0.124: icmp_seq=69 ttl=255 time=1.30 ms

1 change: 1 addition & 0 deletions docs/library/esp32.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
The ``esp32`` module contains functions and classes specifically aimed at
controlling ESP32 modules.

To adjust operating power see :ref:`esp32-lowpower <esp32-lowpower>`.

Functions
---------
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/library/img/ESP32-micropython-160Mhz.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ports/esp32/boards/sdkconfig.base
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ CONFIG_PM_ENABLE=y
CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=2
CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y
CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP=y
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y

# UDP
CONFIG_LWIP_PPP_SUPPORT=y
Expand Down
3 changes: 3 additions & 0 deletions ports/esp32/esp32_rmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ STATIC mp_obj_t esp32_rmt_make_new(const mp_obj_type_t *type, size_t n_args, siz
mp_raise_ValueError(MP_ERROR_TEXT("clock_div must be between 1 and 255"));
}

// TODO: provide an option to use REF_TICK (1Mhz) to enable low-power operation
// with dynamic frequency scaling.

esp32_rmt_obj_t *self = m_new_obj_with_finaliser(esp32_rmt_obj_t);
self->base.type = &esp32_rmt_type;
self->channel_id = channel_id;
Expand Down
5 changes: 4 additions & 1 deletion ports/esp32/machine_pwm.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ STATIC ledc_timer_config_t timer_cfg = {
.duty_resolution = PWRES,
.freq_hz = PWFREQ,
.speed_mode = PWMODE,
.timer_num = PWTIMER
.timer_num = PWTIMER,
#ifdef LEDC_USE_REF_TICK
.clk_cfg = LEDC_USE_REF_TICK, // using REF_TICK to allow dynamic freq scaling
#endif
};

STATIC void pwm_init(void) {
Expand Down
6 changes: 4 additions & 2 deletions ports/esp32/machine_uart.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ STATIC void machine_uart_init_helper(machine_uart_obj_t *self, size_t n_args, co
}
uart_config_t uartcfg = {
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.rx_flow_ctrl_thresh = 0
.rx_flow_ctrl_thresh = 0,
.use_ref_tick = 1,
};
uint32_t baudrate;
uart_get_baudrate(self->uart_num, &baudrate);
Expand Down Expand Up @@ -267,7 +268,8 @@ STATIC mp_obj_t machine_uart_make_new(const mp_obj_type_t *type, size_t n_args,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.rx_flow_ctrl_thresh = 0
.rx_flow_ctrl_thresh = 0,
.use_ref_tick = 1,
};

// create instance
Expand Down
9 changes: 9 additions & 0 deletions ports/esp32/make
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#! /usr/bin/bash
export ESPIDF=/home/src/esp32/esp-idf-micropython
export IDF_PATH=${ESPIDF}
export PATH=/home/src/esp32/esp-idf-micropython/xtensa-esp32-elf/bin:${PATH}
export BOARD=${BOARD:-GENERIC}
export PORT=${PORT:-/dev/ttyUSB0}
#CROSS_COMPILE = xtensa-esp32-elf-

make -j4 "$@"
72 changes: 55 additions & 17 deletions ports/esp32/modmachine.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,69 @@ typedef enum {
MP_SOFT_RESET
} reset_reason_t;

STATIC mp_obj_t machine_freq(size_t n_args, const mp_obj_t *args) {
STATIC mp_obj_t machine_freq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
if (n_args == 0) {
// get
return mp_obj_new_int(esp_clk_cpu_freq());
} else {
// set
mp_int_t freq = mp_obj_get_int(args[0]) / 1000000;
if (freq != 20 && freq != 40 && freq != 80 && freq != 160 && freq != 240) {
mp_raise_ValueError(MP_ERROR_TEXT("frequency must be 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz"));
}

// setting freq/sleep
enum {ARG_freq, ARG_min_freq, ARG_auto_light_sleep};
const mp_arg_t allowed_args[] = {
{ MP_QSTR_freq, MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_min_freq, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_auto_light_sleep, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

// validate frequency
mp_int_t freq = args[ARG_freq].u_int / 1000000;
if (freq != 20 && freq != 40 && freq != 80 && freq != 160 && freq != 240) {
mp_raise_ValueError(MP_ERROR_TEXT("frequency must be 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz"));
}
esp_pm_config_esp32_t pm;
pm.max_freq_mhz = freq;
pm.min_freq_mhz = freq;
pm.light_sleep_enable = false;

// check optional mininum frequency keyword argument
if (args[ARG_min_freq].u_int != 0) {
mp_int_t mf = args[ARG_min_freq].u_int / 1000000;
if (mf != 10 && mf != 20 && mf != 40 && mf != 80 && mf != 160 && mf != 240) {
mp_raise_ValueError(MP_ERROR_TEXT("frequency must be 10Mhz, 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz"));
}
esp_pm_config_esp32_t pm;
pm.max_freq_mhz = freq;
pm.min_freq_mhz = freq;
pm.light_sleep_enable = false;
esp_err_t ret = esp_pm_configure(&pm);
if (ret != ESP_OK) {
pm.min_freq_mhz = mf;
}

#if 0
// commented-out because it is ineffective unless the calls to ulTaskNotifyTake in
// mphalport.c use a delay of 4 ticks minimum. Don't want to change those due to
// insufficiently explored side-effects and auto-light-sleep is not that usable in
// the current state anyway... leaving this in for future reference

// check optional auto-light-sleep keyword argument
if (args[ARG_auto_light_sleep].u_bool) {
pm.light_sleep_enable = true;
}
#endif

// apply new setting and check result
esp_err_t ret = esp_pm_configure(&pm);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NOT_SUPPORTED) {
mp_raise_ValueError(MP_ERROR_TEXT("auto light-sleep not supported"));
} else {
mp_printf(&mp_plat_print, "esp_pm_configure ret=%d\n", ret);
mp_raise_ValueError(NULL);
}
while (esp_clk_cpu_freq() != freq * 1000000) {
vTaskDelay(1);
}
return mp_const_none;
}
while (esp_clk_cpu_freq() != freq * 1000000) {
vTaskDelay(1);
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_freq_obj, 0, 1, machine_freq);
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(machine_freq_obj, 0, machine_freq);

STATIC mp_obj_t machine_sleep_helper(wake_type_t wake_type, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {

Expand Down
13 changes: 12 additions & 1 deletion ports/esp32/modnetwork.c
Original file line number Diff line number Diff line change
Expand Up @@ -320,18 +320,20 @@ STATIC mp_obj_t esp_active(size_t n_args, const mp_obj_t *args) {
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_active_obj, 1, 2, esp_active);

STATIC mp_obj_t esp_connect(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_ssid, ARG_password, ARG_bssid };
enum { ARG_ssid, ARG_password, ARG_bssid, ARG_listen_interval };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_, MP_ARG_OBJ, {.u_obj = mp_const_none} },
{ MP_QSTR_, MP_ARG_OBJ, {.u_obj = mp_const_none} },
{ MP_QSTR_bssid, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
{ MP_QSTR_listen_interval, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
};

// parse args
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

wifi_config_t wifi_sta_config = {{{0}}};
uint8_t ps_mode = WIFI_PS_MIN_MODEM;

// configure any parameters that are given
if (n_args > 1) {
Expand All @@ -353,8 +355,17 @@ STATIC mp_obj_t esp_connect(size_t n_args, const mp_obj_t *pos_args, mp_map_t *k
wifi_sta_config.sta.bssid_set = 1;
memcpy(wifi_sta_config.sta.bssid, p, sizeof(wifi_sta_config.sta.bssid));
}
if (args[ARG_listen_interval].u_int > 0) {
wifi_sta_config.sta.listen_interval = args[ARG_listen_interval].u_int;
ps_mode = WIFI_PS_MAX_MODEM;
} else if (args[ARG_listen_interval].u_int < 0) {
ps_mode = WIFI_PS_NONE;
}

// apply config
ESP_EXCEPTIONS(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_sta_config));
}
esp_wifi_set_ps(ps_mode); // set power-save mode depending on listen_interval

// connect to the WiFi AP
MP_THREAD_GIL_EXIT();
Expand Down