如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发本身的service和characteristic?如何区分ATT和GATT?有没有什么工具能够对BLE设备进行压力测试?如何提升BLE设备的数据上传速度?本文将对以上问题进行解答。html
在不少应用场合,BLE只是做为一个数据透传模块,即将设备端数据上传给手机,同时接收手机端下发的数据。本文将和你们一块儿,一步一步演示如何开发一个BLE透传应用程序。按照本文的说明,你们能够很快就实现一个BLE透传应用,BLE透传应用已是BLE应用中比较复杂的一种,一旦你们掌握了BLE透传应用,其余BLE应用开发就更不在话下了。本文还会以BLE透传为例子,来解释BLE service和characteristic等概念,以帮助你们理解如何定义和开发本身的BLE service和characteristic等,从而完全理解BLE协议栈中的ATT和GATT的运行原理。而后,本文还将手把手教你们如何提升BLE数据传输速度(蓝牙4.2的理论吞吐率大概为100kB/s,而咱们实际达到了80kB/s,已经很是接近理论值)。最后,咱们将告诉你们如何使用安卓版nRF Connect来对你的BLE设备进行压力测试,以测试设备的稳定性和可靠性。固然,文章的最后也会告诉你们如何找到安卓和iOS手机app开发参考代码。git
1) Nordic nRF52或者nRF51开发板1块。请参考“Nordic nRF51/nRF52开发流程说明”,购买相应开发板(DK)。github
2) 开发环境搭建。简述以下(详细说明请参考“Nordic nRF51/nRF52开发环境搭建”):编程
注:若是你使用的是Linux系统/Mac系统,或者你使用的不是Keil5-MDK,请参考“Nordic nRF51/nRF52开发环境搭建”来搭建你的开发环境。数组
Nordic SDK已经提供了一个直接就能够编译和运行的数据透传应用程序:ble_app_uart,Nordic将BLE透传服务称为Nordic UART Service(NUS),因此在Nordic SDK中,NUS就是BLE透传服务。请按照以下步骤运行SDK自带的ble_app_uart程序:浏览器
1) 确认本身的芯片型号或者开发板。若是采用Nordic官方开发板的话,芯片型号和开发板编号对应关系以下:安全
这里我会以nRF52832开发板PCA10040为例来阐述整个开发过程,其余开发板与之相似,你们本身能够触类旁通来开始本身的开发之旅。服务器
2) 将开发板与PC机经过USB线相连,同时打开开发板电源(将左下角的拨位开关打到“ON”位置),打开桌面版nRF Connect,选择启动“Programmer”应用,因为驱动以前已经安装好了,设备能够当即识别成功。执行“full erase”操做,以擦除芯片原始内容。 架构
3) 打开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_packsapp
后续将以52832开发板为例来阐述,51822与之相似就再也不阐述了。
注:Nordic SDK例程目录结构为:SDK版本/ examples /协议角色/例子名称/开发板型号/协议栈型号/工具链类型/具体工程,好比下面例子:
Nordic每个例子都支持5种工具链:Keil5/Keil4/IAR/GCC/SES,以下所示:
4) 编译程序。若是你已经按照以前的说明配置好了开发环境,那么这里编译是不会报任何错的。(若是你遇到了编译错误,请从新按照前面说明去搭建你的开发环境,不要怀疑SDK例子代码有问题哦)
5) 下载程序。程序下载包括2步:一先下载softdevice,二再下载应用。Softdevice是Nordic蓝牙协议栈的名称,整个开发过程当中只需下载一次。应用就是咱们这里的ble_app_uart程序。若是你的开发板已经下载了其余代码,那么最好先把开发板全擦一次,而后再下载softdevice和应用。
6) 链接手机。打开手机蓝牙和手机版nRF connect。在nRF connect中,你将看到一个广播设备:Nordic_UART,这个就是开发板的广播名字。点击“CONNECT”,手机将与设备创建链接,并开始服务发现过程,链接成功后,LED1熄灭,LED2点亮,最后将获得以下界面。
上图的Nordic UART Service(NUS)就是咱们的数据透传服务, NUS具体包括两个characteristic:TX和RX,因为NUS是由设备提供的,因此TX表示设备发送数据给手机,RX表示设备接收手机发过来的数据。
7) 测试NUS服务。ble_app_uart使用串口与上位机交互,选择一款串口助手软件,好比Putty,打开该串口软件,并作以下设置:
复位开发板,你会发现串口助手会打印以下信息:
按照第6)步,从新将开发板连上手机,而后点击右上角的“Enable CCCDs”以使能notification,以下所示:
设备接收数据: 点击RX characteristic旁边的向上箭头,经过手机蓝牙往设备发送:12345678,以下所示:
此时设备经过串口打印出刚才接收到的数据,以下所示:
设备发送数据:在串口助手中输入“abcdefgh”并输入“\n”(注:在Putty中,先按“CTRL”再按“J”就会发出“\n”换行符)做为结束符,设备将把串口收到的数据经过蓝牙发送给手机,手机的TX characteristic将显示上述字符串,以下所示:
注:若是你的串口助手发不出“\n”换行符,那么你须要最少输入MTU-3个字符,设备才会把收到的所有字符经过蓝牙发出去
经过上面的测试,你们能够发现Nordic SDK已经把蓝牙数据透传服务作好了,你们能够直接拿过来使用,下面将对其工做原理进行阐述,最后在Nordic蓝牙透传例子ble_app_uart上进行二次开发,以增长一些其余有用功能。若是你们以为Nordic ble_app_uart已经能够知足本身的需求,并且也不想花时间去研究里面的原理,那么章节3/4/5/6/7.1能够略过不看。
BLE采用了client/server (C/S)架构来进行数据交互,C/S架构是一种很是常见的架构,在咱们身边随处可见,好比咱们常常用到的浏览器和服务器也是一种C/S架构,这其中浏览器是客户端client,服务器是服务端server,server好比淘宝服务器,提供商品信息,广告,社交等服务,而浏览器,好比微软的IE,就能够用来请求这些服务,并使用server提供的服务。BLE与此相似,通常而言设备提供服务,所以设备是server,手机使用设备提供的服务,所以手机是client。好比蓝牙体温计,它能够提供“体温”数据服务,所以是一个server,而手机则能够请求“体温”数据以显示在手机上,所以手机是一个client。
服务是以数据为载体的,因此说server提供服务其实就是提供各类有价值的数据。
上图所示的Request和Response其实就是咱们常常说的ATT命令(ATT PDU),也就是说Client和Server之间经过ATT PDU进行交互。另外,一个数据“37”,有多是说体温“37度”,也有多是说心率“37次”或者湿度“37%”,所以Server须要将数据进行包装和分类,在BLE中,数据是经过characteristic进行包装的,并且多个characteristic组成一个service,service是一个独立的服务单元,或者说service是一个基本的BLE应用。所以咱们能够把上图细化为:
若是某个service是一个蓝牙联盟定义的标准服务,也能够称其为profile,好比HID/心率计/体温计/血糖仪等,都是标准蓝牙服务,所以都有相应的profile规格书。
如文章“深刻浅出低功耗蓝牙(BLE)协议栈”所讲,BLE协议栈架构以下所示:
如上图所示,用户开发应用程序或者说service的时候,调用的都是GATT API,而GATT又调用了ATT API,前面也讲过,BLE数据最终都是经过ATT PDU来传输的,那么为何还须要GATT层?直接操做ATT层不也能够达到一样的目的吗?
前面也提过,Server是经过characteristic来表示数据的,虽然一条数据最有价值的部分是它的值(value),可是仅有value是不够,好比27,究竟是表示27°温度仍是27%湿度;若是表示的是温度,那么它的单位是摄氏度仍是华氏度。同时每一个value还有相应的读写属性以及权限属性,所以一个characteristic包含三种条目:characteristic声明,characteristic的值以及characteristic的描述符(能够有多个描述符),以下所示:
因为一个service能够包含多个characteristic,characteristic declaration就是每一个characteristic的分界符,解析时一旦遇到characteristic declaration,就能够认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value的读写属性等。Characteristic value就是数据的值了,这个比较好理解就再也不说了。Characteristic descriptor就是数据的额外信息,好比温度的单位是什么,数据是用小数表示仍是百分比表示等之类的数据描述信息。CCCD是一种特殊的characteristic descriptor,通常而言,都是client来访问server的characteristic,咱们把这种操做称为读或者写。另外,server能够直接把本身的characteristic的值告诉client,咱们称其为notify或者indicate,跟read操做相比,只有须要传输数据的时候或者说只有当数据有效时,server才开始notify或者indicate数据到client,所以这种操做方式能够大大节省server的功耗。有时候client不想监听characteristic notify或者indicate过来的数据,那么就可使用CCCD来关闭characteristic的notify或者indicate功能;若是client又须要监听characteristic的notify或者indicate,那么它能够从新使能CCCD来打开相关操做。总结一下,当characteristic具备notify或者indicate操做功能时,那么必须为其添加相应CCCD,以方便client来使能或者禁止notify或者indicate功能。
无论是characteristic declaration,characteristic value仍是characteristic descriptor,实现的时候,咱们都是用attribute来表达的,也就是说,他们每个都是一个attribute,attribute能够用下图来表示:
因为这个UUID众所周知,蓝牙联盟将本身定义的attribute或者数据只用16bit UUID来表示,好比0x1234,其实它也是128bit,完整表示为:
Attribute type通常是由service和characteristic规格来定义,站在蓝牙协议栈角度来看,ATT层定义了一个通讯的基本框架,数据的基本结构,以及通讯的指令,而GATT层就是前文所述的service和characteristic,GATT层用来赋予每一个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也能够通讯起来,但会产生兼容性问题以及通讯的低效率。
你们还记不记得设备与手机nRF connect链接成功后呈现的界面,我这里再贴一下:
能够看到手机呈现的就是上文讲的service和characteristic,nRF Connect为了让整个界面变得更美观,将访问属性,UUID,handle都分列来表示了,以至于不少初学者会把理论和现实两者对应不起来。Nordic以前推出过一款Master Control Panel(MCP),MCP如今已经不推荐使用了,不过MCP有一个好处,它对service和characteristic的组织方式更接近底层实现方式,对你们理解service和characteristic是很是有帮助的。仍是这个设备,我用MCP跟它链接并进行服务发现,你会发现它呈现的界面以下所示:
这个图就跟上面讲的理论知识能够一一对应起来了,NUS包含2个characteristic:RX和TX,每个条目都是一个attribute,NUS服务自己就是一个attribute,而RX characteristic自己又包含2条attribute:一条是declaration attribute,一条是value自己attribute。因为TX支持notify,因此它包含3条attribute,另一条attribute是CCCD。每一个attribute都有一个handle和UUID,handle用来访问该attribute,UUID用来指明该attribute的类型。能够说,server提供数据,而数据是由attribute来表达,全部attribute组成一个attribute table,设备支持的服务不一样,attribute table就不一样。这里说明一下,当你在Nordic已有例程基础上再去添加新的服务或者删除已有的服务,记得必定要去修改ATTR_TAB_SIZE那个宏,不然协议栈初始化会有问题。
Client和Server之间是经过ATT PDU来通讯的,ATT PDU主要包括4类:读,写,notify和indicate。若是一个命令须要response,那么会在相应命令后面加上request;若是一个命令只须要ACK而不须要response,那么它的后面就不会带request。这里要特别强调一点,BLE全部命令都是“必达”的,也就是说每一个命令发出去以后,会立马等ACK信息,若是收到了ACK包,发起方认为命令完成;不然发起方会一直重传该命令直到超时致使BLE链接断开。换句话说,只要你的BLE没有断开,那么你以前发送的数据包,无论它是用什么ATT PDU来发送的,它确定被对方收到了。我估计不少人对此会产生疑问,由于他们常常碰到丢包的状况,其实你们常常碰到的“丢包”,不是空中把包丢了或者包在空中被干扰了,而是你们发送的代码写得有问题,致使你要发送的包没有被安全送达到协议栈射频FIFO中,因此之后你们碰到丢包状况,请先检查你的代码,保证你的数据包正确完整安全地送达到协议栈射频FIFO中,只要数据包放到了协议栈射频FIFO中,蓝牙协议栈就能保证该数据包“必达”对方。既然每一个ATT命令都必达对方,那么还须要request作什么?若是一个命令带有request后缀,那么发起方就能够收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。因此采用request/response方式,应用层能够按顺序地发送一些数据包,这个在不少应用场合是很是有用的。相反,若是你对应用层数据包的顺序没有要求,那么就能够不使用request/response形式。另外Request/response有一个反作用:大大下降通讯的吞吐率,由于request/response必须在不一样的链接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就致使两个链接间隔最多只能发一个数据包,而不带request后缀的ATT命令就没有这个问题,在同一个链接间隔中,你能够同时发多个数据包,这样将大大提升数据的吞吐率。你们能够参考下图来理解request和非request命令的区别:
经常使用的带request的命令:全部read命令,write request,indication等,而经常使用的不带request的命令有write command,notification等,完整的ATT命令列表以下所示:
如今咱们一块儿来看一下ble_app_uart的源代码,看看它是怎么工做起来的。首先咱们来看main函数:
如上所述,ble_stack_init用于初始化配置和使能蓝牙协议栈,其代码以下所示:
其中,nrf_sdh_enable_request须要选择蓝牙协议栈的低频时钟(因为蓝牙协议栈的高频时钟必须为外部32M晶振,因此高频时钟无需配置;而低频时钟能够选择为内部32K RC或者外部32K晶振,因此低频时钟须要人工配置),所以以下宏须要根据实际状况进行调整:
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 };
经过sdk_config.h文件能够看到,默认是选择外部32K晶振做为低频时钟的,若是你想选择内部32K RC做为低频时钟,那么须要作以下修改:
NRF_SDH_CLOCK_LF_SRC = 0 NRF_SDH_CLOCK_LF_RC_CTIV = 16 //每4s启动一次校准 NRF_SDH_CLOCK_LF_RC_TEMP_CTIV = 2 NRF_SDH_CLOCK_LF_ACCURACY = 1 //500ppm
nrf_sdh_ble_default_cfg_set用来配置softdevice协议栈,以下宏是常常须要修改的:
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 size为多大 NRF_SDH_BLE_VS_UUID_COUNT //用户自定义的base UUID有几个 NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE //Attribute table总共占多少协议栈RAM空间 NRF_SDH_BLE_SERVICE_CHANGED //要不要包含service change characteristic
nrf_sdh_ble_enable真正使能BLE功能,它的参数ram_start既是一个输入参数又是一个输出参数,做为输入参数,系统自动会把以下的RAM起始地址传入:
同时nrf_sdh_ble_enable会把当前softdevice配置状况下,它实际须要占用的RAM空间经过ram_start返回,若是这个返回值不等于输入值,那么用户须要把上图的IRAM1起始地址修改为它的返回值。其中NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的取值是须要用户不断去试错的,所以每当你添加了或者删除了BLE service,都须要去调整NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的值,而后去查看nrf_sdh_ble_enable的返回值,看看这个参数的取值是否合理。
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,前面讲了那么多的概念和理论,如今咱们就来看看services_init是如何作到跟理论一致的。services_init经过ble_nus_init添加了一个蓝牙数据透传服务:NUS,那ble_nus_init是怎么将NUS服务添加成功的呢?查看ble_nus_init函数体,你会发现它是分三步来作的:
sd_ble_gatts_characteristic_add(p_nus->service_handle, &char_md, &attr_char_value, &p_nus->rx_handles);
其中,p_nus->service_handle表示该characteristic属于那个service,p_nus->rx_handles是输出值,由协议栈返回,之后访问该characteristic都是经过这个句柄来完成,attr_char_value这个是characteristic的value,char_md这个是characteristic的元数据(metadata),前面第4章也讲过,一个数据除了有value这个characteristic以外,它还包含其余attribute,而这些attribute所有都用char_md来表示,好比这个characteristic value能支持的ATT命令类型,CCCD信息,descriptor信息等,这里要特别指出的是,只有当支持notify或者indicate时,才须要提供cccd_md信息,其余ATT命令不须要cccd_md信息,因此RX characteristic的char_md以下所示,它同时支持write和write request两种写命令,因为它不支持notify或者indicate,因此cccd_md为NULL。
attr_char_value是一个attribute,因此它包含attribute metadata,以下:
attr_char_value具体包含的value信息由如下成员表示:
因为这里把characteristic value放在了协议栈RAM中,因此协议栈会自动为这个value建立一个buffer。若是你想把characteristic value放在用户RAM中,即vloc = BLE_GATTS_VLOC_USER,那么这里你还须要把一个全局数组变量赋给attr_char_value. p_value。
TX characteristic与之相似,就再也不额外解读了。
这里须要特别提醒你们的是,虽然Nordic API结构体参数设计得很复杂,可是大部分红员变量直接就可使用它的默认值0,你只需对你感兴趣的成员变量进行赋值便可,因此你们常常看到以下场合,即先用memset将该结构体变量初始化为0,让其全部成员变量都采用默认值,而后再对某些须要修改的成员变量进行二次赋值。你们必定不要忘了将结构体变量清零这一步操做!
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:
if ((data_array[index - 1] == '\n') || (index >= (m_ble_nus_max_data_len)))
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状态),其他时间系统都是处于idle状态的,从而大大节省系统功耗。
如何快速的把大量数据上传给手机?这是一个很常见的应用场合,如今咱们尝试去修改一下Nordic的原生例程,以实现最高的数据吞吐率。下面咱们经过几种不一样的方法来看看每种方法下它的吞吐率能到多少。
蓝牙spec规定,蓝牙链接间隔最小只能为7.5m,为了达到最高的吞吐率,咱们建立一个timer,让其每7ms发一次数据,看一看此时吞吐率能达到多少。7ms中断服务函数代码以下所示:
static void throughput_timer_handler(void * p_context) { UNUSED_PARAMETER(p_context); ret_code_t err_code; uint16_t length; m_cnt_7ms++; length = m_ble_nus_max_data_len; if (m_conn_handle != BLE_CONN_HANDLE_INVALID) { 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); // } m_len_sent += length; m_data_array[0]++; m_data_array[length-1]++; } NRF_LOG_INFO("time: %d *7ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_7ms,m_len_sent,m_len_sent/(m_cnt_7ms*7)); }
这种作法会致使ble_nus_data_send报“NRF_ERROR_RESOURCES”错误,这个错误表示协议栈无资源应付这么快的调用速度。为此咱们对ble_nus_data_send返回的错误值一律不进行处理,看看会发生什么?咱们发现程序能够正常运行,RTT viewer打印的日志以下所示:
由上图可知,数据上传吞吐率达到了34.8kB/s,其实这个吞吐率是假的,由于中间丢了不少包,但计算吞吐率的时候把丢的包也算进去了。以下图所示,0x6E以后应该为0x6F,但实际发送的数据包编号为0x83,丢包很是严重。
为了防止所谓的“丢包”(前面也提过,这里的丢包不是数据包在空中丢掉了,而是数据包没有安全送到协议栈的buffer中,从而致使丢包),咱们加上以下if语句,只有ble_nus_data_send返回正确时,才认为数据包正确发送,而后才能算入到throughput中:
if (err_code == NRF_SUCCESS) { m_len_sent += length; m_data_array[0]++; m_data_array[length-1]++; }
经过查看nRF connect日志,你会发现此时不会发生丢包了,但吞吐率直接降到了1.6kB/s左右。
咱们对方法一稍做调整,首先咱们持续往发送buffer写数据,直到返回值不是NRF_SUCCESS
do
{
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_len_sent += length;
m_data_array[0]++;
m_data_array[length-1]++;
}
} while (err_code == NRF_SUCCESS);
而后咱们把链接间隔设为尽量小,以期提升吞吐率,以下:
#ifdef CONN_INTERVAL_OPTIMIZE #define MIN_CONN_INTERVAL MSEC_TO_UNITS(8, UNIT_1_25_MS) #define MAX_CONN_INTERVAL MSEC_TO_UNITS(12, UNIT_1_25_MS) #endif
这种方法吞吐率能达到10kB/s,但离咱们的目标仍是很远。
最后咱们把connection event length extension和data length extension都打开(咱们将在方法2+中详细阐述这2个有效提升吞吐率的利器),即定义以下宏:
能够看到吞吐率将达到70kB/s,这个吞吐率仍是不错的。但仔细查看nRF connect日志,你会发现这种模式下仍是有小几率事件会致使“丢包”发生,并且整个发送逻辑也不是很优化,为此咱们想到了METHOD2.
ble_nus_data_send每次成功发送数据包,都会产生一个BLE_NUS_EVT_TX_RDY事件,收到这个事件后,再去调用ble_nus_data_send,丢包的状况就不会再发生了,核心代码以下所示:
if (p_evt->type == BLE_NUS_EVT_TX_RDY)
{
#ifdef METHOD2
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_len_sent += length;
m_data_array[0]++;
m_data_array[length-1]++;
}
NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);
#endif
你们能够本身去查看一下nRF Connect的数据log,这种方式是没有丢包的,可是打开RTT viewer,你会发现他的吞吐率低得可怜,只有1kB/s。
与方法1+相似,咱们在方法2基础上,持续往发送buffer送数据直到返回值不为0,以下:
#ifdef METHOD2_PLUS
//queue multiple tx array
do
{
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_len_sent += length;
m_data_array[0]++;
m_data_array[length-1]++;
}
} while (err_code == NRF_SUCCESS);
NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);
#endif
若是须要支持长的MTU的话,那么须要修改gap event length。gap event length是指一个链接间隔中最多能给某一个设备数据交互的时间长度,若是gap event length设为1ms,而MTU设为247的话,那么协议栈会报配置错误。若是gap event length远大于一个数据长包的长度,那么在一个链接间隔中就能够传送多个长包。有的人为了省事,就把gap event length设的很大,好比大于或者等于链接间隔,这个配置自己是没什么问题的,可是若是一个设备要跟多个设备相连,那么这种配置就会使得设备链接数有限或者其余设备带宽有限,好比不能同时连20个设备,好比其余设备传输速度很慢。因为咱们如今是一对一的链接,偷点懒,我们把gap event length修改为一个合适的值,以使其尽量占满整个链接间隔,以下将gap event length修改成30ms
#define NRF_SDH_BLE_GAP_EVENT_LENGTH 24
注意:为了兼容多链接以及保证其余设备的带宽,咱们通常建议gap event length就使用SDK默认配置:6,这个默认配置已经能够发送一个241字节的MTU长包了,但只能发送一个,为了在一个链接间隔中发送多个长包,能够在不修改gap event length的状况下,经过使能connection event length的作法,以达到一样的目的,以下面代码所示:
#ifdef EVT_LEN_EXT_ON ble_opt_t opt; memset(&opt, 0x00, sizeof(opt)); opt.common_opt.conn_evt_ext.enable = true; err_code = sd_ble_opt_set(BLE_COMMON_OPT_CONN_EVT_EXT, &opt); APP_ERROR_CHECK(err_code); #endif
而后咱们再将链接间隔设为一个合适的值,以保证上述connection event能够占据整个链接间隔。注意:不是链接间隔越短越好,而是整个链接间隔中空闲时间越短越好,也就是说,哪怕链接间隔比较长,若是能保证connection event/connection interval最大,那么就有可能达到最大的吞吐率。
#ifdef CONN_INTERVAL_OPTIMIZE
#define MIN_CONN_INTERVAL MSEC_TO_UNITS(8, UNIT_1_25_MS)
#define MAX_CONN_INTERVAL MSEC_TO_UNITS(12, UNIT_1_25_MS)
#endif
我如今使用的是华为P9手机,它将把MTU设为241,在DLE不开的状况下(此时链路层每一个数据包的长度仍是只有27个字节!),咱们能够看到throughput能够达到10kB以上,以下:
而后咱们再打开DLE功能,此时链路层每一个数据包的长度将变成251字节,以下:
#ifdef DLE_ON case BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST: { NRF_LOG_DEBUG("DLE update request."); ble_gap_data_length_params_t dle_param; memset(&dle_param, 0, sizeof(ble_gap_data_length_params_t)); //0 means auto select DLE err_code = sd_ble_gap_data_length_update(p_ble_evt->evt.gap_evt.conn_handle, &dle_param, NULL); APP_ERROR_CHECK(err_code); } break; #endif
此时咱们能够看到throughput能够达到77kB/s,离蓝牙4.2的理论throughput已经很接近了。这里特别须要指出的是,当DLE使能状况下,connection interval不是越小吞吐率越高,我这里使用的connection interval大概为10ms,若是你们把这个connection interval提升到30ms,有可能吞吐率更高,这里就再也不演示了。
上述代码工程已经上传到百度云盘中,有须要的同窗能够到以下连接下载:
下载“tutorial_ble_app_uart_SDK15_0_0.rar”,而后解压缩到SDK15.0.0以下目录下:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral,便可成功编译运行。
先说明一下,如下内容只能经过安卓版nRF Connect来实现,iOS版nRF Connect不支持以下特性。
相信到如今你们对BLE数据上传机理和实践有个大概的了解,那如何测试BLE数据下行性能,即怎么测试数据从手机传到设备的稳定性和可靠性?咱们是否是必须开发一款手机app来进行相关测试吗?答案是否认的,感谢Nordic给咱们带来了nRF connect,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,看一看它到底长什么样:
<macro name="test" icon="PLAY"> <assert-service description="Ensure Nordic UART Service" uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e"> <assert-characteristic description="Ensure RX Characteristic" uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e"> <property name="WRITE" requirement="MANDATORY"/> </assert-characteristic> </assert-service> <write description="Write 0x31323334 to RX Characteristic" characteristic-uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e" service-uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e" value="31323334" type="WRITE_REQUEST"/> <sleep description="Sleep 300 ms" timeout="300"/> </macro>
你们能够看到,宏就是一些XML标记,你们也能够在此基础上,去修改该XML文件,以实现更复杂的自动化测试,而后经过nRF connect把最新的XML文件装载进来,就能够自动播放了。
若是你还想了解宏更多的用法信息,请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Macros/README.md
前面的宏录制方式,功能仍是比较单一,若是要实现更复杂的自动化测试,能够经过在PC端执行XML脚本方式来实现。经过安卓调试工具ADB,咱们能够直接经过PC来操做nRF connect,而nRF connect又能识别XML脚本,这样就可让nRF connect按照XML脚本意图去执行相关自动化操做。nRF connect支持的全部XML语法都在手机安装目录中(手机内部存储/ Nordic Semiconductor目录)的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。
Nordic提供不少手机端开源app供你们参考,用得最多的就是nRF Toolbox和nRF Blinky(注:nRF connect代码不开源),在nRF Toolbox和nRF Blinky中都有相关的BLE操做库,尤为是nRF Toolbox包含了不少BLE库,好比BLE管理,DFU,数据透传,蓝牙Mesh等等,你们能够参考他们来开发本身的手机端app。
nRF Toolbox软件界面以下所示:
UART就是前文说到的NUS服务,除了nRF connect,其实你们也能够经过nRF Toolbox UART模块来完成第2章所述的操做。nRF Toolbox另外一个用的比较多的功能就是DFU,若是你须要经过手机BLE来实现设备固件的空中升级(OTA),那么能够参考nRF Toolbox DFU模块来编写你的手机端软件。