Linux设备驱动种类及层次
Linux设备驱动的种类:字符设备驱动,块设备驱动,网络设备驱动
字符设备驱动:按照字节流来访问,只能顺序访问,不能无序访问的设备。
块设备驱动:按照block(512字节)来访问,可以顺序访问,也可以无序访问的设备
网络设备驱动:网络设备驱动没有设备文件,网络设备驱动主要是用来实现网络数据的收发工作
字符设备驱动
字符设备驱动的框架结构
内核层的 led_driver是字符设备驱动(驱动可以控制灯的亮灭)
驱动安装后不能自动运行,意思就是说insmod安装驱动的时候只是提供入口,它不能来控制灯闪烁。因为驱动是服务于用户的(应用程序)。所以必须要有一个应用程序来控制灯的驱动,由驱动来控制灯。
驱动写好之后 应用程序怎么访问驱动呢?
文件类型有bsp-lcd这几种
灯是c 字符设备文件。
驱动就会在上层产生一个文件供应用程序访问。
例如:鼠标的例子
应用程序怎么从鼠标中读取数据?
cd /dev/input 路径下有mouse0 mouse1 mouse2 结点
如果想从里面读数据 把它当成一个文件 使用open打开这个文件 read读数据 close关闭
/dev/input/mouse0文件就是驱动对应的设备文件
(输入类设备在input下放,普通设备就直接在dev下)
这个文件相当于给驱动在上层开了一个窗口,通过这个文件能找到这个驱动,并通过这个文件能访问到这个驱动。
**字符设备文件与访问普通文件的区别是:**普通文件通过open read write 访问的时候是通过文件系统 块设备驱动最终在磁盘上存储的。
字符设备文件是一个特殊的文件,如果要访问字符设备文件,就相当于在访问字符设备驱动,通过字符设备驱动访问鼠标等硬件。
对于这个文件怎么能准确找到你的驱动的呢?
对于每一个驱动内核会分配一个唯一的设备号。
/dev/input/mouse0文件和设备号关联
通过设备号就可以在内核中找到唯一的驱动。
设备号的组成:例如 13,32
13 << 20 | 32 组合成一个设备号
此时应用程序要访问灯,
open read write close 访问 /dev/myled,
应用程序通过设备文件就可以找到驱动进而访问驱动,
当open调用的时候要打开设备文件,open是系统调用,系统调用底层通过什么来实现的?
(A7核运行的模式有7种)
软中断 : user模式会切换到svc模式下 就进入到内核空间了
进入到内核空间后,驱动中有这些函数接口led_open led_read led_write led_close
当上层的open调的时候,通过系统调用,通过软中断,最终进入到内核空间,通过设备号找到驱动后就会回调驱动中对应的函数。read write close 同理。
鼠标调用同理。
如果是普通文件他会通过文件系统访问块设备文件,然后把文件存在磁盘上。
而现在的/dev/myled 和/dev/input/mouse0 叫做设备文件。设备文件相当于驱动在上层的抽象。
等价于当你用open read write close时候最终调到设备驱动中的led_open led_read led_write led_close 接口
驱动有操作硬件的能力,但没有操作硬件的逻辑。
控制灯的亮灭是用户实现的,驱动能控制灯的亮或者灭,但是不知道怎么亮怎么灭。
就像网卡驱动一样,网卡驱动可以控制网卡收发数据,但是不知道收发那些数据。
电脑怎么知道open所对应的内核层的函数叫做led_open呢?
通过函数指针实现的。你的应用层open调用的时候,进入到内核空间后,会访问open函数指针。因为你把你的函数赋给了函数指针,所以可以通过函数指针回调到函数。
这些函数指针在内存中不能离散存在,所以把这些函数指放在了一个操作方法结构体中,叫做file_operations 又叫fops。
面试问题:请问同学简述字符设备驱动的流程
内核空间写led_open led_read led_write led_close 函数访问硬件。
驱动能向下访问硬件但是没有逻辑,所以驱动还要向上提供操作接口。
驱动就会在上层创建设备文件(设备结点),设备文件通过设备号找到驱动。上层的
open read write close访问的时候 就会通过设备号在内核找到驱动,回调fops的函数,返回led_open时就可以操作led硬件。
字符设备驱动相关API
#include <linux/fs.h>
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:创建(注册)字符设备驱动
参数:
@major:主设备号 //次设备号[0-255]
major = 0:系统会自动分配主设备号
major > 0:静态指定设备号(但有可能失败)
@name:驱动的名字 cat /proc/devices
linux@ubuntu:/dev/input$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
| |
主设备号 设备的名字
@fops:操作方法结构体
struct file_operations {
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);
}
返回值:
major=0:成功返回主设备号,失败返回错误码
major>0:成功返回0,失败返回错误码
void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
@major:主设备号
@name:设备名字
返回值:无
register_chrdev 创建的是黑色的方块而不是fops
内部的实现先不管
字符设备驱动的实例
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#define CNAME "mycdev"
int major;
int mycdev_open(struct inode* inode, struct file* file)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t mycdev_read(struct file* file,
char __user* ubuf, size_t size, loff_t* offs)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t mycdev_write(struct file* file,
const char __user* ubuf, size_t size, loff_t* offs)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
int mycdev_close(struct inode* inode, struct file* file)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
const struct file_operations fops = {
.open = mycdev_open, //函数指针指向驱动函数
.read = mycdev_read,
.write = mycdev_write,
.release = mycdev_close,
};
//file_operations这个结构体内将mycdev_open这个函数赋给了open这个函数指针
//int mycdev_open(struct inode* inode, struct file* file)
//函数指针的类型和函数的类型要相同
//相当于把open函数赋给了.open这个函数指针
static int __init mycdev_init(void)
{
// 1.注册字符设备驱动
major = register_chrdev(0, CNAME, &fops);
if (major < 0) {
printk("register_chrdev error\n");
return -EAGAIN;
}
printk("register chrdev success,major = %d\n", major);
return 0;
}
static void __exit mycdev_exit(void)
{
// 2.注销字符设备驱动
unregister_chrdev(major, CNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
字符设备驱动的测试流程
1.编译驱动
make arch=x86 modname=mycdev
2.安装驱动
sudo insmod mycdev.ko
3.查看驱动
dmesg --查看主设备号 240
[17773.487979] register char device driver success,major = 240
cat /proc/devices --查看主设备号
(查看驱动的这个步骤,还没有执行open read 等操作,因为还没有写应用程序,没有调用open read write )
(写应用程序的话,open是要打开一个设备文件的,现在在/dev下还没有设备文件,原因是驱动没有设置自动创建设备文件 )
(所以需要创建设备文件 设备结点)
4.创建设备文件
暂时用这一条命令 手动创建设备文件
后面会把此步骤自动化
sudo mknod /dev/mycdev c 240 0
mknod :创建设备文件的命令
/dev/mycdev:设备文件的路径及名字(路径和名字是任意的,但是习惯上放在dev下)
c : c字符设备 b块设备 (n 不是网络设备 网络设备没有设备结点)
235 :主设备号 -----这个主设备号是上面dmesg查看来的 需要自己查看
0 :次设备号[0-255]
5.编写应用程序
让应用程序访问设备文件,通过设备文件访问驱动的open read write 接口
#include <head.h>
int main(int argc, const char* argv[])
{
int fd;
char buf[128] = { 0 };
if ((fd = open("/dev/mycdev", O_RDWR)) == -1)
PRINT_ERR("open error");
write(fd, buf, sizeof(buf));
read(fd, buf, sizeof(buf));
close(fd);
return 0;
}
6.执行应用程序看现象
linux@ubuntu:~/work/day2/03mycdev$ ./a.out
test.c main 8
open error: Permission denied //没有权限
添加权限
sudo ./a.out
或者
sudo chmod 0777 /dev/mycdev
结果说明:结果中可以看到我们的read 和write被调用到了,说明我们的应用程序 调用read和write成功。
用户空间和内核空间数据传递
上边的案例说明应用程序可以调到数据了,但是还不能实现相互的数据传输。
思考:
能不能用内核中的一个指针指向用户空间buffer[128]的字节,然后用指针取数据?
不能。如果在正在取数据(访问应用程序)的过程 ctrl+c 进程终止,内核会直接崩溃。
思考:能不能让用户空间的指针指向内核空间的一块区域取数据??
不能。因为用户空间访问内核空间只有唯一的一种方式----系统调用。
那么用户空间和内核空间想互用数据的话 方法只有一种:
如果内核空间想用用户空间的数据的话:
首先用户空间通过系统调用把地址告诉给内核,然后如果内核把这块空间搬移到内核空间来。
如果用户空间想用内核空间的数据的话:
首先通过系统调用在用户空间分配一块内存,把内核空间的数据拷贝到用户空间内。
用户空间和内核空间数据传递思想
用户空间和内核空间内存是隔离开的。
内核不能够通过直接指向用户空间内存地址,如果通过这个方式访问用户空间的数据,此时如果进程意外终止内核也会崩溃。
用户空间也不能够通过指针直接修改内核空间的数据,不安全,用户空间进入内核空间的方法只有一种系统调用。
所以不能通过指针的形式,让两者进程数据交互。
所以,为了克服上述缺点,如果用户和内核需要进行数据传输使用拷贝完成,即copy_to_user/copy_from_user函数完成。
数据传输的API
#include <linux/uaccess.h>
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
功能:将数据从内核空间拷贝到用户空间
参数:
@to:用户空间的地址
@from:内核空间的地址
@n:大小(单位字节)
返回值:成功返回0,失败返回未拷贝的字节
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
功能:将数据从用户空间拷贝到内核空间
参数:
@to:内核空间的地址
@from:用户空间的地址
@n:大小(单位字节)
返回值:成功返回0,失败返回未拷贝的字节
底层实现借助 memcpy 数据拷贝实现的
__开头的宏是给编译器使用的
用户空间和内核空间数据传输实例
mycdev.c
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#define CNAME "mycdev"
int major;
char kbuf[128] = { 0 };
int mycdev_open(struct inode* inode, struct file* file)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t mycdev_read(struct file* file,
char __user* ubuf, size_t size, loff_t* offs)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
if (size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_to_user(ubuf, kbuf, size);
if(ret){
printk("copy to user error\n");
return -EIO;
}
return size; //此处的返回要和应用层的返回值一致
}
ssize_t mycdev_write(struct file* file,
const char __user* ubuf, size_t size, loff_t* offs)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
if (size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_from_user(kbuf, ubuf, size);
if(ret){
printk("copy from user error\n");
return -EIO;
}
return size; //此处的返回要和应用层的返回值一致
}
int mycdev_close(struct inode* inode, struct file* file)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
const struct file_operations fops = {
.open = mycdev_open,
.read = mycdev_read,
.write = mycdev_write,
.release = mycdev_close,
};
static int __init mycdev_init(void)
{
// 1.注册字符设备驱动
major = register_chrdev(0, CNAME, &fops);
if (major < 0) {
printk("register_chrdev error\n");
return -EAGAIN;
}
printk("register chrdev success,major = %d\n", major);
return 0;
}
static void __exit mycdev_exit(void)
{
// 2.注销字符设备驱动
unregister_chrdev(major, CNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
test.c
#include <head.h>
int main(int argc, const char* argv[])
{
int fd;
char buf[128] = "i am test data uaccess...";
if ((fd = open("/dev/mycdev", O_RDWR)) == -1)
PRINT_ERR("open error");
write(fd, buf, sizeof(buf));
memset(buf,0,sizeof(buf));
read(fd, buf, sizeof(buf));
printf("buf = %s\n",buf);
close(fd);
return 0;
}
注意:底层不能随用随定义变量
//在应用层这样写没问题 在底层不能这样写
int ret = copy_from_user(kbuf, ubuf, size);
用户空间和内核空间数据传输练习
1.传输unsigned int类型的整数
2.传输img_t结构体变量
typedef struct{
int width;
int high;
}img_t;
驱动和上述2.3标题中的一样,应用程序如下:
#include <head.h>
typedef struct {
int width;
int high;
}img_t;
int main(int argc,const char * argv[])
{
int fd;
unsigned int data=1234;
if((fd = open("/dev/mycdev",O_RDWR))==-1)
PRINT_ERR("open error");
write(fd,&data,sizeof(data));
data=0;
read(fd,&data,sizeof(data));
printf("data = %d\n",data);
img_t img = {1920,1080};
write(fd,&img,sizeof(img));
memset(&img,0,sizeof(img));
read(fd,&img,sizeof(img));
printf("width=%d,high=%d\n",img.width,img.high);
close(fd);
return 0;
}
在内核空间操作LED寄存器
内核空间能否直接操作寄存器?
对于LED灯的操作是通过操作寄存器完成的,寄存器的地址是物理地址。(在数据手册上看到的地址全是物理地址)
但是在Linux内核启动之后,在程序中使用的地址是虚拟地址,不能够在内核空间直接操作物理地址。(我们能够通过%p打印出来的地址全是虚拟地址)
如果想要在内核空间操作LED灯,就需要将LED灯的物理地址映射到内核空间对应的虚拟地址,在内核空间操作这个虚拟地址就相当于在到LED的寄存器的物理地址。
uboot启动阶段有没有打开mmu(内存映射单元)?没有。MMU是在内核启动的时候打开的。
MMU会将物理地址映射到0-4G的虚拟地址。
MMU映射过程:在物理地址上加一个3G的偏移,就得到了虚拟地址。
现在咱们的内核空间对应的物理地址都有虚拟地址。我们要把这块虚拟内存对应的物理地址改到LED灯对应的寄存器的物理地址上。
地址映射的API
#include <linux/io.h>
void *ioremap(phys_addr_t offset, unsigned long size)
功能:完成地址映射
参数:
@offset:物理地址
@size:大小
返回值:成功返回虚拟地址,失败返回NULL
void iounmap(void *addr);
功能:取消地址映射
参数:
@addr:虚拟地址
返回值:无
注意:映射多少字节就取消映射多少字节
LED灯对应的物理地址
RCC_MP_AHB4ENSETR 0x50000a28 [4] 1 GPIOE时钟使能
GPIOx_MODER 0x50006000 [21:20] 01 输出
GPIOx_ODR 0x50006014 [10] 1 LED1亮 0 LED1灭