如何调试nRF5 SDK


本文将讲述Nordic nRF5 SDK的主要调试手段,以帮助大家快速定位问题,并解决问题。一般来说,你可以通过打log方式,IDE的debug模式,SDK自带的app_error_check函数,以及命令行方式等多种手段来调试你的代码。

1. 通过打log方式进行调试

nRF5 SDK支持UART和SWD J-Link(RTT)两种底层通信方式来打印日志,SDK14之后日志也可以通过蓝牙或者Flash进行输出和存储打印,一般来说,UART和SWD用得比较多,其中UART使用串口助手来查看日志,SWD使用J-Link RTT Viewer(仅适用Windows)或者J-Link RTT Client(Windows/Mac/Linux系统)来查看打印日志。由于UART日志打印方式会占用一个UART口,而大部分nRF5芯片都只有一个UART口,从而导致资源冲突,为此推荐大家使用RTT方式来打印日志,从而可以将UART口留给正常应用,更重要的是RTT打印方式功耗非常低(几乎可以忽略不计),大家可以在正式release的产品中也使能它,从而发现产品部署后有可能会出现的问题(UART打印方式功耗非常高,在正式release产品中必须关掉它)。

如果使用SWD接口进行日志打印,那么你可以使用J-Link RTT Viewer或者RTTClient来显示日志,二者选其一即可。在Windows平台推荐使用RTT viewer,否则使用RTT Client。

1.1 J-Link RTT Viewer

RTT viewer配置方式如下所示:

 

RTT viewer日志打印窗口如下所示:(使用的是SDK15.3中的ble_app_hrs例子,下同)

 

1.2 J-Link RTTClient

RTT client配置方式如下所示(直接使用jlink命令进行配置):

 

RTT client打印窗口如下所示:

 

 

1.3 UART串口助手

串口助手配置方式如下所示:

  • Baud rate: 115200
  • 8 data bits
  • 1 stop bit
  • No parity
  • HW flow control: None

随便选择一款你熟悉的串口助手,比如Putty或者Termite,Termite打印窗口如下所示:

 

Putty打印窗口如下所示:

 

 

1.4 日志打印模块nRF_Log

nRF5 SDK日志打印功能是通过nRF_Log模块实现的(上面展示的日志都是通过nRF_Log打印出来的),SDK包含的大部分例子都自带打印功能,也就是说包含了nRF_Log模块。一般来说,例子都是默认使用UART进行打印的,如果需要改为RTT进行打印,需要对nRF_Log模块进行配置。在具体讲述nRF_Log模块的配置选项之前,先大概讲述一下nRF_Log的工作原理。

nRF_Log工作原理

nRF_Log模块包含前端和后端两部分代码。前端是面向用户的打印接口,比如NRF_LOG_INFO,它把用户要打印的数据放在一块RAM中。后端用来具体实现打印功能,即把前端RAM中的数据通过不同的后端接口打印出去。目前nRF_Log支持的后端接口有:UART,RTT,Flash和蓝牙。不管采用哪一种后端接口,对用户来说,其调用的前端API是一样的,nRF_Log模块会根据用户的配置自动适配相应后端接口,而且用户可以同时使能多个后端接口,即把日志同时打印到多个后端端口上。nRF_Log模块可以单独使能或禁止某一个模块的打印功能,比如advertising模块,当你调试advertising模块的时候,可以把log打开,调试完毕,把log关闭以让log界面变得更清爽。nRF_Log模块还可以设置打印的级别(Level),如果不分级别的话,打印界面会包含很多打印信息,让我们每次调试都要花费很多时间去寻找自己想要的日志上。设定级别后,我们可以有选择的打印需要的日志信息,没问题时,我们只打印info级别的日志;有问题时,我们就可以把所有debug级别的日志都打印出来。nRF_Log还有一个功能:Deferred,如果不使能Deferred,那么调用NRF_LOG_INFO等API的时候,立马就Flush,即把日志打印出去;如果使能了Deferred,那么调用NRF_LOG_INFO等API的时候,只是把打印数据放在RAM中,真正的打印由main函数中的NRF_LOG_PROCESS完成,这样可以最大程度降低打印对应用本身的影响,尤其在执行一些时序很关键的操作的时候。nRF_Log还支持时间戳打印,即在每条日志之前加上时间戳信息。

nRF_Log配置

从SDK12以后,nRF_Log模块的配置主要放在sdk_config.h文件中,以工程nRF5_SDK_15.3.0_59ac345\examples\ble_peripheral\ble_app_hrs\pca10040\s132\arm5_no_packs为例,nRF_Log的配置选项如下所示:

 

注意:nRF5 SDK v11.0.0及以前版本是没有sdk_config.h文件的,此时你需要到options for target->C/C++->define里面定义一个宏(Keil工程),如果定义“NRF_LOG_USES_UART=1”选择UART日志打印;如果定义”NRF_LOG_USES_RTT=1” 则选择RTT日志打印,如下:

 

还是以nRF5_SDK_15.3.0_59ac345\examples\ble_peripheral\ble_app_hrs\pca10040\s132\arm5_no_packs为例,当nRF_Log配置为info级别,无timestamp,打印信息如下(与前面配置一样)

 

当nRF_Log配置为debug级别,打开timestamp,打印信息如下所示:

 

 

 

2. 使用IDE调试界面进行调试

Nordic产品支持单步,断点,寄存器查看,内存查看,call stack查看等所有常规调试手段。需要注意的是,由于softdevice在底层要维持一定的时钟以及处理很多中断事件,一旦蓝牙跑起来后,只能用一个断点进行全速跑,也就是说,执行完一个断点后,程序内部逻辑和时序已经紊乱,此时不能再继续全速跑第二个断点。如果要看第二个断点的内容,只能删掉第一个断点,重新开始执行。

Keil IDE

通过以下方式打开nRF5 SoC内部寄存器查看窗口:

 

用得比较多的几个Keil窗口:

 

Segger embedded studio IDE

SES的调试界面如下所示(跟Keil很像):

 

这里要特别强调一下,所有SES工程都包含2个版本:Release版和Debug版,Release是没有包含debug信息以节省代码空间,所以如果你要debug一个工程,请先选择debug版本,然后再进行debug,如下所示:

 

3. nRF5 SDK自带的APP_ERROR_CHECK函数

APP_ERROR_CHECK是nRF5 SDK定义的一个用来检查API返回值是否正确的函数,在nRF5 SDK中,NRF_SUCCESS(0)为正确返回值,其它返回值皆为错误值。nRF5 SDK所有协议栈API调用,以及SDK库函数调用,都会用APP_ERROR_CHECK去检查调用的返回值。当出现非法调用时,比如传入的实参不对,API返回值就不会为NRF_SUCCESS,此时APP_ERROR_CHECK就会派上大用场。通过查看APP_ERROR_CHECK函数定义,如下所示:

NRF_BREAKPOINT_COND;

    // On assert, the system can only recover with a reset.

#ifndef DEBUG

    NRF_LOG_WARNING("System reset");

    NVIC_SystemReset();

#else

    app_error_save_and_stop(id, pc, info);

#endif // DEBUG

你会发现APP_ERROR_CHECK行为受宏DEBUG和有没有挂仿真器两个因素的影响,当没有定义DEBUG宏时,系统将直接产生软复位;当定义了DEBUG宏并且没有挂仿真器时,系统将把错误信息保存在call stack中。不管怎么配置,APP_ERROR_CHECK都会把相应的错误信息打印出来,以方便你去排查问题,如下所示。通过打印出来的错误信息,你就可以知道是哪个文件哪一行代码出了什么类型的错误。

 

注意在SDK14以前,app_error_check不会主动把错误信息打印出来,而是把错误信息存在RAM中,然后通过debug模式可以直接查看相关错误信息,如下所示:

 

默认情况下,nRF5 SDK是没有定义DEBUG宏的,所以一旦函数返回值不对,系统就会复位。这里要特别指出的是,在你开发调试过程中,经常会碰到复位的情况,这其中大部分都是由APP_ERROR_CHECK引起的。针对这种情况的复位,你只需要在options for target->C/C++->define里面定义一个宏:DEBUG,就可以快速找出是哪一个文件哪一行代码引出的复位,以及复位原因是什么,如下所示:

 

 

4.使用命令行(CLI)方式进行调试

打log方式只能单方面输出,而不能接受终端的输入,在很多情况下,我们需要动态调整log信息,比如动态更改log的级别,动态更改log的颜色等等,这个时候就需要用到nrf_cli模块,跟nRF_Log相似,nrf_cli同时支持5种后端接口:UART,RTT,BLE,Flash和USB CDC。由于nrf_cli也有log输出功能,因此nRF_Log模块可以直接选择nrf_cli为其后端接口。这里再次强调一下,nrf_log和nrf_cli是两个完全独立的模块,但是nrf_log可以选用nrf_cli为其后端接口,由nrf_cli完成后端打印功能。SDK中提供了三个cli的例子,分别为:

  • \examples\peripheral\cli\pca10040\blank\arm5_no_packs
  • \examples\ble_peripheral\experimental\ble_app_cli\pca10040\s132\arm5_no_packs
  • \examples\ble_central_and_peripheral\experimental\ble_app_interactive\pca10040\s132\arm5_no_packs

如果大家的应用需要命令行交互方式,可以参考上面3个例子,以examples\peripheral\cli\pca10040\blank\arm5_no_packs为例,交互成功后的界面如下所示: