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例程

定义结构体变量

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 秒):无操作,仅初始化

  1. 0msmain 函数完成初始化,启动调度器。三个任务创建后立即进入阻塞态(因无资源触发)。
  2. 0ms:采集定时器(1 秒周期)和休眠定时器(5 秒一次性)启动,开始倒计时。

二、正常采集阶段(1 秒后,无按键操作):采集任务周期性运行

  1. 1000ms :采集定时器超时,触发回调函数 vCollectTimerCb

    • 读取模拟温湿度,向 xSensorQueue 发送数据(队列从空→有数据)。
    • 此时,采集任务因队列有数据被唤醒(从阻塞态→就绪态)。
  2. 1000ms~1010ms:采集任务运行:

    • 从队列取数据,调用 simLcdShow 显示(如 "显示:温度 = 25.1℃,湿度 = 59.9%")。
    • 调用 xTimerReset(xSleepTimer, 0) 重置休眠定时器(重新开始 5 秒倒计时,避免休眠)。
    • 显示完成后,采集任务再次阻塞等待队列(因队列已空)。
  3. 2000ms、3000ms...:重复步骤 1~2,每 1 秒触发一次采集→显示→阻塞,形成周期性运行。

    • 此阶段无其他任务干扰(按键任务和上传任务始终阻塞),CPU 主要被采集任务短时占用(显示操作)。

三、短按触发上传阶段(假设有按键操作,且按下时间 < 1 秒):高优先级任务抢占

假设在 3500ms 时发生短按(按下→松开,间隔 500ms):

  1. 3500ms :模拟按键按下(调用 simKeyPress):

    • 中断中设置 xKeyEvents 的 bit0(按下标志),按键任务因事件组有标志被唤醒(从阻塞态→就绪态)。
    • 因按键任务优先级(2)> 采集任务(1),若此时采集任务正在运行(如 3000ms 时的显示),会被按键任务抢占(采集任务暂停,按键任务立即执行)。
  2. 3500ms~3510ms:按键任务运行:

    • 检测到 bit0(按下),调用 xTimerStart(xLongPressTimer, 0) 启动长按定时器(1 秒一次性,开始倒计时)。
    • 处理完成后,按键任务再次阻塞等待事件组。
  3. 4000ms :模拟按键松开(调用 simKeyRelease):

    • 中断中设置 xKeyEvents 的 bit1(松开标志),按键任务再次被唤醒
  4. 4000ms~4010ms:按键任务运行:

    • 检测到 bit1(松开),调用 xTimerIsTimerActive(xLongPressTimer) 检查:此时长按定时器仅运行了 500ms(未超时),返回 pdTRUE
    • 调用 xTimerStop(xLongPressTimer, 0) 停止长按定时器(避免误判为长按)。
    • 调用 xSemaphoreGive(xUploadSem) 释放上传信号量,上传任务因信号量被唤醒(从阻塞态→就绪态)。
    • 调用 xTimerReset(xSleepTimer, 0) 重置休眠定时器(重新 5 秒倒计时)。
    • 处理完成后,按键任务再次阻塞。
  5. 4010ms~4030ms:上传任务运行(抢占按键任务):

    • 因上传任务优先级(3)> 按键任务(2),立即抢占 CPU。
    • 从队列取最新数据(如温度 25.4℃,湿度 59.6%),调用 simWifiSend 模拟上传。
    • 若上传成功(50% 概率):调用 xTimerStop(xUploadTimer, 0) 停止超时定时器,打印 "上传成功"。
    • 若上传失败:调用 xTimerStart(xUploadTimer, 0) 启动 3 秒超时定时器(准备重传)。
    • 调用 xTimerReset(xSleepTimer, 0) 重置休眠定时器。
    • 处理完成后,上传任务再次阻塞等待信号量。
  6. 4030ms 后:系统回到正常采集阶段,每 1 秒采集显示一次。

四、长按触发休眠阶段(假设有按键操作,且按下时间≥1 秒):任务被挂起

假设在 6000ms 时发生长按(按下后保持 1.5 秒再松开):

  1. 6000ms :按键按下,触发 simKeyPress→按键任务被唤醒,启动长按定时器(1 秒倒计时)。

  2. 7000ms :长按定时器超时(按下已 1 秒),触发回调 vLongPressTimerCb

    • 打印 "检测到长按→进入休眠",调用 vTaskSuspend 依次挂起采集任务、上传任务、按键任务。
    • 此时三个任务均进入挂起态 (停止运行,需 vTaskResume 恢复),系统 "休眠"。
  3. 7500ms :按键松开(调用 simKeyRelease),但因按键任务已被挂起,事件组标志无人处理,无任何反应。

五、无操作休眠阶段(5 秒内无任何操作):任务自动挂起

假设从 10000ms 开始,无采集(实际采集仍在进行,但采集属于 "系统操作"?不,采集是系统自动操作,会重置休眠定时器,这里假设极端情况:采集定时器被意外停止,仅举例):

  1. 10000ms:最后一次操作(如采集显示)完成,休眠定时器开始 5 秒倒计时。

  2. 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();
}
相关推荐
点灯小铭3 小时前
基于单片机的罐体压力控制器设计与实现
数据库·单片机·mongodb·毕业设计·课程设计
夜月yeyue3 小时前
多级流水线与指令预测
linux·网络·stm32·单片机·嵌入式硬件
张人玉12 小时前
C# TCP 服务端开发笔记(TcpListener/TcpClient)
stm32·单片机·嵌入式硬件
就叫飞六吧12 小时前
基于汇编实现led点灯-51单片机-stc89c52rc
嵌入式硬件·学习
宁静致远202112 小时前
FreeRTOS任务同步与通信--事件标志组
stm32·嵌入式·freertos
宁静致远202113 小时前
仿照STM32 HAL库设计思想使用FreeRTOS实现异步非阻塞式设备驱动
stm32·嵌入式硬件
田甲17 小时前
【STM32】墨水屏驱动开发
stm32·单片机·墨水屏
常州晟凯电子科技18 小时前
海思SS626开发笔记之环境搭建和SDK编译
人工智能·笔记·嵌入式硬件·物联网
智者知已应修善业18 小时前
【51单片机32个灯,第一次亮1,2。第二次亮2,3。第三次亮3,4。。。。】2023-2-10
c语言·经验分享·笔记·嵌入式硬件·51单片机