Access GPIO using bit field

Access GPIO and Register using the Bit field

In embedded systems, GPIO registers are often used to control individual pins of a microcontroller. While registers can always be accessed using integer masks and shifts, another method sometimes used is the C bit-field feature.

A bit-field is a property of structures that allows mapping individual bits of a word. This can be convenient for creating structures that align with hardware registers. However, there are some important points you need to keep in mind, which we will discuss.

In this article, we will:

  • Show how to map a bit-field structure to a GPIO register.
  • Demonstrate how to read and write individual bits.
  • Explain the limitations and portability issues of this method.

⚠️ Important Note: The C standard does not define how compilers allocate bit-fields in memory. This means that bit-field layouts may differ between compilers, toolchains, or even optimization levels. For this reason, bit-fields should generally be avoided in production code for hardware register mapping. Instead, bit masking and shifting is the recommended practice.

 

Step to Mapping a Hardware Register with Bit-Fields:

Step 1: Define a Bit-Field Structure

We first define a structure that represents the 32 bits of a register:

/* Structure representing 32 GPIO bits */
typedef struct
{
    volatile unsigned int Bit0 : 1;
    volatile unsigned int Bit1 : 1;
    volatile unsigned int Bit2 : 1;
    volatile unsigned int Bit3 : 1;
    /* ... continue up to Bit31 */
    volatile unsigned int Bit31 : 1;
}

Each field here is 1-bit wide, directly corresponding to a GPIO pin.

 

Step 2: Map the Structure to a Register Address

We then map the structure onto the memory-mapped address of a register. For example, in LPC2119:

volatile SPortPin *psGpioPort = (volatile SPortPin *)0xE002C000;

By typecasting this address, the structure maps directly to the hardware register. Now psGpioPort provides direct access to the GPIO register bits.

 

⚠️ Important:

When mapping a structure to a hardware register address, make sure that:

  • The address is word-aligned (e.g., 32-bit registers should be aligned to 4-byte boundaries).
  • The address is valid and accessible in your microcontroller’s memory map.
  • Failing to meet these conditions may cause unexpected behavior, bus faults, or hard faults.

 

Step 3: Read and Write Individual Bits:

To read the state of a pin:

unsigned int value = psGpioPort->Bit1;

 

To set a pin:

psGpioPort->Bit1 = 1;   // Set pin high


psGpioPort->Bit1 = 0;   // Set pin low

 

⚠️ Note: The C compiler might generate a read-modify-write sequence, which can unintentionally alter other bits if the hardware requires atomic access. That’s another reason why bit-fields are discouraged for registers.

 

Using a Union for Dual Access (bit-field + full register):

For more flexibility, we can combine a bit-field structure with an integer register using a union. This allows access to both the whole register and individual bits:

typedef union
{
    volatile unsigned int PORT;   // Full 32-bit register
    SPortPin GPIO_PIN;            // Individual bits
} UGpioPort;

This lets us choose between:

  • Writing or reading the entire register at once (PORT).
  • Manipulating specific pins (GPIO_PIN).

 

Example: Accessing GPIO with Bit-Fields

Here is a complete example that writes to a GPIO register and then reads back a pin value.

In the C code below, I am attempting to set the 3rd bit of the register located at address 0xE002C000. After writing the value, I will then read back the register to verify that the bit has been correctly updated.

 

Access GPIO using bit field

 

#include <LPC21xx.H>

/* Define bit-field structure (8 bits shown for simplicity) */
typedef struct
{
    volatile unsigned int Bit0 : 1;
    volatile unsigned int Bit1 : 1;
    volatile unsigned int Bit2 : 1;
    volatile unsigned int Bit3 : 1;
    volatile unsigned int Bit4 : 1;
    volatile unsigned int Bit5 : 1;
    volatile unsigned int Bit6 : 1;
    volatile unsigned int Bit7 : 1;
} SPortPin;

/* Union for flexible access */
typedef union
{
    volatile unsigned int PORT;   // Full register
    SPortPin GPIO_PIN;            // Individual pins
} UGpioPort;



/* Function to write to a pin */
void WriteOnPin(UGpioPort* puPort, unsigned char ucPin, unsigned char value)
{
    switch (ucPin)
    {
    case 0:
        puPort->GPIO_PIN.Bit0 = value;
        break;
    case 1:
        puPort->GPIO_PIN.Bit1 = value;
        break;
    case 2:
        puPort->GPIO_PIN.Bit2 = value;
        break;
    case 3:
        puPort->GPIO_PIN.Bit3 = value;
        break;
    case 4:
        puPort->GPIO_PIN.Bit4 = value;
        break;
    case 5:
        puPort->GPIO_PIN.Bit5 = value;
        break;
    case 6:
        puPort->GPIO_PIN.Bit6 = value;
        break;
    case 7:
        puPort->GPIO_PIN.Bit7 = value;
        break;
    }
}

/* Function to read from a pin */
unsigned char ReadFromPin(UGpioPort* puPort, unsigned char ucPin)
{
    unsigned char PinValue = 0;
    switch (ucPin)
    {
    case 0:
        PinValue = puPort->GPIO_PIN.Bit0;
        break;
    case 1:
        PinValue = puPort->GPIO_PIN.Bit1;
        break;
    case 2:
        PinValue = puPort->GPIO_PIN.Bit2;
        break;
    case 3:
        PinValue = puPort->GPIO_PIN.Bit3;
        break;
    case 4:
        PinValue = puPort->GPIO_PIN.Bit4;
        break;
    case 5:
        PinValue = puPort->GPIO_PIN.Bit5;
        break;
    case 6:
        PinValue = puPort->GPIO_PIN.Bit6;
        break;
    case 7:
        PinValue = puPort->GPIO_PIN.Bit7;
        break;
    }
    return PinValue;
}

/* Main Example */
int main(void)
{
    unsigned char PinValue;

    /* Map union to GPIO register address */
    volatile UGpioPort *pUGpioPort = (volatile UGpioPort*)0xE002C000;

    /* Clear register */
    pUGpioPort->PORT = 0x00000000;

    /* Write '1' on 3rd pin (Bit2) */
    WriteOnPin(pUGpioPort, 2, 1);

    /* Read back value of 3rd pin */
    PinValue = ReadFromPin(pUGpioPort, 2);

    return 0;
}

 

Conclusion:

Bit-fields can make register access look cleaner, but they are not portable across compilers. Different compilers may:

  • Allocate bit-fields left-to-right or right-to-left.
  • Insert padding bits differently.
  • Generate unexpected code depending on optimizations.

For production firmware, it is safer to use bit masking and shifting macros. However, for learning purposes, or quick experimentation, bit-fields can help illustrate how GPIO registers map to individual pins.

 

Bit-Fields vs. Bit-Masks for Register Access:

When working with microcontroller registers, you generally have two options:

When working on microcontrollers, direct access to hardware registers is a fundamental task. Registers typically control GPIOs, timers, interrupts, and communication peripherals. Each bit (or group of bits) has a defined purpose, and developers must choose how to represent and manipulate them in C.

Two common approaches are:

Both methods work, but their reliability, safety, and portability differ significantly.

1. Bit-Fields

A bit-field is a C structure where individual members are declared with a specified number of bits. This allows direct access to specific bits as if they were normal variables.

Example:

typedef struct
{
    volatile unsigned int Bit0 : 1;
    volatile unsigned int Bit1 : 1;
    volatile unsigned int Bit2 : 1;
    volatile unsigned int Bit3 : 1;
    /* ... */
    volatile unsigned int Bit31 : 1;
} SPortPin;

volatile SPortPin * const pPort = (volatile SPortPin*)0xE002C000;

/* Set bit 2 */
pPort->Bit2 = 1;

/* Read bit 2 */
Value = pPort->Bit2;

✅ Pros:

  • Easy to understand (looks like normal struct members).
  • No need for bitwise operations.
  • Code looks neat and readable.

⚠️ Cons:

  • Compiler-dependent layout (bit ordering, padding, alignment differ).
  • May generate inefficient or unsafe read-modify-write instructions.
  • Not portable across different compilers/MCUs.

2. Bit-Masks

The bit-mask method is the most widely used and recommended approach for register access in embedded systems. In this method, each bit (or group of bits) is defined as a constant mask, and standard C bitwise operators (|, &, ~, ^) are used to set, clear, or read the desired bits.

Example:

/* Define bit positions as macros */
#define PIN0   (1U << 0)
#define PIN1   (1U << 1)
#define PIN2   (1U << 2)
#define PIN3   (1U << 3)

/* Base address of GPIO port (example) */
volatile unsigned int * const GPIO_PORT = (unsigned int *)0xE002C000;

/* Set bit 2 */
*GPIO_PORT |= PIN2;

/* Clear bit 2 */
*GPIO_PORT &= ~PIN2;

/* Read bit 2 */
Value = (*GPIO_PORT & PIN2) ? 1U : 0U;

✅ Pros:

  • 100% portable across compilers.
  • Predictable code generation.
  • Works safely with hardware registers (avoids hidden read-modify-write issues).
  • Industry-standard approach used in most HAL/LL drivers.

⚠️ Cons:

  • Requires bitwise operators, which beginners may find less intuitive.
  • Slightly more verbose compared to bit-field notation.

Note: Bit-masks may look a bit more verbose than bit-fields, but they are reliable, portable, and safe.

 

Recommended Post: