文章目录
-
- 每日一句正能量
- 导读
- 一、背景:BLE从机开发的"最后一公里"陷阱
- [二、BLE GATT架构与nRF52840协议栈](#二、BLE GATT架构与nRF52840协议栈)
-
- [2.1 协议栈层次与GATT服务树](#2.1 协议栈层次与GATT服务树)
- [2.2 GATT核心概念](#2.2 GATT核心概念)
- [三、开发环境搭建:nRF5 SDK与SoftDevice](#三、开发环境搭建:nRF5 SDK与SoftDevice)
-
- [3.1 SDK选择与工具链](#3.1 SDK选择与工具链)
- [3.2 工具链安装](#3.2 工具链安装)
- [3.3 Flash分区规划](#3.3 Flash分区规划)
- 四、BLE连接时序与事件处理
-
- [4.1 连接建立流程](#4.1 连接建立流程)
- [4.2 事件处理框架](#4.2 事件处理框架)
- 五、自定义服务与特征值设计
-
- [5.1 服务设计原则](#5.1 服务设计原则)
- [5.2 Environmental Sensing服务实现](#5.2 Environmental Sensing服务实现)
- [5.3 CCCD处理的关键陷阱](#5.3 CCCD处理的关键陷阱)
- 六、功耗优化与连接参数调优
-
- [6.1 功耗模型与优化策略](#6.1 功耗模型与优化策略)
- [6.2 连接参数对功耗的影响](#6.2 连接参数对功耗的影响)
- [6.3 广播参数优化](#6.3 广播参数优化)
- [七、MTU与Data Length Extension](#七、MTU与Data Length Extension)
-
- [7.1 MTU协商](#7.1 MTU协商)
- [7.2 Data Length Extension (DLE)](#7.2 Data Length Extension (DLE))
- 八、安全与配对
-
- [8.1 安全模式配置](#8.1 安全模式配置)
- 九、常见问题与调试技巧
- 十、总结:BLE从机设计的系统工程

每日一句正能量
能掌控情绪的人,才能驾驭人生。
情绪是方向盘,不是油门。失控时,情绪是"自动驾驶"的本能反应(杏仁核劫持);掌控时,情绪是"手动挡"的信号工具。掌控不是"憋着不生气",而是觉察---暂停---选择------在刺激和回应之间,留出那片选择的空隙。驾驭人生,靠的是这个空隙里的理性抉择。
愿你在释怀的旷野里,走得轻盈,也走得坚定。
导读
谁说嵌入式只是调包和焊板子?BLE从机开发不是简单的"调用Nordic SDK API"------从GATT服务树的Handle分配,到CCCD的Notify/Indicate状态机,从连接参数的功耗权衡,到SoftDevice与应用程序的Flash分区博弈,每一个设计决策都决定了产品的续航表现和用户体验。本文基于nRF5 SDK + SoftDevice S140,从零构建一个完整的Environmental Sensing从机设备。
一、背景:BLE从机开发的"最后一公里"陷阱
在物联网产品开发中,BLE从机(Peripheral)是最常见的设备形态------智能手环、环境传感器、门锁、Beacon等。然而,许多团队在"最后一公里"遇到以下痛点:
- 服务设计混乱:UUID随意分配,特征值权限错误,导致iOS/Android兼容性差
- 功耗失控:连接间隔设置不当,从机未充分利用Slave Latency,CR2032电池几天耗尽
- Notify丢失:未正确处理CCCD(Client Characteristic Configuration Descriptor),手机收不到数据推送
- MTU瓶颈:默认23字节MTU传输大数据时效率低下,未启用Data Length Extension
- OTA升级缺失:产品发布后无法远程升级固件,SoftDevice与Bootloader分区规划失误
本文基于 nRF52840-DK + nRF5 SDK v17.1.0 + SoftDevice S140 v7.3.0,构建一个完整的Environmental Sensing从机,涵盖GATT服务设计、特征值权限配置、连接参数优化、以及功耗调优的完整技术路径。
二、BLE GATT架构与nRF52840协议栈
2.1 协议栈层次与GATT服务树

nRF52840采用分离式架构:控制器(Controller)集成在芯片内,主机(Host)由SoftDevice提供。两者通过HCI(Host Controller Interface)通信,但nRF52840的HCI是内部接口,无需外部传输。
| 层次 | 功能 | nRF52840实现 |
|---|---|---|
| Physical Layer | 2.4GHz射频,40信道 | 集成balun,+8dBm PA |
| Link Layer | 广播、扫描、连接、加密 | 硬件实现,4.6mA TX/RX |
| L2CAP | 信道复用,MTU协商 | 支持LE Credit-Based Flow Control |
| ATT | 基于Handle的属性读写 | MTU最大247字节(DLE启用) |
| GATT | 服务、特征值、描述符 | 由应用层通过SoftDevice API定义 |
| GAP | 角色、安全、隐私、连接参数 | 广播数据、白名单、绑定管理 |
2.2 GATT核心概念
Handle:16位唯一标识符,从0x0001开始自动分配。每个属性(服务声明、特征值声明、特征值本身、描述符)都有一个Handle。
UUID:16位(SIG定义)或128位(厂商自定义)。自定义服务必须使用128-bit UUID,避免与SIG标准冲突。
CCCD(Client Characteristic Configuration Descriptor,0x2902):这是BLE从机开发中最容易出错的环节。CCCD是一个2字节的描述符,bit0控制Notify,bit1控制Indicate。手机App必须写入CCCD才能接收Notify/Indicate推送。
三、开发环境搭建:nRF5 SDK与SoftDevice
3.1 SDK选择与工具链

| SDK版本 | 特点 | 适用场景 |
|---|---|---|
| nRF5 SDK v17.1.0 | 成熟稳定,SoftDevice支持完整,FreeRTOS可用 | 量产项目,需要S140完整功能 |
| nRF Connect SDK v2.5+ | 基于Zephyr RTOS,开源协议栈 | 新项目,需要Thread/Matter |
| SoftDevice S140 | 支持Central + Peripheral并发,256KB Flash | 复杂角色设备 |
| SoftDevice S113 | 仅Peripheral,128KB Flash | 纯从机设备,节省空间 |
本文选择nRF5 SDK v17.1.0 + SoftDevice S140,原因:
- S140支持并发多连接(1 Central + 8 Peripheral)
- nRF5 SDK的BLE示例最丰富,社区支持最好
- SoftDevice的API稳定,量产验证充分
3.2 工具链安装
bash
# 1. 安装nRF Command Line Tools
# https://www.nordicsemi.com/Products/Development-tools/nrf-command-line-tools
# 包含: nrfjprog (烧录), mergehex (合并hex), J-Link驱动
# 2. 安装Segger Embedded Studio (SES)
# Nordic官方IDE,免费用于nRF设备
# https://www.segger.com/downloads/embedded-studio
# 3. 安装nRF Connect for Desktop
# 包含: Bluetooth LE Explorer (调试), Programmer (烧录), Power Profiler (功耗分析)
# 4. 验证安装
nrfjprog --version # 应输出版本号
3.3 Flash分区规划
nRF52840有1MB Flash,SoftDevice S140占用前256KB:
0x0000_0000 ~ 0x0003_FFFF: SoftDevice S140 (256KB)
0x0004_0000 ~ 0x000B_FFFF: Application (512KB)
0x000C_0000 ~ 0x000D_FFFF: Bootloader (128KB, for DFU)
0x000E_0000 ~ 0x000E_FFFF: Settings / FDS (64KB)
0x000F_0000 ~ 0x000F_FFFF: Reserved (64KB)
关键陷阱 :Application的起始地址必须与SoftDevice的SD_SIZE匹配。S140 v7.3.0的SD_SIZE为0x26000(152KB),但实际占用到0x40000(256KB)以确保对齐。链接器脚本中的FLASH_START必须设置为0x0004_0000。
四、BLE连接时序与事件处理
4.1 连接建立流程

连接建立的完整时序:
- 从机广播 :
sd_ble_gap_adv_start()发送ADV_IND广播包 - 主机扫描:手机扫描到广播,发送SCAN_REQ
- 从机响应:回复SCAN_RSP(包含设备名称等)
- 主机连接:发送CONNECT_IND,进入连接状态
- 连接事件 :SoftDevice触发
BLE_GAP_EVT_CONNECTED - 服务发现:主机读取从机的GATT服务树
- 数据交互:Read/Write/Notify/Indicate
4.2 事件处理框架
c
/* main.c --- BLE事件处理框架 */
#include <stdint.h>
#include <string.h>
#include "nrf_sdh.h"
#include "nrf_sdh_ble.h"
#include "ble.h"
#include "ble_gap.h"
#include "ble_gatts.h"
#include "ble_srv_common.h"
#include "app_error.h"
#include "nrf_log.h"
#define DEVICE_NAME "EnvSensor"
#define MANUFACTURER_NAME "Nordic"
#define APP_BLE_OBSERVER_PRIO 3
#define APP_BLE_CONN_CFG_TAG 1
/* 广播参数 */
#define APP_ADV_INTERVAL 1600 /* 1000ms (in 0.625ms units) */
#define APP_ADV_DURATION 0 /* 无限广播 */
/* 连接参数 */
#define MIN_CONN_INTERVAL MSEC_TO_UNITS(100, UNIT_1_25_MS)
#define MAX_CONN_INTERVAL MSEC_TO_UNITS(200, UNIT_1_25_MS)
#define SLAVE_LATENCY 4
#define CONN_SUP_TIMEOUT MSEC_TO_UNITS(6000, UNIT_10_MS)
/* 全局变量 */
static uint16_t m_conn_handle = BLE_CONN_HANDLE_INVALID;
static ble_uuid_t m_adv_uuids[] =
{
{BLE_UUID_ENVIRONMENTAL_SENSING_SERVICE, BLE_UUID_TYPE_BLE} /* SIG标准UUID */
};
/* BLE事件处理函数 */
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
(void)p_context;
switch (p_ble_evt->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED:
{
NRF_LOG_INFO("Connected, conn_handle: %d", p_ble_evt->evt.gap_evt.conn_handle);
m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
/* 请求连接参数更新 */
ble_gap_conn_params_t conn_params;
conn_params.min_conn_interval = MIN_CONN_INTERVAL;
conn_params.max_conn_interval = MAX_CONN_INTERVAL;
conn_params.slave_latency = SLAVE_LATENCY;
conn_params.conn_sup_timeout = CONN_SUP_TIMEOUT;
ret_code_t err_code = sd_ble_gap_conn_param_update(m_conn_handle, &conn_params);
APP_ERROR_CHECK(err_code);
break;
}
case BLE_GAP_EVT_DISCONNECTED:
{
NRF_LOG_INFO("Disconnected, reason: %d", p_ble_evt->evt.gap_evt.params.disconnected.reason);
m_conn_handle = BLE_CONN_HANDLE_INVALID;
/* 重新启动广播 */
advertising_start();
break;
}
case BLE_GAP_EVT_CONN_PARAM_UPDATE:
{
NRF_LOG_INFO("Connection params updated: interval %d, latency %d, timeout %d",
p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.max_conn_interval,
p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.slave_latency,
p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.conn_sup_timeout);
break;
}
case BLE_GATTS_EVT_WRITE:
{
/* 处理客户端写入(如CCCD配置) */
on_write(p_ble_evt);
break;
}
case BLE_GATTS_EVT_HVN_TX_COMPLETE:
{
/* Notify发送完成,可以继续发送 */
m_hvn_in_progress = false;
break;
}
default:
break;
}
}
/* BLE栈初始化 */
static void ble_stack_init(void)
{
ret_code_t err_code;
/* 启用SoftDevice */
err_code = nrf_sdh_enable_request();
APP_ERROR_CHECK(err_code);
/* 配置BLE连接配置 */
uint32_t ram_start = 0;
err_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
APP_ERROR_CHECK(err_code);
/* 启用BLE栈 */
err_code = nrf_sdh_ble_enable(&ram_start);
APP_ERROR_CHECK(err_code);
/* 注册BLE事件观察者 */
NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
}
五、自定义服务与特征值设计
5.1 服务设计原则

设计原则:
- UUID规划:自定义服务使用128-bit UUID,基于Base UUID + 16-bit偏移
- 特征值粒度:每个传感器一个特征值,避免大数据块传输
- 权限最小化:只开放必要的Read/Write/Notify权限
- CCCD必备:任何支持Notify/Indicate的特征值必须附带CCCD
- MTU意识:大数据包考虑分片或增加MTU
5.2 Environmental Sensing服务实现
c
/* ble_env_sensing.h --- 环境感知服务头文件 */
#ifndef BLE_ENV_SENSING_H__
#define BLE_ENV_SENSING_H__
#include <stdint.h>
#include <stdbool.h>
#include "ble.h"
#include "ble_srv_common.h"
/* 128-bit Base UUID: 0000xxxx-1212-EFDE-1523-785FEABCD123 */
#define ENV_SENSING_BASE_UUID {{0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, \
0xDE, 0xEF, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00}}
/* 16-bit UUID offsets */
#define UUID_ENV_SERVICE 0xEF00
#define UUID_TEMP_CHAR 0xEF01
#define UUID_HUMIDITY_CHAR 0xEF02
#define UUID_PRESSURE_CHAR 0xEF03
#define UUID_COMMAND_CHAR 0xEF10
#define UUID_STATUS_CHAR 0xEF11
/* 特征值属性 */
#define CHAR_PROP_READ (BLE_GATT_HVX_NOTIFICATION)
#define CHAR_PROP_WRITE (BLE_GATT_HVX_NOTIFICATION)
#define CHAR_PROP_NOTIFY (BLE_GATT_HVX_NOTIFICATION)
/* 数据结构 */
typedef struct
{
int16_t temperature; /* 0.01°C, signed */
uint16_t humidity; /* 0.01% RH */
uint32_t pressure; /* 0.1 hPa */
} env_data_t;
typedef struct
{
uint8_t cmd_id; /* Command ID */
uint8_t param[19]; /* Parameters, max 20 bytes */
} command_t;
typedef struct
{
uint16_t service_handle; /* Service handle assigned by SoftDevice */
ble_gatts_char_handles_t temp_handles; /* Temperature characteristic handles */
ble_gatts_char_handles_t humidity_handles; /* Humidity characteristic handles */
ble_gatts_char_handles_t pressure_handles; /* Pressure characteristic handles */
ble_gatts_char_handles_t cmd_handles; /* Command characteristic handles */
ble_gatts_char_handles_t status_handles; /* Status characteristic handles */
uint16_t conn_handle; /* Current connection handle */
bool is_notification_enabled; /* CCCD state */
} ble_env_sensing_t;
/* 初始化函数 */
uint32_t ble_env_sensing_init(ble_env_sensing_t * p_env_sensing);
/* 数据更新函数 */
uint32_t ble_env_sensing_temp_update(ble_env_sensing_t * p_env_sensing, int16_t temp);
uint32_t ble_env_sensing_humidity_update(ble_env_sensing_t * p_env_sensing, uint16_t humidity);
uint32_t ble_env_sensing_pressure_update(ble_env_sensing_t * p_env_sensing, uint32_t pressure);
/* 状态通知 */
uint32_t ble_env_sensing_status_notify(ble_env_sensing_t * p_env_sensing, uint32_t status);
/* 事件处理 */
void ble_env_sensing_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context);
#endif /* BLE_ENV_SENSING_H__ */
c
/* ble_env_sensing.c --- 环境感知服务实现 */
#include "ble_env_sensing.h"
#include "nrf_log.h"
/* 128-bit UUID类型 */
static ble_uuid_t m_base_uuid;
/* 初始化服务 */
uint32_t ble_env_sensing_init(ble_env_sensing_t * p_env_sensing)
{
ret_code_t err_code;
ble_uuid_t ble_uuid;
/* 初始化结构体 */
memset(p_env_sensing, 0, sizeof(ble_env_sensing_t));
p_env_sensing->conn_handle = BLE_CONN_HANDLE_INVALID;
/* 添加Base UUID到SoftDevice的UUID表 */
ble_uuid128_t base_uuid = {ENV_SENSING_BASE_UUID};
err_code = sd_ble_uuid_vs_add(&base_uuid, &m_base_uuid.type);
VERIFY_SUCCESS(err_code);
/* 添加服务 */
ble_uuid.uuid = UUID_ENV_SERVICE;
err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_env_sensing->service_handle);
VERIFY_SUCCESS(err_code);
/* 添加Temperature特征值 */
ble_gatts_char_md_t char_md = {0};
ble_gatts_attr_md_t cccd_md = {0};
ble_gatts_attr_t attr_char_value = {0};
ble_gatts_attr_md_t attr_md = {0};
/* CCCD属性 */
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm);
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm);
cccd_md.vloc = BLE_GATTS_VLOC_STACK;
char_md.p_cccd_md = &cccd_md;
char_md.char_props.read = 1;
char_md.char_props.notify = 1;
/* 特征值属性 */
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm);
BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&attr_md.write_perm);
attr_md.vloc = BLE_GATTS_VLOC_STACK;
attr_md.rd_auth = 0;
attr_md.wr_auth = 0;
attr_md.vlen = 0;
/* UUID */
ble_uuid.uuid = UUID_TEMP_CHAR;
/* 初始值 */
int16_t initial_temp = 2500; /* 25.00°C */
attr_char_value.p_uuid = &ble_uuid;
attr_char_value.p_attr_md = &attr_md;
attr_char_value.init_len = sizeof(int16_t);
attr_char_value.init_offs = 0;
attr_char_value.max_len = sizeof(int16_t);
attr_char_value.p_value = (uint8_t *)&initial_temp;
err_code = sd_ble_gatts_characteristic_add(p_env_sensing->service_handle, &char_md, &attr_char_value, &p_env_sensing->temp_handles);
VERIFY_SUCCESS(err_code);
/* 添加Humidity特征值(类似结构,省略重复代码) */
/* ... */
/* 添加Pressure特征值 */
/* ... */
/* 添加Command特征值(Write/Write Without Response) */
memset(&char_md, 0, sizeof(char_md));
memset(&attr_md, 0, sizeof(attr_md));
char_md.char_props.write = 1;
char_md.char_props.write_wo_resp = 1;
BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&attr_md.read_perm);
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.write_perm);
attr_md.vloc = BLE_GATTS_VLOC_STACK;
attr_md.vlen = 1; /* 可变长度 */
ble_uuid.uuid = UUID_COMMAND_CHAR;
uint8_t initial_cmd[20] = {0};
attr_char_value.p_uuid = &ble_uuid;
attr_char_value.p_attr_md = &attr_md;
attr_char_value.init_len = 1;
attr_char_value.max_len = 20;
attr_char_value.p_value = initial_cmd;
err_code = sd_ble_gatts_characteristic_add(p_env_sensing->service_handle, &char_md, &attr_char_value, &p_env_sensing->cmd_handles);
VERIFY_SUCCESS(err_code);
/* 添加Status特征值(Notify only) */
/* ... */
NRF_LOG_INFO("Environmental Sensing service initialized");
return NRF_SUCCESS;
}
/* 更新温度并Notify */
uint32_t ble_env_sensing_temp_update(ble_env_sensing_t * p_env_sensing, int16_t temp)
{
if (p_env_sensing->conn_handle == BLE_CONN_HANDLE_INVALID)
{
return NRF_ERROR_INVALID_STATE;
}
/* 检查CCCD是否启用Notify */
if (!p_env_sensing->is_notification_enabled)
{
return NRF_ERROR_INVALID_STATE;
}
/* 准备Notify数据 */
ble_gatts_hvx_params_t hvx_params = {0};
uint16_t len = sizeof(int16_t);
hvx_params.handle = p_env_sensing->temp_handles.value_handle;
hvx_params.type = BLE_GATT_HVX_NOTIFICATION;
hvx_params.offset = 0;
hvx_params.p_len = &len;
hvx_params.p_data = (uint8_t *)&temp;
return sd_ble_gatts_hvx(p_env_sensing->conn_handle, &hvx_params);
}
/* BLE事件处理 */
void ble_env_sensing_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
ble_env_sensing_t * p_env_sensing = (ble_env_sensing_t *)p_context;
switch (p_ble_evt->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED:
p_env_sensing->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
break;
case BLE_GAP_EVT_DISCONNECTED:
p_env_sensing->conn_handle = BLE_CONN_HANDLE_INVALID;
p_env_sensing->is_notification_enabled = false;
break;
case BLE_GATTS_EVT_WRITE:
{
ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
/* 检查是否是CCCD写入 */
if (p_evt_write->handle == p_env_sensing->temp_handles.cccd_handle)
{
if (ble_srv_is_notification_enabled(p_evt_write->data))
{
p_env_sensing->is_notification_enabled = true;
NRF_LOG_INFO("Temperature notification enabled");
}
else
{
p_env_sensing->is_notification_enabled = false;
NRF_LOG_INFO("Temperature notification disabled");
}
}
/* 检查是否是Command写入 */
if (p_evt_write->handle == p_env_sensing->cmd_handles.value_handle)
{
command_t cmd;
memcpy(&cmd, p_evt_write->data, p_evt_write->len);
NRF_LOG_INFO("Command received: ID=%d, len=%d", cmd.cmd_id, p_evt_write->len);
/* 处理命令 */
process_command(p_env_sensing, &cmd);
}
break;
}
default:
break;
}
}
5.3 CCCD处理的关键陷阱
陷阱1:未检查CCCD状态就发送Notify
c
/* 错误:直接发送Notify,不检查CCCD */
sd_ble_gatts_hvx(conn_handle, &hvx_params); /* 如果CCCD未启用,返回NRF_ERROR_INVALID_STATE */
/* 正确:先检查CCCD状态 */
if (p_env_sensing->is_notification_enabled)
{
sd_ble_gatts_hvx(conn_handle, &hvx_params);
}
陷阱2:Notify发送过快导致队列溢出
c
/* 错误:连续发送多个Notify */
for (int i = 0; i < 100; i++)
{
sd_ble_gatts_hvx(conn_handle, &hvx_params); /* 可能返回NRF_ERROR_RESOURCES */
}
/* 正确:等待BLE_GATTS_EVT_HVN_TX_COMPLETE事件 */
static volatile bool m_tx_complete = true;
void on_hvn_tx_complete(void)
{
m_tx_complete = true;
}
void send_data_chunk(void)
{
if (m_tx_complete && has_more_data())
{
m_tx_complete = false;
sd_ble_gatts_hvx(conn_handle, &hvx_params);
}
}
六、功耗优化与连接参数调优
6.1 功耗模型与优化策略

nRF52840的功耗主要由射频活动决定。在连接状态下,功耗与连接间隔(Connection Interval)和从机延迟(Slave Latency)直接相关。
| 模式 | 电流 | 优化策略 |
|---|---|---|
| Radio TX (0dBm) | 4.6mA | 使用2Mbps PHY缩短TX时间 |
| Radio TX (+8dBm) | 11.2mA | 仅在需要远距离时启用 |
| Radio RX | 4.6mA | 减少不必要的扫描窗口 |
| CPU Active (64MHz) | 3.4mA | 使用RTC替代忙等待 |
| CPU Sleep (RAM on) | 1.5μA | 启用System ON低功耗模式 |
| System OFF | 0.4μA | 深度睡眠,仅GPIO/RTC唤醒 |
6.2 连接参数对功耗的影响
| 场景 | 连接间隔 | Slave Latency | 平均电流 | 电池寿命(CR2032) |
|---|---|---|---|---|
| 低延迟(游戏手柄) | 7.5ms | 0 | 3.5mA | ~1天 |
| 平衡(健康设备) | 30ms | 4 | 500μA | ~1周 |
| 低功耗(环境传感器) | 100ms | 29 | 50μA | ~2个月 |
c
/* 连接参数更新请求 */
void request_low_power_mode(uint16_t conn_handle)
{
ble_gap_conn_params_t conn_params;
/* 低功耗模式:100ms间隔,跳过29个事件 */
conn_params.min_conn_interval = MSEC_TO_UNITS(100, UNIT_1_25_MS); /* 80 (100ms) */
conn_params.max_conn_interval = MSEC_TO_UNITS(200, UNIT_1_25_MS); /* 160 (200ms) */
conn_params.slave_latency = 29; /* 最多跳过29个连接事件 */
conn_params.conn_sup_timeout = MSEC_TO_UNITS(6000, UNIT_10_MS); /* 600 (6s) */
ret_code_t err_code = sd_ble_gap_conn_param_update(conn_handle, &conn_params);
APP_ERROR_CHECK(err_code);
}
/* 从机延迟的含义:
* Slave Latency = 4 表示:从机可以跳过最多4个连接事件不监听
* 如果主机在每个连接事件都发送数据,从机必须响应
* 如果主机没有数据,从机可以跳过最多4个事件
* 这大幅降低了RF活动,节省功耗
*/
6.3 广播参数优化
c
/* 广播初始化 */
void advertising_init(void)
{
ble_advertising_init_t init = {0};
/* 广播数据 */
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;
/* 广播包含服务UUID */
init.advdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
init.advdata.uuids_complete.p_uuids = m_adv_uuids;
/* 广播间隔:100ms (160 * 0.625ms) */
/* 更长的间隔 = 更低功耗,但更慢被发现 */
init.config.ble_adv_fast_enabled = true;
init.config.ble_adv_fast_interval = 160; /* 100ms */
init.config.ble_adv_fast_timeout = 3000; /* 广播30秒后进入慢速模式 */
/* 慢速广播:1s间隔,极低功耗 */
init.config.ble_adv_slow_enabled = true;
init.config.ble_adv_slow_interval = 3200; /* 2s */
init.config.ble_adv_slow_timeout = 0; /* 无限 */
/* TX功率:0dBm(平衡范围和功耗) */
init.config.ble_adv_primary_phy = BLE_GAP_PHY_1MBPS;
init.config.ble_adv_secondary_phy = BLE_GAP_PHY_2MBPS;
err_code = ble_advertising_init(&m_advertising, &init, advertising_error_handler, NULL);
APP_ERROR_CHECK(err_code);
/* 设置TX功率 */
err_code = sd_ble_gap_tx_power_set(BLE_GAP_TX_POWER_ROLE_ADV, m_advertising.adv_handle, 0);
APP_ERROR_CHECK(err_code);
}
七、MTU与Data Length Extension
7.1 MTU协商
默认MTU为23字节(ATT层),有效载荷为20字节(3字节ATT头)。通过MTU协商可提升至247字节(nRF52840支持的最大值)。
c
/* MTU请求处理 */
void on_mtu_request(ble_evt_t const * p_ble_evt)
{
ble_gatts_evt_exchange_mtu_request_t const * p_request =
&p_ble_evt->evt.gatts_evt.params.exchange_mtu_request;
NRF_LOG_INFO("MTU request: client wants %d", p_request->client_rx_mtu);
/* 响应MTU协商 */
uint16_t server_rx_mtu = NRF_SDH_BLE_GATT_MAX_MTU_SIZE; /* 247 */
ret_code_t err_code = sd_ble_gatts_exchange_mtu_reply(p_ble_evt->evt.gatts_evt.conn_handle, server_rx_mtu);
APP_ERROR_CHECK(err_code);
}
/* 协商完成后的处理 */
void on_mtu_exchange_complete(ble_evt_t const * p_ble_evt)
{
uint16_t negotiated_mtu = p_ble_evt->evt.gatts_evt.params.exchange_mtu_reply.server_rx_mtu;
NRF_LOG_INFO("MTU negotiated: %d bytes", negotiated_mtu);
/* 计算最大ATT payload */
m_max_att_payload = negotiated_mtu - 3; /* 减去ATT头 */
}
7.2 Data Length Extension (DLE)
DLE允许链路层发送更大的数据包(最大251字节),减少分包开销。
c
/* 启用DLE */
void data_length_init(void)
{
ble_gap_data_length_params_t dl_params = {0};
dl_params.max_tx_octets = 251; /* 最大TX字节 */
dl_params.max_tx_time_us = 2120; /* 对应251字节 @ 2Mbps */
dl_params.max_rx_octets = 251;
dl_params.max_rx_time_us = 2120;
ret_code_t err_code = sd_ble_gap_data_length_update(m_conn_handle, &dl_params, NULL);
APP_ERROR_CHECK(err_code);
}
八、安全与配对
8.1 安全模式配置
c
/* 安全配置 */
void security_init(void)
{
ble_gap_sec_params_t sec_param = {0};
sec_param.bond = 1; /* 启用绑定 */
sec_param.mitm = 1; /* 要求MITM保护 */
sec_param.lesc = 1; /* 启用LE Secure Connections */
sec_param.keypress = 0;
sec_param.io_caps = BLE_GAP_IO_CAPS_DISPLAY_YESNO; /* 显示+确认 */
sec_param.oob = 0;
sec_param.min_key_size = 7;
sec_param.max_key_size = 16;
sec_param.kdist_own.enc = 1;
sec_param.kdist_own.id = 1;
sec_param.kdist_peer.enc = 1;
sec_param.kdist_peer.id = 1;
ret_code_t err_code = pm_sec_params_set(&sec_param);
APP_ERROR_CHECK(err_code);
err_code = pm_register(pm_evt_handler);
APP_ERROR_CHECK(err_code);
}
九、常见问题与调试技巧
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 手机无法发现设备 | 扫描列表为空 | 广播间隔过长或TX功率过低 | 检查ble_adv_fast_interval,设置为160(100ms) |
| 连接后立即断开 | supervision timeout | 连接参数不被手机接受 | 手机可能拒绝过大的interval,先请求30ms再逐步增加 |
| Notify收不到 | 手机端无数据 | CCCD未写入或Handle错误 | 确保CCCD Handle正确,检查is_notification_enabled |
| 功耗异常高 | 电池快速耗尽 | Slave Latency=0或连接间隔过短 | 设置Slave Latency≥4,interval≥100ms |
| MTU协商失败 | 大数据传输被截断 | 手机不支持大MTU | 检查手机BLE版本,iOS 11+支持185B,Android 8+支持517B |
| Flash空间不足 | 编译报错 | SoftDevice + App超过1MB | 使用S113替代S140,节省128KB |
十、总结:BLE从机设计的系统工程
nRF52840的BLE从机开发不是简单的"调用SDK API",而是一个涉及射频、协议栈、功耗、安全的系统工程:
- GATT设计:128-bit UUID规划、特征值粒度、权限最小化、CCCD必备
- 连接参数:Interval/Latency/Timeout的功耗-延迟权衡
- MTU优化:DLE启用、大数据包分片策略
- 安全架构:LESC配对、MITM防护、绑定密钥管理
- 功耗调优:广播间隔、TX功率、PHY选择、System ON/OFF模式
本文完整工程代码已按MIT协议开源,包含Environmental Sensing服务实现、连接参数动态调整、MTU协商、以及功耗测试配置。
在BLE开发中,真正的功力不在于调用了sd_ble_gatts_characteristic_add()这个API,而在于理解GATT服务树的Handle分配逻辑、CCCD的状态机管理、以及连接参数对功耗的量化影响。
转载自:https://blog.csdn.net/u014727709/article/details/162208873
欢迎 👍点赞✍评论⭐收藏,欢迎指正