Best general practice I2C registration card

It’s just interesting what is the best practice regarding I²C to C registration cards or rather what other people often use / prefer.

Until that moment, I usually made a lot of definitions, one for each register and one for all bits, masks, shifts, etc. However, lately I have seen some drivers use (possibly packaged) structures instead of specific ones. I think these were Linux kernel modules.

Anyway, they

struct i2c_sensor_fuu_registers { uint8_t id; uint16_t big_register; uint8_t another_register; ... } __attribute__((packed)); 

They will then use offsetof (or the macro) to get the i2c register, and use sizeof for the number of bytes read.

I believe that both approaches have their advantages:

structural approach:

  • (+) The register offsets are all logically contained within the structure instead of writing each register in the definition.
  • (+) Input size is explicitly specified using a data type of the appropriate size.
  • (-) This does not take into account commonly used bit fields
  • (-) This does not take into account register cards that are not mapped to bytes (for example, LM75), where one reads 2 bytes from offset n + 0x00, but n + 0x01 is a different register, not the high / low byte of register n + 0x00
  • (-) This does not take into account large spaces in the address space (for example, registers 0x00, 0x01, 0x80, 0xAA, no in-betweens ...) and (I think?) Relies on optimizing the compiler to get rid of the structure.

determine the approach:

  • (+) Each of the registers together with its bits is usually defined in a block, which makes it easy to find the correct character and rely on a naming convention.
  • (+) Transparent / unaware of spaces in the address space.
  • (-) Each of the registers must be individually defined, even if there are no spaces
  • (-) Since definitions tend to be global, names are usually very long, clogging up the source code with dozens of long symbol names.
  • (-) The sizes of the data being read are usually either encoded magic numbers or (end - beginning + 1), or possibly long symbol names.
  • (o) Transparent / unconscious data size and address on the map.

Basically, I'm looking for a smarter way to handle these cases. I often find that I type many, many painfully long symbol names for each register, and each bit and possibly disguises and shifts (the last two depending on the data type), but simply ending up using only a few of them (but hating to redefine the missing characters later, so I print everything in one session). However, I notice that the read / write byte sizes are mostly magic numbers and usually read data and source code side by side to understand even the most basic interaction.

I wonder how other people deal with such situations. I found several examples on the Internet where people also printed every single register, bit, etc. In a large heading, but nothing completely final ... However, none of the two options above seems too smart at the moment :(

+4
source share
2 answers

WARNING : the method described here uses bit fields whose location in memory is implementation-specific. If you do, be sure to know how your compiler works in this regard.

As you noted, for each method there are advantages and disadvantages. I like the hybrid approach. You can define register offsets, but then use a structure for the contents and a union to indicate bits or the entire register. Inside the union, use the correct size variable for the register size (as you mentioned, sometimes they cannot be addressed by byte). You do not need as much as it determines, and you are less likely to spoil the bit shifts and do not need masks. For instance:

 #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
source

In my ideal world, the hardware developer provided a header file compatible with C ++, C, and ASM. One that was automatically generated based on real hardware registers. One that defined each register and bit / field through both the #defines (for ASM) and typedef'd (for C and C ++) commands. One that specified access properties for each bit and field (read-only, write-only, write-clear, etc.). One of them included comments defining the use and purpose of each register and its bits / fields. In addition, the target and the compiler will need to be considered to ensure that all registers and bit fields are ordered correctly.

I came as close as possible to this ideal in a previous job. I wrote a script that will analyze the register description file (of a specific format) and automatically generate the full header (structures and #defines), as well as a function to reset all readable registers for debugging purposes. I have seen similar approaches in other companies, but not one of them has accepted it to this extent.

I will point out that if you use the typedef structure to define the layout of the register, you can easily take into account large spaces in the register in the definition. for example Just add an element of the array “reserved [80]” or “unused [94]” or “unrealized element [2044]” or “gap [42]” to define a space. You will always use the structure definition as a pointer to the base address of the device, so that it will not occupy the actual size of the structure at any point in memory.

Hope this helps.

+1
source

Source: https://habr.com/ru/post/1447245/


All Articles