Linux 下的驱动程序,不论是字符设备还是paltfrom 虚拟总线设备,看起来很复杂,其实都是固定的一套流程,只要步骤整理出来,实际开发的时候,直接套用模板就可以。
Linux下字符设备的开发遵循一套固定的流程,1. 创建设备 (动态、静态创建) 2. 申请设备号 (主、次设备号) 3. 实现设备的操作方法(file_operations结构体中描述的方法) 4. 向内核中注册设备(将设备号、设备操作方法与设备结构体绑定注册到内核) 5. 在module_exit()函数中实现资源的释放,主要是设备号的释放和注册设备的卸载。6 . 加载驱动模块,创建设备文件 7. 编写应用程序,测试驱动程序。
首先Linux下的驱动程序是按照内核模块的方式进行编写,编译完后要使用Insmod 指令注册到内核中,驱动的入口函数是内核模块的加载函数,也就是再module_init()函数中实现驱动的加载,在module_exit()函数实现驱动的卸载。
下面 我们介绍内核字符设备驱动的编写流程。
1. 创建设备,即定义设备结构体,分为动态和静态两种创建方式。
头文件 #include <linux/cdev.h>
static struct cdev chrdev; //静态方式创建设备结构体
struct cdev *cdev_alloc(void); //动态方式创建设备结构体
2. 申请设备号,设备号分为主设备号和次设备号,主设备号用于唯一的确定设备的驱动程序,次设备号用于唯一的确定在同一类驱动程序下的某个设备。
在Linux下用dev_t 来描述设备号(dev_t 是32位数值类型,其中高12位表示主设备号,低20位表示次设备号)。
头文件 #include <linux/kdev.h>
MAJOR(dev_t dev); //根据设备号分离出主设备号
MAJOR(dev_t dev); //根据设备号分离出次设备号
MKDEV(int major,int minor); //根据主设备号和次设备号还原设备号。
静态申请设备号: 使用register_chrdev_region函数分配设备号,优点简单,缺点容易导致设备号冲突,且需要先查阅内核源码的Documentation/devices.txt文件,确定一个未被使用的主设备号,再进行设备号的注册。
动态申请设备号:使用 alloc_chrdev_region 函数分配设备号,优点简单,缺点无法在安装驱动前创建设备文件(此时不知道设备号),需要在驱动加载后,从/proc/devices中查询设备号,手动创建设备文件。
头文件: #include <linux/fs.h>
静态申请函数:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能: 申请使用从 from 开始的 count 个设备号(主设备号 不变,次设备号增加)
参数:
from:希望申请使用的设备号
count:希望申请使用设备号数目
name:设备名(体现在/proc/devices)
动态申请函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
功能:请求内核动态分配 count 个设备号,且次设备号从 baseminor开始。
参数:
dev:分配到的设备号
baseminor:起始次设备号
count:需要分配的设备号数目
name:设备名(体现在/proc/devices)
3.实现字符设备的操作方法,主要是填充file_operation结构体。
定义fire_operation结构体,并实现里面的操作方法。
static struct file_operations chr_dev_fops = {
.owner = THIS_MODULE,
.open = chr_dev_open, //一般用于设备的初始化
.release = chr_dev_release, //一般用于释放设备初始化使用的资源
.write = chr_dev_write, //向设备写入数据
.read = chr_dev_read, //从设备读出数据
.lseek = chr_dev_lseek, //修改文件当前的读写位置
.ioctl = chr_dev_ioctl, //用于设备控制指令
};
其中编写write 和read 方法是需要注意内存空间的转换,write 是将用户空间的数据写入内核空间,read 是将内核空间的数据读取到用户空间,linux 系统提供了一组内存空间转换函数。
头文件: #include <linux/fs.h>
int copy_from_user(void *to, const void __user *from, int n)
int copy_to_user(void __user *to, const void *from, int n)
参数: to 目标数据地址,from 源数据地址, n 转换字节长度
4. 向内核中注册设备,使用cdev_init 绑定设备操作方法和设备结构体,使用 cdev_add 绑定设备号和设备结构体并进行注册。
头文件: #include <linux/cdev.h>
设备结构体初始化:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数:
cdev: 待初始化的cdev结构
fops: 设备对应的操作函数集
设备结构体注册:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:
p: 待添加到内核的字符设备结构
dev: 设备号
count: 添加的设备个数
5. 在module_exit函数中实现设备卸载时资源的释放。
释放设备号资源:
头文件:#include <linux/fs.h>
void unregister_chrdev_region(dev_t from, unsigned count)
释放从from开始的count个设备号
卸载注册的设备:
头文件:#include <linux/cdev.h>
int cdev_del(struct cdev *p)
参数: p 要注销的字符设备结构
6. 加载驱动模块,创建设备文件。
编译驱动内核模块,加载驱动模块到内核:insmod chrdev.ko
进入 /proc/devices 目录下,查找设备对应的主设备号,执行 mknod /dev/chrdev c 248 0 创建设备文件。
7. 编写应用程序进行内核驱动的测试。
在Linux中一切设备皆文件,对文件的读写即是对设备的数据操作,我们可以使用Linux提供的操作接口open、close、read、write,也可以使用C标准库中的接口(推荐使用,可移植性好)fopen、fclose、fwrite、rfread对设备进行操作。