跑马灯-变形记
跑马灯实验:将一排led灯按一定的时序循环点亮。说到它,相信许多人都要会心一笑。跑马灯实验和PC机软件领域的“hello world”同属于骨灰级的入门实验。几乎所有嵌入式领域初学者都从这个实验开始,满怀激动地踏上技术之路。那第一次用程序点亮led灯的激动,那曾经的青涩与懵懂,隽永难忘。
但多年工作后,我发现那些学校实验代码,和专业软件公司的代码之间,其差距实在难以估量。尤为遗憾的是,有许多工作多年的工程师,写出的代码处也还处在这种“实验水平”,一旦搭建的目标系统功能稍显复杂,就会一团乱麻,程序千疮百孔,全局变量遍地皆是,首尾难顾,维护起来困难重重。
为了避免这些实验代码继续“荼毒”下一代,本章以跑马灯实验为例,朝着商业代码的方向,进行循序渐进的优化变形,让读者明晰学校与社会的差距,并初步领会系统的架构之美。
闲言少叙,先上一段最原始的跑马灯代码。该代码从百度搜索获得。
#include<reg51.h>
#include<intrins.h>
#define uchar unsigned char
#define uint unsigned int
void DelayMS(uint x)
{
uchari;
while(x--)
{
for(i=0;i<255;i++);
}
}
// LED 跑马灯(从右至左)
void main()
{
P1=0xfe;
while(1)
{
if (P1==0x7f)
P1=0xfe;
else
P1=_crol_(P1,1);
DelayMS(80);
}
}
代码1最原始的跑马灯代码
复制代码
将这段代码编译、载入51的目标板,只要led接的是P1口,跑马灯的效果还是能够出来的。可如果从系统的可阅读性、可移植性、软件规范等角度去审视,这段代码无疑可称得上是“简陋”。
接下来,我们将循序渐进对其进行逐次改造,最终成为一个该功能下还算合理的“软件架构” 第一次变形:typedef、函数封装、空格、间隔行、注释。
本系列入口:一线研发之声 之 跑马灯变形记(一)
1) 先从类型重定义说起,“#define uchar unsigned char”这样的语句,其意义恐仅是为了减少敲写“unsigned char”的时间,偷懒之举罢了。类型重定义用意何其深远,区区“define”是万万授受不起的,可详见本书章节“被低估了的typedef”。在本系统中,typedef还用不上,删之。
2) 在main中,对于P1的操作如果用函数包起来,冠以一个恰当的函数名,再对P1这种“裸露端口”和“常量80”加上一个define宏定义,那代码阅读起来就比较不会堵得慌了。
3) 还有,字符间的空格,函数间的空白行,代码注释,这些也是良好程序的重要组成部分。
综上三点进行修改,我们的跑马灯才大概有一个雏形出来。如下:
#include<reg51.h>
#include<intrins.h>
/*********************************************************/
#defineLED_PORT P1 /* led端口映射 */
#defineLIGHT_INTERVAL_TIME 80 /* unit:ms */
/*********************************************************/
void delay_ms(unsigned intms) /* 替换形参名和函数名 */
{
unsigned chari;
while (ms--) /* 加空格 */
{
for(i=0; i<255; i++);/* 加空格 */
}
}
/***** LED 跑马灯(从右至左)********************************/
void led_light_init(void)
{
LED_PORT =0xFE;
}
void led_light_right2left(void)
{
if (0x7F == LED_PORT) /* 防止’==’与’=’的意外 */
{
LED_PORT =0xFE;
}
else
{
LED_PORT =_crol_(LED_PORT,1);
}
}
/*********************************************************/
void main(void)
{
led_light_init();
while(1)
{
led_light_right2left();
delay_ms(LIGHT_INTERVAL_TIME);
}
}
代码2跑马灯的第一次变形
复制代码 第二次变形:精度控制,添加中断定时器
本系列入口:一线研发之声 之 跑马灯变形记(一)
上述代码中,定时器的延时存在两个问题。
(1) 延时程序精度不高。在不同mcu和不同的外部晶振,这个函数都需要修改。当这个系统开启了更多的中断时,这个函数精度受到的影响就是随机性的。
(2) while+for的延时方法,属于一种“硬延时”,生生地耗掉mcu的运行资源。在实时性要求极高的嵌入式领域,这种做法显然不合时宜。
综上,在本次进化,我们需要引进系统的定时器中断功能。它至少涉及两个函数。
/***** LED 跑马灯(从右至左)***************************/
……………….
/**********************************************************/
#define XTAL (36864000UL)
#define TIMER_1MS (XTAL/12UL/1000UL)
/*--------------------------------*/
static volatile unsigned charflag_80ms = 0;
static void timer1(void) interrupt 3 using 1
{
static unsigned char tcnt = 0;
TCNT1 += (-TIMER_1MS);/* 为何如此写法,详见章节…*/
if (++tcnt >= LIGHT_INTERVAL_TIME)
{
tcnt = 0;
flag_80ms = 1;
}
}
void timer_init(void)
{
TMOD= 0x11; // timer0 16-bit, timer1 16-bit
TCNT1 = (-TIMER_1MS);
TR1 = 1;
IE |= 0x0A; // ???? ial 0, enable timer 1, ex0,1
}
复制代码
/*********************************************************/
void main(void)
{
led_light_init();
timer_init();
while (1)
{
if (flag_80ms)
{
flag_80ms = 0;
led_light_right2left();
}
}
}
代码3跑马灯的第二次变形
复制代码
加上定时器中断后,定时器的精度提高了,mcu的运算资源也极大的释放。然而,我们不得不设置了一个全局变量“flag_80ms”,用来沟通main和定时中断,增加一个内部全局变量tcnt,用来累计1ms定时功能。此时,main.c文件里面的代码乱像已显,为此,我们迫切需要第三次的变形。 跑马灯第三次变形:文件/模块划分
本系列入口:一线研发之声 之 跑马灯变形记(一)
不得不说,将每个功能抽象归类,放入不同的文件中进行编译链接,这是一个质的飞跃。但拆分模块将会极大地考验程序员的经验和思维习惯,它并不是单纯的把一个大文件拆分、编译通过就完事了,它体现的是一个工程师对于架构的理解深度。做得好,可以使系统模块间关系简单、层次分明。做得差,混乱之源就此埋下。
在这次的变形中,我们将设备分为如下文件:
单纯的剪切、粘贴操作后,我们会发现两个编译问题:
(1) 在main.c中,无法访问定时器的“flag_80ms全局变量”。
(2) 在timer.c中,无法访问“宏定义常量LIGHT_INTERVAL_TIME”。
对于许多人来说,第1个问题好解决,extern出来就是了。第2的问题就犯浑了,从应用角度讲,这个参数用来控制跑马灯闪烁的间隔时间,应该是跑马灯模块的。从技术实现角度讲,这个参数只有定时器会用到它,应该放在timer.c里面。怎么办呢?最后一个折衷的办法,就是把LIGHT_INTERVAL_TIME放到led_turning.h中,然后在timer.c中包含这个文档。
在这样的惯性思路下,虽然仅有三个.c文件,但其.h文档的依赖关系已经开始令人难过了。于是许多人为了方便,会把所有的.c档通通放到main.h中,或者includes.h中。
不管怎样,按照这个思路,最终我们或许得到了下面这个拆分方案。
上面这几个文件的依赖关系,我们可以整理成下面的依赖图,如下。
这样的实现细节,咋一看貌似也清晰明了,但实际上隐患暗藏。我们仔细分析一下所有文件之间的编译链接依赖关系,拨开迷雾,其实它们间真正的依赖关系是这样的,如下。
让我们以一种“局外人”的态度,来诘问自己几个问题,我们就知道何处别扭了。
(1) 为何timer模块的实现细节,却要取决于led模块如何定义呢?这样扯不清。
(2) 为何在“应用层”main.c文件中,为了使用led_turn模块,还需要把平台文件reg51.h包含进来呢?这如同强迫电视机的用户,去关心遥控器的实现原理一般,荒谬哦。
(3) timer.c模块,把它内部的变量flag_80ms的写权限也开发给main,这样妥当吗?这如同电器内部的高压线裸露在外,只要开了,就总有懵懂的孩童回去触碰,危险!!!
是的,分层思想,每个程序员都懂,在学校也都有好几个学期的专业课程来讲这个事儿。但到了具体的编码实现阶段,就错漏百出。初入泳池的旱鸭子,无论岸上如何背诵游泳精要,入水后一样苦苦挣扎而不得法。 跑马灯的第四次变形:修改模块间的依赖关系
是的,只有把前面的三个诘问解决了,我们才能得到一个相对完美的跑马灯。最终,我们要的是一个干净清爽的关系依赖图,如下。
要实现这样的依赖关系,就需要明白上一次变形中存在的几个“思维误区”。
1) LIGHT_INTERVAL_TIME,这个参数调整跑马灯的间隔时间,最终影响跑马灯的循环闪烁速度,可快可慢。它本质上是属于用户的,如同街上的广告轮廓灯,它的样式和闪烁方式,是由订购者决定的。广告灯的供应商只需给出使用方法即可。
因此,LIGHT_INTERVAL_TIME这个参数是不属于设备层的,它归使用者,归main.c。设备层必须得给出一个接口实现方式,让使用者得以传递LIGHT_INTERVAL_TIME的数值,使“用户”可以灵活地控制跑马灯的运行速度。如何实现呢?这里介绍一种较为符合当前应用的方法。为此,我们需要将timer模块改装如下。
2)
led_turn.h中的端口定义“#defineLED_PORT P1”。许多单片机的工程师喜欢将平台相关的东西定义在.h档里面,其本意是为了移植方便,想着届时只需要修改.h档即可。但这样的话,led_turn.h模块就得依赖文件,不然就无法识别到端口P1。最终造成了所有依赖led_turn.h的模块,会间接依赖。因此,这个P1,必须放到led_turn.c的模块内部。
不要轻视这样的小细节,当系统较为庞大时,这样操作很容易就会造成二次、三次、多次间接依赖,像病毒一样“污染扩散”,牵一发就会动全身,最终使整个系统陷入永沦之地。正所谓,勿以恶小而为之,勿以善小而不为。
3)
上次变形中,我们把timer模块中的全局变量flag_80ms,通过timer.h,开发给main.c读写。在当前这个小小跑马灯系统中,也许问题不大。但对于构建商业软件系统,这是一个极为危险的做法。全局变量本身就是一个需要极力避免的东西,更何况还把写权限开放给外界,这如同将家中大门拆除一般令人心悸。关于全局变量的危害性及处理方法,详见本书章节《全局变量猛于虎》。此处我们用函数get_and_clr_timer_evt()进行了规避,详见上述代码。
多年来,我总结了一个经验,.h档里面,最好都是纯洁无瑕的函数声明。我一直尽力做到这一点,本书的后续章节,会陆续提到这一原则的带来的种种妙处,及其各种实现技巧。
综上,我们得到这样的一个依赖清晰的main.c文件,如下:
这样,我们的跑马灯完成了第四次变身,成为了一个具备良好移植性、可阅读性的商业系统。如果这个跑马灯要移植到M3/M0或者AVR等平台上,只需要修改“设备层”的led_trun.c和timer.c文件即可。而应用层的三个文件无需任何修改,因为它们没有依赖任何平台相关的文件。 变 无 止 境
本系列入口:一线研发之声 之 跑马灯变形记(一)
经历前面的四次变形,应该说,跑马灯这棵小树苗,也算根红苗正了。具备了向上继续生长的条件,长歪掉的可能性较低。因此我们可以进一步拓展其功能。
1)增加一个按键,控制跑马灯的启动和停止。这个时候,有按键抖动滤波、译码和事件传递需要考虑。
2)增加一个串口通讯,敲入不同的命令,实现不同的动作。如下
命令字符串 实现功能
left2right 从左到右闪烁
right2left 从右到左闪烁
stop 停止
这个时候,需要考虑串口的字节流如何截止,使变成一串命令。又如何告诉串口的使用者:包接受完成,请取包分析?
3) 灯,只是一个商业系统里面,最最简单的控制,尚且如此讲究。如果加上一些基本的标配:Lcd,蜂鸣器、矩阵扫描按键,系统又该如何构建?
4)随着系统的日益变大,当main模块while(1)里面代码的运行时间超过LIGHT_INTERVAL_TIME时,跑马灯的间隔控制将会出现较大的时间误差,请问如何避免这个情况。
这些,都是在构建嵌入式系统过程中常会遇到的问题,但对于没有经验的工程师来说,往往会感到阵阵无力与彷徨,难以兼顾系统的稳定性、可维护性和运行效率。
最常见的是:遍地的全局变量交叉混杂、到处的while(1)和看门狗、直入骨髓的if判断陷阱、心力交瘁的依赖关系。
整个系统处于一种“神秘的稳定状态”,
你会敬之畏之,继而离职,下任更快离职,继而瘫痪。 这个讲的不错很详细的 前面有代码的部分看得很仔细,也感到楼主严谨的思维变化,内心暗暗称赞好文章;后面纯文字没有代码的部分一带而过,有点龙头蛇尾的倾向。
后面应该继续用代码演示变形之----跑马灯之模块封装(可移植性);跑马灯之函数标准化(有标准的API接口来指示跑马功能)。。。 支持楼主,希望有更多技术大牛能普及这些系列性关键问题。 对于老手来说,这个是必修的,必须有这种规范化的观念。但对于新手,你这么多东西砸上去他直接晕了,然后跑了。
规范什么的可以之后在写复杂程序的时候树立,先把人领进门再说吧 怎么看到一半有点晕了???楼主后面讲的很好但是代码或者图怎么都没了??还是我手机刷不出??楼主所讲授的书名叫什么?去买本看看。 怎么看到一半有点晕了???楼主后面讲的很好但是代码或者图怎么都没了??还是我手机刷不出??楼主所讲授的书名叫什么?去买本看看。 顶楼主, 像前面的坛友说的一样,后面也加上代码就更直观了;
瑕不掩瑜~!赞一个~~!{:biggrin:} 又看了一遍, 请问楼主说的书是哪本?? 想买来看下 这个...貌似之前论坛已经发过一份pdf上来的,不错 我是从别的地方看到好转来的
一线研发之声 你百度下吧
这年头好人难做
写的确实不错 Free_Bird 发表于 2014-12-8 20:45
顶楼主, 像前面的坛友说的一样,后面也加上代码就更直观了;
瑕不掩瑜~!赞一个~~! ...
我是从别的地方看到好转来的
一线研发之声 你百度下吧
这年头好人难做
写的确实不错 jswd0810 发表于 2014-12-8 19:34
lz说的不错,就是后面几个不全,支持楼主
我是从别的地方看到好转来的
一线研发之声 你百度下吧
这年头好人难做
写的确实不错 cumtgao 发表于 2014-12-8 19:49
前面有代码的部分看得很仔细,也感到楼主严谨的思维变化,内心暗暗称赞好文章;后面纯文字没有代码的部分一 ...
我是从别的地方看到好转来的
一线研发之声 你百度下吧
这年头好人难做
写的确实不错 longwu537 发表于 2014-12-8 20:25
怎么看到一半有点晕了???楼主后面讲的很好但是代码或者图怎么都没了??还是我手机刷不出??楼主所讲授 ...
我是从别的地方看到好转来的
一线研发之声 你百度下吧
这年头好人难做
写的确实不错 这个的确写的不错。从底层开始裸机写,到MCU效率提高,再到一级级慢慢到模块封装。再到黑盒子接口。 slzm40 发表于 2014-12-10 16:17
这个的确写的不错。从底层开始裸机写,到MCU效率提高,再到一级级慢慢到模块封装。再到黑盒子接口。...
我只是觉得比较好就转过来了
看你说的真的说到好的方面了
大牛能细说下吗
非常好。授人与渔! 不错,跑马灯变形记 比一般书上的例子强多了! shinemotou 发表于 2014-12-10 16:35
我只是觉得比较好就转过来了
看你说的真的说到好的方面了
大牛能细说下吗
我也是在学习中,不敢称大牛,也只是在此学习路上走的长一点而已。
其实大家也是这么过来的,不过变形记从头到尾,有人带,路并不长,后面的路才长。 我自学也花费有半年多,有这个开源论坛,资料非常多,懂的利用才是真。
简单讲讲,变形记 代码1,学校教的风格,非OS,我也有在用,但仅仅做驱动简单测试,因为可以避免过多的复杂度,一般新上手一个新芯片都是这么干的,然后进行移植类的优化。 以前刚开始学校学时,都这么写,当你过了很久之后,回头再来看这些代码,会发现看不懂,虽然这个简单,但不好移植到其它MCU,时间延时不好控制精度 ,而且一个delayms(80)对于MCU是非常浪费时间的。为了效率。
这里吐槽下在大学的单片机老师,是个刚毕业没多久硕士研究生的年轻老师,哦,还是个女的,那时单纯的相信,照本循科,呵呵,大学能教出什么学生。 当时有个按键问题问过老师,按键是这样子的(哦,当时是教的汇编,我用C简单实现一下,C也是我后来自己学的)
if(key == 0)
{
delayms(10);
if(key == 0)//按键确实有按下
{
keyvalue = 0x01;
while(!key);//等待按键松开
}
}
当时我问了一个这样子问题,我说那个等待按键松开,如果我不松开,那是不是一直等在那,其它活就没法干了?她看了一下,说你不用管这些,你只要听我的,这样做,考试才能过。 于是我没有再问下去了。 后来自己学了C,学了状态机,学了释放MCU,都是从开源论坛学的,那课我也没怎么上过。
在学校不要过于相信老师,他们只管考试过不过,不管你有没有学进去。
扯远,再继续乱讲一下。
于是出现代码 2 ,定时器加功能简单封装,定时器的利用,去除delayms这样长延时的空转,可以让MCU有效率的去执行其它任务,简单封装,以便突然想修改时,非常方便,
出现代码3,4,模块封装,面向用户或面向其它程序员,人家想利用你的程序,想改闪灯时间,又不想了解你的底层实现,那么代码2就不符合了,一个.c的文件包含所有,不好找出修改的地方,于是利用.h的方法,把需求修改和给外面使用的API放在里面, 又把定时器单独模块化,这边东西很多,具体参考傻孩 子各种精华贴 子可以学习体会。具体做法是把定时器单独模块化,放入一个timer.c,timer.h文件中,在main.c中只要调用timer.h的API,把timer.c中实现全部封装。再细一点分,跑马灯程序也可以单独模块化,在mian.h调用API就行。
代码5,也就是六楼,已经把其它功能加进来了,如按键,如显示,如串口通讯,就是向一个小产品,简单裸机小系统进军, 上面也提到,闪灯出现时间误差, 如何避免? 这就要考虑实际运行情况,把任务分为重要程度去选择,去构建。
怎么说呢,以前我在写程序的时候,满程序的全局变量,都不知道在哪已经做过修改,哪里调用过,一旦出错,就无从查起。 所以现在做程序,不到非不得已,都会把一个全局变量限定在一个模块内,用static 限定。
做为一个程序员,没学会模块封装,分层思想,在你写了许久的程序,写了一个简单的文档。个把月后,当你回头来再看你的程序时,发现你突然想骂,哪个SB写的这样的程序,你才发现,程序头里属着自己的大名时,你才想到自己是多么的悲惨,当你 想要修改一个简单的小功能,要修改上百处时,你就想骂自己了。
以上个人想法,仅做参考。 自己也在学习,最近在调CC1101。
最后说一句,傻孩子的书是好书,不是做广告,而是实在的在教你学校学不到。那是思想。
学习了~~~~~~~~~~ slzm40 发表于 2014-12-14 21:26
我也是在学习中,不敢称大牛,也只是在此学习路上走的长一点而已。
其实大家也是这么过来的,不过变形记 ...
谢谢你的热情回复
受益匪浅呀 本论坛真不错,我也是从这里吸收营养慢慢入门的。
页:
[1]