IPMI SDR 解析:BMC 如何把電源的 PMBus 讀值變成感測器(電源工程師版)
IPMI SDR(Sensor Data Record)逐位元組解析:Full Sensor Record 的結構、M / B / R 換算公式、門檻(UNC / UC / UNR)與遲滯,並說明為什麼 ipmitool 顯示的電源功率和 PMBus 讀值不一樣(8 位元量化),附 Python 解析程式。
這篇是 BMC 如何監控電源 的延伸,建議先看那篇了解整體流程。
SDR 是什麼
SDR = 每個感測器的「說明書」。BMC 內部有一個 SDR Repository(SDR 資料庫),每個感測器、每個 FRU 裝置各有一筆記錄,描述:
- 感測器的名稱、編號、屬於哪個零件(例如第 1 顆電源)
- 類型與單位:溫度 °C、功率 W、轉速 RPM
- 原始值的換算公式
- 警告與故障門檻
常見的記錄類型:
| Record Type | 名稱 | 用途 |
|---|---|---|
01h | Full Sensor Record | 有數值的感測器(溫度、電壓、功率),最常見 |
02h | Compact Sensor Record | 精簡版,常用於只有狀態的感測器(例如電源的 Status) |
03h | Event-Only Record | 只產生事件、沒有讀值的感測器 |
11h | FRU Device Locator | 告訴你某個 FRU 存在哪裡(例如電源的 FRU EEPROM) |
12h | Management 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 = 1 | 1 °C | −128 ~ 127 °C(有號數) |
| 輸入功率 | M = 10 | 10 W | 0 ~ 2550 W |
| 輸入電壓 | M = 2 | 2 V | 0 ~ 510 V |
| 風扇 | M = 100 | 100 RPM | 0 ~ 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 | 值 | 欄位 | 解讀 |
|---|---|---|---|
| 00h | 10 00 | Record ID | 記錄編號 16(低位元組在前) |
| 02h | 51 | SDR Version | IPMI 1.5 / 2.0 格式 |
| 03h | 01 | Record Type | Full Sensor Record |
| 04h | 34 | Record Length | 後面還有 52 Bytes |
| 05h | 20 | Sensor Owner ID | 由 BMC(位址 20h)管理 |
| 06h | 00 | Sensor Owner LUN | LUN 0 |
| 07h | 52 | Sensor Number | 感測器編號 52h,SEL 事件用這個編號指向感測器 |
| 08h | 0A 01 | Entity ID / Instance | 0Ah = 電源供應器,第 1 顆 |
| 0Ah | 7F 68 | Initialization / Capabilities | 感測器初始化與能力設定 |
| 0Ch | 01 | Sensor Type | 01h = 溫度 |
| 0Dh | 01 | Event / Reading Type | 01h = 門檻型感測器 |
| 0Eh | 00 00 00 00 | 事件遮罩 | 哪些門檻事件要產生 SEL(範例中簡化為 0) |
| 12h | 38 00 | Readable / Settable Threshold Mask | 38h:UNC、UC、UNR 可讀 |
| 14h | 80 | Sensor Units 1 | b[7:6] = 10:原始值是二補數(可為負) |
| 15h | 01 | Base Unit | 1 = °C(4 = V、5 = A、6 = W、18 = RPM) |
| 17h | 00 | Linearization | Linear |
| 18h | 01 00 | M + Tolerance | M = 1 |
| 1Ah | 00 00 | B + Accuracy | B = 0 |
| 1Dh | 00 | Rexp / Bexp | b[7:4] = Rexp = 0,b[3:0] = Bexp = 0 |
| 1Fh | 23 | Nominal Reading | 正常值 35 °C |
| 22h | FF 00 | Sensor Max / Min | 原始值範圍 |
| 24h | 55 4B 41 | UNR / UC / UNC | 85 °C / 75 °C / 65 °C |
| 27h | 00 00 00 | LNR / LC / LNC | 未使用 |
| 2Ah | 02 02 | Hysteresis(+ / −) | 遲滯 2 °C |
| 2Fh | C9 | ID String Type/Length | 8 位元 ASCII,長度 9(和 FRU 的 Type/Length 同樣概念) |
| 30h | 50 53 55 … | ID String | PSU1 Temp |
M 和 B 是 10 位元:低 8 位元放在第一個 Byte,高 2 位元放在下一個 Byte 的 b[7:6]。
門檻:UNC、UC、UNR
| 縮寫 | 全名 | 意義 | 常見顯示 |
|---|---|---|---|
| UNC | Upper Non-Critical | 偏高,警告 | Warning |
| UC | Upper Critical | 嚴重偏高,需要處理 | Critical |
| UNR | Upper Non-Recoverable | 極端值,可能造成損壞 | Non-recoverable |
| LNC / LC / LNR | Lower … | 同上,下限方向 |
讀值越過門檻時,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 / Unavailable | BMC 讀 PMBus 失敗(逾時、NACK) | 檢查回應時間與 Clock Stretching,見 常見問題除錯 |
| 電源已保護關機,BMC 沒有任何警告 | BMC 門檻比電源保護門檻高 | 和 BMC 團隊對齊兩邊的門檻 |
| 高功率電源的功率顯示被截在某個值 | SDR 範圍不夠(例如 M = 10 只到 2550 W) | 請 BMC 團隊調整 M 或 R |
結論
- SDR 是每個感測器的說明書:名稱、編號、單位、換算係數、門檻。
- IPMI 感測器讀值只有 1 Byte,用 換算。
- 8 位元量化會讓 BMC 顯示值和 PMBus 讀值有差距,差距大小由 M 和 R 決定。
- UNC / UC / UNR 是 BMC 的警告門檻,和電源內部的 PMBus 保護門檻是兩套設定。
- SDR 的 Sensor Number 是 SEL 事件指向感測器的依據。
延伸閱讀: