Android驱动的系统笔记2


注意:本网站所有内容为自己学习期间记录的笔记,基本都是原作者那边摘抄过来的,不作商业用途也不插播广告。(如有侵权,请联系我删除,15909440083)

北京迅为电子

同款开发板购买链接

【第16期的 最新驱动资料(文档+例程)】
链接:https://pan.baidu.com/s/1Qf5d5_e2u_VklJWJK9wzNQ 提取码:n4q6

第七篇设备树

教程规划,思维导图

image-20240822093927402

第55 章初识设备树

55.1 设备树的由来

设备树(Device Tree)是一种硬件描述机制,用于在嵌入式系统和操作系统中描述硬件设备的特性、连接关系和配置信息。它提供了一种与平台无关的方式来描述硬件,使得内核与硬件之间的耦合度降低,提高了系统的可移植性和可维护性。

在上一篇平台总线内容的学习中,我们使用platform_device 结构体来对硬件设备进行描述,这是一种传统的平台总线设备描述方式。每个platform_device 结构表示一个特定的硬件设备,并通过注册到平台总线上来使得内核能够与该设备进行通信和交互。该结构包含设备的名称、资源(如内存地址、中断号等)、设备驱动程序等信息。

然而,随着时间的推移,Linux 内核中的ARM 部分存在着大量的平台相关配置代码,这些代码通常是杂乱而重复的,导致了维护的困难和工作量的增加。在2011 年3 月17 日,Linux的创始人Linus Torvalds 在ARM Linux 邮件列表中发表了一封帖子,他表达了对ARM 架构配置方式的不满,并宣称”Gaah. Guys, this whole ARM thing is a f*cking pain in the ass”。这引起了广泛的讨论和反思。ARM 社区中的开发者们开始认识到,传统的平台相关配置方式已经变得不可持续,需要一种更加先进和可扩展的方法来解决这个问题。

为了应对这一挑战,ARM 社区开始探索新的硬件描述机制,并逐渐形成了设备树的概念。设备树提供了一种更加灵活和可移植的描述硬件的机制,将设备的描述信息转移到设备树中。设备树使用一种结构化的数据格式,通过描述设备节点、属性和连接关系等信息,使得硬件的描述与具体的平台无关,同时允许多个平台共享相同的设备树描述。

设备树的引入为ARM 架构上的Linux 内核带来了革命性的变化。它提供了一种统一的硬件描述方式,使得不同芯片和板级的支持更加简单和灵活。此外,设备树还提供了硬件配置的可视化和可读性,方便开发者理解和调试硬件。

随着时间的推移,设备树逐渐成为了嵌入式系统和Linux 内核中描述硬件的标准方式。它不仅在ARM 架构上得到了广泛应用,也被扩展到其他架构和平台上。

55.2 设备树基础知识

当描述设备树(Device Tree)时,通常会涉及到以下几个关键术语:DTS、DTSI、DTB 和DTC。下面来对每个术语进行介绍。

  • DTS(Device Tree Source):DTS 是设备树的源文件,采用一种类似于文本的语法来描述硬件设备的结构、属性和连接关系。DTS 文件以.dts 为扩展名,通常由开发人员编写。它是人类可读的形式,用于描述设备树的层次结构和属性信息。
  • DTSI(Device Tree Source Include):DTSI 文件是设备树源文件的包含文件。它扩展了DTS文件的功能,用于定义可重用的设备树片段。DTSI 文件以.dtsi 为扩展名,可以在多个DTS 文件中包含和共享。通过使用DTSI,可以提高设备树的可重用性和可维护性(和C 语言中头文件的作用相同)。
  • DTB(Device Tree Blob):DTB 是设备树的二进制表示形式。DTB 文件是通过将DTS 或DTSI文件编译而成的二进制文件,以.dtb 为扩展名。DTB 文件包含了设备树的结构、属性和连接信息,被操作系统加载和解析。在运行时,操作系统使用DTB 文件来动态识别和管理硬件设备。
  • DTC(Device Tree Compiler):DTC 是设备树的编译器。它是一个命令行工具,用于将DTS和DTSI 文件编译成DTB 文件。DTC 将文本格式的设备树源代码转换为二进制的设备树表示形式,以便操作系统能够加载和解析。DTC 是设备树开发中一个重要的工具。

DTS、DTSI、DTB 和DTC 之间的关系:

  • (1)开发人员使用文本编辑器编写DTS 和DTSI 文件,描述硬件设备的层次结构、属性和连接关系。
  • (2)DTSI 文件可以在多个DTS 文件中包含和共享,以提高设备树的可重用性和可维护性。
  • (3)使用DTC 编译器,开发人员将DTS 和DTSI 文件编译成二进制的DTB 文件,如下图(图55- 1)所示:

image-20240822094029902

  • (4)操作系统在启动过程中加载和解析DTB 文件,以识别和管理硬件设备。

设备树文件存放路径:

ARM 体系结构:

ARM 体系结构下的设备树源文件通常存放在arch/arm/boot/dts/目录中。该目录是设备树源文件的根目录。如下图(图55- 2)所示:

image-20240822094226866

ARM64 体系结构:

设备树源文件路径:ARM64 体系结构下的设备树源文件通常存放在arch/arm64/boot/dts/目录及其子目录中。该目录也是设备树源文件的根目录,并包含了针对不同ARM64 平台和设备的子目录,如下图(图55- 3)所示:

image-20240822094249535

子目录结构:在ARM64 的子目录中,同样会按照硬件平台、设备类型或制造商进行组织和分类。这些子目录的命名可能与特定芯片厂商(如Qualcomm、NVIDIA、Samsung)有关,由于我们本手册使用的soc 是瑞芯微的rk3568 , 所以匹配的设备树目录为arch/arm64/boot/dts/rockchip。每个子目录中可能包含多个设备树文件,用于描述不同的硬件配置和设备类型,这里以rockchip 目录内容如下图(图55- 4)所示:

image-20240822094323207

55.3 设备树的编译

设备树的编译是将设备树源文件(如上述的.dts 文件)转换为二进制的设备树表示形式(.dtb文件)的过程。编译器通常被称为DTC(Device Tree Compiler)
在Linux 内核源码中,DTC(Device Tree Compiler)的源代码和相关工具通常存放在scripts/dtc/目录中,如下图(图55- 5)所示:

image-20240822094414414

在编译完源码之后dtc 设备树编译器会默认生成,如果没有生成相应的dtc 可执行文件,可以查看在内核默认配置文件中CONFIG_DTC 是否使能。

设备树的编译:

在Linux 环境中,可以使用以下命令将设备树源文件编译为二进制设备树文件:

dtc -I dts -O dtb -o output.dtb input.dts

其中,input.dts是输入的设备树源文件,output.dtb是编译后的二进制设备树文件。
编译器会验证设备树源文件的语法和语义,生成与硬件描述相对应的设备树表示形式。

设备树的反编译:

设备树的反编译是将二进制设备树文件转换回设备树源文件的过程,以便进行查看、编辑或修改。反编译器通常也是DTC。
在Linux 环境中,可以使用以下命令将二进制设备树文件反编译为设备树源文件:

dtc -I dtb -O dts -o output.dts input.dtb

其中,input.dtb 是输入的二进制设备树文件,output.dts 是反编译后的设备树源文件。

反编译器会将二进制设备树文件解析并还原为文本形式的设备树源文件,使其可读性更好。

下面来进行一下实际的设备树编译和反编译的演示,首先创建一个名为test.dts 的设备树文件,文件内容如下所示:

/dts-v1/;
/ {

};

创建完成如下图(图55- 6)所示:
image-20240822094646859

这个设备树很简单,只包含了根节点/,而根节点中没有任何子节点或属性。这个示例并没有描述任何具体的硬件设备或连接关系,它只是一个最基本的设备树框架,在本小节只是为了测试设备树的编译和反编译。

然后使用以下命令进行设备树的编译,编译完成如下图(图55- 7)所示:

/home/topeet/Linux/linux_sdk/kernel/scripts/dtc/dtc -I dts -O dtb -o test.dtb test.dts

image-20240822094736031

可以看到test.dtb 就生成了,然后继续使用以下命令对test.dtb 进行反编译,反编译完成如下图(图55- 8)所示:

/home/topeet/Linux/linux_sdk/kernel/scripts/dtc/dtc -I dtb -O dts -o 1.dts test.dtb

image-20240822094800246

可以看到反编译出的1.dts 跟之前的test.dts 内容相同。

第56 章设备树基本语法

56.1 设备树语法讲解1

56.1.1 根节点

设备树使用一种层次结构,其中的根节点(Root Node)是整个设备树的起始点和顶层节点。根节点由一个以/开头的标识符来表示,然后使用{}来包含根节点所在的内容,一个最简单的根节点示例如下所示:

/dts-v1/; 			// 设备树版本信息
/ {
    // 根节点开始
    /*
    在这里可以添加注释,描述根节点的属性和配置
    */
};

其中第一行的设备树中的版本信息行dts-v1 是可选的,可以根据需要选择是否保留。这行注释通常用于指定设备树的语法版本。如果您不需要在设备树中指定版本信息,可以将其删除。

56.1.2 子节点

设备树中的子节点是根节点的直接子项,用于描述具体的硬件设备或设备集合。子节点采用以下特定的格式来表示:

[label:] node-name@[unit-address] {
    [properties definitions]
    [child nodes]
    };

以下是对这些部分的详细介绍:

  • (1)节点标签(Label)(可选):节点标签是一个可选的标识符,用于在设备树中引用该节点。标签允许其他节点直接引用此节点,以便在设备树中建立引用关系。
  • (2)节点名称(Node Name):节点名称是一个字符串,用于唯一标识该节点在设备树中的位置。节点名称通常是硬件设备的名称,但必须在设备树中是唯一的。
  • (3)单元地址(Unit Address)(可选):单元地址用于标识设备的实例。它可以是一个整数、一个十六进制值或一个字符串,具体取决于设备的要求。单元地址的目的是区分相同类型的设备的不同实例,例如在下图(55-6)中名为cpu 的节点通过它们的单元地址值0 和1 来区分,名称为ethernet 的节点通过其单元地址值fe002000 和fe003000 来区分。
image-20240822100423138

(4)属性定义(Properties Definitions):属性定义是一组键值对,用于描述设备的配置和特性。属性可以根据设备的需求进行定义,例如寄存器地址、中断号、时钟频率等,关于这些属性会在后面的小节中进行讲解
(5)子节点(Child Nodes):子节点是当前节点的子项,用于进一步描述硬件设备的子组件或配置。子节点可以包含自己的属性定义和更深层次的子节点,形成设备树的层次结构。

56.1.3 reg 属性

reg 属性用于在设备树中指定设备的寄存器地址和大小,提供了与设备树中的物理设备之间的寄存器映射关系。

reg 属性可以在设备节点中有单个值格式和列表值格式这两种常见格式,接下来将对这两种格式进行介绍:

(1)单个值格式如下所示:

reg = <address size>;

这种格式适用于描述单个寄存器的情况。其中,address设备的起始寄存器地址,可以是一个整数或十六进制值。size 表示寄存器的大小,即占用的字节数。

例如,假设有一个设备节点my_device,使用单个值格式的reg 属性来描述一个4 字节寄存器的地址和大小,可以这样定义:

my_device {
    compatible = "vendor,device";
    reg = <0x1000 0x4>;
    // 其他属性和子节点的定义
}

在这个示例中,my_device 设备节点的reg 属性值为<0x1000 0x4>,表示从地址0x1000 开始的4 字节寄存器区域。

(2)列表值格式如下所示:

reg = <address1 size1 address2 size2 ...>;

当设备具有多个寄存器区域时,可以使用列表值格式的reg 属性来描述每个寄存器区域的地址和大小。通过这种方式,可以指定多个寄存器的位置和大小,以描述设备的完整寄存器映射。

例如,考虑一个设备节点my_device,它具有两个寄存器区域,分别是8 字节和4 字节大小的寄存器。可以使用列表值格式的reg 属性来描述这种情况:

my_device {
    compatible = "vendor,device";
    reg = <0x1000 0x8 0x2000 0x4>;
    // 其他属性和子节点的定义
};

在这个示例中,my_device 设备节点的reg 属性值为<0x1000 0x8 0x2000 0x4>,表示设备有两个寄存器区域。第一个寄存器区域从地址0x1000 开始,大小为8 字节;第二个寄存器区域从地址0x2000 开始,大小为4 字节。

通过使用reg 属性,设备树可以提供有关设备寄存器布局和寄存器访问方式的信息。这对于操作系统的设备驱动程序很重要,因为它们需要了解设备的寄存器映射以正确地与设备进行交互和配置。

56.1.4 address-cells 和size-cells 属性

#address-cells#size-cells 属性用于指定在上个小节中要设置的设备树中地址单元和大小单元的位数。它们提供了设备树解析所需的元数据,以正确解释设备的地址和大小信息。下面对两个属性分别进行介绍:

(1)#address-cells 属性:

#address-cells 属性是一个位于设备树根节点的特殊属性它指定了设备树中地址单元的位数。地址单元是设备树中用于表示设备地址的单个单位。它通常是一个整数,可以是十进制或十六进制值。

#address-cells 属性的值告诉解析设备树的软件在解释设备地址时应该使用多少位来表示一个地址单元。

默认情况下,#address-cells 的值为2,表示使用两个单元来表示一个设备地址。这意味着设备的地址将由两个整数(每个整数使用指定位数的位)组成。

例如,对于一个使用两个32 位(4 字节)整数表示地址的设备,可以在设备树的根节点中设置#address-cells 属性为<2>。

(2)#size-cells 属性:

#size-cells 属性也是一个位于设备树根节点的特殊属性它指定了设备树中大小单元的位数。大小单元是设备树中用于表示设备大小的单个单位。它通常是一个整数,可以是十进制或十六进制值。

#size-cells 属性的值告诉解析设备树的软件在解释设备大小时应该使用多少位来表示一个大小单元

默认情况下,#size-cells 的值为1,表示使用一个单元来表示一个设备的大小。这意味着设备的大小将由一个整数(使用指定位数的位)表示。

例如,对于一个使用一个32 位(4 字节)整数表示大小的设备,可以在设备树的根节点中设置#size-cells 属性为<1>。

这两个属性的存在是为了使设备树能够灵活地描述各种设备的地址和大小表示方式。通过在设备树的根节点中设置适当的#address-cells#size-cells 值,设备树解析软件能够正确地解释设备节点中的地址和大小信息。

以下是两个个示例,展示了根节点中#address-cells 和#size-cells 属性的使用:

示例1:

node1 {
    #address-cells = <1>;
    #size-cells = <1>;
    node1-child {
        reg = <0x02200000 0x4000>;
        // 其他属性和子节点的定义
    };
};

在这个示例中,node1-child 节点的reg 属性使用了<0x02200000 0x4000> 表示地址和大小。由于#address-cells 的值为<1>,表示使用一个单元来表示地址。#size-cells 的值也为<1>,表示使用一个单元来表示大小。

解释后的地址和大小值如下:
地址部分:0x02200000 被解释为一个地址单元,地址为0x02200000。
大小部分:0x4000 被解释为一个大小单元,大小为0x4000。

示例2:

node1 {
    #address-cells = <2>;
    #size-cells = <0>;
    node1-child {
        reg = <0x0000 0x0001>;
        // 其他属性和子节点的定义
    };
};

在这个示例中,node1-child 节点的reg 属性使用了<0x0000 0x0001> 表示地址。由于#address-cells 的值为<2>,表示使用两个单元来表示地址。#size-cells 的值为<0>,表示不使用单元来表示大小。

解释后的地址值如下:
地址部分:0x0000 0x0001 被解释为两个地址单元,其中第一个地址单元为0x0000,第二个地址单元为0x0001。

这种使用#address-cells#size-cells 属性的方式使得设备树可以适应不同设备的寄存器映射和大小表示方式,并确保设备树解析软件能够正确解释设备的地址和大小信息。

56.1.5 model 属性

在设备树中,model 属性用于描述设备的型号或者名称。它通常作为设备节点的一个属性,用来提供关于设备的标识信息。model 属性是可选的,但在实际应用中经常被使用。

model 属性的值是一个字符串,可以是设备的型号、名称、或者其他标识符,用来识别设备。该值通常由设备的厂商定义,并且在设备树中使用。

以下是一个示例,展示了如何在设备树中使用model 属性:

my_device {
    compatible = "vendor,device";
    model = "My Device XYZ";
    // 其他属性和子节点的定义
};

在这个示例中,my_device 节点具有model 属性,其值为”My Device XYZ”。这个值描述了设备的型号或名称为”My Device XYZ”。

model 属性通常用于标识和区分不同的设备,特别是当设备节点的compatible 属性相同或相似时。通过使用不同的model 属性值,可以更加准确地确定所使用的设备类型。

56.1.6 status 属性

在设备树中,status 属性用于描述设备或节点的状态。它是设备树中常见的属性之一,用于表示设备或节点的可用性或操作状态。
status 属性的值可以是以下几种:

  • “okay”:表示设备或节点正常工作,可用。
  • “disabled”:表示设备或节点被禁用,不可用。
  • “reserved”:表示设备或节点已被保留,暂时不可用。
  • “fail”:表示设备或节点初始化或操作失败,不可用。

以下是一个示例,展示了如何在设备树中使用status 属性:

my_device {
    compatible = "vendor,device";
    status = "okay";
    // 其他属性和子节点的定义
};

在这个示例中,my_device 节点具有status 属性,其值为”okay”。这表示设备处于正常工作状态,可用。

通过使用status 属性,设备树可以动态地控制设备的启用和禁用状态。这对于在系统启动过程中选择性地启用或禁用设备,或者在运行时根据特定条件调整设备状态非常有用。

56.1.7 compatible 属性

在设备树中,compatible 属性用于描述设备的兼容性信息。它是设备树中重要的属性之一,用于识别设备节点与驱动程序之间的匹配关系。
compatible 属性的值是一个字符串或字符串列表,用于指定设备节点与相应的驱动程序或设备描述符兼容的规则。通常,compatible 属性的值由设备的厂商定义,并且在设备树中使用。

以下是一些常见的compatible 属性值的示例:

  • (1)单个字符串值:例如”vendor,device”,用于指定设备节点与特定厂商的特定设备兼容。
  • (2)字符串列表:例如[“vendor,device1”, “vendor,device2”],用于指定设备节点与多个设备兼容,通常用于设备节点具有多种变体或配置。
  • (3)通配符匹配:例如"vendor,*",用于指定设备节点与特定厂商的所有设备兼容,不考虑具体的设备标识。

以下是一个示例,展示了如何在设备树中使用compatible 属性:

my_device {
    compatible = "vendor,device";
    // 其他属性和子节点的定义
};

在这个示例中,my_device 节点具有compatible 属性,其值为”vendor,device”。这个值用于标识设备节点与特定厂商的特定设备兼容。

compatible 属性也可以具有多个匹配值,用于指定设备节点与多个设备或驱动程序的兼容性规则。这种情况下,compatible 属性的值是一个字符串列表,每个字符串表示一个匹配值。

以下是一个示例,展示了具有多个匹配值的compatible 属性的用法:

my_device {
    compatible = ["vendor,device1", "vendor,device2"];
    // 其他属性和子节点的定义
};

在这个示例中, my_device 节点具有compatible 属性, 其值为[“vendor,device1”,”vendor,device2”]。这表示设备节点与厂商的device1 和device2 兼容。

通过使用compatible 属性,设备树可以提供设备和驱动程序之间的匹配信息。当设备树被操作系统或设备管理软件解析时,会根据设备节点的compatible 属性值来选择适合的驱动程序进行设备的初始化和配置

56.2 设备树语法讲解2

56.2.1 aliases 节点

aliases 节点是一个特殊的节点,用于定义设备别名。该节点位于设备树的根部,并具有节点路径/aliases。

aliases 节点是一个容器节点,包含一组属性,每个属性都代表一个设备别名。每个属性的名称是别名的标识符,而属性的值是被引用设备节点的路径或设备树中其他节点的路径。

以下是一个示例,演示了如何在设备树中使用aliases 节点:

aliases {
    mmc0 = &sdmmc0;
    mmc1 = &sdmmc1;
    mmc2 = &sdhci;
    serial0 = "/simple@fe000000/seria1@11c500";
};

在给定的例子中,有四个别名的定义:

  • (1)mmc0 别名与设备树中的sdmmc0 节点相关联。通过使用别名mmc0,其他设备节点或客户端程序可以更方便地引用sdmmc0 节点,而不必直接使用其完整路径。
  • (2)mmc1 别名与设备树中的sdmmc1 节点相关联。通过使用别名mmc1,其他设备节点或客户端程序可以更方便地引用sdmmc1 节点,而不必直接使用其完整路径。
  • (3)mmc2 别名与设备树中的sdhci 节点相关联。通过使用别名mmc2,其他设备节点或客户端程序可以更方便地引用sdhci 节点,而不必直接使用其完整路径。
  • (4)serial0 别名与设备树中的路径/simple@fe000000/seria1@11c500 相关联。通过使用别名serial0,其他设备节点或客户端程序可以更方便地引用该路径,而不必记住整个路径字符串。

在别名的定义中,& 符号用于引用设备树中的节点。别名的目的是提供可读性更高的名称,使设备树更易于理解和维护。通过使用别名,可以简化设备节点之间的关联,并减少重复输入设备节点的路径。

客户端程序可以使用别名属性名称来引用完整的设备路径或部分路径。当客户端程序将别名字符串视为设备路径时,应检测并使用别名。这样,设备树的使用者可以更方便地引用设备节点,而不必记住复杂的路径结构。

需要注意的是,**aliases 节点中定义的别名只在设备树内部可见,不能在设备树之外引用。它们主要用于设备树的内部组织和引用,以提高可读性和可维护性。**

56.2.2 chosen 节点

chosen 节点是设备树中的一个特殊节点,用于传递和存储系统引导和配置的相关信息。它位于设备树的根部,并具有路径/chosen。

chosen 节点通常包含以下子节点和属性:

  • (1)bootargs:用于存储引导内核时传递的命令行参数。它可以包含诸如内核参数、设备树参数等信息。在引导过程中,操作系统或引导加载程序可以读取该属性来获取启动参数。
  • (2)stdout-path:用于指定用于标准输出的设备路径。在引导过程中,操作系统可以使用该属性来确定将控制台输出发送到哪个设备,例如串口或显示屏。
  • (3)firmware-name:用于指定系统固件的名称。它可以用于标识所使用的引导加载程序或固件的类型和版本。
  • (4)linux,initrd-start 和linux,initrd-end:这些属性用于指定Linux 内核初始化RAM 磁盘(initrd)的起始地址和结束地址。这些信息在引导过程中被引导加载程序使用,以将initrd 加载到内存中供内核使用。
  • (5)其他自定义属性:chosen 节点还可以包含其他自定义属性,用于存储特定于系统引导和配置的信息。这些属性的具体含义和用法取决于设备树的使用和上下文。关于chosen 节点的实际例子如下所示:
chosen {
    bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

在这个示例中, chosen 节点具有一个属性bootargs , 其值为"root=/dev/nfs rwnfsroot=192.168.1.1 console=ttyS0,115200"
通过这些命令行参数,操作系统或引导加载程序可以配置内核在引导过程中正确地加载NFS 根文件系统,并将控制台输出发送到指定的串口设备。

通过使用chosen 节点,系统引导过程中的相关信息可以方便地传递给操作系统或引导加载程序。这样,系统引导和配置的各个组件可以共享和访问这些信息,从而实现更灵活和可配置的系统引导流程。chosen 节点提供了一种通用的机制,使得不同的设备树和引导系统可以在传递信息方面保持一致性,并且可以根据具体需求扩展和自定义。

56.2.3 device_type 节点

在设备树中,device_type 节点是用于描述设备类型的节点。它通常作为设备节点的一个属性存在。device_type 属性的值是一个字符串,用于标识设备的类型。

device_type 节点的存在有助于操作系统或其他软件识别和处理设备。它提供了设备的基本分类信息,使得驱动程序、设备树解析器或其他系统组件能够根据设备的类型执行相应的操作。

常见的设备类型包括但不限于:

  • (1)cpu:表示中央处理器。
  • (2)memory:表示内存设备。
  • (3)display:表示显示设备,如液晶显示屏。
  • (4)serial:表示串行通信设备,如串口。
  • (5)ethernet:表示以太网设备。
  • (6)usb:表示通用串行总线设备。
  • (7)i2c:表示使用I2C (Inter-Integrated Circuit) 总线通信的设备。
  • (8)spi:表示使用SPI (Serial Peripheral Interface) 总线通信的设备。
  • (9)gpio:表示通用输入/输出设备。
  • (10)pwm:表示脉宽调制设备。

这些只是一些常见的设备类型示例,实际上,设备类型可以根据具体的硬件和设备树的使用情况进行自定义和扩展。根据设备类型,操作系统或其他软件可以加载适当的驱动程序、配置设备资源、建立设备之间的连接等。

56.2.4 自定义属性

设备树中的自定义属性是用户根据特定需求添加的属性。这些属性可以用于提供额外的信息、配置参数或元数据,以满足设备或系统的特定要求。

在设备树中添加自定义属性时,可以在设备节点或其他适当的节点下定义新的属性。自定义属性可以是整数、字符串、布尔值或其他数据类型。它们的命名应遵循设备树的命名约定,并且应该与已有的属性名称避免冲突。

例如可以在设备树中自定义一个管脚标号的属性pinnum,添加好的设备树源码如下所示:

my_device {
    compatible = "my_device";
    pinnum = <0 1 2 3 4>;
};

在上述示例中,my_device 是一个自定义设备节点,并添加了一个自定义属性pinnum。该属性的值<0 1 2 3 4> 是一个整数数组,表示管脚的标号(PIN number)。

通过这样定义pinnum 属性,您可以在设备树中为特定设备指定管教标号,以便操作系统、驱动程序或其他软件组件使用。这可以用于在设备初始化或配置过程中对特定管教进行操作或控制。

第57 章实例分析:中断

57.1 中断相关属性

57.1.1 RK ft5x06 设备树节点

下面展示的是iTOP-RK3568 开发板SDK 源码中的ft5x06 设备树,其中蓝色字体部分就是关于中断相关的描述,包括了interruptsinterrupt-controller#interrupt-cellsinterrupt-parent四种常见属性:

gpio0: gpio@fdd60000 {
    compatible = "rockchip,gpio-bank";
    reg = <0x0 0xfdd60000 0x0 0x100>;
    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&pmucru PCLK_GPI00>, <&pmucru DBCLK_GPI00>;
    gpio-controller;
    #gpio-cells = <2>;
    gpio-ranges = <&pinctrl 0 0 32>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

ft5x06: ft5x06@38 {
    status = "disabled";
    compatible = "edt,edt-ft5306";
    reg = <0x38>;
    touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
    interrupt-parent = <&gpio0>;
    interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
    reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
    touchscreen-size-x = <800>;
    touchscreen-size-y = <1280>;
    touch_type = <1>;
};

其中gpio0 节点是在内核源码的“/arch/arm64/boot/dts/rockchip/rk3568.dtsi”设备树的3549-3560 行定义的, 而ft5x06: ft5x06@38 触摸芯片节点是在内核源码的“/arch/arm64/boot/dts/rockchip/topeet_rk3568_lcds.dtsi”设备树的301-313 行定义的。接下来将会对设备树常见的四个中断属性进行介绍。

57.1.2 interrupts

interrupts 属性用于指定设备的中断相关信息。它描述了中断控制器的类型、中断号以及中断触发类型。下面将对interrupts 属性的各个方面进行介绍。
在第一小节中列举的设备树源码中的gpio0 节点和ft5x06 节点都涉及到了interrupts 属性,如下所示:

gpio0: gpio@fdd60000 {
    ....
    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
    ....
};

ft5x06: ft5x06@38 {
    ....
    interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
    ....
};

gpio0 节点的interrupts 具有三个参数,分别表示中断控制器类型中断号中断触发类型,每个参数的具体描述如下所示:

(1)中断控制器类型:
interrupts 属性的第一个参数指定了中断控制器的类型。常见的类型包括GIC (GenericInterrupt Controller)IRQ (Basic Interrupt Handling) 等。例如,在给定的代码片段中,GIC_SPI 表示中断控制器的类型为GIC SPI 中断。

中断控制器负责管理系统中的中断信号,它可以是硬件中的专用中断控制器,也可以是处理器内部的中断控制器。

(2)中断号:
interrupts 属性的第二个参数指定了设备所使用的中断号。中断号是一个唯一标识符,用于区分不同的中断信号源。系统使用中断号来识别中断源并进行相应的中断处理。

中断号可以是一个整数值,也可以是一个宏定义或符号引用。在给定的代码片段中,33 表示该设备使用的中断号为33。

(3)中断触发类型:
interrupts 属性的第三个参数指定了中断的触发类型,即中断信号的触发条件。常见的触发类型包括边沿触发和电平触发。

  • 边沿触发表示中断信号在从低电平到高电平或从高电平到低电平的变化时触发。触发类型可以是上升沿触发、下降沿触发或双边沿触发
  • 电平触发表示中断信号在保持特定电平状态时触发,可以是高电平触发或低电平触发。

在给定的代码片段中,IRQ_TYPE_LEVEL_HIGH 表示中断的触发类型为高电平触发。触发类型的宏定义在内核源码“include/dt-bindings/interrupt-controller/irq.h”目录下,具体内容如下所示:

#define IRQ_TYPE_NONE 0 // 无中断触发类型
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)	// 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发

而在ft5x06 节点中只有中断号和中断触发类型两个参数,这是为什么呢,带着疑问我们继续学习下面的几个属性。

57.1.3 interrupt-controller

interrupt-controller 属性是设备树中用于描述中断控制器的属性之一。它提供了关于中断控制器的相关信息,以便操作系统和其他设备能够正确配置和使用中断系统。

interrupt-controller 属性用于标识当前节点所描述的设备是一个中断控制器。中断控制器是硬件或软件模块,负责管理和分发中断信号。它接收来自各种设备的中断请求,并根据优先级和配置规则分发中断给相应的处理器或设备。

interrupt-controller 属性本身没有特定的属性值,只需出现在节点的属性列表中即可。出现该属性的存在即表示该节点描述的设备是中断控制器。

57.1.4 interrupt-parent

interrupt-parent 属性是设备树中用于建立中断信号源与中断控制器之间关联的属性。它指定了中断信号源所属的中断控制器节点,以确保正确的中断处理和分发。

interrupt-parent 属性用于指定中断信号源所属的中断控制器。中断信号源是产生中断的设备或其他中断源节点。通过指定中断控制器,操作系统可以正确地将中断请求传递给相应的中断控制器节点进行处理和分发。

interrupt-parent 属性值是一个引用,它指向中断控制器节点的路径或标签。可以使用路径来引用中断控制器节点,如/interrupt-controller-node,或使用标签来引用中断控制器节点,如&interrupt-controller-label,在第一小节例子中的ft5x06 就是通过中断控制器节点和gpio0 中断控制器建立了联系,如下所示:

ft5x06: ft5x06@38 {
    ....
    interrupt-parent = <&gpio0>;
};

中断信号源节点(例如设备节点或其他中断源节点)中的interrupt-parent 属性用于指定中断信号源所属的中断控制器节点。这样,中断信号源就可以将中断请求传递给正确的中断控制器进行处理。中断信号源节点的interrupts 属性中的中断号和其他相关信息将与指定的中断控制器关联起来。

在某些情况下,中断控制器可以形成多级结构,其中一个中断控制器节点可能是另一个中断控制器的父节点。在这种情况下,interrupt-parent 属性可以用于指定层次结构中的上级中断控制器。

57.1.5 #interrupt-cells

#interrupt-cells 属性用于描述中断控制器中每个中断信号源的中断编号单元的数量。中断编号单元是指用于表示中断号和其他相关信息的固定大小的单元。通过指定中断编号单元的数量,操作系统可以正确解析和处理中断信息,并将其与中断控制器和中断信号源进行关联。

#interrupt-cells 属性的值是一个整数,表示中断编号单元的数量。通常,这个值是一个正整数,例如1、2 或3,取决于中断控制器和设备的要求。

在gpio0 的中断控制器为gic,在gic 节点中#interrupt-cells 属性被设置为3,这也就是为什么在gpio0 节点中interrupts 属性有三个值,而ft5x06 的中断控制器为gpio0,在gpio0 节点中#interrupt-cells 属性被设置为2,所以ft5x06 节点的interrupts 属性只有两个值。

57.2 中断实例编写

在上一个小节中对设备树中断要用到的属性进行了讲解,而在本小节将会编写一个在RK3568 上的ft5x06 触摸中断设备树。
首先确定ft5x06 的中断引脚号,由于iTOP-RK3568 有1.2 和1.7 两个版本,所以这里展示了两个版本的原理图:

image-20240822111753364

image-20240822111759747

第一张图为V1.2 版本的原理图,触摸引脚网络标号为TP_INT_L_GPIO0_B5,对应的SOC管脚为GPIO0_B5,第二张图为V1.7 版本的原理图,触摸引脚网络标号为TP_INT_L_GPIO3_A5,对应的SOC 管脚为GPIO3_A5

然后来查看内核源码目录下的“drivers/input/touchscreen/edt-ft5x06.c”文件,这是ft5x06的驱动文件,找到compatible 匹配值相关的部分,如下(图57-3)所示:

image-20240822111817968

这里的compatible 匹配值都可以选择,作者选择的是edt,edt-ft5206,选择其他compatible也是可以的。
在内核源码目录下的“include/dt-bindings/pinctrl/rockchip.h”头文件中,定义了RK 引脚名和gpio 编号的宏定义,如下图(57-4)所示:

image-20240822111837251

可以看到RK 已经将GPIO 组和引脚编号写成了宏定义的形式,通过宏定义可以减少在编写设备树的过程中换算的时间,并且帮助大家进行理解。
至此,关于编写ft5x06 设备树的前置内容就查找完成了,接下来进行设备树的编写。编写完成的设备树如下所示:

V1.2

/dts-v1/;
#include "dt-bindings/pinctrl/rockchip.h"
#include "dt-bindings/interrupt-controller/irq.h"
/{
    model = "This is my devicetree!";
    ft5x06@38 {
        compatible = "edt,edt-ft5206";
        interrupt-parent = <&gpio0>;
        interrupts = <RK_PB5 IRQ_TYPE_EDGE_RISING>;
    };
};

V1.7

/dts-v1/;
#include "dt-bindings/pinctrl/rockchip.h"
#include "dt-bindings/interrupt-controller/irq.h"
/{
    model = "This is my devicetree!";
    ft5x06@38 {
        compatible = "edt,edt-ft5206";
        interrupt-parent = <&gpio3>;
        interrupts = <RK_PA5 IRQ_TYPE_EDGE_RISING>;
    };
};

第1 行: 设备树文件的头部,指定了使用的设备树语法版本。
第3 行:用于定义Rockchip 平台的引脚控制器相关的绑定。
第4 行:用于定义中断控制器相关的绑定。
第5 行:表示设备树的根节点开始。
第6 行:指定了设备树的模型名称,描述为”This is my devicetree!”。
第9 行:指定了设备节点的兼容性字符串,表示该设备与”edt,edt-ft5206” 兼容。
第10 行:指定了中断的父节点,即中断控制器所在的节点。这里使用了一个引用(&gpio0)来表示父节点。
第11 行:指定了中断信号的配置。RK_PB5 表示中断信号的引脚编号,IRQ_TYPE_EDGE_RISING 表示中断类型为上升沿触发。
至此,关于ft5x06 的设备树就讲解完成了。

57.3 其他SOC 设备树对比

而无论使用的是瑞芯微SOC 还是恩智浦、三星的SOC,在设备树关于中断相关的描述都离不开上面提到的四个属性,关于在恩智浦和三星源码中的ft5x06 设备树如下所示:

gpio1: gpio@0209c000 {
    compatible = "fsl,inx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    
    edt-ft5x06@38 {
        compatible = "edt,edt-ft5306", "edt,edt-ft5x06", "edt,edt-ft5406";
        pinctrl-names = "default";
        pinctrl-0 = <&ts_int_pin &ts_reset_pin>;
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 0>;
        reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
        irq-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
        status = "disabled";
    };
};

三星

gpio_c: gpioc {
    compatible = "gpio-controller";
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

ft5x06: ft5x06038 {
    compatible = "edt,edt-ft5406";
    reg = <0x38>;
    pinctrl-names = "default";
    
    #if defined(RGB_1024x600) || defined(RGB_800x480)
    pinctrl-0 = <&tsc2007_irq>;
    interrupt-parent = <&gpio_c>;
    interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
    #endif
    
    #if defined(LvDs_800×1280) || defined(LvDS_1024x768)
    pinctrl-0 = <&gt911_irq>;
    interrupt-parent = <&gpio_b>;
    interrupts = <29 IRQ_TYPE_EDGE_FALLING>;
    #endif
    
    reset-gpios = <&gpio_e 30 0>;
};

对比之后会发现,不同厂商对于中断属性的配置都是类似的,只是里面的参数有些许区别,如果大家在之后遇到了其他平台,只需要稍加区分即可。

第58 章实例分析:时钟

时钟(Clock)用于描述硬件设备和系统中的时钟源以及时钟相关的配置和连接关系。时钟在计算机系统中起着至关重要的作用,用于同步和定时各种硬件设备的操作。时钟可以分为两个主要角色:时钟生产者(clock provider)和时钟消费者(clock consumer)。

58.1 时钟生产者(Clock Provider)

定义:时钟生产者是负责生成和提供时钟信号的硬件或软件模块。它可以是时钟控制器、PLL、时钟发生器等。
设备树节点:时钟生产者在设备树中以时钟节点的形式表示。

时钟节点属性:
(1)clock-cells:该属性用于指定时钟编号的位数。它是一个整数值,表示时钟编号的位数。通常情况下,当clock-cells 为0 时表示一个时钟,为1 表示多个时钟。具体示例如下所示:

示例1:单个时钟
osc24m: osc24m {
    compatible = "clock";
    clock-frequency = <24000000>;
    clock-output-names = "osc24m";
    #clock-cells = <O>;
};

示例2:多个时钟
    clock: clock {
    #clock-cells = <1>;
    clock-output-names = "clock1", "clock2";
};

(2)clock-frequency 属性是设备树中用于指定时钟频率的属性。它用于描述时钟节点所提供的时钟信号的频率,使用Hertz (Hz) 作为单位。对于时钟生产者节点,clock-frequency 属性表示该节点生成的时钟信号的频率。它用于描述时钟控制器、晶振、PLL 等产生时钟信号的硬件或软件模块的输出频率,例如指定时钟频率为24000000 的具体示例如下所示:

osc24m: osc24m {
    compatible = "clock";
    clock-frequency = <24000000>;
    clock-output-names = "osc24m";
    #clock-cells = <O>;
};

(3)assigned-clocksassigned-clock-rates 是设备树中用于描述多路时钟的属性,通常一起使用。

assigned-clocks 属性用于标识时钟消费者节点所使用的时钟源。它是一个整数数组,每个元素对应一个时钟编号。时钟编号是指时钟生产者节点(如时钟控制器)所提供的时钟源的编号。通过在时钟消费者节点中使用assigned-clocks 属性,可以指定该节点所需的时钟源。

assigned-clock-rates 属性用于指定每个时钟源的时钟频率。它是一个整数数组,每个元素对应一个时钟源的频率。时钟频率以Hz (赫兹) 为单位表示。assigned-clock-rates 属性的元素数量和顺序应与assigned-clocks 属性中的时钟编号相对应。

关于assigned-clocksassigned-clock-rates 属性的一个具体示例如下所示:

cru: clock-controller@fdd20000 {
    #clock-cells = <1>;
    assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKVDEC_PRE>;
    assigned-clock-rates = <32768>, <300000000>;
};

(4)clock-indicesclock-indices 属性用于指定时钟消费者节点所使用的时钟源的索引值。它是一个整数数组,每个元素对应一个时钟源的索引。

时钟索引是指时钟生产者节点(如时钟控制器)所提供的时钟源的编号。通过在时钟消费者节点中使用clock-indices 属性,可以明确指定该节点所需的时钟源,并按照特定的顺序进行匹配。一个clock-indices 示例如下所示:

scpi_dvfs: clocks-0 {
    #clock-cells = <1>;
    clock-indices = <0>, <1>, <2>;
    clock-output-names = "atlclk", "aplclk", "gpuclk";
};

scpi_clk: clocks-1 {
    #clock-cells = <1>;
    clock-indices = <3>;
    clock-output-names = "pxlclk";
};

在第一个节点中”atlclk”, “aplclk”, “gpuclk”三个时钟源的索引就分别被设置为了0、1、2,在第二个节点中”pxlclk”时钟源的索引值被设置为了3.

(5)assigned-clock-parents 属性用于指定时钟消费者节点所使用的时钟源的父时钟源。它是一个时钟源引用的数组,每个元素对应一个父时钟源的引用。在时钟的层次结构中,某些时钟源可能是其他时钟源的父时钟源,即它们提供时钟信号给其他时钟源作为输入。通过在时钟消费者节点中使用assigned-clock-parents 属性,可以明确指定该节点所需的父时钟源,并按照特定的顺序进行匹配。一个实际的assigned-clock-parents 属性例子如下所示:

clock: clock {
    assigned-clocks = <&clkcon 0>, <&pll 2>;
    assigned-clock-parents = <&pll 2>;
    assigned-clock-rates = <115200>, <9600>;
};

上述设备树表示了一个名为clock 的时钟消费者节点,具有以下属性:

  • assigned-clocks 属性指定了该节点使用的时钟源,引用了两个时钟源节点:clkcon 0 和pll 2。
  • assigned-clock-parents 属性指定了这些时钟源的父时钟源,引用了pll 2 时钟源节点。
  • assigned-clock-rates 属性指定了每个时钟源的时钟频率,分别是115200 和9600。

58.2 时钟消费者(Clock Consumer)

定义:时钟消费者是依赖时钟信号的硬件设备或模块。它们通过引用时钟生产者节点提供的时钟源来获取时钟信号。
设备树节点:时钟消费者在设备树中的节点中使用属性来引用时钟生产者的时钟源。

时钟消费者属性:
(1)clocks:该属性用于指定时钟消费者节点所需的时钟源。它是一个整数数组,每个元素是一个时钟编号,表示时钟消费者需要的一个时钟源。
(2)clock-names:可选属性,用于指定时钟消费者节点所需时钟源的名称。它是一个字符串数组,与clocks 数组一一对应,用于提供时钟源的描述性名称。

一个时钟消费者示例如下所示:

clock: clock {
    clocks = <&cru CLK_VOP>;
    clock-names = "clk_vop";
};

clocks 属性指定了该节点使用的时钟源,引用了cru 节点中的CLK_VOP 时钟源。
clock-names 属性指定了时钟源的名称,这里是”clk_vop”。

第59 章实例分析:CPU

59.1 cpus 节点

设备树的cpus 节点是用于描述系统中的处理器的一个重要节点。它是处理器拓扑结构的顶层节点,包含了所有处理器相关的信息。下面将详细介绍设备树的cpus 节点的各个方面。

节点结构:
cpus 节点是一个容器节点,其下包含了系统中每个处理器的子节点。每个子节点的名称通常为cpu@X,其中X 是处理器的索引号。每个子节点都包含了与处理器相关的属性,例如时钟频率、缓存大小等。

处理器属性:
cpu@X 子节点中的属性可以包括以下信息:

  • (1)device_type:指示设备类型为处理器(”cpu”)。
  • (2)reg:指定处理器的地址范围,通常是物理地址或寄存器地址。
  • (3)compatible:指定处理器的兼容性信息,用于匹配相应的设备驱动程序。
  • (4)clock-frequency:指定处理器的时钟频率。
  • (5)cache-size:指定处理器的缓存大小。

处理器拓扑关系:
除了处理器的基本属性,cpus 节点还可以包含其他用于描述处理器拓扑关系的节点,以提供更详细的处理器拓扑信息。这些节点可以帮助操作系统和软件了解处理器之间的连接关系、组织结构和特性。

  • cpu-map 节点:描述处理器的映射关系,通常在多核处理器系统中使用。
  • socket 节点:描述多处理器系统中的物理插槽或芯片组。
  • cluster 节点:描述处理器集群,即将多个处理器组织在一起形成的逻辑组。
  • core 节点:描述处理器核心,即一个物理处理器内的独立执行单元。
  • thread 节点:描述处理器线程,即一个物理处理器核心内的线程。

这些节点的嵌套关系可以在cpus 节点下形成一个层次结构,反映了处理器的拓扑结构。上述这些节点会在后面的小节进行介绍。一个单核CPU 设备树和一个四核CPU 设备树示例如下所示:

单核CPU 示例:

cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    cpu0: cpu@0 {
        compatible = "arm,cortex-a7";
        device_type = "cpu";
        // 其他属性...
        };
}

多核CPU 示例:

cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    cpu0: cpu@0 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
    };
    cpu1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
    };
    cpu2: cpu@2 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
    };
    cpu3: cpu@3 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
    };
}

cpus 节点是一个容器节点,包含了cpu0 子节点。该节点使用了#address-cells 和#size-cells 属性来指定地址和大小的单元数量。
cpu0 子节点代表第一个处理器,具有以下属性:

  • compatible 属性指定了处理器的兼容性信息
  • device_type 属性指示设备类型为处理器。

你可以在此基础上继续添加其他属性来描述处理器的特性,如时钟频率、缓存大小等。

59.2 cpu-mapsocketcluster 节点

cpu-map 节点是设备树中用于描述大小核架构处理器的映射关系的节点之一。它的父节点必须是cpus 节点,而子节点可以是一个或多个cluster 和socket 节点。通过cpu-map 节点,可以定义不同核心和集群之间的连接和组织结构。

socket 节点用于描述处理器插槽(socket)之间的映射关系。每个socket 子节点表示一个处理器插槽,可以使用cpu-map-mask 属性来指定该插槽使用的核心。通过为每个socket 子节点指定适当的cpu-map-mask,可以定义不同插槽中使用的核心。这样,操作系统和软件可以了解到不同插槽之间的核心分配情况。

cluster 节点用于描述核心(cluster)之间的映射关系。每个cluster 子节点表示一个核心集群,可以使用cpu-map-mask 属性来指定该集群使用的核心。通过为每个cluster 子节点指定适当的cpu-map-mask,可以定义每个集群中使用的核心。这样,操作系统和软件可以了解到不同集群之间的核心分配情况。

通过在cpu-map 节点中定义socket 和cluster 子节点,并为它们指定适当的cpu-map-mask,可以提供处理器的拓扑结构信息。这对于操作系统和软件来说非常有用,因为它们可以根据这些信息进行任务调度和资源分配的优化,以充分利用大小核架构处理器的性能和能效特性。

一个大小核架构的具体示例如下所示:

cpus {
    #address-cells = <2>;
    #size-cells = <0>;
    cpu-map {
        cluster0 {
            core0 {
            	cpu = <&cpu_l0>;
            };
            core1 {
            	cpu = <&cpu_l1>;
            };
            core2 {
            	cpu = <&cpu_l2>;
            };
            core3 {
            	cpu = <&cpu_l3>;
            };
        };
        cluster1 {
            core0 {
            	cpu = <&cpu_b0>;
            };
            core1 {
            	cpu = <&cpu_b1>;
            };
        };
    };
    
    cpu_l0: cpu@0 {
        device_type = "cpu";
        compatible = "arm,cortex-a53", "arm,armv8";
    };
    cpu_l1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a53", "arm,armv8";
    };
    cpu_l2: cpu@2 {
        device_type = "cpu";
        compatible = "arm,cortex-a53", "arm,armv8";
    };
    cpu_l3: cpu@3 {
        device_type = "cpu";
        compatible = "arm,cortex-a53", "arm,armv8";
    };
    cpu_b0: cpu@100 {
        device_type = "cpu";
        compatible = "arm,cortex-a72", "arm,armv8";
    };
    cpu_b1: cpu@101 {
        device_type = "cpu";
        compatible = "arm,cortex-a72", "arm,armv8";
    };
};

第60 章实例分析:GPIO

60.1 GPIO 相关属性

<1>在GPIO控制器中,必须有一个属性#gpio-cells,表示其他节点如果使用这个GPIO控制器需要几个cell来表示使用哪一个GPIO。
<2>在GPIO控制器中,必须有一个属性gpio-controller,表示他是GPIO控制器。
<3>在设备树中使用GPIO,需要使用属性data-gpios=<&gpio1 12 0>来指定具体的GPIO引脚。data-gpios属性可以为自定义属性。

简化:
gpiol: gpiol {
    gpio-controller
    #gpio-cells = <2>
}
[...]

data-gpios = <&gpio1 12 0><&gpio1 15 0>

60.1.1 RK ft5x06 设备树节点

下面展示的是iTOP-RK3568 开发板SDK 源码中的ft5x06 设备树,其中蓝色字体部分就是关于gpio 相关的描述,包括了gpiosgpio-controller#gpio-cellsgpio-parent四种常见属性:

gpio0: gpio@fdd60000 {
    compatible = "rockchip,gpio-bank";
    reg = <0x0 0xfdd60000 0x0 0x100>;
    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&pmucru PCLK_GPI00>, <&pmucru DBCLK_GPI00>;
    gpio-controller;
    #gpio-cells = <2>;
    gpio-ranges = <&pinctrl 0 0 32>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

ft5x06: ft5x06@38 {
    status = "disabled";
    compatible = "edt,edt-ft5306";
    reg = <0x38>;
    touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
    interrupt-parent = <&gpio0>;
    interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
    reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
    touchscreen-size-x = <800>;
    touchscreen-size-y = <1280>;
    touch_type = <1>;
};

其中gpio0 节点是在内核源码的“/arch/arm64/boot/dts/rockchip/rk3568.dtsi”设备树的3549-3560 行定义的, 而ft5x06: ft5x06@38 触摸芯片节点是在内核源码的“/arch/arm64/boot/dts/rockchip/topeet_rk3568_lcds.dtsi”设备树的301-313 行定义的。接下来将会对设备树常见的四个gpio 属性进行介绍。

60.1.2 gpio-controller

gpio-controller 属性用于标识一个设备节点作为GPIO 控制器。GPIO 控制器是负责管理和控制GPIO 引脚的硬件模块或驱动程序。

gpio-controller 属性通常作为设备节点的一个属性出现,位于设备节点的属性列表中。当一个设备节点被标识为GPIO 控制器时,它通常会定义一组GPIO 引脚,并提供相关的GPIO 控制和配置功能。其他设备节点可以使用该GPIO 控制器来控制和管理其GPIO 引脚。

通过使用gpio-controller 属性,设备树可以明确标识出GPIO 控制器设备节点,使系统可以正确识别和管理GPIO 引脚的配置和控制。

60.1.3 #gpio-cells

#gpio-cells 属性用于指定GPIO 引脚描述符的编码方式。GPIO 引脚描述符是用于标识和配置GPIO 引脚的一组值,例如引脚编号、引脚属性等。

#gpio-cells 属性的属性值是一个整数,表示用于编码GPIO 引脚描述符的单元数。通常,这个值为2。

在第一小节的示例中有1 个gpio 引脚描述属性,由于#gpio-cells 属性被设置为了2,所以每个引脚描述属性中会有两个整数,具体内容如下所示:

ft5x06: ft5x06@38 {
    .....
    reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
    .....
};

RK_PB6GPIO_ACTIVE_LOW 都属于宏定义,会在下面的小节进行讲解。
通过使用#gpio-cells 属性,设备树可以指定GPIO 引脚描述符的编码方式,使系统能够正确识别和解析GPIO 引脚的配置和控制。

60.1.4 gpio-ranges

gpio-ranges 属性是设备树中一个用于描述GPIO 范围映射的属性。它通常用于描述具有大量GPIO 引脚的GPIO 控制器,以简化GPIO 引脚的编码和访问。

在设备树中,GPIO 控制器的每个引脚都有一个本地编号,用于在控制器内部进行引脚寻址。然而,这些本地编号并不一定与外部引脚的物理编号或其他系统中使用的编号一致。为了解决这个问题,可以使用gpio-ranges 属性将本地编号映射到实际的引脚编号。

gpio-ranges 属性是一个包含一系列整数值的列表,每个整数值对应于设备树中的一个GPIO控制器。列表中的每个整数值按照特定的顺序提供以下信息:

  • (1)外部引脚编号的起始值。
  • (2)GPIO 控制器内部本地编号的起始值。
  • (3)引脚范围的大小(引脚数量)。

在第一小节的示例中gpio-ranges 属性的值为<&pinctrl 0 0 32>,其中<&pinctrl>表示引用了名为pinctrl 的引脚控制器节点,**0 0 32 表示外部引脚从0 开始,控制器本地编号从0 开始,共映射了32 个引脚。**

这样,gpio-ranges 属性将GPIO 控制器的本地编号直接映射到外部引脚编号,使得GPIO 引脚的编码和访问更加简洁和直观。

60.1.5 gpio 引脚描述属性

第一小节的设备树中关于gpio 引脚描述属性相关内容如下所示:

ft5x06: ft5x06@38 {
    .....
    reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
    .....
};

gpio 引脚描述属性个数由#gpio-cells 所决定,因为gpio0 节点中的#gpio-cells 属性设置为了2,所以上面设备树gpio 引脚描述属性个数也为2。

其中RK_PB6 定义在内核源码目录下的“include/dt-bindings/pinctrl/rockchip.h”头文件中,定义了RK 引脚名和gpio 编号的宏定义,如下图(图60-1)所示:

image-20240822140646363

GPIO_ACTIVE_LOW 定义在源码目录下的“include/dt-bindings/gpio/gpio.h”中,表示设置为低电平,同理GPIO_ACTIVE_HIGH 就表示将这个GPIO 设置为高电平,但这里只是对设备的描述,具体的设置还是要跟驱动相匹配。

60.1.6 其他属性

本小节将根据下面的设备树示例讲解一下gpio 的其他重要属性,设备树具体内容如下所示:

gpio-controller@00000000 {
    compatible = "foo";
    reg = <0x00000000 0x1000>;
    gpio-controller;
    #gpio-cells = <2>;
    ngpios = <18>;
    gpio-reserved-ranges = <0 4>, <12 2>;
    gpio-line-names = "MMC-CD", "MMC-WP",
                    "voD eth", "RST eth", "LED R",
                    "LED G", "LED B", "col A",
                    "col B", "col C", "col D",
                    "NMI button", "Row A", "Row B",
                    "Row C", "Row D", "poweroff",
                    "reset";
};
  • 第6 行的ngpios 属性指定了GPIO 控制器所支持的GPIO 引脚数量。它表示该设备上可用的GPIO 引脚的总数。在这个例子中,ngpios的值为18,意味着该GPIO 控制器支持18个GPIO 引脚
  • 第7 行的gpio-reserved-ranges 属性定义了保留的GPIO 范围。每个范围由两个整数值表示,用尖括号括起来。保留的GPIO 范围意味着这些GPIO 引脚不可用或已被其他设备或功能保留。在这个例子中,有两个保留范围:<0 4>和<12 2>。<0 4>表示从第0 个引脚开始的连续4 个引脚被保留,而<12 2>表示从第12 个引脚开始的连续2 个引脚被保留。
  • 第8 行的gpio-line-names 属性定义了GPIO 引脚的名称,以逗号分隔。每个名称对应一个GPIO 引脚。这些名称用于标识和识别每个GPIO 引脚的作用或连接的设备。在这个例子中,gpio-line-names 属性列出了多个GPIO 引脚的名称,如”MMC-CD”、”MMC-WP”、”voD eth” 等等。通过这些名称,可以清楚地了解每个GPIO 引脚的功能或用途。

60.2 中断实例编写

在上一个小节中对设备树中断要用到的属性进行了讲解,而在本小节将会编写一个在RK3568 上LED 灯的中断设备树。
首先确定LED 的引脚编号,LED 原理图如下图(图60-2)所示:

image-20240822141028170

从上面的原理图可以得到LED 灯的引脚网络标号为Working_LEDEN_H_GPIO0_B7,对应的引脚为GPIO0_B7

然后来查看内核源码目录下的“drivers/drivers/leds/leds-gpio.c”文件,这是led 的驱动文件,然后找到compatible 匹配值相关的部分,如下(图60-3)所示:

image-20240822141058102

可以看到compatible 匹配值为gpio-leds
最后在内核源码目录下的“include/dt-bindings/pinctrl/rockchip.h”头文件中,定义了RK 引脚名和gpio 编号的宏定义,如下图(图60-4)所示:

image-20240822141115068

在源码目录下的“include/dt-bindings/gpio/gpio.h”文件中定义了引脚极性设置宏定义,如下图(图60-5)所示:

image-20240822141127740

其中GPIO_ACTIVE_HIGH 表示将该引脚设置为高电平,GPIO_ACTIVE_LOW 表示将该引脚设置为低电平。
至此,我们关于编写LED 设备树的前置内容就查找完成了,接下来进行设备树的编写。编写完成的设备树如下所示:

/dts-v1/;
#include "dt-bindings/pinctrl/rockchip.h"
#include "dt-bindings/gpio/gpio.h"
/{
    model = "This is my devicetree!";
    led led@1 {
        compatible = "gpio-leds";
        gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>
    };
};
  • 第1 行: 设备树文件的头部,指定了使用的设备树语法版本。
  • 第3 行:用于定义Rockchip 平台的引脚控制器相关的绑定。
  • 第4 行:用于定义中断控制器相关的绑定。
  • 第5 行:表示设备树的根节点开始。
  • 第6 行:指定了设备树的模型名称,描述为”This is my devicetree!”。
  • 第9 行:指定了设备节点的兼容性字符串,表示该设备与”gpio-leds” 兼容。
  • 第10 行:指定了该LED 设备所使用的GPIO 引脚。&gpio0 是引脚控制器的引用,RK_PB7 是引脚的编号或标识,GPIO_ACTIVE_HIGH 表示该GPIO 引脚的活动电平是高电平。

至此,关于led 的设备树就讲解完成了。

60.3 其他SOC 设备树对比

而无论使用的是瑞芯微SOC 还是恩智浦、三星的SOC,在设备树关于gpio 相关的描述都是类似的,关于在恩智浦和三星源码中的ft5x06 设备树如下所示(关于gpio 相关的属性已经标记为了蓝色):

恩智浦

gpio1: gpio@0209c000 {
    compatible = "fsl,inx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    
    edt-ft5x06@38 {
        compatible = "edt,edt-ft5306", "edt,edt-ft5x06", "edt,edt-ft5406";
        pinctrl-names = "default";
        pinctrl-0 = <&ts_int_pin &ts_reset_pin>;
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 0>;
        reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
        irq-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
        status = "disabled";
    };
};

三星

gpio_c: gpioc {
    compatible = "gpio-controller";
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

ft5x06: ft5x06038 {
    compatible = "edt,edt-ft5406";
    reg = <0x38>;
    pinctrl-names = "default";
    
    #if defined(RGB_1024x600) || defined(RGB_800x480)
    pinctrl-0 = <&tsc2007_irq>;
    interrupt-parent = <&gpio_c>;
    interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
    #endif
    
    #if defined(LvDs_800×1280) || defined(LvDS_1024x768)
    pinctrl-0 = <&gt911_irq>;
    interrupt-parent = <&gpio_b>;
    interrupts = <29 IRQ_TYPE_EDGE_FALLING>;
    #endif
    
    reset-gpios = <&gpio_e 30 0>;
};

对比之后会发现,不同厂商对于gpio 的配置都是类似的,只是里面的参数有些许区别,如果大家在之后遇到了其他平台,只需要稍加区分即可。

第61 章实例分析:pinctrl

61.1 pinmux 介绍

Pinmux(引脚复用)是指在系统中配置和管理引脚功能的过程。在许多现代集成电路中,单个引脚可以具有多个功能,例如作为GPIO、UART、SPI 或I2C 等。通过使用引脚复用功能,可以在这些不同的功能之间切换。

引脚复用通过硬件和软件的方式实现。硬件层面,芯片设计会为每个引脚提供多个功能的选择。这些功能通常由芯片厂商在芯片规格文档中定义。通过编程设置寄存器或开关,可以选择某个功能来连接引脚。这种硬件层面的配置通常是由引脚控制器(Pin Controller)或引脚复用控制器(Pin Mux Controller)负责管理。

软件层面,操作系统或设备驱动程序需要了解和配置引脚的功能。它们使用设备树(DeviceTree)或设备树绑定(Device Tree Bindings)来描述和配置引脚的功能。在设备树中,可以指定引脚的复用功能,将其连接到特定的硬件接口或功能。操作系统或设备驱动程序在启动过程中解析设备树,并根据配置对引脚进行初始化和设置。

那我们要怎样知晓每一个管脚都可以复用成什么功能呢,一般在核心板原理图都会标注出每个管脚的复用功能,如下图(图61-1)所示:

image-20240822141702208

从上图可以看到UART4_RX_M1 对应的引脚可以复用为以下6 个功能LCDC_D16VOP_BT1120_D7GMAC1_RXD0_M0UART4_RX_M1PWM8_M0GPIO3_B1_d,对应的BGA引脚标号为AG1,那这里的AG1 是如何定位的呢。

在BGA(Ball Grid Array,球栅阵列)封装中,引脚标号是用于唯一标识每个引脚的标识符。这些标号通常由芯片制造商定义,并在芯片的规格文档或数据手册中提供。

BGA 芯片的引脚标号通常由字母和数字的组合构成。它们用于在芯片的封装底部的焊盘上进行标记。每个引脚标号都与芯片内部的功能或信号相对应,以便正确连接到印刷电路板(PCB)上的目标位置。RK3568 的引脚标号图如下(图61-2)所示:

image-20240822141736261

可以看到纵向为A-AH 的28 个字母类型标号,横向为1-28 的28 个字母类型标号,瑞芯微也在对应的3568 数据手册中加入了根据BGA 位置制作的复用功能图,部分内容如下图(图61-3)所示:

image-20240822141800339

其中黑色框代表被保留的引脚,其他有颜色的框一般为电源和地,白色的框代表有具体复用功能的引脚。

脚复用提高了芯片的灵活性和可重用性,通过允许同一个引脚在不同的功能之间切换,可以减少硬件设计的复杂性和成本。此外,引脚复用还使得在使用相同芯片的不同应用中可以更加灵活地配置和定制引脚功能。

会在下一个小节中讲解如何使用pinctrl 在设备树中配置引脚的复用。

61.2 使用pinctrl 设置复用关系

pinctrl(引脚控制)用于描述和配置硬件设备上的引脚功能和连接方式。它是设备树的一部分,用于在启动过程中传递引脚配置信息给操作系统和设备驱动程序,以便正确地初始化和控制引脚。

在设备树中,pinctrl(引脚控制)使用了客户端和服务端的概念来描述引脚控制的关系和配置。

61.2.1 客户端(Client)(固定的)

接下来将使用三个例子对客户端要用到的属性进行讲解。

例1:

node {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_hog_1>;
}

在例1 中,pinctrl-names 属性定义了一个状态名称:default。
pinctrl-0 属性指定了第一个状态default 对应的引脚配置。
<&pinctrl_hog_1> 是一个引脚描述符,它引用了一个名为pinctrl_hog_1 的引脚控制器节点。这表示在default 状态下,设备的引脚配置将使用pinctrl_hog_1 节点中定义的配置。

例2:

node {
pinctrl-names = "default", "wake up";
pinctrl-0 = <&pinctrl_hog_1>;				//这个就是default的状态的配置,pinctrl_hog_1是服务端的节点
pinctrl-1 = <&pinctrl_hog_2>;
}

在例2 中,pinctrl-names 属性定义了两个状态名称:default 和wake up。
pinctrl-0 属性指定了第一个状态default 对应的引脚配置,引用了pinctrl_hog_1 节点。

pinctrl-1 属性指定了第二个状态wake up 对应的引脚配置,引用了pinctrl_hog_2 节点。
这意味着设备可以处于两个不同的状态之一,每个状态分别使用不同的引脚配置。

例3:

node {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>;
}

在这个例子中,pinctrl-names 属性仍然定义了一个状态名称:default。
pinctrl-0 属性指定了第一个状态default 对应的引脚配置,但与之前的例子不同的是,它引用了两个引脚描述符:pinctrl_hog_1 和pinctrl_hog_2。
这表示在default 状态下,设备的引脚配置将使用pinctrl_hog_1 和pinctrl_hog_2 两个节点中定义的配置。这种方式可以将多个引脚控制器的配置组合在一起,以满足特定状态下的引脚需求。

至此关于客户端的内容就讲解完成了,低于客户端的内容,不同厂家的编写格式是相同的,而服务端每个厂家就有区别了,在下一个小节将以rk3568 的pinctrl 服务端为例进行讲解。

61.2.2 服务端(Server)(根据平台)

服务端是设备树中定义引脚配置的部分。它包含引脚组和引脚描述符,为客户端提供引脚配置选择。服务端在设备树中定义了pinctrl 节点,其中包含引脚组和引脚描述符的定义。

这里以瑞芯微的RK3568 为例进行pinctrl 服务端的讲解,瑞芯微原厂BSP 工程师为了方便用户通过pinctrl 设置管脚的复用关系,将包含所有复用关系的配置写在了内核目录下的“arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi”设备树中,具体内容如下(图61-4)所示:

image-20240827112404162

pinctrl 节点中就是每个节点的复用功能,然后我们以uart4 的引脚复用为例进行讲解,uart4 的pinctrl 服务端内容如下(图61-5)所示:

image-20240827140548217

其中<3 RK_PB1 4 &pcfg_pull_up><3 RK_PB2 4 &pcfg_pull_up>分别表示将GPIO3PB1 引脚设置为功能4,将GPIO3PB2 也设置为功能4,且电器属性都会设置为上拉。通过查找原理图可以得到两个引脚在BGA 封装位置分别为AG1 和AF2,如下图(图61-6)所示:

image-20240827141158404

然后在rk3568 的数据手册中找到引脚复用表对应的位置,具体内容如下图所示:

image-20240827141223385image-20240827141232242

image-20240827141241969

可以看到功能4 对应串口4 的发送端和接收端,pinctrl 服务端的配置和数据手册中的引脚复用功能是一一对应,那如果要将RK_PB1 和RK_PB2 设置为GPIO 功能要如何设置呢,从上图可以看到GPIO 对应功能0,所以可以通过以下pinctrl 内容将设置RK_PB1 和RK_PB2 设置为GPIO 功能(事实上如果不对该管脚进行功能复用该引脚默认就会设置为GPIO 功能):

<3 RK_PB1 0 &pcfg_pull_up>,
<3 RK_PB2 0 &pcfg_pull_up>;

最后来看客户端对uart4 服务端的引用,具体内容在内核源码目录“arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10-linux.dts”:

image-20240827141417695

通过在客户端中引用服务端的引脚描述符,设备树可以将客户端和服务端的引脚配置关联起来。这样,在设备树被解析和处理时,操作系统和设备驱动程序可以根据客户端的需求,查找并应用适当的引脚配置。

61.3 pinctrl 实例编写

本小节将通过上面学到的pinctrl 相关知识,将led 的控制引脚复用为GPIO 模式。

首先来对rk3568 的设备树结构进行以下介绍,根据sdk 源码目录下的“device/rockchip/rk356x/BoardConfig-rk3568-evb1-ddr4-v10.mk”默认配置文件可以了解到编译的设备树为rk3568-evb1-ddr4-v10-linux.dts,整理好的设备树之间包含关系列表如下所示:

顶层设备树 rk3568-evb1-ddr4-v10-linux.dts
第二级设备树 rk3568-evb1-ddr4-v10.dtsi rk3568-linux.dtsi
第三级设备树 rk3568.dtsi
rk3568-evb.dtsi
topeet_screen_choose.dtsi
topeet_rk3568_lcds.dtsi

Led 在rk3568-evb.dtsi 设备树中已经被正常配置了,如下图(图61- 9)所示:

image-20240827141725887

这时候可能大家就有问题了,这里也并没有配置pinctrl 呀,那为什么led 最后能正常使用呢,这个原因在上节课中其实我们已经提到了,当一个引脚没有被复用为任何功能时,默认就是GPIO 功能,所以这里没有pinctrl led 功能也可以正常使用。
但这里我们仍旧使用pinctrl 对led 进行配置,从而熟练pinctrl,首先注释掉leds 节点,注释完成如下(图61- 10)所示:

image-20240827141810730

保存退出之后,然后进入到rk3568-evb1-ddr4-v10.dtsi 设备树中,找到rk_485_ctl 节点,如下图(图61- 11)所示:

image-20240827141920151

这是根节点的最后一个节点,而且也是用来控制一个GPIO 的,我们完全可以仿照该节点,在该节点下方编写led 控制节点,仿写完成的设备树内容如下所示:

my_led: led {
    compatible = "topeet,led";
    gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "default";
    pinctrl-0 = <&rk_led_gpio>;
};
  • 第1 行:节点名称为led,标签名为my_led。
  • 第2 行:compatible 属性指定了设备的兼容性标识,即设备与驱动程序之间的匹配规则。在这里,设备标识为”topeet,led”,表示该LED 设备与名为”topeet,led” 的驱动程序兼容。
  • 第3 行:gpios 属性指定了与LED 相关的GPIO(通用输入/输出)引脚配置。
  • 第4 行:pinctrl-names 属性指定了与引脚控制相关的命名。default 表示状态0
  • 第5 行:pinctrl-0 属性指定了与pinctrl-names 属性中命名的引脚控制相关联的实际引脚控制器配置。<&rk_led_gpio> 表示引用了名为rk_led_gpio 的引脚控制器配置。

添加完成如下图(图61- 12)所示:

image-20240827142351625

然后继续找到在同一设备树文件的485 pinctrl 服务端节点,具体内容如下(图61- 13)所示:

image-20240827142412208

然后在该节点下方仿写led 控制引脚pinctrl 服务端节点,仿写完成的节点内容如下所示:

rk_led{
    rk_led_gpio:rk-led-gpio {
        rockchip,pins = <0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
    };
};

添加完成之后如下图(图61- 14)所示:

image-20240827142504710

至此,led 的控制引脚就通过pinctrl 被复用为了GPIO 功能,保存退出之后,重新编译内核,没有报错就证明我们的实验完成了。

第62 章dtb 文件格式讲解

设备树Blob (DTB) 格式是设备树数据的平面二进制编码。它用于在软件程序之间交换设备树数据。例如,在启动操作系统时,固件会将DTB 传递给操作系统内核。

DTB 格式在单个、线性、无指针数据结构中对设备树数据进行编码。它由一个小头部和三个可变大小的部分组成:内存保留块、结构块和字符串块。这些应该以该顺序出现在展平的设备树中。因此,设备树结构作为一个整体,当加载到内存地址时,将类似于下图(图62-1)所示:

image-20240827143144552
/dts-v1/;
/ {
    model = "This is my devicetree!";
    #address-cells = <1>;
    #size-cells = <1>;
    chosen {
    	bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
    };
    cpu1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a35", "arm,armv8";
        reg = <0x0 0x1>;
    };
    aliases {
        led1 = "/gpio@22020101";
    };
    node1 {
        #address-cells = <1>;
        #size-cells = <1>;
        gpio@22020102 {
            reg = <0x20220102 0x40>;
        };
    };
    node2 {
        node1-child {
            pinnum = <01234>;
        };
    };
    gpio@22020101 {
        compatible = "led";
        reg = <0x20220101 0x40>;
        status = "okay";
    };
};

而我们之后要分析的是二进制的dtb 文件,所以需要使用dtc 工具将上面的dts 文件编译成dtb 文件,具体命令如下(图62-2)所示:

image-20240827143537756

为了方便用户学习,已经将本章节要讲解的设备树dts 文件和dtb 文件放在了对应的网盘路径下,同时也将pxBinaryViewerSetup 二进制分析软件放在了同一目录下,iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux驱动例程\49_dt_format,如下图(图62-3)所示:

image-20240827143610609

使用二进制分析软件打开deb 文件并设置大端模式之后如下(图62-4)所示:

image-20240827143704746

在接下来的小节中将会对读取出的设备树二进制内容进行讲解。

62.1 Header

devicetree 的头布局由以下C 结构定义。所有的头字段都是32 位整数,以大端格式存储。

struct fdt_header {
    uint32_t magic; // 设备树头部的魔数
    uint32_t totalsize; // 设备树文件的总大小
    uint32_t off_dt_struct; // 设备树结构体(节点数据)相对于文件开头的偏移量
    uint32_t off_dt_strings; // 设备树字符串表相对于文件开头的偏移量
    uint32_t off_mem_rsvmap; // 内存保留映射表相对于文件开头的偏移量
    uint32_t version; // 设备树版本号
    uint32_t last_comp_version; // 最后一个兼容版本号
    uint32_t boot_cpuid_phys; // 启动CPU 的物理ID
    uint32_t size_dt_strings; // 设备树字符串表的大小
    uint32_t size_dt_struct; // 设备树结构体(节点数据)的大小
};

每个字段的描述如下所示:

image-20240827143959237

然后来查看二进制文件,其中4 个字节表示一个单位,前十个单位分别代表上述的十个字段如下图(图62-5)所示:

image-20240827144032405

image-20240827144304278

image-20240827144315370

在接下来的小节中将会对header 提到的内存保留块、结构块和字符串块进行更详细的讲解。

62.2 内存保留块

内存保留块(Memory Reserved Block)是用于客户端程序的保护和保留物理内存区域的列表。这些保留区域不应被用于一般的内存分配,而是用于保护重要数据结构,以防止客户端程序覆盖这些数据。内存保留块的目的是确保特定的内存区域在客户端程序运行时不被修改或使用。由于在示例设备树中没有设置内存保留块,所以相应的区域都为0,如下(图62-6)所示:

image-20240827144358886

保留区域列表: 内存保留块是一个由一组64 位大端整数对构成的列表。每对整数对应一个保留内存区域,其中包含物理地址和区域的大小(以字节为单位)。这些保留区域应该彼此不重叠。
保留区域的用途: 客户端程序不应访问内存保留块中的保留区域,除非引导程序提供的其他信息明确指示可以访问。引导程序可以使用特定的方式来指示客户端程序可以访问保留内存的部分内容。引导程序可能会在文档、可选的扩展或特定于平台的文档中说明保留内存的特定用途。
格式: 内存保留块中的每个保留区域由一个64 位大端整数对表示。每对由以下C 结构表示:

struct fdt_reserve_entry {
    uint64_t address;
    uint64_t size;
};

其中的第一个整数表示保留区域的物理地址,第二个整数表示保留区域的大小(以字节为单位)。每个整数都以64 位的形式表示,即使在32 位架构上也是如此。在32 位CPU 上,整数的高32 位将被忽略。

内存保留块为设备树提供了保护和保留物理内存区域的功能。它确保了特定的内存区域在客户端程序运行时不被修改或使用。这样可以确保引导程序和其他关键组件在需要的情况下能够访问保留内存的特定部分,并保护关键数据结构免受意外修改。

62.3 结构块

结构块是设备树中描述设备树本身结构和内容的部分。它由一系列带有数据的令牌序列组成,这些令牌按照线性树结构进行组织。

(1)令牌类型
结构块中的令牌分为五种类型,每种类型用于不同的目的。
a. FDT_BEGIN_NODE (0x00000001): FDT_BEGIN_NODE 标记表示一个节点的开始。它后面跟着节点的单元名称作为额外数据。节点名称以以空字符结尾的字符串形式存储,并且可以包括单元地址。节点名称后可能需要填充零字节以对齐,然后是下一个标记,可以是除了FDT_END之外的任何标记。

b. FDT_END_NODE (0x00000002): FDT_END_NODE 标记表示一个节点的结束。该标记没有额外的数据,紧随其后的是下一个标记,可以是除了FDT_PROP 之外的任何标记。

c. FDT_PROP (0x00000003): FDT_PROP 标记表示设备树中属性的开始。它后面跟着描述属性的额外数据,该数据首先由属性的长度和名称组成,表示为以下C 结构

struct {
    uint32_t len;
    uint32_t nameoff;
}

长度表示属性值的字节长度,名称偏移量指向字符串块中存储属性名称的位置。在这个结构之后,属性的值作为字节字符串给出。属性值后可能需要填充零字节以对齐,然后是下一个令牌,可以是除了FDT_END 之外的任何标记。

d. FDT_NOP (0x00000004): FDT_NOP 令牌可以被解析设备树的程序忽略。该令牌没有额外的数据,紧随其后的是下一个令牌,可以是任何有效的令牌。使用FDT_NOP 令牌可以覆盖树中的属性或节点定义,从而将其从树中删除,而无需移动设备树blob 中的其他部分。

e. FDT_END (0x00000009): FDT_END 标记表示结构块的结束。应该只有一个FDT_END 标记,并且应该是结构块中的最后一个标记。该标记没有额外的数据,紧随其后的字节应该位于结构块的开头偏移处,该偏移等于设备树blob 标头中的size_dt_struct 字段的值。

(2)树状结构:

设备树的结构以线性树的形式表示。每个节点由FDT_BEGIN_NODE 标记开始,由FDT_END_NODE 标记结束。节点的属性和子节点在FDT_END_NODE 之前表示,因此子节点的FDT_BEGIN_NODE 和FDT_END_NODE 令牌嵌套在父节点的令牌中。

(3)结构块的结束

结构块以单个FDT_END 标记结束。该标记没有额外的数据,它位于结构块的末尾,并且是结构块中的最后一个标记。FDT_END 标记之后的字节应位于结构块的开头偏移处,该偏移等于设备树blob 标头中的size_dt_struct 字段的值。

最后对结构块开头的部分内容进行讲解,具体内容如下(图62-7)所示:

image-20240827144816961

image-20240827144843447

通过使用结构块,设备树可以以一种层次化的方式组织和描述系统中的设备和资源。每个节点可以包含属性和子节点,从而实现更加灵活和可扩展的设备树表示。

62.4 字符串块

字符串块用于存储设备树中使用的所有属性名称。它由一系列以空字符结尾的字符串组成,这些字符串在字符串块中简单地连接在一起,具体示例如下(图62-8)所示:

image-20240827144919021

(1)字符串连接

字符串块中的字符串以空字符(\0)作为终止符来连接。这意味着每个字符串都以空字符结尾,并且下一个字符串紧跟在上一个字符串的末尾。这种连接方式使得字符串块中的所有字符串形成一个连续的字符序列。

(2)偏移量引用

在结构块中,属性的名称是通过偏移量来引用字符串块中的相应字符串的。偏移量是一个无符号整数值,它表示字符串在字符串块中的位置。通过使用偏移量引用,设备树可以节省空间,并且在属性名称发生变化时也更加灵活,因为只需要更新偏移量,而不需要修改结构块中的属性引用。

(3)对齐约束:

字符串块没有对齐约束,这意味着它可以出现在设备树blob 的任何偏移处。这使得字符串块的位置在设备树blob 中是灵活的,并且可以根据需要进行调整,而不会对设备树的解析和处理造成影响。

字符串块是设备树中用于存储属性名称的部分。它由字符串连接而成,并通过偏移量在结构块中进行引用。字符串块的灵活位置使得设备树的表示更加紧凑和可扩展。

第63 章dtb 展开成device_node 实验

在上个小节中我们讲解了设备树deb 的文件格式,那deb 文件是怎样传递给内核的呢,那就进入到本小节的学习吧。

63.1 dtb 展开流程

dtb 展开流程图如下(图63-1)所示:

image-20240827145012209

接下来将会根据上图对deb 的展开流程进行详细的讲解:

  • (1)设备树源文件编写:根据之前的章节中讲解的设备树的基本语法和相关知识编写符合规范的设备树。
  • (2)设备树编译:设备树源文件经过设备树编译器(dtc)进行编译,生成设备树二进制文件(.dtb)。设备树编译器会检查源文件的语法和语义,并将其转换为二进制格式,以便内核能够解析和使用。
  • (3)boot.img 镜像生成boot.img 是一个包含内核镜像、设备树二进制文件和其他一些资源文件的镜像文件目前只是适用于瑞芯微的soc 上,其他厂商的soc 需要具体问题具体分析)。在生成boot.img 时,通常会将内核镜像、设备树二进制文件和其他一些资源文件打包在一起。这个过程可以使用特定的工具或脚本完成。
  • (4)U-Boot 加载:U-Boot(Universal Bootloader)是一种常用的开源引导加载程序,用于引导嵌入式系统。在系统启动过程中,U-Boot 会将boot.img 中的内核和设备树的二进制文件加载到系统内存的特定地址。
  • (5)内核初始化:U-Boot 将内核和设备树的二进制文件加载到系统内存的特定地址后,控制权会转交给内核。在内核初始化的过程中,会解析设备树二进制文件,将其展开为内核可以识别的数据结构,以便内核能够正确地初始化和管理硬件资源。
  • (6)设备树展开:设备树展开是指将设备树二进制文件解析成内核中的设备节点(device_node)的过程。内核会读取设备树二进制文件的内容,并根据设备树的描述信息,构建设备树数据结构,例如设备节点、中断控制器、寄存器、时钟等。这些设备树数据结构将在内核运行时用于管理和配置硬件资源。

而本章节要讲解的重点就在上面的第6 步“设备树的展开”,最终设备树二进制文件会被解析成device_node,device_node 结构体定义在内核源码的“/include/linux/of.h”文件中,具体内容如下所示:

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; // 已删除的属性列表
    struct device_node *parent; // 父设备节点指针
    struct device_node *child; // 子设备节点指针
    struct device_node *sibling; // 兄弟设备节点指针
    
    #if defined(CONFIG_OF_KOBJ)
        struct kobject kobj; // 内核对象(用于sysfs)
    #endif
    
    unsigned long _flags; // 设备节点的标志位
    void *data; // 与设备节点相关的数据指针
    
    #if defined(CONFIG_SPARC)
        const char *path_component_name; // 设备节点的路径组件名称
        unsigned int unique_id; // 设备节点的唯一标识
        struct of_irq_controller *irq_trans; // 设备节点的中断控制器
    #endif
};

然后对该结构体的重要参数进行讲解:

  • (1)**name**:name 字段表示设备节点的名称。设备节点的名称是在设备树中唯一标识该节点的字符串。它通常用于在设备树中引用设备节点。
  • (2)**type**:type 字段表示设备节点的类型。设备节点的类型提供了关于设备节点功能和所属设备类别的信息。它可以用于识别设备节点的用途和特性。
  • (3)**properties**:properties 字段是指向设备节点属性列表的指针。设备节点的属性包含了与设备节点相关联的配置和参数信息。属性以键值对的形式存在,可以提供设备的特定属性、寄存器地址、中断信息等。property 字段同样定义在内核源码的“/include/linux/of.h”文件中,

具体内容如下所示:

struct property {
    char *name; // 属性的名称
    int length; // 属性值的长度(字节数)
    void *value; // 属性值的指针
    struct property *next; // 下一个属性节点指针
    
    #if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
        unsigned long _flags; // 属性的标志位
    #endif
    
    #if defined(CONFIG_OF_PROMTREE)
        unsigned int unique_id; // 属性的唯一标识
    #endif
    
    #if defined(CONFIG_OF_KOBJ)
        struct bin_attribute attr; // 内核对象二进制属性
    #endif
};
  • (4)**parent**(父设备节点): parent 字段指向当前设备节点的父设备节点。设备树中的设备节点按照层次结构组织,每个设备节点都有一个直接上级(父设备节点)。通过parent 字段,可以在设备树中向上遍历设备节点的父节点。
  • (5)**child**(子设备节点): child 字段指向当前设备节点的第一个子设备节点。一个设备节点可以拥有多个子设备节点,在设备树中表示它们是当前设备节点的直接下级。通过child字段获取到第一个子设备节点后,可以使用sibling 字段遍历它们的兄弟节点,从而遍历所有的子节点。
  • (6)**sibling**(兄弟设备节点): sibling 字段指向当前设备节点在设备树中的下一个兄弟设备节点。兄弟设备节点是指在设备树中处于同一级别的设备节点,它们共享相同的父设备节点。通过sibling 字段,可以在同级设备节点之间进行遍历,获取它们的信息或进行操作。

至此,关于device_node 的结构体讲解就完成了,虽然我们现在知道了,dtb 文件最终会展开成device_node 这一可以让内核识别的格式,那更具体的实现流程是怎样的呢,让我们进入下一小节的学习吧。

63.2 dtb 解析过程源码分析

首先来到源码目录下的“/init/main.c”文件,找到其中的start_kernel 函数,start_kernel 函数是Linux 内核启动的入口点,它是Linux 内核的核心函数之一,负责完成内核的初始化和启动过程,具体内容如下所示:

asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;
    set_task_stack_end_magic(&init_task); // 设置任务栈的魔数
    smp_setup_processor_id(); // 设置处理器ID
    debug_objects_early_init(); // 初始化调试对象
    cgroup_init_early(); // 初始化cgroup(控制组)
    local_irq_disable(); // 禁用本地中断
    early_boot_irqs_disabled = true; // 标记早期引导期间中断已禁用
    /*
    * 中断仍然被禁用。进行必要的设置,然后启用它们。
    */
    boot_cpu_init(); // 初始化引导CPU
    page_address_init(); // 设置页地址
    pr_notice("%s", linux_banner); // 打印Linux 内核版本信息
    setup_arch(&command_line); // 架构相关的初始化
    mm_init_cpumask(&init_mm); // 初始化内存管理的cpumask(CPU 掩码)
    setup_command_line(command_line); // 设置命令行参数
    setup_nr_cpu_ids(); // 设置CPU 个数
    setup_per_cpu_areas(); // 设置每个CPU 的区域
    smp_prepare_boot_cpu(); // 准备启动CPU(架构特定的启动CPU 钩子)
    boot_cpu_hotplug_init(); // 初始化热插拔的引导CPU
    build_all_zonelists(NULL); // 构建所有内存区域列表
    page_alloc_init(); // 初始化页面分配器
    ........
}

其中跟设备树相关的函数为第20 行的setup_arch(&command_line);该函数定义在内核源码的“/arch/arm64/kernel/setup.c”文件中,具体内容如下所示:

void __init setup_arch(char **cmdline_p)
{
    init_mm.start_code = (unsigned long) _text;
    init_mm.end_code = (unsigned long) _etext;
    init_mm.end_data = (unsigned long) _edata;
    init_mm.brk = (unsigned long) _end;
    *cmdline_p = boot_command_line;
    
    early_fixmap_init(); // 初始化early fixmap
    early_ioremap_init(); // 初始化early ioremap
    
    setup_machine_fdt(__fdt_pointer); // 设置机器的FDT(平台设备树)
    
    // 初始化静态密钥,早期可能会被cpufeature 代码和早期参数启用
    jump_label_init();
    parse_early_param();
    
    // 在启动可能的早期控制台后,解除屏蔽异步中断和FIQ(一旦我们可以报告发生的系统错误)
    local_daif_restore(DAIF_PROCCTX_NOIRQ);
    
    // 在这个阶段,TTBR0 仅用于身份映射。将其指向零页面,以避免做出猜测性的新条目获取。
    cpu_uninstall_idmap();
    
    xen_early_init(); // Xen 平台的早期初始化
    efi_init(); // EFI 平台的初始化
    arm64_memblock_init(); // ARM64 内存块的初始化
    
    paging_init(); // 分页初始化
    
    acpi_table_upgrade(); // ACPI 表的升级
    
    // 解析ACPI 表以进行可能的引导时配置
    acpi_boot_table_init();
    
    if (acpi_disabled)
        unflatten_device_tree(); // 展开设备树
    bootmem_init(); // 引导内存的初始化
    ............
}

在setup_arch 函数中与设备树相关的函数分别为第13 行的setup_machine_fdt(__fdt_pointer)和第37 行的unflatten_device_tree(),接下来将对上述两个函数进行详细的介绍

63.2.1 setup_machine_fdt(__fdt_pointer)

setup_machine_fdt(__fdt_pointer)中的__fdt_pointer 是dtb 二进制文件加载到内存的地址,该地址由bootloader 启动kernel 时透过x0 寄存器传递过来的,具体的汇编代码在内核源码目录下的“/arch/arm64/kernel/head.S”文件中,具体内容如下所示:

preserve_boot_args:
    mov x21, x0 // x21=FDT
        
__primary_switched:
    str_l x21, __fdt_pointer, x5 // Save FDT pointer
  • 第2 行: 将寄存器x0 的值复制到寄存器x21。x0 寄存器中保存了一个指针,该指针指向设备树(Device Tree)。
  • 第4 行: 将寄存器x21 的值存储到内存地址__fdt_pointer 中。

然后来看setup_machine_fdt 函数,该函数定义在内核源码的“/arch/arm64/kernel/setup.c”文件中,具体内容如下所示:

// 初始化设置机器的设备树
static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
    int size;
    // 将设备树物理地址映射到内核虚拟地址空间
    void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
    const char *name;
    
    // 如果映射成功
    if (dt_virt) {
        // 保留设备树占用的内存区域
        memblock_reserve(dt_phys, size);
    }
    
    // 如果设备树映射失败或者设备树解析失败
    if (!dt_virt || !early_init_dt_scan(dt_virt)) {
        // 输出错误信息
        pr_crit("\n"
        "Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
        "The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
        "\nPlease check your bootloader.",
        &dt_phys, dt_virt);
        
        // 无限循环,等待系统崩溃
        while (true)
        cpu_relax();
    }
    
    // 早期修复完成,将设备树映射为只读模式
    fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);
    
    // 获取设备树的机器名
    name = of_flat_dt_get_machine_name();
    
    // 如果设备树没有机器名,则返回
    if (!name)
        return;
        pr_info("Machine model: %s\n", name); // 输出机器型号信息
        dump_stack_set_arch_desc("%s (DT)", name); // 设置栈转储的架构描述为机器型号
}

此函数用于在内核启动过程中设置机器的设备树。在此函数中,将执行以下步骤:

  • 1.使用fixmap_remap_fdt() 将设备树映射到内核虚拟地址空间中的fixmap 区域。
  • 2.如果映射成功,则使用memblock_reserve() 保留设备树占用的物理内存区域。
  • 3.检查设备树的有效性和完整性,通过调用early_init_dt_scan()进行早期扫描。如果设备树无效或扫描失败,则会输出错误信息并进入死循环。
  • 4.早期修复已完成,现在将设备树映射为只读,通过调用fixmap_remap_fdt() 实现。
  • 5.获取设备树中的机器模型名称,通过调用of_flat_dt_get_machine_name()。
  • 6.如果机器模型名称存在,则输出机器模型的信息,并通过dump_stack_set_arch_desc()设置堆栈描述信息。

其中上面的第3 步调用的early_init_dt_scan() 需要详细的讲解一下,该函数定义在内核源码的“drivers/of/fdt.c”目录下,具体内容如下所示:

bool __init early_init_dt_scan(void *params)
{
    bool status;
    
    // 验证设备树的兼容性和完整性
    status = early_init_dt_verify(params);
    if (!status)
        return false;
    
    // 扫描设备树节点
    early_init_dt_scan_nodes();
    return true;
}

首先,调用early_init_dt_verify() 函数对设备树进行兼容性和完整性验证。该函数可能会检查设备树中的一致性标记、版本信息以及必需的节点和属性是否存在。如果验证失败,函数会返回false。该函数的具体内容如下所示:

bool __init early_init_dt_verify(void *params)
{
    // 验证传入的参数是否为空
    if (!params)
        return false;
    
    // 检查设备树头部的有效性
    // 如果设备树头部无效,返回false
    if (fdt_check_header(params))
        return false;
    
    // 设置指向设备树的指针为传入的参数
    initial_boot_params = params;
    
    // 计算设备树的CRC32 校验值
    // 并将结果保存在全局变量of_fdt_crc32 中
    of_fdt_crc32 = crc32_be(~0, initial_boot_params, fdt_totalsize(initial_boot_params));
    
    // 返回true,表示设备树验证和初始化成功
    return true;
}

第4 行:该进行参数的有效性检查,如果params 为空,则直接返回false,表示参数无效。
第9 行:检查设备树头部的有效性。fdt_check_header 是一个用于检查设备树头部的函数,如果设备树头部无效,则返回false,表示设备树不合法。
第13 行:如果设备树头部有效,程序继续执行,将传入的params 赋值给全局变量initial_boot_params,用来保存设备树的指针。
第17 行,使用crc32_be 函数计算设备树的CRC32 校验值,其中crc32_be 是一个用于计算CRC32 校验值的函数,~0 表示初始值为全1 的位模式。计算完成后,将结果保存在全局变量of_fdt_crc32 中。

然后继续回到early_init_dt_scan() 函数中,如果设备树验证成功(即status 为真),则调用early_init_dt_scan_nodes() 函数。这个函数的作用是扫描设备树的节点并进行相应的处理,该函数的具体内容如下所示:

void __init early_init_dt_scan_nodes(void)
{
    /* 从/chosen 节点中检索各种信息*/
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
    
    /* 初始化{size,address}-cells 信息*/
    of_scan_flat_dt(early_init_dt_scan_root, NULL);
    
    /* 设置内存信息,调用early_init_dt_add_memory_arch 函数*/
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

函数early_init_dt_scan_nodes 被声明为__init,这表示它是在内核初始化阶段被调用,并且在初始化完成后不再需要。该函数的目的是在早期阶段扫描设备树节点,并执行一些初始化操作。

函数中主要调用了of_scan_flat_dt 函数,该函数用于扫描平面设备树(flat device tree)。平面设备树是一种将设备树以紧凑形式表示的数据结构,它不使用树状结构,而是使用线性结构,以节省内存空间。

具体来看,early_init_dt_scan_nodes 函数的执行步骤如下:
(1)**of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line)**:从设备树的/chosen节点中检索各种信息。/chosen 节点通常包含了一些系统的全局配置参数,比如命令行参数。early_init_dt_scan_chosen 是一个回调函数,用于处理/chosen 节点的信息。boot_command_line是一个参数,表示内核启动时的命令行参数。
( 2 ) of_scan_flat_dt(early_init_dt_scan_root, NULL) : 初始化{size,address}-cells 信息。
{size,address}-cells 描述了设备节点中地址和大小的编码方式。early_init_dt_scan_root 是一个回调函数,用于处理设备树的根节点。
( 3 ) of_scan_flat_dt(early_init_dt_scan_memory, NULL) : 设置内存信息, 并调用early_init_dt_add_memory_arch 函数。这个步骤主要用于在设备树中获取内存的相关信息,并将其传递给内核的内存管理模块。early_init_dt_scan_memory 是一个回调函数,用于处理内存信息。

至此,关于setup_machine_fdt(__fdt_pointer)代码的分析就完成了。

63.2.2 unflatten_device_tree

该函数用于解析设备树,将紧凑的设备树数据结构转换为树状结构的设备树,该函数定义在内核源码目录下的“/drivers/of/fdt.c”文件中,具体内容如下所示:

void __init unflatten_device_tree(void)
{
/* 解析设备树*/
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);
/* 获取指向"/chosen" 和"/aliases" 节点的指针,以供全局使用*/
of_alias_scan(early_init_dt_alloc_memory_arch);
/* 运行设备树的单元测试*/
unittest_unflatten_overlay_base();
}

该函数主要用于解析设备树,并将解析后的设备树存储在全局变量of_root 中。函数首先调用__unflatten_device_tree 函数来执行设备树的解析操作。解析后的设备树将使用of_root 指针进行存储。

接下来,函数调用of_alias_scan 函数。这个函数用于扫描设备树中的/chosen 和/aliases 节点,并为它们分配内存。这样,其他部分的代码可以通过全局变量访问这些节点。

最后,函数调用unittest_unflatten_overlay_base 函数,用于运行设备树的单元测试。然后对__unflatten_device_tree 这一设备树的解析函数进行详细的介绍,该函数的具体内容如下所示:

void *__unflatten_device_tree(const void *blob,struct device_node *dad,struct device_node **mynodes,
                              void *(*dt_alloc)(u64 size, u64 align),bool detached)
{
    int size;
    void *mem;
    
    pr_debug(" -> unflatten_device_tree()\n");
    
    if (!blob) {
        pr_debug("No device tree pointer\n");
        return NULL;
    }
    pr_debug("Unflattening device tree:\n");
    pr_debug("magic: %08x\n", fdt_magic(blob));
    pr_debug("size: %08x\n", fdt_totalsize(blob));
    pr_debug("version: %08x\n", fdt_version(blob));
    
    if (fdt_check_header(blob)) {
        pr_err("Invalid device tree blob header\n");
        return NULL;
    }
    
    /* 第一遍扫描,计算大小*/
    size = unflatten_dt_nodes(blob, NULL, dad, NULL);
    if (size < 0)
        return NULL;
    
    size = ALIGN(size, 4);
    pr_debug(" 大小为%d,正在分配内存...\n", size);
    
    /* 为展开的设备树分配内存*/
    mem = dt_alloc(size + 4, alignof(struct device_node));
    if (!mem)
        return NULL;
    
    memset(mem, 0, size);
    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);
    pr_debug(" 正在展开%p...\n", mem);
    
    /* 第二遍扫描,实际展开设备树*/
    unflatten_dt_nodes(blob, mem, dad, mynodes);
    if (be32_to_cpup(mem + size) != 0xdeadbeef)
        pr_warning("End of tree marker overwritten: %08x\n", be32_to_cpup(mem + size));
    
    if (detached && mynodes) {
        of_node_set_flag(*mynodes, OF_DETACHED);
        pr_debug("unflattened tree is detached\n");
    }
    pr_debug(" <- unflatten_device_tree()\n");
    return mem;
}

该函数的重点在两次设备树的扫描上,第一遍扫描的目的是计算展开设备树所需的内存大小。

第29 行:unflatten_dt_nodes 函数的作用是递归地遍历设备树数据块,并计算展开设备树所需的内存大小。它接受四个参数:blob(设备树数据块指针)、start(当前节点的起始地址,初始为NULL)、dad(父节点指针)和mynodes(用于存储节点指针数组的指针,初始为NULL)。第一遍扫描完成后,unflatten_dt_nodes 函数会返回展开设备树所需的内存大小,然后在对大小进行对齐操作,并为展开的设备树分配内存。

第二遍扫描的目的是实际展开设备树,并填充设备节点的名称、类型和属性等信息。

第49 行:再次调用了unflatten_dt_nodes 函数进行第二遍扫描。通过这样的过程,第二遍扫描会将设备树数据块中的节点展开为真正的设备节点,并填充节点的名称、类型和属性等信息。这样就完成了设备树的展开过程。

最后我们来对unflatten_dt_nodes 函数内容进行一下深究,unflatten_dt_nodes 函数具体定义如下所示:

static int unflatten_dt_nodes(const void *blob, void *mem, struct device_node *dad,
                              struct device_node **nodepp)
{
    struct device_node *root; // 根节点
    int offset = 0, depth = 0, initial_depth = 0; // 偏移量、深度和初始深度
    #define FDT_MAX_DEPTH64 // 最大深度
    struct device_node *nps[FDT_MAX_DEPTH]; // 设备节点数组
    void *base = mem; // 基地址,用于计算偏移量
    bool dryrun = !base; // 是否只是模拟运行,不实际处理
    
    if (nodepp)
        *nodepp = NULL; // 如果指针不为空,将其置为空指针
    
    /*
    * 如果@dad 有效,则表示正在展开设备子树。
    * 在第一层深度可能有多个节点。
    * 将@depth 设置为1,以使fdt_next_node() 正常工作。
    * 当发现负的@depth 时,该函数会立即退出。
    * 否则,除第一个节点外的设备节点将无法成功展开。
    */
    if (dad)
        depth = initial_depth = 1;
    
    root = dad; // 根节点为@dad
    nps[depth] = dad; // 将根节点放入设备节点数组
    
    for (offset = 0; offset >= 0 && depth >= initial_depth;
         offset = fdt_next_node(blob, offset, &depth)) {
        if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH))
        	continue;
        
        // 如果未启用CONFIG_OF_KOBJ 并且节点不可用,则跳过该节点
        if (!IS_ENABLED(CONFIG_OF_KOBJ) &&!of_fdt_device_is_available(blob, offset))
        	continue;
        
        // 填充节点信息,并将子节点添加到设备节点数组
        if (!populate_node(blob, offset, &mem, nps[depth],&nps[depth+1], dryrun))
            return mem - base;
        
        if (!dryrun && nodepp && !*nodepp)
            *nodepp = nps[depth+1]; // 将子节点指针赋值给@nodepp
        
        if (!dryrun && !root)
            root = nps[depth+1]; // 如果根节点为空,则将子节点设置为根节点
    }
    if (offset < 0 && offset != -FDT_ERR_NOTFOUND) {
        pr_err("Error %d processing FDT\n", offset);
        return -EINVAL;
    }
    
    // 反转子节点列表。一些驱动程序假设节点顺序与.dts 文件中的节点顺序一致
    if (!dryrun)
        reverse_nodes(root);
    
    return mem - base; // 返回处理的字节数
}

unflatten_dt_nodes 函数的作用我们在上面已经讲解过了,这里重点介绍第31 行的fdt_next_node()函数和第41 行的populate_node 函数。
fdt_next_node() 函数用来遍历设备树的节点。从偏移量为0 开始,只要偏移量大于等于0且深度大于等于初始深度,就执行循环。循环中的每次迭代都会处理一个设备树节点。

在每次迭代中,首先检查深度是否超过了最大深度FDT_MAX_DEPTH,如果超过了,则跳过该节点。

如果未启用CONFIG_OF_KOBJ 并且节点不可用(通过of_fdt_device_is_available() 函数判断),则跳过该节点。
随后调用populate_node() 函数填充节点信息, 并将子节点添加到设备节点数组nps 中。populate_node() 函数定义如下所示:

static bool populate_node(const void *blob, int offset,void **mem, struct device_node *dad,
                          struct device_node **pnp, bool dryrun)
{
    struct device_node *np; // 设备节点指针
    const char *pathp; // 节点路径字符串指针
    unsigned int l, allocl; // 路径字符串长度和分配的内存大小
    
    pathp = fdt_get_name(blob, offset, &l); // 获取节点路径和长度    
    if (!pathp) {
        *pnp = NULL;
        return false;
    }
    allocl = ++l; // 分配内存大小为路径长度加一,用于存储节点路径字符串
    
    np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
                            __alignof__(struct device_node)); // 分配设备节点内存
    
    if (!dryrun) {
        char *fn;
        of_node_init(np); // 初始化设备节点
        np->full_name = fn = ((char *)np) + sizeof(*np); // 设置设备节点的完整路径名
        memcpy(fn, pathp, l); // 将节点路径字符串复制到设备节点的完整路径名中
        if (dad != NULL) {
            np->parent = dad; // 设置设备节点的父节点
            np->sibling = dad->child; // 设置设备节点的兄弟节点
            dad->child = np; // 将设备节点添加为父节点的子节点
        }
    }
    
    populate_properties(blob, offset, mem, np, pathp, dryrun); // 填充设备节点的属性信息
    if (!dryrun) {
        np->name = of_get_property(np, "name", NULL); // 获取设备节点的名称属性
        np->type = of_get_property(np, "device_type", NULL); // 获取设备节点的设备类型属性
        if (!np->name)
            np->name = "<NULL>"; // 如果设备节点没有名称属性,则设置为"<NULL>"
        if (!np->type)
            np->type = "<NULL>"; // 如果设备节点没有设备类型属性,则设置为"<NULL>"
    }
    
    *pnp = np; // 将设备节点指针赋值给*pnp
    return true;
}

populate_node 函数中首先会调用第18 行的unflatten_dt_alloc 函数分配设备节点内存。分配的内存大小为sizeof(struct device_node) + allocl 字节, 并使用__alignof__(structdevice_node) 对齐。然后调用populate_properties 函数填充设备节点的属性信息。该函数会解析设备节点的属性,并根据需要分配内存来存储属性值。

至此,关于dtb 二进制文件的解析过程就讲解完成了,完整的源码分析流程图如下(图63-2)所示:

image-20240827152705940

第64 章device_node 转换成platform_device 实验

在上一章中,我们学习了dtb 二进制文件展开成device_node 的具体流程,而device_node这时候还并不能跟内核中的platform_driver 进行对接,而为了让操作系统能够识别和管理设备,需要将设备节点转换为平台设备。

64.1 转换规则

在之前学习的平台总线模型中,device 部分是用platform_device 结构体来描述硬件资源的,所以内核最终会将内核认识的device_node 树转换platform_ device,但是并不是所有的device_node 都会被转换成platform_ device,只有满足要求的才会转换成platform_ device,转换成platform_device 的节点可以在/sys/bus/platform/devices 下查看,那device_node 节点要满足什么要求才会被转换成platform_device 呢?

  • 根据规则1,首先遍历根节点下包含compatible 属性的子节点,对于每个子节点,创建一个对应的platform_device
  • 根据规则2,遍历包含compatible 属性为”simple-bus“、”simple-mfd“ 或”isa“ 的节点以及它们的子节点。如果子节点包含compatible 属性值则会创建一个对应的platform_device
  • 根据规则3,检查节点的compatible 属性是否包含”arm“ 或”primecell“。如果是,则不将该节点转换为platform_device,而是将其识别为AMBA 设备。

接下来将通过几个设备树示例对上述规则进行实践。

举例1:

/dts-v1/;
/ {
    model = "This is my devicetree!";
    #address-cells = <1>;
    #size-cells = <1>;
    chosen {
        bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
    };
    
    cpu1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a35", "arm,armv8";
        reg = <0x0 0x1>;
    };
    
    aliases {
        led1 = "/gpio@22020101";
    };
    
    node1 {
        #address-cells = <1>;
        #size-cells = <1>;
        gpio@22020102 {
            reg = <0x20220102 0x40>;
        };
    };
    
    node2 {
        node1-child {
            pinnum = <01234>;
        };
    };
    
    gpio@22020101 {
        compatible = "led";
        reg = <0x20220101 0x40>;
        status = "okay";
    };
};

在上面的设备树中,总共有chosencpu1: cpu@1aliasesnode1node2gpio@22020101这六个节点,其中前五个节点都没有compatible 属性,所以并不会被转换为platform_device,而最后一个gpio@22020101 节点符合规则一,在根节点下,且有compatible 属性,所以最后会转换为platform_device

举例2:

/dts-v1/;
/ {
    model = "This is my devicetree!";
    #address-cells = <1>;
    #size-cells = <1>;
    
    chosen {
        bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
    };
    
    cpu1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a35", "arm,armv8";
        reg = <0x0 0x1>;
    };
    
    aliases {
        led1 = "/gpio@22020101";
    };
    
    node1 {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        gpio@22020102 {
            reg = <0x20220102 0x40>;
        };
    };
    
    node2 {
        node1-child {
            pinnum = <01234>;
        };
    };
    
    gpio@22020101 {
        compatible = "led";
        reg = <0x20220101 0x40>;
        status = "okay";
    };
};

相较于示例1 的设备树,这里在node1 节点中添加了compatible 属性,但是这个compatible 属性值为simple-bus,我们需要继续看他的子节点,子节点gpio@22020102 并没有compatible 属性值,所以这里的node1 节点不会被转换。

举例3:

/dts-v1/;
/ {
    model = "This is my devicetree!";
    #address-cells = <1>;
    #size-cells = <1>;
    chosen {
    	bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
    };
    cpu1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a35", "arm,armv8";
        reg = <0x0 0x1>;
    };
    
    aliases {
        led1 = "/gpio@22020101";
    };
    
    node1 {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        gpio@22020102 {
            compatible = "gpio";
            reg = <0x20220102 0x40>;
        };
    };
    
    node2 {
        node1-child {
            pinnum = <01234>;
    	};
    };
    
    gpio@22020101 {
        compatible = "led";
        reg = <0x20220101 0x40>;
        status = "okay";
    };
};

相较于示例2 的设备树,这里在node1 节点的子节点gpio@22020102 中添加了compatible 属性,node1 节点的compatible 属性值为simple-bus,然后需要继续看他的子节点,子节点gpio@22020102compatible 属性值为gpio,所以这里的gpio@22020102 节点会被转换成platform_device

举例4:

/dts-v1/;
/ {
    model = "This is my devicetree!";
    #address-cells = <1>;
    #size-cells = <1>;
    
    chosen {
        bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
    };
    
    cpul: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a35", "arm,armv8";
        reg = <0x0 0x1>;
        amba {
            compatible = "simple-bus";
            #address-cells = <2>;
            #size-cells = <2>;
            ranges;
            dmac_peri: dma-controller@ff250000 {
                compatible = "arm,p1330", "arm,primecell";
                reg = <0x0 0xff250000 0x0 0x4000>;
                interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>,
                <GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
                #dma-cells = <1>;
                arm,pl330-broken-no-flushp;
                arm,p1330-periph-burst;
                clocks = <&cru ACLK DMAC_PERI>;
                clock-names = "apb_pclk";
            };
            dmac_bus: dma-controller@ff600000 {
                compatible = "arm,p1330", "arm,primecell";
                reg = <0x0 0xff600000 0x0 0x4000>;
                interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
                <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
                #dma-cells = <1>;
                arm,pl330-broken-no-flushp;
                arm,pl330-periph-burst;
                clocks = <&cru ACLK_DMAC_BUS>;
                clock-names = "apb_pclk";
            };
        };
    };
};

amba 节点的compatible 值为simple-bus,不会被转换为platform_device,而是作为父节点用于组织其他设备,所以需要来查看他的子节点。

dmac_peri: dma-controller@ff250000 节点: 该节点的compatible 属性包含”arm,p1330“和”arm,primecell“,根据规则3,该节点不会被转换为platform_device,而是被识别为AMBA设备。

dmac_bus: dma-controller@ff600000 节点: 该节点的compatible 属性包含”arm,p1330” 和”arm,primecell”,根据规则3,该节点不会被转换为platform_device,而是被识别为AMBA 设备。

64.2 转换流程源码分析

首先进入到内核源码目录下的“drivers/of/platform.c”文件中,找到第555 行,具体内容如下所示:

arch_initcall_sync(of_platform_default_populate_init);

arch_initcall_sync 是Linux 内核中的一个函数,用于在内核初始化过程中执行架构相关的初始化函数。它属于内核的初始化调用机制,用于确保在系统启动过程中适时地调用特定架构的初始化函数。

在Linux 内核的初始化过程中,各个子系统和架构会注册自己的初始化函数。这些初始化函数负责完成特定子系统或架构相关的初始化工作,例如初始化硬件设备、注册中断处理程序、设置内存映射等。而arch_initcall_sync 函数则用于调用与当前架构相关的初始化函数。

当内核启动时, 调用rest_init() 函数来启动初始化过程。在初始化过程中,arch_initcall_sync 函数会被调用,以确保所有与当前架构相关的初始化函数按照正确的顺序执行。这样可以保证在启动过程中,特定架构相关的初始化工作得到正确地完成。

of_platform_default_populate_init 函数的作用是在内核初始化过程中自动解析设备树,并根据设备树中的设备节点创建对应的platform_device 结构。它会遍历设备树中的设备节点,并为每个设备节点创建一个对应的platform_device 结构,然后将其注册到内核中,使得设备驱动程序能够识别和操作这些设备。该函数的具体内容如下所示:

  • ```c
    static int __init of_platform_default_populate_init(void)
    {
    struct device_node node;
    // 暂停设备链接供应商同步状态
    device_links_supplier_sync_state_pause();

    // 如果设备树尚未填充,则返回错误码
    if (!of_have_populated_dt())
    return -ENODEV;
    /

    * 显式处理某些兼容性,因为我们不想为/reserved-memory 中的每个具有“compatible”的节点创建
    platform_device。
    */
    for_each_matching_node(node, reserved_mem_matches)
    of_platform_device_create(node, NULL, NULL);

    // 查找节点”/firmware”
    node = of_find_node_by_path(“/firmware”);
    if (node) {
    // 使用该节点进行设备树平台设备的填充
    of_platform_populate(node, NULL, NULL, NULL);
    of_node_put(node);
    }

    // 填充其他设备
    fw_devlink_pause();
    of_platform_default_populate(NULL, NULL, NULL);
    fw_devlink_resume();

    return 0;
    }
    
    - 第6 行:暂停设备链接供应商的同步状态,确保设备链接的状态不会在此过程中被改变。
    - 第9 行:检查设备树是否已经被填充。如果设备树尚未填充,则返回错误码-ENODEV。
    - 第16 行:遍历设备树中与reserved_mem_matches 匹配的节点。这些节点是/reserved-memory 中具有"compatible" 属性的节点。
    - 第17 行:为/reserved-memory 中匹配的节点创建platform_device 结构。这些节点不会为每个节点都创建platform_device,而是根据需要进行显式处理。
    - 第20 行:在设备树中查找路径为"/firmware" 的节点。
    - 第23 行:使用找到的节点填充设备树中的平台设备。这些节点可能包含与固件相关的设备。
    - 第28 行:暂停固件设备链接,确保在填充其他设备时链接状态不会改变。
    - 第29 行:填充设备树中的其他设备。
    - 第30 行:恢复固件设备链接。
    - 上诉内容中我们要着重关注的是第29 行的`of_platform_default_populate(NULL, NULL, NULL)`函数,找到该函数的定义之后如下所示:
    
    ```c
    int of_platform_default_populate(struct device_node *root,const struct of_dev_auxdata *lookup,
                                     struct device *parent)
    {
        return of_platform_populate(root, of_default_bus_match_table, lookup, parent);
    }

该函数的作用是调用of_platform_populate 函数来填充设备树中的平台设备,并使用默认的设备匹配表of_default_bus_match_table,设备匹配表内容如下所示:

const struct of_device_id of_default_bus_match_table[] = {
    { .compatible = "simple-bus", },
    { .compatible = "simple-mfd", },
    { .compatible = "isa", },
    
    #ifdef CONFIG_ARM_AMBA
        { .compatible = "arm,amba-bus", },
    #endif /* CONFIG_ARM_AMBA */
    
    {} /* Empty terminated list */
};

上述的设备匹配表就是我们在第一小节中第2 条规则,,函数将自动根据设备树节点的属性匹配相应的设备驱动程序,并填充内核的平台设备列表。接下来找到of_platform_populate函数的定义,该函数的具体内容如下所示:

int of_platform_populate(struct device_node *root,const struct of_device_id *matches,
                         const struct of_dev_auxdata *lookup,struct device *parent)
{
    struct device_node *child;
    int rc = 0;
    
    // 如果root 不为空,则增加root 节点的引用计数;否则,在设备树中根据路径查找root 节点
    root = root ? of_node_get(root) : of_find_node_by_path("/");
    if (!root)
        return -EINVAL;
    
    pr_debug("%s()\n", __func__);
    pr_debug(" starting at: %pOF\n", root);
    
    // 暂停设备链接供应商同步状态
    device_links_supplier_sync_state_pause();
    
    // 遍历root 节点的所有子节点
    for_each_child_of_node(root, child) {
        
        // 创建平台设备并添加到设备树总线
        rc = of_platform_bus_create(child, matches, lookup, parent, true);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    
    // 恢复设备链接供应商同步状态
    device_links_supplier_sync_state_resume();
    
    // 设置root 节点的OF_POPULATED_BUS 标志
    of_node_set_flag(root, OF_POPULATED_BUS);
    
    // 释放root 节点的引用计数
    of_node_put(root);
    return rc;
}

该函数的具体执行步骤如下:

  • 第10 行:检查给定的设备树节点node 是否为有效节点。如果节点为空,函数将立即返回。
  • 第21 行:遍历设备树节点的子节点,查找与平台设备相关的节点。这些节点通常具有compatible 属性,用于匹配设备驱动程序。
  • 第23 行:对于每个找到的平台设备节点,创建一个platform_device 结构,并根据设备树节点的属性设置该结构的各个字段。
  • 第25 行:将创建的platform_device 添加到内核的平台设备列表中,以便设备驱动程序能够识别和操作这些设备。

接下来对该函数的第23 行核心代码of_platform_bus_create(child, matches, lookup, parent,true)函数进行讲解,该函数的具体定义如下所示:

static int of_platform_bus_create(struct device_node *bus,const struct of_device_id *matches,
                         const struct of_dev_auxdata *lookup,struct device *parent, bool strict)
{
    const struct of_dev_auxdata *auxdata;
    struct device_node *child;
    struct platform_device *dev;
    const char *bus_id = NULL;
    void *platform_data = NULL;
    int rc = 0;
    
    /* 确保设备节点具有compatible 属性*/
    if (strict && (!of_get_property(bus, "compatible", NULL))) {
        pr_debug("%s() - skipping %pOF, no compatible prop\n",__func__, bus);
        return 0;
    }
    
    /* 跳过不想创建设备的节点*/
    if (unlikely(of_match_node(of_skipped_node_table, bus))) {
        pr_debug("%s() - skipping %pOF node\n", __func__, bus);
        return 0;
    }
    
    if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
        pr_debug("%s() - skipping %pOF, already populated\n", __func__, bus);
        return 0;
    }
    
    auxdata = of_dev_lookup(lookup, bus);
    if (auxdata) {
        bus_id = auxdata->name;
        platform_data = auxdata->platform_data;
    }
    
    if (of_device_is_compatible(bus, "arm,primecell")) {
        /*
        * 在此处不返回错误以保持与旧设备树文件的兼容性。
        */
        of_amba_device_create(bus, bus_id, platform_data, parent);
        return 0;
    }
    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
    if (!dev || !of_match_node(matches, bus))
        return 0;
    for_each_child_of_node(bus, child) {
        pr_debug(" create child: %pOF\n", child);
        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    
    of_node_set_flag(bus, OF_POPULATED_BUS);
    return rc;
}
  • 第14 行:如果strict 为真且设备节点bus 没有兼容性属性,则输出调试信息并返回0。这个条件判断确保设备节点具有compatible 属性,因为compatible 属性用于匹配设备驱动程序,对应我们在上一小节的第1 条规则。
  • 第21 行:如果设备节点bus 在被跳过的节点表中,则输出调试信息并返回0。这个条件判断用于跳过不想创建设备的节点。
  • 第27 行:如果设备节点bus 的OF_POPULATED_BUS 标志已经设置,则输出调试信息并返回0。这个条件判断用于避免重复创建已经填充的设备节点。
  • 第34 行:使用lookup 辅助数据结构查找设备节点bus 的特定配置信息,并将其赋值给变量bus_idplatform_data。这个步骤用于获取设备节点的特定配置信息,以便在创建平台设备时使用,由于这里传入的参数为NULL,所以下面的条件判断并不会被执行。
  • 第39 行:如果设备节点bus 兼容于”arm,primecell“,则调用of_amba_device_create 函数创建AMBA 设备,并返回0,对应我们在上一小节学习的第3 条规则。
  • 第47 行:调用of_platform_device_create_pdata 函数创建平台设备,并将其赋值给变量dev。然后,检查设备节点bus 是否与给定的匹配表matches 匹配。如果平台设备创建失败或者设备节点不匹配,那么返回0。
  • 第51 行-第58 行:遍历设备节点bus 的每个子节点child,并递归调用of_platform_bus_create 函数来创建子节点的平台设备。

接下来对该函数的第47 行of_platform_device_create_pdata 函数进行讲解,该函数的具体定义如下所示:

static struct platform_device *of_platform_device_create_pdata(struct device_node *np,
                              const char *bus_id,void *platform_data,struct device *parent)
{
    struct platform_device *dev;
    
    /* 检查设备节点是否可用或已填充*/
    if (!of_device_is_available(np) ||of_node_test_and_set_flag(np, OF_POPULATED))
        return NULL;
    
    /* 分配平台设备结构体*/
    dev = of_device_alloc(np, bus_id, parent);
    if (!dev)
        goto err_clear_flag;
    
    /* 设置平台设备的一些属性*/
    dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
    if (!dev->dev.dma_mask)
        dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
    dev->dev.bus = &platform_bus_type;
    dev->dev.platform_data = platform_data;
    of_msi_configure(&dev->dev, dev->dev.of_node);
    of_reserved_mem_device_init_by_idx(&dev->dev, dev->dev.of_node, 0);
    
    /* 将平台设备添加到设备模型中*/
    if (of_device_add(dev) != 0) {
        platform_device_put(dev);
        goto err_clear_flag;
    }
    return dev;
    
    err_clear_flag:
    /* 清除设备节点的已填充标志*/
    of_node_clear_flag(np, OF_POPULATED);
    return NULL;
  • 第10 行:函数会检查设备节点的可用性,即检查设备树对应节点的status 属性。如果设备节点不可用或已经被填充,则直接返回NULL。
  • 第15 行:函数调用of_device_alloc 分配一个平台设备结构体,并将设备节点指针、设备标识符和父设备指针传递给它。如果分配失败,则跳转到err_clear_flag 标签处进行错误处理。
  • 第19 行,函数设置平台设备的一些属性。它将coherent_dma_mask 属性设置为32 位的DMA 位掩码,并检查dma_mask 属性是否为NULL。如果dma_mask 为NULL,则将其指向coherent_dma_mask。然后,函数设置平台设备的总线类型为platform_bus_type,并将平台数据指针存储在platform_data 属性中。接着,函数调用of_msi_configureof_reserved_mem_device_init_by_idx 来配置设备的MSI 和保留内存信息。
  • 第29 行:函数调用of_device_add 将平台设备添加到设备模型中。如果添加失败,则释放已分配的平台设备,并跳转到err_clear_flag 标签处进行错误处理。

至此,关于device_node 转换成platform_device 的具体流程就分析完成了,函数调用流程图如下(图64-1)所示:

image-20240828110128986

第65 章设备树下platform_deviceplatform_driver 匹配实验

在上一章节中我们学习了从device_node 到platform_device 的转换流程,转换完成之后操作系统才能够识别和管理设备,从而与platform_driver 进行匹配,在本章将将会对设备树下platform_device 和platform_driver 的匹配进行讲解。

65.1 of_match_table

在前面平台总线相关章节的学习中,了解到只有platform_device 结构体中的name 属性与platform_driver 结构体中嵌套的driver 结构体name 属性或者id_table 相同才能加载probe 初始化函数。

而为了使设备树能够与驱动程序进行匹配,需要在platform_driver 驱动程序中添加driver结构体的of_match_table 属性。这个属性是一个指向const struct of_device_id 结构的指针,用于描述设备树节点和驱动程序之间的匹配规则。of_device_id 结构体定义在内核源码的“/include/linux/mod_devicetable.h”文件中,具体内容如下所示:

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

struct of_device_id 结构体通常作为一个数组在驱动程序中定义,用于描述设备树节点和驱动程序之间的匹配规则。数组的最后一个元素必须是一个空的结构体,以标记数组的结束。

以下是一个示例,展示了如何在驱动程序中使用struct of_device_id 进行设备树匹配:

static const struct of_device_id my_driver_match[] = {
    { .compatible = "vendor,device-1" },
    { .compatible = "vendor,device-2" },
    { },
};

在上述示例中,my_driver_match 是一个struct of_device_id 结构体数组。每个数组元素都包含了一个compatible 字段,用于指定设备树节点的兼容性字符串。驱动程序将根据这些兼容性字符串与设备树中的节点进行匹配。

65.2 实验程序编写

本次实验的要求使用设备树描述下面的内存资源:
内存资源:
起始地址:0xFDD60000
结束地址:0xFDD60004

然后编写对应的platform_driver 驱动程序,要求跟上述内存资源所创建的节点进行匹配,从而验证上一小节讲解的of_match_table 属性。

65.2.1 设备树的编写

改完成的dts 文件和编译完成的boot.img 镜像对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\54_devicetree_probe\dts。
首先来对rk3568 的设备树结构进行以下介绍,根据sdk 源码目录下的“device/rockchip/rk356x/BoardConfig-rk3568-evb1-ddr4-v10.mk”默认配置文件可以了解到编译的设备树为rk3568-evb1-ddr4-v10-linux.dts,设备树之间的包含关系如下表所示:

image-20240828110500387

rk3568-evb1-ddr4-v10-linux.dts 是顶层设备树,为了便于理解我们之后在该设备树下进行节点的添加(当然这里也可以修改其他设备树),进入该设备树文件之后如下(图65-1)所示:

image-20240828110519083

然后将根据需求编写的设备树节点添加到rk3568-evb1-ddr4-v10-linux.dts 中,要添加的内容如下所示:

/{
    topeet{
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        myLed{
            compatible = "my devicetree";
            reg = <0xFDD60000 0x00000004>;
        };
    };
};

为了避免#address-cells = <1>;#size-cells = <1>;这两个属性改变根节点其他的节点的属性,所以在这里创建了一个topeet 节点。在这个示例中,#address-cells 设置为1 表示地址使用一个32 位的单元,#size-cells 也设置为1 表示大小使用一个32 位的单元。

  • 第5 行:将compatible 属性设置为”simple-bus“用于表示topeet 节点的兼容性,指明它是一个简单总线设备,在转换platform_device 的过程中,会继续查找该节点的子节点。
  • 第8 行:myLed 节点下的compatible 属性为”my devicetree”,表明该节点将会被转换为platform_device
  • 第9 行:这个属性用于描述myLed 节点的寄存器信息。reg 属性的值<0xFDD60000 0x00000004> 表示myLed 设备的寄存器起始地址为0xFDD60000,大小为0x00000004。添加完成如下所示:

image-20240828110850671

保存退出之后,重新编译内核源码,编译完成之后将生成的boot.img 烧写到开发板即可。

66.2.2 驱动程序的编写

本实验驱动对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\54_devicetree_probe\module。

本小节驱动程序是由“第52 章注册platform 驱动实验”程序修改而来,相较于源程序只是添加了of_match_table 相关代码,用来与设备树节点进行匹配。

编写完成的platform_driver.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");

    // 添加设备特定的操作
    // ...

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="my devicetree"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

65.3 运行测试

65.3.1 编译驱动程序

在上一小节中的platform_driver.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成platform_driver.ko 目标文件,至此驱动模块就编译成功了。

65.3.2 运行测试

在进行实验之前,首先要确保开发板烧写的是我们在65.2.1 小节中编译出来的boot.img。开发板启动之后,首先进入到“/proc/device-tree”目录下,查看是否已经存在了topeet 目录,如下图(图65-6)所示:

image-20240828111235903

只有在设备树节点编写正确的前提下,这里才会生成topeet 目录,如果没有出现topeet目录就要回头检查看看了。
然后使用以下命令进行驱动模块的加载,如下图(图65-7)所示:

insmod platform_driver.ko

image-20240828111259839

可以看到成功打印了在probe 函数中的打印,证明我们添加的设备树节点和platform_driver驱动匹配成功了。
然后使用以下命令进行驱动模块的卸载,如下图(图65-8)所示:

rmmod platform_driver.ko

image-20240828111321095

至此,设备树下platform_deviceplatform_driver 匹配实验就完成了。

第66 章of 操作函数实验:获取设备树节点

在上一章节的学习中,我们学习了设备树下platform_deviceplatform_driver 匹配,现在也只是让他们匹配在了一起,但这样显然是不够的,为了完成一些和硬件相关的需求,我们还需要获取到在设备树中编写的一些属性,那驱动是如何获取设备树中的属性呢,让我们一起进入后续章节的学习吧。

66.1 of 操作:获取设备树节点

在Linux 内核源码中提供了一系列的of 操作函数来帮助我们获取到设备树中编写的属性,在内核中以device_node 结构体来对设备树进行描述,所以of 操作函数实际上就是获取device_node 结构体,所以接下来我们学习的of 操作函数的返回值都是device_node 结构体,关于device_node 结构体的具体内容已经在63.1 小节讲解过了,这里不再进行赘述。

66.1.1 of_find_node_by_name函数

of_find_node_by_name 是Linux 内核中用于通过节点名称查找设备树节点的函数。下面是对of_find_node_by_name 函数的详细介绍:

函数原型:
    struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
头文件:
    #include <linux/of.h>
函数作用:
    该函数通过指定的节点名称在设备树中进行查找,返回匹配的节点的struct device_node 指针。
参数含义:
    from:指定起始节点,表示从哪个节点开始查找。如果from 参数为NULL,则从设备树的根节点开始查找。
    name:要查找的节点名称。
返回值:
    如果找到匹配的节点,则返回对应的struct device_node 指针。
    如果未找到匹配的节点,则返回NULL

会在接下来的实验小节中,对该函数进行实际演示。

66.1.2 of_find_node_by_path 函数

of_find_node_by_path 是Linux 内核中用于通过节点路径查找设备树节点的函数。下面是对of_find_node_by_path 函数的详细介绍:

函数原型:
    struct device_node *of_find_node_by_path(const char *path);
头文件:
    #include <linux/of.h>
函数作用:
    该函数根据节点路径在设备树中进行查找,返回匹配的节点的struct device_node 指针。
参数含义:
    path:节点的路径,以斜杠分隔的字符串表示。路径格式为设备树节点的绝对路径,例如/topeet/myLed。
返回值:
    如果找到匹配的节点,则返回对应的struct device_node 指针。
    如果未找到匹配的节点,则返回NULL

of_find_node_by_path 函数通过节点路径在设备树中进行查找。路径是设备树节点从根节点到目标节点的完整路径。可以通过指定正确的路径来准确地访问设备树中的特定节点。
使用of_find_node_by_path 函数时,可以直接传递节点的完整路径作为path 参数,函数会在设备树中查找匹配的节点。这对于已知节点路径的情况非常有用。

66.1.3 of_get_parent 函数

在Linux 内核中,of_get_parent 函数用于获取设备树节点的父节点。下面是对of_get_parent函数的详细介绍:

函数原型:
    struct device_node *of_get_parent(const struct device_node *node);
头文件:
    #include <linux/of.h>
函数作用:
    该函数接收一个指向设备树节点的指针node,并返回该节点的父节点的指针。
参数含义:
    node:要获取父节点的设备树节点指针。
返回值:
    如果找到匹配的节点,则返回对应的struct device_node 指针。
    如果未找到匹配的节点,则返回NULL

使用of_get_parent 函数时,可以将特定的设备树节点作为参数传递给函数,然后它将返回该节点的父节点。这对于在设备树中导航和访问节点之间的层次关系非常有用。
父节点在设备树中表示了节点之间的层次结构关系。通过获取父节点,你可以访问上一级节点的属性和配置信息,从而更好地理解设备树中的节点之间的关系。

66.1.4 of_get_next_child 函数

在Linux 内核中,of_get_next_child 函数用于获取设备树节点的下一个子节点。下面是对of_get_next_child 函数的详细介绍:

函数原型:
    struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);
头文件:
    #include <linux/of.h>
函数作用:
    该函数接收两个参数:node 是当前节点,prev 是上一个子节点。它返回下一个子节点的指针。
参数含义:
    node:当前节点,用于指定要获取子节点的起始节点。
    prev:上一个子节点,用于指定从哪个子节点开始获取下一个子节点。如果为NULL,则从起始节点的第一个子节点开始。
返回值:
    如果找到匹配的节点,则返回对应的struct device_node 指针。
    如果未找到匹配的节点,则返回NULL

使用of_get_next_child 函数时,可以传递当前节点以及上一个子节点作为参数。函数将从上一个子节点的下一个节点开始,查找并返回下一个子节点。
设备树中的子节点表示了节点之间的层次关系。通过获取子节点,你可以遍历和访问当前节点的所有子节点,以便进一步处理它们的属性和配置信息。

64.1.5 of_find_compatible_node 函数

当设备树中存在多个设备节点,需要根据设备的兼容性字符串进行匹配时,可以使用of_find_compatible_node 函数。该函数用于在设备树中查找与指定兼容性字符串匹配的节点。

函数原型:
    struct device_node *of_find_compatible_node(struct device_node *from, const char *type, 
                                                const char *compatible);
头文件:
    #include <linux/of.h>
函数作用:
    在设备树中查找与指定兼容性字符串匹配的节点。
参数含义:
    from:指定起始节点,表示从哪个节点开始查找。如果from 参数为NULL,则从设备树的根节点开始查找。
    type:要匹配的设备类型字符串,通常是compatible 属性中的一部分。
    compatible:要匹配的兼容性字符串,通常是设备树节点的compatible 属性中的值。
返回值:
    如果找到匹配的节点,则返回对应的struct device_node 指针。
    如果未找到匹配的节点,则返回NULL

使用of_find_compatible_node 函数时,可以指定起始节点和需要匹配的设备类型字符串以及兼容性字符串。函数会从起始节点开始遍历设备树,查找与指定兼容性字符串匹配的节点,并返回匹配节点的指针。

64.1.6 of_find_matching_node_and_match 函数

在Linux 内核中,of_find_matching_node_and_match 函数用于根据给定的of_device_id 匹配表在设备树中查找匹配的节点。

函数原型:
    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);
头文件:
    #include <linux/of.h>
函数作用:
    根据给定的of_device_id 匹配表在设备树中查找匹配的节点。
参数含义:
    from:表示从哪个节点开始搜索。通常将上一次调用该函数返回的节点作为参数传递给fr
    om,以便从上一次的下一个节点开始搜索。如果要从设备树的根节点开始搜索,可以将from参数设置为NULL。
    matches:指向一个of_device_id 类型的匹配表,该表包含要搜索的匹配项。
    match:用于输出匹配到的of_device_id 条目的指针。
返回值:
    如果找到匹配的节点,则返回对应的struct device_node 指针。
    如果未找到匹配的节点,则返回NULL

of_find_matching_node_and_match 函数在设备树中遍历节点,对每个节点使用__of_match_node 函数进行匹配。如果找到匹配的节点,将返回该节点的指针,并将match 指针更新为匹配到的of_device_id 条目,函数会自动增加匹配节点的引用计数。以下是使用of_find_matching_node_and_match 函数的示例代码:

#include <linux/of.h>
static const struct of_device_id my_match_table[] = {
    { .compatible = "vendor,device" },
    { /* sentinel */ }
};
const struct of_device_id *match;
struct device_node *np;

// 从根节点开始查找匹配的节点
np = of_find_matching_node_and_match(NULL, my_match_table, &match);

在上述示例中,我们定义了一个of_device_id 匹配表my_match_table,其中包含了一个兼容性字符串为”vendor,device“的匹配项。然后,我们使用of_find_matching_node_and_match 函数从根节点开始查找匹配的节点。

66.2 实验程序编写

本实验驱动对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\55_of_01。

本小节驱动程序是由上一章程序修改而来,相较于源程序只是在probe 函数中添加了本章节学习的of 操作相关代码,用来获取设备树节点。

编写完成的platform_driver.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>

struct device_node *mydevice_node;      
const struct of_device_id *mynode_match;
struct of_device_id mynode_of_match[] = {
	{.compatible="my devicetree"},
	{},
};

// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");

    // 通过节点名称查找设备树节点
    mydevice_node = of_find_node_by_name(NULL, "myLed");
	printk("mydevice node is %s\n", mydevice_node->name);
    
	// 通过节点路径查找设备树节点
    mydevice_node = of_find_node_by_path("/topeet/myLed");
    printk("mydevice node is %s\n", mydevice_node->name);
        
    // 获取父节点
    mydevice_node = of_get_parent(mydevice_node);
    printk("myled's parent node is %s\n", mydevice_node->name);
            
    // 获取子节点
    mydevice_node = of_get_next_child(mydevice_node, NULL);
    printk("myled's sibling node is %s\n", mydevice_node->name);

	// 使用compatible值查找节点
	mydevice_node=of_find_compatible_node(NULL ,NULL, "my devicetree");
	printk("mydevice node is %s\n" , mydevice_node->name);
	
	//根据给定的of_device_id匹配表在设备树中查找匹配的节点
	mydevice_node=of_find_matching_node_and_match(NULL , mynode_of_match, &mynode_match);
	printk("mydevice node is %s\n" ,mydevice_node->name);
	return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="my devicetree"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);
    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

66.3 运行测试

66.3.1 编译驱动程序

在上一小节中的platform_driver.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成platform_driver.ko 目标文件,至此驱动模块就编译成功了。

66.3.2 运行测试

在进行实验之前,首先要确保开发板烧写的是我们在65.2.1 小节中编译出来的boot.img,开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图66-4)所示:

insmod platform_driver.ko

image-20240828112424400

可以看到总共有4 个打印,前两个打印都是查找的myLed 节点,第三个打印是查找的myLed的父节点,也就是topeet 节点,第四个打印是查找的topeet 的子节点,也就又回到了myLed节点。第5 个打印是通过compatible 属性查找到的myLed 节点,第6 个打印是通过of_device_id匹配表查找到的myLed 节点.

然后使用以下命令进行驱动模块的卸载,如下图(图66-5)所示:

rmmod platform_driver.ko

image-20240828112452164

至此,使用of 操作函数获取设备树节点实验就完成了。

第67 章of 操作函数实验:获取属性

67.1 of 操作:获取属性

67.1.1 of_find_property 函数

of_find_property 函数用于在设备树中查找节点下具有指定名称的属性。如果找到了该属性,可以通过返回的属性结构体指针进行进一步的操作,比如获取属性值、属性长度等。

函数原型:
    struct property *of_find_property(const struct device_node *np, const char *name, int *lenp)
头文件:
    #include <linux/of.h>
函数作用:
    该函数用于在节点np 下查找指定名称name 的属性。
函数参数:
    np: 要查找的节点。
    name: 要查找的属性的属性名。
    lenp: 一个指向整数的指针,用于接收属性值的字节数。
返回值:
    如果成功找到了指定名称的属性,则返回对应的属性结构体指针struct property *;如果未找到,则返回NULL

67.1.2 of_property_count_elems_of_size 函数

该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性中元素的数量。调用该函数可以用于获取设备树属性中某个属性的元素数量,比如一个字符串列表的元素数量或一个整数数组的元素数量等。

函数原型:
    int of_property_count_elems_of_size(const struct device_node *np, 
                                        const char *propname, int lem_size)
头文件:
    #include <linux/of.h>
函数作用:
    该函数用于获取属性中指定元素的数量。
函数参数:
    np: 设备节点。
    propname: 需要获取元素数量的属性名。
    elem_size: 单个元素的尺寸。
返回值:
    如果成功获取了指定属性中元素的数量,则返回该数量;
    如果未找到属性或属性中没有元素,则返回0

67.1.3 of_property_read_u32_index 函数

该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性在给定索引位置处的u32 类型的数据值。
这个函数通常用于从设备树属性中读取特定索引位置的整数值。通过指定属性名和索引,可以获取属性中指定位置的具体数值。

函数原型:
    int of_property_read_u32_index(const struct device_node *np, const char *propname, 
                                   u32 index, u32 *out_value)
头文件:
    #include <linux/of.h>
函数作用:
    该函数用于从指定属性中获取指定索引位置的u32 类型的数据值。
函数参数:
    np: 设备节点。
    propname: 要读取的属性名。
    index: 要读取的属性值在属性中的索引,索引从0 开始。
    out_value: 用于存储读取到的值的指针。
返回值:
	如果成功读取到了指定属性指定索引位置的u32 类型的数据值,则返回0;
    如果未找到属性或读取失败,则返回相应的错误码。

67.1.4 of_property_read_u64_index 函数

该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性在给定索引位置处的u64 类型的数据值。
这个函数通常用于从设备树属性中读取特定索引位置的64 位整数值。通过指定属性名和索引,可以获取属性中指定位置的具体数值。

函数原型:
    static inline int of_property_read_u64_index(const struct device_node *np, 
                                                 const char *propname, u32 index, u64 *out_value)
头文件:
    #include <linux/of.h>
函数作用:
    该函数用于从指定属性中获取指定索引位置的u64 类型的数据值。
函数参数:
    np: 设备节点。
    propname: 要读取的属性名。
    index: 要读取的属性值在属性中的索引,索引从0 开始。
    out_value: 用于存储读取到的值的指针。
返回值:
    如果成功读取到了指定属性指定索引位置的u64 类型的数据值,则返回0;
    如果未找到属性或读取失败,则返回相应的错误码。

67.1.5 of_property_read_variable_u32_array 函数

该函数用于从设备树中读取指定属性名的变长数组。通过提供设备节点、属性名和输出数组的指针,可以将设备树中的数组数据读取到指定的内存区域中。同时,还需要指定数组的最小大小和最大大小,以确保读取到的数组符合预期的大小范围。

函数原型:
    int of_property_read_variable_u32_array(const struct device_node *np, const char *propname, 
                                            u32 *out_values, size_t SZ_min, size_t SZ_max)
函数作用:
    从指定属性中读取变长的u32 数组。
函数参数和返回值:
    np: 设备节点。
    propname: 要读取的属性名。
    out_values: 用于存储读取到的u8 数组的指针。
    SZ_min: 数组的最小大小。
    SZ_max: 数组的最大大小。
返回值:
    如果成功读取到了指定属性的u8 数组,则返回数组的大小。
    如果未找到属性或读取失败,则返回相应的错误码。

上面介绍的函数用于从指定属性中读取变长的u32 数组,下面是另外三个读取其他数组大小的函数:
这里给出了四个函数,用于从设备树中读取数组类型的属性值:

从指定属性中读取变长的u8 数组:

int of_property_read_variable_u8_array(const struct device_node *np, const char *propname, 
                                       u8 *out_values,size_t SZ_min, size_t SZ_max)

从指定属性中读取变长的u16 数组:

int of_property_read_variable_u16_array(const struct device_node *np, const char *propname, 
                                        u16*out_values, size_t SZ_min, size_t SZ_max)

从指定属性中读取变长的u64 数组:

int of_property_read_variable_u64_array(const struct device_node *np, const char *propname, 
                                        u64*out_values, size_t SZ_min, size_t SZ_max)

67.1.6 of_property_read_string 函数

该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性的字符串值,最后返回读取到的字符串的指针,通常用于从设备树属性中读取字符串值。通过指定属性名,可以获取属性中的字符串数据。

函数原型:
    static inline int of_property_read_string(const struct device_node *np, 
                                              const char *propname, const char **out_string)
头文件:
    #include <linux/of.h>
函数作用:
    该函数用于从指定属性中读取字符串。
函数参数:
    np: 设备节点。
    propname: 要读取的属性名。
    out_string: 用于存储读取到的字符串的指针。
返回值:
    如果成功读取到了指定属性的字符串,则返回0;
    如果未找到属性或读取失败,则返回相应的错误码。

67.2 实验程序编写

本实验驱动对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\56_of_02。

本小节驱动程序是由65 章驱动程序修改而来,由于本章节获取设备树属性的函数需要在查找到设备树节点的前提下使用,所以在下面的程序中,先在probe 函数中添加获取设备树节点的相关内容,然后添加了本章节学习的of 操作相关代码,用来获取设备树节点相关属性。

编写完成的platform_driver.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>

struct device_node *mydevice_node;      
int num;
u32 value_u32;
u64 value_u64;
u32 out_value[2];
const char *value_compatible;
struct property *my_property;

// 平台设备的探测函数
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");

    // 通过节点名称查找设备树节点
    mydevice_node = of_find_node_by_name(NULL, "myLed");
    printk("mydevice node is %s\n", mydevice_node->name);

    // 查找compatible属性
    my_property = of_find_property(mydevice_node, "compatible", NULL);
    printk("my_property name is %s\n", my_property->name);

    // 获取reg属性的元素数量
    num = of_property_count_elems_of_size(mydevice_node, "reg", 4);
    printk("reg num is %d\n", num);

    // 读取reg属性的值
    of_property_read_u32_index(mydevice_node, "reg", 0, &value_u32);
    of_property_read_u64_index(mydevice_node, "reg", 0, &value_u64);
    printk("value u32 is 0x%X\n", value_u32);
    printk("value u64 is 0x%llx\n", value_u64);

    // 读取reg属性的变长数组
    of_property_read_variable_u32_array(mydevice_node, "reg", out_value, 1, 2);
    printk("out_value[0] is 0x%X\n", out_value[0]);
    printk("out_value[1] is 0x%X\n", out_value[1]);

    // 读取compatible属性的字符串值
    of_property_read_string(mydevice_node, "compatible", &value_compatible);
    printk("compatible value is %s\n", value_compatible);

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="my devicetree"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

67.3 运行测试

67.3.1 编译驱动程序

export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += platform_driver.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules #make 操作
clean:
    make -C $(KDIR) M=$(PWD) clean #make clean 操作

对于Makefile 的内容注释已在上图添加,保存退出之后,来到存放platform_driver.c 和Makefile 文件目录下,如下图(图67-1)所示:然后使用命令“make”进行驱动的编译,编译完生成platform_driver.ko 目标文件,至此驱动模块就编译成功了。

67.3.2 运行测试

在进行实验之前,首先要确保开发板烧写的是我们在65.2.1 小节中编译出来的boot.img,开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图67-4)所示:

insmod platform_driver.ko

image-20240828113912798

可以看到总共有8 个打印,第一个打印表示查找到的节点为myLed,接下来的打印都是使用该节点进行的属性查找。第二个打印表示查找的属性名为“compatible”,第三个打印表示查找的reg 属性数量为2,第四个和第五个分别表示读取到的32 位和64 位的reg 属性值,第6 个和第7 个打印表示reg 的第一个属性值和第二个属性值,第8 个打印表示compatite 属性
值为“my devicetree”。
然后使用以下命令进行驱动模块的卸载,如下图(图67-5)所示:

rmmod platform_driver.ko

image-20240828113937356

至此,使用of 操作函数获取设备树节点实验就完成了。

第68 章ranges 属性实验

68.1 platform_get_resource 获取设备树资源

在上个章节中讲解了使用of 操作函数来获取设备树的属性,由于设备树在系统启动的时候都会转化为platform 设备, 那我们能不能直接在驱动中使用在53.1 小节中讲解的platform_get_resource 函数直接获取platform_device 资源呢?

68.1.1 驱动程序编写

带着疑惑我们这里仍旧以65 章的驱动程序为原型, 在probe 函数中加入使用platform_get_resource 函数获取reg 资源的函数,添加完成的驱动程序内容如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>

// 平台设备的初始化函数
struct resource *myresources;
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");
    // 获取平台设备的资源
    myresources = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (myresources == NULL) {
        // 如果获取资源失败,打印value_compatible 的值
        printk("platform_get_resource is error\n");
    }
    printk("reg valus is %llx\n" , myresources->start);
    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");
    // 清理设备特定的操作
    // ...
    return 0;
}

const struct of_device_id of_match_table_id[] = {
    {.compatible="my devicetree"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
        .of_match_table = of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;
    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }
    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");
    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);
    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

编译成模块之后,放到开发板上进行加载,打印信息如下(图68-1)所示:

image-20240829092256561

可以看到使用platform_get_resource 函数获取reg 资源的函数失败了,在下一个小节中将分析获取资源失败的原因。

68.1.2 获取资源失败源码分析

platform_get_resource 定义在内核源码目录下的”/drivers/base/platform.c“目录下,具体内容如下所示:

struct resource *platform_get_resource(struct platform_device *dev,
                                       unsigned int type, unsigned int num)
{
    u32 i;
    for (i = 0; i < dev->num_resources; i++) {
        struct resource *r = &dev->resource[i];
        if (type == resource_type(r) && num-- == 0)
            return r;
    }
    return NULL;
}

该函数返回NULL 符合第一小节中的情况,返回NULL 的情况有两种可能性,一种是没进入上面的for 循环直接返回了NULL,另外一种是进入了for 循环,但是类型匹配不正确,跳出for循环之后再返回NULL。这里的类型一定是匹配的,所以我们就来寻找为什么没有进入for 循环,这里只有一种可能,也就是dev->num_resources 为0。

所以现在的目标来到了寻找dev->num_resources 是在哪里进行的赋值,前面已经讲解过了由设备树转换为platform 的过程,而且在系统启动后,在对应目录下也有了相应的节点:

image-20240829092533132

证明转换是没问题的,所以继续寻找中间转换过程中有关资源数量的相关函数,定位到了of_platform_device_create_pdata 函数,该函数定义在内核源码目录下的“drivers/of/platform.c”文件中,具体内容如下所示:

static struct platform_device *of_platform_device_create_pdata(struct device_node *np,
                             const char *bus_id, void *platform_data, struct device *parent)
{
    struct platform_device *dev;
    /* 检查设备节点是否可用或已填充*/
    if (!of_device_is_available(np) ||of_node_test_and_set_flag(np, OF_POPULATED))
        return NULL;
    
    /* 分配平台设备结构体*/
    dev = of_device_alloc(np, bus_id, parent);
    if (!dev)
        goto err_clear_flag;
    
    /* 设置平台设备的一些属性*/
    dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
    if (!dev->dev.dma_mask)
        dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
    dev->dev.bus = &platform_bus_type;
    dev->dev.platform_data = platform_data;
    of_msi_configure(&dev->dev, dev->dev.of_node);
    of_reserved_mem_device_init_by_idx(&dev->dev, dev->dev.of_node, 0);

    /* 将平台设备添加到设备模型中*/
    if (of_device_add(dev) != 0) {
        platform_device_put(dev);
        goto err_clear_flag;
    }
    
    return dev;
    err_clear_flag:
    /* 清除设备节点的已填充标志*/
    of_node_clear_flag(np, OF_POPULATED);
    return NULL;
}

第15 行:函数调用of_device_alloc 分配一个平台设备结构体,并将设备节点指针、设备标识符和父设备指针传递给它,正是该函数决定的resource.num,然后找到该函数的定义,如下所示:

struct platform_device *of_device_alloc(struct device_node *np, const char *bus_id,
                                        struct device *parent)
{
    struct platform_device *dev;
    int rc, i, num_reg = 0, num_irq;
    struct resource *res, temp_res;
    dev = platform_device_alloc("", PLATFORM_DEVID_NONE);
    if (!dev)
        return NULL;
    
    /* count the io and irq resources */
    while (of_address_to_resource(np, num_reg, &temp_res) == 0)
        num_reg++;
    num_irq = of_irq_count(np);
    
    /* Populate the resource table */
    if (num_irq || num_reg) {
        res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);
        if (!res) {
            platform_device_put(dev);
            return NULL;
        }
        
        dev->num_resources = num_reg + num_irq;
        dev->resource = res;
        for (i = 0; i < num_reg; i++, res++) {
            rc = of_address_to_resource(np, i, res);
            WARN_ON(rc);
    	}
        if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
            pr_debug("not all legacy IRQ resources mapped for %pOFn\n",np);
    }
    
    dev->dev.of_node = of_node_get(np);
    dev->dev.fwnode = &np->fwnode;
    dev->dev.parent = parent ? : &platform_bus;
    if (bus_id)
    dev_set_name(&dev->dev, "%s", bus_id);
    else
    of_device_make_bus_id(&dev->dev);
    return dev;
}

在第26 行出现了for 循环的dev->num_resources = num_reg + num_irq;reg 的number 和irq的number,由于在设备树中并没有添加中断相关的属性num_irq 为0,那这里的num_reg 是哪里确定的呢。
我们向上找到14、15 行,具体内容如下所示:

/* count the io and irq resources */
while (of_address_to_resource(np, num_reg, &temp_res) == 0)
    num_reg++;

然后跳转到while 循环中的of_address_to_resource 函数,该函数定义在内核源码目录的drivers/of/address.c 文件中,具体内容如下所示:

int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
{
    const __be32 *addrp;
    u64 size;
    unsigned int flags;
    const char*name = NULL;
    
    addrp = of_get_address(dev, index, &size, &flags);
    if (addrp == NULL)
        return -EINVAL;
    
    /* Get optional "reg-names" property to add a name to a resource */
    of_property_read_string_index(dev, "reg-names", index, &name);
    return __of_address_to_resource(dev, addrp, size, flags, name, r);
}

第9 行,获取reg 属性的地址、大小和类型,在设备树中reg 属性已经存在了,所以这里会正确返回。
第14 行,读取reg-names 属性,由于设备树中没有定义这个属性,所以该函数不会有影响。
最后具有决定性作用的函数就是返回的__of_address_to_resource 函数了,跳转到该函数的定义如下所示:

static int __of_address_to_resource(struct device_node *dev,
                                    const __be32 *addrp, u64 size, unsigned int flags,
                                    const char *name, struct resource *r)
{
    u64 taddr;
    if (flags & IORESOURCE_MEM)
        taddr = of_translate_address(dev, addrp);
    else if (flags & IORESOURCE_IO)
        taddr = of_translate_ioport(dev, addrp, size);
    else
        return -EINVAL;
    
    if (taddr == OF_BAD_ADDR)
        return -EINVAL;
    
    memset(r, 0, sizeof(struct resource));
    r->start = taddr;
    r->end = taddr + size - 1;
    r->flags = flags;
    r->name = name ? name : dev->full_name;
    return 0;
}

reg 属性的flags 为IORESOURCE_MEM,所以又会执行第9 行的of_translate_address 函数,跳转到该函数,该函数的定义如下所示:

u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
{
    struct device_node *host;
    u64 ret;
    ret = __of_translate_address(dev, in_addr, "ranges", &host);
    if (host) {
        of_node_put(host);
        return OF_BAD_ADDR;
    }
    return ret;
}

该函数的重点在第6 行,上述函数实际上是__of_translate_address 函数的封装,其中传入的第三个参数“ranges”是我们要关注的重点,继续跳转到该函数的定义,具体内容如下所示:

static u64 __of_translate_address(struct device_node *dev,const __be32 *in_addr, 
                                  const char *rprop,struct device_node **host)
{
    struct device_node *parent = NULL;
    struct of_bus *bus, *pbus;
    __be32 addr[OF_MAX_ADDR_CELLS];
    int na, ns, pna, pns;
    u64 result = OF_BAD_ADDR;
    
    pr_debug("** translation for device %pOF **\n", dev);
    
    /* Increase refcount at current level */
    of_node_get(dev);
    *host = NULL;
    
    
    /* Get parent & match bus type */
    parent = of_get_parent(dev);
    if (parent == NULL)
        goto bail;
    
    bus = of_match_bus(parent);
    /* Count address cells & copy address locally */
    bus->count_cells(dev, &na, &ns);
    if (!OF_CHECK_COUNTS(na, ns)) {
        pr_debug("Bad cell count for %pOF\n", dev);
        goto bail;
    }
    memcpy(addr, in_addr, na * 4);
    
    pr_debug("bus is %s (na=%d, ns=%d) on %pOF\n",bus->name, na, ns, parent);
    of_dump_addr("translating address:", addr, na);
    
    /* Translate */
    for (;;) {
        struct logic_pio_hwaddr *iorange;
        /* Switch to parent bus */
        of_node_put(dev);
        dev = parent;
        parent = of_get_parent(dev);
        /* If root, we have finished */
        if (parent == NULL) {
            pr_debug("reached root node\n");
            result = of_read_number(addr, na);
            break;
    	}
        
        /*
        * For indirectIO device which has no ranges property, get
        * the address from reg directly.
        */
        iorange = find_io_range_by_fwnode(&dev->fwnode);
        if (iorange && (iorange->flags != LOGIC_PIO_CPU_MMIO)) {
            result = of_read_number(addr + 1, na - 1);
            pr_debug("indirectIO matched(%pOF) 0x%llx\n",
            dev, result);
            *host = of_node_get(dev);
            break;
        }
        
        /* Get new parent bus and counts */
        pbus = of_match_bus(parent);
        pbus->count_cells(dev, &pna, &pns);
        if (!OF_CHECK_COUNTS(pna, pns)) {
            pr_err("Bad cell count for %pOF\n", dev);
            break;
        }
        
        pr_debug("parent bus is %s (na=%d, ns=%d) on %pOF\n",pbus->name, pna, pns, parent);
        
        /* Apply bus translation */
        if (of_translate_one(dev, bus, pbus, addr, na, ns, pna, rprop))
            break;
        
        /* Complete the move up one level */
        na = pna;
        ns = pns;
        bus = pbus;
        of_dump_addr("one level translation:", addr, na);
    }
    bail:
    of_node_put(parent);
    of_node_put(dev);
    return result;
}

第18 行,parent = of_get_parent(dev);获取父节点和匹配的总线类型
第24 行,bus->count_cells(dev, &na, &ns);获取address-cell 和size-cells
然后是一个for 循环,在76 行使用of_translate_one 函数进行转换,其中rprop 参数表示要转换的资源属性,该参数的值为传入的“ranges”,然后我们继续跳转到该函数,该函数的具体内容如下所示:

static int of_translate_one(struct device_node *parent, struct of_bus *bus,
                            struct of_bus *pbus, __be32 *addr,
                            int na, int ns, int pna, const char *rprop)
{
    const __be32 *ranges;
    unsigned int rlen;
    int rone;
    u64 offset = OF_BAD_ADDR;
    
    /*
    * Normally, an absence of a "ranges" property means we are
    * crossing a non-translatable boundary, and thus the addresses
    * below the current cannot be converted to CPU physical ones.
    * Unfortunately, while this is very clear in the spec, it's not
    * what Apple understood, and they do have things like /uni-n or
    * /ht nodes with no "ranges" property and a lot of perfectly
    * useable mapped devices below them. Thus we treat the absence of
    * "ranges" as equivalent to an empty "ranges" property which means
    * a 1:1 translation at that level. It's up to the caller not to try
    * to translate addresses that aren't supposed to be translated in
    * the first place. --BenH.
    *
    * As far as we know, this damage only exists on Apple machines, so
    * This code is only enabled on powerpc. --gcl
    */
    ranges = of_get_property(parent, rprop, &rlen);
    if (ranges == NULL && !of_empty_ranges_quirk(parent)) {
        pr_debug("no ranges; cannot translate\n");
        return 1;
    }
    
    if (ranges == NULL || rlen == 0) {
        offset = of_read_number(addr, na);
        memset(addr, 0, pna * 4);
        pr_debug("empty ranges; 1:1 translation\n");
        goto finish;
    }
    
    pr_debug("walking ranges...\n");
    /* Now walk through the ranges */
    rlen /= 4;
    rone = na + pna + ns;
    for (; rlen >= rone; rlen -= rone, ranges += rone) {
        offset = bus->map(addr, ranges, na, ns, pna);
        if (offset != OF_BAD_ADDR)
        break;
    }
    
    if (offset == OF_BAD_ADDR) {
        pr_debug("not found !\n");
        return 1;
    }
    memcpy(addr, ranges + na, 4 * pna);
    
    finish:
    of_dump_addr("parent translation for:", addr, pna);
    pr_debug("with offset: %llx\n", (unsigned long long)offset);
    
    /* Translate it into parent bus space */
    return pbus->translate(addr, offset, pna);
}

在该函数的第26 行使用of_get_property 函数获取“ranges”属性,但由于在我们添加的设备树节点中并没有该属性,所以这里的ranges 值就为NULL,第27 行的条件判断成立,也就会返回1。
接下来再根据这个返回值继续分析上级函数:of_translate_one 函数返回1 之后,上一级的_of_translate_address 的返回值就为OF BADADDR,再上一级的of_translate_address 返回值也是OF BAD _ADDR,继续向上查找_of_address_to_resource 函数会返回EINVALof address_ to resource 返回EINVAL,所以num_reg 为0;到这里关于为什么platform_get_resource 函数获取资源失败的问题就找到了,只是因为在设备树中并没有这个名为ranges 这个属性,所以只需要对设备树进行ranges 属性的添加即可,要修改的设备树为arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10-linux.dts,修改完成如下(图68-3)所示:

image-20240829100053478

然后重新编译内核,将编译生成的boot.img 烧写进开发板之后重新加载上面编写的驱动程序,可以看到之前获取失败的打印就消失了,而且成功打印出了reg 属性的第一个值,如下图(图68-4)所示:

image-20240829100114406

虽然这里的问题解决了,但引起的思考并没有结束,那我们在这里添加的ranges 属性的作用是啥呢,带着疑问,开始下一小节的学习吧。

68.2 ranges 属性

68.2.1 ranges 属性介绍

ranges 属性是一种用于描述设备之间地址映射关系的属性。它在设备树(Device Tree)中使用,用于描述子设备地址空间如何映射到父设备地址空间。设备树是一种硬件描述语言,用于描述嵌入式系统中的硬件组件和它们之间的连接关系。

设备树中的每个设备节点都可以具有ranges 属性,其中包含了地址映射的信息。下面是一个常见的格式:

ranges = <child-bus-address parent-bus-address length>;

或者

ranges;

然后对上述格式中每个部分进行解释:

  • child-bus-address:子设备地址空间的起始地址。它指定了子设备在父设备地址空间中的位置。具体的字长由ranges 所在节点的#address-cells 属性决定。
  • parent-bus-address:父设备地址空间的起始地址。它指定了父设备中用于映射子设备的地址范围。具体的字长由ranges 的父节点的#address-cells 属性决定。
  • length:映射的大小。它指定了子设备地址空间在父设备地址空间中的长度。具体的字长由ranges 的父节点的#size-cells 属性决定。
  • ranges 属性的值为空时,表示子设备地址空间和父设备地址空间具有完全相同的映射,即1:1 映射。这通常用于描述内存区域,其中子设备和父设备具有相同的地址范围。
  • ranges 属性的值不为空时,按照指定的映射规则将子设备地址空间映射到父设备地址空间。具体的映射规则取决于设备树的结构和设备的特定要求。

然后以下面的设备树为例进行ranges 属性的讲解,设备树内容如下所示:

/dts-v1/;
/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    ....
    external-bus {
        #address-cells = <2>;
        #size-cells = <1>;
        ranges = <0 0 0x10100000 0x10000
                    1 0 0x10160000 0x10000
                    2 0 0x30000000 0x30000000>;
        // Chipselect 1, Ethernet
        // Chipselect 2, i2c controller
        // Chipselect 3, NOR Flash
    .......

这里以ranges 的第一个属性值为例进行具体解释如下:
external-bus 节点中#address-cells 属性值为2 表示child-bus-address 由两个值表示,也就是0 和0,父节点的#address-cells 属性值和#size-cells 属性值为1,表示parent-bus-addresslength 都由一个表示,也就是0x101000000x10000,该ranges 值表示将子地址空间(0x0-0xFFFF)映射到父地址空间0x10100000 - x1010FFFF,这里的例子为带参数ranges 属性映射,不带参数的ranges 属性为1:1 映射,较为简单,这里不再进行举例。

在嵌入式系统中,不同的设备可能连接到相同的总线或总线控制器上,它们需要在物理地址空间中进行正确的映射,以便进行数据交换和通信。例如,一个设备可能通过总线连接到主处理器或其他设备,而这些设备的物理地址范围可能不同。ranges 属性就是用来描述这种地址映射关系的。

68.2.2 设备分类

根据上面讲解的映射关系可以将设备分为两类:内存映射型设备和非内存映射型设备。

(1)内存映射型设备:

内存映射型设备是指可以通过内存地址进行直接访问的设备。这类设备在物理地址空间中的一部分被映射到系统的内存地址空间中,使得CPU 可以通过读写内存地址的方式与设备进行通信和控制。

特点:
(1)直接访问:内存映射型设备可以被CPU 直接访问,类似于访问内存中的数据。这种直接访问方式提供了高速的数据传输和低延迟的设备操作。
(2)内存映射:设备的寄存器、缓冲区等资源被映射到系统的内存地址空间中,使用读写内存的方式与设备进行通信。
(3)读写操作:CPU 可以通过读取和写入映射的内存地址来与设备进行数据交换和控制操作。

在设备树中,内存映射型设备的设备树举例如下所示:

/dts-v1/;
/ {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;
    serial@101f0000 {
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000>;
    };
    gpio@101f3000 {
        compatible = "arm,pl061";
        reg = <0x101f3000 0x1000
        0x101f4000 0x10>;
    };
    spi@10115000 {
        compatible = "arm,pl022";
        reg = <0x10115000 0x1000>;
    };
};

第5 行的ranges 属性表示该设备树中会进行1:1 的地址范围映射。

(2)非内存映射型设备:

非内存映射型设备是指不能通过内存地址直接访问的设备。这类设备可能采用其他方式与CPU 进行通信,例如通过I/O 端口、专用总线或特定的通信协议。

特点:

  • (1)非内存访问:非内存映射型设备不能像内存映射型设备那样直接通过内存地址进行访问。它们可能使用独立的I/O 端口或专用总线进行通信。
  • (2)特定接口:设备通常使用特定的接口和协议与CPU 进行通信和控制,例如SPI、I2C、UART 等。
  • (3)驱动程序:非内存映射型设备通常需要特定的设备驱动程序来实现与CPU 的通信和控制。

在设备树中,非内存映射型设备的设备树举例如下所示:

/dts-v1/;
/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    ....
    external-bus {
        #address-cells = <2>;
        #size-cells = <1>;
        ranges = <0 0 0x10100000 0x10000
                1 0 0x10160000 0x10000
                2 0 0x30000000 0x30000000>;
        
        // Chipselect 1, Ethernet
        // Chipselect 2, i2c controller
        // Chipselect 3, NOR Flash
        ethernet@0,0 {
            compatible = "smc,smc91c111";
            reg = <0 0 0x1000>;
        };
        
        i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <1 0 0x1000>;
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <0x58>;
            };
        };
    } ;
};

68.2.3 映射地址计算

接下来以上面列举的非内存映射型设备的设备树中的ethernet@0 节点为例,计算该网卡设备的映射地址。
首先,找到ethernet@0 所在的节点,并查看其reg 属性。在给定的设备树片段中,ethernet@0 的reg 属性为<0 0 0x1000>。在根节点中,#address-cells 的值为1,表示地址由一个单元格组成。

接下来,根据ranges 属性进行地址映射计算。在external-bus 节点的ranges 属性中,有三个映射条目:

  • 第一个映射条目为“0 0 0x10100000 0x10000”,表示外部总线的地址范围为0x101000000x1010FFFF。该映射条目的第一个值为0,表示与external-bus 节点的第一个子节点(ethernet@0,0)相关联。
  • 第二个映射条目:“1 0 0x10160000 0x10000”,表示外部总线的地址范围为0x101600000x1016FFFF。该映射条目的第一个值为1,表示与external-bus 节点的第二个子节点(i2c@1,0)相关联。
  • 第三个映射条目:“2 0 0x30000000 0x30000000”,表示外部总线的地址范围为0x300000000x5FFFFFFF。该映射条目的第一个值为2,表示与external-bus 节点的第三个子节点相关联。

由于ethernet@0 与external-bus 的第一个子节点相关联,并且它的reg 属性为<0 0 0x1000>,我们可以进行以下计算:
ethernet@0 的物理起始地址= 外部总线地址起始值=0x10100000
ethernet@0 的物理结束地址= 外部总线地址起始值+ (ethernet@0 的reg 属性的第二个值-1)
= 0x10100000 + 0xFFF
= 0x10100FFF
因此,ethernet@0 的物理地址范围为0x10100000 - 0x10100FFF,至此,关于映射地址的计算就讲解完成了,大家可以根据同样的方法计算i2c@1 的物理地址。

第69 章of 操作函数实验:获取中断资源

69.1 of 操作:获取中断资源

69.1.1 irq_of_parse_and_map 函数

该函数的主要功能是解析设备节点的”interrupts”属性,并将对应的中断号映射到系统的中断号。”interrupts”属性通常以一种特定的格式表示,可以包含一个或多个中断号。通过提供索引号,可以获取对应的中断号。

函数原型:
    unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
头文件:
    #include <linux/of_irq.h>
函数作用:
    从设备节点的"interrupts"属性中解析和映射对应的中断号
参数说明:
    dev:设备节点,表示要解析的设备节点。
    index:索引号,表示从"interrupts"属性中获取第几个中断号。
返回值:
    是一个无符号整数,表示成功解析和映射的中断号。

69.1.2 irq_get_trigger_type 函数

该函数的主要功能是从给定的中断数据结构中提取中断触发类型。中断触发类型描述了中断信号的触发条件,例如边沿触发(edge-triggered)或电平触发(level-triggered)等。

函数原型:
    u32 irqd_get_trigger_type(struct irq_data *d);
头文件:
    #include <linux/irq.h>
函数作用:
    从中断数据结构(irq_data)中获取对应的中断触发类型。
参数说明:
    d:中断数据结构(irq_data),表示要获取中断触发类型的中断。
返回值:
    是一个无符号32 位整数,表示成功获取的中断触发类型。

69.1.3 irq_get_irq_data 函数

函数irq_get_irq_data 的作用是根据中断号获取对应的中断数据结构(irq_data)。

函数原型:
    struct irq_data *irq_get_irq_data(unsigned int irq);
头文件:
    #include <linux/irq.h>
函数作用:
    根据中断号获取对应的中断数据结构。
参数说明:
    irq:中断号,表示要获取中断数据结构的中断号。
返回值:
    指向irq_data 结构体的指针,表示成功获取的中断数据结构。

69.1.4 gpio_to_irq 函数

该函数的主要功能是将给定的GPIO 编号转换为对应的中断号。在某些系统中,GPIO 可以配置为中断引脚,当特定事件发生时触发中断。通过该函数,可以根据GPIO 编号获取与之关联的中断号,以便进行中断处理等操作。

函数原型:
    int gpio_to_irq(unsigned int gpio);
头文件:
    #include <linux/gpio.h>
函数作用:
    根据GPIO 编号获取对应的中断号。
参数说明:
    gpio:GPIO 编号,表示要获取中断号的GPIO。
返回值:
    是一个整数,表示成功获取的中断号。

69.1.5 of_irq_get 函数

该函数的主要功能是从给定的设备节点的”interrupts”属性中解析并获取对应的中断号。”interrupts”属性通常以一种特定的格式表示,可以包含一个或多个中断号。通过提供索引号,可以获取对应的中断号

函数原型:
    int of_irq_get(struct device_node *dev, int index);
头文件:
    #include <linux/of_irq.h>
函数作用:
    是从设备节点的"interrupts"属性中获取对应的中断号。
参数说明:
    dev:设备节点,表示要获取中断号的设备节点。
    index:索引号,表示从"interrupts"属性中获取第几个中断号。
返回值:
    是一个整数,表示成功获取的中断号。

69.1.6 platform_get_irq 函数

platform_get_irq 函数的主要功能是根据给定的平台设备和索引号获取对应的中断号。平台设备是指与特定硬件平台相关的设备。在某些情况下,平台设备可能具有多个中断号,通过提供索引号,可以获取对应的中断号。

函数原型:
    int platform_get_irq(struct platform_device *dev, unsigned int num);
函数作用:
    根据平台设备和索引号获取对应的中断号。
头文件:
    linux/platform_device.h
参数说明:
    dev:平台设备,表示要获取中断号的平台设备。
    num:索引号,表示从中获取第几个中断号。
返回值:
    是一个整数,表示成功获取的中断号。

69.2 实验程序编写

69.2.1 设备树的修改

本实验修改完成的设备树和编译完成的boot.img 对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\57_of_03\dts

由于本章节要获取的是中断相关的资源,所以需要在设备树中添加有关中断的设备节点,在第57 章节的学习中,我们已经对中断实例进行了讲解,所以这里直接对rk3568-evb1-ddr4-v10-linux.dts 设备树进行中断节点的添加,添加的内容如下所示:

myirq {
    compatible = "my_devicetree_irq";
    interrupt-parent = <&gpio3>;
    interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
};

添加完成如下图(图69-1)所示:

image-20240829104143669

保存退出之后,重新编译内核源码,得到boot.img 内核镜像之后烧写到开发板即可。

69.2.2 实验程序的编写

本实验驱动对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\57_of_03

本小节驱动程序是由65 章驱动程序修改而来,由于本章节获取中断属性的函数需要在查找到设备树节点的前提下使用,所以在下面的程序中,先在probe 函数中查找设备树节点,然后添加了本章节学习的of 操作相关代码和其他一些相关的函数,用来获取设备树节点中断资源。

编写完成的platform_driver.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
int num;
int irq;
struct irq_data *my_irq_data;
struct device_node *mydevice_node;
u32 trigger_type;

// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");

    // 查找设备节点
    mydevice_node = of_find_node_by_name(NULL, "myirq");
    
    // 解析和映射中断
    irq = irq_of_parse_and_map(mydevice_node, 0);
    printk("irq is %d\n", irq);
    
    // 获取中断数据结构
    my_irq_data = irq_get_irq_data(irq);
    // 获取中断触发类型
    trigger_type = irqd_get_trigger_type(my_irq_data);
    printk("trigger type is 0x%x\n", trigger_type);
    
    // 将GPIO转换为中断号
    irq = gpio_to_irq(101);
    printk("irq is %d\n", irq);
    
    // 从设备节点获取中断号
    irq = of_irq_get(mydevice_node, 0);
    printk("irq is %d\n", irq);
    
    // 获取平台设备的中断号
    irq = platform_get_irq(pdev, 0);
    printk("irq is %d\n", irq);
    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="my_devicetree_irq"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

69.3 运行测试

69.3.1 编译驱动程序

对于Makefile 的内容注释已在上图添加,保存退出之后,来到存放platform_driver.c 和Makefile 文件目录下,如下图(图69-2)所示:

image-20240829104603535

然后使用命令“make”进行驱动的编译,编译完生成platform_driver.ko 目标文件,至此驱动模块就编译成功了。

69.3.2 运行测试

在进行实验之前,首先要确保开发板烧写的是我们在69.2.1 小节中编译出来的boot.img,开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图69-5)所示:

insmod platform_driver.ko

image-20240829104637622

可以看到总共有5 个打印,第1、3、4、5 个打印都是获取的中断号为113,第2 个打印的是中断的类型, 即IRQ_TYPE_LEVEL_LOW , 该触发类型的宏定义在内核源码“include/dt-bindings/interrupt-controller/irq.h”目录下,具体内容如下所示:

#define IRQ_TYPE_NONE 0 // 无中断触发类型
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)// 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发

可以看到IRQ_TYPE_LEVEL_LOW 的宏定义为8,证明上面的打印正确。
然后使用以下命令进行驱动模块的卸载,如下图(图69-6)所示:

rmmod platform_driver.ko

image-20240829104730890

至此,使用of 操作函数获取中断资源实验就完成了。

第70 章参考文档:设备树bindings

在前面的章节中,我们已经介绍了许多设备树编写相关的知识,当然上面我们讲解的都是标准属性,但当我们遇到非标准属性或无法理解的属性时,要如何处理呢?这时候就不得不提到bindings 文档了。

Documentation/devicetree/bindings 目录是Linux 内核源码中的一个重要目录,用于存储设备树(Device Tree)的bindings 文档。设备树是一种描述硬件平台和设备配置的数据结构,它以一种可移植和独立于具体硬件的方式描述了设备的属性、寄存器配置、中断信息等。

bindings 目录中的文档提供了有关设备树的各种设备和驱动程序的详细说明和用法示例。这些文档对于开发人员来说非常重要,因为它们提供了在设备树中描述硬件和配置驱动程序所需的属性和约定。bindings 目录截图如下(图70-1)所示:

image-20240829104812252

接下来对Documentation/devicetree/bindings 目录的一些常见子目录和其内容的概述:

  • arm:包含与ARM 体系结构相关的设备和驱动程序的bindings 文档。
  • clock:包含与时钟设备和时钟控制器相关的bindings 文档。
  • dma:包含与直接内存访问(DMA)控制器和设备相关的bindings 文档。
  • gpio:包含与通用输入输出(GPIO)控制器和设备相关的bindings 文档。
  • i2c:包含与I2C 总线和设备相关的bindings 文档。
  • interrupt-controller:包含与中断控制器相关的bindings 文档。
  • media:包含与多媒体设备和驱动程序相关的bindings 文档。
  • mfd:包含与多功能设备(MFD)子系统和设备相关的bindings 文档。
  • networking:包含与网络设备和驱动程序相关的bindings 文档。
  • power:包含与电源管理子系统和设备相关的bindings 文档。
  • spi:包含与SPI 总线和设备相关的bindings 文档。
  • usb:包含与USB 控制器和设备相关的bindings 文档。
  • video:包含与视频设备和驱动程序相关的bindings 文档。

每个子目录中的文档通常以.txt 或.yaml 的扩展名保存,使用文本或YAML 格式编写。这些文档提供了有关设备树中属性的详细说明、属性的语法、可选值和用法示例。它们还描述了设备树的约定和最佳实践,以帮助开发人员正确地配置和描述硬件设备和驱动程序。

通过阅读Documentation/devicetree/bindings 目录中的文档,开发人员可以了解各种设备和驱动程序的设备树属性的含义和用法,以便正确地配置和描述硬件平台和设备。这有助于实现硬件与软件之间的正确匹配和交互,使系统能够正确识别和使用硬件设备。

第八篇设备树插件

第71 章设备树插件介绍

在上一篇中,我们学习了设备树的相关内容,那么本篇开始学习设备树的扩展机制——设备树插件。下面让我们一起进入设备树插件的学习吧。

71.1 什么是设备树插件

Linux4.4 以后引入了动态设备树(Dynamic DeviceTree)。设备树插件(Device Tree Overlay)是一种用于设备树(Device Tree)的扩展机制。设备树是一种用于描述硬件设备的数据结构,广泛应用于嵌入式系统中,特别是基于Linux 内核的系统中。

设备树插件允许在运行时动态修改设备树的内容,以便添加,修改或删除设备节点和属性。它提供了一种灵活的方式来配置和管理硬件设备,而无需重新编译整个设备树。通过使用设备树插件,开发人员可以在不重新启动系统的情况下对硬件进行配置更改。

设备树插件(Dynamic DeviceTree)通常以一种文本格式定义,称为设备树源文件(DeviceTree Source,DTS)。DTS 文件描述了设备树的结构和属性,包括设备节点,寄存器地址,中断信息等。设备树插件可以通过加载和解析设备树文件,并将其合并到现有的设备树中,从而实现对设备树的动态修改。

71.2 设备树插件的应用场景

使用设备树插件,可以实现一些常见的配置变化,比如添加外部设备,禁用不需要的设备,修改设备属性等。这对于嵌入式系统的开发和调试非常有用,特别是面对多种硬件配置或需要频繁更改硬件配置的情况下。

第72 章设备树插件语法和编译实验

在上一章节中,我们介绍了设备树插件和概念和作用,本章节将继续深入探讨设备树插件,重点关注其语法和编译过程。无论是在嵌入式系统开发还是设备驱动开发中,掌握设备树插件的语法和编译过程都是非常重要的。接下来,让我们一起深入了解这些内容,为更好地应用设备树插件打下坚实的基础。

72.1 设备树插件语法

设备树插件的语法格式基于设备树源文件的语法,但是有一些特定的语法和指令用于描述插件的行为。下面是设备树插件语法格式的一般结构和示例。我们新建overlay.dts,如下(图72- 1)所示:

image-20240829105225168

我们编写overlay.dts,如下所示:
1 首先添加插件头部声明,它指定了插件的名称和版本等信息,并指定了要修改的设备树的路径,如下所示:

/dts-v1/;
/plugin/;

2 插件节点名称用于定义要添加,修改或删除的设备节点及其属性。它使用与设备树源文件相同的语法,但在节点名称前面使用特定的修饰符来指示插件的操作。比如设备树中rs485 节点为如下所示,rs485 节点位于(图72- 2)根节点下。

image-20240829105303972

那么我们如果在设备树插件中要为这个节点添加overlay_node 节点,可以有如下几种表达方式:

image-20240829105314902 image-20240829105329417

这四种方式是等效的,大家了解即可。

72.2 设备树插件编译

我们将上个小节编写的overlay.dts 的方法二三四注释掉,保留方法一,然后编译设备树插件overlay.dts,输入以下命令:

/home/topeet/Linux/linux_sdk/kernel/scripts/dtc/dtc -I dts -O dtb overlay.dts -o overlay.dtbo

image-20240829105435697

反编译设备树,输入以下命令:

/home/topeet/Linux/linux_sdk/kernel/scripts/dtc/dtc -I dtb -O dts overlay.dtbo -o 1.dts

image-20240829105503704

反编译成功之后,查看1.dts,可以比较下overlay.dts 和1.dts 的区别。通过反编译设备树有助于理解和修改设备树配置,帮助开发者更好地进行系统开发,调试和故障排除。

第73 章设备树插件使用实验

在上一章节中,我们详细介绍了设备树插件的语法和编译过程,为了更好地理解和应用这些知识,本章节将重点关注设备树插件在实际实验操作中的使用方法。让我们开始实际使用设备树插件吧!

73.1 准备实验环境

我们首先烧写网盘资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\65_dtbocfg”目录下的Linux 系统镜像,然后将设备树插件dtbocfg.ko 拷贝到系统中,最后使用“insmod dtbocfg.ko”命令加载驱动,如下(图73-1)所示:

image-20240829105538246

然后输入命令cat proc/filesystems 检查configfs 是否挂载成功。挂载成功如下(图73-2)所示:

image-20240829105605010

73.2 设备树插件的使用

在上一个小节中,我们烧写了支持设备树插件的内核镜像,并且加载了dtbocfg.ko。在此基础上,本小节来讲述如何使用设备树插件。
在上一章节中,我们编写了overlay.dts。在overlay.dts 中,rk-485-ctl 节点下添加新的节点overlay_node 节点,如下(图73-3)所示:

image-20240829105623804

使用dtc 编译器编译得到dtbo 文件,并将dtbo 拷贝到开发板上。

/home/topeet/Linux/linux_sdk/kernel/scripts/dtc/dtc -I dts -O dtb overlay.dts -o overlay.dtbo

image-20240829105649100

我们将编译好的dtbo 文件拷贝到开发板上,如下图(图73-5)所示:

image-20240829105659781

我们进到系统/sys/kernel/config/device-tree/overlays/(这个目录需要加载设备树插件才会生成)目录下。如下图(图73-6)所示:

image-20240829105717123

在这个目录下使用以下命令创建一个内核对象,如下图(图73-7)所示:

mkdir test

image-20240829105733544

使用命令cd test 进到test 文件夹,如下图(图73-8)所示:

image-20240829105741647

使用以下命令写入dtbo 中,如下图(图73-9)所示:

cat /overlay.dtbo > dtbo

image-20240829105758995

使用以下命令使能dtbo,如下图(图73-10)所示:

echo 1 > status

image-20240829105819613

此时我们可以使用以下命令看到加载的节点。

ls /proc/device-tree/rk-485-ctl/overlay_node/

image-20240829105837861

如果我们想删掉使用dtbo 修改的节点,在/sys/kernel/config/device-tree/overlays 下使用“rmdir test”即可。如下图(图73-12)所示:

image-20240829105859553

此时我们可以使用命令“ ls /proc/device-tree/rk-485-ctl/ ” 查看, 已经看不到添加的overlay_node 节点了。

image-20240829105917441

73.3 加载多个dtbo

我们准备第二个dtbo 文件,修改overlay_node 节点中的status 属性。如下(图73-14)所示:

image-20240829110049524

在这个目录下使用命令mkdir test1 创建一个内核对象。如下图(图73-15)所示:

image-20240829110102323

使用命令“cd test”进到test1 文件夹,如下图(图73-16)所示:

image-20240829110210189

使用命令“cat /overlay2.dtbo > dtbo”写进dtbo 中,如下图(图73-17)所示:

image-20240829110223858

使用命令“echo 1 > status”使用dtbo,如下图(图73-18)所示:

image-20240829110246776

此时我们可以使用命令“cat /proc/device-tree/rk-485-ctl/overlay_node/status”看到属性值已经被修改了过来,如下图(图73-19)所示:

image-20240829110341645

删除test1 文件夹,如下图(图73-20)所示:

image-20240829110352891

可以看到status 的属性值已经被修改了回来,如下图(图73-21)所示:

image-20240829110400579

第74 章虚拟文件系统ConfigFS 介绍

前面几个章节中,我们详细讲解了设备树插件的语法和编译过程,了解了如何利用设备树插件实现设备树的模块化和复用。本章节将进一步拓展我们的知识,介绍虚拟文件系统ConfigFS。虚拟文件系统ConfigFS 是一个特殊的文件系统,旨在提供一种动态配置Linux 内核和设备的机制。让我们一起深入学习ConfigFS,并将其应用于实际实验操作中,为我们的设备树和系统开发带来更多的可能性和创新性。

74.1 常用的虚拟文件系统

在Linux 内核中,有几个常用的虚拟文件系统,虚拟文件系统提供了一个内核抽象层,使得应用程序可以通过统一的文件访问接口操作不同类型的文件和设备。它简化了应用程序的开发和维护,提供了更高的可移植性和灵活性,并提供了管理文件系统和访问底层硬件的功能。

其中最常见的虚拟文件系统如下所示:

  • 1 procfs 是一个虚拟文件系统,提供了对系统内核运行时状态的访问接口。它以文件和目录的形式表示内核中的进程,设备,驱动程序和其他系统信息。通过读取和写入procfs 中的文件,可以获取和修改有关系统状态的信息。
  • 2 sysfs 是一个虚拟文件系统,用于表示系统中的设备,驱动程序和其他内核对象。它提供一种统一的接口,通过文件和目录来访问和配置这些对象的属性和状态。Sysfs 常用于设备驱动程序和系统管理工具,用于查看和控制系统的硬件和内核对象。
  • 3 configfs 是一个虚拟文件系统,用于动态配置和管理内核对象。它提供了一种以文件和目录的形式访问内核对象的接口,允许用户在运行时添加,修改和删除内核对象,而无需重新编译内核或重新启动系统。ConfigFS 常用于配置和管理设备,驱动程序和子系统。

这些虚拟文件系统在功能上有一些区别,如下所示:

  • Procfs 主要用于访问和管理进程信息,提供了有关进程,内核参数和系统状态的信息。
  • Sysfs 主要用于表示和配置系统中的设备,驱动程序和其他内核对象,提供了一种统一的接口来访问和控制这些对象的属性和状态。
  • Configfs 主要用于动态配置和管理内核对象,提供了一种以文件和目录的形式访问内核对象的接口,允许在运行时添加,修改和删除内核对象。

74.2 设备树插件选择ConfigFS 原因

在上个章节,我们学习了设备树插件的使用,从而理出设备树插件的实现方式,

通过上节课设备树插件的使用我们可以理出设备树的插件的实现方式,如下图(图74-1)所示:

image-20240829110759977

要实现上述的功能,用户空间需要和内核进行交互,也就是将dtbo 加载到内存里面去。sysfs 虚拟文件系统的作用是将内核中的数据、属性以文件的方式导出到用户空间。导出到用户空间以后,读这些文件表示读取设备的文件,写这些文件就表示控制设备。configfs 作用的英文解释为Userspace-driven kernel object configuration,翻译过来就是用户空间配置内核对象。

所以configfs 与sysfs 恰恰相反,sysfs 导出内核对象给用户空间,configfs 是从用户空间去配置内核对象,并且不需要重新编译内核或者修改内核代码。所以configfs 更适合设备树插件这个技术。

选择ConfigFS 虚拟文件系统作为设备树插件的实现方式,可以满足设备树插件对动态性、灵活性的需求,使得设备树的配置和管理更加方便和高效。

第75 章ConfigFS 的核心数据结构

在前面的章节中,我们对于ConfigFS 有了一个感性的认识,本章节将进一步深入,探讨ConfigFS 的核心数据结构,这将为我们理解ConfigFS 的内部工作原理提供基础。让我们一起深入学习ConfigFS 的核心数据结构吧!

75.1 关键数据结构

接下来我们将要学习如何创建内核对象,然后生成对应的文件和目录。
在创建之前,我们先要了解下和ConfigFs 文件系统相关的几个核心数据结构。
ConfigFS 的核心数据结构主要包括以下几个部分:

  • configfs_subsystem:configfs_subsystem 是一个顶层的数据结构,用于表示整个ConfigFS 子系统。它包含了根配置项组的指针,以及ConfigFS 的其他属性和状态信息。
  • config_group:config_group 是一种特殊类型的配置项,表示一个配置项组。它可以包含一组相关的配置项,形成一个层次结构。config_group 结构包含了父配置项的指针,以及指向子配置项的链表。
  • config_item:这是ConfigFS 中最基本的数据结构,用于表示一个配置项。每个配置项都是一个内核对象,可以是设备、驱动程序、子系统等。config_item 结构包含了配置项的类型、名称、属性、状态等信息,以及指向父配置项和子配置项的指针。

这些数据结构之间的关系可以形成一个树形结构,其中configfs_subsystem 是根节点,config_group 表示配置项组,config_item 表示单个配置项。子配置项通过链表连接在一起,形成父子关系。如下表(图75-1)所示:

image-20240829111135116

75.2 子系统、容器和config_item

本小节我们来了解下子系统,容器,config_item 的结构体。
configfs_subsystem 结构体,如下所示:

struct configfs_subsystem {
    struct config_group su_group;
    struct mutex su_mutex;
};

configfs_subsystem 结构体中包含config_group 结构体,config_group 结构体如下所示:

struct config_group {
    struct config_item cg_item;
    struct list_head cg_children;
    struct configfs_subsystem *cg_subsys;
    struct list_head default_groups;
    struct list_head group_entry;
};

config_group 结构体中包含config_item 结构体,config_item 结构体如下所示:

struct config_item {
    char *ci_name;
    char ci_namebuf[CONFIGFS_ITEM_NAME_LEN]; //目录的名字
    struct kref ci_kref;
    struct list_head ci_entry;
    struct config_item *ci_parent;
    struct config_group *ci_group;
    const struct config_item_type *ci_type; //目录下属性文件和属性操作
    struct dentry *ci_dentry;
};

接下来我们来分析设备树插件驱动代码,如下(图75-2)所示:

image-20240829111310452

这段代码定义了一个名为dtbocfg_root_subsysconfigfs_subsystem 结构体实例,表示ConfigFS 中的一个子系统。
首先,dtbocfg_root_subsys.su_group 是一个config_group 结构体,它表示子系统的根配置项组。在这里,该结构体的.cg_item 字段表示根配置项组的基本配置项。

  • .ci_namebuf = "device-tree":配置项的名称设置为”device-tree”,表示该配置项的名称为”device-tree”。
  • .ci_type = &dtbocfg_root_type:配置项的类型设置为dtbocfg_root_type,这是一个自定义的配置项类型。

接下来,.su_mutex 字段是一个互斥锁,用于保护子系统的操作。在这里,使用了__MUTEX_INITIALIZER 宏来初始化互斥锁。
通过这段代码,创建了一个名为”device-tree”的子系统,它的根配置项组为空。可以在该子系统下添加更多的配置项和配置项组,用于动态配置和管理设备树相关的内核对象。Linux系统下创建了device-tree 这个子系统,如下图(图75-3)所示:

image-20240829111606061

接下来继续分析设备树插件驱动代码中注册配置项组的部分,如下图(图75-4)所示:

image-20240829111616319

这段代码是一个初始化函数dtbocfg_module_init(),用于初始化和注册ConfigFS 子系统和配置项组。首先,通过config_group_init()函数初始化了dtbocfg_root_subsys.su_group,即子系统的根配置项组。接下来,使用config_group_init_type_name()函数初始化了dtbocfg_overlay_group,表示名为”overlays”的配置项组,并指定了配置项组的类型为dtbocfg_overlays_type,这是一个自定义的配置项类型。然后,调用configfs_register_subsystem()
函数注册了dtbocfg_root_subsys 子系统。如果注册失败,将打印错误信息,并跳转到register_subsystem_failed 标签处进行错误处理。接着,调用configfs_register_group()函数注册了dtbocfg_overlay_group 配置项组,并将其添加到dtbocfg_root_subsys.su_group 下。如果注册失败,同样会打印错误信息,并跳转到register_group_failed 标签处进行错误处理。最后,如果所有的注册过程都成功,将打印”OK”消息,并返回0,表示初始化成功。如果在注册配置项组失败时,会先调用configfs_unregister_subsystem()函数注销之前注册的子系统,然后返回注册失败的错误码retval。

这段代码的作用是初始化和注册一个名为”device-tree”的ConfigFS 子系统,并在其下创建一个名为”overlays”的配置项组。Linux 系统下,在device-tree 子系统下创建了overlays 容器,如下图(图75-5)所示:

image-20240829111758628

75.3 属性和方法

我们要在容器下放目录或属性文件,所以我们看一下config_item 结构体,如下所示:

struct config_item {
    char *ci_name;
    char ci_namebuf[CONFIGFS_ITEM_NAME_LEN]; //目录的名字
    struct kref ci_kref;
    struct list_head ci_entry;
    struct config_item *ci_parent;
    struct config_group *ci_group;
    const struct config_item_type *ci_type; //目录下属性文件和属性操作
    struct dentry *ci_dentry;
};

config_item 结构体中包含了config_item_type 结构体,config_item_type 结构体如下所示:

struct config_item_type {
    struct module *ct_owner;
    struct configfs_item_operations *ct_item_ops; //item(目录)的操作方法
    struct configfs_group_operations *ct_group_ops; //group(容器)的操作方法
    struct configfs_attribute **ct_attrs; //属性文件的操作方法
    struct configfs_bin_attribute **ct_bin_attrs; //bin 属性文件的操作方法
};

config_item_type 结构体中包含了struct configfs_item_operations 结构体,如下所示:

struct configfs_item_operations {
    //删除item 方法,在group 下面使用rmdir 命令会调用这个方法
    void (*release)(struct config_item *);
    int (*allow_link)(struct config_item *src, struct config_item *target);
    void (*drop_link)(struct config_item *src, struct config_item *target);
};

config_item_type 结构体中包含了struct configfs_group_operations 结构体,如下所示:

struct configfs_group_operations {
    //创建item 的方法,在group 下面使用mkdir 命令会调用这个方法
    struct config_item *(*make_item)(struct config_group *group, const char *name);
    //创建group 的方法
    struct config_group *(*make_group)(struct config_group *group, const char *name);
    int (*commit_item)(struct config_item *item);
    void (*disconnect_notify)(struct config_group *group, struct config_item *item);
    void (*drop_item)(struct config_group *group, struct config_item *item);
};

config_item_type 结构体中包含了struct configfs_attribute 结构体,如下所示:

struct configfs_attribute {
const char *ca_name; 			// 属性文件的名字
struct module *ca_owner; 		// 属性文件文件的所属模块
umode_t ca_mode; 			   // 属性文件访问权限
    
// 读写方法的函数指针,具体功能需要自行实现。
ssize_t (*show)(struct config_item *, char *);
ssize_t (*store)(struct config_item *, const char *, size_t); };

75.4 总结

在上面几个小节中,对ConfigFS 的核心数据结构做出了详细的解释,本小节我们来总结一下。这些核心数据结构相互关联,通过在ConfigFS 层级结构进行组织和管理,使得设备的配置和管理更加灵活和可定制。如下图(图75-6)所示:

image-20240902112626884

理解ConfigFS 的核心数据结构对于深入使用和定制ConfigFS 非常重要,可以帮助开发者更好地进行设备的配置和管理,提高系统的灵活性和可扩展性。如果大家还有不理解的地方,建议反复观看视频学习。

第76 章注册configfs 子系统实验

在上个章节中,我们深入学习了configfs 的核心数据结构,我们理解了它们在ConfigFS 中的层级关系,以及他们如何用于设备的动态配置和管理。本章节我们将以实验的方式来应用我们所学的知识,自己编写一个设备树插件驱动,实现注册一个configfs 子系统。接下来我们开始编写驱动吧!

76.1 实验程序的编写

76.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\59_configfs_subsystem\module
我们编写驱动代码创建一个名为“myconfigfs”的configfs 子系统,并将其注册到内核中。编写完成的configfs_subsystem.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>

//定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type ={
  .ct_owner = THIS_MODULE,
  .ct_item_ops = NULL,
  .ct_group_ops = NULL,
  .ct_attrs = NULL,
};

//定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem ={
  .su_group = {
    .cg_item = {
      .ci_namebuf = "myconfigfs",
      .ci_type = &myconfig_item_type,
    },
  },

};

//模块的初始化函数
static int myconfigfs_init(void)
{
  //初始化配置组
  config_group_init(&myconfigfs_subsystem.su_group);
  //注册子系统
  configfs_register_subsystem(&myconfigfs_subsystem);
  return 0;
}

// 模块退出函数
static void myconfigfs_exit(void)
{
  configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfigfs_init); // 指定模块的初始化函数
module_exit(myconfigfs_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

76.2 运行测试

76.2.1 编译驱动程序

在上一小节中的configfs_subsystem.c 代码同一目录下创建Makefile 文件(记得用交叉编译的GCC),Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成configfs_subsystem.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

76.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图76-4)所示:

insmod configfs_subsystem.ko

image-20240902115204071

驱动加载之后,我们进入/sys/kernel/config 目录下,可以看到注册生成的myconfigfs 子系统,如下图(图76-5)所示:

image-20240902115223958

最后可以使用以下命令进行驱动的卸载,如下图(图76-6)所示:

rmmod configfs_subsystem

image-20240902115409045

至此,注册configfs 子系统实验就完成了。

第77 章注册group 容器实验

上一章节,我们编写驱动程序注册了一个configfs 子系统,本章节在上个章节的基础上进行注册group 容器的实验。

77.1 实验程序的编写

77.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\60_config_group\module。
本章编写的驱动文件在上个章节驱动文件的基础上进行编写。我们接下来注册group 容器实验。在这个实验中,我们将创建一个配置组,并将其注册到ConfigFS 子系统中,编写完成的config_group.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>

// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;
// 定义一个名为"mygroup_config_item_type"的config_item_type结构体,用于描述配置项类型。
static const struct config_item_type mygroup_config_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_item_ops = NULL,
    .ct_group_ops = NULL,
    .ct_attrs = NULL,
};

// 定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = NULL,
};

// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
    .su_group = {
        .cg_item = {
            .ci_namebuf = "myconfigfs",
            .ci_type = &myconfig_item_type,
        },
    },
};

// 模块的初始化函数
static int myconfig_group_init(void)
{
  // 初始化配置组
  config_group_init(&myconfigfs_subsystem.su_group);
  // 注册子系统
  configfs_register_subsystem(&myconfigfs_subsystem);

  // 初始化配置组"mygroup"
  config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
  // 在子系统中注册配置组"mygroup"
  configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
  return 0;
}

// 模块退出函数
static void myconfig_group_exit(void)
{
  // 注销子系统
  configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

77.2 运行测试

77.2.1 编译驱动程序
在上一小节中的config_group.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成config_group.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

77.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图77-4)所示:

insmod config_group.ko

image-20240902115725297

驱动加载之后,我们进入/sys/kernel/config 目录下,可以看到注册生成的myconfigfs 子系统,如下图所示:

image-20240902115743272

然后我们进入注册生成的myconfigfs 子系统,如下图所示,可以看到注册生成的mygroup容器。

image-20240902115802055

最后可以使用以下命令进行驱动的卸载,如下图(图77-7)所示:

rmmod config_group

image-20240902115815988

至此,注册group 容器实验就完成了。

第78 章用户空间创建item 实验

通过前面的学习,我们已经成功在/sys/kernel/config/目录下创建了myconfigfs 子系统,并在这个子系统下创建了mygroup 容器,但是mygroup 容器下不能使用mkdir 创建item。本章节我们来学习用代码实现用mkdir 命令创建item 功能。

78.1 实验程序的编写

78.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\61_mkdir_item\module。
本章编写的驱动文件在上个章节驱动文件的基础上进行编写。我们编写驱动程序,实现用mkdir 命令创建item 功能。编写完成的mkdir_item.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>

// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;

// 自定义的配置项结构体
struct myitem
{
    struct config_item item;
};

// 配置项释放函数
void myitem_release(struct config_item *item)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    kfree(myitem);
    printk("%s\n", __func__);
}

// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
    .release = myitem_release,
};

// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_item_ops = &myitem_ops,
};

// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
    struct myitem *myconfig_item;
    printk("%s\n", __func__);
    myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
    config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
    return &myconfig_item->item;
}

// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
    .make_item = mygroup_make_item,
};

// 定义名为"mygroup_config_item_type"的config_item_type结构体,用于描述配置项类型。
static const struct config_item_type mygroup_config_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = &mygroup_ops,
};

// 定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = NULL,
};

// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
    .su_group = {
        .cg_item = {
            .ci_namebuf = "myconfigfs",
            .ci_type = &myconfig_item_type,
        },
    },
};

// 模块的初始化函数
static int myconfig_group_init(void)
{
    // 初始化配置组
    config_group_init(&myconfigfs_subsystem.su_group);
    // 注册子系统
    configfs_register_subsystem(&myconfigfs_subsystem);

    // 初始化配置组"mygroup"
    config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
    // 在子系统中注册配置组"mygroup"
    configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
    return 0;
}

// 模块退出函数
static void myconfig_group_exit(void)
{
    // 注销子系统
    configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

78.2 运行测试

78.2.1 编译驱动程序

在上一小节中的mkdir_item.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成mkdir_item.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

78.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图78-4)所示:

insmod mkdir_item.ko

image-20240902120048909

驱动加载之后,我们进入/sys/kernel/config 目录下,可以看到注册生成的myconfigfs 子系统,如下图(图78-5)所示:

image-20240902120108259

然后我们进入注册生成的myconfigfs 子系统,如下图(图78-6)所示,可以看到注册生成的mygroup 容器。

image-20240902120255651

然后输入“ mkdir test ” 命令创建config_item , 如下图所示, 创建成功之后, 打印“mygroup_make_item”,说明驱动程序中mygroup_make_item 函数成功执行。

image-20240902135536252

输入“rmdir test”命令删除item,如下图(图78-8)所示:

image-20240902135602738

最后可以使用以下命令进行驱动的卸载,如下图(图78-9)所示:

rmmod mkdir_item

image-20240902135619949

至此,用户空间创建item 实验就完成了。

第79 章完善drop 和release 函数实验

当我们在命令行使用rmdir 命令删除item 时,会执行驱动中的release 函数,本章节来学习一个新函数——drop 函数。一起开始本章的学习吧。

79.1 release 和drop 函数的区别

release.drop_item 是两个不同的成员字段,用于不同的目的:
.release 成员字段是在struct config_item_type 结构体中定义的一个回调函数指针。它指向一个函数,当configfs 中的配置项被释放或删除时,内核会调用该函数来执行相应的资源释放操作。它通常用于释放与配置项相关的资源,比如释放动态分配的内存、关闭打开的文件描述符等。
.drop_item 是在struct configfs_group_operations 结构体中定义的一个回调函数指针。它指向一个函数,当configfs 中的配置组(group)被删除时,内核会调用该函数来处理与配置组相关的操作。这个函数通常用于清理配置组的状态、释放相关的资源以及执行其他必要的清理操作。.drop_item 函数在删除配置组时被调用,而不是在删除单个配置项时被调用。

.release 成员字段用于配置项的释放操作,而.drop_item 成员字段用于配置组的删除操作。它们分别在不同的上下文中执行不同的任务,但都与资源释放和清理有关。

79.2 实验程序的编写

79.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\62_drop\module。
本章编写的驱动文件在上个章节驱动文件的基础上进行编写。在驱动程序中实现了删除配置项的函数,它会释放配置项相关的资源。编写完成的drop.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>

// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;

// 自定义的配置项结构体
struct myitem{
    struct config_item item;
};

// 配置项释放函数
void myitem_release(struct config_item *item){
    struct myitem *myitem = container_of(item, struct myitem, item);
    kfree(myitem);
    printk("%s\n", __func__);
}

// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
    .release = myitem_release,
};

// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_item_ops = &myitem_ops,
};

// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name){
    struct myitem *myconfig_item;
    printk("%s\n", __func__);
    myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
    config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
    return &myconfig_item->item;
}

// 删除配置项函数
void mygroup_delete_item(struct config_group *group, struct config_item *item){
    struct myitem *myitem = container_of(item, struct myitem, item);
    config_item_put(&myitem->item);
    printk("%s\n", __func__);
}

// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
    .make_item = mygroup_make_item,
    .drop_item = mygroup_delete_item,
};

// 定义名为"mygroup_config_item_type"的config_item_type结构体,用于描述配置项类型。
static const struct config_item_type mygroup_config_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = &mygroup_ops,
};

// 定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = NULL,
};

// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
    .su_group = {
        .cg_item = {
            .ci_namebuf = "myconfigfs",
            .ci_type = &myconfig_item_type,
        },
    },
};

// 模块的初始化函数
static int myconfig_group_init(void){
    // 初始化配置组
    config_group_init(&myconfigfs_subsystem.su_group);
    // 注册子系统
    configfs_register_subsystem(&myconfigfs_subsystem);

    // 初始化配置组"mygroup"
    config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
    // 在子系统中注册配置组"mygroup"
    configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
    return 0;
}

// 模块退出函数
static void myconfig_group_exit(void){
    // 注销子系统
    configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

79.3 运行测试

79.3.1 编译驱动程序

在上一小节中的drop.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成drop.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

79.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图79-4)所示:

insmod drop.ko

image-20240903110120924

驱动加载之后,我们进入/sys/kernel/config 目录下,可以看到注册生成的myconfigfs 子系统,如下图(图79-5)所示:

image-20240903110048092

然后我们进入注册生成的myconfigfs 子系统,如下图(图79-6)所示,可以看到注册生成的mygroup 容器。

image-20240903111155102

然后输入“mkdir test”命令创建config_item,如下图(图79-7)所示,创建成功之后,打印“mygroup_make_item”。

image-20240903111358875

输入“ rmdir test ” 命令删除item, 如下图所示, 执行rmdir 命令之后, 依次执行了mygroup_delete_item 函数和myitem_release 函数。

image-20240903111427078

最后可以使用以下命令进行驱动的卸载,如下图(图79-9)所示:

rmmod mkdir_item

image-20240903111438313

至此,完善drop 和release 函数实验就完成了。

第80 章注册attribute 实验

在前面的几章实验中,我们编写驱动程序,实现了注册ConfigFS 子系统,注册group 容器,支持使用mkdir 命令创建item,完善了drop 和release 函数的功能。在之前的实验中,我们成功创建了item,但是item 下面没有创建属性和操作项,那么本章节我们来学习如何注册属性。

80.1 实验程序的编写

80.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\63_attr\module。
本章编写的驱动文件在上个章节驱动文件的基础上进行编写。驱动实现在item 目录下生成属性read 和write,并对read 属性进行读取和对write 属性进行写入。编写完成的attr.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>

// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;

// 自定义的配置项结构体
struct myitem
{
    struct config_item item;
    int size;
    void *addr;
};

// 配置项释放函数
void myitem_release(struct config_item *item)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    kfree(myitem);
    printk("%s\n", __func__);
};

// 读取配置项内容的回调函数
ssize_t myread_show(struct config_item *item, char *page)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    memcpy(page, myitem->addr, myitem->size);
    printk("%s\n", __func__);
    return myitem->size;
};

// 写入配置项内容的回调函数
ssize_t mywrite_store(struct config_item *item, const char *page, size_t size)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    myitem->addr = kmemdup(page, size, GFP_KERNEL);
    myitem->size = size;
    printk("%s\n", __func__);
    return myitem->size;
};

// 创建只读配置项
CONFIGFS_ATTR_RO(my, read);
// 创建只写配置项
CONFIGFS_ATTR_WO(my, write);

// 配置项属性数组
struct configfs_attribute *my_attrs[] = {
    &myattr_read,
    &myattr_write,
    NULL,
};

// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
    .release = myitem_release,
};

// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_item_ops = &myitem_ops,
    .ct_attrs = my_attrs,
};

// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
    struct myitem *myconfig_item;
    printk("%s\n", __func__);
    myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
    config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
    return &myconfig_item->item;
}

// 删除配置项函数
void mygroup_delete_item(struct config_group *group, struct config_item *item)
{
    struct myitem *myitem = container_of(item, struct myitem, item);

    config_item_put(&myitem->item);
    printk("%s\n", __func__);
}

// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
    .make_item = mygroup_make_item,
    .drop_item = mygroup_delete_item,
};

// 配置项类型结构体
static const struct config_item_type mygroup_config_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = &mygroup_ops,
};

// 配置项类型结构体
static const struct config_item_type myconfig_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = NULL,
};

// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
    .su_group = {
        .cg_item = {
            .ci_namebuf = "myconfigfs",
            .ci_type = &myconfig_item_type,
        },
    },
};

// 模块的初始化函数
static int myconfig_group_init(void)
{
    // 初始化配置组
    config_group_init(&myconfigfs_subsystem.su_group);
    // 注册子系统
    configfs_register_subsystem(&myconfigfs_subsystem);

    // 初始化配置组"mygroup"
    config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
    // 在子系统中注册配置组"mygroup"
    configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
    return 0;
}

// 模块退出函数
static void myconfig_group_exit(void){
    // 注销子系统
    configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

80.2 运行测试

80.2.1 编译驱动程序

在上一小节中的attr.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成attr.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

80.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图80-4)所示:

insmod attr.ko

image-20240903111856132

驱动加载之后,我们进入/sys/kernel/config 目录下,可以看到注册生成的myconfigfs 子系统,如下图(图80-5)所示:

image-20240903111918215

然后我们进入注册生成的myconfigfs 子系统,如下图(图80-6)所示,可以看到注册生成的mygroup 容器。

image-20240903111933903

然后输入“mkdir test”命令创建config_item,如下图(图80-7)所示,创建成功之后,打印“mygroup_make_item”。

image-20240903112000137

然后进入到test 目录下,如下图(图80-8)所示。有生成的属性:read 和write

image-20240903112014620

我们输入以下命令对属性进行读写操作,如下图(图80-9)所示:

cat read
echo 1 > write

image-20240903112033481

在上图中,我们分别对属性read 和write 进行读写操作后,分别打印“myread_show”和“mywrite_store”。
输入“rmdir test”命令删除item,如下图(图80-10)所示:

image-20240903112058501

最后可以使用以下命令进行驱动的卸载,如下图(图80-11)所示:

rmmod attr

image-20240903112115971

至此,注册attribute 实验就完成了。

第81 章实现多级目录实验

经过前面理论的学习,我们了解到一个配置组也就是(group)可以包含多个配置项(item)和子配置组。配置项和配置组都是作为配置组的成员存在的。配置项和配置组之间通过指针进行关联,以形成一个层次结构。本章节我们来学习在设备树插件驱动程序中实现多级目录。

81.1 实验程序的编写

81.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\64_make_group\module。
编写完成的make_group.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>

// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;

// 自定义的配置项结构体
struct myitem
{
    struct config_item item;
    int size;
    void *addr;
};
// 自定义的配置组结构体
struct mygroup
{
    struct config_group group;
};

// 配置项释放函数
void myitem_release(struct config_item *item)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    kfree(myitem);
    printk("%s\n", __func__);
};

// 读取配置项内容的回调函数
ssize_t myread_show(struct config_item *item, char *page)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    memcpy(page, myitem->addr, myitem->size);
    printk("%s\n", __func__);
    return myitem->size;
};

// 写入配置项内容的回调函数
ssize_t mywrite_store(struct config_item *item, const char *page, size_t size)
{
    struct myitem *myitem = container_of(item, struct myitem, item);
    myitem->addr = kmemdup(page, size, GFP_KERNEL);
    myitem->size = size;
    printk("%s\n", __func__);
    return myitem->size;
};

// 创建只读配置项
CONFIGFS_ATTR_RO(my, read);
// 创建只写配置项
CONFIGFS_ATTR_WO(my, write);

// 配置项属性数组
struct configfs_attribute *my_attrs[] = {
    &myattr_read,
    &myattr_write,
    NULL,
};

// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
    .release = myitem_release,
};

// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_item_ops = &myitem_ops,
    .ct_attrs = my_attrs,
};

// 配置组类型结构体
static struct config_item_type mygroup_type = {
    .ct_owner = THIS_MODULE,

};

// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
    struct myitem *myconfig_item;
    printk("%s\n", __func__);
    myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
    config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
    return &myconfig_item->item;
}

// 删除配置项函数
void mygroup_delete_item(struct config_group *group, struct config_item *item)
{
    struct myitem *myitem = container_of(item, struct myitem, item);

    config_item_put(&myitem->item);
    printk("%s\n", __func__);
}
// 创建配置组函数
struct config_group *mygroup_make_group(struct config_group *group, const char *name)
{
    struct mygroup *mygroup;
    printk("%s\n", __func__);
    mygroup = kzalloc(sizeof(*mygroup), GFP_KERNEL);
    config_group_init_type_name(&mygroup->group, name, &mygroup_type);
    return &mygroup->group;
};

// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
    .make_item = mygroup_make_item,
    .drop_item = mygroup_delete_item,
    .make_group = mygroup_make_group,
};

// 配置项类型结构体
static const struct config_item_type mygroup_config_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = &mygroup_ops,
};

// 配置项类型结构体
static const struct config_item_type myconfig_item_type = {
    .ct_owner = THIS_MODULE,
    .ct_group_ops = NULL,
};

// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
    .su_group = {
        .cg_item = {
            .ci_namebuf = "myconfigfs",
            .ci_type = &myconfig_item_type,
        },
    },
};

// 模块的初始化函数
static int myconfig_group_init(void)
{
    // 初始化配置组
    config_group_init(&myconfigfs_subsystem.su_group);
    // 注册子系统
    configfs_register_subsystem(&myconfigfs_subsystem);

    // 初始化配置组"mygroup"
    config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
    // 在子系统中注册配置组"mygroup"
    configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
    return 0;
}

// 模块退出函数
static void myconfig_group_exit(void){
    // 注销子系统
    configfs_unregister_subsystem(&myconfigfs_subsystem);
}

module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

81.2 运行测试

81.2.1 编译驱动程序

在上一小节中的make_group.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成make_group.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

81.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图81-4)所示:

insmod make_group.ko

image-20240903112339582

驱动加载之后,我们进入/sys/kernel/config 目录下,可以看到注册生成的myconfigfs 子系统,如下图(图81-5)所示:

image-20240903112428425

然后我们进入注册生成的myconfigfs 子系统,如下图(图81-6)所示,可以看到注册生成的mygroup 容器。

image-20240903112442838

然后输入“mkdir test”命令创建config_item,如下图(图81-7)所示,创建成功之后,打印“mygroup_make_group”。我们进入创建的group——test 目录下,此时可以在test 目录下创建item——test2。但由于在驱动中我们并没有实现在group 下创建item 功能,所以会提示创建test2 没有权限。

image-20240903112514261

输入“rmdir test”命令删除group,如下图(图81-8)所示:

image-20240903112523779

最后可以使用以下命令进行驱动的卸载,如下图(图81-9)所示:

rmmod make_group

image-20240903112540022

至此,实现多级目录实验就完成了。

第82 章移植设备树插件驱动实验

在73 章节,我们学会了如何使用设备树插件。在本章节中,我们将探讨如何移植设备树插件到iTOP-RK3568 开发板上。移植设备树插件主要包括以下几个步骤

  • 1 配置内核支持挂载configfs 虚拟文件系统。
  • 2 配置内核支持设备树插件
  • 3 移植设备树插件驱动

接下来开始移植设备树插件驱动吧!

82.1 挂载configfs 虚拟文件系统

首先我们打开Linux 内核源码,输入以下命令打开menuconfig 配置界面。

image-20240903113043059

界面打开之后,将下图(图82-2)中的选项勾选。

image-20240903113055474

勾选之后保存退出,然后输入以下命令

cp .config arch/arm64/configs/rockchip_linux_defconfig
cd ../
./build.sh kernel

将编译之后的内核镜像烧写到开发板上,接着使用mount 命令检查configfs 虚拟文件系统是否挂载成功。挂载成功如下图(图82-3)所示:

image-20240903113139910

如果系统没有自动挂载configfs 虚拟文件系统,需要输入以下命令挂载:

mount -t configfs none /sys/kernel/config

82.2 配置内核支持设备树插件

首先我们打开Linux 内核源码,输入以下命令打开menuconfig 配置界面。

image-20240903113207356

界面打开之后,将下图中的选项勾选。

image-20240903113225795 image-20240903113241834

勾选之后保存退出,然后输入以下命令

cp .config arch/arm64/configs/rockchip_linux_defconfig
cd ../
./build.sh kernel

内核编译成功之后,接下来我们开始移植设备树插件驱动。

82.3 移植驱动

现在我们已经学完了configfs 虚拟文件系统的数据结构。如果水平足够,完全可以自己编写驱动实现一个设备树插件。但是我们没有必要重复造轮子,github 上有大神编写好的设备树插件驱动,为了方便大家使用,放在了网盘资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\65_dtbocfg”目录下,如下图(图82-7)所示:

image-20240903113338576

dtbocfg.c 是设备树插件驱动,我们只要将此驱动编译成驱动模块或者编译进内核即可。在网盘资料中提供了编译好的dtbocfg.ko 文件。
好了,设备树插件驱动移植完毕,设备树插件的使用,可以查看本手册73 章节。

/*********************************************************************************
 *
 *       Copyright (C) 2016-2021 Ichiro Kawazome
 *       All rights reserved.
 * 
 *       Redistribution and use in source and binary forms, with or without
 *       modification, are permitted provided that the following conditions
 *       are met:
 * 
 *         1. Redistributions of source code must retain the above copyright
 *            notice, this list of conditions and the following disclaimer.
 * 
 *         2. Redistributions in binary form must reproduce the above copyright
 *            notice, this list of conditions and the following disclaimer in
 *            the documentation and/or other materials provided with the
 *            distribution.
 * 
 *       THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *       "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *       LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *       A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
 *       OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *       SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *       LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *       DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *       THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 *       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *       OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 ********************************************************************************/
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_fdt.h>
#include <linux/configfs.h>
#include <linux/types.h>
#include <linux/stat.h>
#include <linux/limits.h>
#include <linux/file.h>
#include <linux/version.h>

/**
 * Device Tree Overlay Item Structure
 */
struct dtbocfg_overlay_item {
    struct config_item	    item;
#if (LINUX_VERSION_CODE < 0x041100)
    struct device_node*     node;
#endif
    int                     id;
    void*                   dtbo;
    int                     dtbo_size;
};

/**
 * dtbocfg_overlay_create() - Create Device Tree Overlay
 * @overlay: Pointer to Device Tree Overlay Item
 * return    Success(0) or Error Status.
 */
static int dtbocfg_overlay_item_create(struct dtbocfg_overlay_item *overlay)
{
    int ret_val;

#if (LINUX_VERSION_CODE >= 0x041100)
    {
        int ovcs_id = 0;

        ret_val = of_overlay_fdt_apply(overlay->dtbo,overlay->dtbo_size, &ovcs_id);
        if (ret_val != 0) {
            pr_err("%s: Failed to apply overlay (ret_val=%d)\n", __func__, ret_val);
            goto failed;
        }
        overlay->id = ovcs_id;
        pr_debug("%s: apply OK(id=%d)\n", __func__, ovcs_id);
    }
#else
    
#if (LINUX_VERSION_CODE >= 0x040700)
    of_fdt_unflatten_tree(overlay->dtbo, NULL, &overlay->node);
#else
    of_fdt_unflatten_tree(overlay->dtbo, &overlay->node);
#endif
    if (overlay->node == NULL) {
        pr_err("%s: failed to unflatten tree\n", __func__);
        ret_val = -EINVAL;
        goto failed;
    }
    pr_debug("%s: unflattened OK\n", __func__);

#if (LINUX_VERSION_CODE >= 0x040F00)
    {
        int ovcs_id = 0;

        ret_val = of_overlay_apply(overlay->node, &ovcs_id);
        if (ret_val != 0) {
            pr_err("%s: Failed to apply overlay (ret_val=%d)\n", __func__, ret_val);
            goto failed;
        }
        overlay->id = ovcs_id;
        pr_debug("%s: apply OK(id=%d)\n", __func__, ovcs_id);
    }
#else
    {
        of_node_set_flag(overlay->node, OF_DETACHED);

        ret_val = of_resolve_phandles(overlay->node);
        if (ret_val != 0) {
            pr_err("%s: Failed to resolve tree\n", __func__);
            goto failed;
        }
        pr_debug("%s: resolved OK\n", __func__);

        ret_val = of_overlay_create(overlay->node);
        if (ret_val < 0) {
            pr_err("%s: Failed to create overlay (ret_val=%d)\n", __func__, ret_val);
            goto failed;
        }
        overlay->id = ret_val;
    }
#endif

#endif
    pr_debug("%s: create OK\n", __func__);
    return 0;

  failed:
    return ret_val;
}

/**
 * dtbocfg_overlay_item_release() - Relase Device Tree Overlay
 * @overlay: Pointer to Device Tree Overlay Item
 * return    none
 */
static void dtbocfg_overlay_item_release(struct dtbocfg_overlay_item *overlay)
{
    if (overlay->id >= 0) {
#if (LINUX_VERSION_CODE >= 0x040F00)
        of_overlay_remove(&overlay->id);
#else        
        of_overlay_destroy(overlay->id);
#endif        
        overlay->id = -1;
    }
}

/**
 * container_of_dtbocfg_overlay_item() - Get Device Tree Overlay Item Pointer from Configuration Item
 * @item:  Pointer to Configuration Item
 * return  Pointer to Device Tree Overlay Item
 */
static inline struct dtbocfg_overlay_item* container_of_dtbocfg_overlay_item(struct config_item *item)
{
    return item ? container_of(item, struct dtbocfg_overlay_item, item) : NULL;
}

/**
 * dtbocfg_overlay_item_status_store() - Set Status Attibute
 * @item:  Pointer to Configuration Item
 * @page:  Pointer to Value Buffer
 * @count: Size of Value Buffer Size
 * return  Stored Size or Error Status.
 */
static ssize_t dtbocfg_overlay_item_status_store(struct config_item *item, const char *buf, size_t count)
{
    struct dtbocfg_overlay_item *overlay = container_of_dtbocfg_overlay_item(item);
    ssize_t       status;
    unsigned long value;
    if (0 != (status = kstrtoul(buf, 10, &value))) {
        goto failed;
    }
    if (value == 0) {
        if (overlay->id >= 0) {
            dtbocfg_overlay_item_release(overlay);
        }
    } else {
        if (overlay->id  < 0) {
            dtbocfg_overlay_item_create(overlay);
        }
    }
    return count;
  failed:
    return -EPERM;
}

/**
 * dtbocfg_overlay_item_status_show() - Show Status Attibute
 * @item : Pointer to Configuration Item
 * @page : Pointer to Value for Store
 * return  String Size or Error Status.
 */
static ssize_t dtbocfg_overlay_item_status_show(struct config_item *item, char *page)
{
    struct dtbocfg_overlay_item *overlay = container_of_dtbocfg_overlay_item(item);
    return sprintf(page, "%d\n", overlay->id >= 0 ? 1 : 0);
}

/**
 * dtbocfg_overlay_item_dtbo_write() - Write Device Tree Blob to Configuration Item
 * @item : Pointer to Configuration Item
 * @page : Pointer to Value Buffer 
 * @count: Size of Value Buffer
 * return  Stored Size or Error Status.
 */
static ssize_t dtbocfg_overlay_item_dtbo_write(struct config_item *item, const void *buf, size_t count)
{
    struct dtbocfg_overlay_item *overlay = container_of_dtbocfg_overlay_item(item);

    if (overlay->dtbo_size > 0) {
        if (overlay->id >= 0) {
            return -EPERM;
        }
        kfree(overlay->dtbo);
        overlay->dtbo      = NULL;
        overlay->dtbo_size = 0;
    }

    overlay->dtbo = kmemdup(buf, count, GFP_KERNEL);
    if (overlay->dtbo == NULL) {
        overlay->dtbo_size = 0;
        return -ENOMEM;
    } else {
        overlay->dtbo_size = count;
        return count;
    }
}

/**
 * dtbocfg_overlay_item_dtbo_read() - Read Device Tree Blob from Configuration Item
 * @item : Pointer to Configuration Item
 * @page : Pointer to Value for Store, or NULL to query the buffer size
 * @size : Size of the supplied buffer
 * return  Read Size
 */
static ssize_t dtbocfg_overlay_item_dtbo_read(struct config_item *item, void *buf, size_t size)
{
    struct dtbocfg_overlay_item *overlay = container_of_dtbocfg_overlay_item(item);

    if (overlay->dtbo == NULL)
        return 0;

    if (buf != NULL)
        memcpy(buf, overlay->dtbo, overlay->dtbo_size);

    return overlay->dtbo_size;
}

/**
 * Device Tree Blob Overlay Attribute Structure
 */
CONFIGFS_BIN_ATTR(dtbocfg_overlay_item_, dtbo, NULL, 1024 * 1024); // 1MiB should be way more than enough
CONFIGFS_ATTR(dtbocfg_overlay_item_, status);

static struct configfs_attribute *dtbocfg_overlay_attrs[] = {
    &dtbocfg_overlay_item_attr_status,
    NULL,
};

static struct configfs_bin_attribute *dtbocfg_overlay_bin_attrs[] = {
    &dtbocfg_overlay_item_attr_dtbo,
    NULL,
};

/**
 * dtbocfg_overlay_release() - Release Device Tree Overlay Item
 * @item : Pointer to Configuration Item
 * Return  None
 */
static void dtbocfg_overlay_release(struct config_item *item)
{
    struct dtbocfg_overlay_item *overlay = container_of_dtbocfg_overlay_item(item);

    pr_debug("%s\n", __func__);

    dtbocfg_overlay_item_release(overlay);

    if (overlay->dtbo) {
        kfree(overlay->dtbo);
        overlay->dtbo      = NULL;
        overlay->dtbo_size = 0;
    }

    kfree(overlay);
}

/**
 * Device Tree Blob Overlay Item Structure
 */
static struct configfs_item_operations dtbocfg_overlay_item_ops = {
    .release        = dtbocfg_overlay_release,
};

static struct config_item_type dtbocfg_overlay_item_type = {
    .ct_item_ops    = &dtbocfg_overlay_item_ops,
    .ct_attrs       = dtbocfg_overlay_attrs,
    .ct_bin_attrs   = dtbocfg_overlay_bin_attrs,
    .ct_owner       = THIS_MODULE,
};

/**
 * dtbocfg_overlay_group_make_item() - Make Device Tree Overlay Group Item
 * @group: Pointer to Configuration Group
 * @name : Pointer to Group Name
 * Return  Pointer to Device Tree Overlay Group Item
 */
static struct config_item *dtbocfg_overlay_group_make_item(struct config_group *group, const char *name)
{
    struct dtbocfg_overlay_item *overlay;

    pr_debug("%s\n", __func__);

    overlay = kzalloc(sizeof(*overlay), GFP_KERNEL);

    if (!overlay)
        return ERR_PTR(-ENOMEM);
    overlay->id        = -1;
    overlay->dtbo      = NULL;
    overlay->dtbo_size = 0;

    config_item_init_type_name(&overlay->item, name, &dtbocfg_overlay_item_type);
    return &overlay->item;
}

/**
 * dtbocfg_overlay_group_drop_item() - Drop Device Tree Overlay Group Item
 * @group: Pointer to Configuration Group
 * @item : Pointer to Device Tree Overlay Group Item
 */
static void dtbocfg_overlay_group_drop_item(struct config_group *group, struct config_item *item)
{
    struct dtbocfg_overlay_item *overlay = container_of_dtbocfg_overlay_item(item);

    pr_debug("%s\n", __func__);

    config_item_put(&overlay->item);
}

/**
 * Device Tree Blob Overlay Sub Group Structures
 */
static struct configfs_group_operations dtbocfg_overlays_ops = {
    .make_item      = dtbocfg_overlay_group_make_item,
    .drop_item      = dtbocfg_overlay_group_drop_item,
};

static struct config_item_type dtbocfg_overlays_type = {
    .ct_group_ops   = &dtbocfg_overlays_ops,
    .ct_owner       = THIS_MODULE,
};

static struct config_group dtbocfg_overlay_group;

/**
 * Device Tree Blob Overlay Root Sub System Structures
 */
static struct configfs_group_operations dtbocfg_root_ops = {
    /* empty - we don't allow anything to be created */
};

static struct config_item_type dtbocfg_root_type = {
    .ct_group_ops   = &dtbocfg_root_ops,
    .ct_owner       = THIS_MODULE,
};

static struct configfs_subsystem dtbocfg_root_subsys = {
    .su_group = {
        .cg_item = {
            .ci_namebuf = "device-tree",
            .ci_type    = &dtbocfg_root_type,
        },
    },
  .su_mutex = __MUTEX_INITIALIZER(dtbocfg_root_subsys.su_mutex),
};

/**
 * dtbocfg_module_init()
 */
static int __init dtbocfg_module_init(void)
{
    int retval = 0;

    pr_info("%s\n", __func__);

    config_group_init(&dtbocfg_root_subsys.su_group);
    config_group_init_type_name(&dtbocfg_overlay_group, "overlays", &dtbocfg_overlays_type);

    retval = configfs_register_subsystem(&dtbocfg_root_subsys);
    if (retval != 0) {
        pr_err( "%s: couldn't register subsys\n", __func__);
        goto register_subsystem_failed;
    }

    retval = configfs_register_group(&dtbocfg_root_subsys.su_group, &dtbocfg_overlay_group);
    if (retval != 0) {
        pr_err( "%s: couldn't register group\n", __func__);
        goto register_group_failed;
    }

    pr_info("%s: OK\n", __func__);
    return 0;

  register_group_failed:
    configfs_unregister_subsystem(&dtbocfg_root_subsys);
  register_subsystem_failed:
    return retval;
}

/**
 * dtbocfg_module_exit()
 */
static void __exit dtbocfg_module_exit(void)
{
    configfs_unregister_group(&dtbocfg_overlay_group);
    configfs_unregister_subsystem(&dtbocfg_root_subsys);
}

module_init(dtbocfg_module_init);
module_exit(dtbocfg_module_exit);

MODULE_AUTHOR("ikwzm");
MODULE_DESCRIPTION("Device Tree Overlay Configuration File System");
MODULE_LICENSE("Dual BSD/GPL");

第83 章设备树插件驱动分析实验

在上个章节中,我们成功移植了设备树插件驱动,而在本章节中,我们将深入研究设备树驱动的分析过程。
大家有了configfs 虚拟文件系统数据结构的基础以后,现在分析设备树插件的驱动就非常容易了。
网盘资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\65_dtbocfg”dtbocfg.c 为设备树插件驱动文件。
在驱动文件中,生成device-tree/overlays 目录结构,如下图(图83-1)所示:

image-20240903113557377

dtbocfg_overlays_type 中实现了ct_group_ops 下的make_itemdrop_item。如下图(图83-2)所示:

image-20240903113643398

在命令行输入mkdir 命令会去执行的函数,如下图(图83-3)所示:

image-20240903113711478

dtbocfg_overlay_item_type 中实现了attrsbin_attrsct_item_ops,如下图(图83-4)所示:

image-20240903113800453

定义了dtbocfg_overlay_item_attr_dtbo 结构体。实现了dtbocfg_overlay_item_dtbo_readdtbocfg_overlay_item_dtbo_write 函数,如下图(图83-5)所示:

image-20240903113830732

重点看当给status 写1 的时候会发生什么事情,如下图(图83-6)所示:

image-20240903113851216

当status 写入1 的时,会执行dtbocfg_overlay_item_create 函数。在这个函数中又去执行了of_overlay_fdt_apply 函数。of_overlay_fdt_apply 函数如下图(图83-7)所示:

image-20240903113927290

设备树插件(dtbo)里面的节点也要被转换成device_node,有的device_node 也要被转换成platform_device。不过在进行转换之前,of_overlay_fdt_apply 函数会先创建一个改变集。然后根据这个改变集去进行修改。

创建改变集的目的是为了方便对设备树进行修改和复原。设备树是一种静态的数据结构,一旦被编译和加载到内核中,就难以直接修改。为了解决这个问题,设备树覆盖功能引入了改变集的概念。

改变集是一个描述设备树变化的数据结构,它记录了对设备树的修改操作,如添加、删除或修改节点。通过创建改变集,我们可以在运行时对设备树进行动态修改,而无需修改原始的设备树源文件。

通过创建改变集,我们可以方便地定义需要进行的修改操作,而不必直接操作设备树的底层结构。这提供了一种高层次的抽象,使我们能够以更简洁和可读的方式描述设备树的变化。同时,改变集也可以被保存、传递和应用到其他设备树上,方便在不同系统或环境中进行设备树的配置和定制。

此外,改变集还可以用于设备树的复原。在某些情况下,我们可能需要在运行时撤销对设备树的修改并恢复到原始状态。通过应用反向的改变集,我们可以还原设备树,使其回到修改之前的状态,实现修改的复原。

因此,创建改变集提供了一种方便、可控和可复原的方式来修改设备树,使设备树的管理和配置更加灵活和可靠。

第84 章设备树插件参考资料介绍

通过上述章节的学习,设备树插件的知识已经学习完了,本章节将介绍设备树插件其他的一些参考资料。
在linux 源码中linux_sdk/kernel/Documentation/filesystems/configfs 目录下的configfs.txt。
内容解释如下所示:

[什么是configfs?]

  • configfs 是一种基于RAM 的文件系统,提供了与sysfs 功能相反的功能。sysfs 是内核对象的基于文件系统的视图,而configfs 是内核对象(或config_items)的基于文件系统的管理器。
  • 使用sysfs 时,在内核中创建一个对象(例如,当发现设备时),并将其注册到sysfs 中。然后,它的属性会出现在sysfs 中,允许用户空间通过readdir(3)/read(2)读取属性。它可能允许通过write(2)修改某些属性。重要的是,对象在内核中创建和销毁,内核控制sysfs 表示的生命周期,而sysfs 只是对所有这些的一种窗口。
  • 通过显式的用户空间操作(例如mkdir(2)),可以创建一个configfs config_item。通过rmdir(2)销毁它。属性在mkdir(2)时出现,并且可以通过read(2)和write(2)读取或修改。与sysfs 类似,readdir(3)查询项目和/或属性的列表。可以使用symlink(2)将项目组合在一起。与sysfs 不同,表示的生命周期完全由用户空间驱动。支持项目的内核模块必须响应此操作。
  • sysfs 和configfs 可以并且应该在同一系统上同时存在。它们不是彼此的替代品。

【使用configfs】

  • configfs 可以作为模块编译或集成到内核中。您可以通过以下方式访问它:

    mount -t configfs none /config
  • 除非也加载了客户端模块,否则configfs 树将为空。这些模块将其项目类型注册为configfs 的子系统。一旦加载了客户端子系统,它将显示为/config 下的一个或多个子目录。与sysfs 一样,configfs 树始终存在,无论是否挂载在/config 上。

  • 可以通过mkdir(2)创建项目。项目的属性也将同时出现。readdir(3)可以确定属性是什么,read(2)可以查询其默认值,write(2)可以存储新值。不要在一个属性文件中混合多个属性。

  • configfs 有两种类型的属性:

    • 普通属性(Normal attributes)类似于sysfs 属性,是小型的ASCII 文本文件,最大大小为一页(PAGE_SIZE,在i386 上为4096)。最好每个文件只使用一个值,并且与sysfs 相同的注意事项也适用。configfs 期望write(2)一次存储整个缓冲区。当写入普通configfs 属性时,用户空间进程应首先读取整个文件,修改要更改的部分,然后将整个缓冲区写回。
    • 二进制属性(Binary attributes)与sysfs 二进制属性类似,但语义上有一些细微的变化。不适用PAGE_SIZE限制,但整个二进制项必须适应单个内核vmalloc 的缓冲区。来自用户空间的write(2)调用是缓冲的,并且在最终关闭时将调用属性的write_bin_attribute 方法,因此用户空间必须检查close(2)的返回代码以验证操作是否成功完成。为避免恶意用户OOM(Out of Memory)内核,有每个二进制属性的最大缓冲区值。
  • 当需要销毁项目时,使用rmdir(2)将其删除。如果任何其他项目通过symlink(2)链接到它,则无法销毁
    该项目。可以使用unlink(2)删除链接。

【配置FakeNBD:一个示例】

  • 假设有一个网络块设备(Network Block Device,NBD)驱动程序,允许您访问远程块设备。将其称为FakeNBD。FakeNBD 使用configfs 进行配置。显然,将有一个方便的程序供系统管理员使用来配置FakeNBD,但某种方式下,该程序必须告知驱动程序。这就是configfs 的用武之地。
  • 加载FakeNBD 驱动程序时,它会向configfs 注册自己。readdir(3)可以看到这一点:
# ls /config
fakenbd
  • 可以使用mkdir(2)创建fakenbd 连接。名称是任意的,但工具可能会对名称进行一些处理。也许它是一个UUID 或磁盘名称:
# mkdir /config/fakenbd/disk1
# ls /config/fakenbd/disk1
target device rw
  • target 属性包含FakeNBD 将连接到的服务器的IP 地址。device 属性是服务器上的设备。可预测的是,rw 属性确定连接是只读还是读写。
# echo 10.0.0.1 > /config/fakenbd/disk1/target
# echo /dev/sda1 > /config/fakenbd/disk1/device
# echo 1 > /config/fakenbd/disk1/rw
  • target 属性包含FakeNBD 将连接到的服务器的IP 地址。device 属性是服务器上的设备。可预测的是,rw 属性确定连接是只读还是读写。

【使用configfs 进行编码】

  • configfs 中的每个对象都是一个config_item。config_item 反映了子系统中的一个对象。它具有与该对象上的值相匹配的属性。configfs 处理该对象及其属性的文件系统表示,使得子系统只需关注基本的show/store 交互。

  • 项目是在config_group 内创建和销毁的。组是共享相同属性和操作的项目集合。项目通过mkdir(2)创建,并通过rmdir(2)移除,但configfs 会处理这些操作。组具有一组操作来执行这些任务。

  • 子系统是客户端模块的顶层。在初始化过程中,客户端模块向configfs 注册子系统,该子系统将在configfs 文件系统的顶部显示为一个目录。子系统也是一个config_group,并且可以执行config_group 的所有功能。

    [struct config_item]
    
    struct config_item {
        char *ci_name;
        char ci_namebuf[UOBJ_NAME_LEN];
        struct kref ci_kref;
        struct list_head ci_entry;
        struct config_item *ci_parent;
        struct config_group *ci_group;
        struct config_item_type *ci_type;
        struct dentry *ci_dentry;
    };
    
    void config_item_init(struct config_item *);
    void config_item_init_type_name(struct config_item *,const char *name,
                                    struct config_item_type *type);
    struct config_item *config_item_get(struct config_item *);
    void config_item_put(struct config_item *);
    • 通常,struct config_item 被嵌入在一个容器结构中,这个结构实际上代表了子系统正在做的事情。该结构中的config_item 部分是对象与configfs 进行交互的方式。
    • 无论是在源文件中静态定义还是由父config_group 创建,config_item 都必须调用其中一个_init()函数。这将初始化引用计数并设置适当的字段。
    • 所有使用config_item 的用户都应该通过config_item_get()对其进行引用,并在完成后通过config_item_put()释放引用。
    • 单独一个config_item 不能做更多的事情,只能出现在configfs 中。通常,子系统希望该项显示和/或存储属性,以及其他一些操作。为此,它需要一个类型。
[struct config_item_type]

struct configfs_item_operations {
    void (*release)(struct config_item *);
    int (*allow_link)(struct config_item *src,struct config_item *target);
    void (*drop_link)(struct config_item *src,struct config_item *target);
};

struct config_item_type {
    struct module *ct_owner;
    struct configfs_item_operations *ct_item_ops;
    struct configfs_group_operations *ct_group_ops;
    struct configfs_attribute **ct_attrs;
    struct configfs_bin_attribute **ct_bin_attrs;
};

config_item_type 的最基本功能是定义可以在config_item 上执行的操作。所有动态分配的项目都需要提供ct_item_ops->release()方法。当config_item 的引用计数达到零时,将调用此方法。

[struct configfs_attribute]

struct configfs_attribute {
    char *ca_name;
    struct module *ca_owner;
    umode_t ca_mode;
    ssize_t (*show)(struct config_item *, char *);
    ssize_t (*store)(struct config_item *, const char *, size_t);
};
  • 当config_item 希望将属性显示为项目的configfs 目录中的文件时,它必须定义一个描述该属性的configfs_attribute。然后将属性添加到以NULL 结尾的config_item_type->ct_attrs 数组中。当项目出现在configfs 中时,属性文件将显示为configfs_attribute->ca_name 文件名。configfs_attribute->ca_mode 指定文件权限。

  • 如果属性是可读的并提供了一个->show 方法,每当用户空间请求对属性进行read(2)时,该方法将被调用。如果属性是可写的并提供了一个->store 方法,每当用户空间请求对属性进行write(2)时,该方法将被调用。

    [struct configfs_bin_attribute]
    
    struct configfs_attribute {
        struct configfs_attribute cb_attr;
        void *cb_private;
        size_t cb_max_size;
    };
    • 当需要使用二进制数据块作为文件内容显示在项目的configfs 目录中时,可以使用二进制属性(binary attribute)。为此,将二进制属性添加到以NULL 结尾的config_item_type->ct_bin_attrs 数组中,当项目出现在configfs 中时,属性文件将显示为configfs_bin_attribute->cb_attr.ca_name 文件名。configfs_bin_attribute->cb_attr.ca_mode 指定文件权限。

    • cb_private 成员供驱动程序使用,而cb_max_size 成员指定要使用的vmalloc 缓冲区的最大大小。

    • 如果二进制属性是可读的,并且config_item 提供了ct_item_ops->read_bin_attribute()方法,那么每当用户空间请求对属性进行read(2)时,该方法将被调用。对于write(2),也会发生相反的情况。读取/写入是缓冲的,因此只会发生单个读取/写入;属性本身不需要关心这一点。[struct config_group]

    • config_item 不能独立存在。创建config_item 的唯一方法是通过在config_group 上执行mkdir(2)操作。这将触发创建子项。

      struct config_group {
          struct config_item cg_item;
          struct list_head cg_children;
          struct configfs_subsystem *cg_subsys;
          struct list_head default_groups;
          struct list_head group_entry;
      };
      void config_group_init(struct config_group *group);
      void config_group_init_type_name(struct config_group *group,const char *name,
                                       struct config_item_type *type);
    • config_group 结构包含一个config_item。适当配置该项意味着组可以作为一个独立的项进行操作。然而,它还可以做更多的事情:它可以创建子项或子组。这是通过在组的config_item_type 上指定的组操作来实现的。

    struct configfs_group_operations {
        struct config_item *(*make_item)(struct config_group *group,const char *name);
        struct config_group *(*make_group)(struct config_group *group,const char *name);
        int (*commit_item)(struct config_item *item);
        void (*disconnect_notify)(struct config_group *group,struct config_item *item);
        void (*drop_item)(struct config_group *group,struct config_item *item);
    };
  • 组通过提供ct_group_ops->make_item()方法来创建子项。如果提供了该方法,它将在组的目录中的mkdir(2)操作中调用。子系统分配一个新的config_item(或更常见的是其容器结构),对其进行初始化,并将其返回给configfs。然后,configfs 将填充文件系统树以反映新的项。

  • 如果子系统希望子项本身成为一个组,子系统将提供ct_group_ops->make_group()。其他操作与之相同,在组上使用组的_init()函数。

  • 最后,当用户空间对项或组调用rmdir(2)时,将调用ct_group_ops->drop_item()。由于config_group 也是一个config_item,因此不需要单独的drop_group()方法。子系统必须对在项分配时初始化的引用执行config_item_put()。如果子系统没有其他工作要执行,可以省略ct_group_ops->drop_item()方法,configfs将代表子系统对项执行config_item_put()。

  • 重要提示:drop_item()是void 类型的,因此无法失败。当调用rmdir(2)时,configfs 将从文件系统树中删除该项(假设没有子项)。子系统负责对此作出响应。如果子系统在其他线程中引用该项,则内存是安全的。实际上,该项从子系统的使用中消失可能需要一些时间。但它已经从configfs 中消失了。

  • 当调用drop_item()时,项的链接已经被拆除。它不再引用其父项,并且在项层次结构中没有位置。如果客户端在此拆除发生之前需要进行一些清理工作,子系统可以实现ct_group_ops->disconnect_notify()方法。该方法在configfs 将项从文件系统视图中删除之后、但在将项从其父组中删除之前调用。与drop_item()一样,disconnect_notify()是void 类型的,不会失败。客户端子系统不应在此处删除任何引用,因为它们仍然必须在drop_item()中执行。

  • 只要config_group 仍然具有子项,就无法删除它。这在configfs 的rmdir(2)代码中实现。不会调用->drop_item(),因为项尚未被删除。rmdir(2)将失败,因为目录不为空。

    [struct configfs_subsystem]
  • 子系统通常在module_init 时间注册自身。这告诉configfs 将子系统显示在文件树中。

    struct configfs_subsystem {
        struct config_group su_group;
        struct mutex su_mutex;
    };
    
    int configfs_register_subsystem(struct configfs_subsystem *subsys);
    void configfs_unregister_subsystem(struct configfs_subsystem *subsys);
  • 一个子系统由一个顶级的config_group 和一个互斥锁组成。config_group 是创建子config_item 的地方。对于子系统来说,这个组通常是静态定义的。在调用configfs_register_subsystem()之前,子系统必须通过常规的group_init()函数对组进行初始化,并且还必须初始化互斥锁。

  • 当注册调用返回时,子系统将处于活动状态,并且将通过configfs 可见。此时,可以调用mkdir(2),子系统必须准备好接收该调用。

【示例】

这些基本概念的最佳示例是在samples/configfs/configfs_sample.c 中的simple_children 子系统/组和simple_child 项。它展示了一个简单的对象,显示和存储属性,以及一个简单的组来创建和销毁这些子项。

【层次结构导航和子系统互斥锁】

  • configfs 提供了一个额外的功能。由于config_groups 和config_items 出现在文件系统中,它们按层次结构排列。子系统永远不会触及文件系统的部分,但子系统可能对此层次结构感兴趣。因此,层次结构通过config_group->cg_childrenconfig_item->ci_parent 结构成员进行镜像。
  • 子系统可以通过cg_children 列表和ci_parent 指针遍历子系统创建的树。这可能与configfs 对层次结构的管理发生竞争,因此configfs 使用子系统互斥锁来保护修改操作。每当子系统想要遍历层次结构时,必须在子系统互斥锁的保护下进行。
  • 在新分配的项尚未链接到该层次结构时,子系统将无法获取互斥锁。类似地,在放弃项尚未取消链接时,它也无法获取互斥锁。这意味着项的ci_parent 指针在项位于configfs 中时永远不会为NULL,并且项仅在其父项的cg_children 列表中存在相同的时间段。这允许子系统在持有互斥锁时信任ci_parent 和cg_children。

【通过symlink(2)进行项聚合】

  • configfs 通过group->item 父/子关系提供了一个简单的组。然而,通常需要在父/子连接之外进行聚合。这是通过symlink(2)实现的。
  • config_item 可以提供ct_item_ops->allow_link()和ct_item_ops->drop_link()方法。如果存在->allow_link()方法,可以使用config_item 作为链接源调用symlink(2)。这些链接仅允许在configfs config_item 之间创建。任何在configfs 文件系统之外的symlink(2)尝试都将被拒绝。
  • 调用symlink(2)时,将使用源config_item 的->allow_link()方法和自身和目标项作为参数。如果源项允许链接到目标项,则返回0。如果源项只希望链接到某种类型的对象(例如,在其自己的子系统中),则可以拒绝链接。
  • 在符号链接上调用unlink(2)时,将通过->drop_link()方法通知源项。与->drop_item()方法一样,这是一个无返回值的函数,无法返回失败。子系统负责响应该更改。
  • 在任何项链接到其他项时,无法删除config_item,也无法在有项链接到它时删除config_item。在configfs中,不允许存在悬空的符号链接。

【自动创建的子组】

  • 新的config_group 可能希望具有两种类型的子config_item。虽然可以通过->make_item()中的魔术名称来编码这一点,但更明确的做法是让用户空间看到这种分歧的方法。
  • configfs 提供了一种方法,通过该方法可以在父级创建时自动在其中创建一个或多个子组。因此,mkdir(“parent”)将导致创建”parent”、”parent/subgroup1”,一直到”parent/subgroupN”。类型为1 的项现在可以在”parent/subgroup1”中创建,类型为N 的项可以在”parent/subgroupN”中创建。
  • 这些自动创建的子组,或默认组,不排除父组的其他子项。如果存在ct_group_ops->make_group(),可以直接在父组上创建其他子组。
  • 通过向父config_group 结构添加它们,configfs 子系统可以指定默认组。这是一个关于configfs 的C 代码示例,它展示了configfs 的一些基本概念和用法。
  • 首先,在代码中定义了一个名为”simple_children”的子系统/组和一个名为”simple_child”的项。这个简单的对象展示了如何显示和存储属性,并且使用一个简单的组来创建和销毁这些子项。
  • configfs 中的子系统和组是以层次结构排列的,可以通过config_group->cg_children 和config_item->ci_parent 来遍历子系统创建的树。为了保护修改操作,configfs 使用了子系统互斥锁。
  • 在代码中还使用了symlink(2)函数来创建项之间的链接。config_item 可以提供allow_link()和drop_link()方法来控制链接的创建和删除。使用symlink(2)函数创建的链接只允许在configfs 中的config_item 之间创建,对于configfs 文件系统之外的symlink(2)尝试会被拒绝。
  • 此外,代码中还介绍了自动创建的子组的概念。通过在父级创建时自动创建一个或多个子组,可以更方便地组织config_item。这些自动创建的子组不会排除父组的其他子项。
  • 这只是一个简单的示例,用于介绍configfs 的基本概念和用法。实际使用configfs 时,可能会有更复杂的场景和用法。

Linux 内核源码linux_sdk/kernel/samples/configfs 目录下的configfs_sample.c,如下所示:

/*
 * vim: noexpandtab ts=8 sts=0 sw=8:
 *
 * configfs_example_macros.c - This file is a demonstration module
 *      containing a number of configfs subsystems.  It uses the helper
 *      macros defined by configfs.h
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 021110-1307, USA.
 *
 * Based on sysfs:
 * 	sysfs is Copyright (C) 2001, 2002, 2003 Patrick Mochel
 *
 * configfs Copyright (C) 2005 Oracle.  All rights reserved.
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>

#include <linux/configfs.h>



/*
 * 01-childless
 *
 * This first example is a childless subsystem.  It cannot create
 * any config_items.  It just has attributes.
 *
 * Note that we are enclosing the configfs_subsystem inside a container.
 * This is not necessary if a subsystem has no attributes directly
 * on the subsystem.  See the next example, 02-simple-children, for
 * such a subsystem.
 */

struct childless {
	struct configfs_subsystem subsys;
	int showme;
	int storeme;
};

static inline struct childless *to_childless(struct config_item *item)
{
	return item ? container_of(to_configfs_subsystem(to_config_group(item)),
			struct childless, subsys) : NULL;
}

static ssize_t childless_showme_show(struct config_item *item, char *page)
{
	struct childless *childless = to_childless(item);
	ssize_t pos;

	pos = sprintf(page, "%d\n", childless->showme);
	childless->showme++;

	return pos;
}

static ssize_t childless_storeme_show(struct config_item *item, char *page)
{
	return sprintf(page, "%d\n", to_childless(item)->storeme);
}

static ssize_t childless_storeme_store(struct config_item *item,
		const char *page, size_t count)
{
	struct childless *childless = to_childless(item);
	unsigned long tmp;
	char *p = (char *) page;

	tmp = simple_strtoul(p, &p, 10);
	if (!p || (*p && (*p != '\n')))
		return -EINVAL;

	if (tmp > INT_MAX)
		return -ERANGE;

	childless->storeme = tmp;

	return count;
}

static ssize_t childless_description_show(struct config_item *item, char *page)
{
	return sprintf(page,
"[01-childless]\n"
"\n"
"The childless subsystem is the simplest possible subsystem in\n"
"configfs.  It does not support the creation of child config_items.\n"
"It only has a few attributes.  In fact, it isn't much different\n"
"than a directory in /proc.\n");
}

CONFIGFS_ATTR_RO(childless_, showme);
CONFIGFS_ATTR(childless_, storeme);
CONFIGFS_ATTR_RO(childless_, description);

static struct configfs_attribute *childless_attrs[] = {
	&childless_attr_showme,
	&childless_attr_storeme,
	&childless_attr_description,
	NULL,
};

static const struct config_item_type childless_type = {
	.ct_attrs	= childless_attrs,
	.ct_owner	= THIS_MODULE,
};

static struct childless childless_subsys = {
	.subsys = {
		.su_group = {
			.cg_item = {
				.ci_namebuf = "01-childless",
				.ci_type = &childless_type,
			},
		},
	},
};


/* ----------------------------------------------------------------- */

/*
 * 02-simple-children
 *
 * This example merely has a simple one-attribute child.  Note that
 * there is no extra attribute structure, as the child's attribute is
 * known from the get-go.  Also, there is no container for the
 * subsystem, as it has no attributes of its own.
 */

struct simple_child {
	struct config_item item;
	int storeme;
};

static inline struct simple_child *to_simple_child(struct config_item *item)
{
	return item ? container_of(item, struct simple_child, item) : NULL;
}

static ssize_t simple_child_storeme_show(struct config_item *item, char *page)
{
	return sprintf(page, "%d\n", to_simple_child(item)->storeme);
}

static ssize_t simple_child_storeme_store(struct config_item *item,
		const char *page, size_t count)
{
	struct simple_child *simple_child = to_simple_child(item);
	unsigned long tmp;
	char *p = (char *) page;

	tmp = simple_strtoul(p, &p, 10);
	if (!p || (*p && (*p != '\n')))
		return -EINVAL;

	if (tmp > INT_MAX)
		return -ERANGE;

	simple_child->storeme = tmp;

	return count;
}

CONFIGFS_ATTR(simple_child_, storeme);

static struct configfs_attribute *simple_child_attrs[] = {
	&simple_child_attr_storeme,
	NULL,
};

static void simple_child_release(struct config_item *item)
{
	kfree(to_simple_child(item));
}

static struct configfs_item_operations simple_child_item_ops = {
	.release		= simple_child_release,
};

static const struct config_item_type simple_child_type = {
	.ct_item_ops	= &simple_child_item_ops,
	.ct_attrs	= simple_child_attrs,
	.ct_owner	= THIS_MODULE,
};


struct simple_children {
	struct config_group group;
};

static inline struct simple_children *to_simple_children(struct config_item *item)
{
	return item ? container_of(to_config_group(item),
			struct simple_children, group) : NULL;
}

static struct config_item *simple_children_make_item(struct config_group *group,
		const char *name)
{
	struct simple_child *simple_child;

	simple_child = kzalloc(sizeof(struct simple_child), GFP_KERNEL);
	if (!simple_child)
		return ERR_PTR(-ENOMEM);

	config_item_init_type_name(&simple_child->item, name,
				   &simple_child_type);

	simple_child->storeme = 0;

	return &simple_child->item;
}

static ssize_t simple_children_description_show(struct config_item *item,
		char *page)
{
	return sprintf(page,
"[02-simple-children]\n"
"\n"
"This subsystem allows the creation of child config_items.  These\n"
"items have only one attribute that is readable and writeable.\n");
}

CONFIGFS_ATTR_RO(simple_children_, description);

static struct configfs_attribute *simple_children_attrs[] = {
	&simple_children_attr_description,
	NULL,
};

static void simple_children_release(struct config_item *item)
{
	kfree(to_simple_children(item));
}

static struct configfs_item_operations simple_children_item_ops = {
	.release	= simple_children_release,
};

/*
 * Note that, since no extra work is required on ->drop_item(),
 * no ->drop_item() is provided.
 */
static struct configfs_group_operations simple_children_group_ops = {
	.make_item	= simple_children_make_item,
};

static const struct config_item_type simple_children_type = {
	.ct_item_ops	= &simple_children_item_ops,
	.ct_group_ops	= &simple_children_group_ops,
	.ct_attrs	= simple_children_attrs,
	.ct_owner	= THIS_MODULE,
};

static struct configfs_subsystem simple_children_subsys = {
	.su_group = {
		.cg_item = {
			.ci_namebuf = "02-simple-children",
			.ci_type = &simple_children_type,
		},
	},
};


/* ----------------------------------------------------------------- */

/*
 * 03-group-children
 *
 * This example reuses the simple_children group from above.  However,
 * the simple_children group is not the subsystem itself, it is a
 * child of the subsystem.  Creation of a group in the subsystem creates
 * a new simple_children group.  That group can then have simple_child
 * children of its own.
 */

static struct config_group *group_children_make_group(
		struct config_group *group, const char *name)
{
	struct simple_children *simple_children;

	simple_children = kzalloc(sizeof(struct simple_children),
				  GFP_KERNEL);
	if (!simple_children)
		return ERR_PTR(-ENOMEM);

	config_group_init_type_name(&simple_children->group, name,
				    &simple_children_type);

	return &simple_children->group;
}

static ssize_t group_children_description_show(struct config_item *item,
		char *page)
{
	return sprintf(page,
"[03-group-children]\n"
"\n"
"This subsystem allows the creation of child config_groups.  These\n"
"groups are like the subsystem simple-children.\n");
}

CONFIGFS_ATTR_RO(group_children_, description);

static struct configfs_attribute *group_children_attrs[] = {
	&group_children_attr_description,
	NULL,
};

/*
 * Note that, since no extra work is required on ->drop_item(),
 * no ->drop_item() is provided.
 */
static struct configfs_group_operations group_children_group_ops = {
	.make_group	= group_children_make_group,
};

static const struct config_item_type group_children_type = {
	.ct_group_ops	= &group_children_group_ops,
	.ct_attrs	= group_children_attrs,
	.ct_owner	= THIS_MODULE,
};

static struct configfs_subsystem group_children_subsys = {
	.su_group = {
		.cg_item = {
			.ci_namebuf = "03-group-children",
			.ci_type = &group_children_type,
		},
	},
};

/* ----------------------------------------------------------------- */

/*
 * We're now done with our subsystem definitions.
 * For convenience in this module, here's a list of them all.  It
 * allows the init function to easily register them.  Most modules
 * will only have one subsystem, and will only call register_subsystem
 * on it directly.
 */
static struct configfs_subsystem *example_subsys[] = {
	&childless_subsys.subsys,
	&simple_children_subsys,
	&group_children_subsys,
	NULL,
};

static int __init configfs_example_init(void)
{
	int ret;
	int i;
	struct configfs_subsystem *subsys;

	for (i = 0; example_subsys[i]; i++) {
		subsys = example_subsys[i];

		config_group_init(&subsys->su_group);
		mutex_init(&subsys->su_mutex);
		ret = configfs_register_subsystem(subsys);
		if (ret) {
			printk(KERN_ERR "Error %d while registering subsystem %s\n",
			       ret,
			       subsys->su_group.cg_item.ci_namebuf);
			goto out_unregister;
		}
	}

	return 0;

out_unregister:
	for (i--; i >= 0; i--)
		configfs_unregister_subsystem(example_subsys[i]);

	return ret;
}

static void __exit configfs_example_exit(void)
{
	int i;

	for (i = 0; example_subsys[i]; i++)
		configfs_unregister_subsystem(example_subsys[i]);
}

module_init(configfs_example_init);
module_exit(configfs_example_exit);
MODULE_LICENSE("GPL");

上面的驱动文件,大家可以好好分析下代码。至此,设备树模型课程学习完毕。

第九篇设备模型

第85 章设备模型基本框架-kobject 和kset

85.1 什么是设备模型

字符设备驱动通常适用于相对简单的设备,对于一些更复杂的功能,比如说电源管理和热插拔事件管理,使用字符设备框架可能不够灵活和高效。为了应对更复杂的设备和功能,Linux内核提供了设备模型。设备模型允许开发人员以更高级的方式来描述硬件设备和它们之间的关系,并提供一组通用API 和机制来处理设备的注册,热插拔事件,电源管理等。

通过使用设备模型,驱动开发人员可以将更多的底层功能交给内核来处理,而不必重复实现这些基础功能。这使得驱动的编写更加高级和模块化,减少了重复工作和出错的可能性。对于一些常见的硬件设备,如USB、i2c 和平台设备,内核已经提供了相应的设备模型和相关驱动,开发人员可以基于这些模型来编写驱动,从而更快地实现特定设备的功能,并且可以借助内核的电源管理和热插拔事件管理功能。

总之,使用设备模型可以帮助简化驱动开发过程,并提供更高级的功能和灵活性,使得驱动开发人员能够更好地适应复杂的硬件设备需求。

85.2 设备模型的好处

设备模型在内核驱动中扮演着重要的角色,它提供了一种统一的方式来描述硬件设备和它们之间的关系。以下是设备模型在内核驱动中的几个重要方面。

  • 1 代码复用:设备模型允许多个设备复用同一个驱动。通过在设备树或总线上定义不同的设备节点,这些设备可以使用相同的驱动进行初始化和管理。这样可以减少代码的冗余,提高驱动的复用性和维护性。
  • 2 资源的动态申请和释放:设备模型提供了一种机制来动态申请和释放设备所需的资源,如内存,中断等。驱动可以使用这些机制来管理设备所需的资源,确保在设备初始化和关闭时进行正确的资源分配和释放。
  • 3 简化驱动编写: 设备模型提供了一组通用API 和机制,使得驱动编写更加简化和模块化。开发人员可以使用这些API 来注册设备,处理设备事件,进行设备的读写操作等,而无需重复实现这些通用功能。
  • 4 热插拔机制:设备模型支持热插拔机制,能够在运行时动态添加或移除设备。当设备插入或拔出时,内核会生成相应的热插拔事件,驱动可以通过监听这些事件来执行相应的操作,如设备的初始化或释放。
  • 5 驱动的面向对象思想:设备模型的设计借鉴了面向对象编程(OOP)的思想。每个设备都被看作是一个对象,具有自己的属性和方法,并且可以通过设备模型的机制进行继承和扩展。这种设计使得驱动的编写更加模块化和可扩展,可以更好地应对不同类型的设备和功能需求。

总之,设备模型在内核驱动中扮演着关键的角色,通过提供统一的设备描述和管理机制,简化了驱动的编写和维护过程,提高了代码的复用性和可维护性,并支持热插拔和动态资源管理等重要功能。

85.3 kobject 和kset 基本概念

kobject 和kset 是Linux 内核中用于管理内核对象的基本概念。
**kobject(内核对象)**是内核中抽象出来的通用对象模型,用于表示内核中的各种实体。kobject是一个结构体,其中包含了一些描述该对象的属性和方法。它提供了一种统一的接口和机制,用于管理和操作内核对象。kobject 结构体在内核源码kernel/include/linux/kobject.h 文件中,如下所示:

struct kobject {
    const char *name;
    struct list_head entry;
    struct kobject *parent;
    struct kset *kset;
    struct kobj_type *ktype;
    struct kernfs_node *sd; /* sysfs directory entry */
    struct kref kref;
    
    #ifdef CONFIG_DEBUG_KOBJECT_RELEASE
        struct delayed_work release;
    #endif
    
    unsigned int state_initialized:1;
    unsigned int state_in_sysfs:1;
    unsigned int state_add_uevent_sent:1;
    unsigned int state_remove_uevent_sent:1;
    unsigned int uevent_suppress:1;
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
};

这是Linux 内核中的struct kobject 结构体的定义。它包含了一些字段用于表示和管理内核对象。
下面是一些主要字段的解释:

  • const char *name:表示kobject 的名称,通常用于在/sys 目录下创建对应的目录。
  • struct list_head entry:用于将kobject 链接到父kobject 的子对象列表中,以建立层次关系。
  • struct kobject *parent:指向父kobject,表示kobject 的层次关系。
  • struct kset *kset:指向包含该kobject 的kset,用于进一步组织和管理kobject。
  • struct kobj_type *ktype:指向定义kobject 类型的kobj_type 结构体,描述kobject 的属性和操作。
  • struct kernfs_node *sd:指向sysfs 目录中对应的kernfs_node,用于访问和操作sysfs 目录项。
  • struct kref kref:用于对kobject 进行引用计数,确保在不再使用时能够正确释放资源。
  • unsigned int 字段:表示一些状态标志和配置选项,例如是否已初始化、是否在sysfs 中、是否发送了add/remove uevent 等。

该结构体还包含了一些与特定配置相关的保留字段。这些字段共同构成了kobject 的基本属性和关系,用于在内核中表示和管理不同类型的内核对象。通过这些字段,可以建立层次化的关系,进行资源管理和操作。每一个kobject 都会对应系统/sys/下的一个目录,如下图所示。

image-20240903142614500

之前我们在学习平台总线时,查看平台总线要进入/sys/bus 目录下,bus 目录下的文件都是和总线相关的目录,比如amba 总线,CPU 总线,platform 总线。如下图所示:

image-20240903142626983

kobject 表示系统/sys 下的一个目录,而目录又是有多个层次,所以对应kobject 的树状关系如下图所示:

image-20240903142655212

我们来分析一下上图。在kobject 结构体中,parent 指针用于表示父kobject,从而建立了kobject 之间的层次关系,类似于目录结构中的父目录和子目录的关系。一个kobject 可以有一个父kobject 和多个子kobject,通过parent 指针可以将它们连接起来形成一个层次化的结构,类似于目录结构中,一个目录可以有一个父目录和多个子目录,通过目录的路径可以表示目录之间的层次关系。这种层次化的关系可以方便地进行遍历,查找和管理,使得内核对象能够按照层次关系进行组织和管理。这种设计使得kobject 的树状结构在内核中具有很高的灵活性和可扩展性。

接下来我们继续学习kset。
**kset(内核对象集合)**是一种用于组织和管理一组相关kobject 的容器。kset 是kobject 的一种扩展,它提供了一种层次化的组织结构,可以将一组相关的kobject 组织在一起。kset 在内核里面用struct kset 结构体来表示,定义在include/linux/kobject.h 头文件中,如下所示:

struct kset {
    struct list_head list;
    spinlock_t list_lock;
    struct kobject kobj;
    const struct kset_uevent_ops *uevent_ops;
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
} __randomize_layout;

这是Linux 内核中的struct kset 结构体的定义。kset 是一种用于组织和管理kobject 的集合。下面是一些主要字段的解释:

  • struct list_head list:用于将kset 链接到全局kset 链表中,以便对kset 进行遍历和管理。
  • spinlock_t list_lock:用于保护对kset 链表的并发访问,确保线程安全性。
  • struct kobject kobj:作为kset 的kobject 表示,用于在/sys 目录下创建对应的目录,并与kset 关联。
  • const struct kset_uevent_ops *uevent_ops:指向kset 的uevent 操作的结构体,用于处理与kset 相关的uevent 事件。

该结构体还包含了一些与特定配置相关的保留字段。kset 通过包含一个kobject 作为其成员,将kset 本身表示为一个kobject,并使用kobj 来管理和操作kset。通过list 字段,kset 可以链接到全局kset 链表中,以便进行全局的遍历和管理。同时,list_lock 字段用于保护对kset 链表的并发访问。kset 还可以定义自己的uevent 操作,用于处理与kset 相关的uevent 事件,例如在添加或删除kobject 时发送相应的uevent 通知。

这些字段共同构成了kset 的基本属性和关系,用于在内核中组织和管理一组相关的kobject。kset 提供了一种层次化的组织结构,并与sysfs 目录相对应,方便对kobject 进行管理和操作。

85.4 kset 和kobject 的关系

在Linux 内核中,kset 和kobject 是相关联的两个概念,它们之间存在一种层次化的关系,

image-20240903142939186

  1. kset 是kobject 的一种扩展:kset 可以被看作是kobject 的一种特殊形式,它扩展了kobject并提供了一些额外的功能。kset 可以包含多个kobject,形成一个层次化的组织结构。
  2. kobject 属于一个kset:每个kobject 都属于一个kset。kobject 结构体中的struct kset *kset字段指向所属的kset。这个关联关系表示了kobject 所在的集合或组织。

通过kset 和kobject 之间的关系,可以实现对内核对象的层次化管理和操作。kset 提供了对kobject 的集合管理接口,可以通过kset 来迭代、查找、添加或删除kobject。同时,kset 也提供了特定于集合的功能,例如在集合级别处理uevent 事件。
总结起来,kset 和kobject 之间的关系是:一个kset 可以包含多个kobject,而一个kobject只能属于一个kset。kset 提供了对kobject 的集合管理和操作接口,用于组织和管理具有相似特性或关系的kobject。这种关系使得内核能够以一种统一的方式管理和操作不同类型的内核对象。

第86 章创建kobject 实验

本章节是关于创建kobject 的实践,通过使用kobject 的API 函数来创建系统根目录下的目录。本章介绍了两种创建kobject 的方法,一种是使用kobject_create_and_add 函数,另一种是使用kzalloc 和kobject_init_and_add 函数,还介绍了如何释放创建的kobject。最后,实验演示了将驱动程序加载到开发板上,并验证了创建的kobject 是否成功。

86.1 实验程序的编写

86.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\66_make_kobj\module
我们编写驱动代码演示如何在Linux 内核模块中创建和管理kobject 对象,其中包括俩种方法创建kobject 对象,分别使用kobject_create_and_addkobject_init_and_add 函数。通过这些操作,可以创建具有层次关系的kobject 对象,并在模块初始化和退出时进行相应的管理的释放。编写完成的make_kobj.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

// 定义了三个kobject指针变量:mykobject01、mykobject02、mykobject03
struct kobject *mykobject01;
struct kobject *mykobject02;
struct kobject *mykobject03;

// 定义了一个kobj_type结构体变量mytype,用于描述kobject的类型。
struct kobj_type mytype;
// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;
    // 创建kobject的第一种方法
    // 创建并添加了名为"mykobject01"的kobject对象,父kobject为NULL
    mykobject01 = kobject_create_and_add("mykobject01", NULL);
    // 创建并添加了名为"mykobject02"的kobject对象,父kobject为mykobject01。
    mykobject02 = kobject_create_and_add("mykobject02", mykobject01);

    // 创建kobject的第二种方法
    // 1 使用kzalloc函数分配了一个kobject对象的内存
    mykobject03 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    // 2 初始化并添加到内核中,名为"mykobject03"。
    ret = kobject_init_and_add(mykobject03, &mytype, NULL, "%s", "mykobject03");

    return 0;
}

// 模块退出函数
static void mykobj_exit(void)
{
    // 释放了之前创建的kobject对象
    kobject_put(mykobject01);
    kobject_put(mykobject02);
    kobject_put(mykobject03);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

86.2 运行测试

86.2.1 编译驱动程序

在上一小节中的make_kobj.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成make_kobj.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

81.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图86-4)所示:

insmod make_kobj.ko

image-20240906111236295

驱动加载之后,我们进入/sys/目录下,如下图所示:

image-20240906112957261

如上图所示,我们发现kobject01,kobject03 创建在系统根目录/sys 目录下,kobject02 的父节点是kobject01,所以被创建在mykobject02 目录下。现在我们成功验证了创建kobject 就是在系统根目录/sys 目录下创建一个文件夹,他们是一一对应的关系。
最后可以使用以下命令进行驱动的卸载,如下图(图86-6)所示:

rmmod make_kobj

image-20240906113122480

至此,创建kobject 实验就完成了。

第87 章创建kset 实验

本章节是关于在Linux 上创建kset 的实验。在实验中介绍了如何使用代码创建kset,并将多个kobject 与kset 关联起来。通过演示实验现象,讲解了kset 是一组kobject 的集合,并解释了kobject 在sys 目录下生成的原因。

87.1 实验程序的编写

87.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\67_make_kset\module。
我们编写驱动代码,这段代码用于定义并初始化两个自定义内核对象mykobject01 和mykobject02,并将它们添加到一个自定义内核对象集合mykset 中。这些自定义内核对象可以用于在Linux 内核中表示和管理特定的功能或资源。代码中的注释对各个部分进行了解释,帮助理解代码的功能。编写完成的make_kset.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

// 定义kobject结构体指针,用于表示第一个自定义内核对象
struct kobject *mykobject01;
// 定义kobject结构体指针,用于表示第二个自定义内核对象
struct kobject *mykobject02;
// 定义kset结构体指针,用于表示自定义内核对象的集合
struct kset *mykset;
// 定义kobj_type结构体,用于定义自定义内核对象的类型
struct kobj_type mytype;

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 创建并添加kset,名称为"mykset",父kobject为NULL,属性为NULL
    mykset = kset_create_and_add("mykset", NULL, NULL);

    // 为mykobject01分配内存空间,大小为struct kobject的大小,标志为GFP_KERNEL
    mykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    // 将mykset设置为mykobject01的kset属性
    mykobject01->kset = mykset;
    // 初始化并添加mykobject01,类型为mytype,父kobject为NULL,格式化字符串为"mykobject01"
    ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");

    // 为mykobject02分配内存空间,大小为struct kobject的大小,标志为GFP_KERNEL
    mykobject02 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    // 将mykset设置为mykobject02的kset属性
    mykobject02->kset = mykset;
    // 初始化并添加mykobject02,类型为mytype,父kobject为NULL,格式化字符串为"mykobject02"
    ret = kobject_init_and_add(mykobject02, &mytype, NULL, "%s", "mykobject02");

    return 0;
}

// 模块退出函数
static void mykobj_exit(void)
{
    // 释放mykobject01的引用计数
    kobject_put(mykobject01);

    // 释放mykobject02的引用计数
    kobject_put(mykobject02);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

87.2 运行测试

87.2.1 编译驱动程序

在上一小节中的make_kset.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成make_kset.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

87.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图87-4)所示:

insmod make_kset.ko

image-20240906113320309

驱动加载之后,我们进入/sys/目录下,可以看到创建生成的kset,如下图所示,我们进到mykset 目录下,可以看到创建的kobject

image-20240906113336586

最后可以使用以下命令进行驱动的卸载,如下图(图87-6)所示:

rmmod make_kset

至此,创建kset 实验就完成了。

第88 章为什么要引入设备模型

88.1 设备模型简介

设备模型在内核驱动中扮演着关键的角色,通过提供统一的设备描述和管理机制,简化了驱动的编写和维护过程,提高了代码的复用性和可维护性,并支持热插拔和动态资源管理等重要功能。设备模型包含以下四个概念:

  1. 总线(Bus):总线是设备模型中的基础组件,用于连接和传输数据的通信通道。总线可以是物理总线(如PCI、USB)或虚拟总线(如虚拟设备总线)。总线提供了设备之间进行通信和数据传输的基本机制。
  2. 设备(Device):设备是指计算机系统中的硬件设备,例如网卡、显示器、键盘等。每个设备都有一个唯一的标识符,用于在系统中进行识别和管理。设备模型通过设备描述符来描述设备的属性和特性
  3. 驱动(Driver):驱动是设备模型中的软件组件,用于控制和管理设备的操作。每个设备都需要相应的驱动程序来与操作系统进行交互和通信。驱动程序负责向设备发送命令、接收设备事件、进行设备配置等操作
  4. 类(Class):类是设备模型中的逻辑组织单元,用于对具有相似功能和特性的设备进行分类和管理。类定义了一组共享相同属性和行为的设备的集合。通过设备类,可以对设备进行分组、识别和访问

设备模型的设计目的是为了提供一种统一的方式来管理和操作系统中的各种硬件设备。通过将设备、驱动和总线等概念进行抽象和标准化,设备模型可以提供一致的接口和数据结构,简化驱动开发和设备管理,并实现设备的兼容性和可移植性。

88.2 相关结构体

在Linux 设备模型中,虚构了一条名为“platform”的总线,用来连接一些直接与CPU 相连的设备控制器。这种设备控制器通常不符合常见的总线标准,比如PCI 总线和USB 总线,所以Linux 使用platform 总线来管理这些设备。Platform 总线允许设备控制器与设备驱动程序进行通信和交互。设备控制器在设备树中定义,并通过设备树与对应的设备驱动程序匹配。在设备模型中,Platform 总线提供了一种统一的接口和机制来注册和管理这些设备控制器。设备驱动程序可以通过注册到Platform 总线的方式,与相应的设备控制器进行绑定和通信。设备驱动程序可以访问设备控制器的寄存器、配置设备、处理中断等操作,如下图所示:

image-20240906114119162

当作为嵌入式/驱动开发人员时,了解设备,驱动程序,总线和类这几个结构体的概念和关系非常重要。尽管在芯片原厂提供的BSP 中已经实现了设备模型,但是了解这些概念可以帮助您更好地理解设备的工作原理,驱动程序的编写和设备的管理。

88.2.1 struct bus_type

bus_type 结构体是Linux 内核中用于描述总线的数据结构,定义在include/linux/device.h头文件中。以下是对bus_type 结构体的一般定义。

struct bus_type {
    const char *name;
    const char *dev_name;
    struct device *dev_root;
    const struct attribute_group **bus_groups;
    const struct attribute_group **dev_groups;
    const struct attribute_group **drv_groups;
    
    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    void (*sync_state)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);
    int (*online)(struct device *dev);
    int (*offline)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    int (*num_vf)(struct device *dev);
    int (*dma_configure)(struct device *dev);
    
    const struct dev_pm_ops *pm;
    const struct iommu_ops *iommu_ops;
    
    struct subsys_private *p;
    struct lock_class_key lock_key;
    bool need_parent_lock;
    
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
};

以下是结构体包含的成员和其作用的简要说明

  • name:总线类型的名称
  • dev_name:总线设备名称
  • dev_root: 总线设备的根设备
  • bus_groups: 总线类型的属性组
  • dev_groups: 设备的属性组
  • drv_groups: 驱动程序的属性组

以下是一些回调函数成员

  • match: 设备和驱动程序之间的匹配函数
  • uevent:设备的事件处理函数
  • probe: 设备的探测函数
  • sync_state: 设备状态同步函数
  • remove: 设备的移除函数
  • online:设备上线函数
  • offline:设备离线函数
  • suspend: 设备的挂起函数
  • resume: 设备的恢复函数
  • num_vf: 设备的虚拟功能数目函数
  • dma_configure:设备的DMA 配置函数

以下是一些其他成员:

  • pm:设备的电源管理操作。
  • iommu_ops:设备的IOMMU 操作。
  • p:子系统私有数据。
  • lock_key:用于锁机制的锁类别键。
  • need_parent_lock:是否需要父级锁。

88.2.2 struct device

device 结构体是Linux 内核中用于描述设备的数据结构,定义在include/linux/device.h 头文件中。以下是device 结构体的一般定义:

struct device {
    //设备的父设备
    struct device *parent;
    //私有指针
    struct device_private *p;
    //对应的kobj
    struct kobject kobj;
    //设备初始化的名字
    const char *init_name;
    //设备类型
    const struct device_type *type;
    //设备所属的总线
    struct bus_type *bus;
    struct device_driver *driver;
    ...........
    //设备所属的类
    struct class *class;
    //设备的属性组
    const struct attribute_group **groups; /* optional groups */
    ...........
};

88.2.3 struct device_driver

struct device_driver 是Linux 内核中描述设备驱动程序的数据结构, 定义在include/linux/device.h 头文件中。以下是struct device_driver 的一般定义:

struct device_driver {
    const char *name;
    struct bus_type *bus;
    
    struct module *owner;
    const char *mod_name; /* used for built-in modules */
    
    bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
    enum probe_type probe_type;
    
    const struct of_device_id *of_match_table;
    const struct acpi_device_id *acpi_match_table;
    
    int (*probe) (struct device *dev);
    void (*sync_state)(struct device *dev);
    int (*remove) (struct device *dev);
    void (*shutdown) (struct device *dev);
    int (*suspend) (struct device *dev, pm_message_t state);
    int (*resume) (struct device *dev);
    const struct attribute_group **groups;
    
    const struct dev_pm_ops *pm;
    void (*coredump) (struct device *dev);
    
    struct driver_private *p;
    
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
};

以下是该结构体包含的成员和其作用的简要说明。

  • name:设备驱动程序的名称
  • bus: 设备驱动程序所属的总线类型
  • owner:拥有该驱动程序的模块。
  • mod_name:用于内置模块的名称。
  • suppress_bind_attrs:禁用通过sysfs 进行绑定/解绑的属性。
  • probe_type:探测类型,用于指定探测的方式。
  • of_match_table:Open Firmware(OF)设备匹配表。
  • acpi_match_table:ACPI 设备匹配表。
  • groups:驱动程序的属性组。
  • pm:电源管理操作。
  • p:驱动程序的私有数据。

以下是一些回调函数成员

  • probe:设备的探测函数,用于初始化和配置设备,注意:device 和device_driver 必须挂在同一个bus 下。这样才可以触发probe
  • sync_state: 设备状态同步函数
  • remove:设备的移除函数
  • shutdown: 设备的关机函数
  • suspend: 设备的挂起函数
  • resume: 设备的恢复函数
  • coredump: 设备的核心转储函数

88.2.4 struct class

struct class 是Linux 内核中描述设备类的数据结构,定义在include/linux/device.h 头文件中。以下是struct class 的一般定义:

struct class {
    const char *name;
    struct module *owner;
    
    const struct attribute_group **class_groups;
    const struct attribute_group **dev_groups;
    struct kobject *dev_kobj;
    
    int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
    char *(*devnode)(struct device *dev, umode_t *mode);
    
    void (*class_release)(struct class *class);
    void (*dev_release)(struct device *dev);
    
    int (*shutdown_pre)(struct device *dev);
    
    const struct kobj_ns_type_operations *ns_type;
    const void *(*namespace)(struct device *dev);
    
    void (*get_ownership)(struct device *dev, kuid_t *uid, kgid_t *gid);
    
    const struct dev_pm_ops *pm;
    
    struct subsys_private *p;
    
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
};

以下是该结构体包含的成员和其作用的简要说明

  • name : 设备类的名称
  • owner:拥有该类的模块

以下是一些属性组成员

  • class_groups: 类属性组, 用于描述设备类的属性, 在类注册到内核时, 会自动在/sys/class/xxx_class 下创建对应的属性文件
  • dev_groups: 设备属性组,用于描述设备的属性,在类注册到内核时,会自动在该类下的设备目录创建对应的属性文件
  • dev_kobj: 设备的内核对象

以下是一些回调函数成员

  • dev_uevent :设备的事件处理函数
  • devnode : 生成设备节点的函数

以下是一些其他成员

  • class_release: 类资源的释放函数
  • dev_release: 设备资源的释放函数
  • shutdown_pre: 设备关机前的回调函数
  • ns_type: 命名空间类型操作
  • namespace: 命名空间函数
  • get_ownership: 获取设备所有权的函数
  • pm: 电源管理操作
  • p: 子系统私有数据

第89 章进一步探究设备模型

在前面的章节中,我们介绍了设备模型四个重要的组成部分:总线,设备,驱动,类。然而,要更深入地理解设备模型,我们需要进一步探究其代码层面的实现。在第86 章节实验中,我们创建了kobject。本章节我们从代码的层面分析下——为什么当创建kobj 的时候,父节点为NULL,会在系统根目录/sys 目录下创建呢。

89.1 什么是sysfs 文件系统

sysfs 文件系统是Linux 内核提供的一种虚拟文件系统,用于向用户空间提供内核中设备,驱动程序和其他内核对象的信息。它以一种层次结构的方式组织数据,并将这些数据表示为文件和目录,使得用户空间可以通过文件系统接口访问和操作内核对象的属性。

sysfs 提供了一种统一的接口,用于浏览和管理内核中的设备、总线、驱动程序和其他内核对象。它在/sys 目录下挂载,用户可以通过查看和修改/sys 目录下的文件和目录来获取和配置内核对象的信息。

89.2 设备模型的基本框架

为什么说kobject 和kset 是设备模型的基本框架呢?本小节来进行详细阐述。
当使用kobject 时,通常不会单独使用它,而是将其嵌入到一个数据结构中。这样做的目的是将高级对象接入到设备模型中。比如cdev 结构体和platform_device 结构体,如下所示:

cdev 结构体如下所示,其成员有kobject

struct cdev {
    struct kobject kobj;		//内嵌到cdev 中的kobject
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
}

platform_device 结构体如下所示,其成员有device 结构体,在device 结构体中包含了kobject结构体。

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

device 结构体如下所示:

struct device {
    //设备的父设备
    struct device *parent;
    //私有指针
    struct device_private *p;
    
    //对应的kobj
    struct kobject kobj;
    
    //设备初始化的名字
    const char *init_name;
    //设备类型
    const struct device_type *type;
    //设备所属的总线
    struct bus_type *bus;
    struct device_driver *driver;
    ...........
    //设备所属的类
    struct class *class;
    //设备的属性组
    const struct attribute_group **groups; /* optional groups */
    ...........
};

所以我们也可以把总线,设备,驱动看作是kobject 的派生类。因为他们都是设备模型中的实体,通过继承或扩展kobject 来实现与设备模型的集成。
在Linux 内核中,kobject 是一个通用的基础结构,用于构建设备模型。每个kobject 实例对应于sys 目录下的一个目录,这个目录包含了该kobject 相关的属性,操作和状态信息。如下图所示:(纠正:下图的bus的kobject的子右边 应该是derive的kobject)

image-20240906120105090

因此,可以说kobject 是设备模型的基石,通过创建对应的目录结构和属性文件, 它提供了一个统一的接口和框架,用于管理和操作设备模型中的各个实体。

89.3 代码层面分析

在系统启动的时候会在/sys 目录下创建以下目录,如下图所示:

image-20240906135716693

让我们从代码层面一层层解释一下为什么当使用kobject_create_and_add()函数创建kobject 时,父节点为NULL 会在系统根目录/sys 下创建。
逐步追踪路径如下所示:

kobject_create_and_add->kobject_add->kobject_add_varg->kobject_add_internal->
    create_dir->sysfs_create_dir_ns(fs/sysfs/dir.c)

接下来我们看一下kobject_create_and_add 函数实现,如下所示:

struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
{
    struct kobject *kobj;
    int retval;
    kobj = kobject_create();
    if (!kobj)
        return NULL;
    retval = kobject_add(kobj, parent, "%s", name);
    if (retval) {
        pr_warn("%s: kobject_add error: %d\n", __func__, retval);
        kobject_put(kobj);
        kobj = NULL;
    }
    return kobj;
}
EXPORT_SYMBOL_GPL(kobject_create_and_add);

在上述代码中,首先调用了kobject_create()函数创建看一个新的kobject。该函数分配了一个新的kobject 结构体,并对其进行初始化,包括将kobject 的name 字段设置为传入的name参数,将kobject 的parent 字段设置为传入的parent 参数。

接下来,函数调用kobject_add()将新创建的kobject 添加到设备模型中,我们进一步追究下kobject_add()实现,如下所示:

int kobject_add(struct kobject *kobj, struct kobject *parent,const char *fmt, ...){
    va_list args;
    int retval;
    if (!kobj)
        return -EINVAL;
    if (!kobj->state_initialized) {
        pr_err("kobject '%s' (%p): tried to add an uninitialized object, something is seriously wrong.\n",kobject_name(kobj), kobj);
        dump_stack();
        return -EINVAL;
    }
    va_start(args, fmt);
    retval = kobject_add_varg(kobj, parent, fmt, args);
    va_end(args);
    return retval;
}

在上述函数中,函数调用kobject_add_varg(),该函数会根据传入的参数将kobj 添加到设备模型中。kobject_add_varg()函数的实现可能会根据具体情况进行一些额外的处理,例如创建对应的目录并设置父节点等。我们进一步探究下kobject_add_varg()函数的实现,如下所示:

static __printf(3, 0) int kobject_add_varg(struct kobject *kobj,struct kobject *parent,
                                           const char *fmt, va_list vargs)
{
    int retval;
    retval = kobject_set_name_vargs(kobj, fmt, vargs);
    if (retval) {
        pr_err("kobject: can not set name properly!\n");
        return retval;
    }
    kobj->parent = parent;
    return kobject_add_internal(kobj);
}

在上述函数中,kobject_add_varg()函数用于将指定的kobject 添加到设备模型中。它通过调用kobject_set_name_vargs()设置kobject 的名称,并将父节点赋值给kobject 的parent 字段。然后,它调用kobject_add_internal()函数执行实际的添加操作。接下来我们进一步探究下kobject_add_internal()函数的实现,如下所示:

static int kobject_add_internal(struct kobject *kobj)
{
    int error = 0;
    struct kobject *parent;
    if (!kobj)
        return -ENOENT;
    
    if (!kobj->name || !kobj->name[0]) {
        WARN(1,"kobject: (%p): attempted to be registered with empty name!\n",kobj);
        return -EINVAL;
    }
    parent = kobject_get(kobj->parent);
    
    /* join kset if set, use it as parent if we do not already have one */
    if (kobj->kset) {
        if (!parent)
            parent = kobject_get(&kobj->kset->kobj);
        kobj_kset_join(kobj);
        kobj->parent = parent;
    }
    pr_debug("kobject: '%s' (%p): %s: parent: '%s', set: '%s'\n",\
             object_name(kobj), kobj, __func__,
             parent ? kobject_name(parent) : "<NULL>",
             kobj->kset ? kobject_name(&kobj->kset->kobj) : "<NULL>");
    
    error = create_dir(kobj);
    if (error) {
        kobj_kset_leave(kobj);
        kobject_put(parent);
        kobj->parent = NULL;
        /* be noisy on error issues */
        if (error == -EEXIST)
            pr_err("%s failed for %s with -EEXIST, don't try to register things with the same name in the same directory.\n",__func__, kobject_name(kobj));
        else
            pr_err("%s failed for %s (error: %d parent: %s)\n",__func__, kobject_name(kobj), 
                   error,parent ? kobject_name(parent) : "'none'");
    } 
    else
        kobj->state_in_sysfs = 1;
    return error;
}

kobject_add_internal()函数用于在设备模型中添加指定的kobject。它会检查kobject 的有效性和名称是否为空,并处理kobject 所属的kset 相关的操作。然后,它会创建kobject 在sysfs中的目录,并处理创建失败的情况。最后,它会设置kobject 的相关状态,并返回相应的错误。我们继续追究create_dir()函数的实现,如下所示:

static int create_dir(struct kobject *kobj)
{
    const struct kobj_ns_type_operations *ops;
    
    int error;
    error = sysfs_create_dir_ns(kobj, kobject_namespace(kobj));
    if (error)
        return error;
    
    error = populate_dir(kobj);
    if (error) {
        sysfs_remove_dir(kobj);
        return error;
    }
    /*
    * @kobj->sd may be deleted by an ancestor going away. Hold an
    * extra reference so that it stays until @kobj is gone.
    */
    sysfs_get(kobj->sd);
    
    /*
    * If @kobj has ns_ops, its children need to be filtered based on
    * their namespace tags. Enable namespace support on @kobj->sd.
    */
    ops = kobj_child_ns_ops(kobj);
    if (ops) {
        BUG_ON(ops->type <= KOBJ_NS_TYPE_NONE);
        BUG_ON(ops->type >= KOBJ_NS_TYPES);
        BUG_ON(!kobj_ns_type_registered(ops->type));
        
        sysfs_enable_ns(kobj->sd);
    }
    return 0;
}

create_dir()函数用于创建与给定kobject 相关联的目录,并填充该目录。它还处理了引用计数、命名空间操作等相关的逻辑。如果创建目录或填充目录时发生错误,函数会相应地处理并返回错误码。函数调用sysfs_create_dir_ns()函数来创建与kobj 相关联的目录。sysfs_create_dir_ns实现如下所示:

/**
* sysfs_create_dir_ns - create a directory for an object with a namespace tag
* @kobj: object we're creating directory for
* @ns: the namespace tag to use
*/
int sysfs_create_dir_ns(struct kobject *kobj, const void *ns)
{
    struct kernfs_node *parent, *kn;
    kuid_t uid;
    kgid_t gid;
    BUG_ON(!kobj);
    
    if (kobj->parent)
        parent = kobj->parent->sd;
    else
        parent = sysfs_root_kn;
    
    if (!parent)
        return -ENOENT;
    kobject_get_ownership(kobj, &uid, &gid);
    
    kn = kernfs_create_dir_ns(parent, kobject_name(kobj),
                              S_IRWXU | S_IRUGO | S_IXUGO, uid, gid,kobj, ns);
    if (IS_ERR(kn)) {
        if (PTR_ERR(kn) == -EEXIST)
            sysfs_warn_dup(parent, kobject_name(kobj));
        return PTR_ERR(kn);
    }
    kobj->sd = kn;
    return 0;
}

在上面的函数中,当没有父节点的时候,父节点被赋值成了sysfs_root_kn,即/sys 目录根目录的节点。如果有parent,则它的父节点为kobj->parent->sd,然后调用kernfs_create_dir_ns创建目录。那么sysfs_root_kn 是在什么时候创建的呢?我们找到fs/sysfs/mount.c 文件,如下所示:

int __init sysfs_init(void)
{
    int err;
    sysfs_root = kernfs_create_root(NULL, KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK,NULL);
    if (IS_ERR(sysfs_root))
        return PTR_ERR(sysfs_root);
    
    sysfs_root_kn = sysfs_root->kn;
    err = register_filesystem(&sysfs_fs_type);
    if (err) {
        kernfs_destroy_root(sysfs_root);
        return err;
    }
    return 0;
}

通过上述对API 函数的分析,我们可以总结出创建目录的规律,如下所示:

  • 1、无父目录、无kset,则将在sysfs 的根目录(即/sys/)下创建目录。
  • 2、无父目录、有kset,则将在kset 下创建目录,并将kobj 加入kset.list。
  • 3、有父目录、无kset,则将在parent 下创建目录。
  • 4、有父目录、有kset,则将在parent 下创建目录,并将kobj 加入kset.list。

第90 章虚拟文件系统sysfs 目录层次分析实验

90.1 sys 目录对设备模型的层次结构

我们进入到Linux 系统的/sys 目录下,可以看到如下文件夹。

image-20240906141940761

和设备模型有关的文件夹为bus,class,devices。完整路径为如下所示:

/sys/bus
/sys/class
/sys/devices

/sys/devices该目录包含了系统中所有设备的子目录。每个设备子目录代表一个具体的设备,通过其路径层次结构和符号链接反映设备的关系和拓扑结构。每个设备子目录中包含了设备的属性、状态和其他相关信息。如下所示:

image-20240906142036237

/sys/bus该目录包含了总线类型的子目录。每个子目录代表一个特定类型的总线,例如PCI、USB 等。每个总线子目录中包含与该总线相关的设备和驱动程序的信息。

image-20240906142224260

比如I2C 总线下连接的设备,如下所示:

image-20240906142244908

/sys/class该目录包含了设备类别的子目录。每个子目录代表一个设备类别,例如磁盘、网络接口等。每个设备类别子目录中包含了属于该类别的设备的信息。如下图所示:

image-20240906142413539

使用class 进行归类的好处有以下几点:

  1. 逻辑上的组织:通过将设备按照类别进行归类,可以在设备模型中建立逻辑上的组织结构。这样,相关类型的设备可以被放置在同一个类别目录下,使得设备的组织结构更加清晰和可管理。
  2. 统一的接口和属性:每个设备类别目录下可以定义一组统一的接口和属性,用于描述和配置该类别下所有设备的共同特征和行为。这样,对于同一类别的设备,可以使用相同的方法和属性来操作和配置,简化了设备驱动程序的编写和维护。
  3. 简化设备发现和管理:通过将设备进行分类,可以提供一种简化的设备发现和管理机制。用户和应用程序可以在类别目录中查找和识别特定类型的设备,而无需遍历整个设备模型。这样,设备的发现和访问变得更加高效和方便。
  4. 扩展性和可移植性:使用class进行归类可以提供一种扩展性和可移植性的机制。当引入新的设备类型时,可以将其归类到现有的类别中,而无需修改现有的设备管理和驱动程序。这种扩展性和可移植性使得系统更加灵活,并且对于开发人员和设备供应商来说,更容易集成新设备。

比如应用现在要设置gpio
如果使用类可以直接使用以下命令:

echo 1 > /sys/class/gpio/gpio157/value

如果不使用类,使用以下命令:

echo 1 > /sys/devices/platform/fe770000.gpio/gpiochip4/gpio/gpio157/value

90.2 sys 目录层次图解

Sys 目录层次结构如下图所示:

image-20240906142632399

第91 章什么是引用计数器

91.1 什么是引用计数器。

引用计数器(reference counting)是一种内存管理技术,用于跟踪对象或资源的引用数量。它通过在对象被引用时增加计数值,并在引用被释放时减少计数值,以确定何时可以安全地释放对象或资源。

引用计数器的基本原理如下:

  • 对象或资源被创建时,引用计数器初始化为1。
  • 当有新的引用指向对象或资源时,引用计数器增加。
  • 当引用不再指向对象或资源时(引用被删除、超出作用域等),引用计数器减少。
  • 当引用计数器的值为0 时,表示没有任何引用指向对象或资源,可以安全地释放对象或资源,并进行相关的清理操作。

91.2 引用计数器kref 介绍

kref 是Linux 内核中提供的一种引用计数器实现,它是一种轻量级的引用计数技术,用于管理内核中的对象的引用计数。

在Linux 系统中,引用计数器用结构体kref 来表示。struct kref 定义在include/linux/kref.h头文件当中,本质是一个int 型变量。如下所示:

struct kref {
    refcount_t refcount;
};

typedef struct {
    atomic_t refs;
} refcount_t;

typedef struct {
    int counter;
} atomic_t;

在使用引用计数器时,通常会将结构体kref 嵌入到其他结构体中,例如struct kobject,以实现引用计数的管理。如下所示:

image-20240906143002280

为了实现引用计数功能,struct kobject 通常会包含一个嵌入的struct kref 对象。这样可以通过对struct kref 的操作来对struct kobject 进行引用计数的管理,并在引用计数减少到0 时释放相关资源。

再比如结构体device_node,如下所示:

image-20240906143027798

91.3 常用api 函数

struct kref 提供了一些常用的API 函数来进行引用计数的增加和减少操作,下面是一些常用的struct kref API 函数。

kref_init(struct kref *kref):

函数作用:初始化一个struct kref 对象。在使用引用计数之前,必须先调用此函数进行初始化。初始化kerf 的值为1
函数原型:
static inline void kref_init(struct kref *kref){
    refcount_set(&kref->refcount, 1);
}

kref_get(struct kref *kref):

函数作用:增加struct kref 的引用计数。每次调用此函数,引用计数都会增加。kref 计数值加1
函数原型:
static inline void kref_get(struct kref *kref){
    refcount_inc(&kref->refcount);
}

kref_put(struct kref *kref, void (*release)(struct kref *)):

函数作用:减少struct kref 的引用计数,并在引用计数减少到零时调用release 函数来进行资源的释放。通常,release 函数会在其中执行对象的销毁和内存释放等操作。
函数原型:
static inline int kref_put(struct kref *kref, void (*release)(struct kref *kref))
{
    if (refcount_dec_and_test(&kref->refcount)) {
        release(kref);
        return 1;
    }
    return 0;
}

void refcount_set(refcount_t *r, int n)

函数作用:设置kerf 的计数值。
函数原型:
static inline void refcount_set(refcount_t *r, int n){
    atomic_set(&r->refs, n);
}

第92 章引用计数器实验

92.1 实验程序的编写

92.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\68_kref\module

我们编写驱动代码,这段代码用于定义并初始化两个自定义内核对象mykobject01 和mykobject02,并将它们添加到一个自定义内核对象集合mykset 中。这些自定义内核对象可以用于在Linux 内核中表示和管理特定的功能或资源。代码中的注释对各个部分进行了解释,帮助理解代码的功能和目的。编写完成的kref.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

// 定义了三个kobject指针变量:mykobject01、mykobject02、mykobject03
struct kobject *mykobject01;
struct kobject *mykobject02;
struct kobject *mykobject03;

// 定义了一个kobj_type结构体变量mytype,用于描述kobject的类型。
struct kobj_type mytype;
// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;
    // 创建kobject的第一种方法
    // 创建并添加了名为"mykobject01"的kobject对象,父kobject为NULL
    mykobject01 = kobject_create_and_add("mykobject01", NULL);
    printk("mykobject01 kref is %d\n", mykobject01->kref.refcount.refs.counter);

    // 创建并添加了名为"mykobject02"的kobject对象,父kobject为mykobject01。
    mykobject02 = kobject_create_and_add("mykobject02", mykobject01);
    printk("mykobject01 kref is %d\n", mykobject01->kref.refcount.refs.counter);
    printk("mykobject02 kref is %d\n", mykobject02->kref.refcount.refs.counter);
    // 创建kobject的第二种方法
    // 1 使用kzalloc函数分配了一个kobject对象的内存
    mykobject03 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    // 2 初始化并添加到内核中,名为"mykobject03"。
    ret = kobject_init_and_add(mykobject03, &mytype, NULL, "%s", "mykobject03");
    printk("mykobject03 kref is %d\n", mykobject03->kref.refcount.refs.counter);
    return 0;
}

// 模块退出函数
static void mykobj_exit(void)
{
    printk("mykobject01 kref is %d\n", mykobject01->kref.refcount.refs.counter);
    printk("mykobject02 kref is %d\n", mykobject02->kref.refcount.refs.counter);
    printk("mykobject03 kref is %d\n", mykobject03->kref.refcount.refs.counter);
    // 释放了之前创建的kobject对象
    kobject_put(mykobject01);
    printk("mykobject01 kref is %d\n", mykobject01->kref.refcount.refs.counter);
    printk("mykobject02 kref is %d\n", mykobject02->kref.refcount.refs.counter);
    printk("mykobject03 kref is %d\n", mykobject03->kref.refcount.refs.counter);
    kobject_put(mykobject02);
    printk("mykobject01 kref is %d\n", mykobject01->kref.refcount.refs.counter);
    printk("mykobject02 kref is %d\n", mykobject02->kref.refcount.refs.counter);
    printk("mykobject03 kref is %d\n", mykobject03->kref.refcount.refs.counter);
    kobject_put(mykobject03);
    printk("mykobject01 kref is %d\n", mykobject01->kref.refcount.refs.counter);
    printk("mykobject02 kref is %d\n", mykobject02->kref.refcount.refs.counter);
    printk("mykobject03 kref is %d\n", mykobject03->kref.refcount.refs.counter);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

92.2 运行测试

92.2.1 编译驱动程序

在上一小节中的kref.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成kref.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

92.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图92-4)所示:

insmod kref.ko

image-20240909094539148

如上图所示,驱动加载之后,第一条打印为“mykobject01 kref is 1”,因为创建了mykobject01,所以引用计数器的值为1,如下图所示的I。第二条打印为:“mykobject01 kref is 2”,因为在mykobject01 目录下创建了子目录mykobject02,所以mykobject01 的计数器值为2,mykobject02的计数器值为1,如下图所示的II。

image-20240909094610589

现在我们拓展学习一下,如上图III 所示,如果在objectA 下面创建俩个object,objectA 的计数器值为3。如上图所示IV,如果在objectA 下面创建俩个object,那么objectA 的计数器值为3,在objectB 下创建object,那么objectB 的计数器值为2,objectC 的计数器值为1。最后可以使用以下命令进行驱动的卸载,如下图(图21-11)所示:

rmmod kref

image-20240909094631377

如上图所示,计数器的值打印如上。当引用计数器的值为0 时,表示没有任何引用指向对象或资源,可以安全地释放对象或资源,并进行相关的清理操作。
至此,引用计数器实验就完成了。

第93 章kobject 释放实例分析实验

通过上个章节的实验,我们已经知道引用计数器是如何工作的。当引用计数器的值变为0后,会自动调用自定义的释放函数去执行释放的操作。那么kboj 是如何释放的呢,本章节我们来从代码层面进行下分析。

93.1 Kobj 是如何释放的?

要学习kobject 是如何释放的,那么我们要先明白kobj 是如何创建的。对于kobject 的创建,我们可以进一步分析这两种方法的实现细节。

  1. 使用kobject_create_and_add()函数创建kobject:
  • kobject_create_and_add()函数首先调用kobject_create()函数,该函数使用kzalloc()为kobject 分配内存空间。在kobject_create()函数中,调用kobject_init()函数对分配的内存进行初始化,并指定了默认的ktype。接下来,kobject_create_and_add()函数调用kobject_add()函数将kobject 添加到系统中,使其可见。
  • kobject_add()函数内部调用了kobject_add_internal()函数,该函数负责将kobject 添加到父对象的子对象列表中,并创建相应的sysfs 文件系统条目。
  1. 使用kobject_init_and_add()函数创建kobject:
    kobject_init_and_add()函数需要手动分配内存,并通过kobject_init()函数对分配的内存进行初始化。此时需要自己实现ktype 结构体。初始化完成后,调用kobject_add()函数将kobject添加到系统中。

无论是哪种方法,最终都会调用kobject_add()函数将kobject 添加到系统中,以使其可见。了解了kobject 的创建过程后,我们可以深入学习kobject 的释放过程。
我们来追踪下kobject 的释放函数——kobject_put()函数的实现。
在Linux 内核中,kobject_put()函数用于减少kobject 的引用计数,并在引用计数达到0 时释放kobject 相关的资源。如下图所示,在函数里面,当引用计数器的值变为0 以后,会调用release 函数执行释放的操作。

image-20240909094937461

Linux 系统帮我们实现好了释放函数,如下图所示:

image-20240909094947629

在上图的release 函数中,该函数最终会去调用kobject_cleanup 函数,kobject_cleanup 函数实现如下图所示:

image-20240909095008351

如上图所示,函数定义了一个名叫kobject_cleanup 的静态函数,参数为一个指向struct kobject 结构体的指针kobj。函数内部定义了一个指向struct kobj_type 结构体的指针t,用于获取kobj 的类型信息。还定义了一个指向常量字符的指针name,用于保存kobj 的名称。
接下来,使用pr_debug 打印调试信息,显示kobject 的名称、地址、函数名称和父对象的地址。
然后,检查kobj 的类型信息t 是否存在,并且检查t->release 是否为NULL。如下图所示:

image-20240909095639007

如果t 存在但t->release 为NULL,表示kobj 的类型没有定义释放函数,会打印调试信息指示该情况。

接下来,检查kobj 的状态变量state_add_uevent_sent 和state_remove_uevent_sent。如果state_add_uevent_sent 为真而state_remove_uevent_sent 为假,表示调用者没有发送”remove” 事件,会自动发送”remove” 事件。

然后,检查kobj 的状态变量state_in_sysfs。如果为真,表示调用者没有从sysfs 中删除kobj,会自动调用kobject_del() 函数将其从sysfs 中删除。

接下来,再次检查t 是否存在,并且检查t->release 是否存在。如果存在,表示kobj 的类型定义了释放函数,会调用该释放函数进行资源清理。如下图所示:

image-20240909095751067

最后,检查name 是否存在。如果存在,表示kobj 的名称是动态分配的,会释放该名称的内存。这就是kobject_cleanup() 函数的实现。它负责执行kobject 的资源清理和释放操作,包括处理类型信息、发送事件、删除sysfs 中的对象以及调用释放函数。

kobject_cleanup() 函数的实现表明,最终调用的释放函数是在kobj_type 结构体中定义的。这解释了为什么在使用kobject_init_and_add() 函数时,kobj_type 结构体不能为空的原因。因为释放函数是在kobj_type 结构体中定义的,如果不实现释放函数,就无法进行正确的资源释放。

接下来,我们来看一下在Linux 内核中,dynamic_kobj_ktype 是一个kobj_type 结构体对象,用于定义动态创建的kobject 的类型。它指定了释放函数和sysfs 操作。如下图所示:

image-20240909100236371

如下图所示:dynamic_kobj_release 函数如下图所示:

image-20240909100555658

在上图中,使用kfree 函数对创建的kobj 进行了释放。总结起来,kobj_type 结构体中的释放函数是为了确保在释放kobject 时执行必要的资源清理和释放操作,以确保系统的正确运行

第94 章引入并完善kobject_type 结构体

在上个章节中,我们掌握了kobject_init_and_add()函数需要手动分配内存,并通过kobject_init()函数对分配的内存进行初始化。此时需要自己实现ktype 结构体。那么ktype 结构体如何实现呢,本章节将进一步学习,以实验的方式带着大家进行操作。

94.1 实验程序的编写

94.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\69_ktype\module
我们编写驱动代码,该代码实现了一个简单的内核模块,创建了一个自定义的kobject 对象,并定义了相应的初始化和释放函数。编写完成的ktype.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

// 定义了kobject指针变量:mykobject03
struct kobject *mykobject03;

// 定义kobject的释放函数
static void dynamic_kobj_release(struct kobject *kobj)
{
    printk("kobject: (%p): %s\n", kobj, __func__);
    kfree(kobj);
}

// 定义了一个kobj_type结构体变量mytype,用于描述kobject的类型。
struct kobj_type mytype = {
    .release = dynamic_kobj_release,
};

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 创建kobject的第二种方法
    // 1 使用kzalloc函数分配了一个kobject对象的内存
    mykobject03 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    // 2 初始化并添加到内核中,名为"mykobject03"。
    ret = kobject_init_and_add(mykobject03, &mytype, NULL, "%s", "mykobject03");

    return 0;
}

// 模块退出函数
static void mykobj_exit(void)
{
    kobject_put(mykobject03);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

94.2 运行测试

94.2.1 编译驱动程序

在上一小节中的ktype.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ktype.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

94.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图94-4)所示:

insmod ktype.ko

image-20240909101823249

驱动加载之后,最后可以使用以下命令进行驱动的卸载,如上图(图94-4)所示,dynamic_kobj_release 函数被成功执行。

rmmod ktype

至此,引入并完善kobject_type 结构体实验就完成了。

第95 章创建属性文件并实现读写功能实验1

本章节我们来探索如何创建具有读写功能的属性文件。属性文件通过sysfs 文件系统提供了一种方便的方式来与内核对象进行交互。我们将深入研究一个代码示例,演示如何创建具有属性的自定义kobject,使我们能够读取和写入值。让我们开始吧!

95.1 实验程序的编写

95.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\70_attr\module
我们编写驱动代码,该代码创建了一个自定义的kobject,并在sysfs 中创建了两个属性——value1 和value2,允许读取和写入这些属性的值。模块的初始化函数负责创建和添加kobject,而退出函数则负责释放相关资源。编写完成的attr.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h> 

// 自定义的kobject结构体,包含一个kobject对象和两个整型值
struct mykobj
{
    struct kobject kobj;
    int value1;
    int value2;
};

// 定义了mykobj结构体指针变量mykobject01
struct mykobj *mykobject01;

// 自定义的kobject释放函数
static void dynamic_kobj_release(struct kobject *kobj)
{
    struct mykobj *mykobject01 = container_of(kobj, struct mykobj, kobj);
    printk("kobject: (%p): %s\n", kobj, __func__);
    kfree(mykobject01);
}

// 自定义的attribute对象value1和value2
struct attribute value1 = {
    .name = "value1",
    .mode = 0666,
};
struct attribute value2 = {
    .name = "value2",
    .mode = 0666,
};

// 将attribute对象放入数组中
struct attribute *myattr[] = {
    &value1,
    &value2,
    NULL,
};

// 自定义的show函数,用于读取属性值
ssize_t myshow(struct kobject *kobj, struct attribute *attr, char *buf)
{
    ssize_t count;
    struct mykobj *mykobject01 = container_of(kobj, struct mykobj, kobj);
    if (strcmp(attr->name, "value1") == 0)
    {
        count = sprintf(buf, "%d\n", mykobject01->value1);
    }
    else if (strcmp(attr->name, "value2") == 0)
    {
        count = sprintf(buf, "%d\n", mykobject01->value2);
    }
    else
    {
        count = 0;
    }
    return count;
}

// 自定义的store函数,用于写入属性值
ssize_t mystore(struct kobject *kobj, struct attribute *attr, const char *buf, size_t size)
{
    struct mykobj *mykobject01 = container_of(kobj, struct mykobj, kobj);
    if (strcmp(attr->name, "value1") == 0)
    {
        sscanf(buf, "%d\n", &mykobject01->value1);
    }
    else if (strcmp(attr->name, "value2") == 0)
    {
        sscanf(buf, "%d\n", &mykobject01->value2);
    }
    return size;
}

// 自定义的sysfs_ops结构体,包含show和store函数指针
struct sysfs_ops myops = {
    .show = myshow,
    .store = mystore,
};

// 自定义的kobj_type结构体,包含释放函数、默认属性和sysfs_ops
static struct kobj_type mytype = {
    .release = dynamic_kobj_release,
    .default_attrs = myattr,
    .sysfs_ops = &myops,
};

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 分配并初始化mykobject01
    mykobject01 = kzalloc(sizeof(struct mykobj), GFP_KERNEL);
    mykobject01->value1 = 1;
    mykobject01->value2 = 1;

    // 初始化并添加mykobject01到内核中,名为"mykobject01"
    ret = kobject_init_and_add(&mykobject01->kobj, &mytype, NULL, "%s", "mykobject01");

    return 0;
}

// 模块的退出函数
static void mykobj_exit(void)
{
    // 释放mykobject01
    kobject_put(&mykobject01->kobj);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

95.2 运行测试

95.2.1 编译驱动程序

在上一小节中的attr.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成attr.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

95.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图95-4)所示:

insmod attr.ko

image-20240909102342259

驱动加载之后,我们进入/sys/目录下,可以看到创建生成的myobject01,如下图所示

image-20240909102354649

我们进到myobject01 目录下,可以看到创建的属性文件value1 和value2。

image-20240909102403481

我们可以使用echo 和cat 命令对属性值进行写入和读取,如下图所示:

image-20240909102415399

最后可以使用以下命令进行驱动的卸载,如下图(95-8)所示:

rmmod attr

image-20240909102435535

至此,创建属性文件并实现读写功能实验就完成了。

第96 章优化属性文件读写函数实验

在上个章节中,我们创建具有属性的自定义kobject,并且能够读取和写入值。那么读写属性文件有什么作用呢?我们可以通过属性文件实现用户空间和内核空间信息交换的功能。通过属性文件实现用户空间和内核空间信息交换,用户可以根据自己的需求自定义属性和操作,增强了系统的灵活性和可定制性。本章节我们在上个章节实验的基础上优化属性文件读写函数实验。让我们开始吧!

96.1 实验程序的编写

96.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\71_attr02\module。
我们编写驱动代码,在驱动代码中,创建了一个自定义的kobject,并在sysfs 中创建了两个属性文件,并实现了读取和写入这些属性的功能。属性对象value1 和value2 被定义为kobj_attribute 结构体,并使用__ATTR 宏进行初始化。编写完成的attr.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>

// 自定义的kobject结构体,包含一个kobject对象和两个整型值
struct mykobj
{
    struct kobject kobj;
    int value1;
    int value2;
};

// 定义了mykobj结构体指针变量mykobject01
struct mykobj *mykobject01;

// 自定义的show函数,用于读取属性值
ssize_t show_myvalue1(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count;
    count = sprintf(buf, "show_myvalue1\n");
    return count;
};

// 自定义的store函数,用于写入属性值
ssize_t store_myvalue1(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("buf is %s\n", buf);
    return count;
};

// 自定义的show函数,用于读取属性值
ssize_t show_myvalue2(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count;
    count = sprintf(buf, "show_myvalue2\n");
    return count;
};

// 自定义的store函数,用于写入属性值
ssize_t store_myvalue2(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("buf is %s\n", buf);
    return count;
};

// 定义attribute对象value1和value2
struct kobj_attribute value1 = __ATTR(value1, 0664, show_myvalue1, store_myvalue1);
struct kobj_attribute value2 = __ATTR(value2, 0664, show_myvalue2, store_myvalue2);

// 自定义的kobject释放函数
static void dynamic_kobj_release(struct kobject *kobj)
{
    struct mykobj *mykobject01 = container_of(kobj, struct mykobj, kobj);
    printk("kobject: (%p): %s\n", kobj, __func__);
    kfree(mykobject01);
}

// 将attribute对象放入数组中
struct attribute *myattr[] = {
    &value1.attr,
    &value2.attr,
    NULL,
};

// 自定义的show函数,用于读取属性值
ssize_t myshow(struct kobject *kobj, struct attribute *attr, char *buf)
{
    ssize_t count;
    struct kobj_attribute *kobj_attr = container_of(attr, struct kobj_attribute, attr);
    count = kobj_attr->show(kobj, kobj_attr, buf);
    return count;
}

// 自定义的store函数,用于写入属性值
ssize_t mystore(struct kobject *kobj, struct attribute *attr, const char *buf, size_t size)
{
    struct kobj_attribute *kobj_attr = container_of(attr, struct kobj_attribute, attr);
    return kobj_attr->store(kobj, kobj_attr, buf, size);
}

// 自定义的sysfs_ops结构体,包含show和store函数指针
struct sysfs_ops myops = {
    .show = myshow,
    .store = mystore,
};

// 自定义的kobj_type结构体,包含释放函数、默认属性和sysfs_ops
static struct kobj_type mytype = {
    .release = dynamic_kobj_release,
    .default_attrs = myattr,
    .sysfs_ops = &myops,
};

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 分配并初始化mykobject01
    mykobject01 = kzalloc(sizeof(struct mykobj), GFP_KERNEL);
    mykobject01->value1 = 1;
    mykobject01->value2 = 1;

    // 初始化并添加mykobject01到内核中,名为"mykobject01"
    ret = kobject_init_and_add(&mykobject01->kobj, &mytype, NULL, "%s", "mykobject01");

    return 0;
}

// 模块的退出函数
static void mykobj_exit(void)
{
    // 释放mykobject01
    kobject_put(&mykobject01->kobj);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出/
MODULE_LICENSE("GPL");    // 模块使用的许可证
MODULE_AUTHOR("topeet");  // 模块的作者

96.2 运行测试

96.2.1 编译驱动程序

在上一小节中的attr.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成attr.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

96.2.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图96-4)所示:

insmod attr.ko

image-20240909103239591

驱动加载之后,我们进入/sys/目录下,可以看到创建生成的myobject01,如下图所示

image-20240909103251138

我们进到myobject01 目录下,可以看到创建的属性文件value1 和value2。

image-20240909103326619

我们可以使用echo 和cat 命令对属性值进行写入和读取,如下图所示:

image-20240909103336653

最后可以使用以下命令进行驱动的卸载,如下图(图96-8)所示:

rmmod attr

image-20240909103350942

至此,优化属性文件读写实验就完成了。

第97 章创建属性文件并实现读写功能实验2

在95 章的实验中,我们使用kobject_init_and_add 函数创建kobject,并创建了属性文件,实现了读写功能。在93 章节中,我们学习到创建kobject 有俩种方法。本章节我们使用另一种方法—— kobject_create_and_add 函数创建kobject,并创建属性文件,实现读写功能。让我们开始吧

97.1 实验程序的编写

97.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\72_attr03\module
我们编写驱动代码,使用kobject_create_and_add 函数创建了一个名为”mykobject01” 的自定义kobject,并在其中创建了两个属性文件”value1” 和”value2”。可以通过读取和写入这些属性文件来实现用户空间和内核空间的信息交换。编写完成的attr.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>

// 定义了mykobj结构体指针变量mykobject01
struct kobject *mykobject01;

// 自定义的show函数,用于读取属性值
ssize_t show_myvalue1(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count;
    count = sprintf(buf, "show_myvalue1\n");
    return count;
};

// 自定义的store函数,用于写入属性值
ssize_t store_myvalue1(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("buf is %s\n", buf);
    return count;
};

// 自定义的show函数,用于读取属性值
ssize_t show_myvalue2(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count;
    count = sprintf(buf, "show_myvalue2\n");
    return count;
};

// 自定义的store函数,用于写入属性值
ssize_t store_myvalue2(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("buf is %s\n", buf);
    return count;
};

// 定义attribute对象value1和value2
struct kobj_attribute value1 = __ATTR(value1, 0664, show_myvalue1, store_myvalue1);
struct kobj_attribute value2 = __ATTR(value2, 0664, show_myvalue2, store_myvalue2);

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // // 分配并初始化mykobject01
    // mykobject01 = kzalloc(sizeof(struct mykobj), GFP_KERNEL);
    // mykobject01->value1 = 1;
    // mykobject01->value2 = 1;
    // // 初始化并添加mykobject01到内核中,名为"mykobject01"
    // ret = kobject_init_and_add(&mykobject01->kobj, &mytype, NULL, "%s", "mykobject01");

    mykobject01 = kobject_create_and_add("mykobject01", NULL);
    ret = sysfs_create_file(mykobject01, &value1.attr);
    ret = sysfs_create_file(mykobject01, &value2.attr);
    return ret;
}

// 模块的退出函数
static void mykobj_exit(void)
{
    // 释放mykobject01
    kobject_put(mykobject01);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出/
MODULE_LICENSE("GPL");    // 模块使用的许可证
MODULE_AUTHOR("topeet");  // 模块的作者

接下来驱动编写好之后,我们修改Linux 源码中的kernel/lib/kobject.c 文件,添加如下打印:

image-20240909104020863

然后重新编译内核镜像,单独烧写内核镜像。

97.2 运行测试

97.2.1 编译驱动程序

在上一小节中的make_kset.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成attr.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

97.2.2 运行测试

内核镜像更新之后,开发板启动,使用以下命令进行驱动模块的加载,如下图(图97-5)所示:

insmod attr.ko

image-20240909104139572

驱动加载之后,我们进入/sys/目录下,可以看到创建生成的myobject01,如下图所示

image-20240909104149009

我们进到myobject01 目录下,可以看到创建的属性文件value1 和value2。

image-20240909104204485

我们可以使用echo 和cat 命令对属性值进行写入和读取,如下图所示,可以看到在写入和读取的过程中,会打印我们在内核中添加的打印。

image-20240909104216861

最后可以使用以下命令进行驱动的卸载,如下图(图97-9)所示:

rmmod attr

image-20240909104347333

至此,创建属性文件并实现读写功能实验2 就完成了。

第98 章创建多个属性文件的简便方法

在上一章节中,我们创建了一个属性文件,但是在情况中,我们可能需要创建N 个属性文件,如果还按照上个章节的方法是非常麻烦的。本章节我们来学习创建多个属性文件的简便方法,这样可以更好地组织和管理相关属性文件,提供更清晰和一致的sysfs 接口。

98.1 sysfs_create_group 函数

sysfs_create_group 函数,用于在sysfs 中创建一个组(group)。组是一组相关的属性文件的集合,可以将它们放在同一个目录下提供更好的组织性和可读性。
函数原型如下所示:

int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);

此函数有俩个参数,分别为如下所示:

  • kobj: 指向包含目标组的kobject 的指针。
  • grp: 指向attribute_group 结构体的指针,该结构体定义了组中的属性文件。

attribute_group 结构体定义如下:

struct attribute_group {
    const char *name;
    const struct attribute **attrs;
    mode_t (*is_visible)(struct kobject *kobj, struct attribute *attr, int index);
};

结构体中包含如下字段:

  • name: 组的名称,将用作sysfs 目录的名称。
  • attrs: 指向属性文件数组的指针
  • is_visible : 可选的回调函数,用于决定每个属性文件是否可见。如果为NULL,则所有属性文件都可见。

属性文件数组是一个以struct attribute *类型为元素的数组,以NULL 结束。每个struct attribute 结构体表示一个属性文件,可以使用&运算符将属性对象(如struct kobject_attribute)的.attr 字段传递给属性文件数组。
下面展示使用sysfs_create_group 创建一个组并添加属性文件

struct attribute *attrs[] = {
    &attr1.attr,
    &attr2.attr,
    NULL,
};

const struct attribute_group attr_group = {
    .name = "my_group",
    .attrs = attrs,
};
sysfs_create_group(kobj, &attr_group);

在上述示例中,我们创建了一个名叫“my_group”的组,并将属性文件attr1 和attr2 添加到该组中,然后使用sys_create_group 将该组添加到指定的kobject kobj 中。接下来我们开始做实验。

98.2 实验程序的编写

98.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\73_attr04\module。
我们编写驱动代码,创建了一个名为”mykobject01” 的自定义kobject,并在其中使用上小节介绍的方式创建了两个属性文件”value1” 和”value2”。编写完成的attr.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>

// 定义了mykobj结构体指针变量mykobject01
struct kobject *mykobject01;

// 自定义的show函数,用于读取属性值
ssize_t show_myvalue1(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count;
    count = sprintf(buf, "show_myvalue1\n");
    return count;
};

// 自定义的store函数,用于写入属性值
ssize_t store_myvalue1(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("buf is %s\n", buf);
    return count;
};

// 自定义的show函数,用于读取属性值
ssize_t show_myvalue2(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    ssize_t count;
    count = sprintf(buf, "show_myvalue2\n");
    return count;
};

// 自定义的store函数,用于写入属性值
ssize_t store_myvalue2(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("buf is %s\n", buf);
    return count;
};

// 定义attribute对象value1和value2
struct kobj_attribute value1 = __ATTR(value1, 0664, show_myvalue1, store_myvalue1);
struct kobj_attribute value2 = __ATTR(value2, 0664, show_myvalue2, store_myvalue2);

struct attribute *attr[] = {
    &value1.attr,
    &value2.attr,
    NULL,
};

const struct attribute_group my_attr_group = {
    .name = "myattr",
    .attrs = attr,
};

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 创建并添加kobject "mykobject01"
    mykobject01 = kobject_create_and_add("mykobject01", NULL);

    // 在kobject "mykobject01" 中创建属性组
    ret = sysfs_create_group(mykobject01, &my_attr_group);

    return ret;
}

// 模块的退出函数
static void mykobj_exit(void)
{
    // 释放kobject "mykobject01"
    kobject_put(mykobject01);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL");    // 模块使用的许可证
MODULE_AUTHOR("topeet");  // 模块的作者

接下来驱动编写好之后,我们修改Linux 源码中的kernel/lib/kobject.c 文件,添加如下打印:

image-20240909104908855

然后重新编译内核镜像,单独烧写内核镜像。

98.3 运行测试

98.3.1 编译驱动程序

在上一小节中的attr.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成attr.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

98.3.2 运行测试

内核镜像更新之后,开发板启动,使用以下命令进行驱动模块的加载,如下图(图98-5)所示:

insmod attr.ko

image-20240909104959549

驱动加载之后,我们进入/sys/目录下,可以看到创建生成的myobject01,如下图所示

image-20240909105010858

最后可以使用以下命令进行驱动的卸载,如下图(图98-6)所示:

rmmod attr

image-20240909105025262

至此,创建多个属性文件实验就完成了。

第99 章注册一个自己的总线实验

在设备模型中,包含总线、设备、驱动和类四个概念。在前面的章节中,我们学习了设备模型的基本框架kobject 和kset。而在本章节中,我们将学习设备模型中总线的概念。并进行实验——注册一个自己的总线。

99.1 总线注册API 函数

我们进入开发板的/sys/bus 目录下,/sys/bus 是Linux 系统中的一个目录,用于表示总线(bus)子系统的根目录。如果我们自己注册一个总线,会在此目录下显示。

image-20240909105100362

bus_register 是一个函数,用于将一个自定义总线注册到Linux 内核中。

函数原型如下:

int bus_register(struct bus_type *bus);

参数bus 是一个指向struct bus_type 结构体的指针,表示要注册的自定义总线。关于bus_type 结构体的介绍请参阅88.2.1 小节
函数返回一个整数值,表示注册操作的结果。通常情况下,返回值为0 表示注册成功,负值表示注册失败。

bus_unregister 是一个函数,用于取消注册一个已经注册的自定义总线。

函数原型如下:

void bus_unregister(struct bus_type *bus);

参数bus 是一个指向struct bus_type 结构体的指针,表示要取消注册的自定义总线。该函数没有返回值。
接下来,我们使用上述的API 函数编写驱动代码进行实验。

99.2 实验程序的编写

99.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\74_bus01\module
我们编写驱动代码,驱动中定义了一个名为”mybus” 的自定义总线,并指定了该总线的匹配回调函数mybus_match 和探测回调函数mybus_probe。编写完成的bus.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/device.h>

int mybus_match(struct device *dev, struct device_driver *drv)
{
    return (strcmp(dev_name(dev), drv->name) == 0);
};

int mybus_probe(struct device *dev)
{
    struct device_driver *drv = dev->driver;
    if (drv->probe)
        drv->probe(dev);
    return 0;
};

struct bus_type mybus = {
    .name = "mybus",
    .match = mybus_match,
    .probe = mybus_probe,
};

// 模块的初始化函数
static int bus_init(void)
{
    int ret;
    ret = bus_register(&mybus); 
    return 0;
}

// 模块退出函数
static void bus_exit(void)
{
    bus_unregister(&mybus);
}

module_init(bus_init); // 指定模块的初始化函数
module_exit(bus_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

99.3 运行测试

99.3.1 编译驱动程序

在上一小节中的bus.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成bus.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

99.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图99-3)所示:

insmod bus.ko

image-20240909111713802

驱动加载之后,我们进入/sys/bus 目录下,可以看到创建生成的总线mybus,如下图所示,我们进到mybus 目录下,可以看到创建属性文件。

image-20240909111726978

最后可以使用以下命令进行驱动的卸载,如下图(图99-4)所示:

rmmod bus

image-20240909111835787

至此,注册一个自己的总线实验就完成了。

第100 章在总线目录下创建属性文件实验

在上个章节中,我们成功注册了一个自定义总线,并为其创建了必要的数据结构和函数。现在,在本章节中,我们将继续深入了解总线的概念,并在总线目录下创建属性文件以扩展其功能。通过创建属性文件,我们可以为总线添加额外的信息和控制选项,以便与设备和驱动进行交互。这些属性文件可以用于读取总线的状态、设置参数或执行其他相关操作。让我们开始吧!

100.1 总线下创建属性API 函数

bus_create_file() 函数用于在总线的sysfs 目录下创建一个属性文件。

int bus_create_file(struct bus_type *bus, struct kobject *kobj, const struct attribute *attr);
参数说明:
    bus:指向总线类型结构体struct bus_type 的指针,表示要创建属性文件的总线。
    kobj:指向内核对象struct kobject 的指针,表示要在其下创建属性文件的内核对象。
    attr:指向属性struct attribute 的指针,表示要创建的属性文件的属性。
返回值:
    成功时返回0,否则返回负数错误代码。

在调用bus_create_file()函数之前,需要先定义好属性结构体struct attribute,并将其相关字段填充好。通常,属性结构体会包含以下字段:
.name:属性的名称。
.mode:属性的访问权限。
示例用法:

struct bus_attribute mybus_attr = {
    .attr = {
        .name = "value",
        .mode = 0664,
    },
    .show = mybus_show,
};
ret = bus_create_file(&mybus, &mydevice.kobj, &mybus_attr.attr);

上述示例代码创建了一个名为”value” 的属性文件,并指定了访问权限为0664。在创建属性文件时,还可以指定其他属性的回调函数,如.show、.store 等,以实现对属性值的读取和写入操作。
请注意,在使用bus_create_file() 函数之前,需要确保总线对象和内核对象已正确初始化和注册。
接下来我们开始编写驱动文件,进行实验。

100.2 实验程序的编写

100.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\75_bus02\module。

我们编写驱动代码,定义了一个名为”mybus” 的总线,并实现了总线的匹配回调函数mybus_match 和设备探测回调函数mybus_probe。同时,还定义了一个名为”value” 的属性文件,并实现了属性的显示回调函数mybus_show。编写完成的bus.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/device.h>
#include <linux/sysfs.h>

int mybus_match(struct device *dev, struct device_driver *drv)
{
    // 检查设备名称和驱动程序名称是否匹配
    return (strcmp(dev_name(dev), drv->name) == 0);
};

int mybus_probe(struct device *dev)
{
    struct device_driver *drv = dev->driver;
    if (drv->probe)
        drv->probe(dev);
    return 0;
};

struct bus_type mybus = {
    .name = "mybus",                 // 总线的名称
    .match = mybus_match,            // 设备和驱动程序匹配的回调函数
    .probe = mybus_probe,            // 设备探测的回调函数
};
EXPORT_SYMBOL_GPL(mybus);             // 导出总线符号

ssize_t value_show(struct bus_type *bus, char *buf)
{
    // 在 sysfs 中显示总线的值
    return sprintf(buf, "%s\n", "mybus_show");
};


// struct bus_attribute mybus_attr = {
//     .attr = {
//         .name = "value",             // 属性的名称
//         .mode = 0664,                // 属性的访问权限
//     },
//     .show = mybus_show,               // 属性的 show 回调函数
// };

// static BUS_ATTR(value,0664,mybus_show,NULL);   //用这个宏定义可以替换上面的结构体定义,就更简单一些.但是这个会报错
static BUS_ATTR_RO(value);      //value_show,和value_store  名称是固定的,要用括号里面的名字
// struct bus_attribute bus_attr_value = __ATTR(value,0664,mybus_show,NULL);

// 模块的初始化函数
static int bus_init(void)
{
    int ret;
    ret = bus_register(&mybus);       // 注册总线
    // ret = bus_create_file(&mybus, &mybus_attr);  // 在 sysfs 中创建属性文件
    ret = bus_create_file(&mybus, &bus_attr_value);  // 在 sysfs 中创建属性文件 //用这个宏定义名字得改一下

    return 0;
}

// 模块退出函数
static void bus_exit(void)
{
    bus_remove_file(&mybus, &bus_attr_value);  // 从 sysfs 中移除属性文件 //用这个宏定义名字得改一下
    bus_unregister(&mybus);                // 取消注册总线
}

module_init(bus_init);                    // 指定模块的初始化函数
module_exit(bus_exit);                    // 指定模块的退出函数

MODULE_LICENSE("GPL");                    // 模块使用的许可证
MODULE_AUTHOR("topeet");                  // 模块的作者

100.3 运行测试

100.3.1 编译驱动程序

在上一小节中的bus.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成bus.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

100.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图100-5)所示:

insmod bus.ko

image-20240909112351031

驱动加载之后,我们进入/sys/bus 目录下,可以看到创建生成的总线mybus,如下图所示,我们进到mybus 目录下,可以看到创建属性文件value。

image-20240909112402034

最后可以使用以下命令进行驱动的卸载,如下图(图100-7)所示:

rmmod bus

image-20240909112512006

至此,在总线目录下创建属性文件实验就完成了。

第101 章总线注册流程理论分析实验

在上个章节中,我们成功地在总线目录下创建了属性文件,为总线添加了额外的信息和控制选项。现在,在本章节中,我们将深入分析总线注册的流程,从代码的层面来理解总线的实现过程。通过这样的分析,我们将更好地理解总线注册的内部机制,并能够应用这些知识来实现更复杂的总线功能。让我们深入代码,一起学习总线注册的流程吧!

101.1 bus_register 函数解析

开发板上电,我们进入到开发板的/sys/bus/mybus 目录下。

image-20240909112648295

如上图所示,为什么在sys/bus 目录下会生成mybus 目录以及对应的devices,drivers,drivers_autoprobe,drivers_probe,uevent 目录和属性呢?
在开发板/sys 目录下的目录都对应一个kobject,所以我们猜测mybus 目录和devices,drivers目录和kobject 有关系的。而kobject 一般要嵌入到其他结构体中去使用。如下图所示,kobject嵌入到device 结构体中。

image-20240909113147919

在struct device 结构体中包含了kobject 结构体,而struct bus_type 结构体又包含了struct device 结构体。如下图所示:

image-20240909113206383

所以我们猜测这些/sys/bus/下的目录是和struct device 结构体中的kobj 有关系。接下来我
们追踪bus_register 函数,函数原型如下所示:

/*
bus_register - 注册一个驱动核心子系统
@bus: 要注册的总线
注册总线时,首先将总线与kobject 基础设施关联起来,然后注册属于该总线的子系统:
归属于该子系统的设备和驱动程序。
*/
int bus_register(struct bus_type *bus)
{
    int retval;
    struct subsys_private *priv;
    struct lock_class_key *key = &bus->lock_key;
    
    // 分配并初始化一个subsys_private 结构体,用于保存子系统的相关信息
    priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    priv->bus = bus;
    bus->p = priv;
    
    // 初始化一个阻塞通知链表bus_notifier
    BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);
    
    // 设置子系统的名称
    retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
    if (retval)
        goto out;
    
    // 设置子系统的kset 和ktype
    priv->subsys.kobj.kset = bus_kset;
    priv->subsys.kobj.ktype = &bus_ktype;
    priv->drivers_autoprobe = 1;
    
    // 注册子系统的kset
    retval = kset_register(&priv->subsys);
    if (retval)
        goto out;
    
    // 在总线上创建一个属性文件
    retval = bus_create_file(bus, &bus_attr_uevent);
    if (retval)
        goto bus_uevent_fail;
    
    // 创建并添加"devices" 子目录的kset
    priv->devices_kset = kset_create_and_add("devices", NULL, &priv->subsys.kobj);
    if (!priv->devices_kset) {
        retval = -ENOMEM;
        goto bus_devices_fail;
    }
    
    // 创建并添加"drivers" 子目录的kset
    priv->drivers_kset = kset_create_and_add("drivers", NULL, &priv->subsys.kobj);
    if (!priv->drivers_kset) {
        retval = -ENOMEM;
        goto bus_drivers_fail;
    }
    
    // 初始化接口链表、互斥锁和设备/驱动的klist
    INIT_LIST_HEAD(&priv->interfaces);
    __mutex_init(&priv->mutex, "subsys mutex", key);
    klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
    klist_init(&priv->klist_drivers, NULL, NULL);
    
    // 添加驱动探测文件
    retval = add_probe_files(bus);
    if (retval)
        goto bus_probe_files_fail;
    
    // 添加总线的属性组
    retval = bus_add_groups(bus, bus->bus_groups);
    if (retval)
        goto bus_groups_fail;
    
    // 打印调试信息,表示总线注册成功
    pr_debug("bus: '%s': registered\n", bus->name);
    return 0;
    
    bus_groups_fail:
        remove_probe_files(bus);
    bus_probe_files_fail:
        kset_unregister(bus->p->drivers_kset);
    bus_drivers_fail:
        kset_unregister(bus->p->devices_kset);
    bus_devices_fail:
        bus_remove_file(bus, &bus_attr_uevent);
    bus_uevent_fail:
        kset_unregister(&bus->p->subsys);
    out:
        kfree(bus->p);
        bus->p = NULL;
    return retval;
}
EXPORT_SYMBOL_GPL(bus_register);
*/

以下是该函数的主要步骤:

  • 1 分配并初始化一个subsys_private 结构体,该结构体用于保存子系统的相关信息。
  • 2 将bus 结构体与subsys_private 关联起来。
  • 3 初始化一个阻塞通知链表bus_notifier。
  • 4 通过kobject_set_name() 设置子系统的名称。
  • 5 设置子系统的相关属性,如kset 和ktype。
  • 6 注册子系统的kset。
  • 7 创建并添加”devices” 和”drivers” 两个子目录的kset。
  • 8 初始化子系统的接口链表、互斥锁和设备/驱动的klist。
  • 9 添加驱动探测文件。
  • 10 添加总线的属性组。
  • 11 打印调试信息,表示总线注册成功。

接下来我们对上述步骤进行补充讲解。
在步骤1 中,出现了新的结构体:struct subsys_private,如下所示:

struct subsys_private {
    struct kset subsys;
    struct kset *devices_kset;
    struct list_head interfaces;
    struct mutex mutex;
    struct kset *drivers_kset;
    struct klist klist_devices;
    struct klist klist_drivers;
    struct blocking_notifier_head bus_notifier;
    unsigned int drivers_autoprobe:1;
    struct bus_type *bus;
    struct kset glue_dirs;
    struct class *class;
};

struct subsys_private 是一个结构体,用于保存驱动核心子系统(bus)的私有信息。每个子系统都可以有私有数据,这些私有数据存储在struct subsys_private 结构体中。那么什么是子系统呢?

在Linux 中,子系统是一种机制,用于将特定功能的实现抽象为一个独立的实体。它提供了一种方便的方式,将相关的代码和数据结构组织在一起,以实现特定的功能。子系统可以被视为一个功能模块,它封装了相关的功能和操作,使得用户和应用程序可以通过统一的接口与其交互。

在Linux 中,存在许多常见的子系统,每个子系统都负责实现特定的功能。以下是一些常见的子系统示例。

  • 虚拟文件系统(VFS)子系统:VFS 子系统提供了对不同文件系统的统一访问接口,使得应用程序可以透明地访问各种文件系统(如ext4、NTFS、FAT 等),而无需关心底层文件系统的具体实现。
  • 设备驱动子系统:设备驱动子系统管理和控制硬件设备的驱动程序。它提供了与硬件设备交互的接口,使得应用程序可以通过驱动程序与设备进行通信和控制。
  • 网络子系统:网络子系统负责管理和控制网络相关的功能。它包括网络协议栈、套接字接口、网络设备驱动程序等,用于实现网络通信和网络协议的处理。
  • 内存管理子系统:内存管理子系统负责管理系统的物理内存和虚拟内存。它包括内存分配、页面置换、内存映射等功能,用于有效地分配和管理系统的内存资源。
  • 进程管理子系统:进程管理子系统负责管理和控制系统中的进程。它包括进程的创建、调度、终止等功能,以及进程间通信的机制,如信号、管道、共享内存等。
  • 电源管理子系统:电源管理子系统负责管理和控制系统的电源管理功能。它可以用于控制电源的开关、电源模式的切换、节能功能的实现等。
  • 文件系统子系统:文件系统子系统负责管理和控制文件系统的创建、格式化、挂载、数据存取等操作。它支持各种文件系统类型,如ext4、FAT、NTFS 等。
  • 图形子系统:图形子系统负责管理和控制图形显示功能,包括显示驱动程序、窗口管理、图形渲染等。它提供了图形界面的支持,使得用户可以通过图形方式与计算机交互。

在步骤2 中,priv->bus = bus;将priv 结构体中的bus 成员设置为当前注册的总线。这样做的目的是将bus 成员与当前总线建立关联。通过将bus 成员设置为当前总线,priv 结构体可以获取并访问与该总线相关的信息和功能。这种关联可以使priv 结构体在操作当前总线时更加方便和高效。

在步骤2 中,bus->p = priv;将priv 结构体指针存储在当前注册的总线结构体的成员p 中,目的是让当前注册的总线结构体能够快速地找到并访问与之关联的priv 结构体。通过将priv结构体指针存储在总线结构体的成员中,总线可以轻松地获取与之相关的私有数据结构。这种关联使得总线能够直接访问和操作与特定总线相关的数据和功能,而无需通过其他方式来查找或传递指针。

在步骤8 中,klist_init 函数用于初始化两个内核链表(klist),分别是priv->klist_devices 和priv->klist_drivers。

klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);这行代码初始化了名为priv->klist_devices 的内核链表。klist_devices_get 和klist_devices_put 是两个回调函数,用于在向链表添加或移除元素时执行相应的操作。通常,这些回调函数用于在链表中的每个元素被引用或释放时执行额外的操作。例如,当设备被添加到链表时,klist_devices_get函数可能会增加设备的引用计数;当设备从链表中移除时,klist_devices_put 函数可能会减少设备的引用计数。

klist_init(&priv->klist_drivers, NULL, NULL);这行代码初始化了名为priv->klist_drivers 的内核链表,但与第一个初始化不同,这里没有提供回调函数。因此,这个链表在添加或移除元素时不会执行额外的操作。这种情况下,链表主要用于存储驱动程序对象,而不需要附加的处理逻辑

通过分析bus_register 函数,我们对设备模型有了更深层次的感悟,如下所示:

  1. kobject 和kset 是设备模型的基本框架,它们可以嵌入到其他结构体中以提供设备模型的功能。kobject 代表设备模型中的一个对象,而kset 则是一组相关的kobject 的集合。
  2. 属性文件在设备模型中具有重要作用,它们用于在内核空间和用户空间之间进行数据交换。属性文件可以通过sysfs 虚拟文件系统在用户空间中表示为文件,用户可以读取或写入这些文件来与设备模型进行交互。属性文件允许用户访问设备的状态、配置和控制信息,从而实现了设备模型的管理和配置。
  3. sysfs 虚拟文件系统在设备模型中扮演关键角色,它可以将设备模型的组织层次展现出来。通过sysfs,设备模型中的对象、属性和关系可以以目录和文件的形式在用户空间中表示。这种组织形式使用户能够以层次结构的方式浏览和管理设备模型,从而方便地获取设备的信息、配置和状态。sysfs 提供了一种统一的接口,使用户能够通过文件系统操作来与设备模型进行交互,提供了设备模型的可视化和可操作性。

第102 章platform 总线注册流程实例分析实验

在上个章节中,我们详细地从代码的层面分析了总线注册的流程,并深入了解了总线的实现过程。现在,在本章节中,我们将继续承接前文的内容,进一步分析platform 总线的注册流程。本章节将深入研究platform 总线注册的关键步骤。揭示platform 总线注册的内部机制。让我们开始吧!

内核在初始化的过程中调用platform_bus_init()函数来初始化平台总线,调用流程如下所示:

image-20240909114439368

我们直接分析platform_bus_init 函数。在内核driver/base/platform.c 文件中注册了platform文件,platform_bus_init 函数如下所示:

int __init platform_bus_init(void)
{
    int error;
    early_platform_cleanup(); // 提前清理平台总线相关资源
    error = device_register(&platform_bus); // 注册平台总线设备
    if (error) {
        put_device(&platform_bus); // 注册失败,释放平台总线设备
        return error; // 返回错误代码
    }
    error = bus_register(&platform_bus_type); // 注册平台总线类型
    if (error) {
        device_unregister(&platform_bus); // 注册失败,注销平台总线设备
        return error; // 返回错误代码
    }
    of_platform_register_reconfig_notifier(); // 注册平台重新配置的通知器
    return error; // 返回错误代码(如果有)
}

函数首先清空总线early_platform_device_list 上的所有节点。然后使用调用device_register(platform_bus_type) 注册平台总线设备,将platform_bus 结构体注册到设备子系统中。然后使用bus_register(&platform_bus_type)函数注册平台总线类型,将platform_bus_type结构体注册到总线子系统中。

在上面的函数中使用bus_register(&platform_bus_type)注册了平台总线。platform_bus_type结构体如下所示:

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

上述代码定义了一个名为platform_bus_type 的struct bus_type 结构体变量,表示平台总线类型。

该结构体变量的成员包括:

  • .name = “platform”:指定平台总线类型的名称为”platform”。
  • .dev_groups = platform_dev_groups:指定设备组的指针,用于定义与平台总线相关的设备属性组。
  • .match = platform_match:指定匹配函数的指针,用于确定设备是否与平台总线兼容。
  • .uevent = platform_uevent:指定事件处理函数的指针,用于处理与平台总线相关的事件。
  • .dma_configure = platform_dma_configure:指定DMA 配置函数的指针,用于配置平台总线上的DMA。
  • .pm = &platform_dev_pm_ops:指定与电源管理相关的操作函数的指针,用于管理平台总线上的设备电源。

我们重点来看看platform_match 函数,如下所示:

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

platform_match 是一个用于判断设备和驱动程序是否匹配的函数。它接受两个参数:dev表示设备对象指针,drv 表示驱动程序对象指针。上述函数的主要逻辑如下:

  • 1 首先,将dev 和drv 分别转换为struct platform_device 和struct platform_driver 类型的指针,以便后续使用。
  • 2 检查pdev->driver_override 是否设置。如果设置了,表示只要与指定的驱动程序名称匹配,即可认为设备和驱动程序匹配。函数会比较pdev->driver_override 和drv->name 的字符串是否相等,如果相等则返回匹配(非零)。
  • 3 如果pdev->driver_override 未设置,首先尝试进行OF 风格的匹配(Open Firmware)。调用of_driver_match_device(dev, drv)函数,该函数会检查设备是否与驱动程序匹配。如果匹配成功,则返回匹配(非零)。
  • 4 如果OF 风格的匹配失败,接下来尝试进行ACPI 风格的匹配(Advanced Configuration and Power Interface)。调用acpi_driver_match_device(dev, drv)函数,该函数会检查设备是否与驱动程序匹配。如果匹配成功,则返回匹配(非零)。
  • 5 如果ACPI 风格的匹配也失败,最后尝试根据驱动程序的ID 表进行匹配。检查pdrv->id_table 是否存在。如果存在,则调用platform_match_id(pdrv->id_table, pdev)函数来检查设备是否与ID 表中的任何条目匹配。如果匹配成功,则返回匹配(非零)。
  • 6 如果以上所有匹配尝试都失败,最后使用驱动程序名称与设备名称进行比较。比较pdev->namedrv->name 的字符串是否相等,如果相等则返回匹配(非零)。

通过上述分析, 我们终于明白了为什么在platform 总线匹配优先级的时候,of_match_table>id_table>name
至此,platform 总线注册流程分析完毕。

第103 章在总线下注册设备实验

在99 章节中,我们成功地注册了自定义的总线,并确保该总线已被系统识别和管理。现在,在本章节中,将继续构建在该总线上注册设备的流程。让我们继续构建在自定义总线上注册设备的过程,并为我们的设备模型增添更多的功能和扩展性。让我们开始吧!

103.1 实验程序的编写

103.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\76_device\module。
我们编写驱动代码,定义了一个名为”mybus” 的总线,并实现了总线的匹配回调函数mybus_match 和设备探测回调函数mybus_probe。同时,还定义了一个名为”value” 的属性文件,并实现了属性的显示回调函数mybus_show。编写完成的bus.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/device.h>
#include <linux/sysfs.h>

int mybus_match(struct device *dev, struct device_driver *drv)
{
    // 检查设备名称和驱动程序名称是否匹配
    return (strcmp(dev_name(dev), drv->name) == 0);
};

int mybus_probe(struct device *dev)
{
    struct device_driver *drv = dev->driver;
    if (drv->probe)
        drv->probe(dev);
    return 0;
};

struct bus_type mybus = {
    .name = "mybus",                 // 总线的名称
    .match = mybus_match,            // 设备和驱动程序匹配的回调函数
    .probe = mybus_probe,            // 设备探测的回调函数
};
EXPORT_SYMBOL_GPL(mybus);             // 导出总线符号

ssize_t value_show(struct bus_type *bus, char *buf)
{
    // 在 sysfs 中显示总线的值
    return sprintf(buf, "%s\n", "mybus_show");
};


// struct bus_attribute mybus_attr = {
//     .attr = {
//         .name = "value",             // 属性的名称
//         .mode = 0664,                // 属性的访问权限
//     },
//     .show = mybus_show,               // 属性的 show 回调函数
// };

// static BUS_ATTR(value,0664,mybus_show,NULL);   //用这个宏定义可以替换上面的结构体定义,就更简单一些.但是这个会报错
static BUS_ATTR_RO(value);      //value_show,和value_store  名称是固定的,要用括号里面的名字
// struct bus_attribute bus_attr_value = __ATTR(value,0664,mybus_show,NULL);

// 模块的初始化函数
static int bus_init(void)
{
    int ret;
    ret = bus_register(&mybus);       // 注册总线
    // ret = bus_create_file(&mybus, &mybus_attr);  // 在 sysfs 中创建属性文件
    ret = bus_create_file(&mybus, &bus_attr_value);  // 在 sysfs 中创建属性文件 //用这个宏定义名字得改一下

    return 0;
}

// 模块退出函数
static void bus_exit(void)
{
    bus_remove_file(&mybus, &bus_attr_value);  // 从 sysfs 中移除属性文件 //用这个宏定义名字得改一下
    bus_unregister(&mybus);                // 取消注册总线
}

module_init(bus_init);                    // 指定模块的初始化函数
module_exit(bus_exit);                    // 指定模块的退出函数

MODULE_LICENSE("GPL");                    // 模块使用的许可证
MODULE_AUTHOR("topeet");                  // 模块的作者

我们编写驱动文件device.c,在驱动中,Linux 内核中创建一个自定义设备并将其注册到自定义总线上。
编写好的驱动如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/device.h>
#include <linux/sysfs.h>

extern struct bus_type mybus;

void myrelease(struct device *dev)
{
    printk("This is myrelease\n");
};

struct device mydevice = {
    .init_name = "mydevice",      // 设备的初始化名称
    .bus = &mybus,                // 所属总线
    .release = myrelease,         // 设备的释放回调函数
    .devt = ((255 << 20 | 0)),    // 设备号
};

// 模块的初始化函数
static int device_init(void)
{
    int ret;
    ret = device_register(&mydevice);  // 注册设备

    return 0;
}

// 模块退出函数
static void device_exit(void)
{
    device_unregister(&mydevice);      // 取消注册设备
}

module_init(device_init);              // 指定模块的初始化函数
module_exit(device_exit);              // 指定模块的退出函数

MODULE_LICENSE("GPL");                 // 模块使用的许可证
MODULE_AUTHOR("topeet");               // 模块的作者

103.3 运行测试

103.3.1 编译驱动程序

在上一小节中的device.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成bus.ko 目标文件,至此device.ko 驱动模块就编译成功了,bus.ko 也需要重新编译,接下来进行测试。

103.3.2 运行测试

开发板启动之后,使用以下命令进行bus.ko 驱动模块的加载,如下图(图103-4)所示:

insmod bus.ko

image-20240909115636783

然后加载device.ko 驱动模块,如下图所示:

insmod device.ko

image-20240909115650818

驱动加载之后,我们进入/sys/devices 目录下,如下图所示,有注册生成的设备。

image-20240909115708524

我们进入到/sys/bus/mybus/devices 目录下,如下图所示,在总线下注册的设备为mydevice。

image-20240909115722236

最后可以使用以下命令进行驱动的卸载,如下所示:

rmmod device
rmmod bus

至此,在总线下注册设备实验就完成了。

第104 章设备注册流程分析实验

在上个章节中,我们成功在自定义总线上注册了设备,使得系统能够正确地管理和操作该设备。本章节我们将深入分析设备注册的流程,从代码的层面来理解设备的注册过程。

104.1 device_register 函数分析

device_register 函数原型定义在drivers/base/core.c 文件中,如下所示:

int device_register(struct device *dev)
{
    device_initialize(dev);
    return device_add(dev);
}
EXPORT_SYMBOL_GPL(device_register);

上述代码展示了一个名为device_register 的函数实现,用于注册设备到内核中。函数接受一个指向struct device 类型的设备对象指针作为参数。首先,代码调用device_initialize 函数对设备对象进行初始化。接下来,代码调用device_add 函数将设备添加到内核中。device_add函数会将设备添加到设备总线的设备列表中,并执行与设备添加相关的操作,例如分配设备号、创建设备节点等。最后,函数返回设备添加的结果,通常是一个整数值表示成功或失败的状态码。

104.1.1 device_initialize 函数

device_initialize 函数原型,如下所示:

void device_initialize(struct device *dev)
{
    dev->kobj.kset = devices_kset;
    kobject_init(&dev->kobj, &device_ktype);
    INIT_LIST_HEAD(&dev->dma_pools);
    mutex_init(&dev->mutex);
    lockdep_set_novalidate_class(&dev->mutex);
    spin_lock_init(&dev->devres_lock);
    INIT_LIST_HEAD(&dev->devres_head);
    device_pm_init(dev);
    set_dev_node(dev, -1);
    
    #ifdef CONFIG_GENERIC_MSI_IRQ
        INIT_LIST_HEAD(&dev->msi_list);
    #endif
    
    INIT_LIST_HEAD(&dev->links.consumers);
    INIT_LIST_HEAD(&dev->links.suppliers);
    INIT_LIST_HEAD(&dev->links.needs_suppliers);
    INIT_LIST_HEAD(&dev->links.defer_hook);
    dev->links.status = DL_DEV_NO_DRIVER;
}
EXPORT_SYMBOL_GPL(device_initialize);

上述代码展示了一个名为device_initialize 的函数实现,用于对设备对象进行初始化。函数接收一个指向struct device 类型的设备对象指针作为参数。

首先,代码将设备对象的kobj.kset 成员设置为devices_kset,表示该设备对象所属的kset为devices_kset,即设备对象属于devices 子系统。

接下来,代码调用kobject_init 函数初始化设备对象的kobj 成员,使用device_ktype 作为ktype。通过这个函数调用,设备对象的kobject 被正确地初始化和设置。

然后,代码使用INIT_LIST_HEAD 宏初始化设备对象的dma_pools、msi_list、consumers、suppliers、needs_suppliers 和defer_hook 等链表头,以确保它们为空链表。

代码接着调用mutex_init 函数初始化设备对象的mutex 互斥锁,用于对设备进行互斥操作。通过lockdep_set_novalidate_class 函数,设置dev->mutex 的验证类别为无效,以避免死锁分析器对该互斥锁的验证。

接下来,代码调用spin_lock_init 函数初始化设备对象的devres_lock 自旋锁,用于对设备资源进行保护。通过INIT_LIST_HEAD 宏初始化设备对象的devres_head 链表头,以确保它为空链表。

代码接着调用device_pm_init 函数初始化设备对象的电源管理相关信息。然后,代码使用set_dev_node函数将设备对象的设备节点设置为-1,表示没有指定设备节点。在#ifdef CONFIG_GENERIC_MSI_IRQ 条件编译块内,代码使用INIT_LIST_HEAD 宏初始化设备对象的msi_list 链表头,用于管理设备的MSI(消息信号中断)信息。

最后,代码使用INIT_LIST_HEAD 宏初始化设备对象的consumers、suppliers、needs_suppliers和defer_hook 等链表头,用于管理设备间的连接关系。代码将设备对象的status 成员设置为DL_DEV_NO_DRIVER,表示设备当前没有驱动程序。

104.1.2 device_add 函数

接下来我们分析device_add 函数,函数实现如下所示:

int device_add(struct device *dev)
{
    struct device *parent;
    struct kobject *kobj;
    struct class_interface *class_intf;
    int error = -EINVAL;
    struct kobject *glue_dir = NULL;
    
    // 获取设备的引用
    dev = get_device(dev);
    if (!dev)
        goto done;
    if (!dev->p) {
    // 如果设备的私有数据(private data)未初始化,则进行初始化
        error = device_private_init(dev);
        if (error)
            goto done;
    }
    
    /*
    * 对于静态分配的设备(应该都会被转换),需要初始化设备名称。
    * 我们禁止读回名称,并强制使用dev_name()函数。
    */
    if (dev->init_name) {
        // 初始化设备的名称
        dev_set_name(dev, "%s", dev->init_name);
        dev->init_name = NULL;
    }
    
    /* 子系统可以指定简单的设备枚举*/
    if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
        // 如果设备的名称为空,并且设备所属总线的名称不为空,则设置设备名称
        dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
    
    if (!dev_name(dev)) {
        error = -EINVAL;
        goto name_error;
    }
    
    pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
    
    // 获取设备的父设备引用
    parent = get_device(dev->parent);
    
    // 获取设备的父kobject
    kobj = get_device_parent(dev, parent);
    if (IS_ERR(kobj)) {
        error = PTR_ERR(kobj);
        goto parent_error;
    }
    
    if (kobj)
        dev->kobj.parent = kobj;
    
    // 使用父设备的NUMA 节点
    if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
        set_dev_node(dev, dev_to_node(parent));
    
    // 首先,向通用层注册设备
    // 需要在此之前设置设备的名称,并将parent 设置为NULL
    error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
    if (error) {
        glue_dir = get_glue_dir(dev);
        goto Error;
    }
    
    // 通知平台设备的添加
    if (platform_notify)
        platform_notify(dev);
    
    // 创建设备的uevent 属性文件
    error = device_create_file(dev, &dev_attr_uevent);
    if (error)
        goto attrError;
    
    // 添加设备类的符号链接
    error = device_add_class_symlinks(dev);
    if (error)
        goto SymlinkError;
    
    // 添加设备的属性
    error = device_add_attrs(dev);
    if (error)
        goto AttrsError;
    
    // 将设备添加到总线
    error = bus_add_device(dev);
    if (error)
        goto BusError;
    
    // 在设备电源管理目录中添加设备
    error = dpm_sysfs_add(dev);
    if (error)
        goto DPMError;
    
    // 添加设备到电源管理
    device_pm_add(dev);
    
    // 如果设备的devt 存在主设备号
    if (MAJOR(dev->devt)) {
        // 创建设备的dev 属性文件
        error = device_create_file(dev, &dev_attr_dev);
        if (error)
            goto DevAttrError;
        
        // 创建设备的sys 设备节点
        error = device_create_sys_dev_entry(dev);
        if (error)
            goto SysEntryError;
        
        // 在devtmpfs 上创建设备节点
        devtmpfs_create_node(dev);
    }
    
    // 通知设备添加的事件链
    if (dev->bus)
        blocking_notifier_call_chain(&dev->bus->p->bus_notifier,BUS_NOTIFY_ADD_DEVICE, dev);
    
    // 为设备的kobject 发送KOBJ_ADD 事件
    kobject_uevent(&dev->kobj, KOBJ_ADD);
    
    /*
    * 检查其他设备(消费者)是否一直在等待该设备(供应者)的添加,
    * 以便可以创建设备链接。
    *
    * 这需要在device_pm_add()之后进行,因为device_link_add()
    * 要求在调用之前注册供应者。
    *
    * 但是,这也需要在bus_probe_device()之前发生,以确保等待的消费者在驱动程序绑定到设备之前
    可以链接到它。
    */
    if (dev->fwnode && !dev->fwnode->dev) {
        dev->fwnode->dev = dev;
        fw_devlink_link_device(dev);
    }
    
    // 对总线中的设备进行探测
    bus_probe_device(dev);
    
    // 如果存在父设备,则将当前设备添加到父设备的子设备列表中
    if (parent)
        klist_add_tail(&dev->p->knode_parent,&parent->p->klist_children);
    
    // 如果设备有类别
    if (dev->class) {
        mutex_lock(&dev->class->p->mutex);
        
        // 将设备添加到类别的设备列表中
        klist_add_tail(&dev->knode_class,&dev->class->p->klist_devices);
        
        // 通知任何接口设备已添加
        list_for_each_entry(class_intf,&dev->class->p->interfaces, node)
        if (class_intf->add_dev)
            class_intf->add_dev(dev, class_intf);
        mutex_unlock(&dev->class->p->mutex);
    }
    
    done:
        // 释放设备的引用
        put_device(dev);
        return error;
    
    SysEntryError:
        // 如果存在主设备号,则移除设备的dev 属性文件
        if (MAJOR(dev->devt))
        device_remove_file(dev, &dev_attr_dev);
    
    DevAttrError:
        // 移除设备的电源管理
        device_pm_remove(dev);
        // 从设备电源管理目录中移除设备
        dpm_sysfs_remove(dev);
    DPMError:
        // 从总线中移除设备
        bus_remove_device(dev);
    BusError:
        // 移除设备的属性
        device_remove_attrs(dev);
    AttrsError:
        // 移除设备类的符号链接
        device_remove_class_symlinks(dev);
    SymlinkError:
        // 移除设备的uevent 属性文件
        device_remove_file(dev, &dev_attr_uevent);
    attrError:
        // 为设备的kobject 发送KOBJ_REMOVE 事件
        kobject_uevent(&dev->kobj, KOBJ_REMOVE);
        // 获取设备的粘合目录
        glue_dir = get_glue_dir(dev);
        // 删除设备的kobject
        kobject_del(&dev->kobj);
    Error:
        // 清理设备的粘合目录
        cleanup_glue_dir(dev, glue_dir);
        parent_error:
        // 释放父设备的引用
        put_device(parent);
    name_error:
        // 释放设备的私有数据
        kfree(dev->p);
        dev->p = NULL;
        goto done;
}

这个device_add 函数用于向系统中添加设备。让我们逐步理解代码并逐行解释其功能:

int device_add(struct device *dev)
{
    struct device *parent;
    struct kobject *kobj;
    struct class_interface *class_intf;
    int error = -EINVAL;
    struct kobject *glue_dir = NULL;

该函数以指向struct device的指针作为参数。它初始化了函数中使用的一些局部变量。

dev = get_device(dev);
if (!dev)
    goto done;

调用get_device 函数以获取设备的引用。

if (!dev->p) {
    error = device_private_init(dev);
    if (error)
        goto done;
}

如果设备结构体的p 成员为空,那么调用device_private_init 函数进行设备的私有初始化。

if (dev->init_name) {
    dev_set_name(dev, "%s", dev->init_name);
    dev->init_name = NULL;
}

如果设备的init_name 成员非空,那么使用dev_set_name 函数将其作为设备的名称,并将init_name 设置为空。

if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
    dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);

如果设备的名称为空且设备的总线(bus)和总线的名称(dev_name)非空,那么使用总线名称和设备ID 设置设备的名称。

if (!dev_name(dev)) {
    error = -EINVAL;
    goto name_error;
}

如果设备的名称为空,那么设置错误码为-EINVAL,并跳转到name_error 标签处。

pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

parent = get_device(dev->parent);
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) {
    error = PTR_ERR(kobj);
    goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj;

打印调试信息,包括设备的名称和函数名。获取设备的父设备,并设置设备的父对象。如果获取父对象的过程中发生错误,那么将错误码设为获取父对象的返回值,并跳转到parent_error 标签处。

if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
    set_dev_node(dev, dev_to_node(parent));

如果设备有父设备且设备的节点号为NUMA_NO_NODE,那么将设备的节点号设为父设备的节点号。

error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
if (error) {
    glue_dir = get_glue_dir(dev);
    goto Error;
}

使用kobject_add 函数将设备的内核对象添加到内核对象层次结构中。如果添加过程中发生错误,那么获取设备的”粘合目录”(glue_dir)并跳转到Error标签处。

if (platform_notify)
    platform_notify(dev);

如果存在平台通知函数(platform_notify),则调用该函数通知平台设备已添加。

error = device_create_file(dev, &dev_attr_uevent);
if (error)
    goto attrError;

error = device_add_class_symlinks(dev);
if (error)
    goto SymlinkError;

error = device_add_attrs(dev);
if (error)
    goto AttrsError;

error = bus_add_device(dev);
if (error)
    goto BusError;

error = dpm_sysfs_add(dev);
if (error)
    goto DPMError;

device_pm_add(dev);

创建设备的uevent 属性文件。添加设备的类符号链接。添加设备的属性。添加设备到总线。添加设备电源管理相关的sysfs 接口。启动设备接下来的代码主要是处理设备的设备号(devt)相关操作,以及通知相关组件设备添加的过程。

在上述代码中,使用bus_add_device 函数添加设备到总线中,我们再来追踪下这个函数。

int bus_add_device(struct device *dev)
{
    struct bus_type *bus = bus_get(dev->bus); // 获取设备所属的总线类型(bus_type)的指针
    int error = 0; // 错误码初始化为0
    if (bus) { // 如果成功获取总线类型指针
        pr_debug("bus: '%s': add device %s\n", bus->name, dev_name(dev)); // 打印调试信息,包括总线名称和设备名称
        error = device_add_groups(dev, bus->dev_groups); // 将设备添加到总线类型的设备组(dev_groups)中
        if (error) // 如果添加过程中发生错误
            goto out_put; // 跳转到out_put 标签处,执行错误处理代码
        
        error = sysfs_create_link(&bus->p->devices_kset->kobj,
              &dev->kobj, dev_name(dev)); // 在总线类型的设备集(kset)的内核对象(kobj)下创建设备的符号链接
        if (error) // 如果创建过程中发生错误
            goto out_groups; // 跳转到out_groups 标签处,执行错误处理代码
        
        error = sysfs_create_link(&dev->kobj,&dev->bus->p->subsys.kobj, 
                "subsystem"); // 在设备的内核对象(kobj)下创建指向总线类型子系统(subsystem)的符号链接
        if (error) // 如果创建过程中发生错误
            goto out_subsys; // 跳转到out_subsys 标签处,执行错误处理代码
        
        klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices); // 将设备的节点添加到总线类型的设备列表中
    }
    return 0; // 返回0 表示成功添加设备
    
    out_subsys:
    sysfs_remove_link(&bus->p->devices_kset->kobj, dev_name(dev)); // 移除设备和总线类型子系统之间的符号链接
    
    out_groups:
    device_remove_groups(dev, bus->dev_groups); // 从总线类型的设备组中移除设备
    
    out_put:
    bus_put(dev->bus); // 减少设备的总线引用计数
    return error; // 返回错误码
}

该代码是一个设备添加到总线的函数bus_add_device()。下面是对该函数的分析:

  1. bus_get(dev->bus):通过设备的bus 字段获取设备所属的总线类型(bus_type)的指针。这个函数会增加总线的引用计数,确保总线在设备添加过程中不会被释放。
  2. if (bus):检查总线类型指针是否有效。
  3. pr_debug(“bus: ‘%s’: add device %s\n”, bus->name, dev_name(dev)):打印调试信息,包括总线名称和设备名称,以便跟踪设备添加的过程。
  4. device_add_groups(dev, bus->dev_groups):将设备添加到总线类型的设备组(dev_groups)中。设备组是一组属性文件,用于在设备的sysfs 目录中显示和设置设备的属性。
  5. sysfs_create_link(&bus->p->devices_kset->kobj, &dev->kobj, dev_name(dev)):在总线类型的设备集(devices_kset)的内核对象(kobj)下创建设备的符号链接。这个符号链接将设备的sysfs 目录链接到总线类型的设备集目录中。
  6. sysfs_create_link(&dev->kobj, &dev->bus->p->subsys.kobj, “subsystem”):在设备的内核对象(kobj)下创建指向总线类型子系统(subsystem)的符号链接。这个符号链接将设备的sysfs目录链接到总线类型子系统的目录中。
  7. klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices):将设备的节点添加到总线类型的设备列表中。这个步骤用于维护总线类型下的设备列表。

至此,device_register 函数分析结束。

第105 章platform 总线设备注册流程实例分析实验

在上个章节中,我们详细分析了设备是如何注册到总线上的过程,在本章节中,我们将进一步探讨platform 设备是如何注册到platform 总线上的。
在平台设备驱动中, 我们使用platform_device_register 函数注册平台总线设备。platform_device_register 函数实现如下所示:

int platform_device_register(struct platform_device *pdev)
{
    device_initialize(&pdev->dev);
    arch_setup_pdev_archdata(pdev);
    return platform_device_add(pdev);
}

device_initialize 函数在上个章节重点分析过, 在此不作过多解释。我们重点来看platform_device_add 函数,函数实现如下所示:

int platform_device_add(struct platform_device *pdev)
{
    u32 i;
    int ret;
    
    // 检查输入的平台设备指针是否为空
    if (!pdev)
        return -EINVAL;
    
    // 如果平台设备的父设备为空,将父设备设置为platform_bus
    if (!pdev->dev.parent)
        pdev->dev.parent = &platform_bus;
    
    // 将平台设备的总线设置为platform_bus_type
    pdev->dev.bus = &platform_bus_type;
    
    // 根据平台设备的id 进行不同的处理
    switch (pdev->id) {
        default:
        // 根据设备名和id 设置设备的名字
        dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
        break;
            
        case PLATFORM_DEVID_NONE:
        // 如果id 为PLATFORM_DEVID_NONE,则只使用设备名作为设备的名字
        dev_set_name(&pdev->dev, "%s", pdev->name);
        break;
            
        case PLATFORM_DEVID_AUTO:
        /*
        * 自动分配的设备ID。将其标记为自动分配的,以便我们记住它需要释放,
        * 并且为了避免与显式ID 的命名空间冲突,我们附加一个后缀。
        */
        ret = ida_simple_get(&platform_devid_ida, 0, 0, GFP_KERNEL);
        if (ret < 0)
            goto err_out;
        pdev->id = ret;
        pdev->id_auto = true;
        dev_set_name(&pdev->dev, "%s.%d.auto", pdev->name, pdev->id);
        break;
    }
    
    // 遍历平台设备的资源列表,处理每个资源
    for (i = 0; i < pdev->num_resources; i++) {
        struct resource *p, *r = &pdev->resource[i];
        
        // 如果资源的名称为空,则将资源的名称设置为设备的名字
        if (r->name == NULL)
        r->name = dev_name(&pdev->dev);
        p = r->parent;
        if (!p) {
            
            // 如果资源没有指定父资源,则根据资源类型设置默认的父资源
            if (resource_type(r) == IORESOURCE_MEM)
                p = &iomem_resource;
            else if (resource_type(r) == IORESOURCE_IO)
                p = &ioport_resource;
    	}
        
        // 如果父资源存在,并且将资源插入到父资源中失败,则返回错误
        if (p && insert_resource(p, r)) {
            dev_err(&pdev->dev, "failed to claim resource %d: %pR\n", i, r);
            ret = -EBUSY;
            goto failed;
        }
    }
    
    // 打印调试信息,注册平台设备
    pr_debug("Registering platform device '%s'. Parent at %s\n",dev_name(&pdev->dev), dev_name(pdev->dev.parent));
    
    // 添加设备到设备层级中,注册设备
    ret = device_add(&pdev->dev);
    if (ret == 0)
        return ret;
    
    failed:
        // 如果设备ID 是自动分配的,需要移除已分配的ID
        if (pdev->id_auto) {
            ida_simple_remove(&platform_devid_ida, pdev->id);
            pdev->id = PLATFORM_DEVID_AUTO;
        }
    
        // 在失败的情况下,释放已插入的资源
        while (i--) {
            struct resource *r = &pdev->resource[i];
            if (r->parent)
                release_resource(r);
        }
    
    err_out:
        // 返回错误码
        return ret;
}

上述函数表示向平台总线中添加平台设备。下面是函数的执行过程及注释:

  • 第6~8 行代码中,检查传入的平台设备指针是否为空,如果为空则返回无效参数错误码。
  • 第11 行代码中,如果平台设备的父设备为空,则将父设备设置为platform_bus。将平台设备的总线设置为platform_bus_type。
  • 第17 行~39 行代码中,根据平台设备的ID 进行不同的处理:默认情况下,根据设备名称和ID 设置设备的名称。如果ID 为PLATFORM_DEVID_NONE,则只使用设备名称作为设备的名称。如果ID 为PLATFORM_DEVID_AUTO,则自动分配设备ID。使用ida_simple_get 函数获取一个可用的ID,并将设备ID 标记为自动分配。设备名称将附加一个后缀以避免与显式ID 的命名空间冲突。

设置设备的名字,名字有三种格式,如下图所示:

image-20240909144609163

image-20240909144616376

  • 第42 行~64 行代码中,遍历平台设备的资源列表,处理每个资源。如果资源的名称为空,则将资源的名称设置为设备的名称。如果资源没有指定父资源,则根据资源类型设置默认的父资源。如果父资源存在,并且将资源插入到父资源中失败,则返回忙碌错误码。
    在Linux 操作系统中,为了方便管理设备资源,所有设备资源都会添加到资源树中。每个设备资源都会有一个父资源,用于表示该设备资源所属的资源的根。
  • 第67 行代码,打印调试信息,注册平台设备。
  • 第71 行代码,调用device_add 函数将设备添加到设备层级结构中进行设备注册。如果注册成功,则返回0。

到此,platform 总线设备注册流程分析完毕。

第106 章为什么注册总线之前要先注册设备实例分析实验

在上个章节中,我们详细分析了设备如何注册到总线上的过程,其中包括platform 设备的注册。但在注册platform 设备之前,会先调用device_register() 函数注册一个platform bus设备。

为了更好地理解这一过程,需要了解platform 总线的特性。在Linux 内核中,platform 总线是一种特殊的总线类型,用于管理与硬件平台紧密相关的设备。它提供了一种机制,使得与特定硬件平台相关的设备能够在系统中得到正确的识别和初始化。

在注册platform 总线之前,需要先注册一个platform bus 设备。这个platform bus 设备充当了platform 总线的代表,它是platform 总线与设备之间的桥梁。通过注册platform bus设备,系统可以识别到platform 总线的存在,并为后续的platform 设备注册提供必要的基础。通过调用device_register() 函数注册platform bus 设备,可以将其添加到设备层次结构中,并与platform 总线相关联。这样,当platform 总线初始化时,它可以找到并识别这个platform bus 设备,进而完成platform 设备的注册和管理。

接下来我们从代码的层面进行分析这个问题。
platform_bus_init 函数如下图所示:

image-20240909144859549

先调用device_register 函数注册paltform_bus 这个设备,会在/sys/devices 目录下创建目录/sys/devices/platform,此目录所有platform 设备的父目录,即所有platform_device 设备都会在/sys/devices/platform 下创建子目录,如下图所示:

image-20240909152551038

创建好platform bus 设备之后,使用platform_device_add 函数将platform_device 结构体添加到platform 总线中进行注册,代码实现如下所示:

image-20240912095848722

至此,注册总线之前要先注册设备实验分析完毕。

第107 章在自己的总线下注册驱动实验

107.1 实验程序的编写

107.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\77_driver\module
我们编写驱动代码,这段代码的作用是注册一个驱动程序,该驱动程序属于名为”mydevice”的总线类型”mybus”,并在探测设备时调用”mydriver_probe”函数,移除设备时调用”mydriver_remove”函数。编写完成的driver.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/device.h>
#include <linux/sysfs.h>

extern struct bus_type mybus;

int mydriver_remove(struct device *dev){
    printk("This is mydriver_remove\n");
    return 0;
};

int mydriver_probe(struct device *dev){
    printk("This is mydriver_probe\n");
    return 0;
};


struct device_driver mydriver = {
    .name = "mydevice",
    .bus = &mybus,
    .probe = mydriver_probe,
    .remove = mydriver_remove,
    
};

// 模块的初始化函数
static int mydriver_init(void)
{
    int ret;
    ret = driver_register(&mydriver);
    
    return ret;
}

// 模块退出函数
static void mydriver_exit(void)
{
    driver_unregister(&mydriver);
}

module_init(mydriver_init); // 指定模块的初始化函数
module_exit(mydriver_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

107.3 运行测试

107.3.1 编译驱动程序

在上一小节中的driver.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成driver.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

107.3.2 运行测试

开发板启动之后,使用以下命令进行bus.ko 驱动模块的加载,如下图(图107-4)所示:

insmod bus.ko

image-20240912100204584

然后加载device.ko 驱动模块,如下图所示:

insmod device.ko

image-20240912100244826

然后加载driver.ko 驱动模块,如下图所示:

image-20240912100255311

驱动加载之后,我们进入/sys/devices 目录下,如下图所示,有注册生成的设备。

image-20240912100312052

我们进入到/sys/bus/mybus/devices 目录下,如下图所示,在总线下注册的设备为mydevice。然后进入到/sys/bus/mybus/driver 目录下,如下图所示,在总线下注册的驱动为mydevice

image-20240912100343744

最后可以使用以下命令进行驱动的卸载,如下所示:

rmmod device
rmmod driver
rmmod bus

至此,在总线下注册驱动实验就完成了。

第108 章驱动注册流程分析实验

在上一章节中,我们学习了设备注册到自定义总线下,并进行了实验。在本章节中,我们将继续探讨驱动是如何注册到总线上的。
一旦驱动程序的结构体和匹配信息准备就绪,我们就可以调用driver_register() 函数将驱动程序注册到总线上。

108.1 driver_register 函数解析

driver_register 函数实现如下所示:

int driver_register(struct device_driver *drv)
{
    int ret;
    struct device_driver *other;
    
    // 检查总线是否已初始化
    if (!drv->bus->p) {
        pr_err("Driver '%s' was unable to register with bus_type '%s' because the bus was not initialized.\n",drv->name, drv->bus->name);
        return -EINVAL;
    }
    
    // 检查驱动程序的方法是否需要更新
    if ((drv->bus->probe && drv->probe) ||(drv->bus->remove && drv->remove) ||
        (drv->bus->shutdown && drv->shutdown))
        printk(KERN_WARNING "Driver '%s' needs updating - please use bus_type methods\n", drv->name);
    
    // 检查驱动程序是否已被注册
    other = driver_find(drv->name, drv->bus);
    if (other) {
        printk(KERN_ERR "Error: Driver '%s' is already registered, aborting...\n", drv->name);
        return -EBUSY;
    }
    
    ret = bus_add_driver(drv); // 将驱动程序添加到总线
    if (ret)
        return ret;
    
    ret = driver_add_groups(drv, drv->groups); // 添加驱动程序的组属性
    if (ret) {
        bus_remove_driver(drv); // 移除已添加的驱动程序
        return ret;
    }
    kobject_uevent(&drv->p->kobj, KOBJ_ADD); // 发送内核对象事件,通知驱动程序添加成功
    return ret; // 返回注册结果
}

driver_register 函数用于注册设备驱动程序并将其添加到总线中。以下是该函数的功能解释:

  • 第5~11 行代码检查总线是否已初始化:
    • 首先,通过drv->bus 访问设备驱动程序结构体中的总线信息。如果总线的p 成员为NULL,表示总线未初始化。如果总线未初始化,则打印错误消息,并返回-EINVAL 错误码表示无效的参数。
  • 第13 行~19 行代码检查驱动程序的方法是否需要更新:
    • 通过检查驱动程序结构体中的bus->probe 和drv->probe、bus->remove 和drv->remove、bus->shutdown 和drv->shutdown 成员是否同时存在来判断。如果存在需要更新的方法组合,说明驱动程序需要更新。在这种情况下,打印警告消息,建议使用bus_type 方法进行更新。
  • 第21 行~26 行检查驱动程序是否已被注册:
    • 调用driver_find 函数来查找是否已经注册了同名的驱动程序。如果找到同名驱动程序,表示驱动程序已经注册过。在这种情况下,打印错误消息,并返回-EBUSY 错误码表示设备忙。
  • 第28 行代码添加驱动程序到总线:
    • 调用bus_add_driver 函数将驱动程序添加到总线。如果添加失败,则返回相应的错误码。
  • 第31 行代码添加驱动程序的组属性:
    • 调用driver_add_groups 函数将驱动程序的组属性添加到驱动程序中。如果添加失败,则调用bus_remove_driver 函数移除已添加的驱动程序,并返回相应的错误码。
  • 第36 行代码发送内核对象事件:
    • 调用kobject_uevent 函数向驱动程序的内核对象发送事件,通知驱动程序已成功添加到系统中。

综上所述,driver_register 函数的功能是注册设备驱动程序并将其添加到总线中,同时进行各种检查和错误处理操作。
在上面代码中,调用bus_add_driver 函数将驱动程序添加到总线。我们来详细分析下bus_add_driver 函数。

int bus_add_driver(struct device_driver *drv)
{
    struct bus_type *bus;
    struct driver_private *priv;
    int error = 0;
    
    // 获取总线对象
    bus = bus_get(drv->bus);
    if (!bus)
        return -EINVAL; // 返回无效参数错误码
    
    pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);
    
    // 分配并初始化驱动程序私有数据
    priv = kzalloc(sizeof(*priv), GFP_KERNEL);
    if (!priv) {
        error = -ENOMEM;
        goto out_put_bus;
    }
    klist_init(&priv->klist_devices, NULL, NULL);
    priv->driver = drv;
    drv->p = priv;
    priv->kobj.kset = bus->p->drivers_kset;
    
    // 初始化并添加驱动程序的内核对象
    error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,"%s", drv->name);
    if (error)
        goto out_unregister;
    
    // 将驱动程序添加到总线的驱动程序列表
    klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
    
    // 如果总线启用了自动探测,则尝试自动探测设备
    if (drv->bus->p->drivers_autoprobe) {
        error = driver_attach(drv);
        if (error)
            goto out_unregister;
    }
    // 将驱动程序添加到模块
    module_add_driver(drv->owner, drv);
    
    // 创建驱动程序的uevent 属性文件
    error = driver_create_file(drv, &driver_attr_uevent);
    if (error) {
        printk(KERN_ERR "%s: uevent attr (%s) failed\n",__func__, drv->name);
    }
    
    // 添加驱动程序的组属性
    error = driver_add_groups(drv, bus->drv_groups);
    if (error) {
        /* How the hell do we get out of this pickle? Give up */
        printk(KERN_ERR "%s: driver_create_groups(%s) failed\n",__func__, drv->name);
    }
    
    // 如果驱动程序不禁止绑定属性文件,则添加绑定属性文件
    if (!drv->suppress_bind_attrs) {
        error = add_bind_files(drv);
        if (error) {
            /* Ditto */
            printk(KERN_ERR "%s: add_bind_files(%s) failed\n",__func__, drv->name);
        }
    }
    return 0; // 返回成功
    
    out_unregister:
        kobject_put(&priv->kobj);
        /* drv->p is freed in driver_release() */
        drv->p = NULL;
    out_put_bus:
        bus_put(bus);
        return error; // 返回错误码
}

bus_add_driver 该函数用于将设备驱动程序添加到总线中。以下是功能的详细解释:

  • 第8 行代码获取总线对象:
    • 通过drv->bus 访问设备驱动程序结构体中的总线信息。通过调用bus_get 函数获取总线对象。如果总线对象不存在,则返回-EINVAL 错误码表示无效的参数。
  • 第15 行~23 行代码分配并初始化驱动程序私有数据:
    • 调用kzalloc 函数为驱动程序的私有数据结构体priv 分配内存,并使用GFP_KERNEL 标志进行内存分配。如果内存分配失败,则返回-ENOMEM 错误码表示内存不足。使用klist_init函数初始化priv 结构体中的设备列表。设置priv 结构体中的驱动程序指针,并将其赋值为当前的驱动程序。将drv->p 指向priv 结构体,以便后续的释放操作。
  • 第25 行代码初始化并添加驱动程序的内核对象:
    • 设置priv->kobj.kset 成员为总线对象的drivers_kset。调用kobject_init_and_add 函数初始化并添加驱动程序的内核对象。如果初始化或添加失败,则跳转到out_unregister 进行错误
      处理。
  • 第31 行代码将驱动程序添加到总线的驱动程序列表:
    • 使用klist_add_tail 函数将驱动程序的节点添加到总线的驱动程序列表中。

到此,如何在总线下注册驱动流程已经分析完毕。

第109 章probe 函数执行流程分析实验

在上个章节中,我们在代码中分析了驱动在总线下注册的流程,本章节我们继续分析代码,分析probe 函数执行流程。
接着上个章节分析bus_add_driver 函数。bus_add_driver 函数实现如下所示:

int bus_add_driver(struct device_driver *drv)
{
    struct bus_type *bus;
    struct driver_private *priv;
    int error = 0;
    
    // 获取总线对象
    bus = bus_get(drv->bus);
    if (!bus)
        return -EINVAL; // 返回无效参数错误码
    
    pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);
    
    // 分配并初始化驱动程序私有数据
    priv = kzalloc(sizeof(*priv), GFP_KERNEL);
    if (!priv) {
        error = -ENOMEM;
        goto out_put_bus;
    }
    klist_init(&priv->klist_devices, NULL, NULL);
    priv->driver = drv;
    drv->p = priv;
    priv->kobj.kset = bus->p->drivers_kset;
    
    // 初始化并添加驱动程序的内核对象
    error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,"%s", drv->name);
    if (error)
        goto out_unregister;
    
    // 将驱动程序添加到总线的驱动程序列表
    klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
    
    // 如果总线启用了自动探测,则尝试自动探测设备
    if (drv->bus->p->drivers_autoprobe) {
        error = driver_attach(drv);
        if (error)
            goto out_unregister;
    }
    // 将驱动程序添加到模块
    module_add_driver(drv->owner, drv);
    
    // 创建驱动程序的uevent 属性文件
    error = driver_create_file(drv, &driver_attr_uevent);
    if (error) {
        printk(KERN_ERR "%s: uevent attr (%s) failed\n",__func__, drv->name);
    }
    
    // 添加驱动程序的组属性
    error = driver_add_groups(drv, bus->drv_groups);
    if (error) {
        /* How the hell do we get out of this pickle? Give up */
        printk(KERN_ERR "%s: driver_create_groups(%s) failed\n",__func__, drv->name);
    }
    
    // 如果驱动程序不禁止绑定属性文件,则添加绑定属性文件
    if (!drv->suppress_bind_attrs) {
        error = add_bind_files(drv);
        if (error) {
            /* Ditto */
            printk(KERN_ERR "%s: add_bind_files(%s) failed\n",__func__, drv->name);
        }
    }
    return 0; // 返回成功
    
    out_unregister:
        kobject_put(&priv->kobj);
        /* drv->p is freed in driver_release() */
        drv->p = NULL;
    out_put_bus:
        bus_put(bus);
        return error; // 返回错误码
}
  • 第31 行~37 行代码自动探测设备:
    • 如果总线启用了自动探测(drivers_autoprobe 标志),则调用driver_attach 函数尝试自动探测设备。
      如果自动探测失败,则跳转到out_unregister 进行错误处理。
      变量drivers_autoprobe 也可以在用户空间通过属性文件drivers_autoprobe 来控制,再次体现属性文件的作用。
  • image-20240912102010300
  • 第39 行代码将驱动程序添加到模块:
    • 调用module_add_driver 函数将驱动程序添加到模块中。
  • 第42 行~46 行代码创建驱动程序的uevent 属性文件:
    • 调用driver_create_file 函数为驱动程序创建uevent 属性文件。如果创建失败,则打印错误消息。
  • 第48 行~53 行代码添加驱动程序的组属性:
    • 调用driver_add_groups 函数将驱动程序的组属性添加到驱动程序中。如果添加失败,则打印错误消息。
  • 第56 行~63 行添加绑定属性文件:
    • 如果驱动程序没有禁止绑定属性文件(suppress_bind_attrs 标志),则调用add_bind_files函数添加绑定属性文件。如果添加失败,则打印错误消息。

在上述函数中,使用driver_attach 函数来探测设备,我们进一步分析下driver_attach 函数。

int driver_attach(struct device_driver *drv)
{
    return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
}
EXPORT_SYMBOL_GPL(driver_attach);

bus_for_each_dev 函数实现如下所示:

int bus_for_each_dev(struct bus_type *bus, struct device *start,
                     void *data, int (*fn)(struct device *, void *))
{
    struct klist_iter i;
    struct device *dev;
    int error = 0;
    
    // 检查总线对象是否存在
    if (!bus || !bus->p)
        return -EINVAL; // 返回无效参数错误码
    
    // 初始化设备列表迭代器
    klist_iter_init_node(&bus->p->klist_devices, &i,(start ? &start->p->knode_bus : NULL));
    // 遍历设备列表并执行指定的函数
    while (!error && (dev = next_device(&i)))
        error = fn(dev, data);
    
    // 退出设备列表迭代器
    klist_iter_exit(&i);
    return error; // 返回执行过程中的错误码(如果有)
}

这个函数的作用是遍历指定总线上的所有设备,并对每个设备执行指定的函数fn。以下是函数的参数和功能解释:

  • bus:指定要遍历的总线对象。

  • start:指定开始遍历的设备对象。如果为NULL,则从总线的第一个设备开始遍历。

  • data:传递给函数fn 的额外数据。

  • fn:指定要执行的函数,该函数接受一个设备对象和额外数据作为参数,并返回一个整数错误码。

  • 第9 行代码中,首先,检查传入的总线对象是否存在以及与该总线相关的私有数据是否存在。如果总线对象或其私有数据不存在,返回-EINVAL 表示无效的参数错误码。

  • 第13 行~14 行代码中,接下来,初始化设备列表迭代器,以便遍历总线上的设备。使用klist_iter_init_node 函数初始化一个设备列表迭代器。传递总线对象的设备列表klist_devices、迭代器对象i,以及可选的起始设备的节点指针。然后,在一个循环中遍历设备列表并执行指定的函数。

  • 第17 行代码中,使用next_device 函数从迭代器中获取下一个设备。如果存在下一个设备,则调用传入的函数指针fn,并将当前设备和额外的数据参数传递给它。如果执行函数时出现错误,将错误码赋值给error。

  • 第21 行代码中,最后,退出设备列表迭代器,释放相关资源。使用klist_iter_exit 函数退出设备列表的迭代器。

总的来说, bus_for_each_dev() 函数主要是提供了一个遍历指定总线上的设备对象列表,并对每个设备对象进行特定操作的快捷方式,可以用于驱动程序中需要管理和操作大量设备实例的场景。
fn 要执行的函数为__driver_attach 函数,函数实现如下所示:

static int __driver_attach(struct device *dev, void *data)
{
    struct device_driver *drv = data; // 传入的数据参数作为设备驱动对象
    int ret;
    /*
    * Lock device and try to bind to it. We drop the error
    * here and always return 0, because we need to keep trying
    * to bind to devices and some drivers will return an error
    * simply if it didn't support the device.
    *
    * driver_probe_device() will spit a warning if there
    * is an error.
    */
    ret = driver_match_device(drv, dev); // 尝试将驱动程序绑定到设备上
    if (ret == 0) {
        /* no match */
        return 0; // 如果没有匹配,则返回0
    } 
    else if (ret == -EPROBE_DEFER) {
        dev_dbg(dev, "Device match requests probe deferral\n");
        driver_deferred_probe_add(dev); // 请求推迟探测设备
    }  
    else if (ret < 0) {
        dev_dbg(dev, "Bus failed to match device: %d", ret);
        return ret; // 总线无法匹配设备,返回错误码
    } /* ret > 0 means positive match */
    
    if (driver_allows_async_probing(drv)) {
        /*
        * Instead of probing the device synchronously we will
        * probe it asynchronously to allow for more parallelism.
        *
        * We only take the device lock here in order to guarantee
        * that the dev->driver and async_driver fields are protected
        */
        dev_dbg(dev, "probing driver %s asynchronously\n", drv->name);
        device_lock(dev); // 锁定设备以保护dev->driver 和async_driver 字段
            if (!dev->driver) {
            get_device(dev);
            dev->p->async_driver = drv; // 设置设备的异步驱动程序
            async_schedule(__driver_attach_async_helper, dev); // 异步调度驱动程序的附加处理函数
        }
        device_unlock(dev); // 解锁设备
        return 0;
    }
    
    device_driver_attach(drv, dev); // 同步探测设备并绑定驱动程序
    return 0; // 返回0 表示成功执行驱动程序附加操作
}

上述函数中使用driver_match_device 函数尝试将驱动程序绑定到设备上,我们再来看看driver_match_device 函数。

static inline int driver_match_device(struct device_driver *drv,struct device *dev)
{
    return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}

这是一个内联函数driver_match_device 的代码片段。该函数用于检查设备是否与驱动程序匹配。以下是对代码的解释:
drv:指向设备驱动程序对象的指针。
dev:指向设备对象的指针。

函数的执行过程如下:

  1. 首先,检查驱动程序对象的bus 字段是否为NULL,以及bus 字段的match 函数是否存在。驱动程序对象的bus 字段表示该驱动程序所属的总线。match 函数是总线对象中的一个函数指针,用于检查设备与驱动程序是否匹配。
  2. 如果match`函数存在,则调用总线对象的match 函数,传入设备对象和驱动程序对象作为参数。
    drv->bus->match(dev, drv)表示调用总线对象的match 函数,并将设备对象和驱动程序对象作为参数传递给该函数。dev 是用于匹配的设备对象。drv 是用于匹配的驱动程序对象。
  3. 如果总线对象的match 函数返回0,则表示设备与驱动程序不匹配,函数将返回0。返回值为0 表示不匹配
  4. 如果总线对象的match 函数返回非零值(大于0),则表示设备与驱动程序匹配,函数将返回1。返回值为1 表示匹配。
  5. 如果总线对象的match 函数不存在(为NULL),则默认认为设备与驱动程序匹配,函数将返回1。
    这段代码使用了条件运算符? : 来判断总线对象的match 函数是否存在,以便选择执行相应的逻辑。如果总线对象没有提供match 函数,那么默认认为设备与驱动程序匹配,返回值为1。

如果设备和驱动匹配上,会继续执行device_driver_attach 函数,如下所示:

int device_driver_attach(struct device_driver *drv, struct device *dev)
{
    int ret = 0;
    __device_driver_lock(dev, dev->parent);
    /*
    * If device has been removed or someone has already successfully
    * bound a driver before us just skip the driver probe call.
    */
    if (!dev->p->dead && !dev->driver)
        ret = driver_probe_device(drv, dev);
    __device_driver_unlock(dev, dev->parent);
    return ret;
}

driver_probe_device 函数,如下所示:

int driver_probe_device(struct device_driver *drv, struct device *dev)
{
    int ret = 0;
    
    // 检查设备是否已注册,如果未注册则返回错误码-ENODEV
    if (!device_is_registered(dev))
        return -ENODEV;
    
    // 打印调试信息,表示设备与驱动程序匹配
    pr_debug("bus: '%s': %s: matched device %s with driver %s\n",drv->bus->name, 
             __func__, dev_name(dev), drv->name);
    
    // 获取设备供应商的运行时引用计数
    pm_runtime_get_suppliers(dev);
    
    // 如果设备有父设备,获取父设备的同步运行时引用计数
    if (dev->parent)
        pm_runtime_get_sync(dev->parent);
    
    // 等待设备的运行时状态达到稳定
    pm_runtime_barrier(dev);
    
    // 根据初始化调试标志选择调用真实的探测函数
    if (initcall_debug)
        ret = really_probe_debug(dev, drv);
    else
        ret = really_probe(dev, drv);
    
    // 请求设备进入空闲状态(省电模式)
    pm_request_idle(dev);
    
    // 如果设备有父设备,释放父设备的运行时引用计数
    if (dev->parent)
        pm_runtime_put(dev->parent);
    
    // 释放设备供应商的运行时引用计数
    pm_runtime_put_suppliers(dev);
    
    // 返回探测函数的执行结果
    return ret;
}

经过前面代码的分析,总结设备和驱动匹配流程函数,如下图所示

image-20240912103404026

probe 函数的执行,我们来分析really_probe 函数。

static int really_probe(struct device *dev, struct device_driver *drv)
{
    int ret = -EPROBE_DEFER; // 初始化返回值为延迟探测
    int local_trigger_count = atomic_read(&deferred_trigger_count); // 获取当前延迟探测计数
    // 判断是否启用了驱动移除测试
    bool test_remove = IS_ENABLED(CONFIG_DEBUG_TEST_DRIVER_REMOVE) &&!drv->suppress_bind_attrs;

    if (defer_all_probes) {
        /*
        * defer_all_probes 的值只能通过device_defer_all_probes_enable() 设置,
        * 而该函数会紧接着调用wait_for_device_probe(),以避免任何竞争情况。
        */
        dev_dbg(dev, "Driver %s 强制延迟探测\n", drv->name);
        driver_deferred_probe_add(dev);
        return ret;
    }
    
    ret = device_links_check_suppliers(dev); // 检查设备的供应者链路
    if (ret == -EPROBE_DEFER)
    	driver_deferred_probe_add_trigger(dev, local_trigger_count); // 将设备添加到延迟探测触发列表
    if (ret)
    	return ret;
    
    atomic_inc(&probe_count); // 增加探测计数
    pr_debug("bus: '%s': %s: 正在使用设备%s 探测驱动程序%s\n",drv->bus->name, 
             __func__, drv->name, dev_name(dev));
    if (!list_empty(&dev->devres_head)) {
        dev_crit(dev, "探测之前存在资源\n");
        ret = -EBUSY;
        goto done;
    }
    
re_probe:
    dev->driver = drv;
    /* 如果使用了pinctrl,绑定引脚*/
    ret = pinctrl_bind_pins(dev);
    if (ret)
        goto pinctrl_bind_failed;

    ret = dma_configure(dev); // 配置DMA
    if (ret)
        goto probe_failed;
    if (driver_sysfs_add(dev)) { // 添加驱动的sysfs
        printk(KERN_ERR "%s: driver_sysfs_add(%s) 失败\n",__func__, dev_name(dev));
        goto probe_failed;
    }
    // 如果设备有电源管理域并且存在激活函数,激活电源管理域
    if (dev->pm_domain && dev->pm_domain->activate) { 
        ret = dev->pm_domain->activate(dev);
        if (ret)
        goto probe_failed;
    }

    if (dev->bus->probe) { // 如果总线有探测函数,调用总线的探测函数
        ret = dev->bus->probe(dev);
        if (ret)
        goto probe_failed;
    } 
    else if (drv->probe) { // 否则调用驱动的探测函数
        ret = drv->probe(dev);
        if (ret)
        goto probe_failed;
    }
    
    if (test_remove) { // 如果启用了驱动移除测试
        test_remove = false;
        if (dev->bus->remove) // 如果总线有移除函数,调用总线的移除函数
            dev->bus->remove(dev);
        else if (drv->remove) // 否则调用驱动的移除函数
            drv->remove(dev);
        devres_release_all(dev); // 释放设备的资源
        driver_sysfs_remove(dev); // 移除驱动的sysfs
        dev->driver = NULL;
        dev_set_drvdata(dev, NULL);
        // 如果设备有电源管理域并且存在解除函数,解除电源管理域
        if (dev->pm_domain && dev->pm_domain->dismiss) 
            dev->pm_domain->dismiss(dev);
        pm_runtime_reinit(dev); // 重新初始化电源管理运行时
        goto re_probe; // 重新进行探测
    }

    pinctrl_init_done(dev); // 完成pinctrl 的初始化

    // 如果设备有电源管理域并且存在同步函数,同步电源管理域
    if (dev->pm_domain && dev->pm_domain->sync) 
        dev->pm_domain->sync(dev);
    driver_bound(dev); // 驱动绑定成功
    ret = 1;
    pr_debug("bus: '%s': %s: %s: 将设备%s 绑定到驱动程序%s\n",drv->bus->name,
             __func__, dev_name(dev), drv->name);
    goto done;
    
probe_failed:
    if (dev->bus)
        blocking_notifier_call_chain(&dev->bus->p->bus_notifier,BUS_NOTIFY_DRIVER_NOT_BOUND, dev);

pinctrl_bind_failed:
    device_links_no_driver(dev); // 将设备与驱动解除绑定
    devres_release_all(dev); // 释放设备的资源
    dma_deconfigure(dev); // 取消DMA 配置
    driver_sysfs_remove(dev); // 移除驱动的sysfs
    dev->driver = NULL;
    dev_set_drvdata(dev, NULL);
    
    // 如果设备有电源管理域并且存在解除函数,解除电源管理域
    if (dev->pm_domain && dev->pm_domain->dismiss) 
        dev->pm_domain->dismiss(dev);
    
    pm_runtime_reinit(dev); // 重新初始化电源管理运行时
    dev_pm_set_driver_flags(dev, 0); // 设置设备的驱动标志为0
    
    switch (ret) {
    case -EPROBE_DEFER:
        /* 驱动程序请求延迟探测*/
        dev_dbg(dev, "Driver %s 请求延迟探测\n", drv->name);
        driver_deferred_probe_add_trigger(dev, local_trigger_count); // 将设备添加到延迟探测触发列表
        break;
    case -ENODEV:
    case -ENXIO:
        pr_debug("%s: 对%s 的探测拒绝匹配%d\n",
        drv->name, dev_name(dev), ret);
        break;
    default:
        /* 驱动程序匹配但探测失败*/
        printk(KERN_WARNING "%s: 对%s 的探测失败,错误码%d\n",drv->name, dev_name(dev), ret);
    }
    /*
    * 忽略->probe 返回的错误,以便下一个驱动程序可以尝试运行。
    */
    ret = 0;
    
done:
    atomic_dec(&probe_count); // 减少探测计数
    wake_up_all(&probe_waitqueue); // 唤醒等待探测的进程
    return ret;
}

至此,probe 函数执行流程分析完毕。

第110 章加载驱动和加载设备先后顺序分析实验

经过前面的实验章节,我们不管是先加载device.ko 还是driver.ko,驱动和设备都可以匹配成功。所以我们可以猜测不管是device 驱动还是driver 驱动,都会有匹配操作。
在drivers/base/core.c 文件中的device_ add 函数中调用了bus_ probe_ device 函数。如下图所示:

image-20240912104812074

bus_ probe_ device 函数,如下图所示,此函数中最重要的是device_initial_probe 函数

image-20240912104825223

device_initial_probe 函数,如下所示

image-20240912104838782

__device_attach 函数如下所示:

static int __device_attach(struct device *dev, bool allow_async)
{
    int ret = 0;
    device_lock(dev);
    if (dev->p->dead) {
        goto out_unlock;
    } 
    else if (dev->driver) {
        // 如果设备已经绑定了驱动程序,则返回1
        if (device_is_bound(dev)) {
            ret = 1;
            goto out_unlock;
        }
        // 尝试将设备与驱动程序进行绑定
        ret = device_bind_driver(dev);
        if (ret == 0)
            ret = 1;
        else {
            // 绑定失败,将设备的驱动程序指针置为NULL
            dev->driver = NULL;
            ret = 0;
        }
    } 
    else {
        // 如果设备没有驱动程序,需要遍历总线上的驱动程序进行匹配
        struct device_attach_data data = {
            .dev = dev,
            .check_async = allow_async,
            .want_async = false,
    	};
        // 如果设备有父设备,调用pm_runtime_get_sync() 增加父设备的引用计数
        if (dev->parent)
            pm_runtime_get_sync(dev->parent);

        // 遍历总线上的驱动程序,调用__device_attach_driver() 进行匹配
        ret = bus_for_each_drv(dev->bus, NULL, &data,__device_attach_driver);
        if (!ret && allow_async && data.have_async) {
            /*
            * 如果无法同步找到适合的驱动程序,并且允许异步探测以及有驱动程序要求异步探测,
            * 则尝试进行异步探测。
            */
            dev_dbg(dev, "scheduling asynchronous probe\n");
            // 增加设备的引用计数,以确保在异步探测期间设备不会被释放
            get_device(dev);
            // 调度异步任务__device_attach_async_helper() 进行异步探测
            async_schedule(__device_attach_async_helper, dev);
        } else {
            // 如果无法异步探测或者没有驱动程序要求异步探测,则调用pm_request_idle() 进入空闲状态
            pm_request_idle(dev);
        }
        // 如果设备有父设备,调用pm_runtime_put() 减少父设备的引用计数
        if (dev->parent)
            pm_runtime_put(dev->parent);
    }
    out_unlock:
        // 解锁设备
        device_unlock(dev);
        return ret;
}

__device_attach 函数的主要逻辑如下:
首先,通过调用device_lock(dev) 锁定设备,确保在进行设备附加操作时不会被其他线程干扰。接下来,检查设备的状态。如果设备已经被标记为死亡,那么直接跳转到解锁设备的位置,并返回0。
如果设备已经绑定了驱动程序,则返回1。如果设备没有绑定驱动程序,则尝试将设备与驱动程序进行绑定。如果绑定成功,返回1;否则,将设备的驱动程序指针置为NULL,并返回0。
如果设备既没有绑定驱动程序,也没有被标记为死亡,那么需要遍历总线上的驱动程序进行匹配。为此,定义了一个结构体struct device_attach_data,其中包含了设备、是否允许异步探测以及是否有驱动程序要求异步探测的信息。
如果设备有父设备,调用pm_runtime_get_sync(dev->parent) 增加父设备的引用计数。

​ 调用bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver) 遍历总线上的驱动程序,并调用__device_attach_driver 进行匹配。__device_attach_driver 是一个回调函数,用于判断驱动程序是否适配当前设备。
​ 如果无法同步找到适合的驱动程序,并且允许异步探测以及有驱动程序要求异步探测,则调度异步任务__device_attach_async_helper 进行异步探测。在异步探测之前,会增加设备的引用计数以确保设备在异步探测期间不会被释放。异步探测会在后台进行,不会阻塞当前线程。
​ 如果无法异步探测或者没有驱动程序要求异步探测,则调用pm_request_idle(dev) 进入空闲状态,让设备进入省电模式。

在上述函数中,使用device_bind_driver 函数将设备与驱动程序进行绑定,如下所示:

image-20240912110216279

driver_bound 函数如下所示,用于将驱动程序绑定到设备上。

static void driver_bound(struct device *dev)
{
if (device_is_bound(dev)) {
// 如果设备已经绑定了驱动程序,则输出警告信息并返回
printk(KERN_WARNING "%s: device %s already bound\n",
__func__, kobject_name(&dev->kobj));
return;
}
// 输出绑定信息
pr_debug("driver: '%s': %s: bound to device '%s'\n", dev->driver->name,
__func__, dev_name(dev));
// 将设备添加到驱动程序的设备链表中
klist_add_tail(&dev->p->knode_driver, &dev->driver->p->klist_devices);
// 更新设备的驱动程序链接状态
device_links_driver_bound(dev);
// 检查设备的电源管理回调函数
device_pm_check_callbacks(dev);
/*
* 确保设备不再位于延迟探测列表中,并启动重试所有待处理设备
*/
driver_deferred_probe_del(dev);
driver_deferred_probe_trigger();
// 如果设备有总线,调用总线通知链进行通知
if (dev->bus)
blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
BUS_NOTIFY_BOUND_DRIVER, dev);
// 发送内核对象事件通知
kobject_uevent(&dev->kobj, KOBJ_BIND);
}

上述代码的作用是将驱动和设备进行绑定,首先,通过调用device_is_bound(dev) 检查设备是否已经绑定了驱动程序。如果设备已经绑定了驱动程序,则输出警告信息并返回。如果设备未绑定驱动程序,将输出绑定信息,其中包括驱动程序的名称、函数名和设备的名称。接下来,通过调用klist_add_tail() 将设备添加到驱动程序的设备链表中。这样,驱动程序可以通过遍历该链表来访问所有已绑定的设备。

然后,调用device_links_driver_bound() 更新设备的驱动程序链接状态。这个函数会确保设备和驱动程序之间的链接关系是正确的。

至此,加载驱动和加载设备先后顺序分析完毕。

第111 章platform 总线注册驱动流程实例分析实验

本章节我们来分析驱动是如何注册到platform 平台总线上的。在52 章中,我们编写了平台总线驱动。在驱动中使用platform_driver_register 函数进行平台驱动注册。

platform_driver_register 函数实现如下所示:

#define platform_driver_register(drv) __platform_driver_register(drv, THIS_MODULE)

__platform_driver_register 函数实现如下所示:

int __platform_driver_register(struct platform_driver *drv,struct module *owner)
{
    drv->driver.owner = owner;
    drv->driver.bus = &platform_bus_type;
    drv->driver.probe = platform_drv_probe;
    drv->driver.remove = platform_drv_remove;
    drv->driver.shutdown = platform_drv_shutdown;
    return driver_register(&drv->driver);
}

这是Linux 内核中用于注册平台驱动程序的函数__platform_driver_register 的代码片段。该函数用于完成平台驱动程序的注册,并将其与平台总线类型进行关联。
下面是对代码片段的解释:

  1. drv->driver.owner = owner; 此行将指定的owner 参数赋值给drv->driver.owner,表示驱动程序的所有者模块。
  2. drv->driver.bus = &platform_bus_type;此行将指向平台总线类型的指针&platform_bus_type 赋值给drv->driver.bus,将驱动程序与平台总线进行关联。
  3. drv->driver.probe = platform_drv_probe; 此行将指定的platform_drv_probe 函数赋值给drv->driver.probe,表示驱动程序的探测函数。
  4. drv->driver.remove = platform_drv_remove;此行将指定的platform_drv_remove 函数赋值给drv->driver.remove,表示驱动程序的移除函数。
  5. drv->driver.shutdown = platform_drv_shutdown;此行将指定的platform_drv_shutdown 函数赋值给drv->driver.shutdown,表示驱动程序的关机函数。
  6. return driver_register(&drv->driver);此行调用driver_register 函数,将驱动程序的struct driver结构体作为参数进行注册。driver_register 函数会将驱动程序添加到内核的驱动程序列表中,并进行相应的初始化。

driver_register 函数在之前的章节已经学习过了,所以接下来我们重点看看platform 总线的probe 函数是如何执行的。

static int platform_drv_probe(struct device *_dev)
{
    // 将传递给驱动程序的设备指针转换为platform_driver 结构体指针
    struct platform_driver *drv = to_platform_driver(_dev->driver);
    
    // 将传递给驱动程序的设备指针转换为platform_device 结构体指针
    struct platform_device *dev = to_platform_device(_dev);
    int ret;
    // 设置设备节点的默认时钟属性
    ret = of_clk_set_defaults(_dev->of_node, false);
    if (ret < 0)
        return ret;
    
    // 将设备附加到电源域
    ret = dev_pm_domain_attach(_dev, true);
    if (ret)
        goto out;
    
    // 调用驱动程序的探测函数(probe)
    if (drv->probe) {
        ret = drv->probe(dev);
        if (ret)
            dev_pm_domain_detach(_dev, true);
    }
    
out:
    // 处理探测延迟和错误情况
    if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
        dev_warn(_dev, "probe deferral not supported\n");
        ret = -ENXIO;
    }
    return ret;
}

该函数的主要逻辑如下:

  • 1 首先,将传递给驱动程序的设备指针_dev 转换为platform_driver 结构体指针drv,将传递给驱动程序的设备指针_dev 转换为platform_device 结构体指针dev。
  • 2 使用of_clk_set_defaults() 函数设置设备节点的默认时钟属性。这个函数会根据设备节点的属性信息配置设备的时钟。
  • 3 调用dev_pm_domain_attach() 将设备附加到电源域。这个函数会根据设备的电源管理需求,将设备与相应的电源域进行关联。
  • 4 如果驱动程序的probe 函数存在,调用它来执行设备的探测操作。drv->probe(dev) 表示调用驱动程序的probe 函数,并传递platform_device 结构体指针dev 作为参数。如果探测失败,会调用dev_pm_domain_detach() 分离设备的电源域。
  • 5 处理探测延迟和错误情况。如果驱动程序设置了prevent_deferred_probe 标志,并且返回值为-EPROBE_DEFER , 则表示探测被延迟。在这种情况下如果驱动程序设置了prevent_deferred_probe 标志,并且返回值为-EPROBE_DEFER,则表示探测被延迟。在这种情况下,代码会打印一个警告信息probe deferral not supported,并将返回值设置为-ENXIO,表示设备不存在。

总体而言,该函数的作用是执行平台驱动程序的探测操作,在设备上调用驱动程序的probe 函数,并处理探测延迟和错误情况。

到此,platform 总线注册驱动实验分析完毕。

第十篇热插拔

第112 章热插拔简介

112.1 什么是热插拔

热插拔是指在设备运行的情况下,能够安全地插入或拔出硬件设备,而无需关闭或重启系统。这意味着你可以在计算机或其他电子设备上插入或拔出硬件组件(比如USB 设备,扩展卡,硬件驱动器等),而无需关机或中断正在进行的操作。

热插拔的主要目的是提供方便性和灵活性。通过热插拔,你可以快速更换或添加硬件设备,而无需停止正在进行的任务。这在许多场景下非常有用,比如

  • 1 USB 设备:你可以随时插入或拔出USB 设备,比如鼠标,键盘,打印机,存储设备等,而无需重新启动系统。
  • 2 硬盘驱动器:在某些服务器或存储系统中,你可以在运行时添加或替换硬盘驱动器,以扩展存储容量或替换故障驱动器。
  • 3 扩展卡:你可以在计算机上插入或拔出显卡,网卡或声卡等扩展卡,以满足不同的需求或升级硬件性能。

为了支持热插拔功能,硬件设备和系统必须具备相应的支持。硬件方面,设备接口必须设计成可以插入和拔出而不会损坏设备或系统。系统需要提供相应的驱动程序和管理功能,以便在插入和拔出设备时进行正确的配置和识别。

112.2 热插拔的机制

热插拔是内核和用户空间之间,通过调用用户空间程序(如hotplug、udev 和mdev)的交互。当需要通知用户内核发生了某种热插拔事件时,内核才调用这个用户空间程序来实现交互。

在Linux 内核中,热插拔机制支持USB 设备、PCI 设备甚至CPU 等部件的动态插入和拔出。这个机制实现了底层硬件、内核空间和用户空间程序之间的连接,并且一直在不断演变和改进。设备文件系统是用来管理设备文件的一种机制,在Linux 中有三种常见的设备文件系统:devfs、mdev 和udev

  • **devfs:**devfs 是基于内核的动态设备文件系统,最早出现在Linux 2.3.46 内核中。它通过动态创建和管理设备节点的方式来处理设备文件。然而,devfs 存在一些限制和性能问题,从Linux 2.6.13 版本开始被移除。
  • **mdev:**mdev 是一个轻量级的热插拔设备文件系统,通常用于嵌入式Linux 系统。它是udev的简化版本,使用uevent_helper 机制来处理设备的插入和拔出事件。mdev 在设备插入时调用相应的用户程序来创建设备节点。
  • **udev:**udev 是目前在PC 机上广泛使用的热插拔设备文件系统。它基于netlink 机制,监听内核发送的uevent 来处理设备的插入和拔出。udev 能够动态创建和管理设备节点,并在设备插入时加载适当的驱动程序。它提供了丰富的配置选项,使用户能够灵活地管理设备文件。

这些设备文件系统在Linux 中扮演着重要的角色,它们负责管理设备文件,使得用户空间程序可以方便地与底层硬件进行交互。udev 是目前应用最广泛的设备文件系统,而mdev 主要用于嵌入式系统中,提供了轻量级的设备管理功能。

第113 章内核是如何发送事件到用户空间

113.1 相关接口函数

kobject_uevent 是Linux 内核中的一个函数,用于生成和发送uevent 事件。它是udev 和其他设备管理工具与内核通信的一种方式。
kobject_uevent 函数的原型如下所示:

参数说明:
kobj : 要发送uevent 事件的内核对象(kobject)
action: 表示触发uevent 的动作,可以是设备的插入,拔出,属性变化等。以下是一些常见的action 参数值。这些动作类型用于描述设备发生的不同事件,通过将相应的动作类型作为action 参数传递给kobject_uevent 函数,可以触发相应的uevent 事件,通知用户空间的udev进行相应的操作。

  • KOBJ_ADD:表示设备的添加或插入操作,表示添加一个对象到内核对象系统中。
  • KOBJ_REMOVE:表示设备的移除或拔出操作,表示从内核对象系统中删除一个对象。
  • KOBJ_CHANGE:表示设备属性的修改操作,表示对内核对象进行更改,例如属性修改等。
  • KOBJ_MOVE:表示设备的移动操作,即设备从一个位置移动到另一个位置。
  • KOBJ_ONLINE:表示设备的上线操作,即设备从离线状态变为在线状态,使其可以被访问。
  • KOBJ_OFFLINE:表示设备的离线操作,即设备从在线状态变为离线状态,使其不可以被访问。
  • KOBJ_BIND:表示将一个设备连接到内核对象上
  • KOBJ_UNBIND: 表示从内核对象上将一个设备解绑
  • KOBJ_MAX:表示枚举类型的最大值,通常用于表示没有任何操作行为。

kobject_uevent 函数的主要作用是在内核中生成uevent 事件,并通过netlink 机制将该事件发送给用户空间的udev。在调用该函数时,内核会将相关的设备信息和事件类型封装为uevent消息,并通过netlink 套接字将消息发送给用户空间。

用户空间的udev 会接收到这些uevent 消息,并根据消息中的设备信息和事件类型来执行相应的操作,例如创建或删除设备节点,加载或卸载驱动程序等。

113.2 udevadm 命令

udevadm 是一个用于与udev 设备管理器进行交互的命令行工具。它提供了一系列的子命令,用于查询和管理设备、触发uevent 事件以及执行其他与udev 相关的操作。一些常见的udevadm 子命令及其功能如下:

  • 1 udevadm info:用于获取设备的详细信息,包括设备路径、属性、驱动程序等。
  • 2 udevadm monitor:用于监视和显示当前系统中的uevent 事件。它会实时显示设备的插入、拔出以及其他相关事件。
  • 3 udevadm trigger:用于手动触发设备的uevent 事件。可以使用该命令模拟设备的插入、拔出等操作,以便触发相应的事件处理。
  • 4 udevadm settle:用于等待udev 处理所有已排队的uevent 事件。它会阻塞直到udev 完成当前所有的设备处理操作。
  • 5 udevadm control:用于与udev 守护进程进行交互,控制其行为。例如,可以使用该命令重新加载udev 规则、设置日志级别等。
  • 6 udevadm test:用于测试udev 规则的匹配和执行过程。可以通过该命令测试特定设备是否能够正确触发相应的规则。

这些是一些常见的udevadm 子命令,它们提供了与udev 设备管理器交互的便捷方式,用于设备信息查询、事件监控、事件触发、规则测试等操作。通过使用udevadm 工具,用户可以更好地理解和管理Linux 系统中的设备和udev 机制。

在iTOP-RK3568 开发板上烧写buildroot 系统,输入“udevadm monitor &”可以监视和显示当前系统中的uevent 事件,在之后的实验中,我们要使用这个方法。如下(图113-1)所示:

image-20240912113438585

113.3 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\78_uevent\module
编写完成的uevent.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

struct kobject *mykobject01;
struct kset *mykset;
struct kobj_type mytype;

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 创建并添加一个kset
    mykset = kset_create_and_add("mykset", NULL, NULL);

    // 分配并初始化一个kobject
    mykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    mykobject01->kset = mykset;

    // 初始化并添加kobject到kset
    ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");

    // 触发一个uevent事件,表示kobject的属性发生了变化
    ret = kobject_uevent(mykobject01, KOBJ_CHANGE);

    return 0;
}

// 模块退出函数
static void mykobj_exit(void)
{
    // 释放kobject
    kobject_put(mykobject01);
    kset_unregister(mykset);
}

module_init(mykobj_init);  // 指定模块的初始化函数
module_exit(mykobj_exit);  // 指定模块的退出函数

MODULE_LICENSE("GPL");    // 模块使用的许可证
MODULE_AUTHOR("topeet");  // 模块的作者

113.4 运行测试

113.4.1 编译驱动程序

在上一小节中的uevent.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成uevent.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

113.3.2 运行测试

开发板启动之后,使用命令“udevadm monitor &”监视和显示当前系统中的uevent 事件。然后使用以下命令进行驱动模块的加载,如下图(图113-5)所示:

insmod uevent.ko

image-20240912114615760

image-20240912114624496

驱动加载之后,如上图所示udev 接收到change 动作,说明uevent 事件已经发送成功了。/mykset/mykobject01 是kobject 在根目录/sys/下的路径。
最后可以使用以下命令进行驱动的卸载,如下图(图113-7)所示:

rmmod uevent

image-20240912114758787

至此,发送事件到用户空间实验就完成了。

第114 章内核发送事件到用户空间的方法

在前一章节中,我们成功地通过uevent 机制将内核事件发送到用户空间。然而,本章节我们尝试在实验中去掉了创建kset 的步骤,并发现用户空间无法收到事件。在分析这个问题之前,让我们回顾一下前一章节中的实验。

114.1 实验现象

在前一章节的代码示例中,我们演示了如何在内核中创建kset 和kobject,并将其添加到sysfs 文件系统中。这样做的目的是为了在sysfs 中创建相应的目录结构,以便用户空间能够找到正确的sysfs 路径来监听事件或访问设备属性。然而,当我们去掉了创建kset 的步骤后,用户空间无法收到事件。这是因为没有正确的sysfs 路径供用户空间监听事件。

上个章节的驱动代码修改如下图(图114-1)所示:

image-20240912115216594

编译之后,加载驱动程序,如下图(图114-2)所示:

image-20240912115241329

为了解决这个问题,我们需要在代码层面确保正确创建kset 和kobject,并将其添加到sysfs文件系统中。这样,用户空间才能够通过sysfs 路径找到相应的事件或属性文件。在接下来的部分,我们将通过代码分析来解释为什么没有创建kset 会导致用户空间无法接收事件。

114.2 代码分析

在上一小节的实验中,为什么没有创建kset 会导致用户空间无法接收事件呢?要搞清楚这个问题,我们可以追踪下kobject_uevent 函数。如下所示:

/**
* kobject_uevent - notify userspace by sending an uevent
*
* @kobj: struct kobject that the action is happening to
* @action: action that is happening
*
* Returns 0 if kobject_uevent() is completed with success or the
* corresponding error when it fails.
*/
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
    return kobject_uevent_env(kobj, action, NULL);
}
EXPORT_SYMBOL_GPL(kobject_uevent);

在上述代码中,可以看出kobject_uevent()函数是用于通知用户空间的,它通过发送uevent来实现。该函数接受俩个参数:

  • kobject 表示发生操作的struct kobject 对象,
  • action 表示正在发生的操作。在这个函数中调用了kobject_uevent_env()函数,并将其返回值作为自己的返回值。这说明kobject_uevent()函数实际上是一个简化的接口,它将kobject_uevent_env()函数的调用参数设置为NULL,并返回相同的结果。

kobject_uevent_env 函数代码比较多,接下来我们拆分为几个部分给大家讲解下。

第一部分,如下所示:

  • ```c
    /**

    • kobject_uevent_env - send an uevent with environmental data //发送带有环境变量数据的事件
    • @kobj: struct kobject that the action is happening to //正在发生动作的对象
    • @action: action that is happening //正在发生的动作
    • @envp_ext: pointer to environmental data //环境变量的指针
    • Returns 0 if kobject_uevent_env() is completed with success or the
    • corresponding error when it fails.
      */
      
      
      
      第二部分如下所示:
      
      ```c
      int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
      char *envp_ext[])
      {
          struct kobj_uevent_env *env; //指向kobj_uevent_env 结构体的指针,用于存储发送的事件和环境变量
          const char *action_string = kobject_actions[action]; //事件的类型
          const char *devpath = NULL;//存放kobject 的路径
          const char *subsystem; //存放所属子系统的名称
          struct kobject *top_kobj; //指向顶层top_kobj 的kobject 指针
          struct kset *kset; //指向kset 的指针,表示kobject 所属的kset
          const struct kset_uevent_ops *uevent_ops; //指向struct kset_uevent_ops 结构体的指针
          int i = 0; //计数器i,用来编译环境变量数组
          int retval = 0 //表示函数的执行结果,也就是返回值

    第三部分如下所示:

    if (action == KOBJ_REMOVE)
        kobj->state_remove_uevent_sent = 1;
    pr_debug("kobject: '%s' (%p): %s\n",kobject_name(kobj), kobj, __func__);
    
    /* search the kset we belong to */
    top_kobj = kobj;
    while (!top_kobj->kset && top_kobj->parent)
        top_kobj = top_kobj->parent;
    
    if (!top_kobj->kset) {
        pr_debug("kobject: '%s' (%p): %s: attempted to send uevent without kset!\n", 
                 kobject_name(kobj), kobj,__func__);
        return -EINVAL;
    }
    kset = top_kobj->kset;
    uevent_ops = kset->uevent_ops;

    以上代码中, 首先检查action 是否为KOBJ_REMOVE , 如果是, 则设置kobj->state_remove_uevent_sent 为1,表示“remove”事件已发送。

    接下来通过调用pr_debug()函数输出调试信息,显示相关的kobject 名称和指针以及当前函数名称。

    然后,通过循环查找kobj 所属的kset,直到找到具体有效kset 的顶层kobj,即kset 的根节点。这是通过沿着kobj 的父对象链向上遍历实现的,如果找不到有效的kset,则输出调试信息,并返回-EINVAL 表示发送uevent 失败。

    这个过程的目的是为了确定当前kobj 所属的kset,以便后面的uevent 发送能够正确地通知用户空间的应用程序。因为uevent 是通过netlink socket 发送给用户空间的应用程序的,而netlink socket 是基于kset 的。

    最后,将找到的kset 赋值给变量kset,并将其uevent_ops 字段赋值给变量uevent_ops,以便后续使用。

    第四部分如下所示:

/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {
    pr_debug("kobject: '%s' (%p): %s: uevent_suppress caused the event to drop!\n",
             kobject_name(kobj), kobj, __func__);
    return 0;
}

在上面的代码中, 首先检查kobj->uevent_suppress 是否为1 , 如果设置kobj->uevent_suppress,则输出调试信息表示该事件被跳过,并返回0 表示成功。

第五部分如下所示:

/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {
    pr_debug("kobject: '%s' (%p): %s: uevent_suppress caused the event to drop!\n",
             kobject_name(kobj), kobj, __func__);
    return 0;
}

/* skip the event, if the filter returns zero. */
if (uevent_ops && uevent_ops->filter)
    if (!uevent_ops->filter(kset, kobj)) {
        pr_debug("kobject: '%s' (%p): %s: filter function caused the event to drop!\n",
                 kobject_name(kobj), kobj, __func__);
        return 0;
    }

/* originating subsystem */
if (uevent_ops && uevent_ops->name)
    subsystem = uevent_ops->name(kset, kobj);
else
    subsystem = kobject_name(&kset->kobj);

if (!subsystem) {
    pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the event to drop!\n", 
             kobject_name(kobj), kobj,__func__);
    return 0;
}

在上面的代码中,检查uevent_ops 是否存在,并且uevent_ops->filter 是否存在。如果两者都存在,并且调用uevent_ops->filter(kset, kobj),返回值为0,则表示过滤器函数导致该事件被跳过。输出相应的调试信息,并返回0 表示成功。

然后,根据uevent_ops 中的name 字段获取原始子系统的名称。如果uevent_ops->name 存在,则调用uevent_ops->name(kset, kobj)函数获取子系统名称,否则使用kset 的名称作为子系统名称。如果无法获取子系统名称,输出调试信息,并返回0 表示成功。

综上所述,这部分代码主要用于检查是否应该跳过发送uevent 的条件,如uevent_suppress标志、过滤器函数的返回结果以及获取原始子系统名称。如果满足这些条件,则该事件将被跳过,并返回0 表示成功。

第六部分,如下所示:

if (IS_ENABLED(CONFIG_ARCH_ROCKCHIP) &&IS_ENABLED(CONFIG_FREEZER) &&IS_ENABLED(CONFIG_ANDROID)) {
    /*
    * Android healthd try to listen power_supply subsystem uevent,
    * but which will block system from suspend on big.LITTLE system
    * because thermal_cooling_device_unregister will called when
    * cpufreq_exit. So ignore this uevent when suspend.
    */
    extern bool pm_freezing;
    if (pm_freezing && !strcmp(subsystem, "thermal"))
        return 0;
}

上面的代码是一个条件判断,用于特定条件下跳过发送uevent 的操作,它首先使用IS_ENABLED()宏进行编译时配置的条件判断,检查是否启用了CONFIG_ARCH_ROCKCHIP、CONFIG_FREEZER 和CONFIG_ANDROID 配置选项。如果这三个选项都启用了,那么代码将进入条件块部分。内部的注释解释了为什么需要跳过发送uevent 的操作。在big.LITTLE 系统上,当cpufreq_exit 函数被调用时,thermal_cooling_device_unregister 函数也会被调用,这可能会导致系统挂起失败,为了避免这种情况,该函数忽略了thermal 子系统的uevent 事件。

代码中使用了一个外部变量pm_freezing,如果pm_freezing 为真(即系统正在进入挂起状态),并且子系统名称与”thermal”相同时,将返回0 表示跳过发送uevent 的操作。

第七部分,如下所示:

/* environment buffer */
env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
if (!env)
    return -ENOMEM;

/* complete object path */
devpath = kobject_get_path(kobj, GFP_KERNEL);
if (!devpath) {
    retval = -ENOENT;
    goto exit;
}

/* default keys */
retval = add_uevent_var(env, "ACTION=%s", action_string);
if (retval)
    goto exit;
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
if (retval)
    goto exit;
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
if (retval)
    goto exit;

/* keys passed in from the caller */
if (envp_ext) {
    for (i = 0; envp_ext[i]; i++) {
        retval = add_uevent_var(env, "%s", envp_ext[i]);
        if (retval)
            goto exit;
    }
}

/* let the kset specific function add its stuff */
if (uevent_ops && uevent_ops->uevent) {
    retval = uevent_ops->uevent(kset, kobj, env);
    if (retval) {
        pr_debug("kobject: '%s' (%p): %s: uevent() returned %d\n", kobject_name(kobj), kobj,
                 __func__, retval);
        goto exit;
    }
}

上面代码中,首先,它分配了一个kobj_uevent_env 结构体的内存空间,并检查是否成功分配。然后,它获取了kobj 对象所在的路径,并检查是否成功获取。接下来,它添加了一些默认的键值对到环境变量中,包括ACTION、DEVPATH 和SUBSYSTEM。最后,如果有额外的键值对传入,则将其也添加到环境变量中。循环的终止条件是envp_ext[i] 为空指针。

接下来它首先检查uevent_ops 和uevent_ops->uevent 是否存在。如果存在,则调用uevent_ops->uevent 函数,并传递kset、kobj 和env 作为参数。如果uevent_ops->uevent 返回非零值(表示出错),则会打印一条带有错误信息的调试消息,然后跳转到标签exit。

第八部分,如下所示:

switch (action) {
    case KOBJ_ADD:
        /*
        * Mark "add" event so we can make sure we deliver "remove"
        * event to userspace during automatic cleanup. If
        * the object did send an "add" event, "remove" will
        * automatically generated by the core, if not already done
        * by the caller.
        */
        kobj->state_add_uevent_sent = 1;
        break;
        
    case KOBJ_UNBIND:
        zap_modalias_env(env);
        break;
        
    default:
        break;
}

在上面的代码中,当action 为KOBJ_ADD 时,将kobject 对象的state_add_uevent_sent 成员设置为1,表示已经发送了“add”事件,以便在自动清理期间将“remove”事件传递给用户空间。如果该对象已经发送了“add”事件,内核会自动发送“remove”事件,否则需要调用者手动发送“remove”事件。

当action 为KOBJ_UNBIND 时,调用zap_modalias_env 函数清除环境变量中MODALIAS 变量。MODALIAS 是一个特殊的环境变量,用于描述设备的模块别名。在设备驱动程序加载时,内核会根据MODALIAS 环境变量自动加载相应的驱动程序模块。在设备卸载时,需要清除MODALIAS 环境变量,以便下次重新加载设备驱动程序时能够正确地识别设备。

第九部分,如下所示:

retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
if (retval) {
    mutex_unlock(&uevent_sock_mutex);
    goto exit;
}
retval = kobject_uevent_net_broadcast(kobj, env, action_string,devpath);

上述代码将一个名为”SEQNUM” 的环境变量添加到uevent 环境变量列表中,并将其值设置为uevent_seqnum 的值加1。其中,add_uevent_var 是一个内部函数,用于将一个键值对添加到uevent 环境变量列表中。如果添加失败,函数将返回一个非零值,同时会释放uevent_sock_mutex 互斥锁并跳转到exit 标签处进行清理操作。这个函数的主要作用是为uevent 事件添加一个唯一的序列号,以便在处理uevent 事件时可以识别它们的顺序。通俗的话讲就是每次发送一个事件,都要有它的事件号,该事件号不能重复,也会被加到环境变量里面。

kobject_uevent_net_broadcast 是一个内核函数,用于将一个uevent 事件发送到系统中所有的网络命名空间中。它的参数包括kobj,env,action_string 和devpath。其中,kobj 是与uevent 事件相关的内核对象,env 是一个包含uevent 环境变量的列表,action_string 是一个字符串,表示uevent 事件的类型,devpath 是一个字符串,表示与uevent 事件相关的设备
路径。该函数会遍历系统中所有的网络命名空间,并将uevent 事件发送到每个网络命名空间中。这个函数的主要作用是在内核中广播一个uevent 事件,以便用户空间的应用程序可以接收并处理这些事件。

第十部分,如下所示:

#ifdef CONFIG_UEVENT_HELPER
    /* call uevent_helper, usually only enabled during early boot */
    if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
        struct subprocess_info *info;
        
        retval = add_uevent_var(env, "HOME=/");
        if (retval)
            goto exit;
        
        retval = add_uevent_var(env,"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
        if (retval)
            goto exit;
        
        retval = init_uevent_argv(env, subsystem);
        if (retval)
            goto exit;
        
        retval = -ENOMEM;
        info = call_usermodehelper_setup(env->argv[0], env->argv,env->envp, GFP_KERNEL,
                                         NULL, cleanup_uevent_env, env);
        if (info) {
            retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
            env = NULL; /* freed by cleanup_uevent_env */
        }
    }
#endif

在内核中调用用户空间的uevent_helper 程序来处理uevent 事件。uevent_helper 是一个用户空间程序, 它可以在内核空间生成uevent 事件时被调用。如果CONFIG_UEVENT_HELPER 宏被定义, 那么内核会在生成uevent 事件时调用uevent_helper 程序,以便在用户空间中处理这些事件。在上述代码中,如果uevent_helper 变量不为空且kobj_usermode_filter 函数返回false,那么就会调用call_usermodehelper_setup 函数来启动一个用户空间进程,并将env 中的参数传递给该进程。在这个过程中,env 中的参数将会被转换成环境变量,并被传递给用户空间进程。\

以上通过分析kobject_uevent_env()函数,了解到此函数用于在内核中生成并发送uevent事件到用户空间。如果没有创建kset,会导致用户空间无法接收事件,可以详细见代码分析的第三部分。

第115 章完善kset_uevent_ops 结构体实验

在前面的章节中,我们了解了Linux 内核是如何发送uevent 事件到用户空间的。通过分析代码,我们知道在内核中,通过kset 数据结构来组织kobject,并通过kset_uevent_ops 结构体来定义uevent 事件的处理函数。这些函数可以在接收到特定事件时执行自定义的操作。

为了完善这个机制,我们需要对kset_uevent_ops 结构体进行实验和定制。通过填充这个结构体中的字段,我们可以定义自己的事件处理函数,从而响应特定的事件。让我们开始做实验吧!

115.1 实验程序的编写

115.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\79_uevent_ops\module。

我们编写驱动程序,首先,需要创建一个自定义的kset 对象,并将其与相应的kobject 关联起来。然后,我们可以定义一个包含所需事件处理逻辑的函数,并将其赋值给uevent 字段,这样当事件发生时,内核就会调用我们定义的处理函数。在处理函数中,可以根据特定的事件类型执行我们想要的操作,例如读取设备信息、处理设备状态变化等。我们还可以使用add_uevent_var 函数向uevent 环境中添加自定义的键值对,以提供额外的信息给用户空间。

通过这种方式,我们可以定制内核中的uevent 事件处理机制,使其适应特定需求。这种定制化的事件处理机制可以用于各种场景,例如设备驱动程序、模块加载和卸载等。

总结起来,通过实验和完善kset_uevent_ops结构体,我们可以扩展内核的uevent 事件处理能力,使其更加灵活和适应各种应用场景。这为我们提供了在内核和用户空间之间进行通信和交互的强大工具。编写完成的uevent_ops.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

struct kobject *mykobject01;
struct kobject *mykobject02;
struct kset *mykset;
struct kobj_type mytype;

// 定义一个回调函数,返回kset的名称
const char *myname(struct kset *kset, struct kobject *kobj)
{
    return "my_kset";
};

// 定义一个回调函数,处理kset的uevent事件
int myevent(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env)
{
    add_uevent_var(env, "MYDEVICE=%s", "TOPEET");
    return 0;
};

// 定义一个回调函数,用于过滤kset中的kobject
int myfilter(struct kset *kset, struct kobject *kobj)
{
    if (strcmp(kobj->name, "mykobject01") == 0){
        return 0; // 返回0表示通过过滤
    }else{
        return 1; // 返回1表示过滤掉
    }
};

struct kset_uevent_ops my_uevent_ops = {
    .filter = myfilter,
    .uevent = myevent,
    .name = myname,
};

// 模块的初始化函数
static int mykobj_init(void)
{
    int ret;

    // 创建并添加一个kset
    mykset = kset_create_and_add("mykset", &my_uevent_ops, NULL);

    // 分配并初始化一个kobject
    mykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    mykobject01->kset = mykset;

    // 初始化并添加kobject到kset
    ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");

    // 分配并初始化一个kobject
    mykobject02 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
    mykobject02->kset = mykset;

    // 初始化并添加kobject到kset
    ret = kobject_init_and_add(mykobject02, &mytype, NULL, "%s", "mykobject02");

    // 触发一个uevent事件,表示mykobject01的属性发生了变化
    ret = kobject_uevent(mykobject01, KOBJ_CHANGE);
    // 触发一个uevent事件,表示mykobject02被添加
    ret = kobject_uevent(mykobject02, KOBJ_ADD);

    return 0;
}

// 模块退出函数
static void mykobj_exit(void)
{
    // 释放kobject
    kobject_put(mykobject01);
    kobject_put(mykobject02);
    kset_unregister(mykset);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

115.2 运行测试

115.2.1 编译驱动程序

在上一小节中的uevent_ops.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成uevent_ops.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

115.2.2 运行测试

开发板启动之后,使用命令“udevadm monitor &”监视和显示当前系统中的uevent 事件。然后使用以下命令进行驱动模块的加载,如下图(图115-4)所示:

insmod uevent_ops.ko

image-20240912142919680

image-20240912142925382

驱动加载之后,如上图所示udev 接收到add 动作,说明uevent 事件已经发送成功了。/mykset/mykobject02 是kobject 在根目录/sys/下的路径。
最后可以使用以下命令进行驱动的卸载,如下图(图115-6)所示:

rmmod uevent_ops

image-20240912143027282

至此,完善kset_uevent_ops 实验就完成了。

在上一章的实验中,我们填充了三个回调函数,分别为.filter = myfilter, .uevent =myevent, .name = myname,但在最后的实验中只验证filter 过滤了kset 中的kobject1,那另外两个回调函数要如何验证呢?

内核通过kobject uevent 接口发送广播事件之后,用户空间可以通过netlink 来监听这些广播信息。通过监听广播信息,就可以获取到携带环境变量的事件,在本章节将会对netlink 进行详细的讲解。

Netlink 是Linux 内核中用于内核和用户空间之间进行双工通信的机制。它基于socket 通信机制,并提供了一种可靠的、异步的、多播的、有序的通信方式。

Netlink 机制的主要特点包括:

  • (1)双工通信:Netlink 允许内核和用户空间之间进行双向通信,使得内核可以向用户空间发送消息,同时也可以接收来自用户空间的消息。
  • (2)可靠性:Netlink 提供了可靠的消息传递机制,保证消息的完整性和可靠性。它使用了确认和重传机制,以确保消息的可靠传输。
  • (3)异步通信:Netlink 支持异步通信,即内核和用户空间可以独立地发送和接收消息,无需同步等待对方的响应。
  • (4)多播支持:Netlink 允许向多个进程或套接字广播消息,以实现一对多的通信。
  • (5)有序传输:Netlink 保证消息的有序传输,即发送的消息按照发送的顺序在接收端按序接收。

Netlink 的应用广泛,常见的应用包括:

  • (1)系统管理工具:如ifconfig、ip 等工具使用Netlink 与内核通信来获取和配置网络接口的信息。
  • (2)进程间通信:进程可以使用Netlink 进行跨进程通信,实现进程间的数据交换和协调。
  • (3)内核模块和用户空间应用程序的通信:内核模块可以通过Netlink 向用户空间应用程序发送通知或接收用户空间应用程序的指令。

116.2.1 创建socket

Linux socket 编程中,创建套接字是构建网络应用程序的第一步。套接字可以理解为应用程序和网络之间的桥梁,用于在网络上进行数据的收发和处理。该系统调用的原型和所需头文件如下所示:

所需头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int socket(int domain, int type, int protocol);

其中,domain 参数指定了套接字的协议族,type 参数指定了套接字的类型,protocol 参数指定了套接字所使用的具体协议。下面分别介绍这三个参数的含义:

(1)协议族
协议族指定了套接字所使用的协议类型,常用的协议族包括AF_INETAF_INET6AF_UNIX等。其中,AF_INET 表示IPv4 协议族,AF_INET6 表示IPv6 协议族,AF_UNIX 表示Unix 域协议族,这里的协议族为netlink,所以该参数要在程序中设置为AF_ NETLINK。

(2)套接字类型
套接字类型指定了套接字的数据传输方式,常用的套接字类型包括SOCK_STREAMSOCK_DGRAMSOCK_RAW 等。其中,SOCK_STREAM 表示面向连接的流套接字,主要用于可靠传输数据,例如TCP 协议SOCK_DGRAM 表示无连接的数据报套接字,主要用于不可靠传输数据,例如UDP 协议。在本实验中该参数要设置为SOCK_RAW 表示原始套接字,可以直接访问底层网络协议。

(3)协议类型
协议类型指定了套接字所使用的具体协议类型,常用的协议类型包括IPPROTO_TCPIPPROTO_UDPIPPROTO_ICMP 等。其中,IPPROTO_TCP 表示TCP 协议,IPPROTO_UDP 表示UDP协议,IPPROTO_ICMP 表示ICMP 协议,在本实验中,我们要设置为NETLINK_KOBJECT_UEVENT

在本小节中将使用以下代码创建一个新的套接字:

int socket_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
  • AF_NETLINK:指定了使用Netlink 协议族。Netlink 协议族是一种Linux 特定的协议族,用于内核和用户空间之间的通信。
  • SOCK_RAW:指定了创建原始套接字,这种套接字类型可以直接访问底层协议,而不需要进行协议栈处理。在这种情况下,我们可以直接使用Netlink 协议进行通信。
  • NETLINK_KOBJECT_UEVENT:指定了Netlink 协议的一种类型,即kobject uevent 类型。kobject uevent 用于内核对象相关的事件通知,当内核中的kobject 对象发生变化时,会通过此类型的Netlink 消息通知用户空间。

116.2.2 绑定套接字

创建套接字后,需要将其与一个网络地址绑定,以便其他计算机可以访问该套接字。在Linux系统下,可以使用bind()系统调用绑定套接字和地址。该系统调用的原型和所需头文件如下所示:

所需头文件
#include <sys/types.h>
#include <sys/socket.h>
    
函数原型
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

(1) sockfd 参数指定了需要绑定的套接字描述符,
(2) addr 参数指定了需要绑定的地址信息,这里使用sockaddr_nl 结构体,sockaddr_nl 结构体的定义如下:

struct sockaddr_nl {
    sa_family_t nl_family; // AF_NETLINK
    unsigned short nl_pad; // zero
    uint32_t nl_pid; // port ID
    uint32_t nl_groups; // multicast groups mask
};
  • nl_family:表示地址族,此处固定为AF_NETLINK,指示使用Netlink 协议族。
  • nl_pad:填充字段,设置为0。在结构体中进行字节对齐时使用。
  • nl_pid:端口ID,表示进程的标识符。可以将其设置为当前进程的PID,也可以设为0,表示不加入任何多播组。
  • nl_groups:多播组掩码,用于指定感兴趣的多播组。当设置为1 时,表示用户空间进程只会接收内核事件的基本组的内核事件。这意味着,用户空间进程将只接收到属于基本组的内核事件,而不会接收其他多播组的事件。

(3) addrlen 参数:addrlen 参数是一个整数,指定了addr 所指向的结构体对应的字节长度。它用于确保正确解析传递给addr 参数的结构体的大小。

具体编程示例如下所示:

struct sockaddr_nl *nl; // 定义一个指向struct sockaddr_nl 结构体的指针nl
bzero(nl, sizeof(struct sockaddr_nl)); // 将nl 指向的内存区域清零,确保结构体的字段初始化为0
nl->nl_family = AF_NETLINK; // 设置nl 结构体的nl_family 字段为AF_NETLINK,指定地址族为Netlink
nl->nl_pid = 0; // 设置nl 结构体的nl_pid 字段为0,表示目标进程ID 为0,即广播给所有进程
nl->nl_groups = 1; // 设置nl 结构体的nl_groups 字段为1,表示只接收基本组的内核事件
ret = bind(socket_fd, (struct sockaddr *)nl, sizeof(struct sockaddr_nl)); //使用bind 函数将socket_fd 套接字与nl 地址结构体绑定在一起
if (ret < 0) {
    printf("bind error\n");
    return -1;
}

116.2.3 接收数据

Netlink 套接字在接收数据时不需要调用listen 函数,而是可以直接使用recv 函数进行接收。下面是recv 函数的相关说明:

头文件:
    #include <sys/types.h>
    #include <sys/socket.h>
函数原型:
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
函数参数:
    sockfd:指定套接字描述符,即要接收数据的Netlink 套接字。
    buf:指向数据接收缓冲区的指针,用于存储接收到的数据。
    len:指定要读取的数据的字节大小。
    flags:指定一些标志,用于控制数据的接收方式。通常情况下,可以将其设置为0。
返回值:
    成功情况下,返回实际读取到的字节数。
    如果返回值为0,表示对方已经关闭了连接。
    如果返回值为-1,表示发生了错误,可以通过查看errno 变量来获取具体的错误代码。

使用recv 函数可以从指定的Netlink 套接字中接收数据,并将其存储在提供的缓冲区中。函数的返回值表示实际读取到的字节数,可以根据返回值来判断是否成功接收到数据。

接收数据的具体代码示例如下所示:

while (1) {
    bzero(buf, 4096); // 将缓冲区buf 清零,确保数据接收前的初始化
    len = recv(socket_fd, &buf, 4096, 0); // 从socket_fd 套接字接收数据,存储到缓冲区buf 中,最大接收字节数为4096
    for (i = 0; i < len; i++) {
        if (*(buf + i) == '\0') { // 如果接收到的数据中有'\0' 字符,将其替换为'\n',以便在打印时换行显示
        	buf[i] = '\n';
        }
    }
    printf("%s\n", buf); // 打印接收到的数据
}

116.3 实验程序的编写

本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\80_netlink
根据上一小节所讲解的内容,使用netlink 监听广播信息的应用程序netlink.c.c 代码如下所示:

#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>

int main(int argc, char *argv[]) {
    int ret;
    struct sockaddr_nl *nl;  // 定义一个指向struct sockaddr_nl 结构体的指针nl
    int len = 0;
    char buf[4096] = {0};  // 数据接收缓冲区
    int i = 0;

    bzero(nl, sizeof(struct sockaddr_nl));  // 将nl 指向的内存区域清零,确保结构体的字段初始化为0
    nl->nl_family = AF_NETLINK;  // 设置nl 结构体的nl_family 字段为AF_NETLINK,指定地址族为Netlink
    nl->nl_pid = 0;  // 设置nl 结构体的nl_pid 字段为0,表示目标进程ID 为0,即广播给所有进程
    nl->nl_groups = 1;  // 设置nl 结构体的nl_groups 字段为1,表示只接收基本组的内核事件

    int socket_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);  // 创建一个Netlink套接字
    if (socket_fd < 0) {
        printf("socket error\n");
        return -1;
    }

    ret = bind(socket_fd, (struct sockaddr *)nl, sizeof(struct sockaddr_nl));  // 使用bind 函数将socket_fd 套接字与nl 地址结构体绑定在一起
    if (ret < 0) {
        printf("bind error\n");
        return -1;
    }

    while (1) {
        bzero(buf, 4096);  // 将缓冲区buf 清零,确保数据接收前的初始化
        len = recv(socket_fd, &buf, 4096, 0);  // 从socket_fd 套接字接收数据,存储到缓冲区buf 中,最大接收字节数为4096

        for (i = 0; i < len; i++) {
            if (*(buf + i) == '\0') {  // 如果接收到的数据中有'\0' 字符,将其替换为'\n',以便在打印时换行显示
                buf[i] = '\n';
            }
        }

        printf("%s\n", buf);  // 打印接收到的数据
    }

    return 0;
}

116.4 运行测试

116.4.1 编译应用程序

下面进行应用程序编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个netlink 的可执行程序,
如下图(图116-1)所示:

aarch64-linux-gnu-gcc -o netlink netlink.c

image-20240913102611838

下面进行程序的测试。

116.4.2 运行测试

本小节测试所使用的驱动文件为上一章编译生成的uevent_ops.ko,应用程序为上一小节编译出来的netlink。
开发板启动之后,首先使用以下命令让应用程序在后台运行,如下图(图116-2)所示:

./netlink &

image-20240913102638354

然后继续使用以下命令加载uevent_ops.ko 驱动,打印如下图(116-3)所示:

insmod uevent_ops.ko

image-20240913102656406

SUBSYSTEM=my_kset,表示设备或对象所属的子系统。在这里,子系统是”my_kset”。MYDEVICE 表示设备的名称或标识。在这里,设备的名称是”TOPEET”,正是我们在回调函数中所设置的。

最后可以使用以下命令进行驱动的卸载,如下图(图116-4)所示:

rmmod uevent_ops.ko

image-20240913102757411

至此,使用netlink 监听广播信息实验就完成了。

第117 章uevent_helper 实验

在前面的章节中已经讲解了广播这一内核发送事件到用户空间的方法,而在本章节中将会讲解内核发送事件到用户空间的第二种方法-调用可执行程序。

117.1 设置uevent_helper

在114.2 小节的第十部分中进行了定义,具体内容如下所示:

#ifdef CONFIG_UEVENT_HELPER
/* call uevent_helper, usually only enabled during early boot */
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
    struct subprocess_info *info;
    
    retval = add_uevent_var(env, "HOME=/");
    if (retval)
        goto exit;
    
    retval = add_uevent_var(env,"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
    if (retval)
        goto exit;
    
    retval = init_uevent_argv(env, subsystem);
    if (retval)
        goto exit;
    
    retval = -ENOMEM;
    info = call_usermodehelper_setup(env->argv[0], env->argv,env->envp, GFP_KERNEL,
                                     NULL, cleanup_uevent_env, env);
    if (info) {
        retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
        env = NULL; /* freed by cleanup_uevent_env */
    }
}
#endif

第3 行为一个if 表达式,它检查uevent_helper 数组的第一个元素是否为真。并调用kobj_usermode_filter 函数进行用户模式过滤, uevent_helper 定义如下所示:

char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;

其中CONFIG_UEVENT_HELPER_PATH 是一个宏定义在内核源码的“include/generated/autoconf.h”文件中,如下所示:

#define CONFIG_UEVENT_HELPER_PATH ""

该宏为空, 所以为了使能uevent_helper 功能需要在图形配置界面使能CONFIG_UEVENT_HELPERCONFIG_UEVENT_HELPER_PATH 两个宏。首先来到内核源码目录下,
如下图(图117-1)所示:

image-20240913103059263

然后输入以下命令将平台切换为arm64、加载rk3568 的默认配置文件和进入图像配置界面,如下图(图117- 2)所示:

image-20240913103114892

然后需要在menuconfig 的图形配置界面进行以下配置:

配置1:

Device Drivers
    Generic Driver Options
        [*] Support for uevent helper		//选中
        (/sbin/mdev) path to uevent helper	//设置mdev 路径

image-20240913103205334

配置2:

File systems
    Pseudo filesystems.
        [*]/proc file system support//选中

image-20240913103240758

配置3:

File systems
    Pseudo filesystems.
        [*] Sysctl support(/proc/sys)//选中

image-20240913103303788

配置4:

[*]Networking support		//选中

image-20240913103329022

在上面的配置1 中设置了uevent helper 和相对应的路径,这就是配置方法1,但是这种方式需要重新编译内核,使用起来较为麻烦,除了方法一之外还有更快捷的方法2 和方法3,具体内容如下所示:

配置方法2:
无论是否配置了CONFIG_UEVENT_HELPER_PATH,在系统启动后,可以使用以下命令来设置uevent_helper:

echo /sbin/mdev > /sys/kernel/uevent_helper

这将把uevent_helper 设置为/sbin/mdex。

配置方法3:
无论是否配置了CONFIG_UEVENT_HELPER_PATH,在系统启动后,可以使用以下命令来设置uevent_helper

echo /sbin/mdev > /proc/sys/kernel/hotplug

这将把uevent_helper 设置为/sbin/mdexw.。

需要注意的是配置方法2 和配置方法3 依赖于上面的配置2、3、4 选项,并且可以通过配置方法2 和配置方法3 修改配置方法1 中已经写好的值。

对/proc/sys/kernel/hotplug 和/sys/kernel/uevent_helper 进行读写都是为了对uevent_helper属性进行读写操作。

/sys/kernel/uevent_helper 是sysfs 文件系统中的一个文件,它是uevent_helper 属性的接口。通过对该文件进行读写操作,可以读取或修改uevent_helper 属性的值。在内核源码的kernel/ksysfs.c 目录下可以找到对uevent_helper 属性的定义和相关操作的实现,具体内容如下所示:

#ifdef CONFIG_UEVENT_HELPER
/* uevent helper program, used during early boot */
static ssize_t uevent_helper_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    return sprintf(buf, "%s\n", uevent_helper);
}
static ssize_t uevent_helper_store(struct kobject *kobj, struct kobj_attribute *attr,
                                   const char *buf, size_t count)
{
    if (count + 1 > UEVENT_HELPER_PATH_LEN)
        return -ENOENT;
    memcpy(uevent_helper, buf, count);
    uevent_helper[count] = '\0';
    if (count && uevent_helper[count - 1] == '\n')
        uevent_helper[count - 1] = '\0';
    return count;
}
KERNEL_ATTR_RW(uevent_helper);
#endif

uevent_helper_show 函数用于将uevent_helper 的值写入buf 中,并返回写入的字符数。
uevent_helper_store 函数用于将buf 中的值复制到uevent_helper 中,并根据需要进行处理,然后返回写入的字符数。
/proc/sys/kernel/hotplug 是一个虚拟文件,用于配置内核中的热插拔事件处理程序。通过对该文件进行写操作,可以设置uevent_helper 属性的值。在内核源码的kernel/sysctl.c 文件中,可以看到对hotplug 操作其实是对uevent_helper 进行操作。具体内容如下所示:

#ifdef CONFIG_UEVENT_HELPER
{
    .procname = "hotplug",
    .data = &uevent_helper,
    .maxlen = UEVENT_HELPER_PATH_LEN,
    .mode = 0644,
    .proc_handler = proc_dostring,
}

这段代码定义了一个名为hotplug 的文件,用于处理uevent 事件。它与uevent_helper 属性相关联。

  • .procname 表示文件名,即/proc/hotplug
  • .data 是一个指向uevent_helper 结构体的指针,用于保存与该文件相关的数据。该指针指向uevent_helper 结构体,用于处理uevent 事件。
  • .maxlen 表示文件的最大长度,即文件内容的最大长度。该值为UEVENT_HELPER_PATH_LEN,表示文件内容的最大长度为UEVENT_HELPER_PATH_LEN
  • .mode 表示文件的访问权限。该值为0644,表示该文件的权限为-rw-r--r--,即所有用户都可以读取该文件,但只有root 用户可以写入该文件。

117.2 处理uevent 事件

在上一章节中我们使用了netlink 机制来监听内核向用户空间发送的uevent 事件,而在上一小节中我们设置了uevent_helper,所以本小节将会学习调用用户空间程序来处理内核发送的uevent 事件。

117.2.1 编写应用程序

本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\81_mdev
本小节编写的是应用程序,所要实现的效果极其简单,只是获取SUBSYSTEM 环境变量并打印即可,编写完成的应用程序内容如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd = open("/dev/ttyFIQ0", O_WRONLY);
    dup2(fd, STDOUT_FILENO);
    printf("SUBSYSTEM is %s\n", getenv("SUBSYSTEM"));
    return 0;
}

117.2.2 编译应用程序

下面进行应用程序编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个mdev 的可执行程序,如下图(图117-7)所示:

aarch64-linux-gnu-gcc -o mdev mdev.c

下面进行程序的测试。

117.2.3 运行测试

本小节测试所使用的驱动文件为115 章编译生成的uevent_ops.ko,应用程序为上一小节编译出来的mdev
mdev 可执行文件和uevent_ops.ko 驱动存放在开发板的/mnt 目录下,如下图(图117-8)所示:

image-20240913104702689

在第一小节中讲解了三种配置方法,配置方法1 是在内核中直接指定的,使用起来较为麻烦,这里直接使用配置方法2 和配置方法3 这两种方式进行演示。

配置方法2:

echo /mnt/mdev > /sys/kernel/uevent_helper

然后使用以下命令加载uevent_ops.ko 驱动,如下图(图117-9)所示:
image-20240913104828830

配置方法3:

echo /mnt/mdev > /proc/sys/kernel/hotplug

然后使用以下命令加载uevent_ops.ko 驱动,如下图(图117-10)所示:
image-20240913104908337
上面两种配置都可以打印出内核加载时传递的SUBSYSTEM 环境变量,最后可以使用以下命令进行驱动的卸载,如下图(图54-12)所示:

rmmod uevent_ops.ko

image-20240913104929156

至此,uevent_helper 实验就完成了。

第118 章使用udev 挂载U 盘和T 卡实验

在前面章节中所讲解的都是关于热插拔的理论和较为简单的实验,而为了加深大家对热插拔的理解以及在可以在实际工作中进行应用,所以在本章节将进行实践,使用udev 挂载U 盘和T 卡。

118.1 配置buildroot 文件系统支持udev

上一章中我们编写了一个名为mdev 的应用程序,用来处理uevent 事件,而实际上udev和mdev 的可执行程序都是很复杂的,也并不需要我们自己来写,只需要在构建buildroot 文件系统时勾选对应的选项即可。首先来到buildroot 的源码目录下,如下图(图118-1)然后使用以下命令加载rk3568 的默认配置文件)所示:

image-20240913105008662

然后使用以下命令加载rk3568 的默认配置文件并进入到图形化配置界面,如下图(图118-2)所示:

make rockchip_rk3568_defconfig
make menuconfig

image-20240913105043045

由于本章节使用的是udev 所以需要在在System configuration 菜单中, 选择/dev
management (Dynamic using devtmpfs + eudev),如下图(图118-3)所示:

image-20240913105114785

默认情况下选中的就是udev,相应的镜像已经放在了“iTOP-RK3568 开发板【底板V1.7版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动程序\82_udev_u 盘_TF 卡”目录下如下图所示:

image-20240913105159650

烧写该镜像之后,可以在串口终端输入以下命令来查看udev 是否已经启用了,如下图(图118-4)所示:

ps -aux | grep -nR udev

image-20240913105221678

检查到/sbin/udevd 进程就表示当前系统使用的是udev,至此配置buildroot 文件系统支持udev 就完成了。

118.2 使用udev 挂载U 盘

本小节编写完成的文件对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程04_Linux 驱动程序\82_udev_u 盘_TF 卡\U 盘
在上一小节中配置buildroot 使能了udev,而要想使用udev 来实现U 盘的自动挂载,还需在开发板的/etc/udev/rules.d 目录下创建相应的规则文件(/etc/udev/rules.d 目录不存在可以手动创建,一般都已经存在了),这里我们创建一个名为001.rules 的文件,如下图(图118-5)所示:

image-20240913105306993

然后向该文件中添加以下内容:

KERNEL=="sd[a-z][0-9]", SUBSYSTEM=="block", ACTION=="add", RUN+="/etc/udev/rules.d/usb/usb-add.sh %k"
SUBSYSTEM=="block", ACTION=="remove", RUN+="/etc/udev/rules.d/usb/usb-remove.sh"

(1)KERNEL=="sd[a-z][0-9]"
KERNEL:表示匹配设备的内核名。
"sd[a-z][0-9]":是一个正则表达式模式,sd:表示设备名以”sd” 开头,[a-z]:表示设备名的第三个字符是小写字母,[0-9]:表示设备名的第四个字符是数字。
这个模式用于匹配USB 存储设备的块设备节点,如/dev/sda1、/dev/sdb2 等。

(2)SUBSYSTEM=="block"
SUBSYSTEM:表示匹配设备的子系统名称。
“block”:表示设备的子系统是块设备子系统,即与磁盘、分区等相关的设备。
这部分规则是为了确保只匹配块设备子系统下的设备。

(3)ACTION=="add"和ACTION=="remove"
ACTION:表示匹配设备的动作。
“add”:表示设备被添加。
“remove”:表示设备被yichu。
这部分规则是为了处理设备被添加和被删除的事件。

(5)RUN+="/etc/udev/rules.d/usb/usb-add.sh %k"
RUN+=”…”:表示在匹配的设备上执行指定的命令。
"/etc/udev/rules.d/usb/usb-add.sh":是要执行的命令的路径,即在设备添加时执行/etc/udev/rules.d/usb/usb-add.sh 脚本文件。
%k:是Udev 提供的一个变量,表示匹配的设备的内核名。

可以注意到当块设备被添加的时候会执行/etc/udev/rules.d/usb/usb-add.sh 脚本,块设备被删除的时候会执行/etc/udev/rules.d/usb/usb-remove.sh 脚本,所以接下来我们要完善这两个脚本内容,首先在/etc/udev/rules.d/目录下创建名为usb 的文件夹,并在这个创建usb-add.sh 和usb-remove.sh 脚本,如下图(图118-6) 所示:

image-20240913105833366

然后在/etc/udev/rules.d/usb/usb-add.sh 文件中写入以下内容:

#!/bin/sh
/bin/mount -t vfat /dev/$1 /mnt

在/etc/udev/rules.d/usb/usb-remove.sh 文件中写入以下内容:

#!/bin/sh
sync
/bin/umount -l /mnt

添加完成如下图(图118-7)所示:

image-20240913105934654

添加完成之后还需要使用chmod 命令赋予两个脚本的可执行权限,如下图(图118-8)所示:

image-20240913105946724

至此关于udev 自动挂载U 盘的相关配置文件完成了,首先输入以下df 命令查看当前的挂载情况,如下图(图118-9)所示:

image-20240913110007561

可以看到当前并没有关于U 盘相关的挂载信息,然后插入U 盘,相关打印如下(图118-10)所示:

然后重新使用df 命令查看当前的挂载情况,如下图(图118-11)所示:

image-20240913110032725

可以看到U 盘sda1 就成功挂载到了/mnt 目录,然后拔掉U 盘,重新使用df 命令查看当前挂载情况,可以发现/dev/sda1 设备已经消失了,如下图所示:

image-20240913110044454

至此,使用udev 自动挂载U 盘实验就完成了。

118.3 使用udev 挂载TF 卡

本小节编写完成的文件对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程04_Linux 驱动程序\82_udev_u 盘_TF 卡\TF 卡
在上一个小节中我们实现了U 盘的自动挂载,而为了帮助同学们举一反三,本小节要使用udev 实现TF 卡的自动挂载,将TF 卡挂载到/mnt 目录下,在不做任何修改的情况下,直接插入TF 卡,会发现TF 卡直接挂载到了/mnt/sdcard 目录,如下图所示:

image-20240913110126840

这是因为在/lib/udev/rules.d 目录下已经帮我们添加了很多的udev 规则,如下图所示:

image-20240913110142631

这里的规则文件跟我们上一小节自己创建的规则文件所实现的作用是相同的,只是/etc/udev/rules.d/目录的规则文件比/lib/udev/rules.d 目录的规则文件优先级高。
所以要实现TF 卡自动挂载到/mnt 命令让就需要进入到/etc/udev/rules.d/目录下,这次我们创建一个名为002.rules 的文件,如下图所示:

image-20240913110200894

然后向该文件中添加以下内容:

KERNEL=="mmcblk[0-9]p[0-9]", SUBSYSTEM=="block", ACTION=="add", RUN+="/etc/udev/rules.d/tf/tf-add.sh %k"
SUBSYSTEM=="block", ACTION=="remove", RUN+="/etc/udev/rules.d/tf/tf-remove.sh"

(1)KERNEL=="sd[a-z][0-9]"
KERNEL:表示匹配设备的内核名。
"sd[a-z][0-9]":是一个正则表达式模式,sd:表示设备名以”sd” 开头,[a-z]:表示设备名的第三个字符是小写字母,[0-9]:表示设备名的第四个字符是数字。
这个模式用于匹配USB 存储设备的块设备节点,如/dev/sda1、/dev/sdb2 等。

(2)SUBSYSTEM=="block"
SUBSYSTEM:表示匹配设备的子系统名称。
“block”:表示设备的子系统是块设备子系统,即与磁盘、分区等相关的设备。
这部分规则是为了确保只匹配块设备子系统下的设备。

(3)ACTION=="add"和ACTION=="remove"
ACTION:表示匹配设备的动作。
“add”:表示设备被添加。
“remove”:表示设备被yichu。
这部分规则是为了处理设备被添加和被删除的事件。

(5)RUN+="/etc/udev/rules.d/usb/usb-add.sh %k"
RUN+=”…”:表示在匹配的设备上执行指定的命令。
"/etc/udev/rules.d/usb/usb-add.sh":是要执行的命令的路径,即在设备添加时执行/etc/udev/rules.d/usb/usb-add.sh 脚本文件。
%k:是Udev 提供的一个变量,表示匹配的设备的内核名。

当TF 卡块设备被添加的时候会执行/etc/udev/rules.d/usb/tf-add.sh 脚本,TF 卡块设备被删除的时候会执行/etc/udev/rules.d/tf/tf-remove.sh 脚本,所以接下来我们要完善这两个脚本内容,首先在/etc/udev/rules.d/目录下创建名为tf 的文件夹,并在这个创建tf-add.sh 和tf-remove.sh 脚本,如下图所示:

然后在/etc/udev/rules.d/usb/tf-add.sh 文件中写入以下内容:

#!/bin/sh
/bin/mount -t vfat /dev/$1 /mnt

/etc/udev/rules.d/usb/tf-remove.sh 文件中写入以下内容:

#!/bin/sh
sync
/bin/umount -l /mnt

添加完成如下图所示:

image-20240913110648301

添加完成之后还需要使用chmod 命令赋予两个脚本的可执行权限,如下图所示:

image-20240913110701785

至此关于udev 自动挂载TF 卡的相关配置文件完成了,首先输入以下df 命令查看当前的挂载情况,如下图所示:

image-20240913110713200

可以看到当前并没有关于TF 卡相关的挂载信息,然后插入U 盘,相关打印如下所示:

image-20240913110722518

然后重新使用df 命令查看当前的挂载情况,如下图所示:

image-20240913110740595

可以看到TF 卡mmcblk1p1 就成功挂载到了/mnt 目录,然后拔掉U 盘,重新使用df 命令查看当前挂载情况,可以发现/dev/mmcblk1p1 设备已经消失了,如下图所示:

image-20240913110754231

至此,使用udev 自动挂载TF 卡的实验就完成了。

第119 章使用mdev 挂载U 盘和T 卡实验

在上一个章节中使用udev 实现了U 盘和TF 卡的自动挂载,本章节将使用mdev 来实现U盘和TF 卡的自动挂载。

119.1 配置buildroot 文件系统支持mdev

在上一章我们配置buildroot 文件系统支持了udev,而要想buildroot 系统支持mdev 也需要进行相似的配置。首先来到buildroot 的源码目录下,如下图所示:

image-20240913110832982

然后使用以下命令加载rk3568 的默认配置文件并进入到图形化配置界面,如下图所示:

make rockchip_rk3568_defconfig
make menuconfig

image-20240913110850797

由于本章节使用的是mdev 所以需要在在System configuration 菜单中, 选择/devmanagement (Dynamic using devtmpfs + mdev) ---> ,如下图所示:

image-20240913110917246

除了buildroot 需要配置之外,还需要配置busybox 的相关选项,默认已经配置好了,如下图所示:

image-20240913110931778

编译完成的镜像已经放在了“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动程序\83_mdev_u 盘_TF 卡”目录下如下图所示:

image-20240913110952583

烧写该镜像之后,可以在串口终端输入以下命令来查看mdev 是否已经启用了,如下图所示:

ps -aux | grep -nR mdev

image-20240913111013990

检查到/sbin/mdev 进程就表示当前系统使用的是mdev,至此配置buildroot 文件系统支持mdev 就完成了。

119.2 使用mdev 挂载U 盘

本小节编写完成的文件对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程04_Linux 驱动程序\83_mdev_u 盘_TF 卡\U 盘

跟udev 相同,mdev 也需要添加相应的规则,不同的是mdev 使用/etc/mdev.conf 文件来配置mdev 工具的规则和行为,要想使用mdev 自动挂载U 盘需要向/etc/mdev.conf 文件中添加以下两条规则,

sd[a-z][0-9] 0:0 666 @/etc/mdev/usb_insert.sh
sd[a-z] 0:0 666 $/etc/mdev/usb_remove.sh

这两个规则用于处理U 盘的热插拔事件,并执行相应的操作。在/etc/mdev.conf 文件中,每一行都是一个规则,具有以下格式:

<设备节点正则表达式> <设备的所有者:设备的所属组> <设备的权限> <设备插入或移除时需要执行的命令>

下面是对上述两个规则的详细介绍:
(1)sd[a-z][0-9] 是一个正则表达式模式,用于匹配以”sd” 开头,后跟一个小写字母和一个数字的设备节点,例如/dev/sda1、/dev/sdb2 等。
(2)0:0 666 表示设置设备节点的所有者和权限。0:0 表示所有者和所属组的用户ID 和组ID 均为0,即root 用户。666 表示权限为可读可写。
(3)@/etc/mdev/usb_insert.sh 表示当符合规则的设备插入时,mdev 会执行/etc/mdev/usb_insert.sh 脚本。@ 符号表示执行的是一个shell 命令。
(4)$/etc/mdev/usb_remove.sh 表示当符合规则的设备移除时,mdev 会执行/etc/mdev/usb_remove.sh 脚本。$ 符号表示执行的是一个内部命令。

规则添加完成之后就要去对应的目录下添加usb_insert.shusb_remove.sh 脚本文件了,首先创建/etc/mdev 目录并进入到该目录创建usb_insert.sh 和usb_remove.sh 两个文件如下图所示:

image-20240913111348448

然后在/etc/mdev/usb_insert.sh 文件中写入以下内容:

#!/bin/sh
if [ -d /sys/block/*/$MDEV ]; then
    mount /dev/$MDEV /mnt
    sync
fi

在/etc/mdev/usb_remove.sh 文件中写入以下内容:

#!/bin/sh
sync
/bin/umount -l /mnt

添加完成如下图所示:

image-20240913111610230

添加完成之后还需要使用chmod 命令赋予两个脚本的可执行权限,如下图所示:

image-20240913111630575

至此关于mdev 自动挂载U 盘的相关配置文件完成了,首先输入以下df 命令查看当前的挂载情况,如下图所示:

image-20240913111641937

可以看到当前并没有关于U 盘相关的挂载信息,然后插入U 盘,相关打印如下所示:

image-20240913111650025

然后重新使用df 命令查看当前的挂载情况,如下图所示:

image-20240913111700161

可以看到U 盘sda1 就成功挂载到了/mnt 目录,然后拔掉U 盘,重新使用df 命令查看当前挂载情况,可以发现/dev/sda1 设备已经消失了,如下图所示:

image-20240913111710186

至此,使用mdev 自动挂载U 盘实验就完成了。

119.3 使用mdev 挂载TF 卡

本小节编写完成的文件对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程04_Linux 驱动程序\83_mdev_u 盘_TF 卡\TF 卡

在上一个小节中我们实现了U 盘的自动挂载,而为了帮助同学们举一反三,本小节要使用mdev 实现TF 卡的自动挂载,跟U 盘自动挂载相同,TF 卡自动挂载也需要向/etc/mdev.conf 文件中添加以下两条类似的规则,

mmcblk[0-9]p[0-9] 0:0 666 @/etc/mdev/tf_insert.sh
mmcblk[0-9] 0:0 666 $/etc/mdev/tf_remove.sh

这两个规则用于处理U 盘的热插拔事件,并执行相应的操作。在/etc/mdev.conf 文件中,每一行都是一个规则,具有以下格式:

<设备节点正则表达式> <设备的所有者:设备的所属组> <设备的权限> <设备插入或移除时需要执行的命令>

下面是对上述两个规则的详细介绍:
(1)mmcblk[0-9]p[0-9] 是一个正则表达式模式,用于匹配以”mmcblk” 开头的TF 卡块设备,例如/dev/mmcblk1p1 等。
(2)0:0 666 表示设置设备节点的所有者和权限。0:0 表示所有者和所属组的用户ID 和组ID 均为0,即root 用户。666 表示权限为可读可写。
(5)@/etc/mdev/tf_insert.sh 表示当符合规则的设备插入时,mdev 会执行/etc/mdev/tf_insert.sh 脚本。@ 符号表示执行的是一个shell 命令。
(6)$/etc/mdev/tf_remove.sh 表示当符合规则的设备移除时,mdev 会执行/etc/mdev/tf_remove.sh 脚本。$ 符号表示执行的是一个内部命令。

规则添加完成之后就要去对应的目录下添加tf_insert.shtf_remove.sh 脚本文件了,首先进入到/etc/mdev 目录创建tf_insert.shtf_remove.sh 两个文件如下图所示:

image-20240913112418311

然后在/etc/mdev/tf_insert.sh 文件中写入以下内容:

#!/bin/sh

if [ -d /sys/block/*/$MDEV ]; then
    mount /dev/$MDEV /mnt
    sync
fi

/etc/mdev/tf_remove.sh 文件中写入以下内容:

#!/bin/sh
sync
/bin/umount -l /mnt

添加完成如下图所示:

image-20240913112649922

添加完成之后还需要使用chmod 命令赋予两个脚本的可执行权限,如下图所示:

image-20240913112708195

至此关于mdev 自动挂载TF 卡的相关配置文件完成了,首先输入以下df 命令查看当前的挂载情况,如下图所示:

image-20240913112722183

可以看到当前并没有关于TF 卡相关的挂载信息,然后插入TF 卡,相关打印如下所示:

image-20240913112737923

然后重新使用df 命令查看当前的挂载情况,如下图所示:

image-20240913112747421

可以看到TF 卡mmcblk1p1 就成功挂载到了/mnt 目录,然后拔掉TF 卡,重新使用df 命令查看当前挂载情况,可以发现/dev/mmcblk1p1 设备已经消失了,如下图所示:

image-20240913112803957

虽然TF 卡设备已经消失了,但是又出现了一个/dev/mmcblk0p8 的设备挂载到了/mnt 目录,这里是正常的,mmcblk0p8 是emmc 里的一个分区,也符合上面我们添加的热插拔规则。
至此,使用mdev 自动挂载TF 卡实验就完成了。

第十一篇pinctrl 子系统

第120 章pinctrl 子系统的引入

Linux 中的pinctrl 子系统(Pin Control Subsystem)是一个用于管理和配置通用输入/输出(GPIO)引脚的框架。它提供了一种标准化的方法,以在Linux 内核中对GPIO 引脚进行配置、分配和控制,从而适应不同的硬件平台和设备。

pinctrl 子系统也符合Linux 内核的设备模型规范,所以pinctrl 子系统同样可以根据设备模型规范分为设备、驱动、总线和类四个部分,本章节将从设备和驱动两个部分来引入pinctrl子系统。

120.1 pinctrl 设备树

在前面设备树相关的章节中已经对pinctrl 节点的编写和使用进行了讲解,设备树的pinctrl可以分为客户端和服务端两个部分,在pinctrl 客户端可以指定引脚描述、引脚组描述和配置描述,以满足其特定的功能和需求,不同厂商在客户端内容的编写格式是相同的。服务端是指提供GPIO 引脚配置的pinctrl 设备树节点,它是描述GPIO 引脚配置和使用规则的节点,定义了一组GPIO 引脚的配置选项,以及这些选项对应的引脚功能和电气特性。

接下来对rk3568 的pinctrl 设备树进行详细的讲解。首先在rk3568.dtsi 设备树根节点下找到pinctrl 节点,具体内容如下所示:

pinctrl: pinctrl {
    compatible = "rockchip,rk3568-pinctrl";
    rockchip,grf = <&grf>;
    rockchip,pmu = <&pmugrf>;
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;
    gpio0: gpio@fdd60000 {
        compatible = "rockchip,gpio-bank";
        reg = <0x0 0xfdd60000 0x0 0x100>;
        interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;
        gpio-controller;
        #gpio-cells = <2>;
        gpio-ranges = <&pinctrl 0 0 32>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    gpio1: gpio@fe740000 {
        compatible = "rockchip,gpio-bank";
        reg = <0x0 0xfe740000 0x0 0x100>;
        interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&cru PCLK_GPIO1>, <&cru DBCLK_GPIO1>;
        gpio-controller;
        #gpio-cells = <2>;
        gpio-ranges = <&pinctrl 0 32 32>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    gpio2: gpio@fe750000 {
        compatible = "rockchip,gpio-bank";
        reg = <0x0 0xfe750000 0x0 0x100>;
        interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&cru PCLK_GPIO2>, <&cru DBCLK_GPIO2>;
        gpio-controller;
        #gpio-cells = <2>;
        gpio-ranges = <&pinctrl 0 64 32>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    gpio3: gpio@fe760000 {
        compatible = "rockchip,gpio-bank";
        reg = <0x0 0xfe760000 0x0 0x100>;
        interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&cru PCLK_GPIO3>, <&cru DBCLK_GPIO3>;
        gpio-controller;
        #gpio-cells = <2>;
        gpio-ranges = <&pinctrl 0 96 32>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    gpio4: gpio@fe770000 {
        compatible = "rockchip,gpio-bank";
        reg = <0x0 0xfe770000 0x0 0x100>;
        interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&cru PCLK_GPIO4>, <&cru DBCLK_GPIO4>;
        gpio-controller;
        #gpio-cells = <2>;
        gpio-ranges = <&pinctrl 0 128 32>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
};
#include "rk3568-pinctrl.dtsi"

在上面的pinctrl 节点中,描述了RK3568 GPIO 控制器的配置和使用方式,pinctrl 节点总共描述了五个GPIO 控制器,分别是gpio0、gpio1、gpio2、gpio3 和gpio4。通过这些GPIO 控制器节点,可以在设备树中配置和控制RK3568 芯片上的GPIO 引脚,包括设置引脚功能、中断处理等。

在设备树的最下方通过include 包含了rk3568-pinctrl.dtsi 设备树,该设备树中包含了所有复用功能的配置,具体内容如下所示(更具体的内容描述可以回顾设备树pinctrl 章节内容):

image-20240913114659357

无论是rk3568.dtsi 设备树中的pinctrl 节点,还是上面rk3568-pinctrl.dtsi 设备树中的一系列复用关系都是由瑞芯微原厂BSP 工程师编写的,我们只需知道如何使用即可,而pinctrl 客户端设备树是由我们自己根据特定需求来编写的,具体可以回顾前面设备树相关的章节,这里就不再进行赘述。

设备树中存放的只是设备的描述信息,而具体的功能实现取决于相应的pinctrl 驱动,根据rk3568.dtsi 设备树中pinctrl 节点的compatible 属性进行查找,可以查找到pinctrl 的驱动文件是内核源码的“/driver/pinctrl/pinctrl-rockchip.c”,如下所示:

image-20240913114741501

在下个小节中将对pinctrl 的驱动部分进行简单的介绍。

120.2 pinctrl 驱动

首先进入到内核源码目录下的“/drivers/pinctrl/pinctrl-rockchip.c”驱动文件中,找到驱动的入口函数,具体内容如下所示:

static struct platform_driver rockchip_pinctrl_driver = {
    .probe = rockchip_pinctrl_probe,
    .driver = {
        .name = "rockchip-pinctrl",
        .pm = &rockchip_pinctrl_dev_pm_ops,
        .of_match_table = rockchip_pinctrl_dt_match,
    },
};

static int __init rockchip_pinctrl_drv_register(void)
{
    return platform_driver_register(&rockchip_pinctrl_driver);
}
postcore_initcall(rockchip_pinctrl_drv_register);

static void __exit rockchip_pinctrl_drv_unregister(void)
{
    platform_driver_unregister(&rockchip_pinctrl_driver);
}

可以看到pinctrl 驱动使用的是platform 总线,当设备和驱动匹配成功之后会进入rockchip_pinctrl_probe 函数进行初始化,probe 函数的具体内容如下所示:

static int rockchip_pinctrl_probe(struct platform_device *pdev)
{
    struct rockchip_pinctrl *info; // Rockchip GPIO 控制器的信息结构体指针
    struct device *dev = &pdev->dev; // 设备结构体指针
    struct rockchip_pin_ctrl *ctrl; // Rockchip GPIO 控制器的配置结构体指针
    struct device_node *np = pdev->dev.of_node, *node; // 设备节点指针
    struct resource *res; // 设备资源指针
    void __iomem *base; // 寄存器基地址指针
    int ret; // 返回值
    
    if (!dev->of_node) {
        dev_err(dev, "device tree node not found\n");
        return -ENODEV;
    }
    
    // 分配并初始化一个rockchip_pinctrl 结构体
    info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL);
    if (!info)
        return -ENOMEM;
    
    info->dev = dev;
    
    // 获取并设置与pdev 相关的rockchip_pin_ctrl 结构体
    ctrl = rockchip_pinctrl_get_soc_data(info, pdev);
    if (!ctrl) {
        dev_err(dev, "driver data not available\n");
        return -EINVAL;
    }
    info->ctrl = ctrl;
    
    // 解析设备树中的"rockchip,grf"节点,获取寄存器映射基地址
    node = of_parse_phandle(np, "rockchip,grf", 0);
    if (node) {
        info->regmap_base = syscon_node_to_regmap(node);
        if (IS_ERR(info->regmap_base))
            return PTR_ERR(info->regmap_base);
    } else {
        // 如果找不到"rockchip,grf"节点,则获取IORESOURCE_MEM 类型的资源,得到寄存器基地址
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        base = devm_ioremap_resource(&pdev->dev, res);
        if (IS_ERR(base))
            return PTR_ERR(base);
        // 配置寄存器映射的最大寄存器地址和名称
        rockchip_regmap_config.max_register = resource_size(res) - 4;
        rockchip_regmap_config.name = "rockchip,pinctrl";
        info->regmap_base = devm_regmap_init_mmio(&pdev->dev, base,&rockchip_regmap_config);

        // 检查旧的dt-bindings
        info->reg_size = resource_size(res);

        // 如果控制器类型为RK3188 且reg_size 小于0x200,则获取第二个IORESOURCE_MEM 类型的资源,作为pull 寄存器的基地址
        if (ctrl->type == RK3188 && info->reg_size < 0x200) {
            res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
            base = devm_ioremap_resource(&pdev->dev, res);
            if (IS_ERR(base))
                return PTR_ERR(base);
            
            // 配置pull 寄存器映射的最大寄存器地址和名称
            rockchip_regmap_config.max_register =resource_size(res) - 4;
            rockchip_regmap_config.name = "rockchip,pinctrl-pull";
            info->regmap_pull = devm_regmap_init_mmio(&pdev->dev,base,&rockchip_regmap_config);
        }
    }
    
    // 尝试查找可选的pmu syscon 引用
    node = of_parse_phandle(np, "rockchip,pmu", 0);
    if (node) {
        info->regmap_pmu = syscon_node_to_regmap(node);
        if (IS_ERR(info->regmap_pmu))
            return PTR_ERR(info->regmap_pmu);
    }
    // 对于某些SoC 进行特殊处理
    if (ctrl->soc_data_init) {
        ret = ctrl->soc_data_init(info);
        if (ret)
            return ret;
    }
    
    // 注册rockchip_pinctrl 设备
    ret = rockchip_pinctrl_register(pdev, info);
    if (ret)
        return ret;
    
    // 设置pdev 的私有数据为info
    platform_set_drvdata(pdev, info);
    // 注册GPIO 设备
    ret = of_platform_populate(np, rockchip_bank_match, NULL, NULL);
    if (ret) {
        dev_err(&pdev->dev, "failed to register gpio device\n");
        return ret;
    }
    dev_info(dev, "probed %s\n", dev_name(dev));
    return 0;
}

上面Probe 函数的作用是初始化和配置Rockchip GPIO 控制器,并将相关信息存储在rockchip_pinctrl 结构体中,最后注册相关设备和GPIO 接口,关于Probe 函数会在后面的小节中进行更加具体的分析。

最后来带领大家思考一个问题,假如我们要配置一个LED 外设,该LED 需要使用一个管脚来进行控制,那这个控制引脚需要复用成GPIO 之后才能完成相应的功能,通过上面内容的学习之后,我们知道是pinctrl 子系统将这个管脚复用为了GPIO 功能,那pinctrl 子系统是什么时候对该引脚进行的复用呢?带着这个疑问,让我们一起进入后面章节的学习吧。

第121 章pinctrl probe 函数讲解

由于pinctrl 驱动的probe 函数理解起来较为复杂,所以在讲解pinctrl 驱动的probe 函数之前,我们先来了解一些与pinctrl 相关的数据结构,本章首先会学习pinctrl_desc 结构体和rockchip_pinctrl 结构体,最后对pinctrl probe 函数进行分析。

121.1 pinctrl_desc 结构体分析

pinctrl_desc 结构体用于描述引脚控制器(pinctrl)的属性和操作。引脚控制器是硬件系统中的一个组件,用于管理和控制引脚的功能和状态。pinctrl_desc 结构体的作用是提供一个统一的接口,用于配置和管理引脚控制器的行为。pinctrl_desc 结构体定义在内核源码目录的“/include/linux/pinctrl/pinctrl.h”文件中,具体内容如下所示:

struct pinctrl_desc {
    const char *name; // 引脚控制器的名称
    const struct pinctrl_pin_desc *pins; // 引脚描述符数组
    unsigned int npins; // 引脚描述符数组的大小
    const struct pinctrl_ops *pctlops; // 引脚控制操作函数指针
    const struct pinmux_ops *pmxops; // 引脚复用操作函数指针
    const struct pinconf_ops *confops; // 引脚配置操作函数指针
    struct module *owner; // 拥有该结构体的模块
    
#ifdef CONFIG_GENERIC_PINCONF
    unsigned int num_custom_params; // 自定义参数数量
    const struct pinconf_generic_params *custom_params; // 自定义参数数组
    const struct pin_config_item *custom_conf_items; // 自定义配置项数组
#endif
    
};
  • (1)const char *name: 引脚控制器的名称,用于标识引脚控制器的唯一性。
  • (2)const struct pinctrl_pin_desc *pins: 引脚描述符数组,是一个指向引脚描述符的指针,用于描述引脚的属性和配置。每个引脚描述符包含了引脚的名称、编号、模式等信息。
  • (3)unsigned int npins: 表示引脚描述符数组中元素的数量,用于确定引脚描述符数组的长度。
  • (4)const struct pinctrl_ops *pctlops: 指向引脚控制操作函数的指针,用于定义引脚控制器的操作接口。通过这些操作函数,可以对引脚进行配置、使能、禁用等操作。
  • (5)const struct pinmux_ops *pmxops: 指向引脚复用操作函数的指针,用于定义引脚的复用功能。复用功能允许将引脚的功能切换为不同的模式,以适应不同的设备需求。
  • (6)const struct pinconf_ops *confops: 指向引脚配置操作函数的指针,用于定义引脚的其他配置选项。这些配置选项可以包括引脚的上拉、下拉配置、电气特性等。
  • (7)struct module *owner: 指向拥有该引脚控制器结构体的模块的指针。这个字段用于跟踪引脚控制器结构体的所有者。
  • (8)unsigned int num_custom_params: 表示自定义配置参数的数量,用于描述引脚控制器的自定义配置参数。
  • (9)const struct pinconf_generic_params *custom_params: 指向自定义配置参数的指针,用于描述引脚控制器的自定义配置参数的属性。自定义配置参数可以根据具体需求定义,用于扩展引脚控制器的配置选项。
  • (10)const struct pin_config_item *custom_conf_items: 指向自定义配置项的指针,用于描述引脚控制器的自定义配置项的属性。自定义配置项可以根据具体需求定义,用于扩展引脚控制器的配置选项。

121.2 rockchip_pinctrl 结构体分析

而瑞芯微为了适应瑞芯微芯片的特定需求和功能,对struct pinctrl_desc 进行了再一次封装。封装后的struct rockchip_pinctrl 结构体在struct pinctrl_desc 的基础上增加了与瑞芯微芯片相关的字段和指针,这种封装可以提供更好的集成性、易用性和扩展性,同时保持与通用引脚控制器框架的兼容性。
rockchip_pinctrl 结构体定义在内核源码目录下的“/drivers/pinctrl/pinctrl-rockchip.h”文件中,具体内容如下所示:

struct rockchip_pinctrl {
    struct regmap *regmap_base; // 基本寄存器映射指针
    int reg_size; // 寄存器大小
    struct regmap *regmap_pull; // 拉取寄存器映射指针
    struct regmap *regmap_pmu; // 电源管理单元寄存器映射指针
    struct device *dev; // 设备指针
    struct rockchip_pin_ctrl *ctrl; // 瑞芯微芯片引脚控制器指针
    struct pinctrl_desc pctl; // 引脚控制器描述符
    struct pinctrl_dev *pctl_dev; // 引脚控制器设备指针
    struct rockchip_pin_group *groups; // 瑞芯微芯片引脚组指针
    unsigned int ngroups; // 引脚组数量
    struct rockchip_pmx_func *functions; // 瑞芯微芯片引脚功能指针
    unsigned int nfunctions; // 引脚功能数量
};

(1)struct regmap *regmap_base:指向基本寄存器映射(regmap)的指针。基本寄存器映射是一个用于访问芯片寄存器的接口,它提供了对芯片寄存器的读写操作。
(2)int reg_size:表示寄存器的字节大小,用于确定寄存器的地址范围。
(3)struct regmap *regmap_pull:指向拉取寄存器映射的指针。拉取寄存器映射用于控制引脚上的上拉和下拉功能。
(4)struct regmap *regmap_pmu:指向电源管理单元(PMU)寄存器映射的指针。PMU寄存器映射用于控制引脚的电源管理功能。
(5)struct device *dev:指向设备结构体的指针。设备结构体用于表示与硬件相关的设备,包括设备的物理地址、中断等信息。
(6)struct rockchip_pin_ctrl *ctrl:指向瑞芯微芯片引脚控制器的指针。这个结构体存储了瑞芯微芯片特定的引脚控制器的相关信息和操作。
(7)struct pinctrl_desc pctl:包含了struct pinctrl_desc 结构体的一个实例。用于描述引脚控制器的属性和操作,包括引脚控制器的名称、引脚描述符数组、函数指针等。
(8)struct pinctrl_dev *pctl_dev:指向引脚控制器设备结构体的指针。引脚控制器设备结构体用于表示引脚控制器在系统中的设备实例,包含了与引脚控制器相关的设备信息和操作接口。
(9)struct rockchip_pin_group *groups:指向瑞芯微芯片引脚组的指针。引脚组是一组相关的引脚,可以一起进行配置和管理。
(10)unsigned int ngroups:表示引脚组数组的大小,用于确定引脚组数组的长度。
(11)struct rockchip_pmx_func *functions:指向瑞芯微芯片引脚功能的指针。引脚功能定义了引脚可以承担的不同功能,例如UART、SPI、I2C 等。
(12)unsigned int nfunctions:引脚功能的数量。它表示引脚功能数组的大小,用于确定引脚功能数组的长度。

121.3 pinctrl probe 函数分析

在上一章中已经找到了瑞芯微pinctrl 驱动文件的probe 函数,具体内容如下所示:

static int rockchip_pinctrl_probe(struct platform_device *pdev)
{
    struct rockchip_pinctrl *info; // Rockchip GPIO 控制器的信息结构体指针
    struct device *dev = &pdev->dev; // 设备结构体指针
    struct rockchip_pin_ctrl *ctrl; // Rockchip GPIO 控制器的配置结构体指针
    struct device_node *np = pdev->dev.of_node, *node; // 设备节点指针
    struct resource *res; // 设备资源指针
    void __iomem *base; // 寄存器基地址指针
    int ret; // 返回值
    
    if (!dev->of_node) {
        dev_err(dev, "device tree node not found\n");
        return -ENODEV;
    }
    
    // 分配并初始化一个rockchip_pinctrl 结构体
    info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL);
    if (!info)
        return -ENOMEM;
    info->dev = dev;
    
    // 获取并设置与pdev 相关的rockchip_pin_ctrl 结构体
    ctrl = rockchip_pinctrl_get_soc_data(info, pdev);
    if (!ctrl) {
        dev_err(dev, "driver data not available\n");
        return -EINVAL;
    }
    info->ctrl = ctrl;
    
    // 解析设备树中的"rockchip,grf"节点,获取寄存器映射基地址
    node = of_parse_phandle(np, "rockchip,grf", 0);
    if (node) {
    info->regmap_base = syscon_node_to_regmap(node);
    if (IS_ERR(info->regmap_base))
        return PTR_ERR(info->regmap_base);
    } else {
        // 如果找不到"rockchip,grf"节点,则获取IORESOURCE_MEM 类型的资源,得到寄存器基地址
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        base = devm_ioremap_resource(&pdev->dev, res);
        if (IS_ERR(base))
            return PTR_ERR(base);

        // 配置寄存器映射的最大寄存器地址和名称
        rockchip_regmap_config.max_register = resource_size(res) - 4;
        rockchip_regmap_config.name = "rockchip,pinctrl";
        info->regmap_base = devm_regmap_init_mmio(&pdev->dev, base,&rockchip_regmap_config);
        // 检查旧的dt-bindings
        info->reg_size = resource_size(res);
        
        // 如果控制器类型为RK3188 且reg_size 小于0x200,则获取第二个IORESOURCE_MEM 类型的资源,作为pull 寄存器的基地址
        if (ctrl->type == RK3188 && info->reg_size < 0x200) {
            res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
            base = devm_ioremap_resource(&pdev->dev, res);
            if (IS_ERR(base))
                return PTR_ERR(base);
            // 配置pull 寄存器映射的最大寄存器地址和名称
            rockchip_regmap_config.max_register =
            resource_size(res) - 4;
            rockchip_regmap_config.name = "rockchip,pinctrl-pull";
            info->regmap_pull = devm_regmap_init_mmio(&pdev->dev,base,&rockchip_regmap_config);
        }
    }
    
    // 尝试查找可选的pmu syscon 引用
    node = of_parse_phandle(np, "rockchip,pmu", 0);
    if (node) {
        info->regmap_pmu = syscon_node_to_regmap(node);
        if (IS_ERR(info->regmap_pmu))
            return PTR_ERR(info->regmap_pmu);
    }
    
    // 对于某些SoC 进行特殊处理
    if (ctrl->soc_data_init) {
        ret = ctrl->soc_data_init(info);
        if (ret)
            return ret;
    }
    
    // 注册rockchip_pinctrl 设备
    ret = rockchip_pinctrl_register(pdev, info);
    if (ret)
        return ret;
    
    // 设置pdev 的私有数据为info
    platform_set_drvdata(pdev, info);
    
    // 注册GPIO 设备
    ret = of_platform_populate(np, rockchip_bank_match, NULL, NULL);
    if (ret) {
        dev_err(&pdev->dev, "failed to register gpio device\n");
        return ret;
    }
    
    dev_info(dev, "probed %s\n", dev_name(dev));
    return 0;
}

接下来对该函数进行详细的分析:

  • (1)第11-14 行:检查设备结构体中的设备树节点是否存在,如果不存在则报错并返回错误码。

  • (2)第17-19 行:使用devm_kzalloc 函数分配一个rockchip_pinctrl 结构体的内存,并将其初始化为0。然后将设备结构体指针赋值给info->dev,以便在后续代码中可以使用设备结构体的信息。

  • (3)第23-29 行:调用rockchip_pinctrl_get_soc_data 函数,根据设备信息获取与该设备相关的rockchip_pin_ctrl 结构体。如果获取失败,则报错并返回错误码。将获取到的结构体指针赋值给info->ctrl。

  • (4)第32-69 行使用of_parse_phandle 函数解析设备树中名为”rockchip,grf”的节点。如果解析成功,则调用syscon_node_to_regmap 函数将节点转换为寄存器映射的基地址,并将结果存储在info->regmap_base 中。如果解析失败,则进入”else”分支。
    在”else”分支中,通过platform_get_resource 函数获取IORESOURCE_MEM 类型的资源,以获取寄存器的基地址。然后使用devm_ioremap_resource 函数将资源映射到内存中,并将结果存储在base 中。接下来, 配置寄存器映射的最大寄存器地址和名称, 并使用devm_regmap_init_mmio 函数初始化寄存器映射,将结果存储在info->regmap_base 中。

    如果控制器类型为RK3188 且reg_size 小于0x200,则获取第二个IORESOURCE_MEM 类型的资源,作为pull 寄存器的基地址。类似地,配置pull 寄存器映射的最大寄存器地址和名称,并使用devm_regmap_init_mmio 函数初始化pull 寄存器映射,将结果存储在info->regmap_pull中。

  • (5)第72-77 行:使用of_parse_phandle 函数解析设备树中名为”rockchip,pmu”的可选节点。如果解析成功,则调用syscon_node_to_regmap 函数将节点转换为寄存器映射的基地址,并将结果存储在info->regmap_pmu 中。

  • (6)第80-84 行:如果ctrl->soc_data_init 不为空,则调用该函数指针所指向的函数,对特定的SoC 进行特殊处理。处理完成后,如果返回值不为0,则返回该错误码。

  • (7)第87-89 行:调用rockchip_pinctrl_register 函数注册rockchip_pinctrl 设备。如果注册失败,则返回错误码。

  • (8)第92 行:使用platform_set_drvdata 函数将info 设置为pdev 的私有数据。

  • (9)第95-100 行:调用of_platform_populate 函数注册GPIO 设备。如果注册失败,则返回错误码。

在probe 函数中需要特别注意的是第87 行的rockchip_pinctrl_register 注册rockchip_pinctrl设备函数, 传入的参数分别为pdev 和rockchip_pinctrl 类型的info , 然后跳转到rockchip_pinctrl_register 函数的定义,该函数的具体内容如下所示:

static int rockchip_pinctrl_register(struct platform_device *pdev,struct rockchip_pinctrl *info)
{
    struct pinctrl_desc *ctrldesc = &info->pctl;
    struct pinctrl_pin_desc *pindesc, *pdesc;
    struct rockchip_pin_bank *pin_bank;
    int pin, bank, ret;
    int k;
    
    // 初始化pinctrl 描述结构体
    ctrldesc->name = "rockchip-pinctrl";
    ctrldesc->owner = THIS_MODULE;
    ctrldesc->pctlops = &rockchip_pctrl_ops;
    ctrldesc->pmxops = &rockchip_pmx_ops;
    ctrldesc->confops = &rockchip_pinconf_ops;
    
    // 为每个引脚分配内存
    pindesc = devm_kcalloc(&pdev->dev,info->ctrl->nr_pins, sizeof(*pindesc),GFP_KERNEL);
    if (!pindesc)
    	return -ENOMEM;
    ctrldesc->pins = pindesc;
    ctrldesc->npins = info->ctrl->nr_pins;
    pdesc = pindesc;
    
    // 遍历每个引脚所属的bank,为每个引脚设置编号和名称
    for (bank = 0, k = 0; bank < info->ctrl->nr_banks; bank++) {
        pin_bank = &info->ctrl->pin_banks[bank];
        for (pin = 0; pin < pin_bank->nr_pins; pin++, k++) {
            pdesc->number = k;
            pdesc->name = kasprintf(GFP_KERNEL, "%s-%d",pin_bank->name, pin);
            pdesc++;
        }
    }
    
    // 解析设备树中的pinctrl 信息
    ret = rockchip_pinctrl_parse_dt(pdev, info);
    if (ret)
        return ret;
    
    // 注册pinctrl 设备
    info->pctl_dev = devm_pinctrl_register(&pdev->dev, ctrldesc, info);
    if (IS_ERR(info->pctl_dev)) {
        dev_err(&pdev->dev, "could not register pinctrl driver\n");
        return PTR_ERR(info->pctl_dev);
    }
    return 0;
}
  • (1)第4 行:将info 的pctl 参数传递给了struct pinctrl_desc *类型的变量ctrldesc,从而使得rockchip_pinctrl 结构体和pinctrl_desc 结构体建立了联系。
  • (2)第11-15 行,对pinctrl 描述结构体进行初始化。这些结构体成员包括name(pinctrl控制器的名称),owner(拥有该pinctrl 控制器的模块),pctlops(pinctrl 控制操作函数),pmxops(pinctrl 引脚复用操作函数)和confops(pinctrl 引脚配置操作函数)。
  • (3)第18-22 行,使用devm_kcalloc 函数在设备的内存上分配一块连续的内存区域,用于存储引脚描述结构体。该函数分配的内存大小为info->ctrl->nr_pins * sizeof(*pindesc)字节。如果内存分配失败,则返回-ENOMEM 错误码。
  • (4)第24-25 行:通过将引脚描述结构体的指针pindesc 赋值给pinctrl 描述结构体的pins成员,将引脚数量info->ctrl->nr_pins 赋值给pinctrl 描述结构体的npins 成员。
  • (5)第27-37 行:通过遍历每个引脚所属的bank,为每个引脚设置编号和名称。首先,定义变量pdesc 指向引脚描述结构体的起始地址。然后,使用两个嵌套的循环,外层循环遍历每个引脚所属的bank,内层循环遍历每个bank 中的引脚。
    • 在内层循环中,首先将当前引脚的编号k 赋值给引脚描述结构体的number 成员,然后使用kasprintf 函数动态分配内存储引脚名称的字符串,并将该字符串赋值给引脚描述结构体的name 成员。kasprintf 函数在内核堆中分配内存,并格式化生成字符串。格式化的字符串为”%s-%d”,其中pin_bank->name 是当前bank 的名称,pin 是当前引脚在bank 中的索引。
  • (6)第39-42 行:调用rockchip_pinctrl_parse_dt 函数来解析设备树中的pinctrl 信息。该函数根据设备树中的描述,设置引脚的默认配置。
  • (7)第45-49 行:调用devm_pinctrl_register 函数注册pinctrl 设备。该函数将pinctrl 描述结构体、pinctrl 相关操作函数和私有数据作为参数,将pinctrl 设备注册到系统中。
    • 其中需要注意第45 行的devm_pinctrl_register 函数,该函数定义在内核源码目录下的“drivers/pinctrl/core.c”文件中,该函数的具体内容如下所示:
struct pinctrl_dev *devm_pinctrl_register(struct device *dev,struct pinctrl_desc *pctldesc,
                                          void *driver_data)
{
    struct pinctrl_dev **ptr, *pctldev;
    
    // 分配用于存储pinctrl_dev 指针的内存
    ptr = devres_alloc(devm_pinctrl_dev_release, sizeof(*ptr), GFP_KERNEL);
    if (!ptr)
    	return ERR_PTR(-ENOMEM);
    
    // 注册pinctrl 设备
    pctldev = pinctrl_register(pctldesc, dev, driver_data);
    if (IS_ERR(pctldev)) {
        devres_free(ptr);
        return pctldev;
    }
    
    // 将pinctrl_dev 指针存储到devres 中
    *ptr = pctldev;
    devres_add(dev, ptr);
    return pctldev;
}

这个函数用于注册pinctrl 设备,并将其与设备关联起来。下面是对每个部分的详细解释:

  • (1)第8-10 行:使用devres_alloc 函数为存储pinctrl_dev 指针的变量ptr 分配内存。devres_alloc 函数是用于管理设备资源的函数,它在设备的资源列表中分配内存。这里分配的内存大小为sizeof(*ptr)字节,即一个pinctrl_dev 指针的大小。如果内存分配失败,则返回-ENOMEM错误码。
  • (2)第13-17 行:调用pinctrl_register 函数注册pinctrl 设备。该函数将pinctrl_desc 结构体、设备指针dev 和驱动程序数据driver_data 作为参数,并返回注册后的pinctrl_dev 指针。如果注册失败(返回错误码),则释放之前分配的内存,并返回相应的错误码。
  • (3)第20-21 行:将pinctrl_dev 指针存储到ptr 指向的内存位置。接下来,使用devres_add 函数将ptr 添加到设备的资源列表中。这样,在设备释放时,会自动释放之前分配的内存。

然后我们继续关注第13 行的pinctrl 设备注册函数pinctrl_register,然后跳转到他的定义,该函数的具体内容如下所示:

struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,struct device *dev, 
                                     void *driver_data)
{
    struct pinctrl_dev *pctldev;
    int error;
    
    // 初始化pinctrl 控制器
    pctldev = pinctrl_init_controller(pctldesc, dev, driver_data);
    if (IS_ERR(pctldev))
        return pctldev;
    
    // 启用pinctrl 控制器
    error = pinctrl_enable(pctldev);
    if (error)
        return ERR_PTR(error);
    return pctldev;
}

这个函数用于注册并启用pinctrl 设备。以下是对每个部分的详细解释:

(1)第8-10 行:调用pinctrl_init_controller 函数初始化pinctrl 控制器。该函数接受pinctrl_desc 结构体、设备指针dev 和驱动程序数据driver_data 作为参数,并返回一个指向已初始化的pinctrl_dev 结构体的指针。
(2)第13-15 行:调用pinctrl_enable 函数启用pinctrl 控制器。该函数接受一个pinctrl_dev结构体指针作为参数,并返回一个代表错误码的整数值。

至此,关于rk3568 的pinctrl probe 函数的分析就完成了,这个时候可能大家还是感觉乱乱的,大家不要着急,会在下面的章节中继续填充pinctrl 子系统的框架。

第122 章pinctrl 子系统函数操作集

再上一章中,我们对pinctrl 的probe 函数进行了详细的讲解,probe 函数的实际作用就是注册并启用pinctrl 设备,pinctrl 设备由pinctrl_desc 结构体所描述,所以在probe 函数中会对pinctrl_desc 结构体中的内容进行填充,具体可以见“121.3 pinctrl probe 函数分析”小节,在本章中将对pinctrl_desc 结构体中的三个函数操作集进行详细的讲解。

122.1 groups 和function

在Pinctrl 子系统中,有两个关键概念:引脚组(groups)和功能(function),在介绍pinctrl子系统函数操作集之前,首先对groups 和function 进行讲解。

1.引脚组(Groups)

引脚组是一组具有相似功能、约束条件或共同工作的引脚的集合。每个引脚组通常与特定的硬件功能或外设相关联。例如,一个引脚组可以用于控制串行通信接口(如UART 或SPI),另一个引脚组可以用于驱动GPIO。

2.功能(Function):

定义了芯片上具有外设功能的功能。每个功能节点对应于一个或多个IO 组(group)的配置信息。这些功能可以是串口、SPI、I2C 等外设功能。
接下来以rk3568-pinctrl.dtsi 设备树文件中的can0 和can1 两个功能为例对上面的内容进行举例,具体内容如下所示:

can0 {
    /omit-if-no-ref/
    can0m0_pins: can0m0-pins {
        rockchip,pins =
            /* can0_rxm0 */
            <0 RK_PB4 2 &pcfg_pull_none>,
            /* can0_txm0 */
            <0 RK_PB3 2 &pcfg_pull_none>;
    };
    
    /omit-if-no-ref/
    can0m1_pins: can0m1-pins {
        rockchip,pins =
            /* can0_rxm1 */
            <2 RK_PA2 4 &pcfg_pull_none>,
            /* can0_txm1 */
            <2 RK_PA1 4 &pcfg_pull_none>;
    };
};

can1 {
    /omit-if-no-ref/
    can1m0_pins: can1m0-pins {
    rockchip,pins =
        /* can1_rxm0 */
        <1 RK_PA0 3 &pcfg_pull_none>,
        /* can1_txm0 */
        <1 RK_PA1 3 &pcfg_pull_none>;
    };
    
    /omit-if-no-ref/
    can1m1_pins: can1m1-pins {
        rockchip,pins =
            /* can1_rxm1 */
            <4 RK_PC2 3 &pcfg_pull_none>,
            /* can1_txm1 */
            <4 RK_PC3 3 &pcfg_pull_none>;
    };
};

在上面的设备树中,can0 和can1 对应两个不同的function,分别为CAN0 控制器和CAN1 控制器。每个控制器中又都有两个不同的groups 引脚组。

(1)CAN0 控制器:
引脚组can0m0-pins:这是CAN0 控制器的第一个引脚组,用于配置CAN0 的引脚。它定义了两个引脚:RK_PB4 和RK_PB3。其中,RK_PB4 用于CAN0 的接收引脚(can0_rxm0),RK_PB3 用于CAN0 的发送引脚(can0_txm0)。

​ 引脚组can0m1-pins:这是CAN0 控制器的第二个引脚组,也用于配置CAN0 的引脚。它定义了两个引脚:RK_PA2 和RK_PA1。其中,RK_PA2 用于CAN0 的接收引脚(can0_rxm1),RK_PA1 用于CAN0 的发送引脚(can0_txm1)。

(2)CAN1 控制器:
引脚组can1m0-pins:这是CAN1 控制器的第一个引脚组,用于配置CAN1 的引脚。它定义了两个引脚:RK_PA0 和RK_PA1。其中,RK_PA0 用于CAN1 的接收引脚(can1_rxm0),RK_PA1 用于CAN1 的发送引脚(can1_txm0)。

​ 引脚组can1m1-pins:这是CAN1 控制器的第二个引脚组,也用于配置CAN1 的引脚。它定义了两个引脚:RK_PC2 和RK_PC3。其中,RK_PC2 用于CAN1 的接收引脚(can1_rxm1),RK_PC3 用于CAN1 的发送引脚(can1_txm1)。

122.2 函数操作集结构体讲解

在pinctrl_desc 结构体中总共有三个函数操作集,具体内容如下所示:

const struct pinctrl_ops *pctlops; 		// 引脚控制操作函数指针
const struct pinmux_ops *pmxops; 		// 引脚复用操作函数指针
const struct pinconf_ops *confops; 		// 引脚配置操作函数指针

本小节将会对上述函数操作集进行介绍

(1)pinctrl_ops
pinctrl_ops 结构体定义在内核源码的“include/linux/pinctrl/pinctrl.h”文件中,具体内容如下所示:

struct pinctrl_ops {
    int (*get_groups_count)(struct pinctrl_dev *pctldev); //获取指定的Pin Control 设备支持的引脚组数量
    const char *(*get_group_name)(struct pinctrl_dev *pctldev,
                                  unsigned selector);// 获取指定引脚组选择器对应的引脚组名称
    
    int (*get_group_pins)(struct pinctrl_dev *pctldev,unsigned selector,const unsigned **pins,
                          unsigned *num_pins);// 获取指定引脚组选择器对应的引脚组中的引脚列表
    
    void (*pin_dbg_show)(struct pinctrl_dev *pctldev, struct seq_file *s,
                         unsigned offset);//在调试信息中输出指定引脚选择器对应的引脚信息
    
    int (*dt_node_to_map)(struct pinctrl_dev *pctldev,struct device_node *np_config,               struct pinctrl_map **map, unsigned *num_maps);// 根据给定的设备树节点,创建与之相关联的Pin Control 映射
    
    void (*dt_free_map)(struct pinctrl_dev *pctldev,struct pinctrl_map *map, 
                        unsigned num_maps);//释放之前通过dt_node_to_map 创建的Pin Control 映射
};

pinctrl_ops 结构体定义了一组操作函数,用于与Pin Control 设备交互。以下是对每个操作函数的简要说明:

(1)get_groups_count:获取引脚组的数量。
(2)get_group_name:获取引脚组的名称。
(3)get_group_pins:获取引脚组的引脚列表。
(4)pin_dbg_show:在调试信息中输出引脚信息。
(5)dt_node_to_map:根据设备树节点创建与之相关联的Pin Control 映射。
(6)dt_free_map:释放通过dt_node_to_map 创建的Pin Control 映射。

这些操作函数用于对引脚控制器进行配置和管理,例如获取引脚组和引脚信息,以及创建和释放与设备树节点相关的引脚映射。

(2)pinmux_ops
pinmux_ops 结构体定义在内核源码的“include/linux/pinctrl/pinctrl.h”文件中,具体内容如下所示:

struct pinmux_ops {
    int (*request) (struct pinctrl_dev *pctldev, unsigned offset);//查看是否可以将某个引脚设置为可用于复用
    int (*free) (struct pinctrl_dev *pctldev, unsigned offset);//request() 回调的反向函数,在请求后释放引脚
    int (*get_functions_count) (struct pinctrl_dev *pctldev);//返回此Pinmux 驱动程序中可选择的命名函数数量
    const char *(*get_function_name) (struct pinctrl_dev *pctldev,unsigned selector);//返回复用选择器的函数名称,核心调用此函数来确定应将某个设备映射到哪个复用设置
    int (*get_function_groups) (struct pinctrl_dev *pctldev,unsigned selector,const char * const **groups,unsigned *num_groups);//返回与某个函数选择器相关联的一组组名称(依次引用引脚)
    
    int (*set_mux) (struct pinctrl_dev *pctldev, unsigned func_selector,unsigned group_selector);//使用特定引脚组启用特定复用功能
    
    int (*gpio_request_enable) (struct pinctrl_dev *pctldev,struct pinctrl_gpio_range *range,
                                unsigned offset);//在特定引脚上请求并启用GPIO
    
    void (*gpio_disable_free) (struct pinctrl_dev *pctldev,struct pinctrl_gpio_range *range,
                               unsigned offset);//在特定引脚上释放GPIO 复用
    
    int (*gpio_set_direction) (struct pinctrl_dev *pctldev,struct pinctrl_gpio_range *range,
                               unsigned offset,bool input);//根据GPIO 配置为输入或输出而进行不同的配置
    
    bool strict;//不允许将同一引脚同时用于GPIO 和其他功能。在批准引脚请求之前,严格检查gpio_owner 和mux_owner
};

struct pinmux_ops 是一个用于描述引脚复用操作的结构体。它定义了一组函数指针,这些函数指针指向了引脚控制器驱动程序中实现的具体功能。下面是对该结构体成员的详细解释:

(1)request: 判断是否可以将某个引脚设置为可用于复用。
(2)free: 该函数是request()函数的反向操作,用于在引脚请求后释放引脚。
(3)get_functions_count: 返回在该引脚控制器驱动程序中可选择的命名函数的数量。
(4)get_function_name: 返回复用选择器的函数名称
(5)get_function_groups: 返回与某个函数选择器相关联的一组组名称(依次引用引脚)。
(6)set_mux: 用于启用特定的复用功能。
(7)gpio_request_enable: 在特定引脚上请求并启用GPIO。
(8)gpio_disable_free: 释放特定引脚上的GPIO 复用。
(9)gpio_set_direction: 根据GPIO 配置将引脚设置为输入或输出。
(10)strict: 表示是否严格检查将同一引脚同时用于GPIO 和其他功能的情况。在批准引脚请求之前,会严格检查gpio_ownermux_owner。如果设置为true,则不允许同时使用同一引脚作为GPIO 和其他功能;如果设置为false,则允许同时使用。

pinmux_ops 结构体提供了一组函数指针,这些函数指针定义了引脚复用操作的各个方面。通过实现这些函数,引脚控制器驱动程序可以与核心交互,并提供引脚复用的功能。核心可以通过调用这些函数来请求、释放引脚,设置复用功能,操作GPIO 等。这个结构体的设计允许引脚控制器驱动程序根据具体的硬件需求和功能定义自己的操作。

(3)pinconf_ops

pinconf_ops 结构体定义在内核源码的“include/linux/pinctrl/pinconf.h”文件中,具体内容如下所示:

struct pinconf_ops {
    
#ifdef CONFIG_GENERIC_PINCONF
    bool is_generic; // 是否为通用引脚配置操作
#endif
    
    int (*pin_config_get) (struct pinctrl_dev *pctldev,unsigned pin,
                           unsigned long *config); // 获取引脚配置信息
    int (*pin_config_set) (struct pinctrl_dev *pctldev,unsigned pin,unsigned long *configs,
                           unsigned num_configs); // 设置引脚配置信息
    int (*pin_config_group_get) (struct pinctrl_dev *pctldev,unsigned selector,
                                 unsigned long *config); // 获取引脚组配置信息
    int (*pin_config_group_set) (struct pinctrl_dev *pctldev,unsigned selector,
                                 unsigned long *configs,unsigned num_configs); // 设置引脚组配置信息
    int (*pin_config_dbg_parse_modify) (struct pinctrl_dev *pctldev,const char *arg,
                                        unsigned long *config); // 解析和修改引脚配置的调试函数
    void (*pin_config_dbg_show) (struct pinctrl_dev *pctldev,struct seq_file *s,
                                 unsigned offset); // 调试函数,显示引脚配置信息
    void (*pin_config_group_dbg_show) (struct pinctrl_dev *pctldev,struct seq_file *s,
                                       unsigned selector); // 调试函数,显示引脚组配置信息
    void (*pin_config_config_dbg_show) (struct pinctrl_dev *pctldev,struct seq_file *s,
                                        unsigned long config); // 调试函数,显示引脚配置的具体信息
};

(1)is_generic: 一个布尔值,表示是否为通用引脚配置操作。在配置文件中定义了CONFIG_GENERIC_PINCONF 时可用。
(2)pin_config_get: 获取引脚配置信息。
(3)pin_config_set: 设置引脚配置信息。
(4)pin_config_group_get: 获取引脚组配置信息。
(5)pin_config_group_set: 设置引脚组配置信息。
(6)pin_config_dbg_parse_modify: 解析和修改引脚配置的调试函数。
(7)pin_config_dbg_show: 显示引脚配置信息的调试函数。
(8)pin_config_group_dbg_show: 显示引脚组配置信息的调试函数。
(9)pin_config_config_dbg_show: 显示引脚配置具体信息的调试函数。

结构体pinconf_ops,用于定义引脚配置操作的函数指针。每个函数指针都对应了一个特定的操作,如获取引脚配置、设置引脚配置、获取引脚组配置等。这些函数在驱动程序中实现,用于对硬件引脚进行配置和控制。

122.3 rockchip_pinctrl 进一步分析

在讲解三个函数操作集的具体实现之前,需要进一步分析rockchip_pinctrl 结构体,在121.2小节中已经对rockchip_pinctrl 结构体进行了简单的讲解,rockchip_pinctrl 结构体是瑞芯微为了适应瑞芯微芯片的特定需求和功能,对struct pinctrl_desc 进行的再一次封装,具体内容如下所示:

struct rockchip_pinctrl {
    struct regmap *regmap_base; // 基本寄存器映射指针
    int reg_size; // 寄存器大小
    struct regmap *regmap_pull; // 拉取寄存器映射指针
    struct regmap *regmap_pmu; // 电源管理单元寄存器映射指针
    struct device *dev; // 设备指针
    struct rockchip_pin_ctrl *ctrl; // 瑞芯微芯片引脚控制器指针
    struct pinctrl_desc pctl; // 引脚控制器描述符
    struct pinctrl_dev *pctl_dev; // 引脚控制器设备指针
    struct rockchip_pin_group *groups; // 瑞芯微芯片引脚组指针
    unsigned int ngroups; // 引脚组数量
    struct rockchip_pmx_func *functions; // 瑞芯微芯片引脚功能指针
    unsigned int nfunctions; // 引脚功能数量
};

本小节主要对最后四个结构体参数groups、ngroups、functions 和nfunctions 的具体作用进行讲解。

在pinctrl 的probe 函数中,首先定义了一个struct rockchip_pinctrl *类型的结构体指针类型的变量info,然后传入了rockchip_pinctrl_register 函数,然后又分别传入了rockchip_pinctrl_parse_dt 函数和devm_pinctrl_register 函数。rockchip_pinctrl_parse_dt 函数稍后再进行讲解,参数传入devm_pinctrl_register 函数函数之后又作为私有数据传入到了pinctrl_register 函数,pinctrl_register 函数内容如下所示:

struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,struct device *dev, 
                                     void *driver_data)
{
    struct pinctrl_dev *pctldev;
    int error;
    
    // 初始化pinctrl 控制器
    pctldev = pinctrl_init_controller(pctldesc, dev, driver_data);
    if (IS_ERR(pctldev))
    	return pctldev;
    
    // 启用pinctrl 控制器
    error = pinctrl_enable(pctldev);
    if (error)
        return ERR_PTR(error);
    
    return pctldev;
}

该函数的作用是注册一个引脚控制设备,用pinctrl_dev 来表示,上面提到的私有数据会传输到pinctrl_init_controller 函数进行初始化和构建pinctrl_dev 结构体,然后跳转到pinctrl_init_controller 的函数定义,具体内容如下所示:

static struct pinctrl_dev *pinctrl_init_controller(struct pinctrl_desc *pctldesc, 
                                                   struct device *dev,void*driver_data)
{
    struct pinctrl_dev *pctldev;
    int ret;
    if (!pctldesc)
    	return ERR_PTR(-EINVAL);
    if (!pctldesc->name)
    	return ERR_PTR(-EINVAL);
    
    pctldev = kzalloc(sizeof(*pctldev), GFP_KERNEL);
    if (!pctldev)
    	return ERR_PTR(-ENOMEM);
    
    /* 初始化引脚控制设备结构体*/
    pctldev->owner = pctldesc->owner; // 设置所有者
    pctldev->desc = pctldesc; // 设置描述符
    pctldev->driver_data = driver_data; // 设置驱动程序数据
    INIT_RADIX_TREE(&pctldev->pin_desc_tree, GFP_KERNEL); // 初始化引脚描述符树
    
#ifdef CONFIG_GENERIC_PINCTRL_GROUPS
    INIT_RADIX_TREE(&pctldev->pin_group_tree, GFP_KERNEL); // 初始化引脚组树
#endif
    
#ifdef CONFIG_GENERIC_PINMUX_FUNCTIONS
    INIT_RADIX_TREE(&pctldev->pin_function_tree, GFP_KERNEL); // 初始化引脚功能树
#endif
    
    INIT_LIST_HEAD(&pctldev->gpio_ranges); // 初始化GPIO 范围链表
    INIT_LIST_HEAD(&pctldev->node); // 初始化节点链表
    pctldev->dev = dev; // 设置设备指针
    mutex_init(&pctldev->mutex); // 初始化互斥锁
    
    /* 检查核心操作函数的有效性*/
    ret = pinctrl_check_ops(pctldev);
    if (ret) {
        dev_err(dev, "pinctrl ops lacks necessary functions\n");
        goto out_err;
    }
    
    /* 如果实现了引脚复用功能,检查操作函数的有效性*/
    if (pctldesc->pmxops) {
        ret = pinmux_check_ops(pctldev);
        if (ret)
            goto out_err;
    }
    
    /* 如果实现了引脚配置功能,检查操作函数的有效性*/
    if (pctldesc->confops) {
        ret = pinconf_check_ops(pctldev);
        if (ret)
            goto out_err;
    }
    
    /* 注册所有引脚*/
    dev_dbg(dev, "try to register %d pins ...\n", pctldesc->npins);
    ret = pinctrl_register_pins(pctldev, pctldesc->pins, pctldesc->npins);
    if (ret) {
        dev_err(dev, "error during pin registration\n");
        pinctrl_free_pindescs(pctldev, pctldesc->pins,pctldesc->npins);
        goto out_err;
    }
    return pctldev;
    
out_err:
    mutex_destroy(&pctldev->mutex);
    kfree(pctldev);
    return ERR_PTR(ret);
}

在该函数中我们要关注的是第19 行内容pctldev->driver_data = driver_data,其中右值driver_data 是从pinctrl 函数的probe 一步一步传递过来的,是一个struct rockchip_pinctrl *类型的结构体指针变量,左值pctldev 为要注册的引脚控制设备(pin controller device),至此两个数据结构建立起了关联,可以通过pctldev 来对rockchip_pinctrl 中的数据进行访问。

接下来分析上面提到的rockchip_pinctrl_parse_dt 函数,rockchip_pinctrl_parse_dt 函数的具体内容如下所示:

static int rockchip_pinctrl_parse_dt(struct platform_device *pdev,struct rockchip_pinctrl *info)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct device_node *child;
    int ret;
    int i;
    
    // 计算子节点数量并更新info 结构体中的计数器
    rockchip_pinctrl_child_count(info, np);
    dev_dbg(&pdev->dev, "nfunctions = %d\n", info->nfunctions);
    dev_dbg(&pdev->dev, "ngroups = %d\n", info->ngroups);
    
    // 为函数和组分配内存空间
    info->functions = devm_kcalloc(dev,info->nfunctions,sizeof(struct rockchip_pmx_func),
                                   GFP_KERNEL);
    if (!info->functions)
        return -ENOMEM;
    info->groups = devm_kcalloc(dev,info->ngroups,sizeof(struct rockchip_pin_group),GFP_KERNEL);
    if (!info->groups)
        return -ENOMEM;
    
    i = 0;
    // 遍历每个子节点,解析函数信息
    for_each_child_of_node(np, child) {
        // 如果节点不是函数节点,则继续下一个节点
        if (!is_function_node(child))
            continue;
        // 解析函数信息并存储到info 结构体中
        ret = rockchip_pinctrl_parse_functions(child, info, i++);
        if (ret) {
            dev_err(&pdev->dev, "failed to parse function\n");
            of_node_put(child);
            return ret;
        }
    }
    return 0;
}
  • (1)第10-14 行:调用函数rockchip_pinctrl_child_count,计算设备节点np 的子节点数量,并更新info 结构体中的计数器,最后打印function 和组的数量。
  • (2)第17-22 行:使用devm_kcalloc 函数为函数分配内存空间,将函数数量乘以每个函数的大小sizeof(struct rockchip_pmx_func),并将指针保存在info->functions 中。
  • (3)第24-29 行:使用devm_kcalloc 函数为组分配内存空间,将组数量乘以每个组的大小sizeof(struct rockchip_pin_group),并将指针保存在info->groups 中。
  • (4)第33-46 行使用for_each_child_of_node 宏遍历设备节点np 的每个子节点child。如果节点child 不是函数节点,则跳过当前节点,继续遍历下一个节点,在每一个循环中调用函数rockchip_pinctrl_parse_functions,解析函数信息,并将结果存储在info 结构体中的相应位置。

这里首先对rockchip_pinctrl_child_count 函数进行讲解,该函数内容如下所示:

static void rockchip_pinctrl_child_count(struct rockchip_pinctrl *info,struct device_node *np)
{
    struct device_node *child;
    // 遍历设备节点的子节点
    for_each_child_of_node(np, child) {
        // 如果子节点不是function 节点,则跳过当前节点,继续遍历下一个节点
        if (!is_function_node(child))
            continue;
        
        // 子节点是function 节点,增加function 计数器
        info->nfunctions++;
        
        // 获取子节点的子节点数量,并增加到组计数器中
        info->ngroups += of_get_child_count(child);
    }
}

在第一个小节中已经讲解了function 和groups,在上面的函数中首先会遍历function,从而得到系统设备树中实际使用的function 数量,保存到nfunctions 参数中,而groups 变量保存的是function 中的引脚组数量,需要通过遍历function 子节点来进行获取,到这里就可以确定rockchip_pinctrl 结构体中的nfunctions 用来记录function 数量,ngroups 用来记录groups 数量
然后来看rockchip_pinctrl_parse_dt 函数的第40 行rockchip_pinctrl_parse_functions 函数,通过该函数可以解析函数信息,该函数的具体内容如下所示:

static int rockchip_pinctrl_parse_functions(struct device_node *np,struct rockchip_pinctrl *info,
                                            u32 index)
{
    struct device_node *child;
    struct rockchip_pmx_func *func;
    struct rockchip_pin_group *grp;
    int ret;
    static u32 grp_index;
    u32 i = 0;
    
    // 打印调试信息,显示正在解析的函数节点和索引
    dev_dbg(info->dev, "parse function(%d): %pOFn\n", index, np);
    
    // 获取当前函数在info->functions 数组中的指针
    func = &info->functions[index];
    
    /* 初始化函数*/
    func->name = np->name;
    
    // 获取函数节点的子节点数量,即关联的组数量
    func->ngroups = of_get_child_count(np);
    if (func->ngroups <= 0)
        return 0;
    
    // 为函数的组指针数组分配内存空间
    func->groups = devm_kcalloc(info->dev,func->ngroups, sizeof(char *), GFP_KERNEL);
    if (!func->groups)
        return -ENOMEM;
    
    // 遍历函数节点的每个子节点
    for_each_child_of_node(np, child) {
        // 将子节点的名称存储到函数的组指针数组中
        func->groups[i] = child->name;
        
        // 获取info->groups 数组中的对应组指针
        grp = &info->groups[grp_index++];
        
        // 解析组信息,并将结果存储到对应的组指针中
        ret = rockchip_pinctrl_parse_groups(child, grp, info, i++);
        if (ret) {
            of_node_put(child);
            return ret;
        }
    }
    return 0;
}

第6-7 行:定义了struct rockchip_pmx_func *类型的变量funcstruct rockchip_pin_group*类型的变量grp,分别用来存放function 信息和groups 信息。
第16 行:将info 的functions 地址赋值给了func,所以rockchip_pinctrl 的functions 参数的作用就是用来存放pinctrl 设备树中的function 信息。
第37 行:将info 的groups 地址赋值给了grp,所以rockchip_pinctrl 的groups 参数的作用就是用来存放pinctrl 设备树中的groups 信息。
在第40 行:使用rockchip_pinctrl_parse_groups 函数来解析组信息,并将结果存储到对应的组指针中。

接下来跳转到rockchip_pinctrl_parse_groups 函数的定义,该函数的具体内容如下所示:

static int rockchip_pinctrl_parse_groups(struct device_node *np,struct rockchip_pin_group *grp,
                                         struct rockchip_pinctrl *info,u32 index)
{
    struct rockchip_pin_bank *bank;
    int size;
    const __be32 *list;
    int num;
    int i, j;
    int ret;
    // 打印调试信息,显示正在解析的组节点和索引
    dev_dbg(info->dev, "group(%d): %pOFn\n", index, np);
    // 初始化组信息
    grp->name = np->name;
    
    /*
    * 绑定格式为rockchip,pins = <bank pin mux CONFIG>,
    * 进行合法性检查并计算引脚数量
    */
    list = of_get_property(np, "rockchip,pins", &size);
    
    // 对返回值不进行检查,因为传递的节点是安全的
    size /= sizeof(*list);
    if (!size || size % 4) {
    dev_err(info->dev, "wrong pins number or pins and configs should be by 4\n");
        return -EINVAL;
    }
    
    // 计算组的引脚数量
    grp->npins = size / 4;
    
    // 为组的引脚数组和数据数组分配内存空间
    grp->pins = devm_kcalloc(info->dev, grp->npins, sizeof(unsigned int),GFP_KERNEL);
    grp->data = devm_kcalloc(info->dev,grp->npins,sizeof(struct rockchip_pin_config),GFP_KERNEL);
    if (!grp->pins || !grp->data)
        return -ENOMEM;
    
    // 遍历列表中的每个元素,每4 个元素表示一个引脚的信息
    for (i = 0, j = 0; i < size; i += 4, j++) {
        const __be32 *phandle;
        struct device_node *np_config;
        
        // 获取管脚号
        num = be32_to_cpu(*list++);
        bank = bank_num_to_bank(info, num);
        if (IS_ERR(bank))
            return PTR_ERR(bank);
        
        // 计算引脚编号
        grp->pins[j] = bank->pin_base + be32_to_cpu(*list++);
        
        // 获取引脚对应的功能选择值
        grp->data[j].func = be32_to_cpu(*list++);
        
        // 获取与引脚相关的配置信息
        phandle = list++;
        if (!phandle)
            return -EINVAL;
        
        // 通过句柄查找配置节点
        np_config = of_find_node_by_phandle(be32_to_cpup(phandle));
        
        // 解析配置信息,并将结果存储到组的数据数组中
        ret = pinconf_generic_parse_dt_config(np_config, NULL,&grp->data[j].configs, &grp->data[j].nconfigs);
        if (ret)
            return ret;
    }
    return 0;
}
  • 第17 行:初始化组信息,将引脚组的名称设置为节点的名称。
  • 第23-29 行:通过of_get_property 函数获取引脚组节点的属性”rockchip,pins”的值,该属性的格式为”bank pin mux CONFIG”。对返回的属性值进行合法性检查,并计算引脚数量。如果属性值为空或者数量不是4 的倍数,将返回错误代码EINVAL。
  • 第31-42 行:首先计算得到的引脚数量,然后根据计算得到的引脚数量为引脚数组和数据数组分配内存空间,这些数组将用于存储引脚的编号和相关的配置信息。如果内存分配失败,函数将返回错误代码ENOMEM。
  • 第45 行:遍历属性值列表中的每个元素,每4 个元素表示一个引脚的信息。
  • 第50-53 行:首先从列表中获取银行号,并使用bank_num_to_bank 函数将引脚号转换为对应的引脚结构体指针。如果转换失败,函数将返回相应的错误代码。
  • 第56 行:根据引脚结构体中的引脚基地址(pin_base)和列表中的值计算引脚的编号,并将其存储在引脚数组(grp->pins)中。
  • 第58 行:从列表中获取与当前引脚相关的功能选择值,并将其存储在数据数组(grp->data)中的相应位置。
  • 第60-72 行从列表中获取与当前引脚相关的配置信息的句柄,并通过该句柄查找对应的配置节点(np_config)。然后,函数使用pinconf_generic_parse_dt_config 解析配置信息,并将解析结果存储在数据数组的相应位置。

至此,关于rockchip_pinctrl 结构体的groups、ngroups、functions 和nfunctions 参数的具
体作用就从源码的角度讲解完成了。

122.4 函数操作集的具体实现

在上面的小节中对函数操作集和一些前置知识进行了讲解,在本小节将会对瑞芯微填充完成的函数操作集具体内容进行介绍。
首先找到内核源码目录下的“ /drivers/pinctrl/pinctrl-rockchip.c ” 文件, 在rockchip_pinctrl_register 函数中对三个函数操作集进行了填充,具体内容如下所示:

ctrldesc->pctlops = &rockchip_pctrl_ops;
ctrldesc->pmxops = &rockchip_pmx_ops;
ctrldesc->confops = &rockchip_pinconf_ops;

接下来对每一个函数操作集的具体实现进行讲解。

122.4.1 pinctrl_ops

在122.2 小节已经对pinctrl_ops 函数操作集结构体进行了讲解,瑞芯微在源码中实现了其中五个函数,具体内容如下所示:

static const struct pinctrl_ops rockchip_pctrl_ops = {
    .get_groups_count = rockchip_get_groups_count, // 获取引脚组数量的函数
    .get_group_name = rockchip_get_group_name,// 获取引脚组名称的函数
    .get_group_pins = rockchip_get_group_pins,// 获取引脚组引脚列表的函数
    .dt_node_to_map = rockchip_dt_node_to_map,// 将设备树节点转换为引脚控制器映射的函数
    .dt_free_map = rockchip_dt_free_map, // 释放引脚控制器映射资源的函数
};

接下来是五个函数的具体内容:

(1)rockchip_get_groups_count

static int rockchip_get_groups_count(struct pinctrl_dev *pctldev)
{
    // 从pinctrl_dev 结构中获取私有数据指针,将其转换为rockchip_pinctrl 结构
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    // 返回rockchip_pinctrl 结构中存储的引脚组数量
    return info->ngroups;
}

(2)rockchip_get_group_name

static const char *rockchip_get_group_name(struct pinctrl_dev *pctldev,unsigned selector)
{
    // 从pinctrl_dev 结构中获取私有数据指针,将其转换为rockchip_pinctrl 结构
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    // 返回指定引脚组的名称
    return info->groups[selector].name;
}

(3)rockchip_get_group_pins

static int rockchip_get_group_pins(struct pinctrl_dev *pctldev,unsigned selector, const unsigned **pins,unsigned *npins)
{
    // 从pinctrl_dev 结构中获取私有数据指针,将其转换为rockchip_pinctrl 结构
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    // 如果选择器超出引脚组的范围,则返回错误码-EINVAL
    if (selector >= info->ngroups)
        return -EINVAL;
    // 将指向引脚组的引脚数组的指针赋值给传入的pins 指针
    *pins = info->groups[selector].pins;
    // 将引脚组中的引脚数量赋值给传入的npins 变量
    *npins = info->groups[selector].npins;
    // 返回成功的状态码0
    return 0;
}

(4)rockchip_dt_node_to_map

static int rockchip_dt_node_to_map(struct pinctrl_dev *pctldev,struct device_node *np,
                                   struct pinctrl_map **map, unsigned *num_maps)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev); // 获取引脚控制器的私有数据指针
    const struct rockchip_pin_group *grp; // 引脚组指针
    struct device *dev = info->dev; // 设备指针
    struct pinctrl_map *new_map; // 新的引脚映射数组
    struct device_node *parent; // 父节点指针
    int map_num = 1; // 映射数量,默认为1
    int i;
    /* 查找引脚组*/
    grp = pinctrl_name_to_group(info, np->name); // 根据节点名称查找对应的引脚组
    if (!grp) {
        dev_err(dev, "unable to find group for node %pOFn\n", np); // 如果找不到引脚组,打印错误信息
        return -EINVAL;
    }
    map_num += grp->npins; // 计算映射数量,包括复用映射和配置映射
    new_map = kcalloc(map_num, sizeof(*new_map), GFP_KERNEL); // 分配内存空间用于存储映射数组
    if (!new_map)
        return -ENOMEM;
    *map = new_map; // 将分配的映射数组赋值给输出参数
    *num_maps = map_num; // 将映射数量赋值给输出参数

    /* 创建复用映射*/
    parent = of_get_parent(np); // 获取节点的父节点
    if (!parent) {
        kfree(new_map); // 如果父节点不存在,释放分配的映射数组内存空间
        return -EINVAL;
    }
    new_map[0].type = PIN_MAP_TYPE_MUX_GROUP; // 设置映射类型为复用映射
    new_map[0].data.mux.function = parent->name; // 复用功能名称为父节点的名称
    new_map[0].data.mux.group = np->name; // 引脚组名称为节点的名称
    of_node_put(parent); // 释放父节点的引用计数
    
    /* 创建配置映射*/
    new_map++; // 映射数组指针向后移动一个位置
    for (i = 0; i < grp->npins; i++) {
        new_map[i].type = PIN_MAP_TYPE_CONFIGS_PIN; // 设置映射类型为配置映射
        new_map[i].data.configs.group_or_pin =
        pin_get_name(pctldev, grp->pins[i]); // 引脚组或引脚名称为引脚组中的引脚名称
        new_map[i].data.configs.configs = grp->data[i].configs; // 配置信息数组为引脚组中该引脚的配置信息
        new_map[i].data.configs.num_configs = grp->data[i].nconfigs; // 配置信息数量为引脚组中该引脚的配置数量
    }
    dev_dbg(dev, "maps: function %s group %s num %d\n",
    (*map)->data.mux.function, (*map)->data.mux.group, map_num); // 打印调试信息,显示创建的引脚
    映射的功能名称、组名和数量
    return 0; // 返回成功标志
}

(5)rockchip_dt_free_map

static void rockchip_dt_free_map(struct pinctrl_dev *pctldev,struct pinctrl_map *map, 
                                 unsigned num_maps)
{
    kfree(map); // 释放引脚控制器映射的内存块
}

122.4.2 pinmux_ops

在122.2 小节已经对pinmux_ops 函数操作集结构体进行了讲解,瑞芯微在源码中实现了其中的四个函数,具体内容如下所示:

static const struct pinmux_ops rockchip_pmx_ops = {
    .get_functions_count = rockchip_pmx_get_funcs_count,// 获取引脚复用功能数量的函数
    .get_function_name = rockchip_pmx_get_func_name, // 获取引脚复用功能名称的函数
    .get_function_groups = rockchip_pmx_get_groups,// 获取引脚复用功能对应的引脚组的函数
    .set_mux = rockchip_pmx_set, // 设置引脚复用功能的函数
};

接下来是五个函数的具体内容:

(1)rockchip_pmx_get_funcs_count

static int rockchip_pmx_get_funcs_count(struct pinctrl_dev *pctldev)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    return info->nfunctions; // 返回引脚复用功能的数量
}

(2)rockchip_pmx_get_func_name

static const char *rockchip_pmx_get_func_name(struct pinctrl_dev *pctldev,unsigned selector)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    return info->functions[selector].name; // 返回引脚复用功能的名称
}

(3)rockchip_pmx_get_groups

static int rockchip_pmx_get_groups(struct pinctrl_dev *pctldev,unsigned selector,
                                   const char * const **groups,unsigned * const num_groups)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    *groups = info->functions[selector].groups; // 返回引脚复用功能对应的引脚组数组
    *num_groups = info->functions[selector].ngroups; // 返回引脚组的数量
    return 0; // 返回成功
}

(4)rockchip_pmx_set

static int rockchip_pmx_set(struct pinctrl_dev *pctldev, unsigned selector,
unsigned group)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    const unsigned int *pins = info->groups[group].pins;
    const struct rockchip_pin_config *data = info->groups[group].data;
    struct device *dev = info->dev;
    struct rockchip_pin_bank *bank;
    int cnt, ret = 0;
    dev_dbg(dev, "enable function %s group %s\n",info->functions[selector].name, info->groups[group].name);
    
    /*
    * 针对所选的引脚组中的每个引脚,将相应的引脚功能号码编程到配置寄存器中。
    */
    for (cnt = 0; cnt < info->groups[group].npins; cnt++) {
        bank = pin_to_bank(info, pins[cnt]);
        ret = rockchip_set_mux(bank, pins[cnt] - bank->pin_base,data[cnt].func);
        if (ret)
            break;
    }
    
    if (ret && cnt) {
        /* 恢复已经设置的引脚设置*/
        for (cnt--; cnt >= 0 && !data[cnt].func; cnt--)
            rockchip_set_mux(bank, pins[cnt] - bank->pin_base, 0);
        return ret;
    }
    return 0; // 返回成功
}

122.4.3 pinconf_ops

在122.2 小节已经对pinconf_ops 函数操作集结构体进行了讲解,瑞芯微在源码中实现了其中五个函数,具体内容如下所示:

static const struct pinconf_ops rockchip_pinconf_ops = {
    .pin_config_get = rockchip_pinconf_get, // 获取引脚配置的函数
    .pin_config_set = rockchip_pinconf_set, // 设置引脚配置的函数
    .is_generic = true, // 表示是通用的引脚配置操作
};

接下来是两个函数的具体内容:

(1)rockchip_pinconf_set

static int rockchip_pinconf_set(struct pinctrl_dev *pctldev, unsigned int pin,
                                unsigned long *configs, unsigned num_configs)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    struct rockchip_pin_bank *bank = pin_to_bank(info, pin);
    struct gpio_chip *gpio = &bank->gpio_chip;
    enum pin_config_param param;
    u32 arg;
    int i;
    int rc;
    for (i = 0; i < num_configs; i++) {
        param = pinconf_to_config_param(configs[i]);
        arg = pinconf_to_config_argument(configs[i]);
        if (param == PIN_CONFIG_OUTPUT || param == PIN_CONFIG_INPUT_ENABLE) {
            /*
            * 检查GPIO 驱动程序是否已经探测到。
            * 锁定确保GPIO 探测完成或者GPIO 驱动程序尚未探测到。
            */
            mutex_lock(&bank->deferred_lock);
            if (!gpio || !gpio->direction_output) {
                // 如果驱动程序尚未探测到,则将配置信息延迟处理并返回。
                rc = rockchip_pinconf_defer_pin(bank, pin - bank->pin_base, param, arg);
                mutex_unlock(&bank->deferred_lock);
                if (rc)
                    return rc;
                break;
            }
            mutex_unlock(&bank->deferred_lock);
        }
        switch (param) {
            case PIN_CONFIG_BIAS_DISABLE:
                // 禁用上下拉电阻
                rc = rockchip_set_pull(bank, pin - bank->pin_base, param);
                if (rc)
                return rc;
            break;

            case PIN_CONFIG_BIAS_PULL_UP:
            case PIN_CONFIG_BIAS_PULL_DOWN:
            case PIN_CONFIG_BIAS_PULL_PIN_DEFAULT:
            case PIN_CONFIG_BIAS_BUS_HOLD:
                // 检查上下拉电阻是否有效
                if (!rockchip_pinconf_pull_valid(info->ctrl, param))
                return -ENOTSUPP;
                if (!arg)
                return -EINVAL;
                // 设置上下拉电阻
                rc = rockchip_set_pull(bank, pin - bank->pin_base, param);
                if (rc)
                return rc;
            break;

            case PIN_CONFIG_OUTPUT:
                // 设置引脚复用功能为GPIO
                rc = rockchip_set_mux(bank, pin - bank->pin_base, RK_FUNC_GPIO);
                if (rc != RK_FUNC_GPIO)
                    return -EINVAL;
                // 设置引脚为输出模式
                rc = gpio->direction_output(gpio, pin - bank->pin_base, arg);
                if (rc)
                    return rc;
            break;

            case PIN_CONFIG_INPUT_ENABLE:
                // 设置引脚复用功能为GPIO
                rc = rockchip_set_mux(bank, pin - bank->pin_base, RK_FUNC_GPIO);
                if (rc != RK_FUNC_GPIO)
                    return -EINVAL;
                // 设置引脚为输入模式
                rc = gpio->direction_input(gpio, pin - bank->pin_base);
                if (rc)
                    return rc;
            break;

            case PIN_CONFIG_DRIVE_STRENGTH:
                // 仅支持某些芯片(如rk3288)的每个引脚独立的驱动强度设置
                if (!info->ctrl->drv_calc_reg)
                    return -ENOTSUPP;
                // 设置引脚的驱动强度
                rc = rockchip_set_drive_perpin(bank, pin - bank->pin_base, arg);
                if (rc < 0)
                    return rc;
            break;

            case PIN_CONFIG_INPUT_SCHMITT_ENABLE:
                // 仅支持某些芯片的施密特触发设置
                if (!info->ctrl->schmitt_calc_reg)
                    return -ENOTSUPP;
                // 设置引脚的施密特触发模式
                rc = rockchip_set_schmitt(bank, pin - bank->pin_base, arg);
                if (rc < 0)
                    return rc;
            break;

            case PIN_CONFIG_SLEW_RATE:
                // 仅支持某些芯片的引脚驱动速率设置
                if (!info->ctrl->slew_rate_calc_reg)
                    return -ENOTSUPP;
                // 设置引脚的驱动速率
                rc = rockchip_set_slew_rate(bank, pin - bank->pin_base, arg);
                if (rc < 0)
                    return rc;
            break;

            default:
                // 不支持的配置参数
                return -ENOTSUPP;
        }
    } /* for each config */
    return 0;
}

(2)rockchip_pinconf_get

/* 获取指定引脚的配置设置*/
static int rockchip_pinconf_get(struct pinctrl_dev *pctldev, unsigned int pin,unsigned long *config)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev);
    struct rockchip_pin_bank *bank = pin_to_bank(info, pin);
    struct gpio_chip *gpio = &bank->gpio_chip;
    enum pin_config_param param = pinconf_to_config_param(*config);
    u16 arg;
    int rc;
    switch (param) {
        case PIN_CONFIG_BIAS_DISABLE:
            // 检查上下拉电阻是否禁用
            if (rockchip_get_pull(bank, pin - bank->pin_base) != param)
            return -EINVAL;
            arg = 0;
        break;
        case PIN_CONFIG_BIAS_PULL_UP:
        case PIN_CONFIG_BIAS_PULL_DOWN:
        case PIN_CONFIG_BIAS_PULL_PIN_DEFAULT:
        case PIN_CONFIG_BIAS_BUS_HOLD:
            // 检查上下拉电阻是否有效,并获取当前的上下拉电阻配置
            if (!rockchip_pinconf_pull_valid(info->ctrl, param))
                return -ENOTSUPP;
            if (rockchip_get_pull(bank, pin - bank->pin_base) != param)
                return -EINVAL;
            arg = 1;
        break;
            
        case PIN_CONFIG_OUTPUT:
            // 检查引脚是否配置为GPIO 输出模式
            rc = rockchip_get_mux(bank, pin - bank->pin_base);
            if (rc != RK_FUNC_GPIO)
                return -EINVAL;
            if (!gpio || !gpio->get) {
                arg = 0;
                break;
            }
            // 获取引脚的输出状态
            rc = gpio->get(gpio, pin - bank->pin_base);
            if (rc < 0)
                return rc;
            arg = rc ? 1 : 0;
        break;
            
        case PIN_CONFIG_DRIVE_STRENGTH:
            // 仅支持某些芯片(如rk3288)的每个引脚独立的驱动强度设置
            if (!info->ctrl->drv_calc_reg)
            return -ENOTSUPP;
            // 获取引脚的驱动强度配置
            rc = rockchip_get_drive_perpin(bank, pin - bank->pin_base);
            if (rc < 0)
            return rc;
            arg = rc;
        break;
            
        case PIN_CONFIG_INPUT_SCHMITT_ENABLE:
            // 仅支持某些芯片的施密特触发设置
            if (!info->ctrl->schmitt_calc_reg)
                return -ENOTSUPP;
            // 获取引脚的施密特触发配置
            rc = rockchip_get_schmitt(bank, pin - bank->pin_base);
            if (rc < 0)
                return rc;
            arg = rc;
        break;
            
        case PIN_CONFIG_SLEW_RATE:
            // 仅支持某些芯片的引脚驱动速率设置
            if (!info->ctrl->slew_rate_calc_reg)
                return -ENOTSUPP;
            // 获取引脚的驱动速率配置
            rc = rockchip_get_slew_rate(bank, pin - bank->pin_base);
            if (rc < 0)
                return rc;
            arg = rc;
        break;
            
        default:
            // 不支持的配置参数
            return -ENOTSUPP;
    }
    *config = pinconf_to_config_packed(param, arg);
    return 0;
}

一般情况下SOC 原厂BSP 工程师已经帮我们写好了上述函数,不需要独自进行编写,所以这里这需要简单的了解即可。

第123 章dt_node_to_map 函数分析

设备树(Device Tree)中存放的是对硬件设备信息的描述,包含了硬件设备的配置和连接信息,例如在pinctrl 节点中的引脚的配置和映射关系。而rockchip_dt_node_to_map 函数的作用就是根据设备树中的节点信息,生成对应的引脚映射数组。这个映射数组将描述硬件功能(如复用功能和配置信息)与设备树中的引脚信息进行绑定。

dt_node_to_map 函数已经在122.4.1 小节的pinctrl_ops 函数操作集中进行了简单的讲解,现在将对该函数进行详细的介绍。该函数的具体实现在内核源码的“drivers/pinctrl/pinctrl-rockchip.c”文件中,在讲解该函数之前需要先对struct pinctrl_map 结构体进行介绍,该结构体定义在内核源码的“include/linux/pinctrl/machine.h”目录下,具体内容如下所示:

struct pinctrl_map {
    const char *dev_name; // 设备名称
    const char *name; // 映射名称
    enum pinctrl_map_type type; // 映射类型
    const char *ctrl_dev_name; // 控制设备名称
    union {
        struct pinctrl_map_mux mux; // 复用映射数据
        struct pinctrl_map_configs configs; // 配置映射数据
    } data;
};

该结构体用于在引脚控制器中定义引脚的映射关系。通过映射类型的不同,可以将引脚与具体的复用功能或配置信息关联起来, 从而实现引脚的配置和控制。然后来看rockchip_dt_node_to_map 函数,该函数的具体内容如下所示:

static int rockchip_dt_node_to_map(struct pinctrl_dev *pctldev,struct device_node *np,
                                   struct pinctrl_map **map, unsigned *num_maps)
{
    struct rockchip_pinctrl *info = pinctrl_dev_get_drvdata(pctldev); // 获取引脚控制器的私有数据指针
    const struct rockchip_pin_group *grp; // 引脚组指针
    struct device *dev = info->dev; // 设备指针
    struct pinctrl_map *new_map; // 新的引脚映射数组
    struct device_node *parent; // 父节点指针
    int map_num = 1; // 映射数量,默认为1
    int i;
    
    /* 查找引脚组*/
    grp = pinctrl_name_to_group(info, np->name); // 根据节点名称查找对应的引脚组
    if (!grp) {
        dev_err(dev, "unable to find group for node %pOFn\n", np); // 如果找不到引脚组,打印错误信息
        return -EINVAL;
    }
    
    map_num += grp->npins; // 计算映射数量,包括复用映射和配置映射
    new_map = kcalloc(map_num, sizeof(*new_map), GFP_KERNEL); // 分配内存空间用于存储映射数组
    if (!new_map)
        return -ENOMEM;
    
    *map = new_map; // 将分配的映射数组赋值给输出参数
    *num_maps = map_num; // 将映射数量赋值给输出参数
    
    /* 创建复用映射*/
    parent = of_get_parent(np); // 获取节点的父节点
    if (!parent) {
        kfree(new_map); // 如果父节点不存在,释放分配的映射数组内存空间
        return -EINVAL;
    }
    
    new_map[0].type = PIN_MAP_TYPE_MUX_GROUP; // 设置映射类型为复用映射
    new_map[0].data.mux.function = parent->name; // 复用功能名称为父节点的名称
    new_map[0].data.mux.group = np->name; // 引脚组名称为节点的名称
    of_node_put(parent); // 释放父节点的引用计数
    
    /* 创建配置映射*/
    new_map++; // 映射数组指针向后移动一个位置
    for (i = 0; i < grp->npins; i++) {
        new_map[i].type = PIN_MAP_TYPE_CONFIGS_PIN; // 设置映射类型为配置映射
        new_map[i].data.configs.group_or_pin =pin_get_name(pctldev, grp->pins[i]); // 引脚组或引脚名称为引脚组中的引脚名称
        new_map[i].data.configs.configs = grp->data[i].configs; // 配置信息数组为引脚组中该引脚的配置信息
        new_map[i].data.configs.num_configs = grp->data[i].nconfigs; // 配置信息数量为引脚组中该引脚的配置数量
    }
    dev_dbg(dev, "maps: function %s group %s num %d\n",(*map)->data.mux.function, (*map)->data.mux.group, map_num); // 打印调试信息,显示创建的引脚映射的功能名称、组名和数量
    return 0; // 返回成功标志
}

第14-20 行:函数根据设备节点的名称使用pinctrl_name_to_group 函数查找与该节点对应的引脚组。如果找不到引脚组,则函数打印错误消息并返回EINVAL 错误代码。
第22 行:函数根据引脚组的引脚数量计算需要创建的映射数量。映射数量包括复用映射和配置映射。
第24-26 行:函数使用kcalloc 函数为映射数组(new_map)分配内存空间。分配的大小为映射数量乘以每个映射的大小。如果内存分配失败,函数将返回ENOMEM 错误代码。
第28-29 行:函数将分配的映射数组(new_map)和映射数量(map_num)通过输出参数map 和num_maps 返回给调用者。
第31-40 行:函数首先获取设备节点的父节点,并将其作为复用映射的功能名称。然后,函数设置第一个映射的类型为PIN_MAP_TYPE_MUX_GROUP,并将父节点的名称作为映射的数据。同时,将设备节点的名称作为映射的组名。最后,函数使用of_node_put 释放父节点的引用计数。
第42-52 行:函数遍历引脚组的引脚数组,并为每个引脚创建一个配置映射。函数设置映射的类型为PIN_MAP_TYPE_CONFIGS_PIN,并将引脚的名称作为映射的数据。同时,将引脚组中该引脚的配置信息和配置数量设置为映射的配置数据。函数使用pin_get_name 函数获取引脚的名称。
rockchip_dt_node_to_map 函数根据设备节点的信息创建引脚映射,包括复用映射和配置映射。复用映射用于将引脚组的功能与父节点的功能关联起来,而配置映射用于将引脚的配置信息与引脚的名称关联起来。这些映射将用于配置引脚控制器,以确保引脚在系统中正确地配置和使用。这个函数在设备树解析过程中被调用,以便为每个设备节点创建相应的引脚映射。

到这里关于pinctrl 子系统第一阶段的讲解到这里就结束了,为了帮助大家整合讲解过的知识点,作者绘制了以下思维导图:
注:该思维导图的存放路径为iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\05_思维导图\01_pinctrl 阶段1.jpg

image-20240914114000036

大家可以根据该思维导图,对上面章节的内容进行梳理,从而真正理解pinctrl 子系统框架。

第124 章pinctrl_bind_pins 函数

在前面的章节中对pinctrl 子系统的probe 函数及其相关框架进行了讲解,但在120.2 小节中提出的“引脚的复用关系是在什么时候被设置的”这一问题并没有被解决,那接下来我们要如何继续解决这一问题呢,带着疑问让我们进入本章节的学习吧。

124.1 dev_pin_info 结构体引入

这里以485 控制引脚的设备树节点和对应的pinctrl 设备树节点进行举例,要添加的485设备树内容如下所示:

rk_485_ctl: rk-485-ctl {
    compatible = "topeet,rs485_ctl";
    gpios = <&gpio0 22 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "default";
    pinctrl-0 = <&rk_485_gpio>;
};

&pinctrl {
    rk_485{
        rk_485_gpio:rk-485-gpio {
            rockchip,pins = <3 13 RK_FUNC_GPIO &pcfg_pull_none>;
        };
    };
};

当上面编写的485 设备树跟驱动匹配成功之后,就会进入相应驱动中的probe 函数,在probe 函数中就可以对设备树中描述的485 使能引脚进行拉高和拉低的操作,从而控制485 的接收和发送。所以可以猜测在进入驱动的probe 函数之前就已经使用pinctrl 子系统对引脚进行了复用,在前面设备模型的学习中,我们已经知道了驱动中的probe 函数是在内核源码目录下的“drivers/base/dd.c”文件中加载执行的,然后找到really probe 函数中与probe 函数加载相关的代码,具体内容如下所示:

re_probe:
dev->driver = drv; // 将设备的驱动程序指针设置为当前驱动

/* 如果使用了pinctrl,在探测之前绑定引脚*/
ret = pinctrl_bind_pins(dev); // 绑定设备的引脚
if (ret)
    goto pinctrl_bind_failed;

ret = dma_configure(dev); // 配置DMA
if (ret)
    goto probe_failed;

if (driver_sysfs_add(dev)) { // 添加驱动程序的sysfs 接口
    printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n",__func__, dev_name(dev));
    goto probe_failed;
}

if (dev->pm_domain && dev->pm_domain->activate) {
    ret = dev->pm_domain->activate(dev); // 激活电源管理域
    if (ret)
        goto probe_failed;
}

if (dev->bus->probe) {
    ret = dev->bus->probe(dev); // 如果总线的探测函数存在,则调用总线的探测函数
    if (ret)
        goto probe_failed;
} 
else if (drv->probe) {
    ret = drv->probe(dev); // 否则调用驱动程序的探测函数
    if (ret)
    goto probe_failed;
}

if (test_remove) {
    test_remove = false;
    if (dev->bus->remove) // 如果总线的移除函数存在,则调用总线的移除函数
        dev->bus->remove(dev);
    else if (drv->remove) // 否则调用驱动程序的移除函数
        drv->remove(dev);

    devres_release_all(dev); // 释放设备资源
    driver_sysfs_remove(dev); // 移除驱动程序的sysfs 接口
    dev->driver = NULL;
    dev_set_drvdata(dev, NULL);
    if (dev->pm_domain && dev->pm_domain->dismiss)
        dev->pm_domain->dismiss(dev); // 解除电源管理域的激活状态
    pm_runtime_reinit(dev); // 重新初始化电源管理运行时
    goto re_probe; // 重新进行设备的探测
}

pinctrl_init_done(dev); // 完成引脚控制器的初始化
if (dev->pm_domain && dev->pm_domain->sync)
    dev->pm_domain->sync(dev); // 同步电源管理域

driver_bound(dev); // 驱动程序与设备绑定成功
ret = 1;
pr_debug("bus: '%s': %s: bound device %s to driver %s\n",drv->bus->name, 
         __func__, dev_name(dev), drv->name);
goto done;
done:
// 执行完成后的处理逻辑

根据注释可以了解到,如果使用了pinctrl 就会调用第5 行的pinctrl_bind_pins()函数进行设备引脚的绑定,然后根据线索跳转到pinctrl_bind_pins 函数,该函数定义在内核源码目录下的“drivers/base/pinctrl.c”文件中,函数具体内容如下所示:

int pinctrl_bind_pins(struct device *dev)
{
    int ret;
    // 检查设备是否重用了节点
    if (dev->of_node_reused)
        return 0;
    
    // 为设备的引脚分配内存空间
    dev->pins = devm_kzalloc(dev, sizeof(*(dev->pins)), GFP_KERNEL);
    if (!dev->pins)
        return -ENOMEM;
    
    // 获取设备的pinctrl 句柄
    dev->pins->p = devm_pinctrl_get(dev);
    if (IS_ERR(dev->pins->p)) {
        dev_dbg(dev, "没有pinctrl 句柄\n");
        ret = PTR_ERR(dev->pins->p);
        goto cleanup_alloc;
    }
    
    // 查找设备的默认pinctrl 状态
    dev->pins->default_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_DEFAULT);
    if (IS_ERR(dev->pins->default_state)) {
        dev_dbg(dev, "没有默认的pinctrl 状态\n");
        ret = 0;
        goto cleanup_get;
    }
    
    // 查找设备的初始化pinctrl 状态
    dev->pins->init_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_INIT);
    if (IS_ERR(dev->pins->init_state)) {
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有初始化的pinctrl 状态\n");
        // 选择默认的pinctrl 状态
        ret = pinctrl_select_state(dev->pins->p, dev->pins->default_state);
    } else {
        // 选择初始化的pinctrl 状态
        ret = pinctrl_select_state(dev->pins->p, dev->pins->init_state);
    }
    
    if (ret) {
        dev_dbg(dev, "无法激活初始的pinctrl 状态\n");
        goto cleanup_get;
    }
#ifdef CONFIG_PM
    /*
    * 如果启用了电源管理,我们还会寻找可选的睡眠和空闲的引脚状态,其语义在
    * <linux/pinctrl/pinctrl-state.h> 中定义
    */
    dev->pins->sleep_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_SLEEP);
    if (IS_ERR(dev->pins->sleep_state))
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有睡眠的pinctrl 状态\n");
    
    dev->pins->idle_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_IDLE);
    if (IS_ERR(dev->pins->idle_state))
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有空闲的pinctrl 状态\n");
#endif
    return 0;
    
    /*
    * 如果对于此设备没有找到pinctrl 句柄或默认状态,
    * 让我们明确释放设备中的引脚容器,因为保留它没有意义。
    */
cleanup_get:
    devm_pinctrl_put(dev->pins->p);
cleanup_alloc:
    devm_kfree(dev, dev->pins);
    dev->pins = NULL;
    /* 返回延迟*/
    if (ret == -EPROBE_DEFER)
        return ret;
    
    /* 返回严重错误*/
    if (ret == -EINVAL)
        return ret;
    /* 我们忽略诸如-ENOENT 的错误,表示没有pinctrl 状态*/
    return 0;
}

关于该函数的具体讲解将在下个小节进行,本小节的目的是引出dev_pin_info 结构体,仔细查看上面的函数内容发现并没有该结构体,但是可以发现很多dev->pins 的结构体指针引用,然后跳转到struct device *结构体的定义,具体内容如下所示:

struct device {
    struct device *parent; // 指向父设备的指针
    struct device_private *p; // 私有数据指针
    struct kobject kobj; // 内核对象,用于设备的管理
    const char *init_name; // 设备的初始名称
    const struct device_type *type; // 设备类型
    struct mutex mutex; // 互斥锁,用于同步对驱动程序的调用
    struct bus_type *bus; // 设备所在的总线类型
    struct device_driver *driver; // 分配该设备的驱动程序
    void *platform_data; // 平台特定的数据,设备核心不会修改它
    void *driver_data; // 驱动程序的数据,使用dev_set/get_drvdata 来设置和获取
    ....
        
#ifdef CONFIG_GENERIC_MSI_IRQ_DOMAIN
    struct irq_domain *msi_domain; // 设备的通用MSI IRQ 域
#endif
    
#ifdef CONFIG_PINCTRL
    struct dev_pin_info *pins; // 设备的引脚信息
#endif
......

在第21-23 行有这样一个宏定义,如果定义了CONFIG_PINCTRL 那记录设备引脚信息的结构体struct dev_pin_info *pins 就会被使能,到这里本小节要讲解的重点终于出现了,接下来跳转到struct dev_pin_info 结构体定义,该结构体的具体内容如下所示:

struct dev_pin_info {
    struct pinctrl *p; // 引脚控制器指针
    struct pinctrl_state *default_state; // 默认状态指针
    struct pinctrl_state *init_state; // 初始化状态指针
#ifdef CONFIG_PM
    struct pinctrl_state *sleep_state; // 睡眠状态指针(仅在支持电源管理时可用)
    struct pinctrl_state *idle_state; // 空闲状态指针(仅在支持电源管理时可用)
#endif
};

下面对结构体中的成员变量进行详细介绍:
(1)struct pinctrl *p;:引脚控制器指针。该指针指向设备所使用的引脚控制器对象,用于对设备的引脚进行控制和配置。
(2)struct pinctrl_state *default_state;:默认状态指针。该指针指向设备的默认引脚配置状态,表示设备在正常操作时的引脚配置。
(3)struct pinctrl_state *init_state;:初始化状态指针。该指针指向设备的初始化引脚配置状态,表示设备在初始化阶段的引脚配置。
(4)struct pinctrl_state *sleep_state;:睡眠状态指针(仅在支持电源管理时可用)。该指针指向设备的引脚配置状态,表示设备在进入睡眠状态时的引脚配置。
(5)struct pinctrl_state *idle_state;:空闲状态指针(仅在支持电源管理时可用)。该指针指向设备的引脚配置状态,表示设备在空闲状态时的引脚配置。

这里仍旧以485 的控制引脚进行举例,要添加的485 设备树内容如下所示:

rk_485_ctl: rk-485-ctl {
    compatible = "topeet,rs485_ctl";
    gpios = <&gpio0 22 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "default";
    pinctrl-0 = <&rk_485_gpio>;
};
&pinctrl {
    rk_485{
        rk_485_gpio:rk-485-gpio {
            rockchip,pins = <3 13 RK_FUNC_GPIO &pcfg_pull_none>;
        };
    };
};

其中第4 行的pinctrl-names 属性指定了设备所使用的引脚控制器为default,即第5 行的pinctrl-0,而pinctrl-0 的值为pinctrl 节点中的rk_485_gpio,所以struct pinctrl_state *default_state 这一默认状态结构体指针会用来存放11 行的引脚复用信息,而在上一章节中也提到了设备树中的pinctrl 节点会转换为pinctrl_map 结构体,那么struct pinctrl_state *default_state 必然会跟pinctrl_map 结构体建立联系,那这个联系是如何建立的呢?就让我们一起进入pinctrl_bind_pins 函数的学习吧。

124.2 pinctrl_bind_pins 函数分析1

在本小节中将对pinctrl_bind_pins 函数的具体内容进行详细讲解,该函数的具体内容如下所示:

int pinctrl_bind_pins(struct device *dev)
{
    int ret;
    // 检查设备是否重用了节点
    if (dev->of_node_reused)
        return 0;
    
    // 为设备的引脚分配内存空间
    dev->pins = devm_kzalloc(dev, sizeof(*(dev->pins)), GFP_KERNEL);
    if (!dev->pins)
        return -ENOMEM;
    
    // 获取设备的pinctrl 句柄
    dev->pins->p = devm_pinctrl_get(dev);
    if (IS_ERR(dev->pins->p)) {
        dev_dbg(dev, "没有pinctrl 句柄\n");
        ret = PTR_ERR(dev->pins->p);
        goto cleanup_alloc;
    }
    
    // 查找设备的默认pinctrl 状态
    dev->pins->default_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_DEFAULT);
    if (IS_ERR(dev->pins->default_state)) {
        dev_dbg(dev, "没有默认的pinctrl 状态\n");
        ret = 0;
        goto cleanup_get;
    }
    
    // 查找设备的初始化pinctrl 状态
    dev->pins->init_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_INIT);
    if (IS_ERR(dev->pins->init_state)) {
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有初始化的pinctrl 状态\n");
        // 选择默认的pinctrl 状态
        ret = pinctrl_select_state(dev->pins->p, dev->pins->default_state);
    } else {
        // 选择初始化的pinctrl 状态
        ret = pinctrl_select_state(dev->pins->p, dev->pins->init_state);
    }
    if (ret) {
        dev_dbg(dev, "无法激活初始的pinctrl 状态\n");
        goto cleanup_get;
    }
#ifdef CONFIG_PM
    /*
    * 如果启用了电源管理,我们还会寻找可选的睡眠和空闲的引脚状态,其语义在
    * <linux/pinctrl/pinctrl-state.h> 中定义
    */
    dev->pins->sleep_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_SLEEP);
    if (IS_ERR(dev->pins->sleep_state))
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有睡眠的pinctrl 状态\n");
    
    dev->pins->idle_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_IDLE);
    if (IS_ERR(dev->pins->idle_state))
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有空闲的pinctrl 状态\n");
#endif
    return 0;
    
    /*
    * 如果对于此设备没有找到pinctrl 句柄或默认状态,
    * 让我们明确释放设备中的引脚容器,因为保留它没有意义。
    */
cleanup_get:
    devm_pinctrl_put(dev->pins->p);
cleanup_alloc:
    devm_kfree(dev, dev->pins);
    dev->pins = NULL;
    /* 返回延迟*/
    if (ret == -EPROBE_DEFER)
        return ret;
    
    /* 返回严重错误*/
    if (ret == -EINVAL)
        return ret;
    
    /* 我们忽略诸如-ENOENT 的错误,表示没有pinctrl 状态*/
    return 0;
}

(1)第5-7 行:函数检查设备是否重用了节点(dev->of_node_reused)。如果设备重用了节点,则直接返回0,表示绑定成功。
(2)第9-12 行:如果设备没有重用节点,则为设备的引脚分配内存空间。使用devm_kzalloc 函数分配了大小为sizeof(*(dev->pins)) 的内存空间,并将其赋值给dev->pins。如果内存分配失败,则返回-ENOMEM。
(3)第14-20 行:获取设备的pinctrl 句柄。使用devm_pinctrl_get 函数获取设备的pinctrl句柄,并将其赋值给dev->pins->p。如果获取失败,函数会打印一条调试信息,并返回获取失败的错误码。(关于pinctrl_bind_pins 函数分析还没有完成,会在后面的章节继续分析)

pinctrl_bind_pins 函数的第15 行使用devm_pinctrl_get 函数获取设备的pinctrl 句柄,该函数定义早内核源码目录下的“drivers/pinctrl/core.c”文件中具体内容如下所示:

struct pinctrl *devm_pinctrl_get(struct device *dev)
{
    struct pinctrl **ptr, *p;
    // 为存储引脚控制器句柄的指针分配内存
    ptr = devres_alloc(devm_pinctrl_release, sizeof(*ptr), GFP_KERNEL);
    if (!ptr)
        return ERR_PTR(-ENOMEM);
    
    // 获取设备的引脚控制器句柄
    p = pinctrl_get(dev);
    if (!IS_ERR(p)) {
        // 如果获取成功,将引脚控制器句柄存储在指针中
        *ptr = p;
        // 将指针添加到设备资源中
        devres_add(dev, ptr);
    } else {
        // 如果获取失败,释放之前分配的指针内存
        devres_free(ptr);
    }
    // 返回引脚控制器句柄或错误码指针
    return p;
}

其中最重要的内容为第10 行,使用pinctrl_get 函数获取引脚控制器句柄,我们继续跳转到该函数,该函数的具体内容如下所示:

struct pinctrl *pinctrl_get(struct device *dev)
{
    struct pinctrl *p;
    // 检查设备指针是否为空
    if (WARN_ON(!dev))
        return ERR_PTR(-EINVAL);
    /*
    * 查看是否有其他组件(如设备核心)已经获取了此设备的引脚控制器句柄。
    * 在这种情况下,返回对该句柄的另一个指针。
    */
    p = find_pinctrl(dev);
    if (p) {
        dev_dbg(dev, "obtain a copy of previously claimed pinctrl\n");
        kref_get(&p->users);
        return p;
    }
    // 创建并返回设备的引脚控制器句柄
    return create_pinctrl(dev, NULL);
}

关于该函数的详细注释已经在上述内容中进行了添加,我们要注意的在返回值中使用的create_pinctrl 函数,该函数会创建并返回设设备的引脚控制器句柄,我们继续跳转到create_pinctrl 的函数定义,具体内容如下所示:

static struct pinctrl *create_pinctrl(struct device *dev,struct pinctrl_dev *pctldev)
{
    struct pinctrl *p;
    const char *devname;
    struct pinctrl_maps *maps_node;
    int i;
    const struct pinctrl_map *map;
    int ret;
    /*
    * 为每个映射创建状态cookie 持有者struct pinctrl。
    * 这是当使用pinctrl_get() 请求引脚控制句柄时消费者将获得的对象。
    */
    p = kzalloc(sizeof(*p), GFP_KERNEL);
    if (!p)
        return ERR_PTR(-ENOMEM);
    p->dev = dev;
    INIT_LIST_HEAD(&p->states);
    INIT_LIST_HEAD(&p->dt_maps);
    ret = pinctrl_dt_to_map(p, pctldev);
    if (ret < 0) {
        kfree(p);
        return ERR_PTR(ret);
    }
    devname = dev_name(dev);
    mutex_lock(&pinctrl_maps_mutex);
    /* 遍历引脚控制映射以定位正确的映射*/
    for_each_maps(maps_node, i, map) {
        /* 映射必须适用于此设备*/
        if (strcmp(map->dev_name, devname))
            continue;
        /*
        * 如果pctldev 不为空,我们正在声明它的独占使用权,
        * 这意味着它自己提供了该设置。
        *
        * 因此,我们必须跳过适用于此设备但由其他设备提供的映射。
        */
        if (pctldev && strcmp(dev_name(pctldev->dev), map->ctrl_dev_name))
            continue;
        ret = add_setting(p, pctldev, map);
        /*
        * 在这一点上,添加设置可能会导致:
        *
        * - 延迟,如果引脚控制设备尚不可用
        * - 失败,如果引脚控制设备尚不可用,
        * 并且该设置是一个独占设置。我们不能推迟它,因为
        * 该独占设置会在设备注册后立即生效。
        *
        * 如果返回的错误不是-EPROBE_DEFER,则我们将
        * 累积错误,以查看是否最终得到-EPROBE_DEFER,
        * 因为那是最糟糕的情况。
        */
        if (ret == -EPROBE_DEFER) {
            pinctrl_free(p, false);
            mutex_unlock(&pinctrl_maps_mutex);
            return ERR_PTR(ret);
        }
    }
    mutex_unlock(&pinctrl_maps_mutex);
    if (ret < 0) {
        /* 如果发生了除推迟以外的其他错误,则在此处返回*/
        pinctrl_free(p, false);
        return ERR_PTR(ret);
    }
    
    kref_init(&p->users);
    
    /* 将引脚控制句柄添加到全局列表*/
    mutex_lock(&pinctrl_list_mutex);
    list_add_tail(&p->node, &pinctrl_list);
    mutex_unlock(&pinctrl_list_mutex);
    
    return p;
}

(1)第4 行-第9 行:函数声明了需要用到的变量和指针,其中struct pinctrl 类型的变量p用于表示创建的引脚控制器句柄,struct pinctrl 结构体用于表示引脚控制器。引脚控制器是硬件系统中管理和控制引脚(GPIO)的组件,它负责配置引脚的功能、电气属性等,该结构体定义在内核源码目录下的“drivers/pinctrl/core.h”文件中,具体内容如下所示:

struct pinctrl {
    struct list_head node; // 用于将引脚控制器添加到全局列表的链表节点
    struct device *dev; // 关联的设备
    struct list_head states; // 存储引脚配置状态的链表,用于跟踪不同的引脚配置状态
    struct pinctrl_state *state; // 当前应用的引脚配置状态
    struct list_head dt_maps; // 存储设备树中定义的引脚映射信息的链表
    struct kref users; // 引脚控制器的引用计数,用于跟踪引脚控制器的引用数量
};

struct pinctrl_maps 类型的变量maps_node 用于遍历引脚控制映射,引脚控制器映射描述了不同引脚控制器的功能和配置与实际硬件引脚之间的对应关系,该结构体定义在内核源码目录下的“drivers/pinctrl/core.h”文件中,具体内容如下所示:

struct pinctrl_maps {
    struct list_head node; // 引脚控制器映射链表节点,用于将该映射添加到全局列表
    const struct pinctrl_map *maps; // 指向引脚控制器映射数组的指针
    unsigned num_maps; // 引脚控制器映射数组中的映射数量
};

其他结构体和变量已经讲解过了,这里就不再进行过多的赘述。
(2)第15-17 行:使用kzalloc 函数为p 分配内存,大小为sizeof(*p),并将分配的内存清零。检查内存分配是否成功,如果失败则返回错误码-ENOMEM。
(3)第18 行:将dev 赋值给p->dev,表示引脚控制器与设备关联。
(3)第19-20 行:使用INIT_LIST_HEAD 宏初始化p->states 和p->dt_maps 字段,它们是链表头,用于存储引脚配置的状态和设备树映射。
(4)第22-26 行:调用pinctrl_dt_to_map 函数将设备树中定义的引脚映射信息转换为struct pinctrl_map 结构,并将其添加到p->dt_maps 链表中。如果转换过程中出现错误(返回值小于0),则释放之前分配的内存,并返回对应的错误码(关于create_pinctrl 函数的讲解在后面的章节会继续,这里要先跳转到他的子函数进行讲解)。

该函数定义在内核源码目录下的“drivers/pinctrl/devicetree.c”文件中,具体内容如下所示:

int pinctrl_dt_to_map(struct pinctrl *p, struct pinctrl_dev *pctldev)
{
    struct device_node *np = p->dev->of_node; // 获取引脚控制器关联设备的设备树节点
    int state, ret;
    char *propname;
    struct property *prop;
    const char *statename;
    const __be32 *list;
    int size, config;
    phandle phandle;
    struct device_node *np_config;
    
    /* 如果CONFIG_OF 启用,且p->dev 不是从设备树实例化而来*/
    if (!np) {
        if (of_have_populated_dt())
            dev_dbg(p->dev, "no of_node; not parsing pinctrl DT\n");
        return 0;
    }
    
    /* 节点内部存储属性名称的指针*/
    of_node_get(np);
    
    /* 对于每个定义的状态ID */
    for (state = 0;; state++) {
        /* 获取pinctrl-* 属性*/
        propname = kasprintf(GFP_KERNEL, "pinctrl-%d", state);
        prop = of_find_property(np, propname, &size);
        kfree(propname);
        if (!prop) {
            if (state == 0) {
                of_node_put(np);
                return -ENODEV;
            }
            break;
        }
        list = prop->value;
        size /= sizeof(*list);

        /* 判断pinctrl-names 属性是否命名了该状态*/
        ret = of_property_read_string_index(np, "pinctrl-names", state, &statename);
        /*
        * 如果未命名,则statename 仅是整数状态ID。但是,为了避免动态分配和之后要释放的
        麻烦,
        * 可以直接将statename 指向属性名称的一部分。
        */
        if (ret < 0) {
            /* strlen("pinctrl-") == 8 */
            statename = prop->name + 8;
        }

        /* 对于其中的每个引用的引脚配置节点*/
        for (config = 0; config < size; config++) {
            phandle = be32_to_cpup(list++);
            /* 查找引脚配置节点*/
            np_config = of_find_node_by_phandle(phandle);
            if (!np_config) {
                dev_err(p->dev, "prop %s index %i invalid phandle\n", prop->name, config);
                ret = -EINVAL;
                goto err;
            }
            /* 解析节点*/
            ret = dt_to_map_one_config(p, pctldev, statename, np_config);
            of_node_put(np_config);
            if (ret < 0)
                goto err;
        }
        /* 如果在设备树中没有条目,则生成一个虚拟状态表条目*/
        if (!size) {
            ret = dt_remember_dummy_state(p, statename);
            if (ret < 0)
                goto err;
        }
    }
    return 0;
    
err:
    pinctrl_dt_free_maps(p);
    return ret;
}

(1)第3 行:获取与引脚控制器关联设备的设备树节点。
(2)第13-18 行:如果设备树节点为空,表示没有设备树信息可供解析。如果内核配置中启用了CONFIG_OF 选项,并且设备并非从设备树实例化,函数会打印一条调试信息并立即返回0。
(3)第21 行:增加设备树节点的引用计数,以确保在解析过程中节点不会被释放。
(4)第24-76 行:对于每个定义的状态ID,循环解析引脚控制器的映射信息具体会执行以下步骤:

  • 第26 行构造属性名称字符串propname,例如”pinctrl-0”、”pinctrl-1” 等等。
  • 第27-35 行:使用of_find_property 函数获取设备树节点np 中的属性propname 的值,并得到属性值的大小size。如果属性不存在,则判断是否是第一个状态ID,如果是,则释放节点引用并返回-ENODEV 表示设备树节点中没有有效的pinctrl 描述。否则,跳出循环。
  • 第36-37 行:将属性值转换为指针列表list,并计算列表的大小。
  • 第40-49 行:如果设备树中的pinctrl-names 属性命名了该状态,则使用of_property_read_string_index 函数读取属性值,并将状态名称存储在statename 变量中。否则,将statename 指向属性名称的一部分,即去除”pinctrl-“ 前缀。
  • 第51-76 行:对于每个引用的引脚配置节点,执行以下步骤:
  • 第53 行:将list 指针指向的phandle 值转换为本地字节序。
  • 第56 行-61 行:使用of_find_node_by_phandle 函数根据phandle 查找引脚配置节点,并将其存储在np_config 变量中。如果找不到引脚配置节点,则打印错误信息并返回-EINVAL。
  • 第64 行:调用dt_to_map_one_config 函数,将引脚配置节点的信息解析为pinctrl 映射,并存储在pctldev 中。
  • 第65 行:递减引脚配置节点的引用计数。
  • 第70-75 行如果在设备树中没有条目,则生成一个虚拟状态表条目,以便后续处理。

其中第64 行dt_to_map_one_config 函数需要特别注意,该函数会从设备树节点中解析出引脚控制器的映射表,并将其存储起来,该函数的具体内容如下所示:

static int dt_to_map_one_config(struct pinctrl *p,struct pinctrl_dev *hog_pctldev,
                                const char *statename,struct device_node *np_config)
{
    struct pinctrl_dev *pctldev = NULL;
    struct device_node *np_pctldev;
    const struct pinctrl_ops *ops;
    int ret;
    struct pinctrl_map *map;
    unsigned num_maps;
    bool allow_default = false;
    
    /* 查找包含np_config 的引脚控制器*/
    np_pctldev = of_node_get(np_config);
    
    for (;;) {
        /* 如果不允许默认配置,则读取pinctrl-use-default 属性*/
        if (!allow_default)
        	allow_default = of_property_read_bool(np_pctldev,"pinctrl-use-default");
        
        /* 获取np_pctldev 的父节点*/
        np_pctldev = of_get_next_parent(np_pctldev);
        
        /* 如果没有父节点或者父节点是根节点,则释放np_pctldev 引用并返回*/
        if (!np_pctldev || of_node_is_root(np_pctldev)) {
            of_node_put(np_pctldev);
            
            /* 检查是否延迟探测驱动程序状态*/
            ret = driver_deferred_probe_check_state(p->dev);
            
            /* 如果启用了模块并且不允许默认配置,并且返回值是-ENODEV,则延迟探测*/
            if (IS_ENABLED(CONFIG_MODULES) && !allow_default && ret == -ENODEV)
                ret = -EPROBE_DEFER;
            return ret;
        }
        
        /* 如果正在创建一个hog,可以使用传递的pctldev */
        if (hog_pctldev && (np_pctldev == p->dev->of_node)) {
            pctldev = hog_pctldev;
            break;
        }
        
        /* 通过np_pctldev 获取pinctrl_dev 结构体*/
        pctldev = get_pinctrl_dev_from_of_node(np_pctldev);
        
        /* 如果获取到了pinctrl_dev 结构体,则跳出循环*/
        if (pctldev)
            break;
        
        /* 不要推迟探测hogs(循环) */
        if (np_pctldev == p->dev->of_node) {
            of_node_put(np_pctldev);
            return -ENODEV;
        }
    }
    of_node_put(np_pctldev);
    
    /*
    * 调用pinctrl 驱动程序解析设备树节点,并生成映射表条目
    */
    ops = pctldev->desc->pctlops;
    
    /* 检查pinctrl 驱动程序是否支持设备树*/
    if (!ops->dt_node_to_map) {
        dev_err(p->dev, "pctldev %s doesn't support DT\n",
        dev_name(pctldev->dev));
        return -ENODEV;
    }
    
    /* 调用pinctrl 驱动程序的dt_node_to_map 方法*/
    ret = ops->dt_node_to_map(pctldev, np_config, &map, &num_maps);
    if (ret < 0)
        return ret;
    
    /* 将映射表块存储起来以供后续使用*/
    return dt_remember_or_free_map(p, statename, pctldev, map, num_maps);
}
  • 第15-51 行:通过循环查找包含指定设备树节点np_config 的引脚控制器。在循环中,会获取当前节点的父节点,并判断是否满足终止条件。如果找到了引脚控制器,将其赋值给变量pctldev,否则返回错误码。
  • 第53-62 行:检查引脚控制器是否支持设备树操作,即是否实现了dt_node_to_map 方法。如果不支持,返回错误码。
  • 第63-67 行:调用引脚控制器的dt_node_to_map 方法,将设备树节点np_config 转换为映射表条目。该方法会解析设备树节点,并根据节点信息生成映射表条目。具体的转换过程由各个引脚控制器的驱动程序实现。
  • 第69 行:将生成的映射表条目存储起来,以供后续使用。存储的操作由函数dt_remember_or_free_map 完成。
    至此,关于pinctrl_bind_pins 函数的第一阶段分析就完成了,在pinctrl_bind_pins 函数的第一阶段详细讲解了设备树节点转换为pinctrl_map 的流程,在下一小节将对pinctrl_bind_pins函数做进一步分析。

124.3 pinctrl_bind_pins 函数分析2

上一小节最后讲解的dt_to_map_one_config 函数用于从设备树节点中解析出引脚控制器的映射表并存储起来,而存储的操作由函数dt_remember_or_free_map 完成,该函数的具体内容如下所示:

static int dt_remember_or_free_map(struct pinctrl *p, const char *statename,
                           struct pinctrl_dev *pctldev,struct pinctrl_map *map, unsigned num_maps)
{
    int i;
    struct pinctrl_dt_map *dt_map;
    
    /* 初始化公共映射表条目字段*/
    for (i = 0; i < num_maps; i++) {
        const char *devname;
        /* 复制设备名称*/
        devname = kstrdup_const(dev_name(p->dev), GFP_KERNEL);
        if (!devname)
            goto err_free_map;
        /* 设置映射表条目的设备名称、状态名称和控制器设备名称*/
        map[i].dev_name = devname;
        map[i].name = statename;
        if (pctldev)
            map[i].ctrl_dev_name = dev_name(pctldev->dev);
    }
    
    /* 记录转换后的映射表条目*/
    dt_map = kzalloc(sizeof(*dt_map), GFP_KERNEL);
    if (!dt_map)
        goto err_free_map;
    dt_map->pctldev = pctldev;
    dt_map->map = map;
    dt_map->num_maps = num_maps;
    list_add_tail(&dt_map->node, &p->dt_maps);
    
    /* 注册映射表条目*/
    return pinctrl_register_mappings(map, num_maps);
    
err_free_map:
    /* 释放映射表条目内存*/
    dt_free_map(pctldev, map, num_maps);
    return -ENOMEM;
}

函数传递参数:

  • p:指向struct pinctrl 结构体的指针,表示引脚控制器的上下文。
  • statename:指向状态名称的指针,表示要设置的状态的名称。
  • pctldev:指向struct pinctrl_dev 结构体的指针,表示引脚控制器设备。
  • map:指向struct pinctrl_map 结构体数组的指针,表示解析得到的映射表条目。
  • num_maps:表示映射表条目的数量。

(1)第9-22 行:通过循环遍历映射表条目数组,为每个条目初始化公共字段。
首先,通过kstrdup_const 函数复制引脚控制器设备的名称,并将返回的指针赋值给devname。
然后,将设备名称、状态名称和控制器设备名称分别赋值给映射表条目的对应字段。

(2)第25-32 行:分配并初始化一个struct pinctrl_dt_map 结构体,用于存储映射表的相关信息,并将其添加到p->dt_maps 链表的尾部。

  • 使用kzalloc 分配内存空间,并将返回的指针赋值给dt_map。
  • 将传入的pctldev、map 和num_maps 分别赋值给dt_map 的对应字段。
  • 使用list_add_tail 函数将dt_map 添加到p->dt_maps 链表中。

struct pinctrl_dt_map 结构体结构体的具体内容如下所示

struct pinctrl_dt_map {
    struct list_head node;//用于将映射表结构体添加到pinctrl 的dt_maps 链表中
    struct pinctrl_dev *pctldev;// 引脚控制器设备
    struct pinctrl_map *map;// 映射表条目数组
    unsigned num_maps;//映射表条目数量
};

(3)第35 行:用pinctrl_register_mappings 函数注册映射表条目。该函数将映射表条目注册到pinctrl 子系统,以便后续可以通过相关接口进行引脚配置和管理。
接下来跳转到pinctrl_register_mappings 函数,该函数定义在内核源码目录下的”drivers/pinctrl/core.c“文件中,具体内容如下所示:

/**
* pinctrl_register_mappings - 注册引脚控制器映射表
*
* @maps: 指向映射表条目数组的指针
* @num_maps: 映射表条目数量
*
* 返回值:0 表示注册成功,负值表示注册失败
*/
int pinctrl_register_mappings(const struct pinctrl_map *maps,unsigned num_maps)
{
    int i, ret;
    struct pinctrl_maps *maps_node;
    pr_debug("add %u pinctrl maps\n", num_maps);
    
    /* 首先对新映射表进行合法性检查*/
    for (i = 0; i < num_maps; i++) {
        // 检查设备名称是否存在
        if (!maps[i].dev_name) {
            pr_err("failed to register map %s (%d): no device given\n",maps[i].name, i);
            return -EINVAL;
        }

        // 检查映射表名称是否存在
        if (!maps[i].name) {
            pr_err("failed to register map %d: no map name given\n",i);
            return -EINVAL;
        }

        // 对于引脚映射类型和配置映射类型,检查引脚控制设备名称是否存在
        if (maps[i].type != PIN_MAP_TYPE_DUMMY_STATE &&!maps[i].ctrl_dev_name) {
            pr_err("failed to register map %s (%d): no pin control device given\n",maps[i].name, i);
            return -EINVAL;
        }
        switch (maps[i].type) {
            case PIN_MAP_TYPE_DUMMY_STATE:
                // 对于虚拟状态映射类型,不进行验证
                break;
            case PIN_MAP_TYPE_MUX_GROUP:
                // 对于复用组映射类型,进行引脚复用映射验证
                ret = pinmux_validate_map(&maps[i], i);
                if (ret < 0)
                    return ret;
            break;
            case PIN_MAP_TYPE_CONFIGS_PIN:
            case PIN_MAP_TYPE_CONFIGS_GROUP:
                // 对于配置映射类型,进行引脚配置映射验证
                ret = pinconf_validate_map(&maps[i], i);
                if (ret < 0)
                    return ret;
            break;
            default:
                // 对于无效的映射类型,返回错误
                pr_err("failed to register map %s (%d): invalid type given\n",maps[i].name, i);
                return -EINVAL;
        }
    }
    
    // 分配映射表节点内存
    maps_node = kzalloc(sizeof(*maps_node), GFP_KERNEL);
    if (!maps_node)
        return -ENOMEM;
    
    // 设置映射表节点的映射表和映射表数量
    maps_node->maps = maps;
    maps_node->num_maps = num_maps;
    
    // 加锁并将映射表节点插入映射表链表末尾
    mutex_lock(&pinctrl_maps_mutex);
    list_add_tail(&maps_node->node, &pinctrl_maps);
    mutex_unlock(&pinctrl_maps_mutex);
    return 0;
}

函数的参数为maps 和num_maps,其中maps 是指向映射表条目数组的指针,num_maps是映射表条目的数量。

(1)第18-64 行:进行一些合法性检查。对于每一个映射表条目,会进行以下的检查操作:

  • 第20-24 行:检查设备名称是否存在

  • 第26-31 行:检查映射表名称是否存在,如果不存在则返回一个错误码。

  • 第33-39 行:对于引脚映射类型和配置映射类型,检查引脚控制设备名称是否存在,如果不存在则返回一个错误码。

  • 第41-63 行:根据映射表的类型做对应的验证操作:

    ​ 如果类型是PIN_MAP_TYPE_DUMMY_STATE,表示是虚拟状态映射类型,不进行验证。
    ​ 如果类型是PIN_MAP_TYPE_MUX_GROUP,表示是复用组映射类型,需要进行引脚复用映射的验证。
    ​ 如果类型是PIN_MAP_TYPE_CONFIGS_PIN 或PIN_MAP_TYPE_CONFIGS_GROUP,表示是配置映射类型, 需要进行引脚配置映射的验证。
    ​ 如果类型不是以上这些有效的类型,则返回一个错误码。

(2)第66-73 行:函数会分配映射表节点的内存,并将映射表和映射表数量设置到映射表节点中。
(3)第75-78 行:函数会获取一个互斥锁,将映射表节点插入到映射表链表的末尾,并释放互斥锁。

pinctrl_register_mappings 函数的作用是注册一个引脚控制器的映射表,进行了一些参数合法性检查和验证,并将映射表节点插入到映射表链表中。
到这里关于pinctrl_bind_pins 函数的第二阶段讲解就完成了,在该阶段大多都是关于赋值的操作并将节点加入到链表中, 但分析到这里我们仍旧没有找到struct pinctrl_state*default_statepinctrl_map 结构体是在什么地方进行的绑定,所以在下个章节我们将继续分析124.2 小节中create_pinctrl 函数接下来的部分。

第125 章add_setting 函数分析

这里继续讲解在上一章中没有讲解完成的create_pinctrl 函数,该函数的具体内容如下所示:

static struct pinctrl *create_pinctrl(struct device *dev,struct pinctrl_dev *pctldev)
{
    struct pinctrl *p;
    const char *devname;
    struct pinctrl_maps *maps_node;
    int i;
    const struct pinctrl_map *map;
    int ret;
    
    /*
    * 为每个映射创建状态cookie 持有者struct pinctrl。
    * 这是当使用pinctrl_get() 请求引脚控制句柄时消费者将获得的对象。
    */
    p = kzalloc(sizeof(*p), GFP_KERNEL);
    if (!p)
        return ERR_PTR(-ENOMEM);
    
    p->dev = dev;
    INIT_LIST_HEAD(&p->states);
    INIT_LIST_HEAD(&p->dt_maps);
    
    ret = pinctrl_dt_to_map(p, pctldev);
    if (ret < 0) {
        kfree(p);
        return ERR_PTR(ret);
    }
    
    devname = dev_name(dev);
    mutex_lock(&pinctrl_maps_mutex);
    
    /* 遍历引脚控制映射以定位正确的映射*/
    for_each_maps(maps_node, i, map) {
        /* 映射必须适用于此设备*/
        if (strcmp(map->dev_name, devname))
            continue;
        /*
        * 如果pctldev 不为空,我们正在声明它的独占使用权,
        * 这意味着它自己提供了该设置。
        *
        * 因此,我们必须跳过适用于此设备但由其他设备提供的映射。
        */
        if (pctldev && strcmp(dev_name(pctldev->dev), map->ctrl_dev_name))
            continue;
        ret = add_setting(p, pctldev, map);
        /*
        * 在这一点上,添加设置可能会导致:
        *
        * - 延迟,如果引脚控制设备尚不可用
        * - 失败,如果引脚控制设备尚不可用,
        * 并且该设置是一个独占设置。我们不能推迟它,因为
        * 该独占设置会在设备注册后立即生效。
        *
        * 如果返回的错误不是-EPROBE_DEFER,则我们将
        * 累积错误,以查看是否最终得到-EPROBE_DEFER,
        * 因为那是最糟糕的情况。
        */
        if (ret == -EPROBE_DEFER) {
            pinctrl_free(p, false);
            mutex_unlock(&pinctrl_maps_mutex);
            return ERR_PTR(ret);
        }
    }
    mutex_unlock(&pinctrl_maps_mutex);
    
    if (ret < 0) {
        /* 如果发生了除推迟以外的其他错误,则在此处返回*/
        pinctrl_free(p, false);
        return ERR_PTR(ret);
    }
    kref_init(&p->users);
    
    /* 将引脚控制句柄添加到全局列表*/
    mutex_lock(&pinctrl_list_mutex);
    list_add_tail(&p->node, &pinctrl_list);
    mutex_unlock(&pinctrl_list_mutex);
    
    return p;
}

在上一章节,关于create_pinctrl 函数我们分析到了第22 行的pinctrl_dt_to_map 函数,但我们还并没有找到dev_pin_info 结构体和map 是在什么时候进行的绑定,所以我们继续向下分析,在第32 行使用for_each_maps 宏定义对引脚控制映射进行了遍历,该宏定义定义在内核源码目录下的“drivers/pinctrl/core.h”文件中,具体内容如下所示:

// 宏定义:用于遍历映射表链表中的每个映射表条目
// _maps_node_: 遍历时使用的映射表节点指针
// _i_: 遍历时使用的计数器变量
// _map_: 遍历时使用的映射表条目指针
#define for_each_maps(_maps_node_, _i_, _map_) \
    list_for_each_entry(_maps_node_, &pinctrl_maps, node) \ // 遍历映射表链表中的每个节点
        for (_i_ = 0, _map_ = &_maps_node_->maps[_i_]; \ // 初始化计数器和映射表条目指针
            _i_ < _maps_node_->num_maps; \ // 循环条件:计数器小于当前节点的映射表数量
            _i_++, _map_ = &_maps_node_->maps[_i_]) // 每次循环增加计数器并更新映射表条目指针

在遍历过程中,首先会检查映射的设备名称是否与当前设备的名称匹配,如果不匹配则跳过。当检查pctldev 不为空且映射是为pctldev 提供的,则跳过该映射。
检查完成之后调用add_setting 函数将映射添加到引脚控制器中,这里的add_setting 就是本章节要讲解的重点,该函数的具体内容如下所示:

static int add_setting(struct pinctrl *p, struct pinctrl_dev *pctldev,
                       const struct pinctrl_map *map)
{
    struct pinctrl_state *state; // 状态对象指针
    struct pinctrl_setting *setting; // 设置对象指针
    int ret;
    
    // 查找状态对象,如果不存在则创建新的状态对象
    state = find_state(p, map->name);
    if (!state)
        state = create_state(p, map->name);
    if (IS_ERR(state))
        return PTR_ERR(state);
    
    // 如果映射类型为虚拟状态映射类型,直接返回
    if (map->type == PIN_MAP_TYPE_DUMMY_STATE)
        return 0;
    // 分配设置对象的内存空间
    setting = kzalloc(sizeof(*setting), GFP_KERNEL);
    if (!setting)
        return -ENOMEM;

    setting->type = map->type; // 设置设置对象的映射类型
    
    // 设置设置对象的引脚控制设备
    if (pctldev)
        setting->pctldev = pctldev;
    else
        setting->pctldev = get_pinctrl_dev_from_devname(map->ctrl_dev_name);
    
    if(!setting->pctldev) {
        kfree(setting);
        // 如果引脚控制设备不存在,返回错误
        // 注意:不推迟探测hogs(循环依赖)
        if (!strcmp(map->ctrl_dev_name, map->dev_name))
            return -ENODEV;
        /*
        * 好吧,我们假设驱动程序目前不存在,
        * 让我们将获取此pinctrl 句柄推迟到以后...
        */
        dev_info(p->dev, "unknown pinctrl device %s in map entry, deferring probe",
                 map->ctrl_dev_name);
        return -EPROBE_DEFER;
    }
    
    // 设置设置对象的设备名称
    setting->dev_name = map->dev_name;
    switch (map->type) {
        case PIN_MAP_TYPE_MUX_GROUP:
            // 对于复用组映射类型,执行引脚复用映射到设置对象的转换
            ret = pinmux_map_to_setting(map, setting);
        break;
            
        case PIN_MAP_TYPE_CONFIGS_PIN:
        case PIN_MAP_TYPE_CONFIGS_GROUP:
            // 对于配置映射类型,执行引脚配置映射到设置对象的转换
            ret = pinconf_map_to_setting(map, setting);
        break;
            
        default:
            ret = -EINVAL;
        break;
    }
    
    if (ret < 0) {
        kfree(setting);
        return ret;
    }
    
    // 将设置对象插入状态对象的设置链表末尾
    list_add_tail(&setting->node, &state->settings);
    return 0;
}

(1)第4-5 行: 分别定义了一个struct pinctrl_state * 类型的状态对象指针和structpinctrl_setting * 的设置对象指针, struct pinctrl_state 结构体定义在内核源码目录下的“drivers/pinctrl/core.h”文件中,具体内容如下所示:

struct pinctrl_state {
    struct list_head node; // 链表节点,用于将状态对象连接到引脚控制器对象的状态链表
    const char *name; // 状态对象的名称字符串指针
    struct list_head settings; // 设置对象链表,包含该状态的所有设置对象
};

而struct pinctrl_setting 结构体定义在内核源码目录下的“drivers/pinctrl/core.h”文件中,具体内容如下所示:

struct pinctrl_setting {
    struct list_head node; // 链表节点,用于将设置对象连接到状态对象的设置链表
    enum pinctrl_map_type type; // 映射类型,表示设置对象的类型
    struct pinctrl_dev *pctldev; // 引脚控制设备对象指针
    const char *dev_name; // 设备名称字符串指针
    
    union {
        struct pinctrl_setting_mux mux; // 复用组映射类型的数据结构
        struct pinctrl_setting_configs configs; // 配置映射类型的数据结构
    } data;
};

(2)第8-13 行:根据映射表条目的名称,使用find_state 函数在引脚控制器对象中查找对应的状态对象,在此之前我们并没有设置状态对象,所以会进入到第二个if 判断,使用create_state 函数创建新的状态对象。create_state 函数内容如下所示:

static struct pinctrl_state *create_state(struct pinctrl *p,const char *name)
{
    struct pinctrl_state *state;
    
    // 为pinctrl_state 结构体分配内存
    state = kzalloc(sizeof(*state), GFP_KERNEL);
    if (!state)
        // 内存分配失败,返回错误指针
        return ERR_PTR(-ENOMEM);
    
    // 设置状态的名称
    state->name = name;
    
    // 初始化状态的设置列表
    INIT_LIST_HEAD(&state->settings);
    
    // 将状态添加到pinctrl 的状态链表中
    list_add_tail(&state->node, &p->states);
    return state;
}
  • (3)第15-17 行:如果映射类型为虚拟状态映射类型(PIN_MAP_TYPE_DUMMY_STATE),直接返回成功。
  • (4)第19-22 行:分配一个设置对象的内存空间。
  • (5)第24 行:设置对象的映射类型为映射表条目中定义的类型。
  • (6)第26-56 行:根据情况设置设置对象的引脚控制设备:
    • 如果pctldev 参数为非空,则将其设置为设置对象的引脚控制设备。
    • 如果pctldev 参数为空,则根据映射表条目中的ctrl_dev_name,使用get_pinctrl_dev_from_devname 函数获取引脚控制设备对象,并将其设置为设置对象的引脚控制设备。
    • 如果获取的引脚控制设备对象为空,说明引脚控制设备不存在,释放设置对象的内存空间并返回错误。如果ctrl_dev_namedev_name 相同,表示映射表条目中的设备名称与控制设备名称相同,返回错误码-ENODEV。如果不相同,表示引脚控制设备目前不存在,打印一条信息并返回错误码-EPROBE_DEFER,表示推迟探测引脚控制设备。
  • (7)第48 行:设置设置对象的设备名称为映射表条目中的dev_name。
  • (8)第50-63 行:根据设置对象的映射类型执行相应的映射转换操作:
    • 对于复用组映射类型(PIN_MAP_TYPE_MUX_GROUP),调用pinmux_map_to_setting 函数执行引脚复用映射到设置对象的转换。
    • 对于配置映射类型(PIN_MAP_TYPE_CONFIGS_PIN 或PIN_MAP_TYPE_CONFIGS_GROUP),调用pinconf_map_to_setting 函数执行引脚配置映射到设置对象的转换。对于其他映射类型,返回错误码-EINVAL。
  • (9)第64 行:如果映射转换操作失败(返回值小于0),释放设置对象的内存空间并返回错误码。
  • (10)第70 行:将设置对象插入状态对象的设置链表末尾。

接下来对第50-63 行中用到的pinmux_map_to_setting 函数和pinconf_map_to_setting 函数进行详细的讲解, pinmux_map_to_setting 函数定义在内核源码目录下的“drivers/pinctrl/pinmux.c”文件中,具体内容如下所示:

/**
* pinmux_map_to_setting - 将引脚映射转换为设置对象
*
* @map: 引脚映射结构指针
* @setting: 引脚设置结构指针
*
* 返回值:0 表示转换成功,负值表示转换失败
*/
int pinmux_map_to_setting(const struct pinctrl_map *map,struct pinctrl_setting *setting)
{
    struct pinctrl_dev *pctldev = setting->pctldev; // 获取引脚控制设备指针
    const struct pinmux_ops *pmxops = pctldev->desc->pmxops; // 获取引脚复用操作指针
    char const * const *groups; // 引脚复用组数组
    unsigned num_groups; // 引脚复用组数量
    int ret;
    const char *group; // 引脚复用组名称
    
    if (!pmxops) {
        dev_err(pctldev->dev, "does not support mux function\n");
        return -EINVAL;
    }
    
    // 将复用函数名称转换为选择器
    ret = pinmux_func_name_to_selector(pctldev, map->data.mux.function);
    if (ret < 0) {
        dev_err(pctldev->dev, "invalid function %s in map table\n",
        map->data.mux.function);
        return ret;
    }
    setting->data.mux.func = ret; // 设置设置对象的复用函数选择器
    // 查询函数对应的复用组信息
    ret = pmxops->get_function_groups(pctldev, setting->data.mux.func, &groups, &num_groups);
    if (ret < 0) {
        dev_err(pctldev->dev, "can't query groups for function %s\n",
        map->data.mux.function);
        return ret;
    }
    if (!num_groups) {
        dev_err(pctldev->dev,"function %s can't be selected on any group\n", map->data.mux.function);
        return -EINVAL;
    }
    if (map->data.mux.group) {
        group = map->data.mux.group;
        ret = match_string(groups, num_groups, group);
        if (ret < 0) {
            dev_err(pctldev->dev,"invalid group \"%s\" for function \"%s\"\n",
                    map->data.mux.function, group);
            return ret;
        }
    } else {
        group = groups[0];
    }
    
    // 获取复用组的选择器
    ret = pinctrl_get_group_selector(pctldev, group);
    if (ret < 0) {
        dev_err(pctldev->dev, "invalid group %s in map table\n",map->data.mux.group);
        return ret;
    }
    setting->data.mux.group = ret; // 设置设置对象的复用组选择器
    return 0;
}

该函数的作用是将引脚映射转换为设置对象。
(1)第19-22 行:检查引脚控制设备是否支持引脚复用操作,如果不支持,则返回错误。
(2)第24-31 行:将映射表中的复用函数名称转换为复用函数的选择器,并将其保存在设置对象的data.mux.func 字段中。
(3)第33-46 行:通过调用引脚复用操作对象的get_function_groups 函数查询复用函数对应的复用组信息,获取复用组的名称数组和数量,并将它们保存在groups 和num_groups 变量中。如果没有任何复用组可供选择,则返回错误。
(4)第47-58 行:根据映射表中指定的复用组名称或者选择第一个复用组名称,并在复用组数组中查找对应的索引。
(5)第60-67 行:通过调用引脚控制设备对象的pinctrl_get_group_selector 函数获取复用组的选择器,并将它保存在设置对象的data.mux.group

pinconf_map_to_setting 函数定义在内核源码目录下的“drivers/pinctrl/pinconf.c”文件中,具体内容如下所示:

/**
* pinconf_map_to_setting - 将引脚配置映射转换为设置对象
*
* @map: 引脚映射结构指针
* @setting: 引脚设置结构指针
*
* 返回值:0 表示转换成功,负值表示转换失败
*/
int pinconf_map_to_setting(const struct pinctrl_map *map, struct pinctrl_setting *setting)
{
    struct pinctrl_dev *pctldev = setting->pctldev; // 获取引脚控制设备指针
    int pin;
    switch (setting->type) {
        case PIN_MAP_TYPE_CONFIGS_PIN: // 针对单个引脚的配置
            pin = pin_get_from_name(pctldev, map->data.configs.group_or_pin); // 通过引脚名称获取引脚号
            if (pin < 0) {
                dev_err(pctldev->dev, "could not map pin config for \"%s\"",
                        map->data.configs.group_or_pin);
                return pin;
            }
            setting->data.configs.group_or_pin = pin; // 设置设置对象的引脚号
        break;
            
        case PIN_MAP_TYPE_CONFIGS_GROUP: // 针对引脚组的配置
            pin = pinctrl_get_group_selector(pctldev, 
                                          map->data.configs.group_or_pin); // 获取引脚组的选择器
            if (pin < 0) {
                dev_err(pctldev->dev, "could not map pin config for \"%s\"",
                        map->data.configs.group_or_pin);
                return pin;
            }
            setting->data.configs.group_or_pin = pin; // 设置设置对象的引脚组选择器
        break;
            
        default:
            return -EINVAL;
    }
    setting->data.configs.num_configs = map->data.configs.num_configs; // 设置设置对象的配置数量
    setting->data.configs.configs = map->data.configs.configs; // 设置设置对象的配置指针
    return 0;
}

该函数的作用是将引脚配置映射转换为设置对象。

  • (1)第15-38 行:根据设置对象的类型进行不同的处理:
    • 对于针对单个引脚的配置,通过调用pin_get_from_name 函数,根据映射表中的引脚名
      称获取引脚号,并将其设置到设置对象的data.configs.group_or_pin 字段中。如果获取引脚号
      失败,则返回错误。
    • 对于针对引脚组的配置,它通过调用pinctrl_get_group_selector 函数,根据映射表中的
      引脚组名称获取引脚组的选择器,并将其设置到设置对象的data.configs.group_or_pin 字段中。
    • 如果获取引脚组选择器失败,则返回错误。
  • (2)第40-42 行:设置设置对象的配置数量和配置指针,分别从映射表中获取。完成转换
    后,函数返回0 表示转换成功。

至此,关于add_setting 函数的讲解就完成了,add_setting 函数的最终目的就是将传入的const struct pinctrl_map *map 的参数值传入到struct pinctrl_setting 类型的变量中,从而进一步提取pinctrl_map 结构体类型变量中的内容。

第126 章通过pinctrl 状态设置引脚复用实验

在上一个小节中讲解了add_setting 函数,关于他的上层函数create_pinctrl 也就讲解完成了,然后继续分析pinctrl_bind_pins 函数,pinctrl_bind_pins 函数内容如下所示:

int pinctrl_bind_pins(struct device *dev)
{
    int ret;
    
    // 检查设备是否重用了节点
    if (dev->of_node_reused)
        return 0;
    
    // 为设备的引脚分配内存空间
    dev->pins = devm_kzalloc(dev, sizeof(*(dev->pins)), GFP_KERNEL);
    if (!dev->pins)
        return -ENOMEM;
    
    // 获取设备的pinctrl 句柄
    dev->pins->p = devm_pinctrl_get(dev);
    if (IS_ERR(dev->pins->p)) {
        dev_dbg(dev, "没有pinctrl 句柄\n");
        ret = PTR_ERR(dev->pins->p);
        goto cleanup_alloc;
    }
    // 查找设备的默认pinctrl 状态
    dev->pins->default_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_DEFAULT);
    if (IS_ERR(dev->pins->default_state)) {
        dev_dbg(dev, "没有默认的pinctrl 状态\n");
        ret = 0;
        goto cleanup_get;
    }
    
    // 查找设备的初始化pinctrl 状态
    dev->pins->init_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_INIT);
    if (IS_ERR(dev->pins->init_state)) {
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有初始化的pinctrl 状态\n");
        // 选择默认的pinctrl 状态
        ret = pinctrl_select_state(dev->pins->p, dev->pins->default_state);
    } else {
        // 选择初始化的pinctrl 状态
        ret = pinctrl_select_state(dev->pins->p, dev->pins->init_state);
    }
    
    if (ret) {
        dev_dbg(dev, "无法激活初始的pinctrl 状态\n");
        goto cleanup_get;
    }
    
#ifdef CONFIG_PM
    /*
    * 如果启用了电源管理,我们还会寻找可选的睡眠和空闲的引脚状态,其语义在
    * <linux/pinctrl/pinctrl-state.h> 中定义
    */
    dev->pins->sleep_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_SLEEP);
    if (IS_ERR(dev->pins->sleep_state))
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有睡眠的pinctrl 状态\n");
    
    dev->pins->idle_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_IDLE);
    if (IS_ERR(dev->pins->idle_state))
        /* 不提供此状态是完全合法的*/
        dev_dbg(dev, "没有空闲的pinctrl 状态\n");
#endif
    return 0;
    
    /*
    * 如果对于此设备没有找到pinctrl 句柄或默认状态,
    * 让我们明确释放设备中的引脚容器,因为保留它没有意义。
    */
    cleanup_get:
        devm_pinctrl_put(dev->pins->p);
    cleanup_alloc:
        devm_kfree(dev, dev->pins);
        dev->pins = NULL;
        /* 返回延迟*/
        if (ret == -EPROBE_DEFER)
            return ret;
    
        /* 返回严重错误*/
        if (ret == -EINVAL)
            return ret;
    
        /* 我们忽略诸如-ENOENT 的错误,表示没有pinctrl 状态*/
        return 0;
}

前面的小节都只是对pinctrl_bind_pins 函数的第15 行获取设备的pinctrl 句柄所使用的devm_pinctrl_get 函数进行的讲解,接下来将继续对后面重要的内容进行讲解。
(1)第22-28 行:查找设备的默认pinctrl 状态。使用pinctrl_lookup_state 函数通过dev->pins->pPINCTRL_STATE_DEFAULT 参数查找设备的默认pinctrl 状态,并将其赋值给dev->pins->default_state。如果查找失败,函数会打印一条调试信息,并将返回值设置为0,表示继续执行。
第23 行调用了pinctrl_lookup_state 函数来查找设备的pinctrl 状态,该函数定义在内核源码目录下的“drivers/pinctrl/core.c”文件中,具体内容如下所示:

struct pinctrl_state *pinctrl_lookup_state(struct pinctrl *p,const char *name)
{
    struct pinctrl_state *state;
    // 在状态链表中查找指定名称的状态对象
    state = find_state(p, name);
    if (!state) {
        if (pinctrl_dummy_state) {
            /* 创建虚拟状态*/
            dev_dbg(p->dev, "使用pinctrl 虚拟状态(%s)\n",name);
            // 如果找不到指定的状态对象,并且存在虚拟状态,则创建一个虚拟状态对象
            state = create_state(p, name);
        } else
            // 如果找不到指定的状态对象,并且不存在虚拟状态,则返回错误指针-ENODEV
            state = ERR_PTR(-ENODEV);
    }
    return state;
}
  • (2)第30-41 行:查找设备的初始化pinctrl 状态。使用pinctrl_lookup_state 函数通过dev->pins->pPINCTRL_STATE_INIT 参数查找设备的初始化pinctrl 状态,并将其赋值给dev->pins->init_state。如果查找失败,函数会打印一条调试信息。如果找不到初始化状态,会选择默认的pinctrl 状态,并将返回值设置为pinctrl_select_state 函数的返回值。
  • (3)第48-56 行:如果配置了电源管理(CONFIG_PM 宏定义),则会继续执行以下步骤。
    首先,查找设备的睡眠pinctrl 状态。使用pinctrl_lookup_state 函数通过dev->pins->pPINCTRL_STATE_SLEEP 参数查找设备的睡眠pinctrl 状态,并将其赋值给dev->pins->sleep_state。如果查找失败,函数会打印一条调试信息。
  • (4)第58-64 行:查找设备的空闲pinctrl 状态。使用pinctrl_lookup_state 函数通过dev->pins->p 和PINCTRL_STATE_IDLE 参数查找设备的空闲pinctrl 状态,并将其赋值给dev->pins->idle_state。如果查找失败,函数会打印一条调试信息。函数返回0,表示引脚绑定成功。
    • 第37 行和第40 行会使用pinctrl_select_state 选择并切换到指定的pinctrl_state(引脚控制状态),该函数定义在内核源码目录下的“drivers/pinctrl/core.c”文件中,具体内容如下所示:
int pinctrl_select_state(struct pinctrl *p, struct pinctrl_state *state)
{
    // 如果当前状态已经是要选择的状态,则无需进行任何操作,直接返回0 表示成功
    if (p->state == state)
        return 0;
    
    // 调用pinctrl_commit_state 函数来应用并切换到新的状态
    return pinctrl_commit_state(p, state);
}

第8 行的返回值中又调用的pinctrl_commit_state 函数应用并切换到新的状态,该函数的具体内容如下所示:

static int pinctrl_commit_state(struct pinctrl *p, struct pinctrl_state *state)
{
    struct pinctrl_setting *setting, *setting2;
    struct pinctrl_state *old_state = p->state;
    int ret;
    if (p->state) {
        /*
        * 对于旧状态中的每个引脚复用设置,取消SW 记录的该引脚组的复用所有者。
        * 任何仍由新状态拥有的引脚组将在下面循环中的pinmux_enable_setting() 调用中重新获取。
        */
        list_for_each_entry(setting, &p->state->settings, node) {
            if (setting->type != PIN_MAP_TYPE_MUX_GROUP)
                continue;
            pinmux_disable_setting(setting);
        }
    }
    
    p->state = NULL;
    
    /* 应用新状态的所有设置*/
    list_for_each_entry(setting, &state->settings, node) {
        switch (setting->type) {
            case PIN_MAP_TYPE_MUX_GROUP:
                ret = pinmux_enable_setting(setting);
            break;

            case PIN_MAP_TYPE_CONFIGS_PIN:
            case PIN_MAP_TYPE_CONFIGS_GROUP:
                ret = pinconf_apply_setting(setting);
            break;

            default:
                ret = -EINVAL;
            break;
        }
        if (ret < 0) {
            // 如果应用设置失败,则回滚新状态的设置
            goto unapply_new_state;
        }
    }
    
    p->state = state;
    return 0;
    
unapply_new_state:
    // 回滚新状态的设置
    list_for_each_entry_safe(setting, setting2, &state->settings, node) {
        switch (setting->type) {
            case PIN_MAP_TYPE_MUX_GROUP:
            pinmux_disable_setting(setting);
            break;

            case PIN_MAP_TYPE_CONFIGS_PIN:
            case PIN_MAP_TYPE_CONFIGS_GROUP:
            pinconf_remove_setting(setting);
            break;
        }
    }
    // 回滚完成后,将状态恢复为旧状态
    p->state = old_state;
    return ret;
}
  • (1)第4 行:保存当前的状态对象到变量old_state 中。
  • (2)第7-18 行:检查是否存在旧的状态,如果存在则取消记录在软件中的旧状态中每个引脚复用设置的复用所有者。这样做是为了确保任何仍由新状态拥有的引脚组在后面的循环中重新获取(通过pinmux_enable_setting()函数调用)。
  • (3)第20 行:将当前的状态设置为NULL,以便在应用新状态时可以正确处理。
  • (4)第22-41 行:遍历新状态的所有设置,并根据设置的类型执行相应的操作:
    • 对于引脚复用设置(PIN_MAP_TYPE_MUX_GROUP),调用pinmux_enable_setting()函数来启用该设置。
    • 对于引脚配置设置(PIN_MAP_TYPE_CONFIGS_PIN 或PIN_MAP_TYPE_CONFIGS_GROUP),调用pinconf_apply_setting()函数来应用该设置。
    • 对于其他类型的设置,将返回一个错误码(-EINVAL)。
    • 如果应用设置失败,则跳转到标签unapply_new_state,执行回滚操作。

最后对52 行的pinmux_enable_setting 函数和56 行的pinconf_apply_setting()函数进行讲解,pinmux_enable_setting 函数定义在内核源码目录下的“drivers/pinctrl/pinmux.c”文件中,具体内容如下所示:

int pinmux_enable_setting(const struct pinctrl_setting *setting)
{
    // 获取相关结构体指针
    struct pinctrl_dev *pctldev = setting->pctldev;
    const struct pinctrl_ops *pctlops = pctldev->desc->pctlops;
    const struct pinmux_ops *ops = pctldev->desc->pmxops;
    int ret = 0;
    const unsigned *pins = NULL;
    unsigned num_pins = 0;
    int i;
    struct pin_desc *desc;
    
    // 如果pctlops->get_group_pins 函数存在,则调用该函数获取组中的引脚信息
    if (pctlops->get_group_pins)
        ret = pctlops->get_group_pins(pctldev, setting->data.mux.group, &pins, &num_pins);
    if (ret) {
        const char *gname;
        // 错误只影响调试数据,因此只发出警告
        gname = pctlops->get_group_name(pctldev,setting->data.mux.group);
        dev_warn(pctldev->dev, "could not get pins for group %s\n", gname);
        num_pins = 0;
    }
    
    // 逐个申请组中的引脚
    for (i = 0; i < num_pins; i++) {
        ret = pin_request(pctldev, pins[i], setting->dev_name, NULL);
        if (ret) {
            const char *gname;
            const char *pname;
            desc = pin_desc_get(pctldev, pins[i]);
            pname = desc ? desc->name : "non-existing";
            gname = pctlops->get_group_name(pctldev, setting->data.mux.group);
            dev_err(pctldev->dev, "could not request pin %d (%s) from group %s on device %s\n",
                    pins[i], pname, gname, pinctrl_dev_get_name(pctldev));
            goto err_pin_request;
        }
    }
    
    // 分配引脚后,编码复用设置
    for (i = 0; i < num_pins; i++) {
        desc = pin_desc_get(pctldev, pins[i]);
        if (desc == NULL) {
            dev_warn(pctldev->dev, "could not get pin desc for pin %d\n", pins[i]);
            continue;
        }
        desc->mux_setting = &(setting->data.mux);
    }
    
    // 调用ops->set_mux 函数设置复用
    ret = ops->set_mux(pctldev, setting->data.mux.func, setting->data.mux.group);
    if (ret)
        goto err_set_mux;
    return 0;
    
err_set_mux:
    // 复用设置失败,清除复用设置
    for (i = 0; i < num_pins; i++) {
        desc = pin_desc_get(pctldev, pins[i]);
        if (desc)
            desc->mux_setting = NULL;
    }
    
err_pin_request:
    // 在错误发生时释放已申请的引脚
    while (--i >= 0)
        pin_free(pctldev, pins[i], NULL);
    return ret;
}

该函数用于启用引脚复用设置。
(1)第13-28 行:调用pctlops->get_group_pins 函数获取指定组中的引脚信息,如果函数存在,则调用该函数,并将引脚信息存储在pins 和num_pins 变量中。如果获取引脚信息失败,发出警告并将num_pins 设置为0。
(2)第30-48 行:使用pin_request 函数申请引脚,并传入引脚控制器设备、引脚编号、设备名称和其他参数。如果申请引脚失败,发出错误信息并跳转到错误处理步骤。
(3)第50-60 行:分配引脚后,使用pin_desc_get 函数获取引脚描述符,并将复用设置指针指向引脚复用信息。
(4)第62-64 行:调用ops->set_mux 函数设置引脚复用,传入引脚控制器设备、复用功能和组信息,以便设置引脚复用。
(5)第66 行:如果引脚复用设置失败,跳转到错误处理步骤。
pinconf_apply_setting()函数定义在内核源码目录下的“drivers/pinctrl/pinconf.c”文件中,具体内容如下所示:

int pinconf_apply_setting(const struct pinctrl_setting *setting)
{
    // 获取相关结构体指针
    struct pinctrl_dev *pctldev = setting->pctldev;
    const struct pinconf_ops *ops = pctldev->desc->confops;
    int ret;
    
    // 检查是否存在pinconf 操作函数集
    if (!ops) {
        dev_err(pctldev->dev, "missing confops\n");
        return -EINVAL;
    }
    
    // 根据设置类型选择相应的操作
    switch (setting->type) {
        case PIN_MAP_TYPE_CONFIGS_PIN:
            // 检查是否存在pin_config_set 操作函数
            if (!ops->pin_config_set) {
                dev_err(pctldev->dev, "missing pin_config_set op\n");
                return -EINVAL;
            }

            // 调用pin_config_set 函数设置单个引脚的配置
            ret = ops->pin_config_set(pctldev,
            setting->data.configs.group_or_pin,
            setting->data.configs.configs,
            setting->data.configs.num_configs);

            if (ret < 0) {
                dev_err(pctldev->dev, "pin_config_set op failed for pin %d\n", 
                        setting->data.configs.group_or_pin);
                return ret;
            }
        break;

        case PIN_MAP_TYPE_CONFIGS_GROUP:
            // 检查是否存在pin_config_group_set 操作函数
            if (!ops->pin_config_group_set) {
                dev_err(pctldev->dev, "missing pin_config_group_set op\n");
                return -EINVAL;
            }

            // 调用pin_config_group_set 函数设置引脚组的配置
            ret = ops->pin_config_group_set(pctldev,
            setting->data.configs.group_or_pin,
            setting->data.configs.configs,
            setting->data.configs.num_configs);

            if (ret < 0) {
                dev_err(pctldev->dev, "pin_config_group_set op failed for group %d\n",  
                        setting->data.configs.group_or_pin);
                return ret;
            }
        break;

        default:
            return -EINVAL;
    }
    return 0;
}

该函数用于应用引脚配置设置。
(1)第8-12 行:检查是否存在引脚配置操作函数集,如果不存在,则返回错误码-EINVAL。
(2)第14-59:根据设置类型进行相应的处理:

  • 如果设置类型为PIN_MAP_TYPE_CONFIGS_PIN,表示对单个引脚进行配置设置。然后检查是否存在pin_config_set 操作函数,如果存在则调用pin_config_set 函数设置单个引脚的配置。如果设置失败,则返回相应的错误码。
  • 如果设置类型为PIN_MAP_TYPE_CONFIGS_GROUP,表示对引脚组进行配置设置,然后检查是否存在pin_config_group_set 操作函数,如果存在则调用pin_config_group_set 函数设置引脚组的配置。如果设置失败,则返回相应的错误码。
  • 如果设置类型不是上述两种类型,则返回错误码-EINVAL

至此,关于pinctrl_bind_pins 函数的重要内容就讲解完成了,通过pinctrl_bind_pins 函数实现了为给定的设备绑定引脚,并在绑定过程中选择和设置适当的pinctrl 状态,在124.1 小节最后提出的struct pinctrl_state *default_statepinctrl_map 结构体是什么时候建立起联系的问题也就解决了。

到这里关于pinctrl 子系统第二阶段的讲解也就完成了,为了帮助大家整合讲解过的知识点,作者绘制了以下思维导图:
注:该思维导图的存放路径为iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\05_思维导图\02_pinctrl 阶段2.jpg

image-20240920104134876

大家可以根据该思维导图,对上面章节的内容进行梳理,从而真正理解pinctrl 子系统框架。

第127 章猜想验证

经过了前面章节的学习,我们已经对pinctrl 子系统中有了一定的了解,下面就来解决我们在120.2 小节中提出的问题。

首先对问题进行一下复述,假如我们要配置一个LED 外设,该LED 需要使用一个管脚来进行亮灭的控制,那这个控制引脚需要复用成GPIO 之后才能完成相应的功能,那pinctrl 子系统是什么时候对该引脚进行的复用呢?

首先我们可以提出两种猜想,第一个猜想是在加载LED 驱动的时候进行的pinctrl 引脚复用,第二种猜想是在加载pinctrl 驱动的时候完成的引脚复用。下面对这两种猜想进行验证。

1.猜想1 验证

第一个猜想是在加载LED 驱动的时候进行的pinctrl 引脚复用,这就符合我们124 章、125章、126 章所讲解的内容。当LED 灯的设备树和驱动匹配之后,就会进入驱动中编写的probe函数, 在此之前会执行“ drivers/base/dd.c ” 文件中的really probe 函数中的子函数pinctrl_bind_pins,该函数会为给定的设备绑定引脚,并在绑定过程中选择和设置适当的pinctrl状态。具体的绑定细节可以去前面的章节中查找。

2.猜想2 验证

第二种猜想是在加载pinctrl 驱动的时候完成的引脚复用,因为pinctrl 子系统也是符合设备模型的规范, 也会执行相应的probe 函数, 所以同样的加在pinctrl 驱动时也会执行“drivers/base/dd.c”文件中的really probe 函数中的子函数pinctrl_bind_pins,那这时会进行pinctrl 管脚的复用设置吗,接下来我们对此进行深入的分析。

在pinctrl 的probe 函数执行之前,会调用pinctrl_bind_pins 函数,根据124.2 小节中讲解到的内容可以知道,根据函数的嵌套,首先会调用的create_pinctrl 函数创建struct pinctrl 类型的引脚控制器,create_pinctrl 函数内容如下所示:

static struct pinctrl *create_pinctrl(struct device *dev, struct pinctrl_dev *pctldev)
{
    struct pinctrl *p;
    const char *devname;
    struct pinctrl_maps *maps_node;
    int i;
    const struct pinctrl_map *map;
    int ret;
    
    /*
    * 为每个映射创建状态cookie 持有者struct pinctrl。
    * 这是当使用pinctrl_get() 请求引脚控制句柄时消费者将获得的对象。
    */
    p = kzalloc(sizeof(*p), GFP_KERNEL);
    if (!p)
        return ERR_PTR(-ENOMEM);
    
    p->dev = dev;
    INIT_LIST_HEAD(&p->states);
    INIT_LIST_HEAD(&p->dt_maps);
    
    ret = pinctrl_dt_to_map(p, pctldev);
    if (ret < 0) {
        kfree(p);
        return ERR_PTR(ret);
    }
    
    devname = dev_name(dev);
    mutex_lock(&pinctrl_maps_mutex);
    
    /* 遍历引脚控制映射以定位正确的映射*/
    for_each_maps(maps_node, i, map) {
        /* 映射必须适用于此设备*/
        if (strcmp(map->dev_name, devname))
            continue;
        /*
        * 如果pctldev 不为空,我们正在声明它的独占使用权,
        * 这意味着它自己提供了该设置。
        *
        * 因此,我们必须跳过适用于此设备但由其他设备提供的映射。
        */
        if (pctldev && strcmp(dev_name(pctldev->dev), map->ctrl_dev_name))
            continue;
        ret = add_setting(p, pctldev, map);
        /*
        * 在这一点上,添加设置可能会导致:
        *
        * - 延迟,如果引脚控制设备尚不可用
        * - 失败,如果引脚控制设备尚不可用,
        * 并且该设置是一个独占设置。我们不能推迟它,因为
        * 该独占设置会在设备注册后立即生效。
        *
        * 如果返回的错误不是-EPROBE_DEFER,则我们将
        * 累积错误,以查看是否最终得到-EPROBE_DEFER,
        * 因为那是最糟糕的情况。
        */
        if (ret == -EPROBE_DEFER) {
            pinctrl_free(p, false);
            mutex_unlock(&pinctrl_maps_mutex);
            return ERR_PTR(ret);
        }
    }
    mutex_unlock(&pinctrl_maps_mutex);
    
    if (ret < 0) {
        /* 如果发生了除推迟以外的其他错误,则在此处返回*/
        pinctrl_free(p, false);
        return ERR_PTR(ret);
    }
    
    kref_init(&p->users);
    
    /* 将引脚控制句柄添加到全局列表*/
    mutex_lock(&pinctrl_list_mutex);
    list_add_tail(&p->node, &pinctrl_list);
    mutex_unlock(&pinctrl_list_mutex);
    
    return p;
}

在第22 行会调用pinctrl_dt_to_map 函数将设备树中定义的引脚映射信息转换为struct pinctrl_map 结构,并将其添加到p->dt_maps 链表中。该函数定义在内核源码目录下的“drivers/pinctrl/devicetree.c”文件中,具体内容如下所示:

int pinctrl_dt_to_map(struct pinctrl *p, struct pinctrl_dev *pctldev)
{
    struct device_node *np = p->dev->of_node; // 获取引脚控制器关联设备的设备树节点
    int state, ret;
    char *propname;
    struct property *prop;
    const char *statename;
    const __be32 *list;
    int size, config;
    phandle phandle;
    struct device_node *np_config;
    
    /* 如果CONFIG_OF 启用,且p->dev 不是从设备树实例化而来*/
    if (!np) {
        if (of_have_populated_dt())
            dev_dbg(p->dev, "no of_node; not parsing pinctrl DT\n");
        return 0;
    }
    
    /* 节点内部存储属性名称的指针*/
    of_node_get(np);
    
    /* 对于每个定义的状态ID */
    for (state = 0;; state++) {
        /* 获取pinctrl-* 属性*/
        propname = kasprintf(GFP_KERNEL, "pinctrl-%d", state);
        prop = of_find_property(np, propname, &size);
        kfree(propname);
        if (!prop) {
            if (state == 0) {
                of_node_put(np);
                return -ENODEV;
            }
            break;
        }
        
        list = prop->value;
        size /= sizeof(*list);
        
        /* 判断pinctrl-names 属性是否命名了该状态*/
        ret = of_property_read_string_index(np, "pinctrl-names", state, &statename);
        /*
        * 如果未命名,则statename 仅是整数状态ID。但是,为了避免动态分配和之后要释放的
        麻烦,
        * 可以直接将statename 指向属性名称的一部分。
        */
        if (ret < 0) {
            /* strlen("pinctrl-") == 8 */
            statename = prop->name + 8;
        }
        
        /* 对于其中的每个引用的引脚配置节点*/
        for (config = 0; config < size; config++) {
            phandle = be32_to_cpup(list++);
            
            /* 查找引脚配置节点*/
            np_config = of_find_node_by_phandle(phandle);
            if (!np_config) {
                dev_err(p->dev, "prop %s index %i invalid phandle\n", prop->name, config);
                ret = -EINVAL;
                goto err;
            }
            
            /* 解析节点*/
            ret = dt_to_map_one_config(p, pctldev, statename, np_config);
            of_node_put(np_config);
            if (ret < 0)
                goto err;
        }
        
        /* 如果在设备树中没有条目,则生成一个虚拟状态表条目*/
        if (!size) {
            ret = dt_remember_dummy_state(p, statename);
            if (ret < 0)
                goto err;
        }
    }
    return 0;
    err:
    pinctrl_dt_free_maps(p);
    return ret;
}

这里传递过来的是pinctrl 的设备树节点,在24-76 行的for 循环中会获取pinctrl-* 属性,而在pinctrl 节点中并没有该属性,pinctrl-* 属性是在一系列的设备节点中添加的,所以会在这里返回错误,同样的错误会一层层的向上级函数传递,最终导致pinctrl_bind_pins 函数返回错误,从而不能设置引脚的复用,所以猜想2 是不正确的。

在121 章中也讲解了瑞芯微的pinctrl 的probe 函数,在该函数中有一个这样的调用关系:

rockchip pinctrl_probe
    rockchip_pinctrl_registér
        devm_pinctrl_register
            pinctrl_register
                pinctrl_enable
                    pinctrl_claim_hogs
                        create_pinctrl

从上面的调用关系可以得到pinctrl 的probe 函数最后也会调用create_pinctrl 来创建struct pinctrl 类型的引脚控制器,从而实现pinctrl 引脚复用设置,同样的这是的设置也是不成功的,
pinctrl_claim_hogs 函数定义在内核源码目录下的“drivers/pinctrl/core.c”文件中,具体内容如下所示:

static int pinctrl_claim_hogs(struct pinctrl_dev *pctldev)
{
    pctldev->p = create_pinctrl(pctldev->dev, pctldev);
    if (PTR_ERR(pctldev->p) == -ENODEV) {
        dev_dbg(pctldev->dev, "no hogs found\n");
        return 0;
    }
    
    if (IS_ERR(pctldev->p)) {
        dev_err(pctldev->dev, "error claiming hogs: %li\n", PTR_ERR(pctldev->p));
        return PTR_ERR(pctldev->p);
    }
    
    pctldev->hog_default = pinctrl_lookup_state(pctldev->p, PINCTRL_STATE_DEFAULT);
    if (IS_ERR(pctldev->hog_default)) {
        dev_dbg(pctldev->dev, "failed to lookup the default state\n");
    } else {
        if (pinctrl_select_state(pctldev->p,pctldev->hog_default))
            dev_err(pctldev->dev,"failed to select default state\n");
    }
    
    pctldev->hog_sleep = pinctrl_lookup_state(pctldev->p, PINCTRL_STATE_SLEEP);
    if (IS_ERR(pctldev->hog_sleep))
        dev_dbg(pctldev->dev, "failed to lookup the sleep state\n");
    return 0;
}

该函数和pinctrl_bind_pins 函数内容相似,所以也会在第二行的create_pinctrl 函数返回错误,所以也就无法执行后面的pinctrl 状态的选择和设置了,所以猜想2 不成立。

至此,关于pinctrl 的相关知识和疑问就都讲解和解答完成了。

第十二篇GPIO 子系统

128.1 什么是GPIO

GPIO 是干什么的呢?从字面意思来看,GPIO=General-Purpose Input/Output(通用输入输出),是一种软件运行期间能够动态配置和控制的通用引脚。通用,就是说它是万金油,干什么都行。输入输出,就是说既能当输入口使用,又能当输出口使用。端口,就是元器件上的一个引脚。

所有的GPIO 在上电后的初始状态都是输入模式,可以通过软件设为上拉或下拉,也可以输入中断信号,驱动强度都是可编程的

我们学linux 、单片机的第一个操作硬件就是点亮第一个led 灯,也就是控制GPIO 的高低电平。在单片机上我们控制一个GPIO 非常的简单,直接操作引脚就可以了。虽然linux 听着比较高大上,但是控制一个GPIO 是非常容易的。我们甚至不用去写驱动,直接命令操作就可以了,因为linux 系统本身有好多成熟的驱动框架,使用这些框架的好处就是当我们更换平台的时候,比如换到RK3568 平台,我们应用程序可以几乎不用做任何改变,就可以直接在新的平台上运行。但是比如说我在stm32 单片机上写了一个控制GPIO 的程序,我现在想把它移植到51 单片机的话,这个移植过程是比较麻烦的,但是在linux 上对于GPIO 设备,甚至可以不用写程序,可以直接在命令行操作。

GPIO 的实际应用举例,比如按键输入,当按下按键的时候,GPIO 引脚的状态会发生变化,可以通过读取GPIO 引脚的状态来检测按键事件,并进行相应的处理。原理图如下图所示:

image-20240920110214432

控制led 灯,GPIO 可以控制LED 的状态,通过设备GPIO 引脚的输出状态,可以控制LED的亮灭,实现指示灯,状态指示等功能。原理图如下所示:

image-20240920110233675

128.2 GPIO 引脚分布

RK3568 有5 组GPIO:GPIO0 到GPIO4。每组GPIO 又以A0 到A7,B0 到B7,C0 到C7,D0 到D7,作为区分的编号。所以RK3568 上的GPIO 是不是应该有5*4*8=160 个呢?但是为什么在数据手册中有152 个GPIO 呢?如下图所示:

image-20240920110319532

实际上RK3568 一共有152 个GPIO , 其中GPIO0_D2 , GPIO0_D7 , GPIO2_C7 ,GPIO4_D3~GPIO4_D7 是没有的,所以是152 个GPIO。

128.3 GPIO 电气属性

我们以RK3568 为例,以具体CPU 的数据手册为准。RK3568 上的GPIO 可以设置为3.3V,也可以设置为1.8V。在实际编程时,高电平(3.3V 或1.8V)用1 表示,低电平用0 表示。

image-20240920110552601

那么我们如何确定RK3568 的GPIO 电平是3.3V 还是1.8V 呢?具体操作方法如下所示:

  • 1 首先打开RK3568 的底板原理图,在底板原理图上查找使用的引脚,查找到引脚对应到核心板连接器上的网络标号。
  • 2 然后打开RK3568 的核心板原理图,在核心板原理图上查找在上一步骤中找到的引脚网络标号,如下图所示,查找到引脚对应的GPIO 和引脚所连接的电源域。

image-20240920110647445

  • 3 然后查找对应的电源域,如下图所示,对应的电压值就是GPIO 引脚的电压。

image-20240920110713589

128.4 GPIO 电气特性

我们以RK3568 为例,打开RK3568 的数据手册,在GPIO 章节中如下所示:

image-20240920110730077

GPIO 是可编程的GPIO,GPIO 除了IO 电平,还有驱动强度,上拉和下拉,这些概念解释如下:

  • 驱动强度(Drive Strength):GPIO 的驱动强度决定了它可以提供的输出电流。通过软件配置,您可以选择合适的驱动强度,以确保GPIO 能够驱动所连接的外部设备或电路。
  • 上拉(Pull-up)和下拉(Pull-down):GPIO 引脚可以通过上拉或下拉电阻来确定其默认电平状态。通过软件配置,您可以选择启用上拉或下拉电阻,以确保GPIO 在未连接外部设备时保持稳定的默认状态。
  • 中断(Interrupt):通过软件配置,您可以启用GPIO 中断功能,以便在GPIO 状态发生变化时及时获得通知。这对于实现事件驱动的应用程序非常有用,可以通过中断来处理GPIO触发的事件。
  • 多功能引脚(Multipurpose Pins):一些GPIO 引脚可能具有多种功能,可以通过软件配置来选择不同的功能。例如,一个GPIO 引脚可以配置为数字输入、数字输出、PWM 输出等。

在设备树pinctl 节点,可以对上述功能进行配置,如下所示:

image-20240920110958486

节点中描述了引脚的配置,比如说<1 RK_PD5 1 &pcfg_pull_up_drv_level_2>描述的是GPIO1_D5 引脚,复用模式为模式1(复用模式可以查看RK3568 的参考手册),GPIO 引脚的上拉驱动强度为2。

第129 章GPIO 控制和操作实验

GPIO 软件编程方式有多种,可以写驱动程序调用GPIO 函数操作GPIO,也可以直接通过操作寄存器的方式操作GPIO,还可以通过sysfs 方式实现对GPIO 的控制。本章节我们来学习使用sysfs 方式实现对GPIO 的控制。

129.1 使用命令通过sysfs 文件系统控制GPIO

129.1.1 内核配置

使用sysfs 方式控制gpio,首先需要底层驱动的支持,需要在make menuconfig 图形化配置界面中加入以下配置:
Device Drivers
->GPIO Support
->/sys/class/gpio/xxxx

image-20240920111251794

129.1.2 GPIO 编号计算

iTOP-RK3568 有5 组GPIO bank:GPIO0GPIO4,每组又以A0A7, B0B7, C0C7, D0~D7 作为编号区分,常用以下公式计算引脚:

GPIO pin 脚计算公式:pin = bank * 32 + number 	//bank 为组号,number 为小组编号
GPIO 小组编号计算公式:number = group * 8 + X

引脚编号= 控制寄存器的寄存器基数+ 控制引脚寄存器位数。在rk3568 中,
GPIO_number 的计算方法为: n*32 + (K-A)*8 + x; 括号里面的A、B、C、D 分别代表数值0、1、2、3, 在计算时候分别对应即可。
下面演示LED9 用户LED 灯的GPIO0_PB7 pin 脚计算方法:

bank = 0; //GPIO0_B7=> 0, bank ∈ [0,4]
group = 1; //GPIO0_B7 => 1, group ∈ {(A=0), (B=1), (C=2), (D=3)}
X = 7; //GPIO4_D7 => 5, X ∈ [0,7]
number = group * 8 + X = 1 * 8 + 7 =15
pin = bank*32 + number= 0 * 32 + 15 = 15;

129.1.3 使用sysfs 控制接口控制GPIO

sysfs 控制接口为/sys/class/gpio/export 和/sys/class/gpio/unexport。如下图所示:

image-20240920111729287

/sys/class/gpio/export 用于将GPIO 控制从内核空间导出到用户空间。/sys/class/gpio/unexport 用于取消GPIO 控制从内核空间到用户空间的导出。export 和unexport,他们都是只写的。GpiochipX 代表GPIO 控制器。

export:用于将指定编号的GPIO 引脚导出。在使用GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。注意export 文件是只写文件,不能读取,将一个指定的编号写入到export 文件中即可将对应的GPIO 引脚导出,以GPIO0_PB7 为例(pin 计算值为15)使用export 文件进行导出(如果没有更换本章开始部分的内核设备树镜像,会导出不成功),导出成功如下图所示:

echo 15 > export

image-20240920111913241

会发现在/sys/class/gpio 目录下生成了一个名为gpio15 的文件夹(gpioX,X 表示对应的编号),该文件夹就是导出来的GPIO 引脚对应的文件夹,用于管理、控制该GPIO 引脚。

**unexport**:将导出的GPIO 引脚删除。当使用完GPIO 引脚之后,需要将导出的引脚删除,同样该文件也是只写文件、不可读,使用unexport 文件进行删除GPIO0_PB7,删除成功如下图所示:

echo 15 > unexport

image-20240920112029733

可以看到之前生成的gpio15 文件夹就会消失!
需要注意的是,并不是所有GPIO 引脚都可以成功导出,如果对应的GPIO 已经被导出或者在内核中被使用了,那便无法成功导出,导出失败如下图所示:

image-20240920112148270

出现上图报错的原因是该GPIO 已经被其他GPIO 使用,需要在内核中找到使用GPIO 的驱动,并取消该驱动才可以正常使用GPIO。在使用GPIO15 时,需要取消Linux 内核源码中LED 灯的配置,如下所示:

image-20240920112201536

再次使用以下命令导出GPIO0_PB7 引脚,导出成功之后进入gpio15 文件夹如下图所示:

echo 15 > export

image-20240920112240786

可以看到gpio15 文件夹下分别有active_low、device、direction、edge、power、subsystem、uevent、value 八个文件,需要关心的文件是active_lowdirectionedge 以及value 这四个属性文件,接下来分别介绍这四个属性文件的作用:

**direction**:配置GPIO 引脚为输入或输出模式。该文件可读、可写,读表示查看GPIO 当前是输入还是输出模式,写表示将GPIO 配置为输入或输出模式;读取或写入操作可取的值为”out”(输出模式)和”in”(输入模式)。

在“/sys/class/gpio/gpio15”目录下使用cat 命令查看direction 输入输出模式,如下图所示:

cat direction

image-20240920112359717

默认状态下的输入输出状态为“in”,由于direction 为可读可写,可以使用以下命令将模式配置为输出,配置完成如下图所示

echo out > direction
cat direction

image-20240920112427642

**active_low**:用于控制极性得属性文件,可读可写,默认情况下为0,使用cat 命令进行文件内容的查看,如下图所示:

cat active_low

image-20240920113026722

当active_low 等于0 时, value 值若为1 则引脚输出高电平,value 值若为0 则引脚输出低电平。当active_low 等于1 时,value 值若为0 则引脚输出高电平,value 值若为1 则引脚输出低电平。

**edge**:控制中断的触发模式,该文件可读可写。在配置GPIO 引脚的中断触发模式之前,需将其设置为输入模式,四种触发模式的设置如下所示:

非中断引脚:echo "none" > edge
上升沿触发:echo "rising" > edge
下降沿触发:echo "falling" > edge
边沿触发: echo "both" > edge

value: 设置高低电平,如果我们要把这个管脚设置成高电平,我们只需要给value 设置成1即可,反之,则设置成0。使用命令

echo 1 > value

反之,把GPIO 设置成低电平,使用命令

echo 0 > value

image-20240920113329177

129.2 使用C 程序通过sysfs 文件系统控制GPIO

129.2.1 控制GPIO 输出实验

本小节代码在配套资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\82_gpioctrl01”目录下。
实验要求:
通过GPIO 输出应用程序控制GPIO 口输出高低电平,以此来控制LED 灯的亮灭。

实验步骤:
首先进入ubuntu 的终端界面输入以下命令来创建gpioctrl.c 文件,如下图所示:

image-20240920113413495

然后向该文件中添加以下内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int fd;                   // 文件描述符
int ret;                  // 返回值
char gpio_path[100];      // GPIO路径
int len;                  // 字符串长度

// 导出GPIO引脚
int gpio_export(char *argv)
{
    fd = open("/sys/class/gpio/export", O_WRONLY); // 打开export文件
    if (fd < 0) {
        printf("open /sys/class/gpio/export error \n"); // 打开文件失败
        return -1;
    }
    len = strlen(argv); // 获取参数字符串的长度
    ret = write(fd, argv, len); // 将参数字符串写入文件,导出GPIO引脚
    if (ret < 0)
    {
        printf("write /sys/class/gpio/export error \n"); // 写入文件失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 取消导出GPIO引脚
int gpio_unexport(char *argv)
{
    fd = open("/sys/class/gpio/unexport", O_WRONLY); // 打开unexport文件
    if (fd < 0)
    {
        printf("open /sys/class/gpio/unexport error \n"); // 打开文件失败
        return -1;
    }
    len = strlen(argv); // 获取参数字符串的长度
    ret = write(fd, argv, len); // 将参数字符串写入文件,取消导出GPIO引脚
    if (ret < 0)
    {
        printf("write /sys/class/gpio/unexport error \n"); // 写入文件失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 控制GPIO引脚的属性
int gpio_ctrl(char *arg, char *val)
{
    char file_path[100]; // 文件路径
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径,格式为“gpio_path/arg”
    fd = open(file_path, O_WRONLY); // 打开文件
    if (fd < 0)
    {
        printf("open file_path error \n"); // 打开文件失败
        return -1;
    }
    len = strlen(val); // 获取参数字符串的长度
    ret = write(fd, val, len); // 将参数字符串写入文件,控制GPIO引脚的属性
    if (ret < 0)
    {
        printf("write file_path error\n"); // 写入文件失败
        return -2;
    }
    close(fd); // 关闭文件
}

int main(int argc, char *argv[]) // 主函数
{
    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建GPIO路径,格式为“/sys/class/gpio/gpio引脚号”
    if (access(gpio_path, F_OK)) // 检查GPIO路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出GPIO引脚
    }
    else
    {
        gpio_unexport(argv[1]); // 存在则取消导出GPIO引脚
    }

    gpio_ctrl("direction", "out"); // 配置GPIO为输出模式
    gpio_ctrl("value", argv[2]);   // 控制GPIO输出高低电平

    gpio_unexport(argv[1]); // 最后取消导出GPIO引脚

    return 0; // 返回0表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c 进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH 
aarch64-linux-gnu-gcc gpioctrl.c -o gpioctrl

image-20240920113821557

最后将交叉编译生成的gpioctrl 文件拷贝到开发板目录下运行即可,如下所示:

image-20240920113833124

输入“./gpioctrl 15 1”命令LED 灯点亮,输入“./gpioctrl 15 0”命令LED 灯熄灭。到此,实验结束。

129.2.2 控制GPIO 输入实验

本小节代码在配套资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\83_gpioctrl02”目录下。

实验要求:
通过GPIO 输入应用程序读取GPIO 口的输入电平。
实验硬件连接:
使用迅为iTOP-RK3568 开发板,使用导线连接开发板背面的引脚GPIO1_B2,另一端连接到电源或者GND。
实验步骤:
首先进入ubuntu 的终端界面输入以下命令来创建gpioctrl.c 文件,如下图所示:

image-20240920113916432

然后向该文件中添加以下内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int fd;              // 文件描述符
int ret;             // 返回值
char gpio_path[100]; // GPIO路径
int len;             // 字符串长度
char file_path[100]; // 文件路径
char buf[2];         // 用于读取 GPIO 值的缓冲区

// 导出 GPIO 引脚
int gpio_export(char *argv)
{
    fd = open("/sys/class/gpio/export", O_WRONLY); // 打开 export 文件
    if (fd < 0)
    {
        printf("open /sys/class/gpio/export error\n"); // 打开文件失败
        return -1;
    }
    len = strlen(argv);         // 获取参数字符串的长度
    ret = write(fd, argv, len); // 将参数字符串写入文件,导出 GPIO 引脚
    if (ret < 0)
    {
        printf("write /sys/class/gpio/export error\n"); // 写入文件失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 取消导出 GPIO 引脚
int gpio_unexport(char *argv)
{
    fd = open("/sys/class/gpio/unexport", O_WRONLY); // 打开 unexport 文件
    if (fd < 0)
    {
        printf("open /sys/class/gpio/unexport error\n"); // 打开文件失败
        return -1;
    }
    len = strlen(argv);         // 获取参数字符串的长度
    ret = write(fd, argv, len); // 将参数字符串写入文件,取消导出 GPIO 引脚
    if (ret < 0)
    {
        printf("write /sys/class/gpio/unexport error\n"); // 写入文件失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 控制 GPIO 引脚的属性
int gpio_ctrl(char *arg, char *val)
{
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径,格式为 "gpio_path/arg"
    fd = open(file_path, O_WRONLY);              // 打开文件
    if (fd < 0)
    {
        printf("open file_path error\n"); // 打开文件失败
        return -1;
    }
    len = strlen(val);         // 获取参数字符串的长度
    ret = write(fd, val, len); // 将参数字符串写入文件,控制 GPIO 引脚的属性
    if (ret < 0)
    {
        printf("write file_path error\n"); // 写入文件失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 读取 GPIO 引脚的值
int gpio_read_value(char *arg)
{
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径,格式为 "gpio_path/arg"
    fd = open(file_path, O_RDONLY);              // 打开文件
    if (fd < 0)
    {
        printf("open file_path error\n"); // 打开文件失败
        return -1;
    }
    ret = read(fd, buf, 1); // 读取文件内容到缓冲区
    if (!strcmp(buf, "1"))
    {
        printf("The value is high\n"); // GPIO 引脚值为高电平
        return 1;
    }
    else if (!strcmp(buf,"0"))
    {
        printf("The value is low\n"); // GPIO 引脚值为低电平
        return 0;
    }
    
    close(fd); // 关闭文件
    return -1;
    
}

int main(int argc, char *argv[]) // 主函数
{
    int value;
    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径,格式为 "/sys/class/gpio/gpio引脚号"
    if (access(gpio_path, F_OK))                           // 检查 GPIO 路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
    }
    else
    {
        gpio_unexport(argv[1]); // 存在则取消导出 GPIO 引脚
    }

    gpio_ctrl("direction", "in");       // 配置 GPIO 为输入模式
    
    value = gpio_read_value("value");   // 读取 GPIO 引脚的值
    printf("The value is %d\n", value); // 打印读取的 GPIO 引脚的值
    gpio_unexport(argv[1]);             // 最后取消导出 GPIO 引脚

    return 0; // 返回 0 表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c 进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH
aarch64-linux-gnu-gcc gpioctrl.c -o gpioctrl

image-20240920114050576

最后将交叉编译生成的gpioctrl 文件拷贝到开发板目录下运行即可。
为了测试输入高电平的状况,作者使用了杜邦线将开发板背面的3.3V 接到了GPIO1_PB2 pin 脚上,然后再次使用以下命令来进行状态的检测,如下图所示:

chmod 777 gpioctrl
./gpioctrl 42

image-20240920114139926

可以看到GPIO1_PB2 pin 脚打印的value 值为高,所以gpio 的状态打印正确。同理,我们将GPIO1_PB2 pin 脚接到GND,再次运行程序,如下图所示:

image-20240920114203652

可以看到GPIO1_PB2 pin 脚打印的value 值为0,所以gpio 的状态打印正确。至此GPIO 输入应用程序在开发板的测试就完成了。

129.3 使用C 程序通过sysfs 文件系统使用GPIO 中断

本小节代码在配套资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\84_gpioctrl03”目录下。
实验要求:
通过GPIO 的输入中断程序,将中断触发方式设置为边沿触发,每当触发中断会打印value的值。

129.3.1 编写应用程序

实验步骤:
首先进入到ubuntu 的终端界面输入以下命令来创建gpioctrl.c 文件,如下图所示:

image-20240920114345272

然后向该文件中添加以下内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>

int fd;              // 文件描述符
int ret;             // 返回值
char gpio_path[100]; // GPIO路径
int len;             // 字符串长度
char file_path[100]; // 文件路径
char buf[2];         // 缓冲区

struct pollfd fds[1]; // poll结构体数组

// 导出GPIO引脚
int gpio_export(char *argv)
{
    fd = open("/sys/class/gpio/export", O_WRONLY); // 打开export文件
    if (fd < 0)
    {
        printf("open /sys/class/gpio/export error \n"); // 打开文件失败
        return -1;
    }
    len = strlen(argv);         // 获取字符串长度
    ret = write(fd, argv, len); // 写入引脚号到export文件
    if (ret < 0)
    {
        printf("write /sys/class/gpio/export error \n"); // 写入失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 取消导出GPIO引脚
int gpio_unexport(char *argv)
{
    fd = open("/sys/class/gpio/unexport", O_WRONLY); // 打开unexport文件
    if (fd < 0)
    {
        printf("open /sys/class/gpio/unexport error \n"); // 打开文件失败
        return -1;
    }
    len = strlen(argv);        // 获取字符串长度
    ret = write(fd, argv, len); // 写入引脚号到unexport文件
    if (ret < 0)
    {
        printf("write /sys/class/gpio/unexport error \n"); // 写入失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 控制GPIO引脚的属性
int gpio_ctrl(char *arg, char *val)
{
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建属性文件的路径
    fd = open(file_path, O_WRONLY);              // 打开属性文件
    if (fd < 0)
    {
        printf("open file_path error \n"); // 打开文件失败
        return -1;
    }
    len = strlen(val);         // 获取字符串长度
    ret = write(fd, val, len); // 写入属性值到属性文件
    if (ret < 0)
    {
        printf("write file_path error\n"); // 写入失败
        return -2;
    }
    close(fd); // 关闭文件
}

// 监听GPIO引脚的中断事件
int gpio_interrupt(char *arg)
{
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径
    fd = open(file_path, O_RDONLY);              // 打开文件
    if (fd < 0)
    {
        printf("open file_path error \n"); // 打开文件失败
        return -1;
    }
    memset((void *)fds, 0, sizeof(fds)); // 清空poll结构体数组
    fds[0].fd = fd;                      // 设置poll结构体的文件描述符
    fds[0].events = POLLPRI;             // 设置poll结构体的事件类型为POLLPRI,表示有紧急数据可读

    read(fd, buf, 2); // 读取文件内容,清除中断事件

    ret = poll(fds, 1, -1); // 调用poll函数等待中断事件发生,阻塞直到事件发生
    if (ret <= 0)
    {
        printf("poll error \n"); // 调用poll失败或超时
        return -1;
    }
    if(fds[0].revents & POLLPRI)
    {
        lseek(fd, 0, SEEK_SET); // 重新定位文件指针到文件开头
        read(fd, buf, 2);       // 读取文件内容,获取中断事件的值
        buf[1] = '\0';
        printf("value is %s\n", buf); // 输出中断事件的值
    }
}

// 读取GPIO引脚的值
int gpio_read_value(char *arg)
{
    sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径
   fd = open(file_path, O_WRONLY); // 打开文件,以只写模式打开是一个错误,应该使用只读模式
    if (fd < 0)
    {
        printf("open file_path error\n"); // 打开文件失败
        return -1;
    }
    ret = read(fd, buf, 1); // 读取文件内容,获取引脚的值
    if (!strcmp(buf, "1"))
    {
        printf("The value is high\n"); // 引脚值为高电平
        return 1;
    }
    else if (!strcmp(buf, "0"))
    {
        printf("The value is low\n"); // 引脚值为低电平
        return 0;
    }
    return -1; // 这里应该返回读取到的引脚值(0或1),而不是返回固定的-1
    close(fd); // 关闭文件(这行代码无法执行到,应该放在read之前)
}

int main(int argc, char *argv[]) // 主函数
{
    int value;
    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建GPIO路径
    if (access(gpio_path, F_OK))                            // 检查GPIO路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出GPIO引脚
    }
    else
    {
        gpio_unexport(argv[1]); // 存在则取消导出GPIO引脚
    }

    gpio_ctrl("direction", "in"); // 设置GPIO引脚为输入模式
    gpio_ctrl("edge", "both");    // 设置GPIO引脚的中断触发方式为上升沿和下降沿
    gpio_interrupt("value");      // 监听GPIO引脚的中断事件

    gpio_unexport(argv[1]); // 最后取消导出GPIO引脚

    return 0; // 返回0表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c 进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH
aarch64-linux-gnu-gcc -o demo64_interrupt demo64_interrupt.c

image-20240920114542255

最后将交叉编译生成的gpioctrl 文件拷贝到开发板目录下即可。

129.3.2 开发板测试

我们使用导线一端连接开发板背面的GPIO1_B2,然后输入以下命令运行程序。

./gpioctrl 42&

由于中断并没有被触发,所以程序会阻塞,等待中断的进行,然后使用杜邦线的另一端将GPIO 底座的3.3V 接到GPIO1_PB2 pin 脚,进行中断的测试,如下图所示:

image-20240920114729836

可以看到中断就被触发了,相应的字符串也被打印了。
至此GPIO 输入中断应用程序在开发板的测试就完成了。

129.4 使用IO 命令操作寄存器控制GPIO

129.4.1 IO 命令

“io” 命令是一个用于Linux 系统的命令行工具,用于读取和写入指定I/O 端口的值。它主要用于与硬件设备进行低级别的交互和调试,在内核阶段读写寄存器。

该命令的语法如下:

io [选项] [地址] [操作] [数据]

其中,选项可以是以下之一:

-b:以字节为单位进行I/O 操作(默认为字)。
-w:以字为单位进行I/O 操作。
-l:以双字为单位进行I/O 操作。

地址是要读取或写入的I/O 端口的十六进制值。
操作可以是以下之一:

r:读取I/O 端口的值。
w:写入数据到I/O 端口。

数据是要写入I/O 端口的十六进制值。

以下是一些使用io 命令的示例:

  1. 读取I/O 端口的值:
io -b -r 0x80

这将以字节为单位读取I/O 端口0x80 的值,并将其显示在终端上。
2. 向I/O 端口写入数据:

io -b -w 0x80 0xAB

这将向I/O 端口0x80 写入十六进制值0xAB。
3. 以字为单位进行读取:

io -w -r 0x1000

这将以字为单位读取I/O 端口0x1000 的值
4. 以双字为单位进行写入:

io -l -w 0x2000 0xDEADBEEF

这将以双字为单位向I/O 端口0x2000 写入十六进制值0xDEADBEEF。

129.4.2 LED 引脚寄存器查找

我们查询到了控制LED 灯的GPIO 为GPIO0_B7。在接下来的实验中需要对GPIO 进行配置,一般情况下需要对GPIO 的复用寄存器,方向寄存器,数据寄存器进行配置。接下来我们打开RK3568 的参考手册part1 查找这几个寄存器的地址。

129.4.2.1 查找复用寄存器

打开参考手册part1 的第三章,GPIOB 的复用寄存器的偏移地址如下(图129-22)所示:

image-20240920115302055

搜索gpio0b7,如下图(图129-23)所示,gpio0b7_selPMU_GRF_GPIO0B_IOMUX_H上,所以偏移地址为0x000C。gpio0b7 可以通过控制[14:12]位来选择复用为哪个功能,我们要控制led 灯,所以功能要复用为gpio。

image-20240920115405416

复用寄存器的基地址如下图(图129-24)所示:

image-20240920115430262

所以**复用寄存器地址=基地址+偏移地址=0xFDC2000C** 。使用io 命令查看此寄存器的地址:

io -r -4 0xFDC2000C

image-20240920115502791

如上图(图129-25)所示,寄存器值为00000001,[14:12]位为000,如下图(图129-26)所示,所以默认设置的为gpio 功能。

image-20240920115520348

129.4.2.2 查找方向寄存器

打开参考手册part1 的第16 章节,数据寄存器的偏移地址如下图(图129-24)所示:

image-20240920115557743

GPIO 有四组GPIO,分别是GPIOA,GPIOB,GPIOC,GPIOD。每组又以A0~A7, B0~B7,C0~C7, D0~D7 作为编号区分。GPIO0B7 在GPIO_SWPORT_DDR_L 上所以,方向寄存器的偏移地址为0x0008。接着查看GPIO_SWPORT_DDR_L 寄存器的具体描述,如下图(图129-25)所示:

如上图(图129-25)所示,[31:16]位属性是WO,也就是只可写入。这[31:16]位是写标志位,是低16 位的写使能。如果低16 位中某一位要设置输入输入输出,则对应高位写标志也应该设置为1。[15:0] 是数据方向控制寄存器低位,如果要设置某个GPIO 为输出,则对应位置1,如果要设置某个GPIO 为输入,则对应位置0。那么GPIO0 B7 ,我们要设置第15 位为输入还是输出,那么对应的[31:16]位写使能也要置1。

打开参考手册part1 的1.1 小节Address Mapping。

image-20240920115852297

image-20240920115900301

如上图(图129-27)所示,GPIO0 的基地址为0xFDD60000方向寄存器的地址=基地址+偏移地址=0xFDD60000+0x0008=0xFDD60008
然后使用IO 命令查看该寄存器的值,如下(图129-28)所示:

image-20240920120018233

如下图(图129-29)所示,第15 位默认为1,设置GPIO0_B7 为输出。

image-20240920120121408

129.4.2.3 查找数据寄存器

打开参考手册part1 的1.1 小节Address Mapping。

image-20240920135250695

image-20240920135258053

如上图(图18-13)所示,GPIO0 的基地址为0xFDD60000
数据寄存器的偏移地址如下(图129-32)所示:

image-20240920135317561

所以数据寄存器的地址为基地址+偏移地址=0xFDD60000。使用IO 命令查看地址的值,如下(图129-33)所示:

image-20240920135336787

我们来看一下这个数据寄存器的描述,如下图(图129-34)所示,

image-20240920135348907

分析上图的方法和在分析方向寄存器的方法同理,由上图可知,如果要控制第15 位为高电平(置1),需要设置31 位为1,那么点亮灯,需要向数据寄存器写入0x8000c040,如下图(图129-35)所示:

image-20240920135415132

如果要灭灯,需要设置第15 位为0 ,第31 位为1,那么向数据寄存器中写入0x80004040,如下图(图129-36)所示:

image-20240920135437807

总结

  • 复用关系寄存器的基地址为0xFDC20000 ,偏移地址为000C ,所以要操作的地址为基地址+偏移地址=0xFDC2000C
  • GPIO 的基地址为0xFDD60000,偏移地址为0x0008,所以方向寄存器要操作的地址为基地址+偏移地址=0xFDD60008,我们要给方向寄存器写入0x80000044 设置为输出。
  • GPIO 的基地址为0xFDD60000,偏移地址为0x0000,所以数据寄存器要操作的地址为基地址+偏移地址=0xFDD60000
  • 默认的数据寄存器的值:0x8000c040 亮灯,0x80004040 灭灯

129.4.3 IO 命令点灯测试

默认GPIO0_B7 是GPIO 模式,然后输入以下命令将方向寄存器设置为输出。

io -w -4 0xFDD60008 0x80008044

接下来设置GPIO 是输出高电平还是低电平,首先查看数据寄存器的值,输入以下命令:

io -r -4 0xFDD60000

image-20240920135924068

给数据寄存器写入0x80008040 输出高电平,灯亮。

io -w -4 0xFDD60000 0x8000c040

给数据寄存器写入0x80008040 输出高电平,灯灭。

io -w -4 0xFDD60000 0x80004040

129.5 通过mem 设备控制GPIO

在上一节中,我们学习了如何通过输入输出命令(IO)来控制LED 灯的GPIO 寄存器,实现了简单的点灯效果。这种方法通过访问设备文件来直接读写寄存器,实现了对硬件的低级控制。

然而,在某些情况下,我们可能无法直接使用IO 命令来访问GPIO 寄存器,或者希望使用更高级的抽象来控制硬件。这时,可以使用/dev/mem 设备来操作物理内存,以实现对GPIO寄存器的访问。

通过打开/dev/mem 设备文件,并将其映射到用户空间的内存中,我们可以直接读写物理内存地址,从而实现对GPIO 寄存器的控制。这种方法相对于IO 命令更加灵活,可以使用更高级的编程语言(如C/C++)来编写控制逻辑。

在本小节中,我们将继续使用C 语言,并通过/dev/mem 设备来控制GPIO 寄存器,实现LED 灯的点灯效果。通过使用/dev/mem 设备进行GPIO 控制,我们可以更加灵活地操作硬件,并且能够使用更高级的编程语言和工具来进行开发和调试。接下来,让我们开始学习如何使用/dev/mem 设备来控制GPIO,进一步扩展我们的硬件控制能力。

129.5.1 Linux 系统用户态访问内核态方式

在Linux 系统中,用户态可以通过多种方式访问内核态,包括:

  1. 通过read/write/ioctl:使用这种方式,用户态程序可以通过读写文件描述符或使用ioctl 系统调用与内核进行通信。例如,可以通过读写特定文件描述符来控制设备或获取设备状态。
  2. 通过sysfs 虚拟文件系统:sysfs 是一种以文件的形式表示设备和内核信息的虚拟文件系统。通过在sysfs 中的特定路径下读写文件,用户态程序可以与内核进行交互,例如控制GPIO 引脚或获取系统信息。
  3. 通过内存映射:内存映射是将用户空间的一段内存区域映射到内核空间的一种机制。通过内存映射,用户态程序可以直接修改内存区域的内容,从而与内核进行通信。这种方式可以实现高效的数据传输和共享。
  4. 通过Netlink:Netlink 是Linux 内核提供的一种通信机制,用于用户态程序与内核之间的双向通信。通过创建Netlink 套接字,用户态程序可以与内核进行交互,发送请求、接收事件通知等。这种方式适用于需要与内核进行复杂交互的场景,例如配置系统参数或发送命令。

这些方法提供了不同的方式和接口,用户态程序可以根据具体需求选择适合的方法与内核进行通信。

129.5.2 /dev/mem 设备

/dev/mem 是Linux 系统中的一个虚拟设备,通常与mmap 结合使用,可以将设备的物理内存映射到用户态,以实现用户空间对内核态的直接访问。无论是标准Linux 系统还是嵌入式Linux 系统,都支持使用/dev/mem 设备。

然而,直接访问内核空间是一项潜在危险的操作,因此只有root 用户才能访问/dev/mem设备。此外有些系统可能需要单独启动/dev/mem 设备的功能。配置启动/dev/mem 设备方法如下所示:

在Linux 源码内核中配置以下选项。

Device Drivers --->
    Character devices--->
        [*] /dev/mem virtual device support

image-20240920140331407

在上一小节中,我们讲解了IO 命令,IO 命令实际上就是基于/dev/mem 设备实现的。如果Linux 内核源码没有配置支持/dev/mem,IO 命令是不能使用的。

129.5.3 /dev/mem 设备的使用方法。

使用/dev/mem 设备需要具有root 权限,并且谨慎操作,因为直接访问内核空间是一项潜在的危险操作。以下是使用/dev/mem 设备的基本步骤:

步骤一:

使用open 函数打开”/dev/mem”文件描述符,并指定访问权限和阻塞方式。访问权限可以是只读(O_RDONLY)、只写(O_WRONLY)或读写(O_RDWR)阻塞方式或非阻塞(O_NDELAY)。

int fd = 0;
fd = open("/dev/mem", O_RDWR | O_NDELAY); /* 读写权限,非阻塞*/

请注意,这里使用O_RDWR 表示读写权限,并使用O_NDELAY 表示非阻塞方式。你可以根据实际需求选择适当的访问权限和阻塞方式。

步骤二:

使用mmap 函数将需要访问的物理地址与”/dev/mem”文件描述符建立映射。mmap 函数将返回一个指向映射内存区域的指针。

char *mmap_addr = NULL;
mmap_addr = (char *)mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd,MMAP_ADDR);

在这里,使用mmap 函数将物理内存地址映射到mmap_addr 指针所指向的内存区域。MMAP_SIZE 表示映射的大小,PROT_READ | PROT_WRITE 表示访问权限为读写,MAP_SHARED表示共享映射,fd 是之前打开的/dev/mem 文件描述符,MMAP_ADDR 是要映射的物理地址。

步骤三:

对映射的地址进行访问,即对寄存器进行读写操作。

int a = 0;
*(int *)mmap_addr = 0xff; // 写地址
a = *(int *)mmap_addr; // 读地址

在这里,使用指针操作对mmap_addr 指向的地址进行读写操作。*(int *)mmap_addr 表示将mmap_addr 解释为int 类型的指针,对于写操作,将0xff 写入该地址;对于读操作,将地址的值读取到变量a 中。

通过上述三个步骤,我们可以使用/dev/mem 设备。接下来我们来了解下mmap 函数。

129.5.4 mmap 函数

mmap 函数解释如下所示:

函数原型:
    void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

函数参数:
    start: 指定文件应被映射到进程空间的起始地址,一般被指定为一个空指针,选择起始地址的任务留给内核来完成。映射成功之后,函数返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
    length: 是映射到调用进程地址空间的字节数。
    prot: 参数指定共享内存的访问权限。可取如下几个值的或。PROT_READ(映射区域可读)PROT_EXEC(映射区域可执行)PROT_WRITE(映射区域可写)PROT_NONE(映射区域不可访问)。
    flags: 由以下几个常值指定,MAP_SHARED,MAP_PRIVATE,MAP_FIXED,其中MAP_SHARED,MAP_PRIVATE 必选其一,MAP_FIXED 不推荐使用。
    fd: 有效的文件描述符。一般是由open()函数返回。
    offset: 文件映射的偏移量,offset 的大小必须是页的整数倍,如果设备为0 代表从文件最前方开始映射。
        
函数返回值:
    成功执行时,mmap()返回被映射区的指针,失败时,mmap()返回-1.

129.5.5 LED 灯实验

本小节代码在配套资料“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\85_gpioctrl04”目录下。

实验要求:
通过编写mem 设备控制GPIO(LED 灯)的应用程序实现LED 灯闪烁的效果。

实验硬件连接:
使用迅为iTOP-RK3568 开发板上的用户灯LED9。

实验步骤:
首先进入ubuntu 的终端界面输入以下命令来创建gpioctrl.c 文件,如下图所示:

image-20240920140928783

然后向该文件中添加以下内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

#define GPIO_REG_BASE 0xFDD60000
#define GPIO_SWPORT_DDR_L_OFFSET 0x0008
#define GPIO_SWPORT_DR_L_OFFSET 0x0000
#define SIZE_MAP 0x1000

// 打开LED灯
void LED_ON(unsigned char *base)
{
    // 设置LED灯的方向为输出
    *(volatile unsigned int *)(base + GPIO_SWPORT_DDR_L_OFFSET) = 0x80008044;
    // 将LED灯打开
    *(volatile unsigned int *)(base + GPIO_SWPORT_DR_L_OFFSET) = 0x80008040;
}

// 关闭LED灯
void LED_OFF(unsigned char *base)
{
    // 设置LED灯的方向为输出
    *(volatile unsigned int *)(base + GPIO_SWPORT_DDR_L_OFFSET) = 0x80008044;
    // 将LED灯关闭
    *(volatile unsigned int *)(base + GPIO_SWPORT_DR_L_OFFSET) = 0x80000040;
}

int main(int argc, char *argv[])
{
    int fd;
    unsigned char *map_base;

    // 打开/dev/mem设备
    fd = open("/dev/mem", O_RDWR);
    if (fd < 0)
    {
        printf("open /dev/mem error \n");
        return -1;
    }

    // 将物理地址映射到用户空间
    map_base = (unsigned char *)mmap(NULL, SIZE_MAP, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO_REG_BASE);
    if (map_base == MAP_FAILED)
    {
        printf("map_base error \n");
        return -2;
    }

    while (1)
    {
        // 打开LED灯
        LED_ON(map_base);
        // 等待1秒
        usleep(1000000);
        // 关闭LED灯
        LED_OFF(map_base);
        // 等待1秒
        usleep(1000000);
    }

    // 解除映射
    munmap(map_base, SIZE_MAP);

    // 关闭文件描述符
    close(fd);

    return 0; // 返回0表示程序正常退出
}

保存退出之后,使用以下命令设置交叉编译器环境,并对gpioctrl.c 进行交叉编译,编译完
成如下图所示:

export PATH=/usr/local/arm64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH
aarch64-linux-gnu-gcc gpioctrl.c -o gpioctrl

image-20240920141725317

最后将交叉编译生成的gpioctrl 文件拷贝到开发板目录下运行即可。

chmod 777 gpioctrl
./gpioctrl 15

image-20240920141744420

程序运行之后,开发板上的用户灯LED 实现了闪烁的效果。

第130 章GPIO 的调试方法

GPIO 的调试方法除了使用IO 命令去查看寄存器,还可以使用其他方法进行GPIO 的调试。

130.1 方法一

debugfs 是Linux 内核提供的一个调试文件系统,可以用于查看和调试内核中的各种信息,包括GPIO 的使用情况。通过挂载debugfs 文件系统,并查看/sys/kernel/debug/目录下的相关文件,可以获取GPIO 的状态,配置和其他调试信息。如下图所示,我们进入/sys/kernel/debug/目录下。

image-20240920142027707

如果上图目录/sys/kernel/debug 目录下没有文件,需要在Linux 内核源码配置debugfs,如下图所示:

image-20240920142211144

配置好之后,重新编译内核源码,烧写内核镜像。
如果没有debugfs,可以使用以下命令进行挂载:

mount -t debugfs none /sys/kernel/debug/

如果有debugfs,可以使用以下命令查看GPIO 的信息。

cat /sys/kernel/debug/gpio

image-20240920142234093

130.2 方法二

当你进入/sys/kernel/debug/pinctrl 目录时,你可以获取有关GPIO 控制器的调试信息。在该目录下,通常会有以下文件和目录:

  1. /sys/kernel/debug/pinctrl/*/pinmux-pins:这些文件列出了每个GPIO 引脚的引脚复用配置。
    你可以查看每个引脚的功能模式、引脚复用选择以及其他相关的配置信息。我们进入到/sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/下面,输入“cat pinmux-pins”,如下图所示:

image-20240920142327934

  1. /sys/kernel/debug/pinctrl/*/pins:这些文件列出了GPIO 的引脚编号,可以查看GPIO 编号。
    我们进入到/sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/下面,输入“cat pins”,如下图所示:

image-20240920142357551

  1. /sys/kernel/debug/pinctrl/*/gpio-ranges:这些文件列出了每个GPIO 控制器支持的GPIO 范围。
    你可以查看GPIO 编号的范围和对应的控制器名称。我们进入到/sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/下面,输入“cat gpio-ranges”,如下图所示:

image-20240920142430771

  1. /sys/kernel/debug/pinctrl/*/pinmux-functions:这些文件列出了每个功能模式的名称以及与之关联的GPIO 引脚。你可以查看各个功能模式的名称和对应的引脚列表。我们进入到/sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/下面,输入“cat pinmux-functions”,如下图所示:

image-20240920142502459

  1. /sys/kernel/debug/pinctrl/*/pingroups:该路径提供有关用于配置和控制系统上的GPIO 引脚的引脚组的信息。我们进入到/sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/下面,输入“cat pingroups”,如下图所示:

image-20240920142550936

  1. /sys/kernel/debug/pinctrl/*/pinconf-pins:这些文件包含了GPIO 引脚的配置信息,如输入/输出模式、上拉/下拉设置等。你可以查看和修改GPIO 的电气属性,以便进行GPIO 的调试和配置。我们进入到/sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/下面,输入“cat pinconf-pins”,如下图所示:

image-20240920142625338

在这些文件和目录中,你可以浏览GPIO 控制器和引脚的相关信息,包括功能模式、复用配置、范围和配置参数等。这些信息可以帮助你了解GPIO 的当前状态和配置,并进行相应的调试和修改。

第131 章GPIO 子系统API 函数的引入

事实上,在前面中断课程中,已经简单接触到了GPIO 子系统中的API 函数,其中用来获取gpio 中断编号的gpio_to_irq 函数就属于GPIO 子系统中的API 函数,在本章节中将对GPIO子系统进行简单的介绍。

在目前的Linux 内核主线中,GPIO(通用输入/输出)子系统存在两个版本,这里将两个版本区分为新版本和旧版本。新版本GPIO 子系统接口是基于描述符(descriptor-based)来实现的,而旧版本的GPIO 子系统接口是基于整数(integer-based)来实现的,在Linux 内核中为了保持向下的兼容性,旧版本的接口在最新的内核版本中仍然得到支持,而随着时间的推移,新版本的GPIO 子系统接口会越来越完善,最终完全取代旧版本,所以在本课程中主要讲解新版本的GPIO 子系统接口。

新的GPIO 子系统接口需要与与设备树(Device Tree)结合使用。使用设备树和新的GPIO接口可以更加灵活地配置和管理系统中的GPIO 资源,提供了更好的可扩展性和可移植性。所以如果没有设备树,就无法使用新的GPIO 接口。

那要如何对新旧GPIO 子系统接口进行区分呢,一个明显的区别是新的GPIO 子系统接口使用了以”gpiod_“作为前缀的函数命名约定,而旧的GPIO 子系统接口使用了以”gpio_“作为前缀的函数命名约定。

一些区分新旧GPIO 子系统接口的示例如下所示:

新的GPIO 子系统接口示例如下所示:

gpiod_set_value()
gpiod_direction_input()
gpiod_direction_output()
gpiod_get_value()
gpiod_request()

旧的GPIO 子系统接口示例:

gpio_set_value()
gpio_direction_input()
gpio_direction_output()
gpio_get_value()
gpio_request()

上面也提到了新的GPIO 子系统接口是基于描述符(descriptor-based)来实现的,由struct gpio_desc 结构体来表示,该结构体定义在内核源码的“drivers/gpio/gpiolib.h”目录下。具体内容如下所示:

struct gpio_desc {
    struct gpio_device gdev; // GPIO 设备结构体
    unsigned long flags; // 标志位,用于表示不同的属性
    
    /* 标志位符号对应的位号*/
    #define FLAG_REQUESTED 0 // GPIO 已请求
    #define FLAG_IS_OUT 1 // GPIO 用作输出
    #define FLAG_EXPORT 2 // 受sysfs_lock 保护的导出标志
    #define FLAG_SYSFS 3 // 通过/sys/class/gpio/control 导出的标志
    #define FLAG_ACTIVE_LOW 6 // GPIO 值为低电平时激活
    #define FLAG_OPEN_DRAIN 7 // GPIO 为开漏类型
    #define FLAG_OPEN_SOURCE 8 // GPIO 为开源类型
    #define FLAG_USED_AS_IRQ 9 // GPIO 连接到中断请求(IRQ)
    #define FLAG_IS_HOGGED 11 // GPIO 被独占占用
    #define FLAG_TRANSITORY 12 // GPIO 在休眠或复位时可能失去值
    
    /* 连接标签*/
    const char *label; // GPIO 的名称
    const char *name; // GPIO 的名称
};

(1)struct gpio_device gdev 是一个struct gpio_device 类型的字段,用于表示GPIO 设备的相关信息。struct gpio_device 结构体通常包含设备的底层硬件控制器等信息。
(2)unsigned long flags:用于表示GPIO 的标志位,以表示不同的属性。通过使用位操作,每个标志位可以单独设置或清除。
(3)第5-14 行用于表示不同标志位的符号常量,与flags 字段中的位号相对应。例如,在flags 字段中的第0 位表示FLAG_REQUESTED,第1 位表示FLAG_IS_OUT,以此类推
(4)const char *label: 这是一个指向字符串的指针,表示GPIO 的标签或名称。
(5)const char *name: 这是一个指向字符串的指针,表示GPIO 的名称。

上面需要重点关注的是然后struct gpio_device 结构体,该结构体同样定义在内核源码的“drivers/gpio/gpiolib.h”目录下,具体内容如下所示:

struct gpio_device {
    int id; // GPIO 设备ID
    struct device *dev; // 对应的设备结构体指针
    struct cdev chrdev; // 字符设备结构体
    struct device *mockdev; // 模拟设备结构体指针
    struct module *owner; // 拥有该GPIO 设备的内核模块指针
    struct gpio_chip *chip; // 对应的GPIO 芯片结构体指针
    struct gpio_desc *descs; // GPIO 描述符数组指针
    int base; // GPIO 编号的起始值
    u16 ngpio; // GPIO 的数量
    const char *label; // GPIO 设备的标签
    void *data; // 与GPIO 设备相关的数据指针
    struct list_head list; // 用于将GPIO 设备结构体连接到链表中
    
#ifdef CONFIG_PINCTRL
    /*
    * 如果启用了CONFIG_PINCTRL 选项,GPIO 控制器可以选择描述它们在SoC 中服务的实际引脚范围。
    * 此信息将由pinctrl 子系统用于配置相应的GPIO 引脚。
    */
    struct list_head pin_ranges; // 描述GPIO 控制器引脚范围的链表
#endif
};

该结构体是用来描述GPIO 设备的数据结构,关于该结构体的参数介绍如下所示:
(1)int id:整型字段,表示GPIO 设备的ID。每个GPIO 设备可以有一个唯一的ID。
(2)struct device *dev:指向struct device 的指针,表示与GPIO 设备相关联的设备结构体。
(3)struct cdev chrdev: 字符设备结构体,用于实现GPIO 设备的字符设备接口。
(4)struct device *mockdev: 指向struct device 的指针,用于表示GPIO 设备的模拟设备结构体。
(5)struct module *owner: 指向拥有该GPIO 设备的内核模块的指针。
(6)struct gpio_chip *chip: 指向struct gpio_chip 的指针,表示与GPIO 设备关联的GPIO 芯片(GPIO 控制器)结构体。
(7)struct gpio_desc *descs: 指向struct gpio_desc 数组的指针,表示与GPIO 设备关联的GPIO 描述符。每个GPIO 描述符用于描述GPIO 的属性和状态。
(8)int base: 表示GPIO 编号的起始值。
(9)u16 ngpio: 16 位无符号整型字段,表示GPIO 的数量。
(10)const char *label: 指向字符串的指针,表示GPIO 设备的标签或名称。
(11)void *data: 指向与GPIO 设备相关的数据的指针,用于存储和访问与GPIO 设备相关的自定义数据。
(12)struct list_head list: 将GPIO 设备结构体连接到链表中的字段,用于管理多个GPIO 设备的列表。

(13)struct list_head pin_ranges (仅在启用CONFIG_PINCTRL 选项时存在): 用于描述GPIO 控制器引脚范围的链表。如果配置了GPIO 控制器的引脚范围,该链表将包含描述每个范围的元素。

在上面一系列的参数中,要重点关注的是struct gpio_chip *chip 这一结构体,表示与GPIO 设备关联的GPIO 芯片(GPIO 控制器)结构体,该结构体定义在内核源码目录下的“include/linux/gpio/driver.h”文件中,具体内容如下所示:

struct gpio_chip {
    const char *label; // GPIO 芯片标签
    struct gpio_device gpiodev; // GPIO 设备
    struct device *parent; // 父设备指针
    struct module *owner; // 拥有者模块指针
    int (*request)(struct gpio_chip *chip, unsigned offset);// 请求GPIO
    void (*free)(struct gpio_chip *chip, unsigned offset); // 释放GPIO
    int (*get_direction)(struct gpio_chip *chip, unsigned offset); // 获取GPIO 方向
    int (*direction_input)(struct gpio_chip *chip, unsigned offset); // 设置GPIO 为输入
    int (*direction_output)(struct gpio_chip *chip, unsigned offset, int value); // 设置GPIO 为输出
    int (*get)(struct gpio_chip chip, unsigned offset); // 获取GPIO 值
    int (*get_multiple)(struct gpio_chip chip, unsigned long *mask, unsigned long *bits); // 获取多个GPIO 的    值
    void (*set)(struct gpio_chip chip, unsigned offset, int value); // 设置GPIO 值
    void (*set_multiple)(struct gpio_chip chip, unsigned long mask, unsigned long *bits); // 设置多个GPIO    的值
    int (*set_config)(struct gpio_chip *chip, unsigned offset, unsigned long config); // 设置GPIO 配置
    int (*to_irq)(struct gpio_chip chip, unsigned offset); // 将GPIO 转换为中断
    void (*dbg_show)(struct seq_file *s, struct gpio_chip chip); // 在调试信息中显示GPIO
    int base; // GPIO 编号的基准值
    u16 ngpio; // GPIO 的数量
    const char *const *names; // GPIO 的名称数组
    ..........
};

struct gpio_chip *chip 这一结构体用于描述GPIO 芯片的属性和操作函数,可以通过函数指针调用相应的函数来请求、释放、设置、获取GPIO 的状态和数值等操作,从而实现对GPIO 的控制和管理,需要注意的是这个结构体中的一系列函数都不需要我们来填充,这些工作都是由芯片原厂工程师来完成的,我们只需要学会新gpio 子系统相应API 函数的使用即可。

在接下来的章节中将对常用的新gpio 子系统API 函数进行讲解。

第132 章获取单个gpio 描述实验

本章节将对新gpio 子系统中获取单个gpio 描述的api 接口进行讲解。

132.1 函数介绍

(1)获取GPIO 描述符gpiod_get

struct gpio_desc *gpiod_get 是Linux 内核中用于获取GPIO 描述符的函数。下面是对该函数的详细介绍:

函数原型:
    struct gpio_desc *__must_check gpiod_get(struct device *dev,const char *con_id,enum gpiod_flags flags);
头文件:
    #include <linux/gpio/consumer.h>
参数:
    dev:指向设备结构体的指针,表示与GPIO 相关联的设备。
    con_id:连接标识符(connection identifier),用于标识所需的GPIO 连接。通常由设备树(Device Tree)或其他设备描述信息定义
    flags:GPIO 描述符的选项标志,用于指定GPIO 的属性和操作模式。以下是一些常用的选项标志(enum gpiod_flags):
        GPIOD_INPUT:将GPIO 配置为输入模式。
        GPIOD_OUTPUT:将GPIO 配置为输出模式。
        GPIOD_ACTIVE_LOW:指示GPIO 的默认电平为低电平(激活低电平)。
        GPIOD_OPEN_DRAIN:将GPIO 配置为开漏输出模式。
        GPIOD_OPEN_SOURCE:将GPIO 配置为开源输出模式。
函数功能:
    获取与给定设备和连接标识符(con_id)相关联的GPIO 描述符。
返回值:
    如果成功获取到GPIO 描述符,则返回指向struct gpio_desc 的指针;如果获取失败,则返回NULL

在Linux 内核中还有另外三个同样是获取GPIO 描述符资源的函数,三个函数内容如下所示:

struct gpio_desc *gpiod_get_index(struct device *dev, const char *con_id, unsigned int idx, enum gpiod_flags flags);
struct gpio_desc *gpiod_get_optional(struct device *dev, const char *con_id, enum gpiod_flags flags);
struct gpio_desc *gpiod_get_index_optional(struct device *dev, const char *con_id, unsigned int index, enum gpiod_flags flags);

相较于上面介绍的gpiod_get 函数,下面的三个函数可能会多一个index 参数和optional的函数后缀,其中index 表示GPIO 的索引值,当设备树的GPIO 属性值包含多个GPIO 引脚描述时, 使用index 来表示每个GPIO 引脚的唯一标识。而带optional() 后缀的函数与不带optional 后缀的函数在功能上是相同的,都用于获取GPIO 描述符,两者的区别在于返回值的不同:

使用带optional() 的函数时,如果获取失败,返回值为NULL。
使用不带optional 的函数时,如果获取失败,返回值是一个特殊的结构表示获取GPIO 描述符失败。

(2)释放GPIO 描述符gpiod_put

gpiod_put() 函数是Linux 内核中用于释放GPIO 描述符资源的函数。下面是对该函数的详细介绍:

//函数原型:
    void gpiod_put(struct gpio_desc *desc);
//头文件:
    #include <linux/gpio/consumer.h>
//参数:
    desc:指向要释放的GPIO 描述符的指针。
//功能:
    gpiod_put() 函数用于释放之前通过gpiod_get() 或类似函数获取的GPIO 描述符。
//返回值:
    无返回值。

132.2 设备树的修改

本小节修改好的设备树以及编译好的boot.img 镜像存放路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\86_gpioctrl05\01_内核镜像

在131 章也提到了新版本的gpio 子系统api 接口要与设备树结合才能使用,所以需要在设备树中将用于获取GPIO 描述符的引脚复用为GPIO 模式。为了让教程更贴近于实战,这里选择RK3568 开发板背面20Pin GPIO 座子的1 号引脚,右边对应的丝印为I2C3_SDA_M0,这里的丝印表示该引脚可以复用为I2C3 的SDA 功能,而在当前的设备树源码中这个引脚是没有任何复用的,该引脚的具体位置如下所示:

image-20240923112900196

在前面pinctrl 的章节中已经学习了如何将一个管脚复用为GPIO,首先我们需要确定该引脚的GPIO 编号,来到RK3568 开发板的底板原理图,找到J39 GPIO 底座,如下图所示:

image-20240923112924218

可以看到1 号管脚的网络标号为I2C3_SDA_M0,然后打开核心板原理图,根据这个网络标号进行搜索,查找到的核心板内容如下所示:

image-20240923112939024

左侧为该引脚的一些复用功能,箭头指向的部分为接下来要用到的GPIO 引脚编号GPIO1_A0,然后对设备树进行内容的添加,从而将该引脚复用为GPIO 的功能。

首先根据上图中的复用功能查看设备树中是否已经对该引脚进行了复用,在确保该引脚无任何复用之后,对rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的添加,在根节点的结尾添加以下内容:

my_gpio:gpiol_a0 {
    compatible = "mygpio";
    my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "default";
    pinctrl-0 = <&mygpio_ctrl>;
};

compatible: 用于指定设备的兼容性字符串,与驱动程序中的值相匹配。
my-gpios: 指定了与该设备相关联的GPIO。&gpiol 表示GPIO 控制器的句柄(handle),RK_PA0 是与该GPIO 相关的资源描述符(resource specifier),GPIO_ACTIVE_HIGH 表示GPIO 的默认电平为高电平。
pinctrl-names 和pinctrl-0: 用于指定引脚控制器(pinctrl)的配置。pinctrl-names 表示引脚控制器配置的名称,这里为”default”。pinctrl-0 指定了与该配置相关联的引脚控制器句柄,这里为&mygpio_ctrl。
添加完成如下图所示:

image-20240923114109716

然后找到pinctrl 节点,在节点尾部添加以下内容,

mygpio {
    mygpio_ctrl: my-gpio-ctrl {
        rockchip,pins = <1 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>;
    };
};

在第三行的内容中,1 表示引脚索引,RK_PA0 表示资源描述符,用于标识与该引脚相关联的物理资源,表示引脚所属的功能组,RK _FUNC_GPI0 表示将引脚的功能设置为GPIO,&pcfg_pull_none 表示引脚配置为无上下拉。

添加完成如下图所示:

image-20240923114514878

至此,关于设备树相关的修改就完成了,保存退出之后,编译内核,然后将生成的boot.img镜像烧写到开发板上即可。

132.3 驱动程序的编写
本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\86_gpioctrl05\02_module
编写完成的gpio_api.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/gpio/consumer.h>

struct gpio_desc *mygpio1;  // GPIO 描述符指针
struct gpio_desc *mygpio2;  // GPIO 描述符指针
int num;  // GPIO 编号

//平台设备初始化函数
// 平台设备初始化函数
static int my_platform_probe(struct platform_device *dev)
{
    printk("This is my_platform_probe\n");

    // 获取可选的GPIO描述符
    mygpio1 = gpiod_get_optional(&dev->dev, "my", 0);
    if (mygpio1 == NULL) {
        printk("gpiod_get_optional error\n");
        return -1;
    }
    num = desc_to_gpio(mygpio1);
    printk("num is %d\n", num);

    // 释放GPIO描述符
    gpiod_put(mygpio1);

    // 获取指定索引的GPIO描述符
    mygpio2 = gpiod_get_index(&dev->dev, "my", 0, 0);
    if (IS_ERR(mygpio2)) {
        printk("gpiod_get_index error\n");
        return -2;
    }
    num = desc_to_gpio(mygpio2);
    printk("num is %d\n", num);

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="mygpio"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
	gpiod_put(mygpio2);
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

132.4 运行测试

132.4.1 编译驱动程序

在上一小节中的gpio_api.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成gpio_api.ko 目标文件,至此驱动模块就编译成功了。

132.4.2 运行测试

首先需要确保当前开发板使用的内核镜像是我们在132.2 小节中修改设备树后编译生成的镜像,然后启动开发板,使用以下命令进行驱动的加载,如下图(图132-9)所示:

insmod gpio_api.ko

image-20240923115010015

可以看到两个跟gpio 的打印,打印出来获取到的gpio 引脚号为32,在前面的章节中也学习过了GPIO 引脚编号的计算公式,GPIO1_A0 对应32 号,然后使用以下命令进行驱动的卸载,
如下图所示:

rmmod gpio_api.ko

image-20240923115036695

至此,获取单个gpio 描述实验就完成了。

第133 章GPIO 操作函数实验

从本章节将对新gpio 子系统中操作GPIO 的相关api 接口函数进行讲解。

133.1 函数介绍

1 获取GPIO 的方向函数:

1)函数原型:
    int gpiod_get_direction(struct gpio_desc *desc);2)头文件:
    #include <linux/gpio/consumer.h>3)参数:
    desc:指向GPIO 描述符的指针。
(4)函数功能:
    gpiod_get_direction 函数用于获取GPIO 的方向,即判断GPIO 是输入还是输出。
(5)返回值:
    返回值为整型,表示GPIO 的方向。如果成功获取到GPIO 方向,返回值为 GPIO_LINE_DIRECTION_IN(0)表示输入,或GPIO_LINE_DIRECTION_OUT(1)表示输出。如果获取失败,返回值为负数,表示错误码。

该函数的作用是获取给定GPIO 描述符所代表的GPIO 的方向。通过该函数,可以确定GPIO是配置为输入还是输出。返回值可以用于进一步判断和处理GPIO 的方向相关逻辑。

2 配置GPIO 的方向函数:

1)函数原型:
    int gpiod_direction_input(struct gpio_desc *desc);
    int gpiod_direction_output(struct gpio_desc *desc, int value);2)头文件:
    #include <linux/gpio/consumer.h>3)参数:
    desc:指向GPIO 描述符的指针。
    value(仅适用于gpiod_direction_output):初始输出值,可以是01。
(4)函数功能:
    gpiod_direction_input 函数用于配置GPIO 的方向为输入。
    gpiod_direction_output 函数用于配置GPIO 的方向为输出,并可指定初始输出值。
(5)返回值:
    返回值为整型,表示配置GPIO 方向的结果。
    如果成功配置GPIO 方向,返回值为0。
    如果配置失败,返回值为负数,表示错误码。

这两个函数用于配置GPIO 的方向。gpiod_direction_input 将给定的GPIO 描述符所代表的GPIO 配置为输入模式。而gpiod_direction_output 将GPIO 配置为输出模式,并可以指定初始输出值。

3 读取GPIO 的电平状态函数:

1)函数原型:
    int gpiod_get_value(const struct gpio_desc *desc);2)头文件:
    #include <linux/gpio/consumer.h>3)参数:
    desc:指向GPIO 描述符的指针。
(4)函数功能:
    gpiod_get_value 函数用于读取GPIO 的电平状态。
(5)返回值:
    返回值为整型,表示GPIO 的电平状态。
    如果成功读取到GPIO 的电平状态,返回值为01,分别表示低电平和高电平。
    如果读取失败,返回值为负数,表示错误码。

该函数用于读取给定GPIO 描述符所代表的GPIO 的电平状态。通过调用该函数,可以获取GPIO 当前的电平状态,以便进一步处理和判断GPIO 的状态。

4 设置GPIO 的电平状态函数:

1)函数原型:
    void gpiod_set_value(struct gpio_desc *desc, int value);2)头文件:
    #include <linux/gpio/consumer.h>3)参数:
    desc:指向GPIO 描述符的指针。
    value:要设置的GPIO 的电平状态,可以是01。
(4)函数功能:
    gpiod_set_value 函数用于设置GPIO 的电平状态。
(5)返回值:无(void)
    该函数用于设置给定GPIO 描述符所代表的GPIO 的电平状态。通过调用该函数,您可以将GPIO设置为特定的电平状态,以便控制外部设备或执行其他相关操作。

value 参数表示要设置的GPIO 的电平状态,可以是0 或1。当value 为0 时,表示设置GPIO 为低电平;当value 为1 时,表示设置GPIO 为高电平。
该函数没有返回值,因为它只是执行设置操作而不需要返回任何结果。

在使用该函数之前,需要确保GPIO 已经被正确地配置为输出模式。

5 将GPIO 描述符转换为中断编号函数:

1)函数原型:
    int gpiod_to_irq(const struct gpio_desc *desc);2)头文件:
    #include <linux/gpio/consumer.h>3)参数:
    desc:指向GPIO 描述符的指针。
(4)函数功能:
    gpiod_to_irq 函数用于将GPIO 描述符转换为中断号。
(5)返回值:
    返回值为整型,表示中断号。
    如果成功将GPIO 描述符转换为中断号,返回值为大于等于0 的中断号。
    如果转换失败,返回值为负数,表示错误码。

该函数用于将给定GPIO 描述符所代表的GPIO 转换为对应的中断号。

133.2 驱动程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\87_gpioctrl06
编写完成的gpio_api.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/gpio/consumer.h>

struct gpio_desc *mygpio1;  // GPIO 描述符指针
struct gpio_desc *mygpio2;  // GPIO 描述符指针
int num;  // GPIO 编号

//平台设备初始化函数
static int my_platform_probe(struct platform_device *dev)
{
    printk("This is mydriver_probe\n");

    mygpio1 = gpiod_get_optional(&dev->dev, "my", 0);
    if (mygpio1 == NULL) {
        printk("gpiod_get_optional error\n");
        return -1;
    }
    num = desc_to_gpio(mygpio1);
    printk("num is %d\n", num);

    gpiod_put(mygpio1);

    mygpio2 = gpiod_get_index(&dev->dev, "my", 0, 0);
    if (IS_ERR(mygpio2)) {
        printk("gpiod_get_index error\n");
        return -2;
    }
    num = desc_to_gpio(mygpio2);
    printk("num is %d\n", num);

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="mygpio"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
	gpiod_put(mygpio2);
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

133.3 运行测试

133.3.1 编译驱动程序

对于Makefile 的内容注释已在上图添加,保存退出之后,来到存放gpio_api.c 和Makefile文件目录下,如下图(图133-1)所示:然后使用命令“make”进行驱动的编译,编译完生成gpio_api.ko 目标文件,至此驱动模块就编译成功了。

133.3.2 运行测试

首先需要确保当前开发板使用的内核镜像是我们在132.2 小节中修改设备树后编译生成的镜像,然后启动开发板,使用以下命令进行驱动的加载,如下图(图133-4)所示:

insmod gpio_api.ko

image-20240923140343926

在驱动程序中首先会将GPIO 的方向设置为输出,并且设置为了高电平,所以上面的第一个打印IO 口方向为输出,而由于已经设置为了高电平,所以第二个打印1 表示引脚为高电平,第三个打印的值为113,表示gpio 转换的中断号,然后使用以下命令进行驱动的卸载,如下图所示:

rmmod gpio_api.ko

image-20240923140404426

至此,GPIO 操作函数实验就完成了。

第134 章三级节点操作函数实验

在上一个章节中讲解了新版本GPIO 子系统中的GPIO 操作实验,而在进行操作之前首先要获取相应的gpio 描述,在前面的示例中获取的都是二级节点的GPIO 描述,那如果我们要如何获取下面led1 和led2 两个三级节点的gpio 描述呢?

my_gpio:gpio1_a0 {
    compatible = "mygpio";
    led1{
        my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>, <&gpio1 RK_PB1 GPIO_ACTIVE_HIGH>;
        pinctrl-names = "default";
        pinctrl-0 = <&mygpio_ctrl>;
    };
    led2{
        my-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
    };
};

如果仍旧使用gpiod_get 来获取gpio 描述会发现是获取不成功呢,获取三级节点的GPIO描述要使用什么函数呢,带着疑问,让我们进入本章节的学习吧。

134.1 函数介绍

1 计算子节点数量

函数原型
    unsigned int device_get_child_node_count(struct device *dev);
头文件
    <linux/device.h>。
参数:
    struct device 类型的指针dev,表示要计算子节点数量的设备节点;。
函数功能:
    用于计算给定设备节点dev 的子节点数量。
返回值:
    如果成功获取子节点数量,返回一个大于0 的无符号整数,表示设备节点的子节点数量。如果获取失败,返回值为0

该函数的功能是通过给定设备节点dev 来计算其子节点的数量。它可以用于在设备驱动程序中了解设备节点的层级结构,以及设备节点下子节点的数量。

2 获取指定节点的GPIO 结构描述

1)函数原型:
    struct gpio_desc *fwnode_get_named_gpiod(struct fwnode_handle *fwnode, const char *propname, 
                                             int index, enum gpiod_flags dflags, const char *label);2)头文件:
    <linux/gpio/consumer.h>。
(3)参数:
    fwnode:指向struct fwnode_handle 的指针,表示要获取GPIO 的节点对象地址。
    propname:属性名,指定要获取的GPIO 的属性名称。
    index:索引值,用于指定要获取的GPIO 在属性中的索引,用于GPIO 属性值包含多个GPIO 引脚描述时。
    dflags:获得到GPIO 后的初始化配置,可以使用以下枚举值:
        GPIOD_ASIS:不进行初始化。
        GPIOD_IN:初始化为输入模式。
        GPIOD_OUT_LOW:初始化为输出模式,输出低电平。
        GPIOD_OUT_HIGH:初始化为输出模式,输出高电平。
        label:标签,用于标识GPIO 的描述。
(4)函数功能:
    该函数通过指定节点的对象地址获取子节点中的GPIO 结构描述。
(5)返回值:
    返回一个指向struct gpio_desc 的指针,表示获取到的GPIO 结构描述。如果获取失败,返回值为NULL

该函数的功能是通过给定的节点对象地址fwnode,获取指定属性名propname 中的GPIO结构描述。可以通过index 参数指定在属性中的索引。获取到的GPIO 结构描述可以用于后续的GPIO 操作。函数还可以根据dflags 参数指定GPIO 的初始化配置,例如设置为输入或输出模式,并指定输出的默认电平。label 参数用于提供GPIO 的描述标签。函数返回获取到的GPIO结构描述指针,如果获取失败,则返回NULL。

3 获取下一个子节点对象地址

函数原型:
    struct fwnode_handle *device_get_next_child_node(struct device *dev, struct fwnode_handle *child);
头文件:
    <linux/device.h>。
参数:
    dev:指向struct device 的指针,表示父设备节点。
    child:指向struct fwnode_handle 的指针,表示当前子设备节点。
函数功能:
    用于获取给定父设备节点dev 的下一个子设备节点。
返回值:
    返回一个指向struct fwnode_handle 的指针,表示下一个子设备节点。
    如果没有下一个子设备节点,返回值为NULL

该函数的功能是在给定的父设备节点dev 下获取当前子设备节点child 的下一个子设备节点。通过调用这个函数,可以遍历父设备节点的所有子设备节点。函数返回下一个子设备节点的struct fwnode_handle 指针,如果没有下一个子设备节点,则返回NULL。这个函数在设备驱动程序开发中常用于遍历设备树中的设备节点。

134.2 设备树的修改

本小节修改好的设备树以及编译好的boot.img 镜像存放路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\88_gpioctrl07\01_内核镜像
由于本章节要获取到三级节点的GPIO 描述,所以要对rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的修改,将根节点中的gpiol_a0 修改为以下内容:

my_gpio:gpio1_a0 {
    compatible = "mygpio";
    led1{
        my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>, <&gpio1 RK_PB1 GPIO_ACTIVE_HIGH>;
        pinctrl-names = "default";
        pinctrl-0 = <&mygpio_ctrl>;
    };
    led2{
        my-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
    };
};

添加完成如下图所示:

image-20240923141449523

至此,关于设备树相关的修改就完成了,保存退出之后,编译内核,然后将生成的boot.img镜像烧写到开发板上即可。

134.3 驱动程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\88_gpioctrl07\02_module
编写完成的gpio_api.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>

unsigned int count;
struct fwnode_handle *child_fw = NULL;
struct gpio_desc *led[2];
int i = 0;
int num = 0;

// 平台设备初始化函数
static int my_platform_probe(struct platform_device *dev) 
{
    printk("This is my_platform_probe\n");
    
    // 获取父设备节点的子设备节点数量
    count = device_get_child_node_count(&dev->dev);
    printk("count is %d\n", count);
    
    for (i = 0; i < count; i++) {
        // 获取下一个子设备节点
        child_fw = device_get_next_child_node(&dev->dev, child_fw);
        
        if (child_fw) {
            // 获取子设备节点中名为 "my-gpios" 的 GPIO 描述
            led[i] = fwnode_get_named_gpiod(child_fw, "my-gpios", 0, 0, "LED");
        }
        
        // 将 GPIO 描述转换为 GPIO 号
        num = desc_to_gpio(led[i]);
        printk("num is %d\n", num);
    }

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="mygpio"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

134.4 运行测试

134.4.1 编译驱动程序

在上一小节中的gpio_api.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成gpio_api.ko 目标文件,至此驱动模块就编译成功了。

134.4.2 运行测试

首先需要确保当前开发板使用的内核镜像是我们在134.2 小节中修改设备树后编译生成的镜像,然后启动开发板,使用以下命令进行驱动的加载,如下图(图134-5)所示:

insmod gpio_api.ko

image-20240923142051160

首先打印出了子节点的数量为2,也就是led1 和led2,接下来的两个num 值分别为32 和40,分别对应两个节点的第一个GPIO 属性的引脚编号,前面也学习过了换算相关的知识,gpio1RK_PA0 和gpio1 RK_PB0 分贝对应32 和40,匹配正确,然后使用以下命令进行驱动的卸载,
如下图所示:

rmmod gpio_api.ko

image-20240923142112814

至此,三级节点操作函数实验就完成了。

第135 章GPIO 子系统与pinctrl 子系统相结合实验

在上一章中我们讲解了三级节点的操作函数,关于新版本的GPIO 子系统接口api 函数的讲解就完成了,而为了加深大家的认知,在本章节将进行GPIO 子系统与pinctrl 子系统相结合的实验,实验的设备树示例如下所示:

my_gpio:gpio1_a0 {
    compatible = "mygpio";
    my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "myled1";
    pinctrl-0 = <&mygpio_ctrl>;
};

在第四行中的pinctrl-names 参数并不是default,这就需要用到我们前面pinctrl 子系统中的知识来查找并设置相应的pinctrl 状态了,所以再第一节中我们将会重新学习一下pinctrl 的一些相关函数。

135.1 函数介绍

(1)获取设备对应的pinctrl 结构体指针函数

函数原型:
    struct pinctrl* pinctrl_get(struct device *dev);
头文件:
    <linux/pinctrl/pinctrl.h>。
参数:
    函数接受一个指向struct device 的指针dev,表示设备对象。
函数功能:
    用于获取与给定设备对象dev 相关联的pinctrl(引脚控制器)实例。
返回值:
    返回一个指向struct pinctrl 的指针,表示获取到的pinctrl 实例。
    如果获取失败或者设备对象不支持pinctrl,则返回NULL

该函数的功能是根据给定的设备对象dev 获取与其相关联的pinctrl 实例。pinctrl 是Linux内核中用于管理和控制引脚的框架。通过调用该函数,可以获得设备对象所使用的pinctrl 实例,以便进行引脚配置和控制操作。

(2)释放pinctrl 指针函数

函数原型:
    void pinctrl_put(struct pinctrl *p);
头文件:
    <linux/pinctrl/pinctrl.h>。
参数:
    函数接受一个指向struct pinctrl 的指针p,表示要释放的pinctrl 实例。
函数功能:
    该函数用于释放由pinctrl_get() 函数获得的pinctrl 实例,以释放相关资源。
返回值:
    无返回值。

该函数的功能是释放由pinctrl_get() 函数获得的pinctrl 实例,以释放相关资源。在使用完pinctrl 实例后,调用该函数可以确保正确释放相关资源,避免内存泄漏。

(3)查找pinctrl 状态函数

函数原型:
    struct pinctrl_state *pinctrl_lookup_state(struct pinctrl *p, const char *name);
头文件:
    <linux/pinctrl/pinctrl.h>。
参数:
    struct pinctrl *p:指向pinctrl 实例的指针,表示要进行状态查找的pinctrl。
    const char *name:指向状态名称的字符串指针,表示要查找的状态名称。
函数功能:
    用于在给定的pinctrl 实例中查找指定名称的pinctrl 状态。
返回值:
    函数返回一个指向struct pinctrl_state 的指针,表示找到的pinctrl 状态。如果未找到或发生错误,则返回NULL

该函数的功能是在给定的pinctrl 实例p 中查找指定名称的pinctrl 状态。pinctrl 状态是与引脚相关的配置和控制状态,例如引脚模式、电气属性等。

(4)设置pinctrl 状态到硬件

函数原型:
    int pinctrl_select_state(struct pinctrl *p, struct pinctrl_state *s);
头文件:
    <linux/pinctrl/pinctrl.h>。
参数:
    struct pinctrl *p:指向pinctrl 实例的指针,表示要进行状态设置的pinctrl。
    struct pinctrl_state *s:指向pinctrl 状态的指针,表示要设置的目标状态。
函数功能:
    用于将指定的pinctrl 状态设置到硬件上。
返回值:
    返回一个整数值,表示操作的结果。如果设置成功,则返回0;否则返回负数错误码。

该函数的功能是将指定的pinctrl 状态s 设置到硬件上。pinctrl 状态是与引脚相关的配置和控制状态,例如引脚模式、电气属性等。

135.2 设备树的修改

本小节修改好的设备树以及编译好的boot.img 镜像存放路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\88_gpioctrl07\01_内核镜像

由于本章节要使用上pinctrl 子系统相关的接口来查找并设置相应的pinctrl 状态,所以要对rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的修改,将根节点中的gpiol_a0 修改为以下内容:

my_gpio:gpio1_a0 {
    compatible = "mygpio";
    my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "myled1";
    pinctrl-0 = <&mygpio_ctrl>;
};

添加完成如下图所示:

image-20240923143314182

至此,关于设备树相关的修改就完成了,保存退出之后,编译内核,然后将生成的boot.img镜像烧写到开发板上即可。

135.3 驱动程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\89_gpioctrl07\02_module
编写完成的gpio_api.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>

struct pinctrl *led_pinctrl; // pinctrl 实例指针
struct pinctrl_state *led_state;// pinctrl 状态指针
int ret;
//平台设备初始化函数
static int my_platform_probe(struct platform_device *dev)
{
    printk("This is mydriver_probe\n");
    
    led_pinctrl = pinctrl_get(&dev->dev);// 获取 pinctrl 实例
    if (IS_ERR(led_pinctrl)) {
        printk("pinctrl_get is error\n");
        return -1;
    }
    
    led_state = pinctrl_lookup_state(led_pinctrl, "myled1");// 查找状态
    if (IS_ERR(led_state)) {
        printk("pinctrl_lookup_state is error\n");
        return -2;
    }
    
    ret = pinctrl_select_state(led_pinctrl, led_state);// 设置状态到硬件
    if (ret < 0) {
        printk("pinctrl_select_state is error\n");
        return -3;
    }
    
    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="mygpio"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

135.4 运行测试

135.4.1 编译驱动程序

在上一小节中的gpio_api.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成gpio_api.ko 目标文件,至此驱动模块就编译成功了。

135.4.2 运行测试

首先需要确保当前开发板使用的内核镜像是我们在135.2 小节中修改设备树后编译生成的镜像,然后启动开发板,首先使用以下命令查看gpio1 RK_PA0 引脚的复用功能,如下图所示:

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep 32

image-20240923143529085

可以看到在没有加载驱动之前,gpio1 RK_PA0 引脚是没有进行复用的,然后使用以下命令进行驱动的加载,如下图(图135-6)所示:

insmod gpio_api.ko

image-20240923143547535

然后重新使用使用以下命令查看gpio1 RK_PA0 引脚的复用功能,如下图所示:

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep 32

image-20240923143616909

根据打印信息可以得到gpio1 RK_PA0 已经被设置为了GPIO 功能,功能和引脚组正是我们在pinctrl 节点中添加的信息,证明已经成功使用了添加的pinctrl-names 状态,然后使用以下命令进行驱动的卸载,如下图所示:

rmmod gpio_api.ko

image-20240923143640964

至此,GPIO 子系统与pinctrl 子系统相结合实验实验就完成了。

第136 章实战:实现动态切换引脚复用功能

再上一个小节中完成了GPIO 子系统与pinctrl 子系统相结合实验,在本章节中将更进一步,实现引脚动态切换引脚复用功能。

这里仍旧使用RK3568 底板背面的20 pin GPIO 底座的1 号管脚来完成本章节要进行的动态切换引脚复用的功能,该引脚的核心板原理图内容如下所示:

image-20240923143708910

左侧为该引脚的一些其他复用功能,在前面的章节中复用的都是GPIO 功能,而本章节中将实现I2C3_SDA 和GPIO 两个复用功能的动态切换。

136.1 设备树的修改

本小节修改好的设备树以及编译好的boot.img 镜像存放路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\90_gpioctrl09\01_内核镜像
首先根据上图中的复用功能查看设备树中是否已经对该引脚进行了复用,在确保该引脚无任何复用之后,rk3568-evb1-ddr4-v10.dtsi 设备树进行内容的添加,将根节点中的gpiol_a0 修改为以下内容:

my_gpio:gpio1_a0 {
    compatible = "mygpio";
    my-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "mygpio_func1", "mygpio_func2";
    pinctrl-0 = <&mygpio_ctrl>;
    pinctrl-1 = <&i2c3_sda>;
};

pinctrl-names 表示引脚控制器配置的名称,这里有两个值,分别对应复用1 和复用2。
pinctrl-0 指定了与该配置相关联的引脚控制器句柄,这里为&mygpio_ctrl,表示复用为gpio功能。

pinctrl-1 指定了与该配置相关联的引脚控制器句柄,这里为&i2c3_sda,表示复用为i2c3_sda 功能。

添加完成如下图所示:

image-20240923143832105

然后找到pinctrl 节点,在节点尾部进行修改和添加,具体内容如下所示:

mygpio_func1 {
    mygpio_ctrl: my-gpio-ctrl {
        rockchip,pins = <1 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>;
    };
};
mygpio_func2 {
    i2c3_sda: i2c3_sda {
        rockchip,pins = <1 RK_PA0 1 &pcfg_pull_none>;
    };
};

修改添加完成如下图所示:

image-20240923144003152

至此,关于设备树相关的修改就完成了,保存退出之后,编译内核,然后将生成的boot.img镜像烧写到开发板上即可。

136.2 驱动程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\90_gpioctrl09\02_module
编写完成的gpio_api.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/device.h>
struct pinctrl *gpio_pinctrl;          // GPIO pinctrl 实例指针
struct pinctrl_state *func1_state;     // 功能1状态
struct pinctrl_state *func2_state;     // 功能2状态
int ret;

ssize_t selectmux_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    unsigned long select;
    select = simple_strtoul(buf, NULL, 10);
    if (select == 1) {
        pinctrl_select_state(gpio_pinctrl, func1_state);     // 选择功能1状态
    } else if (select == 0) {
        pinctrl_select_state(gpio_pinctrl, func2_state);     // 选择功能2状态
    }
    return count;
}
DEVICE_ATTR_WO(selectmux);       // 定义可写的设备属性 selectmux

int pinctrl_get_and_lookstate(struct device *dev)
{
    gpio_pinctrl = pinctrl_get(dev);    // 获取GPIO pinctrl实例
    if (IS_ERR(gpio_pinctrl)) {
        printk("pinctrl_get is error\n");
        return -1;
    }

    func1_state = pinctrl_lookup_state(gpio_pinctrl, "mygpio_func1");    // 查找功能1状态
    if (IS_ERR(func1_state)) {
        printk("pinctrl_lookup_state is error\n");
        return -2;
    }

    func2_state = pinctrl_lookup_state(gpio_pinctrl, "mygpio_func2");    // 查找功能2状态
    if (IS_ERR(func2_state)) {
        printk("pinctrl_lookup_state is error\n");
        return -2;
    }

    return 0;
}

// 平台设备初始化函数
static int my_platform_probe(struct platform_device *dev)
{
    printk("This is mydriver_probe\n");
    pinctrl_get_and_lookstate(&dev->dev);     // 获取并查找GPIO pinctrl实例和状态
    device_create_file(&dev->dev, &dev_attr_selectmux);    // 在设备上创建属性文件
    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="mygpio"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

136.3 运行测试

136.3.1 编译驱动程序

在上一小节中的gpio_api.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成gpio_api.ko 目标文件,至此驱动模块就编译成功了。

136.3.2 运行测试

首先需要确保当前开发板使用的内核镜像是我们在135.2 小节中修改设备树后编译生成的镜像,然后启动开发板,首先使用以下命令查看gpio1 RK_PA0 引脚的复用功能,如下图所示:

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep 32

image-20240923144451054

可以看到在没有加载驱动之前,gpio1 RK_PA0 引脚是没有进行复用的,然后使用以下命令进行驱动的加载,如下图(图54-5)所示:

insmod gpio_api.ko

image-20240923144509674

然后使用以下命令进入/sys/devices/platform/gpio1_a0/目录,其中的selectmux 文件就是用来动态修改服用关系的,如下图所示:

cd /sys/devices/platform/gpio1_a0/

image-20240923144540706

当向selectmux 文件写入0 时表示选择功能2,也就是将该引脚复用为I2C3_SDA,当向selectmux 文件写入1 时表示选择功能1,也就是将该引脚复用为GPIO,这里我们先输入以下命令向selectmux 文件写入1,验证GPIO 的复用

echo 1 > selectmux

image-20240923144615934

然后重新使用使用以下命令查看gpio1 RK_PA0 引脚的复用功能,如下图所示:

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep 32

image-20240923144630499

根据打印信息可以得到gpio1 RK_PA0 已经被设置为了GPIO 功能,然后输入以下命令向selectmux 文件写入0,验证I2C3_SDA 的复用

echo 0 > selectmux

image-20240923144659578

然后重新使用使用以下命令查看gpio1 RK_PA0 引脚的复用功能,如下图所示:

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep 32

image-20240923144717985

根据打印信息可以得到gpio1 RK_PA0 已经被复用为了I2C3_SDA 功能,最后使用以下命令进行驱动的卸载,如下图所示:

rmmod gpio_api.ko

image-20240923144735343

至此,实现动态切换引脚复用功能实战就完成了。

第十三篇输入子系统


文章作者: 葛杨文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 葛杨文 !
评论
  目录