工程结构解析: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开始,长度0x1000interrupts:中断号,这里是5status:状态,"okay"表示启用,"disabled"表示禁用
你可能会问:这个设备树文件放在哪里?Zephyr的查找顺序是:
- 先找应用目录下的
boards/文件夹 - 再找
boards/目录下对应架构和板子的文件夹 - 最后找
dts/目录下的通用设备树
如果你用的是官方开发板,比如nRF52840DK,设备树文件在boards/arm/nrf52840dk_nrf52840.dts。你可以直接打开看看,里面描述了这颗芯片的所有外设。
知识结构图:工程结构全景
下面这张图,我把Zephyr的工程结构画出来了。你可以看到源码树、应用树、构建系统和设备树之间的关系。
从这张图你可以看到,应用树和源码树是分离的。你的应用代码放在src/里,通过CMakeLists.txt告诉构建系统去哪里找Zephyr的源码。配置和硬件描述则通过prj.conf和设备树来覆盖或扩展。
总结一下
这一章我们干了三件事:
- 拆解了Zephyr源码目录,知道每个文件夹是干什么的
- 搭建了应用目录结构,重点讲了
CMakeLists.txt和prj.conf - 初步认识了设备树,知道它是怎么描述硬件的
嗯,这些东西看起来有点琐碎,但它们是后面所有实战的基础。你想想看,如果你连配置文件在哪、设备树怎么改都不知道,后面写再多代码也跑不起来。所以,花点时间把这一章消化掉,绝对值得。