Raspberry Pi 5上のUbuntu 24.04で、GPIO17の割り込み入力を受けてGPIO18に接続したLEDを反転させるLinuxカーネルモジュールを作成しました。
このドライバーでは、LinuxカーネルのGPIO descriptor APIとDevice Tree Overlayを使ってGPIOを扱います。ユーザー空間からは /dev/gpio_led というデバイスファイルを通して、LEDの状態確認と操作ができます。
作成したもの
今回作成した主なファイルは次のとおりです
driver/gpio_led.c
driver/Makefile
overlay/gpio-led-overlay.dts
動作概要
GPIO18をLED出力、GPIO17を割り込み入力として使用します。
GPIO17はプルアップ入力に設定し、スイッチを押すとGNDへ落ちる配線を想定しています。GPIO17が立ち下がると割り込みが発生し、そのタイミングでGPIO18の出力を反転します。
スイッチ入力ではチャタリングが発生するため、ドライバー側で50msのソフトウェアデバウンスを入れています。また、割り込み発生後にGPIO17がLowであることを確認してからLEDを反転するようにしています。

ドライバーの仕様
カーネルモジュールはplatform driverとして実装しました。GPIO番号をドライバー内に直接固定せず、Device Tree OverlayからGPIO descriptorとして取得します。ユーザー空間とのインターフェースにはmisc deviceを使い、ロード時に次のデバイスファイルを作成します。
/dev/gpio_led
| 操作 | 内容 |
| read | 現在のLED状態を 0\n または 1\n で返す |
| write 0 | LEDを消灯する |
| write 1 | LEDを点灯する |
| write toggle | LED状態を反転する |
割り込みはGPIO17の立ち下がりのみを対象にし、IRQF_TRIGGER_FALLING を指定しています。
Device Tree Overlay
GPIO17とGPIO18はDevice Tree Overlayで定義しました。overlay/gpio-led-overlay.dts では、GPIO17を入力、GPIO18を出力として設定します。GPIO17にはプルアップを指定し、ドライバーが参照する led-gpios と button-gpios を定義しています。
gpio-led-overlay.dts
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2712";
fragment@0 {
target = <&gpio>;
__overlay__ {
gpio_led_pins: gpio_led_pins {
brcm,pins = <17 18>;
brcm,function = <0 1>;
brcm,pull = <2 0>;
};
};
};
fragment@1 {
target-path = "/";
__overlay__ {
gpio_led: gpio_led {
compatible = "custom,gpio-led";
pinctrl-names = "default";
pinctrl-0 = <&gpio_led_pins>;
led-gpios = <&gpio 18 0>;
button-gpios = <&gpio 17 0>;
status = "okay";
};
};
};
};
このOverlayにより、カーネルが compatible = "custom,gpio-led" のplatform deviceを作成し、ドライバーのprobe処理が呼び出されます。
ビルド環境
Raspberry Pi 5
Ubuntu 24.04
Linux kernel headers
build-essential
device-tree-compiler
必要なパッケージはRaspberry Pi 5上で次のようにインストールします。
$ sudo apt update
$ sudo apt install -y build-essential linux-headers-raspi device-tree-compiler
外部カーネルモジュールとしてビルドするため、driver/Makefile では現在起動しているカーネルのビルドディレクトリを参照します。
Makefile
obj-m += gpio_led.o
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
WindowsからRaspberry P
gpio_led.cのコード抜粋
ドライバー本体の driver/gpio_led.c では、GPIO descriptor、IRQ番号、LED状態、misc deviceをまとめて管理する構造体を用意しています。
struct gpio_led_data {
struct device *dev; /* Device used for logging and devm helpers. */
struct gpio_desc *led_gpio; /* GPIO18 output descriptor. */
struct gpio_desc *button_gpio; /* GPIO17 input descriptor. */
int irq; /* Linux IRQ number mapped from GPIO17. */
atomic_t led_state; /* Cached LED state: 0 = off, 1 = on. */
unsigned long last_irq_jiffies; /* Last accepted switch interrupt time. */
struct miscdevice miscdev; /* Creates /dev/gpio_led. */
};
LED出力の変更は、キャッシュしている状態と実際のGPIO出力の両方に反映します。
static void gpio_led_set_state(struct gpio_led_data *data, int state)
{
int normalized = !!state;
atomic_set(&data->led_state, normalized);
gpiod_set_value_cansleep(data->led_gpio, normalized);
}
static int gpio_led_toggle_state(struct gpio_led_data *data)
{
int old_state;
int new_state;
do {
old_state = atomic_read(&data->led_state);
new_state = !old_state;
} while (atomic_cmpxchg(&data->led_state, old_state, new_state) != old_state);
gpiod_set_value_cansleep(data->led_gpio, new_state);
return new_state;
}
GPIO17の割り込み処理では、50ms以内の連続した入力を無視し、入力がLowであることを確認してからGPIO18を反転します。
static irqreturn_t gpio_led_irq_thread(int irq, void *dev_id)
{
struct gpio_led_data *data = dev_id;
unsigned long now = jiffies;
int state;
if (time_before(now, data->last_irq_jiffies + msecs_to_jiffies(GPIO_LED_DEBOUNCE_MS)))
return IRQ_HANDLED;
data->last_irq_jiffies = now;
if (gpiod_get_value_cansleep(data->button_gpio) != 0)
return IRQ_HANDLED;
state = gpio_led_toggle_state(data);
dev_dbg(data->dev, "GPIO17 interrupt toggled GPIO18 to %d\n", state);
return IRQ_HANDLED;
}
read では現在のLED状態を 0\n または 1\n として返します。
static ssize_t gpio_led_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
char state_text[3];
int len;
int state;
if (!gpio_led_device)
return -ENODEV;
state = atomic_read(&gpio_led_device->led_state);
len = scnprintf(state_text, sizeof(state_text), "%d\n", state);
return simple_read_from_buffer(buf, count, ppos, state_text, len);
}
write では 0、1、toggle を受け付けます。
static ssize_t gpio_led_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
char command[GPIO_LED_CMD_MAX];
size_t len;
if (!gpio_led_device)
return -ENODEV;
len = min(count, (size_t)GPIO_LED_CMD_MAX - 1);
if (copy_from_user(command, buf, len))
return -EFAULT;
command[len] = '\0';
strim(command);
if (sysfs_streq(command, "0")) {
gpio_led_set_state(gpio_led_device, 0);
} else if (sysfs_streq(command, "1")) {
gpio_led_set_state(gpio_led_device, 1);
} else if (sysfs_streq(command, "toggle")) {
gpio_led_toggle_state(gpio_led_device);
} else {
dev_warn(gpio_led_device->dev, "invalid command: %s\n", command);
return -EINVAL;
}
return count;
}
platform driverのprobe処理では、Device Tree OverlayからGPIOを取得し、GPIO17をIRQへ変換し、最後に /dev/gpio_led を作成します。
static int gpio_led_probe(struct platform_device *pdev)
{
struct gpio_led_data *data;
int ret;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio))
return dev_err_probe(&pdev->dev, PTR_ERR(data->led_gpio),
"failed to get led-gpios\n");
data->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(data->button_gpio))
return dev_err_probe(&pdev->dev, PTR_ERR(data->button_gpio),
"failed to get button-gpios\n");
data->irq = gpiod_to_irq(data->button_gpio);
if (data->irq < 0)
return dev_err_probe(&pdev->dev, data->irq,
"failed to map GPIO17 to IRQ\n");
ret = devm_request_threaded_irq(&pdev->dev, data->irq,
NULL,
gpio_led_irq_thread,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
dev_name(&pdev->dev),
data);
if (ret)
return dev_err_probe(&pdev->dev, ret,
"failed to request GPIO17 IRQ\n");
data->miscdev.minor = MISC_DYNAMIC_MINOR;
data->miscdev.name = "gpio_led";
data->miscdev.fops = &gpio_led_fops;
data->miscdev.parent = &pdev->dev;
data->miscdev.mode = 0666;
ret = misc_register(&data->miscdev);
if (ret)
return dev_err_probe(&pdev->dev, ret,
"failed to register /dev/gpio_led\n");
gpio_led_device = data;
return 0;
}
Overlayの配置とモジュールのロード
Device Tree Overlayは dtc で .dtbo に変換し、Raspberry Pi 5のOverlayディレクトリへ配置します。
dtc -@ -I dts -O dtb -o overlay/gpio-led.dtbo overlay/gpio-led-overlay.dts
sudo cp overlay/gpio-led.dtbo /boot/firmware/overlays/gpio-led.dtbo
/boot/firmware/config.txt には次の行を追加します。
dtoverlay=gpio-led
Device Tree Overlayを初めて有効にした後は、Raspberry Pi 5を再起動します。再起動後、platform deviceが作成され、ドライバーをロードできるようになります。
カーネルモジュールは次のようにロードします。
sudo insmod driver/gpio_led.ko
ロードされると、/dev/gpio_led が作成されます。
手動での動作確認
デバイスファイルが作成されていることを確認します。
$ ls -l /dev/gpio_led
crw-rw-rw- 1 root root 10, 260 5月 20 16:42 /dev/gpio_led
LEDの現在状態は cat で確認できます。
$ cat /dev/gpio_led
LEDを点灯します。
$ echo 1 | sudo tee /dev/gpio_led
LEDを消灯します。
echo 0 | sudo tee /dev/gpio_led
LEDを反転します。
echo toggle | sudo tee /dev/gpio_led
GPIO17に接続したスイッチを押すと割り込みが発生し、GPIO18のLED出力が反転します。
ロード時には dmesg で次のようなログを確認できます。
gpio_led gpio_led: GPIO LED driver loaded: GPIO17 IRQ toggles GPIO18
テストコード
/dev/gpio_led の read と write を確認するために、ユーザー空間で動く簡単なC言語のテストプログラムを用意できます。
このテストコードでは、LEDを消灯、点灯、反転の順に操作し、その都度 /dev/gpio_led から現在状態を読み出します。
// test_gpio_led.c - /dev/gpio_led の read/write 動作を確認するテストコード。
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
// テスト対象のデバイスファイル。
#define GPIO_LED_DEV "/dev/gpio_led"
// /dev/gpio_led へコマンドを書き込む。
static int write_command(const char *command)
{
int fd;
ssize_t written;
fd = open(GPIO_LED_DEV, O_WRONLY);
if (fd < 0) {
perror("open for write");
return -1;
}
written = write(fd, command, strlen(command));
if (written < 0) {
perror("write");
close(fd);
return -1;
}
close(fd);
return 0;
}
// /dev/gpio_led から現在のLED状態を読み出して表示する。
static int read_state(void)
{
char buffer[16];
int fd;
ssize_t read_len;
fd = open(GPIO_LED_DEV, O_RDONLY);
if (fd < 0) {
perror("open for read");
return -1;
}
memset(buffer, 0, sizeof(buffer));
read_len = read(fd, buffer, sizeof(buffer) - 1);
if (read_len < 0) {
perror("read");
close(fd);
return -1;
}
printf("LED state: %s", buffer);
close(fd);
return 0;
}
int main(void)
{
// LEDを消灯して状態を確認する。
if (write_command("0\n") < 0 || read_state() < 0)
return 1;
sleep(1);
// LEDを点灯して状態を確認する。
if (write_command("1\n") < 0 || read_state() < 0)
return 1;
sleep(1);
// LEDを反転して状態を確認する。
if (write_command("toggle\n") < 0 || read_state() < 0)
return 1;
return 0;
}
Raspberry Pi 5上で次のように保存してビルドします。
nano test_gpio_led.c
gcc -Wall -Wextra -o test_gpio_led test_gpio_led.c
実行します。
./test_gpio_led
実行結果の例です。
実行結果の例です。
GPIO17のスイッチ入力による割り込み動作は、テストプログラムとは別に、実際にスイッチを押してGPIO18のLEDが反転することを確認します。
まとめ
Raspberry Pi 5 Ubuntu 24.04上で、GPIO17の割り込み入力を使ってGPIO18のLEDを制御するカーネルドライバーを作成しました。
Device Tree Overlayを使うことで、GPIOの指定をドライバー本体から分離できました。また、misc deviceとして /dev/gpio_led を作成することで、ユーザー空間から read と write による簡単な操作ができるようになりました。
手動コマンドとC言語のテストコードを使うことで、/dev/gpio_led の読み書きとLED制御を確認できます。今回の構成では、GPIO descriptor API、platform driver、Device Tree Overlay、外部カーネルモジュールのビルドという、Linuxデバイスドライバー開発の基本的な要素を一通り確認できました。


コメント