驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁。
具体任务
说明:设备驱动的两个任务方向
Linux系统主要部分:内核、shell、文件系统、应用程序
二、驱动分类
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
1.字符设备(CharDevice)
2.设备(BlockDevice)
3.网络设备(NetDevice)
三、驱动程序的功能
四、驱动开发前提知识
4.1内核态和用户态
KernelMode(内核态)
UserMode(用户态)
Linux利用CPU实现内核态和用户态
Linux实现内核态和用户态切换
Linux只能通过系统调用和硬件中断从用户空间进入内核空间
4.2Linux下应用程序调用驱动程序流程
大致流程
4.3内核模块
Linux驱动有两种运行方式
将驱动编译进Linux内核中,当Linux内核启动的时就会自动运行驱动程序。
将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用相应命令加载驱动模块。
内核模块组成
1.模块加载函数
module_init(xxx_init);2.模块卸载函数
module_exit(xxx_exit);3.模块许可证明
MODULE_LICENSE("GPL")//添加模块LICENSE信息,LICENSE采用GPL协议4.模块参数(可选)
模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间-->内核空间单向的,他对应模块内部的全局变量
5.模块信息(可选)
MODULE_AUTHOR("songwei")//添加模块作者信息6.模块打印printk
printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。
printk函数主要做两件事情:①将信息记录到log中②调用控制台驱动来将信息输出
#defineKERN_SOH"\001"#defineKERN_EMERGKERN_SOH"0"/*紧急事件,一般是内核崩溃*/#defineKERN_ALERTKERN_SOH"1"/*必须立即采取行动*/#defineKERN_CRITKERN_SOH"2"/*临界条件,比如严重的软件或硬件错误*/#defineKERN_ERRKERN_SOH"3"/*错误状态,一般设备驱动程序中使用KERN_ERR报告硬件错误*/#defineKERN_WARNINGKERN_SOH"4"/*警告信息,不会对系统造成严重影响*/#defineKERN_NOTICEKERN_SOH"5"/*有必要进行提示的一些信息*/#defineKERN_INFOKERN_SOH"6"/*提示性的信息*/#defineKERN_DEBUGKERN_SOH"7"/*调试信息*/printk(KERN_DEBUG"gsmi:LogShutdownReason\n");如果使用printk的时候不显式的设置消息级别,那么printk将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为4。
#defineCONSOLE_LOGLEVEL_DEFAULT7CONSOLE_LOGLEVEL_DEFAULT控制着哪些级别的消息可以显示在控制台上,此宏默认为7,意味着只有优先级高于7的消息才能显示在控制台上。
这个就是printk和printf的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为4,4的级别比7高,所示直接使用printk输出的信息是可以显示在控制台上的。
模块操作命令
1.加载模块
insmodXXX.ko
modprobeXXX.ko
2.卸载模块
rmmodXXX.ko
3.查看模块信息
lsmod
modinfo(模块路径)
4.4设备号
MAJOR//用于从dev_t中获取主设备号,将dev_t右移20位即可。MINOR//用于从dev_t中获取次设备号,取dev_t的低20位的值即可。MKDEV//用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号。4.5地址映射
MMU(MemoryManageUnit)内存管理单元
内存映射函数
CPU只能访问虚拟地址,不能直接向寄存器地址写入数据,必须得到寄存器物理地址在Linux系统中对应的虚拟地址。
物理内存和虚拟内存之间的转换,需要用到:ioremap和iounmap两个函数
/*phys_addr:要映射给的物理起始地址(cookie)size:要映射的内存空间大小mtype:ioremap的类型,可以选择MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED和MT_DEVICE_WC,ioremap函数选择MT_DEVICE返回值:__iomem类型的指针,指向映射后的虚拟空间首地址*/#defineioremap(cookie,size)__arm_ioremap((cookie),(size),MT_DEVICE)void__iomem*__arm_ioremap(phys_addr_tphys_addr,size_tsize,unsignedintmtype){returnarch_ioremap_caller(phys_addr,size,mtype,__builtin_return_address(0));}例:获取某个寄存器对应的虚拟地址
#defineaddr(0X020E0068)//物理地址staticvoid__iomem*va;//指向映射后的虚拟空间首地址的指针va=ioremap(addr,4);//得到虚拟地址首地址参数addr:要取消映射的虚拟地址空间首地址
iounmap(va);I/O内存访问函数
当外部寄存器或外部内存映射到内存空间时,称为I/O内存。但是对于ARM来说没有I/O空间,因此ARM体系下只有I/O内存(可以直接理解为内存)。
使用ioremap函数将寄存器的物理地址映射到虚拟地址后,可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
u8readb(constvolatilevoid__iomem*addr)u16readw(constvolatilevoid__iomem*addr)u32readl(constvolatilevoid__iomem*addr)readb、readw和readl分别对应8bit、16bit和32bit读操作,参数addr就是要读取写内存地址,返回值是读取到的数据
voidwriteb(u8value,volatilevoid__iomem*addr)voidwritew(u16value,volatilevoid__iomem*addr)voidwritel(u32value,volatilevoid__iomem*addr)writeb、writew和writel分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr是要写入的地址。
五、设备树
DeviceTree是一种描述硬件的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。
Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性
5.1DTS、DTB和DTC
DTS
DTSI
DTC
DTB
5.2设备树框架
1.根节点:\2.设备节点:nodex①节点名称:node②节点地址:node@0,@后面即为地址3.属性:属性名称(Propertyname)和属性值(Propertyvalue)4.标签5.3DTS语法
dtsi头文件
#include
设备节点
label:node-name@unit-addresslabel:节点标签,方便访问节点:通过&label访问节点,追加节点信息node-name:节点名字,为字符串,描述节点功能unit-address:设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略1.字符串:compatible="arm,cortex-a7";设置compatible属性的值为字符串“arm,cortex-a7”2.32位无符号整数:reg=<0>;设置reg属性的值为03.字符串列表:字符串和字符串之间采用“,”隔开compatible="fsl,imx6ull-gpmi-nand","fsl,imx6ul-gpmi-nand";设置属性compatible的值为“fsl,imx6ull-gpmi-nand”和“fsl,imx6ul-gpmi-nand”。属性
cpp"manufacturer,model"manufacturer:厂商名称model:模块对应的驱动名字
例:
imx6ull-alientekemmc.dts中sound节点是音频设备节点,采用的欧胜(WOLFSON)出品的WM8960,sound节点的compatible属性值如下:
cppcompatible="fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。
sound这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
在根节点来说,Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。如果不支持的话那么这个设备就没法启动Linux内核。
model属性值是一个字符串,一般model属性描述设备模块信息。
status属性和设备状态有关的,status属性值是字符串,描述设备的状态信息。
用于描述子节点的地址信息,reg属性的address和length的字长。
//每个“addresslength”组合表示一个地址范围,//其中address是起始地址,length是地址长度,//#address-cells表明address这个数据所占用的字长,//#size-cells表明length这个数据所占用的字长.reg=
例
uart1:serial@02020000{compatible="fsl,imx6ul-uart","fsl,imx6q-uart","fsl,imx21-uart";reg=<0x020200000x4000>;interrupts=
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字
ranges是一个地址映射/转换表,ranges属性每个项目由子地址、父地址和地址空间长度这三部分组成。
如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长length:子地址空间的长度,由父节点的#size-cells确定此地址长度所占用的字长在根节点“/”中有两个特殊的子节点:aliases和chosen
1.aliases
aliases{can0=&flexcan1;can1=&flexcan2;...usbphy0=&usbphy1;usbphy1=&usbphy2;};aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。
但是,一般会在节点命名的时候会加上label,然后通过&label来访问节点。
2.chosen
chosen不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据(bootargs参数)。
5.4OF操作函数
Linux内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”(称为OF函数)
查找节点
Linux内核使用device_node结构体来描述一个节点:
structdevice_node{constchar*name;/*节点名字*/constchar*type;/*设备类型*/phandlephandle;constchar*full_name;/*节点全名*/structfwnode_handlefwnode;structproperty*properties;/*属性*/structproperty*deadprops;/*removed属性*/structdevice_node*parent;/*父节点*/structdevice_node*child;/*子节点...}structdevice_node*of_find_node_by_name(structdevice_node*from,constchar*name)from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值:找到的节点,如果为NULL表示查找失败。
structdevice_node*of_find_node_by_type(structdevice_node*from,constchar*type)from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
type:要查找的节点对应的type字符串,device_type属性值。
返回值:找到的节点,如果为NULL表示查找失败
structdevice_node*of_find_compatible_node(structdevice_node*from,constchar*type,constchar*compatible)from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
type:要查找的节点对应的type字符串,device_type属性值,可以为NULL
compatible:要查找的节点所对应的compatible属性列表。
structdevice_node*of_find_matching_node_and_match(structdevice_node*from,conststructof_device_id*matches,conststructof_device_id**match)from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
matches:of_device_id匹配表,在此匹配表里面查找节点。
match:找到的匹配的of_device_id。
inlinestructdevice_node*of_find_node_by_path(constchar*path)path:设备树节点中绝对路径的节点名,可以使用节点的别名
获取属性值
Linux内核中使用结构体property表示属性
structproperty{char*name;/*属性名字*/intlength;/*属性长度*/void*value;/*属性值*/structproperty*next;/*下一个属性*/unsignedlong_flags;unsignedintunique_id;structbin_attributeattr;}property*of_find_property(conststructdevice_node*np,constchar*name,int*lenp)np:设备节点。
name:属性名字。
lenp:属性值的字节数,一般为NULL
返回值:找到的属性。
intof_property_count_elems_of_size(conststructdevice_node*np,constchar*propnameintelem_size)np:设备节点。
proname:要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值:0读取成功;
负值:读取失败,
-EINVAL表示属性不存在
-ENODATA表示没有要读取的数据,
-EOVERFLOW表示属性值列表太小
of_property_read_u8_arrayof_property_read_u16_arrayof_property_read_u32_arrayof_property_read_u64_arrayintof_property_read_u8_array(conststructdevice_node*np,constchar*propname,u8*out_values,size_tsz)np:设备节点。
out_value:读取到的数组值,分别为u8、u16、u32和u64。
sz:要读取的数组元素数量。
返回值:0:读取成功;
负值:读取失败
-ENODATA表示没有要读取的数据
intof_property_read_string(structdevice_node*np,constchar*propname,constchar**out_string)np:设备节点。
out_string:读取到的字符串值。
返回值:0,读取成功,负值,读取失败
intof_n_addr_cells(structdevice_node*np)intof_n_size_cells(structdevice_node*np)np:设备节点。
返回值:获取到的#address-cells属性值。
返回值:获取到的#size-cells属性值。
of_iomap函数用于直接内存映射,前面通过ioremap函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过of_iomap函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存。
of_iomap函数本质上也是将reg属性中地址信息转换为虚拟地址,如果reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段,of_iomap函数原型如下:
void__iomem*of_iomap(structdevice_node*np,intindex)np:设备节点。
index:reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0。
返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。
#if1/*1、寄存器地址映射*/IMX6U_CCM_CCGR1=ioremap(regdata[0],regdata[1]);SW_MUX_GPIO1_IO03=ioremap(regdata[2],regdata[3]);SW_PAD_GPIO1_IO03=ioremap(regdata[4],regdata[5]);GPIO1_DR=ioremap(regdata[6],regdata[7]);GPIO1_GDIR=ioremap(regdata[8],regdata[9]);#else//第一对:起始地址+大小-->映射这样就不用获取reg的值IMX6U_CCM_CCGR1=of_iomap(dtsled.nd,0);SW_MUX_GPIO1_IO03=of_iomap(dtsled.nd,1);SW_PAD_GPIO1_IO03=of_iomap(dtsled.nd,2);GPIO1_DR=of_iomap(dtsled.nd,3);GPIO1_GDIR=of_iomap(dtsled.nd,4);#endifof函数在led_init()中应用
intret;u32regdate[14];constchar*str;structproperty*proper;/*1、获取设备节点:*/dtb_led.nd=of_find_node_by_path("/songwei_led");if(dtb_led.nd==NULL){printk("songwei_lednodecannotfound!\r\n");return-EINVAL;}else{printk("songwei_lednodehasbeenfound!\r\n");}/*2、获取compatible属性内容*/proper=of_find_property(dtb_led.nd,"compatible",NULL);if(proper==NULL){printk("compatiblepropertyfindfailed\r\n");}else{printk("compatible=%s\r\n",(char*)proper->value);}/*3、获取status属性内容*/ret=of_property_read_string(dtb_led.nd,"status",&str);if(ret<0){printk("statusreadfailed!\r\n");}else{printk("status=%s\r\n",str);}/*4、获取reg属性内容*/ret=of_property_read_u32_array(dtb_led.nd,"reg",regdate,10);if(ret<0){printk("regpropertyreadfailed!\r\n");}else{u8i=0;printk("regdata:\r\n");for(i=0;i<10;i++)printk("%#X",regdate[i]);printk("\r\n");}六、字符设备驱动
6.1字符设备基本驱动框架
1.模块加载
/*驱动入口函数*/staticint__initxxx_init(void){/*入口函数具体内容*/return0;}/*驱动出口函数*/staticvoid__exitxxx_exit(void){/*出口函数具体内容*/}/*将上面两个函数指定为驱动的入口和出口函数*/module_init(xxx_init);module_exit(xxx_exit)2.注册字符设备驱动
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。字符设备的注册和注销函数原型:
staticinlineintregister_chrdev(unsignedintmajor,constchar*name,conststructfile_operations*fops)staticinlinevoidunregister_chrdev(unsignedintmajor,constchar*name)这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。
一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。
3.内存映射
#defineioremap(cookie,size)__arm_ioremap((cookie),(size),MT_DEVICE)void__iomem*__arm_ioremap(phys_addr_tphys_addr,size_tsize,unsignedintmtype){returnarch_ioremap_caller(phys_addr,size,mtype,__builtin_return_address(0));}返回值:__iomem类型的指针,指向映射后的虚拟空间首地址。
建立映射:映射的虚拟地址=ioremap(IO内存起始地址,映射长度);一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存。
voidiounmap(volatilevoid__iomem*addr)4.应用层和内核层传递数据
应用层和内核层是不能直接进行数据传输的。要想进行数据传输,要借助下面的这两个函数
staticinlinelongcopy_from_user(void*to,constvoid__user*from,unsignedlongn)staticinlinelongcopy_to_user(void__user*to,constvoid*from,unsignedlongn)to:目标地址from:源地址n:将要拷贝数据的字节数返回值:成功返回0,失败返回没有拷贝成功的数据字节数
5.字符设备最基本框架
加载驱动模块后,需手动创建驱动节点文件
mknod/dev/chrdevbasec20006.2新字符设备基本驱动框架
上面的驱动框架,当使用modprobe加载驱动程序以后还需要使用命令mknod手动创建设备节点。
在Linux下通过udev(用户空间程序)来实现设备文件的创建与删除,但是在嵌入式Linux中使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理。
1.设备文件系统
设备文件系统有devfs,mdev,udev这三种
devfs,一个基于内核的动态设备文件系统
devfs缺点(过时原因)
udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
udev和devfs的区别
2.申请设备号
上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号。
intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name)dev:保存申请到的设备号。
baseminor:次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0
count:要申请的设备号数量。
name:设备名字。
intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name);from-要申请的起始设备号
count-设备号个数
name-设备号在内核中的名称
返回0申请成功,否则失败
voidunregister_chrdev_region(dev_tfrom,unsignedcount)from:要释放的设备号。count:表示从from开始,要释放的设备号数量。
//创建设备号if(newchrled.major)//定义了设备号就静态申请{newchrled.devid=MKDEV(newchrled.major,0);register_chrdev_region(newchrled.devid,NEWCHRLED_CNT,NEWCHRLED_NAME);}else//没有定义设备号就动态申请{alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME);//申请设备号newchrled.major=MAJOR(newchrled.devid);//获取分配号的主设备号newchrled.minor=MINOR(newchrled.devid);//获取分配号的次设备号}3.注册字符设备
在Linux中使用cdev结构体表示一个字符设备
structcdev{structkobjectkobj;structmodule*owner;conststructfile_operations*ops;//操作函数集合structlist_headlist;dev_tdev;//设备号unsignedintcount;};在cdev中有两个重要的成员变量:ops和dev,字符设备文件操作函数集合file_operations以及设备号dev_t。
voidcdev_init(structcdev*cdev,conststructfile_operations*fops);例
structcdevtestcdev;//设备操作函数staticstructfile_operationstest_fops={.owner=THIS_MODULE,//其他具体的初始项};testcdev.owner=THIS_MODULE;//初始化cdev结构体变量cdev_init(&testcdev,&test_fops);cdev_add函数用于向Linux系统添加字符设备(cdev结构体变量),首先使用cdev_init函数完成对cdev结构体变量的初始化,然后使用cdev_add函数向Linux系统添加这个字符设备。
将cdev添加到内核同时绑定设备号。
其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作
intcdev_add(structcdev*p,dev_tdev,unsignedcount)p-要添加的cdev结构
dev-绑定的起始设备号
cdev_add(&testcdev,devid,1);//添加字符设备卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备
voidcdev_del(structcdev*p);p-要添加的cdev结构
cdev_del(&testcdev);//删除cdev4.自动创建设备节点
在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。
创建一个class类
structclass*class_create(structmodule*owner,constchar*name);删除一个class类
voidclass_destroy(structclass*cls);//cls要删除的类创建设备
还需要在类下创建一个设备,使用device_create函数在类下面创建设备。
成功会在/dev目录下生成设备文件。
structdevice*device_create(structclass*class,structdevice*parent,dev_tdevt,void*drvdata,constchar*fmt,...)*class——设备类指针,
*parent——父设备指针,
devt——设备号,
*drvdata——额外数据,
*fmt——设备文件名
删除设备
卸载驱动的时候需要删除掉创建的设备
voiddevice_destroy(structclass*class,dev_tdevt);class——设备所处的类devt——设备号
5.文件私有数据
/*newchrled设备结构体*/structnewchrled_dev{dev_tdevid;/*设备号*/structcdevcdev;/*cdev*/structclass*class;/*类*/structdevice*device;/*设备*/intmajor;/*主设备号*/intminor;/*次设备号*/};structnewchrled_devnewchrled;/*led设备*//**@description:打开设备*@param-inode:传递给驱动的inode*@param-filp:设备文件,file结构体有个叫做private_data的成员变量*一般在open的时候将private_data指向设备结构体。*@return:0成功;其他失败*/staticintled_open(structinode*inode,structfile*filp){filp->private_data=&newchrled;/*设置私有数据*/return0;}staticssize_tled_read(structfile*filp,char__user*buf,size_tcnt,loff_t*offt){structnewchrled_dev*dev=(structnewchrled_dev*)filp->private_data;return0;}6.新字符设备驱动程序框架
Linux内核提供了pinctrl子系统和gpio子系统用于GPIO驱动。
7.1pinctrl子系统主要工作内容:
7.2pinctrl的设备树设置
在设备树里面创建一个节点来描述PIN的配置信息。pinctrl子系统一般在iomuxc子节点下,所有需要配置用户自己的pinctrl需要在该节点下添加。例
iomuxc:iomuxc@020e0000{compatible="fsl,imx6ul-iomuxc";reg=<0x020e00000x4000>;pinctrl-names="default";pinctrl-0=<&pinctrl_hog_1>;imx6ul-evk{pinctrl_hog_1:hoggrp-1{fsl,pins=
pinctrl_hog_1子节点所使用的PIN配置信息,如UART1_RTS_B的配置信息
UART1_RTS_B这个PIN是作为SD卡的检测引脚
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19,这是一个宏定义,表示将UART1_RTS_B这个IO复用为GPIO1_IO19(复用属性)
0x17059就是conf_reg寄存器值,设置一个IO的上/下拉、驱动能力和速度(电气属性)
7.3设备树中添加pinctrl模板
1.添加pinctrl设备结点
同一个外设的PIN都放到一个节点里面,在iomuxc节点中下添加“pinctrl_test”节点。节点前缀一定要为“pinctrl_”。
设备树是通过属性来保存信息的,因此需要添加一个属性,属性名字一定要为**fsl,pins**
&iomuxc{pinctrl-names="default";pinctrl-0=<&pinctrl_hog_1>;imx6ul-evk{......pinctrl_led:ledgrp{fsl,pins=
test{pinctrl-names="default","wakeup";pinctrl-0=<&pinctrl_test>;pinctrl-1=<&pinctrl_test_2>;/*其他节点内容*/};例
gpioled{#address-cells=<1>;#size-cells=<1>;compatible="songwei-gpioled";pinctrl-names="default";pinctrl-0=<&pinctrl_led>;led-gpio=<&gpio13GPIO_ACTIVE_LOW>;status="okay";};八、GPIO子系统
当使用pinctrl子系统将引脚的复用设置为GPIO,可以使用GPIO子系统来操作GPIO
8.1GPIO子系统工作内容
通过GPIO子系统功能要实现:
8.2GPIO子系统设备树设置
在具体设备节点中添加GPIO信息
gpioled{#address-cells=<1>;#size-cells=<1>;compatible="songwei-gpioled";pinctrl-names="default";pinctrl-0=<&pinctrl_led>;//gpio信息led-gpio=<&gpio13GPIO_ACTIVE_LOW>;status="okay";};8.3API函数
1.gpio_request
gpio_request函数用于申请一个GPIO管脚,在使用一个GPIO之前一定要使用gpio_request进行申请。
intgpio_request(unsignedgpio,constchar*label)2.gpio_free
如果不使用某个GPIO,需要调用gpio_free函数进行释放。
voidgpio_free(unsignedgpio);//gpio:要释放的gpio标号。3.gpio_direction_input
设置某个GPIO为输入
intgpio_direction_input(unsignedgpio)//gpio:要设置为输入的GPIO标号。4.gpio_direction_output
设置某个GPIO为输出,并且设置默认输出值。
intgpio_direction_output(unsignedgpio,intvalue)5.gpio_get_value
获取某个GPIO的值(0或1)
#definegpio_get_value__gpio_get_valueint__gpio_get_value(unsignedgpio)6.gpio_set_value
设置某个GPIO的值
1.of_gpio_named_count
获取设备树某个属性里面定义了几个GPIO信息。
intof_gpio_named_count(structdevice_node*np,constchar*propname)2.of_gpio_count
此函数统计的是gpios属性的GPIO数量,而of_gpio_named_count函数可以统计任意属性的GPIO信息
intof_gpio_count(structdevice_node*np)3.of_get_named_gpio
获取GPIO编号,在Linux内核中关于GPIO的API函数都要使用GPIO编号,此函数会将设备树中类似<&gpio57GPIO_ACTIVE_LOW>的属性信息转换为对应的GPIO编号。
intof_get_named_gpio(structdevice_node*np,constchar*propname,intindex)8.5pinctrl和gpio子系统使用程序框架
intret=0;/*1、获取设备节点:alphaled*/gpio_led.nd=of_find_node_by_path("/gpioled");if(gpio_led.nd==NULL){printk("songwei_lednodecannotfound!\r\n");return-EINVAL;}else{printk("songwei_lednodehasbeenfound!\r\n");}/*2、获取设备树中的gpio属性,得到LED所使用的LED编号*/gpio_led.led_gpio=of_get_named_gpio(gpio_led.nd,"led-gpio",0);if(gpio_led.led_gpio<0){printk("can'tgetled-gpio");return-EINVAL;}printk("led-gpionum=%d\r\n",gpio_led.led_gpio);/*3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯*/ret=gpio_direction_output(gpio_led.led_gpio,1);if(ret<0){printk("can'tsetgpio!\r\n");}九、内核并发与竞争
9.1并发与竞争概念
Linux系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。
并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。
竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。
临界资源:多个进程访问的资源,共享数据段
临界区:多个进程访问的代码段
9.2原子操作
原子操作是指不能再进一步分割的操作。一般原子操作用于整形变量或者位操作。
Linux内核定义了叫做atomic_t的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h文件中
typedefstruct{intcounter;}atomic_t;如果要使用原子操作API函数,首先要先定义一个atomic_t的变量,
atomic_ta;//定义aatomic_tb=ATOMIC_INIT(0);//定义原子变量b并赋初值为0原子操作API函数
原子位操作API函数
/*gpioled设备结构体*/structgpioled_dev{......intled_gpio;/*led所使用的GPIO编号*/atomic_tlock;/*原子变量*/};structgpioled_devgpioled;/*led设备*/staticintled_open(structinode*inode,structfile*filp){/*通过判断原子变量的值来检查LED有没有被别的应用使用*/if(!atomic_dec_and_test(&gpioled.lock)){atomic_inc(&gpioled.lock);/*小于0的话就加1,使其原子变量等于0*/return-EBUSY;/*LED被使用,返回忙*/}......}staticintled_release(structinode*inode,structfile*filp){structgpioled_dev*dev=filp->private_data;/*关闭驱动文件的时候释放原子变量*/atomic_inc(&dev->lock);return0;}staticint__initled_init(void){intret=0;/*初始化原子变量*/atomic_set(&gpioled.lock,1);/*原子变量初始值为1*/......return0;}staticvoid__exitled_exit(void){......}9.2自旋锁
对于自旋锁而言,如果自旋锁正在被线程A持有,线程B想要获取自旋锁,那么线程B就会处于忙循环-旋转-等待状态,线程B不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。
/*gpioled设备结构体*/structgpioled_dev{......intdev_stats;/*设备使用状态,0,设备未使用;>0,设备已经被使用*/spinlock_tlock;/*自旋锁*/};structgpioled_devgpioled;/*led设备*/staticintled_open(structinode*inode,structfile*filp){......spin_lock_irqsave(&gpioled.lock,flags);/*上锁*/if(gpioled.dev_stats){/*如果设备被使用了*/spin_unlock_irqrestore(&gpioled.lock,flags);/*解锁*/return-EBUSY;}gpioled.dev_stats++;/*如果设备没有打开,那么就标记已经打开了*/spin_unlock_irqrestore(&gpioled.lock,flags);/*解锁*/return0;}staticintled_release(structinode*inode,structfile*filp){....../*关闭驱动文件的时候将dev_stats减1*/spin_lock_irqsave(&dev->lock,flags);/*上锁*/if(dev->dev_stats){dev->dev_stats--;}spin_unlock_irqrestore(&dev->lock,flags);/*解锁*/return0;}/**@description:驱动入口函数*@param:无*@return:无*/staticint__initled_init(void){intret=0;/*初始化自旋锁*/spin_lock_init(&gpioled.lock);......}/**@description:驱动出口函数*@param:无*@return:无*/staticvoid__exitled_exit(void){......}9.3信号量
Linux内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
/*gpioled设备结构体*/structgpioled_dev{......structsemaphoresem;/*信号量*/};structgpioled_devgpioled;/*led设备*/staticintled_open(structinode*inode,structfile*filp){....../*获取信号量*/if(down_interruptible(&gpioled.sem)){/*获取信号量,进入休眠状态的进程可以被信号打断*/return-ERESTARTSYS;}#if0down(&gpioled.sem);/*不能被信号打断*/#endifreturn0;}staticintled_release(structinode*inode,structfile*filp){......up(&dev->sem);/*释放信号量,信号量值加1*/return0;}/*设备操作函数*/staticstructfile_operationsgpioled_fops={......};staticint__initled_init(void){intret=0;/*初始化信号量*/sema_init(&gpioled.sem,1);......}/**@description:驱动出口函数*@param:无*@return:无*/staticvoid__exitled_exit(void){......}9.4互斥体
Linux提供了专门的互斥体mutex(等效信号量为1)。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体(死锁)。在Linux驱动的时遇到需要互斥访问的地方一般使用mutex。
/*gpioled设备结构体*/structgpioled_dev{......structmutexlock;/*互斥体*/};structgpioled_devgpioled;/*led设备*/staticintled_open(structinode*inode,structfile*filp){....../*获取互斥体,可以被信号打断*/if(mutex_lock_interruptible(&gpioled.lock)){return-ERESTARTSYS;}#if0mutex_lock(&gpioled.lock);/*不能被信号打断*/#endifreturn0;}staticintled_release(structinode*inode,structfile*filp){....../*释放互斥锁*/mutex_unlock(&dev->lock);return0;}/*设备操作函数*/staticstructfile_operationsgpioled_fops={......};staticint__initled_init(void){....../*初始化互斥体*/mutex_init(&gpioled.lock);......}staticvoid__exitled_exit(void){......}十、内核定时器
节拍率
系统定时器频率是通过静态预处理定义的(HZ),在系统启动时按照Hz对硬件进行设置。一般ARM体系结构的节拍率多数都等于100。
在编译Linux内核的时候可以通过图形化界面设置系统节拍率,按照如下路径打开配置界面:
->KernelFeatures
->Timerfrequency([=y])
Linux内核会使用CONFIG_HZ来设置自己的系统时钟。
#undefHZ#defineHZCONFIG_HZ#defineUSER_HZ100#defineCLOCKS_PER_SEC(USER_HZ)宏HZ就是CONFIG_HZ,HZ=100,后面编写Linux驱动的时候会常常用到HZ,因为HZ表示一秒的节拍数,也就是频率。
高节拍率:优点
高节拍率:缺点
jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,每次时钟中断处理程序都会增加该变量的值。
当jiffies变量的值超过它的最大存放范围后就会发生溢出,对于32位无符号长整型,最大取值为2^32-1,在溢出前,定时器节拍计数最大为4294967295,如果节拍数达到了最大值后还要继续增加的话,它的值会回绕到0。
10.2内核定时器
内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux内核使用timer_list结构体表示内核定时器
例驱动层
/*命令值*/#defineCLOSE_CMD(_IO(0XEF,0x1))/*关闭定时器*/#defineOPEN_CMD(_IO(0XEF,0x2))/*打开定时器*/#defineSETPERIOD_CMD(_IO(0XEF,0x3))/*设置定时器周期命令*/intmain(intargc,char*argv[]){intfd,ret;char*filename;unsignedintcmd;unsignedintarg;unsignedcharstr[100];if(argc!=2){printf("ErrorUsage!\r\n");return-1;}filename=argv[1];fd=open(filename,O_RDWR);if(fd<0){printf("Can'topenfile%s\r\n",filename);return-1;}while(1){printf("InputCMD:");ret=scanf("%d",&cmd);if(ret!=1){/*参数输入错误*/gets(str);/*防止卡死*/}if(cmd==1)/*关闭LED灯*/cmd=CLOSE_CMD;elseif(cmd==2)/*打开LED灯*/cmd=OPEN_CMD;elseif(cmd==3){cmd=SETPERIOD_CMD;/*设置周期值*/printf("InputTimerPeriod:");ret=scanf("%d",&arg);if(ret!=1){/*参数输入错误*/gets(str);/*防止卡死*/}}ioctl(fd,cmd,arg);/*控制定时器的打开和关闭*/}close(fd);}十一、设备控制接口(ioctl)
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。有些命令是实在找不到对应的操作函数,拓展一些file_operations给出的接口中没有的自定义功能,则需要使用到ioctl()函数。一些没办法归类的函数就统一放在ioctl这个函数操作中,通过指定的命令来实现对应的操作。
11.1应用层
需要规定一些命令码,这些命令码在应用程序和驱动程序中需要保持一致。应用程序只需向驱动程序下发一条指令码,用来通知它执行哪条命令。
#include
#include
ioctl函数的第二个参数cmd为用户与驱动的协议,理论上可以为任意int型数据,,但是为了确保该协议的唯一性,ioctl命令应该使用更科学严谨的方法赋值,在linux中,提供了一种ioctl命令的统一格式,将32位int型数据划分为四个位段,如下图所示:
在内核中,提供了宏接口以生成上述格式的ioctl命令:
#include/uapi/asm-generic/ioctl.h#define_IOC(dir,type,nr,size)\(((dir)<<_IOC_DIRSHIFT)|\((type)<<_IOC_TYPESHIFT)|\((nr)<<_IOC_NRSHIFT)|\((size)<<_IOC_SIZESHIFT))宏_IOC()衍生的接口来直接定义ioctl命令
#include/uapi/asm-generic/ioctl.h/*usedtocreatenumbers*/#define_IO(type,nr)_IOC(_IOC_NONE,(type),(nr),0)#define_IOR(type,nr,size)_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))#define_IOW(type,nr,size)_IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))#define_IOWR(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))内核还提供了反向解析ioctl命令的宏接口:
//include/uapi/asm-generic/ioctl.h/*usedtodecodeioctlnumbers*/#define_IOC_DIR(nr)(((nr)>>_IOC_DIRSHIFT)&_IOC_DIRMASK)#define_IOC_TYPE(nr)(((nr)>>_IOC_TYPESHIFT)&_IOC_TYPEMASK)#define_IOC_NR(nr)(((nr)>>_IOC_NRSHIFT)&_IOC_NRMASK)#define_IOC_SIZE(nr)(((nr)>>_IOC_SIZESHIFT)&_IOC_SIZEMASK)十二、中断机制
中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。
12.1中断API函数
获取中断号函数
每个中断都有一个中断号,通过中断号即可区分不同的中断。在Linux内核中使用一个int变量表示中断号,
或者中断号,中断信息一般写到了设备树里面,可以通过irq_of_parse_and_map函数从interupts属性中提取到对应的设备号。
unsignedintirq_of_parse_and_map(structdevice_node*dev,intindex)使用GPIO的话,可以使用gpio_to_irq函数来获取gpio对应的中断号
intgpio_to_irq(unsignedintgpio)申请中断函数
Linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断,request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用request_irq函数。
request_irq函数会激活(使能)中断,所以不需要手动去使能中断。
intrequest_irq(unsignedintirq,irq_handler_thandler,unsignedlongflags,constchar*name,void*dev)中断标志
中断释放函数
中断使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,free_irq会删除中断处理函数并且禁止中断。
voidfree_irq(unsignedintirq,void*dev)中断处理函数
使用request_irq函数申请中断的时候需要设置中断处理函数
irqreturn_t(*irq_handler_t)(int,void*)irqreturn_t类型定义如下所示:
enumirqreturn{IRQ_NONE=(0<<0),IRQ_HANDLED=(1<<0),IRQ_WAKE_THREAD=(1<<1),};typedefenumirqreturnirqreturn_t;irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式
returnIRQ_RETVAL(IRQ_HANDLED)中断使能和禁止函数
enable_irq和disable_irq用于使能和禁止指定的中断。
voidenable_irq(unsignedintirq)voiddisable_irq(unsignedintirq)voiddisable_irq_nosync(unsignedintirq)使能/关闭全局中断
local_irq_enable()local_irq_disable()在打开或者关闭全局中断时,要考虑到别的任务的感受,要保存中断状态,处理完后要将中断状态恢复到以前的状态
local_irq_save(flags)local_irq_restore(flags)12.2中断的上下部
为保证系统实时性,中断服务程序必须足够简短,如果都在中断服务程序中完成,则会严重降低中断的实时性,
所以,linux系统提出了一个概念:把中断服务程序分为两部分:上半部-下半部。主要目的就是实现中断处理函数的快进快出
中断服务程序分为上半部(tophalf)和下半部(bottomhalf),上半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。
上半部只能通过中断处理程序实现,下半部的实现目前有3种实现方式,分别为:软中断、tasklet、工作队列(workqueues)
(1)软中断
Linux内核使用结构体softirq_action表示软中断
structsoftirq_action{void(*action)(structsoftirq_action*);};在kernel/softirq.c文件中一共定义了10个软中断
staticstructsoftirq_actionsoftirq_vec[NR_SOFTIRQS];NR_SOFTIRQS是枚举类型
enum{HI_SOFTIRQ=0,/*高优先级软中断*/TIMER_SOFTIRQ,/*定时器软中断*/NET_TX_SOFTIRQ,/*网络数据发送软中断*/NET_RX_SOFTIRQ,/*网络数据接收软中断*/BLOCK_SOFTIRQ,BLOCK_IOPOLL_SOFTIRQ,TASKLET_SOFTIRQ,/*tasklet软中断*/SCHED_SOFTIRQ,/*调度软中断*/HRTIMER_SOFTIRQ,/*高精度定时器软中断*/RCU_SOFTIRQ,/*RCU软中断*/NR_SOFTIRQS};一共有10个软中断,数组softirq_vec有10个元素。softirq_action结构体中的action成员变量就是软中断的服务函数。
数组softirq_vec是个全局数组,因此所有的CPU(对于SMP系统而言)都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个CPU所执行的软中断服务函数确是相同的,都是数组softirq_vec中定义的action函数。
要使用软中断,必须先使用open_softirq函数注册对应的软中断处理函数
voidopen_softirq(intnr,void(*action)(structsoftirq_action*))//nr:要开启的软中断,也就是上面的10个软中断//action:软中断对应的处理函数注册好软中断以后需要通过raise_softirq函数触发
voidraise_softirq(unsignedintnr)//nr:要触发的软中断(2)tasklet
tasklet是通过软中断实现的,软中断用轮询的方式处理,假如是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。
为了提高中断处理数量,改进处理效率,产生了tasklet机制。tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。
tasklet机制的优点:
Linux内核使用tasklet_struct结构体来表示tasklet
structtasklet_struct{structtasklet_struct*next;/*下一个tasklet*/unsignedlongstate;/*tasklet状态*/atomic_tcount;/*计数器,记录对tasklet的引用数*/void(*func)(unsignedlong);/*tasklet执行的函数*/unsignedlongdata;/*函数func的参数*/};如果要使用tasklet,必须先定义一个tasklet,然后使用tasklet_init函数初始化tasklet
voidtasklet_init(structtasklet_struct*t,void(*func)(unsignedlong),unsignedlongdata);使用宏DECLARE_TASKLET一次性完成tasklet的定义和初始化,DECLARE_TASKLET定义在include/linux/interrupt.h文件中
voidtasklet_schedule(structtasklet_struct*t)杀死tasklet使用tasklet_kill函数,这个函数会等待tasklet执行完毕,然后再将它移除。该函数可能会引起休眠,所以要禁止在中断上下文中使用。
tasklet_kill(structtasklet_struct*t)tasklet模板
/*定义taselet*/structtasklet_structtesttasklet;/*tasklet处理函数*/voidtesttasklet_func(unsignedlongdata){/*tasklet具体处理内容*/}/*中断处理函数*/irqreturn_ttest_handler(intirq,void*dev_id){....../*调度tasklet*/tasklet_schedule(&testtasklet);......}/*驱动入口函数*/staticint__initxxxx_init(void){....../*初始化tasklet*/tasklet_init(&testtasklet,testtasklet_func,data);/*注册中断处理函数*/request_irq(xxx_irq,test_handler,0,"xxx",&xxx_dev);......}(3)工作队列(workqueue)
工作队列(workqueue)是实现中断下文的机制之一,是一种将工作推后执行的形式。
工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度
工作队列tasklet机制有什么不同呢?
Linux内核使用work_struct结构体表示一个工作
structwork_struct{atomic_long_tdata;structlist_headentry;work_func_tfunc;/*工作队列处理函数*/};这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示
structworkqueue_struct{structlist_headpwqs;structlist_headlist;structmutexmutex;intwork_color;intflush_color;atomic_tnr_pwqs_to_flush;structwq_flusher*first_flusher;structlist_headflusher_queue;structlist_headflusher_overflow;structlist_headmaydays;structworker*rescuer;intnr_drainers;intsaved_max_active;structworkqueue_attrs*unbound_attrs;structpool_workqueue*dfl_pwq;charname[WQ_NAME_LEN];structrcu_headrcu;unsignedintflags____cacheline_aligned;structpool_workqueue__percpu*cpu_pwqs;structpool_workqueue__rcu*numa_pwq_tbl[];};linux内核使用工作者线程(workerthread)来处理工作队列中的各个工作,Linux内核使用worker结构体表示工作者线程
每个worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。
structworker{union{structlist_headentry;structhlist_nodehentry;};structwork_struct*current_work;work_func_tcurrent_func;structpool_workqueue*current_pwq;booldesc_valid;structlist_headscheduled;structtask_struct*task;structworker_pool*pool;structlist_headnode;unsignedlonglast_active;unsignedintflags;intid;chardesc[WORKER_DESC_LEN];structworkqueue_struct*rescue_wq;};初始化工作:INIT_WORK
#defineINIT_WORK(_work,_func)工作的创建和初始化:DECLARE_WORK
#defineDECLARE_WORK(n,f)工作的调度函数:schedule_work
boolschedule_work(structwork_struct*work)工作队列模块
/*定义工作(work)*/structwork_structtestwork;/*work处理函数*/voidtestwork_func_t(structwork_struct*work);{/*work具体处理内容*/}/*中断处理函数*/irqreturn_ttest_handler(intirq,void*dev_id){....../*调度work*/schedule_work(&testwork);......}/*驱动入口函数*/staticint__initxxxx_init(void){....../*初始化work*/INIT_WORK(&testwork,testwork_func_t);/*注册中断处理函数*/request_irq(xxx_irq,test_handler,0,"xxx",&xxx_dev);......}12.3设备树中的中断节点
如果一个设备需要用到中断功能,需要在设备树中配置好中断属性信息,因为设备树是用来描述硬件信息的,然后Linux内核通过设备树配置的中断属性来配置中断功能。
例:imx6ull中断控制器节点
intc:interrupt-controller@00a01000{compatible="arm,cortex-a7-gic";#interrupt-cells=<3>;interrupt-controller;reg=<0x00a010000x1000>,<0x00a020000x100>;};gpio5:gpio@020ac000{compatible="fsl,imx6ul-gpio","fsl,imx35-gpio";reg=<0x020ac0000x4000>;interrupts=
②interrupt-controller节点为空,表示当前节点是中断控制器。
③interrupts:描述中断源信息,对于gpio5来说一共有两条信息:中断类型是SPI,触发电平是IRQ_TYPE_LEVEL_HIGH,中断源一个是74,一个是75
④interrupt-parent,指定父中断,也就是中断控制器。
十三、阻塞与非阻塞IO
13.1阻塞与非阻塞IO原理
这里的IO指的是Input/Output(输入/输出):是应用程序对驱动设备的输入/输出操作。
阻塞IO
阻塞IO操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。
被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足该进程会唤醒。
在阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源让给其他进程。因为阻塞的进程会进入休眠状态,所以必须确保有一个地方能够唤醒休眠的进程。唤醒进程最大可能发生在中断函数里面,因为在硬件资源获得的同时往往伴随着一个中断。Linux内核提供了等待队列(waitqueue)来实现阻塞进程的唤醒工作。
如图,应用程序调用read函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
intfd;intdata=0;fd=open("/dev/xxx_dev",O_RDWR);/*阻塞方式打开,默认是阻塞*/ret=read(fd,&data,sizeof(data));/*读取数据*/非阻塞IO
非阻塞IO操作是在不能进行设备操作时,并不挂起,要么放弃,要么不停地查询,直至可以进行操作为止。非阻塞的进程则不断尝试,直到可以进行I/O。
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,一直往复循环,直到数据读取成功。
若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作应立即返回,read()、write()等系统调用也随即被返回,应用程序收到-EAGAIN返回值。
intfd;intdata=0;fd=open("/dev/xxx_dev",O_RDWR|O_NONBLOCK);/*非阻塞方式打开*/ret=read(fd,&data,sizeof(data));/*读取数据*/13.2阻塞IO使用
应用层(默认打开)
fd=open(filename,O_RDWR);if(fd<0){printf("Can'topenfile%s\r\n",filename);return-1;}ret=read(fd,&data,sizeof(data));if(ret<0){printf("don'tknowhowtoread\n");/*数据读取错误或者无效*/}else{/*数据读取正确*/if(data)/*读取到数据*/printf("keyvalue=%#X\r\n",data);}驱动层(等待队列)
在Linux驱动程序中,使用等待队列(WaitQueue)来实现阻塞进程的唤醒。
Linux内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。
它有两种数据结构:等待队列头(wait_queue_head_t)和等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个list_head类型的域作为连接件。它通过一个双链表把等待task的头和等待的进程列表链接起来。
等待队列头
如果要在驱动中使用等待队列,必须创建并初始化一个等待队列头。等待队列头使用结构体wait_queue_head_t来表示
struct__wait_queue_head{spinlock_tlock;//自旋锁structlist_headtask_list;//链表头};typedefstruct__wait_queue_headwait_queue_head_t;定义好等待队列头以后需要初始化,使用init_waitqueue_head函数初始化等待队列头。
voidinit_waitqueue_head(wait_queue_head_t*q)//q:要初始化的等待队列头等待队列项
每个访问设备的进程都是一个队列项,当设备不可用时就要将这些进程对应的等待队列项添加到等待队列里面。结构体wait_queue_t表示等待队列项
struct__wait_queue{unsignedintflags;void*private;wait_queue_func_tfunc;structlist_headtask_list;};typedefstruct__wait_queuewait_queue_t;使用宏DECLARE_WAITQUEUE定义并初始化一个等待队列项
DECLARE_WAITQUEUE(name,tsk)//name:等待队列项的名字//tsk:等待队列项属于哪个任务(进程),一般设置为current在内核中current相当于全局变量,表示当前进程。所以DECLARE_WAITQUEUE是给当前正在运行的进程创建并初始化了一个等待队列项。
添加/删除队列
当设备不可访问的时就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。
voidadd_wait_queue(wait_queue_head_t*q,wait_queue_t*wait)//q:等待队列项要加入的等待队列头//wait:要加入的等待队列项当设备可以访问后再将进程对应的等待队列项从等待队列头中移除即可。
voidremove_wait_queue(wait_queue_head_t*q,wait_queue_t*wait)//q:要删除的等待队列项所处的等待队列头//wait:要删除的等待队列项唤醒等待睡眠进程
当设备可以使用的时就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数
voidwake_up(wait_queue_head_t*q)//功能:唤醒所有休眠进程voidwake_up_interruptible(wait_queue_head_t*q)//功能:唤醒可中断的休眠进程//q:要唤醒的等待队列头这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up函数可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程,
wake_up_interruptible函数只能唤醒处于TASK_INTERRUPTIBLE状态的进程
等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。
调用的时要确认condition值是真还是假,如果调用condition为真,则不会休眠。
使用模板
在设备结构体中添加一个等待队列头r_wait,因为在Linux驱动中处理阻塞IO需要用到等待队列。
调用init_waitqueue_head函数初始化等待队列头r_wait。
read驱动函数手动休眠等待按键按下
法一:
法二:
定时器中断处理函数执行,表示有按键按下,先在判断一下是否是一次有效的按键,如果是的话就通过wake_up或者wake_up_interruptible函数来唤醒等待队列r_wait。
完成read函数后,设置任务为运行态,将等待队列移除
13.3非阻塞IO使用(轮询)
应用层
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,即轮询。poll、epoll和select可以用于处理轮询。
应用程序通过select、epoll或poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用select、epoll或poll函数的时,设备驱动程序中的poll函数就会执行,因此需要在设备驱动程序中编写poll函数。
1.select
intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout)nfds:所要监视的这三类文件描述集合,最大文件描述符加1
readfds、writefds和exceptfds:指向描述符集合。指明了关心哪些描述符.这三个参数都是fd_set类型的,fd_set类型变量的每一个位都代表了一个文件描述符。
返回值:0:超时发生,没有文件描述符可以进行操作;-1:发生错误;其他值:可以进行操作的文件描述符个数
从一个设备文件中读取数据,要定义一个fd_set变量,这个变量要传递给参数readfds。当定义好一个fd_set变量以后可以使用如下所示几个宏进行操作:
structtimeval{longtv_sec;/*秒*/longtv_usec;/*微妙*/};应用层select函数非阻塞访问模块
在单个线程中,select函数能够监视的文件描述符数量有最大的限制,一般为1024。可以修改内核将监视的文件描述符数量改大。这时可以使用poll函数,poll函数本质上和select没有太大的差别,但是poll函数没有最大文件描述符限制。
intpoll(structpollfd*fds,nfds_tnfds,inttimeout)pollfd结构体
structpollfd{intfd;/*文件描述符*/shortevents;/*请求的事件*/shortrevents;/*返回的事件*/};fd是要监视的文件描述符,如果fd无效,则events监视事件也无效,并且revents返回0。
events是要监视的事件,可监视的事件类型如下:
revents是返回的事件,由Linux内核设置具体的返回事件。
应用层poll函数非阻塞访问模块
voidmain(void){intret;intfd;/*要监视的文件描述符*/structpollfdfds;fd=open(filename,O_RDWR|O_NONBLOCK);/*非阻塞式访问*//*构造结构体*/fds.fd=fd;fds.events=POLLIN;/*监视数据是否可以读取*/ret=poll(&fds,1,500);/*轮询文件是否可操作,超时500ms*/if(ret)/*数据有效*/{....../*读取数据*/......}elseif(ret==0)/*超时*/{......}elseif(ret<0)/*错误*/{......}}3.epoll
epoll就是为处理大并发而准备的,一般常常在网络编程中使用epoll函数。
应用程序需要先使用epoll_create函数创建一个epoll句柄
intepoll_create(intsize)epoll句柄创建成功以后使用epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件
intepoll_ctl(intepfd,intop,intfd,structepoll_event*event)epfd:要操作的epoll句柄,使用epoll_create函数创建的epoll句柄
op:要对epoll句柄进行的操作,可以设置为:
fd:要监视的文件描述符
event:要监视的事件类型,为epoll_event结构体类型指针
返回值:0:成功;-1:失败,并且设置errno的值为相应的错误码。
监视的事件类型为epoll_event结构体类型指针
structepoll_event{uint32_tevents;/*epoll事件*/epoll_data_tdata;/*用户数据*/};events表示要监视的事件,可选的事件如下
最后通过epoll_wait函数来等待事件的发生
intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout)epoll更多的是用在大规模的并发服务器上,因为在这种场合下select和poll并不适合。当设计到的文件描述符比较少的时候就适合用selcet和poll。
驱动层
当应用程序调用select或poll函数来对驱动程序进行非阻塞访问,驱动程序file_operations操作集中的poll函数就会执行。
unsignedint(*poll)(structfile*filp,structpoll_table_struct*wait)filp:要打开的设备文件(文件描述符)
wait:poll_table_struct类型指针,由应用程序传递进来的,将此参数传递给poll_wait()
返回值:向应用程序返回设备或者资源状态,返回状态有:
需要在驱动程序的poll函数中调用poll_wait函数,poll_wait函数不会引起阻塞,只是将应用程序添加到poll_table中
voidpoll_wait(structfile*filp,wait_queue_head_t*wait_address,poll_table*p驱动层poll函数模板(和应用层select、poll对应)
阻塞IO和非阻塞IO都需要应用程序主动去查询设备的使用情况。Linux提供了异步通知机制,驱动程序能主动向应用程序发出通知。
信号是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式通知可以访问,应用程序获取到信号后就可以从驱动设备中读取或者写入数据。
14.1异步通知应用程序
1.注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用signal函数来设置信号的处理函数。
sighandler_tsignal(intsignum,sighandler_thandler)信号中断处理函数
typedefvoid(*sighandler_t)(int)在处理函数执行相应的操作即可。
2.将本应用程序的进程号告诉给内核
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
#include
3.开启异步通知
主要是通过fcntl函数设置进程状态为FASYNC,经过这一步,驱动程序中的fasync函数就会执行。
flags=fcntl(fd,F_GETFL);/*获取当前的进程状态*/fcntl(fd,F_SETFL,flags|FASYNC);/*开启当前进程异步通知功能*/4.应用程序模板
staticintfd=0;/*文件描述符*/staticvoidsigio_signal_func(intsignum){interr=0;unsignedintkeyvalue=0;err=read(fd,&keyvalue,sizeof(keyvalue));if(err<0){/*读取错误*/}else{printf("sigiosignal!keyvalue=%d\r\n",keyvalue);}}intmain(intargc,char*argv[]){intflags=0;char*filename;if(argc!=2){printf("ErrorUsage!\r\n");return-1;}filename=argv[1];fd=open(filename,O_RDWR);if(fd<0){printf("Can'topenfile%s\r\n",filename);return-1;}/*设置信号SIGIO的处理函数*/signal(SIGIO,sigio_signal_func);fcntl(fd,F_SETOWN,getpid());/*设置当前进程接收SIGIO信号*/flags=fcntl(fd,F_GETFL);/*获取当前的进程状态*/fcntl(fd,F_SETFL,flags|FASYNC);/*设置进程启用异步通知功能*/while(1){sleep(2);}close(fd);return0;}14.2异步通知驱动程序
1.内核要使用异步通知需要在驱动程序中定义一个fasync_struct结构体的指针变量一般将fasync_struct结构体指针变量定义到设备结构体中即可。
int(*fasync)(intfd,structfile*filp,inton)3.fasync函数里面一般通过调用fasync_helper函数来初始化前面定义的fasync_struct结构体指针
intfasync_helper(intfd,structfile*filp,inton,structfasync_struct**fapp)fasync_helper函数的前三个参数就是fasync函数的三个参数,第四个参数就是要初始化的fasync_struct结构体指针变量。
当应用程序通过fcntl(fd,F_SETFL,flags|FASYNC)改变fasync标记的时,驱动程序file_operations操作集中的fasync函数就会执行。
4.当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生中断。kill_fasync函数负责发送指定的信号
voidkill_fasync(structfasync_struct**fp,intsig,intband)5.最后,在关闭驱动文件的时候需要在file_operations操作集中的release函数中释放fasync_struct,fasync_struct的释放函数为fasync_helper。
xxx_fasync(-1,filp,0);/*删除异步通知*/xxx_fasync函数就是file_operations操作集中的fasync函数。
6.驱动程序模板
15.1设备驱动的分层思想
Linux内核完全由C语言和汇编语言写成,但是却频繁用到了面向对象的设计思想。
在设备驱动方面,为同类的设备设计了一个框架,框架中的核心层则实现了该设备通用的一些功能。同样的,如果具体的设备不想使用核心层的函数,它可以重载之。
return_typecore_funca(xxx_device*bottom_dev,param1_typeparam1,param1_typeparam2){if(bottom_dev->funca)returnbottom_dev->funca(param1,param2);/*核心层通用的funca代码*/bottom_dev->funca_ops1();/*通用的步骤代码A*/...bottom_dev->funca_ops2();/*通用的步骤代码B*/...bottom_dev->funca_ops3();/*通用的步骤代码C*/}上述core_funca的实现中,会检查底层设备是否重载了funca(),如果重载了,就调用底层的代码,否则直接使用通用层的。这样做的好处是,核心层的代码可以处理绝大多数该类设备的funca()对应的功能,只有少数特殊设备需要重新实现funca()。
15.2驱动的分离与分层
将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),根据获取到的设备信息来初始化设备。
驱动只负责驱动,设备只负责设备,总线法将两者进行匹配。
这就是Linux中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离。
当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有,就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动中查找与之匹配的驱动,如果有,也联系起来。Linux内核中大量的驱动程序都采用总线、驱动和设备模式。
15.3Platform平台总线驱动模型
在Linux2.6以后的设备驱动模型中,需关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。
Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题。但是在嵌入式系统里面,在SoC系统中集成的独立外设控制器、挂接在SoC内存空间的外设等却不依附于此类总线。
基于这一背景,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,驱动称为platform_driver。平台总线模型就是把原来的驱动C文件给分成了俩个C文件,一个是device.c(描述硬件信息,设备树可替代),一个是driver.c(驱动信息)。
一、platform驱动
在Linux内核中,用platform_driver结构体表示platform驱动,platform_driver结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数
structplatform_driver{int(*probe)(structplatform_device*);int(*remove)(structplatform_device*);void(*shutdown)(structplatform_device*);int(*suspend)(structplatform_device*,pm_message_tstate);int(*resume)(structplatform_device*);structdevice_driverdriver;conststructplatform_device_id*id_table;boolprevent_deferred_probe;};platform_device_id结构体内容如下:
structplatform_device_id{charname[PLATFORM_NAME_SIZE];kernel_ulong_tdriver_data;};device_driver结构体定义内容如下:
structdevice_driver{constchar*name;structbus_type*bus;structmodule*owner;constchar*mod_name;/*usedforbuilt-inmodules*/boolsuppress_bind_attrs;/*disablesbind/unbindviasysfs*/conststructof_device_id*of_match_table;//设备树的驱动匹配表conststructacpi_device_id*acpi_match_table;int(*probe)(structdevice*dev);int(*remove)(structdevice*dev);void(*shutdown)(structdevice*dev);int(*suspend)(structdevice*dev,pm_message_tstate);int(*resume)(structdevice*dev);conststructattribute_group**groups;conststructdev_pm_ops*pm;structdriver_private*p;};of_match_table表就是采用设备树时驱动使用的匹配表,也是数组,每个匹配项都为of_device_id结构体类型
structof_device_id{charname[32];chartype[32];charcompatible[128];constvoid*data;};compatible:在支持设备树的内核中,就是通过设备节点的compatible属性值和of_match_table中每个项目的compatible成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。
驱动和设备匹配成功后,驱动会从设备里面获得硬件资源,匹配成功了后,driver.c要从device.c(或者是设备树)中获得硬件资源,那么driver.c就是在probe函数中获得的。
二、platform设备(可以被设备树替代)
在platform平台下用platform_device结构体表示platform设备,如果内核支持设备树的话就不用使用platform_device来描述设备,使用设备树去描述platform_device即可。
structplatform_device{constchar*name;intid;boolid_auto;structdevicedev;u32num_resources;structresource*resource;conststructplatform_device_id*id_entry;char*driver_override;/*Drivernametoforceamatch*//*MFDcellpointer*/structmfd_cell*mfd_cell;/*archspecificadditions*/structpdev_archdataarchdata;};Linux内核使用resource结构体表示资源
structresource{resource_size_tstart;resource_size_tend;constchar*name;unsignedlongflags;structresource*parent,*sibling,*child;};start和end分别表示资源的起始和终止信息,对于内存类的资源,表示内存起始和终止地址,name表示资源名字,flags表示资源类型
使用platform_device_register函数将设备信息注册到Linux内核中
intplatform_device_register(structplatform_device*pdev)如果不再使用platform可以通过platform_device_unregister函数注销掉相应的platform设备
voidplatform_device_unregister(structplatform_device*pdev)三、platform总线
platform设备和platform驱动,相当于把设备和驱动分离了,需要platform总线进行配,platform设备和platform驱动进行内核注册时,都是注册到总线上。
在Linux内核中使用bus_type结构体表示总线
structbus_type{constchar*name;/*总线名字*/constchar*dev_name;structdevice*dev_root;structdevice_attribute*dev_attrs;conststructattribute_group**bus_groups;/*总线属性*/conststructattribute_group**dev_groups;/*设备属性*/conststructattribute_group**drv_groups;/*驱动属性*/int(*match)(structdevice*dev,structdevice_driver*drv);int(*uevent)(structdevice*dev,structkobj_uevent_env*env);int(*probe)(structdevice*dev);int(*remove)(structdevice*dev);void(*shutdown)(structdevice*dev);int(*online)(structdevice*dev);int(*offline)(structdevice*dev);int(*suspend)(structdevice*dev,pm_message_tstate);int(*resume)(structdevice*dev);conststructdev_pm_ops*pm;conststructiommu_ops*iommu_ops;structsubsys_private*p;structlock_class_keylock_key;};match函数:完成设备和驱动之间匹配的,总线使用match函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。
match函数有两个参数:dev和drv,这两个参数分别为device和device_driver类型,即设备和驱动。
platform总线是bus_type的一个具体实例
structbus_typeplatform_bus_type={.name="platform",.dev_groups=platform_dev_groups,.match=platform_match,.uevent=platform_uevent,.pm=&platform_dev_pm_ops,};platform_match匹配函数,用来匹配注册到platform总线的设备和驱动。
四、platform总线具体匹配方法
查看platform_match函数,如何匹配驱动和设备的
staticintplatform_match(structdevice*dev,structdevice_driver*drv){structplatform_device*pdev=to_platform_device(dev);structplatform_driver*pdrv=to_platform_driver(drv);/*Whendriver_overrideisset,onlybindtothematchingdriver*/if(pdev->driver_override)return!strcmp(pdev->driver_override,drv->name);/*AttemptanOFstylematchfirst*/if(of_driver_match_device(dev,drv))return1;/*ThentryACPIstylematch*/if(acpi_driver_match_device(dev,drv))return1;/*Thentrytomatchagainsttheidtable*/if(pdrv->id_table)returnplatform_match_id(pdrv->id_table,pdev)!=NULL;/*fall-backtodrivernamematch*/return(strcmp(pdev->name,drv->name)==0);}驱动和设备的匹配有四种方法。
1.OF类型的匹配
设备树采用的匹配方式,of_driver_match_device函数定义在文件include/linux/of_device.h中。
device_driver结构体(设备驱动)中有个名为of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,设备树中的每个设备节点的compatible属性会和of_match_table表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后probe函数就会执行。
2.ACPI匹配方式
3.id_table匹配每个platform_driver结构体(设备驱动)有一个id_table成员变量,保存了很多id信息。这些id信息存放着这个platformd驱动所支持的驱动类型
4.名字匹配如果第三种匹配方式的id_table不存在的话就直接比较驱动和设备的name字段,如果相等的话就匹配成功。
对于支持设备树的Linux版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式。即第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,直接比较驱动和设备的name字段。
15.4platform总线框架例程
一、应用层
intmain(intargc,char*argv[]){intfd,retvalue;char*filename;unsignedchardatabuf[1];unsignedcharreadbuf[1];if(argc!=3){printf("ErrorUsage!\r\n");return-1;}filename=argv[1];fd=open(filename,O_RDWR);if(fd<0){printf("file%sopenfailed!\r\n",argv[1]);return-1;}databuf[0]=atoi(argv[2]);/*要执行的操作:打开或关闭*/if(databuf[0]==2){retvalue=read(fd,readbuf,sizeof(readbuf));if(retvalue<0){printf("readfile%sfailed!\r\n",filename);}else{printf("readdate:%x\r\n",readbuf[0]);}}else{/*向/dev/led文件写入数据*/retvalue=write(fd,databuf,sizeof(databuf));if(retvalue<0){printf("LEDControlFailed!\r\n");close(fd);return-1;}}retvalue=close(fd);if(retvalue<0){printf("file%sclosefailed!\r\n",argv[1]);return-1;}return0;}