回顾:
1.linux内核提供的GPIO通用的操作库函数
硬件GPIO在内核都给定了一个软件编号,表示他们的唯一性!
硬件GPIO 内核软件编号
GPC1_3 S5PV210_GPC1(3)
内核驱动程序首先向内核申请GPIO资源:
gpio_request
一旦申请成功,就可以进行输入和输出操作:
gpio_direction_output
gpio_direction_input
gpio_set_value
gpio_get_value
GPIO资源驱动程序不再使用时,一定要进行手动释放:
gpio_free
2.linux系统调用
linux系统分为用户空间和内核空间,这两空间进行通信必须通过系统调用(SCI)。
用户空间不能直接访问硬件资源;
内核空间有权限访问硬件资源;
用户如果要访问硬件资源必须通过系统调用,陷入内核空间,然后再进行访问硬件资源。
用户空间如何陷入内核空间:软中断
系统调用实现原理:
1.比如应用程序调用write系统调用函数
2.调用C库的write函数实现
3.保存write的系统调用号到R7,调用svc触发软中断
4.CPU毫无条件的跑到内核准备好的异常向量表中的软中断处理入口vector_swi //至此进程由用户空间陷入内核空间
5.进入内核软中断的入口vector_swi以后,从R7寄存器中取出保存的系统调用号,然后以系统调用号为索引(下标)在内核里的系统调用表中找到write自己对应的内核实现函数sys_write.
6.执行内核sys_write函数,执行完毕,再原路返回到用户空间
如何实现一个新的系统调用:
内核:
1.在内核中添加新的系统调用号:__NR_led//arch/arm/include/asm/unistd.h
2.在系统调用表中添加新项:CALL(sys_led:本质就是函数的地址) //arch/arm/kernel/calls.S
3.在内核源码中添加.c或者内核已有的.c中添加sys_led函数的实现(操作硬件,开关灯)//arch/arm/kernel/sys_arm.c
用户:
syscall(led的系统调用号,sys_led函数的参数)
------------------------------------------------------------------------------------------------------------------------------------------
注意:自己添加新的系统调用产生的不良影响:
如果针对于任何硬件设备或者需求,不利用内核已有的系统调用函数,而是自己实现一套新的系统调用过程,最终导致内核的系统调用号非常之多,还导致系统调用表非常之大。并且内核相关的实现文件led.c必须静态编译到内核,导致zImage的体积过于庞大。所以内核鼓励使用已有的系统调用函数来实现对硬件设备的访问!
问题:如果利用内核已有的系统调用函数,那么内核的实现函数sys_read,sys_write,sys_open,sys_close如何调用到自己的设备驱动封装的函数呢?led_config,uart_read,uart_write,beep_config...
*******************************************************************************************
linux内核设备驱动分类:
1.字符设备:采用字节流形式访问
按键,led,beep,UART,声卡,LCD,触摸屏,ADC,i2c...
2.块设备:采用数据块进行访问,比如一次访问512字节,但是在linux系统中,也可以采用字节流形式访问。跟字符设备的区别在于内核描述它们使用的数据结构不一样!
硬盘,U盘,SD卡,TF卡,光盘,nandflash,norflash,emmc
3.网络设备:有线或者无线,一般要和TCP/IP协议一起使用
******************************************************************************
问:应用程序如何利用linux系统提供的标准系统调用函数来访问硬件呢?
答:回忆UC编程访问文件的过程:
int fd;
char buf[1024] ={0};
fd =open("test.txt", O_RDWR);
write(fd,"hello,world", strlen("hello,world"));
read(fd, buf,1024);
printf("filemsg : %s\n", buf);
close(fd);
注意:fd=0,1,2这三设备不用程序手动open,系统帮你open!
现在利用open,read,write,close访问led设备,首先肯定先open,问:open函数的参数怎么写open(?)?
答:linux,unix都遵循“一切皆文件”的思想,所以对于设备来说,在用户空间的表现形式也是以文件的形式存在,这是这个文件不是普通的文本文件,而是设备文件。设备文件在linux系统根文件系统中的dev目录下。设备文件是在系统启动的时候会动态创建在内存中,掉电丢失!
设备文件:字符设备文件和块设备文件。
设备文件本质上就是硬件设备!
ls /dev/console-lh
crw-rw---- 5, 1 /dev/console
console设备文件包含的属性:
c:表示属于字符设备
5:主设备号
1:次设备号
console:设备文件名
ls /dev/sda1 -lh
brw-rw---- 8, 1 /dev/sda1
b:表示属于块设备
8:主设备号
1:次设备号
sda1:设备文件名
问:如何创建设备文件呢?
答:mknod命令
格式:
创建字符设备文件:
mknod /dev/设备文件名 c 主设备号 次设备号
创建块设备文件:
mknod /dev/设备文件名 b 主设备号 次设备号
例如:
mknod /dev/myledc 250 0
mknod/dev/mybeep c 249 0
mknod/dev/myuart c 248 0
有了设备文件,应用程序就可以open设备了:
int fd =open("/dev/myled", O_RDWR);
主设备号:应用程序根据设备文件的主设备号信息来找到内核了跟自己对应的驱动程序(.c)。一个驱动程序只有唯一的主设备号
次设备号:如果驱动程序同时管理多个设备硬件,可以通过次设备号来分区具体操作的哪个硬件设备。
例如串口:
crw-rw---- 1 root 0 204, 64 Jan 1 00:00 /dev/s3c2410_serial0
crw-rw---- 1 root 0 204, 65 Jan 1 00:00 /dev/s3c2410_serial1
crw-rw---- 1 root 0 204, 66 Jan 1 00:00 /dev/s3c2410_serial2
说明:一般使用次设备号来分区,应用在这些硬件设备的硬件特性都大致相同,可以采用一个驱动来管理多个设备,但需要通过次设备号来分区!
*****************************************************
设备号:包含主设备号,次设备号
设备号的数据类型:dev_t (unsigned int)
高12位存放主设备号
低20位存放次设备号
问:已知设备号,如何获取主,次设备号呢:
答:
主设备号 = MAJOR(设备号);
次设备号 = MINOR(设备号);
问:已知主,次设备号,如何获取设备号呢?
答:
设备号 = MKDEV(主设备号,次设备号);
应用程序根据主设备号找到对应的驱动程序,再根据次设备号找到对应的硬件。所以驱动需要和设备号进行一一的绑定!
问:在设备驱动程序中,如何向内核去申请设备号进行跟驱动完成一一的绑定工作呢?
答:设备号对于内核来说,就是一种宝贵的资源,向内核申请设备号,有两种方法:
方法1:静态申请
实施步骤:
1.系统启动完毕,首先执行cat /proc/devices查看当前系统中哪个主设备处于空闲状态:
Characterdevices: //当前系统中已经注册的字符设备
第一列:表示设备的主设备号
第二列:表示设备的名称,不是设备文件名
一下信息说明,1,2,3,4,5等主设备号已被占有!
然后找一个空闲的主设备号,例如250
1 mem
2 pty
3 ttyp
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
2.根据实际驱动要操作的硬件个数来决定次设备号,如果驱动只操作一个硬件设备,次设备号一般给0.例如开发板上有2个灯,那么对于这个两个灯,对应的次设备号可以给0,也就是将两个灯归为一个硬件设备,当然可以通过其他方法来区分。当然也可以给每一个LED分配一个次设备号,比如LED1->0,LED2->1,这种分配方式设备LED灯都要分配一个设备文件。
3.可以提前创建设备文件
mknod /dev/myled c 250 0
4.合并设备号
dev_t dev = MKDEV(250, 0);
5.1,2两步骤仅仅是人为分配了主设备号和次设备号,还需要正式向内核去申请这个号。
register_chrdev_region(dev_t dev,
unsigned int count,
char *name);
功能:静态向内核申请设备号
dev:合并好的设备号
count:设备的个数,起始说的就是次设备号的个数
name:设备名称,如果申请成功,cat /proc/devices能看到
优点:能够在申请设备号之前提前创建设备文件
缺点:不便于驱动在别的开发板上进行推广。
方法2:动态申请
实施步骤:
1.直接调用alloc_chrdev_region函数向内核申请,内核帮你分配设备号。
intalloc_chrdev_region(dev_t* dev,
unsigned baseminor, unsigned count,
constchar *name);
dev: 内核帮你申请的设备号,保存分配到的设备号
baseminor: 希望分配的起始次设备号,一般写0
count: 需要分配的次设备号数目
name:设备名称(出现在/proc/devices)
优点:便于驱动在别的开发板上进行推广
缺点:在申请设备号之前,不能提前创建设备文件
注意:不管静态申请还是动态申请,主设备号不能为0!
设备号如果驱动不在使用,一定要归还操作系统:
unregister_chrdev_region(dev_tdev,
unsigned int coutn);
dev:分配的设备号
count:次设备号的个数
案例:利用内核模块参数声明的知识点来实现动态申请和静态申请设备号。
module_param(...);
实验步骤:
静态申请:
cat/proc/devices //找一个空闲的主设备号250
insmodmychar_drv.ko major=250
cat/proc/devices //申请结果
250 tarena1
rmmod mychar_drv
cat/proc/devices //没有申请的信息
动态申请:
insmodmychar_drv.ko
cat/proc/devices //申请结果
250 tarena2
**********************************************************************
问:应用程序通过设备文件的设备号(主设备号,次设备号)并且利用标准的系统调用函数找到自己对应的设备驱动,一旦找到了设备驱动,如何调用设备驱动程序中相关操作硬件的函数呢,比如led_config?
答:需要了解掌握linux字符设备驱动编程实现框架。
案例:用C的方式来描述人的特性:
肯定使用struct结构体:
struct people {
int age;
int b;
int c;
char *name;
....
};
1.linux内核描述字符设备设备驱动涉及的数据结构:
内核使用struct cdev结构体来描述字符设备
struct cdev {
const struct file_operations *ops; //给字符设备提供相关的操作硬件的方法(函数),ops仅仅是一个指针,在实例化一个字符设备对象时,一定要将其指向structfile_operations结构体变量(对象)
dev_t dev; //保存记录成功申请的设备号
unsigned int count; //记录字符设备驱动管理的硬件设备的个数
};
给字符设备提供操作硬件的方法涉及的结构体:
structfile_operations {
int (*open)(...)
int (*release)(...)
int (*read)(...);
int (*write)(...);
...
};
案例:如何利用以上两个结构体来实现一个LED字符设备驱动
分析:
1.明确应该给驱动程序提供哪些操作方法,例如现在只需一个open函数即可。
static intled_open(...)
{
printk(".....");
}
2.利用struct file_opertions分配和初始化驱动操作硬件的集合对象
struct file_operations led_fops = {
.open = led_open //led_open函数肯定由驱动来实现,仅仅是一个操作硬件的函数而已!
};
3.利用struct cdev分配一个对象
struct cdev led_cdev; //分配led_cdev字符设备对象
2.初始化这个led_cdev对象
cdev_init(&led_cdev, &led_fops);
函数原型:
int cdev_init(struct cdev *cdev,
const struct file_operations *fops)
3.向内核注册字符设备对象led_cdev
cdev_add(&led_cdev, 申请的设备号, 设备的个数);
函数原型:
int cdev_add(struct cdev *cdev,
dev_t dev, unsigned int count);
注册结果:内核就会有一个真实LED字符设备驱动;
4.卸载字符设备对象
cdev_del(&led_cdev);
问:led_cdev对象注册到内核的哪个地方呢?
答:linux内核已经定义了一个cdev的散列表(数组),这个数组中的每一个元素存放注册的cdev对象指针,以设备号为索引(下标)来存放!
猜测:应用程序通过设备号找到驱动程序,其实是驱动程序通过设备号找到自己的字符设备驱动对象led_cdev,然后在通过这个对象的ops字段找到硬件操作的方法。
问:应用程序如何调用到驱动提供的硬件操作集合中的函数呢?
案例:编写LED字符设备驱动
实验步骤:
PC机:
make
cp led_drv.ko/opt/rootfs/
arm-linux-gcc -oled_test led_test.c
ARM:
insmodled_drv.ko
cat/proc/devices //查看申请的主设备号
mknod /dev/myledc 250 0
./led_test
测试结果:
app:open->软中断->sys_open->驱动struct file_operations的open函数 = led_open
app:close->软中断->sys_close->驱动struct file_operations的release = led_close
问:应用程序通过系统调用函数如何调用到驱动的struct file_operations相关的函数呢?
答:
structinode:linux内核用此结构体来描述一个文件的物理信息
从驱动的角度只关心两个字段:
.i_rdev 保存设备号,mknod时保存设备号
.i_cdev 保存字符设备对象指针,mknod时,内核分配初始化inode对象,然后根据设备号从内核cdev数组中取出之前注册的字符设备对象led_cdev指针,赋值给i_cdev.
inode声明周期:内核从创建设备文件开始,销毁设备文件结束!
structfile:linux内核用此结构体来描述一个文件被打开以后的状态信息,从驱动角度只需关注:
.f_op 最终指向驱动的struct file_operations硬件操作集合
file结构体的生命周期:从open时内核创建此结构体对象来描述文件打开以后的状态,close关闭文件时,内核销毁此对象。
一个文件只有唯一的inode对象,可以有多个file对象!
应用程序调用设备驱动相关操作接口struct file_operations过程:
驱动安装:
1.驱动程序分配初始化硬件操作对象led_fops,并且给出相关的操作方法(led_open,led_close,led_read,led_write)
2.驱动分配初始化注册字符设备对象led_cdev,并且将硬件操作集合led_fops赋值给led_cdev.ops.这样完成了给字符设备驱动提供操作硬件的方法
3.注册字符设备对象,其实就是以设备号为索引,将字符设备对象的指针&led_cdev添加到内核的cdev数组中。
创建设备文件:
4.应用程序利用mknod创建设备文件,内核会给这个设备文件分配一个struct inode对象,然后把设备号赋值给inode.i_rdev
5.内核然后以设备号为索引,在内核的cdev数组中找到之前注册的字符设备对象指针&led_cdev,然后将这个对象指针赋值给inode的i_cdev指针
运行应用程序时:
对文件的访问永远先open:
app:open打开设备文件时:
1.先调用C库的open,C库的open将open的系统调用号保存在R7中,调用SVC触发软中断
2.CPU跳转到内核的异常向量表的入口vector_swi位置
3.根据R7的系统调用号,在系统调用表中找到open的内核实现函数sys_open
4.sys_open函数中要做如下事情:
5.sys_open会分配struct file对象,从已知的字符设备对象led_cdev中的ops取出驱动的硬件操作集合led_fops
5.将led_fops的指针赋值给file对象的f_op
6.然后将设备文件描述符fd和struct file进行绑定关联
7.然后判断一下底层驱动led_fops中有没有open函数的实现(led_open)如果有,调用它,如果没有,给用户永远返回成功!
注意:底层驱动的各个函数返回值是给内核对应的sys_xxx函数使用,不是给应用的系统调用函数
open打开设备文件结论:
file->f_op = &led_fops;
应用程序调用read:
1.app:read->软中断->sys_read:
2.sys_read函数就执行一句话:
通过fd获取关联的struct file
file->f_op->read(...) =&led_fops->read = led_read
应用程序调用close:
1.close->软中断->sys_close:
2.sys_close:
通过fd获取关联的struct file
file->f_op->release =&led_fops->release = led_close
总结:open相当重要!完成了structfile_operations和struct file结构体的关联!