2019年2月24日 星期日

BMC的風扇控制 (Fan speed control)

這篇文章是我剛開始做FSC 功能的時候寫的,因為那時候都找不到主管要我報告的內容,所以報告完就順便整理一下,最近因為在修控制學,之後應該會再補一篇和控制學相關的內容

BMC的風扇控制算一個蠻重要的功能,那這個功能包含了"TACH"和"PWM"這兩個常用訊號怎麼解讀,還有控制演算法"PID(closed loop)"和"Stepwise(openloop)"還有Error control ,但因為每家做法不同,就不會在這邊作介紹

##實作程式碼是來自Facebook的openBMC,圖片都是手繪的


Why and how to do fan speed control ?

在伺服器中的風扇是非常高速也耗電的,根據統計在數據中心的運維成本上,電費佔了7成,並且如果讓風扇長期處在全轉狀態也會有噪音和耗損度的問題。

目前CPU可以達到單一耗電量200W以上,伴隨而來的高熱也會使個晶片老化速度加快,當然伺服器中的SSD, Nic card, E1.S等裝置也是有同樣的情況,因此我們希望能根據這些高熱的元件溫度來決定風扇的轉速,達到省電和正常運作的效果


風扇控制的做法如下:

  1. Thermal team(熱流部門) 通常會提供一個fan table,裡面會詳細描述風扇要怎麼轉
  2. BMC (controller) 會透過PWM訊號去設定風扇轉速
  3. 風扇也能透過TACH訊號來回傳目前的轉速


PWM(Pulse-Width Modulation)

PWM是透過平均電壓來傳遞類比訊號,簡單來說就是在一個cycle中,高電為占百分之多少,就表示他要傳遞的值是多少

例如在一個週期中,如果高電位占25%,低電位占75%,這樣表示我們要傳遞的值是25%,風扇就會轉25%

那如果今天我們都一直是high (高電位),這樣風扇就會全轉,因此在線路圖的位置過程中,都會注意PWM有沒有pull high,避免在BMC更新過程 或是死掉後,機器過熱

那每個周期的時間是多少呢?  PWM的傳遞頻率會定義在風扇的spec中,每一顆風扇的接收頻率有可能會不一樣


TACH (Tachometer)

我們可以透過Tach來傳遞風扇的轉速,有修過機械方面的課程就會知道,馬達會有n個機械原點,傳一圈的話會產生n個pulse,一圈會產生幾個pulse也是定義在風扇的spec中

假如今天風扇轉一圈會產生兩個pulse,我們在一秒內收到1000個pulse,這樣表示風扇一秒轉了500圈,風扇轉速是用rpm(一分鐘轉幾圈)表示的,因此500*60 rpm就是我們要求的值

TACH的訊號大概就長得像底下這樣,只要統計一秒鐘有幾個訊號,就能得到風扇的轉速

Picture provided by: http://www.angelfire.com/super/ghettoretta/m90/windowswitch.htm


System Control  

在傳統控制(Classical Control )理論中,通常會用有沒有feedback 來區分open loop control 和 closed loop control,前者通常就是我們常說的stepwise,後者就是工業控制中廣泛使用的PID control,他們的示意圖如下,接下來會分別介紹這兩個算法的的內容


Open loop control  演算法 (Stepwise)


Open loop,會根據input來直接求出output並輸出,不會參考feedback或任何的actuating error

通常thermal team會給BMC一個表(fan table),表示Sensor 溫度(input)為多少的時候,風扇轉速(output)為多少,大概如下

CPU溫度風扇轉速
6040
7050
8070
9080
10098

實作也相對簡單

# Threshold table class TTable: def __init__(self, table, neg_hyst=0.0, pos_hyst=0.0): self.table = sorted( table, key=lambda in_thr_out: in_thr_out[0], reverse=True) self.compare_fsc_value = 0 self.last_out = None self.neghyst = neg_hyst self.poshyst = pos_hyst def run(self, value, ctx): mini = 0 if value >= self.compare_fsc_value: if math.fabs(self.compare_fsc_value - value) <= self.poshyst: return self.last_out if value <= self.compare_fsc_value: if math.fabs(self.compare_fsc_value - value) <= self.neghyst: return self.last_out for (in_thr, out) in self.table: mini = out if value >= in_thr: self.compare_fsc_value = value self.last_out = out return out self.compare_fsc_value = value self.last_out = mini return mini


















PID  algorithm

PID分別是proportional(比例) , integral(積分) and derivative(微分) 三個單字的縮寫所組成的

我們在做風扇控制的時候,會希望我們的電腦上元件的溫度維持在幾度,以下會以CPU溫度為例子。假設今天我們希望CPU溫度維持在60度左右,那60就是set-point (S),而CPU目前溫度就是process value (P),那S和P之間的差值就是error value (E) ,他們之間的關係就是E=S-P

PID演算法就是拿求得的E分別做比例運算,積分和微分,如下圖所示,算出來的結果會輸出PWM去控制風扇

那我們現在來分別聊聊PID這三個算法


Proportional(比例)

比例控制就是依據當前溫度來決定風扇轉速,將求得的E乘上一個Kp常數

$$P_{out}=K_p\cdot(S-P)$$

因為當E 最大,表示P離S很遠,風扇的轉速需要變化大一點,舉例來說

Set-point: 60 Kp = -2

case 1 : CPU 溫度 70度 → E=60-70 = -10

case 2 : CPU 溫度 90度 → E=60-70 = -30

從兩個case當中,我們可以發現當 | E | 越大,風扇需要的轉速變化也要很大,所以風扇轉速分別就是20和60。


class PID: def __init__(self, setpoint, kp=0.0, ki=0.0, kd=0.0, neg_hyst=0.0, pos_hyst=0.0): self.last_error = 0 self.I = 0 self.kp = kp self.ki = ki self.kd = kd self.minval = setpoint - neg_hyst self.maxval = setpoint + pos_hyst self.last_out = None def run(self, value, ctx): dt = ctx["dt"] # don't accumulate into I term below min hysteresis if value < self.minval: self.I = 0 self.last_out = None # calculate PID values above max hysteresis if value > self.maxval: error = self.maxval - value self.I = self.I + error * dt D = (error - self.last_error) / dt out = self.kp * error + self.ki * self.I + self.kd * D self.last_out = out self.last_error = error return out # use most recently calc'd PWM value return self.last_out























Integral(積分) 

積分控制是根據歷史經驗來決定風扇轉速,將從剛開機到現在的Error value全部相加起來,在乘上Ki

$$I_{out}=K_i\cdot\sum_{k=0}^tE_k $$

E0~Ei相加的值越大表示目前的歷史經驗告訴我們風扇變化要大一點

那相加的值是不會為無限大的,因為E值是有正有負

但I 有個問題 就是overshoot ,在CPU溫度已經達到set-point的時候,風扇卻仍沒減少風量,造成CPU溫度持續下降,原因是因為sum(E)仍未趨近於0,這個問題是存在於PID演算法的,大部分的程式碼會在這個問題上做work around

黃色框框就是對overshoot所做的workaround

class PID: def __init__(self, setpoint, kp=0.0, ki=0.0, kd=0.0, neg_hyst=0.0, pos_hyst=0.0): self.last_error = 0 self.I = 0 self.kp = kp self.ki = ki self.kd = kd self.minval = setpoint - neg_hyst self.maxval = setpoint + pos_hyst self.last_out = None def run(self, value, ctx): dt = ctx["dt"] # don't accumulate into I term below min hysteresis if value < self.minval: self.I = 0 self.last_out = None # calculate PID values above max hysteresis if value > self.maxval: error = self.maxval - value self.I = self.I + error * dt D = (error - self.last_error) / dt out = self.kp * error + self.ki * self.I + self.kd * D self.last_out = out self.last_error = error return out # use most recently calc'd PWM value return self.last_out
























Derivative(微分) 

來到最後的微分,D就是求Error value的變化量,以最近一兩次的誤差變化量來決定風扇轉速

$$D_{out}=K_d\cdot\frac{E_t-E_{t-n}}{n}$$

把這次的error和上次的error做相減在除上時間差,就是D值了

程式碼大概如下:

class PID: def __init__(self, setpoint, kp=0.0, ki=0.0, kd=0.0, neg_hyst=0.0, pos_hyst=0.0): self.last_error = 0 self.I = 0 self.kp = kp self.ki = ki self.kd = kd self.minval = setpoint - neg_hyst self.maxval = setpoint + pos_hyst self.last_out = None def run(self, value, ctx): dt = ctx["dt"] # don't accumulate into I term below min hysteresis if value < self.minval: self.I = 0 self.last_out = None # calculate PID values above max hysteresis if value > self.maxval: error = self.maxval - value self.I = self.I + error * dt D = (error - self.last_error) / dt out = self.kp * error + self.ki * self.I + self.kd * D self.last_out = out self.last_error = error return out # use most recently calc'd PWM value return self.last_out















10 則留言:

  1. 這篇文章非常有趣,很值得後續作參考,但想請教您幾點問題。
    (1)kp、ki、kd是否有公式可供參考 or 此為經驗公式的常數項。
    (2)可否彙整Thermal所需要提供的資料,才能滿足PWM設定條件。

    回覆刪除
    回覆
    1. Hi Joe,
      Q1: 參數調整是根據實際系統情況去決定出來的,演算法有很多種(e.g. Ziegler–Nichols, Cohen-Coon ...)
      Q2: Fan table(哪些sensor 需要做PID及其常數值,哪些需要做stepwise)

      刪除
  2. 作者已經移除這則留言。

    回覆刪除
  3. Hi Iris,
    了解,看來是利用PID公式,進而推算kp、ki、kd。另外,如果按造您的語法,得出來的值是Delta PWM,再用此數字做加減調整,不知我理解是否有誤。

    回覆刪除
    回覆
    1. 是的,而按照PID的公式算出來的就是實際的PWM了,舉例來說
      kp -3/ ki -0.3 / kd -0.3/ setpoint 85 / CPU temp 105
      PWM = (-3)(85-105) + ki*error_history + kd*(error-pre_error)/time ~= 60~70左右(這個就是風扇轉速的百分比了)

      如果PID算出來的值是負數,我們就會給予風扇一個最小PWM(20 duty之類的)

      刪除
  4. 作者已經移除這則留言。

    回覆刪除
  5. 嘗試帶入公式,將設定T=60C;Tamb=40、Tbegin=45;W=15,Set T=60,最後PWM維持89%,結果還算合理。感謝您提供的資訊,可供後續做參考。

    回覆刪除
  6. 請問可否介紹有關 fan speed control for closed loop?

    回覆刪除
    回覆
    1. 不好意思,漏掉了留言,fan speed control for closed loop 目前伺服器中都是採用PID演算法加上一點變形

      刪除
  7. 若套用Thermal 2.0 Fan Control的機制(依照區塊溫度決定,而不是只依賴CPU),也是用同樣的演算法實現嗎?
    理論上用區塊的溫度去決定PWM的高低,在不影響效能的前提又能節省風扇的功耗
    但大廠的BMC似乎比較少做這樣的風扇設計?!

    回覆刪除

注意:只有此網誌的成員可以留言。