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 总线中进行注册,代码实现如下所示:


文章作者: 葛杨文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 葛杨文 !
评论
  目录