第一章:Zephyr与设备树概述
Zephyr RTOS简介
先聊聊Zephyr RTOS吧。说实话,我第一次接触Zephyr是在一个物联网项目里,当时被它「麻雀虽小五脏俱全」的特性吸引了。
Zephyr是一个面向资源受限设备的实时操作系统。它由Linux基金会维护,开源、免费。我个人的感觉是,它特别适合那些需要「小而精」的嵌入式场景——比如智能传感器、可穿戴设备、工业控制器这些。
它的核心特点我列一下:
- 模块化设计:你可以按需裁剪,不需要的功能就不编译进去
- 丰富的驱动支持:从GPIO到蓝牙、WiFi,覆盖面很广
- 安全性:内置了安全启动、加密等机制
- 跨平台:支持ARM、RISC-V、x86等多种架构
我在项目中遇到过一个问题:用FreeRTOS时,每次换硬件平台都要重写底层驱动,特别痛苦。Zephyr的设备树机制帮我解决了这个痛点——后面我会详细讲。
设备树(DTS)的起源与作用
设备树这个概念,最早是从Linux内核那边来的。你想想看,以前嵌入式开发是怎么描述硬件的?
传统做法是:在代码里硬编码各种寄存器地址、中断号、时钟频率。比如这样:
/* 传统方式:硬编码硬件信息 */
#define UART0_BASE 0x40001000
#define UART0_IRQ 33
#define UART0_CLK 16000000
这种做法有什么问题?我举个例子。有一次我接手一个项目,硬件从STM32F4换成了STM32F7。好家伙,所有外设地址全变了。我得一个个去翻数据手册,改代码里的宏定义。改完还担心漏掉哪个。
设备树就是来解决这个问题的。它用一套结构化的文本描述硬件,把「硬件是什么」和「软件怎么用」分离开。
一个典型的设备树节点长这样:
/ {
uart0: serial@40001000 {
compatible = "st,stm32-uart";
reg = <0x40001000 0x400>;
interrupts = <33 0>;
clocks = <&clk_hse 16000000>;
status = "okay";
};
};
你看,地址、中断、时钟都写在这里了。换硬件时,只需要改这个文件,驱动代码基本不用动。
为什么Zephyr选择设备树
这个问题我琢磨过很久。Zephyr团队为什么不用其他方案,偏偏选了设备树?
原因其实很实在:
- 复用Linux生态:Linux内核已经用设备树很多年了,积累了大量的硬件描述文件。Zephyr可以直接借鉴,省了很多事。
- 跨平台统一:Zephyr支持几十种开发板。没有设备树的话,每个板子都得写一套独立的配置代码,维护成本太高了。
- 编译期静态配置:Zephyr是静态编译的,不像Linux那样动态加载。设备树在编译时就被解析成C结构体,运行时没有额外开销。
我记得有一次,客户要求把产品从NXP的i.MX RT系列换成Nordic的nRF系列。要是以前,这得重写大半驱动。但用Zephyr+设备树,我只需要换一个设备树文件,再调整一下Kconfig配置,两天就搞定了。
#address-cells、#size-cells、phandle这些概念,容易让人头晕。别急,后面几章我会手把手带你过一遍。
设备树在Zephyr中的工作流程
简单说说设备树在Zephyr里是怎么跑起来的:
| 步骤 | 做什么 | 生成什么 |
|---|---|---|
| 1. 编写DTS文件 | 描述硬件:外设地址、中断、时钟等 | .dts / .dtsi 文件 |
| 2. 编译时解析 | dtc工具将DTS编译为DTB二进制 | .dtb 文件 |
| 3. 生成头文件 | Zephyr脚本从DTB生成C宏定义 | devicetree.h 等 |
| 4. 驱动使用 | 驱动代码通过宏访问硬件信息 | 编译进固件 |
说白了,设备树就是硬件和驱动之间的「翻译官」。你告诉它硬件长什么样,它帮你生成驱动需要的配置数据。
boards/目录下的DTS文件,看看这个板子有哪些外设、地址怎么分配的。这比看原理图还快。
避坑指南
最后分享几个我踩过的坑:
- 兼容字符串别写错:
compatible = "st,stm32-uart"和compatible = "st,stm32-uart1"是两回事。驱动匹配不上时,先检查这个。 - status属性别漏了:默认是
disabled,你得显式写成okay才能用。我曾经因为这个排查了半天。 - 地址和大小要对应:
reg = <0x40001000 0x400>表示起始地址0x40001000,长度0x400字节。写错了驱动可能访问到错误的内存区域。
嗯,第一章就到这里。设备树的概念先有个印象,后面我们会一步步深入。下一章我会带你实际动手,写一个完整的设备树文件,并把它跑在开发板上。