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

news2024/10/5 22:21:36

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、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输。
  2. 块设备(Block Device)
    • 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。
    • 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。
    • 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。
    • 只有一个块设备可以支持一个安装的文件系统。
    • 比如我们常见的电脑硬盘、SD卡、U盘、光盘等。
  3. 网络设备(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 是具体的驱动文件名字) 的文件进行相应的操作即可实现对硬件的操作。
  • 用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法 来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作
  • 应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 函数,那么在驱动程序中也得有一个名为 open 的函数。
  • 每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合

在这里插入图片描述

4.3 内核模块

Linux 驱动有两种运行方式

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

内核模块组成

  1. 模块加载函数

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

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

    MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
    
  4. 模块参数(可选)
    模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量

  5. 模块信息(可选)

    MODULE_AUTHOR("songwei") //添加模块作者信息
    

模块操作命令

  1. 加载模块
    • insmod XXX.ko
      • 为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
      • insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
    • modprobe XXX.ko
      • 加载模块时会同时加载该模块所依赖的其他模块
  2. 卸载模块
    • rmmod XXX.ko
  3. 查看模块信息
    • lsmod
      • 查看系统中加载的所有模块及模块间的依赖关系
    • modinfo (模块路径)
      • 查看详细信息,内核模块描述信息,编译系统信息

4.4 设备号

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

4.5 地址映射

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

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

内存映射函数

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

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

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

    /*
    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));
    }
    

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

    #define addr (0X020E0068)  // 物理地址
    static void __iomem*  va; //指向映射后的虚拟空间首地址的指针
    va=ioremap(addr, 4);   // 得到虚拟地址首地址
    
  • iounmap,卸载驱动使用 iounmap 函数释放掉 ioremap 函数所做的映射。
    参数 addr:要取消映射的虚拟地址空间首地址

    iounmap(va);
    

I/O内存访问函数

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

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

  • 读操作函数

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

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

  • 写操作函数

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

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

4.6 设备树

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

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

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

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

DTS、DTB和DTC

在这里插入图片描述

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

设备树框架

1.根节点:\

2.设备节点:nodex

        ①节点名称:node

        ②节点地址:node@0, @后面即为地址

3.属性:属性名称(Property   name)和属性值(Property value)

4.标签
  • “/”是根节点,每个设备树文件只有一个根节点。在设备树文件中会发现有的文件下也有“/”根节点,这两个**“/”根节点的内容会合并成一个根节点。**
  • Linux 内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹

DTS语法

  • .dtsi头文件

    #include <dt-bindings/input/input.h>
    #include "imx6ull.dtsi"
    

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

  • 设备节点

    • 设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点

    • 每个节点都通过一些属性信息来描述节点信息,属性就是键—值对

      label: node-name@unit-address
      label:节点标签,方便访问节点:通过&label访问节点,追加节点信息
      node-name:节点名字,为字符串,描述节点功能
      unit-address:设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略
      
    • 设备树源码中常用的几种数据形式

      1.字符串:  compatible = "arm,cortex-a7";设置 compatible 属性的值为字符串“arm,cortex-a7”
      2.32位无符号整数:reg = <0>; 设置reg属性的值为0
      3.字符串列表:字符串和字符串之间采用“,”隔开
      compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
      设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
      
  • 属性

    • compatible属性(兼容属性)

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

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

      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 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/360399.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python爬虫(8)selenium爬虫后数据,存入sqlit3实现增删改查

之前的文章有关于更多操作方式详细解答&#xff0c;本篇基于前面的知识点进行操作&#xff0c;如果不了解可以先看之前的文章 Python爬虫&#xff08;8&#xff09;selenium爬虫后数据&#xff0c;存入sqlit3实现增删改查导入默认包和环境元素定位创建一个sqlit3表将爬虫到的信…

知识蒸馏综述笔记

知识蒸馏的目的 将学习能力强的复杂教师模型中的“知识”迁移到简单的学生模型中 1.知识蒸馏的背景知识 2.解释知识蒸馏的作用机制 3.归纳知识蒸馏中知识的不同形式 4.详细分析和对比了知识蒸馏的各种关键方法&#xff0c; 5.介绍知识蒸馏与其它技术融合、 6.对知识蒸馏…

VirtualBox 安装 CentOS 换成 Rocky Linux

1.下载 VirtualBox并安装https://www.virtualbox.org/wiki/Downloads2.下载Rocky Linux选择 Rocky-9.1-x86_64-dvd.iso 镜像 官网https://rockylinux.org/download阿里云镜像服务https://mirrors.aliyun.com/rockylinux/9.1/isos/x86_64/?spma2c6h.25603864.0.0.70c556799nMQL…

设计模式看 Spring。

设计模式看 Spring。 文章目录设计模式看 Spring。自定义 Spring 框架。7.1 Spring 使用回顾。7.2 Spring 核心功能结构。7.1.1 bean 概述。7.3 Spring IoC 相关接口分析。7.3.1 BeanFactory 解析。7.3.2 BeanDefinition 解析。7.3.3 BeanDefinitionReader 解析。7.3.4 BeanDef…

论文阅读 | Video Frame Synthesis using Deep Voxel Flow

前言&#xff1a; 视频帧生成方法&#xff08;视频插帧/视频预测&#xff09;ICCV2017 oral Video Frame Synthesis using Deep Voxel Flow 引言 当下进行视频帧合成的方法分为两种&#xff0c;第一种是光流法&#xff0c;光流准确的话效果好&#xff0c;光流不准确的话则生…

手摸手,带你实现3D粒子特效

文章摘要&#xff1a;某天&#xff0c;产品小姐姐找到我&#xff0c;要在页面上放一个动态3D模型…不会webGL&#xff1f;没关系&#xff01;今天就来聊一聊如何用从零实现3D粒子特效。 背景 近年来&#xff0c;随着互联网的迅速发展&#xff0c;用户对产品的视觉效果需求也更…

内网渗透(四十六)之横向移动篇-使用系统漏洞ms17010横向移动

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

leaflet 加载geojson数据,随机显示不同颜色的circleMarker

第086个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet项目中加载geojson数据,随机显示不同颜色的circleMarker. 直接复制下面的 vue+leaflet源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共89行)相关API专栏目标示例效果 配置方式…

时间序列数据预测的类型

本文主要内容是使用LSTM网络进行不同类型的时间序列预测任务&#xff0c;不涉及代码&#xff0c;仅仅就不同类型的预测任务和数据划分进行说明。 参考文章&#xff1a;https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting/ 注&#xf…

如何使用Hugo Academic Theme构建自己的github主页

前期条件 自己已经注册好GitHub 原文档&#xff1a;https://wowchemy.com/docs/getting-started/hugo-github-quickstart/ 搜索Hugo Academic Theme&#xff08;网址&#xff09; 进入后的网址为&#xff1a;https://academic-demo.netlify.app/ 点击Get Start 出现如下模板…

【观察】昇腾加速AI“走深向实”,打通落地“最后一公里”

毫无疑问&#xff0c;今天AI正与产业结合得越来越紧密&#xff0c;从智能制造&#xff0c;到智慧医疗&#xff0c;智慧金融、智慧城市等&#xff0c;AI已经开始渗透到我们生活的方方面面&#xff0c;即便是目前来自传统行业用户的AI转型需求尚未完全激活爆发&#xff0c;但仅仅…

利用组件注解符精简Spring配置文件

文章目录利用组件注解符精简Spring配置文件一、利用组件注解精简Spring配置文件1、创建新包2、将4个类给拷贝过去3、修改杀龙任务类4、修改救美任务类5、修改勇敢骑士类6、修改救美骑士类7、创建Spring配置文件利用组件注解符精简Spring配置文件 一、利用组件注解精简Spring配…

什么是生命周期?Activity生命周期的三种状态

什么是生命周期生命周期就是一个对象从创建到销毁的过程&#xff0c;每一个对象都有自己的生命周期。同样&#xff0c;Activity也具有相应的生命周期&#xff0c;Activity的生命周期中分为三种状态&#xff0c;分别是运行状态、暂停状态和停止状态。接下来将针对Activity生命周…

【自动化测试】自动化测试框架那些事儿

无论是在自动化测试实践&#xff0c;还是日常交流中&#xff0c;经常听到一个词&#xff1a;框架。在教学的过程中&#xff0c;同学们一直对“框架”这个词知其然不知其所以然。 最近看了很多自动化相关的资料&#xff0c;加上一些实践&#xff0c;算是对“框架”有了一些理解…

JVM13 类的生命周期

1. 概述 在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义&#xff0c;引用数据类型则需要进行类的加载。 按照 Java 虚拟机规范&#xff0c;从 class 文件到加载到内存中的类&#xff0c;到类卸载出内存为止&#xff0c;它的整个生命周期包…

本地代码推送到Coding

话不多说&#xff0c;开门见山 一、coding准备 注册啊&#xff0c;建项目&#xff0c;这些就不用多说了。 1.创建一个代码仓库 填一下名称和描述就行&#xff0c;其他先不用填&#xff0c;然后点最下面完成创建就行。 2.保存代码仓库地址 二、本地代码仓库准备 已经是在…

Linux 忘记密码解决方法

很多朋友经常会忘记Linux系统的root密码&#xff0c;linux系统忘记root密码的情况该怎么办呢&#xff1f;重新安装系统吗&#xff1f;当然不用&#xff01;进入单用户模式更改一下root密码即可。 步骤如下&#xff1a; 重启linux系统 3 秒之内要按一下回车&#xff0c;出现如…

Web前端:什么是Vue Native 框架?有什么特点?

Vue Native是一个使用Vue.Js开发本地移动应用程序的框架。该框架将文档转换为React Native&#xff0c;进而为你提供适用于Android和iOS的本地应用程序。实际上&#xff0c;Vue Native应用程序据说是React API的包装。Vue将Vue.js和React结合在一起&#xff0c;让你的开发团队充…

【牛客刷题专栏】0x0C:JZ4 二维数组中的查找(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录前言问题…

Django框架之模板过滤器

过滤器 语法如下: 使用管道符号|来应用过滤器&#xff0c;用于进行计算、转换操作&#xff0c;可以使用在变量、标签中。如果过滤器需要参数&#xff0c;则使用冒号:传递参数。 变量|过滤器:参数列举几个如下&#xff1a; safe&#xff0c;禁用转义&#xff0c;告诉模板这个变…