Edited at

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

More than 1 year has passed since last update.


9回目: 他のカーネルモジュールの関数を呼ぶ / GPIO制御関数を使う


本連載について

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


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

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を作っておきます。


myDeviceDriverA.c

#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では、コンパイルしてオブジェクトファイルを作るだけで、リンクは行わないためです。


myDeviceDriverB.c

#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()で登録しています。


myDeviceDriver.c

#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を使って設定するらしい。