FreeRTOS 手动移植教程(七):软件定时器 —— 不占硬件 Timer 的定时回调

在上一篇文章中,我们学习了互斥量,解决了共享资源保护与优先级反转问题。本篇将介绍 FreeRTOS 的软件定时器,它无需占用额外的硬件定时器资源,即可实现单次或周期性的定时回调。我们将通过实验演示如何创建、启动、停止软件定时器,并总结使用中的关键注意事项。


一、软件定时器与硬件定时器的区别

硬件定时器(如 TIM1、SysTick)直接由 MCU 外设实现,精度高、实时性强,但数量有限且配置相对复杂。

FreeRTOS 的软件定时器完全由内核通过一个守护任务(Timer Service Task)实现,它利用系统节拍来检查定时器是否超时,并执行对应的回调函数。其优点包括:

  • 不占用额外硬件资源;
  • 可以动态创建、删除;
  • 回调函数运行在守护任务的上下文,可以安全调用大多数 FreeRTOS API(除部分阻塞函数外)。

当然,软件定时器的精度受系统节拍频率限制(通常为 1ms),且回调执行时间不确定(受守护任务优先级影响),不适用于微秒级硬实时需求。


二、软件定时器的关键 API

功能 API 名称 说明
创建定时器 xTimerCreate 返回定时器句柄,需指定名称、周期、是否自动重载、ID、回调函数
启动定时器 xTimerStart 启动后开始计时
停止定时器 xTimerStop 停止计时,不触发回调
从 ISR 启动 xTimerStartFromISR 中断中使用的启动版本
从 ISR 停止 xTimerStopFromISR 中断中使用的停止版本
更改周期 xTimerChangePeriod 修改定时器的超时时间
获取定时器 ID pvTimerGetTimerID 返回创建时绑定的 ID 指针
删除定时器 xTimerDelete 释放定时器资源

使用软件定时器前,必须在 FreeRTOSConfig.h 中将 configUSE_TIMERS 设置为 1,并配置 configTIMER_TASK_PRIORITYconfigTIMER_QUEUE_LENGTH 等参数。这些在系列第一篇的配置文件中已经设置好,此处无需额外修改。


三、硬件准备与配置

本实验使用板载 PC13 LED,通过软件定时器实现不同频率的闪烁,无需额外硬件。BSP 文件沿用之前的 bsp_led.hbsp_led.c,包含 LED_InitAll()LED3_Toggle() 即可。

确保在 main() 开头调用了:

c 复制代码
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

四、实验一:单次定时器

创建一个只运行一次的软件定时器,延时 3 秒后翻转一次 LED,之后不再触发。

4.1 代码实现

c 复制代码
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "bsp_led.h"

TimerHandle_t xOneShotTimer = NULL;

/* 定时器回调函数 */
void vOneShotCallback(TimerHandle_t xTimer)
{
    LED3_Toggle();   // 3 秒后翻转一次 LED
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    LED_InitAll();

    /* 创建单次定时器:
       参数:名称、周期(3s)、自动重载(pdFALSE)、定时器 ID(0)、回调函数 */
    xOneShotTimer = xTimerCreate(
        "OneShot",
        pdMS_TO_TICKS(3000),
        pdFALSE,          // 单次模式,不自动重载
        (void *)0,
        vOneShotCallback
    );

    if (xOneShotTimer != NULL)
    {
        xTimerStart(xOneShotTimer, 0);  // 启动,超时时间 0 表示立刻开始计时
    }

    vTaskStartScheduler();
    while (1);
}

4.2 实验现象

上电后 LED 保持初始状态(灭),3 秒后翻转一次,之后不再变化。单次定时器适合实现"延时执行"的功能,比在任务中使用 vTaskDelay 更简洁,且不占用额外的任务堆栈。


五、实验二:周期定时器

创建一个自动重载的周期定时器,每 500ms 翻转一次 LED,实现稳定的 1Hz 闪烁。

c 复制代码
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "bsp_led.h"

TimerHandle_t xPeriodicTimer = NULL;

/* 定时器回调函数 */
void vPeriodicCallback(TimerHandle_t xTimer)
{
    LED3_Toggle();
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    LED_InitAll();

    /* 创建周期定时器:500ms 周期,自动重载 */
    xPeriodicTimer = xTimerCreate(
        "Periodic",
        pdMS_TO_TICKS(500),
        pdTRUE,          // 自动重载,周期触发
        (void *)0,
        vPeriodicCallback
    );

    if (xPeriodicTimer != NULL)
    {
        xTimerStart(xPeriodicTimer, 0);
    }

    vTaskStartScheduler();
    while (1);
}

实验现象:LED 以 1Hz 频率稳定闪烁。这个闪烁完全由软件定时器的回调函数驱动,主任务甚至可以是空的。


六、动态控制定时器:启动、停止与修改周期

软件定时器可在运行时被其他任务或中断动态控制。我们通过一个按键任务来模拟:按一次按键,启动定时器;再按一次,停止定时器。按键处理仍使用之前的中断加队列方案。

6.1 中断服务函数(位于 stm32f10x_it.c 中)

c 复制代码
#include "stm32f10x_it.h"
#include "FreeRTOS.h"
#include "queue.h"

extern QueueHandle_t xKeyQueue;

void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        uint8_t event = 1;
        xQueueSendFromISR(xKeyQueue, &event, &xHigherPriorityTaskWoken);
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

6.2 main.c 实现

c 复制代码
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "timers.h"
#include "bsp_led.h"
#include "bsp_exti.h"     // 包含 EXTI0_Init()

TimerHandle_t xControlledTimer = NULL;
QueueHandle_t xKeyQueue = NULL;

/* 定时器回调 */
void vTimerCallback(TimerHandle_t xTimer)
{
    LED3_Toggle();
}

/* 按键处理任务:根据按键状态切换定时器启停 */
void vKeyTask(void *pvParameters)
{
    uint8_t key_val;
    static uint8_t timer_running = 0;

    while (1)
    {
        if (xQueueReceive(xKeyQueue, &key_val, portMAX_DELAY) == pdTRUE)
        {
            if (!timer_running)
            {
                xTimerStart(xControlledTimer, 0);
                timer_running = 1;
            }
            else
            {
                xTimerStop(xControlledTimer, 0);
                timer_running = 0;
            }
        }
    }
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    LED_InitAll();
    EXTI0_Init();   // PA0 按键中断

    /* 创建队列,用于传递按键事件 */
    xKeyQueue = xQueueCreate(5, sizeof(uint8_t));
    if (xKeyQueue == NULL) while (1);

    /* 创建周期定时器,默认不启动(后续由任务启动) */
    xControlledTimer = xTimerCreate(
        "CtrlTimer",
        pdMS_TO_TICKS(200),
        pdTRUE,
        (void *)0,
        vTimerCallback
    );
    if (xControlledTimer == NULL) while (1);

    xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 1, NULL);

    vTaskStartScheduler();
    while (1);
}

实验现象

  • 上电后定时器未启动,LED 不闪烁;
  • 第一次按 PA0 按键,定时器启动,LED 以 2.5Hz(200ms 周期)闪烁;
  • 再次按键,定时器停止,LED 保持当前状态不变;
  • 后续每次按键交替切换启停状态。

七、软件定时器的注意事项

  1. 回调函数不要阻塞

    回调函数运行在 Timer Service Task 中,如果在该任务中调用 vTaskDelay 或等待信号量等阻塞操作,将导致整个定时器守护任务挂起,所有定时器都无法回调。务必保证回调函数快速执行完毕。

  2. 定时精度

    软件定时器的精度为一个系统节拍(通常 1ms),并且回调实际执行时间受守护任务优先级影响。如需微秒级精确控制,请使用硬件定时器。

  3. 回调函数中可以调用的 API

    大多数 FreeRTOS API 都可以在回调中使用,但需注意:不能调用可能导致守护任务阻塞的 API (如等待队列、信号量、互斥量等)。一般可以进行 xSemaphoreGivexQueueSend 等非阻塞操作来通知其他任务。

  4. 定时器的删除

    如果不确定定时器是否正在运行,删除前应调用 xTimerStop 确保其已停止。在回调函数中删除自己是不安全的行为,应避免。

  5. 与任务配合

    软件定时器非常适合触发动作后立即将具体处理交给任务。例如在回调中释放一个信号量或发送队列消息,由专门的任务负责长时间操作,这样既保证了定时精度,又不会阻塞守护任务。


八、总结

本篇学习了 FreeRTOS 软件定时器的使用:

  • 单次定时器用于延时执行;
  • 周期定时器可替代硬件定时器,实现周期性回调;
  • 定时器可被任务动态控制启停,灵活集成到应用逻辑中。

软件定时器为系统设计提供了极大的便利,在需要简单定时功能的场合应优先考虑。下一篇文章,我们将回到中断管理的话题,更系统地梳理 FreeRTOS 下中断优先级、临界区以及如何与任务高效协作。


下一篇:FreeRTOS 中断管理 ------ 优先级、临界区与任务通知。

相关推荐
原创小甜甜1 小时前
Windows 蓝屏自救手册:从紧急记录到硬件排查的完整指南
windows·stm32·单片机
tigershang2 小时前
华为“韬定律”:从“缩小尺寸”到“压缩时间”——后摩尔时代的规则重塑
单片机·华为·系统架构
项目題供诗2 小时前
STM32-TIM编码器接口(十六)
stm32·单片机·嵌入式硬件
都在酒里3 小时前
FreeRTOS 手动移植教程(八):中断管理 —— 优先级、临界区与任务通知
stm32·单片机·嵌入式·rtos·嵌入式软件
搁浅小泽3 小时前
电子器件常见的失效模式及对应的失效原因分析
单片机·嵌入式硬件
振南的单片机世界3 小时前
AFIO重映射:USART1_TX从PA9搬PB6,救活一版PCB
stm32·单片机·嵌入式硬件
不做无法实现的梦~3 小时前
Ubuntu 22.04 下使用 CMSIS-DAP 编译和烧录 STM32
linux·stm32·ubuntu
破晓单片机4 小时前
009、STM32单片机分享:智能窗帘系统
stm32·单片机·嵌入式硬件
清风6666664 小时前
基于单片机的智慧城市垃圾桶系统设计
单片机·毕业设计·智慧城市·课程设计·期末大作业