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

Nordic有2套并存的SDK:老的nRF5 SDK和新的nRF Connect SDK(简称NCS),两套SDK相互独立,大家选择其中一套进行开发即可。一般而言,如果你选择的芯片是nRF51或者nRF52系列,那么推荐使用nRF5 SDK。如果你选择的是Nordic最新产品系列,比如nRF53或者nRF9160,那么请选择nRF Connect SDK。还有一种特殊情况,虽然你选择的是nRF52芯片,但需要使用最新的一些射频技术,比如蓝牙测向,蓝牙Mesh v1.1,低功耗蓝牙音频,那么也需要使用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所有产品线,包括低功耗蓝牙,蜂窝网,WiFi,GPS,2.4G,蓝牙Mesh,Zigbee,Thread,Matter, Homekit, FindMy等,换句话说,由于短距离无线网络和长距离无线网络共用同一个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,mbedtls,nrfxlib等其他仓库。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 nRF Connect SDK安装

NCS开发包比较大,目前大概4G(后续有可能会更大),受限于网络带宽,不能像nRF5 SDK那样,直接提供一个压缩包进行下载,为此我们提供如下几种安装方案,大家根据自己的情况选择一种适合自己的安装方式

2.1.1 手动安装(直接访问GitHub网站,国内速度比较慢)

这个是Nordic官网的安装方式,需要直接访问GitHub,请大家按照:https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/gs_installing.html里面的说明来安装。这种手动安装的好处是:你可以实时跟踪Nordic的发布,除了第一次安装比较费时费力,后面更新非常方便,速度也很快,大家可以使用如下命令跟GitHub上的资源同步:

git fetch origin
git checkout origin/master 或者 git checkout origin/main
west update

注意:如果你没有VPN或者没有半夜起来安装的话,有可能最后安装会失败。

2.1.2 通过nRF Connect桌面版直接GitHub下载(推荐)

首先,下载桌面版nRF connect (同时支持Windows/macOS/Linux平台)。下载链接为https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Connect-for-desktop/Download#infotabs,选择你的平台和版本。

 

 桌面版nRF connect安装成功后,将如下所示:

  

 在nRF connect桌面版中有2个app都可以完成NCS SDK安装,一个是Toolchain Manager,一个是Getting Started Assistant。Windows和Mac用户使用Toolchain manager方式(Linux后面也会支持Toolchain manager),Linux用户使用Getting Started Assistant方式(Getting Started Assistant方式其实就是2.1.1的手动安装方式)。下面将会以Toolchain Manager的方式(支持Windows和macOS)来讲解安装过程。 

首先install Toolchain Manager,然后open,进入settings界面,选择安装目录,如下:

  

 然后重新选择SDK ENVIRONMENTS页面,并选择SDK相应版本进行安装,如下所示:

  

根据网速不同,这个过程持续时间变化很大,我这边网络大概需要20分钟完成所有下载和安装(有的人可能需要一天一夜),安装成功后你将得到如下目录内容:

     

跟2.1相比,这种方案把工具链打包好,不需要你一个一个去下载和安装,节省了不少精力,但是它也访问GitHub网站,所以会碰到跟2.1一样的问题:如果你没有VPN或者没有半夜起来安装的话,有可能最后安装会失败。 

2.1.3 百度网盘下载(仅适用于Windows)

我们把2.1.2安装好的SDK上传到百度网盘,这样大家直接通过百度网盘就可以下载成功了,百度网盘链接:

进入目录“开发你的第一个NCS(Zephyr)应用程序”,选择相应的版本(推荐使用最新版本),直接把相应的压缩包下载下来并解压到本地目录,SDK即算安装完成。注意:这个压缩包只能在Windows系统上运行,不能在Linux和macOS上运行。 

2.1.4 Gitee镜像安装(熟悉Git的同学建议使用此种方式)

nRF Connect SDK仓库是放在GitHub上,因此国内下载速度很慢。为此,我们在Gitee建立了nRF Connect SDK仓库的镜像,大家可以直接从Gitee安装nRF Connect SDK。

如果你对Git毫无概念,建议你参照2.1.3节来安装;反之,如果你有Git相关的常识,那么请往下看,这种方法速度非常快,我们下面会详细介绍它的所有步骤。

工具链安装(其实就是2.1.1节手动安装的一部分步骤)

NCS会用到这些工具:cmake ninja gperf python git dtc-msys2 wget unzip GN west,这些工具都是一些常用的工具,大家可以自己去网上下载安装,下面介绍通过choco第三方安装工具来自动安装上面这些工具

  • 安装Choco
    • 以管理员权限打开Windows PowerShell,然后连续输入如下两个命令
      Set-ExecutionPolicy AllSigned
      Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

      等待几分钟,安装即算完成,你可以输"choco"来确认是否安装成功了

  • 通过Choco安装工具
    • 以管理员权限打开CMD,然后输入如下命令:
      choco feature enable -n allowGlobalConfirmation
      choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
      choco install ninja gperf python git dtc-msys2 wget unzip
    • 说明一下:这里安装的工具都是最新版,比如Python,肯定是最新版的Python,使用最新版的Python,nRF Connect SDK编译的时候会少一些模块,大家通过PIP安装把他们补上就行了。当然大家也可以指定版本安装,以保证最好的兼容性,choco指定版本安装示例如下所示
      choco install python --version=3.9.6
    • 如果你需要开发Matter应用,你还需要安装GN工具,否则不需要。大家自己上网找一下GN工具,这里就不讲了。
  • 通过Python安装west,在CMD输入如下命令
    pip3 install west
  • 安装GNU ARM工具链。开发Zephyr或者NCS应用,你可以使用Zephyr自定义的GNU工具链,也可以使用GNU原生的工具链,两者选其一即可(如果你访问GitHub速度很慢的话,建议直接使用GNU原生的工具链。如果你的Zephyr工程包含RISC-V内核,那么就只能使用Zephyr自定义工具链了)。从NCS v2.0.0开始,Zephyr和NCS都要求使用Zephyr自定义的GNU工具链。
    • Zephyr自定义的GNU工具链下载地址为:https://github.com/zephyrproject-rtos/sdk-ng/releases/,选择你的版本,一般直接选择SDK BundleFull版本,比如基于Windows的zephyr-sdk-0.15.2_windows-x86_64.zip,解压缩放在一个目录下,比如C:\Tools,然后双击里面的“setup.cmd”,安装即算完成。请注意,Zephyr自定义工具链非常大,而且是放在GitHub上,如果没有VPN或者不是半夜起来下载,很有可能就会下载失败。除了前面的HTTP网页直接下载,大家也可以使用如下两条命令完成下载和安装。下载命令:wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.15.2/zephyr-sdk-0.15.2_windows-x86_64.zip,安装命令:unzip zephyr-sdk-0.15.2_windows-x86_64.zip
    • 你也可以下载GNU原生工具链,GNU ARM原生工具链下载地址:https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads (老版本链接:https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads)。选择你的安装包,比如“gcc-arm-none-eabi-10.3-2021.10-win32.zip”,下载下来,解压缩放在一个目录下,比如C:\gnuarmemb,安装即算完成
    • 设置环境变量(可选),大家可以把上述GNU工具放在Windows环境变量中,GNUARMEMB_TOOLCHAIN_PATH用来设置GNU原生工具链地址,而ZEPHYR_SDK_INSTALL_DIR用来设置Zephyr自定义工具链地址,ZEPHYR_BASE(表示Zephyr仓库的地址)和ZEPHYR_TOOLCHAIN_VARIANT的设置两者方法是一样的,设完环境变量后大家就可以随时随地编译Zephyr工程,而无需从特定的地方启动CMD或者VS code。下面是我自己PC环境变量设置的效果图(同时支持GNU原生工具链和Zephyr自定义工具链):


  • 下载IDE,NCS支持两种IDE,二选其一即可。
  • 下载nRF command line tools,见下面的2.2节

至此整个工具链准备完成,我们下面进入SDK下载和安装过程

SDK下载和安装

打开CMD,进入到一个你想安装NCS的目录,输入如下命令:

west init -m https://gitee.com/minhua_ai/sdk-nrf --mr gitee_mirror

下载成功后,将会有一个nrf目录,进入:

cd nrf

然后输入如下命令:

west update

返回到根目录:

cd ..

安装一些Python依赖脚本,即输入如下指令:

pip3 install -r zephyr/scripts/requirements.txt
pip3 install -r nrf/scripts/requirements.txt
pip3 install -r bootloader/mcuboot/scripts/requirements.txt

至此nRF Connect SDK安装完成。

开发的时候,我们一般选择最新的tag(nrf仓库)进行开发,大家输入如下指令查看目前已发布的tag:

cd nrf
git tag

然后选择最新的tag,比如v2.2.0,执行如下操作:

git checkout v2.2.0
west update

如果要使用其他tag,还是上面两步操作,即先checkout某个tag,然后west update,只有west update成功了,才能说明你切换成功,否则就有问题。

注意:通过这种镜像安装的方式,master或者main分支我没有同步下来,大家只能以某个tag为基准来进行开发,实际上大家也必须以tag为基准来开发,因为只有tag版本才会做考核和认证,它的质量才会有保证。

2.2 IDE和nRF command line tools安装

不管采用哪种方式安装nRF Connect SDK,IDE和nRF command line tools还需另外安装。

IDE安装

nRF Connect SDK支持两种IDE:Visual Studio Code和West,二选其一即可:

nRF command line tools安装

nRF command line tools包括Jlink驱动以及Nordic自己开发的一些命令行工具,如nrfjprog,nrfutil以及mergehex等,这些工具对开发非常有帮助。nRF command line tools下载链接为:https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Command-Line-Tools/Download#infotabs。“nRF-Command-Line-Tools_x_x_x_Installer.exe”即为nRF command line tools,选择最新版本下载。

   

大家也可以去上面的百度网盘下载nRF command line tools。  

3. nRF Connect SDK开发环境搭建和说明

3.1 NCS项目介绍

nRF Connect SDK下载成功后将呈现如下内容,可以看出,NCS包含多个目录,每个目录基本对应GitHub上一个仓库,其中nrf目录对应的仓库为NCS的主仓库,不特别说明的情况下,我们讲NCS就是讲nrf仓库。

具体来说,NCS包含nrf,zephyr,bootloader,nrfxlib,modules等多个仓库,其中nrf仓库就是NCS的主仓库(manifest),nrf仓库的版本就是NCS的版本,所以要查看NCS SDK当前版本号,进入nrf目录,输入

git branch

或者

git show

如下: 

 

熟悉git的同学都知道,一个SDK如果包含多个仓库,那么每个仓库都有自己的版本,而且仓库版本之间是相互关联的,以NCS SDK为例,当nrf版本切换为v2.0.0时,其他仓库的版本也需要做相应切换,那么对应nrf v2.0.0的Zephyr仓库版本是多少呢?mcuboot仓库版本是多少呢?如果直接使用git工具,那么你需要一个一个记,然后一个一个checkout。在NCS或者Zephyr中,引入了west工具,这样通过管理nrf仓库版本就可以间接管理其他仓库的版本,比如我们现在把NCS SDK版本切换到v2.0.0,指令如下所示:

git checkout v2.0.0

然后通过west update命令,就可以让其他仓库版本自动跟v2.0.0 nrf仓库同步,这样你不需要记住或者管理其他仓库版本,只需管理nrf仓库版本即可。

west update

  

在NCS中,项目是通过CMakeLists.txt表示的,如下: (大家仔细看看这个目录下所有的文件,后面还会介绍其他一些文件)

如大家所知道的,Keil工程配置一个项目一般是通过.h文件来实现的,在NCS中我们是通过Kconfig和prj.conf(他们最终也会通过Python脚本转成.h文件)来实现,比如上面的prj.conf:(第4章会详细介绍Kconfig)

 

像Keil编译一个项目时需要选择Device,如下:

在NCS中你需要选择一个开发板,选择开发板,就是选择芯片,除此之外,开发板还规定了芯片的一些外设使能情况,以及一些基本外围电路连接情况,这个跟Keil选择Device的操作是异曲同工的,而且NCS这种做法更灵活,功能也更多,扩展性也更好。Zephyr支持的所有ARM开发板一般可以在如下目录找到:

 

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

 上面所有板子的默认配置都可以在zephyr\boards\arm找到,比如nrf5340dk_nrf5340,它的定义目录如下:

上面定义板子的方式就叫Device Tree,Device Tree使用dts文件定义板子配置,dts一般用来定义默认配置,如果某个项目需要跟默认配置不一样的特定配置,则可以使用overlay文件来实现,比如上面的例子:nrf\samples\bluetooth\peripheral_lbs,boards/thingy53_nrf5340_cpuapp.overlay文件,就是一个overlay文件,用来覆盖板子原始默认配置:(第4章会对Device Tree进行详细描述)

 

NCS支持的工具链有2套:一套是Visual studio code,一套是west命令行方式(沿用了Zephyr工具链),其实就是GCC工具链,二套工具链选其一即可,另外早期的nRF Connect SDK还支持Segger embedded studio。下面分别介绍这3套工具链的使用方式。

3.2 Visual Studio Code开发环境搭建(Nordic官方IDE)

3.2.1 nRF Connect插件基本操作

nRF Connect SDK在VS code中的开发是通过nRF Connect插件来完成的,nRF Connect插件包括多个VS Code扩展,大家可以通过VS code的Marketplace进行安装。打开VS Code,进入Marketplace,搜索“nrf”,然后选择“nRF Connect for VS Code Extension Pack”进行安装,一旦nRF Connect for VS Code Extension Pack安装成功,所有nRF Connect插件都自动安装成功。大家可以通过如下方式确认nRF Connect插件有没有安装成功:

SDK配置

一旦nRF Connect插件安装成功,选择VS code的File -> Open folder,打开前面的nRF Connect SDK所在的根目录。然后点击如下图标,进入nRF Connect插件,并进入SDK配置界面:

 

如果是按照Toolchain manager安装好SDK的话,那么上面两个配置框都会默认为你选好。你也可以通过点击下拉箭头,然后通过“Browse”选择新的NCS根目录和工具链位置。

 注意:如果你的GNU工具链是自己手动安装的,那么上面的"nRF Connect Toolchain"请选择“PATH”,即Windows环境变量。

上面配置完成后,我们就可以正式进行项目开发了。为了熟悉nRF Connect开发环境,我们先使用一个例子来说明如何编译,调试,下载等。

打开一个项目

如果你的NCS是从Toolchain manager或者百度网盘安装的,为了保证VS code可以正确找到GNU工具链,推荐大家从Toolchain manager中打开VS code, 如下所示:(如果你是手动安装NCS,就没有这个要求)

 

打开VS Code后,检查一下上面的SDK配置对不对,不对的话,还需要重新修改。

我们现在打开一个标准例子工程,工程所在目录为:nrf\samples\bluetooth\peripheral_uart,即闻名遐迩的蓝牙数据透传例子,点击下面红框之一就可以打开这个项目:

 

然后找到目录:nrf\samples\bluetooth\peripheral_uart:

 

 成功后,将如下所示:

 

项目配置 

点击上面的“Click to create one”或者右键单击通过快捷菜单选择,此时跳出编译配置界面,如下:

  

 其中Board选项用来选择开发板,其中Compatible boards表示这个例子默认支持的板子列表,它对应目录下的sample.yaml中的integration_platforms和platform_allow中列出的板子列表,大家可以比较一下,两者是一样的。这里我们以nrf5340dk_nrf5340_cpuapp为例(你手上如果没有5340dk,也可以使用其他板子),并勾选“Enable debug options”(为后续Debug讲解打下基础,正常开发不用勾选),然后点击“Build Configuration”:

 

查看编译信息 

点击右下角的“Building”,可以看到编译过程日志。(如果编译有问题,请仔细看里面的错误提示)

 

3.2.2 nRF Connect插件介绍以及一些重要按钮或者小菜单介绍 

nRF Connect插件整体概览

编译成功后,将看到如下界面: 

  

 这里要提醒大家一下,上面每个按钮或者菜单项,里面包含多个小按钮或者小菜单,每个小按钮或者小菜单对应的操作都不一样,下面我们说一说一些重要的小菜单选项。

文件搜索

点击下面的放大镜图标,将搜索对应目录的c源文件(可惜不能搜索头文件),这将大大加快VS code的搜索速度:

  

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

Build(编译)操作 

Build按钮暗含三种操作,如下所示:

项目配置-Kconfig

Kconfig按钮用来配置项目,即开关宏或者配置宏的数值,它也包含三种配置方式:Nordic nRF Kconfig, Guiconfig和Menuconfig,三个配置方式异曲同工,用其中一种方式即可(我个人比较喜欢用Guiconfig),最终目的都是修改最终的配置项,即.config文件,第4章将对此进行详细介绍。

 

选择Kconfig后,将出现如下界面:

 

选择Guiconfig,将得到:

选择Menuconfig,将得到:

 

代码烧写(Flash)

Flash按钮用来烧写代码到板子,它暗含如下操作。

 

在烧写代码之前,确保开发板已经接到PC(此时是nRF5340 DK),Nordic开发板做得非常好,只需要一根USB线,一头接PC,一头接开发板USB口,就可以完成J-link调试,UART日志打印,以及RTT日志打印三重功能。另外对于多image系统,点击“Flash”是会把所有image同时烧入到开发板,比如nRF5340会同时烧写应用核和网络核的image。

代码调试(Debug)

下面重点介绍Debug按钮,先说明一下,Debug按钮和Debug with Ozone按钮两者完全不搭架,Debug按钮使用VS Code内嵌调试器进行调试,而Debug with Ozone只是一个快捷键,用来打开第三方软件Ozone,并使用它来调试应用,Ozone调试界面如下所示,感兴趣的同学可以自己去摸索实践。

 

下面说明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 Viewer View”,如下:

 

大家可以看到,存储器内容涂上了各种颜色,这个是nRF Connect的一大特色,将编译器输出的每个Symbol(符号)与存储器地址关联起来,同时每个Symbol使用不同的颜色表示,点击存储器任何地方,将显示该位置处的Symbol信息,如下:

 

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

 

条件断点

下面说说如何在nRF Connect中使用条件断点,首先选择要加条件断点的代码行,然后ctrl+shift+p进入Command Palette,选择“Debug: Add Conditional Breakpoint”,如下:

 

比如,我们要实现一个功能,当blink_status变量大于10时,自动停在main.c的614行。为此,我们可以先点击614代码行,然后按照上面方式调出Expression菜单,并在其中输入表达式:blink_status > 10,即可完成预定的条件断点功能,如下:

 

 条件断点设成功后,界面将如下所示:

 

调试板子原有image

在Debug的时候,我们还经常会碰到一种情况:不下载最新的image到板子,而是使用板子原始的image来调试,nRF Connect也能实现这个功能。默认情况下,nRF Connect会下载刚刚编译好的image到板子以覆盖原始的image,如果想不覆盖原始image并调试原始image的话,你需要改一下Debug的配置,如下:

 

 将flash的值设为false(手动输入即可),这样每次debug的时候,nRF Connect就不会把新image覆盖老image,而是直接使用板子里面的老image进行调试。

3.2.3 nRF Connect与VS code高级功能介绍

代码跳转(Go to Definition) 

 VS code有一个Go to Definition(代码跳转)功能,对应的快捷键为:ctrl+鼠标单击,这是一个非常有用的功能。nRF Connect插件又可以定义一个项目,这样Go to Definition就在本项目跳来跳去,省去了搜索操作,极大方便了用户代码阅读。为了发挥Go to Definition功能,我们需要把上面打开的VS code目录,即NCS根目录,保存为一个workspace,即点击 File -> Save Workspace As, 如下:

保存为workspace后,再编译刚才的项目,此时跳转就会在编译好的项目中跳来跳去,而不是在整个NCS目录中跳来跳去。比如我查看k_sleep的定义,它直接跳到如下界面:

 

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

 

 点击“Go to Definition”后,将自动跳出如下定义:

 

多image或者多项目管理

nRF Connect插件可以同时管理多个项目或者image,比如上面的peripheral_uart例子,如果使用nrf5340dk_nrf5340_cpuapp编译的话,虽然你选择的是应用核,但实际上5340的网络核也会参与进来,最后会同时编译好应用核的image和网络核的image,这个你可以通过展开APPLICATIONS窗口里面的build目录,就一目了然,如下:

 默认系统自动选择对应开发板对应的app工程,以这个例子为例,点击build目录,默认选择应用核下面的build目录,此时我们再点击hci_rpmsg目录,即网络核工程,那么nRF Connect插件自动打开网络核对应的工程,同时所有操作变成对网络核的操作而不是默认的应用核,如下:

 此时点击Debug按钮,将直接调试网络核工程,而不是应用核工程,如下:

  

对第一次接触nRF5340的用户来说,估计对上面应用核和网络核双image管理还不是很理解,我们再举一个nRF52840 OTA例子:zephyr\samples\subsys\mgmt\mcumgr\smp_svr,大家都知道如果nRF52840需要支持OTA功能,那么nRF52840就必须带一个bootloader,加上本身的app,这样就有两个image跑在nRF52840身上,这也是一个典型的多image应用,我们现在编译一下这个例子,看看最后是不是同时生成两个image,nRF Connect插件是不是可以在这两个image之间来回切换。

首先打开这个工程,并做如下配置:

 

 编译,成功后,展开build目录,可以看到它包含两个工程:buildmcubootbuild目录对应app工程,mcuboot目录对应bootloader工程,默认选择app工程,所有窗口按钮操作都是针对app工程的,如下:

点击mcuboot目录,系统切换到bootloader工程,所有操作都变成对mcuboot工程了,如下:

 此时点击Debug按钮,将直接调试mcuboot工程,而不是app工程,如下:

  

我们会在第7章详细讲解NCS如何管理多image系统。 

3.2.4 nRF Terminal插件

我们安装nRF Connect插件的时候,还会自动安装另一个插件:nRF Terminal,这是Nordic在VS code里面开发的一个终端,该终端可以同时支持串口助手和RTT Viewer功能,这样大家就不需要下载额外的串口助手或者RTT Viewer,而且它又是跨Windows/MacOS/Linux,也算是一个便民工具吧。

串口助手功能

欲打开串口助手功能,在左下角“connected devices”窗口,选择一个串口,比如VCOM1,点击旁边的连接图标,然后选择波特率,比如115200 8n1 rtscts:off,此时nRF Terminal将作为一个串口助手而使用,如下:

 如需断开串口,点击左下角的串口名: 

 

 RTT Viewer功能

欲打开RTT Viewer功能,在左下角“connected devices”窗口,点击“RTT”按钮的连接图标,选择芯片,比如“nRF5340 Application core”,然后选择“Automatic search”,此时nRF Terminal将实现RTT Viewer功能,如下:

 

 如需断开RTT,操作与上面的断开串口差不多,在此不再赘述。

关于RTT Viewer,有一点需要大家注意一下,每次复位之后,新日志有可能不会自动显示在屏幕上,此时需要重新连接一次RTT Viewer,新日志才会显示出来。

3.3 West命令行方式开发环境搭建

NCS或者Zephyr项目命令行编译的时候,一般使用west命令,west语法可以通过west --help获得,如下: 

west --help

如果你能看到上面的界面,那么你的环境基本上就没问题了。

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

west build -b nrf52840dk_nrf52840

注:-b指定开发板,这里使用nRF52840开发板

3.3.1 打开命令行终端(CMD)

west命令等NCS命令可以跑在CMD,Bash和Powershell等大家熟悉的终端上,本文将以CMD为例来讲解

通过百度网盘或者Toolchain Manager安装的SDK,CMD不能直接打开,而必须通过Toolchain manager app打开,如下:

 

或者进入如下目录:install folder\toolchains\vx.x.x,并双击git-cmd.exe以打开CMD如下:

  

 通过这两种方式打开的CMD,系统会自动设置好一些环境变量,否则后续编译会有问题。当然,如果你是手动安装的NCS SDK,那么就可以直接打开CMD了。

3.3.2 常用west命令

下面我们会以一个标准例子工程:nrf\samples\bluetooth\peripheral_uart,即闻名遐迩的蓝牙数据透传例子,来详细讲解west常用命令。

编译

编译命令格式如下:

west build -b nrf5340dk_nrf5340_cpuapp -d build_nrf5340dk_nrf5340_cpuapp -p

注:-b指定开发板,-d指定编译目录,-p清除老的编译目录内容,即大编译;如果不带-p,意味着小编译

 代码烧写

编译成功后,就可以烧写了,使用如下命令进行烧写:

west flash --erase -d build_nrf5340dk_nrf5340_cpuapp
--erase表示烧写之前进行全擦,不带--erase表示页擦;-d指定编译目录,系统通过编译目录会自动匹配开发板型号

 代码调试

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

 

 项目配置-Kconfig

你可以使用menuconfig或者guiconfig来查看Kconfig配置信息。

menuconfig对应命令为:

west build -t menuconfig -d build_nrf5340dk_nrf5340_cpuapp
-d表示编译目录

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

注:上述命令有可能需要先安装windows-curses ,即在cmd中执行如下命令:pip install windows-curses --user,此命令的执行需要联网。如果你的电脑无法联网,建议使用guiconfig查看工程配置。

guiconfig对应的命令为:

west build -t guiconfig -d build_nrf5340dk_nrf5340_cpuapp
-d表示编译目录

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

   

3.4 SES开发环境搭建(从NCS v2.0.0开始就不再支持SES)

通过百度网盘或者Toolchain Manager安装的SDK,必须进入如下目录:install folder\toolchain,并双击SEGGER Embedded Studio.cmd以打开SES,而不能在其他地方直接打开SES,如下所示:

  

SES启动成功后,进入Tools->Options,我们需要先对IDE进行配置。

  

按如下方式进行配置:

  

如果上面方式有问题,请按如下方式进行配置:

  

如果你的GNU工具链是手动安装的,上面的Toolchain目录请使用你自己的安装目录,比如C:\gnuarmemb,Zephyr base就是你Zephyr仓库的目录。

配置好之后,我们就可以打开一个工程进行编译了。进入File->Open nRF Connect SDK Project

  

如下为hello_world项目的装载图:

  

注意,当大家装载一个SES项目的时候,有时候会报错,比如如下界面:

 这个时候,你需要点击上面的“OK”,然后从下面窗口找到错误提示。

 

下面将详细说明如何装载一个项目,编译一个项目,下载一个项目和debug一个项目。

以nrf\applications\nrf_desktop项目为例,开发板选择支持LLPM的gaming mouse:nrf52840gmouse_nrf52840

  

装载成功后,编译,如下所示:

  

然后下载,如下所示:

  

如果要Debug,按如下界面进入:

  

注意:如果开发nRF53应用程序,由于nRF53包括两个核:app核和network核,app核用来运行应用程序,network核用来运行射频协议栈,所以每次下载的时候需要同时把两个核的hex都下载进去,但是SES只支持一次下载一个核。在使用SES开发nRF53的时候,项目装载/编译/下载/debug跟其他芯片是一样的,但这些操作只是针对一个核(一般来说都是app核),此时如果还需要network核配合的话,那么需要你手动先把network核的image下载进去,这个可以通过nrfjprog或者west来完成。这里要强调一点的是,当你选择一个蓝牙项目,这个项目默认会同时把app核和network核对应的工程都进行编译,然后生成各自的hex文件,然后你通过nrfjprog或者west把network核的image下载进去,通过SES本身的Target->Download菜单把app核的image下载进去。

3.5 NCS项目编译目录简介 

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

上面的peripheral_uart例子选择板子nrf5340dk_nrf5340_cpuapp编译成功后,在根目录下生成一个build目录,build目录结构如下:

 

 build目录下面有几个重要的文件或者目录

  • zephyr目录,当前app项目(或者说父项目)所有编译生成文件都放在这个目录下
  • hci_rpmsg目录,子项目网络核工程所有编译生成的文件都放在这个目录下,相当于另一个build目录。只有选择5340DK应用核编译时,才会有这个目录;选择nRF52系列芯片,由于没有网络核,也就不会有这个目录
  • build.ninja,ninja项目描述文件
  • CMakeCache.txt,CMake项目描述文件
  • partitions.yml,芯片存储区分区文件,只有多image项目才会出现这个文件 

我们再看zephyr目录结构和内容,如下:

 

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

  • .config,Kconfig生成的最终文件,项目所有的Kconfig和prj.conf文件最终将合并成这个.config文件,通过查看.config文件就知道项目最终配置对不对
  • zephyr.dts,DeviceTree生成的最终文件,项目所有的dts文件和overlay文件最终将生成这个zephyr.dts文件,通过查看zephyr.dts文件就知道项目最终配置对不对
  • zephyr.hex/zephyr.bin,app工程编译生成的image
  • zephyr.map,app工程的map文件
  • zephyr.lst,app工程的反汇编代码
  • merged.hex,多image项目所有image将合并成一个image:merged.hex,以方便代码烧写
  • 升级文件,如果这个项目支持DFU功能,那么在这个目录下还会生成升级文件:app_update.bin,dfu_application.zip,app_signed.hex等

我们再看看子image网络核的hci_rpmsg目录结构和内容:

 

可以看出它跟父目录的build目录结构基本上一模一样,不同的是hci_rpmsg里面所有内容只针对网络核工程,比如上面的zephyr.hex是网络核工程编译的image,而不是再前面的app工程对应的image。

 

4. nRF Connect SDK项目配置和选项

4.1 nRF Connect SDK项目配置介绍-Kconfig和DeviceTree

每一个NCS或者Zephyr项目都包含了非常多的配置项或选项,总数有可能达几千项之多,然而绝大多数配置项我们都不需要去管他们,只需要使用默认值即可,所以开发一个简单的NCS应用程序,有可能我们不需做任何配置,就可以跑起来。当你开发复杂的应用程序的时候,你又会体会到NCS配置的灵活性和方便性了,这其实也是NCS一个很大的优势。本章我们先大概讲解NCS的配置项,后面一章我们再以例子来说明如何更改配置项。NCS或者Zephyr里面主要包含两种配置项:Kconfig和DeviceTree,Kconfig主要负责软件的配置,DeviceTree主要负责板子硬件的配置。两者最终都会生成一个.h文件,其中Kconfig生成的头文件为:autoconf.h,而DeviceTree生成的头文件为devicetree_generated.h(老版本为devicetree_unfixed.h)他们都在如下目录:

  

autoconf.h文件示例如下: 

 

devicetree_generated.h文件示例如下:

  

如果大家开发过nRF5 SDK的话,一定记得里面有个:sdk_config.h,从机理上,sdk_config.h和上面的autoconf.h/devicetree_generated.h并没有本质区别,他们都是完成同样的功能。但是很多人都觉得Kconfig和DeviceTree很复杂,其实他们复杂之处在于如何生成这两个头文件以及使用这两个头文件,但是头文件本身并不复杂。在nRF5 SDK中,用户可以直接修改sdk_config.h文件,由于sdk_config.h文件太大,所以Nordic使用了CMSIS_Configuration_Wizard来格式化这个头文件,以形成如下的图形化界面方便大家去修改它:

  

在NCS或者Zephyr里面,由于autoconf.h/devicetree_generated.h是由Python脚本自动生成的,所以用户不能直接修改autoconf.h/devicetree_generated.h这两个头文件。用户只能去修改生成这两个头文件的输入,以达到间接修改他们的目的。这样做的好处是:更灵活,而且不容易出错(Python会自动帮你找出配置不合理的地方或者语法错误)。可以说,一旦你熟悉了这套配置机制,你就会爱上它。

下面分别对autoconf.h和devicetree_generated.h两者的生成过程进行阐述。

4.1.1 Kconfig (Kconfig, prj.conf及autoconf.h)

我们先把生成autoconf.h文件的整体流程图贴出来,后面再对这个图进行解释:

 

autoconf.h文件是由许许多多的Kconfig文件生成的(注:其实Kconfig来源于Linux系统,NCS或者Zephyr对其进行了继承和定制),基本上每个模块都有自己的Kconfig文件,比如蓝牙controller模块:

  

使用文本编辑器打开Kconfig,你将会看到它的内容大概如下所示:

  

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

除了Kconfig,autoconf.h还有一个输入来源:本项目的配置文件。前面说过,Kconfig文件都是模块自带的,模块为每一个选项都设了一个默认值,如果你想修改这个默认值,怎么办?你不需要跑到模块下面直接把Kconfig文件改了(这样不方便,而且也会影响到其他项目工程的运行)。为此NCS或者Zephyr引入了prj.conf文件,prj.conf文件内容大概如下所示:(除了prj.conf外,其实还有其他标准命令形式的conf文件也能起到同样的效果,这里我们以prj.conf为例来说明)

  

通过prj.conf,大家就可以更改默认的Kconfig选项了,而且这个更改是永久的,并只适用于本项目。

所有的Kconfig和prj.conf结合,先生成一个.conf文件,最后再生成autoconfig.h。.conf文件在如下目录:

 

其内容大概如下所示:

  

可以看出,.config文件格式更接近Kconfig和prj.conf,起到了一个中间桥梁作用。.config和autoconfig.h两者内容是可以一一对应的,因此大部分图形配置系统都是直接调用.config文件来完成图形化配置Kconfig的,前面讲的nRF Connect插件以及west menuconfig/guiconfig他们的操作对象其实都是.config文件。.config是一个临时文件,编译系统默认会以它为基准来生成autoconfig.h,所以一旦你的Kconfig或者prj.conf更新了,必须重新编译系统以更新.config文件,从而生成新的autoconfig.h文件。换句话说,像nRF Connect插件以及west menuconfig/guiconfig由于操作的是.config文件,他们的配置更改都是临时的,一旦重新编译或者更改了原始的Kconfig或者prj.conf,他们的配置更改就都失效了。

另外要强调一点,除了prj.conf可以更改默认配置,其他标准命名形式的conf文件,比如<board>.conf,也是可以起到同样的效果,这里为了方便起见,只拿prj.conf为例来讲解,具体可以参考第8章的说明。

4.1.2 Device Tree( *.dts, *.overlay及devicetree_unfixed.h)

同样我们先把生成devicetree_unfixed.h的整体流程图列出来,后面再对其进行解释:

 

DeviceTree也是Linux里面的概念,NCS或者Zephyr对其进行了继承和定制。在DeviceTree里面,每一种硬件比如芯片或者板子,都可以使用DeviceTree语言进行描述。DeviceTree使用了树形结构,按照层级关系把板子包含的组件一一描述清楚,每块板子都会定义一个dts文件,比如nrf52840dk_nrf52840开发板,它对应的dts文件是nrf52840dk_nrf52840.dts,其内容如下所示:

  

可以看到板子dts文件包含了一个nrf52840_qiaa.dtsi,nrf52840_qiaa.dtsi对应的就是nRF52840这颗芯片本身的dts文件。nrf52840 dtsi里面定义的内容,nRF52840dk开发板直接包含进来,然后在此基础上进行定制,比如dtsi里面将UART0配置为关闭,nRF52840dk可以将其改配为使能;另一种需要修改的情况,nRF52840dk增加了一些其他组件,比如LED/Button/外部Flash等,这些设备都可以成为nrf52840dk_nrf52840.dts里面的一个节点。nrf52840dk_nrf52840.dts是一种比较简单的板子,因此一个dts文件就可以将其表达清楚。我们还会碰到一种情况,几块板子大部分特性都是相同的,只有少数组件不一样,这个时候,我们会把相同的地方拎出来组成一个common.dts,然后这几块板子再引用这个common.dts文件,比如目录:zephyr\boards\arm\nrf9160dk_nrf9160,里面包含两块开发板:nrf9160dk_nrf9160ns和nrf9160dk_nrf9160,两块开发板内容基本上是一样的,所以在这里把相同的内容拎出来组成了一个:nrf9160dk_nrf9160_common.dts,然后nrf9160dk_nrf9160ns.dts和nrf9160dk_nrf9160.dts再引用nrf9160dk_nrf9160_common.dts,这样拆分一下,逻辑关系更清晰,将使系统变得更灵活,扩展性更好。

  

除了<board>.dts,devicetree_unfixed.h还有一个输入来源:本项目的硬件配置文件,即overlay文件。刚才说了,每块板子都有自己的dts文件,里面描述了各个节点的状态,有时候我们会有多个项目共用同一块板子的情况,比如我们的开发板支持很多例子工程,每个例子工程的配置都不一样,有的例子需要打开uart,有的例子需要关闭uart,这种情况怎么办?你不需要跑到开发板定义目录下去更改<board>.dts或者<board>_common.dts,你只需要在本项目下定义一个<board>.overlay文件就可以实现你的目标,<board>.overlay文件内容示例如下所示:

  

通过<board>.overlay,大家就可以更改板子的默认配置,而且这个更改是永久的,并只适用于本项目

<board>.dts和<board>.overlay结合先生成一个zephyr.dts文件,最后再生成devicetree_unfixed.h。zephyr.dts文件在如下目录:

  

其内容大概如下所示:

  

可以看出zephyr.dts就是<board>.dts和<board>.overlay两个文件最终合并的结果,而且zephyr.dts和devicetree_unfixed.h是可以一一对应的,因此大家可以通过查看zephyr.dts来获知自己的硬件配置到底对不对,符不符合预期。

4.1.3 配置程序起始地址和大小

每个应用都需指定其image的ROM起始地址和大小,以及运行时所占RAM的起始地址和大小,这些都需要在工具链中进行配置的,比如Keil,是在如下界面完成相关配置:

  

在NCS中,如果是一个单image应用,程序的起始地址和大小是在Kconfig中配置的(这点NCS跟Zephyr不一样,Zephyr是在DeviceTree中配置),请使用如下两个宏进行配置:

CONFIG_FLASH_LOAD_OFFSET=0x00
CONFIG_FLASH_LOAD_SIZE=0x6f800

一般来说,无线IoT应用都是要求具有固件升级功能,为了升级固件,BootLoader就必不可少,此时一个应用至少有两个image:BootLoader对应的image,以及app对应的image,对于这种多image应用,程序起始地址和大小配置一般不通过Kconfig或者DeviceTree直接来修改,而是交由partition manager(PM)模块来管理,具体请参考7章:开发你的第一个multi-image(多image)应用

4.2 图形化查看Kconfig选项

上面说了,大家可以通过.config文件去查看最终的Kconfig配置,然后通过zephyr.dts文件去查看最终的DeviceTree配置。zephyr.dts文件不是很大,因此推荐使用这种方法去查看。但是.config文件有点长,直接查看它有点累,而且容易搞错,为此NCS三个IDE都有自己的图形化查看工具。

4.2.1 SES

进入Project->Configure nRF Connect SDK Project,如下所示:

  

 

由于一个项目的配置项太多,我们一般在右上角搜索配置项名字,找到它,然后查看它的说明。同时我们可以去尝试修改它,修改成功后,点击“Configure”,配置才能生效。通过图形化界面进行修改,我们可以很快找到合适的配置选项,当大家对系统还不是很熟的时候,推荐使用这种方式去试错。这里强调一下,通过这种方式所做的修改是临时的,一旦项目重启或者缓存刷新了,这里的更改就会失效,所以我们一般推荐使用上面的prj.conf去永久修改配置项。

对于multi-image(多image)应用,点击Project->Configure nRF Connect SDK Project,会同时出现所有image的配置菜单,其中“menuconfig”对应的是主应用的配置项(其他menuconfig对应的是子image的配置项,具体请参考7章:开发你的第一个multi-image(多image)应用),如下:

  

4.2.2 west

命令行方式使用如下命令查看项目配置:

west build -t menuconfig

执行上述命令后,将显示如下界面:

  

注:上述命令需要先安装windows-curses ,即在cmd中执行如下命令:pip install windows-curses –user,此命令的执行需要联网。如果你的电脑无法联网,建议使用guiconfig查看工程配置,其对应的命令为:

west build -t guiconfig

执行上述命令后显示的界面如下所示:

  

由于一个项目的配置项太多,我们一般使用Jump to进行搜索,找到它,然后查看它的说明。同时我们可以去尝试修改它,修改成功后,选择“Save”,配置才能生效。通过图形化界面进行修改,我们可以很快找到合适的配置选项,当大家对系统还不是很熟的时候,推荐使用这种方式去试错。

4.2.3 VS Code

项目编译成功后,就可以通过Kconfig查看配置,默认Kconfig使用nRF Kconfig查看配置项了,如下:

右键单击,你会发现VS Code也支持guiconfig和menuconfig。

 

这里强调一下,上述所有图形化配置方式(三种IDE都一样)所做的修改都是临时的,一旦项目重启或者缓存刷新了,这里的更改就会失效,所以我们一般推荐使用上面的prj.conf去永久修改配置项。

5. nRF Connect SDK中几个比较重要的目录

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

5.1 例子目录

我们先说说例子所在的目录。NCS中商业级的应用程序是放在如下目录:

nrf\applications

  

如果你的应用跟上面的应用相似,那么推荐使用上面的例子,因为他们基本上属于turn-key级的方案,跟成熟的商业应用差不多,你需要的开发工作量最少。

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

  

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

  

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

   

5.2 API目录

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

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

  

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

  

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

modules\hal\nordic\nrfx\drivers\include

  

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

5.3 板子定义目录

通过在cmd输入:

west boards

就可以查看目前Zephyr支持哪些标准板子:

  

上面这些板子都是在如下目录定义的:zephyr\boards\arm。由于Cortex-M33内核支持secure和non-secure(ns)两种应用,所以每一个Cortex-M33内核都包含两种硬件定义:安全和非安全。比如nrf9160dk,虽然物理上只有一块板子,逻辑上我们把它划分成两块板子:nrf9160dk_nrf9160和nrf9160dk_nrf9160ns,nrf9160dk_nrf9160是跑安全应用,而nrf9160dk_nrf9160ns是跑非安全应用。同样nRF5340dk,虽然物理上只有一块板子,但是它有两个核都可以供用户使用,其中app核既可以跑安全应用又可以跑非安全应用,而网络核只能跑一种应用类型,所以nrf5340dk在逻辑上就划分成三块板子:nrf5340dk_nrf5340_cpuapp(app核,跑安全应用),nrf5340dk_nrf5340_cpuappns(app核,跑非安全应用)以及nrf5340dk_nrf5340_cpunet(network核,跑非安全应用)。

  

除了这些标准Zephyr板子,NCS还有一些自定义板子,他们在如下目录:nrf\boards\arm

  

如果你要自定义自己的板子,可以参考上面例子来

6. 开发你的第一个NCS程序

现在我们开始我们第一个NCS程序或者Zephyr程序的开发,在NCS中,有如下两个现成的例子:zephyr\samples\hello_world和zephyr\samples\basic\blinky。hello_world例子就是在串口中打印一串字符串,而blinky例子就是让开发板的LED1一闪一闪,这两个程序直接就可以编译和运行,而且应该所有的Zephyr开发板都可以跑这两个程序。现在我们把这两个程序合成一个程序,即既打印字符串给串口助手,又让开发板LED1一闪一闪,同时我们把字符串打印变成循环打印,并将字符串同时输出到串口助手和RTT viewer。下面我们一步一步给大家演示这个开发过程。

本章所有代码可以到如下百度网盘链接获取,进入“开发你的第一个NCS(Zephyr)应用程序”-> “hello_world”,下载hello_world_ncsv140.rar

6.1 修改hello_world main.c文件

首先,我们以hello_world例子为基础,将这个例子拷到一个其他目录下(任何目录都可(不要有中文和空格等),只要你的环境变量都设好了,所有NCS例子的目录可以随意更改,这个真是非常方便!),这里让大家拷贝到其他目录,只是为了演示NCS编译路径的依赖性做得非常好,没有别的意思。如果你不想拷贝,也没关系,咱们可以直接在原始目录上进行修改,NCS自带git管理系统,非常方便你进行版本管理。我们做如下修改,以循环打印同一条日志:

  

void main(void)
{
    while(1)
    {
        printk("Hello World! %s\n", CONFIG_BOARD);
        k_sleep(K_SECONDS(1));
    }
}

 

6.2 在项目中添加一个新文件blinky.c

然后把blinky代码加到hello_world,这里面就会用到添加一个新文件的技能。我们先把zephyr\samples\basic\blinky\src里面的main.c改名为blinky.c,然后拷贝到hello_world\src目录下。如何把blinky.c添加到项目中来呢?推荐的方法是修改CMakelists.txt文件,通过它加入新的编译文件或者库,或者包含新的目录。我们做如下修改,就可以把blinky.c加进来了:

  

target_sources(app PRIVATE src/blinky.c)

这种方式不管是NCS项目还是Zephyr项目,都能工作成功,而且是我们首推的方式。至于CMakeLists的语法怎么理解和使用?还是那句话:参考例子,不要专门去学习。除了修改CMakeLists方法外,SES还引入了一种图形化方法,如下所示:

   

这种方式只有nrf\samples这个目录下的例子才支持,zephyr\samples这个目录下的例子默认不支持这种方式。

上面图形化方式添加文件,本质上跟修改CMakeLists方法一样,它只不过在CMakeLists文件里面预先放入如下两行标识,这样SES就可以把新添加的文件塞到这两行标识之间:

  

我们把这两行标识放在我们的hello_world例子里面,这样我们也可以通过SES添加文件了。blinky.c文件添加成功后,相应的CMakelists文件也更新了,如下所示:

  

6.3 修改blinky.c文件

好了,现在blinky.c文件已经添加成功了,我们再对其进行修改,修改代码如下所示:

  

void blinky_thread(void)
{
    const struct device *dev;
    bool led_is_on = true;
    int ret;

    dev = device_get_binding(LED0);
    if (dev == NULL) {
        return;
    }

    ret = gpio_pin_configure(dev, PIN, GPIO_OUTPUT_ACTIVE | FLAGS);
    if (ret < 0) {
        return;
    }

    while (1) {
        gpio_pin_set(dev, PIN, (int)led_is_on);
        led_is_on = !led_is_on;
        k_msleep(SLEEP_TIME_MS);
    }
}

K_THREAD_DEFINE(blinky_thread_id, 1024, blinky_thread, NULL, NULL,
        NULL, 7, 0, 0);

如上,我们直接把blinky代码变成另外一个线程以达到我们的目的(当然你也可以把blinky代码变成一个模块,以供hello_world的main调用,这里就不演示这种方式了)。

6.4 编译和运行你的第一个hello_world程序

下面我们以nrf52840dk_nrf52840为例来跑上面的工程,大家也可以用其他开发板来跑,比如nrf5340dk_nrf5340_cpuapp,nrf9160dk_nrf9160ns,这个例子都是支持的

SES工程装载如下所示,然后编译和下载:

  

或者使用west命令进行编译和下载,命令如下所示:

west build -b nrf52840dk_nrf52840 -d build_nrf52840dk_nrf52840 -p
west flash -d build_nrf52840dk_nrf52840

或者使用VS Code,其界面如下所示:

程序下载成功后,串口助手在循环打印“Hello World! nrf52840dk_nrf52840”,然后开发板上LED1在一闪一闪。串口截图如下:

  

我们第一个hello_world程序算是正式大功告成了。

6.5 使用prj.conf和<board>.overlay配置项目

我们现在对hello_world再做一个修改:把log输出到RTT viewer而不是串口助手。传统的nRF5 SDK需要做两件事来达到这个目的:一是修改sdk_config.h文件,二是添加相应文件。但是在NCS里面,你只需在prj.conf里面做如下修改即可达到同样的目的:

  

CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=n

运行后结果如下所示:

  

按道理说,上面的例子已经把日志输出改为RTT Viewer了,这时去测量nrf52840dk电流,应该很低才对,但实际上此时电流大概为500多微安。这是什么原因呢?我们先看一下Kconfig,如下:

再看一下zephyr.dts文件,如下:

  

可以看出,uart0和uart1都处于使能状态,从而导致功耗偏高。我们通过nrf52840dk_nrf52840.overlay文件,将uart0和uart1关闭,修改如下所示:

  

上面修改会把UART关闭,但是UART驱动还是会加载。如果你确认不需要串口(UART)的话,那么我们可以把相应的Kconfig关掉,即在prj.conf把serial模块关掉(注:serial模块在所有项目默认是打开的),如下

CONFIG_SERIAL=n

通过Kconfig关掉串口,此时即使zephyr.dts文件把UART使能了,即“status = "okay"”,串口也是关掉的,因为一旦Kconfig禁掉串口,就意味着串口驱动模块就不会加载,代码都没有,就无从谈起对UART的操作(上电UART默认是关的),这样就算dts文件里面把UART打开,对工程来说,就是改变了几个宏定义的值,仅此而已。

重新编译和下载,这时我们再去测量nrf52840dk电流,此时电流只有几微安,符合预期。

7. 开发你的第一个multi-image(多image)应用

有时一个应用会包含多个image,最典型的情况有三种:一是BootLoader+app,BootLoader一个image,app一个image,二是spm(tfm)+app,spm(tfm)一个image,app一个image(注:spm/tfm是为Cortex-M33非安全应用设计的一个安全引导程序,像nRF9160/nRF5340这种M33内核,所有非安全应用都需要装载spm或者tfm),三是双核,应用核一个image,网络核一个image(比如nRF5340的应用核和网络核)。在NCS中,编译一个项目,会同时生成多个不同image,这种应用就称为multi-image应用。image应用其生成的hex名为:zephy.hex,multi-image应用其生成的hex名为:merged.hex,merged.hex意味着这个项目会生成多个image,然后将他们合并(merge)成一个hex:merged.hex,因此multi-image应用对用户来说,最终也只有一个image,用户只需下载这个image即可。以nrf\samples\nrf9160\http_application_update为例,这是一个典型的多image应用,它包含3个image:BootLoader image,spm image以及app image,这三个image都是在nrf\samples\nrf9160\http_application_update编译目录下生成的,编译成功后,我们将同时看到三个image的编译目录,如下所示:

  

7.1 分区管理(Partition Manager)

传统的SDK,如果一个产品包含多个image,那么每个image都会对应一个项目,用户需要同时维护多个项目,而且需要同时维护这几个项目的版本关联关系。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的。

7.2 多image的hello_world程序开发

我们现在将第6章的hello_world程序跑在多image环境下。现在我们以nrf9160dk_nrf9160ns为例,重新编译一下前述的hello_world程序,由于nrf9160dk_nrf9160ns为非安全应用,前面也说过,所有Cortex-M33非安全应用都会默认使能spm/tfm模块,所以spm/tfm image将会自动加载进来并进行编译。

  

装载成功后,你可以看到build和download的target都变成:merged.hex,而不是以前的zephyr.hex,如下:

  

而且build目录也会多一个spm的build目录,如下所示:

  

程序跑起来后,log如下所示:

  

跟第6章一样,我们再定义一个overlay文件,以将uart0关掉,此时我们去测9160dk的电流,应该只有几微安,但实际上我们测下来还是500多微安,这是为什么呢?因为spm/tfm还使能了serial和uart0,这个可以从spm的.config和zephyr.dts文件得到验证,如下:

  

所以uart0在spm/tfm中打开了,然后程序跳到app,uart0还是处于打开状态,从而导致功耗偏高。那么我们怎么可以便捷的关闭spm/tfm里面的serial模块和uart0?方法一,大家跑到spm/tfm例子里面,然后定义前述的prj.conf以关闭serial模块,但这种方法会影响其他例子;方法二,我们在parent应用中定义spm/tfm的prj.conf,这样spm/tfm只在这个parent应用中关闭了serial模块,对其他应用不产生任何影响。为了将parent应用中的conf和overlay文件(overlay其实对关掉UART没有意义)传给spm/tfm,需要用到NCS编译系统的编译变量,给相应的变量赋值,从而可以将相关文件传递给spm/tfm,具体请参考8

我们对CMakelists文件做如下修改:

  

if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/spm.conf")
  set(spm_CONF_FILE
    prj.conf
    ${CMAKE_CURRENT_LIST_DIR}/spm.conf
  )
endif()

if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/spm.overlay")
  set(spm_DTC_OVERLAY_FILE "${CMAKE_CURRENT_SOURCE_DIR}/spm.overlay")
endif()

通过设置spm_CONF_FILE和spm_DTC_OVERLAY_FILE两个变量,我们将spm.conf和spm.overlay两个文件传给了spm image项目,从而达到控制spm image编译配置目的。注意:如果要把conf和overlay文件传给tfm image,那么就要使用tfm_CONF_FILE和tfm_DTC_OVERLAY_FILE这两个变量

spm.conf我们定义如下选项:

CONFIG_SERIAL=n
CONFIG_UART_CONSOLE=n

spm.overlay我们定义如下节点:

&uart0 {
    status = "disabled";    
};

 

上述这种通过CMake系统变量去修改子image配置的做法,让很多人觉得有点难,为此从ncs v1.5.0开始引入了一种更简单的方式:用户只需在父应用目录下建一个child_image目录,然后把子image的配置文件放在这个目录,配置文件的名字必须跟子image的名字一模一样(子image的名字见8),这样系统就会自动把该目录下的配置传送给子image,如下所示:

我们再重新装载hello_world程序,可以看到在spm中,serial和uart0都关闭了,如下:

  

此时再去测量9160dk电流,就降到几微安了。

一点额外功能

上面日志要不打印到串口助手,要不打印到RTT viewer,而且都是堵塞式打印。我们现在将打印改成异步的,而且同时打印到串口助手和RTT viewer。为此我们将logging模块打开,同时设置如下Kconfig:

  

CONFIG_ASSERT=y
CONFIG_ASSERT_LEVEL=2

CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=2
CONFIG_LOG_BACKEND_RTT=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_BACKEND_RTT_MODE_DROP=y
CONFIG_LOG_MODE_OVERFLOW=y
CONFIG_LOG_PRINTK=y
CONFIG_LOG_PRINTK_MAX_STRING_LENGTH=256
CONFIG_LOG_BUFFER_SIZE=4096
CONFIG_LOG_BACKEND_RTT_MESSAGE_SIZE=256
CONFIG_LOG_STRDUP_BUF_COUNT=64
CONFIG_LOG_STRDUP_MAX_STRING=64
CONFIG_LOG_BACKEND_SHOW_COLOR=n
CONFIG_LOG_BACKEND_FORMAT_TIMESTAMP=n
CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=1024

CONFIG_CONSOLE=y
CONFIG_USE_SEGGER_RTT=y
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=y

只需做上述修改,就可以达到我们前述的目的,非常方便(无需添加文件,无需修改include目录,这就是NCS!)。这次大家可以使用nrf5340dk_nrf5340_cpuapp跑一下试试。

注:上述日志配置直接从nrf_desktop拷贝过来,大家以后也可以参考它来使能自己的log模块。 

8. NCS编译系统几个要注意的点

8.1 NCS几个重要的编译系统变量

在NCS或者Zephyr编译系统中,有几个变量非常重要,每个人最好掌握他们,把他们使用好,会让你的编译变得得心应手,这几个变量是:

ZEPHYR_BASE,用来指示你的Zephyr仓库的绝对目录,比如取值:C:\Nordic\NCS\SDK\tag\v1.4.0\zephyr

BOARD,用来指定编译用的板子,比如取值:nrf52840dk_nrf52840

CONF_FILE,用来指定项目的conf文件,如果没有指定,默认用prj.conf,详细说明见8.2

DTC_OVERLAY_FILE,用来指定项目的overlay文件,如果没有指定,默认用<board>.overlay,详细说明见8.3

PM_STATIC_YML_FILE,用来指定parent应用,即app的pm_static文件,如果没有指定,默认用pm_static.yml,详细说明见7.1

CMAKE_BUILD_TYPE,命令行可以通过这个变量传递一个参数给CMakelists.txt文件或者其他build过程。

上述变量不分大小写,所以CONF_FILE和conf_file是一样的,其他类同。因为这些变量是针对每一个image的,所以每一个image都有自己的board,conf_file,dtc_overlay_file等。对于单image应用,这个好理解也好区分;那如果是多image应用,该如何区分每个image的conf_file和dtc_overlay_file呢?这可以通过使用image专用变量来实现。如前所述,conf_file这个变量本身是作用于app image的,实际上你可以把这个变量看成:app_conf_file,只不过默认都是app image,所以就把app_省略了。当你需要在parent应用中去设置child应用的conf_file,你就不能直接使用conf_file这个变量了(因为它是用来设置parent应用本身的conf文件),而需使用childImageName_conf_file,比如上面的hello_world程序,我们使用了spm_conf_file这个变量,用来设置子image spm的conf_file。跟conf_file变量一样,dtc_overlay_file变量使用了同样的规则。NCS中目前主要有如下4个child image:

  • mcuboot。可升级的第三方开源BootLoader
  • b0(NSIB)。不可升级的Nordic自研BootLoader
  • spm。Cortex-M33非安全应用的安全引导程序,Nordic自己开发的。
  • tfm。trusted-firmware-m,作用跟spm相似,但是符合PSA标准,由第三方开发。

除了上述child image,在编译nRF5340 app核的时候,我们也可以自动包含如下 network核的child image:

  • b0n。nRF5340 网络核的bootloader。(注:b0n跟上面的b0没有任何关系)
  • hci_rpmsg。nRF5340 网络核的蓝牙controller
  • 802154_rpmsg。nRF5340 网络核的802.15.4 controller

通过上面的childImage名字加上前面的编译系统变量,就可以通过parent应用去控制child应用的编译过程,大大方便了多image的开发流程。上述这种通过CMake系统变量去修改子image配置的做法,让很多人觉得有点难,为此从ncs v1.5.0开始引入了一种更简单的方式:用户只需在父应用目录下建一个child_image目录,然后把子image的配置文件放在这个目录,配置文件的名字必须跟子image的名字一模一样,即上面的mcuboot,spm这些特定名字,这样系统就会自动把该目录下的配置传送给子image。

关于上述编译系统变量的使用,大家可以参考:nrf\applications\nrf_desktop和nrf\applications\asset_tracker。

nrf_desktop虽然只有一个CMakeLists.txt,但实际上这个CMakeLists.txt包含了20多个项目,它是怎么做到的呢?它就是通过编译系统变量来实现的。比如要设置某一块板子对应的某一个工程的conf文件,在ncs v1.4.0等老版本NCS中使用了如下语句:

  

set(CONF_FILE "configuration/${BOARD}/app_${CMAKE_BUILD_TYPE}.conf")

比如说,$BOARD=nrf52840dk_nrf52840,$CMAKE_BUILD_TYPE=ZDebug_keyboard,那么它对应的conf文件就是如下这个:

  

在最新版nrf_desktop,比如ncs v1.9.1,conf_file这个变量改为通过命令行方式传进来而不是上面的CMakelists文件,如下所示:

west build -b nrf52840dk_nrf52840 -d build_nrf52840dk_nrf52840 -- -DCONF_FILE=prj_release.conf

 

在ncs v1.4.0老版本NCS中的asset_tracker通过CMakeLists.txt文件定义了子image spm的conf文件,以及定义了一个静态的pm文件,如下所示:

  

set(spm_CONF_FILE ${CMAKE_CURRENT_SOURCE_DIR}/spm.conf)
set(PM_STATIC_YML_FILE
  ${CMAKE_CURRENT_SOURCE_DIR}/configuration/${BOARD}/pm_static.yml
  )

在最新版asset_tracker_v2,比如ncs v1.9.1,则通过child_image目录方式实现了上述同样功能,如下:

大家在写自己的多image应用的时候,可以多借鉴上面的例子,上面三种方式:CMakelists,命令行以及child_image目录,都是可行的,大家选择适合自己的就好。目前看起来,child_image目录方式最简单,所以最新的例子基本上都换成这种方式了,但是前两种方式仍然支持,本质上这三种方式原理是一样的,只不过child_image方式封装了一层,实际上编译系统最终也是把child_image目录下的配置文件赋给了上述系统变量。

8.2 conf文件命名规则及编译顺序

对于单image应用或者多image应用的父应用(主应用),我们一般都是通过prj.conf去配置项目的,除了prj.conf,其实符合如下命名标准的conf文件也可以被系统自动加载进来。

  1. 首先读取CONF_FILE变量,我们可以将多个conf文件都赋给这个变量(每个conf文件之间以分号或者空格隔开),这些配置文件最终会合并成一个。我们可以通过三种方式设置CONF_FILE变量
    • 通过命令行方式传递:-DCONF_FILE=<file1.conf;file2.conf>
    • 在CMakeLists.txt中并且必须在调用find_package(Zephyr)之前(也就是包含boilerplate.cmake之前)
    • 通过CMake变量cache
  2. 否则,系统将使用应用目录下的prj_<build>.conf 和boards/<BOARD>_<build>.conf两者的合并结果
  3. 否则,系统将使用应用目录下的prj_<BOARD>.conf 
  4. 否则,系统将使用应用目录下的boards/<BOARD>.conf和prj.conf 的合并结果
  5. 否则,系统将使用应用目录下的prj.conf 
  6. 如果你在应用根目录下或其他目录下放其他名字的conf文件,即不是上面这些情况,那么这个conf文件将被系统忽略,起不到任何作用

记住:如果同一个Kconfig选项或者符号被配置多次,以最后一次配置为准

8.3 overlay文件命名规则及编译顺序

对于单image应用或者多image应用的父应用(主应用),系统按照如下顺序自动装载overlay文件:

  1. 首先读取DTC_OVERLAY_FILE变量,我们可以同时将多个overlay文件赋给这个变量(每个overlay文件之间以分号或者空格隔开),这些overlay文件最终合并为一个文件。我们可以通过如下方式设置DTC_OVERLAY_FILE变量
    • 通过命令行方式传递:-DDTC_OVERLAY_FILE="file1.overlay;file2.overlay"
    • 在CMakeLists.txt中并且必须在调用find_package(Zephyr)之前(也就是包含boilerplate.cmake之前)
  2. 否则,系统将使用应用目录下的boards/<BOARD>.overlay 
  3. 否则,系统将使用应用目录下的<BOARD>.overlay
  4.  如果你在应用根目录下或其他目录下放其他名字的overlay文件,即不是上面这些情况,那么这个overlay文件将被系统忽略,起不到任何作用

 

 

posted on 2020-12-22 18:02  iini  阅读(52476)  评论(13编辑  收藏  举报

导航