1.理解基本概念
定时器概念:
定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。类似生活中的闹钟,我们可以设置闹钟每天什么时候响,还能设置响的次数,是响一次还是每天都响。
硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。
软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。
- 使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息。
- 使用软件定时器时,需要在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数),在回调函数中处理信息。
其它:
- 软件定时器回调函数的上下文是任务。
- 软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。
- 定时精度与系统时钟的周期有关。
- 一般系统利用 SysTick 作为软件定时器的基础时钟, 软件定时器的回调函数类似硬件的中断服务函数,所以, 回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况(软件定时器回调函数的上下文环境是任务) ,比如 vTaskDelay()以及其它能阻塞任务运行的函数 。
- 两次触发回调函数的时间间隔xTimerPeriodInTicks 叫定时器的定时周期。
FreeRTOS 操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。 FreeRTOS 软件定时器功能上支持:
- 裁剪:能通过宏关闭软件定时器功能。
- 软件定时器创建。
- 软件定时器启动。
- 软件定时器停止。
- 软件定时器复位。
- 软件定时器删除。
FreeRTOS 软件定时器的两种模式:
- 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器进入休眠状态,不再重新执行。
- 周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除。
启用FreeRTOS定时器:
FreeRTOS 通过一个 prvTimerTask 任务(也叫守护任务 Daemon)管理软件定时器,它是在启动调度器时自动创建的。 prvTimerTask 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。 只有将
FreeRTOSConfig.h
中的宏定义configUSE_TIMERS
设置为 1 ,将相关代码编译进来,才能正常使用软件定时器相关功能。
2.应用场景
在很多应用中,我们需要一些定时器任务,硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,无法提供更多的定时器,那么可以采用软件定时器来完成,由软件定时器代替硬件定时器任务。 但需要注意的是软件定时器的精度是无法和硬件定时器相比的, 而且在软件定时器的定时过程中是极有可能被其它中断所打断,因为软件定时器的执行上下文环境是任务。所以,软件定时器更适用于对时间精度要求不高的任务,一些辅助型的任务。
《FreeRTOS内核实现与应用开发实战——基于STM32》 P336
用于当硬件定时器不够用,并且任务对时间精度要求不高时。
3.软件定时器的精度
系统节拍:
- 通常软件定时器以系统节拍周期为计时单位,系统节拍配置为
configTICK_RATE_HZ
,该宏在FreeRTOSConfig.h
中有定义,默认是1000,意为1s内跳动1000次,也就是系统节拍周期为1ms。 - 软件定时器的所定时数值必须是这个节拍周期的整数倍。
- 系统节拍越小,精度也就越高,但是系统开销也将越大,因为这代表在 1 秒中系统进入时钟中断的次数也就越多。
4.使用注意事项
使用软件定时器时候要注意以下几点:
- 软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的 API 接口,在回调函数中也绝对不允许出现死循环。
- 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为
configTIMER_TASK_PRIORITY
,为了更好响应,该优先级应设置为所有任务中最高的优先级。- 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。
- 定时器任务的堆栈大小默认为
configTIMER_TASK_STACK_DEPTH
个字节。
5.常用函数
①创建定时器
xTimerCreate():
/* 创建定时器,返回定时器句柄 */
TimerHandle_t xTimerCreate(const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction);
-
形参pcTimerName:软件定时器的名字,调试用的。
-
形参xTimerPeriodInTicks:软件定时器周期,单位为系统节拍周期。
pdMS_TO_TICKS()
可以把时间单位从 ms 转换为系统节拍周期。 如果软件定时器的周期为100 个 tick, 那么只需要简单的设置xTimerPeriod
的值为 100 即可。如果软件定时器的周期为 500ms, 那么xTimerPeriod
应设置为pdMS_TO_TICKS(500)
。 宏 pdMS_TO_TICKS()只有当configTICK_RATE_HZ
配置成小于或者等于 1000HZ 时才可以使用。 -
形参uxAutoReload:自动重装,即设置定时器模式,周期运行还是单次运行。pdTRUE —— 周期运行,pdFALSE —— 单次运行。
-
形参pvTimerID:软件定时器 ID, 数字形式。 该 ID 典型的用法是当一个回调函数分配给一个或者多个软件定时器时,在回调函数里面根据 ID 号来处理不同的软件定时器。
-
形参pxCallbackFunction:软件定时器的回调函数, 当定时时间到达的时候就会调用这个函数。
要想使用该函数函数必须在头文件FreeRTOSConfig.h
中把宏configUSE_TIMERS
和configSUPPORT_DYNAMIC_ALLOCATION
均定义为1, 并且需要把times.c
这个C文件添加到工程中。
将configUSE_TIMERS
设置为1后还需要设置以下:
/* 定时器守护任务的优先级 */
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-1)
/* 定时器队列的长度 */
#define configTIMER_QUEUE_LENGTH 10
/* 定时器任务的栈深度,即要为定时器任务分配的内存大小 */
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE*2)
定时器创建完成处于休眠状态,需要调用启动函数使定时器运行。
静态创建:xTimerCreateStatic()
。
②启动定时器
1、xTimerStart():
/* 传入定时器句柄,启动定时器 */
#define xTimerStart(xTimer, xTicksToWait)
xTimerGenericCommand((xTimer), tmrCOMMAND_START, (xTaskGetTickCount()), NULL, (xTicksToWait));
2、xTimerStartFromISR():
/* 中断中启动定时器 */
#define xTimerStartFromISR(xTimer, pxHigherPriorityTaskWoken)
xTimerGenericCommand((xTimer), tmrCOMMAND_START_FROM_ISR,
(xTaskGetTickCountFromISR()),
(pxHigherPriorityTaskWoken), 0U);
/*
形参pxHigherPriorityTaskWoken,会被设置为pdTRUE或pdFALSE
如果定时器守护任务的优先级大于或者等于当前被中断的任务的优先级,那么 pxHigherPriorityTaskWoken 的值会在函数
xTimerStartFromISR()内部设置为 pdTRUE,然后在中断退出之前执行一次上下文切换。
*/
- 返回值:pdFAILE或pdPASS。
/* 使用示例 */
void vKeyPressEventInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 启动软件定时器 */
if (xTimerStartFromISR(xBacklightTimer, &xHigherPriorityTaskWoken) != pdPASS) {
/* 软件定时器开启命令没有成功执行 */
}
if (xHigherPriorityTaskWoken != pdFALSE) {
/* 执行上下文切换 */
}
}
③暂停定时器
1、xTimerStop():
/* 传入定时器句柄,暂停定时器,使其进入休眠状态 */
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xBlockTime);
/*
形参xBlockTime:指定超时时间
返回值:pdFAIL、pdPASS
*/
要想使函数xTimerStop()必须在头文件FreeRTOSConfig.h
中把宏 configUSE_TIMERS
定义为1。
2、xTimerStopFromISR():
/* 中断中暂停定时器 */
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
/*
形参pxHigherPriorityTaskWoken,会被设置为pdTRUE或pdFALSE
如果定时器守护任务的优先级大于或者等于当前被中断的任务的优先级,那么 pxHigherPriorityTaskWoken 的值会在函数
xTimerStopFromISR()内部设置为 pdTRUE,然后在中断退出之前执行一次上下文切换。
*/
/* 使用示例 */
/* 停止软件定时器的中断服务函数*/
void vAnExampleInterruptServiceRoutine( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (xTimerStopFromISR(xTimer, &xHigherPriorityTaskWoken) != pdPASS) {
/* 软件定时器停止命令没有成功执行 */
}
if (xHigherPriorityTaskWoken != pdFALSE) {
/* 执行上下文切换 */
}
}
④删除定时器
xTimerDelete():
/* 传入句柄,删除定时器 */
#define xTimerDelete(xTimer, xTicksToWait)
xTimerGenericCommand((xTimer),
tmrCOMMAND_DELETE, 0U, NULL, (xTicksToWait));
/*
形参xTicksToWait:指定超时时间,单位为系统节拍周期(即 tick),如果在FreeRTOS调度器开启之前调用了xTimerStart(),该形参将不起作用。
返回值:pdPASS、pdFAILE
*/
要想使函数 xTimerStop()必须在头文件FreeRTOSConfig.h
中把宏configUSE_TIMERS
定义为1。
6.软件定时器任务
软件定时器任务在启动调度器之前被创建,前提是要将FreeRTOSConfig.h
中的宏定义 configUSE_TIMERS
设置为1。
在调用xTimerCreate()时,指定好了定时器回调函数,由系统帮创建好这个回调函数任务,并不需要手动去调用xTaskCreate()来创建好任务。
7.使用示例
示例:在 FreeRTOS 中创建了两个软件定时器,其中一个软件定时器是单次模式, 5000个tick 调用一次回调函数,另一个软件定时器是周期模式, 1000个tick 调用一次回调函数,在回调函数中输出相关信息。
结果:
代码:
0、在FreeRTOSConfig.h
配置文件中添加以下:
#define configSUPPORT_DYNAMIC_ALLOCATION 1
/* 定时器相关配置 */
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-1)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE*2)
1、STM32F103C8_BSP.c
:
#include "stm32f10x.h" // Device header
void USART1_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStruct);
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStruct);
USART_Cmd(USART1, ENABLE);
}
void USART1_SendByte(uint8_t byte){
USART_SendData(USART1, byte);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
uint32_t Serial_Pow(uint32_t X, uint8_t Y){
uint32_t Result = 1;
while(Y--)
Result *= X;
return Result;
}
// 取出数字各位的数并转为char形式数据来发送
void USART1_SendStringNumber(uint32_t Number, uint8_t Length)
{
uint16_t i;
for(i = 0; i < Length; i++) {
// Number=321 Length=3,则会将3、2、1的ASCII码依次发送出去
USART1_SendByte(Number / Serial_Pow(10,Length - i - 1) % 10 + '0');
}
}
void USART1_SendString(char* str)
{
uint16_t i;
for(i = 0; str[i] != '\0'; i++){
USART1_SendByte(*(str+i));
}
}
2、main.c
:
#include "stm32f10x.h" // Device header
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "STM32F103C8_BSP.h"
static void STM32F103_Init(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
USART1_Init();
KEY_GPIO_Init();
LED_GPIO_Init();
}
/* 定义定时器句柄 */
TimerHandle_t Timer1_Handle = NULL;
TimerHandle_t Timer2_Handle = NULL;
/* 记录定时器执行次数 */
uint32_t Timer1_Count = 0;
uint32_t Timer2_Count = 0;
/* 定义定时器任务 */
void Timer1_Callback(void);
void Timer2_Callback(void);
int main(void)
{
STM32F103_Init();
taskENTER_CRITICAL();
Timer1_Handle = xTimerCreate("Timer1-AutoReload", 1000, pdTRUE, (void*)1,
(TimerCallbackFunction_t)Timer1_Callback);
if(Timer1_Handle != NULL){
/* 如果在启动调度程序之前调用 xTimerStart(),则忽略xTicksToWait,将其设置为0 */
xTimerStart(Timer1_Handle, 0);
}
Timer2_Handle = xTimerCreate("Timer1-AutoReload", 5000, pdFALSE, (void*)1,
(TimerCallbackFunction_t)Timer2_Callback);
if(Timer2_Handle != NULL){
/* 如果在启动调度程序之前调用 xTimerStart(),则忽略xTicksToWait,将其设置为0 */
xTimerStart(Timer2_Handle, 0);
}
vTaskStartScheduler();
taskEXIT_CRITICAL();
while(1);
}
void Timer1_Callback(void)
{
TickType_t tick_num1;
Timer1_Count++;
/* 获取滴答定时器的计数值 */
tick_num1 = xTaskGetTickCount();
USART1_SendString("Timer1_Callback executed ");
USART1_SendStringNumber(Timer1_Count, 5);
USART1_SendString(" times! ===>");
USART1_SendString("Tick Timer1 Value = ");
USART1_SendStringNumber(tick_num1, 5);
USART1_SendString(".\n");
}
void Timer2_Callback(void)
{
TickType_t tick_num2;
Timer2_Count++;
/* 获取滴答定时器的计数值 */
tick_num2 = xTaskGetTickCount();
USART1_SendString("Timer2_Callback executed ");
USART1_SendStringNumber(Timer2_Count, 5);
USART1_SendString(" times! ===>");
USART1_SendString("Tick Timer2 Value = ");
USART1_SendStringNumber(tick_num2, 5);
USART1_SendString(".\n");
}
END