说明本文章基于百问网RTOS教程文档
1.硬件定时器
什么是硬件定时器,由硬件电路构成的定时器。在学习STM32时我们都会学到定时器,这个就是硬件定时器。硬件定时器不单单可以定时,它还可以进行PWM输出等等。硬件定时器每隔一段固定的时间会进入一次中断,我们叫Tick中断。
我们生活中有重复闹钟,有一次性闹钟。闹钟有三种状态:运行,暂停和超时。
闹钟到点需要做什么事情?回调函数,为了方便区分做哪些事我们还会涉入函数参数。
2.FreeRTOS软件定时器
创建定时器一般都是创建一个结构体变量。这个结构体里会有定时器的各种信息,模式,状态,延时,函数,函数参数,还有一个链表。
比如我们有三个定时器定时器A,B,C,A定的时间为2个Tick,B定的时间为5个Tick,C定的时间为10个Tick。
怎么去处理链表中的定时器呢?
·首先在Tick中断里去判断链表的第一项是否超时。如果超时有两种执行选择,下面会讲。第一项没超时那就不管。
怎么判断是否超时?在启动软件定时器时会计算出超时时间,根据当前Count和超时间比较就可知道定时器是否超时。
·如果第一项超时会有两种执行选择。
第一种:在中断中执行
实时操作系统不会再中断中处理复杂任务,因此不会选择中断里处理任务
第二种:在任务中执行(守护任务)
在中断中通知一个定时器任务(守护函数),在定时器任务中调用定时器里的函数执行。
怎么通知?
答:写一个队列,定时器任务会读这个队列处理队列的数据来判断是哪个定时器超时,再去调用定时器里的函数执行。
3.守护任务
3.1守护任务创建
当FreeRTOS的配置项configUSE_TIMERS被设置为1时,在启动调度器时,会自动创建RTOS Damemon Task。
3.2守护任务机制
守护任务中只执行两件事:读队列,处理队列数据
我们用户所有关于定时器的函数里都会去写某个队列,守护任务执行时会去处理队列的数据对定时器执行相关操作:Runing,Stop,Rest,执行超时定时器的任务等等。
因此守护任务的优先级一定要高,否则就无法处理定时任务。因此在没有处理完队列数据前它会一直运行。
3.3回调函数
定时器的回调函数的原型如下:
void ATimerCallback( TimerHandle_t xTimer );
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。
所以,定时器的回调函数不要影响其他人:
- 回调函数要尽快实行,不能进入阻塞状态
- 不要调用会导致阻塞的API函数,比如 vTaskDelay()
- 可以调用 xQueueReceive() 之类的函数,但是超时时间要设为0:即刻返回,不可阻塞
4.定时器函数
根据定时器的状态转换图,就可以知道所涉及的函数:
4.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 );
4.2 删除
动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:
/* 删除定时器
* xTimer: 要删除哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"删除命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
定时器的很多API函数,都是通过发送"命令"到命令队列,由守护任务来实现。
如果队列满了,"命令"就无法即刻写入队列。我们可以指定一个超时时间 xTicksToWait ,等待一会。
4.3 启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。
停止定时器就是设置它的状态为冬眠(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.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 );
4.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 );
4.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 );
5.代码例程
本节代码为:28_timer_game_sound,主要看nwatch\beep.c。
对于无源蜂鸣器,只要设置PWM输出方波,它就会发出声音。在game1游戏中,什么时候发出声音?球与挡球板、转块碰撞时发出声音。什么时候停止声音?发出声音后,过一阵子就应该停止声音。这使用软件定时器来实现。
在初始化蜂鸣器时,创建定时器,代码如下:
25 static TimerHandle_t g_TimerSound;
/* 省略 */
52 void buzzer_init(void)
53 {
54 /* 初始化蜂鸣器 */
55 PassiveBuzzer_Init();
56
57 /* 创建定时器 */
58 g_TimerSound = xTimerCreate( "GameSound",
59 200,
60 pdFALSE,
61 NULL,
62 GameSoundTimer_Func);
63 }
想发出声音时,调用buzzer_buzz函数,代码如下:
78 void buzzer_buzz(int freq, int time_ms)
79 {
80 PassiveBuzzer_Set_Freq_Duty(freq, 50);
81
82 /* 启动定时器 */
83 xTimerChangePeriod(g_TimerSound, time_ms, 0);
84 }
第80行:设置PWM频率。
第83行:启动定时器。
当定时器超时后,GameSoundTimer_Func函数被调用,它会停止蜂鸣器,代码如下:
37 static void GameSoundTimer_Func( TimerHandle_t xTimer )
38 {
39 PassiveBuzzer_Control(0);
40 }
回调函数指针,在创建定时器时传入函数地址。
game1里如何使用音效?先初始化,代码如下:
297 void game1_task(void *params)
298 {
299 g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
300 draw_init();
301 draw_end();
302
303 buzzer_init();
第303行:初始化蜂鸣器。
game1里使用buzzer_buzz函数发出声音,比如碰到砖块时:
412 buzzer_buzz(2000, 100);
第412行会发出2000Hz的声音,维持100ms。