简介:资源针对NRF51822低功耗蓝牙芯片的主模式开发,面向需要实现BLE设备扫描、连接以及主从数据透传的嵌入式开发者。压缩包内含3个文件,包含1个主程序C文件以及一组用于串口与BLE交互的C源文件和头文件,整体大小约11KB。主程序演示主模式扫描与连接流程,辅助源文件则将BLE操作封装为类似UART的接口,便于开发者通过串口调用方式完成数据收发。目前已有419人学习下载。资源重点覆盖BLE堆栈初始化、扫描参数配置、广告数据解析、连接请求发起及GATT自定义服务读写等关键环节,同时展示如何结合串口辅助层实现透明传输,适合希望快速上手NRF51822主从通信或正在调试相关项目的开发者对照使用。 先说说我为什么要写这篇。上周帮一个做智能硬件的老哥调板子,主控是 nRF51822,要从一个 BLE 温湿度传感器上拿数据。这芯片做从设备确实是轻车熟路,可一旦要让它切到主模式,去扫描、连接别人广播的设备,就会冒出一堆课本上看不到的问题:扫描不到、连上就断、偶尔还直接死机。折腾了三天,把 SoftDevice S130 协议栈、GAP 参数、连接参数从头到尾捋了一遍才算跑通。今天就把 nRF51822 主模式扫描连接的完整思路、代码和踩坑记录整理出来,给后面做 BLE 网关、主从转发、数据采集这类项目的朋友一个参考。
1. 主模式扫描连接,要搭的是哪套体系
1.1 典型项目场景
先说清楚什么叫“主模式扫描连接”。在 BLE 的 GAP 层,设备分两种角色:Central(主设备、中心设备)和 Peripheral(从设备、外围设备)。nRF51822 默认最常用的身份是从设备,比如做成一个可以被手机连接的智能手环、蓝牙心率计。但很多产品需要反过来:让 nRF51822 主动去发现周围有没有在广播的从设备,然后挑一个发起连接,连接成功后再读数据或者收通知。比如一个室内数据采集网关,周围放着好几个温湿度节点,节点做从设备,nRF51822 做中心节点,定时扫描、逐个连接、把数据汇总后通过串口或者 Wi-Fi 上传。我这次调试的就是这类需求,核心动作就两个词:扫描、连接。
1.2 角色选择:S110 只能做从机,想扫描必须换 S130
很多朋友第一次做 nRF51822 主模式时容易卡在一个坑里:用着之前从机的例程,直接调用扫描接口,结果协议栈返回一个不支持的错误。原因很简单:你烧进去的 SoftDevice 是 S110(只支持 Peripheral 角色),不是 S130。S110 对应的 API 里根本没有扫描、连接这套 Central 相关接口。nRF51822 这代芯片的主从支持是靠 SoftDevice 固件决定的,不是你在代码里改一个角色定义就能切过去。
所以第一步就要在选型阶段确认:如果项目里有主模式扫描连接需求,一定要选 S130 SoftDevice。S130 支持 Peripheral + Central 主从一体,最多能同时维护一个或多个连接,实际连接数和 RAM/Flash 剩余空间有关,工程上一般保底 4 个连接比较稳。而 S110 只能做从机,撑死支持一堆外设连接,但永远不能主动发起连接。如果你手头板子已经烧了 S110,需要重新刷 S130,之后整个 SDK 的配置和例程也要切换过去。
1.3 扫描连接的整体流程
整个流程用一句话概括就是:广播唤醒扫描,扫描发现设备,连接请求建立链路。细化到代码层面,可以拆成下面五步:
- 初始化协议栈、配置 GAP 参数(设备名、连接参数)。
- 配置扫描参数,调用协议栈接口启动扫描。
- 在广播事件回调里拿到从设备的 MAC 地址、设备名、RSSI 等数据,按需过滤。
- 命中目标后,先停止扫描,再调用连接接口向目标地址发起连接请求。
- 连接建立后,事件回调会收到 BLE_GAP_EVT_CONNECTED,此时才能做 GATT 发现服务、收数据、发数据等操作。
这个流程看起来很直白,实际调试中每一步都可能藏着问题。比如第二步的扫描窗口配得不对,可能扫描非常耗电;第四步没有先停扫描,连接接口可能直接返回 NRF_ERROR_BUSY;第五步连接参数如果超出从设备支持范围,链路可能建了又断。下面我按原理、代码、排查三个维度逐个拆。
2. 扫描和连接里的关键参数,得按单位去算
2.1 广播和扫描模型的通俗理解
把 BLE 的空中行为比作两个人在走廊里找人。从设备是“喊话的人”,每隔一段时间喊一句“我在这里,我叫某某”;主设备是“竖起耳朵听的人”,它不会一直全神贯注地听,而是听一会儿、歇一会儿,这样省电。这个“听一会儿”就是扫描窗口,两次开始听之间的间隔就是扫描间隔。从设备喊话的时间点是随机的,主设备听的时间和喊话时间不一定对得上,所以扫描要持续足够久,并且尽量增加倾听的占空比。
在 S130 的扫描参数里,有三个最基本的量:扫描间隔、扫描窗口、扫描超时。它们直接影响扫描功耗和发现速度。类似的,连接建立后的数据收发,也有一组参数控制双方多久“对一次表”,也就是连接参数。
2.2 扫描间隔、扫描窗口和占空比
扫描间隔(scan_interval)单位是 0.625ms,扫描窗口(scan_window)单位也是 0.625ms。有效监听时间比例就是窗口/间隔,这个比值也叫扫描占空比。比如间隔 100ms、窗口 50ms,也就是 160/80(因为 100ms / 0.625 = 160,50ms / 0.625 = 80),占空比就是 50%,扫描功耗相对较高,但响应快。反之如果间隔 200ms、窗口 20ms,占空比只有 10%,功耗低但容易漏掉广播包。
我一般做低功耗产品时,会先把扫描占空比控制在 20%~50% 之间调试功能,确认能稳定扫描到设备后,再逐步调小窗口、拉大间隔,测一遍丢包率。这里有个硬性约束:扫描窗口必须小于等于扫描间隔,不能窗口比间隔还大。早期我随手把两个值填反过,协议栈直接返回 NRF_ERROR_INVALID_PARAM。
另外还有主动扫描和被动扫描的区别。被动扫描只接收广播包;主动扫描除了接收广播包,还会主动发送扫描请求,从设备收到后会回一个扫描响应包,里面通常带更多信息,比如完整设备名、厂商自定义数据。如果你需要靠完整设备名过滤设备,建议把 active 扫描打开,否则设备名可能被截断。
2.3 连接参数的四要素
连接建立后,主从设备之间维护着一个周期性的数据交互节奏,这个节奏由连接参数决定。四个最关键的参数分别是:
- 最小连接间隔、最大连接间隔:单位 1.25ms。连接间隔就是两个连接事件之间的间隔,连接事件是主从设备在固定频率上的一次数据收发机会。间隔越短,数据实时性越好,功耗越高。
- 从设备延迟(Slave Latency):允许从设备跳过的连接事件个数。比如从设备延迟为 3,表示从设备最多可以连续跳过 3 个连接事件不回复,适合低功耗从机。
- 监督超时(Supervision Timeout):单位 10ms。如果在这个时间内主从双方都没有成功通信,协议栈就认为链接丢失,触发断开事件。
我常用的连接参数组合是:最小间隔 15ms、最大间隔 30ms、从设备延迟 0、监督超时 4000ms。注意这个 4000 是超时时间 4000×10ms=40s,实际我是想配置 4 秒,应该填 400,否则 40 秒的掉线检测时间会让“丢链”变得非常迟钝。这个细节极其容易忽视,后面排查掉线问题时会被它坑得很惨。还是以“能完成功能”为目标,监督超时填 400(4 秒)比较合理。
3. 跑通主模式扫描连接的完整实操
3.1 开发环境与最小工程准备
我先交代一下我的环境,方便你对号入座:芯片 nRF51822 QFAA(16KB RAM、256KB Flash),SoftDevice 用的 S130 v2.0.1,SDK 版本 nRF5_SDK_12.3.0,IDE 用 Keil MDK,Debugger 用 J-Link。这个组合虽然老,但网上资料和例程最多,很多量产项目都是这么跑起来的。
搭建工程时,最简单的方法是直接找一个 Central 例程改,比如 SDK 里的 ble_app_hrs 这种从机例程不行,要看带 central 命名的例程,或者用 nRF5_SDK\examples\ble_central 下的工程。如果是从零新建工程,建议先编译官方 central 例程,确保协议栈、链接脚本、宏定义都正确,再改自己的业务代码。硬件上注意 nRF51822 主模式对射频前端要求不高,但天线匹配和晶振精度会影响扫描灵敏度,所以先确认 32.768K 低频晶振和 16MHz 高频晶振起振正常,再用官方例程扫一个已知设备验证硬件链路。
3.2 协议栈初始化和扫描启动代码
协议栈初始化这一步几乎是固定的模式。先配置时钟,再调用软设备处理相关的初始化,最后使能协议栈。这个阶段要把 GAP 连接参数也配置好,后续从设备请求更新连接参数时,主设备才知道自己支持什么范围。
static void ble_stack_init(void) { // 高频时钟源配置,通常使用内部 HFCLK,或者外部晶振 nrf_clock_lf_cfg_t clock_lf_cfg = { .source = NRF_CLOCK_LF_SRC_XTAL, .rc_ctiv = 0, .rc_temp_ctiv = 0, .xtal_accuracy = NRF_CLOCK_LF_XTAL_ACCURACY_20_PPM }; SOFTDEVICE_HANDLER_INIT(&clock_lf_cfg, NULL); ble_enable_params_t enable_params; memset(&enable_params, 0, sizeof(enable_params)); enable_params.gatts_enable_params.attr_tab_size = BLE_GATTS_ATTR_TAB_SIZE_DEFAULT; // S130 支持中央角色,这里才是主模式的开关 enable_params.common_enable_params.vs_uuid_count = 1; sd_ble_enable(&enable_params); }GAP 参数配置里,我会把扫描启动参数和连接参数分别准备好。扫描参数的代码大致是这样:
static ble_gap_scan_params_t m_scan_params; void scan_start(void) { memset(&m_scan_params, 0, sizeof(m_scan_params)); m_scan_params.active = 1; // 主动扫描,获取扫描响应 m_scan_params.interval = 160; // 100ms,单位0.625ms m_scan_params.window = 80; // 50ms,单位0.625ms m_scan_params.timeout = 0; // 0表示持续扫描,不超时 ret_code_t err_code = sd_ble_gap_scan_start(&m_scan_params); APP_ERROR_CHECK(err_code); }注意不同 SDK 版本里 ble_gap_scan_params_t 结构体字段有细微差别,有的版本需要额外提供扫描 buffer,有的版本把 buffer 放到接口参数里。我这段代码是以 12.3.0 版本为例,如果你用的 SDK 是 15.x 之类的,请参考对应例程里的字段名。填参数之前先打印 sizeof 和字段偏移,能省去很多版本兼容问题。
3.3 广播事件处理与目标筛选
扫描启动后,协议栈会在中断上下文回调里不断上报 BLE_GAP_EVT_ADV_REPORT 事件,你的任务是在回调里判断“是不是我要找的设备”。一个常见的筛选方式是匹配 MAC 地址,或者匹配广播包里的设备名。我板子的目标设备是一个温湿度传感器,广播名固定为“TH_SENSOR”,所以我在回调里解析设备名后做字符串比较。
static void on_ble_gap_evt(ble_gap_evt_t * p_gap_evt) { switch (p_gap_evt->evt_id) { case BLE_GAP_EVT_ADV_REPORT: { const ble_gap_evt_adv_report_t * p_adv_report = &p_gap_evt->params.adv_report; // 简单筛选:设备名匹配 if (strncmp((char *)p_adv_report->data, "TH_SENSOR", 9) == 0) { memcpy(&m_target_addr, &p_adv_report->peer_addr, sizeof(ble_gap_addr_t)); m_target_rssi = p_adv_report->rssi; // 通知主循环,找到目标设备 m_found_target = true; } break; } default: break; } }这里有个经验点:广播数据里的设备名可能不全,如果开启主动扫描,从设备会把完整名称放进扫描响应包,而你收到的 data 是广播包 + 扫描响应包的组合,所以解析时不能只看前几个字节,最好先解析 AD structure。复杂的解析逻辑可以用 SDK 做过的 adv_data_parser,但简单场景直接逐字节匹配一个固定字符串也能用。实测下来这种解析方式最稳,不会因为广播包结构变化而崩。
3.4 发起连接和连接后的收尾
找到目标设备后,不要直接在主循环里疯狂调用连接接口。最好先调用 sd_ble_gap_scan_stop() 停止扫描,然后把目标地址填进连接参数,调用 sd_ble_gap_connect()。连接参数和前面说的连接四要素一致:
static void connect_to_target(void) { ret_code_t err_code; err_code = sd_ble_gap_scan_stop(); // 扫描停止后稍等一个协议栈周期,避免 BUSY nrf_delay_ms(2); ble_gap_conn_params_t conn_params; memset(&conn_params, 0, sizeof(conn_params)); conn_params.min_conn_interval = 12; // 15ms conn_params.max_conn_interval = 24; // 30ms conn_params.slave_latency = 0; conn_params.conn_sup_timeout = 400; // 4s err_code = sd_ble_gap_connect(&m_target_addr, NULL, &conn_params, BLE_CONN_CFG_TAG_DEFAULT); APP_ERROR_CHECK(err_code); }连接请求发出后,协议栈通过事件返回结果。BLE_GAP_EVT_CONNECTED 表示连接成功,BLE_GAP_EVT_DISCONNECTED 表示连接断开。连接成功之后,一般还要做 GATT 服务发现,才能和从设备交换数据。但就“扫描连接”这个主题而言,到这一步核心链路已经通了。我自己的项目里,连接成功后还会把 RSSI、连接句柄、连接间隔打印到串口,方便确认链路质量。
4. 实测问题排查:从扫描不到到连接频繁断开
4.1 扫描不到设备,先按这四个方向查
扫描不到东西是最常见的现象,也是最容易让人怀疑人生的。我的排查顺序是这样:
第一,确认从设备真的在广播。很多时候你不是扫描逻辑错了,而是从设备根本没开广播,或者广播已经进入不可连接状态。用一个手机 App 或 nRF Connect 工具扫一下,如果手机也看不到,问题基本在从设备;如果手机能看到而 nRF51822 看不到,才是主设备这边的问题。
第二,检查 SoftDevice 角色。如果工程用的 S110,扫描接口会直接报错,但这个错误往往被 APP_ERROR_CHECK 吃了,表现为程序卡在错误处理里,看起来像是“没扫描到”。所以在启动扫描前打一行串口日志,确认走过了扫描启动函数。
第三,排查扫描参数。窗口太小、间隔太大,或者广播信道被干扰,都可能导致漏包。我习惯把扫描间隔和窗口临时调到占空比 50%,比如间隔 80、窗口 40,同时打开主动扫描,先把功能跑通再调功耗。
第四,确认射频硬件。nRF51822 的射频前端如果天线匹配不对,或者晶振偏差过大,信号会很差。检查方法是用频谱仪看发射功率,或者对比其他正常板子。
4.2 连接发起失败和超时,多半是这几种原因
扫描到设备了,但连接一直失败,或者发起连接后长时间没有响应。我踩过的坑集中在三块。
一是目标设备广播类型不对。BLE 广播有可连接广播和不可连接广播,比如 iBeacon 信标广播就不可连接。如果 target 广播类型是 ADV_NONCONN_IND,主设备再怎么发连接请求也是白搭。用抓包工具能看到广播包类型,代码里也能从广播头解析出来。
二是连接参数不被从设备接受。有些从设备的连接参数范围很窄,比如只接受 50ms~100ms 的连接间隔,而你发的 min/max 是 15ms/30ms,从设备会拒绝连接请求或连接后直接断开。解决办法是先读从设备的连接参数范围,或者把主设备连接参数范围尽量放宽,等连接建立后再用连接参数更新请求协商。
三是连接请求和扫描停止之间比较仓促。我在代码里加了 2ms 延时后基本不再遇到 NRF_ERROR_BUSY。另外注意不要同时在多个上下文里调用 sd_ble_gap_connect,否则协议栈会返回“连接请求已在处理中”。
4.3 连接后掉线,突破口在断开原因码和连接参数
连接建立后频繁断开,是最难查的一类问题,因为表面现象都一样:连接好了,过一会儿又断,再重连再断。我建议第一件事就是打印 BleGapEvtDisconnected 里的 reason 字段。S130 协议栈常用的 reason 包括 0x08(连接超时,链路层在监督超时时间内没收到数据)、0x13(远端用户终止连接)等。
如果是 reason 0x08,说明链路层在超时时间内一直没有成功完成数据交互。原因往往不是信号差,而是连接参数不合理。比如监督超时太短,而连接间隔又太长,一次偶发丢包就足够触发掉线。我遇到过一个从设备,连接间隔配置到了 80ms,从设备延迟 2,结果监督超时还只留了 300ms,稍微有干扰就断。后来把监督超时调到 2000ms 左右,问题就消失了。
另一种常见情况是电源问题。nRF51822 在发射大功率时瞬间电流会冲到十几毫安,如果供电走的是弱 LDO 或纽扣电池,电压跌落会触发射频异常,导致连接事件丢失。排查时可以外接稳压源,同时把发射功率降到 0dBm 试试。
4.4 排查利器:日志、抓包和 nRF Connect
如果你遇到我的这些问题,手边最好准备三样东西:串口日志、抓包器、手机端 nRF Connect。
串口日志不用多说,把所有协议栈事件和返回值打出来。抓包器能直观看到广播包、扫描请求、连接请求和链路层数据包,我用它定位过广播类型错误和连接参数异常。如果手头暂时没有 BLE 抓包器,用手机上的 nRF Connect 也能做很基础的扫描和连接测试,至少能确认从设备状态。排查顺序上,我建议先从“从设备能不能被别的主设备连上”开始,把问题边界一步步缩小,不要一上来就怀疑 nRF51822 协议栈。
最后再分享一个我常用的调试技巧:在扫描回调里除了筛选目标设备,同时把 RSSI、广播类型、设备名一起打印出来。这样扫描阶段能直观看到信号强度和广播包内容,很多连接问题其实在扫描阶段就已经有苗头了。比如 RSSI 在 -90dBm 以下,就算连接成功,链路稳定性也不会好,这时候先解决天线和距离,比在代码里反复调连接参数更有效。
本文还有配套的精品资源,点击获取