[分享] 手把手教你开发BLE数据透传应用程序(二)
771 查看
6 回复
 楼主 | 发布于 2019-04-29 | 只看楼主
分享到:

5. 常用ATT命令

        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命令列表如下所示:

 

6. 设备端固件代码一览

现在我们一起来看一下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函数体,你会发现它是分三步来做的:

  1. 添加服务的UUID。如果是蓝牙标准服务,这步可以省略。由于NUS不是蓝牙联盟定义的,所以需要调用sd_ble_uuid_vs_add以增加一个供应商自定义的UUID。
  2. 添加服务本身。直接调用sd_ble_gatts_service_add就可以完成。
  3. 添加服务下面的characteristics。server的characteristic一般都是通过sd_ble_gatts_characteristic_add来添加的。以NUS的RX characteristic为例,可以看到:
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状态的,从而大大节省系统功耗。



(2 ) (0 )
回复 举报

回复于 2019-04-29 沙发

谢谢分享
(0 )
评论 (0) 举报

回复于 2019-04-30 2#

谢谢分享!!!!
(0 )
评论 (0) 举报

回复于 2019-05-25 3#

感谢分享!
(0 )
评论 (0) 举报

回复于 2020-02-18 4#

感谢分享
(0 )
评论 (0) 举报

回复于 2020-02-18 5#

感谢分享,欢迎关注我,资料持续更新中。有需要机械臂,电源,硬件电路设计,软件编程,开发板等各种定制的可以私聊我哦,相互学习,共同进步。
(0 )
评论 (0) 举报

回复于 2020-03-21 6#

(0 )
评论 (0) 举报
  • 发表回复
    0/3000





    举报

    请选择举报类别

    • 广告垃圾
    • 违规内容
    • 恶意灌水
    • 重复发帖

    全部板块

    返回顶部