Skip to content

Latest commit

 

History

History
1319 lines (1001 loc) · 53.3 KB

File metadata and controls

1319 lines (1001 loc) · 53.3 KB

四、使用设备树

现代计算机真的是由复杂的外设组成的复杂系统,有成吨的不同配置设置;这就是为什么将设备驱动配置的所有可能变体保存在一个专用文件中可以解决很多问题。对系统的结构进行逻辑描述(也就是说,它们是如何相互连接的,而不仅仅是它们的列表)可以让系统开发人员将注意力集中在设备驱动机制上,而无需管理所有可能的用户设置。

此外,了解每个外设是如何连接到系统的(例如,外设依赖于哪条总线),可以实现真正智能的外设管理系统。这样的系统能够以正确的顺序正确地激活(或停用)特定设备工作所需的所有子系统。

让我们回顾一个例子:想想一个 u 盘,当插入你的电脑时,它会激活几个设备。系统知道 USB 端口连接到特定的 USB 控制器,该控制器在特定的地址映射到系统的内存中,以此类推。

出于所有这些原因(以及其他原因),Linux 开发人员采用了设备树,简单来说,这是一种描述硬件的数据结构。它不是将每个内核设置硬编码到代码中,而是可以用定义良好的数据结构来描述,该数据结构在引导加载程序引导期间被传递给内核。这也是所有设备驱动(和其他内核实体)可以获取其配置数据的地方。

设备树和内核配置文件(Linux 源码上目录中的.config文件)的主要区别在于,虽然这样的文件告诉我们内核的哪些组件是启用的,哪些不是,但是设备树保存着它们的配置。因此,如果我们希望从内核的来源向我们的系统添加一个驱动,我们必须在.config文件中指定它。另一方面,如果我们希望指定驱动的设置(内存地址、特殊设置等),我们必须在设备树中指定它们。

在这一章中,我们将看到如何编写一个设备树,以及如何从中获取对驱动有用的信息。

本章由以下食谱组成:

  • 使用设备树编译器和实用程序
  • 从设备树中获取特定于应用的数据
  • 使用设备树描述角色驱动
  • 下载固件
  • 为特定外围设备配置中央处理器的引脚

技术要求

您可以在附录中找到本章的更多信息。

本章使用的代码和其他文件可以在https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 04下载。

使用设备树编译器和实用程序

我们需要合适的工具将我们的代码转换成 Linux 可以理解的二进制格式。具体来说,我们需要一种方法将设备树源 ( DTS )文件转换为其二进制形式:设备树二进制 ( DTB )。

在这个食谱中,我们将发现如何在我们的系统上安装设备树编译器 ( dtc)以及如何使用它为任何设备树生成二进制文件。

准备好

要将 DTS 文件转换为 DTB 文件,我们必须使用设备树编译器(命名为dtc)和一组适当的工具,我们可以使用这些工具来检查或操作 DTB 文件(设备树实用程序)。

每一个最近的 Linux 版本都在linux/scripts/dtc目录中有自己的dtc程序副本,在内核编译期间使用。然而,我们不需要安装 Linux 源代码就可以在 Ubuntu 上运行dtc及其实用程序;事实上,我们可以通过使用如下常用的安装命令来获得它们:

$ sudo apt install device-tree-compiler

安装后,我们可以如下执行dtc编译器,以显示其版本:

$ dtc -v
Version: DTC 1.4.5

怎么做...

现在,我们准备使用以下步骤将第一个 DTS 文件转换为其等效的 DTB 二进制形式。

  1. 我们可以通过使用带有以下命令行的dtc编译器来做到这一点:
$ dtc -o simple_platform.dtb simple_platform.dts

simple_platform.dts can be retrieved from GitHub sources; however the reader can use his/her own DTS file to test dtc.

现在,我们的 DTB 文件应该在当前目录中可用:

$ file simple_platform.dtb
simple_platform.dtb: Device Tree Blob version 17, size=1602, boot CPU=0, string block size=270, DT structure block size=1276

它是如何工作的...

将 DTS 文件转换成 DTB 文件类似于普通编译器的工作方式,但是应该说一下反向操作。

如果我们看一下simple_platform-reverted.dts,我们注意到它看起来非常类似于原始的simple_platform.dts文件(除了显形、标签和十六进制形式的数字);事实上,关于时钟设置,我们有以下不同之处:

$ diff -u simple_platform.dts simple_platform-reverted.dts | tail -29
-      clks: clock@f00 {
+      clock@f00 {
           compatible = "fsl,mpc5121-clock";
           reg = <0xf00 0x100>;
-          #clock-cells = <1>;
-          clocks = <&osc>;
+          #clock-cells = <0x1>;
+          clocks = <0x1>;
           clock-names = "osc";
+          phandle = <0x3>;
       };

关于串行控制器设置,我们有以下不同之处:

-      serial0: serial@11100 {
+      serial@11100 {
           compatible = "fsl,mpc5125-psc-uart", "fsl,mpc5125-psc";
           reg = <0x11100 0x100>;
-          interrupt-parent = <&ipic>;
-          interrupts = <40 0x8>;
-          fsl,rx-fifo-size = <16>;
-          fsl,tx-fifo-size = <16>;
-          clocks = <&clks 47>, <&clks 34>;
+          interrupt-parent = <0x2>;
+          interrupts = <0x28 0x8>;
+          fsl,rx-fifo-size = <0x10>;
+          fsl,tx-fifo-size = <0x10>;
+          clocks = <0x3 0x2f 0x3 0x22>;
           clock-names = "ipg", "mclk";
       };
   };

从前面的输出中,我们可以看到serial0clks标签已经消失,因为它们在 DTB 文件中是不需要的;显形现在也被明确报告,相应的符号名如ipicclks被替换,所有的数字都被转换成十六进制形式。

还有更多...

设备树是一个非常复杂的软件,它是描述一个系统的强大方式,这就是为什么我们需要更多地谈论它。我们还应该看看设备树实用程序,因为对于内核开发人员来说,管理设备树二进制形式非常有用。

将二进制设备树还原为其源

dtc程序可以恢复编译过程,允许开发人员使用如下命令行从二进制文件中检索源文件:

$ dtc -o simple_platform-reverted.dts simple_platform.dtb

当我们需要检查 DTB 文件时,这非常有用。

请参见

从设备树中获取特定于应用的数据

现在我们知道如何读取设备树文件以及如何在用户空间中管理它。在这个食谱中,我们将看到如何提取它在内核中保存的配置设置。

准备好

为了完成我们的工作,我们可以使用存储在 DTB 的所有数据来启动我们的 ESPRESSObin,然后使用 ESPRESSObin 作为系统测试。

我们知道,ESPRESSObin 的 DTS 文件存储在linux/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts的内核源中,或者可以通过执行dtc命令从运行的内核中提取,如以下代码所示:

# dtc -I fs -o espressobin-reverted.dts /proc/device-tree/

现在让我们把这个文件拆开,因为我们可以用它来验证我们刚刚读取的数据是正确的。

怎么做...

为了展示我们如何从运行的设备树中读取数据,我们可以使用一个来自 GitHub 源的内核模块(就像文件get_dt_data.c中报告的那个)。

  1. 在文件中,我们有一个空的模块exit()函数,因为我们在模块的init()函数中没有分配任何东西;事实上,它只是向我们展示了如何解析设备树。get_dt_data_init()函数接受一个可选的输入参数:存储在path变量中的设备树路径,该变量在以下代码片段中定义:
#define PATH_DEFAULT "/"
static char *path = PATH_DEFAULT;
module_param(path, charp, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(path, "a device tree pathname " \
                       "(default is \"" PATH_DEFAULT "\")");
  1. 然后,作为第一步,get_dt_data_init()函数使用of_find_node_by_path()函数获取指向要检查的所需节点的指针:
static int __init get_dt_data_init(void)
{
    struct device_node *node, *child;
    struct property *prop;

    pr_info("path = \"%s\"\n", path);

    /* Find node by its pathname */
    node = of_find_node_by_path(path);
    if (!node) {
        pr_err("failed to find device-tree node \"%s\"\n", path);
        return -ENODEV;
    }
    pr_info("device-tree node found!\n");
  1. 接下来,它调用print_main_prop()函数,该函数只打印节点的主要属性,如下所示:
static void print_main_prop(struct device_node *node)
{
    pr_info("+ node = %s\n", node->full_name);
    print_property_u32(node, "#address-cells");
    print_property_u32(node, "#size-cells");
    print_property_u32(node, "reg");
    print_property_string(node, "name");
    print_property_string(node, "compatible");
    print_property_string(node, "status");
}

每个打印功能报告如下:

static void print_property_u32(struct device_node *node, const char *name)
{
    u32 val32;
    if (of_property_read_u32(node, name, &val32) == 0)
        pr_info(" \%s = %d\n", name, val32); 
}

static void print_property_string(struct device_node *node, const char *name)
{
    const char *str;
    if (of_property_read_string(node, name, &str) == 0)
        pr_info(" \%s = %s\n", name, str);
}
  1. 对于最后两个步骤,get_dt_data_init()函数使用for_each_property_of_node()宏显示节点的所有属性,for_each_child_of_node()宏迭代节点的所有子节点并显示其所有主要属性,如下图所示:
    pr_info("now move through all properties...\n");
    for_each_property_of_node(node, prop)
        pr_info("-> %s\n", prop->name);

    /* Move through node's children... */
    pr_info("Now move through children...\n");
    for_each_child_of_node(node, child)
        print_main_prop(child);

    /* Force module unloading... */
    return -EINVAL;
    }

它是如何工作的...

在第一步中,很明显,如果我们将模块插入到指定path=<my_path>的内核中,我们会强制要求这个值;否则,我们只接受默认值,即根(由/字符表示)。其余的步骤是不言自明的。

理解代码应该非常容易;实际上get_dt_data_init()函数只是调用of_find_node_by_path(),传递设备路径名;没有错误,我们使用print_main_prop()来显示节点名称和节点的一些主要(或有趣)属性:

static void print_main_prop(struct device_node *node)
{
    pr_info("+ node = %s\n", node->full_name);
    print_property_u32(node, "#address-cells");
    print_property_u32(node, "#size-cells");
    print_property_u32(node, "reg");
    print_property_string(node, "name");
    print_property_string(node, "compatible");
    print_property_string(node, "status");
}

请注意,print_property_u32()print_property_string()功能的定义方式是,如果所提供的属性不存在,则不显示任何内容:

static void print_property_u32(struct device_node *node, const char *name)
{
    u32 val32;
    if (of_property_read_u32(node, name, &val32) == 0)
        pr_info(" \%s = %d\n", name, val32);
}

static void print_property_string(struct device_node *node, const char *name)
{
    const char *str;
    if (of_property_read_string(node, name, &str) == 0)
        pr_info(" \%s = %s\n", name, str);
}

Functions such as of_property_read_u32()/of_property_read_string() and for_each_child_of_node()/for_each_property_of_node() and friends are defined in the header file linux/include/linux/of.h of kernel sources.

一旦从get_dt_data.c文件编译,我们应该得到它的编译版本名为get_dt_data.ko,适合加载到 ESPRESSObin:

$ make KERNEL_DIR=../../../linux
make -C ../../../linux \
            ARCH=arm64 \
            CROSS_COMPILE=aarch64-linux-gnu- \
            SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_4/get_dt_data modules
make[1]: Entering directory '/home/giometti/Projects/ldddc/linux'
  CC [M] /home/giometti/Projects/ldddc/github/chapter_4/get_dt_data/get_dt_data.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC /home/giometti/Projects/ldddc/github/chapter_4/get_dt_data/get_dt_data.mod.o
  LD [M] /home/giometti/Projects/ldddc/github/chapter_4/get_dt_data/get_dt_data.ko
make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux'

以下是我们在新创建的内核模块中使用modinfo时应该得到的结果:

# modinfo get_dt_data.ko 
filename: /root/get_dt_data.ko
version: 0.1
description: Module to inspect device tree from the kernel
author: Rodolfo Giometti
license: GPL
srcversion: 6926CA8AD5E7F8B45C97CE6
depends: 
name: get_dt_data
vermagic: 4.18.0 SMP preempt mod_unload aarch64
parm: path:a device tree pathname (default is "/") (charp)

还有更多...

好的,让我们通过使用以下命令来尝试使用path的默认值:

# insmod get_dt_data.ko

我们应该得到如下输出:

get_dt_data: path = "/"
get_dt_data: device-tree node found!
...

通过使用/作为路径名,我们显然在设备树中找到了对应的条目,因此输出继续如下:

...
get_dt_data: now getting main properties...
get_dt_data: + node = 
get_dt_data: #address-cells = 2
get_dt_data: #size-cells = 2
get_dt_data: name = 
get_dt_data: compatible = globalscale,espressobin
get_dt_data: now move through all properties...
get_dt_data: -> model
get_dt_data: -> compatible
get_dt_data: -> interrupt-parent
get_dt_data: -> #address-cells
get_dt_data: -> #size-cells
get_dt_data: -> name
...

以下是根节点的所有属性,可以对照原始源或在espressobin-reverted.dts文件中进行验证:

/ {
    #address-cells = <0x2>;
    model = "Globalscale Marvell ESPRESSOBin Board";
    #size-cells = <0x2>;
    interrupt-parent = <0x1>;
    compatible = "globalscale,espressobin", "marvell,armada3720", "marvell,armada3710";

Readers should notice that, in this case, the name property is empty due to the fact we are inspecting the root node, and for the compatible property only the first entry is displayed because we used the of_property_read_string() function instead of the corresponding array  of_property_read_string_array() version and friends.

在所有节点的属性之后,我们的程序将遍历它的所有子节点,如下所示:

...
get_dt_data: Now move through children...
get_dt_data: + node = aliases
get_dt_data: name = aliases
get_dt_data: + node = cpus
get_dt_data: #address-cells = 1
get_dt_data: #size-cells = 0
get_dt_data: name = cpus
...
get_dt_data: + node = soc
get_dt_data: #address-cells = 2
get_dt_data: #size-cells = 2
get_dt_data: name = soc
get_dt_data: compatible = simple-bus
get_dt_data: + node = chosen
get_dt_data: name = chosen
get_dt_data: + node = memory@0
get_dt_data: reg = 0
get_dt_data: name = memory
get_dt_data: + node = regulator
get_dt_data: name = regulator
get_dt_data: compatible = regulator-gpio
...

此时,get_dt_data_init()功能做一个return -EINVAL,不是返回错误状态,而是强制模块卸载;事实上,作为最后一条打印出来的消息,我们看到了以下内容:

insmod: ERROR: could not insert module get_dt_data.ko: Invalid parameters

现在,为了展示一种不同的用法,我们可以尝试通过在命令行中指定path=/cpus命令来询问关于系统 CPU 的信息:

# insmod get_dt_data.ko path=/cpus

程序显示找到了一个节点:

get_dt_data: path = "/cpus"
get_dt_data: device-tree node found!

然后它开始打印节点的信息:

get_dt_data: now getting main properties...
get_dt_data: + node = cpus
get_dt_data: #address-cells = 1
get_dt_data: #size-cells = 0
get_dt_data: name = cpus

最后,它显示了所有孩子的属性:

get_dt_data: now move through all properties...
get_dt_data: -> #address-cells
get_dt_data: -> #size-cells
get_dt_data: -> name
get_dt_data: Now move through children...
get_dt_data: + node = cpu@0
get_dt_data: reg = 0
get_dt_data: name = cpu
get_dt_data: compatible = arm,cortex-a53
get_dt_data: + node = cpu@1
get_dt_data: reg = 1
get_dt_data: name = cpu
get_dt_data: compatible = arm,cortex-a53

Note that the following error message can be safely ignored because we force it to automatically retrieve the module to be unloaded by the insmod command: insmod: ERROR: could not insert module get_dt_data.ko: Invalid parameters

以类似的方式,我们可以获得关于 I2C 控制器的信息,如下所示:

# insmod get_dt_data.ko path=/soc/internal-regs@d0000000/i2c@11000
get_dt_data: path = "/soc/internal-regs@d0000000/i2c@11000"
get_dt_data: device-tree node found!
get_dt_data: now getting main properties...
get_dt_data: + node = i2c@11000
get_dt_data: #address-cells = 1
get_dt_data: #size-cells = 0
get_dt_data: reg = 69632
get_dt_data: name = i2c
get_dt_data: compatible = marvell,armada-3700-i2c
get_dt_data: status = disabled
get_dt_data: now move through all properties...
...

请参见

  • 要查看检查设备树的所有可用功能,读者可以查看包含的linux/include/linux/of.h文件,该文件有很好的记录。

使用设备树描述角色驱动

此时,我们已经拥有了通过使用设备树来定义新角色设备所需的所有信息。特别是这一次,为了注册我们的chrdev设备,我们可以使用我们在 第 3 章中跳过的新 API,使用字符驱动

准备好

如前一段所述,我们可以使用设备树节点向系统添加新设备。特别是,我们可以获得如下所述的定义:

chrdev {
    compatible = "ldddc,chrdev";
    #address-cells = <1>;
    #size-cells = <0>;

    chrdev@2 {
        label = "cdev-eeprom";
        reg = <2>;
    };

    chrdev@4 {
        label = "cdev-rom";
        reg = <4>;
        read-only;
    };
};

All these modifications can be applied using the  add_chrdev_devices.dts.patch file  in the root directory of the kernel sources, as shown in the following:

$ patch -p1 < ../github/chapter_04/chrdev/add_chrdev_devices.dts.patch Then the kernel must be recompiled and reinstalled (with the ESPRESSObin's DTB file) in order to take effect.

在这个例子中,我们定义了一个chrdev节点,它定义了一组与"ldddc,chrdev"和两个子节点兼容的新设备;每个子节点用自己的设置定义一个特定的设备。第一个子节点定义了一个标记为"cdev-eeprom""ldddc,chrdev"设备,其reg属性等于2,而第二个子节点定义了另一个标记为"cdev-rom""ldddc,chrdev"设备,其reg属性等于4,其read-only属性。

#address-cells#size-cells属性必须是 1 和 0,因为子设备的reg属性包含一个表示“设备地址”的值。事实上,可寻址的设备使用#address-cells#size-cellsreg属性将地址信息编码到设备树中。

每个可寻址设备获得一个reg属性,如下所示:

reg = <address1 length1 [address2 length2] [address3 length3] ... >

每个元组代表设备使用的地址范围,每个地址或长度值是一个或多个 32 位整数的列表,称为单元(长度也可以是空的,如我们的示例所示)。

由于地址和长度字段可能不同且大小可变,父节点中的#address-cells#size-cells属性用于说明每个子节点字段中有多少单元。

For further information regarding the  #address-cells, #size-cells, and reg properties, you can take a look at the device tree specification at https://www.devicetree.org/specifications/.

怎么做...

现在是时候看看我们如何使用前面的设备树定义来创建我们的 char 设备了(请注意,这次我们将创建多个设备!).

  1. 模块的init()exit()功能都必须重写,如以下代码所示。chrdev_init()样子如下:
static int __init chrdev_init(void)
{
    int ret;

    /* Create the new class for the chrdev devices */
    chrdev_class = class_create(THIS_MODULE, "chrdev");
    if (!chrdev_class) {
        pr_err("chrdev: failed to allocate class\n");
        return -ENOMEM;
    }

    /* Allocate a region for character devices */
    ret = alloc_chrdev_region(&chrdev_devt, 0, MAX_DEVICES, "chrdev");
    if (ret < 0) {
        pr_err("failed to allocate char device region\n");
        goto remove_class;
    }

    pr_info("got major %d\n", MAJOR(chrdev_devt));

    return 0;

remove_class:
    class_destroy(chrdev_class);

    return ret;
}
  1. chrdev_exit()功能如下:
static void __exit chrdev_exit(void)
{
    unregister_chrdev_region(chrdev_devt, MAX_DEVICES);
    class_destroy(chrdev_class);
}

All code can be retrieved from GitHub sources in the chrdev.c file.

  1. 如果我们尝试将模块插入内核,我们应该会得到如下结果:
# insmod chrdev.ko 
chrdev: loading out-of-tree module taints kernel.
chrdev:chrdev_init: got major 239
  1. 要创建角色设备,我们必须使用下一个chrdev_device_register()功能,但是我们必须首先检查设备是否已经创建:
int chrdev_device_register(const char *label, unsigned int id,
                unsigned int read_only,
                struct module *owner, struct device *parent) 
{
    struct chrdev_device *chrdev;
    dev_t devt;
    int ret;

    /* First check if we are allocating a valid device... */
    if (id >= MAX_DEVICES) {
        pr_err("invalid id %d\n", id);
        return -EINVAL;
    }
    chrdev = &chrdev_array[id];

    /* ... then check if we have not busy id */
    if (chrdev->busy) {
        pr_err("id %d\n is busy", id);
        return -EBUSY; 
    }

然后我们做一些比前一章稍微复杂一点的事情,在这一章中我们简单地调用了register_chrdev()函数;现在真正重要的是cdev_init()cdev_add()device_create()函数的调用顺序,它们实际上完成了工作,如下所示:

    /* Create the device and initialize its data */
    cdev_init(&chrdev->cdev, &chrdev_fops);
    chrdev->cdev.owner = owner;

    devt = MKDEV(MAJOR(chrdev_devt), id);
    ret = cdev_add(&chrdev->cdev, devt, 1); 
    if (ret) {
        pr_err("failed to add char device %s at %d:%d\n",
                label, MAJOR(chrdev_devt), id);
        return ret;
    }

    chrdev->dev = device_create(chrdev_class, parent, devt, chrdev,
                   "%s@%d", label, id);
    if (IS_ERR(chrdev->dev)) {
        pr_err("unable to create device %s\n", label); 
        ret = PTR_ERR(chrdev->dev);
        goto del_cdev;
    }

一旦device_create()函数返回成功,我们使用dev_set_drvdata()函数保存一个指向我们的驱动数据的指针,然后像这样初始化:

  dev_set_drvdata(chrdev->dev, chrdev);

 /* Init the chrdev data */
 chrdev->id = id; 
 chrdev->read_only = read_only;
 chrdev->busy = 1;
 strncpy(chrdev->label, label, NAME_LEN);
 memset(chrdev->buf, 0, BUF_LEN);

 dev_info(chrdev->dev, "chrdev %s with id %d added\n", label, id);

 return 0;

del_cdev:
 cdev_del(&chrdev->cdev);

 return ret;
}
EXPORT_SYMBOL(chrdev_device_register);

所有这些功能都在struct chrdev_device上运行,定义如下:

/* Main struct */
struct chrdev_device {
    char label[NAME_LEN];
    unsigned int busy : 1;
    char buf[BUF_LEN];
    int read_only;

    unsigned int id; 
    struct module *owner;
    struct cdev cdev;
    struct device *dev;
};

它是如何工作的...

功能内的第 1 步、**、**中,这次我们使用了alloc_chrdev_region()功能,要求内核预留一些名为chrdev的字符设备(在我们这里,这个数字相当于MAX_DEVICES的定义)。chrdev信息随后存储在chrdev_devt变量中。

在这里,我们应该小心,注意我们也通过调用class_create()函数来创建一个设备类。为设备树定义的每个设备必须属于一个适当的类,由于我们的chrdev驱动是新的,我们需要一个专用的类。

In the next steps, I will be more clear about the reason we need to do it this way; for the moment, we should consider it as a compulsory data allocation. It's quite clear that the  unregister_chrdev_region() function just releases all of the chrdev data allocated in with alloc_chrdev_region(). In step 3, if we take a look at the /proc/devices file, we get the following:

# grep chrdev /proc/devices
239 chrdev

很好!现在我们有了类似于第 3 章与字符驱动合作的东西!但是,这一次,如果我们试图用mknod创建一个特殊的字符文件,并试图从中读取,我们会得到一个错误!

# mknod /dev/chrdev c 239 0
# cat /dev/chrdev
cat: /dev/chrdev: No such device or address

内核告诉我们设备不存在!这是因为我们还没有创建任何东西,只是保留了一些内核内部数据。

步骤 4 、中,前四个字段只是相对于我们的特定实现,而后四个字段几乎存在于每个字符驱动实现中:id字段只是每个chrdev的唯一标识符(请记住,我们的实现支持MAX_DEVICES实例),owner指针用于存储我们的驱动模块的所有者,cdev结构保存关于我们的字符设备的所有内核数据,dev指针指向与我们在设备树中指定的内核相关的内核struct device

所以,cdev_init()是用我们的文件操作来初始化cdevcdev_add()用于定义我们司机的主要和次要号码;device_create()用于将devt数据粘贴到dev指向的数据上;我们的chrdev类(由chrdev_class指针表示)实际上创建了字符设备。

但是chrdev.c文件中没有任何函数调用chrdev_device_register()函数;这就是为什么使用EXPORT_SYMBOL()定义将其声明为导出符号的原因。事实上,这个函数被称为chrdev_req_probe()函数,在另一个模块中被定义为名为chrdev-req.c的文件,这在下面的代码片段中有所报道。该功能首先了解我们需要注册多少台设备:

static int chrdev_req_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct fwnode_handle *child;
    struct module *owner = THIS_MODULE;
    int count, ret;

    /* If we are not registering a fixed chrdev device then get
     * the number of chrdev devices from DTS
     */
    count = device_get_child_node_count(dev);
    if (count == 0)
        return -ENODEV;
    if (count > MAX_DEVICES)
        return -ENOMEM;

然后,对于每个设备,在读取设备属性后,chrdev_device_register()调用在系统上注册该设备(对于设备树中报告的每个设备,如前面的代码所示):

 device_for_each_child_node(dev, child) {
        const char *label;
        unsigned int id, ro;

        /*
         * Get device's properties
         */

        if (fwnode_property_present(child, "reg")) {
            fwnode_property_read_u32(child, "reg", &id);
        } else {
...

        }
        ro = fwnode_property_present(child, "read-only");

        /* Register the new chr device */
        ret = chrdev_device_register(label, id, ro, owner, dev);
        if (ret) { 
            dev_err(dev, "unable to register");
        }
    }

    return 0;
}

但是系统怎么知道什么时候必须调用chrdev_req_probe()函数呢?嗯,继续看chrdev-req.c就很清楚了;事实上,在接近结尾时,我们发现了以下代码:

static const struct of_device_id of_chrdev_req_match[] = {
    {
        .compatible = "ldddc,chrdev",
    },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, of_chrdev_req_match);

static struct platform_driver chrdev_req_driver = {
    .probe = chrdev_req_probe,
    .remove = chrdev_req_remove,
    .driver = {
        .name = "chrdev-req",
        .of_match_table = of_chrdev_req_match,
    },
};
module_platform_driver(chrdev_req_driver);

当我们将chrdev-req.ko模块插入内核时,我们使用module_platform_driver()定义一个新的平台驱动,然后内核开始寻找compatible属性设置为"ldddc,chrdev"的节点;如果找到,它将执行我们设置为chrdev_req_probe()probe指针所指向的功能。这将导致注册新的驱动。

在展示它是如何工作的之前,让我们看一下相反的步骤,目的是在角色驱动分配期间从内核释放我们请求的任何东西。当我们移除chrdev-req.ko模块时,内核调用平台驱动的remove功能,也就是chrdev-req.c文件中的chrdev_req_remove(),部分报告如下:

static int chrdev_req_remove(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct fwnode_handle *child;
    int ret;

    device_for_each_child_node(dev, child) {
        const char *label; 
        int id;

        /*
         * Get device's properties
         */

        if (fwnode_property_present(child, "reg")) 
            fwnode_property_read_u32(child, "reg", &id);
        else
            BUG();
        if (fwnode_property_present(child, "label"))
            fwnode_property_read_string(child, "label", &label);
        else
            BUG();

        /* Register the new chr device */
        ret = chrdev_device_unregister(label, id);
        if (ret)
            dev_err(dev, "unable to unregister");
    }

    return 0;
}

该函数位于chrdev.c文件中,调用chrdev_device_unregister()(针对设备树中的每个chrdev节点),报告如下;它从做一些健全性检查开始:

int chrdev_device_unregister(const char *label, unsigned int id)
{
    struct chrdev_device *chrdev;

    /* First check if we are deallocating a valid device... */
    if (id >= MAX_DEVICES) {
        pr_err("invalid id %d\n", id);
        return -EINVAL;
    }
    chrdev = &chrdev_array[id];

    /* ... then check if device is actualy allocated */
    if (!chrdev->busy || strcmp(chrdev->label, label)) {
        pr_err("id %d is not busy or label %s is not known\n",
                        id, label);
        return -EINVAL;
    }

但是随后它通过使用device_destroy()cdev_del()功能注销驱动:

    /* Deinit the chrdev data */
    chrdev->id = 0;
    chrdev->busy = 0;

    dev_info(chrdev->dev, "chrdev %s with id %d removed\n", label, id);

    /* Dealocate the device */
    device_destroy(chrdev_class, chrdev->dev->devt);
    cdev_del(&chrdev->cdev);

    return 0;
}
EXPORT_SYMBOL(chrdev_device_unregister);

还有更多...

使用设备树不仅对描述外围设备(然后是整个系统)有用;通过使用它,我们还可以访问 Linux 向内核开发人员提供的几个现成的功能。所以让我们来看看最重要(也是最有用)的。

如何在/dev 中创建设备文件

第 3 章使用 Char Drivers 时,当我们创建一个新的字符设备时,用户空间中什么都没有发生,我们不得不使用mknod命令手工创建一个字符设备文件;但是,在本章中,当我们插入第二个内核模块时,这就创建了我们新的chrdev设备。通过从设备树中获取它们的属性,在/dev目录中,两个新的字符文件被自动创建。

正是 Linux 的内核对象机制实现了这种魔力;让我们看看如何。

每当在内核中创建新设备时,都会生成新的内核事件并将其发送到用户空间;然后,这个新事件被解释它的专用应用捕获。这些特殊的应用可能会有所不同,但是几乎所有重要的 Linux 发行版都使用的这种类型的最著名的应用是udev应用。

udev守护进程的诞生是为了替换和创建一种机制,在/dev目录下自动创建特殊的设备文件,它工作得非常好,现在它被用于几个不同的任务。事实上,udev守护进程在系统中添加或删除设备时(或改变状态时)直接从内核接收设备内核事件(称为ueevents),并且对于每个事件,它根据其配置文件执行一组规则。如果规则匹配各种设备属性,则执行该规则,然后在/dev目录中相应地创建新文件;匹配规则还可以提供额外的设备信息,用于创建有意义的符号链接名称、执行脚本等等!

For further information regarding udev rules, a good starting point is a related page in the Debian Wiki at https://wiki.debian.org/udev.

要监控这些事件,我们可以使用udevadm工具,该工具位于udev包中,如以下命令行所示:

# udevadm monitor -k -p -s chrdev
monitor will print the received events for:
KERNEL - the kernel uevent

通过使用monitor子命令,我们选择udevadm监视器特性(因为udevadm可以执行其他几个任务),通过指定-k选项参数,我们要求只显示内核生成的消息(因为一些消息也可能来自用户空间);此外,通过使用-p选项参数,我们要求显示事件属性,并且使用-s选项参数,我们从子系统中选择仅匹配chrdev字符串的消息。

To see all kernel messages, during the chrdev module insertion the kernel sends just execute udevadm monitor command, dropping all of these option arguments.

要查看新事件,只需执行上述命令,然后在另一个终端(或直接从串行控制台)重复内核模块插入。插入chrdev-req.ko模块后,我们看到与之前相同的内核消息:

# insmod chrdev-req.ko 
chrdev cdev-eeprom@2: chrdev cdev-eeprom with id 2 added
chrdev cdev-rom@4: chrdev cdev-rom with id 4 added

然而,在我们执行udevadm消息的终端中,我们现在应该看到如下内容:

KERNEL[14909.624343] add /devices/platform/chrdev/chrdev/cdev-eeprom@2 (chrdev)
ACTION=add
DEVNAME=/dev/cdev-eeprom@2
DEVPATH=/devices/platform/chrdev/chrdev/cdev-eeprom@2
MAJOR=239
MINOR=2
SEQNUM=2297
SUBSYSTEM=chrdev

KERNEL[14909.631813] add /devices/platform/chrdev/chrdev/cdev-rom@4 (chrdev)
ACTION=add
DEVNAME=/dev/cdev-rom@4
DEVPATH=/devices/platform/chrdev/chrdev/cdev-rom@4
MAJOR=239
MINOR=4
SEQNUM=2298
SUBSYSTEM=chrdev

这些是通知udev已经创建了两个名为/dev/cdev-eeprom@2/dev/cdev-rom@4的新设备(带有其他属性)的内核消息,因此udev拥有在/dev目录下创建新文件所需的所有信息。

下载固件

通过使用设备树,我们现在能够为我们的驱动指定许多不同的设置,但是还有最后一件事我们必须看到:如何将固件加载到我们的设备中。事实上,一些设备可能需要一个程序来运行,由于许可证的原因,该程序不能在内核中链接。

在本节中,我们将看到一些示例,说明我们如何要求内核为我们的设备加载固件。

准备好

一些外围设备需要固件才能工作,然后我们需要一种机制来将这样的二进制数据加载到其中。Linux 为我们提供了不同的机制来完成这项工作,它们都引用了request_firmware()函数。

每当我们在驱动中使用request_firmware(..., "filename", ...)函数调用(或它的一个朋友)时(指定一个文件名),内核就开始查看不同的位置:

  • 首先,它会查看引导映像文件,以防固件从其中加载;这是因为我们可以在编译期间将二进制代码与内核捆绑在一起。但是,只有当固件是自由软件时,才允许使用这种解决方案;否则无法链接到 Linux。如果我们也必须重新编译内核,那么在更改固件数据时也不是很灵活。
  • 如果内核中没有存储任何数据,它将开始从文件系统中直接加载固件数据,方法是在多个路径位置中查找filename,从为内核命令行指定的位置开始,使用firmware_class.path="<path>"选项参数,然后在/lib/firmware/updates/<UTS_RELEASE>,然后进入/lib/firmware/updates,然后进入/lib/firmware/<UTS_RELEASE>,最后进入/lib/firmware目录。

<UTS_RELEASE> is the kernel release version number, which can be obtained directly from the kernel by using the uname -r command as in the following: $ uname -r 4.15.0-45-generic

  • 如果最后一步也失败了,那么内核可以尝试回退过程,包括启用固件加载器用户助手。必须通过启用以下内核配置设置来为内核配置启用最后一次加载固件的机会:
CONFIG_FW_LOADER_USER_HELPER=y
CONFIG_FW_LOADER_USER_HELPER_FALLBACK=y

通过使用通常的make menuconfig方法,我们必须通过设备驱动,然后通用驱动选项,和固件加载程序条目来启用它们(见下面的截图)。

启用这些设置并重新编译内核后,我们可以详细研究如何在内核中为驱动加载自定义固件。

怎么做...

首先,我们需要一个专注于固件加载的chrdev-req.c文件的修改版本;这就是为什么最好使用另一个文件。

  1. 为了完成我们的工作,我们可以使用具有以下设备定义的chrdev-fw.c文件:
static const struct of_device_id of_chrdev_req_match[] = {
    {
        .compatible = "ldddc,chrdev-fw_wait",
    },
    {
        .compatible = "ldddc,chrdev-fw_nowait",
    },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, of_chrdev_req_match);

static struct platform_driver chrdev_req_driver = {
    .probe = chrdev_req_probe,
    .remove = chrdev_req_remove,
    .driver = {
        .name = "chrdev-fw",
        .of_match_table = of_chrdev_req_match,
    },
};
module_platform_driver(chrdev_req_driver);

The  chrdev-fw.c file can be found in the GitHub sources for this chapter.

  1. 在这种情况下,我们的探测功能可以如下实现,在chrdev_req_probe()功能开始时,我们读取设备的一些属性:
static int chrdev_req_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct fwnode_handle *fwh = of_fwnode_handle(np);
    struct module *owner = THIS_MODULE;
    const char *file;
    int ret = 0;

    /* Read device properties */
    if (fwnode_property_read_string(fwh, "firmware", &file)) {
        dev_err(dev, "unable to get property \"firmware\"!");
        return -EINVAL;
    }

    /* Load device firmware */
    if (of_device_is_compatible(np, "ldddc,chrdev-fw_wait"))
        ret = chrdev_load_fw_wait(dev, file);
    else if (of_device_is_compatible(np, "ldddc,chrdev-fw_nowait"))
        ret = chrdev_load_fw_nowait(dev, file);
    if (ret)
        return ret;

然后,我们注册 char 设备:

    /* Register the new chr device */
    ret = chrdev_device_register("chrdev-fw", 0, 0, owner, dev);
    if (ret) {
        dev_err(dev, "unable to register");
        return ret;
    }

    return 0;
}
  1. 前一种设备类型调用chrdev_load_fw_wait()函数,该函数执行下一步。它从请求固件的数据结构开始:
static int chrdev_load_fw_wait(struct device *dev, const char *file)
{
    char fw_name[FIRMWARE_NLEN];
    const struct firmware *fw;
    int ret;

    /* Compose firmware filename */
    if (strlen(file) > (128 - 6 - sizeof(FIRMWARE_VER)))
        return -EINVAL;
    sprintf(fw_name, "%s-%s.bin", file, FIRMWARE_VER);

    /* Do the firmware request */
    ret = request_firmware(&fw, fw_name, dev);
    if (ret) {
        dev_err(dev, "unable to load firmware\n");
        return ret;
    }

然后转储接收到的数据,并最终释放固件先前分配的数据结构:

    dump_data(fw->data, fw->size);

    /* Firmware data has been read, now we can release it */
    release_firmware(fw);

    return 0;
}

The FIRMWARE_VER and FIRMWARE_NLEN macros have been defined within the  chrdev-fw.c file as shown in the following: #define FIRMWARE_VER     "1.0.0" #define FIRMWARE_NLEN    128

它是如何工作的...

步骤 1 中,在of_chrdev_req_match[]阵列中,我们现在有两个设备可以用来测试加载固件的不同方式。一个名为ldddc,chrdev-fw_wait的设备可用于测试从文件系统直接加载固件,而另一个名为ldddc,chrdev-fw_nowait的设备可用于测试固件加载器的用户助手。 我用这两个例子向读者展示了两种不同的固件加载技术,但实际上,这两种方法可以用于不同的目的;前者可以在我们的设备自启动以来需要其固件时使用,否则它不能工作(这迫使驱动没有内置),而前者可以在我们的设备即使没有任何固件也可以部分使用时使用,并且它可以在设备初始化后加载(这去除了强制内置形式)。

步骤 2 中,在读取firmware属性(保存固件文件名)后,我们检查设备是否与ldddc,chrdev-fw_waitldddc,chrdev-fw_nowait设备兼容,然后在注册新设备之前,我们调用适当的固件加载功能。

步骤 3 、中,chrdev_load_fw_wait()函数以<name>-<version>.bin形式建立文件名,然后调用名为request_firmware()的有效固件加载函数。作为响应,该函数可能会返回一个错误,该错误会在驱动加载过程中导致错误,或者它可以返回一个适当的结构,该结构将固件保存到具有long fw->size大小字节的buffer fw->data指针中。dump_data()函数只是通过将固件数据打印到内核消息中来转储固件数据,但是release_firmware()函数很重要,必须调用它来通知内核我们已经读取了所有数据并完成了它,然后它才能释放资源。

另一方面,如果我们在设备树中指定ldddc,chrdev-fw_nowait设备,那么将调用chrdev_load_fw_nowait()函数。这个函数的操作方式和以前类似,但最后它调用request_firmware_nowait(),其工作方式类似于request_firmware()。但是,如果固件不是直接从文件系统加载的,它会执行回退过程,这涉及到固件加载程序的用户助手。这个特殊的助手向udev工具(或类似工具)发送一个 uevent 消息,这将导致自动固件加载,或者在 sysfs 中创建一个条目,用户可以使用它来手动加载内核。

chrdev_load_fw_nowait()功能具有以下主体:

static int chrdev_load_fw_nowait(struct device *dev, const char *file)
{
    char fw_name[FIRMWARE_NLEN];
    int ret;

    /* Compose firmware filename */
    if (strlen(file) > (128 - 6 - sizeof(FIRMWARE_VER)))
        return -EINVAL;
    sprintf(fw_name, "%s-%s.bin", file, FIRMWARE_VER);

    /* Do the firmware request */
    ret = request_firmware_nowait(THIS_MODULE, false, fw_name, dev,
            GFP_KERNEL, dev, chrdev_fw_cb);
    if (ret) {
        dev_err(dev,
            "unable to register call back for firmware loading\n");
        return ret;
    } 

    return 0;
}

request_firmware_nowait()request_firmware()之间的一些重要区别在于,前者定义了一个回调函数,每当从用户空间实际加载固件时都会调用该函数,并且它有一个布尔值作为第二个参数,该参数可用于要求内核向用户空间发送或不发送 uevent 消息。通过使用一个值,我们实现了类似于request_firmware()的功能,而如果我们指定了一个错误的值(如我们的情况),我们会强制手动加载固件。

然后,当用户空间进程采取所需的步骤来加载所需的固件时,使用回调函数,我们可以实际加载固件数据,如下例所示:

static void chrdev_fw_cb(const struct firmware *fw, void *context)
{
    struct device *dev = context;

    dev_info(dev, "firmware callback executed!\n");
    if (!fw) {
        dev_err(dev, "unable to load firmware\n");
        return; 
    } 

    dump_data(fw->data, fw->size);

    /* Firmware data has been read, now we can release it */
    release_firmware(fw);
}

在这个函数中,我们实际上采取了与之前相同的步骤来转储内核消息中的固件数据。

还有更多

让我们验证一下这个食谱中的每样东西是如何工作的。作为第一步,让我们尝试使用ldddc,chrdev-fw_wait设备,它使用request_firmware()功能;我们需要设备树中的下一个条目:

--- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts
+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts
@@ -41,6 +41,11 @@
              3300000 0x0>;
          enable-active-high;
      };
+
+     chrdev {
+         compatible = "ldddc,chrdev-fw_wait";
+         firmware = "chrdev-wait";
+     };
 };

 /* J9 */

然后我们必须编译代码,我们可以通过简单地将新的chrdev-fw.c文件添加到makefile中来做到这一点,如下所示:

--- a/chapter_4/chrdev/Makefile
+++ b/chapter_4/chrdev/Makefile
@@ -6,7 +6,7 @@ ARCH ?= arm64
 CROSS_COMPILE ?= aarch64-linux-gnu-

 obj-m = chrdev.o
-obj-m += chrdev-req.o
+obj-m += chrdev-fw.o

 all: modules

一旦我们在 ESPRESSObin 的文件系统中有了新模块,我们可以尝试将它们插入内核,如下所示:

# insmod chrdev.ko 
chrdev: loading out-of-tree module taints kernel.
chrdev:chrdev_init: got major 239
# insmod chrdev-fw.ko 
chrdev-fw chrdev: Direct firmware load for chrdev-wait-1.0.0.bin 
failed with error -2
chrdev-fw chrdev: Falling back to syfs fallback for: chrdev-wait-1.0.0.bin
chrdev-fw chrdev: unable to load firmware
chrdev-fw: probe of chrdev failed with error -11

正如我们所看到的,内核试图加载chrdev-wait-1.0.0.bin文件,但是它找不到它,因为它根本不存在于文件系统中;然后,内核转到 sysfs 回退,但是由于它再次失败,我们得到一个错误,驱动加载也失败了。

为了得到肯定的结果,我们必须在其中一个搜索路径中添加一个名为chrdev-wait-1.0.0.bin的文件;例如,我们可以将其放入/lib/firmware/中,如下例所示:

# echo "THIS IS A DUMMY FIRMWARE FOR CHRDEV DEVICE" > \
 /lib/firmware/chrdev-wait-1.0.0.bin

If the /lib/firmware directory doesn't exist, we can just create it using the mkdir /lib/firmware command.

现在,我们可以按如下方式重新加载我们的chrdev-fw.ko模块:

# rmmod chrdev-fw 
# insmod chrdev-fw.ko 
chrdev_fw:dump_data: 54[T] 48[H] 49[I] 53[S] 20[ ] 49[I] 53[S] 20[ ] 
chrdev_fw:dump_data: 41[A] 20[ ] 44[D] 55[U] 4d[M] 4d[M] 59[Y] 20[ ] 
chrdev_fw:dump_data: 46[F] 49[I] 52[R] 4d[M] 57[W] 41[A] 52[R] 45[E] 
chrdev_fw:dump_data: 20[ ] 46[F] 4f[O] 52[R] 20[ ] 43[C] 48[H] 52[R] 
chrdev_fw:dump_data: 44[D] 45[E] 56[V] 20[ ] 44[D] 45[E] 56[V] 49[I] 
chrdev_fw:dump_data: 43[C] 45[E] 0a[-] 
chrdev chrdev-fw@0: chrdev chrdev-fw with id 0 added

完美!现在固件已经按照要求加载,并且chrdev设备已经正确创建。

现在,我们可以尝试使用第二个设备,方法是如下修改设备树,然后使用新的 DTB 文件重新启动 ESPRESSObin:

--- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts
+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts
@@ -41,6 +41,11 @@
              3300000 0x0>;
          enable-active-high;
      };
+
+     chrdev {
+         compatible = "ldddc,chrdev-fw_nowait";
+         firmware = "chrdev-nowait";
+     };
 };

 /* J9 */

有了这些新的配置设置,如果我们尝试加载chrdev模块,我们会得到以下消息:

# insmod chrdev.ko 
chrdev: loading out-of-tree module taints kernel.
chrdev:chrdev_init: got major 239
# insmod chrdev-fw.ko 
chrdev-fw chrdev: Direct firmware load for chrdev-nowait-1.0.0.bin failed with error -2
chrdev-fw chrdev: Falling back to syfs fallback for: chrdev-nowait-1.0.0.bin
chrdev chrdev-fw@0: chrdev chrdev-fw with id 0 added

这一次,内核还是尝试直接从文件系统加载固件,但是失败了,因为不存在名为chrdev-nowait-1.0.0.bin的文件;然后,它会返回到回退固件加载器用户助手,我们已经强制进入手动模式。然而,驱动的探测功能成功注册了我们的chrdev驱动,即使尚未加载固件,该驱动现在也完全正常工作。

要手动加载固件,我们可以在/sys/class/firmware/目录中使用特殊的 sysfs 条目,如下所示:

# ls /sys/class/firmware/
chrdev-nowait-1.0.0.bin  timeout

chrdev-nowait-1.0.0.bin目录被称为作为fw_name参数传递给request_firmware_nowait()函数的字符串,在它里面,我们可以找到以下文件:

# ls /sys/class/firmware/chrdev-nowait-1.0.0.bin
data  device  loading  power  subsystem  uevent

现在,自动加载固件所需的步骤如下:

# echo 1 > /sys/class/firmware/chrdev-nowait-1.0.0.bin/loading 
# echo "THIS IS A DUMMY FIRMWARE" > /sys/class/firmware/chrdev-nowait-1.0.0.bin/data 
# echo 0 > /sys/class/firmware/chrdev-nowait-1.0.0.bin/loading
chrdev-fw chrdev: firmware callback executed!
chrdev_fw:dump_data: 54[T] 48[H] 49[I] 53[S] 20[ ] 49[I] 53[S] 20[ ] 
chrdev_fw:dump_data: 41[A] 20[ ] 44[D] 55[U] 4d[M] 4d[M] 59[Y] 20[ ] 
chrdev_fw:dump_data: 46[F] 49[I] 52[R] 4d[M] 57[W] 41[A] 52[R] 45[E] 
chrdev_fw:dump_data: 0a[-] 

我们通过将1写入loading文件开始下载程序,然后我们必须将所有固件数据复制到data文件中;然后我们通过在loading文件中写入0来完成下载。一旦我们这样做了,内核就会调用我们的驱动回调,固件就会被加载。

请参见

为特定外围设备配置 CPU 引脚

作为设备驱动开发人员,这项任务非常重要,因为为了能够与外部设备(或内部设备,但有外部信号线)进行通信,我们必须确保每个中央处理器引脚都经过正确配置,能够与这些外部信号进行通信。在本食谱中,我们将了解如何使用设备树来配置 CPU 引脚。

怎么做...

举个简单的例子,让我们尝试修改 ESPRESSObin 的引脚配置。

  1. 首先,我们应该通过查看/sys/bus/platform/drivers/mvebu-uart/目录中的 sysfs 来看一下当前的配置,在这里我们验证当前只启用了一个 UART:
# ls /sys/bus/platform/drivers/mvebu-uart/
d0012000.serial  uevent
# ls /sys/bus/platform/drivers/mvebu-uart/d0012000.serial/tty/
ttyMV0

然后mvebu-uart驱动管理d0012000.serial设备,可以使用/dev/ttyMV0文件访问。我们还可以通过查看 debugfs 中的/sys/kernel/debug/pinctrl/d0013800.pinctrl-armada_37xx-pinctrl/pinmux-pins文件来验证 CPU 的引脚是如何配置的,我们可以看到只有uart1组被启用:

# cat /sys/kernel/debug/pinctrl/d0013800.pinctrl-armada_37xx-p
inctrl/pinmux-pins 
Pinmux settings per pin
Format: pin (name): mux_owner gpio_owner hog?
pin 0 (GPIO1-0): (MUX UNCLAIMED) (GPIO UNCLAIMED)
pin 1 (GPIO1-1): (MUX UNCLAIMED) (GPIO UNCLAIMED)
pin 2 (GPIO1-2): (MUX UNCLAIMED) (GPIO UNCLAIMED)
pin 3 (GPIO1-3): (MUX UNCLAIMED) GPIO1:479
pin 4 (GPIO1-4): (MUX UNCLAIMED) GPIO1:480
pin 5 (GPIO1-5): (MUX UNCLAIMED) (GPIO UNCLAIMED)
...
pin 24 (GPIO1-24): (MUX UNCLAIMED) (GPIO UNCLAIMED)
pin 25 (GPIO1-25): d0012000.serial (GPIO UNCLAIMED) function uart group uart1
pin 26 (GPIO1-26): d0012000.serial (GPIO UNCLAIMED) function uart group uart1
pin 27 (GPIO1-27): (MUX UNCLAIMED) (GPIO UNCLAIMED)
...

For further information about debugfs, see https://en.wikipedia.org/wiki/Debugfs and then following some external links.

  1. 然后,我们应该尝试修改 ESPRESSObin 的 DTS 文件,以启用另一个名为uart1的 UART 设备,其自身的引脚在uart2_pins组中定义如下:
--- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts
+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts
@@ -97,6 +97,13 @@
    status = "okay";
 };

+/* Exported on extension connector P9 at pins 24(UA2_TXD) and 26(UA2_RXD) */
+&uart1 {
+   pinctrl-names = "default";
+   pinctrl-0 = <&uart2_pins>;
+   status = "okay";
+};
+
 /*
  * Connector J17 and J18 expose a number of different features. Some pins are
  * multiplexed. This is the case for instance for the following features:

该引脚组在linux/arch/arm64/boot/dts/marvell/armada-37xx.dtsi文件中定义如下:

    uart2_pins: uart2-pins {
        groups = "uart2";
        function = "uart";
    };

它是如何工作的...

让我们通过测试 pinctrl 修改来检查这是如何工作的。为此,我们必须像往常一样重新生成 ESPRESSObin 的 DTB 文件,并重新启动系统。如果一切正常,我们现在应该有两个 UART 设备,如下所示:

# ls /sys/bus/platform/drivers/mvebu-uart/
d0012000.serial d0012200.serial uevent
# ls /sys/bus/platform/drivers/mvebu-uart/d0012200.serial/tty/
ttyMV1

此外,如果我们再看一下/sys/kernel/debug/pinctrl/d0013800.pinctrl-armada_37xx-pinctrl/pinmux-pins文件,我们会发现这次uart2引脚组已经添加,然后我们的新串行端口在扩展连接器 P9 上的引脚 24 和 26 处可用。

请参见