Raspberry Pi 5 Ubuntu 24.04でGPIO割り込みLEDドライバーを作成する

Raspberry Pi

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 0LEDを消灯する
write 1LEDを点灯する
write toggleLED状態を反転する

割り込みは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 では 01toggle を受け付けます。

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デバイスドライバー開発の基本的な要素を一通り確認できました。

コメント

タイトルとURLをコピーしました