Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

Mastering `const` Pointers & Read-Only Hardware

Recommended resources

Hello! Welcome to your next lesson in Module 2: Data Representation and Low-Level Manipulation.

In our previous lesson, we established that volatile is a critical directive to the compiler, warning it that a variable can change unexpectedly and must be re-read from memory on every access. This prevents incorrect optimizations when dealing with things like memory-mapped I/O and data shared with interrupts.

Today, we'll explore its counterpart: the const qualifier. While volatile manages unexpected changes, const is about enforcing intentional immutability. You'll learn how to use it with pointers to create safer, more readable APIs—a hallmark of a senior embedded engineer. We will then combine these two concepts to master the const volatile pattern, which is essential for correctly interacting with hardware.

This lesson directly addresses the learning outcome: Enforce const correctness with const pointers, pointers-to-const, and const volatile for read-only hardware registers.

1. const as a Contract

At a senior level, it's best to think of const not just as a way to create constants, but as a contract. When you use const, you are making a compile-time promise about your code's intent:

  • To other developers: "This function I wrote will not modify the data you pass to it."
  • To your future self: "I intended for this value to never change."
  • To the compiler: "You can enforce this rule for me and potentially perform optimizations based on this promise."

This contract is especially powerful when used with pointers. However, the syntax can be confusing. Let's break it down.


2. Decoding const with Pointers

There are three primary ways const can be applied to a pointer declaration, each with a distinct meaning.

Constant Pointer VS. Pointer To A Constant | C Programming Tutorial

To start, let's watch a clear and concise explanation of the three main pointer-and-const combinations. This video from Portfolio Courses effectively demonstrates the syntax and behavior of each.

Please watch the video "Constant Pointer VS. Pointer To A Constant". It covers: Constant Pointer (0:00 - 2:45): char * const ptr. The pointer's address is constant, but the data it points to can be changed. Pointer to a Constant (2:45 - 5:55): const char * ptr. The pointer can be changed to point elsewhere, but the data it points to is read-only through this pointer. Constant Pointer to a Constant (5:55 - 7:25): const char * const ptr. Neither the pointer's address nor the data it points to can be changed.

To help remember these rules, you can use a simple trick: read the declaration from right to left.

Harnessing Volatile and Const in Embedded C

This article offers a great mnemonic for reading const declarations and summarizes the rules effectively.

Please read the section titled "It’s important to understand what are the different ways a const keyword could be used". Focus on Types 2, 3, and 4, and pay attention to the tip: "Start reading from right to left..."

Let's summarize with a practical example:




#include <stdio.h>

int main() {
    int var_a = 10;
    int var_b = 20;

    // --- Type 1: Pointer to a const integer ---
    // Read as: "p1 is a pointer to a const int"
    // The data pointed to is constant, but the pointer itself is not.
    const int *p1 = &var_a;
    // *p1 = 15;      // ILLEGAL: Cannot change the value through p1.
    p1 = &var_b;       // LEGAL: Can change where the pointer points.
    printf("p1 points to value: %d\n", *p1);

    // --- Type 2: Const pointer to an integer ---
    // Read as: "p2 is a const pointer to an int"
    // The pointer is constant, but the data it points to is not.
    int * const p2 = &var_a;
    *p2 = 15;          // LEGAL: Can change the value it points to.
    // p2 = &var_b;    // ILLEGAL: Cannot change where the pointer points.
    printf("var_a is now: %d\n", var_a);

    // --- Type 3: Const pointer to a const integer ---
    // Read as: "p3 is a const pointer to a const int"
    // Neither the pointer nor the data can be changed.
    const int * const p3 = &var_a;
    // *p3 = 25;      // ILLEGAL
    // p3 = &var_b;    // ILLEGAL

    return 0;
}

Interview Focus: const in Function Parameters

A very common use case, and a frequent topic in interviews, is using const in function arguments.

Consider a function that prints a large data structure.

// Version 1: Without const
void print_status(SystemStatus *status) {
    // ... logic to print status fields ...
}

// Version 2: With const (Preferred)
void print_status_safe(const SystemStatus *status) {
    // ... logic to print status fields ...
    // status->critical_flag = 1; // This would now cause a compile error!
}

By declaring the parameter as const SystemStatus *status, you establish a contract: this function will only read from the status object. This has several benefits:

  1. Safety: Prevents accidental modification of the caller's data.
  2. Clarity: It clearly communicates the function's intent to anyone reading the header file.
  3. Optimization: The compiler can make stronger assumptions, knowing the data won't be modified within the function.

3. The const volatile Pattern for Read-Only Registers

Now, let's connect this back to hardware. In the last lesson, we learned to use volatile for Memory-Mapped I/O (MMIO) registers.

But what about registers that are read-only from the software's perspective?

  • A hardware status register (e.g., "FIFO empty" flag).
  • A timer's current count register.
  • A device's unique silicon ID register.

For these, we have two requirements:

  1. The value can change at any time due to external hardware events. The compiler must not optimize away reads. This requires volatile.
  2. Our software should never write to this register. An attempt to do so is a bug. This requires const.

This is where we combine them into the const volatile pattern.

Understanding `volatile`, `const`, and `volatile const` in Embedded C
This diagram illustrates the roles of `const` (code can't modify), `volatile` (value can change externally), and their combination, `const volatile`, which is ideal for a read-only hardware status register.

Let's see how this is declared in code.

Can a variable be both Volatile and Const?

The following article explains exactly when and why you need to use both qualifiers together. This is a crucial concept for robust embedded programming.

Please read the section titled "Can a variable be both Volatile and Const?". It gives the perfect rationale: const prevents your program from modifying the value, while volatile ensures your program always reads the latest value from hardware.

Here is a typical implementation for a read-only hardware register, like a timer's counter value:

// Datasheet defines a 32-bit read-only timer counter at this address



#define TIMER_BASE_ADDR   0x40020000
#define TIMER_CNT_OFFSET  0x04

// Define a macro for the register
// Note the `const volatile` type cast



#define TIMER_CNT_REG (*(const volatile uint32_t *)(TIMER_BASE_ADDR + TIMER_CNT_OFFSET))

// This function is correct. volatile ensures a fresh read from hardware every time.
uint32_t get_current_time_us() {
    return TIMER_CNT_REG; 
}

// This function will fail to compile, thanks to `const`.
void attempt_to_reset_timer() {
    // error: assignment of read-only location ‘TIMER_CNT_REG’
    TIMER_CNT_REG = 0; 
}

Using const volatile creates a robust, self-documenting interface to the hardware. The compiler enforces that you can't accidentally write to a read-only register, catching a common class of bugs at compile time.


Conclusion

In this lesson, we elevated your understanding of const from a simple variable qualifier to a powerful tool for creating robust software contracts, especially with pointers.

Key Takeaways:

  • const is a promise about immutability. Mastering its use with pointers (const T*, T* const, const T* const) is essential for writing safe, self-documenting code.
  • Using const for pointer parameters in function prototypes is a key practice for building clean APIs, as it guarantees the function will not modify the caller's data.
  • The const volatile pattern is the definitive way to handle read-only hardware registers. const prevents your code from writing to it, while volatile prevents the compiler from optimizing away reads of a value that can change asynchronously.

Preview of the Next Lesson:

We have now examined two key C qualifiers that instruct the compiler on how to handle variables. Next, we will shift our focus to how the compiler itself organizes data in memory. The lesson will be on Data Alignment & Structure Padding, where you will learn how to calculate structure sizes, why padding is added, and how to rearrange structure members to minimize your memory footprint—a critical skill for memory-constrained embedded systems.