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

More than 1 year has passed since last update.


環境


  • 開発用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チカでやりましたが、今回は、デバイス情報をちゃんとデバイスツリーから取得します。また、open/close/read/writeといったインターフェースを持つちゃんとしたデバイスドライバを作ります。

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


ハードウェアの準備

前回のプロジェクトを使う場合は、そのまま流用可能です。

使用するハードウェアは、前回: Linuxから自作IPをUIOで制御すると同じものです。


  • 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とする)。


開発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を元の状態に戻しておきます。


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社員ではありません。) 後で作成するデバイスドライバ内で、この名前が必要になります。


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チカと同じです。が、このときは開発したカーネルモジュールをいきなりRootFSに取り込みました。実際の開発では、デバッグが必要なため、毎回イメージ作成なんてしていられません。今回は、そこらへんの手順も含めて紹介しようと思います。


カーネルモジュールを作成する

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


開発PCのターミナル

petalinux-create -t modules --name mymodule --enable

petalinux-build -c rootfs


カーネルモジュールの開発フロー (一例)

このフローは、僕オリジナルです。 もっと良い方法がありましたら、ぜひ教えてください!!

基本的な流れとしては、開発PC(Ubuntu)でカーネルモジュール(mymodule.ko)だけをビルドします。それを、ZYBOにscpでコピーして、動作確認/デバッグします。最終的にOKなものが出来たら、全体をビルドしてRootFSを作ります。


開発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:.


ZYBOのターミナル

# 4. 動作確認する.

insmod mymodule.ko
# 5. 終わったらアンロードしておく
rmmod mymodule
# または、modprobe -r mymodule

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


開発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を削除することで回避できますが、毎回やるのは面倒です。以下設定をすることで、無視することが出来ます。(セキュリティにご注意ください)


開発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として、デバイスツリーから情報(レジスタアドレスと割り込み番号)を取得するひな形が作られています。これを参考に、以下のようなコードを実装します。


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)を参考にしてください。面倒なら、staticに保持するのでもいいと思います。


デバイスドライバを使う

前述のコマンドでビルドして、mymodule.koをZYBOにコピーしておきます。そして、ZYBO上でinsmodします。

以下のようなコマンドで、まず/dev/mymodule0が作られていることを確認します。その後のコマンドで、LEDが全OFF, 全ON, 交互にONすればOKです。(自作IP(myip)は出力を反転するようにverilog実装していたので、ON/OFFは逆になります)


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のところで設定できます。