這是你第一次不跟著課文抄範例,而是以工程師的身分交付一個「產品」: 一個從零開始的虛擬 ACPI 裝置。它只有一個 、一個 和一個會回傳 1234 的 Method,小得不能再小, 但五臟俱全——你會走完 ASL 生產線的每一站:撰寫、編譯、 在 acpiexec 裡執行、最後塞進 QEMU 或真機的 Namespace, 在 底下親眼看到它。

之後四個專案的所有基礎設施(工作目錄、編譯流程、驗證手法) 都在本章建立,請把每一步都確實做過,不要只用讀的。

專案目標與驗收標準

專案目標

  • 寫出一個完整、可編譯的 SSDT,內含裝置 : 、、 與方法 。

  • 說明 Namespace、Device、Method 三者的關係: 裝置掛在樹的哪裡、OS 怎麼找到它、方法怎麼被呼叫。

  • 熟練 iasl 與 acpiexec 的基本工作循環(第 21 章工具鏈的實戰化)。

  • 用 initrd override 把自製 SSDT 載入 QEMU 或真機。

驗收標準——完成後你必須能拿出以下證據:

  • 輸出 。

  • acpiexec 中 回傳 (即十進位 1234)。

  • QEMU 或真機的 出現 ,其 檔內容為 。

背景與設計

為什麼用 SSDT,而不是改 DSDT?

第 1 章介紹過表鏈結構:DSDT 是主表、開機必載,SSDT 則是可以有很多張的 「補充表」。DSDT 屬於 OEM,改它等於動手術;SSDT 則像外掛模組, OS 載入後會把內容「嫁接」到同一棵 Namespace 樹上(Module 8 的核心觀念)。 自製裝置一律先做成 SSDT——錯了就拔掉,不會弄壞原廠描述。 這也是業界替既有平台補描述(例如加一顆新感測器)的標準做法。

用到前面哪些章的知識

  • 第 2 章:ASL 經 iasl 編譯成 AML; 的參數就是表頭欄位的來源。

  • 第 3 章:Namespace 樹與 掛載點—— 所有實體裝置都活在 System Bus 底下。

  • 第 8 章:// 這組「身分證三件套」的格式與位元定義。

  • 第 12 章:Method 的宣告、 與 物件。

  • 第 21 章:iasl 與 acpiexec 的操作。

  • Module 10:Linux 的 initrd table override 與 觀察點。

開發循環與目標 Namespace

本專案(以及之後所有專案)都遵循同一條開發循環:

先在 acpiexec 裡把邏輯調到對,再上機——模擬器的一輪迭代只要幾秒, 上機驗證一輪至少要一次重開機,順序反了就是浪費生命。 載入成功後,Namespace 會長出這樣的新枝:

 (root) ├── _SB_ ← 既有的 System Bus Scope │ ├── ... ← 原有裝置(DSDT 描述) │ └── DEV0 ← 本專案新增(來自 SSDT) │ ├── _HID "DEMO0001" ← OS 據此比對 driver │ ├── _UID 0 ← 同型裝置的流水號 │ ├── _STA Method ← 回報存在狀態 │ └── TEST Method ← Return (1234) └── _TZ_ ...

字串型 只有兩種合法長度:7 字元(3 個大寫字母的 vendor ID+4 位十六進位,如 )或 8 字元 (4 個字元的 vendor ID+4 位十六進位,如本專案的 )。 iasl 會檢查格式,寫成 或 都過不了關。 第 8 章介紹的 則是把 7 字元格式壓縮成 32 位元整數的舊式寫法,兩者在 AML 層面等價。

逐步實作

  1. 建立工作目錄 ,之後每個專案一個目錄, 養成把 、、筆記放在一起的習慣。

  2. 用編輯器建立 ,輸入下方完整程式碼。

  3. 執行 ,確認零錯誤零警告。

  4. 進入 acpiexec 互動模式,用 與 驗證(下一節)。

  5. 打包 initrd,丟進 QEMU 或真機,檢查 (下一節)。

完整程式碼如下,總共不到三十行:

DefinitionBlock ("project1.aml", "SSDT", 2, "DEMO", "PROJ1", 0x00000001)

Scope (_SB)

Device (DEV0)

Name (_HID, "DEMO0001") // 4-char vendor + 4 hex Name (_UID, Zero) // instance number

Method (_STA, 0, NotSerialized) Return (0x0F) // present+enabled+shown+ok

Method (TEST, 0, NotSerialized) Debug = "DEV0.TEST called" Return (1234) // = 0x4D2

逐段拆解

DefinitionBlock 標頭——六個參數依序是:AML 輸出檔名 (現代 iasl 其實以命令列為準,此欄位僅存於原始碼)、表簽章 、ComplianceRevision 、OEM ID(最多 6 字元)、 OEM Table ID(最多 8 字元)、OEM Revision。兩個細節值得注意:

  • ComplianceRevision \(\geq 2\) 代表整數是 64 位元 (第 2 章)。之後你會看到 顯示成 ,就是這個欄位的效果。

  • OEM ID 與 Table ID 是你在 dmesg 裡認出自己表的依據—— 載入時核心會印出 與 , 取個有辨識度的名字,除錯時會感謝自己。

——SSDT 不是重新種一棵樹, 而是宣告「以下內容接到既有的 底下」。 第 3 章說過:Scope 打開既有節點,Device 建立新節點。 如果這裡誤用 ,會變成企圖重複定義 System Bus。

身分證三件套—— 決定 OS 拿哪個 driver 來配對; 在同型裝置有多顆時區分個體; 回報狀態位元 (第 8 章):bit 0 存在、bit 1 已啟用、bit 2 顯示於 UI、 bit 3 功能正常, 就是四者全開。省略 時 OS 視同全開,本專案仍刻意寫出,因為 Project 4 會開始操作它。

方法—— 是 ASL 2.0 寫法, 等價於 legacy 的 (第 12 章); 寫進 物件的東西會被 Interpreter 印到除錯通道, 在 acpiexec 是直接印在畫面上,在 Linux 則要開 ACPI debug 才看得到。 用十進位字面值;AML 層面沒有進位制的概念, 所以 acpiexec 顯示結果時一律用十六進位——。

驗證

iasl 編譯

\(iasl project1.dsl Intel ACPI Component Architecture ASL+ Optimizing Compiler/Disassembler version 20260408 ... ASL Input: project1.dsl - 593 bytes 9 keywords 0 source lines AML Output: project1.aml - 115 bytes 3 opcodes 6 named objects Compilation successful. 0 Errors, 0 Warnings, 0 Remarks, ... \end{shell} 115 bytes 的 AML——之後上機排錯時,看到 dmesg 裡的表長度 \code{0x73}(=115),你就知道核心載到的是這一版。 \subsection{acpiexec 模擬執行} \begin{shell}[title={acpiexec 互動驗證(版本字串與位址依環境而異)}]\) acpiexec project1.aml ... ACPI: SSDT 0x7FB5D87069D0 000073 (v02 DEMO PROJ1 00000001 ...) Table [SSDT: PROJ1] (id 02) - 5 Objects with 1 Devices, 2 Methods ACPI: 2 ACPI AML tables successfully acquired and loaded

- namespace ACPI Namespace (from Namespace Root): 0 _SB_ Device 0x7fbf...1f0 000 1 DEV0 Device 0x7fbf...390 002 2 _HID String 0x7fbf...590 002 Len 08 "DEMO0001" 2 _UID Integer 0x7fbf...810 002 = 0000000000000000 2 _STA Method 0x7fbf...8f0 002 Args 0 Len 0003 2 TEST Method 0x7fbf...6c0 002 Args 0 Len 0019

- execute _SB.DEV0.TEST Evaluating _SB.DEV0.TEST ACPI Debug: "DEV0.TEST called" Evaluation of _SB.DEV0.TEST returned object, buffer length 18 [Integer] = 00000000000004D2

- execute _SB.DEV0._STA Evaluating _SB.DEV0._STA [Integer] = 000000000000000F

逐一核對: 證明 確實掛在 底下、四個子物件齊全; 先印出 Debug 字串,再回傳 ——1234 到手。順帶觀察兩件事: acpiexec 連同自己的迷你 DSDT 一共載了 2 張表; 而 輸出裡還有 、 這些 Interpreter 內建的預定義物件,Project 5 會再遇到它們。

initrd 載入 QEMU/真機

Module 10 教過核心的 ACPI table upgrade 機制:AML 要放進 cpio 封存檔內的固定路徑(), 再把封存檔接在 initrd 的最前面,開機時核心就會自動安裝 這張 SSDT。實際操作:

\(mkdir -p kernel/firmware/acpi\) cp project1.aml kernel/firmware/acpi/ \(find kernel | cpio -H newc --create > project1.img 1 block\) cat project1.img /boot/initrd.img-\((uname -r) > combined.img\) qemu-system-x86_64 -m 2G -nographic  -kernel /boot/vmlinuz-\((uname -r) \ -initrd combined.img \ -append "console=ttyS0" \end{shell} \begin{warnbox}[cpio 的路徑必須「相對」] 打包時必須站在 \code{kernel} 目錄的上一層執行 \code{find kernel}, 讓封存檔內的路徑恰好是 \code{kernel/firmware/acpi/project1.aml}。 若不小心打包成絕對路徑(\code{/home/you/kernel/...}), 核心找不到約定的路徑,會安靜地忽略你的表——沒有錯誤訊息, 這是本專案最常見的「無聲失敗」。 \end{warnbox} 開機後檢查三個觀察點: \begin{shell}[title={在 QEMU(或真機)內驗收}]\) sudo dmesg | grep -i "table upgrade" ACPI: Table Upgrade: install [SSDT-DEMO -PROJ1 ]

\(ls /sys/bus/acpi/devices/ | grep DEMO DEMO0001:00\) cat /sys/bus/acpi/devices/DEMO0001:00/path _SB_.DEV0 \(cat /sys/bus/acpi/devices/DEMO0001:00/status 15 \end{shell} \code{path} 顯示 \code{\_SB\_}——四字元補齊的內部名稱, 第 3 章的知識在 sysfs 裡直接可見;\code{status} 的 15 正是 \code{\_STA} 回傳的 \code{0x0F}。在真機上,同樣的 \code{project1.img} 可透過 GRUB 掛載(完整流程見第 30 章階段四); 建議先在 QEMU 練熟再碰實體機器。 \section{常見問題} \begin{warnbox}[學生最容易卡住的三個點] \begin{itemize} \item \textbf{\code{\_HID} 格式錯誤}:\code{DEMO001}(7 字元但 vendor 佔 4 字)或小寫字母都會被 iasl 拒絕,錯誤訊息是 \code{Invalid leading ASCII character} 或 \code{suffix must be 4 hex digits} 一類。 記住規則:3 字母+4 hex,或 4 字元+4 hex。 \item \textbf{acpiexec 路徑打錯}:\code{execute DEV0.TEST} 會回 \code{AE\_NOT\_FOUND}——互動模式下請用完整路徑 \code{\textbackslash\_SB.DEV0.TEST}。若用 \code{-b} 參數從 shell 一次性執行,記得整串用單引號包起來, 否則反斜線會先被 shell 吃掉。 \item \textbf{QEMU 裡看不到裝置}:先確認 dmesg 有沒有 \code{Table Upgrade} 那一行。沒有,就是 initrd 打包或 串接順序的問題(override 的 cpio 必須在最前面、 不可壓縮);有這行但 \code{/sys} 沒裝置, 再回頭檢查 \code{\_STA} 與 \code{Scope} 路徑。 \end{itemize} \end{warnbox} \section{加分挑戰} \begin{exbox}[延伸任務] \begin{enumerate} \item \textbf{第二個裝置}:在同一張 SSDT 加入 \code{DEV1}, \code{\_HID} 同為 \code{DEMO0001}、\code{\_UID} 設為 1。 載入後觀察 \code{/sys/bus/acpi/devices/} 如何用 \code{DEMO0001:00}、\code{DEMO0001:01} 區分兩顆同型裝置。 \item \textbf{帶參數的方法}:新增 \code{Method (ADDX, 2) \{ Return (Arg0 + Arg1) \}}, 在 acpiexec 用 \code{execute \textbackslash\_SB.DEV0.ADDX 0x15 0x2A} 驗證回傳 \code{0x3F}——這是第 12 章 \code{Arg} 物件的實戰。 \item \textbf{回傳複合物件}:把 \code{TEST} 改成回傳 \code{Package () \{ 1234, "hello", Buffer () \{1, 2\} \}}, 觀察 acpiexec 如何逐層列印。真實韌體的 \code{\_BST}、 \code{\_BIF} 回傳的正是這種 Package(第 30 章會解剖)。 \end{enumerate} \end{exbox} \chapter{Project 2:GPIO Controller} \label{ch:proj2} Project 1 的裝置是「空殼」——有身分,但不碰任何硬體。 本專案補上韌體工程師每天在做的那件事:透過暫存器控制硬體。 你將打造一顆虛擬的 GPIO 控制器,完整走一遍 Device \(\to\) OperationRegion \(\to\) Field \(\to\) Method 這條 ACPI 描述硬體的標準鏈路:宣告一塊 MMIO、把它切成 方向/輸出/輸入三個暫存器,再用位元運算寫出 「設方向、寫 pin、讀 pin」三個控制方法。 真實平台上,GPIO 是 ACPI 出鏡率最高的資源之一:電源開關、 LED、感測器中斷腳、觸控板的 reset 線,背後全是同一套操作。 把這條鏈路練熟,你就看得懂真實 DSDT 裡九成的暫存器操作程式碼。 \section{專案目標與驗收標準} \begin{goalbox} \textbf{專案目標} \begin{itemize} \item 用 \code{OperationRegion} 宣告一塊假想 MMIO (SystemMemory \code{0xFED40000}),並以 \code{Field} 切出 DIR/OUT/IN 三個 32 位元暫存器。 \item 寫出 \code{SETD}(設方向)、\code{SETP}(寫 pin)、 \code{GETP}(讀 pin)三個 Method, 以位元運算精準操作單一 pin 而不干擾其他 pin。 \item 在 \code{\_CRS} 中宣告 MMIO 資源,讓 OS 知道這塊位址被誰占用。 \item 理解 acpiexec 如何模擬記憶體區域,並利用它驗證讀寫邏輯。 \end{itemize} \textbf{驗收標準} \begin{itemize} \item \code{iasl} 編譯零錯誤、零警告、零 remark。 \item acpiexec 中依序執行 \code{SETP (5,1)}、\code{SETP (3,1)}、 \code{SETP (5,0)} 後,\code{GOUT} 的值依序為 \code{0x20}、\code{0x28}、\code{0x08}——證明單一 pin 的 set 與 clear 都沒有波及鄰兵。 \item 能口頭解釋:為什麼 \code{SETD}/\code{SETP} 必須 \code{Serialized},而 \code{GETP} 不必。 \end{itemize} \end{goalbox} \section{背景與設計} \subsection{暫存器地圖:先畫圖,再寫碼} 寫任何暫存器操作程式之前,先把 datasheet 的暫存器地圖抄下來—— 這是韌體工程師的肌肉記憶。我們的虛擬控制器管 32 支 pin, 三個 32 位元暫存器、每 pin 一個 bit: \begin{center} \small \begin{tabular}{cllL{5.6cm}} \toprule \rowcolor{acpiblue!10} \textbf{Offset} & \textbf{名稱} & \textbf{Field} & \textbf{意義} \\ \midrule 0x00 & DIR & \code{GDIR} & 方向:bit \(n\)=1 表示 pin \(n\) 為輸出 \\ 0x04 & OUT & \code{GOUT} & 輸出鎖存:輸出 pin 的驅動準位 \\ 0x08 & IN & \code{GPIN} & 輸入準位:讀取腳位目前狀態(唯讀) \\ \bottomrule \end{tabular} \end{center} 基底位址選在 \code{0xFED40000}——這是本教材的假想值。 呼叫鏈自上而下是這樣接起來的: \begin{vflow}[4.5mm] \fnode{OS/acpiexec 呼叫 SETP (5, 1)} \fnodel{第 12 章:Arg0/Arg1}{Method 內位元運算出 mask} \fnodel{第 10 章:Field 存取}{GOUT |= 0x20} \fnodel{第 9 章:Region 轉譯}{OperationRegion GREG} \fnode[flownodealt]{MMIO 0xFED40004 實體寫入} \end{vflow} \subsection{用到前面哪些章的知識} \begin{itemize} \item \textbf{第 6 章}:\code{<<}、\code{|}、\code{\&}、 \code{\textasciitilde} 位元運算子——本專案的靈魂。 \item \textbf{第 8 章}:\code{\_CRS} 與 \code{Memory32Fixed},向 OS 申報資源占用。 \item \textbf{第 9、10 章}(Module 5):\code{OperationRegion} 的 SystemMemory 空間與 \code{Field} 的切割規則。 \item \textbf{第 12、13 章}(Module 6):帶參數的 Method、 \code{Serialized} 對 read-modify-write 的保護。 \end{itemize} \begin{warnbox}[絕對不要把這張表載到真機] \code{0xFED40000} 在許多 Intel 平台上是 TPM 的暫存器空間; 對著別人的硬體亂寫 MMIO,輕則裝置異常、重則系統當機。 本專案的驗證\textbf{只在 acpiexec 模擬器}中進行—— 它根本不會碰真實記憶體,這正是我們敢放手實驗的原因。 想在真機玩 GPIO,請等第 30 章教你找出「自己機器上真正的」 GPIO 控制器之後。 \end{warnbox} \section{逐步實作} \begin{labbox}[Project 2 實作步驟] \begin{enumerate} \item 建立 \code{proj2/},新增 \code{project2.dsl}, 先只寫 Device 骨架+\code{\_CRS},編譯一次確認無誤。 \item 加入 \code{OperationRegion} 與 \code{Field},再編譯一次 ——增量開發,錯誤才好定位。 \item 依序實作 \code{SETD}、\code{SETP}、\code{GETP}, 每加一個方法就編譯一次。 \item 進 acpiexec,照驗證一節的劇本逐步操作暫存器。 \end{enumerate} \end{labbox} \begin{asl}[title={project2.dsl——完整可編譯}] DefinitionBlock ("project2.aml", "SSDT", 2, "DEMO", "PROJ2", 0x00000001) { Scope (\_SB) { Device (GPI0) { Name (_HID, "DEMO0002") Name (_UID, Zero) Method (_STA, 0, NotSerialized) { Return (0x0F) } Name (_CRS, ResourceTemplate () { Memory32Fixed (ReadWrite, 0xFED40000, 0x00001000) }) OperationRegion (GREG, SystemMemory, 0xFED40000, 0x0C) Field (GREG, DWordAcc, NoLock, Preserve) { GDIR, 32, // 0x00: direction, 1 = output GOUT, 32, // 0x04: output latch GPIN, 32, // 0x08: input level (RO on real HW) } // SETD: set direction. Arg0 = pin 0..31, Arg1 = dir Method (SETD, 2, Serialized) { Local0 = (One << Arg0) If (Arg1) { GDIR |= Local0 // set one bit } Else { GDIR &= ~Local0 // clear one bit } Return (GDIR) } // SETP: drive an output pin. Arg0 = pin, Arg1 = level Method (SETP, 2, Serialized) { Local0 = (One << Arg0) If (Arg1) { GOUT |= Local0 } Else { GOUT &= ~Local0 } Return (GOUT) } // GETP: read an input pin. Arg0 = pin Method (GETP, 1, NotSerialized) { Return ((GPIN >> Arg0) & One) } } } } \end{asl} \subsection{逐段拆解} \textbf{Region 與 Field 的對應}——\code{OperationRegion} 宣告 「從 \code{0xFED40000} 起、長 \code{0x0C} 的 SystemMemory」; \code{Field} 再依宣告順序把位元往上堆:\code{GDIR} 佔 bit 0--31 (offset 0x00)、\code{GOUT} 佔 bit 32--63(offset 0x04)、 \code{GPIN} 佔 bit 64--95(offset 0x08)——與暫存器地圖一一對應。 \code{DWordAcc} 要求 Interpreter 以 32 位元為單位存取, 符合多數 GPIO 控制器 datasheet 的規定;如果硬體只接受 32 位元對齊的存取卻用了 \code{ByteAcc},真機上就會讀寫出垃圾。 \textbf{mask 的產生}——\code{Local0 = (One << Arg0)} 把 1 左移到目標 pin 的位置。這一行是第 6 章位元運算的直接應用: pin 5 得到 \code{0x20}、pin 3 得到 \code{0x08}。 \textbf{read-modify-write 三部曲}——\code{GOUT |= Local0} 只點亮 目標 bit;\code{GOUT \&= \textasciitilde Local0} 只熄滅目標 bit。 兩者展開後都是「讀出 32 位元、改一個 bit、整包寫回」。 用 legacy 運算子寫,等價形式是: \begin{asl}[title={同一件事的 legacy 寫法(片段,僅供對照)}] // GOUT |= Local0 Or (GOUT, Local0, GOUT) // GOUT &= ~Local0 And (GOUT, Not (Local0), GOUT) \end{asl} 真實 DSDT 兩種寫法都常見(取決於 OEM 程式碼的年代), 必須都讀得懂。另外注意一個 64 位元陷阱:\code{Not (Local0)} 是對 64 位元整數取反(第 2 章的 ComplianceRevision 效應), pin 5 的反罩其實是 \code{0xFFFFFFFFFFFFFFDF}; 寫回 32 位元 Field 時高位被截掉,結果仍然正確—— 但如果有一天你把 Field 改成 64 位元寬,行為就完全不同了。 \textbf{為什麼 \code{Serialized}}——\code{SETD}/\code{SETP} 是 read-modify-write:兩條執行緒同時 \code{SETP (5,1)}、 \code{SETP (3,1)},若交錯執行,後寫的會蓋掉先寫的 bit(第 13 章 的競態問題)。宣告 \code{Serialized} 讓 Interpreter 對整個 Method 自動上鎖。\code{GETP} 只做單次讀取、沒有中間狀態, 維持 \code{NotSerialized} 即可。 \textbf{回傳暫存器現值}——\code{SETD}/\code{SETP} 最後 \code{Return (GDIR)}/\code{Return (GOUT)},純為驗證方便: acpiexec 每執行一次就直接把暫存器新值印給你看, 不用另外 \code{evaluate}。真實韌體裡這類方法通常不回傳值。 \section{驗證} \subsection{iasl 編譯與 acpiexec 劇本} 編譯無誤後,照下面的劇本操作。留意每一步的回傳值—— 那就是暫存器的即時內容: \begin{shell}[title={acpiexec 驗證劇本(輸出節錄)}]\) iasl project2.dsl ... Compilation successful. 0 Errors, 0 Warnings, 0 Remarks, ...

\(acpiexec project2.aml - execute \_SB.GPI0.SETD 0x5 0x1 [Integer] = 0000000000000020 ; DIR: pin5 -> output - execute \_SB.GPI0.SETP 0x5 0x1 [Integer] = 0000000000000020 ; OUT: 0010 0000 - execute \_SB.GPI0.SETP 0x3 0x1 [Integer] = 0000000000000028 ; OUT: 0010 1000 - execute \_SB.GPI0.SETP 0x5 0x0 [Integer] = 0000000000000008 ; OUT: 0000 1000 - execute \_SB.GPI0.GETP 0x5 [Integer] = 0000000000000000 ; IN reads as 0 (simulated) - evaluate \_SB.GPI0.GOUT [Integer] = 0000000000000008 ; direct field read-back \end{shell} 三個關鍵觀察: \begin{itemize} \item \code{0x20} \(\to\) \code{0x28} \(\to\) \code{0x08}: set pin 3 沒動到 pin 5,clear pin 5 沒動到 pin 3 ——位元運算的隔離性驗證完成,正是驗收標準。 \item \code{evaluate} 可以直接讀一個 Field 單元: Interpreter 會真的執行一次 region 讀取。 這是檢查暫存器狀態最快的手段。 \item \code{GETP} 回 0 不是 bug——見下面的補充說明。 \end{itemize} \begin{notebox}[acpiexec 如何模擬記憶體] acpiexec 不會存取真實實體記憶體:它替每塊 OperationRegion 維護一份私有緩衝區。\textbf{寫入}會記進緩衝區,之後讀同一位址 就回讀寫過的值(所以 \code{GOUT} 讀得回 \code{0x08}); \textbf{從未寫過}的位址則回傳預設填充值 0(所以模擬輸入的 \code{GPIN} 永遠是 0)。想改變填充值,可用 \code{acpiexec -f 0xFF project2.aml} 之類的參數, 讓「未初始化讀取」在測試中無所遁形。 換句話說:模擬器驗證的是你的\textbf{邏輯}, 不是硬體的\textbf{電氣行為}——後者只能上真機。 \end{notebox} \section{常見問題} \begin{warnbox}[學生最容易卡住的三個點] \begin{itemize} \item \textbf{\code{GETP} 永遠回 0 就以為程式錯了}: 模擬的 \code{GPIN} 沒有真實訊號源,0 是正常的。 想驗證 \code{GETP} 的位移邏輯,先用 \code{evaluate} 直接對 \code{GPIN} 寫值再讀(或暫時把 \code{GETP} 改讀 \code{GOUT}),確認 shift-and-mask 正確。 \item \textbf{忘記 \code{Serialized}}:單人測試看不出差別, 上機後偶發性 pin 狀態錯亂,極難重現。凡是 read-modify-write 的方法,一律 \code{Serialized}。 \item \textbf{Field 順序與 offset 對不上}:\code{Field} 是「依序堆疊」,在 \code{GDIR} 前多插一個 8 位元欄位, 後面所有暫存器全部位移。改地圖時請用 \code{Offset (0x04)} 明確錨定,不要依賴默數位元。 \end{itemize} \end{warnbox} \section{加分挑戰} \begin{exbox}[延伸任務] \begin{enumerate} \item \textbf{GPIO consumer 裝置}:新增裝置 \code{LED0}, 在其 \code{\_CRS} 用 \code{GpioIo} 描述子引用 \code{\textbackslash\_SB.GPI0} 的 pin 5(第 8 章資源 描述子的進階用法),體會「控制器提供資源、 裝置消費資源」的分工。 \item \textbf{GPIO 事件}:替 \code{GPI0} 加上 \code{\_AEI} (ACPI Event Interrupt)宣告 pin 7 為事件腳, 並實作對應的事件方法 \code{\_E07},在其中 \code{Notify} 一個裝置——這是筆電「上蓋開關接 GPIO」 的標準模式。 \item \textbf{一行翻轉}:實作 \code{TGLP}(toggle pin), 用 XOR 一行完成:\code{GOUT \textasciicircum= (One << Arg0)},並用 acpiexec 連按兩次驗證值恢復原狀。 \end{enumerate} \end{exbox} \chapter{Project 3:I2C Sensor} \label{ch:proj3} MMIO 之後,第二種你一定會遇到的硬體介面是序列匯流排。 筆電裡的觸控板、溫度感測器、電量計,多半掛在 I2C 上—— 它們不像 MMIO 那樣「位址一寫就到」,每次存取都是一筆 要透過控制器代發的\textbf{交易}(transaction)。 ACPI 用 GenericSerialBus 這種特殊的 OperationRegion 把交易包裝成 Field 存取,讓 ASL 程式碼幾乎感覺不到差異。 本專案模擬一顆經典的 I2C 溫度感測器(以 TI TMP102 為藍本, 位址 \code{0x48}):宣告 I2C 控制器與感測器兩個裝置、 用 \code{\_CRS} 綁定匯流排位址、透過 GenericSerialBus 讀出 兩個 raw byte,再用定點數技巧換算成攝氏溫度回傳—— 完整走一遍「ACPI \(\to\) I2C 裝置 \(\to\) 暫存器 \(\to\) 讀值 \(\to\) 回傳溫度」的資料流。 \section{專案目標與驗收標準} \begin{goalbox} \textbf{專案目標} \begin{itemize} \item 建立 I2C 控制器 \code{I2C0} 與感測器 \code{SEN0} 兩個裝置,用 \code{I2cSerialBusV2} 把兩者接起來。 \item 宣告 GenericSerialBus OperationRegion, 以 \code{AttribByte} 存取感測器的兩個溫度暫存器。 \item 實作 \code{TEMP} 方法:讀兩個 raw byte、位元組合成 12 位元讀值、以整數運算換算成攝氏溫度後 \code{Return}。 \item 掌握 GenericSerialBus 交易緩衝區的固定格式 (狀態+長度+資料)。 \end{itemize} \textbf{驗收標準} \begin{itemize} \item \code{iasl} 編譯零錯誤、零警告 (允許一個 Remark 2173,拆解中會說明它的意義)。 \item acpiexec 中 \code{CONV (0x190)} 回傳 \code{0x9C4} (=2500,即攝氏 25.00 度)——換算邏輯正確。 \item acpiexec 中 \code{TEMP} 回傳 \code{0x3EBE}, 且你能\textbf{解釋這個數字是怎麼來的} (提示:模擬器的填充值)。 \end{itemize} \end{goalbox} \section{背景與設計} \subsection{兩個裝置、一條匯流排} I2C 的世界有兩種角色:控制器(controller)發起交易, 週邊(target)回應交易。ACPI 如實描述這個拓撲—— \code{I2C0} 是控制器裝置,真實平台上它會有自己的 MMIO 與中斷 (我們也照樣宣告,讓範例更貼近實物);\code{SEN0} 是感測器, 它的 \code{\_CRS} 用 \code{I2cSerialBusV2} 描述子說明: 「我掛在 \nspath{\_SB.I2C0} 上、位址 \code{0x48}、 速率 400 kHz」。OS 的 I2C 子系統就靠這行把感測器 掛到正確的匯流排上(第 8 章資源描述子的序列匯流排版本)。 模擬對象 TMP102 的暫存器地圖(節錄): \begin{center} \small \begin{tabular}{clL{6.6cm}} \toprule \rowcolor{acpiblue!10} \textbf{Command} & \textbf{Field} & \textbf{意義} \\ \midrule 0x00 & \code{TMPH} & 溫度高位元組(12 位元讀值的 bit 11--4) \\ 0x01 & \code{TMPL} & 溫度低位元組(bit 3--0 靠左對齊於高 nibble) \\ \bottomrule \end{tabular} \end{center} 溫度解析度是每 LSB \(0.0625\,^{\circ}\mathrm{C}\): raw 讀值 400(\code{0x190})代表 \(400 \times 0.0625 = 25\,^{\circ}\mathrm{C}\)。 \subsection{用到前面哪些章的知識} \begin{itemize} \item \textbf{第 6 章}:位移與遮罩組合位元組、 整數乘除實作定點數換算。 \item \textbf{第 8 章}:\code{\_CRS} 描述子——這次是 \code{I2cSerialBusV2} 而不是 \code{Memory32Fixed}。 \item \textbf{Module 5(第 9--11 章)}:OperationRegion 的 位址空間分類。GenericSerialBus 是「需要 Connection」 的特殊空間:Field 的 offset 不再是位元組位址, 而是傳給裝置的 command byte。 \item \textbf{第 12、13 章}:方法回傳值、\code{Serialized} 與方法內建立具名物件的互動。 \end{itemize} \subsection{一筆溫度讀取的完整資料流} \begin{vflow}[4.2mm] \fnode{OS/acpiexec 呼叫 TEMP} \fnodel{BUFF = TMPH}{GenericSerialBus 交易} \fnodel{Connection 指路}{I2C 控制器驅動發出讀取} \fnode{感測器 0x48 回覆資料位元組} \fnodel{STAT/DATA 拆包}{位元組合出 12 位元 raw} \fnodel{raw * 625 / 100}{定點數換算} \fnode[flownodealt]{Return 溫度(0.01 度為單位)} \end{vflow} 注意中段:一次 \code{BUFF = TMPH} 的 Field 讀取, 在真機上會展開成一整筆 I2C 交易(start、位址、command、 restart、讀資料、stop),由 OS 的控制器驅動代勞—— 這就是 GenericSerialBus 與 SystemMemory 最大的差異: \textbf{Field 存取不再是同步的記憶體讀寫,而是一筆可能失敗的 匯流排交易},所以協定裡才需要狀態位元組。 \section{逐步實作} \begin{labbox}[Project 3 實作步驟] \begin{enumerate} \item 建立 \code{proj3/project3.dsl},先寫 \code{I2C0} 與 \code{SEN0} 兩個裝置骨架(含 \code{\_CRS}),編譯確認。 \item 加入 GenericSerialBus OperationRegion 與 Field (\code{Connection}+\code{AccessAs} 兩行是關鍵),編譯。 \item 先寫純數學的 \code{CONV},這是可以獨立單元測試的部分。 \item 最後寫 \code{TEMP},把交易、錯誤處理、位元組合、 換算串起來。 \end{enumerate} \end{labbox} \begin{asl}[title={project3.dsl——完整可編譯}] DefinitionBlock ("project3.aml", "SSDT", 2, "DEMO", "PROJ3", 0x00000001) { Scope (\_SB) { Device (I2C0) { Name (_HID, "DEMO0003") Name (_UID, Zero) Method (_STA, 0, NotSerialized) { Return (0x0F) } Name (_CRS, ResourceTemplate () { Memory32Fixed (ReadWrite, 0xFED50000, 0x00001000) Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive) { 20 } }) } Device (SEN0) { Name (_HID, "DEMO0004") Name (_UID, Zero) Method (_STA, 0, NotSerialized) { Return (0x0F) } Name (_CRS, ResourceTemplate () { I2cSerialBusV2 (0x0048, ControllerInitiated, 400000, AddressingMode7Bit, "\\_SB.I2C0", 0x00, ResourceConsumer, , Exclusive, ) }) OperationRegion (SREG, GenericSerialBus, Zero, 0x0100) Field (SREG, BufferAcc, NoLock, Preserve) { Connection (I2cSerialBusV2 (0x0048, ControllerInitiated, 400000, AddressingMode7Bit, "\\_SB.I2C0", 0x00, ResourceConsumer, , Exclusive, )), AccessAs (BufferAcc, AttribByte), Offset (0x00), TMPH, 8, // command 0x00: temperature MSB TMPL, 8, // command 0x01: temperature LSB } // CONV: 12-bit raw value -> temperature in 0.01 degC Method (CONV, 1, NotSerialized) { // 1 LSB = 0.0625 degC; *625/100 -> centi-deg Return ((Arg0 * 625) / 100) } // TEMP: read both raw bytes, return 0.01 degC units Method (TEMP, 0, Serialized) { Name (BUFF, Buffer (0x10) {}) CreateByteField (BUFF, 0x00, STAT) CreateByteField (BUFF, 0x02, DATA) BUFF = TMPH // I2C read, command 0x00 If (STAT != Zero) { Return (Ones) // bus error } Local0 = DATA // raw MSB BUFF = TMPL // I2C read, command 0x01 If (STAT != Zero) { Return (Ones) } Local1 = DATA // raw LSB // 12-bit raw = MSB<<4 | LSB>>4 (TMP102 layout) Local2 = ((Local0 << 4) | (Local1 >> 4)) Local0 = CONV (Local2) Debug = Local0 Return (Local0) } } } } \end{asl} \subsection{逐段拆解} \textbf{Connection 與 AccessAs}——GenericSerialBus 的 Field 宣告裡有兩行別的空間看不到的東西。\code{Connection (...)} 指定「接下來的欄位透過哪條連線存取」,內容就是一份 I2C 描述子(與 \code{\_CRS} 中那份相同;一個 Field 裡可以有 多個 Connection 段服務多顆裝置)。\code{AccessAs (BufferAcc, AttribByte)} 指定交易協定——\code{AttribByte} 對應 SMBus 的 read/write byte:一個 command、一個資料位元組。 之後的 \code{TMPH, 8} 就代表「command \code{0x00} 那個暫存器」, \textbf{Field offset 在這裡的語義是 command byte,不是記憶體位址}。 \textbf{交易緩衝區的固定格式}——BufferAcc 的 Field 讀寫的 不是整數而是一包緩衝區,格式由 ACPI 規格固定: \begin{center} \small \begin{tabular}{clL{6.8cm}} \toprule \rowcolor{acpiblue!10} \textbf{Byte} & \textbf{欄位} & \textbf{說明} \\ \midrule 0 & Status & 0=成功;非零=匯流排錯誤(由控制器驅動填入) \\ 1 & Length & 實際傳輸的資料長度 \\ 2\ldots & Data & 資料本體(\code{AttribByte} 時只有 1 byte) \\ \bottomrule \end{tabular} \end{center} 所以程式碼用 \code{CreateByteField} 在 \code{BUFF} 上開兩扇窗: \code{STAT} 對準 byte 0、\code{DATA} 對準 byte 2。 初學者十個有九個把資料對到 byte 1——那是長度欄位, 讀出來的「溫度」會永遠是 1。 \textbf{錯誤處理}——每次交易後先驗 \code{STAT},失敗就回傳 \code{Ones}(64 位元全 1,慣用的錯誤哨兵值)。真實筆電上 I2C 交易可能因裝置忙碌、線路 NAK 而失敗, 不檢查狀態的 ASL 會把垃圾當溫度回報給 OS。 \textbf{位元組合}——TMP102 把 12 位元讀值靠左放進兩個 byte: 高 8 位在 \code{TMPH},低 4 位在 \code{TMPL} 的高 nibble。 \code{(Local0 << 4) | (Local1 >> 4)} 把兩段拼回 12 位元整數, 純粹是第 6 章位移與 OR 的應用。 \textbf{定點數換算}——ASL 只有整數,沒有浮點數。要乘上 \(0.0625\),訣竅是改成「乘 625、除 100」,讓結果以 0.01 度為單位:\(400 \times 625 / 100 = 2500\),即 25.00 度。 \textbf{先乘後除}是鐵則——若寫成 \code{Arg0 / 16} (\(0.0625 = 1/16\))雖然數學等價,但整數除法會直接把 小數截掉,解析度從 0.0625 度掉到 1 度; 先乘 625 把精度墊高再除,才留得住小數兩位。 \textbf{Remark 2173}——iasl 會對 \code{Name (BUFF, ...)} 給一個 remark:「在方法內建立具名物件效率不佳」。這不是錯誤: ACPI 規格書的 GenericSerialBus 範例正是這樣寫的,具名物件 會在方法結束時銷毀、下次呼叫重建,效能損失在這個場景可忽略。 但方法必須宣告 \code{Serialized}——若 \code{NotSerialized} 且兩條路徑同時進入,第二次的 \code{Name} 會撞上第一次尚未 銷毀的 \code{BUFF},拋出 \code{AE\_ALREADY\_EXISTS} (第 13 章的教訓,iasl 也會直接警告你)。 \section{驗證} \subsection{先驗數學,再驗交易} \code{CONV} 是純函式,最適合先做單元測試: \begin{shell}[title={acpiexec:用已知向量驗證換算}]\) iasl project3.dsl ... Compilation successful. 0 Errors, 0 Warnings, 1 Remarks, ...

\(acpiexec project3.aml - execute \_SB.SEN0.CONV 0x190 [Integer] = 00000000000009C4 ; 400 raw -> 25.00 degC - execute \_SB.SEN0.CONV 0x168 [Integer] = 00000000000008CA ; 360 raw -> 22.50 degC - execute \_SB.SEN0.CONV 0x001 [Integer] = 0000000000000006 ; 1 raw -> 0.06 degC (truncated) \end{shell} 對照手算:\code{0x9C4}=2500、\code{0x8CA}=2250, 換算正確。最後一筆展示整數除法的截斷——真實值 0.0625 度 被截成 0.06 度,這是定點數設計時就該心裡有數的誤差。 \subsection{執行 TEMP:看懂模擬值} \begin{shell}[title={acpiexec:完整交易路徑}] - execute \_SB.SEN0.TEMP Evaluating \_SB.SEN0.TEMP ACPI Debug: 0x0000000000003EBE Evaluation of \_SB.SEN0.TEMP returned object, buffer length 18 [Integer] = 0000000000003EBE - evaluate \_SB.SEN0.TMPH Evaluation of \_SB.SEN0.TMPH returned object, buffer length 20 [Buffer] Length 03 = 0000: 00 01 A0 \end{shell} \code{0x3EBE}=16062,也就是攝氏 160.62 度——顯然不是真溫度。 用 \code{evaluate} 直接讀 \code{TMPH} 就真相大白: acpiexec 對 GenericSerialBus 交易回傳的模擬緩衝區是 \code{00 01 A0}——狀態 0(成功)、長度 1、資料 \code{0xA0}。 兩個暫存器都讀到 \code{0xA0},於是 raw \(= (\code{0xA0} \ll 4)\,|\,(\code{0xA0} \gg 4) = \code{0xA0A} = 2570\), \(2570 \times 625 / 100 = 16062 = \code{0x3EBE}\)。 數字全部對上——這正是驗收標準要你「解釋這個數字」的原因: \textbf{模擬器驗證的是交易流程、錯誤處理與換算邏輯; 資料本身是假的}。真實溫度要等這張表接上真硬體才會出現。 \begin{notebox}[規格出處與真機行為] GenericSerialBus 的交易緩衝區格式定義在 ACPI 規格 「GenericSerialBus 位址空間」一節(ACPI 6.5 的 5.5.2.4.5)。 真機上,Linux 由 \code{i2c-core-acpi} 註冊這個空間的 handler:AML 發起交易時,核心會找到 \code{Connection} 指到的控制器、組出真正的 I2C 訊息送上線,再把結果與狀態 填回緩衝區。也就是說,同一份 ASL 在模擬器與真機上 走的是同一條程式路徑,只是最底層的 handler 不同—— 這就是先在 acpiexec 開發、後上真機的理論保證。 \end{notebox} \section{常見問題} \begin{warnbox}[學生最容易卡住的三個點] \begin{itemize} \item \textbf{資料對錯位元組}:\code{CreateByteField (BUFF, 0x01, DATA)} 對到的是 Length 欄位,讀回來永遠是 1。 資料從 byte 2 開始——狀態、長度、資料,背起來。 \item \textbf{ResourceSource 字串的反斜線}:字串常值裡的 \code{\textbackslash} 必須寫成 \code{"\textbackslash\textbackslash\_SB.I2C0"}(兩根), 否則 iasl 報 escape 錯誤或路徑解析失敗。 這與 \code{Scope (\textbackslash\_SB)} 不同—— 後者不在字串裡,一根就好。 \item \textbf{把 Remark 當錯誤、或反過來忽略警告}: Remark 2173 可以安心留著;但若你忘了 \code{Serialized}, iasl 給的是\textbf{警告}(method creates named object, should be serialized)——那個必須修。 分清楚 Error/Warning/Remark 三個層級的嚴重度。 \end{itemize} \end{warnbox} \section{加分挑戰} \begin{exbox}[延伸任務] \begin{enumerate} \item \textbf{加上 \code{\_DSD} 屬性}:用 Device Properties UUID 替 \code{SEN0} 附上 \code{"compatible" = "demo,tmp102"} 與取樣率屬性, 讓 Linux driver 能用 \code{device\_property\_read\_*} 讀到(第 25 章的橋樑)。 \item \textbf{溫度告警}:新增門檻物件 \code{Name (THRS, 2800)} 與檢查方法 \code{CHKT}:呼叫 \code{TEMP},超過門檻就 \code{Notify (SEN0, 0x80)}。在 acpiexec 觀察 Notify handler 印出的訊息(下一章你會大量使用它)。 \item \textbf{負溫度}:TMP102 的 raw 是 12 位元二補數。 擴充 \code{CONV}:bit 11 為 1 時先做符號延伸再換算, 用 \code{CONV (0xE70)} 應得攝氏 \(-25\) 度的表示。 想想看:ASL 整數無號,你要怎麼表達負值? (提示:參考 \code{\_BST} 用大整數表放電率的慣例, 或回傳帶符號旗標的 Package。) \end{enumerate} \end{exbox} \chapter{Project 4:Power Management} \label{ch:proj4} 前三個專案的裝置都「永遠有電」。真實世界不是這樣: 筆電闔上蓋子,觸控板該斷電;喚醒時,電源軌(power rail) 要先穩、裝置才能初始化。Module 7 講過這套 D-state 理論, 本專案把它做出來:一顆 \code{PowerResource} 控制假想的電源軌、 一個裝置掛上 \code{\_PR0}/\code{\_PR3} 與 \code{\_PS0}/\code{\_PS3},再用一個 \code{PWTG}(power toggle) 方法模擬 OSPM 的完整開關時序,最後以 \code{Notify} 通知狀態改變。 這個專案最重要的產出不是程式碼,而是時序感: \textbf{上電時誰先誰後、斷電時誰先誰後}——順序寫反的韌體, 輕則裝置喚不醒,重則對沒電的裝置下指令造成匯流排 hang。 \section{專案目標與驗收標準} \begin{goalbox} \textbf{專案目標} \begin{itemize} \item 實作 \code{PowerResource (PWR0)}:\code{\_ON}/ \code{\_OFF} 操作 GPIO 式的 rail 控制位元, \code{\_STA} 回報 rail 狀態。 \item 裝置 \code{DEV0} 以 \code{\_PR0}/\code{\_PR3} 宣告 電源依賴、以 \code{\_PS0}/\code{\_PS3} 執行狀態切換, D-state 記錄在 \code{Name (PSTA, ...)}。 \item 實作 \code{PWTG}:按 OSPM 的正確順序切換電源, 並 \code{Notify (DEV0, 0x80)}。 \item 畫出(並背下)D3 \(\to\) D0 與 D0 \(\to\) D3 兩個方向的呼叫時序。 \end{itemize} \textbf{驗收標準} \begin{itemize} \item \code{iasl} 編譯零錯誤、零警告、零 remark。 \item acpiexec 初始狀態:\code{PWR0.\_STA} 回 0、 \code{DEV0.\_PSC} 回 3(開機在 D3)。 \item 執行一次 \code{PWTG}:Debug 依序印出 \code{\_ON} 在前、\code{\_PS0} 在後,並看到 Notify 0x80 的訊息;再執行一次,順序變成 \code{\_PS3} 在前、\code{\_OFF} 在後。 \item 能解釋:為什麼兩個方向的順序恰好相反。 \end{itemize} \end{goalbox} \section{背景與設計} \subsection{宣告式與程序式:\code{\_PRx} 與 \code{\_PSx} 的分工} 第 16 章介紹過裝置電源的兩套機制,本專案讓它們同台演出: \begin{itemize} \item \code{\_PR0}/\code{\_PR3} 是\textbf{宣告式}的: 「我要進 D0,需要 Package 裡這些 PowerResource 是開的」。 OSPM 據此做引用計數——多個裝置共享同一條 rail 時, 最後一個離開的人才關燈。 \item \code{\_PS0}/\code{\_PS3} 是\textbf{程序式}的: rail 已就緒後,裝置本身要做的初始化/收尾動作。 \end{itemize} 物件之間的關係如下: \begin{asciitree}[title={Project 4 的物件關係}] \_SB ├── PMIC (OperationRegion) │ └── RAIL, 1 ← 假想 PMIC 的 rail 控制位元 ├── PWR0 (PowerResource) │ ├── _STA ← 回報 0/1(rail 是否供電) │ ├── _ON ← RAIL = 1(實際動硬體) │ └── _OFF ← RAIL = 0 └── DEV0 (Device) ├── _PR0 = { PWR0 } ← 宣告:D0 依賴 PWR0 ├── _PR3 = { PWR0 } ← 宣告:D3hot 仍依賴 PWR0 ├── _PS0 / _PS3 ← 程序:切換時的裝置動作 ├── PSTA ← 目前 D-state 的記錄 └── PWTG ← 實驗用:模擬 OSPM 完整時序 \end{asciitree} \subsection{OS 觀點的完整時序} 這張圖是本章的核心,OSPM 切換裝置電源時的呼叫順序: \begin{vflow}[4.2mm] \fnode[flownodealt]{D0:運作中} \fnodel{OSPM 決定休眠}{DEV0.\_PS3——裝置先收尾} \fnodel{引用計數歸零}{PWR0.\_OFF——rail 才斷電} \fnode[flownodealt]{D3:斷電} \fnodel{OSPM 決定喚醒}{PWR0.\_ON——rail 先上電} \fnodel{電源穩定}{DEV0.\_PS0——裝置再初始化} \fnode[flownodealt]{D0:運作中} \end{vflow} 記憶口訣:\textbf{下行先裝置後電源,上行先電源後裝置}。 理由很物理:收尾動作(存暫存器、通知韌體)需要裝置還有電; 初始化動作同樣需要電源已經穩定。兩個方向都是 「有電的那一端先動作」。 \subsection{用到前面哪些章的知識} \begin{itemize} \item \textbf{第 16 章}:D-state、\code{\_PSx}/\code{\_PRx}/ \code{PowerResource} 的語義(本專案的理論來源)。 \item \textbf{第 14 章}:\code{Notify} 事件通知——0x80 是 「狀態改變,請 OS 重新查詢」的慣用值。 \item \textbf{第 9、10 章}:rail 控制位元用 OperationRegion+Field 實作,手法同 Project 2。 \item \textbf{第 12 章}:跨 Scope 的方法呼叫 (\code{DEV0} 的方法呼叫 \nspath{\_SB.PWR0.\_ON})。 \end{itemize} \section{逐步實作} \begin{labbox}[Project 4 實作步驟] \begin{enumerate} \item 建立 \code{proj4/project4.dsl}:先宣告 PMIC OperationRegion 與 \code{RAIL} 位元(1 位元就夠)。 \item 實作 \code{PowerResource (PWR0)} 三件套: \code{\_STA}、\code{\_ON}、\code{\_OFF},每個都加 Debug 輸出——時序驗證全靠這些訊息。 \item 實作 \code{DEV0}:身分三件套+\code{PSTA}+ \code{\_PR0}/\code{\_PR3}+\code{\_PS0}/\code{\_PS3} +\code{\_PSC}。 \item 最後實作 \code{PWTG},嚴格按照時序圖排列呼叫順序。 \end{enumerate} \end{labbox} \begin{asl}[title={project4.dsl——完整可編譯}] DefinitionBlock ("project4.aml", "SSDT", 2, "DEMO", "PROJ4", 0x00000001) { Scope (\_SB) { // Virtual PMIC: one register, bit 0 = rail enable OperationRegion (PMIC, SystemMemory, 0xFED41000, 0x04) Field (PMIC, DWordAcc, NoLock, Preserve) { RAIL, 1, // bit 0: 1 = rail powered } PowerResource (PWR0, 0x00, 0x0000) { Name (PWRS, Zero) // cached rail state Method (_STA, 0, NotSerialized) { Return (PWRS) // 0 = off, 1 = on } Method (_ON, 0, Serialized) { RAIL = One // touch the hardware bit PWRS = One // then update the cache Debug = "PWR0._ON : rail powered" } Method (_OFF, 0, Serialized) { RAIL = Zero PWRS = Zero Debug = "PWR0._OFF: rail off" } } Device (DEV0) { Name (_HID, "DEMO0005") Name (_UID, Zero) Method (_STA, 0, NotSerialized) { Return (0x0F) } Name (PSTA, 0x03) // D-state bookkeeping: D3 Name (_PR0, Package () { PWR0 }) Name (_PR3, Package () { PWR0 }) Method (_PS0, 0, Serialized) { PSTA = Zero Debug = "DEV0._PS0: enter D0" } Method (_PS3, 0, Serialized) { PSTA = 0x03 Debug = "DEV0._PS3: enter D3" } Method (_PSC, 0, NotSerialized) { Return (PSTA) // current D-state } // PWTG: lab-only toggle, emulates OSPM ordering Method (PWTG, 0, Serialized) { If (\_SB.PWR0._STA () == One) { // D0 -> D3: _PS3 first, then cut the rail _PS3 () \_SB.PWR0._OFF () } Else { // D3 -> D0: rail up first, then _PS0 \_SB.PWR0._ON () _PS0 () } Notify (\_SB.DEV0, 0x80) Return (PSTA) } } } } \end{asl} \subsection{逐段拆解} \textbf{PowerResource 的參數}——\code{PowerResource (PWR0, 0x00, 0x0000)} 的第二個參數是 SystemLevel(此資源在哪個睡眠深度以上 仍需維持,0 表示 S0)、第三個是 ResourceOrder(多資源時的 開關排序權重)。實驗用途填 0 即可,真機上 OEM 用它們編排 多條 rail 的上電順序。 \textbf{\code{PWRS} 快取}——\code{\_ON}/\code{\_OFF} 寫完硬體位元 後,順手把狀態記在 \code{PWRS},\code{\_STA} 只讀快取。 這是真實韌體的常見模式:有些 rail 控制暫存器是唯寫的 (讀不回狀態),或讀取代價高(要走 I2C 問 PMIC), 快取一份是最務實的解法。代價是快取與現實可能脫鉤—— 所以更新順序必須是「先動硬體、再改快取」, 萬一寫硬體失敗,快取才不會謊報。 \textbf{\code{\_PR0} 與 \code{\_PR3} 都掛 PWR0}——D0 需要供電 理所當然;D3 也宣告依賴,代表這裡的 D3 是 \keyterm{D3hot}(主功能斷了、但輔助電源還在,裝置仍可被列舉)。 若 OSPM 決定進一步斷光(\keyterm{D3cold}),才會把 \code{\_PR3} 的資源也放掉。一個 Package 的差異, 就把第 16 章 D3hot/D3cold 的抽象定義變成看得見的程式碼。 \textbf{\code{PWTG} 只是代打}——真實系統中,走這套時序的是 OSPM(Linux 的 ACPI device PM 程式碼),韌體只提供 \code{\_ON}/\code{\_OFF}/\code{\_PSx} 這些「零件」。 acpiexec 裡沒有 OSPM 幫我們跑流程,所以用 \code{PWTG} 把時序自己排一遍——它存在的目的就是讓你把順序寫對一次、 印出來看一次。最後的 \code{Notify (\textbackslash\_SB.DEV0, 0x80)} 通知 OS「狀態變了,重新查詢我」,真機上會觸發 driver 的 notify handler(第 14 章)。 \begin{notebox}[兩種 \code{\_STA},兩種語義] 裝置的 \code{\_STA} 回傳位元遮罩(\code{0x0F}:存在、啟用、 顯示、正常);PowerResource 的 \code{\_STA} 只回傳 0 或 1 (off/on)。名字相同、語義不同,混用是新手常見錯誤: 如果 \code{PWR0.\_STA} 回傳 \code{0x0F},OSPM 會把它當 「on」處理沒錯,但你的程式若拿它跟 \code{One} 比較就會出錯 ——本專案的 \code{PWTG} 恰好就這樣寫,故意讓你踩不得。 \end{notebox} \section{驗證} 按劇本執行,把每一行輸出跟時序圖對照: \begin{shell}[title={acpiexec 完整驗證(輸出節錄)}]\) iasl project4.dsl ... Compilation successful. 0 Errors, 0 Warnings, 0 Remarks, ...

\(acpiexec project4.aml - execute \_SB.PWR0._STA [Integer] = 0000000000000000 ; rail off - execute \_SB.PWR0._ON ACPI Debug: "PWR0._ON : rail powered" - execute \_SB.PWR0._STA [Integer] = 0000000000000001 ; rail is now on - execute \_SB.PWR0._OFF ACPI Debug: "PWR0._OFF: rail off" - execute \_SB.DEV0._PSC [Integer] = 0000000000000003 ; device still in D3 - execute \_SB.DEV0.PWTG ACPI Debug: "PWR0._ON : rail powered" ACPI Debug: "DEV0._PS0: enter D0" ACPI Exec: Global: Received a Device Notify on [DEV0] Value 0x80 (Status Change) [Integer] = 0000000000000000 ; now in D0 - evaluate \_SB.RAIL [Integer] = 0000000000000001 ; hardware bit really set - execute \_SB.DEV0.PWTG ACPI Debug: "DEV0._PS3: enter D3" ACPI Debug: "PWR0._OFF: rail off" ACPI Exec: Global: Received a Device Notify on [DEV0] Value 0x80 (Status Change) [Integer] = 0000000000000003 ; back to D3 \end{shell} 劇本分兩段。前半把 \code{\_ON}/\code{\_STA}/\code{\_OFF} 當獨立零件逐一驗證——它們本來就是設計給 OSPM 單獨呼叫的; 注意此時 \code{\_PSC} 仍回 3:光開關 rail 不會改變 D-state 的記錄,兩套狀態是分開的。後半的 \code{PWTG} 才是整套時序: 驗收重點全在 Debug 行的\textbf{順序}——上行 \code{\_ON} 先於 \code{\_PS0};下行 \code{\_PS3} 先於 \code{\_OFF},與時序圖一致。 acpiexec 內建的 Notify handler 如實回報了 0x80; \code{evaluate \textbackslash\_SB.RAIL} 直接讀硬體位元, 證明 \code{\_ON} 不是只改了快取。 \section{常見問題} \begin{warnbox}[學生最容易卡住的三個點] \begin{itemize} \item \textbf{時序寫反}:先 \code{\_OFF} 再 \code{\_PS3}, acpiexec 照樣跑完不報錯——模擬器裡沒有會壞掉的硬體。 但上真機就是對斷電裝置下指令。驗收時請盯著 Debug 順序核對,不要只看回傳值。 \item \textbf{以為 OSPM 會呼叫 \code{PWTG}}:不會。 OSPM 只認識 \code{\_PSx}/\code{\_PRx}/ \code{\_ON}/\code{\_OFF} 這些規格定義的名字; \code{PWTG} 是我們的實驗治具。真機上驅動 D-state 轉換的是 OS 的 runtime PM,不是你手動執行方法。 \item \textbf{開機初值不一致}:\code{PSTA} 初值 3、 \code{PWRS} 初值 0,兩者必須描述同一個事實 (D3=rail off)。如果 \code{PSTA} 寫 0 但 rail 是關的, OSPM 會以為裝置在 D0,直接跳過上電流程—— 這類「紙面狀態與硬體脫鉤」是電源韌體最典型的 bug。 \end{itemize} \end{warnbox} \section{加分挑戰} \begin{exbox}[延伸任務] \begin{enumerate} \item \textbf{共享 rail 與引用計數}:加入 \code{DEV1}, \code{\_PR0} 同樣掛 \code{PWR0},並在 \code{PWR0} 內 維護 \code{Name (REFC, 0)}:提供 \code{PON}/\code{POFF} 方法做計數(進來加一、離開減一,歸零才真正 \code{\_OFF})。模擬「兩個裝置都睡了,rail 才能關」 的 OSPM 行為。 \item \textbf{喚醒能力 \code{\_PRW}}:替 \code{DEV0} 加上 \code{Name (\_PRW, Package () \{ 0x0D, 3 \})} ——GPE 0x0D、最深可從 S3 喚醒(第 17 章)。 說明它與 \code{\_PR3} 的關係:喚醒源在 D3 仍需要哪些資源維持供電。 \item \textbf{接上 Project 2}:把 \code{RAIL} 位元換成呼叫 \nspath{\_SB.GPI0.SETP}(rail 掛在 GPIO pin 2 上), 體會真實平台「PMIC 或 GPIO 控制 rail」的兩種常見接法, 以及跨裝置呼叫的路徑寫法。 \end{enumerate} \end{exbox} \chapter{Project 5:DSDT Reverse Engineering(Final Project)} \label{ch:proj5} 畢業專案。前四個專案的對象都是你自己寫的表——乾淨、短小、 沒有祕密。這次的對象是\textbf{你手上那台真實筆電的 DSDT}: 數萬行、沒有註解、混雜著十年來各代 OEM 工程師的手筆。 你的任務是把它萃取出來、反組譯、畫出這台機器的「地圖」、 深挖其中一個裝置,最後安全地做一次修改並驗證。 這正是 ACPI Firmware Engineer 的日常: 多數時候你不是從零寫表,而是在別人的表裡找答案、修問題。 本章採「任務書+指引」的形式:每個階段給你目標、指令與 搜尋策略,但不再手把手——地圖要你自己畫,因為每台機器都不同。 請準備:一台 Linux 筆電(或桌機)、Module 9 裝好的 acpica-tools,以及至少半天不被打擾的時間。 \section{專案目標與驗收標準} \begin{goalbox} \textbf{專案目標} \begin{itemize} \item 完整執行「萃取 \(\to\) 反組譯 \(\to\) 測繪 \(\to\) 深度分析 \(\to\) 修改 \(\to\) 驗證」的逆向工程流程。 \item 產出你這台機器的\textbf{機器地圖}:EC、電池、LID、 電源鍵、I2C 觸控板、GPIO 控制器、PCIe root ports 分別在 Namespace 的哪裡、用什麼 \code{\_HID}。 \item 深度解剖一個裝置(建議電池或 LID): 畫出 OperationRegion/Field/Method 的呼叫關係, 並與 dmesg、\code{/sys} 的實際行為對照。 \item 以自訂 SSDT 的方式對真機做一次安全修改並驗證; 了解(但不強制執行)直接改 DSDT 的完整流程。 \end{itemize} \textbf{驗收標準}——繳交四樣交付物: \begin{itemize} \item 工作目錄:含 \code{acpi.dat}、各 \code{.dat}/ \code{.dsl}、你的 SSDT 原始碼與筆記。 \item 機器地圖(表格或樹狀圖,格式不拘但必須完整)。 \item 上機證據:dmesg 的 Table Upgrade 訊息、 \code{/sys/bus/acpi/devices} 中你的裝置、 以及\textbf{還原後}系統正常開機的證明。 \item 一頁分析報告(骨架見階段五)。 \end{itemize} \end{goalbox} \section{背景與設計} \subsection{整體流程} \begin{vflow}[4.2mm] \fnode[flownodealt]{真實筆電} \fnodel{acpidump/acpixtract}{DSDT/SSDT 二進位 .dat} \fnodel{iasl -e -d}{可讀的 dsdt.dsl} \fnode{系統測繪:找 Device/\_HID/Method} \fnode{深度分析:EC、電池、GPIO、I2C} \fnodel{自訂 SSDT(或改 DSDT)}{iasl 重新編譯} \fnodel{initrd override}{重開機載入} \fnode[flownodealt]{dmesg/sysfs 驗證+報告} \end{vflow} 這條流程整合了全書:萃取與反組譯是第 21 章工具鏈; 看懂表靠 Module 2--7 的語法與物件知識; override 機制是 Module 8 與 Module 10 的實戰; 驗證手段則是 Module 10 的 sysfs 與 dmesg 觀察點。 \subsection{安全哲學:先加法,後改法} 修改真機韌體描述有兩條路。\textbf{加法}——寫一張自己的 SSDT 疊加上去:影響範圍只有你新增的物件,拔掉 initrd 就完全還原。 \textbf{改法}——反組譯 DSDT、修改、重編譯、整張替換: 能改到既有行為,但反組譯再重編譯本身就可能引入差異 (工具版本、External 解析),風險高一個量級。 本章的原則:\textbf{能用加法就不用改法;改法只在受控環境 (QEMU 或備用機)先演練過才碰主力機}。 這不是膽小,是職業素養——你改的是開機路徑上的東西。 \section{逐步實作} 前四個階段照順序完成;最後的驗證與報告(階段五)獨立成節。 每完成一個階段就把產出物歸檔進工作目錄,不要憑記憶做下一步。 \subsection{階段一:萃取與反組譯} \begin{labbox}[階段一操作] \begin{enumerate} \item 建立有日期與機型的工作目錄——逆向工程的檔案 會越長越多,從第一天就分門別類。 \item 用 \code{acpidump} 一次倒出所有表, \code{acpixtract} 拆檔。 \item 反組譯 DSDT 時,用 \code{-e} 把所有 SSDT 掛進來 解析跨表引用,否則 External 符號會滿天飛。 \item 立刻把原始 \code{.dat} 檔做一份唯讀備份。 \end{enumerate} \end{labbox} \begin{shell}[title={萃取與反組譯(輸出節錄,數字依機器而異)}]\) mkdir -p  /acpi-lab/mylaptop && cd  /acpi-lab/mylaptop \(sudo acpidump -o acpi.dat\) acpixtract -a acpi.dat ... DSDT - 65239 bytes written (0x0000FEE7) - dsdt.dat SSDT - 1972 bytes written (0x000007B4) - ssdt1.dat SSDT - 3186 bytes written (0x00000C72) - ssdt2.dat ... \(iasl -e ssdt*.dat -d dsdt.dat ... Disassembly completed ASL Output: dsdt.dsl - 1.2 MB\) cp *.dat /some/readonly/backup/

另一條等價的取表路徑是 sysfs——單獨要某一張表時更快:

階段二:系統測繪——畫出機器地圖

打開 ,你面對的是幾萬行陌生程式碼。 不要從頭讀——用搜尋策略定位地標。下表是七個必找目標 與對應的搜尋線索( 是你的好朋友):

lL4.6cmL5.2cm 目標 & 搜尋線索 & 要記下什麼
EC & 、 & 裝置路徑、Region 大小、 事件方法清單
電池 & 、、 & 路徑、讀哪些 EC 欄位、誰對它
LID & 、 & 路徑、 讀的來源欄位
電源鍵 & (固定功能則在 FADT) & 存在與否、 喚醒宣告
I2C 觸控板 & 、 & 匯流排位址、掛在哪個控制器、中斷來源
GPIO 控制器 & 、、 Intel 常見 開頭的 & 控制器路徑、哪些裝置消費它的 pin
PCIe root ports & 、 命名、 & root complex 路徑、各 port 的位址

實際操作長這樣:

\(rg -n "PNP0C09" dsdt.dsl 10382: Name (_HID, EisaId ("PNP0C09"))\) rg -n "EmbeddedControl" dsdt.dsl 10394: OperationRegion (ECRM, EmbeddedControl, 0x00, 0xFF)

\(rg -n "Method \(_Q" dsdt.dsl | head -4 10771: Method (_Q09, 0, NotSerialized) 10786: Method (_Q0B, 0, NotSerialized) 10801: Method (_Q0C, 0, NotSerialized) 10817: Method (_Q11, 0, NotSerialized) \end{shell} 把找到的東西整理成地圖(樹狀圖或表格都行), 每一項都附上行號——之後深度分析與寫報告會反覆回來查。 找不到某一項也是有效資訊:例如桌機沒有 LID、 新平台的電源鍵可能是 FADT 描述的固定功能而非 Namespace 裝置 (把「為什麼沒有」寫進報告,比硬湊一個答案更專業)。 \subsection{階段三:深度分析一個裝置(以電池為例)} 從地圖挑一個裝置往下鑽。建議電池:它的資料流長、 橫跨 EC 與事件機制,最能練功。目標是畫出這張呼叫關係圖 (以下是常見筆電的典型結構,你的機器細節會不同): \begin{asciitree}[title={電池裝置的呼叫關係(典型結構示意)}] \_SB.PCI0.LPCB.EC0_ ← EC(PNP0C09) ├── OperationRegion ECRM (EmbeddedControl, 0x00, 0xFF) ├── Field:BTEX,1 BST0,8 BRTE,16 BVOL,16 ... └── Method _Q09 ← EC 事件:充放電狀態改變 └── Notify (BAT0, 0x80) ← 通知 OS:重新讀 _BST \_SB.PCI0.LPCB.BAT0 ← 電池(PNP0C0A) ├── _STA ← 讀 EC0.BTEX:電池在位?(0x1F/0x0F) ├── _BIF ← 讀設計容量、電壓等欄位,組成 Package └── _BST ← Acquire (ECMX)、讀 BST0/BRTE/BVOL、 組 Package 回傳(狀態、電流、餘量、電壓) \end{asciitree} 分析時回答四個問題:\code{\_BST} 讀了哪些 Field? 有沒有拿 Mutex 保護 EC 存取(第 13 章)? \code{\_Q09} 之類的事件方法對誰 \code{Notify}、值是多少? \code{\_STA} 的回傳值在電池拔除時變成什麼? 然後把紙上分析跟活系統對照: \begin{shell}[title={對照 dmesg 與 sysfs 的實際行為}]\) sudo dmesg | grep -i "battery" ACPI: battery: Slot [BAT0] (battery present)

\(cat /sys/class/power_supply/BAT0/voltage_now 12341000\) acpi_listen # unplug AC, watch the events battery BAT0 00000080 00000001 ac_adapter ACAD 00000080 00000000

印出的 正是 AML 裡 的值——紙上讀到的程式碼 與機器的實際行為在這裡對上,逆向工程的閉環完成。

階段四:安全修改

做法 A(必做):疊加自訂 SSDT。 把下面這張探測表載入你的真機——它加一個虛擬裝置、 在 印出 Debug 訊息,證明你有能力把程式碼 送進自己筆電的 ACPI Interpreter:

DefinitionBlock ("probe.aml", "SSDT", 2, "DEMO", "PROBE", 0x00000001)

Scope (_SB)

Device (PRB0)

Name (_HID, "DEMO0099") Name (_UID, Zero)

Method (_STA, 0, NotSerialized) Return (0x0F)

Method (_INI, 0, Serialized) Debug = "PROBE: custom SSDT is alive" Debug = _OS // interpreter’s OS name Debug = _REV // ACPI revision implemented

Method (PING, 0, NotSerialized) Return (Ones)

會在 OS 建完 Namespace、確認 回報存在之後被自動執行一次——是「開機時跑一段我的 AML」 最正當的掛載點。 與 是 Interpreter 的預定義物件; 先用 acpiexec 試跑,你會看到有趣的答案:

\(iasl probe.dsl && acpiexec probe.aml ... Initializing Device/Processor/Thermal objects ... ACPI Debug: "PROBE: custom SSDT is alive" ACPI Debug: "Microsoft Windows NT" ACPI Debug: 0x0000000000000002 Executed 1 _INI methods requiring 1 _STA executions \end{shell} \code{\_OS} 回答「Microsoft Windows NT」——包括 Linux 在內的 ACPICA 系 Interpreter 都固定回這個字串,因為太多 OEM 的 AML 會比對 \code{\_OS},回答別的反而觸發沒人測過的程式路徑。 接著上真機(GRUB 的 early initrd 機制): \begin{shell}[title={上真機:打包並掛進 GRUB}]\) mkdir -p kernel/firmware/acpi \(cp probe.aml kernel/firmware/acpi/\) find kernel | cpio -H newc –create > probe.img \(sudo cp probe.img /boot/\) echo ’GRUB_EARLY_INITRD_LINUX_CUSTOM="probe.img"’  | sudo tee -a /etc/default/grub \(sudo update-grub\) sudo reboot

做法 B(了解流程,在 QEMU/備用機演練):直接改 DSDT。 完整流程:反組譯(同階段一)\(\to\) 修改內容 \(\to\) 把表頭的 OEM Revision 加一 \(\to\) 重新編譯 \(\to\) 與做法 A 相同的打包與掛載。表頭修改示意:

// Before: // DefinitionBlock ("", "DSDT", 2, "OEMID ", "OEMTBL ", // 0x00000001) // After: bump the OEM revision by one DefinitionBlock ("", "DSDT", 2, "OEMID ", "OEMTBL ", 0x00000002)

\(iasl -ve dsdt.dsl\) mkdir -p kernel/firmware/acpi \(cp dsdt.aml kernel/firmware/acpi/\) find kernel | cpio -H newc –create > dsdt-override.img

核心的 table upgrade 以簽章比對要替換的表——DSDT 一定整張換, 不加版號其實也會生效。加一是專業習慣,理由有二: dmesg 與 會顯示版號,一眼分辨機器上跑的是 原廠表還是你的表;而 Windows 對表有快取機制, 版號不變可能繼續用舊表。順帶一提,反組譯再重編譯的 DSDT 常帶著一批 iasl 警告——那是 OEM 用舊版編譯器留下的歷史, 修掉它們是本章的加分挑戰之一。

驗證與報告(階段五)

重開機後,依序收集證據:

\(sudo dmesg | grep -i "table upgrade" ACPI: Table Upgrade: install [SSDT-DEMO -PROBE ]\) ls /sys/bus/acpi/devices/ | grep DEMO DEMO0099:00

\(cat /sys/bus/acpi/devices/DEMO0099:00/path \_SB_.PRB0 \end{shell} 想在真機 dmesg 看到 \code{\_INI} 的 Debug 輸出, 核心需編譯 \code{CONFIG\_ACPI\_DEBUG} 並加開機參數 \code{acpi.debug\_layer=0xffffffff acpi.debug\_level=0x2} (Module 10 的 Debug object 章節);看不到訊息不代表失敗, sysfs 裡有裝置就是載入成功。你也可以用 acpiexec 對萃取出的 原廠表做離線對照——例如確認某方法存在、試跑不碰硬體的 純邏輯方法: \begin{shell}[title={離線對照:把原廠表餵給 acpiexec}]\) acpiexec dsdt.dat ssdt1.dat - find _BST _SB_.PCI0.LPCB.BAT0._BST Method ...

最後對照驗收清單,一項一項打勾:

lL7.6cm 項目 & 通過條件
萃取 & 、全部 與備份齊全, 反組譯成功
地圖 & 七個目標都有結論(找到路徑,或說明為何不存在)
深度分析 & 呼叫關係圖完成,且至少一項與 dmesg/sysfs / 的實際行為互相印證
安全修改 & dmesg 有 Table Upgrade 訊息、sysfs 看得到
還原 & 移除 override 後重開機,dmesg 無 Table Upgrade、 系統一切正常

分析報告寫一頁就好,骨架如下:

  1. 機器概況:機型、BIOS 版本、核心版本、 表清單(幾張 SSDT、各自大小)。

  2. 機器地圖:七個目標的路徑與 總表。

  3. 深度分析:你挑的裝置、呼叫關係圖、 與實際行為的對照證據。

  4. 修改與驗證:改了什麼、怎麼載入、 驗證輸出、如何還原。

  5. 問題與心得:卡住的地方、 與教材範例不同之處、下一步想挖的東西。

常見問題

  • Secure Boot 與 kernel lockdown:啟用 Secure Boot 時核心進入 lockdown,會拒絕 ACPI table override ——dmesg 出現 lockdown 相關訊息、你的表被無聲忽略。 先用 與 檢查; 必要時在 BIOS 關閉 Secure Boot,或改在 QEMU 完成實驗。

  • 永遠準備還原路:override 只存在於 initrd, 不會燒進韌體——刪掉 、 移除 GRUB 那行、 就完全復原; 最壞情況在 GRUB 選單挑舊核心或用救援 USB 開機即可。 動手前先確認自己知道這條退路怎麼走。

  • 絕不在生產機器上第一次實驗:所有流程先在 QEMU 或備用機跑通,確定指令與檔案路徑都對了, 才在日常工作機上重演。改壞 DSDT 的典型症狀是 開機卡死在早期階段——雖然救得回來, 但你不會想在趕工作的早上處理這件事。

加分挑戰

  1. 修掉一個原廠警告:反組譯你的 DSDT 重新編譯, 挑一個 iasl 警告(常見如「Not all control paths return a value」或未使用的 Local 變數), 讀懂該方法、修正它、重編譯到警告消失。 在 QEMU 驗證後,寫下你對這個警告成因的推測。

  2. 把 Project 1 接上驅動:把 ()的 SSDT 載入真機,再用第 25 章的 platform driver 範本寫一個比對 的 ,載入模組後在 dmesg 看到 probe 成功——從 ASL 到 C driver 的全鏈路貫通。

  3. 比較研究:找第二台不同品牌的機器重跑 階段一與二,比較兩家 OEM 的 EC 事件方法 ( 的數量與用途)與電池實作差異, 寫成半頁筆記。看過兩家的手筆, 你對「什麼是慣例、什麼是怪癖」會有全新的體感。