一、提要
在前面的章节,我已经介绍过了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例程
定义结构体变量
cpp
#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传数据的功能
创建四个软件定时器回调函数
cpp
// -------------------------- 定时器回调函数 --------------------------
// 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 :休眠定时器超时,触发回调
vSleepTimerCb
:打印 "5 秒无操作→自动休眠",挂起三个核心任务,系统进入休眠态。
cpp
// 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函数
cpp
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();
}