小工具知识网

当前位置: 首页 > 行业知识 > 最全Linux驱动开发全流程详细解析(持续更新)

行业知识

最全Linux驱动开发全流程详细解析(持续更新)

2026-10-20 22:18:52 来源:互联网转载

Linux驱动开发详细解析

一、驱动概念

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

  • 具体任务
    1. 读写设备寄存器(实现控制的方式)
    2. 完成设备的轮询、中断处理、DMA通信(CPU与外设通信的方式)
    3. 进行物理内存向虚拟内存的映射(在开启硬件MMU的情况下)
  • 说明:设备驱动的两个任务方向
    1. 操作硬件(向下)
    2. 将驱动程序通入内核,实现面向操作系统内核的接口内容,接口由操作系统实现(向上)

      (驱动程序按照操作系统给出的独立于设备的接口设计,应用程序使用操作系统统一的系统调用接口来访问设备)

    Linux系统主要部分:内核、shell、文件系统、应用程序

    • 内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统
    • 分层设计的思想让程序间松耦合,有助于适配各种平台
    • 驱动的上面是系统调用,下面是硬件

      二、驱动分类

      Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。

      1. 字符设备(Char Device)
        • 字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。
        • 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生。
        • 字符设备驱动程序通常至少要实现open、close、read和write系统调用。
        • 比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输。
        • 块设备(Block Device)
          • 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。
          • 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。
          • 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。
          • 只有一个块设备可以支持一个安装的文件系统。
          • 比如我们常见的电脑硬盘、SD卡、U盘、光盘等。
          • 网络设备(Net Device)
            • 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。
            • 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。
            • 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是read、write等。
            • 比如我们常见的网卡设备、蓝牙设备。

      三、驱动程序的功能

      1. 对设备初始化和释放
      2. 把数据从内核传送到硬件和从硬件读取数据
      3. 读取应用程序传送给设备文件的数据和回送应用程序请求的数据
      4. 检测和处理设备出现的错误

      四、驱动开发前提知识

      4.1 内核态和用户态

      • Kernel Mode(内核态)
        • 内核模式下(执行内核空间的代码),代码具有对硬件的所有控制权限。可以执行所有CPU指令,可以访问任意地址的内存
        • User Mode(用户态)
          • 在用户模式下(执行用户空间的代码),代码没有对硬件的直接控制权限,也不能直接访问地址的内存。
          • 只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址。
          • 程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存

            Linux利用CPU实现内核态和用户态

            • ARM:内核态(svc模式),用户态(usr模式)
            • x86 : 内核态(ring 0 ),用户态(ring 3)// x86有ring 0 - ring3四种特权等级

              Linux实现内核态和用户态切换

              • ARM Linux的系统调用实现原理是采用swi软中断从用户态切换至内核态

              • X86是通过int 0x80中断进入内核态

                Linux只能通过系统调用和硬件中断从用户空间进入内核空间

                • 执行系统调用的内核代码运行在进程上下文中,他代表调用进程执行操作,因此能够访问进程地址空间的所有数据
                • 处理硬件中断的内核代码运行在中断上下文中,他和进程是异步的,与任何一个特定进程无关通常,一个驱动程序模块中的某些函数作为系统调用的一部分,而其他函数负责中断处理

                  4.2 Linux下应用程序调用驱动程序流程

                  • Linux下进行驱动开发,完全将驱动程序与应用程序隔开,中间通过C标准库函数以及系统调用完成驱动层和应用层的数据交换。
                  • 驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对“/dev/xxx” (xxx 是具体的驱动文件名字) 的文件进行相应的操作即可实现对硬件的操作。
                  • 用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法 来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作
                  • 每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。

                    大致流程

                    1. 加载一个驱动模块,产生一个设备文件,有唯一对应的inode结构体
                    2. 应用层调用open函数打开设备文件,对于上层open调用到内核时会发生一次软中断,从用户空间进入到内核空间。
                    3. open会调用到sys_open(内核函数),sys_open根据文件的地址,找到设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
                    4. 根据struct inode结构体里面记录的主设备号和次设备号,在驱动链表(管理所有设备的驱动)里面,根据找到字符设备驱动
                    5. 每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口
                    6. 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
                    7. 执行xxx_open驱动函数。

                    4.3 内核模块

                    Linux 驱动有两种运行方式

                    • 将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
                    • 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用相应命令加载驱动模块。
                      • 内核模块是Linux内核向外部提供的一个插口
                      • 内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行
                      • 内核模块便于驱动、文件系统等的二次开发

                        内核模块组成

                        1. 模块加载函数

                          module_init(xxx_init);
                          • module_init 函数用来向 Linux 内核注册一个模块加载函数,
                          • 参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
                          • 当加载驱动的时, xxx_init 这个函数就会被调用
                          • 模块卸载函数

                            module_exit(xxx_exit);
                            • module_exit函数用来向 Linux 内核注册一个模块卸载函数,
                            • 参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
                            • 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
                            • 模块许可证明

                              MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
                            • 模块参数(可选)

                              模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量

                            • 模块信息(可选)

                              MODULE_AUTHOR("songwei") //添加模块作者信息
                            • 模块打印 printk

                              printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。

                              printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出

                        • printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别

                          #define KERN_SOH  "
                          printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");
                          1" #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
                        • 以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。

                          #define CONSOLE_LOGLEVEL_DEFAULT 7

                          如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4。

                        • 在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:

                          模块操作命令

                          CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。

                          这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。

                        • 为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
                          1. 加载模块
                            • insmod XXX.ko
                              • insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
                              • 加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
                              • modprobe XXX.ko
                                • modprobe 提示无法打开“modules.dep”这个文件 ,输入 depmod 命令即可自动生成 modules.dep
                                • rmmod XXX.ko
                                • 卸载模块
                                  • 查看系统中加载的所有模块及模块间的依赖关系
                                  • 查看模块信息
                                    • lsmod
                                      • 查看详细信息,内核模块描述信息,编译系统信息
                                      • modinfo (模块路径)

                                          4.4 设备号

                        • Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成
                          • 主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
                          • Linux 提供了一个名为 dev_t 的数据类型表示设备号其中高 12 位为主设备号, 低 20 位为次设备
                          • MAJOR // 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。MINOR //用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。MKDEV //用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
                          • 使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号(主)

                            4.5 地址映射

                            MMU(Memory Manage Unit)内存管理单元

                          • 完成虚拟空间到物理空间的映射
                            1. 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
                            2. 内存映射函数

                            3. 对于 32 位的处理器来说,虚拟地址(VA,Virtual Address)范围是 2^32=4GB
                            /*phys_addr:要映射给的物理起始地址(cookie)size:要映射的内存空间大小mtype: ioremap 的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC, ioremap 函数选择 MT_DEVICE返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址*/#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE) void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype){    return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));}

                            CPU只能访问虚拟地址,不能直接向寄存器地址写入数据,必须得到寄存器物理地址在Linux系统中对应的虚拟地址。

                            物理内存和虚拟内存之间的转换,需要用到: ioremap 和 iounmap两个函数

                            • ioremap,用于获取指定物理地址空间对应的虚拟地址空间

                              #define addr (0X020E0068)  // 物理地址static void __iomem*  va; //指向映射后的虚拟空间首地址的指针va=ioremap(addr, 4);   // 得到虚拟地址首地址

                              例:获取某个寄存器对应的虚拟地址

                              iounmap(va);
                            • iounmap,卸载驱动使用 iounmap 函数释放掉 ioremap 函数所做的映射。

                              参数 addr:要取消映射的虚拟地址空间首地址

                              I/O内存访问函数

                              u8 readb(const volatile void __iomem *addr)u16 readw(const volatile void __iomem *addr)u32 readl(const volatile void __iomem *addr)

                              当外部寄存器或外部内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。

                              使用 ioremap 函数将寄存器的物理地址映射到虚拟地址后,可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

                              • 读操作函数

                                void writeb(u8 value, volatile void __iomem *addr)void writew(u16 value, volatile void __iomem *addr)void writel(u32 value, volatile void __iomem *addr)

                                readb、 readw 和 readl 分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值是读取到的数据

                              • 写操作函数

                                五、设备树

                                writeb、 writew 和 writel分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。

                                5.1 DTS、DTB和DTC

                                Device Tree是一种描述硬件的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。

                                Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性

                                • 引入设备树之前:一些与硬件设备相关的具体信息都要写在驱动代码中,如果外设发生相应的变化,那么驱动代码就需要改动。

                                • 引入设备树之后:通过设备树对硬件信息的抽象,驱动代码只要负责处理逻辑,而关于设备的具体信息存放到设备树文件中。如果只是硬件接口信息的变化而没有驱动逻辑的变化,开发者只需要修改设备树文件信息,不需要改写驱动代码。

                                • 设备树源码文件,硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts
                                  • DTS
                                    • 对于一些相同的dts配置可以抽象到dtsi文件中,然后可以用include的方式到dts文件中
                                    • DTSI
                                      • 同一芯片可以做一个dtsi,不同的板子不同的dts,然后include同一dtsi
                                      • 对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置
                                      • dtc是编译dts的工具
                                      • DTC
                                        • dts经过dtc编译之后会得到dtb文件,设备树的二进制执行文件
                                        • DTB

                                            5.2 设备树框架

                                          • dtb通过Bootloader引导程序加载到内核。
                                            1.根节点:\2.设备节点:nodex        ①节点名称:node        ②节点地址:node@0, @后面即为地址3.属性:属性名称(Property   name)和属性值(Property value)4.标签
                                          • “/”是根节点,每个设备树文件只有一个根节点。在设备树文件中会发现有的文件下也有“/”根节点,这两个**“/”根节点的内容会合并成一个根节点。**
                                            • 5.3 DTS语法

                                            • Linux 内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹

                                              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 = ; 设置reg属性的值为03.字符串列表:字符串和字符串之间采用“,”隔开compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
                                              • 设备树源码中常用的几种数据形式

                                                属性

                                              • #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),
                                                • compatible属性(兼容属性)

                                                  cpp "manufacturer,model" manufacturer:厂商名称 model:模块对应的驱动名字

                                                  例:

                                                  imx6ull-alientekemmc.dts 中 sound 节点是 音频设备节点,采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:

                                                  cpp compatible = "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 属性值是一个字符串,一般 model 属性描述设备模块信息。

                                                • status属性

                                                  status 属性和设备状态有关的, status 属性值是字符串,描述设备的状态信息。

                                                • #address-cells 和#size-cells 属性

                                                  用于描述子节点的地址信息,reg属性的address 和 length的字长。

                                                  • #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
                                                  • //每个“address length”组合表示一个地址范围,//其中 address 是起始地址, length 是地址长度,//#address-cells 表明 address 这个数据所占用的字长,// #size-cells 表明 length 这个数据所占用的字长.reg = 
                                                  • 子节点的地址信息描述来自于父节点的#address-cells 和#size-cells的值,而不是该节点本身的值(当前节点的信息是描述子节点的,自己的信息在父节点里)
                                                    uart1: serial@02020000 {    compatible = "fsl,imx6ul-uart",        "fsl,imx6q-uart", "fsl,imx21-uart";    reg = ;    interrupts = ;    clocks = ,        ;    clock-names = "ipg", "per";    status = "disabled";};
                                                  • reg属性

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

                                                    例

                                                    child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长

                                                    uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = 、 #sizecells = ,因此 reg 属性中 address=0x02020000, length=0x4000。都是字长为1.

                                                  • ranges属性

                                                    • ranges属性值可以为空或者按照( child-bus-address , parent-bus-address , length )格式编写的数字

                                                    • ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成。

                                                    • 如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。

                                                      aliases {    can0 = &flexcan1;    can1 = &flexcan2;    ...    usbphy0 = &usbphy1;    usbphy1 = &usbphy2;};
                                                    • 特殊节点

                                                      在根节点“/”中有两个特殊的子节点: aliases 和 chosen

                                                      1. aliases

                                                        5.4 OF操作函数

                                                        aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。

                                                        但是,一般会在节点命名的时候会加上 label,然后通过&label来访问节点。

                                                      2. chosen

                                                        chosen 不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据(bootargs 参数)。

                                                      查找节点

                                                      Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_” (称为OF 函数)

                                                      struct device_node {    const char *name; /* 节点名字 */    const char *type; /* 设备类型 */    phandle phandle;    const char *full_name; /* 节点全名 */    struct fwnode_handle fwnode;     struct property *properties; /* 属性 */    struct property *deadprops; /* removed 属性 */    struct device_node *parent; /* 父节点 */    struct device_node *child; /* 子节点    ...}

                                                      Linux 内核使用 device_node 结构体来描述一个节点:

                                                      struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
                                                      • 通过节点名字查找指定的节点:of_find_node_by_name

                                                        struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

                                                        from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

                                                        name:要查找的节点名字。

                                                        返回值: 找到的节点,如果为 NULL 表示查找失败。

                                                      • 通过 device_type 属性查找指定的节点:of_find_node_by_type

                                                        struct device_node *of_find_compatible_node(struct device_node *from,                                            const char *type,                                            const char *compatible)

                                                        from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

                                                        type:要查找的节点对应的 type 字符串, device_type 属性值。

                                                        返回值: 找到的节点,如果为 NULL 表示查找失败

                                                      • 通过device_type 和 compatible两个属性查找指定的节点:of_find_compatible_node

                                                        struct device_node *of_find_matching_node_and_match(struct device_node *from,                                            const struct of_device_id *matches,                                            const struct of_device_id **match)

                                                        from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

                                                        type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL

                                                        compatible: 要查找的节点所对应的 compatible 属性列表。

                                                        返回值: 找到的节点,如果为 NULL 表示查找失败

                                                      • 通过 of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match

                                                        inline struct device_node *of_find_node_by_path(const char *path)

                                                        from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

                                                        matches: of_device_id 匹配表,在此匹配表里面查找节点。

                                                        match: 找到的匹配的 of_device_id。

                                                        返回值: 找到的节点,如果为 NULL 表示查找失败

                                                      • 通过路径来查找指定的节点:of_find_node_by_path

                                                        获取属性值

                                                        path:设备树节点中绝对路径的节点名,可以使用节点的别名

                                                        返回值: 找到的节点,如果为 NULL 表示查找失败

                                                        struct property {    char *name; /* 属性名字 */    int length; /* 属性长度 */    void *value; /* 属性值 */    struct property *next; /* 下一个属性 */    unsigned long _flags;    unsigned int unique_id;    struct bin_attribute attr;}

                                                        Linux 内核中使用结构体 property 表示属性

                                                        property *of_find_property(const struct device_node *np,                           const char *name,                           int *lenp)
                                                        • 查找指定的属性:of_find_property

                                                          int of_property_count_elems_of_size(const struct device_node *np,                                    const char *propname                                    int elem_size)

                                                          np:设备节点。

                                                          name: 属性名字。

                                                          lenp:属性值的字节数,一般为NULL

                                                          返回值: 找到的属性。

                                                        • 获取属性中元素的数量(数组):of_property_count_elems_of_size

                                                          int of_property_read_u32_index(const struct device_node *np,                                const char *propname,                                u32 index,                                u32 *out_value)

                                                          np:设备节点。

                                                          proname: 需要统计元素数量的属性名字。

                                                          elem_size:元素长度。

                                                          返回值: 得到的属性元素数量

                                                        • 从属性中获取指定标号的 u32 类型数据值:of_property_read_u32_index

                                                          of_property_read_u8_arrayof_property_read_u16_array of_property_read_u32_array of_property_read_u64_array int of_property_read_u8_array(const struct device_node *np,                                const char *propname,                                u8 *out_values,                                size_t sz)

                                                          np:设备节点。

                                                          proname: 要读取的属性名字。

                                                          index:要读取的值标号。

                                                          out_value:读取到的值

                                                          返回值: 0 读取成功;

                                                          负值: 读取失败,

                                                          -EINVAL 表示属性不存在

                                                          -ENODATA 表示没有要读取的数据,

                                                          -EOVERFLOW 表示属性值列表太小

                                                        • 读取属性中 u8、 u16、 u32 和 u64 类型的数组数据

                                                          int of_property_read_string(struct device_node *np,                            const char *propname,                            const char **out_string)

                                                          np:设备节点。

                                                          proname: 要读取的属性名字。

                                                          out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。

                                                          sz: 要读取的数组元素数量。

                                                          返回值: 0:读取成功;

                                                          负值: 读取失败

                                                          -EINVAL 表示属性不存在

                                                          -ENODATA 表示没有要读取的数据

                                                          -EOVERFLOW 表示属性值列表太小

                                                        • 读取属性中字符串值:of_property_read_string

                                                          int of_n_addr_cells(struct device_node *np)int of_n_size_cells(struct device_node *np)

                                                          np:设备节点。

                                                          proname: 要读取的属性名字。

                                                          out_string:读取到的字符串值。

                                                          返回值: 0,读取成功,负值,读取失败

                                                        • 获取 #address-cells 属性值:of_n_addr_cells ,获取 #size-cells 属性值:of_size_cells 。

                                                          void __iomem *of_iomap(struct device_node *np,  int index)

                                                          np:设备节点。

                                                          返回值: 获取到的#address-cells 属性值。

                                                          返回值: 获取到的#size-cells 属性值。

                                                        • 内存映射

                                                          of_iomap 函数用于直接内存映射,前面通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存。

                                                          of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:

                                                          #if 1/* 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);#endif

                                                          np:设备节点。

                                                          index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。

                                                          返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

                                                          例

                                                          of 函数在 led_init() 中应用

                                                          六、字符设备驱动

                                                          int ret;    u32 regdate[14];    const char *str;struct property *proper;    /* 1 、获取设备节点: */    dtb_led.nd = of_find_node_by_path("/songwei_led");    if(dtb_led.nd == NULL)    {        printk("songwei_led node can not found!\r\n");        return -EINVAL;    }else    {        printk("songwei_led node has been found!\r\n");    }    /* 2 、获取 compatible  属性内容 */    proper = of_find_property(dtb_led.nd ,"compatible",NULL);    if(proper == NULL)     {        printk("compatible property find failed\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  

                                                          6.1 字符设备基本驱动框架

                                                          1.模块加载

                                                          /* 驱动入口函数 */static int __init xxx_init(void){    /* 入口函数具体内容 */    return 0;}/* 驱动出口函数 */static void __exit xxx_exit(void){    /* 出口函数具体内容 */}/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit)

                                                          2.注册字符设备驱动

                                                          static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops) static inline void unregister_chrdev(unsigned int major, const char *name)

                                                          对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。

                                                          字符设备的注册和注销函数原型:

                                                          3.内存映射

                                                          这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。

                                                          一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。

                                                          #define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE) void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,unsigned int mtype){    return arch_ioremap_caller(phys_addr,     size,     mtype,    __builtin_return_address(0));}
                                                          • 内存映射

                                                            在Linux中不能直接访问寄存器,要想要操作寄存器需要完成物理地址到虚拟空间的映射。

                                                            void iounmap (volatile void __iomem *addr)

                                                            返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。

                                                            建立映射:映射的虚拟地址 = ioremap(IO内存起始地址,映射长度);

                                                            一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。

                                                            • 解除映射

                                                              4.应用层和内核层传递数据

                                                              static inline long copy_from_user(void *to, const void __user * from, unsigned long n)static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

                                                              应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助下面的这两个函数

                                                              5. 字符设备最基本框架

                                                              to:目标地址

                                                              from:源地址

                                                              n:将要拷贝数据的字节数

                                                              返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数

                                                              6. 创建驱动节点文件

                                                              #define CHRDEVBASE_MAJOR 200//手动设置主设备号#define CHRDEVBASE_NAME  "chrdevbase"//设备名称//内核缓存区static char readbuf[100];//读数据缓存static char writebuf[100];//写数据缓存static char kerneldata[] = {"kernel data!"};//测试数据//硬件寄存器#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址static void __iomem *GPIO_TEST;// __iomem 类型的指针,指向映射后的虚拟空间首地址//打开设备static int chrdevbase_open(struct inode *inode, struct file *filp) {return 0;}// 从设备读取数据 static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) {int retvalue = 0;unsigned char databuf[1];// 读取硬件寄存器#if 0  //读取寄存器状态databuf[0] = readl(GPIO_TEST);retvalue = copy_to_user(buf , databuf, cnt);//读取内核内存#else//测试数据拷贝到读数据缓存中    memcpy(readbuf , kerneldata , sizeof(kerneldata));      //内核中数据(读缓存)拷贝到用户空间    retvalue = copy_to_user(buf , readbuf , cnt);#endif    if(retvalue == 0) printk("kernel senddate ok!\n");     else printk("kernel senddate failed!\n");    return 0;}//向设备写数据 static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt) {int retvalue = 0;//写硬件寄存器#if 0writel(buf[0],GPIO_TEST);//写内核缓存#else//用户数据拷贝到内核空间(写缓存)    retvalue = copy_from_user(writebuf , buf ,cnt);#endif    if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);  else printk("kernel recevdate failed!");    return 0;}//关闭/释放设备static int chrdevbase_release(struct inode *inode , struct file *filp) {return 0;}//设备操作函数static struct file_operations chrdevbase_fops = {    .owner = THIS_MODULE,    .open = chrdevbase_open,    .read = chrdevbase_read,    .write = chrdevbase_write,    .release = chrdevbase_release,};/* 驱动入口函数 */static int __init chrdevbase_init(void){int retvalue = 0;//寄存器物理映射,物理地址映射到虚拟地址指针GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);//注册字符设备驱动    retvalue = register_chrdev(CHRDEVBASE_MAJOR, //主设备号    CHRDEVBASE_NAME, //设备名称    &chrdevbase_fops);//设备操作函数集合        if(retvalue  
                                                              mknod /dev/chrdevbase c 200 0

                                                              加载驱动模块后,需手动创建驱动节点文件

                                                            • 其中“mknod”是创建节点命令,
                                                              • “/dev/chrdevbase”是要创建的节点文件,
                                                              • “c”表示这是个字符设备,
                                                              • “200”是设备的主设备号,
                                                              • “0”是设备的次设备号。
                                                              • 6.2 新字符设备基本驱动框架

                                                              • 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看

                                                                1.设备文件系统

                                                                上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

                                                                在 Linux 下通过 udev(用户空间程序) 来实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。

                                                              • devfs, 一个基于内核的动态设备文件系统
                                                              • 设备文件系统有devfs,mdev,udev这三种

                                                                1. 不确定的设备映射
                                                                • devfs缺点(过时原因)
                                                                  • 没有足够的主/辅设备号
                                                                  • /dev目录下文件太多
                                                                  • udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
                                                                  • 内核内存使用
                                                                    1. 采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动
                                                                    • udev和devfs的区别
                                                                      • udev的Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
                                                                      • mdev,是udev的简化版本,是busybox中所带的程序,适合用在嵌入式系统
                                                                      • 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。

                                                                          2.申请设备号

                                                                        int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

                                                                        上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号。

                                                                        • 动态申请设备号

                                                                          int register_chrdev_region(dev_t from, unsigned count, const char *name);

                                                                          dev:保存申请到的设备号。

                                                                          baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0

                                                                          count: 要申请的设备号数量。

                                                                          name:设备名字。

                                                                        • 静态申请设备号

                                                                          void unregister_chrdev_region(dev_t from, unsigned count)

                                                                          from - 要申请的起始设备号

                                                                          count - 设备号个数

                                                                          name - 设备号在内核中的名称

                                                                          返回0申请成功,否则失败

                                                                        • 释放设备号

                                                                          //创建设备号 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);// 获取分配号的次设备号}

                                                                          from:要释放的设备号。

                                                                          count: 表示从 from 开始,要释放的设备号数量。

                                                                        • 申请设备号模板

                                                                          3.注册字符设备

                                                                          struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;//操作函数集合struct list_head list;dev_t dev;//设备号unsigned int count;};

                                                                          在 Linux 中使用 cdev 结构体表示一个字符设备

                                                                          void cdev_init(struct cdev *cdev, const struct file_operations *fops);

                                                                          在 cdev 中有两个重要的成员变量:ops 和 dev,字符设备文件操作函数集合file_operations 以及设备号 dev_t。

                                                                          • 初始化cdev结构体变量

                                                                            struct cdev testcdev;//设备操作函数static struct file_operations test_fops = {    .owner = THIS_MODULE,    //其他具体的初始项 };testcdev.owner = THIS_MODULE;//初始化 cdev 结构体变量cdev_init(&testcdev, &test_fops); 

                                                                            例

                                                                            int cdev_add(struct cdev *p, dev_t dev, unsigned count)
                                                                          • 将设备添加到内核

                                                                            cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。

                                                                            将cdev添加到内核同时绑定设备号。

                                                                            其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作

                                                                            cdev_add(&testcdev, devid, 1); //添加字符设备

                                                                            p - 要添加的cdev结构

                                                                            dev - 绑定的起始设备号

                                                                            count - 设备号个数

                                                                            例

                                                                            void cdev_del(struct cdev *p);
                                                                          • 将设备从内核注销

                                                                            卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

                                                                            cdev_del(&testcdev); //删除 cdev

                                                                            p - 要添加的cdev结构

                                                                            例

                                                                            4.自动创建设备节点

                                                                            struct class *class_create(struct module *owner, const char *name);

                                                                            上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

                                                                            在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

                                                                            自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。

                                                                            • 创建一个class类

                                                                            • class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
                                                                              • 设备类名对应 /sys/class 目录的子目录名。
                                                                              • 返回值是个指向结构体 class 的指针,也就是创建的类。
                                                                              • void class_destroy(struct class *cls); // cls要删除的类
                                                                              • 删除一个class类

                                                                                struct device *device_create(struct class *class,    struct device *parent,    dev_t devt,    void *drvdata,const char *fmt, ...)
                                                                              • 创建设备

                                                                                还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。

                                                                                成功会在 /dev 目录下生成设备文件。

                                                                                void device_destroy(struct class *class, dev_t devt);

                                                                                *class——设备类指针,

                                                                                *parent——父设备指针,

                                                                                devt——设备号,

                                                                                *drvdata——额外数据,

                                                                                *fmt——设备文件名

                                                                              • 删除设备

                                                                                卸载驱动的时候需要删除掉创建的设备

                                                                                5.文件私有数据

                                                                                class——设备所处的类

                                                                                devt——设备号

                                                                              • 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),
                                                                                • 一个设备的所有属性信息将其做成一个结构体,
                                                                                • 编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
                                                                                • /* newchrled设备结构体 */struct newchrled_dev{dev_t devid;/* 设备号  */struct cdev cdev;/* cdev */struct class *class;/* 类 */struct device *device;/* 设备  */int major;/* 主设备号  */int minor;/* 次设备号   */};struct newchrled_dev newchrled;/* led设备 */ /* * @description: 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 *   一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */static int led_open(struct inode *inode, struct file *filp){filp->private_data = &newchrled; /* 设置私有数据 */return 0;} static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){    struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;return 0;}
                                                                                • 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体

                                                                                  6.新字符设备驱动程序框架

                                                                                  #define NEWCHR_CNT 1#define NEWCHR_NAME "NEWCHR"//内核缓存区static char readbuf[100];//读数据缓存static char writebuf[100];//写数据缓存static char kerneldata[] = {"kernel data!"};//测试数据//硬件寄存器#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址static void __iomem *GPIO_TEST;// __iomem 类型的指针,指向映射后的虚拟空间首地址/* newchr设备结构体 */struct newchr_dev{dev_t devid;/* 设备号  */struct cdev cdev;/* cdev */struct class *class;/* 类 */struct device *device;/* 设备  */int major;/* 主设备号  */int minor;/* 次设备号   */}; struct newchrled_dev newchr;/* newchr设备 *///打开设备static int chrdevbase_open(struct inode *inode, struct file *filp) {filp->private_data = &newchr; /* 设置私有数据 */return 0;}// 从设备读取数据 static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) {int retvalue = 0;unsigned char databuf[1];//读取私有数据struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;// 读取硬件寄存器#if 0  //读取寄存器状态databuf[0] = readl(GPIO_TEST);retvalue = copy_to_user(buf , databuf, cnt);//读取内核内存#else//测试数据拷贝到读数据缓存中    memcpy(readbuf , kerneldata , sizeof(kerneldata));      //内核中数据(读缓存)拷贝到用户空间    retvalue = copy_to_user(buf , readbuf , cnt);#endif    if(retvalue == 0) printk("kernel senddate ok!\n");     else printk("kernel senddate failed!\n");    return 0;}//向设备写数据 static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt) {int retvalue = 0;//读取私有数据struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;//写硬件寄存器#if 0writel(buf[0],GPIO_TEST);//写内核缓存#else//用户数据拷贝到内核空间(写缓存)    retvalue = copy_from_user(writebuf , buf ,cnt);#endif    if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);  else printk("kernel recevdate failed!");    return 0;}//关闭/释放设备static int chrdevbase_release(struct inode *inode , struct file *filp) {return 0;}//设备操作函数static struct file_operations chrdevbase_fops = {    .owner = THIS_MODULE,    .open = chrdevbase_open,    .read = chrdevbase_read,    .write = chrdevbase_write,    .release = chrdevbase_release,};/* 驱动入口函数 */static int __init chrdevbase_init(void){int retvalue = 0;//寄存器物理映射,物理地址映射到虚拟地址指针GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);//申请设备号    if(newchr.major)//静态申请    {        newchr.devid = MKDEV(newchr.major , 0);        register_chrdev_region(newchr.devid, NEWCHR_CNT,NEWCHR_NAME);    }else//动态申请    {        alloc_chrdev_region(&newchr.devid , 0 , NEWCHR_CNT , NEWCHR_NAME);        newchr.major = MAJOR(newchr.devid);        newchr.minor = MINOR(newchr.devid);    }       printk("newche major=%d,minor=%d\r\n",newchr.major , newchr.minor);//字符串设备初始化、注册添加到内核newchr.cdev.owner = THIS_MODULE;    cdev_init(&newchr.cdev , &newchr_fops);    cdev_add(&newchr.cdev , newchr.devid ,NEWCHR_LED_CNT);//创建设备类    newchr.class = class_create(THIS_MODULE , NEWCHR_NAME);    if(IS_ERR(newchr.class))    {        return PTR_ERR(newchr.class);    }//创建类的实例化设备 ,dev下面创建文件    newchr.device = device_create(newchr.class , NULL , newchr.devid ,NULL ,NEWCHR_NAME);    if(IS_ERR(newchr.device))    {        return PTR_ERR(newchr.device);    }    return 0;}/* 驱动出口函数 */static void __exit chrdevbase_exit(void){//解除寄存器映射iounmap(GPIO_TEST);//删除cdev字符串设备cdev_del(&newchr.cdev);//释放设备号    unregister_chrdev_region(newchr.devid , NEWCHR_CNT);//具体设备注销    device_destroy(newchr.class, newchr.devid);    //类注销    class_destroy(newchr.class);}/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(chrdevbase_init);module_exit(chrdevbase_exit);MODULE_LICENSE("GPI");//GPL模块许可证MODULE_AUTHOR("songwei");//作者信息

                                                                                  七、pinctrl子系统

                                                                                  7.1 pinctrl 子系统主要工作内容:

                                                                                  Linux 内核提供了 pinctrl 子系统和 gpio 子系统用于 GPIO 驱动。

                                                                                • 获取设备树中 pin 信息,管理系统中所有的可以控制的 pin, 在系统初始化的时候, 枚举所有可以控制的 pin, 并标识这些 pin
                                                                                  • 根据获取到的 pin 信息来设置 pin 的复用功能,对于 SOC 而言, 其引脚除了配置成普通的 GPIO 之外,若干个引脚还可以组成一个 pin group, 形成特定的功能
                                                                                  • 7.2 pinctrl的设备树设置

                                                                                  • 根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。

                                                                                    开发时只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成。

                                                                                    iomuxc: iomuxc@020e0000 {compatible = "fsl,imx6ul-iomuxc";reg = ;pinctrl-names = "default";pinctrl-0 = ;imx6ul-evk {pinctrl_hog_1: hoggrp-1 {fsl,pins = ;};......}

                                                                                    在设备树里面创建一个节点来描述 PIN 的配置信息。pinctrl 子系统一般在iomuxc子节点下,所有需要配置用户自己的pinctrl需要在该节点下添加。

                                                                                    例

                                                                                  • compatible 属性值为“fsl,imx6ul-iomuxc” ,
                                                                                    • UART1_RTS_B 这个 PIN 是作为 SD 卡的检测引脚
                                                                                    • pinctrl_hog_1 子节点所使用的 PIN 配置信息,如UART1_RTS_B 的配置信息
                                                                                      • 此宏定义后面跟着 5 个数字,0x0090 0x031C 0x0000 0x5 0x0,含义是
                                                                                      • MX6UL_PAD_UART1_RTS_B__GPIO1_IO19,这是一个宏定义,表 示 将 UART1_RTS_B 这个 IO 复用为 GPIO1_IO19(复用属性)

                                                                                          7.3 设备树中添加pinctrl模板

                                                                                        • 0x17059 就是 conf_reg 寄存器值 , 设置一个 IO 的上/下拉、驱动能力和速度(电气属性)
                                                                                          &iomuxc {pinctrl-names = "default";pinctrl-0 = ;imx6ul-evk {......pinctrl_led: ledgrp{fsl,pins = ;};......};
                                                                                          1. 添加pinctrl设备结点

                                                                                            同一个外设的 PIN 都放到一个节点里面,在 iomuxc 节点中下添加“pinctrl_test”节点。节点前缀一定要为“pinctrl_”。

                                                                                          设备树是通过属性来保存信息的,因此需要添加一个属性,属性名字一定要为** fsl,pins **

                                                                                        • 添加具体设备节点,调用pinctrl信息
                                                                                          1. test {    pinctrl-names = "default", "wake up";    pinctrl-0 = ;    pinctrl-1 = ;    /* 其他节点内容 */};

                                                                                          在根节点“/”下创建 LED 灯节点,节点名为“gpioled”

                                                                                          只需要关注gpioled设备节点下的pinctrl-names 和 pinctrl-0 两条语句, 这两句就是引用在 iomuxc 中配置的 pinctrl 节点

                                                                                          gpioled {#address-cells = ;#size-cells = ;compatible = "songwei-gpioled";pinctrl-names = "default";pinctrl-0 = ;led-gpio = ;status = "okay";};
                                                                                          • pinctrl-names = “default”, “wake up”; 设备的状态, 可以有多个状态, default 为状态 0, wake up 为状态 1。

                                                                                          • pinctrl-0 = ;第 0 个状态所对应的引脚配置, 也就是 default 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test里面的管脚配置。

                                                                                          • pinctrl-1 = ;第 1 个状态所对应的引脚配置, 也就是 wake up 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test_2里面的管脚配置。

                                                                                            例

                                                                                          • pinctrl-names = “default”; 设备的状态, 可以有多个状态, default 为状态 0。
                                                                                            • 八、GPIO子系统

                                                                                            • pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点

                                                                                              8.1 GPIO子系统工作内容

                                                                                              当使用 pinctrl 子系统将引脚的复用设置为 GPIO,可以使用 GPIO 子系统来操作GPIO

                                                                                            • 引脚功能的配置(设置为 GPIO,GPIO 的方向, 输入输出模式,读取/设置 GPIO 的值)
                                                                                            • 通过 GPIO 子系统功能要实现:

                                                                                              • 实现软硬件的分离(分离出硬件差异, 有厂商提供的底层支持; 软件分层。 驱动只需要调用接口 API 即可操作 GPIO)
                                                                                              • 8.2 GPIO子系统设备树设置

                                                                                              • iommu 内存管理(直接调用宏即可操作 GPIO)
                                                                                                gpioled {#address-cells = ;#size-cells = ;compatible = "songwei-gpioled";pinctrl-names = "default";pinctrl-0 = ;//gpio信息led-gpio = ;status = "okay";};

                                                                                                在具体设备节点中添加GPIO信息

                                                                                              • led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。
                                                                                                • 8.3 API函数

                                                                                                • 稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号
                                                                                                  int gpio_request(unsigned gpio, const char *label)
                                                                                                  1. gpio_request

                                                                                                    gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request进行申请。

                                                                                                  2. label:给 gpio 设置个名字。
                                                                                                    • gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信

                                                                                                      息,此函数会返回这个 GPIO 的标号

                                                                                                    • 返回值: 0,申请成功;其他值,申请失败。
                                                                                                    • void gpio_free(unsigned gpio);  // gpio:要释放的 gpio 标号。
                                                                                                    • gpio_free

                                                                                                      如果不使用某个 GPIO ,需要调用 gpio_free 函数进行释放。

                                                                                                      int gpio_direction_input(unsigned gpio) //gpio:要设置为输入的 GPIO 标号。
                                                                                                    • gpio_direction_input

                                                                                                      设置某个 GPIO 为输入

                                                                                                      int gpio_direction_output(unsigned gpio, int value)
                                                                                                    • gpio_direction_output

                                                                                                      设置某个 GPIO 为输出,并且设置默认输出值。

                                                                                                    • gpio:要设置为输出的 GPIO 标号。
                                                                                                      • value: GPIO 默认输出值。
                                                                                                      • 返回值: 0,设置成功;负值,设置失败
                                                                                                      • #define gpio_get_value __gpio_get_valueint __gpio_get_value(unsigned gpio)
                                                                                                      • gpio_get_value

                                                                                                        获取某个 GPIO 的值(0 或 1)

                                                                                                      • gpio:要获取的 GPIO 标号。
                                                                                                        • 返回值: 非负值,得到的 GPIO 值;负值,获取失败
                                                                                                        • #define gpio_set_value __gpio_set_valuevoid __gpio_set_value(unsigned gpio, int value)
                                                                                                        • gpio_set_value

                                                                                                          设置某个 GPIO 的值

                                                                                                        • gpio:要设置的 GPIO 标号。
                                                                                                          • value: 要设置的值
                                                                                                          • 8.4 GPIO相关OF函数

                                                                                                  int of_gpio_named_count(struct device_node *np, const char *propname)
                                                                                                  1. of_gpio_named_count

                                                                                                    获取设备树某个属性里面定义了几个GPIO 信息。

                                                                                                  2. np:设备节点。
                                                                                                    • propname:要统计的 GPIO 属性。
                                                                                                    • 返回值: 正值,统计到的 GPIO 数量;负值,失败
                                                                                                    • int of_gpio_count(struct device_node *np)
                                                                                                    • of_gpio_count

                                                                                                      此函数统计的是gpios属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的GPIO 信息

                                                                                                    • np:设备节点。
                                                                                                      • 返回值: 正值,统计到的 GPIO 数量;负值,失败
                                                                                                      • int of_get_named_gpio(struct device_node *np,                       const char *propname,                       int index)
                                                                                                      • of_get_named_gpio

                                                                                                        获取 GPIO 编号,在Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似的属性信息转换为对应的 GPIO 编号。

                                                                                                      • np:设备节点。
                                                                                                        • propname:包含要获取 GPIO 信息的属性名。
                                                                                                        • index: GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0。
                                                                                                        • 返回值: 正值,获取到的 GPIO 编号;负值,失败。
                                                                                                        • 8.5 pinctrl和gpio子系统使用程序框架

                                                                                                  九、内核并发与竞争

                                                                                                  int ret = 0;    /* 1 、获取设备节点:alphaled */    gpio_led.nd = of_find_node_by_path("/gpioled");    if(gpio_led.nd == NULL)    {        printk("songwei_led node can not found!\r\n");        return -EINVAL;    }else    {        printk("songwei_led node has been found!\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  

                                                                                                  9.1 并发与竞争概念

                                                                                                • 多线程并发访问, Linux 是多任务(线程)的系统,多线程访问是最基本的原因
                                                                                                • Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。

                                                                                                  • 并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。

                                                                                                    • 抢占式并发访问,Linux 内核支持抢占,调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
                                                                                                    • 中断程序并发访问,硬件中断的权利可以是很大的。
                                                                                                    • SMP(多核)核间并发访问,多核 CPU 存在核间并发访问。
                                                                                                    • 9.2 原子操作

                                                                                                    • 竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。

                                                                                                    • 临界资源: 多个进程访问的资源,共享数据段

                                                                                                    • 临界区:多个进程访问的代码段

                                                                                                      typedef struct {    int counter;} atomic_t;

                                                                                                      原子操作是指不能再进一步分割的操作。一般原子操作用于整形变量或者位操作。

                                                                                                      Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中

                                                                                                      atomic_t a; //定义 aatomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

                                                                                                      如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,

                                                                                                    • 原子变量 lock,用来实现一次只能允许一个应用访问 LED 灯,led_init 驱动入口函数会将 lock 的值设置为 1。
                                                                                                    • 原子操作API函数

                                                                                                      原子位操作 API 函数

                                                                                                      例

                                                                                                      • 每次打开驱动设备的时候先使用 atomic_dec_and_test 函数将 lock 减 1,如果 atomic_dec_and_test函数返回值为真就表示 lock 当前值为 0,说明设备可以使用
                                                                                                      • /* gpioled设备结构体 */struct gpioled_dev{......int led_gpio;/* led所使用的GPIO编号*/atomic_t lock;/* 原子变量 */};struct gpioled_dev gpioled;/* led设备 */static int led_open(struct inode *inode, struct file *filp){/* 通过判断原子变量的值来检查LED有没有被别的应用使用 */if (!atomic_dec_and_test(&gpioled.lock)) {atomic_inc(&gpioled.lock);/* 小于0的话就加1,使其原子变量等于0 */return -EBUSY;/* LED被使用,返回忙 */}......}static int led_release(struct inode *inode, struct file *filp){struct gpioled_dev *dev = filp->private_data;/* 关闭驱动文件的时候释放原子变量 */atomic_inc(&dev->lock);return 0;}static int __init led_init(void){int ret = 0;/* 初始化原子变量 */atomic_set(&gpioled.lock, 1);/* 原子变量初始值为1 */......return 0;}static void __exit led_exit(void){......}
                                                                                                      • 如果 atomic_dec_and_test 函数返回值为假,就表示 lock 当前值为负数(lock 值默认是 1),lock 值为负数的可能性只有一个,那就

                                                                                                        是其他设备正在使用 LED,退出之前调用函数 atomic_inc 将 lock 加 1,因为此时 lock 的值被减成了负数,必须要对其加 1,将 lock 的值

                                                                                                        变为 0。

                                                                                                        9.2 自旋锁

                                                                                                      • dev_stats 表示设备状态,如果为 0 的话表示设备还没有被使用,如果大于 0 的话就表示设备已经被使用了。
                                                                                                      • 对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。

                                                                                                        由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁。

                                                                                                        例

                                                                                                        • 调用 spin_lock_irqsave 函数获取锁,为了考虑到驱动兼容性,这里并没有使用 spin_lock 函数来获取锁。
                                                                                                        • 判断dev_stats 是否大于 0,如果是的话表示设备已经被使用了,那么就调用 spin_unlock_irqrestore函数释放锁,并且返回-EBUSY。
                                                                                                        • 如果设备没有被使用的话就在将 dev_stats 加 1,表示设备要被使用了,然后调用 spin_unlock_irqrestore 函数释放锁。
                                                                                                        • 在 release 函数中将 dev_stats 减 1,表示设备被释放了
                                                                                                        • /* gpioled设备结构体 */struct gpioled_dev{......int dev_stats;/* 设备使用状态,0,设备未使用;>0,设备已经被使用 */spinlock_t lock;/* 自旋锁 */};struct gpioled_dev gpioled;/* led设备 */static int led_open(struct inode *inode, struct file *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);/* 解锁 */return 0;}static int led_release(struct inode *inode, struct file *filp){....../* 关闭驱动文件的时候将dev_stats减1 */spin_lock_irqsave(&dev->lock, flags);/* 上锁 */if (dev->dev_stats) {dev->dev_stats--;}spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */return 0;}/* * @description: 驱动入口函数 * @param : 无 * @return : 无 */static int __init led_init(void){int ret = 0;/*  初始化自旋锁 */spin_lock_init(&gpioled.lock);......}/* * @description: 驱动出口函数 * @param : 无 * @return : 无 */static void __exit led_exit(void){......}
                                                                                                        • 自旋锁的工作就是保护dev_stats 变量,真正实现对设备互斥访问的是 dev_stats。

                                                                                                          9.3 信号量

                                                                                                        • 信号量可以使等待资源线程进入休眠状态,适用于占用资源比较久的场合
                                                                                                        • Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

                                                                                                          • 信号量会引起休眠,中断不能休眠,所以信号量不能用于中断。
                                                                                                          • /* gpioled设备结构体 */struct gpioled_dev{......struct semaphore sem;/* 信号量 */};struct gpioled_dev gpioled;/* led设备 */static int led_open(struct inode *inode, struct file *filp){....../* 获取信号量 */if (down_interruptible(&gpioled.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断 */return -ERESTARTSYS;}#if 0down(&gpioled.sem);/* 不能被信号打断 */#endifreturn 0;}static int led_release(struct inode *inode, struct file *filp){......up(&dev->sem);/* 释放信号量,信号量值加1 */return 0;}/* 设备操作函数 */static struct file_operations gpioled_fops = {......};static int __init led_init(void){int ret = 0;/* 初始化信号量 */sema_init(&gpioled.sem, 1);......}/* * @description: 驱动出口函数 * @param : 无 * @return : 无 */static void __exit led_exit(void){......}
                                                                                                          • 如果共享资源的持有时间比较短,不适合使用信号量,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势,此时使用自旋锁。

                                                                                                            例

                                                                                                            9.4 互斥体

                                                                                                          • mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
                                                                                                          • Linux 提供了专门的互斥体mutex (等效信号量为1) 。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体(***锁)。在 Linux 驱动的时遇到需要互斥访问的地方一般使用 mutex。

                                                                                                            • mutex 保护的临界区可以调用引起阻塞的 API 函数(信号量也可以)
                                                                                                            • /* gpioled设备结构体 */struct gpioled_dev{......struct mutex lock;/* 互斥体 */};struct gpioled_dev gpioled;/* led设备 */static int led_open(struct inode *inode, struct file *filp){....../* 获取互斥体,可以被信号打断 */if (mutex_lock_interruptible(&gpioled.lock)) {return -ERESTARTSYS;}#if 0mutex_lock(&gpioled.lock);/* 不能被信号打断 */#endifreturn 0;}static int led_release(struct inode *inode, struct file *filp){....../* 释放互斥锁 */mutex_unlock(&dev->lock);return 0;}/* 设备操作函数 */static struct file_operations gpioled_fops = {......};static int __init led_init(void){....../* 初始化互斥体 */mutex_init(&gpioled.lock);......}static void __exit led_exit(void){......}
                                                                                                            • 因为一次只有一个线程可以持有 mutex,所以,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁

                                                                                                              十、内核定时器

                                                                                                              10.1 内核时间管理

                                                                                                              节拍率

                                                                                                              内核必须管理系统的运行时间以及当前的日期和时间。

                                                                                                              硬件为内核提供了一个系统定时器用以计算流逝的时间, 系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定, 称节拍率。

                                                                                                              当时钟中断发生时, 内核就通过一种特殊中断处理程序对其进行处理。 内核知道连续两次时钟中断的间隔时间。 这个间隔时间称为节拍(tick) 。内核就是靠这种已知的时钟中断来计算墙上时间和系统运行时间。

                                                                                                              # undef HZ# define HZ CONFIG_HZ# define USER_HZ 100# define CLOCKS_PER_SEC (USER_HZ)

                                                                                                              系统定时器频率是通过静态预处理定义的(HZ), 在系统启动时按照 Hz 对硬件进行设置。一般 ARM 体系结构的节拍率多数都等于 100。

                                                                                                              在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率, 按照如下路径打开配置界面:

                                                                                                              -> Kernel Features

                                                                                                              -> Timer frequency ( [=y])

                                                                                                              Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。

                                                                                                            • 提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms。能够以更高的精度运行,时间测量也更加准确。
                                                                                                            • 宏 HZ 就是 CONFIG_HZ,HZ=100,后面编写 Linux驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。

                                                                                                              • 高节拍率:优点

                                                                                                                  jiffies

                                                                                                                • 高节拍率:缺点
                                                                                                                  • 高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的精力去处理中断。中断服务函数占用处理器的时间增加,需要根据实际情况,选择合适的系统节拍率。
                                                                                                                    unsigned long timeout;timeout = jiffies + (2 * HZ); /* 超时的时间点 */..../* 判断有没有超时 */if(time_before(jiffies, timeout)) {    /* 超时未发生 */} else {    /* 超时发生 */}

                                                                                                                    全局变量 jiffies 用来记录自系统启动以来产生的节拍的总数。 启动时, 内核将该变量初始化为 0, 每次时钟中断处理程序都会增加该变量的值。

                                                                                                                    因为一秒内时钟中断的次数等于 Hz, 所以 jiffes 一秒内增加的值为 Hz, 系统运行时间以秒为单位计算, 就等于time = jiffes/Hz( jiffes = seconds*HZ)

                                                                                                                    当 jiffies 变量的值超过它的最大存放范围后就会发生溢出, 对于 32 位无符号长整型, 最大取值为 2^32-1,在溢出前, 定时器节拍计数最大为 4294967295, 如果节拍数达到了最大值后还要继续增加的话, 它的值会回绕到 0。

                                                                                                                    • 如果 unkown 超过 known 的话, time_after 函数返回真, 否则返回假。

                                                                                                                    • 如果 unkown 没有超过 known的话 time_before 函数返回真, 否则返回假。

                                                                                                                    • time_after_eq 函数在time_after上,多判断等于这个条件, time_before_eq 也类似

                                                                                                                      10.2 内核定时器

                                                                                                                      为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数

                                                                                                                      • 定时 10ms

                                                                                                                        jiffies +msecs_to_jiffies(10)

                                                                                                                      • 定时 10us

                                                                                                                        jiffies +usecs_to_jiffies(10)

                                                                                                                        struct timer_list{    struct list_head entry;    unsigned long expires; /* 定时器超时时间, 单位是节拍数 */    struct tvec_base *base;    void (*function)(unsigned long); /* 定时处理函数指针 */    unsigned long data; /* 要传递给 function 函数的参数 */    int slack;};

                                                                                                                        Linux 内核定时器采用系统时钟来实现,只需要提供超时时间(定时值)和定时处理函数即可。、

                                                                                                                        内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。

                                                                                                                        Linux 内核使用 timer_list 结构体表示内核定时器

                                                                                                                        例 驱动层

                                                                                                                        ......#define CLOSE_CMD (_IO(0XEF, 0x1))/* 关闭定时器 */#define OPEN_CMD(_IO(0XEF, 0x2))/* 打开定时器 */#define SETPERIOD_CMD(_IO(0XEF, 0x3))/* 设置定时器周期命令 */....../* timer设备结构体 */struct timer_dev{......int timeperiod; /* 定时周期,单位为ms */struct timer_list timer;/* 定义一个定时器*/spinlock_t lock;/* 定义自旋锁 */};struct timer_dev timerdev;/* timer设备 */static int timer_open(struct inode *inode, struct file *filp){......timerdev.timeperiod = 1000;/* 默认周期为1s */ret = led_init();/* 初始化LED IO */if (ret private_data;int timerperiod;unsigned long flags;switch (cmd) {case CLOSE_CMD:/* 关闭定时器 */del_timer_sync(&dev->timer);break;case OPEN_CMD:/* 打开定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));break;case SETPERIOD_CMD: /* 设置定时器周期 */spin_lock_irqsave(&dev->lock, flags);dev->timeperiod = arg;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));break;default:break;}return 0;}/* 设备操作函数 */static struct file_operations timer_fops = {.owner = THIS_MODULE,.open = timer_open,.unlocked_ioctl = timer_unlocked_ioctl,};/* 定时器回调函数 */void timer_function(unsigned long arg){struct timer_dev *dev = (struct timer_dev *)arg;static int sta = 1;int timerperiod;unsigned long flags;sta = !sta;/* 每次都取反,实现LED灯反转 */gpio_set_value(dev->led_gpio, sta);/* 重启定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));  }/* * @description: 驱动入口函数 * @param : 无 * @return : 无 */static int __init timer_init(void){....../* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */init_timer(&timerdev.timer);timerdev.timer.function = timer_function;timerdev.timer.data = (unsigned long)&timerdev;return 0;}static void __exit timer_exit(void){gpio_set_value(timerdev.led_gpio, 1);/* 卸载驱动的时候关闭LED */del_timer_sync(&timerdev.timer);/* 删除timer */#if 0del_timer(&timerdev.tiemr);#endif......}

                                                                                                                        例 应用层

                                                                                                                        十一、设备控制接口(ioctl)

                                                                                                                        /* 命令值 */#define CLOSE_CMD (_IO(0XEF, 0x1))/* 关闭定时器 */#define OPEN_CMD(_IO(0XEF, 0x2))/* 打开定时器 */#define SETPERIOD_CMD(_IO(0XEF, 0x3))/* 设置定时器周期命令 */int main(int argc, char *argv[]){int fd, ret;char *filename;unsigned int cmd;unsigned int arg;unsigned char str[100];if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd  

                                                                                                                        11.1 应用层

                                                                                                                        ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。有些命令是实在找不到对应的操作函数, 拓展一些file_operations给出的接口中没有的自定义功能,则需要使用到ioctl()函数。一些没办法归类的函数就统一放在ioctl这个函数操作中,通过指定的命令来实现对应的操作。

                                                                                                                        #include int ioctl(int fd, unsigned long request, (...)arg);

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

                                                                                                                        11.2 驱动层

                                                                                                                        • fd:文件描述符

                                                                                                                        • request:命令码,应用程序通过下发命令码来控制驱动程序完成对应操作。

                                                                                                                        • (…)arg:可变参数arg,一些情况下应用程序需要向驱动程序传参,参数就通过arg来传递。只能传递一个参数,但内核不会检查这个参数的类型。那么就有两种传参方式:只传一个整数,传递一个指针。

                                                                                                                        • 返回值:如果ioctl执行成功,它的返回值就是驱动程序中ioctl接口给的返回值,驱动程序可以通过返回值向用户程序传参。但驱动程序最好返回一个非负数,因为用户程序中的ioctl运行失败时一定会返回-1并设置全局变量errorno。

                                                                                                                          #include long (*unlocked_ioctl) (struct file * fp, unsigned int request, unsigned long args);long (*compat_ioctl) (struct file * fp, unsigned int request, unsigned long args);

                                                                                                                          驱动程序的ioctl函数体中,实现了一个switch-case结构,每一个case对应一个命令码,case内部是驱动程序实现该命令的相关操作。

                                                                                                                          12.2 ioctr应用和驱动的协议

                                                                                                                          • inode和fp用来确定被操作的设备

                                                                                                                          • request就是用户程序下发的命令

                                                                                                                          • args就是用户程序在必要时传递的参数

                                                                                                                          • 返回值:可以在函数体中随意定义返回值,这个返回值也会被直接返回到用户程序中。通常使用非负数表示正确的返回,而返回一个负数系统会判定为ioctl调用失败。

                                                                                                                          • unlocked_ioctl在无大内核锁(BKL)的情况下调用。64位用户程序运行在64位的kernel,或32位的用户程序运行在32位的kernel上,都是调用unlocked_ioctl函数。

                                                                                                                          • compat_ioctl是64位系统提供32位ioctl的兼容方法,也在无大内核锁的情况下调用。即如果是32位的用户程序调用64位的kernel,则会调用compat_ioctl。如果驱动程序没有实现compat_ioctl,则用户程序在执行ioctl时会返回错误Not a typewriter。

                                                                                                                          • 在字符设备驱动开发中,一般情况下只要实现 unlocked_ioctl 函数即可,因为在 vfs 层的代码是直接调用 unlocked_ioctl 函数

                                                                                                                            #include/uapi/asm-generic/ioctl.h #define _IOC(dir,type,nr,size) \    (((dir)   _IOC_TYPESHIFT) & _IOC_TYPEMASK)#define _IOC_NR(nr)     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)#define _IOC_SIZE(nr)       (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

                                                                                                                            ioctl函数的第二个参数 cmd 为用户与驱动的协议,理论上可以为任意 int 型数据,,但是为了确保该协议的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图所示:

                                                                                                                            • dir(direction):ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据

                                                                                                                            • type(device type):设备类型,占据 8 bit,也称为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识

                                                                                                                            • nr(number):命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增

                                                                                                                            • size:与体系结构相关,ARM下占14bit(_IOC_SIZEBITS),如果数据是int,内核给这个赋的值就是sizeof(int)。

                                                                                                                              在内核中,提供了宏接口以生成上述格式的 ioctl 命令:

                                                                                                                              十二、中断机制

                                                                                                                              • _IOC_DIR(nr) :提取方向

                                                                                                                              • _IOC_TYPE(nr) :提取幻数

                                                                                                                              • _IOC_NR(nr) :提取序数

                                                                                                                              • _IOC_SIZE(nr) :提取数据大小

                                                                                                                                12.1 中断API函数

                                                                                                                                中断是指 CPU 在执行程序的过程中, 出现了某些突发事件急待处理, CPU 必须暂停当前程序的执行,转去处理突发事件, 处理完毕后又返回原程序被中断的位置继续执行。

                                                                                                                                unsigned int irq_of_parse_and_map(struct device_node *dev,int index)
                                                                                                                                1. 获取中断号函数

                                                                                                                                  每个中断都有一个中断号,通过中断号即可区分不同的中断。在 Linux 内核中使用一个 int 变量表示中断号,

                                                                                                                                  或者中断号, 中断信息一般写到了设备树里面, 可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号。

                                                                                                                                2. dev: 设备节点
                                                                                                                                  • index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
                                                                                                                                  • 返回值:中断号
                                                                                                                                  • int gpio_to_irq(unsigned int gpio)
                                                                                                                                  • 使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号

                                                                                                                                  • gpio: 要获取的 GPIO 编号
                                                                                                                                    • 返回值: GPIO 对应的中断号
                                                                                                                                    • int request_irq(unsigned int irq,                irq_handler_t handler,                unsigned long flags,                const char *name,                void *dev)
                                                                                                                                    • 申请中断函数

                                                                                                                                      Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断,

                                                                                                                                      request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。

                                                                                                                                      request_irq 函数会激活(使能)中断,所以不需要手动去使能中断。

                                                                                                                                      void free_irq(unsigned int irq,void *dev)
                                                                                                                                • irq:要申请中断的中断号

                                                                                                                                • handler:中断处理函数,当中断发生会执行此中断处理函数

                                                                                                                                • flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志

                                                                                                                                • name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字

                                                                                                                                • dev: 如果将 flags 设置为 IRQF_SHARED, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。

                                                                                                                                • 返回值: 0 中断申请成功,负值中断申请失败,如果返回-EBUSY 表示中断已经被申请了

                                                                                                                                  中断标志

                                                                                                                                  1. 中断释放函数

                                                                                                                                    中断使用完成以后就要通过 free_irq 函数释放掉相应的中断。 如果中断不是共享的,free_irq 会删除中断处理函数并且禁止中断。

                                                                                                                                  2. irq: 要释放的中断号
                                                                                                                                    • dev:如果中断设置为共享(IRQF_SHARED),此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉
                                                                                                                                    • irqreturn_t (*irq_handler_t) (int, void *)
                                                                                                                                    • 中断处理函数

                                                                                                                                      使用 request_irq 函数申请中断的时候需要设置中断处理函数

                                                                                                                                      enum irqreturn {    IRQ_NONE = (0     void (*action)(struct softirq_action *);};    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};     struct tasklet_struct *next; /* 下一个 tasklet */    unsigned long state; /* tasklet 状态 */    atomic_t count; /* 计数器, 记录对 tasklet 的引用数 */    void (*func)(unsigned long); /* tasklet 执行的函数 */    unsigned long data; /* 函数 func 的参数 */};    /* tasklet 具体处理内容 */} /* 中断处理函数 */irqreturn_t test_handler(int irq, void *dev_id){    ......    /* 调度 tasklet */    tasklet_schedule(&testtasklet);    ......} /* 驱动入口函数 */static int __init xxxx_init(void){    ......    /* 初始化 tasklet */    tasklet_init(&testtasklet, testtasklet_func, data);    /* 注册中断处理函数 */    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);    ......}    atomic_long_t data;    struct list_head entry;    work_func_t func; /* 工作队列处理函数 */};    struct list_head pwqs;    struct list_head list;    struct mutex mutex;    int work_color;    int flush_color;    atomic_t nr_pwqs_to_flush;    struct wq_flusher *first_flusher;    struct list_head flusher_queue;    struct list_head flusher_overflow;    struct list_head maydays;    struct worker *rescuer;    int nr_drainers;    int saved_max_active;    struct workqueue_attrs *unbound_attrs;    struct pool_workqueue *dfl_pwq;    char name[WQ_NAME_LEN];    struct rcu_head rcu;    unsigned int flags ____cacheline_aligned;    struct pool_workqueue __percpu *cpu_pwqs;    struct pool_workqueue __rcu *numa_pwq_tbl[];};    union {        struct list_head entry;        struct hlist_node hentry;    };    struct work_struct *current_work;    work_func_t current_func;    struct pool_workqueue *current_pwq;    bool desc_valid;    struct list_head scheduled;    struct task_struct *task;    struct worker_pool *pool;    struct list_head node;    unsigned long last_active;    unsigned int flags;    int id;    char desc[WORKER_DESC_LEN];    struct workqueue_struct *rescue_wq;};    /* work 具体处理内容 */} /* 中断处理函数 */irqreturn_t test_handler(int irq, void *dev_id){    ......    /* 调度 work */    schedule_work(&testwork);    ......} /* 驱动入口函数 */static int __init xxxx_init(void){    ......    /* 初始化 work */    INIT_WORK(&testwork, testwork_func_t);     /* 注册中断处理函数 */    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);    ......}    compatible = "arm,cortex-a7-gic";    #interrupt - cells = r_wait); */        wake_up_interruptible(&dev->r_wait);    }}static int keyio_init(void){  ......    /*  初始化等待队列头 */    init_waitqueue_head(&imx6uirq.r_wait);    return 0;}static ssize_t imx6uirq_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt){ ......#if 0/*  加入等待队列,等待被唤醒, 也就是有按键按下 */ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));if (ret) {goto wait_error;}#else    DECLARE_WAITQUEUE(wait, current);/* 定义一个等待队列 */if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下 */    {add_wait_queue(&dev->r_wait, &wait);/* 将等待队列添加到等待队列头 */__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */schedule();/* 进行一次任务切换 */if(signal_pending(current)) /* 判断是否为信号引起的唤醒 */        {ret = -ERESTARTSYS;goto wait_error;}__set_current_state(TASK_RUNNING);      /* 将当前任务设置为运行状态 */    remove_wait_queue(&dev->r_wait, &wait);    /* 将对应的队列项从等待队列头删除 */}#endif    ......    wait_error:set_current_state(TASK_RUNNING);/* 设置任务为运行态 */remove_wait_queue(&dev->r_wait, &wait);/* 将等待队列移除 */return ret;}static struct file_operations imx6uirq_fops = {    .owner = THIS_MODULE,    .open = imx6uirq_open,    .read = imx6uirq_read,};
                                                                                                                                      • 第一个参数:要中断处理函数要相应的中断号

                                                                                                                                      • 第二个参数:一个指向 void 的指针,是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构

                                                                                                                                      • 返回值:irqreturn_t 类型

                                                                                                                                        irqreturn_t 类型定义如下所示:

                                                                                                                                      • 在设备结构体中添加一个等待队列头 r_wait,因为在 Linux 驱动中处理阻塞 IO需要用到等待队列。
                                                                                                                                  • 调用 init_waitqueue_head 函数初始化等待队列头 r_wait。
                                                                                                                                  • read驱动函数手动休眠等待按键按下
                                                                                                                                  • 采用等待事件来处理 read 的阻塞访问,wait_event_interruptible 函数等待 releasekey 有效,也就是有按键按下。
                                                                                                                                  • 法一:
                                                                                                                                    • 如果按键没有按下的话进程就会进入休眠状态,因为采用了 wait_event_interruptible 函数,因此进入休眠态的进程可以被信号打断。
                                                                                                                                    • 首先使用 DECLARE_WAITQUEUE 宏定义一个等待队列,
                                                                                                                                    • 法二:
                                                                                                                                      • 如果没有按键按下的话就使用 add_wait_queue 函数将当前任务的等待队列添加到等待队列头 r_wait 中。
                                                                                                                                      • 随后调用__set_current_state 函数设置当前进程的状态为 TASK_INTERRUPTIBLE,也就是可以被信号打断。
                                                                                                                                      • 接下来调用 schedule 函数进行一次任务切换,当前进程就会进入到休眠态。
                                                                                                                                      • 如果有按键按下,那么进入休眠态的进程就会唤醒,然后接着从休眠点开始运行。首先通过 signal_pending 函数判断一下进程是不是由信号唤醒的,如果是由信号唤醒的话就直接返回-ERESTARTSYS 这个错误码。
                                                                                                                                      • 如果不是由信号唤醒的(也就是被按键唤醒的)那么就在 调用__set_current_state 函数将任务状态设置为TASK_RUNNING,然后在调用 remove_wait_queue 函数将进程从等待队列中删除。
                                                                                                                                      • 定时器中断处理函数执行,表示有按键按下,先在判断一下是否是一次有效的按键,如果是的话就通过 wake_up 或者 wake_up_interruptible 函数来唤醒等待队列r_wait。
                                                                                                                                      • 13.3 非阻塞IO使用(轮询)

                                                                                                                                      • 完成read函数后,设置任务为运行态,将等待队列移除

                                                                                                                                        应用层

                                                                                                                                        1.select

                                                                                                                                        如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,即轮询。 poll、 epoll 和 select 可以用于处理轮询。

                                                                                                                                        应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时,设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。

                                                                                                                                        int select(int nfds,            fd_set *readfds,            fd_set *writefds,            fd_set *exceptfds,            struct timeval *timeout)
                                                                                                                                      • nfds: 所要监视的这三类文件描述集合, 最大文件描述符加 1
                                                                                                                                        • readfds 用于监视指定描述符集的读变化,监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取, seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,会根据 timeout 参数来判断是否超时。若将 readfs设置为 NULL,表示不关心任何文件的读变化。
                                                                                                                                        • readfds、 writefds 和 exceptfds:指向描述符集合。指明了关心哪些描述符.这三个参数都是 fd_set 类型的, fd_set 类型变量的每一个位都代表了一个文件描述符。
                                                                                                                                          • writefs 用于监视文件是否可以进行写操作。
                                                                                                                                          • exceptfds 用于监视文件的异常。
                                                                                                                                          • timeout:超时时间,当调用 select 函数等待某些文件描述符可以设置超时时间
                                                                                                                                          • void FD_ZERO(fd_set *set)  void FD_SET(int fd, fd_set *set)void FD_CLR(int fd, fd_set *set)int FD_ISSET(int fd, fd_set *set)
                                                                                                                                          • 返回值: 0: 超时发生,没有文件描述符可以进行操作; -1: 发生错误; 其他值: 可以进行操作的文件描述符个数

                                                                                                                                            从一个设备文件中读取数据,要定义一个 fd_set 变量,这个变量要传递给参数 readfds。当定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:

                                                                                                                                          • FD_ZERO:将 fd_set 变量的所有位都清零
                                                                                                                                            • FD_SET:将 fd_set 变量的某个位置 1,向 fd_set 添加fd文件描述符
                                                                                                                                            • FD_CLR:将 fd_set变量的某个位清零,将fd文件描述符从 fd_set 中删除
                                                                                                                                            • struct timeval {    long tv_sec; /* 秒 */    long tv_usec; /* 微妙 */};
                                                                                                                                            • FD_ISSET: 测试文件描述符 fd是否属于某个集合

                                                                                                                                              超时时间使用结构体 timeval 表示, 当 timeout 为 NULL 的时候就表示无限期的等待。

                                                                                                                                              void main(void){    int ret, fd; /* 要监视的文件描述符 */    fd_set readfds; /* 读操作文件描述符集 */    struct timeval timeout; /* 超时结构体 */    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */     FD_ZERO(&readfds); /* 清除 readfds */    FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */     /* 构造超时时间 */    timeout.tv_sec = 0;    timeout.tv_usec = 500000; /* 500ms */     ret = select(fd + 1, &readfds, NULL, NULL, &timeout);    switch (ret)     {        case 0: /* 超时 */            printf("timeout!\r\n");            break;        case -1: /* 错误 */            printf("error!\r\n");            break;        default: /* 可以读取数据 */            if(FD_ISSET(fd, &readfds)) /* 判断fd 文件描述符是否在readfds里面 */            {                 /* 使用 read 函数读取数据 */            }            break;    }}

                                                                                                                                              应用层select函数非阻塞访问模块

                                                                                                                                              2.poll
                                                                                                                                              int poll(struct pollfd *fds,        nfds_t nfds,        int timeout)    

                                                                                                                                              在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024。可以修改内核将监视的文件描述符数量改大。这时可以使用 poll 函数,poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制。

                                                                                                                                            • fds: 要监视的文件描述符集合以及要监视的事件, 为一个数组,数组元素都是结构体 pollfd类型的。
                                                                                                                                              • nfds: poll 函数要监视的文件描述符数量
                                                                                                                                              • timeout: 超时时间,单位为 ms
                                                                                                                                              • struct pollfd {    int fd; /* 文件描述符 */    short events; /* 请求的事件 */    short revents; /* 返回的事件 */};
                                                                                                                                              • 返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,发生事件或错误的文件描述符数量; 0:超时; -1:发生错误,并且设置 errno 为错误类型

                                                                                                                                                pollfd 结构体

                                                                                                                                              • POLLIN 有数据可以读取。
                                                                                                                                                • fd 是要监视的文件描述符,如果 fd 无效,则events 监视事件也无效,并且 revents返回 0。

                                                                                                                                                • events 是要监视的事件,可监视的事件类型如下:

                                                                                                                                                  • POLLPRI 有紧急的数据需要读取。
                                                                                                                                                  • POLLOUT 可以写数据。
                                                                                                                                                  • POLLERR 指定的文件描述符发生错误。
                                                                                                                                                  • POLLHUP 指定的文件描述符挂起。
                                                                                                                                                  • POLLNVAL 无效的请求。
                                                                                                                                                  • POLLRDNORM 等同于 POLLIN
                                                                                                                                                  • 3.epoll
                                                                                                                                                  • revents 是返回的事件, 由 Linux 内核设置具体的返回事件。

                                                                                                                                                    应用层 poll 函数非阻塞访问模块

                                                                                                                                                    void main(void){    int ret;    int fd; /* 要监视的文件描述符 */    struct pollfd fds;     fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */     /* 构造结构体 */    fds.fd = fd;    fds.events = POLLIN; /* 监视数据是否可以读取 */    ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */    if (ret) /* 数据有效 */    {         ......        /* 读取数据 */        ......    }     else if (ret == 0) /* 超时 */    {         ......    }     else if (ret  
                                                                                                                                                    int epoll_create(int size)

                                                                                                                                                    selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。

                                                                                                                                                    epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。

                                                                                                                                                    应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄

                                                                                                                                                  • size:从 Linux2.6.8 开始此参数已经没有意义了,填写一个大于 0 的值就可以
                                                                                                                                                    • int epoll_ctl(int epfd,            int op,            int fd,            struct epoll_event *event)
                                                                                                                                                    • 返回值: epoll 句柄,如果为-1 的话表示创建失败

                                                                                                                                                      epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件

                                                                                                                                                    • epfd: 要操作的 epoll 句柄,使用 epoll_create 函数创建的 epoll 句柄
                                                                                                                                                      • EPOLL_CTL_ADD 向 epfd 添加文件参数 fd 表示的描述符
                                                                                                                                                      • op: 要对epoll 句柄进行的操作,可以设置为:
                                                                                                                                                        • EPOLL_CTL_MOD 修改参数 fd 的 event 事件。
                                                                                                                                                        • EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符
                                                                                                                                                        • fd:要监视的文件描述符
                                                                                                                                                        • event: 要监视的事件类型,为 epoll_event 结构体类型指针
                                                                                                                                                        • struct epoll_event {        uint32_t events; /* epoll 事件 */        epoll_data_t data; /* 用户数据 */};
                                                                                                                                                        • 返回值: 0:成功; -1:失败,并且设置 errno 的值为相应的错误码。

                                                                                                                                                          监视的事件类型为 epoll_event 结构体类型指针

                                                                                                                                                        • EPOLLIN 有数据可以读取
                                                                                                                                                          • events 表示要监视的事件,可选的事件如下
                                                                                                                                                            • EPOLLOUT 可以写数据
                                                                                                                                                            • EPOLLPRI 有紧急的数据需要读取
                                                                                                                                                            • EPOLLERR 指定的文件描述符发生错误
                                                                                                                                                            • EPOLLHUP 指定的文件描述符挂起
                                                                                                                                                            • EPOLLET 设置 epoll 为边沿触发,默认触发模式为水平触发
                                                                                                                                                            • int epoll_wait(int epfd,    struct epoll_event *events,    int maxevents,    int timeout)
                                                                                                                                                            • EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个 fd,就需要将fd 重新添加到 epoll 里面

                                                                                                                                                              最后通过 epoll_wait 函数来等待事件的发生

                                                                                                                                                            • epfd: 要等待的 epoll
                                                                                                                                                              • events: 指向epoll_event结构体的数组,当有事件发生的时Linux内核会填写 events,调用者可以根据 events 判断发生了哪些事件
                                                                                                                                                              • maxevents: events 数组大小,必须大于 0
                                                                                                                                                              • timeout: 超时时间,单位为 ms
                                                                                                                                                              • 驱动层

                                                                                                                                                              • 返回值: 0:超时; -1:错误;其他值:准备就绪的文件描述符数量

                                                                                                                                                                epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当设计到的文件描述符比较少的时候就适合用 selcet 和 poll。

                                                                                                                                                                unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

                                                                                                                                                                当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问,驱动程序file_operations 操作集中的 poll 函数就会执行。

                                                                                                                                                              • filp: 要打开的设备文件(文件描述符)
                                                                                                                                                                • wait: poll_table_struct 类型指针, 由应用程序传递进来的,将此参数传递给poll_wait()
                                                                                                                                                                • POLLIN 有数据可以读取。
                                                                                                                                                                • 返回值: 向应用程序返回设备或者资源状态,返回状态有:
                                                                                                                                                                  • POLLPRI 有紧急的数据需要读取。
                                                                                                                                                                  • POLLOUT 可以写数据。
                                                                                                                                                                  • POLLERR 指定的文件描述符发生错误。
                                                                                                                                                                  • POLLHUP 指定的文件描述符挂起。
                                                                                                                                                                  • POLLNVAL 无效的请求。
                                                                                                                                                                  • void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
                                                                                                                                                                  • POLLRDNORM 等同于 POLLIN

                                                                                                                                                                    需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中

                                                                                                                                                                  • filp: 要打开的设备文件(文件描述符)
                                                                                                                                                                    • wait_address:要添加到 poll_table 中的等待队列头
                                                                                                                                                                    • /* imx6uirq 设备结构体 */struct imx6uirq_dev{  ......    wait_queue_head_t r_wait; /*  读等待队列头 */};struct imx6uirq_dev imx6uirq; /* irq 设备 */static irqreturn_t key0_handler(int irq, void *dev_id){        struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;    tasklet_schedule(&dev->irqkeydesc[0].testtasklet);    printk("tasklet ok\n");    return IRQ_RETVAL(IRQ_HANDLED);}static void testtasklet_func(unsigned long data){    ......    if(atomic_read(&dev->releasekey))  /*  完成一次按键过程 */    {                                              /* wake_up(&dev->r_wait); */        wake_up_interruptible(&dev->r_wait);    }}static int keyio_init(void){   ......    /*  初始化等待队列头 */    init_waitqueue_head(&imx6uirq.r_wait);    return 0;}static ssize_t imx6uirq_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt){    int ret = 0;    unsigned char keyvalue = 0;    unsigned char releasekey = 0;    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;   if (filp->f_flags & O_NONBLOCK)/* 非阻塞访问 */   { if(atomic_read(&dev->releasekey) == 0)/* 没有按键按下,返回-EAGAIN */return -EAGAIN;} else /* 阻塞访问 */    {/* 加入等待队列,等待被唤醒,也就是有按键按下 */ ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret)         {goto wait_error;}    }......wait_error:return ret;......}unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait){unsigned int mask = 0;struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;poll_wait(filp, &dev->r_wait, wait);/* 将等待队列头添加到poll_table中 */if(atomic_read(&dev->releasekey)) {/* 按键按下 */mask = POLLIN | POLLRDNORM;/* 返回PLLIN */}return mask;}static struct file_operations imx6uirq_fops = {    .owner = THIS_MODULE,    .open = imx6uirq_open,    .read = imx6uirq_read,    .poll = imx6uirq_poll,};
                                                                                                                                                                    • p:file_operations 中 poll 函数的 wait 参数

                                                                                                                                                                      驱动层poll函数模板(和应用层select、poll对应)

                                                                                                                                                                      十四、异步通知机制(信号通知)

                                                                                                                                                                      14.1 异步通知应用程序

                                                                                                                                                                      阻塞IO和非阻塞IO都需要应用程序主动去查询设备的使用情况。Linux 提供了异步通知机制,驱动程序能主动向应用程序发出通知。

                                                                                                                                                                      信号是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式通知可以访问,应用程序获取到信号后就可以从驱动设备中读取或者写入数据。

                                                                                                                                                                      sighandler_t signal(int signum, sighandler_t handler)
                                                                                                                                                                      1. 注册信号处理函数

                                                                                                                                                                        应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。

                                                                                                                                                                      2. signum:要设置处理函数的信号
                                                                                                                                                                        • handler: 信号的处理函数
                                                                                                                                                                        • typedef void (*sighandler_t)(int)
                                                                                                                                                                        • 返回值: 设置成功返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。

                                                                                                                                                                          信号中断处理函数

                                                                                                                                                                          #include #include int fcntl(int fd, int cmd);int fcntl(int fd, int cmd, long arg);int fcntl(int fd, int cmd, struct flock *lock);

                                                                                                                                                                          在处理函数执行相应的操作即可。

                                                                                                                                                                        • 将本应用程序的进程号告诉给内核

                                                                                                                                                                          fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性

                                                                                                                                                                          flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

                                                                                                                                                                          使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核。

                                                                                                                                                                        • 开启异步通知

                                                                                                                                                                          主要是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。

                                                                                                                                                                          static int fd = 0;/* 文件描述符 */static void sigio_signal_func(int signum){int err = 0;unsigned int keyvalue = 0;err = read(fd, &keyvalue, sizeof(keyvalue));if(err  
                                                                                                                                                                        • 应用程序模板

                                                                                                                                                                          14.2 异步通知驱动程序

                                                                                                                                                                      struct fasync_struct {    spinlock_t fa_lock;    int magic;    int fa_fd;    struct fasync_struct *fa_next;    struct file *fa_file;    struct rcu_head fa_rcu;};struct fasync_struct *async_queue; /* 异步相关结构体 */
                                                                                                                                                                      1. 内核要使用异步通知需要在驱动程序中定义一个 fasync_struct 结构体的指针变量

                                                                                                                                                                        一般将 fasync_struct 结构体指针变量定义到设备结构体中即可。

                                                                                                                                                                        int (*fasync) (int fd, struct file *filp, int on)
                                                                                                                                                                      2. 然后需要在设备驱动中实现 file_operations 操作集中的 fasync 函数。

                                                                                                                                                                        int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
                                                                                                                                                                      3. fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针

                                                                                                                                                                        void kill_fasync(struct fasync_struct **fp, int sig, int band)

                                                                                                                                                                        fasync_helper 函数的前三个参数就是 fasync 函数的三个参数,第四个参数就是要初始化的 fasync_struct 结构体指针变量。

                                                                                                                                                                        当应用程序通过fcntl(fd, F_SETFL, flags | FASYNC)改变fasync 标记的时,驱动程序file_operations 操作集中的 fasync 函数就会执行。

                                                                                                                                                                      4. 当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生中断。 kill_fasync函数负责发送指定的信号

                                                                                                                                                                      5. fp:要操作的 fasync_struct
                                                                                                                                                                        • sig: 要发送的信号
                                                                                                                                                                        • band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT
                                                                                                                                                                        • xxx_fasync(-1, filp, 0); /* 删除异步通知 */
                                                                                                                                                                        • 最后,在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct,fasync_struct 的释放函数为 fasync_helper。

                                                                                                                                                                          /* imx6uirq设备结构体 */struct imx6uirq_dev{......struct fasync_struct *async_queue;/* 异步相关结构体 */};struct imx6uirq_dev imx6uirq;/* irq设备 */void timer_function(unsigned long arg){......if(atomic_read(&dev->releasekey)) {/* 一次完整的按键过程 */if(dev->async_queue)kill_fasync(&dev->async_queue, SIGIO, POLL_IN);/* 释放SIGIO信号 */}......}/* * @description     : fasync函数,用于处理异步通知 * @param - fd: 文件描述符 * @param - filp    : 要打开的设备文件(文件描述符) * @param - on      : 模式 * @return          : 负数表示函数执行失败 */static int imx6uirq_fasync(int fd, struct file *filp, int on){struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;return fasync_helper(fd, filp, on, &dev->async_queue);  /* 初始化fasync_struct 结构体指针*/}static int imx6uirq_release(struct inode *inode, struct file *filp){return imx6uirq_fasync(-1, filp, 0); /* 删除异步通知 */}/* 设备操作函数 */static struct file_operations imx6uirq_fops = {.......poll = imx6uirq_poll,.fasync = imx6uirq_fasync,.release = imx6uirq_release,};

                                                                                                                                                                          xxx_fasync 函数就是file_operations 操作集中的 fasync 函数。

                                                                                                                                                                        • 驱动程序模板

                                                                                                                                                                          十五、Platform设备驱动框架

                                                                                                                                                                      15.1 设备驱动的分层思想

                                                                                                                                                                      return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2){    if(bottom_dev->funca)        return bottom_dev->funca(param1, param2);        /* 核心层通用的 funca 代码 */      bottom_dev->funca_ops1(); /*通用的步骤代码 A */    ...    bottom_dev->funca_ops2();/*通用的步骤代码 B */       ...    bottom_dev->funca_ops3();/*通用的步骤代码 C */}

                                                                                                                                                                      Linux 内核完全由 C 语言和汇编语言写成, 但是却频繁用到了面向对象的设计思想。

                                                                                                                                                                      在设备驱动方面,为同类的设备设计了一个框架, 框架中的核心层则实现了该设备通用的一些功能。同样的, 如果具体的设备不想使用核心层的函数, 它可以重载之。

                                                                                                                                                                      15.2 驱动的分离与分层

                                                                                                                                                                      上述 core_funca 的实现中, 会检查底层设备是否重载了 funca(), 如果重载了, 就调用底层的代码, 否则直接使用通用层的。 这样做的好处是, 核心层的代码可以处理绝大多数该类设备的funca()对应的功能,只有少数特殊设备需要重新实现 funca()。

                                                                                                                                                                      15.3 Platform 平台总线驱动模型

                                                                                                                                                                      将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息 (比如从设备树中获取到设备信息),根据获取到的设备信息来初始化设备。

                                                                                                                                                                      驱动只负责驱动,设备只负责设备,总线法将两者进行匹配。

                                                                                                                                                                      这就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离。

                                                                                                                                                                      当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有,就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动中查找与之匹配的驱动,如果有,也联系起来。 Linux 内核中大量的驱动程序都采用总线、驱动和设备模式。

                                                                                                                                                                      一、platform 驱动

                                                                                                                                                                      在 Linux 2.6 以后的设备驱动模型中, 需关心总线、 设备和驱动这 3 个实体, 总线将设备和驱动绑定。

                                                                                                                                                                      Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等的设备而言, 这自然不是问题。但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。

                                                                                                                                                                      基于这一背景, Linux 发明了一种虚拟的总线, 称为 platform 总线, 相应的设备称为 platform_device, 驱动称为 platform_driver。 平台总线模型就是把原来的驱动C文件给分成了俩个 C 文件,一个是 device.c(描述硬件信息,设备树可替代), 一个是 driver.c(驱动信息) 。

                                                                                                                                                                      struct platform_driver {    int (*probe)(struct platform_device *);    int (*remove)(struct platform_device *);    void (*shutdown)(struct platform_device *);    int (*suspend)(struct platform_device *, pm_message_t state);    int (*resume)(struct platform_device *);    struct device_driver driver;    const struct platform_device_id *id_table;    bool prevent_deferred_probe;};

                                                                                                                                                                      在 Linux 内核中, 用platform_driver结构体表示platform驱动,platform_driver 结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数

                                                                                                                                                                    • probe 函数: 当驱动与设备匹配成功以后 probe 函数就会执行, 一般驱动的提供者会编写
                                                                                                                                                                      • remove函数:当 driver 和 device 任意一个 remove 的时, 就会执行该函数
                                                                                                                                                                      • driver: device_driver 结构体变量, Linux 内核里面大量使用到了面向对象的思维, device_driver相当于基类,提供了最基础的驱动框架。 plaform_driver 继承了这个基类,在此基础上又添加了一些特有的成员变量
                                                                                                                                                                      • struct platform_device_id {    char name[PLATFORM_NAME_SIZE];    kernel_ulong_t driver_data;};
                                                                                                                                                                      • id_table 表保存 id 信息。 这些 id 信息存放着platformd 驱动所支持的驱动类型。 id_table是个表(数组), 每个元素的类型为 platform_device_id,

                                                                                                                                                                        platform_device_id 结构体内容如下:

                                                                                                                                                                        struct device_driver {    const char *name;    struct bus_type *bus;     struct module *owner;    const char *mod_name; /* used for built-in modules */     bool suppress_bind_attrs; /* disables bind/unbind via sysfs */     const struct of_device_id *of_match_table; //设备树的驱动匹配表    const struct acpi_device_id *acpi_match_table;    int (*probe) (struct device *dev);    int (*remove) (struct device *dev);    void (*shutdown) (struct device *dev);    int (*suspend) (struct device *dev, pm_message_t state);    int (*resume) (struct device *dev);    const struct attribute_group **groups;    const struct dev_pm_ops *pm;    struct driver_private *p;};

                                                                                                                                                                        device_driver 结构体定义内容如下:

                                                                                                                                                                        struct of_device_id {    char name[32];    char type[32];    char compatible[128];    const void *data;};

                                                                                                                                                                        of_match_table表就是采用设备树时驱动使用的匹配表,也是数组,每个匹配项都为 of_device_id 结构体类型

                                                                                                                                                                        二、platform 设备(可以被设备树替代)

                                                                                                                                                                        compatible: 在支持设备树的内核中, 就是通过设备节点的compatible属性值和of_match_table中每个项目的 compatible 成员变量进行比较, 如果有相等的就表示设备和此驱动匹配成功。

                                                                                                                                                                        驱动和设备匹配成功后,驱动会从设备里面获得硬件资源, 匹配成功了后, driver.c 要从 device.c(或者是设备树) 中获得硬件资源, 那么 driver.c 就是在 probe 函数中获得的。

                                                                                                                                                                        struct platform_device {    const char *name;    int id;    bool id_auto;    struct device dev;    u32 num_resources;    struct resource *resource;     const struct platform_device_id *id_entry;    char *driver_override; /* Driver name to force a match */     /* MFD cell pointer */    struct mfd_cell *mfd_cell;     /* arch specific additions */    struct pdev_archdata archdata;};

                                                                                                                                                                        在 platform 平台下用platform_device结构体表示platform设备, 如果内核支持设备树的话就不用使用 platform_device 来描述设备, 使用设备树去描述platform_device即可。

                                                                                                                                                                      • name:设备名字,要和所使用的 platform 驱动的 name 字段相同,否则设备就无法匹配到对应的驱动。比如对应的 platform 驱动的name字段为xxx-gpio,则此name字段也要设置为xxx-gpio。
                                                                                                                                                                        • id :用来区分如果设备名字相同的时,通过在后面添加一个数字来代表不同的设备
                                                                                                                                                                        • dev:内置的device结构体
                                                                                                                                                                        • num_resources :资源数量,一般为resource 资源的大小(个数),ARRAY_SIZE 来测量一个数组的元素个数。
                                                                                                                                                                        • struct resource {    resource_size_t start;    resource_size_t end;    const char *name;    unsigned long flags;    struct resource *parent, *sibling, *child;};
                                                                                                                                                                        • resource:指向一个资源结构体数组,即设备信息,比如外设寄存器等。

                                                                                                                                                                          Linux 内核使用 resource结构体表示资源

                                                                                                                                                                          int platform_device_register(struct platform_device *pdev)

                                                                                                                                                                          start 和 end 分别表示资源的起始和终止信息,对于内存类的资源,表示内存起始和终止地址, name 表示资源名字, flags 表示资源类型

                                                                                                                                                                          使用 platform_device_register 函数将设备信息注册到 Linux 内核中

                                                                                                                                                                        • pdev:要注册的 platform 设备
                                                                                                                                                                          • void platform_device_unregister(struct platform_device *pdev)
                                                                                                                                                                          • 返回值: 负数,失败; 0,成功

                                                                                                                                                                            如果不再使用 platform可以通过 platform_device_unregister 函数注销掉相应的 platform设备

                                                                                                                                                                          • pdev:要注销的 platform 设备
                                                                                                                                                                            • 三、platform 总线

                                                                                                                                                                            • 返回值: 无
                                                                                                                                                                              struct bus_type {    const char *name; /* 总线名字 */    const char *dev_name;    struct device *dev_root;    struct device_attribute *dev_attrs;    const struct attribute_group **bus_groups; /* 总线属性 */    const struct attribute_group **dev_groups; /* 设备属性 */    const struct attribute_group **drv_groups; /* 驱动属性 */     int (*match)(struct device *dev, struct device_driver *drv);    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);    int (*probe)(struct device *dev);    int (*remove)(struct device *dev);    void (*shutdown)(struct device *dev);     int (*online)(struct device *dev);    int (*offline)(struct device *dev);    int (*suspend)(struct device *dev, pm_message_t state);    int (*resume)(struct device *dev);    const struct dev_pm_ops *pm;    const struct iommu_ops *iommu_ops;    struct subsys_private *p;    struct lock_class_key lock_key;};

                                                                                                                                                                              platform设备和platform驱动,相当于把设备和驱动分离了, 需要 platform 总线进行配, platform 设备和 platform 驱动进行内核注册时, 都是注册到总线上。

                                                                                                                                                                              在 Linux 内核中使用 bus_type 结构体表示总线

                                                                                                                                                                              struct bus_type platform_bus_type = {    .name = "platform",    .dev_groups = platform_dev_groups,    .match = platform_match,    .uevent = platform_uevent,    .pm = &platform_dev_pm_ops,};

                                                                                                                                                                              match 函数:完成设备和驱动之间匹配的,总线使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。

                                                                                                                                                                              match 函数有两个参数: dev 和 drv,这两个参数分别为 device 和 device_driver 类型,即设备和驱动。

                                                                                                                                                                              platform 总线是 bus_type 的一个具体实例

                                                                                                                                                                              四、platform 总线具体匹配方法

                                                                                                                                                                              platform_match 匹配函数, 用来匹配注册到 platform 总线的设备和驱动。

                                                                                                                                                                              static int platform_match(struct device *dev,struct device_driver *drv){    struct platform_device *pdev = to_platform_device(dev);    struct platform_driver *pdrv = to_platform_driver(drv);     /*When driver_override is set,only bind to the matching driver*/    if (pdev->driver_override)        return !strcmp(pdev->driver_override, drv->name);     /* Attempt an OF style match first */    if (of_driver_match_device(dev, drv))        return 1;     /* Then try ACPI style match */    if (acpi_driver_match_device(dev, drv))        return 1;    /* Then try to match against the id table */    if (pdrv->id_table)        return platform_match_id(pdrv->id_table, pdev) != NULL;     /* fall-back to driver name match */    return (strcmp(pdev->name, drv->name) == 0);}

                                                                                                                                                                              查看platform_match函数,如何匹配驱动和设备的

                                                                                                                                                                              15.4 platform 总线框架例程

                                                                                                                                                                              驱动和设备的匹配有四种方法。

                                                                                                                                                                              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 字段。

                                                                                                                                                                              一、应用层

                                                                                                                                                                              二、驱动层(无设备树)

                                                                                                                                                                              int main(int argc, char *argv[]){    int fd, retvalue;    char *filename;    unsigned char databuf[1];    unsigned char readbuf[1];    if(argc != 3)    {        printf("Error Usage!\r\n");        return -1;    }    filename = argv[1];    fd = open(filename, O_RDWR);    if(fd  
                                                                                                                                                                              device.c
                                                                                                                                                                              /*  * 寄存器地址定义 */#define CCM_CCGR1_BASE(0X020C406C)#define SW_MUX_GPIO1_IO03_BASE(0X020E0068)#define SW_PAD_GPIO1_IO03_BASE(0X020E02F4)#define GPIO1_DR_BASE(0X0209C000)#define GPIO1_GDIR_BASE(0X0209C004)#define REGISTER_LENGTH4/*   * 设备资源信息,也就是LED0所使用的所有寄存器 */static struct resource led_resources[] = {[0] = {.start = CCM_CCGR1_BASE,.end = (CCM_CCGR1_BASE + REGISTER_LENGTH - 1),.flags = IORESOURCE_MEM,},[1] = {.start= SW_MUX_GPIO1_IO03_BASE,.end= (SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),.flags= IORESOURCE_MEM,},[2] = {.start= SW_PAD_GPIO1_IO03_BASE,.end= (SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),.flags= IORESOURCE_MEM,},[3] = {.start= GPIO1_DR_BASE,.end= (GPIO1_DR_BASE + REGISTER_LENGTH - 1),.flags= IORESOURCE_MEM,},[4] = {.start= GPIO1_GDIR_BASE,.end= (GPIO1_GDIR_BASE + REGISTER_LENGTH - 1),.flags= IORESOURCE_MEM,},};/* * platform设备结构体  */static struct platform_device leddevice = {.name = "imx6ul-led",.id = -1,.dev = {.release = &led_release,},.num_resources = ARRAY_SIZE(led_resources),.resource = led_resources,};static int __init leddevice_init(void){return platform_device_register(&leddevice);}static void __exit leddevice_exit(void){platform_device_unregister(&leddevice);}module_init(leddevice_init);module_exit(leddevice_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("songwei");
                                                                                                                                                                              driver.c
                                                                                                                                                                              #define LEDDEV_CNT1/* 设备号长度 */#define LEDDEV_NAME"platled"/* 设备名字 */#define LEDOFF 0#define LEDON 1/* 寄存器名 */static void __iomem *IMX6U_CCM_CCGR1;static void __iomem *SW_MUX_GPIO1_IO03;static void __iomem *SW_PAD_GPIO1_IO03;static void __iomem *GPIO1_DR;static void __iomem *GPIO1_GDIR;/* leddev设备结构体 */struct leddev_dev{......};struct leddev_dev leddev; /* led设备 */static struct file_operations led_fops = {.owner = THIS_MODULE,    .read = led_read,.open = led_open,.write = led_write,};/* * @description: flatform驱动的probe函数,当驱动与 *   设备匹配以后此函数就会执行 * @param - dev : platform设备 * @return : 0,成功;其他负值,失败 */static int led_probe(struct platform_device *dev){int i = 0;int ressize[5];u32 val = 0;struct resource *ledsource[5];printk("led driver and device has matched!\r\n");/* 1、获取资源 */for (i = 0; i dev, "No MEM resource for always on\n");return -ENXIO;}ressize[i] = resource_size(ledsource[i]);}......}static int led_remove(struct platform_device *dev){iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);......}/* platform驱动结构体 */static struct platform_driver led_driver = {.driver= {.name= "imx6ul-led",/* 驱动名字,用于和设备匹配 */},.probe= led_probe,.remove= led_remove,};static int __init leddriver_init(void){return platform_driver_register(&led_driver);}static void __exit leddriver_exit(void){platform_driver_unregister(&led_driver);}module_init(leddriver_init);module_exit(leddriver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("songwei");

                                                                                                                                                                              二、驱动层(有设备树)

                                                                                                                                                                              driver.c
                                                                                                                                                                              #define LEDDEV_CNT1/* 设备号长度 */#define LEDDEV_NAME"dts_platled"/* 设备名字 */#define LEDOFF 0#define LEDON 1/* leddev设备结构体 */struct leddev_dev{......    struct device_node *node;/* LED设备节点 */int led0;/* LED灯GPIO标号 */};struct leddev_dev leddev; /* led设备 */static struct file_operations led_fops = {.owner = THIS_MODULE,    .read = led_read,.open = led_open,.write = led_write,};/* * @description: flatform驱动的probe函数,当驱动与 *   设备匹配以后此函数就会执行 * @param - dev : platform设备 * @return : 0,成功;其他负值,失败 */static int led_probe(struct platform_device *dev){......    /* 6、初始化IO */leddev.node = of_find_node_by_path("/gpioled");if (leddev.node == NULL){printk("gpioled node nost find!\r\n");return -EINVAL;} leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);if (leddev.led0                                                 

上一篇:电脑主机怎么连接到tcl(电脑主机怎么连接到笔记本电脑)

下一篇:迷你世界怎么创造营服务器?