嵌入式数据结构设计

数据结构设计  

摘要】本章介绍了结构体中成员的对齐规则,及在此规则上如何调整成员顺序或填充部分字段保证其所占内存大小不会因为编译器的不同导致差异。然后 介绍了如何利用位域设计网络通信协议及由此带来的大小端系统的可移植性问题;同时介绍了用位域在特定平台上配置硬件寄存器的技巧。最后介绍了如何利用 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    自然对界
对于结构体,编译器会自动进行成员变量的对齐,以提高运算效 率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。自然对界即默认对齐方式,是 指按结构体的成员中size最大的成员对齐。例如:

struct naturalalign

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};
 

在上述结构体中,size最大的是u16,其长度为2字节,因而结构体中的u8成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6。

  

1.1.2    指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:

a)       使用伪指令#pragma pack (n),编译器将按照n个字节对齐;

b)       使用伪指令#pragma pack (),取消自定义字节对齐方式。

所有处于“#pragma pack (n)”和“#pragma pack ()”之间的结构体将按照指定对界对齐。当pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。例如:

#pragma pack (n)

struct pack

{

        u8 u8a;

        u32 u32b;

        u8 u8c;

};

#pragma pack ()
 

当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)边界上。故:

 

struct naturalalignA

{

        u16 u16a;

        u32 u32b;

        u8 u8c;

};
 struct naturalalignB

{

        u16 u16a;

        u8 u8c;

        u32 u32b;

};
 struct naturalalignC

{

        u8 u8c;

        u16 u16a;

        u32 u32b;

};
 

sizeof(naturalalignA) = 12

sizeof(naturalalignB) = 8

sizeof(naturalalignC) = 8

 

struct naturalalignD

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};
 struct naturalalignE

{

        u8 u8a;

        u8 u8c;

        u16 u16b;

};
 

sizeof(naturalalignD) = 6

sizeof(naturalalignE) = 4

 

struct naturalalignF

{

        u8 u8a;

        u32 u32b;

        u8 u8c;

};
 struct naturalalignG

{

        u8 u8a;

        u8 u8c;

        u32 u32b;

};
 

sizeof(naturalalignF) =12

sizeof(naturalalignG) = 8

从上面可以看出,从存储空间的角度看,naturalalignA、naturalalignD、naturalalignF都是不合理的设计。

 

1.2.2    填充部分域,避免字节对齐问题
 

struct naturalalignD

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};
 struct naturalalignH

{

        u8 u8a;

        u8 padding;

        u16 u16b;

        u8 u8c;

        u8 padding;

};
 struct naturalalignI

{

        u8 u8a;

        u8 u8c;

        u16 u16b;

};
 

在自然对界情况下,

sizeof(naturalalignD) = sizeof(naturalalignH) = 6

此时并不会因为填充部分域后导致结构体变大,只是避免编译器自动填充而已。但对于指定对界时可能会使结构体变大。

naturalalignI调整了成员的顺序,减少存储空间的同时保证了u16 u16b的对齐,无需设计填充域,同时编译器也不会自动填充

 

struct naturalalignJ

{

        u16 u16a;

        u8 u8c;

        u8 padding;

        u32 u32b;

};
 struct naturalalignK

{

        u8 u8c;

        u8 padding;

        u16 u16a;

        u32 u32b;

};
 struct naturalalignL

{

        u8 u8c;

        u16 u16a;

        u8 padding;

        u32 u32b;

};
 

naturalalignJ中填充后保证了u32 u32b的对齐,naturalalignK填充后保证了u16 u16a的对齐也保证了u32 u32b的对齐,要注意的是naturalalignL表面上填充域后u32 u32b对齐了,但由于u16 u16a未处于二字节对齐边界上,实际上编译器在u8 u8c后自动填充了一个域保证u16 u16a的对齐,在u8 padding后自动填充了三个u8保证u32 u32b的对齐。naturalalignJ和naturalalignK都是合理的填充方式。

 

#pragma pack (2)

struct packA

{

        u16 u16a;

        u32 u32b;

        u8 u8c;

};

#pragma pack ()
 #pragma pack (2)

struct packB

{

        u16 u16a;

        u8 u8c;

        u32 u32b;

};

#pragma pack ()
 #pragma pack (1)

struct packC

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};

#pragma pack ()
 

在指定对界情况下,大于pack字节的将按照设定值进行对齐

sizeof(packA) = 8

sizeof(packB) = 8

sizeof(packC) = 4

由于packA类型变量对齐在sizeof(u32)边界上,2字节对齐时u32 u32b的地址没有对齐在四字节边界上,此时的影响为:

不能进行非对齐访问的处理器:u32 u32temp = packA.b将导致处理器内存访问异常;

可非对齐访问的处理器:u32 u32temp = packA.b对于b的访问实际上是分两个u16来分别访问然后合成一个u32后赋值给u32temp的,比对齐的u32变量访问效率要低。

u32  *u32temp = &packA.b,因为u32temp为u32型指针变量,其值必须为4的倍数,而(&packA.b)并不是4的倍数,显然此处的赋值是不合理的。指针类型强制转换时也可能存在这种异常问题。

 

为了避免指定对界不统一带来的内存访问异常问题,在数据结构设计时总的原则是:不考虑编译器自动填充的情况下,通过适当填充使u16对齐在二字节上,u32对齐在四字节上,此时无论编译器何种对界,结构体的大小总是固定的,且不会存在内存访问问题。

 

struct naturalalignE

{

        u8 u8a;

        u8 u8c;

        u16 u16b;

};
 struct naturalalignK

{

        u8 u8c;

        u8 u8padding;

        u16 u16a;

        u32 u32b;

};
 struct naturalalignH

{

        u8 u8a;

        u8 u8padding;

        u16 u16b;

        u8 u8c;

        u8 u8padding;

};
 

1.2.3    字节对齐问题实例
以我在NTT DoCoMo实习期间遇到的一个实例来说明对齐方式不同导致的不同处理器间的数据交互问题。

 

三种平台:

a)       ARM + Linux,嵌入式平台,未定义数据结构,转发字节流,采用偏移量;

b)       Windows Mobile + PDA,嵌入式平台,将数据组合为了结构体,按照字节对齐;

c)       Windows XP + PC,自然对界。

 

相应的数据结构定义如下:

typedef struct __DBGEstEntry {

  u16 id;

  u8 hopcount;

  u8 sendEst;

} DBGEstEntry;

 

//add by oy 06.8.4

typedef struct __DebugPacket {

  u8 estEntries;

  DBGEstEntry estList;

} DebugPacket;

 

ARM和PDA交互时因为都是按照字节对齐的,在将字节流强制转换为结构体时,对数据的解析方式一致,没有任何问题。后将代码移植到PC平台,同样的程序结果却不一样。究其原因就是因为不同平台采用了不一致的对齐方式。

 

PC平台下VC默认采用8字节自然对界,由于DBGEstEntry最大类型为uu3216_t,因此DebugPacket的成员estEntries后默认补齐了一个u8,相当于此时不同平台采用了不一样的结构来解析数据,就出现异常了。

 

添加一个填充域,指针强制转换时,地址前移一个以取消填充域的影响。

typedef struct __DebugPacket {

  u8 reserved;

  u8 estEntries;

  DBGEstEntry estList;

} DebugPacket;

 

或者针对此数据结构采用指定的对齐方式,和ARM和PDA平台一致。

typedef struct __DebugPacket {

  u8 reserved;

  u8 estEntries;

  DBGEstEntry estList;

} __attribute((packed (1))) DebugPacket;

 

因此对于嵌入式平台上的数据结构设计,一定要合理调整顺序及填充部分域避免平台和编译器的不同导致的字节对齐问题。

 

1.3    采用位域构造结构体
1.3.1    位域设计传输协议
在大多数情况下,我们一般这样定义结构体:

/* 与MCP驱动通信交互的数据帧头部标识*/

typedef struct tag_STRU_DD_FRMHDR

{

    u8     u8FrmFlag;           /* 帧头标识  */

u8     u8SrcModuleId;        /* 源模块ID   */

u8     u8DstModuleId;           /* 目的模块ID   */

u8     u8padding;

u16    u16Length;     /* 数据长度   */

u16    u16padding

} STRU_DD_FRMHDR;

 

对于一般的应用,这已经能很充分地实现数据的“封装”。但是在实际工程中,由于传输链路中每次传输的数据有限,头部越长,导致有效数据越少,如 CAN总线通信中,数据总长8个字节,有效的减少头部信息就很重要。由于数据长度u16Length不便缩短,而u8SrcModuleId和 u8DstModuleId分别用四位就可表示,因此可以用位域来表示此结构体。

 

位域就是一个基本类型变量中的不同的位表示不同的含义。譬如一个硬件寄存器,假设为16 bit ,而每个bit 都可以表达不同的含义。这个时候我们用什么数据结构来表达这个寄存器呢?答案还是结构体!这时要用到结构体的高级特性,就是在基本成员变量的后面添加“: 数据位数”组成新的结构体,如下:

typedef struct tag_STRU_DD_FRMHDR

{

u32   bit8FrmFlag :8;           /* 帧头标识  */

u32   bit4SrcModuleId:4;        /* 源模块ID   */

u32   bit4DstModuleId :4;               /* 目的模块ID   */

u32   bit16Length:16;     /* 数据长度   */

} STRU_DD_FRMHDR;

 

上述结构体中的四个成员加起来只占用了一个unsigned u32 的空间。基本成员变量被拆分后,访问的方法仍然和访问没有拆分的情况是一样的。

 

1.3.2    位域的可移植性问题
在拆分基本成员变量的情况下,我们要特别注意数据的存放顺序,这还与CPU 是Big endian 还是Little endian 来决定。Little endian 和Big endian 是CPU 存放数据的两种不同顺序。对于整型、长整型等数据类型,Big endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节);而Little endian 则相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放数据的低位字节到高位字节)。

 

如我们定义IP 包头结构体为:

struct iphdr {

#if defined(__LITTLE_ENDIAN_BITFIELD)

u8 ihl:4, version:4;

#elif defined (__BIG_ENDIAN_BITFIELD)

 u8 version:4, ihl:4;

#else 

#error "Please fix <asm/byteorder.h>"

#endif

 

u8 tos; u16 tot_len; u16 id; u16 frag_off; u8 ttl; u8 protocol; u16 check; __u32 saddr; __u32 daddr;

};

 

在Little endian 模式下,iphdr 中定义:u8 ihl:4, version:4;

因为位域是从低位字节开始计算的,其存放方式为:

第1 字节低4 位 ihl ,第1 字节高4 位 version (IP 的版本号)

若在Big endian 模式下还这样定义,则存放方式为:

第1 字节低4 位 version (IP 的版本号),第1 字节高4 位 ihl

这与实际的IP 协议是不匹配的,所以在Linux 内核源代码中,IP 包头结构体的定义利用了宏来区分两种不同的情况。

#if defined(__LITTLE_ENDIAN_BITFIELD) …

#elif defined (__BIG_ENDIAN_BITFIELD) …

#endif

 

对于咱们的应用情况,此STRU_DD_FRMHDR是PowerPC和DSP交互时使用的,目前二者都为big endian模式,因此二者采用位域对数据解析时是一致的,但若一方为小端,则数据解析将出错。

 

由此我们总结位域的使用要点:

a)       C/C++ 语言的结构体支持对其中的基本成员变量按位拆分,其使用方法和结构体一致,便于编程;

b)       但要特别注意拆分后的数据的存放顺序,为了支持代码的跨平台移植,应用宏定义来区分大小端的不同数据结构。

 

1.3.3    位域设计硬件配置字
在嵌入式系统中经常要和底层硬件打交道,此时需要配置各种硬件选项,而这些选项通常都是由一个寄存器的不同位表示的,进行配置时经常要用到与或、移位等操作,编程不便,因此可以利用位域的形式来访问不同域,其和结构体的访问形式一样,非常方便。

/* struct defined for the OPTION word for EDMA - big endian */

typedef struct tag_STRU_DD_C64_EDMA_OPT

{

    u32 u32bit3Pri    : 3;        /* Priority */

    u32 u32bit2Esize  : 2;        /* Element size */

    u32 u32bit1TwoDs  : 1;       /* Source dimension */

    u32 u32bit2Sum    : 2;       /* Source address update mode */

    u32 u32bit1TwoDd  : 1;       /* Destination dimension */

    u32 u32bit2Dum    : 2;       /* Destination address update mode */

    u32 u32bit1Tcu32  : 1;        /* Transfer complete u32errupt */

    u32 u32bit4Tcc    : 4;        /* Transfer complete code */

    u32 u32bit1Rsvd1  : 1;        /* Resverd */

    u32 u32bit2Tccm   : 2;        /* Transfer complete code */

    u32 u32bit1Atcu32 : 1;         /* Alternate transfer complete u32r */

    u32 u32bit1Rsvd2  : 1;        /* Resverd */

    u32 u32bit6Atcc   : 6;        /* Alternate transfer complete code */

    u32 u32bit1Rsvd3  : 1;        /* Resverd */

    u32 u32bit1Pdts   : 1;         /* PDT mode for source */

    u32 u32bit1Pdtd   : 1;         /* PDT mode for Destination */

    u32 u32bit1Link   : 1;         /* Link */

    u32 u32bit1Fs     : 1;         /* Frame synchronization */

} STRU_DD_C64_EDMA_OPT;

 

硬件寄存器通常上电后各位为0,大多数选项都是默认的,只需配置部分非0的选项。采用位域时只会更改相关的配置项,不会影响其他位。而没有位域时需要与或等操作避免影响其他位。

 

对于硬件配置字通常定义相关常量来表示不同的配置,如下:

/* Define constants to construct EDMA option word */

/* Priority, 3bit */

#define C_DD_C64_EDMA_OPT_URGENT_PRI         (0x0)

#define C_DD_C64_EDMA_OPT_HIGH_PRI           (0x1)

#define C_DD_C64_EDMA_OPT_MEDIUM_PRI         (0x2)

#define C_DD_C64_EDMA_OPT_LOW_PRI            (0x3)

 

/* Element size, 2bit */

#define C_DD_C64_EDMA_OPT_ESIZE_32           (0x0)

#define C_DD_C64_EDMA_OPT_ESIZE_16           (0x1)

#define C_DD_C64_EDMA_OPT_ESIZE_8            (0x2)

 

1.4    通过union和struct传递不同格式报文
网络通信中进行数据包交互时通常以固定数据区传递,但每次传递的消息类型可能不同,因此固定数据区的有效数据长度可能不一样,此时需要字段标识消息类型和消息长度,以便双方对数据进行解析。

 

每种消息都有共有的消息头开始,然后是特定的消息信息,这样消息的结构将变得非常清晰,统一的格式如下:

typedef struct tag_STRU_MAT_XXX

{

    STRU_DD_MSG_HDR         struMsgHdr;                

    STRU_DD_XXX                    struXXX;                 

} STRU_MAT_XXX;

 

消息头如下:

typedef struct tag_STRU_DD_MSG_HDR

{

    u8      u8OpCode;  

    u8      u8Len;    

    u8                  au8MsgHdrData[C_DD_COMM_MSG_HEADER_DATA_BYTE_LEN];                                      

} STRU_DD_MSG_HDR;

通过消息头中的u8OpCode操作码来确定消息类型,也就确定了消息的具体内容;若对于某种消息,其有效数据长度是可变的,则u8Len将标识消息数据域的长度

 

当消息内容固定时,最好以结构体封装消息内容。

typedef struct tag_STRU_MAT_SET_BPP_TOD_SFN_TSN_REQ

{

    STRU_DD_MSG_HDR     struMsgHdr;     

    u32                 u32TodPart1;       

    u32                 u32TodPart2;        

    u16                 u16SfnPeriod;       

    u16                 u16TsnPeriod;       

    u16                 u16Sfn;         

    u16                 u16Tsn;           

} STRU_MAT_SET_BPP_TOD_SFN_TSN_REQ;

上述方式消息内容零散,同时也不便于将其另行传递给其他模块。

 

封装后的结构如下:

typedef struct tag_STRU _BPP_TOD_SFN_TSN_PARAM

{

    u32                 u32TodPart1;  

    u32                 u32TodPart2;  

    u16                 u16SfnPeriod;  

    u16                 u16TsnPeriod;   

    u16                 u16Sfn;     

    u16                 u16Tsn;   

} STRU _BPP_TOD_SFN_TSN_PARAM;

 

typedef struct tag_STRU_MAT_SET_BPP_TOD_SFN_TSN_PARAM _REQ

{

    STRU_DD_MSG_HDR         struMsgHdr;          

    STRU _BPP_TOD_SFN_TSN_ PARAM  struTimeParam;                                         

} STRU_MAT_SET_BPP_TOD_SFN_TSN_PARAM _REQ;

 

不同消息STRU_MAT_XXX长度不同,但又需要统一的数据区来存储,因此需要定义一个数据结构来统一各种消息,关键在于统一的数据区长度如何确定,其既保证可以容纳所有消息类型同时又不浪费存储空间?

 

一种方式算出各种消息占用的最大存储空间,对于具体的消息数据采用纯数组存储,其根据头部的u8OpCode确定消息类型,然后再强制转换消息数据进行解析。

typedef struct tag_STRU_DD_COMM_MSG

{

    STRU_DD_MSG_HDR     struMsgHdr;                  

    u32      au32CommData C_DD_COMM_MSG_MAX_DATA_WORD_LEN];

} STRU_DD_COMM_MSG;

 

另外一种方式是通过union在同一片内存空间存储不同格式信息,由header来分辨消息类型和长度

typedef struct tag_STRU_DD_MAT_COMM_MSG

{

    STRU_DD_MSG_HDR     struMsgHdr;             

union

{

STRU_ BPP_CUR_LOAD   struCurLoad 

STRU _BPP_TOD_SFN_TSN_ PARAM  struTimeParam; 

STRU_DD_BPP_LOAD_PARAM  struLoadParam;       

}unionMsgData;                  

} STRU_DD_MAT_COMM_MSG;

各种具体的消息共用一块固定长度内存,取决于最大的消息大小

 

不用根据各种消息类型算C_DD_COMM_MSG_MAX_DATA_WORD_LEN的值,消息类型的改变不影响tag_STRU_DD_MAT_COMM_MSG,可以自适应消息类型的变化;但添加新消息类型时需要更改STRU_DD_MAT_COMM_MSG。

 

在传输链路上进行数据传输时并不是传递STRU_DD_MAT_COMM_MSG,而是传递具体的消息头和消息内容,因为有效信息可能并没有这 么多数据,这样可以节省传输时间。此时可用sizeof(STRU_MAT_XXX)或者sizeof(STRU_DD_MSG_HDR) + sizeof(STRU_DD_XXX)

 

1.5    将相关功能变量封装为结构体
何谓相关?是指多个不同意义的变量任何时候都是统一定义、统一初始化、统一传递给其他模块,就好像难兄难弟一样任何时候都在一块,此时就应该将这些变量组织为结构体,优势在于:

a)       便于管理、定义、声明,避免零散的变量;

b)       意义明确,结构清晰;

c)       函数调用时避免传递过多参数,提高调用性能,参数少不易出错。

不足在于对于结构体的访问效率不如单独的变量,但此性能影响很小;为了代码更好的可读性、可移植可维护性性和可靠性,此处结构体的形式更合适。

 

以测试HPI 和DPRAM 发送或者接收时间的统计变量数据结构为例进行说明

typedef struct tag_STRU_TEST_TX_RX_TIME_STATS

{

    u32          u32Start;        /* 发送或者接收的起始点, 以Cycle为单位 */

    u32          u32TimeGap;      /* 发送或者接收时间,以Cycle为单位 */

    u32          u32TimeGapMax;   /* 发送或者接收的最长时间,以Cycle为单位 */

} STRU_TEST_TX_RX_TIME_STATS;

 

对于必须初始化的结构体变量有两种方式进行初始化:

1)       定义时候采用“{}”对各个成员变量赋初始值,如

STRU_TEST_TX_RX_TIME_STATS   struTxRxTime =  {0,0,0}

但是当结构体的成员顺序调整或者增减成员变量时,原有的初始化赋值代码全部需要修改,缺乏可裁减性和可修改性。

2)       对结构体成员分别赋值,如:

struTxRxTime.u32Start = 0;

struTxRxTime.u32TimeGap= 0;

struTxRxTime.u32TimeGapMax= 0;

当结构体成员顺序改变时,上述代码不会受到影响,但增减成员变量时,此处的代码也需要修改,缺乏可裁减性。

 

为了提高此结构体的可移植性,需要提供针对此类变量的各种操作接口,包括初始化、更新维护等。

a)       提供静态定义和动态初始化该数据结构的宏接口:

为了防止用户在定义此类结构体变量时未初始化,提供静态定义宏,采用此宏定义变量就可确保其经过初始化了。因此用户应避免直接采用该数据结构来定义变量。

/* 静态定义STRU_TEST_TX_RX_TIME_STATS 类型变量*/

#define  DD_DECLARE_TX_RX_TIME_STATS(struTxRxTime)  STRU_TEST_TX_RX_TIME_STATS   struTxRxTime =  {0,0,0}

 

但是对于已经定义了的或者是在堆中申请的该类变量,上述静态定义宏就无能为力了,所以还需要提供动态初始化的宏定义接口:

/* 动态初始化已经定义的STRU_TEST_TX_RX_TIME_STATS 类型变量*/

#define DD_INIT_TX_RX_TIME_STATS(pstruTxRxTime)    do{ \

 (pstruTxRxTime)->u32Start = 0; \

 (pstruTxRxTime)->u32TimeGap = 0; \

 (pstruTxRxTime)->u32TimeGapMax = 0; \

     }while(0)

在C中,对于函数参数避免对结构体进行值传递,因为没有C++中的拷贝构造函数,简单的值传递可能造成异常,因此参数肯定是该结构体类型的指 针。那么对于函数宏参数呢?此时并不需要传递变量,但为了动态初始化函数能操作全局、栈以及堆中分配的该类型变量,其参数应为指针形式。

 

此时当数据结构类型发生变化时,只需要修改相关的宏接口即可,其他调用此类宏接口的相关代码无需任何修改,提高了代码的可移植性。

 

b)       设置发送或者接收的起始时间

#define  DD_SET_TIME_STATS_START(pstruTxRxTime)    (pstruTxRxTime)->u32Start = (*C_DD_C64_TIMER2_CNT)

乘以C_DD_C64_TIMER_CLK_DIVIDER后,就可能溢出了,因此u32Start只能以定时器timer个数为单位;因为“.”“->”优先级较高,因此应将pstruTxRxTime参数用括号扩起来,这也是宏参数的基本规则。

 

c)       获得发送或者接收的时间,并更新最大时间

#define  DD_UPDATE_TIME_STATS(pstruTxRxTime)           do{ \

 (pstruTxRxTime)->u32TimeGap = ((*C_DD_C64_TIMER2_CNT )  - (pstruTxRxTime)->u32Start) * C_DD_C64_TIMER_CLK_DIVIDER; \

                                                                                    (pstruTxRxTime)->u32TimeGapMax = (((pstruTxRxTime)->u32TimeGap) > ((pstruTxRxTime)->u32TimeGapMax)) ? ((pstruTxRxTime)->u32TimeGap) : ((pstruTxRxTime)->u32TimeGapMax); \

     }while(0)       

 

在咱们的代码中还有多处此类封装,如下:

将通道收发正确错误统计计数综合为一个数据结构,具有一定的共性,以供利用HPI 、DPRAM 及McBSP的模块使用。

/* HPI 、DPRAM 及McBSP 等通道收发统计变量数据结构 */

typedef struct tag_STRU_TX_RX_CNT_STATS

{

    u32          u32RxOkCnt;     /* 接收正确的次数*/

    u32          u32RxErrCnt;     /* 接收错误的次数*/

   

    u32          u32TxOkCnt;     /* 发送正确的次数*/

    u32          u32TxErrCnt;     /* 发送错误的次数*/

} STRU_TX_RX_CNT_STATS;

 

/* 中断统计变量数据结构 */

typedef struct tag_STRU_ISR_TIME_STATS

{

    u32  u32IsrStartTestFlag;     /* 开始测量中断时间标志,获取u32IsrPrevCounter*/

    u32  u32IsrTotalCnt;         /* 该中断发生的总次数*/

    u32   u32IsrPrevCounter;     /* 上次发生中断时timer2的计数值*/

 

    u32  u32IsrCurGapTime;     /* 两次中断的间隔时间,单位CPU cycle*/

    u32  u32IsrMinGapTime;      /* 两次中断的最小间隔时间,单位CPU cycle*/

    u32  u32IsrMaxGapTime;     /* 两次中断的最大间隔时间,单位CPU cycle*/

} STRU_ISR_TIME_STATS;

 

本文来自CSDN博客,转载请标明出处: http://blog.csdn.net/sailor_8318/archive/2008/07/17/2663633.aspx
2

宏定义设计  

要】本节介绍了嵌入式系统程序设计中采用宏定义进行常量定义的必要性。说明了宏常量定义的基本规则以及如何采用依赖关系定义宏常量来保证其可移植性和裁减性。最后介绍了如何利用宏定义实现掩码偏移量等来高效的进行位操作。

 

【关键词】嵌入式,可移植性,宏定义,依赖关系,掩码,偏移量,位操作

 

 

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年中有多少秒,不考虑润年

#define C_SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

a)       命名风格,为了与普通变量区分开来,宏定义通常全部大写,多个单元之间用下划线隔开;

b)       整个表达式应括起来,若有参数则应将每个参数都括起来,防止替换扩展后可能带来的异常问题;

c)       常量表达式先合并后再替换。预处理器将为你计算常量表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有运行性能代价的。

d)       为常量添加类型信息。宏的不足之一在于缺乏类型安全检查,人为的提供类型信息可以有效检查出此类问题。UL告诉编译器这个常量是无符号长整型数,因此将其赋值给u16型变量会出现告警。

 

1.3    依赖关系定义宏改善移植性
嵌入式系统程序的最大特点是硬件平台的多变性,因此需要根据具体的应用情况更改大量配置,而这 些配置基本都是由宏定义来实现的,放在特定的头文件中,与其他的代码隔离,在一定程度上改善了代码的可移植性。但有些时候,多个宏定义有严重的依赖关系, 增减某个宏会引起其他定义的更改,如何定义这些宏对嵌入式程序的可移植性有很大影响。

 

A

常量分别定义
 #define C_DD_MODULE_ID_AOM  (0x00010101)     /* AOM模块ID */

#define C_DD_MODULE_ID_RRCM (0x00010102)        /* RRCM模块ID */

#define C_DD_MODULE_ID_RLC (0x00010103)        /* RLC模块ID */

#define C_DD_MODULE_ID_TRM  (0x00010104)       /* TRM模块ID */

#define C_DD_MODULE_ID_MCP_MIN        (C_DD_MODULE_ID_AOM)               

#define C_DD_MODULE_ID_MCP_MAX     (C_DD_MODULE_ID_TRM)
 
B

依赖定义
 #define C_DD_MODULE_ID_MCP_MIN  (0x00010101) /* MCP最小模块ID */

#defineC_DD_MODULE_ID_AOM (C_DD_MODULE_ID_MCP_MIN)                        /* AOM模块ID */

#define C_DD_MODULE_ID_RRCM      (C_DD_MODULE_ID_AOM + 1)                        /* RRCM模块ID */

#define C_DD_MODULE_ID_RLC  (C_DD_MODULE_ID_RRCM + 1)                        /* RLC模块ID */

#define C_DD_MODULE_ID_TRM (C_DD_MODULE_ID_RLC + 1)                        /* TRM模块ID */

#define C_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为例进行说明:

#define PRI  DD_C64_EDMA_OPT_PRI

#define C_WIDTH _DD_C64_EDMA_OPT_PRI          (3)

#define C_SHIFT_DD_C64_EDMA_OPT_PRI          (29)

 

可以手动定义对应位的掩码,如下:

#define C_MASK_DD_C64_EDMA_OPT_PRI           (0xe0000000)

但更好的方式是利用位宽和偏移量来自动构成掩码

#define BIT_MASK(_name)  (((1U<< C_WIDTH _##_name))-1)<<( C_SHIFT_##_name)

#define C_MASK_DD_C64_EDMA_OPT_PRI  BIT_MASK(PRI)

BIT_MASK(PRI)经过宏替换后即为:

(((1U<<( C_WIDTH _DD_C64_EDMA_OPT_PRI))-1) 

<<( C_SHIFT_DD_C64_EDMA_OPT_PRI)

 

对于每个位的取值也应该用宏定义来表示,这样清晰明确

#define C_DD_C64_EDMA_OPT_URGENT_PRI         (0x0)

#define C_DD_C64_EDMA_OPT_HIGH_PRI           (0x1)

#define C_DD_C64_EDMA_OPT_MEDIUM_PRI         (0x2)

#define C_DD_C64_EDMA_OPT_LOW_PRI            (0x3)

 

具备了掩码和偏移量即可对各个位进行操作了

#define SET_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宏可应用于待操作的位为多位的情况,当待操作的位仅为一位时,可用更简单的操作方式

#define SET_BIT(_reg,_name)   ((_reg) |= (BIT_MASK(_name))

#define CLR_BIT(_reg,_name)   ((_reg) &= ~(BIT_MASK(_name))

 

以TCINT为例:

#define TCINT  DD_C64_EDMA_OPT_TCINT

#define C_WIDTH _DD_C64_EDMA_OPT_TCINT  (1)

#define C_SHIFT_DD_C64_EDMA_OPT_PRI          (20)

SET_BIT(u32Opt, TCINT)

CLR_BIT(u32Opt, TCINT)

 

本文来自CSDN博客,转载请标明出处: http://blog.csdn.net/sailor_8318/archive/2008/07/16/2663254.aspx
3

摘要】本章介绍了结构体中成员的对齐规则,及在此规则上如何调整成员顺序或填充部分字段保证其所占内存大小不会因为编译器的不同导致差异。然后 介绍了如何利用位域设计网络通信协议及由此带来的大小端系统的可移植性问题;同时介绍了用位域在特定平台上配置硬件寄存器的技巧。最后介绍了如何利用 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    自然对界
对于结构体,编译器会自动进行成员变量的对齐,以提高运算效 率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。自然对界即默认对齐方式,是 指按结构体的成员中size最大的成员对齐。例如:

struct naturalalign

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};
 

在上述结构体中,size最大的是u16,其长度为2字节,因而结构体中的u8成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6。

  

1.1.2    指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:

a)       使用伪指令#pragma pack (n),编译器将按照n个字节对齐;

b)       使用伪指令#pragma pack (),取消自定义字节对齐方式。

所有处于“#pragma pack (n)”和“#pragma pack ()”之间的结构体将按照指定对界对齐。当pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。例如:

#pragma pack (n)

struct pack

{

        u8 u8a;

        u32 u32b;

        u8 u8c;

};

#pragma pack ()
 

当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)边界上。故:

 

struct naturalalignA

{

        u16 u16a;

        u32 u32b;

        u8 u8c;

};
 struct naturalalignB

{

        u16 u16a;

        u8 u8c;

        u32 u32b;

};
 struct naturalalignC

{

        u8 u8c;

        u16 u16a;

        u32 u32b;

};
 

sizeof(naturalalignA) = 12

sizeof(naturalalignB) = 8

sizeof(naturalalignC) = 8

 

struct naturalalignD

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};
 struct naturalalignE

{

        u8 u8a;

        u8 u8c;

        u16 u16b;

};
 

sizeof(naturalalignD) = 6

sizeof(naturalalignE) = 4

 

struct naturalalignF

{

        u8 u8a;

        u32 u32b;

        u8 u8c;

};
 struct naturalalignG

{

        u8 u8a;

        u8 u8c;

        u32 u32b;

};
 

sizeof(naturalalignF) =12

sizeof(naturalalignG) = 8

从上面可以看出,从存储空间的角度看,naturalalignA、naturalalignD、naturalalignF都是不合理的设计。

 

1.2.2    填充部分域,避免字节对齐问题
 

struct naturalalignD

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};
 struct naturalalignH

{

        u8 u8a;

        u8 padding;

        u16 u16b;

        u8 u8c;

        u8 padding;

};
 struct naturalalignI

{

        u8 u8a;

        u8 u8c;

        u16 u16b;

};
 

在自然对界情况下,

sizeof(naturalalignD) = sizeof(naturalalignH) = 6

此时并不会因为填充部分域后导致结构体变大,只是避免编译器自动填充而已。但对于指定对界时可能会使结构体变大。

naturalalignI调整了成员的顺序,减少存储空间的同时保证了u16 u16b的对齐,无需设计填充域,同时编译器也不会自动填充

 

struct naturalalignJ

{

        u16 u16a;

        u8 u8c;

        u8 padding;

        u32 u32b;

};
 struct naturalalignK

{

        u8 u8c;

        u8 padding;

        u16 u16a;

        u32 u32b;

};
 struct naturalalignL

{

        u8 u8c;

        u16 u16a;

        u8 padding;

        u32 u32b;

};
 

naturalalignJ中填充后保证了u32 u32b的对齐,naturalalignK填充后保证了u16 u16a的对齐也保证了u32 u32b的对齐,要注意的是naturalalignL表面上填充域后u32 u32b对齐了,但由于u16 u16a未处于二字节对齐边界上,实际上编译器在u8 u8c后自动填充了一个域保证u16 u16a的对齐,在u8 padding后自动填充了三个u8保证u32 u32b的对齐。naturalalignJ和naturalalignK都是合理的填充方式。

 

#pragma pack (2)

struct packA

{

        u16 u16a;

        u32 u32b;

        u8 u8c;

};

#pragma pack ()
 #pragma pack (2)

struct packB

{

        u16 u16a;

        u8 u8c;

        u32 u32b;

};

#pragma pack ()
 #pragma pack (1)

struct packC

{

        u8 u8a;

        u16 u16b;

        u8 u8c;

};

#pragma pack ()
 

在指定对界情况下,大于pack字节的将按照设定值进行对齐

sizeof(packA) = 8

sizeof(packB) = 8

sizeof(packC) = 4

由于packA类型变量对齐在sizeof(u32)边界上,2字节对齐时u32 u32b的地址没有对齐在四字节边界上,此时的影响为:

不能进行非对齐访问的处理器:u32 u32temp = packA.b将导致处理器内存访问异常;

可非对齐访问的处理器:u32 u32temp = packA.b对于b的访问实际上是分两个u16来分别访问然后合成一个u32后赋值给u32temp的,比对齐的u32变量访问效率要低。

u32  *u32temp = &packA.b,因为u32temp为u32型指针变量,其值必须为4的倍数,而(&packA.b)并不是4的倍数,显然此处的赋值是不合理的。指针类型强制转换时也可能存在这种异常问题。

 

为了避免指定对界不统一带来的内存访问异常问题,在数据结构设计时总的原则是:不考虑编译器自动填充的情况下,通过适当填充使u16对齐在二字节上,u32对齐在四字节上,此时无论编译器何种对界,结构体的大小总是固定的,且不会存在内存访问问题。

 

struct naturalalignE

{

        u8 u8a;

        u8 u8c;

        u16 u16b;

};
 struct naturalalignK

{

        u8 u8c;

        u8 u8padding;

        u16 u16a;

        u32 u32b;

};
 struct naturalalignH

{

        u8 u8a;

        u8 u8padding;

        u16 u16b;

        u8 u8c;

        u8 u8padding;

};
 

1.2.3    字节对齐问题实例
以我在NTT DoCoMo实习期间遇到的一个实例来说明对齐方式不同导致的不同处理器间的数据交互问题。

 

三种平台:

a)       ARM + Linux,嵌入式平台,未定义数据结构,转发字节流,采用偏移量;

b)       Windows Mobile + PDA,嵌入式平台,将数据组合为了结构体,按照字节对齐;

c)       Windows XP + PC,自然对界。

 

相应的数据结构定义如下:

typedef struct __DBGEstEntry {

  u16 id;

  u8 hopcount;

  u8 sendEst;

} DBGEstEntry;

 

//add by oy 06.8.4

typedef struct __DebugPacket {

  u8 estEntries;

  DBGEstEntry estList;

} DebugPacket;

 

ARM和PDA交互时因为都是按照字节对齐的,在将字节流强制转换为结构体时,对数据的解析方式一致,没有任何问题。后将代码移植到PC平台,同样的程序结果却不一样。究其原因就是因为不同平台采用了不一致的对齐方式。

 

PC平台下VC默认采用8字节自然对界,由于DBGEstEntry最大类型为uu3216_t,因此DebugPacket的成员estEntries后默认补齐了一个u8,相当于此时不同平台采用了不一样的结构来解析数据,就出现异常了。

 

添加一个填充域,指针强制转换时,地址前移一个以取消填充域的影响。

typedef struct __DebugPacket {

  u8 reserved;

  u8 estEntries;

  DBGEstEntry estList;

} DebugPacket;

 

或者针对此数据结构采用指定的对齐方式,和ARM和PDA平台一致。

typedef struct __DebugPacket {

  u8 reserved;

  u8 estEntries;

  DBGEstEntry estList;

} __attribute((packed (1))) DebugPacket;

 

因此对于嵌入式平台上的数据结构设计,一定要合理调整顺序及填充部分域避免平台和编译器的不同导致的字节对齐问题。

 

1.3    采用位域构造结构体
1.3.1    位域设计传输协议
在大多数情况下,我们一般这样定义结构体:

/* 与MCP驱动通信交互的数据帧头部标识*/

typedef struct tag_STRU_DD_FRMHDR

{

    u8     u8FrmFlag;           /* 帧头标识  */

u8     u8SrcModuleId;        /* 源模块ID   */

u8     u8DstModuleId;           /* 目的模块ID   */

u8     u8padding;

u16    u16Length;     /* 数据长度   */

u16    u16padding

} STRU_DD_FRMHDR;

 

对于一般的应用,这已经能很充分地实现数据的“封装”。但是在实际工程中,由于传输链路中每次传输的数据有限,头部越长,导致有效数据越少,如 CAN总线通信中,数据总长8个字节,有效的减少头部信息就很重要。由于数据长度u16Length不便缩短,而u8SrcModuleId和 u8DstModuleId分别用四位就可表示,因此可以用位域来表示此结构体。

 

位域就是一个基本类型变量中的不同的位表示不同的含义。譬如一个硬件寄存器,假设为16 bit ,而每个bit 都可以表达不同的含义。这个时候我们用什么数据结构来表达这个寄存器呢?答案还是结构体!这时要用到结构体的高级特性,就是在基本成员变量的后面添加“: 数据位数”组成新的结构体,如下:

typedef struct tag_STRU_DD_FRMHDR

{

u32   bit8FrmFlag :8;           /* 帧头标识  */

u32   bit4SrcModuleId:4;        /* 源模块ID   */

u32   bit4DstModuleId :4;               /* 目的模块ID   */

u32   bit16Length:16;     /* 数据长度   */

} STRU_DD_FRMHDR;

 

上述结构体中的四个成员加起来只占用了一个unsigned u32 的空间。基本成员变量被拆分后,访问的方法仍然和访问没有拆分的情况是一样的。

 

1.3.2    位域的可移植性问题
在拆分基本成员变量的情况下,我们要特别注意数据的存放顺序,这还与CPU 是Big endian 还是Little endian 来决定。Little endian 和Big endian 是CPU 存放数据的两种不同顺序。对于整型、长整型等数据类型,Big endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节);而Little endian 则相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放数据的低位字节到高位字节)。

 

如我们定义IP 包头结构体为:

struct iphdr {

#if defined(__LITTLE_ENDIAN_BITFIELD)

u8 ihl:4, version:4;

#elif defined (__BIG_ENDIAN_BITFIELD)

 u8 version:4, ihl:4;

#else 

#error "Please fix <asm/byteorder.h>"

#endif

 

u8 tos; u16 tot_len; u16 id; u16 frag_off; u8 ttl; u8 protocol; u16 check; __u32 saddr; __u32 daddr;

};

 

在Little endian 模式下,iphdr 中定义:u8 ihl:4, version:4;

因为位域是从低位字节开始计算的,其存放方式为:

第1 字节低4 位 ihl ,第1 字节高4 位 version (IP 的版本号)

若在Big endian 模式下还这样定义,则存放方式为:

第1 字节低4 位 version (IP 的版本号),第1 字节高4 位 ihl

这与实际的IP 协议是不匹配的,所以在Linux 内核源代码中,IP 包头结构体的定义利用了宏来区分两种不同的情况。

#if defined(__LITTLE_ENDIAN_BITFIELD) …

#elif defined (__BIG_ENDIAN_BITFIELD) …

#endif

 

对于咱们的应用情况,此STRU_DD_FRMHDR是PowerPC和DSP交互时使用的,目前二者都为big endian模式,因此二者采用位域对数据解析时是一致的,但若一方为小端,则数据解析将出错。

 

由此我们总结位域的使用要点:

a)       C/C++ 语言的结构体支持对其中的基本成员变量按位拆分,其使用方法和结构体一致,便于编程;

b)       但要特别注意拆分后的数据的存放顺序,为了支持代码的跨平台移植,应用宏定义来区分大小端的不同数据结构。

 

1.3.3    位域设计硬件配置字
在嵌入式系统中经常要和底层硬件打交道,此时需要配置各种硬件选项,而这些选项通常都是由一个寄存器的不同位表示的,进行配置时经常要用到与或、移位等操作,编程不便,因此可以利用位域的形式来访问不同域,其和结构体的访问形式一样,非常方便。

/* struct defined for the OPTION word for EDMA - big endian */

typedef struct tag_STRU_DD_C64_EDMA_OPT

{

    u32 u32bit3Pri    : 3;        /* Priority */

    u32 u32bit2Esize  : 2;        /* Element size */

    u32 u32bit1TwoDs  : 1;       /* Source dimension */

    u32 u32bit2Sum    : 2;       /* Source address update mode */

    u32 u32bit1TwoDd  : 1;       /* Destination dimension */

    u32 u32bit2Dum    : 2;       /* Destination address update mode */

    u32 u32bit1Tcu32  : 1;        /* Transfer complete u32errupt */

    u32 u32bit4Tcc    : 4;        /* Transfer complete code */

    u32 u32bit1Rsvd1  : 1;        /* Resverd */

    u32 u32bit2Tccm   : 2;        /* Transfer complete code */

    u32 u32bit1Atcu32 : 1;         /* Alternate transfer complete u32r */

    u32 u32bit1Rsvd2  : 1;        /* Resverd */

    u32 u32bit6Atcc   : 6;        /* Alternate transfer complete code */

    u32 u32bit1Rsvd3  : 1;        /* Resverd */

    u32 u32bit1Pdts   : 1;         /* PDT mode for source */

    u32 u32bit1Pdtd   : 1;         /* PDT mode for Destination */

    u32 u32bit1Link   : 1;         /* Link */

    u32 u32bit1Fs     : 1;         /* Frame synchronization */

} STRU_DD_C64_EDMA_OPT;

 

硬件寄存器通常上电后各位为0,大多数选项都是默认的,只需配置部分非0的选项。采用位域时只会更改相关的配置项,不会影响其他位。而没有位域时需要与或等操作避免影响其他位。

 

对于硬件配置字通常定义相关常量来表示不同的配置,如下:

/* Define constants to construct EDMA option word */

/* Priority, 3bit */

#define C_DD_C64_EDMA_OPT_URGENT_PRI         (0x0)

#define C_DD_C64_EDMA_OPT_HIGH_PRI           (0x1)

#define C_DD_C64_EDMA_OPT_MEDIUM_PRI         (0x2)

#define C_DD_C64_EDMA_OPT_LOW_PRI            (0x3)

 

/* Element size, 2bit */

#define C_DD_C64_EDMA_OPT_ESIZE_32           (0x0)

#define C_DD_C64_EDMA_OPT_ESIZE_16           (0x1)

#define C_DD_C64_EDMA_OPT_ESIZE_8            (0x2)

 

1.4    通过union和struct传递不同格式报文
网络通信中进行数据包交互时通常以固定数据区传递,但每次传递的消息类型可能不同,因此固定数据区的有效数据长度可能不一样,此时需要字段标识消息类型和消息长度,以便双方对数据进行解析。

 

每种消息都有共有的消息头开始,然后是特定的消息信息,这样消息的结构将变得非常清晰,统一的格式如下:

typedef struct tag_STRU_MAT_XXX

{

    STRU_DD_MSG_HDR         struMsgHdr;                

    STRU_DD_XXX                    struXXX;                 

} STRU_MAT_XXX;

 

消息头如下:

typedef struct tag_STRU_DD_MSG_HDR

{

    u8      u8OpCode;  

    u8      u8Len;    

    u8                  au8MsgHdrData[C_DD_COMM_MSG_HEADER_DATA_BYTE_LEN];                                      

} STRU_DD_MSG_HDR;

通过消息头中的u8OpCode操作码来确定消息类型,也就确定了消息的具体内容;若对于某种消息,其有效数据长度是可变的,则u8Len将标识消息数据域的长度

 

当消息内容固定时,最好以结构体封装消息内容。

typedef struct tag_STRU_MAT_SET_BPP_TOD_SFN_TSN_REQ

{

    STRU_DD_MSG_HDR     struMsgHdr;     

    u32                 u32TodPart1;       

    u32                 u32TodPart2;        

    u16                 u16SfnPeriod;       

    u16                 u16TsnPeriod;       

    u16                 u16Sfn;         

    u16                 u16Tsn;           

} STRU_MAT_SET_BPP_TOD_SFN_TSN_REQ;

上述方式消息内容零散,同时也不便于将其另行传递给其他模块。

 

封装后的结构如下:

typedef struct tag_STRU _BPP_TOD_SFN_TSN_PARAM

{

    u32                 u32TodPart1;  

    u32                 u32TodPart2;  

    u16                 u16SfnPeriod;  

    u16                 u16TsnPeriod;   

    u16                 u16Sfn;     

    u16                 u16Tsn;   

} STRU _BPP_TOD_SFN_TSN_PARAM;

 

typedef struct tag_STRU_MAT_SET_BPP_TOD_SFN_TSN_PARAM _REQ

{

    STRU_DD_MSG_HDR         struMsgHdr;          

    STRU _BPP_TOD_SFN_TSN_ PARAM  struTimeParam;                                         

} STRU_MAT_SET_BPP_TOD_SFN_TSN_PARAM _REQ;

 

不同消息STRU_MAT_XXX长度不同,但又需要统一的数据区来存储,因此需要定义一个数据结构来统一各种消息,关键在于统一的数据区长度如何确定,其既保证可以容纳所有消息类型同时又不浪费存储空间?

 

一种方式算出各种消息占用的最大存储空间,对于具体的消息数据采用纯数组存储,其根据头部的u8OpCode确定消息类型,然后再强制转换消息数据进行解析。

typedef struct tag_STRU_DD_COMM_MSG

{

    STRU_DD_MSG_HDR     struMsgHdr;                  

    u32      au32CommData C_DD_COMM_MSG_MAX_DATA_WORD_LEN];

} STRU_DD_COMM_MSG;

 

另外一种方式是通过union在同一片内存空间存储不同格式信息,由header来分辨消息类型和长度

typedef struct tag_STRU_DD_MAT_COMM_MSG

{

    STRU_DD_MSG_HDR     struMsgHdr;             

union

{

STRU_ BPP_CUR_LOAD   struCurLoad 

STRU _BPP_TOD_SFN_TSN_ PARAM  struTimeParam; 

STRU_DD_BPP_LOAD_PARAM  struLoadParam;       

}unionMsgData;                  

} STRU_DD_MAT_COMM_MSG;

各种具体的消息共用一块固定长度内存,取决于最大的消息大小

 

不用根据各种消息类型算C_DD_COMM_MSG_MAX_DATA_WORD_LEN的值,消息类型的改变不影响tag_STRU_DD_MAT_COMM_MSG,可以自适应消息类型的变化;但添加新消息类型时需要更改STRU_DD_MAT_COMM_MSG。

 

在传输链路上进行数据传输时并不是传递STRU_DD_MAT_COMM_MSG,而是传递具体的消息头和消息内容,因为有效信息可能并没有这 么多数据,这样可以节省传输时间。此时可用sizeof(STRU_MAT_XXX)或者sizeof(STRU_DD_MSG_HDR) + sizeof(STRU_DD_XXX)

 

1.5    将相关功能变量封装为结构体
何谓相关?是指多个不同意义的变量任何时候都是统一定义、统一初始化、统一传递给其他模块,就好像难兄难弟一样任何时候都在一块,此时就应该将这些变量组织为结构体,优势在于:

a)       便于管理、定义、声明,避免零散的变量;

b)       意义明确,结构清晰;

c)       函数调用时避免传递过多参数,提高调用性能,参数少不易出错。

不足在于对于结构体的访问效率不如单独的变量,但此性能影响很小;为了代码更好的可读性、可移植可维护性性和可靠性,此处结构体的形式更合适。

 

以测试HPI 和DPRAM 发送或者接收时间的统计变量数据结构为例进行说明

typedef struct tag_STRU_TEST_TX_RX_TIME_STATS

{

    u32          u32Start;        /* 发送或者接收的起始点, 以Cycle为单位 */

    u32          u32TimeGap;      /* 发送或者接收时间,以Cycle为单位 */

    u32          u32TimeGapMax;   /* 发送或者接收的最长时间,以Cycle为单位 */

} STRU_TEST_TX_RX_TIME_STATS;

 

对于必须初始化的结构体变量有两种方式进行初始化:

1)       定义时候采用“{}”对各个成员变量赋初始值,如

STRU_TEST_TX_RX_TIME_STATS   struTxRxTime =  {0,0,0}

但是当结构体的成员顺序调整或者增减成员变量时,原有的初始化赋值代码全部需要修改,缺乏可裁减性和可修改性。

2)       对结构体成员分别赋值,如:

struTxRxTime.u32Start = 0;

struTxRxTime.u32TimeGap= 0;

struTxRxTime.u32TimeGapMax= 0;

当结构体成员顺序改变时,上述代码不会受到影响,但增减成员变量时,此处的代码也需要修改,缺乏可裁减性。

 

为了提高此结构体的可移植性,需要提供针对此类变量的各种操作接口,包括初始化、更新维护等。

a)       提供静态定义和动态初始化该数据结构的宏接口:

为了防止用户在定义此类结构体变量时未初始化,提供静态定义宏,采用此宏定义变量就可确保其经过初始化了。因此用户应避免直接采用该数据结构来定义变量。

/* 静态定义STRU_TEST_TX_RX_TIME_STATS 类型变量*/

#define  DD_DECLARE_TX_RX_TIME_STATS(struTxRxTime)  STRU_TEST_TX_RX_TIME_STATS   struTxRxTime =  {0,0,0}

 

但是对于已经定义了的或者是在堆中申请的该类变量,上述静态定义宏就无能为力了,所以还需要提供动态初始化的宏定义接口:

/* 动态初始化已经定义的STRU_TEST_TX_RX_TIME_STATS 类型变量*/

#define DD_INIT_TX_RX_TIME_STATS(pstruTxRxTime)    do{ \

 (pstruTxRxTime)->u32Start = 0; \

 (pstruTxRxTime)->u32TimeGap = 0; \

 (pstruTxRxTime)->u32TimeGapMax = 0; \

     }while(0)

在C中,对于函数参数避免对结构体进行值传递,因为没有C++中的拷贝构造函数,简单的值传递可能造成异常,因此参数肯定是该结构体类型的指 针。那么对于函数宏参数呢?此时并不需要传递变量,但为了动态初始化函数能操作全局、栈以及堆中分配的该类型变量,其参数应为指针形式。

 

此时当数据结构类型发生变化时,只需要修改相关的宏接口即可,其他调用此类宏接口的相关代码无需任何修改,提高了代码的可移植性。

 

b)       设置发送或者接收的起始时间

#define  DD_SET_TIME_STATS_START(pstruTxRxTime)    (pstruTxRxTime)->u32Start = (*C_DD_C64_TIMER2_CNT)

乘以C_DD_C64_TIMER_CLK_DIVIDER后,就可能溢出了,因此u32Start只能以定时器timer个数为单位;因为“.”“->”优先级较高,因此应将pstruTxRxTime参数用括号扩起来,这也是宏参数的基本规则。

 

c)       获得发送或者接收的时间,并更新最大时间

#define  DD_UPDATE_TIME_STATS(pstruTxRxTime)           do{ \

 (pstruTxRxTime)->u32TimeGap = ((*C_DD_C64_TIMER2_CNT )  - (pstruTxRxTime)->u32Start) * C_DD_C64_TIMER_CLK_DIVIDER; \

                                                                                    (pstruTxRxTime)->u32TimeGapMax = (((pstruTxRxTime)->u32TimeGap) > ((pstruTxRxTime)->u32TimeGapMax)) ? ((pstruTxRxTime)->u32TimeGap) : ((pstruTxRxTime)->u32TimeGapMax); \

     }while(0)       

 

在咱们的代码中还有多处此类封装,如下:

将通道收发正确错误统计计数综合为一个数据结构,具有一定的共性,以供利用HPI 、DPRAM 及McBSP的模块使用。

/* HPI 、DPRAM 及McBSP 等通道收发统计变量数据结构 */

typedef struct tag_STRU_TX_RX_CNT_STATS

{

    u32          u32RxOkCnt;     /* 接收正确的次数*/

    u32          u32RxErrCnt;     /* 接收错误的次数*/

   

    u32          u32TxOkCnt;     /* 发送正确的次数*/

    u32          u32TxErrCnt;     /* 发送错误的次数*/

} STRU_TX_RX_CNT_STATS;

 

/* 中断统计变量数据结构 */

typedef struct tag_STRU_ISR_TIME_STATS

{

    u32  u32IsrStartTestFlag;     /* 开始测量中断时间标志,获取u32IsrPrevCounter*/

    u32  u32IsrTotalCnt;         /* 该中断发生的总次数*/

    u32   u32IsrPrevCounter;     /* 上次发生中断时timer2的计数值*/

 

    u32  u32IsrCurGapTime;     /* 两次中断的间隔时间,单位CPU cycle*/

    u32  u32IsrMinGapTime;      /* 两次中断的最小间隔时间,单位CPU cycle*/

    u32  u32IsrMaxGapTime;     /* 两次中断的最大间隔时间,单位CPU cycle*/

} STRU_ISR_TIME_STATS;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值