第 8 章的 只是「申報」:告訴 OS 我的暫存器在哪、佔多大, 請 OS 別把資源分給別人。但 AML 自己也常要動手碰硬體—— 要讀狀態暫存器、 要寫預設值、EC 的電池電量要一個 byte 一個 byte 讀出來。ACPI 給 AML 的存取機制分成兩層:本章的 負責「在某個位址空間開一扇窗」, 第 10 章的 Field 再把窗內切成一顆顆具名暫存器。

先窗後刀。本章專心把「窗」講透:四個參數、九種位址空間、 「宣告不碰硬體」的 lazy 語意,以及 就緒通知。

  • 說明 OperationRegion 四個參數的意義,寫出 SystemIO 與 SystemMemory 的 region 宣告。

  • 列出九種 address space,說明各自的位址意義與典型用途。

  • 寫出 PCI_Config region,並說明它與 的搭配關係。

  • 解釋「宣告不碰硬體、存取才碰硬體」與 region handler 的關係。

  • 用 寫出 EC region 的就緒旗標,避免過早存取。

  • 畫出硬體資源、OperationRegion、Field、暫存器的層次圖。

一行宣告,四個參數

先看課綱指定的範例——在 SystemIO 空間開一扇 16 byte 的窗:

OperationRegion (REG0, SystemIO, 0x300, 0x10)

四個參數各司其職:

llL7.4cm 參數 & 本例 & 意義
RegionName & & Namespace 物件名,命名規則同一般物件
RegionSpace & & 位址空間:決定「位址」怎麼解讀
Offset & & 窗口起點,意義隨空間而異(見下節)
Length & & 窗口長度,單位 byte

這行宣告的白話翻譯:「以後我會從 IO port 0x300 開始、長度 16 byte 的範圍讀寫,這扇窗叫 REG0。」值得一提的是,Offset 與 Length 不必是常數——它們是 TermArg,可以放運算式或 Named object。 基底位址開機才知道時(例如先從別的暫存器讀出 BAR), 就靠這個特性動態決定窗口位置。

OperationRegion 在整個存取體系中的位置如下,這張層次圖請刻在腦中:

九種 Address Space

第二個參數 RegionSpace 決定「0x300 這個數字要怎麼解讀」—— 是實體記憶體位址?IO port?還是 PCI config space 的偏移? ACPI 6.5 定義的標準空間如下:

lcL4.6cmL4.2cm RegionSpace & ID & Offset 的意義 & 典型用途
& 0 & 實體記憶體位址(MMIO) & 裝置暫存器、共享緩衝區
& 1 & x86 IO port 編號 & EC 0x62/0x66、CMOS 0x70、 debug port 0x80
& 2 & 該 PCI function 組態空間內的偏移 & 讀 Vendor ID、電源暫存器
& 3 & EC 位址空間偏移(0–255) & 電池、風扇、溫度
& 4 & 交易式:偏移對應 command & SMBus 電池、溫度晶片
& 8 & Connection pin list 的位元索引 & 直接讀寫 GPIO pin
& 9 & 交易式:偏移 = command byte & I2C 裝置暫存器
& 0x0A & PCC subspace 內的偏移 & 與平台韌體信箱通訊
& 0x7F & 由 CPU 廠商定義 & 處理器功耗控制介面

九種空間可以分成三類來記:

  • 位址直通型(SystemMemory、SystemIO、PCI_Config、 EmbeddedControl):offset 就是某種「位址」,讀寫欄位 對應一筆匯流排交易。

  • 交易式(SMBus、GenericSerialBus、PCC):offset 其實是「命令編號」,一次欄位存取展開成一整筆協定交易, 資料以 buffer 進出(第 10 章的 BufferAcc、第 11 章實戰)。

  • 特殊型(GeneralPurposeIO、FFixedHW):GPIO 的 offset 是 pin 的位元索引,必須搭配 Connection(第 11 章); FFixedHW 的解讀由 CPU 廠商自訂,多用於處理器電源介面。

規格裡還有 (5)、PciBarTarget(6)、IPMI(7) 等較少見的空間,以及留給平台自訂的區段;日常 bring-up 會遇到的, 上表九種已經涵蓋。

SystemMemory 與 PCI_Config 實例

SystemIO 的例子已在上節出現(port 0x300),這節補齊另外兩個 最常用的空間。先看 SystemMemory——課綱指定的 MMIO 範例, 0xFED00000 是 Intel 平台 HPET 計時器的慣用基底:

OperationRegion (HPTM, SystemMemory, 0xFED00000, 0x400) Field (HPTM, DWordAcc, NoLock, Preserve) GCPL, 32, // 0x00: general capabilities, low DWord GCPH, 32 // 0x04: general capabilities, high DWord

Field 的語法下一章才正式教,這裡先看形狀:宣告窗口之後,把開頭 兩個 DWord 命名為 、,之後讀 就是讀實體位址 0xFED00000。再看 PCI_Config:

Device (SATA) Name (_ADR, 0x00170000) // device 0x17, function 0 OperationRegion (PCFG, PCI_Config, 0x00, 0x40) Field (PCFG, WordAcc, NoLock, Preserve) VID, 16, // config offset 0x00: Vendor ID DID, 16 // config offset 0x02: Device ID

PCI_Config 的 offset 是「某一顆 PCI function 的 config space 內偏移」。哪一顆?由 region 所在的 Device 決定: interpreter 沿著 Namespace 往上找,用該裝置的 (配合上層 bridge 鏈與 /)定位到 bus/device/function。所以這段 region 必須宣告在 裝置底下才有意義——搬到別的 Scope,讀到的就是別顆裝置的 config space。

Region 只是窗口:宣告不等於存取

一個容易誤解的重點:OperationRegion 的宣告不會產生任何 匯流排交易。interpreter 只是記下「有這麼一扇窗」;真正碰硬體的 時刻,是第一次讀寫掛在窗上的 Field Unit。這個 lazy 語意有兩個 實務後果:

  • 宣告錯誤不會當場爆炸。位址寫錯的 region 可以安靜地躺在 DSDT 裡好幾年,直到某天有人存取它的欄位。

  • 每種空間的存取都由對應的 代勞。 SystemMemory 與 SystemIO 的 handler 由 OS 內建、永遠可用; PCI_Config 名義上也內建,但依賴 PCI 列舉完成; EmbeddedControl、GenericSerialBus、GeneralPurposeIO 的 handler 則由對應的 driver 註冊,開機初期不存在。

handler 什麼時候可用?OS 會呼叫 Scope 內的 通知:

Scope (_SB.PCI0.LPCB.EC0) Name (ECOK, Zero) Method (_REG, 2, NotSerialized) If (Arg0 == 0x03) // 3 = EmbeddedControl space ECOK = Arg1 // 1 = handler ready, 0 = gone Method (SAFE, 0, NotSerialized) If (ECOK == Zero) Return (0xFF) // EC not ready: bail out Return (BTSC) // read an EC field safely

收兩個參數:Arg0 是空間 ID、Arg1 是可用性。 上例把「EC 就緒」記在 旗標裡,其他 method 存取 EC 欄位前先看旗標——第 8 章 warnbox 裡「觸控板消失」的事故, 就是少了這道門。

的 Arg0 用的正是上一節表格的 ID 欄(3 = EC、 9 = GenericSerialBus……),Arg1 為 1 表示 handler 就緒、 0 表示即將移除。另外注意:SystemMemory 與 SystemIO 永遠可用, 不會收到 ;PCI_Config 則要等 PCI 列舉完成—— 在開機極早期的 method(如 )裡讀 config space, 是經典的時序地雷。

OperationRegion 沒有任何安全網:interpreter 不會、也無從檢查 那段位址「屬不屬於你」。實務事故由輕到重:

  • 讀到不存在的 MMIO:匯流排回 0xFF,或直接觸發 machine check 當機。

  • SystemIO 誤寫 0xB2(SMI command port):觸發非預期 SMI, 韌體進入未知狀態。

  • SystemMemory 蓋到 SPI flash controller 的暫存器並亂寫: 可能改寫 flash 保護設定甚至內容,機器直接變磚。

宣告前逐一對照 chipset datasheet;並記住 lazy 語意的陰暗面—— 宣告當下不會爆,第一次存取才爆,出事的呼叫堆疊往往 離元凶很遠。

綜合範例:Debug Port 裝置

把第 8 章的 Device 骨架與本章的 region 合體:一張插在 port 0x300 的 POST code debug 卡,完整可編譯:

DefinitionBlock ("dbg0.aml", "SSDT", 2, "ACME", "DBGPORT", 1) Scope (_SB) Device (DBG0) Name (_HID, "ACME0100") // debug port card Name (_UID, 0) Method (_STA, 0, NotSerialized) Return (0x0F) Name (_CRS, ResourceTemplate () IO (Decode16, 0x0300, 0x0300, 0x01, 0x10) ) OperationRegion (REG0, SystemIO, 0x300, 0x10) Field (REG0, ByteAcc, NoLock, Preserve) PSTC, 8 // offset 0x00: POST code register Method (POST, 1, NotSerialized) PSTC = Arg0 // write a code to the card

逐行拆解:

  • 用 巨集向 OS 申報 「port 0x300 起 16 個 port 是我的」。

  • 向 interpreter 開窗——注意位址與 申報的完全一致,這是鐵律:兩者各走各的,沒有人幫你核對。

  • 把窗口第一個 byte 命名為 ; method 寫它,等同對 port 0x300 做一次 指令。

  • 之後 AML 任何地方呼叫 (例如在 裡打進度碼),debug 卡上的數字 就會跳動——AML 版的 printf 除錯就是這麼做的。

  • OperationRegion 四參數:名稱、address space、offset、 length;offset 的意義由空間決定。

  • 九種空間三類記憶法:位址直通(Memory/IO/PCI_Config/ EC)、交易式(SMBus/GenericSerialBus/PCC)、特殊 (GPIO/FFixedHW)。

  • PCI_Config 的 offset 相對於「region 所在 Device」的 config space,靠 對號。

  • 宣告是 lazy 的:不碰硬體,第一次欄位存取才透過 region handler 碰硬體。

  • EC、GenericSerialBus 等空間的 handler 由 driver 註冊, 是就緒通知;存取前先檢查旗標。

  • 說給 OS 聽、OperationRegion 說給 interpreter 聽,位址必須人工保持一致。

  1. 不看書寫出 OperationRegion 的四個參數,並各用一句話 說明用途。

  2. 替下列需求選 address space:讀 EC 的電池電量、讀 PCI 裝置的 Vendor ID、寫 0xFED90000 的 MMIO 暫存器、 扳一根 GPIO pin、讀 I2C 溫度感測器。

  3. 為什麼位址寫錯的 region 往往潛伏很久才出事?出事時的 呼叫堆疊為什麼常常指不到元凶?

  4. 寫出一個 method 與旗標,讓 GenericSerialBus (空間 ID 9)的欄位存取有就緒保護。

  5. 動手做:仿照綜合範例寫一份 SSDT,宣告 CMOS 的 IO 窗口 (port 0x70 起 2 byte),以 編譯通過。 (下一章會用 IndexField 真正操作它。)

Field

窗開好了,這一章動刀: 把 OperationRegion 切成 一個個具名的 ——從此「讀寫一個變數」就是 「讀寫一顆硬體暫存器」。這是 ASL 最漂亮的抽象:C driver 工程師 眼裡的 / 與 MMIO 指標,在 ASL 裡 全部化成一個等號。

但等號背後藏著三個必答題:一次交易敲多寬(AccessType)? 要不要跟韌體搶鎖(LockRule)?沒被寫到的位元怎麼辦(UpdateRule)? 本章逐一拆解,最後用 IndexField 與 BankField 收拾兩種 「不走尋常路」的暫存器。

  • 用 Field 把 region 切成 Field Unit,寫出等號與 兩種讀寫寫法。

  • 依硬體匯流排寬度選擇 AccessType,說明 BufferAcc 的 特殊角色。

  • 說明 Lock 與 Global Lock 解決的並發問題。

  • 畫出三種 UpdateRule 對「寫 4-bit 欄位進 8-bit 暫存器」 的位元變化。

  • 用 與位元欄位對齊 datasheet,寫出跨 byte 的欄位。

  • 寫出 IndexField 描述 index/data 暫存器對,說明 BankField 的適用場景。

Field:把窗口切成具名暫存器

先看課綱指定的範例——把上一章的 切成三顆暫存器:

OperationRegion (REG0, SystemIO, 0x300, 0x10) Field (REG0, ByteAcc, NoLock, Preserve) CTRL, 8, // offset 0x00 -> port 0x300: control STAT, 8, // offset 0x01 -> port 0x301: status DATA, 8 // offset 0x02 -> port 0x302: data

Field 的第一個參數是 region 名稱,後三個參數是「存取規則」, 本章後面各給一節。大括號內是欄位清單:依宣告順序 從 region 開頭排下去,每項給名稱與位元寬度。排出來的版圖如下:

SystemIO port : 0x300 0x301 0x302 0x303 .. 0x30F Field Unit : CTRL(8) STAT(8) DATA(8) (未切割區域) │ └── CTRL = 1 ← 展開成一筆 out 0x300, 0x01

之後讀寫這些名字,interpreter 就替你產生對應的 IO 交易:

Method (DEMO, 0, NotSerialized) CTRL = 1 // ASL 2.0 style: out 0x300, 0x01 DATA = 0x55 // out 0x302, 0x55 Store (0x55, DATA) // legacy style, exactly the same Local0 = STAT // read: in 0x301

課綱指定的 、 是 ASL 2.0 的 symbolic 寫法;老 DSDT 裡等價的樣子是 。 兩者編譯出的 AML 完全相同,讀舊碼、寫新碼都得認得。 Field Unit 的物件型別是 :讀取時,寬度不超過 64 bit 的欄位轉成 Integer 回來,更寬的欄位回 Buffer (Module 3 的轉型規則在此適用)。

對 Field Unit 的每一次讀寫都是真實的匯流排交易——它不是記憶體 變數,沒有快取、沒有折疊。連續讀同一個欄位兩次,硬體就被敲兩次; 對 read-to-clear 的暫存器,這個差別是天壤之別(見本章 warnbox)。

AccessType:一次交易敲多寬

Field 的第二個參數告訴 interpreter:對這個 region 的每筆交易 應該用多大的存取單位。

llL6.8cm AccessType & 交易寬度 & 使用時機
& 8 bit & IO port、EC、多數 legacy 裝置
& 16 bit & 16-bit 暫存器組(老式 PCI 欄位)
& 32 bit & 絕大多數 MMIO 控制器
& 64 bit & 64-bit MMIO(HPET 計數器等)
& interpreter 自選 & 硬體不挑寬度時的方便選項
& 一整包 buffer & 交易式空間:SMBus、 GenericSerialBus、IPMI

AccessType 本質上是對硬體的承諾。datasheet 常見這種話: 「本暫存器僅支援 32-bit 對齊存取」——這時必須用 ; 若貪方便寫 ,interpreter 會產生四筆 8-bit 交易, 硬體可能直接忽略或行為錯亂,而 AML 端不會有任何錯誤訊息。 反過來,欄位比存取寬度窄或跨越邊界時,interpreter 會自動用 多筆交易讀出、組合、寫回(代價見下一節的 UpdateRule)。

是完全不同的物種:欄位讀寫不再是「位址上的值」, 而是一整包協定交易的資料緩衝區,用於 SMBus、GenericSerialBus 這類「一次存取等於一筆匯流排協定交易」的空間。第 11 章的 I2C 感測器會完整示範。順帶一提:EC 介面天生一次一個 byte, EmbeddedControl region 用 就對了。

LockRule:與韌體共搶硬體

第三個參數只有兩個值: 與 。寫 時,interpreter 在每次欄位存取前取得 ,結束後釋放。

為什麼需要它?歷史原因是 OS 與韌體共用硬體。EC 與 CMOS 這類資源,OS 的 AML 在存取,SMM 韌體(處理 SMI 的那段 BIOS 碼) 也在存取;雙方各自 read-modify-write,不加鎖就會互相踩掉對方的 修改。ACPI 因此定義了一把橫跨 OS 與韌體的鎖。

Global Lock 本體是 FACS 表裡的一個 32-bit 欄位,OS 與韌體用 規格定義的 compare-and-swap 協定競爭;搶不到的一方設下 pending 位元,持有者釋放時透過中斷或 SMI 通知對方再來。細節屬於韌體 架構課,寫 ASL 只需記住:datasheet 或平台文件說 SMM 也會碰的暫存器,Field 就宣告 ;其餘一律 ,因為拿鎖有實際的效能與死鎖風險成本。

UpdateRule:沒被寫到的位元怎麼辦

第四個參數回答一個微妙的問題。假設欄位只佔暫存器的一部分:

Field (REG0, ByteAcc, NoLock, Preserve) , 4, // bits 0-3: reserved, skip (anonymous) CTL4, 4 // bits 4-7: control nibble

寫 時,交易單位是一整個 byte,但你只指定了 高 4 位——低 4 位該寫什麼?三種 UpdateRule 三種答案:

位元編號 : 7 6 5 4 3 2 1 0 暫存器現值 : 1 0 1 0 0 1 1 0 (0xA6) └─────┘ └─────┘ CTL4 欄位 未被寫到的位元

Preserve : 0 0 1 1 0 1 1 0 (0x36) ← 先讀 0xA6,改高 4 位,寫回(讀改寫) WriteAsOnes : 0 0 1 1 1 1 1 1 (0x3F) ← 沒寫到的位元一律補 1 WriteAsZeros : 0 0 1 1 0 0 0 0 (0x30) ← 沒寫到的位元一律補 0

三者的成本與風險:

  • :先讀後寫,兩筆交易。最直覺、最常用, 鄰居位元保持原狀;但多出一次隱藏的讀取。

  • /:不讀,直接寫, 一筆交易。用在「其餘位元寫 1(或 0)無副作用」的硬體, 或刻意避開隱藏讀取時。

狀態暫存器常有兩種特殊行為,跟 Field 的預設習慣正面相撞:

  • read-to-clear(RC):讀取即清零。 寫入鄰近欄位時的那次隱藏讀取,會把還沒被 driver 看到的 狀態位元讀掉;AccessType 太寬「掃到」鄰居暫存器時也一樣。 中斷旗標就這樣無聲消失,事件永遠等不到。

  • write-1-to-clear(W1C):寫 1 清零。 會把讀到的 1 原封不動寫回去——等於把 所有 pending 位元一次清光。這種暫存器該用 (0 是無操作)。

對策:把 RC/W1C 暫存器獨立切成自己的 Field,選對 UpdateRule 與 AccessType,別讓它跟一般欄位共用一條交易路徑。 datasheet 上的 access 欄(RO/RW/RC/W1C)就是你的切割地圖。

Offset()、位元欄位與跨 byte 欄位

真實 datasheet 的暫存器版圖不會乖乖連續。三個工具讓 Field 精準對齊: 跳到指定 byte、匿名欄位「」 跳過 n 個 bit、任意 bit 寬度的欄位(可以只有 1 bit,也可以 跨越 byte 邊界)。以筆電 EC 為例:

OperationRegion (ECRM, EmbeddedControl, 0x00, 0xFF) Field (ECRM, ByteAcc, Lock, Preserve) Offset (0x20), BTSC, 8, // 0x20: battery state of charge ( Offset (0x23), LIDS, 1, // 0x23 bit 0: lid open = 1 ACIN, 1, // 0x23 bit 1: AC adapter present , 6, // 0x23 bits 2-7: reserved Offset (0x30), TMP0, 16 // 0x30-0x31: temperature, 0.1 K units

逐行拆解:

  • :游標直接跳到 region 內偏移 0x20, 中間的 0x00–0x1F 不命名、也永遠不會被這個 Field 碰到。

  • :1-bit 欄位。第 1 章那個 的謎底在此——讀它,interpreter 讀出 0x23 整個 byte,右移、遮罩,只回傳 bit 0。

  • :匿名跳過保留位元,讓後面的欄位對齊。

  • :跨兩個 byte 的欄位。 之下 interpreter 拆成兩筆交易再拼裝——不是 atomic, 若硬體在兩筆交易之間更新數值,你會讀到「上半新、下半舊」 的縫合怪。對會跳動的計數器,寧可分開讀兩個 byte 再自行判斷。

  • 注意這個 Field 宣告了 :EC 正是「OS 與韌體 共搶」的經典案例。

IndexField 與 BankField

有些硬體暫存器太多、位址口太小,於是發明了「間接存取」: 先把想存取的暫存器編號寫進 index port,再從 data port 讀寫值。CMOS RTC(port 0x70/0x71)、 SuperIO(0x2E/0x2F)都是這一族。手寫兩步驟太囉唆, 把它自動化:

OperationRegion (CMOS, SystemIO, 0x70, 0x02) Field (CMOS, ByteAcc, NoLock, Preserve) CIDX, 8, // port 0x70: index register CDAT, 8 // port 0x71: data register IndexField (CIDX, CDAT, ByteAcc, NoLock, Preserve) RSEC, 8, // CMOS 0x00: RTC seconds Offset (0x02), RMIN, 8, // CMOS 0x02: RTC minutes Offset (0x04), RHRS, 8, // CMOS 0x04: RTC hours Offset (0x0E), DIAG, 8 // CMOS 0x0E: diagnostic status Method (GSEC, 0, NotSerialized) Return (RSEC) // = out 0x70, 0x00 then in 0x71

拆解:先用普通 Field 把 index/data 兩個 port 命名為 、; 再宣告一層虛擬 region——欄位的 offset 不是 IO 位址, 而是「要寫進 index port 的編號」。讀 時 interpreter 自動做兩步:把 0x00 寫進 ,再從 讀值。 (小提醒:某些晶片組的 port 0x70 bit 7 兼任 NMI 遮罩, 動 CMOS 前先讀平台文件。)

處理另一種怪招——「分頁」暫存器:同一段 位址視 bank 暫存器的值切換內容。宣告時多帶 bank 暫存器與 bank 值兩個參數:

Field (REG0, ByteAcc, NoLock, Preserve) Offset (0x0F), BSEL, 8 // 0x0F: bank select register BankField (REG0, BSEL, 0, ByteAcc, NoLock, Preserve) R0A0, 8 // bank 0, offset 0x00 BankField (REG0, BSEL, 1, ByteAcc, NoLock, Preserve) R1A0, 8 // bank 1, offset 0x00

存取 時,interpreter 先寫 切頁, 再存取 offset 0x00。SuperIO 的 logical device 切換是最常見的 應用場景。

綜合範例:MMIO 控制器

集大成:一顆虛構的 MMIO 控制器,同一個 region 疊兩層 Field (整暫存器視角與位元視角),加上輪詢邏輯,完整可編譯:

DefinitionBlock ("ctl0.aml", "SSDT", 2, "ACME", "MMIOCTRL", 1) Scope (_SB) Device (CTL0) Name (_HID, "ACME0200") Name (_UID, 0) Name (_CRS, ResourceTemplate () Memory32Fixed (ReadWrite, 0xFED91000, 0x100) ) OperationRegion (CREG, SystemMemory, 0xFED91000, 0x100) Field (CREG, DWordAcc, NoLock, Preserve) CTRL, 32, // 0x00: control register STAT, 32, // 0x04: status register DATA, 32 // 0x08: data register Field (CREG, DWordAcc, NoLock, Preserve) ENAB, 1, // 0x00 bit 0: enable RSTB, 1, // 0x00 bit 1: soft reset , 30, // 0x00 bits 2-31: reserved RDYB, 1, // 0x04 bit 0: ready ERRB, 1 // 0x04 bit 1: error Method (INIT, 0, Serialized) RSTB = One // pulse soft reset RSTB = Zero ENAB = One // enable the controller Method (XFER, 1, Serialized) Local0 = 100 // timeout: 100 polls While ((RDYB == Zero) && (Local0 > 0)) Stall (10) // wait 10 microseconds Local0– If (Local0 == Zero) Return (Zero) // timed out DATA = Arg0 // push the payload Return (One)

逐行拆解:

  • 同一個 疊了兩個 Field:第一層以 32-bit 為 單位看整顆暫存器,第二層鑽進位元。兩層描述同一塊硬體, interpreter 不在乎重疊——這是對照 datasheet 寫碼的 常用手法。

  • 對 寫 1 再寫 0,做出 reset 脈衝;每次寫入在 之下都是 「讀 CTRL、改 bit 1、寫回」。

  • 輪詢 ,每輪 忙等 10 微秒; 不會讓出 CPU,單次別超過 100 微秒,長等待改用 (Module 6 詳述)。

  • 與 條件裡的 都是 ASL 2.0 語法;legacy 等價寫法是 與 。

  • 讀 每次都是真實的 MMIO 讀取——輪詢會不斷 敲硬體,這正是 Field Unit「會碰硬體的變數」本色。

  • Field 把 region 依宣告順序切成 Field Unit;讀寫欄位 就是真實的匯流排交易。

  • AccessType 是對硬體的承諾:交易寬度要符合 datasheet; BufferAcc 專屬交易式空間。

  • 讓存取包在 Global Lock 裡,用於 OS 與 SMM 韌體共搶的硬體(EC、CMOS)。

  • UpdateRule 決定沒被寫到的位元: 讀改寫、 / 直接補 1/0; RC 與 W1C 暫存器是最大陷阱。

  • 、匿名欄位與位元欄位讓版圖精準對齊 datasheet;跨存取寬度的欄位不是 atomic。

  • IndexField 自動化 index/data 間接存取;BankField 處理分頁暫存器。

  1. Field 的三個規則參數(AccessType、LockRule、UpdateRule) 各自回答什麼問題?

  2. 8-bit 暫存器現值 0xA6,在 之下寫 (bits 4–7),暫存器變成多少? 之下呢?各發生幾筆匯流排交易?

  3. 某中斷狀態暫存器是 W1C:為什麼 會誤清 pending 位元?該選哪個 UpdateRule?

  4. 用 IndexField 描述 SuperIO(index 0x2E、data 0x2F), 並寫一個 method 讀出 offset 0x20 的 chip ID。

  5. 動手做:在綜合範例中新增 16-bit 欄位 (offset 0x0C),寫一個 method 回傳版本號, 以 編譯驗證。

GPIO / I2C / SPI / PCI 實戰

現代機器的裝置掛在五花八門的匯流排上:觸控板在 I2C、感測器在 SPI、藍牙在 UART、LED 與開關在 GPIO、大傢伙在 PCI。本章把 Module 4 的 Device 骨架與前兩章的 region/field 機制組裝起來, 逐一寫出這些匯流排的標準描述;貫穿全章的主軸只有一條: controller 與 consumer 的分工。

  • 區分 controller 與 consumer,說明 ResourceSource 的指向。

  • 寫出 GPIO controller 與 GpioIo/GpioInt consumer, 並用 GeneralPurposeIO region 直接讀寫 pin。

  • 寫出 I2C 感測器的 I2cSerialBusV2 連線與 GenericSerialBus 存取 method;SPI 與 UART 比照辦理。

  • 描述 PCI root bridge 與子裝置,讀取 config space。

  • 佈局一份含 EC、I2C 感測器、GPIO LED 的 namespace 全景。

Controller 與 Consumer:兩種角色

一條 I2C 匯流排上有兩種東西:(主控端, SoC 裡的 I2C host)與 (掛在線上的裝置, 例如溫度感測器)。ACPI 描述它們的方式截然不同:

  • Controller 申報實體資源:它是一顆平台裝置, 放 與 ;PC 上常是 PCI 裝置(), SoC 平台常以 列舉。

  • Consumer 申報連線: 放 、 這類 ——不佔位址,只申報 「一條通往 controller 的線」,用 路徑字串寫明「我掛在哪顆 controller、參數為何」。 OS 讀到它,就把 consumer 掛到對應 controller 的匯流排 底下 probe,driver 再經由 controller driver 存取 consumer。

GPIO:從 Controller 到扳一根 Pin

先立 controller。GPIO controller 是一顆平台裝置,用 列舉、申報自己的 MMIO 與中斷; (ACPI Event Interrupts)也掛在它身上,宣告「哪些 pin 觸發時 請 OSPM 執行對應的 event method」:

Device (GPI0) Name (_HID, "INT34C5") // Intel TGL GPIO controller Name (_CRS, ResourceTemplate () Memory32Fixed (ReadWrite, 0xFD6E0000, 0x00010000) Interrupt (ResourceConsumer, Level, ActiveLow, Shared) 14 ) Name (_AEI, ResourceTemplate () GpioInt (Level, ActiveLow, ExclusiveAndWake, PullUp, 0, "
_SB.GPI0", 0, ResourceConsumer, , ) 43
) Method (_L2B, 0, NotSerialized) // lid pin 43 = 0x2B Notify (_SB.LID0, 0x80) // status change event

的參數依序是:觸發(/)、 極性(//)、 共享與喚醒(:此 pin 可在睡眠中喚醒 系統)、上下拉、去彈跳時間(單位 0.01 ms,0 = 不指定), 之後是所有 GPIO 巨集共通的尾巴: (controller 路徑)、index、usage、名稱、vendor data, 大括號內是 pin list。事件 method 的命名規則:pin 編號轉 十六進位接在 (level)或 (edge)後—— pin 43 = 0x2B 就是 ;編號超過 255 改用 。

consumer 端一次到位: 用 把 pin 申報給 driver; region(課綱指定寫法) 加上 ,讓 AML 自己扳 pin:

Device (LED0) Name (_HID, "ACME1103") Name (_CRS, ResourceTemplate () GpioIo (Exclusive, PullNone, 0, 0, IoRestrictionOutputOnly, "
_SB.GPI0", 0, ResourceConsumer, , ) 27 // LED drive pin
) OperationRegion (GPIO, GeneralPurposeIO, 0, 1) Field (GPIO, ByteAcc, NoLock, Preserve) Connection (GpioIo (Exclusive, PullNone, 0, 0, IoRestrictionOutputOnly, "
_SB.GPI0", 0, ResourceConsumer, , ) 27 ), LEDP, 1 // bit 0 = 1st pin in the list
// LEDP = One drives the pin high

逐參數解說(依序):(pin 是否 可共用,幾乎都寫 Exclusive)、(內建上下拉: PullUp/PullDown/PullNone/PullDefault 沿用韌體設定)、 (去彈跳與驅動強度,0 = 預設值)、 (方向限制:None/InputOnly/ OutputOnly)、(controller 完整路徑, ASL 字串中反斜線要寫兩次),尾巴同 :index、 usage、名稱、vendor data,大括號內是 pin list (controller 相對編號)。

region 這一段三明治再拆一次: 開 1 byte 的虛擬窗(容納 8 根 pin 的 位元欄位); 宣告「接下來的欄位走這條 GPIO 線」,欄位位元索引對應 pin list 順序,bit 0 是第一根; 於是 就把 pin 27 拉高——經 controller driver 的 region handler 完成,同樣受 約束。注意 的 給 driver 看、 給 AML 用;兩份指同一根 pin, 慣例寫成一模一樣。

pin list 裡的數字是相對於 ResourceSource 那顆 controller 的索引——不是 SoC datasheet 的 global GPIO 編號, 也不是 Linux gpiochip 的編號;三套對不齊,輕則 LED 不亮, 重則扳到電源樹訊號直接關機。且 是 不被編譯器驗證的字串:路徑打錯 iasl 照樣編過, 直到 OS probe 失敗才露餡。改 GPIO 前先對 schematic 與 controller 文件把 pin 索引算兩遍。

I2C:連線描述與 GenericSerialBus 存取

同一套劇本換到 I2C。controller()與 同一個模子—— 加 MMIO 加中斷,不再重列; 直接看溫度感測器 consumer:

Device (TMP1) Name (_HID, "ACME1102") // I2C temperature sensor Name (_CRS, ResourceTemplate () I2cSerialBusV2 (0x0048, ControllerInitiated, 400000, AddressingMode7Bit, "
_SB.I2C1", 0, ResourceConsumer, , Exclusive, )
)

逐參數解說(依序): 是 slave address(右對齊,不含讀寫位元); 表示交易由 controller 發起(幾乎永遠如此); 是 連線速度(Hz),即 Fast Mode 400 kHz; 是 7-bit 定址(另有 10Bit);再來是 ResourceSource(指向 controller),其後依序為 index、usage、名稱(留空)、 (V2 新增的共享旗標)與 vendor data(留空)。

driver 有了這行就能透過 I2C subsystem 收發。但如果AML 自己要跟感測器講話(例如 想確認它活著),就要 動用第 9 章表格裡的交易式空間 :

Scope (_SB.TMP1) OperationRegion (SBUS, GenericSerialBus, 0x00, 0x100) Field (SBUS, BufferAcc, NoLock, Preserve) Connection (I2cSerialBusV2 (0x0048, ControllerInitiated, 400000, AddressingMode7Bit, "
_SB.I2C1", 0, ResourceConsumer, , Exclusive, )), AccessAs (BufferAcc, AttribByte), TEMP, 8, // command 0x00: temperature CONF, 8 // command 0x01: configuration
Method (RTMP, 0, Serialized) Name (BUFF, Buffer (0x22) ) CreateByteField (BUFF, 0x00, STAT) CreateByteField (BUFF, 0x02, DATA) BUFF = TEMP // issue an I2C read-byte If (STAT == 0x00) // 0 = transfer OK Return (DATA) Return (0xFF) // error marker

這段是本章資訊密度最高的範例,逐層拆解:

  • : 虛擬 region——offset 不是位址,而是 command byte(裝置內的暫存器編號)。 排在 offset 0 就代表 command 0x00, 是 command 0x01。

  • :指定後續欄位的 存取協定為 read/write byte;其他常用協定有 、 與 (固定 n byte,I2C 常用)。

  • BufferAcc 的資料不是純量而是一包 buffer,格式固定: offset 0 是 狀態(0 = 成功,非零 = 錯誤碼)、 offset 1 是長度(block 類協定才有意義)、 offset 2 起是資料。 就在這包 buffer 上開具名視窗。

  • 讀寫方向由等號方向決定: 觸發一筆 I2C read-byte 交易,結果落進 ;反過來寫 就是一筆 write-byte。任何交易後 先檢查 再用資料——I2C 是會 NAK 的。

SPI 與 UART:其他序列匯流排

SPI 與 UART 的 consumer 寫法與 I2C 同一個模子,換巨集與參數 而已(每個 SerialBus 巨集都有不帶 V2 的舊版;V2 多了 Shared 旗標,新碼一律用 V2):

Device (ADC0) Name (_HID, "ACME1104") // SPI analog-digital converter Name (_CRS, ResourceTemplate () SpiSerialBusV2 (0, PolarityLow, FourWireMode, 8, ControllerInitiated, 1000000, ClockPolarityLow, ClockPhaseFirst, "
_SB.SPI1", 0, ResourceConsumer, , Exclusive, )
)

Device (BTH0) Name (_HID, "ACME1105") // Bluetooth module on UART Name (_CRS, ResourceTemplate () UartSerialBusV2 (115200, DataBitsEight, StopBitsOne, 0x00, LittleEndian, ParityTypeNone, FlowControlNone, 64, 64, "
_SB.UAR1", 0, ResourceConsumer, , Exclusive, )
)

參數重點(依序):chip select 編號 (0 = controller 的 CS0)、CS 極性( 為 常態)、線制(FourWire/ThreeWire)、資料位寬(8)、發起端、 時脈 1 MHz,然後是決定 SPI mode 的兩兄弟:Polarity 管 CPOL、Phase 管 CPHA, + = mode 0,四種組合對應 mode 0–3, 抄 datasheet 的時序圖就不會錯。

參數重點(依序):鮑率 115200、資料 位元、停止位元、LinesInUse(bitmask,宣告用到的 modem 控制線, 0x00 = 只有 TX/RX)、位元組序、同位、流控(硬體流控寫 並在 LinesInUse 設 RTS/CTS)、 建議收發緩衝 64 byte,最後同樣以 ResourceSource 指向 controller。

PCI:Root Bridge、_ADR 與 Config Space

PCI 是「可自我列舉」匯流排的代表:子裝置不需要 , 用第 8 章的 對號即可;但樹的根——PCI root bridge——OS 沒辦法自己發現,必須由 ACPI 以 立起來:

Device (PCI0)

Name (_HID, EisaId ("PNP0A08")) // PCIe root bridge Name (_CID, EisaId ("PNP0A03")) // PCI compatible

Device (SATA) Name (_ADR, 0x00170000) // dev 0x17, func 0 OperationRegion (PCFG, PCI_Config, 0x00, 0x40) Field (PCFG, WordAcc, NoLock, Preserve) VID, 16, // 0x00: Vendor ID DID, 16 // 0x02: Device ID Method (PRSN, 0, NotSerialized) Return (VID != 0xFFFF) // all-ones = empty

拆解:

  • 是 PCIe root bridge 的標準 ID,慣例搭配 ;完整的 root 還要以 / 宣告 segment 與 bus 編號、以 申報 bus 範圍與 MMIO window(留給 Module 8)。

  • 以 對應 device 0x17、function 0;PCI_Config region 的 offset 相對 這顆 function 的 config space。

  • 讀 Vendor ID:PCI 規範空槽讀回全 1,所以 是「裝置在不在」的慣用判斷。

SystemMemory 與 SystemIO:完整裝置範例

回頭補上兩個最樸素的空間,呼應第 9 章:HPET 計時器 (MMIO 0xFED00000)與 debug 卡(port 0x300):

Device (TIM0) Name (_HID, EisaId ("PNP0103")) // HPET Name (_CRS, ResourceTemplate () Memory32Fixed (ReadOnly, 0xFED00000, 0x400) ) OperationRegion (MEM0, SystemMemory, 0xFED00000, 0x1000) Field (MEM0, DWordAcc, NoLock, Preserve) CAPL, 32, // 0x00: capabilities, low CAPH, 32 // 0x04: capabilities, high Device (DBG0) Name (_HID, "ACME0100") // debug port card Name (_CRS, ResourceTemplate () IO (Decode16, 0x0300, 0x0300, 0x01, 0x10) ) OperationRegion (REG0, SystemIO, 0x300, 0x10) Field (REG0, ByteAcc, NoLock, Preserve) PSTC, 8 // 0x00: POST code register // PSTC = 0x55 emits a POST code

兩顆裝置各自示範一種空間,共同示範同一條鐵律: 申報給 OS 的位址與 OperationRegion 開窗的位址必須人工 保持一致。region 超出實際硬體範圍去存取,就是第 9 章 warnbox 的劇本。

綜合佈局:EC、I2C 感測器與 GPIO LED

最後把全章角色排進同一棵 Namespace,看清楚彼此的關係:

 └── _SB_ ├── GPI0 ← GPIO controller:MMIO + IRQ 14 │ ├── _AEI GpioInt 43 ← lid 事件來源 │ └── _L2B ← pin 43 的事件 method ├── I2C1 ← I2C controller:MMIO + IRQ 32 ├── PCI0 │ └── LPCB │ └── EC0_ ← EC:PNP0C09,IO 0x62/0x66,_REG ├── TMP1 ← I2cSerialBusV2 0x48 → 指向 I2C1 ├── LED0 ← GpioIo 27 → 指向 GPI0 └── LID0 ← 上蓋開關,_LID(第 1 章的老朋友)

讀圖重點:controller 與 EC 各自申報實體資源;consumer 平行 掛在 下——樹的位置不決定連線關係, 指向全靠 。事件鏈完整可循:lid 翻動、 pin 43 觸發、OSPM 執行 、 打向 LID0、OS 呼叫 讀狀態——第 1 章的場景, 如今每一站你都寫得出來。

  • Controller 申報實體資源(MMIO+中斷);consumer 用 connection descriptor 申報「線」,以 ResourceSource 指回 controller——路徑與 pin 編號編譯器不驗證, bring-up 前先算兩遍。

  • GPIO:/ 描述 pin; GeneralPurposeIO region 加 讓 AML 直接扳 pin; 把 pin 變成事件。

  • I2C: 給 driver 掛線; GenericSerialBus region 加 讓 AML 自己 交易,buffer 格式是 status/length/data。

  • SPI 用 chip select 與 CPOL/CPHA 定 mode;UART 申報 鮑率、格式與流控——同一套 consumer 模板。

  • PCI root bridge 以 立根,子裝置用 對號、PCI_Config region 讀組態空間。

  1. controller 與 consumer 的 內容有什麼本質 差異?OS 分別拿它們做什麼?

  2. pin list 裡的 27 屬於哪一套編號空間? 為什麼對應的 Field Unit 只有 1 bit?

  3. 寫出一顆 SPI 溫度感測器的 consumer 描述:掛在 的 CS1、mode 3、2 MHz、8-bit。

  4. 動手做:把本章的 GPI0、TMP1、LED0 組成一份 SSDT, 以 編譯通過,並手繪 namespace 樹與 「誰指向誰」的連線圖。