X470D4U に OpenBMC を移植する

はじめに

ASRock Rack から発売されている X470D4U に対して OpenBMC を移植した。

X470D4U は IPMI 機能を搭載しており、管理用イーサネットポート(またはホスト共有ポート)を通じて Web ベースの管理インターフェースにアクセスできる。 こうした機能は、各種ベンダーのサーバにも搭載されていて、Dell だったら iDRAC、HP だったら iLO などが一般のご家庭では利用されていることと思う。

X470D4U の IPMI 機能は自分で使うには必要十分で動作も軽快なので特に不満はないのだが、後継製品の発売によるものかファームウェア更新がされなくなってしまっている。 当然 IPMI 機能にも脆弱性は存在し得るので、できれば更新されたソフトウェアスタックを利用したい。実際にファーウェアファイルを binwalk でみてみると バージョン 3.14 ベースの Linux kernel を使っていることがわかる。 上述の理由はモチベーションの一つだが、単に OpenBMC が面白そうというのも多分にある。

OpenBMC は Linux Foundation による OSS の IPMI ソフトウェアスタックで、IBM のコードをベースに Facebook なども開発に参加しているらしい。 ちょっと検索すると IBM は Power サーバの IPMI に使っていて、Facebook は DC のサーバの IPMI に使っているようだ。

X470D4U に搭載されている BMC は Aspped の AST2500 で OpenBMC でよくサポートされているようだ。また ASRock のマザーボードはいくつかオフィシャルでサポートされている他、Renze Nicolai 氏によって今回移植を行う X470D4U の後継である X570D4U の移植もある。 更に検索したところ、X570D4U に対する移植を X470D4U で起動したレポート(中国語)もあった。 実際に試したところ、IPMI ファームウェアを保存するフラッシュのサイズが異なることで起動に失敗したが、サイズ指定を修正したところ起動に成功した。

この時点でほぼ目処がついているが、実際にはボードの違いにより電源管理、センサーなどの微調整が必要になる。

準備

シリアルコンソールへのアクセス

OpenBMC が利用しているブートローダである U-Boot とのやり取りやカーネルメッセージを閲覧するためにシリアルコンソールを用意する必要がある。

ボード上の DEBUG と印字されているピンヘッダがあるので、ここに 3.3V レベルのシリアルケーブルを接続する。 X570D4U の情報を Renze 氏が提供してくれていて、ピンアサインはこれと同一になっている。

シリアル接続に詳しくなかったのでいろいろ検索したが、ピンとケーブルの TX と RX をクロスして接続し、GND 同士はそのまま接続する。(GND の接続はなくても大丈夫かもしれないが GND を接続すると電位が安定するのだと思う) 3.3V は対向に電圧を供給するために使うようで接続は不要らしい。

フラッシュの書き込み方

X470D4U に搭載されているのは winbond の 25Q256JVFQ というフラッシュメモリで、SOP 16 300 mil のパッケージのものが使われている。 このフラッシュは観音開きのソケットに収められていて、ソケットを開けるとそのまま取り外すことができる。 フラッシュの書き込みのために適当なプログラマーとソケットを購入しておくと良い。

また先の Renze 氏のページにあるフラッシュ書き込み用のピンヘッダーが X470D4U にもあるのでこちらを使うと、 フラッシュを取り外さずに書き込みができるようだ。 ただこのピンヘッダはハーフピッチ(1.27 mm)で接続するための適当なコネクタが見つけられなかったので実際には使っていない。

実は物理的なプログラマーを利用せずにフラッシュの書き込みができ、実際にはこちらの方法を利用した。 AST2500 はホスト側から任意のアドレスにアクセスするという機能(脆弱性と言われてしまったが)があり、これを使うとホスト側からフラッシュの書き換えができる。 このツールは ASRock のサポートサイトからバイナリをダウンロードできる。

ビルドと書き込み

コードは次のリポジトリx470d4u ブランチに配置してある。

https://github.com/toshipp/openbmc

上記のリポジトリをクローンしたあと、次のようにビルドを行う。

cd ${PATH_TO_REPO}
. ./setup x470d4u ${PATH_TO_BUILD}
bitbake obmc-phosphor-image

ビルドは時間がかかる上にディスクも大量に消費するので注意すること。

ビルドしたアーティファクトは、${PATH_TO_BUILD}/tmp/deploy/images/x470d4u 以下に出力される。 フラッシュに書き込む raw イメージは、image-bmc ファイル(シンボリックリンク)になるのでこれをホストマシンにコピーして socflash を使って書き込む。

socflash_x64 -s image-bmc

書き込む前には、オリジナルイメージを以下のようにバックアップしておくのが良い。

socflash_x64 -b orig.img

書き込みが終わると自動的に BMC がリスタートするが、デバイスの状態が以前起動していたファームウェアによってされていた設定のままの場合があるので、一旦電源を落として起動するのが良い。

次からは、Web インターフェースの Firmware アップデート画面から更新ができる。 このときに選択するファイルは、obmc-phosphor-image-x470d4u.static.mtd.tar になるので注意する。

移植作業

すでにサポートされなくなってしまったみたいだが、移植作業の流れはドキュメントに記載されているので、これを参考に行った。

既存のシステムをコピーして始めるのが良いらしく、今回は同じく ASRock から発売されている E3C246D4I-2T をコピー元とした。

カーネル

X470D4U に必要となるカーネル機能は upstream に入っているので、カーネルに対する特別な開発は必要ない。 ただし、ボードごとにどのようなデバイスが搭載されているか、どのアドレスに設定されているかが異なるためにその設定を行う必要がある。 このような設定は、デバイスツリーを利用して行う。

OpenBMC ではデバイスツリーは Linux kernel のリポジトリに入れる方針になっているが、X470D4U の dts ファイルは現状存在しないので、OpenBMC 側の recipes-kernel に dts ファイルを追加する。 このファイルは Linux 側に含まれている aspeed-g5.dtsi を継承して、必要な部分だけ設定するようになっている。

いくつかの設定については後述するとして、一部の設定を説明する。 プレフィックス& がつくノードは、ラベルを参照していて、aspeed-g5.dtsi に定義されているノードを上書きしている。

  • chosen ノードは標準出力先の設定とカーネルパラメータを指定している。
  • memory ノードは BMC に搭載されたメモリの物理アドレスとサイズを指定している。
  • bmc-ready ノードでは J0 ピン の GPIO の出力設定を行っている。このピンに電圧をかけていないと BIOS 起動が遅くなる。
  • &fmc ノードでは BMC ファームウェアが保存されているフラッシュの設定を行っている。データシート的にはデフォルトでも問題ないはずだが、やや不安定になる感じがあるので、spi-max-frequency をデフォルトから下げている。
  • &uart5 ノードは BMC の DEBUG ヘッダにつながっているので有効にしている。
  • &mac0 ノードは BMC 専用イーサネットポートを表す。
  • &mac1 ノードはホストと共有しているイーサネットポートを表す。use-ncsi をつけることで、NC-SI を利用して通信ができるようになる。

BIOS ポストコード取得機能

OpenBMC では phosphor-host-postdというデーモンがホストの BIOS ポストコードの読み取りを行っている。

このデーモンが利用するデバイスを有効にするために、デバイスツリーで &lpc_snoop ノードを設定している。

SoL 機能

SoL は Serial over Lan の略で、シリアルコンソールを LAN 経由でアクセスするための機能のことをいう。

AST2500 では virtual UART という機能が存在していて、ホストと BMC がシリアルで接続されているように通信ができる。 ホスト側の BIOS にシリアル出力を SoL にリダイレクトするという設定があるので、これを有効にすることでホストのシリアル出力が BMC 側に転送される。

&vuart ノードでこの機能を有効にする。

KVM 機能

KVM はおそらく最も重要な機能の一つで、ネットワーク越しにホストの画面をみたりキーボード入力を行うことができる。

この機能は OpenBMC ではいくつかのコンポーネントが協調して実装されている。

ローレベルなところでは obmc-ikvmVNC プロトコルサーバとして動作している。 このサーバが AST2500 が提供しているビデオキャプチャ機能をつかってホストの画面を取得し、USB ガジェットドライバを利用してホスト側にキーボード入力やマウス入力を行う。

キャプチャ機能は &video ノードで、USB ガジェットドライバは &vhub ノードで有効にしている。

この VNC サーバは BMC のローカルホストの 5900 番ポートで通信を待ち受けている。

Web インターフェースからこのサーバを利用するために、bmcweb が websocket によるプロトコル変換を行っている。 また、ブラウザ上の VNC クライアントとして noVNCwebui-vue に組み込まれている。

余談だが、USB ガジェットではキーボード入力はスキャンコードでやり取りされるが、noVNC は Javascript なのでスキャンコードではなくて実際に入力される文字しか取得できない。(例えば J と入力したときに、shift + j を使う必要がある) そのため obmc-ikvm では入力文字からスキャンコードの組み合わせを変換しているのだが、これは英語キーボードを前提としているので、日本語キーボードを使っていると入力できない文字がある。この問題を解決するには現状パッチするしかないようだ。

インベントリ機能

IPMI の仕様は FRU 情報について定義していて、この情報は EEPROM などに保存されている。

X470D4U は i2c バスの下に EEPROM がぶら下がっている。eeprom ノードがこの EEPROM を定義している。

entity-manager は EEPROM から FRU 情報を読み取って D-Bus に情報を出力する。 entity-manager は後述する dbus-sensors とも協調し、設定ファイルの jsonExposes にセンサーに関する設定を記載すると、読み取った FRU 情報に合致した設定ファイルのセンサー定義を D-Bus に出力する。

これらの設定は次の json ファイルに記載している。

entity-manager に限らず OpenBMC は D-Bus を使って情報のやり取りをしているので、busctl を使うことでデバッグができる。 busctl listbusctl treebusctl introspect などを使うことで、各プログラムが出力する情報を確認できる。 また、各プログラムは D-Bus メソッドを提供していることもあるので、busctl call を呼び出すこともできる。

センサー機能

各種センサーは dbus-sensors が読み取って D-Bus に情報を出力する。

今回は電圧、ファンスピード、温度についてのセンサーの設定を行った。

電圧は、&adc ノードと iio-hwmon ノードを設定してドライバを設定している。 読み取った値がどこの電圧値なのかと実際の電圧値への変換係数は entity-manager の設定の ADC タイプで設定する。 どのインデックスがどこの電圧値なのかという情報は、公式ファームウェアの画面から推測できる。 また、公式ファームウェアSDR.dat というファイルが含まれるので、IPMI の使用を見つつ次のようなスクリプトを使うと、名前とインデックスの対応が取れる。 (インデックスと名前についての情報はおそらく著作権的には問題ないと思われるが、若干グレーかもしれない。)

import sys
data = open(sys.argv[1], "rb").read()
entries = data.split(b"\x5a\x40")
for e in entries:
    print(e[4-1])
    if e[13-1] == 2:
        idlen = e[48-1] & 0xf
        name = e[49-1:49-1+idlen]
        ownerid = e[6-1]
        ownerlun = e[7-1]
        sensnum = e[8-1]
        entid = e[9-1]
        entins = e[10-1]
        assert(ownerid == 32)
        assert(ownerlun == 0)
        if entid != 7:
            continue
        unit1 = e[21-1]
        unit2 = e[22-1]
        unit3 = e[23-1]
        m = e[25-1]
        assert(e[26-1] == 0)
        assert(e[27-1] == 0)
        assert(e[28-1] == 0)
        assert(e[29-1] == 0)
        Rexp = e[30-1] >> 4
        Bexp = e[30-1] & 0xf
        nominal = e[32-1]
        print(f"{name}, sn={sensnum}, entins={entins}, m={m}, {Rexp}, {Bexp}, {nominal}")

電圧の変換係数も公式ファームウェアの画面と hwmon の値からそれっぽい値を作って、 entity-manager のリポジトリに含まれる ASRock のボードの設定ファイルに書かれている近い値を設定した。

いくつかの電圧値はホスト側の電源が落ちていると 0 に近い値が出力される。そのようなものには "PowerState": "On" を設定して電源が入っているときだけ有効にしている。

またバッテリー電圧は、GPIO の G0 ピンをアサートしてやらないと正しい値が読み取れないらしい。 これは BridgeGpio によって設定できる。 この挙動がわからず悩んでいたが、ほかのボードの設定を見ていて気がついた。

ファンスピードは、&pwm_tacho ノードを設定することでドライバを設定している。 自分の環境では CPU ファンしかファンコントローラーに接続していないので、 regfan-tack-ch の値は間違っているかもしれない。

温度センサーは &i2c1 ノードの下に NCT6779 というチップがぶら下がっているらしい。 このチップはホスト側の電源を利用しているので、ホスト側が起動していないとドライバを読み込んでもデバイスが認識できない。 そのため、デバイスツリーでは設定せずに、entity-manager で "PowerState": "BiosPost" を設定して BIOS 起動が終わってから動的に初期化するようにしている。

ちなみに i2c バスのスキャンは i2cdetect コマンドを使って行えるが、実際に何があるかはわからないので、他のボードの設定やボードのマニュアルを見つつ推測した。 なにか良い方法があるのだろうか。

電源管理

電源管理は x86-power-control で行う。 このプログラムは、GPIO を使って電源オンオフやリセットを行う。 また、電源状態や、BIOS ポストの完了の取得も GPIO を使って行う。 つまり GPIO ピンアサインがわからないといけないのだが、マニュアルにはそのような情報が記載されていない。

とりあえず他のボードの設定を参考に、電源オンオフやリセットを試したところ正しく動いた。 電源状態や BIOS ポストの完了はどうもボードによって異なるらしいので、gpiomon コマンドを使って GPIO ピンの監視をしつつ電源をつけ消ししてそれっぽいところを使うようにした。

スクリーンショット

まとめ

今回の移植でおおよそ公式ファームウェアの機能と同等の機能を持った IPMI を OpenBMC で実現できた。

i2c デバイスや GPIOのピンアサインの特定さえできればそこまで作業量は多くないので公式の保守がなくなっていたり、OSS の IPMI が欲しい人は試してみると良いかもしれない。

ところで電源を GPIO でオンオフしたり、画面キャプチャしたり USB ガジェットを繋げば IPMI ってできちゃうからラズパイとかでも良さそうだよなと思ったら、そのものずばりの PiKVMというのがあるんですね…。