Zephyr入门第三课——在IDE当中一步步完成自定义板子的项目配置
一、Zephyr项目结构
[!IMPORTANT]
后续的一切自定义配置都要基于对较为标准的现代Zephyr项目结构的理解因此这一部分的内容非常的重要
如果发现有不明白的名词,或是没有学过的技术栈,最好借助
AI来进行学习
1. 前言
教程后续讲解使用的开发板,是我于一年前为实验室新来的学弟学妹们教学用,进而绘制的一块STM32H743IIT6的板子,为了方便后续讲解,在这里附上其主要的原理图。
核心板原理图

底板原理图

[!WARNING]
底板中,存在两个硬件问题其一为:
CH340N与STM32的USART2接反了,估计当时是在深夜画的板子,画迷糊了,后面教学弟学妹们也没用这个出串口数据,这两天调的时候才发现😅,也是没招了其二为:
WS2812B无法被驱动,本来想着5V耐受引脚就能正常工作,还能省点钱,结果到手里面发现不行,至今未排查出是何原因,也懒得再去修改打板了,毕竟只是块教学用的板子喵~
教程使用的IDE为Clion,教程对IDE没有限制,只需要你的IDE能够支持CMake项目的加载即可。也就是说,像VS Code这些也都是没问题的。
并且接下来的教程需要你对CMake有一定了解。我认为对于现代化的C/C++项目,CMake是必须要掌握的,虽然它在某些方面上不尽人意,但其足矣应付99%的场景,这就足够了。
2. Zephyr的项目结构
我们首先需要了解的是,Zephyr项目需要哪些东西才能够完成编译运行,之后才好去谈修改。
在第二课中,我们已对Zephyr作出了基本的介绍,简单了解到Zephyr的核心配置机制是Devicetree和Kconfig,那么对于实际的Zephyr项目而言,其配置也同样是围绕着这两者去完成的。
这边直接复制第二课当中对两个核心配置机制的介绍。
Devicetree(设备树)
描述"板子上有哪些硬件"——晶振频率、GPIO 引脚分配、I2C/SPI 外设地址、内存布局等。编译时由设备树编译器(DTC)解析,生成 C 头文件供驱动代码使用。换板子时只需修改 .dts 文件,应用层代码无需改动。
Kconfig
描述"要编译哪些功能"——是否启用网络栈、USB、蓝牙、调试级别、线程数上限等。通过 prj.conf 或 Kconfig.defconfig 文件配置,最终生成 .config 决定哪些源文件参与编译。这套机制直接继承自 Linux 内核。
a. Zephyr的Devicetree
Zephyr通过Devicetree来解耦硬件配置与软件,这么做得到的结果就是,同样的软件,在外部设备没有太大的改变情况下,可以自由的从旧芯片迁移到新的芯片。很多工程师在开发当中,往往容易在代码当中将引脚信息和实际的驱动代码耦合。经验更加丰富的工程师会通过宏定义将引脚信息抽象出来,但即使是这样,驱动进行迁移的时候,还是需要修改引脚信息。而Zephyr通过Devicetree来解耦,方便工程师写的代码能够自由的在不同的芯片之间切换,而不需要去修改驱动里面的代码。这样维护项目就会轻松很多。
不过这么做,自然就引入了新的代价。代价就是,工程师需要学习Devicetree,并且在切换芯片的时候,修改或写一套新的Devicetree,大部分的时候都是做修改任务,很少会是重写一套新的设备树。
说的可能有些抽象,代入到一个实际的场景——点灯。
在STM32 HAL当中,你完成这个任务的步骤一般是,在CubeMX当中选择某个引脚为out,然后根据实际的硬件去配置输出模式是上拉还是推挽,最后在代码里面进行gpio的具体操作。
换到Zephyr当中,则是变成了,根据原理图去写实际关于LED部分的dts(主设备树),如果需要配置输出模式等电气特性,则需要进一步到pinctrl(引脚复用)。同时需要确保Kconfig当中配置了CONFIG_GPIO=y,最后在代码里面进行具体gpio操作。
看起来Zephyr要更加复杂一些,实际上确实Zephyr更加复杂。究其根本是为了解耦而增加了抽象层。而STM32 HAL就只需要点点GUI,就可以完成点灯相关的配置。这么看,好像STM32 HAL要更胜一筹。事实上,在入门阶段,STM32 HAL确实要比Zephyr好很多。不过我们这里更多的是考虑项目的整体可维护性,以及对于项目底层的掌控程度,在这些方面上Zephyr要优于STM32 HAL。
b. Zephyr的Kconfig
Zephyr通过Kconfig来实现项目软件层面的配置。如果你使用过ESP-IDF框架,那么你肯定用过Kconfig,ESP-IDF的配置文件是sdkconfig,使用idf.py menuconfig就可以进入配置界面。

既然Zephyr也是Kconfig,那它可不可以也用menuconfig来配置呢?答案是可以的,但是一点都不推荐你用menuconfig来配置。因为Zephyr它的Kconfig配置流程是这样的。

Zephyr的配置结构是内核config,板级config,应用级config三者共同发挥作用,最终由Kconfig合并引擎将其合并成.config文件,如果你做过Linux kernel编译,那么你肯定认识这个.config。最后再由.config生成autoconf.h头文件,作用于Zephyr kernel(决定各种ifdef是否被启用)。应用级prj.conf会覆盖板级config。
在大部分情况,我们不需要关心内核级config,只需要考虑板级config和应用级config即可。
再说为何不推荐使用menuconfig来改配置,因为menuconfig只能作用于生成出来的.confg,而不能改板级或应用级的配置,但生成出来的.config,它不是持久化的,每次重新生成CMake项目之后,.config的内容就会被刷新一次。
生成出来的.config的位置一般是在cmake-build-release-zephyr/zephyr/.config这里。
那么既然不推荐直接用menuconfig,那么该使用什么呢?
答案是,从官方文档中来,到官方文档中去。同时外加AI的辅助。一切的配置信息需要查阅官方文档后手动编辑prj.conf或board_defconfig。但不代表menuconfig不能用,你可以使用menuconfig在构建目录当中去探索一些配置选项。
总结一下,Zephyr项目由Devicetree和Kconfig组成。Devicetree部分分为dts(主设备树)和pinctrl(引脚复用)这两部分,Kconfig部分分为内核默认config,板子默认config,应用默认config,不考虑内核默认config,板子默认config的文件名类似xxx_defconfig,应用默认config的文件名为prj.conf。此外,板子默认config还有一个默认值补充文件为Kconfig.defconfig。
带着上面的知识来到下面实际的Zephyr项目当中去学习吧喵~
c. 实际的Zephyr项目结构
以下面我手里面自行绘制的STM32H743的典型项目结构为例。
❯ eza --tree --git -I="build|.git|cmake-build-debug-zephyr|cmake-build-release-zephyr"
.
├── boards
│ └── arm
│ └── st_h743_board
│ ├── board.cmake
│ ├── board.yml
│ ├── Kconfig.defconfig
│ ├── Kconfig.st_h743_board
│ ├── st_h743_board-pinctrl.dtsi
│ ├── st_h743_board.dts
│ ├── st_h743_board_defconfig
│ └── support
│ └── openocd.cfg
├── CMakeLists.txt
├── CMakePresets.json
├── prj.conf
└── src
└── main.c
先说说顶层目录下的这些文件都是干什么的。
| 文件/目录 | 作用 |
|---|---|
CMakeLists.txt |
只要你用过CMake,这个文件是干啥的我就不多说了,总结就是整个项目的主要管理配置都靠它 |
CMakePresets.json |
CMake的预设配置。统一存放 Debug/Release 的构建参数,以及一些其他的环境变量,例如BOARD、Python3_EXECUTABLE、CMAKE_BUILD_TYPE 等 |
prj.conf |
应用级Kconfig配置。覆盖 board 默认值,如启用 GPIO、USB、C++、调试选项等 |
src/ |
源码目录。放 main.c / main.cpp 等业务逻辑 |
boards/ |
自定义板子定义目录。Zephyr 通过 BOARD_ROOT 找到这里,按 boards/<arch>/<board_name>/ 的固定结构扫描你的板子。 |
CMakePresets.json
先讲一下CMakePresets.json这个文件,从CMake 3.19版本开始,CMake引入了这个文件,其作用是将CMake项目的构建配置(如构建目录、生成器、编译器、CMake 变量等)以标准化、可共享的 JSON 文件形式固化下来。这么做的好处是可以将常用的项目配置方式,便捷地在团队成员、CI/CD 系统或不同IDE之间共享。
与CMakePresets.json一同的,还有一个CMakeUserPresets.json文件,看名字就知道,CMakeUserPresets.json是用来定义开发者个人的本地预设,而CMakePresets.json则是定义项目级的公共预设。不过这种一般团队协作中才会使用CMakeUserPresets.json。在本教程中,只使用CMakePresets.json。
接着描述一下具体使用场景,CMakePresets.json内部定义了编译器,生成器,CMake 变量,构建目录,环境变量等信息,当你使用一些支持CMake的IDE,IDE在加载你项目的时候,会首先检查当前目录下是否有CMakePresets.json和CMakeUserPresets.json,如果有就合并二者,CMakeUserPresets.json的优先级会高一些,然后把里面写的内容加载到项目CMake配置当中,为后续执行CMakeLists.txt做准备。
我们一会就会见到CMakePresets.json在本教程当中的应用。
prj.conf
prj.conf在前面说Kconfig的时候已经说过了,究其本质,就是个配置文件。
boards/
重点在于boards/ 这个目录,它是你当前使用板子的定义目录,我们下面要重点讲解这个目录内的东西,洗耳恭听喵~
我们首先要区分两个东西,一个是主控芯片,一个是主控芯片的外设。主控芯片有很多种例如MCU,MPU,SoC,而外设则是其他的被主控控制的外围设备。为什么要区分这些呢?在STM32 HAL或者是其他官方库的开发中,我们很多的时候都是配IO,然后根据外设类型,然后去手写驱动,亦或是移植驱动。同时操作标准外设都是使用库当中给的函数。
例如在STM32 HAL当中,想驱动一块ST7789的屏幕,你首先需要配置SPI,接着通过SPI向芯片内部写入手册里面描述的寄存器值,进而驱动屏幕,同时为了方面调用,你会把写寄存器值的操作封装成成体系的函数,以方便快速在屏幕上绘制出东西。
而在Zephyr当中,驱动这个ST7789的方式是类似的,首先你要在设备树当中定义你的外设接的哪些引脚,对外设进行配置,声明它是个SPI设备(旧版Zephyr可以这么做,教程用的最新版需要声明为mipi_dbi),同时将其与Zephyr当中已经写好的相关驱动进行绑定,在这里与st7789v绑定。之后操作屏幕的时候,只需要调用Zephyr统一的API接口,即可完成对屏幕的操作。由于API接口是统一的,因此即使更换屏幕,原来代码当中写的显示可以无缝衔接到新的屏幕上面,只需要修改一下设备树即可喵~
接着我们回归boards/ 目录。这个目录里面放的文件如下所示。xxx的意思是这个是可以替换成任何名字的,以你自己的板子为准去命名即可。
| 文件 | 作用 | 是否必须 |
|---|---|---|
xxx.dts |
主设备树。描述板级硬件:LED、晶振、USB、串口、I2C、SPI 等外设节点 | 必须 |
xxx-pinctrl.dtsi |
引脚复用定义。把芯片引脚(PAxx/PBxx)映射到具体外设功能,如 PA11=USB_DM、PA12=USB_DP | 几乎必须(芯片必须支持引脚复用矩阵,否则不需要) |
xxx_defconfig |
板级默认 Kconfig。定义该板子的默认功能开关,如 CONFIG_SERIAL=y、CONFIG_CLOCK_CONTROL_STM32_CUBE=y |
必须 |
Kconfig.xxx |
板级 Kconfig 符号定义。注册 BOARD_ST_H743_BOARD 这个选项,让 Kconfig 菜单能识别到你的板子 |
必须 |
Kconfig.defconfig |
Kconfig 默认值补充。通常用来根据条件(如 SoC 系列)设置默认选项 | 必须,但是内容可以为空 |
board.cmake |
烧录配置。定义 OpenOCD/J-Link/pyOCD 的烧录参数,CLion 的 Flash 和 Debug 按钮靠它工作 | 必须 |
board.yml |
板子元数据。west boards 枚举用,包含板子名称、架构、支持的文档链接等 |
必须 |
support/openocd.cfg |
OpenOCD 调试脚本。定义芯片型号、调试接口、复位策略等,被 board.cmake 引用 |
如果你用 OpenOCD 烧录/调试就保留;用 pyOCD 或 J-Link 可以删掉 |
眼力优秀的同学应该能够注意到,我省略了一级arm目录,在这里特别说明一个东西。我们之前说,boards/ 是当前使用板子的定义目录,在Zephyr源码当中,也有一个boards目录,这里面放的是各个原厂板子的板级定义。我这里的位置是/home/misaki/MisakiCodes/Env/zephyrproject/zephyr/boards。如果里面恰好有你手里的板子,那么你可以直接使用现成的board,不过初学还是更建议自己写一整套板级定义。
再说省略的arm目录是什么。Zephyr 通过一个CMake变量 BOARD_ROOT 来找到你自定义的板级定义(这个变量你需要写在CMakeLists.txt当中),按 boards/<arch>/<board_name>/ 的固定结构扫描你的板子。这里的arm就对应着arch,也就是主控架构。
3. 在IDE当中一步步完成自定义板子的项目配置
Zephyr的学习非常的奇妙,你会花费大量的时间用于研究配置,初学的时候非常的陡峭,但是一旦熟悉之后,你的效率将会飙升,同时获得对底层的掌控。
根据我们上面所了解的那么多,我们来一步步完成属于你自己板子的自定义项目配置。依旧还是从点灯开始学起,但是后续所有的教程不会涉及到任何通信协议的具体内容,我们只讨论Zephyr本身及其应用方面。
在Zephyr的源码目录当中,有这么一个目录samples,这里面存放的都是Zephyr的官方例程,我们后续的部分内容会在其基础上完成。
现在将zephyr/samples/basic/blinky这个目录,单独复制出来到你自己放Zephyr项目的目录下,这里我修改了目录名字同时放在了/home/misaki/MisakiCodes/zephyrproject/blinky-learn。
这个目录的内容默认是这样的。
❯ eza --tree
.
├── boards
│ └── nrf54h20dk_nrf54h20_cpuppr.overlay
├── CMakeLists.txt
├── prj.conf
├── README.rst
├── src
│ └── main.c
└── tests.yaml
这几个东西没有用,直接无脑删掉会干净一些,分别是README.rst,tests.yaml,以及boards下的nrf54h20dk_nrf54h20_cpuppr.overlay。顺带一提,nrf54h20是一颗多核 SoC。
之后的目录内容是这样子的。
❯ eza --tree
.
├── boards
├── CMakeLists.txt
├── prj.conf
└── src
└── main.c
接着在这个项目的目录下面创建CMakePresets.json文件。并且向其中写入以下内容,下面会说这些内容或字段都是干什么的。
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"cacheVariables": {
"BOARD": "xxx",
"Python3_EXECUTABLE": "/home/misaki/MisakiCodes/Env/zephyrproject/.venv/bin/python3",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"NO_BUILD_TYPE_WARNING": "ON",
"CMAKE_C_COMPILER": "/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc",
"CMAKE_CXX_COMPILER": "/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-g++"
},
"environment": {
"ZEPHYR_BASE": "/home/misaki/MisakiCodes/Env/zephyrproject/zephyr"
}
},
{
"name": "zephyr-debug",
"inherits": "base",
"binaryDir": "${sourceDir}/cmake-build-debug-zephyr",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "zephyr-release",
"inherits": "base",
"binaryDir": "${sourceDir}/cmake-build-release-zephyr",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "zephyr-debug-build",
"configurePreset": "zephyr-debug"
},
{
"name": "zephyr-release-build",
"configurePreset": "zephyr-release"
}
]
}
先说几个外层的字段。
| 字段 | 值 | 作用 |
|---|---|---|
version |
3 |
指定 JSON Schema 版本,对应 CMake 3.21+。 |
configurePresets |
数组 | 定义配置阶段的预设,即运行 cmake 时的参数集。 |
buildPresets |
数组 | 定义构建阶段的预设,即运行 cmake --build 时的参数集。 |
内层里面这边我们主要关注base这个。它是一个基础预设,被设置为了隐藏,不会在CMake的配置里面出现,它的作用是设置一些环境变量,然后作为基础预设被其他具体的配置预设继承。
| 字段名 | 值 | 作用 |
|---|---|---|
name |
base |
预设的名称,用于被其他预设通过 inherits 字段引用继承。 |
hidden |
true |
隐藏该预设。设为 true 后,此预设不会出现在 IDE(如 CLion)的预设选择列表中,但依然可以被其他预设继承,通常用作基础模板。 |
generator |
Ninja |
指定 CMake 的生成器。Ninja 是一个专注于构建速度的构建工具,相比 Make 会更快。 |
| 变量名 | 值 | 作用 |
|---|---|---|
BOARD |
xxx |
Zephyr 专用:指定目标硬件开发板(如 stm32f4_disco)。此处为占位,实际开发需替换。 |
Python3_EXECUTABLE |
/home/.../python3 |
指定 Python 解释器的绝对路径。Zephyr 构建脚本依赖 Python,这里指向了项目的虚拟环境。 |
CMAKE_EXPORT_COMPILE_COMMANDS |
ON |
设为 ON 时,CMake 会在构建目录生成 compile_commands.json 文件。该文件用于编辑器(VS Code / CLion)实现代码补全、跳转和静态检查。 |
NO_BUILD_TYPE_WARNING |
ON |
Zephyr 专用:用于抑制 CMake 关于“未设置构建类型”的警告。因为 Zephyr 有自己的构建类型管理机制,无需依赖标准的 CMAKE_BUILD_TYPE。 |
CMAKE_C_COMPILER |
/home/.../arm-zephyr-eabi-gcc |
指定 C 编译器的绝对路径。这里用的是 Zephyr SDK 中的 ARM 交叉编译器,用于生成嵌入式 ARM 平台的机器码。 |
CMAKE_CXX_COMPILER |
/home/.../arm-zephyr-eabi-g++ |
指定 C++ 编译器的绝对路径,同样使用交叉编译链。 |
| 变量名 | 值 | 作用 |
|---|---|---|
ZEPHYR_BASE |
/home/.../zephyr |
Zephyr 最核心的环境变量:指向 Zephyr 源码树的根目录。Zephyr 的 CMake 脚本和构建系统依赖此变量来查找内核、驱动、库文件等资源。 |
你需要根据你实际的Zephyr环境位置,修改Python3_EXECUTABLE,CMAKE_C_COMPILER,CMAKE_CXX_COMPILER,ZEPHYR_BASE变量,同时修改BOARD变量的值,BOARD的值你可以自定义,例如我这里的板子是H743我就起名叫st_h743_board。记住这个BOARD的值,后面要用的喵!
[!TIP]
注意喵!这里使用的CMake生成器是Ninja,这是一个专注于构建速度的构建工具,正常你使用的IDE应该自带,如果没有就手动装一下就可以。这里自己问AI去吧喵~如果找不到就增加一个
CMAKE_MAKE_PROGRAM变量,里面的值写入你安装的Ninja的位置就可以喵~
接着来简单修改一下CMakeLists.txt的配置内容。
默认的CMakeLists.txt的内容是。
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(blinky)
target_sources(app PRIVATE src/main.c)
先修改一下cmake最小版本为3.30,大于3.21即可。
接着在cmake_minimum_required下面加上以下内容。
# 指定 board 搜索路径为当前应用目录下的 boards/
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) # 这个也可以放在CMakePresets.json当中
为什么要写这个呢?因为Zephyr对于板级目录的搜索方式是这样的。
Zephyr的构建系统会维护一个 BOARD_ROOT 列表,按以下顺序搜索。
| 优先级 | 路径来源 | 说明 |
|---|---|---|
| 1 | 用户显式指定的 BOARD_ROOT |
CMakeLists.txt 里 set(BOARD_ROOT ...) 或 -DBOARD_ROOT=... |
| 2 | ZEPHYR_BASE(内核源码) |
默认包含, Zephyr自带的官方板子在这里 |
| 3 | ZEPHYR_EXTRA_MODULES 中的模块 |
某些外设模块也会带自己的板子定义 |
由此可见,想让你自定义的板级配置被构建系统搜索到就需要设置BOARD_ROOT的值。
同时对于你自定义的板级目录的内容,是有一定的结构要求的。
Zephyr不会随便扫所有子目录,它只认下面的固定结构。
${BOARD_ROOT}/
└── boards/
└── <arch>/ ← 架构目录:arm, riscv, x86, posix...
└── <board_name>/ ← 板子目录名 = BOARD 的值
├── board.yml ← 元数据
└── ... ← dts, defconfig, cmake 等文件
在这里,BOARD_ROOT被设置为了CMAKE_CURRENT_SOURCE_DIR,也就是IDE的当前目录位置。而我们当前目录下面就有boards这个子目录,因此就会被Zephyr的构建系统找到。
那么也就是说,除了在CMakeLists.txt当中增加关于BOARD_ROOT的配置,还需要在boards目录下面按照固定结构创建一些文件夹。
我这里使用的是arm,板子名字是st_h743_board,那么我的目录结构最终是这样的。
❯ eza --tree boards
boards
└── arm
└── st_h743_board
你需要根据你自己的主控芯片架构,和你之前自定义的板子的名字来完成相关目录的新建。我们接下来创建的板级配置文件都在st_h743_board的下面。对于部分配置文件的名字,Zephyr是有要求的,我们也在前面介绍过。主要就是板子名字这个,有的文件是要带上板子的名字的。
所有需要创建的文件有:board.cmake, board.yml, Kconfig.defconfig, Kconfig.xxx, xxx_defconfig, xxx.dts, xxx-pinctrl.dtsi。所有的xxx都可以替换你之前设置的BOARD值的内容。同时,注意你的主控芯片是否有引脚映射这个东西,如果没有的话,xxx-pinctrl.dtsi这个文件是不需要创建的,不过目前大部分芯片都具有这个功能。
这里我没有创建这个support/openocd.cfg,在我们烧录的时候我们再来考虑是否创建这个文件。
接下来是最为重要的一些内容,我们会先了解刚刚创建的这么多文件当中的大部分,接着初步了解设备树要怎么写,完成点灯实验所需的设备树。最后完成对程序的编译和烧录。
board.yml
这个文件是板子的元数据,作用是能够让west boards能列出你的板子,同时CMake也会用这个文件来验证板子。内容按下面的来写,这个配置文件支持写注释。
[!TIP]
west是Zephyr项目的官方命令行工具,可以理解为Zephyr的项目管家。主要完成
- 多仓库管理。
Zephyr不是单仓库,内核、HAL、模块分散在不同 Git 仓库。west通过west.yml统一拉取、更新、切换版本- 构建封装。本质上是
CMake的包装器- 烧录与调试。调用OpenOCD、J-Link、pyOCD等工具烧录固件
board:
name: st_h743_board # 必须和目录名完全一致,其实也就是你之前设置的那个BOARD的值
full_name: ST H743 Custom Board
vendor: st # 厂商缩写(st、nordic、espressif 等)如果不确定名字叫什么就tree一下这里面的内容zephyr/soc
socs:
- name: stm32h743xx # 这个板子用的 SoC 型号,如果不确定名字叫什么就tree一下这里面的内容zephyr/soc
Kconfig.xxx
定义BOARD_ST_H743_BOARD这个配置项,让Kconfig系统知道世界上有这个板子。下面是模板。
# config BOARD_xxx 的 xxx 必须是大写的板子名(st_h743_board -> BOARD_ST_H743_BOARD)
# select SOC_xxx 必须对应你的芯片型号,这是绑定板子和 SoC 的关键,写法也是SOC_ + 大写的芯片型号名字
config BOARD_xxx
bool "ST H743 Custom Board"
select SOC_STM32H743XX
help
My custom STM32H743 board with USB, I2C, SPI, etc.
例如我的就写成这样。
config BOARD_ST_H743_BOARD
bool "ST H743 Board"
select SOC_STM32H743XX
help
My custom STM32H743 board with USB, I2C, SPI, etc.
Kconfig.defconfig
根据板子或SoC条件,设置一些Kconfig的默认值。通常用来处理 如果选了这块板子,就默认打开某某功能。这里暂时不需要加入什么有用的内容,可以先按我下面的来写。BOARD_ST_H743_BOARD这个来源于上面的那个Kconfig.xxx当中定义的板子名称。
if BOARD_ST_H743_BOARD
# 先留空
endif # BOARD_ST_H743_BOARD
# 这里用 if BOARD_ST_H743_BOARD 包裹,表示只有选了这块板子,这里面的一些默认配置才生效。
xxx_defconfig板级默认配置
这块板子出厂默认应该打开哪些功能。它和 prj.conf 的区别是:defconfig 是板子自带的默认值,prj.conf 是应用覆盖的。这个我们前面在介绍Zephyr的Kconfig系统的时候有说到。
这个文件的编写原则是,只放这块板子一定有的基础功能(时钟、串口、GPIO、调试),外设(I2C、SPI、USB、传感器)建议默认 n,由具体应用在 prj.conf 里开启,不要放应用相关的配置。
所有板子都共有的默认配置部分。
# Board 板子身份 你之前设置的BOARD_xxx,按你配置的大写的板子名称替换下面我写的这个即可
CONFIG_BOARD_ST_H743_BOARD=y
下面的Kconfig因为要顾及到不同的板子,所以需要稍微长篇大论一下。
先说一下怎么找到自己板子使用的芯片的配置。
❯ ls
arch doc kernel README.license snippets west.yml
boards drivers lib README.rst soc zephyr-env.cmd
cmake dts LICENSE REUSE.toml submanifests zephyr-env.sh
CMakeLists.txt include LICENSES samples subsys
CODE_OF_CONDUCT.md Kconfig MAINTAINERS.yml scripts tests
CODEOWNERS Kconfig.constants misc SDK_VERSION VERSION
CONTRIBUTING.rst Kconfig.zephyr modules share version.h.in
这个是Zephyr的源码,我们现在关注drivers,soc这两个目录。先了解一下他们的职责。
| 层级 | 代码位置 | 负责内容 |
|---|---|---|
| SoC 层 | zephyr/soc/st/stm32/stm32h7/ |
芯片最底层的启动代码,有复位向量表、系统时钟初始化、链接脚本、中断控制器配置、电源管理 |
| 驱动层 | zephyr/drivers/gpio/、drivers/serial/ 等 |
具体外设的操作,如GPIO 翻转、UART 收发、I2C 读写、SPI 传输 |
我们这里主要关心这两个目录里面有关Kconfig的部分。那么要如何找到你自己SoC的相关配置呢?
有很多种途径,第一种是使用Linux的grep命令即可,不会用grep可以让AI给你生成。
例如对于我的自定义板子来说,我可以进行以下的查找。
查SoC层面的配置信息。
❯ grep -r "config SOC_STM32H7" soc/st/stm32/
soc/st/stm32/stm32h7rsx/Kconfig.soc:config SOC_STM32H7R3XX
soc/st/stm32/stm32h7rsx/Kconfig.soc:config SOC_STM32H7R7XX
soc/st/stm32/stm32h7rsx/Kconfig.soc:config SOC_STM32H7S3XX
......
......
soc/st/stm32/stm32h7x/Kconfig.soc:config SOC_STM32H7B3XXQ
查驱动层的配置信息。
❯ grep -r "config GPIO_STM32" drivers/gpio/ --include="Kconfig*"
drivers/gpio/Kconfig.stm32:config GPIO_STM32
❯ grep -r "config UART_STM32" drivers/serial/ --include="Kconfig*"
drivers/serial/Kconfig.stm32:config UART_STM32
drivers/serial/Kconfig.stm32:config UART_STM32U5_ERRATA_DMAT_AFFECTED
drivers/serial/Kconfig.stm32:config UART_STM32U5_ERRATA_DMAT_LOWPOWER
drivers/serial/Kconfig.stm32:config UART_STM32U5_ERRATA_DMAT_NOCLEAR
查到之后如果不知道配置是什么意思,可以到官网提供的一个查询入口来进行查找确认。你也可以在里面输入板子主控的型号前缀去查找配置。在Zephyr当中,对于不同芯片的Kconfig往往都会有特别一些配置,这都是你需要自行查找的。不过好在我们生活在AI较为发达的时代,很多犄角旮旯的配置都可以提供询问AI来得以确认。这些配置我们也从来不需要去记忆,只需要知道,需要某个功能的时候,我应该怎么去查找到与其相关的配置信息。
刚才我们试图寻找属于自己板子芯片的配置,而在Zephyr当中,专属于某个SoC的配置隶属于厂商实现层。对于所有SoC都通用的配置,则属于通用层的配置,主要有下面这些。
| 子系统 | 通用层符号 | 功能 |
|---|---|---|
| GPIO | CONFIG_GPIO |
通用输入输出 |
| CLOCK | CONFIG_CLOCK_CONTROL |
时钟控制系统 |
| Serial/UART | CONFIG_SERIAL |
串口通信 |
| I2C | CONFIG_I2C |
I2C 总线 |
| SPI | CONFIG_SPI |
SPI 总线 |
| PWM | CONFIG_PWM |
脉冲宽度调制 |
| ADC | CONFIG_ADC |
模数转换 |
| DAC | CONFIG_DAC |
数模转换 |
| RTC | CONFIG_RTC |
实时时钟 |
| Watchdog | CONFIG_WATCHDOG |
看门狗 |
| DMA | CONFIG_DMA |
直接内存访问 |
| Flash | CONFIG_FLASH |
片内/片外 Flash 操作 |
| Sensor | CONFIG_SENSOR |
传感器抽象层 |
| Display | CONFIG_DISPLAY |
显示屏 |
| USB Device | CONFIG_USB_DEVICE_STACK |
USB 设备栈 |
| USB Host | CONFIG_USB_HOST_STACK |
USB 主机栈 |
| CAN | CONFIG_CAN |
CAN 总线 |
| Ethernet | CONFIG_ETHERNET |
以太网 |
| Bluetooth | CONFIG_BT |
蓝牙 |
| WiFi | CONFIG_WIFI |
WiFi |
| LED | CONFIG_LED |
LED 子系统(PWM/GPIO 驱动 LED) |
| Input | CONFIG_INPUT |
输入设备(按键、触摸屏) |
对于我们现在这个点灯的实验,我们应该先启用CONFIG_GPIO,同时根据你使用的SoC实际的型号,查询是否有与其相关的GPIO配置。对于我的STM32的芯片,我的最终板级别默认配置如下。
# Board
CONFIG_BOARD_ST_H743_BOARD=y
# GPIO
CONFIG_GPIO=y
# Clock
CONFIG_CLOCK_CONTROL=y
CONFIG_CLOCK_CONTROL_STM32_CUBE=y
# 允许调试器在 Sleep/Stop 下保持连接(H7 WFI 会关调试域,不开这个 SWD 必失联)
CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP=y
在这里我为我的STM32额外启用了CONFIG_CLOCK_CONTROL_STM32_CUBE与CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP,这些配置信息的功能都可以在Zephyr Kconfig Search当中找到。

board.cmake
Zephyr甚至把烧录也集成到项目当中了,这个文件用于告诉CLion/Zephyr,怎么把固件烧到板子上。更本质的说,是告诉CMake。Zephyr项目的构建都是围绕着CMake来的,项目最终生成出的CMake目标会包含多个,在这其中有一个叫做flash的,使用这个目标,就可以把固件烧录到你的板子上面。但是你必须要在这个board.cmake当中告诉构建系统,要怎么去烧录。
我们可以在board.cmake当中注册一个或多个board runner,并且给其传参。核心语法是。
# 给某个runner传参数,这个需要你自己对相关烧写工具有所了解
board_runner_args(openocd "--cmd-pre-load=targets/stm32h7x.cfg")
# 引入该runner的模板
include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
# ${ZEPHYR_BASE}/boards/common这个目录下面都是相关runner的模板
多个board runner的情况是这样的,例如我的board.cmake。默认会使用第一个,不过写多个会多出很多CMake目标选项,一般写一个就差不多了
# keep first
board_runner_args(stm32cubeprogrammer "--port=swd" "--reset-mode=hw")
board_runner_args(openocd --cmd-post-verify "reset halt")
board_runner_args(openocd --target-handle=_CHIPNAME.cpu0)
# keep first
include(${ZEPHYR_BASE}/boards/common/stm32cubeprogrammer.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd-stm32.board.cmake)
你需要根据你实际使用的烧录器来完成这个文件的内容,如果拿不定主意,可以询问AI或是自行查询相关资料。
Zephyr支持的烧录工具在zephyr/scripts/west\_commands/runners/下面,常见的如下。
| Runner | 适用场景 | 典型芯片 |
|---|---|---|
openocd |
ST-Link、DAP-Link、FT2232 等调试器 | STM32、GD32、ESP32(部分) |
jlink |
Segger J-Link / J-Trace | 几乎所有 ARM Cortex-M |
pyocd |
CMSIS-DAP、ST-Link(通过 pyOCD) | ARM Cortex-M 全系列 |
stm32cubeprogrammer |
ST 官方工具(UART/USB/SWD) | STM32 全系 |
blackmagicprobe |
Black Magic Probe 调试器 | ARM |
dfu-util |
USB DFU 模式(bootloader) | STM32、nRF52、ESP32 |
nrfjprog |
Nordic 官方工具 | nRF52/53/91 |
esp32 |
esptool.py | ESP32 全系 |
uf2 |
UF2 拖拽烧录 | Raspberry Pi Pico、Adafruit 等 |
intel_cyclonev |
Intel FPGA | Cyclone V |
canopen |
CAN 总线烧录 | 特殊场景 |
xxx.dts与xxx-pinctrl.dtsi
到了最为关键的地方,我们现在来初步了解并学习关于设备树的知识,并且完成点灯实验相关设备树的编写。
先来介绍一下这两个文件,xxx.dts是板子的主设备树,xxx-pinctrl.dtsi是引脚复用定义同时还兼顾电气特性描述。
一般而言,大部分的板级描述都需要这两个文件,不过也有部分芯片因为引脚功能完全焊死,没有引脚复用而不需要考虑xxx-pinctrl.dtsi,但是这些芯片大部分人都不一定用得到。同时,xxx-pinctrl这种命名其实只是一个习惯上的约束,改成其他的也问题不大,了解完设备树语法之后就能明白为什么说只是习惯约束了。
下面我们来聚焦于设备树语法结构部分。
-
注释
在设备树当中的注释和
C Language当中的注释是一样的。即//与/* 我是注释喵 */,这样。//用于单行,/* */用于多行注释。不过尽量建议使用/* */而不是//,因为//不是标准dts语法,但在Zephyr当中你确实可以使用//。 -
DTS语法版本声明
对于
xxx.dts主设备树而言,必须在文件的顶部声明以下内容。/dts-v1/; // 这是文件头,告诉DTC,用的是DTS语法第一版。每个 xxx.dts 文件必须以这行开头,否则编译报错。 -
语句结束
在设备树当中,语句结束使用和
C Language一样的分号; -
根节点
设备树当中带了一个
树字,对于任何树来说,都要有一个根节点,设备树也不例外。看名字也能知道,设备树本质上是将芯片的外设通过树的方式描述。设备树的根节点为
/,和Linux的root一样。是整棵硬件描述树的起点。所有硬件信息都必须挂在它下面。同时设备树对于节点的内容以
{}来区分,这和C Language是类似的。那么什么叫节点的内容呢?看下面的代码。/ { // 在这里面写的内容,是隶属于根节点/的 };根节点的名称
/是规定的,其他节点的命名一般可以自定义,不过一般命名都会遵循一些惯例。
也还有其他的一些规定的节点名称,我们先不急,很快就会了解到了。 -
节点
节点是树的分支,可以表示一个硬件对象,也可以作为容器存放其他的节点。下面的
leds就是一个节点,它是根节点的子节点。它不代表具体的硬件,而只是作为一个容器来存放板子上所有的led节点的硬件信息。/ { leds { }; };总结一下是设备树的节点有两种角色。
角色 作用 硬件对象节点 对应真实硬件,有寄存器、中断、时钟 容器节点 不对应真实硬件,只是用来组织归类子节点 -
节点标签
节点名前可以加一个标签,用来在别处引用这个节点。
/ { leds: leds { }; };使用方式就是
标签名: 节点名,这个很有用。很快我们就会感受到。 -
节点属性
节点内部用属性来描述硬件细节。
属性值有很多种类型,先放一张表,后面我们会慢慢来理解表中的内容。
类型 DTS 写法 用途 string "Misaki H743 Board"名称、标识、版本 int <115200>波特率、频率、超时时间 array <0x40004400 0x400>地址+长度、多中断号 uint8-array [de ad be ef 12 34]MAC 地址、设备 ID、初始化序列 string-array "tx", "rx"给通道/资源起名字 boolean hw-flow-control;(无值)开关标志:存在即 true,不写即 false phandle <&usart2>指向另一个节点 phandles <&tx_pa2 &rx_pa3>引用多个节点 phandle-array <&gpioh 9 1>引用控制器 + 参数(GPIO/PWM/DMA) path &flash0 或"/soc/flash@8000000"通过路径指定设备 节点的属性和节点本身一样,有规定的属性,也可以自己自定义属性名。下面给出规定的属性名称。
属性名 规定来源 含义 能不能自定义值 compatibleDevicetree Spec 驱动匹配字符串 值可以自定义,但是没必要 regDevicetree Spec 可寻址资源(地址/长度) 值由硬件 datasheet决定statusDevicetree Spec "okay" 或"disabled"值不能乱填,只能这两个其中之一 interruptsDevicetree Spec 中断描述符 值由 NVIC表决定#address-cellsDevicetree Spec 子节点地址用几个 cell 值通常是 1 或 2 clocksZephyr/SoC 约定 时钟消费者 格式由 SoC绑定决定pinctrl-0Zephyr pinctrl 子系统 引脚配置 值引用 pinctrl节点modelDevicetree Spec 板子型号字符串 值自定义 我们会随着学习的深入不断了解这些规定的属性。
这里为了完成点灯实验,我们先来学习
compatible属性。来看下面完整的一段的.dts代码/dts-v1/; // 声明dts版本 / { leds { compatible = "gpio-leds"; led0: led_0 { }; }; };我们在
leds这个容器节点当中加入了compatible属性,并且设置属性内容为gpio-leds。同时我们还在leds下放置了一个子节点led_0,并且设置其标签为led0。在这里
led_0里面还没有内容,关于它的内容我们待会再说。在这里也能注意到compatible的属性值是string类型的。我们来说一下为什么要将
compatible属性的值设置为gpio-leds,这一点相当重要,gpio-leds不是空穴来风。在
Zephyr的驱动代码当中,正常是这个文件zephyr/drivers/led/led_gpio.c,在这里面有一个宏定义#define DT_DRV_COMPAT gpio_leds。接着在
zephyr/dts/bindings/led/gpio-leds.yaml当中,有一些配置信息。我们来说一下这两个文件是干什么的。我们在编译的时候,会运行
gen_defines.py这个python脚本,这个脚本位置在zephyr/scripts/dts/gen_defines.py。脚本负责实现的功能是去找binding,也就是找绑定关系。我们在自己的
.dts主设备树当中写下了compatible = "gpio-leds",gen_defines.py会找到gpio-leds.yaml,接着根据gpio-leds.yaml的内容验证你的设备树内容,然后将led_gpio.c驱动编译进内核,最终让你能够点亮LED。我们刚刚说
gen_defines.py会根据gpio-leds.yaml的内容来验证设备树的内容,这是个什么验证方法呢?来看一下gpio-leds.yaml的部分代码。description: | Group of GPIO-controlled LEDs. Each LED is defined in a child node of the gpio-leds node. compatible: "gpio-leds" child-binding: description: GPIO LED child node include: - name: led-node.yaml property-allowlist: - label properties: gpios: type: phandle-array required: true在
properties的下面,写着一个gpios,它的required项的值是true,required的意思是必须的,也就是对于声明了compatible为gpio-leds的容器节点下的子硬件节点内部,必须要有gpios这个配置,没有就会报错。那么回过头看我们刚才写的.dts配置,一眼丁真,我们没有这个配置,于是编译就会报错。'gpios' is marked as required in 'gpio-leds.yaml', but does not appear所以我们应该加上这个,那么要怎么加呢,先看我给的代码。
/dts-v1/; // 声明dts版本 #include <st/h7/stm32h743Xi.dtsi> // 在 8. 包含其他文件 当中会有说明 / { leds { compatible = "gpio-leds"; led0: led_0 { gpios = <&gpioh 9 GPIO_ACTIVE_LOW>; // 这里的&符号的作用见 9. 引用 }; }; };gpios的值是一个phandle-array,一共有三个元素,我们一会说说这些元素都是什么意思怎么来的。 -
包含其他文件(include)
dts支持导入其他文件,可以认为就是C/C++的#include。Zephyr实际用C预处理器处理#include,所有宏定义那些你几乎都可以使用。我们上面的代码当中就包含了一个头文件
#include <st/h7/stm32h743Xi.dtsi>,这个头文件里面有gpioh的定义,同时这个头文件本身又导入了其他的头文件。GPIO_ACTIVE_LOW这个也是被包括到了这个头文件当中。它的定义在zephyr/dt-bindings/gpio/gpio.h当中。GPIO_ACTIVE_LOW标志低电平有效。/** GPIO pin is active (has logical value '1') in low state. */ #define GPIO_ACTIVE_LOW (1 << 0) /** GPIO pin is active (has logical value '1') in high state. */ #define GPIO_ACTIVE_HIGH (0 << 0)gpioh的定义长这样。在stm32h7.dtsi当中。stm32h7.dtsi经过一些中间文件被st/h7/stm32h743Xi.dtsi导入。gpioh: gpio@58021c00 { compatible = "st,stm32-gpio"; gpio-controller; #gpio-cells = <2>; reg = <0x58021c00 0x400>; clocks = <&rcc STM32_CLOCK(AHB4, 7)>; };关于
gpioh的定义我们不会在这一节当中进行教学,我们会在设备树这一具体章节当中进行更加深入的学习。 -
引用
这是
dts最强大的语法,在根节点外,用&,修改已定义的节点,或是其他作用。引用的形式有两大类。
① 属性值里的引用(作为数据)
② 节点级的引用(作为操作对象)先来简单看看这两类。
① 属性值里的引用
属性值的引用。
gpios = <&gpioh 9 GPIO_ACTIVE_LOW>;这里面的&gpioh就是属性值引用的一种,隶属于引用 + 参数这一类型。具体什么意思呢?我们在节点属性这个部分有一张属性值类型表,在这里的这种写法是
phandle-array这个类型。用途是引用控制器 + 参数。这么写是为了描述我们定义的led_0硬件节点使用的gpio是ph9,同时默认低电平有效,即低电平LED亮。这么写有种函数传参的感觉。gpioh这个节点的定义当中有这么一段#gpio-cells = <2>;,这个是在说明引用gpioh的时候,需要向其传入两个参数。led_0 { gpios = <&gpioh 9 GPIO_ACTIVE_LOW>; /* 三个分别对应 * phandle cell 0 cell 1 * 控制器 引脚号 标志位 */ };ok,那么问题又来了,这两个参数
cell是哪里定义的,哪里规定一定要以这种顺序去写的?Zephyr还是通过.yaml配置文件进行binding。也就是在Zephyr的源码当中有一个配置文件,里面描述了当一个gpio节点有#gpio-cells这种属性的时候要怎么去处理,配置文件是gpio-controller.yaml。我们暂时不去展开讲,当前阶段知道就行。② 节点级的引用
这里我们先来简单了解一下。
如果你尝试打开过
stm32h7.dtsi这些Zephyr源码,ST工程师写好的关于STM32 H7芯片的片上外设dts定义,你会发现里面有很多硬件节点都有一个属性status = "disabled";。这个属性是用来描述节点的状态的,值为
disabled的时候,节点是不启用的,当我们引用这个stm32h7.dtsi头文件的时候,我们可以这么写来修改节点的属性来打开它。/ { /// }; &usart1 { status = "okay"; };usart1是串口节点,默认是disabled,这里重新修改属性值可以覆盖原来usart1的status属性的值。同时我们还能注意到,节点引用修改是写在根节点外面的。我们这里引用的
usart1是节点的标签。serial@40011000这个才是usart1节点的真实名字,十分建议使用节点的标签来引用节点。也就是说,引用节点修改的话要在根节点外面写,同时最好使用节点的标签。我们在后面深入学习设备树的时候会说明为什么。
-
aliases我们前面说过,根节点
/是规定的节点名称,这里我们要额外学习的aliases也是一个规定的节点名称。它的作用是给某个硬件节点起别名。这个节点位于根节点下面。先来说明这样一件事情,对于下面的代码。
/dts-v1/; #include <st/h7/stm32h743Xi.dtsi> / { leds { compatible = "gpio-leds"; led0: led_0 { gpios = <&gpioh 9 GPIO_ACTIVE_LOW>; }; }; };我们在实际的
.c代码当中要如何获取并使用led0呢?我们要通过完整路径来找到,即/leds/led0。这样看起来好像没有问题,可是如果某个硬件节点的路径很长要怎么办?aliases这个节点就是来解决这样的问题的。/dts-v1/; #include <st/h7/stm32h743Xi.dtsi> / { leds { compatible = "gpio-leds"; led0: led_0 { gpios = <&gpioh 9 GPIO_ACTIVE_LOW>; }; }; aliases { led0 = &led0; }; };像上面这么写,我们就可以在代码当中直接使用
led0来找到我们的led_0硬件节点了。 -
chosenchosen 是设备树里的一个特殊虚拟节点,位于根节点/下。它不对应任何物理硬件,而是用来存放系统级配置,也就是告诉Zephyr Kernel这个系统的重要资源用哪个设备。/dts-v1/; #include <st/h7/stm32h743Xi.dtsi> / { chosen { zephyr,sram = &sram0; zephyr,flash = &flash0; }; };sram0与flash0都被定义在我们导入的头文件当中,zephyr,sram与zephyr,flash,这些也是定义在了某个yaml文件当中,不过一般没必须去翻阅源码,因为常用的就那几个,下面给出常用的zephyr,xxx属性清单。属性名 用途 zephyr,console默认 printk/日志输出设备 zephyr,shell-uartShell 子系统的 UART zephyr,flash默认 Flash 设备(settings/NVS 用) zephyr,code-partition当前运行的代码分区(用于 FOTA) zephyr,sram系统主 RAM(链接脚本用) zephyr,dtcmDTCM 高速数据 RAM(H7 特有) zephyr,itcmITCM 指令 RAM zephyr,entropy硬件熵源(TRNG) zephyr,can-primary主 CAN 控制器 zephyr,can-secondary次 CAN 控制器 zephyr,bt-c2h-uart蓝牙 HCI UART zephyr,bt-mon-uart蓝牙 monitor UART zephyr,ieee802154802.15.4 射频设备 zephyr,ipcIPC 共享内存设备 zephyr,settings-partitionsettings 子系统专用分区 zephyr,mcuboot-button-gpiosMCUboot 进入恢复模式的按键 zephyr,mcuboot-led-gpiosMCUboot 状态 LED 对于自己的设备,你需要把默认使用的
Flash设备和主ram通过chosen来确定好。有关chosen的内容,我们会在后面揭开它的面纱。 -
xxx-pinctrl.dtsiSTM32的引脚默认是GPIO,如果我们使用的IO口要配置成usart或I2C等,一般需要确保它被正确配置为推挽输出或上拉、高速或低速。这通过pinctrl子系统完成。我们前面多次说明,不是所有的芯片都需要这个pinctrl,不过大部分的芯片都需要它。如何确认这一点有很多种方式,最快的方式是去看Zephyr源码的boards目录里面,同型号的芯片官方有没有为其写xxx-pinctrl.dtsi文件。pinctrl子系统除了可以进行引脚的电气特性配置,也可以完成引脚复用配置。我们当前完成点灯这个实验,并不需要去配置
xxx-pinctrl.dtsi的内容。我们将会在usart等片上外设中接触到pinctrl。 -
时钟
一般来说要让板子跑起来是必须要给时钟信号的。如果你不去配置时钟,
Zephyr对于时钟树不复杂的芯片,会有默认的时钟配置。不过对于我手里面的STM32 H743 IIT6,它的时钟树相当的复杂,Zephyr不会为其完成默认时钟配置,需要开发者手动在设备树当中完成配置。因此我们在这里简单说一下时钟相关的配置。说之前先简单补充一下时钟树的概念。
STM32系列的外设,一般都会挂在不同的总线上。不同的总线频率不完全相同,需要的时钟频率也不完全相同。我的H7有APB和AHB这两种总线。不同的芯片时钟来源也有所不同,有的既有内部时钟也有外部时钟,有的只有外部时钟源。内外部还会细分成高速与低速时钟。能读到这里证明你肯定有
MCU基础,我就不阐述时钟信号是怎么从晶振起振到MCU启动了。因为总线频率不同,以及有时会有一些特别的外设要为其单独提供特别的时钟信号。因此从某个时钟源出发的信号,一定会被分出多个分支,最终在逻辑上演变成一棵树。时钟信号一般在树中的某些分支节点被降低频率或升高频率。
讲完了概念之后,我们来说明一下我们要怎么配置时钟。
基础的时钟节点一般都已经被预先定义好了,我们需要做的是引用它并修改它内部某些变量的值。先来看看下面我板子的时钟配置。
/* 时钟配置 */ &rcc { /* 系统时钟源选择 */ clocks = <&clk_hsi>; /* 选用内部高速时钟HSI作为系统时钟源 */ /* HSI 固定为 64MHz,由SoC dtsi中的&clk_hsi节点定义 */ clock-frequency = <64000000>; // 时钟频率 /* D1 域分频(内核 + AXI + APB3)*/ d1cpre = <1>; /* D1CPRE: CPU 内核时钟分频 */ /* 1 = 不分频,CPU = 64MHz / 1 = 64MHz */ hpre = <1>; /* HPRE: AXI/D1 总线时钟分频 */ /* 1 = 不分频,AXI = 64MHz / 1 = 64MHz */ d1ppre = <1>; /* D1PPRE: APB3 总线分频 */ /* 1 = 不分频,APB3 = 64MHz / 1 = 64MHz */ /* 覆盖外设:LTDC、WWDG */ /* D2 域分频(APB1 + APB2)*/ d2ppre1 = <1>; /* D2PPRE1: APB1 总线分频 */ /* 1 = 不分频,APB1 = 64MHz / 1 = 64MHz */ /* 覆盖外设:USART2/3, I2C1/2/3, SPI2/3, TIM2~7, DAC */ d2ppre2 = <1>; /* D2PPRE2: APB2 总线分频 */ /* 1 = 不分频,APB2 = 64MHz / 1 = 64MHz */ /* 覆盖外设:USART1/6, SPI1/4/5, TIM1/8/15~17, ADC1~3 */ /* D3 域分频(APB4)*/ d3ppre = <1>; /* D3PPRE: APB4 总线分频 */ /* 1 = 不分频,APB4 = 64MHz / 1 = 64MHz */ /* 覆盖外设:GPIOA~K, SYSCFG, EXTI, RTC 接口 */ };这里我们的最终任务只是点灯,因此不需要大费周章的配置时钟。
这里我配置了
rcc节点,这里面的属性看起来非常的复杂,导致这么复杂是因为STM32 H7的问题。在点灯这个示例当中,有的芯片甚至不需要重写这个
rcc里面的属性,就算要写也只需要制定一下clocks和clock-frequency的值即可。关于你的芯片的内部时钟节点叫什么名字,最快的方式就是去查dtsi头文件的内容。或者去询问AI,这也很方便。 -
电源
对于
STM32 H7这种高性能MCU,它的核心电源有三种不同的硬件方案,分别是走内部ldo,走内部smps,外部直接供电三种。一般都会选择内部ldo来为核心提供纯净的电源。一般很多芯片内部电源方案都是固定的,没有可选项,你不确定可以问一下
AI来查询一下。在我这里要这么写
dts来确定电源硬件方案。&pwr { power-supply = "ldo"; };
至此,我们已经初步学习了设备树的基础内容,但不是全部,不过这也足够我们点灯了。
最后给出我的STM32 H743IIT6点灯使用的完整dts代码。
/dts-v1/;
#include <st/h7/stm32h743Xi.dtsi>
/ {
chosen {
zephyr,sram = &sram0;
zephyr,flash = &flash0;
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpioh 9 GPIO_ACTIVE_LOW>;
};
};
aliases {
led0 = &led0;
};
};
&gpioh {
status = "okay";
};
/* 时钟配置 */
&rcc {
/* 系统时钟源选择 */
clocks = <&clk_hsi>; /* 选用内部高速时钟HSI作为系统时钟源 */
clock-frequency = <64000000>; // 时钟频率
/* D1 域分频(内核 + AXI + APB3)*/
d1cpre = <1>; /* D1CPRE: CPU 内核时钟分频 */
hpre = <1>; /* HPRE: AXI/D1 总线时钟分频 */
d1ppre = <1>; /* D1PPRE: APB3 总线分频 */
/* D2 域分频(APB1 + APB2)*/
d2ppre1 = <1>; /* D2PPRE1: APB1 总线分频 */
d2ppre2 = <1>; /* D2PPRE2: APB2 总线分频 */
/* D3 域分频(APB4)*/
d3ppre = <1>; /* D3PPRE: APB4 总线分频 */
};
&pwr {
power-supply = "ldo"; /* AMS1117-3.3 供电 */
};
你需要找到Zephyr源码当中你板子所用SoC的dtsi源码导入,接着完成与你的外设符合的dts配置。
我们甚至不需要修改main.c的任何内容,只要你给LED的标签是led0。
4. 项目编译与程序烧录
如果前面的都完成了配置,那么现在可以来进行点灯项目的编译与烧录了。
我们给出两种方式,一种是命令行,另一种是走Clion IDE打开项目图形化烧录。
a. 命令行
首先cd到你的blinky项目目录当中。在这个目录下面输入。--preset用于告诉CMake,使用当前目录下的CMakePresets.json的内容。zephyr-debug这是来自CMakePresets.json的内容,如果你写的和我写的不一样,是需要修改的。
cmake --preset zephyr-debug
预期的完美输出如下。
❯ cmake --preset zephyr-debug
Preset CMake variables:
BOARD="st_h743_board"
CMAKE_BUILD_TYPE="Debug"
CMAKE_CXX_COMPILER="/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-g++"
CMAKE_C_COMPILER="/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc"
CMAKE_EXPORT_COMPILE_COMMANDS="ON"
NO_BUILD_TYPE_WARNING="ON"
Python3_EXECUTABLE="/home/misaki/MisakiCodes/Env/zephyrproject/.venv/bin/python3"
Preset environment variables:
ZEPHYR_BASE="/home/misaki/MisakiCodes/Env/zephyrproject/zephyr"
Loading Zephyr default modules (Zephyr base).
-- Application: /home/misaki/MisakiCodes/zephyrproject/blinky-learn
-- CMake version: 3.28.3
-- Found Python3: /home/misaki/MisakiCodes/Env/zephyrproject/.venv/bin/python3 (found suitable version "3.12.12", minimum required is "3.12") found components: Interpreter
-- Cache files will be written to: /home/misaki/.cache/zephyr
-- Zephyr version: 4.4.99 (/home/misaki/MisakiCodes/Env/zephyrproject/zephyr)
-- Found west (found suitable version "1.5.0", minimum required is "0.14.0")
-- Board: st_h743_board, qualifiers: stm32h743xx
-- Found host-tools: zephyr 1.0.1 (/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1)
-- Found toolchain: zephyr 1.0.1 (/home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1)
-- Found Dtc: /home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/hosttools/sysroots/x86_64-pokysdk-linux/usr/bin/dtc (found suitable version "1.7.0", minimum required is "1.4.6")
-- Found BOARD.dts: /home/misaki/MisakiCodes/zephyrproject/blinky-learn/boards/arm/st_h743_board/st_h743_board.dts
-- Generated zephyr.dts: /home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr/zephyr/zephyr.dts
-- Generated pickled edt: /home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr/zephyr/edt.pickle
-- Generated devicetree_generated.h: /home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr/zephyr/include/generated/zephyr/devicetree_generated.h
Parsing /home/misaki/MisakiCodes/Env/zephyrproject/zephyr/Kconfig
Loaded configuration '/home/misaki/MisakiCodes/zephyrproject/blinky-learn/boards/arm/st_h743_board/st_h743_board_defconfig'
Merged configuration '/home/misaki/MisakiCodes/zephyrproject/blinky-learn/prj.conf'
Configuration saved to '/home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr/zephyr/.config'
Kconfig header saved to '/home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr/zephyr/include/generated/zephyr/autoconf.h'
-- Found Ccache: /usr/local/bin/ccache (found suitable version "4.13.6", minimum required is "4.12")
-- Found GnuLd: /home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/arm-zephyr-eabi/bin/ld.bfd (found version "2.43.1")
-- The C compiler identification is GNU 14.3.0
-- The CXX compiler identification is GNU 14.3.0
-- The ASM compiler identification is GNU
-- Found assembler: /home/misaki/MisakiCodes/Env/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc
-- Using ccache: /usr/local/bin/ccache
-- Found gen_kobject_list: /home/misaki/MisakiCodes/Env/zephyrproject/zephyr/scripts/build/gen_kobject_list.py
-- Configuring done (5.8s)
-- Generating done (0.1s)
-- Build files have been written to: /home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr
如果有报错,建议先询问AI,这是当下最快最低成本解决问题的方案。如果AI也无法解决,可以评论留言,我会尽力而为为你解答。
接着编译即可。
cmake --build --preset zephyr-debug-build
预期的完美输出类似下面。不同的芯片肯定会有区别的。
❯ cmake --build --preset zephyr-debug-build
[1/149] Preparing syscall dependency handling
[3/149] Generating include/generated/zephyr/version.h
-- Zephyr version: 4.4.99 (/home/misaki/MisakiCodes/Env/zephyrproject/zephyr), build: v4.4.0-8453-g882fe0e39afb
[149/149] Linking C executable zephyr/zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 14504 B 2 MB 0.69%
RAM: 4272 B 512 KB 0.81%
ITCM: 0 B 64 KB 0.00%
DTCM: 0 B 128 KB 0.00%
EXTMEM: 0 B 256 MB 0.00%
SRAM0: 0 B 512 KB 0.00%
SRAM1: 0 B 128 KB 0.00%
SRAM2: 0 B 128 KB 0.00%
SRAM4: 0 B 64 KB 0.00%
SRAM3: 0 B 32 KB 0.00%
IDT_LIST: 0 B 32 KB 0.00%
Generating files from /home/misaki/MisakiCodes/zephyrproject/blinky-learn/cmake-build-debug-zephyr/zephyr/zephyr.elf for board: st_h743_board/stm32h743xx
最后就是烧录。用下面的命令。还记得之前在board.cmake写下的配置吗?在这里就发挥作用了。烧录的时候会选择你制定的烧录工具。烧录工具需要你自行安装,不会装问AI就可以。ST系列的可以安装配置CubeCLT。
ninja -C cmake-build-debug-zephyr flash
预计的结果。我这里使用的是stm32cubeprogrammer。
❯ ninja -C cmake-build-debug-zephyr flash
ninja: Entering directory `cmake-build-debug-zephyr'
[0/1] Flashing st_h743_board
WARNING: CMake flash target is deprecated, call west directly instead
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner stm32cubeprogrammer
reset after flashing requested
-------------------------------------------------------------------
STM32CubeProgrammer v2.20.0
-------------------------------------------------------------------
ST-LINK SN : 066EFF554949835087043512
ST-LINK FW : V2J46M33
Board : --
Voltage : 3.24V
SWD freq : 4000 KHz
Connect mode: Under Reset
Reset mode : Hardware reset
Device ID : 0x450
Revision ID : Rev Y
Device name : STM32H7xx
Flash size : 2 MBytes
Device type : MCU
Device CPU : Cortex-M7
BL Version : 0xD2
Opening and parsing file: zephyr.hex
Memory Programming ...
File : zephyr.hex
Size : 14.16 KB
Address : 0x08000000
Erasing memory corresponding to segment 0:
Erasing internal memory sector 0
Download in Progress:
[==================================================] 100%
File download complete
Time elapsed during download operation: 00:00:01.104
MCU Reset
Software reset is performed
到此完成烧录,观察现象即可。
b. Clion IDE
在Clion当中打开你的blinky项目,正常会弹出这样的界面。

关闭默认的Debug配置,或删除,接着启用zephyr-debug和zephyr-release这两个预设配置,他们来自CMakePresets.json。
之后点击确定,然后Clion会调用CMake来完成对项目的加载,报错的话就去问AI。
成功之后左边项目栏目应该会类似这样。

这里面有很多与你的芯片无关的源码目录,你需要将它们批量排除掉,按住键盘上的Ctrl用鼠标左键多选,然后右键->将目录标记为->已排除。如果你不知道哪些是有关的,哪些是无关的,也可以询问AI,或者全部保留。你的项目应该是叫blinky之类的,展开它即可。排除之后是这样的。

接着编译即可,点击小锤子就能编译。编译成功之后就是烧录。展开CMake生成的目标,选择flash。然后点击小锤子烧录就行。

最后就是观察运行现象。
回到main.c当中。其源码为。
/*
* Copyright (c) 2016 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS 1000
/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)
/*
* A build error on this line means your board is unsupported.
* See the sample documentation for information on how to fix this.
*/
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void)
{
int ret;
bool led_state = true;
if (!gpio_is_ready_dt(&led)) {
return 0;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
return 0;
}
while (1) {
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
return 0;
}
led_state = !led_state;
printf("LED state: %s\n", led_state ? "ON" : "OFF");
k_msleep(SLEEP_TIME_MS);
}
return 0;
}
我们在dts当中使用定义的aliases节点的内容可以使用DT_ALIAS宏来获取。然后通过GPIO_DT_SPEC_GET来获得led的相关配置。最后在main.c当中使用。具体的一些接口我们会在之后进行讲解。不过稍微有一些C Language的基础应该就能看懂代码意思。运行结果是LED亮1s,灭1s。

这颗白色的LED就是亮起的灯。
至此我们完成了点灯的实验。
Zephyr的点灯要比其他库复杂的多,并且光是学习个点灯就需要你掌握大量基础知识。学习路线之陡峭自不必说,不过一旦掌握,所得的收益是翻倍的。
十分建议在学习的过程中借助AI的辅助,但也切勿觉得可以全靠AI来完成,重要的是你自己能够理解明白。否则你会完全不知道你的AI在做些什么。