裝置驅動程式與核心模組

記憶體映射輸入輸出(memory-mapped I/O)

/ MMIO, 'em-em-eye-oh' /

軟體究竟怎麼操作裝置的控制旋鈕?有兩種設計。在記憶體映射 I/O 中,裝置的控制與狀態暫存器被弄得看起來像普通的記憶體位址:寫入某個特定位址會送一個命令給裝置,從它讀取則回傳裝置的狀態。CPU 用的是它用於 RAM 的那一套 load 與 store 指令;硬體把那些位址導向裝置而非記憶體。

具體而言,裝置在某段實體位址範圍暴露一塊暫存器。要用它們,驅動程式以 ioremap 映射那段範圍,得到一個核心虛擬指標;然後在正確偏移處寫入 *reg(透過 writel/readl 之類的存取器)就真的戳動了一個硬體暫存器。例如 writel(1, base + 0x10) 可能意指「把偏移 0x10 的控制暫存器第 0 位元設為 1 以啟動裝置」。另一種設計是埠映射 I/O(PMIO),主要用於 x86,把裝置暫存器放在一個獨立的 I/O 位址空間,只能用特殊指令 in(讀一個埠)與 out(寫一個埠)抵達,例如 outb(value, 0x3F8)——這些埠不屬於記憶體位址空間,且需要特權指令。MMIO 在現代硬體上勝出,因為它不需要特殊指令,而且暫存器可以橫跨整個位址空間那麼寬。

MMIO 之所以重要,是因為它是驅動程式真正命令硬體的根本方式——現代驅動程式裡幾乎每一次戳暫存器都是 MMIO 存取。關鍵的誠實:MMIO 位址不是普通記憶體。讀取可能有副作用(讀狀態暫存器可能把它清掉)、值會自己改變(裝置會更新它們),而編譯器絕不可快取、重排或省略這些存取——這正是為什麼你用 volatile 正確的存取函式與記憶體屏障,而絕不用對一個被快取指標的單純解參考。

// MMIO(到處都能用): void __iomem *base = ioremap(phys_addr, size); writel(START_BIT, base + 0x10); // 戳控制暫存器 u32 status = readl(base + 0x14); // 讀狀態暫存器 // PMIO(僅 x86,獨立的 I/O 空間): outb(0x01, 0x3F8); // 寫一個位元組到埠 0x3F8

MMIO 透過 ioremap 存取器用普通的 load/store;PMIO 在獨立的埠空間中用特殊的 in/out 指令。

MMIO 位址不是普通記憶體:讀取可能有副作用、值會自己改變,而且存取不可被快取或重排——請用 writel/readl 風格的存取器與屏障,絕不用對被快取指標的單純 *p。

又称
MMIOmemory-mapped registers記憶體映射 I/O