最全Linux驱动开发全流程详细解析

驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁。

具体任务

说明:设备驱动的两个任务方向

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#include"imx6ull.dtsi"设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts设备树文件中,还可以通过“#include”来引用.h、.dtsi和.dts文件。

设备节点

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=reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,reg属性的值一般是(address,length)对.

uart1:serial@02020000{compatible="fsl,imx6ul-uart","fsl,imx6q-uart","fsl,imx21-uart";reg=<0x020200000x4000>;interrupts=;clocks=<&clksIMX6UL_CLK_UART1_IPG>,<&clksIMX6UL_CLK_UART1_SERIAL>;clock-names="ipg","per";status="disabled";};uart1的父节点aips1:aips-bus@02000000设置了#address-cells=<1>、#sizecells=<1>,因此reg属性中address=0x02020000,length=0x4000。都是字长为1.

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=;};......}compatible属性值为“fsl,imx6ul-iomuxc”,

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=;};......};2.添加具体设备节点,调用pinctrl信息

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应用层

需要规定一些命令码,这些命令码在应用程序和驱动程序中需要保持一致。应用程序只需向驱动程序下发一条指令码,用来通知它执行哪条命令。

#includeintioctl(intfd,unsignedlongrequest,(...)arg);11.2驱动层

#includelong(*unlocked_ioctl)(structfile*fp,unsignedintrequest,unsignedlongargs);long(*compat_ioctl)(structfile*fp,unsignedintrequest,unsignedlongargs);12.2ioctr应用和驱动的协议

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=,;gpio-controller;#gpio-cells=<2>;interrupt-controller;#interrupt-cells=<2>;};①#interrupt-cells:此中断控制器下设备的cells大小,一般会使用interrupts属性描述中断信息,#interrupt-cells描述了interrupts属性的cells大小,一条信息有几个cells。每个cells都是32位整型值,对于ARM处理的GIC来说,一共有3个cells。

②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#includeintfcntl(intfd,intcmd);intfcntl(intfd,intcmd,longarg);intfcntl(intfd,intcmd,structflock*lock);使用fcntl(fd,F_SETOWN,getpid())将本应用程序的进程号告诉给内核。

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;}

THE END
1.微消息群发微消息群发6.0分| 2413人| 27.47Mb 安全下载 360手机助手官方下载,安全高速 应用介绍应用详情:微消息群发是一款微信群发服务软件,同时群发助手也称得上是微粉帮手、微商营销 版本:2.3.25 更新时间:2024-12-10 10:18:05 开发者:布谷园(北京)科技有限公司http://m.app.haosou.com/detail/index?pname=com.fblhb.wxmsgkeeper&id=4573770
2.哔哩哔哩批量群发软件,助力轻松运营B站b站使用教程命令提示符但有了【老马定制软件】,它能把内容梳理得井井有条,比如开头用引人入胜的引言引出主题,中间把各个观点、案例等按照重要程度或者逻辑顺序依次排列,结尾再做个恰到好处的总结升华,让读者阅读起来轻松又顺畅,很容易就能理解和接受文章传达的信息啦。 使用【老马定制软件】发布文章之后呀,它会自动生成对应链接呢。这https://m.163.com/dy/article/JJH9EDP00556A27X.html
3.2007年3月全国计算机三级考试《网络技术》笔试真题11、传输速率为10Gbps的局域网每一秒钟可以发送的比特数为 A) 1ⅹ106 B) 1ⅹ108 C) 1ⅹ1010 D) 1ⅹ1012 12、以下关于网络协议的描述中,错误的是 A) 为保证网络中结点之间有条不紊地交换数据,需要制订一套网络协议 B) 网络协议的语义规定了用户数据下控制信息的结构和格式 https://www.oh100.com/kaoshi/ncre3/tiku/197205.html
4.江西省消防物联网系统设计施工验收标准本标准规定了消防物联网系统的设置范围,体系架构和基本要求,感知设计要求、传输设计要求、应用设计要求及系统的施工、调试、验收、运维管理等内容。本标准适用于本省消防物联网系统的设计、施工、验收和运维管理https://www.gdliontech.cn/content/?2727.html
5.Windows.h常用API函数转错倒是没有错,只是窗口句柄有问题,消息发送给了主窗口。接收消息的窗口不对。记事本窗口界面有些有什么东西呢?菜单,编辑框,状态栏等控件,控件也是窗口,既然是窗口,那当然它们也有窗口句柄,而在记事本里是在哪里打字的?编辑框控件里打字的嘛!所以消息应该发送编辑框控件,那如何获得记事本里编辑框控件的窗口句柄呢?https://blog.csdn.net/yu57955/article/details/107374023
6.扛住100亿次红包请求的架构是这样设计的!我是这样解决的:利用 NTP 服务,同步所有的服务器时间,客户端利用时间戳来判断自己的此时需要发送多少请求。 算法很容易实现:假设有 100 万用户,则用户 id 为 0-999999。要求的 QPS 为 5 万,客户端得知 QPS 为 5 万,总用户数为 100 万,它计算 100 万/5 万=20,所有的用户应该分为 20 组。 https://www.51cto.com/article/613210.html
7.新年寄语简短(精选19篇)12.溪水不知道为什么总要流向大海,蝴蝶不知道为什么总要在空中飞舞,我不知道为什么短信总要发送给你。可能是元旦到了的缘故吧,祝你元旦节快乐。 13.新年之特殊行动:开心地笑,要惊天动地;疯狂地侃,要谈天说地;放情地唱,要呼天唤地;疯狂地吃,要欢天喜地;踏心地睡,要昏天暗地;信息传情谊,要谢天谢地。祝https://www.kaoyanmiji.com/wendang/762580.html
8.www.jxmzxx.com/appnews他的内心深处有着坚定的信仰,在敌人的心脏里默默为党收集情报,传递消息。 每一次获取情报都像是在刀尖上跳舞,一不小心就会被敌人发现。 他还有一个隐藏身份,那就是军统特工。 这个身份让他的处境更加复杂,他要在三方势力之间周旋,哪边都不能露出破绽。 http://www.jxmzxx.com/appnews_120221.html
9.杭州法院重要信息系统异地容灾备份项目根据应用需求不同可以自由调整条带大小达到最优化模式,可实现4K、8K、16K、32K、64K、128K、256K、512K、1024K、2048K、4096K等不同大小的条带,同时可在线更改RAID组条带大小。 多通道管理 配置含多通道管理软件,可在服务器与盘阵间实现路径的负载均衡/故障切换;增加主机无需许可费用,支持Windows、Linux及UNIXhttps://m.cgwenjian.com/view/file/202210270000309353
10.中国移动网上大学传输设备OTN中兴L3题库3. 10G系统测试中要求发送端业务单板的消光比要高于多少 A.6dB B.3dB C.8dB D.10dB 答案:D 4. 光缆线路的主要敷设方式不正确的是() A.沿井 B.直埋 C.管道 D.架空 答案:A 5. 改造扩容工作包括:( )、方案的制定和认可、升级申请、实施阶段、后续异常处理、现场培训、资料整理、经验总结等步骤。 A.硬https://www.360docs.net/doc/1b226772dc36a32d7375a417866fb84ae45cc3a0.html
11.BOSS效果器GT100使用说明书.pdfBOSS效果器GT-100使用说明书.pdf 关闭预览 想预览更多内容,点击免费在线预览全文 免费在线预览全文 第2 版 使用手册 如何获取用户手册的 PDF 版本 本产品用户手册的 PDF 文件和补充材料可通过 Roland 网站获取。 ? GT-100 用户手册(本文档) ? GT-100 参数手册 * *这些资料不随产品提供,您可以根据需要https://m.book118.com/html/2021/0702/8021130122003115.shtm
12.100句文案,穿越冬天我应该发送那条本不应该发送的消息吗? 亦或 我是否发送了本应删除的不合适消息? 解决这种情况的最佳表情包是什么? 这些真的是植物鸡块? 为什么看起来尝起来都像鸡块? - 100%的困惑 0%的鸡肉 植物鸡肉块 点击查看项目详情 来自汉堡王。汉堡王通过一系列展现人们疑惑心理状态的海报,宣传品牌推出的植物肉鸡块。 https://www.digitaling.com/articles/897798.html
13.智东西早报:中科大首次实现皮秒量级序列发生全国全面启用新能源7月16日晚间消息,Uber今日推出了一款名为“Spotlight”的工具,旨在强化司机与乘客之间的通信,让司机在约定地点更轻松地找到乘客。通过这款工具,乘客可点亮其智能手机屏幕,并显示特定颜色,从而让司机有一个更好的寻找目标。点亮屏幕后,这款工具还能向司机发送一条消息,告诉其屏幕颜色。 https://zhidx.com/p/125024.html
14.VB高级编程100例Timer函数可以返回一秒的小数部分) Do While bDT If Timer - sPrevious >= Then bDT = False Loop bDT = True End Sub (通信传输速率为 9600bps,则最快速度 发送一个字节,仪表每秒发送 50帧数据,每帧数据有 4个字节,即每秒发送 200个字节,平均 发送一个字节,连续读取串口数据时要在程序中添加循环等待程序https://doc.mbalib.com/view/e8c063745aa4c9df703902110c6300a8.html
15.CDPR没能实现的赛博朋克世界——《ChromeBook2》装备篇机核G这些程序预封装在小塑料盒里,附有说明书和存储在标准数据芯片上的软件(每个芯片1MU,每个技能等级占用1MU)。由于它们的MU大小,这些程序必须存储在电脑的内存单元上才能使用。这些技能程序会增加用户的INT技能,但是它们需要运行时间(每个技能等级需要一秒),不像MRAM技能芯片那样。可用的技能程序包括会计、人类学、植物学https://www.gcores.com/articles/176033
16.抢先行动,赢得丰厚礼品:平台登录站点抢先2024王中王资料免费领取,28加拿大官网入口,888彩票3.0版本软件特色,27200注500万彩票中奖,100彩票安卓版2024年恩典365,7588con查询澳门,28.7下载,4777777开奖结果查询,139kj本港台开奖直播现场2018彩票手机认证送彩金,105有啥梗吗,012最权威彩票,24333齐齐发,10bet线上娱乐网站13086最新消息,12mc,cc,24小时线路检测中心,http://m.yghsix.cn/20241214/99495576766.html
17.www.scmc老外肛交 深夜免费在线www一秒进入 金桔软件下载 久久久久久久久久久午夜女同性恋视频 扣逼自慰潮吹床上激战 我就想要看看黄片 少妇李芬┅┅快┅┅用力 优质国产在线成人 一级99黄色视频 伴郎强上准新娘 黄片区免费观看 aiss爱丝无内丝袜自慰风云网 牛牛影视AV老牛影视AV无码 日韩激情欧美 中国农村夫妻三级http://www.scmc-xa.com/xxxr/584373.htm