The "Holy Bible" for embedded engineers
Understanding const, volatile, and restrict keywords for embedded C programming
Think of qualifiers as contracts:
const
: intent is read-only at this access pointvolatile
: value may change outside the compiler’s view (hardware/ISR)restrict
: this pointer is the only way to access the referenced objectvolatile
prevents the compiler from caching HW register values.const
enables placement in ROM and better optimization.restrict
allows the compiler to vectorize/memcpy efficiently in hot paths.// Read-only lookup table likely in Flash
static const uint16_t lut[] = {1,2,3,4};
// Memory-mapped I/O register
#define GPIOA_ODR (*(volatile uint32_t*)0x40020014u)
// Non-aliasing buffers (improves copy performance)
void copy_fast(uint8_t * restrict dst, const uint8_t * restrict src, size_t n);
volatile
from a polled status register read and compile with -O2
; inspect assembly to see hoisted loads.restrict
on a memset/memcpy-like loop and measure on target.volatile
is about visibility, not atomicity or ordering.const
expresses intent and may change placement; don’t cast it away to write.restrict
only when you can prove no aliasing.Platform note: For I/O ordering on some MCUs/SoCs, pair volatile accesses with memory barriers when required by the architecture.
1) Volatile visibility lab
// Configure an ISR to toggle a flag; poll in main with and without volatile
static /*volatile*/ uint32_t flag = 0;
void ISR(void){ flag++; }
int main(void){
uint32_t last = 0;
for(;;){ if(flag != last){ last = flag; heartbeat(); } }
}
volatile
.2) ROM placement lab
static /*const*/ uint16_t lut[1024] = { /* ... */ };
const
; inspect the map for .rodata
vs .data
and boot copy size.3) Restrict speedup lab
void add(uint32_t* /*restrict*/ a, const uint32_t* /*restrict*/ b, size_t n){
for(size_t i=0;i<n;i++) a[i]+=b[i];
}
volatile
?const
objects ever be modified through another alias legally?restrict
undefined or unsafe?Embedded_C/Memory_Mapped_IO.md
for register patternsEmbedded_C/Compiler_Intrinsics.md
for barriersEmbedded_C/Memory_Models.md
for placement and startup costsType qualifiers in C provide important hints to the compiler about how variables should be treated:
These qualifiers are especially important in embedded systems for:
Type qualifiers are keywords in C that modify the behavior of variables and provide hints to the compiler about how data should be treated. They help ensure correct program behavior, especially in embedded systems where hardware interaction and optimization are critical.
Compiler Hints:
Memory Access Control:
Embedded System Impact:
const Qualifier:
volatile Qualifier:
restrict Qualifier:
Hardware Interaction:
Safety and Reliability:
Performance Optimization:
Hardware Register Access:
// Without volatile - may not work correctly
uint32_t* const gpio_register = (uint32_t*)0x40020014;
uint32_t value = *gpio_register; // Compiler may optimize away
// With volatile - guaranteed to work
volatile uint32_t* const gpio_register = (uint32_t*)0x40020014;
uint32_t value = *gpio_register; // Always reads from hardware
Interrupt Safety:
// Without volatile - interrupt may not be detected
bool interrupt_flag = false;
// With volatile - interrupt will be detected
volatile bool interrupt_flag = false;
Performance Optimization:
// Without restrict - compiler can't optimize
void copy_data(uint8_t* dest, const uint8_t* src, size_t size);
// With restrict - compiler can optimize aggressively
void copy_data(uint8_t* restrict dest, const uint8_t* restrict src, size_t size);
High Impact Scenarios:
Low Impact Scenarios:
How Compilers Work:
Optimization Examples:
// Without volatile - compiler may optimize away
uint32_t counter = 0;
while (counter < 100) {
// Some work...
counter++; // Compiler may optimize this loop
}
// With volatile - compiler won't optimize away
volatile uint32_t counter = 0;
while (counter < 100) {
// Some work...
counter++; // Compiler preserves this access
}
Read-Only Access:
Volatile Access:
Exclusive Access:
Memory Safety:
Code Correctness:
The const
qualifier indicates that a variable or object should not be modified. It provides compile-time protection against accidental modifications and enables compiler optimizations.
Read-Only Semantics:
const Applications:
// Read-only variables
const uint32_t MAX_BUFFER_SIZE = 1024;
const float VOLTAGE_REFERENCE = 3.3f;
const uint8_t LED_PIN = 13;
// Attempting to modify const variable causes compilation error
// MAX_BUFFER_SIZE = 2048; // ❌ Compilation error
uint8_t data = 0x42;
const uint8_t* ptr1 = &data; // Pointer to const data
uint8_t* const ptr2 = &data; // Const pointer to data
const uint8_t* const ptr3 = &data; // Const pointer to const data
// ptr1 can point to different data, but can't modify it
// ptr2 can't point to different data, but can modify it
// ptr3 can't point to different data and can't modify it
// Function that doesn't modify input data
uint32_t calculate_checksum(const uint8_t* data, uint16_t length) {
uint32_t checksum = 0;
for (uint16_t i = 0; i < length; i++) {
checksum += data[i]; // Read-only access
}
return checksum;
}
// Function that takes const structure
void print_sensor_data(const sensor_reading_t* reading) {
printf("ID: %d, Value: %.2f\n", reading->id, reading->value);
// Can't modify reading->value
}
// Function returning const pointer
const uint8_t* get_lookup_table(void) {
static const uint8_t table[] = {0x00, 0x01, 0x02, 0x03};
return table; // Caller can't modify table
}
// Function returning const structure
const sensor_config_t* get_default_config(void) {
static const sensor_config_t config = {
.id = 1,
.enabled = true,
.timeout = 1000
};
return &config;
}
// Read-only hardware registers
const volatile uint32_t* const ADC_DATA = (uint32_t*)0x4001204C;
const volatile uint32_t* const GPIO_IDR = (uint32_t*)0x40020010;
// Reading from read-only registers
uint32_t adc_value = *ADC_DATA; // Read ADC data
uint32_t gpio_input = *GPIO_IDR; // Read GPIO input
The volatile
qualifier indicates that a variable can change unexpectedly, typically by hardware or other threads. It prevents the compiler from optimizing away memory accesses and ensures that every access to the variable actually reads from or writes to memory.
Unexpected Changes:
Compiler Behavior:
volatile Applications:
// Hardware register definitions
volatile uint32_t* const GPIO_ODR = (uint32_t*)0x40020014;
volatile uint32_t* const GPIO_IDR = (uint32_t*)0x40020010;
volatile uint32_t* const UART_DR = (uint32_t*)0x40011000;
// Writing to hardware register
*GPIO_ODR |= (1 << 5); // Set GPIO pin
// Reading from hardware register
uint32_t input_state = *GPIO_IDR; // Read GPIO input
// UART communication
void uart_send_byte(uint8_t byte) {
*UART_DR = byte; // Write to UART data register
}
uint8_t uart_receive_byte(void) {
return (uint8_t)*UART_DR; // Read from UART data register
}
// Variables modified by interrupts
volatile bool interrupt_flag = false;
volatile uint32_t interrupt_counter = 0;
volatile uint8_t received_data = 0;
// Interrupt service routine
void uart_interrupt_handler(void) {
received_data = (uint8_t)*UART_DR; // Read received data
interrupt_flag = true; // Set flag
interrupt_counter++; // Increment counter
}
// Main loop checking interrupt flag
void main_loop(void) {
while (!interrupt_flag) {
// Wait for interrupt
}
// Process received data
process_data(received_data);
interrupt_flag = false; // Clear flag
}
// Shared data between threads
volatile uint32_t shared_counter = 0;
volatile bool shutdown_requested = false;
// Thread 1: Increment counter
void thread1_function(void) {
while (!shutdown_requested) {
shared_counter++;
delay_ms(100);
}
}
// Thread 2: Monitor counter
void thread2_function(void) {
uint32_t last_counter = 0;
while (!shutdown_requested) {
if (shared_counter != last_counter) {
printf("Counter: %u\n", shared_counter);
last_counter = shared_counter;
}
}
}
Without volatile (may not work):
// Compiler may optimize away this access
uint32_t* const gpio_register = (uint32_t*)0x40020014;
uint32_t value = *gpio_register; // May be optimized away
// Compiler may optimize this loop
bool flag = false;
while (!flag) {
// Wait for flag to be set
}
With volatile (guaranteed to work):
// Compiler won't optimize away this access
volatile uint32_t* const gpio_register = (uint32_t*)0x40020014;
uint32_t value = *gpio_register; // Always reads from hardware
// Compiler won't optimize this loop
volatile bool flag = false;
while (!flag) {
// Wait for flag to be set
}
The restrict
qualifier indicates that a pointer provides exclusive access to the data it points to. It enables aggressive compiler optimizations by guaranteeing that the pointer doesn’t alias other pointers.
Exclusive Access:
Compiler Optimizations:
restrict Applications:
// Function with restrict parameters
void copy_data(uint8_t* restrict dest, const uint8_t* restrict src, size_t size) {
for (size_t i = 0; i < size; i++) {
dest[i] = src[i]; // Compiler can optimize this aggressively
}
}
// Function with overlapping parameters (no restrict)
void copy_data_overlap(uint8_t* dest, const uint8_t* src, size_t size) {
for (size_t i = 0; i < size; i++) {
dest[i] = src[i]; // Compiler must be conservative
}
}
// Local variables with restrict
void process_array(uint32_t* restrict data, size_t size) {
uint32_t* restrict temp = malloc(size * sizeof(uint32_t));
if (temp != NULL) {
// Process data with exclusive access
for (size_t i = 0; i < size; i++) {
temp[i] = data[i] * 2; // Compiler can optimize
}
// Copy back
for (size_t i = 0; i < size; i++) {
data[i] = temp[i]; // Compiler can optimize
}
free(temp);
}
}
// Optimized matrix multiplication
void matrix_multiply(float* restrict result,
const float* restrict a,
const float* restrict b,
int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
float sum = 0.0f;
for (int k = 0; k < n; k++) {
sum += a[i * n + k] * b[k * n + j];
}
result[i * n + j] = sum;
}
}
}
Without restrict (conservative optimization):
void add_arrays(int* a, int* b, int* result, int size) {
for (int i = 0; i < size; i++) {
result[i] = a[i] + b[i]; // Compiler must be conservative
}
}
With restrict (aggressive optimization):
void add_arrays(int* restrict a, int* restrict b, int* restrict result, int size) {
for (int i = 0; i < size; i++) {
result[i] = a[i] + b[i]; // Compiler can optimize aggressively
}
}
Type qualifiers can be combined to provide multiple guarantees:
const volatile:
const restrict:
volatile restrict:
// Read-only hardware registers
const volatile uint32_t* const ADC_DATA = (uint32_t*)0x4001204C;
const volatile uint32_t* const GPIO_IDR = (uint32_t*)0x40020010;
// Read-write hardware registers
volatile uint32_t* const GPIO_ODR = (uint32_t*)0x40020014;
volatile uint32_t* const UART_DR = (uint32_t*)0x40011000;
// Function with multiple qualifiers
void process_data(const uint8_t* restrict input,
uint8_t* restrict output,
volatile uint32_t* restrict status,
size_t size) {
// Process input data (read-only, no aliasing)
for (size_t i = 0; i < size; i++) {
output[i] = input[i] * 2; // Compiler can optimize
}
// Update status (volatile, no aliasing)
*status = PROCESSING_COMPLETE;
}
// Configuration structure with multiple qualifiers
typedef struct {
const uint32_t id;
const uint32_t timeout;
volatile bool enabled;
volatile uint32_t counter;
} device_config_t;
// Global configuration
const volatile device_config_t* const device_config =
(device_config_t*)0x20000000;
#include <stdint.h>
#include <stdbool.h>
// Hardware register definitions
#define GPIOA_BASE 0x40020000
#define GPIOA_ODR (GPIOA_BASE + 0x14)
#define GPIOA_IDR (GPIOA_BASE + 0x10)
#define UART_BASE 0x40011000
#define UART_DR (UART_BASE + 0x00)
#define UART_SR (UART_BASE + 0x00)
// Hardware register pointers
volatile uint32_t* const gpio_odr = (uint32_t*)GPIOA_ODR;
const volatile uint32_t* const gpio_idr = (uint32_t*)GPIOA_IDR;
volatile uint32_t* const uart_dr = (uint32_t*)UART_DR;
const volatile uint32_t* const uart_sr = (uint32_t*)UART_SR;
// Interrupt variables
volatile bool uart_interrupt_received = false;
volatile uint8_t uart_received_data = 0;
volatile uint32_t interrupt_counter = 0;
// Configuration constants
const uint32_t MAX_BUFFER_SIZE = 1024;
const uint8_t LED_PIN = 5;
const uint32_t UART_TIMEOUT_MS = 1000;
// Function with multiple qualifiers
void process_buffer(const uint8_t* restrict input,
uint8_t* restrict output,
size_t size) {
// Process data with exclusive access
for (size_t i = 0; i < size; i++) {
output[i] = input[i] * 2; // Compiler can optimize
}
}
// Interrupt service routine
void uart_interrupt_handler(void) {
// Read received data
uart_received_data = (uint8_t)*uart_dr;
// Set interrupt flag
uart_interrupt_received = true;
// Increment counter
interrupt_counter++;
}
// Main function
int main(void) {
// Initialize hardware
*gpio_odr |= (1 << LED_PIN); // Set LED pin
// Main loop
while (1) {
// Check for UART interrupt
if (uart_interrupt_received) {
// Process received data
uint8_t processed_data = uart_received_data * 2;
// Send processed data back
*uart_dr = processed_data;
// Clear interrupt flag
uart_interrupt_received = false;
}
// Read GPIO input
uint32_t gpio_input = *gpio_idr;
// Process based on GPIO state
if (gpio_input & (1 << 0)) {
// Button pressed
*gpio_odr |= (1 << LED_PIN);
} else {
// Button released
*gpio_odr &= ~(1 << LED_PIN);
}
}
return 0;
}
Problem: Hardware register access without volatile Solution: Always use volatile for hardware registers
// ❌ Bad: Missing volatile
uint32_t* const gpio_register = (uint32_t*)0x40020014;
uint32_t value = *gpio_register; // May be optimized away
// ✅ Good: Using volatile
volatile uint32_t* const gpio_register = (uint32_t*)0x40020014;
uint32_t value = *gpio_register; // Always reads from hardware
Problem: Using const when data should be modifiable Solution: Use const only for truly read-only data
// ❌ Bad: const when data should be modifiable
const uint8_t buffer[100]; // Can't modify buffer
// ✅ Good: const only for read-only data
const uint8_t lookup_table[] = {0x00, 0x01, 0x02, 0x03};
uint8_t buffer[100]; // Modifiable buffer
Problem: Using restrict when pointers may alias Solution: Use restrict only when pointers don’t alias
// ❌ Bad: restrict when pointers may alias
void bad_function(int* restrict a, int* restrict b) {
// a and b might point to same memory
for (int i = 0; i < 10; i++) {
a[i] = b[i]; // Undefined behavior if aliased
}
}
// ✅ Good: restrict only when no aliasing
void good_function(int* restrict a, int* restrict b) {
// a and b are guaranteed to not alias
for (int i = 0; i < 10; i++) {
a[i] = b[i]; // Safe optimization
}
}
Problem: Not using const for read-only parameters Solution: Use const for parameters that shouldn’t be modified
// ❌ Bad: No const for read-only parameter
void print_data(uint8_t* data, size_t size) {
for (size_t i = 0; i < size; i++) {
printf("%u ", data[i]);
}
}
// ✅ Good: const for read-only parameter
void print_data(const uint8_t* data, size_t size) {
for (size_t i = 0; i < size; i++) {
printf("%u ", data[i]);
}
}
Next Steps: Explore Bit Manipulation to understand low-level bit operations, or dive into Structure Alignment for memory layout optimization.