当前位置:首页 > 公众号精选 > 嵌入式微处理器
[导读]C语言到底该怎么学,单片机coder怎么才能顺利转型成为嵌入式programer?21ic论坛有一“镇站之宝”的超长经验分享贴,特此分享给所有热爱coding的你。


C语言到底该怎么学,单片机coder怎么才能顺利转型成为嵌入式programer?21ic论坛有一“镇站之宝”的超长经验分享贴,特此分享给所有热爱coding的你。


之前和大家谈了一点UML在嵌入式开发中的使用,以及链表、哈希表等数据结构在实现对象之间的交互机制(设计模式)的一点简单实例。有很多朋友表示很感兴趣,21ic高手云集,有点班门弄斧的感觉,所以还望尽情拍砖。之前的帖子很乱,除了因为太随意没有准备外,更主要是因为本人也处于半瓶子阶段,所谈问题题目又太大。对此我只能凭借拙见,谈点个人的理解,由于本人是这方面的新手,凭借一己之热情,大放厥词,还请各位斧正。

其实UML就是一个工具,提供了用例图、顺序图、活动图、类图、状态机图、部署图、包图等工具,辅助工程师完成:分析->设计->实施->测试的整个过程。每个过程都有细分,例如分析阶段:首先是需求分析,然后有系统分析,再次有对象结构分析等。需求分析阶段会使用到用例图、顺序图、状态机图、顺序图等,需求分析阶段的最重要的要素是软件的外部功能视图和使用场景等。其中前者使用用例图表述,它也提供了沟通用户和开发者的桥梁;后者用顺序图或状态机图等表述,提供了系统每个功能实现的可能路径。其他过程和需求分析阶段类似,这里篇幅所限就不再一一提及。UML就是这样同我们的设计过程关联起来的。

将面向对象的方法用于MCU是有必要的,也是可能的,当然也是很有效的。这样的努力最起码可以拉近mcu开发者同其他领域的C开发者之间的距离,弥补那道似乎难以逾越的鸿沟,比如看到linux内核代码时,你会发现原来如此亲切。当然,随着对面向对象方法的深入理解,你会发现C++也不再那么让你不知道如何使用,或者把C++用得像面向过程的语言一样。当然本人C++菜鸟,还望高手指教。

然而面向对象的方法也非一蹴而就,一朝搞定,它是一个循序渐进的过程,特别是应用与mcu这样的平台,好多东西是靠摸索出来的。如何开始,先从何处下手是个问题。

21ic同仁liufb提议:“正如《重构与模式》所说:如果想成为一名更优秀的软件设计师,了解优秀软件设计的演变过程比学习优秀设计本身更有价值,因为设计的演变过程中蕴藏着大智慧。”

我决定发掘一下我近十年以来的阶段性C代码,试图去发现一点什么,这个我之前还从未尝试过,能找到的一起的代码也寥寥无几。不过我觉得值得一试,那就从此开始吧。

努力发掘,搜索N年前的邮箱,居然找到了当时在一款AT89X52单片机上的处女作。就从它开始入手了。

时代背景:2006年,郑州某小公司,之前的工作是修手机,然后是在某气体传感器公司焊接维护生产设备,再后来在这家小公司画电路板,然而软件才是我的最爱。好不容易boss开恩,让我参与到写代码的行列。之前的进度让在郑州这种蜗牛般的工作节奏的大氛围里面的boss也觉得忍无可忍,于是我加入了。

代码太长,截取一部分吧。里面只有我写的一个子函数,大部分是同事写的。 

由于做开始工作的同事不太会用多文件,所以这个项目的代码只有一个文件,连头文件都没有,整个文件有2600行代码。以下我将列举它的三大部分:

          1.全局变量部分。
          2.部分子函数。
          3.main函数。

最后我将会用现在的眼光,结合大师(Grady Booch)的经典,分析一下这部分代码。

全局变量部分:
bit FUN,Shift_on,Add_on,Sub_on,fun_flag;bit dspflagz1,dspflagz2,dspflagz3;unsigned char z1,z2,td3,working,DSP_m,DSP_n;unsigned char l1,l2,r,m;bitflagv,flagy,flags,flag0,flagx,beepflag1,beepflag2,flagt,flagw;bit working_on,function_on, AINTSYR_on,AINTSYW_on,BINTSYR_on,BINTSYW_on ;bitprogram_on,program_woking,up_flag,down_flag,up_on,down_on;unsigned char AINTSY_state, BINTSY_state, function_state;unsigned char tx1,tx2,tx3,tw,TX,t;unsigned char display_state ,x1,x2,w1,w2;unsigned char program_state,program_working;unsigned char clk_number;unsigned char code DS[]={0,33,63,86,100,86,63,33};unsigned chards_curtime[6]={0x05,0x03,0x02,0x01,0x07,0x06};unsigned char clk_data[6]={06,1,1,0,0,1};unsigned char set_time[6];sbit switch_work= 0xB0;sbit switch_function=0xB1;sbit switch_program=0xB2;sbit switch_up=0x90;sbit switch_down=0x91;sbit switch_AINTSYR=0x92;sbit switch_AINTSYW=0x93;sbit switch_BINTSYR=0x94;sbit switch_BINTSYW=0x95;sbit RS=0xA2;sbit RW=0xA1;sbit E=0xA0;sbit CS2=0xA3;sbit CS1=0xA4;sbit DACS1=0xA7;sbit DACS2=0xA6;sbit DACS3=0xA5;sbit ds_sclk=0xB3 ; /*初始化变量*/sbit ds_io=0xB4;sbit ds_rst=0xB5;
初评:除了最后是管脚定义外,前边都是全局的标志位或全局变量。

这个 void text(void)可是我的处女作啊
//////////////////////////////////////////////////////////////////////////////////////////////////////void text(void){
bit Flag_add; /*加一标志*/ bit Flag_sub; /*减一标志*/ unsigned char max_value; /*各时间单位的最大值*/ unsigned char min_value; /*各时间单位的最小值*/ /*if(FUN==1) { */ /*定义标志位*/ if(switch_work==0) /*移位键*/ { if(Shift_on==0) { Shift_on=1; buzzer(); clk_number++; if(clk_number>6)clk_number=1; } } else Shift_on=0; if(switch_up==0) /*加一键*/ { if(Add_on==0) { Add_on=1; buzzer(); Flag_add=1; } } else Add_on=0; if(switch_down==0) /*减一键*/ { if(Sub_on==0) { Sub_on=1; buzzer(); Flag_sub=1; } } else Sub_on=0; switch(clk_number) { case 1: max_value=99;min_value=0;break; case 2: max_value=12;min_value=1;break; case 3: if(clk_data[1]==1|| clk_data[1]==3|| clk_data[1]==5|| clk_data[1]==7|| clk_data[1]==8|| clk_data[1]==10|| clk_data[1]==12) max_value=31; /*1,3,5,7,8,10,12*/ else if( clk_data[1]==4|| clk_data[1]==6|| clk_data[1]==9|| clk_data[1]==11) max_value=30; /*4,6,9,11*/ else if((clk_data[0]%4==0)||clk_data[0]==0) max_value=29; /*闰年*/ else max_value=28; min_value=1; break; case 4: max_value=23;min_value=0;break; case 5: max_value=59;min_value=0;break; case 6: max_value=7;min_value=1;break; } if(Flag_add==1) { clk_data[clk_number-1]++; Flag_add=0; if(clk_data[clk_number-1]>max_value) clk_data[clk_number-1]=min_value; } else if(Flag_sub==1) { clk_data[clk_number-1]--; Flag_sub=0; if(clk_data[clk_number-1]-1]==0xff) clk_data[clk_number-1]=max_value; } if(switch_function==0) { if(function_on==0) { function_on=1; FUN=0; buzzer(); function_state=1; fun0_flag=1; set_time[0]=(clk_data[4]/10)*0x10+(clk_data[4]%10); set_time[1]=(clk_data[3]/10)*0x10+(clk_data[3]%10); set_time[2]=(clk_data[2]/10)*0x10+(clk_data[2]%10); set_time[3]=(clk_data[1]/10)*0x10+(clk_data[1]%10); set_time[4]=(clk_data[5]/10)*0x10+(clk_data[5]%10); set_time[5]=(clk_data[0]/10)*0x10+(clk_data[0]%10);

} } else { function_on=0; }
}
注:上面这个函数是我在51里面跑过的第一段代码,很有收藏价值,哈哈

//////////////////////////////////////////////////////////////////////////////////////////////////////
下边是我同事的一个函数,女工程师,女中豪杰,哈哈
//////////////////////////////////////////////////////////////////////////////////////////////////////
void check_switch(){ if(FUN==0) { if(switch_work==0) { if(working_on==0) {  working_on=1; buzzer(); if(working==1) { working=0; flag0=0; } else { working=1; flag0=1; } } } else { working_on=0; }
if(switch_function==0){ if(function_on==0){ function_on=1; buzzer(); program_state=1; if(function_state==1){ function_state=2;
} else { if(function_state==2){ function_state=3;

} else { if(function_state==3){ function_state=4; FUN=1; fun1_flag=1; working=0; flagx=1; } else { function_state=1;
} } } } } else { function_on=0; }
if(switch_program==0) { if(program_on==0) { program_on=1; buzzer(); program_working=1;
flagv=1; flagy=1; flagt=1; flagw=1; } } else { program_on=0;
} if(switch_up==0) { if(up_flag==0) { up_flag=1; up_on=1; buzzer(); } } else { up_flag=0; } if(switch_down==0) { if(down_flag==0) { down_flag=1; down_on=1; buzzer(); } } else { down_flag=0; } if(switch_AINTSYR==0) { if(AINTSYR_on==0) { AINTSYR_on=1; buzzer(); if(AINTSY_state<=63) { AINTSY_state=AINTSY_state+1; }
} } else { AINTSYR_on=0; }
if(switch_AINTSYW==0) { if(AINTSYW_on==0) { AINTSYW_on=1; buzzer(); if(AINTSY_state>=1) { AINTSY_state=AINTSY_state-1; }
} } else { AINTSYW_on=0; } if(switch_BINTSYR==0) { if(BINTSYR_on==0) { BINTSYR_on=1; buzzer(); if(BINTSY_state<=63) { BINTSY_state=BINTSY_state+1; }
} } else { BINTSYR_on=0; }
if(switch_BINTSYW==0) { if(BINTSYW_on==0) { BINTSYW_on=1; buzzer(); if(BINTSY_state>=1) { BINTSY_state=BINTSY_state-1; }
} } else { BINTSYW_on=0; } } else { text(); }
}
初评:注意到没有,几乎没有用到参数传递,整个代码所有数据全是通过全局变量传递的。这个我将在后边做出分析。

最后是main函数:
void main(){  init_working();/*初始化程序*/

start_t0:check_switch();
prepare_work(); if(fun0_flag==1){ ds_settime(set_time); } display();
goto start_t0;}

站在巨人的肩上才能看得更远,先找一个这样的肩膀吧。从关于代码演变的基础知识说起:

【基础知识普及】

Wegner将早期高级语言做了分类:



以上内容摘自GradyBooch《面向对象:分析与设计》
扩展一下Wegner的分类:



以上内容摘自Grady Booch《面向对象:分析与设计》

接下来看一下第一代和第二代早期程序设计语言的程序结构:
程序设计语言-早期结构.jpg (18.77 KB, 下载次数: 0)

特点:
1. 所有子程序共用全局数据。
2. 共用数据带来大量交叉耦合。
3. 大量的标志位或者数据定义很难读懂它到底代表什么意思。

从前面的分类看,C怎么也是3代以后的语言(1970-1980)。再看一下我前面的例子中我和我同事的代码,几乎就是第一代和第二代。

早期语言写程序的特点:

1.几乎很少使用参数传递,所有子程序几乎全部依靠共有全局变量来传递数据。
2.共享全局变量带来的交叉耦合让这个代码调试起来非常费劲,因为每个函数都不是独立的,它依赖自身使用的大量全局变量。在MCU上,因为中断函数带来的并发性,如果有全局变量在中断内外都用到,那就会带来很多麻烦。这样的代码如果在多任务环境中将会更糟糕。
3.再看看可读性,大量全局变量和全局标志位让代码的可读性非常差:首先是如此之多的变量和标志位所要表达的意图,再者他们分散得到处都是,即使不考虑并发性也让阅读的人摸不到头脑。
4.看看扩展性和可维护性,如果有bug被测出,你定位问题将是极其困难的,因为这里的子程序没有内聚性,功能不独立,再加上可读性差,情况非常糟糕。如果要添加新功能,也是一件极其麻烦的事情,牵一发而动全身,要改的地方太多了。这样的编程方式也无法将项目做大。

你有这样的经历吗?刚开始做单片机的差不多都是这样的思维吧,除非之前有过其它领域的C开发经验。

为什么三代以后的语言写的程序出现了二代前期以前的特点?我们可以将它称作“语言的返祖现象”(这个将在后边专门讨论,这个概念可是咱首次提出的哦,哈哈,臭美一下)。

说明一个问题:每一代语言的开发者都是针对当时成熟而又先进的软件思想完成的,它里面支持他所要实现的那种编程思想所具备的大部分特征。但是如果使用者自己没有理解这种新的思想,而不使用这些新的特征,就会出现这种“返祖”现象,就像我身边很多人使用C++的时候,完全是用C的思维在编程,那些面向对象的特征完全成了他们的限制,这也算是“返祖现象”。

既然你发现了它的缺陷,既然你已经知道它的‘错’在哪里,那问题可以得到解决了吧!问题怎么解决,如何改进?别着急!接下来我们将会看语言的下一次革新,还会附上下一阶段的代码做为实例。我简直就是一部活生生的发展史了。

插播【基础知识普及】
软件固有特性:复杂性等问题
【软件固有的复杂性】
* 问题域的复杂性 :
         —— 非功能性需求(可用性,健壮性,成本,性能等)的加入。
         —— 开发人员同用户之间沟通的困难。
         —— 设计过程中的需求变更。
* 管理开发过程的困难性
* 软件中随处可见的灵活性
—— 这直接导致了软件开发领域不能像其他领域,可以对每个构件给出一个行业标准,比如机械制造行业,软件行业却很少能如此。这导致开发者需要打造大部分模块。
* 描述离散系统行为的问题
—— 无法用连续系统建模的方式,本身也不受物理规律支配。

【复杂系统的5个属性】
* 层次性:可以逐级划分子系统。
* 相对本原:不同观察者对同系统组件的本原性认识不同。
* 分离关注:根据“组件”内部之间关联程度高于“组件”间外部关联程度来划分组件。
* 共同模式:如细胞,管脉系统在动物和植物中都有。
* 稳定的中间形式:复杂系统是演化而来的,曾经的‘复杂系统’或其他‘复杂系统’变成基本组成。

【复杂系统的规范形式】
* 复杂系统的两种构成层次:1. 组成部分(Part of) 2. 是一种(Is a)


【控制复杂性的技巧】
控制复杂性的核心技巧:分而治之
算法分解 (自顶向下的结构化设计)强调了事件的顺序。
面向对象的分解 :强调了一些代理。

如果把之前的代码比作旧石器时代,接下来就是新石器时代了。
在开始分析07年的代码之前,先科普一下:
【普及基础知识】
第二代和第三代前期程序设计语言的程序结构:

程序设计-二-三代半.jpg (47.98 KB, 下载次数: 0)

和图2-1相比有不同吗?对了。子程序不再那么单一,它有了嵌套的结构。看看这个时期引入了什么特点:
1.子函数调用支持嵌套。
2.支持各种参数传递。
3.程序拥有更丰富的控制结构。
4.声明的可见性范围多样化。如全局变量和函数内部的局部变量。
这个时候开始出现结构化程序设计。

这个是07年,我管这个时期叫做:

【新石器时代】
这是07年中的一个研发项目,是一个电力抄表终端,下面代码是他的液晶显示屏(128x64)的菜单部分。

代码有点长,先看头文件:
menu.h

#include"display_leaf.h" //定义了显示叶子的函数//字模数组extern root_canshu[],root_shuju[],root_zhuangtai[],exit[],zhongduancanshu[],fukongcanshu[],celiangdian0[], celiangdian0canshu[],celiangdian1canshu[], celiangdian1[],celiangdian2[],celiangdian3[],zhongduanzhuangtai[],zhuzhantongxin[],fuhekongzhi[] ,tongxincanshu[],gaojingcanshu[],duankoucanshu[],biaoxieyi[],jibencanshu[],gongkongcanshu[] ,diankongcanshu[],dianzijishu[],biaojitongxin[],gongkongfangan[],diankongfangan[],diannengliang[], xuliang[],shunshiliang[],huanyingshiyong[],celiangdian0shuju[],celiangdian1shuju[],celiangdian2shuju[],celiangdian3shuju[];//--------------------------------------------------------------------------//-------------------------------------------// 子界面编号数组//-------------------------------------------unsigned char Son0[1] ={1}; //对应界面0unsigned char Son1[4] ={2,3,4,0};unsigned char Son2[5] ={5,6,7,8,1};unsigned char Son3[5] ={9,10,11,12,1};unsigned char Son4[5] ={55,56,57,58,1};unsigned char Son5[5] ={31,32,33,34,2};unsigned char Son6[5] ={35,36,37,38,2};unsigned char Son7[3] ={39,40,2};unsigned char Son8[3] ={41,42,2};unsigned char Son9[4] ={43,44,45,3};unsigned char Son10[4] ={46,47,48,3};unsigned char Son11[4] ={49,50,51,3};unsigned char Son12[4] ={52,53,54,3};//*****************//以下是增加的叶子//*****************
//-----------------------------------------------------------//各个界面显示内容指针数组//-----------------------------------------------------------unsigned char *menu_char0[1]={huanyingshiyong};unsigned char *menu_char1[4]={root_canshu,root_shuju,root_zhuangtai,exit}; //各元素枝指向对应行显示内容的字模数组unsigned char *menu_char2[5]={zhongduancanshu,fukongcanshu,celiangdian0canshu,celiangdian1canshu,exit};unsigned char *menu_char3[5]={celiangdian0shuju,celiangdian1shuju,celiangdian2shuju,celiangdian3shuju,exit};unsigned char *menu_char4[5]={zhongduanzhuangtai,zhuzhantongxin,biaojitongxin,fuhekongzhi,exit};unsigned char *menu_char5[5]={tongxincanshu,gaojingcanshu,duankoucanshu,biaoxieyi,exit};unsigned char *menu_char6[5]={jibencanshu,gongkongcanshu,diankongcanshu,gongkongfangan,exit};unsigned char *menu_char7[3]={jibencanshu,gaojingcanshu,exit};unsigned char *menu_char8[3]={jibencanshu,gaojingcanshu,exit};unsigned char *menu_char9[4]={diannengliang,xuliang,shunshiliang,exit};unsigned char *menu_char10[4]={diannengliang,xuliang,shunshiliang,exit};unsigned char *menu_char11[4]={diannengliang,xuliang,shunshiliang,exit};unsigned char *menu_char12[4]={diannengliang,xuliang,shunshiliang,exit};
//-----------------------------------------------------------// 各行文字数量//-----------------------------------------------------------unsigned char char_num0[1] = {4};unsigned char char_num1[4] = {2,2,2,2};unsigned char char_num2[5] = {4,4,6,6,2};unsigned char char_num3[5] = {6,6,6,6,2};unsigned char char_num4[5] = {4,4,4,4,2};unsigned char char_num5[5] = {4,4,4,3,2};unsigned char char_num6[5] = {4,4,4,4,2};unsigned char char_num7[3] = {4,4,2};unsigned char char_num8[3] = {4,4,2};unsigned char char_num9[4] = {3,2,3,2};unsigned char char_num10[4] = {3,2,3,2};unsigned char char_num11[4] = {3,2,3,2};unsigned char char_num12[4] = {3,2,3,2};
//-----------------------------------------------------------struct INTERFACE{//当前行号对应子菜单编号,和反显行,对于不需要反显示的无意义//行号在每次进入新的界面时清零 unsigned char MAX_ROW_NUM; //该界面的最大行数 unsigned char **MENU_char; //指向一指针数组,该数组元素为指向各行显示内容的指针 unsigned char *Son_num; //指向当前界面子界面编号数组的指针 unsigned char *Row_num; //指向当前界面各行文字数量数组的指针};
struct INTERFACE Windows[13] ={ {1,menu_char0,Son0,char_num0}, {4,menu_char1,Son1,char_num1}, {5,menu_char2,Son2,char_num2}, {5,menu_char3,Son3,char_num3}, {5,menu_char4,Son4,char_num4}, {5,menu_char5,Son5,char_num5}, {5,menu_char6,Son6,char_num6}, {3,menu_char7,Son7,char_num7}, {3,menu_char8,Son8,char_num8}, {4,menu_char9,Son9,char_num9}, {4,menu_char10,Son10,char_num10}, {4,menu_char11,Son11,char_num11}, {4,menu_char12,Son12,char_num12}};

//叶子节点的数据结构struct LEAF{ unsigned char Father_num; //该叶子要返回的父亲界面号 void (*Display_leaf)(); //指向该叶子的显示函数的指针};struct LEAF leaf[28] ={ {5,display_tongxincanshu}, {5,display_gaojingcanshu}, {5,display_duankoucanshu}, {5,display_biaoxieyi}, {6,display_jibencanshu}, {6,display_gongkongcanshu}, {6,display_diankongcanshu}, {6,display_gongkongfangan}, {7,display_cljibencanshu}, {7,display_clgaojingcanshu}, {8,display_cljibencanshu}, {8,display_clgaojingcanshu}, {9,display_diannengliang}, {9,display_xuliang}, {9,display_shunshiliang}, {10,display_diannengliang}, {10,display_xuliang}, {10,display_shunshiliang}, {11,display_diannengliang}, {11,display_xuliang}, {11,display_shunshiliang}, {12,display_diannengliang}, {12,display_xuliang}, {12,display_shunshiliang}, {4,display_zhongduanzhuangtai}, {4,display_zhuzhantongxinzhuangtai}, {4,display_biaojitongxinzhuangtai},{4,display_fuhekongzhizhuangtai}};
//-----------------------------------------函数--------------------------------------------------void Display_char(unsigned char,unsigned char ,unsigned char *,unsigned char);void Display_row(unsigned char,unsigned char ,unsigned char ,unsigned char *,unsigned char);void Display_window(unsigned char,unsigned char); //

//-----------------  menu.c 部分函数  --------------

//-----------------------------------------函数--------------------------------------------------void Display_char(unsigned char,unsigned char ,unsigned char *,unsigned char);void Display_row(unsigned char,unsigned char ,unsigned char ,unsigned char *,unsigned char);void Display_window(unsigned char,unsigned char); 
//-------------------------------------------------------------------------//功能: 显示16x16汉字//参数: begneX : 行地址 beginRow:列地址 n: 汉字数量 // s: 显示内容 标志寄存器: 是否反显//调用: display_Lf() display_Rf()//zkq 2007.06.04//-------------------------------------------------------------------------void display16x16RL(unsigned char beginX,unsigned char beginRow,unsigned char n,unsigned char *s,unsigned char 标志寄存器){ unsigned char i; if(标志寄存器) { LCD_Write_ComR(0xb8|beginX); LCD_Write_ComR(0x40|0); for(i=0;i<64;i++) { LCD_Write_DatR(0xff); } LCD_Write_ComL(0xb8|beginX); LCD_Write_ComL(0x40|0); for(i=0;i<64;i++) { LCD_Write_DatL(0xff); } LCD_Write_ComR(0xb8|beginX + 1); LCD_Write_ComR(0x40|0); for(i=0;i<64;i++) { LCD_Write_DatR(0xff); } LCD_Write_ComL(0xb8|beginX + 1); LCD_Write_ComL(0x40|0); for(i=0;i<64;i++) { LCD_Write_DatL(0xff); } } for(i=0;i { displayrow(i,beginX,beginRow+i,s,标志寄存器); }}
//--------------------------------------------------------------------------------------
void Display_window(unsigned char win_num,unsigned char sel_row){
unsigned char j,f; if(Windows[win_num].MAX_ROW_NUM < 4) f = Windows[win_num].MAX_ROW_NUM - 1; else f = 3; if(sel_row<4) { for(j=0;j<=f;j++) { if(j==sel_row) display16x16RL(2*j,1,*(Windows[win_num].Row_num + j),*(Windows[win_num].MENU_char + j),1); else display16x16RL(2*j,1,*(Windows[win_num].Row_num + j),*(Windows[win_num].MENU_char + j),0); } } else if(sel_row<8) { for(j=0;j<=Windows[win_num].MAX_ROW_NUM - 5;j++) { if(j==sel_row%4) display16x16RL(2*j,1,*(Windows[win_num].Row_num + j + 4),*(Windows[win_num].MENU_char + j + 4),1); else display16x16RL(2*j,1,*(Windows[win_num].Row_num + j + 4),*(Windows[win_num].MENU_char + j + 4),0); } }}

不到一年的时间,是不是有所长进呢?
为什么呢?

1. 06年年底研究了一下邵贝贝翻译的那本《嵌入式实时操作系统uCOS-II》
   * 对操作系统有了一点了解,包括多任务,任务间的同步和通信、优先级,内存管理,移植等。
   * 对数据结构的使用,对指针的使用。
   * 代码风格等。

2.我的boss老张的工作热情深深感染了我,他是我的boss也是我的朋友,他为我提供了一个使用ucos的平台。就在我昨天写帖子的时候,又听他讲linux下基于QT的组态软件的使用效果,那是他最近做的一款PLC的嵌入式平台的一部分。有这样的朋友我很开心。这个小菜单就是老张当年专注于电力抄表项目的时候我给他做的。

3.当然,06年后半年我不少泡郑州大学新校区的图书馆。《深入理解linux内核》也瞟过两眼,看着很困难。蹭过信息工程学院的《数据结构》收获很大,里面讲的那个循环队列至今都在使用中。那本道格拉斯的《嵌入式与实时系统开发》当时候看着就像天书。

大师就是大师,Booch的总结太到位了,他简直就是在讲我本人啊,我又中招了。

再回头看一下第二代和第三代前期的程序设计语言的特点。这次我唯一没有做到的是模块化,这个时候我还没有将C的特点发挥出来。继续对上面的代码做些分析吧。

如果从菜单的实现角度讲,这是一个很差劲的设计。主要是没有对显示内容本身做进一步的抽象,没有类似按钮,文本框,复选框等,窗口只有几行文字,被选中的行反显。因为没有真正意义上的“窗体”的概念,所以也没有真正意义上同窗口绑定的“事件”的概念,三个按键:上翻键,下翻键用来移动光标,确认键用来进入该项关联的窗口,关联关系被上面头文件里面的全局数组和全局结构体数组定义。

菜单本身如果改进的话,可以通过上面提到的做进一步抽象的方法。从窗口和窗口的组成元素(文本框,按钮,复选框等等)的独立实现上努力,再加上事件。

单单从程序设计的角度看:

1. 结构上,最主要的逻辑关系还是通过全局变量之间的组织关系来实现的。
2. 可读性很差,窗口内容很不直观,因为反映窗口(或窗口组成部分)属性的内容分散在各个数组里面。
3. 可扩展性差,添加或者修改一个窗口(或窗口组成部分)需要更改很多地方。
4. 没有一个统一的事件处理机制,按键的处理结果也是通过上面数组的关系反映的。

我应该收回对第二个阶段的比喻:远古时代。我的2007应该算是青铜时代了:虽然水平很菜,但是热情而有想法。

而我的2008好比中世纪,因为郑州有些公司实在太无所事事,除了画几个PCB或者做一些散碎的不知所谓的小项目,剩下的就是像中世纪的巫师那样做一些不登台面的事情:私下学点linux环境下工具的使用和简单开发。漫长的一年毫无事事,没有什么明显收获。linux的接触犹如地理大发现,我对新大陆充满了渴望。整个2009年犹如我的大航海时代。这一年我接手了一个linux的应用项目,是电网监测系统的一部分;这一年我跑遍了全国很多地方的“国家电网”:河南的,山东的,湖北的...知名电力电网公司,比如:四方、南瑞等。我拿我们的电力电网监测从设备系统和他们的主设备对接,既维护原有系统,又增加新的功能,对于接触Linux应用开发不久的我来说,这是一个很好的机会。之前的系统是时工做的,程序写得很有技巧,对于当时的我来说难度也是颇大的。对此时工常常给予我指导和帮助,我至今都十分感激。

所以接下来我会分析一下这部分代码,当然开始之前我会插入一点小知识,就是前面提到过的对于问题的面向过程的分解(算法分解),因为这部分最典型的特定就是算法分解。
更多操作

算法分解:

对于接受过自顶而下的结构化程序设计的人来说,首先想到的是用算法分解将复杂系统划分成简单的部分。其中每一个模块是某个总体过程(some overall process)的一个主要步骤(a major step)。下面例子是Booch给出的一个程序的部分设计,完成更新一个主控文件的内容。它完全就是对一个流程的逐级细化。



这样划分的模块虽然可以独立编译,但是完全没有封装的概念,它只关注流程,只是功能的划分。接下来我的实例中你会体会到这一点,当然前辈的功力还是很深厚的,程序一直很稳定,但是阅读起来很费劲,扩展性不好,当然像TCP部分也做了一定的封装。整体上主要还是按照功能而不是实体做为模块划分的标准。即使如此,我从中还是收益匪浅的。

接下我会把代码的各个功能模块做些介绍,期间插入我自己的理解,最后根据分析结果做些总结。后边的代码没有那么简单了,加上多年没有看过,以后就不再设什么前提。温故而知新,很多新发现也是出乎我意料的,因为今天的眼光不同以前。大家有什么意见和观点尽管提,感兴趣的话一起开始发现之旅吧。

先看大概介绍一下这个程序:首先,因为保密的缘故,我不能将整个系统的功能做出描述,设计业务方面的内容也不会出现,但是这不影响我们关心的问题:代码本身的讨论和分析。
这个程序运行的平台是:moxa的嵌入式工控计算机,好像是74xx系列,平台是xsceal(当初是ARM架构v5TE指令集的CPU。2006年6月,Intel将其通信及应用处理器业务出售给Marvell公司,并作为一系列不同微架构的处理器的品牌),当然这个程序是平台无关的,要求有两个网卡设备和若干个串口设备即可,完全可以跑在所以类unix平台下,只需要重新编译即可。和它相连的下行设备也是一个ARM9+linux2.4.x的平台,它从该设备获取前端采集数据,以私有协议的形式通信。上行设备为国家电网控制中心主站,它响应主站的请求,以电力通信规约(国标)通信。设备本身的串口支持链接几路下行的485设备。整个系统结构就是这样,详细的功能我这边就不做描述了。希望能谅解,这也和我们讨论的主题无关。
首先从内容上对这个程序做个大概的浏览:

所有代码:

头文件:



C文件:
配置文件:


makefile:

既然主要还是算法分解型的,每个文件里面都是这些分解后的结果,粗略介绍一下这个程序的分解情况,尝试将亮点呈现出来,同时指明改进的想法和好处。

先看一下main函数:
int main(void){ ....... init_system(); //系统初始化
InitPassiveSock(); //监听套接字初始化
pthread_create(&ReceID, NULL, (void *)&RecePross, NULL); //创建接收处理线程
while(1){ QuerySocketsMsg(); //处理socket ....... } .......}

所在文件:main.c,各个过程完成如下功能:

一. 系统初始化(init_system):

代码:
void init_system(void){ init_para(); // 1 init_serial(); // 2 init_ethernet(); // 3 ......}

过程:

        1. 读取配置文件,结果存入全局数据结构。
        2. 初始化串口通信相关数据结构,打开串口,创建串口处理线程等。
        3. 初始化tcp通信相关数据结构。(仅此而已)
       
这一部分比较简单,不做更详细分解。下一部分:二. 监听套接字初始化
更多操作

二. 监听套接字初始化

开始之前,先介绍一下这个程序最主要的几个数据结构:

/* 套接字结构 */typedef struct Sockets { fd_set readfds, writefds, exceptfds; // 1. 要被检测的可读、可写、例外的套接字集合
int PmuSock; // 2. PMU规范套接字 ......
struct Sockets_Comm *Comm; // 3. 为设置中每一个通讯通道建立网络接口} Sockets;

1. 做过IO复用的都知道这个。
2. 用来监听(listen())主站链接的监听socket。
3. 是个结构体数组:数组长度等于通信通道数量。结构体定义如下:


 /* COMM网络结构 */ typedef struct Sockets_Comm{ int Protocol; // 1. 该链接使用的规约类型 int MainSock; // 2. 主套接字(即上面提到的监听套接字accept到的链接套接字) pthread_t MainThreadID; // 4. 主线程ID,该线程用于处理该链接通信。 pthread_cond_t MainCond; pthread_mutex_t MainMutex;
............ }Sockets_Comm;


        1. 规约类型:该链接使用哪种通信协议。
        2. (如上注释)
        3. (如上注释)

【插播】   ----知识点

开闭原则

在面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”[1],这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

开闭原则的命名被应用在两种方式上。这两种方式都使用了继承来解决明显的困境,但是它们的目的,技术以及结果是不同的。

梅耶开闭原则

勃兰特·梅耶一般被认为是最早提出开闭原则这一术语的人,[来源请求]在他1988年发行的《面向对象软件构造》中给出。这一想法认为一旦完成,一个类的实现只应该因错误而修改,新的或者改变的特性应该通过新建不同的类实现。新建的类可以通过继承的方式来重用原类的代码。衍生的子类可以或不可以拥有和原类相同的接口。

梅耶的定义提倡实现继承。具体实现可以通过继承方式来重用,但是接口规格不必如此。已存在的实现对于修改是封闭的,但是新的实现不必实现原有的接口。

多态开闭原则

在20世纪90年代,开闭原则被广泛的重新定义由于抽象化接口的使用,在这中间实现可以被改变,多种实现可以被创建,并且多态化的替换不同的实现。

相比梅耶的使用方式,多态开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用。已存在的接口对于修改是封闭的,并且新的实现必须,至少,实现那个接口。

罗伯特·C·马丁1996年发表的文章《开闭原则》[2]是使用这种方法的启发式著作。在2001年,Craig Larman把开闭原则关联到了Alistair Cockburn的名为受护的变量的模式以及David Parnas关于信息隐藏的讨论。[3]


以上内容来源于《维基百科》,总得来说,开闭原则不仅针对面向对象设计,即使面向过程设计的软件,如果尽可能做到这一点也是很有必要的。虽然面向过程的先天性决定了,这是困难的。然而有些规模庞大的软件:如linux内核,虽然它本身具备面向对象思想,终究不是一个完全面向对象的大工程,但是它的开闭原则做得很到位,这个了解的人一定深有体会。

最近太忙,代码分析耽误了一阵子,恐怕一时半会儿还不能续上,最近一直在考虑一个问题:面向对象真的是最好的吗?真的完全适用于嵌入式开发吗?

曾经看到过Linus在一个帖子里痛批了面向对象语言。他认为面向对象语言以对象为核心,加一些相关联的方法,简直是呓语。重要的东西应该是数据结构,对象本身有啥重要?真正有意思的,是在不同类型的不同对象交互而且有锁规则的时候。但是,即使是这时候,封装什么“对象接口”也绝对错误,因为不再是单一对象的问题了。他的结论是,面向对象解决的都是一些小问题。
确实有很多“大人物”一直强调:“最重要的是数据结构”。

关于这个问题,我最近几天在考虑。一个基本的现象是:目前几乎所有的操作系统都不是以对象为核心的,就拿大家熟悉的ucos来说,它最核心的是:

多任务的实现 --- systick中断中利用TCB(任务控制块)链表和任务状态信息(就绪、空闲、挂起等)完成对任务的调度。
优先级的实现 --- 有一个专门的数据结构(优先级数组)来实现优先级。
中断的管理 ------
内存的管理 ------ 内存控制块
任务间的通信和同步等 ------ 消息队列,互斥量,邮箱等

无不体现了数据结构的重要,这里面几乎看不到对象的存在。

到底对象的适用范围在哪里,我们该怎么做?其实这是一个很有挑战性的问题,目前还没有看到过专门讨论这个问题的论著。首先我们大致可以看到:所有“平台部分”代码(像操作系统)最好不要以对象为中心,在没有使用操作系统的应用里,对CPU的管理:中断的管理,并发的实现,消息和同步机制等也属于这个范畴,这部分的特点是:
1. 所完成功能是对CPU功能的扩充和管理。
2. 虽然CPU平台有差异,但是体系结构相同或相似,处理方法大部分相同(否则就不存在跨平台的OS)。
3. 这部分是同CPU打交道的,和人类的思维差别很大。
所有业务相关的信息最好要以对象为中心。除了上述“平台部分”的代码,剩下应该都是业务(广义)上的内容,这部分的特点是:

1. 所完成的功能是系统具体的业,不是平台的扩展和管理。
2. 该部分和CPU无关,可以以对象为中心。

所以,关于这个问题可以给出以下结论:

在软件领域,可以不用想物理学那样,需要考虑到理论的统一性,比如:宏观上用一套定理,量子尺度用另一套就会让人不舒服 --- 凭什么同样的宇宙给出不一样的描述方式?这也是爱因斯坦后半辈子想解决的问题。软件领域不受物理规律支配,能完成功能,解决问题就是王道。完全可以针对系统的不同实体部分采用不同的设计方法学。事实上我们一直都是这么做的,看看我们的个人电脑系统的操作系统部分和上面的应用软件。

这样,我们的问题就变成:

1. 在那些开发过程中需要做这样的划分(平台部分和应用部分)
2. 怎么划分两部分的界线。
3. 怎么才能最有效的结合两部分,有没有一套完整统一的方法。
4. 两部分各自的实现问题。

其中第4个问题是我们千百本书里面介绍有的。要这样,最近一直在想这个问题:

转一断关于面向对象局限性的讨论,不错!

Soul:我在写书讨论“面向对象的局限性”

我 :En.这个倒与我的意见一致。哈哈哈。

我 :“绝对可以用面向过程的方法来实现任意复杂的系统。要知道,航天飞机也是在面向过程的时代上的天。但是,为了使一切变得不是那么复杂,还是出现了‘面向对象程序设计’的方法。”

我 :——哈,我那本书里,在“面向对象”一部分前的引文中。就是这样写的。

Soul:现在的程序是按照冯。诺伊曼的第一种方案做的,本来就是顺序的,而不是同步的。CPU怎么说都是一条指令一条指令执行的。

我 :面向过程是对“流程”、“结构”和“编程方法”的高度概括。而面向对象本身只解决了“结构”和“编程方法”的问题,而并没有对“流程”加以改造。

Soul:确实如此。确实如此。

我 :对流程进一步概括的,是“事件驱动”程序模型。而这个模型不是OO提出的,而是Windows的消息系统内置的。所以,现在很多人迷惑于“对象”和“事件”,试图通过OO来解决一切的想法原本就是很可笑的。

Soul:我先停下来,和你讨论这个问题,顺便补充到书里去。

我 :如果要了解事件驱动的本质,就应该追溯到Windows内核。这样就涉及到线程、进程和窗体消息系统这些与OO无关的内容。所以,整个RAD的编程模型是OO与OS一起构建的。现在很多的开发人员只知其OO的外表,而看不到OS的内核,所以也就总是难以提高。

Soul:OO里面我觉得事件的概念是很牵强的,因为真正的对象之间是相互作用,就好像作用力和反作用力,不会有个“顺序”的延时。

我 :应该留意到,整个的“事件”模型都是以“记录”和“消息”的方式来传递的。也就是说,事件模型停留在“面向过程”编程时代使用的“数据结构”的层面上。因此,也就不难明白,使用/不使用OO都能写Windows程序。

我 :因为流程还是在“面向过程”时代。

Soul:所以所谓的面向对象的事件还是“顺序”的。所以我们经常要考虑一个事件发生后对其他过程的影响,所以面向对象现在而言是牵强的。

我 :如果你深入OS来看SEH,来看Messages,就知道这些东西原本就不是为了“面向对象”而准备的。面向对象封装了这些,却无法改造它们的流程和内核。因为OO的抽象层面并不是这个。

我 :事件的连续性并不是某种编程方法或者程序逻辑结构所决定的。正如你前面所说的,那是CPU决定的事。

Soul:比如条件选择,其实也可以用一种对象来实现,而事实没有。这个是因为cpu的特性和面向对象太麻烦。

我 :可能,将CPU做成面向对象的可能还是比较难于想象和理解。所以MS才启动.NET Framework。我不认为.NET在面向对象方法上有什么超越,也不认为它的FCL库会有什么奇特的地方。——除了它们足够庞大。但是我认为,如果有一天OS也是用.NET Framework来编写的,OS一级的消息系统、异常机制、线程机制等等都是.NET的,都是面向对象的。那么,在这个基础上,将“事件驱动”并入OO层面的模型,才有可能。

Soul:所以我发觉面向对象的思维第一不可能彻底,第二只能用在总体分析层上。在很多时候,实质上我们只是把一个顺序的流程折叠成对象。

我 :倒也不是不可能彻底。有绝对OO的模型,这样的模型我见过。哈哈~~但说实在的,我觉得小应用用“绝对OO”的方式来编写,有失“应用”的本意。我们做东西只是要“用”,而不是研究它用的是什么模型。所以,“Hello World”也用OO方式实现,原本就只是出现在教科书中的Sample罢了。哈哈。

Soul:还有不可能用彻底的面向对象方法来表达世界。 因为这个世界不是面向对象的。 是关系网络图,面向对象只是树,只能片面的表达世界。所以很多时候面向对象去解决问题会非常痛苦。所以编程退到数据结构更合理,哈哈。

我 :如果内存是“层状存取”的,那么我们的“数据结构”就可以基于多层来形成“多层数据结构”体系。如果内存是“树状存取”的,那么我们当然可以用“树”的方式来存取。——可惜我们只有顺序存取的内存。

我 :程序=数据+算法 ——这个是面向过程时代的事。 程序=数据+算法+方法 ——在OO时代,我们看到了事件驱动和模型驱动,所以出现了“方法”问题。

Soul:我的经验是:总体结构->面向对象,关系->数据结构,实现->算法

Soul:看来我们对面向对象的认识还是比较一致的。


思绪如脱缰的野马,本来是想分析一下过去的代码,看看有什么发现没有。最后变成了“漫谈”,索性“发散思维”一把。

一直在思考和实践嵌入式编程方面的问题,“前辈”们给出的忠告是:一定要“积累”,针对各种具体问题,大家分享了自己多年的经验,总结起来主要有一下内容:

    1. 编程技巧:可移植性问题、模块化,C语言问题等等。
    2. 功能实现:菜单的实现问题、GUI
    3. 工具的使用:keil,gcc,uml等。
    4. 外设驱动:显示屏,can,输入设备等。
    5. 功能模块:加密,校验,滤波等等。
    6. 处理器相关:中断处理技巧,寄存器操作,定时器等等。

其实,在我看来这个领域的不规范才是最大的问题,正如“抽象”是软件实践最有用的利器一样,只有积累没有总结和提炼便无法深入一样,因为事情是做不完的,这个行业涉及领域越来越多。而我们自己又往往把我们自己在mcu上面的工作特殊化和边缘化了:技巧的东西给过分夸大,平台被过分依赖,代码越来越有“个性”。将mcu和大部分“通用处理器”(暂时这样称呼吧,其实除了PC部分长期被Intel的x86垄断很久意外,世界上有多少种CPU架构啊,他们都可以被称为“非通用”的)对立起来,然后说我是做“51”的,他是做“arm"的。
      
其实我们做嵌入式遇到的上面大部分问题其实都是之前已经解决的问题,我们完全可以站在巨人的肩上,在这个基础上考虑更深层次的问题,只要别人已经解决过的问题,我们何不消化利用起来而自己从新总结呢?也不奇怪,我们的教科书中单片机就是讲一下cpu外设,汇编的基本操作,C语言给几个简单例子,仅此而已。工作中忙于应付老板的催促谁还有工夫去考虑,去学习,去借鉴,去整理? 站在巨人的肩上不是简单拿他的代码过来用用就算了,不是在论坛”跪求“某某问题紧急问题如何解决,得之而后就算了。要看这个问题解决了,是不是类似的问题都能这样做;我还能拿这样的方式去解决什么问题;这样做还有什么不足的地方,我该如何改进它等等。久而久之才能比别人做得更好,如果把创造性的想法加入进来,或许你也会创造奇迹。所谓规范化就是从开发过程的每个细节都先要吸收现成的,比较它已经被总结为”规范“,然后去改变和创造,用你的新“规范”去征服问题,征服别人。当一个需求摆正面前的时候,我们如何下手?如何分析问题,给出系统准确的定位,如何去设计规划,如何去实施,如何测试并改进。特殊需求如何满足?等等。
      
所以说,规范才是根本的。上面5类问题其实前人都总结过很好的解决方法,一套行之有效的方法也有,我们何不拿过来用?

END

本文系21ic资深网友keer_zu编写


推荐阅读

STC16是开发者的噩梦?

少写点if-else吧,它的效率有多低你知道吗?

早期MCU芯片是怎么加密的?


→点关注,不迷路←

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

嵌入式ARM

扫描二维码,关注更多精彩内容

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

该系列产品有助于嵌入式设计人员在更广泛的系统中轻松实现USB功能

关键字: 单片机 嵌入式设计 USB

支持高达48V@5A的PD受电模式,达到目前USB PD最高标准。

关键字: 嵌入式 开发板

【2024年4月8日,德国慕尼黑讯】低碳化和数字化是当今时代人们面临的两大核心挑战,人类社会需要依靠创新和先进的技术,才能破除挑战、推动转型进程。在德国纽伦堡举办的2024国际嵌入式展(Embedded World 20...

关键字: 半导体 微控制器 嵌入式

TDK 株式会社(TSE:6762)进一步扩充 Micronas 嵌入式电机控制器系列 HVC 5x,完全集成电机控制器与 HVC-5222D 和 HVC-5422D,以驱动小型有刷(BDC)、无刷(BLDC)或步进电机...

关键字: 嵌入式 电机控制器 内存

单片机编程语言是程序员与微控制器进行交流的桥梁,它们构成了单片机系统的软件开发基石,决定着如何有效、高效地控制和管理单片机的各项资源。随着微控制器技术的不断发展,针对不同应用场景的需求,形成了丰富多样的编程语言体系。本文...

关键字: 单片机 微控制器

单片机,全称为“单片微型计算机”或“微控制器”(Microcontroller Unit,简称MCU),是一种高度集成化的电子器件,它是现代科技领域的关键组件,尤其在自动化控制、物联网、消费电子、汽车电子、工业控制等领域...

关键字: 单片机 MCU

STM32是由意法半导体公司(STMicroelectronics)推出的基于ARM Cortex-M内核的32位微控制器系列,以其高性能、低功耗、丰富的外设接口和强大的生态系统深受广大嵌入式开发者喜爱。本文将详细介绍S...

关键字: STM32 单片机

嵌入式开发作为信息技术领域的重要分支,其涉及的语言种类繁多,各具特色。这些语言的选择取决于目标平台的特性、性能需求、开发者的熟练程度以及项目的具体要求。本文将详细介绍几种常见的嵌入式开发语言,包括C语言、C++、汇编语言...

关键字: 嵌入式开发 C语言

嵌入式开发作为信息技术领域的重要分支,在当今智能化社会中的地位日益显著。它不仅在日常生活中的消费电子产品、工业自动化、汽车电子、航空航天等诸多领域发挥着不可或缺的作用,而且随着物联网、大数据、人工智能技术的发展,嵌入式开...

关键字: 嵌入式 信息技术

在当前的科技浪潮中,单片机作为嵌入式系统的重要组成部分,正以其强大的功能和广泛的应用领域受到越来越多行业的青睐。在众多单片机中,W79E2051以其卓越的性能和稳定的工作特性,成为市场上的明星产品。本文将深入探讨W79E...

关键字: 单片机 w79e2051单片机
关闭
关闭