IPMI / BMC 系列約 6 分鐘

IPMI SDR 解析:BMC 如何把電源的 PMBus 讀值變成感測器(電源工程師版)

IPMI SDR(Sensor Data Record)逐位元組解析:Full Sensor Record 的結構、M / B / R 換算公式、門檻(UNC / UC / UNR)與遲滯,並說明為什麼 ipmitool 顯示的電源功率和 PMBus 讀值不一樣(8 位元量化),附 Python 解析程式。

📘 參考規格書IPMI · Intelligent Platform Management Interface Specification v2.0原廠文件 ↗

這篇是 BMC 如何監控電源 的延伸,建議先看那篇了解整體流程。

SDR 是什麼

SDR = 每個感測器的「說明書」。BMC 內部有一個 SDR Repository(SDR 資料庫),每個感測器、每個 FRU 裝置各有一筆記錄,描述:

  • 感測器的名稱、編號、屬於哪個零件(例如第 1 顆電源)
  • 類型與單位:溫度 °C、功率 W、轉速 RPM
  • 原始值的換算公式
  • 警告與故障門檻

常見的記錄類型:

Record Type名稱用途
01hFull Sensor Record有數值的感測器(溫度、電壓、功率),最常見
02hCompact Sensor Record精簡版,常用於只有狀態的感測器(例如電源的 Status)
03hEvent-Only Record只產生事件、沒有讀值的感測器
11hFRU Device Locator告訴你某個 FRU 存在哪裡(例如電源的 FRU EEPROM)
12hManagement Controller Device Locator描述 BMC 等管理控制器

這篇的重點是 Full Sensor Record。

讀值是兩段式的:1 Byte + 公式

IPMI 讀取感測器的命令 Get Sensor Reading 只回傳 1 Byte 的原始值。要知道它代表幾度、幾瓦,必須查這個感測器的 SDR,用裡面的係數換算:

flowchart LR
    P["電源 PMBus<br>READ_PIN = 508 W<br>(LINEAR11)"] --> B["BMC 轉成 1 Byte<br>508 ÷ 10 = 50.8 → 51 = 33h"]
    B --> G["Get Sensor Reading<br>回傳 33h"]
    S["SDR:M = 10、B = 0、R = 0<br>單位 W"] --> C
    G --> C["y = 10 × 51 = 510 W"]
    C --> U["ipmitool 顯示 510 W"]

所以 BMC 顯示的值不一定等於電源回報的值:中間經過一次量化到 8 位元,解析度由 SDR 的係數決定。

換算公式

這個公式和 PMBus 的 Direct 格式 是同一個概念,都是「斜率 × 原始值 + 偏移,再乘上 10 的次方」。

解析度:8 位元只有 256 階

原始值只有 1 Byte,所以範圍 ÷ 256 就是最小解析度:

感測器SDR 係數解析度可表示範圍
溫度M = 11 °C−128 ~ 127 °C(有號數)
輸入功率M = 1010 W0 ~ 2550 W
輸入電壓M = 22 V0 ~ 510 V
風扇M = 100100 RPM0 ~ 25500 RPM

部分 BMC(例如 OpenBMC)會依感測器的最大、最小值自動計算 M、B、R,所以不同平台上同一顆電源的解析度可能不同。

Full Sensor Record 的結構

以下是一筆「PSU1 Temp」的 Full Sensor Record(57 Bytes):

00: 10 00 51 01 34 20 00 52 0A 01 7F 68 01 01 00 00
10: 00 00 38 00 80 01 00 00 01 00 00 00 00 00 00 23
20: 00 00 FF 00 55 4B 41 00 00 00 02 02 00 00 00 C9
30: 50 53 55 31 20 54 65 6D 70
Offset值欄位解讀
00h10 00Record ID記錄編號 16(低位元組在前)
02h51SDR VersionIPMI 1.5 / 2.0 格式
03h01Record TypeFull Sensor Record
04h34Record Length後面還有 52 Bytes
05h20Sensor Owner ID由 BMC(位址 20h)管理
06h00Sensor Owner LUNLUN 0
07h52Sensor Number感測器編號 52h,SEL 事件用這個編號指向感測器
08h0A 01Entity ID / Instance0Ah = 電源供應器,第 1 顆
0Ah7F 68Initialization / Capabilities感測器初始化與能力設定
0Ch01Sensor Type01h = 溫度
0Dh01Event / Reading Type01h = 門檻型感測器
0Eh00 00 00 00事件遮罩哪些門檻事件要產生 SEL(範例中簡化為 0)
12h38 00Readable / Settable Threshold Mask38h:UNC、UC、UNR 可讀
14h80Sensor Units 1b[7:6] = 10:原始值是二補數(可為負)
15h01Base Unit1 = °C(4 = V、5 = A、6 = W、18 = RPM)
17h00LinearizationLinear
18h01 00M + ToleranceM = 1
1Ah00 00B + AccuracyB = 0
1Dh00Rexp / Bexpb[7:4] = Rexp = 0,b[3:0] = Bexp = 0
1Fh23Nominal Reading正常值 35 °C
22hFF 00Sensor Max / Min原始值範圍
24h55 4B 41UNR / UC / UNC85 °C / 75 °C / 65 °C
27h00 00 00LNR / LC / LNC未使用
2Ah02 02Hysteresis(+ / −)遲滯 2 °C
2FhC9ID String Type/Length8 位元 ASCII,長度 9(和 FRU 的 Type/Length 同樣概念)
30h50 53 55 …ID StringPSU1 Temp

M 和 B 是 10 位元:低 8 位元放在第一個 Byte,高 2 位元放在下一個 Byte 的 b[7:6]。

門檻:UNC、UC、UNR

縮寫全名意義常見顯示
UNCUpper Non-Critical偏高,警告Warning
UCUpper Critical嚴重偏高,需要處理Critical
UNRUpper Non-Recoverable極端值,可能造成損壞Non-recoverable
LNC / LC / LNRLower …同上,下限方向

讀值越過門檻時,BMC 會產生 SEL 事件,例如「Upper Non-critical going high」,詳見 IPMI SEL 解析。

**遲滯(Hysteresis)**是為了避免讀值在門檻附近來回跳動、一直產生事件:例如 UNC = 65 °C、遲滯 2 °C,讀值超過 65 °C 觸發警告後,要降到 63 °C 以下才會解除。

用 ipmitool 查看 SDR

ipmitool sdr list                  # 所有感測器的目前讀值
ipmitool sdr type "Power Supply"   # 只看電源類型的感測器
ipmitool sensor get "PSU1 Temp"    # 單一感測器的讀值與門檻
ipmitool sdr get "PSU1 Temp"       # 單一感測器的 SDR 詳細內容(含 M、B、R)
ipmitool sdr dump sdr.bin          # 把整個 SDR Repository 存成檔案

在底層,ipmitool 是用 Reserve SDR Repository、Get SDR 等命令一筆一筆讀出 SDR,再用 Get Sensor Reading 讀原始值,最後套公式換算。

用 Python 解析 Full Sensor Record

#!/usr/bin/env python3
"""sdr_decode.py:解析 IPMI Full Sensor Record(SDR Type 01h),並換算感測器讀值"""
import sys

SENSOR_TYPES = {0x01: "Temperature", 0x02: "Voltage", 0x03: "Current", 0x04: "Fan",
                0x08: "Power Supply", 0x0B: "Other Units-based"}
UNITS = {1: "°C", 4: "V", 5: "A", 6: "W", 18: "RPM"}
THRESHOLDS = ["LNC", "LC", "LNR", "UNC", "UC", "UNR"]       # Readable Mask 的 bit 0 ~ 5


def signed(value, bits):
    return value - (1 << bits) if value & (1 << (bits - 1)) else value


class FullSensorRecord:
    def __init__(self, rec):
        if rec[3] != 0x01:
            raise ValueError(f"不是 Full Sensor Record(Record Type = {rec[3]:02X}h)")
        self.record_id = rec[0] | rec[1] << 8
        self.sensor_number = rec[7]
        self.entity = (rec[8], rec[9])
        self.sensor_type = rec[12]
        self.analog_format = rec[20] >> 6                    # 0 無號、1 一補數、2 二補數
        self.unit = UNITS.get(rec[21], f"unit {rec[21]}")
        self.m = signed(rec[24] | (rec[25] >> 6) << 8, 10)
        self.b = signed(rec[26] | (rec[27] >> 6) << 8, 10)
        self.r_exp = signed(rec[29] >> 4, 4)
        self.b_exp = signed(rec[29] & 0x0F, 4)
        readable = rec[18]
        raw = {"UNR": rec[36], "UC": rec[37], "UNC": rec[38],
               "LNR": rec[39], "LC": rec[40], "LNC": rec[41]}
        self.thresholds = {name: raw[name] for bit, name in enumerate(THRESHOLDS)
                           if readable >> bit & 1}
        length = rec[47] & 0x1F
        self.name = rec[48:48 + length].decode("ascii")

    def convert(self, x):
        """y = (M × x + B × 10^Bexp) × 10^Rexp"""
        if self.analog_format == 2:
            x = signed(x, 8)
        elif self.analog_format == 1:
            x = -(~x & 0xFF) if x & 0x80 else x
        return (self.m * x + self.b * 10 ** self.b_exp) * 10 ** self.r_exp

    def describe(self):
        step = abs(self.m) * 10 ** self.r_exp
        lines = [
            f"Sensor   : {self.name}(#{self.sensor_number:02X}h,Record ID {self.record_id})",
            f"Type     : {SENSOR_TYPES.get(self.sensor_type, f'{self.sensor_type:02X}h')},單位 {self.unit}",
            f"Formula  : y = ({self.m} × x + {self.b} × 10^{self.b_exp}) × 10^{self.r_exp}",
            f"Resolution: {step:g} {self.unit},範圍 {self.convert(0):g} ~ {self.convert(255):g} {self.unit}"
            if self.analog_format == 0 else
            f"Resolution: {step:g} {self.unit},範圍 {self.convert(0x80):g} ~ {self.convert(0x7F):g} {self.unit}",
        ]
        for name in ("UNR", "UC", "UNC", "LNC", "LC", "LNR"):
            if name in self.thresholds:
                raw = self.thresholds[name]
                lines.append(f"  {name:<4}: {self.convert(raw):g} {self.unit}(原始值 {raw:02X}h)")
        return "\n".join(lines)


if __name__ == "__main__":
    with open(sys.argv[1], "rb") as f:
        sdr = FullSensorRecord(f.read())
    print(sdr.describe())
    for arg in sys.argv[2:]:                 # 選擇性:換算 Get Sensor Reading 的原始值
        x = int(arg, 16)
        print(f"讀值 {x:02X}h → {sdr.convert(x):g} {sdr.unit}")

執行結果:

$ python3 sdr_decode.py psu1_temp.sdr 23 C0
Sensor   : PSU1 Temp(#52h,Record ID 16)
Type     : Temperature,單位 °C
Formula  : y = (1 × x + 0 × 10^0) × 10^0
Resolution: 1 °C,範圍 -128 ~ 127 °C
  UNR : 85 °C(原始值 55h)
  UC  : 75 °C(原始值 4Bh)
  UNC : 65 °C(原始值 41h)
讀值 23h → 35 °C
讀值 C0h → -64 °C

$ python3 sdr_decode.py psu1_input_power.sdr 33
Sensor   : PSU1 Input Power(#53h,Record ID 17)
Type     : Other Units-based,單位 W
Formula  : y = (10 × x + 0 × 10^0) × 10^0
Resolution: 10 W,範圍 0 ~ 2550 W
  UC  : 900 W(原始值 5Ah)
  UNC : 850 W(原始值 55h)
讀值 33h → 510 W

電源工程師要知道的事

現象原因怎麼處理
BMC 顯示值和 PMBus 讀值差一點SDR 的 8 位元量化查 SDR 解析度,差距在一階以內屬正常
讀值在門檻附近時,SEL 一直出現事件電源讀值跳動,或 SDR 遲滯設太小電源端平滑讀值,或請 BMC 團隊調整遲滯
感測器顯示 na / UnavailableBMC 讀 PMBus 失敗(逾時、NACK)檢查回應時間與 Clock Stretching,見 常見問題除錯
電源已保護關機,BMC 沒有任何警告BMC 門檻比電源保護門檻高和 BMC 團隊對齊兩邊的門檻
高功率電源的功率顯示被截在某個值SDR 範圍不夠(例如 M = 10 只到 2550 W)請 BMC 團隊調整 M 或 R

結論

  • SDR 是每個感測器的說明書:名稱、編號、單位、換算係數、門檻。
  • IPMI 感測器讀值只有 1 Byte,用 y=(Mx+B⋅10Bexp)⋅10Rexpy = (M x + B \cdot 10^{B_{exp}}) \cdot 10^{R_{exp}} 換算。
  • 8 位元量化會讓 BMC 顯示值和 PMBus 讀值有差距,差距大小由 M 和 R 決定。
  • UNC / UC / UNR 是 BMC 的警告門檻,和電源內部的 PMBus 保護門檻是兩套設定。
  • SDR 的 Sensor Number 是 SEL 事件指向感測器的依據。

延伸閱讀: