5

只是想知道在 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) 透明/不知道数据大小与地图中的地址。

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

我想知道其他人如何处理这些情况?我在网上找到了一些示例,其中人们还费力地在一个大标题中输入了每个寄存器、位等,但没有什么非常确定的……但是,在这一点上,上面的两个选项似乎都不太聪明:(

4

2 回答 2

2

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

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

#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);
}
于 2012-11-20T16:22:48.953 回答
2

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

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

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

希望有帮助。

于 2012-11-20T20:43:28.560 回答