1. 为什么选nRF52840做蓝牙LED控制
1.1 这颗芯片到底强在哪
nRF52840是Nordic家的一款多协议SoC,Cortex-M4F内核跑64MHz,带1MB Flash和256KB RAM,原生支持蓝牙5.0、Thread、Zigbee和802.15.4。我第一次拿到它的开发板时,最直观的感受就是“这玩意儿比STM32F103那种纯MCU多了整套射频前端”。做蓝牙LED控制这种入门项目,它其实有点大材小用,但正因为资源富余,你可以在上面随便折腾OTA、Mesh、自定义GATT,不用担心Flash不够。
蓝牙5.0相比4.2最大的提升是广播数据容量从31字节扩展到255字节,理论速率翻倍到2Mbps,广播距离也能拉得更远。做LED控制时,这些特性意味着你可以把LED的状态、颜色、亮度等信息直接塞进广播包,手机不用连接就能读到,省掉一次握手流程。
1.2 为什么用Keil而不是Segger Embedded Studio
Nordic官方现在主推nRF Connect SDK加Segger Embedded Studio,但Keil MDK在国内嵌入式圈子的普及率太高了。你随便找个电子专业的实验室,十台电脑里八台装着Keil。用Keil开发nRF52840的好处是调试器生态成熟、编译速度快、断点调试直观,尤其是查看外设寄存器的时候,Keil的System Viewer比Segger那套顺手不少。
不过要注意,Keil本身不直接支持nRF52840的器件包,你得先装Nordic的Device Family Pack。这个包在Keil官网的Pack Installer里能搜到,或者去Nordic官网下载nRF5 SDK时里面会附带。我建议直接下nRF5 SDK 17.1.0这个版本,它是最后一个完整支持Keil的SDK,之后的nRF Connect SDK就转向CMake和Segger了。
1.3 这个项目适合谁
如果你刚学完STM32的GPIO点灯,想往蓝牙方向迈一步,这个项目就是为你准备的。它不需要你懂射频理论,也不用画PCB,一块现成的开发板加一根USB线就能跑起来。做完之后你会理解BLE协议栈的基本结构、GATT服务的组织方式,以及如何用GPIO控制外设。对于想转做物联网产品的工程师来说,这是性价比最高的入门路径。
2. 开发环境搭建的完整流程
2.1 软件清单与版本选择
先把要用的东西列清楚,避免版本不匹配导致编译报错:
| 软件 | 推荐版本 | 说明 |
|---|---|---|
| Keil MDK | 5.36以上 | 建议用5.38,对Cortex-M4F支持稳定 |
| nRF5 SDK | 17.1.0 | 最后一个完整支持Keil的版本 |
| Nordic Device Family Pack | 8.27.1 | 在Keil Pack Installer里安装 |
| nRF Command Line Tools | 10.24.0 | 包含nrfjprog烧录工具 |
| 串口助手 | 任意 | 用于查看调试输出 |
Keil的安装路径千万别带中文和空格,我踩过这个坑,编译时会出现莫名其妙的“cannot open source input file”错误。安装在C:\Keil_v5这种纯英文路径下最稳妥。
2.2 SDK目录结构与工程模板选择
nRF5 SDK解压后目录很多,你只需要关注这几个:
components:协议栈、驱动、库文件都在这里examples:官方例程,按外设和协议分类config:配置文件modules:Nordic自己写的模块
做LED控制,直接拿examples\ble_peripheral\ble_app_blinky这个例程改。它已经实现了BLE LED Button Service,包含一个LED特征和一个按键特征,手机连上后能读写。路径是examples\ble_peripheral\ble_app_blinky\pca10056\s140\arm5_no_packs,pca10056对应nRF52840 DK,s140是协议栈版本,arm5_no_packs表示用Keil打开。
2.3 Keil工程配置的关键参数
打开工程后,先检查这几个地方:
Target选项里,Device要选NordicSemiconductor::nRF52840_xxAA。如果下拉框里没有,说明Device Family Pack没装好,去Pack Installer里搜Nordic重新装。
C/C++选项卡里的预定义宏很关键,默认应该有BOARD_PCA10056、CONFIG_GPIO_AS_PINRESET、NRF52840_XXAA、S140、SOFTDEVICE_PRESENT。少一个都可能编译不过。
Linker选项卡里,Scatter File要指向..\..\..\..\..\..\components\softdevice\s140\hex\s140_nrf52_7.2.0_softdevice.hex对应的sct文件。这个文件定义了Flash和RAM的分配,协议栈占用了Flash起始的0x26000空间,你的应用代码从0x26000之后开始放。
Debug选项卡里,选J-LINK / J-TRACE Cortex,Port选SW,速度设4000kHz。Flash Download里要勾选“Reset and Run”,这样烧录完自动运行。
2.4 协议栈烧录的先后顺序
很多人第一次烧nRF52840会懵:为什么程序跑不起来?因为协议栈和应用是分开烧的。正确顺序是:
- 先烧协议栈hex文件:
nrfjprog --program s140_nrf52_7.2.0_softdevice.hex --chiperase - 再烧应用hex:在Keil里点Download,或者用
nrfjprog --program your_app.hex
如果只烧应用不烧协议栈,程序会卡在sd_ble_enable返回错误。我建议在Keil的User选项卡里加一条Before Build命令,自动烧协议栈,省得每次手动操作。
3. BLE协议栈与GPIO的协同设计
3.1 蓝牙LED服务的GATT结构
BLE的核心是GATT(通用属性配置文件),它把数据组织成服务(Service)和特征(Characteristic)。LED控制服务通常包含这几个特征:
- LED State Characteristic:UUID 0x2A56,可读可写,1字节,0表示灭,1表示亮
- Button State Characteristic:UUID 0x2A57,可读可通知,1字节,表示按键状态
每个特征下面还有CCCD(客户端特征配置描述符),用来控制是否开启通知。手机端写入LED State的值后,协议栈会触发一个写事件回调,你在回调里操作GPIO。
3.2 GPIO初始化与引脚映射
nRF52840 DK上有4个LED,对应引脚如下:
| LED编号 | 引脚 | 说明 |
|---|---|---|
| LED1 | P0.13 | 低电平点亮 |
| LED2 | P0.14 | 低电平点亮 |
| LED3 | P0.15 | 低电平点亮 |
| LED4 | P0.16 | 低电平点亮 |
初始化代码用Nordic的GPIO驱动:
#include "nrf_gpio.h" #include "boards.h" void led_init(void) { nrf_gpio_cfg_output(LED_1); nrf_gpio_cfg_output(LED_2); nrf_gpio_cfg_output(LED_3); nrf_gpio_cfg_output(LED_4); // 默认全部熄灭(高电平) nrf_gpio_pin_set(LED_1); nrf_gpio_pin_set(LED_2); nrf_gpio_pin_set(LED_3); nrf_gpio_pin_set(LED_4); }nrf_gpio_cfg_output这个函数内部做了几件事:设置引脚方向为输出、配置驱动能力为标准驱动、关闭输入缓冲、关闭上拉下拉。如果你需要更大驱动电流,比如直接驱动LED而不加限流电阻,可以用nrf_gpio_cfg手动配置为高驱动模式。
3.3 写事件回调的处理逻辑
当手机写入LED State特征时,协议栈会调用ble_led_evt_handler,事件类型是BLE_LED_EVT_LED_STATE_CHANGE。在这个回调里读取新值并操作GPIO:
static void led_state_update(uint8_t state) { if (state & 0x01) { nrf_gpio_pin_clear(LED_1); // 低电平点亮 } else { nrf_gpio_pin_set(LED_1); } if (state & 0x02) { nrf_gpio_pin_clear(LED_2); } else { nrf_gpio_pin_set(LED_2); } // LED3、LED4同理 }这里用位掩码的方式,一个字节控制4个LED,手机端发0x0F就全亮,发0x00就全灭。这种设计比每个LED单独一个特征更节省GATT资源。
3.4 广播数据的组织方式
蓝牙5.0的扩展广播允许255字节,但为了兼容老手机,建议还是用传统31字节广播。广播包里放这几样东西:
- Flags:0x06,表示支持LE General Discoverable和BR/EDR Not Supported
- Complete Local Name:设备名,比如“NRF_LED_CTRL”
- Manufacturer Specific Data:可以放LED当前状态,手机不连接就能读到
广播间隔设100ms比较合适,太短费电,太长手机扫描响应慢。用ble_gap_adv_params_t结构体配置:
static ble_gap_adv_params_t m_adv_params = { .properties.type = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED, .p_peer_addr = NULL, .fp = BLE_GAP_ADV_FP_ANY, .interval = 160, // 100ms (160 * 0.625ms) .duration = 0, // 一直广播 .primary_phy = BLE_GAP_PHY_1MBPS, };4. 从编译到烧录的实操记录
4.1 编译常见错误与修复
第一次编译大概率会遇到这几个错误:
错误1:cannot open source input file "nrf_sdh.h"
原因:Include路径没配全。在C/C++选项卡的Include Paths里加上:
..\..\..\..\..\..\components\softdevice\s140\headers ..\..\..\..\..\..\components\ble\ble_services\ble_lbs ..\..\..\..\..\..\components\libraries\bootloader错误2:undefined symbol sd_ble_gap_adv_set_configure
原因:协议栈版本和头文件不匹配。检查SOFTDEVICE_PRESENT和S140宏是否定义,以及s140_nrf52_7.2.0_softdevice.hex是否烧录成功。
错误3:region RAM overflowed by 1024 bytes
原因:RAM分配不够。nRF52840有256KB RAM,协议栈默认占0x26000起始的一部分。在Scatter File里把RAM起始地址改成0x20002A98,大小改成0x3D568。这个值是根据协议栈版本算出来的,s140 7.2.0需要0x2A98的RAM给协议栈。
4.2 烧录与调试连接
烧录用nrfjprog命令行最稳:
# 擦除整片 nrfjprog --eraseall # 烧协议栈 nrfjprog --program s140_nrf52_7.2.0_softdevice.hex --verify # 烧应用 nrfjprog --program ble_app_blinky.hex --verify # 复位运行 nrfjprog --reset如果Keil里点Download报“No Cortex-M Device found”,检查J-Link驱动是否装好,USB线是否插在DK的DEBUG口而不是nRF口。nRF52840 DK有两个USB口,左边是DEBUG,右边是nRF,烧录必须用左边。
4.3 手机端验证流程
烧录成功后,打开手机上的nRF Connect或者LightBlue:
- 扫描设备,找到“NRF_LED_CTRL”
- 点击连接,等待服务发现完成
- 找到LED State特征,UUID 0x2A56
- 点写入,输入0x01,LED1亮;输入0x00,LED1灭
- 输入0x0F,四个LED全亮
如果写入后LED没反应,先看串口输出有没有打印“LED state changed”,没有的话说明写回调没触发,检查CCCD是否配置正确。有打印但LED不亮,用万用表量引脚电压,正常应该是0V(亮)和3.3V(灭)之间切换。
4.4 功耗实测数据
用Nordic Power Profiler Kit II测了一下不同状态下的电流:
| 状态 | 平均电流 | 说明 |
|---|---|---|
| 广播中(100ms间隔) | 1.2mA | 四个LED全灭 |
| 连接中(无数据传输) | 0.8mA | 连接间隔100ms |
| 连接中(LED全亮) | 4.5mA | 四个LED各2mA |
| 系统OFF | 1.5uA | 调用sd_power_system_off |
做电池供电的产品时,广播间隔和LED驱动电流是两个最大的功耗来源。把广播间隔拉到500ms,电流能降到0.4mA左右。
5. 踩坑记录与排查技巧
5.1 协议栈版本不匹配的坑
nRF5 SDK 17.1.0配套的协议栈是s140 7.2.0,如果你从别处拷了一个s140 6.1.1的hex烧进去,sd_ble_enable会返回NRF_ERROR_INVALID_PARAM。排查方法:在ble_stack_init里打印sd_ble_enable的返回值,如果是0x08就是版本不对。解决办法就是SDK和协议栈版本严格对应,别混用。
5.2 GPIO被协议栈占用的坑
nRF52840的P0.09和P0.10默认被NFC天线占用,如果你把LED接在这两个脚上,怎么配置都不亮。需要在system_nrf52840.c里把CONFIG_NFCT_PINS_AS_GPIOS宏打开,或者在sdk_config.h里定义NRF_NFCT_PINS_AS_GPIOS。我建议LED避开这两个脚,省得改配置。
5.3 广播名乱码的坑
设备名在广播包里是UTF-8编码,如果你在代码里写了中文设备名,手机扫描出来会是乱码。用纯英文或者拼音,长度别超过20字节,否则会挤掉其他广播数据。
5.4 连接后立即断开的坑
手机连上后马上断开,串口打印BLE_GAP_EVT_DISCONNECTED,原因码0x13(Remote User Terminated Connection)。这通常是连接参数协商失败,手机请求的连接间隔超出了协议栈允许的范围。在ble_gap_conn_params_t里把min_conn_interval设成12(15ms),max_conn_interval设成24(30ms),slave_latency设0,conn_sup_timeout设400(4秒),兼容性最好。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 编译报找不到头文件 | Include路径缺失 | 补全SDK各组件的Include Path |
| 烧录后无广播 | 协议栈未烧录 | 先烧s140 hex再烧应用 |
| 手机搜不到设备 | 广播未启动 | 检查ble_advertising_start返回值 |
| 写入特征无反应 | CCCD未配置 | 确认特征属性包含BLE_GATT_CHR_F_WRITE |
| LED常亮不灭 | 引脚初始电平错误 | 初始化时调用nrf_gpio_pin_set |
| 连接不稳定 | 连接参数太激进 | 放宽连接间隔和超时时间 |
5.6 调试技巧:用RTT看日志
Keil的Debug Viewer看printf输出需要配置ITM,比较麻烦。我习惯用Segger RTT,在SDK里已经集成了SEGGER_RTT_printf,直接调用就能在J-Link RTT Viewer里看到输出,不占用串口引脚。初始化只要在main开头加一行SEGGER_RTT_Init(),之后用SEGGER_RTT_printf(0, "LED state: %d\n", state)就能打印。
6. 功能扩展与进阶方向
6.1 加个PWM调光
LED控制只做亮灭太单调,用PWM可以做呼吸灯和亮度调节。nRF52840有4个PWM外设,每个支持4通道。用nrf_drv_pwm库配置:
static nrf_drv_pwm_config_t pwm_cfg = { .output_pins = {LED_1, LED_2, LED_3, LED_4}, .irq_priority = APP_IRQ_PRIORITY_LOWEST, .base_clock = NRF_PWM_CLK_1MHz, .count_mode = NRF_PWM_MODE_UP, .top_value = 1000, .load_mode = NRF_PWM_LOAD_INDIVIDUAL, .step_mode = NRF_PWM_STEP_AUTO };top_value设1000,占空比就是比较值除以1000。手机端发两个字节,高字节是LED编号,低字节是亮度值,就能单独调每个LED的亮度。
6.2 加个定时器做自动闪烁
用app_timer创建一个周期定时器,每500ms翻转一次LED状态,手机端可以开关这个自动模式。app_timer的精度取决于RTC,nRF52840的RTC跑32768Hz,定时误差在毫秒级,做LED闪烁完全够用。
6.3 广播包里塞传感器数据
如果你在板子上接了温湿度传感器,可以把数据放进广播的Manufacturer Specific Data里,手机不连接就能读到。蓝牙5.0的扩展广播支持255字节,放几个传感器的数据绰绰有余。格式自己定义,比如前两字节是厂商ID,后面跟温度、湿度、电量。
6.4 低功耗优化清单
做电池产品的话,这几条必须做:
- 广播间隔拉到500ms以上
- 连接间隔协商到100ms以上
- 不用LED时把引脚配成输入断开
- 关闭不需要的外设时钟
- 用
sd_power_system_off进入深度睡眠 - 把调试日志关掉,RTT也费电
我实测过,优化前平均电流2.1mA,优化后降到0.3mA,同样的CR2032电池续航从3天拉到20天。
6.5 从Keil迁移到nRF Connect SDK
如果你后面要做量产项目,建议尽早转到nRF Connect SDK。它基于Zephyr RTOS,支持多线程、设备树、Kconfig,代码组织更现代。迁移的主要工作是重写驱动层,把nrf_drv换成Zephyr的device driver模型,应用逻辑基本不用动。Nordic提供了迁移指南,按步骤走一周能搞定。
我在实际项目里踩过最深的坑就是协议栈版本和SDK版本不匹配,折腾了一整天才定位到。后来养成习惯,每次新建工程先确认SDK版本号,再去对应目录拿协议栈hex,再也没出过这个问题。另外Keil的Pack Installer有时候会抽风,装完Device Family Pack后器件列表里还是不显示nRF52840,重启Keil或者手动指定Pack路径就能解决。