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();
}
相关推荐
沉在嵌入式的鱼2 小时前
linux串口对0X0D、0X0A等特殊字符的处理
linux·stm32·单片机·特殊字符·串口配置
学习路上_write2 小时前
AD5293驱动学习
c语言·单片机·嵌入式硬件·学习
影阴2 小时前
存储器和寄存器
stm32·单片机·嵌入式硬件
吃西瓜的年年3 小时前
3. C语言核心语法2
c语言·嵌入式硬件·改行学it
李洛克073 小时前
RDMA CM UDP 通信完整指南
单片机·网络协议·udp
思茂信息4 小时前
CST电动车EMC仿真——电机控制器MCU滤波仿真
javascript·单片机·嵌入式硬件·cst·电磁仿真
小曹要微笑4 小时前
I2C总线技术解析(纯文字版)
单片机·嵌入式硬件·esp32·iic
我送炭你添花4 小时前
可编程逻辑器件(PLD)的发展历程、原理、开发与应用详解
嵌入式硬件·fpga开发
袖手蹲4 小时前
Arduino UNO Q 从 Arduino Cloud 远程控制闪烁 LED
人工智能·单片机·嵌入式硬件·电脑
平凡灵感码头5 小时前
第一次做蓝牙产品,从零开发 嵌入式开发日志(2)AC63NSDK 完整合并版目录说明
stm32·单片机·嵌入式硬件