Module 5 的 OperationRegion 與 Field 讓你能描述硬體暫存器,但 與 Field 物件終究是「靜態資料」——它們安靜地躺在 Namespace 裡等人來讀。 真正讓 DSDT「動起來」的是 :一段包在名字裡的 AML 程式,OS 在正確的時機呼叫它,它替 OS 敲硬體、算結果、回報狀態。 從本章起,ASL 對你而言不再只是描述語言,而是一門真正的程式語言。

本章先建立 Method 的完整心智模型:誰呼叫它、何時呼叫、宣告語法每一欄 的意義;接著深入 這個最常被寫錯的選項;最後把多個 Method 組成一個小型計算模組,練習「方法呼叫方法」的完整寫法。

  • 說明 Method 與 Name 靜態物件的差別,以及 OS 在哪些時機呼叫 Method。

  • 寫出完整的 Method 宣告,正確選擇參數個數與 SerializeRule。

  • 說明 Serialized 解決的重入問題,並判斷何時必須使用。

  • 用 ASL 2.0 與 legacy 兩種語法呼叫 Method 並使用回傳值。

  • 說明方法呼叫方法時的名稱解析、遞迴限制與方法內物件的生命週期。

  • 除錯 AE_ALREADY_EXISTS 這類典型的 Method 執行期錯誤。

誰會呼叫 Method?從靜態描述到可執行程式

先複習一個關鍵差異: 在表載入時求值一次, 之後 就是一個躺在 Namespace 裡的整數;而 的本體是一段 AML 位元組碼,載入時只登記名字,每次被呼叫才逐條執行。 換句話說,Name 回答「是什麼」,Method 回答「怎麼做」。

那麼誰來呼叫?在 ACPI 的世界裡,Method 的呼叫者幾乎都是 OS—— 這正是 OSPM「韌體描述、OS 決策」精神的延伸:

llL5.6cm 呼叫者 & 時機 & 典型 Method
OS(列舉) & 開機建立裝置樹時 & 、、
OS(電源) & 睡眠、喚醒、D-state 轉換 & 、、–
OS(事件) & GPE 觸發、EC query & 、、
Driver & 執行期主動查詢與控制 & 、、
其他 Method & AML 內部呼叫 & 你自己寫的輔助方法

一次 Method 呼叫的完整旅程如下:

注意最後一站:Method 的執行者永遠是 OS 內建的 ACPI Interpreter (第 1 章講過),韌體只負責在開機前把這段位元組碼放進表裡。 所以 Method 寫錯的症狀是「OS 執行期報錯」,而不是開機自檢失敗—— 這決定了 Module 10 教的除錯方式都繞著 OS 端的訊息打轉。

Method 宣告:完整語法解剖

Method 宣告最完整的形式有六個欄位,日常使用通常只寫前二到四個:

Method (MethodName, NumArgs, SerializeRule, SyncLevel, ReturnType, ParameterTypes) // TermList: statements executed on every call

llL6.6cm 欄位 & 可省略 & 意義
MethodName & 否 & 4 字元名稱,一般 Namespace 命名規則
NumArgs & 可(預設 0) & 參數個數 0–7,決定能用幾個
SerializeRule & 可 & (預設)或
SyncLevel & 可(預設 0) & 0–15,僅 Serialized 時有意義
ReturnType & 可 & 給 iasl 靜態檢查用,如
ParameterTypes & 可 & 各參數型別清單,同樣僅供檢查

課綱的起手式範例如下——一個把兩數相加的方法,以及呼叫它的測試方法:

DefinitionBlock ("add2.aml", "SSDT", 2, "DEMO", "ADD2DEMO", 0x01)

Scope (_SB)

Method (ADD2, 2) // 2 args, NotSerialized Return (Arg0 + Arg1)

Method (TST0, 0) Local0 = ADD2 (10, 20) // ASL 2.0 style Store (ADD2 (30, 40), Local1) // legacy style Return (ADD2 (Local0, Local1)) // methods compose

逐行看重點:

  • :宣告兩個參數,省略 SerializeRule 表示 。參數在方法內以 、 存取,最多到 ——這是 AML 編碼層面的硬限制, 第 13 章會再深入。

  • :ASL 2.0 允許運算式直接出現在 裡;legacy 寫法得先 再 。

  • :呼叫語法就是「名稱加括號」, 回傳值可以直接指定給變數、當運算元、或丟進條件式。

最後兩個欄位(ReturnType、ParameterTypes)只給 iasl 做編譯期靜態檢查, 不會編進 AML,市售 DSDT 幾乎沒人寫。另外要注意:iasl 只能檢查 同一張表內的呼叫;跨表呼叫(例如 SSDT 呼叫 DSDT 的方法) 得用 宣告對方,此時參數個數寫錯,編譯器無從發現, 要到執行期才爆炸——第 13 章會示範爆炸現場。

呼叫 Method 與使用回傳值

呼叫語法在 ASL 2.0 與 legacy 兩代寫法中都存在,你在舊 DSDT 反組譯輸出裡兩種都會遇到,必須都看得懂:

// ASL 2.0 (recommended for new code) Local0 = ADD2 (10, 20) Local1 = ADD2 (1, 2) + 3 // call inside expression If (ADD2 (Local0, 5) > 10) // call inside condition Local2 = 1

// Legacy (what you will see in shipping DSDTs) Store (ADD2 (10, 20), Local0) Store (Add (ADD2 (1, 2), 3), Local1) If (LGreater (ADD2 (Local0, 5), 10)) Store (1, Local2)

幾條實用規則:

  • 不需要回傳值時,把呼叫單獨寫成一行即可,如 。

  • 有三種姿態: 帶值返回、 或 空手返回、 以及執行到方法結尾自然結束。

  • 一個方法可以有多個 ,分散在不同的 If 分支—— 但要確保「每條路徑」的行為都是你要的。

如果某條執行路徑沒有明確 ,呼叫端拿到什麼是 未定義行為:有的 interpreter 隱含回傳最後一次運算的結果 (所謂 implicit return,ACPICA 提供 slack mode 相容這種寬鬆行為), 有的回傳零或直接報錯。同一份 DSDT 在 Windows 正常、在 Linux 出錯, 不少案例正是踩了這條線。準則很簡單:只要呼叫端會用到回傳值, 每條路徑都要明確 Return。iasl 會對「有些路徑有 Return、有些沒有」 的方法提出警告,別忽略它。

Serialized:方法重入與 AE_ALREADY_EXISTS

是宣告欄位裡最容易被忽略、也最常引發詭異錯誤的一個。 要理解它,先看一個反面教材:

Method (GETB, 0, NotSerialized) // BUG: not serialized Name (BUF0, Buffer (8) ) // creates a named object CreateWordField (BUF0, 0, WRD0) // another named object WRD0 = 0x1234 Return (BUF0)

問題出在 與 :方法內建立的具名物件 會真的掛進 Namespace(掛在方法自己的 scope 底下), 等方法返回時才移除。現在想像兩條執行緒幾乎同時呼叫 ——現代 OS 的 ACPI 子系統本來就是多執行緒的,兩個 driver 同時查詢、或 GPE 事件撞上 driver 呼叫,都稀鬆平常:

  • 執行緒 A 進入方法,成功建立 。

  • 執行緒 B 也進入方法,執行 時發現 Namespace 裡已經有同名物件——建立失敗。

  • Interpreter 中止執行緒 B 的這次呼叫,回報 。

在 Linux 上,dmesg 會留下這樣的犯罪現場:

\(dmesg | grep "ACPI Error" ACPI Error: [BUF0] Namespace lookup failure, AE_ALREADY_EXISTS (20230628/dsfield-184) ACPI Error: Aborting method \_SB.EC0.GETB due to previous error (AE_ALREADY_EXISTS) (20230628/psparse-529) \end{shell} 解法只要一個字:把宣告改成 \code{Method (GETB, 0, Serialized)}。 Serialized 等於替方法配一把隱含的 Mutex:第二個呼叫者會排隊, 等第一個離開後才進入,具名物件屆時早已移除,自然不再撞名。 iasl 其實會主動提醒你——對「建立具名物件卻宣告 NotSerialized」的方法, 編譯時會給一條 remark: \begin{shell}[title={iasl 對未序列化方法的提醒}]\) iasl getb.dsl ... getb.dsl 14: Method (GETB, 0, NotSerialized) Remark 2120 - ^ Control Method should be made Serialized (due to creation of named objects within)

  • 方法內用了 、 系列、 等會建立具名物件的敘述 \(\to\) 一律 Serialized,沒有例外。

  • 方法對硬體做「讀—改—寫」序列、或操作共享的暫存器組 \(\to\) 用 Serialized 保護,避免兩次呼叫交錯。

  • 純運算、只碰 與 \(\to\) 即可,讓呼叫可以並行。

  • 看到 iasl 的 Remark 2120,當成錯誤處理,不要放行。

至於第四欄 SyncLevel:進入 Serialized 方法等同於取得一把 SyncLevel 為 N 的隱含 Mutex,而 ACPI 規定持有 SyncLevel N 的鎖時, 只能再取得 SyncLevel 大於等於 N 的鎖——這是防止死鎖的階層鎖設計。 絕大多數韌體把它留在預設值 0;只有在方法之間有巢狀鎖需求時 才需要精心安排,屆時記得「外層小、內層大」。

方法呼叫方法、遞迴與方法內的 Namespace

方法內呼叫另一個方法時,名稱解析走 Module 2 教過的規則: 先在目前 scope 找,找不到就一層層往上。實務建議是: 同一個 Device 內的輔助方法用相對名稱(如 ), 跨裝置、跨 scope 一律寫絕對路徑(如 ), 省得日後搬動程式碼時解析結果悄悄改變。

方法內建立的具名物件有三條性質,合在一起記:

  • 動態建立:執行到 那行才建立, 不是載表時建立。

  • 外部不可見:物件掛在方法 scope 下, 方法外的程式碼與 OS 都查不到它。

  • 返回即銷毀:方法結束(或中止)時自動移除, 下次呼叫重新建立。

那遞迴呢?規格允許方法呼叫自己,但有兩道現實的牆。 第一,ACPICA 對同一方法的巢狀重入設了上限(255 層), 超過就回報 ; 第二,含具名物件的方法一遞迴就撞 ——而且 Serialized 也救不了,因為隱含 Mutex 對同一條執行緒是可重入的,擋不住自己撞自己。 結論:AML 是硬體控制的膠水語言,不是寫演算法的地方, 需要重複就用 While 迴圈,不要用遞迴。

完整範例:感測器數值處理模組

把本章所學組裝起來:一個由三個 Method 構成的小型計算模組, 模擬「把感測器原始讀值換算成毫度並夾在合理範圍內」的常見需求。

DefinitionBlock ("calc.aml", "SSDT", 2, "DEMO", "CALCDEMO", 0x01)

Scope (_SB)

Device (CALC)

Name (_HID, "DEMO0001")

// Larger of two integers Method (MAX2, 2) If (Arg0 >= Arg1) Return (Arg0) Return (Arg1)

// Clamp Arg0 into the range [Arg1, Arg2] Method (CLM3, 3) Local0 = MAX2 (Arg0, Arg1) // lower bound If (Local0 > Arg2) Return (Arg2) // upper bound Return (Local0)

// Raw sensor value -> milli-degrees, clamped. // Uses a named scratch buffer: must be Serialized Method (SCAL, 1, Serialized) Name (TMPB, Buffer (4) ) CreateDWordField (TMPB, 0, VAL0) VAL0 = Arg0 * 125 // 1 LSB = 125 mC Return (CLM3 (VAL0, 0, 100000))

逐行拆解:

  • 與 只碰 和 , 不建立具名物件,所以放心用預設的 NotSerialized, 讓多個呼叫者可以並行。

  • 用相對名稱呼叫同 scope 的 : 先套下界,再用 If 套上界。兩個 分別覆蓋兩條路徑,沒有漏網之魚。

  • 內有 與 , 依準則宣告 。 是 buffer field, 寫入時自動依 32 位元寬度截斷——這是拿 Buffer 當「固定寬度暫存器」用的慣用手法。

  • :把另一個方法的 回傳值直接轉交,呼叫鏈 \(\to\) \(\to\) 深度只有 3,離 255 的重入上限遠得很。

用 iasl 編譯確認無誤後,你已經會寫「可執行」的 ASL 了。 但眼尖的你應該注意到:參數與區域變數還有很多沒講清楚的細節—— 到底是複本還是參考? 沒指定初值可以直接讀嗎? 這些正是下一章的主題。

  • Name 載表時求值一次是「資料」;Method 每次呼叫才執行, 是「程式」。呼叫者是 OS(列舉、電源、事件)、driver 與其他方法。

  • 宣告六欄位:名稱、參數個數(0–7)、SerializeRule、 SyncLevel、ReturnType、ParameterTypes;日常只寫前二到四個。

  • 方法內建立具名物件就必須 , 否則多執行緒重入會以 中止。

  • 呼叫語法兩代並存: 與 ;會用到回傳值的方法, 每條路徑都要明確 。

  • 方法內物件動態建立、外部不可見、返回即銷毀; 遞迴技術上可行但實務禁用,重複邏輯交給 While。

  1. 說出 Method 與 Name 物件的三個差異:求值時機、能否帶參數、 對 Namespace 的影響。

  2. 一個宣告為 NotSerialized 的方法內含 , 兩條執行緒同時呼叫它,逐步描述會發生什麼事、 dmesg 會出現哪個錯誤碼。

  3. 把 legacy 寫法 改寫成 ASL 2.0 語法。

  4. 為 12.6 的模組新增 (回傳較小值), 並改寫 讓它同時用 與 完成夾範圍,兩條路徑合併成一條。

  5. 動手:把 calc.dsl 存檔並用 編譯; 接著故意把 改回 NotSerialized 再編譯一次, 觀察並記下 iasl 的 remark 內容。

Method Arguments

上一章把 Method 當成一個會動的黑盒子;本章打開盒子。 一次方法呼叫的內部世界由四個部分組成:Arguments(呼叫者給的)、 Locals(自己用的)、Namespace Objects(動態建立的)、 Return Value(交回去的)。搞清楚每一部分的生命週期與傳遞語意, 是寫出正確 Method 的前提,也是讀懂一大類 dmesg 錯誤訊息的鑰匙。

這一章的知識點瑣碎但環環相扣,我們按「先畫全景圖,再逐格放大」的 順序走:四個組成、Arg 的傳遞語意、Local 的初始化規則、 參數個數不匹配的下場,最後是 RefOf 傳參考與 Package 進出的完整範例。

  • 畫出 Method 一次呼叫的四個組成,並說明各自的生命週期。

  • 說明 Arg0 到 Arg6 的傳遞語意:何時拿到複本、何時拿到參考。

  • 說明 Local0 到 Local7 的初始化規則, 除錯 AE_AML_UNINITIALIZED_LOCAL。

  • 解釋參數個數不匹配時 interpreter 的行為與對應錯誤碼。

  • 用 RefOf 與 DerefOf 讓被呼叫的方法把結果寫回呼叫端的變數。

  • 寫出傳遞 Package 與回傳 Package 的 Method。

一次呼叫的世界:Method 的四個組成

每次呼叫 Method,interpreter 都會替這次呼叫建立一個全新的執行環境, 方法返回時整組拆除。它的內容物固定是四類:

Method (STAT, 2, Serialized) 的「一次呼叫」 ├── Arguments Arg0   Arg6:呼叫者傳入,最多 7 個 ├── Local Variables Local0   Local7:方法私有暫存,共 8 個 ├── Namespace Objects 方法內 Name() 等動態建立,返回時銷毀 └── Return Value Return (...) 交回呼叫者的唯一輸出

「每次呼叫都是全新的一份」這句話值得多想三秒: 兩條執行緒同時執行同一個方法時,各自擁有獨立的 Arg 與 Local, 互不干擾——所以上一章說「只碰 Arg 與 Local 的方法可以放心 NotSerialized」;真正共享的是 Namespace(第三格)與硬體, 那才是需要 Serialized 與 Mutex 保護的東西。

Arguments:Arg0 到 Arg6

宣告 代表呼叫者要給兩個參數, 方法內用 、 取用。上限七個( 到 )是 AML 位元組碼格式的硬限制; 真需要傳更多資料,就打包成一個 Package 傳進來(本章 13.6 示範)。

傳遞語意是本節的重頭戲。基本型別——Integer、String、Buffer—— 可以當成逐值傳遞來理解:方法拿到的是「這次呼叫自己的一份」, 對 整個重新賦值,呼叫端完全無感:

Method (NOPE, 1) Arg0 = 999 // rebinds the local slot only

Method (TRY0, 0) Local0 = 5 NOPE (Local0) Return (Local0) // still 5, not 999

想讓被呼叫者改到呼叫端的東西,正規做法是下一節的 明確傳參考。先把語意整理成表:

lL4.2cmL4.6cm 呼叫者傳入 & Arg 裡是什麼 & callee 能改到呼叫端嗎
Integer 常數或變數 & 值的複本 & 不能
String、Buffer & 該次呼叫的一份物件 & 整個覆寫不能
Package & 物件的參考 & 改元素可能可以(見補充)
& 明確的物件參考 & 能,這是正規寫回管道

嚴格說,AML 傳遞 String、Buffer、Package 時給的都是物件參考, 所以在 callee 內用 直接改 的某個元素, ACPICA 上會改到呼叫端的物件;但「整個覆寫 」 只是讓這個 slot 改指別的東西。這個邊角語意在不同 OS 的 interpreter 上有實作差異,是跨平台相容性的地雷區。 實務準則:要寫回就用 RefOf 明講, 不要依賴隱含共享;把「逐值」當成預設心智模型最安全。

Locals:Local0 到 Local7

每次呼叫有八個區域變數 到 , 型別是動態的:存整數它就是整數,存 Buffer 它就是 Buffer。 關鍵規則只有一條:Local 一開始是「未初始化」狀態, 寫過才能讀。讀一個沒寫過的 Local,interpreter 直接中止方法:

\(dmesg | grep "ACPI Error" ACPI Error: AE_AML_UNINITIALIZED_LOCAL, While resolving operands (20230628/dswexec-431) ACPI Error: Aborting method \_SB.DEMO.BADM due to previous error (AE_AML_UNINITIALIZED_LOCAL) (20230628/psparse-529) \end{shell} 這個錯誤最常見的長相不是「忘記初始化」這麼直白, 而是\textbf{某條 If 分支忘了設值}: \begin{asl}[title={片段:只有一條路徑初始化 Local 的典型 bug}] Method (BADM, 1) { If (Arg0 > 10) { Local0 = 1 } Return (Local0) // BUG: Arg0 <= 10 leaves Local0 unset } \end{asl} 養成習慣:要在分支裡賦值的 Local,先在分支前給預設值 (\code{Local0 = 0}),讓每條路徑都有明確定義。 \section{參數個數不匹配:Interpreter 不會救你} 呼叫端給的參數個數與宣告不符時會怎樣?分三種情況: \begin{itemize} \item \textbf{同一張表內}:iasl 看得到宣告, 編譯期直接報錯(Too few/too many arguments),過不了關。 \item \textbf{跨表呼叫、少給}:編譯期查不到,執行期 callee 一讀到沒人給值的 Arg,就以 \code{AE\_AML\_UNINITIALIZED\_ARG} 中止——症狀跟 未初始化 Local 一模一樣,只是錯誤碼不同。 \item \textbf{跨表呼叫、多給}:多的參數被默默忽略, 通常「看起來沒事」,直到有人改了方法簽名才一起爆發。 \end{itemize} \begin{warnbox}[AML 沒有執行期的參數個數檢查] AML 位元組碼裡的方法呼叫只是「名稱+一串運算元」, interpreter 不做跨表的編譯期檢查,也不做執行期的個數核對。 SSDT 呼叫 DSDT 方法時,\code{External} 宣告寫錯參數個數、 或日後有人改了 DSDT 端的 NumArgs 而忘了同步 SSDT, 都只會在執行期以 \code{AE\_AML\_UNINITIALIZED\_ARG} 收場。 維護多張表時,把共用方法的簽名當成 ABI 看待: \textbf{改簽名等於改介面,所有呼叫端都要盤點}。 \end{warnbox} \section{傳參考:RefOf 與 DerefOf} 需要「被呼叫者把結果寫回呼叫端變數」時,用 \code{RefOf} 取得物件參考傳進去。callee 端的規則要背起來: \begin{itemize} \item \textbf{讀}:\code{Arg0} 本身是參考,取值要用 \code{DerefOf (Arg0)}。 \item \textbf{寫}:對存著參考的 \code{Arg0} 執行 \code{Store (值, Arg0)},會\textbf{寫穿}參考、 改到被參考的物件——這是 ACPI 規格明定的行為。 \end{itemize} \begin{asl}[title={片段:透過參考把結果寫回呼叫端}] Name (CNT0, 5) Method (INCR, 1) // Arg0: reference to an Integer { Local0 = DerefOf (Arg0) + 1 Store (Local0, Arg0) // writes through the reference } Method (TRY1, 0) { INCR (RefOf (CNT0)) // CNT0: 5 -> 6 Return (CNT0) } \end{asl} 逐行拆解: \begin{itemize} \item \code{RefOf (CNT0)} 產生指向 \code{CNT0} 的參考物件, 以它為實參呼叫 \code{INCR}。 \item callee 內 \code{DerefOf (Arg0)} 取出目前值 5,加一後 \code{Store (Local0, Arg0)} 寫穿參考,\code{CNT0} 變成 6。 \item 若 callee 想把參考存進 Local 再操作,注意下面補充框的差異。 \end{itemize} \begin{notebox}[Arg 與 Local 對參考的待遇不同] 同樣存著一個參考,\code{Store} 到 \textbf{Arg} 是「寫穿」 (改到被參考的物件);\code{Store} 到 \textbf{Local} 卻是「覆蓋」(Local 改存新值,參考被丟掉)。 想透過 Local 裡的參考寫回目標,得寫 \code{Store (值, DerefOf (Local0))} 這類明確解參考的形式。 這是 ACPI 規格裡最不對稱的角落之一,跨 interpreter 行為也最容易出入——寫回邏輯儘量放在 Arg 上,簡單、可攜。 \end{notebox} \section{完整範例:Package 進、Package 出} Package 是 ASL 裡的萬用容器,方法之間傳「一組資料」全靠它。 下面的範例涵蓋三個常見動作:接收 Package 並走訪加總、 建立 Package 回傳、以及呼叫端拆包取值。 \begin{asl}[title={args.dsl:Package 與參考的綜合演練}] DefinitionBlock ("args.aml", "SSDT", 2, "DEMO", "ARGSDEMO", 0x01) { Scope (\_SB) { Name (CNT0, 0) // shared counter // Sum all integers inside a package Method (PSUM, 1) { Local0 = 0 // accumulator Local1 = 0 // index Local2 = SizeOf (Arg0) While (Local1 < Local2) { Local0 += DerefOf (Arg0 [Local1]) Local1++ } Return (Local0) } // Build a status package: [id, raw, scaled, flag] Method (STAT, 2, Serialized) { Name (PKG0, Package (4) {0, 0, 0, 0}) PKG0 [0] = Arg0 PKG0 [1] = Arg1 PKG0 [2] = Arg1 * 10 If (Arg1 > 100) { PKG0 [3] = 1 } Return (PKG0) } // Increment an integer through a reference Method (INC2, 1) { Local0 = DerefOf (Arg0) + 1 Store (Local0, Arg0) } Method (DEMO, 0, Serialized) { Name (VALS, Package () {3, 5, 8}) Local0 = PSUM (VALS) // 16 INC2 (RefOf (CNT0)) // CNT0: 0 -> 1 Local1 = STAT (1, 120) // returned package Local2 = DerefOf (Local1 [2]) // scaled = 1200 Return (Local0 + Local2) } } } \end{asl} 逐行拆解: \begin{itemize} \item \code{PSUM} 用 \code{SizeOf (Arg0)} 取元素個數, \code{DerefOf (Arg0 [Local1])} 是「索引再解參考」的標準連招 ——\code{Index}(此處是 \code{[]} 語法糖)回傳的是元素參考, 要 \code{DerefOf} 才拿到值。 \item \code{STAT} 在方法內 \code{Name} 出 \code{PKG0}, 所以宣告 \code{Serialized}(第 12 章的準則)。 \code{PKG0 [0] = Arg0} 是 ASL 2.0 的元素賦值, 等價於 legacy 的 \code{Store (Arg0, Index (PKG0, 0))}。 \item \code{STAT} 直接 \code{Return (PKG0)}:方法返回時 \code{PKG0} 這個具名物件會銷毀,但回傳的 Package 內容會交給呼叫端——interpreter 負責延長它的壽命。 \item \code{DEMO} 依序演練三招:傳 Package 進去、 傳參考讓 \code{INC2} 寫回 \code{CNT0}、 接住 \code{STAT} 的回傳再用 \code{DerefOf (Local1 [2])} 拆包。 \end{itemize} 参數超過七個怎麼辦?答案也在這個範例裡:把資料打包。 呼叫 \code{STAT (1, 120)} 若改成傳一個 \code{Package () \{1, 120, ...\}},一個 Arg 就能載任意多的欄位 ——這是 \code{\_DSM} 這類標準方法的慣用介面設計。 \begin{keybox}[本章重點回顧] \begin{itemize} \item 一次呼叫四個組成:Arguments、Locals、Namespace Objects、 Return Value;前兩者每次呼叫獨立,互不干擾。 \item Arg 上限七個(\code{Arg0}--\code{Arg6}); 基本型別視為逐值,要寫回就用 \code{RefOf} 傳參考、 \code{DerefOf} 取值,\code{Store} 到 Arg 會寫穿參考。 \item Local 共八個,未初始化就讀會以 \code{AE\_AML\_UNINITIALIZED\_LOCAL} 中止方法; 分支賦值前先給預設值。 \item 參數個數只有同表編譯期檢查;跨表錯配在執行期以 \code{AE\_AML\_UNINITIALIZED\_ARG} 爆發。 \item Package 是傳遞複合資料的標準容器: \code{DerefOf (Pkg [i])} 取元素、\code{Pkg [i] = x} 寫元素、 超過七個參數就打包。 \end{itemize} \end{keybox} \begin{exbox} \begin{enumerate} \item 不看書畫出 Method 的四個組成,並各用一句話說明生命週期。 \item 為什麼兩條執行緒同時執行「只用 Arg 與 Local 的方法」不需要 Serialized?共享的東西到底在哪一格? \item 寫一個 \code{SWAP} 方法,用兩個 RefOf 參數交換呼叫端 兩個整數變數的值。 \item 下面的方法哪裡會出錯、錯誤碼是什麼: 方法宣告兩個參數,呼叫端只給一個, 且方法內第一行就是 \code{Local0 = Arg1}。 \item 動手:把 args.dsl 編譯通過後,故意把 \code{DEMO} 裡的 \code{PSUM (VALS)} 改成 \code{PSUM (VALS, 1)} 再編譯, 記下 iasl 的錯誤訊息;再想想若這發生在跨表呼叫會怎樣。 \end{enumerate} \end{exbox} \chapter{Notify} \label{ch:notify} 到目前為止,所有互動都是單向的:OS 呼叫 AML,AML 回答。 但電池拔了、上蓋闔了、溫度飆了——這些事 OS 不會自己知道, 必須由韌體這一側主動開口。\keyterm{Notify} 就是 AML 對 OS 說話的唯一正式管道:它把「某裝置發生了某類事件」丟進 OS 的事件佇列, 喚起對應的 driver 來處理。ACPI 筆電上幾乎所有「有感」的功能 ——電量圖示更新、闔蓋睡眠、風扇轉速——背後都是一條 Notify 事件鏈。 本章沿著事件的旅程走一遍:先看事件的源頭 GPE 與 SCI, 再看兩種 GPE handler 的差異、Notify 的語法與代碼表, 最後用 EC 電池事件把整條鏈串起來,並在 Linux 上實際觀察。 \begin{goalbox} \begin{itemize} \item 畫出從 GPE 硬體訊號到 OS driver 的完整事件鏈。 \item 分辨 \code{\_Lxx} 與 \code{\_Exx} handler 的觸發與清除差異。 \item 查表選用正確的 Notify 代碼,說明標準碼與裝置自訂碼的分界。 \item 寫出 EC 的 \code{\_Qxx} query 方法,用 Notify 通知電池裝置。 \item 在 Linux 上用 acpi\_listen 與 GPE 計數器觀察 Notify 事件。 \end{itemize} \end{goalbox} \section{從同步到非同步:為什麼需要 Notify} 第 12 章的 Method 呼叫是\textbf{同步}的:OS 出題、AML 當場作答。 但硬體事件天生是\textbf{非同步}的——使用者何時拔電源線, 誰也料不到。ACPI 的解法分兩段:硬體側用中斷(GPE)把 OS 叫醒, AML 側用 Notify 告訴 OS「該找哪個裝置的 driver、發生哪類事」。 \begin{keybox}[Notify 是 ACPI 世界的「中斷」] Notify 之於 ACPI,就像中斷之於 CPU:非同步、由下往上、 只帶極少量資訊。\code{Notify (裝置, 代碼)} 只送出一個代碼, \textbf{不夾帶任何資料}——OS 收到後要自己回頭呼叫該裝置的 Method(如 \code{\_BST})查詳情。這是典型的 「門鈴模型」:門鈴只告訴你有人來,開門看是誰是你的事。 這個 pull 設計讓事件本身極輕量,也天然去重: 狀態再怎麼連環變化,OS 查到的永遠是最新值。 \end{keybox} \section{事件的源頭:GPE 與 SCI} \keyterm{GPE}(General Purpose Event)是晶片組提供的一組 「通用事件腳位」,每個 GPE 對應狀態暫存器裡的一個 bit。 FADT 描述了 GPE 暫存器區塊(GPE0\_BLK 等)的位址, 每個 bit 都有 status(發生了沒)與 enable(要不要理它)兩半。 任何一個致能中的 GPE status 被硬體置 1,晶片組就對 CPU 發出 \keyterm{SCI}(System Control Interrupt,傳統上是 IRQ 9) ——這是 ACPI 專屬的中斷線。 OS 收到 SCI 後的標準動作是:掃描 GPE status 暫存器找出是哪一個 bit,然後到 Namespace 的 \nspath{\_GPE} scope 下找對應的 handler method 執行。整條鏈如下: \begin{vflow}[4.2mm] \fnode{裝置拉起 GPE 腳位} \fnode{GPE status bit 置 1} \fnodel{晶片組}{CPU 收到 SCI 中斷(IRQ 9)} \fnodel{OS ACPI core}{掃描 status,找到 GPE 0x02} \fnodel{查 Namespace}{執行 \_GPE.\_L02 或 \_GPE.\_E02} \fnodel{handler 查詢硬體}{Notify (目標裝置, 事件代碼)} \fnode{OS 把事件派送給對應 driver} \end{vflow} 注意分工:GPE 只說「第 2 號事件線響了」, 至於是哪個裝置、什麼事,由 handler method 裡的 AML 查詢硬體後用 Notify 補完。一條 GPE 線常常由多個裝置共用, handler 的第一件事就是逐一檢查誰在按門鈴。 \section{Level 與 Edge:\_Lxx 與 \_Exx} GPE handler 的名字有嚴格格式:\code{\_L} 或 \code{\_E} 加上兩位十六進位的 GPE 編號,放在 \nspath{\_GPE} scope 下。 字首決定這條 GPE 的觸發模式,而觸發模式決定了 status bit 由誰、在何時清除: \begin{center} \small \begin{tabular}{lL{4.9cm}L{4.9cm}} \toprule \rowcolor{acpiblue!10} & \textbf{\code{\_Lxx}(Level)} & \textbf{\code{\_Exx}(Edge)} \\ \midrule 觸發條件 & 訊號電位持續為高 & 訊號出現一次跳變 \\ status 清除 & handler 執行\textbf{完}後由 OS 清除 & 執行 handler \textbf{前}先清除 \\ 適用事件源 & 可能持續 assert 的來源(EC、thermal) & 一次性脈衝(按鈕類) \\ 漏事件風險 & 低:電位還在就會再觸發 & 靠「先清再跑」接住執行中的新脈衝 \\ \bottomrule \end{tabular} \end{center} \begin{asl}[title={片段:\_GPE scope 中的兩種 handler}] Scope (\_GPE) { // GPE 0x02, level-triggered: PCIe hotplug slot Method (_L02, 0) { // query slot status registers here ... Notify (\_SB.PCI0.SLT1, 0x01) // device check } // GPE 0x0D, edge-triggered: dock eject button Method (_E0D, 0) { Notify (\_SB.DOCK, 0x03) // eject request } } \end{asl} 同一個 GPE 編號只能有一種字首——\code{\_L02} 與 \code{\_E02} 二選一,由硬體接線與晶片組設定決定,不是你想選哪個就選哪個。 韌體工程師的職責是把字首寫得跟硬體一致: 把 level 來源寫成 \code{\_Exx},事件會在 handler 還沒處理完來源時就被清掉狀態,之後電位持續為高卻再也不觸發, 症狀是「事件偶爾漏掉」,非常難查。 \section{Notify 語法與代碼表} 語法只有一行,但兩個參數都有講究: \begin{asl}[title={片段:Notify 語法}] Notify (\_SB.BAT0, 0x80) // target object, notification value \end{asl} 目標必須是 \code{Device}、\code{ThermalZone} 或 \code{Processor} 物件;代碼是一個位元組,空間被規格切成三段: \begin{center} \small \begin{tabular}{llL{6.9cm}} \toprule \rowcolor{acpiblue!10} \textbf{代碼} & \textbf{名稱} & \textbf{意義} \\ \midrule 0x00 & Bus Check & 此點以下的匯流排結構可能變了,請重新列舉 \\ 0x01 & Device Check & 檢查此裝置是否出現/消失(熱插拔) \\ 0x02 & Device Wake & 此裝置剛喚醒了系統(搭配 \code{\_PRW}) \\ 0x03 & Eject Request & 使用者請求退出此裝置(搭配 \code{\_EJ0}) \\ 0x04 & Device Check Light & 同 0x01,但不必重掃子樹 \\ 0x05--0x0D & (其餘標準碼) & 頻率不符、電源故障、PLD 變更等 \\ 0x0E--0x7F & 保留 & 規格保留,不可自用 \\ 0x80--0xBF & 裝置自訂 & 意義由\textbf{裝置類別}的規格段落定義 \\ 0xC0--0xFF & OEM 自訂 & 硬體廠商自行定義 \\ \bottomrule \end{tabular} \end{center} 0x80 起的「裝置自訂」段是日常主力。同一個 0x80, 掛在電池上和掛在熱區上意義不同——查表時要先看裝置類別: \begin{center} \small \begin{tabular}{llL{6.2cm}} \toprule \rowcolor{acpiblue!10} \textbf{裝置類別} & \textbf{代碼} & \textbf{意義(OS 的後續動作)} \\ \midrule 電池(PNP0C0A) & 0x80 & 狀態變了,重讀 \code{\_BST} \\ 電池(PNP0C0A) & 0x81 & 資訊變了,重讀 \code{\_BIF}/\code{\_BIX} \\ AC 變壓器(ACPI0003) & 0x80 & 插拔了,重讀 \code{\_PSR} \\ 上蓋(PNP0C0D) & 0x80 & 開闔了,重讀 \code{\_LID} \\ 熱區(ThermalZone) & 0x80 & 溫度事件,重讀 \code{\_TMP} \\ 熱區(ThermalZone) & 0x81 & trip point 變了,重讀 \code{\_PSV} 等 \\ Processor & 0x80 & \code{\_PPC} 變了,重新評估限速 \\ Processor & 0x81 & \code{\_CST} 變了,重讀 C-state 表 \\ \bottomrule \end{tabular} \end{center} \begin{warnbox}[兩個經典的 Notify 誤用] \textbf{誤用保留碼}:0x0E--0x7F 是規格保留區,拿來當自訂事件, 現在沒事不代表下一版規格不會賦予它新意義,屆時 OS 的反應會完全出乎你意料。自訂事件一律從 0x80 起跳。 \textbf{事件風暴}:handler 裡 Notify 之後,OS 會回頭呼叫 \code{\_BST} 這類查詢方法;若查詢方法又碰了會觸發同一條 GPE 的硬體(例如寫 EC 暫存器導致 EC 再拉 SCI),就會形成 Notify\)\(查詢\)\(再觸發的無窮迴圈, 症狀是 CPU 一顆核心被 SCI 吃滿。查詢路徑要保持「唯讀」。 \end{warnbox} \section{完整範例:EC query 到電池事件鏈} 實務上最常見的 Notify 源頭是 \keyterm{Embedded Controller}。 EC 有自己的事件機制:它拉起 SCI 後,OS 的 EC driver 會送 query 命令問它「什麼事」,EC 回一個位元組(如 0x17), OS 便執行 EC 裝置底下名為 \code{\_Q17} 的方法—— \code{\_Q} 加上 query 值的兩位十六進位,命名規則和 \code{\_Lxx} 如出一轍。以下是「電池狀態改變」的完整劇本: \begin{asl}[title={ecbat.dsl:EC query 事件到電池 Notify}] DefinitionBlock ("ecbat.aml", "SSDT", 2, "DEMO", "ECBAT", 0x01) { Scope (\_SB) { Device (BAT0) { Name (_HID, EisaId ("PNP0C0A")) Name (_UID, 0) Method (_STA, 0) { Return (0x1F) // present + functioning } Method (_BST, 0, Serialized) { Name (PKG0, Package (4) {0, 0, 0, 0}) PKG0 [0] = \_SB.EC0.BST0 // state bits PKG0 [1] = 0x500 // rate (demo value) PKG0 [2] = \_SB.EC0.BRC0 // remaining capacity PKG0 [3] = \_SB.EC0.BPV0 // present voltage Return (PKG0) } } Device (EC0) { Name (_HID, EisaId ("PNP0C09")) Name (_GPE, 0x17) // EC SCI uses GPE 0x17 OperationRegion (ECRM, EmbeddedControl, 0x00, 0xFF) Field (ECRM, ByteAcc, Lock, Preserve) { Offset (0x60), BST0, 8, // battery state bits BRC0, 16, // remaining capacity BPV0, 16 // present voltage } // EC query 0x17: battery status changed Method (_Q17, 0) { Notify (\_SB.BAT0, 0x80) // status change } // EC query 0x18: battery inserted or removed Method (_Q18, 0) { Notify (\_SB.BAT0, 0x81) // info change } } } } \end{asl} 逐行拆解: \begin{itemize} \item \code{Name (\_GPE, 0x17)}:EC 裝置用這個物件告訴 OS 「我的 SCI 走 GPE 0x17」。OS 的 EC driver 會接管這條 GPE, 所以你\textbf{不需要}也不應該再寫 \code{\_L17}—— query 分派是 EC driver 的內建行為。 \item \code{\_Q17} 是事件的 AML 入口:EC 韌體回報 query 值 0x17 時被執行。它只做一件事——Notify 電池裝置。 \textbf{保持 \code{\_Qxx} 輕薄}是鐵則:重活留給 OS 回頭呼叫的查詢方法。 \item 兩個 query 用不同代碼:0x80(狀態變了,如電量、充放電) 讓 OS 重讀 \code{\_BST};0x81(電池本體變了,如拔插電池) 讓 OS 重讀 \code{\_BIF}/\code{\_BIX} 拿設計容量等靜態資訊。 \item \code{\_BST} 從 EC 的 Field 讀出目前狀態組成 Package 回傳 (欄位順序是規格定死的:狀態、放電速率、剩餘容量、電壓)。 真實韌體還會配合 \code{\_REG} 確認 EC region 可用(Module 5), 示範版從簡。 \end{itemize} 整條事件鏈的時序如下——從硬體到電量圖示更新,九站接力: \begin{vflow}[4mm] \fnode{電池狀態改變(例:拔掉充電器)} \fnode{EC 韌體更新內部暫存器,拉起 SCI} \fnode{GPE 0x17 status 置 1,CPU 收到中斷} \fnodel{EC driver}{送 query 命令,EC 回覆 0x17} \fnodel{Interpreter}{執行 \_SB.EC0.\_Q17} \fnode[flownodealt]{Notify (\_SB.BAT0, 0x80)} \fnodel{事件派送}{OS 電池 driver 收到通知} \fnodel{回頭查詢}{driver 呼叫 \_BST 讀最新狀態} \fnode{更新電量圖示/通知 userspace} \end{vflow} 看清楚金色那站的位置:Notify 是整條鏈的「轉運站」, 上游是硬體與 GPE 機制,下游是 OS 的事件派送。 AML 在其中只負責兩小段(\code{\_Q17} 與 \code{\_BST}), 但這兩小段寫錯,整條鏈就斷。 \section{在 Linux 上觀察 Notify} 除錯 Notify 最好的工具是「看得到事件流」。Linux 提供好幾個觀察點, 從 GPE 計數器到 userspace 事件一應俱全: \begin{shell}[title={觀察 GPE 觸發與 ACPI 事件}]\) cat /sys/firmware/acpi/interrupts/sci 1337 \(cat /sys/firmware/acpi/interrupts/gpe17 42 enabled unmasked # acpid ships the acpi_listen tool: events in real time\) acpi_listen battery PNP0C0A:00 00000080 00000001 ac_adapter ACPI0003:00 00000080 00000000 button/lid PNP0C0D:00 00000080 00000001

每行四欄:事件類別、裝置( 加 instance)、Notify 代碼、事件資料。拔插電源線、開闔上蓋, 就能看到對應的 0x80 事件即時滾出來; 對照 計數器的增長,整條鏈的每一站都可驗證。 若懷疑「GPE 有觸發但沒有事件」,比對這兩個數字是第一步 ——計數器動了但 acpi_listen 沒動靜,問題就出在 AML 這段。

核心把 ACPI 事件透過 netlink socket 廣播給 userspace, acpid 只是最常見的訂閱者(另一路是桌面環境經由 udev/D-Bus 接手)。 古早的 介面已淘汰。除錯時記住分層: dmesg 看核心與 AML 層、 看 netlink 層、 桌面行為(如自動睡眠)是最上層——一層層對,很快就能定位斷點。 Module 10 會把這套除錯流程展開成完整章節。

  • 事件鏈五大站:硬體 \(\to\) GPE/SCI \(\to\) handler method (//)\(\to\) Notify \(\to\) OS driver。

  • 是 level(跑完才清 status)、 是 edge(先清再跑),字首必須與硬體觸發特性一致。

  • Notify 目標限 Device/ThermalZone/Processor; 代碼 0x00–0x0D 是標準事件,0x80 起才是裝置自訂, 同一代碼在不同裝置類別意義不同。

  • Notify 不帶資料,OS 收到後回頭呼叫查詢方法(pull 模型) ——所以 要輕薄,查詢方法要唯讀。

  • Linux 觀察三件套:GPE 計數器、dmesg、。

  1. 依序說出電池事件鏈的九站,並標出哪兩站是 AML 負責的。

  2. 與 的差別是什麼? 若把 EC 這種 level 事件源錯寫成 edge handler, 會出現什麼症狀、為什麼難查?

  3. 查表回答:電池狀態改變、電池被拔走、使用者按下退出鍵, 分別該用哪個 Notify 代碼?

  4. Notify 為什麼設計成不帶資料?如果讓它夾帶完整的電池狀態, 會產生哪些新問題(提示:事件排隊時狀態又變了怎麼辦)?

  5. 動手:在一台 Linux 筆電上執行 , 拔插電源線並開闔上蓋各一次,記下每一行輸出; 再用 前後對照,驗證 SCI 計數的變化。