FreeRTOS 快速入门(九)之软件定时器


一、软件定时器的特性

使用定时器跟使用手机闹钟是类似的:

  • 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
  • 指定类型,定时器有两种类型:
    • 一次性(One-shot timers):
      这类定时器启动后,它的回调函数只会被调用一次;
      可以手工再次启动它,但是不会自动启动它。
    • 自动加载定时器(Auto-reload timers):
      这类定时器启动后,时间到之后它会自动启动它;
      这使得回调函数被周期性地调用。
  • 指定要做什么事,就是指定回调函数

实际的闹钟分为:有效、无效两类。软件定时器也是类似的,它由两种状态:

  • 运行(RunningActive):运行态的定时器,当指定时间到达之后,它的回调函数会被调用
  • 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用

定时器运行情况示例如下:

  • Timer1:它是一次性的定时器,在 t1 启动,周期是 6 个Tick。经过 6 个 tick 后,在 t7 执行回调函数。它的回调函数只会被执行一次,然后该定时器进入冬眠状态。
  • Timer2:它是自动加载的定时器,在 t1 启动,周期是 5 个 Tick。每经过 5 个 tick 它的回调函数都被执行,比如在 t6、t11、t16 都会执行。

FreeRTOS 的配置上,如果要是用定时器就需要配置下面几个宏定义:

//打开定时器
#define configUSE_TIMERS                1

//定时器的优先级
#define configTIMER_TASK_PRIORITY       50

//定时器栈大小
#define configTIMER_TASK_STACK_DEPTH    50

//定时器队列大小
#define configTIMER_QUEUE_LENGTH        50

二、软件定时器的上下文

1、守护任务

FreeRTOS 中有一个 Tick 中断,软件定时器基于 Tick 来运行。但是,在哪里执行定时器函数呢?FreeRTOSRTOS,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。

所以,在 FreeRTOS 中,不在 Tick 中断中执行定时器函数,而在某个任务里执行,这个任务就是:RTOS Damemon TaskRTOS 守护任务。以前被称为"Timer server",但是这个任务要做并不仅仅是定时器相关,所以改名为:RTOS Damemon Task

FreeRTOS 的配置项 configUSE_TIMERS 被设置为 1 时,在启动调度器时,会自动创建 RTOS Damemon Task

我们自己编写的任务函数要使用定时器时,是通过"定时器命令队列"(timer command queue)和守护任务交互,如下图所示:

守护任务的优先级为:configTIMER_TASK_PRIORITY;定时器命令队列的长度为 configTIMER_QUEUE_LENGTH

2、守护任务的调度

守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:

  • 处理命令:从命令队列里取出命令、处理
  • 执行定时器的回调函数

能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。

3、回调函数

定时器的回调函数的原型如下:

void ATimerCallback( TimerHandle_t xTimer );

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

所以,定时器的回调函数不要影响其他人:

  • 回调函数要尽快实行,不能进入阻塞状态
  • 不要调用会导致阻塞的API函数,比如 vTaskDelay()
  • 可以调用 xQueueReceive() 之类的函数,但是超时时间要设为 0:即刻返回,不可阻塞

4、软件定时器定时器的控制块

    typedef struct tmrTimerControl                  /* The old naming convention is used to prevent breaking kernel aware debuggers. */
    {
        const char * pcTimerName;                   /*<< Text name.  This is not used by the kernel, it is included simply to make debugging easier. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
        ListItem_t xTimerListItem;                  /*<< Standard linked list item as used by all kernel features for event management. */
        TickType_t xTimerPeriodInTicks;             /*<< How quickly and often the timer expires. */
        void * pvTimerID;                           /*<< An ID to identify the timer.  This allows the timer to be identified when the same callback is used for multiple timers. */
        TimerCallbackFunction_t pxCallbackFunction; /*<< The function that will be called when the timer expires. */
        #if ( configUSE_TRACE_FACILITY == 1 )
            UBaseType_t uxTimerNumber;              /*<< An ID assigned by trace tools such as FreeRTOS+Trace */
        #endif
        uint8_t ucStatus;                           /*<< Holds bits to say if the timer was statically allocated or not, and if it is active or not. */
    } xTIMER;
typedef xTIMER Timer_t

定时器控制块中一共含有7个变量,其作用解析如下:

  • const char * pcTimerName:记录定时器名字
  • ListItem_t xTimerListItem:定时器的列表项,用于插入定时器列表;
  • TickType_t xTimerPeriodInTicks:定时器的周期,单位为系统节拍周期,即 tick;
  • void * pvTimerID:定时器的 ID,整数形式。该 ID 是当一个回调函数分配给一个或多个定时器时,可以根据 ID 不同处理回调函数中不同程序;
  • TimerCallbackFunction_t pxCallbackFunction:定时器回调函数;
  • UBaseType_t uxTimerNumber:跟踪工具分配的 ID,如 FreeRTOS+Trace;
  • uint8_t ucStatus:保存计时器是否静态分配,以及它是否处于活动状态;

在定义完成控制块后,代码中会使用 typedef xTIMER Timer_t 重定义控制块的变量别名,在后续代码及开发中可以使用别名完成程序开发。

三、软件定时器的函数

1、创建

要使用定时器,需要先创建它,得到它的句柄。
有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:

/** 使用动态分配内存的方法创建定时器
 * 
 * pcTimerName:         定时器名字, 用处不大, 一般在调试时用到
 * xTimerPeriodInTicks: 周期, 以Tick为单位
 * uxAutoReload:        类型, pdTRUE表示自动加载, pdFALSE表示一次性
 * pvTimerID:           回调函数可以使用此参数, 比如分辨是哪个定时器
 * pxCallbackFunction:  回调函数
 * 
 * 返回值: 成功则返回TimerHandle_t, 否则返回NULL
 */
TimerHandle_t xTimerCreate( const char * const pcTimerName,
							const TickType_t xTimerPeriodInTicks,
							const UBaseType_t uxAutoReload,
							void * const pvTimerID,
							TimerCallbackFunction_t pxCallbackFunction );

/** 使用静态分配内存的方法创建定时器
 * 
 * pcTimerName:         定时器名字, 用处不大, 一般在调试时用到
 * xTimerPeriodInTicks: 周期, 以Tick为单位
 * uxAutoReload:        类型, pdTRUE表示自动加载, pdFALSE表示一次性
 * pvTimerID:		    回调函数可以使用此参数, 比如分辨是哪个定时器
 * pxCallbackFunction:  回调函数
 * pxTimerBuffer: 		传入一个StaticTimer_t结构体, 将在上面构造定时器
 * 
 * 返回值: 成功则返回TimerHandle_t, 否则返回NULL
 */
TimerHandle_t xTimerCreateStatic( const char * const pcTimerName,
								  TickType_t xTimerPeriodInTicks,
							      UBaseType_t uxAutoReload,
								  void * pvTimerID,
								  TimerCallbackFunction_t pxCallbackFunction,
								  StaticTimer_t *pxTimerBuffer );

回调函数的类型是:

void ATimerCallback( TimerHandle_t xTimer );
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

2、删除

动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:

/* 删除定时器
 * xTimer: 要删除哪个定时器
 * xTicksToWait: 超时时间
 * 返回值: pdFAIL表示"删除命令"在xTicksToWait个Tick内无法写入队列
 * pdPASS表示成功
 */
BaseType_t xTimerDelete( TimerHandle_t xTimer, 
						 TickType_t xTicksToWait );

定时器的很多 API 函数,都是通过发送"命令"到命令队列,由守护任务来实现。

如果队列满了,"命令"就无法即刻写入队列。我们可以指定一个超时时间 xTicksToWait,等待一会。

3、启动/停止

启动定时器就是设置它的状态为运行态(RunningActive)。

停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。

涉及的函数原型如下:

/** 启动定时器
 * 
 * xTimer: 哪个定时器
 * xTicksToWait: 超时时间
 * 
 * 返回值: pdFAIL表示"启动命令"在xTicksToWait个Tick内无法写入队列
 	      pdPASS表示成功
 */
 BaseType_t xTimerStart( TimerHandle_t xTimer, 
 					     TickType_t xTicksToWait );
 
/** 启动定时器(ISR版本)
* 
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 如果守护任务的优先级比当前任务的高, 
		则 *pxHigherPriorityTaskWoken = pdTRUE, 表示需要进行任务调度
		
* 返回值: pdFAIL表示"启动命令"无法写入队列;pdPASS表示成功
*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
							   BaseType_t *pxHigherPriorityTaskWoken );
						
/* 停止定时器
 * xTimer: 哪个定时器
 * xTicksToWait: 超时时间
 * 
 * 返回值: pdFAIL表示"停止命令"在xTicksToWait个Tick内无法写入队列
 * pdPASS表示成功
 */
BaseType_t xTimerStop( TimerHandle_t xTimer,
 					   TickType_t xTicksToWait );

/** 停止定时器(ISR版本)
 * 
 * xTimer: 哪个定时器
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 如果守护任务的优先级比当前任务的高,
 		则"*pxHigherPriorityTaskWoken = pdTRUE", 表示需要进行任务调度
 		
 * 返回值: pdFAIL表示"停止命令"无法写入队列;pdPASS表示成功
 */
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
							  BaseType_t *pxHigherPriorityTaskWoken );

注意,这些函数的 xTicksToWait 表示的是,把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。

xTicksToWait 不是定时器本身的超时时间,不是定时器本身的"周期"。

创建定时器时,设置了它的周期(period)。xTimerStart() 函数是用来启动定时器。假设调用 xTimerStart() 的时刻是 tX,定时器的周期是 n,那么在 tX+n 时刻定时器的回调函数被调用。如果定时器已经被启动,但是它的函数尚未被执行,再次执行 xTimerStart() 函数相当于执行 xTimerReset(),重新设定它的启动时间。

4、复位

从定时器的状态转换图可以知道,使用 xTimerReset() 函数可以让定时器的状态从冬眠态转换为运行态,相当于使用 xTimerStart() 函数。

如果定时器已经处于运行态,使用 xTimerReset() 函数就相当于重新确定超时时间。假设调用 xTimerReset() 的时刻是 tX,定时器的周期是 n,那么 tX+n 就是重新确定的超时时间。

复位函数的原型如下:

/** 复位定时器
 * 
 * xTimer: 哪个定时器
 * xTicksToWait: 超时时间
 * 
 * 返回值: pdFAIL表示"复位命令"在xTicksToWait个Tick内无法写入队列;pdPASS表示成功
 */
BaseType_t xTimerReset( TimerHandle_t xTimer, 
						TickType_t xTicksToWait );

/** 复位定时器(ISR版本)
 * 
 * xTimer: 哪个定时器
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 如果守护任务的优先级比当前任务的高,
 		则"*pxHigherPriorityTaskWoken = pdTRUE", 表示需要进行任务调度
 * 
 * 返回值: pdFAIL表示"停止命令"无法写入队列;pdPASS表示成功
 */
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
							   BaseType_t *pxHigherPriorityTaskWoken );

5、修改周期

从定时器的状态转换图可以知道,使用 xTimerChangePeriod() 函数,处理能修改它的周期外,还可以让定时器的状态从冬眠态转换为运行态。

修改定时器的周期时,会使用新的周期重新计算它的超时时间。假设调用 xTimerChangePeriod() 函数的时间 tX,新的周期是 n,则 tX+n 就是新的超时时间。

相关函数的原型如下:

/** 修改定时器的周期
 * 
 * xTimer: 哪个定时器
 * xNewPeriod: 新周期
 * xTicksToWait: 超时时间, 命令写入队列的超时时间
 * 
 * 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列;pdPASS表示成功
 */
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
							   TickType_t xNewPeriod,
							   TickType_t xTicksToWait );
						
/** 修改定时器的周期
 * 
 * xTimer: 哪个定时器
 * xNewPeriod: 新周期
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 如果守护任务的优先级比当前任务的高,
 		则"*pxHigherPriorityTaskWoken = pdTRUE", 表示需要进行任务调度
 * 
 * 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列;pdPASS表示成功
 */
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
									  TickType_t xNewPeriod,
								      BaseType_t *pxHigherPriorityTaskWoken );

6、定时器ID

定时器的结构体如下,里面有一项 pvTimerID,它就是定时器ID:

怎么使用定时器ID,完全由程序来决定:

  • 可以用来标记定时器,表示自己是什么定时器
  • 可以用来保存参数,给回调函数使用

它的初始值在创建定时器时由 xTimerCreate() 这类函数传入,后续可以使用这些函数来操作:

  • 更新ID:使用 vTimerSetTimerID() 函数
  • 查询ID:查询 pvTimerGetTimerID() 函数

这两个函数不涉及命令队列,它们是直接操作定时器结构体。函数原型如下:

/** 获得定时器的ID
 * 
 * xTimer: 哪个定时器
 * 
 * 返回值: 定时器的ID
 */
void *pvTimerGetTimerID( TimerHandle_t xTimer );

/** 设置定时器的ID
 * 
 * xTimer: 哪个定时器
 * pvNewID: 新ID
 * 
 * 返回值: 无
 */
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );
  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值