驱动开发硬核特训 · Day 18:深入理解字符设备驱动与子系统的协作机制(以 i.MX8MP 为例)
日期:2025年04月23日
回顾:2025年04月22日(Day 17:Linux 中的子系统概念与注册机制)
本日主题:字符设备驱动 × 子系统协作机制剖析
学习目标:理解字符设备的注册原理,掌握其与子系统间的接口关系与工作流,结合实际代码实现。
一、前言:设备模型之后,我们为什么学习字符设备驱动?
在前面的内容中,我们系统掌握了设备模型的构成和原理,包括 bus
、device
、driver
之间的匹配与生命周期关系。这一模型为 Linux 驱动提供了统一的管理方式。但这只是驱动“框架”的一环,驱动真正提供“功能”的部分往往以“字符设备”的形式呈现。
尤其是在嵌入式平台如 NXP i.MX8MP EVK 上,GPIO、I2C、PWM、摄像头等模块的访问大多通过 /dev/
下的字符设备进行,而这些字符设备的背后通常归属于某个子系统(例如:input、sound、misc、video、tty 等)。因此,今天我们要解决的是:
- 字符设备到底是什么?
- 它如何注册?如何与用户态通信?
- 它和设备模型有何关系?
- 子系统在其中扮演了什么角色?
二、字符设备驱动基础知识回顾
2.1 什么是字符设备?
字符设备(Character Device)是一种一次读取/写入一个字符流的设备接口,相对于块设备(如磁盘)而言,它没有缓冲区机制、没有对齐限制。常见的字符设备包括:
- 串口
/dev/ttyS*
- GPIO
/dev/gpiochip*
- I2C
/dev/i2c-*
- 自定义控制器设备(如
/dev/mypwm
)
它们通常通过 file_operations
结构体实现系统调用的处理,如 read
、write
、ioctl
等。
2.2 注册字符设备的方法(核心 API)
字符设备的注册过程大致分为以下几步:
-
申请设备号:
alloc_chrdev_region(&devt, 0, 1, "demo_char");
-
初始化 cdev 并添加到系统:
cdev_init(&cdev, &fops); cdev_add(&cdev, devt, 1);
-
创建设备节点:
class_create(); device_create();
-
注销字符设备(卸载时):
device_destroy(); class_destroy(); unregister_chrdev_region();
三、子系统:字符设备驱动背后的组织者
在 Linux 中,并非所有字符设备都以“裸 API”的方式注册,大量的字符设备实际上是依附于子系统框架进行注册的。
3.1 子系统为何存在?
子系统(subsystem)是为了解决如下问题而出现的:
- 提供统一的设备类与行为抽象(如 input, tty, misc)
- 简化字符设备注册流程(抽象 class、cdev、major/minor)
- 提供统一的 ioctl、poll、open 行为(如 video4linux 的 V4L2)
举例:
子系统 | 设备名 | 注册方式 |
---|---|---|
input | /dev/input/event* | input_register_device() |
misc | /dev/misc/* | misc_register() |
tty | /dev/ttyS* | tty_register_driver() |
video | /dev/video* | video_register_device() |
这些子系统往往会帮你封装掉字符设备注册流程,你只需关注“实现业务逻辑”。
3.2 子系统与字符设备的关系图
用户空间 内核空间
---------- -----------------------------------/dev/video0 ---> V4L2子系统 ----> 注册字符设备↑│platform_device / i2c_device / ...
四、实战:以 i.MX8MP 上的 LED 控制器为例讲解字符设备注册
4.1 示例背景
我们以 gpio-leds
中的一个 LED 为例,演示如何构造一个字符设备用于控制其亮灭。
设备树片段(位于 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";};
};
4.2 简单字符设备驱动代码
#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 = 32 * 2 + 16 = 80
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 init done\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");
编译为模块,加载后可直接:
echo 1 > /dev/ledchar # 点亮
echo 0 > /dev/ledchar # 熄灭
五、为什么不能只靠设备模型或子系统?
设备模型的本质是“管理对象关系”,子系统的作用是“组织类行为”,但真正实现具体功能的,还得靠字符设备驱动这一“落地接口”。
我们可以这样理解它们三者的关系:
模块 | 角色 |
---|---|
设备模型 | 提供注册匹配机制(device/driver) |
子系统 | 封装字符设备注册流程,组织行为一致性 |
字符设备驱动 | 实现具体功能,操作底层硬件 |
六、总结
今日你学习了:
- 字符设备的基本概念与注册流程
- 子系统对字符设备的封装与简化作用
- 实际示例中字符设备如何与 GPIO 结合
- 三大模块(设备模型、子系统、字符设备驱动)间的工作边界与协作逻辑
字符设备是 Linux 驱动开发中最贴近用户层的部分,掌握这一点将为后续如 input 设备、V4L2 摄像头、tty 驱动打下基础。
七、问答环节(回顾与思考)
cdev_add()
与device_create()
有什么区别?- misc_register 和标准字符设备注册方法有什么异同?
- 为什么有的驱动不需要显式注册字符设备?
- 子系统能否单独工作,不借助设备模型?
视频教程请关注 B 站:“嵌入式 Jerry”
明日预告(Day 19):misc_register
机制与子系统封装详解 —— 带你剖析为什么字符设备能“一行搞定”。
如果你觉得今天的训练内容对你有帮助,欢迎留言交流,也欢迎转发给其他 Linux 驱动学习者。