C
Linux
RaspberryPi
kernel
デバイスドライバ

組み込みLinuxデバイスドライバの作り方 (11)

11回目: デバイスツリーにI2Cデバイスを追加する

本連載について

組み込みLinuxのデバイスドライバをカーネルモジュールとして開発するためのHowTo記事です。本記事の内容は全てラズパイ(Raspberry Pi)上で動かせます。

本記事に登場するソースコード全体

https://github.com/take-iwiw/DeviceDriverLesson/tree/master/11_01

今回の内容

これまで、いくつかのデバイスドライバを実装してきました。しかし、どのデバイスドライバも、「ハードウェア固有の情報」を直接デバイスドライバ内に持ってしまっていました。

例えば、5回目では、直接メモリマップドレジスタを叩くことでGPIOデバドラを実装しました。この時、オフセットアドレスなどのチップ(SoC)固有の情報をデバドラ内で持っていました。これは良くありません。(そもそも、GPIOそのもののデバドラを作ることはなく、Linuxカーネル標準の関数を普通は使います。)

仮に、Linuxカーネル標準の関数を使っても、まだハードウェア固有情報をデバドラは持ちえます。10回目では、Linuxカーネル標準のI2C制御関数を使用してI2Cデバイスにアクセスしました。しかし、対象となるI2Cデバイスが、どのI2Cバスにつながっているかや、スレーブアドレスといったボード固有の情報をデバドラ内に記載していました。

こういったことは、現在ではルール的に許されていません。理由は、SoC、ボードのバリエーション毎にコードがどんどん増加してしまうためです。現在では、こういったハードウェア固有情報はデバイスツリーによって管理しています。

前回、I2C接続された加速度センサ(LIS3DH)のデバイスドライバを作りました。しかし、このときは、手動でカーネルに対してI2C機器の情報を教えてあげる必要がありました。今回は、この情報をデバイスツリーに組込んでみようと思います。本記事ではRaspberryPi2 Model Bを対象として記載します

デバイスツリーに関連するファイル

「デバイスツリー」と言っていますが、その実態はバイナリ形式のファームウェアになります。拡張子は.dtbになります。これは、ラズパイでは/bootの下にあります。例えば、RaspberryPi2 Model Bの場合は、/boot/bcm2709-rpi-2-b.dtbになります。

ls /boot/*.dtb
/boot/bcm2708-rpi-0-w.dtb     /boot/bcm2709-rpi-2-b.dtb  /boot/bcm2835-rpi-a-plus.dtb  /boot/bcm2835-rpi-zero.dtb
/boot/bcm2708-rpi-b.dtb       /boot/bcm2710-rpi-3-b.dtb  /boot/bcm2835-rpi-b.dtb       /boot/bcm2836-rpi-2-b.dtb
/boot/bcm2708-rpi-b-plus.dtb  /boot/bcm2710-rpi-cm3.dtb  /boot/bcm2835-rpi-b-plus.dtb
/boot/bcm2708-rpi-cm.dtb      /boot/bcm2835-rpi-a.dtb    /boot/bcm2835-rpi-b-rev2.dtb

SoC固有情報はDTSIファイルに、ボード固有情報はDTSファイルに記載することになっています。これらをDTCという特別なコンパイラでコンパイルすることで、先ほどのDTBファイルが作られます。ラズパイ2の場合は/linux/arch/arm/boot/dts/bcm2709-rpi-2-b.dtsにあります。

dtbファイルを直接書き換える(手抜き版)

デバイスツリーに追加する

後で書きますが、Linuxのソースツリーを持ってきて、/linux/arch/arm/boot/dts/bcm2709-rpi-2-b.dtsを編集してコンパイルするのが正しい方法です。しかし、本連載ではこれまでラズパイ上でできるだけお手軽に確認してきました。今回も、まずはお手軽にやってみようと思います。

事前に、SDカード内のbcm2709-rpi-2-b.dtsをPC等にバックアップしておいてください。最悪、起動しなくなる可能性があります。また、デバイスツリー用のコンパイラ(DTC)をインストールします。sudo apt-get install device-tree-compiler

続いて、現在使用されているDTBファイルを逆コンパイルしてDTSに戻します。

dtc -I dtb -O dts /boot/bcm2709-rpi-2-b.dtb > dis_bcm2709-rpi-2-b.dts

これによって、dis_bcm2709-rpi-2-b.dtsというテキストファイルが作られます。このファイルには、このボード(ラスパイ2)上でのデバイスの接続情報がツリー状に記載されています。また、レジスタ番地なども記載されています。今回は、I2C_1にLIS3DH(スレーブアドレス=0x18)を接続するとします。まず、I2C1のノードを探します。アドレスで書かれてしまっていますが、i2c@7e804000の所になります。そこに、mydeviceというノードを追加します。compatiblemycompany,myoriginaldeviceを設定することで、「mycompanyというメーカー製のmyoriginaldeviceという名前のデバイス」だと指定します。また、regの所にはスレーブアドレスを設定します。これによってカーネルは、このボードには、I2C_1の0x18には"mycompany,myoriginaldevice"というデバイスがつながっている、と認識します。

dis_bcm2709-rpi-2-b.dts
省略
i2c@7e804000 {
    compatible = "brcm,bcm2835-i2c";
    reg = <0x7e804000 0x1000>;
    interrupts = <0x2 0x15>;
    clocks = <0x7 0x14>;
    #address-cells = <0x1>;
    #size-cells = <0x0>;
    status = "disabled";
    pinctrl-names = "default";
    pinctrl-0 = <0x10>;
    clock-frequency = <0x186a0>;
    phandle = <0x20>;

    /* 自分用のデバイスを追加 */
    mydevice@18 {
        compatible = "mycompany,myoriginaldevice";
        reg = <0x18>;
    };
};
省略

編集したら、以下のコマンドでコンパイルして、元のDTBファイルに上書きします。一応アクセス権の設定もしておきます。

dtc -O dtb -o bcm2709-rpi-2-b.dtb dis_bcm2709-rpi-2-b.dts
chmod 755 bcm2709-rpi-2-b.dtb
sudo cp bcm2709-rpi-2-b.dtb /boot/bcm2709-rpi-2-b.dtb
sudo reboot yes

/proc/device-treeでデバイスツリーの情報を確認できます。再起動後に、I2C1の下にmydeviceが追加していれば成功です。

ls /proc/device-tree/soc/i2c@7e804000/mydevice@18/
   compatible  name  reg

デバイスドライバ側の対応

デバイスドライバ側では、対応するデバイスを登録する必要があります。先ほど、デバイスツリーには"mycompany,myoriginaldevice"というデバイスを登録したので、それに対応するようにします。そのために、struct of_device_id"mycompany,myoriginaldevice"を設定して、struct i2c_driver.of_match_tableに登録します。それ以外は、前回の実装内容と同じです。ソースコードを以下に示します。

myDeviceDriver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <linux/of_platform.h>

/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice"              /* /proc/devices等で表示されるデバイス名 */

/* このデバイスドライバで取り扱うデバイスのマッチングテーブル */
/* dts内の、下記に対応
    i2c@7e804000 {
        mydevice@18 {
            compatible = "mycompany,myoriginaldevice";
            reg = <0x18>;
        };
*/
static const struct of_device_id mydevice_of_match_table[] = {
    {.compatible = "mycompany,myoriginaldevice",},
    { },
};
MODULE_DEVICE_TABLE(of, mydevice_of_match_table);

/* このデバイスドライバで取り扱うデバイスを識別するテーブルを登録する */
/* 重要なのは最初のnameフィールド。これでデバイス名を決める。後ろはこのドライバで自由に使えるデータ。ポインタや識別用数字を入れる */
static struct i2c_device_id mydevice_i2c_idtable[] = {
    {"MyI2CDevice", 0},
    { }
};
MODULE_DEVICE_TABLE(i2c, mydevice_i2c_idtable);

static int mydevice_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    printk("mydevice_i2c_probe\n");
    if(id != NULL)
        printk("id.name = %s, id.driver_data = %d", id->name, id->driver_data);
    if(client != NULL)
        printk("slave address = 0x%02X\n", client->addr);

    /* 通常はここで、このデバドラでサポートしているデバイスかどうかチェックする */

    int version;
    version = i2c_smbus_read_byte_data(client, 0x0f);
    printk("id = 0x%02X\n", version);

    return 0;
}

static int mydevice_i2c_remove(struct i2c_client *client)
{
    printk("mydevice_i2c_remove\n");
    return 0;
}

static struct i2c_driver mydevice_driver = {
    .driver = {
        .name           = DRIVER_NAME,
        .owner          = THIS_MODULE,
        .of_match_table = mydevice_of_match_table,
    },
    .id_table       = mydevice_i2c_idtable,     // このデバイスドライバがサポートするI2Cデバイス
    .probe          = mydevice_i2c_probe,       // 対象とするI2Cデバイスが認識されたときに呼ばれる処理
    .remove         = mydevice_i2c_remove,      // 対象とするI2Cデバイスが取り外されたときに呼ばれる処理
};

module_i2c_driver(mydevice_driver);

デバイスツリーに記載されている接続情報に基づいて、カーネルが対応するデバイスドライバを呼んでくれます。mydevice_i2c_probe()に渡されるstruct i2c_client内には、デバイスツリーで設定したスレーブアドレスが格納されています。

一点、注意点としては、この場合はstruct i2c_device_idにはNULLが入ります。そのためか分かりませんが、.probeはそのうち廃止されるようです。代わりに、.probe_newを使用するようです。.probe_newではstruct i2c_device_idが消されています。

動かしてみる

以下コマンドでビルド、ロードします。

make && sudo insmod MyDeviceModule.ko
dmesg
[ 3507.003163] mydevice_i2c_probe
[ 3507.003175] slave address = 0x18
[ 3507.003656] id = 0x33

ログを見ると、ちゃんとprobeが呼ばれて、I2C通信もできていることが分かります。前回は、手動でデバイス接続通知が必要でしたが、今回は不要だったという点が違いです

ソースコードからDTBを作る

環境準備

ちゃんと、ラズパイ用のLinuxソースコードからDTBを作ってみます。まず、ソースコードを取得します。また、ビルドに必要なツールもインストールしておきます。基本的には(https://www.raspberrypi.org/documentation/linux/kernel/building.md) の通りです。ネイティブビルドなので、他に準備は不要です。

sudo apt-get install git bc
git clone https://github.com/raspberrypi/linux.git

ラズパイ用のDTSファイルを編集してみる

Raspberry Pi 2 Model B用のDTSファイルは/linux/arch/arm/boot/dts/bcm2709-rpi-2-b.dtsになります。(このファイルは、先ほど逆コンパイルしたdtsファイルよりもだいぶすっきりしています。これは、複数ファイルに分かれているためです。このdtsファイルは、先頭でbcm2709.dtsiをincludeしています。先ほど逆コンパイルしたdtsファイルは全部入りだからごちゃごちゃしているのだと思われます。)

/linux/arch/arm/boot/dts/bcm2709-rpi-2-b.dtsを先ほどと同じように編集します。I2C_1の所に、デバイスを追加します。

/linux/arch/arm/boot/dts/bcm2709-rpi-2-b.dts
省略
&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;
    clock-frequency = <100000>;

    /* 自分用のデバイスを追加 */
    mydevice@18 {
        compatible = "mycompany,myoriginaldevice";
        reg = <0x18>;
    };
};
省略

DTBをビルドしてみる

DTSファイルの編集が終わったら、以下のコマンドでDTBだけをコンパイルします。ラズパイ上のコンパイルでも1分かからずに終わります。出来上がったDTBファイルを/bootにコピーします。

cd ~/linux
KERNEL=kernel7
make bcm2709_defconfig
make -j4 dtbs
sudo cp arch/arm/boot/dts/*.dtb /boot/
sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/
sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/

ノート

overlays用DTBは、カーネル起動後に、動的に接続状態が変わるときに使うらしいです。なので、特にコピーはしないでもいいかも。

動かしてみる

デバイスドライバは、先ほどの手抜きバージョンと同じでOKです。再起動すると、同じ結果になるはずです。

おわりに

I2Cデバイスを例に、デバイスツリーをいじってみました。この記事で触れた内容はまだまだデバイスツリーの触りにすぎません。デバイスツリーについて、もっと深く知るためには、こちらの記事+リンク先をご参考にしてください。