2025年Linux驱动开发进阶(linux驱动开发项目)

Linux驱动开发进阶(linux驱动开发项目)svg xmlns http www w3 org 2000 svg style display none svg

大家好,我是讯享网,很高兴认识大家。



 <svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> <path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path> </svg> 

讯享网

————————————————————————————————
现开通针对在校生嵌入式学习咨询服务,学习路线可见下文:
拉依达的嵌入式学习和秋招经验-CSDN博客
咨询详情请加vx:songwei4615,加vx请备注CSDN咨询

讯享网
————————————————————————————————

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

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

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

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

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

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

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

在这里插入图片描述

  • 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驱动函数

在这里插入图片描述

Linux 驱动有两种运行方式

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

内核模块组成

  1. 模块加载函数
    讯享网
    • module_init 函数用来向 Linux 内核注册一个模块加载函数,
    • 参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
    • 当加载驱动的时, xxx_init 这个函数就会被调用
  2. 模块卸载函数
     
      
    • module_exit函数用来向 Linux 内核注册一个模块卸载函数,
    • 参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
    • 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
  3. 模块许可证明
    讯享网
  4. 模块参数(可选)
    模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量
  5. 模块信息(可选)
     
  6. 模块打印 printk
    printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。
    printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出
  • printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别
    讯享网
  • 以下代码就是设置“gsmi: Log Shutdown Reason ”这行消息的级别为 KERN_EMERG。
     

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

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

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

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

模块操作命令

  1. 加载模块
    • insmod XXX.ko
      • 为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
      • insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
    • modprobe XXX.ko
      • 加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
      • modprobe 提示无法打开“modules.dep”这个文件 ,输入 depmod 命令即可自动生成 modules.dep
  2. 卸载模块
    • rmmod XXX.ko
  3. 查看模块信息
    • lsmod
      • 查看系统中加载的所有模块及模块间的依赖关系
    • modinfo (模块路径)
      • 查看详细信息,内核模块描述信息,编译系统信息
  • Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成
  • 主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
  • Linux 提供了一个名为 dev_t 的数据类型表示设备号其中高 12 位为主设备号, 低 20 位为次设备
  • 使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号(主)
 

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

  1. 完成虚拟空间到物理空间的映射
  2. 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
  3. 对于 32 位的处理器来说,虚拟地址(VA,Virtual Address)范围是 2^32=4GB
    在这里插入图片描述

内存映射函数

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

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

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

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

     
  • iounmap,卸载驱动使用 iounmap 函数释放掉 ioremap 函数所做的映射。
    参数 addr:要取消映射的虚拟地址空间首地址
    讯享网

I/O内存访问函数

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

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

  • 读操作函数
     

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

  • 写操作函数
    讯享网

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

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

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

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

在这里插入图片描述

  • DTS
    • 设备树源码文件,硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts
  • DTSI
    • 对于一些相同的dts配置可以抽象到dtsi文件中,然后可以用include的方式到dts文件
    • 同一芯片可以做一个dtsi,不同的板子不同的dts,然后include同一dtsi
    • 对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置
  • DTC
    • dtc是编译dts的工具
  • DTB
    • dts经过dtc编译之后会得到dtb文件,设备树的二进制执行文件
    • dtb通过Bootloader引导程序加载到内核。
 
  • “/”是根节点,每个设备树文件只有一个根节点。在设备树文件中会发现有的文件下也有“/”根节点,这两个“/”根节点的内容会合并成一个根节点。
  • Linux 内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹

dtsi头文件

讯享网

设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts 设备树文件中,还可以通过“#include”来引用.h、 .dtsi 和.dts 文件。

设备节点

  • 设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点
  • 每个节点都通过一些属性信息来描述节点信息,属性就是键—值对
     
  • 设备树源码中常用的几种数据形式
    讯享网

属性

  • compatible属性(兼容属性)

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

    属性值有两个,分别为“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的字长。

    • #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),
    • #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
    • 子节点的地址信息描述来自于父节点的#address-cells 和#size-cells的值,而不是该节点本身的值(当前节点的信息是描述子节点的,自己的信息在父节点里)
     
  • reg属性
    reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息, reg 属性的值一般是(address, length)对.

    讯享网

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

  • ranges属性
    • ranges属性值可以为或者按照( child-bus-address , parent-bus-address , length )格式编写的数字
    • ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成。
    • 如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换
     
  • 特殊节点

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

    1. aliases
      讯享网

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

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

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

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

查找节点

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

 
  • 通过节点名字查找指定的节点:of_find_node_by_name
    讯享网

    from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    name:要查找的节点名字。
    返回值: 找到的节点,如果为 NULL 表示查找失败。

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

    from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    type:要查找的节点对应的 type 字符串, device_type 属性值。
    返回值: 找到的节点,如果为 NULL 表示查找失败

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

    from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL
    compatible: 要查找的节点所对应的 compatible 属性列表。
    返回值: 找到的节点,如果为 NULL 表示查找失败

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

    from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
    matches: of_device_id 匹配表,在此匹配表里面查找节点。
    match: 找到的匹配的 of_device_id。
    返回值: 找到的节点,如果为 NULL 表示查找失败

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

获取属性值

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

 
  • 查找指定的属性:of_find_property
    讯享网

    np:设备节点。
    name: 属性名字。
    lenp:属性值的字节数,一般为NULL
    返回值: 找到的属性。

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

    np:设备节点。
    proname: 需要统计元素数量的属性名字。
    elem_size:元素长度。
    返回值: 得到的属性元素数量

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

    np:设备节点。
    proname: 要读取的属性名字。
    index:要读取的值标号。
    out_value:读取到的值
    返回值: 0 读取成功;
    负值: 读取失败,
    -EINVAL 表示属性不存在
    -ENODATA 表示没有要读取的数据,
    -EOVERFLOW 表示属性值列表太小

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

    np:设备节点。
    proname: 要读取的属性名字。
    out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
    sz: 要读取的数组元素数量。
    返回值: 0:读取成功;
    负值: 读取失败
    -EINVAL 表示属性不存在
    -ENODATA 表示没有要读取的数据
    -EOVERFLOW 表示属性值列表太小

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

    np:设备节点。
    proname: 要读取的属性名字。
    out_string:读取到的字符串值。
    返回值: 0,读取成功,负值,读取失败

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

    np:设备节点。
    返回值: 获取到的#address-cells 属性值。
    返回值: 获取到的#size-cells 属性值。

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

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

    讯享网

    np:设备节点。
    index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
    返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

     

of 函数在 led_init() 中应用

讯享网

在这里插入图片描述

1.模块加载

 

2.注册字符设备驱动

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

讯享网

这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。
一般字符设备的注册驱动模块的入口函数 xxx_init 中进行,字符设备的注销驱动模块的出口函数 xxx_exit 中进行。

3.内存映射

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

返回值: __iomem 是编辑器标记,指向映射后的虚拟空间首地址。
建立映射:映射的虚拟地址 = ioremap(IO内存起始地址,映射长度)
一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。

  • 解除映射
讯享网

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

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

 

to:目标地址
from:源地址
n:将要拷贝数据的字节数
返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数

5. 字符设备最基本框架

讯享网

6. 创建驱动节点文件

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

 
  • 其中“mknod”是创建节点命令,
  • “/dev/chrdevbase”是要创建的节点文件,
  • “c”表示这是个字符设备,
  • “200”是设备的主设备号,
  • “0”是设备的次设备号。
  • 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看

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

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

1.设备文件系统

设备文件系统有devfs,mdev,udev这三种

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

2.申请设备号

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

  • 动态申请设备号
    讯享网

    dev:保存申请到的设备号。
    baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0
    count: 要申请的设备号数量。
    name:设备名字。

  • 静态申请设备号
     

    from - 要申请的起始设备号
    count - 设备号个数
    name - 设备号在内核中的名称
    返回0申请成功,否则失败

  • 释放设备号
    讯享网
  • 申请设备号模板
     

3.注册字符设备

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

讯享网

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

  • 初始化cdev结构体变量
     

    讯享网
  • 将设备添加到内核

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

    将cdev添加到内核同时绑定设备号
    其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作

     

    p - 要添加的cdev结构
    dev - 绑定的起始设备号
    count - 设备号个数

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

    p - 要添加的cdev结构

    讯享网

4.自动创建设备节点

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

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

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

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

    讯享网
  • 创建设备
    还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。
    成功会在 /dev 目录下生成设备文件。
     

    *class——设备类指针,
    *parent——父设备指针,
    devt——设备号,
    *drvdata——额外数据,
    *fmt——设备文件名

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

5.文件私有数据

  • 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),
  • 一个设备的所有属性信息将其做成一个结构体,
  • 编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
  • 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体
 

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

讯享网

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

  • 获取设备树中 pin 信息管理系统中所有的可以控制的 pin, 在系统初始化的时候, 枚举所有可以控制的 pin, 并标识这些 pin
  • 根据获取到的 pin 信息来设置 pin 的复用功能,对于 SOC 而言, 其引脚除了配置成普通的 GPIO 之外,若干个引脚还可以组成一个 pin group, 形成特定的功能
  • 根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。

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

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

 
  • compatible 属性值为“fsl,imx6ul-iomuxc” ,
  • pinctrl_hog_1 子节点所使用的 PIN 配置信息,如UART1_RTS_B 的配置信息
    • UART1_RTS_B 这个 PIN 是作为 SD 卡的检测引脚
    • MX6UL_PAD_UART1_RTS_B__GPIO1_IO19,这是一个宏定义,表 示 将 UART1_RTS_B 这个 IO 复用为 GPIO1_IO19(复用属性)
      • 此宏定义后面跟着 5 个数字,0x0090 0x031C 0x0000 0x5 0x0,含义是<mux_reg conf_reg input_reg mux_mode input_val>
    • 0x17059 就是 conf_reg 寄存器值 , 设置一个 IO 的上/下拉、驱动能力和速度(电气属性)
  1. 添加pinctrl设备结点
    同一个外设的 PIN 都放到一个节点里面,在 iomuxc 节点中下添加“pinctrl_test”节点。节点前缀一定要为“pinctrl_”。

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

讯享网
  1. 添加具体设备节点,调用pinctrl信息

在根节点“/”下创建 LED 灯节点,节点名为“gpioled”
只需要关注gpioled设备节点下的pinctrl-names 和 pinctrl-0 两条语句, 这两句就是引用iomuxc 中配置的 pinctrl 节点

 
  • pinctrl-names = “default”, “wake up”; 设备的状态, 可以有多个状态, default 为状态 0, wake up 为状态 1。
  • pinctrl-0 = <&pinctrl_test>;第 0 个状态所对应的引脚配置, 也就是 default 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test里面的管脚配置。
  • pinctrl-1 = <&pinctrl_test_2>;第 1 个状态所对应的引脚配置, 也就是 wake up 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test_2里面的管脚配置。

讯享网
  • pinctrl-names = “default”; 设备的状态, 可以有多个状态, default 为状态 0。
  • pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点

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

通过 GPIO 子系统功能要实现:

  • 引脚功能的配置(设置为 GPIO,GPIO 的方向, 输入输出模式,读取/设置 GPIO 的值)
  • 实现软硬件的分离(分离出硬件差异, 有厂商提供的底层支持; 软件分层。 驱动只需要调用接口 API 即可操作 GPIO)
  • iommu 内存管理(直接调用宏即可操作 GPIO)

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

 
  • led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。
  • 稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号
  1. gpio_request

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

    讯享网
    • gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信
      息,此函数会返回这个 GPIO 的标号
    • label:给 gpio 设置个名字。
    • 返回值: 0,申请成功;其他值,申请失败。
  2. gpio_free
    如果不使用某个 GPIO ,需要调用 gpio_free 函数进行释放。
     
  3. gpio_direction_input
    设置某个 GPIO 为输入
    讯享网
  4. gpio_direction_output
    设置某个 GPIO 为输出,并且设置默认输出值。
     
      
    • gpio:要设置为输出的 GPIO 标号。
    • value: GPIO 默认输出值。
    • 返回值: 0,设置成功;负值,设置失败
  5. gpio_get_value
    获取某个 GPIO 的值(0 或 1)
    讯享网
    • gpio:要获取的 GPIO 标号。
    • 返回值: 非负值,得到的 GPIO 值;负值,获取失败
  6. gpio_set_value
    设置某个 GPIO 的值
     
      
    • gpio:要设置的 GPIO 标号。
    • value: 要设置的值
  1. of_gpio_named_count
    获取设备树某个属性里面定义了几个GPIO 信息。
    讯享网
    • np:设备节点。
    • propname:要统计的 GPIO 属性。
    • 返回值: 正值,统计到的 GPIO 数量;负值,失败
  2. of_gpio_count
    此函数统计的是gpios属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的GPIO 信息
     
      
    • np:设备节点。
    • 返回值: 正值,统计到的 GPIO 数量;负值,失败
  3. of_get_named_gpio
    获取 GPIO 编号,在Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号。
    讯享网
    • np:设备节点。
    • propname:包含要获取 GPIO 信息的属性名。
    • index: GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0。
    • 返回值: 正值,获取到的 GPIO 编号;负值,失败。
 

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

  • 并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。
    • 多线程并发访问, Linux 是多任务(线程)的系统,多线程访问是最基本的原因
    • 抢占式并发访问,Linux 内核支持抢占,调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
    • 中断程序并发访问,硬件中断的权利可以是很大的。
    • SMP(多核)核间并发访问,多核 CPU 存在核间并发访问。
  • 竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。
  • 临界资源: 多个进程访问的资源,共享数据段
  • 临界区:多个进程访问的代码段

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

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

讯享网

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

 

原子操作API函数
在这里插入图片描述
原子位操作 API 函数
在这里插入图片描述
在这里插入图片描述

  • 原子变量 lock,用来实现一次只能允许一个应用访问 LED 灯,led_init 驱动入口函数会将 lock 的值设置为 1。
  • 每次打开驱动设备的时候先使用 atomic_dec_and_test 函数将 lock 减 1,如果 atomic_dec_and_test函数返回值为真就表示 lock 当前值为 0,说明设备可以使用
  • 如果 atomic_dec_and_test 函数返回值为假,就表示 lock 当前值为负数(lock 值默认是 1),lock 值为负数的可能性只有一个,那就
    是其他设备正在使用 LED,退出之前调用函数 atomic_inc 将 lock 加 1,因为此时 lock 的值被减成了负数,必须要对其加 1,将 lock 的值
    变为 0。
讯享网

对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。

由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • dev_stats 表示设备状态,如果为 0 的话表示设备还没有被使用,如果大于 0 的话就表示设备已经被使用了。
  • 调用 spin_lock_irqsave 函数获取锁,为了考虑到驱动兼容性,这里并没有使用 spin_lock 函数来获取锁。
  • 判断dev_stats 是否大于 0,如果是的话表示设备已经被使用了,那么就调用 spin_unlock_irqrestore函数释放锁,并且返回-EBUSY。
  • 如果设备没有被使用的话就在将 dev_stats 加 1,表示设备要被使用了,然后调用 spin_unlock_irqrestore 函数释放锁。
  • 在 release 函数中将 dev_stats 减 1,表示设备被释放了
  • 自旋锁的工作就是保护dev_stats 变量,真正实现对设备互斥访问的是 dev_stats。
 

Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

  • 信号量可以使等待资源线程进入休眠状态,适用于占用资源比较久的场合
  • 信号量会引起休眠,中断不能休眠,所以信号量不能用于中断。
  • 如果共享资源的持有时间比较短,不适合使用信号量,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势,此时使用自旋锁。
    在这里插入图片描述

讯享网

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

  • mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
  • mutex 保护的临界区可以调用引起阻塞的 API 函数(信号量也可以)
  • 因为一次只有一个线程可以持有 mutex,所以,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁
    在这里插入图片描述
 

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

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

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

节拍率

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

在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率, 按照如下路径打开配置界面:
-> Kernel Features
-> Timer frequency ( [=y])
在这里插入图片描述
Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。

讯享网

宏 HZ 就是 CONFIG_HZ,HZ=100,后面编写 Linux驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率

  • 高节拍率:优点
    • 提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms。能够以更高的精度运行,时间测量也更加准确。
  • 高节拍率:缺点
    • 高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的精力去处理中断。中断服务函数占用处理器的时间增加,需要根据实际情况,选择合适的系统节拍率。

jiffies

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

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

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

  • 如果 unkown 超过 known 的话, time_after 函数返回真, 否则返回假。
  • 如果 unkown 没有超过 known的话 time_before 函数返回真, 否则返回假。
  • time_after_eq 函数在time_after上,多判断等于这个条件, time_before_eq 也类似
 

为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数
在这里插入图片描述

  • 定时 10ms
    jiffies +msecs_to_jiffies(10)
  • 定时 10us
    jiffies +usecs_to_jiffies(10)

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

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

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

讯享网

在这里插入图片描述
在这里插入图片描述

例 驱动层

 

例 应用层

讯享网

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

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

 
  • fd:文件描述符
  • request:命令码,应用程序通过下发命令码来控制驱动程序完成对应操作。
  • (…)arg:可变参数arg,一些情况下应用程序需要向驱动程序传参,参数就通过arg来传递。只能传递一个参数,但内核不会检查这个参数的类型。那么就有两种传参方式:只传一个整数,传递一个指针
  • 返回值:如果ioctl执行成功,它的返回值就是驱动程序中ioctl接口给的返回值,驱动程序可以通过返回值向用户程序传参。但驱动程序最好返回一个非负数,因为用户程序中的ioctl运行失败时一定会返回-1并设置全局变量errorno。

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

讯享网
  • 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 函数

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() 衍生的接口来直接定义 ioctl 命令

讯享网
  • _IO(type, nr):用来定义不带参数的ioctl命令
  • _IOR(type,nr,size):用来定义用户程序向驱动程序写参数的ioctl命令
  • _IOW(type,nr,size):用来定义用户程序从驱动程序读参数的ioctl命令
  • _IOWR(type,nr,size):用来定义带读写参数的驱动命令

内核还提供了反向解析 ioctl 命令的宏接口:

 
  • _IOC_DIR(nr) :提取方向
  • _IOC_TYPE(nr) :提取幻数
  • _IOC_NR(nr) :提取序数
  • _IOC_SIZE(nr) :提取数据大小

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

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

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

    讯享网
    • dev: 设备节点
    • index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
    • 返回值:中断号

    使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号

     
      
    • gpio: 要获取的 GPIO 编号
    • 返回值: GPIO 对应的中断号
  2. 申请中断函数
    Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断,
    request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数
    request_irq 函数会激活(使能)中断,所以不需要手动去使能中断
    讯享网
  • irq:要申请中断的中断号
  • handler:中断处理函数,当中断发生会执行此中断处理函数
  • flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志
  • name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字
  • dev: 如果将 flags 设置为 IRQF_SHAREDdev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
  • 返回值: 0 中断申请成功,负值中断申请失败,如果返回-EBUSY 表示中断已经被申请了

    中断标志
    在这里插入图片描述

  1. 中断释放函数
    中断使用完成以后就要通过 free_irq 函数释放掉相应的中断如果中断不是共享的,free_irq 会删除中断处理函数并且禁止中断
     
      
    • irq: 要释放的中断号
    • dev:如果中断设置为共享(IRQF_SHARED),此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉
  2. 中断处理函数

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

    讯享网
    • 第一个参数:要中断处理函数要相应的中断号
    • 第二个参数:一个指向 void 的指针,是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构
    • 返回值:irqreturn_t 类型

      irqreturn_t 类型定义如下所示:

       

      irqreturn_t 是个枚举类型, 一共有三种返回值。 一般中断服务函数返回值使用如下形式

      讯享网
  3. 中断使能和禁止函数

    enable_irq 和 disable_irq 用于使能和禁止指定的中断。

     
      
    • irq:要禁止的中断号
    • disable_irq 函数要等到当前正在执行的中断处理函数执行完才返回, 因此需要保证不会产生新的中断, 并且确保所有已经开始执行的中断处理程序已经全部退出。
    • disable_irq_nosync 函数调用以后立即返回, 不会等待当前中断处理程序执行完毕。

    使能/关闭全局中断

    讯享网
    • local_irq_enable 用于使能当前处理器中断系统,
    • local_irq_disable 用于禁止当前处理器中断系统。
    • 一般不能直接简单粗暴的通过这两个函数来打开或者关闭全局中断,这样会使系统崩溃。

    在打开或者关闭全局中断时,要考虑到别的任务的感受,要保存中断状态,处理完后要将中断状态恢复到以前的状态

     
      
    • local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。
    • local_irq_restore 用于恢复中断,将中断到 flags 状态。

为保证系统实时性, 中断服务程序必须足够简短,如果都在中断服务程序中完成, 则会严重降低中断的实时性,
所以, linux 系统提出了一个概念: 把中断服务程序分为两部分: 上半部-下半部 。主要目的就是实现中断处理函数的快进快出

中断服务程序分为上半部(top half)和下半部(bottom half)上半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。

上半部只能通过中断处理程序实现下半部的实现目前有 3 种实现方式, 分别为: 软中断、 tasklet 、工作队列(work queues)

在这里插入图片描述

(1)软中断

Linux 内核使用结构体 softirq_action 表示软中断

讯享网

在 kernel/softirq.c 文件中一共定义了 10 个软中断

 

NR_SOFTIRQS 是枚举类型

讯享网

一共有 10 个软中断,数组 softirq_vec 有 10 个元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数

数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。

  • 要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数
     
  • 注册好软中断以后需要通过 raise_softirq 函数触发

    讯享网

(2)tasklet

tasklet是通过软中断实现的, 软中断用轮询的方式处理, 假如是最后一种中断, 则必须循环完所有的中断类型, 才能最终执行对应的处理函数。

为了提高中断处理数量,改进处理效率, 产生了 tasklet 机制。 tasklet 采用无差别的队列机制, 有中断时才执行, 免去了循环查表之苦。

tasklet 机制的优点

  • 无类型数量限制, 效率高, 无需循环查表, 支持 SMP 机制。
  • 一种特定类型的 tasklet 只能运行在一个 CPU 上, 不能并行, 只能串行执行。
  • 多个不同类型的 tasklet 可以并行在多个CPU 上。
  • 软中断是静态分配的, 在内核编译好之后, 就不能改变。
  • 但 tasklet 就灵活许多, 可以在运行时改变,比如添加模块时 。

调用 tasklet 以后, tasklet 绑定的函数并不会立马执行, 而是有中断以后, 经过一个很短的不确定时间在来执行。
在这里插入图片描述
Linux 内核使用 tasklet_struct 结构体来表示 tasklet

 
  • next: 链表中的下一个 tasklet, 方便管理和设置 tasklet
  • state: tasklet 的状态
  • count: 表示 tasklet 是否出在激活状态, 如果是 0, 就处在激活状态, 如果非 0, 就处在非激活状态
  • void (*func)(unsigned long): 结构体中的 func 成员是 tasklet 的绑定函数, data 是它唯一的参数。func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数
  • date: 函数执行的传递给func的参数

如果要使用 tasklet, 必须先定义一个 tasklet然后使用 tasklet_init 函数初始化 tasklet

讯享网
  • t:要初始化的 tasklet
  • func: tasklet 的处理函数。
  • data: 要传递给 func 函数的参数

使用宏 DECLARE_TASKLET 一次性完成 tasklet 的定义和初始化, DECLARE_TASKLET 定义在include/linux/interrupt.h 文件中

 
  • name:要定义的 tasklet 名字, 就是一个 tasklet_struct 类型的变量
  • func:tasklet 的处理函数
  • data:传递给 func 函数的参数

上半部中断处理函数调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行

讯享网
  • t:要调度的 tasklet,DECLARE_TASKLET 宏里面的 name, tasklet_struct 类型的变量

杀死 tasklet 使用 tasklet_kill 函数,这个函数会等待 tasklet 执行完毕, 然后再将它移除。 该函数可能会引起休眠, 所以要禁止在中断上下文中使用。

 
  • t:要删除的 tasklet

tasklet模板

讯享网
  • ①: 定义一个 tasklet 结构体
  • ②: 动态初始化 tasklet
  • ③: 编写 tasklet 的执行函数
  • ④: 在中断上文调用 tasklet
  • ⑤: 卸载模块的时候删除 tasklet

(3)工作队列(workqueue)

工作队列(workqueue) 是实现中断下文的机制之一, 是一种将工作推后执行的形式

工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度

工作队列tasklet 机制有什么不同呢?
tasklet 也是实现中断下文的机制, 最主要的区别是 tasklet不能休眠, 而工作队列是可以休眠的。 所以, tasklet 可以用来处理比较耗时间的事情, 而工作队列可以处理非常复杂并且更耗时间的事情。因此如果要推后的工作可以睡眠就可以选择工作队列,否则的话就只能选择软中断或tasklet。

Linux 内核使用 work_struct 结构体表示一个工作

 

这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示

讯享网

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux 内核使用worker 结构体表示工作者线程

每个 worker 都有一个工作队列工作者线程处理自己工作队列中的所有工作。在驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。

 

初始化工作:INIT_WORK

讯享网
  • _work :要初始化的工作(work_struct)
  • _func :工作对应的处理函数

工作的创建和初始化:DECLARE_WORK

 
  • n :定义的工作(work_struct)
  • f: 工作对应的处理函数

工作的调度函数: schedule_work

讯享网
  • work: 要调度的工作。
  • 返回值: 0 成功,其他值 失败

工作队列模块

 

如果一个设备需要用到中断功能,需要在设备树中配置好中断属性信息, 因为设备树是用来描述硬件信息的, 然后 Linux 内核通过设备树配置的中断属性来配置中断功能。

例:imx6ull中断控制器节点

讯享网

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

  • 第一个 cells: 中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断
  • 第二个 cells: 中断号, SPI中断号的范围为 0~987, PPI中断号的范围为 0~15
  • 第三个 cells: 标志, bit[3:0]表示中断触发类型, 为1表示上升沿触发, 为2表示下降沿触发, 为4表示高电平触发, 为8表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码

②interrupt-controller 节点为空, 表示当前节点是中断控制器。

③interrupts :描述中断源信息, 对于 gpio5 来说一共有两条信息: 中断类型是 SPI, 触发电平是 IRQ_TYPE_LEVEL_HIGH, 中断源 一个是74, 一个是 75

④interrupt-parent,指定父中断,也就是中断控制器。

这里的 IO 指的是 Input/Output(输入/输出):是应用程序对驱动设备的输入/输出操作

阻塞IO

阻塞IO操作是指在执行设备操作时, 若不能获得资源, 则挂起进程直到满足可操作的条件后再进行操作

被挂起的进程进入睡眠状态, 被从调度器的运行队列移走, 直到等待的条件被满足该进程会唤醒

在阻塞访问时, 不能获取资源的进程将进入休眠, 它将 CPU 资源让给其他进程。 因为阻塞的进程会进入休眠状态, 所以必须确保有一个地方能够唤醒休眠的进程。唤醒进程最大可能发生在中断函数里面, 因为在硬件资源获得的同时往往伴随着一个中断。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
在这里插入图片描述
如图,应用程序调用 read 函数从设备中读取数据,当设备不可用数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。

 

非阻塞IO

非阻塞IO操作是在不能进行设备操作时, 并不挂起, 要么放弃, 要么不停地查询直至可以进行操作为止。非阻塞的进程则不断尝试, 直到可以进行 I/O。
在这里插入图片描述
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,一直往复循环,直到数据读取成功。

若用户以非阻塞的方式访问设备文件, 则当设备资源不可获取时, 设备驱动的 xxx_read() 、 xxx_write( ) 等操作应立即返回, read( ) 、 write( ) 等系统调用也随即被返回, 应用程序收到-EAGAIN 返回值。

讯享网

应用层(默认打开)

 

驱动层(等待队列)

在 Linux 驱动程序中,使用等待队列( Wait Queue) 来实现阻塞进程的唤醒

Linux 内核的等待队列是以双循环链表为基础数据结构, 与进程调度机制紧密结合, 能够用于实现核心的异步事件通知机制。

它有两种数据结构: 等待队列头(wait_queue_head_t)等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个 list_head 类型的域作为连接件。 它通过一个双链表等待 task的头等待的进程列表链接起来。

  1. 等待队列头
    如果要在驱动中使用等待队列,必须创建并初始化一个等待队列头。等待队列头使用结构体 wait_queue_head_t 来表示
    讯享网

    定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头

     
  2. 等待队列项
    每个访问设备的进程都是一个队列项, 当设备不可用时就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项
    讯享网

    使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项

     

    内核中 current 相当于全局变量 , 表 示 当 前 进 程 。所以DECLARE_WAITQUEUE是给当前正在运行的进程创建并初始化了一个等待队列项。

  3. 添加/删除队列
    设备不可访问的时就需要将进程对应的等待队列项 添加到前面创建的等待队列头中, 只有添加到等待队列头中以后进程才能进入休眠态。
    讯享网

    设备可以访问后再将进程对应的等待队列项等待队列头中移除即可。

     
  4. 唤醒等待睡眠进程
    设备可以使用的时就要唤醒进入休眠态的进程, 唤醒可以使用如下两个函数
    讯享网

    这两个函数会将这个等待队列头中所有进程都唤醒
    wake_up 函 数 可 以 唤 醒 处 于TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状 态 的 进 程 ,
    wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程

  5. 等待事件
    除了主动唤醒以外, 也可以设置等待队列等待某个事件, 当这个事件满足以后自动唤醒等待队列中的进程。
    调用的时要确认 condition 值是真还是假, 如果调用 condition 为真, 则不会休眠
     

    使用等待队列实现阻塞访问重点注意两点:

    • ①、将任务或者进程加入到等待队列头,
    • ②、在合适的点唤醒等待队列,一般都是中断处理函数里面。
  6. 使用模板
    讯享网

在这里插入图片描述

  • 在设备结构体中添加一个等待队列头 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。
  • 完成read函数后,设置任务为运行态,将等待队列移除

应用层

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

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

1.select
 
  • nfds: 所要监视的这三类文件描述集合, 最大文件描述符加 1
  • readfds、 writefds 和 exceptfds:指向描述符集合。指明了关心哪些描述符.这三个参数都是 fd_set 类型的, fd_set 类型变量的每一个位都代表了一个文件描述符。
    • readfds 用于监视指定描述符集的读变化,监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取, seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,会根据 timeout 参数来判断是否超时。若将 readfs设置为 NULL,表示不关心任何文件的读变化。
    • writefs 用于监视文件是否可以进行写操作
    • exceptfds 用于监视文件的异常
  • timeout:超时时间,当调用 select 函数等待某些文件描述符可以设置超时时间
  • 返回值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 中删除
  • FD_ISSET: 测试文件描述符 fd是否属于某个集合

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

 

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

讯享网
2.poll

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

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

pollfd 结构体

讯享网
  • fd 是要监视的文件描述符,如果 fd 无效,则events 监视事件也无效,并且 revents返回 0。
  • events 是要监视的事件,可监视的事件类型如下:
    • POLLIN 有数据可以读取。
    • POLLPRI 有紧急的数据需要读取。
    • POLLOUT 可以写数据。
    • POLLERR 指定的文件描述符发生错误。
    • POLLHUP 指定的文件描述符挂起。
    • POLLNVAL 无效的请求。
    • POLLRDNORM 等同于 POLLIN
  • revents 是返回的事件, 由 Linux 内核设置具体的返回事件。

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

 
3.epoll

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

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

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

讯享网
  • size:从 Linux2.6.8 开始此参数已经没有意义了,填写一个大于 0 的值就可以
  • 返回值: epoll 句柄,如果为-1 的话表示创建失败

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

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

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

讯享网
  • events 表示要监视的事件,可选的事件如下
    • EPOLLIN 有数据可以读取
    • EPOLLOUT 可以写数据
    • EPOLLPRI 有紧急的数据需要读取
    • EPOLLERR 指定的文件描述符发生错误
    • EPOLLHUP 指定的文件描述符挂起
    • EPOLLET 设置 epoll 为边沿触发,默认触发模式为水平触发
    • 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。

驱动层

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

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

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

 
  • filp: 要打开的设备文件(文件描述符)
  • wait_address:要添加到 poll_table 中的等待队列头
  • p:file_operations 中 poll 函数的 wait 参数

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

讯享网

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

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

  1. 注册信号处理函数

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

     
      
    • signum:要设置处理函数的信号
    • handler: 信号的处理函数
    • 返回值: 设置成功返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。

    信号中断处理函数

    讯享网

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

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

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

     

    使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核
    在这里插入图片描述

  3. 开启异步通知

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

    讯享网
  4. 应用程序模板
     
  1. 内核要使用异步通知需要在驱动程序中定义一个 fasync_struct 结构体指针变量

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

    讯享网
  2. 然后需要在设备驱动中实现 file_operations 操作集中的 fasync 函数
     
  3. fasync 函数里面一般通过调用 fasync_helper 函数初始化前面定义的 fasync_struct 结构体指针
    讯享网

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

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

  4. 当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生中断。 kill_fasync函数负责发送指定的信号
     
      
    • fp:要操作的 fasync_struct
    • sig: 要发送的信号
    • band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT
  5. 最后,在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct,fasync_struct 的释放函数为 fasync_helper。
    讯享网

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

  6. 驱动程序模板
     

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

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

讯享网

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

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

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

这就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离
在这里插入图片描述
当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有,就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动中查找与之匹配的驱动,如果有,也联系起来。 Linux 内核中大量的驱动程序都采用总线、驱动和设备模式。

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

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

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

一、platform 驱动

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

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

platform_device_id 结构体内容如下:

讯享网

device_driver 结构体定义内容如下:

 

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

讯享网

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

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

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

在 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 来测量一个数组的元素个数。
  • resource:指向一个资源结构体数组,即设备信息,比如外设寄存器等。

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

讯享网

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

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

 
  • pdev:要注册的 platform 设备
  • 返回值: 负数,失败; 0,成功

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

讯享网
  • pdev:要注销的 platform 设备
  • 返回值: 无

三、platform 总线

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

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

 

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

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

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

讯享网

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

四、platform 总线具体匹配方法

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

 

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

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

一、应用层

讯享网

二、驱动层(无设备树)

device.c
 
driver.c
讯享网

二、驱动层(有设备树)

driver.c
 

小讯
上一篇 2025-05-13 23:04
下一篇 2025-04-16 18:01

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/202408.html