第5篇:给 Linux 内核写一份"硬件说明书":设备树 DTS 入门

2 阅读6分钟

本文是《从零到一:把一块 RK3568 开发板移植到 OpenHarmony 6.1 全记录》系列的第 5 篇。设备树是嵌入式移植的必修课,这篇从"为什么需要"讲到"怎么读",配合真实代码片段。

上一篇文章讲了开机的 5 棒接力。第④棒 Linux 内核要接管硬件,但内核怎么知道"这块板的屏幕接在哪个引脚"?答案就是今天的主角:设备树(Device Tree)

一、内核为什么需要一份"说明书"

内核是通用的,板子是千差万别的。怎么让同一份内核适配不同的板子?历史上只有两条路:

  • 老办法(硬编码):每块板的硬件信息写死在 C 代码里 → 换块板就要改代码重编内核,几百块板就是几百份内核,灾难
  • 现代办法(设备树):硬件信息写在一个独立的数据文件里,内核启动时读取它。同一份内核 + 不同的说明书 = 适配不同的板子

这个说明书就是 DTS(Device Tree Source,设备树源文件),编译后是二进制 DTB,内核读的是 DTB。

类比:内核是"万用遥控器",DTS 是"这台设备怎么接线"的说明书。换设备只换说明书,不用换遥控器。

二、DTS 长什么样:先读一段真实的

这是我在 CHESHIT10 板移植中写的设备树片段(以太网部分,已简化):

&gmac0 {                                /* 主芯片的第 0 个网卡控制器,补充说明 */
    phy-mode = "rmii";                  /* 用 RMII 这种接线模式 */
    snps,reset-gpio = <&gpio0 RK_PD5 GPIO_ACTIVE_LOW>;
                                        /* 复位引脚接 gpio0 的 D5 脚,低电平有效 */
    phy-handle = <&rmii_phy0>;          /* 配套的网卡芯片是下面定义的 rmii_phy0 */
    status = "okay";                    /* 启用这个设备 */
};

翻译成人话:"主芯片的网卡控制器 gmac0:用 RMII 模式接线,复位信号接在 gpio0_D5 引脚(低电平触发),它管着的网卡芯片叫 rmii_phy0,启用它。"

设备树就是几百条这样的条目组成的树状结构(所以叫"树")。

10 分钟语法速成

节点名: 标签 {            /* 一个"条目",{ } 里写属性 */
    属性名 = <值1 值2>;    /* 尖括号:数字 */
    属性名 = "字符串";     /* 引号:字符串 */
    属性名 = [十六进制];   /* 方括号:原始字节(屏幕初始化序列就是这种) */
};

三个最常用的写法:

  • &gmac0 { ... } —— 给别处定义过的节点补充/覆盖属性(这叫"引用")
  • status = "okay" / "disabled" —— 启用/禁用设备
  • #include "xxx.dtsi" —— 引用另一个文件,把说明书拆成多个章节(像拼图)

三、一份真实板子的说明书长什么样

我写的 CHESHIT10 说明书结构(真实文件):

rk3568-cheshit10-7inch.dts        ← 总目录(入口):包含下面所有章节
├── rk3568.dtsi               ← 第1章:芯片说明书(RK3568 有哪些控制器)   [官方自带]
├── rk3568-linux.dtsi         ← 第2章:开机参数、调试串口                [官方自带]
├── rk3568-cheshit10.dtsi         ← 第3章:【我写的】CHESHIT10 这块板的说明书
│   ├── #include "rk3568-toybrick-x0.dtsi"  ← 先引用官方参考板当底稿
│   └── 然后覆盖 CHESHIT10 不一样的地方:
│       ├── 以太网:官方板 RGMII 接线 → CHESHIT10 改 RMII(网卡芯片不同)
│       ├── 屏幕通路:官方板主屏接 DSI0 → CHESHIT10 接 DSI1
│       ├── 触摸:I2C3 总线上挂 4 个候选触摸芯片
│       ├── 摄像头:官方传感器删掉,换成 GC2053+GC2093
│       ├── USB:5V 供电引脚、Hub 复位引脚改到 CHESHIT10 走线
│       └── RTC 时钟芯片:HYM8563 挂 I2C1
├── panel-cheshit10-7inch-jh071.dtsi  ← 第4章:【我写的】屏幕说明书
└── touch-cheshit10.dtsi              ← 第5章:【我写的】触摸芯片说明书

为什么分层? 因为这块板要支持多块屏、多颗触摸芯片(第 9 篇变体框架):换屏只换第 4 章,换触摸只改第 5 章,公共部分(第 3 章)不动。这就是工程上"高内聚、低耦合"的落地。

四、屏幕的"密语":初始化序列

屏幕说明书里最不容错的部分是这段(真实项目片段):

panel-init-sequence = [
    15 00 02 30 00
    39 00 05 F7 49 61 02 00
    ...(几十行)...
    05 78 01 11      /* 发送命令 0x11(屏幕启动),等 120ms */
];

这段像"咒语"一样的十六进制是什么?屏幕驱动 IC 的初始化指令流。屏幕模组上电后必须按固定顺序收到一串命令才会正常显示,顺序是屏厂给的。格式:[命令类型 延时 长度 命令字节 数据...],例如 05 78 01 11 = "类型05,延时120ms,长度1,发送命令 0x11"。

移植要点:这段咒语必须一字不差地从原厂系统抄过来——写错一个字节,屏幕可能白屏、花屏、不亮。 这是"翻译工作"里最不容错的部分。

还有 timing(时序)参数:

clock-frequency = <51000000>;    /* 像素时钟 51MHz */
hactive = <1024>; vactive = <600>;   /* 分辨率 1024x600 */
hfront-porch = <160>; hsync-len = <24>; hback-porch = <136>;
vfront-porch = <12>;  vsync-len = <2>;  vback-porch = <21>;

这组数字描述"每画一帧画面,信号怎么走",也是屏厂给的,抄错屏幕会闪、偏、黑。

五、写错说明书的代价:三个真实教训

写设备树是精细活,这次移植踩了三个坑,值得每个入门者提前知道:

  1. 删除节点必须放顶层:/delete-node/ &标签; 语句放在 &overlay { } 块内部会直接语法错误(新版 dtc 的严格检查)
  2. 删除前先查引用:被声卡等节点引用的芯片节点,宁可 status="disabled" 也不要删除——删了会留下"悬空引用",编译报错
  3. 改完先用 dtc 单独验证:不要等几小时的内核编译才发现 DTS 有错,用 dtc 工具 30 秒验证一次:
cpp -nostdinc -undef -x assembler-with-cpp \
  -I <头文件目录> -I <dts目录> rk3568-cheshit10-7inch.dts \
  | dtc -I dts -O dtb -o test.dtb -
# 只关注 Error/FATAL;Warning 与官方参考板同级即可接受

写在最后

设备树是嵌入式 Linux/Android/OpenHarmony 通用的技能,学会了三端通吃。入门就一句话:DTS 是硬件的说明书,移植的大部分工作就是在写/翻译这份说明书。

下一篇是实战:我是怎么把 Android 4.19 内核的设备树,翻译成 OpenHarmony 6.6 内核能读的版本的。


📚 本系列文章导航

《从零到一:把一块 RK3568 开发板移植到 OpenHarmony 6.1 全记录》

  1. Ubuntu 24.04 编译 OpenHarmony 6.1:我踩过的 11 个坑
  2. 我把一块跑 Android 11 的开发板,换成了 OpenHarmony
  3. 零基础拆解:一块 RK3568 开发板里有什么
  4. 按下电源后发生了什么?嵌入式开机全流程
  5. 给 Linux 内核写一份"硬件说明书":设备树 DTS 入门
  6. 设备树翻译实战:把 Android 4.19 的 DTS 搬到 OpenHarmony 6.6
  7. OpenHarmony HDF 驱动框架入门:屏幕驱动"参数化"改造
  8. 从零写一个 OpenHarmony 触摸驱动:TW3106 移植实录
  9. 一块板子如何兼容 4 种触摸芯片和 3 种 4G 模组?
  10. 移植全景复盘:从 0 到可烧写镜像,我做了什么