很多刚接触nRF52840的朋友,第一句问我都是"Keil里面怎么新建一个蓝牙工程"。每次听到这个问题我都挺感慨的,因为大家默认把难点放在了Keil工程配置上,但实际上,nRF52840这套开发流程,真正的门槛压根不在Keil,而在协议栈(SoftDevice)跟应用代码之间的关系。你只要把"先烧协议栈、再烧应用、最后用手机APP验证"这条主线捋清楚,整个环境搭建其实就是十几分钟的事。
这篇东西我不会绕弯子,直接从实际开发顺序出发,把Keil环境下从SoftDevice烧录到nRF Connect APP联调的全过程拆开讲。里面会涉及具体软件版本、烧录命令、工程配置项、常见报错的处理方式,以及我实际调试中踩过的一些坑。无论你是刚从STM32转过来的,还是第一次摸Nordic芯片,跟着这篇走一遍,应该能少走不少弯路。
1. 动手之前先搞清楚nRF52840的开发模型
在打开Keil之前,我建议你先花两分钟理解一下nRF52840的开发架构。很多人之所以卡住,不是不会点鼠标,而是没搞明白Nordic的代码运行方式跟常规MCU不一样。
1.1 双镜像架构:SoftDevice与应用代码的关系
nRF52840和STM32这类裸机MCU最大的区别在于,它默认跑的是双镜像架构。芯片Flash里同时存在两个独立的镜像:
- SoftDevice(协议栈镜像):Nordic提供的预编译二进制协议栈,占用Flash的低地址段。它负责BLE协议栈、射频收发、广播、连接管理等底层逻辑,以S140这个型号用得最多。
- Application(应用镜像):你自己写的业务代码,编译后烧录在SoftDevice之后的地址段,调用SoftDevice提供的API接口实现具体功能。
你可以把SoftDevice理解成一部电话的基带处理系统,你写的App代码则负责业务逻辑。两者通过SVC中断和内存共享机制通信,编译时应用代码链接到SoftDevice暴露的符号,运行时通过软中断触发协议栈执行蓝牙相关操作。
这个架构带来的直接结果就是:烧录必须分两步走。你不能像STM32那样,一个HEX文件烧进去就完事。必须先烧SoftDevice,再烧应用代码,或者用mergehex把两个HEX合并成一个再统一烧录。
提示:nRF52840的Flash起始地址是0x00000000,SoftDevice S140占用的地址范围一般是0x00000000~0x00026000(约152KB),应用代码从0x00026000开始。不同SDK版本、不同SoftDevice版本,这个起始地址会稍有差异,具体以SDK里的链接脚本为准。
1.2 选对工具链:Keil MDK + nRF5 SDK + 命令行工具
标题里写了"Keil版",那就围绕Keil来搭。需要用到的软件工具一共四个:
| 工具 | 用途 | 建议版本 |
|---|---|---|
| Keil MDK | 编译、调试应用代码 | 5.37及以上 |
| nRF5 SDK | 官方固件库、示例工程、协议栈文件 | 17.1.0(最稳定) |
| nRF Command Line Tools | 烧录、合并HEX、读写寄存器 | 10.x及以上 |
| nRF Connect for Desktop | 图形化烧录、APP调试 | 最新版即可 |
Keil MDK很好理解,主要用来写代码和编译。nRF5 SDK是Nordic官方提供的开发包,里面包含大量示例工程和驱动库。nRF Command Line Tools里最核心的是两个可执行文件:nrfjprog(命令行烧录工具)和mergehex(HEX合并工具)。
有人会问,Keil直接就能下载调试,为什么还要装命令行工具?因为nRF Connect for Desktop的Programmer功能和nrfjprog的烧录能力,其底层行为跟Keil的Flash Download并不完全一样。实际开发中你总会遇到Keil烧不进去、需要命令行恢复芯片的情况,比如最常见的APPROTECT锁死问题。所以命令行工具是必须装的,省不掉。
nRF Connect for Desktop是Nordic官方出的一套图形化工具集合,其中Programmer用于烧录固件,另外它还集成了BLE调试、RTT Viewer、Power Profiler等一堆实用功能。后面APP调试环节会重点用到它。
1.3 硬件准备:核心板、调试器、从机连接方案
硬件方面,nRF52840的开发板选择很多,常见的有:
- nRF52840 DK:Nordic官方开发板,板载J-Link OB调试器,自带USB口,最适合新手。
- nRF52840 Dongle:USB小棒子,主要用于抓包和做BLE外设,没有板载调试器,需要外接调试器烧录。
- 第三方核心板:比如各种国产nRF52840模组/核心板,性价比高,但需要自备J-Link或DAP-Link调试器。
新手第一块板子我建议直接上nRF52840 DK,不为别的,就因为板载调试器省心。DK板子上的J-Link OB会被识别成一个虚拟串口和一个调试接口,数据线插上就能用,不需要额外接SWD线。
如果是第三方核心板,需要确认调试器的连接方式,一般是SWDIO、SWCLK、GND、VTOC四个引脚,另外建议把SWO引脚也接出来,方便调试时看RTT日志。
2. 从零开始:Keil下的工程准备与SoftDevice烧录
软件装好、硬件连上,接下来就是正经的操作流程。我先说烧录,再说编译,这个顺序别搞反了。因为Keil工程模板在编译时需要链接SoftDevice的符号,如果芯片里没有烧录SoftDevice,就算代码编译通过、烧录进去,运行时也会直接进HardFault。
2.1 下载并解压nRF5 SDK,确认SoftDevice版本
nRF5 SDK 17.1.0的压缩包解压后,你会看到components、examples、modules等目录。跟协议栈直接相关的是components/softdevice目录,里面按芯片分类存放了各个SoftDevice版本的源文件、头文件和预编译库。
nRF52840对应的是S140协议栈,支持的BLE特性包括BLE 5.0的2Mbps速率、Coded PHY远距离模式、广播扩展等。SDK 17.1.0自带的S140版本是7.2.0,这是最常见的组合。
你需要关注的关键路径:
nRF5_SDK_17.1.0_ddde560/ ├── components/ │ ├── softdevice/ │ │ ├── s140/ │ │ │ ├── hex/ │ │ │ │ └── s140_nrf52_7.2.0_softdevice.hex │ │ │ ├── headers/ │ │ │ └── LICENSE │ └── ... ├── examples/ │ ├── ble_peripheral/ │ │ └── ble_app_blinky/ │ │ ├── pca10056/ │ │ │ └── s140/ │ │ │ └── arm5/ │ │ └── ...ble_app_blinky这个例子是Nordic官方为新版SDK准备的入门示例,功能简单:一个按键控制LED,同时通过BLE通知手机状态。它的工程在pca10056/s140/arm5目录下,pca10056就是nRF52840 DK的板卡代号。
提示:SDK解压路径尽量不要包含中文和空格,Keil对路径兼容性比较敏感。我习惯解压到
D:\nRF5_SDK_17.1.0这种纯英文路径,避免后续编译出现奇怪的问题。
2.2 HEX文件合并:让烧录一步到位
前面说过,双镜像架构意味着要烧两个HEX。每次调试都用nrfjprog先烧协议栈、再烧应用,其实也能用,但效率低,而且容易搞混当前芯片里烧的是什么版本。
更推荐的做法是:用mergehex把SoftDevice的HEX文件和应用编译出来的HEX文件合并成一个,然后烧录合并后的HEX。这样每一次操作都相当于把芯片恢复到"当前固件的完整状态",从管理上更可控。
mergehex的使用格式:
mergehex -m s140_nrf52_7.2.0_softdevice.hex app.hex -o full.hex其中-m表示合并模式,后面依次输入需要合并的HEX文件,-o指定输出文件名。合并完成后,full.hex就包含了协议栈和应用的全部内容,直接烧这个文件即可。
Keil编译生成的HEX文件默认在工程目录的_build文件夹下,名称一般跟工程名相同。
2.3 使用nRF Connect for Desktop的Programmer完成烧录
如果你觉得命令行的方式太命令行,也可以用图形化的nRF Connect for Desktop,操作更直观。
打开nRF Connect for Desktop,左侧选择Programmer应用,点击"Select device"选择你的nRF52840开发板。然后点击"Add HEX file"选择合并后的完整HEX文件(或者分别添加SoftDevice和应用HEX)。确认文件列表无误后,点击"Write"按钮开始烧录。
烧录完成后,Programmer会显示当前Flash的占用情况,你可以直观看到协议栈和应用各自占用了哪些地址段。这个可视化功能在排查"应用地址没对齐""协议栈版本不对"这类问题时非常好用。
2.4 命令行烧录方式:nrfjprog的常用操作
虽然图形化工具有良好的可达性,但在批量生产、自动化测试或者远程调试时,命令行工具的效率优势就体现出来了。nrfjprog是Nordic官方提供的命令行烧录工具,最常见的几个操作:
# 查看当前连接的设备 nrfjprog --ids # 擦除整个芯片 nrfjprog --eraseall # 烧录HEX文件(合并后的完整固件) nrfjprog --program full.hex --chiperase # 读Flash内容到文件(备份固件时用) nrfjprog --readcode backup.hex # 复位运行 nrfjprog --reset--chiperase参数表示在烧录前先整片擦除,这样能保证芯片处于干净的初始状态,避免残留数据影响。不使用--chiperase时,nrfjprog会按页擦除,速度更快,但有极低概率遇到Flash残留导致运行异常。
需要特别提醒的是:第一次烧录或者芯片状态异常时,优先使用整片擦除。因为SoftDevice和应用代码的地址区间有严格约束,如果之前烧过别的固件,Flash里可能存在预期之外的数据,整片擦除能把风险降到最低。
3. Keil工程里的配置项逐一拆解:别盲目抄模板
拿到官方示例工程,打开Keil,编译,下载,跑起来——这是最理想的路径。但大多数情况下你会在工程配置环节卡住,因为官方模板里有几个关键配置项是"隐含约定"的,新人不知道这些配置的意义,就容易改错。
3.1 打开示例工程并切换芯片型号
用Keil打开ble_app_blinky\pca10056\s140\arm5\ble_app_blinky_pca10056_s140.uvprojx工程文件后,第一步确认工程配置的芯片型号是否正确。
打开Options for Target(Alt+F7),在Device选项卡里确认芯片是nRF52840_xxAA。如果工程是从别的型号移植过来的,这里要注意切换芯片后,编译器的宏定义和链接脚本也可能需要相应调整。
nRF52840是Cortex-M4F内核,带FPU,所以Keil里还需要确认Target选项卡的FPU选项设置为"Single Precision"。这个配置项直接影响浮点运算相关的编译指令,设置不对会导致编译报错。
3.2 Preprocessor Symbols:那些必须存在的宏定义
在C/C++选项卡的Preprocessor Symbols里,你会看到一堆宏定义。其中最关键的是:
| 宏定义 | 作用 |
|---|---|
| BOARD_PCA10056 | 指定板卡型号,决定引脚映射关系 |
| BSP_DEFINE_ONLY | 只定义BSP相关常量和函数声明,不参与实际硬件初始化 |
| CONFIG_GPIO_AS_PINRESET | 配置GPIO作为复位引脚 |
| FLOAT_ABI_HARD | 启用硬件浮点运算 |
| S140 | 指定SoftDevice型号为S140 |
| NRF52840_XXAA | 指定芯片型号 |
这些宏定义不是凭空写上去的,它们对应SDK源码里的条件编译分支。比如定义了S140,SDK的nrf_sdm.h等头文件才会包含S140相关的API声明和内存布局定义。如果你把S140改成S113,编译时就会引用不存在的符号,直接报错。
注意:
NRF52840_XXAA这个宏与你Keil里选的芯片型号必须一致,两者共同决定了代码里nrf52840.h这个头文件的使用路径。如果你选的芯片是nRF52832,却定义了NRF52840_XXAA,编译时头文件里寄存器地址会变得乱七八糟,很难排查。
3.3 Linker配置与分散加载文件
编译链接环节,Keil用的是分散加载文件(sct文件)来告诉链接器代码段、数据段该放在哪个地址。
以ble_app_blinky为例,链接脚本的Flash起始地址是0x26000,对应S140协议栈占用的结束地址。如果你误把这个地址改成0x00000000,编译不会报错,但烧进去后一运行就进HardFault——因为应用代码覆盖了协议栈,协议栈的数据被破坏了。
Flash起始地址的确认方法是查看SoftDevice的链接脚本模板:
nRF5_SDK_17.1.0/components/softdevice/s140/toolchain/armgcc/...或者直接看ble_app_blinky工程里的s140.icf(IAR格式)或sct文件(Keil格式)中LR_IROM1和ER_IROM1的起始地址。比较稳妥的办法:访问Nordic的memory layout建议文档,或者直接用SDK示例自带的配置,因为官方例子肯定是能跑的。
提示:当你创建自己的自定义工程时,最简单可靠的方案是直接复制官方示例的工程文件,再改名修改。不要从空白工程开始配置链接脚本,这个环节手动配置的出错概率极高,而且报错方式非常不直观,浪费时间。
3.4 编译常见报错:missing header、mismatched type
编译阶段最常见的报错大致有这些:
"No such file or directory: nrf_sdm.h"
这个报错的根源是头文件搜索路径不对。Keil工程的C/C++选项卡里的Include Paths配置项,需要手动添加SDK里各个组件的头文件目录。官方示例自带完整的路径配置,如果是你新建的工程,需要参照官方示例添加以下路径:
components/softdevice/s140/headers components/softdevice/common components/softdevice/s140/headers/nrf52 components/toolchain/cmsis/include modules/nrfx/mdk components/boards components/libraries/util ...(还有很多,直接复制官方示例最稳妥)"identifier nrf_clock_lf_cfg_t is undefined"
这类报错多为SDK版本与SoftDevice版本混用导致。SDK 17.1.0必须搭配S140 7.2.0,如果你从网上找到的示例是旧SDK的代码,里面的API定义跟当前SDK的头文件对不上,就会出现类型未定义、函数参数不匹配这类问题。解决办法是升级示例代码到当前SDK版本,或者检查工程里有没有意外包含了旧版本的头文件。
"undefined symbol: app_error_fault_handler"
这类链接错误说明你的工程缺少了某个源文件。回到官方示例工程,对比Project窗口里的文件列表,把缺少的.c文件添加进来即可。新人很容易在这个环节漏文件,建议直接基于官方模板改。
4. 最小BLE从机实践:广播、连接、收发数据
环境准备好之后,我们来干正事。用ble_app_blinky作为底子,改成自己的最小BLE从机,实现广播、连接、接收手机下发的数据、向手机发送数据。这一节会结合代码讲原理,让你在改代码的时候知道自己在改什么。
4.1 BLE从机的初始化顺序:协议栈→GAP→GATT
BLE从机的启动过程是有先后顺序的,不能乱。这个顺序是Nordic SDK约定好的,乱序调用会直接返回错误码。
标准流程:
- softdevice_handler_init:初始化协议栈,配置时钟。低功耗蓝牙对时钟精度有要求,nRF52840默认使用外部低频晶振(LFXO)作为BLE协议栈的时钟源,这个晶振的精度决定了BLE连接的稳定性和广播的时序精度。
- ble_enable:启用协议栈,传入RAM起始地址和大小。芯片的RAM有一部分要预留给协议栈使用,这部分地址不能给应用代码用。你会在工程里看到
RAM_START和RAM_SIZE两个宏,就是在设置应用代码能用的RAM范围。 - gap_params_init:配置GAP参数,比如设备名称、连接间隔范围、从机延迟等。
- gatt_init:初始化GATT层,注册GATT事件处理函数。
- services_init:注册你的自定义服务和特征。
- advertising_init:配置广播数据包内容。
- advertising_start:启动广播,让手机能扫到你。
关键代码示例:
static void ble_stack_init(void) { ret_code_t err_code; err_code = nrf_sdh_enable_request(); APP_ERROR_CHECK(err_code); // 配置BLE协议栈参数 uint32_t ram_start = 0; err_code = nrf_sdh_ble_enable(&ram_start, APP_RAM_BASE); APP_ERROR_CHECK(err_code); // 注册BLE事件处理函数 err_code = nrf_sdh_ble_observable_append(&ble_obs, BLE_OBSERVER_PRIO_1); APP_ERROR_CHECK(err_code); }这段代码里的APP_RAM_BASE是一个在sdk_config.h里定义的宏,表示应用代码可用的RAM起始地址。这个地址由协议栈决定,编译时如果设置得不对,工程会编译不通过。这也是为什么我反复强调用官方模板工程,因为RAM分配的问题官方已经算好了。
4.2 广播配置:让手机能发现你的设备
广播是BLE外设的"门面"。手机端能否在扫描列表里看到你的设备,取决于广播包里有没有合法有效的广播数据。
static void advertising_init(void) { ret_code_t err_code; ble_advertising_init_t init; memset(&init, 0, sizeof(init)); init.advdata.name_type = BLE_ADVDATA_FULL_NAME; init.advdata.include_appearance = true; init.advdata.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE; init.config.ble_adv_fast_enabled = true; init.config.ble_adv_fast_interval = APP_ADV_INTERVAL; init.config.ble_adv_fast_timeout = APP_ADV_TIMEOUT_IN_SECONDS; err_code = ble_advertising_init(&init); APP_ERROR_CHECK(err_code); ble_advertising_conn_cfg_tag_set(BLE_CONN_CFG_TAG_DEFAULT, 0); }广播间隔(APP_ADV_INTERVAL)是广播的核心参数,默认值是MSEC_TO_UNITS(40, UNIT_0_625_MS),也就是40ms。这个值调小,手机扫描时发现设备的速度更快,但功耗更高;调大则相反。
如果你改了广播间隔,发现手机扫描不到设备,先别急着怀疑代码逻辑,看看是不是间隔设置得太大了。我调试时把间隔调到500ms以上,手机端有时就会明显感觉"扫不到"或者"时有时无",但如果只是当成低功耗设置,很容易被忽略。
4.3 实现自定义服务:添加一个可读可写的特征
BLE的数据交互是通过GATT服务(Service)和特征(Characteristic)完成的。一个服务是一个逻辑容器,特征是这个容器里实际承担数据传输任务的单元。
以添加一个"LED控制特征"为例:
static uint8_t led_state = 0; static void led_char_add(void) { ret_code_t err_code; ble_uuid_t ble_uuid; ble_gatts_char_md_t char_md; ble_gatts_attr_t attr_char_value; ble_uuid_t attr_uuid; ble_gatts_attr_md_t attr_md; uint8_t uuid_type; // 注册自定义服务UUID ble_uuid128_t base_uuid = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; base_uuid.uuid128[15] = 0x59; err_code = sd_ble_uuid_vs_add(&base_uuid, &uuid_type); APP_ERROR_CHECK(err_code); ble_uuid.type = uuid_type; ble_uuid.uuid = 0xFF01; memset(&char_md, 0, sizeof(char_md)); memset(&attr_char_value, 0, sizeof(attr_char_value)); memset(&attr_md, 0, sizeof(attr_md)); // 可读可写 attr_md.read_perm = (ble_gap_conn_sec_mode_t){.sm = 1, .lv = 1}; attr_md.write_perm = (ble_gap_conn_sec_mode_t){.sm = 1, .lv = 1}; attr_md.vloc = BLE_GATTS_VLOC_STACK; char_md.char_props.read = 1; char_md.char_props.write = 1; attr_char_value.p_uuid = &ble_uuid; attr_char_value.p_attr_md = &attr_md; attr_char_value.init_len = sizeof(uint8_t); attr_char_value.max_len = sizeof(uint8_t); attr_char_value.p_value = &led_state; err_code = sd_ble_gatts_characteristic_add(BLE_GATT_HANDLE_INVALID, &char_md, &attr_char_value, &led_char_handles); APP_ERROR_CHECK(err_code); }这段代码里,sm=1, lv=1表示"无需加密,任何已连接设备都可以对这个特征进行读写"。如果你调试时手机端写入数据总是提示权限不足,多半是这里的权限设置成了需要配对或加密的等级。
4.4 BLE事件处理:在回调函数里响应手机端请求
BLE的事件处理是通过注册回调函数实现的。每当手机端读写特征、建立连接、断开连接时,协议栈会产生一个包含事件类型的结构体,回调函数里根据事件类型做相应处理。
static void ble_evt_handler(const ble_evt_t *p_ble_evt, void *p_context) { switch (p_ble_evt->header.evt_id) { case BLE_GAP_EVT_CONNECTED: // 连接成功事件 m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle; break; case BLE_GAP_EVT_DISCONNECTED: // 断开连接,重新开始广播 m_conn_handle = BLE_CONN_HANDLE_INVALID; break; case BLE_GATTS_EVT_WRITE: // 手机端写入数据,根据特征句柄判断是哪个特征 if (p_ble_evt->evt.gatts_evt.params.write.handle == led_char_handles.value_handle) { led_state = p_ble_evt->evt.gatts_evt.params.write.data[0]; // 执行对应的控制动作 } break; default: break; } }这里涉及一个新手容易困惑的概念:特征句柄(Handle)。BLE协议里,每个服务、特征、描述符在GATT表中都有一个唯一的句柄编号,应用层通过句柄来区分数据来自哪个特征。led_char_handles.value_handle就是刚才添加的那个特征的句柄,你需要在全局范围内保存它,以便在事件回调里比对。
4.5 用RTT Viewer或串口打印调试信息
调试BLE相关代码,日志输出几乎必不可少。nRF52840上最简单的日志方式是Segger RTT,它通过调试器的SWD接口直接读取目标芯片内存,不需要占用串口引脚。
在Keil工程里,nrf_log默认配置了RTT作为后端。你只需要在代码里:
NRF_LOG_INFO("BLE connected, conn_handle: %d", p_ble_evt->evt.gap_evt.conn_handle); NRF_LOG_FLUSH();然后在nRF Connect for Desktop里打开RTT Viewer应用,连接设备后就能在终端窗口看到日志输出。串口方式也可以配置,但RTT不用额外占引脚、不需要设定波特率,对于引脚紧张的项目来说非常方便。
注意:
NRF_LOG_FLUSH()这个函数要酌情调用。RTT缓冲区的写入会暂用CPU时间,如果你在中断服务函数里频繁调用,会影响实时性。我的习惯是:低频事件(连接、断开、按键)直接FLUSH,高频数据(传感器上报)只LOG不FLUSH,攒到一定量再统一输出。
5. 手机APP调试:nRF Connect的完整用法
一个BLE工程"看起来能跑"和"真正能调试"之间,隔着一个好用的BLE调试工具。这里我强烈建议你用Nordic官方出的nRF Connect,它分为移动版(Android/iOS)和桌面版(Windows/macOS),功能覆盖了大部分BLE开发调试场景。
5.1 扫描与连接:检查广播参数是否正常
打开手机上的nRF Connect App,首页会自动开启BLE扫描。在扫描列表里,你会看到所有正在广播的BLE设备,包括你开发板上的设备。
这个时候可以检查几个广播参数:
- 设备名称:是否显示为你代码里设置的
DEVICE_NAME,如果显示不了名称,很可能是广播包的数据格式配置有误。 - 信号强度(RSSI):设备靠近手机时RSSI应该明显变大,如果不变或异常,检查天线区域是否有遮挡。
- 广播类型:nRF Connect会显示是"Connectable"还是"Non-connectable",如果手机只能扫描不能连接,检查广播标志位配置。
扫描不到设备时的排查顺序:先看代码里advertising_start有没有成功调用(检查RTT日志),再看广播包数据格式,最后用桌面的Sniffer抓包确认芯片到底有没有在发广播包。
5.2 连接后查看服务:验证GATT表结构
点击设备名称连接,nRF Connect会列出这个设备包含的所有Service和Characteristic。这是一个非常重要的验证环节。
你需要检查:
- Generic Access服务(0x1800)和Generic Attribute服务(0x1801)是否存在。这是BLE协议规定的必选服务,如果这两个都没有,说明协议栈初始化有问题。
- 自定义服务的UUID是否与你代码里注册的UUID一致。
- 特征属性(Properties)是否符合预期:可读、可写、可通知,是否都正确显示。
有时你发现服务列表里多了一个你代码里没写的服务,或者某个特征突然消失,通常是上次烧录的应用代码和当前代码不一致导致的,重新上电或重新烧录即可解决。
5.3 数据交互实操:向设备写入指令并读取返回值
确认服务列表正常后,就可以开始实际的数据交互测试了。
步骤一:写入数据。找到LED控制特征,点击"Up Arrow"图标,在弹出的对话框里输入要写入的字节。如果你代码里定义的是1字节控制值,填01或00。
步骤二:读取数据。点击"Down Arrow"图标,App会发送Read请求,设备端返回当前特征值。如果代码里没有实现Read回调,这里会报错或者返回空。
步骤三:订阅通知(Notification)。如果代码里配置了通知属性,点击特征列表里的"Start Notification"或"Subscribe"按钮,然后触发设备端主动上报数据,观察App是否实时收到。
这个步骤看似简单,却是排查"手机收不到数据"这类问题的关键节点。如果订阅通知按钮是灰色的无法点击,说明特征的notify属性没有被正确配置;如果订阅成功但收不到数据,排查sd_ble_gatts_hvx调用的返回值以及连接句柄是否正确。
5.4 BLE抓包:用Wireshark定位协议层问题
当你需要深入分析蓝牙链路层问题时,手机APP能提供的信息就不够用了。这时候需要用到BLE抓包工具。
nRF52840的抓包方案比较简单:用nRF52840 Dongle作为抓包器,烧录Nordic官方的BLE Sniffer固件,再接上Wireshark使用。
具体步骤:
- 下载nRF Sniffer for BLE固件(在Nordic官网可以找到)。
- 用nRF Connect for Desktop的Programmer把Sniffer固件烧录到nRF52840 Dongle(作为抓包器)里。
- 电脑上安装Wireshark以及对应的nRF Sniffer插件。
- Wireshark中选择对应接口,设置要抓取的BLE信道和MAC地址,点击开始抓包。
抓包能让你看到BLE空中链路上真实的广播包和连接包,在排查"广播包某些字段没生效""连接参数协商失败""加密配对异常"这类问题时非常有用。比如广播包字段丢失,手机端的扫描列表里能看到设备但显示不了名字,用Wireshark看广播包里的AD Type就能定位是字段格式错误还是广播数据超长被截断。
5.5 常见问题:"没有找到设备"与"烧录失败"
"手机扫描不到设备"——先别急着怀疑代码,按以下顺序排查:
- 检查开发板的供电电流是否足够,尤其是用USB口直接供电时,有些电脑USB口电流不足会导致射频功率异常,广播距离变短到几厘米,这样手机当然扫不到。
- 确认设备有没有进休眠模式。nRF52840的System ON空闲模式功耗极低,如果代码里启用了休眠且没有配置唤醒源,芯片会进入深度睡眠,射频自然就停了。
- 用nRF Connect for Desktop的RTT Viewer看日志,确认广播启动函数执行成功。
"Keil下载时报错:Cannot access target"——这个报错说明Keil跟目标芯片之间的调试链路没通,常见原因:
- 调试器驱动没装好,设备管理器里看到的是未知设备。重新安装J-Link驱动即可解决。
- 芯片已经进入System OFF模式,调试接口被禁用了。这时需要用nrfjprog执行一次
--recover恢复。 - 连接线接触不良,尤其是用杜邦线连接第三方核心板时,SWDIO和SWCLK引脚接触不稳定,也会出现这种间歇性连不上的问题。
6. 那些不写在文档里的坑:芯片锁定、协议栈版本不匹配与调试经验
最后这部分,是整篇文章最值钱的干货。以下问题是我在实际项目中踩过的、或者在社区里反复看到新人被困住的场景,写出来帮你省几天时间。
6.1 nRF52840的"永久锁定"到底是什么情况
网上关于"nrf52840永久锁定"的说法满天飞,但实际绝大多数情况都不是真正永久损坏,而是APPROTECT(访问保护)机制把调试接口禁用了。
nRF52系列芯片有一个叫APPROTECT的寄存器,它的作用跟STM32的读保护类似:防止别人通过调试接口读取芯片的Flash内容。当你启用了APPROTECT后,Keil再想通过SWD接口连接芯片调试,会直接失败,表现就是"芯片连不上了"。
很多新手在代码里通过nrf_power_gpregret_set或者直接写了相关配置,或者不小心烧录了带有APPROTECT保护的固件,然后发现芯片"变砖"了。
解决方案其实很简单:
nrfjprog --recover--recover命令会通过调试接口把APPROTECT位清除,恢复芯片的可调试状态。执行后芯片会被整片擦除,Flash里的固件就没了,但芯片本身是活过来了。
注意:
--recover不是万能的。如果芯片进入的是System OFF模式下且调试接口被禁用,--recover仍然可以恢复;但如果调试接口的物理连接本身有问题(比如调试引脚被代码复用了、接线错误),那任何软件手段都救不了。所以当你的项目用到复用调试引脚的配置时,务必做好恢复预案。
6.2 协议栈版本与应用代码不匹配的经典报错
这也是入坑高频问题。nRF5 SDK 17.1.0搭配S140 7.2.0是官方验证过的组合,但你在实际开发中可能因为各种原因混用了版本:
报错示例1:
nrf_sdh.c(0): error: #20: identifier "NRF_SDH_BLE_OBSERVER_PRIO_LEVELS" is undefined报错示例2:
Undefined symbol sd_ble_gatts_characteristic_add (referred from main.o)这两个报错反映的都是同一个问题:SDK的代码版本与SoftDevice的头文件/SDK配置不匹配。
处理方法是先确认一套"黄金组合":
| SDK版本 | SoftDevice版本 | 兼容状态 |
|---|---|---|
| 17.1.0 | S140 7.2.0 | 官方推荐,稳定 |
| 16.0.0 | S140 6.1.1 | 较老,但可以跑 |
| 15.3.0 | S140 6.1.0 | 老项目常见 |
如果你想要使用较新协议栈版本,不要试图在旧SDK里改了头文件路径就完事,你还要同步更新sdk_config.h、链接脚本、RAM地址等一大堆联动配置,非常容易出问题。我的建议是:先跑通官方黄金组合,再谈升级。
6.3 连接一段时间后自动断开的问题
BLE连接在运行一段时间后自动断开,是最难排查的一类问题。我在实际项目中遇到过三种常见诱因:
诱因一:看门狗没喂。nRF52840的看门狗默认是关闭的,但有些教程会让你开启看门狗防死机。如果主循环里的喂狗操作被某个阻塞操作卡住,芯片会复位,然后BLE连接自然就断了。排查方式是看RTT日志里有没有复位标志记录:NRF_POWER->RESETREAS寄存器会记录上次复位的原因。
诱因二:连接参数协商失败。手机与从机建立连接后,双方会协商连接间隔、从机延迟、超时时间。如果你的代码里设置的连接间隔过短(比如小于7.5ms),有些手机可能无法支持,就会拒绝协商,直接断开。排查方法是抓包或者看协议栈返回的ble_gap_evt_conn_param_update_request事件处理情况。
诱因三:TX Power过高导致射频自激。这个概率较低,但确实存在。当发射功率设置到最大值+8dBm时,在近距离(比如开发板紧贴着手机天线)可能出现射频饱和导致丢包严重,进而触发连接超时。解决方法是把TX Power调低一档,或者物理上拉开距离测试。
6.4 协处理器模式:为什么有时需要先关闭SoftDevice才能调试
最后一个经验之谈。在Keil的Debugger设置里,你可能会遇到这种情况:程序在SoftDevice的某段代码里跑飞了,但你单步调试时发现无法在应用代码里打断点。
这是双镜像架构下的正常现象。SoftDevice运行在特权模式,应用代码运行在非特权模式。当调试器尝试在特权模式下打断点时,需要额外的配置支持。Keil里有几个设置项可以缓解这个情况:
- Debugger选项卡里勾选"Run to main()",让程序启动后自动跑到main函数,避免停在启动代码或协议栈初始化代码里。
- 如果要在SoftDevice的回调里打断点,建议在应用代码的回调函数入口处打断点,而不是在协议栈代码内部打断。
- 开启"Watch Windows & Performance Analyzer"中的周期刷新功能,减少调试器对实时性的干扰。
另外,当你在调试BLE应用时,手机端尽量保持已经连接但不要频繁操作。因为每次交互都会触发协议栈的中断处理,而调试器的断点会暂停整个芯片的处理流程,两者叠加会导致协议栈超时出错。
7. 后续还能怎么玩:从Hello World到实际产品
跑通上面的流程,你的nRF52840开发环境就算真正建立起来了。很多人在这一步就停下来,转而开始直接写业务逻辑,但我觉得还有几件事情值得你先做一下,因为它们会在后续开发中反复曝光你的认知盲区。
第一件事是把SDK里几个经典的示例工程都编译一遍。不只是ble_app_blinky,还有ble_app_uart(串口透传,最常用的业务开发模板)、ble_app_hrs(心率服务示例,适合理解多服务注册)、throughput(测吞吐率,适合验证协议栈配置)。这些工程全都解压一遍、编译一遍、烧录一遍,你对SDK的目录结构、文件依赖关系、编译流程会建立起非常直观的感知,之后自定义工程时就知道该包含哪些文件、不该包含哪些文件。
第二件事是学会使用pca10056的引脚映射表。nRF52840的引脚大多可以任意映射到外设,但DK板上有一些引脚是固定的(比如LED、按键、NFC、调试口),如果代码里随意配置引脚,很容易跟板载外设冲突。建议收藏Nordic官方的nRF52840 DK引脚分配文档,每次画板子、接外设之前先查表确认。
第三件事是理解nRF Connect for Desktop里几个重要工具的分工。Programmer用来烧录,RTT Viewer用来打印日志,Bluetooth Low Energy用来做基础调试,Power Profiler搭一个电流探头可以做功耗分析。如果产品需要做到低功耗,Power Profiler这个工具你迟早要用。
如果你打算往BLE开发这条路深入走,接下来值得研究的方向清单大致是这样的:
- 连接参数的动态协商,搞清楚不同Android/iOS机型对连接参数的要求差异。
- BLE配对与加密(LE Secure Connections),涉及MITM保护、Just Works、Passkey等配对方式。
- 基于GATT的透传协议设计,比如如何把MTU从默认23字节提到247字节,如何分帧发送大数据包。
- Nordic的DFU(Device Firmware Upgrade)机制,包括Bootloader、DFU服务,以及它跟SoftDevice之间的配合关系。
那些刚接触这块还想再深入的朋友,如果你在调通第一个Demo之后觉得"好像也就这么回事",我建议你去看看ble_app_uart的源码,尝试把它改成自己的透传工具,再把它跟手机端nRF Toolbox App联调一下。到你真正把"手机发指令→设备执行动作→设备上报状态→手机更新界面"这套闭环捋通顺的时候,你对BLE应用开发的整个脉络就算真正建立了。