Skip to content

🔒 volatile — Hardware Mapping

volatile nói với compiler: "Đừng optimize biến này — giá trị có thể thay đổi bất kỳ lúc nào!"

Analogy: Kiểm tra hộp thư

┌─────────────────────────────────────────────────────────────────┐
│                    MAILBOX CHECKING ANALOGY                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   KHÔNG có volatile = Nhớ trong đầu                             │
│   ─────────────────────────────────                             │
│   Bạn đã kiểm tra hộp thư sáng nay → "Không có thư"            │
│   Buổi chiều: "Chắc vẫn không có thư" (không kiểm tra lại)     │
│   → Bỏ lỡ thư quan trọng!                                       │
│                                                                 │
│   CÓ volatile = Kiểm tra thật mỗi lần                          │
│   ─────────────────────────────────                             │
│   Mỗi lần cần biết → ĐI RA kiểm tra hộp thư thật              │
│   → Không bao giờ bỏ lỡ!                                        │
│                                                                 │
│   Hardware register = Hộp thư                                   │
│   volatile = Luôn kiểm tra từ memory, không dùng cache          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Tại sao cần volatile?

Compiler Optimization Problem

cpp
// Hardware register at address 0x40000000
uint8_t* status_reg = (uint8_t*)0x40000000;

// Wait for hardware to set bit 0
while ((*status_reg & 0x01) == 0) {
    // Wait...
}

Compiler nghĩ: "Biến này không thay đổi trong loop → chỉ cần đọc 1 lần!"

cpp
// Compiler tối ưu thành:
uint8_t cached = *status_reg;  // Đọc 1 lần
while ((cached & 0x01) == 0) {
    // Infinite loop! Never re-reads!
}

Giải pháp: volatile

cpp
// ✅ CORRECT: volatile forces re-read every time
volatile uint8_t* status_reg = (volatile uint8_t*)0x40000000;

while ((*status_reg & 0x01) == 0) {
    // Compiler MUST read from memory each iteration
}

When to Use volatile?

┌─────────────────────────────────────────────────────────────────┐
│                    WHEN TO USE volatile                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✅ USE volatile:                                               │
│  ────────────────                                               │
│  • Memory-mapped hardware registers                             │
│  • Variables shared with ISR (Interrupt Service Routine)        │
│  • Variables shared between threads (though use atomic better)  │
│  • Memory shared with DMA controller                            │
│                                                                 │
│  ❌ DON'T USE volatile:                                         │
│  ──────────────────────                                         │
│  • Regular variables (performance hit)                          │
│  • Thread synchronization (use std::atomic instead)             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Memory-Mapped I/O Example

cpp
// GPIO register definitions
struct GPIO_Registers {
    volatile uint32_t DATA;    // 0x00: Data register
    volatile uint32_t DIR;     // 0x04: Direction register
    volatile uint32_t STATUS;  // 0x08: Status register
};

// Map to hardware address
GPIO_Registers* const GPIOA = (GPIO_Registers*)0x40020000;

void blinkLED() {
    // Set pin 5 as output
    GPIOA->DIR |= (1 << 5);
    
    while (true) {
        GPIOA->DATA |= (1 << 5);   // LED ON
        delay(500);
        GPIOA->DATA &= ~(1 << 5);  // LED OFF
        delay(500);
    }
}

void waitForButton() {
    // Wait until button pressed (bit 3 goes HIGH)
    while ((GPIOA->STATUS & (1 << 3)) == 0) {
        // volatile ensures we re-read STATUS each time
    }
    
    // Button was pressed!
}

volatile with ISR

cpp
// ✅ MUST be volatile - modified by ISR
volatile bool buttonPressed = false;
volatile uint32_t tickCount = 0;

// Interrupt Service Routine
extern "C" void EXTI3_IRQHandler() {
    buttonPressed = true;
    // Clear interrupt flag...
}

extern "C" void SysTick_Handler() {
    tickCount++;  // Updated every 1ms
}

int main() {
    while (true) {
        // Without volatile, compiler might cache these values!
        if (buttonPressed) {
            buttonPressed = false;
            handleButton();
        }
        
        // Use tickCount for timing
        if (tickCount >= 1000) {
            tickCount = 0;
            doPeriodicTask();
        }
    }
}

volatile vs const

Có thể kết hợp cả hai!

cpp
// Read-only hardware register
// Value can change (by hardware) but code can't write to it
volatile const uint32_t* const CHIP_ID = 
    (volatile const uint32_t*)0x1FFF7A10;

void printChipID() {
    uint32_t id = *CHIP_ID;  // Read hardware ID
    // *CHIP_ID = 0;         // ❌ Compile error - const!
}
KeywordValue can changeCode can modify
volatileYes (externally)Yes
constNoNo
volatile constYes (externally)No

volatile Does NOT Mean Atomic!

⚠️ CRITICAL

volatile không đảm bảo thread-safety hay atomicity!

cpp
volatile int counter = 0;

// Thread 1
void increment() {
    counter++;  // NOT ATOMIC! Still a race condition!
}

// Thread 2
void decrement() {
    counter--;  // NOT ATOMIC!
}

// ✅ For thread safety, use std::atomic
#include <atomic>
std::atomic<int> safeCounter{0};

Compiler Optimization Prevention

volatile prevents these optimizations:

OptimizationWithout volatileWith volatile
CachingRead once, reuseRe-read every time
ReorderingMay reorderMaintains order
Dead store eliminationMay skip writesAll writes happen
cpp
volatile uint8_t* reg = (volatile uint8_t*)0x40000000;

// Without volatile, compiler might:
// - Combine writes
// - Skip "useless" writes
// - Reorder operations

*reg = 0x01;  // Write 1
*reg = 0x02;  // Write 2 (compiler might skip 1!)
*reg = 0x03;  // Write 3

// With volatile: ALL 3 writes happen in order!

📚 Tổng kết

AspectDescription
PurposePrevent compiler caching/optimization
Use forHardware registers, ISR-shared vars
NOT forThread synchronization (use atomic)
EffectForces memory read/write every time

➡️ Tiếp theo

Trong embedded, khi nào dùng smart pointers và khi nào dùng raw pointers?

Smart Pointers in Embedded → — unique_ptr vs raw pointers.