--- tags: I/O, System Software --- # PCI/PCIe(2): 電源管理 ## 基本概念 電源管理是一種以效能降低為代價,允許將設備置於低功耗狀態來節省能源的功能。一般來說,當裝置利用很低或是完全不活動時,就會進入低功耗狀態。當需要再次使用該裝置時,則再將其恢復到 active 的模式(全速/全功率狀態)。 有兩種主要的方式可以將 PCI 裝置設置成低功耗模式: 一種是相容於 [PCI Bus Power Management Interface Specification](https://pcisig.com/specifications/conventional/pci_bus_power_management_interface/) 標準定義的 device capabilities,此方式通稱 Native PCI PM;另一種則是由 platform firmware 提供特殊的省電方式(e.g. ACPI BIOS)以被 kernel 所使用。 支援 Native PCI PM 的裝置通常可以產生稱為 Power management event (PME) 的喚醒訊號,這讓 kernel 得以認知到外部事件,以得知調整裝置電源活動為 active 狀態的需求。換句話說,收到 PME 後,kernel 應會對應裝置置於全速/全功率狀態。 然而,PCI PM 標準並沒有規範將 PME 從裝置傳送到 CPU 和作業系統的方法。因此,假設是由 platform firmware 執行此任務,即使 PCI 裝置有被設定以產生 PME,也可能同時需要準備 platform firmware 以通知 CPU 來自裝置的 PME(例如產生中斷)。 反過來說,如果 platform firmware 提供的方法被用於改變設備的電源狀態,通常 platform 還會提供使裝置產生喚醒訊號的方法。然而在這種情況下,經常還是需要為裝置使用 Native PCI PM 機制產生 PME 做好準備,因為 platform 提供的方法依賴於此。 總結來說,PCI PM 和依賴於 platform 的電源管理方式很可能需要被同步使用以達目的。 ### Native PCI PM PCI PM 規範的電源管理實作對於傳統 PCI 裝置是可選的,但對於 PCI Express(PCIe) 裝置是強制性的。如果裝置支援 PCI PM 規範,則其 PCI configuration space 中具有 8 bytes 的電源管理功能欄位,以描述和控制與 Native PCI PM 相關的標準功能。 PCI PM 規格定義了裝置(D0-D3)和 bus(B0-B3)的 4 種電源狀態。數字越高,在該狀態下消耗的功率越少。但是也代表返回 active 狀態(D0/B0)的延遲時間相應越長。此外,規格中定義了兩種不同的 D3 狀態。一個是 D3hot,這是可以透過軟體進入的 D3。另一種狀態是 D3cold,是 PCI 設備在電源電壓 (Vcc) 移除時所處的狀態。注意到在本文撰寫時 Linux 並不支持 Bus 的電源管理,因此後續將著重在裝置部分。 無論是否遵從標準,任何裝置應該都可以是 D0 或 D3cold。而支持 PCI PM 則至少必要支援 D3hot 和 D0,D1 和 D2 是屬可選項目。 支援 PCI PM 規範的 PCI 裝置可以透過軟體進入任何支援的低功耗狀態(D3cold 除外)。在 D1-D3hot 中,裝置的配置暫存器必須可由軟體存取,儘管 I/O 和其記憶體空間被停用。因為這樣才可允許裝置以軟體方式回到 D0。裝置可能經歷的電源狀態轉換如下: | Current | New | | --------- |:---------- | | D0 | D1, D2, D3 | | D1 | D2,D3 | | D2 | D2,D3 | | D1,D2, D3 | D0 | :::info 即 D-state 允許進入更深層的省電狀態,但回復時必須直接返回 D0 ::: 支援 PCI PM 規範的 PCI 裝置可以透過軟體在任何電源狀態 (D0-D3) 下產生 PME,但並不強制要求從所有支援的電源狀態都要允許產生 PME。 ### ACPI Device Power Management 基於 platform firmware 的 PCI 裝置電源管理的是特定於系統的。但是,一種常見的作法是遵循 Advanced Configuration and Power Interface (ACPI) 規範,後者為大多數基於 x86 的系統所符合。 在這種方法下,ACPI BIOS 提供了稱為 "control methods" 的特殊功能,這些功能可以由核心執行以完成特定任務,例如將裝置置於低功耗狀態。 這些 control methods 使用稱為 ACPI Machine Language (AML) 的 bytecode language 進行編碼,並儲存在機器的 BIOS 中。Kernel 可以從 BIOS 載入它們,並在需要使用時藉由 AML interpreter 執行之。則 interpreter 將可以透過 AML 進行計算、記憶體操作或存取 I/O 空間。 ACPI control method 可分為不綁定特定裝置的 global control method 和為每個裝置單獨定義的 device control method。後者也就意味著 device control method 只能用於處理 BIOS 編寫者事先預期的裝置。 ACPI 規範假設設備可以處於 D0、D1、D2 和 D3 的四種電源狀態之一,這些狀態大致對應於 Native PCI PM D0-D3 狀態(但沒有考慮 D3hot 和 D3cold 之間的差異)。對於裝置的每個電源狀態,必須啟用一組對應電源資源才能進入。這些電源資源將藉助它們自己單獨的 control method *_ON* 和 *_OFF* 進行控制。 若要將裝置設定成 ACPI 電源狀態 Dx(x = {0, 1, 2, 3}),核心應該進行事項如下: 1. 使用 *_ON* control method 啟用處於此狀態的裝置所需的電源資源 2. (Optional)如果裝置要為從低功耗狀態 (D1-D3) 離開產生喚醒訊號,則執行 *_DSW*(或 *_PSW*)control method 3. 執行為裝置定義的 *_PSx* control method 裝置的電源狀態經常會在睡眠狀態或返回工作狀態期間發生變化。 ACPI 定義了四種系統睡眠狀態 S1、S2、S3 和 S4,數字越大表示睡眠越深且越省電。S0 為 active 狀態。核心應可透過執行設備的 *_SxD* control method(x= {0,1,2,3,4}) 進入對應睡眠。如果裝置反過來需要將系統從目標睡眠狀態喚醒,應該使用裝置的 *_SxW* control method。還可以使用設備的 *_PRW* control method 來了解需要啟用哪些電源資源才能使裝置能夠產生喚醒訊號。 ### Wakeup Signaling 如果 PCI 裝置產生喚醒訊號,無論是以 Native PCI PME,或是在將裝置置於低功耗狀態之前執行 *_DSW*(或 *_PSW*)ACPI control method 的形式,都必須被捕獲並適當處理。如果這些訊號在系統處於工作狀態(ACPI S0)時發送,則應將它們轉換為中斷,以便核心可以為生成它們的裝置處理觸發的事件; 如果它們在系統睡眠時發送,則應該使其觸發系統核心邏輯的喚醒。 #### ACPI 在基於 ACPI 的系統上,PCI 裝置發送的喚醒訊號被轉換為 ACPI General-Purpose Events(GPE),這些事件是來自硬體的訊號,以產生需被回應並執行的各種事件。每個 GPE 都與一個或多個潛在事件的來源相關。具體來說,GPE 可以與能夠發出喚醒訊號的 PCI 設備相關聯。 在 ACPI BIOS 中記錄著 GPE 和事件來源之間的關係資訊,因此核心可以從中讀取這些訊息。 當系統休眠時(ACPI S1-S4),GPE 可能會被觸發,在這種情況下,系統喚醒由其核心邏輯啟動,且在稍後識別導致系統喚醒的信號之設備來源。在這種情況下使用的 GPE 稱為 wakeup GPEs。當系統處於工作狀態(ACPI S0)時,GPE 也會被觸發。在這種情況下,系統的核心邏輯會產生 System Control Interrup(SCI)來通知 kernel 事件發生。然後,SCI handler 將識別導致中斷產生的 GPE 為何,藉此 kernel 可以識別事件來源。用於在系統處於工作狀態時通知事件發生的 GPE 稱為 runtime GPEs。 #### Non-ACPI 在非基於 ACPI 的系統上,沒有標準的方法來處理傳統 PCI 設備發送的喚醒訊號,但有一種用於 PCIe 的方法。PCIe 規範引入了一種機制,可用於將 native PCI PME 轉換為 root port interrupt。原因是對於傳統的 PCI 設備,native PME 是 out-of-band 的訊號,因此它們是獨立路徑的,不需要經過橋接器;但對於 PCIe 設備,它們是 in-band 訊號,因此訊息必須通過 PCIe 的結構,包括從裝置到 Root complex 路徑上的 root port。。因此,可以引入一種機制,每當 root port 從其下面的設備之一接收到 PME 時,root port 就會產生中斷。然後,發送 PME 訊息裝置的 PCIe Requester ID 被記錄在 root port 的暫存器中,則 interrupt handler 可以從該暫存器讀取該 ID 從而識別裝置。 整合在 Root complex 的 PCIe endpoints 發送的 PME 訊息不會通過 root port,而是導致 Root complex event controller 生成中斷訊號。 原則上,本機 PCIe PME 訊號可以搭配 GPE 一起用於基於 ACPI 的系統。但要這樣使用,核心必須要求系統的 ACPI BIOS 釋放對 root port configuration 暫存器的控制。若非如此,ACPI BIOS 將不允許 kernel 控制或修改暫存器內容,進而核心無法使用 Native PCIe PME。 ## 驅動裝置介面 ### 資料結構 在 Linux 中,在裝置電源管理的核心層(PM core)和 PCI 裝置驅動程式之間提供了一個中間層。具體來說是 PCI 子系統的 `struct bus_type pci_bus_type; pci_bus_type->pm` 欄位指向的 `struct dev_pm_ops pci_dev_pm_ops`,後者就包含多個裝置電源管理相關的 callback 函式。舉例來說: ```cpp const struct dev_pm_ops pci_dev_pm_ops = { .prepare = pci_pm_prepare, .complete = pci_pm_complete, .suspend = pci_pm_suspend, .resume = pci_pm_resume, .freeze = pci_pm_freeze, .thaw = pci_pm_thaw, .poweroff = pci_pm_poweroff, .restore = pci_pm_restore, .suspend_noirq = pci_pm_suspend_noirq, .resume_noirq = pci_pm_resume_noirq, .freeze_noirq = pci_pm_freeze_noirq, .thaw_noirq = pci_pm_thaw_noirq, .poweroff_noirq = pci_pm_poweroff_noirq, .restore_noirq = pci_pm_restore_noirq, .runtime_suspend = pci_pm_runtime_suspend, .runtime_resume = pci_pm_runtime_resume, .runtime_idle = pci_pm_runtime_idle, }; ``` 這些 callback 會由 PM core 在裝置電源管理的各種情況下執行,每個 callback 詳細的作用會在後續討論。 而 PCI 裝置是由結構 `struct pci_dev` 描述,這也包含這些 callback 操作可能會用到的幾個欄位: ```cpp struct pci_dev { ... pci_power_t current_state; /* Current operating state. */ int pm_cap; /* PM capability offset in the configuration space */ unsigned int pme_support:5; /* Bitmask of states from which PME# can be generated */ unsigned int pme_poll:1; /* Poll device's PME status bit */ unsigned int d1_support:1; /* Low power state D1 is supported */ unsigned int d2_support:1; /* Low power state D2 is supported */ unsigned int no_d1d2:1; /* D1 and D2 are forbidden */ unsigned int wakeup_prepared:1; /* Device prepared for wake up */ unsigned int d3hot_delay; /* D3hot->D0 transition time in ms */ ... }; ``` ### 初始化 PCI Subsystem 在初始化時將設定好 `struct pci_dev` 欄位,以準備日後進行電源管理所需資料。其中相關的兩個函數是 `pci_pm_init()` 和 `platform_pci_wakeup_init()`。 `pci_pm_init()` 將檢查裝置是否支援 Native PCI PM,如果支援,則將 standard configuration space 中電源管理的 capability 結構位址儲存在 `pci_dev->pm_cap` 欄位中。接下來會檢查裝置支援哪些 PCI 狀態以及裝置在哪些狀態可以產生 PME,同樣調整 `pci_dev` 底下的相應欄位。 `platform_pci_wakeup_init()` 檢查裝置是否可以在 platform firmware(如 ACPI BIOS)的幫助下發出喚醒訊號。更新 `pci_dev` 中相應欄位以使用韌體提供的方法來避免裝置發出喚醒訊號。 ## Callbacks PCI 裝置驅動程式可以透過提供 callback 來允許 PCI 子系統的電源管理將其參與到電源管理的流程之中。目前進行的方式可分兩種,一種是透過提供 `dev_pm_ops`;另一種為 legacy 方式,其使用 `struct pci_driver` 中的 `.suspend()` 和 `.resume()` callback 進行處理。Legacy 方法不允許定義 runtime 電源管理,也不適合任何新的驅動程式,原則上是不推薦使用的方法。也因此本文只會專注在新的電源管理方式。 只要 PCI 裝置驅動程式定義底下的 `struct dev_pm_ops` 使其非 NULL,`struct pci_driver` 中的 legacy PM callback 將被忽略(即使它們不是 NULL)。否則的話,就當成是 legacy 方式進行處理。 以下將針對新的 PCI PM 機制的各種 callback 進行說明。 ### `prepare()` `prepare()` 會使用的時間點是: * suspend * hibernation(建立 image 前) * hibernation(power off 前) * restore(image 剛載入到記憶體時) 原則上只有只有當驅動程式的裝置具有通常可以隨時註冊的子裝置時,才需要提供此 callback。在這種情況下,`prepare()` 的作用是防止註冊設備的新子設備,直到執行 `resume_noirq()`、`thaw_noirq()` 或 `restore_noirq()` 其中之一。 除此之外,`prepare()` 可能會執行一些設備 suspend 前的準備操作,但注意到它不應該分配記憶體 :::info 如果需要額外的記憶體來 suspend,例如透過 [Suspend/Hibernate notifier](https://docs.kernel.org/driver-api/pm/notifiers.html) ::: ### `suspend()` 此 callback 只有在 suspend 會用到,在 `prepare()` 後運行。 `suspend()` 會使裝置暫停以準備好由 PCI subsystem 將其置於低功耗狀態。原則上不需在 PCI 驅動程式的 `suspend()` 中保存 standard configuration 暫存器、準備喚醒機制和將其置於低功耗狀態。所有這些操作都會由 PCI subsystem 處理。 在極少數情況下,我們會想在 PCI 驅動程式中執行這些操作而非 PCI 子系統。這時應分別使用 `pci_save_state()`、`pci_prepare_to_sleep()` 和 `pci_set_power_state()`,分別進行: 1. 保存裝置的 standard configuration 暫存器 2. 為系統喚醒做好準備 3. 將裝置置於低功耗狀態 注意到如果驅動程式一旦呼叫 `pci_save_state()`,PCI 子系統將默認由驅動程式自行處理 `pci_prepare_to_sleep()` 或 `pci_set_power_state()`。 在執行 `suspend()` 時,允許可以讓驅動程式的 interrupt handler 被觸發,以處理來自裝置的中斷。因此所有依賴驅動程式處理中斷能力的 suspend 操作都應該在此 callback 中處理(相對於 `suspend_noirq()`)。 ### `suspend_noirq()` `suspend_noirq()` 僅在 suspend 時會用到。在對系統中的所有裝置執行 `suspend()` 且 PM core 停用裝置中斷之後會執行之。 `suspend_noirq()` 和 `suspend()` 的差異在於,當 `suspend_noirq()` 運行時,不會呼叫驅動程式的中斷處理程序。因此,如果在 `suspend_noirq()` 可以執行一些在 `suspend()` 做的話可能導致 race condition 的操作。 ### `freeze()` `freeze()` 是 hibernation 相關的 callback。在兩種情況下執行: 1. 準備進入休眠,建立 system image 系統映像之前 2. 從休眠恢復,system image 加載到記憶體後 `freeze()` 的作用實際上類似於 `suspend()`。對 driver 來說,只有在需由驅動程式負責將裝置設置於低功耗狀態的極少數情況下,它們才需要在實作上有所不同。 不過要留意在 `freeze()` 不準備裝置喚醒或將其置於低功耗狀態的一般情況下,它和 `freeze_noirq()` 仍都應該使用 `pci_save_state()` 來保存裝置的 stadard configuration 暫存器。 ### `freeze_noirq()` `freeze_noirq()` 是 hibernation 相關的 callback。執行情況為: 1. 準備進入休眠,建立 system image 系統映像之前 2. 從休眠恢復,system image 加載到記憶體後 這個 callback 會在 PM core 禁用裝置中斷後執行。 其作用也類似於 `suspend_noirq()`,且一般來說很少需要實作 `freeze_noirq()`。 ### `poweroff()` `poweroff()` 是 hibernation 相關的 callback。在系統將 system image 儲存到持久性儲存裝置後執行。 `poweroff()` 的作用很類似於 `suspend()` 和 `freeze()`,關鍵的差異是其不需要保存 standard configuration 暫存器的內容。 如果有驅動程式本身想要將裝置置於低功耗狀態,而不是藉 PCI 子系統協助的特例。則 `poweroff()` 應使用 `pci_prepare_to_sleep()` 和 `pci_set_power_state()` 來準備裝置的喚醒並將其置於低功耗狀態,但不需要保存 standard configuration 暫存器。 :::warning 上述特例中,雖然不需要 `pci_save_state()` 底下保存 standard configuration 暫存器的功能,但是否還是需要其中將 `pci_dev->state_saved` 設為 true 的功能? 因為若不這麼做,PCI 子系統還是會去呼叫 `pci_prepare_to_sleep()` 和 `pci_set_power_state()`? ::: ### `poweroff_noirq()` `poweroff_noirq()` 是 hibernation 相關的 callback。在系統將 system image 儲存到持久性儲存裝置後執行。這個 callback 會在 PM core 禁用裝置中斷後執行。 其作用類似於上述的 `suspend_noirq()` 和 `freeze_noirq()`,但差異是不需要保存裝置暫存器。`poweroff_noirq()` 和 `poweroff()` 之間的差異類似於 `suspend_noirq()` 和 `suspend()` 之間的差異。 ### `resume_noirq()` `resume_noirq()` 僅在準備 resume、PM core 啟用 nonboot CPU 後執行。當 `resume_noirq()` 運行時,驅動程式對應裝置的 interrupt handler 不會被觸發,因此此 callback 可以執行可能與 interrupt handler 產生 race 的操作。 由於 PCI 子系統在會在 `resume_noirq()` 運行以前無條件地將所有裝置置於 full power 狀態,並還原其 standard configuration 暫存器,因此通常不需要特地實作 `resume_noirq()`。一般來說,它應該只用於執行如果由 `resume()` 執行會導致 race 的邏輯。 ### `resume()` `resume()` 僅在需要 resume 時執行。 其負責恢復裝置到前一次 suspend 之前配置,重返至全速狀態。在 `resume()` 返回後,裝置就應該能夠以正常的方式處理 I/O 需求。 ### `thaw_noirq()` `thaw_noirq()` 是專屬 hibernation 時使用的,會使用的狀況有 1. 建立 system image 且 PM core 啟用 nonboot CPU 後 2. 系統 restore 時載入 image 失敗,啟用非開機 CPU 後 當 `thaw_noirq()` 運作時,不會呼叫驅動程式的 interrupt handler。 `thaw_noirq()` 的作用類似於 `resume_noirq()` 的作用。差別在於 `thaw_noirq()` 一般情況下不需要修改裝置暫存器的內容。 ### `thaw()` `thaw()` 是專屬 hibernation 時使用的,接著 `thaw_noirq()` 被呼叫。此 callback 將恢復裝置原本的凍結狀況,以便在 `thaw()` 返回後可以正常工作。 ### `restore_noirq()` `restore_noirq()` 是特定於 hibernation 使用。會在 boot kernel 已將控制權傳遞給 image kernel 並且 nonboot CPU 已由 image kenrl 的 PM core 啟用後運行。 其類似於 `resume_noirq()`,對於絕大多數 PCI 裝置驅動程式來說,`resume_noirq()` 和 `restore_noirq()` 之間沒有區別。 ### `restore()` `restore_noirq()` 是特定於 hibernation 使用。接著 `restore()` 被呼叫。 其類似 `resume()`,就像 `restore_noirq()` 類似 `resume_noirq()` 一樣。即對於絕大多數 PCI 裝置驅動程式來說,`resume()` 和 `restore()` 之間沒有區別。 ### `complete()` `complete()` 會在以下幾種情況使用上: 1. resume 的 `resume()` 階段之後 2. hibernation 時,儲存 system image 前,`thaw()` 執行之後 3. restore 時,系統回到休眠前的狀態,`restore()` 所有的裝置後 4. image 載入到記憶體失敗,boot kernel 中具有驅動程式的所有裝置執行 `thaw()` 之後 這 callback 原則上並非必要,但與 `prepare()` 對應,如果有在 `prepare()` 相對需要反轉的操作則可能需要。 ### `runtime_suspend()` `runtime_suspend()` 專用於 runtime PM。當裝置即將進行 runtime suspend 時(暫停並進入低功耗狀態),PM core 將使用之。 ### `runtime_resume()` `runtime_resume()` 專用於 runtime PM。當裝置即將進行 runtime resume 時(復原 I/O 運作並進入 full power 狀態),PM core 將使用之。 ### `runtime_idle()` `runtime_idle()` 專用於 runtime PM。在需要根據 PM core 的資訊判斷是否 suspend 時就會執行之。舉例來說,它會在 `runtime_resume()` 返回後立即自動執行,以防裝置只是因一些虛假事件而 resume。 ### Others - [ ] _early - [ ] _late ## Helper Macros 雖然前述的每個 callback 都可以由各自獨立的函數定義,但若求方便,將 `struct dev_pm_ops` 中的多個函式指標指向同一個實作也是允許的。而 kernel 中也提供一些 macros 可用於此目的。 ### `SIMPLE_DEV_PM_OPS` `SIMPLE_DEV_PM_OPS(name, suspend_fn, resume_fn)` macro 用來宣告一個 `struct dev_pm_ops`,其中給定的 `suspend_fn` 將同時由 `.suspend()`、`.freeze()` 和 `.poweroff()` 指向,而 `resume_fn` 則由由 `.resume()`、`.thaw()` 和 `.restore()`。其他 `dev_pm_ops` 則未設定。 ### `UNIVERSAL_DEV_PM_OPS` `UNIVERSAL_DEV_PM_OPS` 與 `SIMPLE_DEV_PM_OPS` 類似,但它另外將 `.runtime_resume()` 指標設定為與 `.resume()` 相同的值,並將 `.runtime_suspend()` 設定為 `.suspend()` 相同的值。 ### `SET_SYSTEM_SLEEP_PM_OPS` `SET_SYSTEM_SLEEP_PM_OPS` 可以用在 `struct dev_pm_ops` 的內部宣告,以將 `.suspend()`、.`freeze()` 和 `.poweroff()` 指向同一個函式,並且將另一個函由 `.resume()`、`.thaw ()` 和 `.restore()` 指向。 換句話說 `SIMPLE_DEV_PM_OPS` 底下實作即是由 `SET_SYSTEM_SLEEP_PM_OPS` 再展開。 ## Flags PM core 允許裝置驅動程式設定特殊的 flags,以影響 PM core 以及 PCI bus 相關的中間層程式邏輯對電源管理的處理方式。具體的使用方式是在驅動程式的 probe 階段時藉 `dev_pm_set_driver_flags()` 設定一次,而且這之後不應再直接對其更新。 ### `DPM_FLAG_NO_DIRECT_COMPLETE` 防止 PM core 使用 direct-complete 機制。即如果 suspend 啟動時,裝置處於 runtime suspend 狀態,則允許跳過原本 suspend 時該做的 `suspend()`、`suspend_noirq()`。注意對一個裝置設置此 flag 的話,其上級所有裝置也會受影響,因此只有在絕對必要時才應使用之。 ### `DPM_FLAG_SMART_PREPARE` 一旦設置 `DPM_FLAG_SMART_PREPARE`,只有當裝置驅動程式提供的 `->prepare` 回傳正值時,從 `pci_pm_prepare()` 才會傳回正值。這意味著驅動程式可以動態選擇不使用 direct-complete (設定 `DPM_FLAG_NO_DIRECT_COMPLETE` 意味著永遠不使用)。 ### `DPM_FLAG_SMART_SUSPEND ` 這 flag 表示對驅動程式來說,裝置可以在 suspend 時安全的處於 runtime suspend 狀態。因此 `pci_pm_suspend()`、`pci_pm_freeze()` 和 `pci_pm_poweroff()` 時,若裝置為 runtime suspend 狀態,PCI core 不會從將其從中 resune(除了一些 PCI 特定原因)。此外: * 如果裝置在狀態範圍轉換的後期仍保持 runtime suspend 狀態,它會導致 `pci_pm_suspend_late/noirq()` 和 `pci_pm_poweroff_late/noirq()` 提前返回 * 如果裝置在 `pci_pm_resume_noirq()` 或 `pci_pm_restore_noirq()` 中處 runtime suspend,則其 runtime PM 狀態將變更為 active(設為 D0) ### `DPM_FLAG_MAY_SKIP_RESUME` 這 flag 表示裝置在系統將轉換到工作狀態時,仍可以保持 suspend 狀態,即驅動程式允許跳過其 noirq 和 early resume。PM core 將結合考慮此 flag 以及裝置的 `power.may_skip_resume`(在特定情況會由 `pci_pm_suspend_noirq()` 設定)。若決定跳過,`pci_pm_resume_noirq()` 和 `pci_pm_resume_early()` 將提早返回,不執行裝置驅動程式的對應 callback ## System sleep Model 規範上有幾種不同類型的電源轉換,其中的每一種都需要以特定方式對裝置進行處理。PM core 為此目的會階段性的執行對應的電源管理 callback。這些階段都在任務凍結後運行。 ### System Suspend 當系統準備進入記憶體內容會被保留的睡眠狀態類型時(例如 ACPI S1-S3),進行的階段為: 1. prepare 2. suspend 3. suspend_noirq ```cpp pci_pm_prepare() pci_pm_suspend() pci_pm_suspend_noirq() ``` `pci_pm_prepare()` 首先在 `pm_runtime_resume()` 的協助下將裝置置於 "fully functional" 狀態。然後,執行裝置驅動程式的 `pm->prepare()` callback。 `pci_pm_suspend()` 檢查裝置的驅動程式是否採用 legacy suspend callback。若是,在這種情況下,執行之並返回其結果。接下來,如果裝置的驅動程式不提供 `pm->suspend()`,則呼叫 `pci_pm_default_suspend()` 來使用預設的 suspend 流程,這會簡單地停用裝置的基礎功能(詳見 [`do_pci_disable_device`](https://elixir.bootlin.com/linux/latest/source/drivers/pci/pci.c#L2243))。如果定義 `pm->suspend()` 則執行之,並在失敗時回傳其結果。最後,如有必要,呼叫 `pci_fixup_device()` 以套用裝置在 suspend 時需處理特殊情況的方法。注意到對於 PCI 裝置,suspend 階段是非同步執行的。也就是說 `pci_pm_suspend()` callback 可以對任何一對已知不相互依賴的 PCI 裝置平行執行(從 root 到 leaf 的裝置路徑完全不重合)。 `pci_pm_suspend_noirq()` 在呼叫 `suspend_device_irqs()` 之後執行。這表示在其執行時,不會有對應裝置驅動程式的 interrupt handler 介入。首先檢查裝置的驅動程式是否採 legacy suspend callback,若是,則將執行之並回傳結果。其次,如果不提供 `pm->suspend_noirq()`,則保存裝置的 standard configuration 暫存器並回傳成功。否則,執行裝置驅動程式的 `pm->suspend_noirq()`,如果失敗則傳回其結果。最終,為裝置發出喚醒訊號做準備(如果需要),並將其置於低功耗狀態。特別留意裝置驅動程式的 callback 可能未執行保存 standard configuration 暫存器的操作,`pci_pm_suspend_noirq()` 可以判斷是否有此狀況並協助備份。 Suspend 將裝置置於的低功耗狀態是當睡眠狀態中,裝置可以發出喚醒訊號的最低功耗狀態。與 runtime PM 相同,發出信號喚醒的機制取決於 PCI subsystem 確定,後者負責為設備進行準備以從目標睡眠狀態發出喚醒信號並得以正確醒來。 一般來說,採用傳統 PM 介面的 PCI 裝置驅動程式不會為裝置做發出喚醒訊號的準備並置其於低功耗狀態。然而,如果驅動程式的 `pm->suspend()` 或 `pm->suspend_noirq()` 保存了裝置的 standard configuration 暫存器,`pci_pm_suspend_noirq()` 會假定裝置已自己準備好發出喚醒訊號並置於低功耗狀態。原則上不鼓勵 PCI 裝置驅動程式這樣做,但在某些情況下,在驅動程式中這樣做可能可以得到較理想的結果。 ### System Resume Resume 與 suspend 相對,指系統從保存記憶體內容的睡眠狀態(ACPI S1-S3)到工作狀態(ACPI S0)的轉換時。經歷階段會是: 1. resume_noirq 2. resume 3. complete ```cpp pci_pm_resume_noirq() pci_pm_resume() pci_pm_complete() ``` `pci_pm_resume_noirq()` 首先將所有裝置置於 full power 狀態、恢復其 standard configuration 暫存器,然後必要時 `pci_fixup_device()` 處理裝置相關的特殊 resume 行為。留意到這些行為無論是否使用 legacy 界面皆會進行。換句話說,無論實作方式所有 PCI 裝置此時都處於 full power 狀態,並且保證在 resume 期間 interrupt handler 第一次被呼叫時,它們的 standard configuration 暫存器已恢復。這允許核心避免仍在 suspend 的裝置之驅動程式處理共享中斷的問題。接著就判斷是採 legacy PCI 或新介面決定使用的 `pm->resume_noirq()` callback。 `pci_pm_resume()` 程式首先檢查裝置的 standard configuration 暫存器是否已恢復,如果沒有恢復則恢復它們(理論上這僅存在 suspend 失敗的錯誤路徑中)。再來有必要的話應用與裝置相關的 `pci_fixup_device()`。接著,如果裝置的驅動程式以 legacy 實現,則執行驅動程式之並傳回其結果。否則,PCI core 將把裝置的喚醒訊號機制將關閉,並且執行 `pm->resume()` 並傳回結果。 類似於 suspend,PCI 設備的 resume 也是非同步執行的。意味著如果兩個 PCI 設備不相互依賴,則可以平行位兩者執行 `pci_pm_resume()`。 最後 `pci_pm_complete()` 將執行 `pm->complete()`(如果已定義)。 ### System Hibernation Hibernation (休眠)是相較於 suspend 更深層且更複雜的睡眠機制,因為它需要額外建立 system image 並將其寫入持久性儲存媒介(如 HDD)。image 以 atomic 方式建立,並且在此之前所有裝置都應已凍結/暫停。 凍結的前提是要能夠釋放的記憶體(至少 50% 的系統 RAM 可用)。凍結將分以下三個階段進行: 1. prepare 2. freeze 3. freeze_noirq ```cpp pci_pm_prepare() pci_pm_freeze() pci_pm_freeze_noirq() ``` `pci_pm_prepare()` 的部分與 suspend 中說明相同。 `pci_pm_freeze()` 類似 `pci_pm_suspend()`,但執行的 callback 是 `pm->freeze()`。此外這裡沒有使用 `pci_fixup_device()` 處理硬體特例。 `pci_pm_freeze_noirq()` 同樣類似於 `pci_pm_suspend_noirq()`,但呼叫 `pm->freeze_noirq()`。留意這裡並不會嘗試為裝置準備好訊號喚醒,也不會置其於低功耗狀態。但如果驅動程式的 callback 未保存裝置的 configure 暫存器,會在此刻保存它們。 凍結完成並建立好 system image 之後,接下來要做的是保存它到硬碟中。然而,矛盾的是所有裝置都被凍結因此無法處理 I/O,但保存 image 卻又需要處理 I/O。因此,這裡必須暫時再將裝置恢復到 active 狀態。在此進行以下階段: ```cpp pci_pm_thaw_noirq() pci_pm_thaw() pci_pm_complete() ``` 首先做 `pci_pm_thaw_noirq()`。類似 `pci_pm_resume_noirq()`,它將裝置恢復成 active 狀態,並恢復其 standard configuration 暫存器。接著執行裝置驅動程式的 `pm->thaw_noirq()`。 `pci_pm_thaw()` 也和 `pci_pm_resume()` 類似,但它運行裝置驅動程式的 `pm->thaw()`。 最後的 `pci_pm_complete()` 與 resume 同理。 儲存好 image 後,最後要將裝置電源關閉,然後系統才能進入目標睡眠狀態(ACPI S4)。這分三個階段完成: 1. prepare 2. poweroff 3. poweroff_noirq ```cpp pci_pm_prepare() pci_pm_poweroff() pci_pm_poweroff_noirq() ``` `pci_pm_prepare()` 的部分與 suspend 中說明相同。 `pci_pm_poweroff()` 和 `pci_pm_poweroff_noirq()` 分別與 `pci_pm_suspend()` 和 `pci_pm_suspend_noirq()` 類似,即便這並不會嘗試儲存 standard configuration 暫存器。 ### System Restore Restore 相對於 Hibernation,將 hibernation 時建立的 image 載入到記憶體中以恢復休眠前的內容,然後才能恢復休眠前的活動。 Hibernation image 由稱為 boot kernel 的新核心載入到記憶體中。Boot kernel 是由 bootlaoder 載入並執行。Boot kernel 載入 image 後,需要再將自己的程式碼和資料用 image 中儲存的 "hibernated" kernel(又稱 image kernel)來取代。為此,所有裝置都會要先像在休眠期間建立 image 一樣的狀態被凍結,即 prepare, freeze, freeze_noirq。 ```cpp pci_pm_prepare() pci_pm_freeze() pci_pm_freeze_noirq() ``` 注意到受這些階段影響的裝置只有在 boot kernel 中具有驅動程式者;其他裝置仍將處於 bootloader 所設置的任何狀態。 如果休眠前的記憶體內容恢復失敗,boot kernel 將使用上述的 thaw_noirq、thaw, and complete 執行解凍(只會影響 boot kernel 中包含的驅動程式之相關裝置),然後繼續正常運作;通常裝況應該可以成功恢復,則此時 boot kernel 會將 CPU 轉交給 image kernel,然後 image kernel 再負責將系統恢復到工作狀態。為了實現這一點,它必須恢復裝置的 hibernation 前之功能,涉及三個階段: 1. restore_noirq 2. restore 3. complete ```cpp pci_pm_restore_noirq() pci_pm_restore() pci_pm_complete() ``` 這些 callback 分別與 `pci_pm_resume_noirq()` 和 `pci_pm_resume()` 類似,但它們執行的是裝置驅動程式的 `pm->restore_noirq()` 和 `pm->restore()` 回呼(如果可用)。 `pci_pm_complete()` 則與 resume 階段所進行一致。 ## Runtime Power Management Model ### 基本概念 除了上述的 system sleep model 類型的電源管理,PCI 裝置驅動程式還以可選方式允許實作另一種電源管理機制: runtime power management(runtime PM)。 儘管非必要,但如果相關裝置有可靠方法可以驗證其未被使用的狀態,則實現之可以對省電機制帶來正向效益。舉例來說,例如當網路線從 adapter 移除或沒有裝置連接到 USB controller 時。 為了支援 PCI runtime PM,驅動程式首先需要實作`runtime_suspend()` 和 `runtime_resume()`。另外也可能需要 `runtime_idle()`,後者主要目的是防止裝置每次在`runtime_resume()` 後又立刻被再次 suspend。或者,作為替代方案 `runtime_suspend()` 必須檢查是否真的有 suspend 必要,若否則回傳 `-EAGAIN`。 原則上,Runtime PM 的啟用與否取決於 PM core,PCI 裝置驅動程式不需要也不應該嘗試啟用它。在 `pci_pm_init()` 時,`pm_runtime_forbid()` 會將 runtime PM 暫時禁止。除此之外,在執行裝置驅動程式提供的 probe 之前,每個 PCI 裝置的 runtime PM counter 都會透過 `local_pci_probe()` 遞增。如果 PCI 驅動程式實現了 runtime PM callback 並打算使用 PM core 和 PCI 子系統提供的 runtime PM framework,則需要在其 probe callback 中遞減此 runtime PM counter。否則的話,counter 將始終不為零,並且永遠不會 runtime suspend。最簡單的方法是呼叫 `pm_runtime_put_noidle()`,但如果驅動程式想要立即做 auto suspend,則可以呼叫 `pm_runtime_put_autosuspend()` 來實現此目的。 留意到驅動程式的 `runtime_suspend()` callback 可能會在 counter 遞減後立即執行,因為 userspace 可能已經透過 sysfs 導致 `pm_runtime_allow()` 解鎖裝置的 runtime PM,因此裝置驅動程式必須做好應對這種情況的準備。驅動程式本身不應該呼叫 `pm_runtime_allow()`,它應該讓 userspace 或某些平台特定的程式碼執行此操作。驅動程式所應做的是隨時準備好在 `pm_runtime_allow()` 後立即正確處理裝置的 runtime PM。這隨時可能發生,甚至在驅動程式載入之前。 當驅動程式的 remove 被執行時,它必須與 probe 階段對 runtime PM counter 的遞減做平衡。即如果它在 probe 中減少了 counter,則必須在 remove callback 中執行 `pm_runtime_get_noresume()` 來對應之。 Runtime PM 框架的工作方式是處理 suspend 或 resume 裝置的 request,或檢查它們是否 idle(即檢查隨後是否可以要求 suspend 他們)。這些 request 會放入電源管理的 workqueue `pm_wq`。儘管在某些情況下,PM request 會由 PM core 自動安插(例如,在處理 resume request 後,PM core 會自動插入檢查是否 idle 的 request),但主要還是由驅動程式負責對其裝置的電源管理 request 進行排隊。為此,他們應該使用 PM core 提供的 runtime PM helpers。此外,也可以選擇不向 `pm_wq` 發出 request 的同步 suspend/resume,這也大多是由其驅動程式完成的,同樣使用 PM core 的 helpers 達此目的。詳見 [Linux 核心設計: Power Management(2): Runtime Power Management model](https://hackmd.io/@RinHizakura/SkziZRa9a) 中的討論。 ### Driver Interface PCI 子系統在 PCI 裝置的 runtime 電源管理中起著至關重要的作用。它使用 [Runtime Power Management Framework for I/O Devices](https://docs.kernel.org/power/runtime_pm.html)中描述的通用框架來提供 runtime PM,也就是以下的幾個 callbacks: ```cpp pci_pm_runtime_suspend() pci_pm_runtime_resume() pci_pm_runtime_idle() ``` 這些 callbacks 會由 core runtime PM 執行。 此外,PCI 子系統還提供在 PCI 設備低功耗狀態下,產生 runtime 喚醒訊號時要處理之所需的機制。目前包含 Native PCI Express PME 和基於 ACPI GPE 喚醒訊號的處理。 首先,在 `pm_schedule_suspend()` 或 `pm_runtime_suspend()` 下,PCI 裝置進入低功耗/suspend 狀態,對於 PCI 設備,會呼叫 `pci_pm_runtime_suspend()` 來完成實際工作。為此,對應 PCI 裝置的驅動程式則必須提供 `pm->runtime_suspend()` 為 `pci_pm_runtime_suspend()` 運行。如果 `pm->runtime_suspend()` 成功返回,則保存裝置的標準配置暫存器,裝置準備產生喚醒訊號,最後將其置於目標低功耗狀態。 在這種機制下,裝置進入的低功耗狀態會是裝置所可以發出喚醒訊號的最低功耗狀態。發送喚醒訊號的確切方法取決於 PCI 子系統初始化時確認的 Native PCI PM 和 platforam firmware 功能決定。只要平台支援允許,PCI 子系統可以兼用兩者。 預計上。裝置驅動程式的 `pm->runtime_suspend()` 不會嘗試讓裝置準備好訊號喚醒或將主動將其置於低功耗狀態。這些任務理當由 PCI 子系統來完成。 :::warning 那麼 `pm->runtime_suspend()` 具體需要負責哪些類型的工作? 這有待釐清 ::: 使用 `pm_request_resume()` 或 `pm_runtime_resume()` 會將被 suspend 的裝置恢復成 active 狀態,這兩個函式都會使用到 `pci_pm_runtime_resume()`。同理,裝置驅動程式需提供 `pm->runtime_resume()`。注意到在執行 `pm->runtime_resume()` 之前,`pci_pm_runtime_resume()` 會將裝置帶回全功率狀態,防止裝置 resume 途中發出喚醒訊號,並且恢復其 configuratiion 暫存器。因此 `pm->runtime_resume()` 實作上可以不必擔心裝置在返回為 active 狀態的特殊情況。 一般而言,會有兩種不同的情況下呼叫 `pci_pm_runtime_resume()`。一是由裝置驅動程式的請求被主動調用,例如,有一些資料需要被對應裝置處理時。另一種情況是由於被動由裝置自身發出的喚醒訊號調用(remote wakeup)。 `pci_pm_runtime_idle()` 由 `pm_runtime_idle()` 和 `pm_request_idle()` 調用,以考慮條件是否符合以將特定裝置 suspend。一般來說,PCI subsystem 並不直接知道裝置是否可以 suspend,因此它會運行裝置驅動程式的 `pm->runtime_idle()` 來決定實際運作。如果後者回傳表示結果為正確的 0 值,則在 `pm_runtime_suspend()` 的協助下將裝置進行 suspend。 ## ASPM (Active State Power Management) [ASPM](https://en.wikipedia.org/wiki/Active_State_Power_Management) 是一種用於管理 PCIe link 的電源管理協議。作為進一步擴充 PCIe 電源管理的規格,ASPM 允許 PCIe link 可以在無需系統驅動程式的參與下進入低功耗狀態。換句話說,ASPM 是硬體自動運作的機制。 傳統的省電設計中,主機軟體必須與裝置控制器進行互動,相互協調出適合的省電設定。以 USB 為例,主機驅動程式必須告訴主機控制器停止傳送 Start of Frame(SOF) 封包,USB 裝置才能切換至低耗電模式。但由於研判何時關閉這些連結埠的程序相當複雜,主機軟體有時只有在整部個人電腦進入閒置狀態(global idle)時,才能讓 USB 匯流排暫停運作。 理想上,如果裝置是閒置狀態時,可以單獨進入省電模式,對於整體的功耗應可以帶來正向效益。而 ASPM 就是能做到接近這種理想的作法。由於不需要主機、裝置或者驅動程式的互動,就能自動控制連結狀態。因此,這種設計讓連結能在不論多短暫閒置時間也可立即切換至低耗電狀態,亦能縮短連結從低功耗復原至全速模式的延遲時間。 如圖,PCIe 定義五種電源模式: ![image](https://hackmd.io/_uploads/rk0vxpTsa.png) * L0: fully active * L0s: standby * L1: low power standby * L2: low power sleep * L3: link off 數字越大,表示越省電的狀態。 下圖展示了每個 L-state 在相關元件上的設定和開關: ![image](https://hackmd.io/_uploads/BylLI-Tpia.png) :::info 此為邏輯上的狀態機,物理層上參照 LTSSM。 ::: 由表中可知,ASPM 可以介入的地方會是在 L0 到 L0s/L1 間的狀態切換。 ### L1 PM Substate 回顧 L1 的特性,在此狀態下,除非 clock power management(使用 CLKREQ#)允許,否則所有電源和所有 reference clock 均需保持活動。PLL 可選關閉或開啟,而此時 Tx 和 Rx 將對應到關閉或 idle。Link common mode voltages 也需保持開啟。在此可看到 L1 狀態的可選活動元件的數量是受限的,即便退出的延遲很短,但功耗上可能無法滿足市場預期的要求。 另一方面,L2 將所有 clock 和供電關閉,雖然省電效果理想,但副作用是退出的時間過長,在特定場景中可能效益不高。 為了彌補 L1 和 L2 各自的不足之處,PCIe 重新定義了兩個新的 L1 substate: L1.1 和 L1.2,作為補充。如果 PCIe 連結的兩端均支援並啟用 L1 substate,則允許通過此方式建立連接;反之只能在傳統 L1 電源狀態下運作。 在 L1.1 和 1.2 下,PLL 的開關不再為可選而是直接移除,連帶 Rx/Tx 也被關閉。而 L1.2 進一步還關閉 Link common mode voltages 以節省更多電源,因此恢復時間也會比 L1.1 長。具體差異可見下表的整理: ![image](https://hackmd.io/_uploads/ryBPZGona.png) > [Using PCI Express L1 Sub-States To Minimize Power Consumption In Advanced Process Nodes](https://semiengineering.com/using-pci-express-l1-sub-states-to-minimize-power-consumption-in-advanced-process-nodes/) ## Reference * [PCI Power Management](https://docs.kernel.org/power/pci.html) * [LTSSM与电源管理](https://blog.csdn.net/u010443710/article/details/104410930) * [簡介PCI Express: Link Training and Status State Machine( LTSSM 狀態機 )](https://datongfirmware.blogspot.com/2021/03/pci-express-link-training-and-status.html) * [PCIE电源管理(Power Management)](https://www.zhihu.com/tardis/zm/art/623500018?source_id=1003) * [The Importance of Efficient SSD Power Management](https://phisonblog.com/the-importance-of-efficient-ssd-power-management-2/) * [PCIE ASPM](https://boy-asmc.blogspot.com/2009/08/pcie-aspm.html) * [Using PCI Express L1 Sub-States To Minimize Power Consumption In Advanced Process Nodes](https://semiengineering.com/using-pci-express-l1-sub-states-to-minimize-power-consumption-in-advanced-process-nodes/)