10回目: I2Cを使ったデバイスドライバを作る
本連載について
組み込みLinuxのデバイスドライバをカーネルモジュールとして開発するためのHowTo記事です。本記事の内容は全てラズパイ(Raspberry Pi)上で動かせます。
- 1回目: ビルド環境準備と、簡単なカーネルモジュールの作成
- 2回目: システムコールハンドラとドライバの登録(静的な方法)
- 3回目: システムコールハンドラとドライバの登録(動的な方法)
- 4回目: read/writeの実装とメモリのお話
- 5回目: ラズパイ用のGPIOデバドラの実装
- 6回目: ioctlの実装
- 7回目: procfs用インタフェース
- 8回目: debugfs用インタフェース
- 9回目: 他のカーネルモジュールの関数を呼ぶ / GPIO制御関数を使う
- 10回目: I2Cを使ったデバイスドライバを作る <--------------------- 今回の内容
- 11回目: デバイスツリーにI2Cデバイスを追加する
- 12回目: 作成したデバイスドライバを起動時にロードする
本記事に登場するソースコード全体
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/10_01
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/10_02
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/10_03
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/10_04
今回の内容
前回、GPIO制御関数を使ってLED/ボタンを制御するデバイスドライバを作りました。今回は、I2Cで接続されたデバイスのデバイスドライバを作ります。I2Cそのもののデバイスドライバではありません。前回のGPIOと同様に、I2Cのデバドラ自体はSoCメーカーなどが実装したものが用意されています。そして、そのデバイスドライバは<linux/i2c.h>
で宣言されている標準的な関数で呼ぶことが出来ます。ですので、積極的に使うべきです。移植性、汎用性、信頼性が高まります。
対象ボードとしてはRaspberry Pi 2。対象I2Cデバイスは加速度センサ(LIS3DH)として、説明します。しかし、デバイスそのものの制御が目的ではないので、特にこれは重要ではないです。加速度センサを使用しますが、今回はデバイスIDの取得しかしません。これは、シンプルに通信確認だけをしたいためです。特に意味はありません。他の情報でも確認しやすいものがあればそれでもかまいません。以下の情報だけ、事前に固定させてください。
- ラズパイのI2C_1とLIS3DHのSCL, SDAを接続する
- LIS3DHのスレーブアドレスは0x18
- LIS3DHのデバイスIDを読むには、0x0F番地のレジスタを読む。正しく読めたら0x33が返ってくる
事前準備と確認 (ラズパイ用)
raspi-config
でI2Cを有効にしてください。その後、下記コマンドで通信出来ていることを確認してください。i2cdetect
でI2C_1に接続されている全デバイス情報が確認できます。i2cget
でLIS3DHのデバイスIDを取得しています。0x33と出力されればOKです。そうでない場合は、何かがおかしいので、接続などを確認してください。
i2cdetect -y 1
i2cget -y 1 0x18 0x0f b
0x33
I2C周りのお話
GPIOのように、関数コール1つで簡単に制御できればいいのですが、I2Cの場合は少し複雑です。いくつかの事前知識が必要になります。
I2Cデバイスの制御は、複数の階層に分かれたモジュール達によって実現されています。ハードウェアから見ていきます。まず、I2Cそのものがあります(I2C Bus)。Raspberry Piの場合は、I2Cは全部で3つあり、それぞれI2C_0、I2C_1、I2C_2で、別々のI2Cバスになります。それぞれのバスにつながった、I2C機器があります。例えば、I2C_1のバスに加速度センサ(LIS3DH、アドレス=0x18)が接続されたいたら、それは一つのI2C Clientになります。
ソフトウェア側を下から見てみます。まず、I2C Busを制御するI2C Adapterがあります。I2Cそのものの制御は、SoCに依存する処理です。そのため、SoC毎に別々のソースコードがあります。ラズパイの場合は、i2c-bcm2835.cになります。また、通信時に用いられるアルゴリズムが書かれたi2c-algoがあります。i2c-core.cがメインの処理で、i2c_smbus_read_byte()
といった汎用的な関数を提供します。I2C Client Driversがそのi2c_smbus_read_byte()
などを使用して、各I2C機器の制御を行います。また、汎用的なi2cデバイスファイル(/dev/i2c-1
)を用意するために、i2c-dev.cがいます。これらは全てカーネル側のコードです。
レイヤ | 名称 | 説明 | |
---|---|---|---|
SW | ↑ | I2C Client Drivers | 各I2C機器のデバイスドライバ、i2c-dev |
| | i2c-core | I2C通信のメイン処理 | |
| | i2c-algo-XXX | 通信用アルゴリズム | |
↓ | I2C Adapters | I2Cそのものの制御をするところ。レジスタをいじったり、チップ依存の処理 | |
HW | - | I2C Bus | 例えば、I2C_0、I2C_1 |
- | I2C Client | 各I2C機器 (例えば、加速度センサ(LIS3DH)) |
今回、我々が作るのは、上記の表にあるI2C Client Driver、具体的にはI2C接続された加速度センサ(LIS3DH)のデバイスドライバになります。そのため、「i2c-coreの関数を呼ぶだけでしょ?」と思うかもしれませんが、そう簡単にはいきません。実際にどうなるかは、後述しますが、ここではひとまず、上の表の関係を理解しておいてください。
簡単なI2C機器のデバイスドライバ
一番基本的な、I2C機器のデバイスドライバのコードが下記になります。色々と見慣れないものがあります。コードの下の方から見ていきます。mydevice_init()
とmydevice_exit()
が、本デバイスドライバがロード/アンロードされたときの処理です。その中で、i2c_add_driver()
とi2c_del_driver()
を呼んでいます。引数には、何やらテーブルを渡しています。
I2Cは「バス」で接続するインターフェースです。そのため、このデバイスドライバがロードされているからといって、常に対象となるI2C機器(今回の場合は加速度センサ)が接続されているとは限りません。I2Cだとイメージしづらいのですが、USBをイメージしたらわかりやすいと思います。そのため、I2Cバスに機器が接続/切断されたときの処理が必要になります。その時の処理等を登録/解除するのがi2c_add_driver()
とi2c_del_driver()
になります。登録するテーブルの型はstruct i2c_driver
になります。このデバイスドライバでサポートするI2C機器情報を.id_table
に登録します。登録内容は先頭の方で宣言しているstruct i2c_device_id mydevice_i2c_idtable[]
になります。第1メンバが対応するデバイスの名前になります。カーネルはこの名前(char name[])で対応するデバドラを探すので、非常に重要なメンバになります(ユニーク番号などではない!)。第2メンバはこのデバイスドライバ内で使うプライベートデータになります。何でもいいのですが、通常は識別用の数字を入れるようです。カーネルがI2C機器の接続/切断の時に呼ぶ関数を.probe
と.remove
に登録します。.driver
にはこのデバイスドライバの名前などを登録します。
mydevice_i2c_probe()
内で、i2c_smbus_read_byte_data()
関数を呼んで、認識されたI2C機器と通信しています。今回の場合は、LIS3DHが接続されたとしているので、0x0F番地の値(機器ID)を読んでいます。i2c_smbus_read_byte_data()
に渡す第1引数はstruct i2c_client
で、その中にI2Cバス情報(使用するI2C Adapter)やスレーブアドレスを格納します。これらの情報は、この関数mydevice_i2c_probe()
が呼ばれるときに貰えます。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <asm/uaccess.h>
/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice" /* /proc/devices等で表示されるデバイス名 */
/* このデバイスドライバで取り扱うデバイスを識別するテーブルを登録する */
/* 重要なのは最初の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("i2c_lcd_probe\n");
printk("id.name = %s, id.driver_data = %d", id->name, id->driver_data);
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,
},
.id_table = mydevice_i2c_idtable, // このデバイスドライバがサポートするI2Cデバイス
.probe = mydevice_i2c_probe, // 対象とするI2Cデバイスが認識されたときに呼ばれる処理
.remove = mydevice_i2c_remove, // 対象とするI2Cデバイスが取り外されたときに呼ばれる処理
};
/* ロード(insmod)時に呼ばれる関数 */
static int mydevice_init(void)
{
printk("mydevice_init\n");
/* 本デバイスドライバを、I2Cバスを使用するデバイスドライバとして登録する */
i2c_add_driver(&mydevice_driver);
return 0;
}
/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
printk("mydevice_exit\n");
i2c_del_driver(&mydevice_driver);
}
module_init(mydevice_init);
module_exit(mydevice_exit);
ノート: お決まりの処理を簡略化
I2C機器用のデバイスドライバでは、ロード/アンロード時に行う処理は、ほとんどの場合struct i2c_driver
の登録だけです。そのため、以下のように簡略化できます。
module_i2c_driver(mydevice_driver);
動かしてみる
以下のように、ビルド、ロードして、ログを見てみます。
make && sudo insmod MyDeviceModule.ko
dmesg
[ 8589.545480] mydevice_init
ログを見ると、mydevice_init()
が呼ばれただけで、mydevice_i2c_probe()
が呼ばれていません。これは、先ほど説明した通り、mydevice_i2c_probe()
は機器が認識されたときに呼ばれるのですが、現時点では何のI2C機器も接続されていないためです。USBだったらおそらく自動で認識されるのでしょうが、I2Cの場合は手動で教えてあげる必要があります。/sys/bus/i2c/devices/i2c-1/new_device
に値を書き込むことで、カーネルに新しいデバイスを認識させます。今回は、本デバイスドライバ用の機器が接続されたことにしたいので、「MyI2CDevice」という名前のデバイスがスレーブアドレス=0x18で接続されたことにします。このセットの情報が、先ほどの表で出た「I2C Client」の実体になります。
sudo bash -c 'echo MyI2CDevice 0x18 > /sys/bus/i2c/devices/i2c-1/new_device'
dmesg
[ 9179.410883] mydevice_i2c_probe
[ 9179.410902] id.name = MyI2CDevice, id.driver_data = 0
[ 9179.410912] slave address = 0x18
[ 9179.411301] id = 0x33
[ 9179.412045] i2c i2c-1: new_device: Instantiated device MyI2CDevice at 0x18
すると、上のログのように、mydevice_i2c_probe()
が呼ばれています。また、その時に渡されるclient
パラメータには、実体化したI2C Clientの情報が入っていることが分かります。そして、その情報(cLient)を使って、通信ができ、正しいidを取得出来ていることが分かります。
ノート 1
今回は、『「MyI2CDevice」という名前の機器を取り扱う』、と決めました。そのため、struct i2c_device_id
テーブルに「MyI2CDevice」という名前を設定してカーネルに登録しました。そして、手動で「MyI2CDevice」というデバイスをカーネルに認識させたので、本デバイスドライバのmydevice_i2c_probe()
が呼ばれました。カーネルは、あくまで名前だけで判断しているため、他のデバイスと名前がかぶってしまった場合などに、間違った機器を制御しようとしてしまう可能性があります。mydevice_i2c_probe()
の probe というところに注意してほしいのですが、この関数で本来やるべきは、このデバイスドライバでこの機器を取り扱うかどうかのチェックになります。もしも対応機器であれば0を返して、そうでなければ-1を返します。その確認のために、通常はスレーブアドレスを確認したり、通信して機器のIDやバージョン情報を取得したりします。今回は決め打ちで常に0を返しています。
ノート 2
機器の取り外しには以下のコマンドを使用します。
sudo bash -c 'echo 0x18 > /sys/bus/i2c/devices/i2c-1/delete_device'
コード上でI2C機器を認識させる
先ほどの例では、手動でシェルスクリプトから機器を認識させました。おそらく推奨はされないと思うのですが、以下のようにコードからも同じことが出来ます。
/* ロード(insmod)時に呼ばれる関数 */
static struct i2c_client *i2c_clie = NULL;
static int mydevice_init(void)
{
printk("mydevice_init\n");
/* 本デバイスドライバを、I2Cバスを使用するデバイスドライバとして登録する */
i2c_add_driver(&mydevice_driver);
/* 動的にデバイス実体を作る (https://www.kernel.org/doc/Documentation/i2c/instantiating-devices) */
/* I2C1に接続された、"MyI2CDevice"という名前で、スレーブアドレスが0x18のデバイスを作る */
struct i2c_adapter *i2c_adap;
i2c_adap = i2c_get_adapter(1);
struct i2c_board_info i2c_board_info = {
I2C_BOARD_INFO("MyI2CDevice", 0x18)
};
i2c_clie = i2c_new_device(i2c_adap, &i2c_board_info);
i2c_put_adapter(i2c_adap);
return 0;
}
/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
printk("mydevice_exit\n");
i2c_del_driver(&mydevice__driver);
if(i2c_clie) i2c_unregister_device(i2c_clie);
}
module_init(mydevice_init);
module_exit(mydevice_exit);
この方法は、Linuxのドキュメント(instantiating-devices)にも記載されていたので、実装自体はOKなのですが、実装場所がよろしくないです。どういった機器が接続されるかというのは、ボード依存の情報なので、board_bcm2835.cのbcm2835_init(void)
あたりに書くべきです。さらに、i2c_register_board_info()
を使った方がいいみたいです。
また、デバイスツリー(.dts)に記載する方法もあるようですが、そこはまだよく分かっていません。
sysfsを使ったインターフェース
ここまでで、I2C機器の認識と、最初の通信が出来ました。続いて、ユーザ、あるいはユーザ空間のプログラムとのインターフェースを作ります。基本的にはいつも通り、デバイスファイルを使ったやり取りになります。まずは簡単にできるsysfsを使ったインターフェースを作ります。sysfsについては(7回目)[https://qiita.com/take-iwiw/items/548444999d2dfdc06f46#%E3%83%8E%E3%83%BC%E3%83%882-sysfs] で軽く触れました。この時は、モジュール のパラメータとして扱いました。今回は、I2C機器のパラメータとして取り扱う必要があります。
機器ID(バージョン情報)を取得するだけの簡単なインターフェースを考えます。手順は非常に簡単で、①readされたときのハンドラ関数の定義、②作成するデバイスファイルの属性設定、③カーネルに登録する、です。②のためにはDEVICE_ATTR
というヘルパーマクロを使用します。③のためにはdevice_create_file()
関数をprobeのタイミングで呼びます。
「version」というファイルを作成して、読んだときにget_version()
という関数を呼ぶコードを下記に示します。DEVICE_ATTR
の第1引数が作成するファイル名になります。第2引数がアクセス権限。第3引数がread時のハンドラ関数で、第4引数がwrite時のハンドラ関数になります。今回、write操作は行わないので、NULL指定しています。read時のハンドラ関数に設定したget_version()
では、先ほどと同じく、機器ID情報を読んで、bufに詰めて返しているだけです。I2C関数を使うためには、struct i2c_client
の情報が必要なのですが、この関数にはstruct device
が渡されます。しかし、この中にstruct i2c_client
が入っているので、簡単に取得できます。probeのタイミングでdevice_create_file()
を使用して、登録します。第1引数は同じくstruct device
です。第2引数は登録するファイルの属性一式になります。これは先ほどのDEVICE_ATTR
マクロによって作られています。DEVICE_ATTR
の第1引数に設定した名前に、"dev_attr_"というプリフィックスをつけたものを渡します。
static ssize_t get_version(struct device *dev, struct device_attribute *dev_attr, char * buf)
{
printk("get_version\n");
struct i2c_client * client = to_i2c_client(dev);
int version;
version = i2c_smbus_read_byte_data(client, 0x0f);
return sprintf(buf, "id = 0x%02X\n", version);
}
static DEVICE_ATTR(version, S_IRUGO, get_version, NULL);
static int mydevice_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
printk("mydevice_i2c_probe\n");
printk("id.name = %s, id.driver_data = %d", id->name, (int)(id->driver_data));
printk("slave address = 0x%02X\n", client->addr);
/* 通常はここで、このデバドラでサポートしているデバイスかどうかチェックする */
/* このデバイスドライバの属性読み書き用のsysfsファイルを作る */
/* (/sys/devices/platform/soc/3f804000.i2c/i2c-1/1-0018/version) を作る */
device_create_file(&client->dev, &dev_attr_version);
return 0;
}
static int mydevice_i2c_remove(struct i2c_client *client)
{
printk("mydevice_i2c_remove\n");
device_remove_file(&client->dev, &dev_attr_version);
return 0;
}
その後、以下のようにビルド、ロード、デバイスの認識を行います。すると、/sys/devices/platform/soc/3f804000.i2c/i2c-1/1-0018/version
というファイルが作成され、catで読むと、idが取得出来ていることが分かります。
make && sudo insmod MyDeviceModule.ko
sudo bash -c 'echo MyI2CDevice 0x18 > /sys/bus/i2c/devices/i2c-1/new_device'
cat /sys/devices/platform/soc/3f804000.i2c/i2c-1/1-0018/version
id = 0x33
/devを使ったインターフェース
sysfsだとお手軽にできて便利なのですが、やはりいつものopen/close/read/write/ioctl (+ select/poll/seekなどなど)を使いたい場合もあると思います。そのためには、今まで通りの方法で、デバイスファイルを作る必要があります。今まではロードのタイミングでデバイスファイルを作っていましたが、今回はprobeのタイミングになります。また、I2C制御関数を使うためには、struct i2c_client
が必要になります。probeのタイミングで貰ったstruct i2c_client
をどこかに保持して、readやwriteの時の参照する必要があります。これは、static変数で保持すれば簡単にできるのですが、少しちゃんとやろうと思います。これを実現するために、container_of()
というヘルパーマクロを使います。
事前知識
container_of
container_of()
は以下で定義されるヘルパーマクロで、コンパイル時に実行されます。
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
#endif
type
には構造体名、member
にはその構造体内のメンバ名を入れます。そして、ptr
にはそのメンバの実際のポインタを入れます。すると、そのptr
を格納する構造体の先頭アドレスを返してくれます。
i2c_set_clientdata
i2c_set_clientdata()
は以下で定義される関数です。struct i2c_client
に紐づいた情報を自由に保存することが出来ます。この情報は何でもいいのですが、通常は、probe時にallocしたデバイスドライバ独自の構造体へのポインタを保持します。取り出すときはi2c_get_clientdata()
を使います。
void i2c_set_clientdata(struct i2c_client *dev, void *data)
コード
probe時に貰ったstruct i2c_client
を、open/close/read/write時に参照できるようにしたコードが下記になります。まず、ガバっとコードを載せてしまいます。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <asm/uaccess.h>
/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice" /* /proc/devices等で表示されるデバイス名 */
static const unsigned int MINOR_BASE = 0; /* このデバイスドライバで使うマイナー番号の開始番号と個数(=デバイス数) */
static const unsigned int MINOR_NUM = 1; /* マイナー番号は 0 */
/*** I2Cデバイスの管理情報 ***/
/* このデバイスドライバで取り扱うI2Cデバイス */
enum mydevice_i2c_model {
MYDEVICE_MODEL_A = 0,
MYDEVICE_MODEL_NUM,
};
/* このデバイスドライバで取り扱うデバイスを識別するテーブルを登録する */
/* 重要なのは最初のnameフィールド。これでデバイス名を決める。後ろはこのドライバで自由に使えるデータ。ポインタや識別用数字を入れる */
static struct i2c_device_id mydevice_i2c_idtable[] = {
{"MyI2CDevice", MYDEVICE_MODEL_A},
{ }
};
MODULE_DEVICE_TABLE(i2c, mydevice_i2c_idtable);
/* 各I2Cデバイス(client)に紐づく情報。probe時に設定してi2c_set_clientdataで保持しておく */
struct mydevice_device_info {
struct cdev cdev; /* probeされたI2Cデバイス(client)とcdevを対応付けるために必要。open時にcontainer_ofで探す */
unsigned int mydevice_major; /* このデバイスドライバのメジャー番号(動的に決める) */
struct class *mydevice_class; /* デバイスドライバのクラスオブジェクト */
struct i2c_client *client;
/* 他に必要なら追加する。mutexなど */
};
/* open時に呼ばれる関数 */
static int mydevice_open(struct inode *inode, struct file *file)
{
printk("mydevice_open");
/* このopenを持つcdev(inode->i_cdev)を持つmydevice_device_info を探す */
struct mydevice_device_info *dev_info;
dev_info = container_of(inode->i_cdev, struct mydevice_device_info, cdev);
if (dev_info == NULL || dev_info->client == NULL) {
printk(KERN_ERR "container_of\n");
return -EFAULT;
}
file->private_data = dev_info;
printk("i2c address = %02X\n",dev_info->client->addr);
return 0;
}
/* close時に呼ばれる関数 */
static int mydevice_close(struct inode *inode, struct file *file)
{
printk("mydevice_close");
return 0;
}
/* read時に呼ばれる関数 */
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_read");
struct mydevice_device_info *dev_info = filp->private_data;
struct i2c_client * client = dev_info->client;
int version;
version = i2c_smbus_read_byte_data(client, 0x0f);
return sprintf(buf, "id = 0x%02X\n", version);
}
/* write時に呼ばれる関数 */
static ssize_t mydevice_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_write");
return count;
}
/* 各種システムコールに対応するハンドラテーブル */
struct file_operations s_mydevice_fops = {
.open = mydevice_open,
.release = mydevice_close,
.read = mydevice_read,
.write = mydevice_write,
};
static int mydevice_i2c_create_cdev(struct mydevice_device_info *dev_info)
{
int alloc_ret = 0;
int cdev_err = 0;
dev_t dev;
/* 空いているメジャー番号を確保する */
alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
if (alloc_ret != 0) {
printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret);
return -1;
}
/* 取得したdev( = メジャー番号 + マイナー番号)からメジャー番号を取得して保持しておく */
dev_info->mydevice_major = MAJOR(dev);
dev = MKDEV(dev_info->mydevice_major, MINOR_BASE); /* 不要? */
/* cdev構造体の初期化とシステムコールハンドラテーブルの登録 */
cdev_init(&dev_info->cdev, &s_mydevice_fops);
dev_info->cdev.owner = THIS_MODULE;
/* このデバイスドライバ(cdev)をカーネルに登録する */
cdev_err = cdev_add(&dev_info->cdev, dev, MINOR_NUM);
if (cdev_err != 0) {
printk(KERN_ERR "cdev_add = %d\n", alloc_ret);
unregister_chrdev_region(dev, MINOR_NUM);
return -1;
}
/* このデバイスのクラス登録をする(/sys/class/mydevice/ を作る) */
dev_info->mydevice_class = class_create(THIS_MODULE, "mydevice");
if (IS_ERR(dev_info->mydevice_class)) {
printk(KERN_ERR "class_create\n");
cdev_del(&dev_info->cdev);
unregister_chrdev_region(dev, MINOR_NUM);
return -1;
}
/* /sys/class/mydevice/mydevice* を作る */
for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
device_create(dev_info->mydevice_class, NULL, MKDEV(dev_info->mydevice_major, minor), NULL, "mydevice%d", minor);
}
return 0;
}
static void mydevice_i2c_delete_cdev(struct mydevice_device_info *dev_info)
{
dev_t dev = MKDEV(dev_info->mydevice_major, MINOR_BASE);
/* /sys/class/mydevice/mydevice* を削除する */
for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
device_destroy(dev_info->mydevice_class, MKDEV(dev_info->mydevice_major, minor));
}
/* このデバイスのクラス登録を取り除く(/sys/class/mydevice/を削除する) */
class_destroy(dev_info->mydevice_class);
/* このデバイスドライバ(cdev)をカーネルから取り除く */
cdev_del(&dev_info->cdev);
/* このデバイスドライバで使用していたメジャー番号の登録を取り除く */
unregister_chrdev_region(dev, MINOR_NUM);
}
static int mydevice_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
printk("mydevice_i2c_probe\n");
printk("id.name = %s, id.driver_data = %d", id->name, (int)(id->driver_data));
printk("slave address = 0x%02X\n", client->addr);
/* 通常はここで、このデバドラでサポートしているデバイスかどうかチェックする */
/* open/close/read/writeでもi2c_clientは使うので、保持する */
struct mydevice_device_info *dev_info;
dev_info = (struct mydevice_device_info*)devm_kzalloc(&client->dev, sizeof(struct mydevice_device_info), GFP_KERNEL);
dev_info->client = client;
i2c_set_clientdata(client, dev_info);
/* このデバイスドライバをキャラクタ型としてカーネルに登録する。(/sys/class/mydevice/mydevice* を作る) */
if(mydevice_i2c_create_cdev(dev_info)) return -ENOMEM;
return 0;
}
static int mydevice_i2c_remove(struct i2c_client *client)
{
printk("mydevice_i2c_remove\n");
struct mydevice_device_info *dev_info;
dev_info = i2c_get_clientdata(client);
mydevice_i2c_delete_cdev(dev_info);
return 0;
}
static struct i2c_driver mydevice_driver = {
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
},
.id_table = mydevice_i2c_idtable, // このデバイスドライバがサポートするI2Cデバイス
.probe = mydevice_i2c_probe, // 対象とするI2Cデバイスが認識されたときに呼ばれる処理
.remove = mydevice_i2c_remove, // 対象とするI2Cデバイスが取り外されたときに呼ばれる処理
};
/* 本デバイスドライバを、I2Cバスを使用するデバイスドライバとして登録する */
module_i2c_driver(mydevice_driver);
コードの先頭で、struct mydevice_device_info
という独自の構造体を定義しています。ここには、struct i2c_client
、つまり、各I2Cデバイス(クライアント)に紐づく情報を格納します。(ここでいう「紐づく」とは、struct i2c_client
が与えられたら、格納した情報(struct mydevice_device_info
)をgetできるという意味です。)
今回、I2Cデバイスが接続されるたび、つまり、probeのタイミングで新しいデバイスファイル(cdev)を作ることにします。これは、今までロード時にやっていたのと全く同じ処理です。デバイスファイル作成処理はmydevice_i2c_create_cdev()
関数に抽出しています。今までは、struct cdev cdev;
、unsigned int mydevice_major;
、struct class *mydevice_class;
は、staticで保持していました。今回は、struct mydevice_device_info
構造体内に入れます。また、この構造体の領域はprobeのタイミングでdevm_kzalloc
で動的に確保します。devm_kzalloc
を使うことで、デバイスが消えたタイミングでこのメモリも自動的に開放してくれます。また、同じstruct mydevice_device_info
構造体の中に、I2C制御に必要なstruct i2c_client
も保存しておきます。そして、このstruct mydevice_device_info
構造体をi2c_set_clientdata()
を使用して、このI2Cクライアントに関連付けられた領域に保存します(どこだろう?? カーネルが良きにやってくれるんだと思う)。これらの処理は、全て機器が接続されたタイミング(mydevice_i2c_probe()
)で行います。
mydevice_i2c_probe()
内で、キャラクタ型デバイスファイル(cdev)を作ったので、open/close/read/writeが使えます。まず、mydevice_open()
を見ます。この関数の第1引数struct inode *inode
内に、cdevへのポインタが格納されています。inode->i_cdev
で参照できます。先ほど、probeのタイミングでデバイスファイルを作るときに、cdevは独自の構造体struct mydevice_device_info
内に保存しました。そのため、container_of
を使用して、このcdevが格納されている構造体の先頭アドレスを求めることが出来ます。dev_info = container_of(inode->i_cdev, struct mydevice_device_info, cdev);
。そして、この中にstruct i2c_client
の情報も入れていたので、これを使ってI2C制御が可能となります。実際には、read/writeの時に使うので、さらにfile
構造体内のprivate_data
に入れて、read/writeのタイミングで参照できるようにしています。
動かしてみる
以下のように、ビルド、ロードして、I2C機器を認識させます。すると、/dev/mydevice0
が作成されています。そして、このデバイスをcatで読むと、mydevice_read()
関数が呼ばれて、その中でI2C通信して、ID値を返していることが分かります。
make && sudo insmod MyDeviceModule.ko
sudo bash -c 'echo MyI2CDevice 0x18 > /sys/bus/i2c/devices/i2c-1/new_device'
ls /dev/mydevice0
/dev/mydevice0
cat /dev/mydevice0
id = 0x33
おわりに
ちょっと長めの記事になってしまいましたが、最初に挙げたシンプルなコードが基本形ですので、まずはそれを抑えればOKだと思います。他のバスを使用したデバイスドライバ(SPIやUSB、PCI等)も、基本は同じ考えで行けると思います。(detect処理やsuspend時の処理が追加で必要になると思います)
また、記事の中で、「I2Cで接続されたデバイスのデバイスドライバ」とか「I2Cデバイスのデバイスドライバ」とか「I2C機器のデバイスドライバ」等、冗長な表現がありました。これは、「I2Cそのもののデバイスドライバ」と区別したかったためです。どこかで言葉を定義すればよかったのですが、忘れてしまいました。