linux实现设备驱动-字符型设备驱动和杂项设备驱动
linux驱动分类
(1)字符设备驱动 (Character Device Driver):
- 特点: 数据按字节流 (byte stream) 形式访问,即数据访问有严格的顺序性。
- 数据传输为字节流形式,顺序读写,不带缓存,无固定块大小。
- 通常采用中断驱动或轮询方式处理 I/O 请求,响应实时性较强。
- 设备节点通常位于 /dev 目录下,通过文件描述符进行操作。
- 示例: LED灯、按键、串口(UART)等,输入输出设备(鼠标、键盘、屏幕、摄像头)
- 管理方式: 通过设备号 (Device Number) 管理。
(2)块设备驱动 (Block Device Driver):
- 特点: 数据以固定大小的块 (block)(大小为512k) 为单位进行访问,带缓存,通常用于存储设备。
- 示例: 硬盘、SSD、U盘等。访问时可以随机读取任意位置的数据,不受顺序限制。
- 管理方式: 通过设备号 (Device Number) 管理。
(3)网络设备驱动 (Network Device Driver):
- 特点: 集成复杂的协议栈 (Protocol Stack),如TCP/IP、UDP等。
- 示例: 网卡。
- 管理方式: 按照名字 (Name) 管理,如
ens33。
一、字符型设备驱动:
1.字符型设备驱动不自动生成设备节点(‘myled)
- 手动创建设备节点 :(在开发板中创建)
①前提条件:开发板已运行 Linux 内核,且需知道目标设备的主设备号和次设备号(可通过内核日志dmesg或驱动代码获取)。
②

③验证与权限设置:

- 自动生成设备节点 class_create() device_create()
在init函数中调用class_create和device_create接口,结合sysfs自动生成设备节点。

2.设备号 (Device Number):设备号是内核用于管理驱动设备的一个32位无符号整数 (u32)。它被划分为两部分:
- 主设备号 (Major Number): 高12位,用于区分设备类型。例如,所有LED灯使用同一个主设备号,所有按键使用另一个主设备号。
- 次设备号 (Minor Number): 低20位,用于区分同一类型设备中的不同个体。例如,4个LED灯分别使用不同的次设备号(0, 1, 2, 3)。
(1)嵌入式 Linux 设备驱动调用流程示意图分析:

1.用户应用层(user app)
- 应用程序通过
open(led)或fopen(led)发起设备操作请求,依赖libc库将请求转换为系统调用。
2. 根文件系统层(rootfs)
- 设备节点(如
led_1、key_2、uart_3)存在于 /dev 目录,是用户层与驱动层的交互入口。
3. 系统调用与驱动管理层
- 系统调用
syscall_open(led)将用户请求传入内核,内核通过设备号匹配对应的驱动(如 LED 驱动匹配设备号 1)。 - 内核驱动管理模块(含
open操作与驱动索引 “1、2、3”)负责分发请求到具体驱动。
4. 设备驱动层
- 包含
LED_DRV、KEY_DRV、UART_DRV三类字符设备驱动,是硬件操作的直接实现层,完成如 LED 点亮、按键检测、串口数据收发等具体功能。
5. 硬件设备层
- 对应实际硬件(LED、KEY、UART),驱动通过硬件寄存器、中断等机制完成物理层操作。
(2)字符型设备驱动开发流程:
将驱动程序写在内核顶层目录/driver/char目录下:
1.Linux 内核模块的 “标准注册接口”:
确保驱动能被内核正确加载和卸载,是字符设备驱动(如示例中的 LED 驱动)模块化管理的关键实现。


2.确定设备号,设备名称:
为驱动程序分配一个唯一的设备号。通常,主设备号从较大的数字开始分配(如255),因为较小的号码已被系统占用。
// 定义主设备号、次设备号和设备名称
#define MAJOR_NUM 248 // 主设备号
#define MINOR_NUM 0 // 次设备号
#define DEV_NAME "led1" // 设备名称
3.定义并实现操作方法 (file_operations结构体)
定义一个 struct file_operations 结构体(驱动层操作函数集合),其中包含指向驱动程序具体操作函数的指针,如 open, read, write, release (close)。
// 声明操作函数原型
static int open(struct inode *node, struct file *file);
static ssize_t read(struct file *file, char __user *buf, size_t len, loff_t *loff);
static ssize_t write(struct file *file, const char __user *buf, size_t len, loff_t *loff);
static int close(struct inode *node, struct file *file);// 定义并初始化 file_operations 结构体
static struct file_operations fops = {.owner = THIS_MODULE, // 指向当前模块,用于防止模块在被引用时被卸载.open = open, // 绑定 open 函数.read = read, // 绑定 read 函数.write = write, // 绑定 write 函数.release = close // 绑定 release (close) 函数
};
4. 定义一个cdev结构体:
描述字符型设备相关的信息(设备号、设备名称...),绑定设备号与操作方法
static dev_t devno; // 设备号变量
static struct cdev cdev; // cdev 结构体实例// 在模块初始化函数中执行绑定
static int __init led1_init(void)
{int ret = 0;// 1. 使用 MKDEV 宏组合主、次设备号devno = MKDEV(MAJOR_NUM, MINOR_NUM);// 2. 初始化 cdev 结构体,关联操作方法cdev_init(&cdev, &fops);// 3. 将 cdev 添加到内核中,指定设备号和数量ret = cdev_add(&cdev, devno, 1);if(ret < 0)goto err_cdev_add; // 错误处理...
}
5.实现对应init函数(向内核注册驱动入口函数 insmod xxx.ko)
①注册字符型设备
- 静态注册:register_chrdev_region(使用预先定义的
MAJOR_NUM和MINOR_NUM向内核申请设备号范围) - 动态注册:alloc_chrdev_region(如果静态省请未成功,系统自动分配一个未被占用的设备号)

②初始化cdev结构体:



6.驱动层实现硬件交互逻辑在read/write等接口中,通过寄存器操作、中断处理或 DMA 完成硬件数据读写。



7.注销与清理:实现对应exit函数(驱动注销函数 rmmod xxx)
①注销字符型设备:unregister_chrdev_region();
②注销cdev结构体 :cdev_del();
③删除类和设备节点 class_destroy() ;device_destroy()
④释放硬件资源 :iounmap();

8.应用程序调用流程
用户程序通过标准库函数(如 open, read, write)发起系统调用,最终由内核调用驱动程序中对应的函数。
// 用户空间应用程序 (led1_app.c)
int main(int argc, const char *argv[])
{int fd = open("/dev/led1", O_RDWR); // 调用 open 系统调用if(fd < 0) { /* 错误处理 */ }write(fd, "ledon", 5); // 调用 write 系统调用write(fd, "ledoff", 6); // 调用 write 系统调用close(fd); // 调用 close 系统调用return 0;
}
(3)最后通过动态编译的方法将新的驱动模块加载到开发板中

二、杂项设备驱动:
特点:
- 无需手动分配主设备号(固定使用主设备号
10),只需指定次设备号和设备名称。 - 自动生成设备节点
- 开发高效:内核通过封装了杂项设备的
file_operations默认实现,开发者只需关注具体硬件操作逻辑,减少代码量。 - 适合快速开发简单字符设备(如 LED、按键等)。
(1)杂项设备驱动开发流程:
1.定义杂项设备结构体:
初始化 struct miscdevice,指定设备名称(name)、次设备号(minor,可设为 MISC_DYNAMIC_MINOR 动态分配)、操作接口(fops 关联 file_operations)。
static struct miscdevice my_miscdev = {.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号.name = "my_misc", // 设备节点名 /dev/my_misc.fops = &my_fops, // 关联操作函数集
};2.实现 file_operations 操作集:
封装设备核心操作(open/read/write/release 等),与普通字符设备驱动逻辑一致。
static struct file_operations my_fops = {.owner = THIS_MODULE,.open = my_open,.read = my_read,.write = my_write,
};3.模块初始化与注册在 module_init 函数中调用 misc_init 注册函数:
①注册杂项设备:misc_register();
static int __init my_misc_init(void) {return misc_register(&my_miscdev); // 注册成功自动创建设备节点
}
module_init(my_misc_init);②硬件资源初始化 :request_irq();
4.模块退出与注销:在 module_exit 函数中调用 misc_exit释放资源:
static void __exit my_misc_exit(void) {misc_deregister(&my_miscdev);
}
module_exit(my_misc_exit);5.完成open read write release函数
后续操作同字符型驱动
(3)字符型设备驱动和杂项设备驱动的区别
| 对比维度 | 字符型设备驱动 | 杂项设备驱动 |
|---|---|---|
| 设备号管理 | 需手动分配主设备号(静态register_chrdev_region或动态alloc_chrdev_region),次设备号按需定义 | 主设备号固定为 10,次设备号可动态分配(MISC_DYNAMIC_MINOR)或手动指定 |
| 注册接口 | 通过cdev_init+cdev_add注册,需手动关联file_operations | 通过misc_register注册,内核封装部分逻辑,只需初始化struct miscdevice |
| 开发复杂度 | 较高,需处理设备号、类注册、节点创建等全流程 | 较低,内核简化注册流程,自动创建设备节点 |
| 适用场景 | 复杂字符设备(串口、音频、传感器等,需精细设备号管理) | 简单字符设备(LED、按键、小型传感器等,追求开发效率) |
| 设备节点创建 | 需手动mknod或通过class_create+device_create自动创建 | 注册后内核自动在/dev目录创建设备节点 |
| 灵活性 | 可完全自定义设备号和操作逻辑,灵活性高 | 受限于主设备号 10,功能相对单一 |
编写驱动→配置 Makefile,Kconfig→动态编译生成.ko→加载测试→(可选)集成到内核。
