nRF52840低功耗蓝牙从机开发:服务与特征值设计

文章目录


每日一句正能量

能掌控情绪的人,才能驾驭人生。

情绪是方向盘,不是油门。失控时,情绪是"自动驾驶"的本能反应(杏仁核劫持);掌控时,情绪是"手动挡"的信号工具。掌控不是"憋着不生气",而是觉察---暂停---选择------在刺激和回应之间,留出那片选择的空隙。驾驭人生,靠的是这个空隙里的理性抉择。

愿你在释怀的旷野里,走得轻盈,也走得坚定。

导读

谁说嵌入式只是调包和焊板子?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 连接建立流程

连接建立的完整时序:

  1. 从机广播sd_ble_gap_adv_start()发送ADV_IND广播包
  2. 主机扫描:手机扫描到广播,发送SCAN_REQ
  3. 从机响应:回复SCAN_RSP(包含设备名称等)
  4. 主机连接:发送CONNECT_IND,进入连接状态
  5. 连接事件 :SoftDevice触发BLE_GAP_EVT_CONNECTED
  6. 服务发现:主机读取从机的GATT服务树
  7. 数据交互: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 服务设计原则

设计原则

  1. UUID规划:自定义服务使用128-bit UUID,基于Base UUID + 16-bit偏移
  2. 特征值粒度:每个传感器一个特征值,避免大数据块传输
  3. 权限最小化:只开放必要的Read/Write/Notify权限
  4. CCCD必备:任何支持Notify/Indicate的特征值必须附带CCCD
  5. 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",而是一个涉及射频、协议栈、功耗、安全的系统工程:

  1. GATT设计:128-bit UUID规划、特征值粒度、权限最小化、CCCD必备
  2. 连接参数:Interval/Latency/Timeout的功耗-延迟权衡
  3. MTU优化:DLE启用、大数据包分片策略
  4. 安全架构:LESC配对、MITM防护、绑定密钥管理
  5. 功耗调优:广播间隔、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

欢迎 👍点赞✍评论⭐收藏,欢迎指正