RK3399内核驱动实战:获取设备号控制LED的四种方法(由浅入深、代码注释详尽)
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 与设备号配置,减少魔法数字与板卡差异带来的问题。
(完)