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 (高电位),这样风扇就会全转,因此在线路图的review过程中,都会注意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
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

 实作也相对简单,就一个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_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 (=-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 = 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

风扇控制会用到的概念大概就这些 :)
剩下的部分因为各家做法都不一样,所以就先不讨论

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐