- 1回目: 開発環境の準備
- 2回目: Hello Worldプロジェクト
- 3回目: PSのGPIOでLチカ
- 4回目: PLのAXI GPIOでPSからLチカ
- 5回目: PLだけでLチカ
- 6回目: 自作IPでLチカ
- 7回目: ブートイメージを作る
- 8回目: Linux起動する
- 9回目: Linuxカーネルを少しカスタマイズする
- 10回目: LinuxのRootFSをカスタマイズする / PythonでHello World
- 11回目: LinuxユーザアプリケーションでLチカ
- 12回目: LinuxカーネルモジュールでLチカ
- 13回目: LAN(Ethernet 0)を使う
- 14回目: Linuxユーザアプリをデバッグする / RootFSに取り込む
- 15回目: Linux起動時にアプリケーションを自動実行させる
- 16回目: Linuxから自作IPをUIOで制御する
- 17回目: Linuxで自作IPのデバイスドライバを作る <--- 今回の内容
- 18回目: IoT化してスマホからLチカ
環境
- 開発用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とする)。
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
を元の状態に戻しておきます。
/include/ "system-conf.dtsi"
/ {
};
これによって、myip_0は、components/plnx_workspace/device-tree/device-tree-generation/pl.dtsi
で定義されている通り、xlnx
社のmyip-1.0
というデバイスとして扱われます。本当のプロジェクトでは、これらの名前はVivado上で変更しておくべきです。(当然僕はXilinx社員ではありません。) 後で作成するデバイスドライバ内で、この名前が必要になります。
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/
に作成されます。
petalinux-create -t modules --name mymodule --enable
petalinux-build -c rootfs
カーネルモジュールの開発フロー (一例)
このフローは、僕オリジナルです。 もっと良い方法がありましたら、ぜひ教えてください!!
基本的な流れとしては、開発PC(Ubuntu)でカーネルモジュール(mymodule.ko
)だけをビルドします。それを、ZYBOにscpでコピーして、動作確認/デバッグします。最終的にOKなものが出来たら、全体をビルドしてRootFSを作ります。
# 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:.
# 4. 動作確認する.
insmod mymodule.ko
# 5. 終わったらアンロードしておく
rmmod mymodule
# または、modprobe -r mymodule
実際には、一発でOKになることはないので、動作確認後、「1. コーディングする」に戻る。完成したら、下記のコマンドでimage.ubを作る。
# 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
を削除することで回避できますが、毎回やるのは面倒です。以下設定をすることで、無視することが出来ます。(セキュリティにご注意ください)
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として、デバイスツリーから情報(レジスタアドレスと割り込み番号)を取得するひな形が作られています。これを参考に、以下のようなコードを実装します。
#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は逆になります)
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
のところで設定できます。