レガシーガジェット研究所

気になったことのあれこれ。

キャラクタデバイスドライバを作ってみた。

概要

ラズパイのGPIOを対象としたデバイスドライバを作成した。基本的に行うことは以前書いた以下の記事と同じであるためレジスタなどの説明はここでは割愛する。

https://k-onishi.hatenablog.jp/entry/2018/11/24/112806

作成

システムコール対応の関数テーブル作成

今回は以下のような関数を実装した。

struct file_operations my_file_ops = {
  .owner = THIS_MODULE,
  .open = my_open,
  .release = my_close,
  .read = my_read,
  .write = my_write,
  .unlocked_ioctl = my_ioctl, /* 64 bits */
  .compat_ioctl   = my_ioctl, /* 32 bits */
};

open及びrelease物理アドレスと仮想アドレスのマッピングを行い、ioctlで操作対象のピンの変更、そして実際の読み書きはread及びwriteで行う。

file_operationsは実装できるシステムコールに対応した処理を保持する構造体で以下のように定義されている。

/*
 * NOTE:
 * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
 * can be called without the big kernel lock held in all filesystems.
 */
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*dir_notify)(struct file *filp, unsigned long arg);
    int (*flock) (struct file *, int, struct file_lock *);
};

メジャー番号とマイナー番号の取得

以下ではalloc_chrdev_region関数でメジャー番号の取得を行い、dev_t型の変数からMAJORマクロを用いてメジャー番号を取得している。

dev_t dev;

alloc_ret = alloc_chrdev_region(&dev, MINOR_NUMBER_START, NUMBER_MINOR_NUMBER, DRIVER_NAME);
if (alloc_ret != 0) {
    printk(KERN_ERR "failed to alloc_chrdev_region()\n");
    return -1;
}

major_number = MAJOR(dev);

dev_tは以下のように定義されておりunsigned intであることがわかる。そしてその変数から決められた上位12ビットをメジャー番号としている。

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t     dev_t;

// include/linux/kdev_t.h
#define MINORBITS  20
#define MINORMASK  ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)   (((ma) << MINORBITS) | (mi))

alloc_chrdev_region()は以下のように定義されており__register_chrdev_regionの戻り値からメジャー番号とマイナー番号を設定している。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
    struct char_device_struct *cd;
    cd = __register_chrdev_region(0, baseminor, count, name);
    if (IS_ERR(cd))
        return PTR_ERR(cd);
    *dev = MKDEV(cd->major, cd->baseminor);
    return 0;
}

キャラクタデバイス構造体の初期化

/* initialize cdev and function table */
cdev_init(&my_char_dev, &my_file_ops);
my_char_dev.owner = THIS_MODULE;

cdev_init()は以下のように定義されておりキャラクタデバイス構造体のopsメンバに引数であるシステムコールハンドラ関数のテーブルを設定している。

void cdev_init(struct cdev *cdev, struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    cdev->kobj.ktype = &ktype_cdev_default;
    kobject_init(&cdev->kobj);
    cdev->ops = fops;
}

cdevは以下のように定義されており、先ほど作成したシステムコールテーブルを保持するメンバが存在する。

struct cdev {
    struct kobject kobj;
    struct module *owner;
    struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

ドライバの登録

デバイスドライバの登録はcdev_add()で行う。キャラクタデバイス構造体やデバイス番号を保持している変数、当該デバイスに割り当てるマイナー番号の数などの設定を行う。

cdev_err = cdev_add(&my_char_dev, dev, NUMBER_MINOR_NUMBER);
if (cdev_err != 0) {
  printk(KERN_ERR "failed to cdev_add()\n");
  unregister_chrdev_region(dev, NUMBER_MINOR_NUMBER);
  return -1;
}

バイスのクラス登録

class_create()でクラス登録を行う。当該処理で/sys/class/my_device/が作成される。

my_char_dev_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(my_char_dev_class)) {
  printk(KERN_ERR "class_create()\n");
  cdev_del(&my_char_dev);
  unregister_chrdev_region(dev, NUMBER_MINOR_NUMBER);
  return -1;
}

バイスファイルの作成

device_create()で実際に指定のメジャー番号及びマイナー番号のデバイスを登録する。(ex. sys/class/my_device/my_device)

device_create(my_char_dev_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);

実装に関して

  • module_init() & module_exit()
  • PAGE_SIZE
  • ioremap_nocache()
  • get_user() & put_user();

module_init() & module_exit()

モジュールのロード及びアンロード時に呼び出される関数を登録。 先ほどの説明した一連の処理はmodule_initの引数となっている関数内で行う。

module_init(my_init);
module_exit(my_exit);

PAGE_SIZE

カーネルが仮想アドレス空間においてメモリをチャンクとして扱う際のサイズ。

// include/asm-i386/page.h
#define PAGE_SHIFT 12
#define PAGE_SIZE  (1UL << PAGE_SHIFT)

ioremap_nocache()

カーネル空間に物理アドレスマッピングを行う。

base_address = (int)ioremap_nocache(
    GPIO_ADDRESS, PAGE_SIZE);
return 0;

get_user() & put_user()

ユーザ空間<->カーネル空間でデータのコピーを行う。

static ssize_t my_write(struct file* file, const char __user* buff, size_t count, loff_t *pos)
{
  int mode;
  get_user(mode, &buff[0]);
  :
  (省略)
  :
static ssize_t my_read(struct file* file, 
    char __user* buff, 
        size_t count, loff_t *pos)
{
  int num_reg = (pin_number / NUM_PIN_EACH_REG);
  int offset = (pin_number % NUM_PIN_EACH_REG);
  int addr = base_address + GPLEV0_OFFSET 
    + (REG_GAP * num_reg);
  unsigned int reg_value = MEMORY(addr);
  int value = ((reg_value >> offset) & 1UL);
  put_user(value + '0', &buff[0]);
  return count;
}

エコシステム

  • udev
  • uevent
  • *.rules
  • /lib/modules

udev

Linuxカーネル用のデバイス管理ツール。/dev以下の管理やホットスワップファームウェアのロードなどを行う。Linux上ではデーモンとして動作する。

新たにデバイスが接続された際や解除された際にはnetlink(カーネルモジュール及びユーザ空間のプロセスの間で行われる情報のやりとりに用いられる)ソケット経由でueventsudevに送信する。udev/etc/udev/rules.d/*.rulesを参照しルールにマッチしたデバイスノードを作成する。

uevent

sudo udevadm monitor --envを実行してechoなんかで書き込むとudevのメッセージを見ることができる(できなかった・・・)

ex.

$ sudo ls -l /sys/class/input/mice/uevent
-rw-r--r-- 1 root root 4096 Nov 29 22:26 /sys/class/input/mice/uevent

*.rules

udevがデバイスを認識してデバイスファイルを作成するために用いられるファイル。

$ sudo ls -l /etc/udev/rules.d/
total 0

中身は以下のようなフォーマットになっている

KERNEL=="nvram", GROUP="adm", MODE="0660"

/lib/modules

insmodを使用せずとも以下のような箇所にドライバモジュールを配置しておくことでデバイス検出時にモジュールがロードされる。

$ sudo ls -l  /lib/modules/4.4.0-116-generic/kernel/drivers/gpio/ | more
total 560
-rw-r--r-- 1 root root 10622 Feb 13  2018 gpio-104-idio-16.ko
-rw-r--r-- 1 root root  8966 Feb 13  2018 gpio-adp5520.ko
-rw-r--r-- 1 root root 11254 Feb 13  2018 gpio-adp5588.ko
-rw-r--r-- 1 root root 13646 Feb 13  2018 gpio-amd8111.ko
-rw-r--r-- 1 root root 13662 Feb 13  2018 gpio-amdpt.ko
-rw-r--r-- 1 root root  8774 Feb 13  2018 gpio-arizona.ko
-rw-r--r-- 1 root root 13486 Feb 13  2018 gpio-crystalcove.ko
-rw-r--r-- 1 root root 10158 Feb 13  2018 gpio-da9052.ko
-rw-r--r-- 1 root root  9158 Feb 13  2018 gpio-da9055.ko
-rw-r--r-- 1 root root 14854 Feb 13  2018 gpio-dln2.ko
-rw-r--r-- 1 root root 19214 Feb 13  2018 gpio-dwapb.ko
-rw-r--r-- 1 root root 24702 Feb 13  2018 gpio-f7188x.ko
-rw-r--r-- 1 root root 15550 Feb 13  2018 gpio-generic.ko
-rw-r--r-- 1 root root 15854 Feb 13  2018 gpio-ich.ko
-rw-r--r-- 1 root root 11406 Feb 13  2018 gpio-it87.ko
-rw-r--r-- 1 root root  9214 Feb 13  2018 gpio-janz-ttl.ko
-rw-r--r-- 1 root root  9998 Feb 13  2018 gpio-kempld.ko
-rw-r--r-- 1 root root  9294 Feb 13  2018 gpio-lp3943.ko
-rw-r--r-- 1 root root  7478 Feb 13  2018 gpio-max7300.ko
-rw-r--r-- 1 root root  7990 Feb 13  2018 gpio-max7301.ko
-rw-r--r-- 1 root root  8294 Feb 13  2018 gpio-max730x.ko
-rw-r--r-- 1 root root 13926 Feb 13  2018 gpio-max732x.ko
-rw-r--r-- 1 root root  8942 Feb 13  2018 gpio-mc33880.ko
-rw-r--r-- 1 root root 24566 Feb 13  2018 gpio-mcp23s08.ko
-rw-r--r-- 1 root root 20630 Feb 13  2018 gpio-ml-ioh.ko
-rw-r--r-- 1 root root 22366 Feb 13  2018 gpio-pca953x.ko
-rw-r--r-- 1 root root 17542 Feb 13  2018 gpio-pcf857x.ko
-rw-r--r-- 1 root root 10246 Feb 13  2018 gpio-rdc321x.ko
-rw-r--r-- 1 root root 13230 Feb 13  2018 gpio-sch311x.ko
-rw-r--r-- 1 root root  9446 Feb 13  2018 gpio-sch.ko
-rw-r--r-- 1 root root  8526 Feb 13  2018 gpio-tps65912.ko
-rw-r--r-- 1 root root 15550 Feb 13  2018 gpio-twl4030.ko
-rw-r--r-- 1 root root  8014 Feb 13  2018 gpio-twl6040.ko
-rw-r--r-- 1 root root  7982 Feb 13  2018 gpio-ucb1400.ko
-rw-r--r-- 1 root root 12870 Feb 13  2018 gpio-viperboard.ko
-rw-r--r-- 1 root root 11390 Feb 13  2018 gpio-vx855.ko
-rw-r--r-- 1 root root 11414 Feb 13  2018 gpio-wm831x.ko
-rw-r--r-- 1 root root  8782 Feb 13  2018 gpio-wm8350.ko
-rw-r--r-- 1 root root 11998 Feb 13  2018 gpio-wm8994.ko

実装

https://github.com/k-onishi/gpio-driver

資料

このエントリは以下の勉強会に参加した際に発表したものと内容は同じである。

linux-kernel.connpass.com

www.slideshare.net