Day 10:I2C Driver (Part 1) - 使用 Device Tree 來找 Driver

前面提到:Device Tree 可以提供硬體的資訊,而 driver 可以借助這個資訊來「找到」對應的硬體。所以可以互相搭配來幫 Device Tree 上描述到的硬體來找到對應的 driver

  1. Device Tree 方面:任務是更動 Device Tree,想辦法在目前的 Device Tree 加上一些東西告訴他說「現在多了這個硬體」。或是用專業的術語:在 Device Tree 上新增某個硬體對應的節點,並且在 compatible 的屬性中,加上硬體相容的相關資訊。而這件事情可以用 device tree overlay 來做到。
  2. Driver 方面:在 driver 中加上他該支援的硬體的相關資訊。這個結構會是個 of_device_id 的陣列,到時候要加到 device 這個結構的 of_match_table 中。

這樣一來,在配對的時候,就可以同時比對 1. 跟 2. 的資訊去知道哪個 driver 需要負責處理哪個 device tree 中的節點。而這就是接下來要做的實驗。

接下來的情況是:假想現在要 i2c1 這個 I2C 上的 0x8 位置有個 Arduino 。然後不知道為什麼我就是很想弄成 I2C Driver,要看看能不能幫這個 Arduino 弄一個 i2c driver 出來。為了簡單,現在這個驅動程式只會幫 Arduino 佔用一個 i2c 的位址,其他什麼都不會做。

Step 1:Device Tree 方面

要新增節點,除了暴力改原來的 Device Tree 之外,另外一個方法就是使用 Device Tree Overlay。前面講到:Device Tree 後面的東西可以疊在前面的上面,現在這邊就是要利用這個特性來「加上」新的硬體描述。這樣就可以更有彈性地調整與維護 Device Tree,也比較不用擔心原來的東西被改到爛掉。

Step 1 - 0:原先的 Device Tree

在開始之前,可以看看一下原先的 device tree 長什麼樣子。這可以去查看 /proc/device-tree 這個資料夾。這個資料夾的階層會對應 device tree 的階層,所以如果用 tree 這個程式去看這個資料夾的話:

$ tree /proc/device-tree | less

就會發現這個資料夾其實就是把 device tree 中每一個節點及屬性,都用資料夾與檔案顯示出來 (就像 sysfs 那樣)。以 i2c1 的部分為例,大致上像下面這樣:

│   ├── i2c@7e804000
│   │   ├── #address-cells
│   │   ├── clock-frequency
│   │   ├── clocks
│   │   ├── compatible
│   │   ├── interrupts
│   │   ├── name
│   │   ├── phandle
│   │   ├── pinctrl-0
│   │   ├── pinctrl-names
│   │   ├── reg
│   │   ├── #size-cells
│   │   └── status

為什麼知道 i2c@7e804000i2c1 是同一個東西呢?這可以去 /proc/device-tree/aliases 查:

$ ls /proc/device-tree/aliases

就會發現裡面是:

audio      gpio   i2c_arm  mmc     serial0  spi2
aux        i2c    i2c_vc   mmc0    serial1  thermal
axiperf    i2c0   i2s      mmc1    soc      uart0
dma        i2c1   intc     name    sound    uart1
ethernet0  i2c10  leds     random  spi0     usb
fb         i2c2   mailbox  sdhost  spi1     watchdog

接著 cat i2c1,就會發現:

/soc/i2c@7e804000

事實上,alias 就是 device tree 冒號前面的那個東西。以下面這個例子為例, i2c@7e804000 這個節點的 alias 就是 i2c1。這個 alias 可以方便之後在 Device Tree 的其他部分去直接參考他。

i2c1: i2c@7e804000 {
    ...
};

而接下來的任務,就是要想辦法在上面加上一個節點。

Step 1 - 1:Device Tree Overlay

現在這個問題是這樣:在 device tree 中,i2c 部分的硬體描述可能是像這樣的:

i2c1: i2c@7e804000 {
    ...
};

然後現在希望達成的結果的是:這裡面中多出想要加入的硬體。比如說在 0x8 上面希望多一塊 Arduino。也就是最後的結果大概像這樣:

i2c1: i2c@7e804000 {
    ...
    arduino-i2c@8 {
        ...
        reg = <0x8>;
    };
};

Step 1 - 2:寫出要疊上去的部分

要做到這件事情,要把要疊的東西放在不同的 fragment@n{ ... }; 中,其中 n 依序從 0 開始往上增加。比如說現在要新增一個 arduino-i2c 佔用 i2c 的 0x8 位址,那就把 target 設定成 &i2c1,並且把要疊上去的東西寫在 __overlay__ 當中:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";
    fragment@0 {
        target = <&i2c1>;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            arduino-i2c@8 {
                compatible = "arduino";
                reg = <0x8>;
            };
        };
    };
};

這邊除了提供位址是 0x8 之外,另外一件重要的事情是 compatible 的字串。等一下在 *driver* 中的時候,就會以這個字串為配對的依據。 寫完之後,假定這個檔案叫做 arduino-i2c.dts,接下來就用 dtc 編譯成 .dtbo 檔:

$ dtc -@ -I dts -O dtb -o arduino-i2c.dtbo arduino-i2c.dts

然後移動到 /boot/overlays/ 裡面:

$ sudo cp arduino-i2c.dtbo /boot/overlays/

Step 3 - 3:修改 /boot/config.txt

最後,修改 /boot/config.txt,把 dtoverlay 多加上要 overlay 的 .dtbo 檔。以這邊為例,就是 arduino-i2c,所以用 vim 編輯:

$ sudo vim /boot/config.txt

然後在檔案中,新增下面的部分:

# Uncomment this to enable infrared communication.
#dtoverlay=gpio-ir,gpio_pin=17
#dtoverlay=gpio-ir-tx,gpio_pin=18

+# Uncomment this to use arduino i2c transfer
+dtoverlay=arduino-i2c
+dtdebug=1

# Additional overlays and parameters are documented /boot/overlays/README

這邊把 dtdebug 也打開,這樣就可以用 vcdbg 這個工具除錯。

按:如果有需要的話,可以先把原先的 config.txt 備份起來

Step 3 - 4:重新開機

重新開機之後,可以用 vcdbg 來看看出現了什麼訊息:

$ sudo vcdbg log msg

如果成功的話,就會出現 Open overlay ... 之類的訊息,像下面這樣:

[...]
001812.784: dtdebug:   override i2c_arm: string target 'status'
001818.938: brfs: File read: 1911 bytes
+001833.006: dtdebug: Opened overlay file 'overlays/arduino-i2c.dtbo'
001833.503: brfs: File read: /mfs/sd/overlays/arduino-i2c.dtbo
001839.485: Loaded overlay 'arduino-i2c'
001839.570: dtparam: audio=on
[...]

(失敗的話會出現類似 001368.106: dtdebug: Failed to open overlay file "..." 的訊息)

而這時如果去 /ptoc/device-tree 看的話:

$ tree /proc/device-tree | less

就會發現多出了剛剛新增的節點:

│   ├── i2c@7e804000
│   │   ├── #address-cells
│   │   ├── arduino-i2c@8
│   │   │   ├── compatible
│   │   │   ├── name
│   │   │   └── reg
│   │   ├── clock-frequency
│   │   ├── clocks
│   │   ├── compatible
│   │   ├── interrupts
│   │   ├── name
│   │   ├── phandle
│   │   ├── pinctrl-0
│   │   ├── pinctrl-names
│   │   ├── reg
│   │   ├── #size-cells
│   │   └── status

Step 2:I2C Client Driver 方面

現在已經知道 i2c1 中上面有掛一個 compatiblearduino 的硬體了,接下來的 driver 就要想辦法說「我可以給 compatible 裡面有 arduino 的東西用!」。而這個資訊要記錄在 of_device_id 這個結構中。

Step 2 - 1:提供 of_device_id

這時把前面 device tree overlay 時,填寫的 compatible 裡面的字串寫進去。並且注意最後面要多留一個空白的:

static struct of_device_id dummy_id_tables [] = {
    { .compatible="arduino", },
    { }
};

這個結構可以在 include/linux/mod_devicetable.h 中找到。結構大概長成這樣:

/*
 * Struct used for matching a device
 */
struct of_device_id {
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void *data;
};

除了 compatible 這個屬性之外,另一個重要的東西是 data:這裡面可以存某個裝置需要用的資料。可以想像如果 compatible 不只有一個,那麼就可以依照不同的硬體,去提供不同的資料。

Step 2 - 2:註冊 of_device_id

接下來要把這個東西用 MODULE_DEVICE_TABLE 註冊:

MODULE_DEVICE_TABLE(of, dummy_id_tables);

Step 2 - 3:填進 i2c_driver 結構中

之後要把這個 of_device_id 填進 i2c_driverstruct deviceof_match_table 裡面。所以到時候就會出現類似這樣的東西:

static struct i2c_driver dummy_drv = {
    .probe = dummy_probe,
    .remove = dummy_remove,
    .driver = {
        .name = "dummy device 0.1",
    .owner = THIS_MODULE,
    .of_match_table = dummy_id_tables,
    },
};

不過,這個東西除了填進 of_match_table 之外,還要填進 proberemove。因為 proberemove 都還沒有做,所以現在先把他們做出來。

Step 2 - 4:實作 probe 跟 remove

proberemove 兩個函數實作出來。因為現在只是要做一個「佔某一個 I2C 位子」的 driver,為了方便這邊就只印東西出來:

static int dummy_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    pr_info("Dummy device is being probed.\n");
    return 0;    
}

static int dummy_remove(struct i2c_client *client)
{
    pr_info("Dummy device is removing.\n");
    return 0;
}

Step 2 - 5:完整的程式碼

這時,完整的程式大概就會像這樣:

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

static int dummy_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    pr_info("Dummy device is being probed.\n");
    return 0;    
}

static int dummy_remove(struct i2c_client *client)
{
    pr_info("Dummy device is removing.\n");
    return 0;
}

static struct of_device_id dummy_id_tables [] = {
    { .compatible="arduino", },
    { }
};

MODULE_DEVICE_TABLE(of, dummy_id_tables);

static struct i2c_driver dummy_drv = {
    .probe = dummy_probe,
    .remove = dummy_remove,
    .driver = {
        .name = "dummy device 0.1",
    .owner = THIS_MODULE,
    .of_match_table = dummy_id_tables,
    },
};

module_i2c_driver(dummy_drv);
MODULE_LICENSE("GPL");

這邊可能會納悶為什麼唯有 module_initmodule_exit,可是多了 module_i2c_driver。這是因為 module_i2c_driver 自動就會幫忙做這件事。如果這整個模組初始化跟結束的時候都沒有其他的事情要做,那麼可以用他。可以參考 include/linux/i2c.h 中的敘述:

/**
 * module_i2c_driver() - Helper macro for registering a modular I2C driver
 * @__i2c_driver: i2c_driver struct
 *
 * Helper macro for I2C drivers which do not do anything special in module
 * init/exit. This eliminates a lot of boilerplate. Each module may only
 * use this macro once, and calling it replaces module_init() and module_exit()
 */
#define module_i2c_driver(__i2c_driver) \
    module_driver(__i2c_driver, i2c_add_driver, \
            i2c_del_driver)

Step 2 - 6:Makefile

Makefile 其實跟之前的核心模組相差不大,只是程式名稱跟模組的名稱有變而已。不過為了完整還是放上來:

PWD := $(shell pwd)
KVERSION := $(shell uname -r)
KERNEL_DIR := /lib/modules/$(shell uname -r)/build

MODULE_NAME = dummy_i2c_drv
obj-m := $(MODULE_NAME).o

all:
    make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
    make -C $(KERNEL_DIR) M=$(PWD) clean

Step 2 - 7:編譯、插入、確認結果

使用 make 進行編譯:

$ make

這時候裡面會出現一個 dummy_i2c_drv.ko。這個就是核心模組。在尚未插入前,i2cdetect 的結果是像下面這樣的:沒有人在用 I2C:

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

現在把這個模組載入:

$ sudo insmod dummy_i2c_drv.ko

接著再用一次 i2cdetect,就會發現剛剛 Arduino 的那個 0x8 位址現在被一個核心模組佔用住了:

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- UU -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

而如果這時查看 dmesg

$ dmesg

也會發現裡面出現了 probe 成功的訊息:

[ 1329.705486] dummy_i2c_drv: loading out-of-tree module taints kernel.
[ 1547.898723] Dummy device is being probed.

最後,把這個模組移除:

$ sudo rmmod dummy_i2c_drv

接著再一次 i2cdetect,就會發現沒有核心模組佔用這個 I2C 了:

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

最後,看看移除的時候,dmesg 出現了什麼訊息:

$ dmesg

這時就發現了剛剛 remove 中要印出來的東西:

[ 4205.630400] Dummy device is removing.