接上一篇整体架构介绍后,相信大家对NXP低功耗协议的使用还是没有什么概念(我没写错你也没看错)。由于第一篇博文信息量过大,没有在NXP BLE SDK上做过一定开发的的同学看起来肯定是云里雾里。从本篇开始将BLE SDK中逐个功能进行剖析,并且尽可能按照由浅入深的顺序发布。
前置条件
由于网上有大量的文章介绍BLE技术,这儿就不从零讲起基本概念了,假定各位同学对BLE广播与扫描功能有基本的了解,至少知道得以下几个方面:
- 广播和扫描的意义(为什么双方要做这件事)
- 双方的角色扮演(简单说主机扫描,从机广播)
- 基本的广播参数:时间间隔,广播通道,扫描占空比等
- 广播中带有一些数据,如果知道他们的格式就更好了
虽然文章主要目的是要在介绍如何在NXP低功耗蓝牙SDK中进行广播和扫描,但如果一上来就讲API又过于干涩。在涉及到笔者认为有必要展开的协议内容的时候我还是会将一下原理的。
BLE 5对广播和扫描这部分有一些比较大的变化,目前市面上相关的应用还比较少,因此本篇仍以BLE 4.2规范中定义的功能作为出发点,后续如有必要再单独介绍BLE 5带来的广播扩展功能。
什么时候可以开始广播 & 扫描?
在BLE协议规范中,对主机(Host)和控制器(Controller)初始化的流程有一个清晰定义,用户要走完这个流程才能向协议栈提交请求,同时BLE芯片也需要对射频部分寄存器进行初始化来保证RF电路的正常工作。NXP BLE SDK中的系统入口main_task()
任务体实现里,在正式进入事件loop之前将会调用Ble_Initialize()
来准备以上两项工作。注意!是准备,而非完成。也就是说该函数返回后,协议栈可能仍然没有准备好。记得千万不要直接在这个函数后面开启广播和扫描!
那何时才可以呢?初始化过程在调用Ble_Initialize()
时SDK会让安装一个默认的回调函数App_GenericCallback()
,回调触发后再由她转而触发用户层的BleApp_GenericCallback()
。当协议栈完成所有初始化工作后,用户将在这个回调函数里收到gInitializationComplete_c
事件,这才标志着用户可以正常使用BLE协议栈提供的服务了。
广播(advertising)
在NXP BLE SDK中涉及到广播主要是4个API:
// 设置广播数据(和扫描回复数据)
bleResult_t Gap_SetAdvertisingData(gapAdvertisingData_t* pAdvertisingData,
gapScanResponseData_t* pScanResponseData);
// 设置广播参数
bleResult_t Gap_SetAdvertisingParameters(gapAdvertisingParameters_t*
pAdvertisingParameters);
// 开启广播
bleResult_t Gap_StartAdvertising(gapAdvertisingCallback_t advertisingCallback,
gapConnectionCallback_t connectionCallback);
// 停止广播
bleResult_t Gap_StopAdvertising(void);
以上的4个API都是异步的,就意味着调用后会立刻得到返回值,该返回值仅表示函数调用(如参数传递是否正确,内存是否充足)的结果,实际功能的执行完成,用户应等到各个的回调事件到来作为判断。
下表列出了相关的事件:
事件Tag | 触发函数&事件 | 用户回调函数 |
---|---|---|
gAdvertisingDataSetupComplete_c | Gap_SetAdvertisingData | 通用回调函数BleApp_GenericCallback
|
gAdvertisingParametersSetupComplete_c | Gap_SetAdvertisingParameters | 通用回调函数BleApp_GenericCallback
|
gAdvertisingStateChanged_c * | Gap_StartAdvertising Gap_StopAdvertising | 广播回调函数BleApp_AdvertisingCallback
|
注意:用户需要自己记录当该事件产生时广播是被打开了还是被关闭了。
通常用户需要先设置好广播数据和广播参数再开启广播,需要按照一定顺序调用这几个API(利用回调作为衔接)。在使用NXP BLE SDK的时,ble conn manager和每一份例程代码都已经帮用户把广播流程规划好了,用户只需要在合适的时候调用例程代码中的 BleApp_Advertise()
即可。如果例程带的广播策略符合用户的需求,则用户只需要关心app_config.c
中所填充的广播数据和广播参数即可。
广播数据
根据BLE协议规定,广播包可以发送最多31个字节的数据,如果设备支持扫描请求(Scan Request),还可以在扫描回复(Scan Response)里在回复31个字节,这样最长也就是62字节的信息量。这些信息不是随便填的,必须按照BLE协议规范定义的格式,主机才能正确的解析其中的内容。一个广播包(或者Scan Response包)由如若干AD Structure结构组成,在代码层面,NXP协议栈提供了一个由gapAdStructure_t组成的结构体数组,用户可以将AD数据段依次填入。下面是一个有3个AD Structure的示例:
static const gapAdStructure_t advScanStruct[3] = {
{
.length = NumberOfElements(adData0) + 1,
.adType = gAdFlags_c,
.aData = (uint8_t *)adData0
},
{
.length = NumberOfElements(uuid_service_qpps) + 1,
.adType = gAdComplete128bitServiceList_c,
.aData = (uint8_t *)uuid_service_qpps
},
{
.adType = gAdShortenedLocalName_c,
.length = 8,
.aData = (uint8_t*)"NXP_QPPS"
}
};
详细格式请参照Core Spec中GAP章节以及CSS(核心规范补充)。
广播参数说明
typedef struct gapAdvertisingParameters_tag {
uint16_t minInterval;
uint16_t maxInterval;
bleAdvertisingType_t advertisingType;
bleAddressType_t ownAddressType
bleAddressType_t peerAddressType;
bleDeviceAddress_t peerAddress;
gapAdvertisingChannelMapFlags_t channelMap;
gapAdvertisingFilterPolicy_t filterPolicy;
} gapAdvertisingParameters_t;
成员 | 取值范围 | 说明 |
---|---|---|
minInterval | 0x20 - 0x4000 | 20ms - 10.24s(步进0.625ms),最小建议广播间隔 |
maxInterval | 0x20 - 0x4000 | 20ms - 10.24s(步进0.625ms),最大建议广播间隔 |
advertisingType | gAdvConnectableUndirected_c, gAdvDirectedHighDutyCycle_c, gAdvScannable_c, gAdvNonConnectable_c, gAdvDirectedLowDutyCycle_c | 四种广播策略(其中定向广播还分两种) |
ownAddressType | gBleAddrTypePublic_c 或 gBleAddrTypeRandom_c | 该参数决定广播包的地址类型 |
peerAddressType | gBleAddrTypePublic_c 或 gBleAddrTypeRandom_c | 该参数只在定向广播时有效,决定了定向广播包中填写的对方地址类型 |
peerAddress | 48位蓝牙地址 | 该参数只在定向广播时有效,决定了定向广播包中填写的对方蓝牙地址 |
channelMap | gAdvChanMapFlag37_c, gAdvChanMapFlag38_c,gAdvChanMapFlag39_c | 广播通道的bitmap |
filterPolicy | gProcessAll_c,gProcessConnAllScanWL_c,gProcessScanAllConnWL_c,gProcessWhiteListOnly_c | 广播白名单策略 |
扫描(scan)
协议栈涉及到扫描有3个API:
// 设置扫描策略
bleResult_t Gap_SetScanMode(gapScanMode_t scanMode,
gapAutoConnectParams_t* pAutoConnectParams);
// 开启扫描
bleResult_t Gap_StartScanning(gapScanningParameters_t* ScanningParameters,
gapScanningCallback_t scanningCallback,
bool_t enableFilterDuplicates);
// 停止扫描
bleResult_t Gap_StopScaning(void);
同广播API,他们也都是异步执行的。其中Gap_SetScanMode
是一个可选的API,用户可以在开启扫描之前通过这个函数来配置扫描的策略,以决定是否将所有扫描到的从设备都抛给应用层(默认),或者仅对‘LimitedDiscovery’或'GeneralDiscovery'的广播进行上报。同时还定义是否主机自动连接指定的从机,无需用户在应用层显示调用第三部分要介绍的Gap_Connect
动作,在此模式下不会上报被扫描的从机。枚举类型gapScanMode_tag的定义如下:
typedef enum gapScanMode_tag {
gDefaultScan_c,
gLimitedDiscovery_c,
gGeneralDiscovery_c,
gAutoConnect_c
} gapScanMode_t;
Gap_StartScanning
的参数是自解释的,主要工作也是在app_config.c
中填写扫描参数结构体ScanningParameters
,下面小节对每个参数作了说明。
扫描参数说明
typedef struct gapScanningParameters_tag {
bleScanType_t type;
uint16_t interval;
uint16_t window;
bleAddressType_t ownAddressType;
bleScanningFilterPolicy_t filterPolicy;
} gapScanningParameters_t;
成员 | 取值范围 | 说明 |
---|---|---|
type | gScanTypePassive_c 或 gScanTypeActive_c | 选择被动或者主动扫描 |
interval | 0x04 - 0x4000 | 2.5ms - 10.24s(步进0.625ms),两次扫描的间隔 |
window | 0x04 - 0x4000 | 2.5ms - 10.24s(步进0.625ms),每次扫描的持续事件,当window = interval时连续扫描 |
ownAddressType | gBleAddrTypePublic_c 或 gBleAddrTypeRandom_c | 该参数决定在Scan request时主机发出的包的地址类型 |
filterPolicy | gScanAll_c 或 gScanWithWhiteList_c | 是否启用Scan白名单 |
连接 (connect)
主设备在完成扫描流程后,获取了周围设备的列表和基本信息。如果要进一步与某一个设备进行用户数据交互则双方需要进入连接状态(非beacon的应用)。
建立(和断开)BLE连接只涉及到2个API和2个Event:
// 建立连接,仅主设备允许调用
bleResult_t Gap_Connect(gapConnectionRequestParameters_t* pParameters,
gapConnectionCallback_t connCallback);
// 断开连接,主从都可以调用
bleResult_t Gap_Disconnect(deviceId_t deviceId);
这两个API各自对应了一个'连接'和'断开连接'事件,通知应用层示连接已经建立或者已经断开:
事件Tag | 触发函数 | 回调函数 |
---|---|---|
gConnEvtConnected_c * | Gap_Connect | 连接回调函数BleApp_ConnectionCallback
|
gConnEvtDisconnected_c * | Gap_Disconnect | 连接回调函数BleApp_ConnectionCallback
|
注意:这两个事件并非只有当调用函数才会产生,如果对端是主设备与我们建立连接,作为从设备我们也会得gConnEvtConnected_c 。同样,如果对端主动与我们断开,或者链路因某些异常原因断开,协议栈也会抛出gConnEvtDisconnected_c事件。这种会被协议栈主动触发的事件在SDK中还有不少,我们可以称为异步事件。
在NXP BLE SDK开发时,Gap_Connect
的API被包在App_Connect
内的,用户一般是调用App_Connect
,她传入的连接回调函数会在用户线程上下文中被调用。在实时操作系统环境下,事件处理中即使有长时间占用CPU的行为(比如打印出事件参数),也不会对协议栈造成影响。这个我们在前面架构介绍中有讲解这个机制,有困惑的同学可以回去上一篇文章。
连接请求参数
调用Gap_Connect
时需要主设备填充一组连接参数,这组参数将由协议栈送给Controller,由Controller最终决定如何与对端设备建立连接(Controller将考虑其他连接的时序要求)
typedef struct gapConnectionRequestParameters_tag {
uint16_t scanInterval;
uint16_t scanWindow;
bleInitiatorFilterPolicy_t filterPolicy;
bleAddressType_t ownAddressType;
bleAddressType_t peerAddressType;
bleDeviceAddress_t peerAddress;
uint16_t connIntervalMin;
uint16_t connIntervalMax;
uint16_t connLatency;
uint16_t supervisionTimeout;
uint16_t connEventLengthMin;
uint16_t connEventLengthMax;
bool_t usePeerIdentityAddress;
} gapConnectionRequestParameters_t;
成员 | 取值范围 | 说明 |
---|---|---|
scanInterval | 0x04 - 0x4000 | 2.5ms - 10.24s(步进0.625ms),两次扫描的间隔 |
scanWindow | 0x04 - 0x4000 | 2.5ms - 10.24s(步进0.625ms),每次扫描的持续事件,当window = interval时连续扫描 |
filterPolicy | gUseDeviceAddress_c, gUseWhiteList_c | 是否启动Initiator白名单 |
ownAddressType | gBleAddrTypePublic_c 或 gBleAddrTypeRandom_c | 该参数决定在Connect request中自己的地址类型 |
peerAddressType | gBleAddrTypePublic_c 或 gBleAddrTypeRandom_c | 该参数决定在Connect request时被连接设备的地址类型 |
peerAddress | 48位蓝牙地址 | 该参数决定在Connect request时被连接设备的地址 |
connIntervalMin | 0x06 - 0x0C80 | 7.5ms - 4s(步进1.25ms)建议最小的连接间隔 |
connIntervalMax | 0x06 - 0x0C80 | 7.5ms - 4s(步进1.25ms)建议最大的连接间隔 |
connLatency | 0 - 499 | 从机可忽略的连接事件数量 |
supervisionTimeout | 0x0A - 0x0C80 | 100ms - 32s(步进10ms)最大超时断链间隔 |
connEventLengthMin | 0 - 0xFFFF | 连接事件最小长度 |
connEventLengthMax | 0 - 0xFFFF | 连接事件最大长度 |
usePeerIdentityAddress | TRUE or FALSE | 当Controller Privacy开启时是否使用 |
NXP BLE SDK作为主设备类型的例程在app_config.c
都会给出一个连接参数配置gConnReqParams
,用户直接根据自己需求修改即可,peerAddress
和peerAddressType
两个参数则来自于扫描得到的数据。至于主设备连接哪一个扫描到的从设备,这完全是用户的定义的行为了,通过广播数据的字段来判断是一种常用的做法。
同时广播与扫描
有的产品担任双角色,既可以作为主设备扫描并连接其他从设备,同时也可以作为从设备,广播被其他主设备发现并连接。甚至需要同一时刻做这两件事(同时广播和扫描)。
虽然通常蓝牙芯片只有一个RF端口,但广播和扫描这两项功能一个是只发送数据(Tx),一个仅接收数据(Rx),因此射频上不存在问题。从BLE 4.1协议规范后,定义了BLE芯片在链路层支持多个状态机,这样就完整的支持了同时进行广播和扫描,以及建立多个连接,和混合的拓扑结构。
在NXP BLE SDK中实现这个功能是非常简单的,首先确认使用的协议栈库文件是支持主从设备的,而非仅支持从设备(peripheral)的库,然后按照文章前面介绍的广播与扫描API调用和Event处理流程操作即可,二者并不冲突。
连接之后
建立完连接只是万里长征第一步,连接建立后双方通常还不能直接进行用户数据交互,后面还要进行一系列BLE协议规范所要求的过程,如配对加密,客户端对服务器端的服务进行查询,若知后事如何,请听下回分解。