FreeRTOS_API模块综合应用篇(八)
一、提要
在前面的章节,我已经介绍过了FreeRTOS系统的队列、信号量、事件标志组、互斥锁、软件定时器等这几大API知识模块。
接下来,我将把这几大核心的API知识模块通过一个全面的、典型的应用场景,把他们全部串联联系起来,让大家能在自己的项目中更加协调熟练的使用这些API函数模块。
二、场景介绍
场景需求概述
功能1:周期性采集温湿度、光照数据(1 秒一次),并在 LCD 显示;
功能2:支持用户按键操作:短按(<1 秒)触发即时数据上传,长按(≥1 秒)触发低功耗模式;
功能3:数据上传到服务器时,需检测超时(3 秒未收到应答则重传,最多 3 次);
功能4:无任何操作(包括按键和数据上传)5 分钟后,自动进入休眠(关闭 LCD、暂停采集)。
我们可以设计一个"智能环境监测终端"的典型场景,同时也是为了后文的第四部分的“智能环境监测终端_FreeRTOS例程”的解释做铺垫。
三、智能环境监测终端_软件架构_文字说明
注:如果读者对互斥锁、软件定时器等这些API知识模块比较熟悉的话,也可以先跳到第四部分先粗看一遍代码,先对例程的结构有初步的认识,再看文字说明,效果会更好(以看代码为主,看文字为辅)。
3.1 整体架构
注:IPC机制其实就是队列、信号量、事件标志组等这些衔接任务之间通信的操作
3.2 功能1实现:周期性数据采集流程(队列 + 周期性定时器)
细节:
定时器用
xTimerCreate("采集定时器", pdMS_TO_TICKS(1000), pdTRUE, ...)
创建,回调中读取传感器数据(轻量操作),通过xQueueSend
将数据(温湿度、光照)写入队列;采集任务阻塞等待队列(
xQueueReceive
),收到数据后更新 LCD 显示(复杂操作放任务,避免阻塞定时器服务)
总结:创建周期性定时器,在定时器回调函数每隔1s读传感器数据,然后将数据发送给队列。采集任务接收到队列的数据之后,进行滤波、计算等复杂操作,然后进行LCD显示。
3.3 功能2实现:按键交互处理流程(信号量 + 事件组 + 一次性定时器)
细节:
按键按下时,中断服务函数通过
xEventGroupSetBitsFromISR
设置 “按键按下” 标志(bit0),交互任务(xEventGroupWaitBits
)检测到后启动 “长按检测定时器”(1 秒一次性);若 1 秒内按键松开(中断设 “按键松开” 标志 bit1),交互任务停止长按定时器,通过
xSemaphoreGive
释放 “即时上传” 信号量,上传任务被唤醒执行一次上传;若 1 秒内未松开,长按定时器回调触发 “低功耗模式”(通知系统任务)。
总结:创建一次性定时器,回调函数实现按键扫描(和定时器按键同理)。当按键短按(不超过1s时),回调函数触发短按标志位释放信号量,释放信号量之后交互任务获得信号量
3.4 功能3实现:数据上传超时重传流程(一次性定时器 + 互斥锁)
细节:
上传任务发送数据后,用
xTimerStart
启动 “3 秒超时定时器”;若收到服务器应答,上传任务调用
xTimerStop
停止定时器;若超时未应答,定时器回调通过互斥锁(
xSemaphoreTake
)保护重传计数,计数≤3 则触发重传(通知上传任务),否则记录失败日志。
3.5 功能4实现:无操作休眠控制流程(一次性定时器 + 任务挂起 / 恢复)
细节:
系统任务监听所有操作事件(采集、按键、上传),任一事件发生时调用
xTimerReset
重置 “5 分钟休眠定时器”(刷新计时);若 5 分钟无任何操作,定时器回调通过
vTaskSuspend
挂起采集、上传、交互任务,关闭外设进入低功耗;低功耗中按键按下(中断),系统任务调用
vTaskResume
恢复所有任务,重置休眠定时器。
四、 智能环境监测终端_FreeRTOS例程
定义结构体变量
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "event_groups.h"
#include "timers.h"// -------------------------- 全局句柄定义 --------------------------
// 队列:传递传感器数据(温度+湿度)
QueueHandle_t xSensorQueue;
// 信号量:触发数据上传
SemaphoreHandle_t xUploadSem;
// 事件标志组:按键状态(bit0=按下,bit1=松开)
EventGroupHandle_t xKeyEvents;
// 互斥锁:保护重传计数
SemaphoreHandle_t xRetryMutex;
// 定时器句柄
TimerHandle_t xCollectTimer; // 采集定时器(1秒周期)
TimerHandle_t xLongPressTimer; // 长按检测定时器(1秒一次性)
TimerHandle_t xUploadTimer; // 上传超时定时器(3秒一次性)
TimerHandle_t xSleepTimer; // 休眠定时器(5秒一次性)
// 任务句柄(用于休眠时挂起)
TaskHandle_t xCollectTaskHandle, xUploadTaskHandle, xKeyTaskHandle;int retryCnt = 0;//触发重传计数
// -------------------------- 模拟硬件函数 --------------------------
BaseType_t simWifiSend(float temp, float humi)// 模拟WiFi发送数据(随机返回是否成功)
{static int cnt = 0;cnt++;return (cnt % 2 == 0) ? pdTRUE : pdFALSE; // 模拟50%成功率
}
为了方便大家理解,我用简单的cnt计数来模拟wifi传数据的功能
创建四个软件定时器回调函数
// -------------------------- 定时器回调函数 --------------------------
// 1. 采集定时器回调(1s周期):读传感器→发队列
void vCollectTimerCb(TimerHandle_t xTimer)
{struct {float temp; float humi;}Sensor;simSensorRead(&Sensor.temp, &Sensor.humi);//模拟读取温湿度// 发送数据到队列(不阻塞,失败忽略)xQueueSend(xSensorQueue,&Sensor, 0);
}// 2. 长按检测定时器回调(1s一次性):触发长按逻辑
void vLongPressTimerCb(TimerHandle_t xTimer)
{printf("检测到长按→进入休眠\r\n");// 挂起核心任务vTaskSuspend(xCollectTaskHandle);vTaskSuspend(xUploadTaskHandle);vTaskSuspend(xKeyTaskHandle);
}// 3. 上传超时定时器回调(3s一次性):处理重传
void vUploadTimerCb(TimerHandle_t xTimer)
{xSemaphoreTake(xRetryMutex, portMAX_DELAY); // 保护重传计数(获取互斥锁)if (retryCnt < 3){retryCnt++;printf("上传超时→第%d次重传\r\n", retryCnt);xSemaphoreGive(xUploadSem); // 触发重传}else{retryCnt = 0;printf("重传次数耗尽→上传失败\r\n");}xSemaphoreGive(xRetryMutex);//释放互斥锁
}// 4. 休眠定时器回调(一次性):无操作超时休眠
void vSleepTimerCb(TimerHandle_t xTimer)
{printf("5秒无操作→自动休眠\r\n");// 挂起核心任务vTaskSuspend(xCollectTaskHandle);vTaskSuspend(xUploadTaskHandle);vTaskSuspend(xKeyTaskHandle);
}
在长按检测定时器回调中,由于后续的上传任务也使用到了retryCnt变量,所以我们需要使用互斥锁,来保护我们的共享资源retryCnt变量
创建三个任务
4.1 三个任务运行的时间线
前提:任务优先级与初始状态
- 优先级:上传任务(3)> 按键任务(2)> 采集任务(1)(高优先级任务可抢占低优先级任务)。
- 初始状态:系统启动后,三个任务均进入
while(1)
循环,但因等待资源(队列 / 信号量 / 事件组)而阻塞(不占用 CPU):- 采集任务:阻塞等待
xSensorQueue
有数据。 - 按键任务:阻塞等待
xKeyEvents
事件组有按键标志。 - 上传任务:阻塞等待
xUploadSem
信号量被释放。
- 采集任务:阻塞等待
一、系统启动初期(0~1 秒):无操作,仅初始化
- 0ms:
main
函数完成初始化,启动调度器。三个任务创建后立即进入阻塞态(因无资源触发)。 - 0ms:采集定时器(1 秒周期)和休眠定时器(5 秒一次性)启动,开始倒计时。
二、正常采集阶段(1 秒后,无按键操作):采集任务周期性运行
1000ms:采集定时器超时,触发回调函数
vCollectTimerCb
:- 读取模拟温湿度,向
xSensorQueue
发送数据(队列从空→有数据)。 - 此时,采集任务因队列有数据被唤醒(从阻塞态→就绪态)。
- 读取模拟温湿度,向
1000ms~1010ms:采集任务运行:
- 从队列取数据,调用
simLcdShow
显示(如 “显示:温度 = 25.1℃,湿度 = 59.9%”)。 - 调用
xTimerReset(xSleepTimer, 0)
重置休眠定时器(重新开始 5 秒倒计时,避免休眠)。 - 显示完成后,采集任务再次阻塞等待队列(因队列已空)。
- 从队列取数据,调用
2000ms、3000ms...:重复步骤 1~2,每 1 秒触发一次采集→显示→阻塞,形成周期性运行。
- 此阶段无其他任务干扰(按键任务和上传任务始终阻塞),CPU 主要被采集任务短时占用(显示操作)。
三、短按触发上传阶段(假设有按键操作,且按下时间 < 1 秒):高优先级任务抢占
假设在 3500ms 时发生短按(按下→松开,间隔 500ms):
3500ms:模拟按键按下(调用
simKeyPress
):- 中断中设置
xKeyEvents
的 bit0(按下标志),按键任务因事件组有标志被唤醒(从阻塞态→就绪态)。 - 因按键任务优先级(2)> 采集任务(1),若此时采集任务正在运行(如 3000ms 时的显示),会被按键任务抢占(采集任务暂停,按键任务立即执行)。
- 中断中设置
3500ms~3510ms:按键任务运行:
- 检测到 bit0(按下),调用
xTimerStart(xLongPressTimer, 0)
启动长按定时器(1 秒一次性,开始倒计时)。 - 处理完成后,按键任务再次阻塞等待事件组。
- 检测到 bit0(按下),调用
4000ms:模拟按键松开(调用
simKeyRelease
):- 中断中设置
xKeyEvents
的 bit1(松开标志),按键任务再次被唤醒。
- 中断中设置
4000ms~4010ms:按键任务运行:
- 检测到 bit1(松开),调用
xTimerIsTimerActive(xLongPressTimer)
检查:此时长按定时器仅运行了 500ms(未超时),返回pdTRUE
。 - 调用
xTimerStop(xLongPressTimer, 0)
停止长按定时器(避免误判为长按)。 - 调用
xSemaphoreGive(xUploadSem)
释放上传信号量,上传任务因信号量被唤醒(从阻塞态→就绪态)。 - 调用
xTimerReset(xSleepTimer, 0)
重置休眠定时器(重新 5 秒倒计时)。 - 处理完成后,按键任务再次阻塞。
- 检测到 bit1(松开),调用
4010ms~4030ms:上传任务运行(抢占按键任务):
- 因上传任务优先级(3)> 按键任务(2),立即抢占 CPU。
- 从队列取最新数据(如温度 25.4℃,湿度 59.6%),调用
simWifiSend
模拟上传。 - 若上传成功(50% 概率):调用
xTimerStop(xUploadTimer, 0)
停止超时定时器,打印 “上传成功”。 - 若上传失败:调用
xTimerStart(xUploadTimer, 0)
启动 3 秒超时定时器(准备重传)。 - 调用
xTimerReset(xSleepTimer, 0)
重置休眠定时器。 - 处理完成后,上传任务再次阻塞等待信号量。
4030ms 后:系统回到正常采集阶段,每 1 秒采集显示一次。
四、长按触发休眠阶段(假设有按键操作,且按下时间≥1 秒):任务被挂起
假设在 6000ms 时发生长按(按下后保持 1.5 秒再松开):
6000ms:按键按下,触发
simKeyPress
→按键任务被唤醒,启动长按定时器(1 秒倒计时)。7000ms:长按定时器超时(按下已 1 秒),触发回调
vLongPressTimerCb
:- 打印 “检测到长按→进入休眠”,调用
vTaskSuspend
依次挂起采集任务、上传任务、按键任务。 - 此时三个任务均进入挂起态(停止运行,需
vTaskResume
恢复),系统 “休眠”。
- 打印 “检测到长按→进入休眠”,调用
7500ms:按键松开(调用
simKeyRelease
),但因按键任务已被挂起,事件组标志无人处理,无任何反应。
五、无操作休眠阶段(5 秒内无任何操作):任务自动挂起
假设从 10000ms 开始,无采集(实际采集仍在进行,但采集属于 “系统操作”?不,采集是系统自动操作,会重置休眠定时器,这里假设极端情况:采集定时器被意外停止,仅举例):
10000ms:最后一次操作(如采集显示)完成,休眠定时器开始 5 秒倒计时。
15000ms:休眠定时器超时,触发回调
打印 “5 秒无操作→自动休眠”,挂起三个核心任务,系统进入休眠态。vSleepTimerCb
:
// 1. 采集任务:从队列取数据→显示
void vCollectTask(void *pvParam)
{struct {float t; float h;} data;while (1){//阻塞等待队列数据xQueueReceive(xSensorQueue, &data, portMAX_DELAY);LCDShow(data.t, data.h);//用LCD显示屏显示温度,湿度数据// 有操作→重置休眠定时器xTimerReset(xSleepTimer, 0);}
}// 2. 交互任务:处理按键事件(短按/长按)
void vKeyTask(void *pvParam)
{EventBits_t xBits;while (1){// 等待按键按下(bit0)或松开(bit1)xBits = xEventGroupWaitBits(xKeyEvents,(1 << 0) | (1 << 1), // 等待的标志位pdTRUE, // 清除标志位pdFALSE, // 任意标志满足portMAX_DELAY);if (xBits & (1 << 0)) // 按键按下{printf("按键按下→启动长按检测\r\n");xTimerStart(xLongPressTimer, 0); // 启动1秒定时器}else if (xBits & (1 << 1)) // 按键松开{printf("按键松开→停止长按检测\r\n");// 若长按定时器未触发(1秒内松开)→ 短按if (xTimerIsTimerActive(xLongPressTimer))//判断是否超时{// 若1秒内松开→短按,触发上传xTimerStop(xLongPressTimer, 0);// 停止定时器xSemaphoreGive(xUploadSem); // 给信号量:触发上传任务}// 有操作→重置休眠定时器xTimerReset(xSleepTimer, 0);}}
}// 3. 上传任务:等待上传信号→发送数据→处理超时
void vUploadTask(void *pvParam)
{struct {float t; float h;} data;while (1){// 等待上传信号(短按或重传触发)xSemaphoreTake(xUploadSem, portMAX_DELAY);// 从队列取最新数据(非阻塞,取最近的)xQueueReceive(xSensorQueue, &data, 0);printf("开始上传:温度=%.1f,湿度=%.1f\r\n", data.t, data.h);if (simWifiSend(data.t, data.h)){// 发送成功→停止超时定时器xTimerStop(xUploadTimer, 0);printf("上传成功\r\n");//上传成功→重置重传计数xSemaphoreTake(xRetryMutex, portMAX_DELAY);//获取互斥锁retryCnt = 0; xSemaphoreGive(xRetryMutex);//释放互斥锁}else{// 发送失败→启动超时定时器(3秒后重传)xTimerStart(xUploadTimer, 0);}// 有操作→重置休眠定时器xTimerReset(xSleepTimer, 0);}
}
4.2 if (xTimerIsTimerActive(xLongPressTimer))的运行逻辑
我重点说说交互任务中的if (xTimerIsTimerActive(xLongPressTimer))
LongPressTimer
是一个 1 秒一次性定时器(用于检测按键长按),xTimerIsTimerActive(xLongPressTimer)
的作用是 区分 “短按” 和 “长按”:
当按键松开时,调用
xTimerIsTimerActive(xLongPressTimer)
检查:若返回
pdTRUE
:说明定时器仍在活动(1 秒倒计时未结束)→ 按键按下时间 <1 秒 → 判定为 “短按”,触发数据上传。若返回
pdFALSE
:说明定时器已超时(1 秒倒计时已结束,且回调已执行)→ 按键按下时间 ≥ 1 秒 → 判定为 “长按”,已触发休眠(无需额外处理)。
main函数
int main(void)
{// 1. 创建IPC组件:队列、信号量、事件组、互斥锁(无依赖,先创建)xSensorQueue = xQueueCreate(5, sizeof(struct {float t; float h;}));// 队列大小5,元素大小为结构体大小xUploadSem = xSemaphoreCreateBinary();// 创建二值信号量(初始空)xKeyEvents = xEventGroupCreate();// 创建事件组xRetryMutex = xSemaphoreCreateMutex();// 创建互斥锁// 2. 创建定时器:指定名称、周期、类型、参数、回调函数xCollectTimer = xTimerCreate("CollectTimer", pdMS_TO_TICKS(1000), pdTRUE, NULL, vCollectTimerCb);xLongPressTimer = xTimerCreate("LongPressTimer", pdMS_TO_TICKS(1000), pdFALSE, NULL, vLongPressTimerCb);xUploadTimer = xTimerCreate("UploadTimer", pdMS_TO_TICKS(3000), pdFALSE, NULL, vUploadTimerCb);xSleepTimer = xTimerCreate("SleepTimer", pdMS_TO_TICKS(5000), pdFALSE, NULL, vSleepTimerCb);// 3. 创建任务:指定任务函数、名称、栈大小(128字,视系统调整)、参数、优先级、任务句柄xTaskCreate(vCollectTask, "CollectTask", 128, NULL, 1, &xCollectTaskHandle);xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 2, &xKeyTaskHandle);xTaskCreate(vUploadTask, "UploadTask", 128, NULL, 3, &xUploadTaskHandle);// 4. 启动定时器:采集定时器(1秒周期)和休眠定时器(5秒无操作)先启动xTimerStart(xCollectTimer, 0);xTimerStart(xSleepTimer, 0); // 启动无操作计时// 启动调度器vTaskStartScheduler();
}