之前到各個地方去聊到OpenBMC會是一個適用於各個產品的codebase,都會收到大家以困惑的表情回饋給我。停頓了0.99秒之後,我就會立刻反應過來,其中一個讓人難以認同的關鍵原因在於——即使服務的運算邏輯或方法可以共用,不同產品之間的「參數」卻往往完全不同。如果這些參數不能 hard code/ifdef 在程式裡,那到底該怎麼設定?差異這麼多,難道不會讓程式碼變得支離破碎嗎? 這些不可避免的產品差異,究竟要如何在程式設計上被妥善收斂,而不是一路擴散成維護上的負擔?
這個質疑非常合理。如果我們還用傳統的嵌入式開發思維,把所有東西都寫死在程式碼裡,那 OpenBMC 充其量只是一堆零散函式庫的集合,稱不上是一個「框架」。
但 OpenBMC 的核心價值,恰恰就在於解決這個問題。它的設計哲學是「策略與實作分離」。今天,我就以風扇控制服務phosphor-pid-control 為例,和大家聊聊,程式碼裡的差異性是如何「收斂」的,以及我們該如何優雅地管理不同產品間的設定。「策略與實作分離」
第一層收斂:用 JSON 定義「硬體拓撲」與「散熱策略」
首先,最核心的差異——也就是每個產品的硬體配置和 PID 控制參數——是完全從主程式中抽離的。phosphor-pid-control 在啟動時,tryRestartControlLoops()會去尋找一個名為 config.json 的設定檔。
這個 JSON 檔案,就是一份「系統散熱藍圖」。它用結構化的方式描述了:
- 有哪些感測器 (Sensors):定義了溫度感測器的名稱、在 DBus 上的路徑、讀取方式(主動監聽或被動讀取)、甚至超時時間。
- 有哪些散熱區域 (Zones):將感測器和風扇進行分組。例如,CPU 區域可能由
CPU_Temp和DIMM_Temp感測器觸發,並控制FAN_0和FAN_1。機箱區域則可能由Ambient_Temp控制所有的風扇。 - 每個區域的控制演算法 (PIDs):這就是策略的核心。你可以為每個區域定義一或多個 PID 控制器。每個控制器可以有不同的輸入感測器、不同的 PID 參數(P、I、D 係數)、不同的目標溫度 (Setpoint),以及不同的風扇轉速輸出範圍。
這意味著,我們的 A 產品和 B 產品,就算散熱設計截然不同,但它們可以執行完全相同的 phosphor-pid-control 執行檔。我們所要做的,僅僅是在各自的韌體映像檔中,提供一份專屬於它們的 config.json。
第二層收斂:用「介面」抽象化「硬體互動」
好,現在我們把硬體配置和參數抽離了。但下一個問題來了:讀取感測器的方式可能不同。有些感測器是透過 hwmon 的 sysfs 檔案節點讀取,有些則是其他 DBus 服務提供的屬性。寫入風扇 PWM 的方式也可能不同。難道這些都要在主邏輯裡用 if-else 來判斷嗎?(NO~~~
在 phosphor-pid-control 的程式碼裡,你會看到一個關鍵檔案 interfaces.hpp。它定義了兩個非常重要的抽象基礎類別 (Abstract Base Class):
ReadInterface:它只定義了一個純虛擬函式read()。任何東西只要能提供一個溫度讀值,就可以繼承它。WriteInterface:它定義了write()。任何東西只要能接收一個值來設定轉速,就可以繼承它。
PID 演算法的核心邏輯,操作的對象從來不是一個具體的「DBus 感測器」或「Sysfs 風扇」,而僅僅是 ReadInterface 和 WriteInterface。這就像一份合約:PID 演算法說:「我保證我只會呼叫 read() 和 write()」,而感測器實作類別則說:「我保證我會提供 read() 和 write() 的具體實作」。
所以,當我們在 JSON 中定義一個感測器類型是 dbus 時,工廠函式就會建立一個 DbusSensor 物件;當類型是 sysfs 時,就建立一個 SysfsSensor 物件。但對 PID 迴圈來說,它們都只是 ReadInterface,一視同仁。
這帶來了巨大的擴充性。如果明天我們有一個新的感測器是透過 I2C 讀值的,我們需要做什麼?不是去修改 PID 主迴圈,而是去新增一個 I2cSensor 類別,實作那個 read() 介面。核心程式碼,完全不用動。這就是第二層收斂,它將「如何做事」的細節完美地封裝了起來。
小番外篇:interfaces.hpp 可說是風扇控制的靈魂
SOLID 是由 Robert C. Martin (Uncle Bob) 提出的五大物件導向設計原則的縮寫。這些原則的目的只有一個:對抗軟體腐化。所謂的軟體腐化,指的就是程式碼隨著時間推移,變得難以測試、牽一髮而動全身(僵化)、以及無法重用(黏滯)。
interfaces.hpp 這個檔案可以說是 phosphor-pid-control 專案的靈魂,它完美體現了「依賴反轉原則 (Dependency Inversion Principle)」和「介面隔離原則 (Interface Segregation Principle)」。
依賴反轉原則 (Dependency Inversion Principle, DIP)
——「高層模組不應該依賴低層模組,兩者都應該依賴於抽象。」
在傳統的嵌入式開發思維中,我們很容易寫出直觀但耦合度極高的程式碼。例如,PID 控制演算法(高層策略)需要讀取溫度,我們可能會直接在演算法裡呼叫 read_i2c_register(...) 或 dbus_get_property(...)(低層機制)。
這種寫法看似直接,卻埋下了巨大的隱患:PID 演算法被「綁死」在特定的硬體或通訊協定上了。
- 如果你想在另一個使用 SPI 介面讀取溫度的平台上重用這套 PID 演算法?做不到,因為程式碼裡寫死了 I2C。
- 如果你想在電腦上跑單元測試,驗證 PID 邏輯是否正確?做不到,因為你的電腦沒有那個 I2C 暫存器。
這就是 DIP 要解決的問題。在 interfaces.hpp 中,我們定義了 ReadInterface。
class ReadInterface {
virtual ReadReturn read(void) = 0;
};
這就是所謂的「抽象」。現在,PID 演算法不再依賴「I2C」或「DBus」,它只依賴 ReadInterface。
- PID 演算法:「不管你是誰,只要你能實作
read()給我數值就好。」 - 具體的感測器(如
DbusPassiveSensor):「我實作了ReadInterface,我負責去 DBus 撈資料。」
依賴關係被反轉了。原本是「演算法」依賴「硬體」,現在是「演算法」與「硬體」都依賴於 interfaces.hpp 定義的這個介面。這讓 phosphor-pid-control 能夠以同一套邏輯核心,透過抽換不同的 ReadInterface 實作,輕鬆適應 OpenBMC 支援的成千上萬種硬體平台。
介面隔離原則 (Interface Segregation Principle, ISP)
——「客戶端不應該被迫依賴它們不使用的方法。」
ISP 強調的是介面的純粹性與精確性。試想一下,如果我們為了圖方便,定義了一個包山包海的 HardwareDevice 介面:
// 錯誤示範:臃腫的介面
class HardwareDevice {
virtual double readTemperature() = 0;
virtual void setFanSpeed(double pwm) = 0;
};
對於一個單純的「溫度感測器」來說,它只負責讀值,根本無法設定轉速。但因為繼承了這個臃腫的介面,它被迫要實作 setFanSpeed(),可能只能在裡面留空或拋出例外。這不僅讓程式碼變得醜陋,更讓呼叫者感到困惑:「為什麼我可以對溫度計設定轉速?」
回到 interfaces.hpp,你會發現設計者非常刻意地將讀取與寫入拆分開來:
ReadInterface:專注於「讀取」。任何能產生數值的東西(溫度計、電壓計、功率計)都實作它。WriteInterface:專注於「寫入」。任何能接受控制訊號的東西(風扇 PWM、水泵轉速)都實作它。
這就是 ISP 的精髓。PID 控制器在蒐集輸入數據時,只索取 ReadInterface;在輸出控制訊號時,只索取 WriteInterface。這種設計讓系統中的每個組件都保持輕量、職責單一,且互不干擾。回到 interfaces.hpp,你會發現設計者非常刻意地將讀取與寫入拆分開來:
ReadInterface:專注於「讀取」。任何能產生數值的東西(溫度計、電壓計、功率計)都實作它。WriteInterface:專注於「寫入」。任何能接受控制訊號的東西(風扇 PWM、水泵轉速)都實作它。
這就是 ISP 的精髓。PID 控制器在蒐集輸入數據時,只索取 ReadInterface;在輸出控制訊號時,只索取 WriteInterface。這種設計讓系統中的每個組件都保持輕量、職責單一,且互不干擾。
以上這些設計帶來了三大好處:
- 可擴充性 (Extensibility):當需要支援新的感測器或風扇控制器時,我們只需要新增一個實作這些介面的新類別,而完全不需要修改核心的 PID 演算法。
- 可測試性 (Testability):在進行單元測試時,我們可以輕易地建立一個「假的」(Mock) 感測器或風扇物件來模擬各種硬體行為(例如,溫度急遽上升、感測器讀取失敗),從而驗證 PID 演算法的反應是否正確。
- 可維護性 (Maintainability):程式碼的職責被清晰地劃分開來。負責 PID 演算法的工程師和負責硬體驅動的工程師可以專注於各自的領域,降低了程式碼的耦合度。
所以接下來閱讀 interfaces.hpp 時,請不要只把它看作是一堆 C++ 的 class 和 struct 定義。它是整個 phosphor-pid-control 專案的架構樞紐。














