iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
自我挑戰組

韌體工程師的不只 0x10 個問題系列 第 18

Day 18 - volatile 關鍵字

  • 分享至 

  • xImage
  •  

Day 14Day 17 介紹了「中斷」與「多執行緒」,其實就是為本篇的內容:volatile 鋪陳。

volatile 關鍵字的意義為「這個變數可能隨時被外部改變,不要對其做最佳化」,也就是告訴編譯器不要只讀取暫存器上的快取值。

volatile 的使用時機

那什麼時候會不希望編譯器只讀快取值呢?至少有下列三種情況:

硬體暫存器

首先是與前幾篇文章無關的「硬體暫存器」,也就是會直接操作到硬體的暫存器,簡單範例如下:

#include <stdio.h>

volatile int status = 0; // 用簡單的 int 變數模擬硬體暫存器

void hardware_action() {
    // 模擬硬體把 status 設為 1
    status = 1;
}

int main() {
    // 呼叫 hardware_action() 函數
    // 把「硬體暫存器」的 status 改成 1
    hardware_action();

    // 持續檢查 status,直到變成 1
    // 如果一開始 status 沒有寫 volatile,則可能只讀到 CPU 暫存器上的舊值 0
    // 而沒有讀到硬體透過 hardware_action() 把硬體暫存器的 status 改成 1
    // 造成跳不出這個迴圈,一直等待
    while (status == 0) {
        // 什麼都不做,單純等待
    }
    
    // status 不是 0 之後才有辦法執行這裡
    printf("Do something\n");

    return 0;
}

硬體暫存器的值很容易被硬體改變,例如本例中的 status,如果沒有加上 volatile,則即便硬體改變 status 的值,也不會被編譯器注意到,因為在經過優化後,編譯器讀的是存在 CPU 暫存器的舊值,而非硬體暫存器的新 status 值。

註:使用 volatile int status = 0; 時,其實變數是存在主記憶體,真正的硬體暫存器會用如以下的方法定義:

#define STATUS_REG (*(volatile int*)0x40011000)

這樣就可以透過 STATUS_REG 這巨集名字,直接操作 0x40011000 這個硬體暫存器位址中的值。

中斷服務常式 ISR

另一種使用情境是 Day 14 提到的「中斷服務常式(Interrupt Service Routine, ISR)」,就拿該篇文章的例子來說:

#define PI 3.14159

// 旗標 flag 同時被主程式 main() 與 ISR 存取
// 如果沒有 volatile,編譯器可能只會讀取放在 CPU 暫存器的舊值
// 就算 ISR 把 flag 的值改變,主程式也讀不到
volatile int flag = 0; 

__interrupt void my_isr(void) {
    flag = 1; // ISR 修改旗標,從舊值 0 改成新值 1
}

int main() {
    while (1) {
        if (flag) {  // 主程式根據旗標的值,判斷是否進入 if 區塊
            double area = PI * 2.0 * 2.0;
            printf("Area = %f\n", area);
            flag = 0;
        }
    }
    return 0;
}

主程式 main() 透過旗標 flag 決定執行內容,而 flag 的值會被 ISR my_isr() 更改,如果 flag 沒有寫上 volatile,則編譯器可能只會讀取到暫存的舊值 0,就算 ISR 把旗標改成 1 也不會被編譯器發現。

多執行緒共用變數

在多執行緒工作中,當不同執行緒共用變數時,也要加上 volatile

首先來看沒有 volatile 可能會產生的問題:

#include <iostream>
#include <thread>

int flag = 0; // 沒有 volatile

// 新執行緒會執行的函數,把 flag 變成 1
void setFlag() {
    flag = 1;
}

int main() {
    // 新執行緒,會把 flag 變成 1
    std::thread t(setFlag);

    // 主執行緒一直在等 flag 變成不是 0
    while (flag == 0) {
        // 什麼都不做
    }

    t.join();
    
    std::cout << "flag = " << flag << std::endl;
    return 0;
}

主執行緒 main() 會根據旗標 flag 的值是否為 0 決定是否跳出 while() 迴圈,而 flag 的值被新執行緒 std::thread t(setFlag); 改變。

然而,在沒有寫 volatile 的情況下,即便新執行緒把 flag 改成 1,編譯器可能還是只讀取到放在 CPU 暫存器上的舊 flag0,導致跳不出 while() 迴圈,該迴圈以下的程式碼都將不會執行。

而如果在 flag 加上 volatile 如下:

#include <iostream>
#include <thread>

volatile int flag = 0; // 有 volatile

// 新執行緒會執行的函數,把 flag 變成 1
void setFlag() {
    flag = 1;
}

int main() {
    // 新執行緒,會把 flag 變成 1
    std::thread t(setFlag);
    
    // 主執行緒等 flag 變成不是 0 後跳出此 while 迴圈
    while (flag == 0) {
        // 什麼都不做
    }

    t.join();
    
    std::cout << "flag = " << flag << std::endl;
    return 0;
}

這裡的 flag 有綁定 volatile,表示編譯器不會只讀取放在 CPU 暫存器的舊 flag 值,新執行緒把 flag 改成 1 後也能被編譯器發現,如此才保證能讓主執行緒正常跳出 while() 迴圈,執行後面的工作。

參考資料

  1. Use of the volatile keyword in C (Lextel Inc)

上一篇
Day 17 - 執行緒超入門
下一篇
Day 19 - volatile 的延伸問題
系列文
韌體工程師的不只 0x10 個問題19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言