0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Linuxデバイスドライバ(その3)

0
Posted at

カーネルに組み込まれたデバイスドライバ

M5Stack CoreMP135を用いて、カーネルに組み込まれたデバイスドライバにトライする。機能的には「その2」と同じで、ボタンおよびLEDの制御である。なお、「M5Stack CoreMP135にさわる(その2:DebianのBuild)」も参考。ChatGPTをかなり利用。

特徴

最初からカーネルに組み込む方法は、モジュールロード方式と比べると、作法が異なるらしい。

モジュールロード カーネル組み込み
module_init() builtin_platform_driver()
init() probe()
exit() remove()
手動ロード(insmod) Device Tree(DT)による自動的な組み込み

流れ的に下記となる。

  1. kernel起動
  2. DT読み込み
  3. builtin_platform_driverによる登録
  4. DTのcompatibleスキャンおよび該当デバイス発見
  5. probe()がCall

ソースコード

コード内に適宜コメント追記(「その2」のコードコメントも参照)。ほとんど、ChatGPT作成コード。ただし、1回目に提示されたコードでは、コンパイルできず。この辺りはなんとかならないものか、、、(ChatGPTに依頼すると、時々、この手の状況が発生する)。

gpio_test.c
#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(&gt->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(&gt->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(&gt->wq);
	INIT_WORK(&gt->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(&gt->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(&gt->misc_led);
	if (ret) {
		misc_deregister(&gt->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(&gt->misc_led);
	misc_deregister(&gt->misc_button);
	cancel_work_sync(&gt->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ファイル内に記載されている。下記ファイルに、上記アドレス情報が見つかる。

stm32mp131.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されているようだ(詳細略)

stm32mp135f-coremp135.dts
...

#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 にて、選択できる(下記参照)。

menuconfig-DeviceDrivers.png
menuconfig-MiscDevices.png
menuconfig-GPIO.png

ここで、”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を修正する。

creat_coremp135_debian12_image.sh
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)となっている。このあたりは、ハードウェアの特性に従って、適切に調整(修正)する必要があるだろう。

ただ、カーネルに組み込まれたデバイスドライバを実現することが、今回の目的であり、機能実現やコードの正確性は、別の機会としたい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?