Structures, Bitfields, and Unions

Structures, Bitfields, and Unions

The article is in close reference to the knowledge sought from the Udemy course: https://www.udemy.com/course/microcontroller-embedded-c-programming/

Structure

A data organization used to create user-defined data types in C. This comes in handy when handling variables of various data types that have a unified relationship.

 struct CarModel
{
    unsigned int CarNumber;
    uint32_t carPrice;
    uint16_t carMaxSpeed;
    float carWeight;
};

The above snippet is a structure definition. Structure definitions do not consume any memory. It’s identical to a description/record.

In the above case, struct CarModel is the data type.

struct CarMode1
{
    unsigned int CarNumber;
    uint32_t carPrice;
    uint32_t carMaxSpeed;
    float carWeight;
};

struct CarMode1 a,b,c; //Here's where memory is allocated!

// The memory consumed by each of the variables will be: (4+4+2+4) bytes = 14 bytes

The data type of a, b, and c is struct CarModel.

Programming Example: Write a program to create a carModel structure discussed and create 2 variables of type carModel. Initialize the variables with the below-given data and then print them.

  1. 2021,15000, 220, 1330
  2. 4031, 35000, 160, 1900.96
#include <stdio.h>
#include <stdint.h>

void printStr(unsigned int, uint32_t, uint32_t, float);

struct carModel
{
    unsigned int carNumber;
    uint32_t carPrice;
    uint32_t carMaxSpeed;
    float carWeight;
};

void printStr(unsigned int carN, uint32_t carP, uint32_t carS, float carW)
{
    printf("\nCar Number: %d\n", carN);
    printf("Car Price: %d\n", carP);
    printf("Car Max Speed: %d\n", carS);
    printf("Car Weight: %.2f\n",carW);
}

int main(void)
{
    struct carModel a,b;

    a = {2021,15000,220,1330};
    b = {4031,35000,160,1900.96};

    printStr(a.carNumber,a.carPrice,a.carMaxSpeed,a.carWeight);
    printStr(b.carNumber,b.carPrice,b.carMaxSpeed,b.carWeight);

    return 0;
}

C89 vs C99 method

a = {2021,15000,220,1330}; //C89 method - Order is important
a = {.carNumber = 2021, .carPrice = 15000, .carMaxSpeed = 220, .carWeight = 1330}; //C99 method - Order is not important

Size of a structure

#include <stdio.h>
#include <stdint.h>

void printStr(unsigned int, uint32_t, uint32_t, float);

struct carModel
{
    unsigned int carNumber;
    uint32_t carPrice;
    uint32_t carMaxSpeed;
    float carWeight;
};

void printStr(unsigned int carN, uint32_t carP, uint32_t carS, float carW)
{
    printf("\nCar Number: %u\n", carN);
    printf("Car Price: %d\n", carP);
    printf("Car Max Speed: %d\n", carS);
    printf("Car Weight: %.2f\n",carW);
}

int main(void)
{
    struct carModel a,b;
    /*
    //a = {2021,15000,220,1330};        //C89 method - Order is important
    a = {.carNumber = 2021, .carPrice = 15000, .carMaxSpeed = 220, .carWeight = 1330};     //C99 method - Order is not important
    b = {4031,35000,160,1900.96};

    printStr(a.carNumber,a.carPrice,a.carMaxSpeed,a.carWeight);
    printStr(b.carNumber,b.carPrice,b.carMaxSpeed,b.carWeight);*/

    printf("Size of variable a: %i\n",sizeof(a));
    printf("Size of variable b: %i\n",sizeof(b));

    return 0;
}

image.png

In the above case, the size is equal to theoretical calculations, 4*4 bytes = 16 bytes.

Consider the following example:

#include <stdio.h>
#include <stdint.h>

void printStr(unsigned int, uint32_t, uint32_t, float);

struct carModel
{
    unsigned int carNumber;
    uint32_t carPrice;
    uint16_t carMaxSpeed;
    float carWeight;
};

void printStr(unsigned int carN, uint32_t carP, uint32_t carS, float carW)
{
    printf("\nCar Number: %u\n", carN);
    printf("Car Price: %d\n", carP);
    printf("Car Max Speed: %d\n", carS);
    printf("Car Weight: %.2f\n",carW);
}

int main(void)
{
    struct carModel a,b;
    /*
    //a = {2021,15000,220,1330};        //C89 method - Order is important
    a = {.carNumber = 2021, .carPrice = 15000, .carMaxSpeed = 220, .carWeight = 1330};     //C99 method - Order is not important
    b = {4031,35000,160,1900.96};

    printStr(a.carNumber,a.carPrice,a.carMaxSpeed,a.carWeight);
    printStr(b.carNumber,b.carPrice,b.carMaxSpeed,b.carWeight);*/

    printf("Size of variable a: %i\n",sizeof(a));
    printf("Size of variable b: %i\n",sizeof(b));

    return 0;
}

image.png

Theoretically, the size of the structure variables should be, (4+4+2+4) bytes = 14 bytes.

Here the size allocated to the structure is still 16 bytes, and this is because of a factor called Structure Padding.

Aligned and Un-aligned data access

For efficiency, the compiler generates instructions to store variables in their natural size boundary addresses in the memory. This applies to structures as well.

Natural Size boundaries for the data types char, short, and int are:

→ char

Memory Addresses: 0x00 0x01 0x02 0x03

→ short

Memory Addresses: 0x00 0x02 0x04 0x06

→ int

Memory Addresses: 0x00 0x04 0x08 0x0C

Structure Padding

Analyzing the alignment of data variables in a structure.

#include <stdio.h>
#include <stdint.h>

struct Dataset
{
    char a;     //1 byte
    int b;      //4 bytes
    short c;    //2 bytes
    char d;     //1 bytes
};

int main(void)
{
    struct Dataset data;

    data.a = 0x11;
    data.b = 0xAABBCCDD;
    data.c = 0x4231;
    data.d = 0x11;

    uint8_t *ptr = (uint8_t*)&data;

    printf("Memory Address          Content\n");
    printf("===============================\n");

    for(int i=0;i<sizeof(data);i++)
    {
        printf("%p                      %x\n",ptr,*ptr);
        ptr++;
    }
    printf("Size of the structure: %d",sizeof(data));

    return 0;
}

image.png

In a structure, each data member is data aligned according to the natural size boundary of the data types.

0x0061FF0F is left blank because the natural size boundary of int is 4 bytes.

Reason for Structure padding

When the data stored is aligned, it becomes easier for the MCU to perform R/W operations and the memory transactions are less expensive. Unaligned data requires more instructions for memory access. This increases the code size(number of instructions) required for data access. The only drawback of padding is the loss of some amount of memory.

Calculating Structure size manually

struct data
{
    char a;
    int b;
    char c;
    short d;
};

image.png

Packed Structure

An unaligned structure is also called a packed structure. The process of storing data in an unaligned fashion is called packing.

struct data
{
    char a;
    int b;
    char c;
    int d;
}__attribute__((packed));

image.png

Aliasing Structures with typedef

typedef is used to alias a primitive/user-defined data type.

#include <stdio.h>

typedef struct  //<tagname is optional>
{
    int a;
}data_t;

int main(void)
{
    data_t d  = {.a=1};
    printf("a = %d",d);
    return 0;
}

_t following the alias name is a convention used to distinguish between enum and typedef.

→ A structure cannot contain a variable of itself in its definition, yet, self-referential pointers are allowed, and such a structure is termed a self-referential structure. → A structure within a structure is allowed.

Structures and Pointers

  • Creating pointer variables of a structure
  • Reading and writing data to the member elements using structure pointers.

    Using primitive data type pointers to access structure elements is tedious as it requires the user to calculate structure padding.

Thereby, the following approach is followed:

→ Create a pointer of the structure data type,

→ Store the address of the structure variable,

→ Use the pointer with the arrow symbol to access every member of the structure.

#include <stdio.h>

typedef struct  //optional tag
{
    int a;
    char b;
    short c;
}data;

int main(void)
{
    data d = {.a=1, .b='a', .c=5};
    data *ptr = (data*)&d;         //store the memory address of d
    printf("a = %d\n",ptr->a);     //-> is used with a structure pointer
    printf("b = %c\n",ptr->b);     //*(address_of_second_member)
    printf("c = %d\n",ptr->c);

    return 0;
}

Member access:

**#include <stdio.h>

typedef struct  //optional tag
{
    int a;
    char b;
    short c;
}data;

int main(void)
{
    data d;
    data *ptr = (data*)&d;      //store the address of d in pointer variable 

    //Structure Member access using pointers
    ptr->a = 1;         //*(address_of_a) = 1;
    ptr->b = 'a';       //*(address_of_b) = 'a';
    ptr->c = 3;         //*(address_of_c) = 3;

    printf("a = %d\n",ptr->a);
    printf("b = %c\n",ptr->b);
    printf("c = %d\n",ptr->c);

    return 0;
}**

Passing Structure Pointers to a function

#include <stdio.h>

typedef struct  //optional tag
{
    int a;
    char b;
    short c;
}data;

void DisplayMembers(data*);

int main(void)
{
    data d;
    DisplayMembers((data*)&d); //PascalCase
    return 0;
}

void DisplayMembers(data *ptr)
{
    ptr->a = 1;
    ptr->b = 'a';
    ptr->c = 1;

    printf("a = %d\n",ptr->a);
    printf("b = %c\n",ptr->b);
    printf("c = %d\n",ptr->c);
}

image.png

The program won't work if the function prototype is above the structure definition. This is the reason to have these in a separate header file.

Write a program to decode a given 32-bit packet of information and print the values of the different fields. Create a structure with member elements as packet fields as shown below.

image.png

#include <stdio.h>
#include <stdint.h>

typedef struct
{
    unsigned char addr_mode;
    unsigned char short_addr;
    unsigned char long_addr;
    unsigned char sensor;
    unsigned char bat;
    uint16_t payload;
    unsigned char status;
    unsigned char crc;
}packet;

void decode(packet *ptr, uint32_t data);
void display(packet *ptr);

void decode(packet *ptr, uint32_t data)
{
    ptr->addr_mode = (data>>31)&0x1;
    ptr->short_addr = (data>>29)&0x3;
    ptr->long_addr = (data>>21)&0xFF; 
    ptr->sensor = (data>>18)&0x7;
    ptr->bat = (data>>15)&0x7;
    ptr->payload = (data>>3)&0xFFF;
    ptr->status = (data>>2)&0x1;
    ptr->crc = data&0x3;

    display(ptr);
}

void display(packet *ptr)
{
    printf("Addr mode: %x\n",ptr->addr_mode);
    printf("Short address: %x\n",ptr->short_addr);
    printf("Long address: %x\n",ptr->long_addr);
    printf("Sensor: %x\n",ptr->sensor);
    printf("Bat: %x\n",ptr->bat);
    printf("Payload: %x\n",ptr->payload);
    printf("Status: %x\n",ptr->status);
    printf("CRC: %x\n",ptr->crc);
}

int main(void)
{
    uint32_t data;
    packet pkt;

    printf("Enter a 32 bit packet: ");
    scanf("%x",&data);

    decode(&pkt,data);

    printf("Size of the packet: %d",sizeof(pkt));

    return 0;
}

image.png

Structures and Bitfields

In the above program example, it is seen that only a part of the variable(size corresponding to the data type), is used, and the remaining bits are asserted with 0s.

Eg: CRC

image.png

This wastage of memory bits can be eliminated using bit fields.

→ The appropriate data type for the member elements is decided.

→ For the above case, since it’s a packet, uint32_t is chosen as the data type.

image.png

The following code demonstrates the application of a bitfield,

#include <stdio.h>
#include <stdint.h>

typedef struct
{
    uint32_t addr_mode      :1;
    uint32_t short_addr     :2;
    uint32_t long_addr      :8;
    uint32_t sensor         :3;
    uint32_t bat            :3;
    uint32_t payload        :12;
    uint32_t status         :1;
    uint32_t crc            :2;
}packet;

void decode(packet *ptr, uint32_t data);
void display(packet *ptr);

void decode(packet *ptr, uint32_t data)
{
    ptr->addr_mode = (data>>31)&0x1;
    ptr->short_addr = (data>>29)&0x3;
    ptr->long_addr = (data>>21)&0xFF; 
    ptr->sensor = (data>>18)&0x7;
    ptr->bat = (data>>15)&0x7;
    ptr->payload = (data>>3)&0xFFF;
    ptr->status = (data>>2)&0x1;
    ptr->crc = data&0x3;

    display(ptr);
}

void display(packet *ptr)
{
    printf("Addr mode: %x\n",ptr->addr_mode);
    printf("Short address: %x\n",ptr->short_addr);
    printf("Long address: %x\n",ptr->long_addr);
    printf("Sensor: %x\n",ptr->sensor);
    printf("Bat: %x\n",ptr->bat);
    printf("Payload: %x\n",ptr->payload);
    printf("Status: %x\n",ptr->status);
    printf("CRC: %x\n",ptr->crc);
}

int main(void)
{
    uint32_t data;
    packet pkt;

    printf("Enter a 32 bit packet: ");
    scanf("%x",&data);

    decode(&pkt,data);

    printf("Size of the packet: %d",sizeof(pkt));

    return 0;
}

image.png

Unions

A union is similar to a structure, except that all members start at the same memory location.

Memory allocation difference between a structure and a union:

image.png

In a union, memory is re-used by both variables, and the memory allocated is the size of the biggest member. This is used when one of the two variables is used at a time. Modifying one variable overwrites the other.

#include <stdio.h>
#include <stdint.h>

typedef union 
{
    uint16_t short_addr;
    uint32_t long_addr;
}data;

int main(void)
{
    data a;
    a.short_addr = 0xAABB;
    a.long_addr = 0xCCCCCCCC;

    printf("Short address: %x\n",a.short_addr);
    printf("Long address: %x",a.long_addr);

    return 0;
}

image.png

The short address is over-written with the long one, as the memory is shared between the 2 variables.

Simplifying the 32-bit decoder using a nested structure - Eliminates logical operations used to decode bits into respective structure members.

#include <stdio.h>
#include <stdint.h>

typedef union
 {
    uint32_t datapkt;
    struct
    {
        uint32_t crc        :1;
        uint32_t status     :2;
        uint32_t payload    :12;
        uint32_t bat        :3;
        uint32_t sensor     :3;
        uint32_t long_addr  :8;
        uint32_t short_addr :2;
        uint32_t addr_mode  :1;
    }packet_fields;
}packet;

void display(packet *ptr);

void display(packet *ptr)
{
    printf("Addr mode: %x\n",ptr->packet_fields.addr_mode);
    printf("Short address: %x\n",ptr->packet_fields.short_addr);
    printf("Long address: %x\n",ptr->packet_fields.long_addr);
    printf("Sensor: %x\n",ptr->packet_fields.sensor);
    printf("Bat: %x\n",ptr->packet_fields.bat);
    printf("Payload: %x\n",ptr->packet_fields.payload);
    printf("Status: %x\n",ptr->packet_fields.status);
    printf("CRC: %x\n",ptr->packet_fields.crc);
}

int main(void)
{
    uint32_t data;
    packet pkt;

    printf("Enter a 32 bit packet: ");
    scanf("%x",&data);

    pkt.datapkt = data;
    display(&pkt);

    printf("Size of the packet: %d",sizeof(pkt));

    return 0;
}

image.png

References: