天天看点

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

文章目录

  • 一、如何提高BLE 数据传输速率?
    • 1.1 Nordic BLE 最大数据吞吐率是多少?
    • 1.2 如何获知BLE 当前数据吞吐率?
    • 1.3 如何提高BLE 数据传输速率?
      • 1.3.1 LE 1M PHY 最大数据吞吐率
      • 1.3.2 LE 2M PHY 最大数据吞吐率
    • 1.4 如何同步数据的生产与发送?
  • 二、如何设置广播连接参数以满足低功耗需求?
  • 更多文章:
我们开发的BLE 设备多数都有两点要求:一是低功耗,电池供电需要持续工作数周甚至数个月;二是将BLE peripheral产生的数据快速传送给central,传输数据功耗较高,提高传输速率缩短传输时间也有利于降低平均功耗。我们该如何设置广播参数与连接参数以达到我们要求的功耗呢?该如何设置连接参数与报文长度(PDU / MTU)以尽可能达到最大传输速率呢?

一、如何提高BLE 数据传输速率?

BLE 数据传输相关的服务中有一个比较基础的串口透传服务,本文以nRF5_SDK_17.0.2 中的ble_app_uart 工程为例,展示如何提高BLE 的数据传输速率。

在尝试提高BLE 数据传输速率前,需要先获得两个信息:

  1. 当前使用的BLE 协议栈支持的理论最大数据吞吐率是多少?
  2. 如何获知当前的BLE 数据传输速率是多少?

1.1 Nordic BLE 最大数据吞吐率是多少?

对于第一个问题,我们可以从Nordic 协议栈规格说明书中获知,比如使用s132 softdevice 可以参考文档:S132 SoftDevice SoftDevice Specification v7.1,查阅Bluetooth Low Energy data throughput 章节,数据传输速率使用下面的公式:

#define OPCODE_LENGTH        1
#define HANDLE_LENGTH        2

Throughput_bps = num_packets * (ATT_MTU - OPCODE_LENGTH - HANDLE_LENGTH) * 8 / seconds
           

这里统计的传输数据指的是应用数据,ATT_MTU 减去Attribute protocol PDU Opcode 和Attribute Handle 字段长度,剩下的就是Attribute Value 字段(也即有效的应用数据)。每个字节占8 比特,下表中的传输速率单位是kbps(如果要换算成 KB/s 需要除以8),下表Connection interval 与Connection Event Length 相等:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

从上表可知,跟传输速率相关的因素主要有ATT MTU size、Connection interval、Connection Event Length、Communication Mode、LE PHY speed 等。比如ATT MTU size 为23、Connection interval 与Connection Event Length 取7.5 ms、Communication Mode 为Send Notification、LE 1M PHY 的最大速率为24 KB/s;ATT MTU size 为247、Connection interval 与Connection Event Length 取50 ms、Communication Mode 为Send Notification、LE 2M PHY 的最大速率为165.94 KB/s。

1.2 如何获知BLE 当前数据吞吐率?

一般BLE peripheral 作为GATT Server 向BLE central 也即GATT Client 传输数据,想获得BLE 当前的数据吞吐率,一般有三种方式:

  1. BLE peripheral 端统计单位时间内发送出去的数据量;
  2. BLE central 端统计单位时间内接收到的数据量;
  3. BLE sniffer 抓取单位时间内传输的报文中有效数据量。

Nordic 手机端的应用并没有提供显示当前数据吞吐率的功能,我们开发GATT Server 应用再去修改BLE central 代码比较麻烦。BLE sniffer 抓包分析倒是比较方便,wireshark + nRF sniffer 抓包方案容易丢包也没有直接统计数据吞吐率指标,专业的蓝牙分析仪Ellisys Bluetooth Explorer 倒是可以直接统计数据吞吐率,蓝牙分析仪成本太高。因此,本文选择第一种方案,在BLE peripheral 代码中添加统计数据发送量的功能,并通过RTT Log 打印出来。

我们在.\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart 示例工程的基础上添加统计单位时间内数据发送量的代码,Nordic UART Service 我们在博文:如何实现扫码连接BLE 设备的功能? 中已经介绍过了,二者主要的代码逻辑差不多,主要有两点不同:

  • ble_app_uart 工程在GAP 阶段作为Advertiser,在函数advertising_init 中初始化广播包内容、广播间隔、广播超时时间等,然后在函数advertising_start 中开始广播;ble_app_uart_c 工程在GAP 阶段作为Scanner 和Initiator,在函数scan_init 中设置扫描过滤条件、注册scan_evt_handler 等,然后在函数scan_start 中开始扫描周围的广播设备;
  • ble_app_uart 工程在GATT 阶段作为GATT Server,在函数services_init --> ble_nus_init 中添加NUS service(包括RX Characteristic、TX Characteristic)、注册nus_data_handler 等,其中NUS 为Primary Service 对外提供串口透传服务;ble_app_uart_c 工程在GATT 阶段作为GATT Client,在函数db_discovery_init 和nus_c_init 中发现对端设备提供了哪些services(特别是NUS 服务)、注册db_disc_handler 和ble_nus_c_evt_handler 等,发现NUS 服务后就可以访问该服务了。

本文就不展开介绍ble_app_uart 工程代码逻辑了,我们重点关心的是GATT Server 如何使用NUS 服务向GATT Client 发送数据。从函数uart_event_handle 代码可以看出,使用函数ble_nus_data_send 可以通过NUS 服务向对端设备发送数据,该函数的声明如下:

// .\nRF5_SDK_17.0.2_d674dde\components\ble\ble_services\ble_nus\ble_nus.h

/**@brief   Function for sending a data to the peer.
 *
 * @details This function sends the input string as an RX characteristic notification to the
 *          peer.
 *
 * @param[in]     p_nus       Pointer to the Nordic UART Service structure.
 * @param[in]     p_data      String to be sent.
 * @param[in,out] p_length    Pointer Length of the string. Amount of sent bytes.
 * @param[in]     conn_handle Connection Handle of the destination client.
 *
 * @retval NRF_SUCCESS If the string was sent successfully. Otherwise, an error code is returned.
 */
uint32_t ble_nus_data_send(ble_nus_t * p_nus,
                           uint8_t   * p_data,
                           uint16_t  * p_length,
                           uint16_t    conn_handle);
           

既然是统计单位时间内GATT Server 发送出去的数据量,自然需要一个定时器资源,我们选用低功耗的app_timer。为了提高数据发送速率,我们选择Send Notification 模式。为了少做无用功,我们在NUS Notification enable 的情况下再开始发送数据,当连接断开后便停止发送数据。新增用于统计BLE data throughput 的代码如下:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c

/**@brief Resources related to throughput testing.
 */
#define DATA_THROUGHPUT_INTERVAL            APP_TIMER_TICKS(5)                   /**< data throughput interval (ticks). */
APP_TIMER_DEF(m_timer_throughput_id);

uint32_t m_data_sent_length = 0;
uint8_t m_data_array[BLE_NUS_MAX_DATA_LEN] = {0};

/**@brief Data generation timer timeout handler function.
 */
static void data_throughput_timeout_handler(void * p_context)
{
    UNUSED_PARAMETER(p_context);
    
    static uint32_t timeout_count = 0;
    ret_code_t err_code;
    
    timeout_count++;

    do
    {
        uint16_t length = BLE_NUS_MAX_DATA_LEN;
        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
        if ((err_code != NRF_ERROR_INVALID_STATE) &&
            (err_code != NRF_ERROR_RESOURCES) &&
            (err_code != NRF_ERROR_NOT_FOUND))
        {
            APP_ERROR_CHECK(err_code);
        }

        if(err_code == NRF_SUCCESS)
        {
            m_data_sent_length += length;
            m_data_array[0]++;
            m_data_array[length-1]++;
        }
    } while (err_code == NRF_SUCCESS);

    // Timer interval 5 ms, when the timer reaches 1 second 
    if(timeout_count == 200)
    {
        // Send m_data_sent_length bytes of data within 1 second, which is equal to m_data_sent_length * 8 / 1024 kilobits of data
        NRF_LOG_INFO("****** BLE data throughput: %d kbps ******", m_data_sent_length >> 7);
        m_data_sent_length = 0;
        timeout_count = 0;
    }
}

/**@brief Function for initializing the timer module.
 */
static void timers_init(void)
{
    ......
    // Create a data generation timer for testing throughput.
    err_code = app_timer_create(&m_timer_throughput_id, 
                                APP_TIMER_MODE_REPEATED, 
                                data_throughput_timeout_handler);
    APP_ERROR_CHECK(err_code);
}

/**@brief Function for handling the data from the Nordic UART Service.
 */
static void nus_data_handler(ble_nus_evt_t * p_evt)
{

    if (p_evt->type == BLE_NUS_EVT_RX_DATA) {
        ......
    } else if(p_evt->type == BLE_NUS_EVT_COMM_STARTED) {
        // Start data throughput timers.
        ret_code_t err_code;
        err_code = app_timer_start(m_timer_throughput_id, 
                                   DATA_THROUGHPUT_INTERVAL,
                                   NULL);
        APP_ERROR_CHECK(err_code);
    }
}

/**@brief Function for handling BLE events.
 */
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
    uint32_t err_code;

    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GAP_EVT_CONNECTED:
            ......
        case BLE_GAP_EVT_DISCONNECTED:
            ......
            // Stop data throughput timers.
            err_code = app_timer_stop(m_timer_throughput_id);
            APP_ERROR_CHECK(err_code);
            break;

        case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
        ......
    }
}
           

上述代码主要包含两部分:

  1. app_timer 资源的创建、开始与结束,包括超时回调函数的注册。当NUS Notification enable 事件发生时开始定时器,当连接断开时停止定时器;
  2. 超时回调函数data_throughput_timeout_handler 的实现,主要有两个任务:一是调用函数ble_nus_data_send 发送数据(参照函数uart_event_handle 内调用函数ble_nus_data_send 并检查返回值的代码,仅当返回NRF_SUCCESS 时才计入已发送数据);二是通过RTT Log 打印1 秒内发送出去的数据量。

值得一提的是,每个定时周期可以发送不止一个数据包,博文链路层空口报文设计 中提到LE 1M PHY 发送最大PDU 约需2.3 ms,上面的代码设置的定时周期为5 ms,因此每个定时周期可以发送多个数据包,我们将ble_nus_data_send 放到循环体内,当返回值为NRF_SUCCESS 时继续循环发送下一个数据包。

通过RTT Log 打印当前BLE 数据吞吐量的代码已经实现了,编译工程 --> 将代码烧录到nRF52 DK 内,PC 端打开J-Link RTT Viewer,手机端打开nRF Connect for mobile。点击Enable CCCDs 或者Tx Characteristic 右边的图标使能NUS Notification,nRF52 DK 开始通过BLE 向手机端发送数据,nRF Connect --> Show log 可以查看接收到的数据,J-Link RTT Viewer 开始打印当前的BLE 数据吞吐率:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

1.3 如何提高BLE 数据传输速率?

上图展示的BLE 数据吞吐率只有41 kbps,远低于nordic softdevice 支持的最大数据吞吐率,这是怎么回事呢?

BLE 数据吞吐率的计算公式:

Throughput_kbps = num_packets * (ATT_MTU - 3) * 8 / 1000				// num_packets 为单位时间也即 1 秒内发送的数据包个数
				= (num_packets_interval / CONN_INTERVAL) * (ATT_MTU - 3) * 8 / 1000			// num_packets_interval 为单个连接间隔内发送的数据包个数,CONN_INTERVAL 为连接间隔,单位是秒
           

1.3.1 LE 1M PHY 最大数据吞吐率

上述工程默认的ATT_MTU 值为247,CONN_INTERVAL 为20 ~ 75 ms,由Throughput_kbps 等于41 kbps 可反推出num_packets_interval 等于1(CONN_INTERVAL 取中间值47.5 ms)。一个连接间隔只发送出去一个数据包,这大概是BLE 数据吞吐率这么低的主要原因吧,该如何提高BLE Throughput_kbps 呢?

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

前面也谈到,影响BLE Throughput_kbps 的因素主要有ATT MTU size、Connection interval、Connection Event Length、Communication Mode、LE PHY speed 等,ATT MTU size 已经设置为BLE 支持的最大值247,Connection Event 值为7.5 ms,也即一个连接周期最多只有Connection Event 时间传输数据,这个值远小于Connection interval,我们需要让Connection Event 占满Connection interval。为便于跟nordic softdevice 规格说明书中的值对比,这里设置连接参数如下:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c
#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(50, UNIT_1_25_MS)             /**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(50, UNIT_1_25_MS)             /**< Maximum acceptable connection interval (75 ms), Connection interval uses 1.25 ms units. */

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\pca10040\s132\config\sdk_config.h
#define NRF_SDH_BLE_GAP_EVENT_LENGTH 40				// The time set aside for this connection on every connection interval in 1.25 ms units.

#define NRF_SDH_BLE_GAP_DATA_LENGTH 251
#define NRF_SDH_BLE_GATT_MAX_MTU_SIZE 247
           

编译工程 --> 烧录到nRF52 DK,J-Link RTT Viewer 打印RTT Log 信息如下:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

在执行函数sd_ble_enable 时返回NRF_ERROR_NO_MEM,也即分配给softdevice 的RAM 空间不足,需要为softdevice 预留更多的空间(也即缩减application 可用RAM 空间)。我们按照RTT Log 调整RAM_Start 和RAM_Size 如下:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

重新编译工程并烧录代码,nRF Connect for mobile 使能notification 或CCCDs,J-Link RTT Viewer 打印的BLE 数据吞吐率如下:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

BLE 最大数据吞吐率已经达到697 kbps了,很接近nordic softdevice 规格说明书中的702.8 kbps(也即87.85 KB/s),多打印会儿是可以看到BLE data throughput 达到七百以上的,BLE 数据传输速率达到了softdevice 支持的最大值。

如果已知Throughput_kbps 值为702.8 kbps,ATT_MTU 值为247,CONN_INTERVAL 值为50 ms,可以通过公式反求出单个连接间隔内发送出去的数据包个数为18,也即每个数据包以send notification 模式发送出去所需的平均时间为2.78 ms(包括radio 启动和切换时间、协议栈调度时间等)。

1.3.2 LE 2M PHY 最大数据吞吐率

从nordic softdevice 规格说明书可知,还可以使用BLE 5.0 新增的LE 2M PHY 特性进一步提高数据吞吐率。链路层使用LE 2M PHY,可以在更短的时间发送完等长度的数据包,也即在一个连接间隔可以发送更多的数据包,实现更大的传输速率。

当Connection interval 与Connection event 均为50 ms,ATT_MTU size 为247,采用Send Notification 通信模式和LE 2M PHY 物理链路,可以达到的Throughput_kbps 值为1327.5 kbps,通过公式可反求出单个连接间隔内发送出去的数据包个数为34,也即每个数据包以send notification 模式发送出去所需的平均时间为1.47 ms(由于radio 启动与切换时间、协议栈调度时间基本固定,因此发送单个数据包使用LE 2M PHY 比LE 1M PHY 所需时间的一半略多)。如何启用LE 2M PHY 呢?

前面通过修改宏变量值就可以更新Data Length 和Connection Parameters,这些更新过程在链路层有相应的控制报文交互(参阅博文:Link Layer Control Protocol),对于PHY Update 也有对应的链路层控制报文交互。

上述工程ble_app_uart 代码中跟PHY Update 相关的主要代码如下:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c
/**@brief Function for handling BLE events.
 */
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
    uint32_t err_code;

    switch (p_ble_evt->header.evt_id)
    {
        ......
        case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
        {
            NRF_LOG_DEBUG("PHY update request.");
            ble_gap_phys_t const phys =
            {
                .rx_phys = BLE_GAP_PHY_AUTO,
                .tx_phys = BLE_GAP_PHY_AUTO,
            };
            err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
            APP_ERROR_CHECK(err_code);
        } break;
        ......
    }
}

// .\nRF5_SDK_17.0.2_d674dde\components\softdevice\s132\headers\ble_gap.h
/**@defgroup BLE_GAP_PHYS GAP PHYs
 * @{ */
#define BLE_GAP_PHY_AUTO                         0x00    /**< Automatic PHY selection. Refer @ref sd_ble_gap_phy_update for more information.*/
#define BLE_GAP_PHY_1MBPS                        0x01    /**< 1 Mbps PHY. */
#define BLE_GAP_PHY_2MBPS                        0x02    /**< 2 Mbps PHY. */
#define BLE_GAP_PHY_CODED                        0x04    /**< Coded PHY. */
#define BLE_GAP_PHY_NOT_SET                      0xFF    /**< PHY is not configured. */

/**@brief Supported PHYs in connections, for scanning, and for advertising. */
#define BLE_GAP_PHYS_SUPPORTED  (BLE_GAP_PHY_1MBPS | BLE_GAP_PHY_2MBPS) /**< All PHYs except @ref BLE_GAP_PHY_CODED are supported. */
           

上面的代码是处理BLE_GAP_EVT_PHY_UPDATE_REQUEST 事件的,从BLE_GAP_PHYS_SUPPORTED 可以看出nRF52 DK 是支持BLE_GAP_PHY_2MBPS 的,变量phys 的值如果设置为BLE_GAP_PHY_1MBPS 或BLE_GAP_PHY_2MBPS 则强制选择相应的PHY,上述代码phys 设置为BLE_GAP_PHY_AUTO 则会自动选择当前最合适的PHY。如果在BLE central 端请求使用LE 2M PHY,BLE peripheral 端也会更新到LE 2M PHY(前提是BLE central 端与peripheral 端均支持LE 2M PHY,且peripheral 端未强制指定PHY)。

手机端是否支持LE 2M PHY,可以从nRF connect for mobile --> Device information 界面查看“High speed(PHY 2M) supported” 项为“YES” 表示支持LE 2M PHY。nRF connect for mobile 连接到nRF52 DK 广播名NORDIC_UART 后,点击“Enable CCCDs”使能NUS Notification,点击"Set preferred PHY" Tx/Rx PHY 均选择“LE 2M(Double speed)”。PC 端J-Link RTT Viewer 打印的BLE 数据吞吐率如下:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

我们看到了很奇怪的现象,理论上切换到LE 2M PHY,BLE 数据吞吐率应该提高近一倍的,实际情况却是大幅下降,这是怎么回事呢?

我们也是在每个定时周期循环发送数据包,直到函数ble_nus_data_send 的返回值不为NRF_SUCCESS 或者函数data_throughput_timeout_handler 被更高优先级的中断抢占(协议栈softdevice 事件的优先级高于application 中断的优先级)。同样的代码LE 1M PHY 可以接近最大数据吞吐率,切换到LE 2M PHY 数据吞吐率反而下降了,我们可以合理猜测循环发送数据包的过程出问题了,也即函数ble_nus_data_send 的返回值不是NRF_SUCCESS 而过早的退出了循环。该如何验证并解决该问题呢?

这里选用的Send Notification 通信模式,server 可以连续向Client 发送多个数据包而不需要等待response 或Confirmation 报文(Client 可能来不及处理数据包而直接丢弃),因此可以达到较高的数据吞吐率:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

调用函数ble_nus_data_send,实际上是将应用层待发送数据指针传给softdevice 协议栈,放入到radio FIFO 中,当radio 将数据包成功发送出去后,softdevice 协议栈会返回BLE_GATTS_EVT_HVN_TX_COMPLETE 事件通知应用层数据包已成功发送出去。NUS 服务则会返回BLE_NUS_EVT_TX_RDY 事件通知应用层数据包已通过NUS 服务成功发送出去。

前面的问题既然猜测是由函数ble_nus_data_send 返回值非NRF_SUCCESS 而过早退出循环导致每个定时周期发送的数据包太少引起的,我们可以在每次触发BLE_NUS_EVT_TX_RDY 事件时再次循环调用函数ble_nus_data_send 发送下一个数据包。每成功发送一个数据包触发一次BLE_NUS_EVT_TX_RDY 事件,调用一次函数ble_nus_data_send,理论上应该能解决上述问题,我们添加如下代码:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c

/**@brief Function for handling the data from the Nordic UART Service.
 */
static void nus_data_handler(ble_nus_evt_t * p_evt)
{

    if (p_evt->type == BLE_NUS_EVT_RX_DATA) {
        ......
    } else if(p_evt->type == BLE_NUS_EVT_COMM_STARTED) {
        // Start data throughput timers.
        ......
    } else if (p_evt->type == BLE_NUS_EVT_TX_RDY) {
        ret_code_t err_code;
        
        do {
        	uint16_t length = BLE_NUS_MAX_DATA_LEN;
            err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
            if ((err_code != NRF_ERROR_INVALID_STATE) &&
                (err_code != NRF_ERROR_RESOURCES) &&
                (err_code != NRF_ERROR_NOT_FOUND))
            {
                APP_ERROR_CHECK(err_code);
            }
        
            if(err_code == NRF_SUCCESS)
            {
                m_data_sent_length += length;
                m_data_array[0]++;
                m_data_array[length-1]++;
            }
        } while (err_code == NRF_SUCCESS);
    }
}
           

重新编译工程并烧录代码,nRF Connect for mobile 点击“Enable CCCDs”使能NUS Notification,点击"Set preferred PHY" Tx/Rx PHY 均选择“LE 2M(Double speed)”,J-Link RTT Viewer 打印的BLE 数据吞吐率如下:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

切换到LE 2M PHY 后,BLE 数据吞吐率果然大幅提升,上图显示吞吐率可以达到1220 kbps (也即152.5 KB/s),已经比较接近nordic softdevice 支持的最大吞吐率1327.5 kbps 了,多打印会儿是可以看到BLE data throughput 达到一千三以上的,BLE 数据传输速率达到了softdevice 支持的最大值。

1.4 如何同步数据的生产与发送?

前面的代码直接对数组首尾字节自增后发送,实际应用场景中都是将断开连接期间暂时保存在本设备的数据或者sensor 实时产生的数据,在BLE 建立连接后分包发送给BLE Central 设备。如何保证数据包的有序发送呢?

我们很容易想到,可以借助FIFO 缓冲队列实现数据包的有序发送,这里使用nordic 提供的queue 库,生产出来的待发送数据有序入队,要发送的数据从队列中取用即可。

我们将上述持续发送BLE 数据包的代码修改为使用queue 的形式,首先需要将目录 .\nRF5_SDK_17.0.2_d674dde\components\libraries\queue 下的源文件和头文件路径添加进工程中,再在main.c 文件中包含"nrf_queue.h" 头文件,在main.c 中添加或修改如下代码:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c
......
#include "nrf_queue.h"
......
#define QUEUE_ELEMENT_NUMBERS               32
#define PKGS_PER_TIMER_PERIOD               8

uint8_t m_data_array[QUEUE_ELEMENT_NUMBERS][BLE_NUS_MAX_DATA_LEN] = {0};
typedef struct
{
    uint8_t * p_data;
    uint16_t length;
} m_element_t;

NRF_QUEUE_DEF(m_element_t, m_buf_queue, QUEUE_ELEMENT_NUMBERS, NRF_QUEUE_MODE_NO_OVERFLOW);
......
/**@brief Use queue to send ble data.
 */
void ble_data_send_with_queue(void)
{
    ret_code_t err_code;
    m_element_t data_item;
    uint16_t length = BLE_NUS_MAX_DATA_LEN;

    while(!nrf_queue_is_empty(&m_buf_queue))
    {
        err_code = nrf_queue_pop(&m_buf_queue, &data_item);
        APP_ERROR_CHECK(err_code);

        length = MIN(length, data_item.length);
        err_code = ble_nus_data_send(&m_nus, data_item.p_data, &length, m_conn_handle);
        if ((err_code != NRF_ERROR_INVALID_STATE) &&
            (err_code != NRF_ERROR_RESOURCES) &&
            (err_code != NRF_ERROR_NOT_FOUND))
        {
            APP_ERROR_CHECK(err_code);
        }
        if(err_code == NRF_SUCCESS)
            m_data_sent_length += length;
        else
            break;
    }
}

/**@brief Data generation timer timeout handler function.
 */
static void data_throughput_timeout_handler(void * p_context)
{
    UNUSED_PARAMETER(p_context);
    
    static uint32_t timeout_count = 0;
    ret_code_t err_code;

    static uint8_t value = 0;
    m_element_t data_item;
    uint16_t length = BLE_NUS_MAX_DATA_LEN;
    uint8_t pkgs = PKGS_PER_TIMER_PERIOD;
    
    timeout_count++;

    while (!nrf_queue_is_full(&m_buf_queue) && pkgs--)
    {
        m_data_array[value % QUEUE_ELEMENT_NUMBERS][0] = value;
        m_data_array[value % QUEUE_ELEMENT_NUMBERS][length-1] = value;

        data_item.p_data = &m_data_array[value % QUEUE_ELEMENT_NUMBERS][0];
        data_item.length = length;

        err_code = nrf_queue_push(&m_buf_queue, &data_item);
        APP_ERROR_CHECK(err_code);

        value++;
    }

    ble_data_send_with_queue();

    // Timer interval 5 ms, when the timer reaches 1 second 
    if(timeout_count == 200)
    {
        // Send m_data_sent_length bytes of data within 1 second, which is equal to m_data_sent_length * 8 / 1024 kilobits of data
        NRF_LOG_INFO("****** BLE data throughput: %d kbps ******", m_data_sent_length >> 7);
        m_data_sent_length = 0;
        timeout_count = 0;
        value = 0;
    }
}
......
/**@brief Function for handling the data from the Nordic UART Service.
 */
static void nus_data_handler(ble_nus_evt_t * p_evt)
{

    if (p_evt->type == BLE_NUS_EVT_RX_DATA) {
        ......
    } else if(p_evt->type == BLE_NUS_EVT_COMM_STARTED) {
        ......
    } else if(p_evt->type == BLE_NUS_EVT_TX_RDY) {
        ble_data_send_with_queue();
    }
}
           

使用queue 同步数据的产生与发送,队列未满时将生产的数据入队,队列非空时从队列中取出下一个元素通过调用函数ble_nus_data_send 将其发送出去。

编译工程报错,提示nrf_queue 函数未定义,我们需要在sdk_config.h 文件中启用NRF_QUEUE 模块相关的宏变量如下:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\pca10040\s132\config\sdk_config.h

#define NRF_QUEUE_ENABLED 1
           

重新编译工程并烧录代码,nRF Connect for mobile 点击“Enable CCCDs”使能NUS Notification,点击"Set preferred PHY" Tx/Rx PHY 均选择“LE 2M(Double speed)”,J-Link RTT Viewer 打印的BLE 数据吞吐率如下:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

使用queue 同步数据产生与发送,PHY 使用LE 1M 时最大数据吞吐率为726 kbps,PHY 切换到LE 2M 时最大数据吞吐率为1334 kbps,均略高于nordic softdevice 支持的最大值,达到了我们预期的效果。

二、如何设置广播连接参数以满足低功耗需求?

我们开发的BLE peripheral 多数都有低功耗要求,由电池供电,如何满足电池续航需求呢?

Nordic 提供了nRF 芯片理论功耗计算网页Online Power Profiler for BLE,我们可以在该页面修改参数,看理论功耗是否满足我们的设计要求。如果已经试产出产品了,也可以借助Power Profiler Kit 或Power Profiler Kit II 测量产品的真实功耗。

假设我们使用CR2032 纽扣电池(额定容量为220 mAh,额定电压3.0 V)供电,使用寿命一年,每天平均连接两个小时,其余时间处于idle 空闲状态,我们该如何设置广播参数与连接参数,以满足我们的设计续航要求呢?

假设我们选用nRF52832 芯片,Idle current 为2 uA,全年待机共消耗电量 = 365 * 24 * 2 uAh = 17.52 mAh。在产品寿命期间,BLE 连接通信时间约730 小时,可供BLE 连接消耗的电量约200 mAh,BLE 连接的平均功耗为274 uA。电池并不仅仅为BLE 通信供电,还为必要的传感器与外设工作供电,考虑到传感器与外设工作的时间比BLE 连接通信的时间更长,我们假设仅电池电量的1/3 供BLE 广播连接通信使用,其余2/3 为传感器和芯片外设工作供电,因此BLE 广播连接通信的平均功耗应控制在90 uA 左右。我们在Online Power Profiler for BLE 页面配置如下参数:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

芯片选择nRF52832、CR2032 的额定电压为3.0 V、穿戴设备Radio Tx Power 选择 0 dBm 可以满足传输距离需求(可根据BLE 在空气中的路径损耗公式,结合通讯距离要求选择合适的Tx Power)。

DC/DC regulator 是一种效率很高的稳压器,原理是DC->AC->DC,既可以升压也可以降压。与之相比,还有一种低成本的LDO (Low Dropout regulator) 稳压器,效率比DC/DC 低些,只能降压使用且对输入输出电压差有限制。如果想达到更低的功耗,可以选择DC/DC,如果想进一步降低成本,可以选择LDO。这里我们选择更高效率的DC/DC regulator。

BLE 芯片通常需要两个时钟信号,比如nRF52 DK 上高频晶振频率为32 MHz、低频晶振频率为32.768 KHz,高频晶振驱动MCU 和高速外设工作,低频晶振可以大幅降低芯片的待机功耗(idle 或sleep 状态耗电的高频时钟关闭,仅保留低频时钟方便计时和唤醒)。高频时钟信号都需要外部晶振提供,低频时钟信号既可以外部晶振提供也可以使用MCU 内部的RC 振荡器获得。如果使用MCU 内部的RC 振荡器作为低频时钟则需要定期对其进行校准,需要大概 1.0 uA 的校准电流,且时钟精度略低些(较低的时钟精度也会增加BLE 通讯功耗)。

配置低频时钟信号的代码如下(工程ble_app_uart 默认选择的外部晶振作为低频时钟信号,若想选择MCU 内部振荡器作为低频时钟,修改sdk_config.h 中如下的四个宏变量值即可,本文选择默认的外部晶振):

// .\nRF5_SDK_17.0.2_d674dde\components\softdevice\common\nrf_sdh.c

/**@brief Function for requesting to enable the SoftDevice, is called in the function ble_stack_init.
 */
ret_code_t nrf_sdh_enable_request(void)
{
    ......
    nrf_clock_lf_cfg_t const clock_lf_cfg =
    {
        .source       = NRF_SDH_CLOCK_LF_SRC,
        .rc_ctiv      = NRF_SDH_CLOCK_LF_RC_CTIV,
        .rc_temp_ctiv = NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,
        .accuracy     = NRF_SDH_CLOCK_LF_ACCURACY
    };
    ......
}

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\pca10040\s132\config\sdk_config.h
......
// <h> Clock - SoftDevice clock configuration
//==========================================================
// <o> NRF_SDH_CLOCK_LF_SRC  - SoftDevice clock source.
 
// <0=> NRF_CLOCK_LF_SRC_RC 
// <1=> NRF_CLOCK_LF_SRC_XTAL 
// <2=> NRF_CLOCK_LF_SRC_SYNTH 

#ifndef NRF_SDH_CLOCK_LF_SRC
#define NRF_SDH_CLOCK_LF_SRC 1
#endif

// <o> NRF_SDH_CLOCK_LF_RC_CTIV - SoftDevice calibration timer interval. 
#ifndef NRF_SDH_CLOCK_LF_RC_CTIV
#define NRF_SDH_CLOCK_LF_RC_CTIV 0
#endif

// <o> NRF_SDH_CLOCK_LF_RC_TEMP_CTIV - SoftDevice calibration timer interval under constant temperature. 
// <i> How often (in number of calibration intervals) the RC oscillator shall be calibrated
// <i>  if the temperature has not changed.

#ifndef NRF_SDH_CLOCK_LF_RC_TEMP_CTIV
#define NRF_SDH_CLOCK_LF_RC_TEMP_CTIV 0
#endif

// <o> NRF_SDH_CLOCK_LF_ACCURACY  - External clock accuracy used in the LL to compute timing.
 
// <0=> NRF_CLOCK_LF_ACCURACY_250_PPM 
// <1=> NRF_CLOCK_LF_ACCURACY_500_PPM 
// <2=> NRF_CLOCK_LF_ACCURACY_150_PPM 
// <3=> NRF_CLOCK_LF_ACCURACY_100_PPM 
// <4=> NRF_CLOCK_LF_ACCURACY_75_PPM 
// <5=> NRF_CLOCK_LF_ACCURACY_50_PPM 
// <6=> NRF_CLOCK_LF_ACCURACY_30_PPM 
// <7=> NRF_CLOCK_LF_ACCURACY_20_PPM 
// <8=> NRF_CLOCK_LF_ACCURACY_10_PPM 
// <9=> NRF_CLOCK_LF_ACCURACY_5_PPM 
// <10=> NRF_CLOCK_LF_ACCURACY_2_PPM 
// <11=> NRF_CLOCK_LF_ACCURACY_1_PPM 

#ifndef NRF_SDH_CLOCK_LF_ACCURACY
#define NRF_SDH_CLOCK_LF_ACCURACY 7
#endif
           

BLE 广播通信阶段作为Advertising(connectable) role,假设TX payload 为31 bytes,当设置Advertising interval 为160 ms 时,Total average current 为87 uA,可满足我们的功耗需求,我们可以设置如下的宏变量(将Advertising interval 设置为160 ms):

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c
......
#define APP_ADV_INTERVAL                256                                          /**< The advertising interval (in units of 0.625 ms. This value corresponds to 160 ms). */
#define APP_ADV_DURATION                18000                                       /**< The advertising duration (180 seconds) in units of 10 milliseconds. */
......
           

BLE 连接通信阶段作为Connection(peripheral) role,启用Data Packet Length Extension 和Connection Event Length Extension,假设TX payload per event 和RX payload per event 均为251 bytes,选择LE 1M PHY,我们在Online Power Profiler for BLE 页面配置如下参数:

如何实现BLE 最大数据吞吐率并满足设计功耗要求?一、如何提高BLE 数据传输速率?二、如何设置广播连接参数以满足低功耗需求?更多文章:

我们配置较小的Connection interval 可以在BLE peripheral 有数据传输需求时及时通知BLE central,配置较大的Slave latency 可以让BLE peripheral 在没有数据传输需求时跳过一定的连接事件以降低功耗。我们配置Connection interval 为25 ms、Slave latency 为 14 时,Total average current 为89 uA,可满足我们的功耗需求,我们可以设置如下的宏变量:

// .\nRF5_SDK_17.0.2_d674dde\examples\ble_peripheral\ble_app_uart\main.c
......
#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(20, UNIT_1_25_MS)             /**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(30, UNIT_1_25_MS)             /**< Maximum acceptable connection interval (30 ms), Connection interval uses 1.25 ms units. */
#define SLAVE_LATENCY                   14                                           /**< Slave latency. */
#define CONN_SUP_TIMEOUT                MSEC_TO_UNITS(4000, UNIT_10_MS)             /**< Connection supervisory timeout (4 seconds), Supervision Timeout uses 10 ms units. */
......
           

本工程源码下载地址:https://github.com/StreamAI/Nordic_nRF5_Project/tree/main/examples/ble_peripheral\ble_app_uart。

更多文章:

  • 《如何抓包分析BLE 空口报文(GAP + GATT + LESC procedure)?》
  • 《如何实现扫码连接BLE 设备的功能?》
  • 《Nordic_nRF5_Project》
  • 《Nordic nRF5 SDK documentation》
  • 《BLE 技术(三)— Link Layer Packet format 》
  • 《BLE 技术(四)— Link Layer communication protocol 》
  • 《BLE 技术(五)— Generic Access Profile》
  • 《BLE 技术(六)— GATT Profile + ATT protocol》
  • 《Bluetooth Core Specification_v5.2》

继续阅读