8000 ports/esp32: support dynamic freq scaling and wifi power save · micropython/micropython@32cc9e0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 32cc9e0

Browse files
committed
ports/esp32: support dynamic freq scaling and wifi power save
ports/esp32: fix some peripheral clocks for low-power op
1 parent 1993c8c commit 32cc9e0

15 files changed

+258
-23
lines changed

docs/esp32/quickref.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,17 @@ Notes:
366366

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

369+
Low-power operation
370+
-------------------
371+
372+
See :ref:`esp32-lowpower <esp32-lowpower>` ::
373+
374+
import machine, network
375+
machine.freq(80000000, min_freq=10000000)
376+
wifi = network.WLAN(network.STA_IF)
377+
...
378+
wifi.connect('SSID', 'PASSWD', listen_interval=3)
379+
369380
RMT
370381
---
371382

docs/library/esp32-lowpower.rst

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
.. currentmodule:: esp32-lowpower
2+
3+
.. _esp32-lowpower:
4+
5+
:mod:`esp32-lowpower` --- low power options for the ESP32
6+
=========================================================
7+
8+
The ``machine`` and ``network`` modules have a number of options that enable
9+
low power operation on the ESP32. Overall, these options offer approximately a 2x reduction in
10+
operating power while being connected to a WiFi access point by slowing the CPU when
11+
it is idle and by listening to the access point's beacons less frequently. These low
12+
power options are not without drawbacks, in particular, the ESP32 will respond
13+
with more delay to incoming packets or connections as well as potentially to I/O
14+
events.
15+
16+
Note that in addition to the options described below the ESP32 offers light-sleep
17+
and deep-sleep modes
18+
as part of the ``machine`` module: both modes consume less power than the
19+
options presented here but they suspend normal operation of WiFi and I/O.
20+
21+
.. module:: machine
22+
:synopsis: functions related to the hardware
23+
24+
ESP32-specific low-power options in ``machine``
25+
-----------------------------------------------
26+
27+
.. function:: freq(max_freq, [key=None, \*, ...])
28+
29+
The ``machine.freq`` function may be used to set the frequency of the esp32's processor
30+
cores. *max_freq* sets the maximum frequency in Hz and accepts the values
31+
20000000, 40000000, 80000000, 160000000, and 240000000. The optional keyword parameter
32+
*min_freq* sets the minimum frequency, which causes FreeRTOS to reduce the
33+
clock rate when the processor is idle. It accepts the value 10000000 in addition
34+
to those accepted for *max_freq*.
35+
36+
Note that allowing FreeRTOS to change the processor frequency dynamically by setting different
37+
max/min frequencies can affect some I/O peripherals:
38+
- UART, LEDC (``machine.PWM``): not affected
39+
- RMT: frequency varies, need to be fixed (TODO item)
40+
- SPI, I2C, I2S, SDMMC: not affected, they lock the frequency while active
41+
Please consult the ESP-IDF
42+
section on `Dynamic Frequency Scaling
43+
<https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/power_management.html#dynamic-frequency-scaling-and-peripheral-drivers>` for more details.
44+
45+
The optional and highly experimental *auto_light_sleep* keyword parameter allows automatic
46+
light sleep to be enabled in which case the system enters light-sleep mode
47+
automatically when idle (see the power management section of the ESP-IDF documentation).
48+
This setting is currently difficult to use because it causes most
49+
I/O peripherals to stop functioning, including the console UART and many GPIO
50+
pins. It is only provided for completeness and to enable further experimentation
51+
with low-power modes.
52+
53+
.. module:: network
54+
:synopsis: network configuration
55+
56+
ESP32-specific low-power options in ``network``
57+
-----------------------------------------------
58+
59+
.. function:: AbstractNIC.connect([service_id, key=None, \*, ...])
60+
61+
For the WLAN ``STA_IF`` the *connect* function supports an optional
62+
*listen_interval* keyword parameter which causes the WiFi driver to
63+
use the 802.11 power-save-mode (PSM) with the specified beacon-skip interval.
64+
65+
The effect of the listen interval is that the ESP32 tells its access point to queue packets
66+
that are destined for it and to flag the presence of such packets in the standard
67+
WiFi beacon (typ. every 100ms). The ESP32 then enables the radio just in time to receive a
68+
beacon, check the flag, explicitly retrieve queued packets if there are any, and then
69+
it turns the radio off again.
70+
71+
A *listen_interval* value of N >0 causes the ESP32 to wake up and listen every
72+
N beacons (e.g. a value of 5 can cause packets to be queued for up to about 500ms
73+
assuming the standard beacon interval of 100ms).
74+
A value of 0 enables PSM and uses the DTIM value broadcast by the access point as
75+
listen interval. A DTIM setting is available in many access points, but not all.
76+
A value of -1 disables PSM and causes the ESP32 to keep the radio on at all times.
77+
The default value is 1.
78+
79+
Low-power examples
80+
------------------
81+
82+
The following scope capture shows the power consumption in default mode while connected
83+
to Wifi that has DTIM=1 and being pinged once a second. This mode is equivalent to calling
84+
``machine.freq(160000000)`` and ``connect(..., listen_interval=0)``.
85+
(Because the AP's DTIM setting is 1 the same behavior could be observed by setting
86+
*listen_interval=1*.)
87+
88+
.. image:: img/ESP32-micropython-160Mhz.png
89+
:alt: Scope capture of power consumption in default mode
90+
:width: 638px
91+
92+
The blue trace at the bottom shows power consumption in mA at 50mA per vertical division
93+
and a time resolution of 100ms per horizontal division.
94+
For the majority of the time the consumption hovers around 35mA to 60mA but every 100ms
95+
it spikes up to about 120-140mA when the WiFi radio is turned on to receive the
96+
access point's beacon. At the trigger point (500ms into the trace) the beacons indicates
97+
that a packet is queued (presumably due to the pings) and the ESP32 picks-up the packet
98+
and responds to the ping (the first thicker spike up to about 190mA), delays for approx
99+
60ms, and then transmits to the AP that it is re-entering power-save-mode (the thinner
100+
spike to ~190mA).
101+
102+
Sample output from the ping (running on a Linux box on the same network) shows delays
103+
up to about 100ms::
104+
64 bytes from 192.168.0.124: icmp_seq=1 ttl=255 time=49.2 ms
105+
64 bytes from 192.168.0.124: icmp_seq=2 ttl=255 time=67.5 ms
106+
64 bytes from 192.168.0.124: icmp_seq=3 ttl=255 time=95.1 ms
107+
64 bytes from 192.168.0.124: icmp_seq=4 ttl=255 time=114 ms
108+
64 bytes from 192.168.0.124: icmp_seq=5 ttl=255 time=35.4 ms
109+
64 bytes from 192.168.0.124: icmp_seq=6 ttl=255 time=57.5 ms
110+
111+
The cyan trace at the top shows when the micropython interpreter sleeps, i.e. yields the
112+
application processor core: while high the processor is yielded and while low micropython
113+
runs. In this capture the interpreter is idle and just wakes up every 400ms to check
114+
events and yield again.
115+
116+
The next scope capture shows the same situation but with
117+
``machine.freq(80000000, min_freq=10000000)`` and ``connect(..., listen_interval=5)```.
118+
119+
.. image:: img/ESP32-micropython-10-80Mhz-li5.png
120+
:alt: Scope capture of power consumption in low-power mode
121+
:width: 637px
122+
123+
In this capture the scope settings are identical to above. The idle power consumption is now reduced
124+
to approx 12mA and when the radio is on the consumption is around 115mA. The trigger point
125+
again shows an incoming ping and response with about the same timing as previously.
126+
However the frequency at which the ESP32 listens to the access point's beacons is now
127+
500ms as requested with the ``listen_interval`` parameter. One such listen period can be seen
128+
20ms before the end of the trace.
129+
130+
Sample output from the ping shows irregular and sometimes long delays::
131+
64 bytes from 192.168.0.124: icmp_seq=44 ttl=255 time=86.9 ms
132+
64 bytes from 192.168.0.124: icmp_seq=45 ttl=255 time=110 ms
133+
64 bytes from 192.168.0.124: icmp_seq=46 ttl=255 time=136 ms
134+
64 bytes from 192.168.0.124: icmp_seq=47 ttl=255 time=399 ms
135+
64 bytes from 192.168.0.124: icmp_seq=48 ttl=255 time=76.5 ms
136+
64 bytes from 192.168.0.124: icmp_seq=49 ttl=255 time=97.8 ms
137+
138+
Averaged out these scope traces show a reduction of power consumption from around 63mA to
139+
around 25mA, but this should be taken as a rough estimate only because the processor utilitzation
140+
and WiFi traffic have a big impact on the average consumption.
141+
142+
For completeness, the following cropped capture shows power consumption with
143+
PSM turned off, i.e., ``machine.freq(160000000)`` and ``connect(..., listen_interval=-1)``.
144+
145+
.. image:: img/ESP32-micropython-160Mhz-noli.png
146+
:alt: Scope capture of power consumption with power-save off
147+
:width: 636px
148+
149+
While the average power consumption is around 125mA the ping response times are better than with
150+
power-save enabled::
151+
64 bytes from 192.168.0.124: icmp_seq=64 ttl=255 time=2.59 ms
152+
64 bytes from 192.168.0.124: icmp_seq=65 ttl=255 time=2.42 ms
153+
64 bytes from 192.168.0.124: icmp_seq=66 ttl=255 time=1.68 ms
154+
64 bytes from 192.168.0.124: icmp_seq=67 ttl=255 time=1.36 ms
155+
64 bytes from 192.168.0.124: icmp_seq=68 ttl=255 time=1.62 ms
156+
64 bytes from 192.168.0.124: icmp_seq=69 ttl=255 time=1.30 ms
157+

docs/library/esp32.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
The ``esp32`` module contains functions and classes specifically aimed at
1010
controlling ESP32 modules.
1111

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

1314
Functions
1415
---------
Loading
Loading
24.4 KB
Loading

ports/esp32/boards/sdkconfig.base

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ CONFIG_PM_ENABLE=y
2323
CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=2
2424
CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y
2525
CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP=y
26+
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
2627

2728
# UDP
2829
CONFIG_LWIP_PPP_SUPPORT=y

ports/esp32/esp32_rmt.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ STATIC mp_obj_t esp32_rmt_make_new(const mp_obj_type_t *type, size_t n_args, siz
8383
mp_raise_ValueError("clock_div must be between 1 and 255");
8484
}
8585

86+
// TODO: provide an option to use REF_TICK (1Mhz) to enable low-power operation
87+
// with dynamic frequency scaling.
88+
8689
esp32_rmt_obj_t *self = m_new_obj_with_finaliser(esp32_rmt_obj_t);
8790
self->base.type = &esp32_rmt_type;
8891
self->channel_id = channel_id;

ports/esp32/machine_pwm.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ STATIC ledc_timer_config_t timer_cfg = {
6262
.duty_resolution = PWRES,
6363
.freq_hz = PWFREQ,
6464
.speed_mode = PWMODE,
65-
.timer_num = PWTIMER
65+
.timer_num = PWTIMER,
66+
.clk_cfg = LEDC_USE_REF_TICK, // using REF_TICK to allow dynamic freq scaling
6667
};
6768

6869
STATIC void pwm_init(void) {

ports/esp32/machine_uart.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ STATIC void machine_uart_init_helper(machine_uart_obj_t *self, size_t n_args, co
129129
}
130130
uart_config_t uartcfg = {
131131
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
132-
.rx_flow_ctrl_thresh = 0
132+
.rx_flow_ctrl_thresh = 0,
133+
.use_ref_tick = 1,
133134
};
134135
uint32_t baudrate;
135136
uart_get_baudrate(self->uart_num, &baudrate);
@@ -267,7 +268,8 @@ STATIC mp_obj_t machine_uart_make_new(const mp_obj_type_t *type, size_t n_args,
267268
.parity = UART_PARITY_DISABLE,
268269
.stop_bits = UART_STOP_BITS_1,
269270
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
270-
.rx_flow_ctrl_thresh = 0
271+
.rx_flow_ctrl_thresh = 0,
272+
.use_ref_tick = 1,
271273
};
272274

273275
// create instance

ports/esp32/make

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#! /usr/bin/bash
2+
export ESPIDF=/home/src/esp32/esp-idf-micropython
3+
export IDF_PATH=${ESPIDF}
4+
export PATH=/home/src/esp32/esp-idf-micropython/xtensa-esp32-elf/bin:${PATH}
5+
export BOARD=${BOARD:-GENERIC}
6+
export PORT=${PORT:-/dev/ttyUSB0}
7+
#CROSS_COMPILE = xtensa-esp32-elf-
8+
9+
make -j4 "$@"

ports/esp32/modmachine.c

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,31 +65,69 @@ typedef enum {
6565
MP_SOFT_RESET
6666
} reset_reason_t;
6767

68-
STATIC mp_obj_t machine_freq(size_t n_args, const mp_obj_t *args) {
68+
STATIC mp_obj_t machine_freq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
6969
if (n_args == 0) {
7070
// get
7171
return mp_obj_new_int(esp_clk_cpu_freq());
72-
} else {
73-
// set
74-
mp_int_t freq = mp_obj_get_int(args[0]) / 1000000;
75-
if (freq != 20 && freq != 40 && freq != 80 && freq != 160 && freq != 240) {
76-
mp_raise_ValueError("frequency must be 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz");
72+
}
73+
74+
// setting freq/sleep
75+
enum {ARG_freq, ARG_min_freq, ARG_auto_light_sleep};
76+
const mp_arg_t allowed_args[] = {
77+
{ MP_QSTR_freq, MP_ARG_INT, {.u_int = 0} },
78+
{ MP_QSTR_min_freq, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
79+
{ MP_QSTR_auto_light_sleep, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} },
80+
};
81+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
82+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
83+
84+
// validate frequency
85+
mp_int_t freq = args[ARG_freq].u_int / 1000000;
86+
if (freq != 20 && freq != 40 && freq != 80 && freq != 160 && freq != 240) {
87+
mp_raise_ValueError("frequency must be 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz");
88+
}
89+
esp_pm_config_esp32_t pm;
90+
pm.max_freq_mhz = freq;
91+
pm.min_freq_mhz = freq;
92+
pm.light_sleep_enable = false;
93+
94+
// check optional mininum frequency keyword argument
95+
if (args[ARG_min_freq].u_int != 0) {
96+
mp_int_t mf = args[ARG_min_freq].u_int / 1000000;
97+
if (mf != 10 && mf != 20 && mf != 40 && mf != 80 && mf != 160 && mf != 240) {
98+
mp_raise_ValueError("frequency must be 10Mhz, 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz");
7799
}
78-
esp_pm_config_esp32_t pm;
79-
pm.max_freq_mhz = freq;
80-
pm.min_freq_mhz = freq;
81-
pm.light_sleep_enable = false;
82-
esp_err_t ret = esp_pm_configure(&pm);
83-
if (ret != ESP_OK) {
100+
pm.min_freq_mhz = mf;
101+
}
102+
103+
#if 0
104+
// commented-out because it is ineffective unless the calls to ulTaskNotifyTake in
105+
// mphalport.c use a delay of 4 ticks minimum. Don't want to change those due to
106+
// insufficiently explored side-effects and auto-light-sleep is not that usable in
107+
// the current state anyway... leaving this in for future reference
108+
109+
// check optional auto-light-sleep keyword argument
110+
if (args[ARG_auto_light_sleep].u_bool) {
111+
pm.light_sleep_enable = true;
112+
}
113+
#endif
114+
115+
// apply new setting and check result
116+
esp_err_t ret = esp_pm_configure(&pm);
117+
if (ret != ESP_OK) {
118+
if (ret == ESP_ERR_NOT_SUPPORTED) {
119+
mp_raise_ValueError("auto light-sleep not supported");
120+
} else {
121+
mp_printf(&mp_plat_print, "esp_pm_configure ret=%d\n", ret);
84122
mp_raise_ValueError(NULL);
85123
}
86-
while (esp_clk_cpu_freq() != freq * 1000000) {
87-
vTaskDelay(1);
88-
}
89-
return mp_const_none;
90124
}
125+
while (esp_clk_cpu_freq() != freq * 1000000) {
126+
vTaskDelay(1);
127+
}
128+
return mp_const_none;
91129
}
92-
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_freq_obj, 0, 1, machine_freq);
130+
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(machine_freq_obj, 0, machine_freq);
93131

94132
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) {
95133

ports/esp32/modnetwork.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,18 +320,20 @@ STATIC mp_obj_t esp_active(size_t n_args, const mp_obj_t *args) {
320320
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_active_obj, 1, 2, esp_active);
321321

322322
STATIC mp_obj_t esp_connect(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
323-
enum { ARG_ssid, ARG_password, ARG_bssid };
323+
enum { ARG_ssid, ARG_password, ARG_bssid, ARG_listen_interval };
324324
static const mp_arg_t allowed_args[] = {
325325
{ MP_QSTR_, MP_ARG_OBJ, {.u_obj = mp_const_none} },
326326
{ MP_QSTR_, MP_ARG_OBJ, {.u_obj = mp_const_none} },
327327
{ MP_QSTR_bssid, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
328+
{ MP_QSTR_listen_interval, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
328329
};
329330

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

334335
wifi_config_t wifi_sta_config = {{{0}}};
336+
uint8_t ps_mode = WIFI_PS_MIN_MODEM;
335337

336338
// configure any parameters that are given
337339
if (n_args > 1) {
@@ -353,8 +355,17 @@ STATIC mp_obj_t esp_connect(size_t n_args, const mp_obj_t *pos_args, mp_map_t *k
353355
wifi_sta_config.sta.bssid_set = 1;
354356
memcpy(wifi_sta_config.sta.bssid, p, sizeof(wifi_sta_config.sta.bssid));
355357
}
358+
if (args[ARG_listen_interval].u_int > 0) {
359+
wifi_sta_config.sta.listen_interval = args[ARG_listen_interval].u_int;
360+
ps_mode = WIFI_PS_MAX_MODEM;
361+
} else if (args[ARG_listen_interval].u_int < 0) {
362+
ps_mode = WIFI_PS_NONE;
363+
}
364+
365+
// apply config
356366
ESP_EXCEPTIONS( esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_sta_config) );
357367
}
368+
esp_wifi_set_ps(ps_mode); // set power-save mode depending on listen_interval
358369

359370
// connect to the WiFi AP
360371
MP_THREAD_GIL_EXIT();

ports/esp32/mpconfigport.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ void *esp_native_code_commit(void *, size_t, void *);
248248
mp_handle_pending(true); \
249249
MICROPY_PY_USOCKET_EVENTS_HANDLER \
250250
MP_THREAD_GIL_EXIT(); \
251+
/* yield the processor for 1 tick or until notified. To allow auto-light-sleep mode \
252+
// to kick-in this would have to be raised to 4 at least. */ \
253+
ulTaskNotifyTake(pdFALSE, 1); \
251254
MP_THREAD_GIL_ENTER(); \
252255
} while (0);
253256
#else

ports/esp32/mphalport.c

Lines changed: 0 additions & 2 deletions
68
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ int mp_hal_stdin_rx_chr(void) {
6666
return c;
6767
}
68
MICROPY_EVENT_POLL_HOOK
69-
ulTaskNotifyTake(pdFALSE, 1);
7069
}
7170
}
7271

@@ -129,7 +128,6 @@ void mp_hal_delay_ms(uint32_t ms) {
129128
break;
130129
}
131130
MICROPY_EVENT_POLL_HOOK
132-
ulTaskNotifyTake(pdFALSE, 1);
133131
}
134132
if (dt < us) {
135133
// do the remaining delay accurately

0 commit comments

Comments
 (0)
0