Skip to content

Instantly share code, notes, and snippets.

@igrr
Last active November 25, 2023 21:27
Show Gist options
  • Save igrr/54f7fbe0513ac14e1aea3fd7fbecfeab to your computer and use it in GitHub Desktop.
Save igrr/54f7fbe0513ac14e1aea3fd7fbecfeab to your computer and use it in GitHub Desktop.
ESP32 ESP-IDF example illustrating how to go back to sleep from deep sleep wake stub
/*
* This sample illustrates how to go back to deep sleep from the
* deep sleep wake stub.
*
* Consider the use case of counting pulses from an external sensor,
* where the pulses arrive at a relatively slow rate.
*
* ESP32 is configured to go into deep sleep mode, and wake up from
* a GPIO pin connected to the external pulse source.
* Once the pulse arrives, ESP32 wakes up from deep sleep and runs
* deep sleep wake stub. This stub function is stored in RTC fast
* memory, so it can run without waiting for the whole firmware
* to be loaded from flash.
*
* This function (called wake_stub below) increments the pulse counter,
* stored in RTC_SLOW_MEM. This memory is also preserved when going
* into deep sleep. Then the wake stub decides whether to continue
* booting the firmware, or to go back to sleep. In this simple example,
* the stub starts firmware when the pulse counter reaches 100.
* Note: in real application, counting needs to be continued when the
* application has started, for example using the PCNT peripheral.
*
*/
#include <stdio.h>
#include <string.h>
#include "esp_sleep.h"
#include "esp_attr.h"
#include "rom/rtc.h"
#include "rom/ets_sys.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/rtc_io_reg.h"
#include "soc/uart_reg.h"
#include "soc/timer_group_reg.h"
// Pin used for pulse counting
// GPIO0 is RTC_GPIO11 (see esp32_chip_pin_list_en.pdf)
#define PULSE_CNT_GPIO_NUM 0
#define PULSE_CNT_RTC_GPIO_NUM 11
#define PULSE_CNT_IS_LOW() \
((REG_GET_FIELD(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT) \
& BIT(PULSE_CNT_RTC_GPIO_NUM)) == 0)
// Pulse counter value, stored in RTC_SLOW_MEM
static size_t RTC_DATA_ATTR s_pulse_count;
static size_t RTC_DATA_ATTR s_max_pulse_count;
// Function which runs after exit from deep sleep
static void RTC_IRAM_ATTR wake_stub();
void app_main(void)
{
if (rtc_get_reset_reason(0) == DEEPSLEEP_RESET) {
printf("Wake up from deep sleep\n");
printf("Pulse count=%d\n", s_pulse_count);
} else {
printf("Not a deep sleep wake up\n");
}
s_pulse_count = 0;
s_max_pulse_count = 20;
printf("Going to deep sleep in 1 second\n");
printf("Will wake up after %d pulses\n", s_max_pulse_count);
vTaskDelay(1000/portTICK_PERIOD_MS);
// Set the wake stub function
esp_set_deep_sleep_wake_stub(&wake_stub);
// Wake up on low logic level
ESP_ERROR_CHECK( esp_sleep_enable_ext1_wakeup(
1LL << PULSE_CNT_GPIO_NUM, ESP_EXT1_WAKEUP_ALL_LOW) );
// Enter deep sleep
esp_deep_sleep_start();
}
static const char RTC_RODATA_ATTR wake_fmt_str[] = "count=%d\n";
static const char RTC_RODATA_ATTR sleep_fmt_str[] = "sleeping\n";
static void RTC_IRAM_ATTR wake_stub()
{
// Increment the pulse counter
s_pulse_count++;
// and print the pulse counter value:
ets_printf(wake_fmt_str, s_pulse_count);
if (s_pulse_count >= s_max_pulse_count) {
// On revision 0 of ESP32, this function must be called:
esp_default_wake_deep_sleep();
// Return from the wake stub function to continue
// booting the firmware.
return;
}
// Pulse count is <s_max_pulse_count, go back to sleep
// and wait for more pulses.
// Wait for pin level to be high.
// If we go to sleep when the pin is still low, the chip
// will wake up again immediately. Hardware doesn't have
// edge trigger support for deep sleep wakeup.
do {
while (PULSE_CNT_IS_LOW()) {
// feed the watchdog
REG_WRITE(TIMG_WDTFEED_REG(0), 1);
}
// debounce, 10ms
ets_delay_us(10000);
} while (PULSE_CNT_IS_LOW());
// Print status
ets_printf(sleep_fmt_str);
// Wait for UART to end transmitting.
while (REG_GET_FIELD(UART_STATUS_REG(0), UART_ST_UTX_OUT)) {
;
}
// Set the pointer of the wake stub function.
REG_WRITE(RTC_ENTRY_ADDR_REG, (uint32_t)&wake_stub);
// Go to sleep.
CLEAR_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_SLEEP_EN);
SET_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_SLEEP_EN);
// A few CPU cycles may be necessary for the sleep to start...
while (true) {
;
}
// never reaches here.
}
@helmut64
Copy link

helmut64 commented Aug 26, 2021

I have a wakeup_stub on the ESP32-C3, I simplified the code example which works on the ESP32 and S2 but not on the C3. Basically I like to re-enter after the deep sleep into deep sleep period agin. This works after the first wakeup of 10 secs, it goes into the second deepsleep via the stub code. When the second wakeup expires after another 10 secs the ESP32-C2 boots without running into my wakeup_stub code.

It looks like something with the "REG_WRITE(RTC_ENTRY_ADDR_REG, (uint32_t)&wake_stub);" does not work.

Any ideas?
BTW: The C3 RISC-V ESP is a great device.

static void RTC_IRAM_ATTR wake_stub()
{
	esp_default_wake_deep_sleep();
	
    REG_WRITE(TIMG_WDTFEED_REG(0), 1); // feed the watchdog


    SET_PERI_REG_MASK(RTC_CNTL_TIME_UPDATE_REG, RTC_CNTL_TIME_UPDATE);
#if defined(ARDUINO_ESP32_DEV) && not defined(ARDUINO_ESP32S2_DEV)
    while(GET_PERI_REG_MASK(RTC_CNTL_TIME_UPDATE_REG, RTC_CNTL_TIME_VALID) == 0) {
     	// on the ESP32 we need to wait unti the register is valid
        ets_delay_us(1); // don't flood RTC bus
    }
	SET_PERI_REG_MASK(RTC_CNTL_INT_CLR_REG, RTC_CNTL_TIME_VALID_INT_CLR);
#endif

#ifdef ARDUINO_ESP32_DEV
	uint64_t t_now = READ_PERI_REG(RTC_CNTL_TIME_LOW1_REG);
	t_now |= ((uint64_t) READ_PERI_REG(RTC_CNTL_TIME_HIGH1_REG)) << 32;
#else // ESP-S2 & C2
	uint64_t t_now = READ_PERI_REG(RTC_CNTL_TIME0_REG);
	t_now |= ((uint64_t) READ_PERI_REG(RTC_CNTL_TIME1_REG)) << 32;
#endif

    uint64_t t = t_now + _deepsleepTicks; // calculated in deep sleep to avoid 64 mul/divs here

	rtc_cntl_ll_set_wakeup_timer(t); // can be called because it is static inline (not in flash)
    
    // Set the pointer of the wake stub function.
    REG_WRITE(RTC_ENTRY_ADDR_REG, (uint32_t)&wake_stub);

    // Go to sleep.
    CLEAR_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_SLEEP_EN);
    SET_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_SLEEP_EN);
    // A few CPU cycles may be necessary for the sleep to start...
    while (true) {
        ;
    }
	// never reaches here.
}

@tsVestor
Copy link

@helmut64 I can confirm that none of the given examples work on ESP32-C3. They imediatly reset the chip when reentering deep sleep. @igrr Could you please give us an example of how to go from deep sleep wake stubs on the ESP32-C3 back to deep sleep?

@helmut64
Copy link

Exactly re-issuing a C3 deep sleep without booting, this is the missing part. For me the stub solution is very important, using it I can handle some GPIO IO and re-entering into deep sleep to save energy and to do it quickly. Booting takes 300ms, the stub solution can do the same task within 2 ms.

@helmut64
Copy link

helmut64 commented Jan 2, 2022

I was testing this a little bit more on the C3, because the first wakeup works (after my period of 10 seconds) and the second wakeup does not call the stub, but after 2nd timeout period it boots with cause wakeup timer.
To me sounds like, setting the wakeup stub within the wakeup stub does not work right.

@tsVestor
Copy link

tsVestor commented Jan 7, 2022

I have encountered exactly the same problem. For our product it is absolutely necessary to switch from the wake up stub back to deep sleep and then to wake up stub again. Otherwise the current consumption is way to high and we have to use an competitors SoC. Can you please check this? @igrr

@igrr
Copy link
Author

igrr commented Jan 7, 2022

@tsVestor @helmut64 I have opened a feature request for this in ESP-IDF project: espressif/esp-idf#8208. Please follow this link and click "Subscribe" to get notified about updates!

@andrew-elder
Copy link

@igrr - I have the following code for a wake stub for an ESP32S3. I don't see that GPIO 9 toggles. Is there an example somewhere?

    // Clear the HOLD bit
    CLEAR_PERI_REG_MASK(RTC_CNTL_PAD_HOLD_REG, BIT(9));

    // Enable GPIO output
    REG_WRITE(RTC_GPIO_ENABLE_W1TS_REG, BIT(RTC_GPIO_ENABLE_W1TS_S + 9));

    // Set the GPIO output bit high
    REG_WRITE(RTC_GPIO_OUT_W1TS_REG, BIT(RTC_GPIO_OUT_DATA_W1TS_S + 9));

    // feed the watchdog
    REG_WRITE(TIMG_WDTFEED_REG(0), 1);
    // delay 1ms
    ets_delay_us(1000);

    // Set the GPIO output bit low
    REG_WRITE(RTC_GPIO_OUT_W1TC_REG, BIT(RTC_GPIO_OUT_DATA_W1TC_S + 9));

Note GPIO 9 does toggle elsewhere in the code when the main application starts. Is there a better forum to use to ask this question?

@igrr
Copy link
Author

igrr commented Mar 31, 2022

@andrew-elder I'm sorry, I am not well familiar with gpio registers of the esp32-s3. Please open an issue at https://github.com/espressif/esp-idf/issues, IDF project team will try to help you.

@helmut64
Copy link

@andrew-elder I use on the regular ESP32: GPIO_OUTPUT_SET(_LED, 1);
This works on the ESP32, C3, and I believe S2, I have not tested the S3 with this.

@andrew-elder
Copy link

I was not setting the RTC MUX before writing to the output. I added

    // Set RTC mux
    REG_WRITE(RTC_IO_TOUCH_PAD9_REG, RTC_IO_TOUCH_PAD9_MUX_SEL_M);

and that fixed the issue.

@andrew-elder
Copy link

@igrr - I created an issue https://esp32.com/viewtopic.php?f=13&t=27067 here. Is that the correct place?

@igrr
Copy link
Author

igrr commented Apr 4, 2022

@andrew-elder I would recommend https://github.com/espressif/esp-idf/issues — issues there are tracked (unlike the posts on the forum)

@andrew-elder
Copy link

@igrr - is it ok to file my issue as a "bug report" - even though it isn't really a bug (or at least I wouldn't think it is)?

@gadget-man
Copy link

GPIO_OUTPUT_SET

@andrew-elder I use on the regular ESP32: GPIO_OUTPUT_SET(_LED, 1);
This works on the ESP32, C3, and I believe S2, I have not tested the S3 with this.

@helmut64 I'm trying to do the same, but I can't get GPIO_OUTPUT_SET to do anything when loading in the wake stub. I've tried both the GPIO and RTC_GPIO pin references, and the pin never goes high. Is there anything else you set/enabled to make the GPIO/RTC pin go high during the wake stub?

@gadget-man
Copy link

Sorry to barge in on this - just for the rare case someone looking for answers will get here and I'd like to share my findings about WAKE UP STUB and controlling RTC GPIOs. For wakeup - I managed to wake on both EXT1 and TIMER by setting up wakeup mask like this (before the SLEEP command: #define WAKEUP_MASK 10 //both TIMER and EXT1 (2+8) REG_SET_FIELD(RTC_CNTL_WAKEUP_STATE_REG, RTC_CNTL_WAKEUP_ENA, WAKEUP_MASK); // Wake up on timer and EXT1 In this way, if one correctly sets EXT1 triggers (wakeup on '1') and the timer, either will wake up and execute the stub.

As for controlling RTC_GPIOs - upon entering SLEEP, most RTC_GPIOs are set to HOLD enabled. To disable HOLD within the RAM STUB, the standard ESP-IDF functions cannot be used since they are inaccessible when RAM STUB is executed. The way around is to directly set/clear the relevant bits in the control registers. The HOLD function is controlled for RTC GPIOs by individual registers like the RTC_IO_TOUCH_PAD0_REG (for TOUCH0/GPIO4). To unhold - clear bit 31. To hold back - set bit 31:

REG_WRITE(RTC_IO_TOUCH_PAD0_REG,REG_READ(RTC_IO_TOUCH_PAD0_REG)|0x80000000); //hold TOUCH0 - GPIO4 REG_WRITE(RTC_IO_TOUCH_PAD0_REG,REG_READ(RTC_IO_TOUCH_PAD0_REG)&0x7fffffff); //Unhold TOUCH0 - GPIO4

Once unheld, the pins can be controlled by writing the relevant bits of the IO set and clear registers: RTC_GPIO_OUT_W1TS_REG - to set a GPIO to 1 RTC_GPIO_OUT_W1TC_REG - to set a GPIO to 0 #define SET_GPIO4 REG_WRITE(RTC_GPIO_OUT_W1TS_REG,BIT(RTC_GPIO_OUT_DATA_W1TS_S + 10)) //GPIO4 is RTC_GPIO_10 so it's BIT(10) #define CLR_GPIO4 REG_WRITE(RTC_GPIO_OUT_W1TC_REG,BIT(RTC_GPIO_OUT_DATA_W1TC_S + 10)) //GPIO4 is RTC_GPIO_10 so it's BIT(10)

Hope this provides some well needed (and searched) information for struggling developers like myself. N.

Many thanks @ntasher, your guide on waking from both timer and EXT1/0 really helped me. However I’m still really struggling to set a pin (gpio_27) high in the wake stub. Do you have a code example you could share? I’ve tried the extracts you provided here but nothing happens! Many thanks

@helmut64
Copy link

I use only GPIO_OUTPUT_SET(_LED, 1). This works on the C3 and regular ESP32. I tested this with lower GPIO pins only.
PS: I am using the wakeup stub only for showing some board activity by blinking ever 10 secs an LED, that user understand that the device is still working. The wakeup stub helps a lot to save energy for this status updates.

@gadget-man
Copy link

I use only GPIO_OUTPUT_SET(_LED, 1). This works on the C3 and regular ESP32. I tested this with lower GPIO pins only.
PS: I am using the wakeup stub only for showing some board activity by blinking ever 10 secs an LED, that user understand that the device is still working. The wakeup stub helps a lot to save energy for this status updates.

Many thanks - you pointed me in the right direction. Turns out there was nothing wrong with my idea - I'd just forgotten that I was holding the GPIO in my main code. Doing the following in the wake stub now works absolutely fine:

gpio_pad_unhold(GPIO_NUM_27); GPIO_OUTPUT_SET(GPIO_NUM_27,1); gpio_pad_hold(GPIO_NUM_27);

@tsVestor
Copy link

@helmut64 could you please share your example of how to blink an LED with the wake_stub every few seconds? I have tried all your code snippets, but my C3 wakes up imideatly after reentering deep sleep if I have RTC_TIMER_TRIG_EN enabled. Nevertheless, waking up with RTC_GPIO_TRIG_EN alone works. Thank you in advance!

@aag071976
Copy link

Please help:
i need read after wake up stub: digitalRead(dataPin)
and then return to stub slep
but system reset by watchdog: rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
How to read GPIO input from a wake stub?
please help - i need working example

@0x0fe
Copy link

0x0fe commented Jul 7, 2023

does anyone know if there is a way to calibrate the ADC in the wake stub? the reason i need this : i use the wake stud to check the value of GPIO36 which is the line of ADKEY, this line has 6 tact switches with a 1% resistor ladder, and i need the system to wake up only if one specific key is pressed. So far nothing special, it works, however the internal vref variance between chips is quite high, and in my resistor ladder i have to use relatively close values to ensure any key can trigger a falling edge, becauee my adkey driver relies on this (interrupt on falling edgee, followed by calibrated ADC readings, filtering and discremination).
So, for now i can read the raw value of the ADC in stub, and set a relatively wide range, but i have no guarantee that one chip in the batch will not be outside this range, for now the range is set to 900-1100 and i already saw raw values pretty close to both ends while tesing on few chips.

For reference, here is my resistor ladder

//bt        5.6K  175mV
//left      6.8K  210mV
//top       8.2K  250mV
//side top  10K   300mV
//side bt   12K   354mV
//enc       15K   430mV

and what i do at the end of the stub

  adcValue = SENS.sar_meas_start1.meas1_data_sar;
  SENS.sar_meas_wait2.force_xpd_sar = SENS_FORCE_XPD_SAR_PD; // adc power off
  if(adcValue<900 || adcValue>1100){
    REG_WRITE(RTC_ENTRY_ADDR_REG, (uint32_t)&esp_wake_deep_sleep);
    CLEAR_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_SLEEP_EN);
    SET_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_SLEEP_EN);
    while(true){;}    
  } 

I also wonder if there is a way to set attenuation of the ADC in the stub.

@igrr
Copy link
Author

igrr commented Jul 7, 2023

@0x0fe I would probably try to use the ADC calibration API to convert a few raw values into calibrated voltage values, then use linear interpolation to calculate the raw values which correspond to the threshold voltages you need. (Essentially, implement a makeshift "real voltage to ADC count" conversion which IDF doesn't provide.)
Then, store these ADC counts in variables and use these variables in the ULP program.

@0x0fe
Copy link

0x0fe commented Jul 7, 2023

@igrr interresting idea, but here i dont specially need to use the calibrated RAW adc values, i can use the millivolt values as in the rest of the firmware, my problem is more to read the efuse vref from the stub, so that i can calculate the calibrated millivolt value.

@0x0fe
Copy link

0x0fe commented Jul 7, 2023

oh, maybe i can store the characteristics in some RTC variables after initial boot and then use these directly to make the conversion in the stub? my concern is how much i can do in the stub, if i recall well i cannot even multiply.

@helmut64
Copy link

helmut64 commented Jul 7, 2023

The problem with the stub-code is that you cannot call anything which is located in the flash memory (e.g. C-Library helper for the 64-bit multiply). The easiest way is to disassemble your stub code and verify for any calls outside.

@0x0fe
Copy link

0x0fe commented Jul 7, 2023

yes, i checked the calibrations function, and it seems very unlikely i can perform any of that in the stub. So i guess @igrr idea is still the best way, when the system goes to sleep, i know that this specifc key is pressed, i can store the raw ADC value corresponding to the key press in an RTC variable and based my stub range check on this value. Still it would be nice to set the attenuation correctly in the stub, i dont know what it defaults to.

@0x0fe
Copy link

0x0fe commented Jul 7, 2023

so, i did that:
at runtime, when the key is pressed, store its raw value in an RTC_DATA_ATTR.
In the stub, check the adc value against this RTC variable, adding some range below and over it.
It works fine.

@peteDDD
Copy link

peteDDD commented Nov 25, 2023

This is great code, and I have used it successfully on ESP32 but it does not work on ESP32-S3. Specifically, as far as I can tell, I have it down to one instruction not working on the ESP32-S3: the setting of the wake stub from within the wake stub.

// Set the pointer of the wake stub function.
REG_WRITE(RTC_ENTRY_ADDR_REG, (uint32_t)&wake_stub);

I have written about this here and hope we can find some code that does work on the ESP32-S3 https://esp32.com/viewtopic.php?f=2&t=37002&p=123908#p123908

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment