开发你的第一个nRF Connect SDK(NCS)/Zephyr应用程序

############ 本文更新于2025年2月22日,对齐nRF54L15DK和nRF Connect SDK v2.9.0 ############

Nordic有2套并存的SDK:老的nRF5 SDK和新的nRF Connect SDK(简称NCS),两套SDK相互独立,大家选择其中一套进行开发即可。一般而言,如果你选择的芯片是nRF51或者nRF52系列,那么推荐使用nRF5 SDK。如果你选择的是Nordic最新产品系列,比如nRF54,nRF53,nRF70或者nRF91系列,那么请选择nRF Connect SDK。还有一种特殊情况,虽然你选择的是nRF52芯片,但需要使用最新的技术或者特性,比如蓝牙测向,蓝牙Mesh,蓝牙低功耗音频,PAWR,Matter,FindMy,那么也需要使用nRF Connect SDK,换句话说,最新的技术或者特性,Nordic都只会在NCS上进行开发,而nRF5 SDK将进入维护阶段不再增加新的特性(如发现bug,会对其进行修复的)。关于nRF5 SDK的介绍,请参考这篇博文之前的博文,基本上都是基于nRF5 SDK进行阐述的,尤其是这篇:https://www.cnblogs.com/iini/p/9043565.html,详细讲解了nRF5 SDK开发环境的搭建,这里不再赘述。下面将只对NCS SDK进行阐述,以期让大家快速了解这个Nordic最新SDK,并尽快熟悉和上手。 

1. nRF Connect SDK介绍

nRF Connect SDK,简称NCS,是Nordic最新的SDK平台,该平台将支持Nordic所有产品线,包括蓝牙低功耗,蜂窝网,Wi-Fi,GPS,2.4G,蓝牙Mesh,Zigbee,Thread,Matter, Homekit, FindMy,ANT+,DECT NR+,PMIC等,换句话说,由于短距离无线网络和长距离无线网络共用同一个SDK,将使得你同时具备两种网络的开发经验,因为他们的框架是一样的,驱动是一样的,网络协议栈的编写风格也是相仿的。只要你熟悉了其中一种网络的开发,那么相关的经验可以迅速复制到新网络平台上。

nRF Connect SDK内嵌Zephyr RTOS,并沿用了Zephyr project的编译系统。Zephyr Project是Linux基金会推出的一个Apache2.0开源项目,版权非常友好,适合用于商业项目开发。Zephyr Project是一个合作社区,其产品就是Zephyr,具体包括Zephyr RTOS,Zephyr组件以及Zephyr编译系统等。Zephyr很多地方都模拟了Linux,比如使用了DeviceTree和Kconfig,对Linux很熟的同学,阅读Zephyr代码会感到很亲切的。经常有人问:为什么NCS要使用Zephyr RTOS?其实答案就蕴含在Zephyr Project的愿景中:The Zephyr™ Project strives to deliver the best-in-class RTOS for connected resource-constrained devices, built be secure and safe。Zephyr的愿景跟Nordic的产品策略高度吻合,这也是为什么Nordic要选Zephyr的主要原因。NCS SDK和Zephyr Project两者最大的区别有3个:一是owner不同,NCS SDK由Nordic负责,Zephyr SDK由Linux基金会负责。NCS开发中碰到的所有问题,Nordic都将负责解决。二是产品管理不一样,NCS SDK将由Nordic完成所有相关测试和考核,并符合Nordic产品开发,测试,发布和质量控制流程,因此NCS有自己的版本,并跟Zephyr版本控制解耦。三是NCS具有很多增强特性,比如Nordic特有的蓝牙链路层等。所以,从产品角度看,NCS SDK和Zephyr SDK是两套完全不同的SDK。但是,如果从代码角度看,那么NCS SDK和Zephyr SDK又基本上是一样的。在本文章的下面部分,在不引起混淆的情况下,经常会把NCS和Zephyr两个概念换着使用,因为本质上他们是一个东西。

NCS使用了CMake编译系统,并使用了大量Python脚本以辅助生成一些头文件,代码和hex,这些都大大增加开发的可移植性和便利性。

NCS SDK放在GitHub上,里面包含多个仓库,其主仓库(manifest)是nrf,同时还包含zephyr,mcuboot,nrfxlib,matter,hostap等其他仓库。NCS SDK可以同时在Windows,macOS和Linux上运行。

由于Zephyr和NCS的build系统是一样的,如果你正在学习Zephyr RTOS,那么也可以参考本篇文章,从NCS SDK开始学习Zephyr。由于NCS SDK新增了很多特性,比如图形化的DEBUG,丰富的例程,你会发现从NCS SDK学习Zephyr是一条便捷通道。

2. nRF Connect SDK开发环境安装和搭建

2.1 前置安装

在我们开始正式的开发环境配置之前,我们先需要下载如下工具:

2.2 VS Code开发环境搭建

Microsoft Visual Studio Code(VS Code)是Nordic推荐的开发nRF Connect SDK应用的跨平台IDE工具,所以nRF Connect SDK开发环境搭建都是在VS Code中进行的。

2.2.1 插件安装

首先下载相应的插件。打开VS Code,进入Marketplace,搜索“nrf”,然后选择“nRF Connect for VS Code Extension Pack”进行安装,一旦nRF Connect for VS Code Extension Pack安装成功,所有nRF插件都自动安装成功。目前Nordic开发了如下nRF插件:

 

安装成功后,界面如下所示:

2.2.2 Toolchain安装

nRF Connect SDK开发使用的Toolchain可以通过VS Code直接安装,上面的nRF Connect for VS code插件安装成功后,点击左边的插件图标,进入WELCOME面板,然后选择Manage toolchains,在右边的列表框中选择Install Toolchain,如下: 

根据你选择的nRF Connect SDK版本选择对应版本的Toolchain,比如nRF Connect SDK v2.9.0对应的Toolchain如下所示: 

然后开始下载Toolchain,如下: 

下载成功后,自动解压和安装,如下: 

如下为安装成功的提示: 

这里需要注意的是,Toolchain安装可以不用VPN,但使用VPN可以让安装更稳定更可靠。

2.2.3 nRF Connect SDK安装

由于nRF Connect SDK放在GitHub服务器上,下载和安装nRF Connect SDK的时候请一定要使用VPN,否则很有可能就会下载不完整或者失败。

上面的nRF Connect for VS code插件安装成功后,点击左边的插件图标,进入WELCOME面板,选择Manage SDKs,然后在右边列表框中选择Install SDK,如下: 

然后选择相应版本的nRF Connect SDK,如下: 

请注意,如果你是一个新用户,强烈建议你选择最新版本,即列表里面版本最高的版本,上面是v2.9.0,但是当你读到这时,最高版本有可能已经到v3.0.0,v4.0.0,甚至更高,请选择此时最高版本。

选择好版本后,然后选择SDK安装根目录,一般使用默认推荐的目录即可,如下。大家千万不要使用很长的目录作为安装根目录,否则在Windows上编译例子的时候,经常会碰到目录名太长的编译报错。 

然后VS code开始下载nRF Connect SDK,如下:

  

安装成功界面如下所示:

由于SDK放在Github上,经常碰到下载不完整的情况,而且这种情况又不会报错,为此我们可以通过下面的方式去检验nRF Connect SDK是否下载完整和正确,选择Manage west workspace,然后选择West Update,如下:

如果nRF Connect SDK已经完整下载并正确,此时在OUTPUT窗口,你将会看到下面的信息输出: 

如果nRF Connect SDK还缺少一些组件没有下载完整,此时在OUTPUT窗口,你将会看到类似下面这样的报错信息:

nRF Connect SDK和工具链安装成功后,都放在Windows如下目录里面:

 

可以看出,所有版本工具链都放在toolchains目录中,每个版本nRF Connect SDK放在以版本为名的目录中,比如v2.9.0。打开一个nRF Connect SDK目录,比如v2.9.0,可以看到nRF Connect SDK包含多个仓库,其中nrf是主仓库:

至此,nRF Connect SDK开发环境已经搭好。

2.2.4 百度网盘下载nRF Connect SDK和工具链(仅适用于Windows)

如前所述,nRF Connect SDK放在github上,下载经常出问题,为此我们把上面的nRF Connect SDK和工具链上传到了百度网盘,这样大家直接通过百度网盘就可以下载了,百度网盘链接:

进入目录“开发你的第一个NCS(Zephyr)应用程序”,选择相应的版本(推荐使用最新版本),直接把相应的压缩包下载下来并解压到前面提及的c:/ncs对应目录中。nRF Connect SDK直接解压,不需做其他额外配置。工具链解压到C:\ncs\toolchains目录后,还需更新文件C:\ncs\toolchains\toolchains.json,加入新安装的工具链,如下添加了nRF Connect SDK v2.9.0对应的工具链:

上面更新操作主要让nRF Connect VS code插件可以找到这个工具链,从而使用这个工具链。   

3. nRF Connect SDK工程概览

3.1 用CMake语法组织NCS工程

在介绍NCS工程之前,我们先看看大家熟悉的Keil工程,我们打开一个Keil工程,如下:

打开后,左边窗口会显示这个工程包含的所有文件,这些文件其实是通过如下窗口一个一个添加进去的:

Keil工程还需要添加包含目录,这个通过如下窗口实现的:

可以看出,Keil添加源文件和目录的方式比较直观,但缺乏灵活性,而且效率低下。

和Keil工程一样,nRF Connect SDK工程也需要添加源文件和目录,但它不是使用图形方式去添加源文件和目录,而是通过CMake命令或者函数来添加源文件和目录。具体来说,nRF Connect SDK工程是通过CMakeLists.txt表示的,比如zephyr\samples\hello_world\CMakeLists.txt: 

再比如nrf\applications\nrf_desktop的CMakeLists.txt文件:

咱们再仔细看看zephyr\samples\hello_world这个工程,它的CMakeLists.txt只添加了一个main.c文件,而且这个main.c文件只有一行打印语句,如下:

没有kernel和串口驱动的支持,上面的printf是跑不起来的,按照我们的经验来理解,一个工程也不可能只有一个main.c,实际上当我们把这个工程编译后,我们发现这个工程包含了很多文件,如下:

这些文件是怎么添加进来的呢?这里面就会用到后面会讲的Kconfig配置,Kconfig其实就是一大堆宏(define),这些宏都有默认值,我们的kernel/串口驱动等文件就是因为Kconfig默认使能了,从而默认装载了。以上面zephyr\drivers\serial\uart_nrfx_uarte.c文件为例,文件目录zephyr\drivers\serial下面也有一个CMakeLists.txt文件,内容如下:

可以看出,当CONFIG_UART_NRFX_UART这个Kconfig使能的时候,系统就会自动装载uart_nrfx_uart.c,当你选择Nordic板子编译这个工程时,CONFIG_UART_NRFX_UART这个Kconfig就会默认使能,从而自动装载uart_nrfx_uart.c文件。Zephyr每个目录下面都有一个CMakeLists.txt文件,这许许多多的CMakeLists.txt文件和众多的Kconfig合在一起,让我们轻松便利地装载自己需要的文件或目录,从而构建出自己需要的工程,这种灵活性和可扩展性是Zephy非常大的一个特色

3.2 选择Board(板子)进行编译

使用Keil编译一个工程时需要选择Device,如下:

同样的,在NCS中你需要选择一个board(板子),选择板子,就是选择芯片,除此之外,开发板还规定了芯片的一些外设使能情况,以及一些基本外围电路连接情况,这个跟Keil选择Device的操作是异曲同工的,而且NCS这种做法更灵活,功能也更多,扩展性也更好。Nordic定义的所有板子放在目录:zephyr\boards\nordic,

以及nrf\boards\nordic: 

那某个具体的例子到底支持哪些板子呢?这个可以从例子自带的sample.yaml得知,比如上面的例子:nrf\samples\bluetooth\peripheral_lbs,打开sample.yaml文件,我们可以看到:

上面红框框起来的板子都是经过测试的,本例子肯定支持。至于红框中没有列出的板子,不能说就不支持,只能说没有测过。上面板子命名方式如何理解呢?以nRF54L15为例,它对应的其中一款板子编号为:nrf54l15dk/nrf54l15/cpuapp/ns,其中:

  • nrf54l15dk表示开发板的名称或者编号
  • nrf54l15表示开发板包含的芯片的型号或者名称,一个开发板有可能包含若干个芯片
  • cpuapp表示芯片的app核,nRF54L15除了包含一个Cortex-M33的内核,还包含一个RISC-V的FLPR核
  • ns表示non-secure(非安全域),表示app核的一个特殊应用:TF-M应用

这样我们在编译nrf\samples\bluetooth\peripheral_lbs这个例子的时候,选择nrf54l15dk/nrf54l15/cpuapp,编译后的工程就可以在nRF54L15上运行,如下:

编译的时候,每个应用都需指定其image的ROM起始地址和大小,以及运行时所占RAM的起始地址和大小,比如Keil,是在如下界面完成相关配置:

在NCS中,上述ROM和RAM信息也是放在board定义文件中,即下面所说的DeviceTree文件中,大家通过修改DeviceTree文件,就可以修改相应的ROM和RAM信息,比如:

这个是单image情况,在NCS中还有一种多image情况,即芯片需要同时跑多个image,最典型的例子就是同时跑bootloader和app,这种情况我们一般在partition文件里面指定ROM和RAM信息,如下:

我们后面会对pm文件进行详细阐述,这里有一个大概印象就好。

3.3 使用Kconfig和DeviceTree配置工程 

在Keil工程中,我们一般使用头文件来配置工程,比如下面的sdk_config.h:

为了方便查看这个头文件,点击下面的“Configuration Wizard”,如下:

这个工具一下子把一行一行的define(宏)图形化了,方便了我们找到相应的宏。

在NCS中,我们是通过两个头文件来完成上述同样的功能:autoconf.h和devicetree_generated.h,autoconfig.h长下面这样:

devicetree_generated.h长下面这样:

autoconf.h和devicetree_generated.h有点特殊,他们是编译系统(主要是Python脚本)编译生成的,因此这两个文件用户不能直接修改,用户只能通过这两个文件的输入文件去间接修改这两个文件。

autoconf.h是由许许多多的Kconfig文件和多个以.conf(比如prj.conf)为后缀的文件融合而生成的,通过文本编辑器就可以打开这些Kconfig和conf文件,比如nrf\samples\bluetooth\peripheral_lbs\Kconfig:

比如nrf\samples\bluetooth\peripheral_lbs\prj.conf:

Kconfig是宏定义的地方,而prj.conf等其他conf文件是引用宏或者给宏赋值的地方。比如宏CONFIG_BT_LBS_SECURITY_ENABLED,它在Kconfig变成config BT_LBS_SECURITY_ENABLED,表示宏在这里定义,宏只能定义一次,后面要引用它或者给它赋值,就不能再使用“config BT_LBS_SECURITY_ENABLED”这种格式了,而应该直接使用CONFIG_BT_LBS_SECURITY_ENABLED,比如你在prj.conf文件中给它赋值为No:CONFIG_BT_LBS_SECURITY_ENABLED=n。大家可以仔细比对一下,可以得知autoconf.h里面的条目跟Kconfig或者conf文件是一一对应的。Keil有图形化工具来配置头文件,Kconfig有没有相应的工具呢?答案是肯定的,我们会在后面详细阐述Kconfig原理和使用说明,下面是图形化工具的一个简图:

devicetree_generated.h是有许许多多的dts/dtsi文件和多个以overlay后缀(比如nrf54l15dk_nrf54l15_cpuapp.overlay)结尾的文件融合而生成的,通过文本编辑器就可以打开这些dts和overlay文件,前面提到的选择板子进行编译,板子的定义就是dts文件,也就是说板子的dts文件除了可以用来指定编译目标,也可以用来定义设备有关的宏,比如zephyr\boards\nordic\nrf54l15dk\nrf54l15dk_common.dtsi:

 

比如nrf\samples\bluetooth\peripheral_uart\boards\nrf54l15dk_nrf54l15_cpuapp.overlay:

dts/dtsi是宏定义的地方,而nrf54l15dk_nrf54l15_cpuapp.overlay等其他overlay文件是引用宏或者给宏赋值的地方。比如current-speed = <115200>,不管是在dts/dtsi文件,还是在overlay文件,他们的格式是一样的,都是current-speed = <115200>,但是只有在dts/dtsi定义过current-speed,你在overlay文件中才能引用它并对它重新赋值。至于current-speed = <115200>如何转换成#define DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed 115200,这个意义并不大,因为在我们的代码中,我们不是通过宏DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed来引用这个波特率的,而是通过DeviceTree API来获取波特率,这个DeviceTree API会最终输出DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed这个宏,所以我们不需要关心devicetree_generated.h这个文件,我们只需要关心DeviceTree的语法格式以及DeviceTree一些常用API就可以了。同样Nordic也提供图形化工具来帮助大家配置和修改DeviceTree,这个我们会在后面详细阐述,下面是工具的一个简图:

3.4 nRF Connect SDK几个重要目录

如前所述,nRF Connect SDK包含了多个仓库,每个仓库都是相互独立的,而且每个仓库包含的代码都很多,如果一行一行代码读下去,那将是一个无底洞。所以实际开发中,我们都是参考例子,按照例子去做,碰到不懂的API,再去看API说明,循环往复,最终完成自己的开发。

我们先说说applications目录,NCS中商业级的应用程序都放在:nrf\applications: 

如果你的应用跟上面的应用相似,那么推荐你直接参考上面的应用来开发,因为他们属于turn-key级的方案,非常接近商业级应用,你需要的开发工作量最少。

其次是如下例子目录nrf\samples,这个都是Nordic自己开发的一些例程: 

然后就是Zephyr自带的例子zephyr\samples:

大家有时候会觉得nrf或者zephyr例子还是不够多,比如很多驱动API怎么用,好像没有例子。其实Zephyr所有API的的使用,都可以在zephyr\tests下面找到示例,所以当你找不到例子的时候,不妨在这里找一找:

如果想绕开Zephyr RTOS,直接操作Nordic底层驱动nrfx API,那么可以参考modules\hal\nordic\nrfx\samples\src这里面的例子:

NCS里面包含众多API,到底该使用哪些API?API说明又在哪里?一般而言,我们只使用仓库里面的include目录下的API,API说明也在那里

比如nrf仓库的include目录:nrf\include

Zephyr标准API的include目录zephyr\include\zephyr:

其他仓库也是遵守这个规范的,比如Nordic开发的底层驱动API(与RTOS无关):modules\hal\nordic\nrfx\drivers\include 

注:有些人会问,modules\hal\nordic\nrfx\drivers\include和zephyr\include\zephyr\drivers两个目录里面的驱动API,我到底该使用哪个呢?zephyr\include\zephyr\drivers这个是Zephyr标准的驱动API,按照Zephyr标准来定义的,它调用了底层API:modules\hal\nordic\nrfx\drivers\include,modules\hal\nordic\nrfx\drivers\include这里面的API都是Nordic自己实现的,跟平台无关。所以说,一般推荐使用zephyr\include\zephyr\drivers这里面的API,只有这里面没有或者实现不了的功能(比如将同一个引脚动态分配给UART和SPI,Zephyr标准API就无能为力),这个时候才使用modules\hal\nordic\nrfx\drivers\include这里面的API。

4 运行你第一个nRF Connect SDK应用

大家对NCS工程有一个大概了解后,就可以动手跑一下例子,实际感受一下NCS工程。大家可以跑一下zephyr\samples\hello_world这个例子,基本上所有板子都会支持这个例子,这个例子会在串口助手里面打印出“Hello World!”,由于例子比较简单,我们就不在这里演示。

前面介绍nRF Connect SDK工程概览的时候,大部分时候我们是以nrf\samples\bluetooth\peripheral_lbs这个为例的,peripheral_lbs是一个经典的蓝牙跑马灯例子,大家也可以跑一下peripheral_lbs这个例子,来感受一下NCS工程。

4.1 运行蓝牙数据透传例子peripheral_uart

下面我会以nrf\samples\bluetooth\peripheral_uart这个为例来详细阐述NCS工程的组织,编译,运行和debug等,peripheral_uart就是闻名遐迩的蓝牙数据透传例子,这个例子支持Nordic所有蓝牙芯片,比如nRF52840/nRF52832/nRF5340/nRF54L15/nRF54H20,如前所述,你选择的板子不一样,编译的代码就不一样,如果你选择板子nrf52840dk_nrf52840,那么编译的程序就自动适配nRF52840芯片;如果你选择nrf54l15dk_nrf54l15_cpuapp,那么编译的程序就自动适配nRF54L15芯片......下面我们以nrf54l15dk_nrf54l15_cpuapp为例来编译和运行peripheral_uart例子(除了在选择Board target的时候不一样,其他板子跟它基本上一模一样)。

(注:如果你是中文Windows系统,编译nRF Connect SDK v2.9.0例子时,会报找不到板子错误,请将文件zephyr/scripts/list_boards.py第229行改成:"board yml.open('r'.encoding='utf-8')as f:",以解决这个编码错误

为了浏览代码方便,可以先通过VS Code的File -> Open Folder打开nRF Connect SDK v2.9.0目录,如下:

选择APPLICATIONS标签页,选择Open Existing Application,如下:

 

选择目录:nrf\samples\bluetooth\peripheral_uart,如下: 

打开上述项目后,点击“Add Build Configuration”,将自动跳出Add Build Configuration页面,在Board target列表框中,选择板子:nrf54l15dk_nrf54l15_cpuapp,如下: 

由于前面已经打开了nRF Connect SDK v2.9.0目录,这里自动帮你选好了SDK版本和Toolchain版本,你可以根据自己情况选择合适的SDK版本和Toolchain版本。

然后选择Configuration文件,如果留空,默认选择prj.conf,大家也可以选择其他配置项。 

其他一些配置项的说明如下所示: 

选择Build Configuration,开始编译,编译成功后,你将看到如下输出: 

整个VS code一览图如下所示: 

我们先把编译成功后的代码下载到板子中,看看板子运行效果。在点击ACTIONS窗口中的“Flash”按钮之前,请确保nRF54L15开发板已连入PC,连接的时候只要将一根USB type C电缆(手机充电线就可以)一头连PC,一头连板子的DEBUGGER USB口,即完成所有连线操作,后续不管是日志打印还是Debug,都可以通过这根USB电缆完成,连接效果图如下所示:

下载成功后,会有如下输出日志:

这个时候我们可以利用一个串口助手查看程序运行日志,比如Putty等,这里我们利用Nordic另一个VS Code插件:nRF Terminal来查看日志。首先点击下面红框里的图标:(这里选择COM4口,因为日志是从COM4口输出)

继续选择COM4,如下: 

选择波特率以及是否有流控:

为了后面演示方便,我们把nRF Terminal的模式切换为行模式,如下:

此时可以按一下USB口旁边的RESET按钮,让板子复位一下,此时我们可以看到:

打开手机版nRF Connect,此时我们可以看到如下广播:

点击“CONNECT”,连上后,我们将看到如下蓝牙服务: 

点击RX Characteristic旁边的箭头,发送“Hello, nRF54”,如下:

在nRF Terminal中,输入“Hello, phone”并回车,如下:

此时手机端界面如下所示: 

可以看出,手机把“Hello, nRF54”通过蓝牙传给了nRF54L15开发板,nRF54L15开发板把“Hello, phone”通过蓝牙传给了手机,实现了蓝牙的双向数据透传功能。

4.2 nRF Connect VS code插件功能介绍

下面我们看看nRF Connect插件的一些重要功能。

首先我们看一下ACTIONS窗口,这个窗口包含多个条目:Build/Debug/Flash/nRF Kconfig GUI等,每个条目其实包含多个功能,即点击条目不同地方,执行不同操作,这个大家一定要注意。

4.2.1 编译

编译的时候,一定要先选好工程,默认是build目录,即编译所有工程;展开build目录,下面会有多个工程,选择其中一个工程,则只编译这个工程。一般来说,我们推荐选择build目录,即编译所有工程。当把鼠标放在ACTIONS窗口中的Build条目,我们可以看到Build条目包含多个功能,当鼠标点击最左端图标,表示小编译,即只编译改动部分;当鼠标点击“循环箭头”,表示大编译,即把原来的编译目录删掉重新编译工程;当鼠标点击“三个点”图标,跳出一个配置菜单。

4.2.2 烧写

和编译一样,Flash之前一定要选好工程,一般来说,建议直接选择Build目录,而不要选择下面的具体工程,这样点击一下就可以把所有工程都烧进去,点击Flash条目不同地方,呈现不同功能,如下:

如前所述,在执行Build和Flash操作之前,选择APPLICATIONS窗口的build目录,即所有工程。但在执行Debug操作,查看Kconfig以及查看设备树时,请先选择build目录下面一个具体的工程,这个在多工程情况下尤为重要。

4.2.3 调试代码

在调试代码之前,请先确保编译的时候选择了debugging优化级别:(选择debugging优化级别,可以看到更多debug信息,当然你不选择debugging优化级别也是可以的)

选好debugging优化级别后,重新编译,编译成功后点击ACTIONS窗口中的“Debug”条目,进入Debug页面,如下:

这里要特别强调一下,nRF Connect支持查看每个线程的call stack,这个功能真是太棒了,如下所示,每个线程的call stack显示得一清二楚:

而且我们还可以查看每个线程的状态,欲查看每个线程的状态,ctrl+shift+p进入Command Palette,选择“nRF Debug: Focus on Thread Viewer View”,如下:

然后,你将看到:

我们也可以查看存储器里面的内容,即Flash和RAM里面的原始内容,查看方法是:ctrl+shift+p进入Command Palette,选择“nRF Debug: Focus on Memory Explorer View”,如下:

大家可以把鼠标放在各个存储器地址上试一下,可以看到这个地址对应的符号,点击“Show Symbol in File”,可以跳到符号定义的地方,如下:

如果要查看芯片某个外设的情况,在Peripherals窗口区找到要查看的外设,比如UARTE20,展开即可看到它的运行情况,如下:

点击代码行左侧,即可添加断点: 

除了可以添加普通断点,还可以添加条件断点。首先选择要加条件断点的代码行,然后ctrl+shift+p进入Command Palette,选择“Debug: Add Conditional Breakpoint”,如下: 

跳出一个“表达式”窗口, 假设我们要实现一个功能,当blink_status变量大于10时,自动停在main.c的dk_set_led这行,为此,我们可以输入表达式:blink_status > 10,即可完成预定的条件断点功能,如下: 

调试的时候,我们经常会碰到一种情况:不覆盖出问题板子的固件,直接使用有问题的image调试,这个时候,我们就不能直接点击Debug条目来进入调试界面,而是要调出上下文菜单,选择“Debug without Flashing”,如下:

除了使用VS code内带的调试功能,我们还可以使用外部的Ozone来调试代码(请确认Ozone软件已经安装并且有DK连到PC),直接点击“Debug with Ozone”条目,系统自动打开此工程的Ozone调试界面,大家就可以使用Ozone来调试这个工程了,如下:

4.2.4 代码浏览

VS code自带的搜索功能默认搜索的范围是整个NCS目录下所有的文件,这种搜索效率比较低,如下:

 nRF Connect插件内置的搜索功能则只会在指定的文件夹内搜索,大幅提升了搜索效率,尤其对于Linux和MacOS这种路径长度不受限的系统来说,我们可以直接选择工程的顶级目录,这样就相当于在当前工程中进行搜索。Windows由于路径名长度的255字节限制,我们只能选择下面一些子目录来搜索,如下:

比如我们搜索“bt_enable”这个函数,结果如下:

VS code有一个Go to Definition(代码跳转)功能,对应的快捷键为:ctrl+鼠标左键单击,如下:

 

nRF Connect插件也集成了这个功能,这样Go to Definition就可以在当前选择的工程中跳来跳去,省去了搜索操作,极大方便了用户代码阅读。比如查看k_sleep的定义,它直接跳到如下界面:

Go to Definition不仅可以用来看代码,也可以用来看conf文件,比如查看prj.conf里面的“CONFIG_LOG=y”,如下:

4.2.5 查看Kconfig文件

如前所述,每一个NCS工程包含多个kconfig和conf文件,这些文件最终会合并成一个文件.config。我们现在来查看一下peripheral_uart这个工程的Kconfig文件,首先在Applications窗口选择“peripheral_uart”工程,然后展开如下的Kconfig目录,我们的确可以看到peripheral_uart工程包含了很多Kconfig和conf文件,大家可以一一点击这些文件以实际看看他们长什么样。

这些Kconfig和conf文件最终会合并成一个.config文件,它位于如下位置:

前面说过,这个.config文件又会转成autoconf.h,最终通过define(宏定义)的方式在代码中发挥作用,如下:

从上可以看出,“CONFIG_LOG=y”转换成了“#define CONFIG_LOG 1”。

前面也提到过,用户不能直接通过编辑autoconf.h或者.config文件来修改宏定义的值,用户只能修改Kconfig或者conf文件来间接达成这个目标。一般而言,是通过修改prj.conf文件来达成目标,但是prj.conf是一张空白纸,用户对NCS工程里面包含的CONFIG定义又不熟,以至于很多用户无从下手。其实,用户可以通过Kconfig图形工具来查看和编辑Kconfig文件,目前NCS支持nRF Kconfig GUI和menuconfig(或者guiconfig)来查看和编辑Kconfig文件。下面我们实际演示一下nRF Kconfig GUI的使用。

首先,在Applications窗口选择“peripheral_uart”工程,然后点击actions窗口的“nRF Kconfig GUI”条目,然后点击上面红圈的“Toggle Panel”按钮,关闭下面窗口,并拖动下面红圈的横线,把下面的信息窗口拖大一些,如下:

我们查看一下当前工程的日志配置情况,输入“log”,跳出如下界面: 

点击“Jump to Item”,我们将得到Logging模块所有配置,如下:

展开Backends菜单,我们可以看到peripheral_uart工程是使用RTT Viewer作为日志输出控制台。

我们可以打开RTT Viewer来验证一下,我们可以直接使用VS code里面的nRF Terminal来做RTT viewer。首先,点击下面的“connect”图标:

然后选择你的J-link设备,成功后,我们将看到如下日志:

关于RTT Viewer,有一点需要大家注意一下,每次复位之后,新日志有可能不会自动显示在屏幕上,此时需要重新连接一次RTT Viewer,新日志才会显示出来。后面我们会详细讲解如何编辑Kconfig文件以生成自己需要的conf文件。

4.2.6查看DeviceTree文件

如前所述,每一个NCS工程包含多个dts/dtsi和overlay文件,这些文件最终会合并成一个文件zephyr.dts。我们现在来查看一下peripheral_uart这个工程的DevieTree文件,首先在Applications窗口选择“peripheral_uart”工程,然后展开如下的Devicetree目录,我们的确可以看到peripheral_uart工程包含了很多dts/dtsi和overlay文件,大家可以一一点击这些文件以实际看看他们长什么样。

这些dts/dtsi和overlay文件最终会合并成一个zephyr.dts文件,它位于如下位置:

 前面说过,这个zephyr.dts文件又会转成devicetree_generated.h,最终通过define(宏定义)的方式在代码中发挥作用,如下:

从上可以看出,uart20的波特率最终定义成了115200。

前面也提到过,用户不能直接通过编辑devicetree_generated.h或者zephyr.dts文件来修改宏定义的值,用户只能修改dts/dtsi和overlay文件来间接达成这个目标。一般而言,是通过修改app.overlay(或者<board name>.overlay)文件来达成目标,但是app.overlay(或者<board name>.overlay)是一张空白纸,用户对Devicetree语法又不熟,以至于很多用户无从下手。其实,用户可以通过Nordic Devicetree Visual Editor来查看和编辑Devicetree文件。下面我们实际演示一下Devicetree Visual Editor的使用。

首先,在Applications窗口选择“peripheral_uart”工程,然后点击actions窗口的“Devicetree”条目,或者直接点击左边工具栏的“Devicetree Visual Editor”图标,如下:

我们查看一下uart20的波特率,如下: 

如果要修改Devicetree文件,请确保本工程根目录下有一个app.overlay文件(适用所有板子),或者boards目录下有板子自己的<board>.overlay文件(上面peripheral_uart工程就是采用这种方式),否则你将修改系统的DeviceTree文件,将影响系统里其他工程。后面我们会详细讲解如何编辑DeviceTree文件以生成自己需要的overlay文件。 

4.3 west命令行方式开发环境介绍

除了可以使用nRF Connect插件来编译和烧写NCS工程,用户也可以使用west命令来编译和烧写NCS工程。west命令需要在专门的命令行环境中执行,目前有两种方式可以打开nRF Connect SDK的命令行环境,一是直接使用VS code里面的nRF Connect终端,二是使用nrfutil命令打开操作系统自带的终端。

打开nRF Connect终端方式如下:

成功界面如下所示:

通过nrfutil命令可以直接打开操作系统自带的终端并包含nRF Connect SDK工具链环境,在shell中输入如下命令:

1
nrfutil toolchain-manager launch --ncs-version v2.9.0 --terminal

上述命令执行成功后,我们将看到:

一个典型的west命令如下所示:

west build -b nrf54l15dk/nrf54l15/cpuapp
注:-b指定板子,这里使用nRF54L15开发板

下面我们还是以nrf\samples\bluetooth\peripheral_uart工程为例来详细讲解west命令使用方式,并使用Windows的CMD终端来执行west命令。

首先,进入工程所在的目录:

cd C:/ncs/v2.9.0/nrf/samples/bluetooth/peripheral_uart

输入如下命令编译此工程:

west build -b nrf54l15dk/nrf54l15/cpuapp -p

编译成功后,使用如下命令将代码下载到板子中:

west flash 

大家可以使用GDB命令或者Ozone进行Debug调试,Ozone调试界面如下所示。感兴趣的同学可以自己去摸索和实践,这里就不再赘述。

你可以使用menuconfig来查看Kconfig文件,menuconfig对应命令为:

west build -t menuconfig

执行上述命令后,将显示如下界面,大家可以按照界面提示去查看或者临时修改配置值。

4.4 编译输出目录一览

不管采用VS Code还是west命令行,他们底层的工作逻辑是一样的,所以最终编译生成的内容是一样的。VS Code或者west编译成功后,将在NCS工程根目录下生成一个build目录,所有编译有关的内容都将放在这个目录下。下面我们大概介绍一下这个目录的结构和内容。

上面的peripheral_uart例子编译成功后,将在工程根目录下生成一个build目录,build目录结构如下:

 如果使能了MCUboot的话,那么build目录会包含一个app工程目录和一个mcuboot工程目录,以及升级用的zip文件,如下: 

可以看出,build目录下面有几个重要的文件或者目录

  • 各个子工程的编译输出目录,比如上面的peripheral_uart目录,mcuboot目录等,每个子工程所有编译生成文件都放在这个目录下
  • build.ninja,ninja项目描述文件
  • CMakeCache.txt,CMake项目描述文件
  • partitions.yml,芯片存储区分区文件,详细描述每个子工程放在NVM那个区域,使用那个区域的RAM
  •  merged.hex,所有工程对应的映像文件将合并成一个merged.hex文件,用于直接烧写代码
  • dfu_application.zip,升级用的zip包,将这个压缩包传给手机或者服务器以传输升级文件

我们再看一个具体子工程目录的结构和内容,比如peripheral_uart目录,我们主要查看它下面的zephyr目录的内容,如下:

大家可以看到这个目录下有很多重要文件:

  • .config,Kconfig生成的最终文件,此工程所有的Kconfig和.conf文件最终将合并成这个.config文件,通过查看.config文件就知道此工程最终配置符不符合你的期望
  • zephyr.dts,DeviceTree生成的最终文件,此工程所有的dts/dtsi文件和.overlay文件最终将生成这个zephyr.dts文件,通过查看zephyr.dts文件就知道此工程最终配置符不符合你的期望
  • zephyr.bin,此子工程编译生成的image
  • zephyr.map,此子工程的map文件

5. 修改nRF Connect SDK工程配置文件

每一个NCS或者Zephyr工程都包含了非常多的配置项或选项,总数有可能达几千项之多,然而绝大多数配置项我们都不需要去管他们,只需要使用默认值即可,所以开发一个简单的NCS应用程序,有可能我们不需做任何配置,就可以跑起来。当你开发复杂的应用程序的时候,又可以利用NCS的Kconfig/Devicetree配置系统,非常方便又灵活地配置你的工程。很多人会问,既然Kconfig和DeviceTree都可以配置NCS工程,那我要定义一个宏的时候是放在Kconfig里面还是DeviceTree里面?Kconfig主要负责软件的配置,DeviceTree主要负责板子硬件的配置。Devicetree有严格的语法要求,它能定义的宏是预先规定好的并且非常有限,一般来说,我们直接引用现有Devicetree配置文件里面的宏定义,而不额外添加新的宏定义。换句话说,新的宏定义我们一般都放在Kconfig文件里面

如3.3节所述,Kconfig配置系统最终通过autoconf.h在代码中发挥作用,Devicetree配置系统最终通过devicetree_generated.h在代码中发挥作用,但是autoconf.h和devicetree_generated.h有点特殊,他们是编译系统(主要是Python脚本)编译生成的,因此这两个文件用户不能直接修改,用户只能通过这两个文件的输入文件去间接修改这两个文件。autoconf.h是由许许多多的Kconfig文件和多个以.conf(比如prj.conf)为后缀的文件融合而生成的,通过文本编辑器就可以打开并编辑这些Kconfig和.conf文件。devicetree_generated.h是有许许多多的dts/dtsi文件和多个以.overlay(比如app.overlay)为后缀的文件融合而生成的,通过文本编辑器就可以打开并编辑这些dts/dtsi和overlay文件

5.1 使用prj.conf和nRF Kconfig GUI修改Kconfig配置

如前所述,每个模块里面的Kconfig文件是宏定义的地方,而例子目录里面的prj.conf等其他.conf文件是引用宏或者给宏赋值的地方。还是以前面提及的logging backend配置为例,打开nRF Kconfig GUI,找到这个配置,并跳到它定义的地方,如下:

 然后我们可以看到这个Kconfig定义在如下目录:

 我们打开如下的Kconfig.uart文件:

如前所述,“config LOG_BACKEND_UART”表示定义了一个宏:CONFIG_LOG_BACKEND_UART,这个宏在prj.conf里面有如下赋值:

也就是说,日志打印UART后端关闭了,我们现在将其打开,如下:

我们一般不在nRF Kconfig里面直接修改Kconfig的值,如下:

如上所示,点击“Apply”之后,修改保存在.config文件里面,这是一个临时文件,每次执行大编译,你这里的修改就作废,所以我们一般都在prj.conf里面修改。当然,你可以不点击“Apply”而直接点击“Save to File”,然后选择prj.conf文件(你选择nrf54l15dk_nrf54l15_cpuapp.conf文件也是一样的,两者的区别是prj.conf适用于所有板子,而nrf54l15dk_nrf54l15_cpuapp.conf仅适用于nrf54l15dk_nrf54l15_cpuapp),这样nRF Kconfig会自动把刚才的修改保存到prj.conf里面,如下:

注:目前nRF Kconfig在显示peripheral_uart工程的日志配置时有一点问题,它没有显示出UART后端配置项,如下所示,哪怕LOG_BACKEND_UART没有使能,它也应该显示出来:

而且RTT_CONSOLE明明使能了,它显示为没有使能。如下,.config文件显示RTT_CONSOLE使能了:

碰到这种情况怎么办呢?一切以生成的.config文件为准,它说使能了就使能了,它说没有使能就没有使能,哪怕你的Kconfig或者prj.conf或者nRF Kconfig显示的值跟它不一样。如果nRF Kconfig显示的值跟.config不一样,一般是nRF Kconfig出问题了;如果是Kconfig或者prj.conf设置的值跟最后生成的.config不一样,这个时候很有可能在其他模块或者其他地方设置了其他值,或者你的设置依赖没有打开,导致你的设置无效,大家只要在下面的Config files目录搜索相关配置值,就可以找到冲突的地方:

除了系统模块可以定义Kconfig文件,你也可以在自己的工程目录里面定义自己的Kconfig文件,如何定义?依葫芦画瓢,仿照例子来即可。记住,在NCS或者Zephyr里面,只要可以用文本编辑器打开的文件,他们的语法都是直接可读的,不需要你另外去学习他们,直接仿照例子,你就可以定制自己的内容。

peripheral_uart工程除了prj.conf文件外,还有其他.conf文件,比如下面这些.conf文件:

 跟prj.conf文件不一样,这些.conf文件的命名都是用户自己定的,因此Zephyr编译系统不会自动包含它们,如果你需要把这些.conf文件包含到自己的工程中,需要手动包含他们,如下:

除了prj.conf文件外,boards目录下面的以<board>.conf命名的.conf文件也是Zephyr标准conf文件,Zephyr编译系统可以自动找到他们,并和prj.conf里面的配置一起合并,需要注意的是,<board>.conf优先级高于prj.conf当一个宏同时在prj.conf和<board>.conf里面赋值,以<board>.conf里面的赋值为最后结果。请记住,目录名必须是“boards”,里面的.conf文件必须以板子名字来命名,如下:

最后,请记住prj.conf里面的配置适用于所有板子,而<board>.conf仅适用于本board对应的工程

下面我们实际演示一下prj.conf配置修改,我们将日志的UART后端打开,如下:

然后重新编译并下载代码到板子中,打开串口助手,我们发现日志的确输出到串口中了:

5.2 修改Devicetree配置 

如前所述,每个仓库的boards和dts目录下定义了很多板子,这些板子是由dts/dtsi文件来描述的,dts/dtsi是宏定义的地方,如下:

而工程目录中的.overlay文件是引用宏或者给宏赋值的地方,如下:

以上面的current-speed为例,不管是在dts/dtsi文件,还是在overlay文件,他们的格式是一样的,都是current-speed = <数值>,但是只有在dts/dtsi定义过current-speed,你在overlay文件中才能引用它并对它重新赋值。至于current-speed = <115200>如何转换成#define DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed 115200(位于devicetree_generated.h文件中),这个意义并不大,因为在我们的代码中,我们不是直接通过宏DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed来引用这个波特率的,而是通过DeviceTree API来获取波特率,这个DeviceTree API会最终输出DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed这个宏,所以我们不需要关心devicetree_generated.h这个文件,我们只需要关心DeviceTree的语法格式以及DeviceTree一些常用API就可以了。

5.2.1 Devicetree语法概述

我们还是以nrf\samples\bluetooth\peripheral_uart\build\peripheral_uart\zephyr\zephyr.dts为例来看看dts语法:

Devicetree语法的核心就是节点(node),一个dts文件包含多个节点,这些节点再组织成一个树形结构,树形结构的根就是“/”,换句话说,“/”下面包含多个节点,节点内又可以包含子节点,如下为uart@c6000节点定义:

复制代码
              uart20: uart@c6000 {
                compatible = "nordic,nrf-uarte";
                reg = < 0xc6000 0x1000 >;
                interrupts = < 0xc6 0x1 >;
                status = "okay";
                endtx-stoptx-supported;
                frame-timeout-supported;
                current-speed = < 0x1c200 >;
                pinctrl-0 = < &uart20_default >;
                pinctrl-1 = < &uart20_sleep >;
                pinctrl-names = "default", "sleep";
            };
复制代码

一般而言,一个节点定义包含如下要素:

  • 节点ID,每个节点都有一个node identifier,Devicetree API一般都是通过节点ID来找到这个节点的。节点ID是由编译系统自动生成的,不体现在dts文件里面,但它是Devicetree操作的核心。
  • 节点名,如uart@c6000,这个是必选的,@是名字的一部分,@后面跟着地址,可以是物理绝对地址,也可以是相对地址。我们可以通过/soc/peripheral_50000000/uart_c6000找到这个节点(dts文件里面符号名可以大写和小写,但代码中一律转成小写;符号名可以包含, @ - 等特殊字符,但代码中这些特殊字符都转成了下划线符号_),对应Devicetree API就是DT_PATH(soc, peripheral_50000000, uart_c6000),这种方式获得node ID还是有点复杂,我们更喜欢用下面这种方式
  • nodelabel,如uart20,非必选。一般而言,带有nodelabel的节点才是一个真正意义上的节点,它一般会对应一个物理设备,比如一个芯片外设或者外挂的一个传感器等,访问这种节点时,你可以不用DT_PATH方式,而直接采用DT_NODELABEL方式,比如DT_NODELABEL(uart20),来获取这个节点ID。带nodelabel的节点是一个“独立的”节点,你可以在overlay文件中直接引用这个节点;而不带nodelabel的节点就不那么独立了,你必须通过路径的方式来引用它,也就是放在根节点“/”里面,如下:

    另外大家一定要分清nodelabel和属性“label”的区别,有些节点会有一个属性“label”,这个只是巧合,属性也叫label,如下:   这个label跟我们的nodelabel没有半点关系。 

  • 属性(property),如compatible,status,current-speed等,都是属性,所有属性都放在{ };中,属性的值就是我们要的宏定义值,得到节点ID后,我们就可以通过DT_PROP来获得属性值,比如DT_PROP(DT_NODELABEL(uart20), current_speed),这个API最终将展开成DT_N_S_soc_S_peripheral_50000000_S_uart_c6000_P_current_speed,即115200。

我们运用上面的Devicetree节点知识来解读一下下面的overlay文件:

可以看出,根节点下面包含了chosen,aliases和zephyr,user三个节点,chosen包含一个属性:nordic,pm-ext-flash, aliases包含三个属性:myi2c,myspi,myuart,zephyr,user包含两个属性:io-channels和extint-gpios。我们可以通过Devicetree API获得这些属性的值,比如DT_ALIAS(myuart)就可以得到uart30的node ID。一些人估计会困惑,我们不是可以直接通过DT_NODELABEL(uart30)来获得uart30的node ID,为什么还要多此一举?其实这个是为了适配不同的硬件,有的硬件是uart30,有的硬件是uart0,我们引入一个别名myuart,从而兼容所有硬件。chosen的作用跟aliases差不多,zephyr,user用来操作GPIO的。

节点里面有一个特别的属性:compatible,编译系统通过compatible属性值找到这个节点对应的binding文件,这个binding文件以yaml为后缀,compatible属性值为名,实际上节点里面可以定义哪些属性,每个属性的取值范围等都是由这个yaml文件决定的,如下:

还有一个非常特殊的属性status,status有“okay”和“disabled”两种取值,“okay”表示使能节点对应的设备,“disabled”表示关闭节点对应的设备,此时即使设备驱动代码已经装载,它还是无法工作。有时,在Zephyr自带驱动中,系统会根据compatible和status这两个属性来决定要不要装载相应的驱动程序。

节点是Devicetree语法的核心,一般而言“{ };”包起来的就是一个节点,节点一般对应一个物理设备,每个节点都有节点ID,得到了节点ID,就得到了操作设备的“句柄”,我们就可以利用Devicetree API去读取节点里面的属性值,从而完成对相应设备的操作。

5.2.2 使用<board>.overlay和Devicetree Visual Editor修改Devicetree配置

我们一般通过修改overlay文件来改变Devicetree默认配置,我们经常使用的有app.overlay和<board>.overlay,如下:

需要注意的是,<board>.overlay优先级高于app.overlay,当一个属性值同时在app.overlay和<board>.overlay里面赋值,以<board>.overlay里面的赋值为最终结果。按照Zephyr编译系统要求,<board>.overlay所在目录的名字必须是“boards”,<board>必须是板子名字,app.overlay放在工程根目录下,这样Zephyr编译系统才能自动找到这两个文件,并将其合并,否则你需要手动选择非标准命名方式的overlay文件。另外,请记住app.overlay适用于所有板子的工程,而<board>.overlay仅适用于本board对应的工程

实际中,我们经常结合Nordic Devicetree Visual Editor和参考代码一起来完成设备树相关配置,修改Devicetree配置前,请先确保在应用工程根目录下有app.overlay文件或者在boards目录有<board>.overlay文件,之后我们所有的修改都放在这个overlay文件,而不是直接修改系统里面定义的dts/dtsi文件。

我们现在实际演示一下如何修改uart20波特率,在Applications窗口选择“peripheral_uart”工程,然后点击actions窗口的“Devicetree”条目,或者直接点击左边工具栏的“Devicetree Visual Editor”图标,如下:

找到uart20波特率配置地方,如下: 

 确认编辑器选择了boards\nrf54l15dk_nrf54l15_cpuapp.overlay这个文件,并更改波特率为230400,如下:

此时打开源文件boards\nrf54l15dk_nrf54l15_cpuapp.overlay,我们可以看到Editor自动把修改保存到这个文件,如下:

保存,编译并下载代码到板子中,你就会发现修改生效了。

我们再看Devicetree Visual Editor几个其他示例,如下为更改SPI引脚的操作示例:

除了通过spi00外设去修改pin脚,我们也可以通过pinctrl节点去修改spi00引脚,两者的修改最终都会体现在overlay文件中。

如下为配置ADC channel的操作示例:

上面的channel@0和channel@1需要先在overlay文件中定义,然后才能呈现在Devicetree Visual Editor中。

前面提到过chosen,aliases和zephyr,user三个特殊节点,这三个特殊节点一般直接在overlay文件进行修改,如果你的zephyr.dts文件没有包含这3个节点,那么在Devicetree Visual Editor将看不到这三个节点,反之,你的overlay文件或者dts文件包含了这3个节点,那么Devicetree Visual Editor就可以查看并编辑他们,如下:

zephyr,user是一个非常实用的节点,当你需要随意定义一个property,又不想写相应的binding文件,就可以将其定义在zephyr,user节点中,我们一般在这个节点中定义中断,GPIO和ADC通道。

上述演示中会用到这个例子https://github.com/aiminhua/ncs_samples/tree/master/ble_comprehensive(或者使用gitee链接https://gitee.com/minhua_ai/ncs_samples/tree/master/ble_comprehensive)。 

Devicetree Visual Editor还有一个非常实用的功能,就是图形化查看每个引脚的定义,以及引脚定义有没有冲突,如下:

通过上面的图,我们可以一目了然的知道每个引脚的定义,这在查找引脚定义有没有冲突时非常有用。

最后提一下,Devicetree Visual Editor是用来帮助你熟悉Devicetree语法的,一旦你熟悉了Devicetree语法(实际上你把https://github.com/aiminhua/ncs_samples/tree/master/ble_comprehensive这个例子里面的用法掌握好就差不多了),就可以抛开Devicetree Visual Editor,直接在overlay文件里面修改配置,这样效率最高。

5.2.3 外设驱动和Devicetree API

如前所述,通过Kconfig和Devicetree,Zephyr系统在初始化kernel的时候会同时初始化每个使能的外设驱动,也就是说,在跑main()之前,外设驱动已经装载并初始化成功,用户在main()之后是看不到外设初始化代码的,无法对其进行初始化处理,用户需在Devicetree中配置外设驱动初始化参数。外设驱动程序放在zephyr\drivers目录,大家可以仔细研究一下每个外设驱动是如何装载和初始化的,下面我们以Nordic I2C外设驱动为例,简要介绍Zephyr设备驱动模型。

打开文件zephyr\drivers\i2c\i2c_nrfx_twim.c:

 可以看到这个驱动文件以及相关驱动是用Kconfig决定是否装载,然后通过Devicetree API定义了一系列常量,比如i2c设备,各种i2c设备初始化条目,以及常量初始化值,并通过kernel的z_sys_init_run_level()来自动调用i2c_nrfx_twim_init(),从而在kernel装载时自动完成i2c设备初始化工作。

I2C外设驱动初始化参数是直接从Devicetree配置中提取出来的,如下:

上面的I2C_FREQUENCY定义如下:

#define I2C_FREQUENCY(idx)      I2C_NRFX_TWIM_FREQUENCY(DT_PROP_OR(I2C(idx), clock_frequency,      \
                                   I2C_BITRATE_STANDARD))

可以看出,它直接提取下面clock-frequency的属性值:

I2C设备初始化成功后,就可以调用I2C传输函数了,如前所述,所有可供用户调用的I2C API都位于文件:zephyr\include\zephyr\drivers\i2c.h,比如i2c_write_dt/i2c_read_dt/i2c_write/i2c_read等,这些API最终都是调用i2c_transfer,i2c_transfer定义如下: 

api->transfer将回调如下函数:

仔细观察上述I2C操作API,你会发现他们都要求一个输入参数:const struct device *dev,即设备句柄,不仅是I2C,Zephyr所有设备都是通过设备句柄来引用和操作的,得到设备句柄是操作设备的第一步,一般而言,我们可以通过Devicetree API来获取设备句柄,比如DEVICE_DT_GET,它的定义如下:

可以看到这个API的输入参数就是节点ID,如前所述,节点ID可以通过DT_NODELABEL,DT_PATH,DT_CHOSEN,DT_ALIAS等获得,比如DT_NODELABEL定义如下:

这里要特别强调一下,这些Devicetree API都是大写的,因为他们本质上都是宏,而不是真正意义上的API,这些宏会在预编译阶段展开,如下面例子所示:

在开发nRF Connect SDK应用程序时,用到比较多的外设有:uart,spi,i2c,ADC,中断和IO口。下面我们将以nRF54L15DK为例,一步步详细讲解这几个外设的操作方式。

5.2.3.1 UART设备操作示例

假设我们要操作uart30,首先在overlay里面对它进行如下赋值:

&uart30 {
    status = "okay";
    current-speed = < 1000000 >;
    /delete-property/ rts-pin;
    /delete-property/ cts-pin;
    /delete-property/ hw-flow-control;
};

nRF54L15使用uart30,nRF52840有可能使用uart1,两个uart编号是不一样的,但是我们又想用同一个程序去兼容不同uart编号,为此我们定义如下uart别名:

aliases {
    myuart = &uart30;  
};    

这样在代码中我们可以引用myuart,而不同的板子可以对应不同的overlay文件,不同的overlay文件可以选择不同的uart编号,从而实现一个程序代码兼容不同uart外设。

uart设备句柄可以通过如下API获得:

const struct device *uart = DEVICE_DT_GET(DT_ALIAS(myuart));

uart发送函数示例如下:

struct uart_data_t *tx = k_malloc(sizeof(*tx));
uart_tx(uart, tx->data, tx->len, SYS_FOREVER_MS);

uart接收函数示例如下:

uart_rx_enable(uart, uart_rx_buf[0], sizeof(uart_rx_buf[0]), UART_WAIT_FOR_RX);

5.2.3.2 I2C设备操作示例

假设我们要操作i2c22,首先在overlay里面对它进行如下赋值:

复制代码
&i2c22 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;
    pinctrl-0 = < &i2c22_default >;
    pinctrl-1 = < &i2c22_sleep >;
    pinctrl-names = "default", "sleep";
    i2c_dev_0: i2c_dev_0@50 {
        compatible = "i2c-device";
        reg = <0x50>;
    };
    i2c_dev_1: i2c_dev_1@b {
        compatible = "i2c-device";
        reg = <0x0B>;
    };      
};
复制代码

由于I2C是总线,它下面需要外挂设备,这里我们假设外挂了i2c_dev_0(I2C从机地址为0x50)和i2c_dev_1(I2C从机地址为0x0B)。跟前面UART一样,为了兼容不同I2C外设,我们定义如下I2C别名:

aliases {
    myi2c = &i2c22; 
};    

I2C设备句柄可以通过如下API获得:

const struct i2c_dt_spec i2c_dev0 = I2C_DT_SPEC_GET(DT_NODELABEL(i2c_dev_0));
const struct i2c_dt_spec i2c_dev1= I2C_DT_SPEC_GET(DT_NODELABEL(i2c_dev_1));

I2C写函数示例如下:

i2c_write_dt(&i2c_dev0, (uint8_t *)&addr16, EEPROM_SIM_ADDRESS_LEN_BYTES);

I2C读函数示例如下:

i2c_read_dt(&i2c_dev0, pdata, size);

5.2.3.3 SPI设备操作示例

假设我们要操作spi21,首先在overlay里面对它进行如下赋值:

复制代码
&spi21 {
    status = "okay";
    compatible = "nordic,nrf-spim";
    pinctrl-0 = <&spi21_default>;
    pinctrl-1 = <&spi21_sleep>;
    pinctrl-names = "default", "sleep";
    cs-gpios = <&gpio2 10 GPIO_ACTIVE_LOW>, <&gpio2 8 GPIO_ACTIVE_LOW>;
    spi_dev_0: spi_dev_0@0 {
        compatible = "spi-user-define";
        reg = <0>;
        spi-max-frequency = <DT_FREQ_M(4)>;
    };
    spi_dev_1: spi_dev_1@1 {
        compatible = "spi-user-define";
        reg = <1>;
        spi-max-frequency = <DT_FREQ_M(8)>;
    };                  
};
复制代码

由于SPI是总线,它下面需要外挂设备,这里我们假设外挂了spi_dev_0(CS脚为P2.10)和spi_dev_1(CS脚为P2.08)。由于Zephyr系统自带的dts binding文件没有定义SPI子设备,我们还需要在工程根目录下面创建dts\bindings\spi-user-define.yaml,内容如下:

复制代码
#
# Copyright (c) 2022 Kumar Gala <galak@kernel.org>
#
# SPDX-License-Identifier: Apache-2.0
#

description: |
    This binding provides resources required to build and run in Zephyr.

compatible: "spi-user-define"

include: [spi-device.yaml]
复制代码

跟前面UART一样,为了兼容不同SPI外设,我们定义如下SPI别名:

aliases {
    myspi = &spi21; 
};    

SPI设备句柄可以通过如下API获得:

static struct spi_dt_spec spi_dev0 = SPI_DT_SPEC_GET(DT_NODELABEL(spi_dev_0), SPI_OP, 0);
static struct spi_dt_spec spi_dev1 = SPI_DT_SPEC_GET(DT_NODELABEL(spi_dev_1), SPI_OP, 0);

SPI传输函数示例如下:

spi_transceive_dt(&spi_dev0, &tx_set, &rx_set);

5.2.3.4 ADC设备操作示例

 假设打开ADC通道0和通道1,并将通道0映射到物理引脚AIN7,通道1直接接VDD,相关overlay配置如下:

复制代码
&adc {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;

    channel@0 {
        reg = <0>;
        zephyr,gain = "ADC_GAIN_2_5";
        zephyr,reference = "ADC_REF_INTERNAL";
        zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
        zephyr,input-positive = <NRF_SAADC_AIN7>;
        zephyr,vref-mv = <900>;
        zephyr,resolution = <10>;                    
    };
    
    channel@1 {
        reg = <1>;
        zephyr,gain = "ADC_GAIN_2_5";
        zephyr,reference = "ADC_REF_INTERNAL";
        zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
        zephyr,input-positive = <NRF_SAADC_VDD>;
        zephyr,vref-mv = <900>;
        zephyr,resolution = <10>;                
    };

};
复制代码

为了得到上面channel@0和channel@1两个节点里面的属性值,我们会在zephyr,user节点中定义io-channels属性,如下:

zephyr,user {
    io-channels = <&adc 0>, <&adc 1>;    
};

然后我们可以使用如下代码得到每个ADC通道的句柄:

#define DT_SPEC_AND_COMMA(node_id, prop, idx) \
    ADC_DT_SPEC_GET_BY_IDX(node_id, idx),

static const struct adc_dt_spec adc_channels[] = {
    DT_FOREACH_PROP_ELEM(DT_PATH(zephyr_user), io_channels,
                 DT_SPEC_AND_COMMA)
};

配置ADC通道的API见下:

adc_channel_setup_dt(&adc_channels[i]);

通过如下代码初始化ADC采样buffer并读取ADC采样值:

adc_sequence_init_dt(&adc_channels[i], &sequence);
adc_read_dt(&adc_channels[i], &sequence);

5.2.3.5 GPIO设备操作示例

我们先在zephyr,user中定义要操作的GPIO,如下:

zephyr,user {
    extint-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
};

通过下面代码获取GPIO设备句柄:

const struct gpio_dt_spec ext_int =
        GPIO_DT_SPEC_GET(ZEPHYR_USER_NODE, extint_gpios);

下面代码用来配置GPIO口:

gpio_pin_configure_dt(&ext_int, GPIO_INPUT | GPIO_PULL_UP);
gpio_pin_configure_dt(&ext_int, GPIO_OUTPUT);

操作GPIO口代码如下:

gpio_pin_set_dt(&ext_int, 0)
  gpio_pin_get_dt(&ext_int)

5.2.3.6 外部中断设备操作示例 

跟GPIO设备一样,我们先在zephyr,user中定义要操作的外部中断IO口,如下:

zephyr,user {
    extint-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>;
};

通过下面代码获取外部中断IO口设备句柄:

const struct gpio_dt_spec ext_int =
        GPIO_DT_SPEC_GET(ZEPHYR_USER_NODE, extint_gpios);

下面代码用来配置GPIO口:

gpio_pin_configure_dt(&ext_int, GPIO_INPUT | GPIO_PULL_UP);
gpio_pin_interrupt_configure_dt(&ext_int, GPIO_INT_EDGE_TO_INACTIVE);
gpio_init_callback(&ext_int_cb_data, ext_int_isr, BIT(ext_int.pin));
gpio_add_callback(ext_int.port, &ext_int_cb_data);

一旦外部中断发生,将执行如下ISR:

void ext_int_isr(const struct device *dev, struct gpio_callback *cb,
            uint32_t pins)
{
    LOG_INF("External interrupt occurs on pin 0x%x at %lldms", pins, k_uptime_get());  
    k_sem_give(&sem_i2c_op);    

 

我做了一个例子,除了包含上面的UART,SPI,I2C,ADC,中断,GPIO操作示例,它还包含QSPI,Flash访问,蓝牙OTA,蓝牙数据透传等操作示例,大家可以自己去研究一下这个例子,例子同时放在Github和Gitee上:

5.3 sysbuild和多image管理

有时一个应用会包含多个image,最典型的情况有三种:一是bootloader + app,bootloader一个image,app一个image,二是tfm + app,tfm一个image,app一个image(注:tfm是trustzone体系里面的安全image,用于装载非安全应用app),三是双核,应用核一个image,网络核一个image(比如nRF5340的应用核和网络核)。在NCS中,编译一个工程,会同时生成多个不同image,这种工程就称为multi-image工程。

传统的SDK,如果一个芯片包含多个image,那么每个image都会对应一个工程,用户需要同时维护多个工程,而且需要同时维护这几个工程的版本关联关系。为了简化多image管理,Zephyr引入了sysbuild概念。sysbuild用来将多个build系统(或者说多个工程)放在同一个build系统(或者说同一个工程)下管理,当你编译当前工程(为了叙述方便,暂且称其为主工程)时,sysbuild会自动把关联的其他工程(为了叙述方便,暂且称其为子工程)一起编译,并在当前工程的build目录下生成其他工程的build输出文件。配合Zephyr的Kconfig配置系统,可以在“神不知鬼不觉”的情况下,偷偷把其他外部工程包含进来,对开发者来说,你只面对一个工程,但编译的时候编译了多个工程,生成了多个image,而且每个子工程的配置可以在主工程里面指定,实现了一个工程管理多个image的目的。

如前所述,每个工程编译后都会生成一个zephyr.hex(zephyr.bin)文件,选用sysbuild后,除了在子工程目录下生成zephyr.hex,还会在build顶级目录下生成一个merged.hex,对于单image工程,这个merged.hex内容跟zephyr.hex内容一样;对于多image工程,这个merged.hex是各个工程的zephyr.hex合并而成。不管单image还是多image工程,用户直接下载merged.hex到板子就可以一次下完工程下面所有image。

我们现在直接看一个sysbuild的例子,使用nrf5340dk/nrf5340/cpuapp编译工程:nrf\samples\bluetooth\peripheral_uart,编译成功后,展开build目录,我们可以看到它下面包含两个工程:一个是跑在应用核的peripheral_uart工程,一个是跑在网络核的ipc_radio工程,如下:

点击“peripheral_uart”,nRF Connect VS code插件切换到此工程的操作界面,如下:

点击“ipc_radio”,nRF Connect VS code插件切换到此工程的操作界面,如下:

 需要提醒大家的是,ipc_radio工程位于目录:nrf\applications\ipc_radio,这里只是把它的image编译进来了,管理这个工程还是要回到nrf\applications\ipc_radio,也就是说,在VS code里面对这个工程的源代码修改会直接影响原工程。一般来说,对于这种标准的外部工程 ,我们是不修改源代码的,我们只修改它的配置,而配置是可以在peripheral_uart工程目录下进行,下面对其进行阐述。

5.3.1 命名空间 

如前所述,在主工程目录下,sysbuild可以同时修改主工程和子工程的配置,每个工程都有自己的Kconfig和Devicetree,而且sysbuild系统也有自己的Kconfig,sysbuild怎么区分各个工程的Kconfig和Devicetree呢?或者说,在主工程目录下,如何给其他工程的Kconfig和Devicetree去赋值?这里面就要利用到sysbuild的命名空间概念,sysbuild把每个工程包括sysbuild系统本身都看作是一个独立的命名空间,所以同一个Kconfig可以同时存在各个命名空间中。在不引起混淆情况下,编译系统可以按照传统方式访问每个工程里面的Kconfig配置和Devicetree配置;当会引起混淆情况下,我们就用命名空间来区分配置的具体归属。如果是sysbuild系统级的配置(脱离于所有工程外),它的配置以SB_打头;如果是mcuboot的配置,它的配置以mcuboot打头;如果是网络核的配置,它的配置以ipc_radio打头......应用工程的命名空间就是应用工程的名字,这个只会在west命令行输入参数的时候会用到。

sysbuild系统级的Kconfig文件放在主工程根目录下,如下所示,Kconfig.sysbuild文件和sysbuild.conf文件就是sysbuild系统级配置文件,用来配置sysbuild系统本身:

sysbuild.conf大概长下面这样:(注:这是我临时添加进去的一个文件)

sysbuild系统级的Kconfig选项并不多,一般用来自动加载某些外部image,比如MCUboot,网络核image等,常见的Kconfig有SB_CONFIG_BOOTLOADER_MCUBOOT,SB_CONFIG_PM_EXTERNAL_FLASH_MCUBOOT_SECONDARY,SB_CONFIG_NETCORE_IPC_RADIO等,更多Kconfig见:https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/app_dev/config_and_build/sysbuild/sysbuild_forced_options.html

主工程以外的其他子工程的配置项都放在sysbuild目录下,并以每个子工程的名字作为配置文件的名字,如下:

 

命名空间内的配置项还是跟正常的配置项一样命名和赋值,比如mcuboot目录下prj.conf:

这里解释一下,为什么sysbuild.conf里面的Kconfig需要叫命名空间SB,而上面的prj.conf不需要叫命名空间mcuboot?其实两者都需要命名空间,因为默认的命名空间是主工程本身,主工程不需要加命名空间,其他工程都需要加命名空间,所以sysbuild.conf里面的Kconfig需要加SB_ 。上面的prj.conf是放在sysbuild/mcuboot目录下的,这个其实就是约定了它的命名空间为子工程mcuboot,命名空间约定好了,里面的Kconfig就不会跟主工程的搞混,也就不用画蛇添足加上mcuboot_之类的前缀。

5.3.2 用文件后缀组织不同的配置文件集

我们经常会碰到一种情况:将工程区分为release版和debug版本,release简洁高效,debug版日志丰富方便调试,传统的做法是通过一个DEBUG宏来区分release版和debug版,但这种做法功能非常有限,因为很多时候release版和debug版不是一个DEBUG宏能区分的,需要多个宏,有时有可能连硬件连接都不一样,为此,Zephyr引入了File Suffix(文件后缀)机制,我们给.conf文件/overlay文件/pm文件加上同一个文件后缀,表示他们属于同一个配置文件集,当选择这个文件后缀时,所有相关配置文件将被自动选择并编译。

比如nrf\applications\nrf_desktop,定义了release,keyboard,fast_pair等多个文件后缀,如下:(我们只用红圈圈出了“release”文件后缀)

下面我们以用nRF5340DK编译nrf\samples\bluetooth\peripheral_uart为例,演示一下文件后缀的使用。在工程根目录下,我们看到如下文件后缀:bt_rpc:(注:这个文件后缀把整个蓝牙协议栈都跑在网络核)

添加编译配置时,我们按照如下方式指定bt_rpc文件后缀:

编译成功后,我们会发现sysbuild下面所有工程都会自动选择带bt_rpc后缀的配置,如下:

5.3.4 修改sysbuild Kconfig以自动加载MCUboot

 如前所示,使用nRF54L15DK编译nrf\samples\bluetooth\peripheral_uart工程时,默认只编译一个app image,我们现在让这个工程同时编译app和MCUboot两个image,为此,我们在工程根目录下,新建一个sysbuild.conf文件,并在其中加入配置:SB_CONFIG_BOOTLOADER_MCUBOOT=y,如下:

然后我们什么都不改,按照之前的方式再编译一次这个工程,成功后,我们有:

 

5.4 程序分区和pm_static.yml

 

NCS中引入了partition manager(PM)模块(注:PM和前面的SPM是两个完全不同的模块,二者之间没有任何联系),由PM完成对多个image的管理,以及存储划分。在PM中,主应用称为parent应用,其他应用称为child应用,通过使能parent应用的某些Kconfig,可以自动装载child应用,然后自动编译child应用,然后生成child应用的hex,并将child应用的hex和parent应用的hex合并在一起生成前文所述的merged.hex,这一切都发生在parent应用的build目录中。

PM是如何工作的呢?PM首先假定有一个app image,也就是我们的parent应用,这个应用对应的hex就是前文所述的zephyr.hex,那么app image放在Flash什么地方呢?这个是由PM动态决定的,PM将根据各个image的相对位置,来决定最终的image排列。一般来说,parent应用是默认应用,它不需要特别去指定自己的位置,而child应用则需要告诉PM自己的位置在哪里,这个是通过child应用目录下面的pm.yml文件来实现的,pm.yml会告诉PM本child应用会放在那里,pm.yml文件内容大概如下所示:

  

pm.yml是按照相对位置来决定本child应用的位置的,而且里面会用到Kconfig或者DeviceTree的宏定义,所以前面的<board>.dts文件会定义很多image slot,其实也是为了给PM引用的。pm.yml看起来又是一种新格式文件,让人觉得有点不适应,其实还是那句话,你不需要专门去学习这个文件的原理,它的语法和格式都是你直接可以读懂的,多看看例子,自然就明白了。而且一般开发过程中,大家也不需要关心child应用目录下的文件,大家只需关心parent应用目录下的相关PM文件即可。

那么parent应用目录下有哪些PM文件呢?首先就是build根目录下多image最终布局的partitions.yml文件,以nrf\samples\nrf9160\http_application_update为例,其partitions.yml文件如下所示:

 

partitions.yml文件是由PM模块自动生成的,用户不能直接修改。

然后就是pm.config 和pm_config.h文件,这两个文件一一对应,pm.config和partitions.yml放在同一个目录下,其内容大概如下所示:

  

而pm_config.h是C语言代码直接引用的文件,它在build\zephyr\include\generated目录下,以nrf\samples\nrf9160\http_application_update为例,其pm_config.h文件如下所示:

  

一般来说,使用PM自动生成的存储layout就可以了,只有一个配置有可能需要改:settings_storage的大小,这个可以通过Kconfig选项CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE直接修改就可以了。

如果需要人为指定某个image的位置,也就是说需要对自动生成的partitions.yml进行修改,怎么办呢?其实很简单,把partitions.yml这个文件拷出来,放在项目的根目录下,然后将其重新命名为:pm_static.yml,然后大家就可以按照自己的需求将里面的值进行修改。这里补充一下PM的另一个工作机理,当PM检测到parent应用根目录下面有pm_static.yml文件,它就不会再自动去划分存储空间,而是直接使用这个静态的存储空间layout。

由于大家对PM困惑很多,下面以DFU分区的角度对这个问题进行阐述,大家看下面这几段文字时,最好有一点NCS DFU概念。

所谓分区(Partition),就是对Flash(包括内部Flash和外部flash)或者RAM物理区域进行一个逻辑划分,人为划定哪块区域干什么工作,比如把MCUboot这个image放在0x0000到0xC000这块区域,这种分区是人为的,所以你可以随意调整,比如你把MCUboot放在0x0000到0x10000,当然也是可以的。我们对Flash或者RAM进行分区,目的就是为了把空间利用好,给各个分区一个ID以便后续引用,如果代码里不引用这个分区,那么此分区只是一个占位符而已,比如app和mcuboot这两个分区。

我们看一下https://github.com/aiminhua/ncs_samples/tree/master/smp_dfu/ble_intFlash这个例子生成的partitions.yml:

 

从上面可以看出,这个partitions.yml定义了很多分区,比如app,mcuboot,mcuboot_pad,mcuboot_primary等(冒号前面的就是分区名),而且每一个分区规定了它的起始地址,结束地址,大小,相对位置以及放在什么物理存储器上,比如app这个分区:

 

关于分区名,只有“app”这个名字是必须有,而且是固定的,代表着主应用程序image;其他分区名,比如mcuboot,settings_storage,external_flash等,都是随意定义的,可以修改。比如0x0~0xc000这块内部Flash区,上面取名叫mcuboot,你也可以改成“my_boot”之类的名字,这个也没关系的,取名字主要考虑两点:一是能醒目标识这块区域的功能,二是跟代码里面的引用对起来,比如如下分区定义,经常有人困惑:

 

第一个“external_flash”是分区名,第二个“external_flash”是物理存储器名。作为分区名的“external_flash”,其实我们可以改成其他名字,以消除某些困惑,之所以使用这个名字,是因为老的littlefs例子里面对外部文件系统所在区域就称为“external_flash”,代码如下所示: 

复制代码
复制代码
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(external_flash);
static struct fs_mount_t fs_mnt = {
    .type = FS_LITTLEFS,
    .fs_data = &external_flash,
    .storage_dev = (void *)FLASH_AREA_ID(external_flash),
    .mnt_point = "/lfs",
};
复制代码
复制代码

实际上最新的littlefs例子已经把这块区域重新命名为:littlefs_storage或者storage,所以大家可以把这块分区名改为littlefs_storage,如下:

 

partitions.yml里面使用的region其实是在这个文件:nrf\cmake\partition_manager.cmake定义的,大家可以通过build目录下的regions.yml文件得知目前定义了几个物理存储器:

 

至于partitions.yml里面使用的placement/span等,这个是用来指定各个分区的相对位置的,很多人会疑问,既然指定了分区的起始地址和结束地址,那还有必要去指定各个分区的相对位置吗?这种情况下的确没必要再指定相对位置了,其实这里弄反了一件事情:partitions.yml里面的地址是placement相对位置定下来之后的结果。使用placement相对位置,为编译系统动态确定各个分区的位置提供了便利。如果是我们自己来划分存储器的分区,我们就可以直接使用绝对地址的方式静态指定各个分区的位置(当然使用placement也是可以的)。

如何人为静态指定?答案就是把刚才动态生成的partitions.yml文件拷贝到项目根目录下,然后改名为:pm_static.yml,然后再按照自己的需求去修改,比如smp_dfu/ble_extFlash这个例子,如果由系统动态生成partitions.yml文件,此时mcuboot_secondary分区所在地址为0x0~0xf0000,而文件系统external_flash或者littlefs_storage分区所在地址为0xf0000~0x800000,实际上很多客户喜欢把文件系统放在外部Flash 0x00地址,而把secondary slot放在外部flash最后,据此可以做如下修改:

 

这个pm_static.yml文件没有定义的分区,还是由系统动态分配。有时为了后续升级方便,我们会在pm_static.yml文件里面把所有的分区都按照自己的规划重新定义一遍,这样就不担心某个image突然变大而导致新的partitions.yml跟老的文件不兼容,从而无法升级。在定义pm_static.yml文件时,有如下规则必须遵守:

  • mcuboot_primary大小必须等于mcuboot_secondary,而且CONFIG_BOOT_MAX_IMG_SECTORS最好也等于他们大小/4096
  • 如果使用了一个region(flash_primary这个region除外),那么这个region每一块区域都要属于一个分区名字,不能出现某块区域没有分区名字情况。比如上面重新定义了external_flash region,根据regions.yml文件定义,external_flash总共有8Mbytes,那么这8Mbytes都必须有一个分区名字,而我们定义的littlefs_storage和mcuboot_secondary两个分区的确包含了全部8MB区域。如果我们定义littlefs_storage所在区域为0x0~0x700000,而mcuboot_secondary所在区域为0x710000~0x800000,那么系统就会报错,因为这里还有一个空隙(gap):0x700000~0x710000是没有取分区名字的。解决这个问题有两个办法:一个就是上面的方法把0x700000~0x710000划到littlefs_storage分区,一个就是给这块区域专门取一个名字,比如:my_unused_area(见下面示意),也是可以解决问题的。

 

对于flash_primary这个region,由于系统默认认为必须要有一个“app”分区,所以它可以存在而且只能存在一个空隙(gap),这样系统默认这个gap就是“app”分区。当然你也可以把flash_primary所有区域都分好区,包括“app”分区。

  • regions.yml文件里面各个存储器的物理大小必须符合实际,这个通过修改dts文件来保证的。这里面最容易出错的就是external_flash,external_flash的大小在regions.yml文件里面是以字节为单位(在kconfig文件里面也是以字节为单位的),但是external_flash对应的设备树,比如MX25R64,它在dts文件里面是以bit为单位的,所以当大家使用其他外部Flash的时候,请仔细检查这些size对不对
  • settings_storage,即settings使用的分区,大家可以将分区名改成:storage,这是其一,其二settings系统最终使用的最大flash区域大小是由CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE决定,而不是settings_storage分区本身大小决定,所以建议大家把CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE的值设为settings_storage分区大小。
  • 至于RAM分区,道理也是一样的。这里需要注意的是,RAM各个分区的大小大家可以直接到dts文件里面去调整,而无需在pm_static.yml文件里面调整。当然,大家在pm_static.yml里面调整也是可以的,殊途同归,达到目的就好了。对于nRF52系列,只有一个sram_primary分区,这个没什么好讲的;对于nRF53系列,除了sram_primary这个分区,它还有rpmsg_nrf53_sram分区以及pcd_sram分区,其中rpmsg_nrf53_sram是用来蓝牙协议栈host和controller之间进行双核通讯的,而pcd_sram是用来升级网络核image的。

5.5 nRF Connect SDK重要编译系统变量小结

前面提到了nRF Connect SDK众多配置选项,现将其归纳总结如下:

nRF Connect SDK常用编译系统变量

变量名

目的

如何使用

Name of the Kconfig option

为单个构建将给定的 Kconfig 选项设置为特定值。
Kconfig 选项名称可能会受到变量命名空间和 sysbuild Kconfig 命名空间的影响

-D<name_of_Kconfig_option>=<value>

CONF_FILE

选择要用于应用程序的基础 Kconfig 配置文件,并覆盖自动选择过程。
如果应用程序或示例支持多种构建类型,此变量也可用于选择其中一种可用的构建类型。
-DCONF_FILE=<file_name>.conf
-DCONF_FILE=prj_<build_type_name>.conf

SB_CONF_FILE

选择要用于应用程序的基础 sysbuild Kconfig 配置文件,并覆盖自动选择过程

-DSB_CONF_FILE=<file_name>.conf

EXTRA_CONF_FILE

提供要与基础配置文件 “混合” 的额外 Kconfig 片段文件

-DEXTRA_CONF_FILE=<file_name>.conf

DTC_OVERLAY_FILE

选择要用于应用程序的基础设备树覆盖文件,并覆盖自动选择过程

-DDTC_OVERLAY_FILE=<file_name>.overlay

EXTRA_DTC_OVERLAY_FILE

提供额外的自定义设备树覆盖文件,以与基础设备树覆盖文件 “混合”

-DEXTRA_DTC_OVERLAY_FILE=<file_name>.overlay

SHIELD

选择一种受支持的开发板扩展板(shield)来构建固件

-DSHIELD=<shield> (-D<image_name>_SHIELD for images)

FILE_SUFFIX

如果应用程序或示例支持,选择一种可用的带后缀的配置

-DFILE_SUFFIX=<configuration_suffix> (-D<image_name>_FILE_SUFFIX for images)

-S (west) or SNIPPET (CMake)

选择一个代码片段(Snippet),以便在构建期间添加到应用程序固件中

-S <name_of_snippet> (applies the snippet to all images)
-DSNIPPET=<name_of_snippet> (-D<image_name>_SNIPPET=<name_of_snippet> for images)

PM_STATIC_YML_FILE

为分区管理器(Partition Manager)脚本选择一个静态配置文件

-DPM_STATIC_YML_FILE=pm_static_<suffix>.yml

 
你可以在 nRF Connect for VS Code 扩展和命令行中使用这些参数。构建变量会根据你提供的顺序依次应用。以下是nRF Connect VS code插件指定这些变量的方式:
  • FILE_SUFFIX (and CONF_FILE) - 在Base configuration files中选择

  • EXTRA_CONF_FILE - 在Extra Kconfig fragments中添加

  • DTC_OVERLAY_FILE - 在Base devicetree overlays中选择

  • EXTRA_DTC_OVERLAY_FILE - 在Extra devicetree overlays中添加

  • SNIPPET - 在Snippets中选择

  • Other variables - 在Extra CMake arguments中提供,并以--开头

6. 让peripheral_uart支持蓝牙空中升级 

现在我们开始我们第一个NCS程序或者Zephyr程序的开发,在NCS中,有如下两个现成的例子:zephyr\samples\hello_world和zephyr\samples\basic\blinky。重新编译和下载,这时我们再去测量nrf52840dk电流,此时电流只有几微安,符合预期。

 

 

7. 将一个nRF5 SDK例子移植到nRF Connect SDK中

 

posted on   iini  阅读(59647)  评论(15编辑  收藏  举报

相关博文:
阅读排行:
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8
点击右上角即可分享
微信分享提示