目录
软件定时器
在FreeRTOS中可以设置无数个软件定时器,都是基于系统滴答中断。
使用软件定时器需要指定时间:启动定时器和运行回调函数。启动定时器和运行回调函数的间隔为定时器的周期。
使用软件定时器需要指定类型:一次性(回调函数只被调用一次,可手动再次启动)或自动加载(回调函数间歇调用)。
使用软件定时器需要指定事件:指定回调函数。
守护任务
FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行。定时器函数一般在中断里执行,如在中断中判断定时器是否超时,如果超时就调用回调函数。
但FreeRTOS是RTOS,不允许在内核、中断中执行不确定的代码(如果定时器函数很耗时会影响整个系统)。所以FreeRTOS中,不在Tick中断中执行定时器函数。
而是在RTOS Damemon Task(RTOS守护任务)里执行。当FreeRTOS配置项configUSE_TIMERS被设置为1,在启动调度器时会自动创建RTOS守护任务。
我们编写的任务函数要使用定时器时,是通过定时器命令队列(timer command queue)和守护任务交互。
守护任务的优先级为:configTIMER_TASK_PRIORITY,定时器命令队列长度为configTIMER_QUEUE_LENGTH。
当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:
处理命令:从命令队列里取出命令、处理。
执行定时器的回调函数。
能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。
/* 定时器的回调函数 */ void ATimerCallback( TimerHandle_t xTimer );
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。所以,定时器的回调函数不能影响其他任务:
回调函数要尽快执行,不能进入阻塞状态。
不用调用会导致阻塞的API函数,如vTaskDelay()。
可以调用xQueueReceive()等函数,但是超时时间要设为0,不阻塞。
创建定时器
TimerHandle_t xTimerCreate( const char * const pcTimerName, // 定时器名字
const TickType_t xTimerPeriodInTicks, // 定时器周期, 以Tick为单位
const UBaseType_t uxAutoReload, // 定时器是否自动重装载, pdTRUE表示自动加载, pdFALSE表示一次性
void * const pvTimerID, // 回调函数可以使用此参数, 比如分辨是哪个定时器
TimerCallbackFunction_t pxCallbackFunction ); // 回调函数
/* 返回值: 成功则返回TimerHandle_t, 否则返回NULL */
TimerHandle_t xTimerCreateStatic( const char * const pcTimerName, // 定时器名字
TickType_t xTimerPeriodInTicks, // 定时器周期, 以Tick为单位
UBaseType_t uxAutoReload, // 定时器是否自动重装载, pdTRUE表示自动加载, pdFALSE表示一次性
void * pvTimerID, // 回调函数可以使用此参数, 比如分辨是哪个定时器
TimerCallbackFunction_t pxCallbackFunction, // 回调函数
StaticTimer_t *pxTimerBuffer ); // 传入一个StaticTimer_t结构体, 将在结构体构造定时器
/* 返回值: 成功则返回TimerHandle_t, 否则返回NULL */
void ATimerCallback( TimerHandle_t xTimer );
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );
删除定时器
动态分配的定时器,不再需要时可以删除以回收内存。
/*
* xTimer: 要删除哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"删除命令"在指定超时时间内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
定时器的很多API函数都是通过发送命令到命令队列,由守护任务来实现。如果队列满了,命令就无法立即写入队列,需要指定一个超时时间。
启动定时器
启动定时器就是设置它的状态为运行态。
xTicksToWait不是定时器超时时间,也不是定时器周期。
如果定时器已经被启动,但它的回调函数还没有被执行时,再次执行xTimerStart()函数相当于执行xTimerReset()函数,重新设定它的启动时间。
/*
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"启动命令"在指定超时时间内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
/*
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,如果守护任务的优先级比当前任务的高,
则*pxHigherPriorityTaskWoken = pdTRUE,表示需要进行任务调度
* 返回值: pdFAIL表示"启动命令"无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
停止定时器
启动定时器就是设置它的状态为睡眠态,让它无法运行。
/*
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"停止命令"在指定超时时间内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
/*
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,如果守护任务的优先级比当前任务的高,
* 则*pxHigherPriorityTaskWoken = pdTRUE,表示需要进行任务调度
* 返回值: pdFAIL表示"停止命令"无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
复位定时器
使用xTimerReset()函数可以让定时器的状态从睡眠态转换为运行态,相当于使用xTimerStart()函数。
如果定时器已经处于运行态,使用xTimerReset()函数相当于重新确定超时时间。
/*
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"复位命令"在指定超时时间内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
/*
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,如果守护任务的优先级比当前任务的高,
* 则*pxHigherPriorityTaskWoken = pdTRUE,表示需要进行任务调度
* 返回值: pdFAIL表示"停止命令"无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
修改定时器周期
使用xTimerChangePeriod()函数,除了能修改定时器周期外,还可以让定时器的状态从睡眠态转换为运行态。
修改定时器周期时,会使用新的周期重新计算它的超时时间。
/* 返回值: pdFAIL表示"修改周期命令"在指定超时时间内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, /* xTimer: 哪个定时器 */
TickType_t xNewPeriod, /* xNewPeriod: 新周期 */
TickType_t xTicksToWait ); /* xTicksToWait: 超时时间, 命令写入队列的超时时间 */
/* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,如果守护任务的优先级比当前任务的高,
* 则*pxHigherPriorityTaskWoken = pdTRUE,表示需要进行任务调度
* 返回值: pdFAIL表示"修改周期命令"在指定超时时间内内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, /* xTimer: 哪个定时器 */
TickType_t xNewPeriod, /* xNewPeriod: 新周期 */
BaseType_t *pxHigherPriorityTaskWoken );
定时器ID
typedef struct tmrTimerControl
{
const char *pcTimerName;
ListItem_t xTimerListItem;
TickType_t xTimerPeriodInTicks;
void *pvTimerID; // 定时器ID
TimerCallbackFunction_t pxCallbackFunction;
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber;
#endif
uint8 t ucStatus;
} xTIMER;
怎么使用定时器ID,完全由程序来决定:
可以用来标记定时器,表示自己是什么定时器
可以用来保存参数,供回调函数使用
它的初始值在创建定时器时由xTimerCreate()函数传入,后续可以使用这些函数来操作:
更新ID:使用vTimerSetTimerID()函数
查询ID:使用pvTimerGetTimerID()函数
这两个函数不涉及命令队列,都是直接操作定时器结构体的。
/*
* xTimer: 哪个定时器
* 返回值: 定时器的ID
*/
void *pvTimerGetTimerID( TimerHandle_t xTimer );
/*
* xTimer: 哪个定时器
* pvNewID: 新ID
*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );
应用场景:一般使用
要使用定时器,需要一些准备工作。
/* 1. 工程中 */ 添加 timer.c /* 2. 配置文件FreeRTOSConfig.h中 */ ##define configUSE_TIMERS 1 /* 使能定时器 */ ##define configTIMER_TASK_PRIORITY 31 /* 守护任务的优先级, 尽可能高一些 */ ##define configTIMER_QUEUE_LENGTH 5 /* 命令队列长度 */ ##define configTIMER_TASK_STACK_DEPTH 32 /* 守护任务的栈大小 */ /* 3. 源码中 */ ##include "timers.h"
static volatile uint8_t flagONEShotTimerRun = 0; // 一次性
static volatile uint8_t flagAutoLoadTimerRun = 0; // 自动加载
static void vONEShotTimerFunc( TimerHandle_t xTimer );
static void vAutoLoadTimerFunc( TimerHandle_t xTimer );
/*-----------------------------------------------------------*/
##define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS( 10 )
##define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS( 20 )
int main( void )
{
TimerHandle_t xOneShotTimer;
TimerHandle_t xAutoReloadTimer;
prvSetupHardware();
xOneShotTimer = xTimerCreate(
"OneShot", // 名字, 不重要
mainONE_SHOT_TIMER_PERIOD, // 周期
pdFALSE, // 一次性
0, // ID
vONEShotTimerFunc // 回调函数
);
xAutoReloadTimer = xTimerCreate(
"AutoReload", // 名字, 不重要
mainAUTO_RELOAD_TIMER_PERIOD, // 周期
pdTRUE, // 自动加载
0, // ID
vAutoLoadTimerFunc // 回调函数
);
if (xOneShotTimer && xAutoReloadTimer)
{
/* 启动定时器 */
xTimerStart(xOneShotTimer, 0);
xTimerStart(xAutoReloadTimer, 0);
/* 启动调度器 */
vTaskStartScheduler();
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
static void vONEShotTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
flagONEShotTimerRun = !flagONEShotTimerRun;
printf("run vONEShotTimerFunc %d\r\n", cnt++);
}
static void vAutoLoadTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
flagAutoLoadTimerRun = !flagAutoLoadTimerRun;
printf("run vAutoLoadTimerFunc %d\r\n", cnt++);
}
应用场景:消除抖动
使用机械开关时经常碰到抖动问题,引脚电平在短时间内反复变化。
怎么读到确定的按键状态呢?
连续读很多次,直到数值稳定,但浪费CPU资源。
使用定时器,结合中断使用。
对于第2种方法,处理方法如下。
要使用定时器,需要一些准备工作。
/* 1. 工程中 */ 添加 timer.c /* 2. 配置文件FreeRTOSConfig.h中 */ ##define configUSE_TIMERS 1 /* 使能定时器 */ ##define configTIMER_TASK_PRIORITY 31 /* 守护任务的优先级, 尽可能高一些 */ ##define configTIMER_QUEUE_LENGTH 5 /* 命令队列长度 */ ##define configTIMER_TASK_STACK_DEPTH 32 /* 守护任务的栈大小 */ /* 3. 源码中 */ ##include "timers.h"
/*-----------------------------------------------------------*/
static TimerHandle_t xKeyFilteringTimer;
void vEmulateKeyTask( void *pvParameters );
static void vKeyFilteringTimerFunc( TimerHandle_t xTimer );
/*-----------------------------------------------------------*/
#define KEY_FILTERING_PERIOD pdMS_TO_TICKS( 20 )
int main( void )
{
prvSetupHardware();
xKeyFilteringTimer = xTimerCreate(
"KeyFiltering", // 名字, 不重要
KEY_FILTERING_PERIOD, // 周期
pdFALSE, // 一次性
0, // ID
vKeyFilteringTimerFunc // 回调函数
);
/* 在这个任务中多次调用xTimerReset来模拟按键抖动 */
xTaskCreate( vEmulateKeyTask, "EmulateKey", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
void vEmulateKeyTask( void *pvParameters )
{
int cnt = 0;
const TickType_t xDelayTicks = pdMS_TO_TICKS( 200UL );
for( ;; )
{
/* 模拟按键抖动, 多次调用xTimerReset */
xTimerReset(xKeyFilteringTimer, 0);
cnt++;
xTimerReset(xKeyFilteringTimer, 0);
cnt++;
xTimerReset(xKeyFilteringTimer, 0);
cnt++;
printf("Key jitters %d\r\n", cnt);
vTaskDelay(xDelayTicks);
}
}
static void vKeyFilteringTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
printf("vKeyFilteringTimerFunc %d\r\n", cnt++);
}
实验现象:
Key jitters 3
vKeyFilteringTimerFunc 0
Key jitters 6
vKeyFilteringTimerFunc 1
Key jitters 9
vKeyFilteringTimerFunc 2
...
在任务函数中多次调用xTimerReset函数,只触发一次定时器回调函数。
资源管理
屏蔽中断
taskENTER_CRITICA(); // 屏蔽中断
taskEXIT_CRITICAL(); // 重新使能中断
taskENTER_CRITICAL_FROM_ISR(); // 屏蔽中断
taskEXIT_CRITICAL_FROM_ISR(); // 重新使能中断
taskENTER_CRITICA() / taskEXIT_CRITICAL() 间 或 taskENTER_CRITICAL_FROM_ISR() / taskEXIT_CRITICAL_FROM_ISR()间:
低优先级的中断被屏蔽了。优先级 ≤ configMAX_SYSCALL_INTERRUPT_PRIORITY。
高优先级的中断可以产生。但在这些高优先级的中断ISR里不允许使用FreeRTOS的API函数。
任务调度依赖于中断、依赖于API函数。所以这两段代码间不会用任务调度产生。
taskENTER_CRITICA() / taskEXIT_CRITICAL() 间还可以递归使用该宏,内部会记录嵌套的深度,只有嵌套深度变为0时,调用taskEXIT_CRITICAL()才会重新使能中断。
使用 taskENTER_CRITICA() / taskEXIT_CRITICAL() 来访问临界资源是很粗鲁的方法:中断无法正常运行、任务调度无法进行。所以之间的代码要尽可能快速执行。
暂停/恢复调度器
如果有别的任务竞争临界资源,可以把中断关掉,也可以禁止别的任务运行(但代价太大,会影响中断的处理)。
如果只是禁止别的任务竞争,不需要关中断,暂停调度器就可以了。期间中断还是可以发生和处理。
/* 暂停调度器 */
void vTaskSuspendAll( void );
/* 恢复调度器
* 返回值: pdTRUE表示在暂定期间有更高优先级的任务就绪了,可以不理会这个返回值
*/
BaseType_t xTaskResumeAll( void );
vTaskSuspendScheduler();
/* 访问临界资源 */
xTaskResumeScheduler();