为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

EMS-模块化编程的分层设计经验

2011-10-28 37页 doc 522KB 32阅读

用户头像

is_758931

暂无简介

举报
EMS-模块化编程的分层设计经验模块化编程的分层设计经验 模块化编程的分层设计经验 操作要点: 1、每一层直接对下一层操作,尽量避免交叉调用或越级调用 2、某些器件会把硬件驱动层合并成一个文件时,则归于较高的层 3、相同功能的外部函数尽量一致,尽量保证通用性 4、对于初次编程的模块,要严格保证中间各层的正确性好处: 1、对于后期维护扩展,只需修改应用层和物理层,根据需要扩展功能层 2、一个新项目只需把要用到的文件加入工程,简单修改调试就出来了 3、随着模块的不断积累,新的项目将越来越容易完成,后期的维护扩展也变得非常简单了 4、对于C语言编程,只需简单修改物...
EMS-模块化编程的分层设计经验
模块化编程的分层设计经验 模块化编程的分层设计经验 操作要点: 1、每一层直接对下一层操作,尽量避免交叉调用或越级调用 2、某些器件会把硬件驱动层合并成一个文件时,则归于较高的层 3、相同功能的外部函数尽量一致,尽量保证通用性 4、对于初次编程的模块,要严格保证中间各层的正确性好处: 1、对于后期维护扩展,只需修改应用层和物理层,根据需要扩展功能层 2、一个新项目只需把要用到的文件加入工程,简单修改调试就出来了 3、随着模块的不断积累,新的项目将越来越容易完成,后期的维护扩展也变得非常简单了 4、对于C语言编程,只需简单修改物理层就可完成不同单片机间的移植一般分为以下几层:    ---应用层--面向用户 软|     ↓ 件|---层--现成的协议栈、软件包、库,大多是移植,不自己写,如FAT、TCPIP、OS、GAME等 相|     ↓ 关|     ↓    ---功能层--实现器件无关性,实现器件的各种功能扩展和器件通用性处理,如LCD的线、圆、矩形等功能,如EEPROM的块写,自己的print 硬|     ↓      件|---器件层--实现硬件无关性,保证IO无关性,只提供器件的基本功能,如字节读写、点 驱|     ↓ 动 ---物理层--IO相关,直接操作硬件,实现硬件连接的多种 对应文件举例1:    ---应用层--面向用户的主程序 软|     ↓ 件|---协议层--如FAT、TCPIP、OS等现成的协议栈、算法、游戏等 相|     ↓ 关|     ↓    ---功能层--如文件lcd.c;led.c;eeprom.c;time.c;ir.c;keybord.c;harddisk.c;引出LCD的线、圆、矩形、填充等功能 硬|     ↓             ↓ 件|---器件层--文件lcd61202.c;lcd1520.c;lcd6963.c;lcd133x.c;lcd44780.c;lcd162x.c;lcd856x.c或者lcd1602.c;lcd12864.c;lcd320240.c等,引出基本的初始化、定位、写点、写字节函数 驱|     ↓             ↓ 动 ---物理层--文件lcd61202_io.c;lcd61202_bus.c;引出器件的基本读写函数 对应文件应用举例2:    ---应用层--面向用户的主程序 软|     ↓ 件|---协议层--如FAT、TCPIP、OS等现成的协议栈、算法、游戏等 相|     ↓ 关|     ↓    ---功能层--如文件lcd.c;led.c;eeprom.c;time.c;ir.c;keybord.c;harddisk.c;如EEPROM的块写统一化 硬|     ↓                         ↓ 件|---器件层--文件ee24xx.c;ee93xx.c;ee_sdcard.c;ee29xx.c;ee28f.c;ee39xx.c;等 驱|     ↓             ↓ 动 ---物理层--文件bus_i2c.c;bus_spi.c等 一个大的单片机程序往往包含很多模块,我是这样组织的 1。每一个C源文件都要建立一个与之名字一样的H文件,里面仅仅包括该C文件的函数的声明,其他的什么也不会有,比如变量的定义啊等等不应该有。 2。建立一个所有的文件都要共同使用的头文件,里面当然就是单片机的管脚使用的定义,还有里面放那些需要的KEIL系统的头文件,比如 #include,#include等等,把这个文件命名为common.h, 或者干脆就叫main.h 3,每个C源文件应该包含自己的头文件以及那个共同的使用的头文件,里面还放自己本文件内部使用的全局变量或者以extern定义的全局变量 4。主文件main.c里面包含所有的头文件包括那个共同使用的文件,main.c里面的函数可以再做一个头文件,也可以直接放在文件的开头部分声明就可以了,里面一般还有中断服务程序也放在main.c里面 5。对于那些贯穿整个工程的变量,可以放在那个共同的使用的头文件里面,也可以用extern关键字在某个C源文件里面定义,哪个文件要使用就重复定义一下 6.建立工程的时候,只要把C源文件加到工程中,把H文件直接放到相应的目录下面就可以了,不需要加到工程里。 单片机系统模块化编程的一些想法 51核类型单片机是目前应用较为广泛的一款MCU,编写单片机程序常常成为嵌入式软件开发软件入门级的训练。一般而言,51程序代码量少,考虑模块化程序相对较少。一种常规做法就是主程序采用while循环,再者通过中断中设置一些标志位;笔者在51单片机程序开发过程,发现公司的单片机程序更新很快,基本每个人都要修改一点,一段时间后原有代码想法都很难找到。还有一种是移植操作系统后,然后进行代码规范化,比如移植UCOS-ii等嵌入式操作系统,但是往往代码量增加很快,对存储容量本来就少的51单片机有较大的压力。 51模块化程序设计的最重要问题,笔者认为就是找到一种合理的程序结构,而且它能胜任实际的51单片机程序开发。考虑到文中前面提到的问题,笔者主要针对第一种主程序while循环结构进行修改。首先增加任务结构体定义,其中函数指针pFun指向实际的任务函数,nDelay表示延时时间,以ms为单位,以下涉及时间都一样的。而nRunme表示运行的次数,nPeriod表示运行的时间周期。 … struct _TASK_DEFINE { void (code *pTask)(void); // 指向实际任务函数 UINT16 nDelay; // 延时时间 UBYTE8 nRunme; // 运行次数 UINT16 nPeriod; // 运行周期 } S_TASK , *pS_TASK; … 系统中设定全局的任务列表变量 S_TASK SCH_TASK_G[TASK_TOTAL_NUM]; 其中TASK_TOTAL_NUM为系统设定的任务最大数量。在进入while主循环前,需要添加相应的任务,采用函数 SCH_ADD_TASK(…),后面再详细阐述。系统的主循环采用遍历的,实现实际任务的运行。 … while(1) { SCH_DISPATCH_TASK(); /* 遍历实现任务切换 */ /*51系统进入空闲模式,定时中断能唤醒*/ SCH_Goto_Sleep(); } … SCH_DISPATCH_TASK遍历函数的主要实现代码如下, … UBYTE nIndex; for(nIndex=0; nIndex < TASK_TOTAL_NUM; nIndex++) { if( (SCH_TASK_G+nIndex)-> nRunme > 0) { (*(SCH_TASK_G+nIndex)->pTask)(); // 运行实际的任务 (SCH_TASK_G+nIndex)-> nRunme--; // 运行次数减1 if((SCH_TASK_G+nIndex)-> nPeriod == 0) /* 执行一次型任务后删除之 */ { SCH_DEL_TASK(nIndex); } } … 定时器实现1ms定时中断,在中断中进行任务刷新(SCH_UPDATE_TASK函数),这也是实现系统结构关键一步。定时器可以采用定时Timer0,有的52型单片机也可以采用定时器Timer2,总之实现1ms时间的定时中断。 SCH_UPDATE_TASK()的主要实现代码如下 … UBYTE8 nIndex; for(nIndex=0; nIndex < TASK_TOTAL_NUM; nIndex++) { if( (SCH_TASK_G+nIndex)-> nDelay == 0) { (SCH_TASK_G+nIndex)-> nRunme++; // 运行次数加1 /* 获得实际的时间周期 */ if((SCH_TASK_G+nIndex)-> nPeriod > 0) { (SCH_TASK_G+nIndex)-> nDelay = (SCH_TASK_G+nIndex)-> nPeriod; } } else { (SCH_TASK_G+nIndex)-> nDelay--; } } … 在进行主程序while循环前,必须要添加相应的任务函数SCH_ADD_TASK,其主要的实现代码如下: … UBYTE8 SCH_ADD_TASK( void (code *pFun)(void), const UINT16 tDelay, const UINT16 tPeriod) { UBYTE8 nIndex; nIndex=0; while( (SCH_TASK_G+nIndex)-> pTask != NULL ) && (nIndex < TASK_TOTAL_NUM) ) { nIndex ++ ; } if( nIndex == TASK_TOTAL_NUM) { return TASK_OVER_FLOW; } /* 增加任务到列表中 */ SCH_TASK_G[nIndex] ->pTask = pFun; SCH_TASK_G[nIndex]->nDelay = tDelay; SCH_TASK_G[nIndex]->nRunme = 0; SCH_TASK_G[nIndex]->nPeriod = tPeriod; return nIndex; } … 主体代码基本实现,51系统开发主要的工作就是增加一个任务函数,在实际的任务实现相应的功能,这样构成的单片机系统就比较好控制,维护代码也很简单。 当然还有就是任务函数的执行时间必须控制1ms以内,即是每 1ms时间标度中要执行的任务时间不超过1ms。如果执行时间较长的任务,总有一些办法对其划分为较小的若干个小任务。 笔者在实际开发中也设想过一个问题,假定A任务每2ms执行一次,执行需要的时间为0.5ms;而B任务每4ms执行一次,执行所需的时间为0.5ms。如果两个任务运行在同一个时标(1ms)中,就可能导致运行单个时标运行任务超过1ms的限制;由于4ms的间隔也是2ms延时的整数倍关系,执行完全有可能。一种常见的解决方法是加大定时中断时间的时标,把1ms修改成2ms,但是同时系统的时间敏感度减少了。笔者想到一种方法,设置 两个任务结构体中延时nDelay不同,A任务延时初值0,而B任务延时初值为1;这样实际A任务执行时间标度为2*n, 而B为4*m+1(n, m为正整数),而2*n != 4*m+1(奇偶数),从而使A与B任务永远不可能同时在一个时标(1ms)中执行。 以上是在51单片机开发模块化设计的一些想法,在几个实际项目开发也得到较好的应用,当然可能还会有一些没有考虑的问题,欢迎各位提出更好的建议! 模块化设计原则:高内聚 第一步:创建头文件(源文件与头文件同名如delay.c与delay.h) 第二步:防止重复包含处理     在.h文件里加入 #ifndefXXXX #defineXXXX ....... #endif 例如: #ifndef_DELAY_H__ #define_DELAY_H__ ....... #endif 第三步: 代码封装(内部调用【.h封装外部调用的部分】)     封装成函数或者宏定义以便提高可读性和可修改文件,尽量少用或者不用全局变量 第四步:使用源文件 .c文件添加到文件中 模块化编程实例: delay.h文件 #ifndef __DELAY_H__ #define __DELAY_H__ #define uchar unsingned char #define uint unsigned int void delay50us(uint t); void delay50ms(uint t); #endif delay.c文件 #include #include"模块化编程实例.h" void delayus(uint t)//延时函数  { uint j;   for(;t>0;t--)   for(j=6245;j>0;j--);  }  void delayms(uint t)//延时函数  { uint j;   for(;t>0;t--)   for(j=6245;j>0;j--);  } 数码管.h文件 #ifndef __DELAY_H__ #define __DELAY_H__ #define"模块化编程实例.h" #define uint unsigned int void dispaytable(uchar *p); void dispayt(uchar num0,uchar num1,uchar num2,uchar num3,uchar num4,uchar num5,uchar num6,uchar num7,); #endif 数码管.c文件 #include"数码管.h" #include"模块化编程实例.h" unsigned char code smg_du[]={0xfe,0xfd,0xfb,0xf7,0xef, 0xbf,0x7f}; unsigned char code smg_we[]={0x00,0x00,0x3e,0x41, 0x41,0x41,0x3e,0x00}; void display_table(uchar *p) {   uchar i;  foe(i=0;i<8;i++)  {    P1=smg_du[*p];    P2=smg_we[i];    delay_50us(20);  }         } void display(uchar num0,uchar num1,uchar num2,uchar num3,uchar num4,uchar num5,uchar num6,uchar num7,) {   P1=smg_du[mun0];    P2=smg_we[0];    delay_50us(20);     P1=smg_du[mun1];    P2=smg_we[1];    delay_50us(20);     P1=smg_du[mun2];    P2=smg_we[2];    delay_50us(20);     P1=smg_du[mun3];    P2=smg_we[3];    delay_50us(20);     P1=smg_du[mun4];    P2=smg_we[4];    delay_50us(20);     P1=smg_du[mun5];    P2=smg_we[5];    delay_50us(20);     P1=smg_du[mun6];    P2=smg_we[6];    delay_50us(20);     P1=smg_du[mun7];    P2=smg_we[7];    delay_50us(20); } mian.c文件 #include"数码管.h" #include #include"模块化编程实例.h" sbit rst=P3^6; unsigned char table[]={2,3,4,5,6,7,8,9}; void main() {  rst=0;  while(1)  { display_tale(table);  } } C语言高效编程   编写高效简洁的C语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方请各位指教。 第1招:以空间换时间   计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决问题的第1招——以空间换时间。 例如:字符串的赋值。 方法A,通常的办法: #define LEN 32 char string1 [LEN]; memset (string1,0,LEN); strcpy (string1,“This is a example!!”); 方法B: const char string2[LEN] =“This is a example!”; char * cp; cp = string2 ; (使用的时候可以直接用指针来操作。)   从上面的例子可以看出,A和B的效率是不能比的。在同样的存储空间下,B直接使用指针就可以操作了,而A需要调用两个字符函数才能完成。B的缺点在于灵活性没有A好。在需要频繁更改一个字符串内容的时候,A具有更好的灵活性;如果采用方法B,则需要预存许多字符串,虽然占用了大量的内存,但是获得了程序执行的高效率。   如果系统的实时性要求很高,内存还有一些,那我推荐你使用该招数。   该招数的变招——使用宏函数而不是函数。举例如下: 方法C: #define bwMCDR2_ADDRESS 4 #define bsMCDR2_ADDRESS 17 int BIT_MASK(int __bf) { return ((1U << (bw ## __bf)) - 1) << (bs ## __bf); } void SET_BITS(int __dst, int __bf, int __val) { __dst = ((__dst) & ~(BIT_MASK(__bf))) | \ (((__val) << (bs ## __bf)) & (BIT_MASK(__bf)))) } SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber); 方法D: #define bwMCDR2_ADDRESS 4 #define bsMCDR2_ADDRESS 17 #define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS) #define BIT_MASK(__bf) (((1U << (bw ## __bf)) - 1) << (bs ## __bf)) #define SET_BITS(__dst, __bf, __val) \ ((__dst) = ((__dst) & ~(BIT_MASK(__bf))) | \ (((__val) << (bs ## __bf)) & (BIT_MASK(__bf)))) SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber);   函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU时间。而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。   D方法是我看到的最好的置位操作函数,是ARM公司源码的一部分,在短短的三行内实现了很多功能,几乎涵盖了所有的位操作功能。C方法是其变体,其中滋味还需大家仔细体会。 第2招:数学方法解决问题   现在我们演绎高效C语言编写的第二招——采用数学方法来解决问题。   数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序的执行效率有数量级的提高。 举例如下,求 1~100的和。 方法E int I , j; for (I = 1 ;I<=100; I ++){ j += I; } 方法F int I; I = (100 * (1+100)) / 2   这个例子是我印象最深的一个数学用例,是我的计算机启蒙老师考我的。当时我只有小学三年级,可惜我当时不知道用公式 N×(N+1)/ 2 来解决这个问题。方法E循环了100次才解决问题,也就是说最少用了100个赋值,100个判断,200个加法(I和j);而方法F仅仅用了1个加法,1次乘法,1次除法。效果自然不言而喻。所以,现在我在编程序的时候,更多的是动脑筋找规律,最大限度地发挥数学的威力来提高程序运行的效率。 第3招:使用位操作   实现高效的C语言编写的第三招——使用位操作,减少除法和取模的运算。   在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例如下: 方法G int I,J; I = 257 /8; J = 456 % 32; 方法H int I,J; I = 257 >>3; J = 456 - (456 >> 4 << 4);   在字面上好像H比G麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法G调用了基本的取模函数和除法函数,既有函数调用,还有很多汇编代码和寄存器参与运算;而方法H则仅仅是几句相关的汇编,代码更简洁,效率更高。当然,由于编译器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 来看,效率的差距还是不小。相关汇编代码就不在这里列举了。 运用这招需要注意的是,因为CPU的不同而产生的问题。比如说,在PC上用这招编写的程序,并在PC上调试通过,在移植到一个16位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。 第4招:汇编嵌入   高效C语言编程的必杀技,第四招——嵌入汇编。   “在熟悉汇编语言的人眼里,C语言编写的程序都是垃圾”。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方法 ——嵌入汇编,混合编程。   举例如下,将数组一赋值给数组二,要求每一字节都相符。 char string1[1024],string2[1024]; 方法I int I; for (I =0 ;I<1024;I++) *(string2 + I) = *(string1 + I) 方法J #ifdef _PC_ int I; for (I =0 ;I<1024;I++) *(string2 + I) = *(string1 + I); #else #ifdef _ARM_ __asm { MOV R0,string1 MOV R1,string2 MOV R2,#0 loop: LDMIA R0!, [R3-R11] STMIA R1!, [R3-R11] ADD R2,R2,#8 CMP R2, #400 BNE loop } #endi   方法I是最常见的方法,使用了1024次循环;方法J则根据平台不同做了区分,在ARM平台下,用嵌入汇编仅用128次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为0的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于LCD数据的拷贝过程。根据不同的CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。   虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在不同平台移植的过程中,卧虎藏龙,险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才可以采用。切记,切记。   使用C语言进行高效率编程,我的体会仅此而已。在此以本文抛砖引玉,还请各位高手共同切磋。希望各位能给出更好的方法,大家一起提高我们的编程技巧。 C语言嵌入式系统编程修炼之软件架构 模块划分   模块划分的"划"是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。C语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,牛顿定律遇到了相对论),C语言模块化程序设计需理解如下概念:   (1) 模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明;   (2) 某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明;   (3) 模块内的函数和全局变量需在.c文件开头冠以static关键字声明;   (4) 永远不要在.h文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如: /*module1.h*/ int a = 5; /* 在模块1的.h文件中定义int a */ /*module1 .c*/ #include "module1.h" /*在模块1中包含模块1的.h文件 /*module2 .c*/ #include "module1.h" /* 在模块2中包含模块1的.h文件 /*module3 .c*/ #include "module1.h" /* 在模块3中包含模块1的.h文*/ 以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,这个世界上从来不需要这样的程序。正确的做法是: /*module1.h*/ extern int a; /* 在模块1的.h文件中声明int a */ /*module1 .c*/ #include "module1.h" /* 在模块1中包含模块1的.h文件 int a = 5; /* 在模块1的.c文件中定义int a */ /*module2 .c*/ #include "module1.h" /* 在模块2中包含模块1的.h文件 /*module3 .c*/ #include "module1.h" /* 在模块3中包含模块1的.h文件 这样如果模块1、2、3操作a的话,对应的是同一片内存单元。   一个嵌入式系统通常包括两类模块:   (1)硬件驱动模块,一种特定硬件对应一个模块;   (2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。   多任务还是单任务   所谓"单任务系统"是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并行(微观上可能串行)地"同时"执行多个任务。   多任务的并发执行通常依赖于一个多任务操作系统(OS),多任务OS的核心是系统调度器,它使用任务控制块(TCB)来管理任务调度功能。TCB包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务被激活时,要用到这些信息。此外,TCB还被用来存放任务的"上下文"(context)。任务的上下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。当发生任务切换时,当前运行的任务的上下文被存入TCB,并将要被执行的任务的上下文从它的TCB中取出,放入各个寄存器中。  嵌入式多任务OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遥不可及的神坛之物,我们可以用不到1000行代码实现一个针对80186处理器的功能最简单的OS内核,作者正准备进行此项工作,希望能将心得贡献给大家。   究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。 单任务程序典型架构   (1)从CPU复位时的指定地址开始执行;   (2)跳转至汇编代码startup处执行;   (3)跳转至用户主程序main执行,在main中完成:   a.初试化各硬件设备;   b.初始化各软件模块;   c.进入死循环(无限循环),调用各模块的处理函数 用户主程序和各模块的处理函数都以C语言完成。用户主程序最后都进入了一个死循环, 其首选方案是: while(1) { } 有的程序员这样写: for(;;) { } 这个语法没有确切表达代码的含义,我们从for(;;)看不出什么,只有弄明白for(;;)在C语言中意味着无条件循环才明白其意。 下面是几个"著名"的死循环:   (1)操作系统是死循环;   (2)WIN32程序是死循环;   (3)嵌入式系统软件是死循环;   (4)多线程程序的线程处理函数是死循环。   你可能会辩驳,大声说:"凡事都不是绝对的,2、3、4都可以不是死循环"。Yes,you are right,但是你得不到鲜花和掌声。实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要OS杀死它的WIN32程序,不需要一个刚开始RUN就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。有时候,过于严谨制造的不是便利而是麻烦。君不见,五层的TCP/IP协议栈超越严谨的ISO/OSI七层协议栈大行其道成为事实上的标准? 经常有网友讨论: printf("%d,%d",++i,i++); /* 输出是什么?*/ c = a+++b; /* c=? */ 等类似问题。面对这些问题,我们只能发出由衷的感慨:世界上还有很多有意义的事情等着我们去消化摄入的食物。   实际上,嵌入式系统要运行到世界末日。   中断服务程序   中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断。许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序(ISR),类似于__interrupt、#program interrupt等。当一个函数被定义为ISR的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。   中断服务程序需要满足如下要求:   (1)不能返回值;   (2)不能向ISR传递参数;   (3) ISR应该尽可能的短小精悍;   (4) printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。   在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。 /* 存放中断的队列 */ typedef struct tagIntQueue {  int intType; /* 中断类型 */  struct tagIntQueue *next; }IntQueue; IntQueue lpIntQueueHead; __interrupt ISRexample () {  int intType;  intType = GetSystemType();  QueueAddTail(lpIntQueueHead, intType); /* 在队列尾加入新的中断 */ } 在主程序循环中判断是否有中断: While(1) {  If( !IsIntQueueEmpty() )  {   intType = GetFirstInt();   switch(intType) /* 是不是很象WIN32程序的消息解析函数? */   {    /* 对,我们的中断类型解析很类似于消息驱动 */    case xxx: /* 我们称其为"中断驱动"吧? */     …     break;    case xxx:     …     break;    …   }  } } 按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。 硬件驱动模块   一个硬件驱动模块通常应包括如下函数:   (1)中断服务程序ISR   (2)硬件初始化   a.修改寄存器,设置硬件参数(如UART应设置其波特率,AD/DA设备应设置其采样速率等);   b.将中断服务程序入口地址写入中断向量表: /* 设置中断向量表 */ m_myPtr = make_far_pointer(0l); /* 返回void far型指针void far * */ m_myPtr += ITYPE_UART; /* ITYPE_UART: uart中断服务程序 */ /* 相对于中断向量表首地址的偏移 */ *m_myPtr = &UART _Isr; /* UART _Isr:UART的中断服务程序 */ (3)设置CPU针对该硬件的控制线   a.如果控制线可作PIO(可编程I/O)和控制信号用,则设置CPU内部对应寄存器使其作为控制信号;   b.设置CPU内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。   (4)提供一系列针对该设备的操作接口函数。例如,对于LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。   C的面向对象化   在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。而C语言中的struct仅仅是数据的集合,我们可以利用函数指针将struct模拟为一个包含数据和操作的"类"。下面的C程序模拟了一个最简单的"类": #ifndef C_Class #define C_Class struct #endif C_Class A {  C_Class A *A_this; /* this指针 */  void (*Foo)(C_Class A *A_this); /* 行为:函数指针 */  int a; /* 数据 */  int b; }; 我们可以利用C语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。C模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。   总结   本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。 setjmp构建简单协作式多任务系统 本文介绍的是利用标准C语言setjmp库函数实现的具备此特点的协作式多任务系统。从本质上讲,实时多任务操作系统应该具备按照优先级抢占调度的内核。然而,在实际应用中,抢中式的多任务某种程序上带来了用户程序设计时数据保护的困难,并且,具备抢占功能的多任务内核设计时困难也比较多,这会增加操作系统自身的代码,也使它在小资源单片机系统中应用较少;而协作多任务系统的调度只在用户指定的时机发生,这会大大简化内核和用户系统的设计,尤其本文实现的系统通过条件查询来放弃CPU,既符合传统单片机程序设计的思维,又带来了多任务、模块化、可重入的编程便利。 Setjmp是标准C语言库函数的组成部分,它可以实现程序执行中的远程转操作。具体来说,它可以在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到setjmp的下一条语句执行。实际上,setjmp函数将发生调用处的局部环境保存在一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放(函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。我们的系统中就是分析了setjmp标准库函数的特点,以简单的方式实现了协作式多任务。 1 演示程序 为了便于理解,首先给出多任务演示程序的源代码。这个程序演示了协作式多任务切换、任务的动态生成、多任务共用代码等功能,一共使用了init_coos初始化根任务(也就是C语言main函数)、creat_task创建新任务和WAITFOR查询条件这3个基本的系统调用。由于面向嵌入式系统,因而程序不会中止并且运行中也没有进行任何输出,需要借助适合的调试工具来理解多任务系统的运行。 example.c文件清单: #include #include“co-os.h” void tskfunc1(int argc,void *argv); void tskfunc2(int argc,void *argv); void subfunc(void); volatile int cnt,test; int main(void){ int i; init_coos(400); creat_tsk(tskfunc1,12,NULL,400); creat_tsk(tskfunc2,0,NULL,400); i=0; while(1){ WAITFOR(cnt= =8); while(i++argc); test=0x55; /*使用函数调用在子程序中测试WAITFOR*/ subfunc(); while(i++15); cnt=0; } } void subfunc(void){ int i; WAITFOR(cnt<5); for(i=0;i<++)test=0x10*i; } 2 内核构成 内核包括一个供外部用户程序包含的头文件(co-os.h)和具体实现的源文件(co-os.c),它们提供了演示程序中用到的3个系统调用。内核的实现代码假定了CPU堆栈是向下增长的,并且通过宏来直接操作堆栈指针。以下代码在Microsoft VC6 for x86、Borland C++ Builder 5.5、SDS CrossCode7.0 for 68K和GCC3.2 for AVR四种平台中测试过,只需在co-os.h头文件中定义相应的平台类型即可顺利编译。 (1)co-os.h文件清单 #include /*选择X86_VC6,X86_BC5,AVR_GCC或M68H_SDS.*/ #define X86_VC6 #define MAX_TSK 10 typedef struct { void (*entry)(int argc,void *argv); jmp_buf env; int argc; void *argv; }TVB; extern TCB tcb[MAX_TSK]; -------------------------- extern int task_num,tskid; void init_coos(int mainstk); int creat_tsk(void(*entry)(int argc,void *argv),int argc,void *argv,int stksize); #define WAITFOR(condition)do{ setjmp(tcb[tskid].env); if(!(condition)){ tskid++; if(tskid>=task_num)tskid=0; longijmp(tcb[tskid].env,1); } }while(0)---------------------------------------------- (2)co-os.c文件清单 #include "co-os.h" #if defined(X86_VC6)||defined(X86_BC5) #define SAVE_SP(p) _asm mov p,esp #define RESTORE_SP(p) _asm mov esp,p #elif defined(AVR_GCC) #include #define SAVE_SP(p) p=(int*)SP #define RESTORE_SP(p) SP=(int)p #elif defined(M68K_SDS) #define SAVE_SP(p) asm("MOVE.L A7,{"#p"}") #define RESTORE_SP(p) asm("MOVE.L {"#p"},A7") #endif TCB tcb[MAX_TSK]; Int task_num=1; Int tskid; Static int stktop,oldsp; Void init_coos(int mainstk){ SAVE_SP(stktop); stktop=stktop+sizeof(void(*)(void))/sizeof(int) -(mainstk+sizeof(int)-1)/sizeof(int); } int creat_tsk(void(*entry)(int argc,void *argv), int argc,void *argv,int stksize){ if(task_num>=MAX_TSK)terurn-1; SAVE_SP(oldsp); RESTORE_SP(stktop); If(!setjmp(tcb[task_num].env)){ RESTORE_SP(oldsp); tcb[task_num].entry=entry; tcb[task_num].argc=argc; tcb[task_num].argv=argv; task_num++; stktop-=(stksize+sizeof(int)-1)/sizeof(int); } else tcb[tskid].entry(tcb[tskid].argc,tcb[tskid].argv); return 0; } 3 代码说明 任务代码通过执行setjmp设置本任务下次查询时的返回点,然后在等待条件放弃掉CPU跳转到下一任务的返回点处执行。如此周而复始,让各任务都获得轮转运行的机会,也要求各任务都需要主动通过等待条件的方式放弃掉CPU。系统中除了中断服务程序之外,所有任务都是平等的,都应该遵循同样的规则和其它任务一起协作运行。基本系统中没有设计杀死任务的调用,这要求各任务都应当设计成某种形式的无限循环。 任务中等待的条件可以是任务复杂的表达式或都函数调用,也可以是中断服务程序设置的全局变量(注意加volatile定义)。一般在任务执行时会让下次等待的条件不再满足,避免某个任务一直霸占CPU将系统饿死。在嵌入式软件中还经常会遇到任务定时启动和超时等待在I/O操作上,在我们的系统中可以维护一个时间计数器,只需在适当的地方记录时刻,然后在任务查询条件中判断当前计数器和记录时刻之间的差值就可以了。 内核实现的代码则相当简法。由于主要的保护和恢复任务现场的工作都由C语言标准库setjmp实现了,我们就只需要操纵一下堆栈指针防止不同的任务使用了重叠的局部环境,这个工作在初始化和创建任务的时候通过预定义的两个宏来实现。在init_coos函数中,记录了主任务(main函数)保留mainstk字节堆栈后的新栈顶位置(stktop),然后在每次creat_task时都根据要求为每个任务保留stksize字节的堆栈并重新计算下一个stktop。在creat_task函数中利用了setjmp函数的返回值(直接返回时为0,longjmp跳转返回时非0),使得一方面creat_task能正常回到调用者,又让下次轮转到新任务时能够找到创建时的入口。 co-os.h中定义的WAITFOR使用了一个do{…while(0)实现多语句宏的C语言小技巧,这样能保证在任何情况下WAITFOR都可以如单条语句一样在源程序中使用,需要担心多了或者少了大括弧破坏if/else匹配之类的问题,并且,所有的编译器都会优化掉这个假循环。 为了尽量使程序简单并说明问题,以上代码中没有考虑中断相关的数据保护问题。实际运行的系统中,如果首先写堆栈指针不能一步完成(如AVR这样的8位机),那么,在写操作正在进行的时候绝对不能允许中断;另外,在任务中查询的条件如果和中断有关,那么也必须考虑数据的完整性。 4 性能分析 所有的协作式多任务系统中任务切换时间都和用户代码(是否长期占用CPU)、就绪时刻有关,比标准系统略差的是,我们的简单系统中不支持任务优先级,并且完成一轮查询调度的时间还和任务数目有关。这也是为了达到简单和可移植性目标而不得已作出的牺牲。在各任务间切换查询条件通过C语言标准库函数longjmp实现,一般来讲longjmp函数要从内存中恢复大部分CPU寄存器,执行它也需要若干条指令的时间。 为了面向嵌入式系统应用,任务控制块(TCB)采用静态数组来实现,这样要求预先确定系统的最大任务数(co-os.h中的MAX_TSK)。如果需要,也可以通过环波链表来动态管理任务控制块(TCB),这时可以简单实现任务的动态创建和删除,并且通过指针来访问TCB也要比通过下标(tskid)略快一点。 在某些情况下,如果在中断返回后需要执行某关键任务,可以考虑通过设置“高级中断”的方法来实现。具体地讲,就是在中断返回前改变返回地址到某函数入口(“高级中断服务程序”),同时保留原返回地址到堆栈中,这样在“高级中断服务程序”完成后执行return就又回到了正常的多任务查询。使用“高级中断”时要注意现场保护的衔接,并且这种技巧显然和CPU和体系结构有关。 5 结论 setjmp是标准C语言中用于远程跳转的库函数,利用它可方便实现一个简单易移植的协作式多任务系统。该系统功能完备、编程简单、易于学习,适合一些中小规模的嵌入式软件使用;并且,以此为基础,还可以用一些与平台相关的编程技巧提高其实时性和灵活性。 嵌入式程序设计中C代码的优化 1  引言     嵌入式计算机系统是指完成特定功能的计算机系统,它具有软件代码小,自动化程度高,响应速度快等特点。特别适合于要求实时和多任务的应用体系。由于嵌入式系统受时间、空间和运行环境的严格限制,使得嵌入式系统软件的开发变得非常困难,为了开发出高性能的嵌入式系统,开发语言的选择十分关键。     目前,在嵌入式系统开发过程中可使用的语言很多,其中C和C++应用得最广泛。C++在面向对象、结构化等方面对C进行了改进,但在程序代码容量、执行速度等方面性能不如C语言。C语言既有低级语言的直接控制硬件的能力,又有高级语言的灵活性,在嵌入式系统中得到了最广泛的应用。在嵌入式的系统开发中,出于对低价产品的需求,硬件的设计者需要提供刚好足够的存储器和完成工作的处理能力。所以在嵌入式软件设计的最后一个阶段变成了对代码的优化阶段。     经过本人在嵌入式系统设计过程中的实践,下面介绍几种简单且行之有效的C/C++代码的优化方法。考虑到读者已经学过了C/C++的原理,有一些已经是经验丰富的编程员,本文的叙述将尽可能深入浅出。 2  变量的处理     开始编程时,一般首先遇到的是初始化的问题。建议使用英文单词的缩写代表变量,比如代表高度指针的变量可取名highpoint 或简写为hiptr。用缩写减少字母可加快程序的运行速度并减少占用的内存。另外变量类型选取的范围越小运行速度越快占用的内存越少。比如int型变量比long型变量运行速度越快,但变量类型选取的范围越小算术运算时越容易产生溢出错误。     在声明局部变量的时候可以使用寄存器变量register关键字。这就使得编译器把变量放入寄存器中,而不是在堆栈中,合理使用这种方法可以提高执行速度。函数调用越频繁,提高代码的效率越明显
/
本文档为【EMS-模块化编程的分层设计经验】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
热门搜索

历史搜索

    清空历史搜索