基于硬件定时器的软件定时器

概括

硬件定时器很精确,软件定时器无论如何都有延迟,主要用在不需要精确定时的地方,而且软件定时比较浪费单片机资源。
梳理
讲到定时器,大家多多少少都会接触到硬件定时器,但是由于有时候资源的限制,又难免会出现使用软件定时器的情况,但是讲定时器需要从硬件定时器开始讲,软件定时器是在其基础之上延伸出来的。

硬件定时器

1.一般硬件定时器集成在CPU的内部,有的可以使用外置的硬件定时器芯片,可以人为通过编程来设置硬件定时器的工作频率,硬件定时器一旦设定好了工作频率,只要上电,那么硬件定时器就会周期性的给CPU输出一个中断信号,称这个中断信号为时钟中断,linux内核已经实现好了时钟中断对应的服务程序,这个服务程序也称之为时钟中断服务函数,既然硬件定时器周期性的给CPU产生时钟中断,那么对应的中断服务程序就会被内核周期性的调用;

2.硬件延时利用定时器/计数器芯片,或用微控制器内部的定时器/计数器,实际上,它就是对晶振的分频(分频系数可编程设置),得到一个精确的低频的周期信号,用这个周期信号(比如10ms)去触发中断,每10ms调用一次定时中断服务程序。在定时中断服务程序中加入计数变量,就可以得到任意的定时了。在10ms没有到时,微控制器可以运行其它程序,10ms到时再自动进去中断服务程序以处理定时任务,不会像软件延时阻塞了。

软件定时器

软件定时器是在硬件定时器基础之上出来的定时器,可以由一个硬件定时器模拟出成千上万个软件定时器,这样程序在需要使用较多定时器的时候就不会受限于硬件资源的不足,这是软件定时器的一个优点,即数量不受限制。但由于软件定时器是通过程序实现的,其运行和维护都需要耗费一定的CPU资源,同时精度也相对硬件定时器要差一些。
实现方法
典型的实现方法是:通过一个硬件定时器产生固定的时钟节拍,每次硬件定时器中断到,就对一个全局的时间标记加一,每个软件定时器都保存着到期时间,程序需要定期扫描所有运行中的软件定时器,将各个到期时间与全局时钟标记做比较,以判断对应软件定时器是否到期,到期则执行相应的回调函数,并关闭该定时器。
注意
因为是在内核态中进行的中断服务,我们定义全局变量需要使用volatile,前面的博客也有讲到他的用法,明确有三种情况,否则造成定时器失败。如:

volatile uint32_t whiole_cnt;
void tickCnt_Update(void)
{
    
    
    whiole_cnt++;
}

定一个我们需要的回调函数:argv,argc为其参数:

typedef void callback(void *argv ,uint16_t argc);

一旦开始运行,whiole_cnt将不停地加一,而每个软件定时器都记录着一个到期时间,只要whiole_cnt大于该到期时间,就代表定时器到期了。
上面的代码都能理解,这时如果我们需要一个定时,需要确定哪些东西呢?

typedef struct time
{
    
    
	unsigned char state;//此软件定时的状态
	unsigned char mode;//单次计时还是循环计时
	uint32_t timer_cnt;//此软件计时值与whiole_cnt比较
	uint32_t period;//循环计时时时间间隔
	callback *cb;//回调函数指针
	void *argv;//回调函数参数,根据实际情况可增可减
	uint16_t argc;//回调函数参数,根据实际情况可增可减
}_timer;

这是单个软件定时器的基本情况,如果需要多个,可以这样:
定义两个定时器数组

#define NUM 2
_timer TIME[NUM];

如上只是一个框架,我们需要先对他初始化,以免影响到后期使用,
初始化函数:

void TIME_NUM_INIT(void)
{
    
    
	int i=0; 
	for( i ;i<NUM;i++)//循环初始化结构体
	{
    
    
		TIME[i].timer_cnt=0;
		TIME[i].period=0;
		TIME[i].state=stop;
		TIME[i].mode=once;
		TIME[i].cb=NULL;
		TIME[i].argv=NULL;
		TIME[i].argc=0;
	}
}

TIME[i].state有三种状态:

typedef enum state
{
    
    
	stop=1,
	runing,
	time_out
}timer_status;

TIME[i].mode有两种状态:

typedef enum mode
{
    
    
	once=1,
	on_once
}timer_mode;

初始化完,实际上还没有真正的定时器出现,下一步做的就是,封装结构体,构造单独的定时器:

void TIME_START(uint8_t id,uint32_t timer,uint32_t delay,unsigned char mode,void *cb,void *argv,uint16_t argc)
{
    
    
	TIME[id].timer_cnt=timer;
	TIME[id].period=delay;
	TIME[id].mode=mode;
	TIME[id].cb=cb;
	TIME[id].argv=argv;
	TIME[id].argc=argc;
	TIME[id].state=runing;
}

根据定义的NUM值,分别初始化,初始化后就可以跑了,如:想要定时一个500ms的软件定时器,且此时定时器中断的时间为10ms,则需要50个中断即可:

TIME_START(0,50,0,once,Time ,"12345",15);

Time是回调函数指针:
argv,argc为其参数

int *Time(void *argv,uint16_t argc)
{
    
    
	printf("运行状态:%d",time_state(0));
	printf("%s,%d\r\n",argv,argc);
	
}

初始化完成后,我们需要一直扫描软件定时器,主要的原因就是跟定时器中断的变量作比较,判断是否定时时间到!被扫描的函数是这样的:

void check_time(void)
{
    
    
	uint16_t i=0;
	  for(i;i<NUM;i++)
			{
    
    
				switch(TIME[i].state)
				{
    
    
					case stop:
						break;
						
					case runing:	
						if(TIME[i].timer_cnt<=whiole_cnt)
						{
    
    
							TIME[i].state=time_out;
							TIME[i].cb(TIME[i].argv,TIME[i].argc);
						}
						break;
						
					case time_out:
						if(TIME[i].mode==once)
						{
    
    
							TIME[i].state=stop;
						}
						else
						{
    
    
							TIME[i].timer_cnt=whiole_cnt+TIME[i].period;
							TIME[i].state=runing;
						}
						break;
					default:break;		
				}
			}
}

我们在主函数中定义一个while(1),一直扫描,方可达到效果。
主函数main.c

int main(void)
{
    
    
	
	LED_Init();
	delay_init();
	timer_init();
	usart_init(115200);
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
   
	TIME_NUM_INIT();//初始化

	TIME_START(0,2,2,on_once,Time ,"12345",15);//定时器1
	TIME_START(1,4,2,once,Time ,"6789",30);//定时器2初始化
	
	 while(1)
	 {
    
     
		 check_time();
		 if(TIME[1].state==stop)//如果软件定时器1定时时间到,LED0反转
		 {
    
    
			  LED0=!LED0;
		 }
	 }
}

别忘记了回调函数!

总结

不管哪种模式,定时器到期后,都将执行回调函数,以下是该函数的定义,参数指针argv为void指针类型,便于传入不同类型的参数。

猜你喜欢

转载自blog.csdn.net/weixin_42271802/article/details/106062854