Python と Numpy で UIO を制御


## はじめに

### UIO とは

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

ユーザー空間で UIO を利用する際は、/dev/uio0 を open して mmap すると、デバイスのレジスタ空間が見えます。この記事では、Python と numpy を使って UIO のデバイスレジスタをアクセスする方法を示します。

## 2025年1月28日 追記

numpy を使う方法は手軽で良いのですが、筆者は numpy 本来の目的と異なる使い方をしているのではないかという疑念を持っています。このまま numpy に頼りつづけるのは少々不安なので、numpy を使わずに済むように、低レベルな I/O アクセスを行うための C で記述した Python モジュールを作りました。詳細は以下の記事を参照してください。

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


## Uio クラス

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

Uio クラスでは numpy、mmap、os、glob、re パッケージを使うのでインポートします。

```Python:uio.py
import numpy as np
import mmap
import os
import glob
import re
```

### 初期化メソッド

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$
```

そこで、初期化時には名前を指定して、その名前を持つ uio を検索して初期しています。

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

    @staticmethod
    def find_device_file(device_name):
        device_file = None
        r = re.compile("/sys/class/uio/(.*)/name")
        for uio_device_name_file in glob.glob("/sys/class/uio/uio*/name"):
            f = open(uio_device_name_file, "r")
            uio_device_name = f.readline().strip()
            if uio_device_name == device_name:
                m = r.match(uio_device_name_file)
                device_file = m.group(1)
            f.close()
        return device_file
        
    def __init__(self, name):
        self.name        = name
        self.device_name = Uio.find_device_file(self.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):
        os.read(self.device_file, 4)
```

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

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 sys_class_file_name(self, name):
        return '/sys/class/uio/%s/%s' % (self.device_name, name)

    def read_class_integer(self, name):
        file_name = self.sys_class_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("Invalid value %s in %s " % (value, file_name))
        
    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 のデバイスレジスタをアクセスするための ```Uio.Regs``` クラスのインスタンスを生成します。```Uio.Regs``` に関しては次節を参照してください。

インスタンスを生成する際、前節で説明した複数のレジスタ空間を持つ場合にどのレジスタ空間かを指定する 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("region range error")
        return Uio.Regs(memmap, regs_offset, regs_length)
```

## Uio.Regs クラス

mmap.mmap() でユーザー空間にマッピングしているレジスタ空間を numpy の frombuffer() を使って numpy の配列として見えるようにします。レジスタへのアクセスは、ここで作った numpy の配列にアクセスすることで行います。

```Python:uio.py
    class Regs:
        
        def __init__(self, memmap, offset, length):
            self.memmap = memmap
            self.offset = offset
            self.length = length
            self.alloc_word_array()
            self.alloc_byte_array()
            
        def alloc_word_array(self):
            self.word_array  = np.frombuffer(self.memmap, np.uint32, self.length>>2, self.offset)

        def alloc_byte_array(self):
            self.byte_array  = np.frombuffer(self.memmap, np.uint8 , self.length>>0, self.offset)

        def read_word(self, offset):
            return int(self.word_array[offset>>2])

        def read_byte(self, offset):
            return int(self.byte_array[offset>>0])

        def write_word(self, offset, data):
            self.word_array[offset>>2] = np.uint32(data)

        def write_byte(self, offset, data):
            self.byte_array[offset>>0] = np.uint8(data)

```

### 補足(注意事項)

レジスタのアクセスの際は、この例のように numpy の配列を使ってください。
Python の mmap オブジェクトの read/write メソッドは使わないでください。
詳細は次の記事を参照してください。

 *[『Python の mmap.read メソッドを使って arm64 の非キャッシュ領域をアクセスするときの挙動について』@Qiita](https://qiita.com/ikwzm/items/e048f94d9aef4c12e39c)


## 使用例

```Python:sample.py

from uio     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)))

```