一、核心概念
1.设备号:Linux中每个设备文件(例如/dev/xxx)都对应一个设备号,格式是主设备号 + 次设备号:
1)主设备号:驱动程序的 "唯一标识",内核通过主设备号,直接找到该
设备对应的驱动程序(file_operations 结构体);
2)次设备号:同一驱动下的 "设备编号",区分同一驱动管理的多个设备。
2.file_operations结构体:驱动程序和Linux内核虚拟文件系统(VFS)的 “接口协议”,用户空间调用open/read/write时,内核会通过这个结构体找到驱动里对应的函数;
3.设备节点:也叫设备文件,路径通常是/dev/xxx,是Linux为了让用户空间程序能通过标准文件操作(open/read/write/close)访问硬件设备而设计的特殊文件,它不存储任何数据,只在文件系统的索引节点(inode)中记录了设备类型(字符/块)和对应的主/次设备号,是用户空间与内核驱动、硬件之间的 “桥梁”;
二、设备号的分配方式
1.静态分配(register_chrdev_region)
用法:手动指定主设备号 + 次设备号范围;
dev_t devno = MKDEV(237, 0); // 组合主设备号237、次设备号0
register_chrdev_region(devno, 3, "mychardev"); // 申请次0~2,共3个设备
2.动态分配(alloc_chrdev_region)
alloc_chrdev_region(&devno, 0, 3, "mychardev"); // 次0~2,主设备号由内核分配
//分配后可拆分主/次设备号
int major = MAJOR(devno); // 获取主设备号
int minor = MINOR(devno); // 获取次设备号
三、测试
1.源代码
mychardrv.c
#include<linux/module.h>#include<linux/kernel.h>#include<linux/fs.h>#include<linux/cdev.h>#include<linux/uaccess.h>#include<linux/slab.h>#define DEVICE_NAME "mychardev"// 设备名称#define DEVICE_SIZE 1024 // 设备虚拟内存大小static struct cdev my_cdev;static dev_t dev_num;static char *device_memory;// 打开设备staticintmy_open(struct inode *inode, struct file *filp){ printk(KERN_INFO "Device opened\n"); filp->private_data = device_memory; return 0;}// 释放设备staticintmy_release(struct inode *inode, struct file *filp){ printk(KERN_INFO "Device released\n"); return 0;}// 从设备读取数据staticssize_tmy_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){ size_t bytes_read = 0; char *data = filp->private_data; if (count > DEVICE_SIZE - *f_pos) { count = DEVICE_SIZE - *f_pos; } if (copy_to_user(buf, data + *f_pos, count)) { return -EFAULT; } bytes_read = count; *f_pos += bytes_read; return bytes_read;}// 向设备写入数据staticssize_tmy_write(struct file *filp, constchar __user *buf, size_t count, loff_t *f_pos){ size_t bytes_written = 0; char *data = filp->private_data; if (count > DEVICE_SIZE - *f_pos) { count = DEVICE_SIZE - *f_pos; } if (copy_from_user(data + *f_pos, buf, count)) { return -EFAULT; } bytes_written = count; *f_pos += bytes_written; return bytes_written;}// 文件操作结构体static struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .read = my_read, .write = my_write,};// 驱动模块初始化函数staticint __init my_driver_init(void){ int ret; // 分配设备号 动态分配 1 个字符设备编号,次设备号从 0 开始 ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { printk(KERN_ERR "Failed to allocate device number\n"); return ret; } printk(KERN_INFO "Allocated character device region: major=%d, minor=%d\n", MAJOR(dev_num), MINOR(dev_num)); // 初始化字符设备 cdev_init(&my_cdev, &my_fops); my_cdev.owner = THIS_MODULE; // 添加字符设备到系统 ret = cdev_add(&my_cdev, dev_num, 1); if (ret < 0) { printk(KERN_ERR "Failed to add cdev\n"); unregister_chrdev_region(dev_num, 1); return ret; } // 分配设备内存 device_memory = kmalloc(DEVICE_SIZE, GFP_KERNEL); if (!device_memory) { printk(KERN_ERR "Failed to allocate device memory\n"); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); return -ENOMEM; } printk(KERN_INFO "My character device driver loaded successfully\n"); return 0;}// 驱动模块退出函数staticvoid __exit my_driver_exit(void){ kfree(device_memory); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO "My character device driver unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_AUTHOR("axun");MODULE_DESCRIPTION("A simple character device driver");MODULE_LICENSE("GPL");
2.Mafile及编译加载驱动模块参考第一个驱动程序文章
3.创建设备节点
0)sudo -i #进入root用户
1)cat /proc/devices | grep mychardev #查看主设备号
2)mknod /dev/mychardev c 240 0 #通过设备号手动创建设备节点(后面会讲怎么通过在程序中自动创建设备节点)
3)ls /dev/mychardev -l #查看创建的设备节点
4)rm /dev/mychardev #手动删除设备节点(后面会讲怎么通过在程序中自动删除设备节点)
4.测试驱动程序
1)shell程序调用驱动程序
调用流程:用户命令→系统调用→内核处理→驱动执行
echo "hello" > /dev/mychardev #将字符串"hello"写入驱动程序中的缓存区
cat /dev/mychardev #读取驱动程序中缓存区数据并显示
2)用户自定义程序调用驱动程序
调用流程:用户程序→系统调用→内核处理→驱动执行
3)这两种方式本质上都是通过系统调用接口去调用驱动程序的。