开源一个轻量级的定时器控制器SmartTimer

2019-12-18 18:48发布

本帖最后由 elecsun 于 2016-6-24 16:30 编辑

0. 前言:
一直以来,都渴望为开源世界贡献自己的作品。但一是之前一直忙碌于公司的项目,确实也没法开源;二是总觉得自己的代码丑陋,不够优雅、简洁。怕贴出来后贻笑大方。
最近一年更换了工作,没有那么忙了,于是有时间把以前的积累沉淀一下。即使公司的项目,也不用拼死拼活赶时间了,于是做项目时会有更多的思考。比如,怎么做才能把业务逻辑和工具组件分开,使工具组件可以在多个项目中复用;怎么做才能使整个系统的框架更加简洁优雅。
于是,这个我命名为SmartTimer的工具就诞生了。几经修改,觉得可以拿出来和大家分享了。决定开源前还略犹豫,因为我之前用到的,比较熟悉的开源项目如RT-Thread、EasyFlash等等,都非常棒,代码漂亮,结构严谨。觉得我的代码拿出来就是出洋相。但后来一想,只有拿出来晒,被各位高手”喷过”,才会有更大的进步,才会使SmartTimer更加完善,这也是开源精神最重要的意义之一,于是我就献丑了。


1.SmartTimer能干什么?

SmartTimer是一个轻量级的基于STM32的定时器控制器,在单片机”裸跑”的情况下,可以很方便的实现异步编程。

2        . SmartTimer的一般用法:
2.1 Runlater。
在单片机编程中,想实现在”xxx毫秒后调用xxx函数”的功能,一般有两种方法:一种是阻塞的,非精确的方式,就是用for(i=0;i<0xffff;i++);这种循环等待的方式,来非精确的延迟一段时间,然后再顺序执行下面的程序;
另一种方式是利用定时器实现异步的精确延时。这种方式有又两种选择,即把XXX函数在定时器中断里执行,或者只在定时器中断里设定标志位。在系统的主While循环中检测这个标志位,当标志置位后,去运行XXX函数。

从理论上来说,以上介绍的3中runlater的方式中,采用定时器设定标志位的方法最好。因为首先主程序不用阻塞,在等待的时间里,MCU完全可以去做其他的事情,其次在定时器中断里不用占用太多的时间,节约中断资源。但这种方式有一个缺点就是,实现起来相对麻烦一些。因为如果你要有N个runlater的需求,那么就得设置N个标志位,还要考虑定时器的分配、设定。在程序主While循环里也会遍布N个查询标志位的if语句。如果N足够多,其实大于5个,就会比较头疼。这样会使主While循环看起来很乱。这样的实现不够简洁、优雅。

说了这么多,这部分还有一句话,SmartTimer可以优雅的解决runlater的问题。 :)
2.2        Runloop
在定时器编程方面还有另一个典型需求,就是“每隔xxx毫秒运行一次XXX函数,一共运行XXX次”。这个实现起来和runlater差不多,就是加一个运行次数的技术标志。我就不再赘述了。还是那句话:

SmartTimer可以优雅的实现Runloop功能。
2.3        Delay
并不是说非阻塞就一定比阻塞好,因为在某些场景下,必须得用到阻塞,使单片机停下来等待某个事件。那么SmartTimer也可以提供这个功能。


3. SmartTimer的高级用法
所谓的高级用法,并不是说SmartTimer有隐藏模式,能开启黑科技。而是说,如果你能转变思路,举一反三地话,可以利用SmartTimer提供的简单功能实现更加优化、合理的系统结构。
传统的单片机裸跑一般采用状态机模式,就是在主While循环里设定一些标志位或是设定好程序进行的步骤,根据事件的进程来跳转程序。简单的说来,这是一种顺序执行的程序结构。其灵活性和实时性并不高,尤其是当需要处理的业务越来越多,越来越复杂时,状态机会臃肿不堪,一不留神(其实是一定以及肯定)就会深埋bug于其中,调试解决BUG时也会异常痛苦。
如果你能转换一下思路,不再把业务逻辑中各个模块的关系看成基于因果(顺序),而是基于时间,模块间如果需要确定次序可以采用标志位进行同步。那么恭喜你,你已经有了采用实时系统的思想,可以尝试使用RT-thread等操作系统来完成你的项目了。
但是,使用操作系统有几个问题,第一是当单片机资源有限的时候,使用操作系统恐怕不太合适;第二是学习操作系统本身有一定的难度,至少你需要花费一定的时间;第三如果你的项目复杂度没有那么高,使用操作系统有点大材小用。
那么,请允许我没羞没臊的说一句,其实SmartTimer中的Runloop功能可以简单的实现基于时间的主程序框架。


4.关于Demo
Demo程序比较简单,主要是为了测试SmartTimer的功能,即寻找Bug。各位可以试用一下。Demo程序基本可以体现Runlater,Runloop,Delay功能。同时也能基本体现基于时间的编程思想(单片机裸跑程序框架)。

5.SmartTimer的使用
SmartTimer.h中声明的函数并不多,总共有7个:
void timer_init ( void );
void timer_tick (void);
void timer_mainloop ( void );
void timer_loop ( uint16_t delayms, void (*callback)(void), uint16_t times);
void timer_runlater ( uint16_t delayms, void (*callback)(void));
void timer_delay ( uint16_t delayms);
uint8_t timer_get_eventnum(void);

下面我将逐一介绍
5.1 必要的前提
SmartTimer能够工作的必要条件是:
A.        设置Systick时钟(也可以是其他的定时器TIMx,我选择的是比较简单的Systick),我默认设置为10ms,使用者可以根据自己的情况来更改。
Systick时钟的设置在timer_init函数中,该函数必须在主程序初始化阶段调用一次。
B.        在定时器中断函数中调用timer_tick();可以说,这个函数是SmartTimer的引擎,如A步骤所述,默认情况下,每10ms,定时器中断会调用一次timer_tick();
C.        在主While循环中执行timer_mainloop(),这个函数的主要做用是回收使用完的timer事件的资源。

5.2 开始使用SmartTimer
做好以上的搭建工作,就可以开始使用SmartTimer了。首先上场的是
void timer_runlater ( uint16_t delayms, void (*callback)(void));
该函数接受两个参数,参数delayms传入延迟多长时间,注意这里的单位是根据之前A步骤里,你设置的时间滴答来确定的。即默认单位是10ms;第二个参数是回调函数的函数指针,目前只支持没有参数,且无返回值的回调函数,未来会考虑加入带参数和返回值的回调
举例:timer_runlater(100,ledflash); //1秒(100*10ms=1s)后,执行void ledflash(void)函数

其次是
void timer_loop ( uint16_t delayms, void (*callback)(void), uint16_t times);
这个函数的参数意义同runlater差不多,我就不详细说明了。
该函数接收3个参数,delayms为延迟时间,callback为回调函数指针,times是循环次数。
举例:timer_runloop(50,ledflash,5); // 每500ms,执行一次ledflash(),总共执行5次
      timer_runloop(80,ledflash, TIMER_LOOP_FOREVER); // 每800ms,执行一次ledflash(),无限循环。

最后是
void timer_delay ( uint16_t delayms);
这个函数会阻塞主程序,并延迟一段时间。


另外,uint8_t timer_get_eventnum(void); 这个函数是调试时候用的,可以返回当前的timer event的数目。
SmartTimer可接受的Timer event数量是有上限的,这个上限由smarttimer.h中的宏定义#define        TIMEREVENT_MAX_SIZE      20                定义的。默认为20个,你可以根据实际情况增加或减少。


最后,由于本人才疏学浅,在代码和工程严谨度上还有待提高,所以SmartTimer还很不完善,希望各位大神能提出改进意见,使SmartTimer更加完善。如果SmartTimer能够帮助到各位朋友,将是我的极大荣幸。


SmartTimer的源码和Demo程序下载地址在GitHub上:https://github.com/lmooml/SmartTimer.git
由于Github有被墙的风险,稍后会发布在在国内的代码托管网站git.oschina.net上



友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
94条回答
elecsun
2019-12-25 05:36
本帖最后由 elecsun 于 2016-7-14 16:58 编辑

0.        优化前

收到坛子里很多朋友的鼓励和反馈,在这里一并感谢。有几位朋友提出SmartTimer定时器调度可以进一步优化,其中@科技猎人给出了他的解决方案:
[MsgOS]高效的延时处理机制
http://www.amobbs.com/thread-5653428-1-1.html

@科技猎人对调度器的优化处理给了我很大启发,在思考了几天后,觉得还是他的处理方式最合理。于是用他的处理方式对SmartTimer进行了优化。在此,再次向@科技猎人表示感谢!

顺便说下,我下载了MsgOS,并进行了简单测试,觉得很不错。在目前版本下,算是比较轻量级的,想学习操作系统原理或是想提高自己嵌入式编程的同学,可以学习MsgOS的源码,在功能和代码量还比较少的情况下,比较容易看懂代码。

1.        优化思想


在1.0版本的SmartTimer里,我在每次定时器心跳中断函数中,会遍历所有的定时器事件,对每个事件都操作一遍。这样随着定时器事件越来越多,定时器中断函数执行的时间会越长。
事实上,如果把所有的定时事件的间隔进行排序,那么只需要处理间隔最短的定时事件即可。
请恕我太懒。。。。大家参看@科技猎人的帖子即可明白
[MsgOS]高效的延时处理机制
http://www.amobbs.com/thread-5653428-1-1.html

(以下内容,是我假设你们已经看过@科技猎人的帖子后给出的说明,如果看不明白,再回去看一遍。。。。。。)

我按照自己的需求实现了双链表切换和溢出处理。
a.        首先设一个uint32_t的全局变量current_tick,作为计数器。在每次定时器中断函数中,这个变量都会+1,直到溢出;
b.        设置两个事件链表A和B,每次current_tick溢出就切换一下当前事件链表。与之对相应的,当有一个新的定时事件加入时,判断定时时间加上current_tick是否会溢出,如果溢出的话,就把这个事件加到非当前事件链表上(如果当前链表为A,就加到B上)。
c.        我们来看看current_tick溢出先后会发生什么事情。假如current_tick当前值为0xFFFF000,当前事件链表为A,在A上有1个定时事件event1,event1将会在current_tick = 0xFFFFFFAA时完成定时。每次定时中断current_tick都会+1,当current_tick等于0xFFFFFF00时,新加入了一个事件event2,而event2的定时时间为0x00000110,那么由于current_tick是一个uint32_t的变量,所以current_tick+0x00000110会溢出,实际值会变为0x10。当检测到event2的定时时间会溢出时,就把event2加入到B事件链表上。定时器接着运行,当current_tick=0xFFFFFFAA时,置位event1;然后current_tick继续+1,直到溢出后,讲当前事件列表切换到B;定时器继续运行,直到current_tick=0x10,置位event2.

总结一句话就是,优化的核心就是双链表切换,以及只处理定时间隔最小的事件。

2.        旧版BUG

在优化SmartTimer的时候,发现了之前版本有个不小的Bug,可以说是个低级错误,在这里对已经使用之前版本的朋友说声抱歉。
Bug是事件回收部分有错误,我没有将需要回收的事件放入回收列表中,导致已经失效的事件始终无法释放。
由于新版本的改动较大,甚至回收机制都重构了,由数组处理改为链表处理,所以我没有在旧版本上处理bug,而是选择直接更新新版本。
3.        新版本稳定性
新版本只进行简单测试,我设计了几个测试用例,主要测试了列表切换和事件回收,暂时未发现BUG,但是基于旧版BUG的教训,请大家最好在充分理解源码的基础上再进行使用,这样即使有bug,也可以在使用中察觉并修正。
当然,你也可以选择不使用。。。。。仅仅用来参考学习。

4.        关于SmartTimer的实时性

坛友@qwe2231695提出
” 当出现1ms 5ms 10ms 20ms 100ms 任务时,第100ms会非常忙,因为所有任务都跑” 这个问题。我想了下,我的SmartTimer从原理上,基本上无法解决这个问题,因为它本身不是一个实时的处理机制。
不过,在SmartTimer的机制下,这个问题也能说的通,因为在第100ms卡住的时间,会在100ms后补回来。应为定时标志是在定时器中断中设置的,也就是说虽然在100ms时卡住了,但是“应该处理的事件”虽然没被处理,但是被打上了标记。当100ms卡顿结束后,会直接运行”应该处理而没被处理”的事件,直到把应该处理的事件全部执行完。
用一个形象的比较就是,我们玩的游戏(无论手游还是PC端游戏),可能由于机器性能或网速的原因,会引起卡帧的情况,一旦网速或性能恢复,游戏就会“快进”,加速到正确的时间点上。

实际上,我做这个SmartTimer就是从之前看过的一个游戏引擎上得来的灵感。


5.        下一步计划
A.        增加删除事件的方法
B.        暂无。。。。


最后,其实想做一些图片详细介绍下SmartTimer的实现思路和方法,但是手上没有趁手工具(其实懒癌发作),就只用文字描述了,但愿我的文字没有让你们看的痛苦。。。。。

感谢对我提出意见和鼓励的朋友们,做这个项目让我收获很大。

一周热门 更多>