カーネルに組み込まれたデバイスドライバ
M5Stack CoreMP135を用いて、カーネルに組み込まれたデバイスドライバにトライする。機能的には「その2」と同じで、ボタンおよびLEDの制御である。なお、「M5Stack CoreMP135にさわる(その2:DebianのBuild)」も参考。ChatGPTをかなり利用。
特徴
最初からカーネルに組み込む方法は、モジュールロード方式と比べると、作法が異なるらしい。
| モジュールロード | カーネル組み込み |
|---|---|
| module_init() | builtin_platform_driver() |
| init() | probe() |
| exit() | remove() |
| 手動ロード(insmod) | Device Tree(DT)による自動的な組み込み |
流れ的に下記となる。
- kernel起動
- DT読み込み
- builtin_platform_driverによる登録
- DTのcompatibleスキャンおよび該当デバイス発見
- probe()がCall
ソースコード
コード内に適宜コメント追記(「その2」のコードコメントも参照)。ほとんど、ChatGPT作成コード。ただし、1回目に提示されたコードでは、コンパイルできず。この辺りはなんとかならないものか、、、(ChatGPTに依頼すると、時々、この手の状況が発生する)。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/wait.h>
#include <linux/jiffies.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEBOUNCE_MS 50
/* ドライバプライベートデータ */
struct gpio_test_dev {
struct device *dev;
/* GPIO */ /* GPIO */
struct gpio_desc *button; /* ボタン */
struct gpio_desc *led; /* LED */
/* IRQ / debounce */
int irq; /* 割り込み */
struct work_struct work; /* 割り込み時に実行される関数にリンクする変数 */
unsigned long last_jiffies; /* Timer割り込み回数 */
/* event */
wait_queue_head_t wq; /* 割り込みQueue変数 */
int event; /* 処理イベント発生 */
int pressed; /* ボタン押下発生 */
/* misc devices */
struct miscdevice misc_button; /* カーネルへの登録(ボタン) */
struct miscdevice misc_led; /* カーネルへの登録(LED) */
};
/* ----- /dev/gpio_button ----- */
/* 割り込み時に実行される関数 */
static void gpio_test_work(struct work_struct *work)
{
struct gpio_test_dev *gt =
container_of(work, struct gpio_test_dev, work);
unsigned long now = jiffies; /* 起動からのTimer割り込み回数 */
int val;
if (time_before(now,
gt->last_jiffies + msecs_to_jiffies(DEBOUNCE_MS)))
goto out; /* チャタリング対策時間内であれば抜ける */
gt->last_jiffies = now; /* 現在のTimer割り込み回数の記録 */
/* 現在のGPIO値を取得 */
val = gpiod_get_value(gt->button);
/*
* val == 0 being pushed 押下(プルアップのため0)
* pressed == 0 not notified 未通知
*/
if (val == 0 && gt->pressed == 0) {
gt->pressed = 1; /* 押下あり */
gt->event = 1; /* イベント処理済み */
wake_up_interruptible(>->wq); /* Readルーチンwakeup */
dev_dbg(gt->dev, "gpio_test_work: button pressed; wake_up\n"); /* デバッグON時にカーネルログに記録(dmesg) */
}
/*
* val == 1 button released ボタンリリース
*/
if (val == 1)
gt->pressed = 0; /* 押下されていない */
out:
enable_irq(gt->irq); /* 割り込み許可 */
}
/* 割り込みハンドラ */
static irqreturn_t gpio_test_irq(int irq, void *data)
{
struct gpio_test_dev *gt = data;
disable_irq_nosync(irq); /* 新しい割り込み禁止 */
schedule_work(>->work); /* workにリンクしているgpio_test_work()を後ほど実行(ハンドラをすぐに抜けるため) */
return IRQ_HANDLED; /* 割り込み処理完了 */
}
/* ボタンデバイスopen */
static int gpio_button_open(struct inode *inode, struct file *file)
{
struct miscdevice *mdev = file->private_data;
struct gpio_test_dev *gt =
container_of(mdev, struct gpio_test_dev, misc_button);
file->private_data = gt;
dev_dbg(gt->dev, "gpio_button_open\n");
return 0;
}
/* ボタンデバイスread */
static ssize_t gpio_button_read(struct file *file,
char __user *buf,
size_t count,
loff_t *ppos)
{
struct gpio_test_dev *gt = file->private_data;
int ret;
const char msg[] = "Pressed\n"; /* ユーザーランドに返る文字列 */
if (*ppos)
return 0;
dev_dbg(gt->dev, "gpio_button_read: waiting event\n");
/* イベント待ち(ブロック) */
ret = wait_event_interruptible(gt->wq, gt->event);
dev_dbg(gt->dev, "gpio_button_read: event happend\n");
if (ret)
return ret;
gt->event = 0; /* イベント初期化 */
if (copy_to_user(buf, msg, sizeof(msg))) /* ユーザーランドへ文字列コピー */
return -EFAULT;
*ppos = sizeof(msg);
return sizeof(msg); /* 文字列長さをリターン */
}
/* ファイルオペレーション(ボタン用) */
static const struct file_operations gpio_button_fops = {
.owner = THIS_MODULE,
.open = gpio_button_open, /* openエントリ */
.read = gpio_button_read, /* readエントリ */
};
/* ----- /dev/gpio_led ----- */
/* LEDデバイスopen */
static int gpio_led_open(struct inode *inode, struct file *file)
{
struct miscdevice *mdev = file->private_data;
struct gpio_test_dev *gt =
container_of(mdev, struct gpio_test_dev, misc_led);
file->private_data = gt;
dev_dbg(gt->dev, "gpio_led_open\n");
return 0;
}
/* LEDデバイスwrite */
static ssize_t gpio_led_write(struct file *file,
const char __user *buf,
size_t count,
loff_t *ppos)
{
struct gpio_test_dev *gt = file->private_data;
char c;
if (copy_from_user(&c, buf, 1)) /* ユーザーランドからのwrite内容をコピー */
return -EFAULT;
dev_dbg(gt->dev, "gpio_led_write: %c\n", c);
if (c == '1') /* 1 */
gpiod_set_value(gt->led, 1); /* GPIO ON */
else if (c == '0') /* 0 */
gpiod_set_value(gt->led, 0); /* GPIO OFF */
return count;
}
/* ファイルオペレーション(LED用) */
static const struct file_operations gpio_led_fops = {
.owner = THIS_MODULE,
.open = gpio_led_open, /* openエントリ */
.write = gpio_led_write, /* writeエントリ */
};
/* ----- probe ----- */
static int gpio_test_probe(struct platform_device *pdev)
{
struct gpio_test_dev *gt;
int ret;
/* プライベートデータ用領域確保 */
gt = devm_kzalloc(&pdev->dev, sizeof(*gt), GFP_KERNEL);
if (!gt)
return -ENOMEM;
gt->dev = &pdev->dev;
/* 割り込み処理ルーチン登録 */
init_waitqueue_head(>->wq);
INIT_WORK(>->work, gpio_test_work);
/* ボタン関連初期処理 */
gt->button = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(gt->button))
return PTR_ERR(gt->button);
gt->pressed = 0;
/* LED関連初期処理 */
gt->led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(gt->led))
return PTR_ERR(gt->led);
/* 割り込み関連初期処理 */
gt->irq = gpiod_to_irq(gt->button);
if (gt->irq < 0)
return gt->irq;
ret = devm_request_irq(&pdev->dev,
gt->irq,
gpio_test_irq,
IRQF_TRIGGER_FALLING,
"gpio_button",
gt);
if (ret)
return ret;
/* ボタンデバイス登録 */
gt->misc_button.minor = MISC_DYNAMIC_MINOR;
gt->misc_button.name = "gpio_button";
gt->misc_button.fops = &gpio_button_fops;
ret = misc_register(>->misc_button);
if (ret)
return ret;
gt->misc_button.this_device->driver_data = gt;
/* LEDデバイス登録 */
gt->misc_led.minor = MISC_DYNAMIC_MINOR;
gt->misc_led.name = "gpio_led";
gt->misc_led.fops = &gpio_led_fops;
ret = misc_register(>->misc_led);
if (ret) {
misc_deregister(>->misc_button);
return ret;
}
gt->misc_led.this_device->driver_data = gt;
/* ドライバ登録 */
platform_set_drvdata(pdev, gt);
dev_info(&pdev->dev, "gpio_button / gpio_led ready\n"); /* カーネルログ(dmesg) */
return 0;
}
/* ----- remove ----- */
static int gpio_test_remove(struct platform_device *pdev)
{
struct gpio_test_dev *gt = platform_get_drvdata(pdev);
/* 解放処理 */
misc_deregister(>->misc_led);
misc_deregister(>->misc_button);
cancel_work_sync(>->work);
dev_info(&pdev->dev, "gpio_button / gpio_led end\n");
return 0;
}
/* ----- Device Tree (DT) 用 ----- */
static const struct of_device_id gpio_test_of_match[] = {
{ .compatible = "m5stack,gpio-test" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_test_of_match);
static struct platform_driver gpio_test_driver = {
.probe = gpio_test_probe,
.remove = gpio_test_remove,
.driver = {
.name = "gpio_test",
.of_match_table = gpio_test_of_match,
},
};
/* ドライバ登録 */
builtin_platform_driver(gpio_test_driver);
MODULE_LICENSE("GPL");
なお、下記コマンドにより、dev_dbg()のメッセージ(デバッグログ)がカーネルログに記録される。
$ echo 'file gpio_test.c +p' > /sys/kernel/debug/dynamic_debug/control
オフにするには、下記コマンドを実行。"+"でON、"-"でOFFという意味。
$ echo 'file gpio_test.c -p' > /sys/kernel/debug/dynamic_debug/control
また、上記コードでは、readするしないかかわらず、割り込み(ボタン押下)を必ず検出するアルゴリズムとなっているので、readせずにボタン押下しても割り込みが発生し、その後readするとreadが即リターンする。
Device Tree(DT)
詳細については、下記を参照。
ハードウェア(ペリフェラル)を制御する上での情報(アドレスなど)を格納したものである。一般的に、”arch/arm/boot/dts”に、DT情報が存在する。後述する方法で、カーネルBuild環境を構築すると、下記ディレクトリに、dts(device tree source)、dtsi(dts incluce)、dtb(device tree binary)が配置される。
CoreMP135_buildroot-external-st/tools/build_coremp135_buidlroot/CoreMP135_buildroot/output/build/linux-custom/arch/arm/boot/dts
$ ls stm32mp135f-coremp135.dt*
stm32mp135f-coremp135.dtb stm32mp135f-coremp135.dts
対象となるDTはstm32mp135f-coremp135.dtsであり、下記は、追加および修正内容である。
まず、”/ { ... };”(ルートノード)内の最後に下記記載を行う。
gpio_test: gpio-test {
compatible = "m5stack,gpio-test";
button-gpios = <&gpioc 6 GPIO_ACTIVE_LOW>;
led-gpios = <&gpioc 7 GPIO_ACTIVE_HIGH>;
};
その1でも記載したとおり、PC6およびPC7を利用しているUARTを無効化する(下記)。
&usart6 {
status = "disabled";
};
PC6およびPC7をGPIOとして利用するための定義。下記を最後に追加。
&pinctrl {
pc6_pc7_gpio: pc6-pc7-gpio {
pins {
/* PC6: Button input */
pinmux = <STM32_PINMUX('C', 6, GPIO)>;
bias-pull-up;
};
pins2 {
/* PC7: LED output */
pinmux = <STM32_PINMUX('C', 7, GPIO)>;
drive-push-pull;
bias-disable;
};
};
};
なお、ボタンが接続されるPC6には、プルアップ(bias-pull-up)の設定を行う。ファイルの最後に、下記を追加。
/ {
aliases {
gpioc = &{/soc/pinctrl@50002000/gpio@50004000};
};
};
このアドレスらしき数値は、dtsiファイル内に記載されている。下記ファイルに、上記アドレス情報が見つかる。
...
pinctrl: pinctrl@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp135-pinctrl";
ranges = <0 0x50002000 0x8400>;
interrupt-parent = <&exti>;
st,syscfg = <&exti 0x60 0xff>;
pins-are-numbered;
...
gpioc: gpio@50004000 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x2000 0x400>;
clocks = <&rcc GPIOC>;
st,bank-name = "GPIOC";
ngpios = <16>;
gpio-ranges = <&pinctrl 0 32 16>;
};
...
今回の対象となるstm32mp135f-coremp135.dtsのincludeファイルを一つ一つ辿ると、stm32mp131.dtsiがincludeされているようだ(詳細略)
...
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/leds/common.h>
#include <dt-bindings/rtc/rtc-stm32.h>
#include "stm32mp135.dtsi"
#include "stm32mp13xf.dtsi"
#include "stm32mp13-pinctrl.dtsi"
...
イメージBuild
下記は最終結果、正直、最も苦労したところである。Ubuntu上で実施している。
カーネルBuild環境作成
本家サイトに従い、ソースコードなどを取得する。
$ git clone https://github.com/m5stack/CoreMP135_buildroot-external-st.git
Build。
$ cd CoreMP135_buildroot-external-st/tools/
$ sudo ./creat_coremp135_debian12_image.sh
...
/home/undeux3/proj/CoreMP135/CoreMP135_buildroot-external-st/tools
M5_CoreMP135_debian12_20260209.img creat success!
debian用のイメージとともに、カーネルBuild環境が作成される。
$ cd CoreMP135_buildroot-external-st/tools/\
build_coremp135_buidlroot/CoreMP135_buildroot
$
$ sudo chown -R $USER:$USER .
編集などが容易となるよう、所有者の変更を行なっておく。
デバイスドライバBuildに向けて
Miscデバイスとして登録する。
cd output/build/linux-custom/drivers/misc/
ソースコード
前述ソースコードを、”gpio_test.c”として、このディレクリに配置。
Makefile
gpio_test.cコンパイル用定義の追加。
obj-$(CONFIG_GPIO_TEST) += gpio_test.o
Kconfig
menuconfig用定義の追加。
config GPIO_TEST
tristate "M5Stack CoreMP135 GPIO Button/LED driver"
depends on GPIOLIB && OF
help
GPIO button (PC6) and LED (PC7) driver for CoreMP135.
BuildするTopディレクトに戻り、”make menuconfig”実施。
$ cd ~/proj/CoreMP135/CoreMP135_buildroot-external-st/tools/build_coremp135_buidlroot/CoreMP135_buildroot
$ make menuconfig
ここでは、単に”Save”するのみ。次に、本デバイスドライバの取り込み実施。
$ linux-menuconfig
Device Drivers --> Misc devices にて、選択できる(下記参照)。
ここで、”Save”する。
$ ls -l output/build/linux-custom/.config
-rw-r--r-- 1 name1 name1 193035 2月 10 09:33 output/build/linux-custom/.config
本デバイスドライバを含んだ”.config”が作成される。
Build
$ make
終了後、オプジェクトができていることを確認。
$ ls -l output/build/linux-custom/drivers/misc/gpio_test.*
-rw-r--r-- 1 name1 name1 6417 2月 10 09:16 output/build/linux-custom/drivers/misc/gpio_test.c
-rw-r--r-- 1 name1 name1 125892 2月 10 09:36 output/build/linux-custom/drivers/misc/gpio_test.o
DTS,DTB
$ ls output/build/linux-custom/arch/arm/boot/dts/stm32mp135f-coremp135.dt*
output/build/linux-custom/arch/arm/boot/dts/stm32mp135f-coremp135.dtb
output/build/linux-custom/arch/arm/boot/dts/stm32mp135f-coremp135.dts
上記”stm32mp135f-coremp135.dts”を修正(前述の内容)、DTBを作成するのであるが、ここは手動で実施する必要があった。
$ cpp -nostdinc \
-I include \
-I arch/arm/boot/dts \
-undef -x assembler-with-cpp \
arch/arm/boot/dts/stm32mp135f-coremp135.dts \
/tmp/stm32mp135f-coremp135.dts.pp
$
$ dtc -I dts -O dtb \
/tmp/stm32mp135f-coremp135.dts.pp \
-o arch/arm/boot/dts/stm32mp135f-coremp135.dtb
creat_coremp135_debian12_image.sh修正
本Scriptをそのまま実行しても、上記で作成したstm32mp135f-coremp135.dtbは使われない。無理やりコピーする必要があるようで、Scriptを修正する。
mkdir -p rootfs_overlay ;sudo cp rootfs/boot rootfs_overlay/ -a
# 下記1行を追加(上記cpで別のdtbが使われてしまうので)
sudo cp \
../build_coremp135_buidlroot/CoreMP135_buildroot/output/build/linux-custom/arch/arm/boot/dts/stm32mp135f-coremp135.dtb \
rootfs_overlay/boot/stm32mp135f-coremp135.dtb
mkdir -p rootfs_overlay/usr/lib ;sudo cp rootfs/lib/modules rootfs_overlay/usr/lib/ -a
mkdir -p rootfs_overlay/usr/lib ;sudo cp rootfs/lib/firmware rootfs_overlay/usr/lib/ -a
再Build
$ cd ~/proj/CoreMP135/CoreMP135_buildroot-external-st/tools/
$ sudo ./creat_coremp135_debian12_image.sh
....
/home/undeux3/proj/CoreMP135/CoreMP135_buildroot-external-st/tools
M5_CoreMP135_debian12_20260210.img creat success!
作成された”M5_CoreMP135_debian12_20260210.img”を、マイクロSDに書き込む。
$ sudo dd if=./M5_CoreMP135_debian12_20260210.img of=/dev/sdb bs=4M
検証
マイクロSDを本体に入れて起動。デバッグログをONにした状態で実施。コマンドdmesgにて、メッセージを確認できる。
起動
# dmesg | grep gpio_test
[ 0.680360] gpio_test gpio-test: gpio_button / gpio_led ready
#
# ls /dev/gpio*
/dev/gpio_button /dev/gpiochip1 /dev/gpiochip4 /dev/gpiochip7
/dev/gpio_led /dev/gpiochip2 /dev/gpiochip5 /dev/gpiochip8
/dev/gpiochip0 /dev/gpiochip3 /dev/gpiochip6
デバイス登録ログが見える。また、デバイス(/dev/gpio_button, /dev/gpio_led)が作成されている。
LED
# echo 1 > /dev/gpio_led
# echo 0 > /dev/gpio_led
# dmesg | tail
...
[ 346.998467] gpio_test gpio-test: gpio_led_open
[ 346.998583] gpio_test gpio-test: gpio_led_write: 1
[ 350.512281] gpio_test gpio-test: gpio_led_open
[ 350.512399] gpio_test gpio-test: gpio_led_write: 0
ボタン
# cat /dev/gpio_button
Pressed
# dmesg | tail
...
[ 390.397815] gpio_test gpio-test: gpio_button_open
[ 390.397982] gpio_test gpio-test: gpio_button_read: waiting event
[ 399.788052] gpio_test gpio-test: gpio_button_read: event happend
[ 399.790530] gpio_test gpio-test: gpio_test_work: button pressed; wake_up
終わりに
正しくボタン押下を検出しないこともあった。上記コードでは、立ち下がり検出(IRQF_TRIGGER_FALLING)となっている。このあたりは、ハードウェアの特性に従って、適切に調整(修正)する必要があるだろう。
ただ、カーネルに組み込まれたデバイスドライバを実現することが、今回の目的であり、機能実現やコードの正確性は、別の機会としたい。


