回顾:
linux内核字符设备驱动实现
1.linux内核设备驱动分类
字符设备:字节流,串口,LED,按键,蜂鸣器,ADC,声卡,显卡,LCD液晶屏,触摸屏,各类传感器,GPS,GPRS,蓝牙
块设备:512字节,硬盘,光盘,SD卡,TF卡,nandflash(SLC,MLC,TLC),emmc,U盘
网络设备:网卡,配合网络协议栈
2.设备文件
“一切皆文件”;
硬件设备在用户空间以设备文件的形式存在;
在/dev/目录下,设备文件存在于内存中;
用户访问设备就是通过访问设备文件来进行,同时要配合系统调用函数(open,close,read,write...);
mknod /dev/xxx c主设备号 次设备号 ,用于创建设备文件!
3.设备号
主设备号:应用程序根据主设备号能够找到自己对应的字符设备驱动。一个设备驱动只有唯一的一个主设备号。
次设备号:应用程序根据主设备号找到驱动以后,如果驱动管理多个同类的硬件设备,驱动再通过次设备号来分区具体操作哪个硬件设备个体!
数据类型:dev_t (unsigned int)
typedef unsigendint dev_t;
高12:主
低20:次
MAJOR
MINOR
MKDEV
设备号对于内核来说是一种宝贵的资源,所以驱动要使用某一个设备号的时候一定要先向内核去申请(类似申请内存一样),当然设备号驱动不再使用时,一定要归还给内核。
申请方法:
1.静态申请
register_chrdev_region(dev_t dev, int count,name);
2.动态申请
alloc_chrdev_region(dev_t *dev, 0, count, name);
4.字符设备涉及的4个重要数据结构
struct cdev:
描述字符设备;
重要字段:
dev_t dev; //存放设备号
int count; //存放设备的个数
struct file_operations *ops; //通过这个指针能够给字符设备附加一些操作硬件的方法
如何使用:
分配字符设备对象
struct cdevled_cdev;
初始化对象:dev,count,ops
cdev_init(分配字符设备对象指针,硬件操作方法的指针);
ops指针最终指向驱动分配初始化的操作集合led_fops;
注册字符设备对象到内核中
cdev_add(分配字符设备对象指针,设备号,设备个数);
其实就是将分配字符设备对象指针以设备号为索引添加到内核的cdev数组中;
字符设备驱动一旦注册成功,静静等待着用户来访问!
structfile_operations:
就是包含了一堆的函数指针,这些函数指针指向驱动的某个函数
,并且把这个结构体指针赋值给cdev对象的.ops.在通过cdev将这些硬件的方法给用户使用:
structfile_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open, //打开设备
.release = led_close, //关闭设备
.read = led_read, //读设备
.write = led_write //写设备
};
app:open->软中断->sys_open->led_open
app:close->软中断->sys_close->led_close
app:read->软中断->sys_read->led_read
app:write->软中断->sys_write->led_write
...
本质上这个结构体提供的方法是用户来使用!
structfile_operations不能直接注册到内核中,要通过cdev间接的注册到内核中!
struct inode:
用来描述一个文件(所有文件)的物理信息,文件存在,内核就会分配一个inode对象来描述这个文件的物理信息,文件一旦销毁,内核也会将对应的inode对象销毁!
每当用户mknod创建设备文件时,内核做了哪些事情?
1.内核分配inode对象
2.内核根据mknod命令指定的主设备号,次设备号,初始化inode对象的一个成员i_rdev(指定的设备号),驱动通过inode来获取设备号,从而获取次设备号,用于驱动来分区具体操作的哪个设备个体!
3.内核以设备号为索引,在内核的cdev数组中找到对应的字符设备对象led_cdev指针,然后将这个指针赋值给inode对象的一个成员i_cdev(inode和字符设备驱动进行关联)
struct file:
用来描述一个文件被成功打开以后的状态信息,文件被打开,内核就会创建一个file对象来描述文件被打开以后的状态,文件被关闭,内核也会销毁对应的file对象!
一个文件只有一个inode,但是可以有多个file;
应用程序访问设备永远先打开(open)设备:
1.app:in fd =open("/dev/myled");
2.C库的open函数实现,保存open的系统调用号到R7中,调用SVC触发软中断
3.CPU跳转到内核的异常向量表的入口地址
4.根据R7中的系统调用号,在系统调用表找对应的函数sys_open;
5.sys_open内核会做:
5.1 分配file对象
5.2 初始化file对象的一个成员f_op,通过已知的cdev对象,从这个对象的ops中获取硬件操作集合(&led_fops)指针,然后把这个指针赋值给f_op,这样file对象就有对应的硬件操作方法!其他的系统调函数至此再跟cdev对象没有任何关系,只跟file的f_op有关系。
5.3 并且将设备文件描述符fd和file对象进行关联!
5.4 判断file->f_op是否有open函数,如果有,调用,如果没有,给用户永远返回成功!
6.用户读设备:
1.app:read(fd, buf, size);
2.sys_read:
根据fd获取关联的file
file->f_op->read(...) =&led_fops->read = led_read.
**********************************************************
案例:在昨天的驱动代码中,在led_open和led_close函数中,通过inode指针来获取设备号,并打印出来!
提示:
static intled_open(struct inode *inode, struct file *file)
{
int major = MAJOR(inode->i_rdev);
int minor = MINOR(inode->i_rdev);
}
********************************************************
structfile_operations相关操作接口:
对设备的打开,关闭操作:
int(*open)(struct inode *inode, struct file *file);
int(*release)(struct inode *inode, struct file *file);
以上两个函数,底层驱动可以不用实现,如果不做实现,用户空间的open永远返回成功!
对设备的读写操作:
ssize_t(*read)(struct file *file, char __user *buf,
size_t count, loff_t *ppos);
ssize_t(*write)(struct file *file, char __user *buf,
size_t count, loff_t *ppos);
前者表示用户读设备
后者表示用户写设备
file:sys_open创建的file对象,对应用户空间read,write函数的第一个参数fd
buf:但凡内核用__user修饰的指针变量,那么这个指针变量指向的地址永远是用户空间(0x00000000~0xBFFFFFFF).所以这个buf指向用户空间的缓冲区(内存)。在内核空间不能直接访问操作这个buf(*buf = 1,这是错误的!),需要利用内核提供的内存拷贝函数来实现。buf是和用户空间read,write的第二个参数进行关联。
count:请求读写的字节数,和用户空间read,write的第三个参数关联。
ppos:记录上一次读写位置,如果想获取上一次的读写位置:
loff_t pos =*ppos;如果操作完毕以后,有必要更新位置:
*ppos = 新位置;
返回值:实际读写的字节数
使用注意事项:
1.如果用户调用read,write来读写设备,驱动程序必须给定read,write的函数实现;
2.read,write的第二个参数buf指向用户空间,不能在内核空间直接访问操作,必须利用内核提供的内存拷贝函数实现内核空间和用户空间的数据传递!
内核提供的内存拷贝函数:
注意:这些内存拷贝函数不代表读,写行为,仅仅代表数据流的走向!
内核缓冲区(源) -------->用户缓冲区(目标)
copy_to_user(void *to, void *from, unsigned long n)
to:目标,它是地址
from:源,它是地址
n:拷贝字节数
这个函数能够对任何数据类型进行拷贝,包括结构体!
put_user(data,ptr)
data:内核变量,而不是地址;不能是结构体;
ptr:用户缓冲区首地址,它是地址
这个函数在使用的时候,ptr的数据类型一定要和data的数据类型保持一致!例如:
int data = 100;//驱动定义的变量
int *p = (int*)buf; //把buf从char *转换成int *
put_user(data,p); //将内核的变量data拷贝到用户空间的buf中,拷贝4个字节
用户缓冲区(源) -------->内核缓冲区(目的)
copy_from_user(void *to, void *from, n)
to:目标,它是地址
from:源,它是地址
n:拷贝字节数
这个函数能够对任何数据类型进行拷贝,包括结构体!
get_user(data,ptr)
data:内核变量,而不是地址;不能是结构体;
ptr:用户缓冲区首地址,它是地址
这个函数在使用的时候,ptr的数据类型一定要和data的数据类型保持一致!例如:
int data; //驱动定义的变量
int *p = (int*)buf; //把buf从char *转换成int *
get_user(data,p); //从用户空间的buf中拷贝4个字节到内核的data变量中
案例:利用write来实现开关所有的灯,用户写1,开灯,用户写0,关灯,并且用户能够获取灯的状态!
分析:
read/write->structfile_operations->cdev->分配,初始化,注册->GPIO资源的申请->配置->释放
应用程序:
写设备
int fd;
int ucmd; //用户缓冲区
fd =open("/dev/myled", O_RDWR);
ucmd = 1; //向内核驱动写1
write(fd,&ucmd, sizeof(ucmd));
驱动:
led_write(file,buf, count, ppos);
参数对应关系:
fd->file
&ucmd->buf
sizeof(ucmd)->count
ppos->驱动维护操作的文件读写位置
应用程序:
读设备
int ustate;
read(fd,&ustate, sizeof(ustate)); //从驱动中获取数据,并将数据信息赋值给用户的ustate变量
驱动:
led_read(file,buf, count, ppos);
参数对应关系:
fd->file
&ustate->buf
sizeof(ustate)->count;
ppos->内核记录文件读位置
实验步骤:
PC:
make
arm-linux-gcc -oled_test led_test.c
cp led_drv.ko/opt/rootfs
cp led_test/opt/rootfs
ARM:
insmodled_drv.ko
cat/proc/devices //查看主设备号
mknod /dev/myledc 250 0
./led_test on
./led_test off
案例:升级驱动,能够让用户指定操作某一个灯的开关
./led_test on 1
./led_test on 2
./led_test off 1
./led_test off 2
分析:
之前的应用程序传递给驱动的操作数据信息只有一个(开关命令);如果要满足现在的要求,应用程序需要传递2个数据信息(一个是开关命令,一个是灯的编号)
struct led_cmd {
int cmd; //开关命令,1开,0关
int index;//灯编号,1,2
};
注意:创建设备文件名不能为/dev/led(官方LED驱动使用此设备文件)
*********************************************************
向设备发送命令接口ioctl
structfile_operations {
int (*ioctl)(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg);
};
应用程序ioctl系统调用函数说明:
头文件:#include
函数原型:
int ioctl(int d, int request, ...); //可变参函数
函数功能:能够利用此函数实现与设备的交互(读写)
参数说明:
d:设备文件描述符fd,与驱动的file关联,file再跟inode关联。
request:向设备发送的命令,命令最终会赋值给驱动ioctl的第三个参数cmd
...:还可以跟一个参数,这个参数一般指定为用户空间的缓冲区首地址,也就是所谓ioctl的第三个参数,这个参数是一个地址,它对应的就是驱动ioctl函数的第四个参数。
说明:linux内核用unsigned long这种数据类型是一定道理的,对于编译器来说,unsignedlong它的长度永远为4个字节,所以unsigned long变量可以存放任何值,包括地址!但是unsigned int 的数据类型的长度有可能为2个字节,也有可能是4个字节,一般默认是4个字节。
ioctl用户空间使用范例:
#define LED_ON 0x100001 //开灯命令
#define LED_OFF 0x100002 //关灯命令
ioctl用两个参数:
ioctl(fd,LED_ON); //向设备发送开灯命令
ioctl(fd,LED_OFF); //向设备发送关灯命令
ioctl用三个参数:
int index = 1;
ioctl(fd, LED_ON, &index); //向设备发送命令,并且传递给设备驱动一个用户空间缓冲区首地址(&index)
驱动ioctl函数:
app:ioctl->sys_ioctl->led_ioctl:
int(*ioctl)(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg);
函数功能:接收用户发来的命令,并且响应处理命令
参数:
inode:文件节点指针
file:文件指针
以上两个结构体指针跟用户空间的fd关联;
cmd:保存用户ioctl发来的命令,也对应用户空间ioctl的第二个参数。
arg:它的数据类型是unsigned long型,表明arg能够存放任何值,包括地址,所以arg存放ioctl发来的用户空间缓冲区的首地址,在驱动使用arg时,一定要进行数据类型的转换,并且驱动不能直接访问操作arg(类似read,write的buf),如果驱动要从arg用户缓冲区中获取数据或者写入数据,必须利用内核提供的4个内存拷贝函数!
驱动:
led_ioctl(...) {
int index;
copy_from_user(&index, (int *)arg, 4);
}
案例:利用ioctl实现灯的开关
应用程序ioctl(fd, ucmd, &index);
驱动ioctl(inode, file, kcmd, arg);
对应关系:
fd->inode,file
ucmd->kcmd
&index->arg:内核不能直接访问arg,因为arg存放的是用户缓冲区的首地址(&index),如果要对用户空间缓冲区进行访问,必须利用内核提供的4个内存拷贝函数。如果利用内存拷贝函数,要注意对arg进行数据类型的转换!
案例:利用write或者ioctl实现控制蜂鸣器!