Skip to content

C Language - Understanding the bitwise operators and ANSI C Type Qualifiers

Published: at 12:00 PM

As I learn more about firmware programming, I happen upon techniques that are not the common for me and my background in sofware applications. Bitwise operators may be used during firmware initialization to configure hardware such as GPIO ports, enable timers and interrupts. Once configured the settings may not be visited again, but when you do, some head scratching may occur repurposing an existing configure:

    /* Step 1: Enable clock for GPIOA and GPIOB */
    RCC_APB2ENR |= (1 << 2);      // Bit 2 (IOPAEN) enables GPIOA peripheral
    RCC_APB2ENR |= (1 << 3);      // Bit 3 (IOPBEN) enables GPIOB peripheral

    GPIOA_CRL &= ~(0xF << 4*0);   // Clear bits 3:0
    GPIOA_CRL |=  (0x2 << 4*0);   // MODE=10 (Output 2 MHz), CNF=00 (Push-pull)

Table of Content

Function Definition

Let’s start with a small program that I wrote for STM32F103xx (Blue Pill) microcontroller, and dive deep into the bitwise operators and bitwise shift operator, plus the ‘volatile’ qualifier.

/* Peripheral Base Addresses */
#define RCC_BASE      0x40021000
#define RCC_APB2ENR   (*(volatile uint32_t *)(RCC_BASE + 0x18))

/* CRL = Configuration Register Low: Controls pins 0–7 */
#define GPIOA_CRL     (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_BSRR    (*(volatile uint32_t *)(GPIOA_BASE + 0x10))
#define GPIOA_BRR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))

#define GPIOB_CRL     (*(volatile uint32_t *)(GPIOB_BASE + 0x00))
#define GPIOB_BSRR    (*(volatile uint32_t *)(GPIOB_BASE + 0x10))
#define GPIOB_BRR     (*(volatile uint32_t *)(GPIOB_BASE + 0x14))

static void GPIO_Init(void);
void delay(volatile uint32_t);

int main(void) {

  GPIO_Init();

    while (1)
    {
        GPIOA_BSRR = (1 << 0);  // Drive PA0 HIGH → LED ON
        GPIOB_BRR  = (1 << 0);  // Drive PB0 LOW  → LED OFF
        delay(500000);

        GPIOB_BSRR = (1 << 0);  // Drive PB0 HIGH → LED ON
        GPIOA_BRR  = (1 << 0);  // Drive PA0 LOW  → LED OFF
        delay(500000);
    }
}

static void GPIO_Init(void) {

    /* Step 1: Enable clock for GPIOA and GPIOB */
    RCC_APB2ENR |= (1 << 2);      // Bit 2 (IOPAEN) enables GPIOA peripheral
    RCC_APB2ENR |= (1 << 3);      // Bit 3 (IOPBEN) enables GPIOB peripheral

    GPIOA_CRL &= ~(0xF << 4*0);   // Clear bits 3:0
    GPIOA_CRL |=  (0x2 << 4*0);   // MODE=10 (Output 2 MHz), CNF=00 (Push-pull)

    GPIOB_CRL &= ~(0xF << 4*0);   // Clear bits 3:0
    GPIOB_CRL |=  (0x2 << 4*0);   // MODE=10 (Output 2 MHz), CNF=00 (Push-pull)
}

Reading from top down, ‘RCC_BASE’ value was determined by reading the STM32F10xxx reference manual. The address ‘0x40021000’ represents the start of RCC register boundary.

/* Peripheral Base Addresses */
#define RCC_BASE      0x40021000
#define RCC_APB2ENR   (*(volatile uint32_t *)(RCC_BASE + 0x18))

Memory map

RCC_APB2ENR is 0x18 bits offset from the RCC_BASE. Memory map

#define RCC_APB2ENR   (*(volatile uint32_t *)(RCC_BASE + 0x18))

Understanding volatile

volatile instructs the compiler not to optimize access to RCC_APB2ENR. Normally, a compiler assumes that a variable only changes when the program modifies it. Hardware registers are different. A peripheral, timer, interrupt controller, or other hardware component may change the value stored at a memory location without the compiler’s knowledge.

In this example, RCC_APB2ENR refers to the Reset and Clock Control (RCC) APB2 Peripheral Clock Enable Register:

#define RCC_APB2ENR   (*(volatile uint32_t *)(RCC_BASE + 0x18))

The compiler must perform a real memory access every time the program reads or writes this register. Without volatile, the compiler could cache the register value in a CPU register, remove repeated accesses, or reorder operations. Such optimizations may produce incorrect behavior when communicating with hardware.

The same reasoning applies to the GPIO registers:

#define GPIOA_CRL     (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_BSRR    (*(volatile uint32_t *)(GPIOA_BASE + 0x10))
#define GPIOA_BRR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))

These memory locations correspond to physical hardware. Every write must reach the microcontroller peripheral exactly as written.

Aside: Decoding a Register Definition

Before continuing, let’s quickly unpack this expression:

(*(volatile uint32_t *)(GPIOA_BASE))

Reading from the inside out:

(volatile uint32_t *)(GPIOA_BASE)

casts the address to a pointer to a volatile uint32_t.

*(volatile uint32_t *)(GPIOA_BASE)

then dereferences that pointer, allowing the program to access the 32-bit value stored at that location.

This pattern is commonly used in firmware register definitions:

#define GPIOA_CRL (*(volatile uint32_t *)(GPIOA_BASE + 0x00))

As a result, GPIOA_CRL behaves much like a normal variable:

GPIOA_CRL = 0x02;

However, instead of writing to RAM, the assignment writes directly to a memory-mapped hardware register.

Now, onto ANSI C Type qualifers.

Understanding const

Another ANSI C type qualifier commonly used in firmware is const.

The const qualifier indicates that a value cannot be modified after initialization.

For example:

const uint32_t led_pin = 0;

Attempting to modify led_pin later results in a compiler error:

led_pin = 1;   // Error

Firmware projects often use const for:

For example:

const uint32_t GPIOA_CLOCK_ENABLE_BIT = (1 << 2);

Using const communicates intent to both the compiler and future developers. The value is meant to remain unchanged throughout program execution.

It is common to combine const and pointers:

const uint32_t *register_ptr;

This declaration indicates that the data pointed to by register_ptr cannot be modified through the pointer.

Understanding the Bitwise Left Shift Operator (<<)

One of the most frequently used operators in firmware development is the bitwise left shift operator.

The syntax is:

value << shift_count

Each shift moves all bits to the left by the specified number of positions.

For example:

1 << 2

Starting value:

00000001

After shifting left two positions:

00000100

The result is decimal 4.

In firmware, the left shift operator provides a convenient way to create bit masks.

Consider this code:

RCC_APB2ENR |= (1 << 2);

The expression:

(1 << 2)

produces:

00000000000000000000000000000100

Bit 2 is set while all other bits remain cleared.

According to the STM32F103 reference manual, bit 2 of the APB2 Peripheral Clock Enable Register is IOPAEN (I/O Port A Clock Enable).

Therefore:

RCC_APB2ENR |= (1 << 2);

enables the clock for GPIOA.

Similarly:

RCC_APB2ENR |= (1 << 3);

creates a mask with bit 3 set, enabling GPIOB through the IOPBEN bit.

Understanding the Bitwise OR Operator (|)

The bitwise OR operator compares two values bit-by-bit.

If either bit is 1, the result is 1.

Bit ABit BResult
000
011
101
111

Firmware developers commonly use OR operations to set specific bits while leaving all other bits unchanged.

Consider:

RCC_APB2ENR |= (1 << 2);

The |= operator is shorthand for:

RCC_APB2ENR = RCC_APB2ENR | (1 << 2);

Suppose the register currently contains:

00000000

The mask contains:

00000100

The result becomes:

00000100

Only bit 2 changes.

Understanding the Bitwise AND Operator (&)

The bitwise AND operator compares two values bit-by-bit.

A result bit becomes 1 only when both input bits are 1.

Bit ABit BResult
000
010
100
111

Firmware developers often use AND operations to preserve or isolate specific bits.

Consider:

GPIOA_CRL &= ~(0xF << 4*0);

This line clears the configuration bits associated with PA0 before new settings are applied.

The GPIOA_CRL register is the GPIO Port A Configuration Register Low. It controls pins PA0 through PA7.

Each pin consumes four configuration bits.

For PA0, the configuration occupies bits 3:0.

The expression:

0xF

represents:

1111

Since PA0 starts at bit position 0:

0xF << 0

remains:

0000 1111

Understanding the Bitwise NOT Operator (~)

The bitwise NOT operator inverts every bit.

~0xF

changes:

0000 1111

to:

1111 0000

Applying the NOT operator creates a mask that clears specific bits while preserving all others.

This is exactly what occurs here:

GPIOA_CRL &= ~(0xF << 4*0);

The mask becomes:

1111 1111 1111 1111 1111 1111 1111 0000

When combined with the AND operator, bits 3:0 are cleared.

Configuring PA0

After clearing the existing configuration, the code applies the desired settings:

GPIOA_CRL |= (0x2 << 4*0);

The value:

0x2

is:

0010

According to the STM32F103 reference manual:

MODE = 10
CNF  = 00

This configures PA0 as:

The OR operation inserts these bits into the now-cleared configuration field.

The same process is repeated for PB0 using the GPIOB_CRL register.

Summary

This example introduces several foundational concepts used throughout firmware development:

Once these concepts become familiar, reading microcontroller datasheets and reference manuals becomes much easier. You begin to see registers not as large hexadecimal values, but as collections of individual control bits that can be manipulated with a small set of powerful C operators.


Next Post
Driveway Gate Opener Part 4 - LCD