ZYBO (Zynq) 初心者ガイド (17) Linuxで自作IPのデバイスドライバを作る

- [1回目: 開発環境の準備](https://qiita.com/take-iwiw/items/966f252f6ca954aff08b)
- [2回目: Hello Worldプロジェクト](https://qiita.com/take-iwiw/items/24a8a94741fdbb80f62a)
- [3回目: PSのGPIOでLチカ](https://qiita.com/take-iwiw/items/39d2ed00fa88a1204fce)
- [4回目: PLのAXI GPIOでPSからLチカ](https://qiita.com/take-iwiw/items/a3b935a4e4dbd41593e1)
- [5回目: PLだけでLチカ](https://qiita.com/take-iwiw/items/efcdfca1866b9941fd2a)
- [6回目: 自作IPでLチカ](https://qiita.com/take-iwiw/items/b0f2ce7b4653bc6f32b9)
- [7回目: ブートイメージを作る](https://qiita.com/take-iwiw/items/ece0cdf83e6e1908fad8)
- [8回目: Linux起動する](https://qiita.com/take-iwiw/items/6e6915f7318689818368)
- [9回目: Linuxカーネルを少しカスタマイズする](https://qiita.com/take-iwiw/items/6f2644d16cbfdb021d07)
- [10回目: LinuxのRootFSをカスタマイズする / PythonでHello World](https://qiita.com/take-iwiw/items/94a90047978664159681)
- [11回目: LinuxユーザアプリケーションでLチカ](https://qiita.com/take-iwiw/items/5e4ab9a106acfda2c866)
- [12回目: LinuxカーネルモジュールでLチカ](https://qiita.com/take-iwiw/items/e7efc48d92d7b979efdc)
- [13回目: LAN(Ethernet 0)を使う](https://qiita.com/take-iwiw/items/ac489acaca1ab54d7ce8)
- [14回目: Linuxユーザアプリをデバッグする / RootFSに取り込む](https://qiita.com/take-iwiw/items/8134ca7098622219b177)
- [15回目: Linux起動時にアプリケーションを自動実行させる](https://qiita.com/take-iwiw/items/50065df6cdd1ba8bcbd6)
- [16回目: Linuxから自作IPをUIOで制御する](https://qiita.com/take-iwiw/items/da91ce4dc2a8a8df3c0a)
- __[17回目: Linuxで自作IPのデバイスドライバを作る](https://qiita.com/take-iwiw/items/05eb1204cb51ce0a5c03) <--- 今回の内容__
- [18回目: IoT化してスマホからLチカ](https://qiita.com/take-iwiw/items/e9311292c5078d785699)

# 環境
- 開発用PC: Windows 10 64-bit 
	- Vivado 2017.4 WebPACKライセンス
	- Xilinx SDK 2017.4 <- 今回は未使用
- 開発用PC (Linux): Ubuntu 16.04 本家 (日本語版じゃない) (on VirtualBox 5.2.4)
	- PetaLinux 2017.4
- ターゲットボード: ZYBO (Z7-20) 

# Linuxで自作IPのデバイスドライバを作る
前回は自作のIPを、UIOを使用してユーザ空間から制御しました。今回は、カーネル空間で動くデバイスドライバを作ります。カーネルモジュールの作り方は[12回目: LinuxカーネルモジュールでLチカ](https://qiita.com/take-iwiw/items/e7efc48d92d7b979efdc)でやりましたが、今回は、デバイス情報をちゃんとデバイスツリーから取得します。また、open/close/read/writeといったインターフェースを持つちゃんとしたデバイスドライバを作ります。

(ちなみに、Xilinxのフォーラムを見る限り、Xilinxの人もわざわざデバイスドライバを作るよりも、UIOを使うことを推奨している雰囲気がありました。せっかく調べたので、この記事を書いていますが、僕も実際の開発ではUIOを使うと思います。)

# ハードウェアの準備
前回のプロジェクトを使う場合は、そのまま流用可能です。
使用するハードウェアは、[前回: Linuxから自作IPをUIOで制御する](https://qiita.com/take-iwiw/items/da91ce4dc2a8a8df3c0a)と同じものです。

- PSを配置 (Ethernet0のMDIO接続を修正)
- 自作IP(myip)を配置
	- レジスタの下位4-bitの値を出力するだけのIP
	- 出力先はLED (M14、M15、G14、D18)
	- レジスタアドレス = `0x43C00000`
	- 名前は`myip_0`

ビットストリーム付きのhdfファイルを出力して、Ubuntu側にコピーしておきます。(project_1.sdk/design_1_wrapper.hdf)

# Linuxイメージを作る
新規にプロジェクトを作る場合は、いつも通りの手順で、プロジェクトを作り、作成したハードウェアでコンフィグします。(プロジェクト名はMyIPとする)。

```sh:開発PCのターミナル
cd ~/work/peta
petalinux-create --type project --template zynq --name MyIP
cd MyIP/
petalinux-config --get-hw-description=../project_1.sdk
	# プロジェクトの設定は何も変えずにExit
petalinux-build
petalinux-package --boot --force --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/design_1_wrapper.bit --u-boot
```


## デバイスツリーを元に戻しておく (前回のプロジェクト流用の場合)
前回のプロジェクトをそのまま使っている場合、myip_0は`generic-io`として扱われます。`project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi`を元の状態に戻しておきます。

```txt:project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
/include/ "system-conf.dtsi"
/ {
};
```

これによって、myip_0は、`components/plnx_workspace/device-tree/device-tree-generation/pl.dtsi`で定義されている通り、`xlnx`社の`myip-1.0`というデバイスとして扱われます。本当のプロジェクトでは、これらの名前はVivado上で変更しておくべきです。(当然僕はXilinx社員ではありません。)　後で作成するデバイスドライバ内で、この名前が必要になります。

```txt:components/plnx_workspace/device-tree/device-tree-generation/pl.dtsi(編集不要)
        myip_0: myip@43c00000 {
            compatible = "xlnx,myip-1.0";
            reg = <0x43c00000 0x10000>;
            xlnx,s00-axi-addr-width = <0x4>;
            xlnx,s00-axi-data-width = <0x20>;
        };
```

## ZYBOでLinuxを起動しておく
出来上がったLinuxイメージでZYBOを起動しておきます。また、開発用PCとネットワーク接続できていることを確認しておきます。以後、ZYBOのIPアドレスを192.168.1.87として説明します。


# デバイスドライバを開発する
基本的には、[12回目: LinuxカーネルモジュールでLチカ](https://qiita.com/take-iwiw/items/e7efc48d92d7b979efdc)と同じです。が、このときは開発したカーネルモジュールをいきなりRootFSに取り込みました。実際の開発では、デバッグが必要なため、毎回イメージ作成なんてしていられません。今回は、そこらへんの手順も含めて紹介しようと思います。

## カーネルモジュールを作成する
今回、デバイスドライバはカーネルモジュールとして作成します。下記コマンドで、`mymodule`というカーネルモジュールを作成します。このカーネルモジュール=デバイスドライバなので、実際にはもっとそれっぽい名前にしてください (アンダースコア(`_`)は使えないです。大文字もダメっぽい)。ソースコードなどは、`project-spec/meta-user/recipes-modules/mymodule/`に作成されます。

```sh:開発PCのターミナル
petalinux-create -t modules --name mymodule --enable
petalinux-build -c rootfs
```

## カーネルモジュールの開発フロー (一例)
このフローは、僕オリジナルです。 __もっと良い方法がありましたら、ぜひ教えてください！！__
基本的な流れとしては、開発PC(Ubuntu)でカーネルモジュール(`mymodule.ko`)だけをビルドします。それを、ZYBOにscpでコピーして、動作確認/デバッグします。最終的にOKなものが出来たら、全体をビルドしてRootFSを作ります。

```sh:開発PCのターミナル
# 1. コーディングする
code project-spec/meta-user/recipes-modules/mymodule/files/mymodule.c &
# 2. mymoduleだけをビルドする
petalinux-build -c mymodule
# 3. ZYBOにコピー
scp build/tmp/sysroots/plnx_arm/lib/modules/4.9.0-xilinx-v2017.4/extra/mymodule.ko root@192.168.1.87:.
```

```sh:ZYBOのターミナル
# 4. 動作確認する.
insmod mymodule.ko
# 5. 終わったらアンロードしておく
rmmod mymodule
# または、modprobe -r mymodule
```

実際には、一発でOKになることはないので、動作確認後、「1. コーディングする」に戻る。完成したら、下記のコマンドでimage.ubを作る。

```sh:開発PCのターミナル
# 6. 完成したらパッケージ化して、image.ubを作る
petalinux-build -x package
```

毎回image.ubを作成するのに比べて、だいぶ効率的にできます。それでもまだ、時間がかかります。`petalinux-build -c mymodule`がモジュール単体ビルドのはずなのに、2, 3分かかる。。。

### known_hostsエラー回避
ZYBOのRootFSの展開先としてinitramfsを使用している場合、ZYBO起動のたびにsshキーが変わってしまいます。そのため、開発PCからscpやsshするときにエラーが出てしまいます。これは`~/.ssh/known_hosts`を削除することで回避できますが、毎回やるのは面倒です。以下設定をすることで、無視することが出来ます。(セキュリティにご注意ください)

```sh:開発PCのターミナル
echo "StrictHostKeyChecking no" >> ~/.ssh/config
echo "UserKnownHostsFile /dev/null"  >> ~/.ssh/config
chmod 600 ~/.ssh/config
```

## デバイスドライバを実装する
デバイスドライバの仕様は以下の通りとします。

- キャラクタ型
- write
	- "on": 全LED点灯
	- "off": 全LED消灯
	- その他: LEDを交互に点灯
- read
	- なにもしない
- レジスタアドレスなどのデバイス情報は、デバイスツリーから取得する (platform_driver)

### コード
自動生成されたmymodule.cに、platform_driverとして、デバイスツリーから情報(レジスタアドレスと割り込み番号)を取得するひな形が作られています。これを参考に、以下のようなコードを実装します。

```c:mymodule.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <asm/uaccess.h>

#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>

/* Standard module information, edit as appropriate */
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR
	("My Company");
MODULE_DESCRIPTION
	("mymodule - loadable module template generated by petalinux-create -t modules");

#define DRIVER_NAME "mymodule"

static const unsigned int MINOR_BASE = 0;   /* このデバイスドライバで使うマイナー番号の開始番号と個数(=デバイス数) */
static const unsigned int MINOR_NUM  = 1;   /* マイナー番号は 0 */

/* デバイスに紐づく情報。probe時に設定して、dev_set_drvdataで保持しておく */
struct mymodule_local {
	/* デバイスドライバの管理情報 */
	struct cdev   cdev;            /* probeされたデバイスとcdevを対応付けるために必要。open時にcontainer_ofで探す */
	unsigned int  mymodule_major;  /* このデバイスドライバのメジャー番号(動的に決める) */
	struct class  *mymodule_class;  /* デバイスドライバのクラスオブジェクト */

	/* デバイス情報 */
	unsigned long mem_start;
	unsigned long mem_end;
	void __iomem *base_addr;
};

#define REG(addr) (*((volatile unsigned int*)(addr)))


/* open時に呼ばれる関数 */
static int mymodule_open(struct inode *inode, struct file *file)
{
	printk("mymodule_open\n");

	/* このopenを持つcdev(inode->i_cdev)を含むmymodule_localのポインタ を探す (その中にあるデバイス情報を参照するため) */
	struct mymodule_local *lp;
	lp = container_of(inode->i_cdev, struct mymodule_local, cdev);
	if (lp  == NULL) {
		printk(KERN_ERR "container_of\n");
		return -EFAULT;
	}
	file->private_data = lp;

	return 0;
}

/* close時に呼ばれる関数 */
static int mymodule_close(struct inode *inode, struct file *file)
{
	printk("mymodule_close\n");
	return 0;
}

/* read時に呼ばれる関数 */
static ssize_t mymodule_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	printk("mymodule_read\n");
	return 0;
}

/* write時に呼ばれる関数 */
static ssize_t mymodule_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	printk("mymodule_write\n");
	struct mymodule_local *lp = filp->private_data;

	char val[16];
    if (copy_from_user(val, buf, count) != 0) return -EFAULT;
	
	if (strncmp(val, "on", 2) == 0) {
		REG(lp->base_addr) = 0x000F;
	} else if (strncmp(val, "off", 3) == 0) {
		REG(lp->base_addr) = 0x0000;
	} else {
		REG(lp->base_addr) = 0x000A;
	}

	return count;
}

/* 各種システムコールに対応するハンドラテーブル */
struct file_operations mymodule_fops = {
	.open    = mymodule_open,
	.release = mymodule_close,
	.read    = mymodule_read,
	.write   = mymodule_write,
};

/* キャラクタ型のデバイスドライバを作る処理 (作成したcdevはmymodule_localに保存する) */
static int mymodule_create_cdev(struct mymodule_local *lp)
{
	int minor = 0;
	int alloc_ret = 0;
	int cdev_err = 0;
	dev_t dev;

	/* 空いているメジャー番号を確保する */
	alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
	if (alloc_ret != 0) {
		printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
		return -1;
	}

	/* 取得したdev( = メジャー番号 + マイナー番号)からメジャー番号を取得して保持しておく */
	lp->mymodule_major = MAJOR(dev);
	dev = MKDEV(lp->mymodule_major, MINOR_BASE);  /* 不要? */

	/* cdev構造体の初期化とシステムコールハンドラテーブルの登録 */
	cdev_init(&lp->cdev, &mymodule_fops);
	lp->cdev.owner = THIS_MODULE;

	/* このデバイスドライバ(cdev)をカーネルに登録する */
	cdev_err = cdev_add(&lp->cdev, dev, MINOR_NUM);
	if (cdev_err != 0) {
		printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
		unregister_chrdev_region(dev, MINOR_NUM);
		return -1;
	}

	/* このデバイスのクラス登録をする(/sys/class/mymodule/ を作る) */
	lp->mymodule_class = class_create(THIS_MODULE, "mymodule");
	if (IS_ERR(lp->mymodule_class)) {
		printk(KERN_ERR  "class_create\n");
		cdev_del(&lp->cdev);
		unregister_chrdev_region(dev, MINOR_NUM);
		return -1;
	}

	/* /sys/class/mymodule/mymodule* を作る */
	for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
		device_create(lp->mymodule_class, NULL, MKDEV(lp->mymodule_major, minor), NULL, "mymodule%d", minor);
	}

	return 0;
}

/* キャラクタ型のデバイスドライバを削除する処理 */
static void mymodule_delete_cdev(struct mymodule_local *lp)
{
	int minor = 0;
	dev_t dev = MKDEV(lp->mymodule_major, MINOR_BASE);

	/* /sys/class/mymodule/mymodule* を削除する */
	for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
		device_destroy(lp->mymodule_class, MKDEV(lp->mymodule_major, minor));
	}

	/* このデバイスのクラス登録を取り除く(/sys/class/mymodule/を削除する) */
	class_destroy(lp->mymodule_class);

	/* このデバイスドライバ(cdev)をカーネルから取り除く */
	cdev_del(&lp->cdev);

	/* このデバイスドライバで使用していたメジャー番号の登録を取り除く */
	unregister_chrdev_region(dev, MINOR_NUM);
}

/* デバイス接続を検知したときに呼ばれる処理  (デバイス情報を取得/保持して、/dev/mymodule0を作る) */
static int mymodule_probe(struct platform_device *pdev)
{
	struct resource *r_mem; /* IO mem resources */
	struct device *dev = &pdev->dev;
	struct mymodule_local *lp = NULL;

	int rc = 0;
	dev_info(dev, "Device Tree Probing\n");
	/* Get iospace for the device */
	r_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!r_mem) {
		dev_err(dev, "invalid address\n");
		return -ENODEV;
	}
	lp = (struct mymodule_local *) kmalloc(sizeof(struct mymodule_local), GFP_KERNEL);
	if (!lp) {
		dev_err(dev, "Cound not allocate mymodule device\n");
		return -ENOMEM;
	}
	dev_set_drvdata(dev, lp);
	lp->mem_start = r_mem->start;
	lp->mem_end = r_mem->end;

	if (!request_mem_region(lp->mem_start,
				lp->mem_end - lp->mem_start + 1,
				DRIVER_NAME)) {
		dev_err(dev, "Couldn't lock memory region at %p\n",
			(void *)lp->mem_start);
		rc = -EBUSY;
		goto error1;
	}

	lp->base_addr = ioremap_nocache(lp->mem_start, lp->mem_end - lp->mem_start + 1);
	if (!lp->base_addr) {
		dev_err(dev, "mymodule: Could not allocate iomem\n");
		rc = -EIO;
		goto error2;
	}

	dev_info(dev,"mymodule at 0x%08x mapped to 0x%08x\n",
		(unsigned int __force)lp->mem_start,
		(unsigned int __force)lp->base_addr);

	/* このデバイスドライバをキャラクタ型としてカーネルに登録する。(/sys/class/mymodule/mymodule* を作る) */
	if(mymodule_create_cdev(lp)) goto error2;

	return 0;

error2:
	release_mem_region(lp->mem_start, lp->mem_end - lp->mem_start + 1);
error1:
	kfree(lp);
	dev_set_drvdata(dev, NULL);
	return rc;
}


static int mymodule_remove(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct mymodule_local *lp = dev_get_drvdata(dev);
	mymodule_delete_cdev(lp);
	release_mem_region(lp->mem_start, lp->mem_end - lp->mem_start + 1);
	kfree(lp);
	dev_set_drvdata(dev, NULL);
	return 0;
}

/* このデバイスドライバで取り扱うデバイスは、xlnx社のmyip-1.0 */
#ifdef CONFIG_OF
static struct of_device_id mymodule_of_match[] = {
	{ .compatible = "xlnx,myip-1.0", },
	{ /* end of list */ },
};
MODULE_DEVICE_TABLE(of, mymodule_of_match);
#else
# define mymodule_of_match
#endif


static struct platform_driver mymodule_driver = {
	.driver = {
		.name = DRIVER_NAME,
		.owner = THIS_MODULE,
		.of_match_table	= mymodule_of_match,
	},
	.probe		= mymodule_probe,
	.remove		= mymodule_remove,
};

static int __init mymodule_init(void)
{
	printk("mymodule_init\n");
	return platform_driver_register(&mymodule_driver);
}


static void __exit mymodule_exit(void)
{
	printk("mymodule_exit\n");
	platform_driver_unregister(&mymodule_driver);
}

module_init(mymodule_init);
module_exit(mymodule_exit);
```

一番重要なのは、`struct of_device_id mymodule_of_match[]`です。`.compatible = "xlnx,myip-1.0"`とすることで、このデバイスドライバは`xlnx,myip-1.0`というデバイスを取り扱う、と登録します。OSは、デバイスツリー情報に基づいて、`xlnx,myip-1.0`というデバイスが接続されていることを知っているので、このデバイスに対応したデバイスドライバを探します。その結果、このmymoduleの`mymodule_probe()`が呼ばれます。

`mymodule_probe()`内で、`platform_get_resource()`を使用して、デバイスツリーからデバイス情報を取得します。このデバイス(myip)ではレジスタアドレスのみを取得します(具体的には`0x43c00000`)。続いて、キャラクタ型のデバイスドライバとして登録します。これによって、`/sys/class/mymodule/mymodule0`が作られ、udevによって自動的に`/dev/mymodule0`が作られます。

実際にデバイス情報(myipのレジスタアドレス)が必要になるのは、writeの時です。そのため、この情報を`mymodule_local`構造体内に保持しておきます。ここの仕組みについては、[組み込みLinuxデバイスドライバの作り方 (10)](https://qiita.com/take-iwiw/items/dbc544864f0e9873270a)を参考にしてください。面倒なら、staticに保持するのでもいいと思います。



## デバイスドライバを使う
前述のコマンドでビルドして、mymodule.koをZYBOにコピーしておきます。そして、ZYBO上でinsmodします。
以下のようなコマンドで、まず`/dev/mymodule0`が作られていることを確認します。その後のコマンドで、LEDが全OFF, 全ON, 交互にONすればOKです。(自作IP(myip)は出力を反転するようにverilog実装していたので、ON/OFFは逆になります)

```sh:ZYBO上のターミナル
root@MyIP:~# ls /dev/mymodule0
/dev/mymodule0
root@MyIP:~# echo "on" > /dev/mymodule0
oot@MyIP:~# echo "off" > /dev/mymodule0
root@MyIP:~# echo "aaa" > /dev/mymodule0
```

実際にはこの後に、`/dev/mymodule0`にアクセスするユーザアプリケーションを作ることになると思いますが、省略します。

# Linuxイメージに組み込む
実装、テストがOKなら、`petalinux-build -x package`で、image.ubを作ります。
image.ubをSDカードに上書きコピーして、ZYBOを起動します。mymodule.koは`/lib/modules/4.9.0-xilinx-v2017.4/extra/mymodule.ko`に取り込まれます。なお、起動後、手動でmodprobeやinsmodしないでも、自動的にmymodule.koがロードされます。これは、OSがデバイスツリー情報に基づいて`xlnx,myip-1.0`に対応するデバイスドライバを探して、mymodule.koをロードしてくれるためです。

このように、mymodule.koが自動ロードされるようなイメージで起動しているときに、再度mymodule.koをデバッグしたいときは、最初に`modprobe -r mymodule`するのを忘れないようにしてください。

# カーネルモジュールを取り除く
何らかの事情で、作成したカーネルモジュール (デバイスドライバ)をイメージに取り込みたくない場合は、`petalinux-config -c rootfs`で現れるmenuconfig内の`modules`のところで設定できます。