環境

設備: Raspberry pi 5
OS : $ uname -a

Linux pi 6.6.31+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.6.31-1+rpt1 (2024-05-29) aarch64 GNU/Linux

參考資料

The Linux Kernel Module Programming Guide

事前作業

Raspberry Pi 5 Pinout

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

更新 Packages

sudo apt update
sudo apt upgrade -y

安裝 Kernel Header

sudo apt install -y raspberrypi-kernel-headers

安裝工具包

sudo apt install -y build-essential

重啟載入新 Kernel

sudo reboot

$ls /dev 可以查看設備編號,透過給予這些編號將其與 kernel 連接。

顯示設備編號分別有 character dev 與 block dev

$ cat /proc/devices 

基本操作

當前有的 moduel

sudo lsmod

也可以直接從指定路徑尋找

sudo cat /proc/modules
$ sudo lsmod | grep i2c_i801

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

載入 module

sudo insmod [module_name].ko

載卸 module

sudo rmmod [module_name].ko

實作

Hellow World

kernel module 相比應用程序,必須告訴 kernel 我在哪,我該被載卸,因此必須設定 __init__exit

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

hello.c

#include <linux/init.h> /* Needed for the macros */ 
#include <linux/module.h> /* Needed by all modules */ 

static int __init hello_init(void) 
{ 
	printk("Hello world!!!\n"); 
	return 0; 
} 

static void __exit hello_exit(void) 
{ 
	printk("Goodbye, world\n"); 
} 

module_init(hello_init); 
module_exit(hello_exit); 

MODULE_LICENSE("GPL");

Makefile

obj-m += hello.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:	
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

-C 到達此目錄後在編譯,編譯 hello.c 來生成 hello.o,然後進一步生成 hello.ko
M= 使 Makefile 在試圖建立模塊前,回到你的 module 源碼目錄。

make

$ make
make -C /lib/modules/6.6.51+rpt-rpi-2712/build M=/home/pi/LKM/01_HelloWorld modules
make[1]: Entering directory '/usr/src/linux-headers-6.6.51+rpt-rpi-2712'
  CC [M]  /home/pi/LKM/01_HelloWorld/hello.o
  MODPOST /home/pi/LKM/01_HelloWorld/Module.symvers
  CC [M]  /home/pi/LKM/01_HelloWorld/hello.mod.o
  LD [M]  /home/pi/LKM/01_HelloWorld/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.6.51+rpt-rpi-2712'
$ ls
hello.c   hello.mod    hello.mod.o  Makefile       Module.symvers
hello.ko  hello.mod.c  hello.o      modules.order

結果

$ sudo insmod hello.ko 
$ sudo rmmod hello.ko

另開一個終端來顯示輸出內容

$ sudo dmesg -W
[ 2075.106107] Hello world!!!
[ 2080.365640] Goodbye, world
  • dmesg : 顯示 kernel 環境的訊息。
  • -W : 監控模式

Read and Write

驅動程式類型 Block device 與 character device

  • Block device
    Block devices 可透過 /dev 中的 Filesystem nodes 來 Accessed。一個 Block 只能處理一到多個完整 Block 的 I/O 操作,大小為 512 bytes (或是更大的2的次方)。Linux 可以讓 User Space 讀取 Block device 如同操作File 一樣,而不用一次讀一整個 Block,在 User Space 中使用與 Char 基本上無差別。有差別的是在 Kernel & Driver 這一層完全不同。
  • Character devices
    Character devices 可以被當作 A stream of bytes 來被 Accessed,就像 File 一樣。因此 Char dirver 就要負責至少 open, close, read, and write 的 system calls 操作,有的還有提供 ioctl 的操作方式。常見的例子有 Text Console (/dev/console) and the Serial Ports (/dev/ttyS0),都是 Streaming 結構。Char devices 由 Filesystem nodes 來 accessed, such as /dev/tty1 and /dev/lp0。但跟一般 File 不同的是,一般 File 可以把指標往前往後來操作檔案,但是多數 Char device 只是 Data Channels,只能依序 Access。當然也有例外,而該例外多出現在 Frame Grabbers,就會像 Data Areas 一樣,可以在之中隨意前後操作,多用 mmap or lseek 來配合使用。
  • Block 與 Character 的差異
    Block devices 是以固定大小長度來傳送轉移資料,而 Character devices 是以不定長度的字元傳送資料,而其操作方式在 User Spcae 則大同小異。

取自 Driver (驅動) - 從零開始的開源地下城

read_write.c

#include <linux/init.h> /* Needed for the macros */ 
#include <linux/module.h> /* Needed by all modules */ 
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

/* buffer for data */
static char buffer[255];
static size_t  buffer_pointer = 0;

/* variables for device and device class */
static dev_t my_dev_num;
static struct class *my_class;
static struct cdev my_dev;

#define DRIVER_NAME "dummydriver"
#define DRIVER_CLASS "MyModuleClass"

/* read data out of the buffer */
static ssize_t driver_read(struct file *file, char *usr_buffer, size_t count, loff_t *offs){
	int to_copy, not_copied, delta;

	/* get amount of data to cp*/
	to_copy = min(count, buffer_pointer);

	/* copy data to user*/
	not_copied = copy_to_user(usr_buffer, buffer, to_copy);

	/* calcaulate data */
	delta = to_copy - not_copied;

	return delta;
}

/* write data to buffer */
static ssize_t driver_write(struct file *file, const char *usr_buffer, size_t count, loff_t *offs){
	int to_copy, not_copied, delta;

	/* get amount of data to cp*/
	to_copy = min(count, sizeof(buffer));

	/* copy data to user*/
	not_copied = copy_from_user(buffer, usr_buffer, to_copy);
	buffer_pointer = to_copy;

	/* calcaulate data */
	delta = to_copy - not_copied;

	return delta;
}

static int driver_open(struct inode *driver_file, struct file *instance){
	printk("dev_num - open was called!\n");
	return 0;
}

static int driver_close(struct inode *driver_file, struct file *instance){
	printk("dev_num - close was called!\n");
	return 0;
}

static struct file_operations fops = {
	.owner = THIS_MODULE,
	.open = driver_open,
	.release = driver_close,
	.read = driver_read,
	.write = driver_write
};

static int __init hello_init(void) 
{ 
	printk("Hello world!!!\n"); 

	/* allocate a dev num*/
	if(alloc_chrdev_region(&my_dev_num, 0, 1, DRIVER_NAME) < 0){
		printk("device number cant be allocated!\n");
		return -1;
	}
	printk("read_write - device number Major: %d, Minor: %d was registered!\n", my_dev_num >> 20, my_dev_num && 0xfffff);

	/* create device class*/
	if((my_class = class_create(DRIVER_CLASS)) == NULL){
		printk("device class cant be created!\n");
		goto ClassError;
	}

	/* create device file */
	if(device_create(my_class, NULL, my_dev_num, NULL, DRIVER_NAME) == NULL){
		printk("cant create device file!\n");
		goto FileError;
	}

	/* initalize device file */
	cdev_init(&my_dev, &fops);

	/* register device to kernel */
	if(cdev_add(&my_dev, my_dev_num, 1) == -1){
		printk("register of device to kernel failed!\n");
		goto AddError;
	}

	return 0; 
AddError:
	device_destroy(my_class, my_dev_num);
FileError:
	class_destroy(my_class);
ClassError:
	unregister_chrdev_region(my_dev_num, 1);
	return -1; 
} 

static void __exit hello_exit(void) 
{ 
	cdev_del(&my_dev);
	device_destroy(my_class, my_dev_num);
	class_destroy(my_class);
	unregister_chrdev_region(my_dev_num, 1);
	printk("Goodbye, world\n"); 
} 

module_init(hello_init); 
module_exit(hello_exit); 

MODULE_LICENSE("GPL");

uaccess.h 用作 user 與 kernel 之間的數據傳輸,例如 copy_to_usercopy_from_user

/include/linux/uaccess.h

copy_to_user(void __user *to, const void *from, unsigned long n)
copy_from_user(void *to, const void __user *from, unsigned long n)

下圖說明了讀取操作以及資料如何在用戶空間和驅動程式之間傳輸:

  1. when the driver has enough data available (starting with the OFFSET position) to accurately transfer the required size (SIZE) to the user.
  2. when a smaller amount is transferred than required.
    image
    image

設備編號

alloc_chrdev_region : 向 linux kernel 申請設備號給定次設備號和名字,告訴linux要申請幾個設備號,讓linux直接幫你尋找主設備號
register_chrdev_region : 自定義初始設備編號
unregister_chrdev_region : 註銷設備號

註冊完會出現在 /proc/devicessysfs 中,若註冊成功返回 0 ,失敗則為負值。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name);
int register_chrdev_region(dev_t from, unsigned count, const char *name);
void unregister_chrdev_region(dev_t from, unsigned count)

文件操作

/include/linux /fs.h

file_operations 結構體負責實現系統調用

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
			unsigned int flags);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	void (*splice_eof)(struct file *file);
	int (*setlease)(struct file *, int, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
	int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
	int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
				unsigned int poll_flags);
} __randomize_layout;

當中 struct module *owner; 並非一個操作,他指向擁有這個結構的 module 指針。用來阻止 module 被卸載。

新字元設備註冊方式 cdev.h

/include/linux/cdev.h

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
} __randomize_layout;

使用 cdev 表示 char device,然後使用 cdev_init() 初始化結構體,然後透過 cdev_add() 進行添加到linux kernel中,最後,註銷時使用 cdev_del()

void cdev_init(struct cdev *cdev, const struct file_operations *fops);
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
void cdev_del(struct cdev *p);

錯誤處理

在註冊時發生任何錯誤,第一件事是決定 module 是否能夠無論如何繼續初始化他自己,常常,在一個註冊失敗後模組可以繼續操作, 如果需要可以功能降級. 在任何可能的時候, 你的模組應盡力向前, 並提供事情失敗後具備的能力。
若確定註冊失敗將導致整個 module 無法加載,使用 goto 可以省略大量複雜的程式。

	/* create device class*/
	if((my_class = class_create(DRIVER_CLASS)) == NULL){
		printk("device class cant be created!\n");
		goto ClassError;
	}

	/* create device file */
	if(device_create(my_class, NULL, my_dev_num, NULL, DRIVER_NAME) == NULL){
		printk("cant create device file!\n");
		goto FileError;
	}

	/* initalize device file */
	cdev_init(&my_dev, &fops);

	/* register device to kernel */
	if(cdev_add(&my_dev, my_dev_num, 1) == -1){
		printk("register of device to kernel failed!\n");
		goto AddError;
	}

	return 0; 
AddError:
	device_destroy(my_class, my_dev_num);
FileError:
	class_destroy(my_class);
ClassError:
	unregister_chrdev_region(my_dev_num, 1);
	return -1; 

goto 語句在失敗情況下使用, 在事情變壞之前只對先前已成功註冊的設施進行註銷。

注意: 按照註冊時得順序相反的註銷

Makefile

obj-m += read_write.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:	
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

掛載 module

$ sudo insmod read_write.ko 
$ sudo rmmod read_write.ko 
$ dmesg | tail
[11959.284419] Hello world!!!
[11959.284428] read_write - device number Major: 510, Minor: 1 was registered!
[11961.040756] Goodbye, world

執行結果

$ sudo insmod read_write.ko 
$ ls /dev/dummydriver 
/dev/dummydriver
$ sudo chmod 666 /dev/dummydriver 
$ echo "Hello World!" > /dev/dummydriver 
$ head -n 1 /dev/dummydriver 
Hello World!

GPIO driver

gpio_driver.c

#include <linux/init.h> /* Needed for the macros */ 
#include <linux/module.h> /* Needed by all modules */ 
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>

/* variables for device and device class */
static dev_t my_dev_num;
static struct class *my_class;
static struct cdev my_dev;

#define DRIVER_NAME "LED_gpio_driver"
#define DRIVER_CLASS "MyModuleClass" // After class_create, the device exports to /sys/class/

#define GPIO_LED 575  // Corresponds to gpio4
#define GPIO_BUTTON 588  // Corresponds to gpio17

/* read data out of the buffer */
static ssize_t driver_read(struct file *file, char *usr_buffer, size_t count, loff_t *offs){
	int to_copy, not_copied, delta;
	char tmp[3] = " \n";

	/* get amount of data to cp*/
	to_copy = min(count, sizeof(tmp));

	/* Read value of button */
	printk("gpio 17 value: %d\n", gpio_get_value(GPIO_BUTTON));
	tmp[0] = gpio_get_value(GPIO_BUTTON) + '0'; // store to tmp[0]

	/* copy data to user*/
	not_copied = copy_to_user(usr_buffer, &tmp, to_copy);

	/* calcaulate data */
	delta = to_copy - not_copied;

	return delta;
}

/* write data to buffer */
static ssize_t driver_write(struct file *file, const char *usr_buffer, size_t count, loff_t *offs){
	int to_copy, not_copied, delta;
	char value;

	/* get amount of data to cp*/
	to_copy = min(count, sizeof(value));

	/* copy data from user*/
	not_copied = copy_from_user(&value, usr_buffer, to_copy);
	switch(value){
		case '0':
			gpio_set_value(GPIO_LED, 0);
			break;
		case '1':
			gpio_set_value(GPIO_LED, 1);
			break;
		default:
			printk("Invalid value!\n");
			break;
	}

	/* calcaulate data */
	delta = to_copy - not_copied;

	return delta;
}

static int driver_open(struct inode *driver_file, struct file *instance){
	printk("dev_num - open was called!\n");
	return 0;
}

static int driver_close(struct inode *driver_file, struct file *instance){
	printk("dev_num - close was called!\n");
	return 0;
}

static struct file_operations fops = {
	.owner = THIS_MODULE,
	.open = driver_open,
	.release = driver_close,
	.read = driver_read,
	.write = driver_write
};

static int __init hello_init(void) 
{ 
	printk("Hello world!!!\n"); 

	/* allocate a dev num*/
	if(alloc_chrdev_region(&my_dev_num, 0, 1, DRIVER_NAME) < 0){
		printk("device number cant be allocated!\n");
		return -1;
	}
	printk("read_write - device number Major: %d, Minor: %d was registered!\n", my_dev_num >> 20, my_dev_num && 0xfffff);

	/* create device class*/
	if((my_class = class_create(DRIVER_CLASS)) == NULL){
		printk("device class cant be created!\n");
		goto ClassError;
	}

	/* create device file */
	if(device_create(my_class, NULL, my_dev_num, NULL, DRIVER_NAME) == NULL){
		printk("cant create device file!\n");
		goto FileError;
	}

	/* initalize device file */
	cdev_init(&my_dev, &fops);

	/* register device to kernel */
	if(cdev_add(&my_dev, my_dev_num, 1) == -1){
		printk("register of device to kernel failed!\n");
		goto AddError;
	}

	/* set gpio 4 init */
	if(gpio_request(GPIO_LED, "rpi-gpio-4")){
		printk("gpio 4 can't be allocated!\n");
		goto AddError;
	}

	/* set gpio 4 direction */
	if(gpio_direction_output(GPIO_LED, 0)){
		printk("can't set gpio 4 to output!\n");
		goto Gpio4Error;
	}

		/* set gpio 17 init */
	if(gpio_request(GPIO_BUTTON, "rpi-gpio-17")){
		printk("gpio 17 can't be allocated!\n");
		goto AddError;
	}

	/* set gpio 17 direction */
	if(gpio_direction_input(GPIO_BUTTON)){
		printk("cant set gpio 17 to input!\n");
		goto Gpio17Error;
	}

	return 0; 
Gpio17Error:
	gpio_free(GPIO_BUTTON);
Gpio4Error:
	gpio_free(GPIO_LED);
AddError:
	device_destroy(my_class, my_dev_num);
FileError:
	class_destroy(my_class);
ClassError:
	unregister_chrdev_region(my_dev_num, 1);
	return -1; 
} 

static void __exit hello_exit(void) 
{ 
	gpio_set_value(GPIO_LED, 0);
	gpio_free(GPIO_BUTTON);
	gpio_free(GPIO_LED);
	cdev_del(&my_dev);
	device_destroy(my_class, my_dev_num);
	class_destroy(my_class);
	unregister_chrdev_region(my_dev_num, 1);
	printk("Goodbye, world\n"); 
} 

module_init(hello_init); 
module_exit(hello_exit); 

/* Meta Information */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tsai Zhong-Han");
MODULE_DESCRIPTION("A simple gpio driver for setting a LED and reading a button");

GPIO 初始化:

	/* set gpio 4 init */
	if(gpio_request(GPIO_LED, "rpi-gpio-4")){
		printk("gpio 4 can't be allocated!\n");
		goto AddError;
	}

	/* set gpio 4 direction */
	if(gpio_direction_output(GPIO_LED, 0)){
		printk("can't set gpio 4 to output!\n");
		goto Gpio4Error;
	}

		/* set gpio 17 init */
	if(gpio_request(GPIO_BUTTON, "rpi-gpio-17")){
		printk("gpio 17 can't be allocated!\n");
		goto AddError;
	}

	/* set gpio 17 direction */
	if(gpio_direction_input(GPIO_BUTTON)){
		printk("cant set gpio 17 to input!\n");
		goto Gpio17Error;
	}

以下為 GPIO 相關函式
gpio_request : 根據 GPIO 編號註冊該 GPIO 並指定 label
gpio_direction_inputgpio_direction_output : 設置 GPIO 輸出/輸入模式

static inline int gpio_request(unsigned gpio, const char *label)
{
	return -ENOSYS;
}

static inline int gpio_direction_input(unsigned gpio)
{
	return gpiod_direction_input(gpio_to_desc(gpio));
}

static inline int gpio_direction_output(unsigned gpio, int value)
{
	return gpiod_direction_output_raw(gpio_to_desc(gpio), value);
}

Makefile

obj-m += gpio_driver.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:	
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

問題: 載入 module 時無法判別出 4 就是 GPIO4 ,因此查了 $ sudo cat /sys/kernel/debug/gpio 後才知道 GPIO4 對應到的是 gpio-575

錯誤結果

[ 9162.264345] Hello world!!!
[ 9162.264352] read_write - device number Major: 510, Minor: 1 was registered!
[ 9162.264530] gpio 4 can't be allocated!

以下為查詢結果(省略部分內容,請自行查閱)

 $ sudo cat /sys/kernel/debug/gpio 
gpiochip10: GPIOs 512-543, parent: platform/107d508500.gpio, gpio-brcmstb@107d508500:
 gpio-512 (-                   )
 gpio-513 (2712_BOOT_CS_N      |spi10 CS0           ) out hi ACTIVE LOW
 gpio-514 (2712_BOOT_MISO      )
 gpio-515 (2712_BOOT_MOSI      )
 gpio-516 (2712_BOOT_SCLK      )
 ...
gpiochip0: GPIOs 571-624, parent: platform/1f000d0000.gpio, pinctrl-rp1:
 gpio-571 (ID_SDA              )
 gpio-572 (ID_SCL              )
 gpio-573 (GPIO2               )
 gpio-574 (GPIO3               )
 gpio-575 (GPIO4               )
 gpio-576 (GPIO5               )
 gpio-577 (GPIO6               )
 gpio-578 (GPIO7               )
 gpio-579 (GPIO8               )
 gpio-580 (GPIO9               )
 gpio-581 (GPIO10              )
 gpio-582 (GPIO11              )
 gpio-583 (GPIO12              )
 gpio-584 (GPIO13              )
 gpio-585 (GPIO14              )
 gpio-586 (GPIO15              )
 gpio-587 (GPIO16              )
 gpio-588 (GPIO17              )
 gpio-589 (GPIO18              )
 gpio-590 (GPIO19              )
 gpio-591 (GPIO20              )
 gpio-592 (GPIO21              )
 gpio-593 (GPIO22              )
 gpio-594 (GPIO23              )
 gpio-595 (GPIO24              )
 gpio-596 (GPIO25              )
 gpio-597 (GPIO26              )
 gpio-598 (GPIO27              )
 ...

修正

+#define GPIO_LED 575

    /* set gpio 4 init */
-   if(gpio_request(4, "rpi-gpio-4")){
+   if(gpio_request(GPIO_LED, "rpi-gpio-4")){
		printk("gpio 4 can't be allocated!\n");
		goto AddError;
	}

	/* set gpio 4 direction */
-   if(gpio_direction_output(4, 0)){
+   if(gpio_direction_output(GPIO_LED, 0)){
		printk("can't set gpio 4 to output!\n");
		goto Gpio4Error;
	}

輸出結果

$ ls /dev/LED_gpio_driver 
/dev/LED_gpio_driver
$ ls /dev/LED_gpio_driver -al
crw------- 1 root root 510, 0 Nov  7 02:12 /dev/LED_gpio_driver
$ sudo chmod 666 /dev/LED_gpio_driver 
$ echo 1 > /dev/LED_gpio_driver 
$ echo 0 > /dev/LED_gpio_driver
$ head -n 1 /dev/LED_gpio_driver 
0
$ head -n 1 /dev/LED_gpio_driver 
1

head -n 1 /dev/LED_gpio_driver 命令嘗試從 /dev/LED_gpio_driver 設備檔中讀取第一行內容。

  1. LED 開關
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  2. Button 開關
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

LCD Driver

lcd_driver.c

#include <linux/init.h> /* Needed for the macros */ 
#include <linux/module.h> /* Needed by all modules */ 
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <linux/delay.h>

/* variables for device and device class */
static dev_t my_dev_num;
static struct class *my_class;
static struct cdev my_dev;

#define DRIVER_NAME "lcd"
#define DRIVER_CLASS "MyModuleClass" // After class_create, the device exports to /sys/class/

#define ENABLE_PIN 573  // corresponds to GPIO 2
#define REGISTER_SELECT_PIN 574  // corresponds to GPIO 3
#define LCD_DATA_PIN_0 575  // corresponds to GPIO 4
#define LCD_DATA_PIN_1 588  // corresponds to GPIO 17
#define LCD_DATA_PIN_2 598	// corresponds to GPIO 27
#define LCD_DATA_PIN_3 593	// corresponds to GPIO 22
#define LCD_DATA_PIN_4 581	// corresponds to GPIO 10
#define LCD_DATA_PIN_5 580	// corresponds to GPIO 9
#define LCD_DATA_PIN_6 582	// corresponds to GPIO 11
#define LCD_DATA_PIN_7 576	// corresponds to GPIO 5

/*lcd char buffer */
static char lcd_buffer[17];

unsigned int gpios[] = {
	ENABLE_PIN,
	REGISTER_SELECT_PIN,
	LCD_DATA_PIN_0,
	LCD_DATA_PIN_1,
	LCD_DATA_PIN_2,
	LCD_DATA_PIN_3,
	LCD_DATA_PIN_4,
	LCD_DATA_PIN_5,
	LCD_DATA_PIN_6,
	LCD_DATA_PIN_7
};
#define ENABLE gpios[0]
#define REGISTER_SELECT gpios[1]

/* generate a pulse on the enable signal */
void lcd_enable(void){
	gpio_set_value(gpios[0], 1);
	msleep(20);
	gpio_set_value(gpios[0], 0);
}

/* set 8 bit data bus */
void lcd_send_byte(char data){
	int i;
	for(i = 0; i < 8; i++){
		gpio_set_value(gpios[i+2], ((data) & (1<<i)) >> i);
	}
	lcd_enable();
	msleep(20);
}

/* send command to the lcd  */
void lcd_command(uint8_t data){
	gpio_set_value(REGISTER_SELECT, 0); // RS to instruction
	lcd_send_byte(data);
}

void lcd_data(uint8_t data){
	gpio_set_value(REGISTER_SELECT, 1); // RS to data
	lcd_send_byte(data);
}


/* write data to buffer */
static ssize_t driver_write(struct file *file, const char *usr_buffer, size_t count, loff_t *offs){
	int to_copy, not_copied, delta;

	/* get amount of data to cp*/
	to_copy = min(count, sizeof(lcd_buffer));

	/* copy data from user*/
	not_copied = copy_from_user(lcd_buffer, usr_buffer, to_copy);

	/* calcaulate data */
	delta = to_copy - not_copied;

	/* set the new data to the display */
	lcd_command(0x1); // clear display

	for(int i = 0; i < to_copy; i++){
		lcd_data(lcd_buffer[i]);
	}

	return delta;
}

static int driver_open(struct inode *driver_file, struct file *instance){
	printk("dev_num - open was called!\n");
	return 0;
}

static int driver_close(struct inode *driver_file, struct file *instance){
	printk("dev_num - close was called!\n");
	return 0;
}

static struct file_operations fops = {
	.owner = THIS_MODULE,
	.open = driver_open,
	.release = driver_close,
	.write = driver_write
};

static int __init ModuleInit(void) 
{ 
	printk("Hello kernel!!!\n"); 
	char *names[] = {"ENABLE_PIN", "REGISTER_SELECT", "DATA_PIN0", 
					"DATA_PIN1", "DATA_PIN2", "DATA_PIN3", "DATA_PIN4", 
					"DATA_PIN5", "DATA_PIN6", "DATA_PIN7"};
	int i;

	/* allocate a dev num*/
	if(alloc_chrdev_region(&my_dev_num, 0, 1, DRIVER_NAME) < 0){
		printk("device number cant be allocated!\n");
		return -1;
	}
	printk("read_write - device number Major: %d, Minor: %d was registered!\n", my_dev_num >> 20, my_dev_num && 0xfffff);

	/* create device class*/
	if((my_class = class_create(DRIVER_CLASS)) == NULL){
		printk("device class cant be created!\n");
		goto ClassError;
	}

	/* create device file */
	if(device_create(my_class, NULL, my_dev_num, NULL, DRIVER_NAME) == NULL){
		printk("cant create device file!\n");
		goto FileError;
	}

	/* initalize device file */
	cdev_init(&my_dev, &fops);

	/* register device to kernel */
	if(cdev_add(&my_dev, my_dev_num, 1) == -1){
		printk("register of device to kernel failed!\n");
		goto AddError;
	}

	/* initialize GPIOs */
	printk("initializing GPIOs\n");
	for(i = 0; i < 10; i++){
		if(gpio_request(gpios[i], names[i])){
			printk("gpio %d can't be allocated!\n", gpios[i]);
			goto GPIOInitError;
		}
	}

	printk("setting GPIOs to output\n");
	for(i = 0; i < 10; i++){
		if(gpio_direction_output(gpios[i], 0)){
			printk("gpio %d can't be set to output!\n", gpios[i]);
			goto GPIODirectionError;
		}
	}

	/* init the display */
	lcd_command(0x30); // 8 bit mode
	lcd_command(0xf); // display on, cursor on, blinking on
	lcd_command(0x1); // clear display 
	char text[] = "Hello!";
	for(i = 0; i < sizeof(text)-1; i++){
		lcd_data(text[i]);
	}

	return 0; 

GPIODirectionError:
	i = 9; // set i to 9 to free all the GPIOs
GPIOInitError:
	for(;i>=0; i--)
		gpio_free(gpios[i]);
AddError:
	device_destroy(my_class, my_dev_num);
FileError:
	class_destroy(my_class);
ClassError:
	unregister_chrdev_region(my_dev_num, 1);
	return -1; 
} 

static void __exit ModuleExit(void) 
{ 
	lcd_command(0x1); // clear display
	for(int i = 0; i < 10; i++){
		gpio_set_value(gpios[i], 0);
		gpio_free(gpios[i]);
	}
	cdev_del(&my_dev);
	device_destroy(my_class, my_dev_num);
	class_destroy(my_class);
	unregister_chrdev_region(my_dev_num, 1);
	printk("Goodbye, kernel\n"); 
} 

module_init(ModuleInit); 
module_exit(ModuleExit); 

/* Meta Information */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tsai Zhong-Han");
MODULE_DESCRIPTION("A simple gpio driver for setting a LED and reading a button");

image

Command 對照表
image

Makefile

obj-m += lcd_driver.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:	
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
 

輸出結果

$ sudo insmod lcd_driver.ko
$ ls /dev/lcd -al
crw------- 1 root root 510, 0 Nov 10 01:08 /dev/lcd
$ sudo chmod 666 /dev/lcd
$ echo "Tsai Zhong-Han  " > /dev/lcd

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

IOCtl

說明

參考教學 : Linux/Device_Driver/IOCTL
IOCtl 說明 :
IOCTL is referred to as Input and Output Control, which is used to talk to device drivers. This system call is available in most driver categories. The major use of this is in case of handling some specific operations of a device for which the kernel does not have a system call by default

處理 kernel 預設沒有系統呼叫的情境 => 讀寫設備 register 、修改 Serial port 的鮑率

步驟:

  1. 在 Driver 建立 IOCtl 命令
  2. 在 Driver 編寫 IOCtl 函式
  3. 在 user space 建立 IOCtl 命令
  4. 在 user space 使用 IOCtl 系統呼叫

定義 ioctl 指令

#define "ioctl name" __IOX("magic number","command number","argument type")

其中 __IOX 可以是:
IO:不含參數的 ioctl
IOW:具有寫入參數的 ioctl (copy_from_user)
IOR:具有讀取參數的 ioctl (copy_to_user)
IOWR:具有寫入和讀取參數的 ioctl

  • Magic Number 是唯一的數字,用來區分所有 ioctl 呼叫。有時設置為主設備編號。
  • 指派給 ioctl 的編號,用來區分命令編號
  • Data 類型
    範例:
#define IOC_MAGIC 100

/* Set the message of the device */
#define WR_VALUE  _IOW(IOC_MAGIC, 0, int32_t *)

/* Get the message of the device */
#define RD_VALUE  _IOR(IOC_MAGIC, 1, int32_t *)

定義 ioctl 函式

int  ioctl(struct inode *inode,struct file *file,unsigned int cmd,unsigned long arg)

inode:是正在處理的檔案的索引節點號
file:是指向應用程式傳遞的檔案的檔案指標
cmd:是從 user space 呼叫的 ioctl 指令
arg:是從 user space 傳遞的參數

範例:

static long int my_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
    switch(cmd){
        case WR_VALUE:
            if(copy_from_user(&value, (int32_t *)arg, sizeof(value))){
                printk("Data Write : Error\n");
            }
            printk("Value = %d", value);
            break;
        case RD_VALUE:
            if(copy_to_user((int32_t *)arg, &value, sizeof(value))){
                printk("Data Read : Error\n");
            }
            break;
        default:
            printk("Default\n");
            break;
    }
    return 0;
}

static struct file_operations fops = {
	.owner = THIS_MODULE,
	.open = driver_open,
	.release = driver_close,
    .unlocked_ioctl = my_ioctl
};

在 user space 使用 IOCTL 系統呼叫
引入標頭檔 #include <sys/ioctl.h>

long ioctl( "file descriptor","ioctl command","Arguments");

file descriptor:需要執行ioctl指令的開啟文件,一般是設備文件。
ioctl command:為實現所需功能而實現的 ioctl 指令
arguments:需要將參數傳遞給 ioctl 指令。

user space 範例:

int dev = open("/dev/ioctl_ex", O_RDWR); 

printf("Writing Value to Driver\n");
ioctl(dev, WR_VALUE, (int32_t*) &value); 
printf("Reading Value from Driver\n");
ioctl(dev, RD_VALUE, (int32_t*) &value);

其中 O_RDWR 開啟讀寫功能,還有像是 O_RDONLYO_WRONLY

程式碼

ioctl_ex.c
Makefile

test.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include "ioctl_ex.h"
 
int main()
{
        int32_t value;
 
        printf("\nOpening Driver\n");
        int dev = open("/dev/ioctl_ex", O_RDWR);
        if(dev == -1) {
                printf("Cannot open device file...\n");
                return 0;
        }

        ioctl(dev, RD_VALUE, (int32_t*) &value);
        printf("Initial value is %d\n", value);

        printf("Enter the Value to send\n");
        scanf("%d",&value);
        printf("Writing Value to Driver\n");
        ioctl(dev, WR_VALUE, (int32_t*) &value); 
 
        printf("Reading Value from Driver\n");
        ioctl(dev, RD_VALUE, (int32_t*) &value);
        printf("Value is %d\n", value);
 
        printf("Closing Driver\n");
        close(dev);
}

實際結果

image

procfs

說明

/proc 並非存儲在硬碟上的實體文件系統,而是由內核在記憶體中動態生成的虛擬文件系統。這意味著其內容反映了系統的即時狀態,並且隨著系統的運行而變化。

/procfs 可以充當連接用戶空間和核心空間的橋樑。user space 程式可以使用 /proc 中的檔案來讀取 kernel 導出的資訊。

/proc 檔案系統中的每個條目都提供了一些來自核心的資訊,例如:
/proc/meminfo 給出了系統中正在使用的記憶體的詳細資訊,若要讀取此項目的資訊只需

$cat /proc/meminfo

更多項目=>

/proc/devices — 註冊字元和區塊主編號
/proc/iomem — 系統上實體 RAM 和匯流排設備位址
/proc/ioports — 系統上的 I/O 連接埠位址(特別是對於 x86 系統)
/proc/interrupts — 註冊的中斷請求號碼
/proc/filesystems — 目前活動的檔案系統驅動程式
/proc/cpuinfo — 有關係統上 CPU 的信息
/proc/[pid]:每個運行中的進程都有一個對應的目錄,[pid] 是進程的 ID,該目錄包含有關該進程的詳細資訊

當我們想要調試核心模組時,/proc 檔案系統也非常有用。在偵錯時,我們可能想知道模組中各種變數的值或模組正在處理的資料。在這種情況下,我們可以為自己建立一個 /proc 條目,並轉儲我們想要在該條目中查看的任何資料。

/proc 也可以用於透過寫入核心的方式向核心傳遞數據,因此可以有兩種 /proc

  1. 僅從核心空間讀取資料的條目
  2. 從核心空間讀取資料以及將資料寫入核心空間的條目

程式碼

procfs_ex.c

創建在 /proc/* 底下的資料夾 API

struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent)
  • name : 創建 /proc 底下資料夾名稱
  • parent : 指向該資料的指標

創建 proc 底下的 entry

struct proc_dir_entry *proc_create ( const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops )
  • name : entry 名稱
  • mode : 存取模式, 0666 => 所有人、群組、其他人皆可讀寫, 開頭的 0 表示八進制
  • parent 指向該 entry 的指標
  • proc_fops : 將在其中建立 proc entry 的文件操作的結構

註冊該 proc entry filesystem

static struct proc_ops proc_fops = {
    .proc_read = procfs_read,
    .proc_write  = procfs_write,
};

需要注意在這結構體只適用 Linux v5.6+ ,詳情可以看 lkmpg_the-procops-structure

Makefile

實際運行

image

sysfs

說明

sysfs 是針對特定裝置將系統資訊從核心空間匯出到使用者空間的常用方法

$ ls /sys/
block  bus  class  dev  devices  firmware  fs  kernel  module  power

sysfs 與 procfs 差異在於,sysfs 專注於設備和驅動程序的屬性和狀態,procfs 提供系統運行時信息和執行緒資訊

kobject

  • kobject 是核心(kernel)與 sysfs 文件系統之間的橋樑,定義在 <linux/kobject.h> 檔案中
  • sysfs 是 Linux 中用來展示內核對象(如設備和驅動)的虛擬文件系統
  • 一個 kobject 通常代表 kernel 中的某個物件,例如設備(device)或其他資源
#define KOBJ_NAME_LEN 20 

struct kobject {
 char *k_name;
 char name[KOBJ_NAME_LEN];
 struct kref kref;
 struct list_head entry;
 struct kobject *parent;
 struct kset *kset;
 struct kobj_type *ktype;
 struct dentry *dentry;
};

那該如何在 /sys 底下建立子目錄,使用 kobject_create_and_add 函式

struct kobject * kobject_create_and_add ( const char * name, struct kobject * parent);
  • name : 名稱
  • parent : 說明參考如下
    If you pass kernel_kobj to the second argument, it will create the directory under /sys/kernel/. If you pass firmware_kobj to the second argument, it will create the directory under /sys/firmware/. If you pass fs_kobj to the second argument, it will create the directory under /sys/fs/. If you pass NULL to the second argument, it will create the directory under /sys/.

kobject 結構體註冊失敗將回傳 NULL

This function creates a kobject structure dynamically and registers it with sysfs. If the kobject was not able to be created, NULL will be returned.

當結束此結構,使用 kobject_put 函式,該結構體將在不再被使用時被動態釋放

When you are finished with this structure, call kobject_put and the structure will be dynamically freed when it is no longer being used

範例:

struct kobject *kobj_ref;

/*Creating a directory in /sys/kernel/ */
kobj_ref = kobject_create_and_add("etx_sysfs",kernel_kobj); //sys/kernel/etx_sysfs

/*Freeing Kobj*/
kobject_put(kobj_ref);

創建 sysfs file
在上面函式中我們建立 /sys 底下的子目錄,現在我們需要建立一個 sysfs 文件,該文件用於透過sysfs來實現 user space 與 kernel space 的互動。所以我們可以使用 sysfs 屬性來建立 sysfs 檔案

定義屬性: Kobj_attribute

struct kobj_attribute {
 struct attribute attr;
 ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr, char *buf);
 ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count);
};

其中 attribute 結構體定義名稱與權限

struct attribute {
    const char *name; // 名稱
    umode_t mode;     // 權限(八進制表示,如 0444, 0666 等)
};
  • attr : 定義屬性
  • show : 指向在 sysfs 中讀取檔案時將呼叫的函數的指標,當我們向 sysfs 屬性寫入內容時,會呼叫store 函數
  • store : 指向將檔案寫入 sysfs 時將被呼叫的函數的指標,當我們讀取 sysfs 屬性時,就會呼叫show 函數

建立 sysfs 檔案

int sysfs_create_file ( struct kobject *  kobj, const struct attribute * attr);
  • kobj : 正在為其建立的物件
  • attr : 屬性描述

完成 sysfs 檔案後,您應該使用以下命令刪除該文件 sysfs_remove_file

void sysfs_remove_file ( struct kobject *  kobj, const struct attribute * attr);

範例:

struct kobj_attribute etx_attr = __ATTR(etx_value, 0660, sysfs_show, sysfs_store);

static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf)
{
    return sprintf(buf, "%d", etx_value);
}

static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count)
{
        sscanf(buf,"%d",&etx_value);
        return count;
}

//This Function will be called from Init function
/*Creating a directory in /sys/kernel/ */
kobj_ref = kobject_create_and_add("etx_sysfs",kernel_kobj);
 
/*Creating sysfs file for etx_value*/
if(sysfs_create_file(kobj_ref,&etx_attr.attr)){
    printk(KERN_INFO"Cannot create sysfs file......\n");
    goto r_sysfs;
}
//This should be called from exit function
kobject_put(kobj_ref); 
sysfs_remove_file(kernel_kobj, &etx_attr.attr);

程式碼

sysfs_test.c
當中使用 __ATTR macro 以下為 linux/kobject.h 中的定義

/ include / linux / sysfs.h

#define __ATTR(_name, _mode, _show, _store) {				\
	.attr = {.name = __stringify(_name),				\
		 .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },		\
	.show	= _show,						\
	.store	= _store,						\
}

kobject_create_and_add 函式的第二個參數中傳遞 kernel_kobj ,表示它將在 /sys/kernel/ 下建立目錄

If you pass kernel_kobj to the second argument, it will create the directory under /sys/kernel/. If you pass firmware_kobj to the second argument, it will create the directory under /sys/firmware/. If you pass fs_kobj to the second argument, it will create the directory under /sys/fs/. If you pass NULL to the second argument, it will create the directory under /sys/.

kobj_ref = kobject_create_and_add("test_dir", kernel_kobj);

Makefile

實際結果

image

Device tree

參考:

說明

程式碼

實際結果

待更新

PWM Driver

查詢 Raspberry pi 5 上 pwm pin 對應在 kernel 上的編號

$ sudo cat /sys/kernel/debug/pwm
platform/107d517a80.pwm, 2 PWM devices
 pwm-0   ((null)              ): period: 0 ns duty: 0 ns polarity: normal
 pwm-1   ((null)              ): period: 0 ns duty: 0 ns polarity: normal

0.96 OLED Driver (i2c)

啟用 Raspberry pi5 的 i2c

sudo raspi-config

選擇 Interfacing Options
找到 I2C,按下 Enable

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --   

確認是否有接正確並且顯示偵測到 i2c 設備

番外篇 : GPIO examples failed to compile on Kernel 6.10+ #285

更新 kernel 至 6.12.1

參考 : How to update your Raspberry Pi Kernel and install Kernel Headers

sudo apt update
sudo apt full-upgrade
sudo apt install rpi-update
sudo rpi-update next
sudo reboot

安裝 rpi-source 以取得核心標頭

sudo apt install git bc bison flex libssl-dev make libncurses5-dev
sudo wget https://raw.githubusercontent.com/jgartrel/rpi-source/master/rpi-source -O /usr/bin/rpi-source
sudo chmod +x /usr/bin/rpi-source
rpi-source --tag-update
rpi-source --default-config

此命令會取得核心原始碼並確保核心標頭與目前正在執行的核心正確對齊。
結果:

$ uname -r
6.12.1-v8-16k+

問題

lkmpg 的 issue GPIO examples failed to compile on Kernel 6.10+ #285

其中的問題點在於 gpio_request_array() & gpio_free_array() 在 kernel 版本 6.10.rc1 中已被移除 Commit dbcedec

改成使用 gpio_request() 來啟用 GPIO 即可

在 unregister 使用 gpio_free() 來取代 gpio_free_array()

linux/gpio.h

/**
 * struct gpio - a structure describing a GPIO with configuration
 * @gpio:	the GPIO number
 * @flags:	GPIO configuration as specified by GPIOF_*
 * @label:	a literal description string of this GPIO
 */
struct gpio {
	unsigned	gpio;
	unsigned long	flags;
	const char	*label;
};

修改後程式碼

/* 
 * intrpt.c - Handling GPIO with interrupts 
 * 
 * Based upon the RPi example by Stefan Wendler (devnull@kaltpost.de) 
 * from: 
 *   https://github.com/wendlers/rpi-kmod-samples 
 * 
 * Press one button to turn on a LED and another to turn it off. 
 */ 
 
#include <linux/gpio.h> 
#include <linux/interrupt.h> 
#include <linux/kernel.h> /* for ARRAY_SIZE() */ 
#include <linux/module.h> 
#include <linux/printk.h> 
 
static int button_irqs[] = { -1, -1 }; 
 
/* Define GPIOs for LEDs. 
 * TODO: Change the numbers for the GPIO on your board. 
 */ 
static struct gpio leds[] = { { 4, GPIOF_OUT_INIT_LOW, "LED 1" } }; 
 
/* Define GPIOs for BUTTONS 
 * TODO: Change the numbers for the GPIO on your board. 
 */ 
static struct gpio buttons[] = { { 17, GPIOF_IN, "LED 1 ON BUTTON" }, 
                                 { 18, GPIOF_IN, "LED 1 OFF BUTTON" } }; 
 
/* interrupt function triggered when a button is pressed. */ 
static irqreturn_t button_isr(int irq, void *data) 
{ 
    /* first button */ 
    if (irq == button_irqs[0] && !gpio_get_value(leds[0].gpio)) 
        gpio_set_value(leds[0].gpio, 1); 
    /* second button */ 
    else if (irq == button_irqs[1] && gpio_get_value(leds[0].gpio)) 
        gpio_set_value(leds[0].gpio, 0); 
 
    return IRQ_HANDLED; 
} 

static int __init intrpt_init(void) 
{ 
    int ret = 0; 
 
    pr_info("%s\n", __func__); 
 
    /* register LED gpios */ 
    ret = gpio_request(leds[0].gpio, leds[0].label); 
 
    if (ret) { 
        pr_err("Unable to request GPIOs for LEDs: %d\n", ret); 
        return ret; 
    } 
 
    /* register BUTTON gpios */ 
    ret = gpio_request(buttons[0].gpio, buttons[0].label); 
 
    if (ret) { 
        pr_err("Unable to request GPIOs for BUTTONs: %d\n", ret); 
        goto fail1; 
    } 

    ret = gpio_request(buttons[1].gpio, buttons[1].label); 
 
    if (ret) { 
        pr_err("Unable to request GPIOs for BUTTONs: %d\n", ret); 
        goto fail2; 
    } 
 
    pr_info("Current button1 value: %d\n", gpio_get_value(buttons[0].gpio)); 
 
    ret = gpio_to_irq(buttons[0].gpio); 
 
    if (ret < 0) { 
        pr_err("Unable to request IRQ: %d\n", ret); 
        goto fail3; 
    } 
 
    button_irqs[0] = ret; 
 
    pr_info("Successfully requested BUTTON1 IRQ # %d\n", button_irqs[0]); 
 
    ret = request_irq(button_irqs[0], button_isr, 
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, 
                      "gpiomod#button1", NULL); 
 
    if (ret) { 
        pr_err("Unable to request IRQ: %d\n", ret); 
        goto fail3; 
    } 
 
    ret = gpio_to_irq(buttons[1].gpio); 
 
    if (ret < 0) { 
        pr_err("Unable to request IRQ: %d\n", ret); 
        goto fail3; 
    } 
 
    button_irqs[1] = ret; 
 
    pr_info("Successfully requested BUTTON2 IRQ # %d\n", button_irqs[1]); 
 
    ret = request_irq(button_irqs[1], button_isr, 
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, 
                      "gpiomod#button2", NULL); 
 
    if (ret) { 
        pr_err("Unable to request IRQ: %d\n", ret); 
        goto fail4; 
    } 
 
    return 0; 
 
/* cleanup what has been setup so far */ 
fail4: 
    free_irq(button_irqs[0], NULL); 

fail3: 
    gpio_free(buttons[1].gpio); 

fail2: 
    gpio_free(buttons[0].gpio); 
 
fail1: 
    gpio_free(leds[0].gpio); 
 
    return ret; 
} 
 
static void __exit intrpt_exit(void) 
{ 
    pr_info("%s\n", __func__); 
 
    /* free irqs */ 
    free_irq(button_irqs[0], NULL); 
    free_irq(button_irqs[1], NULL); 
 
    /* turn all LEDs off */ 
    gpio_set_value(leds[0].gpio, 0); 
 
    /* unregister */ 
    gpio_free(leds[0].gpio); 
    gpio_free(buttons[0].gpio);
    gpio_free(buttons[1].gpio);
} 

module_init(intrpt_init); 
module_exit(intrpt_exit); 
 
MODULE_LICENSE("GPL"); 
MODULE_DESCRIPTION("Handle some GPIO interrupts");

編譯結果成功

$ make
make -C /lib/modules/6.12.1-v8-16k+/build M=/home/jeremypi/LKM/intrpt modules
make[1]: Entering directory '/home/jeremypi/linux-fcadf633eb574f99c13b8dcd127a3934343ea404'
  CC [M]  /home/jeremypi/LKM/intrpt/intrpt.o
  MODPOST /home/jeremypi/LKM/intrpt/Module.symvers
  CC [M]  /home/jeremypi/LKM/intrpt/intrpt.mod.o
  CC [M]  /home/jeremypi/LKM/intrpt/.module-common.o
  LD [M]  /home/jeremypi/LKM/intrpt/intrpt.ko
make[1]: Leaving directory '/home/jeremypi/linux-fcadf633eb574f99c13b8dcd127a3934343ea404'

pull request 可能會用到 git 知識

先將你新的修改加到暫存區 (stage):

git add .

變更上次 commit 可以用

git commit --amend

這會把目前暫存的檔案變更,融合進「上一次 commit」,並讓你可以編輯該 commit 訊息。
如果只想替換內容但不改訊息,可加上 no-edit。

如果這個分支已經 push 到遠端,要用 force push:

git push --force