工程结构解析:Zephyr源码与应用的骨架

好,咱们正式开始第二讲。上一章我们把Zephyr跑起来了,但说实话,那时候你看到的只是一个黑盒子。今天我们就把它拆开,看看里面到底长什么样。

我个人习惯,学一个新系统,第一件事就是看它的目录结构。这就像你拿到一本新书,先翻翻目录,知道哪章讲什么,心里才有底。Zephyr的工程结构,说白了就两大块:源码树和应用树。搞懂这两棵树的布局,你后面写代码、改配置、加驱动,才能做到指哪打哪。

Zephyr源码目录结构:核心骨架

你从GitHub拉下来的Zephyr主仓库,根目录下大概长这样。我挑几个最关键的讲,其他的你以后慢慢熟悉。

目录/文件 作用 我的经验
arch/ 架构相关代码,ARM、RISC-V、x86都在这里 移植新平台时,这里是你主要改的地方
kernel/ RTOS内核核心,调度器、信号量、队列 咱们课程后半段会死磕这里
drivers/ 所有外设驱动,GPIO、I2C、SPI、UART 我经常在这里找现成驱动改一改就用
dts/ 设备树源文件和绑定(bindings) 新板子必改,后面细说
soc/ SoC级代码,时钟、电源管理、中断控制器 芯片原厂通常提供这部分
boards/ 板级支持包,每个开发板一个文件夹 你手上的板子配置都在这里
samples/ 官方示例,从hello_world到蓝牙、网络 我入门时把这里跑了个遍
tests/ 单元测试和集成测试 验证内核行为时很有用
cmake/ CMake模块和工具链配置 一般不用动,但出问题时要查
zephyr/ 核心头文件和Kconfig配置树 你写#include <zephyr.h>时就是引用这里
我的习惯:拿到一个新版本,我会先看boards/和soc/,确认我的硬件平台是否被支持。如果找不到,那就意味着要自己写板级支持包了——嗯,那是后面章节的内容。

应用程序目录结构:你的项目长什么样

Zephyr的应用,不是随便扔几个.c文件就行的。它有一套固定的组织方式。我建议你新建项目时,直接按这个模板来:

my_zephyr_app/
├── src/
│   ├── main.c
│   └── my_module.c
├── CMakeLists.txt
├── prj.conf
├── boards/
│   └── my_custom_board.dts
└── dts/
    └── bindings/
        └── my_driver.yaml

这里最核心的三个文件,我一个个说。

CMakeLists.txt:构建系统的指挥中心

Zephyr用CMake作为构建系统。你想想看,一个项目有成百上千个源文件,编译器怎么知道该编译哪些?就是靠这个文件。

一个最简单的CMakeLists.txt长这样:

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_zephyr_app)

target_sources(app PRIVATE
    src/main.c
    src/my_module.c
)

注意看第二行:find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})。这行命令告诉CMake去环境变量ZEPHYR_BASE指向的路径找Zephyr的构建系统。如果你没有设置这个环境变量,或者设错了,构建就会失败。我曾经在一个新电脑上折腾了半小时,最后发现是环境变量没设对。

避坑指南:千万不要手动设置ZEPHYR_BASE为某个固定路径。用west init和west update后,运行source zephyr-env.sh(Linux/macOS)或zephyr-env.cmd(Windows)来自动设置。否则你换项目时容易乱。

prj.conf:Kconfig配置的入口

Zephyr的配置系统叫Kconfig,它继承自Linux内核。说白了,就是通过开关来控制哪些功能被编译进固件。prj.conf就是你的应用专属配置开关文件。

举个例子,你想用UART打印日志:

CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_PRINTK=y

每一行就是一个配置选项。=y表示启用,=n或直接不写表示禁用。你可能会问:我怎么知道有哪些配置选项?

嗯,这里有个小技巧:在Zephyr源码根目录运行west build -t menuconfig,会弹出一个图形化配置界面,你可以像逛菜单一样浏览所有选项。我个人习惯是先在里面搜一遍,找到需要的选项,再手动写到prj.conf里。

核心原则:只开启你需要的功能。每多开一个CONFIG_*=y,固件体积就大一点,编译时间就长一点。嵌入式开发,资源是有限的。

设备树(Device Tree):硬件描述语言

设备树,英文叫Device Tree,是Zephyr(以及Linux)用来描述硬件的一种方式。你想想看,一个SoC内部有CPU、内存、GPIO控制器、UART、I2C……它们怎么连接的?地址是多少?中断号是多少?这些信息都写在设备树里。

设备树文件以.dts为后缀,编译后会生成.dtb(二进制)文件。Zephyr在启动时会解析这个文件,然后初始化对应的硬件。

一个简单的设备树节点长这样:

/ {
    soc {
        uart0: serial@40002000 {
            compatible = "arm,pl011";
            reg = <0x40002000 0x1000>;
            interrupts = <5>;
            status = "okay";
        };
    };
};

我来解释一下:

  • uart0:节点名字,你可以理解为这个外设的标签
  • serial@40002000:@后面是它的基地址
  • compatible:兼容性字符串,驱动通过这个来匹配硬件
  • reg:寄存器地址范围,这里是0x40002000开始,长度0x1000
  • interrupts:中断号,这里是5
  • status:状态,"okay"表示启用,"disabled"表示禁用

你可能会问:这个设备树文件放在哪里?Zephyr的查找顺序是:

  1. 先找应用目录下的boards/文件夹
  2. 再找boards/目录下对应架构和板子的文件夹
  3. 最后找dts/目录下的通用设备树

如果你用的是官方开发板,比如nRF52840DK,设备树文件在boards/arm/nrf52840dk_nrf52840.dts。你可以直接打开看看,里面描述了这颗芯片的所有外设。

我的经验:刚开始接触设备树时,不要试图一次性看懂所有内容。先找到你关心的外设节点,比如UART、GPIO、I2C,看看它们的地址和中断号。等你需要添加新硬件时,再深入学习怎么写设备树绑定(binding)。

知识结构图:工程结构全景

下面这张图,我把Zephyr的工程结构画出来了。你可以看到源码树、应用树、构建系统和设备树之间的关系。

Zephyr工程结构全景图 Zephyr工程结构全景图 Zephyr源码树 arch/ (架构代码) kernel/ (内核核心) drivers/ (驱动) dts/ (设备树) soc/ (SoC代码) boards/ (板级支持) samples/ (示例) 应用树 src/ (源代码) CMakeLists.txt prj.conf (配置) boards/ (板级覆盖) dts/ (设备树覆盖) 构建系统 CMake (构建配置) Kconfig (功能开关) Device Tree Compiler 工具链 (GCC/ARMCC) west (元工具) 应用树通过CMakeLists.txt引用源码树,prj.conf控制Kconfig开关,设备树覆盖文件可替换源码树中的默认硬件描述

从这张图你可以看到,应用树和源码树是分离的。你的应用代码放在src/里,通过CMakeLists.txt告诉构建系统去哪里找Zephyr的源码。配置和硬件描述则通过prj.conf和设备树来覆盖或扩展。

总结一下

这一章我们干了三件事:

  • 拆解了Zephyr源码目录,知道每个文件夹是干什么的
  • 搭建了应用目录结构,重点讲了CMakeLists.txt和prj.conf
  • 初步认识了设备树,知道它是怎么描述硬件的

嗯,这些东西看起来有点琐碎,但它们是后面所有实战的基础。你想想看,如果你连配置文件在哪、设备树怎么改都不知道,后面写再多代码也跑不起来。所以,花点时间把这一章消化掉,绝对值得。