最佳通用实践I2C寄存器映射

时间:2021-09-23 21:14:35

Just wondering what the best practice regarding I²C register maps in C or rather what other people use often/prefer.

想知道关于我²C的最佳实践在C或注册地图,而别人经常使用/喜欢。

Up to this point, I have usually done lots of defines, one for every register and one for all the bits, masks, shifts etc. However, lately I've seen some drivers use (possibly packed) structs instead of defined. I think these were Linux kernel modules.

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

Anyway, they would

无论如何,他们将

struct i2c_sensor_fuu_registers {

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

} __attribute__((packed));

Then they'd use offsetof (or a macro) to get the i2c register and use sizeof for the number of bytes to read.

然后使用offsetof(或宏)获取i2c寄存器,并使用sizeof表示要读取的字节数。

I find that both approaches have their merit:

我发现这两种方法都有各自的优点:

struct approach:

结构体的方法:

  • (+) Register offsets are all logically contained inside a struct instead of having to spell each register out in a define.
  • (+)寄存器偏移量在逻辑上都包含在结构体中,而不必在定义中拼写每个寄存器。
  • (+) Entry sizes are explicitly stated using a data type of appropriate size.
  • (+)使用适当大小的数据类型显式地声明条目大小。
  • (-) This doesn't account for bit fields which are widely used
  • (-)这并不包括广泛使用的位字段
  • (-) This doesn't account for register maps that aren't byte mapped (e.g. LM75), where one reads 2 bytes from offset n+0x00, yet n+0x01 is another register, not the high/low byte of register n+0x00
  • (-)这并不表示没有字节映射的寄存器映射(例如LM75),其中一个从偏移量n+0x00读取2个字节,而n+0x01是另一个寄存器,而不是寄存器n+0x00的高/低字节
  • (-) This doesn't account for large gaps in address space (e.g. registers at 0x00, 0x01, 0x80, 0xAA, no in-betweens...) and (I think?) relies on compiler optimization to get rid of the struct.
  • (-)这并不能解释地址空间的大空白(例如,在0x00、0x01、0x80、0xAA、没有中间…)和(我认为?)依赖于编译器优化来除去结构。

define approach:

定义的方法:

  • (+) Each of the registers along with its bits is usually defined in a block, making finding the right symbol easy and relying on a naming convention.
  • (+)每个寄存器及其位通常在块中定义,使查找正确的符号变得容易,并依赖于命名约定。
  • (+) Transparent/unaware of address space gaps.
  • (+)透明/不知道地址空间的差距。
  • (-) Each of the registers have to be defined individually, even when there are no gaps
  • (-)每个寄存器都必须单独定义,即使没有间隙
  • (-) Because defines tend to be global, the names are usually very long, somewhat littering the source code with dozens of long symbol names.
  • (-)因为定义往往是全局的,所以名称通常很长,在源代码中有些地方用几十个长符号名称。
  • (-) Sizes of data to read are usually either hard-coded magic numbers or (end - start + 1) style computations with possibly long symbol names.
  • (-)要读取的数据大小通常是硬编码的魔术数字或(end - start + 1)风格的计算,可能包含很长的符号名称。
  • (o) Transparent/unaware of data size vs. address in map.
  • (o)透明/不知道地图中的数据大小和地址。

Basically, I'm looking for a smarter way to handle these cases. I often find myself typing lots and lots of agonizingly long symbol names for each and every register and each bit and possibly masks and shifts (latter two depending on data type) as well, just to end up using just a few of them (but hating to redefine missing symbols later on, which is why I type all in one session). Still, I notice that sizes of bytes to read/write are mostly magic numbers and usually reading the datasheet and source code side-by-side is required to understand even the most basic interaction.

基本上,我正在寻找一种更聪明的方式来处理这些案件。我经常发现自己打字很多苦闷地长的符号名称为每一个寄存器和每一位可能面具和变化(后两个根据数据类型),最后使用只是其中的一些(但讨厌重新定义缺少符号之后,这就是为什么我类型在一个会话)。不过,我注意到,读/写的字节大小大多是神奇的数字,而且通常需要并排读取数据表和源代码,才能理解最基本的交互。

I wonder how other people handle these kinds of situations? I found some examples online where people also arduously typed every single register, bit etc. in a big header, but nothing quite definitive... However, neither of the two options above seems too smart at this point :(

我想知道其他人是如何处理这种情况的?我在网上找到了一些例子,在这个例子中,人们还会在一个大标题中对每一个注册表、bit等进行严格的分类,但没有什么是非常确定的。然而,上述两种选择在这一点上似乎都不太明智:(

2 个解决方案

#1


2  

WARNING: The method described here uses bitfields, whose arrangement in memory is implementation specific. If you do this, make sure you know how your compiler works in this regard.

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

As you point out, there are advantages and disadvantages to each method. I like a hybrid approach. You can define register offsets, but then use a struct for the contents and a union to specify the bits or the entire register. Inside the union, use the correct size variable for the size of the register (as you mentioned sometimes they're not byte addressable). You don't need quite as many defines, and you're less likely to mess up bit shifts and don't need masks. For example:

正如你所指出的,每种方法都有优缺点。我喜欢混合的方法。您可以定义寄存器偏移量,然后使用一个struct来定义内容和一个联合来指定位或整个寄存器。在union内部,对寄存器的大小使用正确的size变量(正如您所提到的,有时它们不是字节可寻址的)。你不需要太多的定义,你也不太可能搞砸一些轮班,也不需要面具。例如:

#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);
}

#2


1  

In my ideal world, the hardware designer would supply a header file compatible with C++, C, and ASM. One that was auto-generated based on the actual hardware registers. One that defined every register and bit/field via both #defines (for ASM) and typedef'd structures (for C and C++). One that indicated the access properties of every bit and field (read-only, write-only, write-clear, etc.). One that included comments defining the use and purpose of each register and its bits/fields. It would also need to account for target endianness and compiler, to make sure any registers and bitfields were ordered correctly.

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

I got as close to this ideal as I could at a previous job. I wrote a script that would parse a register description file (of a format I defined) and auto-generate a full header (structures and #defines) as well as a function to dump all the readable registers for debugging purposes. I've seen similar approaches at other companies, but none that took it to that extent.

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

I'll point out that if you use a typedef struct to define your register layout then you can easily account for large register gaps in the definition. e.g. Just add a "reserved[80]" or "unused[94]" or "unimplemented[2044]" or "gap[42]" array element to define the gap. You'll always use the struct definition as a pointer to the hardware base address anyway, so it won't take up the actual size of the struct anywhere in memory.

我将指出,如果您使用typedef struct来定义您的寄存器布局,那么您可以很容易地在定义中考虑到较大的寄存器间隔。只需添加一个“预留[80]”或“未使用[94]”或“未实现[2044]”或“gap[42]”数组元素来定义间隙。无论如何,您都将使用struct定义作为指向硬件基地址的指针,因此它不会占用内存中任何地方的结构体的实际大小。

Hope that helps.

希望有帮助。

#1


2  

WARNING: The method described here uses bitfields, whose arrangement in memory is implementation specific. If you do this, make sure you know how your compiler works in this regard.

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

As you point out, there are advantages and disadvantages to each method. I like a hybrid approach. You can define register offsets, but then use a struct for the contents and a union to specify the bits or the entire register. Inside the union, use the correct size variable for the size of the register (as you mentioned sometimes they're not byte addressable). You don't need quite as many defines, and you're less likely to mess up bit shifts and don't need masks. For example:

正如你所指出的,每种方法都有优缺点。我喜欢混合的方法。您可以定义寄存器偏移量,然后使用一个struct来定义内容和一个联合来指定位或整个寄存器。在union内部,对寄存器的大小使用正确的size变量(正如您所提到的,有时它们不是字节可寻址的)。你不需要太多的定义,你也不太可能搞砸一些轮班,也不需要面具。例如:

#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);
}

#2


1  

In my ideal world, the hardware designer would supply a header file compatible with C++, C, and ASM. One that was auto-generated based on the actual hardware registers. One that defined every register and bit/field via both #defines (for ASM) and typedef'd structures (for C and C++). One that indicated the access properties of every bit and field (read-only, write-only, write-clear, etc.). One that included comments defining the use and purpose of each register and its bits/fields. It would also need to account for target endianness and compiler, to make sure any registers and bitfields were ordered correctly.

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

I got as close to this ideal as I could at a previous job. I wrote a script that would parse a register description file (of a format I defined) and auto-generate a full header (structures and #defines) as well as a function to dump all the readable registers for debugging purposes. I've seen similar approaches at other companies, but none that took it to that extent.

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

I'll point out that if you use a typedef struct to define your register layout then you can easily account for large register gaps in the definition. e.g. Just add a "reserved[80]" or "unused[94]" or "unimplemented[2044]" or "gap[42]" array element to define the gap. You'll always use the struct definition as a pointer to the hardware base address anyway, so it won't take up the actual size of the struct anywhere in memory.

我将指出,如果您使用typedef struct来定义您的寄存器布局,那么您可以很容易地在定义中考虑到较大的寄存器间隔。只需添加一个“预留[80]”或“未使用[94]”或“未实现[2044]”或“gap[42]”数组元素来定义间隙。无论如何,您都将使用struct定义作为指向硬件基地址的指针,因此它不会占用内存中任何地方的结构体的实际大小。

Hope that helps.

希望有帮助。