陈拓 2022/07/28-2022/11/04
1. 概述
在《WSL构建nRF5 SDK + ARM GCC开发环境》
https://zhuanlan.zhihu.com/p/544907537
https://blog.csdn.net/chentuo2000/article/details/125933307?spm=1001.2014.3001.5502
一文中我们已经构建了nRF5 SDK + ARM GCC的基本开发环境。
本文我们用RTT打印调试日志。
nRF5芯片只有一个串口,如果串口已经被使用,或者手头的板子上串口没有引出,这时可以使用JLink仿真器的RTT输出调试信息。
在上文中我们为了兼顾以前的项目安装了nRF5 SDK 14.2,现在我们已经升级到了nRF5_SDK_17.1.0_ddde560。
2. 编译和烧写项目
我们以ble_app_blinky为例:
~/nrf/nRF5_SDK_17.1.0_ddde560/examples/ble_peripheral/ble_app_blinky
2.1 设置sdk_config.h
修改sdk_config.h文件。
~/nrf/nRF5_SDK_17.1.0_ddde560/examples/ble_peripheral/ble_app_blinky/pca10040/s132/config/sdk_config.h
- 使能NRF日志NRF_LOG_ENABLED
#ifndef NRF_LOG_ENABLED
#define NRF_LOG_ENABLED 1
#endif
- 使能RTT日志NRF_LOG_BACKEND_RTT_ENABLED
#ifndef NRF_LOG_BACKEND_RTT_ENABLED
#define NRF_LOG_BACKEND_RTT_ENABLED 1
#endif
- 禁用串口输出日志NRF_LOG_BACKEND_UART_ENABLED
#ifndef NRF_LOG_BACKEND_UART_ENABLED
#define NRF_LOG_BACKEND_UART_ENABLED 0
#endif
2.2 初始化RTT打印功能
在主函数main中,第一步就是初始化打印日志功能 log_init()。
int main(void)
{
// Initialize.
log_init();
log_init()函数
static void log_init(void)
{
ret_code_t err_code = NRF_LOG_INIT(NULL);
APP_ERROR_CHECK(err_code);
NRF_LOG_DEFAULT_BACKENDS_INIT();
}
初始化之后就可以用RTT打印了,像这样:
NRF_LOG_INFO("Blinky example started.");
2.3 编译项目
cd ~/nrf/nRF5_SDK_17.1.0_ddde560/examples/ble_peripheral/ble_app_blinky/pca10040/s132/armgcc
编译:make
cc1: all warnings being treated as errors错误。
在Makefile文件中找到CFLAGS += -Wall -Werror
行首加#号注释掉:
#CFLAGS += -Wall -Werror
再编译:
2.4 烧写hex文件
- 清除memory
nrfjprog -f NRF52 --eraseall
- 下载烧录项目的hex文件
nrfjprog -f nrf52 --program nrf52832_xxaa.hex -sectorerase --verify
- 重启nRF52832
nrfjprog -f nrf52 --reset
- 下载烧录蓝牙协议栈
和上文中的项目不同的是本项目需要有协议栈的支持,协议栈所在的目录:
~/nrf/nRF5_SDK_17.1.0_ddde560/components/softdevice/s132/hex/s132_nrf52_7.2.0_softdevice.hex
要注意的是协议栈的版本和SDK版本以及芯片型号是配套的。
nrfjprog -f nrf52 --program s132_nrf52_7.2.0_softdevice.hex -sectorerase --verify
重启nRF52832
nrfjprog -f nrf52 --reset
3. 用JLink仿真器的RTT打印Log信息
3.1 RTT日志的打印方式
在程序中RTT使用NRF_LOG_INFO函数打印日志信息,和串口打印语句printf不同,RTT的NRF_LOG_INFO语句是否立即输出要在sdk_config.h文件中进行设置:
// <q> NRF_LOG_DEFERRED - Enable deffered logger.
// <i> Log data is buffered and can be processed in idle.
#ifndef NRF_LOG_DEFERRED
#define NRF_LOG_DEFERRED 1
#endif
默认设置为NRF_LOG_DEFERRED 1
这时NRF_LOG_INFO语句并不立即输出打印,而是把打印数据放在RAM中,真正的打印由main函数中的NRF_LOG_PROCESS完成。
在官方的例程中是在CPU空闲时调用NRF_LOG_PROCESS函数打印,代码如下:
- main函数
int main(void)
{
// Initialize.
log_init();
leds_init();
timers_init();
buttons_init();
power_management_init();
ble_stack_init();
gap_params_init();
gatt_init();
services_init();
advertising_init();
conn_params_init();
// Start execution.
NRF_LOG_INFO("Blinky example started.");
advertising_start();
// Enter main loop.
for (;;)
{
idle_state_handle();
}
}
- idle_state_handle函数
/**@简要说明 处理空闲状态的函数 (main loop)。
*
* @详细说明 如果没有在等待的日志操作,则休眠到下一个事件发生。
*/
static void idle_state_handle(void)
{
if (NRF_LOG_PROCESS() == false)
{
nrf_pwr_mgmt_run();
}
}
语句NRF_LOG_PROCESS()执行打印输出。
这种打印策略可以减少在程序执行过程中打印输出对于程序正常运行的影响,因为打印输出是要耗费时间的。
3.2 日志级别
上面我们在程序中使用NRF_LOG_INFO函数打印输出调试信息。为了使打印的日志更具有针对性,比如只打印错误部分日志,或者只打印警告部分日志,便有了日志分级。
日志分5个级别,在sdk_config.h中设置:
// <o> NRF_LOG_DEFAULT_LEVEL - Default Severity level
// <0=> Off
// <1=> Error
// <2=> Warning
// <3=> Info
// <4=> Debug
#ifndef NRF_LOG_DEFAULT_LEVEL
#define NRF_LOG_DEFAULT_LEVEL 3
#endif
<0=> Off:关闭日志输出
<1=> Error:输出错误信息,对应的Log输出函数为NRF_LOG_ERROR
<2=> Warning:输出警告信息,对应的Log输出函数为NRF_LOG_WARNING
<3=> Info: 输出基本信息,对应的Log输出函数为NRF_LOG_INFO
<4=> Debug:输出调试信息,对应的Log输出函数为NRF_LOG_DEBUG
高级别的日志输出包含低级别的输出:
如程序中的默认设置NRF_LOG_DEFAULT_LEVEL 3,这时1、2、3级别的日志就会输出。
如果设置NRF_LOG_DEFAULT_LEVEL 4,那么所有4个级别的日志都会输出。
3.3 用J-Link RTT Viewer打印信息
J-Link RTT的3个工具都可以用,我们用J-Link RTT Viewer。
点开J-Link RTT Viewer之后先进行设置:
OK
RTT LOG显示RTC初始化成功,之后的程序出现错误,我们不知道问题出在哪里。下面我们借助RTT来找出问题所在。
4. 让RTT打印程序出错的文件和行号
4.1 APP_ERROR_CHECK函数
APP_ERROR_CHECK是nRF5 SDK定义的一个用来检查API返回值是否正确的函数,在nRF5 SDK中,NRF_SUCCESS(0)为正确返回值,其它返回值皆为错误值。
- 查看APP_ERROR_CHECK函数定义
在VSCode中用鼠标右击APP_ERROR_CHECK > 转到定义
查看APP_ERROR_CHECK的代码:
/**@摘要 如果提供的错误代码不是NRF_SUCCESS (0),则调用错误处理函数的宏。
*
* @参数[in] ERR_CODE 提供给错误处理程序的错误代码。
*/
#define APP_ERROR_CHECK(ERR_CODE) \
do \
{ \
const uint32_t LOCAL_ERR_CODE = (ERR_CODE); \
if (LOCAL_ERR_CODE != NRF_SUCCESS) \
{ \
APP_ERROR_HANDLER(LOCAL_ERR_CODE); \
} \
} while (0)
APP_ERROR_CHECK调用了错误处理程序函数的宏APP_ERROR_HANDLER。
用同样的方法找到APP_ERROR_HANDLER
/**@brief 用于调用错误处理函数的宏。
*
* @param[in] ERR_CODE 提供给错误处理程序的错误代码。
*/
#ifdef DEBUG
#define APP_ERROR_HANDLER(ERR_CODE) \
do \
{ \
app_error_handler((ERR_CODE), __LINE__, (uint8_t*) __FILE__); \
} while (0)
#else
#define APP_ERROR_HANDLER(ERR_CODE) \
do \
{ \
app_error_handler_bare((ERR_CODE)); \
} while (0)
#endif
如果定义了DEBUG就调用错误处理程序app_error_handler,它会显示行号。否则调用app_error_handler_bare,不显示行号,就像上面看到的那样。
为了调用app_error_handler,我们要在Makefile里设置一下。
4.2 让APP_ERROR_CHECK显示错误码、文件名和出错行号
在Makefile文件中找到CFLAGS的最后一行,添加一行:CFLAGS += -DDEBUG
编译烧写。
之后需要将J-Link RTT Viewer断开重新连接一下:
File > Disconnect
File > Disconnect
这样就可以看到新的信息了,错误码、文件名和行号都有了。
错误码7表示无效参数。
我们看183行前面一条出错的语句:
sd_ble_gap_device_name_set函数有3个参数:
&sec_mode, (const uint8_t *)DEVICE_NAME, strlen(DEVICE_NAME)
其中sec_mode定义如下:
ble_gap_conn_sec_mode_t sec_mode;
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);
DEVICE_NAME定义如下:
strlen(DEVICE_NAME)是DEVICE_NAME长度长度。
这些都是官方例程中的代码,不应有错。
5. 消除编译器引起的错误
同样的的代码在Win10系统Keil中编译时没问题。看来问题出在不同的编译器对代码的处理不同。这种错误可能和编译器的优化有关,我们关闭编译器的优化试试。
在Makefile中优化设置如下:
# Optimization flags
OPT = -O3 -g3
5.1 GCC优化选项
-O0:默认的优化选项,减少编译时间和生成完整的调试信息。
-O/-O1:这两个都是开启level 1的编译优化。开启编译优化会导致更长的编译时间,对于大函数还会消耗更多的内存空间。level1的编译优化下,编译器会尝试减少代码段大小和优化程序的执行时间,但不执行需要消耗大量编译时间的优化。
-O2:相比于-O1,-O2打开了更多的编译优化开关
-O3:在-O2的基础上,level 3级别优化
-Os:优化生成的目标文件的大小
-Ofast:为了提高程序的执行速度,GCC可以无视严格的语言标准。-Ofast会开启所有-O3的编译开关,且会对不符合标准的程序进行优化
-Og:优化调试信息。相对于-O0生成的调试信息,-Og是为了能够生成更好的调试信息。和-O0一样,-Og选项关闭了很多优化开关
-g0:不生成调试信息,相当于没有使用-g
-g1:生成最小的调试信息,足够在不打算调试的程序中进行堆栈查看。最小调试信息包括函数描述,外部变量,行数表,但不包括局部变量信息
-g2:默认-g的调试级别
-g3:相对-g,生成额外的信息,例如所有的宏定义
5.2 修改Makefile的优化级别
将3级优化-O3改为-O0:
OPT = -O0 -g3
再编译烧写测试:
可以了。
如果想只对特定的代码设置优化级别可以参考下面的链接:
https://devzone.nordicsemi.com/f/nordic-q-a/88778/wierd-error-7-happening-in-init
6. 获取蓝牙的MAC地址
下面的内容来自后面列出的参考文档1。
6.1 添加代码
在main.c中添加。
- 头文件
#include "ble_gap.h"
在main函数中添加下面的代码:
// 读取蓝牙MAC地址。头文件#include "ble_gap.h"
ble_gap_addr_t bleAddr; // 定义结构体变量
sd_ble_gap_addr_get(&bleAddr); // 获取MAC地址
uint8_t address[6];
uint8_t i;
for(i = 0; i < 6; i++) {
address[i] = bleAddr.addr[5 - i];
}
NRF_LOG_INFO("MAC Address %02x:%02x:%02x:%02x:%02x:%02x", address[0], address[1], address[2], address[3], address[4], address[5]);
代码的位置:
6.2 编译烧写测试
显示MAC地址:
6.3 用nRF Connect查看MAC地址
NORDIC的工具软件nRF Connect可以用来测试蓝牙的广播和连接。有PC版和移动版。我们在手机上用移动版测试。
- iphone手机
- 安卓手机
对比可以看出苹果手机只显示设备名称Nordic_Blinky,不显示MAC地址。
7. J-Link RTT Viewer多虚拟终端
见《嵌入式芯片调试神器-J-Link RTT详解》
https://blog.csdn.net/suxiang198/article/details/126534730
- 示例代码
int main()
{
// 初始化RTT
SEGGER_RTT_Init();
// 进入主循环
while (1) {
// 设置虚拟端口0,并输出日志,如果不设置,默认都会用Terminal 0
SEGGER_RTT_SetTerminal(0);
SEGGER_RTT_printf(0, "Hello, SEGGER RTT Terminal 0!\r\n");
// 设置虚拟端口1,并输出日志
SEGGER_RTT_SetTerminal(1);
SEGGER_RTT_printf(0, "Hello, SEGGER RTT Terminal 1!\r\n");
// 设置虚拟端口2,并输出日志
SEGGER_RTT_SetTerminal(2);
SEGGER_RTT_printf(0, "Hello, SEGGER RTT Terminal 2!\r\n");
Delay_ms(1000);
// 另外可以输出在RTT Viewer不同颜色的日志,颜色定义可参考SEGGER_RTT.h
// 如下为通过红色字体输出日志
// SEGGER_RTT_printf(0, RTT_CTRL_TEXT_RED"Hello, SEGGER RTT Terminal 0!\r\n");
}
}
8. SEGGER_RTT_printf的高级用法见
见《nRF52832闪存FDS使用(SDK17.1.0)》
nRF52832闪存FDS使用(SDK17.1.0)_晨之清风的博客-CSDN博客
参考文档
- NRF52832学习笔记(11)——蓝牙MAC地址
https://www.jianshu.com/p/f56e0d9e9432 - GCC编译优化和调试选项
https://blog.csdn.net/jinchengzhou/article/details/120703911