为何xTimerPeriodInTicks会影响ESP32+FreeRTOS代码的运行耗时?
问题背景
我正在撰写报告,需在ESP32平台结合FreeRTOS测量多个独立函数的执行周期,当前目标为onesecwindow.fifo_iir_filter_datapoint()函数。
初始定时器实现代码
float geninputarray[SAMPLING_RATE]; //stores the values of an artificial signal we can use to test the algorithm fifo_buffer onesecwindow = fifo_buffer(SAMPLING_RATE); esp_err_t init_input_generator() { dsps_tone_gen_f32(geninputarray, SAMPLING_RATE, 4, 0.04, 0); //10Hz TimerHandle_t input_signal_timer = xTimerCreate("input_timer", pdMS_TO_TICKS(10), pdTRUE, (void *)0, input_generator_callback); xTimerStart(input_signal_timer, 0); return ESP_OK; } void input_generator_callback(TimerHandle_t xTimer) { xTaskCreatePinnedToCore( input_counter, "input_generator", 4096, NULL, tskIDLE_PRIORITY + 11, NULL, PRO_CPU_NUM); } void input_counter(TimerHandle_t xTimer) { stepcounter = (stepcounter + 1) % SAMPLING_RATE; insert_new_data(geninputarray[stepcounter]); vTaskDelete(nullptr); // to gracefully end the task as returning is not allowed } esp_err_t insert_new_data(float datapoint) { onesecwindow.fifo_write(datapoint); // writes the new datapoint into the onesec window unsigned int start_time = dsp_get_cpu_cycle_count(); onesecwindow.fifo_iir_filter_datapoint(); unsigned int end_time = dsp_get_cpu_cycle_count(); printf("%i\n", (end_time-start_time)); }
异常现象
增大定时器周期时,测量耗时显著变长,且存在周期性异常值:
- 10ms周期输出示例:
1567, 630, 607, 624, 591, 619, 649, 1606, 607 - 40ms周期输出示例:
1904, 600, 1894, 616, 1928, 1928, 607, 1897, 628
重构后的循环任务代码
改用vTaskDelay替代定时器,但问题依然存在:
#include <esp_dsp.h> //Official ESP-DSP library float geninputarray[SAMPLING_RATE]; //stores the values of an artificial signal we can use to test the algorithm fifo_buffer onesecwindow = fifo_buffer(SAMPLING_RATE); esp_err_t init_input_generator() { dsps_tone_gen_f32(geninputarray, SAMPLING_RATE, 4, 0.04, 0); //10Hz xTaskCreatePinnedToCore( input_counter, // the actual function to be called "input_generator", 4096, NULL, tskIDLE_PRIORITY + 5, NULL, APP_CPU_NUM); return ESP_OK; } void input_counter(TimerHandle_t xTimer) { while (true) { stepcounter = (stepcounter + 1) % SAMPLING_RATE; insert_new_data(geninputarray[stepcounter]); vTaskDelay(4); } } esp_err_t insert_new_data(float datapoint) { onesecwindow.fifo_write(datapoint); // writes the new datapoint into the onesec window unsigned int start_time = dsp_get_cpu_cycle_count(); onesecwindow.fifo_iir_filter_datapoint(); unsigned int end_time = dsp_get_cpu_cycle_count(); printf("%i\n", (end_time-start_time)); }
任务始终绑定同一核心、使用相同优先级,无法理解为何定时器周期/任务延迟时长会影响测量结果,求技术解惑。
问题根源分析
1. 缓存失效导致的执行波动
ESP32的ICache/DCache会在任务长时间休眠时,将fifo_iir_filter_datapoint()的代码或相关数据逐出缓存。当任务再次唤醒执行时,需要从Flash/SRAM重新加载缓存,这会导致第一次执行函数的耗时大幅增加。定时器周期/延迟越长,缓存失效的概率越高,异常耗时出现的频率和幅度也会变大。
2. 系统任务的调度干扰
即使任务绑定核心且优先级固定,FreeRTOS内核仍会执行系统级任务(如IDLE任务、定时器服务任务、ESP-IDF后台任务),这些任务可能与测量任务竞争CPU资源。当定时器周期变长时,测量任务的执行时机更可能与系统任务的调度窗口重叠,导致测量耗时被拉长。
3. 初始代码的额外开销
第一种实现中,每次定时器触发都创建新任务,xTaskCreatePinnedToCore本身存在系统开销,且任务创建/删除过程会引入额外的CPU调度动作,干扰测量结果的准确性。
解决方案
1. 锁定缓存,避免失效
将目标函数标记为缓存常驻,使用ESP-IDF的IRAM_ATTR宏,确保函数加载到IRAM中,不会被ICache逐出:
void IRAM_ATTR fifo_buffer::fifo_iir_filter_datapoint() { // 原函数实现 }
2. 优化任务结构与预热
- 保持测量任务长期运行,避免频繁创建/删除任务;
- 在正式测量前预热函数,提前加载缓存,消除首次执行的额外耗时:
void input_counter(void *arg) { // 预热:连续执行5次目标函数,确保缓存加载完成 for(int i=0; i<5; i++){ onesecwindow.fifo_iir_filter_datapoint(); } while (true) { stepcounter = (stepcounter + 1) % SAMPLING_RATE; onesecwindow.fifo_write(geninputarray[stepcounter]); // 仅测量目标函数耗时 unsigned int start_time = dsp_get_cpu_cycle_count(); onesecwindow.fifo_iir_filter_datapoint(); unsigned int end_time = dsp_get_cpu_cycle_count(); printf("%i\n", (end_time-start_time)); vTaskDelay(pdMS_TO_TICKS(10)); } }
3. 隔离系统干扰
- 将测量任务设置为最高优先级,确保执行过程不被抢占:
xTaskCreatePinnedToCore( input_counter, "input_generator", 4096, NULL, configMAX_PRIORITIES - 1, // 最高优先级 NULL, PRO_CPU_NUM);
- 暂时禁用不必要的系统功能(如WiFi、蓝牙),减少后台任务干扰。
4. 稳定CPU频率
如果使用dsp_get_cpu_cycle_count()测量,需先锁定CPU频率,避免动态调频影响周期计数的准确性:
esp_clk_cpu_freq_set(ESP_CPU_FREQ_240M); // 锁定CPU到240MHz
内容的提问来源于stack exchange,提问作者TomK

