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 解析程式與常見錯誤。
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 | 值 | 欄位 | 解讀 |
|---|---|---|---|
| 0 | 01h | Format Version | 版本 1(b[3:0],b[7:4] 保留) |
| 1 | 00h | Internal Use Area Offset | 不存在 |
| 2 | 00h | Chassis Info Area Offset | 不存在 |
| 3 | 01h | Board Info Area Offset | 從 01h × 8 = 第 8 Byte(08h) 開始 |
| 4 | 07h | Product Info Area Offset | 從 07h × 8 = 第 56 Byte(38h) 開始 |
| 5 | 00h | MultiRecord Area Offset | 不存在 |
| 6 | 00h | PAD | 固定 00h |
| 7 | F7h | Checksum | 讓 8 個位元組加總為 0 |
Zero Checksum 怎麼算
FRU 的 Checksum 規則是:整個區域(包含 Checksum 本身)所有位元組加總,取低 8 位元必須等於 0。
以 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
| 位置 | 值 | 欄位 | 解讀 |
|---|---|---|---|
| 08h | 01 | Format Version | 版本 1 |
| 09h | 06 | Area Length | 06h × 8 = 48 Bytes(08h ~ 37h) |
| 0Ah | 00 | Language Code | 0 = 英文 |
| 0Bh ~ 0Dh | F0 71 F7 | Mfg Date/Time | 見下方說明 |
| 0Eh | C4 + 4 Bytes | Board Manufacturer | ACME |
| 13h | C9 + 9 Bytes | Board Product Name | CRPS-1600 |
| 1Dh | CA + 10 Bytes | Board Serial Number | SN12345678 |
| 28h | CA + 10 Bytes | Board Part Number | PN-1600-01 |
| 33h | C0 | FRU File ID | 空欄位 |
| 34h | C1 | End of Fields | 欄位結束 |
| 35h ~ 36h | 00 00 | Padding | 補到 8 的倍數 |
| 37h | 58 | Checksum | 整個區域加總為 0 |
欄位的順序是固定的:製造商 → 名稱 → 序號 → 料號 → FRU File ID,之後可以接任意數量的自訂欄位,最後以 C1h 結束。
製造日期:從 1996 年起算的分鐘數
Mfg Date/Time 是 3 Bytes、低位元組在前的整數,代表從 1996 年 1 月 1 日 00:00 起算的分鐘數:
值為 00 00 00 代表未指定。
3. Type/Length 位元組:最關鍵的一個 Byte
每個字串欄位前面都有一個 Type/Length 位元組,告訴你「後面的資料是什麼格式、有幾個 Byte」:
| b[7:6] | 類型 | 說明 |
|---|---|---|
00 | 二進位 / 未指定 | 原始資料,顯示時通常用十六進位 |
01 | BCD plus | 每 4 位元一個字元:0~9、空白、-、. |
10 | 6 位元 ASCII | 3 Bytes 打包 4 個字元(只有大寫與部分符號) |
11 | 8 位元 ASCII | 最常見,一般的文字字串 |
解讀範例
| Type/Length | 二進位 | 類型 | 長度 | 意義 |
|---|---|---|---|---|
C4h | 11 000100 | 8 位元 ASCII | 4 | 後面 4 Bytes 是文字 |
CAh | 11 001010 | 8 位元 ASCII | 10 | 後面 10 Bytes 是文字 |
C0h | 11 000000 | 8 位元 ASCII | 0 | 空欄位(欄位存在,但沒有內容) |
C1h | 11 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
| 位置 | 欄位 | 說明 |
|---|---|---|
| 38h | Format Version | 01h |
| 39h | Area Length | 06h × 8 = 48 Bytes |
| 3Ah | Language Code | 00h = 英文 |
| 之後 | Manufacturer Name、Product Name、Part/Model Number、Product Version、Serial Number、Asset Tag、FRU File ID | 依序排列的 Type/Length 欄位 |
| 66h | C1h | 結束標記 |
| 67h | 8Bh | Checksum |
這個區域剛好不需要補 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 | 欄位 |
|---|---|
| 0 | Record Type ID |
| 1 | b7 = 最後一筆記錄;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 |
| 修改序號後沒有更新 Checksum | ipmitool 會回報 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。
延伸閱讀: