设备树编译机制迁移实践
难度级别:🟡 中级
目标读者:驱动开发者、项目维护者
阅读时间: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) -E | gcc -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-driver3. 验证编译结果
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.dtb4. 部署到板子
bash
./scripts/driver_helper/deploy_driver.sh out/driver_artifacts/example-driver/alpha-board --target=tftp5. 在板子上验证
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总结
我们的迁移实现了以下目标:
✅ 成功迁移的功能
完整的include支持
- 支持内核include语法
- 支持dt-bindings
- 支持条件编译
简化的编译流程
- 去除Makefile依赖
- 使用Bash脚本
- 更容易理解和维护
保持兼容性
- 与内核设备树完全兼容
- 可以直接使用内核的dtsi文件
- 生成的dtb文件与内核一致
📊 性能对比
| 指标 | 内核方式 | 我们的方式 |
|---|---|---|
| 编译时间 | ~2秒(首次) | ~0.5秒 |
| 依赖检查 | 完整 | 无 |
| 灵活性 | 低 | 高 |
| 调试难度 | 高 | 低 |
| 学习曲线 | 陡峭 | 平缓 |
🎯 适用场景
我们的方式适合:
- ✅ 驱动开发项目
- ✅ 快速迭代开发
- ✅ 小型设备树项目
- ✅ 需要独立编译的场景
内核方式适合:
- ✅ 大型内核开发
- ✅ 需要增量编译
- ✅ 复杂的设备树依赖
- ✅ 并行编译需求
🔧 未来改进方向
添加依赖检查(可选):
bash# 简单的mtime检查 if [[ "$dts_file" -nt "$dtb_file" ]]; then # 需要重新编译 fi支持更多架构:
- 添加ARM64支持
- 添加RISC-V支持
并行编译(可选):
bash# 使用 GNU parallel ls *.dts | parallel -j 4 'dtc -I dts -O dtb -o {.dtb} {}'
扩展阅读
- 内核设备树编译机制 - 深入理解内核实现
- 驱动基建文档 - 完整的驱动开发系统文档
- example_driver验证 - 验证步骤详解
下一步:阅读驱动脚本使用指南,了解如何在实际开发中使用设备树编译系统。