回顾:
1.阻塞和非阻塞
阻塞:休眠,等待队列,2个编程方法
非阻塞:不休眠,立即返回到用户空间
应用程序:open(...,O_NONBLOCK);
驱动:if(file->f_flags & O_NONBLOCK) //成立非阻塞
2.linux内核内存相关内容
内存空间:32,4G
IO空间:16,64K
3.物理地址和虚拟地址,MMU
4.用户空间,内核空间和物理内存之间的映射实现
用户空间的虚拟内存和物理内存之间的映射是动态的!
内核空间的虚拟内存和物理内存之间的映射在内核初始化时实现一部分的一一映射!
linux内核为了访问所有的物理内存,linux内核将内核的1G虚拟内存进行划分为若干个区域:
X86:
直接内存映射区
动态内存映射区
永久内存映射区
固定内存映射区
S5PV210:
异常向量表
固定内存映射区
DMA内存映射区:kmalloc(100, GFP_DMA);
动态内存映射区:
直接内存映射区:
5.linux内核内存分配的方法:
kmalloc/kfree
kzalloc = kmalloc+memset
大小问题:最小32字节,最大是128K/4M
直接内存映射区分配
物理连续,虚拟连续
分配内存时指定的标志(行为):
GFP_KERNEL:休眠
GFP_ATOMIC:不休眠
__get_free_pages/free_pages
最小1页,最大4M
直接内存映射区分配
物理连续,虚拟连续
vmalloc/vfree:
动态内存映射区分配
可能会休眠
虚拟上连续,物理上不一定连续
驱动中定义全局的数组:
static char buf[5*1024];
static char buf[5*1024] = {0xaa};
内核启动参数vmalloc=
内核启动参数mem=,驱动配合ioremap进行映射
8M对应的内核虚拟地址=iorempa(8M物理内存的起始地址,8M);
linux内核如何将物理地址(不仅仅包含物理内存的地址还包括其他外设的物理地址,比如GPIO寄存器的物理地址,nandflash控制器的寄存器的物理地址)映射到内核的虚拟地址上?
内核虚拟地址 =ioremap(物理地址,大小);
ioremap映射使用的内核虚拟内存是动态内存映射区中的内存!
解除地址映射:
iounmap(映射的虚拟地址);
**********************************************************
案例:寄存器编辑器软件
./regeditor_test w 0xe0200080 0x11000
./regeditor_test w 0xe0200084 0x18
写过程:
用户应用需要将操作的地址和数据发送给内核
读过程:
./regeditor_test r 0xe0200080
./regeditor_test r 0xe0200084
用户应用需要将操作的地址发送给内核
注意:在读取寄存器时,只需要关心要观察的bit位即可!
*****************************************************************************************
linux内核链表:
传统链表:
内核链表:
struct list_head
list_add
list_add_tail
list_del
list_entry=container_of
list_for_each
list_for_each_safe
都定义在include/linux/list.h
以上方法的形参的数据类型都是structlist_head
**********************************************************
案例:分析ioremap案例中驱动,然后如果把LED对应的GPIO换成别的GPIO,需要做哪些修改:
CW210开发板 TARENA开发板
GPC1_3 ----------------> GPF1_5
GPC1_4 ----------------> GPF1_6
0xe0200080 0xe0200140
[12:15] [3] [23:20] [5]
[16:19] [4] [24:27] [6]
总结:通过分析驱动代码,发现驱动包含两部分内容:
一部分纯软件信息,例如设备号,字符设备对象,设备类,逻辑运算
另一部分纯硬件信息,例如GPIO的编号,GPIO寄存器的地址;
如果这个驱动程序要实现从CW210平台转移到TARENA平台,发现原先驱动几乎都要从头到尾看一遍,检查一遍,但凡涉及硬件相关的内容,都需要修改,但是纯软件的内容无需修改,这个代码的可移植性非常差(移植性非常差:硬件一变,要修改的内容太多了);类似:
pr_debug("pi = %f\n", 3.14);
pr_debug("pi = %f\n", 3.14);
改正:
#define PI (3.1415)
pr_debug("pi = %f\n", PI);
pr_debug("pi = %f\n", PI);
问:如何解决驱动代码的可移植性非常差的问题?
答:
明确:驱动:硬件+软件
linux内核提出了“分离思想”用于解决驱动的移植性问题!
分离思想:本质目的就是将驱动涉及的纯硬件和纯软件进行分离,如果一旦硬件进行修改,只需要修改纯硬件那部分内容,纯软件的内容无需修改!
linux内核分离思想的实现过程:
1.首先内核已经帮你定义好了一个虚拟总线(软件实现)platform_bus_type(平台总线),并且在这个总线上维护这两个链表dev链表和drv链表;内核虚拟总线对应的数据类型:
struct bus_type,平台总线的类型为虚拟总线类型,是一个虚拟总线类型的一个对象!struct bus_typeplatform_bus_type;
2.dev链表上每一个节点存放的硬件相关的信息,并且对应的硬件节点的数据结构是struct platform_device.每当用这个结构体分配一个对象,然后根据实际的硬件初始化这个对象,然后注册添加到dev链表上,最后内核会帮你去遍历drv链表,取出drv链表上每一个软件节点,调用总线提供的match函数,比较软件节点和硬件节点的name是否相等(strcmp),如果相等内核会调用软件节点的probe函数,然后将硬件节点的首地址传递probe函数,让probe函数获取硬件操作硬件;
如果没有匹配成功(没有找到匹配的软件节点),那就等待软件节点的到来!
3.drv链表上每一个节点存放的软件相关的信息,并且对应的软件节点的数据结构是struct platform_driver.每当用这个结构体分配一个对象,然后根据实际的软件操作,然后注册添加到drv链表上,最后内核会帮你去遍历dev链表,取出dev链表上每一个硬件节点,调用总线提供的match函数,比较软件节点和硬件节点的name是否相等(strcmp),如果相等内核会调用软件节点的probe函数,然后将硬件节点的首地址传递probe函数,让probe函数获取硬件操作硬件;
如果没有匹配成功(没有找到匹配的硬件节点),那就等待硬件节点的到来!
4.实际驱动开发只需关注:
struct platform_device用它如何去描述硬件信息;
structplatform_driver用它如何去描述软件信息;
总结:probe函数是否被调用,代表着软件和硬件是否结合!如果被调用,软件和硬件再度结合,如果没有调用,这个驱动不完整!
5.struct platform_device使用过程;
结构体:
struct platform_device {
const char * name;
int id;
struct resource * resource; //sizeof指针永远等于4
u32 num_resources;
structdevice dev;
};
成员说明:
name:硬件节点的名称,必须要指定初始化,这个字段相当重要,用于软件和硬件的匹配!
id:硬件设备编号,如果只有一个设备,id为-1,如果有多个同名的设备,通过id来区分:0,1,2,3...
resource:用来装载设备的硬件信息,这个指针指向struct resource结构体描述的硬件信息
num_resources:用resource结构体描述的硬件信息的个数!
dev:其中只关注一个成员platform_data(void *类型)以及release(函数指针(void (* release(struct device*)))),一般也可以用这个字段来装载驱动开发人员自己定义的描述硬件信息的结构体(led_resource,btn_resource);
//描述装载硬件信息的数据结构
struct resource {
unsignedlong start; //硬件资源的起始信息
unsignedlong end; //硬件资源的结束信息
unsignedlong flags;//硬件资源的标识
};
flags:硬件资源标识:
IORESOURCE_MEM:表示这类设备的访问像内存一样,是内存资源;例如寄存器地址
IORESOURCE_IRQ:IO资源,例如GPIO的软件编号,中断号
例如,描述XEITN0
struct resource led_res[] = {
[0]= {
.start = 寄存器的起始地址;
.end = 寄存器的结束地址
.flags = IORESOURCE_MEM
},
[1]= {
.start = IRQ_EINT(0),
.end = IRQ_EINT(0),
.flags = IORESOURCE_IRQ
}
};
如何使用:
1.分配初始化硬件节点platform_device
struct platform_device led_dev = {
.name= "myled",
.id= -1,
.resource= led_res,//指向用struct resource结构体描述的硬件信息
.num_resources= ARRAY_SIZE(led_res),
.dev= {
.platform_data = &led_info,//指向自己封装的硬件结构体
.release=led_release//必须有
}
};
2.向内核的dev链表添加注册硬件节点
platform_device_register(&led_dev);
1.帮你完成将硬件节点led_dev添加到dev链表中
2.添加完毕以后,内核帮你遍历drv链表,(取出drv链表上的每一个软件节点,调用平台总线提供的match函数,进行软件节点的name与硬件节点的name的比较,若相等,则将硬件节点的地址&led_dev传递给软件节点的成员probe函数!)进行一系列的后续操作!
案例:编写led_dev.c实现描述LED灯的硬件相关的信息!
--------------------------------------------------------------------------------------------------------------------------------------------
6.struct platform_driver使用过程;
struct platform_driver {
int (*probe)(structplatform_device *pdev);//最重要的成员
int (*remove)(structplatform_device *pdev);
struct device_driver driver;
void(*shutdown)(struct platform_device *);
int(*suspend)(struct platform_device *, pm_message_t state);
int(*resume)(struct platform_device *);
};
probe函数:
每当硬件和软件匹配成功,内核调用此函数;这个函数的调用,代表驱动的生命周期正式启动!
形参pdev指向匹配成功的硬件节点的首地址;
remove函数:
每当卸载软件或者硬件节点时,内核调用此函数,这个函数的调用,代表驱动的生命结束!
其他三个函数跟电源管理机制相关:
shutdown:关闭系统,内核调用
suspend:系统待机休眠,内核调用
resume:系统唤醒,内核调用
driver成员只需关注其中的name,这个name用于匹配!
如何使用呢?
1.分配初始化软件节点
struct platform_driver led_drv = {
.driver = {
.name ="myled" //必须写,用于匹配
},
.probe = led_probe, //匹配成功调用
.remove = led_remove,
//如果涉及电源管理
.suspend= led_suspend,
.resume= led_resume
};
2.向内核的drv链表添加软件节点
platform_driver_register(&led_drv);
1.添加软件节点到drv链表
2.遍历dev链表,进行一系列的后续操作
案例:编写led_drv.c来描述操作硬件的软件信息
案例:添加GPC1_4的支持!添加2个灯的支持!
案例:不采用structresource描述硬件信息
//自己定义描述硬件的数据结构
led_dev.c:
struct led_resource {
unsignedlong addr; //寄存器的起始物理地址
intsize; //寄存器的地址空间
intpin[2]; //GPIO硬件编号
};
static struct led_resource led_info = {
.addr= 0xe0200080,
.size= 8,
.pin= {3, 4}
};
led_drv.c
struct led_resource *pdata =pdev->dev.platform_data;
pdata->addr/pdata->size/pdata->pin[0]/pdata->pin[1]