Raspberry Pi に接続したLCD1602キャラクタデバイスのデバイスドライバを自作するために、
まずは、ローダブルカーネルモジュールを作ろうと思います。
最終的には、カーネルに組み込んでRaspberry Pi の電源ON時から、
LCD1602キャラクタデバイスを制御しようと思っていますが、
ここでは、ローダブルカーネルモジュールの作り方とアクセス確認のやり方について書いていきます。
使用する環境は、Ubuntu 20.04 です。
1.ローダブルカーネルモジュールとは?
デバイスドライバには、
・カーネルに組み込まず動的にロード/アンロードできるデバイスドライバ
・カーネルビルド時に一緒にカーネルに組み込む静的なデバイスドライバ
の2種類があり、
ローダブルカーネルモジュールは、前者のデバイスドライバを指します。
ローダブルカーネルモジュールは、
コマンドラインからロード/アンロードができるデバイスドライバなので、
カーネルを再ビルドして組み込む必要がありません。
なので、デバイスドライバを修正しても簡単に動作確認することができます。
2.ローダブルカーネルモジュール作成に必要な知識
デバイスドライバのソースコードを作成するためには、まず以下のことを理解しておく必要があります。
・file_operations構造体について
・module_init(), module_exit()について
・ユーザー空間からアクセスするためのデバイスファイル作成について
・includeするカーネルヘッダファイルについて
file_operations構造体について
file_operations構造体とは、デバイスドライバのインターフェースを設定するための構造体で、
このfile_operations構造体のメンバに設定した関数を使用することで、
ユーザー空間プログラムからデバイスドライバにアクセスできるようになります。
file_operations構造体のメンバは、関数ポインタになっていて、
デバイスドライバのインタフェースを作成する際に、その関数の戻り値と引数を合わせる必要があります。
file_operations構造体は、カーネルソースツリーのinclude/linux/fs.hで定義されており、
file_operations構造体のメンバの一部を定義順に一覧表にしてみました。
No. | 戻り値 | 関数名 | 引数 |
---|---|---|---|
1 | ssize_t | (*read) | (struct file *, char __user *, size_t, loff_t *) |
2 | ssize_t | (*write) | (struct file *, char __user *, size_t, loff_t *) |
3 | int | (*open) | (struct inode *, struct file *) |
4 | int | (*release) | (struct inode *, struct file *) |
5 | long | (*unlocked_ioctl) | (struct file *, unsigned int, unsigned long) |
6 | long | (*compat_ioctl) | (struct file *, unsigned int, unsigned long) |
No1,2のread/writeは、デバイスに対してデータを送受信する際に使用、
No3、4のopen/releaseは、ドライバを使用するためのハンドラを取得/解放、
No5、6のioctlは、read/write以外の自分の好きな操作を設定できます。
read/writeは、デバイスによって必要無い場合がありますが、open/releaseは必ず必要です。
また、ドライバの内部状態の取得などread/writeと異なる用途の場合には、ioctlで対応することができます。
module_init(), module_exit()について
module_init()/module_exit()は、ローダブルカーネルモジュールをロード/アンロードする時に、
コールされる関数をカーネルにセットする関数です。
例えば、module_init( testdrv_init )とすると、ロード時にtestdrv_init()関数が実行されます。
また、module_exit( testdrv_exit )とすると、アンロード時にtestdrv_exit()関数が実行されます。
ユーザー空間からアクセスするためのデバイスファイル作成について
ユーザー空間プログラムからローダブルカーネルモジュールにアクセスする方法の1つとして、
デバイスファイルを使用する方法があります。
デバイスファイルとは、アクセスしたいデバイスに対応した特殊なファイルのことを指し、/dev配下に配置されています。
このデバイスファイルは、modules_init()の処理の中で作成します。
ここでは、デバイスファイルを作成するために使用する関数について一覧表にしてみました。
関数と引数 | 説明 | 実体があるファイル名 |
---|---|---|
alloc_chrdev_region() | デバイスファイルのメジャー番号を動的に取得する。 | fs/char_dev.c |
cdev_init() | cdev構造体の初期化を行う。 | fs/char_dev.c |
cdev_add() | キャラクタドライバとしてシステムに追加する。 | fs/char_dev.c |
class_create() | /sys/class配下にデバイス情報のファイルを作成する。 | drivers/base/class.c |
device_create() | /dev配下にデバイスファイルを作成する。 | drivers/base/core.c |
includeするカーネルヘッダファイルについて
様々なカーネルヘッダファイルをインクルードする必要があります。
例えば、file_operations構造体を使用するには、fs.hをインクルードする必要があります。
ここでは、次項のサンプルプログラムをコンパイルするために必要なカーネルヘッダファイルと
そのヘッダファイルに宣言されている関数や構造体について、表にしました。
ヘッダファイル名 | 構造体名・関数名 |
---|---|
linux/module.h | module_init()関数、module_exit()関数 |
linux/fs.h | file_operations構造体、alloc_chrdev_region()関数 |
linux/cdev.h | cdev構造体、cdev_init()関数、cdev_add()関数、cdev_del()関数 |
linux/device.h | class_create()関数、device_create()関数 |
linux/uaccess.h | copy_to_user()関数、copy_from_user()関数 |
3.ローダブルカーネルモジュールのソースコードサンプル
2項を踏まえて、ローダブルカーネルモジュールのサンプルプログラムは、下記のような感じです。
ファイル名:testdrv.c
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include "testdrv.h" MODULE_LICENSE("GPL"); static char *DEV_NAME = "testdrv"; //本ドライバのデバイスファイル名 static struct cdev cdev_testdrv; static struct class *class_testdrv; static dev_t dev; #define MINOR_BASE 0 // マイナー番号の基準番号 #define MINOR_NUM 1 // デバイスファイルの数 char cBuf[10] = "testdrv"; char cState[10] = "ON"; struct testdrv_values values; // open()関数 static int testdrv_open(struct inode* inode, struct file *file) { printk(KERN_INFO "Call testdrv_open\n"); return 0; } // close()関数 static int testdrv_close(struct inode* inode, struct file *file) { printk(KERN_INFO "Call testdrv_close\n"); return 0; }
// ioctl()関数 static long testdrv_ioctl(struct file* fp, unsigned int cmd, unsigned long arg) { printk(KERN_INFO "Call testdrv_ioctl\n"); switch ( cmd ) { case GET_STATE: printk(KERN_INFO "switch GET_STATE \n"); values.state = 100; values.a = 4; if ( copy_to_user( (void __user *)arg, &values, sizeof(values) ) !=0 ) { return -EFAULT; } break; case SET_STATE: printk(KERN_INFO "switch SET_STATE \n"); if ( copy_from_user( &values, (void __user *)arg, sizeof(values) ) !=0 ) { return -EFAULT; } printk(KERN_INFO "state=%d \n", values.state); printk(KERN_INFO "a=%d \n", values.a); break; default: printk(KERN_INFO "not command \n"); break; } return 0; } // read()関数 static ssize_t testdrv_read(struct file* fp, char __user *buf , size_t size, loff_t *loff) { printk(KERN_INFO "Call testdrv_read\n"); if ( copy_to_user( buf, cBuf, size ) !=0 ) { return -EFAULT; } return 0; }
// write()関数 static ssize_t testdrv_write(struct file* fp, const char __user *buf , size_t size, loff_t *loff) { printk(KERN_INFO "Call testdrv_write\n"); if ( copy_from_user( cBuf, buf, size ) !=0 ) { return -EFAULT; } printk(KERN_INFO "Write: %s\n", cBuf); return 0; } // file_operations構造体 static const struct file_operations testdrv_fops = { .open = testdrv_open, .release = testdrv_close, .unlocked_ioctl = testdrv_ioctl, .compat_ioctl = testdrv_ioctl, .read = testdrv_read, .write = testdrv_write, }; // init()関数 static int testdrv_init(void) { int minor = 0; int ret = 0; int cdev_err = 0; printk(KERN_INFO "Call testdrv_init \n"); ret = alloc_chrdev_region( &dev, MINOR_BASE, MINOR_NUM, DEV_NAME); if (ret != 0) { printk(KERN_ERR "alloc_chrdev_region ret = %d\n", ret); return -1; } cdev_init(&cdev_testdrv, &testdrv_fops); cdev_testdrv.owner = THIS_MODULE; cdev_err = cdev_add( &cdev_testdrv, dev, MINOR_NUM); if (cdev_err != 0) { printk(KERN_ERR "cdev_add = %d\n", cdev_err); unregister_chrdev_region(dev, MINOR_NUM); return -1; } // /sys/class 配下にtestdrvフォルダを作成 class_testdrv = class_create( THIS_MODULE, "testdrv" ); if( IS_ERR( class_testdrv ) ) { printk(KERN_ERR "class create"); cdev_del( &cdev_testdrv ); unregister_chrdev_region(dev, MINOR_NUM); } // /dev配下にデバイスファイルを作成 for ( minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++ ) { printk(KERN_INFO "device create\n"); device_create( class_testdrv, NULL, dev , NULL, "testdrv%d", minor); } printk(KERN_INFO "testdrv loaded\n"); return 0; } // exit()関数 static void testdrv_exit(void) { int minor = 0; printk(KERN_INFO "Call testdrv_exit\n"); for ( minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++ ) { device_destroy( class_testdrv, dev ); } class_destroy( class_testdrv ); cdev_del( &cdev_testdrv ); unregister_chrdev_region(dev, MINOR_NUM); printk(KERN_INFO "Call testdrv unloaded\n"); } // module_xxx()関数 module_init(testdrv_init); module_exit(testdrv_exit);
以下は、ioctlで使用する構造体を定義しているヘッダです。
ファイル名:testdrv.h
#ifndef TESTDRV_H_ #define TESTDRV_H_ #include <linux/ioctl.h> struct testdrv_values { int a; int state; }; #define TESTDRV_IOC_CMD 'TESTDRV' #define GET_STATE _IOW( TESTDRV_IOC_CMD, 1, struct testdrv_values) #define SET_STATE _IOW( TESTDRV_IOC_CMD, 2, struct testdrv_values) #endif //TESTDRV_H_
ここでは、全ての関数処理にprintk()文を入れて、
ユーザー空間プログラムからコールできていることを後述で確認したいと思います。
4.ローダブルカーネルモジュールのコンパイル
3項のローダブルカーネルモジュールのプログラムをコンパイルするために、まずMakefileを作成します。
Makefileの内容は、こんな感じです。
ファイル名:Makefile
## for x86 KERNELSRCDIR = /lib/modules/$(shell uname -r)/build ARCH=x86 CROSS_COMPILE= ## for ARM #KERNELSRCDIR = /lib/modules/5.4.74-v7l+/build #ARCH=arm #CROSS_COMPILE=arm-linux-gnueabihf- VERBOSE = 0 obj-m := testdrv.o all: make -C $(KERNELSRCDIR) \ M=$(PWD) \ KBUILD_VERBOSE=$(VERBOSE) \ ARCH=$(ARCH) \ CROSS_COMPILE=$(CROSS_COMPILE) \ modules clean: make -C $(KERNELSRCDIR) M=$(PWD) KBUILD_VERBOSE=$(VERBOSE) ARCH=$(ARCH) clean
Makfileを作成後、3項のサンプルプログラムをコンパイルしてみます。
$ make
結果は、こんな感じでローダブルカーネルモジュールである拡張子koファイルとその他が生成されます。
また、クロスコンパイル環境が整っていれば、Makefile内でコメントにしている、
ARM用の環境変数に切り替えることで、クロスコンパイルすることもできます。
5.ローダブルカーネルモジュールのロードとアンロード
コンパイルして生成されたローダブルカーネルモジュール ( .koファイル )をLinux上にロードするためには、
下記のように、insmodコマンドを使用します。
$ sudo insmod testdrv.ko
実行後、/dev配下にtestdrv0というデバイスファイルが作成されます。
また、ロードされているか確認するためには、下記のように、lsmodコマンドを使用します。
$ lsmod
そして、アンロードするためには、下記のように、rmmodコマンドを使用します。
$ sudo rmmod testdrv
6.ローダブルカーネルモジュールのアクセス確認
デバイスドライバにアクセスするユーザー空間のサンプルプログラムは、こんな感じです。
ファイル名:testdrv_app.c
#include <stdio.h>
#include <fcntl.h>
#include <errno.h> #include "testdrv.h" int main() { char cBuffer[32]; struct testdrv_values values; values.state=0; // OPEN int fd0 = open("/dev/testdrv0", O_RDWR); if( fd0 < 0) { printf("Open Error %d\n ", fd0 ); perror("open"); return -1; } // READ if( read( fd0, cBuffer, 10 ) != 0 ){ perror("read"); return -1; } printf("read = %s\n", cBuffer ); // WRITE if( write( fd0, cBuffer, 10 ) != 0 ){ perror("write"); return -1; } printf("written\n"); values.state=1345; values.a=2020; // IOCTL if( ioctl( fd0, GET_STATE, &values ) < 0 ) { perror("ioctl GET_STATE"); return -1; } printf("a = %d \n", values.a); printf("state = %d \n", values.state); values.state=1; values.a=5; if( ioctl( fd0, SET_STATE, &values ) < 0 ) { perror("ioctl GET_STATE"); return -1; }
// CLOSE
if( close(fd0) != 0 ) { printf("Close Error\n"); return -1; } }
このユーザー空間プログラムのコンパイルと実行は、下記のような感じです。
$ gcc testdrv_app.c $ sudo ./a.out
gccでコンパイルするとWarningが出ますが無視しても大丈夫です。
そして、a.outの実行結果は、下記のようになります。
ここで何故、a.outをルート権限実行しているかというと、デバイスファイルにアクセスするためです。
デバイスファイルを作成した時点では、パーミッションがルートのR/W可のみになっています。
これを手動などでパーミッションを変更すれば、a.out実行時にsudoをしなくても良くなります。
また、ローダブルカーネルモジュールのプログラム内で使用しているprintk()は、
printf()のように標準出力されません。
printk()の出力先は、/var/log/syslogファイルに出力されます。
なので、今までに実行したinsmod/rmmod、a.out実行時のログは、
/var/log/syslogファイルを見てみると下図の赤枠のように出力されています。
これで、ローダブルカーネルモジュールのベースの完成です。
あとは、各デバイスに対応した処理を付け足していくだけになります。
7.最後に
ローダブルカーネルモジュールのベースとなるプログラムができましたので、
次は、これをベースにLCD1602キャラクタデバイスのデバイスドライバを作成していきます。
また今回は、/sys/classのファイルについては触れませんでしたが、いつか書きたいと思います。
関連・おすすめ記事
Raspberry Pi 3B+ でI2CキャラクタLCD(1602) の動作確認 - 水瓶座列車
LCD1602(I2C)を制御する方法を解析調査した結果まとめ - 水瓶座列車
Raspberry Pi(ラズパイ)のOSをバックアップ・リストアする方法 - 水瓶座列車
Bluetoothでマルチペアリング(複数台接続)できるマウスのおすすめランキングベスト5 - 水瓶座列車
Bluetoothでマルチペアリング(複数台接続)できるキーボードのおすすめランキングベスト5 - 水瓶座列車
Raspberry Pi OSのカーネルソースビルド手順解説 - 水瓶座列車