13. FreeRTOS的软件定时器

一、FreeRTOS的软件定时器

1.1、FreeRTOS的软件定时器简介

  软件定时器是指具有定时功能的软件,FreeRTOS 提供的软件定时器允许在创建前设置一个软件定时器定时超时时间,在软件定时器成功创建并启动后,软件定时器开始定时,当软件定时器的定时时间达到或超过先前设置好的软件定时器定时器超时时间时,软件定时器就处于超时状态,此时软件定时器就会调用相应的回调函数,一般这个回调函数的处理的事务就是需要周期处理的事务。

  FreeRTOS 提供的软件定时器功能,属于 FreeRTOS 的中可裁剪可配置的功能,如果要使能软件定时器功能,那需要在 FreeRTOSConfig.h 文件中将 configUSE_TIMERS 配置项配置成 1。

  使能了软件定时器功能后,在调用函数 vTaskStartScheduler() 开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做 软件定时器服务任务软件定时器服务任务,主要负责软件定时器超时的逻辑判断、调用超时软件定时器的超时回调函数以及处理软件定时器命令队列。

  软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数,例如 vTaskDelay()vTaskDelayUntil() 和一些会到时任务阻塞的等到事件函数,这些函数将会导致软件定时器服务任务阻塞,这是不可以出现的。

  使能了软件定时器功能后,在调用函数 vTaskStartScheduler() 开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做 软件定时器服务任务。软件定时器服务任务,主要负责 软件定时器超时的逻辑判断调用超时软件定时器的超时回调函数 以及 处理软件定时器命令队列

  FreeRTOS 提供了许多软件定时器相关的 API 函数,这些 API 函数,大部分都是往定时器的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列,是提供给 FreeRTOS 中的软件定时器使用的,用户是不能直接访问的。软件定时器命令队列的操作过程如下图所示:

软件定时器命令队列操作示意图

  上图中,左侧的代码为应用程序中用户任务的代码,而右侧的代码为软件定时器服务任务的代码。当用户任务需要操作软件定时器时,就需要调用软件定时器相关的 API 函数,例如图中调用了函数 vTaskStart() 启动软件定时器的定时,而函数 vTaskStart() 实际上会往软件定时器命令队列写入一条消息(发送命令),这条消息就包含了待操作的定时器对象以及操作的命令(启动软件定时器),软件定时器服务任务就会去读取软件定时器命令队列中的消息(接收命令),并处理这些消息(处理命令)。可以看出,用户任务并不会直接操作软件定时器对象,而是发送命令给软件定时器服务任务,软件定时器服务任务接收到命令后,根据命令内容去操作软件定时器。

1.2、软件定时器的相关配置

  用户可以根据需要配置 FreeRTOSConfig.h 文件中的 configUSE_TIMERS,同时 FreeRTOSConfig.h 文件中还有一些其他与软件定时器相关的配置项。

#define configUSE_TIMERS                                1                               // 1: 使能软件定时器,默认: 0 

  此宏用于 使能软件定时器功能,如果要使用软件定时器功能,则需要将该宏定义定义为 1。开启软件定时器功能后,系统会系统创建软件定时器服务任务。

#define configTIMER_TASK_PRIORITY                       (configMAX_PRIORITIES - 1)      // 定义软件定时器任务的优先级,无默认configUSE_TIMERS为1时需定义 

  此宏用于 配置软件定时器服务任务的任务优先级,当使能了软件定时器功能后,需要配置该宏定义,此宏定义可以配置为 0 ~ (configMAX_PRIORITY-1) 的任意值。

#define configTIMER_QUEUE_LENGTH                        5                               // 定义软件定时器命令队列的长度,无默认configUSE_TIMERS为1时需定义 

  此宏用于 配置软件定时器命令队列的队列长度,当使能了软件定时器功能后,需要配置该宏定义,若要正常使用软件定时器功能,此宏定义需定义成一个大于 0 的值。

#define configTIMER_TASK_STACK_DEPTH                    (configMINIMAL_STACK_SIZE * 2)  // 定义软件定时器任务的栈空间大小,无默认configUSE_TIMERS为1时需定义 

  此宏用于 配置软件定时器服务任务的栈大小,当使能了软件定时器功能后,需要配置该宏定义,由于所有软件定时器的定时器超时回调函数都是由软件定时器服务任务调用的,因此这些软件定时器超时回调函数运行时使用的都是软件定时器服务任务的栈。

软件定时器的超时回调函数是在软件定时器服务任务中被调用的,服务任务不是专为某个定时器服务的,它还要处理其他定时器。

回调函数要尽快实行,不能进入阻塞状态,即不能调用那些会阻塞任务的 API 函数,如:vTaskDelay()。

访问队列或者信号量的非零阻塞时间的 API 函数也不能调用。

1.3、单次定时器与周期定时器

  FreeRTOS 提供的软件定时器还能够根据需要设置成 单次定时器周期定时器。当 单次定时器 定时超时后,不会自动启动下一个周期的定时,而 周期定时器 在定时超时后,会自动地启动下一个周期的定时。

  • 单次定时器:单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,不会自动重新开启定时,不过可以被手动重新开启。
  • 周期定时器:周期定时器的一旦启动以后就会在执行完回调函数以后自动的重新启动 ,从而周期地执行其软件定时器回调函数。

单次定时器和周期定时器差异示意图

  上图展示了单次定时器和周期定时器之间的差异,图中的垂直虚线的间隔时间为一个单位时间,可以理解为一个系统时钟节拍。其中 Timer1 为周期定时器,定时超时时间为 2 个单位时间,Timer2 为单次定时器,定时超时时间为 1 个单位时间。可以看到,Timer1 在开启后,一直以 2 个时间单位的时间间隔重复执行,为 Timer2 则在第一个超时后就不在执行了。

1.4、软件定时器的状态

  软件定时器可以处于一下两种状态中一种:

  • 休眠态:休眠态软件定时器可以通过其句柄被引用,但是因为没有运行,所以其定时超时回调函数不会被执行。
  • 运行态:处于运行态或在上次定时超时后再次定时超时的软件定时器,会执行其定时超时回调函数。

  单次定时器的状态转化图,如下图所示:

单次定时器状态转换图

  周期定时器的状态转换图,如下图所示:

周期定时器状态转换图

新创建的软件定时器处于休眠状态 ,也就是未运行的。

二、软件定时器相关API函数

2.1、软件定时器的结构体

  下面来看一下软件定时器的控制块结构体,软件定时器的控制块结构体定义在文件 os.h 中,具体的代码如下所示:

typedef struct tmrTimerControl
{
    const char * pcTimerName;                                                   // 软件定时器名字
    ListItem_t xTimerListItem;                                                  // 软降定时器列表项
    TickType_t xTimerPeriodInTicks;                                             // 软件定时器的周期
    void * pvTimerID;                                                           // 软件定时器的ID
    portTIMER_CALLBACK_ATTRIBUTE TimerCallbackFunction_t pxCallbackFunction;    // 软件定时器的回调函数
    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxTimerNumber;                                              // 软件定时器的编号
    #endif
    uint8_t ucStatus;                                                           // 软件定时器的状态
} xTIMER;

2.2、创建软件定时器

  FreeRTOS 提供了两种创建软件定时器的方式,分别为动态方式创建软件定时器和静态方式创建软件定时器,两者的区别在于静态方式创建软件定时器时,需要用户提供创建软件定时器所需的内存空间,而使用动态方式创建软件定时器时,FreeRTOS 会自动从 FreeRTOS 管理的堆中分配创建软件定时器所需的内存空间。

2.2.1、动态方式创建软件定时器

  动态方式创建软件定时器 API 函数的函数原型如下所示:

// 返回值: NULL: 软件定时器创建失败; 其它值: 软件定时器创建成功,返回其句柄;
TimerHandle_t xTimerCreate( const char * const pcTimerName,                     // 软件定时器名
                            const TickType_t xTimerPeriodInTicks,               // 定时超时时间,单位:系统时钟节拍
                            const BaseType_t xAutoReload,                       // 定时器模式,pdTRUE: 周期定时器; pdFALSE: 单次定时器
                            void * const pvTimerID,                             // 软件定时器ID,用于多个软件定时器公用一个超时回调函数
                            TimerCallbackFunction_t pxCallbackFunction );       // 软件定时器超时回调函数

2.2.2、静态方式创建软件定时器

  静态方式创建软件定时器 API 函数的函数原型如下所示:

// 返回值: NULL: 软件定时器创建失败; 其它值: 软件定时器创建成功,返回其句柄;
TimerHandle_t xTimerCreateStatic( const char * const pcTimerName,               // 软件定时器名
                                  const TickType_t xTimerPeriodInTicks,         // 定时超时时间,单位:系统时钟节拍
                                  const BaseType_t xAutoReload,                 // 定时器模式,pdTRUE: 周期定时器; pdFALSE: 单次定时器;
                                  void * const pvTimerID,                       // 软件定时器ID,用于多个软件定时器公用一个超时回调函数
                                  TimerCallbackFunction_t pxCallbackFunction,   // 软件定时器超时回调函数
                                  StaticTimer_t * pxTimerBuffer );              // 创建软件定时器所需的内存空间

2.3、删除定时器

  FreeRTOS 提供了用于删除软件定时器的 API 函数,函数原型如下所示:

// xTimer为待删除的软件定时器的句柄; xTicksToWait为发送命令到软件定时器命令队列的最大等待时间;
// 返回pdPASS表示软件定时器删除成功; 返回pdFAIL表示软件定时器删除失败;
#define xTimerDelete( xTimer, xTicksToWait ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_DELETE, 0U, NULL, ( xTicksToWait ) )

2.4、开启软件定时器

  FreeRTOS 提供了两个用于开启软件定时器定时的 API 函数,这个两个函数分别用于在任务和在中断中开启软件定时器定时。

2.4.1、在任务中开启软件定时器

  在任务中开启软件定时器定时 API 函数的函数原型如下所示:

// xTimer为待开启的软件定时器的句柄; xTicksToWait为发送命令到软件定时器命令队列的最大等待时间
// 返回pdPASS表示软件定时器开启成功; 返回pdFAIL表示软件定时器开启失败
#define xTimerStart( xTimer, xTicksToWait ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )

2.4.2、在中断中开启软件定时器

  在中断中开启软件定时器定时 API 函数的函数原型如下所示:

// xTimer为待开启的软件定时器的句柄; pxHigherPriorityTaskWoken用于标记函数退出后是否需要进行任务切换;
// 返回pdPASS表示软件定时器开启成功; 返回pdFAIL表示软件定时器开启失败
#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )

2.5、停止软件定时器

  FreeRTOS 提供了两个用于停止软件定时器定时的 API 函数,这个两个函数分别用于在任务和在中断中停止软件定时器定时。

2.5.1、在任务中停止软件定时器

  在任务中停止软件定时器定时 API 函数的函数原型如下所示:

// xTimer为待停止的软件定时器的句柄; xTicksToWait为发送命令到软件定时器命令队列的最大等待时间;
// 返回pdPASS表示软件定时器停止成功; 返回pdFAIL表示软件定时器停止失败;
#define xTimerStop( xTimer, xTicksToWait ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, ( xTicksToWait ) )

2.5.2、在中断中停止软件定时器

  在中断中停止软件定时器定时 API 函数的函数原型如下所示:

// xTimer为待停止的软件定时器的句柄; pxHigherPriorityTaskWoken用于标记函数退出后是否需要进行任务切换;
// 返回pdPASS表示软件定时器停止成功; 返回pdFAIL表示软件定时器停止失败;
#define xTimerStopFromISR( xTimer, pxHigherPriorityTaskWoken ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP_FROM_ISR, 0, ( pxHigherPriorityTaskWoken ), 0U )

2.6、复位软件定时器

  FreeRTOS 提供了两个用于复位软件定时器定时的 API 函数,这个两个函数分别用于在任务和在中断中复位软件定时器定时。该功能将使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新定时。

2.6.1、在任务中复位软件定时器

  在任务中复位软件定时器定时 API 函数的函数原型如下所示:

// xTimer为待复位的软件定时器的句柄; xTicksToWait为发送命令到软件定时器命令队列的最大等待时间;
// 返回pdPASS表示软件定时器复位成功; 返回pdFAIL表示软件定时器复位失败;
#define xTimerReset( xTimer, xTicksToWait ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )

2.6.2、在中断中复位软件定时器

  在中断中复位软件定时器定时 API 函数的函数原型如下所示:

// xTimer为待复位的软件定时器的句柄; pxHigherPriorityTaskWoken用于标记函数退出后是否需要进行任务切换;
// 返回pdPASS表示软件定时器复位成功; 返回pdFAIL表示软件定时器复位失败;
#define xTimerResetFromISR( xTimer, pxHigherPriorityTaskWoken ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )

2.7、更改定时超时时间

  FreeRTOS 提供了两个分别用于任务和中断的更改软件定时器的定时超时时间的 API 函数。

2.7.1、在任务中更改定时超时时间

  在任务中更改软件定时器的定时超时时间 API 函数的函数原型如下所示:

// xTimer为待更改定时超时时间的软件定时器的句柄; xNewPeriod为新的定时超时时间,单位:系统时钟节拍; xTicksToWait为发送命令到软件定时器命令队列的最大等待时间
// 返回pdPASS表示软件定时器定时超时时间更改成功; 返回pdFAIL表示软件定时器定时超时时间更改失败;
#define xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD, ( xNewPeriod ), NULL, ( xTicksToWait ) )

2.7.2、在中断中更改定时超时时间

  在中断中更改软件定时器的定时超时时间 API 函数的函数原型如下所示:

// xTimer为待更改定时超时时间的软件定时器的句柄; xNewPeriod为新的定时超时时间,单位:系统时钟节拍; pxHigherPriorityTaskWoken用于标记函数退出后是否需要进行任务切换;
// 返回pdPASS表示软件定时器定时超时时间更改成功; 返回pdFAIL表示软件定时器定时超时时间更改失败;
#define xTimerChangePeriodFromISR( xTimer, xNewPeriod, pxHigherPriorityTaskWoken ) \
    xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, ( xNewPeriod ), ( pxHigherPriorityTaskWoken ), 0U )

三、实验例程

  main() 函数内容如下:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
    LED_Init();
    Key_Init();

    freertos_demo();
  
    return 0;
}

  FreeRTOS 例程入口函数:

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}

  START_TASK 任务配置:

TimerHandle_t timer1_handle;
TimerHandle_t timer2_handle;

void timer_callback(TimerHandle_t pxTimer);

/**
 * START_TASK 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define START_TASK_PRIORITY     1
#define START_TASK_STACK_SIZE   128

TaskHandle_t start_task_handle;

void start_task(void *pvParameters );

/**
 * @brief 开始任务的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();                                                       // 进入临界区,关闭中断

    timer1_handle = xTimerCreate("timer1", 1000, pdFALSE, (void *)1, timer_callback);  // 单次定时器
    timer2_handle = xTimerCreate("timer2", 1000, pdTRUE, (void *)2, timer_callback);    // 周期定时器

    xTaskCreate((TaskFunction_t        ) task1,                                 // 任务函数
                (char *                ) "task1",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK1_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task1_handle);                        // 任务句柄

    vTaskDelete(NULL);                                                          // 删除任务自身

    taskEXIT_CRITICAL();                                                        // 退出临界区,重新开启中断
}

  TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define TASK1_PRIORITY     2
#define TASK1_STACK_SIZE   128

TaskHandle_t task1_handle;

void task1(void *pvParameters);

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    while (1)
    {
        switch (Key_Scan(0))
        {
        case WKUP_PRESS:
            xTimerStart(timer1_handle, portMAX_DELAY);                          // 开启软件定时器
            break;

        case KEY1_PRESS:
            xTimerStart(timer2_handle, portMAX_DELAY);                          // 开启软件定时器
            break;
  
        case KEY2_PRESS:
            xTimerStop(timer1_handle, portMAX_DELAY);                           // 停止软件定时器
            break;

        case KEY3_PRESS:
            xTimerStop(timer2_handle, portMAX_DELAY);                           // 停止软件定时器
            break;

        default:
            break;
        }

        vTaskDelay(10);
    }
}

  软件定时器超时回调函数:

/**
 * @brief 定时器的回调函数
 * 
 * @param pxTimer 定时器句柄
 */
void timer_callback(TimerHandle_t pxTimer)
{
    if (pxTimer == timer1_handle)
    {
        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
    }
    else if (pxTimer == timer2_handle)
    {
        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
    }
}
posted @ 2024-03-23 19:00  星光映梦  阅读(480)  评论(0)    收藏  举报