9回目: 他のカーネルモジュールの関数を呼ぶ / GPIO制御関数を使う
本連載について
組み込み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/09_01
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/09_02
今回の内容
本連載の4回目で、ラズパイ用のGPIOデバイスドライバを実装してみました。その時は、レジスタを直接叩くことで、制御を行いました。レジスタ番地や設定値はBCM2835のデータシートを見ながら設定しました。これらは「チップ依存」の情報です。センサーやモーターといった外付けのデバイスを制御するデバイスドライバを作る時に、いちいちチップのデータシートなんて見たくありません。GPIO制御用の関数があるので、それを使ってみます。
これと関連して、まずは他のカーネルモジュールで定義された関数を呼んでみる、ということをやってみようと思います。
他のカーネルモジュールで定義された関数を呼ぶ
以前、4回目: read/writeの実装とメモリのお話の所で述べたように、カーネルは全体で1つのメモリ空間を共有します。これにはカーネルモジュールも含まれます。そのため、自分が実装しているカーネルモジュールから、他のカーネルモジュールの関数を呼んだり、カーネルそのものに静的に組み込まれている関数を呼ぶことが出来ます。
関数を提供するカーネルモジュールAを作る
他のモジュールからも呼べるような関数を作るには、関数を定義した後に、EXPORT_SYMBOL
でエクスポートしてあげるだけです。EXPORT_SYMBOL
によって、その関数がカーネルのシンボルテーブルに登録されて、他のカーネルモジュールから呼べるようになります。
ロード(insmod)とアンロード(rmmod)用エントリ関数と、関数(mydevicea_func()
)だけを定義したモジュールを作ります。これを、MyDeviceDriverAとします。本来は関数宣言をヘッダに記載すべきですが、面倒なので省略します。
make
して、MyDeviceDriverA.koを作っておきます。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDeviceA" /* /proc/devices等で表示されるデバイス名 */
void mydevicea_func(void)
{
printk("This is a message in mydevicea_func\n");
}
/* カーネルのシンボルテーブルに登録する。他のカーネルモジュールから呼べるようにする */
EXPORT_SYMBOL(mydevicea_func);
/* ロード(insmod)時に呼ばれる関数 */
static int mydevicea_init(void)
{
printk("[A]: mydevicea_init\n");
mydevicea_func();
return 0;
}
/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevicea_exit(void)
{
printk("[A]: mydevicea_exit\n");
}
module_init(mydevicea_init);
module_exit(mydevicea_exit);
提供された関数を呼ぶカーネルモジュールBを作る
先ほど用意した関数を呼ぶカーネルモジュールBを作ります。通常のC言語と同じように呼べます。今回は関数宣言ヘッダを省略したので、externで呼び側で宣言しています。あまりお行儀はよくないです。このモジュールBをロード(insmod)するタイミングで、先ほどの関数(mydevicea_func()
)を呼んでみます。
make
して、MyDeviceDriverB.koを作っておきます。mydevicea_func()
の実態はここにはないのですが、カーネルモジュールを作る時には問題ありません。カーネルモジュール用のmakeでは、コンパイルしてオブジェクトファイルを作るだけで、リンクは行わないためです。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDeviceB" /* /proc/devices等で表示されるデバイス名 */
/* ロード(insmod)時に呼ばれる関数 */
static int mydeviceb_init(void)
{
printk("[B]: mydeviceb_init\n");
extern void mydevicea_func(void);
mydevicea_func();
return 0;
}
/* アンロード(rmmod)時に呼ばれる関数 */
static void mydeviceb_exit(void)
{
printk("[B]: mydeviceb_exit\n");
}
module_init(mydeviceb_init);
module_exit(mydeviceb_exit);
実行してみる
まず、MyDeviceDriverA.koをロードします。
sudo insmod MyDeviceModuleA.ko
dmesg
[16909.979207] [A]: mydevicea_init
[16909.979223] This is a message in mydevicea_func
ロード後、dmesgでログを見ると、モジュールAのinit処理と、init内で関数を呼んだことが分かります。これは、特に問題ないと思います。
続いて、MyDeviceDriverB.koをロードします。
sudo insmod MyDeviceModuleB.ko
dmesg
[17087.119434] [B]: mydeviceb_init
[17087.119449] This is a message in mydevicea_func
すると、このようにモジュールBからもモジュールA内で定義した関数を呼べていることが分かります。
依存関係
モジュールBがモジュールAを使用しているという依存関係になります。そのため、モジュールBをロードする前にモジュールAをロードしておく必要があります。でないとモジュールBのロード時にエラーが発生します。同様に、モジュールAをアンロードする前にモジュールBをアンロードしておく必要があります。先にモジュールAをアンロードしようとするとエラーが発生します。
必要な内容をちゃんと実装して、作成したカーネルモジュール(.ko)を適切な場所に配置すれば、insmod
の代わりにmodprobe
を使うことで自動的に依存関係のあるモジュールもロードしてくれるらしいです。
GPIO制御関数を使用するカーネルモジュールを作る
チップ依存のGPIO制御関数
冒頭で述べたように、外部デバイスやオンボードデバイスのデバイスドライバを作る限りは、チップのデータシートを見てレジスタ設定をするといったことは行わないと思います。自分がSoCメーカーのエンジニアだったり、機能拡張したい場合、有志でチップ依存デバイスの開発をする場合は必要になると思いますが。(なので、完全に無関係ではいられません。バグもあるだろうし。)
デバイスドライバからGPIOを制御するときには、そういった方々が作成してくれた関数を呼びます。この時、みんなバラバラなフォーマットで実装しているのではなく、linux/gpio.h
にあるようなインターフェースになるように実装してくれています。そのため、ユーザ(といってもデバドラ開発者)は、linux/gpio.h
にある関数を使ってGPIO制御ができます。ここにある関数を使用していれば、別のチップ上でも同じコードを使用することが出来ます。(ドキュメントを見ると、「GPIOの機能は多岐にわたるから、出来る限りlinux/gpio.h
に沿ってね。」みたいなことが書いてありました。そのため、使用するチップによっては異なる可能性もあります。)
どのチップ用のGPIO制御処理を使うかは、カーネルビルド時の設定で決まります。ラズパイの場合には、bcm2835用の処理を使うようになっているはずです。BCM2835用のGPIO処理はpinctrl-bcm2835.c
にありました。深く追えていませんが、linux/gpio.h
にある関数を呼ぶと、最終的にpinctrl-bcm2835.c
の各処理にたどり着くのだと思います。(GPIOの基本的な制御のほかに、機能ピンとしてのMUX制御などもあるため、かなり複雑なことになっているようです。)
とにかく、linux/gpio.h
にある関数を使ってGPIO制御を行えばいい
GPIO制御関数
基本的なGPIO制御をするのに必要な関数です。
-
int gpio_direction_output(unsigned gpio, int value)
- GPIOを出力に設定する。gpioはピン番号。valueは初期出力値(0 = low, 1 = high)
-
int gpio_direction_input(unsigned gpio)
- GPIOを入力に設定する。gpioはピン番号
-
void gpio_set_value(unsigned gpio, int value)
- GPIOに出力する。gpioはピン番号。valueは出力値(0 = low, 1 = high)
-
int gpio_to_irq(unsigned gpio)
- GPIOの指定したピンの割り込み番号を取得する
-
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
- 割り込みハンドラの登録をする。これはGPIOに限った関数ではありません
GPIO制御関数を使用するカーネルモジュールを作る
read/writeとかを実装するのが面倒なので、以下のようなシンプルな仕様のデバイスドライバ(カーネルモジュール)を作ります。前提として、ラズパイのGPIO4にLEDを、GPIO17にボタンを接続しているとします。LEDは抵抗経由で3.3Vに接続。ボタンはGNDに接続し、GPIO17側はプルアップします。面倒なら、GPIO4の出力はテスタで見たり、GPIO17の入力は3.3V/GNDに直結でもOKです。
作成するカーネルモジュールの仕様
- モジュールロード(insmod)時に、
- GPIO4(LED)を出力に設定し、Lowを出力する
- GPIO17(ボタン)を入力に設定し、割り込みハンドラを登録する
- モジュールアンロード(rmmod)時に、
- 登録した割り込みハンドラを取り除く
- 割り込みハンドラ内で、
- GPIO17(ボタン)の入力値をprintkする
コードは以下のようになります。割り込みハンドラはmydevice_gpio_intr()
になります。これを、ロード時にrequest_irq()
で登録しています。
#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/gpio.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice" /* /proc/devices等で表示されるデバイス名 */
#define GPIO_PIN_LED 4
#define GPIO_PIN_BTN 17
static irqreturn_t mydevice_gpio_intr(int irq, void *dev_id)
{
printk("mydevice_gpio_intr\n");
int btn;
btn = gpio_get_value(GPIO_PIN_BTN);
printk("button = %d\n", btn);
return IRQ_HANDLED;
}
/* ロード(insmod)時に呼ばれる関数 */
static int mydevice_init(void)
{
printk("mydevice_init\n");
/* LED用のGPIO4を出力にする。初期値は1(High) */
gpio_direction_output(GPIO_PIN_LED, 1);
/* LED用のGPIO4に0(Low)を出力にする */
gpio_set_value(GPIO_PIN_LED, 0);
/* ボタン用のGPIO17を入力にする */
gpio_direction_input(GPIO_PIN_BTN);
/* ボタン用のGPIO17の割り込み番号を取得する */
int irq = gpio_to_irq(GPIO_PIN_BTN);
printk("gpio_to_irq = %d\n", irq);
/* ボタン用のGPIO17の割り込みハンドラを登録する */
if (request_irq(irq, (void*)mydevice_gpio_intr, IRQF_SHARED | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "mydevice_gpio_intr", (void*)mydevice_gpio_intr) < 0) {
printk(KERN_ERR "request_irq\n");
return -1;
}
return 0;
}
/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
printk("mydevice_exit\n");
int irq = gpio_to_irq(GPIO_PIN_BTN);
free_irq(irq, (void*)mydevice_gpio_intr);
}
module_init(mydevice_init);
module_exit(mydevice_exit);
実行してみる
以下の通り、ビルドしてロードします。
make
sudo insmod MyDeviceModule.ko
すると、LEDが点灯するはずです。 その後、何回かボタンを押すか、GPIO17を3.3V/GNDにつないだりしてみます。
dmesg
[19652.388837] mydevice_init
[19652.388873] gpio_to_irq = 183
[19654.100437] mydevice_gpio_intr
[19654.100457] button = 0
[19656.061705] mydevice_gpio_intr
[19656.061727] button = 1
dmesgでログを見ると、登録した割り込みハンドラが呼ばれて、その中でGPIO入力値がprintされていることが分かります。ちなみに、割り込み状況は/proc/interrupts
で確認できます。
cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
183: 7 0 0 0 pinctrl-bcm2835 17 Edge mydevice_gpio_intr
プルアップ/プルダウン設定は?
コードから設定する一般的な方法はないらしい(https://raspberrypi.stackexchange.com/questions/44924/how-to-set-pull-up-down-resistors-in-a-kernel-module )。デバイスツリーで設定するらしい。brcm,bcm2835-gpio.txtを見ると、dtsファイルで、brcm,pull
を使って設定するらしい。