Skip to content

设备树编译机制迁移实践

难度级别:🟡 中级

目标读者:驱动开发者、项目维护者

阅读时间:10-15分钟

前置知识:阅读过内核设备树编译机制,了解Bash脚本

目录

为什么需要迁移

内核编译方式的限制

内核的设备树编译依赖于完整的内核构建系统:

bash
# 必须在内核源码树中
cd linux_mainline/
make arch/arm/boot/dts/board.dtb

# 依赖:
# - 内核配置(.config)
# - 内核构建基础设施
# - 大量的Makefile规则

问题

  • ❌ 无法独立编译单个设备的设备树
  • ❌ 需要完整的内核源码树
  • ❌ 编译过程复杂,不易调试
  • ❌ 不适合快速迭代开发

独立项目的需求

在驱动开发项目中,我们需要:

  • ✅ 快速编译修改后的设备树
  • ✅ 不依赖完整内核构建系统
  • ✅ 支持内核include语法
  • ✅ 易于集成到CI/CD流程

目标:复制内核的两阶段编译流程,但去除对内核构建系统的依赖。

迁移的挑战

挑战1:include路径处理

内核使用符号链接和复杂的Makefile变量来管理include路径:

makefile
DTC_INCLUDE := $(srctree)/scripts/dtc/include-prefixes

问题:在独立项目中,我们无法访问这些路径。

挑战2:预处理参数

内核使用复杂的gcc参数组合:

makefile
dtc_cpp_flags = -Wp,-MMD,$(depfile).pre.tmp -nostdinc \
                -I $(DTC_INCLUDE) -undef -D__DTS__

问题:需要理解每个参数的作用,并正确设置。

挑战3:依赖管理

内核使用Make的依赖追踪机制:

makefile
$(obj)/%.dtb: $(obj)/%.dts $(DTC) $(DT_TMP_SCHEMA) FORCE
	$(call if_changed_dep,dtc)

问题:在Bash脚本中需要重新实现依赖检查。

我们的实现

实现位置

文件scripts/lib/driver_buildlib.sh(build_device_tree函数)

核心代码

bash
build_device_tree() {
    local driver_dir="$1"
    local output_dir="$2"
    local kernel_type="${3:-$DEFAULT_KERNEL_TYPE}"

    # ... 前面的代码省略 ...

    # 按内核方式:先用gcc预处理,再用dtc编译
    local gcc_args=(
        -E -nostdinc -P -x assembler-with-cpp
        -I "${kdir}/arch/arm/boot/dts"
        -I "${kdir}/arch/arm/boot/dts/nxp/imx"
        -I "${kdir}/include"
        -I "$board_dts_dir"
        -undef -D__DTS__
    )

    # 创建临时文件存储预处理结果
    local dtc_tmp="/tmp/dtc-$(basename "$dts_file" .dts).tmp"

    # 先用gcc预处理,再用dtc编译
    local dtc_output
    dtc_output=$(gcc "${gcc_args[@]}" -o "$dtc_tmp" "$dts_file" 2>&1 && \
                dtc -I dts -O dtb "${include_args_array[@]}" -o "$dtb_file" "$dtc_tmp" 2>&1)
    local dtc_status=$?

    # 清理临时文件
    rm -f "$dtc_tmp"
}

关键改进点

1. 直接指定include路径

bash
local gcc_args=(
    -E -nostdinc -P -x assembler-with-cpp
    -I "${kdir}/arch/arm/boot/dts"           # 架构设备树
    -I "${kdir}/arch/arm/boot/dts/nxp/imx"   # SoC特定设备树
    -I "${kdir}/include"                     # dt-bindings
    -I "$board_dts_dir"                      # 项目板级设备树
    -undef -D__DTS__
)

对比内核

  • ❌ 内核:使用符号链接(include-prefixes)
  • ✅ 我们:直接指定绝对路径

优势

  • ✅ 更简单直观
  • ✅ 不依赖符号链接
  • ✅ 易于调试

2. 使用gcc代替cpp

bash
gcc "${gcc_args[@]}" -o "$dtc_tmp" "$dts_file"

对比内核

  • 内核:$(HOSTCC) -E(实际上是gcc -E)
  • 我们:直接使用gcc

优势

  • ✅ 命令更简洁
  • ✅ 兼容性更好(某些系统cpp命令不同)

3. 添加 -P 选项

bash
-E -nostdinc -P -x assembler-with-cpp

-P选项的作用:删除预处理输出中的行号信息。

为什么添加?

  • 设备树不需要行号信息
  • 减少输出文件大小
  • 避免dtc混淆

4. 简化错误处理

bash
dtc_output=$(gcc ... && dtc ... 2>&1)
local dtc_status=$?

对比内核

  • 内核:分离的错误检查和日志
  • 我们:捕获所有输出,统一处理

优势

  • ✅ 代码更简单
  • ✅ 错误信息更完整
  • ✅ 易于调试

对比分析

编译流程对比

阶段内核方式我们的方式差异
预处理$(HOSTCC) -Egcc -E -P添加-P删除行号
include路径符号链接直接路径更简单直接
依赖管理Makefile依赖无依赖检查牺牲增量编译
错误处理分离检查统一捕获更简洁

功能对比

功能内核我们备注
支持#include完全兼容
支持宏定义完全兼容
支持条件编译完全兼容
dt-bindings完全兼容
增量编译不需要(小项目)
并行编译不需要(少量文件)
符号生成不需要(无overlay)

复杂度对比

内核方式:
  Makefile规则 → 依赖生成 → 并行编译 → 链接
  复杂度:🔴🔴🔴🔴🔴

我们的方式:
  Bash脚本 → gcc预处理 → dtc编译 → 完成
  复杂度:🟢🟢🟢

实战验证

验证步骤

1. 创建带include的设备树

文件driver/device_tree/alpha-board/example-driver/imx6ull-aes-example-driver.dts

c
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/dts-v1/;

#include "imx6ull.dtsi"
#include "imx6ull-aes.dtsi"

/ {
    model = "Awesome Embedded Studio IMX6ULL Example Driver";
    compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

    fake-i2c@50 {
        compatible = "fake,i2c-device";
        reg = <0x50>;
        status = "disabled";
    };
};

2. 编译设备树

bash
./scripts/driver_helper/build_driver.sh example-driver

3. 验证编译结果

bash
# 查看编译产物大小
ls -lh out/driver_artifacts/example-driver/alpha-board/*.dtb

# 反编译查看内容
dtc -I dtb -O dts out/driver_artifacts/example-driver/alpha-board/imx6ull-aes-example-driver.dtb

4. 部署到板子

bash
./scripts/driver_helper/deploy_driver.sh out/driver_artifacts/example-driver/alpha-board --target=tftp

5. 在板子上验证

bash
# 查看设备树型号
cat /sys/firmware/devicetree/base/model

# 查看节点是否存在
ls -la /sys/firmware/devicetree/base/fake-i2c*

# 查看节点属性
cat /sys/firmware/devicetree/base/fake-i2c@50/compatible

预期结果

✅ 编译成功,无错误
✅ 设备树大小合理(~35K,包含完整内核结构)
✅ 板子上可以找到所有fake节点
✅ 节点属性正确(compatible、reg等)

常见问题

Q1: 为什么不使用内核的构建系统?

A: 内核构建系统虽然强大,但对于驱动开发来说:

  • ❌ 过于复杂
  • ❌ 需要完整配置
  • ❌ 编译时间长
  • ❌ 不适合快速迭代

我们的方式:

  • ✅ 简单直接
  • ✅ 无需配置
  • ✅ 编译快速
  • ✅ 易于调试

Q2: include路径是否需要手动更新?

A: 不需要。我们动态检测内核源码位置:

bash
local kdir="${DRIVER_PROJECT_ROOT}/third_party/${kernel_name}"

只要内核在标准位置,include路径会自动正确。

Q3: 如何支持新的架构?

A: 添加对应的include路径:

bash
-I "${kdir}/arch/<新架构>/boot/dts"

Q4: 为什么不需要增量编译?

A: 设备树文件的特点:

  • 数量少(每个驱动通常1-2个)
  • 修改不频繁
  • 编译很快(<1秒)

增量编译的收益很小,不值得增加复杂度。

Q5: 如何调试编译错误?

A: 查看完整错误信息:

bash
# 脚本会输出完整的gcc和dtc错误信息
./scripts/driver_helper/build_driver.sh example-driver

# 如果需要更详细的信息,可以手动运行:
gcc -E -nostdinc -P -x assembler-with-cpp \
    -I third_party/linux_mainline/arch/arm/boot/dts \
    -I third_party/linux_mainline/include \
    -I driver/device_tree/alpha-board/linux \
    -undef -D__DTS__ \
    -o /tmp/test.dts.tmp \
    driver/device_tree/alpha-board/example-driver/test.dts

# 然后用dtc编译预处理后的文件:
dtc -I dts -O dtb -o test.dtb /tmp/test.dts.tmp

总结

我们的迁移实现了以下目标:

✅ 成功迁移的功能

  1. 完整的include支持

    • 支持内核include语法
    • 支持dt-bindings
    • 支持条件编译
  2. 简化的编译流程

    • 去除Makefile依赖
    • 使用Bash脚本
    • 更容易理解和维护
  3. 保持兼容性

    • 与内核设备树完全兼容
    • 可以直接使用内核的dtsi文件
    • 生成的dtb文件与内核一致

📊 性能对比

指标内核方式我们的方式
编译时间~2秒(首次)~0.5秒
依赖检查完整
灵活性
调试难度
学习曲线陡峭平缓

🎯 适用场景

我们的方式适合

  • ✅ 驱动开发项目
  • ✅ 快速迭代开发
  • ✅ 小型设备树项目
  • ✅ 需要独立编译的场景

内核方式适合

  • ✅ 大型内核开发
  • ✅ 需要增量编译
  • ✅ 复杂的设备树依赖
  • ✅ 并行编译需求

🔧 未来改进方向

  1. 添加依赖检查(可选):

    bash
    # 简单的mtime检查
    if [[ "$dts_file" -nt "$dtb_file" ]]; then
        # 需要重新编译
    fi
  2. 支持更多架构

    • 添加ARM64支持
    • 添加RISC-V支持
  3. 并行编译(可选):

    bash
    # 使用 GNU parallel
    ls *.dts | parallel -j 4 'dtc -I dts -O dtb -o {.dtb} {}'

扩展阅读


下一步:阅读驱动脚本使用指南,了解如何在实际开发中使用设备树编译系统。

Built with VitePress