1.什么是软件定时器
软件定时器是一种由程序模拟的定时器,在需要使用大量定时器的情况下,可以通过一个硬件定时器模拟成许多软件定时器,这样可以克服硬件资源有限的问题。软件定时器的一个优点是数量不受限制。
然而,由于软件定时器是通过程序来实现的,它的运行和维护会消耗一定的CPU资源,并且相对于硬件定时器,它的精度也稍差一些。
2.软件定时器的实现原理
在像Linux、uC/OS和FreeRTOS等操作系统中,都提供了软件定时器,它们的实现原理基本相同。典型的实现方式是:利用一个硬件定时器产生固定的时钟节拍,每当硬件定时器中断发生时,就对一个全局的时间标记进行加一操作。每个软件定时器都记录着自己的到期时间。
程序需要定期扫描所有正在运行的软件定时器,将每个定时器的到期时间与全局时钟标记进行比较,以判断是否到期。如果到期,则执行相应的回调函数,并关闭该定时器。
上述是单次定时器的实现方式。如果需要实现周期性定时器,即到期后继续重新定时,只需要在执行完回调函数后,获取当前时间标记的值,加上延时时间,作为下一次定时器到期的时间,然后继续运行该软件定时器即可。
3.基于STM32的软件定时器
3.1 时钟节拍
软件定时器需要一个硬件时钟源作为基准,这个时钟源有一个固定的节拍(可以理解为秒针的每次滴答),用一个32位的全局变量tickCnt来记录这个节拍的变化:
static volatile uint32_t tickCnt = 0; //软件定时器时钟节拍
每来一个节拍就对tickCnt加一(记录滴答了多少下):
/* 需在定时器中断内执行 */
void tickCnt_Update(void)
{
tickCnt++;
}
一旦开始运行,tickCnt将不停地加一,而每个软件定时器都记录着一个到期时间,只要tickCnt大于该到期时间,就代表定时器到期了。
3.2 数据结构
软件定时器的数据结构决定了其执行的性能和功能,一般可分为两种:数组结构和链表结构。什么意思呢?这是(多个)软件定时器在内存中的存储方式,可以用数组来存,也可以用链表来存。
两者的优劣之分就是两种数据结构的特性之分:数组方式的定时器查找较快,但数量固定,无法动态变化,数组大了容易浪费内存,数组小了又可能不够用,适用于定时事件明确且固定的系统;链表方式的定时器数量可动态增减,易造成内存碎片(如果没有内存管理),查找的时间开销相对数组大,适用于通用性强的系统,Linux,uC/OS,FreeRTOS等操作系统用的都是链表式的软件定时器。
本文使用数组结构:
static softTimer timer[TIMER_NUM]; //软件定时器数组
数组和链表是软件定时器整体的数据结构,当具体到单个定时器时,就涉及软件定时器结构体的定义,软件定时器所具有的功能与其结构体定义密切相关,以下是本文中软件定时器的结构体定义:
typedef struct softTimer {
uint8_t state; //状态
uint8_t mode; //模式
uint32_t match; //到期时间
uint32_t period; //定时周期
callback *cb; //回调函数指针
void *argv; //参数指针
uint16_t argc; //参数个数
}softTimer;
定时器的状态共有三种,默认是停止,启动后为运行,到期后为超时。
typedef enum tmrState {
SOFT_TIMER_STOPPED = 0, //停止
SOFT_TIMER_RUNNING, //运行
SOFT_TIMER_TIMEOUT //超时
}tmrState;
模式有两种:到期后就停止的是单次模式,到期后重新定时的是周期模式。
typedef enum tmrMode {
MODE_ONE_SHOT = 0, //单次模式
MODE_PERIODIC, //周期模式
}tmrMode;
不管哪种模式,定时器到期后,都将执行回调函数,以下是该函数的定义,参数指针argv为void指针类型,便于传入不同类型的参数。
typedef void callback(void *argv, uint16_t argc);
上述结构体中的模式state和回调函数指针cb是可选的功能,如果系统不需要周期执行的定时器,或者不需要到期后自动执行某个函数,可删除此二者定义。
3.3 定时器操作
3.3.1 初始化
首先是软件定时器的初始化,对每个定时器结构体的成员赋初值,虽说static变量的初值为0,但个人觉得还是有必要保持初始化变量的习惯,避免出现一些奇奇怪怪的BUG。
void softTimer_Init(void)
{
uint16_t i;
for(i=0; i
3.3.2 启动
启动一个软件定时器不仅要改变其状态为运行状态,同时还要告诉定时器什么时候到期(当前tickCnt值加上延时时间即为到期时间),单次定时还是周期定时,到期后执行哪个函数,函数的参数是什么,交代好这些就可以开跑了。
void softTimer_Start(uint16_t id, tmrMode mode, uint32_t delay, callback *cb, void *argv, uint16_t argc)
{
assert_param(id
上面函数中的assert_param()用于参数检查,类似于库函数assert()。
3.3.3 更新
本文中软件定时器有三种状态:停止,运行和超时,不同的状态做不同的事情。停止状态最简单,啥事都不做;运行状态需要不停地检查有没有到期,到期就执行回调函数并进入超时状态;超时状态判断定时器的模式,如果是周期模式就更新到期时间,继续运行,如果是单次模式就停止定时器。这些操作都由一个更新函数来实现:
void softTimer_Update(void)
{
uint16_t i;
for(i=0; icase SOFT_TIMER_STOPPED:
break;
case SOFT_TIMER_RUNNING:
if(timer[i].match break;
case SOFT_TIMER_TIMEOUT:
if(timer[i].mode == MODE_ONE_SHOT) {
timer[i].state = SOFT_TIMER_STOPPED;
} else {
timer[i].match = tickCnt_Get() + timer[i].period;
timer[i].state = SOFT_TIMER_RUNNING;
}
break;
default:
printf("timer[%d] state error!\r\n", i);
break;
}
}
}
3.3.4 停止
如果定时器跑到一半,想把它停掉,就需要一个停止函数,操作很简单,改变目标定时器的状态为停止即可:
void softTimer_Stop(uint16_t id)
{
assert_param(id
3.3.5 读状态
又如果想知道一个定时器是在跑着呢还是已经停下来?也很简单,返回它的状态:
uint8_t softTimer_GetState(uint16_t id)
{
return timer[id].state;
}
或许这看起来很怪,为什么要返回,而不是直接读?别忘了在前面3.2节中定义的定时器数组是个静态全局变量,该变量只能被当前源文件访问,当外部文件需要访问它的时候只能通过函数返回,这是一种简单的封装,保持程序的模块化。
3.4 测试
最后,当然是来验证一下我们的软件定时器有没达到预想的功能。
定义三个定时器:
定时器TMR_STRING_PRINT只执行一次,1s后在串口1打印一串字符;
定时器TMR_TWINKLING为周期定时器,周期为0.5s,每次到期都将取反LED0的状态,实现LED0的闪烁;
定时器TMR_DELAY_ON执行一次,3s后点亮LED1,跟第一个定时器不同的是,此定时器的回调函数是个空函数nop(),点亮LED1的操作通过主循环中判断定时器的状态来实现,这种方式在某些场合可能会用到。
static uint8_t data[] = {1,2,3,4,5,6,7,8,9,0};
int main(void)
{
USART1_Init(115200);
TIM4_Init(TIME_BASE_MS);
TIM4_NVIC_Config();
LED_Init();
printf("I just grabbed a spoon.\r\n");
softTimer_Start(TMR_STRING_PRINT, MODE_ONE_SHOT, 1000, stringPrint, data, 5);
softTimer_Start(TMR_TWINKLING, MODE_PERIODIC, 500, LED0_Twinkling, NULL, 0);
softTimer_Start(TMR_DELAY_ON, MODE_ONE_SHOT, 3000, nop, NULL, 0);
while(1) {
softTimer_Update();
if(softTimer_GetState(TMR_DELAY_ON) == SOFT_TIMER_TIMEOUT) {
LED1_On();
}
}
}
以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !