本文以正点原子 STM32F407 探索者为例通过 I2C EEPROM 和 SPI Flash 两个实际外设从源码层面拆解 Zephyr 的三层设备树模型并追踪一条compatible属性如何串联起硬件描述、驱动匹配和 Kconfig 配置。目录核心问题三行代码背后发生了什么第一层SoC 级设备树 —— 芯片有什么第二层Pinctrl 设备树 —— 引脚能配成什么第三层板级设备树 —— 我用了什么、怎么连的完整链路compatible → 绑定文件 → Kconfig → 驱动总结核心问题三行代码背后发生了什么当你在板级 DTS 中写下i2c1 { pinctrl-0 i2c1_scl_pb8 i2c1_sda_pb9; status okay; eeprom0: eeprom50 { compatible atmel,at24c02, atmel,at24; }; };这三行代码触发了什么为什么status okay能让一个外设活过来compatible atmel,at24是怎么让eeprom_at2x.c被编译进固件的要回答这些问题必须理解 Zephyr 的三层设备树架构。SoC 级 dtsi芯片有什么 ↓ Pinctrl 级 dtsi引脚能配成什么 ↓ Board 级 dts我用了什么、怎么连的下面以 I2C1 和 SPI1 为例逐层拆解源码。第一层SoC 级设备树 —— 芯片有什么文件位置zephyrproject/zephyr/dts/arm/st/f4/stm32f4.dtsi这是 Zephyr 源码树中的文件不是你的项目里的。它定义了 STM32F4 系列所有芯片共有外设的寄存器信息。I2C1 节点的源码分析i2c1: i2c40005400 { compatible st,stm32-i2c-v1; clock-frequency I2C_BITRATE_STANDARD; #address-cells 1; #size-cells 0; reg 0x40005400 0x400; clocks rcc STM32_CLOCK(APB1, 21U); interrupts 31 0, 32 0; interrupt-names event, error; status disabled; };逐字段解析i2c1: i2c40005400冒号左边i2c1是节点标签你的板级 DTS 中i2c1就是通过它来引用这个节点的。冒号右边i2c40005400是节点名和单元地址。i2c是设备类型40005400是寄存器基地址。compatible st,stm32-i2c-v1这是整个体系中最关键的属性。它告诉 Zephyr“这个硬件是 STM32 的 I2C 控制器 V1 版本”。Zephyr 的驱动框架通过这个字符串来匹配驱动后面第四部分会详细展开。V1 表示 STM32F1/F2/F4 系列使用的 I2C 控制器 IP区别于 F7/H7 使用的 V2。clock-frequency I2C_BITRATE_STANDARDSoC 级设置的默认时钟频率。I2C_BITRATE_STANDARD宏展开为 100000100kHz。这是一个默认值板级 DTS 可以覆写。如果你的板子 I2C 设备支持 400kHz可以在板级覆盖为I2C_BITRATE_FAST。#address-cells 1和#size-cells 0设备树规范属性告诉解析器这个总线上的子节点地址用 1 个 32 位 cell 表示不需要 size。这就是为什么 EEPROM 节点可以写reg 0x50单 cell I2C 地址而不是reg 0x50 0。reg 0x40005400 0x400寄存器基地址和大小。0x40005400是 STM32F407 的 I2C1 外设寄存器起始地址0x400是寄存器块大小1KB。这个值在编写板级 DTS 时不需要关心—— 它是芯片设计决定的SoC 级已经定义好了。你可以打开 STM32F407 参考手册翻到存储器映射章节对照验证这个地址。clocks rcc STM32_CLOCK(APB1, 21U)指定时钟来源。STM32_CLOCK(APB1, 21U)表示来自 APB1 总线的第 21 位时钟使能位。同样不需要板级关心但理解它有助于排查时钟相关问题 —— 如果 I2C 不工作可以检查 APB1 时钟是否被正确配置。interrupts 31 0, 32 0I2C1 有两个中断源编号 31 的 event 中断传输完成等和编号 32 的 error 中断仲裁丢失、超时等。0是中断优先级板级可覆写。status disabled核心要点SoC 级所有外设默认都是disabled。这很合理 —— 芯片有这个外设但你的板子不一定用了它。板级 DTS 必须显式写status okay来使能。SPI1 节点的源码分析在同一个文件中spi1: spi40013000 { compatible st,stm32-spi; #address-cells 1; #size-cells 0; reg 0x40013000 0x400; clocks rcc STM32_CLOCK(APB2, 12U); interrupts 35 5; status disabled; };对比 I2C1SPI1 有几个值得注意的差异compatible st,stm32-spi没有 V1/V2 后缀STM32 的 SPI 控制器在 F1/F2/F4/F7/H7 上都使用同一个驱动spi_ll_stm32.c通过 LL 库的 HAL 抽象层屏蔽了寄存器差异。所以不需要版本区分。reg 0x40013000 0x400SPI1 的寄存器基地址在 APB2 总线上I2C1 在 APB1 上0x13000vs0x05400的编址反映了它们在芯片内部的物理位置。interrupts 35 5SPI1 只有一个中断源编号 35优先级为 5。clocks rcc STM32_CLOCK(APB2, 12U)SPI1 挂在 APB2 总线上I2C1 挂在 APB1 上这会影响最高时钟频率 —— APB2 最高 84MHzAPB1 最高 42MHz。第一层小结stm32f4.dtsi 回答的问题 ├── 芯片有哪些 I2C 控制器 → i2c1, i2c2, i2c3 ├── 它们的寄存器在哪 → reg 基地址 大小 ├── 它们用什么时钟 → clocks rcc ... ├── 它们的中断号是多少 → interrupts ... ├── 它们默认开启了吗 → status disabled ← 必须在板级覆写第一层给出的核心信息是外设的身份证地址、中断、时钟。这些是芯片手册的直接翻译与具体板子无关。第二层Pinctrl 设备树 —— 引脚能配成什么文件位置zephyrproject/modules/hal/stm32/dts/st/f4/stm32f407v(e-g)tx-pinctrl.dtsi这个文件在modules/hal/stm32/下是 STM32 HAL 模块的一部分。文件名中的v(e-g)tx表示 LQFP100 封装正点原子探索者使用的封装。为什么需要第二层STM32 的一个 GPIO 引脚可以配置为多种功能。以 PB8 为例功能AF 编号GPIO 输入/输出AF0TIM4_CH3AF2I2C1_SCLAF4SDIO_D4AF12CAN1_RXAF9同一个 PB8配成 AF4 就是 I2C 时钟线配成 AF9 就是 CAN 接收。Pinctrl 文件为每个引脚的每种功能都预定义了对应的宏避免手动算寄存器值。I2C1 引脚的 Pinctrl 宏/omit-if-no-ref/ i2c1_scl_pb8: i2c1_scl_pb8 { pinmux STM32_PINMUX(B, 8, AF4); bias-pull-up; drive-open-drain; };逐行解析/omit-if-no-ref/编译器指示如果这个宏没有被任何地方引用就不要生成这段代码。这很关键 —— pinctrl 文件定义了几百个引脚宏但你的板子只用其中十几个。omit-if-no-ref保证了最终固件不会膨胀。i2c1_scl_pb8: i2c1_scl_pb8标签名。你的板级 DTS 中i2c1_scl_pb8就是引用它。命名规范为外设_功能_引脚i2c1外设scl功能pb8引脚。pinmux STM32_PINMUX(B, 8, AF4)将 GPIOB 的第 8 脚配置为 AF4I2C1_SCL。STM32_PINMUX是一个宏展开后生成 32 位寄存器值包含了 GPIO 端口号、引脚号和复用功能编号。AF4 来自 STM32F407 数据手册 —— 你可以在引脚定义表的 “Alternate Function” 列中找到这个值。bias-pull-up使能内部上拉电阻。I2C 总线要求数据线在空闲时保持高电平虽然板子上已经有 4.7K 外部上拉但开启内部上拉可以提供额外的驱动能力。drive-open-drain配置为开漏输出。I2C 协议要求开漏模式 —— 设备只能拉低总线不能主动拉高。总线的上拉由电阻完成。这允许多个设备共享同一总线而不会发生冲突。SPI1 引脚的 Pinctrl 宏/omit-if-no-ref/ spi1_sck_pb3: spi1_sck_pb3 { pinmux STM32_PINMUX(B, 3, AF5); bias-pull-down; slew-rate very-high-speed; }; /omit-if-no-ref/ spi1_miso_pb4: spi1_miso_pb4 { pinmux STM32_PINMUX(B, 4, AF5); bias-pull-down; }; /omit-if-no-ref/ spi1_mosi_pb5: spi1_mosi_pb5 { pinmux STM32_PINMUX(B, 5, AF5); bias-pull-down; };对比 I2C 的 pinctrlSPI 有几个关键差异bias-pull-downvsbias-pull-upSPI 总线的 SCK 和 MOSI 在空闲时被拉到低电平下拉而 I2C 的 SCL/SDA 在空闲时被拉到高电平上拉。这是两种总线协议的电气特性决定的。slew-rate very-high-speed仅 SCK 有SPI 时钟频率可达 80MHz需要较高的信号斜率slew rate来保证时钟边沿的清晰度。I2C 只有 100kHz不需要高速斜率。MISO 和 MOSI 没有这个属性因为从设备Flash输出的 MISO 信号斜率由从设备自己控制。STM32_PINMUX(B, 3, AF5)vsSTM32_PINMUX(B, 8, AF4)SPI1 使用 AF5I2C1 使用 AF4。AF 编号从 STM32 数据手册的引脚复用表中查得。第二层小结stm32f407v(e-g)tx-pinctrl.dtsi 回答的问题 ├── PB8 能配成 I2C1_SCL 吗 → 能AF4宏名 i2c1_scl_pb8 ├── PB3 能配成 SPI1_SCK 吗 → 能AF5宏名 spi1_sck_pb3 ├── I2C 引脚需要什么电气配置 → 开漏 上拉 ├── SPI SCK 需要什么电气配置 → 高速斜率 下拉第二层给出的核心信息是每个引脚的技能列表和电气参数。板级 DTS 只需要从中选择要用到的技能来引用。第三层板级设备树 —— 我用了什么、怎么连的文件位置你的项目目录/boards/st/stm32f407_alientek/stm32f407_alientek.dts这是你自己的文件是三层模型中唯一需要你编写的。它的作用是从 SoC 级提供的外设清单中选你要用的从引脚级提供的功能菜单中选你要配的然后把它们组装起来。I2C1 板级配置的完整源码分析i2c1 { pinctrl-0 i2c1_scl_pb8 i2c1_sda_pb9; pinctrl-names default; clock-frequency I2C_BITRATE_STANDARD; status okay; eeprom0: eeprom50 { compatible atmel,at24c02, atmel,at24; reg 0x50; size 256; pagesize 8; address-width 8; timeout 5; }; };逐行解析i2c1引用 SoC 级stm32f4.dtsi中定义的i2c1节点标签。这是在说“我要配置芯片上的 I2C1 外设”。pinctrl-0 i2c1_scl_pb8 i2c1_sda_pb9引用 Pinctrl 级定义的i2c1_scl_pb8和i2c1_sda_pb9宏。这是在说“I2C1 的 SCL 用 PB8、SDA 用 PB9”。pinctrl-0是默认引脚状态。一个外设可以有多个 pinctrl 状态如pinctrl-1表示低功耗引脚状态但大多数情况下只有pinctrl-0。pinctrl-names default给pinctrl-0这个状态起名叫default。驱动初始化时会自动应用default状态的引脚配置。clock-frequency I2C_BITRATE_STANDARD覆写了 SoC 级的默认时钟频率。虽然这里写的和 SoC 级一样100kHz但如果你的 I2C 设备支持更快可以改为I2C_BITRATE_FAST400kHz。status okay最关键的一行。SoC 级默认是disabled板级必须覆写为okayZephyr 才会为这个外设加载驱动、分配资源、初始化硬件。写disabled或干脆不写i2c1 {}块这个外设就不会被初始化。eeprom0: eeprom50I2C 总线上的子设备。eeprom0是标签供aliases引用。50是设备在 I2C 总线上的 7 位地址24C02 的 A0/A1/A2 接地 0x50。compatible atmel,at24c02, atmel,at24驱动匹配的钥匙。第一个atmel,at24c02是最具体的型号第二个atmel,at24是 fallback 族名。Zephyr 的 Kconfig 使用atmel,at24来触发驱动使能详见第四部分。reg 0x50I2C 设备的 7 位地址。这个值被 I2C 驱动用来在总线上寻址该设备。注意是 7 位地址不含 R/W 位STM32 硬件会自动处理读写位的添加。size 256EEPROM 容量 256 字节。24C02 的 “02” 表示 2Kbit 256Byte。驱动通过eeprom_get_size()返回此值。pagesize 824C02 的页大小 8 字节。写操作跨页时需要分页写入先写页内部分等 5ms 写周期结束再写下一页。驱动自动处理分页逻辑。address-width 8单字节地址。容量 ≤ 2Kbit 的 EEPROM 使用 8 位地址0~255。容量更大的型号如 24C256需要使用 16 位地址。timeout 5写周期超时 5ms。EEPROM 内部写入需要时间通常 5ms驱动在写操作后会等待这段时间再继续。SPI1 板级配置的完整源码分析spi1 { pinctrl-0 spi1_sck_pb3 spi1_miso_pb4 spi1_mosi_pb5; pinctrl-names default; cs-gpios gpiob 14 GPIO_ACTIVE_LOW; status okay; w25q128: flash0 { compatible jedec,spi-nor; reg 0; spi-max-frequency 80000000; size DT_SIZE_M(16); jedec-id [ef 40 18]; }; };与 I2C1 不同的关键属性cs-gpios gpiob 14 GPIO_ACTIVE_LOWSPI 片选信号。正点原子的 25Q128 片选连接在 PB14 上这是一个普通 GPIO不是 SPI 控制器的硬件 NSS。cs-gpios让 SPI 驱动用 GPIO 方式控制片选 —— 通信前拉低 PB14、通信后拉高 PB14。这比硬件 NSS 更可靠因为硬件 NSS 在某些模式下有自动翻转的 bug。GPIO_ACTIVE_LOW表示低电平选中芯片25Q128 标准。如果有多个 SPI 从设备可以写多个 cs-gpioscs-gpios gpiob 14 0, gpioa 4 0;子设备节点用reg 0、reg 1分别对应。reg 0片选索引对应cs-gpios数组的第 0 个元素。与 I2C 的reg 0x50完全不同 —— I2C 的 reg 是总线地址SPI 的 reg 是片选编号。spi-max-frequency 80000000SPI 最大时钟频率 80MHz。25Q128 最高支持 104MHz根据数据手册取 80MHz 是保守值。这个值被 SPI 驱动用来配置时钟预分频器。如果 Flash 通信不稳定数据错误、JEDEC ID 读不到可以降低此值如 40MHz排查。compatible jedec,spi-nor通用 JEDEC SPI NOR Flash 兼容字符串。不需要写winbond,w25q128这样的具体型号 —— JEDEC CFI 标准保证了所有 NOR Flash 的操作方式一致读 JEDEC ID、读状态寄存器、写使能、页编程、扇区擦除等命令都是标准的。Zephyr 的spi_nor.c驱动启动时会读取 Flash 的 JEDEC ID和你 DTS 中写的jedec-id做比对匹配上了就初始化。size DT_SIZE_M(16)Flash 容量 16MB128Mbit。DT_SIZE_M(16) 16 * 1024 * 1024 16777216。jedec-id [ef 40 18]Winbond W25Q128 的 JEDEC IDef Winbond制造商 ID40 18 W25Q128设备 ID。驱动启动时会发送0x9F命令读取 Flash 的 JEDEC ID与此值比对。如果不匹配驱动初始化失败device_is_ready()返回 false。第三层小结stm32f407_alientek.dts 回答的问题 ├── 这块板子用了 I2C1 吗 → 用了status okay ├── I2C1 的 SCL/SDA 用哪个引脚 → PB8(AF4) / PB9(AF4) ├── I2C1 总线上挂了什么设备 → 24C02 EEPROM地址 0x50 ├── 这块板子用了 SPI1 吗 → 用了status okay ├── SPI1 的 SCK/MISO/MOSI 用哪些引脚 → PB3/PB4/PB5(AF5) ├── SPI1 的片选用哪个 GPIO → PB14低有效 ├── SPI1 上挂了什么 Flash → W25Q128, 16MB, JEDEC ID ef4018第三层是三层模型的核心 —— SoC 级提供清单Pinctrl 级提供菜单板级负责选择和组装。完整链路compatible → 绑定文件 → Kconfig → 驱动上面三个层次讲清楚了硬件怎么描述。接下来追踪一条compatible属性如何触发整个驱动编译和加载链条。第 1 步DTS 中声明 compatible// 板级 stm32f407_alientek.dts eeprom0: eeprom50 { compatible atmel,at24c02, atmel,at24; };第 2 步绑定文件验证构建系统根据compatible找到对应的绑定文件zephyrproject/zephyr/dts/bindings/mtd/atmel,at24.yaml绑定文件 key 行compatible:atmel,at24include:[atmel,at2x-base.yaml,i2c-device.yaml]设备树编译器DTC在编译时用这个 yaml 验证你的 DTS 节点size属性存在吗 →atmel,at2x-base.yaml声明了size: required: truepagesize属性存在吗 → 同上required: true节点放在 I2C 控制器下面合法吗 →i2c-device.yaml验证了父节点必须是 I2C 控制器如果验证失败编译直接报错。这保证了不会出现编译通过但运行时找不到驱动的情况。第 3 步生成 DT_HAS 宏构建系统扫描所有 DTS 节点为每个compatible生成存在性宏// build/name/zephyr/include/generated/zephyr/devicetree_generated.h#defineDT_N_S_soc_S_i2c_40005400_S_eeprom_50_COMPAT_MATCHES_atmel_at24c021#defineDT_N_S_soc_S_i2c_40005400_S_eeprom_50_COMPAT_MATCHES_atmel_at241#defineDT_COMPAT_HAS_OKAY_atmel_at24c021#defineDT_N_INST_atmel_at24c02_NUM_OKAY1#defineDT_FOREACH_OKAY_atmel_at24c02(fn)fn(...)关键宏是DT_COMPAT_HAS_OKAY_atmel_at24c02对应atmel,at24c02和DT_FOREACH_OKAY_atmel_at24(fn)对应atmel,at24。第 4 步Kconfig 条件触发在drivers/eeprom/Kconfig中config EEPROM_AT24 bool I2C EEPROMs compatible with Atmels AT24 family default y depends on DT_HAS_ATMEL_AT24_ENABLED # ← 关键匹配的是 atmel,at24 select I2C select EEPROM_AT2X条件depends on DT_HAS_ATMEL_AT24_ENABLED的含义DT_HAS_ATMEL_AT24_ENABLED就是第 3 步中DT_COMPAT_HAS_OKAY_atmel_at24在 Kconfig 域的映射名这就是为什么 DTS 中必须带atmel,at24fallback—— 如果只写atmel,at24c02生成的宏是DT_HAS_ATMEL_AT24C02_ENABLED与 Kconfig 的DT_HAS_ATMEL_AT24_ENABLED不匹配整个驱动不会被编译。第 5 步驱动源码编译drivers/eeprom/CMakeLists.txt中的编译控制zephyr_library_sources_ifdef(CONFIG_EEPROM_AT2X eeprom_at2x.c)zephyr_library_sources_ifdef的含义如果CONFIG_EEPROM_AT2Xy就编译eeprom_at2x.c。Kconfig 的链条是CONFIG_EEPROMprj.conf 中写 CONFIG_EEPROMy → 使能 EEPROM 子系统框架 → CONFIG_EEPROM_AT24DTS 中有 atmel,at24 节点时自动 y → 选中 CONFIG_EEPROM_AT2X → 编译 eeprom_at2x.c第 6 步应用代码获取设备#includezephyr/device.h// I2C EEPROM — 通过别名conststructdevice*devDEVICE_DT_GET(DT_ALIAS(eeprom_0));// SPI Flash — 通过 compatibleconststructdevice*devDEVICE_DT_GET_ONE(jedec_spi_nor);两种方式的区别方式场景原理DT_ALIAS(eeprom_0)DEVICE_DT_GET()板上有多个同类设备时需区分通过别名精确定位一个节点返回该节点的设备指针DEVICE_DT_GET_ONE(jedec_spi_nor)全局只有一个该类设备遍历所有节点返回第一个匹配该 compatible 的设备指针SPI Flash 的 Kconfig 链条对比# drivers/flash/Kconfig.spi_nor config SPI_NOR bool SPI NOR Flash support default y depends on DT_HAS_JEDEC_SPI_NOR_ENABLED # ← 匹配 DTS 中的 jedec,spi-nor select FLASH select FLASH_HAS_PAGE_LAYOUT链条CONFIG_SPI_NORDTS 中有 jedec,spi-nor 节点时自动 y → select FLASH → 编译 flash_page_layout.c → 编译 spi_nor.c jesd216.c注意jedec,spi-nor在 Kconfig 中只需要这一个 compatible 就能匹配不像atmel,at24c02需要 fallback。因为 JEDEC NOR Flash 是一个通用标准不存在型号特定的 Kconfig 依赖。完整链路图板级 DTS 绑定文件 Kconfig 驱动源码 ──────────────────────────────────────────────────────────────────────────────────────────── compatible atmel,at24.yaml config EEPROM_AT24 eeprom_at2x.c atmel,at24c02 ────→ compatible: ────→ depends on ────→ zephyr_library_ atmel,at24 atmel,at24 DT_HAS_ATMEL_AT24 sources_ifdef( _ENABLED CONFIG_EEPROM_AT2X eeprom_at2x.c) compatible jedec,spi-nor.yaml config SPI_NOR spi_nor.c jedec,spi-nor ────→ compatible: ────→ depends on ────→ zephyr_library_ jedec,spi-nor DT_HAS_JEDEC_SPI sources_ifdef( _NOR_ENABLED CONFIG_SPI_NOR spi_nor.c)总结Zephyr 的三层设备树模型实现了关注点分离层次文件谁维护回答的问题SoC 级stm32f4.dtsiST/Zephyr 社区芯片有什么外设、在哪个地址、用哪个中断引脚级stm32f407v(e-g)tx-pinctrl.dtsiST/Zephyr 社区每个引脚能配成什么功能、电气参数是什么板级stm32f407_alientek.dts你我的板子用了哪些外设、接了哪些引脚、挂了什么设备一条compatible属性串联起整个体系DTS compatible → 绑定文件验证 → DT_HAS_xxx 宏 → Kconfig 条件使能 → 驱动源码编译理解了这个链路你就掌握了 Zephyr 设备树的核心。下一次添加新外设时你可以自己追踪打开 SoC dtsi 确认外设存在 → 打开 pinctrl dtsi 确认引脚宏可用 → 在板级 DTS 中组装 → 打开 bindings 确认属性合法 → 在.config中验证 Kconfig 链。推荐的实践编译后打开build/name/zephyr/zephyr.dts合并后的完整设备树和build/name/zephyr/.config最终 Kconfig 配置对着它们一一验证你写的配置是否生效。这是最快的学习方式。