Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1350502
  • 博文数量: 281
  • 博客积分: 8800
  • 博客等级: 中将
  • 技术积分: 3346
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-17 22:31
文章分类

全部博文(281)

文章存档

2013年(1)

2012年(18)

2011年(16)

2010年(44)

2009年(86)

2008年(41)

2007年(10)

2006年(65)

我的朋友

分类: LINUX

2006-08-16 20:11:22

在嵌入式应用中,或许你对offsetof接ゲ欢嗌踔粮?久患??H绻?钦庋??敲创诱庖豢唐鹁秃煤玫卣莆账??盟?晌?愕挠忠簧笔诛蛋伞?br>1. offsetof与EEPROM
我们许多人可能都使用过一些非挥发性的存储器,如常见的EEPROM。我们经常使用它们在存储一些系统的配置参数和设备信息。在所有的EEPROM中,通过串口访问的占了大多数。一般来说,对串口的访问都是按字节进行的,这使得我们不可避免会设计出下面的
接口去访问EEPROM的信息:

/*从EEPROM 偏移量offset处读取nBytes到RAM地址dest*/
ee_rd(uint16_t offset, uint16_t nBytes, uint8_t * dest);

然而,这种接口必须要知道偏移量offset和读取字节数nBytes。可能你会采用下面的方法解决方法解决这个问题:
定义一个数据结构和一个指向这个数据结构的指针,并初始化这个指针为EEPROM的起始地址EEPROM_BASE.
---------------------------- <-EPPROM_BASE:0x0000000
| i | f | c |   |   |   |...
----------------------------      
|   |   |   |   |   |   |...
----------------------------      
|   |   |   |   |   |   |...
----------------------------      
...
----------------------------

#define EEPROM_BASE 0x0000000/*配置信息的起始地址*/
typedef struct
{  
  int   i;
  float f;
  char   c;
} EEPROM;

EEPROM * const pEE = EEPROM_BASE

ee_rd(&(pEE->f), sizeof(pEE->f), dest);

没错,这种方法的确可以达到访问指定地址的信息。不过这种方法也存在下面的问题:
a.容易使代码维护人员人误以为在ee_rd接口内部也存在EEPROM的数据结构。
b.当你编写一些自己感觉良好编译器不报错的代码,比如pEE->f = 3.2,你可能意想不到灾难将要来临。
c.这个接口没有很好地体现EEPROM所隐含的硬件特性。

到这里,有人可能会想到offsetof(那些没用过甚至没见过的朋友别急,后面马上会详解offsetof)来解决这个问题:

/*offsetof获取数据成员在数据结构中的偏移量
比如成员f在EEPROM数据结构中的偏移量,这里为什么
要强制转化0,这是个有深度的问题,在后面也会详细说明*/
#define offsetof(type, f) ((size_t) \
  ((char *)&((type *)0)->f - (char *)(type *)0))

typedef struct
{
  int   i;
  float f;
  char   c;
} EEPROM;

ee_rd(offsetof(EEPROM,f), 4, dest);

如果你能想到这里说明你对offsetof有一定程度的理解,不过还可以改进。如果让编译器来计算nBytes而不是我们自己给出4那就更好了。这时,一定有人会马上提到sizeof。是的。可是怎么使用呢,我们不能用sizeof(EEPROM.f)来计算nBytes吧?!我想那些对offsetof有较深理解的同志一定会这么办:

/*类似于offsetof的定义*/
#define SIZEOF(s,m) ((size_t) sizeof(((s *)0)->m))

ee_rd(offsetof(EEPROM, f), SIZEOF(EEPROM, f), &dest);

很不错! 其实还可以精简为下面的最终形式:
#define EE_RD(M,D)   ee_rd(offsetof(EEPROM,M), SIZEOF(EEPROM,M), D)
EE_RD(f, &dest);

哈哈,这样我们只用传递两个参数,不用再考虑应该从那里读取数据以及读取多少的问题。
先打住,有人会说这种简化都是建立在EEPROM_BASE为0x0000000基础之上的,可能会反问,如果配置信息不是从0地址开始的呢?

Good question.其实我们可以通过下面的方法解决。
#define EEPROM_BASE 0x00000a10

typedef struct
{
  char   pad[EEPROM_BASE];/*使数据结构的前EEPROM_BASE个字节填"空"*/
  int   i;
  float f;
  char   c;
} EEPROM;

---------------------------- 0x00000000
|   |   |   |   |   |   |...
----------------------------  
...
---------------------------- <-EPPROM_BASE:0x00000a10        
| i | f | c |   |   |   |...
----------------------------      
|   |   |   |   |   |   |...
----------------------------      
...

使用offsetof简化EEPROM的串口访问的确很妙。这里还有一个很好的例子。
在嵌入式应用中,我们时常将一些I/O寄存器映射到内存地址空间进行访问。
这种映射使原本复杂的寄存器访问变得象访问普通的RAM地址一样方便。
在我们视频会议系统中,PowerPC 8250访问外部的ROM控制器(ROM controller)的
寄存器就是通过这种方式实现的。ROM控制器所有的寄存器被映射到从I/O寄存器空间基地址0x10000000(IO_BASE)偏移0x60000(ROMCONOffset)字节的一段内存。每个寄存器占用四个字节,并有一个数据结构与它们对应。比如控制ROM控制器工作状态的寄存器对应数据结构ROMCON_ROM_CONTROL,配置PCI总线A的寄存器对应数据结构ROMCON_CONFIG_A,下面先看看这些数据结构的定义:

#define IO_BASE     0x10000000
#define ROMCONOffset 0x60000

typedef unsigned int NW_UINT32;

typedef struct _ROMCON_CONFIG_A {
  union {
    struct {
        UINT32 pad4:21;       /* unused   */
        UINT32 pad3:2;       /* reserved */
        UINT32 pad2:5;       /* unused   */
        UINT32 EnablePCIA:1;
        UINT32 pad1:1;       /* reserved */
        UINT32 EnableBoot:1;      
        UINT32 EnableCpu:1;   /*bit to enable cpu*/
    } nlstruct;
    struct {
        UINT32 ConfigA;
    } nlstruct4;
  } nlunion;
} ROMCON_CONFIG_A, *PROMCON_CONFIG_A;

typedef struct _ROMCON_ROM_CONTROL {
  union {
    struct {
        UINT32 TransferComplete:1;
        UINT32 pad3:1;         /* unused */
        UINT32 BondPad3To2:2;
        UINT32 Advance:3;
        UINT32 VersaPortDisable:1;
        UINT32 pad2:1;         /* unused */
        UINT32 FastClks:1;
        UINT32 pad1:7;         /* unused */
        UINT32 CsToFinClks:2;
        UINT32 OeToCsClks:2;
        UINT32 DataToOeClks:2;
        UINT32 OeToDataClks:3;
        UINT32 CsToOeClks:2;
        UINT32 AddrToCsClks:2;      
        UINT32 AleWidth:2;
    } nlstruct;
    struct {
        UINT32 RomControl;
    } nlstruct4;
  } nlunion;
} ROMCON_ROM_CONTROL, *PROMCON_ROM_CONTROL;

typedef struct
{
  ROMCON_CONFIG_A   ConfigA;
  ROMCON_CONFIG_B   ConfigB;
  ROMCON_ROM_CONTROL RomControl;
  ...
}ROMCON, *PROMCON;

---------------------------- <-IO_BASE:0x10000000  
|   |   |   |   |   |   |...
----------------------------      
|   |   |   |   |   |   |...
...
---------------------------- <-ROMCONOffset(ROMCON):0x60000    
|   |   |   |   |   |   |...
---------------------------- <-ROMCON_ROM_CONTROL        
...
----------------------------

那么如何访问ROMCON_ROM_CONTROL对应寄存器呢,比如ROMCON_ROM_CONTROL对应寄存器的VersaPortDisable位?
估计有人可能会这样做:
事先定义成员RomControl(ROMCON中用ROMCON_ROM_CONTROL定义的实例)相对与ROMCON的偏移量,
#define ROMCONRomControlOffset 0x8

然后设计访问ROM的接口如下:
/*读取ROM控制器位于src位置的寄存器数据到dest*/
typedef unsigned long dword_t;

void rom_read(dword_t* src, uint32_t* dest);
void rom_write(dword_t* src, uint32_t* dest);

最后利用这个偏移量做下面的操作:
ROMCON_ROM_CONTROL tRomCtrl={0};
dword_t* pReg=(dword_t*)(IO_BASE+ROMCONOffset+\
  ROMCONRomControlOffset);

rom_read(pReg,(uint32_t)*(&tRomCtrl));

/*查看寄存器的VersaPortDisable位,如果该位没有启用就启用它*/
if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)
{
tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;
rom_write(pReg,(uint32_t)*(&tRomCtrl));
}

这样做确实可以达到访问相应寄存器的目的。但是,如果和ROM相关的寄存器很多,那么定义、记忆和管理那么多偏移量不是很不方便吗?到这里,如果你对前面关于offsetof还有印象的话,我想你可能会作下面的优化:
#define ROMCON_ADDR(m)   (((size_t)IO_BASE+\
                (size_t)ROMCONOffset+\
                (size_t)offsetof(ROMCON,m))

ROMCON_ROM_CONTROL tRomCtrl={0};
dword_t* pReg=(dword_t*)ROMCON_ADDR(ConfigA);

rom_read(pReg,(uint32_t)*(&tRomCtrl));

/*查看寄存器的VersaPortDisable位,如果没有启动就启动它*/
if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)
{
tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;
rom_write(pReg,(uint32_t)*(&tRomCtrl));
}

2.offsetof的来龙去脉
通过前面的举例,你可能对如何使用offsetof已经不陌生了吧。offsetof对那些搞
C++的人可能很熟悉,因为offsetof类似于sizeof,也是一种系统操作符,你不用考虑它是怎么定义的。这个操作符offsetof的定义可以在ANSI C 编译器所带的stddef.h中找到。在嵌入式系统里,不同开发商,不同架构处理器和编译器都有不同的offsetof定义形式:
/* Keil 8051 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)

/* Microsoft x86 */
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)

/* Motorola coldfire */
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))

/* GNU GCC 4.0.2 */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)

虽然定义形式不同,但功能都是返回成员在数据结构中的偏移量,都是为了提高代码的可移植性。

下面拿KEIL 8051的定义来作点解释:
((s *)0):强制转化成数据结构指针,并使其指向地址0;
((s *)0)->m:使该指针指向成员m
&(((s *)0)->m):获取该成员m的地址
(size_t)&(((s *)0)->m):转化这个地址为合适的类型

你可能会迷惑,这样强制转换后的结构指针怎么可以用来访问结构体字段?呵呵,其实这个表达式根本没有也不打算访问m字段。ANSI C标准允许任何值为0的常量被强制转换成任何一种类型的指针,并且转换结果是一个NULL指针,因此((s*)0)的结果就是一个类型为s*的NULL指针。如果利用这个NULL指针来访问s的成员当然是非法的,但&(((s*)0)->m)的意图并非想存取s字段内容,而仅仅是计算当结构体实例的首址为((s*)0)时m字段的地址。聪明的编译器根本就不生成访问m的代码,而仅仅是根据s的内存布局和结构体实例首址在编译期计算这个(常量)地址,这样就完全避免了通过NULL指针访问内存的问题。又因为首址的值为0,所以这个地址的值就是字段相对于结构体基址的偏移。

这里有个地方需要注意:就是offsetof虽然同样适用于union结构,但它不能用于计算位域(bitfield)成员在数据结构中的偏移量。

typedef struct
{
unsigned int a:3;
unsigned int b:13;
unsigned int c:16;
}foo;

使用offset(foo,a)计算a在foo中的偏移量,编译器会报错;
阅读(1478) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~