文章总结(帮你们节约时间)
- 任务通知是FreeRTOS中一种轻量级的任务间通信机制,相比传统的信号量和队列具有更高的效率和更少的内存占用。
- 软件定时器提供了一种在指定时间后执行回调函数的机制,支持一次性和周期性两种模式,适用于各种定时任务场景。
- 在ESP32和Arduino平台上,这两个特性的实现具有平台特定的优化,能够充分利用硬件资源。
- 通过合理的配置和使用,可以构建高效、稳定的嵌入式系统,实现复杂的多任务协调和定时控制。
你是否曾经在开发嵌入式系统时感到困惑:为什么明明写了多任务代码,但系统运行起来却像个蜗牛一样慢?为什么明明设置了定时器,但时间总是不准确?别急,今天我们就来深入探讨FreeRTOS中两个非常重要但经常被忽视的特性:任务通知和软件定时器。
想象一下,如果把FreeRTOS比作一个繁忙的工厂,那么任务通知就像是工人之间的手势暗号,简单快捷却能传达重要信息;而软件定时器则像是工厂里的时钟系统,确保每个流程都能在正确的时间点执行。掌握了这两个工具,你就能让你的嵌入式系统像一台精密的瑞士钟表一样运行!
任务通知篇
任务通知简介
任务通知是FreeRTOS 8.2.0版本引入的一个革命性特性,它为任务间通信提供了一种全新的解决方案。你可能会问:"既然已经有了信号量、队列、事件组这些经典的通信机制,为什么还需要任务通知呢?"答案很简单:效率!
传统的IPC(进程间通信)机制就像是邮政系统,每次通信都需要创建一个"信封"(内核对象),然后通过"邮递员"(调度器)来传递消息。而任务通知则更像是直接在对方耳边说话,省去了所有的中间环节。
任务通知的核心思想是:每个任务都有一个32位的通知值和一个通知状态。发送任务可以直接向目标任务发送通知,而不需要创建任何中间对象。这种机制的优势是显而易见的:
内存效率:不需要额外的内核对象,每个任务的通知机制只占用几个字节的内存。想象一下,如果你的系统有100个任务,使用传统的信号量可能需要几KB的内存,而使用任务通知几乎不占用额外内存。
速度优势:由于没有中间对象,任务通知的执行速度比传统IPC机制快45%左右。这就像是从"写信→投递→取信→读信"的过程简化为"直接告诉"的过程。
功能丰富:虽然简单,但任务通知支持多种通信模式:简单通知、计数通知、位标志通知和数值通知。这就像是一个多功能工具,既能当锤子用,也能当螺丝刀用。
在ESP32和Arduino平台上,任务通知的实现更是针对这些平台的特性进行了优化。ESP32作为一个双核处理器,任务通知可以在不同的CPU核心之间高效传递;而在Arduino平台上,任务通知为传统的单线程开发模式提供了一个平滑的多任务过渡方案。
任务通知的运作机制
要理解任务通知的运作机制,我们首先需要了解它的数据结构。每个任务控制块(TCB)都包含以下与通知相关的成员:
typedef struct tskTaskControlBlock {
// ... 其他成员
volatile uint32_t ulNotifiedValue; // 通知值
volatile uint8_t ucNotifyState; // 通知状态
// ... 其他成员
} tskTCB;
通知状态可以是以下三种之一:
taskNOT_WAITING_NOTIFICATION
:任务没有等待通知taskWAITING_NOTIFICATION
:任务正在等待通知taskNOTIFICATION_RECEIVED
:任务收到了通知
这种设计就像是一个简单的状态机,每个状态都有明确的含义和转换条件。
发送通知的过程:
当任务A要向任务B发送通知时,FreeRTOS会执行以下步骤:
-
检查目标任务状态:首先检查任务B是否处于等待通知状态。如果是,则直接唤醒任务B;如果不是,则更新通知值并设置通知状态为已收到。
-
更新通知值:根据不同的通知类型,更新目标任务的通知值。这可能是简单的设置、增加、或者位操作。
-
任务调度:如果目标任务被唤醒且优先级高于当前任务,则触发任务切换。
这个过程就像是给朋友发短信一样简单直接。不需要复杂的协议,不需要中间服务器,消息直接到达目标。
接收通知的过程:
当任务要接收通知时,会发生以下情况:
-
检查通知状态:如果已经有通知在等待,立即返回通知值;如果没有,则进入等待状态。
-
阻塞等待:如果指定了超时时间,任务会在指定时间内等待通知;如果指定了无限等待,任务会一直阻塞直到收到通知。
-
清理通知状态:收到通知后,根据配置可能会清除通知值或保留通知值。
通知值的操作模式:
任务通知支持多种操作模式,每种模式都有其特定的应用场景:
-
简单通知模式:不传递数据,只是简单地通知目标任务某个事件发生了。这就像是给朋友一个"OK"的手势,不需要更多信息。
-
计数通知模式:通知值作为计数器使用,每次发送通知时增加计数。这就像是计算有多少个任务在等待某个资源。
-
位标志模式:通知值的每一位代表一个不同的事件。这就像是一个多功能的开关面板,每个开关控制不同的功能。
-
数值传递模式:直接传递一个32位的数值。这就像是发送一个包含具体信息的消息。
在ESP32平台上的特殊考虑:
ESP32作为双核处理器,任务通知的实现需要考虑核心间同步的问题。FreeRTOS使用了以下机制来确保线程安全:
-
原子操作:使用ESP32的原子指令来更新通知值,确保操作的原子性。
-
核心间中断:当需要在不同核心之间发送通知时,使用核心间中断来通知目标核心。
-
内存屏障:使用适当的内存屏障指令来确保内存操作的顺序性。
这些机制的存在使得任务通知在多核环境下也能正确工作,就像是在两个房间之间安装了一个对讲机系统。
任务通知的函数接口详解
任务通知的API设计遵循了FreeRTOS一贯的简洁性原则,主要包括发送通知和接收通知两大类函数。让我们逐一分析这些函数的使用方法和注意事项。
发送通知函数族:
最基本的发送通知函数是xTaskNotify()
:
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction);
这个函数就像是一个多功能的信使,可以根据不同的eAction
参数执行不同的任务:
eNoAction
:仅仅是通知目标任务,不更改通知值。这就像是轻拍朋友的肩膀,告诉他"嘿,注意一下"。eSetBits
:将通知值与指定值进行OR运算。这就像是在一个多功能遥控器上按下多个按钮。eIncrement
:将通知值加1,忽略ulValue
参数。这就像是在计数器上加1。eSetValueWithOverwrite
:直接设置通知值,即使之前有未处理的通知。这就像是强制覆盖之前的消息。eSetValueWithoutOverwrite
:只有在没有未处理通知时才设置通知值。这就像是礼貌地等待对方读完上一条消息再发送新消息。
在实际应用中,你可能会这样使用:
// 简单通知:告诉LED任务该闪烁了
xTaskNotify(xLEDTaskHandle, 0, eNoAction);
// 位标志通知:告诉处理任务有多个事件发生
xTaskNotify(xProcessTaskHandle,
(1 << SENSOR_EVENT) | (1 << BUTTON_EVENT),
eSetBits);
// 计数通知:增加等待处理的数据包数量
xTaskNotify(xNetworkTaskHandle, 0, eIncrement);
// 数值通知:传递具体的温度值
xTaskNotify(xDisplayTaskHandle, temperature, eSetValueWithOverwrite);
为了支持中断服务程序,FreeRTOS还提供了xTaskNotifyFromISR()
函数:
BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken);
这个函数的特殊之处在于最后一个参数pxHigherPriorityTaskWoken
,它用于指示是否需要在ISR结束后进行任务切换。这就像是在紧急情况下,救护车司机需要知道是否需要立即改变路线。
接收通知函数族:
接收通知的主要函数是ulTaskNotifyTake()
:
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
TickType_t xTicksToWait);
这个函数有两种工作模式:
-
清除模式(
xClearCountOnExit = pdTRUE
):接收通知后将通知值清零。这就像是一次性使用的门票,用完就作废。 -
减少模式(
xClearCountOnExit = pdFALSE
):接收通知后将通知值减1。这就像是一叠票,每次用掉一张。
另一个重要的函数是xTaskNotifyWait()
:
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait);
这个函数更加灵活,允许你指定在等待开始时和结束时要清除的位。这就像是一个智能的邮箱,可以自动分类和处理不同类型的邮件。
在ESP32上的优化实现:
ESP32平台对任务通知进行了特殊的优化,特别是在处理硬件中断时。考虑一个典型的GPIO中断处理场景:
// GPIO中断处理函数
void IRAM_ATTR gpio_isr_handler(void* arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 通知处理任务
xTaskNotifyFromISR(xGPIOProcessTaskHandle,
(1 << GPIO_PIN_NUMBER),
eSetBits,
&xHigherPriorityTaskWoken);
// 如果有更高优先级的任务被唤醒,进行任务切换
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
// GPIO处理任务
void gpio_process_task(void* param) {
uint32_t ulNotificationValue;
while (1) {
// 等待GPIO中断通知
if (xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY)) {
// 处理不同的GPIO事件
for (int i = 0; i < 32; i++) {
if (ulNotificationValue & (1 << i)) {
// 处理GPIO i的事件
handle_gpio_event(i);
}
}
}
}
}
任务通知的性能分析:
让我们来看一个具体的性能对比。假设我们要在两个任务之间传递1000次简单信号:
使用信号量的方式:
// 创建信号量:约24字节内存
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
// 发送信号:约15μs(包括内核对象操作)
xSemaphoreGive(xSemaphore);
// 接收信号:约18μs(包括内核对象操作)
xSemaphoreTake(xSemaphore, portMAX_DELAY);
使用任务通知的方式:
// 无需创建对象:0字节额外内存
// 发送通知:约8μs(直接操作任务控制块)
xTaskNotify(xTargetTask, 0, eNoAction);
// 接收通知:约10μs(直接操作任务控制块)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
从这个对比可以看出,任务通知在内存使用和执行速度上都有明显优势。这就像是从"写信→投递→取信→读信"的传统邮政系统升级到了"直接打电话"的现代通信方式。
任务通知的局限性和注意事项:
虽然任务通知有很多优势,但也有一些局限性需要注意:
-
一对一通信:任务通知只支持一对一的通信,不能像队列那样支持多对一或一对多的通信模式。
-
单一通知值:每个任务只有一个32位的通知值,如果需要传递更复杂的数据结构,仍需要使用队列。
-
不可排队:如果目标任务没有及时处理通知,新的通知可能会覆盖旧的通知值。
-
依赖任务句柄:发送通知时需要知道目标任务的句柄,这在某些动态创建任务的场景中可能不太方便。
了解这些局限性后,我们就能更好地决定何时使用任务通知,何时使用传统的IPC机制。
软件定时器篇
软件定时器的基本概念
软件定时器是FreeRTOS中一个非常实用的特性,它允许你在指定的时间后执行特定的回调函数。如果把FreeRTOS比作一个管弦乐队,那么软件定时器就像是指挥家的指挥棒,确保每个乐器都能在正确的时间点响起。
你可能会问:"既然硬件定时器已经很好用了,为什么还需要软件定时器呢?"这就像是问"既然有了闹钟,为什么还需要手表的计时功能?"答案是:灵活性和数量!
硬件定时器虽然精确,但数量有限。以ESP32为例,它只有4个通用定时器,但你的应用可能需要几十个不同的定时任务。这时候软件定时器就派上用场了,它可以创建几乎无限数量的定时器,每个定时器都可以独立配置和管理。
软件定时器的核心特性:
-
回调函数执行:当定时器到期时,会调用预先注册的回调函数。这就像是预约了一个闹钟,时间到了就会响起。
-
一次性和周期性:软件定时器可以配置为一次性(like a countdown timer)或周期性(like a metronome)。
-
动态管理:可以在运行时创建、删除、启动、停止和重新配置定时器。
-
优先级管理:所有定时器回调函数都在同一个专门的定时器任务中执行,这个任务的优先级可以配置。
软件定时器的工作原理:
FreeRTOS使用一个专门的任务(Timer Task)来管理所有的软件定时器。这个任务维护着一个定时器列表,按照到期时间排序。当系统Tick中断发生时,定时器任务会检查是否有定时器到期,如果有,就执行相应的回调函数。
这种设计就像是一个专业的时间管理师,他有一个详细的时间表,知道每个时间点该做什么事情。当时间到了,他就会按照计划执行相应的任务。
软件定时器的数据结构:
每个软件定时器都有一个控制块(Timer Control Block),包含以下关键信息:
typedef struct tmrTimerControl {
const char *pcTimerName; // 定时器名称(用于调试)
ListItem_t xTimerListItem; // 链表项,用于链接到定时器链表
TickType_t xTimerPeriodInTicks; // 定时器周期(以tick为单位)
UBaseType_t uxAutoReload; // 自动重载标志
void *pvTimerID; // 定时器ID(用户数据)
TimerCallbackFunction_t pxCallbackFunction; // 回调函数
#if (configUSE_TRACE_FACILITY == 1)
UBaseType_t uxTimerNumber; // 定时器编号(用于跟踪)
#endif
} xTIMER;
这个结构体就像是每个定时器的"身份证",包含了定时器的所有重要信息。
软件定时器与硬件定时器的对比:
让我们来看一个详细的对比表:
特性 | 硬件定时器 | 软件定时器 |
---|---|---|
数量限制 | 有限(ESP32有4个) | 理论上无限(受内存限制) |
精度 | 非常高(硬件级别) | 较低(取决于系统tick) |
资源消耗 | 占用硬件资源 | 占用少量RAM和CPU时间 |
配置复杂度 | 较复杂(需要配置寄存器) | 简单(API调用) |
中断处理 | 直接触发中断 | 通过任务执行回调 |
实时性 | 高(硬件级别) | 中等(取决于任务调度) |
这就像是专业摄影师的单反相机(硬件定时器)和智能手机的相机(软件定时器)的区别:前者功能强大但数量有限,后者虽然精度稍低但使用方便且数量充足。
软件定时器的应用场景
软件定时器在嵌入式系统中有着广泛的应用场景,就像是一个多功能的时间管理工具,能够适应各种不同的需求。
LED闪烁控制:
这是最经典的应用场景之一。想象一下,你需要控制多个LED以不同的频率闪烁,如果用硬件定时器,很快就会资源不足。但用软件定时器,你可以轻松实现:
// 不同频率的LED闪烁
TimerHandle_t xLED1Timer, xLED2Timer, xLED3Timer;
void led1_callback(TimerHandle_t xTimer) {
gpio_set_level(LED1_PIN, !gpio_get_level(LED1_PIN));
}
void led2_callback(TimerHandle_t xTimer) {
gpio_set_level(LED2_PIN, !gpio_get_level(LED2_PIN));
}
void led3_callback(TimerHandle_t xTimer) {
gpio_set_level(LED3_PIN, !gpio_get_level(LED3_PIN));
}
// 创建不同周期的定时器
xLED1Timer = xTimerCreate("LED1", pdMS_TO_TICKS(500), pdTRUE, NULL, led1_callback);
xLED2Timer = xTimerCreate("LED2", pdMS_TO_TICKS(1000), pdTRUE, NULL, led2_callback);
xLED3Timer = xTimerCreate("LED3", pdMS_TO_TICKS(2000), pdTRUE, NULL, led3_callback);
这就像是指挥一个LED交响乐团,每个LED都有自己的节拍!
超时监控:
在网络通信或传感器读取中,经常需要设置超时机制。软件定时器提供了一个优雅的解决方案:
TimerHandle_t xTimeoutTimer;
void timeout_callback(TimerHandle_t xTimer) {
// 超时处理:关闭连接、重试或报告错误
ESP_LOGE(TAG, "Operation timeout!");
connection_state = DISCONNECTED;
xTaskNotify(xNetworkTaskHandle, TIMEOUT_EVENT, eSetBits);
}
// 开始某个操作时启动超时定时器
void start_operation_with_timeout(uint32_t timeout_ms) {
xTimerChangePeriod(xTimeoutTimer, pdMS_TO_TICKS(timeout_ms), 0);
xTimerStart(xTimeoutTimer, 0);
// 开始实际操作...
}
// 操作完成时停止超时定时器
void operation_completed(void) {
xTimerStop(xTimeoutTimer, 0);
// 处理操作结果...
}
这就像是给每个重要操作都配了一个"看门狗",确保系统不会因为某个操作卡住而整体瘫痪。
周期性数据采集:
在IoT应用中,经常需要定期采集传感器数据。软件定时器可以轻松实现这一需求:
struct sensor_timer_data {
int sensor_id;
float* data_buffer;
size_t buffer_size;
size_t current_index;
};
void sensor_sampling_callback(TimerHandle_t xTimer) {
struct sensor_timer_data* timer_data = (struct sensor_timer_data*)pvTimerGetTimerID(xTimer);
// 读取传感器数据
float sensor_value = read_sensor(timer_data->sensor_id);
// 存储到缓冲区
timer_data->data_buffer[timer_data->current_index] = sensor_value;
timer_data->current_index = (timer_data->current_index + 1) % timer_data->buffer_size;
// 如果缓冲区满了,通知处理任务
if (timer_data->current_index == 0) {
xTaskNotify(xDataProcessTaskHandle, timer_data->sensor_id, eSetValueWithOverwrite);
}
}
// 为不同传感器创建不同的采样定时器
void setup_sensor_sampling(void) {
for (int i = 0; i < NUM_SENSORS; i++) {
struct sensor_timer_data* timer_data = malloc(sizeof(struct sensor_timer_data));
timer_data->sensor_id = i;
timer_data->data_buffer = malloc(BUFFER_SIZE * sizeof(float));
timer_data->buffer_size = BUFFER_SIZE;
timer_data->current_index = 0;
TimerHandle_t xTimer = xTimerCreate(
sensor_names[i],
pdMS_TO_TICKS(sampling_intervals[i]),
pdTRUE, // 周期性
timer_data,
sensor_sampling_callback
);
xTimerStart(xTimer, 0);
}
}
这就像是为每个传感器都配了一个专门的"采样员",按照预定的时间表定期"巡视"并收集数据。
系统状态监控:
软件定时器还可以用于系统健康监控,定期检查系统状态:
void system_health_check_callback(TimerHandle_t xTimer) {
// 检查内存使用情况
size_t free_heap = esp_get_free_heap_size();
if (free_heap < MIN_FREE_HEAP) {
ESP_LOGW(TAG, "Low memory warning: %d bytes free", free_heap);
}
// 检查任务栈使用情况
UBaseType_t stack_high_water = uxTaskGetStackHighWaterMark(NULL);
if (stack_high_water < MIN_STACK_REMAINING) {
ESP_LOGW(TAG, "Low stack warning: %d bytes remaining", stack_high_water);
}
// 检查WiFi连接状态
if (wifi_is_connected() == false) {
ESP_LOGW(TAG, "WiFi disconnected, attempting reconnect");
wifi_reconnect();
}
// 更新系统运行时间统计
update_system_uptime();
}
// 创建系统健康检查定时器
TimerHandle_t xHealthCheckTimer = xTimerCreate(
"HealthCheck",
pdMS_TO_TICKS(30000), // 每30秒检查一次
pdTRUE, // 周期性
NULL,
system_health_check_callback
);
这就像是给系统配了一个"家庭医生",定期体检,及时发现并处理潜在问题。
用户交互超时:
在有用户界面的应用中,软件定时器可以用来处理用户交互超时:
TimerHandle_t xUserInteractionTimer;
void user_interaction_timeout_callback(TimerHandle_t xTimer) {
// 用户长时间无操作,进入省电模式
ESP_LOGI(TAG, "User inactive, entering sleep mode");
// 关闭显示屏
display_off();
// 降低CPU频率
esp_pm_configure(&pm_config_sleep);
// 通知其他任务进入省电状态
xTaskNotifyFromISR(xPowerManagementTaskHandle, ENTER_SLEEP_MODE, eSetBits, NULL);
}
// 用户有操作时重置定时器
void reset_user_interaction_timer(void) {
xTimerReset(xUserInteractionTimer, 0);
}
// 按键中断处理函数
void IRAM_ATTR button_isr_handler(void* arg) {
// 重置用户交互定时器
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerResetFromISR(xUserInteractionTimer, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
这就像是一个贴心的管家,当主人长时间不在家时,会自动关闭不必要的电器以节省电费。
软件定时器的精度
软件定时器的精度是一个经常被问到的问题,特别是对于那些从硬件定时器转向软件定时器的开发者。要理解软件定时器的精度,我们需要先了解FreeRTOS的时间基准系统。
系统Tick的概念:
FreeRTOS的时间基准是系统Tick,这是一个周期性的时钟中断。就像是系统的心跳一样,每隔一定时间就"跳动"一次。Tick的频率由configTICK_RATE_HZ
配置项决定,通常设置为100Hz、1000Hz或更高。
Ttick=1configTICK_RATE_HZT_{tick} = \frac{1}{configTICK\_RATE\_HZ}Ttick=configTICK_RATE_HZ1
例如,如果configTICK_RATE_HZ = 1000
,那么每个Tick的时间是1ms。
软件定时器的精度限制:
软件定时器的精度受到以下因素限制:
-
Tick精度限制:软件定时器的最小时间单位是一个Tick。如果Tick频率是1000Hz,那么定时器的最小精度就是1ms。
-
调度延迟:当定时器到期时,需要等待定时器任务被调度执行。如果系统中有更高优先级的任务在运行,定时器回调函数的执行会被延迟。
-
回调函数执行时间:如果回调函数执行时间过长,会影响其他定时器的精度。
让我们用一个具体的例子来说明:
// 假设系统Tick频率为1000Hz(1ms)
// 创建一个500.5ms的定时器
TimerHandle_t xTimer = xTimerCreate(
"TestTimer",
pdMS_TO_TICKS(500.5), // 实际上会被舍入为500或501个Tick
pdTRUE,
NULL,
timer_callback
);
在这个例子中,500.5ms会被转换为Tick数:
Tickcount=500.5ms1ms=500.5Tick_{count} = \frac{500.5ms}{1ms} = 500.5Tickcount=1ms500.5ms=500.5
由于Tick计数必须是整数,所以会被舍入为500或501个Tick(取决于具体实现)。
提高软件定时器精度的方法:
- 增加Tick频率:将
configTICK_RATE_HZ
设置为更高的值可以提高精度,但会增加系统开销。
// 在FreeRTOSConfig.h中
#define configTICK_RATE_HZ 10000 // 10kHz,精度为0.1ms
- 优化定时器任务优先级:确保定时器任务有足够高的优先级,以减少调度延迟。
// 在FreeRTOSConfig.h中
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
- 减少回调函数执行时间:保持回调函数简短,复杂的处理放到其他任务中执行。
// 好的做法:简短的回调函数
void timer_callback(TimerHandle_t xTimer) {
// 只发送通知,不做复杂处理
xTaskNotify(xProcessTaskHandle, TIMER_EVENT, eSetBits);
}
// 在处理任务中进行复杂操作
void process_task(void *param) {
uint32_t ulNotificationValue;
while (1) {
if (xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY)) {
if (ulNotificationValue & TIMER_EVENT) {
// 进行复杂的处理
complex_processing();
}
}
}
}
软件定时器精度的测量:
在实际应用中,我们可以通过以下方式测量软件定时器的精度:
#include "esp_timer.h"
static int64_t last_callback_time = 0;
static int64_t callback_count = 0;
static int64_t total_jitter = 0;
void precision_test_callback(TimerHandle_t xTimer) {
int64_t current_time = esp_timer_get_time(); // 获取微秒级时间戳
if (last_callback_time != 0) {
int64_t interval = current_time - last_callback_time;
int64_t expected_interval = 1000000; // 1秒 = 1000000微秒
int64_t jitter = abs(interval - expected_interval);
total_jitter += jitter;
callback_count++;
// 每100次回调打印一次统计信息
if (callback_count % 100 == 0) {
int64_t avg_jitter = total_jitter / callback_count;
ESP_LOGI(TAG, "Average jitter: %lld microseconds", avg_jitter);
}
}
last_callback_time = current_time;
}
// 创建精度测试定时器
TimerHandle_t xPrecisionTestTimer = xTimerCreate(
"PrecisionTest",
pdMS_TO_TICKS(1000), // 1秒间隔
pdTRUE,
NULL,
precision_test_callback
);
不同应用场景的精度需求:
不同的应用场景对定时器精度有不同的要求:
-
LED闪烁:精度要求不高,几十毫秒的误差都可以接受。软件定时器完全胜任。
-
用户界面更新:需要相对较高的精度,但几毫秒的误差通常不会被用户察觉。
-
网络超时:精度要求中等,秒级的超时可以容忍几十毫秒的误差。
-
PWM信号生成:需要非常高的精度,通常需要使用硬件定时器。
-
音频播放:需要极高的精度,必须使用硬件定时器或专门的音频硬件。
ESP32平台的特殊考虑:
在ESP32平台上,还需要考虑以下因素:
-
WiFi和蓝牙影响:ESP32的WiFi和蓝牙模块会产生中断,可能影响定时器精度。
-
电源管理:ESP32的动态频率调节功能可能影响定时器精度。
-
双核处理:定时器任务运行在哪个核心上可能会影响精度。
// 在ESP32上优化定时器精度的配置
void optimize_timer_precision(void) {
// 禁用动态频率调节
esp_pm_config_esp32_t pm_config = {
.max_freq_mhz = 240,
.min_freq_mhz = 240, // 固定频率
.light_sleep_enable = false
};
esp_pm_configure(&pm_config);
// 将定时器任务固定到特定核心
#if configNUM_CORES > 1
TaskHandle_t xTimerTaskHandle = xTimerGetTimerDaemonTaskHandle();
vTaskCoreAffinitySet(xTimerTaskHandle, (1 << 1)); // 固定到核心1
#endif
}
软件定时器的运作机制
要深入理解软件定时器的运作机制,我们需要探索FreeRTOS内部的实现细节。软件定时器的工作原理就像是一个精密的时钟工厂,每个定时器都是一个独立的时钟,而定时器任务则是那个负责管理所有时钟的工匠。
定时器任务的核心作用:
FreeRTOS创建了一个专门的任务来管理所有的软件定时器,这个任务被称为"Timer Task"或"Timer Daemon Task"。这个任务的主要职责是:
- 维护定时器列表:按照到期时间排序的定时器链表
- 处理定时器命令:创建、删除、启动、停止定时器的命令
- 执行到期回调:当定时器到期时执行相应的回调函数
- 管理定时器状态:跟踪每个定时器的状态变化
定时器任务的主循环可以简化为以下伪代码:
void prvTimerTask(void *pvParameters) {
TickType_t xNextExpireTime;
for (;;) {
// 计算下一个定时器的到期时间
xNextExpireTime = prvGetNextExpireTime();
// 等待命令或者等到下一个定时器到期
if (xQueueReceive(xTimerQueue, &xMessage, xNextExpireTime) == pdPASS) {
// 处理定时器命令(创建、删除、启动、停止等)
prvProcessTimerCommand(&xMessage);
} else {
// 超时表示有定时器到期了
prvProcessExpiredTimers();
}
}
}
定时器列表的管理:
FreeRTOS使用两个链表来管理定时器:
- 活动定时器列表:存储当前正在运行的定时器
- 溢出定时器列表:处理系统Tick计数器溢出的情况
这种双链表设计是为了处理系统Tick计数器的溢出问题。当系统运行时间很长时,Tick计数器会溢出回零,这时就需要特殊处理。
// 简化的定时器列表结构
static List_t xActiveTimerList1;
static List_t xActiveTimerList2;
static List_t *pxCurrentTimerList;
static List_t *pxOverflowTimerList;
// 插入定时器到合适的列表
static void prvInsertTimerInActiveList(Timer_t * const pxTimer) {
TickType_t xNextExpiryTime = pxTimer->xTimerPeriodInTicks;
// 检查是否会溢出
if (xNextExpiryTime < xTickCount) {
// 插入到溢出列表
vListInsert(pxOverflowTimerList, &(pxTimer->xTimerListItem));
} else {
// 插入到当前活动列表
vListInsert(pxCurrentTimerList, &(pxTimer->xTimerListItem));
}
}
定时器命令队列:
定时器任务通过一个命令队列接收来自其他任务的命令。这种设计确保了定时器操作的线程安全性。常见的命令包括:
typedef enum {
tmrCOMMAND_START,
tmrCOMMAND_STOP,
tmrCOMMAND_CHANGE_PERIOD,
tmrCOMMAND_DELETE,
tmrCOMMAND_RESET,
tmrCOMMAND_START_FROM_ISR,
tmrCOMMAND_STOP_FROM_ISR,
tmrCOMMAND_RESET_FROM_ISR,
tmrCOMMAND_CHANGE_PERIOD_FROM_ISR
} TMRCommand_t;
typedef struct {
TMRCommand_t eCommandType;
union {
struct {
Timer_t *pxTimer;
TickType_t xOptionalValue;
} xTimerGenericCommand;
} u;
} DaemonTaskMessage_t;
这种命令队列机制就像是一个邮政系统,其他任务可以给定时器任务"写信",告诉它要做什么操作。定时器任务收到"信件"后,会按照信件内容执行相应的操作。
定时器到期处理流程:
当定时器到期时,FreeRTOS会执行以下处理流程:
- 检查到期定时器:遍历活动定时器列表,找出所有到期的定时器
- 移除到期定时器:将到期的定时器从活动列表中移除
- 执行回调函数:调用定时器的回调函数
- 处理自动重载:如果是周期性定时器,重新计算下次到期时间并插入列表
- 处理删除请求:如果是一次性定时器,释放定时器资源
static void prvProcessExpiredTimers(void) {
Timer_t *pxTimer;
TickType_t xCurrentTime = xTaskGetTickCount();
// 处理所有到期的定时器
while (listLIST_IS_EMPTY(pxCurrentTimerList) == pdFALSE) {
pxTimer = (Timer_t *)listGET_OWNER_OF_HEAD_ENTRY(pxCurrentTimerList);
// 检查定时器是否到期
if (pxTimer->xTimerPeriodInTicks > xCurrentTime) {
break; // 这个定时器还没到期,后面的也不会到期
}
// 移除到期的定时器
uxListRemove(&(pxTimer->xTimerListItem));
// 执行回调函数
pxTimer->pxCallbackFunction(pxTimer);
// 如果是周期性定时器,重新插入列表
if (pxTimer->uxAutoReload == pdTRUE) {
prvReloadTimer(pxTimer);
}
}
}
系统Tick中断的协调:
软件定时器与系统Tick中断有着密切的关系。每当系统Tick中断发生时,FreeRTOS会检查是否有软件定时器到期。这个过程是通过vApplicationTickHook()
函数实现的:
void vApplicationTickHook(void) {
// 检查是否有定时器到期
if (xTimerPendFunctionCall != NULL) {
// 唤醒定时器任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(xTimerTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
这种设计就像是一个闹钟店的老板,每当墙上的大钟响起时,他就会检查店里所有的闹钟,看看是否有到时间的需要响铃。
内存管理和性能优化:
软件定时器的内存管理也是一个重要的考虑因素。每个定时器都需要一个定时器控制块(Timer Control Block),大小大约为:
// 定时器控制块的大小估算
sizeof(Timer_t) = sizeof(char*) + // pcTimerName
sizeof(ListItem_t) + // xTimerListItem
sizeof(TickType_t) + // xTimerPeriodInTicks
sizeof(UBaseType_t) + // uxAutoReload
sizeof(void*) + // pvTimerID
sizeof(void*) + // pxCallbackFunction
sizeof(UBaseType_t); // uxTimerNumber (可选)
在ESP32平台上,这大约是32-40字节。如果你有100个定时器,总共只需要3-4KB的内存,这相比硬件定时器的资源限制是非常经济的。
定时器任务的优先级考虑:
定时器任务的优先级设置是一个需要仔细考虑的问题。如果优先级过低,定时器的精度会受到影响;如果优先级过高,可能会影响其他关键任务的执行。
// 推荐的优先级设置
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 2)
#define configTIMER_TASK_STACK_DEPTH 512
这就像是在管弦乐队中为指挥家安排座位,既要让他能看到所有乐手,又不能让他挡住观众的视线。
多核处理的特殊考虑:
在ESP32这样的多核处理器上,定时器任务的调度还需要考虑核心亲和性:
void setup_timer_task_affinity(void) {
TaskHandle_t xTimerTaskHandle = xTimerGetTimerDaemonTaskHandle();
// 将定时器任务固定到特定核心
vTaskCoreAffinitySet(xTimerTaskHandle, (1 << 1)); // 固定到核心1
// 或者允许在任意核心上运行
// vTaskCoreAffinitySet(xTimerTaskHandle, tskNO_AFFINITY);
}
定时器链表的动态切换:
FreeRTOS使用了一个聪明的技巧来处理系统Tick溢出的问题。当系统Tick计数器即将溢出时,会切换活动定时器列表:
static void prvSwitchTimerLists(void) {
// 交换当前列表和溢出列表
List_t *pxTemp = pxCurrentTimerList;
pxCurrentTimerList = pxOverflowTimerList;
pxOverflowTimerList = pxTemp;
// 重新处理所有定时器
prvProcessExpiredTimers();
}
这种机制确保了即使在系统运行很长时间后,定时器依然能够正确工作。就像是一个永不停歇的时钟,即使指针转了一圈又一圈,依然能准确计时。
软件定时器函数接口详解
软件定时器的API设计体现了FreeRTOS一贯的设计理念:简单易用但功能强大。就像是一把瑞士军刀,看起来简单,但每个功能都经过精心设计。
创建定时器函数:
最基本的定时器创建函数是xTimerCreate()
:
TimerHandle_t xTimerCreate(const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction);
这个函数的参数设计非常巧妙:
pcTimerName
:定时器名称,主要用于调试。就像给每个定时器起个名字,方便识别。xTimerPeriodInTicks
:定时器周期,以系统Tick为单位。这就是定时器的"心跳间隔"。uxAutoReload
:是否自动重载。pdTRUE
表示周期性定时器,pdFALSE
表示一次性定时器。pvTimerID
:用户数据指针,可以用来区分不同的定时器实例。pxCallbackFunction
:回调函数指针,定时器到期时会调用这个函数。
让我们看一个完整的例子:
// 定义不同类型的定时器回调函数
void led_blink_callback(TimerHandle_t xTimer) {
int led_pin = (int)pvTimerGetTimerID(xTimer);
gpio_set_level(led_pin, !gpio_get_level(led_pin));
}
void sensor_read_callback(TimerHandle_t xTimer) {
sensor_data_t* sensor = (sensor_data_t*)pvTimerGetTimerID(xTimer);
sensor->value = read_sensor_value(sensor->pin);
xTaskNotify(xDataProcessTask, sensor->id, eSetValueWithOverwrite);
}
void timeout_callback(TimerHandle_t xTimer) {
const char* operation = (const char*)pvTimerGetTimerID(xTimer);
ESP_LOGW(TAG, "Operation %s timeout!", operation);
}
// 创建不同类型的定时器
void create_various_timers(void) {
// 创建LED闪烁定时器(周期性)
TimerHandle_t xLEDTimer = xTimerCreate(
"LED Blink", // 定时器名称
pdMS_TO_TICKS(500), // 500ms周期
pdTRUE, // 自动重载(周期性)
(void*)LED_PIN, // 用户数据:LED引脚号
led_blink_callback // 回调函数
);
// 创建传感器读取定时器(周期性)
static sensor_data_t temp_sensor = {.id = 1, .pin = TEMP_SENSOR_PIN};
TimerHandle_t xSensorTimer = xTimerCreate(
"Temperature Sensor",
pdMS_TO_TICKS(1000), // 1秒周期
pdTRUE, // 周期性
&temp_sensor, // 用户数据:传感器信息
sensor_read_callback
);
// 创建超时定时器(一次性)
TimerHandle_t xTimeoutTimer = xTimerCreate(
"Network Timeout",
pdMS_TO_TICKS(5000), // 5秒超时
pdFALSE, // 一次性
"Network Connection", // 用户数据:操作描述
timeout_callback
);
// 启动定时器
xTimerStart(xLEDTimer, 0);
xTimerStart(xSensorTimer, 0);
// 超时定时器根据需要启动
}
定时器控制函数:
定时器创建后,我们需要各种控制函数来管理它们:
启动定时器:
BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xBlockTime);
这个函数就像是按下秒表的开始按钮。xBlockTime
参数指定了如果定时器命令队列满时的等待时间。
停止定时器:
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xBlockTime);
这就像是按下秒表的停止按钮。停止后的定时器可以重新启动。
重置定时器:
BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xBlockTime);
重置定时器会将定时器的计数重新开始。这就像是重新启动一个计时器。
修改定时器周期:
BaseType_t xTimerChangePeriod(TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xBlockTime);
这个函数允许在运行时修改定时器的周期,非常有用。
让我们看一个实际的应用场景:
// 自适应LED闪烁频率
TimerHandle_t xAdaptiveLEDTimer;
static int current_blink_speed = 1; // 1=慢,2=中,3=快
void adaptive_led_callback(TimerHandle_t xTimer) {
static int blink_count = 0;
// 切换LED状态
gpio_set_level(LED_PIN, !gpio_get_level(LED_PIN));
// 每20次闪烁检查一次系统负载
if (++blink_count >= 20) {
blink_count = 0;
int cpu_usage = get_cpu_usage_percent();
// 根据CPU使用率调整闪烁速度
int new_speed;
if (cpu_usage < 30) {
new_speed = 1; // 慢闪:系统空闲
} else if (cpu_usage < 70) {
new_speed = 2; // 中速:系统正常
} else {
new_speed = 3; // 快闪:系统繁忙
}
// 如果速度改变,更新定时器周期
if (new_speed != current_blink_speed) {
current_blink_speed = new_speed;
TickType_t new_period;
switch (new_speed) {
case 1: new_period = pdMS_TO_TICKS(1000); break; // 1秒
case 2: new_period = pdMS_TO_TICKS(500); break; // 0.5秒
case 3: new_period = pdMS_TO_TICKS(200); break; // 0.2秒
}
xTimerChangePeriod(xAdaptiveLEDTimer, new_period, 0);
}
}
}
void setup_adaptive_led(void) {
xAdaptiveLEDTimer = xTimerCreate(
"Adaptive LED",
pdMS_TO_TICKS(1000), // 初始周期1秒
pdTRUE, // 周期性
NULL,
adaptive_led_callback
);
xTimerStart(xAdaptiveLEDTimer, 0);
}
这个例子展示了如何根据系统状态动态调整定时器行为,就像是一个智能的系统负载指示器。
删除定时器:
BaseType_t xTimerDelete(TimerHandle_t xTimer, TickType_t xBlockTime);
删除定时器会释放相关资源。这就像是丢弃一个不再需要的闹钟。
中断安全函数:
对于需要在中断服务程序中使用的场景,FreeRTOS提供了专门的ISR安全函数:
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xTimerChangePeriodFromISR(TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken);
这些函数的使用场景通常是在硬件中断发生时启动或重置软件定时器:
// 按键中断处理函数
void IRAM_ATTR button_isr_handler(void* arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 重置按键去抖动定时器
xTimerResetFromISR(xButtonDebounceTimer, &xHigherPriorityTaskWoken);
// 启动按键长按检测定时器
xTimerStartFromISR(xButtonLongPressTimer, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
// 按键去抖动回调函数
void button_debounce_callback(TimerHandle_t xTimer) {
// 去抖动时间结束,读取按键状态
if (gpio_get_level(BUTTON_PIN) == 0) {
// 按键确实被按下
xTaskNotify(xButtonProcessTask, BUTTON_PRESSED, eSetBits);
}
// 停止长按检测定时器
xTimerStop(xButtonLongPressTimer, 0);
}
// 按键长按检测回调函数
void button_long_press_callback(TimerHandle_t xTimer) {
// 长按时间到达
xTaskNotify(xButtonProcessTask, BUTTON_LONG_PRESSED, eSetBits);
}
定时器查询函数:
FreeRTOS还提供了一些查询函数,用于获取定时器的状态信息:
// 获取定时器的用户数据
void* pvTimerGetTimerID(TimerHandle_t xTimer);
// 设置定时器的用户数据
void vTimerSetTimerID(TimerHandle_t xTimer, void *pvNewID);
// 获取定时器周期
TickType_t xTimerGetPeriod(TimerHandle_t xTimer);
// 获取定时器到期时间
TickType_t xTimerGetExpiryTime(TimerHandle_t xTimer);
// 检查定时器是否活跃
UBaseType_t uxTimerGetReloadMode(TimerHandle_t xTimer);
这些函数在调试和动态管理定时器时非常有用:
// 定时器状态监控任务
void timer_monitor_task(void* param) {
TimerHandle_t timers[] = {xLEDTimer, xSensorTimer, xTimeoutTimer};
int num_timers = sizeof(timers) / sizeof(timers[0]);
while (1) {
ESP_LOGI(TAG, "=== Timer Status Report ===");
for (int i = 0; i < num_timers; i++) {
if (timers[i] != NULL) {
TickType_t period = xTimerGetPeriod(timers[i]);
TickType_t expiry = xTimerGetExpiryTime(timers[i]);
TickType_t current = xTaskGetTickCount();
ESP_LOGI(TAG, "Timer %d: Period=%d, Expires in %d ticks",
i, period, expiry - current);
}
}
vTaskDelay(pdMS_TO_TICKS(10000)); // 每10秒报告一次
}
}
定时器任务管理函数:
最后,还有一些用于管理定时器任务本身的函数:
// 获取定时器任务句柄
TaskHandle_t xTimerGetTimerDaemonTaskHandle(void);
// 挂起定时器任务
void vTimerSetTimerID(TimerHandle_t xTimer, void *pvNewID);
这些函数主要用于高级的系统管理场景,比如在系统进入低功耗模式时暂停定时器任务。
性能优化技巧:
在使用软件定时器时,有一些性能优化的技巧:
- 批量操作:如果需要同时操作多个定时器,尽量批量进行:
void batch_timer_operations(void) {
// 批量启动多个定时器
xTimerStart(xTimer1, 0);
xTimerStart(xTimer2, 0);
xTimerStart(xTimer3, 0);
// 而不是分别启动并等待
// xTimerStart(xTimer1, portMAX_DELAY);
// xTimerStart(xTimer2, portMAX_DELAY);
// xTimerStart(xTimer3, portMAX_DELAY);
}
- 回调函数优化:保持回调函数简短,复杂操作通过任务通知传递给其他任务:
// 好的做法:简短的回调函数
void optimized_callback(TimerHandle_t xTimer) {
int timer_id = (int)pvTimerGetTimerID(xTimer);
xTaskNotify(xWorkerTask, timer_id, eSetValueWithOverwrite);
}
// 避免的做法:复杂的回调函数
void bad_callback(TimerHandle_t xTimer) {
// 避免在回调函数中进行复杂操作
complex_calculation();
file_write_operation();
network_send_data();
}
- 合理设置定时器任务优先级:
// 在FreeRTOSConfig.h中设置合适的优先级
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 2)
#define configTIMER_TASK_STACK_DEPTH 1024
通过这些优化技巧,可以确保软件定时器在系统中高效运行,就像是一个经过精心调校的时钟系统。
实际应用案例综合展示:
让我们通过一个完整的IoT数据采集系统来展示软件定时器和任务通知的综合应用:
// 系统配置
#define NUM_SENSORS 3
#define DATA_BUFFER_SIZE 100
#define UPLOAD_INTERVAL_MS 60000 // 1分钟上传一次
// 传感器数据结构
typedef struct {
int id;
int pin;
float* data_buffer;
int buffer_index;
int sample_interval_ms;
TimerHandle_t sample_timer;
} sensor_t;
// 系统状态
typedef struct {
sensor_t sensors[NUM_SENSORS];
TimerHandle_t upload_timer;
TimerHandle_t status_timer;
TimerHandle_t watchdog_timer;
TaskHandle_t data_process_task;
TaskHandle_t upload_task;
bool system_healthy;
} iot_system_t;
static iot_system_t g_iot_system;
// 传感器采样回调函数
void sensor_sample_callback(TimerHandle_t xTimer) {
sensor_t* sensor = (sensor_t*)pvTimerGetTimerID(xTimer);
// 读取传感器数据
float value = read_sensor_value(sensor->pin);
// 存储到缓冲区
sensor->data_buffer[sensor->buffer_index] = value;
sensor->buffer_index = (sensor->buffer_index + 1) % DATA_BUFFER_SIZE;
// 如果缓冲区满了,通知数据处理任务
if (sensor->buffer_index == 0) {
xTaskNotify(g_iot_system.data_process_task,
(1 << sensor->id), eSetBits);
}
}
// 数据上传回调函数
void data_upload_callback(TimerHandle_t xTimer) {
xTaskNotify(g_iot_system.upload_task, UPLOAD_TRIGGER, eSetBits);
}
// 系统状态检查回调函数
void system_status_callback(TimerHandle_t xTimer) {
// 检查系统健康状态
bool healthy = check_system_health();
if (healthy != g_iot_system.system_healthy) {
g_iot_system.system_healthy = healthy;
if (healthy) {
ESP_LOGI(TAG, "System recovered");
// 恢复正常采样频率
for (int i = 0; i < NUM_SENSORS; i++) {
xTimerChangePeriod(g_iot_system.sensors[i].sample_timer,
pdMS_TO_TICKS(g_iot_system.sensors[i].sample_interval_ms),
0);
}
} else {
ESP_LOGW(TAG, "System unhealthy, reducing sample rate");
// 降低采样频率以节省资源
for (int i = 0; i < NUM_SENSORS; i++) {
xTimerChangePeriod(g_iot_system.sensors[i].sample_timer,
pdMS_TO_TICKS(g_iot_system.sensors[i].sample_interval_ms * 2),
0);
}
}
}
// 重置看门狗定时器
xTimerReset(g_iot_system.watchdog_timer, 0);
}
// 看门狗回调函数
void watchdog_callback(TimerHandle_t xTimer) {
ESP_LOGE(TAG, "System watchdog timeout! Restarting...");
esp_restart();
}
// 数据处理任务
void data_process_task(void* param) {
uint32_t ulNotificationValue;
while (1) {
if (xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY)) {
// 处理每个传感器的数据
for (int i = 0; i < NUM_SENSORS; i++) {
if (ulNotificationValue & (1 << i)) {
process_sensor_data(&g_iot_system.sensors[i]);
}
}
}
}
}
// 数据上传任务
void upload_task(void* param) {
uint32_t ulNotificationValue;
while (1) {
if (xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY)) {
if (ulNotificationValue & UPLOAD_TRIGGER) {
upload_all_sensor_data();
}
}
}
}
// 系统初始化
void iot_system_init(void) {
// 初始化传感器
int sample_intervals[] = {1000, 2000, 5000}; // 不同传感器的采样间隔
for (int i = 0; i < NUM_SENSORS; i++) {
g_iot_system.sensors[i].id = i;
g_iot_system.sensors[i].pin = SENSOR_PINS[i];
g_iot_system.sensors[i].data_buffer = malloc(DATA_BUFFER_SIZE * sizeof(float));
g_iot_system.sensors[i].buffer_index = 0;
g_iot_system.sensors[i].sample_interval_ms = sample_intervals[i];
// 创建采样定时器
char timer_name[20];
snprintf(timer_name, sizeof(timer_name), "Sensor%d", i);
g_iot_system.sensors[i].sample_timer = xTimerCreate(
timer_name,
pdMS_TO_TICKS(sample_intervals[i]),
pdTRUE, // 周期性
&g_iot_system.sensors[i],
sensor_sample_callback
);
xTimerStart(g_iot_system.sensors[i].sample_timer, 0);
}
// 创建上传定时器
g_iot_system.upload_timer = xTimerCreate(
"Upload",
pdMS_TO_TICKS(UPLOAD_INTERVAL_MS),
pdTRUE,
NULL,
data_upload_callback
);
xTimerStart(g_iot_system.upload_timer, 0);
// 创建状态检查定时器
g_iot_system.status_timer = xTimerCreate(
"Status",
pdMS_TO_TICKS(10000), // 每10秒检查一次
pdTRUE,
NULL,
system_status_callback
);
xTimerStart(g_iot_system.status_timer, 0);
// 创建看门狗定时器
g_iot_system.watchdog_timer = xTimerCreate(
"Watchdog",
pdMS_TO_TICKS(120000), // 2分钟超时
pdFALSE, // 一次性
NULL,
watchdog_callback
);
xTimerStart(g_iot_system.watchdog_timer, 0);
// 创建处理任务
xTaskCreate(data_process_task, "DataProcess", 2048, NULL, 5,
&g_iot_system.data_process_task);
xTaskCreate(upload_task, "Upload", 4096, NULL, 4,
&g_iot_system.upload_task);
g_iot_system.system_healthy = true;
}
这个综合示例展示了如何将任务通知和软件定时器结合使用,构建一个完整的IoT数据采集系统。系统包含了数据采集、处理、上传、状态监控和看门狗等功能,每个功能都通过定时器和任务通知进行协调。
总的来说,FreeRTOS的任务通知和软件定时器为嵌入式系统开发提供了强大而灵活的工具。任务通知提供了高效的任务间通信机制,而软件定时器则提供了灵活的定时任务管理。在ESP32和Arduino平台上,这两个特性得到了很好的支持和优化,能够满足各种复杂的应用需求。
掌握这两个特性,就像是掌握了嵌入式开发的"太极拳":任务通知如"推手",在任务间传递信息;软件定时器如"站桩",稳定地提供时间基准。两者结合,能够构建出既高效又稳定的嵌入式系统。
记住,好的嵌入式系统就像一台精密的手表,每个齿轮都恰到好处地发挥作用。通过合理使用任务通知和软件定时器,你也能打造出这样的"作品"!