Misakiotoha
Misakiotoha
发布于 2026-08-05 / 401 阅读
1
0

Zephyr入门第三课——在IDE当中一步步完成自定义板子的项目配置

Zephyr入门第三课——在IDE当中一步步完成自定义板子的项目配置

一、Zephyr项目结构

[!IMPORTANT]
后续的一切自定义配置都要基于对较为标准的现代Zephyr项目结构的理解

因此这一部分的内容非常的重要

如果发现有不明白的名词,或是没有学过的技术栈,最好借助AI来进行学习

1. 前言

教程后续讲解使用的开发板,是我于一年前为实验室新来的学弟学妹们教学用,进而绘制的一块STM32H743IIT6​的板子,为了方便后续讲解,在这里附上其主要的原理图。
核心板原理图
SCH_Schematic1_1-Core_2026-07-26.png

底板原理图
SCH_Schematic2_1-Bottom_2026-07-26.png

[!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就可以进入配置界面。

esp-idf-menuconfig.png

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

mermaid.png

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 的 FlashDebug 按钮靠它工作 必须
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当中找到。

Kconfig 搜索.png

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这种命名其实只是一个习惯上的约束,改成其他的也问题不大,了解完设备树语法之后就能明白为什么说只是习惯约束了。

下面我们来聚焦于设备树语法结构部分。

  1. 注释

    在设备树当中的注释和C Language​当中的注释是一样的。即//​与/* 我是注释喵 */​,这样。//​用于单行,/* */​用于多行注释。不过尽量建议使用/* */​而不是//​,因为//​不是标准dts​语法,但在Zephyr​当中你确实可以使用//

  2. DTS语法版本声明

    对于xxx.dts主设备树而言,必须在文件的顶部声明以下内容。

    /dts-v1/;
    // 这是文件头,告诉DTC,用的是DTS语法第一版。每个 xxx.dts 文件必须以这行开头,否则编译报错。
    
  3. 语句结束

    在设备树当中,语句结束使用和C Language​一样的分号;

  4. 根节点

    设备树当中带了一个​字,对于任何来说,都要有一个根节点,设备树也不例外。看名字也能知道,设备树本质上是将芯片的外设通过树的方式描述。

    设备树的根节点为/​,和Linux​的root一样。是整棵硬件描述树的起点。所有硬件信息都必须挂在它下面。

    同时设备树对于节点的内容以{}​来区分,这和C Language是类似的。那么什么叫节点的内容呢?看下面的代码。

    / {
    	// 在这里面写的内容,是隶属于根节点/的
    };
    

    根节点的名称/是规定的,其他节点的命名一般可以自定义,不过一般命名都会遵循一些惯例。
    也还有其他的一些规定的节点名称,我们先不急,很快就会了解到了。

  5. 节点

    节点是树的分支,可以表示一个硬件对象,也可以作为容器存放其他的节点。下面的leds​就是一个节点,它是根节点的子节点。它不代表具体的硬件,而只是作为一个容器来存放板子上所有的led节点的硬件信息。

    / {
    	leds {
    
    	};
    };
    

    总结一下是设备树的节点有两种角色。

    角色 作用
    硬件对象节点 对应真实硬件,有寄存器、中断、时钟
    容器节点 不对应真实硬件,只是用来组织归类子节点
  6. 节点标签

    节点名前可以加一个标签,用来在别处引用这个节点。

    / {
    	leds: leds {
    
    	};
    };
    

    使用方式就是 标签名: 节点名,这个很有用。很快我们就会感受到。

  7. 节点属性

    节点内部用属性来描述硬件细节。

    属性值有很多种类型,先放一张表,后面我们会慢慢来理解表中的内容。

    类型 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" 通过路径指定设备

    节点的属性和节点本身一样,有规定的属性,也可以自己自定义属性名。下面给出规定的属性名称。

    属性名 规定来源 含义 能不能自定义值
    compatible Devicetree Spec 驱动匹配字符串 值可以自定义,但是没必要
    reg Devicetree Spec 可寻址资源(地址/长度) 值由硬件datasheet决定
    status Devicetree Spec "okay"​ 或 "disabled" 值不能乱填,只能这两个其中之一
    interrupts Devicetree Spec 中断描述符 值由NVIC表决定
    #address-cells Devicetree Spec 子节点地址用几个 cell 值通常是 1 或 2
    clocks Zephyr/SoC 约定 时钟消费者 格式由SoC绑定决定
    pinctrl-0 Zephyr pinctrl 子系统 引脚配置 值引用pinctrl节点
    model Devicetree 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,一共有三个元素,我们一会说说这些元素都是什么意思怎么来的。

  8. 包含其他文件(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的定义我们不会在这一节当中进行教学,我们会在设备树这一具体章节当中进行更加深入的学习。

  9. 引用

    这是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节点的真实名字,十分建议使用节点的标签来引用节点。

    也就是说,引用节点修改的话要在根节点外面写,同时最好使用节点的标签。我们在后面深入学习设备树的时候会说明为什么。

  10. 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硬件节点了。

  11. chosen

    chosen​ 是设备树里的一个特殊虚拟节点,位于根节点/​下。它不对应任何物理硬件,而是用来存放系统级配置,也就是告诉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-uart Shell 子系统的 UART
    zephyr,flash 默认 Flash 设备(settings/NVS 用)
    zephyr,code-partition 当前运行的代码分区(用于 FOTA)
    zephyr,sram 系统主 RAM(链接脚本用)
    zephyr,dtcm DTCM 高速数据 RAM(H7 特有)
    zephyr,itcm ITCM 指令 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,ieee802154 802.15.4 射频设备
    zephyr,ipc IPC 共享内存设备
    zephyr,settings-partition settings 子系统专用分区
    zephyr,mcuboot-button-gpios MCUboot 进入恢复模式的按键
    zephyr,mcuboot-led-gpios MCUboot 状态 LED

    对于自己的设备,你需要把默认使用的Flash​设备和主ram​通过chosen​来确定好。有关chosen的内容,我们会在后面揭开它的面纱。

  12. xxx-pinctrl.dtsi

    STM32​的引脚默认是GPIO​,如果我们使用的IO​口要配置成usart​或I2C​等,一般需要确保它被正确配置为推挽输出或上拉、高速或低速。这通过pinctrl​子系统完成。我们前面多次说明,不是所有的芯片都需要这个pinctrl​,不过大部分的芯片都需要它。如何确认这一点有很多种方式,最快的方式是去看Zephyr​源码的boards​目录里面,同型号的芯片官方有没有为其写xxx-pinctrl.dtsi文件。

    pinctrl​子系统除了可以进行引脚的电气特性配置,也可以完成引脚复用配置

    我们当前完成点灯这个实验,并不需要去配置xxx-pinctrl.dtsi​的内容。我们将会在usart​等片上外设中接触到pinctrl

  13. 时钟

    一般来说要让板子跑起来是必须要给时钟信号的。如果你不去配置时钟,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,这也很方便。

  14. 电源

    对于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项目,正常会弹出这样的界面。

Clion_Open.png

关闭默认的Debug​配置,或删除,接着启用zephyr-debug​和zephyr-release​这两个预设配置,他们来自CMakePresets.json

之后点击确定,然后Clion​会调用CMake​来完成对项目的加载,报错的话就去问AI

成功之后左边项目栏目应该会类似这样。

Choose in.png

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

Choose over.png

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

Clion flash.png

最后就是观察运行现象。

回到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。

点灯实验.jpg

这颗白色的LED就是亮起的灯。

至此我们完成了点灯的实验。

Zephyr的点灯要比其他库复杂的多,并且光是学习个点灯就需要你掌握大量基础知识。学习路线之陡峭自不必说,不过一旦掌握,所得的收益是翻倍的。

十分建议在学习的过程中借助AI​的辅助,但也切勿觉得可以全靠AI​来完成,重要的是你自己能够理解明白。否则你会完全不知道你的AI在做些什么。


评论