Python と mmapio モジュール で UIO を制御


## はじめに

### UIO とは

UIO はユーザー空間でデバイスドライバを作成する仕組みです。

ユーザー空間で UIO を利用する際は、/dev/uio0 を open して mmap すると、デバイスのレジスタ空間が見えます。

### mmapio モジュールとは

mmapio モジュールは低レベルな I/O アクセスを行うための C で記述した Python モジュールです。詳細は以下の記事を参考にしてください。

  * [『メモリマップド I/O レジスタにアクセスする Python モジュール を C で記述』@Qiita](https://qiita.com/ikwzm/items/c9d13045aac50e32ea35)

この記事で紹介する mmapio モジュールと uio モジュールは以下の URL で公開しています。

  * https://github.com/ikwzm/python-mmapio

この記事では、mmapio モジュールを使って UIO のデバイスレジスタをアクセスする方法を示します。

## Uio クラス

python-mmapio は以下のようなディレクトリの構成になっています。Python パッケージの名前は mmapio です。

 * `setup.py`
 * `mmapio/`
   - `__init__.py`
   - `mmapio.c`
   - `uio.py`

Uio クラスは uio モジュール(uio.py)にあります。この章では Uio クラスについて説明します。

### 必要なライブラリをインポート

Uio クラスでは mmap、os、glob、re パッケージを使うのでインポートします。また、mmapio モジュールから MemoryMappedIO クラスをインポートします。

```Python:mmapio/uio.py
import mmap
import os
import glob
import re
from .mmapio import MemoryMappedIO
```

### 初期化メソッド

uio は、通常、```'uio0'``` や ```'uio1'``` などの uio + デバイス番号 みたいなのがデバイス名になります。しかし uio が多くなると、このようなデバイス名では使いたい uio デバイスを特定するのが困難になります。通常 uio には、```'uio0'``` などのデバイス名の他に、デバイスツリーなどで指定した名前を持っています。
例えば次のデバイスツリーで uio を作成すると、

```devicetree
/dts-v1/; /plugin/;
/ {
	fragment@0 {
		target-path = "/amba_pl@0";
		#address-cells = <2>;
		#size-cells = <2>;

		__overlay__ {
			#address-cells = <2>;
			#size-cells = <2>;

			negative-uio {
				compatible = "generic-uio";
				reg = <0x0 0xA0010000 0x0 0x10000>;
				interrupt-parent = <&gic>;
				interrupts = <0 89 4>;
			};
		};
	} ;
} ;
```

/sys/class/uio/uioX/name (uioのデバイス番号Xは任意の整数)に デバイスツリーのノード名(この例では negative-uio)がセットされます。

```console
shell$ cat /sys/class/uio/uio4/name
negative-uio
shell$
```

そこで、初期化時に 引数 name で名前が指定された場合はその名前を持つ uio を検索して初期しています。

```Python:uio.py
class Uio:
    """A simple uio class"""

    @staticmethod
    def find_device_by_name(name):
        device_name = None
        r = re.compile("/sys/class/uio/(.*)/name")
        for uio_name_file in glob.glob("/sys/class/uio/uio*/name"):
            f = open(uio_name_file, "r")
            uio_name = f.readline().strip()
            if uio_name == name:
                m = r.match(uio_name_file)
                device_name = m.group(1)
            f.close()
        return device_name
        
    def __init__(self, name=None, device_name=None):
        if   device_name != None:
            self.device_name = device_name
            self.name        = self.read_class_attribute("name")
        elif name != None:
            self.name        = name
            self.device_name = Uio.find_device_by_name(self.name)
            if self.device_name == None:
                raise ValueError("[{}.(name={})] Invalid device_name={} ".format(self.__class__.__name__, name, self.device_name))
        self.device_file = os.open('/dev/%s' % self.device_name, os.O_RDWR | os.O_SYNC)
        self.memmap_dict = {}

```


### 割り込み関連のメソッド

UIO にはデバイスドライバに直接ライト/リードすることで割り込みを制御出来ます。詳しくは次の記事を参照してください。

 * [「UIO(User space IO)の割り込みの使い方の例」@Qiita](https://qiita.com/ikwzm/items/b22592c31cdbb9ab6cf7)


```Python:uio.py
    def irq_on(self):
        os.write(self.device_file, bytes([1, 0, 0, 0]))

    def irq_off(self):
        os.write(self.device_file, bytes([0, 0, 0, 0]))
        
    def wait_irq(self, timeout=None):
        ready, _, _ = select.select([self.device_file], [], [], timeout)
        if not ready:
            return None
        else:
            bytes = os.read(self.device_file, 4)
            count = int.from_bytes(bytes, byteorder="little", signed=True)
            return count
```

### マッピング情報関連のメソッド

UIO が reg プロパティで指定するレジスタ空間(アドレスとサイズの組)は複数指定することが出来ます。
例えば次のデバイスツリーで UIO を作成すると、この UIO は二つのレジスタ空間を持っています。

```devicetree
	#address-cells = <2>;
	#size-cells = <2>;
 
	uio_sample {
		compatible  = "generic-uio";
		reg = <0x04 0x00000000 0x0 0x00010000
		       0x04 0x00010000 0x0 0x00010000>;
	};
```

これらのレジスタ空間のマッピング情報は /sys/class/uio/uioX/maps/ (uioのデバイス番号Xは任意の整数) を読むことで得られます。

```console
shell$ cat /sys/class/uio/uioX/maps/map0/addr
0x400000000
shell$ cat /sys/class/uio/uioX/maps/map0/size
0x10000
shell$ cat /sys/class/uio/uioX/maps/map1/addr
0x400010000
shell$ cat /sys/class/uio/uioX/maps/map1/size
0x10000
```

そこで、これらのレジスタ空間のマッピング情報を得る関数を定義します。

```Python:uio.py
    def class_attribute_file_name(self, name):
        return '/sys/class/uio/%s/%s' % (self.device_name, name)

    def read_class_attribute(self, name):
        file_name = self.class_attribute_file_name(name)
        with open(file_name, "r") as file:
            return file.readline().strip()
        return None
        
    def read_class_integer(self, name):
        file_name = self.class_attribute_file_name(name)
        with open(file_name, "r") as file:
            value = file.readline().strip()
        if   value.startswith("0x") or value.startswith("0X"):
            return int(value, 16)
        elif value.isdigit():
            return int(value, 10)
        raise ValueError("[{}.read_class_integer(name={})] Invalid value {} ".format(self.__class__.__name__, name, value))
        
    def get_map_addr(self, index=0):
        return self.read_class_integer('maps/map%d/addr'   % index)

    def get_map_size(self, index=0):
        return self.read_class_integer('maps/map%d/size'   % index)

    def get_map_offset(self, index=0):
        return self.read_class_integer('maps/map%d/offset' % index)

    def get_map_info(self, index=0):
        map_addr   = self.get_map_addr(index)
        map_size   = self.get_map_size(index)
        map_offset = self.get_map_offset(index)
        return dict(addr=map_addr, size=map_size, offset=map_offset)
```

### レジスタアクセス用のオブジェクトを生成

UIO のデバイスレジスタをアクセスするための ```MemoryMappedIO``` クラスのインスタンスを生成します。```MemoryMappedIO``` に関しては[『メモリマップド I/O レジスタにアクセスする Python モジュール を C で記述』](https://qiita.com/ikwzm/items/c9d13045aac50e32ea35)を参照してください。

インスタンスを生成する際、前節で説明した複数のレジスタ空間を持つ場合にどのレジスタ空間かを指定する index 、マッピングするレジスタ空間の開始アドレスを指定する offset、マッピングするレジスタ空間の大きさを指定する length を指定することが出来ます。

レジスタ空間をユーザーからアクセス出来るように memmap() を使ってユーザー空間にマッピングします。
ここで少々面倒なのが、マッピングする際のサイズのアライメントです。memmap() で指定するサイズは、ページ単位で割り切れなければなりません。

また、memmap() を使ってユーザー空間にマッピングされた先頭アドレスはページ単位でアライメントされています。もともとのレジスタ空間の開始アドレスがページ単位でアライメントされていない場合には、その分補正が必要です。

UIO の仕様では、memmap() で指定するオフセットは、複数のレジスタ空間を持つ場合にどのレジスタ空間かを指定する index にページサイズを乗じた値を指定します。これにより、どのレジスタ空間をマッピングするかを特定します。

```Python:uio.py
    def regs(self, index=0, offset=0, length=None):
        if index in self.memmap_dict.keys():
            memmap_info = self.memmap_dict[index]
            memmap      = memmap_info['memmap']
            mmap_offset = memmap_info['offset']
            mmap_size   = memmap_info['size']
            mmap_addr   = memmap_info['addr']
        else:
            page_size   = os.sysconf("SC_PAGE_SIZE")
            map_info    = self.get_map_info(index)
            mmap_addr   = map_info['addr']
            mmap_offset = ((map_info['addr'] + map_info['offset'])        ) % page_size
            mmap_size   = ((map_info['size'] + page_size - 1) // page_size) * page_size
            memmap      = mmap.mmap(self.device_file,
                                    mmap_size,
                                    mmap.MAP_SHARED,
                                    mmap.PROT_READ | mmap.PROT_WRITE,
                                    index*page_size)
            memmap_info = {'memmap': memmap, 'size': mmap_size, 'addr': mmap_addr, 'offset': mmap_offset}
            self.memmap_dict[index] = memmap_info
        regs_offset = mmap_offset + offset
        if   length == None:
            regs_length = mmap_size - regs_offset
        elif regs_offset + length <= mmap_size:
            regs_length = length
        else:
            raise ValueError("[{}.regs(index={},offset={},length={})] region range error ".format(self.__class__.__name__, index, offset, length))
        return MemoryMappedIO(memmap, regs_offset, regs_length)
```

## 使用例

```Python:sample.py

from mmapio import Uio

class Sample:

    START_REGS  = 0x00
    ARG0_REGS   = 0x04
    ARG1_REGS   = 0x08
    RETURN_REGS = 0x0C

    def __init__(self)
        self.uio  = Uio('negative-uio')
        self.regs = self.uio.regs()
    
    def func(self, arg0, arg1)
        self.regs.write_word(Sample.ARG0_REGS , arg0)
        self.regs.write_word(Sample.ARG1_REGS , arg1)
        self.regs.write_byte(Sample.START_REGS, 1)
        self.uio.irq_on()
        self.uio.wait_irq()
        return self.regs.read_word(Sample.RETURN_REGS)

if __name__ == '__main__':
    sample = Sample()
    for x in range (0,9):
        for y in range (0,9):
            print ("func({0},{1}) => {2}".format(x, y, sample.func(x,y)))

```

## 参考

  * [『メモリマップド I/O レジスタにアクセスする Python モジュール を C で記述』@Qiita](https://qiita.com/ikwzm/items/c9d13045aac50e32ea35)
  * [『Python と Numpy で UIO を制御』@Qiita](https://qiita.com/ikwzm/items/d7559858d099c86c350c)
  * [『Python の mmap.read メソッドを使って arm64 の非キャッシュ領域をアクセスするときの挙動について』@Qiita](https://qiita.com/ikwzm/items/e048f94d9aef4c12e39c)