lang="en-GB"> Embedded C: Struct and Union (Part 2) - Atadiat

Embedded C: Struct and Union (Part 2)

Usage Cautions and Real-life Applications

The first part of this article has introduced the concepts behind Structs and Unions, and how to define and use each either independently, or even interleaved .i.e. a Union inside a Struct. In this part, we’re going to see useful applications of Struct and Union in Embedded C/C++, like how to use them to map registers to access its fields and we are going to discuss some pros and cons of bit fields.

Bit Fields in A Nutshell

Bit fields are designed specifically to reduce the needed memory amount to a minimum, where the same memory location can be divided to “bit fields” instead of having a dedicated location for every bit field. To declare a bit field inside a Struct, use the “:” operator followed by the number of bits as an integer value.
typedef struct foo
{
       unsigned char a:4;
       unsigned char b:3;
       unsigned char c:1;
} foo_t;

What bit fields do, is basically masking bitwise operations to access the value of its fields. They are actually memory addresses with specific lengths (foo occupies 1 Byte). To examine this, we will disassemble an (.elf) file after compiling a c++ code for an AVR MCU (Arduino Sketch). The assembly code shows how to access a value inside “foo”:

lds    r24, 0x01DB    ; Load foo 1-Byte-length value from 0x01DB address to a general purpose register
andi    r24, 0xF0    ; Mask the value according to the field lengths (foo.a is a 4-bit-length field)

So, using bit fields saves memory, that’s right, but add more instructions to access the bit fields variables. One more line of code couldn’t be a problem, but this is not the best case. There are complicated Structs or even simple ones but with special cases i.e. when using bit fields belongs to 2 bytes in the same time. Let’s tweak “foo” a little to explain how:

struct {
 unsigned int a: 4;
 unsigned int b: 6;
 unsigned int c: 1;
 unsigned int d: 8;
 unsigned int e: 3;
 unsigned int f: 2;
} foo;

“b” field has 4 bits belong to the first byte (0x8001db) and 2 bits belong to the next byte (0x8001dc). So the compiler has to add much more lines to deal with “foo.b” and that’s clear in the assembly code.

; The below is for foo.b = foo.b + d;
a84:    8f 73         andi    r24, 0x3F    ; 63
a86:    86 5f         subi    r24, 0xF6    ; 246
a88:    98 2f         mov    r25, r24
a8a:    92 95         swap    r25
a8c:    90 7f         andi    r25, 0xF0    ; 240
a8e:    40 91 db 01     lds r20, 0x01DB    ; 0x8001db <foo>
a92:    4f 70         andi    r20, 0x0F    ; 15
a94:    49 2b         or    r20, r25
a96:    40 93 db 01     sts 0x01DB, r20    ; 0x8001db <foo>
a9a:    82 95         swap    r24
a9c:    83 70         andi    r24, 0x03    ; 3
a9e:    90 91 dc 01     lds r25, 0x01DC    ; 0x8001dc <foo+0x1>
aa2:    9c 7f         andi    r25, 0xFC    ; 252
aa4:    89 2b         or    r24, r25
aa6:    80 93 dc 01     sts 0x01DC, r24    ; 0x8001dc <foo+0x1>

Let’s see what it will look like if foo.a was used with the old foo’s definition

; The below is for foo.a = foo.a + d;
a58:    68 2f         mov    r22, r24
a5a:    6f 70         andi    r22, 0x0F    ; 15
a5c:    90 91 db 01     lds r25, 0x01DB    ; 0x8001db <foo>
a60:    90 7f         andi    r25, 0xF0    ; 240
a62:    96 2b         or    r25, r22
a64:    90 93 db 01     sts 0x01DB, r25    ; 0x8001db <foo>

Less lines of code!

So it’s important to keep an eye on how fields are divided. For instance, the last “foo” definition can be modified to be like this:

struct {
 unsigned int a: 4;
 unsigned int   : 4;
 unsigned int b: 6;
 unsigned int c: 1;
 unsigned int   : 1;
 unsigned int d: 8;
 unsigned int e: 3;
 unsigned int f: 2;
} foo;

So a pad was added as needed to avoid unwanted behavior/performance.

Don’t Trust The Code, Listen to the Compiler

Dealing with bit field needs open eyes as we have seen in the last example, where the Struct division has a great impact on performance and size of code. Now, two examples can say why doesn’t the compiler understand the struct definition as we expect. The following examples are adapted from questions appeared on Stackoverflow website.

Example #1

struct
{
       unsigned char a:4;
       unsigned char b:8;
       unsigned char c:4;

} foo;

struct
{
       unsigned char a:4;
       unsigned char b;
       unsigned char c:4;

} FOO;

These two Structs should be the same, right?  Saying “unsigned char b:8;” and “unsigned char b;” seems the same for us. But actually, if we print the size of foo and FOO, we will find that foo’s size is 2 and size of FOO is 3. But Why? The compiler understands the first one as the following:

In the second Struct:

Example #2

struct
{
       unsigned long a:1;
        unsigned long b:32;
       unsigned long c:1;

}mystruct1;

struct
{
       unsigned long a:1;
        unsigned long b:31;
       unsigned long c:1;
}mystruct2;

In this two Structs, most of us will expect to have the same size (both 8 Bytes), but the fact is the first one will have the following Bytes:

While the second one will have:

Should I Use Bit Field Or Not?

 

As any other solution in engineering world, it’s not always a win-win situation. Bit-fields save place in data memory, and they also provide a simplified way to set and get values (that aren’t byte-aligned) rather than using bitwise operations. On the other hand, this means that whenever you use a bit-field variable, the processor/compiler will perform READ-MODIFY-WRITE operations. As the memory location is shared with others, then the compiler will read the entire variable to store it in a temporary place, mask other fields, change the value and then restore the value of unchanged fields (this is what is called READ-MODIFY-WRITE operation).

So it’s true that we saved some space in the SRAM, but we will need more instructions for each reading/writing operation in the Flash memory. Accessing a bit-field variable is another concern when using bit-field. It is not an atomic operation, which could lead to faults in some critical code sections. Especially for shared bit-field variables between interrupts and processes.

Many developers advocate to avoid using bit fields because it makes the code less portable as changing the compiler/version means changing how it looks and do with bit field. They also advocate to use bit-banding (the hardware version of bit fields) when it’s available. Actually, the past line was written in an article I wrote about bit-field variables.

Have a look for this great discussion via Hacker News board about this article.

Real Life Applications of Structs and Unions in Embedded Systems

Application #1: Access to Bits, Nibbles and Bytes of Variables

Note: Order in struct is important. It starts from LSB to MSB.

typedef union  
{
   uint8_t val;

   struct {
   uint8_t _0:1;
   uint8_t _1:1;
   uint8_t _2:1;
   uint8_t _3:1;
   uint8_t _4:1;
   uint8_t _5:1;
   uint8_t _6:1;
   uint8_t _7:1;
   } bits;

   struct 
  {
   uint8_t _0:4;
   uint8_t _1:4;
  } nibbles;

} byte8_t;


typedef union  
{
   uint16_t val;

   struct {
      byte8_t _1;
      byte8_t _0;
   } bytes;

} byte16_t;

 

Application #2: Implementing Protocols

Any non-ASCII protocol has fields with special meaning inside each byte (or any other size of data). Let’s say that we have a protocol that starts with one byte called “command”, and has the first bit to indicate direction, if it’s (get or response), and 2 bits as an address and rest is the command id. Using bitwise operation may be annoying. As Struct already do the necessary bitwise operations, then it’s useful to implement the protocol packets using a Union.

union
{
   struct {
   uint8_t dir:1; // bit 0
   uint8_t add:2; // 1 .. 2 bits
   units8_t id:5; // 3 .. 7 bits
   } fields;
   uint8_t val;
} CMD;

Thus, when sending or receiving a cmd packet can be done using that Union. A demo pseudo-code:

CMD.fields.dir = 0;
CMD.fields.add = 2;
CMD.fields.id = 7;

send(CMD.val);

if(newcmd)
{
CMD.val = receive();
if(CMD.fields.add != dDeviceadd)
//error
}

Application #3: Access to MCU Registers

Representing hardware registers as Bitfields is a very handy trick and a useful way for the ease of MCU register access. This techniques is used widely in SDKs i.e. ARM cortex M3/M4 SiliconLabs’ Gecko SDK. Using this technique, each register can be accessed using a bit field struct with its name. Later this struct will point to the peripheral/register base-address.

For EFM32 SDK, a struct was defined for GPIO:

typedef struct
{
 GPIO_P_TypeDef P[6];          /**< Port configuration bits */

 uint32_t       RESERVED0[10]; /**< Reserved for future use **/
 __IOM uint32_t EXTIPSELL;     /**< External Interrupt Port Select Low Register  */
 __IOM uint32_t EXTIPSELH;     /**< External Interrupt Port Select High Register  */
 __IOM uint32_t EXTIRISE;      /**< External Interrupt Rising Edge Trigger Register  */
 __IOM uint32_t EXTIFALL;      /**< External Interrupt Falling Edge Trigger Register  */
 __IOM uint32_t IEN;           /**< Interrupt Enable Register  */
 __IM uint32_t  IF; /**< Interrupt Flag Register  */
 __IOM uint32_t IFS;           /**< Interrupt Flag Set Register  */
 __IOM uint32_t IFC;           /**< Interrupt Flag Clear Register  */

 __IOM uint32_t ROUTE;         /**< I/O Routing Register */
 __IOM uint32_t INSENSE;       /**< Input Sense Register */
 __IOM uint32_t LOCK;          /**< Configuration Lock Register  */
} GPIO_TypeDef;                 /** @} */

Later in another header file, there is a “define” for a pointer with “GPIO_TypeDef” type casting to the real address:

#define GPIO ((GPIO_TypeDef *) GPIO_BASE) /**< GPIO base pointer */

Now when any register need from GPIO, simply can be access using GPIO pointer. A Demo:

void GPIO_DriveModeSet(GPIO_Port_TypeDef port, GPIO_DriveMode_TypeDef mode)
{   
GPIO->P[port].CTRL = (GPIO->P[port].CTRL & ~(_GPIO_P_CTRL_DRIVEMODE_MASK)) | (mode << _GPIO_P_CTRL_DRIVEMODE_SHIFT);
}

This method of mapping hardware registers was stated in an article via ARM information Center and discussed in detail in “Representing and Manipulating Hardware in Standard C and C++” by Dan Saks besides other ways.

Exit mobile version