【Nordic】如何极致实现Nordic 蓝牙性能

news/2024/11/24 8:33:24/

在很多应用场合,BLE只是作为一个数据透传模块,即将设备端数据上传给手机,同时接收手机端下发的数据。本文将和大家一起,一步一步演示如何开发一个BLE透传应用程序。按照本文的说明,大家可以很快就实现一个BLE透传应用,BLE透传应用已经是BLE应用中比较复杂的一种,一旦大家掌握了BLE透传应用,其他BLE应用开发就更不在话下了。

本文还将手把手教大家如何提高BLE数据传输速度(蓝牙4.2的理论吞吐率大概为100kB/s,而我们实际达到了85kB/s,已经非常接近理论值)。最后,我们将告诉大家如何使用安卓版nRF Connect来对你的BLE设备进行压力测试,以测试设备的稳定性和可靠性。文章的最后还会告诉大家如何找到安卓和iOS手机BLE app开发参考代码。

°
这是一篇纯实践的文章:

如果你对低功耗蓝牙的一些基本概念还不是很懂的话,那么建议你先看一下这篇文章:【Nordic博文分享系列】低功耗蓝牙ATT/GATT/Profile/Service/Characteristic规格解读,有了BLE这些基本概念后,再去看下面的内容,就容易多了。

代码链接

文中涉及的所有代码都可以在百度网盘找到,地址如下所示:

链接:Enfi插件https://pan.baidu.com/s/1FKTfY3Q_zBVvviO7KC7Gyg#list/path=%2Fblog

密码:y8fb

下载“ble_app_uart_hs_SDK16_0_0.rar”,解压缩到SDK16.0.0如下目录下:nRF5SDK160098a08e2\examples\ble_peripheral,即可成功编译和运行。

注意:虽然本文代码是基于SDK16.0.0开发的,但所有新增代码都可以直接拷贝到/DK15.0.0/SDK15.2.0/SDK15.3.0的ble_app_uart例子,并可以成功编译和运行,他们是完全兼容的。

1、开发准备

Nordic nRF52或者nRF51开发板1块。请参考“Nordic nRF51/nRF52开发流程说明”,购买相应开发板(DK)。

开发环境搭建。简述如下(详细说明请参考“Nordic nRF51/nRF52开发环境搭建”):

安装Keil5 MDK

安装SDK。如果你使用的是nRF52开发板,请安装最新版nRF5 SDK(现在最新版本是:nRF5 SDK v16.0.0),下载链接:https://www.nordicsemi.com/Software-and-Tools/Software/nRF5-SDK/Download#infotabs。如果你手上是nRF51开发板,请选择nRF5 SDK v12.3.0(nRF51最高SDK版本只能到12.3.0,后续SDK就不再支持nRF51了)

安装ARM CMSIS4.5.0,下载链接:https://github.com/ARM-software/CMSIS/releases/download/v4.5.0/ARM.CMSIS.4.5.0.pack。

安装Keil5 Device Family Pack,下载链接:https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-MDK/Download#infotabs。

安装nRF Command Line Tools,下载链接(Windows版):https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Command-Line-Tools/Download#infotabs。

安装安卓版或者iOS版nRF connect。iOS版nRF connect请到苹果app store下载。安卓版nRF connect可以到Nordic Github官网上下载,下载链接为:

https://github.com/NordicSemiconductor/Android-nRF-Connect/releases

安装PC版nRF connect。PC版nRF connect下载链接(Windows版):

https://www.nordicsemi.com/eng/nordic/download_resource/58847/15/21277021/108233

注:如果你使用的是Linux系统/Mac系统,或者你使用的不是Keil5-MDK,请参考“Nordic nRF51/nRF52开发环境搭建”来搭建你的开发环境。

2、Nordic ble_app_uart应用程序

°
Nordic SDK已经提供了一个直接就可以编译和运行的数据透传应用程序:ble_app_uart,Nordic将BLE透传服务称为Nordic UART Service(NUS),所以在Nordic SDK中,NUS就是BLE透传服务。请按照如下步骤运行SDK自带的ble_app_uart程序:

确认自己的芯片型号或者开发板。如果采用Nordic官方开发板的话,芯片型号和开发板编号对应关系如下:

nRF52832和nRF52810对应开发板编号为PCA10040。虽然52832和52810共用同一块开发板,但是他们在SDK中的项目编号是不一样的,52832对应PCA10040目录,52810对应PCA10040e目录,由于52810和52832 PIN to PIN兼容,软件也是完全兼容的,因此SDK很多项目只有PCA10040的目录,而没有PCA10040e目录,此时需要你自己来建立PCA10040e对应的目录和工程,具体说明可参考:https://infocenter.nordicsemi.com/topic/sdk_nrf5_v16.0.0/nrf52810_user_guide.html?cp=7_1_5_0。

nRF52840和nRF52811对应开发板编号为PCA10056。与832/810相似,52840对应的工程目录为PCA10056,而52811对应的工程目录为PCA10056e。如何建立一个PCA10056e的项目工程可以参考:https://infocenter.nordicsemi.com/topic/sdk_nrf5_v16.0.0/nrf52811_user_guide.html?cp=7_1_5_1。

nRF52840 dongle编号为PCA10059。PCA10059是一个可以直接插入电脑的小dongle,它也是使用52840芯片,但本身不带JLink接口芯片。

nRF52833对应开发板编号为PCA10100

nRF51系列对应开发板编号为PCA10028

这里我会以nRF52832开发板PCA10040为例来阐述整个开发过程,其他开发板与之类似,大家可以举一反三来开始自己的开发之旅

将开发板与PC机通过USB线相连,同时打开开发板电源(将左下角的拨位开关打到“ON”位置)

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

打开SDK中的ble_app_uart程序。如果是52832开发板,请打开:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral\ble_app_uart\pca10040\s132\arm5_no_packs;如果是51822开发板,请打开:nRF5_SDK_12.3.0_d7731ad\examples\ble_peripheral\ble_app_uart\pca10028\s130\arm5_no_packs

后续将以52832开发板为例来阐述,51822等其他芯片与之类似就不再阐述了。

注:Nordic SDK例程目录结构为:SDK版本/ examples /蓝牙角色/例子名称/开发板型号/协议栈型号/工具链类型/具体工程,比如下面例子:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

Nordic每一个例子都支持5种工具链:Keil5/Keil4/IAR/GCC/SES,如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
编译程序

如果你已经按照之前的说明配置好了开发环境,那么这里编译是不会报任何错的。(如果你遇到了编译错误,请重新按照前面说明去搭建你的开发环境,不要怀疑SDK例子代码有问题哦)

°
下载程序

程序下载包括2步:一先下载softdevice,二再下载应用。Softdevice是Nordic蓝牙协议栈的名称,整个开发过程中只需下载一次。应用就是我们这里的ble_app_uart程序。如果你的开发板已经下载了其他代码,那么最好先把开发板全擦一次,然后再下载softdevice和应用。

芯片全擦(可选)。你可以使用nRF connect桌面版或者nrfjprog,二选其一来执行擦除操作。

打开桌面版nRF Connect,选择启动“Programmer”应用,由于驱动之前已经安装好了,设备可以立即识别成功,执行“Erase all”操作,以擦除芯片原始内容

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

使用nrfjprog执行全擦操作

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

蓝牙协议栈下载(整个开发周期只需下载一次)。在Keil ‘select target’下拉列表中,默认选择的是Keil工程对应的Target,即‘nrf52832_xxaa’。我们还可以选择另一个target ‘flash_s132_nrf52_7.0.1_softdevice’,即softdevice对应的target,然后点击“下载download”(不需要编译哦!),此时会把softdevice下载到开发板中。

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

注:你也可以通过桌面版nRF connect的programmer或者nrfjprog来下载softdevice,下载的时候,找到相应的softdevice hex文件,然后直接下载进去即可。

应用下载。重新选择Target:‘nrf52832_xxaa’,点击“下载Download”,此时会把ble_app_uart应用程序下载到开发板中。此时开发板的LED1闪烁,表示程序运行正常

°
链接手机

打开手机蓝牙和手机版nRF connect。在nRF connect中,你将看到一个广播设备:Nordic_UART,这个就是开发板的广播名字。点击“CONNECT”,手机将与设备建立连接,并开始服务发现过程,连接成功后,LED1熄灭,LED2点亮,两个关键界面如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

上图的Nordic UART Service(NUS)就是我们的数据透传服务, NUS具体包括两个characteristic:TX和RX,由于NUS是由设备提供的,所以TX表示设备发送数据给手机,RX表示设备接收手机发过来的数据。

°
测试NUS服务

ble_app_uart使用串口与上位机交互,选择一款串口助手软件,比如Putty,打开该串口软件,并做如下设置:

Baud rate: 115.200kbps

8data bits

1 stop bit

No parity

HW flow control: None

复位开发板,你会发现串口助手会打印如下信息:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

重新将开发板连上手机,然后点击右上角的“Enable CCCDs”以使能notification,如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
设备接收数据

点击RX characteristic旁边的向上箭头,通过手机蓝牙往设备发送:12345678,如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

此时设备通过串口打印出刚才接收到的数据,如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
设备发送数据

在串口助手中输入“abcdefgh”并输入“\n”(注:在Putty中,先按“CTRL”再按“J”就会发出“\n”换行符)作为结束符,设备将把串口收到的数据通过蓝牙发送给手机,手机的TX characteristic将显示上述字符串,如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

注:如果你的串口助手发不出“\n”换行符,那么你需要最少输入MTU-3个字符,设备才会把收到的全部字符通过蓝牙发出去。

通过上面的测试,大家可以发现Nordic SDK已经把蓝牙数据透传服务做好了,大家可以直接拿过来使用,下面将对其工作原理进行阐述,最后在Nordic蓝牙透传例子ble_app_uart上进行二次开发,以增加一些其他有用的功能。

3、设备端固件代码一览

°
现在我们一起来看一下ble_app_uart的源代码,看看它是怎么工作起来的。首先我们来看main函数:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

如上所述,ble_stack_init用于初始化配置和使能蓝牙协议栈,其代码如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

其中,nrf_sdh_enable_request需要选择蓝牙协议栈的低频时钟(由于蓝牙协议栈的高频时钟必须为外部32M晶振,所以高频时钟无需配置;而低频时钟可以选择为内部32K RC或者外部32K晶振,所以低频时钟需要人工配置),因此如下宏需要根据实际情况进行调整:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
测试NUS服务通过sdk_config.h文件可以看到,默认是选择外部32K晶振作为低频时钟的,如果你想选择内部32KRC作为低频时钟,那么需要做如下修改:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

NRF_SDH_BLE_PERIPHERAL_LINK_COUNT //作为从模式的连接同时能有几个

NRF_SDH_BLE_CENTRAL_LINK_COUNT //作为主模式的连接同时能有几个

NRF_SDH_BLE_TOTAL_LINK_COUNT //一共同时可以支持多少个连接,NRF_SDH_BLE_TOTAL_LINK_COUNT= NRF_SDH_BLE_PERIPHERAL_LINK_COUNT+ NRF_SDH_BLE_CENTRAL_LINK_COUNT

NRF_SDH_BLE_GATT_MAX_MTU_SIZE//MTU为多大

NRF_SDH_BLE_GAP_DATA_LENGTH//链路层数据包的最大长度,它的值要大于NRF_SDH_BLE_GATT_MAX_MTU_SIZE

NRF_SDH_BLE_GAP_EVENT_LENGTH//一次连接间隔中分配给某个connection的物理层时间,这个时间必须大于等于NRF_SDH_BLE_GAP_DATA_LENGTH最长的包占用的时间,否则协议栈初始化会报错。如果这个时间是NRF_SDH_BLE_GAP_DATA_LENGTH的两倍甚至更多,那么在一个连接间隔中就可以连发两个甚至多个notify或者write command命令

NRF_SDH_BLE_VS_UUID_COUNT //用户自定义的base UUID有几个

NRF_SDH_BLE_SERVICE_CHANGED //要不要包含service change characteristic

NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE //Attribute table总共占多少蓝牙协议栈RAM空间,这个宏的取值是需要用户不断去试错的,每当你添加了或者删除了BLE service/characteristic,那么attribute table占用的RAM的空间就会变。注意:NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE是蓝牙协议栈占用的总RAM空间的一部分,所以当NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE变大时,softdevice占用的总RAM空间也会变大。Softdevice占用的总RAM空间是在编译的时候静态设置的,如下所示,本应用程序分配了0x20000000~0x20002AD8 RAM空间给协议栈(所有的nRF5芯片的RAM物理起始地址都是0x20000000),即softdevice总共占用了0x2AD8的RAM空间。而0x20002AD8往上直到RAM最高物理地址就是留给应用程序使用的,本程序分配了0xD528 RAM空间给应用程序。0x2AD8 + 0xD528 = 0x10000 = 64kB,正好是nRF52832的RAM总空间大小。

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
nrf_sdh_ble_enable真正使能BLE功能

它的参数ram_start既是一个输入参数又是一个输出参数,作为输入参数,系统自动会把上面的RAM起始地址(0x20002AD8)传入,同时nrf_sdh_ble_enable会把softdevice当前配置情况下,它实际需要占用的RAM空间通过ram_start返回,如果这个返回值不等于输入值,那么用户需要把上图的IRAM1起始地址修改成它的返回值。

这里面又分两种情况:

如果返回值大于输入值,那么必须调整IRAM1起始地址,否则协议栈初始化失败;

如果返回值小于输入值,这种情况也推荐去调整IRAM1起始地址,

当然如果你不调整IRAM1起始地址也没有关系,因为这种情况表示协议栈实际需要的RAM的空间小于你分配给它的RAM空间,所以协议栈初始化不会出问题,这种情况只是会浪费一些RAM空间而已。

请注意,任何协议栈配置宏的修改,包括NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE,都有可能导致协议栈占用的RAM空间发生变化,进而需要去调整IRAM1起始地址。

一种常见的场景是,用户添加了某个service,从而需要先增大NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的值,然后通过nrf_sdh_ble_enable得到ram_start实际起始地址,最后把ram_start的值赋给IRAM1起始地址。

°
NRF_SDH_BLE_OBSERVER

NRF_SDH_BLE_OBSERVER用来为本地文件(此处为main.c文件)注册一个BLE回调函数(此处为ble_evt_handler),NRF_SDH_BLE_OBSERVER这个宏执行成功后,所有的BLE事件都会被ble_evt_handler捕获。

进入ble_evt_handler,你会发现BLE有上百个回调事件,你不需要每个都处理,你只需要处理你关心的事件即可,比如连接成功事件BLE_GAP_EVT_CONNECTED或者连接断开事件BLE_GAP_EVT_DISCONNECTED。

NRF_SDH_BLE_OBSERVER有一个很大的好处:某个文件如果需要捕获BLE事件,那么它只需在本文件中某处(可以在函数内也可以在函数外)调用NRF_SDH_BLE_OBSERVER这个宏去注册一个回调函数即可,而不再需要在其它文件中去注册这个回调函数,将模块的耦合性降到最低,符合模块化编程思想。

°
gap_params_init用来修改广播名字和连接间隔的

gatt_init用来修改底层数据包长度的。advertising_init用来修改广播包内容,广播间隔以及广播超时时间。conn_params_init用来请求更新连接间隔的。

°
下面我们来重点讲一下services_init

services_init用来添加服务和characteristic,“【Nordic博文分享系列】低功耗蓝牙ATT/GATT/Profile/Service/Characteristic规格解读”讲了那么多的概念和理论,现在我们就来看看services_init是如何做到跟理论一致的。services_init通过ble_nus_init添加了一个蓝牙数据透传服务:NUS,那ble_nus_init是怎么将NUS服务添加成功的呢?查看ble_nus_init函数体,你会发现它是分三步来做的:

添加base UUID。如果是蓝牙标准UUID,这步可以省略。由于NUS不是蓝牙联盟定义的,所以需要调用sd_ble_uuid_vs_add以添加一个供应商自定义的UUID。

添加服务本身。直接调用sd_ble_gatts_service_add就可以完成。

添加服务下面的characteristics。服务的characteristic现在可以通过characteristic_add直接添加完成(characteristic_add最终是通过调用sd_ble_gatts_characteristic_add实现自己目的的)。以NUS的TX characteristic添加为例,其对应代码为:

characteristic_add(p_nus->service_handle,&add_char_params, &p_nus->tx_handles)

其中,p_nus->service_handle表示该characteristic属于那个service,p_nus->tx_handles是输出参数,由协议栈返回,以后访问该characteristic都是通过这些句柄来完成,p_nus->tx_handles是一个结构体,它包含如下成员变量:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
characteristic核心attribute是value attribute

add_char_params (类型为ble_add_char_params_t)是对characteristic的参数进行赋值,如“【Nordic博文分享系列】低功耗蓝牙ATT_GATT_Profile_Service_Characteristic规格解读”所述,characteristic包含多个attribute,每个attribute都有自己的value/handle/uuid/permission,所以ble_add_char_params_t这个结构体设计的比较复杂。

这里需要大家明白的是,characteristic核心attribute是value attribute,所以当我们讲characteristic的时候,其实隐含是在说value attribute。换句话说,我们定义characteristic的参数,其实是在定义value的参数,比如我们定义characteristic的访问权限,其实就是指value的访问权限。add_char_params参数解读如下

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

TX characteristic只需赋值上述参数即可,但对某些其他characteristic,它需要赋值的参数会更多,这里再罗列一下ble_add_char_params_t这个结构体其他一些关键参数:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

is_defered_read/ is_defered_write,即authorize read/write,即在read/write characteristic value之前,先进入用户回调函数,由用户回调函数决定这个read/write操作是否允许,以及value最终为多少。

is_value_user,默认情况下所有attribute/characteristic是存放在协议栈专用RAM中(即上述的IRAM1起始地址以下地方),这种做法的好处是所有attribute由协议栈自动管理,用户无需操心。但有时候,用户想自己控制某个characteristic,那么这个时候就可以把is_value_user设为1,将其放在应用程序RAM空间。is_value_user和is_defered_read配合使用,可以达到一些意想不到效果。

这里需要特别提醒大家的是,虽然Nordic API结构体参数设计得很复杂,但是大部分成员变量都直接采用0作为它的默认值,换句话说,大家只需对自己感兴趣的变量进行赋值即可,其他的变量可以直接采用默认值,因此大家经常会看到如下操作场合,即先用memset将该结构体变量初始化为0,让所有成员变量都采用默认值,然后再对某些需要修改的成员变量进行二次赋值。大家一定不要忘了将结构体变量清零这一步操作!

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

°
ble_nus_init同时注册了nus_data_handler回调函数

当设备收到手机发过来的数据时,就会触发nus_data_handler,用户可以在nus_data_handler中对接收到的数据进行处理,本例程中nus_data_handler直接将ble收到的数据通过uart口转发出去。如果用户需要发送数据给手机,在连接成功和notify使能的情况下,直接调用ble_nus_data_send即可,而ble_nus_data_send又是通过调用协议栈API:sd_ble_gatts_hvx来实现数据发送功能的。

那么什么时候需要发送数据给手机?本例程的做法是,当串口有数据过来并满足如下条件时调用ble_nus_data_send:

即遇到换行符或者字符数达到MTU,设备才会把串口收到的数据发给手机,这个测试的时候请注意一下。

main函数最后将调用API让协议栈跑起来,如果你的设备是一个从设备(peripheral),那么请调用ble_advertising_start,ble_advertising_start将开启可连接的广播,从而让你的设备连接成功之后成为从设备。如果你的设备是一个主设备(central),那么请调用sd_ble_gap_scan_start,sd_ble_gap_scan_start将开启设备的扫描功能,从而让你的设备连接成功之后变为主设备。

最后我们来看main循环,它只有一个函数:idle_state_handle,idle_state_handle先把需要打印的日志打印完,然后让系统进入idle状态(Nordic SoC spec称其为System ON状态),一旦有协议栈事件或者中断事件发生,系统将唤醒,以处理相关事件回调函数,然后再执行一遍idle_state_handle。

注意:在idle状态下,蓝牙连接或者广播可以正常进行而不受影响,蓝牙连接或者广播都是周期性的,在一个周期中,蓝牙连接或者广播只持续很短一段时间(几百微妙到几毫秒,与数据包长度有关),CPU或者系统只会在这段时间内工作,其余时间系统都是处于idle状态的,比如广播间隔200ms,CPU或者系统不是持续工作200ms,他们其实只工作几百微妙,大部分时间(199ms多)系统都是处于idle状态的,这就是为什么广播或者连接状态系统平均电流很低的原因。如下为广播间隔200ms时,系统的实时电流波形:

如下为200ms连接间隔时对应的实时电流波形:

4、定制你的BLE数据透传应用程序

°
在ble_app_uart基础上加入一些定制功能

我们现在在ble_app_uart基础上加入一些定制功能。很多BLE应用场合,都需要快速地把大量数据从设备上传给手机,我们现在模拟这种应用场景,看一下如何修改原始的ble_app_uart以达到我们的目的。

这里假设有一个timer,这个timer每7ms发送一次数据,以模拟传感器等每7ms生成一次数据情况,我们现在来看看如何让这些数据快速发出去并且不发生丢包。这个Timer的ID设为m_timer_speed,它的回调函数为:throughput_timer_handler。原始数据假设放在数组m_data_array,数组的长度正好等于MTU长度的倍数。

也就是说,每隔7ms,进入一次throughput_timer_handler,在这个handler中需要把m_data_array中的数据全部正确发出去,这个就是我们的需求。假设m_data_array数据长度为420字节(m_data_array的长度依情况而定,这里只是一个假设),那么此时就需要每7ms发送420字节数据,相当于420B/7ms = 60kB/s。

蓝牙传输速度跟得上吗?怎么做可以满足这个需求呢?在讲解参考代码之前,我们先看一些反例或者误区,这样或许更能帮助大家去理解相关工作机理。

4.1、一些使用上的误区:

大家首先想到的做法就是在throughput_timer_handler直接调用 ble_nus_data_send,如下:

°
这种做法首先存在如下两个大问题:

没有检测返回值err_code。实际上,在这种情况下调用ble_nus_data_send,ble_nus_data_send经常返回NRF_ERROR_RESOURCES,只要err_code不是NRF_SUCCESS,就意味着该数据没有成功放入射频FIFO中,从而出现所谓“丢包”现象。不是包在空中丢了,而是包没有正确放入射频FIFO中,这个就是丢包的原因。

length变量没有进行最大值限制。ble_nus_data_send不能发送任意长度的数据包,它只能发送小于等于MTU长度的数据包。

如果我们不对返回值进行检测,同时把length设为MTU,然后每调用一次ble_nus_data_send,执行一次m_len_sent += length,即代码如下所示(代码片段1):

我们可以看到如下日志:

由上图可知,数据上传吞吐率达到了34.8kB/s,看起来不错,其实这个吞吐率是假的,因为中间丢了很多包,但计算吞吐率的时候把丢的包也算进去了。如下图手机端nRF Connect日志所示,0x6E之后应该为0x6F,但实际发送的数据包编号为0x83,丢包非常严重。

°
防止所谓的“丢包”

为了防止所谓的“丢包”(前面也提过,这里的丢包不是数据包在空中丢掉了,而是数据包没有安全送到协议栈的buffer中,从而导致丢包),我们加上如下if语句(代码片段2),只有ble_nus_data_send返回正确时,才认为数据包正确发送,然后才能算入到最后的数据吞吐率中。

m_data_array只有在返回成功时才自增(自增表示新数据有效),所以新数据生成速度会比较慢,从而导致数据吞吐率下降。通过查看nRF connect日志,你会发现此时不会发生丢包了,但吞吐率直接降到了4kB/s左右。/之前我们也讲过,为了提高吞吐率,我们可以在一个连接间隔中发多个包。如何可以做到一个连接间隔中发送多个包?除了协议栈需要做一些额外的配置,代码层面可以这么做(代码片段3):

测试下来,你会发现上面两份代码(代码片段2和代码片段3)测试结果是一样的。那么问题到底出在哪?下面进行解读。

4.2、 参考代码解读:

欲达到最大的数据吞吐率,必须保证蓝牙带宽尽可能被利用,换句话说,必须保证蓝牙包尽可能占满整个时间轴,如下所示:

反之,如果连接间隔很长,而且一个间隔只发一个包,那么哪怕这个包包长为251字节,吞吐率也不可能高,如下所示:

粗略来说,

蓝牙数据传输吞吐率 = 一个连接间隔传输的数据 / 连接间隔 = 一个数据包长度 * 包数 / 连接间隔

比如连接间隔设为60ms,一个间隔只传一个244字节的数据包,此时吞吐率为:244B/60ms = 4.1 kB/s,这就是4.1节数据传输速率上不去的根本原因。

实际情况比上面公式复杂得多,但是我们做理论分析时可以以这个公式作为参考。从上面公式可知,欲提高数据吞吐率,可以从数据包长度,每个连接间隔发送的总包数,以及连接间隔等三个方面入手,这里需要提醒大家的是,不是包长越长越好,也不是间隔越短越好,必须将三者统一起来一起考虑。

如果发送短包时能保证一个间隔发送很多短包,那么此时短包有可能比长包速率还要快;如果连接间隔大时能保证一个间隔发送很多的数据包,那么此时大的连接间隔速率有可能比小的连接间隔还要快。

针对不同的手机,不同的蓝牙设备,大家一定要统筹好这三个参数,让他们整体达到一个最优值,从而得到你的应用场景下的最高吞吐率。下面看看我们的参考代码如何调整这三个参数的

°
一个数据包的长度取决于下面两个参数

NRF_SDH_BLE_GAP_DATA_LENGTH最大值为251,这里MTU设为247,也就相当于把数据包应用数据最大长度设为247-3=244字节

°
连接间隔由下面两个参数建议:

上面把最小连接间隔和最大连接间隔都设为30ms,请注意最终连接间隔设为多少只能由蓝牙主设备决定,蓝牙从设备只有建议权。由于我们的设备是一个从设备,所以上面就是对主机提出建议:希望主机把连接间隔设为30ms,主机可以接受也可以拒绝这个请求。

°
一个连接间隔可以发多个包由下面参数决定:

#define NRF_SDH_BLE_GAP_EVENT_LENGTH 24

NRF_SDH_BLE_GAP_EVENT_LENGTH的单位是1.25ms,所以这里的NRF_SDH_BLE_GAP_EVENT_LENGTH是30ms,如果连接间隔也为30ms,那就意味着整个连接间隔都可以用来发送蓝牙数据包,大家知道一个251字节的蓝牙数据包和它的ACK包总共在空中大概持续2.5ms时间,这样我们可以大概估算30ms连接间隔中理论上可以发的包数:

NRF_SDH_BLE_GAP_EVENT_LENGTH / 2.5ms = 12,虽然NRF_SDH_BLE_GAP_EVENT_LENGTH等于连接间隔,但不表示整个连接间隔都可以用来发送蓝牙数据包,因为每个连接间隔还需要预留一些时间给协议栈调度,射频初始化,以及应用程序执行,这个时间假设为3ms,那么上面的配置情况下,30ms连接间隔理论上可以发送的最大包数为:(30-3)/2.5 = 10,即理论上蓝牙数据传输最高速度可以达到:(244*10)/30ms= 81kB/s。

回到上一节问题,为什么代码片段3和代码片段2测试结果差不多呢?因为代码片段3没有对协议栈进行额外配置,所以传输速率起不来。现在我们把上面的配置加上去,看一下代码片段3传输速率能到多少?

数据吞吐率只有32kB/s,跟理论值81kB/s还是差了不少,这个又是什么原因呢?

°
为了分析问题,我们把打印方式改成如下方式:

相应打印日志如下所示:

可以看出,我们的设备每49ms才发了4个包或者5个包,这就是为什么它的吞吐率只有30kB/s左右的原因。49ms发4个包或者5个包,到底是协议栈配置问题,还是手机端限制问题,抑或是应用程序逻辑问题?从上面的日志我们可以推出,协议栈射频FIFO为4(这个是日志给我们的推论,我们也暂且这么认为),而不是我们预想的10,换句话说,30ms连接间隔最多发4个包,即244*4/30ms = 32kB/s,正好跟我们实测值差不多。

如果我们把连接间隔改为14ms,那么吞吐率是不是可以达到:244*4/14ms = 69.7kB/s

可以看到,基本上达到预期了。大家可以尝试把连接间隔进一步减少,看一下吞吐率会不会进一步提高?答案是否,具体原因大家可以自己去想一下。

但是60kB/s跟我们的理论速度80kB/s相比,还是相差有一点大,有没有办法能不能进一步提高吞吐率?我们现在把连接间隔重新改为30ms,然后在nus_data_handler中加入如下代码:

ble_nus_data_send 对应的ATT PDU是notification,notification是没有response的,但它还是有ACK的,ACK表示对方已经正确收到该包。在Nordic SDK中,BLE_NUS_EVT_TX_RDY事件即表示notification的ACK包,也就是说,每收到BLE_NUS_EVT_TX_RDY这个事件,表示一个或者多个notification包已经被对方正确接收。

假设协议栈射频FIFO为10并且已满,此时协议栈将自动将这10个包发出去,每发出去1个包或者多个包(这里用n替代),产生一次BLE_NUS_EVT_TX_RDY回调事件,此时FIFO中还有10-n个包待发送,换句话说,射频FIFO又可以重新入队n个新的数据包进来。通过这种方式,我们可以让一个连接间隔所有时间都用来发数据包,直至下一个连接间隔。

下面为新代码对应的日志,由日志可见,设备在30ms的连接间隔中发出了10个蓝牙包,说明之前的协议栈配置没有问题,手机也支持10个蓝牙包,是我们的应用逻辑出问题了,没有让协议栈跑到极限。

如下所示,这种情况下蓝牙数据传输速率可以达到80kB/s左右,基本上快接近蓝牙4.2的理论吞吐率了。

4.3、蓝牙5.0高速率模式

°
蓝牙5.0提出了一个高速率模式,即引入了一个新的调制解调符号率:

2Msps,即1秒钟可以传2M个bit,一个bit传输只需1/2M=0.5us。蓝牙4.x低功耗模式只支持1Mbps,也就是说传输1bit需要1us时间。采用2M高速率模式,传输同样长度的蓝牙数据包,所花费的时间只有1M模式的一半,换句话说,同样的连接间隔中,2M模式可以发出的数据包基本上是1M模式的两倍,根据吞吐率计算公式:

蓝牙数据传输吞吐率 = 一个数据包长度 * 包数 / 连接间隔

包数翻倍,在其他条件不变的情况下,相当于数据传输速率翻倍。

只有手机和蓝牙设备同时支持2M模式,并且有一方主动要求更新物理层为2M时,蓝牙通信才会采用2M,上述的速率翻倍才可能实现。

Nordic所有的nRF52设备都支持高速率2M模式,而手机是否支持2M模式,可以通过手机版nRF Connect的“Device information”菜单查看,比如我的手机显示如下信息,说明它是支持2M模式的。

硬件已经满足了,那软件需要如何修改呢?2M或者1M模式,这个是物理层的概念,对应用来说是完全透明的,也就是说,不管采用2M还是1M模式,应用代码都是一模一样,不需做任何修改。ble_app_uart已经支持2M模式了,相关代码如下所示:

phy参数有4种:

BLE_GAP_PHY_1MBPS,强制选择1M模式

BLE_GAP_PHY_2MBPS,强制选择2M模式

BLE_GAP_PHY_CODED,强制选择低速率的长距离模式

BLE_GAP_PHY_AUTO,协议栈自动选择合适的phy层,一般会选择最快最合适的那个phy

请注意:BLE_GAP_EVT_PHY_UPDATE_REQUEST是响应主机端的phy update请求的,如果主机端不发phy update请求,那么phy仍然维持默认的1M模式。碰到这种情况,设备端需要主动发起phy update请求,怎么做呢?只需在连接成功事件中,调用sd_ble_gap_phy_update这个函数即可,这样哪怕手机端不发起phy update请求,设备端也会主动请求phy update。

4.2节的参考代码我都是把phy强制设为BLE_GAP_PHY_1MBPS,现在我们把它重新设为BLE_GAP_PHY_AUTO,然后手机端主动发起2M phy update请求,如下所示:

此时蓝牙数据吞吐率如下所示:146kB/s,快接近1M模式下的吞吐率的两倍,符合我们的预期。

4.4、更接近实际应用场景的参考代码

°
参考数据与用户的实际应用场景差别很大:

上面的代码只是为了测试蓝牙数据通信最高速率能到多少,但用户的实际应用场景与此差别很大,主要差别就是数据生成方式。上面的代码有效数据生成方式非常简单:直接对原始的数组元素进行自增,实际应用场景数据都是成块生成的,我们这里假设每7ms生成420B字节数据,换句话说,蓝牙数据传输速率必须达到60kB/s,才能把数据全部正确发出去,而不出现所谓丢包或者不同步现象。

在代码中,每生成一个420B数据,我们将其拆成2包数据放到队列(queue)中,然后在7ms timer中断中以及BLE_NUS_EVT_TX_RDY回调中,我们会去查询queue是否为空,如果不空就把队列中的数据通过蓝牙发出去,核心代码如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

所有相关代码我们都是通过APP_QUEUE这个宏来控制的,大家可以自己去看一下相关代码,这里就不再解读了。

经过测试我们发现(代码需要打开APP_QUEUE这个宏),当手机亮屏的时候(此时手机需要协调WiFi和蓝牙活动),丢包现象时有发生;当手机息屏的时候,基本上没有丢包现象发生,日志如下所示:

724fc025389c99277aba807ddf7f0d99b0217afb1d654-l0wjMv_fw658.jpg

这是一个非常接近实际应用场景的代码,大家可以直接参考它来完成自己的开发。这里要注意一点,由于环境的复杂性以及手机需要调度多种射频活动,“丢包”不可避免,大家需要做的是,当“丢包”发生时,即上文的queue发生溢出时,该如何处理?一般而言,都是舍弃相关数据以让程序可以正常往下跑。

上述所有代码都已上传到百度网盘,网盘地址见文章最开始的地方。

5、如何测试BLE设备的稳定性

°
怎么测试固件和手机交互的功能和稳定性?

在开发蓝牙设备固件的时候,不可避免需要用手机对其进行测试,尤其需要对其进行稳定性测试。一般而言,固件开发和手机app开发是相互独立的,很多时候我们会碰到固件开发差不多了但手机app还没有开发好,这种情况下怎么测试固件和手机交互的功能和稳定性?答案是nRF connect手机版。nRF connect很多功能都简单明了,一看就会,大家可以用它们来做功能性测试。

这里我们讲一下nRF connect的宏录制功能,大家可以用宏录制功能来测试BLE通信的稳定性。强调一下,宏录制功能目前只有安卓版nRF Connect支持,iOS版nRF Connect还不支持这个特性。

5.1、手机端宏录制方式

°
如何测试BLE数据下行性能?

相信到现在大家对BLE数据上传机理和实践有个大概的了解,那如何测试BLE数据下行性能,即怎么测试数据从手机传到设备的稳定性和可靠性?我们是不是必须开发一款手机app来进行相关测试呢?其实不用的,大家可以直接使用Nordic的nRF connect手机版来与你的设备进行交互,从而完成基本功能验证,同时可以使用nRF connect宏录制功能来对设备进行稳定性测试和压力测试。下面我们来讲讲宏录制是怎么工作的。

所谓宏录制,就是把你对nRF connect的操作录制下来,然后通过宏播放实现自动化操作。由于nRF connect是一个容器,并支持JavaScript和HTML语法,宏其实就是一个XML脚本,nRF connect定义了自己的一套XML标签操作,遵守这套XML标签操作,就可以对nRF connect进行自动化操作。nRF connect支持的所有XML语法都在手机安装目录\Nordic Semiconductor中的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。下面具体讲一下宏录制的操作过程。

当nRF connect连接设备成功后,你会发现右下角有一个红点,那个就是宏录制菜单。

点击下面的红点,我们开始宏录制操作:

然后我们按照普通操作来操作nRF connect,这些操作最终对应的BLE指令会被录制下来,以便后续重复播放。我们先把“1234”发送给设备,如下:

发送完上述指令后,我们加一个300ms的延时,如下:

然后我们点击完成按钮,保存该宏,可以看出这个宏包括两条操作:发送“1234”到设备,然后睡眠300ms。

将宏命名为“test”并保存:

到此宏已经录制成功了,现在我们开始展示宏的神奇功能。如下,选择循环播放模式,然后点击“开始”按钮开始循环播放该录制宏。

大家可以看到,nRF connect先执行“Write 0x31323334 to RX characteristic”,然后睡眠300ms,然后又执行“Write 0x31323334 to RX characteristic”,如此循环往复。打开串口助手,你会发现设备已经收到了手机发过来的一连串“1234”,如下。

我们把刚才的test宏导出为XML,看一看它到底长什么样:

大家可以看到,宏就是一些XML标记,大家也可以在此基础上,去修改该XML文件,以实现更复杂的自动化测试,然后通过nRF connect把最新的XML文件装载进来,就可以自动播放了。

如果你还想了解宏更多的用法信息,请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Macros/README.md

5.2、电脑端XML方式

°
透过XML脚本方式来实现更复杂的自动化测试

前面的宏录制方式,功能还是比较单一,如果要实现更复杂的自动化测试,可以通过在PC端执行XML脚本方式来实现(这个XML格式跟上面的XML是有差别的)。通过安卓调试工具ADB,我们可以直接通过PC来操作nRF connect,这样就可以让nRF connect按照XML脚本意图去执行相关自动化操作。nRF connect支持的所有XML语法都在手机安装目录中(手机内部存储/ Nordic Semiconductor目录)的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。

欲了解更多信息请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Automated%20tests/README.md

6、开发手机端app代码

°
Nordic提供很多手机端开源app供大家参考:

用得最多的就是nRF Toolbox和nRF Blinky(注:nRF connect代码不开源),在nRF Toolbox和nRF Blinky中都有相关的BLE操作库,尤其是nRF Toolbox包含了很多BLE库,比如BLE管理,DFU,数据透传,蓝牙Mesh等等,大家可以参考他们来开发自己的手机端app。

Android版nRF Toolbox源代码及开发说明请参考:https://github.com/NordicSemiconductor/Android-nRF-Toolbox

iOS版nRF Toolbox源代码及开发说明请参考:https://github.com/NordicSemiconductor/IOS-nRF-Toolbox

°
nRF Toolbox软件界面如下所示:

UART就是前文说的NUS服务,除了nRF connect,其实大家也可以通过nRF Toolbox UART模块来完成第2章所述的操作。nRF Toolbox另一个用的比较多的功能就是DFU,如果你需要通过手机BLE来实现设备固件的空中升级(OTA),那么可以参考nRF Toolbox DFU模块来编写你的手机端app软件。


http://www.ppmy.cn/news/562631.html

相关文章

什么是蓝牙适配器?它有哪些性能特点?-道合顺大数据Infinigo

蓝牙(Bluetooth)是无线网络传输技术的一种,原本是用来取代红外的。与红外技术相比,蓝牙无需对准就能传输数据,传输距离小于10米(红外的传输距离在几米以内)。而在信号放大器的帮助下,通讯距离甚至可达100米左右。蓝牙技术非常适合…

共享单车蓝牙锁方案phy6222系列蓝牙芯片共享产品共享设备低成本BLE

公共交通工具的"最后一公里"是城市居民出行采用公共交通出行的主要障碍,也是建设绿色城市、低碳城市过程中面临的主要挑战。随着科技的发展,物联网领域市场的不断开拓BLE协议在共享交通工具上打开了突破口,目前已广泛应用及遍布我们…

新一代蓝牙5.3到底有哪些新东西

1 概述 2021年7月,蓝牙官方组织SIG释放了代码Syndney的5.3版本蓝牙核心协议文档,此版本仍在第5个大版本中,属于小功能升级,那这个版本带来了哪些功能升级呢? 1.1 AdvDataInfo in Periodic Advertising 同步广播是蓝…

java pc 蓝牙_Nokia PC 套件与蓝牙适配器连接教程(转)--个人推荐

在一个干净的操作系统上,安装蓝牙适配器驱动后,只要蓝牙适配器已经被作为成功的识别为系统设备。我们继续安装诺基亚PC套件,这样一点问题也没有,PC套件会很顺利的通过蓝牙适配器识别出手机。PC套件提示“配对” 手机上输入相同的“…

两种方式获得蓝牙适配器

第一种 BluetoothManager manager (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter adapter1 manager.getAdapter(); 第二种 BluetoothAdapter adapter2 BluetoothAdapter.getDefaultAdapter(); 区别 我在日志把两个适配器都打印出来…

绿联 蓝牙适配器 linux,绿联蓝牙适配器

现在很多朋友生活中都会有听音乐的习惯,因此,音箱基本是每个家庭必备的一个设备。然而传统的3.5mm音箱没有蓝牙功能,我们要使用传统音箱听音乐只能连接电脑后才能够任意播放音乐,这种局限性在智能移动设备泛滥的今天,感…

机器学习——手写数据集的介绍以及案例讲解

系列文章目录 机器学习聚类——DBSCAN(Density-based spatial clustering of applications with noise,基于密度的聚类算法) 机器学习集成学习——Adaboost分离器算法 机器学习聚类算法——BIRCH算法、DBSCAN算法、OPTICS算法 机器学习的…

magic 2.0下使用杂牌蓝牙适配器并通过手机gprs上网

一、.安装 bluez-libs 2.24 http://download.chinaunix.net/download/0001000/990.shtml bluez-utils 2.24 http://download.chinaunix.net/download/0001000/991.shtml 官方网站上有提供3.7版的下载。但经过小弟试验,好像因为3.x版的不支持kdebluetooth1.0-0.…