目录
2.2.2 configTIMER_TASK_PRIORITY
2.2.3 configTIMER_QUEUE_LENGTH
2.2.4 configTIMER_TASK_STACK_DEPTH
定时器可以说是每个 MCU 都有的外设,有的 MCU 其定时器功能异常强大,比如提供 PWM、输入捕获等功能。但是最常用的还是定时器最基本的功能——定时,通过定时器来完成需要周期性处理的事务。MCU 自带的定时器属于硬件定时器,不同的 MCU 其硬件定时器数量不同,因为要考虑成本的问题。FreeRTOS 提供了定时器功能,不过是软件定时器,软件定时器的精度没有硬件定时器那么高,但是对于普通的精度要求不高的周期性处理任务来说足够了。当 MCU 的硬件定时器不够的时候考虑使用软件定时器。
1. 软件定时器简介
软件定时器运行设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期,简而言之,当定时器的定时周期到了之后就会执行回调函数。
软件定时器的回调函数是在定时器服务任务中执行的,所以一定不能在回调函数中调用任何会阻塞任务的 API 函数!比如,定时器回调函数中千万不能调用 vTaskDelay()、vTaskDelayUnti(),还有一些访问队列或者信号量的非零阻塞时间的 API 函数也不能调用。
2. 定时器服务/Daemon 任务
2.1 定时器服务任务与队列
定时器是一个可选的、不属于 FreeRTOS 内核的功能,它是由定时器服务(或 Daemon)任务来提供的。FreeRTOS 提供了很多定时器有关的 API 函数,这些 API 函数大多都使用 FreeRTOS 的队列发送命令给定时器服务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给 FreeRTOS 的软件定时器使用的,用户不能直接访问!
上图左侧部分属于用户应用程序的一部分,并且会在某个用户创建的用户任务中调用。图中右侧部分是定时器服务任务的任务函数,定时器命令队列将用户应用任务和定时器服务任务连接在一起。上图中,应用程序调用了函数 xTimerReset(),结果就是复位命令会被发送到定时器命令队列中,定时器服务任务会处理这个命令。应用程序是通过函数 xTimerReset() 间接的向定时器命令队列发送了复位命令,并不是直接调用类似 xQueueSend() 这样的队列操作函数发送的。
2.2 定时器相关配置
通过上图的学习我们知道软件定时器有一个定时器服务任务和定时器命令队列,这两个需要一定的配置。配置方法和我们之前的学习的 FreeRTOSConfig.h 一样。
2.2.1 configUSE_TIMERS
如果要使用软件定时器的话宏 configUSE_TIMERS 一定要设置为 1,当设置为 1 的话定时器服务任务就会在启动 FreeRTOS 调度器的时候自动创建。
2.2.2 configTIMER_TASK_PRIORITY
设置软件定时器服务任务的任务优先级,可以在 0~(configMAX_PRIORITIES-1)。优先级一定要根据实际的应用要求来设置。如果定时器服务任务的优先级设置的高的话,定时器命令队列中的命令和定时器回调函数就会及时的得到处理。
2.2.3 configTIMER_QUEUE_LENGTH
此宏用来设置定时器命令队列的队列长度。
2.2.4 configTIMER_TASK_STACK_DEPTH
此宏用来设置定时器服务任务的任务堆栈大小,单位为字,不是字节!,对于 STM32 来说一个字是 4 个字节。由于定时器服务任务中会执行定时器的回调函数,因此任务堆栈的大小一定要根据定时器的回调函数来设置。
2.3 单次定时器和周期定时器
软件定时器分为两种:单次定时器和周期定时器,单次定时器的话定时器回调函数就执行一次,比如定时 1s,当定时时间到了以后就会执行一次回调函数,然后定时器就会停止运行。对于单次定时器我们可以再次手动重新启动(调用相应的 API 函数即可),但是单次定时器不能自动重启。相反的,周期定时器一旦启动以后就会在执行完回调函数以后自动的重新启动,这样回调函数就会周期性的执行。
上图中 Timer1 为单次定时器,定时器周期为 100,Timer2 为周期定时器,定时器周期为 200.
2.4 复位软件定时器
有时候我们可能会在定时器正在运行的时候需要复位软件定时器,复位软件定时器的话会重新计算定时周期到达的时间点,这个新的时间点是相对于复位定时器的那个时刻计算的,并不是第一次启动软件定时器的那个时间点。如下图所示,Timer1 是单次定时器,定时周期是 5s:
上图中展示了定时器的复位过程,这是一个通过按键打开 LCD 背光的例子,我们假定当唤醒键被按下的时候应用程序打开 LCD 背光,当 LCD 背光点亮以后如果 5s 之内唤醒键没有再次按下就自动熄灭。如果在这 5s 之内唤醒键被按下了,LCD 背光就从按下的这个时刻起再亮 5s。
FreeRTOS 提供了两个 API 函数来完成软件定时器的复位:
函数:
xTimerReset() 复位软件定时器,用在任务中。
xTimerResetFromISR() 复位软件定时器,用在中断服务函数中。
2.4.1 函数 xTimerReset()
复位一个软件定时器,此函数只能用在任务中,不能用于中断服务函数中!此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerReset(TimerHandle_t xTimer,
TickType_t xTicksToWait)
参数:
xTimer: 要复位的软件定时器的句柄。
xTicksToWait: 设置阻塞时间,调用函数 xTimerReset() 开启软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_RESET 命令,既然是向队列发送消息,那么
必然会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
2.4.2 函数 xTimerResetFromISR()
此函数是 xTimerReset() 的中断版本,此函数用于中断服务函数中!此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer,
BaseType_t* pxHigherPriorityTaskWoken);
参数:
xTimer: 要复位的软件定时器的句柄。
pxHigherPriorityTaskWoken: 记录退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行设置,用户只需要提供一个变量来保存
这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
2.5 创建软件定时器
使用软件定时器之前要先创建软件定时器。
函数:
xTimerCreate(): 使用动态方法创建软件定时器
xTimerCreateStatic(): 使用静态方法创建软件定时器
2.5.1 函数 xTimerCreate()
此函数用于创建一个软件定时器,所需要的内存通过动态内存管理方法分配。新创建的软件定时器处于休眠状态,也就是未运行的。函数 xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod() 和 xTimerChangePeriodFromISR() 可以使新创建的定时器进入活动状态,此函数的原型如下:
TimerHandle_t xTimerCreate(const char* const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void* pvTimerID,
TimerCallbackFunction_t pxCallbackFunction)
参数:
pcTimerName: 软件定时器名字,名字是一串字符串,用于调试使用。
xTimerPeriodInTicks: 软件定时器的定时器周期,单位是时钟节拍数。可以借助 portTICK_PERIOD_MS 将 ms 单位转换为时钟节拍数。比如说,定时器的周期为 100 个时钟节拍的
话,那么 xTimerPeriodInTicks 就为 100,当定时器周期为 500ms 的时候 xTimerPeriodInTicks 就可以设置为 (500/portTICK_PERIOD_MS)
uxAutoReload: 设置定时器模式,单次定时器还是周期定时器。当参数为 pdTRUE 的时候表示创建的是周期定时器。如果为 pdFALSE 的话表示创建的是单次定时器。
pvTimerID: 定时器 ID 号,一般情况下每个定时器都有一个回调函数,当定时器定时周期到了以后就会执行这个回调函数。但是 FreeRTOS 也支持多个定时器共用同一个回
调函数,在回调函数中根据定时器的 ID 号来处理不同的定时器。
pxCallbackFunction: 定时器回调函数,当定时器定时周期到了以后就会调用这个函数。
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
2.5.2 函数 xTimerCreateStatic()
此函数用于创建一个软件定时器,所需要的内存需要用户自行分配。新创建的软件定时器处于休眠状态,也就是未运行的。函数 xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod() 和 xTimerChangePeriodFromISR() 可以使新创建的定时器进入活动状态,此函数的原型如下:
TimerHandle_t xTimerCreate(const char* const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void* pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t* pxTimerBuffer)
参数:
pcTimerName: 软件定时器名字,名字是一串字符串,用于调试使用。
xTimerPeriodInTicks: 软件定时器的定时器周期,单位是时钟节拍数。可以借助 portTICK_PERIOD_MS 将 ms 单位转换为时钟节拍数。比如说,定时器的周期为 100 个时钟节拍的
话,那么 xTimerPeriodInTicks 就为 100,当定时器周期为 500ms 的时候 xTimerPeriodInTicks 就可以设置为 (500/portTICK_PERIOD_MS)
uxAutoReload: 设置定时器模式,单次定时器还是周期定时器。当参数为 pdTRUE 的时候表示创建的是周期定时器。如果为 pdFALSE 的话表示创建的是单次定时器。
pvTimerID: 定时器 ID 号,一般情况下每个定时器都有一个回调函数,当定时器定时周期到了以后就会执行这个回调函数。但是 FreeRTOS 也支持多个定时器共用同一个回
调函数,在回调函数中根据定时器的 ID 号来处理不同的定时器。
pxCallbackFunction: 定时器回调函数,当定时器定时周期到了以后就会调用这个函数。
pxTimerBuffer: 参数指向一个 StaticTimer_t 类型的变量,用来保存定时器结构体。
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
2.6 开启软件定时器
如果软件定时器停止运行的话可以使用 FreeRTOS 提供的两个开启函数来重新启动软件定时器。
函数:
xTimerStart(): 开启软件定时器,用于任务中。
xTimerStartFromISR(): 开启软件定时器,用于中断中。
2.6.1 函数 xTimerStart()
启动软件定时器,函数 xTimerStartFromISR() 是这个函数的中断版本,可以用在中断服务函数中。如果软件定时器没有运行的话调用函数 xTimerStart() 就会计算定时器到期时间,如果软件定时器正在运行的话调用函数 xTimerStart() 的结果和 xTimerReset() 一样。此函数是个宏,真正执行的是函数 xTimerGenericCommand,函数原型如下:
BaseType_t xTimerStart(TimerHandle_t xTimer,
TickType_t xTicksToWait)
参数:
xTimer: 要开启的软件定时器的句柄。
xTicksToWait: 设置阻塞时间,调用函数 xTimerStart() 开启软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_START 命令,既然是向队列发送消息,必然会涉及到
入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
2.6.2 函数 xTimerStartFromISR()
此函数是函数 xTimerStart() 的中断版本,用在中断服务函数中,此函数是个宏,真正执行的是函数 xTimerGenericCommand(),此函数原型如下:
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer,
BaseType_t* pxHigherPriorityTaskWoken);
参数:
xTimer: 要开启的软件定时器的句柄
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
2.7 停止软件定时器
软件定时器有开启的 API 函数,也有停止的 API 函数。
函数:
xTimerStop(): 停止软件定时器,用于任务中。
xTimerStopFromISR(): 停止软件定时器,用于中断服务函数中。
2.7.1 函数 xTimerStop()
此函数用于停止一个软件定时器,此函数用于任务中,不能用在中断服务函数中!此函数是一个宏,真正调用的是函数 xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerStop(TimerHandle_t xTimer,
TickType_t xTicksToWait)
参数:
xTimer: 要停止的软件定时器的句柄。
xTicksToWait: 设置阻塞时间,调用函数 xTimerStop() 停止软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_STOP 命令,向队列发送消息,必然会涉及到入
队阻塞时间的设置。
返回值:
pdPASS: 软件定时器停止成功,其实就是命令发送成功。
pdFAIL: 软件定时器停止失败,命令发送失败。
2.7.2 函数 xTimerStopFromISR()
此函数是 xTimerStop() 的中断版本,此函数用于中断服务函数中!此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,
BaseType_t* pxHigherPriorityTaskWoken);
参数:
xTimer: 要停止的软件定时器句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置,用户不用进行设置,用户只需要提供一个变量来保存这个值。
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器停止成功,其实就是命令发送成功。
pdFAIL: 软件定时器停止失败,命令发送失败。
3. 软件定时器实验
本实验设计两个任务:start_task 和 timercontrol_task 这两个任务的任务功能如下:
start_task:用来创建任务 timercontrol_task() 和两个软件定时器。
timercontrol_task:控制两个软件定时器的开启和停止。
实验中还创建了两个软件定时器:AutoReloadTimer_Handle 和 OneShotTimer_Handle,这两个定时器分别为周期定时器和单次定时器。定时器 AutoReloadTimer_Handle 的定时器周期为 1000 个时钟节拍(1s),定时器 OneShotTimer_Handle 的定时器周期为 2000 个时钟节拍(2s)。
使用软件定时器之前一定先设置相关的宏!!!
#ifndef configUSE_TIMERS #define configUSE_TIMERS 1 //如果要使用软件定时器的话 configUSE_TIMERS 一定要设置为 1,当设置为 1 的时候定时器服务任务就会在启动 FreeRTOS 调度器时自动创建。 #endif
/* The timers module relies on xTaskGetSchedulerState(). */ #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 */
3.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "timers.h" //必须使用这个头文件
//任务优先级
#define START_TASK_PRIO 1 //用于创建其他任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define TIMERCONTROL_TASK_PRIO 2 //定时器控制任务
//任务堆栈大小
#define TIMERCONTROL_STK_SIZE 256
//任务句柄
TaskHandle_t TimerControlTask_Handler;
//任务函数
void timercontrol_task(void *pvParameters);
TimerHandle_t AutoReloadTimer_Handle; //周期定时器句柄
TimerHandle_t OneShotTimer_Handle; //单次定时器句柄
void AutoReloadCallback(TimerHandle_t xTimer); //周期定时器回调函数
void OneShotCallback(TimerHandle_t xTimer); //单次定时器回调函数
//LCD刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW,BROWN,
BRRED, GRAY };
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
delay_init(168);
uart_init(115200);
LED_Init();
KEY_Init();
BEEP_Init();
LCD_Init();
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR=RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Software Timer");
LCD_ShowString(30,50,200,16,16,"KEY_UP:Start Tmr1");
LCD_ShowString(30,70,200,16,16,"KEY0:Start Tmr2");
LCD_ShowString(30,90,200,16,16,"KEY1:Stop Tmr1 and Tmr2");
LCD_DrawLine(0,108,239,108); //画线
LCD_DrawLine(119,108,119,319); //画线
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,115,314); //画一个矩形
LCD_DrawLine(5,130,115,130); //画线
LCD_DrawRectangle(125,110,234,314); //画一个矩形
LCD_DrawLine(125,130,234,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(6,111,110,16,16,"AutoTim:000");
LCD_ShowString(126,111,110,16,16,"OneTim: 000");
//创建开始任务
xTaskCreate((TaskFunction_t)start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler);//任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建软件周期定时器
AutoReloadTimer_Handle=xTimerCreate((const char* )"AutoReloadTimer", //软件定时器名字,一串字符串,用于调试
(TickType_t )1000, //周期数,1000个时钟节拍,1s
(UBaseType_t )pdTRUE, //pdTRUE表示周期定时器
(void* )1, //定时器 ID 号
(TimerCallbackFunction_t)AutoReloadCallback); //周期定时器回调函数
//创建软件单次定时器
OneShotTimer_Handle=xTimerCreate((const char* )"OneShotTimer", //软件定时器名字,一串字符串,用于调试
(TickType_t )2000, //周期数,2000个时钟节拍,2s
(UBaseType_t )pdFALSE, //pdFALSE表示单次定时器
(void* )2, //定时器 ID 号
(TimerCallbackFunction_t)OneShotCallback); //单次定时器回调函数
//创建开始任务
xTaskCreate((TaskFunction_t)timercontrol_task, //任务函数
(const char* )"timercontrol_task", //任务名称
(uint16_t )TIMERCONTROL_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )TIMERCONTROL_TASK_PRIO, //任务优先级
(TaskHandle_t* )&TimerControlTask_Handler);//任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//TimerControl的任务函数
void timercontrol_task(void *pvParameters)
{
u8 key,num;
while(1)
{
//只有两个定时器都创建成功了才能对其进行操作
if((AutoReloadTimer_Handle!=NULL)&&(OneShotTimer_Handle!=NULL))
{
key = KEY_Scan(0);
switch(key)
{
case WKUP_PRES: //当KEY_UP按键按下的时候打开周期定时器
xTimerStart(AutoReloadTimer_Handle,0); //开启周期定时器
printf("开启定时器1\r\n");
break;
case KEY0_PRES: //当KEY0按键按下的时候打开单次定时器
xTimerStart(OneShotTimer_Handle,0); //开启单次定时器
printf("开启定时器2\r\n");
break;
case KEY1_PRES: //当KEY1按键按下的时候关闭定时器1和定时器2
xTimerStop(OneShotTimer_Handle,0); //关闭单次定时器
xTimerStop(AutoReloadTimer_Handle,0); //关闭周期定时器
printf("关闭定时器1和定时器2\r\n");
break;
}
}
num++;
if(num==50) //每500msLED0闪烁一次
{
num=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,10个时钟节拍
}
}
//周期定时器回调函数
void AutoReloadCallback(TimerHandle_t xTimer)
{
static u8 tmr1_num=0;
tmr1_num++; //周期定时器执行次数加1
LCD_ShowxNum(70,111,tmr1_num,3,16,0x80); //显示周期定时器的执行次数
LCD_Fill(6,131,114,313,lcd_discolor[tmr1_num%14]); //填充区域
}
//单次定时器的回调函数
void OneShotCallback(TimerHandle_t xTimer)
{
static u8 tmr2_num=0;
tmr2_num++;
LCD_ShowxNum(190,111,tmr2_num,3,16,0x80); //显示单次定时器的执行次数
LCD_Fill(126,131,233,313,lcd_discolor[tmr2_num%14]);
LED1=!LED1;
printf("定时器2运行结束\r\n");
}
当按下 KEY0 键以后,xTimerStart 会使单次定时器开始执行,当定时器计时的 2s 时间到了之后就会调用回调函数 OneShotCallback(),屏幕右侧区域的背景色会被刷新为其他颜色,背景颜色逐渐刷新完成以后定时器 OneShotTimer_Handle 就会停止运行。
当按下 KEY_UP 键的话周期定时器就会开始运行,屏幕左侧区域的背景色会被刷新为其他颜色。由于定时器 AutoReloadTimer_Handle 是周期定时器,所以不会停止运行,除非按下 KEY1 键同时关闭定时器 AutoReloadTimer_Handle 和 OneShotTimer_Handle。