Linux驱动
1 驱动分类
常规分类:字符设备、块设备、网络设备
字符设备:一种按字节来访问的设备,字符驱动负责驱动字符设备,这样的驱动通常实现open、close、read和write系统调用。如串口、LED、按键;
块设备:以块(一般为512字节)为最小传输单位的设备,块设备不能按字节处理数据。在Linux系统中运行块设备传输任意数目的字节。块设备与字符设备的区别是驱动与内核的接口不同。如硬盘、flash、SD卡。
网络设备:可以是一个硬件设备,如网卡;也可以是一个纯粹的软件设备,如回环接口(lo)。一个网络接口负责发送和接收数据报文。
总线分类:USB设备、PCI设备、平台总线设备
2 硬件访问
驱动程序要控制设备是通过设备内寄存器控制的。
硬件访问步骤:->地址映射->寄存器读写
在Linux系统中,无论是内核程序还是应用程序,都只能使用虚拟地址,而芯片手册中给出的寄存器地址或者RAM地址则是物理地址,无法直接使用。因此,读写寄存器的第1步就是将它的物理地址映射为虚拟地址。
地址映射包括动态映射和物理映射;
动态映射:在驱动程序中采用ioremap()函数将物理地址映射为虚拟地址:
函数原型: void *ioremap(physaddr,size)
physaddr:待映射的物理地址
size:映射的区域长度
返回值:映射后的虚拟地址
静态映射:根据用户事先指定的映射关系,在内核启动时,自动将物理地址映射为虚拟地址。
用户是通过map_desc结构体来指明物理地址与虚拟地址的映射关系。
struct map_desc{
unsigned long virtual; /* 映射后的虚拟地址 */
unsigned long pfn; /* 物理地址所在的页帧号 */
unsigned long length; /* 映射长度 */
unsigned int type; /* 映射的设备类型 */
};
pfn: 利用__phys_to_pfn(物理地址)可以计算出物理地址所在的物理页帧号
内核寄存器读写函数:
unsigned ioread8(void *addr0) unsigned ioread16(void *addr0) unsigned ioread32(void *addr0) unsigned readb(address) unsigned readw(address) unsigned readl(address) void iowrite8(u8 value,void *addr) void iowrite16(u16 value,void *addr) void iowrite32(u32 value,void *addr) void writeb(unsigned value,address) void writew(unsigned value,address) void writel(unsigned value,address)
3 字符设备文件
字符设备驱动程序是通过字符设备文件被用户调用。
通过字符设备文件,应用程序可以使用相应的字符设备驱动程序来控制字符设备。
应用程序首先通过文件名找到字符设备文件,假如要从设备中读出或者写入数据都是从字符设备文件展开的,字符设备文件是设备驱动程序和应用程序的一个媒介,应用程序对设备的操作是通过字符设备文件来完成的。
创建字符设备文件:
mknod命令
mknod /dev/文件名 c 主设备号 次设备号 //c表示char
例:mknod /dev/memdev0 c 253 0
字符设备文件与驱动程序通过主设备号建立起联系,字符设备文件对应一个主设备号,驱动程序对应一个主设备号,如果两个号相等说明两种之间是一一对应的关系。
当用户去操作设备的时候内核就会找到相应的驱动程序。
通过cat /proc/devices打印主设备号
次设备号0~255
ls /dev/memdev0 //查看
显示:/dev/memdev0
有了字符设备文件,也有了设备驱动程序,就要编写应用程序。就是通过字符设备文件访问设备驱动程序。
要访问硬件,其实就是访问硬件里的寄存器,假如定义一个数组,数组里头有5个整型的元素,每一个整型元素就可以模拟一个寄存器,要去操作硬件,最后可以变为操作数组,比如要把数据写入寄存器,实际上就变成往数组里头写入数据。通过驱动程序往数组里头写入数据。如果要从设备里头读出数据,通过驱动程序从数组里头读出数据。
4 字符设备驱动实例
驱动程序通常采用内核模块的程序结构来进行编码。因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块。
驱动程序文件包含:memdev.c、Makefile;应用程序文件包含:write_mem.c、read_mem.c;
1 通过命令make,编译驱动程序得到文件memdev.ko,并将其拷贝到nfs开发板挂载的目录,然后安装驱动程序
insmod memdev.ko 安装memdev.ko
lsmod 查看驱动
2 使用命令查看设备号:cat /proc/devices
3 创建字符设备文件:
mknod /dev/memdev0 c 253 0 //名字不能跟已有的重复
4 查看创建的设备字符文件
ls /dev/memdev0
显示: /dev/memdev0
5 编译应用程序write_mem.c
arm-linux-gcc write_mem.c -o write_mem
运行:./write_mem
报错:-/bin/sh: ./write_mem:not found (应用程序依赖的库找不到)
通过:arm-linux-readlef -d write_mem 查询应用程序所依赖的动态链接库
通过ls命令查看,开发板中没有这个库,所以报错;
解决办法:
1 直接将libc.so.6复制到开发板中
2 采样静态编译的方法
arm-linux-gcc -static write_mem.c -o write_mem
采用静态编译后运行:./write_mem
6 编译应用程序read_mem.c
arm-linux-gcc -static read_mem.c -o read_mem
运行:./read_mem
结果:dst is 2013
5 字符设备驱动程序模板
1 设备描述结构
在任何一种驱动模型中,设备都会用内核中的一种结构来描述。我们的符设备在内核中使用struct cdev来描述。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //设备操作集
struct list_head list;
dev_t dev; //设备号
unsigned int count; //设备数
};
查看设备号:ls –l dev下都是设备文件
eg:crw-r----- 1 root root 10, 223 12月 15:00.10 uinput
10:主设备号 223:次设备号
主设备号反映设备类型,次设备号区分同类型设备
设备号操作:
Linux内核中使用dev_t类型来定义设备号,dev_t这种类型其实质为32位的unsigned int,其中高12位为主设备号,低20位为次设备号.
问1:如果知道主设备号,次设备号,怎么组合成dev_t类型
答:dev_t dev = MKDEV(主设备号,次设备号)
问2: 如何从dev_t中分解出主设备号?
答: 主设备号 = MAJOR(dev_t dev)
问3: 如何从dev_t中分解出次设备号?
答: 次设备号=MINOR(dev_t dev)
设备号分配:静态申请和动态分配
静态申请:
开发者自己选择一个数字作为主设备号,然后通过函数register_chrdev_region向内核申请使用。缺点:如果申请使用的设备号已经被内核中的其他驱动使用了,则申请失败。
动态分配:
使用alloc_chrdev_region由内核分配一个可用的主设备号。优点:因为内核知道哪些号已经被使用了,所以不会导致分配到已经被使用的号。
设备号注销
不论使用何种方法分配设备号,都应该在驱动退出时,使用unregister_chrdev_region函数释放这些设备号。
2 操作函数集
Struct file_operations是一个函数指针的集合,定义能在设备上进行的操作。结构中的函数指针指向驱动中的函数, 这些函数实现一个针对设备的操作, 对于不支持的操作则设置函数指针为 NULL。例如:
https://blog.csdn.net/littlelee111/article/details/10133759
struct file_operations dev_fops ={
.llseek = NULL,
.read = dev_read,
.write = dev_write,
.ioctl = dev_ioctl,
.open = dev_open,
.release = dev_release,
};
3 字符设备初始化
分配cdev:可以采用静态和动态两种办法
静态分配:
struct cdev mdev;
动态分配:
struct cdev *pdev = cdev_alloc();
初始化cdev:
structcdev的初始化使用cdev_init函数来完成。
cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数:
cdev: 待初始化的cdev结构
fops: 设备对应的操作函数集
注册cdev:
注册使用cdev_add函数来完成。
cdev_add(structcdev *p, dev_t dev, unsigned count)
参数:
p: 待添加到内核的符设备结构
dev: 设备号
count: 该类设备的设备个数
4 设备操作原型
int (*open)(struct inode *, struct file *) 打开设备,响应open系统
int (*release)(struct inode *, struct file *);关闭设备,响应close系统调用
loff_t (*llseek)(struct file *, loff_t, int);重定位读写指针,响应lseek系统调用
ssize_t (*read)(struct file *,char __user *,size_t,loff_t *)从设备读取数据,响应read系统调用
ssize_t(*write)(struct file*,const char __user*,size_t,loff_t*)向设备写入数据,响应write系统调用
struct file
在Linux系统中,每一个打开的文件,在内核中都会关联一个struct file,它由内核在打开文件时创建, 在文件关闭后释放。
重要成员:
loff_t f_pos /*文件读写指针*/
struct file_operations *f_op /*该文件所对应的操作*/
struct inode
每一个存在于文件系统里面的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息。因此, 它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是却会关联一个inode 结构。
重要成员:
dev_t i_rdev:设备号
设备操作open
open设备方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:
标明次设备号
启动设备
设备操作release
release方法的作用正好与open相反。这个设备方法有时也称为close,它应该:关闭设备。
设备操作read
read设备方法通常完成2件事情:
从设备中读取数据(属于硬件访问类操作)
将读取到的数据返回给应用程序
ssize_t (*read) (struct file *filp, char __user *buff, size_t count, loff_t *offp)
参数分析:
filp:与符设备文件关联的file结构指针, 由内核创建。
buff : 从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。
count: 请求传输的数据量,由read系统调用提供该参数。
offp: 文件的读写位置,由内核从file结构中取出后,传递进来
buff参数是来源于用户空间的指针,这类指针都不能被内核代码直接引用,必须使用专门的函数
int copy_from_user(void *to, const void __user *from, int n)
int copy_to_user(void __user *to, const void*from, intn)
设备操作write
write设备方法通常完成2件事情:
从应用程序提供的地址中取出数据
将数据写入设备(属于硬件访问类操作)
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
5 驱动注销
当我们从内核中卸载驱动程序的时候,需要使用cdev_del函数来完成符设备的注销。