OS・実行環境

デバイスドライバ

ハードを制御するためのソフト。

概要

デバイスドライバ(Device Driver)は、オペレーティングシステムがハードウェアを制御するためのソフトウェアです。GPIO・UART・I2C・SPI・USB・ネットワークインターフェースなど、あらゆるハードウェアにはそれに対応するドライバが必要です。ドライバはハードウェアの詳細(レジスタアドレス・プロトコル・タイミング)を隠蔽し、OSやアプリケーションに統一されたインターフェース(API)を提供します。

組み込みLinuxでは、デバイスドライバはカーネルの一部(カーネルモジュール)として動作し、ロード可能モジュール(.ko)として動的にロード/アンロードできます。ベアメタルRTOS環境では、ベンダー提供のHAL(Hardware Abstraction Layer)がドライバに相当します。

歴史・背景

ドライバの概念はOSの黎明期から存在します。Unixは1969年の設計から「デバイスをファイルとして扱う(Everything is a file)」という思想を持ち、ドライバがデバイスをファイルとして抽象化する設計が確立されました。

Linuxカーネルは1991年の公開時からドライバフレームワークを持っていましたが、当初はボードごとのカスタマイズが多く混乱していました。2000年代にsysfs(デバイスの属性をファイルシステムとして公開)・udev(ホットプラグデバイス管理)・デバイスモデル(kobject)が整備されました。

デバイスツリーの採用(2011年〜)により、ドライバとハードウェア設定の分離が進み、1つのドライバが複数のボード・デバイスをサポートできる汎用設計が標準となりました。現在のLinuxカーネルには数万本のドライバが収録されています。

技術仕様

Linuxドライバの種類

種類デバイス例インターフェース
キャラクタデバイスUART, GPIO, ADC/dev/ttyS0, /dev/gpio等
ブロックデバイスeMMC, SD, HDD/dev/mmcblk0 等
ネットワークデバイスEthernet, Wi-Fieth0, wlan0(ifconfig等)
IIOデバイスIMU, 気圧センサー/sys/bus/iio/devices/
GPIOデバイスGPIO拡張IC/dev/gpiochipN
I2Cドライバセンサー, EEPROM/sys/bus/i2c/devices/
SPIドライバフラッシュ, ディスプレイ/dev/spidevX.Y
USB ドライバカメラ, HID, ストレージ/dev/video0, /dev/input/
Regmap/MFD複合デバイス(PMIC等)サブシステム経由

Linuxカーネルモジュールの基本構造

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/i2c.h>
#include <linux/gpio/consumer.h>

/* ドライバがサポートするデバイスのリスト(Device Treeの compatible にマッチ) */
static const struct of_device_id mydev_of_match[] = {
    { .compatible = "myvendor,mydevice-v1", },
    { .compatible = "myvendor,mydevice-v2", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

/* プライベートデータ構造 */
struct mydev_priv {
    struct i2c_client *client;
    struct gpio_desc *reset_gpio;
    /* ... */
};

/* デバイス検出・初期化(probingと呼ぶ) */
static int mydev_probe(struct i2c_client *client,
                       const struct i2c_device_id *id) {
    struct mydev_priv *priv;
    int ret;

    priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv) return -ENOMEM;
    
    priv->client = client;
    i2c_set_clientdata(client, priv);
    
    /* GPIOをDevice Treeから取得 */
    priv->reset_gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_LOW);
    if (IS_ERR(priv->reset_gpio)) return PTR_ERR(priv->reset_gpio);
    
    /* ハードウェア初期化 */
    ret = mydev_hw_init(priv);
    if (ret) return ret;

    dev_info(&client->dev, "mydevice initialized\n");
    return 0;
}

/* デバイス取り外し・シャットダウン */
static void mydev_remove(struct i2c_client *client) {
    struct mydev_priv *priv = i2c_get_clientdata(client);
    /* リソース解放(devm_*を使った場合は自動解放) */
    mydev_hw_shutdown(priv);
}

/* I2Cドライバ定義 */
static struct i2c_driver mydev_driver = {
    .driver = {
        .name = "mydevice",
        .of_match_table = mydev_of_match,
    },
    .probe  = mydev_probe,
    .remove = mydev_remove,
};

module_i2c_driver(mydev_driver);  /* init/exit を自動生成 */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("My Name <me@example.com>");
MODULE_DESCRIPTION("MyDevice I2C driver");

キャラクタデバイスドライバ

#include <linux/cdev.h>
#include <linux/fs.h>

#define DEVICE_NAME "mychardev"

static dev_t dev_num;
static struct cdev my_cdev;

/* open/release/read/write/ioctlハンドラ */
static int my_open(struct inode *inode, struct file *file) { return 0; }
static int my_release(struct inode *inode, struct file *file) { return 0; }

static ssize_t my_read(struct file *f, char __user *buf,
                        size_t count, loff_t *ppos) {
    char kbuf[] = "hello from driver\n";
    size_t len = min(count, sizeof(kbuf));
    if (copy_to_user(buf, kbuf, len)) return -EFAULT;
    return len;
}

static ssize_t my_write(struct file *f, const char __user *buf,
                          size_t count, loff_t *ppos) {
    char kbuf[64] = {};
    if (copy_from_user(kbuf, buf, min(count, sizeof(kbuf))))
        return -EFAULT;
    pr_info("mydev: received '%s'\n", kbuf);
    return count;
}

#define MY_IOCTL_RESET _IO('m', 1)
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
        case MY_IOCTL_RESET:
            /* ハードウェアリセット処理 */
            break;
        default:
            return -EINVAL;
    }
    return 0;
}

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
};

static int __init my_init(void) {
    alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev_num, 1);
    return 0;
}
module_init(my_init);

devm(管理付きリソース)の活用

devm_*プレフィックスのAPIはデバイスのライフサイクルに紐付けてリソースを自動解放します:

/* devm_*を使うと remove() での手動解放が不要 */
irq = devm_request_irq(&client->dev, irq_num, my_isr, 0, "mydev", priv);
mem = devm_ioremap_resource(&client->dev, &res);
gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_LOW);
clk = devm_clk_get(&client->dev, "apb_pclk");
buf = devm_kzalloc(&client->dev, BUF_SIZE, GFP_KERNEL);

動作原理

ドライバのバインディング(probe)

Linuxカーネルのドライバモデルでは、ドライバとデバイスのマッチングは以下の流れで行われます:

1. カーネル起動時に Device Tree を解析
2. DTの各ノードに対応する「デバイス」をバスに登録
3. ドライバの of_match_table の compatible と DT の compatible を比較
4. マッチしたらドライバの probe() 関数を呼び出す
5. probe() 内でハードウェア初期化・割り込み登録・sysfs登録等を行う
# バインディング状態の確認
ls /sys/bus/i2c/drivers/bme280/
# → 1-0076  (I2Cバス1のアドレス0x76にバインドされている)

# 手動でバインド/アンバインド
echo "1-0076" > /sys/bus/i2c/drivers/bme280/unbind
echo "1-0076" > /sys/bus/i2c/drivers/bme280/bind

割り込み処理

/* 割り込みの登録 */
int irq = gpio_to_irq(gpio_num);
ret = devm_request_threaded_irq(&dev, irq,
    NULL,              /* ハードウェアIRQハンドラ(NULLでデフォルト)*/
    my_thread_fn,      /* スレッドハンドラ(プロセス文脈で実行)*/
    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
    "mydev", priv);

static irqreturn_t my_thread_fn(int irq, void *data) {
    struct mydev_priv *priv = data;
    /* 割り込み後の重い処理(プロセス文脈なのでスリープ可) */
    process_interrupt_data(priv);
    return IRQ_HANDLED;
}

用途・ユースケース

センサードライバ(IIOサブシステム)

温湿度・気圧・IMUセンサーはLinuxのIIO(Industrial I/O)サブシステムのドライバとして実装されます。/sys/bus/iio/devices/iio:device0/以下にセンサー値が公開され、ユーザー空間から読み取れます。

GPIOドライバと libgpiod

GPIO操作はLinux 4.8以降ではlibgpiodライブラリとchardev(/dev/gpiochipN)インターフェースが推奨されます:

# gpiodetect: GPIO チップの一覧
gpiodetect

# gpioget: GPIO の状態読み取り
gpioget gpiochip0 17

# gpioset: GPIO の設定
gpioset gpiochip0 17=1

ベアメタル/RTOSでのHALドライバ

ベアメタル環境では、LinuxカーネルのようなドライバフレームワークはなくベンダーのHAL(Hardware Abstraction Layer)を使います:

/* STM32 HAL の I2C 操作(ベアメタル) */
HAL_I2C_Master_Transmit(&hi2c1, 0x76 << 1, tx_data, 2, HAL_MAX_DELAY);
HAL_I2C_Master_Receive(&hi2c1, 0x76 << 1, rx_data, 6, HAL_MAX_DELAY);

実装・開発のポイント

ドライバデバッグの方法

# カーネルログでドライバの動作確認
dmesg | grep mydevice
dmesg -w   # リアルタイム監視

# dynamic debug: 特定ドライバのデバッグメッセージを有効化
echo "module mydevice +p" > /sys/kernel/debug/dynamic_debug/control

# ドライバの属性確認
ls /sys/class/mydev/
cat /sys/class/mydev/mydev0/status

# 割り込み統計
cat /proc/interrupts | grep mydev

よくあるバグと対処

  1. プローブ失敗: dmesg | grep -i "error\|fail\|probe"で原因確認
  2. GPIOの競合: 同じGPIOを複数ドライバが要求するとエラー。DTでGPIOの所有者を明確にする
  3. I2Cアドレス不一致: i2cdetect -y 1でバス上のデバイスを確認
  4. 割り込みが来ない: ピン設定・電気的接続・レジスタ設定を確認
  5. ページフォルト: ユーザー空間のポインタをcopy_from_userなしに直接操作したとき

他技術との比較

Linux ドライバ vs RTOSドライバ

項目LinuxカーネルドライバRTOSのHAL(FreeRTOS等)
構造カーネルモジュールベンダー提供の関数ライブラリ
開発言語C(一部アセンブリ)C / C++
デバッグdmesg, debugfs, dynamic debugUART printf, JTAG
ホットプラグサポート(USB等)非対応(静的構成)
汎用性高い(デバイスツリーで多ボード対応)ボード固有
難易度高い(カーネルAPI習熟が必要)比較的低い(HALは使いやすい)

関連用語

参考リンク