Proxmox (Intel 82599ES) からSFP+ EEPROMをbit-bangで書き換えるツールを書いた

目次

  1. はじめに
  2. なぜ普通には書けないのか
  3. 82599ESのI2CCTLレジスタ
  4. 前提条件の確認
  5. ツールの実装
  6. 使い方
  7. チェックサムの再計算
  8. 落とし穴
  9. SFF-8472 メモリマップ早見表
  10. 参考

はじめに

前の記事でx510のVCStackに汎用DACを通すためにSFP EEPROMを書き換えた話を書きました。 その中で、ツールの詳細は解説してなかったので今回はその実装について紹介します。

GitHubの以下のリポジトリにコードが載ってます。

hamuchan214/sfp-eeprom-bitbang

なぜ普通には書けないのか

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名前役割
0CLK_INSCL の現在値 (read)
1CLK_OUTSCL drive (0=drive low, 1=release→pull-upでhigh)
2DATA_INSDA の現在値 (read)
3DATA_OUTSDA 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>/resource0O_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コマンド一覧

コマンド内容
scanI2Cアドレス 0x08-0x77 を総当たり
read <off>A0hの1バイト読み出し
read-range <off> <len>A0hを指定長読み出し
write <off> <val>A0hの1バイト書き込み(読み戻し検証付き)
dumpA0hの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範囲意味
0x001Identifier (0x03 = SFP/SFP+)
0x021Connector type (0x07=LC, 0x21=Copper Pigtail)
0x03-0x0A8Transceiver Compliance Codes
0x14-0x2316Vendor Name (ASCII)
0x25-0x273Vendor OUI
0x28-0x3716Vendor PN (ASCII)
0x3F1CC_BASE(チェックサム)
0x44-0x5316Vendor SN (ASCII)
0x54-0x5B8Date code (YYMMDD)
0x5F1CC_EXT(チェックサム)
0x60-0x7F32Vendor Specific

書き換え用途ごとのターゲット:

  • ベンダーロック回避 → 0x14-0x23(Vendor Name)と 0x28-0x37(Vendor PN)
  • OUIだけ変更 → 0x25-0x27
  • Transceiver Typeを変える → 0x03(Compliance Codes)

どのケースでもCC_BASE / CC_EXTの再計算を忘れずに。


参考