文章目录

    • 一、Linux设备模型介绍
      • (1)设备驱动模型总体介绍
      • (2)设备驱动模型文件表现
      • (3)设备驱动模型工作原理
        • 【1】总线
        • 【2】设备
        • 【3】驱动
        • 【4】注册流程
    • 二、平台设备驱动介绍
      • (1)平台设备
        • 【1】platform_device 结构体
        • 【2】注册/注销平台设备
      • (2)平台驱动
        • 【1】platform_driver 结构体
        • 【2】注册/注销平台驱动
    • 三、设备树(device tree)介绍
      • (1)引入设备树原因
      • (2)设备树解决的问题
      • (3)设备树的构造
      • (4)设备树框架
        • 【1】节点基本格式
        • 【2】节点属性
    • 四、GPIO子系统以及pinctrl子系统介绍
      • (1) pinctrl 子系统
        • 【1】pinctrl子节点编写格式
        • 【2】添加pinctrl子节点到iomuxc节点下
      • (2)GPIO子系统
        • 【1】GPIO子系统简介
        • 【2】GPIO 子系统常用 API 函数讲解

我们实现设备驱动开发的时候,需要提前了解一些基础知识,下面慢慢介绍。
主要参考资料:野火i.MX Linux开发实战指南

一、Linux设备模型介绍

(1)设备驱动模型总体介绍

我们在上面一篇关于字符驱动开发的文章已经大概知道字符驱动开发的流程了,但是具体的驱动开发中涉及到的有很多硬件的信息,这些信息该放在哪里呢?

早期内核(2.4之前)没有统一的设备驱动模型,就是将设备(硬件信息)和驱动的代码都放在一个文件中,然后编译运行,但是假设某一个管脚(硬件信息)修改了,这个驱动代码就需要重新写了,这是很不合理的。

Linux 作为一个发展成熟、功能齐全、结构复杂的操作系统,它对于代码的可维护性、复用性非常看重。Linux内核 2.6 版本中正式引入设备驱动模型,将我们编写的驱动代码分成了两块:设备驱动。设备负责提供硬件资源,驱动代码负责去使用这些设备提供的硬件资源。并由总线将它们联系起来。这样子就构成以下图形中的关系。


设备模型通过几个数据结构来反映当前系统中总线、设备以及驱动的工作状况,提出了以下几个重要概念:

  • 设备 (device):挂载在某个总线的物理设备;
  • 驱动 (driver):与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
  • 总线(bus):负责管理挂载对应总线的设备以及驱动;
  • 类 (class):对于具有相同功能的设备,归结到一种类别,进行分类管理。

(2)设备驱动模型文件表现

我们知道在 Linux 中一切皆“文件”,在根文件系统中有个/sys 文件目录,里面记录各个设备之间的关系。下面介绍/sys 下几个较为重要目录的作用。

root@igkboard:~# cd /sys/root@igkboard:/sys# lsblockbusclassdevdevicesfirmwarefskernelmodulepower
  • /sys/bus目录

/sys/bus 目录下的每个子目录都是注册好了的总线类型。
这里是设备按照总线类型分层放置的目录结构,每个子目录 (总线类型) 下包含两个子目录——devicesdrivers 文件夹:

root@igkboard:/sys# cd bus/root@igkboard:/sys/bus# lsac97 clocksourcecpu event_sourcegpioi2cmdio_busmmc nvmempci-epf rpmsgsdioseriospiulpivirtioworkqueueclockeventscontainerdp-auxgenpd hid iiomedia mmc_rpmbpciplatformscsi serialsocteeusb w1

其中 devices 下是该总线类型下的所有设备,而这些设备都是符号链接(符号链接【软链接】是一类特殊的文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用),它们分别指向真正的设备(/sys/devices/下)。

drivers下是所有注册在这个总线上的驱动,每个 driver 子目录下是一些可以观察和修改的 driver 参数。

  • /sys/devices目录
root@igkboard:/sys/bus# ls /sys/devices/armv7_cortex_a7breakpointmmdcplatformsoc0softwaresystemvirtual

/sys/devices 目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。

一般来说,所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices 是内核对系统中所有设备的分层次表达模型,也是/sys 文件系统管理设备的最重要的目录结构。

  • /sys/class 目录

/sys/class 目录下则是包含所有注册在 kernel(内核) 里面的设备类型,这是按照设备功能分类的设备模型,我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,按照设备功能分类无论它挂载在哪条总线上都是归类到/sys/class/input 下。

(3)设备驱动模型工作原理

“总线-设备-驱动”它们之间是如何相互配合工作的呢?下面我们来介绍一下。

在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动,同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。在插入的同时总线会执行一个 bus_type 结构体中 match 的方法对新插入的设备/驱动进行匹配。(它们之间最简单的匹配方式则是对比名字,存在名字相同的设备/驱动便成功匹配)。在匹配成功的时候会调用驱动 device_driver 结构体中 probe 方法 (通常在 probe 中获取设备资源,具体的功能可由驱动编写人员自定义),并且在移除设备或驱动时,会调用 device_driver结构体中 remove 方法。

【1】总线

总线是连接处理器和设备之间的桥梁,总线代表着同类设备需要共同遵守的工作时序,是连接处理器和设备之间的桥梁。我们接触到的设备大部分是依靠总线来进行通信的,它们之间的物理连接如图所示:

触摸芯片是依赖于 I2C;鼠标、键盘等 HID 设备,则是依赖于 USB。从功能上讲,这些设备都是将文字、字符、控制命令或采集的数据等信息输入到计算机。

总线驱动则负责实现总线的各种行为,其管理着两个链表,分别是添加到该总线的设备链表以及注册到该总线的驱动链表。当你向总线添加(移除)一个设备(驱动)时,便会在对应的列表上添加新的节点,同时对挂载在该总线的驱动以及设备进行匹配,在匹配过程中会忽略掉那些已经有驱动匹配的设备。

在实际编写 linux 驱动模块时,Linux 内核已经为我们写好了大部分总线驱动,正常情况下我们一般不会去注册一个新的总线。

【2】设备

驱动开发的过程中,我们最关心的莫过于设备以及对应的驱动了。我们编写驱动的目的,最终就是为了使设备可以正常工作。

1 struct device {2 const char *init_name;/*指定该设备的名称,总线匹配时,一般会根据比较名字,来进行配对;*/3 struct device *parent;/* 表示该设备的父对象*/4 struct bus_type *bus;/*表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。*/5 struct device_driver *driver;6 void *platform_data;/* 特定设备的私有数据,通常定义在板级文件中;*/7 void *driver_data;/* 同上,驱动层可通过 dev_set/get_drvdata 函数来获取该成员;*/8 struct device_node *of_node;/*存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table 以及设备树的 compatible 属性进行比较之后,将匹配的节点保存到该变量。*/9 dev_t devt;/*类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys 目录中导出对应的设备。*/10 struct class *class;11 void (*release)(struct device *dev);12 const struct attribute_group **groups; /* optional groups */13 struct device_private *p;14 };

在 Linux 中,一切都是以文件的形式存在,设备也不例外。/sys/devices目录记录了系统中所有设备,实际上在 sys 目录下所有设备文件最终都会指向该目录对应的设备文件;此外还有另一个目录/sys/dev 记录所有的设备节点,但实际上都是些链接文件,同样指向了 devices 目录下的文件。

【3】驱动

设备能否正常工作,取决于驱动。驱动需要告诉内核,自己可以驱动哪些设备,如何初始化设备。在内核中,使用 device_driver 结构体来描述我们的驱动。具体就不多介绍了,因为后面的驱动实现用到的并非设备驱动结构体。

1 struct device_driver {2 const char *name;/*指定驱动名称,总线进行匹配时,利用该成员与设备名进行比较;*/3 struct bus_type *bus;/*表示该驱动依赖于哪个总线,内核需要保证在驱动执行之前,对应的总线能够正常工作;*/45 struct module *owner;/*表示该驱动的拥有者,一般设置为 THIS_MODULE;*/6 const char *mod_name; /* used for built-in␣,→modules */78 bool suppress_bind_attrs; /* disables bind/unbind via sysfs */910 const struct of_device_id *of_match_table;/*指定该驱动支持的设备类型。当内核使能设备树时,会利用该成员与设备树中的 compatible 属性进行比较。*/11 const struct acpi_device_id *acpi_match_table;12/*probe :当驱动以及设备匹配后,会执行该回调函数,对设备进行初始化。通常的代码,都是以 main 函数开始执行的,但是在内核的驱动代码,都是从 probe 函数开始的。remove : 当设备从操作系统中拔出或者是系统重启时,会调用该回调函数;*/13 int (*probe) (struct device *dev);14 int (*remove) (struct device *dev);1516 const struct attribute_group **groups;17 struct driver_private *p;1819 };

内核提供了 driver_register 函数以及 driver_unregister 函数来注册/注销驱动,成功注册的驱动会记录在/sys/bus//drivers

【4】注册流程

系统启动之后会调用 buses_init 函数创建/sys/bus 文件目录,这部分系统在开机时已经帮我们准备好了,接下去就是通过总线注册函数 bus_register 进行总线注册,注册完总线后在总线的目录下生成 devices 文件夹和 drivers 文件夹,最后分别通过 device_register 以及 driver_register 函数注册相对应的设备和驱动。


二、平台设备驱动介绍

我们已经对设备驱动模型进行了深入剖析,在设备驱动模型中,引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象,与我们 SOC 中物理总线的概念并不严格相等:

  • 物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
  • 驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为它们进行配对。

一般对于 I2C、SPI、USB 这些常见类型的物理总线来说,Linux 内核会自动创建与之相应的驱动总线,因此 I2C 设备、SPI 设备、USB 设备自然是注册挂载在相应的总线上。

但是,实际项目开发中还有很多结构简单的设备,对它们进行控制并不需要特殊的时序。它们也就没有相应的物理总线,比如 led、rtc 时钟、蜂鸣器、按键等等,Linux 内核将不会为它们创建相应的驱动总线。

为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux 内核引入了一种虚拟的总线——平台总线(platform bus)。平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为=平台设备,对应的设备驱动则被称为平台驱动

平台设备驱动的核心依然是 Linux 设备驱动模型,平台设备使用 platform_device 结构体来进行表示,其继承了设备驱动模型中的 device 结构体。而平台驱动使用 platform_driver 结构体来进行表示,其则是继承了设备驱动模型中的 device_driver结构体。

(1)平台设备

【1】platform_device 结构体

内核使用 platform_device 结构体来描述平台设备,结构体原型如下:

1 struct platform_device {2 const char *name;3 int id;4 struct device dev;5 u32 num_resources;6 struct resource *resource;7 const struct platform_device_id *id_entry;8 /* 省略部分成员 */9 };
  • name:设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致;
  • id:指定设备的编号,Linux 支持同名的设备,而同名设备之间则是通过该编号进行区分;
  • dev:Linux 设备模型中的 device 结构体,linux 内核大量使用了面向对象思想,platform_device通过继承该结构体可复用它的相关代码,方便内核管理平台设备;
  • num_resources:记录资源的个数,当结构体成员 resource 存放的是数组时,需要记录 resource数组的个数,内核提供了宏定义 ARRAY_SIZE 用于计算数组的个数;
  • resource:平台设备提供给驱动的资源,如 irq,dma,内存等等。该结构体会在接下来的内容进行讲解;
  • id_entry:平台总线提供的另一种匹配方式,原理依然是通过比较字符串,这部分内容会在平台总线小节中讲,这里的 id_entry 用于保存匹配的结果。

我们注意到 plat-form_device 结构体中,有个 device 结构体类型的成员 dev。在前面章节,我们提到过 Linux 设备模型使用 device 结构体来抽象物理设备,该结构体的成员 platform_data 可用于保存设备的私有数据。platform_data 是 void *类型的万能指针,无论你想要提供的是什么内容,只需要把数据的地址赋值给 platform_data 即可,还是以 GPIO 引脚号为例,示例代码如下:

1 unsigned int pin = 10;23 struct platform_device pdev = {4 .dev = {5 .platform_data = &pin;6 }7 }

【2】注册/注销平台设备

当我们定义并初始化好 platform_device 结构体后,需要把它注册、挂载到平台设备总线上。注册平台设备需要使用 platform_device_register() 函数,该函数原型如下:

int platform_device_register(struct platform_device *pdev)
  • 参数:pdev: platform_device 类型结构体指针
  • 成功: 0
  • 失败:负数

同样,当需要注销、移除某个平台设备时,我们需要使用 platform_device_unregister 函数,来通知平台设备总线去移除该设备:

void platform_device_unregister(struct platform_device *pdev)
  • 参数: pdev: platform_device 类型结构体指针
  • 返回值:无

(2)平台驱动

【1】platform_driver 结构体

内核中使用 platform_driver 结构体来描述平台驱动,结构体原型如下所示:

1 struct platform_driver {23 int (*probe)(struct platform_device *);4 int (*remove)(struct platform_device *);5 struct device_driver driver;6 const struct platform_device_id *id_table;78 };
  • probe:函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化;
    remove:函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是 probe 函数实现操作的逆过程;
  • driver:Linux 设备模型中用于抽象驱动的 device_driver 结构体,platform_driver 继承该结构体,也就获取了设备模型驱动对象的特性;
  • id_table:表示该驱动能够兼容的设备类型。platform_device_id 结构体原型如下所示:
struct platform_device_id {char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };

【2】注册/注销平台驱动

当我们初始化了 platform_driver 之后,通过 platform_driver_register() 函数来注册我们的平台驱动,该函数原型如下:

int platform_driver_register(struct platform_driver *drv);
  • 参数: drv: platform_driver 类型结构体指针
  • 成功: 0
  • 失败:负数

由于 platform_driver 继承了 driver 结构体,结合 Linux 设备模型的知识,当成功注册了一个平台驱动后,就会在/sys/bus/platform/driver 目录下生成一个新的目录项。

当卸载的驱动模块时,需要注销掉已注册的平台驱动,platform_driver_unregister() 函数用于注销已注册的平台驱动,该函数原型如下:

void platform_driver_unregister(struct platform_driver *drv);
  • 参数: drv: platform_driver 类型结构体指针
  • 返回值:无

三、设备树(device tree)介绍

设备树的作用就是描述一个硬件平台的硬件资源。这个“设备树”可以被 bootloader(uboot) 传递到内核,内核可以从设备树中获取硬件信息。

(1)引入设备树原因

在上面我们讲到了platform_device结构体中的resource结构体,它是用来存放平台设备提供给驱动的资源,如 irq,dma,内存等等。该结构体如下:

struct resource {resource_size_t start;resource_size_t end;const char *name;unsigned long flags;/* 省略部分成员 */};
  • name:指定资源的名字,可以设置为 NULL;
  • start、end:指定资源的起始地址以及结束地址;
  • flags:用于指定该资源的类型,在 Linux 中,资源包括 I/O、Memory、Register、IRQ、DMA、Bus 等多种类型。

我们可以通过相关的接口去从结构体中调用。某些硬件资源(比如中断号的定义、寄存器的地址等)都是在一个C文件中,保存在/arch/arm/plat-xxx/arch/arm/mach-xxx目录下。

随着处理器数量的增多用于描述“硬件平台板级细节”的文件越来越多导致 Linux 内核非常臃肿,Linux 之父发现这个问题之后决定使用设备树解决这个问题。设备树简单、易用、可重用性强,linux3.x 之后大多采用设备树编写驱动。

(2)设备树解决的问题

设备树其实就是一个文本文件,通过一些语法去描述开发板上的各种硬件资源(比如每个控制器的中断号,每个控制器寄存器的起始地址是多少等)。

后缀名为·dts 的文件,经过编译之后成为dtb 文件,内核编译的时候,会将设备树文件中描述的硬件资源单独加载到内核执行。

解决了硬件解耦的问题:内核和某个具体的开发板平台。驱动开发的流程发生了一定的变化,不需要从使用函数接口从source结构体中去获取了,而是解析设备树从设备树中获取。

(3)设备树的构造

设备树描述硬件资源时有两个特点:

  • 第一,以“树状”结构描述硬件资源。例如本地总线为树的“主干”在设备树里面称为“根节点”,挂载到本地总线的 IIC 总线、SPI 总线、UART 总线为树的“枝干”在设备树里称为“根节点的子节点”,IIC 总线下的 IIC 设备不止一个,这些“枝干”又可以再分。
  • 第二,设备树可以像头文件(.h 文件)那样,一个设备树文件引用另外一个设备树文件,这样可以实现“代码”的重用。例如多个硬件平台都使用 i.MX6ULL 作为主控芯片,那么我们可以将 i.MX6ULL 芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀,其他设备树文件直接使用“# includexxx”引用即可。

DTS、DTC 和 DTB 是常见的几个缩写:

  • DTS 是指.dts 格式的文件,是一种 ASII 文本格式的设备树描述,也是我们要编写的设备树源码,一般一个.dts 文件对应一个硬件平台,位于 Linux 源码的“/arch/arm/boot/dts”目录下。
  • DTC 是指编译设备树源码的工具,一般情况下我们需要手动安装这个编译工具。
  • DTB 是设备树源码编译生成的文件,类似于我们 C 语言中“.C”文件编译生成“.bin”文件。

(4)设备树框架

设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。我们来看看“设备树”到底是什么样子的:

wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts$ 
/* * Device Tree Source for LingYun IGKBoard(IoT Gateway Kit Board) * Based on imx6ul-14x14-evk.dts/imx6ul-14x14-evk.dtsi * * Copyright (C) 2022 LingYun IoT System Studio. * Author: Guo Wenxue *//dts-v1/;#include "imx6ull.dtsi"/ {model = "LingYun IoT System Studio IoT Gateway Board";compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";chosen {stdout-path = &uart1;};memory@80000000 { device_type = "memory";reg = <0x80000000 0x20000000>;};leds {compatible = "gpio-leds";pinctrl-names = "default";pinctrl-0 = <&pinctrl_gpio_leds>;status = "okay";sysled {lable = "sysled";gpios = <&gpio4 16 GPIO_ACTIVE_HIGH>;linux,default-trigger = "heartbeat";default-state = "off";};};....../*+--------------+| Misc Modules |+--------------+*/&snvs_poweroff {status = "okay";};&snvs_pwrkey {status = "okay";};&uart1 {pinctrl-names = "default";pinctrl-0 = <&pinctrl_uart1>;status = "okay";};&pwm1 { /* backlight */#pwm-cells = ; pinctrl-names = "default";pinctrl-0 = <&pinctrl_pwm1>;status = "okay";};

设备树源码分为三部分:

  • 头文件,设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。imx6ull.dtsi由NXP官方提供,是一个imx6ull平台“共用”的设备树文件。
  • 设备树节点,“/ {…};”表示“根节点”,每一个设备树只有一个根节点。不同文件的根节点最终会合并为一个。在根节点内部的“chosen{…}”、“leds{…}”等字符,都是根节点的子节点。
  • 设备树节点追加内容,子节点比根节点下的子节点多了一个“&”, 这表示该节点在向已经存在的子节点追加数据。本代码中的“&pwm1{…}”、“&uart1{…}”等等追加的目标节点,就是定义在==“imx6ul.dtsi”==中。

imx6ull.dtsi头文件,在内核源码/arch/arm/boot/dts/imx6ull.dtsi

pwm1: pwm@2080000 { /*节点标签:节点名称@单元地址*/compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm"; /*model属性用于指定设备的制造商和型号*/reg = <0x02080000 0x4000>; /*reg属性描述设备资源在其父总线定义的地址空间内的地址*/interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>; /*描述中断相关的信息*/clocks = <&clks IMX6UL_CLK_PWM1>, /*初始化GPIO外设时钟信息*/<&clks IMX6UL_CLK_PWM1>;clock-names = "ipg", "per";#pwm-cells = ; /*表示有多少个cells来描述pwm引脚*/status = "disabled"; /*状态属性用于指示设备的“操作状态”*/};

到目前为止我们知道设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。

【1】节点基本格式

设备树中的每个节点都按照以下约定命名:

node-name@unit-address{属性1 = …属性2 = …属性3 = …子节点…}
  • node-name 节点名称,用于指定节点的名称。它的长度为1至31个字符,只能由“数字、大小字母、英文逗号句号、下划线和加减号”组成,节点名应当使用大写或小写字母开头并且能够描述设备类别。
  • @unit-address,其中的符号“@”可以理解为是一个分割符,“unit-address”用于指定“单元地址”,它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unitaddress”。注意同级别的设备树下相同级别的子节点节点名唯一 node-name@unit-address 的整体要求同级唯一。
  • 节点标签,节点名的简写,当其它位置需要引用时可以使用节点标签来向该节点中追加内容。在imx6ul.dtsi头文件中,节点名“pwm”前面多了个“pwm1”,这个“pwm1”就是我们所说的节点标签。节点路径,通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,“不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一”。类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。
  • 节点属性:节点的“{}”中包含的内容是节点属性,通常情况下一个节点包含多个属性信息, 这些属
    性信息就是要传递到内核的“板级硬件描述信息”,驱动中会通过一些API函数获取这些信息。

设备树最主要的内容是编写节点的节点属性,通常情况下一个节点代表一个设备。

【2】节点属性

compatible属性

compatible属性值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。设备树中的每一个设备的节点都要有一个compatible属性。系统通过compatible属性决定绑定哪一个设备的设备驱动,是用来查找节点的方法之一,也可以通过节点名或节点路径查找指定节点。例如系统初始化时会初始化platform总线上的设备时,根据设备节点”compatible”属性和驱动中of_match_table对应的值加载对应的驱动。

举个栗子:

my_leds节点:

my_leds {compatible = "my-gpio-leds"; /*设置“compatible”属性值,与led的平台驱动做匹配*/pinctrl-names = "default"; /*定义引脚状态*/pinctrl-0 = <&pinctrl_my_gpio_leds>; /*指定LED灯的引脚pinctrl信息*/status = "okay";led-gpios = <&gpio5 8 GPIO_ACTIVE_HIGH>; /*指定引脚使用的哪个GPIO 引脚名字= */default-state = "off";};

系统初始化时会初始化 platform 总线上的设备时,根据设备节点”compatible”属性和驱动中of_match_table 对应的值,匹配了就加载对应的驱动。

static const struct of_device_id leds_match_table[] = {{.compatible = "my-gpio-leds"},{/* sentinel */},};MODULE_DEVICE_TABLE(of, leds_match_table);/*内核中使用platform_driver结构体来描述平台驱动*/static struct platform_driver gpio_led_driver ={.probe= led_probe, //安装驱动的时候会执行的函数.remove = led_remove,//驱动卸载的时候会执行的函数.driver = {//描述驱动的属性.name= "my_led", //name域.owner = THIS_MODULE,//使用者,一般都是THIS_MODULE.of_match_table = leds_match_table,//驱动能够兼容的设备类型},};

model属性
model 属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式。

/ {model = "LingYun IoT System Studio IoT Gateway Board";......

status属性
状态属性用于指示设备的“操作状态”,通过 status 可以去禁止设备或者启用设备,可用的操作状态如下表。默认情况下不设置 status 属性设备是使能的。
前面的栗子汇中看见:status = “okay”。

其他属性就不过多介绍了,想知更多的可以参考开头的资料。


四、GPIO子系统以及pinctrl子系统介绍

现在我们可以通过在驱动程序代码里使用设备树接口,来获取到外设的信息了。但是我们怎么去操作这些外设呢?总不能直接在驱动代码中操作吧,这样是不合理的。对于有些外设,是具备抽象条件的,也就是说我们可以将对这些外设的操作统一起来。

(1) pinctrl 子系统

pinctrl 子系统主要用于管理芯片的引脚。

imx6ull 芯片拥有众多的片上外设,大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制,例如我们熟悉的 I2C、SPI、LCD、等等。而我们知道芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性,一个芯片引脚往往可以做为多个片上外设的功能引脚。


I2C1 的 SCL 和 SDA 的功能引脚不单单只可以使用在 I2C 上,也可以作为多个外设的功能引脚,如普通的 GPIO 引脚,串口的接收发送引脚等,在设计硬件时我们可以根据需要灵活的选择其中的一个。设计完硬件后每个引脚的功能就确定下来了,假设我们将上面的两个引脚连接到其他用串口控制的外部设备上,那么这两个引脚功能就做为了 UART4 的接收、发送引脚。在编程过程中,无论是裸机还是驱动,一般首先要设置引脚的复用功能并且设置引脚的 PAD 属性(驱动能力、上下拉等等)。

在驱动程序中我们需要手动设置每个引脚的复用功能,不仅增加了工作量,编写的驱动程序不方便移植,可重用性差等。更糟糕的是缺乏对引脚的统一管理,容易出现引脚的重复定义。假设我们在 I2C1 的驱动中将 UART4_RX_DATA 引脚和 UART4_TX_DATA 引脚复用为 SCL 和 SDA,恰好在编写 UART4 驱动驱动时没有注意到 UART4_RX_DATA 引脚和 UART4_TX_DATA 引脚已经被使用,在驱动中又将其初始化为 UART4_RX 和 UART4_TX,这样 IIC1 驱动将不能正常工作,并且这种错误很难被发现。

pinctrl 子系统是由芯片厂商来实现的, 简单来说用于帮助我们管理芯片引脚并自动完成引脚的初始化,而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。

【1】pinctrl子节点编写格式

imx6ull.dtsi (前面设备树引用的头文件)这个文件是芯片厂商官方将芯片的通用的部分单独提出来的一些设备树配置。在iomuxc 节点中汇总了所需引脚的配置信息,pinctrl 子系统存储使用着 iomux 节点信息。

每个芯片厂商的 pinctrl 子节点的编写格式并不相同,这不属于设备树的规范,是芯片厂商自定义的。我们想添加自己的 pinctrl 节点,只要依葫芦画瓢按照格式编写即可。然后放在设备驱动的&iomux节点下,并且在驱动中添加相关的节点信息,就可以使用了。


引脚复用宏定义在下面讲到。

【2】添加pinctrl子节点到iomuxc节点下

举一个栗子:

这是我们需要添加的pinctrl子节点(与上面的格式是一样的):

pinctrl_my_gpio_leds: my-gpio-leds {fsl,pins = <MX6UL_PAD_SNVS_TAMPER8_GPIO5_IO08 0x17059 /* led run */>;};

引脚复用宏定义在哪儿找:

wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts$ cat imx6ul-pinfunc.h /* SPDX-License-Identifier: GPL-2.0-only *//* * Copyright 2014 - 2015 Freescale Semiconductor, Inc. */#ifndef __DTS_IMX6UL_PINFUNC_H#define __DTS_IMX6UL_PINFUNC_H/* * The pin function ID is a tuple of *  */#define MX6UL_PAD_BOOT_MODE0__GPIO5_IO100x0014 0x02a0 0x0000 5 0#define MX6UL_PAD_BOOT_MODE1__GPIO5_IO110x0018 0x02a4 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER0__GPIO5_IO000x001c 0x02a8 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER1__GPIO5_IO010x0020 0x02ac 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER2__GPIO5_IO020x0024 0x02b0 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER3__GPIO5_IO030x0028 0x02b4 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER4__GPIO5_IO040x002c 0x02b8 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER5__GPIO5_IO050x0030 0x02bc 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER6__GPIO5_IO060x0034 0x02c0 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER7__GPIO5_IO070x0038 0x02c4 0x0000 5 0#define MX6UL_PAD_SNVS_TAMPER8__GPIO5_IO080x003c 0x02c8 0x0000 5 0......

具体宏定义的介绍可以参考开头的资料。
设置引脚属性为0x17059(参照内核的设备树,默认设置成0x17059)。
然后将我们上面说的pinctrl子节点添加到igkboard.dts文件中iomuxc节点下就可以了。

/*+----------------------+| Basic pinctrl iomuxc |+----------------------+*/&iomuxc {pinctrl-names = "default";pinctrl_my_gpio_leds: my-gpio-leds {fsl,pins = < MX6UL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x17059 /* led run */>;};pinctrl_camera_clock: cameraclockgrp {fsl,pins = <MX6UL_PAD_CSI_MCLK__CSI_MCLK 0x1b088>;};......

(2)GPIO子系统

【1】GPIO子系统简介

在没有使用 GPIO 子系统之前,如果我们想点亮一个 LED,首先要得到 led 相关的配置寄存器,再手动地读、改、写这些配置寄存器实现控制 LED 的目的。有了 GPIO 子系统之后这部分工作由GPIO 子系统帮我们完成,我们只需要调用 GPIO 子系统提供的 API 函数即可完成 GPIO 的控制动作。

gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。

添加的节点信息举栗子:

pinctrl 配置好以后就是设置 gpio 了,我们写的驱动程序通过读取 GPIO5_IO08 的值来控制我们的LED灯,但是驱动程序怎么知道连接的引脚是 GPIO5_IO08 呢?肯定是需要设备树告诉驱动啊!在设备树中my_leds节点下添加一个属性来描述这个引脚就行了,驱动直接读取这个属性值就知道读取哪一个GPIO了,并且利用GPIO提供的API函数可以设置其输入输出等进行控制。

 my_leds {compatible = "my-gpio-leds"; /*设置“compatible”属性值,与led的平台驱动做匹配*/ pinctrl-names = "default"; /*定义引脚状态*/ pinctrl-0 = <&pinctrl_my_gpio_leds>; /*指定LED灯的引脚pinctrl信息*/ status = "okay"; /*低电平有效选择“GPIO_ACTIVE_LOW”高电平有效选择“GPIO_ACTIVE_HIGH”*/ led-gpios= <&gpio5 8 GPIO_ACTIVE_HIGH>;/*指定引脚使用的哪个GPIO 引脚名字= */ default-state = "off"; };

【2】GPIO 子系统常用 API 函数讲解

GPIO输出设置函数

static inline int gpio_direction_output(unsigned gpio, int value);

函数参数:

  • gpio:设置的 GPIO 的编号。
  • value:设置的输出值,为 1 输出高电平,为 0 输出低电平。

返回值:

  • 成功: 返回 0
  • 失败: 返回负数

GPIO输入设置函数

static inline int gpio_direction_input(unsigned gpio)

函数参数:

  • gpio: 要设置的 GPIO 的编号。

返回值:

  • 成功: 返回 0
  • 失败: 返回负数。

获取 GPIO 编号函数

static inline int of_get_named_gpio(struct device_node *np, const char*propname, int index)

参数:

  • np:指定设备节点。
  • propname: GPIO 属性名,与设备树中定义的属性名对应。
  • index:引脚索引值,在设备树中一条引脚属性可以包含多个引脚,该参数用于指定获取那个引脚。

返回值:

  • 成功:获取的 GPIO 编号(这里的 GPIO 编号是根据引脚属性生成的一个非负整数),
  • 失败: 返回负数。

GPIO 申请函数

static inline int gpio_request(unsigned gpio, const char *label);

参数:

  • gpio: 要申请的 GPIO 编号,该值是函数 of_get_named_gpio 的返回值
  • label: 引脚名字,相当于为申请得到的引脚取了个别名。

返回值:

  • 成功: 返回 0,
  • 失败: 返回负数。

GPIO 释放函数

 static inline void gpio_free(unsigned gpio);

gpio_free 函数与 gpio_request 是一对相反的函数,一个申请,一个释放。一个 GPIO 只能被申请一次,当不再使用某一个引脚时记得将其释放掉。
参数:

  • gpio:要释放的 GPIO 编号。

返回值:

获取 GPIO 引脚值函数

用于获取引脚的当前状态。无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。

static inline int gpio_get_value(unsigned gpio);

设置 GPIO 引脚值函数

该函数只用于那些设置为输出模式的 GPIO。

gpio_set_value(unsigned gpio, int value);