IPMI / BMC 系列約 7 分鐘

IPMI FRU 資料格式完整解析:Common Header、Board / Product Area 逐位元組看懂

新手也看得懂的 IPMI FRU 格式教學:用一份 104 Bytes 的實際 FRU 資料逐位元組解析 Common Header、Board Info Area、Product Info Area,搞懂 ×8 偏移、Type/Length 位元組、1996 年起算的製造日期與 Zero Checksum,附 Python 解析程式與常見錯誤。

📘 參考規格書IPMI FRU · IPMI Platform Management FRU Information Storage Definition v1.0 · Rev 1.3原廠文件 ↗

FRU 是什麼、存在哪裡

伺服器裡可以現場更換的零件,例如主機板、電源供應器、風扇板、背板,通常各自帶一顆小 EEPROM(例如 24C02,256 Bytes),存放符合 IPMI FRU 規格的資料。BMC 開機時讀取這些 EEPROM,建立整台機器的資產清單。

以 CRPS 電源為例,常見的做法是 PMBus 控制器和 FRU EEPROM 掛在同一條 SMBus 上,用不同位址區分。FRU EEPROM 的位址在規格書上常寫成 A0h、A2h、A4h、A6h(依插槽位置),這是 8 位元寫法,在 Linux 上對應 7 位元的 0x50 ~ 0x53。實際位址請以產品規格書為準。

在 BMC 上用 ipmitool 讀取的結果,看起來像這樣:

FRU Device Description : Builtin FRU Device (ID 0)
 Board Mfg Date        : Sat Oct 31 12:00:00 2026
 Board Mfg             : ACME
 Board Product         : CRPS-1600
 Board Serial          : SN12345678
 Board Part Number     : PN-1600-01
 Product Manufacturer  : ACME
 Product Name          : CRPS-1600
 Product Part Number   : PN-1600-01
 Product Version       : A01
 Product Serial        : SN12345678

這篇要做的,就是看懂這些文字在 EEPROM 裡實際長什麼樣子。

整體結構

FRU 資料由一個固定 8 Bytes 的 Common Header 開頭,後面接著幾個區域(Area),每個區域都是選配的:

區域內容
Common Header版本、各區域的位置、Checksum
Internal Use Area廠商內部使用,格式自訂
Chassis Info Area機箱類型、機箱料號與序號
Board Info Area電路板製造商、名稱、序號、料號、製造日期
Product Info Area產品製造商、名稱、型號、版本、序號、資產標籤
MultiRecord Area電源資訊、DC 輸出等結構化記錄

範例資料

以下是一份完整的 FRU 資料,共 104 Bytes,包含 Common Header、Board Info Area 和 Product Info Area:

0000: 01 00 00 01 07 00 00 F7 01 06 00 F0 71 F7 C4 41
0010: 43 4D 45 C9 43 52 50 53 2D 31 36 30 30 CA 53 4E
0020: 31 32 33 34 35 36 37 38 CA 50 4E 2D 31 36 30 30
0030: 2D 30 31 C0 C1 00 00 58 01 06 00 C4 41 43 4D 45
0040: C9 43 52 50 53 2D 31 36 30 30 CA 50 4E 2D 31 36
0050: 30 30 2D 30 31 C3 41 30 31 CA 53 4E 31 32 33 34
0060: 35 36 37 38 C0 C0 C1 8B

接下來一段一段拆開。

1. Common Header(00h ~ 07h)

0000: 01 00 00 01 07 00 00 F7
Byte值欄位解讀
001hFormat Version版本 1(b[3:0],b[7:4] 保留)
100hInternal Use Area Offset不存在
200hChassis Info Area Offset不存在
301hBoard Info Area Offset從 01h × 8 = 第 8 Byte(08h) 開始
407hProduct Info Area Offset從 07h × 8 = 第 56 Byte(38h) 開始
500hMultiRecord Area Offset不存在
600hPAD固定 00h
7F7hChecksum讓 8 個位元組加總為 0

Zero Checksum 怎麼算

FRU 的 Checksum 規則是:整個區域(包含 Checksum 本身)所有位元組加總,取低 8 位元必須等於 0。

Checksum=(256−(前面所有位元組的總和 mod 256)) mod 256\text{Checksum} = (256 - (\text{前面所有位元組的總和} \bmod 256)) \bmod 256

以 Common Header 為例:01 + 00 + 00 + 01 + 07 + 00 + 00 = 09h,所以 Checksum = 100h − 09h = F7h。驗證:09h + F7h = 100h,低 8 位元是 00h ✓。

2. Board Info Area(08h ~ 37h)

0008: 01 06 00 F0 71 F7
000E: C4 41 43 4D 45                               ← "ACME"
0013: C9 43 52 50 53 2D 31 36 30 30                ← "CRPS-1600"
001D: CA 53 4E 31 32 33 34 35 36 37 38             ← "SN12345678"
0028: CA 50 4E 2D 31 36 30 30 2D 30 31             ← "PN-1600-01"
0033: C0                                           ← 空欄位
0034: C1 00 00 58                                  ← 結束、補 0、Checksum
位置值欄位解讀
08h01Format Version版本 1
09h06Area Length06h × 8 = 48 Bytes(08h ~ 37h)
0Ah00Language Code0 = 英文
0Bh ~ 0DhF0 71 F7Mfg Date/Time見下方說明
0EhC4 + 4 BytesBoard ManufacturerACME
13hC9 + 9 BytesBoard Product NameCRPS-1600
1DhCA + 10 BytesBoard Serial NumberSN12345678
28hCA + 10 BytesBoard Part NumberPN-1600-01
33hC0FRU File ID空欄位
34hC1End of Fields欄位結束
35h ~ 36h00 00Padding補到 8 的倍數
37h58Checksum整個區域加總為 0

欄位的順序是固定的:製造商 → 名稱 → 序號 → 料號 → FRU File ID,之後可以接任意數量的自訂欄位,最後以 C1h 結束。

製造日期:從 1996 年起算的分鐘數

Mfg Date/Time 是 3 Bytes、低位元組在前的整數,代表從 1996 年 1 月 1 日 00:00 起算的分鐘數:

F0 71 F7→F771F0h=16,216,560 分鐘\texttt{F0 71 F7} \rightarrow \texttt{F771F0h} = 16{,}216{,}560 \text{ 分鐘} 1996-01-01 00:00+16,216,560 分鐘=2026-10-31 12:00\text{1996-01-01 00:00} + 16{,}216{,}560 \text{ 分鐘} = \text{2026-10-31 12:00}

值為 00 00 00 代表未指定。

3. Type/Length 位元組:最關鍵的一個 Byte

每個字串欄位前面都有一個 Type/Length 位元組,告訴你「後面的資料是什麼格式、有幾個 Byte」:

b[7:6]類型說明
00二進位 / 未指定原始資料,顯示時通常用十六進位
01BCD plus每 4 位元一個字元:0~9、空白、-、.
106 位元 ASCII3 Bytes 打包 4 個字元(只有大寫與部分符號)
118 位元 ASCII最常見,一般的文字字串

解讀範例

Type/Length二進位類型長度意義
C4h11 0001008 位元 ASCII4後面 4 Bytes 是文字
CAh11 0010108 位元 ASCII10後面 10 Bytes 是文字
C0h11 0000008 位元 ASCII0空欄位(欄位存在,但沒有內容)
C1h11 000001——欄位結束標記

4. Product Info Area(38h ~ 67h)

0038: 01 06 00
003B: C4 41 43 4D 45                               ← Manufacturer "ACME"
0040: C9 43 52 50 53 2D 31 36 30 30                ← Product Name "CRPS-1600"
004A: CA 50 4E 2D 31 36 30 30 2D 30 31             ← Part/Model "PN-1600-01"
0055: C3 41 30 31                                  ← Version "A01"
0059: CA 53 4E 31 32 33 34 35 36 37 38             ← Serial "SN12345678"
0064: C0                                           ← Asset Tag(空)
0065: C0                                           ← FRU File ID(空)
0066: C1 8B                                        ← 結束、Checksum
位置欄位說明
38hFormat Version01h
39hArea Length06h × 8 = 48 Bytes
3AhLanguage Code00h = 英文
之後Manufacturer Name、Product Name、Part/Model Number、Product Version、Serial Number、Asset Tag、FRU File ID依序排列的 Type/Length 欄位
66hC1h結束標記
67h8BhChecksum

這個區域剛好不需要補 0:從 38h 到 C1h 共 47 Bytes,加上 Checksum 正好 48 Bytes。

Product Area 和 Board Area 很像,差別在沒有製造日期,多了 Version 與 Asset Tag。一般來說,Board 描述「這塊電路板」,Product 描述「整個出貨的產品」,兩者的序號不一定相同。

5. 其他區域

Chassis Info Area

結構和 Board Area 類似:版本、長度,接著是 1 Byte 的 Chassis Type(例如 Rack Mount Chassis,代碼沿用 SMBIOS 定義),之後是 Chassis Part Number、Chassis Serial Number 和自訂欄位,最後 C1h、補 0、Checksum。電源這類零件通常不放這個區域。

MultiRecord Area

用來存放結構化的二進位記錄,每筆記錄有 5 Bytes 的標頭:

Byte欄位
0Record Type ID
1b7 = 最後一筆記錄;b[3:0] = 記錄格式版本(2h)
2資料長度
3資料的 Checksum
4標頭的 Checksum

常見的 Record Type:00h Power Supply Information(額定功率、輸入範圍等)、01h DC Output、02h DC Load,C0h ~ FFh 為廠商自訂。電源供應器的 FRU 幾乎都會用到 MultiRecord,逐位元組解析請見下一篇:電源供應器 FRU EEPROM 解析:MultiRecord、Power Supply Information 與 DC Output。

用 Python 解析 FRU

以下程式解析 Common Header、Chassis / Board / Product Info Area,並驗證每個區域的 Checksum:

#!/usr/bin/env python3
"""fru_decode.py:解析 IPMI FRU 資料(Common Header、Chassis / Board / Product Info Area)"""
import sys
from datetime import datetime, timedelta

BOARD_FIELDS = ["Manufacturer", "Product Name", "Serial Number", "Part Number", "FRU File ID"]
PRODUCT_FIELDS = ["Manufacturer", "Product Name", "Part/Model Number", "Version",
                  "Serial Number", "Asset Tag", "FRU File ID"]
CHASSIS_FIELDS = ["Part Number", "Serial Number"]


def checksum_ok(data):
    """Zero checksum:所有位元組加總的低 8 位元必須是 0"""
    return sum(data) & 0xFF == 0


def decode_field(tl, raw):
    """依 Type/Length 位元組的 b[7:6] 解碼欄位內容"""
    kind = tl >> 6
    if kind == 0b11:                                   # 8 位元 ASCII
        return raw.decode("latin-1")
    if kind == 0b10:                                   # 6 位元 ASCII(3 Bytes 打包 4 個字元)
        chars = []
        for i in range(0, len(raw) - len(raw) % 3, 3):
            v = raw[i] | raw[i + 1] << 8 | raw[i + 2] << 16
            chars += [chr((v >> s & 0x3F) + 0x20) for s in (0, 6, 12, 18)]
        return "".join(chars)
    if kind == 0b01:                                   # BCD plus
        table = "0123456789 -.???"
        return "".join(table[b >> 4] + table[b & 0x0F] for b in raw)
    return raw.hex(" ").upper()                        # 00:二進位


def read_fields(area, pos, names):
    """從 pos 開始讀取 Type/Length 欄位,直到 C1h 結束標記"""
    result = {}
    i = 0
    while area[pos] != 0xC1:
        tl = area[pos]
        length = tl & 0x3F
        raw = area[pos + 1:pos + 1 + length]
        name = names[i] if i < len(names) else f"Custom {i - len(names) + 1}"
        result[name] = decode_field(tl, raw)
        pos += 1 + length
        i += 1
    return result


def parse_area(data, offset, kind):
    area = data[offset:offset + data[offset + 1] * 8]
    if area[0] != 0x01 or not checksum_ok(area):
        raise ValueError(f"{kind} Info Area 版本或 Checksum 錯誤")
    if kind == "Board":
        minutes = area[3] | area[4] << 8 | area[5] << 16
        info = {"Mfg Date": "未指定" if minutes == 0
                else str(datetime(1996, 1, 1) + timedelta(minutes=minutes))}
        info.update(read_fields(area, 6, BOARD_FIELDS))
    elif kind == "Product":
        info = read_fields(area, 3, PRODUCT_FIELDS)
    else:  # Chassis
        info = {"Chassis Type": f"{area[2]:02X}h"}
        info.update(read_fields(area, 3, CHASSIS_FIELDS))
    return info


def parse_fru(data):
    header = data[:8]
    if header[0] != 0x01 or not checksum_ok(header):
        raise ValueError("Common Header 版本或 Checksum 錯誤")
    result = {}
    for kind, idx in (("Chassis", 2), ("Board", 3), ("Product", 4)):
        if header[idx]:
            result[kind] = parse_area(data, header[idx] * 8, kind)
    if header[5]:
        result["MultiRecord"] = {"Offset": f"{header[5] * 8} Bytes(本程式未解析)"}
    return result


if __name__ == "__main__":
    with open(sys.argv[1], "rb") as f:
        fru = parse_fru(f.read())
    for area_name, fields in fru.items():
        print(f"[{area_name}]")
        for k, v in fields.items():
            print(f"  {k:<18}: {v}")

用上面的範例資料執行:

$ python3 fru_decode.py fru.bin
[Board]
  Mfg Date          : 2026-10-31 12:00:00
  Manufacturer      : ACME
  Product Name      : CRPS-1600
  Serial Number     : SN12345678
  Part Number       : PN-1600-01
  FRU File ID       :
[Product]
  Manufacturer      : ACME
  Product Name      : CRPS-1600
  Part/Model Number : PN-1600-01
  Version           : A01
  Serial Number     : SN12345678
  Asset Tag         :
  FRU File ID       :

從 EEPROM 取得 FRU 資料

在 Linux 上,可以用核心的 at24 EEPROM 驅動把整顆 EEPROM 讀成檔案(以 bus 1、位址 0x50、24C02 為例):

echo 24c02 0x50 | sudo tee /sys/bus/i2c/devices/i2c-1/new_device
sudo cat /sys/bus/i2c/devices/1-0050/eeprom > fru.bin
hexdump -C fru.bin | head

在有 BMC 的伺服器上,則直接用 ipmitool:

ipmitool fru print          # 列出所有 FRU
ipmitool fru read 0 fru.bin # 把 FRU ID 0 存成檔案

新手常見錯誤

錯誤正確觀念
把 Header 的 07h 當成第 7 Byte位置單位是 8 Bytes:07h × 8 = 第 56 Byte
把 Area Length 當成 Byte 數也是 8 Bytes 為單位,而且包含最後的 Checksum
把 CAh 當成長度 202長度只看 b[5:0]:CAh & 3Fh = 10
看到 C0h 就停止解析C0h 是空欄位,C1h 才是結束
用 Unix Time 解讀製造日期1996-01-01 起算的分鐘數,3 Bytes 低位元組在前
以為 Checksum 是 XOR 或 CRC是 Zero Checksum:加總後低 8 位元為 0
修改序號後沒有更新 Checksumipmitool 會回報 Checksum 錯誤,甚至不顯示資料
序號變長,只改字串本身後面所有欄位會位移,要重新計算 Area Length、Padding、Checksum,必要時也要更新 Header 中後續區域的位置
以為字串以 00h 結尾字串長度完全由 Type/Length 決定,沒有 NUL 結尾

結論

  • FRU 資料 = Common Header + 各區域,位置與長度的單位都是 8 Bytes。
  • 每個字串欄位前面有 Type/Length:b[7:6] 是類型,b[5:0] 是長度;C0h 空欄位、C1h 結束。
  • 製造日期是 1996 年起算的分鐘數,3 Bytes 低位元組在前。
  • 每個區域都用 Zero Checksum:加總後低 8 位元為 0。
  • 修改內容後,記得重新計算長度、補 0 與 Checksum。

延伸閱讀: