在一个连接当中,主设备会在每个连接事件里向从设备发送数据包。一个连接事件是指主设备和从设备之间相互发送数据包的过程。连接事件的进行始终位于一个频率,每个数据包会在上个数据包发完之后等待 150μs 再发送。
连接间隔决定了主设备与从设备的交互间隔;它是指两个连续的连接事件开始处的时间距离,可以是7.5ms ~ 4s内的任意值,但必须为 1.25ms 的整数倍。要确定从设备与主设备的实际交互间隔,需要用到从设备延迟这一参数,代表从设备在必须侦听之前可以忽略多少个连接事件。
如下图所示,连接事件被一个个的连接间隔分开。从主设备发送数据包开始,每个连接事件可以持续进行,直至主设备或从设备停止响应。在连接事件之外,主从设备之间不发送任何数据包。
举个例子,如果连接间隔为 100ms,从设备延迟是 9,那么从设备可以忽略 9 个链接事件,但不得不侦听第 10 个连接事件。换言之,从设备必须每秒侦听一次,而此时监控超时的最小值应为 1010ms。反过来,另一个极端的例子是,如果监控超时使用了 32s 的最大值,对于间隔为 100ms 的链路,从设备延时必须小于等于 319。
虽然如此,如果将从设备延迟设为可行的最大值,在监控超时发生前从设备只能获得唯一一次侦听主设备的机会,这可不是一个好主意。因此,建议至少给从设备留出 6 次侦听的机会。在前面的例子中,如果连接间隔为 100ms ,从设备延迟为 9,那么监控超时应该至少为 6s,这样一来,链路在最终断开前从设备至少会有 6 次侦听的机会。
主设备和从设备建立连接之后,所有的数据通信都是在连接事件(Connection Events)中进行的。
尖刺的波就是连接事件(Connection events),剩下的Sleeping是睡眠时间,设备在建立连接之后的大多数时间都是处于Sleeping,这种情况下耗电量比较低,而在连接事件(Connection events)中,耗电量就相对高很多,这也是BLE为什么省电的原因之一。
每个连接事件(Connection events)中,都需要由Master发起包,再由Slave回复。
Master即主机,简称M;Slave即从机,简称S。抓包过程中看到的M->S或者S->M即主机到从机或者从机到主机。
通过修改下面三个参数,就可以设置BLE连接过程中的传输速度和功耗。
1.Connection Interval(连接间隔)
Connection Interval(GAPROLE_MIN_CONN_INTERVAL && GAPROLE_MAX_CONN_INTERVAL)连接间隔,在BLE的两个设备的连接中使用跳频机制。两个设备使用特定的信道发送和接收数据,然后过一段时间后再使用新的信道(BLE协议栈的链路层处理信道的切换)。两个设备在切换信道后发送和接收数据称为一个连接事件。尽管没有应用数据被发送和接收,两个设备仍旧会交换链路层数据(空包 Empty PDU)来维持连接。
这个连接间隔就是指在一个连接事件(Connection events)的开始到下一个连接事件(Connection events)的开始的时间间隔。连接间隔以1.25ms为单元,连接间隔的范围是6 ~ 3200既7.5ms ~ 4s之间。
2.Slave Latency(从设备延迟或者从设备时延)
允许Slave(从设备)在没有数据要发的情况下,跳过一定数目的连接事件(Connection events),在这些连接事件(Connection events)中不必回复Master(主设备)的包,这样就能更加省电。
范围可以是0 ~ 499
更详细的使用解析如下:
Slave Latency = OFF也就是Slave Latency为0时,Master发包,Slave必须回复,如果不回复,Master就会认为Slave那边接收不正常。
Slave Latency = ON也就是Slave Latency不为0的时候,图中Slave Latency为 3。Master发包,Slave没有数据要回复的时候,就会忽略 3 个连接事件,在第 4 个连接事件接收到Master发送的数据之后,回复Master。如果Slave有数据要发送就会唤醒,也就是说即使Slave Latency为 3,但是在Master发第二包的时候Slave有数据要回复,这个时候就会立即回复Master而不是等到 3 个连接事件之后的第 4 个连接事件去回复。
3.Supervision Timeout(超时时间或者监控超时)
这个参数设定了一个超时时间,如果BLE在这个时间内没有发生通信的话,就会自动断开。
单位是 10ms,该变量的范围是10 ~ 3200,折算成时间范围是100ms ~ 32s 。
连接间隔、从机时延以及超时时间这三者必须满足如下公式:
S u p e r v i s i o n T i m e o u t > ( 1 + s l a v e L a t e n c y ) ∗ ( c o n n e c t i o n I n t e r v a l ) Supervision Timeout > (1 +slaveLatency)* (connectionInterval) SupervisionTimeout>(1+slaveLatency)∗(connectionInterval)
上述公式必须满足,否则连接就会不正常断开。
这三个连接参数不同情况下对通信速率和功耗的影响:
Connection Interval缩短,Master和Slave通信更加频繁,提高数据吞吐速度,缩短了数据发送的时间,当然也增加了功耗。
Connection Interval增长,通信频率降低,数据吞吐速度降低,增加了数据发送的时间,当然,这种设置降低了功耗。
Slave Latency减少或者设置为 0,每次Connection Events中都需要回复Master的包,功耗会上升,数据发送速度会提高。
Slave Latency加长,功耗下降,数据发送速度降低。
连接建立时,主设备通过链接请求数据包发送连接参数。当连接活跃了一段时间,连接参数也许不再适用于当前使用的服务。出于提高效率的目的,连接参数需要进行更新。较之首先断开连接、接着更换新参数重新连接,还有一种在链路中更新参数更为简单的途径,如下图所示:
为此,主设备向从设备发送连接更新请求,即LL_CONNECTION_UPDATE_REQ,当中携带了新的参数。这些参数不必进行协商,从设备或者接受和使用它们,或者断开链路。连接更新请求中包含了早先创建连接时用过的一部分参数,还有一个称为瞬时(instant)的新参数:
1. 传输窗口大小
2. 传输窗口偏移量
3. 连接间隔
4. 从设备延迟
5. 监控超时
6. 瞬时
瞬时参数决定了连接更新的开始时刻。发送消息时,主设备为连接更新选定一个未来的时间点,并且放在消息中。接到消息后,从设备会记住这个未来的时刻,届时再切换至新的连接参数。这有助于解决无线系统里的一个最大问题----报文重传。只要数据包的重传次数足够,并最终在瞬时之前传输成功,上述过程执行起来就不会有问题。但是,如果该数据包届时没能完成传输,链路就有可能丢失。
由于低功耗蓝牙没有时钟,要决定瞬时时刻只有依靠计算连接事件的个数。因此,每一个连接事件都会被计数,链路上的第一个连接事件,也就是在连接请求之后的位于首个传输窗口里的连接事件记为 0。因此,瞬时实际上是一个连接事件的计数器,相应的连接事件到来时就使用新的参数。为了让从设备收到数据包,主设备必须为其提供足够的机会。不过从设备延迟是多少,都应该至少保证 6 次数据发送机会。也就是说,如果从设备延迟为 500ms,那么瞬时通常被设定在 3s 之后的某个未来时刻。
瞬时到来时,从设备开始侦听发送窗口,就好像连接建立的过程那样。主设备能够调整从设备的计时,总体而言不超过 1.25ms。不过,由于主设备可能还是一个经典蓝牙设备,上述调整使其得以协调低功耗蓝牙从设备,从而更好地完成调度。一旦该过程结束,新的连接间隔、监控超时、从设备延迟值将投入使用。
“连接参数更新请求”命令可以让从设备更新链路层的连接参数,如下图所示。这些参数包括连接间隔(从设备希望主设备允许从设备发送数据包的频率)、从设备延迟(从设备能够忽略主设备的连接事件的最大值)以及监控超时。
在连接中,如果从设备希望修改当前的连接参数则可以使用该命令。比方说,如果连接事件的间隔有可能太快了,导致过多的电量浪费。这在从设备时延很大时没有问题,但如果不是这样,从设备将会频繁的侦听链路。这在一些情况下是必要的,例如设备间首次绑定、互发多个数据包、探索服务和设备特性等。但在很多其他情况下,尽可能地减少从设备必须侦听连接事件的数量对提高电池寿命至关重要。
连接参数更新请求命令仅用于从设备向主设备发送,这是由于主设备随时都能启动链路层连接参数更新控制(Connection Parameter Update Control)规程。如果该命令由主设备发送,从设备会将其视为一个错误,并返回带有“命令不理解”原因代码的“命令拒绝”命令。
从设备可以在任何时候发送该命令;收到该信息的主设备如果可以修改连接参数,则将返回“连接参数更新响应”(Connection Parameter Update Response),其中的结果代码设为“接受(accepted)”。随后,主设备将会启动链路层连接参数更新控制规程。
当然,如果主设备不同意从设备的请求参数,它可以发送结果代码为“拒绝(rejected)”的连接参数更新响应命令以拒绝请求。此时从设备有两个选择:要么接受主设备希望的正在使用的连接参数,要么终止连接。终止连接的做法咋看起来可能让人觉得很激进,但是,假如使用当前的参数从设备将会在一周内耗尽电量,而使用请求的参数则可以持续数年,很明显,合理的选择只有一个。
修改连接参数时,如果要减少主设备拒绝从设备请求的可能性,可以在请求里设置一个可接受的参数范围。经过精心设计的从设备会乐意接受很宽的参数范围。由于主设备可能正忙于实时会话音频连接或者高质量语音连接等任务,它可以接受一定范围内的连接间隔参数。设备可接受的间隔参数会根据当前任务的不同而不同,可能有别于上一次设备连接时的参数。
要提高主设备接受连接参数的机率,还有个方法是从设备提供一个合理的从设备延迟。主设备可以选择最合适的连接事件间隔,从设备则使用最佳功耗的从设备延迟参数。
举个例子,如果从设备想每 600ms 同步一次,它可以请求范围 100ms ~ 750ms 的连接间隔参数,并带上从设备延迟5。如果主设备选择 100ms,则从设备每6个连接事件同步一次;如果主设备选择 200ms,则从设备每 3 个连接事件同步一次,实现其所期望的 600ms 间隔;如果主设备选择 300ms,则从设备忽略每隔一个连接事件同步一次;如果主设备选择 400ms,则从设备每 400ms 同步一次。
本文特别之处
下面介绍一下在TI的CC2540和CC2541中,连接参数修改的方法。
(一)连接成功建立之后从设备自动申请修改连接参数。
我们以TI 1.4.0协议栈中的“simpleBLEPeripheral”工程为例来进行讲解,在这个工程的“Projects\ble\SimpleBLEPeripheral\Source\simpleBLEPeripheral.c”应用文件中定义了如下的宏:
// Whether to enable automatic parameter update request when a connection is formed
#define DEFAULT_ENABLE_UPDATE_REQUEST TRUE
从上面的注释中,我们可以看出这个宏的作用是当一个连接建立的时候,是否需要自动申请连接参数更新。当设置为“TRUE”的时候就是需要,当设置为“FALSE”的时候就是不需要。那这个宏到底是如何起作用的呢?下面我们来看一下。
uint8 enable_update_request = DEFAULT_ENABLE_UPDATE_REQUEST;
GAPRole_SetParameter( GAPROLE_PARAM_UPDATE_ENABLE, sizeof( uint8 ), &enable_update_request );
case GAPROLE_PARAM_UPDATE_ENABLE:
if ( (len == sizeof ( uint8 )) && (*((uint8*)pValue) <= TRUE) )
{
gapRole_ParamUpdateEnable = *((uint8*)pValue);
}
else
{
ret = bleInvalidRange;
}
break;
// 连接成功建立之后底层返回的事件
case GAP_LINK_ESTABLISHED_EVENT:
{
gapEstLinkReqEvent_t *pPkt = (gapEstLinkReqEvent_t *)pMsg;
if ( pPkt->hdr.status == SUCCESS )
{
VOID osal_memcpy( gapRole_ConnectedDevAddr, pPkt->devAddr, B_ADDR_LEN );
gapRole_ConnectionHandle = pPkt->connectionHandle;
gapRole_state = GAPROLE_CONNECTED;
if ( gapRole_RSSIReadRate )
{
// Start the RSSI Reads
VOID osal_start_timerEx( gapRole_TaskID, RSSI_READ_EVT, gapRole_RSSIReadRate );
}
// Store connection information
// 保存连接刚建立时的连接参数
gapRole_ConnInterval = pPkt->connInterval;
gapRole_ConnSlaveLatency = pPkt->connLatency;
gapRole_ConnTimeout = pPkt->connTimeout;
// Check whether update parameter request is enabled
// 检测更新连接参数请求是否被使能
if ( gapRole_ParamUpdateEnable == TRUE )
{
// Get the minimum time upon connection establishment before the
// peripheral can start a connection update procedure.
// 获取设置的时间间隔,从机将在连接建立之后
// 延时至少该时间间隔之后触发连接参数更新事
// 件。
uint16 timeout = GAP_GetParamValue( TGAP_CONN_PAUSE_PERIPHERAL );
// 在延时timeout*1000 ms之后触发连接参数更新事件
osal_start_timerEx( gapRole_TaskID, START_CONN_UPDATE_EVT, timeout*1000 );
}
上面的注释非常清楚了,在连接成功建立返回的事件中判断我们设置的宏,如果设置为“TRUE”,那就获取我们设置的时间间隔,在延时我们设置的时间间隔(上面注释中提到至少,因为用的是系统定时器,有可能在执行别的事件,所以实际的延时时间会大于我们设置的时间,当然,一般情况下偏移的那点时间是可以忽略的)之后,触发连接参数更新事件,进行连接参数的更新。那上面源码中获取的时间间隔以及后面要更新的连接参数是在什么地方设置的呢?下面我们继续回到应用层文件中查看相关设置。
在“Projects\ble\SimpleBLEPeripheral\Source\simpleBLEPeripheral.c”文件中定义了如下宏用来设置自动更新连接参数时,相关连接参数的值,源码如下:
// Minimum connection interval (units of 1.25ms, 80=100ms) if automatic parameter update request is enabled
// 如果自动更新连接参数请求被使能的话,用到的最小连接间隔,单位1.25 ms
#define DEFAULT_DESIRED_MIN_CONN_INTERVAL 80
// Maximum connection interval (units of 1.25ms, 800=1000ms) if automatic parameter update request is enabled
// 如果自动更新连接参数请求被使能的话,用到的最大连接间隔,单位1.25 ms
#define DEFAULT_DESIRED_MAX_CONN_INTERVAL 800
// Slave latency to use if automatic parameter update request is enabled
// 如果自动更新连接参数请求被使能的话,用到的从机时延
#define DEFAULT_DESIRED_SLAVE_LATENCY 0
// Supervision timeout value (units of 10ms, 1000=10s) if automatic parameter update request is enabled
// 如果自动更新连接参数请求被使能的话,用到的超时时间,单位10 ms
#define DEFAULT_DESIRED_CONN_TIMEOUT 1000
// Connection Pause Peripheral time value (in seconds)
// 如果自动更新连接参数请求被使能的话,用到的时间间隔,单位s
#define DEFAULT_CONN_PAUSE_PERIPHERAL 6
VOID GAP_SetParamValue( TGAP_CONN_PAUSE_PERIPHERAL, DEFAULT_CONN_PAUSE_PERIPHERAL );
uint16 desired_min_interval = DEFAULT_DESIRED_MIN_CONN_INTERVAL;
uint16 desired_max_interval = DEFAULT_DESIRED_MAX_CONN_INTERVAL;
uint16 desired_slave_latency = DEFAULT_DESIRED_SLAVE_LATENCY;
uint16 desired_conn_timeout = DEFAULT_DESIRED_CONN_TIMEOUT;
GAPRole_SetParameter( GAPROLE_MIN_CONN_INTERVAL, sizeof( uint16 ), &desired_min_interval );
GAPRole_SetParameter( GAPROLE_MAX_CONN_INTERVAL, sizeof( uint16 ), &desired_max_interval );
GAPRole_SetParameter( GAPROLE_SLAVE_LATENCY, sizeof( uint16 ), &desired_slave_latency );
GAPRole_SetParameter( GAPROLE_TIMEOUT_MULTIPLIER, sizeof( uint16 ), &desired_conn_timeout );
上述操作在“Projects\ble\Profiles\Roles\peripheral.c”文件里的具体实现我们就不一起看了,因为里面其实就是一个赋值的过程,所以大家自行查看即可。
(二)连接成功建立之后从设备在需要的时候去修改某个连接参数或者全部的连接参数。
uint16 desired_min_interval = DEFAULT_DESIRED_MIN_CONN_INTERVAL;
GAPRole_SetParameter( GAPROLE_MIN_CONN_INTERVAL, sizeof( uint16 ), &desired_min_interval );
uint16 desired_max_interval = DEFAULT_DESIRED_MAX_CONN_INTERVAL;
GAPRole_SetParameter( GAPROLE_MAX_CONN_INTERVAL, sizeof( uint16 ), &desired_max_interval );
uint16 desired_slave_latency = DEFAULT_DESIRED_SLAVE_LATENCY;
GAPRole_SetParameter( GAPROLE_SLAVE_LATENCY, sizeof( uint16 ), &desired_slave_latency );
uint16 desired_conn_timeout = DEFAULT_DESIRED_CONN_TIMEOUT;
GAPRole_SetParameter( GAPROLE_TIMEOUT_MULTIPLIER, sizeof( uint16 ), &desired_conn_timeout );
uint16 minConnInterval;
uint16 maxConnInterval;
uint16 slaveLatency;
uint16 timeoutMultiplier;
// Update connection parameters
GAPRole_SendUpdateParam( minConnInterval, maxConnInterval, slaveLatency, timeoutMultiplier, GAPROLE_TERMINATE_LINK);
GAPRole_SendUpdateParam传入的前四个参数在之前都已经介绍过了,下面介绍下最后一个参数,最后一个参数设置的是连接参数更新失败后的操作,可取值定义在peripheral.h文件中,如下:
/**
* Possible actions the peripheral device may take if an unsuccessful parameter
* update is received.
*
* Parameters for GAPRole_SendUpdateParam() only
*/
#define GAPROLE_NO_ACTION 0 // Take no action upon unsuccessful parameter updates
#define GAPROLE_RESEND_PARAM_UPDATE 1 // Continue to resend request until successful update
#define GAPROLE_TERMINATE_LINK 2 // Terminate link upon unsuccessful parameter updates
GAPROLE_NO_ACTION:没有任何动作
GAPROLE_RESEND_PARAM_UPDATE:重新发送参数更新请求
GAPROLE_TERMINATE_LINK:断开连接
对于上述介绍的两种修改连接参数的方法,个人建议还是采用第二种方法,因为第一种方法在修改某一个参数的时候,可能会导致其他参数的变化,比如我们只修改了连接间隔,但从机延时可能会随之改变,这样我们就无法根据自己的需求控制连接参数的更新。
第二种方法在使用的时候有可能碰到一种情况,就是我们只想修改某一个或者某两个连接参数,剩下的参数想保持原有的,这样的话,我们需要在修改连接参数之前先去读取连接参数,然后将需要修改的参数进行重新设置即可,读取连接参数的代码如下:
uint16 interval;
uint16 latency;
uint16 timeout;
GAPRole_GetParameter(GAPROLE_CONN_INTERVAL, &interval);
GAPRole_GetParameter(GAPROLE_CONN_LATENCY, &latency);
GAPRole_GetParameter(GAPROLE_CONN_TIMEOUT, &timeout);
下面我们通过一个实例来具体了解下连接参数修改的方法,需求是将连接间隔修改为25,从机延迟修改为8,超时时间不修改,更新失败后重新发送参数更新请求,本实例中主设备是安卓设备,从设备是CC2541。代码实现如下:
uint16 interval;
uint16 latency;
uint16 timeout;
GAPRole_GetParameter(GAPROLE_CONN_INTERVAL, &interval);
GAPRole_GetParameter(GAPROLE_CONN_LATENCY, &latency);
GAPRole_GetParameter(GAPROLE_CONN_TIMEOUT, &timeout);
GAPRole_SendUpdateParam( 25, 25, 8, timeout, GAPROLE_RESEND_PARAM_UPDATE);
该过程抓包显示如下:
从抓到的包中我们看到首先是S->M,即从设备发送连接参数更新请求,请求中带有申请的连接参数,然后M->S,即主设备返回连接参数更新响应,Result为0,表示同意修改更新。最后M->S发送Data type为Control的链路层连接参数更新控制规程,携带同意的连接参数,这样,新的连接参数就会投入使用。
注意修改连接参数的时候要满足一定的要求:
安卓设备作主设备时,连接参数满足的要求见本篇博文第二节“连接参数介绍”中提到的内容。另外实际开发过程中发现安卓设备作主设备时存在一个问题,就是部分安卓设备连接BLE设备之后,只能进行一次连接参数的修改。
苹果系统设备作主设备时,连接参数更新的要求比较苛刻,如下:
- Interval Max * (Slave Latency + 1) ≤ 2 seconds
- Interval Min ≥ 20 ms
- Interval Min + 20 ms ≤ Interval Max
- Slave Latency ≤ 4
- connSupervisionTimeout ≤ 6 seconds
- Interval Max * (Slave Latency + 1) * 3 < connSupervisionTimeout
即:
最大连接间隔时间 *(从机延迟 + 1) ≤ 2s
最小连接间隔时间 ≥ 20 ms
最小连接间隔时间 + 20 ms ≤ 最大连接间隔时间
从机延迟 ≤ 4
超时时间 ≤ 6s
最大连接间隔时间 *(从机延迟 + 1)* 3 < 超时时间
所以如果你的BLE从设备需要被IOS主设备连接,那你的BLE从设备的默认申请的连接参数一定要满足上述要求,并且连接过程中修改连接参数的时候也要满足上述要求。
参考资料
推荐阅读(点击标题可跳转阅读)
[1] 深入浅出低功耗蓝牙(BLE)协议栈,使用Ubertooth one扫描嗅探低功耗蓝牙
[2] BLE 5协议栈-直接测试模式
MaiweiE-com | WeChat ID:Yida_Zhang2
机器学习+智能控制