搜索
bottom↓
回复: 28

跑马灯-变形记

[复制链接]

出0入0汤圆

发表于 2014-12-8 18:46:37 | 显示全部楼层 |阅读模式
跑马灯实验:将一排led灯按一定的时序循环点亮。
说到它,相信许多人都要会心一笑。跑马灯实验和PC机软件领域的“hello world”同属于骨灰级的入门实验。几乎所有嵌入式领域初学者都从这个实验开始,满怀激动地踏上技术之路。那第一次用程序点亮led灯的激动,那曾经的青涩与懵懂,隽永难忘。
但多年工作后,我发现那些学校实验代码,和专业软件公司的代码之间,其差距实在难以估量。尤为遗憾的是,有许多工作多年的工程师,写出的代码处也还处在这种“实验水平”,一旦搭建的目标系统功能稍显复杂,就会一团乱麻,程序千疮百孔,全局变量遍地皆是,首尾难顾,维护起来困难重重。
为了避免这些实验代码继续“荼毒”下一代,本章以跑马灯实验为例,朝着商业代码的方向,进行循序渐进的优化变形,让读者明晰学校与社会的差距,并初步领会系统的架构之美。
闲言少叙,先上一段最原始的跑马灯代码。该代码从百度搜索获得。
      
#include<reg51.h>
#include<intrins.h>
#define uchar unsigned char
#define uint   unsigned int
void DelayMS(uint x)
{
    uchar  i;
    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口,跑马灯的效果还是能够出来的。可如果从系统的可阅读性、可移植性、软件规范等角度去审视,这段代码无疑可称得上是“简陋”。
   接下来,我们将循序渐进对其进行逐次改造,最终成为一个该功能下还算合理的“软件架构”

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。

出0入0汤圆

 楼主| 发表于 2014-12-8 18:47:31 | 显示全部楼层
第一次变形:  typedef、函数封装、空格、间隔行、注释。
本系列入口:一线研发之声 之 跑马灯变形记(一)
1) 先从类型重定义说起,“#define uchar unsigned char”这样的语句,其意义恐仅是为了减少敲写“unsigned char”的时间,偷懒之举罢了。类型重定义用意何其深远,区区“define”是万万授受不起的,可详见本书章节“被低估了的typedef”。在本系统中,typedef还用不上,删之。
2) 在main中,对于P1的操作如果用函数包起来,冠以一个恰当的函数名,再对P1这种“裸露端口”和“常量80”加上一个define宏定义,那代码阅读起来就比较不会堵得慌了。
3) 还有,字符间的空格,函数间的空白行,代码注释,这些也是良好程序的重要组成部分。
综上三点进行修改,我们的跑马灯才大概有一个雏形出来。如下:
      
#include<reg51.h>
#include<intrins.h>
/*********************************************************/
#define  LED_PORT                                P1 /* led端口映射 */
#define  LIGHT_INTERVAL_TIME        80 /* unit:ms     */
/*********************************************************/
void delay_ms(unsigned intms) /* 替换形参名和函数名 */
{
    unsigned char  i;
    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  跑马灯的第一次变形
复制代码

出0入0汤圆

 楼主| 发表于 2014-12-8 18:48:17 | 显示全部楼层
第二次变形:精度控制,添加中断定时器
本系列入口:一线研发之声 之 跑马灯变形记(一)
上述代码中,定时器的延时存在两个问题。
(1)    延时程序精度不高。在不同mcu和不同的外部晶振,这个函数都需要修改。当这个系统开启了更多的中断时,这个函数精度受到的影响就是随机性的。
(2)    while+for的延时方法,属于一种“硬延时”,生生地耗掉mcu的运行资源。在实时性要求极高的嵌入式领域,这种做法显然不合时宜。
综上,在本次进化,我们需要引进系统的定时器中断功能。它至少涉及两个函数。
      
/***** LED 跑马灯(从右至左)***************************/
……………….
/**********************************************************/
#define XTAL                        (36864000UL)
#define TIMER_1MS                (XTAL/12UL/1000UL)        
/*--------------------------------*/
static volatile unsigned char  flag_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文件里面的代码乱像已显,为此,我们迫切需要第三次的变形。

出0入0汤圆

 楼主| 发表于 2014-12-8 18:48:56 | 显示全部楼层
跑马灯第三次变形:文件/模块划分
本系列入口:一线研发之声 之 跑马灯变形记(一)
不得不说,将每个功能抽象归类,放入不同的文件中进行编译链接,这是一个质的飞跃。但拆分模块将会极大地考验程序员的经验和思维习惯,它并不是单纯的把一个大文件拆分、编译通过就完事了,它体现的是一个工程师对于架构的理解深度。做得好,可以使系统模块间关系简单、层次分明。做得差,混乱之源就此埋下。
在这次的变形中,我们将设备分为如下文件:


单纯的剪切、粘贴操作后,我们会发现两个编译问题:
(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,这样妥当吗?这如同电器内部的高压线裸露在外,只要开了,就总有懵懂的孩童回去触碰,危险!!!
是的,分层思想,每个程序员都懂,在学校也都有好几个学期的专业课程来讲这个事儿。但到了具体的编码实现阶段,就错漏百出。初入泳池的旱鸭子,无论岸上如何背诵游泳精要,入水后一样苦苦挣扎而不得法。

出0入0汤圆

 楼主| 发表于 2014-12-8 18:49:27 | 显示全部楼层
跑马灯的第四次变形:修改模块间的依赖关系
是的,只有把前面的三个诘问解决了,我们才能得到一个相对完美的跑马灯。最终,我们要的是一个干净清爽的关系依赖图,如下。

要实现这样的依赖关系,就需要明白上一次变形中存在的几个“思维误区”。
1)        LIGHT_INTERVAL_TIME,这个参数调整跑马灯的间隔时间,最终影响跑马灯的循环闪烁速度,可快可慢。它本质上是属于用户的,如同街上的广告轮廓灯,它的样式和闪烁方式,是由订购者决定的。广告灯的供应商只需给出使用方法即可。
因此,LIGHT_INTERVAL_TIME这个参数是不属于设备层的,它归使用者,归main.c。设备层必须得给出一个接口实现方式,让使用者得以传递LIGHT_INTERVAL_TIME的数值,使“用户”可以灵活地控制跑马灯的运行速度。如何实现呢?这里介绍一种较为符合当前应用的方法。为此,我们需要将timer模块改装如下。



2)        
led_turn.h中的端口定义“#define  LED_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文件即可。而应用层的三个文件无需任何修改,因为它们没有依赖任何平台相关的文件。

出0入0汤圆

 楼主| 发表于 2014-12-8 18:50:06 | 显示全部楼层
变 无 止 境
本系列入口:一线研发之声 之 跑马灯变形记(一)
经历前面的四次变形,应该说,跑马灯这棵小树苗,也算根红苗正了。具备了向上继续生长的条件,长歪掉的可能性较低。因此我们可以进一步拓展其功能。
1)  增加一个按键,控制跑马灯的启动和停止。这个时候,有按键抖动滤波、译码和事件传递需要考虑。
2)  增加一个串口通讯,敲入不同的命令,实现不同的动作。如下
命令字符串        实现功能
left2right        从左到右闪烁
right2left        从右到左闪烁
stop        停止
这个时候,需要考虑串口的字节流如何截止,使变成一串命令。又如何告诉串口的使用者:包接受完成,请取包分析?
3) 灯,只是一个商业系统里面,最最简单的控制,尚且如此讲究。如果加上一些基本的标配:Lcd,蜂鸣器、矩阵扫描按键,系统又该如何构建?
4)  随着系统的日益变大,当main模块while(1)里面代码的运行时间超过LIGHT_INTERVAL_TIME时,跑马灯的间隔控制将会出现较大的时间误差,请问如何避免这个情况。
这些,都是在构建嵌入式系统过程中常会遇到的问题,但对于没有经验的工程师来说,往往会感到阵阵无力与彷徨,难以兼顾系统的稳定性、可维护性和运行效率。
最常见的是:遍地的全局变量交叉混杂、到处的while(1)和看门狗、直入骨髓的if判断陷阱、心力交瘁的依赖关系。
整个系统处于一种“神秘的稳定状态”,
你会敬之畏之,继而离职,下任更快离职,继而瘫痪。

出0入0汤圆

发表于 2014-12-8 19:42:10 | 显示全部楼层
这个讲的不错很详细的

出0入0汤圆

发表于 2014-12-8 19:49:13 | 显示全部楼层
前面有代码的部分看得很仔细,也感到楼主严谨的思维变化,内心暗暗称赞好文章;后面纯文字没有代码的部分一带而过,有点龙头蛇尾的倾向。
后面应该继续用代码演示变形之----跑马灯之模块封装(可移植性);跑马灯之函数标准化(有标准的API接口来指示跑马功能)。。。

出0入0汤圆

发表于 2014-12-8 20:02:38 来自手机 | 显示全部楼层
支持楼主,希望有更多技术大牛能普及这些系列性关键问题。

出0入0汤圆

发表于 2014-12-8 20:03:25 | 显示全部楼层
对于老手来说,这个是必修的,必须有这种规范化的观念。但对于新手,你这么多东西砸上去他直接晕了,然后跑了。
规范什么的可以之后在写复杂程序的时候树立,先把人领进门再说吧

出0入0汤圆

发表于 2014-12-8 20:25:03 | 显示全部楼层
怎么看到一半有点晕了???楼主后面讲的很好但是代码或者图怎么都没了??还是我手机刷不出??楼主所讲授的书名叫什么?去买本看看。

出0入0汤圆

发表于 2014-12-8 20:25:19 | 显示全部楼层
怎么看到一半有点晕了???楼主后面讲的很好但是代码或者图怎么都没了??还是我手机刷不出??楼主所讲授的书名叫什么?去买本看看。

出0入0汤圆

发表于 2014-12-8 20:45:25 | 显示全部楼层
顶楼主, 像前面的坛友说的一样,后面也加上代码就更直观了;

瑕不掩瑜~!  赞一个~~!

出0入0汤圆

发表于 2014-12-8 21:01:17 | 显示全部楼层
又看了一遍,   请问楼主说的书是哪本??   想买来看下

出0入0汤圆

发表于 2014-12-9 00:25:15 | 显示全部楼层
这个...貌似之前论坛已经发过一份pdf上来的,不错

出0入0汤圆

 楼主| 发表于 2014-12-10 14:27:40 | 显示全部楼层
我是从别的地方看到好转来的   
一线研发之声 你百度下吧
这年头好人难做
写的确实不错  

出0入0汤圆

 楼主| 发表于 2014-12-10 14:28:00 | 显示全部楼层
Free_Bird 发表于 2014-12-8 20:45
顶楼主, 像前面的坛友说的一样,后面也加上代码就更直观了;

瑕不掩瑜~!  赞一个~~! ...

我是从别的地方看到好转来的   
一线研发之声 你百度下吧
这年头好人难做
写的确实不错  

出0入0汤圆

 楼主| 发表于 2014-12-10 14:28:40 | 显示全部楼层
jswd0810 发表于 2014-12-8 19:34
lz说的不错,就是后面几个不全,支持楼主

我是从别的地方看到好转来的   
一线研发之声 你百度下吧
这年头好人难做
写的确实不错  

出0入0汤圆

 楼主| 发表于 2014-12-10 14:29:03 | 显示全部楼层
cumtgao 发表于 2014-12-8 19:49
前面有代码的部分看得很仔细,也感到楼主严谨的思维变化,内心暗暗称赞好文章;后面纯文字没有代码的部分一 ...

我是从别的地方看到好转来的   
一线研发之声 你百度下吧
这年头好人难做
写的确实不错  

出0入0汤圆

 楼主| 发表于 2014-12-10 14:30:09 | 显示全部楼层
longwu537 发表于 2014-12-8 20:25
怎么看到一半有点晕了???楼主后面讲的很好但是代码或者图怎么都没了??还是我手机刷不出??楼主所讲授 ...

我是从别的地方看到好转来的   
一线研发之声 你百度下吧
这年头好人难做
写的确实不错  

出0入0汤圆

发表于 2014-12-10 16:17:41 | 显示全部楼层
这个的确写的不错。从底层开始裸机写,到MCU效率提高,再到一级级慢慢到模块封装。再到黑盒子接口。

出0入0汤圆

 楼主| 发表于 2014-12-10 16:35:39 | 显示全部楼层
slzm40 发表于 2014-12-10 16:17
这个的确写的不错。从底层开始裸机写,到MCU效率提高,再到一级级慢慢到模块封装。再到黑盒子接口。  ...

我只是觉得比较好  就转过来了
看你说的  真的说到好的方面了
大牛能细说下吗

出0入0汤圆

发表于 2014-12-10 16:40:51 | 显示全部楼层
非常好。授人与渔!

出0入0汤圆

发表于 2014-12-10 17:23:59 | 显示全部楼层
不错,跑马灯变形记 比一般书上的例子强多了!

出0入0汤圆

发表于 2014-12-14 21:26:30 | 显示全部楼层
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。  
        最后说一句,傻孩子的书是好书,不是做广告,而是实在的在教你学校学不到。那是思想。

       

出0入0汤圆

发表于 2014-12-16 15:45:07 | 显示全部楼层
学习了~~~~~~~~~~

出0入0汤圆

 楼主| 发表于 2014-12-18 17:29:08 | 显示全部楼层
slzm40 发表于 2014-12-14 21:26
我也是在学习中,不敢称大牛,也只是在此学习路上走的长一点而已。
其实大家也是这么过来的,不过变形记 ...

谢谢你的热情回复
受益匪浅呀

出0入0汤圆

发表于 2014-12-20 19:52:15 | 显示全部楼层
本论坛真不错,我也是从这里吸收营养慢慢入门的。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-3-29 13:53

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表