文章目录
0x01 软件定时器
定时器概念: 定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。
- 硬件定时器:硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式,用户在中断中处理信息。
- 软件定时器:软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。需要我们创建的软件定时器时指定时间到达后要调用的函数(也称为超时函数/回调函数)中处理信息。
软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。定时精度与系统时钟的周期有关。
一般系统利用 SysTick 作为软件定时器的基础时钟,软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况。
RTOS中的软件定时器支持单次模式和周期模式:
- 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器进入休眠状态,不再重新执行。
- 周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除。
FreeRTOS 通过一个 prvTimerTask 任务(也叫守护任务 Daemon)管理软定时器,它是在启动调度器时自动创建的,为了满足用户定时需求。要使用软件定时器需要将FreeRTOSConfig.h中的宏定义configUSE_TIMERS设置为1。开启这个宏也需要开启configTIMER_TASK_PRIORITY、configTIMER_QUEUE_LENGTH、configTIMER_TASK_STACK_DEPTH这些宏,否则编译会报如下错误:
#if configUSE_TIMERS == 1
#ifndef configTIMER_TASK_PRIORITY
#error If configUSE_TIMERS is set to 1 then configTIMER_TASK_PRIORITY must also be defined.
#endif /* configTIMER_TASK_PRIORITY */
#ifndef configTIMER_QUEUE_LENGTH
#error If configUSE_TIMERS is set to 1 then configTIMER_QUEUE_LENGTH must also be defined.
#endif /* configTIMER_QUEUE_LENGTH */
#ifndef configTIMER_TASK_STACK_DEPTH
#error If configUSE_TIMERS is set to 1 then configTIMER_TASK_STACK_DEPTH must also be defined.
#endif /* configTIMER_TASK_STACK_DEPTH */
#endif /* configUSE_TIMERS */
-
configTIMER_TASK_PRIORITY定义的是定时器任务的优先级,值可以是0~(configMAX_PRIORITIES-1),这个值定义的就是定时器任务prvTimerTask的优先级; -
configTIMER_QUEUE_LENGTH定义的是定时器队列的长度,也就是能定义几个软件定时器; -
configTIMER_TASK_STACK_DEPTH定义的是定时器任务的堆栈大小,图中定义的是256字,这个堆栈也是定时器回调函数会使用的堆栈,所以不能定义得太小。
这里使用预编译指令#if(依据),#error(提示信息)。这个可以学起来。如果报错没有加入这几个宏定义,可以自己加入:
#define configTIMER_TASK_PRIORITY (2)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 256
加入后如果报错: L6218E: Undefined symbol vApplicationGetTimerTaskMemory (referred from timers.o).,可以检查自己的宏configSUPPORT_STATIC_ALLOCATION是否置位,需要更改为0:

应用场景
需要定时任务,但是硬件定时器受到硬件的限制,数量上不足以满足其要求,其缺陷是容易被其他中断打断,因为软件定时器执行的上下文环境为任务,所以软件定时器适用于对时间精度要求不高的任务,一些辅助型的任务。
定时器精度
在操作系统中,通常软件定时器以系统节拍周期为计时单位。系统节拍配置为 configTICK_RATE_HZ,表示系统时钟频率,1s跳动了多少次,该宏在 FreeRTOSConfig.h 中有定义,默认是 1000,则系统时钟节拍周期就为1ms。软件定时器的所定时数值必须为这个节拍周期的整倍数。由于节拍定义了系统中定时器能够分辨的精确度,系统可以根据实际系统 CPU 的处理能力和实时性需求设置合适的数值,系统节拍周期的值越小,精度越高,但是系统开销也将越大,因为这代表在 1 秒中系统进入时钟中断的次数也就越多。
运作机制
软件定时器是可选的系统资源,在创建定时器的时候就会分配一块内存空间。当用户创建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表,FreeRTOS 中采用两个定时器列表维护软件定时器,pxCurrentTimerList 与pxOverflowTimerList是列表指针,在初始化的时候分别指向 xActiveTimerList1 与 xActiveTimerList2:
PRIVILEGED_DATA static List_t xActiveTimerList1 = {
0};
PRIVILEGED_DATA static List_t xActiveTimerList2 = {
0};
PRIVILEGED_DATA static List_t *pxCurrentTimerList = NULL;
PRIVILEGED_DATA static List_t *pxOverflowTimerList = NULL;
-
pxCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。
-
pxOverflowTimerList 列表是在软件定时器溢出的时候使用,作用与 pxCurrentTimerList一致。
软件定时器可以采用消息队列进行通讯,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。
假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中,所以,在定时器启动函数中,FreeRTOS 是采用队列的方式发送一个消息给软件定时器任务,任务被唤醒从而执行接收到的命令。
添加软件定时器任务:
系统当前时间 xTimeNow 值为 0,注意:xTimeNow 其实是一个局部变量,是根据 xTaskGetTickCount()函数获取的,实际它的值就是全局变量 xTickCount 的值,下文都采用它表示当前系统时间。在当前系统中已经创建并启动了 1 个定时器 Timer1;系统继续运行,当系统的时间 xTimeNow 为 20 的时候,用户创建并且启动一个定时时间为 100 的定时器 Timer2,此 时 Timer2 的 溢出 时间 xTicksToWait 就 为定 时时间 +系统 当前时 间(100+20=120),然后将 Timer2 按 xTicksToWait 升序插入软件定时器列表中;假设当前系统时间 xTimeNow 为 40 的时候,用户创建并且启动了一个定时时间为 50 的定时器Timer3 , 那 么 此 时 Timer3 的 溢 出 时 间 xTicksToWait 就 为 40+50=90 , 同 样 安 装xTicksToWait 的数值升序插入软件定时器列表中:
那么系统如何处理软件定时器列表?系统在不断运行,而 xTimeNow(xTickCount)随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,xTimeNow 变量会加 1),在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间xTimeNow 是否大于或等于下一个定时器唤醒时间 xTicksToWait,若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。执行完对应的回调函数后,会将其从软件定时器列表中删除,如果这个定时器是周期性的,则系统会根据Timer下一次唤醒时间重新将定时器添加到软件定时器列表中,也是根据时间来进行升序排序。
使用软件定时器需要注意:

软件定时器控制模块
typedef struct tmrTimerControl
{
const char *pcTimerName; //软件定时器名字,用于调试的,一般通过其句柄来使用
ListItem_t xTimerListItem; //软件定时器列表项,用于插入定时器列表
TickType_t xTimerPeriodInTicks;//软件定时器的周期
UBaseType_t uxAutoReload; //pdFALSE为单次,pdTRUE为周期
void *pvTimerID; //软件定时器ID,在回调中根据ID来执行任务
TimerCallbackFunction_t pxCallbackFunction; //回调函数
#if( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber; /*<< An ID assigned by trace tools such as FreeRTOS+Trace */
#endif
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; //标记定时器使用的内存,删除时判断是否需要释放内存。
#endif
} xTIMER;
函数接口
软件定时器的很多 API 函数通过一个名字叫“定时器命令队列”的队列来给定时器守护任务发送命令。该定时器命令队列由 RTOS 内核提供,且应用程序不能够直接访问,其消息队列的长度由宏 configTIMER_QUEUE_LENGTH 定义。
xTimerCreate()
创建软件定时器函数,软件定时器在创建成功后是处于休眠状态的,可以使用 xTimerStart()、xTimerReset()、 xTimerStartFromISR() 、 xTimerResetFromISR() 、 xTimerChangePeriod() 和 xTimerChangePeriodFromISR()这些函数将其状态转换为活跃态。
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
TimerHandle_t xTimerCreate( const char * const pcTimerName, //软件定时器名字
const TickType_t xTimerPeriodInTicks, //软件定时器周期
const UBaseType_t uxAutoReload, //设置周期or单次模式
void * const pvTimerID, //软件定时器ID
TimerCallbackFunction_t pxCallbackFunction ) //回调函数
{
Timer_t *pxNewTimer;
// 申请内存
pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );
if( pxNewTimer != NULL )
{
//内存申请成功,进行初始化软件定时器
prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
/* Timers can be created statically or dynamically, so note this
timer was created dynamically in case the timer is later
deleted. */
pxNewTimer->ucStaticallyAllocated = pdFALSE;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
}
return pxNewTimer;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
- xTimerPeriodInTicks:软件定时器的周期,单位为系统节拍周期(即 tick)。使用pdMS_TO_TICKS()可以把时间单位从 ms 转换为系统节拍周期。如果软件定时器的周期为100个 tick,那么只需要简单的设置 xTimerPeriod 的值为 100 即可。如果软件定时器的周期为 500ms,那么 xTimerPeriod 应设置为 pdMS_TO_TICKS(500)。宏 pdMS_TO_TICKS()只有当 configTICK_RATE_HZ 配置成小于或者等于 1000HZ 时才可以使用。
prvInitialiseNewTimer()
初始化一个软件定时器。
static void prvInitialiseNewTimer( const char * const pcTimerName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
Timer_t *pxNewTimer )
{
//断言,判断定时器的周期是否大于0
configASSERT( ( xTimerPeriodInTicks > 0 ) );
if( pxNewTimer != NULL )
{
//初始化软件定时器列表与创建软件定时器消息队列
prvCheckForValidListAndQueue();
//初始化软件定时信息,这些信息保存在软件定时器控制块中
pxNewTimer->pcTimerName = pcTimerName;
pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;
pxNewTimer->uxAutoReload = uxAutoReload;
pxNewTimer->pvTimerID = pvTimerID;
pxNewTimer->pxCallbackFunction = pxCallbackFunction;
vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );
traceTIMER_CREATE( pxNewTimer );
}
}
在 prvCheckForValidListAndQueue()函数中系统将初始化软件定时器列表与创建软件定时器消息队列,也叫“定时器命令队列”,因为在使用软件定时器的时候,用户是无法直接控制软件定时器的,必须通过“定时器命令队列”向软件定时器发送一个命令,软件定时器任务被唤醒就去执行对应的命令操作。其中调用了xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );
xTimerStart()
软件定时器启动函数,上面只是创建,处于休眠状态,需要使用这个函数来使其开始工作。系统开始运行时,会自动创建一个任务prvTimerTask,如果没有运行中的定时器,则会进入等待阻塞的状态,启动函数就是通过“定时器命令队列”向定时器任务发送一个启动命令,定时器任务获得命令就解除阻塞,然后执行启动软件定时器命令。
#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), //定时器句柄
tmrCOMMAND_START, ///启动命令
( xTaskGetTickCount() ), //获取当前系统时间
NULL, //中断发送命令才有用,其他时NULL
( xTicksToWait ) ) //超时阻塞时间
xTicksToWait:用户指定超时阻塞时间,单位为系统节拍周期(即 tick)。调用xTimerStart()的任务将被锁定在阻塞态,在软件定时器把启动的命令成功发送到定时器命令队列之前。如果在 FreeRTOS 调度器开启之前调用 xTimerStart(),形参将不起作用。命令有如下:
#define tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR ( ( BaseType_t ) -2 )
#define tmrCOMMAND_EXECUTE_CALLBACK ( ( BaseType_t ) -1 )
#define tmrCOMMAND_START_DONT_TRACE ( ( BaseType_t ) 0 )
#define tmrCOMMAND_START ( ( BaseType_t ) 1 )
#define tmrCOMMAND_RESET ( ( BaseType_t ) 2 )
#define tmrCOMMAND_STOP ( ( BaseType_t ) 3 )
#define tmrCOMMAND_CHANGE_PERIOD ( ( BaseType_t ) 4 )
#define tmrCOMMAND_DELETE (

文章详细介绍了FreeRTOS操作系统中软件定时器的工作原理、API函数以及使用注意事项,包括定时器的创建、启动、停止和删除。同时,讨论了任务通知的概念,包括发送和接收通知的函数,以及它们在中断环境中的版本。这些机制在实时操作系统中用于实现轻量级的任务同步和通信。


1438

被折叠的 条评论
为什么被折叠?



