第一个问题:字节对齐
在做一个小型通信系统时,为了方便通信帧解析以及数据存储,通过结构体定义了该数据帧的结构。代码写完之后进行系统调试,一切进展顺利,最后当调试到存储部分时出了问题,通过监视发现通讯帧结构正确但是存储数据始终不正确,经过深入分析发现系统中定义了如下一个结构体HostNode,在存储的时候采用sizeof运算符对HostNode结构体获取长度,按照设计该长度应该为6+2+1=9字节,而实际上sizeof运算符得到的长度为10,这导致了存储错误。
typedef struct
{
my_u8 std_addr[6];
my_u16 short_addr;
my_u8 status;
}HostNode;
追究问题的根本原因在于部分32位MCU设计上为了追求更高的效率默认采用字对齐方式对数据进行运算和处理的,网络上相关讨论已经比较多,这里引用http://jinguo.javaeye.com/blog/361928的记录作为参考。
首先,声明几个概念:
(1)、对象:在C语言中使用结构体类型、共同体类型、或内部基本类型所定义的变量或常量,就称为对象。对象占据了一块实际的存储器空间,这块空间有固定的起始地址和字节数。
(2)、引用:使用对象有两种方法:“对象名”和“引用”。当你在源代码中定义一个对象时,编译器就会为它分配一块存储器,此时,我们就可以使用“对象名”来操作该对象。但是对于程序运行时动态分配的某一块存储器空间(对象),就没法使用“对象名”了,而只能使用“引用”。所以,“引用”就是指向特定类型的对象的指针。
在32位嵌入式系统中,单字节对象是1字节对齐的;双字节对象是2字节对齐的;四字节对象是4字节对齐的;其它结构体或共同体对象是8字节对齐的。也就是说:
(1)在定义一个单字节对象时,该对象的起始地址可以是任何整数;
(2)定义一个双字节对象时,该对象的起始地址必定是2的倍数的整数;
(3)定义一个四字节对象时,该对象的起始地址必定是4的倍数的整数;
(4)定义一个结构体或共同体对象时,该对象的起始地址必定是8的倍数的整数。
以上说的对象包括“结构体或共同体对象的成员对象”。
字节对齐的故障只能出现在“引用”的使用过程中。当使用“对象名”来操作对象时,根本不用担心字节对齐问题。
在ADS环境下,有“ALIGN” 、“__align(x)” 、“__packed”关键字用于字节对齐处理。ALIGN用于汇编语言,__align(x)用于C语言,__packed用于放弃字节对齐。
单字节对齐类型的引用可以操作任何对象,双字节对齐类型的引用可以操作双字节、四字节、八字节对齐的对象。只有遵守这个规则,程序才可能是健壮的。
如果我们想使用双字节对齐类型的引用来操作单字节对齐对象,那么你在定义该引用时必须使用__packed关键字
第二个问题:C语言的运算优先顺序
在原通信系统设计中使用了如下一句代码
if((end_dev_bit_map[i]<<j)&0x80 != 0x80)
{
}else
{
}
结果为调试带来了很大的麻烦。本来条件表达式想表明如果end_dev_bit_map[i]从左向右的第j位如果不为1,而事实上因为表达式(end_dev_bit_map[i]<<j)&0x80 != 0x8是按照从左到右进行运算的,而!=和&处于同一个运算级别,就导致了先计算0x80!=0x80,该结果为0,然后再与之前的表达式进行&运算,恒等于零,导致整个条件表达式恒为假,从而引起程序错误。
总结
1.32位系统中定义结构体必须谨慎,尤其是存在强制类型转换的代码,需要考虑结构体中的字对齐产生的“空隙”。
2,经过keil对stm32f101设备的仿真发现,采用__packet紧缩型定义的结构体读写操作并没有浪费时间,因此非对齐型结构体定义对速度的影响因设备而异。
3.在C代码编写时最好多加括号以保证运算按照自己设计的先后顺序进行(尽管这样做会使代码比较难看),这样做的好处是代码移植性好,没有风险。