Skip to main content

Welcome to a Grasp lesson

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

Lesson 50

Combining Type Qualifiers

Recommended resources

Hello! Welcome to the sixth lesson in our module on type qualifiers.

In our last few lessons, we've built a solid understanding of C's type qualifiers individually. We know const enforces read-only access from the program's perspective, volatile prevents compiler optimizations for memory that can change unexpectedly, and restrict signals the absence of pointer aliasing to unlock performance gains.

Today, we'll focus on how these qualifiers, particularly const and volatile, work together. Your work in AUTOSAR middleware and ADAS communication stacks involves constant interaction with hardware, shared memory, and other system components where memory doesn't behave like simple program variables. Mastering these combinations is essential for writing safe, correct, and efficient low-level code. Our learning outcome is to correctly combine multiple type qualifiers (e.g., const volatile).

Are const and volatile Opposites?

At first glance, const (the value shouldn't change) and volatile (the value might change at any moment) can seem contradictory. The key is to understand who or what each qualifier is addressing:

  • const: This is a restriction on the programmer (enforced by the compiler). It says, "My C code should not attempt to modify this value." It creates a read-only view of the data from within the program.
  • volatile: This is a directive to the compiler. It says, "Something outside the normal flow of this program—like hardware, an interrupt service routine (ISR), or another thread—might change this value. Therefore, do not optimize away any reads from or writes to this memory location. Every access in the source code must correspond to an actual memory access in the compiled binary."

They are not opposites; they are orthogonal concepts that describe different properties of a variable or the memory it represents.

To explore this, we'll look at a resource that introduces this combination.

Usage of const and volatile together

The article 'Usage of const and volatile together' from FastBitLab provides a concise explanation of why these two qualifiers are not contradictory and how they can be used together in an embedded context.

Please read the initial sections of this article, focusing on: The introduction, which explicitly states that const and volatile are not opposites. The explanation of const volatile. Note the distinction it makes: const is for the programmer, while volatile is for the compiler. The example of a read-only input data register. This is the canonical use case for this combination.


The Classic Example: Memory-Mapped I/O Registers

The most common and important use case for const volatile is accessing memory-mapped hardware registers. Your experience with microcontroller architecture and gateway development means you've likely encountered this pattern frequently.

Consider a hardware status register (e.g., a UART receive status register):

  • The hardware updates this register's bits when new data arrives or an error occurs. The program must always read the latest value from the hardware, so the pointer used to access it must point to volatile data.
  • The program should only read this status register to check its state. It should never write to it. Therefore, the pointer should treat the data as const.

This leads to a declaration like this:
const volatile uint32_t *status_reg_ptr;

Let's break this down:

  • status_reg_ptr is a pointer...
  • to a uint32_t...
  • that is volatile (its value can change externally, so the compiler must not optimize reads)...
  • and is const (your program code is not allowed to write to it).

The pointer status_reg_ptr itself is a normal variable; you could reassign it to point to a different status register.

Now, what if the register's address itself is fixed and should never change? This is common when defining registers with macros. You would make the pointer const as well:

const volatile uint32_t * const STATUS_REG = (const volatile uint32_t *)0x40001000;

Here, STATUS_REG is a const pointer, meaning it will always point to address 0x40001000.


A Systematic Guide to Combining Qualifiers

Parsing these complex declarations can be challenging. The "right-left rule" is a helpful heuristic, but a systematic review of the common combinations is the most effective way to internalize them.

The following resource, born from an embedded developer's interview question, provides a perfect, detailed breakdown.

What is the point of declaring "const volatile int *p"?

This Stack Overflow page, 'What is the point of declaring "const volatile int *p"?', offers a brilliant, step-by-step explanation of seven different pointer declarations, progressively adding const and volatile qualifiers.

Please read the numbered list (1-7) in the highest-rated answer. This is the core of our lesson. For each of the seven declarations, focus on understanding: What is qualified? Is it the data, the pointer, or both? What does const apply to? What is being made read-only for the programmer? What does volatile apply to? What value must the compiler re-read on every access?

Let's summarize and solidify the key combinations from that resource, mapping them to practical scenarios you'd encounter:

Declaration Meaning Common Use Case
uint32_t *p; Pointer to a mutable uint32_t. General purpose pointer to data your program can read and write.
const uint32_t *p; Pointer to a const uint32_t. (Data is read-only). Function parameter for data that should not be modified by the function (e.g., a configuration struct).
uint32_t * const p; const pointer to a mutable uint32_t. (Pointer address is fixed). A fixed pointer to a buffer that your program needs to modify, like a shared DMA buffer address.
volatile uint32_t *p; Pointer to a volatile uint32_t. (Data can change externally). Pointer to a read-write hardware control register or a variable shared between an ISR and main code.
uint32_t * volatile p; volatile pointer to a mutable uint32_t. (Pointer address can change externally). A rare case. The pointer itself is shared with an ISR/thread which might retarget it to a different buffer.
const volatile uint32_t *p; Pointer to a const volatile uint32_t. (Data is read-only for the program but can change externally). Pointer to a read-only hardware status register. This is the interview question's subject.
const volatile uint32_t * const p; const pointer to a const volatile uint32_t. (Fixed address, read-only data for the program, data can change externally). The typical way to define a memory-mapped read-only register via a macro. The address and write-access are fixed.
const volatile uint32_t * const volatile p; volatile const pointer to const volatile data. (Both the data and the pointer's address itself can change externally, but the program can't change either). A highly unusual case. Could be used if the address of the status register itself is stored in a volatile location.
Test your understanding!

You are working on the gateway for an ADAS system. There is a specific memory address, 0xE0001004, which acts as a hardware FIFO (First-In, First-Out) buffer's data port for receiving CAN frames.

  • Reading from this address pops the next 32-bit word of a CAN frame from the FIFO. Each read could produce a different value as the hardware fills the FIFO.
  • Writing to this address is not allowed and could put the hardware in an invalid state.
  • The address 0xE0001004 is fixed by the hardware design.

Which of the following declarations is the most precise and safe way to define a pointer variable named CAN_RX_FIFO to access this data port? Explain your choice by breaking down why each qualifier is (or is not) necessary.

  1. uint32_t * const CAN_RX_FIFO = (uint32_t *)0xE0001004;
  2. volatile uint32_t * CAN_RX_FIFO = (volatile uint32_t *)0xE0001004;
  3. const volatile uint32_t * CAN_RX_FIFO = (const volatile uint32_t *)0xE0001004;
  4. const volatile uint32_t * const CAN_RX_FIFO = (const volatile uint32_t *)0xE0001004;
Show answer

The correct declaration is 4. const volatile uint32_t * const CAN_RX_FIFO = (const volatile uint32_t *)0xE0001004;

Here's the breakdown:

  • const (on the right, qualifying the pointer): The address of the FIFO port, 0xE0001004, is fixed. The pointer CAN_RX_FIFO should never be reassigned to point somewhere else. This const makes the pointer itself read-only. This is necessary for safety.
  • volatile (on the left, qualifying the data): Each read from the address can yield a different value as the hardware FIFO is populated by incoming CAN messages. The compiler must not optimize away successive reads (e.g., by assuming the value is the same as the last time it was read). This volatile is necessary for correctness.
  • const (on the left, qualifying the data): The programmer should not be allowed to write to this address. The FIFO is a read-only port for the CPU. This const prevents accidental writes in the C code, which would cause a compile-time error. This is necessary for safety.

Therefore, you need a const pointer to const volatile data.


Differentiating Volatile Data vs. Volatile Pointers

One of the trickiest parts of this topic is distinguishing between a pointer to volatile data (volatile T *p) and a volatile pointer (T * volatile p). A quick video recap can help cement this distinction.

How to use the volatile keyword in C?

Let's watch a short segment from Jacob Sorber's video on the volatile keyword. This clip clearly explains the different ways volatile can be applied to a pointer declaration.

Watch the section from 4:26 to 4:56. The presenter quickly covers the four key combinations involving volatile, which will reinforce the parsing logic we've just discussed.

This distinction is crucial. In 99% of embedded use cases, you will be dealing with pointers to volatile data (volatile T *). The case of a volatile pointer (T * volatile p) is much rarer, as it implies the address of the pointer itself is being modified by an external agent.


Conclusion

In this lesson, we've dissected how C's type qualifiers can be combined to create precise, safe, and correct descriptions of memory, especially in an embedded context. You now have the tools to handle complex pointer declarations that accurately model the behavior of hardware and shared memory.

Key Takeaways:

  • const and volatile are not mutually exclusive; they describe orthogonal properties. const is a rule for the programmer, while volatile is a directive to the compiler.
  • The combination const volatile is essential for read-only memory-mapped registers that are updated by hardware (e.g., status registers, input data ports).
  • You can apply qualifiers to the data (const volatile T *p), the pointer (T * const volatile p), or both (const volatile T * const volatile p).
  • The most common and robust declaration for a fixed, read-only hardware register is const volatile T * const REGISTER_NAME, which makes both the pointer's address and the data's writability constant from the program's perspective, while acknowledging the data can change externally.

Preview of the Next Lesson:
We've spent this module building up walls and contracts with const to make our programs safer. But what happens when you absolutely need to bypass these protections? In our next lesson, we will analyze the risks and appropriate use cases for casting away const. This is a powerful but dangerous technique, and understanding when and how to use it (and when not to) is critical for any advanced C programmer.