PCI ボードをデバイスドライバを用いずに python から操作する

# これは何か
PCI ボードをデバイスドライバに頼らず python を使って操作してみます。

### 環境、必要なもの
- Linux (root 権限が必要)
- python
    - [portio モジュール](http://portio.inrim.it/)
    - [mmap 標準モジュール](https://docs.python.jp/3/library/mmap.html)
- PCI ボードの I/O 関連資料


# 背景知識
- PCI : 周辺機器への通信規格
    - wikipedia : [Peripheral Component Interconnect](https://ja.wikipedia.org/wiki/Peripheral_Component_Interconnect)
- PCI 設定ヘッダ (PCI configuration header)
    - 機器の設定情報 (通信方式, 割り当てアドレス, ベンダ ID, 装置 ID, など)
    - PCI 設定空間を通してアクセス
        -  [lspci](https://linuxjm.osdn.jp/html/pciutils/man8/lspci.8.html) で参照できる
   - 参考資料
        - The Linux Kernel : [7. PCI](http://archive.linux.or.jp/JF/JFdocs/The-Linux-Kernel-7.html)
        - wikipedia : [PCI configuration space](https://en.wikipedia.org/wiki/PCI_configuration_space)
        - OS Dev.org : [PCI](http://wiki.osdev.org/PCI)
        - OS project : [Device/PCI](http://www.wiki.os-project.jp/?Device%2FPCI)
- PCI 設定空間 (PCI configuration space)
    - PCI 設定ヘッダへのアクセスを提供
- PCI I/O 空間
    - 機器へ直接通信する I/O のアドレス空間。低速。
    - sys/io.h 内に定義された関数 (inb, outb など; 実態はインラインアセンブリ) で CPU に直接指示してアクセス
    - python からは io.h の関数をラッパーした [portio モジュール](http://portio.inrim.it/) を使用可能
- PCI メモリ空間
    - 機器内のレジスタと対応づけられたメモリ空間
    - /dev/mem を open することでファイルとしてアクセス可能
    - 仮想アドレス空間を実メモリと対応づけるために、[mmap 関数](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/mmap.2.html) を使用する
    - python からは、[mmap 標準モジュール](https://docs.python.jp/3/library/mmap.html) を使用可能
- 参考になった情報
    - [デバイスドライバに頼らないハードウェア操作 - 東北学院大学 工学部 機械知能工学科 熊谷研究室](http://www.mech.tohoku-gakuin.ac.jp/rde/contents/linux/drivers/nodriver.html)

# 手順
## 1. 機器の情報を収集する
lspci を使って、設定ヘッダの情報を確認

```
$ lspci -v -nn
...
...
05:0c.0 Unassigned class [ff00]: Interface Corp Device [1147:0c5d] (rev 03)
	Subsystem: Interface Corp Device [1147:0001]
	Flags: slow devsel, IRQ 19
	I/O ports at 2000 [size=32]
	I/O ports at 2048 [size=4]

05:0d.0 Unassigned class [ff00]: Interface Corp Device [1147:0c8e] (rev 01)
	Subsystem: Interface Corp Device [1147:2090]
	Flags: slow devsel, IRQ 16
	Memory at 90502000 (32-bit, non-prefetchable) [size=64]
	Memory at 90501000 (32-bit, non-prefetchable) [size=64]
	Memory at 90503000 (32-bit, non-prefetchable) [size=32]
...
...
```

- オプション
    - -v : 通信方式、アドレス、を含む詳細を表示
    - -nn : ベンダ ID、装置 ID と共に、名称も表示

- 見方
    - 05:0c.0 に接続された Interface Corp (ID 0x1147) の装置 ID 0x0c5d です
        - 2 つの PCI I/O port を持っています
            - アドレス 0x2000 から 32 byte
            - アドレス 0x2048 から 4 byte
    - 05:0d.0 に接続された Interface Corp (ID 0x1147) の装置 ID 0x0c8e です
        - 3 つの PCI Memory port を持っています
            - アドレス 0x90502000 から 64 byte
            - アドレス 0x90501000 から 64 byte
            - アドレス 0x90503000 から 32 byte


## 2. PCI I/O port へ接続する

```
$ sudo ipython
```
PCI I/O port へのアクセスのために、ルート権限が必要です。

```
>>> import portio

# level を 3 に設定し、I/O ポートへのアクセスを許可します
>>> portio.iopl(3)   

# 0x2000 から 1 byte を読み出します
>>> portio.inb(0x2000)
254
```
- 注意点：
    - inb や、outb に渡す port を間違えると、誤った機器に命令を送りつけてしまい、危険です。


## 3. PCI メモリ port へ接続する

```
$ sudo ipython
```
/dev/mem へのアクセスのために、ルート権限が必要です。

```
>>> import os
>>> import mmap

# メモリを読込専用で開きます
>>> f = os.open('/dev/mem', os.O_RDONLY)

# mmap でマッピングします
>>> m = mmap.mmap(f, 32, prot=mmap.PROT_READ, offset=0x90503000)

# 7 byte 目を読んでみます
>>> m.seek(0x07)
>>> m.read(1)
b'\x00'
```
- 注意点：
    - mmap の offset に渡すアドレス値を間違えると、他のプロセスなどが使用中のメモリ領域へアクセスしてしまい、危険です。


# pypci モジュール

- 上記の手順だと、煩雑だし、アドレスを間違える危険性があるので、そこらへんをうまいことしてくれるモジュールを作りました。
- PCI I/O port, PCI メモリ port の違いによらず、統一した API で操作できます。
- [pypci モジュール](https://github.com/ars096/pypci)
    - インストール : ```pip install pypci```

## 使い方
```
>>> import pypci

# PCI 設定情報を収集します。
# ベンダー ID、装置 ID を使って絞り込みます。
>>> board = pypci.lspci(vendor=0x1147, device=3214)

# port に関する情報は、Base Address Register に書き込まれています。
>>> board[0].bar
[BaseAddressRegister(type='mem', addr=2421170176, size=64),
 BaseAddressRegister(type='mem', addr=2421166080, size=64),
 BaseAddressRegister(type='mem', addr=2421174272, size=32)]

# BaseAddressRegister オブジェクトを使って通信します
>>> bar2 = board[0].bar[2]


# 読み込みの例
>>> pypci.read(bar2, 0x0c, 4)
b'\x00\x00\x00\x0c'

# アドレス範囲を超えると、エラーです
>>> pypci.read(bar2, 0x1d, 4)
BadAccessError: PCI addr space is 0x90503000-0x9050301f while tried to access 0x9050301d-0x90503020.


# 書き込みの例
>>> pypci.write(bar[2], 0x04, b'\x01')

>>> data = struct.pack('<I', 1234567)
>>> pypci.write(bar[2], 0x00, data)
```