嵌入式系统程序可移植性设计及性能优化.docx
《嵌入式系统程序可移植性设计及性能优化.docx》由会员分享,可在线阅读,更多相关《嵌入式系统程序可移植性设计及性能优化.docx(53页珍藏版)》请在冰豆网上搜索。
嵌入式系统程序可移植性设计及性能优化
嵌入式系统程序可移植性设计及性能优化
【摘要】在嵌入式系统的程序设计中,由于软硬件平台的多变性,对程序的可移植性、可扩充性、可裁减性及可维护性等有更严格的要求。
本文从宏定义设计、数据结构设计及函数设计等方面,简单介绍了可移植性的设计问题。
在嵌入式应用中非常注重代码的时空效率,即产生的代码运行时间和占用的存储空间尽可能少。
程序设计一章介绍了如何提高程序的运行效率的相关技巧。
【关键词】嵌入式,可移植性,可维护,可裁减,宏定义设计,数据结构设计,时空效率
1宏定义设计-1-
1.1为何要采用宏定义?
-1-
1.2宏定义的基本规则-1-
1.3依赖关系定义宏改善移植性-1-
1.4通过偏移量和掩码进行位操作-2-
2数据结构设计-4-
2.1结构体中成员对齐规则-4-
2.1.1自然对界-4-
2.1.2指定对界-4-
2.2合理设计成员顺序-5-
2.2.1减少结构体存储空间-5-
2.2.2填充部分域,避免字节对齐问题-6-
2.2.3字节对齐问题实例-7-
2.3采用位域构造结构体-8-
2.3.1位域设计传输协议-8-
2.3.2位域的可移植性问题-9-
2.3.3位域设计硬件配置字-10-
2.4通过union和struct传递不同格式报文-11-
2.5将相关功能变量封装为结构体-13-
3函数设计-15-
3.1避免过多函数参数,提高调用性能-15-
3.2合理设计模块,减小耦合度-16-
3.3用宏函数提高时间效率-18-
3.3.1宏参数的基本规则-18-
3.3.2宏语句的基本规则-18-
3.3.3宏的副作用-20-
3.4Const修饰输入指针参数-21-
4程序设计-22-
4.1循环转置-22-
4.2减小运算强度-23-
4.2.1位操作实现求余运算-23-
4.2.2用移位实现乘除法运算-23-
4.2.3将循环体内的乘法运算改成循环自加运算-23-
4.3减少不变计算-24-
4.3.1循环内部避免恒定式-24-
4.3.2避免结构体深度访问-25-
4.4减少存储访问指令周期和个数-26-
4.5查表-28-
4.6使用自加、自减指令-28-
4.7根据频率进行case排序-29-
4.8函数指针表替代switch-case-30-
嵌入式系统程序可移植性设计及性能优化之一
――――宏定义设计
【摘要】本节介绍了嵌入式系统程序设计中采用宏定义进行常量定义的必要性。
说明了宏常量定义的基本规则以及如何采用依赖关系定义宏常量来保证其可移植性和裁减性。
最后介绍了如何利用宏定义实现掩码偏移量等来高效的进行位操作。
【关键词】嵌入式,可移植性,宏定义,依赖关系,掩码,偏移量,位操作
1宏定义设计-1-
1.1为何要采用宏定义?
-1-
1.2宏定义的基本规则-1-
1.3依赖关系定义宏改善移植性-1-
1.4通过偏移量和掩码进行位操作-2-
1 宏定义设计
1.1 为何要采用宏定义?
在程序设计过程中,对于经常使用的一些常量,如果将它直接写到程序中去,一旦常量的数值发生变化,就必须逐个找出程序中所有的常量,并逐一进行修改,这样必然会降低程序的可维护性。
因此,应尽量通过预处理命令方式将常量定义为特定的字符,这样常量就有了统一的表现形式,不会出现输入错误导致的不一致性。
另外宏常量意义明确,大大改善了代码的可读性。
只读的变量也可以实现上述宏常量所带来的可移植性、可靠性及可读性等特点,但其要占据存储空间,需要访问内存,相比宏常量的立即数寻址而言效率要低。
在C++中提倡用const只读变量来定义常量,是因为这样可以提供更严格的类型安全检查。
但由于C中const只读变量不能用于某些场合,因此在嵌入式C中仍多数采用宏来定义常量。
1.2 宏定义的基本规则
下面以一个实例来说明宏定义的基本规则,如用预处理指令#define声明一个常量,用以表明1年中有多少秒,不考虑润年
#defineC_SECONDS_PER_YEAR(60*60*24*365)UL
a) 命名风格,为了与普通变量区分开来,宏定义通常全部大写,多个单元之间用下划线隔开;
b) 整个表达式应括起来,若有参数则应将每个参数都括起来,防止替换扩展后可能带来的异常问题;
c) 常量表达式先合并后再替换。
预处理器将为你计算常量表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有运行性能代价的。
d) 为常量添加类型信息。
宏的不足之一在于缺乏类型安全检查,人为的提供类型信息可以有效检查出此类问题。
UL告诉编译器这个常量是无符号长整型数,因此将其赋值给u16型变量会出现告警。
1.3 依赖关系定义宏改善移植性
嵌入式系统程序的最大特点是硬件平台的多变性,因此需要根据具体的应用情况更改大量配置,而这些配置基本都是由宏定义来实现的,放在特定的头文件中,与其他的代码隔离,在一定程度上改善了代码的可移植性。
但有些时候,多个宏定义有严重的依赖关系,增减某个宏会引起其他定义的更改,如何定义这些宏对嵌入式程序的可移植性有很大影响。
A
常量分别定义
#defineC_DD_MODULE_ID_AOM(0x00010101)/*AOM模块ID*/
#defineC_DD_MODULE_ID_RRCM(0x00010102)/*RRCM模块ID*/
#defineC_DD_MODULE_ID_RLC(0x00010103)/*RLC模块ID*/
#defineC_DD_MODULE_ID_TRM(0x00010104)/*TRM模块ID*/
#defineC_DD_MODULE_ID_MCP_MIN(C_DD_MODULE_ID_AOM)
#defineC_DD_MODULE_ID_MCP_MAX(C_DD_MODULE_ID_TRM)
B
依赖定义
#defineC_DD_MODULE_ID_MCP_MIN(0x00010101)/*MCP最小模块ID*/
#defineC_DD_MODULE_ID_AOM(C_DD_MODULE_ID_MCP_MIN)/*AOM模块ID*/
#defineC_DD_MODULE_ID_RRCM(C_DD_MODULE_ID_AOM+1)/*RRCM模块ID*/
#defineC_DD_MODULE_ID_RLC(C_DD_MODULE_ID_RRCM+1)/*RLC模块ID*/
#defineC_DD_MODULE_ID_TRM(C_DD_MODULE_ID_RLC+1)/*TRM模块ID*/
#defineC_DD_MODULE_ID_MCP_MAX(C_DD_MODULE_ID_TRM)/*MCP最大模块ID*/
“A常量分别定义”方式,因为各个宏定义值必须连续,若更改或者删除C_DD_MODULE_ID_AOM,其他的定义基本都受到影响,将严重影响到代码的可扩充性和可裁减性。
“B依赖定义”方式,其原则是:
a) base用常量定义;
b) 第一个定义为base;
c) 其他的在上一个基础上加1;
d) max项即为最后一项。
这样整体改动起来只需要更改base;在中间删除或添加部分项时只需要更改一个上下衔接处即可;添加则比较简单,只需要在原有最后项后添加即可。
这种方式若改动部分定义对其他定义的影响最小,大大改善了代码的可移植性。
1.4 通过偏移量和掩码进行位操作
嵌入式系统经常需要和硬件打交道,而配置硬件寄存器则是系统初始化阶段的重要任务,如何清晰明了的进行配置决定了代码的可读性。
尽管可以使用位域,但位域是不可移植的,因此利用宏定义来解决可移植性问题。
对于每一个待操作相应位来说,应具备以下几个标识:
待操作的位名称B_NAME;
操作位所占据的位宽B_WIDTH_NAME
操作位第一位的偏移量B_SHIFT_NAME
操作位的掩码B_MASK_NAME
以PRI为例进行说明:
#definePRIDD_C64_EDMA_OPT_PRI
#defineC_WIDTH_DD_C64_EDMA_OPT_PRI(3)
#defineC_SHIFT_DD_C64_EDMA_OPT_PRI(29)
可以手动定义对应位的掩码,如下:
#defineC_MASK_DD_C64_EDMA_OPT_PRI(0xe0000000)
但更好的方式是利用位宽和偏移量来自动构成掩码
#defineBIT_MASK(_name)(((1U<#defineC_MASK_DD_C64_EDMA_OPT_PRIBIT_MASK(PRI)
BIT_MASK(PRI)经过宏替换后即为:
(((1U<<(C_WIDTH_DD_C64_EDMA_OPT_PRI))-1)/
<<(C_SHIFT_DD_C64_EDMA_OPT_PRI)
对于每个位的取值也应该用宏定义来表示,这样清晰明确
#defineC_DD_C64_EDMA_OPT_URGENT_PRI(0x0)
#defineC_DD_C64_EDMA_OPT_HIGH_PRI(0x1)
#defineC_DD_C64_EDMA_OPT_MEDIUM_PRI(0x2)
#defineC_DD_C64_EDMA_OPT_LOW_PRI(0x3)
具备了掩码和偏移量即可对各个位进行操作了
#defineSET_BITS(_reg,_name_val)/
((_reg)=((_reg)&~(BIT_MASK(_name)))/
|(((_val)<<(C_SHIFT_##_name))&(BIT_MASK(_name))))
通过如下方式调用设置优先级
SET_BITS(u32Opt,PRI,C_DD_C64_EDMA_OPT_HIGH_PRI);
即实现了将u32Opt的PRI等位设置为C_DD_C64_EDMA_OPT_HIGH_PRI
SET_BITS宏可应用于待操作的位为多位的情况,当待操作的位仅为一位时,可用更简单的操作方式
#defineSET_BIT(_reg,_name)((_reg)|=(BIT_MASK(_name))
#defineCLR_BIT(_reg,_name)((_reg)&=~(BIT_MASK(_name))
以TCINT为例:
#defineTCINTDD_C64_EDMA_OPT_TCINT
#defineC_WIDTH_DD_C64_EDMA_OPT_TCINT
(1)
#defineC_SHIFT_DD_C64_EDMA_OPT_PRI(20)
SET_BIT(u32Opt,TCINT)
CLR_BIT(u32Opt,TCINT)
嵌入式系统程序可移植性设计及性能优化之二
--数据结构设计
【摘要】本章介绍了结构体中成员的对齐规则,及在此规则上如何调整成员顺序或填充部分字段保证其所占内存大小不会因为编译器的不同导致差异。
然后介绍了如何利用位域设计网络通信协议及由此带来的大小端系统的可移植性问题;同时介绍了用位域在特定平台上配置硬件寄存器的技巧。
最后介绍了如何利用union在不同系统间传输变长数据包及如何进行数据封装并提供相关操作接口的相关技巧。
【关键词】嵌入式,可移植性,数据结构,结构体对齐规则,非对齐访问,位域,传输协议,大小端,硬件配置字,数据封装,初始化操作接口
2数据结构设计-4-
2.1结构体中成员对齐规则-4-
2.1.1自然对界-4-
2.1.2指定对界-4-
2.2合理设计成员顺序-5-
2.2.1减少结构体存储空间-5-
2.2.2填充部分域,避免字节对齐问题-6-
2.2.3字节对齐问题实例-7-
2.3采用位域构造结构体-8-
2.3.1位域设计传输协议-8-
2.3.2位域的可移植性问题-9-
2.3.3位域设计硬件配置字-10-
2.4通过union和struct传递不同格式报文-11-
2.5将相关功能变量封装为结构体-13-
1 数据结构设计
程序设计是算法和数据结构的集合,因此数据结构是程序设计的基础,就象建造豪华的公寓也必须从一砖一瓦开始。
大型的C/C++程序,势必要涉及一些进行数据组合的结构体,这些结构体可以将原本意义属于一个整体的数据组合在一起。
嵌入式系统软硬件平台具备多变性,不同处理器对于数据对齐访问的要求不同,另外不同的编译器可以设置不同的数据对齐规则,这些都将导致结构体在不同软硬件平台下的可移植性问题。
在网络协议、通信控制等嵌入式系统的C/C++编程中,经常要传送的不是简单的字节流(u8型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
经验不足的开发人员往往将所有需要传送的内容依顺序保存在u8型数组中,通过指针偏移的方法传送网络报文等信息。
这样做编程复杂,易出错,按顺序存储的数组不便于增添其他成分,因此一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改,移植性差;而结构体的成员增减时不影响原有单元的操作,因为编译器会自动计算各个成员的偏移量。
因此从某种程度上来说,会不会用struct及怎样用struct对程序的可移植性和可读性有较大影响。
1.1 结构体中成员对齐规则
1.1.1 自然对界
对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。
缺省情况下,编译器为结构体的每个成员按其自然对界(naturalalignment)条件分配空间。
各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界即默认对齐方式,是指按结构体的成员中size最大的成员对齐。
例如:
structnaturalalign
{
u8u8a;
u16u16b;
u8u8c;
};
在上述结构体中,size最大的是u16,其长度为2字节,因而结构体中的u8成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6。
1.1.2 指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:
a) 使用伪指令#pragmapack(n),编译器将按照n个字节对齐;
b) 使用伪指令#pragmapack(),取消自定义字节对齐方式。
所有处于“#pragmapack(n)”和“#pragmapack()”之间的结构体将按照指定对界对齐。
当pragmapack(n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
例如:
#pragmapack(n)
structpack
{
u8u8a;
u32u32b;
u8u8c;
};
#pragmapack()
当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。
而当n为2时,其发挥了作用,使得sizeof(naturalalign)的结果为8。
另外,通过__attribute((packed(n)))也可以单个结构体的成员对齐在n字节边界,这样即使平台改变了,编译器不同了,也将采用统一的对界方式,这种方式对于不同体系的处理器之间的数据交互很重要,移植性好。
1.2 合理设计成员顺序
1.2.1 减少结构体存储空间
在默认的自然对界情况下,若最大数据类型为u32,则u32四字节对齐,u16二字节对齐,整个结构体大小为sizeof(u32)的倍数,该结构体定义的变量首地址自动对齐在sizeof(u32)边界上。
故:
structnaturalalignA
{
u16u16a;
u32u32b;
u8u8c;
};
structnaturalalignB
{
u16u16a;
u8u8c;
u32u32b;
};
structnaturalalignC
{
u8u8c;
u16u16a;
u32u32b;
};
sizeof(naturalalignA)=12
sizeof(naturalalignB)=8
sizeof(naturalalignC)=8
structnaturalalignD
{
u8u8a;
u16u16b;
u8u8c;
};
structnaturalalignE
{
u8u8a;
u8u8c;
u16u16b;
};
sizeof(naturalalignD)=6
sizeof(naturalalignE)=4
structnaturalalignF
{
u8u8a;
u32u32b;
u8u8c;
};
structnaturalalignG
{
u8u8a;
u8u8c;
u32u32b;
};
sizeof(naturalalignF)=12
sizeof(naturalalignG)=8
从上面可以看出,从存储空间的角度看,naturalalignA、naturalalignD、naturalalignF都是不合理的设计。
1.2.2 填充部分域,避免字节对齐问题
structnaturalalignD
{
u8u8a;
u16u16b;
u8u8c;
};
structnaturalalignH
{
u8u8a;
u8padding;
u16u16b;
u8u8c;
u8padding;
};
structnaturalalignI
{
u8u8a;
u8u8c;
u16u16b;
};
在自然对界情况下,
sizeof(naturalalignD)=sizeof(naturalalignH)=6
此时并不会因为填充部分域后导致结构体变大,只是避免编译器自动填充而已。
但对于指定对界时可能会使结构体变大。
naturalalignI调整了成员的顺序,减少存储空间的同时保证了u16u16b的对齐,无需设计填充域,同时编译器也不会自动填充
structnaturalalignJ
{
u16u16a;
u8u8c;
u8padding;
u32u32b;
};
structnaturalalignK
{
u8u8c;
u8padding;
u16u16a;
u32u32b;
};
structnaturalalignL
{
u8u8c;
u16u16a;
u8padding;
u32u32b;
};
naturalalignJ中填充后保证了u32u32b的对齐,naturalalignK填充后保证了u16u16a的对齐也保证了u32u32b的对齐,要注意的是naturalalignL表面上填充域后u32u32b对齐了,但由于u16u16a未处于二字节对齐边界上,实际上编译器在u8u8c后自动填充了一个域保证u16u16a的对齐,在u8padding后自动填充了三个u8保证u32u32b的对齐。
naturalalignJ和naturalalignK都是合理的填充方式。
#pragmapack
(2)
structpackA
{
u16u16a;
u32u32b;
u8u8c;
};
#pragmapack()
#pragmapack
(2)
structpackB
{
u16u16a;
u8u8c;
u32u32b;
};
#pragmapack()
#pragmapack
(1)
structpackC
{
u8u8a;
u16u16b;
u8u8c;
};
#pragmapack()
在指定对界情况下,大于pack字节的将按照设定值进行对齐
sizeof(packA)=8
sizeof(packB)=8
sizeof(packC)=4
由于packA类型变量对齐在sizeof(u32)边界上,2字节对齐时u32u32b的地址没有对齐在四字节边界上,此时的影响为:
不能进行非对齐访问的处理器:
u32u32temp=packA.b将导致处理器内存访问异常;
可非对齐访问的处理器:
u32u32temp=packA.b对于b的访问实际上是分两个u16来分别访问然后合成一个u32后赋值给u32temp的,比对齐的u32变量访问效率要低。
u32*u32temp=&packA.b,因为u32temp为u32型指针变量,其值必须为4的倍数,而(&packA.b)并不是4的倍数,显然此处的赋值是不合理的。
指针类型强制转换时也可能存在这种异常问题。
为了避免指定对界不统一带来的内存访问异常问题,在数据结构设计时总的原则是:
不考虑编译器自动填充的情况下,通过适当填充使u16对齐在二字节上,u32对齐在四字节上,此时无论编译器何种对界,结构体的大小总是固定的,且不会存在内存访问问题。
structnaturalalignE
{
u8u8a;
u8u8c;
u16u16b;
};
structnaturalalignK
{
u8u8c;
u8u8padding;
u16u16a;
u32u32b;
};
structnaturalalignH
{
u8u8a;
u8u8padding;
u16u16b;
u8u8c;
u8u8padding;
};
1.2.3 字节对齐问题实例
下面以一个实例来说明对齐方式不同导致的不同处理器间的数据交互问题。
三种平台:
a) ARM+Linux,嵌入式平台,未定义数据结构,转发字节流,采用偏移量;
b) WindowsMobile+PDA,嵌入式平台,将数据组合为了结构体,按照字节对齐;
c) WindowsXP+PC,自然对界。
相应的数据结构定义如下:
typedefstruct__DBGEstEntry{
u16id;