Open Firmware 设备树单元测试

作者:Gaurav Minocha <gaurav.minocha.os@gmail.com>

1. 简介

本文档解释了执行 OF 单元测试所需的测试数据是如何动态附加到活动树上的,且与机器的体系结构无关。

建议在继续阅读之前先阅读以下文档。

  1. Linux 与设备树

  2. http://www.devicetree.org/Device_Tree_Usage

OF 自测试(Selftest)旨在测试提供给设备驱动开发者的接口(include/linux/of.h),以便从未展平的设备树数据结构中获取设备信息等。大多数设备驱动程序在各种用例中都会使用此接口。

2. 详细输出(EXPECT)

如果单元测试检测到问题,它将在控制台上打印警告或错误消息。由于故意设置了不正确的单元测试数据,单元测试还会触发来自其他内核代码的警告和错误消息。这导致人们产生困惑:触发的消息是测试的预期结果,还是与单元测试无关的真实问题。

单元测试中已添加了 ‘EXPECT : text’(开始)和 ‘EXPECT / : text’(结束)消息,用于报告预期的警告或错误。开始消息在触发警告或错误之前打印,结束消息在触发警告或错误之后打印。

EXPECT 消息会导致控制台产生大量难以阅读的冗余信息。为此创建了脚本 scripts/dtc/of_unittest_expect,用于过滤这些冗余信息,并高亮显示触发的警告/错误与预期警告/错误之间的不匹配项。有关更多信息,请参阅 ‘scripts/dtc/of_unittest_expect --help’。

3. 测试数据(Test-data)

设备树源文件(drivers/of/unittest-data/testcases.dtso)包含执行 drivers/of/unittest.c 中自动化单元测试所需的测试数据。请查看该文件夹

drivers/of/unittest-data/tests-*.dtsi

以获取包含在 testcases.dtso 中的设备树源包含文件(.dtsi)。

当内核在启用 CONFIG_OF_UNITTEST 的情况下构建时,将使用以下 make 规则

$(obj)/%.dtbo: $(src)/%.dtso $(DTC) FORCE
        $(call if_changed_dep,dtc)

将 DT 源文件(testcases.dtso)编译为二进制 blob(testcases.dtbo),也称为展平 DT。

此后,使用以下规则将上述二进制 blob 包装为汇编文件(testcases.dtbo.S)

$(obj)/%.dtbo.S: $(obj)/%.dtbo FORCE
        $(call if_changed,wrap_S_dtb)

该汇编文件被编译为目标文件(testcases.dtbo.o),并被链接到内核镜像中。

3.1. 添加测试数据

未展平的设备树结构

未展平的设备树由以树状结构连接的 device_node 组成,如下所述

// following struct members are used to construct the tree
struct device_node {
    ...
    struct  device_node *parent;
    struct  device_node *child;
    struct  device_node *sibling;
    ...
};

图 1 描述了仅考虑子指针和兄弟指针的机器未展平设备树的通用结构。还存在另一个指针 *parent,用于反向遍历树。因此,在特定层级上,子节点和所有兄弟节点都具有指向公共节点的 parent 指针(例如,child1、sibling2、sibling3、sibling4 的 parent 指向根节点)

root ('/')
|
child1 -> sibling2 -> sibling3 -> sibling4 -> null
|         |           |           |
|         |           |          null
|         |           |
|         |        child31 -> sibling32 -> null
|         |           |          |
|         |          null       null
|         |
|      child21 -> sibling22 -> sibling23 -> null
|         |          |            |
|        null       null         null
|
child11 -> sibling12 -> sibling13 -> sibling14 -> null
|           |           |            |
|           |           |           null
|           |           |
null        null       child131 -> null
                        |
                        null

图 1:未展平设备树的通用结构

在执行 OF 单元测试之前,需要将测试数据附加到机器的设备树(如果存在)上。因此,当调用 selftest_data_add() 时,它首先读取通过以下内核符号链接到内核镜像中的展平设备树数据

__dtb_testcases_begin - address marking the start of test data blob
__dtb_testcases_end   - address marking the end of test data blob

其次,它调用 of_fdt_unflatten_tree() 来反展平(unflatten)展平的二进制 blob。最后,如果机器的设备树(即活动树)存在,它会将反展平的测试数据树附加到活动树上,否则它将自身附加为活动设备树。

attach_node_and_children() 使用 of_attach_node() 将节点附加到活动树中,具体解释如下。为了说明这一点,将图 2 中描述的测试数据树附加到图 1 中描述的活动树

root ('/')
    |
testcase-data
    |
test-child0 -> test-sibling1 -> test-sibling2 -> test-sibling3 -> null
    |               |                |                |
test-child01      null             null             null

图 2:要附加到活动树的示例测试数据树。

根据上述场景,活动树已经存在,因此不需要附加根节点(‘/’)。所有其他节点都是通过对每个节点调用 of_attach_node() 来附加的。

在函数 of_attach_node() 中,新节点被附加为活动树中给定父节点的子节点。但是,如果父节点已经有一个子节点,则新节点会替换当前的子节点并将其转变为自己的兄弟节点。因此,当测试用例数据节点附加到上面的活动树(图 1)时,最终的结构如图 3 所示

root ('/')
|
testcase-data -> child1 -> sibling2 -> sibling3 -> sibling4 -> null
|               |          |           |           |
(...)             |          |           |          null
                |          |         child31 -> sibling32 -> null
                |          |           |           |
                |          |          null        null
                |          |
                |        child21 -> sibling22 -> sibling23 -> null
                |          |           |            |
                |         null        null         null
                |
                child11 -> sibling12 -> sibling13 -> sibling14 -> null
                |          |            |            |
                null       null          |           null
                                        |
                                        child131 -> null
                                        |
                                        null
-----------------------------------------------------------------------

root ('/')
|
testcase-data -> child1 -> sibling2 -> sibling3 -> sibling4 -> null
|               |          |           |           |
|             (...)      (...)       (...)        null
|
test-sibling3 -> test-sibling2 -> test-sibling1 -> test-child0 -> null
|                |                   |                |
null             null                null         test-child01

图 3:附加测试用例数据后的活动设备树结构。

敏锐的读者可能已经注意到,与先前的结构(图 2)相比,test-child0 节点变成了最后一个兄弟节点。在附加了第一个 test-child0 之后,又附加了 test-sibling1,这会把子节点(即 test-child0)推成兄弟节点,并使自身成为子节点,如上所述。

如果发现重复的节点(即活动树中已经存在具有相同 full_name 属性的节点),则不会附加该节点,而是通过调用函数 update_node_properties() 将其属性更新到活动树的节点中。

3.2. 移除测试数据

一旦测试用例执行完成,就会调用 selftest_data_remove 以移除最初附加的设备节点(首先分离叶节点,然后向上移除父节点,最后移除整棵树)。selftest_data_remove() 调用 detach_node_and_children(),后者使用 of_detach_node() 从活动设备树中分离节点。

为了分离节点,of_detach_node() 会视情况将给定节点父节点的子指针更新为其兄弟节点,或者将前一个兄弟节点连接到给定节点的兄弟节点。就是这样 :)