PCIe で GPIO しよう（今回は output のみ）

[さくらインターネット Advent Calendar 2019](https://qiita.com/advent-calendar/2019/sakura) 19日目 の記事です。

さくらインターネットの @homelith です。
日々生まれる「やりたいこと」を「できる」に変えてみた１記録です。

# 要約

* パソコンの PCIe ポート経由で、機器操作等に使いやすい簡易な信号入出力 (GPIO) を制御しようと思います。
* 安価なボードで、メモリマップ IO で手軽に叩ける方法を考えました。
* RTL8111E チップ搭載の PCIe ネットワークカードを使用することで、出力ピン 2 個を作り dd コマンドで制御できました。
* が、入力ピンは作れませんでした。残念です。

# 導入

* ある日のチャットで仕事場の仲間の発言がありました（掲載の本人許諾済）
![pibo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/965fe59f-fcf5-04e7-74c8-58f33bd0143d.png "ドライバがロードされたらピボッて鳴るだけの PCIe デバイスほしい")
    - PC-98 の起動音風の音とのこと（筆者は実機では聞いたことがありません）
    - 有志が製作した電源投入時に 1 回鳴るデバイスはあるものの、あくまで PC からの通信確立の上で能動的に鳴らしたいとのこと

* ちょうど PCIe 接続の LAN カードが手元にありました。
![cif-gbe2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/85653612-2d21-caa6-86a7-c86efc7c8816.jpeg "CIF-GBE2 ギガビットイーサネット増設ボード")
    - http://www.century.co.jp/products/cif-gbe2.html 多分このモデルと思われます。
    - 現在は終売している模様です。

* これを改造して簡単な信号入出力ボードにすれば、音を鳴らせるかも。
    - また、GPIO (General Purpose Input / Output) として他にも活用できるかも、と考えました。

　（注：機器の改造は製造メーカーによる製造保証を無くす他、発火等の危険を伴う行為です。
　　　　当記事は試験目的に限定しできるだけの安全が確保された環境で行ったものです。
　　　　当記事はあくまで読み物として読んでいただき、内容をご自身で試したりしない事をお勧めいたします。
　　　　当記事の方法等を用い何らかの損害を被られた場合も筆者・所属組織はその責を負いかねます。）

# 検討

* 使用チップは RTL8111E、低価格 PCIe Gigabit Ethernet NIC には高確率で搭載されているため、ノウハウが流用できる可能性が高そうです。大体のモデルが市価 1000 円～ 2500 円前後で販売されているようです。
![rtl8111.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/bbf590d1-d5f5-a4e3-049b-5504f46a82e9.jpeg "RTL8111E Chip")

* アップデートが繰り返されており、現行は [RTL8111G](https://www.realtek.com/en/products/communications-network-ics/item/rtl8111g) のようですが、概ねレジスタマップ等は似通っているのではないかと思います。

* https://github.com/badgerious/re_ledctl に LED の点灯パターンを変更するプログラムが公開されています。その内容やその他情報を読み込むと：
    - 制御レジスタオフセット 18h-19h の 16bit のうち、[11:0] の 12bit は点灯パターン（例えば、送信時には点滅したり、リンクモードに合わせて点灯・消灯したり）を設定するビットとなっています。これを全て 0 とすることで全 LED を消灯できます。
    - 同レジスタの [14:12] に 3 つの LED の正論理・負論理切り替えビットがあります。これを切り替えることで、消灯＝論理 High と 消灯＝論理 Low を切り替えられるので、実際には正論理・負論理切り替えビットの値に応じて LED が付いたり消えたりする、つまり信号出力として使えるはずです。

# やってみる

* Linux (Debian GNU/Linux 9 stretch, kernel 4.9) 上で実施しました。

### ハードウェア編

* このデバイスでは、下図中の黄色枠（一番上）の位置にGND、水色枠（上から２番目）の位置にオレンジ LED を制御する信号線、ピンク色枠（上から３番目）の位置に緑色 LED を制御する信号線が来ています。
![wiring.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/8c5e5445-ba30-6e5b-1e12-c9fc7ecf195d.jpeg "配線図")

* 信号の規格は 3.3V、スタンバイ時は常に High が出力されており、Low 出力で LED が点灯します。PC の電源が切れていてもスタンバイ電源で 3.3V がかけられている模様です。

* 適当なピンソケットを接着しはんだ付けすることで、必要な時はジャンパワイヤ等を使って PC 筐体の外にケーブルを引き出して使えるようにしました。（ブラケットに穴をあけてケーブルを通しました）

![cable_and_bracket.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/17de292d-d17b-7dd3-1bd9-6078a07045a7.jpeg "ケーブル接続とブラケット加工")

* 今回使う LED はどうもインタフェースがリンクアップしていないと操作できないようです。よって何か対向装置と LAN ケーブルでつないでリンクアップさせるか、下図のようなループバックアダプタを作ってリンクアップさせます。
![conn.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/84ff51b9-bd00-620c-8c45-6fa6b385a478.jpeg "RJ45 ループバックアダプタ")

* 当企画発案者が NUCLEO-F303K8 ベースで作った「ピボッ」音発生装置があるのでつなぎます。
    - A12 ピンの立ち上がりエッジトリガで1回発音します。
    - 音源の波形を分析して合成し、DAC で出力しているらしいです。

![buzzer.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216085/47821169-cdfe-5321-31eb-552486094f84.jpeg "「ピボッ」音発生装置")


### ソフトウェア編

* rtl8111e 用ドライバをアンロードします（システム内で同じドライバを使ったイーサネットポートがある場合、それら全てが使えなくなります）

```shell:シェル上で
$ sudo modprobe -r r8169
```

* PCIe のベースレジスタを探します

```shell:シェル上で
$ sudo lspci -vb
```
lspci の結果を読んで REALTEK 製デバイスの記述を見つけ、アドレス値をメモします。
今回は 0xf0004000 に第一領域、0xf0000000 に第二領域がマッピングされていました。

* LED の外部制御を有効にする

目的は RTL8111E のステータスレジスタ 0x50 の bit[7:6] を両方立て、LED のカスタム点灯制御を有効にすることです。
一旦該当レジスタを読み込み、読み込まれた値に 0xC0 を OR して書き込みます。

```shell:シェル上で
$ sudo dd if=/dev/mem bs=1 count=1 skip=4026548304 | xxd
1+0 レコード入力
1+0 レコード出力
1 byte copied, 0.00046646 s, 2.1 kB/s
00000000: 01                                       .

$ echo -en "\xC1" | sudo dd of=/dev/mem bs=1 count=1 seek=4026548304
1+0 レコード入力
1+0 レコード出力
1 byte copied, 0.000383355 s, 2.6 kB/s

$ sudo dd if=/dev/mem bs=1 count=1 skip=4026548304 | xxd
1+0 レコード入力
1+0 レコード出力
1 byte copied, 0.00046646 s, 2.1 kB/s
00000000: c1                                       .
```

skip / seek の値にはベースレジスタ第一領域のオフセットとステータスレジスタオフセット 0x50 を足した値、f0004050（環境によって変わります）を 10進数に変換したものを入れます。


* LED を操作する

```shell:シェル上で（出力は省略）
$ echo -en "\x00\x00" | sudo dd of=/dev/mem bs=2 count=1 seek=2013274124 # 両 LED とも消灯（信号レベル High）
$ echo -en "\x01\x00" | sudo dd of=/dev/mem bs=2 count=1 seek=2013274124 # LED 1 が点灯（信号レベル Low）
$ echo -en "\x02\x00" | sudo dd of=/dev/mem bs=2 count=1 seek=2013274124 # LED 2 が点灯（信号レベル Low）
$ echo -en "\x03\x00" | sudo dd of=/dev/mem bs=2 count=1 seek=2013274124 # 両 LED とも点灯（信号レベル Low）
```

skip / seek の値にはベースレジスタ第一領域のオフセットと LED 制御レジスタオフセット 0x18 を足した値、f0004018（環境によって変わります）を 10進数に変換し、2で割ったもの（ブロックサイズを 2byte としたため）を入れます。

なんとなく coreutils だけでやりたくなったので dd を使っていますが、devmem2 ツールなどを使うとより簡易に /dev/mem を読み書きできます。
参考：https://armadillo.atmark-techno.com/howto/accessing-any-address

* devmem2 ツールを使った場合のコマンド例

```shell:シェル上で（出力は省略）
$ sudo ./devmem2 0xf0004050 b 0xC1 # LED の外部制御有効化
$ sudo ./devmem2 0xf0004018 h 0x0100 # LED 1 点灯（信号レベル Low）
$ sudo ./devmem2 0xf0004018 h 0x0200 # LED 2 点灯（信号レベル Low）
```

# 結果

* 動作の様子
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">dd でレジスタを叩くと、PCIe LAN カードの LED 信号線経由で PC-98 風ビープ音がトリガされるものを作った（ビープ音発生装置製作・提供：<a href="https://twitter.com/misodengaku?ref_src=twsrc%5Etfw">@misodengaku</a>） <a href="https://t.co/5qhlSkCadX">pic.twitter.com/5qhlSkCadX</a></p>&mdash; homelith (@homelith) <a href="https://twitter.com/homelith/status/1207600364821696512?ref_src=twsrc%5Etfw">December 19, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

* とりあえずコマンド入力で鳴らすことはできました。
* カーネルモジュール等にしてシステム起動時にロードすると PC-98 風になるかもしれません。


# 感想と振り返り

* PCIe はシステムのメモリマップ I/O として見えるため簡単にアクセスできるはずなのですが、普通はその高速性を生かした複雑な機能のボード（グラフィックボードや各種アクセラレータ）とドライバの組で使用されるため、中々生で触れる機会が少ないものでした。
* PCIe 接続 NIC と、RTL8111E チップの LED 制御機能を使うことで、「簡単にアクセスできる」を実際に体感できました。
* 色々検討したのですが、入力ピンは作れませんでした。PCIe 経由の手軽な I/O に良さそうなデバイスを今後も探してみようと思います。

# 補足
* PCIe-RS232C 変換ボードを使って、シリアルポートとして叩くのが一番簡単かもしれません。
* /dev/mem 直接アクセス、がやってみたかったのでこれはこれで。