文章

FreeRTOS软件定时器

软件定时器

MCU 一般都自带定时器,属于硬件定时器,但是不同的 MCU 其硬件定时器数量不同,有时需要考虑成本的问题。在硬件定时器不够用的时候,FreeRTOS也提供了定时器功能,不过是属于软件定时器,其定时精度没有硬件定时器高,但是对于精度要求不高的周期性任务也足够了

1. 软件定时器介绍

软件定时器允许设置一段时间,当设置的时间到达之后就会执行回调函数。软件定时器的回调函数是在定时器服务任务中执行的,因此不能在回调函数中调用会阻塞任务的 API 函数

定时器是一个可选的、不属于 FreeRTOS 内核的功能,是由定时器服务任务来提供的。定时器相关 API 函数大多是使用定时器命令队列发送命令给定时器服务任务的,用户不能直接访问该命令队列

img

如上图示,定时器命令队列将用户应用任务和定时器服务任务连接在一起。用户应用程序调用了函数 xTimerReset(),其结果是复位命令会被发送到定时器命令队列中,再由定时器服务任务来处理这个命令

软件定时器分为两种:单次定时器和周期定时器。单次定时器在定时时间到了后执行一次回调函数就会停止运行;周期定时器一旦启动就会周期性的执行回调函数

img

定时器的相关配置:

configUSE_TIMERS 宏置 1:自动创建定时器服务任务configTIMER_TASK_PRIORITY:软件定时器服务任务的任务优先级configTIMER_QUEUE_LENGTH:设置定时器命令队列的队列长度configTIMER_TASK_STACK_DEPTH:设置定时器服务任务的任务堆栈大小

FreeRTOS 启动调度器的时候会自动创建定时器服务任务,其源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
BaseType_t xTimerCreateTimerTask( void ){
  BaseType_t xReturn = pdFAIL;
  /* 检查软件定时器列表和队列,若没有创建内存空间则新建	*/
  prvCheckForValidListAndQueue();
  if( xTimerQueue != NULL ){
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	{
	  /*****此处省略静态创建代码*****/
	}
	#else
	{
	  /* 为了满足软件定时器的实时性,软件定时器任务的优先级设置为最大值 */
	  xReturn = xTaskCreate(prvTimerTask,
							"Tmr Svc",
							configTIMER_TASK_STACK_DEPTH,
							NULL,
		((UBaseType_t)configTIMER_TASK_PRIORITY)|portPRIVILEGE_BIT,
							&xTimerTaskHandle);
		}
	#endif /* configSUPPORT_STATIC_ALLOCATION */
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
  configASSERT( xReturn );
  return xReturn;
}
/* 检查软件定时器列表和队列 */
static void prvCheckForValidListAndQueue( void ){
  taskENTER_CRITICAL();
  {
	/* 若队列为空,则进行列表的初始化和队列的创建 */
	if( xTimerQueue == NULL ){
	  vListInitialise( &xActiveTimerList1 );
	  vListInitialise( &xActiveTimerList2 );
	  pxCurrentTimerList = &xActiveTimerList1;
	  pxOverflowTimerList = &xActiveTimerList2;
	  /* 开始创建消息队列 */
	  #if( configSUPPORT_STATIC_ALLOCATION == 1 )
	  {
		/*****************************/
		/*****此处省略静态创建代码*****/
		/*****************************/
	  }
	  #else
	  {
		xTimerQueue = xQueueCreate((UBaseType_t)configTIMER_QUEUE_LENGTH, sizeof(DaemonTaskMessage_t));
	  }
	  #endif
	  /*****************************/
	  /*****此处省略部分其他代码*****/
	  /*****************************/
	}
	else
	{
	  mtCOVERAGE_TEST_MARKER();
	}
  }
  taskEXIT_CRITICAL();
}

2. 软件定时器 API 函数

2.1 复位软件定时器

复位软件定时器,若软件定时器已经启动,则重新计算超时时间;若软件定时器没有启动,则启动软件定时器

1
2
3
4
5
6
7
8
/********************复位软件定时器,用在任务中************************************/
BaseType_t xTimerReset(TimerHandle_t xTimer,	//要复位的软件定时器句柄
					TickType_t xTicksToWait)	//阻塞时间
/********************复位软件定时器,用在中断服务函数中*****************************/
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer, //要复位的软件定时器句柄
			BaseType_t * pxHigherPriorityTaskWoken) //退出后是否进行任务切换
/********************************************************************************/
返回值:定时器复位成功返回pdPASS;失败返回pdFAIL

复位定时器函数是一个宏,最终调用 xTimerGenericCommand() 函数

1
2
3
4
5
6
7
#define xTimerReset(xTimer, xTicksToWait) 			\
		xTimerGenericCommand((xTimer), 				\
							 tmrCOMMAND_RESET, 		\
							 (xTaskGetTickCount()), \
							 NULL, 					\
							 (xTicksToWait))		\
//参数:1、软件定时器句柄;2、定义Reset编号;3、当前的系统的Tick值;4、null;5、阻塞时间
2.2 创建软件定时器

创建一个软件定时器,并返回一个软件定时器句柄。创建软件定时器后,软件定时器并没有启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/********************动态创建软件定时器*******************************************/
TimerHandle_t xTimerCreate(char * const pcTimerName,//软件定时器名字
					 TickType_t xTimerPeriodInTicks,//定时器周期(单位是时钟节拍数)
						   UBaseType_t uxAutoReload,//定时器模式(单次还是周期)
					         void * const pvTimerID,//定时器ID号
		 TimerCallbackFunction_t pxCallbackFunction)//定时器回调函数
/********************静态创建软件定时器*******************************************/
TimerHandle_t xTimerCreateStatic(char * const pcTimerName,//软件定时器名字
					 TickType_t xTimerPeriodInTicks,//定时器周期(单位是时钟节拍数)
						   UBaseType_t uxAutoReload,//定时器模式(单次还是周期)
					         void * const pvTimerID,//定时器ID号
		 TimerCallbackFunction_t pxCallbackFunction,//定时器回调函数
		              StaticTimer_t * pxTimerBuffer)//保存定时器结构体
/********************************************************************************/
返回值:创建成功返回软件定时器句柄;失败返回NULL

创建软件定时器函数 xTimerCreate() 的源码分析如下示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
TimerHandle_t xTimerCreate(	const char * const pcTimerName,
							const TickType_t xTimerPeriodInTicks,
							const UBaseType_t uxAutoReload,
							void * const pvTimerID,
							TimerCallbackFunction_t pxCallbackFunction){
  Timer_t *pxNewTimer;
  //动态分配 软件定时器控制块内存空间
  pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );
  if( pxNewTimer != NULL ){
	//进入控制初始化
	prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );
  }
  return pxNewTimer;
}
	
static void prvInitialiseNewTimer(const char * const pcTimerName,
								  const TickType_t xTimerPeriodInTicks,
								  const UBaseType_t uxAutoReload,
								  void * const pvTimerID,
								  TimerCallbackFunction_t pxCallbackFunction,
								  Timer_t *pxNewTimer){
  /* 0 is not a valid value for xTimerPeriodInTicks. */
  configASSERT( ( xTimerPeriodInTicks > 0 ) );
  if( pxNewTimer != NULL ){
	/* 再次判断是否已经创建 队列 初始化了列表 */
	prvCheckForValidListAndQueue();
	/* 1、进行软件定时器控制块信息的赋值	2、把当前软件定时器列表项初始化,便于以后使用*/
	pxNewTimer->pcTimerName = pcTimerName;
	pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;
	pxNewTimer->uxAutoReload = uxAutoReload;
	pxNewTimer->pvTimerID = pvTimerID;
	pxNewTimer->pxCallbackFunction = pxCallbackFunction;
	vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );
	traceTIMER_CREATE( pxNewTimer );
  }
}
2.3 开启软件定时器
1
2
3
4
5
6
7
8
/****************开启软件定时器,用在任务中***********************************/
BaseType_t xTimerStart(TimerHandle_t xTimer,	//要复位的软件定时器句柄
					TickType_t xTicksToWait)	//阻塞时间
/****************开启软件定时器,用在中断服务函数中****************************/
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer, //要复位的软件定时器句柄
			BaseType_t * pxHigherPriorityTaskWoken) //退出后是否进行任务切换
/********************************************************************************/
返回值:定时器开启成功返回pdPASS;失败返回pdFAIL

开始软件定时器函数是一个宏,最终调用 xTimerGenericCommand() 函数

1
2
3
4
5
6
7
#define xTimerStart(xTimer, xTicksToWait)           \
		xTimerGenericCommand((xTimer),              \
							 tmrCOMMAND_START, 		\
					         (xTaskGetTickCount()), \
							 NULL, 					\
							 (xTicksToWait))        \
//参数:1、软件定时器句柄;2、定义Start编号;3、当前的系统的Tick值;4、null;5、阻塞时间
2.4 停止软件定时器
1
2
3
4
5
6
7
8
/****************停止软件定时器,用在任务中***********************************/
BaseType_t xTimerStop(TimerHandle_t xTimer,		//要复位的软件定时器句柄
					TickType_t xTicksToWait)	//阻塞时间
/****************停止软件定时器,用在中断服务函数中****************************/
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,  //要复位的软件定时器句柄
			BaseType_t * pxHigherPriorityTaskWoken) //退出后是否进行任务切换
/********************************************************************************/
返回值:定时器停止成功返回pdPASS;失败返回pdFAIL

停止软件定时器函数是一个宏,最终也是调用 xTimerGenericCommand() 函数

1
2
3
4
5
6
7
#define xTimerStop(xTimer, xTicksToWait)            \
		xTimerGenericCommand((xTimer),              \
							 tmrCOMMAND_STOP, 		\
					         0U, 					\
							 NULL, 					\
							 (xTicksToWait))        \
//参数:1、软件定时器句柄;2、定义Stop编号;3、不需要传入消息;4、null;5、阻塞时间

函数 xTimerGenericCommand() 的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
BaseType_t xTimerGenericCommand(TimerHandle_t xTimer, 
						 const BaseType_t xCommandID,//标识触发的类型
					 const TickType_t xOptionalValue,//xTaskGetTickCount
 		BaseType_t * const pxHigherPriorityTaskWoken,
  					  const TickType_t xTicksToWait){
  BaseType_t xReturn = pdFAIL;
  DaemonTaskMessage_t xMessage;
  if( xTimerQueue != NULL ){
	xMessage.xMessageID = xCommandID;
	xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;
	xMessage.u.xTimerParameters.pxTimer = ( Timer_t * ) xTimer;
	/* 判断命令类型 */
	if( xCommandID < tmrFIRST_FROM_ISR_COMMAND ){
	  /* 判断调度器状态 */
	  if( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING ){
		xReturn = xQueueSendToBack(xTimerQueue, &xMessage, xTicksToWait);
	  }
	  else{
		xReturn = xQueueSendToBack(xTimerQueue, &xMessage, tmrNO_DELAY);
	  }
	}
	else{
	  xReturn = xQueueSendToBackFromISR(xTimerQueue,&xMessage,pxHigherPriorityTaskWoken);
	}
	traceTIMER_COMMAND_SEND(xTimer, xCommandID, xOptionalValue, xReturn);
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
  return xReturn;
}

3. 软件定时器应用实例

本实例介绍 FreeRTOS 软件定时的的创建、开启和停止函数的操作与使用

使用 STM32CubeMX 将 FreeRTOS 移植到工程中,创建一个任务、两个软件定时器(单次和周期定时器)

TimerControl_Task:控制两个软件定时器的开启和停止PeriodicTimer:周期定时器,定时周期为 1000msOnceTimer:单次定时器,定时周期为 2000ms

3.1 STM32CubeMX 设置
  • RCC 设置外接 HSE,时钟设置为 72M
  • PC0/PC1 设置为 GPIO 推挽输出模式、上拉、高速、默认输出电平为高电平
  • PA0 设置为 GPIO 输入模式、下拉模式;PE2/PE3/PE4 设置为 GPIO 输入模式、上拉模式
  • USART1 选择为异步通讯方式,波特率设置为 115200Bits/s,传输数据长度为 8Bit,无奇偶校验,1 位停止位;
  • 激活 FreeRTOS,添加任务,设置任务名称、优先级、堆栈大小、函数名称等参数

img

  • 使能软件定时器,设置优先级等参数

img

  • 添加单次定时器和周期定时器

img

  • 使用 FreeRTOS 操作系统,需要将 HAL 库的 Timebase Source 从 SysTick 改为其他定时器,选好定时器后,系统会自动配置 TIM
  • 输入工程名,选择路径(不要有中文),选择 MDK-ARM V5;勾选 Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击 GENERATE CODE,生成工程代码
3.2 MDK-ARM 软件编程
  • 创建按键驱动文件 key.c 和 key.h,参考按键输入例程
  • 添加 TimerControl_Task 任务函数代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void TimerControl_Task(void const * argument){
  uint8_t key;
  for(;;){	
	if((PeriodicTimerHandle != NULL)&&(OnceTimerHandle != NULL)){
	  key = KEY_Scan(0);
	  switch(key){
		case KEY_UP_PRES :
		  /* 设置软件定时器周期值 */
		  xTimerChangePeriod(PeriodicTimerHandle,1000,0);
		  xTimerStart(PeriodicTimerHandle,0);
		  printf("PeriodicTimer Start......!\r\n");
		  break;
		case KEY_DOWN_PRES :
		  xTimerStop(PeriodicTimerHandle,0);
		  printf("PeriodicTimer Stop******!\r\n");
		  break;
		case KEY_LEFT_PRES :
		  /* 设置软件定时器周期值 */
		  xTimerChangePeriod(OnceTimerHandle,2000,0);
		  xTimerStart(OnceTimerHandle,0);
		  printf("OnceTimer Start......!\r\n");
		  break;
		case KEY_RIGHT_PRES :
		  xTimerStop(OnceTimerHandle,0);
		  printf("OnceTimer Stop******!\r\n");
		  break;
	  }
	}
    osDelay(10);
  }
}
  • 添加周期定时器和单次定时器的回调函数
1
2
3
4
5
6
7
8
void OnceTimer_Callback(void const * argument){
  HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);
  printf("OnceTimer Finished!\r\n");
}

void PeriodicTimer_Callback(void const * argument){
  HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_1); 
}
3.3 下载验证

编译无误下载到开发板后,打开串口调试助手

按下 KEY_UP 按键,开启周期定时器,此时 LED2 指示灯每隔 1s 闪烁一次按下 KEY_DOWN 按键,关闭周期定时器,LED2 停止闪烁按下 KEY_LEFT 按键,开启单次定时器,2s 后 LED1 状态翻转,停止运行按下 KEY_RIGHT 按键,关闭单次定时器

img

以上转载自博主“安迪西嵌入式”的FreeRTOS专栏,仅作学习记录,如有侵权,请联系删除。

本文由作者按照 CC BY 4.0 进行授权