用 Python / Linux 讀取 PMBus 電源:i2c-tools、smbus2 與 hwmon 實作
在 Linux 上用 Python 讀取 PMBus 電源的完整實作:i2c-tools 快速驗證、smbus2 讀取 LINEAR11 / LINEAR16 量測值、STATUS_WORD 解碼、Block Read 讀取 MFR_ID、PEC 驗證與 QUERY,以及用核心 pmbus 驅動透過 hwmon / sensors 監控。
準備環境
硬體:Linux 上要有一個 I2C 介面
| 選項 | 說明 |
|---|---|
| 伺服器 BMC | 直接存取主機板上連接電源的 I2C 匯流排 |
| Raspberry Pi | 內建 I2C(/dev/i2c-1),適合實驗室測試 |
| USB 轉 I2C | 例如 MCP2221(Linux 內建 hid-mcp2221 驅動),接在一般 PC 上 |
軟體
sudo apt install i2c-tools python3-pip
pip install smbus2
# 讓目前使用者不用 sudo 就能存取 I2C
sudo usermod -aG i2c $USER # 重新登入後生效
第一步:用 i2c-tools 確認連線
# 列出所有 I2C 匯流排
i2cdetect -l
# 掃描 bus 1 上的裝置
i2cdetect -y 1
找到電源的位址後(以下以 0x58 為例),先用幾個唯讀命令確認通訊正常:
i2cget -y 1 0x58 0x98 b # PMBUS_REVISION
i2cget -y 1 0x58 0x79 w # STATUS_WORD
i2cget -y 1 0x58 0x88 w # READ_VIN(LINEAR11 原始值)
第二步:Python 工具
以下程式用 smbus2 存取 I2C,換算方式都來自系列文章:
- 量測值用 LINEAR11,
READ_VOUT用 LINEAR16 + VOUT_MODE - 狀態用 STATUS_WORD 解碼
- Block Read 與 Process Call 的 PEC 由程式自己驗證
#!/usr/bin/env python3
"""pmbus_tool.py:在 Linux 上用 smbus2 讀取 PMBus 電源"""
import argparse
import time
from smbus2 import SMBus, i2c_msg
# PMBus 命令碼
CLEAR_FAULTS = 0x03
CAPABILITY = 0x19
QUERY = 0x1A
VOUT_MODE = 0x20
STATUS_WORD = 0x79
READ_VIN, READ_IIN, READ_VOUT, READ_IOUT = 0x88, 0x89, 0x8B, 0x8C
READ_TEMPERATURE_1 = 0x8D
READ_POUT, READ_PIN = 0x96, 0x97
PMBUS_REVISION = 0x98
MFR_ID, MFR_MODEL, MFR_SERIAL = 0x99, 0x9A, 0x9E
STATUS_WORD_BITS = [
"NONE_OF_THE_ABOVE", "CML", "TEMPERATURE", "VIN_UV_FAULT",
"IOUT_OC_FAULT", "VOUT_OV_FAULT", "OFF", "BUSY",
"UNKNOWN", "OTHER", "FANS", "POWER_GOOD#",
"MFR_SPECIFIC", "INPUT", "IOUT/POUT", "VOUT",
]
def linear11(raw):
"""LINEAR11:b[15:11] 指數 N、b[10:0] 尾數 Y,兩者都是二補數"""
n = raw >> 11
if n > 15:
n -= 32
y = raw & 0x7FF
if y > 1023:
y -= 2048
return y * 2.0 ** n
def linear16(raw, vout_mode):
"""LINEAR16:16 位元無號尾數,指數在 VOUT_MODE 的 b[4:0]"""
if vout_mode & 0xE0:
raise ValueError(f"VOUT_MODE=0x{vout_mode:02X} 不是 Linear 格式")
n = vout_mode & 0x1F
if n > 15:
n -= 32
return raw * 2.0 ** n
def smbus_pec(data):
"""CRC-8/SMBUS:多項式 0x07、初始值 0"""
crc = 0
for b in data:
crc ^= b
for _ in range(8):
crc = ((crc << 1) ^ 0x07) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
return crc
def decode_status_word(sw):
return [name for bit, name in enumerate(STATUS_WORD_BITS) if sw >> bit & 1]
class PMBusDevice:
def __init__(self, bus, addr, pec=False):
self.bus = bus
self.addr = addr
self.pec = pec
self.bus.pec = pec # Byte / Word 交易由驅動計算與檢查 PEC
self._vout_mode = None
# ---- Byte / Word ----
def read_byte(self, cmd):
return self.bus.read_byte_data(self.addr, cmd)
def read_word(self, cmd):
return self.bus.read_word_data(self.addr, cmd)
def read_linear11(self, cmd):
return linear11(self.read_word(cmd))
def read_vout(self):
if self._vout_mode is None:
self._vout_mode = self.read_byte(VOUT_MODE)
return linear16(self.read_word(READ_VOUT), self._vout_mode)
def status_word(self):
return self.read_word(STATUS_WORD)
def clear_faults(self):
self.bus.write_byte(self.addr, CLEAR_FAULTS)
# ---- Block Read / Process Call ----
def _block_transfer(self, write, max_len):
"""寫入 write 後 Repeated Start 讀回 Byte Count + 資料(+ PEC),自行驗證 PEC"""
wr = i2c_msg.write(self.addr, write)
rd = i2c_msg.read(self.addr, 1 + max_len + (1 if self.pec else 0))
self.bus.i2c_rdwr(wr, rd)
raw = list(rd)
count = raw[0]
if count > max_len:
raise IOError(f"Byte Count {count} 超過 {max_len}")
data = raw[1:1 + count]
if self.pec:
frame = [self.addr << 1, *write, self.addr << 1 | 1, count, *data]
if smbus_pec(frame) != raw[1 + count]:
raise IOError("PEC 錯誤")
return data
def read_block(self, cmd, max_len=32):
return self._block_transfer([cmd], max_len)
def read_string(self, cmd):
return bytes(self.read_block(cmd)).decode("ascii", "replace").strip()
def query(self, cmd):
"""QUERY (1Ah):回傳 None 表示不支援"""
q = self._block_transfer([QUERY, 1, cmd], 1)[0]
return q if q & 0x80 else None
def print_report(psu):
for name, cmd in (("MFR_ID", MFR_ID), ("MFR_MODEL", MFR_MODEL), ("MFR_SERIAL", MFR_SERIAL)):
try:
print(f"{name:<11}: {psu.read_string(cmd)}")
except OSError as e: # 不支援的命令會 NACK;PEC 錯誤也會到這裡
print(f"{name:<11}: (讀取失敗:{e})")
print(f"VIN : {psu.read_linear11(READ_VIN):8.2f} V")
print(f"IIN : {psu.read_linear11(READ_IIN):8.2f} A")
print(f"VOUT : {psu.read_vout():8.3f} V")
print(f"IOUT : {psu.read_linear11(READ_IOUT):8.2f} A")
print(f"PIN : {psu.read_linear11(READ_PIN):8.1f} W")
print(f"POUT : {psu.read_linear11(READ_POUT):8.1f} W")
print(f"TEMP1 : {psu.read_linear11(READ_TEMPERATURE_1):8.1f} °C")
sw = psu.status_word()
print(f"STATUS_WORD: 0x{sw:04X} {' '.join(decode_status_word(sw)) or 'OK'}")
def main():
ap = argparse.ArgumentParser(description="讀取 PMBus 電源")
ap.add_argument("--bus", type=int, default=1, help="I2C bus 編號")
ap.add_argument("--addr", type=lambda s: int(s, 0), default=0x58, help="PMBus 位址")
ap.add_argument("--pec", action="store_true", help="啟用 PEC")
ap.add_argument("--watch", type=float, metavar="SEC", help="每隔幾秒重複讀取")
args = ap.parse_args()
with SMBus(args.bus) as bus:
psu = PMBusDevice(bus, args.addr, pec=args.pec)
while True:
print_report(psu)
if not args.watch:
break
print("-" * 40)
time.sleep(args.watch)
if __name__ == "__main__":
main()
執行
python3 pmbus_tool.py --bus 1 --addr 0x58
python3 pmbus_tool.py --bus 1 --addr 0x58 --pec --watch 2 # 啟用 PEC,每 2 秒更新
輸出範例(數值依實際電源而定):
MFR_ID : ACME
MFR_MODEL : CRPS-1600
MFR_SERIAL : SN12345678
VIN : 230.00 V
IIN : 2.20 A
VOUT : 12.000 V
IOUT : 41.50 A
PIN : 508.0 W
POUT : 498.0 W
TEMP1 : 35.2 °C
STATUS_WORD: 0x0000 OK
程式重點說明
Byte / Word 交易:交給 smbus2
read_byte_data()、read_word_data() 對應 SMBus 的 Read Byte / Read Word。read_word_data() 回傳的值已經把低 / 高位元組組合好,可以直接用 LINEAR11 解碼。設定 bus.pec = True 後,這類交易的 PEC 由核心驅動計算與檢查。
Block Read:用 i2c_rdwr() 自己處理
MFR_ID 這類 Block Read,smbus2 雖然有 read_block_data(),但很多 I2C 控制器不支援 SMBus Block Read。比較通用的做法是用 i2c_rdwr():
- 寫入命令碼
- Repeated Start 後一次讀回最大長度(Byte Count + 32 Bytes + PEC)
- 依第一個 Byte 的 Byte Count 取出實際資料
多讀的位元組會被忽略。因為這種方式繞過了 SMBus 層,PEC 要自己算,包含兩次位址位元組。
VOUT_MODE 只讀一次
read_vout() 第一次讀取時把 VOUT_MODE 快取起來。VOUT_MODE 在電源運作中通常不會改變;如果你的電源允許修改它,就要在修改後清除快取。
另一條路:Linux 核心 pmbus 驅動 + hwmon
如果只是要監控,不一定要自己寫程式。Linux 核心內建通用的 pmbus 驅動,會自動偵測電源支援的命令,並把量測值放到 hwmon 介面:
# 載入驅動,並告訴核心 bus 1 的 0x58 是 PMBus 裝置
sudo modprobe pmbus
echo pmbus 0x58 | sudo tee /sys/bus/i2c/devices/i2c-1/new_device
# 用 lm-sensors 查看
sudo apt install lm-sensors
sensors
sensors 會列出電壓、電流、功率、溫度與風扇轉速。也可以直接讀 sysfs:
cat /sys/class/hwmon/hwmon*/name # 找到 pmbus 對應的 hwmonN
cat /sys/class/hwmon/hwmonN/in2_input # 電壓,單位 mV
cat /sys/class/hwmon/hwmonN/curr2_input # 電流,單位 mA
cat /sys/class/hwmon/hwmonN/power2_input # 功率,單位 µW
cat /sys/class/hwmon/hwmonN/temp1_input # 溫度,單位 m°C
| 項目 | 自己寫 Python | 核心 pmbus 驅動 |
|---|---|---|
| 適合 | 測試工具、除錯、讀寫任意命令 | 長期監控、整合到現有監控系統 |
| 彈性 | 任何命令都能讀寫 | 只提供驅動支援的量測與告警 |
| 格式換算 | 自己處理 | 驅動自動處理 |
常見問題
| 症狀 | 可能原因 |
|---|---|
i2cdetect 找不到電源 | 接線、上拉電阻、電壓準位、電源待機電源(Standby)沒有供電 |
讀取回 Remote I/O error | 裝置 NACK:位址錯誤,或電源不支援這個命令 |
| 讀值偶爾錯誤或亂跳 | 雜訊或 Timeout;啟用 PEC 找出錯誤的交易 |
READ_VOUT 數值離譜 | 用 LINEAR11 解了 LINEAR16,或忘了讀 VOUT_MODE |
| 溫度讀到 2000 多度 | LINEAR11 的尾數沒有做二補數轉換 |
PEC 錯誤 | 電源不支援 PEC(查 CAPABILITY (19h) 的 b7),或線路雜訊 |
Device or resource busy | 核心驅動已經綁定這個位址 |
結論
- 用 i2c-tools 先確認位址與通訊,再寫程式。
- smbus2 的
read_byte_data()/read_word_data()處理 Byte / Word 交易;Block Read 與 Process Call 用i2c_rdwr(),PEC 自己驗證。 - 換算規則:量測值用 LINEAR11,
READ_VOUT用 LINEAR16 + VOUT_MODE。 - 只需要監控時,用核心 pmbus 驅動 + hwmon / sensors 最省事。
延伸閱讀: