LoginSignup
7
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-01-15

環境

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

7
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
12