最佳通用做法I2C寄存器映射

时间:2012-11-20 14:54:59

标签: c i2c

只是想知道在C中使用I²C寄存器映射的最佳做法是什么,或者更确切地说是其他人经常使用/更喜欢的。

到目前为止,我经常做很多定义,一个用于每个寄存器,一个用于所有位,掩码,移位等。 但是,最近我看到一些驱动程序使用(可能是打包的)结构而不是定义。我认为这些是Linux内核模块。

无论如何,他们会

struct i2c_sensor_fuu_registers {

    uint8_t  id;
    uint16_t big_register;
    uint8_t  another_register;
    ...

} __attribute__((packed));

然后他们使用offsetof(或宏)来获取i2c寄存器,并使用sizeof作为要读取的字节数。

我发现这两种方法都有其优点:

结构方法:

  • (+)寄存器偏移在逻辑上都包含在结构中,而不必在定义中拼写每个寄存器。
  • (+)使用适当大小的数据类型明确说明条目大小。
  • ( - )这不考虑广泛使用的位字段
  • ( - )这不考虑非字节映射的寄存器映射(例如LM75),其中一个从偏移量n + 0x00读取2个字节,而n + 0x01是另一个寄存器,而不是高/低字节寄存器n + 0x00
  • ( - )这不会解释地址空间中的大间隙(例如寄存器在0x00,0x01,0x80,0xAA,没有中间...)和(我认为?)依靠编译器优化来摆脱结构。

定义方法:

  • (+)每个寄存器及其位通常在一个块中定义,使得找到正确的符号变得容易并依赖于命名约定。
  • (+)透明/不知道地址空间间隙。
  • ( - )每个寄存器必须单独定义,即使没有间隙
  • ( - )因为定义往往是全局的,所以名称通常很长,有点乱丢几十个长符号名称的源代码。
  • ( - )要读取的数据大小通常是硬编码的幻数或(end-start + 1)样式计算,可能有很长的符号名称。
  • (o)透明/不知道数据大小与地图中的地址。

基本上,我正在寻找一种更智能的方法来处理这些情况。我经常发现自己为每个寄存器和每个位输入了许多令人痛苦的长符号名称,并且可能还有掩码和移位(后两种取决于数据类型),最后只使用其中几个(但讨厌稍后重新定义缺失的符号,这就是我在一个会话中键入所有内容的原因)。 不过,我注意到读/写字节的大小大多是幻数,通常需要并排读取数据表和源代码才能理解最基本的交互。

我想知道其他人如何应对这种情况?我在网上找到了一些例子,人们也在一个大标题中粗略地输入每一个寄存器,位等等,但没有什么是明确的......但是,上面两个选项中的任何一个看起来都不太明智:(

2 个答案:

答案 0 :(得分:2)

警告:此处描述的方法使用位域,其在内存中的排列是特定于实现的。如果这样做,请确保您知道编译器在这方面的工作原理。

正如您所指出的,每种方法都有优点和缺点。我喜欢混合方法。您可以定义寄存器偏移,但是然后使用结构作为内容,使用union来指定位或整个寄存器。在联合内部,使用正确的大小变量作为寄存器的大小(正如您所提到的,有时它们不是字节可寻址的)。你不需要那么多的定义,你不太可能弄乱位移并且不需要掩码。例如:

#define unsigned char u8;
#define unsigned short u16;

#define CTL_REG_ADDR  0x1234
typedef union {
  struct { 
    u16 not_used:10; //top 10 bits ununsed
    u16 foo_bits:3;  //a multibit register
    u16 bar_bit:1;   //just one bit
    u16 baz_bits:2;  //2 more bits
  } fields;
  u16 raw;
} CTL_REG_DATA;

#define STATUS_REG_ADDR 0x58
typedef union {
  struct { 
    u8 bar_bits:4;  //upper nibble
    u8 baz_bits:4;  //lower nibble
  } fields;
  u8 raw;
} STATUS_REG_DATA;

//use them like the following
u16 readregister(u16);
void writeregister(u16,u16);

CTL_REG_DATA reg;
STATUS_REG_DATA rd;
rd = readregister(STATUS_REG_ADDR);
if (rd.fields.bar_bit) {
   reg.raw = 0xffff;        //set every bit
   reg.fields.bar_bit = 0;  //but clear this one bit
   writeregister(CTL_REG_ADDR, reg);
}

答案 1 :(得分:1)

在我的理想世界中,硬件设计人员将提供与C ++,C和ASM兼容的头文件。一个是基于实际硬件寄存器自动生成的。通过#defines(对于ASM)和typedef'd结构(对于C和C ++)定义每个寄存器和位/字段的一个。一个表示每个位和字段的访问属性(只读,只写,写清除等)。其中包括定义每个寄存器的用途和用途及其位/字段的注释。它还需要考虑目标字节序和编译器,以确保正确排序任何寄存器和位域。

我在以前的工作中尽可能接近这个理想。我编写了一个脚本来解析寄存器描述文件(我定义的格式)并自动生成一个完整的头(结构和#defines)以及一个函数来转储所有可读的寄存器以进行调试。我在其他公司看到了类似的方法,但没有一种方法能够达到这种程度。

我要指出,如果使用typedef结构来定义寄存器布局,那么您可以轻松地解释定义中的大寄存器间隙。例如只需添加“reserved [80]”或“unused [94]”或“unmplemented [2044]”或“gap [42]”数组元素来定义间隙。无论如何,您总是将结构定义用作指向硬件基址的指针,因此它不会占用内存中任何位置的实际结构大小。

希望有所帮助。