Skip to content

我看我玩STM32就好好的,怎么到你Linux需要设备树? ​

您要是玩过 STM32,这个疑问我太懂了。外设怎么接、寄存器在哪、时钟开多少,这些“板子的事实”在您的工程里都有下落:CubeMX 上点一点生成,再不济翻着手册直接怼寄存器,反正都亲手写进代码了。一份工程对一块板,换板就改工程,一个人这么干天经地义,确实轮不到“设备树”出场。Linux 的处境是反过来的:一份内核要往数不清的板子上跑,板子的事实没法再跟着哪一个人的工程走。这些事实由谁交给内核,就是设备树要回答的问题。空讲没意思,咱们从最小的一件事说起。

咱们从“让串口打出一个字符”说起 ​

好,现在我相信您之前大抵是玩过一些 Linux 开发板的,把板子给盘活了。嗯,大概吧。但是我要是说,我们屏幕上的串口吐出来的东西,其实走了很远。您会感到意外吗?

我用最直白,最不绕弯子,最简洁的话问你:要让日志里的文字从串口输出,需要哪些硬件和代码配合?

相信您专门来了解设备树了,那大抵是理解了 Linux 内核这个基本的概念了,对吧。Linux 内核负责管理机器的资源,串口驱动是内核体系里负责操作串口硬件的代码。我们驱动负责的事情就是知道怎样检查控制器的状态、把待发送的数据写进去。

驱动操作的硬件叫 UART(通用异步收发器)控制器,负责串口数据的发送和接收。要是扯到UART,我们又可以好好唠嗑了不是。所以咱们这里关心发送:驱动把数据交给控制器,控制器将待发送的数据转换成电信号,经引脚和板上的线路送出去。你在板边看到的串口连接器,提供的就是连接这些信号的接口。

你常会在开发板的介绍里看到 SoC(片上系统)这个词。它是一颗集成了 CPU 核心和多种控制器的芯片,里面通常集成了内存控制器、串口控制器等。芯片中的 CPU 核心执行程序,这些控制器负责相应的硬件功能。串口控制器这样的单元也叫外设,它们可以集成在 SoC 的内部。

咱们还得分清 SoC 和整块板:板上通常还有供电电路、连接器和其他芯片,这些元件通过板上的线路连接起来。所以只知道 SoC 的型号,还不能知道这块板的全部硬件配置。

现在咱们知道了串口输出需要驱动操作控制器。接下来的问题是:驱动怎么知道板上的控制器在哪,又该使用哪些资源?

驱动知道“怎么做”,还得知道“对谁做” ​

咱们把串口驱动需要的信息分成两堆,事情就清楚了。咱们把头一堆叫操作规则:往哪个寄存器写什么样的值会触发发送,状态位怎么看。你去看驱动的源码,写的就是它,同类控制器共享同一套。另一堆是实例资源,机器换一台就变一套:寄存器区域从哪个地址开始,输入时钟是多少,板子有没有把它引到连接器上。你翻驱动代码找不到它们,也推不出来。

咱们当场说清“寄存器”这个词,这里说的是驱动的寄存器哈!它是设备暴露给软件的控制或状态单元,软件按硬件手册规定的地址读写它,设备就照着动。它跟 CPU 内部那个“通用寄存器”只是同姓,不是一家。那“往一个地址写值”怎么就能碰到设备?这靠内存映射 I/O:CPU 的地址空间里划出一段,访问它对应的是某个设备的寄存器,而非普通内存。咱们造一个教学假设帮你落地:某 SoC 里,UART 控制器占 0x1000 开始、长度 0x100 字节的一段地址。数字和布局都是咱们编的,不对应任何真实 UART。

现在你把两堆信息放在一起看:驱动带着操作规则,面对的却是“每块板资源都可能不同”的现实。光会说“我会操作串口”,它回答不了“这板上的串口在哪、时钟几何”。谁来填这个缺口?

两块板子:同一份驱动,不同的硬件事实 ​

咱们把缺口做实。假设有板 A 和板 B,差异列成下表。

项目板 A板 B
串口控制器的编程接口某种相同接口与板 A 兼容
寄存器区域起始地址0x10000x2000
寄存器区域长度0x100 字节0x100 字节
控制器输入时钟24 MHz48 MHz
板上是否引出该串口是否

表里的设定是咱们编的教学构造。咱们假设两块板用同系列 SoC 的两个变体,所以控制器地址不同,你别由此推“同一颗 SoC 换块电路板就能改内部地址”。同一 SoC 的两块板,也可能在外接芯片、晶振和供电上各有各的样子。

咱们围着表问三个问题。接口相同,驱动能复用什么?操作规则那一整堆,一份代码就够。地址、时钟、引出与否,谁来告诉驱动?这些是每块板的事实,得从外面递给内核,这正是“硬件描述”要干的活。拿着板 A 的描述喂给板 B 会怎样?驱动可能按 0x1000 去访问,而板 B 的控制器在 0x2000,于是访问到错误的资源,或者初始化失败。咱们只说到这一步:具体怎么坏,取决于实际系统,咱们不替它编结局。

为什么不能让 Linux 自己把设备都找出来 ​

你可能会想,USB 设备插上,系统就能认出来,SoC 里这些控制器怎么就不行?这个问题涉及到枚举,即系统按总线约定,主动询问“总线上挂着谁、你是什么”。

有的总线天生会回答。USB 和 PCI 设计了较完善的发现机制,设备接入时能报上身份。设备树规范拿 PCI 举例:它的架构允许探测挂接的设备,所以你可能不必为单个 PCI 设备写节点,但系统里通常仍要用一个节点描述 PCI 主桥。你看,发现机制和描述可以搭伙,不是二选一。

SoC 内部的控制器,系统就常常问不出来了:芯片把它们接在内部总线上,没有统一的“报到”环节,地址和时钟由芯片设计决定,板上连线又由电路板设计决定。这类信息你问不出来,只能有人告诉它。两个边界咱们说在前头:USB 能发现设备,不等于不需要驱动,“怎么操作”仍是驱动的活。设备树也只是提供硬件描述的一种方式,PC 世界常见的另一种叫 ACPI,咱们不展开比较。

老办法:把板子的说明写进 C 代码 ​

在没有统一描述手段的年代,一部分嵌入式 Linux 平台走板级支持的路子:让系统认识和初始化一块板所需的描述与代码,直接写成 C 编进内核。注意范围,这是“部分嵌入式平台曾常用”,你说不成“Linux 从来都这样”。

咱们看一份真实的板级文件。Linux v3.7 的源码树里,Freescale(后来的 NXP)i.MX51 Babbage 开发板的板级说明,就住在 mach-mx51_babbage.c。咱们摘串口相关的三小段:

c
/* Linux v3.7 arch/arm/mach-imx/mach-mx51_babbage.c,GPL-2.0,摘引有删节 */
static iomux_v3_cfg_t mx51babbage_pads[] = {
	/* UART1 */
	MX51_PAD_UART1_RXD__UART1_RXD,
	MX51_PAD_UART1_TXD__UART1_TXD,
	MX51_PAD_UART1_RTS__UART1_RTS,
	MX51_PAD_UART1_CTS__UART1_CTS,
	/* ……中间还有 UART2、UART3 与其它外设的引脚 */
};

static const struct imxuart_platform_data uart_pdata __initconst = {
	.flags = IMXUART_HAVE_RTSCTS,	/* 这块板的串口带 RTS/CTS 流控 */
};

static void __init mx51_babbage_init(void)
{
	/* ……公共初始化从略 */
	imx51_add_imx_uart(0, &uart_pdata);	/* UART1:带流控 */
	imx51_add_imx_uart(1, NULL);		/* UART2:不带 */
	/* …… */
}

你在这几行里看到的全是“这块板的事实”:哪些引脚接了串口,哪个串口要流控。工程师把 SoC 一侧的公共信息放进共享的 devices-imx51.h,比如各控制器的寄存器地址,板级文件只补板与板的差异。这个分层你记好,待会儿它会在新办法里原样重现。

麻烦也在这里。想支持一块新板,你得往内核源码树里添一个 C 文件,过一遍源码维护和构建流程。v3.7 的 mach-imx 目录下,mach- 开头的板级文件挂了 36 个,结构相似而数字各异,同一类事实散落各处,各自被改、各自漂移。

公道话咱们也要说:老模型同样能共享代码、支持多块板,设备树带来的改善是“把描述从专用代码里分离出来、按数据组织”,并没有从无到有变出什么新能力。顺带一句来历:设备树不是 Linux 为 ARM 发明的,它继承自 Open Firmware 的传统(IEEE 1275 一脉),Linux 把这套描述结构接了过来用。

新办法:把说明变成一份独立的数据 ​

还是这块 Babbage 板,还是 v3.7 那棵源码树,描述换了活法:操作规则留在驱动里,硬件事实搬出 C 代码,变成可以单独提供的数据。咱们看它的两层数据怎么分。SoC 公共层在 imx51.dtsi,多块板共享:

dts
/* Linux v3.7 arch/arm/boot/dts/imx51.dtsi,GPL-2.0,摘引 */
uart1: serial@73fbc000 {
	compatible = "fsl,imx51-uart", "fsl,imx21-uart";
	reg = <0x73fbc000 0x4000>;
	interrupts = <31>;
	status = "disabled";
};

咱们把板级层放进 imx51-babbage.dts,只补这块板的差异:

dts
/* Linux v3.7 arch/arm/boot/dts/imx51-babbage.dts,GPL-2.0,摘引 */
uart1: serial@73fbc000 {
	pinctrl-0 = <&pinctrl_uart1_1>;
	fsl,uart-has-rtscts;
	status = "okay";
};

你对照着数,老办法里的分工原样重现:SoC 层记着“这颗芯片的 UART1 从 0x73fbc000 开始,兼容两代编程接口,默认关着”,板级层只补“引脚接好了、要流控、启用”。合起来,正好是 devices-imx51.h 加 mach-mx51_babbage.c 各自管的那些事实,但它们不再是代码,是数据。内核与驱动是共享的常量,各板的描述是每板一份的变量。前提也要标清:内核已经支持这些平台,所需驱动都在。

这个思路咱们叫它数据驱动:程序依据输入的数据选择资源、执行已有的逻辑,而不是为每种情况重写一套程序。

咱们仓库贯穿的玩具示例 mini.dts 就是极简版,0.3 咱们逐行读它、亲手编译它。你只需记住:人写的是 DTS 源码,机器读的是编译出的 DTB 二进制,中间做转换的工具叫 dtc。

到这里,“为什么需要设备树”的动机闭环了:驱动要资源,总线问不出,板级代码又太重,于是描述成为独立的数据。描述长什么样、约定谁来定、谁跟它打交道?咱们在 0.2 接着看。

本节自检 ​

合上文章,你拿四个小问题验一验自己,都答得上来,0.2 读起来就不悬空。

  1. 回到开头那块板子:内核想用其中的串口,需要知道它的哪些事实?
  2. 串口驱动已经会操作某款控制器,为什么还需要这块板的资源信息?
  3. 板 A 和板 B 接口兼容但资源不同:哪些内容能共用,哪些必须分别描述?
  4. USB 设备插上系统就能认出来,是不是就代表不需要驱动了?
参考答案(想好再展开)
  1. 它是谁(哪类控制器、什么编程模型)、在哪(寄存器区域的地址)、条件如何(输入时钟、是否引出)。
  2. 操作规则是“这类控制器怎么操作”,资源是“这台机器上它在哪、条件如何”。前者驱动自带,后者随板而变,推不出来。
  3. 操作规则共用一份驱动。地址、时钟、引出与否各写各的描述。
  4. 发现只回答“你是谁”,“怎么操作”仍要驱动。枚举与驱动是两件事。

延伸阅读 ​

咱们在本节引了两处官方说法,出处在这里,想核对的点进去:

板 A/B 的比较表和 0x1000 一类教学地址,是咱们标注过的假设。Babbage 的三段摘引是 Linux 内核 v3.7 的真实源码,出处与许可(GPL-2.0,摘引有删节)随文标注,原文已缓存到本机 .claude/materials/kernel-excerpts/。