Linux 驱动开发流程详解 / 设备驱动和设备匹配过程

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

土豆居士 2024 年 06 月 20 日 11:50

一、驱动概念

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

具体任务

  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 是具体的驱动文件名字) 的文件进行相应的操作即可实现对硬件的操作。
  • 用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法 来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作
  • 每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 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 这个函数就会被调用

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") //添加模块作者信息

6.模块打印 printk

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

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

  • printk 可以根据日志级别对消息进行分类,一共有8 个日志级别
#define KERN_SOH  "\001"
#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。
printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");

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

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

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 (模块路径)

  • 查看详细信息,内核模块描述信息,编译系统信息

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 是要写入的地址。

五、设备树

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

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

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

5.1 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引导程序加载到内核。

5.2 设备树框架

1.根节点:\

2.设备节点:nodex

        ①节点名称:node

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

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

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

5.3 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属性(兼容属性)

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的字长。

  • #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),
  • #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
  • 子节点的地址信息描述来自于父节点的#address-cells 和#size-cells的值,而不是该节点本身的值(当前节点的信息是描述子节点的,自己的信息在父节点里)
//每个“address length”组合表示一个地址范围,
//其中 address 是起始地址, length 是地址长度,
//#address-cells 表明 address 这个数据所占用的字长,
// #size-cells 表明 length 这个数据所占用的字长.
reg = <address1 length1 address2 length2 address3 length3……>
  • reg属性

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

uart1: serial@02020000 {
   
    compatible = "fsl,imx6ul-uart",
        "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x02020000 0x4000>;
    interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_UART1_IPG>,
        <&clks IMX6UL_CLK_UART1_SERIAL>;
    clock-names = "ipg", "per";
    status = "disabled";
};

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

  • ranges属性

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

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

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

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

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

1.aliases

aliases {
    can0 = &flexcan1;
    can1 = &flexcan2;
    ...
    usbphy0 = &usbphy1;
    usbphy1 = &usbphy2;
};

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

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

2.chosen

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

5.4 OF操作函数

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

查找节点

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

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; /* 子节点
    ...
}
  • 通过节点名字查找指定的节点:of_find_node_by_name
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)

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

name:要查找的节点名字。

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

  • 通过 device_type 属性查找指定的节点:of_find_node_by_type
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

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

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

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

  • 通过device_type 和 compatible两个属性查找指定的节点:of_find_compatible_node
struct device_node *of_find_compatible_node(struct device_node *from,
                                            const char *type,
                                            const char *compatible)

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

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

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

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

  • 通过of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match
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 表示从根节点开始查找整个设备树。

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

match:找到的匹配的 of_device_id。

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

  • 通过路径来查找指定的节点:of_find_node_by_path
inline struct device_node *of_find_node_by_path(const char *path)

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

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

获取属性值

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

struct property {
   
    char *name; /* 属性名字 */
    int length; /* 属性长度 */
    void *value; /* 属性值 */
    struct property *next; /* 下一个属性 */
    unsigned long _flags;
    unsigned int unique_id;
    struct bin_attribute attr;
}
  • 查找指定的属性:of_find_property
property *of_find_property(const struct device_node *np,
                           const char *name,
                           int *lenp)

np:设备节点。

name:属性名字。

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

返回值:找到的属性。

  • 获取属性中元素的数量(数组):of_property_count_elems_of_size
int of_property_count_elems_of_size(const struct device_node *np,
                                    const char *propname
                                    int elem_size)

np:设备节点。

proname:要读取的属性名字。

index:要读取的值标号。

out_value:读取到的值

返回值:0 读取成功;

负值: 读取失败,

-EINVAL 表示属性不存在

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

-EOVERFLOW 表示属性值列表太小

  • 读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
of_property_read_u8_array
of_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:要读取的属性名字。

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

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

返回值:0:读取成功;

负值: 读取失败

-EINVAL 表示属性不存在

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

-EOVERFLOW 表示属性值列表太小

  • 读取属性中字符串值:of_property_read_string
int of_property_read_string(struct device_node *np,
                            const char *propname,
                            const char **out_string)

np:设备节点。

proname:要读取的属性名字。

out_string:读取到的字符串值。

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

  • 获取**#address-cells属性值:of_n_addr_cells ,获取#size-cells**属性值:of_size_cells 。
int of_n_addr_cells(struct device_node *np)
int of_n_size_cells(struct device_node *np)

np:设备节点。

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

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

  • 内存映射

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

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

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

np:设备节点。

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

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

#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

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 < 0)
    {
   
        printk("status read failed!\r\n");
    }else
    {
   
        printk("status = %s\r\n",str);
    }

    /* 4 、获取 reg  属性内容 */
    ret = of_property_read_u32_array(dtb_led.nd, "reg", regdate, 10);
    if(ret < 0)
    {
   
        printk("reg property read failed!\r\n");
    }else
    {
   
        u8 i = 0;
        printk("reg data:\r\n");
        for(i = 0; i < 10; i++)
            printk("%#X ", regdate[i]);
        printk("\r\n");
    }

六、字符设备驱动

图片

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)

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

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

3.内存映射

  • 内存映射在Linux中不能直接访问寄存器,要想要操作寄存器需要完成物理地址到虚拟空间的映射。
#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));
}

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

建立映射:映射的虚拟地址 = ioremap(IO内存起始地址,映射长度);一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。

  • 解除映射
void iounmap (volatile void __iomem *addr)

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)

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

5. 字符设备最基本框架

#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 0
	writel(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 < 0) printk("chrdevbase driver register failed\n");
    printk("chrdevbase_init()\r\n");
    return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
   
	//解除寄存器映射
	iounmap(GPIO_TEST);
	//注消字符设备驱动
	unregister_chrdev(CHRDEVBASE_MAJOR ,  	//主设备号
						CHRDEVBASE_NAME);	//设备名称
    printk("chrdevbase_exit()\r\n");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息

6. 创建驱动节点文件

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

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

6.2 新字符设备基本驱动框架

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

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

1.设备文件系统

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

devfs, 一个基于内核的动态设备文件系统

devfs缺点(过时原因)

  • 不确定的设备映射
  • 没有足够的主/辅设备号
  • /dev目录下文件太多
  • 内核内存使用

udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开

udev和devfs的区别

  • 采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动
  • udev的Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
  • 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
  1. mdev,是udev的简化版本,是busybox中所带的程序,适合用在嵌入式系统

2.申请设备号

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

  • 动态申请设备号
int alloc_chrdev_region(dev_t *dev,
						unsigned baseminor,
						unsigned count,
						const char *name)

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

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

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

name:设备名字。

  • 静态申请设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name);

from - 要申请的起始设备号

count - 设备号个数

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

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

  • 释放设备号
void unregister_chrdev_region(dev_t from, unsigned count)

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

  • 申请设备号模板
//创建设备号
if (newchrled.major)   //定义了设备号就静态申请
{
   
	newchrled.devid = MKDEV(newchrled.major, 0);
	register_chrdev_region(newchrled.devid,
							NEWCHRLED_CNT,
							NEWCHRLED_NAME);
}
else   //没有定义设备号就动态申请
{
   

	alloc_chrdev_region(&newchrled.devid,
						0,
						NEWCHRLED_CNT,
						NEWCHRLED_NAME);//申请设备号
	newchrled.major = MAJOR(newchrled.devid);	//获取分配号的主设备号
	newchrled.minor = MINOR(newchrled.devid);	// 获取分配号的次设备号
}

3.注册字符设备

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

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

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

  • 初始化cdev结构体变量
void cdev_init(struct cdev *cdev,
				const struct file_operations *fops);

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

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

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

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

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

p - 要添加的cdev结构

dev - 绑定的起始设备号

count - 设备号个数

cdev_add(&testcdev, devid, 1); //添加字符设备
  • 将设备从内核注销

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

void cdev_del(struct cdev *p);

p - 要添加的cdev结构

cdev_del(&testcdev); //删除 cdev

4.自动创建设备节点

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

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

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

创建一个class类

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

删除一个class类

void class_destroy(struct class *cls); // cls要删除的类

创建设备

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

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

struct device *device_create(struct class *class,
    						struct device *parent,
    						dev_t devt,
    						void *drvdata,
							const char *fmt, ...)

*class——设备类指针,

*parent——父设备指针,

devt——设备号,

*drvdata——额外数据,

*fmt——设备文件名

删除设备

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

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

class——设备所处的类 devt——设备号

5.文件私有数据

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

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 0
	writel(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子系统

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

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

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

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

图片

7.2 pinctrl的设备树设置

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

iomuxc: iomuxc@020e0000 {
   
	compatible = "fsl,imx6ul-iomuxc";
	reg = <0x020e0000 0x4000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>;
	imx6ul-evk {
   
		pinctrl_hog_1: hoggrp-1 {
   
			fsl,pins = <
				MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
				.......
			>;
	};
......
}

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 的上/下拉、驱动能力和速度(电气属性)

7.3 设备树中添加pinctrl模板

1.添加pinctrl设备结点

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

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

&iomuxc {
   
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>;
	imx6ul-evk {
   
		......
			pinctrl_led: ledgrp{
   
						fsl,pins = <
							MX6UL_PAD_GPIO1_IO03__GPIO1_IO03	0x10b0
						>;
				};
		......
	};

2.添加具体设备节点,调用pinctrl信息

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

test {
   
    pinctrl-names = "default""wake up";
    pinctrl-0 = <&pinctrl_test>;
    pinctrl-1 = <&pinctrl_test_2>;
    /* 其他节点内容 */
};
  • 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里面的管脚配置。

gpioled {
   
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "songwei-gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_led>;
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
		status = "okay";
	};
  • pinctrl-names = “default”; 设备的状态, 可以有多个状态, default 为状态 0。
  • pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点

八、GPIO子系统

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

8.1 GPIO子系统工作内容

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

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

8.2 GPIO子系统设备树设置

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

gpioled {
   
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "songwei-gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_led>;
		//gpio信息
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值