前言
在嵌入式软件领域中,定时器是非常重要及常用的概念,还记得我刚毕业的时候,还是一个只会delay的小菜鸡,然后大佬看不下去了,给我讲了时间片轮询,我才真的入了工程师的门。
比如10hz的频率闪烁一颗LED,我们可以在一个1ms定时器中断中累加一个变量,在主循环中判断这个变量,基于此变量去执行任务,这就是最基础的时间片轮询机制了。
一直在工作了好几年之后,我基本都还是基于此概念去编程。但这样任务量一多,需要用到的时间变量就多起来了,我当时最多只是用一个结构体什么的把变量关系组织起来。毫无工程结构可言,代码一团糟,也不好移植啊啥的,重复性工作极多。
后来接触了nrf52840这颗芯片,说实话,它的sdk简直一团糟,我一度认为难看到极点(当然我认为是我水平不够导致我这样认为的…)。在它的sdk里有一个文件app_timer.c,真的,当时是惊艳到我的,原来软件定时器的接口可以设计得如此精简和漂亮。
呐,就像下面这样。创建并启动timer之后,它就会按照你设置的时长和模式开始工作起来了,时间到了就执行回调函数里面的内容。非常好用~~
APP_TIMER_DEF(my_timer_id);//定义timer
err_code = app_timer_create(&my_timer_id, APP_TIMER_MODE_REPEATED, my_timeout_handler);//创建timer并设定模式,绑定回调函数
static void my_timeout_handler (void * p_context)//timer的回调函数
{
//add your code here
}
err_code = app_timer_start(my_timer_id, APP_TIMER_TICKS(10), NULL); //启动timer并定时10ms
err_code = app_timer_stop(my_timer_id);//停止timer
但是上面这一套只能用在nordic他自己的芯片上,我想把它抽象出来,不依赖芯片平台,这样以后的工作效率那不就提升起来了~~。说干就干。
接口设计
1、数据结构
我想要这个软件定时器能基于一个硬件定时器扩展得足够多,并且使用起来尽可能简洁。于是我引入了链表,创建好的定时器就绑定在一个链表上,然后按固定时间去扫描这个链表,并执行对应内容。
链表其实挺难写的,至少让我从头写一个能用的链表,还真得花不少时间,于是,我掏出了Adam Dunkels大神的链表组件…就是写lwip那个大神。嗯,这个链表也贼经典,也是我见到的二重指针的经验用例,以后有空可以写篇博客。
LIST(timer_list); // 创建一个timer_list链表,后面创建的定时器全都挂在timer_list下面
设计软件定时器的回调函数类型,Nordic的软件定时器回调有个 void * p_context,还有很多操作系统啊都会有这种,我大致理解就是它可以传递一个任意类型的指针进去,然后访问这个内容。不过我从来没有用过,应该是一个比较高级的东西…
看不懂,那就无参无返回。
typedef void (*b_timerout_handler_t)(void);
typedef struct{
void * list_next;//使用Adam Dunkels大神的链表组件,结构体第一个内容得是这个
uint32_t end_val; //定时器时间到的终值
uint32_t repeat_period;//定时器定时时间的重载值
b_timerout_handler_t handler;//回调
uint8_t active;//定时器工作状态指示
}b_timer_t;
软件定时器一搬有两种模式,一种是单次执行,一种是重复执行。单次执行很简单,时间到了就把定时器停止了就行,重复执行就是时间到了,再重新装载一下定时器终值即可。用个枚举类型来表示。
typedef enum
{
B_TIMER_MODE_SINGLE_SHOT,
B_TIMER_MODE_REPEATED
} b_timer_mode_t;
在大神的链表里面有一个挺有意思的宏,在预编译阶段,它用来将s1和s2连接起来,大神用它来--------给变量取名字!!!
#define LIST_CONCAT2(s1, s2) s1##s2
#define LIST_CONCAT(s1, s2) LIST_CONCAT2(s1, s2)
拿来主义,参考参考大神的代码,大神定义链表是这样的。
#define LIST(name) \
static void *LIST_CONCAT(name,_list) = NULL; \
static list_t name = (list_t)&LIST_CONCAT(name,_list)
//拆开后就是
static void *name_list = NULL;
static list_t name = (list_t)&name_list;
//list_t是void **
呐,LIST(name)就是定义了一个list_t类型的 name,它指向了name_list 。
好好好,我也这样搞
#define B_CONCAT(p1, p2) LIST_CONCAT(p1, p2)
//
typedef b_timer_t * b_timer_id_t;
#define _B_TIMER_DEF(timer_id) \
static b_timer_t B_CONCAT(timer_id,_data) = { \
.active = 0, \
}; \
const b_timer_id_t timer_id = &B_CONCAT(timer_id,_data);
//定义一个名称为timer_test1_id的软件定时器~~
_B_TIMER_DEF(timer_test1_id)
2、创建定时器
创建定时器,就是将宏定义的timer_test1_id给初始化一下,并且添加到链表timer_list中。代码很简单
uint8_t b_timer_create(b_timer_id_t const * p_timer,
b_timer_mode_t mode,
b_timerout_handler_t timeout_handler)
{
if (p_timer == NULL || timeout_handler == NULL) return 1;
b_timer_id_t p_t = (b_timer_id_t) *p_timer;
p_t->handler = timeout_handler;
p_t->repeat_period = (mode == B_TIMER_MODE_REPEATED) ? 1 : 0;
p_t->active = 0;
list_add(timer_list,p_t);//用大神的接口,将p_t添加到链表timer_list中~
return 0;
}
3、删除定时器
uint8_t b_timer_delete(b_timer_id_t const * p_timer)
{
b_timer_id_t p_t = (b_timer_id_t) *p_timer;
p_t->active = 0;
list_remove(timer_list,p_t);//从timer_list中移除p_t
return 0;
}
4、启动定时器
static uint32_t sys_timer_now = 0;
uint32_t b_get_sys_timer(void)
{
return sys_timer_now;
}
uint8_t b_timer_start(b_timer_id_t const * p_timer, uint32_t timeout_ticks)
{
b_timer_id_t p_t = (b_timer_id_t) *p_timer;
if (p_t->active)
{
return 0;
}
p_t->end_val = b_get_sys_timer() + timeout_ticks;
if (p_t->repeat_period)
{
p_t->repeat_period = timeout_ticks;
}
p_t->active = 1;
return 0;
}
5、停止定时器
uint8_t b_timer_stop(b_timer_id_t const * p_timer)
{
b_timer_id_t p_t = (b_timer_id_t) *p_timer;
p_t->active = 0;
return 0;
}
6、定时器更新
void timer_update(void)//一般我是在硬件定时器中断中调用,比如1ms一次。也可以主循环周期调用,但那样就可能更不怎么准了
{
b_timer_id_t item = NULL;
uint32_t cur_sys_timer = sys_timer_now;
cur_sys_timer++;//调用一次,时基加1。
//遍历链表
for(item = (b_timer_id_t)list_head(timer_list); item != NULL ; item = list_item_next(item))
{
if(item->active){
if(cur_sys_timer == item->end_val){ //必须是==判断,不能≥,因为item->end_val如果溢出,那么cur_sys_timer就会一直>end_val。
if(item->repeat_period){//根据模式去决定重装载终值还是停止定时器。
item->end_val = cur_sys_timer + item->repeat_period;
}else{
item->active = 0;
}
item->handler();//回调
}
}
}
sys_timer_now = cur_sys_timer;
}
使用实例
随便找个demo演示下~~
1、硬件定时器初始化,并在中断中调用timer_update
void Timer6_Handler(void)
{
TIM_ClearINT(TIM6);
TIM_Cmd(TIM6, DISABLE);
timer_update();//timer_update调用频率确定软件定时器单tick时间。
//Add user code here
TIM_Cmd(TIM6, ENABLE);
}
2、定义软件定时器,并创建回调函数
_B_TIMER_DEF(timer_test1_id)
_B_TIMER_DEF(timer_test2_id)
void timer_test1(void)
{
xprintf("我是timer_test1\r\n");
}
void timer_test2(void)
{
xprintf("我是timer_test2,停止timer_test1\r\n");
b_timer_stop(&timer_test1_id);
}
3、创建并配置定时器
b_timer_create(&timer_test1_id,B_TIMER_MODE_REPEATED,timer_test1);//重复模式
b_timer_create(&timer_test2_id,B_TIMER_MODE_SINGLE_SHOT,timer_test2);//单次模式
4、启动定时器
b_timer_start(&timer_test1_id,100);//100tick执行一次
b_timer_start(&timer_test2_id,1000);//1000tick执行一次
5、效果
可见,执行了10次timer1回调,1次timer2回调,并且关闭了timer1。
ojbk,能用了。