LoginSignup
22
25

More than 3 years have passed since last update.

Arm Linuxのドライバ(キャラクタデバイス)を書く

Last updated at Posted at 2019-05-10

動機

毎回聞かれるたびに説明するのがめんどいのでパブリックな場所にまとめておきたいとおもった。
説明用の機能なので、実用的か?は微妙。

仕様

  • キャラクタデバイス
  • device-treeから設定を変更する(Arm標準、x86は知らん)
    • とりあえず物理アドレスが設定できること
  • write()で物理アドレスへ書き込み
  • read()で物理アドレスから読み出し

本当は割り込みも扱いたいけど、書くとしても別記事かな。

前提知識

キャラクタデバイスとは

wikipedia(Device_file#Character_devices)

ユーザ空間から読み書きできるファイルを作って、そのファイルを介してハードウェアの世界とつながることにする、という理解をしている。
ファイルを作っておけば、CでもPythonでもBashでもashでも好きに読み書きできる。

devicde-treeとは

wikipedia(Device_tree)

今回着目する機能としては、CPUに接続されるハードウェアと対応するドライバがなんであるかを記述する部分。
x86では使われないが、Armでは接続されるハードウェアと、そのドライバのペアが無限に存在しうることが想定されたので、作られたものと理解している。
独特な記法だが、目的について納得していればすんなり書けると思う。

このブログ(とあるエンジニアの備忘log)の説明が好き。

ソースコード

ヘッダとか

#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> /* struct cdev */
#include <linux/fs.h> /* struct file_operations */
#include <linux/device.h> /* class_create */
#include <asm/uaccess.h> /* copy_to_user copy_from_user */

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

/* Standard module information, edit as appropriate */
MODULE_LICENSE("GPL"); // GPLがないと組み込めない
MODULE_AUTHOR("nv-h");
MODULE_DESCRIPTION("My Character Device");

#define DRIVER_NAME "my_char_dev"
#define DRIVER_VER  "Ver.0001"

#define CHAR_DEV_MAX_MINORS (32)

以下二つ以外は、ほぼお決まりの部分だと思う。
* DRIVER_NAME (デバイスツリーから呼ぶための名前兼デバイスファイル名とする)
* DRIVER_VER (バージョンを起動時とかに表示させてみるための定義)

初期化処理と終了処理

/* デバイスツリーからの呼び出しのための記述 */
static struct of_device_id my_char_dev_of_match[] = {
    { .compatible = DRIVER_NAME "," DRIVER_NAME, },
    { /* end of list */ },
};
MODULE_DEVICE_TABLE(of, my_char_dev_of_match);

/* デバイスツリーからの呼び出しのための記述 */
static struct platform_driver my_char_dev_driver = {
    .driver = {
        .name = DRIVER_NAME,
        .owner = THIS_MODULE,
        .of_match_table = my_char_dev_of_match,
    },
    .probe      = my_char_dev_probe, // ドライバプローブ処理関数(後で説明)
    .remove     = my_char_dev_remove, // ドライバリムーブ処理関数(後で説明)
};

/* ドライバの初期化処理 */
static int __init my_char_dev_init(void)
{
    dev_t dev;
    printk("%s %s\n", DRIVER_NAME, DRIVER_VER);

    /* Create device class for this device */
    my_char_dev_class = class_create(THIS_MODULE, DRIVER_NAME);

    if (my_char_dev_class == NULL) {
        printk(KERN_ERR "Failed to register "DRIVER_NAME" class\n");
        return -1;
    }

    /* Allocate char device for this driver */
    if (alloc_chrdev_region(&dev, 0, CHAR_DEV_MAX_MINORS, DRIVER_NAME) < 0) {
        pr_err("\r\n Error allocating char device \r\n");
        class_destroy(my_char_dev_class);
        return -1;
    }

    my_char_dev_major = MAJOR(dev);

    return platform_driver_register(&my_char_dev_driver);
}


/* ドライバの終了処理 */
static void __exit my_char_dev_exit(void)
{
    platform_driver_unregister(&my_char_dev_driver);
    unregister_chrdev_region(MKDEV(my_char_dev_major, 0), CHAR_DEV_MAX_MINORS);
    class_destroy(my_char_dev_class);
    printk(KERN_ALERT "%s exit.\n", DRIVER_NAME);
}

module_init(my_char_dev_init);
module_exit(my_char_dev_exit);

読めばわかる、よね。

プローブ処理

my_char_dev_probe()がプローブ処理本体

  1. device-treeの内容を見てメモリリソースを確保
    • my_char_dev_get_phy_addr()
  2. デバイスの情報格納用の構造体を実体化(これはいろんな主義があるっぽい)
    • my_char_dev_resister()
    • my_char_dev_fopsは構造体で、読み書きなどの関数を格納するが、後述する。

お約束みたいなのが多いので、各API(?)はググってくだしあ。


struct my_char_dev_t {
    u32 phy_addr;
    u32 phy_size;
    void __iomem *vir_addr;

    /* キャラクタデバイス登録用 */
    int minor;
    struct cdev cdev;
    dev_t dev;
    struct device *char_dev;

    /* デバイスツリーからの情報 */
    const char *dev_file_name;
};

/* 複数デバイス接続用 */
static struct class *my_char_dev_class;
static int my_char_dev_major;
static int my_char_dev_next_minor = 0;


static int my_char_dev_get_phy_addr(struct platform_device *pdev, struct my_char_dev_t *lp)
{
    struct device *dev = &pdev->dev;
    struct device_node *node = pdev->dev.of_node;

    if (of_property_read_u32(node, "phy_addr", &lp->phy_addr)) {
        dev_err(dev, "Could not read \"phy_addr\" from device tree.\n");
        return -ENODEV;
    }
    if (of_property_read_u32(node, "phy_size", &lp->phy_size)) {
        dev_err(dev, "Could not read \"phy_size\" from device tree.\n");
        return -ENODEV;
    }

    if (!request_mem_region(lp->phy_addr, lp->phy_size, DRIVER_NAME)) {
        dev_err(dev, "Could not reserve phy add. 0x%08X\n"
            , lp->phy_addr);
        return -EBUSY;
    }

    lp->vir_addr = ioremap_nocache(lp->phy_addr, lp->phy_size);
    if (lp->vir_addr == NULL) {
        dev_err(dev, "Could not map phy addr. 0x%08X\n", lp->phy_addr);
        release_mem_region(lp->phy_addr, lp->phy_size);
        return -EIO;
    }

    return 0;
}


static int my_char_dev_resister(struct platform_device *pdev, struct my_char_dev_t *lp)
{
    struct device *dev = &pdev->dev;
    struct device_node *node = pdev->dev.of_node;

    /* Create device file */
    if (my_char_dev_next_minor < CHAR_DEV_MAX_MINORS) {
        lp->minor = my_char_dev_next_minor++;
    } else {
        dev_err(dev, "Minor file number %d exceed the max minors %d.\n",
            my_char_dev_next_minor, CHAR_DEV_MAX_MINORS);
        return -1;
    }
    /* Initialize character device */
    cdev_init(&lp->cdev, &my_char_dev_fops);
    lp->cdev.owner = THIS_MODULE;
    lp->dev = MKDEV(my_char_dev_major, lp->minor);
    if (cdev_add(&lp->cdev, lp->dev, 1)) {
        dev_err(dev, "chardev registration failed.\n");
        return -1;
    }

    /* Create device */
    if (of_property_read_string(node, "dev_file_name", &lp->dev_file_name)) {
        dev_err(dev, "Could not read \"dev_file_name\" from device tree.\n");
        return -ENODEV;
    }
    lp->char_dev = device_create(my_char_dev_class, NULL, lp->dev, NULL,
        "%s", lp->dev_file_name);
    if (lp->char_dev == NULL) {
        dev_err(dev, "Cannot create device file.\n");
        return -1;
    }

    return 0;
}


static int my_char_dev_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_char_dev_t *lp = NULL;
    int rc = 0;

    lp = (struct my_char_dev_t *) kzalloc(sizeof(struct my_char_dev_t), GFP_KERNEL);
    if (!lp) {
        dev_err(dev, "Cound not allocate device\n");
        return -ENOMEM;
    }

    dev_set_drvdata(dev, lp);

    lp->base_addr = NULL;
    lp->vir_addr = NULL;

    /* メモリ(バッファ)確保(デバイスツリーの内容から判断) */
    rc = my_char_dev_get_phy_addr(pdev, lp);
    if (rc) goto probe_error;

    /* キャラクタデバイスの登録(デバイスツリーの内容から名前を決定) */
    rc = my_char_dev_resister(pdev, lp);
    if (rc) goto probe_error;

    return 0;

probe_error:
    if (lp->vir_addr) {
        iounmap(lp->vir_addr);
        release_mem_region(lp->phy_addr, lp->phy_size);
    }

    kfree(lp);
    dev_set_drvdata(dev, NULL);
    return rc;
}

リムーブ処理

プローブに比べると楽。

  1. メモリリソースを解放
  2. デバイス構造体を解放
static int my_char_dev_remove(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_char_dev_t *lp = dev_get_drvdata(dev);

    /* キャラクタデバイス削除 */
    device_destroy(my_char_dev_class, MKDEV(my_char_dev_major, lp->minor));
    cdev_del(&lp->cdev);

    /* 確保したリソースを解放 */
    if (lp->vir_addr) {
        iounmap(lp->vir_addr);
        release_mem_region(lp->phy_addr, lp->phy_size);
    }

    /* ドライバ自体の削除 */
    unregister_chrdev_region(lp->dev, 0);
    kfree(lp);
    dev_set_drvdata(dev, NULL);

    return 0;
}

読み書き動作確認の実装

file_operations型のmy_char_dev_fopsにopen/write/read/close処理をまとめておく。
このmy_char_dev_fopsは、上記のプローブ処理で呼ばれている。

struct file_operations my_char_dev_fops = {
    .owner = THIS_MODULE,
    .open = my_char_dev_open, // open()したときに呼ばれる関数
    .read = my_char_dev_read, // read()したときに呼ばれる関数
    .write = my_char_dev_write, // write()したときに呼ばれる関数
    .release = my_char_dev_release, // close()したときに呼ばれる関数
};

各関数は好きに実装すればよい。

int my_char_dev_open(struct inode *inode, struct file *fp)
{
    struct my_char_dev_t *lp;
    lp = container_of(inode->i_cdev, struct my_char_dev_t, cdev);
    fp->private_data = lp;

    /* 開いた数をカウント(特に意味はない) */
    lp->open_count++;
    return 0;
}


/* 指定されたサイズ分読み出す */
ssize_t my_char_dev_read(struct file *fp, char __user *buf, size_t size, loff_t *f_pos)
{
    struct my_char_dev_t *lp = fp->private_data;
    u32 transfered;
    size_t not_transfered; // copyできた数

    if (size == 0) {
        printk(KERN_WARNING "[%d]my size 0 is not allowed.\n", lp->minor);
        return -EFAULT;
    }

    transfered = size; // 実用的な回路の場合、DMA転送してもらって、成功した数などを入れる。
    not_transfered = copy_to_user(buf, lp->vir_addr, transfered);

    if ((transfered - not_transfered) == 0) {
        printk(KERN_WARNING "[%d]my_char_dev_read: copy_to_user() error.\n", lp->minor);
        return -EFAULT;
    }

    return transfered - not_transfered;
}


/* 指定されたサイズ分書いてみる */
ssize_t my_char_dev_write(struct file *fp, const char __user *buf, size_t size, loff_t *f_pos)
{
    struct my_char_dev_t *lp = fp->private_data;
    size_t not_transfered; // copyできた数
    u32 transfered;

    not_transfered = copy_from_user(lp->vir_addr, buf, size);

    if ((size - not_transfered) == 0) {
        printk(KERN_WARNING "[%d]my_char_dev_write: copy_from_user() error.\n", lp->minor);
        return -EFAULT;
    }

    transfered = size - not_transfered; // 実用的な回路の場合、DMA転送してもらって、成功した数などを入れる。

    return transfered;
}


int my_char_dev_release(struct inode *inode, struct file *fp)
{
    struct my_char_dev_t *lp;
    lp = container_of(inode->i_cdev, struct my_char_dev_t, cdev);

    lp->open_count--;
    debug("[%d]close(%d)\n", lp->minor, lp->open_count);

    return 0;
}

device-treeの記述

こんな感じ。compatibleの部分はドライバで記述した内容に合わせておくこと。

char_dev_0: char_dev@40000000 {
    compatible = "my_char_dev,my_char_dev";
    reg = <0x41000000 0x1000>;
    dev_file_name = "char_dev_0";
    phy_addr = <0x40000000>;
    phy_size = <0x00001000>;
};
char_dev_1: char_dev@41000000 { // 2つのデバイスも可能
    compatible = "my_char_dev,my_char_dev";
    reg = <0x41000000 0x1000>;
    dev_file_name = "char_dev_1";
    phy_addr = <0x41000000>;
    phy_size = <0x00001000>;
};

よく考えたら、物理アドレスはreg = XXXのやつを使えばいいから現仕様ではphy_addrとか要らん。

22
25
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
22
25