Merge branch 'feature/esp_timer_isr_dispatch_method_restore' into 'master'

esp timer: Add ISR dispatch method

Closes IDF-1172 and IDF-1173

See merge request espressif/esp-idf!11572
This commit is contained in:
Angus Gratton 2021-02-23 06:21:14 +00:00
commit fb1488abba
11 changed files with 566 additions and 162 deletions

View File

@ -32,6 +32,25 @@ menu "High resolution timer (esp_timer)"
FreeRTOS timer task size, see "FreeRTOS timer task stack size" option
in "FreeRTOS" menu.
config ESP_TIMER_INTERRUPT_LEVEL
int "Interrupt level"
default 1
range 1 3 if IDF_TARGET_ESP32
range 1 1 if !IDF_TARGET_ESP32
help
It sets the interrupt level for esp_timer ISR in range 1..3.
A higher level (3) helps to decrease the ISR esp_timer latency.
config ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
bool "Support ISR dispatch method"
default n
help
Allows using ESP_TIMER_ISR dispatch method (ESP_TIMER_TASK dispatch method is also avalible).
- ESP_TIMER_TASK - Timer callbacks are dispatched from a high-priority esp_timer task.
- ESP_TIMER_ISR - Timer callbacks are dispatched directly from the timer interrupt handler.
The ISR dispatch can be used, in some cases, when a callback is very simple
or need a lower-latency.
choice ESP_TIMER_IMPL
prompt "Hardware timer to use for esp_timer"
default ESP_TIMER_IMPL_TG0_LAC if IDF_TARGET_ESP32

View File

@ -43,6 +43,7 @@
#include <stdio.h>
#include <stdbool.h>
#include "esp_err.h"
#include "sdkconfig.h"
#ifdef __cplusplus
extern "C" {
@ -65,13 +66,10 @@ typedef void (*esp_timer_cb_t)(void* arg);
*/
typedef enum {
ESP_TIMER_TASK, //!< Callback is called from timer task
/* Not supported for now, provision to allow callbacks to run directly
* from an ISR:
ESP_TIMER_ISR, //!< Callback is called from timer ISR
*/
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
ESP_TIMER_ISR, //!< Callback is called from timer ISR
#endif
ESP_TIMER_MAX, //!< Count of the methods for dispatching timer callback
} esp_timer_dispatch_t;
/**
@ -228,6 +226,16 @@ int64_t esp_timer_get_next_alarm(void);
*/
esp_err_t esp_timer_dump(FILE* stream);
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
/**
* @brief Requests a context switch from a timer callback function.
*
* This only works for a timer that has an ISR dispatch method.
* The context switch will be called after all ISR dispatch timers have been processed.
*/
void esp_timer_isr_dispatch_need_yield(void);
#endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
#ifdef __cplusplus
}
#endif

View File

@ -51,6 +51,21 @@ void esp_timer_impl_deinit(void);
*/
void esp_timer_impl_set_alarm(uint64_t timestamp);
/**
* @brief Set up the timer interrupt to fire at a particular time for a particular alarm module.
*
* If the alarm time is too close in the future, implementation should set the
* alarm to the earliest time possible.
*
* @param timestamp time in microseconds when interrupt should fire (relative to
* boot time, i.e. as returned by esp_timer_impl_get_time)
*
* @param alarm_id Id alarm:
* 0 - alarm_0 for the ESP_TIMER_TASK dispatch method,
* 1 - alarm_1 for the ESP_TIMER_ISR dispatch method.
*/
void esp_timer_impl_set_alarm_id(uint64_t timestamp, unsigned alarm_id);
/**
* @brief Notify esp_timer implementation that APB frequency has changed
*

View File

@ -56,7 +56,7 @@
#define EVENT_ID_DELETE_TIMER 0xF0DE1E1E
typedef enum {
FL_DISPATCH_METHOD = (1 << 0), //!< 0=Callback is called from timer task, 1=Callback is called from timer ISR
FL_ISR_DISPATCH_METHOD = (1 << 0), //!< 0=Callback is called from timer task, 1=Callback is called from timer ISR
FL_SKIP_UNHANDLED_EVENTS = (1 << 1), //!< 0=NOT skip unhandled events for periodic timers, 1=Skip unhandled events for periodic timers
} flags_t;
@ -80,11 +80,11 @@ struct esp_timer {
};
static inline bool is_initialized(void);
static esp_err_t timer_insert(esp_timer_handle_t timer);
static esp_err_t timer_insert(esp_timer_handle_t timer, bool without_update_alarm);
static esp_err_t timer_remove(esp_timer_handle_t timer);
static bool timer_armed(esp_timer_handle_t timer);
static void timer_list_lock(void);
static void timer_list_unlock(void);
static void timer_list_lock(esp_timer_dispatch_t timer_type);
static void timer_list_unlock(esp_timer_dispatch_t timer_type);
#if WITH_PROFILING
static void timer_insert_inactive(esp_timer_handle_t timer);
@ -93,21 +93,29 @@ static void timer_remove_inactive(esp_timer_handle_t timer);
__attribute__((unused)) static const char* TAG = "esp_timer";
// list of currently armed timers
static LIST_HEAD(esp_timer_list, esp_timer) s_timers =
LIST_HEAD_INITIALIZER(s_timers);
// lists of currently armed timers for two dispatch methods: ISR and TASK
static LIST_HEAD(esp_timer_list, esp_timer) s_timers[ESP_TIMER_MAX] = {
[0 ... (ESP_TIMER_MAX - 1)] = LIST_HEAD_INITIALIZER(s_timers)
};
#if WITH_PROFILING
// list of unarmed timers, used only to be able to dump statistics about
// all the timers
static LIST_HEAD(esp_inactive_timer_list, esp_timer) s_inactive_timers =
LIST_HEAD_INITIALIZER(s_timers);
// lists of unarmed timers for two dispatch methods: ISR and TASK,
// used only to be able to dump statistics about all the timers
static LIST_HEAD(esp_inactive_timer_list, esp_timer) s_inactive_timers[ESP_TIMER_MAX] = {
[0 ... (ESP_TIMER_MAX - 1)] = LIST_HEAD_INITIALIZER(s_timers)
};
#endif
// task used to dispatch timer callbacks
static TaskHandle_t s_timer_task;
// lock protecting s_timers, s_inactive_timers
static portMUX_TYPE s_timer_lock = portMUX_INITIALIZER_UNLOCKED;
static portMUX_TYPE s_timer_lock[ESP_TIMER_MAX] = {
[0 ... (ESP_TIMER_MAX - 1)] = portMUX_INITIALIZER_UNLOCKED
};
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
// For ISR dispatch method, a callback function of the timer may require a context switch
static volatile BaseType_t s_isr_dispatch_need_yield = pdFALSE;
#endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
esp_err_t esp_timer_create(const esp_timer_create_args_t* args,
esp_timer_handle_t* out_handle)
@ -115,7 +123,8 @@ esp_err_t esp_timer_create(const esp_timer_create_args_t* args,
if (!is_initialized()) {
return ESP_ERR_INVALID_STATE;
}
if (args == NULL || args->callback == NULL || out_handle == NULL) {
if (args == NULL || args->callback == NULL || out_handle == NULL ||
args->dispatch_method < 0 || args->dispatch_method >= ESP_TIMER_MAX) {
return ESP_ERR_INVALID_ARG;
}
esp_timer_handle_t result = (esp_timer_handle_t) calloc(1, sizeof(*result));
@ -124,11 +133,14 @@ esp_err_t esp_timer_create(const esp_timer_create_args_t* args,
}
result->callback = args->callback;
result->arg = args->arg;
result->flags = (args->dispatch_method ? FL_DISPATCH_METHOD : 0) |
result->flags = (args->dispatch_method ? FL_ISR_DISPATCH_METHOD : 0) |
(args->skip_unhandled_events ? FL_SKIP_UNHANDLED_EVENTS : 0);
#if WITH_PROFILING
result->name = args->name;
esp_timer_dispatch_t dispatch_method = result->flags & FL_ISR_DISPATCH_METHOD;
timer_list_lock(dispatch_method);
timer_insert_inactive(result);
timer_list_unlock(dispatch_method);
#endif
*out_handle = result;
return ESP_OK;
@ -142,14 +154,16 @@ esp_err_t IRAM_ATTR esp_timer_start_once(esp_timer_handle_t timer, uint64_t time
if (!is_initialized() || timer_armed(timer)) {
return ESP_ERR_INVALID_STATE;
}
timer_list_lock();
timer->alarm = esp_timer_get_time() + timeout_us;
int64_t alarm = esp_timer_get_time() + timeout_us;
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
timer_list_lock(dispatch_method);
timer->alarm = alarm;
timer->period = 0;
#if WITH_PROFILING
timer->times_armed++;
#endif
esp_err_t err = timer_insert(timer);
timer_list_unlock();
esp_err_t err = timer_insert(timer, false);
timer_list_unlock(dispatch_method);
return err;
}
@ -161,16 +175,18 @@ esp_err_t IRAM_ATTR esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t
if (!is_initialized() || timer_armed(timer)) {
return ESP_ERR_INVALID_STATE;
}
timer_list_lock();
period_us = MAX(period_us, esp_timer_impl_get_min_period_us());
timer->alarm = esp_timer_get_time() + period_us;
int64_t alarm = esp_timer_get_time() + period_us;
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
timer_list_lock(dispatch_method);
timer->alarm = alarm;
timer->period = period_us;
#if WITH_PROFILING
timer->times_armed++;
timer->times_skipped = 0;
#endif
esp_err_t err = timer_insert(timer);
timer_list_unlock();
esp_err_t err = timer_insert(timer, false);
timer_list_unlock(dispatch_method);
return err;
}
@ -193,25 +209,32 @@ esp_err_t esp_timer_delete(esp_timer_handle_t timer)
if (timer_armed(timer)) {
return ESP_ERR_INVALID_STATE;
}
timer_list_lock();
// A case for the timer with ESP_TIMER_ISR:
// This ISR timer was removed from the ISR list in esp_timer_stop() or in timer_process_alarm() -> LIST_REMOVE(it, list_entry)
// and here this timer will be added to another the TASK list, see below.
// We do this because we want to free memory of the timer in a task context instead of an isr context.
int64_t alarm = esp_timer_get_time();
timer_list_lock(ESP_TIMER_TASK);
timer->flags &= ~FL_ISR_DISPATCH_METHOD;
timer->event_id = EVENT_ID_DELETE_TIMER;
timer->alarm = esp_timer_get_time();
timer->alarm = alarm;
timer->period = 0;
timer_insert(timer);
timer_list_unlock();
timer_insert(timer, false);
timer_list_unlock(ESP_TIMER_TASK);
return ESP_OK;
}
static IRAM_ATTR esp_err_t timer_insert(esp_timer_handle_t timer)
static IRAM_ATTR esp_err_t timer_insert(esp_timer_handle_t timer, bool without_update_alarm)
{
#if WITH_PROFILING
timer_remove_inactive(timer);
#endif
esp_timer_handle_t it, last = NULL;
if (LIST_FIRST(&s_timers) == NULL) {
LIST_INSERT_HEAD(&s_timers, timer, list_entry);
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
if (LIST_FIRST(&s_timers[dispatch_method]) == NULL) {
LIST_INSERT_HEAD(&s_timers[dispatch_method], timer, list_entry);
} else {
LIST_FOREACH(it, &s_timers, list_entry) {
LIST_FOREACH(it, &s_timers[dispatch_method], list_entry) {
if (timer->alarm < it->alarm) {
LIST_INSERT_BEFORE(it, timer, list_entry);
break;
@ -223,22 +246,32 @@ static IRAM_ATTR esp_err_t timer_insert(esp_timer_handle_t timer)
LIST_INSERT_AFTER(last, timer, list_entry);
}
}
if (timer == LIST_FIRST(&s_timers)) {
esp_timer_impl_set_alarm(timer->alarm);
if (without_update_alarm == false && timer == LIST_FIRST(&s_timers[dispatch_method])) {
esp_timer_impl_set_alarm_id(timer->alarm, dispatch_method);
}
return ESP_OK;
}
static IRAM_ATTR esp_err_t timer_remove(esp_timer_handle_t timer)
{
timer_list_lock();
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
timer_list_lock(dispatch_method);
esp_timer_handle_t first_timer = LIST_FIRST(&s_timers[dispatch_method]);
LIST_REMOVE(timer, list_entry);
timer->alarm = 0;
timer->period = 0;
if (timer == first_timer) { // if this timer was the first in the list.
uint64_t next_timestamp = UINT64_MAX;
first_timer = LIST_FIRST(&s_timers[dispatch_method]);
if (first_timer) { // if after removing the timer from the list, this list is not empty.
next_timestamp = first_timer->alarm;
}
esp_timer_impl_set_alarm_id(next_timestamp, dispatch_method);
}
#if WITH_PROFILING
timer_insert_inactive(timer);
#endif
timer_list_unlock();
timer_list_unlock(dispatch_method);
return ESP_OK;
}
@ -249,24 +282,21 @@ static IRAM_ATTR void timer_insert_inactive(esp_timer_handle_t timer)
/* May be locked or not, depending on where this is called from.
* Lock recursively.
*/
timer_list_lock();
esp_timer_handle_t head = LIST_FIRST(&s_inactive_timers);
esp_timer_dispatch_t dispatch_method = timer->flags & FL_ISR_DISPATCH_METHOD;
esp_timer_handle_t head = LIST_FIRST(&s_inactive_timers[dispatch_method]);
if (head == NULL) {
LIST_INSERT_HEAD(&s_inactive_timers, timer, list_entry);
LIST_INSERT_HEAD(&s_inactive_timers[dispatch_method], timer, list_entry);
} else {
/* Insert as head element as this is the fastest thing to do.
* Removal is O(1) anyway.
*/
LIST_INSERT_BEFORE(head, timer, list_entry);
}
timer_list_unlock();
}
static IRAM_ATTR void timer_remove_inactive(esp_timer_handle_t timer)
{
timer_list_lock();
LIST_REMOVE(timer, list_entry);
timer_list_unlock();
}
#endif // WITH_PROFILING
@ -276,31 +306,37 @@ static IRAM_ATTR bool timer_armed(esp_timer_handle_t timer)
return timer->alarm > 0;
}
static IRAM_ATTR void timer_list_lock(void)
static IRAM_ATTR void timer_list_lock(esp_timer_dispatch_t timer_type)
{
portENTER_CRITICAL_SAFE(&s_timer_lock);
portENTER_CRITICAL_SAFE(&s_timer_lock[timer_type]);
}
static IRAM_ATTR void timer_list_unlock(void)
static IRAM_ATTR void timer_list_unlock(esp_timer_dispatch_t timer_type)
{
portEXIT_CRITICAL_SAFE(&s_timer_lock);
portEXIT_CRITICAL_SAFE(&s_timer_lock[timer_type]);
}
static void timer_process_alarm(esp_timer_dispatch_t dispatch_method)
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
static IRAM_ATTR bool timer_process_alarm(esp_timer_dispatch_t dispatch_method)
#else
static bool timer_process_alarm(esp_timer_dispatch_t dispatch_method)
#endif
{
/* unused, provision to allow running callbacks from ISR */
(void) dispatch_method;
timer_list_lock();
timer_list_lock(dispatch_method);
bool processed = false;
esp_timer_handle_t it;
while (1) {
it = LIST_FIRST(&s_timers);
it = LIST_FIRST(&s_timers[dispatch_method]);
int64_t now = esp_timer_impl_get_time();
if (it == NULL || it->alarm > now) {
break;
}
processed = true;
LIST_REMOVE(it, list_entry);
if (it->event_id == EVENT_ID_DELETE_TIMER) {
// It is handled only by ESP_TIMER_TASK (see esp_timer_delete()).
// All the ESP_TIMER_ISR timers which should be deleted are moved by esp_timer_delete() to the ESP_TIMER_TASK list.
// We want to free memory of the timer in a task context instead of an isr context.
free(it);
it = NULL;
} else {
@ -314,7 +350,7 @@ static void timer_process_alarm(esp_timer_dispatch_t dispatch_method)
} else {
it->alarm += it->period;
}
timer_insert(it);
timer_insert(it, true);
} else {
it->alarm = 0;
#if WITH_PROFILING
@ -326,19 +362,26 @@ static void timer_process_alarm(esp_timer_dispatch_t dispatch_method)
#endif
esp_timer_cb_t callback = it->callback;
void* arg = it->arg;
timer_list_unlock();
timer_list_unlock(dispatch_method);
(*callback)(arg);
timer_list_lock();
timer_list_lock(dispatch_method);
#if WITH_PROFILING
it->times_triggered++;
it->total_callback_run_time += esp_timer_impl_get_time() - callback_start;
#endif
}
}
} // while(1)
if (it) {
esp_timer_impl_set_alarm(it->alarm);
if (dispatch_method == ESP_TIMER_TASK || (dispatch_method != ESP_TIMER_TASK && processed == true)) {
esp_timer_impl_set_alarm_id(it->alarm, dispatch_method);
}
} else {
if (processed) {
esp_timer_impl_set_alarm_id(UINT64_MAX, dispatch_method);
}
}
timer_list_unlock();
timer_list_unlock(dispatch_method);
return processed;
}
static void timer_task(void* arg)
@ -350,10 +393,29 @@ static void timer_task(void* arg)
}
}
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
IRAM_ATTR void esp_timer_isr_dispatch_need_yield(void)
{
assert(xPortInIsrContext());
s_isr_dispatch_need_yield = pdTRUE;
}
#endif
static void IRAM_ATTR timer_alarm_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(s_timer_task, &xHigherPriorityTaskWoken);
bool isr_timers_processed = false;
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
// process timers with ISR dispatch method
isr_timers_processed = timer_process_alarm(ESP_TIMER_ISR);
xHigherPriorityTaskWoken = s_isr_dispatch_need_yield;
s_isr_dispatch_need_yield = pdFALSE;
#endif
if (isr_timers_processed == false) {
vTaskNotifyGiveFromISR(s_timer_task, &xHigherPriorityTaskWoken);
}
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
@ -407,16 +469,20 @@ esp_err_t esp_timer_deinit(void)
}
/* Check if there are any active timers */
if (!LIST_EMPTY(&s_timers)) {
return ESP_ERR_INVALID_STATE;
for (esp_timer_dispatch_t dispatch_method = ESP_TIMER_TASK; dispatch_method < ESP_TIMER_MAX; ++dispatch_method) {
if (!LIST_EMPTY(&s_timers[dispatch_method])) {
return ESP_ERR_INVALID_STATE;
}
}
/* We can only check if there are any timers which are not deleted if
* profiling is enabled.
*/
#if WITH_PROFILING
if (!LIST_EMPTY(&s_inactive_timers)) {
return ESP_ERR_INVALID_STATE;
for (esp_timer_dispatch_t dispatch_method = ESP_TIMER_TASK; dispatch_method < ESP_TIMER_MAX; ++dispatch_method) {
if (!LIST_EMPTY(&s_inactive_timers[dispatch_method])) {
return ESP_ERR_INVALID_STATE;
}
}
#endif
@ -463,16 +529,18 @@ esp_err_t esp_timer_dump(FILE* stream)
/* First count the number of timers */
size_t timer_count = 0;
timer_list_lock();
LIST_FOREACH(it, &s_timers, list_entry) {
++timer_count;
}
for (esp_timer_dispatch_t dispatch_method = ESP_TIMER_TASK; dispatch_method < ESP_TIMER_MAX; ++dispatch_method) {
timer_list_lock(dispatch_method);
LIST_FOREACH(it, &s_timers[dispatch_method], list_entry) {
++timer_count;
}
#if WITH_PROFILING
LIST_FOREACH(it, &s_inactive_timers, list_entry) {
++timer_count;
}
LIST_FOREACH(it, &s_inactive_timers[dispatch_method], list_entry) {
++timer_count;
}
#endif
timer_list_unlock();
timer_list_unlock(dispatch_method);
}
/* Allocate the memory for this number of timers. Since we have unlocked,
* we may find that there are more timers. There's no bulletproof solution
@ -486,17 +554,19 @@ esp_err_t esp_timer_dump(FILE* stream)
}
/* Print to the buffer */
timer_list_lock();
char* pos = print_buf;
LIST_FOREACH(it, &s_timers, list_entry) {
print_timer_info(it, &pos, &buf_size);
}
for (esp_timer_dispatch_t dispatch_method = ESP_TIMER_TASK; dispatch_method < ESP_TIMER_MAX; ++dispatch_method) {
timer_list_lock(dispatch_method);
LIST_FOREACH(it, &s_timers[dispatch_method], list_entry) {
print_timer_info(it, &pos, &buf_size);
}
#if WITH_PROFILING
LIST_FOREACH(it, &s_inactive_timers, list_entry) {
print_timer_info(it, &pos, &buf_size);
}
LIST_FOREACH(it, &s_inactive_timers[dispatch_method], list_entry) {
print_timer_info(it, &pos, &buf_size);
}
#endif
timer_list_unlock();
timer_list_unlock(dispatch_method);
}
fprintf(stream, "Timer stats:\n");
#if WITH_PROFILING
@ -507,7 +577,9 @@ esp_err_t esp_timer_dump(FILE* stream)
#endif
/* Print the buffer */
fputs(print_buf, stream);
if (stream != NULL) {
fputs(print_buf, stream);
}
free(print_buf);
return ESP_OK;
@ -516,12 +588,16 @@ esp_err_t esp_timer_dump(FILE* stream)
int64_t IRAM_ATTR esp_timer_get_next_alarm(void)
{
int64_t next_alarm = INT64_MAX;
timer_list_lock();
esp_timer_handle_t it = LIST_FIRST(&s_timers);
if (it) {
next_alarm = it->alarm;
for (esp_timer_dispatch_t dispatch_method = ESP_TIMER_TASK; dispatch_method < ESP_TIMER_MAX; ++dispatch_method) {
timer_list_lock(dispatch_method);
esp_timer_handle_t it = LIST_FIRST(&s_timers[dispatch_method]);
if (it) {
if (next_alarm > it->alarm) {
next_alarm = it->alarm;
}
}
timer_list_unlock(dispatch_method);
}
timer_list_unlock();
return next_alarm;
}

View File

@ -214,51 +214,61 @@ int64_t IRAM_ATTR esp_timer_impl_get_time(void)
int64_t esp_timer_get_time(void) __attribute__((alias("esp_timer_impl_get_time")));
void IRAM_ATTR esp_timer_impl_set_alarm_id(uint64_t timestamp, unsigned alarm_id)
{
static uint64_t timestamp_id[2] = { UINT64_MAX, UINT64_MAX };
portENTER_CRITICAL_SAFE(&s_time_update_lock);
timestamp_id[alarm_id] = timestamp;
timestamp = MIN(timestamp_id[0], timestamp_id[1]);
if (timestamp != UINT64_MAX) {
// Use calculated alarm value if it is less than ALARM_OVERFLOW_VAL.
// Note that if by the time we update ALARM_REG, COUNT_REG value is higher,
// interrupt will not happen for another ALARM_OVERFLOW_VAL timer ticks,
// so need to check if alarm value is too close in the future (e.g. <2 us away).
int32_t offset = s_timer_ticks_per_us * 2;
do {
// Adjust current time if overflow has happened
if (timer_overflow_happened() ||
((REG_READ(FRC_TIMER_COUNT_REG(1)) > ALARM_OVERFLOW_VAL) &&
((REG_READ(FRC_TIMER_CTRL_REG(1)) & FRC_TIMER_INT_STATUS) == 0))) {
// 1. timer_overflow_happened() checks overflow with the interrupt flag.
// 2. During several loops, the counter can be higher than the alarm and even step over ALARM_OVERFLOW_VAL boundary (the interrupt flag is not set).
timer_count_reload();
s_time_base_us += s_timer_us_per_overflow;
}
s_mask_overflow = false;
int64_t cur_count = REG_READ(FRC_TIMER_COUNT_REG(1));
// Alarm time relative to the moment when counter was 0
int64_t time_after_timebase_us = (int64_t)timestamp - s_time_base_us;
// Calculate desired timer compare value (may exceed 2^32-1)
int64_t compare_val = time_after_timebase_us * s_timer_ticks_per_us;
compare_val = MAX(compare_val, cur_count + offset);
uint32_t alarm_reg_val = ALARM_OVERFLOW_VAL;
if (compare_val < ALARM_OVERFLOW_VAL) {
alarm_reg_val = (uint32_t) compare_val;
}
REG_WRITE(FRC_TIMER_ALARM_REG(1), alarm_reg_val);
int64_t delta = (int64_t)alarm_reg_val - (int64_t)REG_READ(FRC_TIMER_COUNT_REG(1));
if (delta <= 0) {
/*
When the timestamp is a bit less than the current counter then the alarm = current_counter + offset.
But due to CPU_freq in some case can be equal APB_freq the offset time can not exceed the overhead
(the alarm will be less than the counter) and it leads to the infinity loop.
To exclude this behavior to the offset was added the delta to have the opportunity to go through it.
*/
offset += abs((int)delta) + s_timer_ticks_per_us * 2;
} else {
break;
}
} while (1);
}
portEXIT_CRITICAL_SAFE(&s_time_update_lock);
}
void IRAM_ATTR esp_timer_impl_set_alarm(uint64_t timestamp)
{
portENTER_CRITICAL_SAFE(&s_time_update_lock);
// Use calculated alarm value if it is less than ALARM_OVERFLOW_VAL.
// Note that if by the time we update ALARM_REG, COUNT_REG value is higher,
// interrupt will not happen for another ALARM_OVERFLOW_VAL timer ticks,
// so need to check if alarm value is too close in the future (e.g. <2 us away).
int32_t offset = s_timer_ticks_per_us * 2;
do {
// Adjust current time if overflow has happened
if (timer_overflow_happened() ||
((REG_READ(FRC_TIMER_COUNT_REG(1)) > ALARM_OVERFLOW_VAL) &&
((REG_READ(FRC_TIMER_CTRL_REG(1)) & FRC_TIMER_INT_STATUS) == 0))) {
// 1. timer_overflow_happened() checks overflow with the interrupt flag.
// 2. During several loops, the counter can be higher than the alarm and even step over ALARM_OVERFLOW_VAL boundary (the interrupt flag is not set).
timer_count_reload();
s_time_base_us += s_timer_us_per_overflow;
}
s_mask_overflow = false;
int64_t cur_count = REG_READ(FRC_TIMER_COUNT_REG(1));
// Alarm time relative to the moment when counter was 0
int64_t time_after_timebase_us = (int64_t)timestamp - s_time_base_us;
// Calculate desired timer compare value (may exceed 2^32-1)
int64_t compare_val = time_after_timebase_us * s_timer_ticks_per_us;
compare_val = MAX(compare_val, cur_count + offset);
uint32_t alarm_reg_val = ALARM_OVERFLOW_VAL;
if (compare_val < ALARM_OVERFLOW_VAL) {
alarm_reg_val = (uint32_t) compare_val;
}
REG_WRITE(FRC_TIMER_ALARM_REG(1), alarm_reg_val);
int64_t delta = (int64_t)alarm_reg_val - (int64_t)REG_READ(FRC_TIMER_COUNT_REG(1));
if (delta <= 0) {
/*
When the timestamp is a bit less than the current counter then the alarm = current_counter + offset.
But due to CPU_freq in some case can be equal APB_freq the offset time can not exceed the overhead
(the alarm will be less than the counter) and it leads to the infinity loop.
To exclude this behavior to the offset was added the delta to have the opportunity to go through it.
*/
offset += abs((int)delta) + s_timer_ticks_per_us * 2;
} else {
break;
}
} while (1);
portEXIT_CRITICAL_SAFE(&s_time_update_lock);
esp_timer_impl_set_alarm_id(timestamp, 0);
}
static void IRAM_ATTR timer_alarm_isr(void *arg)
@ -360,8 +370,9 @@ esp_err_t esp_timer_impl_init(intr_handler_t alarm_handler)
{
s_alarm_handler = alarm_handler;
const int interrupt_lvl = (1 << CONFIG_ESP_TIMER_INTERRUPT_LEVEL) & ESP_INTR_FLAG_LEVELMASK;
esp_err_t err = esp_intr_alloc(ETS_TIMER2_INTR_SOURCE,
ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM,
ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM | interrupt_lvl,
&timer_alarm_isr, NULL, &s_timer_interrupt_handle);
if (err != ESP_OK) {

View File

@ -154,29 +154,39 @@ int64_t IRAM_ATTR esp_timer_impl_get_time(void)
int64_t esp_timer_get_time(void) __attribute__((alias("esp_timer_impl_get_time")));
void IRAM_ATTR esp_timer_impl_set_alarm_id(uint64_t timestamp, unsigned alarm_id)
{
static uint64_t timestamp_id[2] = { UINT64_MAX, UINT64_MAX };
portENTER_CRITICAL_SAFE(&s_time_update_lock);
timestamp_id[alarm_id] = timestamp;
timestamp = MIN(timestamp_id[0], timestamp_id[1]);
if (timestamp != UINT64_MAX) {
int64_t offset = TICKS_PER_US * 2;
uint64_t now_time = esp_timer_impl_get_counter_reg();
timer_64b_reg_t alarm = { .val = MAX(timestamp * TICKS_PER_US, now_time + offset) };
do {
REG_CLR_BIT(CONFIG_REG, TIMG_LACT_ALARM_EN);
REG_WRITE(ALARM_LO_REG, alarm.lo);
REG_WRITE(ALARM_HI_REG, alarm.hi);
REG_SET_BIT(CONFIG_REG, TIMG_LACT_ALARM_EN);
now_time = esp_timer_impl_get_counter_reg();
int64_t delta = (int64_t)alarm.val - (int64_t)now_time;
if (delta <= 0 && REG_GET_FIELD(INT_ST_REG, TIMG_LACT_INT_ST) == 0) {
// new alarm is less than the counter and the interrupt flag is not set
offset += abs((int)delta) + TICKS_PER_US * 2;
alarm.val = now_time + offset;
} else {
// finish if either (alarm > counter) or the interrupt flag is already set.
break;
}
} while(1);
}
portEXIT_CRITICAL_SAFE(&s_time_update_lock);
}
void IRAM_ATTR esp_timer_impl_set_alarm(uint64_t timestamp)
{
portENTER_CRITICAL_SAFE(&s_time_update_lock);
int64_t offset = TICKS_PER_US * 2;
uint64_t now_time = esp_timer_impl_get_counter_reg();
timer_64b_reg_t alarm = { .val = MAX(timestamp * TICKS_PER_US, now_time + offset) };
do {
REG_CLR_BIT(CONFIG_REG, TIMG_LACT_ALARM_EN);
REG_WRITE(ALARM_LO_REG, alarm.lo);
REG_WRITE(ALARM_HI_REG, alarm.hi);
REG_SET_BIT(CONFIG_REG, TIMG_LACT_ALARM_EN);
now_time = esp_timer_impl_get_counter_reg();
int64_t delta = (int64_t)alarm.val - (int64_t)now_time;
if (delta <= 0 && REG_GET_FIELD(INT_ST_REG, TIMG_LACT_INT_ST) == 0) {
// new alarm is less than the counter and the interrupt flag is not set
offset += abs((int)delta) + TICKS_PER_US * 2;
alarm.val = now_time + offset;
} else {
// finish if either (alarm > counter) or the interrupt flag is already set.
break;
}
} while(1);
portEXIT_CRITICAL_SAFE(&s_time_update_lock);
esp_timer_impl_set_alarm_id(timestamp, 0);
}
static void IRAM_ATTR timer_alarm_isr(void *arg)
@ -222,8 +232,9 @@ esp_err_t esp_timer_impl_init(intr_handler_t alarm_handler)
REG_WRITE(LOAD_REG, 1);
REG_SET_BIT(INT_CLR_REG, TIMG_LACT_INT_CLR);
const int interrupt_lvl = (1 << CONFIG_ESP_TIMER_INTERRUPT_LEVEL) & ESP_INTR_FLAG_LEVELMASK;
esp_err_t err = esp_intr_alloc(INTR_SOURCE_LACT,
ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM,
ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM | interrupt_lvl,
&timer_alarm_isr, NULL, &s_timer_interrupt_handle);
if (err != ESP_OK) {

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "sys/param.h"
#include "esp_timer_impl.h"
#include "esp_err.h"
#include "esp_timer.h"
@ -73,11 +74,21 @@ int64_t IRAM_ATTR esp_timer_impl_get_time(void)
int64_t esp_timer_get_time(void) __attribute__((alias("esp_timer_impl_get_time")));
void IRAM_ATTR esp_timer_impl_set_alarm_id(uint64_t timestamp, unsigned alarm_id)
{
static uint64_t timestamp_id[2] = { UINT64_MAX, UINT64_MAX };
portENTER_CRITICAL_SAFE(&s_time_update_lock);
timestamp_id[alarm_id] = timestamp;
timestamp = MIN(timestamp_id[0], timestamp_id[1]);
if (timestamp != UINT64_MAX) {
systimer_hal_set_alarm_target(SYSTIMER_ALARM_2, timestamp);
}
portEXIT_CRITICAL_SAFE(&s_time_update_lock);
}
void IRAM_ATTR esp_timer_impl_set_alarm(uint64_t timestamp)
{
portENTER_CRITICAL_SAFE(&s_time_update_lock);
systimer_hal_set_alarm_target(SYSTIMER_ALARM_2, timestamp);
portEXIT_CRITICAL_SAFE(&s_time_update_lock);
esp_timer_impl_set_alarm_id(timestamp, 0);
}
static void IRAM_ATTR timer_alarm_isr(void *arg)
@ -103,13 +114,14 @@ void esp_timer_impl_advance(int64_t time_us)
esp_err_t esp_timer_impl_init(intr_handler_t alarm_handler)
{
s_alarm_handler = alarm_handler;
const int interrupt_lvl = (1 << CONFIG_ESP_TIMER_INTERRUPT_LEVEL) & ESP_INTR_FLAG_LEVELMASK;
#if SOC_SYSTIMER_INT_LEVEL
int int_type = 0;
#else
int int_type = ESP_INTR_FLAG_EDGE;
#endif // SOC_SYSTIMER_INT_LEVEL
esp_err_t err = esp_intr_alloc(ETS_SYSTIMER_TARGET2_EDGE_INTR_SOURCE,
ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM | int_type,
ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM | int_type | interrupt_lvl,
&timer_alarm_isr, NULL, &s_timer_interrupt_handle);
if (err != ESP_OK) {

View File

@ -15,6 +15,8 @@
#include "esp_freertos_hooks.h"
#include "esp_rom_sys.h"
#define SEC (1000000)
#if CONFIG_ESP_TIMER_IMPL_FRC2
#include "soc/frc_timer_reg.h"
#endif
@ -921,3 +923,236 @@ TEST_CASE("Test a latency between a call of callback and real event", "[esp_time
TEST_ESP_OK(esp_timer_stop(periodic_timer));
TEST_ESP_OK(esp_timer_delete(periodic_timer));
}
#ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
static int64_t old_time[2];
static void timer_isr_callback(void* arg)
{
int num_timer = *((int*)arg);
int64_t now = esp_timer_get_time();
int64_t dt = now - old_time[num_timer];
old_time[num_timer] = now;
if (num_timer == 1) {
esp_rom_printf("(%lld): \t\t\t\t timer ISR, dt: %lld us\n", now, dt);
assert(xPortInIsrContext());
} else {
esp_rom_printf("(%lld): timer TASK, dt: %lld us\n", now, dt);
assert(!xPortInIsrContext());
}
}
TEST_CASE("Test ESP_TIMER_ISR dispatch method", "[esp_timer]")
{
TEST_ESP_OK(esp_timer_dump(stdout));
int timer[2]= {1, 2};
const esp_timer_create_args_t periodic_timer1_args = {
.callback = &timer_isr_callback,
.dispatch_method = ESP_TIMER_ISR,
.arg = &timer[0],
.name = "ISR",
};
esp_timer_handle_t periodic_timer1;
TEST_ESP_OK(esp_timer_create(&periodic_timer1_args, &periodic_timer1));
TEST_ESP_OK(esp_timer_start_periodic(periodic_timer1, 400000));
const esp_timer_create_args_t periodic_timer2_args = {
.callback = &timer_isr_callback,
.dispatch_method = ESP_TIMER_TASK,
.arg = &timer[1],
.name = "TASK",
};
esp_timer_handle_t periodic_timer2;
TEST_ESP_OK(esp_timer_create(&periodic_timer2_args, &periodic_timer2));
TEST_ESP_OK(esp_timer_start_periodic(periodic_timer2, 500000));
printf("timers created\n");
vTaskDelay(10 * 1000 / portTICK_PERIOD_MS);
TEST_ESP_OK(esp_timer_stop(periodic_timer1));
TEST_ESP_OK(esp_timer_stop(periodic_timer2));
TEST_ESP_OK(esp_timer_dump(stdout));
TEST_ESP_OK(esp_timer_delete(periodic_timer1));
TEST_ESP_OK(esp_timer_delete(periodic_timer2));
printf("timers deleted\n");
TEST_ESP_OK(esp_timer_dump(stdout));
}
static void dump_task(void* arg)
{
bool* stop_dump_task = (bool*) arg;
while (*stop_dump_task == false) {
TEST_ESP_OK(esp_timer_dump(NULL));
}
vTaskDelete(NULL);
}
static void isr_callback(void* arg)
{
assert(xPortInIsrContext());
}
static void task_callback(void* arg)
{
assert(!xPortInIsrContext());
}
TEST_CASE("Test ESP_TIMER_ISR dispatch method is not blocked", "[esp_timer]")
{
const esp_timer_create_args_t periodic_timer1_args = {
.callback = &isr_callback,
.dispatch_method = ESP_TIMER_ISR,
.arg = NULL,
.name = "ISR",
};
esp_timer_handle_t periodic_timer1;
TEST_ESP_OK(esp_timer_create(&periodic_timer1_args, &periodic_timer1));
TEST_ESP_OK(esp_timer_start_periodic(periodic_timer1, 500));
const esp_timer_create_args_t periodic_timer2_args = {
.callback = &task_callback,
.dispatch_method = ESP_TIMER_TASK,
.arg = NULL,
.name = "TASK",
};
esp_timer_handle_t periodic_timer2;
TEST_ESP_OK(esp_timer_create(&periodic_timer2_args, &periodic_timer2));
TEST_ESP_OK(esp_timer_start_periodic(periodic_timer2, 5000));
printf("timers created\n");
bool stop_dump_task = false;
xTaskCreatePinnedToCore(&dump_task, "dump", 4096, &stop_dump_task, UNITY_FREERTOS_PRIORITY, NULL, 0);
vTaskDelay(10 * 1000 / portTICK_PERIOD_MS);
stop_dump_task = true;
vTaskDelay(100 / portTICK_PERIOD_MS);
TEST_ESP_OK(esp_timer_stop(periodic_timer1));
TEST_ESP_OK(esp_timer_stop(periodic_timer2));
TEST_ESP_OK(esp_timer_dump(stdout));
TEST_ESP_OK(esp_timer_delete(periodic_timer1));
TEST_ESP_OK(esp_timer_delete(periodic_timer2));
printf("timer deleted\n");
}
static void isr_callback1(void* arg)
{
assert(xPortInIsrContext());
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
esp_rom_printf("isr_callback1: timer ISR\n");
SemaphoreHandle_t done = *(SemaphoreHandle_t*) arg;
xSemaphoreGiveFromISR(done, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
esp_timer_isr_dispatch_need_yield();
}
}
static void task_callback1(void* arg)
{
assert(0);
}
TEST_CASE("Test ESP_TIMER_ISR, stop API cleans alarm reg if TASK timer list is empty", "[esp_timer]")
{
SemaphoreHandle_t done = xSemaphoreCreateBinary();
const esp_timer_create_args_t timer1_args = {
.callback = &isr_callback1,
.dispatch_method = ESP_TIMER_ISR,
.arg = &done,
.name = "ISR",
};
esp_timer_handle_t timer1;
TEST_ESP_OK(esp_timer_create(&timer1_args, &timer1));
TEST_ESP_OK(esp_timer_start_periodic(timer1, 5 * SEC));
const esp_timer_create_args_t timer2_args = {
.callback = &task_callback1,
.dispatch_method = ESP_TIMER_TASK,
.arg = NULL,
.name = "TASK",
};
esp_timer_handle_t timer2;
TEST_ESP_OK(esp_timer_create(&timer2_args, &timer2));
TEST_ESP_OK(esp_timer_start_once(timer2, 3 * SEC));
printf("timers created\n");
printf("stop timer2\n");
TEST_ESP_OK(esp_timer_stop(timer2));
TEST_ASSERT(xSemaphoreTake(done, 6 * 1000 / portTICK_PERIOD_MS));
printf("stop timer1\n");
TEST_ESP_OK(esp_timer_stop(timer1));
TEST_ESP_OK(esp_timer_dump(stdout));
TEST_ESP_OK(esp_timer_delete(timer1));
TEST_ESP_OK(esp_timer_delete(timer2));
vSemaphoreDelete(done);
printf("timer deleted\n");
}
static void isr_callback2(void* arg)
{
assert(0);
}
static void task_callback2(void* arg)
{
assert(!xPortInIsrContext());
esp_rom_printf("task_callback2: timer TASK\n");
SemaphoreHandle_t done = *(SemaphoreHandle_t*) arg;
xSemaphoreGive(done);
}
TEST_CASE("Test ESP_TIMER_ISR, stop API cleans alarm reg if ISR timer list is empty", "[esp_timer]")
{
SemaphoreHandle_t done = xSemaphoreCreateBinary();
const esp_timer_create_args_t timer1_args = {
.callback = &isr_callback2,
.dispatch_method = ESP_TIMER_ISR,
.arg = NULL,
.name = "ISR",
};
esp_timer_handle_t timer1;
TEST_ESP_OK(esp_timer_create(&timer1_args, &timer1));
TEST_ESP_OK(esp_timer_start_once(timer1, 3 * SEC));
const esp_timer_create_args_t timer2_args = {
.callback = &task_callback2,
.dispatch_method = ESP_TIMER_TASK,
.arg = &done,
.name = "TASK",
};
esp_timer_handle_t timer2;
TEST_ESP_OK(esp_timer_create(&timer2_args, &timer2));
TEST_ESP_OK(esp_timer_start_periodic(timer2, 5 * SEC));
printf("timers created\n");
printf("stop timer1\n");
TEST_ESP_OK(esp_timer_stop(timer1));
TEST_ASSERT(xSemaphoreTake(done, 6 * 1000 / portTICK_PERIOD_MS));
printf("stop timer2\n");
TEST_ESP_OK(esp_timer_stop(timer2));
TEST_ESP_OK(esp_timer_dump(stdout));
TEST_ESP_OK(esp_timer_delete(timer1));
TEST_ESP_OK(esp_timer_delete(timer2));
vSemaphoreDelete(done);
printf("timer deleted\n");
}
#endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD

View File

@ -11,6 +11,8 @@ Although FreeRTOS provides software timers, these timers have a few limitations:
Hardware timers are free from both of the limitations, but often they are less convenient to use. For example, application components may need timer events to fire at certain times in the future, but the hardware timer only contains one "compare" value used for interrupt generation. This means that some facility needs to be built on top of the hardware timer to manage the list of pending events can dispatch the callbacks for these events as corresponding hardware interrupts happen.
An interrupt level of the handler depends on the :ref:`CONFIG_ESP_TIMER_INTERRUPT_LEVEL` option. It allows to set this: 1, 2 or 3 level (by default 1). Raising the level, the interrupt handler can reduce the timer processing delay.
``esp_timer`` set of APIs provides one-shot and periodic timers, microsecond time resolution, and 64-bit range.
Internally, ``esp_timer`` uses a 64-bit hardware timer :ref:`CONFIG_ESP_TIMER_IMPL`:
@ -21,12 +23,17 @@ Internally, ``esp_timer`` uses a 64-bit hardware timer :ref:`CONFIG_ESP_TIMER_IM
.. note: The FRC2 is a legacy option for ESP32 until v4.2, a 32-bit hardware timer was used. Starting at v4.2, use the new LAC timer option instead, it has a simpler implementation, and has smaller run time overhead because software handling of timer overflow is not needed.
Timer callbacks are dispatched from a high-priority ``esp_timer`` task. Because all the callbacks are dispatched from the same task, it is recommended to only do the minimal possible amount of work from the callback itself, posting an event to a lower priority task using a queue instead.
Timer callbacks can dispatched by two methods:
.. note: Provisions are made to dispatch some simple callbacks directly from the interrupt handler, if needed. However this option is not implemented at the moment.
- ``ESP_TIMER_TASK``
- ``ESP_TIMER_ISR``. Available only if :ref:`CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD` is enabled (by default disabled).
``ESP_TIMER_TASK``. Timer callbacks are dispatched from a high-priority ``esp_timer`` task. Because all the callbacks are dispatched from the same task, it is recommended to only do the minimal possible amount of work from the callback itself, posting an event to a lower priority task using a queue instead.
If other tasks with priority higher than ``esp_timer`` are running, callback dispatching will be delayed until ``esp_timer`` task has a chance to run. For example, this will happen if a SPI Flash operation is in progress.
``ESP_TIMER_ISR``. Timer callbacks are dispatched directly from the timer interrupt handler. This method is useful for some simple callbacks which aim for lower latency.
Creating and starting a timer, and dispatching the callback takes some time. Therefore there is a lower limit to the timeout value of one-shot ``esp_timer``. If :cpp:func:`esp_timer_start_once` is called with a timeout value less than 20us, the callback will be dispatched only after approximately 20us.
Periodic ``esp_timer`` also imposes a 50us restriction on the minimal timer period. Periodic software timers with period of less than 50us are not practical since they would consume most of the CPU time. Consider using dedicated hardware peripherals or DMA features if you find that a timer with small period is required.
@ -47,6 +54,14 @@ The timer can be started in one-shot mode or in periodic mode.
Note that the timer must not be running when :cpp:func:`esp_timer_start_once` or :cpp:func:`esp_timer_start_periodic` is called. To restart a running timer, call :cpp:func:`esp_timer_stop` first, then call one of the start functions.
Callback functions
------------------
.. note: Keep the callback functions as short as possible otherwise it will affect all timers.
Timer callbacks which are processed by ``ESP_TIMER_ISR`` method should not have inside the context switch call - ``portYIELD_FROM_ISR()`` instead of this use the :cpp:func:`esp_timer_isr_dispatch_need_yield` function.
The context switch will be done after all ISR dispatch timers have been processed, if required by the system.
esp_timer during the light sleep
--------------------------------

View File

@ -1,3 +1,4 @@
# This config is split between targets since different component needs to be included (esp32, esp32s2)
CONFIG_IDF_TARGET="esp32"
TEST_COMPONENTS=freertos esp32 esp_ipc esp_system esp_timer driver heap pthread soc spi_flash vfs
CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=y

View File

@ -5,5 +5,6 @@ CONFIG_ESP_INT_WDT_TIMEOUT_MS=800
CONFIG_SPIRAM_OCCUPY_NO_HOST=y
CONFIG_ESP32_WIFI_RX_IRAM_OPT=n
CONFIG_ESP32_WIFI_IRAM_OPT=n
CONFIG_ESP_TIMER_PROFILING=n
# Disable encrypted flash reads/writes to save IRAM in this build configuration
CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=n