BMC的风扇控制算一个蛮重要的功能,那这个功能包含了"TACH"和"PWM"这两个常用讯号怎么解读,还有最常用的控制演算法"PID(closed loop)"和"Stepwise(openloop)",而Error control ,因为每家做法不同,就不会在这边作介绍
##实作代码是来自Facebook的openBMC
在服务器中的风扇是非常高速也耗电的,根据统计在数据中心的运维成本上,电费占了7成,并且如果让风扇长期处在全转状态也会有噪音和耗损度的问题。
目前CPU可以达到单一耗电量200W以上,伴随而来的高热也会使个晶片老化速度加快,当然服務器中的SSD, NIC card, E1.S等装置也是有同样的情况,因此我们希望能根据这些高热的元件温度来决定风扇的转速,达到省电和正常运作的效果
PWM是透过平均电压来传递类比讯号,简单来说就是在一个cycle中,高电为占百分之多少,就表示他要传递的值是多少
例如在一个周期中,如果高电位占25%,低电位占75%,这样表示我们要传递的值是25%,风扇就会转25%
那如果今天我们都一直是high (高电位),这样风扇就会全转,因此在线路图的review过程中,都会注意PWM有没有pull high,避免在BMC更新过程或是死掉后,机器过热
那每个周期的时间是多少呢? PWM的传递频率会定义在风扇的spec中,每一颗风扇的接收频率有可能会不一样
我们可以透过Tach来传递风扇的转速,有修过机械方面的课程就会知道,马达会有n个机械原点,传一圈的话会产生n个pulse,一圈会产生几个pulse也是定义在风扇的spec中
假如今天风扇转一圈会产生两个pulse,我们在一秒内收到1000个pulse,这样表示风扇一秒转了500圈,风扇转速是用rpm(一分钟转几圈)表示的,因此500*60 rpm就是我们要求的值
TACH的讯号大概就长得像底下这样,只要统计一秒钟有几个讯号,就能得到风扇的转速
在传统控制(Classical Control )理论中,通常会用有没有feedback 来区分open loop control 和 closed loop control,前者通常就是我们常说的stepwise,后者就是工业控制中广泛使用的PID control,他们的示意图如下,接下来会分别介绍这两个算法的的内容
Open loop,会根据input来直接求出output并输出,不会参考feedback或任何的actuating error
通常thermal team会给BMC一个表(fan table),表示Sensor 温度(input)为多少的时候,风扇转速(output)为多少,大概如下
CPU 温度 | 风扇转速 |
60 | 40 |
70 | 50 |
80 | 70 |
90 | 80 |
100 | 98 |
实作也相对简单,就一个for 回圈就可以了
# 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_hystdef run(self, value, ctx):
mini = 0if value >= self.compare_fsc_value:
if math.fabs(self.compare_fsc_value - value) <= self.poshyst:
return self.last_outif value <= self.compare_fsc_value:
if math.fabs(self.compare_fsc_value - value) <= self.neghyst:
return self.last_outfor (in_thr, out) in self.table:
mini = out
if value >= in_thr:
self.compare_fsc_value = value
self.last_out = out
return outself.compare_fsc_value = value
self.last_out = mini
return mini
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常数
因为当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 (=-2*-10) 和60 (=-30*-2)
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 = Nonedef 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
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 = Nonedef 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的变化量,以最近一两次的误差变化量来决定风扇转速
把这次的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 = Nonedef 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
风扇控制会用到的概念大概就这些 🙂
剩下的部分因为各家做法都不一样,所以就先不讨论