RK3399 內核驅動實戰:獲取設備號控制 LED 的四種方法(由淺入深、代碼注釋詳盡)
在 Linux 字符設備驅動開發中,設備號(major
+ minor
)是內核與用戶空間溝通的橋梁。文章圍繞設備號這一條線展開,從基礎原理,到四種實現方式,并結合完整代碼與注釋,講明每一步為什么要這樣做、容易出錯的地方以及如何糾正。
目錄
- 設備號基礎(
dev_t
、MAJOR
/MINOR
/MKDEV
) - 方法一:
register_chrdev()
(最簡單,快速上手) - 方法二:
alloc_chrdev_region()
+cdev
(工程化、推薦) - 方法三:多設備(多個 minor) +
iminor()
/imajor()
(同一驅動管理多實例) - 方法四:
kfifo
+ 多minor
(虛擬設備示例) - 編譯、加載、測試(Makefile、insmod、用戶態程序)
- 常見坑與調試清單
- 總結與建議
1. 設備號基礎:dev_t
與三個宏
dev_t
:內核用來表示設備號的類型(一般是 32 位數,包含 major 和 minor)。MAJOR(dev_t d)
:取主設備號(major)。MINOR(dev_t d)
:取次設備號(minor)。MKDEV(major, minor)
:通過主次號生成dev_t
。
例:dev_t dev = MKDEV(100, 0);
對應主設備號 100,次設備號 0。用戶空間設備節點 /dev/xxx
會包含這個 dev_t
,內核 VFS 據此找到驅動并調用 file_operations
。
2. 方法一:register_chrdev()
—— 最簡單、快速(單設備適用)
要點
- 調用
register_chrdev(0, name, &fops)
時傳入 0 表示動態分配主設備號,返回值即為分到的主設備號。 - 適用于簡單單設備驅動示例或教學場景。不使用
cdev
,擴展性有限。 - 注意:GPIO 在模塊卸載時再釋放,不要在
init
里立即釋放。
完整內核模塊示例(基于提供代碼,做出必要修正與注釋)
// file: led_register.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/gpio.h>struct rk_led {unsigned int major; // 主設備號dev_t devno; // dev_t(major/minor)struct class *cls; // 用于 device_createstruct device *dev;int gpio_led; // GPIO 編號(示例:GPIO1_C7 -> 55)
};static struct rk_led *led_device;/* open: 打開設備(用戶態 open("/dev/led") 會觸發) */
static int led_open(struct inode *inode, struct file *filp)
{// 打開時點亮 LEDgpio_direction_output(led_device->gpio_led, 1);pr_info("led: open -> ON (gpio=%d)\n", led_device->gpio_led);return 0;
}/* release: 關閉設備(用戶態 close() 會觸發) */
static int led_release(struct inode *inode, struct file *filp)
{// 關閉時熄滅 LEDgpio_direction_output(led_device->gpio_led, 0);pr_info("led: release -> OFF (gpio=%d)\n", led_device->gpio_led);return 0;
}/* 僅實現 open/release,示例簡單 */
static const struct file_operations fops = {.owner = THIS_MODULE,.open = led_open,.release = led_release,
};static int __init led_init(void)
{int ret;// 1. 申請驅動結構體內存led_device = kzalloc(sizeof(*led_device), GFP_KERNEL);if (!led_device)return -ENOMEM;// 2. 動態注冊主設備號(major=0 表示動態分配)ret = register_chrdev(0, "led_drv", &fops);if (ret < 0) {pr_err("register_chrdev failed: %d\n", ret);goto err_register;}led_device->major = ret;led_device->devno = MKDEV(led_device->major, 0);pr_info("led: registered major=%u\n", led_device->major);// 3. 創建設備類與 /dev 節點(需要 udev 支持)led_device->cls = class_create(THIS_MODULE, "led_cls");if (IS_ERR(led_device->cls)) {ret = PTR_ERR(led_device->cls);pr_err("class_create failed: %d\n", ret);goto err_class;}led_device->dev = device_create(led_device->cls, NULL,led_device->devno, NULL, "led");if (IS_ERR(led_device->dev)) {ret = PTR_ERR(led_device->dev);pr_err("device_create failed: %d\n", ret);goto err_device;}// 4. 初始化硬件:申請 GPIO 并設為輸出(這里不要立即 free)led_device->gpio_led = 55; // 示例:GPIO1_C7ret = gpio_request(led_device->gpio_led, "led_pin");if (ret) {pr_err("gpio_request failed: %d\n", ret);goto err_gpio;}gpio_direction_output(led_device->gpio_led, 0); // 默認熄滅pr_info("led: init ok (register_chrdev path)\n");return 0;err_gpio:device_destroy(led_device->cls, led_device->devno);
err_device:class_destroy(led_device->cls);
err_class:unregister_chrdev(led_device->major, "led_drv");
err_register:kfree(led_device);return ret;
}static void __exit led_exit(void)
{// 釋放 GPIOgpio_set_value(led_device->gpio_led, 0);gpio_free(led_device->gpio_led);// 銷毀設備節點與類device_destroy(led_device->cls, led_device->devno);class_destroy(led_device->cls);// 注銷動態分配的 major(與 register_chrdev 配套)unregister_chrdev(led_device->major, "led_drv");kfree(led_device);pr_info("led: exit ok (register_chrdev path)\n");
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("example");
MODULE_DESCRIPTION("LED driver using register_chrdev()");
用戶態測試程序(簡單打開 /dev/led)
// test_open.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>int main(void)
{int fd = open("/dev/led", O_RDONLY);if (fd < 0) {perror("open /dev/led");return 1;}sleep(3); // 打開 3 秒close(fd);return 0;
}
3. 方法二:alloc_chrdev_region()
+ cdev
—— 工程化、推薦(支持擴展)
要點
alloc_chrdev_region(&devno, firstminor, count, name)
:動態分配一段連續設備號,firstminor
表示請求的起始次設備號(通常設為 0),count
表示要申請的數量。- 使用
struct cdev
綁定fops
,通過cdev_add()
將該 cdev 注冊到內核并與設備號關聯。 - 推薦在正式驅動中使用該方法,擴展性好、規范性強。
完整內核模塊示例(含 write/copy_from_user)
// file: led_cdev.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>struct rk_led {dev_t devno; // 分配到的起始 devunsigned int major;struct class *cls;struct device *dev;struct cdev cdev; // 嵌入式 cdev(不需要 cdev_alloc)int gpio_led;int on;
};static struct rk_led *led_device;/* open - 不對 gpio 做太多操作,點亮/熄滅放到 write */
static int led_open(struct inode *inode, struct file *filp)
{pr_info("led_cdev: open\n");return 0;
}/* release - 確保設備關閉時熄滅 */
static int led_release(struct inode *inode, struct file *filp)
{gpio_direction_output(led_device->gpio_led, 0);pr_info("led_cdev: release -> OFF\n");return 0;
}/* write - 從用戶空間讀入整數(0/1),控制 LED */
static ssize_t led_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{int on = 0;if (count < sizeof(on))return -EINVAL;if (copy_from_user(&on, buf, sizeof(on)))return -EFAULT;led_device->on = on ? 1 : 0;gpio_direction_output(led_device->gpio_led, led_device->on);pr_info("led_cdev: write on=%d\n", led_device->on);return sizeof(on);
}static const struct file_operations fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,.release = led_release,
};static int __init led_init(void)
{int ret;led_device = kzalloc(sizeof(*led_device), GFP_KERNEL);if (!led_device)return -ENOMEM;// 1) 動態申請設備號(申請 1 個,從 minor 0 開始)ret = alloc_chrdev_region(&led_device->devno, 0, 1, "led_drv");if (ret) {pr_err("alloc_chrdev_region failed: %d\n", ret);goto err_alloc;}led_device->major = MAJOR(led_device->devno);pr_info("led_cdev: got major=%u minor=%u\n",led_device->major, MINOR(led_device->devno));// 2) 初始化并注冊 cdev(嵌入式 cdev)cdev_init(&led_device->cdev, &fops);led_device->cdev.owner = THIS_MODULE;ret = cdev_add(&led_device->cdev, led_device->devno, 1);if (ret) {pr_err("cdev_add failed: %d\n", ret);goto err_cdev_add;}// 3) 創建設備類與 /dev 節點led_device->cls = class_create(THIS_MODULE, "led_cls");if (IS_ERR(led_device->cls)) {ret = PTR_ERR(led_device->cls);pr_err("class_create failed: %d\n", ret);goto err_class;}led_device->dev = device_create(led_device->cls, NULL,led_device->devno, NULL, "led");if (IS_ERR(led_device->dev)) {ret = PTR_ERR(led_device->dev);pr_err("device_create failed: %d\n", ret);goto err_device;}// 4) 申請并配置 GPIO(不要立即 free)led_device->gpio_led = 13; // 示例:GPIO0_B5ret = gpio_request(led_device->gpio_led, "led_pin");if (ret) {pr_err("gpio_request failed: %d\n", ret);goto err_gpio;}gpio_direction_output(led_device->gpio_led, 0);pr_info("led_cdev: init ok\n");return 0;err_gpio:device_destroy(led_device->cls, led_device->devno);
err_device:class_destroy(led_device->cls);
err_class:cdev_del(&led_device->cdev);
err_cdev_add:unregister_chrdev_region(led_device->devno, 1);
err_alloc:kfree(led_device);return ret;
}static void __exit led_exit(void)
{// 釋放 GPIOgpio_set_value(led_device->gpio_led, 0);gpio_free(led_device->gpio_led);// 銷毀設備與類device_destroy(led_device->cls, led_device->devno);class_destroy(led_device->cls);// 刪除 cdev 并釋放設備號cdev_del(&led_device->cdev);unregister_chrdev_region(led_device->devno, 1);kfree(led_device);pr_info("led_cdev: exit ok\n");
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("example");
MODULE_DESCRIPTION("LED driver using alloc_chrdev_region + cdev");
用戶態測試程序(寫 0/1 控制 LED)
// test_write.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>int main(void)
{int fd = open("/dev/led", O_WRONLY);if (fd < 0) {perror("open /dev/led");return 1;}int on = 1;write(fd, &on, sizeof(on));sleep(1);on = 0;write(fd, &on, sizeof(on));close(fd);return 0;
}
4. 方法三:多設備(多個 minor) + iminor()
/imajor()
—— 一個驅動管多實例
要點
- 使用
alloc_chrdev_region(&dev, firstminor, count, name)
申請一段連續的count
個設備號(從firstminor
開始)。 - 使用
cdev_add(&cdev, dev0, count)
將這段連續 minor 全部注冊給同一個fops
。 - 在
open()
中用iminor(inode)
獲取次設備號,根據次設備號選擇具體硬件(不同 gpio)。 - 通過
device_create()
為每個 minor 創建一個/dev/ledX
設備節點,用戶空間分別操作各個實例。
完整示例(管理 4 個 LED)
// file: led_multi.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>#define LED_CNT 4 // 管理 LED 設備數量
#define BASE_MINOR 0 // 起始 minor,可以設為 0// 示例的 GPIO 表:請根據實際板卡修改
static int gpio_table[LED_CNT] = {55, 13, 54, 56};struct rk_led_ctrl {dev_t base_dev; // alloc_chrdev_region 返回的 devunsigned int major;struct class *cls;struct cdev cdev; // 嵌入式 cdev
};static struct rk_led_ctrl *g_ctrl;static int led_open(struct inode *inode, struct file *filp)
{int minor = iminor(inode); // 真實 minor(如 0、1、2、3)int idx = minor - BASE_MINOR; // 轉換為 0..LED_CNT-1 索引if (idx < 0 || idx >= LED_CNT)return -ENODEV;// 將 idx 存到 filp->private_data 中,方便 write/release 使用filp->private_data = (void *)(long)idx;pr_info("led_multi: open minor=%d idx=%d gpio=%d\n",minor, idx, gpio_table[idx]);return 0;
}static int led_release(struct inode *inode, struct file *filp)
{int idx = (int)(long)filp->private_data;// 關閉設備時把對應 LED 關掉gpio_direction_output(gpio_table[idx], 0);pr_info("led_multi: release idx=%d -> OFF\n", idx);return 0;
}static ssize_t led_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{int idx = (int)(long)filp->private_data;int on = 0;if (count < sizeof(on))return -EINVAL;if (copy_from_user(&on, buf, sizeof(on)))return -EFAULT;gpio_direction_output(gpio_table[idx], on ? 1 : 0);pr_info("led_multi: write idx=%d on=%d\n", idx, on);return sizeof(on);
}static const struct file_operations fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,.release = led_release,
};static int __init led_multi_init(void)
{int ret, i;dev_t dev0;g_ctrl = kzalloc(sizeof(*g_ctrl), GFP_KERNEL);if (!g_ctrl)return -ENOMEM;// 1) 申請 LED_CNT 個連續設備號(從 BASE_MINOR 開始)ret = alloc_chrdev_region(&g_ctrl->base_dev, BASE_MINOR, LED_CNT, "led_multi");if (ret) {pr_err("alloc_chrdev_region failed: %d\n", ret);goto err_alloc;}g_ctrl->major = MAJOR(g_ctrl->base_dev);dev0 = MKDEV(g_ctrl->major, BASE_MINOR);pr_info("led_multi: major=%d minors=%d..%d\n", g_ctrl->major, BASE_MINOR, BASE_MINOR + LED_CNT - 1);// 2) 初始化并添加 cdev (一次性綁定整個區間)cdev_init(&g_ctrl->cdev, &fops);g_ctrl->cdev.owner = THIS_MODULE;ret = cdev_add(&g_ctrl->cdev, dev0, LED_CNT);if (ret) {pr_err("cdev_add failed: %d\n", ret);goto err_cdev;}// 3) 創建設備類與多個 /dev/ledX 節點g_ctrl->cls = class_create(THIS_MODULE, "led_cls");if (IS_ERR(g_ctrl->cls)) {ret = PTR_ERR(g_ctrl->cls);pr_err("class_create failed: %d\n", ret);goto err_class;}for (i = 0; i < LED_CNT; i++) {device_create(g_ctrl->cls, NULL,MKDEV(g_ctrl->major, BASE_MINOR + i),NULL, "led%d", i);}// 4) 申請并設置所有 GPIO(按表格)for (i = 0; i < LED_CNT; i++) {ret = gpio_request(gpio_table[i], "led_pin");if (ret) {pr_err("gpio_request[%d]=%d failed\n", i, ret);goto err_gpio;}gpio_direction_output(gpio_table[i], 0);}pr_info("led_multi: init ok\n");return 0;err_gpio:while (--i >= 0)gpio_free(gpio_table[i]);for (i = 0; i < LED_CNT; i++)device_destroy(g_ctrl->cls, MKDEV(g_ctrl->major, BASE_MINOR + i));class_destroy(g_ctrl->cls);
err_class:cdev_del(&g_ctrl->cdev);
err_cdev:unregister_chrdev_region(g_ctrl->base_dev, LED_CNT);
err_alloc:kfree(g_ctrl);return ret;
}static void __exit led_multi_exit(void)
{int i;for (i = 0; i < LED_CNT; i++) {gpio_set_value(gpio_table[i], 0);gpio_free(gpio_table[i]);}for (i = 0; i < LED_CNT; i++)device_destroy(g_ctrl->cls, MKDEV(g_ctrl->major, BASE_MINOR + i));class_destroy(g_ctrl->cls);cdev_del(&g_ctrl->cdev);unregister_chrdev_region(g_ctrl->base_dev, LED_CNT);kfree(g_ctrl);pr_info("led_multi: exit ok\n");
}module_init(led_multi_init);
module_exit(led_multi_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("example");
MODULE_DESCRIPTION("LED multi-device driver (multiple minors)");
用戶態測試(分別對 /dev/led0、/dev/led1 寫 0/1)
// test_multi.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main(void)
{int fd0 = open("/dev/led0", O_WRONLY);if (fd0 < 0) { perror("open led0"); return 1; }int on = 1;write(fd0, &on, sizeof(on));sleep(1);on = 0;write(fd0, &on, sizeof(on));close(fd0);return 0;
}
“如果想在沒有硬件的情況下練習設備號與多實例驅動,可以用 方法四:kfifo + 多 minor。它和方法三結構幾乎一樣,只是實例換成了內存 FIFO。”
5. 方法四:kfifo + 多個 minor —— 虛擬字符設備(無硬件依賴)
要點
- 和方法三一樣,使用
alloc_chrdev_region()
分配一段連續設備號; - 每個 minor 對應一個 獨立的 kfifo 緩沖區,用來存儲用戶態寫入的數據;
- 用戶態對不同的
/dev/virtdevX
節點進行讀寫,就像操作多個獨立的管道; - 特別適合用來做 驅動開發練習、調試 IPC 通道 或 教學示例,因為它不依賴真實硬件。
完整內核模塊示例(每個 minor 一個 kfifo)
// file: virt_kfifo.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/kfifo.h>#define DEV_CNT 4 // 創建多少個虛擬設備
#define BASE_MINOR 0 // 起始 minor
#define FIFO_SIZE 128 // 每個 kfifo 的大小(字節)struct virt_dev {struct kfifo fifo; // 每個設備獨立的 FIFO 緩沖區struct device *dev; // 對應 /dev 節點
};struct virt_ctrl {dev_t base_dev; // 起始設備號unsigned int major;struct class *cls;struct cdev cdev; // 一個 cdev 管理多個 minorstruct virt_dev *vdevs;
};static struct virt_ctrl *g_ctrl;/* open - 保存當前 minor 到 filp->private_data */
static int virt_open(struct inode *inode, struct file *filp)
{int minor = iminor(inode); // 獲取當前打開的次設備號int idx = minor - BASE_MINOR;if (idx < 0 || idx >= DEV_CNT)return -ENODEV;filp->private_data = &g_ctrl->vdevs[idx];pr_info("virt_kfifo: open minor=%d\n", minor);return 0;
}/* release - 無需特別操作 */
static int virt_release(struct inode *inode, struct file *filp)
{pr_info("virt_kfifo: release\n"); //這里只做演示,實際可能需要清空 FIFOreturn 0;
}/* write - 將用戶態數據寫入 kfifo */
static ssize_t virt_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{struct virt_dev *vdev = filp->private_data;unsigned int copied;int ret;if (kfifo_is_full(&vdev->fifo)) {pr_warn("virt_kfifo: fifo full, dropping data\n");return -ENOSPC;}ret = kfifo_from_user(&vdev->fifo, buf, count, &copied);if (ret)return ret;pr_info("virt_kfifo: write %u bytes\n", copied);return copied;
}/* read - 從 kfifo 中取數據返回給用戶態 */
static ssize_t virt_read(struct file *filp, char __user *buf,size_t count, loff_t *ppos)
{struct virt_dev *vdev = filp->private_data;unsigned int copied;int ret;if (kfifo_is_empty(&vdev->fifo)) {pr_info("virt_kfifo: fifo empty\n");return 0; // EOF}ret = kfifo_to_user(&vdev->fifo, buf, count, &copied);if (ret)return ret;pr_info("virt_kfifo: read %u bytes\n", copied);return copied;
}static const struct file_operations fops = {.owner = THIS_MODULE,.open = virt_open,.release = virt_release,.read = virt_read,.write = virt_write,
};/* 模塊加載 */
static int __init virt_init(void)
{int ret, i;dev_t dev0;g_ctrl = kzalloc(sizeof(*g_ctrl), GFP_KERNEL);if (!g_ctrl)return -ENOMEM;/* 1) 分配設備號 */ret = alloc_chrdev_region(&g_ctrl->base_dev, BASE_MINOR, DEV_CNT, "virt_kfifo");if (ret)goto err_alloc;g_ctrl->major = MAJOR(g_ctrl->base_dev);dev0 = MKDEV(g_ctrl->major, BASE_MINOR);pr_info("virt_kfifo: major=%d minors=%d..%d\n", g_ctrl->major,BASE_MINOR, BASE_MINOR + DEV_CNT - 1);/* 2) 注冊 cdev */cdev_init(&g_ctrl->cdev, &fops);g_ctrl->cdev.owner = THIS_MODULE;ret = cdev_add(&g_ctrl->cdev, dev0, DEV_CNT);if (ret)goto err_cdev;/* 3) 創建設備類 */g_ctrl->cls = class_create(THIS_MODULE, "virt_cls");if (IS_ERR(g_ctrl->cls)) {ret = PTR_ERR(g_ctrl->cls);goto err_class;}/* 4) 初始化每個設備的 kfifo 與 /dev 節點 */g_ctrl->vdevs = kcalloc(DEV_CNT, sizeof(struct virt_dev), GFP_KERNEL);if (!g_ctrl->vdevs) {ret = -ENOMEM;goto err_vdevs;}for (i = 0; i < DEV_CNT; i++) {ret = kfifo_alloc(&g_ctrl->vdevs[i].fifo, FIFO_SIZE, GFP_KERNEL);if (ret) {pr_err("virt_kfifo: kfifo_alloc[%d] failed\n", i);goto err_fifo;}g_ctrl->vdevs[i].dev = device_create(g_ctrl->cls, NULL,MKDEV(g_ctrl->major, BASE_MINOR + i),NULL, "virtdev%d", i);}pr_info("virt_kfifo: init ok\n");return 0;err_fifo:while (--i >= 0)kfifo_free(&g_ctrl->vdevs[i].fifo);kfree(g_ctrl->vdevs);
err_vdevs:class_destroy(g_ctrl->cls);
err_class:cdev_del(&g_ctrl->cdev);
err_cdev:unregister_chrdev_region(g_ctrl->base_dev, DEV_CNT);
err_alloc:kfree(g_ctrl);return ret;
}/* 模塊卸載 */
static void __exit virt_exit(void)
{int i;for (i = 0; i < DEV_CNT; i++) {device_destroy(g_ctrl->cls, MKDEV(g_ctrl->major, BASE_MINOR + i));kfifo_free(&g_ctrl->vdevs[i].fifo);}kfree(g_ctrl->vdevs);class_destroy(g_ctrl->cls);cdev_del(&g_ctrl->cdev);unregister_chrdev_region(g_ctrl->base_dev, DEV_CNT);kfree(g_ctrl);pr_info("virt_kfifo: exit ok\n");
}module_init(virt_init);
module_exit(virt_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("example");
MODULE_DESCRIPTION("Virtual multi-device driver using kfifo");
用戶態測試程序
// test_kfifo.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main(void)
{int fd0 = open("/dev/virtdev0", O_RDWR);int fd1 = open("/dev/virtdev1", O_RDWR);if (fd0 < 0 || fd1 < 0) {perror("open virtdev");return 1;}const char *msg = "hello fifo!";char buf[64] = {0};// 寫入 virtdev0write(fd0, msg, strlen(msg));// 從 virtdev0 讀回read(fd0, buf, sizeof(buf));printf("read from virtdev0: %s\n", buf);// 寫入 virtdev1write(fd1, "abc123", 6);memset(buf, 0, sizeof(buf));read(fd1, buf, sizeof(buf));printf("read from virtdev1: %s\n", buf);close(fd0);close(fd1);return 0;
}
- 方法四本質上還是 方法三(多 minor 管理多個實例) 的擴展;
- 區別在于:這里的“實例”不是實際硬件,而是 kfifo 緩沖區;
- 這樣,
/dev/virtdev0
、/dev/virtdev1
… 就像多個獨立的管道,讀寫互不干擾;
6. 編譯、加載、測試(Makefile 與基本命令)
Makefile(通用內核模塊編譯)
# Makefile
obj-m := led_register.o # 或 led_cdev.o / led_multi.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)all:$(MAKE) -C $(KDIR) M=$(PWD) modulesclean:$(MAKE) -C $(KDIR) M=$(PWD) clean
加載模塊與調試
# 編譯
make# 以 root 加載模塊
sudo insmod led_cdev.ko # 或 led_register.ko / led_multi.ko# 看 dmesg 輸出,確認 major/minor 及初始化信息
dmesg | tail -n 20# 查看 /dev 是否生成節點(若無 udev,需手動 mknod)
ls -l /dev/led*
# 若未生成,可手動:
# sudo mknod /dev/led c <major> <minor>
# sudo chmod 666 /dev/led# 測試(用戶態程序)
gcc -o test_write test_write.c
./test_write# 卸載模塊
sudo rmmod led_cdev
dmesg | tail -n 20
7. 常見坑與調試清單(務必注意)
- 在 init 中立刻
gpio_free()
:會導致后續open/write
訪問 GPIO 時沒有權限或行為異常。應在模塊卸載(exit)中釋放 GPIO。 - 忘了設置
fops.owner = THIS_MODULE
:會導致模塊在被打開時引用計數不正確,可能阻止卸載。 copy_from_user
參數和長度:write()
中應嚴格制約拷貝長度,避免把用戶緩沖區全部拷入內核。device_create()
后沒有 /dev 節點:通常是因為udev
未運行或規則延遲。可用mknod
手動創建。- 資源回滾要嚴格:
init
中任一步失敗,都必須回退前面成功分配的資源,避免內核資源泄漏(GPIO、cdev、device/class、devno、kmalloc)。代碼示例中給出了清理路徑。 - GPIO 編號與設備樹不一致:生產驅動建議通過設備樹(DT)或 gpiod API 獲取 GPIO,避免魔法數字。
- 并發保護:若多個進程/CPU 同時寫 LED,視需求加鎖(spinlock / mutex)以保證狀態一致;但對簡單 LED 寫操作通常足夠輕量。
- LED 極性:板上 LED 有可能是低電平點亮(active-low),測試時注意電平與電路關系。
8. 總結與推薦
方法 | 接口 | 是否用 cdev | 是否支持多實例 | 適用場景 |
---|---|---|---|---|
方法一 | register_chrdev | 否 | 否 | 簡單實驗、快速驗證 |
方法二 | alloc_chrdev_region + cdev | 是 | 支持(擴展) | 正式驅動,推薦 |
方法三 | 多 minor + imajor/iminor | 是 | 是 | 一個驅動管多個硬件 |
方法四 | kfifo + 多 minor | 是 | 是 | 虛擬設備、IPC、練習 |
register_chrdev()
:實現極簡、適合單設備驗證,但不推薦用于生產驅動。alloc_chrdev_region()
+cdev
:規范、工程化,支持擴展,推薦作為首選實現方式。- 多實例(多個 minor) +
iminor()
:當一個驅動管理多個硬件實例(如多個 LED、多個傳感器)時,使用連續 minor 并在open()
中區分是最佳實踐。
實現驅動時,務必保證資源申請與釋放路徑對稱、避免在 init
中提前釋放硬件資源、并在 fops
中正確使用 THIS_MODULE
。生產環境下建議結合設備樹(或 platform data / of_device)來確定 GPIO 與設備號配置,減少魔法數字與板卡差異帶來的問題。
(完)