中斷服務常式(interrupt service routine, ISR)
/ EYE-ess-ar /
想像你正在看書,門鈴響了。你夾好書籤、起身去應門、盡快處理好門口的人,然後回來從剛剛停下的地方接著讀。中斷服務常式(ISR)就是處理器在「門鈴響」時——某個硬體要求立即關注時——所執行的函式,之後它會回到原本在做的事。
機制上:中斷觸發時,硬體自動保存足夠的當前狀態(在 Cortex-M 上會把數個暫存器推入堆疊)、在向量表裡查出處理常式、然後跳過去。你的 ISR 執行、做它的小任務——讀進收到的位元組、清掉計時器旗標、複製一筆取樣——接著執行一道特殊的返回,把保存的狀態還原,被中斷的程式碼就彷彿什麼都沒發生般繼續。寫好 ISR 有兩條主導規則。第一,「保持簡短」:ISR 執行時,其他工作(常含其他中斷)會被延遲,所以做最少的事,把繁重工作留給主迴圈。第二,「小心共享資料」:ISR 可能在你主程式碼的任意兩道指令之間觸發,所以兩者都會碰的變數必須標成 volatile(讓編譯器每次都重讀),並以原子方式或暫時關閉中斷來存取,否則就會有資料競爭。常見模式是 ISR 設一個 volatile 旗標,主迴圈再注意到它。
它之所以重要,是因為 ISR 是 MCU 及時回應真實世界、又不必持續燒 CPU 週期去檢查(另一條路是輪詢)的方式。誠實的陷阱:ISR 一般不可呼叫會阻塞或非可重入的函式(printf()、malloc()、長延遲)——這麼做可能造成死結或破壞狀態;你必須清掉硬體的中斷旗標,否則 ISR 會沒完沒了地重複觸發;而與 ISR 共享卻「沒」標 volatile 的變數可能被快取在暫存器裡,於是主迴圈永遠看不到更新。「測試時沒問題」在這裡特別危險,因為競爭與漏旗標的臭蟲依賴時序、間歇出現。
volatile uint32_t ticks = 0; // 與 main 共享:必須是 volatile void SysTick_Handler(void) { // 每 1 ms 觸發一次 ticks++; // 微小、快速、不呼叫會阻塞的函式 } // 硬體自動清除 SysTick 旗標 // 主迴圈只是讀它: uint32_t now = ticks; // 因為 volatile 而每次都重讀
一個 1 ms 的 tick ISR:簡短、設定一個 volatile 計數器、不用 printf 或 malloc。主迴圈透過那個 volatile 變數讀取計數器。
讓 ISR 保持微小且不阻塞、清掉硬體旗標,並把任何與主程式碼共享的變數標成 volatile——而對於 ISR 可能拆開的多位元組值,光 volatile 還不夠:用一小段關中斷的區間保護它們。