流程:1编写好驱动模块安装好 2建立对应的的驱动设备文件 3编写应用程序对驱动文件进行读写。
总体思想:将设备文件和驱动模块建立映射关系,应用层对设备文件进行文件操作,驱动程序在文件操作集中实现对应功能操作,实现对硬件寄存器的控制,整个驱动开发就完成了。
1.查询驱动程序的的主设备号:cat proc/devices
主设备号:安装好驱动模块后,将会生成一个主设备号,将驱动文件和驱动程序关联的ID,应用层管理设备就是对设备文件进行读写,而设备文件需要关联相应的设备驱动的主设备号,之后才能通过驱动程序进行正确的硬件操作。
2.创建字符设备文件
2.1 mknod /dev/文件名 c(指明字符设备) 主设备号 次设备号(非负数0-255)
次设备号:一个驱动程序可以对应多个设备文件,一个设备文件对应于一个硬件设备,说明同一个驱动程序可以同时控制多个硬件设备,在后面的设备描述结构中会有这个变量。
应用程序对某一个设备文件进行读写时,设备文件通过主设备号找到对应的驱动模块,然后通过次设备号找到对应的硬件设备。可能多个硬件设备都是由同一个驱动模块来控制的。
2.2 使用函数在驱动中创建
3.查询开发板应用程序所依赖的库:在pc端运行
arm-linux-readelf -d binaryname(二进制文件名)
当在开发板上运行程序提示找不到文件的时候,说明开发板没有依赖的库文件可能,那么要么解决方法可以是将对应的库下载下来push到开发板,或者将应用程序进行静态编译。
静态编译:在pc端查询应用程序的库文件在开发板上没有的时候使用静态
arm-linux-gcc -static filename -o binaryname
4.设备驱动模型分为:驱动模块初始化 实现设备操作 驱动注销 三步 对于驱动模块需要做的就是对设备描述符的成员变量通过内核函数进行注册和赋值的过程,因此需要需要实现一个驱动模块,先要了解这个驱动设备的类型,然后了解它对应的设备描述符,之后就是将硬件的物理地址映射成虚拟地址,通过函数进行虚拟地址的读写即可。
4.1.驱动模块初始化分为:
4.1.1 分配设备的描述结构:不同的设备驱动类型,其描述结构不同
4.1.2 初始化描述结构
4.1.3 注册设备描述结构
4.1.4 硬件初始化
5.字符设备对应的设备描述结构:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; // 设备操作集
struct list_head list;
dev_t dev; //主次设备号 高12 位为主 低20位为次
//通过ls -l /dev/ 查看设备驱动文件的主次设备号
unsigned int count; // 设备数表明当前驱动模块管理多少个硬件设备
};
6.设备号的管理:
申请设备号
静态申请:register_chrdev_region,自己选择一个数字注册,如果被注册过则失败。
动态申请:alloc_chrdev_region,系统内核分配一个设备号给第一个传入的参数dev_t *dev中。参数需要传递第一个次设备号的开始,以及次设备数量,还需要传入一个字符串用来作为设备名字,可以使用命令来查询注册时的设备号cat proc/devices 。
dev_t dev = MKDEV(主设备号,次设备号)
主设备号 = MAJOR(dev_t dev) 次设备号=MINOR(dev_t dev)
注销设备号 unregister_chrdev_region
7.设备操作集:
自己编写的的系统调度:应用对系统函数等进行系统调度时,先调用swi函数切换到内核空间,然后去关联内核函数,关联的方式就是在swi软中断函数调用时传入一个数字编码,此数字编码在内核中一个结构体数组中注册了一个内核函数。
应用在使用read等系统调用的时候,系统会调用系统指令用svc切换到内核空间,然后去调用统一的接口函数,在接口函数中寻找到read对应的函数编码no,在依据函数编码去查表,找到sys_read,之后在syc_read中根据传入的文件描述符找到对用的struct file,调用struct file中含有的设备操作集结构体中的read函数,如果是自己创建的设备文件,则会去调用自己实现的驱动模块中的read函数。否则调用系统实现的对于文件内容block的读写函数。struct file在后面会讲解。
8. 驱动初始化(module_init):
8.1 分配cdev: 静态分配:定义一个cdev结构体
动态奉陪:定义一个指针,调用函数struct cdev *pdev=cdev_alloc();
8.2 初始化cdev: cdev_init(struct cdev *pdev,const struct file_operations *ops);
将实现的操作集ops和cdev中的操作集关联起来。
8.3 字符设备注册:cdev_add(struct cdev *pdev,dev_t *dev,unsigned int count);
将申请好的设备号和设备数量与待注册到内核的设备结构体关联。
注销设备:cdev_del(&pdev)
9.实现设备方法:将硬件操作替换成读写驱动中缓冲区(数组变量)的内容
在打开文件时,系统自动关联文件操作结构体struct file,里面记录文件当前指针以及该文件对应的操作函数集(普通文件进行普通的系统调度,设备文件调用对应的驱动里面的设备操作集)等,关闭时自动释放。
一个文件在在创建时,建立一个inode号,里面有设备号dev_t i_rdev。
Open函数需要做的将inode的次设备号读取出来并判断,以此来将一些唯一标识(区分同一驱动不同设备文件)保存到struct file中的一个内核提供给用户的变量的中去,例如来将对应硬件寄存器的基地址保存到每个文件在打开是创建的对应的struct file中变量private_data去。
Read/write需要做是利用函数copy_to_user/copy_from_user在内核中地址读与用户提供的地址间进行通信,内核地址与用户地址是不能直接进行内存拷贝与赋值的。例如,read:利用copy_to_user函数将struct file中变量private_data保存的基地址加上read传入的偏移量所形成的新的地址中的内容拷贝到read传入的用户空间的的地址中去,内容的大小为read函数传入的变量size的大小,就完成了用户读设备文件的操作的。Write同理。
Lseek就是将struct file中变量f_pos文件指针的值改变为基地址加上lseek传入的位置参数的值。
Close自己写一些想写的操作即可,struct file在文件关闭时,系统自动释放这个变量会。
10.设备控制:
应用层使用 int ioctl(int fd,unsigned long cmd,...)第三个参数取决于第二个参数,看看是否需要传入。
内核层使用响应函数:long (*unlocked_ioctl)(struct file *filp,unsigned int cmd,unsigned long arg ),内核版本在2.6.36之前使用另外一个
然后在自己写的驱动代码函数unlocked_ioctl中使用switch来判断参数cmd,依次来进行进行对硬件的寄存器进行操作实现对硬件模块的控制。当时当没有任何一个匹配时,需要返回-EINVAL。
cmd这个整型参数按功能被划分成几个区段
Type(类型/幻数): 表明这是属于哪个设备的命令。八位
Number(序号),用来区分同一设备的不同命令
Direction:参数传送的方向,可能的值是 _IOC_NONE(没有
数据传输), _IOC_READ, _IOC_WRITE(向设备写入参数)
Size: 参数长度,第三个参数的长度
cmd被Linux系统定义了一些宏来完成帮助定义cmd,不需要自己去拼接,在应用层和驱动层都可以识别出这些宏。应用层调用ioctl函数时通过参数cmd传入这些宏,在驱动层函数unlocked_ioctl中使用switch来判别cmd,之后进行相应的硬件寄存器设置。这些宏有以下,其中type为类型,nr为序号,datatype为第三个参数的数据类型。至于参数传递的方向,这个宏本身就可以区分
_IO(type,nr):不带参数的命令
_IOR(type,nr,datatype):从设备中读参数的命令
_IOW(type,nr,datatype):向设备写入参数的命令
可以给上面这些宏来取一个别名,会更加方便来进行使用。实例如下:
#define MEM_MAGIC ‘m’ //定义幻数,字符m就是一个字符,八个位
#define MEM_SET _IOW(MEM_MAGIC, 0, int)
11.对硬件操作:首先将通过ioremap函数将物理地址转化为虚拟地址,然后通过函数writel往得到的虚拟地址写值。
unsigned int * ioremap(物理地址数值,机器字长) 将物理地址的数值按照机器字长转换为虚拟地址返回。
Writel(写入数值,虚拟地址) 将数值写入虚拟地址,也就是写入物理地址即硬件对应的寄存器。
12.驱动注销(module_exit):cdev_del(&pdev); /*注销设备*/
unregister_chrdev_region(dev_t dev, 2); /*释放设备号*/