目录
本章基于FreeRtos V9.0.0版本分析
一、软件定时器简介
硬件定时器受硬件的限制,数量有限,FreeRTOS提供了软件定时器来满足用户对定时器的需求量;软件定时器工作前设置好定时时间、回调函数,由定时器任务统一监视和处理。
软件定时器定时时间以时间片为单位,在精度上低于硬件定时器。
系统自带定时器任务prvTimerTask,为保证定时器实时性,定时器任务优先级设置为最高。
软件定时器功能上支持:定制(宏定义configUSE_TIMERS关闭)、创建、启动、停止、复位、删除等操作。
软件定时器类型上支持:单次模式、周期模式;
单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数,之后就将该定时器进入休眠状态,不再重新执行。
周期模式:这个定时器会按照设置的定时周期循环执行回调函数,直到用户将定时器停止或删除。
二、软件定时器结构
定时器有固定的结构,独立于任务存在;定时器任务通过双向链表统一管理所有定时器,其他任务和中断对定时器的操作,则通过队列进行命令传递。
2.1 定时器结构
xTimerListItem: 定时器链表条目,用于插入到链表,条目值xTimerListItem->xTimerListItem表示定时停止时间。
xTimerPeriodInTicks:定时器时间周期,单位为时间片,当用于循环定时器时,为定时器循环步长。
uxAutoReload:0.单次定时器,需要手动启动, 1.循环定时器,第一次需要手动启动,之后自动启动。
pvTimerID:标识定时器的ID,这允许在对多个计时器使用相同的回调时识别计时器。
pxCallbackFunction:定时器回调函数,定时时间到,在定时器任务prvTimerTask中执行,形参为定时器句柄,可以通过定时器中参数pvTimerID来区别多个计时器使用相同的回调,该函数需要迅速执行完成,不能有阻塞代码。
// 定时器回调函数订义
typedef void (*TimerCallbackFunction_t)( TimerHandle_t xTimer );
/* 软件定时器结构体. */
typedef struct tmrTimerControl
{
const char *pcTimerName; /*<< 定时器名称*/
ListItem_t xTimerListItem; /*<< 定时器链表条目*/
TickType_t xTimerPeriodInTicks;/*<< 定时器时器周期,单位为时间片*/
UBaseType_t uxAutoReload; /*<< 0.需要手动启动,单次定时器 1.自动启动,循环定时器. */
void *pvTimerID; /*<< 定时器的ID。在对多个定时器使用相同回调时识别定时器. */
TimerCallbackFunction_t pxCallbackFunction; /*<< 回调函数. */
// 可视化跟踪调试
#if( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber; /*<<用于跟踪的ID */
#endif
// 静态分配禁止释放标志
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;
#endif
} xTIMER;
2.2 双链表管理
链表结构和插入方式见《基于STM32F103ZE平台分析FreeRtos(二)——任务部分》章节。
定时器的链表管理方式与阻塞任务链表的管理方式一致。
条目插入方式:定时器链表按照条目值(xTimerListItem->xTimerListItem)从大到小的顺序插入链表,表头的条目值最小,表尾的条目值最大;条目值表示定时器定时结束时间。
链表管理方式: 定时器采用双链表管理的方式,主要原因是32位时间片计数器长期运行肯定会出现溢出翻转的现象(0xFFFFFFFF-->0x00000000),当定时器结束时间判断出翻转时,就必须使用一个备份的链表维护。初始化时,pxCurrentTimerList=&xActiveTimerList1; pxOverflowTimerList=&xActiveTimerList2;如果定时器定时结束时间没有出现翻转,定时器插入到链表pxCurrentTimerList维护,定时器定时结束时间出现翻转,定时器插入到链表pxOverflowTimerList维护。当全局时间片出现翻转(0xFFFFFFFF-->0x00000000)时,进行双链表切换:pxCurrentTimerList=&xActiveTimerList2; pxOverflowTimerList=&xActiveTimerList1;
如此交替使用。在定时器管理中,我只需要对工作链表指针pxCurrentTimerList管理即可。
// 定时器双链表管理
PRIVILEGED_DATA static List_t xActiveTimerList1; // 定时器链表1
PRIVILEGED_DATA static List_t xActiveTimerList2; // 定时器链表2
PRIVILEGED_DATA static List_t *pxCurrentTimerList; // 当前链表指针
PRIVILEGED_DATA static List_t *pxOverflowTimerList; // 溢出链表指针
// 微型条目
struct xMINI_LIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue; /*<数值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*<指向列表中下一个条目*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;/*<指向列表中前一个条目*/
};
// 条目
struct xLIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue /*<数值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*<指向列表中下一个条目。*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;/*<指向列表中前一个条目。*/
void * powner; /*<指向条目的对象(通常是TCB)的指针*/
void * configLIST_VOLATILE pvContainer; /*<指向该条目所在列表*/
};
// 链表
typedef struct xLIST
{
configLIST_VOLATILE UBaseType_t uxNumberOfItems; /*<条目个数*/
ListItem_t * configLIST_VOLATILE pxIndex; /*<条目位置指针,哨兵节点*/
MiniListItem_t xListEnd; /*<链表条目根节点*/
} List_t;
2.3 消息结构
定时器的操作采用消息队列的方式,消息队列xTimerQueue在定时器任务新建之前自动完成新建,消息队列长度由宏定义configTIMER_QUEUE_LENGTH控制,消息队列固定为FIFO模式。
消息结构可分为两种类型:定时器类型、回调函数类型,两种类型的数据结构通过共用体定义,使用时二选一。
// 消息结构
typedef struct tmrTimerQueueMessage
{
BaseType_t xMessageID; // 消息参数
union
{
TimerParameter_t xTimerParameters; // 定时器数据
// 函数挂起参数参数
#if ( INCLUDE_xTimerPendFunctionCall == 1 )
CallbackParameters_t xCallbackParameters; // 回调函数数据
#endif
} u;
} DaemonTaskMessage_t;
2.3.1 定时器消息结构
定时器消息必须指向一个定时器,定时器带有的回调函数以定时器句柄为形参。
// 定时器数据结构
typedef struct tmrTimerParameters
{
TickType_t xMessageValue; // 定时开始时间
Timer_t * pxTimer; // 定时器指针
} TimerParameter_t;
2.3.2 回调函数消息结构
回调函数消息可以带有2个形参(一个指针,一个无符号参数),无需定时器参与;定时器任务收到该消息后会直接执行回调函数。
// 回调函数指针定义
typedef void (*PendedFunction_t)( void *, uint32_t );
// 回调函数数据
typedef struct tmrCallbackParameters
{
PendedFunction_t pxCallbackFunction; /* << 回调函数指针. */
void *pvParameter1; /* << 回调函数形参1.指针型 */
uint32_t ulParameter2; /* << 回调函数形参2.无符号整形*/
} CallbackParameters_t;
2.4 消息类型
根据接口不同,消息结构可分为如下种类:回调函数、启动、复位、停止、变更周期、删除。
/* 定时器ID*/
/*-------------------------函数回调命令--------------------------------*/
#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 ( ( BaseType_t ) 5 )// 删除
/*-------------------------中断中定时器命令-----------------------------*/
#define tmrFIRST_FROM_ISR_COMMAND ( ( BaseType_t ) 6 )// 中断命令界限
#define tmrCOMMAND_START_FROM_ISR ( ( BaseType_t ) 6 )// 启动(中断)
#define tmrCOMMAND_RESET_FROM_ISR ( ( BaseType_t ) 7 )// 复位(中断)
#define tmrCOMMAND_STOP_FROM_ISR ( ( BaseType_t ) 8 )// 停止(中断)
#define tmrCOMMAND_CHANGE_PERIOD_FROM_ISR ( ( BaseType_t ) 9 )// 变更周期(中断)
三、软件定时器工作机制
在操作系统初始化时会创建好定时器任务,包括:定时器队列(xTimerQueue)初始化、定时器双链表初始化、定时器任务(prvTimerTask)创建。
定时器的创建是独立于任务模块存在,设置好定时器的周期、回调函数、定时模式等,线程或中断通过接口操作定时器,如:启动、停止、复位、变更周期等;操作接口将各类操作以消息的形式发送到定时器消息队列xTimerQueue。
定时器任务prvTimerTask接收并解析消息,对于函数回调消息会直接执行,对于定时器消息,解析出定时器、定时时间,并根据消息类型对定时器进行启动、复位、停止、删除、修改周期的操作。
定时器启动\复位操作:重置定时器后,对于定时时间已过的情况,执行回调函数后重新发消息到队列;对于时间未到的定时器,将定时器条目插入到双链表管理。
定时器停止操作:将定时器条目从链表移除,不会在执行其回调函数。
定时器修改周期操作:将定时器条目从链表移除,变更周期后,以当前时间为基准,重置定时器,并插入到双链表管理。
定时器任务对消息处理完成后,会对链表进行管理,如果链表中有定时器时间到了,任务会从链表移除定时器条目,并执行其回调函数;对于循环定时器,执行完回调后根据周期重置定时器,并插入双链表。
定时器任务阻塞:当链表中定时器未到时间时,任务会再去处理消息,如果消息队列也是空的,任务会进入阻塞状态:如果链表中没有定时器,任务永久阻塞,直到有消息发送;如果链表中有定时器,阻塞时间根据最近定时时间和当前时间的差计算。
逻辑图:
四、软件定时器接口
4.1 定时器接口
定时器接口主要是定时器新建和各个属性获取、设置。
// 创建定时器(动态)
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )
#endif
// 创建定时器(静态)
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t *pxTimerBuffer )
#endif
// 获取定时器ID
void *pvTimerGetTimerID( const TimerHandle_t xTimer)
// 设置定时器ID
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID)
// 获取定时器名称
const char * pcTimerGetName( TimerHandle_t xTimer)
// 获取定时器周期
TickType_t xTimerGetPeriod( TimerHandle_t xTimer)
// 获取定时器停止状态(单次定时器到期、或者停止定时器)
BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer)
4.2 定时器创建
定时器有两种创建接口:动态定时器创建、静态定时器创建;与任务创建类似。静态创建即通过全局变量存储,不能释放的;动态创建为动态内存,可以生灭;本章我们仅以动态创建为例进行分析。
4.3 获取\设置定时器ID
在不同定时器调用同一回调函数时,设置该属性,用于区分不同的定时器。
/*----------------------获取定时器ID-------------------------------------*/
void *pvTimerGetTimerID( const TimerHandle_t xTimer )
{
Timer_t * const pxTimer = ( Timer_t * ) xTimer;
void *pvReturn;
configASSERT( xTimer );
// 进入临界区
taskENTER_CRITICAL();
{
pvReturn = pxTimer->pvTimerID;
}
// 退出临界区
taskEXIT_CRITICAL();
return pvReturn;
}
/*-------------------------设置定时器ID----------------------------------*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID )
{
Timer_t * const pxTimer = ( Timer_t * ) xTimer;
configASSERT( xTimer );
// 进入临界区
taskENTER_CRITICAL();
{
pxTimer->pvTimerID = pvNewID;
}
// 退出临界区
taskEXIT_CRITICAL();
}
4.4 获取定时器名称
主要方便监视定时器。
/*-----------------获取定时器名称--------------------------------*/
const char * pcTimerGetName( TimerHandle_t xTimer )
{
Timer_t *pxTimer = ( Timer_t * ) xTimer;
configASSERT( xTimer );
return pxTimer->pcTimerName;
}
4.5 获取定时器周期
/*---------------------获取定时器周期--------------------------------------*/
TickType_t xTimerGetPeriod( TimerHandle_t xTimer )
{
Timer_t *pxTimer = ( Timer_t * ) xTimer;
configASSERT( xTimer );
return pxTimer->xTimerPeriodInTicks;
}
4.6 获取定时器运行态
获取定时器运行态是通过定时器条目是否在链表中来判断的,在任务中启动定时器后,由于定时器任务优先级高,会先解析启动命令,并把定时器插入到链表中,即运行态。
单次定时器的启动或定时器删除命令可以通过该接口先判断是否非运行态。
循环定时器启动后会一直处于运行态,直到定时器停止或删除。 单次定时器运行之后,就退出运行态。
中断中使用该函数需要考虑好,如果中断中执行定时器启动命令,由于中断优先级高,使用该接口会误判的可能。
/*---------------------检查定时器是否生效中--------------------------------------*/
BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer )
{
BaseType_t xTimerIsInActiveList;
Timer_t *pxTimer = ( Timer_t * ) xTimer;
configASSERT( xTimer );
/*定时器是否在时间到的列表? */
taskENTER_CRITICAL();
{
xTimerIsInActiveList = ( BaseType_t ) !( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) );
}
taskEXIT_CRITICAL();
return xTimerIsInActiveList;
}
/*----------
五、软件定时器操作接口
定时器操作接口主要包括定时器启动、复位、停止、删除、周期修改和回调函数;这些接口均通过发送消息至定时器队列xTimerQueue;由定时器任务prvTimerTask统一解析消息和执行回调函数。
// 定时器启动接口(线程)
#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
// 定时器停止接口(线程)
#define xTimerStop( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, ( xTicksToWait ) )
// 定时器周期修改接口(线程)
#define xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD, ( xNewPeriod ), NULL, ( xTicksToWait ) )
// 定时器删除接口(线程)
#define xTimerDelete( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_DELETE, 0U, NULL, ( xTicksToWait ) )
// 定时器启动接口(中断)
#define xTimerReset( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
// 定时器启动接口(中断)
#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )
// 定时器停止接口(中断)
#define xTimerStopFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP_FROM_ISR, 0, ( pxHigherPriorityTaskWoken ), 0U )
// 定时器周期修改接口(中断)
#define xTimerChangePeriodFromISR( xTimer, xNewPeriod, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, ( xNewPeriod ), ( pxHigherPriorityTaskWoken ), 0U )
// 定时器复位接口(中断)
#define xTimerResetFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )
// 回调函数接口(中断)
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken );
// 回调函数接口(线程)
BaseType_t xTimerPendFunctionCall( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, TickType_t xTicksToWait );
// 消息发送接口(线程\中断)
BaseType_t xTimerGenericCommand( TimerHandle_t xTimer, const BaseType_t xCommandID, const TickType_t xOptionalValue, BaseType_t * const pxHigherPriorityTaskWoken, const TickType_t xTicksToWait );
5.1 消息发送接口
// 消息发送接口
BaseType_t xTimerGenericCommand( TimerHandle_t xTimer, const BaseType_t xCommandID, const TickType_t xOptionalValue, BaseType_t * const pxHigherPriorityTaskWoken, const TickType_t xTicksToWait );
形参:
xTimer:定时器;
xCommandID:消息命令;
xOptionalValue:定时器起始时间;
*pxHigherPriorityTaskWoken:高优先级任务释放标志;
xTicksToWait:任务阻塞时间;
阻塞机制:
线程模式下,调度器如果在运行态,阻塞时间为xTicksToWait;调度器如果在暂停态,无阻塞(防止死锁); 定时器任务在定时器处理过程中可能会发送消息,此过程需要暂停定时器,所以该过程发送的消息无阻塞现象。
中断模式下,调用中断发送消息接口,无阻塞。
代码详解:
BaseType_t xTimerGenericCommand( TimerHandle_t xTimer, const BaseType_t xCommandID, const TickType_t xOptionalValue, BaseType_t * const pxHigherPriorityTaskWoken, const TickType_t xTicksToWait )
{
BaseType_t xReturn = pdFAIL;
DaemonTaskMessage_t xMessage;
configASSERT( xTimer );
if( xTimerQueue != NULL )
{
// 【1】创建消息
xMessage.xMessageID = xCommandID;
xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;
xMessage.u.xTimerParameters.pxTimer = ( Timer_t * ) xTimer;
// 【2】线程发送消息
if( xCommandID < tmrFIRST_FROM_ISR_COMMAND )// 任务命令
{
// 【2.1】调度器运行态,阻塞时间为xTicksToWait
if( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING )
{
xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait );
}
// 【2.1】调度器暂停态,为防止死锁,阻塞时间为0,
else
{
xReturn = xQueueSendToBack( xTimerQueue, &xMessage, tmrNO_DELAY );
}
}
// 【2】中断发送消息
else
{
xReturn = xQueueSendToBackFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
}
traceTIMER_COMMAND_SEND( xTimer, xCommandID, xOptionalValue, xReturn );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xReturn;
}
5.2 定时器启动
启动定时器复用消息发送函数,线程发送命令固定为tmrCOMMAND_START,中断发送命令固定为tmrCOMMAND_START_FROM_ISR,发送的定时起始时间固定为接口调用时的全局时间片计数xTickCount。
线程模式下的阻塞时间xTicksToWait 可设。
// 启动定时器(线程)
#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
// 启动定时器(中断)
#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )
5.3 定时器复位
定时器复位操作复用消息发送函数,线程发送命令为tmrCOMMAND_RESET,中断发送命令为tmrCOMMAND_RESET_FROM_ISR,发送的定时起始时间固定为接口调用时全局时间片计数xTickCount,与定时器启动一致。
线程模式下的阻塞时间xTicksToWait 可设。
// 复位定时器(线程)
#define xTimerReset( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
// 复位定时器(中断)
#define xTimerResetFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )
5.4 定时器停止
定时器停止接口复用消息发送函数,线程发送命令为tmrCOMMAND_STOP,中断发送命令为tmrCOMMAND_STOP_FROM_ISR,定时起始时间复位为0。
线程模式下的阻塞时间xTicksToWait 可设。
// 定时器停止(线程)
#define xTimerStop( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, ( xTicksToWait ) )
// 定时器停止(中断)
#define xTimerStopFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP_FROM_ISR, 0, ( pxHigherPriorityTaskWoken ), 0U )
5.5 定时器周期修改
定时器周期修改接口复用消息发送函数,线程发送命令为tmrCOMMAND_CHANGE_PERIOD,中断发送命令为tmrCOMMAND_CHANGE_PERIOD_FROM_ISR。
定时起始时间是定时器任务解析时获取全局时间片计数xTickCount;新修改的周期为xNewPeriod(非0);新的定时结束时间为xTickCount+xNewPeriod。
// 定时器周期修改接口(线程)
#define xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD, ( xNewPeriod ), NULL, ( xTicksToWait ) )
// 定时器周期修改接口(中断)
#define xTimerChangePeriodFromISR( xTimer, xNewPeriod, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, ( xNewPeriod ), ( pxHigherPriorityTaskWoken ), 0U )
5.6 定时器删除
定时器删除接口只能在线程中调用,复用消息发送函数,线程发送命令为tmrCOMMAND_DELETE,定时器任务接收到该命令后会释放定时器内存,调用该接口最好确保该定时器已经停止。
// 定时器删除接口(线程)
#define xTimerDelete( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_DELETE, 0U, NULL, ( xTicksToWait ) )
5.7 回调函数接口
回调函数接口也是向定时器队列xTimerQueue发送消息,定时器任务解析出该消息会立刻执行回调函数,回调函数不能有阻塞的代码。
与定时器消息相比较,该接口会立刻执行回调,并且回调函数可以携带2个参数,而定时器中的回调函数的形参是定时器句柄。
// 回调函数接口(中断)
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken )
{
DaemonTaskMessage_t xMessage;
BaseType_t xReturn;
xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;
xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;
xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;
xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;
xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );
return xReturn;
}
// 回调函数接口(线程)
BaseType_t xTimerPendFunctionCall( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, TickType_t xTicksToWait )
{
DaemonTaskMessage_t xMessage;
BaseType_t xReturn;
configASSERT( xTimerQueue );
// 发送至任务
xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK;
xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;// 函数回调
xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1; // 参数1
xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2; // 参数2
xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait ); // 发送
tracePEND_FUNC_CALL( xFunctionToPend, pvParameter1, ulParameter2, xReturn );
return xReturn;
}
六、定时器任务
6.1 定时器任务创建
定时器任务由系统自带,可定制(宏定义configUSE_TIMERS投退)。
变量初始化:创建定时器任务前先初始化全局变量,主要有两部分:双向链表初始化、消息队列初始化,消息队列长度在系统中通过宏定义configTIMER_QUEUE_LENGTH设置,固定为先入先出(FIFO)模式。
// 定时器任务全局变量初始化
static void prvCheckForValidListAndQueue( void )
{
//【1】进入临界区
taskENTER_CRITICAL();
{
if( xTimerQueue == NULL )
{
// 【2】双链表初始化
vListInitialise( &xActiveTimerList1 );
vListInitialise( &xActiveTimerList2 );
pxCurrentTimerList = &xActiveTimerList1;
pxOverflowTimerList = &xActiveTimerList2;
// 【3】消息队列初始化
xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );
// 【4】队列注册表【选配】
#if ( configQUEUE_REGISTRY_SIZE > 0 )
{
if( xTimerQueue != NULL )
{
vQueueAddToRegistry( xTimerQueue, "TmrQ" );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
【5】退出临界区
taskEXIT_CRITICAL();
}
定时器任务创建:
任务入口函数:prvTimerTask
任务名称:“Tmr Svc”
任务优先级:(configMAX_PRIORITIES-1) ,即最高优先级。
任务句柄:xTimerTaskHandle
// 动态任务创建,优先级最高,入口函数prvTimerTask
xReturn = xTaskCreate( prvTimerTask,"Tmr Svc",configTIMER_TASK_STACK_DEPTH,NULL,( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,&xTimerTaskHandle );
6.2 任务源码
整体分析:任务整体可分为2个模块:定时器处理、消息解析;源码如下所示,逻辑图可参照【三、软件定时器工作机制】章节。
/*----------------定时器任务入口-------------------------------*/
static void prvTimerTask( void *pvParameters )
{
TickType_t xNextExpireTime; // 下个定时时刻
BaseType_t xListWasEmpty; // 当前链表为空标志
( void ) pvParameters; // 任务参数
// 任务钩子,启动时调用
#if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
{
extern void vApplicationDaemonTaskStartupHook( void );
vApplicationDaemonTaskStartupHook();
}
#endif
for( ;; )
{
/*获取最近定时器的时间片计数*/
xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
/*定时器处理*/
prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
/*消息队列解析.*/
prvProcessReceivedCommands();
}
}
6.3 定时器处理模块
6.3.1 源码分析
1.定时器处理先暂停调度;
2.全局时间片计数翻转处理:
(1)发生时间翻转时,说明当前链表pxCurrentTimerList中所有定时器均应响应,按从小到大顺序依次执行pxCurrentTimerList中所有的定时器回调函数,并移除执行完成的定时器。
(2)对于循环定时器,需要重置定时器结束时间,如果重置后时间在翻转时间前,则需要将该重置后的定时器重新插入到当前链表pxCurrentTimerList,会按顺序再次执行,这样保证定时器不丢失,并且顺序正确。如果重置后时间翻转后,则将该定时器通过消息队列发送,由消息队列处理模块处理,不能在该模块下处理。
(3)当前链表中所有的定时器回调函数均执行完成后,双链表指针交换指向位置。
List_t *pxTemp = pxCurrentTimerList;
pxCurrentTimerList = pxOverflowTimerList;
pxOverflowTimerList = pxTemp;
3.全局时间片没有发生翻转:
(1)从当前链表pxCurrentTimerList检测定时器定时时间是否到,如果有定时结束的,执行该定时器回调函数,执行完成后移除该定时器。
(2)对于执行完成的循环定时器,需要重置,如果重置后定时结束时间在当前时间之前,则将该定时器通过消息队列发送,命令为tmrCOMMAND_START_DONT_TRACE(直接启动),由消息队列处理模块处理,防止顺序乱套,不能在该模块下处理。如果重置后时间在当前时间之后,则需要将该重置后的定时器重新插入到定时器链表(可能是当前链表pxCurrentTimerList,也可能是溢出链表pxOverflowTimerList)。
4.如果此时两个链表均为空,消息队列也为空,则永久阻塞定时器任务(挂起任务),由消息发送接口去解锁。
5.如果链表非空,消息队列为空,则阻塞定时器任务,阻塞时间为最近定时结束时间与当前时间的差。
6.恢复调度器。
/*-----------------------------------------------------------*/
// 进程定时器或块任务
// xNextExpireTime最近时间 xListWasEmpty链表空
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, BaseType_t xListWasEmpty )
{
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;
vTaskSuspendAll();//暂停任务
{
// 定时器翻转判断
xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
// 未翻转
if( xTimerListsWereSwitched == pdFALSE )
{
// 最近定时器时间到
if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
{
( void ) xTaskResumeAll();
prvProcessExpiredTimer( xNextExpireTime, xTimeNow );
}
// 时间未到或链表空,阻塞
else
{
// 当前链表空,改判溢出队列是否为空
if( xListWasEmpty != pdFALSE )
{
xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );
}
// 均为空则无限期等待消息
vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );
// 恢复任务
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
// 翻转,直接跳过
else
{
( void ) xTaskResumeAll();
}
}
}
6.3.2 逻辑图分析
6.4 消息解析模块
6.4.1 源码分析
1.模块无阻塞接收定时器队列xTimerQueue中的所有消息。
2.消息处理:
(1)函数回调消息,直接执行回调函数,函数带有2个参数。
(2)启动、复位类消息,取全局时间片计数器,判断定时器结束时间是否到,如果到了会直接执行定时器回调函数,并且对循环定时器重置后(定时停止时间+周期),重新发送启动消息,这样做可以保证执行顺序,如果定时器重置后时间任然在当前时间之前,会在循环接收消息中处理,不会妨碍处理顺序。如果定时时间没到,将定时器插入到定时器链表(可能是当前链表pxCurrentTimerList,也可能是溢出链表pxOverflowTimerList),由定时器处理模块处理。
(3)停止类,将定时器从链表移除即可。
(4)变更周期,将定时器周期进行修改,并把定时定时停止时间修改为当前时间片+新周期,将定时器插入到链表(可能是当前链表pxCurrentTimerList,也可能是溢出链表pxOverflowTimerList)。
(3)删除类,将定时器内存释放。
// 进程接收命令处理
static void prvProcessReceivedCommands( void )
{
DaemonTaskMessage_t xMessage;
Timer_t *pxTimer;
BaseType_t xTimerListsWereSwitched, xResult;
TickType_t xTimeNow;
//【1】无阻塞接收,快速处理
while( xQueueReceive( xTimerQueue, &xMessage, tmrNO_DELAY ) != pdFAIL )
{
// 回调函数命令
#if ( INCLUDE_xTimerPendFunctionCall == 1 )
{
/*负命令是挂起的函数调用,而不是定时器命令。*/
if( xMessage.xMessageID < ( BaseType_t ) 0 )
{
// 回调函数
const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );
configASSERT( pxCallback );
pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_xTimerPendFunctionCall */
/* 定时器命令. */
if( xMessage.xMessageID >= ( BaseType_t ) 0 )
{
pxTimer = xMessage.u.xTimerParameters.pxTimer;
// 定时器在链表中;定时器已经在链表中,从链表移除定时器,删除命令时,此处已处理,后后面不再处理
if( listIS_CONTAINED_WITHIN( NULL,&( pxTimer->xTimerListItem )) == pdFALSE )
{
// 移除
( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );
// 最新时间,并判断是否溢出
xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
// 消息命令处理
switch( xMessage.xMessageID )
{
// 启动、复归类(消息数据表示起始时间)
case tmrCOMMAND_START :
case tmrCOMMAND_START_FROM_ISR :
case tmrCOMMAND_RESET :
case tmrCOMMAND_RESET_FROM_ISR :
case tmrCOMMAND_START_DONT_TRACE :
// 添加链表到相应位置,数据越小越靠前
if( prvInsertTimerInActiveList( pxTimer, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE )
{
/* 计时器在被添加到活动计时器列表之前已经过期。现在处理它. */
pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );// 回调
traceTIMER_EXPIRED( pxTimer );
// 自动循环,发送命令
if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
{
xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, NULL, tmrNO_DELAY );
configASSERT( xResult );
( void ) xResult;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
break;
// 停止类
case tmrCOMMAND_STOP :
case tmrCOMMAND_STOP_FROM_ISR :
/*进入该函数会先进行移除,故此处不再移除. */
break;
// 变更周期
case tmrCOMMAND_CHANGE_PERIOD :
case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR :
pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;
configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );
( void ) prvInsertTimerInActiveList( pxTimer, ( xTimeNow + pxTimer->xTimerPeriodInTicks ), xTimeNow, xTimeNow );
break;
// 删除(释放动态)
case tmrCOMMAND_DELETE :
#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
{
vPortFree( pxTimer );
}
#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
{
if( pxTimer->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
{
vPortFree( pxTimer );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
break;
default :
/* Don't expect to get here. */
break;
}
}
}
}
6.4.2 逻辑图分析
七、应用实例
7.1 函数回调(无定时器)
#define configUSE_TIMERS 1
// 数据刷新接口
uint32 data[100];
uint32 num=100;
void vDataRef(Uint32 *pdate,uint32 index);
// 任务A对数据进行采集
void taskA()
{
for(;;)
{
// 回调函数
xTimerPendFunctionCall( vDataRef,data ,num,1000);
vTaskDelay(1000);
}
}
void main()
{
......
//启动调度器(定时器任务在调动中自动启动)
vTaskStartScheduler();
}
7.2 函数定时单次回调
#define configUSE_TIMERS 1
// 定时器
TimerHandle_t pxTimer;
// 数据刷新接口
void vDataRef(TimerHandle_t xTimer);
// 任务A对数据进行采集
void taskA()
{
xTimerStart( pxTimer, 1000);
for(;;)
{
// 启动定时器(启动前先判断状态)
if(!xTimerIsTimerActive(pxTimer))
xTimerStart( pxTimer, 1000);
}
}
void main()
{
//新建单次定时器,回调函数vDataRef,定时器周期1000时间片
pxTimer=xTimerCreate("Timer1",1000,0,NULL,vDataRef);
// 新建任务A
......
//启动调度器(定时器任务在调动中自动启动)
vTaskStartScheduler();
}
7.3 函数定时循环回调
#define configUSE_TIMERS 1
// 定时器
TimerHandle_t pxTimer;
// 数据刷新接口
void vDataRef(TimerHandle_t xTimer);
// 任务A对数据进行采集
void taskA()
{
// 启动定时器(循环定时器启动一次即可)
xTimerStart( pxTimer, 1000);
for(;;)
{
}
}
void main()
{
// 新建循环定时器,回调函数vDataRef,定时器周期1000时间片
pxTimer=xTimerCreate("Timer1",1000,1,NULL,vDataRef);
// 新建任务A
......
//启动调度器(定时器任务在调动中自动启动)
vTaskStartScheduler();
}