12. 软件定时器

12.1. 软件定时器的基本概念

定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。 类似生活中的闹钟,我们可以设置闹钟每天什么时候响,还能设置响的次数,是响一次还是每天都响。

定时器有硬件定时器和软件定时器之分:

硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器, 接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。

软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上, 使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。

使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息; 而使用软件定时器时,需要我们在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数, 为了统一,下文均用回调函数描述),在回调函数中处理信息。

注意:软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。

软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。定时精度与系统时钟的周期有关。 一般系统利用SysTick作为软件定时器的基础时钟,软件定时器的回调函数类似硬件的中断服务函数, 所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况(软件定时器回调函数的上下文环境是任务), 比如vosDelay()以及其他能阻塞任务运行的函数,两次触发回调函数的时间间隔叫定时器的定时周期。

FreeRTOS操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。 FreeRTOS软件定时器功能上支持:

  • 裁剪:能通过宏关闭软件定时器功能。

  • 软件定时器创建。

  • 软件定时器启动。

  • 软件定时器停止。

  • 软件定时器复位。

  • 软件定时器删除。

FreeRTOS提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时间到之后都会调用软件定时器的回调函数, 用户可以在回调函数中加入要执行的工程代码。

单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器删除,不再重新执行。

周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除,具体见下图。

计数信号量

图19‑1软件定时器的单次模式与周期模式

FreeRTOS 通过一个prvTimerTask任务(也叫守护任务Daemon)管理软定时器,它是在启动调度器时自动创建的, 为了满足用户定时需求。prvTimerTask任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。 只有设置 FreeRTOSConfig.h中的宏定义configUSE_TIMERS设置为1 ,将相关代码编译进来,才能正常使用软件定时器相关功能。

12.2. 软件定时器应用场景

在很多应用中,我们需要一些定时器任务,硬件定时器受硬件的限制,数量上不足以满足用户的实际需求, 无法提供更多的定时器,那么可以采用软件定时器来完成,由软件定时器代替硬件定时器任务。 但需要注意的是软件定时器的精度是无法和硬件定时器相比的,因为在软件定时器的定时过程中是极有可能被其他中断所打断, 因为软件定时器的执行上下文环境是任务。所以,软件定时器更适用于对时间精度要求不高的任务,一些辅助型的任务。

12.3. 软件定时器的精度

在操作系统中,通常软件定时器以系统节拍周期为计时单位。系统节拍是系统的心跳节拍,表示系统时钟的频率,就类似人的心跳, 1s能跳动多少下,系统节拍配置为configTICK_RATE_HZ,该宏在FreeRTOSConfig.h中有定义,默认是1000。 那么系统的时钟节拍周期就为1ms(1s跳动1000下,每一下就为1ms)。软件定时器的所定时数值必须是这个节拍周期的整数倍, 例如节拍周期是10ms,那么上层软件定时器定时数值只能是10ms,20ms,100ms等,而不能取值为15ms。 由于节拍定义了系统中定时器能够分辨的精确度,系统可以根据实际系统CPU的处理能力和实时性需求设置合适的数值, 系统节拍周期的值越小,精度越高,但是系统开销也将越大,因为这代表在1秒中系统进入时钟中断的次数也就越多。

12.4. 软件定时器的运作机制

软件定时器是可选的系统资源,在创建定时器的时候会分配一块内存空间。当用户创建并启动一个软件定时器时, FreeRTOS会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表, FreeRTOS中采用两个定时器列表维护软件定时器,pxCurrentTimerList与pxOverflowTimerList是列表指针, 在初始化的时候分别指向xActiveTimerList1与xActiveTimerList2,具体如下。

软件定时器用到的列表
1
2
3
4
5
6
7
PRIVILEGED_DATA static List_t xActiveTimerList1;

PRIVILEGED_DATA static List_t xActiveTimerList2;

PRIVILEGED_DATA static List_t *pxCurrentTimerList;

PRIVILEGED_DATA static List_t *pxOverflowTimerList;

pxCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList列表中。 系统在定时器任务中扫描pxCurrentTimerList中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。 否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话, 那后面的定时器定时时间自然没到。

pxOverflowTimerList 列表是在软件定时器溢出的时候使用,作用与pxCurrentTimerList一致。

同时,FreeRTOS的软件定时器还有采用消息队列进行通信,利用“定时器命令队列”向软件定时器任务发送一些命令, 任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。假如定时器任务处于阻塞状态, 我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务, 并且在任务中将新添加的软件定时器添加到软件定时器列表中,所以,在定时器启动函数中, FreeRTOS是采用队列的方式发送一个消息给软件定时器任务,任务被唤醒从而执行接收到的命令。

例如:系统当前时间xTimeNow值为0,注意:xTimeNow其实是一个局部变量,是根据xTaskGetTickCount()函数获取的, 实际它的值就是全局变量xTickCount的值,下文都采用它表示当前系统时间。在当前系统中已经创建并启动了1个定时器Timer1; 系统继续运行,当系统的时间xTimeNow为20的时候,用户创建并且启动一个定时时间为100的定时器Timer2, 此时Timer2的溢出时间xTicksToWait就为定时时间+系统当前时间(100+20=120), 然后将Timer2按xTicksToWait升序插入软件定时器列表中;假设当前系统时间xTimeNow为40的时候, 用户创建并且启动了一个定时时间为50的定时器Timer3,那么此时Timer3的溢出时间xTicksToWait就为40+50=90, 同样安装xTicksToWait的数值升序插入软件定时器列表中,在定时器链表中插入过程具体见下图。

定时器链表示意图1

同理创建并且启动在已有的两个定时器中间的定时器也是一样的,如下

定时器链表示意图2

那么系统如何处理软件定时器列表?系统在不断运行,而xTimeNow(xTickCount)随着SysTick的触发一直在增长 (每一次硬件定时器中断来临,xTimeNow变量会加1),在软件定时器任务运行的时候会获取下一个要唤醒的定时器, 比较当前系统时间xTimeNow是否大于或等于下一个定时器唤醒时间xTicksToWait,若大于则表示已经超时, 定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。 以上图为例,讲解软件定时器调用回调函数的过程,在创建定Timer1并且启动后,假如系统经过了50个tick,xTimeNow从0增长到50, 与Timer1的xTicksToWait值相等,这时会触发与Timer1对应的回调函数,从而转到回调函数中执行用户代码, 同时将Timer1从软件定时器列表删除,如果软件定时器是周期性的, 那么系统会根据Timer1下一次唤醒时间重新将Timer1添加到软件定时器列表中,按照xTicksToWait的升序进行排列。 同理,在xTimeNow=40的时候创建的Timer3, 在经过130个tick后(此时系统时间xTimeNow是40,130个tick就是系统时间xTimeNow为170的时候), 与Timer3定时器对应的回调函数会被触发,接着将Timer3从软件定时器列表中删除, 如果是周期性的定时器,还会按照xTicksToWait升序重新添加到软件定时器列表中。

使用软件定时器时候要注意以下几点:

  • 软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的API接口, 在回调函数中也绝对不允许出现死循环。

  • 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为configTIMER_TASK_PRIORITY, 为了更好响应,该优先级应设置为所有任务中最高的优先级。

  • 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。

  • 定时器任务的栈大小默认为configTIMER_TASK_STACK_DEPTH个字节。

12.5. 软件定时器控制块

cmsis_os2.h
1
2
3
4
5
6
typedef struct {
   const char                   *name;   //软件定时器名
   uint32_t                 attr_bits;   //暂未使用到
   void                      *cb_mem;    //软件定时器控制块(静态创建)内存
   uint32_t                   cb_size;   //软件定时器控制块(静态创建)大小
} osTimerAttr_t;

其中cb_mem、cb_size成员变量用于软件定时器的静态创建, 当这两个成员变量为空时将使用动态内存分配的方式创建软件定时器。

12.6. 软件定时器函数接口讲解

12.6.1. 软件定时器创建函数osTimerNew()

软件定时器与FreeRTOS内核其他资源一样,需要创建才允许使用的,软件定时器创建函数osTimerNew(), 函数源码如下

cmsis_os2.h
 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
osTimerId_t osTimerNew (osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr) {
   const char *name;
   TimerHandle_t hTimer;
   TimerCallback_t *callb;
   UBaseType_t reload;
   int32_t mem;

   hTimer = NULL;
   //不能在中断中使用软件定时器函数
   if (!IS_IRQ() && (func != NULL)) {
   //以动态内存分配的方式分配TimerCallback_t 其中包含一个函数指针以及函数指针参数
      callb = pvPortMalloc (sizeof(TimerCallback_t));

      //填充定时器回调结构体
      if (callb != NULL) {
         callb->func = func;
         callb->arg  = argument;

         //软件定时器类型:单次还是循环
         if (type == osTimerOnce) {
            reload = pdFALSE;
         } else {
            reload = pdTRUE;
         }

         mem  = -1;
         name = NULL;

         if (attr != NULL) {
            if (attr->name != NULL) {
               name = attr->name;
            }

            if ((attr->cb_mem != NULL) && (attr->cb_size >= sizeof(StaticTimer_t))) {
               mem = 1;  //静态创建软件定时器标志
            }
            else {
               if ((attr->cb_mem == NULL) && (attr->cb_size == 0U)) {
               mem = 0; //动态创建软件定时器标志
               }
            }
         }
         else {
            mem = 0;
         }
         //静态内存创建软件定时器
         if (mem == 1) {
            hTimer = xTimerCreateStatic (name, 1, reload, callb, TimerCallback, (StaticTimer_t *)attr->cb_mem);
         }
         //动态内存创建软件定时器
         else {
            if (mem == 0) {
               hTimer = xTimerCreate (name, 1, reload, callb, TimerCallback);
            }
         }
      }
   }

   return ((osTimerId_t)hTimer);
}

参数

  • func :软件定时器的回调函数,当定时时间到达的时候就会调用这个函数。

  • type :定时器类型,使用单次类型还是循环类型。

  • argument :传给软件定时器回调函数的参数。

  • attr :描述软件定时器属性的结构体。

返回值: 若创建成功则返回事件组ID,用于访问创建的软件定时器。创建失败则返回NULL

12.6.2. 软件定时器删除函数osTimerDelete()

osTimerDelete()用于删除一个已经被创建成功的软件定时器,删除之后就无法使用该定时器, 并且定时器相应的资源也会被系统回收释放,其函数源码如下所示

cmsis_os2.c
 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
osStatus_t osTimerStop (osTimerId_t timer_id) {
   TimerHandle_t hTimer = (TimerHandle_t)timer_id;
   osStatus_t stat;

   //不能在中断里使用osTimerStop函数
   if (IS_IRQ()) {
      stat = osErrorISR;
   }
   //参数出错判断
   else if (hTimer == NULL) {
      stat = osErrorParameter;
   }
   else {
      //判断定时器是否可用
      if (xTimerIsTimerActive (hTimer) == pdFALSE) {
         stat = osErrorResource;
      }
      //停止定时器
      else {
         if (xTimerStop (hTimer, 0) == pdPASS) {
            stat = osOK;
         } else {
            stat = osError;
         }
      }
   }

   return (stat);
}

参数

  • timer_id :软件定时器ID

返回值:当成功时返回osOK,失败返回负值。

12.6.3. 软件定时器启动函数osTimerStart()

软件定时器在创建完成的时候是处于休眠状态的, 需要使用相关函数将软件定时器活动起来,而osTimerStart()函数就是可以让处于休眠的定时器开始工作,并设置定时器的时间。 其函数源码如下所示

cmsis_os2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
osStatus_t osTimerStart (osTimerId_t timer_id, uint32_t ticks) {
   TimerHandle_t hTimer = (TimerHandle_t)timer_id;
   osStatus_t stat;

   //不能在中断里使用osTimerStart函数
   if (IS_IRQ()) {
      stat = osErrorISR;
   }
   //参数判断
   else if (hTimer == NULL) {
      stat = osErrorParameter;
   }

   else {
      //设置定时器的运行周期
      if (xTimerChangePeriod (hTimer, ticks, 0) == pdPASS) {
         stat = osOK;
      } else {
         stat = osErrorResource;
      }
   }

   return (stat);
}

参数

  • timer_id :软件定时器ID

  • ticks :软件定时器需要设置的时间。

返回值:当成功时返回osOK,失败返回负值。

12.6.4. 软件定时器停止函数osTimerStop()

osTimerStop()用于停止一个已经启动的软件定时器,其函数源码如下所示

cmsis_os2.c
 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
osStatus_t osTimerStop (osTimerId_t timer_id) {
   TimerHandle_t hTimer = (TimerHandle_t)timer_id;
   osStatus_t stat;

   //不能在中断里使用osTimerStop函数
   if (IS_IRQ()) {
      stat = osErrorISR;
   }
   //参数出错判断
   else if (hTimer == NULL) {
      stat = osErrorParameter;
   }
   else {
      //判断定时器是否可用
      if (xTimerIsTimerActive (hTimer) == pdFALSE) {
         stat = osErrorResource;
      }
      //停止定时器
      else {
         if (xTimerStop (hTimer, 0) == pdPASS) {
            stat = osOK;
         } else {
            stat = osError;
         }
      }
   }
   return (stat);
}

参数

  • timer_id :软件定时器ID

返回值:当成功时返回osOK,失败返回负值。

12.7. 软件定时器实验

软件定时器实验是在STM32CubeIDE中创建了两个FreeRTOS软件定时器,其中一个软件定时器是单次模式, 5000个tick调用一次回调函数,另一个软件定时器是周期模式,1000个tick调用一次回调函数, 在回调函数中输出相关信息。

在本次实验中,我们并不需要在线程中运行什么代码,因此并不需要创建其他的线程,保持默认即可。 如下所示

消息队列配置

使用STM32CubeIDE创建了两个FreeRTOS定时器,如下所示

消息队列配置

软件定时器1配置如下

消息队列配置

软件定时器配置选项介绍如下

  • Timer Name :创建的定时器名

  • Callback : 回调函数入口名

  • Type :定时器类型,使用单次类型还是循环类型。这里配置定时器1的类型为周期循环模式。

  • Code Generation Option :生成定时器代码的方式,可使用弱定义的方式生成代码,这里我们选择保持默认。

  • Parameter :传给定时器回调函数的参数。

  • Allocation :创建软件定时器的方式,可选择使用静态创建还是使用动态内存分配。

  • Control Block Name 使用静态创建时软件定时器内存控制块类型变量名。

软件定时器2配置

消息队列配置

在软件定时器2中,配置定时器的类型为单次执行,当执行一次之后便停止。

使用STM32CubeIDE配置完FreeRTOS之后便可生成工程代码。 生成与FreeRTOS相关的代码在app_freertos.c文件中。

12.7.1. app_freertos.c

本小节只讲解重点部分代码,完整代码请打开工程查看。

12.7.1.1. MX_FREERTOS_Init函数

app_freertos.c
1
2
3
4
5
6
7
void MX_FREERTOS_Init(void) {

   Timer01Handle = osTimerNew(Swtmr1_Callback, osTimerPeriodic, NULL, &Timer01_attributes);
   Timer02Handle = osTimerNew(Swtmr2_Callback, osTimerOnce, NULL, &Timer02_attributes);

   defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
}

在MX_FREERTOS_Init函数中创建了两个软件定时器,使用osTimerNew创建软件定时器之后,定时器并没有开始工作, 还需要手动调用API启动定时器。

12.7.1.2. Swtmr1_Callback函数

Swtmr1_Callback函数是我们创建的第一个软件定时器的回调函数,在该函数中将打印本函数的调用次数、滴答定时器的数值 以及翻转LED1;

其源码如下所示

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void Swtmr1_Callback(void *argument)
{
   uint32_t tick_num1;

   TmrCb_Count1++;                                           /* 每回调一次加一 */

   tick_num1 = osKernelGetTickCount();       /* 获取滴答定时器的计数值 */

   LED1_TOGGLE;

   printf("Swtmr1_Callback函数执行 %lu 次\n", TmrCb_Count1);
   printf("滴答定时器数值=%lu\n", tick_num1);
}

12.7.1.3. Swtmr2_Callback函数

Swtmr2_Callback函数是第二个软件定时器的回调函数,其内容和第一个软件定时器的回调函数类似,代码如下所示

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void Swtmr2_Callback(void *argument)
{
   /* USER CODE BEGIN Swtmr2_Callback */
   uint32_t tick_num2;

   TmrCb_Count2++;                                           /* 每回调一次加一 */

   tick_num2 = osKernelGetTickCount();       /* 获取滴答定时器的计数值 */

   printf("Swtmr2_Callback函数执行 %lu 次\n", TmrCb_Count2);
   printf("滴答定时器数值=%lu\n", tick_num2);
   /* USER CODE END Swtmr2_Callback */
}

12.7.1.4. 启动软件定时器

现在所有的定时器已经准备就绪,只缺启动定时器了,用户可以在创建软件定时器之后便启动定时器, 这里我们在STM32CubeIDE默认生成的线程中启动软件定时器,如下所示

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void StartDefaultTask(void *argument)
{
   /* USER CODE BEGIN StartDefaultTask */
   osTimerStart (Timer01Handle, 1000);  //定时器1以1000个tick周期运行
   osTimerStart (Timer02Handle, 5000);  //定时器2以5000个tick的时间运行
   /* Infinite loop */
   for(;;)
   {
      osDelay(1);
   }
   /* USER CODE END StartDefaultTask */
}

12.8. 软件定时器实验现象

在串口助手中可看到定时器1每一秒运行一次,而定时器2在运行一次之后便不再运行。

消息队列配置