Proxmox (Intel 82599ES) からSFP+ EEPROMをbit-bangで書き換えるツールを書いた
目次
はじめに
前の記事でx510のVCStackに汎用DACを通すためにSFP EEPROMを書き換えた話を書きました。 その中で、ツールの詳細は解説してなかったので今回はその実装について紹介します。
GitHubの以下のリポジトリにコードが載ってます。
なぜ普通には書けないのか
SFPのEEPROMはI2Cで読み書きします。アドレス構成はこんな感じ:
| アドレス | ページ | 主な内容 |
|---|---|---|
0x50 (A0h) | 主EEPROM | ベンダー名・型番・compliance code等 |
0x51 (A2h) | 診断ページ | 温度・電流・パスワード領域 |
じゃあ i2c-tools で叩けばいいじゃんと思う方も居ると思いますが、そうはいかない。
ethtool -m でEEPROMを読み出すことはできるが、書き込みAPIは存在しないからですね。
i2cdetect -l を叩いてもSFPのI2Cバスは /dev/i2c-* に出てこない。ixgbeドライバがチップ内蔵のI2Cマスターを占有しているため、ユーザーランドから直接触る手段がないっぽい(詳しくは検証してないけど)
従来の選択肢はだいたいこのどれか:
- 外部書き込み器 (CH341A, Nucleo, Raspberry Pi + ブレイクアウト基板)
- SFPモジュールを分解してEEPROM ICに直結
- ゴールドフィンガーにハンダ付け(やりたくない)
全部なにかしらのハード作業が必要で、非常に面倒くさい。
82599ESのI2CCTLレジスタ
Davis McCoy 氏の記事で知った事項だが、82599ESのBAR0には I2CCTL という32bitレジスタがある。オフセットは 0x028。
| bit | 名前 | 役割 |
|---|---|---|
| 0 | CLK_IN | SCL の現在値 (read) |
| 1 | CLK_OUT | SCL drive (0=drive low, 1=release→pull-upでhigh) |
| 2 | DATA_IN | SDA の現在値 (read) |
| 3 | DATA_OUT | SDA drive (0=drive low, 1=release) |
I2Cコントローラの実装はなく、4bitのGPIOが生えているだけ。START/STOP/ACKはすべてソフトウェアでbit-bangする必要がある。
これに /sys/bus/pci/devices/<BDF>/resource0 をmmap()することでユーザー空間から直接アクセスできる。ポイントは ixgbeを動かしたまま相乗りできること。ixgbeがPCIプローブ時にPHY電源を入れてSFPケージのI2Cバスを生かしてくれるので、そこに乗っかる形になる。
ixgbeが定期的にSFP EEPROMをポーリングしているのでトランザクション中に競合する可能性はあるが、ポーリング頻度は低く(数秒に1回)、1バイトのbit-bangは1ms以下で終わるので、実用上は問題ないはず。万全を期すなら事前に ip link set <iface> down しておくのが無難かな〜。
前提条件の確認
NICの確認
lspci | grep -iE '82599|Ethernet'
01:00.0 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)
01:00.1 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)
Intel X520ブランドは82599チップ搭載なのでOK。X540/X550はBAR0オフセットが違う可能性があって未検証です。
BAR0アドレスの確認
lspci -vv -s 01:00.0 | grep -iE 'region 0|memory at'
# Region 0: Memory at bf800000 (64-bit, prefetchable) [size=512K]
カーネルlockdownの確認
cat /sys/kernel/security/lockdown 2>/dev/null
# [none] integrity confidentiality ← [none] が現在のモードならOK
SecureBoot + kernel lockdownが有効だと /dev/mem 経由は制限されるが、今回は resource0 経由なので影響なし。CONFIG_STRICT_DEVMEM=y でも動きます。
対象ポートの選び方
書き換え対象は未使用ポートを選ぶ。本番通信中のポート(bondやvmbrのSLAVEになっている)は絶対に触らないようにしましょう。
ip link show enp1s0f0
ip link show enp1s0f1
bridge link show
DOWN ステートのポート or SFP+が未挿入のポートが理想。そも本番Proxmoxで作業するなって話ではある。
ツールの実装
レジスタアクセス部分
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <time.h>
#define I2CCTL_OFFSET 0x00000028
#define I2CCTL_CLK_IN (1U << 0)
#define I2CCTL_CLK_OUT (1U << 1)
#define I2CCTL_DATA_IN (1U << 2)
#define I2CCTL_DATA_OUT (1U << 3)
#define I2C_DELAY_US 5 /* 5us half-period → ~100kHz */
mmap部分
int fd = open("/sys/bus/pci/devices/0000:01:00.1/resource0",
O_RDWR | O_SYNC);
volatile uint8_t *bar0 = mmap(NULL, 0x80000,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
/sys/bus/pci/devices/<BDF>/resource0 を O_RDWR で開いてmmapします。ライブラリ依存なしでlibcだけです。
SCL/SDAのbit-bang
static void i2c_set_scl(int high) {
uint32_t v = *(volatile uint32_t*)(bar0 + I2CCTL_OFFSET);
if (high) v |= I2CCTL_CLK_OUT;
else v &= ~I2CCTL_CLK_OUT;
*(volatile uint32_t*)(bar0 + I2CCTL_OFFSET) = v;
}
SDAも同様に I2CCTL_DATA_OUT を操作する。CLK_IN / DATA_IN を読むことで、バスの現在状態を確認できる(はず)。
バイト送信
static int i2c_write_byte(uint8_t b) {
for (int i = 7; i >= 0; i--) {
i2c_write_bit((b >> i) & 1);
}
return i2c_read_bit(); /* ACK */
}
I2Cタイミングは100kHz程度が安全だと勝手に思ってます。プルアップが弱い場合は I2C_DELAY_US を10usや20usに増やす。1MHzとかを狙うと波形が丸まってダメなことが多いです。
主な関数構成
pci_mmap_bar0() : resource0 を mmap
i2c_set_scl/sda() : CLK_OUT / DATA_OUT を制御
i2c_get_scl/sda() : CLK_IN / DATA_IN を読む
i2c_start/stop() : START/STOP condition
i2c_write_bit/byte() : ビット/バイト送信、ACK受信
i2c_read_bit/byte() : ビット/バイト受信、ACK送信
sfp_read() : SFPからのNバイト読み出し
sfp_write_byte() : SFPへの1バイト書き込み+書き込み待ち
書き込み検証
if (sfp_write_byte(SFP_ADDR_A0, off, val) < 0) return -1;
uint8_t after;
sfp_read(SFP_ADDR_A0, off, &after, 1);
if (after != val) {
/* write-protectedの可能性 */
return -1;
}
EEPROMは内部書き込みサイクルが5〜10ms あるので、書き込み後は必ずウェイトを入れる。
使い方
ビルド
apt install -y gcc # Proxmoxはデフォルトで入ってない
gcc -O2 -Wall -o sfp_eeprom_82599 sfp_eeprom_82599.c
CLIコマンド一覧
| コマンド | 内容 |
|---|---|
scan | I2Cアドレス 0x08-0x77 を総当たり |
read <off> | A0hの1バイト読み出し |
read-range <off> <len> | A0hを指定長読み出し |
write <off> <val> | A0hの1バイト書き込み(読み戻し検証付き) |
dump | A0hの256バイト全ダンプ |
I2Cスキャン
sudo ./sfp_eeprom_82599 0000:01:00.1 scan
[info] BAR0 mapped at 0x7f..., size=0x80000
Found device at 0x50
Found device at 0x51
Found device at 0x52
Found device at 0x53
0x50/0x51 がA0hのwrite/readアドレス、0x52/0x53 がA2h。4個見えてるけど実態は2デバイス。
dumpとethtool -mの照合
sudo ./sfp_eeprom_82599 0000:01:00.1 dump
sudo ethtool -m enp1s0f1 raw on length 256 | xxd
両者が一致していればI2Cタイミングに問題なし。
書き込みテスト(非破壊)
Byte 0x5E は多くのSFPで未使用スペース。ここに書いて戻すことでwrite protectionの有無を確認できる。
sudo ./sfp_eeprom_82599 0000:01:00.1 read 0x5e
# 0x5e = 0x00
sudo ./sfp_eeprom_82599 0000:01:00.1 write 0x5e 0x01
# OK: write verified
sudo ./sfp_eeprom_82599 0000:01:00.1 write 0x5e 0x00
WARN: wrote 0x01 but read 0x00 (write-protected?) が出たら A2h のパスワード領域(0x7B-0x7E)に書き込みが必要なSFPです。それか書き込みENピンがICについてて、ハード上で書き込みが無効化されている可能性もあります。
本番書き換えバッチ
#!/bin/bash
set -e
PCI=0000:01:00.1
PROG=./sfp_eeprom_82599
# バックアップ(必須)
ethtool -m enp1s0f1 raw on length 256 > backup_$(date +%s).bin
# 書き換え
$PROG $PCI write 0x03 0x01 # 例: 1X Copper Passive bit を立てる
$PROG $PCI write 0x3f 0x81 # CC_BASE を追従
# 確認
$PROG $PCI dump
ethtool -m enp1s0f1 | grep -E 'Transceiver type|Vendor name'
ロールバックは逆の値を書けばOK。
チェックサムの再計算
SFF-8472にはチェックサムが2つある。どちらもベンダー固有領域(0x60-0xFF)は計算対象外。
CC_BASE(byte 0x3F)
cc_base = sum(data[0x00:0x3F]) & 0xFF
ベンダー名・PN・OUI等を書き換えた場合は再計算が必要。これを忘れるとスイッチに弾かれることがある。
CC_EXT(byte 0x5F)
cc_ext = sum(data[0x40:0x5F]) & 0xFF
Vendor SN / Date codeを書き換えた場合に再計算が必要。
注意点
BAR0のmmapが失敗する
open(.../resource0) failed: Permission denied
→ sudo で実行する。
全部0xFFが返る
SDAが常に浮いている(プルアップが見えてない)可能性があります。SFPが挿さっていないか、タイミングが速すぎるので I2C_DELAY_US を増やしてみましょう。
ACKが返らない
SFP/DACが挿さっていないか、タイミング問題かixgbeとの競合。リトライを実装しておくと楽。
書き込みが反映されない
write protectionがかかっているSFPは A2h の 0x7B-0x7E にパスワードを書く必要があります。これに関してはベンダー問い合わせ案件です(絶対教えてくれないと思うけど)。あとはEEPROM書き込みサイクル待ちが足りていない可能性もある。EEPROM_WRITE_MS を10ms→20msに増やしたりしたらいいんじゃないでしょうかね。
書き換え後にethtool -mが更新されない
モジュールを抜き差しするかリンクを落として上げれば反映される。
ip link set enp1s0f1 down && ip link set enp1s0f1 up
SFF-8472 メモリマップ早見表
| Offset | 範囲 | 意味 |
|---|---|---|
| 0x00 | 1 | Identifier (0x03 = SFP/SFP+) |
| 0x02 | 1 | Connector type (0x07=LC, 0x21=Copper Pigtail) |
| 0x03-0x0A | 8 | Transceiver Compliance Codes |
| 0x14-0x23 | 16 | Vendor Name (ASCII) |
| 0x25-0x27 | 3 | Vendor OUI |
| 0x28-0x37 | 16 | Vendor PN (ASCII) |
| 0x3F | 1 | CC_BASE(チェックサム) |
| 0x44-0x53 | 16 | Vendor SN (ASCII) |
| 0x54-0x5B | 8 | Date code (YYMMDD) |
| 0x5F | 1 | CC_EXT(チェックサム) |
| 0x60-0x7F | 32 | Vendor Specific |
書き換え用途ごとのターゲット:
- ベンダーロック回避 →
0x14-0x23(Vendor Name)と0x28-0x37(Vendor PN) - OUIだけ変更 →
0x25-0x27 - Transceiver Typeを変える →
0x03(Compliance Codes)
どのケースでもCC_BASE / CC_EXTの再計算を忘れずに。