FreeRTOS定时器

发布于:2025-06-15 ⋅ 阅读:(16) ⋅ 点赞:(0)

img

1.特性

定时器周期(Period):

  • 定时器启动后,会在指定的时间间隔到达时触发回调函数。
  • 这个时间间隔称为定时器的周期,即从定时器启动到回调函数执行的时间长度。

软件定时器有两种状态:

  • 运行(Running 或 Active)

    • 当定时器处于运行状态时,它的计时器正在倒计时,到期后会自动执行回调函数。
    • 对于自动加载定时器,每次回调执行完后,它会自动重启,继续保持运行状态。
  • 冬眠(Dormant)

    • 冬眠态的定时器虽然仍可以通过句柄访问,但它不会触发回调函数。
    • 一次性定时器在回调执行后会进入冬眠状态,直到手动再次启动它。

定时器类型:
定时器主要有两种类型,每种类型的触发方式不同:

  • 一次性定时器(One-shot timers):

    • 启动后,经过一个周期后仅触发一次回调函数。
    • 如果需要再次触发,则必须手动重新启动定时器。
    • 这种类型适合只需要执行一次任务的场景,如延迟操作、超时处理等。
  • 自动加载定时器(Auto-reload timers):

    • 启动后,在每个周期结束时自动重启,因此回调函数会被周期性地调用。
    • 不需要手动重新启动,适用于周期性任务,如定时采样、周期性状态更新等。

回调函数:

  • 定时器启动时,必须指定一个回调函数,这个函数会在定时器到期时被调用。
  • 回调函数中可以执行需要定时执行的操作,就像手机闹钟响后执行特定任务一样。

手册中的例子:

img

2.运行环境

2.1 守护任务

直觉上可能认为定时器在 Tick 中断中判断是否超时,并直接调用回调函数。但这样做会带来两个问题:

  1. 中断上下文限制:在 Tick 中断中执行回调可能导致长时间阻塞,影响系统响应,甚至引起系统延迟。
  2. 不可预知的代码执行:内核中断上下文中执行的代码必须非常短小,不允许调用阻塞API(如 vTaskDelay()),否则会破坏实时性。

为了避免这些问题,FreeRTOS 将定时器的回调函数放在一个专门的任务中执行,这个任务就是RTOS 守护任务(Timer Daemon Task)

  • 它不是在 Tick 中断里运行,而是在任务上下文中运行,因此可以执行较复杂的回调函数。
  • 通过这种设计,定时器回调函数不影响系统中断响应,同时能调用大部分 API(但仍应避免阻塞操作)。

当配置项 configUSE_TIMERS 设置为 1 时,启动调度器时系统会自动创建守护任务。

  1. 处理定时器命令:守护任务从“定时器命令队列”中取出命令(例如启动、停止定时器命令),并执行相应操作。
  2. 执行定时器回调函数:当定时器超时时,守护任务调用对应的回调函数。

守护任务的优先级由 configTIMER_TASK_PRIORITY 定义;定时器命令队列的长度由 configTIMER_QUEUE_LENGTH 定义。
守护任务的调度与其他任务相同——只有当它是就绪态中优先级最高的任务时才会运行。

因此定时器的回调函数能否被及时处理取决于守护任务什么时候能够被执行,优先级最高能够抢占的话,就能较快处理回调函数。

守护任务优先级较低:

img

  • t1:Task1(优先级较高)正在运行,而守护任务处于阻塞状态(等待命令或定时器超时)。
  • t2:Task1调用 xTimerStart(),这个调用仅仅把一个“启动定时器”的命令发送到定时器命令队列。由于 Task1优先级较高,守护任务(优先级低)不能马上抢占 CPU。
  • t3:Task1执行完 xTimerStart() 后继续运行,此时定时器命令仍滞留在命令队列中。
  • t4:当 Task1因某些原因进入阻塞态后,守护任务获得 CPU,开始从命令队列中取出命令并启动定时器。
  • t5:守护任务处理完所有命令后再次进入阻塞态,轮到其他任务或 Idle 任务执行。

**守护任务优先级较高:

**img

  • t1:Task1 正在运行,守护任务处于阻塞状态。
  • t2:Task1 调用 xTimerStart(),将命令发送到定时器命令队列。由于守护任务优先级较高,它立即抢占 CPU,从命令队列中取出命令开始启动定时器。
  • t3:在 Task1 调用 xTimerStart() 的过程中被守护任务抢占,Task1暂时暂停。
  • t4:守护任务处理完命令后,Task1继续执行 xTimerStart() 的剩余部分,并返回。
  • t5:此后,定时器超时时间是从 Task1 调用 xTimerStart() 时开始计算的,不受守护任务处理命令延迟的影响。

2.2 回调函数

关于定时器的回调函数也有一些要求:

  • 回调函数必须快速完成,不应包含长时间运行或阻塞的操作。
  • 避免调用可能阻塞的 API(例如 vTaskDelay());如果必须调用诸如 xQueueReceive() 之类的 API,超时时间应设置为 0,即刻返回。
  • 回调函数不应影响守护任务的整体响应,确保其他定时器命令也能及时处理。

2.3 内部源码

来看看守护在内部是如何被创建的,找到timers.c:

BaseType_t xTimerCreateTimerTask( void )
{
    BaseType_t xReturn = pdFAIL;   /* 初始化返回值,默认返回失败 */

    /* 此函数在调度器启动时被调用,前提是 configUSE_TIMERS 被设置为 1。
     * 先检查定时器服务任务所依赖的基础设施(比如定时器命令队列和定时器列表)
     * 是否已经创建或初始化。若已经创建,则无需重复初始化。 */
    prvCheckForValidListAndQueue();

    /* 如果定时器命令队列已经被创建,则可以创建定时器任务 */
    if( xTimerQueue != NULL )
    {
        #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
            {
                StaticTask_t * pxTimerTaskTCBBuffer = NULL;    /* 用于存储任务控制块 (TCB) 的静态内存 */
                StackType_t * pxTimerTaskStackBuffer = NULL;     /* 用于存储任务栈的静态内存 */
                uint32_t ulTimerTaskStackSize;                   /* 定时器任务的栈大小 */

                /* 调用应用程序提供的回调函数获取定时器任务的静态内存。
                 * 该函数会把任务的 TCB 和栈内存地址,以及栈大小返回给应用程序。 */
                vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );

                /* 使用静态内存创建定时器任务:
                 * prvTimerTask 为任务函数,也就是定时器守护任务的函数
                 * configTIMER_SERVICE_TASK_NAME 为任务名称,
                 * ulTimerTaskStackSize 为任务栈深度,
                 * NULL 为任务参数,
                 * (configTIMER_TASK_PRIORITY | portPRIVILEGE_BIT) 为任务优先级(可能加上特权位),
                 * pxTimerTaskStackBuffer 和 pxTimerTaskTCBBuffer 为预先分配的静态内存。 */
                xTimerTaskHandle = xTaskCreateStatic( prvTimerTask,
                                                      configTIMER_SERVICE_TASK_NAME,
                                                      ulTimerTaskStackSize,
                                                      NULL,
                                                      ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                                                      pxTimerTaskStackBuffer,
                                                      pxTimerTaskTCBBuffer );

                /* 如果任务句柄不为空,说明定时器任务创建成功 */
                if( xTimerTaskHandle != NULL )
                {
                    xReturn = pdPASS;
                }
            }
        #else /* 如果不支持静态内存分配,则使用动态分配 */
            {
                /* 使用 xTaskCreate 动态创建定时器任务:
                 * prvTimerTask 为任务函数,
                 * configTIMER_SERVICE_TASK_NAME 为任务名称,
                 * configTIMER_TASK_STACK_DEPTH 为任务栈深度,
                 * NULL 为任务参数,
                 * (configTIMER_TASK_PRIORITY | portPRIVILEGE_BIT) 为任务优先级,
                 * &xTimerTaskHandle 存储创建后返回的任务句柄。 */
                xReturn = xTaskCreate( prvTimerTask,
                                       configTIMER_SERVICE_TASK_NAME,
                                       configTIMER_TASK_STACK_DEPTH,
                                       NULL,
                                       ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                                       &xTimerTaskHandle );
            }
        #endif /* configSUPPORT_STATIC_ALLOCATION */

    }
    else
    {
        /* 如果定时器命令队列没有创建成功,则触发覆盖测试标记(用于测试覆盖率) */
        mtCOVERAGE_TEST_MARKER();
    }

    /* 断言 xReturn 必须为 pdPASS,否则程序运行时将报错 */
    configASSERT( xReturn );

    /* 返回任务创建结果:pdPASS 表示成功,pdFAIL 表示失败 */
    return xReturn;
}

这段代码就是实现了创建定时器服务任务(Timer Service Task,也就是定时器守护任务)的功能,其中关键的任务函数就是:prvTimerTask,继续查找一下这个函数,发现是并没有这个函数的,但是找到了:

static portTASK_FUNCTION( prvTimerTask, pvParameters )

prvTimerTask作为了一个参数,又查找了一下portTASK_FUNCTION,发现在portmacro.h中定义了一个这样的宏:

#define portTASK_FUNCTION( vFunction, pvParameters )          void vFunction( void * pvParameters )

# 宏名称:portTASK_FUNCTION
# vFunction:任务函数的名称。
# pvParameters:任务函数的参数名称(通常是一个 void* 指针)。

其实就是freertos用于规范任务函数的声明格式,确保代码的可移植性和一致性,这样看来static portTASK_FUNCTION( prvTimerTask, pvParameters )实际上就是等同于:void prvTimerTask(void * pvParameters ),也就说static portTASK_FUNCTION( prvTimerTask, pvParameters )定义的就是守护任务的内容,来看看其定义;

static portTASK_FUNCTION( prvTimerTask, pvParameters )
{
    TickType_t xNextExpireTime;      /* 变量:存储下一个将到期的定时器的绝对到期时间 */
    BaseType_t xListWasEmpty;        /* 变量:标记定时器列表是否为空。若为空,可能无需立即唤醒任务 */

    /* 这里将 pvParameters 转换为 void,以避免编译器关于未使用参数的警告 */
    ( void ) pvParameters;

    #if ( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
        {
            extern void vApplicationDaemonTaskStartupHook( void );

            /* 如果配置了守护任务启动钩子(Daemon Task Startup Hook),则在定时器任务开始运行时,
             * 允许应用程序在任务上下文中执行一些初始化代码。这对于需要在调度器启动后初始化的
             * 应用代码非常有用。 */
            vApplicationDaemonTaskStartupHook();
        }
    #endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */

    /* 无限循环,定时器任务一直运行 */
    for( ; ; )
    {
        /* 第一步:查询当前定时器列表是否存在定时器,若存在,则获得下一个即将到期的定时器的到期时间。
         * 函数 prvGetNextExpireTime() 会检查定时器列表,并返回一个 Tick 值,该值表示下一个定时器到期的时刻。
         * 同时,它会通过 xListWasEmpty 参数告知调用者:如果定时器列表为空,则无定时器等待处理。 */
        xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );

        /* 第二步:调用 prvProcessTimerOrBlockTask() 函数
         * 作用:判断是否有定时器已经超时,或者需要阻塞等待定时器超时或有命令到达。
         * 参数 xNextExpireTime 表示下一个定时器到期的时刻,
         * xListWasEmpty 表示定时器列表是否为空。
         *
         * 具体:
         * - 如果有定时器到期,则该函数会调用对应的回调函数处理定时器超时事件。
         * - 如果没有定时器到期,则定时器任务会进入阻塞状态,等待到达指定时间或等待命令队列中有新命令。
         */
        prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );

        /* 第三步:处理定时器命令队列中的所有命令。
         * 定时器命令队列可能包含启动、停止、重载定时器等命令,
         * 它们都是由其他任务通过定时器 API 发来的。
         * 函数 prvProcessReceivedCommands() 会遍历并执行这些命令,确保定时器状态与应用请求保持同步。
         */
        prvProcessReceivedCommands();
    }
}

这玩意就是FreeRTOS 定时器守护任务的核心函数。无非三大功能,一直循环工作:

  1. 检查当前定时器列表,找出下一个即将到期的定时器。
  2. 根据是否有定时器到期,决定立即处理定时器回调,或者阻塞等待。
  3. 处理定时器命令队列中发来的命令(如启动、停止定时器等)。

3.和Linux对比

在 Linux 中,定时器通常在内核中以软中断(softirq)或专门的定时器线程的形式运行。不同点:

  • 执行上下文

    • Linux 定时器:通常在内核上下文中运行,可能由软中断调度;定时器回调可能会影响整个系统的中断延迟。
    • FreeRTOS 定时器:回调函数在 RTOS 守护任务中运行,不在中断上下文中执行,这样可以避免长时间中断阻塞,提高系统实时性。
  • 调度策略

    • Linux:定时器任务优先级由内核调度策略决定,有时会有较高的调度权重,且可以与用户态任务共享 CPU。
    • FreeRTOS:定时器守护任务的优先级由配置参数 configTIMER_TASK_PRIORITY 定义,开发者需合理设置以确保定时器命令及时处理,同时不干扰其他关键任务。
  • 资源占用

    • Linux 定时器:通常支持高精度计时和复杂的定时器功能,但实现较为复杂;使用内核对象管理。
    • FreeRTOS 定时器:设计更简单、轻量,适用于嵌入式系统。利用定时器命令队列和守护任务解耦定时器回调执行,降低了中断负担。

4.ID

在生产过程中,肯定会不止只有一个定时器,会创建多个定时器,那么这些定时器是如何去标记好区分的?看下一定时器的结构体就行了,其结构成员pvTimerID:

/* 定时器控制块结构体(原名称tmrTimerControl用于兼容内核感知调试工具) */
typedef struct tmrTimerControl
{
    /* 定时器名称(字符串指针) 
     * [功能] 用于调试时标识定时器,内核本身不使用此字段。
     * [注意] 允许使用未限定的char类型(根据Lint规则仅允许字符串和单字符使用) 
     * [示例] 可设置为"LED_Blink_Timer"等有意义的名称 */
    const char * pcTimerName;

    /* 定时器链表项(数据结构)
     * [功能] 内核事件管理的标准链表项,用于将定时器插入定时器队列
     * [关联] 与调度器中的xTimerList链表配合使用
     * [操作] 通过vListInsert()等链表API管理 */
    ListItem_t xTimerListItem;

    /* 定时器周期(以Tick为单位)
     * [功能] 定义定时器的触发间隔时间
     * [单位] 1 Tick = 1 / configTICK_RATE_HZ 秒
     * [注意] 对于一次性定时器表示首次触发间隔,对于自动重载定时器表示周期间隔
     * [示例] 若设为100,表示100个Tick后触发 */
    TickType_t xTimerPeriodInTicks;

    /* 定时器标识符(通用指针)
     * [功能] 唯一标识定时器实例,用于同一回调函数处理多个定时器的场景
     * [用法] 在回调函数中通过pvTimerID区分不同定时器
     * [注意] 建议使用结构体指针或哈希值等具有唯一性的标识 */
    void * pvTimerID;

    /* 定时器回调函数指针
     * [功能] 定时器到期时执行的函数
     * [类型] 函数原型:void CallbackFunc(TimerHandle_t xTimer)
     * [注意] 回调函数应避免执行阻塞操作 */
    TimerCallbackFunction_t pxCallbackFunction;

    #if ( configUSE_TRACE_FACILITY == 1 )
        /* 跟踪工具分配的定时器编号
         * [功能] 用于FreeRTOS+Trace等调试工具追踪定时器行为
         * [生成] 由内核在创建定时器时自动分配
         * [查看] 可通过uxTimerGetTimerNumber() API获取 */
        UBaseType_t uxTimerNumber;
    #endif

    /* 定时器状态标志位(8位无符号整型)
     * [位域定义]:
     *   bit0 - 定时器活动状态(1:运行中,0:休眠)
     *   bit1 - 内存分配方式(1:静态分配,0:动态分配)
     *   bit2~7 - 保留位
     * [操作] 通过宏pdTRUE/pdFALSE设置状态
     * [注意] 直接修改此字段可能导致状态不一致,建议使用API管理 */
    uint8_t ucStatus;
} xTIMER;

更新ID:使用 vTimerSetTimerID() 函数

查询ID:查询 pvTimerGetTimerID() 函数

5.数据传输

在 2.1 中说过:当配置项 configUSE_TIMERS 设置为 1 时,启动调度器时系统会自动创建守护任务。来看看如果定义了,会设置什么内容,在FreeRTOS.h中:

#if configUSE_TIMERS == 1
    /* 
     * 定时器服务任务优先级检查 (configTIMER_TASK_PRIORITY)
     * 定义定时器守护任务(Timer Service Task)的调度优先级
     * 0~(configMAX_PRIORITIES-1),通常建议设置为中等优先级
     * 未定义时会导致定时器命令无法被及时处理
     * #define configTIMER_TASK_PRIORITY 3
     */
    #ifndef configTIMER_TASK_PRIORITY
        #error 启用定时器功能(configUSE_TIMERS=1)时,必须定义configTIMER_TASK_PRIORITY 
    #endif /* configTIMER_TASK_PRIORITY */

    /* 
     * 定时器命令队列长度检查 (configTIMER_QUEUE_LENGTH)
     * 定义定时器命令队列的最大容量,影响同时处理的定时器操作数量
     * ≥5,高负载场景建议10以上
     * 队列过小可能导致xTimerStart等操作失败(返回pdFAIL)
     * 需求队列长度 = 并发定时器操作峰值数 + 安全余量
     */
    #ifndef configTIMER_QUEUE_LENGTH
        #error 启用定时器功能(configUSE_TIMERS=1)时,必须定义configTIMER_QUEUE_LENGTH
    #endif /* configTIMER_QUEUE_LENGTH */

    /* 
     * 定时器任务栈深度检查 (configTIMER_TASK_STACK_DEPTH)
     * 定义定时器守护任务的栈空间大小(以字为单位)
     *  根据架构不同,建议≥100(如STM32可设为128)
     * 栈溢出会导致系统崩溃,建议通过uxTaskGetStackHighWaterMark()监控
     * 需考虑回调函数的最大栈消耗 + 系统安全余量
     */
    #ifndef configTIMER_TASK_STACK_DEPTH
        #error 启用定时器功能(configUSE_TIMERS=1)时,必须定义configTIMER_TASK_STACK_DEPTH
    #endif /* configTIMER_TASK_STACK_DEPTH */

#endif /* configUSE_TIMERS */

可以看出还必须定义另外三个宏,否则使用定时器时是会出错的,其中优先级、栈深度的宏定义是可以理解,毕竟定时器的守护任务是需要指定栈深度以及优先级的,但是为什么还要还要定义队列的最大容量呢?

  • FreeRTOS 的软件定时器功能通过 守护任务(Timer Service Task) 实现,而该守护任务与其他任务/中断之间的通信完全依赖 定时器命令队列。队列的作用可归纳为:

    • 命令中转站:所有定时器操作(如启动、停止、重置)均通过发送命令到队列,由守护任务异步处理。
    • 线程安全:避免多任务同时操作定时器数据结构导致的竞态条件。
    • 优先级解耦:允许低优先级任务发送命令,由高优先级的守护任务及时处理。
  • 简单点就是,用户程序这边调用了定时器的相关操作函数,比如启动/删除/停止定时器等,同时通过队列来传输到内核的

img

6.操作函数

img

  • 创建与删除定时器
    使用 xTimerCreate / xTimerCreateStatic 创建定时器;使用 xTimerDelete 删除定时器(仅适用于动态创建)。
  • 启动/停止/复位定时器
    使用 xTimerStart / xTimerStartFromISR 启动定时器;使用 xTimerStop / xTimerStopFromISR 停止定时器;使用 xTimerReset / xTimerResetFromISR 复位定时器,使定时器重新开始计时。
  • 修改定时器周期
    使用 xTimerChangePeriod / xTimerChangePeriodFromISR 动态修改定时器的周期,新周期的到期时间从调用时刻开始计算。

6.1 创建

动态创建定时器:

TimerHandle_t xTimerCreate( const char * const pcTimerName,
                            const TickType_t xTimerPeriodInTicks,
                            const UBaseType_t uxAutoReload,
                            void * const pvTimerID,
                            TimerCallbackFunction_t pxCallbackFunction );
  • pcTimerName:定时器名称,仅用于调试时识别,不影响功能。

  • xTimerPeriodInTicks:定时器周期,单位是 Tick。定时器启动后,在经过该 Tick 数后会触发回调函数。

  • uxAutoReload:定时器类型。

    • pdTRUE 表示自动加载定时器(周期性定时器),在每个周期结束后自动重启。
    • pdFALSE 表示一次性定时器,触发回调函数后进入冬眠状态,需要手动重新启动。
  • pvTimerID:定时器 ID,供回调函数使用,以便识别定时器来源或关联其他数据。

  • pxCallbackFunction:回调函数,当定时器超时时调用。回调函数的原型为:

typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );
  • 返回值:

    • 成功则返回定时器句柄(TimerHandle_t);如果内存分配失败则返回 NULL。

静态创建定时器:

TimerHandle_t xTimerCreateStatic( const char * const pcTimerName,
                                  const TickType_t xTimerPeriodInTicks,
                                  const UBaseType_t uxAutoReload,
                                  void * const pvTimerID,
                                  TimerCallbackFunction_t pxCallbackFunction,
                                  StaticTimer_t *pxTimerBuffer );
  • 除了与动态创建相同的参数外,多了:

    • pxTimerBuffer:指向一个 StaticTimer_t 类型的结构体内存,该内存由用户提前分配,用于保存定时器数据结构。
  • 返回值:成功返回定时器句柄,失败返回 NULL。

6.2 删除

对于动态创建的定时器,当不再需要时可以调用删除函数以回收内存。

BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
  • xTimer:要删除的定时器句柄。
  • xTicksToWait:写入删除命令到定时器命令队列的超时时间(Tick 数)。如果队列满,调用者可以等待一段时间(如使用 pdMS_TO_TICKS() 转换后的 Tick 数);若在规定时间内命令无法写入,则返回失败。

返回值:

  • pdPASS 表示命令成功写入队列,定时器删除命令已发出;
  • pdFAIL 表示在 xTicksToWait 内无法写入删除命令到队列。

6.3 启动

启动定时器就是使其状态变为“运行态

任务中启动定时器:

BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
  • xTimer:要启动的定时器句柄。

  • xTicksToWait:写入“启动定时器”命令到命令队列的超时时间。注意,此参数并非定时器的周期,而是命令写入队列时等待的时间。

  • 返回值:

    • pdPASS 表示启动命令成功写入命令队列;
    • pdFAIL 表示在 xTicksToWait Tick 内无法写入启动命令。

ISR 中启动定时器:

BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
  • xTimer:定时器句柄。

  • pxHigherPriorityTaskWoken:指向变量的指针,如果该操作使得定时器守护任务(Timer Daemon Task)从阻塞中唤醒,并且其优先级高于当前任务,则该变量被置为 pdTRUE,指示中断退出后进行任务切换。

  • 返回值:

    • pdPASS 表示成功写入启动命令;
    • pdFAIL 表示写入命令失败。

6.4 停止

停止则使其进入“冬眠态”。

任务中停止定时器:

BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
  • xTimer:定时器句柄。

  • xTicksToWait:写入“停止定时器”命令的超时时间。

  • 返回值:

    • pdPASS 表示成功写入停止命令;
    • pdFAIL 表示在规定时间内无法写入命令。

ISR 中停止定时器:

BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
  • 与启动定时器的 ISR 版本类似,多了 pxHigherPriorityTaskWoken 参数。

6.5 复位(重置)

复位定时器会将其超时时间重新设定,使其从当前时刻开始计算周期。复位可以看作是重新启动定时器。

任务中复位定时器:

BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
  • xTimer:要复位的定时器句柄。

  • xTicksToWait:写入复位命令到命令队列的超时时间。

  • 返回值:

    • pdPASS 表示复位命令成功写入队列;
    • pdFAIL 表示在 xTicksToWait 内写入命令失败。

ISR 中复位定时器:

BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
  • 同上,多了 pxHigherPriorityTaskWoken 参数,用于中断上下文中判断是否需要上下文切换。

6.6 修改周期

除了启动和复位定时器,FreeRTOS 还提供修改定时器周期的接口,这样可以动态地改变定时器的触发间隔。

任务中修改定时器周期:

BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, TickType_t xNewPeriod, TickType_t xTicksToWait );
  • xTimer:要修改周期的定时器句柄。

  • xNewPeriod:新的周期,以 Tick 为单位。修改后,下一个回调的触发时间为当前时间加上这个新周期。

  • xTicksToWait:写入“修改周期”命令到定时器命令队列的超时时间。
    如果命令无法在规定时间内写入队列,则返回 pdFAIL。

  • 返回值:

    • pdPASS 表示命令成功写入队列,周期修改命令生效;
    • pdFAIL 表示写入命令失败。

ISR 中修改定时器周期:

BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, TickType_t xNewPeriod, BaseType_t *pxHigherPriorityTaskWoken );
  • 同任务版本,但无法阻塞,因此立即尝试写入命令。

  • pxHigherPriorityTaskWoken:用于检测是否需要在中断退出时进行任务调度。

  • 返回值:

    • pdPASS 表示成功;
    • pdFAIL 表示命令写入失败。

6.7 注意事项

  • xTicksToWait 参数
    在所有涉及命令写入定时器命令队列的函数中,xTicksToWait 表示调用者等待将命令写入队列的时间,而不是定时器的超时时间或周期。这是因为定时器的所有操作(启动、停止、复位、修改周期)都是通过向定时器命令队列发送命令实现的。
  • 命令队列满时,命令写入会失败(或阻塞等待,取决于 xTicksToWait 的设置)
  • 这些命令最终由定时器守护任务处理,守护任务的优先级(configTIMER_TASK_PRIORITY)和命令队列长度(configTIMER_QUEUE_LENGTH)决定了定时器命令的响应速度,从而影响定时器回调函数的调用时刻。

7.示例:一般使用

/* 定义全局变量 */
static TimerHandle_t xMyTimerHandle;   // 定时器句柄,用于引用创建的定时器
static int flagTimer = 0;              // 用于演示定时器回调执行时改变的标志变量

/*-----------------------------------------------------------
 * Task1Function:
 * 这是一个示例任务,该任务在开始运行时启动定时器,
 * 然后进入无限循环,不断打印消息。
 * 定时器启动后,会按照设定的周期自动触发回调函数。
 *-----------------------------------------------------------*/
void Task1Function(void * pvParameters)
{
    volatile int i = 0;

    /* 启动定时器 xMyTimerHandle,xTicksToWait 参数为 0 表示
       如果定时器命令队列满了,则不等待,立即返回 */
    xTimerStart(xMyTimerHandle, 0);

    /* 进入任务主循环 */
    while (1)
    {
        /* 打印任务执行信息 */
        printf("Task1Function ...\r\n");

        /* 此处可添加延时函数,例如 vTaskDelay(),以免任务占用过多CPU资源
           但本例为了简单演示,不添加延时 */
    }
}

/*-----------------------------------------------------------
 * Task2Function:
 * 此任务示例目前没有做任何事情,可用作后续扩展。
 *-----------------------------------------------------------*/
void Task2Function(void * pvParameters)
{
    volatile int i = 0;
    while (1)
    {
        /* 空循环,暂时未实现功能 */
    }
}

/*-----------------------------------------------------------
 * MyTimerCallbackFunction:
 * 定时器回调函数,当定时器超时时,该函数会被 RTOS 定时器守护任务调用。
 * 此处演示每次定时器超时时,将全局标志 flagTimer 取反,并打印调用次数。
 *-----------------------------------------------------------*/
void MyTimerCallbackFunction( TimerHandle_t xTimer )
{
    static int cnt = 0;  // 静态变量,用于记录回调函数被调用的次数
    flagTimer = !flagTimer;  // 取反 flagTimer 的值,演示状态变化
    printf("MyTimerCallbackFunction: cnt = %d\r\n", cnt++);
}

/*-----------------------------------------------------------
 * main 函数:程序入口
 * 主要步骤:
 * 1. 硬件初始化;
 * 2. 创建定时器(这里使用动态分配内存的方法);
 * 3. 创建任务;
 * 4. 启动调度器。
 *-----------------------------------------------------------*/
int main( void )
{
    TaskHandle_t xHandleTask1;  // 用于存储 Task1 的任务句柄

#ifdef DEBUG
    debug();  // 如果定义了 DEBUG,调用调试函数
#endif

    /* 初始化硬件,平台相关的初始化函数 */
    prvSetupHardware();

    /* 输出启动信息 */
    printf("Hello, world!\r\n");

    /* 创建定时器
       参数说明:
       - "mytimer":定时器的名称(用于调试);
       - 100:定时器周期,单位为 Tick;
       - pdTRUE:自动加载定时器(自动重载),即定时器回调会周期性调用;
       - NULL:定时器ID,这里不使用,可以传递额外信息供回调函数识别定时器;
       - MyTimerCallbackFunction:定时器超时后的回调函数 */
    xMyTimerHandle = xTimerCreate("mytimer", 100, pdTRUE, NULL, MyTimerCallbackFunction);

    /* 检查定时器创建是否成功 */
    if (xMyTimerHandle == NULL)
    {
        printf("Error: Cannot create timer\r\n");
        /* 这里可以加入错误处理代码 */
    }

    /* 创建 Task1 任务,任务函数为 Task1Function
       参数说明:
       - "Task1":任务名称;
       - 100:任务栈大小(单位通常为字节或堆栈深度,依平台而定);
       - NULL:传递给任务的参数;
       - 1:任务优先级;
       - &xHandleTask1:存储任务句柄的变量地址 */
    xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);

    /* 如果需要,还可以创建 Task2 任务(此处被注释掉,可根据需要启用)
       xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL); */

    /* 启动任务调度器。启动调度器后,所有创建的任务开始并发执行,
       同时定时器服务任务也会被自动创建(如果配置了定时器功能)。 */
    vTaskStartScheduler();

    /* 如果程序运行到这里,通常表示调度器启动失败,
       可能是内存不足导致无法创建空闲任务。 */
    return 0;
}

还是需要在FreeRTOSConfig.h中去定义一些宏:

#define configUSE_TIMERS             1
#define configTIMER_TASK_PRIORITY    (configMAX_PRIORITIES-2)
#define configTIMER_QUEUE_LENGTH     10
#define configTIMER_TASK_STACK_DEPTH 100

img

高/低电平都是100ms,configTICK_RATE_HZ设定是1000,也就是一个tick是1ms,定时器周期设定的是100个,也就是100ms,守护任务会抢占然后执行定时器的回调函数

8.示例:定时器防抖

在实际的按键操作中,可能会有机械抖动:

img

按下或松开一个按键,它的 GPIO 电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。

如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。怎么处理?

  • 按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报
  • 使用定时器

显然第 1 种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。怎么使用定时器?看下图:

img

核心在于:在 GPIO 中断中并不立刻记录按键值,而是修改定时器超时时间,10ms 后再处理。

  • 如果 10ms 内又发生了 GPIO 中断,那就认为是抖动,这时再次修改超时时间为 10ms(也就是重置定时器)。
  • 只有 10ms 之内再无 GPIO 中断发生,那么定时器的回调函数才会被调用。在定时器函数中打印按键值。

下面代码实现类似,只不过设置的超时时间是2s

/* 定时器句柄和标志变量定义 */
static TimerHandle_t xMyTimerHandle;  // FreeRTOS软件定时器句柄
static int flagTimer = 0;            // 定时器回调函数触发的状态标志

/* 任务1函数 */
void Task1Function(void * param)
{
    volatile int i = 0;  // volatile防止编译器优化
    
    // 注意:此处定时器启动被注释,实际使用需取消注释
    //xTimerStart(xMyTimerHandle, 0);  // 启动定时器(0表示不阻塞)
    
    while (1)
    {
        // 典型问题:任务中未添加阻塞函数(如vTaskDelay),
        // 将导致该任务持续占用CPU,建议添加延时释放CPU
        //printf("Task1Function ...\r\n");
    }
}

/* 任务2函数(框架,暂无实际功能) */
void Task2Function(void * param)
{
    volatile int i = 0;
    while (1)
    {
    }
}

/* 定时器回调函数 */
void MyTimerCallbackFunction( TimerHandle_t xTimer )
{
    static int cnt = 0;    // 静态变量保持计数状态
    flagTimer = !flagTimer; // 翻转状态标志
    
    /* 
     * 注意:在回调中调用printf等耗时函数可能影响实时性,
     * 建议仅在调试时使用,实际应用替换为更高效的操作
     */
    printf("Get GPIO Key cnt = %d\r\n", cnt++);
}

/*-----------------------------------------------------------*/

/* 按键GPIO初始化 */
void KeyInit(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // 使能GPIOA时钟
    
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;   // 配置PA0引脚
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式(检测低电平)
    GPIO_Init(GPIOA, &GPIO_InitStructure);        // 应用配置
}

/* 按键中断初始化 */
void KeyIntInit(void)
{
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能复用功能时钟
    
    /* 映射GPIOA0到EXTI0中断线 */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    /* 配置EXTI0中断线 */
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;          // 中断线0
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 双边沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;           // 使能中断线
    EXTI_Init(&EXTI_InitStructure);                     // 应用配置

    /* 配置NVIC中断控制器 */
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQChannel;     // 外部中断0通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  // 抢占优先级0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级0
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;            // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                            // 应用配置
}

/* EXTI0中断服务函数 */
void EXTI0_IRQHandler(void)
{
    static int cnt = 0;  // 中断计数
    
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)  // 确认是EXTI0中断
    {
        printf("EXTI0_IRQHandler cnt = %d\r\n", cnt++);  // 调试输出
        
        /* 
         * 关键操作:重置定时器实现消抖
         * 问题:此处应使用xTimerResetFromISR()而非xTimerReset()
         * 原因:在中断中调用非ISR结尾的API可能导致上下文错误
         * 修正建议:
         * BaseType_t xHigherPriorityTaskWoken = pdFALSE;
         * xTimerResetFromISR(xMyTimerHandle, &xHigherPriorityTaskWoken);
         * portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
         */
        xTimerReset(xMyTimerHandle, 0);  // 重置定时器(2000 ticks后触发回调)
        
        EXTI_ClearITPendingBit(EXTI_Line0);  // 清除中断标志
    }     
}

/* 主函数 */
int main( void )
{
    TaskHandle_t xHandleTask1;  // 任务1句柄(暂未使用)
    
#ifdef DEBUG
    debug();  // 调试初始化(如果定义了DEBUG)
#endif

    prvSetupHardware();  // 硬件初始化(假设包含时钟配置等)
    printf("Hello, world!\r\n");  // 启动信息输出

    KeyInit();      // 初始化按键GPIO
    KeyIntInit();   // 配置按键中断

    /* 
     * 创建软件定时器:
     * 参数1:定时器名称(调试用)
     * 参数2:周期2000 ticks(单位取决于configTICK_RATE_HZ)
     * 参数3:自动重载模式(pdFALSE表示单次定时器)
     * 参数4:定时器ID(NULL表示不设置)
     * 参数5:回调函数指针
     */
    xMyTimerHandle = xTimerCreate("mytimer", 2000, pdFALSE, NULL, MyTimerCallbackFunction);

    // 创建任务1(优先级1,堆栈深度100字)
    xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
    //xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);  // 任务2暂未启用
    
    vTaskStartScheduler();  // 启动FreeRTOS调度器

    /* 
     * 正常情况下不会执行到这里,
     * 除非内存不足导致空闲任务创建失败
     */
    return 0;
}