目录
(1)RCC、SYS、Code Generator、USART3、TIM6、NVIC
在FreeRTOS中,自动创建的任务有空闲任务和定时器服务任务。FreeRTOS可以通过定时器服务任务提供软件定时器功能。在某些对定时精度要求不太高,无须使用硬件定时器的情况下,可以使用FreeRTOS的软件定时器。
一、软件定时器概述
1、软件定时器的特性
软件定时器(software timer)是FreeRTOS中的一种对象,它的功能与一般高级语言中的软件定时器的功能类似。FreeRTOS中的软件定时器不直接使用任何硬件定时器或计数器,而是依赖系统中的定时器服务任务(timer service task)来工作。定时器服务任务也称为守护任务(daemon task)。
软件定时器有一个定时周期,还有一个回调函数。在定时器开始工作后,当流逝的时间达到定时周期时,就会执行其回调函数。根据回调函数执行的频度,软件定时器分为以下两种类型。
(1)软件定时器的两种类型
- 单次定时器(one-shot timer),回调函数执行一次后,定时器就停止工作。
- 周期定时器(periodic timer),回调函数会循环执行,定时器一直工作。
(2)软件定时器的两种状态
定时器有休眠和运行两种状态。
1) 休眠(dormant)状态
处于休眠状态的定时器不会执行其回调函数,但是可以对其进行操作,例如设置其定时周期。定时器在以下几种情况下处于休眠状态。
- 定时器创建后,就处于休眠状态。
- 单次定时器执行一次回调函数后,进入休眠状态。
- 定时器使用函数xTimerStop()停止后,进入休眠状态。
2) 运行(running)状态
处于运行状态的定时器,不管是单次定时器,还是周期定时器,在流逝的时间达到定时周期时,都会执行其回调函数。定时器在以下几种情况下处于运行状态。
- 使用函数xTimerStart()启动后,定时器进入运行状态。
- 定时器在运行状态时,被函数xTimerReset()复位起始时间后,依然处于运行状态。
软件定时器的各种操作实际上是在系统的定时器服务任务里完成的。与空闲任务一样,定时器服务任务是FreeRTOS自动创建的一个任务,如果要使用软件定时器,就必须创建此任务。在用户任务里执行的各种指令,例如,启动定时器xTimerStart()、复位定时器xTimerReset()、停止定时器xTimerStop()等,都是通过一个队列发送给定时器服务任务的,这个队列称为定时器指令队列(timer command queue)。定时器服务任务读取定时器指令队列里的指令,然后执行相应的操作。
用户任务、定时器指令队列、定时器服务任务之间的关系如下图所示。定时器服务任务和定时器指令队列是FreeRTOS自动创建的,其操作都是内核实现的,使用定时器只需在用户任务里执行相应的函数即可。
除了执行定时器指令队列里的指令,定时器服务任务还在定时到期(expire)时执行定时器的回调函数。由于FreeRTOS里的延时功能就是由定时器服务任务实现的,因此在定时器的回调函数里,不能出现使系统进入阻塞状态的函数,如vTaskDelay()、vTaskDelayUntil()等。回调函数可以调用等待信号量、事件组等对象的函数,但是等待的节拍数必须设置为0。
2、软件定时器的相关配置
在FreeRTOS中,使用软件定时器需要进行一些相关参数的配置。在CubeMX中,FreeRTOS的Config parameters页面中的Software timer definitions里有一组参数,其默认设置如下图。这4个参数的意义如下。
- USE_TIMERS,是否使用软件定时器,默认为Enabled,且不可修改。使用软件定时器时,系统就会自动创建定时器服务任务
- TIMER_TASK_PRIORITY,定时器服务任务的优先级,默认值是2,比空闲任务的优先级高(空闲任务的优先级为0)。设置范围是0~55,因为总的优先级个数是56。
- TIMER_QUEUE_LENGTH,定时器指令队列的长度,设置范围是1~255。
- TIMER_TASK_STACK_DEPTH,定时器服务任务的栈空间大小,默认值是256Words,设置范围是128~32768个字。
3、定时器服务任务的优先级
定时器服务任务是FreeRTOS中的一个普通任务,与空闲任务一样,它也参与系统的任务调度。定时器服务任务执行定时器指令队列中的定时器操作指令,或定时器的回调函数。定时器服务任务的优先级由参数configTIMER_TASK_PRIORITY设定,至少要高于空闲任务的优先级,默认值为2。
使用定时器的用户任务的优先级可能高于定时器服务任务的优先级,也可能低于定时器服务任务的优先级,所以,定时器服务任务执行定时器操作指令的时机是不同的。假设系统中只有一个用户任务TaskA操作定时器,其优先级低于定时器服务任务(也就是Daemon Task)的优先级,那么在任务TaskA中执行一个xTimerStart()指令时,任务的执行时序如图所示。
- 在t2时刻,用户任务TaskA调用函数xTimerStart(),实际上是向定时器指令队列写入指令,这会使定时器服务任务退出阻塞状态,因为其优先级高于用户任务TaskA,它会抢占执行,所以TaskA进入就绪状态,定时器服务任务进入运行状态。
- 在t3时刻,定时器服务任务处理完TaskA发送到队列中的定时器操作指令后,重新进入阻塞状态,用户任务TaskA重新进入运行状态。
- 在t4时刻,用户任务TaskA才从调用函数xTimerStart()中退出,继续执行TaskA里的其他代码。
- 在t5时刻,用户任务TaskA进入阻塞状态,空闲任务进入运行状态。
如果用户任务TaskA的优先级高于定时器服务任务的优先级,则任务的执行时序如下图所示。
- 在t2时刻,任务TaskA调用函数xTimerStart(),向定时器指令队列发送指令。TaskA的优先级高于定时器服务任务的优先级,所以定时器服务任务接收队列指令后,也不能抢占CPU进入运行状态,而只能进入就绪状态。
- 在t3时刻,任务TaskA从函数xTimerStart()返回,继续执行后面的代码。
- 在t4时刻,任务TaskA处理结束,进入阻塞状态,定时器服务任务进入运行状态,处理定时器指令队列里的指令。
- 在t5时刻,定时器服务任务处理完指令后进入阻塞状态,空闲任务进入运行状态。
综上,定时器服务任务处理定时器指令队列中的指令的时机是不同的。但是,不管是哪种情况,定时器的起始时间都是从发送“启动定时器”指令到队列开始计算的,也就是从调用xTimerStart()函数或xTimerReset()函数的时刻开始计算,而不是从定时器服务任务执行相应指令的时刻开始计算。例如图中,定时器的启动时刻是t2,而不是t4。
二、软件定时器的相关函数
1、相关函数概述
软件定时器相关的函数在文件timers.h和timers.c中予以定义和实现,在用户任务程序中,可以调用的常用函数见下表:
分组 | 函数 | 功能 |
创建和删除 | xTimerCreate() | 创建一个定时器,动态分配内存 |
xTimerCreateStatic() | 创建一个定时器,静态分配内存 | |
xTimerDelete() | 删除一个定时器 | |
查询和设置 | pcTimerGetName() | 返回定时器的字符串名称 |
vTimerSetTimerID() | 设置定时器ID | |
pvTimerGetTimerID() | 获取定时器ID | |
xTimerChangePeriod() | 修改定时周期,周期用节拍数表示 | |
xTimerChangePeriodFromISR() | xTimerChangePeriod()的ISR版本 | |
xTimerGetPeriod() | 返回定时器的定时周期,单位是节拍数 | |
xTimerIsTimerActive() | 查询一个定时器是否处于活动状态 | |
xTimerGetExpiryTime() | 查询定时器还需多少个节拍才能产生定时到期 | |
启动、停止 | xTimerStart() | 启动一个定时器 |
xTimerStartFromISR() | xTimerStart()的ISR版本 | |
xTimerStop() | 停止一个定时器 | |
xTimerStopFromISR() | xTimerStop()的ISR版本 | |
xTimerReset() | 复位一个定时器,重新设置定时器的起始时间 | |
xTimerResetFromISR() | xTimerReset()的ISR版本 |
2、部分函数详解
(1)创建定时器
函数xTimerCreate()以动态分配内存方式创建定时器,函数xTimerCreateStatic()以静态分配内存方式创建定时器。一般使用动态分配内存方式创建定时器,其原型定义如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
TimerHandle_t xTimerCreate( const char * const pcTimerName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )
{
//
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
/*-----------------------------------------------------------*/
各个参数的意义如下:
- pcTimerName是定时器的字符串名称。创建定时器后,可以调用pcTimerGetName()返回这个字符串。
- xTimerPeriodInTicks是定时周期,用节拍数表示,可以使用宏函数pdMS_TO_TICKS()将毫秒时间转换为节拍数。
- uxAutoReload用于设置定时器的类型,pdTRUE表示周期定时器,pdFALSE表示单次定时器。定时器的类型无法在创建后更改。
- pvTimerID为定时器的ID。如果多个定时器使用同一个回调函数,则可以通过这个ID来区分定时器。创建定时器后,可以调用vTimerSetTimerID()重新设置一个定时器的ID,可以调用函数pVTimerGetTimerID()返回一个定时器的ID。
- pxCallbackFunction是回调函数的名称。类型TimerCallbackFunction_t是回调函数类型指针,其原型定义如下:
/*
* Defines the prototype to which timer callback functions must conform.
*/
typedef void (*TimerCallbackFunction_t)( TimerHandle_t xTimer );
所以,回调函数有固定的输入参数定义,传递的参数xTimer就是定时器的句柄。
函数xTimerCreate()的返回值是所创建的定时器的句柄,在操作定时器的函数里,都需要使用这个句柄作为定时器的引用。函数返回值类型是TimerHandle_t,就是一个void类型指针,其原型定义如下:
typedef void*TimerHandle_t;
定时器创建后,处于休眠状态,需要使用函数xTimerStart()启动定时器,定时器才能进入运行状态。
在CubeMX中配置FreeRTOS时,用户可以可视化地创建软件定时器,在生成的代码中用CMSIS-RTOS标准接口函数osTimerNew()创建定时器,它会在内部自动调用xTimerCreate()或xTimerCreateStatic()。
(2)设置定时周期
在创建定时器时,用户就可以设置其定时周期。在创建定时器后,用户可以使用函数xTimerChangePeriod()在休眠状态或运行状态下,修改定时器的定时周期。其原型定义如下:
#define xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD, ( xNewPeriod ), NULL, ( xTicksToWait ) )
函数xTimerChangePeriod()中3个参数的意义是:XTimer是定时器的句柄;xNewPeriod是设置的新的定时周期,用节拍数表示;XTicksToWait是将此指令发送到定时器指令队列时,等待的节拍数。前文已经介绍过,在用户任务中执行的定时器操作函数,实际上就是向定时器指令队列发送消息,队列可能暂时没有剩余空间,那么函数就需要等待。
函数xTimerChangePeriod()的返回值为pdTRUE或pdFALSE。pdFALSE表示在等待超时后,指令还没有发送给定时器指令队列。pdTRUE表示指令成功发送到了定时器指令队列。至于定时器服务任务何时执行队列中的指令,则由任务调度器根据各个任务的优先级决定。其他进行定时器操作的函数,如xTimerStart()、xTimerStop()等的返回值都与此相同,表示的意义都是指令是否成功发送到了定时器指令队列。
xTimerChangePeriod()实际上是个宏定义函数,它调用了函数xTimerGenericCommand(),这是向定时器指令队列发送指令的通用函数,xTimerStart()、xTimerStop()等函数也是调用此函数,只是传递的参数不同。函数xTimerGenericCommand()的原型定义如下:
BaseType_t xTimerGenericCommand( TimerHandle_t xTimer, const BaseType_t xCommandID, const TickType_t xOptionalValue, BaseType_t * const pxHigherPriorityTaskWoken, const TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
其中各个参数的意义如下:
- xTimer是所操作的定时器的句柄。
- xCommandID是执行的定时器操作指令ID,这些指令ID都是一些宏定义常数。所有的指令ID定义如下,从指令ID名称即可知道其作用,例如,tmrCOMMAND_START表示启动定时器。
/*-----------------------------------------------------------
* MACROS AND DEFINITIONS
*----------------------------------------------------------*/
/* IDs for commands that can be sent/received on the timer queue. These are to
be used solely through the macros that make up the public software timer API,
as defined below. The commands that are sent from interrupts must use the
highest numbers as tmrFIRST_FROM_ISR_COMMAND is used to determine if the task
or interrupt version of the queue send function should be used. */
#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 )
- xOptionalValue是指令的参数,例如,函数xTimerChangePeriod()在调用xTimerGenericCommand()时,需要传递xNewPeriod作为指令的参数。
- pxHigherPriorityTaskWoken是一个BaseType_t类型的指针型变量,是一个返回数据,表示执行完函数后是否需要进行上下文切换,这个参数是ISR版本里使用的。
- xTicksToWait是执行函数时的等待节拍数,也就是向队列写入数据时等待的节拍数。
在中断ISR里修改定时器周期的是xTimerChangePeriodFromISR(),其原型定义如下:
#define xTimerChangePeriodFromISR( xTimer, xNewPeriod, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, ( xNewPeriod ), ( pxHigherPriorityTaskWoken ), 0U )
这个函数也是调用函数xTimerGenericCommand(),返回的参数pxHigherPriorityTaskWoken表示在退出ISR时是否需要申请进行上下文切换。在调用函数xTimerGenericCommand()时,其最后一个参数xTicksToWait被设置为0。
执行xTimerChangePeriodFromISR()返回的参数pxHigherPriorityTaskWoken用于申请进行上下文切换,也就是作为portYIELD_FROM_ISR()的参数。执行的示意代码如下:
BaseType_t taskWoken= pdFALSE;
xTimerChangePeriodFromISR( xTimer, xNewPeriod, &taskWoken);
portYIELD_FROM_ISR(taskWoken);
(3)查询定时器定时周期
函数xTimerGetPeriod()用于返回定时器的定时周期,其原型定义如下:
TickType_t xTimerGetPeriod( TimerHandle_t xTimer ) PRIVILEGED_FUNCTION;
其中,参数xTimer是所操作的定时器的句柄,其返回值是用节拍数表示的定时周期。
(4)查询定时器是否处于运行状态
函数xTimerIsTimerActive()用于查询一个定时器是否处于运行状态,其原型定义如下:
BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer ) PRIVILEGED_FUNCTION;
函数若返回pdTRUE,则表示定时器处于运行状态;否则,就是处于休眠状态。定时器处于运行状态是指定时器已经开始计时,定时到期时会运行其回调函数。
(5)启动定时器
启动定时器使用的函数是xTimerStart()或xTimerStartFromISR(),它们都是宏函数,实际都是调用函数xTimerGenericCommand()。例如,xTimerStart()的定义如下:
#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )
启动定时器也是发送指令到定时器指令队列,需要设置等待超时时间xTicksToWait。如果函数返回pdTRUE,则表示启动定时器的指令成功发送给了定时器指令队列。
函数xTimerStart()在调用xTimerGenericCommand()时传递的指令参数是xTaskGetTickCount(),也就是FreeRTOS嘀嗒计数器当前的计数值,这会作为定时器计时的起始时间。所以,定时器的起始时间是从指令发送到指令队列的时刻开始计算的,而不是从定时器服务任务执行指令的时刻开始计算的。
定时器启动后进入运行状态,在定时到期时执行其回调函数。如果是单次定时器,执行完一次回调函数后,定时器就进入休眠状态;如果是周期定时器,定时器会重新开始计时,到下一个周期时间到时,再运行一次回调函数,如此循环,所以定时器一直处于运行状态。
可以在FreeRTOS的内核启动之前,调用xTimerStart()启动定时器,例如,在函数MX_FREERTOS_Init()中调用xTimerStart(),但是定时器实际启动是要等到内核启动之后的。所以,如果要使定时更加精确,可以在主任务里启动定时器。
函数xTimerStart()相应的ISR版本是xTimerStartFromISR(),其原型定义如下:
(6)停止定时器
执行函数xTimerStop()可以使处于运行状态的定时器停止,进入休眠状态。其原型定义如下:
#define xTimerStop( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, ( xTicksToWait ) )
相应的ISR版本是xTimerStopFromISR(),其原型定义如下:
#define xTimerStopFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP_FROM_ISR, 0, ( pxHigherPriorityTaskWoken ), 0U )
(7)复位定时器
复位定时器的函数是xTimerReset()。若定时器处于休眠状态,xTimerReset()的作用与xTimerStart()完全相同;若定时器处于运行状态,则xTimerReset()会将定时器的起始时刻重置为当前时刻。函数xTimerReset()的原型定义如下:
#define xTimerReset( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
对一个定时周期为5的定时器使用xTimerStart()和xTimerReset()函数,运行时序如图所示。
- 在t1时刻,启动定时器,定时器的预期到期时刻为t6。
- 在t4时刻,调用xTimerReset()复位定时器,重新计算定时器的到期时刻,此时变为t9。
- 在t9时刻,定时时间到,执行定时器的回调函数。
定时器复位函数的ISR版本是xTimerResetFromISR(),其原型定义如下:
#define xTimerResetFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET_FROM_ISR, ( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )
三、软件定时器使用示例
1、示例功能和CubeMX项目设置
本示例演示软件定时器的使用,本示例的功能和工作流程如下。
- 创建一个周期定时器Timer_Periodic,定时周期为1s,使LED1闪烁。
- 创建一个单次定时器Timer_Once,定时周期为5s,到期使LED2熄灭。
- 创建一个任务,在任务中检测KeyRight键,该键处于按下状态时使LED2点亮,并使Timer_Once复位。
-
继续使用旺宝红龙开发板STM32F407ZGT6 KIT V1.0。
-
引用KEYLED文件夹,用到2个LED和KeyRight。
-
一些设置可以参考本文作者发布的其他文章:
细说STM32单片机FreeRTOS消息缓冲区及其应用实例-CSDN博客 https://wenchm.blog.csdn.net/article/details/148189024?spm=1011.2415.3001.5331
(1)RCC、SYS、Code Generator、USART3、TIM6、NVIC
参数设置可见参考文章。
(2)GPIO
(3)FreeRTOS
在SYS组件模式设置中,设置TIM6作为基础时钟源。
启用FreeRTOS,设置接口为CMSIS_V2,所有“config”和“INCLUDE_”参数保持默认值。特别是软件定时器相关的设置,保持默认设置。
设计1个Task_Main任务:
在Timers and Semaphores页面设计两个定时器:
2、程序功能实现
(1)主程序
完成设置后,在CubeMX中生成代码。在CubelDE中打开项目,将KEY_LED驱动程序目录添加到项目的搜索路径。添加用户功能代码后,主程序代码如下:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "cmsis_os.h"
#include "usart.h"
#include "gpio.h"
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void MX_FREERTOS_Init(void);
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART3_UART_Init();
/* USER CODE BEGIN 2 */
// Start Menu
uint8_t startstr[] = "Demo10_1:Soft Timer.\r\n";
HAL_UART_Transmit(&huart3,startstr,sizeof(startstr),0xFFFF);
uint8_t startstr1[] = "LED1 toggle each 1sec.\r\n";
HAL_UART_Transmit(&huart3,startstr1,sizeof(startstr1),0xFFFF);
uint8_t startstr2[] = "LED2 will be off in 5sec.\r\n";
HAL_UART_Transmit(&huart3,startstr2,sizeof(startstr2),0xFFFF);
uint8_t startstr3[] = "Press KeyRight[S5] to reset or restart Timer_Once.\r\n\r\n";
HAL_UART_Transmit(&huart3,startstr3,sizeof(startstr3),0xFFFF);
/* USER CODE END 2 */
/* Init scheduler */
osKernelInitialize();
/* Call init function for freertos objects (in cmsis_os2.c) */
MX_FREERTOS_Init();
/* Start scheduler */
osKernelStart();
/* We should never get here as control is now taken by the scheduler */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
//以下代码省去。
(2)FreeRTOS对象初始化
使用函数MX_FREERTOS_Init()创建在CubeMX中设计的任务和两个定时器。文件freertos.c还包含两个定时器的回调函数代码框架。文件freertos.c中的FreeRTOS对象初始化相关代码如下,未展示任务函数和定时器回调函数的初始代码:
自动生成includes和手动添加私有includes:
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "timers.h"
#include "usart.h"
#include "keyled.h"
#include <stdio.h>
/* USER CODE END Includes */
手动添加私有全局变量:
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
uint32_t counter = 0;
/* USER CODE END Variables */
自动添加任务函数定义和软件定时器函数定义:
/* Definitions for Task_Main */
osThreadId_t Task_MainHandle;
const osThreadAttr_t Task_Main_attributes = {
.name = "Task_Main",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for Timer_Periodic */
osTimerId_t Timer_PeriodicHandle;
const osTimerAttr_t Timer_Periodic_attributes = {
.name = "Timer_Periodic"
};
/* Definitions for Timer_Once */
osTimerId_t Timer_OnceHandle;
const osTimerAttr_t Timer_Once_attributes = {
.name = "Timer_Once"
};
自动生成函数原型声明:
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
/* USER CODE END FunctionPrototypes */
void AppTask_Main(void *argument);
void AppTimer_Periodic(void *argument);
void AppTimer_Once(void *argument);
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
自动生成MX_FREERTOS_Init()函数初始化代码,在函数中自动创建软件定时器和任务函数:
/**
* @brief FreeRTOS initialization
* @param None
* @retval None
*/
void MX_FREERTOS_Init(void)
{
/* Create the timer(s) */
/* creation of Timer_Periodic */
Timer_PeriodicHandle = osTimerNew(AppTimer_Periodic, osTimerPeriodic, NULL, &Timer_Periodic_attributes);
/* creation of Timer_Once */
Timer_OnceHandle = osTimerNew(AppTimer_Once, osTimerOnce, NULL, &Timer_Once_attributes);
/* Create the thread(s) */
/* creation of Task_Main */
Task_MainHandle = osThreadNew(AppTask_Main, NULL, &Task_Main_attributes);
}
在函数MX_FREERTOS_Init()中创建定时器时使用了CMSIS-RTOS的标准接口函数osTimerNew(),这个函数的原型定义如下:
osTimerId_t osTimerNew (osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr);
其中,参数func是回调函数的名称;参数type表示定时器类型,是枚举类型osTimerType_t,枚举值oSTimerPeriodic表示周期定时器,osTimerOnce表示单次定时器;argument是创建定时器时的参数,一般设置为NULL;attr是定时器属性,是osTimerAttr_t结构体类型指针。
定时器的属性用osTimerAttr_t类型的结构体变量定义,这个结构体在文件cmsis_os2.h中定义,其定义如下,各成员变量的意义见注释。注意,这个属性结构体里没有定时周期参数。
/// Attributes structure for timer.
typedef struct {
const char *name; ///< name of the timer
uint32_t attr_bits; ///< attribute bits
void *cb_mem; ///< memory for control block
uint32_t cb_size; ///< size of provided memory for control block
} osTimerAttr_t;
从文件freertos.c中定时器的属性定义和创建定时器的代码可以看到,它设置了定时器字符串名称、回调函数和定时器类型,但是没有设置定时器的定时周期。跟踪osTimerNew()的源代码会发现,它创建的定时器的周期自动设置为1,所以在后面还需要调用函数xTimerChangePeriod()设置定时器的周期。
创建后的定时器处于休眠状态,需要用xTimerStart()函数启动定时器。
(3)任务和定时器的功能实现
进一步地,自动添加任务函数、软件定时器回调函数代码框架,并手动添加它们的函数体内容。本示例的主要功能由任务Task_Main的任务函数和两个定时器的回调函数完成。在文件freertos.c中添加用户功能代码,完成后的代码如下所示:
/* USER CODE BEGIN Header_AppTask_Main */
/**
* @brief Function implementing the Task_Main thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_AppTask_Main */
void AppTask_Main(void *argument)
{
/* USER CODE BEGIN AppTask_Main */
/* Infinite loop */
//Set timer cycle
xTimerChangePeriod(Timer_PeriodicHandle, pdMS_TO_TICKS(1000), portMAX_DELAY);
xTimerChangePeriod(Timer_OnceHandle, pdMS_TO_TICKS(5000), portMAX_DELAY);
LED2_ON();
counter = 0; //Clear count
xTimerStart(Timer_PeriodicHandle,portMAX_DELAY); //Start timer
xTimerStart(Timer_OnceHandle,portMAX_DELAY); //Start timer
for(;;)
{
KEYS cueKey=ScanPressedKey(20); //Detection button
if (cueKey==KEY_RIGHT) //KeyRight=S5
{
counter=0; //Clear count
if (xTimerIsTimerActive(Timer_OnceHandle)==pdFALSE) //Sleep state
LED2_ON(); //LED2 flare
xTimerReset(Timer_OnceHandle, portMAX_DELAY); //Timer_Once reset
vTaskDelay(300); //eliminate key jitter effects
}
else
vTaskDelay(10);
}
/* USER CODE END AppTask_Main */
}
/* AppTimer_Periodic function */
void AppTimer_Periodic(void *argument)
{
/* USER CODE BEGIN AppTimer_Periodic */
LED1_Toggle(); //LED1 flare
counter++; //Count value, the number of seconds after the last reset or startup of Timer_Once.
printf("counter Value= %ld\r\n",counter);
/* USER CODE END AppTimer_Periodic */
}
/* AppTimer_Once function */
void AppTimer_Once(void *argument)
{
/* USER CODE BEGIN AppTimer_Once */
LED2_Toggle();
/* USER CODE END AppTimer_Once */
}
代码解析:
任务Task_Main的代码在进入无限for循环之前,设置了两个定时器的定时周期,并启动了两个定时器。单次定时器Timer_Once在定时到期时,LED2熄灭。周期定时器Timer_Periodic的功能是使LED1输出翻转,并且每次使全局计数变量counter值加1,在串口助手上显示counter的值。
在任务Task_Main的for循环里检测KeyRight键是否按下,如果按下了,就使counter的值变为0,再调用函数xTimerIsTimerAtive()判断定时器Timer_Once是否处于休眠状态,如果处于休眠状态,就重新点亮LED2,再调用xTimerReset()复位定时器Timer_Once。如果定时器处于休眠状态,函数xTimerReset()的功能就与xTimerStart()一样。
进一步地,手动添加私有函数代码:
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart3,(uint8_t*)&ch,1,0xFFFF);
return ch;
}
/* USER CODE END Application */
3、运行与调试
构建项目后,我们将其下载到开发板上并运行测试,运行时会看到以下现象。
- 系统复位后,LED1闪烁,LED2点亮,串口助手上显示计数秒数。如果不按KeyRight键,计数到5之后LED2熄灭。LED2不会闪烁,说明单次定时器Timer_Once的回调函数只执行了1次。
- 如果在串口助手上显示的秒数达到5之前按下KeyRight键,会从0开始重新计数,计数到5之后LED2才熄灭,这说明函数xTimerReset()复位定时器时能使其重新开始计时。
- 如果LED2已经熄灭了,按KeyRight键,LED2会重新点亮,计数5s后又熄灭。说明定时器处于休眠状态时,函数xTimerReset()的功能与xTimerStart()一样。