驱动开发硬核特训 · Day 19:从字符设备出发,掌握 Linux 驱动的实战路径(含 gpio-leds 控制示例)
视频教程请关注 B 站:“嵌入式 Jerry”
一、背景说明:字符设备驱动的角色定位
在 Linux 内核驱动体系中,**字符设备驱动(Character Device Driver)**扮演着关键的桥梁作用,它直接向用户空间程序提供 read/write/ioctl
等接口,适用于键盘、LED、GPIO、串口等多类设备。而在日常的嵌入式开发中,我们往往会面对一个核心问题:
“设备树已经定义了硬件设备,子系统(比如
gpio-leds
)也帮我点亮了 LED,为何还要写字符设备驱动?”
本篇博文将围绕这个问题,逐步展开,从设备模型 → 子系统 → 字符设备驱动之间的逻辑关系梳理,再深入实践,完成一个基于 i.MX8MP 平台的 LED 控制字符设备驱动,掌握字符设备的创建、注册与用户空间控制。
二、核心概念回顾:设备模型、子系统与字符设备的关系
2.1 设备模型(Device Model)
Linux 内核的设备模型为设备、驱动和总线之间的关系建立了统一的框架:
struct device
:表示一个实际设备;struct device_driver
:对应驱动;struct bus_type
:设备与驱动的桥梁。
所有设备都要在 device model
下注册,但设备模型 本身不提供功能接口,它只建立了设备的逻辑与生命周期。
2.2 子系统(Subsystem)
子系统是在设备模型基础上的功能分类,如:
- input 子系统:键盘、触摸屏;
- block 子系统:磁盘;
- tty 子系统:串口;
- gpio-leds 子系统:LED 控制。
子系统的出现解放了开发者,某些简单设备无需写驱动,只要配置设备树即可,例如 gpio-leds
自动帮你注册 LED class device
,并在 /sys/class/leds
下生成节点,支持用户空间直接操作。
但问题来了:
如果我们需要通过
write()
等方式直接从应用控制 LED 呢?
这就要用到:
2.3 字符设备驱动(Character Device Driver)
字符设备驱动负责建立 /dev/mydev
这样的接口,实现 file_operations
结构体,从而让用户空间通过 open/read/write 与内核驱动打交道。它是最基础、最灵活的驱动方式。
三、问题提出:gpio-leds 为何无法直接与字符设备结合?
以设备树片段为例(位于 imx8mp-evk.dts
):
gpio-leds {compatible = "gpio-leds";pinctrl-names = "default";pinctrl-0 = <&pinctrl_gpio_led>;status {label = "yellow:status";gpios = <&gpio3 16 GPIO_ACTIVE_HIGH>;default-state = "on";};
};
系统自动生成 /sys/class/leds/yellow:status/brightness
节点,我们可:
echo 1 > /sys/class/leds/yellow:status/brightness # 点亮
echo 0 > /sys/class/leds/yellow:status/brightness # 熄灭
但此类节点仅通过 sysfs 接口工作,并未与 /dev/ledchar 之类的字符设备产生映射关系。
四、实战:为 gpio-leds 控制引入字符设备接口
我们以 gpio3_16
(GPIO 编号为 32 * 2 + 16 = 80
)为例,构造一个最小的字符设备驱动模块。
4.1 驱动代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>#define GPIO_NUM 80 // 对应 gpio3_16
static dev_t devt;
static struct cdev cdev;
static struct class *led_class;static ssize_t led_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{char kbuf[4];if (copy_from_user(kbuf, buf, len))return -EFAULT;gpio_set_value(GPIO_NUM, kbuf[0] == '1' ? 1 : 0);return len;
}static struct file_operations led_fops = {.owner = THIS_MODULE,.write = led_write,
};static int __init led_dev_init(void)
{gpio_request(GPIO_NUM, "led_gpio");gpio_direction_output(GPIO_NUM, 1);alloc_chrdev_region(&devt, 0, 1, "led_char");cdev_init(&cdev, &led_fops);cdev_add(&cdev, devt, 1);led_class = class_create(THIS_MODULE, "led_class");device_create(led_class, NULL, devt, NULL, "ledchar");pr_info("ledchar device initialized\n");return 0;
}static void __exit led_dev_exit(void)
{gpio_set_value(GPIO_NUM, 0);gpio_free(GPIO_NUM);device_destroy(led_class, devt);class_destroy(led_class);cdev_del(&cdev);unregister_chrdev_region(devt, 1);
}module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
4.2 编译与测试
编译为模块:
obj-m += ledchar.o
加载模块:
insmod ledchar.ko
用户空间控制:
echo 1 > /dev/ledchar # 点亮
echo 0 > /dev/ledchar # 熄灭
五、总结对比:sysfs 与字符设备的协作与区别
方式 | 适用场景 | 优势 | 劣势 |
---|---|---|---|
sysfs接口 | 自动生成的 class 设备 | 无需写驱动,通用 | 接口单一,不支持复杂逻辑 |
字符设备 | 精细控制 / 用户接口 | 可定义多操作 / 定制协议 | 需自行注册,开发成本略高 |
两者结合 | 桥接通用与专用 | 可在 sysfs 生成节点 + 自定义 | 更强大但设计复杂 |
在一些复杂场景(如 SPI 设备、定制协议、UI 控制等)中,字符设备几乎不可替代。
六、延伸思考:如何从子系统向字符设备扩展?
未来开发中,建议按如下路径深入:
- 明确内核已支持的子系统是否能满足需求;
- 通过 class_create + cdev_add 绑定字符设备接口;
- 使用
udev
自定义/dev
节点自动创建设备文件; - 编写自定义
ioctl
进行配置交互(可参考 camera/v4l2)。
七、写在最后
本节通过一个实战示例,深入讲解了:
- 设备树中的 gpio-leds 与字符设备的区别;
- 如何构造字符设备驱动访问 GPIO;
- sysfs 与
/dev
的本质差异。
我们将在下一节中进一步引入 ioctl
控制方法,让字符设备驱动更具交互能力。
每日一句:不要轻视字符设备,它是所有复杂驱动的起点。
视频教程请关注 B 站:“嵌入式 Jerry”