B91系列BLE多连接SDK
SDK介绍
Telink B91m BLE Multiple Connection SDK 提供基于 B91m 系列 MCU 的 BLE 多连接应用的参考代码,用户可以在此基础上开发自己的应用程序。其中的 Multiple 指多个(大于等于1)Central 或 Peripheral 角色共存,比如自身同时作为 4 个 Central 和 3 个 Peripheral(简称C4P3)。 除了 BLE Stack 部分,Telink B91m BLE Multiple Connection SDK 中其他功能模块(比如Flash、时钟、GPIO、IR等)都和 Telink B91 BLE Single Connection SDK 是一样的,为了避免这些模块的重复介绍,请用户同时准备好对应的 Telink B91 BLE Single Connection SDK Handbook。本文档在后面每一个模块的介绍时都会具体说明当前模块是否和 Telink B91 BLE Single Connection SDK Handbook 中完全一致,如不一致也会详细介绍差异在哪里。
需要准备的 Telink BLE B91 Single Connection SDK handbook链接如下:
说明:
此说明是关于该 Handbook 中的命名更新的说明。2021 年 9 月,蓝牙技术联盟(SIG)正式将 从机 Slave 更名为 Peripheral,主机 Master 更名为 Central,同时还有一些其他术语的命名修改(详情参考 SIG 官方文档 《Appropriate Language Mapping Table》)。所以 Telink B91m BLE Multiple Connection SDK 和 Handbook 也同步开始了名称替换进程。主要的替换列表包括但不限于:
旧名称 | 新名称 | 相关修改 |
---|---|---|
Slave | peripheral/periphr | 其他代表从机的缩写,如s变p,per |
Master | central | 其他代表主机的缩写,如m变c,cen |
White List | Filter Accept List |
当前 Handbook 中的命名与 Telink B91m BLE Multiple Connection SDK V4.1.0.0 中的命名保持同步,我们努力保证新的命名在整个 Handbook 中得到了全面的更新,但仍然存在混用的情况,还请谅解。
同时,为了与 Telink B91m BLE Multiple Connection SDK V4.1.0.0 之前的版本保持兼容,我们仍然允许使用旧的名字,可以参考 sdk/stack/ble/controller/contr_comp.h 和 sdk/stack/ble/host/host_comp.h。虽然保证了旧版本 SDK 的兼容性 ,但我们仍然建议用户使用新版本 SDK,并开始更名,一方面长期来看我们的 SDK 最终会全部更名,并弃用旧的名字,另一方面,从蓝牙协议上讲,新的名字更“友善”和“规范”。
我们深刻意识到这可能对使用者造成的混淆和不便,我们将继续推进多个 SDK 和 Handbook 的命名更新进程。感谢您的阅读和理解。
适用IC
Telink B91m BLE Multiple Connection SDK 适用B91 和 B92 系列的 MCU。需要注意在使用不同MCU时,要选择不同的bootloader文件(bootloader文件在1.3节讲解)。
软件组织架构
Telink B91m BLE Multiple Connection SDK 软件架构包括 应用层 和 BLE Stack 两部分。
文件结构
在 IDE 中导入 Telink B91m BLE Multiple Connection SDK 后,显示的文件组织结构如下图所示(以 B91 为例)。顶层文件夹有8个:algorithm,application,boot,common,drivers,proj_lib,stack,vendor。
官方推荐IDE: B91 系列使用 Andes Telink_DRS, B92 系列使用 Telink IoT Studio。
algorithm:提供一些通用的算法,如aes_ccm。大多数算法对应的C文件被封装在库文件中,只留对应的头文件。
application:提供一些通用的应用处理程序,如print、keyboard等。
boot:提供MCU的software bootloader,即MCU上电启动或deep sleep唤醒后的汇编处理程序,为后面C程序的运行做准备。
common:提供一些通用的跨平台的处理函数,如内存处理函数、字符串处理函数等。
drivers:提供MCU外设驱动程序,如Clock、Flash、I2C、USB、GPIO、UART等。
proj_lib:存放SDK运行所必需的库文件。BLE协议栈、RF驱动、PM驱动等文件,被封装在库文件中。
stack:存放BLE协议栈相关的头文件。源文件被编译到库文件里面,对于用户不可见。
vendor:用于存放demo code或者用户自己的code。
Telink B91m BLE Multiple Connection SDK 提供了4个demo,包括feature_test、acl_central_demo、acl_connection_demo、acl_peripheral_demo。这些demo工程在vendor文件夹下,如下图所示。
acl_peripheral_demo默认配置支持suspend mode和deepsleep retention mode,其余demo默认配置关闭功耗管理,如果希望支持deepsleep retention mode请参考acl_peripheral_demo。
以acl_connection_demo为例来讲解demo文件结构,文件构成如下图所示:
main.c
main.c 文件中包含 main 函数和中断处理函数。main函数是程序执行的入口,包含了系统正常工作所需的配置,建议用户不要对它进行任何修改。中断处理函数是系统触发中断时候的入口函数。其中 Telink B91m BLE Multiple Connection SDK 支持主频(CCLK)最低为 32M,最高为 96M。
_attribute_ram_code_ int main(void)
{
/* this function must called before "sys_init()" when:
* (1). For all IC: using 32K RC for power management,
(2). For B91 only: even no power management */
blc_pm_select_internal_32k_crystal();
#if (MCU_CORE_TYPE == MCU_CORE_B91)
sys_init(DCDC_1P4_LDO_1P8, VBAT_MAX_VALUE_GREATER_THAN_3V6);
#elif (MCU_CORE_TYPE == MCU_CORE_B92)
sys_init(DCDC_1P4_LDO_2P0, VBAT_MAX_VALUE_GREATER_THAN_3V6, GPIO_VOLTAGE_3V3);
wd_32k_stop();
#endif
/* detect if MCU is wake_up from deep retention mode */
int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup(); //MCU deep retention wakeUp
CCLK_32M_HCLK_32M_PCLK_16M; //时钟初始化
rf_drv_ble_init(); //RF初始化
gpio_init(!deepRetWakeUp); //GPIO初始化
if( deepRetWakeUp ){ //MCU wake_up from deepSleep retention mode
user_init_deepRetn ();
}
else{ //MCU power_on or wake_up from deepSleep mode
user_init_normal();
}
irq_enable();
while(1)
{
main_loop ();
}
return 0;
}
app_config.h
这是用户配置文件,用于对整个系统的相关参数(例如:BLE参数,GPIO配置,低功耗使能/失能,加密使能/失能等)进行配置。
后面介绍各个模块时会对app_config.h中的各个参数的含义进行详细说明。
application file
app.c:用户主文件,用于完成BLE系统初始化、数据处理、低功耗处理等。
app_att.c:这个文件提供了GATT service表和profile文件,GATT service表中已提供了标准的GATT服务、标准的GAP服务、标准的HID服务以及一些私有服务等。用户可以参考这些添加自己的service和profile。
app_ui.c:该文件主要提供了按键功能。
app_buffer.c:该文件用于定义stack各层使用的buffer,例如:LinkLayer TX & RX buffer、L2CAP layer MTU TX & RX buffer、HCI TX & RX buffer等。
common file
路径在vendor/common。
blt_soft_timer.c:该文件提供了软件定时器的实现方案。
custom_pair.c:该文件提供了泰凌自定义的一套pair的方案。
device_manage.c:该文件主要是连接设备信息的管理(例如:connection handle, attribute handle, BLE device address, address type等)。这些信息是用户在开发应用时需要使用的。
simple_sdp.c:该文件提供了Central role简单的SDP(Service Discovery Protocol)实现方案。
BLE stack entry
BLE 中断处理入口函数是 blc_sdk_irq_handler()。
BLE 逻辑和数据处理入口函数是 blc_sdk_main_loop (),它负责处理BLE协议栈相关的数据和事件。
Software bootloader介绍
Telink B91m BLE Multiple Connection SDK 中,不同型号的MCU对应不同的bootloader文件。bootloader文件存放在boot文件夹下。Telink的bootloader文件是由两部分构成,link文件和cstartup.S汇编文件。
B91多连接的bootloader文件
B91 的 bootloader 文件如下图所示:
默认运行的是 cstartup_B91.S及boot_general.link文件,此时SDK会占用I-SRAM及D-SRAM空间,I-SRAM中包括retention_reset、aes_data、retention_data、ramcode、和unused I-SRAM area。D-SRAM中包括data、sbss、bss、heap、unused D-SRAM area和stack。
如果想把128K的D-SRAM空间全部留给用户使用,需要做如下更改。
(1) 将cstartup_B91.S文件开始的#if 1改成#if 0。
(2) 将cstartup_B91_DLM.S文件开始的#if 0改成#if 1。
(3) 将B91_ble_multiconn_sdk/boot/B91/boot_DLM.link的内容复制到B91_ble_multiconn_sdk/boot.link文件中。
B92多连接的bootloader文件
B92 的 bootloader 文件如下图所示:
注意:
Telink B91m BLE Multiple Connection SDK中B91默认配置deepsleep retention 64K mode, B92默认配置deepsleep retention 96K mode。
library介绍
下图为Telink B91m BLE Multiple Connection SDK 的proj_lib文件夹目前已经提供的library。
Telink B91m BLE Multiple Connection SDK B91所有的demo都使用liblt_B91.a, B92所有的demo都使用liblt_B92.a。
MCU基础模块
MCU地址空间
MCU地址空间分配
请参考Telink B91 BLE Single Connection SDK Handbook 对应章节的介绍。
SRAM空间分配
请参考Telink B91 BLE Single Connection SDK Handbook对应章节的介绍。
MCU地址空间访问
请参考Telink B91 BLE Single Connection SDK Handbook对应章节的介绍。
SDK Flash空间的分配
多连接采用自动读Flash size进行Flash配置的方法,无需用户根据自己产品实际情况手动配置Flash size。 Flash存储信息以一个sector的大小(4K byte)为基本的单位,因为Flash的擦除是以sector为单位的(擦除函数为flash_erase_sector),理论上同一类的信息需要存储在一个sector里面,不同类的信息需要存放在不同的sector(防止擦除信息时将其他类的信息误擦除)。所以建议user在使用Flash存储定制信息时遵循“不同类信息放在不同sector”的原则。
Telink B91m BLE Multiple Connection SDK 中有四类信息需要存储在Flash中,分别是MAC,校准信息,加密配对信息和SDP信息。这些参数在SDK中都默认分配了不同的Flash空间的。
MAC和校准信息存放的Flash空间会随芯片Flash的大小不同而不同,默认情况下,对于1M Flash的芯片来说,MAC存放在0xFF000开始的4K Flash空间中,校准信息存放在0xFE000开始的4K Flash空间中。对于2M Flash的芯片来说,MAC存放在0x1FF000开始的4K Flash空间中,校准信息存放在0x1FE000开始的4K Flash空间中。
/**************************** 1 M Flash *******************************/
#ifndef CFG_ADR_MAC_1M_FLASH
#define CFG_ADR_MAC_1M_FLASH 0xFF000
#endif
#ifndef CFG_ADR_CALIBRATION_1M_FLASH
#define CFG_ADR_CALIBRATION_1M_FLASH 0xFE000
#endif
......
/**************************** 2 M Flash *******************************/
#ifndef CFG_ADR_MAC_2M_FLASH
#define CFG_ADR_MAC_2M_FLASH 0x1FF000
#endif
#ifndef CFG_ADR_CALIBRATION_2M_FLASH
#define CFG_ADR_CALIBRATION_2M_FLASH 0x1FE000
#endif
加密配对信息和SDP信息也存储在独立的Flash空间上,默认情况下,对于1M Flash的芯片来说,加密配对信息存放在0xF4000开始8K Flash空间中,SDP信息存放在0xF2000开始的8K Flash空间中。对于2M Flash的芯片来说,加密配对信息存放在0x1EC000开始8K Flash空间中,SDP信息存放在0x1EA000开始的8K Flash空间中。
SDK能根据Flash大小自动配置相应的MAC和校准值存储空间。用户也可以根据自己的需要修改vendor/common/ble_flash.h中的相应的宏来修改默认的MAC和校准值存储空间,此时需要注意是要相应的修改泰凌量产治具上的烧写地址。
#ifndef FLASH_SMP_PAIRING_MAX_SIZE
#define FLASH_SMP_PAIRING_MAX_SIZE (2*4096)
#endif
#ifndef FLASH_SDP_ATT_MAX_SIZE
#define FLASH_SDP_ATT_MAX_SIZE (2*4096)
#endif
......
/* SMP pairing and key information area */
#ifndef FLASH_ADR_SMP_PAIRING_1M_FLASH
#define FLASH_ADR_SMP_PAIRING_1M_FLASH 0xF4000 //F4000 & F5000 & F6000 & F7000
#endif
/* for ACL Central simple SDP: bonding ACL Peripheral GATT service critical information area */
#ifndef FLASH_SDP_ATT_ADDRESS_1M_FLASH
#define FLASH_SDP_ATT_ADDRESS_1M_FLASH 0xF2000 //F2000 & F3000
#endif
......
/* SMP pairing and key information area */
#ifndef FLASH_ADR_SMP_PAIRING_2M_FLASH
#define FLASH_ADR_SMP_PAIRING_2M_FLASH 0x1EC000 //1EC000 & 1ED000 & 1EE000 & 1EF000
#endif
/* for ACL Central simple SDP: bonding ACL Peripheral GATT service critical information area */
#ifndef FLASH_SDP_ATT_ADDRESS_2M_FLASH
#define FLASH_SDP_ATT_ADDRESS_2M_FLASH 0x1EA000 //1EA000 & 1EB000
#endif
时钟模块
请参考Telink B91 BLE Single Connection SDK Handbook对应章节的介绍。
多连接SDK与单连接SDK不同的地方在于:多连接SDK至少使用CLK_32M系统时钟,其他时钟速度太慢,无法满足多连接SDK的运行。
中断嵌套
中断嵌套功能简述
B91m系列的芯片支持中断嵌套功能,先说明下三个概念:中断优先级,中断阈值,中断抢占。
(1) 中断优先级是每个中断的等级,在初始化中断的时候需要配置;
(2) 中断阈值是指响应中断的阈值,只有中断优先级高于中断阈值的中断才会被触发;
(3) 中断抢占是指当两个中断的优先级都高于中断阈值,如果当前较低优先级的中断正在被响应,较高优先级的中断可以被触发,抢占较低优先级的中断,执行完较高优先级的中断后再继续执行较低优先级的中断。
注意:
中断嵌套功能默认是打开的,并且中断阈值默认为0。
中断优先级可以设置的范围1~3,中断优先级目前只能支持最高设置到3,数字越大优先级越高,优先级的枚举如下:
typedef enum{
IRQ_PRI_LEV0,//Never interrupt
IRQ_PRI_LEV1,
IRQ_PRI_LEV2,
IRQ_PRI_LEV3,
}irq_priority_e;
BLE SDK在初始化的blc_ll_initBasicMCU里已经将BLE中断(“rf_irq”和“stimer_irq”)的中断优先级设置为IRQ_PRI_LEV2,并且中断阈值设置为0(LEV1~ LEV3优先级的中断都可以被触发)。
用户定义的APP普通中断,需要将中断优先级设置为IRQ_PRI_LEV1,不用限制执行时间,BLE中断和APP高级中断会抢占APP普通中断。
如果用户有APP高级中断的需求,需要将中断优先级设置为IRQ_PRI_LEV3。在使用APP高级中断时需注意:
-
中断处理函数必须放到ram_code段
-
中断处理函数中不允许访问Flash
-
中断处理函数执行时间小于50us
在执行Flash空间的擦除、读写操作函数时,会将中断阈值设置为1,执行完Flash操作函数后再将中断阈值设置为0,因此在读写Flash操作过程中允许BLE中断和用户APP高级中断插入。如果BLE中断和用户APP高级中断函数存放在Flash中,Flash预取指操作和读写Flash操作会出现时序冲突,造成死机。如果BLE中断和用户APP高级中断函数中存在读写Flash的操作,多个读写Flash操作也会出现时序冲突,造成死机。因此需将BLE中断和用户APP高级中断函数放在ram_code段,以及函数内禁止访问Flash。由于用户APP高级中断会抢占BLE中断和APP普通中断,所以用户也必须限制高级中断函数执行时间小于50us以免影响到BLE中断。
中断嵌套的使用
App普通中断
比如用户想设置一个PWM的APP普通中断,在配置中断的时候定义中断优先级为IRQ_PRI_LEV1,方法如下。
plic_set_priority(IRQ16_PWM, IRQ_PRI_LEV1);
中断响应函数类型不限。
void pwm_irq_handler(void)
{
……
}
App高级中断
比如用户想设置一个Timer0的APP高级中断,在配置中断的时候定义中断优先级为IRQ_PRI_LEV3,方法如下。
plic_set_priority(IRQ4_TIMER0, IRQ_PRI_LEV3);
中断响应函数必须定义为ram_code段,方法如下。
_attribute_ram_code_ void timer0_irq_handler(void)
{
……
}
中断使用限制
BLE中断要求及时响应,所以无论是优先级为IRQ_PRI_LEV3的APP中断,还是用户关闭全局中断,都限制最长时间为50us,需要用户特别注意。
BLE模块
本手册以Bluetooth Core Specification 5.4版本为参考。
BLE SDK软件架构
标准BLE SDK软件架构
根据Bluetooth Core Specification,一个比较标准的BLE SDK架构如下图所示。
在上图所示的架构中,BLE协议栈分为Host和Controller两部分。
-
Controller作为BLE底层协议,包括Physical Layer(PHY)和Link Layer(LL)。Host Controller Interface(HCI)是Controller与Host的唯一通信接口,Controller与Host所有的数据交互都通过该接口完成。
-
Host作为BLE上层协议,协议上有Logic Link Control and Adaption Protocol(L2CAP)、Attribute Protocol(ATT)、Security Manager Protocol(SMP),Profile包括Generic Access Profile(GAP)、Generic Attribute Profile(GATT)。
-
应用层(APP)包含user自己相关应用代码和各种service对应的Profile,user通过GAP去控制访问Host。Host通过HCI与Controller完成数据交互,如下图所示。
(1) BLE Host通过HCI cmd去操作设置Controller,这些HCI cmd对应本章后面将要介绍的controller API。
(2) Controller通过HCI向host上报各种HCI event,本章也会具体介绍。
(3) Host将需要发送给对方设备的数据通过HCI传送到Controller,Controller将数据直接丢到Physical Layer进行发送。
(4) Controller在Physical Layer收到的RF数据,先判断是属于Link Layer的数据还是Host的数据:如果是Link Layer的数据,直接处理数据;如果是Host的数据,则通过HCI将数据传到Host。
Telink BLE SDK软件架构
Telink BLE Multiple Connection Controller
Telink B91m BLE Multiple Connection SDK支持标准的BLE controller,包括HCI、PHY(Physical Layer)和LL(Link Layer)。
Telink B91m BLE Multiple Connection SDK包含Link Layer的五种标准状态(standby、advertising、scanning、initiating、connection),connection状态下同时支持最多 4 个 Central role 和 4 个 Peripheral role。
controller架构图如下:
Telink BLE Multiple Connection Whole Stack (Controller+Host)
Telink B91m BLE Multiple Connection SDK提供 BLE Multiple Connection Whole Stack(Controller + Host)参考设计,只有对于Central SDP(service discovery)无法做到完全支持,后面的章节会具体介绍。
Telink BLE stack架构会对上面标准的结构做一些简化处理,使得整个SDK的系统资源开销(包括Sram、运行时间、功耗等)最小,其架构如下图所示。SDK中提供的demo都是基于该架构。
图中实心箭头所示的数据交互是user可以通过各种接口来操作控制的,会提供user API。空心箭头是协议栈内部完成的数据交互,user无法参与。
HCI是Controller与Host的数据通信接口(和L2CAP层对接),但不是唯一的接口,APP应用层也可以直接与Link Layer进行数据交互。Power Management(PM)低功耗管理单元被内嵌到Link layer,应用层可以调用PM相关接口进行功耗管理的设置。
考虑到效率,应用层与Host的数据交互不通过GAP来访问控制,协议栈在ATT、SMP和L2CAP都提供了相关接口,可以和应用层直接交互。但是Host所有Event需要通过GAP层和应用层交互。
Host层以Attribute Protocol为基础,实现了Generic Attribute Profile(GATT)。应用层基于GATT,定义user自己需要的各种profile和service。该BLE SDK提供几个基本的profile,包括HIDS、BAS、OTA等。
下面基于这个架构对BLE 多连接协议栈各部分做一些基本的介绍,并给出各层的user API。
其中Physical Layer完全由Link Layer控制,且不需要应用层任何的参与,这部分不介绍。
虽然Host与Controller的部分数据交互还是靠HCI来完成,但基本都是Host和Controller协议栈完成,应用层几乎不参与,只需要在L2CAP层注册HCI数据回调处理函数就行了,所以对HCI部分也不做介绍。
Controller
Connection Number配置
supportedMaxCentralNum & supportedMaxPeripheralNum
Telink B91m BLE Multiple Connection SDK 将Connection Central role最大数量称为supportedMaxCentralNum,将Connection Peripheral role最大数量称为supportedMaxPeripheralNum,他们是由library决定的,如下表所示:
IC | library | supportedMaxCentral-Num | supportedMaxPeripheralNum |
---|---|---|---|
B91 | liblt_9518 | 4 | 4 |
B92 | liblt_9528 | 4 | 4 |
SDK 可以通过下面的 API 查询当前 Stack 支持的 Central 和 Peripheral 数量。
int blc_ll_getSupportedMaxConnNumber(void);
appMaxCentralNum & appMaxPeripheralNum
在 supportedMaxCentralNum 和 supportedMaxPeripheralNum 已经确定的前提下,用户可以通过下面API来设置自己应用上想要的最大 Central 和 Peripheral 数量,分别称为 appMaxCentralNum 和 appMaxPeripheralNum。
ble_sts_t blc_ll_setMaxConnectionNumber(int max_master_num, int max_slave_num);
这个API只允许在初始化的时候调用,即Link Layer运行之前就需要确定好相关连接数,不允许后面再修改。
用户的 appMaxCentralNum 和 appMaxPeripheralNum 必须小于或等于 supportedMaxCentralNum 和 supportedMaxPeripheralNum。
参考例程 设计上在初始化的时候都使用了该API:
blc_ll_setMaxConnectionNumber(ACL_CENTRAL_MAX_NUM, ACL_PERIPHR_MAX_NUM);
用户需要在app_config.h中定义自己的appMaxCentralNum和appMaxPeripheralNum,即SDK中的ACL_CENTRAL_MAX_NUM和ACL_PERIPHR_MAX_NUM。
#define ACL_CENTRAL_MAX_NUM 4
#define ACL_PERIPHR_MAX_NUM 4
appMaxCentralNum和appMaxPeripheralNum能够节省MCU的各种资源,比如针对C4P4的library,用户如果只需要用到C3P2,将ACL_CENTRAL_MAX_NUM和ACL_PERIPHR_MAX_NUM分别设为3和2后:
(1) 节省SRAM资源
Link Layer TX Central FIFO和TX Peripheral FIFO、L2CAP Central MTU buffer和L2CAP Peripheral MTU buffer都是根据appMaxCentralNum和appMaxPeripheralNum来分配的,所以可以节省一些Sram资源。具体请参考文档后面TX FIFO相关的介绍。
(2) 节省时间资源和功耗。
对于C4P4,Stack必须等到currentCentralNum为4时才会停止Scan动作,必须等到currentPeripheralNum为4时才会停止Advertising动作。而对于C3P2,Stack等到currentCentralNum为3时才会停止Scan动作,currentPeripheralNum为2时就会停止Advertising动作,这样就少了不必要的Scan和Advertising,能够节省PHY层带宽,也能降低MCU功耗。
currentMaxCentralNum & currentMaxPeripheralNum
用户定义了appMaxCentralNum和appMaxPeripheralNum后,确定了Link Layer运行时创建的 Central 和 Peripheral 最大数量。但Central和Peripheral在某一时刻的数量还是不确定的,比如appMaxCentralNum为4时,任何时刻Central的数量可能是0,1,2,3,4。
SDK提供了以下3个API,供用户实时查询当前Link Layer上的Central和Peripheral数量。
int blc_ll_getCurrentConnectionNumber(void);//Central + Peripheral connection number
int blc_ll_getCurrentCentralRoleNumber(void);//Central role number
int blc_ll_getCurrentPeripheralRoleNumber(void);//Peripheral role number
Link Layer状态机
用户可以先参考Telink B91 BLE Single Connection SDK中Link Layer 状态机的介绍,Link Layer 5个基本状态都是支持的,将Connection state再分为Connection Peripheral role和Connection Central role的话,Link Layer在任意时刻一定是且只能是以下6个状态中的1个:Standby、Advertising、Scanning、Initiating、Connection Peripheral role、Connection Central role。
而对于Telink B91m BLE Multiple Connection SDK ,由于要同时支持多个Central和Peripheral,Link Layer无法做到在某一时刻只处于某一种状态,必须是几种状态的组合。
Telink B91m BLE Multiple Connection SDK 的Link Layer状态机比较复杂,只做一个大致的介绍,能够满足用户对底层的基本理解以及相应API的使用。
Link Layer状态机初始化
Telink B91m BLE Multiple Connection SDK 将每个基本状态按照模块化的设计,对需要使用的模块,需要提前初始化。
MCU的初始化是必须的,API如下:
void blc_ll_initBasicMCU (void);
Standby模块的添加API如下,这个是必须的,所有的BLE应用都需要初始化。
void blc_ll_initStandby_module (u8 *public_adr);
实参public_adr 是BLE public mac address 的指针。
其他几个状态(Advertising、Scanning、ACL Central、ACL Peripheral)对应模块的初始化API分别如下:
void blc_ll_initLegacyAdvertising_module(void);
void blc_ll_initLegacyScanning_module(void);
void blc_ll_initAclConnection_module(void);
void blc_ll_initAclCentralRole_module (void);
void blc_ll_initAclPeripheralRole_module(void);
Link Layer状态组合
Initiating状态相对比较简单,当Scan状态需要对某个广播设备发起连接时,Link Layer进入Initiating状态,在一定的时间内(这个时间称为create connection timeout)要么建立连接成功,多出一个Connection Central role,要么建立连接失败,Link Layer重新回到Scanning状态。为了简化Link Layer状态机的介绍,更方便用户的理解,下面的介绍中都忽略Initiating这个短暂的临时状态。
Telink B91m BLE Multiple Connection SDK Link Layer状态机可以从两个角度去描述,一是Advertising和Peripheral的转换;二是 Scanning 和 Central的转换;这两个角度之间互不影响。
以C1P1为例分析,假设用户的appMaxCentralNum和appMaxPeripheralNum都是1。C1P1 Advertising和Peripheral切换的状态机如下:
图中adv_enable和adv_disable指的是条件发生时,用户最后一次调用blc_ll_setAdvEnable(adv_enable)设定的状态。
C1P1 Scanning和Central切换的状态机如下:
图中scan_enable和scan_disable指的是条件发生时,用户最后一次调用blc_ll_setScanEnable(scan_enable, filter_duplicate)设定的状态。
Advertising和Peripheral、Scanning和Central都各自有3种状态,由于这二者之间逻辑完全独立,互相不影响,那么最终Link Layer组合状态共有3*3=9种,如下表所示:
2A | 2B | 2C | |
---|---|---|---|
1A | Standby | Scanning | Central |
1B | Advertising | Advertising + Scanning | Advertising + Central |
1C | Peripheral | Peripheral + Scanning | Peripheral + Central |
以一个较为复杂的C4P4分析,假设用户的appMaxCentralNum和appMaxPeripheralNum分别是4和4,C4P4 Advertising和Peripheral切换的状态机如下:
C4P4 Scanning和Central切换的状态机如下:
Advertising和Peripheral有9种可能的状态,Scanning和Central有9种可能的状态,由于这二者之间逻辑完全独立,互相不影响,那么最终Link Layer组合状态共有9*9=81种,如下表所示:
2A | 2B | 2C | 2D | 2E | 2F | 2G | 2H | 2I | |
---|---|---|---|---|---|---|---|---|---|
1A | Standby | Scanning | Scanning Central*1 |
Scanning Central*2 |
Scanning Central*3 |
Central*4 | Central*1 | Central*2 | Central*3 |
1B | Adv | Adv Scanning |
Adv Scanning Central*1 |
Adv Scanning Central*2 |
Adv Scanning Central*3 |
Adv Central*4 |
Adv Central*1 |
Adv Central*2 |
Adv Central*3 |
1C | Adv Periphr*1 |
Adv Periphr*1 Scanning |
Adv Periphr*1 Scanning Central*1 |
Adv Periphr*1 Scanning Central*2 |
Adv Periphr*1 Scanning Central*3 |
Adv Periphr*1 Central*4 |
Adv Periphr*1 Central*1 |
Adv Periphr*1 Central*2 |
Adv Periphr*1 Central*3 |
1D | Adv Periphr*2 |
Adv Periphr*2 Scanning |
Adv Periphr*2 Scanning Central*1 |
Adv Periphr*2 Scanning Central*2 |
Adv Periphr*2 Scanning Central*3 |
Adv Periphr*2 Central*4 |
Adv Periphr*2 Central*1 |
Adv Periphr*2 Central*2 |
Adv Periphr*2 Periphr*2 Central*3 |
1E | Adv Periphr*3 |
Adv Periphr*3 Scanning |
Adv Periphr*3 Scanning Central*1 |
Adv Periphr*3 Scanning Central*2 |
Adv Periphr*3 Scanning Central*3 |
Adv Periphr*3 Central*4 |
Adv Periphr*3 Central*1 |
Adv Periphr*3 Central*2 |
Adv Periphr*3 Central*3 |
1F | Periphr*4 | Periphr*4 Scanning |
Periphr*4 Scanning Central*1 |
Periphr*4 Scanning Central*2 |
Periphr*4 Scanning Central*3 |
Periphr*4 Central*4 |
Periphr*4 Central*1 |
Periphr*4 Central*2 |
Periphr*4 Central*3 |
1G | Periphr*1 | Periphr*1 Scanning |
Periphr*1 Scanning Central*1 |
Periphr*1 Scanning Central*2 |
Periphr*1 Scanning Central*3 |
Periphr*1 Central*4 |
Periphr*1 Central*1 |
Periphr*1 Central*2 |
Periphr*1 Central*3 |
1H | Periphr*2 | Periphr*2 Scanning |
Periphr*2 Scanning Central*1 |
Periphr*2 Scanning Central*2 |
Periphr*2 Scanning Central*3 |
Periphr*2 Central*4 |
Periphr*2 Central*1 |
Periphr*2 Central*2 |
Periphr*2 Central*3 |
1I | Periphr*3 | Periphr*3 Scanning |
Periphr*3 Scanning Central*1 |
Periphr*3 Scanning Central*2 |
Periphr*3 Scanning Central*3 |
Periphr*3 Central*4 |
Periphr*3 Central*1 |
Periphr*3 Central*2 |
Periphr*3 Central*3 |
如果用户的appMaxCentralNum/appMaxPeriphrNum不是C1P1或者C4P4,请根据以上分析方法去分析。
前面介绍了supportedMaxCentralNum / supportedMaxPeripheralNum和appMaxCentralNum / appMaxPeripheralNum的概念,对应上面状态机组合表里面Central和Peripheral的个数,再定义两个概念currentCentralNum和currentPeripheralNum,表示当前时刻Link Layer实际Central和Peripheral的数量,比如在上面表中‘1D2E’组合状态时,currentCentralNum为3,currentPeripheralNum为2。
Link Layer时序
Link Layer时序比较复杂,这里只介绍一些最基本的知识,足以让用户理解,并合理使用相关API。
Link Layer 5种基本的单状态Standby、Advertising、Scanning、Initiating、Connection,忽略只有Central create connection时才用到的短暂的Initiating,我们只对剩余4种状态的时序做简单的介绍。
本节我们以C4P4状态为例来说明,假设appMaxCentralNum和appMaxPeripheralNum分别是4和4。
各个子状态(Advertising、Central0 ~ Central3、Peripheral0 ~ Peripheral3、Scanning、UI task)会以下图为指示:
Standby state时序
对应C4P4在表3.3中的1A2A状态。
当Link Layer处于Idle state时,Link Layer和Physical Layer没有任何任务要处理,blc_sdk_main_loop 函数完全不起作用,也不会产生任何中断。可以认为UI entry(UI task)占据了整个main_loop的时间。
Scanning only, no Adverting, no Connection时序
对应C4P4在表3.3中的1A2B状态。
此时只需要处理Scanning状态,Scan的效率最高。Link Layer根据Scan interval去切换channel 37/38/39,时序图如下:
根据Scan window的大小决定真实的Scan时间,如果Scan window等于Scan interval,所有的时间都在Scan;如果Scan window小于Scan interval,在Scan interval里面选择从最前面开始和Scan window相等的时间来进行Scan。
图上所示的Scan window大约是Scan interval的60%,在前60%的时间里,Link Layer处于Scanning状态,PHY层在收包,同时用户可以利用这段时间在main_loop中执行自己的UI task。后40%的时间不是Scanning状态,PHY层停止工作,用户可以利用这段时间在main_loop中执行自己的UI task,对于后面将要介绍的低功耗管理的设计,这段时间也可以让MCU进入suspend以降低整机功耗。
Advertising only, no Scanning, no Connection时序
对应C4P4在表3.3中的1B2A状态。
根据Adv interval将Advertising Event分配到时间轴上,时序图如下:
Adv Event的所有细节参考Telink B91 BLE Single Connection SDK Handbook 中Adv Event的详细介绍即可,二者是一样的。
用户可以利用非Adv时间在main_loop中执行自己的UI task。
Advertising, Scanning, no Connection时序
对应C4P4在表3.3中的1B2B状态。
首先根据Adv interval先将Advertising Event分配到时间轴上,然后再分配Scanning,时序图如下:
由于应用上对广播的时间准确性要求比扫描更高,此时Adv Event具有较高优先级,先分配好Adv Event的时序,然后将Adv Event之间的剩余时间用来做Scan,同时用户可以利用这段剩余时间在main_loop中执行自己的UI task。当用户设置的Scan window等于Scan interval时,图中的Scan duration会填满剩余时间;当用户设置的Scan window小于Scan interval时,Link Layer会自动计算,得到一个Scan duration满足以下条件:Scan duration/(Adv interval + rand_dly)尽量等于Scan window/Scan interval。
Connection, Advertising, Scanning时序
Connection连接的数量还没达到设定的最大值,此时仍然有advertising和scanning状态存在。
下图对应C4P4在表3.3中的1C2C状态。
首先进行连接任务的分配(不管是 Central 还是 Peripheral),会按照各自连接的时序进行分配。如果多个任务占用了同一个时间段而发生冲突,则会按照优先级高低进行分配,高优先级抢占。舍弃的任务会自动增加优先级,以保证不会一直被丢弃。
然后进行adv任务的分配,原则是:
(1) 和上次adv事件的时间间隔要大于设置的最小adv interval时间。
(2) 和下一个任务之间的时间大于一定值(3.75ms),因为adv完成需要一定的时间。
(3) 分配的时间段没有其他连接任务占用。
最后进行scan任务的分配,原则是:只要两个任务之间有比较充足的时间,这段时间就会分配给scan任务,同样也会根据Scan window/Scan interval来确认scan的百分比。
Connection, no Advertising, no Scanning时序
Connection连接数量已经达到了设定的最大值,此时不存在advertising和scanning状态。
下图对应C4P4在表3.3中的1G2H状态。
下图对应C4P4在表3.3中的1E2F状态。
此时只有连接任务,按照各自连接的时序进行任务分配。如果发生了冲突,则以优先级高低进行分配,高优先级任务抢占,被放弃的任务会自动增加优先级,增加下次冲突时抢占的几率。
ACL TX FIFO & ACL RX FIFO
ACL TX FIFO定义及设置
应用层和BLE Host所有的数据最终都需要通过Controller的Link Layer完成RF数据的发送,在Link Layer中根据user设置的连接数量,定义了相应的TX FIFO。
ACL TX FIFO的定义如下:
u8 app_acl_cen_tx_fifo[ACL_CENTRAL_TX_FIFO_SIZE * ACL_CENTRAL_TX_FIFO_NUM * ACL_CENTRAL_MAX_NUM] = {0};
u8 app_acl_per_tx_fifo[ACL_PERIPHR_TX_FIFO_SIZE * ACL_PERIPHR_TX_FIFO_NUM * ACL_PERIPHR_MAX_NUM] = {0};
ACL Central 和 ACL Peripheral 的 TX FIFO 分开定义。以 ACL Peripheral为例说明,ACL Central 原理一样,类推即可。
数组 app_acl_per_tx_fifo 的大小与三个宏相关:
(1) ACL_PERIPHR_MAX_NUM 是最大连接数量,即 appMaxPeripheralNum。用户可以根据需要在 app_config.h 中修改这个值。
(2) ACL_PERIPHR_TX_FIFO_SIZE是每个sub_buffer的size,与ACL Peripheral发送数据可能的最大值相关。在SDK中使用如下宏定义实现。
#define ACL_PERIPHR_TX_FIFO_SIZE CAL_LL_ACL_TX_FIFO_SIZE(ACL_PERIPHR_MAX_TX_OCTETS)
其中CAL_LL_ACL_TX_FIFO_SIZE是一个公式,跟MCU的实现方式有关,不同的MCU计算方法可能会不一样,可以参考app_buffer.h中注释说明了解细节。
ACL_PERIPHR_MAX_TX_OCTETS是用户定义的PeripheralMaxTxOctets。如果客户用到DLE(Data Length Extension),这个值需要相应的修改;默认值为27对应不使用DLE时的最小值,用来节省Sram。具体细节请参考app_buffer.h中的注释说明,以及 Bluetooth Core Specification 中的描述。
(3) ACL_PERIPHR_TX_FIFO_NUM,sub_buffer的number,该值的选取请参考app_buffer.h中的注释说明。该值与客户应用中数据发送量有一定关系,如果数据发送量大且实时性要求较高时,可以考虑number大一些。
user根据实际情况去定义TX FIFO,并且Central TX FIFO和Peripheral TX FIFO分开进行定义,这样一是为每个connection的数据分别缓存在各自的TX FIFO中,各个connection之间TX数据不会相互干扰;二是也可以根据实际情况,灵活定义TX FIFO的大小,相应的减少ram的消耗。比如:
- Peripheral需要DLE功能,而Central不需要DLE,这样就可以分别定义FIFO,节省ram空间。关于DLE的讲解,请参考3.2.5 MTU和DLE章节。
- 比如客户实际使用的是3主2从,客户就可以只定义3个Central tx fifo,2个Peripheral tx fifo,从而减少ram的使用,节省ram空间:
#define ACL_CENTRAL_MAX_NUM 3
#define ACL_PERIPHR_MAX_NUM 2
下面我们以图来描述一下各种状态下TX FIFO的设置,让大家有一个更直观的认识。这里以B91为例,其他芯片(如B92)是同样的原理,类推即可。
(1) C4P4,ACL Central、ACL Peripheral都不使用DLE
假设相关的定义如下:
#define ACL_CENTRAL_MAX_NUM 4
#define ACL_PERIPHR_MAX_NUM 4
#define ACL_CENTRAL_MAX_TX_OCTETS 27
#define ACL_PERIPHR_MAX_TX_OCTETS 27
#define ACL_CENTRAL_TX_FIFO_SIZE CAL_LL_ACL_TX_FIFO_SIZE(ACL_CENTRAL_MAX_TX_OCTETS)
#define ACL_CENTRAL_TX_FIFO_NUM 8
#define ACL_PERIPHR_TX_FIFO_SIZE CAL_LL_ACL_TX_FIFO_SIZE(ACL_PERIPHR_MAX_TX_OCTETS)
#define ACL_PERIPHR_TX_FIFO_NUM 8
则TX FIFO定义为下面的值:
u8 app_acl_cen_tx_fifo[40 * 8 * 4] = {0};
u8 app_acl_per_tx_fifo[40 * 8 * 4] = {0};
图示如下:
每一个connection都对应一个tx fifo,并且每一个connection fifo的数量都是8(0 ~ 7),0 ~ 7的 size都是一样的(40B):
(2) C4P4,ACL Central使用了DLE且CentralMaxTxOctets为最大值251,ACL Peripheral不使用DLE。
假设相关的定义如下:
#define ACL_CENTRAL_MAX_NUM 4
#define ACL_PERIPHR_MAX_NUM 4
#define ACL_CENTRAL_MAX_TX_OCTETS 251
#define ACL_PERIPHR_MAX_TX_OCTETS 27
#define ACL_CENTRAL_TX_FIFO_SIZE CAL_LL_ACL_TX_FIFO_SIZE(ACL_CENTRAL_MAX_TX_OCTETS)
#define ACL_CENTRAL_TX_FIFO_NUM 8
#define ACL_PERIPHR_TX_FIFO_SIZE CAL_LL_ACL_TX_FIFO_SIZE(ACL_PERIPHR_MAX_TX_OCTETS)
#define ACL_PERIPHR_TX_FIFO_NUM 8
则TX FIFO定义为下面的值:
u8 app_acl_cen_tx_fifo[264 * 8 * 4] = {0};
u8 app_acl_per_tx_fifo[40 * 8 * 4] = {0};
从图可以看出,Central 和 Peripheral 每个 connection 的 fifo数量是一样的,都是8个(0~7)。但是 Central 每个 fifo size是264B,而Peripheral的fifo size是40B。
(3) C3P2,ACL Central、ACL Peripheral都不使用DLE
ACL RX FIFO定义及设置
ACL RX FIFO的定义如下:
u8 app_acl_rx_fifo[ACL_RX_FIFO_SIZE * ACL_RX_FIFO_NUM] = {0};
ACL Central和ACL Peripheral共用ACL RX FIFO。以ACL Peripheral为例说明,ACL Central原理一样,类推即可。
数组app_acl_rx_fifo的大小与二个宏相关:
(1) ACL_RX_FIFO_SIZE是buffer size,与ACL Peripheral接收数据可能的最大值相关。在SDK中使用如下宏定义实现。
#define ACL_RX_FIFO_SIZE CAL_LL_ACL_RX_FIFO_SIZE(ACL_CONN_MAX_RX_OCTETS)
其中CAL_LL_ACL_RX_FIFO_SIZE是一个公式,跟MCU的实现方式有关,不同的MCU计算方法可能会不一样,可以参考app_buffer.h中注释说明了解细节。
ACL_CONN_MAX_RX_OCTETS是用户定义的PeripheralMaxRxOctets。如果客户用到DLE(Data Length Extension),这个值需要相应的修改;默认值为27对应不使用DLE时的最小值,用来节省Sram。具体细节请参考app_buffer.h中的注释说明,以及 Bluetooth Core Specification 中的详细描述。
(2) ACL_RX_FIFO_NUM,buffer number,该值的选取请参考app_buffer.h中的注释说明。该值与客户应用中数据接收量有一定关系,如果数据接收量大且实时性要求较高时,可以考虑number大一些。
假设C4P4 ACL RX FIFO的定义如下:
#define ACL_CENTRAL_MAX_NUM 4
#define ACL_PERIPHR_MAX_NUM 4
#define ACL_CONN_MAX_RX_OCTETS 27
#define ACL_RX_FIFO_SIZE CAL_LL_ACL_RX_FIFO_SIZE(ACL_CONN_MAX_RX_OCTETS)
#define ACL_RX_FIFO_NUM 16
对应的ACL RX FIFO分配如下图所示:
RX overflow 分析
参考Telink B91 BLE Single Connection Handbook上的介绍,原理一样。
MTU和DLE概念及使用方法
MTU和DLE概念说明
Bluetooth Core Specification 从 4.2 版本开始增加了data length extension(DLE)。
Telink B91m BLE Multiple Connection SDK Link Layer上支持data length extension,且rf_len长度支持到Bluetooth Core Specification 上最大长度251 bytes。详情请参考Bluetooth Core Specification V5.4, Vol 6, Part B, 2.4.2.21 LL_LENGTH_REQ and LL_LENGTH_RSP。
在具体讲解之前,我们需要搞清楚 MTU 和 DLE 的概念是什么。先看一张图:
下图是 MTU 和 DLE 所包含的内容:
-
MTU代表最大传输单元,在计算机网络中用于定义可以由特定协议发送的协议数据单元 (PDU, Protocol Data Unit) 的最大大小。
-
Attribute MTU(规范定义的ATT_MTU)是客户端和服务器之间可以发送的最大ATT payload大小。Bluetooth Core Specification规定MTU最小值是23 bytes。
所谓的DLE,就是data length extension数据长度扩展。Bluetooth Core Specification规定DLE最小值是27 bytes。
如果要想在一个packet中携带更多数据,就需要Central和Peripheral之间进行协商,通过LL_LENGTH_REQ和LL_LENGTH_RSP交互DLE大小。
Bluetooth Core Specification规定DLE长度最小为27 bytes,最大是251 bytes。251bytes是因为rf length字段是一个byte,能表示的最大长度是255,如果是加密链路,还需要4 bytes MIC字段:251 + 4 = 255。
MTU和DLE自动交互方法
User如果需要使用data length extension功能,按如下步骤设置。SDK中也提供了相应的MTU&DLE使用demo,参考feature_test工程中的feature_dle。
在vendor/feature_test/feature_config.h中定义宏:
#define FEATURE_TEST_MODE TEST_LL_DLE
(1) MTU size exchange
首先需要进行MTU的交互,只需要修改CENTRAL_ATT_RX_MTU和PERIPHR_ATT_RX_MTU即可,它们分别代表ACL Central和ACL Peripheral的RX MTU size。默认为最小的值是23,改为想要的值即可。CAL_MTU_BUFF_SIZE是固定的计算公式,不能修改。
MTU size exchange的流程确保双方最小值MTU size生效,防止peer device在BLE L2cap层无法处理长包,并且大于等于23。
#define CENTRAL_ATT_RX_MTU 23
#define PERIPHR_ATT_RX_MTU 23
#define CENTRAL_L2CAP_BUFF_SIZE CAL_L2CAP_BUFF_SIZE(CENTRAL_ATT_RX_MTU)
#define PERIPHR_L2CAP_BUFF_SIZE CAL_L2CAP_BUFF_SIZE(PERIPHR_ATT_RX_MTU)
然后初始化里面调用以下API分别设置ACL Central和ACL Peripheral的RX MTU size。注意:这两个API需要放到blc_gap_init()之后才会生效。
blc_att_setCentralRxMTUSize(CENTRAL_ATT_RX_MTU);
blc_att_setPeripheralRxMTUSize(PERIPHR_ATT_RX_MTU);
MTU size exchange的实现,请参考本文档“ATT & GATT”部分的详细说明---3.4.2.4.7 Exchange MTU Request, Exchange MTU Response,也可以参考feature_test工程中feature_dle的写法。
(2) 设置connMaxTxOctets和connMaxRxOctets
其次需要设置Central和Peripheral的DLE的大小,参考ACL TX FIFO和ACL RX FIFO的介绍,只需要修改下面几个宏即可,27改为想要的值。如果这些数值不是默认的27,stack在连接后会自动进行DLE的交互(也可以使用API禁用该功能,需要的时候再进行DLE交互)。
#define ACL_CONN_MAX_RX_OCTETS 27
#define ACL_CENTRAL_MAX_TX_OCTETS 27
#define ACL_PERIPHR_MAX_TX_OCTETS 27
然后初始化里面调用以下API分别设置ACL Central和ACL Peripheral的rx DLE size和tx DLE size。
ble_sts_t blc_ll_setAclConnMaxOctetsNumber(u8 maxRxOct, u8 maxTxOct_Central, u8 maxTxOct_Peripheral)
(3) 收发长包的操作
请user先参考本文档“ATT & GATT”部分的一些说明,包括Handle Value Notification和Handle Value Indication,Write request和Write Command等。
在以上3个步骤都正确完成的基础上,可以开始收发长包。
发长包调用ATT层的Handle Value Notification和Handle Value Indication对应的API即可,分别如下所示,将要发送的数据地址和长度分别带入下面的形参"*p”和"len”即可。
ble_sts_t blc_gatt_pushHandleValueNotify (u16 connHandle, u16 attHandle, u8 *p, int len);
ble_sts_t blc_gatt_pushHandleValueIndicate (u16 connHandle, u16 attHandle, u8 *p, int len);
收长包只要处理Write request和Write Command对应的回调函数“w”即可,在回调函数里,引用形参指针指向的数据。
MTU和DLE手动交互方法
如果user由于某种特殊情况,不希望stack自动交互MTU/DLE,SDK也提供了相应API,由user根据具体情况来决定何时进行MTU/DLE的交互。手动交互大部分的设置和自动交互一致,不同的处理如下描述。
对于MTU,在初始化的时候调用API blc_att_setCentralRxMTUSize(23)和blc_att_setPeripheralRxMTUSize(23)将初始的MTU设置为最小值23,stack比较后不会进行自动交互。 当user需要进行MTU交互时,再调用这两个API(blc_att_setCentralRxMTUSize和blc_att_setPeripheralRxMTUSize),将MTU设置为实际值。然后再调用API blc_att_requestMtuSizeExchange()触发MTU交互即可。
对于DLE,可以在初始化的时候使用API blc_ll_setAutoExchangeDataLengthEnable(0)来disable自动交互,等到需要交互的时候再调用API blc_ll_sendDateLengthExtendReq ()触发DLE交互即可。这两个API的具体说明参考controller API章节。
Coded PHY/2M PHY
Coded PHY和2M PHY是《Core_v5.0》新增加的feature,很大程度上扩展了BLE的应用场景,Coded PHY包含S2(500 kbps)和S8(125 kbps)以适应更远距离的应用,2M PHY(2 Mbps)大大提高了BLE带宽。2M PHY/Coded PHY 可以使用在广播通道,也可以用在连接状态下的数据通道。
Coded PHY/2M PHY Demo介绍
Telink B91m BLE Multiple Connection SDK 中,为节省Sram,Coded PHY/2M PHY默认是关闭的,用户如果选择使用此Feature,可以手动打开。
- local device参考feature_test/feature_2M_coded_phy。
在feature_config.h中定义宏:
#define FEATURE_TEST_MODE TEST_2M_CODED_PHY_CONNECTION
- peer device可以选择使用手机或其他标准的BLE Central设备;也可以使用任何一个支持Coded PHY/2M PHY的Telink BLE SDK,编译出BLE Central设备运行,比如使用Telink B91 BLE Single Connection SDK 编译的xxx_kma_Central_dongle。
Code PHY/2M PHY API介绍
(1) API blc_ll_init2MPhyCodedPhy_feature()
void blc_ll_init2MPhyCodedPhy_feature(void);
用于使能Coded PHY/2M PHY。
(2) API blc_ll_setPhy()
ble_sts_t blc_ll_setPhy( u16 connHandle, le_phy_prefer_mask_t all_phys, le_phy_prefer_type_t tx_phys, le_phy_prefer_type_t rx_phys, le_ci_prefer_t phy_options);
Bluetooth Core Specification 标准接口,详细请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.49 LE Set PHY command。用于触发local device主动申请PHY exchange。若PHY exchange的判定结果为可以使用新的PHY,则Central设备会触发PHY update,新的PHY很快会生效。
connHandle:Central/Peripheral connHandle根据实际情况填写,参考“Connection Handle”。
其他参数请参考Bluetooth Core Specification 定义、结合SDK上枚举类型定义和demo用法去理解。
Channel Selection Algorithm #2
Channel Selection Algorithm #2 是Bluetooth Core Specification V5.0中新添加的Feature,拥有更强的抗干扰能力,有关信道选择算法的具体说明请参考Bluetooth Core Specification V5.4, Vol 6, Part B, 4.5.8.3 Channel Selection algorithm #2。
Telink B91m BLE Multiple Connection SDK 中,CSA #2默认是关闭的,用户如果选择使用此Feature,可以手动打开,需要在user_init()调用下面的API使能。
void blc_ll_initChannelSelectionAlgorithm_2_feature(void);
只有local device和peer Central/Peripheral device都同时支持CSA #2(ChSel字段都设置为1),CSA #2才能用于连接。
Controller API
所示的标准BLE协议栈架构中,应用层是无法与Controller的Link Layer直接数据交互的,必须通过Host把数据往下发,最终由Host通过HCI把控制命令传送给Link Layer。Host通过HCI接口下发的所有Controller控制命令都在Bluetooth Core Specification中严格定义了,详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E: Host Controller Interface Functional Specification。
Telink B91m BLE Multiple Connection SDK采用Whole Stack架构,应用层跨越Host直接操作设置Link Layer,但使用的API都是严格按照Bluetooth Core Specification标准的HCI部分的API。
Controller API的声明在stack/ble/controller目录下的头文件中,根据Link Layer状态机功能的分类分为ll.h、leg_adv.h、leg_scan.h、leg_init.h、acl_Peripheral.h、acl_Central.h、acl_conn.h等,user可以根据Link Layer的功能去寻找,比如跟legacy advertising相关功能的API就应该都在leg_adv.h中声明。
在stack/ble/ble_common.h中定义了枚举类型ble_sts_t,该类型作为SDK中大多数API的返回值类型,只有调用API的设置参数都正确且被协议栈接受时,才会返回BLE_SUCCESS(值为0);返回的其他非0值都表示设置错误,且每一个不同的值都对应一种错误类型。后面的API具体说明中,会列举每一个API所有可能的返回值,并解释各个错误返回值的具体错误原因。
这个返回值类型ble_sts_t不仅限于Link Layer的API,对Host层一些API也适用。
BLE MAC address初始化
本文档中的BLE MAC address最基本的类型包括public address和random static address。
调用如下API获得public address 和random static address:
void blc_initMacAddress(int flash_addr, u8 *mac_public, u8 *mac_random_static);
flash_addr填flash上存储MAC address的地址即可,参考文档前面的介绍2.1.4 SDK Flash空间的分配。如果不需要random static address,上面获取的mac_random_static忽略即可。
BLE public MAC address成功获取后,调用Link Layer初始化的API,将MAC address传入BLE协议栈:
blc_ll_initStandby_module (mac_public); //mandatory
bls_ll_setAdvData
详情请参照Bluetooth Core Specification V5.4 Vol 4, Part E, 7.8.7 LE Set Advertising Data command。
BLE协议栈里,广播包的格式如上图所示,前两个byte是Header,后面是Advertising PDU,最多37 bytes,包含AdvA(6B)和AdvData(最大31B)。
下面的API用于设置AdvData部分的数据:
ble_sts_t blc_ll_setAdvData (u8 *data, u8 len);
data指针指向PDU的首地址,len为数据长度。
返回类型ble_sts_t可能返回的结果如下表所示:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。
user可以在初始化的时候调用该API设置广播数据,也可以于程序运行时在main_loop里随时调用该API来修改广播数据。
bls_ll_setScanRspData
详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.8 LE Set Scan Response Data command。
类似于上面广播包PDU的设置,scan response PDU的设置使用API:
ble_sts_t blc_ll_setScanRspData (u8 *data, u8 len);
data指针指向PDU的首地址,len为数据长度。
返回类型ble_sts_t可能返回的结果如下表所示:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。
user可以在初始化的时候调用该API设置scan response data,也可以于程序运行时在main_loop里随时调用该API来修改scan response data。
bls_ll_setAdvParam
详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.5 LE Set Advertising Parameters command。
BLE协议栈里Advertising Event(简称Adv Event)如上图所示,指的是在每一个T_advEvent,Peripheral进行一轮广播,在三个广播channel(channel 37、channel 38、channel 39)上各发一个包。
下面的API对Adv Event相关的参数进行设置。
ble_sts_t blc_ll_setAdvParam( adv_inter_t intervalMin, adv_inter_t intervalMax,
adv_type_t advType, own_addr_type_t ownAddrType,
u8 peerAddrType, u8 *peerAddr, adv_chn_map_t adv_channelMap, adv_fp_type_t advFilterPolicy);
(1) intervalMin & intervalMax
设置广播时间间隔adv interval的范围,以0.625 ms为基本单位,范围在20 ms ~ 10.24 s之间,并且intervalMin小于等于intervalMax。
SDK在连接态下广播间隔使用intervalMin,在非连接态下广播间隔使用intervalMax。
若设置的intervalMin > intervalMax,intervalMin会被强制等于intervalMax。
根据不同广播包的类型,intervalMin和intervalMax的值有一些限定,请参照(Vol 6/Part B/ 4.4.2.2 “Advertising Events”)。
(2) advType
参考Bluetooth Core Specification,四种基本的广播事件类型如下:
上图中Allowable response PDUs for advertising event部分用YES和NO说明了各种类型广播事件是否对其他设备的Scan request和Connect Request进行响应,如:第一个Connectable Undirected Event对Scan request和Connect Request都能响应,而Non-connectable Undireted Event对它们都不响应。
注意第二个Connectable Directed Event对Connect Request响应那个"YES"右上角加了“*”号,表示它只要收到匹配的Connect Request,就一定会响应,而不会被whitelist过滤。剩下的3个"YES"表示可以响应相应的请求,但实际需要依赖于whitelist的设置,根据whitelist的过滤条件来决定最终是否响应,后面的whitelist中会详细介绍。
以上四种广播事件中,Connectable Directed Event又分为Low Duty Cycle Directed Advertising和High Duty Cycle Directed Advertising,这样一共能够得到五种广播事件类型,如下定义(stack/ble/ble_common.h):
/* Advertisement Type */
typedef enum{
ADV_TYPE_CONNECTABLE_UNDIRECTED = 0x00, // ADV_IND
ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY = 0x01, //ADV_INDIRECT_IND (high duty cycle)
ADV_TYPE_SCANNABLE_UNDIRECTED = 0x02 //ADV_SCAN_IND
ADV_TYPE_NONCONNECTABLE_UNDIRECTED = 0x03, //ADV_NONCONN_IND
ADV_TYPE_CONNECTABLE_DIRECTED_LOW_DUTY = 0x04, //ADV_INDIRECT_IND (low duty cycle)
}adv_type_t;
默认最常用的广播类型为ADV_TYPE_CONNECTABLE_UNDIRECTED。
(3) ownAddrType
指定广播地址类型时,ownAddrType 4个可选的值如下:
typedef enum{
OWN_ADDRESS_PUBLIC = 0,
OWN_ADDRESS_RANDOM = 1,
OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC = 2,
OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM = 3,
}own_addr_type_t;
这里只介绍前两个参数。
OWN_ADDRESS_PUBLIC表示广播的时候使用public MAC address,实际地址来自MAC address初始化时API blc_initMacAddress(flash_sector_mac_address, mac_public, mac_random_static)的设置。
OWN_ADDRESS_RANDOM表示广播的时候使用random static MAC address,该地址来源于下面API设定的值:
ble_sts_t blc_ll_setRandomAddr(u8 *randomAddr);
(4) peerAddrType & *peerAddr
当advType被设置为直接广播包类型directed adv(ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY 和 ADV_TYPE_CONNECTABLE_DIRECTED_LOW_DUTY)时,peerAddrType和*peerAddr用于指定peer device MAC Address的类型和地址。
当advType为其他类型时,peerAddrType和*peerAddr的值都无效,可以设定为0和NULL。
(5) adv_channelMap
设定广播channel,可以选择channel 37、38、39中任意一个或多个。adv_channelMap的值可设置如下3个或它们中任意或组合。
typedef enum{
BLT_ENABLE_ADV_37 = BIT(0),
BLT_ENABLE_ADV_38 = BIT(1),
BLT_ENABLE_ADV_39 = BIT(2),
BLT_ENABLE_ADV_ALL = (BLT_ENABLE_ADV_37 | BLT_ENABLE_ADV_38 | BLT_ENABLE_ADV_39),
}adv_chn_map_t;
(6) advFilterPolicy
用于设定发送广播包时,对其他设备的scan request和connect request采取的过滤策略。过滤的地址需要提前存储到whitelist中。在后面whitelist介绍中详细解释。
可设置的4种过滤类型如下,若不需要whitelist过滤功能,选择ADV_FP_NONE。
typedef enum {
ADV_FP_ALLOW_SCAN_ANY_ALLOW_CONN_ANY = 0x00,
ADV_FP_ALLOW_SCAN_WL_ALLOW_CONN_ANY = 0x01,
ADV_FP_ALLOW_SCAN_ANY_ALLOW_CONN_WL = 0x02,
ADV_FP_ALLOW_SCAN_WL_ALLOW_CONN_WL = 0x03,
ADV_FP_NONE = ADV_FP_ALLOW_SCAN_ANY_ALLOW_CONN_ANY
} adv_fp_type_t;
返回值ble_sts_t可能出现的值和原因如下表所示:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。
按照Bluetooth Core Specification HCI部分Host command的设计,Set Advertising parameters同时设置了上面的8个参数。同时设置的思路也是合理的,因为一些不同的参数之间是有耦合关系的,比如advType和advInterval,在不同的advType下,对intervalMin和intervalMax的范围限定会不一样,所以会有不同的范围检查,如果将set advType和set advInterval拆成两个不同的API,彼此间的范围检查就无法控制。
bls_ll_setAdvEnable
详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.9 LE Set Advertising Enable command。
ble_sts_t blc_ll_setAdvEnable(adv_en_t adv_enable);
en为1时,Enable Advertising;en为0时,Disable Advertising。
Enable或Disable Advertising的状态机变化可以参考3.2.2.2 Link Layer状态组合。
返回值ble_sts_t可能出现的值和原因如下表所示:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
HCI_ERR_CONN_REJ_LIMITED_RESOURCES | 0x0D | appMaxPeripheralNum为0,不允许设置 |
blc_ll_setAdvCustomedChannel
该API用于定制特殊的advertising channel & scanning channel,只对一些非常特殊的应用有意义,如BLE mesh。
void blc_ll_setAdvCustomedChannel(u8 chn0, u8 chn1, u8 chn2);
chn0/chn1/chn2填需要定制的频点,默认的标准频点是37/38/39,比如设置3个advertising channel分别为 2420 MHz、2430 MHz、2450 MHz,可如下调用:
blc_ll_setAdvCustomedChannel (8, 12, 22);
常规BLE应用使用该API可以实现这样的功能,如果user在一些使用场景希望用到单通道广播&单通道扫描,比如将advertising channel & scanning channel固定为39,可如下调用:
blc_ll_setAdvCustomedChannel (39, 39, 39);
需要注意的是该API会同时更改广播和扫描的通道。
blc_ll_setScanParameter
详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.10 LE Set Scan Parameters command。
ble_sts_t blc_ll_setScanParameter ( scan_type_t scan_type,
u16 scan_interval, u16 scan_window,
own_addr_type_t ownAddrType,
scan_fp_type_t scanFilter_policy);
参数解析:
(1) scan_type
可选择passive scan和active scan,区别是active scan会在收到adv packet基础上发scan_req以获取设备scan_rsp的更多信息,scan rsp包也会通过adv report event传给BLE Host;passive scan不发scan req。
typedef enum {
SCAN_TYPE_PASSIVE = 0x00,
SCAN_TYPE_ACTIVE = 0x01,
} scan_type_t;
(2) scan_interval/scan window
scan_interval设置Scanning state时频点的切换时间,单位为0.625 ms,scan_window为扫描窗口时间。如果设置的scan_window > scan_interval,实际的scan window设置为scan_interval。
底层会根据scan_window / scan_interval 计算出scan_percent,以达到降低功耗的目的。Scanning的具体细节可以参考“3.2.3.2”和“3.2.3.4”和“3.2.3.5”。
(3) ownAddrType
指定scan req包地址类型时,ownAddrType有4个可选的值如下:
typedef enum{
OWN_ADDRESS_PUBLIC = 0,
OWN_ADDRESS_RANDOM = 1,
OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC = 2,
OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM = 3,
}own_addr_type_t;
OWN_ADDRESS_PUBLIC表示Scan的时候使用public MAC address,实际地址来自MAC address初始化时API blc_initMacAddress(int flash_addr, u8 mac_public, u8 mac_random_static)的设置。
OWN_ADDRESS_RANDOM表示Scan的时候使用random static MAC address,该地址来源于下面API设定的值:
ble_sts_t blc_llms_setRandomAddr(u8 *randomAddr);
(4) scan filter policy
typedef enum {
SCAN_FP_ALLOW_ADV_ANY=0x00,//except direct adv address not match
SCAN_FP_ALLOW_ADV_WL=0x01,//except direct adv address not match
SCAN_FP_ALLOW_UNDIRECT_ADV=0x02,//and direct adv address match initiator's resolvable private MAC
SCAN_FP_ALLOW_ADV_WL_DIRECT_ADV_MACTH=0x03, //and direct adv address match initiator's resolvable private MAC
} scan_fp_type_t;
目前支持的scan filter policy为下面2个:
SCAN_FP_ALLOW_ADV_ANY表示Link Layer对scan到的adv packet不做过滤,直接report到BLE Host。
SCAN_FP_ALLOW_ADV_WL则要求scan到的adv packet必须是在whitelist里面的,才report到BLE Host。
返回值ble_sts_t可能出现的值和原因如下表所示:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。
blc_ll_setScanEnable
详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.11 LE Set Scan Enable command。
ble_sts_t blc_ll_setScanEnable(scan_en_t scan_enable, dupFilter_en_t filter_duplicate);
scan_enable参数类型有如下2个可选值:
typedef enum {
BLC_SCAN_DISABLE = 0x00,
BLC_SCAN_ENABLE = 0x01,
} scan_en_t;
scan_enable为1时,Enable Scanning;scan_enable为0时,Disable Scanning。
Enable/Disable Scanning的状态机变化可以参考3.2.2.2 Link Layer状态组合。
filter_duplicate参数类型有如下2个可选值:
typedef enum {
DUP_FILTER_DISABLE = 0x00,
DUP_FILTER_ENABLE = 0x01,
} dupFilter_en_t;
filter_duplicate为 1 时,表示开启重复包过滤,此时对每个不同的adv packet,Controller只向Host上报一次adv report event;filter_duplicate为 0 时,不开启重复包过滤,对scan到的adv packet会一直上报给Host。
返回值ble_sts_t见下表:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
当设置了scan_type为active scan(3.2.8.7 blc_ll_setScanParameter)、Enable Scanning后,对每个device,只读一次scan_rsp并上报给Host。因为每次Enable Scanning后,Controller会对不同设备的scan_rsp进行记录,将它们存储到scan_rsp列表里,确保后面不会再次去读该设备的scan_req。
若user需要多次上报同一个device的scan_rsp,可以通过blc_ll_setScanEnable重复设置Enable Scanning实现,因为每次Enable/Disable Scanning时,设备的scan_rsp列表都会清0。
blc_ll_createConnection
详情请参照Bluetooth Core Specification V5.4, Vol 4, Part E, 7.8.12 LE Create Connection command。
ble_sts_t blc_ll_createConnection(u16 scan_interval,u16 scan_window,
init_fp_type_t initiator_filter_policy,
u8 adr_type, u8 *mac, u8 own_adr_type,
u16 conn_min, u16 conn_max,u16 conn_latency,
u16 timeout, u16 ce_min, u16 ce_max )
(1) scan_inetrval/scan window
scan_interval/scan_window在该API中暂时没有处理。如果需要设置可以使用“3.2.8.7 blc_ll_setScanParameter”。
(2) initiator_filter_policy
指定当前连接设备的策略,可选如下两种:
typedef enum {
INITIATE_FP_ADV_SPECIFY = 0x00, //connect ADV specified by host
INITIATE_FP_ADV_WL = 0x01, //connect ADV in whiteList
} init_fp_type_t;
INITIATE_FP_ADV_SPECIFY表示连接的设备地址是后面的adr_type/mac;
INITIATE_FP_ADV_WL表示根据whitelist里面的设备来连接,此时adr_type/mac无意义。
(3) adr_type/ mac
initiator_filter_policy为INITIATE_FP_ADV_SPECIFY时,连接地址类型为adr_type(BLE_ADDR_PUBLIC或者BLE_ADDR_RANDOM)、地址为mac[5…0]的设备。
(4) own_adr_type
指定建立连接的Central role使用的MAC address类型。ownAddrType 4个可选的值如下。
typedef enum{
OWN_ADDRESS_PUBLIC = 0,
OWN_ADDRESS_RANDOM = 1,
OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC = 2,
OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM = 3,
}own_addr_type_t;
OWN_ADDRESS_PUBLIC表示连接的时候使用public MAC address,实际地址来自MAC address初始化时API blc_llms_initStandby_module (mac_public)的设置。
OWN_ADDRESS_RANDOM表示连接的时候使用random static MAC address,该地址来源于下面API设定的值:
ble_sts_t blc_llms_setRandomAddr (u8 *randomAddr);
(5) conn_min/ conn_max/ conn_latency/ timeout
这4个参数规定了建立连接后Central role的连接参数,同时这些参数也会通过connection request发给Peripheral,Peripheral也会是同样的连接参数。
conn_min/conn_max指定conn interval的范围,单位为1.25 ms。如果appMaxCentralNum > 1,conn_min/conn_max参数无效,SDK中Central role conn interval默认固定为25(实际interval为31.25 ms = 25 ×1.25 ms),这种情况下可以在建立连接前调用blc_ll_setAclCentralConnectionInterval更改设置;如果appMaxCentralNum为1,SDK中Central role conn interval直接使用conn_max的值。
conn_latency指定connection latency,一般设为0。
timeout指定connection supervision timeout,单位为10 ms。
(6) ce_min/ ce_max
Telink B91m BLE Multiple Connection SDK 暂未处理ce_min/ ce_max。
返回值列表:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
HCI_ERR_CONN_REJ_ LIMITED_RESOURCES | 0x0D | Link Layer已经处于Initiating state,不再接收新的create connection或当前device处于Connection state |
API不会进行参数合理性检查,user需要注意设置参数的合理性。
blc_ll_setCreateConnectionTimeout
ble_sts_t blc_ll_setCreateConnectionTimeout(u32 timeout_ms);
返回值为BLE_SUCCESS,timeout_ms单位为ms。
当blc_ll_createConnection触发进入Initiating state后,如果长时间无法建立连接,会触发Initiate timeout,退出Initiating state。
Telink B91m BLE Multiple Connection SDK 默认的Initiate timeout时间为5秒。如果User不希望使用默认的这个时间,可以调用blc_ll_setCreateConnectionTimeout设置自己需要的Initiate timeout。
blc_ll_setAclCentralConnectionInterval
ble_sts_t blc_ll_setAclCentralConnectionInterval(u16 conn_interval);
返回值为BLE_SUCCESS,conn interval单位为1.25 ms。
这个API设置Central base connection interval基准,通过这个基准可以将多个Central的时序错开,保证多Central同时连接的情况下时序不冲突,提高数据传输效率。实际生效的Central connection interval是这个基准的1/2/3/4/6/8/12倍。
blc_ll_setAutoExchangeDataLengthEnable
void blc_ll_setAutoExchangeDataLengthEnable(int auto_dle_en)
auto_dle_en:0,禁止stack自动进行DLE交互;1,stack主动进行DLE交互。
默认情况下,如果初始化DLE的长度不是27,stack在连接后会自动进行DLE的交互。如果用户不希望stack主动交互,可以使用此API关闭。等需要进行交互的时候,由用户自己调用相关API blc_ll_sendDateLengthExtendReq再进行交互。
注意:
如果要禁止stack自动交互DLE,需要在初始化的时候调用该API。
blc_ll_sendDateLengthExtendReq
ble_sts_t blc_ll_sendDateLengthExtendReq (u16 connHandle, u16 maxTxOct)
connHandle:指定需要更新连接参数的connection。
maxTxOct:需要设置的DLE长度。
返回类型ble_sts_t可能返回的结果如下表所示:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。
blc_ll_setDataLengthReqSendingTime_after_connCreate
void blc_ll_setDataLengthReqSendingTime_after_connCreate(int time_ms)
设置pending 时间。
用于设置在连接后等待time_ms(单位:毫秒)后执行DLE的交互。
blc_ll_disconnect
ble_sts_t blc_ll_disconnect(u16 connHandle, u8 reason);
调用此API在Link Layer上发送一个terminate给peer Central/Peripheral device,主动断开连接。
connHandle为当前connection的handle值。
reason为断开原因,reason的设置详请参照Bluetooth Core Specification V5.4, Vol 1, Part F, 2 Error code descriptions。
若不是系统运行异常导致的terminate,应用层一般指定reason为HCI_ERR_REMOTE_USER_TERM_CONN = 0x13,blc_ll_disconnect(connHandle, HCI_ERR_REMOTE_USER_TERM_CONN);
调用该API主动发起断开连接后,一定会触发HCI_EVT_DISCONNECTION_COMPLETE事件,在该事件的回调函数里可以看到对应的terminate reason和这个手动设置的reason是一样的。
一般情况下直接调用该API可以成功发送terminate并断连,但也存在一些特殊情况会导致该API调用失败,根据返回值ble_sts_t可以了解对应的错误原因。建议应用层调用该API时,检查一下返回值是否为BLE_SUCCESS。
返回值如下表。
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
HCI_ERR_UNKNOWN_CONN_ID | 0x02 | connHandle错误或找不到对应的连接 |
HCI_ERR_CONN_REJ_LIMITED_RESOURCES | 0x3E | 大量数据正在发送,暂时无法接受该命令 |
rf_set_power_level_index
Telink B91m BLE Multiple Connection SDK 提供了BLE RF packet能量设定的API。
API原型如下:
void rf_set_power_level_index (rf_power_level_index_e idx);
B91的idx设置参考drivers/B91/rf.h中定义的枚举变量rf_power_level_index_e,B92的idx设置参考drivers/B92/rf.h中定义的枚举变量rf_power_level_index_e。
该API设定的RF发包能量,对广播包和连接包同时有效,且在程序的任意位置都可以设置,实际发包时的能量以时间上最近一次的设置为准。
注意:
rf_set_power_level_index这个函数内部是对MCU RF相关的一些寄存器进行设置,而一旦MCU进入sleep(包括suspend/deepsleep retention)后,这些寄存器的值都会丢失。所以user需要注意,每次sleep唤醒后,这个函数必须得重新设置一遍。比如在SDK demo中使用了BLT_EV_FLAG_SUSPEND_EXIT事件回调,来确保每次suspend醒来rf power都被重新设置一遍。
Whitelist & Resolvinglist
前面介绍过,Advertising/Scanning/Initiating state的filter_policy中都涉及到Whitelist,会根据Whitelist中的设备进行相应的操作。实际Whitelist概念中包含Whitelist和Resolvinglist两部分。
通过peer_addr_type和peer_addr可以判断peer device地址类型是否RPA(resolvable private address)。使用下面的宏判断即可。
#define IS_NON_RESOLVABLE_PRIVATE_ADDR(type, addr)
((type)==BLE_ADDR_RANDOM && (addr[5] & 0xC0) == 0x00)
非RPA地址才可以存储到whitelist中,目前Telink B91m BLE Multiple Connection SDK 中whitelist最多存储4个设备。
Whitelist相关API如下:
ble_sts_t blc_ll_clearWhiteList(void);
Reset whitelist,返回值为BLE_SUCCESS。
ble_sts_t blc_ll_addDeviceToWhiteList(u8 type, u8 *addr);
添加一个设备到whitelist,返回值列表:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
HCI_ERR_MEM_CAP_EXCEEDED | 0x07 | whitelist已满,添加失败 |
ble_sts_t blc_ll_removeDeviceFromWhiteList(u8 type, u8 *addr);
从whitelist删除之前添加的设备,返回值为BLE_SUCCESS。
RPA(resolvable private address)设备,需要使用Resolvinglist。为了节省ram使用,目前Telink B91m BLE Multiple Connection SDK 中 Resolvinglist最多存储2个设备:
#define MAX_RESOLVING_LIST_SIZE 2
Resolvinglist相关API如下:
ble_sts_t blc_ll_clearResolvingList(void);
Reset Resolvinglist。返回值BLE_SUCCESS。
ble_sts_t blc_ll_setAddressResolutionEnable (addr_res_en_t resolution_en);
设备地址解析使用,如果要使用Resolvinglist 解析地址,一定要打开使能。不需要解析的时候,可以关闭。
ble_sts_t blc_ll_addDeviceToResolvingList(ida_type_t peerIdAddrType, u8 *peerIdAddr, u8 *peer_irk, u8 *local_irk);
添加使用RPA地址的设备,peerIdAddrType/ peerIdAddr和peer-irk填peer device宣称的identity address和irk,这些信息会在配对加密过程中存储到Flash中,user可以在“3.4.4 SMP”找到获取这些信息的接口。对于local_irk, SDK暂时没有处理,填NULL即可。
ble_sts_t blc_ll_removeDeviceFromResolvingList(ida_type_t peerIdAddrType, u8 *peerIdAddr);
删除之前添加的设备。返回值BLE_SUCCESS。
Whitelist/Resolvinglist实现地址过滤的使用,请参考feature_test/feature_whitelist工程。
在vendor/feature_test/app_config.h中定义宏:
#define FEATURE_TEST_MODE TEST_WHITELIST
Host Controller Interface
HCI(Host Controller Interface)是Host和Controller交换数据的桥梁,它定义了Host和Controller交互的各种数据类型,如:CMD,Event,ACL,SCO,ISO等。HCI使得蓝牙Host和Controller的实现在不同硬件平台上成为可能。HCI具体内容可以查看Bluetooth Core Specification V5.4, Vol 4: Host Controller Interface。
HCI Transport是HCI的传输层,负责传输HCI各种数据类型的数据。 HCI Transport 定义了HCI不同数据类型的Type Indicator,如下图所示。
HCI软件架构
Telink B91m BLE Multiple Connection SDK 中HCI软件架构如下图所示。HCI Transport是HCI Transport Layer的软件实现,这部分源码完全向用户开放;Controller HCI主要实现了HCI CMD和HCI ACL的解析和处理以及产生HCI Event,同时为HCI Transport提供功能接口。本节将围绕HCI Transport和Controller HCI接口来详细描述Telink HCI的使用方法。
HCI Transport用于传输HCI协议包,它不需要解析HCI协议包,只需要按照HCI Type接收HCI 协议包,然后交给Controller HCI处理即可。HCI Transport支持多种硬件接口,如:USB,UART,SDIO等,其中常用的是UART接口,Telink B91m BLE Multiple Connection SDK 目前只提供了UART接口的HCI Transport。Telink BLE HCI transport的软件实现可以查看SDK中的vendor/common/hci_transport文件夹。
HCI UART Transport 支持两种协议,H4和H5,这两种协议Telink B91m BLE Multiple Connection SDK 都支持,且以源码的形式开放使用。Telink SDK HCI Transport的软件架构如下图所示。
H4 Protocol是HCI UART Transport H4协议的软件实现;H5 Protocol是HCI UART Transport H5协议的软件实现;HCI Transport Control是HCI Transport的配置管理层,提供用户使用HCI Transport所需要的一切,因此,使用HCI Transport的用户只需要关注该层即可。
H4 Protocol
H4 PDU
H4 PDU格式如下图所示。
H4 PDU由HCI Type Indicator和Payload组成。HCI Type Indicator指明了Payload的内容,HCI Type Indicator可取的值如下图所示;Payload可以是HCI CMD,HCI ACL和HCI Event等HCI Protocol包。
Host发送H4 PDU到Controller,H4 Protocol软件实现了H4 PDU的接收和解析,具体实现可以查看vendor/common/hci_transport文件夹中的hci_tr_h4.c 和 hci_tr_h4.h文件。
用户在使用H4协议软件时需要根据需求配置UART Rx Buffer大小和Buffer个数。可以通过hci_tr_h4.h文件中的宏HCI_H4_TR_RX_BUF_SIZE和HCI_H4_TR_RX_BUF_NUM来配置。实际上,为了方便用户的使用,SDK 会自动计算H4 UART Buffer Size。用户只需要配置hci_tr.h文件中的宏HCI_TR_RX_BUF_SIZE即可。 HCI_H4_TR_RX_BUF_NUM用户一般不需要修改,除非有特殊需求。
H4 API
H4 Protocol 提供了3个API:
void HCI_Tr_H4Init(hci_fifo_t *pFifo);
void HCI_Tr_H4RxHandler(void);
void HCI_Tr_H4IRQHandler(void);
**void HCI_Tr_H4Init(hci_fifo_t *pFifo) **
功能: 该函是H4的初始化,包括UART,RX Buffer等。
参数:
参数 | 描述 |
---|---|
pFifo | 指向Controller HCI Rx FIFO, 用于存储接收并解析成功的HCI协议包供Controller 处理。该参数由Controller HCI提供 |
说明: 该函数用户一般不需要调用,它由HCI Transport Control层调用。
**void HCI_Tr_H4RxHandler(void) **
功能: 该函数实现了H4 PDU的解析和处理功能。
参数: 无。
说明: 该函数用户一般不需要调用,它由HCI Transport Control层调用。
**void HCI_Tr_H4IRQHandler(void) **
功能: 该函数实现了UART RX/TX中断处理。
参数: 无。
说明: 该函数用户一般不需要调用,它由HCI Transport Control层调用。
H5 Protocol
H5又称3wire UART。H5支持软件流控和重传机制。相较H4来说,H5具有更高的可靠性,但是其传输效率没有H4高。H5具体内容参看Bluetooth Core Specification V5.4, Vol 4, Part D: Three-wire UART Transport Layer。
H5 PDU(Protocol Data Unit)在传输之前需要被编码,在解析H5 PDU之前需要解码,H5 PDU的编解码由Slip Layer完成。
Telink H5软件架构如所示。UART用于数据的收发;Slip层实现了H5 PDU的编码器和解码器,负责H5 PDU的编码和解码;H5 Handler实现了H5 PDU的解析和处理、H5 Link的创建以及流量控制和重传控制。用户可以在vendor/common/hci_transport文件夹中的hci_tr_h5.c,hci_slip.c,hci_h5.c文件中查看H5的实现。
Slip Layer
**(1)Slip 编码 **
Slip layer编码时会在每个H5包的开始和结束的地方放置一个字节的C0,H5包中出现的所有C0都将被编码成DB DC序列;H5包中出现的所有DB将被编码成DB DD序列;这里的DB DC和DB DD被称为Slip的转义序列,所有Slip的转义序列都开始于DB。Slip的转义序列表如下图所示。
Telink B91m BLE Multiple Connection SDK 中Slip的编码是通过API HCI_Slip_EncodePacket(u8 *p, u32 len)来实现的。编码后的数据将存储在Slip Encode buffer中。Slip的Encode Buffer Size可以通过宏HCI_SLIP_ENCODE_BUF_SIZE来设置,为了方便用户使用,SDK已经对HCI_SLIP_ENCODE_BUF_SIZE实现了自动计算,不需要用户配置。用户只需要配置 vendor/common/hci_transport/hci_tr.h文件中的宏HCI_TR_RX_BUF_SIZE即可。
**(2) Slip解码 **
当收到Slip包以后,通过Slip包的起始和结束标志C0即可得到一个完整的Slip包。然后根据Slip的转义字节表将DB DC和DB DD序列等转化为C0和 DB,这样Slip就被解码了,接下来就是解析H5 PDU。
Telink B91m BLE Multiple Connection SDK 中Slip的解码由void HCI_Slip_DecodePacket(u8 *p, 32 len)来实现,解码后的数据存储在Slip Decode Buffer中。Slip解码Buffer的大小可以通过宏HCI_SLIP_DECODE_BUF_SIZE设置。为了方便用户使用,SDK已经对HCI_SLIP_DECODE_BUF_SIZE实现了自动计算,不需要用户配置。用户只需要配置vendor/common/hci_transport/hci_tr.h文件中的宏HCI_TR_RX_BUF_SIZE即可。
H5 Handle
H5 Handler实现了H5 PDU的解析和处理、H5 Link 创建以及流量控制和重传控制。
(1) H5 PDU
H5 PDU(Protocol Data Unit)。H5 PDU包含Packet Header,Payload和可选的Data Integrity Check 3个字段。
H5 PDU Header构成如下:
Sequence Number(SEQ):对于不可靠包来说,SEQ应该设置为0 ;对于可靠包来说,SEQ表示包的序号。每收到一个新包,SEQ应该加1 。SEQ的范围0-7。SEQ不变,表示重传。
Acknowledgment Number(ACK):ACK应该设置为设备期望的下一个包序号,范围0-7。
Data Integrity Check Present:设置为1,表示需要对PDU的Payload字段进行CRC校验,也即PDU中的Data Integrity Check存在,否则不存在
Reliable Packet:设置为1,使用可靠传输,SEQ将生效。
Packet Type:H5定义了8种包类型:
Payload length:PDU中payload字段的长度。
Header CheckSum:H5 PDU Header的和校验值。
H5 Handler通过函数void HCI_H5_PacketHandler(u8 *p, u32 len)来实现对H5 PDU的解析和处理,这个函数是H5 内部使用的函数,用户不需要调用。
(2) H5 Link 建立和配置信息交换
Host和Controller交换H5数据包之前需要先建立H5 连接并协商配置信息。H5 link 建立需要使用SYNC message、SYNC_RSP message、CONFIG message和CONFIG_RSP message。
初始时,H5处于Uninitialized 状态,并且不断发送SYNC message。当收到SYNC_RSP message以后进入Initialized状态,并不断发送CONFIG message;当收到CONFIG_RSP message以后,H5进入Active状态,此时H5 link建立成功,可以收发数据。
Telink H5初始时以250 ms的间隔发送SYNC message,当收到CONFIG_RSP message以后,进入Initialized状态,并以250 ms的间隔发送CONFIG message,当收到CONFIG_RSP message以后,进入连接态。
在CONFIG message和CONFIG_RSP message中包含Host和Controller使用的连接参数,双方取共同部分作为最终的连接参数。H5连接的配置信息主要包括Sliding Window Size、OOF Flow Control、Data Integrity Check Type和Version。
Sliding Window Size:设置不需要立即ACK的最大数据包数。当Sliding Window Size = 1时,意味着Controller发送一包以后,必须等到Host ACK以后才能传输下一个包;当Sliding Window Size = N (N>1)时,Controller可以发送N包而不需要等待Host的ACK,但是Controller发送第N包以后,必须等待Host ACK才能发送其他数据包。Sliding Window Size目的是提高H5传输效率。目前Telink SDK只支持Sliding Window = 1的情况,Sliding Window > 1的情况也很容易扩展。
OOF Flow Control:使能软件流控,这个不常用,故不做详述。
Data Integrity Check Type:设置为1,则H5 PDU中与Data Integrity Check相关的字段都将启用。
Version:设置H5 (3 wire UART)版本。目前是v1.0版本。
Telink B91m BLE Multiple Connection SDK 中,用户可以通过如下宏来配置H5连接参数。
#define HCI_H5_SLIDING_WIN_SIZE 1
#define HCI_H5_OOF_FLW_CTRL HCI_H5_OOF_FLW_CTRL_NONE
#define HCI_H5_DATA_INTEGRITY_LEVEL HCI_H5_DATA_INTEGRITY_LEVEL_NONE
#define HCI_H5_VERSION HCI_H5_VERSION_V1_0
(3) H5数据交互以及重传
Host和Controller建立H5连接以后双方就可以交互数据包了。H5支持流控和重传机制,流控和重传是通过H5 PDU Header中的SEQ和ACK 字段来实现的。下图是H5数据交互以及重传的例子。
设备A发送SEQ为6,ACK为3的包给设备B,SEQ为6表示设备B期望的包序号是6,ACK为3表示设备A期望的下一个包序号是3。设备B收到设备A的包以后,发送SEQ为3,ACK为7的包给设备A。设备B的SEQ为3,是因为设备A期望的包是3;设备B的ACK为7,是因为设备B收到了序号为6的包而期望新包7,依次类推。
设备A发送SEQ为0,ACK为5的包给设备B,但是由于一些原因设备B没有收到这个包,设备B由于没有收到设备A的包,一段时间以后会重传上一个包。
流控:
Host和Controller建立H5连接以后,主要交互Data包和pure ACK包。Pure ACK包是H5 Packet Type为0的包。正常情况下,对端设备发送一个Data包,本地设备回复一个Data包,然后持续进行下去,但是对端和本地设备总会有没有Data包可发送的时候,这个时候设备可以发送pure ACK包来代替Data包,例如:host使能了scan,controller会不断的上报adv report,controller会有源源不断的Data 包,但是Host却没有大量的Data包可发送,如果controller发送data 包到host,却收不到host的回复,那么就会导致controller一直重传,host将再也收不到新的adv report,此时host需要发送pure ACK包来代替Data包,相当于回复controller。这样host会不断的收到新的adv report。
重传:
重传有多种情况:本地设备发送data包以后,如果在timeout到达之前还没收到对端设备的回复(data包或者pure ACK包),本地设备会resend;如果收到对方的data包或者pure ACK包中的ACK值指示需要本地重传的,本地设备将resend。重传时SEQ应该保持不变。
H5相关API
H5有两个重要的API:
void HCI_H5_Init(hci_fifo_t *pHciRxFifo, hci_fifo_t *pHciTxFifo);
void HCI_H5_Poll(void);
void HCI_H5_Init(hci_fifo_t *pHciRxFifo, hci_fifo_t *pHciTxFifo)
功能: 该函数用于初始化H5。
参数:
参数 | 描述 |
---|---|
pHciRxFifo | 指向Controller HCI Rx FIFO |
pHciTxFifo | 指向Controller HCI Tx FIFO,H5将接管HCI Tx FIFO,不需要用户管理,降低了使用难度。 说明:该函数用户一般不需要调用,它由HCI Transport Control层调用。 |
说明: 该函数用户一般不需要调用,它由HCI Transport Control层调用。
void HCI_H5_Poll(void)
功能: 该函数用于管理H5包的解析、发送、resend和流控。
参数: 无。
说明: 该函数用户一般不需要调用,它由HCI Transport Control层调用。
HCI Transport Control
HCI Transport Control是Telink HCI Transport的集中管理层,它是连接HCI Transport和Controller HCI的桥梁,同时,它也为用户提供了配置和使用HCI Transport所需的宏和API。对于使用Telink Controller工程的用户来说,只需要关注HCI Transport Control层即可。HCI Transport Control提供的宏和API可以在 Controller工程中的hci_tr.h里找到。
HCI Transport 配置
HCI Transport Control提供了一系列的配置宏,下面详细描述。
用户可以通过如下的宏来选择使用的transport 协议。Telink B91m BLE Multiple Connection SDK 默认使用HCI_TR_H4。
/*! HCI transport layer protocol selection. */
#define HCI_TR_H4 0
#define HCI_TR_H5 1
#define HCI_TR_USB 2 /*!< Not currently supported */
#define HCI_TR_MODE HCI_TR_H4
用户可以通过如下宏设置Transport Rx Patch和Tx Path上UART buffer 最大Size。HCI_TR_RX_BUF_SIZE应该设置为最大可能接收到的HCI包的大小;HCI_TR_TX_BUF_SIZE应该设置为最大可能发送的HCI包大小,这对于H4和H5都适用。例如:最大Rx HCI ACL为27B,最大Rx HCI Cmd Payload为65B,那么HCI_TR_RX_BUF_SIZE应该设置为1B(HCI Type length) + 4B (HCI ACL Header Length) + MAX(27,65) = 70B,HCI_TR_TX_BUF_SIZE计算方法一样。
#define HCI_TR_RX_BUF_SIZE HCI_RX_FIFO_SIZE
#define HCI_TR_TX_BUF_SIZE HCI_TX_FIFO_SIZE
/*! HCI UART transport pin define */
#define HCI_TR_RX_PIN GPIO_PB0
#define HCI_TR_TX_PIN GPIO_PA2
#define HCI_TR_BAUDRATE (1000000)
HCI_TR_RX_PIN、HCI_TR_TX_PIN用于设置UART Tx/Rx引脚。HCI_TR_BAUDRATE用于设置UART波特率。
关于UART波特率选择需要注意:由于BLE可以采用1M和2M,因此UART波特率应该做相应的匹配,否则当传输ACL Data量很大时可能会出现buffer不够的情形;另外,当波特率很低时,调大buffer和buffer个数将会消耗较大RAM,因此需要做好波特率匹配。我们推荐当使用的BLE速率是1M时,UART波特率选择最好大于等于1M;当使用的BLE速率是2M时,UART波特率选择最好大于等于2M。
HCI Transport API
为了方便用户使用,HCI Transport最终给用户留了必要的API,实际使用时用户只需要调用这些API即可。
void HCI_TransportInit(void);
void HCI_TransportPoll(void);
void HCI_TransportIRQHandler(void);
void HCI_TransportInit(void)
功能: 该函数是各种Transport协议初始化的封装。用户使用HCI Transport功能之前需要通过该函数初始化HCI Transport。
参数: 无
void HCI_TransportPoll(void)
功能: 该函数是对各种 Transport协议任务处理器的封装,用户需要在main loop中调用。
参数: 无
void HCI_TransportIRQHandler(void)
功能: 该函数是对各种Transport协议使用中断的封装。用户需要在中断中调用该API。
参数: 无
Controller HCI
Controller HCI接口实现了HCI协议包的解析和处理以及产生HCI Event。HCI具体内容参看Bluetooth Core Specification V5.4 Vol 4。本节将讲解几个重要的API。
Controller HCI提供必要的API供用户使用。
ble_sts_t blc_ll_initHciRxFifo(u8 *pRxbuf, int fifo_size, int fifo_number);
ble_sts_t blc_ll_initHciTxFifo(u8 *pTxbuf, int fifo_size, int fifo_number);
ble_sts_t blc_ll_initHciAclDataFifo(u8 *pAclbuf, int fifo_size, int fifo_number);
int blc_hci_handler(u8 *p, int n);
blc_ll_initHciRxFifo(u8 *pRxbuf, int fifo_size, int fifo_number)
功能: 该函数用于初始化HCI Rx FIFO。HCI Rx FIFO可以管理多组Rx buffer。 HCI Rx FIFO用于存储接收到的HCI包。HCI Rx Buffer需要用户在应用层定义并通过本函数注册。controller工程中已经定义好了HCI Rx Buffer,用户可以参看工程中的app_buffer.c和app_buffer.h文件。
参数:
参数 | 说明 |
---|---|
pRxbuf | 指向Rx buffer |
fifo_size | Rx FIFO中每个buffer的size,必须是16字节对齐 |
fifo_number | Rx FIFO中buffer的个数,必须是2的指数幂 |
说明:用户需要在初始化时调用
blc_ll_initHciTxFifo(u8 *pTxbuf, int fifo_size, int fifo_number)
功能: 该函数用于初始化HCI Tx FIFO。HCI Tx FIFO可以管理多组Tx buffer。HCI Tx FIFO用于存储Controller将要发送给Host的HCI Evt和HCI ACL。HCI Tx Buffer需要用户在应用层定义并通过本函数注册。 controller工程中已经定义好了HCI Tx Buffer,用户可以参看工程中的app_buffer.c和app_buffer.h文件。
参数:
参数 | 描述 |
---|---|
pTxbuf | 指向Tx buffer |
fifo_size | Tx FIFO中每个buffer的size,必须是4字节对齐 |
fifo_number | Tx FIFO中buffer的个数,必须是2的指数幂 |
说明:用户需要在初始化时调用
blc_ll_initHciAclDataFifo(u8 *pAclbuf, int fifo_size, int fifo_number)
功能: 该函数用于初始化HCI ACL FIFO。HCI ACL FIFO可以管理多组ACL buffer。HCI ACL FIFO用于存储Host下发给Controller的ACL Data。 HCI ACL Buffer需要用户在应用层定义并通过本函数注册。 controller工程中已经定义好了HCI ACL Buffer,用户可以参看工程中的app_buffer.c和app_buffer.h文件。
参数:
参数 | 说明 |
---|---|
pAclbuf | 指向Acl buffer |
fifo_size | Acl FIFO中每个buffer的size,必须是4字节对齐 |
fifo_number | Acl FIFO中buffer的个数,必须是2的指数幂 |
说明:用户需要在初始化时调用
int blc_hci_handler(u8 *p, int n)
功能:该函数是Controller HCI包处理器,实现了HCI CMD和ACL包的解析和处理。
参数:
参数 | 说明 |
---|---|
p | 指向接收到的HCI协议包(使用H4 PDU格式) |
n | 未使用,因为HCI协议包中包含的length信息 |
说明: 该函数被HCI Transport Control层调用,一般情况下不需要用户调用。
Controller Event
为了满足user在应用层对multiple connection BLE stack底层一些关键动作的记录和处理,SDK提供了两种类型的event如下图所示:一是BLE controller定义的标准的HCI event;二是BLE host定义的一些协议栈流程交互的事件通知型GAP event(也可以认为是host event,这部分具体介绍请参考本文档“GAP event”章节)。本小节主要介绍Controller event。
注意:
在Telink B91 BLE Single Connection SDK 上,telink提供了自己定义的一套controller event,和Bluetooth Core Specification 规定的HCI event大部分是一样的,在Telink B91m BLE Multiple Connection SDK 中,就去掉了重复部分中telink 定义的event,user使用标准的event即可。
Controller HCI Event 分类
Controller HCI event是按Bluetooth Core Specification标准设计的。
如下图Host + Controller架构所示,Controller HCI event是通过HCI将Controller所有的event报告给Host。
Controller HCI event的定义,详情请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.7 Events。其中7.7.65 LE Meta event指HCI LE(low energy) event,其他的都是普通的HCI event。Telink B91m BLE Multiple Connection SDK也将Controller HCI event分为两类:HCI event和HCI LE event。由于Telink B91m BLE Multiple Connection SDK 主打低功耗蓝牙,所以对HCI event只支持了最基本的几个,而HCI LE event绝大多数都支持。
Controller HCI event相关的宏定义、接口定义等请参考stack/ble/hci目录下的头文件。
如果user需要在Host或App层接收Controller HCI event,首先需要注册Controller HCI event的callback函数,其次需要将对应event的mask打开,mask打开API见下面event分析。
Controller HCI event的callback函数原型和注册接口分别为:
typedef int (*hci_event_handler_t) (u32 h, u8 *para, int n);
void blc_hci_registerControllerEventHandler(hci_event_handler_t handler);
callback函数原型中的u32 h是一个标记,底层协议栈多处会用到,user只需要知道下面一个即可:
#define HCI_FLAG_EVENT_BT_STD (1<<25)
HCI_FLAG_EVENT_BT_STD这个标志表示当前event为Controller HCI event。
Callback函数原型中para和n表示event的数据和数据长度,该数据和Bluetooth Core Specification中定义的一致。用户可参考代码中的如下用法以及app_controller_event_callback函数的具体实现。
blc_hci_registerControllerEventHandler(app_controller_event_callback);
常用Controller HCI event
Telink B91m BLE Multiple Connection SDK 中支持了绝大多数HCI event,下面介绍客户可能会用到的event。
#define HCI_EVT_DISCONNECTION_COMPLETE 0x05
#define HCI_EVT_LE_META 0x3E
(1) HCI_EVT_DISCONNECTION_COMPLETE
详情请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.7.5 Disconnection Complete event。回调指针指向的数据结构如下:
typedef struct {
u8 status;
u16 connHandle;
u8 reason;
} hci_disconnectionCompleteEvt_t;
(2) HCI_EVT_LE_META
表示当前是HCI LE event,根据后面的sub event code判断具体的event类型。
HCI event中除了HCI_EVT_LE_META(HCI_EVT_LE_META使用blc_hci_le_setEventMask_cmd来打开event mask),其他都要通过下面API来打开event mask。
ble_sts_t blc_hci_setEventMask_cmd(u32 evtMask); //eventMask: BT/EDR
event mask的定义如下所示:
#define HCI_EVT_MASK_DISCONNECTION_COMPLETE 0x0000000010
若user未通过该API设置HCI event mask,SDK默认只打开HCI_EVT_MASK_DISCONNECTION_COMPLETE对应的mask,即保证Controller disconnect event的上报。
常用HCI LE event
当HCI event中event code为HCI_EVT_LE_META,就是HCI LE event,subevent code最常用的且user可能需要了解的如下,其他的不做介绍。
#define HCI_SUB_EVT_LE_CONNECTION_COMPLETE 0x01
#define HCI_SUB_EVT_LE_ADVERTISING_REPORT 0x02
#define HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE 0x03
#define HCI_SUB_EVT_LE_PHY_UPDATE_COMPLETE 0x0C
(1) HCI_SUB_EVT_LE_CONNECTION_COMPLETE
详情请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.7.65.1 LE Connection Complete event。回调指针指向的数据结构如下:
typedef struct {
u8 subEventCode;
u8 status;
u16 connHandle;
u8 role;
u8 peerAddrType;
u8 peerAddr[6];
u16 connInterval;
u16 PeripheralLatency;
u16 supervisionTimeout;
u8 CentralClkAccuracy;
} hci_le_connectionCompleteEvt_t;
(2) HCI_SUB_EVT_LE_ADVERTISING_REPORT
详情请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.7.65.2 LE Advertising Report event。回调指针指向的数据结构如下:
typedef struct {
u8 subcode;
u8 nreport;
u8 event_type;
u8 adr_type;
u8 mac[6];
u8 len;
u8 data[1];
} event_adv_report_t;
当controller的Link Layer scan到正确的adv packet后,通过 HCI_SUB_EVT_LE_ADVERTISING_REPORT上报给Host。
该event的数据长度不定(取决于adv packet的payload),如下所示,具体数据含义请直接参考Bluetooth Core Specification。
注意:
Telink B91m BLE Multiple Connection SDK中的LE Advertising Report Event每次只报一个adv packet,即上图中的i为1。
(3) HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE
详情请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.7.65.3 LE Connection Update Complete event。
当Controller上的connection update生效时,向Host上报 HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE。回调指针指向的数据结构如下:
typedef struct {
u8 subEventCode;
u8 status;
u16 connHandle;
u16 connInterval;
u16 connLatency;
u16 supervisionTimeout;
} hci_le_connectionUpdateCompleteEvt_t;
(4) HCI_SUB_EVT_LE_PHY_UPDATE_COMPLETE
详情请参考Bluetooth Core Specification V5.4, Vol 4, Part E, 7.7.65.12 LE PHY Update Complete event。
回调指针指向的数据结构如下:
typedef struct {
u8 subEventCode;
u8 status;
u16 connHandle;
u8 tx_phy;
u8 rx_phy;
} hci_le_phyUpdateCompleteEvt_t;
HCI LE event需要通过下面的API来打开mask。
ble_sts_t blc_hci_le_setEventMask_cmd(u32 evtMask); //eventMask: LE
evtMask的定义也对应上面给出一些,其他的event 用户可以在hci_const.h中查到。
#define HCI_LE_EVT_MASK_CONNECTION_COMPLETE 0x00000001
#define HCI_LE_EVT_MASK_ADVERTISING_REPORT 0x00000002
#define HCI_LE_EVT_MASK_CONNECTION_UPDATE_COMPLETE 0x00000004
若user未通过该API设置HCI LE event mask,SDK默认所有HCI LE event都不打开。
Host
L2CAP
逻辑链路控制与适配协议通常简称为L2CAP(Logical Link Control and Adaptation Protocol),它向上连接应用层,向下连接控制器层,发挥主机与控制器之间的适配器的作用,使上层应用操作无需关心控制器的数据处理细节。
BLE的L2CAP层是经典蓝牙L2CAP层的简化版本,它在基础模式下,不执行分段和重组,不涉及流程控制和重传机制,仅使用固定信道进行通信。L2CAP的简化结构如下图所示,简单说就是将应用层数据分包发给BLE controller,将BLE controller收到的数据打包成不同CID数据上报给host层。
L2CAP根据Bluetooth Core Specification设计,主要功能是完成Controller和Host的数据对接,绝大部分都在协议栈底层完成,需要user参与的地方很少。user根据以下几个API进行设置即可。
注册L2CAP数据处理函数
Telink B91m BLE Multiple Connection SDK 架构中,Controller的数据通过HCI与Host对接,从HCI到Host数据,首先会在L2CAP层处理,使用下面API注册该处理函数:
void blc_hci_registerControllerDataHandler (hci_data_handler_t handle);
L2CAP层处理Controller数据的函数为:
int blc_l2cap_pktHandler (u16 connHandle, u8 *raw_pkt);
该函数已经在协议栈中实现,它会对接收到的数据进行解析后向上传输给ATT、SIG或SMP。
初始化:
blc_hci_registerControllerDataHandler (blt_l2cap_pktHandler);
更新连接参数
(1) Peripheral请求更新连接参数
在BLE协议栈中,Peripheral通过L2CAP层CONNECTION PARAMETER UPDATE REQUEST 命令向 Central申请一组新的连接参数。该命令格式如下所示,详情请参照Bluetooth Core Specification V5.4, Vol 3, Part A, 4.20 L2CAP_CONNECTION_PARAMETER_UPDATE_REQ (code 0x12)。
Telink B91m BLE Multiple Connection SDK 提供了Peripheral主动申请更新连接参数的API,用来向Central发送CONNECTION PARAMETER UPDATE REQUEST命令。
void bls_l2cap_requestConnParamUpdate (u16 connHandle, u16 min_interval, u16 max_interval, u16 latency, u16 timeout);
该API仅限Peripheral使用。min_interval和max_interval的单位为1.25 ms,timeout的单位为10 ms。
Telink B91m BLE Multiple Connection SDK 提供了Peripheral设置发送更新连接参数请求时间的API:
void bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate( u16 connHandle, int time_ms)
以连接建立时刻为时间基准点,经过time_ms后才会将连接参数更新请求发送出去,不调用该API,默认设置为1000 ms。
如果调用API bls_l2cap_requestConnParamUpdate的时间已经在建立连接后的time_ms之后,则立刻发出连接参数跟新请求。
在应用中,SDK提供了获取连接请求结果的GAP Event ‘GAP_EVT_L2CAP_CONN_PARAM_UPDATE_RSP’,用于通知用户Peripheral申请的连接参数请求是否被接受,如上图所示,Central接受了Peripheral的Connection_Param_Update_Req参数。
其中app_host_event_callback函数参考如下:
int app_host_event_callback(u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event){
.......
case GAP_EVT_L2CAP_CONN_PARAM_UPDATE_RSP:
{
(gap_l2cap_connParamUpdateRspEvt_t*) p= (gap_l2cap_connParamUpdateRspEvt_t*) para;
if( p->result == CONN_PARAM_UPDATE_ACCEPT ){
//the LE Central Host has accepted the connection parameters
}
else if( p->result == CONN_PARAM_UPDATE_REJECT ){
//the LE Central Host has rejected the connection parameter
}
}
Break;
......
}
return 0;
}
(2) Central回应更新申请
peer Peripheral申请新的连接参数后,Central收到该命令,回CONNECTION PARAMETER UPDATE RESPONSE命令,详情请参照Bluetooth Core Specification V5.4, Vol 3, Part A, 4.21 L2CAP_CONNECTION_PARAMETER_UPDATE_RSP (code 0x13)。
关于实际的Android、iOS设备是否接受user所申请的连接参数,跟各个厂家BLE Central的做法有关,各家标准并不统一。
Telink B91m BLE Multiple Connection SDK 中,不管是否接受Peripheral的参数申请,都使用下面API对该申请进行答复:
void blc_l2cap_SendConnParamUpdateResponse(u16 connHandle, u8 req_id, conn_para_up_rsp result);
该API仅限Central使用。connHandle指定当前connection handle,req->id是连接参数更新请求中的Idetifier值,connParaRsp参考如下:
typedef enum{
CONN_PARAM_UPDATE_ACCEPT = 0x0000,
CONN_PARAM_UPDATE_REJECT = 0x0001,
}conn_para_up_rsp;
Telink B91m BLE Multiple Connection SDK 中,Central在判断合适的连接参数请求后,如果用户注册了GAP_EVT_L2CAP_CONN_PARAM_UPDATE_REQ,判断是否同意连接参数更新由用户在事件回调里执行,如果没有注册Central会在底层直接进入连接参数更新流程。
当GAP_EVT_L2CAP_CONN_PARAM_UPDATE_REQ生效情况下,如果用户不同意连接参数请求,需要在回调里调用blc_l2cap_SendConnParamUpdateResponse,并且第三个参数设置为CONN_PARAM_UPDATE_REJECT。如果用户同意连接参数请求,需要在回调里先调用blc_l2cap_SendConnParamUpdateResponse,并且第三个参数设置为CONN_PARAM_UPDATE_ACCEPT,然后再调用blm_l2cap_processConnParamUpdatePending进入连接参数更新流程。
void blm_l2cap_processConnParamUpdatePending(u16 connHandle, u16 min_interval, u16 max_interval, u16 latency, u16 timeout);
(3) 在Link Layer上更新连接参数
Central可以直接执行连接参数更新,也可以由Peripheral发送conn para update req,并且Central回conn para update rsp接受申请后,就会有下面的流程。
Central会发送link layer层的LL_CONNECTION_UPDATE_REQ命令,如下图所示。
Peripheral收到更新请求后,更新连接参数。Central和Peripheral都会触发 HCI_SUB_EVT_LE_CONNECTION_UPDATE_ COMPLETE 的HCI事件。
ATT & GATT
GATT基本单位Attribute
GATT定义了两种角色:Server和Client。Telink B91m BLE Multiple Connection SDK 中,Peripheral设备是Server,Android、iOS或Central设备是Client。Server需要提供多个service供Client访问。
GATT的service实质是由多个Attribute构成,每个Attribute都具有一定的信息量,当多个不同种类的Attribute组合在一起时,就能够反映出一个基本的service。
一个Attribute的基本内容和属性包括以下:
(1) Attribute Type: UUID
UUID用来区分每一个attribute的类型,其全长为16个bytes。BLE标准协议中UUID长度定义为2个bytes,这是因为peer device设备都遵循同一套转换方法,将2个bytes的UUID转换成16个bytes。
user直接使用蓝牙标准的2 byte 的UUID时,Central设备都知道这些UUID代表的设备类型。SDK中已经定义了一些标准的UUID,分布在以下文件中:stack/ble/service/hids.h、stack/ble/attr/gatt_uuid.h。
Telink私有的一些profile(OTA、SPP、MIC等),标准蓝牙里面不支持,在stack/ble/attr/gatt_uuid.h中定义这些私有的UUID,长度为16 bytes。
(2) Attribute Handle
service拥有多个Attribute,这些Attribute组成一个Attribute Table。在Attribute Table中,每一个Attribute都有一个Attribute Handle值,用来区分每一个不同的Attribute。Peripheral和Central建立连接后,Central通过Service Discovery过程解析读取到Peripheral的Attribute Table,并根据Attribute Handle的值来对应每一个不同的Attribute,这样它们后面的数据通信只要带上Attribute Handle,对方就知道是哪个Attribute的数据了。
(3) Attribute Value
每个Attribute都有对应的Attribute Value,用来作为request、response、notification和indication的数据。在该SDK中,Attribute Value用指针和指针所指区域的长度来描述。
Attribute and ATT Table
为了实现Peripheral端的GATT service,SDK设计了一个Attribute Table,该Table由多个基本的Attribute组成。基本的Attribute的定义为:
typedef struct attribute
{
u16 attNum;
u8 perm;
u8 uuidLen;
u32 attrLen; //4 bytes aligned
u8* uuid;
u8* pAttrValue;
att_readwrite_callback_t w;
att_readwrite_callback_t r;
} attribute_t;
结合目前该SDK给出的参考Attribute Table来说明以上各项的含义。Attribute Table代码见app_att.c,如下截图所示:
请注意,Attribute Table的定义前面加了const:
const attribute_t my_Attributes[ ] = { ... };
const关键字会让编译器将这个数组的数据最终都存储到flash,以节省ram空间。这个Attribute Table定义在Flash上的所有内容是只读的,不能改写。
(1) attNum
attNum有两个作用。
attNum的第一个作用是表示当前Attribute Table中所有有效Attribute数目,即Attribute Handle的最大值,该数目只在Attribute Table数组的第0项无效的Attribute中使用:
{57,0,0,0,0,0}, // ATT_END_H – 1 = 57
attNum = 57表示当前Attribute Table中共有57个Attribute。
在BLE里,Attribute Handle值从0x0001开始,往后加一递增,而数组的下标从0开始,在Attribute Table里加上上面这个虚拟的Attribute,正好使得后面每个Attribute在数据里的下标号等于其Attribute Handle的值。当定义好了Attribute Table后,数Attribute在当前Attribute Table数组中的下标号,就能知道该Attribute当前的Attribute Handle值。
将Attribute Table中所有的Attribute数完,数到最后一个的编号就是当前Attribute Table中有效Attribute的数目attNum,目前SDK中为57,user如果添加或删除了Attribute,需要对此attNum进行修改,可以参考vendor/acl_connection_demo/app_att.h的枚举ATT_HANDLE。
attNum第二个作用是用于指定当前的service由几个Attribute构成。
每一个service的第一个Attribute的UUID都必须是GATT_UUID_PRIMARY_SERVICE(0x2800),在这个Attribute上的attNum指定从当前Attribute开始往后数,总共有attNum个Attribute属于该service的组成部分。
如上面截图所示,gap service UUID为GATT_UUID_PRIMARY_SERVICE的第一个Attribute,其attNum为7,则Attribute Handle 0x0001 ~ Attribute Handle 0x0007这7个Attribute是属于gap service的描述。
同样,上图中的HID service的首个Attribute的attNum设为27后,从这个Attribute开始往后连续27个Attribute都属于HID service。
除了第0项Attribute和每一个service首个Attribute外,其他所有的Attribute的attNum的值都必须设为0。
(2) perm
perm是permission的简写。
perm用于指定当前Attribute被Client访问的权限。
权限有以下10种,每个Attribute的权限都必须为下面的值或它们的组合。
#define ATT_PERMISSIONS_READ 0x01
#define ATT_PERMISSIONS_WRITE 0x02
#define ATT_PERMISSIONS_AUTHEN_READ 0x61
#define ATT_PERMISSIONS_AUTHEN_WRITE 0x62
#define ATT_PERMISSIONS_SECURE_CONN_READ 0xE1
#define ATT_PERMISSIONS_SECURE_CONN_WRITE 0xE2
#define ATT_PERMISSIONS_AUTHOR_READ 0x11
#define ATT_PERMISSIONS_AUTHOR_WRITE 0x12
#define ATT_PERMISSIONS_ENCRYPT_READ 0x21
#define ATT_PERMISSIONS_ENCRYPT_WRITE 0x22
注意:
目前Telink B91m BLE Multiple Connection SDK 暂不支持授权读和授权写。
(3) uuid and uuidLen
按照之前所述,UUID分两种:BLE标准的2 bytes UUID和Telink私有的16 bytes UUID。通过uuid和uuidLen可以同时描述这两种UUID。
uuid是一个u8型指针,uuidLen表示从指针开始的地方连续uuidLen个byte的内容为当前UUID。Attribute Table是存在flash上的,所有的UUID也是存在flash上的,所以uuid是指向flash的一个指针。
a) BLE标准的2 bytes UUID:
如Attribute Handle = 0x0002的devNameCharacter那个Attribute,相关代码如下:
#define GATT_UUID_CHARACTER 0x2803
static const u16 my_characterUUID = GATT_UUID_CHARACTER;
static const u8 my_devNameCharVal[5] = {
CHAR_PROP_READ,
U16_LO(GenericAccess_DeviceName_DP_H), U16_HI(GenericAccess_DeviceName_DP_H),
U16_LO(GATT_UUID_DEVICE_NAME), U16_HI(GATT_UUID_DEVICE_NAME)
};
{0,ATT_PERMISSIONS_READ,2,sizeof(my_devNameCharVal),(u8*)(&my_characterUUID), (u8*)(my_devNameCharVal), 0},
UUID=0x2803在BLE中表示character,uuid指向my_devNameCharVal在flash中的地址,uuidLen为2,peer Central来读这个Attribute时,UUID会是0x2803。
b) Telink私有的16 bytes UUID:
如OTA的Attribute,相关代码:
#define TELINK_SPP_DATA_OTA
0x12,0x2B,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00
static const u8 otaOutUuid[16] = {TELINK_SPP_DATA_OTA};
static u8 my_OtaData = 0x00;
{0,ATT_PERMISSIONS_RDWR,16,sizeof(my_OtaData),(u8*)(&my_OtaUUID), (&my_OtaData), &otaWrite, NULL},
uuid指向my_OtaData在flash中的地址,uuidLen为16,Central来读这个Attribute时,UUID会是0x000102030405060708090a0b0c0d2b12。
(4) pAttrValue and attrLen
每一个Attribute都会有对应的Attribute Value。pAttrValue是一个u8型指针,指向Attribute Value所在RAM/Flash的地址,attrLen用来反映该数据在RAM/Flash上的长度。当Central读取Peripheral某个Attribute的Attribute Value时,SDK从Attribute的pAttrValue指针指向的区域(RAM/Flash)开始,取attrLen个数据回给Central。
UUID是只读的,所以uuid是指向flash的指针;而Attribute Value可能会涉及到写操作,如果有写操作必须放在RAM上,所以pAttrValue可能指向RAM,也可能指向Flash。
Attribute Handle=0x0027 hid Information的Attribute,相关代码:
const u8 hidInformation[] =
{
U16_LO(0x0111), U16_HI(0x0111), // bcdHID (USB HID version),0x11,0x01
0x00, // bCountryCode
0x01 // Flags
};
{0,ATT_PERMISSIONS_READ,2, sizeof(hidInformation),(u8*)(&hidInformationUUID), (u8*)(hidInformation), 0},
在实际应用中,hidInformation 4个byte 0x01 0x00 0x01 0x11是只读的,不会涉及到写操作,所以定义时可以使用const关键字存储在Flash上。 pAttrValue指向hidInformation在flash上的地址,此时attrlen以hidInformation实际的长度取值。当Central读该Attribute时,会根据pAttrValue和attrLen返回0x01000111给Central。
Central读该Attribute时BLE抓包如下图,Central使用ATT_Read_Req命令,假定设置要读的AttHandle = 0x0023 = 35,对应着SDK中Attribute Table中的hid information。
Attribute Handle=0x002C battery value的Attribute,相关代码:
u8 my_batVal[1] = {99};
{0,ATT_PERMISSIONS_READ,2,sizeof(my_batVal),(u8*)(&my_batCharUUID), (u8*)(my_batVal), 0}
实际应用中,反应当前电池电量的my_batVal值会根据ADC采样到的电量而改变,然后通过Peripheral主动notify或者Central主动读的方式传输给Central,所以my_batVal应该放在内存上,此时pAttrValue指向my_batVal在RAM上的地址。
(5) 回调函数w
回调函数w是写函数。函数原型:
typedef int (*att_readwrite_callback_t)(void* p);
user如果需要定义回调写函数,须遵循上面格式。回调函数w是optional的,对某一个具体的Attribute来说,user可以设置回调写函数,也可以不设置回调(不设置回调的时候用空指针0表示)。
回调函数w触发条件为:当Peripheral收到的Attribute PDU的Attribute Opcode为以下三个时,Peripheral会检查回调函数w是否被设置:
a) opcode = 0x12, Write Request.
b) opcode = 0x52, Write Command.
c) opcode = 0x18, Execute Write Request.
Peripheral收到以上写命令后,如果没有设置回调函数w,Peripheral会自动向pAttValue指针所指向的区域写Central传过来的值,写入的长度为Central数据包格式中的l2capLen-3;如果user设置了回调函数w,Peripheral收到以上写命令后执行user的回调函数w,此时不再向pAttrValue指针所指区域写数据。这两个写操作是互斥的,只能有一个生效。
user设置回调函数w是为了处理Central在ATT层的Write Request、Write Command和Execute Write Request命令,如果没有设置回调函数w,需要评估pAttrValue所指向的区域是否能够完成对以上命令的处理(如pAttrValue指向flash无法完成写操作;或者attrLen长度不够,Central的写操作会越界,导致其他数据被错误的改写)。
回调函数w的void型p指针指向Central写命令的具体数值。实际p指向一片内存,内存上的值如下面结构体所示。
typedef struct{
u8 type;
u8 rf_len; //User do not use this member, because it may be changed by stack layer.
u16 l2capLen;
u16 chanId;
u8 opcode;
u16 handle;
u8 dat[20];
}rf_packet_att_t;
p指向第一个元素type。写过来的数据有效长度为l2cap - 3,第一个有效数据为dat[0]。
int my_WriteCallback(u16 connHandle, void * p)
{
rf_packet_att_t *pw = (rf_packet_att_t *)p;
int len = pw->l2capLen - 3;
//add your code
//valid data is pw->dat[0] ~ pw->dat[len-1]
return 1;
}
上面这个结构体rf_packet_att_t所在位置为stack/ble/ble_format.h。
注意:
结构体rf_packet_att_t内的rf_len,用户不要使用,rf_len在拼包时有可能会被改写,请使用l2capLen换算后再使用。
(6) 回调函数r
回调函数r是读函数。函数原型:
typedef int (*att_readwrite_callback_t)(void* p);
user如果需要定义回调读函数,须遵循上面格式。回调函数r是optional的,对某一个具体的Attribute来说,user可以设置回调读函数,也可以不设置回调(不设置回调的时候用空指针0表示)。
回调函数r触发条件为:当Peripheral收到的Attribute PDU的Attribute Opcode为以下两个时,Peripheral会检查回调函数r是否被设置:
a) opcode = 0x0A, Read Request.
b) opcode = 0x0C, Read Blob Request.
Peripheral收到以上读命令后,
a) 如果用户设置了回调读函数,执行该函数,根据该函数的返回值决定是否回复Read Response/Read Blob Response:
-
若返回值为1,Peripheral不回复Read Response/Read Blob Response给Central。
-
若返回值为其他值,Peripheral从pAttrValue指针所指向的区域读attrLen个值用Read Response/Read Blob Response回复给Central。
b) 如果用户没有设置回调读函数,Peripheral从pAttrValue指针所指向的区域读attrLen个值用Read Response/Read Blob Response回复给Central。
如果用户想在收到Central的Read Request/Read Blob Request后修改即将回复的Read Response/Read Blob Response的内容,就可以注册对应的回调函数r,在回调函数里修改pAttrValue指针所指ram的内容,并且return的值只能是0。
(7) Attribute Table结构
根据以上对Attribute的详细说明,使用Attribute Table构造Service结构如下图所示。第一个Attribute的attnum用于指示当前ATT Table Attribute的数量,剩余的Attribute首先按Service分组,每一组的头一条Attribute是该Service的declaration,并且使用attnum指定后面紧跟的多少条Attribute属于该Service的具体描述。实际每组Service的第一条是一个Primary Service。
#define GATT_UUID_PRIMARY_SERVICE 0x2800 //!< Primary Service
const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;
(8) ATT table Initialization
GATT & ATT初始化只需要将应用层的Attribute Table的指针传到协议栈即可,提供的API:
void bls_att_setAttributeTable (u8 *p);
p为Attribute Table的指针。
GATT Service Security
在介绍GATT Service Security前,用户可以先了解一下SMP相关的内容。
请参考“SMP”章节相关的详细介绍,了解LE配对方式、加密等级等基础知识。
下图是Bluetooth Core Specification给出的GATT服务安全等级服务请求之间映射关系,详细可以参考《core5.0》(Vol3/Part C/10.3 AUTHENTICATION PROCEDURE)。
用户可以很清楚的看到:
-
第一列跟当前连接的Peripheral设备是否处于加密状态下有关;
-
第二列(local Device’s Access Requirement for service)则跟用户设置的ATT表中特性的权限(Permission Access)设置有关,如下图所示;
-
第三列又分为4个子列,这4个子列则对应当前LE安全模式1下四个级别(具体说就是当前的设备配对状态是否是如下4种中的一种:
a) No authentication and no encryption
b) Unauthenticated pairing with encryption
c) Authenticated pairing with encryption
d) Authenticated LE Secure Connections
最终GATT service security的实现跟SMP初始化时的参数配置,包括支持的最高安全级别设置、ATT表中的特性权限设置等都有关系,而且跟Central也有关系,比如Peripheral设置的SMP能支持的最高等级是Authenticated pairing with encryption,但是Central具备的最高安全等级是Unauthenticated pairing with encryption,此时如果ATT表中某个写特性的权限是ATT_PERMISSIONS_AUTHEN_WRITE,那么Central在写该特性时,我们会回复加密等级不够的错误。
用户可以设定ATT表中特性权限实现如下应用:
比如Peripheral设备支持的最高安全级别是Unauthenticated pairing with encryption,但是不想连接后使用发送Security Request这种方式去触发Central开始配对,那么客户可以将某些具备notify属性的客户端特性配置(Client Characteristic Configuration,简称CCC)属性的权限设置为ATT_PERMISSIONS_ENCRYPT_WRITE,那么Central只有写该CCC后,Peripheral会回复其安全级别不够,这会触发Central开启配对加密流程。
注意:
用户设置的安全级别只表示设备能支持的最高安全级别,只要ATT表中特性的权限(ATT Permission)不超过实际生效的最高级别就可以通过GATT service security管控。对于LE安全模式1中的等级4来说,如果用户只设置Authenticated LE Secure Connections一种级别,则代表当前设置支持LE Secure Connections only。
Attribute PDU and GATT API
根据Bluetooth Core Specification,Telink B91m BLE Multiple Connection SDK 目前支持的Attribute PDU有以下几类:
-
Requests:client发送给server的数据请求。
-
Responses:server收到client的request后发送的数据回复。
-
Commands:client发送给server的命令。
-
Notifications:server发送给client的数据。
-
Indications:server发送给client的数据。
-
Confirmations:client对server Indication数据的确认。
下面结合之前介绍的Attribute结构和Attribute Table结构,对ATT层所有的 ATT PDU进行分析。
Read by Group Type Request, Read by Group Type Response
Read by Group Type Request和Read by Group Type Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.4.9 ATT_READ_BY_GROUP_TYPE_REQ/3.4.4.10 ATT_READ_BY_GROUP_TYPE_RSP。
Central发送Read by Group Type Request,在该命令中指定起始和结束的attHandle,指定attGroupType。Peripheral收到该Request后,遍历当前Attribute table,在指定的起始和结束的attHandle中找到符合attGroupType的Attribute Group,通过Read by Group Type Response回复Attribute Group信息。
上图所示,Central查询Peripheral的UUID为0x2800的primaryServiceUUID的Attribute Group信息:
#define GATT_UUID_PRIMARY_SERVICE 0x2800
const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;
参考当前demo code,Attribute table中有以下几组符合该要求:
(1) attHandle从0x0001 ~ 0x0007的Attribute Group,Attribute Value为SERVICE_UUID_GENERIC_ACCESS(0x1800)。
(2) attHandle从0x0008 ~ 0x000B的Attribute Group,Attribute Value为SERVICE_UUID_GENERIC_ATTRIBUTE(0x1801)。
(3) attHandle从0x000C ~ 0x000E的Attribute Group,Attribute Value为SERVICE_UUID_DEVICE_INFORMATION(0x180A)。
(4) attHandle从0x000F ~ 0x0029的Attribute Group,Attribute Value为SERVICE_UUID_HUMAN_INTERFACE_ DEVICE(0x1812)。
(5) attHandle从0x002A ~ 0x002D的Attribute Group,Attribute Value为SERVICE_UUID_BATTERY(0x180F)。
(6) attHandle从0x002E ~ 0x0035的Attribute Group,Attribute Value为 TELINK_SPP_UUID_SERVICE (0x10,0x19,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00)。
(7) attHandle从0x0036 ~ 0x0039的Attribute Group,Attribute Value为 TELINK_OTA_UUID_SERVICE(0x12,0x19,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00)。
Peripheral将以上7个GROUP的attHandle和attValue的信息通过Read by Group Type Response回复给Central,最后一个ATT_Error_Response表明所有的Attribute Group都已回复完毕,Response结束,Central看到这个包也会停止发送Read by Group Type Request。
使用下面API实现Read by Group Request:
ble_sts_t blc_gatt_pushReadByGroupTypeRequest(u16 connHandle, u16 start_attHandle, u16 end_attHandle, u8 *uuid, int uuid_len);
Read by Group Response的数据,在app_gatt_data_handler函数里可以读取并处理。
Find by Type Value Request, Find by Type Value Response
Find by Type Value Request和Find by Type Value Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.3.3 ATT_FIND_BY_TYPE_VALUE_REQ/3.4.3.4 ATT_FIND_BY_TYPE_VALUE_RSP。
Central发送Find by Type Value Request,在该命令中指定起始和结束的attHandle,指定AttributeType和Attribute Value。Peripheral收到该Request后,遍历当前Attribute table,在指定的起始和结束的attHandle中找到AttributeType和Attribute Value相匹配的Attribute,通过Find by Type Value Response回复Attribute。
使用下面API实现Find by Type Value Request:
ble_sts_t blc_gatt_pushFindByTypeValueRequest(u16 connHandle, u16 start_attHandle, u16 end_attHandle, u16 uuid, u8 *attr_value, int len);
Find by Type Value Response的数据,在app_gatt_data_handler函数里可以读取并处理。
Read by Type Request, Read by Type Response
Read by Type Request和Read by Type Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.4.1 ATT_READ_BY_TYPE_REQ/3.4.4.2 ATT_READ_BY_TYPE_RSP。
Central发送Read by Type Request,在该命令中指定起始和结束的attHandle,指定AttributeType。Peripheral收到该Request后,遍历当前Attribute table,在指定的起始和结束的attHandle中找到符合AttributeType的Attribute,通过Read by Type Response回复Attribute。
上图所示,Central读attType为0x2A00的Attribute,Peripheral中Attribute Handle为0x0003的Attribute:
static const u8 my_devName[] = {'m','u','l','t','i','_','c','o','n','n'};
#define GATT_UUID_DEVICE_NAME 0x2a00
const u16 my_devNameUUID = GATT_UUID_DEVICE_NAME;
{0,ATT_PERMISSIONS_READ,2,sizeof(my_devName), (u8*)(&my_devNameUUID), (u8*)(my_devName), 0}
Read by Type response中length为8,attData中前两个byte为当前的attHandle 0003,后面6个bytes为对应的Attribute Value。
使用下面API实现Read by Type Request:
ble_sts_t blc_gatt_pushReadByTypeRequest(u16 connHandle, u16 start_attHandle, u16 end_attHandle, u8 *uuid, int uuid_len);
Read by Type Response的数据,在app_gatt_data_handler函数里可以读取并处理。
Find information Request, Find information Response
Find information request和Find information response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.3.1 ATT_FIND_INFORMATION_REQ/3.4.3.2 ATT_FIND_INFORMATION_RSP。
Central发送Find information request,指定起始和结束的attHandle。Peripheral收到该命令后,将起始和结束的所有attHandle对应Attribute 的UUID通过Find information response回复给Central。如下图所示,Central要求获得attHandle 0x0016 ~0x0018三个Attribute的information,Peripheral回复这三个Attribute的UUID。
Read Request, Read Response
Read Request和Read Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.4.3 ATT_READ_REQ/3.4.4.4 ATT_READ_RSP。
Central发送Read Request,指定某一个attHandle为0x0017,Peripheral收到后通过Read Response回复指定的Attribute的Attribute Value(若设置了回调函数r,执行该函数),如下图所示。
使用下面API实现Read Request
ble_sts_t blc_gatt_pushReadRequest(u16 connHandle, u16 attHandle);
Read Response的数据,在app_gatt_data_handler函数里可以读取并处理。
Read Blob Request, Read Blob Response
Read Blob Request和Read Blob Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.4.5 ATT_READ_BLOB_REQ/3.4.4.6 ATT_READ_BLOB_RSP。
当Peripheral某个Attribute的Attribute Value值的长度超过MTU_SIZE(目前Telink B91m BLE Multiple Connection SDK 中默认为23)时,Central需要启用Read Blob Request来读取该Attribute Value,从而使得Attribute Value可以分包发送。Central在Read Blob Request指定attHandle和ValueOffset,Peripheral收到该命令后,找到对应的Attribute,根据ValueOffset值通过Read Blob Response回复Attribute Value(若设置了回调函数r,执行该函数)。
如下图所示,Central读Peripheral的HID report map(report map很大,远远超过23)时,首先发送Read Request,Peripheral回Read response,将report map前一部分数据回给Central。之后Central使用Read Blob Request,Peripheral通过Read Blob Response回数据给Central。
使用下面API实现Read Blob Request
ble_sts_t blc_gatt_pushReadBlobRequest(u16 connHandle, u16 attHandle, u16 offset);
Read Blob Response的数据,在app_gatt_data_handler函数里可以读取并处理。
Exchange MTU Request, Exchange MTU Response
Exchange MTU Request和Exchange MTU Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.2.1 ATT_EXCHANGE_MTU_REQ/3.4.2.2 ATT_EXCHANGE_MTU_RSP。
如下面所示,Central和Peripheral通过Exchange MTU Request和Exchange MTU Response获知对方的MTU size。
当GATT层的的数据访问过程中出现超过一个RF包长度的数据,涉及到GATT层分包和拼包时,需要提前和peer Central/Peripheral交互双方的RX MTU size,也就是MTU size exchange的过程。MTU size exchange的目的是为了实现GATT层长包数据的收发。
(1) 用户可以通过注册GAP event回调并开启eventMask: GAP_EVT_MASK_ATT_EXCHANGE_MTU来获取EffectiveRxMTU,其中:
EffectiveRxMTU=min(ClientRxMTU, ServerRxMTU)。
本文档“GAP event”章节会详细介绍GAP event。
(2) GATT层收长包数据的处理。
ServerRxMTU和ClientRxMTU默认为23,最大ServerRxMTU/ClientRxMTU可以支持到和理论值一样(仅受限于ram空间)。当应用中需要使用到分包重拼时,使用下面API修改Central端RX size:
ble_sts_t blc_att_setCentralRxMtuSize(u16 cen_mtu_size);
使用下面API修改Peripheral端RX size:
ble_sts_t blc_att_setPeripheralRxMtuSize(u16 per_mtu_size);
返回值列表:
ble_sts_t | Value | ERR Reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
GATT_ERR_INVALID_ PARAMETER | 见SDK中定义 | 大于定义的buffer size,即:mtu_s_rx_fifo或mtu_m_rx_fifo |
注意:
上述两个API设置是ATT_Exchange_MTU_req/ATT_Exchange_MTU_rsp命令交互时的MTU数值。该数值不能大于实际定义的buffer size,即变量:mtu_m_rx_fifo[ ]和 mtu_s_rx_fifo[ ],这两个array variable是在app_buffer.c中定义的。
只要使用上面API设置的MTU不是默认值23,连接建立后,SDK会主动发起MTU的交互流程,通过注册Host事件GAP_EVT_ATT_EXCHANGE_MTU,可以在回调函数中看到MTU交互的结果。
Write Request, Write Response
Write Request和Write Response,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.5.1 ATT_WRITE_REQ/3.4.5.2 ATT_WRITE_RSP。
Central发送Write Request,指定某个attHandle,并附带相关数据。Peripheral收到后,找到指定的Attribute,根据user是否设置了回调函数w决定数据是使用回调函数w来处理还是直接写入对应的Attribute Value,并回复Write Response。
下图所示为Central向attHandle为0x0016的Attribute写入Attribute Value为0x0001,Peripheral收到后执行该写操作,并回Write Response。
使用下面API实现Write Request
ble_sts_t blc_gatt_pushWriteRequest (u16 connHandle, u16 attHandle, u8 *p, int len);
Write Response的数据,在app_gatt_data_handler函数里可以读取并处理。
Write Command
Write Command,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.5.3 ATT_WRITE_CMD。
Central发送Write Command,指定某个attHandle,并附带相关数据。Peripheral收到后,找到指定的Attribute,根据user是否设置了回调函数w决定数据是使用回调函数w来处理还是直接写入对应的Attribute Value,不回复任何信息。
使用下面API实现Write Command
ble_sts_t blc_gatt_pushWriteCommand (u16 connHandle, u16 attHandle, u8 *p, int len);
Queued Writes
Queued Writes包含Prepare Write Request/Response和Execute Write Request/Response等ATT协议,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.6 Queued writes。
注意:
在使用Queued Writes时,需要在初始化的时候调用API blc_att_setPrepareWriteBuffer来分配prepare write的存储buffer,节省ram考虑默认并未进行初始设置。
Prepare Write Request和Execute Write Request可以实现如下两种功能:
a) 提供长属性值的写入功能。
b) 允许在一个单独执行的原子操作中写入多个值。
Prepare Write Request包含AttHandle、ValueOffset和PartAttValue,这和Read_Blob_Req/Rsp类似。这说明Client既可以在队列中准备多个属性值,也可以准备一个长属性值的各个部分。这样,在真正执行准备队列之前,Client可以确定某属性的所有部分都能写入Server。
注意:
Telink B91m BLE Multiple Connection SDK 当前版本仅支持a)长属性值写入功能,长属性值最大长度小于等于244字节。
如下图所示,Central向Peripheral某个特性写很长的字符串:“I am not sure what a new song”(字节数远远超过23,使用默认MTU情况下)时,首先发送Prepare Write Request,偏移0x0000,将“I am not sure what”部分数据写给Peripheral,Peripheral向Central回Prepare Write Response。之后Central发送Prepare Write Request,偏移0x12,将“ a new song”部分数据写给Peripheral,Peripheral向Central回Prepare Write Response。当Central将长属性值全部写完成后,发送Execute Write Request给Peripheral,Flags为1:表示写立即生效,Peripheral回复Execute Write Response,整个Prepare write过程结束。
这里我们可以看到Prepare Write Response也包含请求中的AttHandle、ValueOffset和PartAttValue。这样做的目的为了数据传递的可靠性。Client可以对比Response和Request的字段值,确保准备的数据被正确接收。
Handle Value Notification
Handle Value Notification,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.7.1 ATT_HANDLE_VALUE_NTF。
上图所示为Bluetooth Core Specification中Handle Value Notification的格式。
Telink B91m BLE Multiple Connection SDK 提供了API,用于某个Attribute的Handle Value Notification。user调用这个API以将自己需要notify的数据push到底层的BLE软件fifo,协议栈会在最近的收发包interval时将软件fifo的数据push到硬件fifo,最终通过RF发送出去。
ble_sts_t blc_gatt_pushHandleValueNotify(u16 connHandle, u16 attHandle, u8 *p, int len);
connHandle为对应Connection state的connHandle,attHandle为对应Attribute的attHandle,p为要发送的连续内存数据的头指针,len指定发送的数据的字节数。该API支持自动拆包功能(根据EffectiveMaxTxOctets做分包处理,即链路层RF RX/TX最大收发字节数的较小值,DLE可能会修改该值,默认为27),可将一个很长的数据拆成多个BLE RF packet发送出去,所以len可以支持很大。
Link Layer在Conn state时,一般情况下直接调用该API可以成功push数据到底层软件fifo,但也存在一些特殊情况会导致该API调用失败,根据返回值ble_sts_t可以了解对应的错误原因。
调用该API时,建议用户检查返回值的是否为BLE_SUCCESS,若不为BLE_SUCCESS,则需要等待一段时间后再次push。
返回值列表如下:
ble_sts_t | Value | ERR reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
GAP_ERR_INVALID_PARAMETER | 0xC0 | 无效参数 |
SMP_ERR_PAIRING_BUSY | 0xA1 | 处于配对阶段 |
GATT_ERR_DATA_LENGTH_EXCEED_ MTU_SIZE | 0xB5 | len大于ATT_MTU-3,要发送的数据长度超出了ATT层支持的最大数据长度ATT_MTU |
LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer处于None Conn state |
LL_ERR_ENCRYPTION_BUSY | 0x82 | 处于加密阶段,不能发送数据 |
LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | 有大数据量任务在运行,软件Tx fifo不够用 |
GATT_ERR_DATA_PENDING_DUE_TO_ SERVICE_DISCOVERY_BUSY | 0xB4 | 处于遍历服务阶段,不能发数据 |
Handle Value Indication
Handle Value Indication,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.7.2 ATT_HANDLE_VALUE_IND。
上图所示为Bluetooth Core Specification中Handle Value Indication的格式。
Telink B91m BLE Multiple Connection SDK 提供API,用于某个Attribute的Handle Value Indication。user调用这个API以将自己需要indicate的数据push到底层的BLE软件fifo,协议栈会在最近的收发包interval时将软件fifo的数据push到硬件fifo,最终通过RF发送出去。
ble_sts_t blc_gatt_pushHandleValueIndicate (u16 connHandle, u16 attHandle, u8 *p, int len);
connHandle为对应Connection state的connHandle,attHandle为对应Attribute的attHandle,p为要发送的连续内存数据的头指针,len指定发送的数据的字节数。该API支持自动拆包功能(根据EffectiveMaxTxOctets做分包处理,即链路层RF RX/TX最大收发字节数的较小值,DLE可能会修改该值,默认为27,下文将介绍其替换API,见备注),可将一个很长的数据拆成多个BLE RF packet发送出去,所以len可以支持很大。
Bluetooth Core Specification里规定了每一个indicate的数据,都要等到client的confirm才能认为indicate成功,未成功时不能发送下一个indicate数据。
Link Layer在Conn state时,一般情况下直接调用该API可以成功push数据到底层软件fifo,但也存在一些特殊情况会导致该API调用失败,根据返回值ble_sts_t可以了解对应的错误原因。
调用该API时,建议用户检查返回值的是否为BLE_SUCCESS,若不为BLE_SUCCESS,则需要等待一段时间后再次push。
返回值列表如下:
ble_sts_t | Value | ERR reason |
---|---|---|
BLE_SUCCESS | 0 | 操作成功 |
GAP_ERR_INVALID_PARAMETER | 0xC0 | 无效参数 |
SMP_ERR_PAIRING_BUSY | 0xA1 | 处于配对阶段 |
GATT_ERR_DATA_LENGTH_EXCEED_ MTU_SIZE | 0xB5 | len大于ATT_MTU-3,要发送的数据长度超出了ATT层支持的最大数据长度ATT_MTU |
LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer处于None Conn state |
LL_ERR_ENCRYPTION_BUSY | 0x82 | 处于加密阶段,不能发送数据 |
LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | 有大数据量任务在运行,软件Tx fifo不够用 |
GATT_ERR_DATA_PENDING_DUE_TO_ SERVICE_DISCOVERY_BUSY | 0xB4 | 处于遍历服务阶段,不能发数据 |
GATT_ERR_PREVIOUS_INDICATE_ DATA_HAS_NOT_CONFIRMED | 0xB1 | 前一个indicate数据还没有被Central确认 |
Handle Value Confirmation
Handle Value Confirmation,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part F, 3.4.7.3 ATT_HANDLE_VALUE_CFM。
应用层每调用一次blc_gatt_pushHandleValueIndicate,向Central发送indicate数据后,Central会回复一个confirm,表示对这个数据的确认,然后Peripheral才可以继续发送下一个indicate数据。
从上图中可以看出,Confirmation并不指定是对哪一个具体handle的确认,对所有不同handle上的indicate数据都统一回复一个Confirmation。
为了让应用层了解发送出去的indicate data是否已经被Confirm,用户可以通过注册GAP event回调,并开启相应的eventMask:GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM来获取Confirm事件,本文档“GAP event”小节会详细介绍GAP event。
blc_att_setServerDataPendingTime_upon_ClientCmd
Telink B91m BLE Multiple Connection SDK 底层在SDP过程及SDP过后的data pending time(默认300 ms)时间内不允许notify和indicate操作。如果user需要改变data pending time,可以使用此API。
void blc_att_setServerDataPendingTime_upon_ClientCmd(u8 num_10ms);
参数以10 ms为单位,如参数代入30,则表示30*10 ms,即300 ms。
GAP
GAP初始化
Telink B91m BLE Multiple Connection SDK 中,因为central和peripheral在一个设备中同时扮演,所以在初始化时就不区分central和peripheral设备了。
初始化函数:
void blc_gap_init(void);
由前文我们知道,应用层与Host的数据交互不通过GAP来访问控制,协议栈在ATT、SMP和L2CAP都提供了相关接口,可以和应用层直接交互。目前SDK的GAP层主要处理host层上的事件,GAP初始化主要是注册host层事件处理函数入口。
GAP Event
GAP event则是ATT、GATT、SMP、GAP等host协议层交互过程中产生的事件。从前文我们可以知道,目前SDK事件主要分为两大类:Controller event和GAP(host) event,其中controller event又分为HCI event和LE HCI event。
Telink B91m BLE Multiple Connection SDK 中新增了GAP event处理,主要是协议栈事件分层更加清晰,协议栈处理用户层交互事件更加便捷,特别是SMP相关的处理,如Passkey的输入,配对结果通知用户等。
如果user需要在App层接收GAP event,首先需要注册GAP event的callback函数,其次需要将对应event的mask打开。
GAP event的callback函数原型和注册接口分别为:
typedef int (*gap_event_handler_t) (u32 h, u8 *para, int n);
void blc_gap_registerHostEventHandler (gap_event_handler_t handler);
callback函数原型中的u32 h是GAP event标记,底层协议栈多处会用到。
下面列出几个用户可能会用到的事件:
#define GAP_EVT_SMP_PAIRING_BEGIN 0
#define GAP_EVT_SMP_PAIRING_SUCCESS 1
#define GAP_EVT_SMP_PAIRING_FAIL 2
#define GAP_EVT_SMP_CONN_ENCRYPTION_DONE 3
#define GAP_EVT_SMP_TK_DISPALY 4
#define GAP_EVT_SMP_TK_REQUEST_PASSKEY 5
#define GAP_EVT_SMP_TK_REQUEST_OOB 6
#define GAP_EVT_SMP_TK_NUMERIC_COMPARE 7
#define GAP_EVT_ATT_EXCHANGE_MTU 16
#define GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM 17
callback函数原型中para和n表示event的数据和数据长度,下文将详细说明以上列出的GAP event。User可参考demo code中如下用法以及app_host_event_callback函数的具体实现。
blc_gap_registerHostEventHandler( app_host_event_callback );
GAP event需要通过下面的API来打开mask。
void blc_gap_setEventMask(u32 evtMask);
eventMask的定义也对应上面给出一些,其他的event mask用户可以在ble/gap/gap_event.h中查到。
#define GAP_EVT_MASK_SMP_PAIRING_BEGIN (1<<GAP_EVT_SMP_PAIRING_BEGIN)
#define GAP_EVT_MASK_SMP_PAIRING_SUCCESS (1<<GAP_EVT_SMP_PAIRING_SUCCESS)
#define GAP_EVT_MASK_SMP_PAIRING_FAIL (1<<GAP_EVT_SMP_PAIRING_FAIL)
#define GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE (1<<GAP_EVT_SMP_CONN_ENCRYPTION_DONE)
#define GAP_EVT_MASK_SMP_TK_DISPALY (1<<GAP_EVT_SMP_TK_DISPALY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY (1<<GAP_EVT_SMP_TK_REQUEST_PASSKEY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_OOB (1<<GAP_EVT_SMP_TK_REQUEST_OOB)
#define GAP_EVT_MASK_SMP_TK_NUMERIC_COMPARE (1<<GAP_EVT_SMP_TK_NUMERIC_COMPARE)
#define GAP_EVT_MASK_ATT_EXCHANGE_MTU (1<<GAP_EVT_ATT_EXCHANGE_MTU)
#define GAP_EVT_MASK_GATT_HANDLE_VLAUE_CONFIRM (1<<GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM)
若user未通过该API设置GAP event mask,那么当GAP 相应的event产生时将不会通知应用层。
注意:
以下论述GAP event时,均设定注册了GAP event回调,且开启了对应的eventMask。
(1) GAP_EVT_SMP_PAIRING_BEGIN
事件触发条件:当Peripheral和Central刚刚连接进入connection state,Peripheral发送SM_Security_Req命令后,Central发送SM_Pairing_Req请求开始配对,Peripheral收到这个配对请求命令时,触发该事件,表示配对开始。
数据长度n: 4。
回传指针p:指向一片内存数据,对应如下结构体:
typedef struct {
u16 connHandle;
u8 secure_conn;
u8 tk_method;
} gap_smp_pairingBeginEvt_t;
connHandle表示当前连接句柄。
secure_conn为1表示使用安全加密特性(LE Secure Connections),否则将使用LE legacy pairing。
tk_method表示接下来配对使用什么样的TK值方式:例如JustWorks、PK_Init_Dsply_Resp_Input、PK_Resp_Dsply_Init_Input,Numric_Comparison等。
(2) GAP_EVT_SMP_PAIRING_SUCCESS
事件触发条件:配对整个流程正确完成时产生该事件,该阶段即为LE配对阶段之密钥分发阶段3(Key Distribution, Phase 3),如果有密钥需要分发,则等待双方密钥分发完成后触发配对成功事件,否则直接触发配对成功事件。
数据长度n:4。
回传指针p:指向一片内存数据,对应如下结构体:
typedef struct {
u16 connHandle;
u8 bonding;
u8 bonding_result;
} gap_smp_pairingSuccessEvt_t;
connHandle表示当前连接句柄。
bonding为1表示启用bonding功能,否则不启用。
bonding_result表示bonding的结果:如果没有开启bonding功能,则为0,如果开启了bonding功能,则还需要检查加密Key是否被正确的存储在FLASH中,存储成功为1,否则为0。
(3) GAP_EVT_SMP_PAIRING_FAIL
事件触发条件:由于Peripheral或Central其中一个不符合标准配对流程,或者通信中出现报错等异常原因导致配对流程终止。
数据长度n:2。
回传指针p:指向一片内存数据,对应如下结构体:
typedef struct {
u16 connHandle;
u8 reason;
} gap_smp_pairingFailEvt_t;
connHandle表示当前连接句柄。
reason表示配对失败的原因,这里列出几个常见的配对失败原因值,其他配对失败原因值我们可以参考SDK目录下的“stack/ble/smp/smp_const.h”文件。
配对失败值的具体含义,详情请参考Bluetooth Core Specification V5.4, Vol 3, Part H, 3.5.5 Pairing Failed。
#define PAIRING_FAIL_REASON_CONFIRM_FAILED 0x04
#define PAIRING_FAIL_REASON_PAIRING_NOT_SUPPORTED 0x05
#define PAIRING_FAIL_REASON_DHKEY_CHECK_FAIL 0x0B
#define PAIRING_FAIL_REASON_NUMUERIC_FAILED 0x0C
#define PAIRING_FAIL_REASON_PAIRING_TIEMOUT 0x80
#define PAIRING_FAIL_REASON_CONN_DISCONNECT 0x81
(4) GAP_EVT_SMP_CONN_ENCRYPTION_DONE
事件触发条件:Link Layer加密完成时(Link Layer收到Central发的start encryption response)触发。
数据长度n:3。
回传指针p:指向一片内存数据,对应如下结构体:
typedef struct {
u16 connHandle;
u8 re_connect; //1: re_connect, encrypt with previous distributed LTK; 0: pairing , encrypt with STK
} gap_smp_connEncDoneEvt_t;
connHandle表示当前连接句柄。
re_connect为1表示快速回连(将使用之前分发的LTK加密链路),若该值为0则表示当前加密是第一次加密。
(5) GAP_EVT_SMP_TK_DISPALY
事件触发条件:Peripheral收到Central发送的Pairing_Req后,根据对端设备的配对参数和本地设备的配对参数配置,我们就可以知道接下来配对使用什么样的TK(pincode)值方式。如果启用的是PK_Resp_Dsply_Init_Input(即:Peripheral端显示6位pincode码,Central端负责输入6位pincode码)方式,则会立即触发。
数据长度n:4。
回传指针p:指向一个u32型变量tk_set,该值即为Peripheral需要通知应用层的6位pincode码,应用层需要显示该6位码值,参考代码如下:
case GAP_EVT_SMP_TK_DISPALY:
{
char pc[7];
u32 pinCode = *(u32*)para;
sprintf(pc, "%d", pinCode);
printf("TK display:%s\n", pc);
}
break;
pincode可以通过以下API在初始化时进行设置,如设置为123456:
blc_smp_setDefaultPinCode(123456);
用户将Peripheral上看到的6位pincode码输入到Central设备上(如手机),完成TK输入,配对流程得以继续执行。如果用户输入pincode错误或者点击取消,则配对流程失败。
(6) GAP_EVT_SMP_TK_REQUEST_PASSKEY
事件触发条件:当Peripheral设备启用Passkey Entry方式时,且使用的PK_Init_Dsply_Resp_Input或者PK_BOTH_INPUT配对方式时,会触发该事件,通知用户需要输入TK值。用户在收到该事件后就需要通过IO输入TK值(超时30s如果还未输入则配对失败),输入TK值的API:blc_smp_setTK_by_PasskeyEntry在“SMP参数配置”章节有说明。
数据长度n:0。
回传指针p:NULL。
(7) GAP_EVT_SMP_TK_REQUEST_OOB
事件触发条件:当Peripheral设备启用传统配对OOB方式时,会触发该事件,通知用户需要通过OOB方式输入16位TK值。用户在收到该事件后就需要通过IO输入16位TK值(超时30s如果还未输入则配对失败),输入TK值的API:blc_smp_setTK_by_OOB在“SMP参数配置”章节有说明。
数据长度n:0。
回传指针p:NULL。
(8) GAP_EVT_SMP_TK_NUMERIC_COMPARE
事件触发条件:Peripheral收到Central发送的Pairing_Req后,根据对端设备的配对参数和本地设备的配对参数配置我们就可以知道接下来配对使用什么样的TK(pincode)值方式,如果启用的是Numeric_Comparison方式,则会立即触发。(Numeric_Comparison方式即数值比较,属于smp4.2安全加密,Central和Peripheral设备均会弹出显示6位pincode码以及“YES”和“NO”对话框,用户需要检查两端设备显示的pincode是否一致,并需要两端分别确认是否点击“YES”以确认TK校验是否通过)。
数据长度n:4。
回传指针p:指向一个u32型变量pinCode,该值即为Peripheral需要通知应用层的6位pincode码,应用层需要显示该6位码值,并提供“YES”和“NO”的确认机制。
(9) GAP_EVT_ATT_EXCHANGE_MTU
事件触发条件:无论是Central端发送Exchange MTU Request,Peripheral回复Exchange MTU Response,还是Peripheral端发送Exchange MTU Request,Central回复Exchange MTU Response,两种情况下均会触发。
数据长度n:6。
回传指针p:指向一片内存数据,对应如下结构体:
typedef struct {
u16 connHandle;
u16 peer_MTU;
u16 effective_MTU;
} gap_gatt_mtuSizeExchangeEvt_t;
connHandle表示当前连接句柄。
peer_MTU表示对端的RX MTU值。
effective_MTU = min(CleintRxMTU, ServerRxMTU),CleintRxMTU表示客户端的RX MTU size值,ServerRxMTU表示服务端的RX MTU size值。Central和Peripheral交互了彼此的MTU size后,取两者最小值作为彼此交互的最大MTU size值。
(10) GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM
事件触发条件:应用层每调用一次bls_att_pushIndicateData(或者调用blc_gatt_pushHandleValueIndicate),向Central发送indicate数据后,Central会回复一个confirm,表示对这个数据的确认,Peripheral收到该confirm时触发。
数据长度n:0。
回传指针p:NULL。
SMP
Security Manager(SM)为 LE 设备提供加密所需要的各种 Key,确保数据的机密性。加密链路可以避免第三方“攻击者”拦截、破译或者篡改空中数据原始内容。SMP详细内容请参考Bluetooth Core Specification V5.4, Vol 3, Part H: Security Manager Specification。
SMP 安全等级
Bluetooth Core Specification V4.2 新增了一种称作安全连接(LE Secure Connections)的配对方式,进一步增强了安全性,而此前的配对方式,我们统称传统配对(LE Legacy Pairing)。
Telink BLE Mulitple Connection SDK 提供以下4个安全等级,参考Bluetooth Core Specification V5.4, Vol 3, Part C, 10.2 LE security modes:
a) No authentication and no encryption (LE security Mode 1 Level 1)
b) Unauthenticated pairing with encryption (LE security Mode 1 Level 2)
c) Authenticated pairing with encryption (LE security Mode 1 Level 3)
d) Authenticated LE Secure Connections (LE security Mode 1 Level 4)
注意:
- 所有连接全部支持到最高安全级别,主从可以配置不同的安全级别;
- 当前不支持不同连接配置为不同的安全级别;
- 本端设备设定的安全级别只表示本端设备可能达到的最高安全级别,想要达到设定的安全级别跟两个因素有关:
(a)peer device 设定能支持的最高安全级别 >= local device 设定能支持的最高安全级别;
(b)local device 和 peer device 按照各自设定的 SMP 参数正确处理完配对整个流程(如果存在配对的话)。
举例来说,用户设置 Peripheral 端能够支持的最高安全等级是 Mode 1 Level 3,但是连接 Peripheral 的 Central 设置为不支持配对加密(最高只支持 Mode 1 Level 1),那么连接后 Peripheral 和 Central 不会进行配对流程,Peripheral 实际使用的安全级别是 Mode 1 Level 1。
使用以下 API 设置 local device 能支持的最高安全等级。
void blc_smp_setSecurityLevel(le_security_mode_level_t mode_level);
void blc_smp_setSecurityLevel_central(le_security_mode_level_t mode_level);
void blc_smp_setSecurityLevel_periphr(le_security_mode_level_t mode_level);
说明:
在 Telink B91m BLE Multiple Connection SDK 中,配置 SMP 相关参数的 API 如没有特别说明,都会有如下 3 种配置形式:
-
统一配置 Central role 和 Peripheral role 参数的 API(...);
-
单独配置所有 Central role 参数的 API_Central(...);
-
单独配置所有 Peripheral role 参数的 API_Peripheral(...);
SMP参数配置
在调用 GAP 的初始化时,会初始化 SMP,并将 SMP 的参数初始化为默认值:
-
默认支持的最高安全等级:Unauthenticated_Paring_with_Encryption,即 Mode 1 Level 2;
-
默认绑定模式:Bondable_Mode(参考 blc_smp_setBondingMode() API 说明);
-
默认 IO 能力:IO_CAPABILITY_NO_INPUT_NO_OUTPUT;
-
默认配对方式:Legacy Pairing Just Works。
初始化完成后,先通过 SMP 参数配置的 API 配置 SMP 参数,再通过以下 API 将应用层配置的参数带入底层进行初始配置。
void blc_smp_smpParamInit(void);
下面介绍 SMP 参数配置的相关 API。
void blc_smp_setPairingMethods(pairing_methods_t method); //_Peripheral()/_Central()
该套 API 用于配置 SMP 配对方式,Legacy 或 Secure Connections。
注意:
Secure Connection的安全配对方式,需要MTU>=65。
void blc_smp_setIoCapability(pairing_methods_t method); //_Peripheral()/_Central()
该套 API 用于配置 SMP IO 能力(见下图),决定 Key 产生的方式,参考Bluetooth Core Specification V5.4, Vol 3, Part H, 2.3.5.1 Selecting Key Generation Method。
void blc_smp_enableAuthMITM(int MITM_en); //_Peripheral()/_Central()
该套 API 用于配置 SMP 的 MITM(Man in the Middle) flag,用于提供 Authentication,安全级别在 Mode 1 Level 3 及以上时,要求该参数为 1。其中参数 MITM_en 的值为 0 对应失能;1 对应使能。
void blc_smp_enableOobAuthentication(int OOB_en); //_Peripheral()/_Central()
该套 API 用于配置 SMP 的 OOB flag,需要安全级别在 Mode 1 Level 3 及以上。其中参数 OOB_en 的值为 0 对应失能;1 对应使能。
设备会根据本地设备和对端设备的 OOB 及 MITM flag 决定使用 OOB 方式还是根据 IO 能力决定选择什么样的配对方式,参考Bluetooth Core Specification V5.4, Vol 3, Part H, 2.3.5.1 Selecting Key Generation Method。
void blc_smp_setBondingMode(bonding_mode_t mode); //_Peripheral()/_Central()
该套 API 用于配置 是否将 SMP 过程产生的 Key 存在 Flash 中,如果设置为 Bondable_Mode,用户就可以利用 SMP 信息进行自动回连,回连时不会重新进行配对;如果设置为 Non_Bondable_Mode,则产生的 Key 不会存储在 Flash 中,断线之后无法进行自动回连,需要进行重新配对。
void blc_smp_enableKeypress(int keyPress_en); //_Peripheral()/_Central()
该套 API 用于配置是否需使能 Key Press 功能。其中参数 keyPress_en 的值为 0 对应失能;1 对应使能。
void blc_smp_setSecurityParameters(bonding_mode_t mode, int MITM_en, pairing_methods_t method, int OOB_en, int keyPress_en, io_capability_t ioCapablility); //_Peripheral()/_Central()
该套 API 用于整体性配置前述 SMP 参数,各参数和以上 API 分别具有如下对应关系:
parameter | API |
---|---|
mode | void blc_smp_setBondingMode(bonding_mode_t mode); |
MITM_en | void blc_smp_enableAuthMITM(int MITM_en); |
method | void blc_smp_setPairingMethods(pairing_methods_t method); |
OOB_en | void blc_smp_enableOobAuthentication(int OOB_en); |
keyPress_en | void blc_smp_enableKeypress(int keyPress_en); |
ioCapablility | void blc_smp_setIoCapability(pairing_methods_t method); |
void blc_smp_setEcdhDebugMode(ecdh_keys_mode_t mode); //_periphr()/_central()
该套 API 用于配置 Security Connections 是否启用椭圆加密密匙的 Debug 密钥对。安全连接配对情况下使用了椭圆加密算法,可以有效避免窃听,但用户无法通过 sniffer 工具解析 BLE 空中包, 所以 Bluetooth Core Specification 给出了一组用于 Debug 的椭圆加密私钥/公钥对,只要开启这个模式,部分 BLE sniffer 工具就可以用这个已知的密钥去解密链路。
注意:
Peripheral 和 Central 仅允许一方的密钥配置为 Debug 密钥对,否则连接不具有安全性,失去了配对的意义,协议规定其非法。
void blc_smp_setDefaultPinCode(u32 pinCodeInput); //_periphr()/_central()
该套 API 用于配置 Passkey Entry 或 Numeric Comparison 配对方式下 Display 设备显示的默认 Pincode。参数范围在 [0,999999] 之内。
u8 blc_smp_setTK_by_PasskeyEntry (u16 connHandle, u32 pinCodeInput); //connHandle区分连接链路
该 API 用于在 Passkey Entry 配对方式下 Input 设备输入 TK 值。返回值 1 代表设置成功,0 代表当前没有要求 Input 设备输入 TK 值。
说明:
这里解释一下 TK,Passkey,Pincode 三者之间的关系,TK(Temporary Key),临时密钥,作为 SMP 过程中最基础的原始密钥,其产生方式有多种:如 Just Works 默认产生 TK=0;Passkey Entry 方式输入 TK 的值,这个值在应用层被称为 Pincode;OOB 方式通过 OOB 数据生成 TK。可以简单理解为 Pincode 产生 Passkey,Passkey 产生 TK,只不过,这个“产生”并不一定改变其值。
u8 blc_smp_setTK_by_OOB (u16 connHandle, u8 *oobData); //connHandle区分连接链路
该 API 用于设置 OOB 配对方式下设备的 OOB 数据。参数 oobData 表示需要设置的 16 位 OOB 数据数组的头指针。返回值 1 代表设置成功,0 代表当前没有要求 Input 设备输入 TK 值。
u8 blc_smp_isWaitingToSetTK(u16 connHandle); //connHandle区分连接链路
该 API 用于获取 Passkey Entry 或 OOB 配对方式下,Input 设备是否等待 TK 输入。返回 1 表示等待输入。
void blc_smp_setNumericComparisonResult(u16 connHandle, bool YES_or_NO); //connHandle区分连接链路
该 API 用于在 Security Connections 下 Numeric Comparison 配对方式下设置设备输入的 YES 或 NO。当用户确认显示的 6 位数值和对端一致时,可以输入 1(“YES”),不一致则输入 0(“NO”)。
u8 blc_smp_isWaitingToCfmNumericComparison(u16 connHandle); //connHandle区分连接链路
该 API 用于获取 Security Connections 下 Numeric Comparison 配对方式下,设备是否等待输入 Yes or No。返回 1 表示等待输入。
int blc_smp_isPairingBusy(u16 connHandle); //connHandle区分连接链路
该 API 用于查询连接是否正在配对中。返回值 0 表示不在配对,1 表示正在配对中。
SMP 流程配置
-
SMP 安全请求(Security Request)只有 Peripheral 可以发送,用于主动请求对端 Central 进行配对流程,是 SMP 的可选流程。
-
SMP 配对请求(Pairing Request)只有 Central 可以发送,用于通知 Peripheral 开始配对流程。
SMP 安全请求
blc_smp_configSecurityRequestSending(secReq_cfg newConn_cfg, secReq_cfg reConn_cfg, u16 pending_ms);
该 API 用于灵活地配置 Peripheral 发送 Security Request 的时机。
注意:
只有连接建立之前调用才有效,建议在初始化时配置。
枚举类型 secReq_cfg 定义如下:
typedef enum {
SecReq_NOT_SEND = 0, //连接建立后,Peripheral 不会主动发送 Security Request
SecReq_IMM_SEND = BIT(0), //连接建立后,Peripheral 会立即发送 Security Request
SecReq_PEND_SEND = BIT(1), //连接建立后,Peripheral 等待 pending_ms(单位毫秒)后再决定是否发送 Security Request
}secReq_cfg;
newConn_cfg:用于配置新的连接。如果 Peripheral 配置为 SecReq_PEND_SEND,且在 pending_ms 前就收到 Central 的 Pairing Request 包,则不会再发送 Security Request。
reConn_cfg:用于配置回连。配对绑定过的设备,下次再连接的时候(即回连),Central 有时候不一定会主动发起 LL_ENC_REQ 来加密链路,此时如果 Peripheral 发 Security Request 可以触发 Central 加密链路。如果 Peripheral 配置为 SecReq_PEND_SEND,且在 pending_ms 之前已经收到 Central 的 LL_ENC_REQ 包,则不会再发送 Security Request。
pending_ms:当 newConn_cfg 和 reConn_cfg 任何一项配置为 SecReq_PEND_SEND 时,该参数才有作用。
SMP 配对请求
void blc_smp_configPairingRequestSending( PairReq_cfg newConn_cfg, PairReq_cfg reConn_cfg);
该 API 用于灵活地配置 Central 发送 Pairing Request 的时机。
注意:
只能在连接之前调用,建议在初始化时配置。
枚举类型 PairReq_cfg 定义如下:
typedef enum {
PairReq_SEND_upon_SecReq = 0, // Central 发送 Pairing Request 依赖于收到 Peripheral 发送的 Security Request
PairReq_AUTO_SEND = 1, // Central 一经连接便会自动发送 Pairing Request
}PairReq_cfg;
SMP 配对方法
SMP 配对方法主要围绕 SMP 四个安全等级的配置展开。
Mode 1 Level 1
设备不支持加密配对,即禁用 SMP 功能,初始化配置:
blc_smp_setSecurityLevel(No_Security);
Mode 1 Level 2
设备最高支持 Unauthenticated_Paring_with_Encryption,如 Legacy Pairing 和 Secure Connections 配对方式下的 Just Works 配对模式。
- LE Legacy Just works 的初始化配置:
//blc_smp_setPairingMethods(LE_Legacy_Pairing); //Default
//blc_smp_setSecurityLevel_Central(Unauthenticated_Pairing_with_Encryption); //Default
blc_smp_smpParamInit();
- LE Security Connections Just works 的初始化配置:
blc_smp_setPairingMethods(LE_Secure_Connection);
blc_smp_smpParamInit();
Mode 1 Level 3
设备最高支持 Authenticated pairing with encryption,如 Legacy Pairing 的 Passkey Entry、Out of Band。
该级别需要设备支持 Authentication,Authentication 能确保配对双方身份的合法性。
- LE Legacy Passkey Entry 方式 Display 设备的初始化配置:
blc_smp_setSecurityLevel(Authenticated_Pairing_with_Encryption);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_DISPLAY_ONLY);
//blc_smp_setDefaultPinCode(123456);
blc_smp_smpParamInit();
或
blc_smp_setSecurityLevel(Authenticated_Pairing_with_Encryption);
blc_smp_setSecurityParameters(Bondable_Mode, 1, LE_Legacy_Pairing, 0, 0, IO_CAPABILITY_DISPLAY_ONLY);
blc_smp_smpParamInit();
这里涉及到显示 TK 的 GAP event:GAP_EVT_SMP_TK_DISPALY,请参考“GAP event”章节。
- LE Legacy Passkey Entry 方式 Input 设备的初始化配置:
blc_smp_setSecurityLevel(Authenticated_Pairing_with_Encryption);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABLITY_KEYBOARD_ONLY);
blc_smp_smpParamInit();
或
blc_smp_setSecurityLevel(Authenticated_Pairing_with_Encryption);
blc_smp_setSecurityParameters(Bondable_Mode, 1, LE_Legacy_Pairing, 0, 0, IO_CAPABLITY_KEYBOARD_ONLY);
blc_smp_smpParamInit();
用户调用以下 API 来设置 TK:
void blc_smp_setTK_by_PasskeyEntry (u16 connHandle, u32 pinCodeInput);
- LE Legacy OOB 的初始化配置:
blc_smp_setSecurityLevel(Authenticated_Pairing_with_Encryption);
blc_smp_enableOobAuthentication(1);
blc_smp_smpParamInit();
或
blc_smp_setSecurityLevel_periphr(Authenticated_Pairing_with_Encryption);
blc_smp_setSecurityParameters_periphr(Bondable_Mode, 1, LE_Legacy_Pairing, 1, 0, IO_CAPABILITY_KEYBOARD_DISPLAY);
blc_smp_smpParamInit();
这里涉及到请求 OOB 数据的 GAP event:GAP_EVT_SMP_TK_REQUEST_OOB,请参考“GAP event”章节。
Mode 1 Level 4
设备最高支持 Authenticated LE Secure Connections,如 Secure Connections 的 Numeric Comparison、Passkey Entry、Out of Band。
- Secure Connections Passkey Entry 方式的初始化配置:
与 Legacy Pairing Passkey Entry 基本一致,唯一不同的是需要在初始化最开始的地方设置配对方式为“安全连接配对”:
blc_smp_setSecurityLevel(Authenticated_LE_Secure_Connection_Pairing_with_Encryption);
blc_smp_setParingMethods(LE_Secure_Connection);
...//参考 Mode 1 Level 3 配置方式
- Secure Connections Numeric Comparison 的初始化配置:
blc_smp_setSecurityLevel(Authenticated_LE_Secure_Connection_Pairing_with_Encryption);
blc_smp_setParingMethods(LE_Secure_Connection);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABLITY_DISPLAY_YESNO);
blc_smp_smpParamInit();
或
blc_smp_setSecurityLevel_central(Authenticated_LE_Secure_Connection_Pairing_with_Encryption);
blc_smp_setSecurityParameters_central(Bondable_Mode, 1, LE_Secure_Connection, 0, 0, IO_CAPABLITY_DISPLAY_YESNO);
blc_smp_smpParamInit();
这里涉及到请求 Yes/No 的 GAP event:GAP_EVT_SMP_TK_NUMERIC_COMPARE,请参考“GAP event”章节。
- Secure Connections OOB 方式,SDK 暂不支持。
SMP Storage
无论设备作为 Central 还是 Peripheral,在与其他设备进行 SMP 绑定后,需要将一些 SMP 相关的信息保存到 Flash 中,以便在设备重新上电后,可以实现自动回连。该过程称为 SMP Storage。
SMP Storage 区域
在 Flash 中用于存储 SMP 绑定信息的区域称为 SMP Storage 区域。
对于 Telink B91m BLE Multiple Connection SDK,SMP Storage 区域起始位置由宏 FLASH_ADR_SMP_PAIRING 指定(1M Flash 默认为 0xFA000)。SMP Storage 区域分为 2 个区,分别称为 A区、B区,占用的空间相等,由宏 FLASH_SMP_PAIRING_MAX_SIZE 指定(默认为 0x2000,即 8K,所以总 SMP Storage 区域大小为 16K)。每个区的 (FLASH_SMP_PAIRING_MAX_SIZE-0x10) 偏移量(默认为 0x1FF0)位置为 “区有效 Flag”,0x3C 代表有效,0xFF 代表未生效。用户可以使用下面的 API 重新配置 SMP Storage 区域:
void blc_smp_configPairingSecurityInfoStorageAddressAndSize (int address, int size_byte); //address and size must be 4K aligned
- address :SMP Storage 区域起始地址(也是A区起始地址);
- size_byte :每个 SMP 区的大小,A区 和 B区 大小相等。
下面的 API 用于获取当前 SMP Storage 有效区的起始地址:
u32 blc_smp_getBondingInfoCurStartAddr(void);
配对后,默认先从 SMP Storage A区 开始存储 SMP 绑定信息,当 A区 的绑定信息量达到警戒线(8KB * 3/4 = 96 Bytes * 64,也就是最多存储 64 个 Bonding Info)后,会将其中有效的绑定信息迁移到 B区,置 "区有效 Flag" 为 0x3C,并将 A区 清空。同理,当 B区 绑定信息达到警戒线,则切换到 A区,并清空 B区。可以通过以下 API 来确认当前的 SMP Storage 有效区的信息量是否达到了警戒线:
bool blc_smp_isBondingInfoStorageLowAlarmed(void);
- 返回值:0 表示未到警戒线,1 表示已经到了警戒线。
如果需要清空 SMP Storage 中的信息并重置 SMP 绑定信息,建议在非连接态下调用以下 API:
void blc_smp_eraseAllBondingInfo(void);
Bonding Info
在 SMP Storage 中存储的每一组 SMP 绑定信息,称为一个 Bonding Info 块,SMP Storage 默认依照配对先后依次将 Bonding Info 填入 SMP Storage 区域,参考其结构 smp_param_save_t,得到:
-
一个 Bonding Info 块的大小为 96 Bytes (0x60);
-
Bonding Info 块的第一个 Byte,即 flag 成员,代表该 Bonding Info 块的状态,如果 flag & 0x0F == 0x0A,则说明该 SMP 绑定信息有效;如果 Central 的 Bonding Info 块的 flag 成员的值为 0x00,代表设备已经解绑;如果 flag 的 bit7 为 0,代表支持 RPA,详情参考 RPA 功能章节(SDK中暂未完整释放该功能);
-
Bonding Info 块的第二个 Byte,即 role_dev_idx,代表自身所扮演的角色,如果 bit7 为 1,代表自身为 Central,如果 bit7 为 0,则代表在该连接中扮演 Peripheral 角色;
-
SMP 获取的 peer Id Address 和 local/peer IRK 均存在 Bonding Info 块中。
下图为 SMP Storage 内容的一段参考,该段内容表示该 Bonding Info 块有效,设备为 Central 角色:
可以使用以下 API 通过 peer device 的 MAC 地址获取其 Bonding Info:
u32 blc_smp_loadBondingInfoByAddr(u8 isCentral, u8 PeripheralDevIdx, u8 addr_type, u8* addr, smp_param_save_t* smp_param_load);
- isCentral : 自身的角色,0 表示 Peripheral,非 0 表示 Central;
- PeripheralDevIdx : 在不涉及多地址功能时,该参数为 0;
- addr_type : peer device 的地址类型, 参考 BLE_ADDR_PUBLIC 和 BLE_ADDR_RANDOM;
- addr : peer device 的 MAC 地址;
- smp_param_load : 出参,指向 peer device 对应的 Bonding Info 块。
- 返回值:peer device 对应的 Bonding Info 块在 Flash 中的首地址。
为便于应用层使用,针对 Central 角色,提供一个根据 peer Peripheral 的 MAC 获取其配对状态的 API:
u32 blc_smp_searchBondingPeripheralDevice_by_PeerMacAddress( u8 peer_addr_type, u8* peer_addr);
使用以下 API 通过 peer device 的 MAC 地址,将其对应的 Bonding Info 删除(实际上,并未删除,只是通过置 flag 使其失效):
int blc_smp_deleteBondingPeripheralInfo_by_PeerMacAddress(u8 peer_addr_type, u8* peer_addr);
最大绑定数量
对于 Telink B91m BLE Multiple Connection SDK,默认最多可以保存 8 个有效 peer Peripheral 的 SMP 信息,和 4 个有效 peer Central 的 SMP 信息(“有效” 代表设备可以回连成功,也就是 Bonding Info 块的 flag 成员表示当前状态为有效),这在 SDK 中分别称为 Central 和 Peripheral 的最大绑定数量(Bonding Device Max Number)。用户也可以通过下面的 API 重新配置 SMP Storage 的最大绑定数量:
ble_sts_t blc_smp_setBondingDeviceMaxNumber ( int Central_max_bonNum, int Peripheral_max_bondNum);
注意:
在V4.0.1.3之前使用一下API声明:
* cen_max_bonNum: 自身作为 Central 角色的最大 peer Peripheral 绑定数量,最大为8,传入参数超过8时,按照8设定;ble_sts_t blc_smp_setBondingDeviceMaxNumber ( int cen_max_bonNum, int per_max_bondNum);
* isCentral : 自身的角色,0 表示 Peripheral,非 0 表示 Central; * perDevIdx : 在不涉及多地址功能时,该参数为 0; * 返回值:有效绑定设备的数量,isCentral 为 0 时,表示绑定的有效 peer Central的数量,isCentral 非 0 时,表示绑定的有效 peer Peripheral 的数量。* per_max_bondNum: 自身作为 Peripheral 角色的最大 peer Central 绑定数量,最大为4,传入参数超过4时,按照4设定。 达到最大绑定数量时,下一个绑定的设备将会顶替掉当前同角色有效设备中最早绑定的设备。具体而言,会将新的设备的 Bonding Info 继续向 Flash 中写入,置 flag 为有效,同时置同角色有效 Bonding Info 中的第一个设备的 flag 为无效。 举例而言,如果设置了 blc_smp_setBondingDeviceMaxNumber(8, 4),当绑定 8 个 peer Peripheral 后,一旦绑定第 9 个 peer Peripheral,最老的那个(第1个)peer Peripheral 的 Bonding Info 失效,并向Flash中继续存储第 9 个 peer Peripheral 设备的 Bonding Info。 用户可以通过以下 API 获取当前 Peripheral 或 Central 的绑定数量: ```C u8 blc_smp_param_getCurrentBondingDeviceNumber(u8 isCentral, u8 perDevIdx);
SMP Bonding Info Index
SMP 中为每个 Bonding Device 的绑定信息都分配了一个序号,称为 Bonding Info Index,Bonding Info Index 的值默认根据绑定的先后顺序在 Bonding Device Max Number 中进行分配。如当 Central 的 Bonding Device Max Number 为 2 时,先后配对的两个 peer Peripheral 的 Bonding Info Index 分别为 0 和 1。
这样,除了上述通过 peer device MAC 地址的方式获取 Bonding Info,也可以在已知设备 Bonding Info Index 的情况下,通过 Bonding Info Index 来获取 Bonding Info:
u32 blc_smp_loadBondingInfoFromFlashByIndex(u8 isCentral, u8 PeripheralDevIdx, u8 index, smp_param_save_t* smp_param_load);
以下 API 用于设置 Bonding Info Index 的分配原则,SDK 中暂时还没有释放该功能,仅用于部分用户的特殊需求:
void blc_smp_setBondingInfoIndexUpdateMethod(index_updateMethod_t method);
Custom Pair
多主多从的设备中,如果Central设备disable了SMP,则SDK无法自动完成配对和解配对操作,需要在应用层添加配对管理。基于此,Telink自定义了一套配对与解配对的方案。
如果用户需要使用自定义的配对管理,首先需要初始化该功能,调用如下API:
blc_smp_setSecurityLevel_Central(No_Security);//禁用SMP功能
user_central_host_pairing_management_init();//自定义方式
(1) Flash存储方法设计
使用的flash数据区sector为SMP 配对信息存储位置,根据flash大小自动判断起始位置,数据区大小由用户定义,定义如下:
#define FLASH_CUSTOM_PAIRING_MAX_SIZE 4096
从存储起始位置开始每8个bytes划分为一个area,称8 bytes area。每个area可以存储一个Peripheral的mac address,其中第一个byte是标志位,第二个byte为地址类型,后面6个bytes 为mac address。
typedef struct {
u8 bond_mark;
u8 adr_type;
u8 address[6];
} macAddr_t;
flash存储过程中使用依次往后推8 bytes area的方法,第一个byte标志位写为0x5A,表示当前地址有效。
如果要某个Peripheral设备解配对,多主多从设备需要擦掉这个设备的MAC address,只需要将之前存储该MAC address 8 bytes area的标志位(即第一个byte)写为0x00即可。
采用上面这种8bytes顺延方法的原因是,程序在运行过程中不能调用flash_erase_sector这个函数擦flash,因为该操作擦一个sector 4K的flash耗时在20-200ms之间,这个时间会引起BLE时序错误。
将所有的Peripheral MAC的配对存储和解配对擦除使用0x5A和0x00标志位来表示,当8 bytes area越来越多,可能会占满整个sector 4K flash导致出错时,在初始化的时候加了特别处理: 从存储起始位置开始读取8 bytes area信息,将所有的有效MAC address读到RAM中的Peripheral MAC table。这过程中检查是否8 bytes area太多,太多的话,就擦掉整个sector,然后将RAM中维护的Peripheral MAC table重新写回存储起始位置开始的8 bytes area。
(2) Peripheral MAC table
#define USER_PAIR_ACL_PERIPHR_MAX_NUM 4
typedef struct __attribute__((packed)) {
u8 bond_mark;
u8 adr_type;
u8 address[6];
} macAddr_t;
typedef struct __attribute__((packed)) {
u32 bond_flash_idx[USER_PAIR_ACL_PERIPHR_MAX_NUM];
macAddr_t bond_device[USER_PAIR_ACL_PERIPHR_MAX_NUM];
u8 curNum;
} user_periphrMac_t;
user_periphrMac_t user_tbl_periphrMac;
用上面结构在RAM中使用 Peripheral MAC table维护所有的配对设备,改变宏USER_PAIR_Peripheral_MAX_NUM即可定义自己想要的最多允许几个配对,Telink B91m BLE Multiple Connection SDK 中默认为4,指维护4个设备的配对,user可以修改这个值。
user_tbl_periphrMac中curNum表示当前flash上记录了几个有效的Peripheral设备,bond_flash_idx数组记录有效地址在flash上的8 bytes area起始地址相对于配对信息存储起始位置的偏移量(当解配对这个设备时,可以通过这个偏移量找到8 bytes area的标志位,将其写为0x00),bond_device数组记录MAC address。
(3) 相关API说明
基于上面FLASH存储设计和RAM中 Peripheral MAC table的设计,分别有以下几个API可以调用。
a. user_central_host_pairing_management_init
void user_central_host_pairing_management_init(void);
用户自定义配对管理初始化函数,启用自定义方式时需要调用该初始化函数.
b. user_tbl_peripheral_mac_add
int user_tbl_peripheral_mac_add(u8 adr_type, u8 *adr);
添加一个Peripheral mac, return 1表示成功, 0 表示失败。当有新的设备配对上时,需要调用此函数。
函数先判断当前flash和Peripheral MAC table中设备是否已经到达最大值。若没有到最大值,无条件添加到Peripheral MAC table,并在FLASH的一个 8 bytes area上存储。
若已经到最大值。涉及到处理的策略问题:是不允许配对还是直接覆盖最老的,Telink demo的方法是直接覆盖最老的,先使用user_tbl_Peripheral_mac_delete_by_index(0) 删掉当前设备,再往Peripheral mac table里面加入新的。User可以根据自己的策略去修改这个函数的实现。
c. user_tbl_peripheral_mac_search
int user_tbl_peripheral_mac_search(u8 adr_type, u8 * adr)
根据adv report的设备地址搜索该设备是否已经在Peripheral MAC table中,即判断当前发广播包的设备是否之前已经和Central配对上,若是已经配对过的设备可以直接连接。
d. user_tbl_peripheral_mac_delete_by_adr
int user_tbl_peripheral_mac_delete_by_adr(u8 adr_type, u8 *adr)
通过指定地址删除一个配对的设备。
e. user_tbl_peripheral_mac_delete_by_index
void user_tbl_peripheral_mac_delete_by_index(int index)
通过指定index删除配对设备。Index值反映的是设备配对的顺序。如果最大配对个数为1,配上的那个设备index永远为0;如果如果最大配对个数为2,第一个配上的设备index为0,第二个配上的设备index为1;依次类推。
f. user_tbl_peripheral_mac_delete_all
void user_tbl_peripheral_mac_delete_all(void)
删除所有配对设备。
(4) 连接和配对
Central收到Controller上报的广播包时,有以下两种情况会和Peripheral进行连接:
a. 调用函数user_tbl_peripheral_mac_search来检查当前设备是否已经和Central配对过并且没有被解配对,如果已经配对过,可以自动连接。
central_auto_connect = user_tbl_peripheral_mac_search(pa->adr_type, pa->mac);
if(central_auto_connect) { create connection }
b. 若当前广播设备不在Peripheral MAC table里面,不符合自动连接,检查是否满足手动配对条件。SDK中默认设置了两个手动配对方案,在当前广播设备距离足够近的前提下,一是多主多从设备上配对键被按下;二是当前广播数据是Telink定义的配对广播包数据。代码:
if(user_manual_pairing) { create connection }
若是手动配对触发的连接建立,在连接成功建立后,即HCI LE CONECTION ESTABLISHED EVENT上报时,将当前设备添加到Peripheral MAC table中:
c. 解配对
当解配对条件生效时,多主多从设备先调用blc_llms_disconnect断开连接,然后调用user_tbl_salve_mac_delete _by_adr函数删除该设备。
Device Manage & Simple SDP
如前面对于 GATT 的描述,在 BLE 中,Peripheral 充当 GATT Server 角色时,会维护一个 GATT Service 的表,表中的每条 Attribute 都对应有个一 Attribute handle 值。而对于 Central,想要获取 Peripheral 的这些信息,需要通过 SDP 过程获取,并进行维护,以供需要时使用。
为了便于用户使用,Telink B91m BLE Multiple Connection SDK 为用户提供了一个连接设备管理方案的实现 Device Manage 以及一个 Central 做 SDP 获取 peer Peripheral 的 GATT Service 表的简单实现。不仅可以为 Central 管理 peer Peripheral 的 GATT Service 表,还可以用于随时通过对端设备的部分信息,调取该设备的其他相关信息。该方案全部以源码的形式提供,用户可以参考 SDK 中 vendor/common/device_manage.* 文件及 vendor/common/simple_sdp.* 文件。
Telink B91m BLE Multiple Connection SDK 使用如下数据结构来管理 Attribute handle 和 Connection handle 。
typedef struct
{
u16 conn_handle;
u8 conn_role; // 0: Central; 1: Peripheral
u8 conn_state; // 1: connect; 0: disconnect
u8 char_handle_valid; // 1: peer device's attHandle is available; 0: peer device's attHandle not available
u8 rsvd[3]; // for 4 Byte align
u8 peer_adrType;
u8 peer_addr[6];
u8 peer_RPA; //RPA: resolvable private address
u16 char_handle[CHAR_HANDLE_MAX];
}dev_char_info_t;
在SDK中,使用数组 conn_dev_list[] 来记录和维护对端设备的参数,如下图所示。
当与其他设备建立连接时,在connection complete event中通过调用dev_char_info_insert_by_conn_event() 将对端设备的身份信息存入conn_dev_list[]。
如果自己是 Central 角色,且启用了 Simple SDP 功能,会先通过 dev_char_info_search_peer_att_handle_by_peer_mac() 查询对端设备的 GATT Service 表是否已经在 Flash 中,如果在,直接从 Flash 中取出通过dev_char_info_add_peer_att _handle()放到 conn_dev_list[]中:
如果不在,将通过 app_service_discovery() 来获取。获取到之后,会调用函数 dev_char_info_add_peer_att_handle() 和 dev_char_info_store_peer_att_handle() 将 peer Peripheral GATT Service 表分别存放到 RAM 和 FLASH 中,以便后续使用,如下图所示。
注意: SDP 是一个很复杂的部分,对于 Telink B91m BLE Multiple Connection SDK,由于芯片资源有限,SDP 不能做的像手机那样复杂。这里给出的是一个简单参考。
用户可以通过根据connHandle dev_char_info_search_by_connhandle() 来取用 GATT Service 表中的 Attribute handle,其返回值是 conn_dev_list[index] 结构体的指针,指向该 connHandle 所对应的 conn_dev_list[] 数组中的那个元素。
LE Advertising Extensions
随着BLE应用越来越广泛,功能也随之增加了很多,下面我们来介绍在core5.0引入的LE Advertising Extensions,包含:Extended advertising、Periodic advertising、Extended Scan、Periodic Scan。这些功能的引入也为后面LE Audio做了准备,当然这些功能也可以根据实际情况用作其他用途。
扩展广播 (Extended Advertising)
BLE core 5.0之前版本,有一个较大的限制,广播数据的payload太小了,只有31B。但是又不能单纯的增加payload来扩容,因为37、38、39是广播物理信道,大家都在使用,冲突的概率很大,payload越长冲突几率越大。所以在core 5.0进行了Advertising Extensions,增加了广播集、周期性广播的概念,既可以解决载荷太小的问题,又能解决冲突概率大的问题。因为使用的其他37个data channel,可以使用跳频机制来减少冲突几率。
注意:core spec也将最大载荷限制在了1650B,即AUX_**加上所有对应的chain packet的有效数据不超过1650B。
在《Core_V4.2》及以前的版本,0 ~ 36 这 37 个信道主要用LE-ACL连接的数据信道。在《Core_V5.0》广播信道定义中,将37,38,39三个信道定义为主广播信道 (Primary Advertising channel),其余37个信道叫做辅助广播信道或者第二广播信道 (Secondary Advertising channel), 辅助广播信道也可以用于发送或接收广播数据。 对于extended advertising,在primary advertising channel上发送的只有ADV_EXT_IND,在secondary advertising channel发送的广播包都是以AUX_**命名。
注意:在primary advertising channel上可以使用1M和coded phy,不能使用2M phy(至少在最新core spec5.4版本中仍然是这样规定的)。
Advertising Extensions也为LE Audio的实现打下了基础。因为在建立ACL连接初期,就需要知道peer device的较多信息,比如Audio的一些参数、时序信息、加密信息等等。这些需要较多的广播载荷。 扩展广播包含ADV_EXT_IND、AUX_ADV_IND及对应的AUX_CHAIN_IND。如果需要广播更多数据(最多 1650 字节),控制器可以将数据分段并使用AuxPtr将各个分段“串联”起来。每个分段都可以在不同的信道上传输。ADV_EXT_IND(AuxPtr)--->AUX_ADV_IND(AuxPtr)--->AUX_CHAIN_IND(AuxPtr)--->AUX_CHAIN_IND......
扩展广播的核心思想:广播数据可以用数据信道来传输。
所有的扩展广播名称及详细描述:
legacy advertising与extended advertising比较:
广播集 (Advertising Sets)
Extended advertising引入了广播集概念,即一个设备可以“同时”进行多个广播,每个广播可以使用不同的interval、不同的PDU data、不同的PDU type、不同的phy等等,比如可以“同时”发送可连接广播和不可连接广播。不同广播集使用extended header中的ADI字段进行区分。
注意: 目前B91m多连接SDK支持创建最大广播集 数量是4个。
Extended Advertising 相关的 API 介绍
ble_sts_t blc_ll_initExtendedAdvModule_initExtendedAdvSetParamBuffer(u8 *pBuff_advSets, int num_advSets);
void blc_ll_initExtendedAdvDataBuffer(u8 *pExtAdvData, int max_len_advData);
pExtAdvData:buffer首地址。 max_len_advData:buffer size。blc_ll_setExtAdvData设置的数据长度不能大于该值。
void blc_ll_initExtendedScanRspDataBuffer(u8 *pScanRspData, int max_len_scanRspData);
pScanRspData: scan response buffer首地址。 max_len_scanRspData:buffer size。blc_ll_setExtScanRspData设置的数据长度不能大于该值。
ble_sts_t blc_ll_setExtAdvParam( u8 adv_handle, advEvtProp_type_t adv_evt_prop, u32 pri_advInter_min, u32 pri_advInter_max,
adv_chn_map_t pri_advChnMap, own_addr_type_t ownAddrType, u8 peerAddrType, u8 *peerAddr,
adv_fp_type_t advFilterPolicy, tx_power_t adv_tx_pow, le_phy_type_t pri_adv_phy, u8 sec_adv_max_skip,
le_phy_type_t sec_adv_phy, u8 adv_sid, u8 scan_req_noti_en);
ble_sts_t blc_ll_setExtAdvData (u8 adv_handle, int advData_len, u8 *advData);
ble_sts_t blc_ll_setExtScanRspData(u8 adv_handle, int scanRspData_len, u8 *scanRspData);
ble_sts_t blc_ll_setExtAdvEnable(adv_en_t enable, u8 adv_handle, u16 duration, u8 max_extAdvEvt);
ble_sts_t blc_ll_setAdvRandomAddr(u8 adv_handle, u8* rand_addr);
ble_sts_t blc_ll_removeAdvSet(u8 adv_handle);
ble_sts_t blc_ll_clearAdvSets(void);
周期广播 (Periodic Advertising)
周期广播也是core 5.0引入的概念,如果需要固定周期来发送数据,就需要使用periodic advertising。周期广播interval和ACL interval的概念是相同的,每个interval使用不同的频点,使用CSA#2跳频算法,周期广播使用 37 个二级广播信道 (secondary advertising channels)。 periodic advertising是:由ADV_EXT_IND AuxPtr引导出AUX_ADV_IND,由AUX_ADV_IND SyncInfo引导出AUX_SYNC_IND。如果数据需要chain包来继续发送,则由AUX_SYNC_IND AuxPtr引导出对应的AUX_CHAIN_IND。即: ADV_EXT_IND(AuxPtr)--->AUX_ADV_IND(SyncInfo)--->AUX_SYNC_IND(AuxPtr)--->AUX_CHAIN_IND(AuxPtr)--->AUX_CHAIN_IND......
注意:周期广播引出之后,引出周期广播的ADV_EXT_IND及AUX_ADV_IND可以继续发送,也可以停止。如果停止,对于已经同步到周期广播的设备没影响,但是对于还没有同步上的设备或者是刚上电的设备就同步不上对应的周期广播了。是否停止由用户根据实际情况来决定。
另:扩展广播interval > 周期广播interval:每个ADV_EXT_IND之间会有多个AUX_SYNC_IND出现。扩展广播interval < 周期广播interval:多个ADV_EXT_IND最终指向同一个AUX_SYNC_IND。
周期广播可以让扫描设备更节省功耗,因为只需要固定的时间点扫描即可。周期广播是 LE Audio broadcast解决方案的关键组成部分。
周期广播间隔确定给定广播集的周期广播可以发生的频率。它从 AUX_SYNC_IND PDU 的传输开始,然后是一系列零个或多个 AUX_CHAIN_IND PDU,如下图所示。
注意: 目前B91m多连接SDK支持创建最大2个周期广播 (4个广播集中只有2个可以引导出周期广播)。
Periodic Advertising 相关的 API 介绍
PA 广播 (Advertising) 相关的 API:
void blc_ll_initPeriodicAdvModule_initPeriodicdAdvSetParamBuffer(u8 *pBuff, int num_periodic_adv);
ble_sts_t blc_ll_setPeriodicAdvParam(adv_handle_t adv_handle, u16 advInter_min, u16 advInter_max, perd_adv_prop_t property);
void blc_ll_initPeriodicAdvDataBuffer(u8 *perdAdvData, int max_len_perdAdvData);
ble_sts_t blc_ll_setPeriodicAdvData(adv_handle_t adv_handle, u16 advData_len, u8 *advdata);
ble_sts_t blc_ll_setPeriodicAdvEnable(u8 per_adv_enable, adv_handle_t adv_handle);
扩展扫描 (Extended SCAN)
对于传统广播包的获取,传统扫描设备只需要扫描 37、38、39 (Primary Advertising channel)这个 3 个信道,只需按照扫描窗口和扫描周期在这3个信道间来回切换,扫描窗口期间只要收到广播数据就按照协议要求规则处理即可。 但是如果要扫描扩展广播包,需要扫描并获取主广播信道37/38/39(Primary Advertising channel)上 的ADV_EXT_IND 广播包,然后需要解析出其中是否包含AuxPtr信息,如果存在的话,需要获取AuxPtr中携带的下一个Auxiliary广播包的时序、跳频信息、PHY等信息,扫描设备需要根据这些信息在合适的窗口监听广播包。如果遇到包链较长的情况,扫描设备继续处理下一个 AuxPtr 直到无 Auxptr字段为止,收完整包后根据广播包是否是可扫描或可连接以及包类型做相应的处理。蓝⽛核⼼规范包含完整的详细信息,请参考《Core_V5.4》, Vol 6, Part B 4.4.3 Scanning state 。
扩展扫描的复杂性在于如何获取辅助信道上的广播包,而广播包可能存在多级引导的情况。
Extended SCAN 相关的 API 介绍
void blc_ll_initExtendedScanning_module(void);
ble_sts_t blc_ll_setExtScanParam ( own_addr_type_t ownAddrType, scan_fp_type_t scan_fp, scan_phy_t scan_phys, scan_type_t scanType_0, scan_inter_t scanInter_0, scan_wind_t scanWindow_0, scan_type_t scanType_1, scan_inter_t scanInter_1, scan_wind_t scanWindow_1);
ble_sts_t blc_ll_setExtScanEnable (scan_en_t extScan_en, dupe_fltr_en_t filter_duplicate, scan_durn_t duration, scan_period_t period);
周期扫描 (Periodic SCAN)
扫描设备可以有以下两种方式中与周期广播序列 (train) 同步:
(1) 设备本身可以扫描ADV_EXT_IND及AUX_ADV_IND PDU,并使用 SyncInfo字段的内容,如:周期广播interval、时序偏移和要使用的信道等信息来建立与periodic advertising的同步。(参考 周期广播 (Periodic Advertising)和扩展扫描 (Extended SCAN)小节)。
(2) 设备可以通过 LE-ACL 连接,从另一个设备接收此信息,不需要在primary channel扫描即可与周期广播设备同步。这个过程我们称之为Periodic Advertising Sync Transfer---PAST。参考Periodic Advertising Sync Transfer (PAST)章节。
periodic scan相关的 API 介绍
void blc_ll_initPeriodicAdvertisingSynchronization_module(void);
ble_sts_t blc_ll_periodicAdvertisingCreateSync ( option_msk_t options, u8 adv_sid, u8 adv_adrType, u8 *adv_addr, u16 skip, sync_tm_t sync_timeout, u8 sync_cte_type);
ble_sts_t blc_ll_periodicAdvertisingCreateSyncCancel (void);
ble_sts_t blc_ll_periodicAdvertisingTerminateSync (u16 sync_handle);
ble_sts_t blc_ll_addDeivceToPeriodicAdvertiserList (u8 adv_adrType, u8 *adv_addr, u8 adv_sid);
ble_sts_t blc_ll_removeDeivceFromPeriodicAdvertiserList (u8 adv_adrType, u8 *adv_addr, u8 adv_sid);
ble_sts_t blc_ll_clearPeriodicAdvertiserList (void);
ble_sts_t blc_ll_readPeriodicAdvertiserListSize (u8 *perdAdvListSize);
ble_sts_t blc_ll_periodicAdvertisingReceiveEnable (u16 sync_handle, sync_adv_rcv_en_msk enable);
Periodic Advertising Sync Transfer (PAST)
PAST 是《Core_V5.1》新增加的特性,主要用于通知接收设备如何同步到周期广播。 蓝⽛核⼼规范包含完整的详细信息,请参考《Core_V5.4》, Vol 6, Part B 4.6.23 Periodic Advertising Sync Transfer - Sender 和 4.6.24 Periodic Advertising Sync Transfer - Recipient。
PAST 模式1
如上图,在没有PAST的情况下,smart phone已经与TV进行了周期广播同步。这能让smart phone收到周期广告数据包AUX_SYNC_IND。同时,smart phone还能与smart watch建立ACL连接。如果smart watch希望从TV处获得周期广播数据包,那么smart watch需要自行扫描并与TV进行周期广播同步。smart watch在完成这一过程时需要耗费额外的时间和电能,然而此类设备往往电量有限。 在有PAST的情况下,情况会截然不同。在相同的情景中,如果smart watch想要扫描并与TV进行周期广播同步,smart phone能够通过LE ACL将周期广播同步信息通过LL_PERIODIC_SYNC_IND传输到smart watch,watch就可以与TV进行周期广播同步了。PAST能够简化这一过程并帮助电量有限的设备节省电能。 找了一个网上的英文,讲解很到位,不需要再翻译。Some device types, with limited power, may not be able to afford the energy cost associated with the periodic advertising synchronization procedure or may have limitations in duty cycle or scan time that prevent it from working.The new Periodic Advertising SyncTransfer (PAST) feature allows another less constrained device to perform the synchronization procedure and then pass the acquired synchronization details over a point-to-point Bluetooth Low Energy connection to the other,constrained device. For example.a smartphone could scan for AUX_SYNC_IND packets from a TV and then pass them over a connection to an associated smart watch so that the watch can then benefit from using periodic advertising and scanning to acquire data from the TV.
注意: PAST涉及三方设备:广播设备(TV)、辅助器(smart phone,作为 Central)、接收设备 (smart watch,作为 Peripheral),其中广播设备(TV)和辅助器(smart phone)在实现上可以是同一个设备,也可以独立存在,这意味着PAST存在两种模式:(1) 辅助器(central)通过扩展扫描获取广播源时序信息;(2)辅助器(central)自身就是广播源。下图展示的是模式(2)的情形。PAST 模式2
PAST相关API介绍
void blc_ll_initPAST_module(void);
ble_sts_t blc_ll_periodicAdvSyncTransfer(u16 connHandle, u16 serviceData, u16 syncHandle);
ble_sts_t blc_ll_periodicAdvSetInfoTransfer(u16 connHandle, u16 serviceData, u8 advHandle);
ble_sts_t blc_ll_setPeriodicAdvSyncTransferParams(u16 connHandle, u8 mode, u16 skip, u16 syncTimeout, u8 cteType);
ble_sts_t blc_ll_setDftPeriodicAdvSyncTransferParams(u8 mode, u8 skip, u16 syncTimeout, u8 cteType);
PAwR
BLE Spec 从 core_5.4 开始增加了PAwR。 PAwR全称Periodic Advertising with Responses,用于通过周期性广播(具体内容可参考周期广播章节)将数据和命令发送给特定的同步设备,同时可以接收同步设备的响应信息,目前PAwR的典型案例是电子货架标签(ESL)。
PAwR基本原理
跟据功能不同,PAwR中分为广播者和观察者两种角色。广播者负责进行PAwR广播,向观察者发送控制命令和数据,同时接收观察者的响应数据。观察者负责监听相关的PAwR广播,并做出响应。 PAwR在周期广播的基础上,充分利用periodic event的间隔时间,将这一段时间分为多个subevent。如下图所示,每个subevent分配有唯一的编号,以ESL为例,该编号对应ESL设备的group ID,即具有相同group ID的ESL设备会同时监听对应的subevent。
subevent的结构如下图所示:
在subevent起始位置,会发送下面两种类型的同步包:
-
AUX_SYNC_SUBEVENT_IND:包含控制命令和数据的同步请求包
-
AUX_CONNECT_REQ:ACL连接请求
发送完成后,等待一定的延迟并进入接收状态。可以看到接收的时序中被分为多个slot,该slot被称为响应槽,监听该subevent的观察者往往有多个,每个观察者需要在各自对应的响应槽时间内进行响应,响应槽的分配可根据具体场景进行设置。以ESL为例,观察者做出响应时的响应槽编号在每一次subevent内动态分配,由广播者的同步广播包中的命令顺序和观察者的自身ID共同决定,具体分配过程可参考《Electronic Shelf Label Profile》(5.3.1.4.2 “ Allocation of response slots to ESLs”)。
PAwR同步
上面我们提到PAwR是在周期广播基础上进行的扩展,subevent的监听和响应都是在已经建立同步的基础上进行的。 为实现同步,作为观察者首先需要知道PAwR事件发生周期(periodic advertising interval),以及下一次PAwR事件发生时刻(syncPacketWindowOffest)。 然后,结合观察者配置的subevent ID和响应槽编号,同时需要知道以下信息来确定需要监听和响应的时刻:
-
Num_Subevents:一个周期内的subevent数量
-
Subevent_interval:一个subevent开始到下一个subevent开始的时间
-
Response_Slot_Delay:从subevent开始到第一个响应槽的时间。
-
Response_Slot_spacing:从一个响应槽开始到下一个响应槽开始的时间
-
Num_Response_Slots:subevent中响应槽的数量
上述信息的获取有两种方式,一种是通过观察者设备直接扫描获取,AUX_ADV_IND中ACAD包含上述信息。第二种是通过PAST,PAST是指广播者或第三方设备首先与观察者设备建立ACL连接,将包含同步信息的PAST packet发送给观察者设备完成同步(具体内容可参考PAST章节)。
PAwR相关API介绍
广播者端API
ble_sts_t blc_ll_initPeriodicAdvWrModule_initPeriodicdAdvWrSetParamBuffer(u8 *pBuff, int num_periodic_adv);
void blc_ll_initPeriodicAdvWrDataBuffer(u8 *pSubeventData, int subeventDataLenMax, int subeventDataCnt);
//不通过HCI,host层直接设置controler层相关参数
ble_sts_t blc_ll_setPeriodicAdvParam_v2(adv_handle_t adv_handle,
u16 advInter_min,
u16 advInter_max,
perd_adv_prop_t property,
u8 numSubevents,u8 subeventInterval,
u8 responseSlotDelay,
u8 responseSlotSpace,
u8 numResponseSlots);
//通过HCI command 进行调用
ble_sts_t blc_hci_le_setPeriodicAdvParam_v2(hci_le_setPeriodicAdvParamV2_cmdParam_t* pCmdParam);
ble_sts_t blc_ll_setPeriodicAdvEnable(u8 per_adv_enable, adv_handle_t adv_handle);
ble_sts_t blc_ll_periodicAdvSetInfoTransfer(u16 connHandle, u16 serviceData, u8 advHandle);
//不通过HCI,host层直接操作controle层buffer
ble_sts_t blc_ll_setPeriodicAdvSubeventData(adv_handle_t adv_handle, u8 num_subevent, pdaSubevtData_subevtCfg_t* pSubevtCfg);
//通过HCI command 进行调用
ble_sts_t blc_hci_le_setPeriodicAdvSubeventData(hci_le_setPeridAdvSubeventData_cmdParam_t* pcmdParam, hci_le_setPeridAdvSubeventDataRetParams_t *pRetParams)
//不通过HCI,控制controler层建立连接
ble_sts_t blc_ll_extended_createConnection_v2 (adv_handle_t adv_handle, u8 subevent,
init_fp_t filter_policy, own_addr_type_t ownAdrType, u8 peerAdrType, u8 *peerAddr, init_phy_t init_phys,
scan_inter_t scanInter_0, scan_wind_t scanWindow_0, conn_inter_t conn_min_0, conn_inter_t conn_max_0, conn_tm_t timeout_0,
scan_inter_t scanInter_1, scan_wind_t scanWindow_1, conn_inter_t conn_min_1, conn_inter_t conn_max_1, conn_tm_t timeout_1,
scan_inter_t scanInter_2, scan_wind_t scanWindow_2, conn_inter_t conn_min_2, conn_inter_t conn_max_2, conn_tm_t timeout_2 );
//通过HCI command 进行调用
ble_sts_t blc_hci_le_extended_createConnection_v2( hci_le_ext_createConnV2_cmdParam_t * pCmdParam);
观察者端API
ble_sts_t blc_ll_initPAwRsync_module(int num_pawr_sync);
ble_sts_t blc_ll_initPAwRsync_rspDataBuffer(u8 *pdaRspData, int maxLen_pdaRspData);
void blc_ll_switch2PAwR_syncSubevt0(st_pda_sync_t* pPAwR_sync)
ble_sts_t blc_hci_le_setPeriodicSyncSubevent(u16 sync_handle,
u16 pda_prop,
u8 num_subevent,
u8* pSubevent)
ble_sts_t blc_hci_le_setPAwRsync_rspData( u16 sync_handle,
u16 req_pdaEvtCnt,
u8 req_subEvtCnt,
u8 rsp_subEvtCnt,
u8 rsp_slotIdx,
u8 rspDataLen,
u8* pRspData)
BLE spec标准接口,设置响应数据。 详情请参照《core5.4》(vol4/Part E/7.8.126 "LE Set Periodic Advertising Response Data command")。
PAwR相关EVENT
详情请参照《core5.4》(vol4/Part E/7.7.65 " LE Meta event")。
HCI_LE_Periodic_Advertising_Subevent_Data_Request
HCI_LE_Periodic_Advertising_Response_Report
HCI_LE_Periodic_Advertising_Report[v2]
HCI_LE_Periodic_Advertising_Sync_Lost
HCI_LE_Periodic_Advertising_Sync_Established[v2]
HCI_LE_Periodic_Advertising_Sync_Transfer_Received[v2]
PAwR数据格式
使用PAwR技术的数据格式目前尚无统一定义,用户可参考ESL数据格式进行自定义。
低功耗管理
低功耗管理(Low Power Management)也可以称为功耗管理(Power Management),本文档中会简称为PM。
低功耗驱动
低功耗模式
MCU正常执行程序时处于working mode,此时工作电流在3~7mA之间。如果需要省功耗需要进入低功耗模式。
低功耗模式(low power mode)又称sleep mode,包括3种:suspend mode、deepsleep mode和deepsleep retention mode。
Module | suspend | deepsleep retention | deepsleep |
---|---|---|---|
Sram | 100% keep | first 32K/64K/96K keep, others lost | 100% lost |
digital register | 99% keep | 100% lost | 100% lost |
analog register | 100% keep | 99% lost | 99% lost |
上表为3种sleep mode下Sram、数字寄存器(digital register)、模拟寄存器(analog register)状态保存的统计说明。
(1) Suspend mode (sleep mode 1)
此时程序停止运行,类似一个暂停功能。MCU大部分硬件模块断电,PM模块维持正常工作。此时IC电流B91在40-50uA之间。当suspend被唤醒后,程序继续执行。
suspend mode下所有的SRAM和analog register都能保存状态,绝大部分digital register都保持状态。digital register中存在少量会掉电的,如baseband电路中少量的digital register,user需要关注的是API rf_set_power_level_index()设置的寄存器,本文档前面已经介绍,这个API需要在每次suspend醒来后都重新调用一次。
(2) Deepsleep mode (sleep mode 2)
此时程序停止运行,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。在deepsleep mode下IC电流小于1uA。如果内置flash的standby电流出现较大的1uA左右,可能导致测量到deepsleep为1~2uA。deepsleep mode wake up时,MCU将重新启动,类似于上电的效果,程序会重新开始进行初始化。
Deepsleep mode下,除了analog register上有少数几个register能保存状态,其他所有Sram、digital register、analog register全部掉电丢失。
(3) Deepsleep retention mode (sleep mode 3)
上面的deepsleep mode,电流很低,但是无法存储Sram信息;suspend mode Sram和register可以保持不丢,但是电流偏高。
为了实现一些需要sleep时电流很低又要能够确保sleep唤醒后能立刻恢复状态的应用场景(比如BLE长睡眠维持连接),B91m增加了一种sleep mode 3:deepsleep with Sram retention mode,简称deepsleep retention(或deep retention)。根据Sram retention area的大小不同,B91分为deepsleep retention 32K Sram和deepsleep retention 64K Sram,B92分为deepsleep retention 32K Sram、deepsleep retention 64K Sram和deepsleep retention 96K Sram。
deepsleep retention mode也是一种deepsleep,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。功耗是在deepsleep mode基础上增加retention Sram消耗的电,电流在2~3uA之间。deepsleep retention mode wake up时,MCU将重新启动,程序会重新开始进行初始化。
deepsleep retention mode和deepsleep mode在register状态保存方面表现一致,几乎全部掉电。deepsleep retention mode跟deepsleep mode相比,Sram的前32K/64K/96K可以保持不掉电,剩余的Sram全部掉电。
低功耗唤醒源
B91m MCU的低功耗唤醒源示意图如下,suspend/deepsleep/deepsleep retention都可以被GPIO PAD和timer唤醒。Telink B91m BLE Multiple Connection SDK 中,只关注2种唤醒源,如下所示(注意code中PM_TIM_RECOVER_START和PM_TIM_RECOVER_END两个定义不是唤醒源):
typedef enum {
PM_WAKEUP_PAD = BIT(3),
......
PM_WAKEUP_TIMER = BIT(5),
......
}pm_sleep_wakeup_src_e;
如上图所示,MCU的suspend/deepsleep/deepsleep retention在硬件上有2个唤醒源:TIMER、GPIO PAD。
-
唤醒源PM_WAKEUP_TIMER来自硬件32k timer(32k RC timer or 32k Crystal timer)。32k timer在SDK中已经被正确初始化,user在使用时不需要任何配置,只需要在cpu_sleep_wakeup()中设置该唤醒源即可。
-
唤醒源PM_WAKEUP_PAD来自GPIO模块,除MSPI 4个管脚外所有的GPIO(PAx/PBx/PCx/PDx/PEx)的高低电平都具有唤醒功能。
配置GPIO PAD唤醒sleep mode的API:
typedef enum{
Level_Low=0,
Level_High,
} GPIO_LevelTypeDef;
void cpu_set_gpio_wakeup (GPIO_PinTypeDef pin, GPIO_LevelTypeDef pol, int en);
pin为GPIO定义。
pol为唤醒极性定义: Level_High表示高电平唤醒,Level_Low表示低电平唤醒。
en: 1表示enable,0表示disable。
举例说明:
cpu_set_gpio_wakeup (GPIO_PC2, Level_High, 1); //GPIO_PC2 PAD唤醒打开, 高电平唤醒
cpu_set_gpio_wakeup (GPIO_PC2, Level_High, 0); //GPIO_PC2 PAD唤醒关闭
cpu_set_gpio_wakeup (GPIO_PB5, Level_Low, 1); //GPIO_PB5 PAD唤醒打开, 低电平唤醒
cpu_set_gpio_wakeup (GPIO_PB5, Level_Low, 0); //GPIO_PB5 PAD唤醒关闭
低功耗模式的进入和唤醒
在Telink B91m BLE Multiple Connection SDK中suspend和deepsleep retention由stack进行管控,不推荐客户自己设置进入suspend/deepsleep retention。不过user可以设置进入deepsleep模式。
设置MCU进入睡眠和唤醒的API为:
int cpu_sleep_wakeup (pm_sleep_mode_e sleep_mode, SleepWakeupSrc_TypeDef wakeup_src,
unsigned int wakeup_tick);
- 第一个参数sleep_mode:设置sleep mode,有以下4个选择,目前客户可以选择的只有一个:deepsleep mode。(suspend和deepsleep retention由stack管控)
typedef enum {
......
DEEPSLEEP_MODE = 0x30,
......
}pm_sleep_mode_e;
-
第二个参数wakeup_src:设置当前的suspend/deepsleep的唤醒源,参数只能是PM_WAKEUP_PAD、PM_WAKEUP_TIMER中的一个或者多个。如果wakeup_src为0,那么进入低功耗sleep mode后,无法被唤醒。
-
第三个参数wakeup_tick:当wakeup_src中设置了PM_WAKEUP_TIMER时,需要设置wakeup_tick来决定timer在何时将MCU唤醒。如果没有设置PM_WAKEUP_TIMER唤醒,该参数无意义。
wakeup_tick的值是一个绝对值,按照本文档前面介绍的System Timer tick来设置,当System Timer tick的值达到这个设定的wakeup_tick后,sleep mode被唤醒。wakeup_tick的值需要根据当前的System Timer tick的值,加上由需要睡眠的时间换算成的绝对时间,才可以有效地控制睡眠时间。如果没有考虑当前的System Timer tick,直接对wakeup_tick进行设置,唤醒的时间点就无法控制。
由于wakeup_tick是绝对时间,必须在32bit的System Timer tick能表示的范围之内,所以这个API能表示的最大睡眠时间是有限的。目前的设计是最大睡眠时间为32bit能表示的最大System Timer tick对应时间的7/8。System Timer tick最大能表示大概268s,那么最长sleep时间时间为268*7/8=234s,即下面delta_Tick不能超过234s。
cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER, clock_time() + delta_tick);
返回值为当前sleep mode的唤醒源的集合,该返回值各bit对应表示的唤醒源为:
typedef enum {
......
WAKEUP_STATUS_TIMER = BIT(1),
WAKEUP_STATUS_PAD = BIT(3),
......
STATUS_GPIO_ERR_NO_ENTER_PM = BIT(7),
......
}pm_wakeup_status_e;
a) WAKEUP_STATUS_TIMER这个bit为1,说明当前sleep mode是被Timer唤醒。
b) WAKEUP_STATUS_PAD这个bit为1,说明当前sleep mode是被GPIO PAD唤醒。
c) WAKEUP_STATUS_TIMER和WAKEUP_STATUS_PAD同时为1时,表示Timer和GPIO PAD两个唤醒源同时生效了。
d) STATUS_GPIO_ERR_NO_ENTER_PM是一个比较特殊的状态,表示当前发生了GPIO唤醒错误:比如当设置了某个GPIO PAD高电平唤醒,而在这个GPIO为高电平的时候尝试调用cpu_sleep_wakeup进入suspend,且设置了PM_WAKEUP_PAD唤醒源。此时会出现无法进入suspend,MCU立刻退出cpu_sleep_wakeup函数,给出返回值STATUS_GPIO_ERR_NO_ENTER_PM。
一般采用如下的形式来控制睡眠时间:
cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_TIMER, clock_time() + delta_Tick);
delta_Tick是一个相对的时间(比如100* CLOCK_16M_SYS_TIMER_CLK_1MS),加上当前的clock_time()就变成了绝对时间。
举例说明cpu_sleep_wakeup的用法:
cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_PAD, 0);
程序执行该函数时进入suspend mode,只能被GPIO PAD唤醒。
cpu_sleep_wakeup (DEEPSLEEP_MODE , PM_WAKEUP_TIMER, clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1MS);
程序执行该函数时进入deepsleep mode,只能被Timer唤醒,唤醒时间为当前时间加上10ms,所以deepsleep 时间为10ms。
cpu_sleep_wakeup (DEEPSLEEP_MODE , PM_WAKEUP_PAD | PM_WAKEUP_TIMER,
clock_time() + 50* CLOCK_16M_SYS_TIMER_CLK_1MS);
程序执行该函数时进入deepsleep模式,可被GPIO PAD和Timer唤醒,Timer唤醒的时间设置为50ms。如果在50ms结束之前触发了GPIO的唤醒动作,MCU会被GPIO PAD唤醒;如果50ms内无GPIO动作,MCU会被Timer唤醒。
cpu_sleep_wakeup (DEEPSLEEP_MODE, PM_WAKEUP_PAD, 0);
程序执行该函数时进入deepsleep mode,可被GPIO PAD唤醒。
低功耗唤醒后运行流程
当user调用API cpu_sleep_wakeup()后,MCU进入sleep mode;当唤醒源触发MCU唤醒后,对于不同的sleep mode,MCU的软件运行流程不一致。
下面详细介绍suspend、deepsleep、deepsleep retention 3种sleep mode被唤醒后的MCU运行流程。请参考下图。
MCU上电(Power on)之后,各流程的介绍:
(1) 运行硬件bootloader(Run hardware bootloader)
MCU硬件上执行一些固定的动作,这些动作固化在硬件上,软件无法修改。
举几个例子说明一下这些动作,比如:读flash的boot启动标记,判断当前应该运行的firmware是存储在flash地址 0上的,还是在flash地址0x20000上的(跟OTA相关);读flash相应位置的值,判断当前需要从flash上拷贝多少数据到Sram,作为常驻内存的数据(参考第2章对Sram分配的介绍)。
运行硬件bootloader部分由于涉及到flash上数据拷贝到sram,一般执行时间较长,比如拷贝10K数据大概耗时5ms左右。
(2) 运行软件bootloader(Run software bootloader)
hardware bootloader运行结束之后,MCU开始运行software bootloader。Software bootloader就是前面介绍过的vector端。
Software bootloader是为了给后面C语言程序的运行设置好内存环境,可以理解为整个内存的初始化。
(3) 系统初始化(System initialization)
System initialization对应main函数中cpu_wakeup_init到user_init之前各硬件模块初始化(包括cpu_wakeup_init、rf_drv_init、gpio_init、clock_init),设置各硬件模块的数字/模拟寄存器状态。
(4) 用户初始化(User initialization)
User initialization对应SDK中函数user_init或user_init_normal/ user_init_deepRetn。
(5) main_loop
User initialization完成后,进入while(1)控制的main_loop。main_loop中进入sleep mode之前的一系列操作称为"Operation Set A”,sleep 唤醒之后一系列操作称为"Operation Set B”。
对照上图sleep mode流程分析。
(6) no sleep
如果没有sleep mode,MCU的运行流程为在while(1)中循环,反复执行“Operation Set A” ->“Operation Set B”。
(7) suspend
如果调用cpu_sleep_wakeup函数进入suspend mode,当suspend被唤醒后,相当于cpu_sleep_wakeup函数的正常退出,MCU运行到"Operation Set B”。
suspend是最干净的sleep mode,在suspend期间所有的Sram数据能保持不变,所有的数字/模拟寄存器状态也保持不变(只有几个特殊的例外);suspend唤醒后,程序接着原来的位置运行,几乎不需要考虑任何sram和寄存器状态的恢复。suspend的缺点是功耗偏高。
(8) deepsleep
如果调用cpu_sleep_wakeup函数进入deepsleep mode,当deepsleep被唤醒后,MCU会重新回到Run hardware bootloader。
可以看出,deepsleep wake_up跟Power on的流程是几乎一致的,所有的软硬件初始化都得重新做。
MCU进入deepsleep后,所有的Sram和数字/模拟寄存器(只有几个模拟寄存器例外)都会掉电,所以功耗很低,MCU电流小于1uA。
(9) deepsleep retention
如果调用cpu_sleep_wakeup函数进入deepsleep retention mode,当deepsleep retention被唤醒后,MCU会重新回到Run software bootloader。
deepsleep retention是介于suspend和deepsleep之间的一种sleep mode。
suspend因为要保存所有的sram和寄存器状态而导致电流偏高;deepsleep retention不需要保存寄存器状态,Sram只保留前32K/64K/96K不掉电,所以功耗比suspend低很多,只有2uA左右。
deepsleep wake_up后需要把所有的流程重新运行一遍,而deepsleep retention可以跳过"Run hardware bootloader”这一步,这是因为Sram的前32K/64K/96K上数据是不丢的,不需要再从flash上重新拷贝一次。但由于Sram上retention area有限,"run software bootloader”无法跳过,必须得执行;由于deepsleep retention无法保存寄存器状态,所以system initilization必须执行,寄存器的初始化需要重新设置。deepsleep retention wake_up后的User initialization deep retention可以做一些优化改进,和MCU power on/deepsleep wake_up后的User initialization normal做区分处理。
API pm_is_MCU_deepRetentionWakeup
由图"sleep mode wakeup work flow"可以看到,MCU power on、deepsleep wake_up、deepsleep retention wake_up 这3种情况都需要经过Run software bootloader、System initialization、User initialization。 在运行system initialization、user initialization 2个步骤时,user需要知道当前MCU是否被deepsleep retention wake_up的,以便做一些区分于power on、deepsleep wake_up的设置。PM driver提供判断是否deepsleep retention wake_up的API为:
int pm_is_MCU_deepRetentionWakeup(void);
return 值为1,表示deepsleep retention wake_up;return 值为0,表示power on或deepsleep wake_up。
BLE低功耗管理
BLE PM初始化
如果使用了低功耗模式,需要将BLE PM模块初始化,调用下面API即可。
void blc_ll_initPowerManagement_module(void);
若不需要低功耗模式,不调用此API,则PM相关的代码和变量都不会被编译到程序中,可以节省firmware size和sram size。
BLE PM for Link Layer
Telink B91m BLE Multiple Connection SDK 中对Legacy advertising state、Scanning state、ACL connection central和ACL connection peripheral做了低功耗管理。
需要说明的是,SDK目前peripheral使用latency是有限制条件的,如果不满足限制条件每个interval都会进行收发包。即便是作为peripheral接受了对方central的连接参数,其中latency不为0,SDK也会按照latency为0进行RF收发数据。
peripheral使用latency的限制条件:
(1)只有Legacy advertising和ACL peripheral任务。
(2)ACL peripheral只有1个连接(后面SDK会优化支持更多的peripheral连接)。
(3)如果有Legacy advertising,最小广播间隔需要大于195ms。
对于Idle state,SDK不提供任何低功耗管理。由于此状态不涉及BLE RF任何动作(即blc_sdk_main_loop函数完全无效),user可以自行调用PM driver去做一些低功耗管理。
sleep for advertising “only advertising”
当只使能了广播,关闭了scan功能,即Link Layer处于advertising state时,时序如下:
当到达advertising时间时,就会从sleep唤醒,然后处理广播事件。处理完成后,stack会判断下一次Adv Event的时间点到目前时间的差值,如果满足条件,就会进入sleep降低功耗。Adv Event消耗的时间和具体情况有关,比如:用户只设置了37channel;ADV packet长度比较小;在channel37或38收到了SCAN_REQ或CONNECT_IND等等。
sleep for scanning “only scanning”
根据Scan window的大小决定实际的scanning时间,如果Scan window等于Scan interval,所有的时间都在scanning;如果Scan window小于Scan interval,从Scan interval的前面部分开始去分配时间来进行scanning,等效时间参考Scan window。
图上所示的Scan window大约是Scan interval的40%,在前40%的时间里,Link Layer处于scanning状态,PHY层在收包,同时用户可以利用这段时间在main_loop中执行自己的UI task。后60%的这段时间MCU进入sleep以降低整机功耗。
设置占比的API如下:
blc_ll_setScanParameter(SCAN_TYPE_PASSIVE, SCAN_INTERVAL_200MS, SCAN_WINDOW_50MS, OWN_ADDRESS_PUBLIC, SCAN_FP_ALLOW_ADV_ANY);
sleep for connection
进入sleep条件是:
(1) 下一个任务距离目前任务结束的时间间隔大小;
(2) RX FIFO中是否有数据未处理;
(3) BRX POST和BTX POST执行完成;
(4) 设备自身没有任何事件pending。
如果距离下一个任务的时间间隔比较大,且RX FIFO没有数据,也没有任何事件pending,当BRX POST或BTX POST执行后,底层就会让MCU进入sleep。等到下一个任务即将到来前,timer唤醒MCU开始执行该任务。
相关变量
BLE PM软件处理流程部分会出现很多变量,用户有必要了解这些变量。 Telink B91m BLE Multiple Connection SDK 在底层定义了结构体 “st_llms_pm_t” ,下面只列出该结构体部分变量(API介绍时需要用到的变量)。
typedef struct {
u8 deepRt_en;
u8 deepRet_type;
u8 wakeup_src;
u16 sleep_mask;
u16 user_latency;
u32 deepRet_thresTick;
u32 deepRet_earlyWakeupTick;
u32 sleep_taskMask;
u32 next_task_tick;
u32 current_wakeup_tick;
}st_llms_pm_t;
st_llms_pm_t blmsPm;
注意:
上述的结构体变量被封装在library中,这里给出定义只是为了方便后面的介绍,用户不允许对这个结构体变量进行任何操作。
下面的介绍中会经常出现类似 “blmsPm.sleep_mask” 的变量。
API blc_pm_setSleepMask
用于配置低功耗管理的API:
void blc_pm_setSleepMask (sleep_mask_t mask);
使用blc_pm_setSleepMask设置blmsPm.sleep_mask(默认值为PM_SLEEP_DISABLE)。
这个API的源码为:
void blc_pm_setSleepMask (sleep_mask_t mask)
{
u32 r = irq_disable();
......
blmsPm.sleep_mask = mask;
......
u32 r = irq_disable();
}
blmsPm.sleep_mask的设置,可以选择下面几个值中的一个,也可以选择多个值的“或”操作。
typedef enum {
PM_SLEEP_DISABLE = 0,
PM_SLEEP_LEG_ADV = BIT(0),
PM_SLEEP_LEG_SCAN = BIT(1),
PM_SLEEP_ACL_PERIPHR = BIT(2),
PM_SLEEP_ACL_CENTRAL = BIT(3),
PM_SLEEP_EXT_ADV = BIT(4),
PM_SLEEP_CIS_PERIPHR = BIT(8),
PM_SLEEP_CIS_CENTRAL = BIT(9),
}sleep_mask_t;
PM_SLEEP_DISABLE表示sleep disable,不允许MCU进入sleep。
PM_SLEEP_LEG_ADV和PM_SLEEP_LEG_SCAN分别用于控制Legacy advertising state和Scanning state时MCU进入sleep。
PM_SLEEP_ACL_PERIPHR和PM_SLEEP_ACL_CENTRAL分别用于控制ACL connection peripheral和ACL connection central时MCU进入sleep。
该API最常用的2种情况如下:
(1) blc_pm_setSleepMask(PM_SLEEP_DISABLE);
MCU不允许进入sleep。
(2) blc_pm_setSleepMask(PM_SLEEP_LEG_ADV | PM_SLEEP_LEG_SCAN | PM_SLEEP_ACL_PERIPHR | PM_SLEEP_ACL_CENTRAL);
MCU在Legacy advertising state、Scanning state、ACL connection peripheral和ACL connection central时都允许进入sleep。
API blc_pm_setWakeupSource
user通过上面的API blc_pm_setSleepMask设置MCU进入sleep mode(suspend或deepsleep retention),通过下面的API可设置sleep mode的唤醒源。
void blc_pm_setWakeupSource (pm_sleep_wakeup_src_e wakeup_src)
{
blmsPm.wakeup_src = (u8)wakeup_src;
}
wakeup_src可以选择唤醒源PM_WAKEUP_PAD。
该API设置底层变量blmsPm.wakeup_src。
MCU在Legacy advertising state、Scanning state、ACL connection central和ACL connection peripheral进入sleep mode时,实际的唤醒源为:
blmsPm.wakeup_src | PM_WAKEUP_TIMER
即PM_WAKEUP_TIMER是一定会有的,不依赖于user的设定,这是为了保证MCU一定要在特定的时间点唤醒去处理接下来的ADV task、SCAN task、central task、peripheral task。
每次调用blc_pm_setWakeupSource设置唤醒源后,一旦MCU进入sleep mode被唤醒后,blmsPm.wakeup_src会被清0。
API blc_pm_setDeepsleepRetentionType
前面介绍了deepsleep retention根据retention sram size的差别有分为 32K/64K/96K sram retention。当sleep mode中deepsleep retention mode生效时,SDK会根据设置进入相应的deepsleep retention mode。
B91可选的模式只有以下两种,32K和64K,SDK默认的deepsleep retention mode为DEEPSLEEP_MODE_RET_SRAM_LOW64K:
typedef enum {
SUSPEND_MODE = 0x00,
DEEPSLEEP_MODE = 0x30,
DEEPSLEEP_MODE_RET_SRAM_LOW32K = 0x21,
DEEPSLEEP_MODE_RET_SRAM_LOW64K = 0x03,
DEEPSLEEP_RETENTION_FLAG = 0x0F,
}pm_sleep_mode_e;
typedef enum {
SUSPEND_MODE = 0x00,
DEEPSLEEP_MODE = 0x70,
DEEPSLEEP_MODE_RET_SRAM_LOW32K = 0x61,
DEEPSLEEP_MODE_RET_SRAM_LOW64K = 0x43,
DEEPSLEEP_MODE_RET_SRAM_LOW96K = 0x07,
DEEPSLEEP_RETENTION_FLAG = 0x0F,
}pm_sleep_mode_e;
void blc_pm_setDeepsleepRetentionType(pm_sleep_mode_e sleep_type)
{
blmsPm.deepRet_type = sleep_type;
}
注意:
该API的调用必须在blc_ll_initPowerManagement_module之后才能生效。
API blc_pm_setDeepsleepRetentionEnable
该API用于使能deepsleep retention mode。
typedef enum {
PM_DeepRetn_Disable = 0x00,
PM_DeepRetn_Enable = 0x01,
} deep_retn_en_t;
void blc_pm_setDeepsleepRetentionEnable (deep_retn_en_t en)
{
blmsPm.deepRt_en = en;
}
API blc_pm_setDeepsleepRetentionThreshold
在BLE task存在,满足以下条件,suspend才会被自动切换为deepsleep retention:
//判断sleep mode是suspend mode还是deepsleep retention mode
pm_sleep_mode_e sleep_M = SUSPEND_MODE;
if( blmsPm.deepRt_en && (u32)(blmsPm.current_wakeup_tick - clock_time() - blmsPm.deepRet_thresTick) < BIT(30) ){
sleep_M = (pm_sleep_mode_e)blmsPm.deepRet_type;
}
第一个条件blmsPm.deepRt_en,需要调用API blc_pm_setDeepsleepRetentionEnable 使能,前面已经介绍过。
第二个条件(u32)(blmsPm.current_wakeup_tick - clock_time() - blmsPm.deepRet_thresTick) < BIT(30),表示sleep的持续时间(即唤醒时间减去实时时间)超过特定的时间阀值时(即blmsPm.deepRet_thresTick),MCU的sleep mode才会从suspend自动切换为deepsleep retention。
API blc_pm_setDeepsleepRetentionThreshold用于设置suspend切换到deepsleep retention触发条件中的时间阀值,这个设计是为了追求更低的功耗。
void blc_pm_setDeepsleepRetentionThreshold(u32 threshold_ms)
{
blmsPm.deepRet_thresTick = threshold_ms * SYSTEM_TIMER_TICK_1MS;
}
PM软件处理流程
低功耗管理的软件处理流程,下面将使用代码与伪代码相结合的方式来说明,目的是为了让user了解处理流程的所有逻辑细节。
blc_sdk_main_loop
Telink B91m BLE Multiple Connection SDK 中,blc_sdk_main_loop在一个while(1)的结构中被反复调用。
while(1)
{
////////////////////////////////////// BLE entry /////////////////////////////////
blc_sdk_main_loop();
////////////////////////////////////// UI entry /////////////////////////////////
// UI task
////////////////////////////////////// PM entry /////////////////////////////////
app_process_power_management();
}
blc_sdk_main_loop函数在while(1)中不断被执行,BLE低功耗管理的code在blc_sdk_main_loop函数中,所以低功耗管理的code也是一直在被执行。
下面是blc_sdk_main_loop函数中低功耗管理逻辑的实现。
void blc_sdk_main_loop (void)
{
......
if( blmsPm.sleep_mask == PM_SLEEP_DISABLE )
{
return; // PM_SLEEP_DISABLE, can not enter sleep mode;sleep time
}
if( !tick1_exceed_tick2(blmsPm.next_task_tick, clock_time() + PM_MIN_SLEEP_US) )
{
return; //too short, can not enter sleep mode.
}
if( bltSche.task_mask && (blmsPm.sleep_taskMask & bltSche.task_mask) != bltSche.task_mask )
//是否有task(adv、scan、central、peripheral)
//sleep_taskMask是否允许该状态(adv、scan、central、peripheral)进入sleep
{
return;
}
if ( (brx_post | btx_post | adv_post | scan_post) == 0 )
{
return; //只能在各个任务完成后才允许进入sleep
}
else
{
blt_sleep_process(); //process sleep & wakeup
}
......
}
(1) 当bltmsPm.sleep_mask为PM_SLEEP_DISABLE时,直接退出,不会执行blt_sleep_process函数。所以user使用blc_pm_setSleepMask(PM_SLEEP_DISABLE)时,低功耗管理的逻辑就会完全失效,MCU不会进入sleep,while(1)的loop一直在执行。
(2) 如果睡眠时间太短,也不会进入sleep。
(3) 当存在任务,比如adv task、scan task、central task、peripheral task,但是如果相应task的sleep_taskMask没有使能也不会进入低功耗模式。
(4) 如果Adv Event或Scan Event或Conn state Central role的Btx Event或Conn state Peripheral role的Brx Event正在执行,blt_sleep_process函数也不会被执行,这是因为此时RF的任务正在运行,SDK需要保证Adv Event/Scan Event/Btx Event/Brx Event结束之后才能进sleep mode。
当以上几个条件都满足时,才去执行blt_sleep_process函数。
blt_sleep_process
blt_sleep_process函数的逻辑实现如下所示。
void blt_sleep_process (void)
{
......
blmsPm.current_wakeup_tick = blmsPm.next_task_tick;//记录唤醒时间点
//执行BLT_EV_FLAG_SLEEP_ENTER回调函数
blt_p_event_callback (BLT_EV_FLAG_SLEEP_ENTER, NULL, 0);
//进入低功耗函数
u32 wakeup_src = cpu_sleep_wakeup (sleep_M, PM_WAKEUP_TIMER | blmsPm.wakeup_src, blmsPm.current_wakeup_tick);
//执行BLT_EV_FLAG_SUSPEND_EXIT回调函数
blt_p_event_callback (BLT_EV_FLAG_SUSPEND_EXIT, (u8 *)&wakeup_src, 1);
blmsPm.wakeup_src = 0;
......
}
上面是blt_sleep_process函数的简要流程,这里看到2个sleep相关event回调函数的执行的时机:BLT_EV_FLAG_SLEEP_ENTER、BLT_EV_FLAG_SUSPEND_EXIT。
关于怎么进入sleep mode,最终调用了driver中的API cpu_sleep_wakeup:
cpu_sleep_wakeup(pm_sleep_mode_e sleep_mode, SleepWakeupSrc_TypeDef wakeup_src, unsigned int wakeup_tick);
唤醒源为PM_WAKEUP_TIMER | blmsPm.wakeup_src,Timer唤醒无条件生效,是为了保证MCU在下一个task到来前唤醒。
blt_sleep_process函数退出时将blmsPm.wakeup_src的值复位,所以需要注意API blc_pm_setWakeupSource设置唤醒源的生命周期,每次设置的值只对最近一次要进入的sleep mode有效。
API blc_pm_getWakeupSystemTick
下面的API用于获取低功耗管理计算的sleep醒来的时间点(System Timer tick),即T_wakeup。
u32 blc_pm_getWakeupSystemTick (void);
T_wakeup的计算是在接近cpu_sleep_wakeup函数处理前,应用层只能在BLT_EV_FLAG_SLEEP_ENTER事件回调函数里才能得到准确的T_wakeup。
假设用户在sleep时间比较长的情况下,需要按键唤醒。下面我们说明一下设置方法。
我们需要使用BLT_EV_FLAG_SLEEP_ENTER事件回调函数和blc_pm_getWakeupSystemTick。
BLT_EV_FLAG_SLEEP_ENTER的回调注册方法如下:
blc_ll_registerTelinkControllerEventCallback (BLT_EV_FLAG_SLEEP_ENTER, &app_set_kb_wakeup);
_attribute_ram_code_ void app_set_kb_wakeup (u8 e, u8 *p, int n)
{
/* sleep time > 100ms. add GPIO wake_up */
if(((u32)(blc_pm_getWakeupSystemTick() - clock_time())) > 100 * SYSTEM_TIMER_TICK_1MS){
blc_pm_setWakeupSource(PM_WAKEUP_PAD); //GPIO PAD wake_up
}
}
以上举例,如果sleep时间超过100ms,就添加GPIO唤醒。user可以根据实际情况来调整。
这里只是提供了一个接口,客户根据实际情况来决定是否使用。
GPIO唤醒的注意事项
唤醒电平有效时无法进入sleep mode
由于Telink MCU的GPIO 唤醒是靠高低电平唤醒,而不是上升沿下降沿唤醒,所以当配置了GPIO PAD唤醒时,比如设置了某个GPIO PAD高电平唤醒suspend,要确保MCU在调用cpu_sleep_wakeup进入suspend时,当前的这个GPIO读到的电平不能是高电平。若当前己经是高电平了,实际进入cpu_sleep_wakeup函数里面,触发suspend时是无效的,会立刻退出来,即完全没有进入suspend。
如果出现以上情况,可能会造成意想不到的问题,比如本来想进入deepsleep后被唤醒,程序重新执行,结果MCU无法进入deepsleep,导致code继续运行,不是我们预想的状态,整个程序的flow可能会乱掉。
user在使用Telink的GPIO PAD唤醒时,要注意避免这个问题。
如果应用层没有很好的规避这个问题,在调用cpu_sleep_wakeup函数时发生了GPIO PAD唤醒源已经生效的情况,为了防止程序进入不可预知的逻辑,PM driver做了一些改善:
(1) suspend & deepsleep retention mode
如果是suspend和deepsleep retention mode,都会很快退出函数cpu_sleep_wakeup,该函数给出的返回值可能出现两种情况:
-
PM模块上检测到了GPIO PAD生效的状态,返回WAKEUP_STATUS_PAD;
-
PM模块上没有检测到GPIO PAD生效的状态,返回STATUS_GPIO_ERR_NO_ENTER_PM
(2) deepsleep mode
如果是deepsleep mode,PM driver会在底层自动将MCU reset(此时的reset跟watchdog reset效果一致),程序回到“Run hardware bootloader”开始重新运行。
应用层定时唤醒
在BLE task存在且不考虑GPIO PAD唤醒的前提下,一旦进入sleep mode,只能在SDK计算好的时间点T_wakeup唤醒,user无法在某一个特定的时间点将sleep提前唤醒。为了增加PM的灵活性,SDK增加了应用层定时唤醒的API和它的回调函数。
应用层定时唤醒API:
void blc_pm_setAppWakeupLowPower(u32 wakeup_tick, u8 enable);
wakeup_tick为定时唤醒的System Timer tick值; enable为1时打开该唤醒功能,enable为0时关闭。
应用层定时唤醒发生时,执行blc_pm_registerAppWakeupLowPowerCb注册的回调函数,其原型和API如下:
typedef void (*pm_appWakeupLowPower_callback_t)(int);
pm_appWakeupLowPower_callback_t pm_appWakeupLowPowerCb = NULL;
void blc_pm_registerAppWakeupLowPowerCb(pm_appWakeupLowPower_callback_t cb)
{
pm_appWakeupLowPowerCb = cb;
}
以ACL Conn state Peripheral role为例:
当user使用blc_pm_setAppWakeupLowPower设置了应用层定时唤醒的app_wakeup_tick,SDK在进入sleep前,会检查app_wakeup_tick是否在T_wakeup之前。
(1) 如果app_wakeup_tick在T_wakeup之前,如下图所示,就会在app_wakeup_tick触发sleep提前唤醒;
(2) 如果app_wakeup_tick在T_wakeup之后,MCU还是会在T_wakeup唤醒。
低电检测
电池电量检测(battery power detect/check),在Telink BLE SDK和相关文档中也可能出现其他的名字,包括:电池电量检测(battery power detect/check)、低电池检测(low battery detect/check)、低电量检测(low power detect/check)、电池检查(battery detect/check)等。比如SDK中相关文件和函数出现battery_check、battery_detect、battery_power_check等命名。
本文档统一以“低电检测(low battery detect)”这个名称进行说明。
低电检测的重要性
使用电池供电的产品,由于电池电量会逐渐下降,当电压低到一定的值后会引起很多问题:
a) B91m 工作电压的范围为1.8V~4.3V。当电压低于1.8V时,B91m 已经无法保证稳定地工作。
b) 当电池电压较低时,由于电源的不稳定,Flash的“write”和“erase”操作可能有出错的风险,造成program firmware和用户数据被异常修改,最终导致产品失效。根据以往的量产经验,我们将这个可能出风险的低压阀值设定为2.0V。
根据上面的描述,使用电池供电的产品,必须设定一个安全电压值(secure voltage),只有当电压高于这个安全电压的时候才允许MCU继续工作;一旦电压低于安全电压,MCU停止运行,需要立刻被shutdown(SDK上使用进入deepsleep mode来实现)。
安全电压也称为报警电压,这个电压值的选取,目前SDK默认使用2.0V。如果user在硬件电路中出现了不合理的设计,导致电源网络稳定性的恶化,安全电压值还需要继续提高,比如2.1V、2.2V等。
对于Telink BLE SDK开发实现的产品,只要使用了电池供电,低电检测都必须是该产品整个生命周期实时运行的任务,以保证产品的稳定性。
低电检测的实现
低电检测需要使用ADC对电源电压进行测量。user请参考文档Data sheet和Driver SDK Developer Handbook相关ADC章节,先对B91m的ADC模块进行必要的了解。
低电检测的实现,结合SDK demo “acl_central_demo”给出的实现来说明,参考文件battery_check.h和battery_check.c。
必须确保app_config.h文件中宏“BATT_CHECK_ENABLE”是被打开的,user使用低电检测功能时需要注意。
#define BATT_CHECK_ENABLE 1
低电检测的注意事项
低电检测是一个基本的ADC采样任务,在实现ADC采样电源电压时,有一些需要注意的问题,说明如下:
建议使用GPIO输入通道
B91m的采样方式可采用Vbat或GPIO模拟信号输入的方式进行采样,但Vbat通道采样精度较差,对采样精度要求高的场合建议通过外部GPIO方式采样。
可用的GPIO输入通道为PB0~PB7、PD0、PD1对应的input channel。
typedef enum{
ADC_GPIO_PB0 = GPIO_PB0 | (0x1<<12),
ADC_GPIO_PB1 = GPIO_PB1 | (0x2<<12),
ADC_GPIO_PB2 = GPIO_PB2 | (0x3<<12),
ADC_GPIO_PB3 = GPIO_PB3 | (0x4<<12),
ADC_GPIO_PB4 = GPIO_PB4 | (0x5<<12),
ADC_GPIO_PB5 = GPIO_PB5 | (0x6<<12),
ADC_GPIO_PB6 = GPIO_PB6 | (0x7<<12),
ADC_GPIO_PB7 = GPIO_PB7 | (0x8<<12),
ADC_GPIO_PD0 = GPIO_PD0 | (0x9<<12),
ADC_GPIO_PD1 = GPIO_PD1 | (0xa<<12),
}adc_input_pin_def_e;
使用GPIO input channel对电源电压进行ADC采样,其具体使用方式如下:在硬件电路设计上,将电源直接和GPIO input channel连接。ADC初始化时,将GPIO设为高阻态(ie、oe、output全部设0),此时GPIO上的电压等于电源电压,直接进行ADC采样即可。
User可通过“acl_central_demo”o的app_config.h中的宏切换GPIO input channel:
//若使用GPIO input channel,将该值置为0
#define VBAT_CHANNEL_EN 1
demo中默认选择PB1为GPIO input channel,PB1作为普通GPIO功能,初始化时所有状态(ie、oe、output)使用默认状态即可,不做特殊修改,用户若想切换GPIO,可选择上述定义的GPIO输入通道修改下面定义即可。
#define GPIO_BAT_DETECT GPIO_PB1
#define PB1_FUNC AS_GPIO
#define PB1_INPUT_ENABLE 0
#define PB1_DATA_OUT 0
#define ADC_INPUT_PIN_CHN ADC_GPIO_PB1
只能使用差分模式
虽然B91m ADC input mode同时支持单端模式(Single Ended Mode)和差分模式(Differential Mode),但由于某些特定的原因,Telink规定:只能使用差分模式,单端模式不允许使用。
差分模式的input channel分为positive input channel(正端输入通道)和negative input channel(负端输入通道),被测量的电压值为positive input channel电压减去negative input channel电压得到的电压差。
如果ADC采样的input channel只有1个,使用差分模式时,将当前input channel设置为positive input channel,将GND设为negative input channel。这样二者的电压差和positive input channel电压相等。
SDK中低压检测使用了差分模式,函数接口如下:
adc_set_diff_input(ADC_INPUT_PIN_CHN >> 12, GND);
不同的ADC任务需要切换
低压检测无法与其他ADC任务同时运行,必须采用切换的方式来实现。
低电检测的单独使用
在SDK demo中,“acl_central_demo”、“acl_connection_demo”、“acl_peripheral_demo”工程中实现了低电检测功能,user需要在app_config.h中开启低电检测的功能进行使用。用户若在其他feature_demo中使用低电检测功能,可参考已经实现的demo中app_config.h的定义和app.c中低电检测相关接口调用逻辑。
低电检测初始化
参考adc_bat_detect_init函数的实现。
ADC初始化的顺序必须满足下面的流程:先power off(掉电)sar adc,然后配置其他参数,最后power on(上电)sar adc。所有ADC采样的初始化都必须遵循这个流程。
_attribute_ram_code_ void adc_bat_detect_init(void)
{
adc_power_off(); // power off sar adc
...... // add ADC Configuration
adc_power_on(); // power on sar adc
}
Sar adc power on与power off之间的配置,user尽量不要去修改,使用这些默认的设置就行。User如果选择了不同的GPIO input channel,直接修改前面讲述的app_config.h有关宏的定义即可。
adc_bat_detect_init初始化函数在app_battery_power_check中调用的code为:
if(!adc_hw_initialized){
adc_hw_initialized = 1;
adc_bat_detect_init();
}
这里使用了一个变量adc_hw_initialized,只有该变量为0时调用一次初始化,并将其置1;该变量为1时不再初始化。adc_hw_initialized在下面API中也会被操作。
void battery_set_detect_enable (int en)
{
lowBattDet_enable = en;
if(!en){
adc_hw_initialized = 0; //need initialized again
}
}
使用了adc_hw_initialized的设计可以实现的功能有:
a) 与其他ADC任务(“ADC other task”)的切换
先不考虑sleep mode(suspend/deepsleep retention)的影响,只分析低电检测与其他ADC任务的切换。
因为需要考虑低电检测与其他ADC任务的切换使用,可能需要adc_bat_detect_init被多次执行,所以不能写到user initialization中,必须在main_loop里实现。
第一次执行app_battery_power_check函数时,adc_bat_detect_init被执行,且后面不会被反复执行。
一旦“ADC other task”需要执行时,将抢走ADC的使用权,确保“ADC other task”初始化时必须调用battery_set_detect_enable(0),此时会将adc_hw_initialized清0。
等“ADC other task”完成后,交出ADC的使用权。app_battery_power_check再次执行,由于adc_hw_initialized值为0,必须再次执行adc_bat_detect_init,这样就保证了低电检测每次切回来时都会重新初始化。
b) 对suspend和deepsleep retention的自适应处理
将sleep mode考虑进来。
adc_hw_initialized这个变量使用必须定义成一个“data”段或“bss”段上的变量,不能定义到retention_data上。定义在“data”段或“bss”上可以保证每次deepsleep retention wake_up后在执行software bootloader(即cstartup_xxx.S)时这个变量会被重新初始化为0;而suspend wake_up后这个变量可以保持不变。
adc_bat_detect_init函数里面配置的register的共同特征是:在suspend mode下不掉电,可以保存状态;在deepsleep retention mode下会掉电。
如果MCU进入suspend mode,醒来后再次执行app_battery_power_check时,adc_hw_initialized的值和suspend之前一致,不需要重新执行adc_bat_detect_init函数。
如果MCU进入deepsleep retention mode,醒来后adc_hw_initialized为0,必须重新执行adc_bat_detect_init,ADC相关的register状态需要被重新配置。
adc_bat_detect_init函数中设定register的状态可以在suspend期间保持不掉电。
SDK中对adc_bat_detect_init函数添加了关键字“_attribute_ram_code_”以设置为ram_code,最终目的是为了优化长睡眠连接态的功耗。比如对典型的 10ms * (99+1) = 1s 的长睡眠连接,每1s醒来一次,中间的长睡眠使用的是deepsleep retention mode,那么每次醒来后adc_bat_detect_init一定会重新执行一次,加入到ram_code后执行速度会变得更快。
这个“_attribute_ram_code_”不是必须的。在产品应用中,user可以根据deepsleep retention area的使用情况,结合功耗测试的结果,来决定是否将此函数放入到ram_code中。
低电检测处理
在main_loop中,调用user_battery_power_check函数实现低电检测的处理,相关code如下:
#if (BATT_CHECK_ENABLE)
/*The frequency of low battery detect is controlled by the variable lowBattDet_tick, which is executed every
500ms in the demo. Users can modify this time according to their needs.*/
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(BAT_DEEP_THRESHOLD_MV);
}
#endif
只有在其他ADC任务需要抢占ADC使用权时,才能通过调用battery_set_detect_enable改变lowBattDet_enable的值:当其他ADC任务开始时,调用battery_set_detect_enable(0),此时main_loop中不会再调用user_battery_power_check函数;在其他ADC任务结束后,调用battery_set_detect_enable(1),交出ADC使用权,此时main_loop中又可以调用user_battery_power_check函数。
变量lowBattDet_tick来控制低电检测的频率,Demo中为每500ms执行一次低电检测。User可以根据自己的需求来修改这个时间值。
user_battery_power_check函数被放到ram_code上,参考上面对“adc_bat_detect_init”放在ram_code的说明,也是为了节省运行时间,优化功耗。
这个“attribute_ram_code”不是必须的。在产品应用中,user可以根据deepsleep retention area的使用情况,结合功耗测试的结果,来决定是否将此函数放入到ram_code中。
_attribute_ram_code_ void user_battery_power_check(u16 alarm_vol_mv);
低压报警
user_battery_power_check的参数为阈值电压,单位为mV。根据前文介绍,SDK中默认设置deepsleep为2000mV,在main_loop的低压检测中,当电源电压低于2000mV时,进入deepsleep模式;当电源电压高于2200mV时才会执行唤醒。
B91m demo中使用进入deepsleep的方式来实现shutdown MCU,进入休眠前设置有LED闪烁提示,并且设置了按键可以唤醒。
程序被shutdown后,进入可被唤醒的deepsleep mode。此时如果发生按键唤醒,SDK会在user initialization(user_init_normal())的时候先快速做一次低电检测,而不是等到main_loop中检测。这样处理的原因是为了避免应用上的错误,举例说明如下:
在deepsleep状态下被按键触发唤醒时(此时工作电压仍然小于唤醒电压),从main_loop的处理来看,需要至少500ms的时间才会去做低电检测,即芯片异常工作500ms。
因为这个原因,SDK必须在user initialization的时候就提前做低电检测,必须在这一步就阻止发生上面的情况。所以在user initialization的时候,添加低电检测,SDK中函数接口为:
#if (BATT_CHECK_ENABLE)
user_battery_power_check(2000);
#endif
在user_battery_power_check函数中如果是从deepsleep状态中唤醒,采用低电检测的阈值电压为参数alarm_vol_mv+200mV,其原因主要是:在shutdown模式唤醒后的快速低电检测时,将报警电压稍微调高一些,调高的幅度比低电检测的最大误差稍大,因此需要对唤醒时检测的电压作出提高的设定。一般来说,只有当某次低电检测发现电压低于2000mV进入shutdown模式后,才会出现恢复电压2200mV,所以user不用担心这个2200mV会对实际电压2V~2.2V的产品误报低压。
低电检测和Amic Audio
参考低电检测单独使用模式中详细的介绍,对于需要实现Amic Audio的产品,只要做好低电检测和Amic Audio的切换即可。
按照低电检测单独使用的方式,程序开始运行后,默认低电检测先开启。当Amic Audio被触发时,做以下两件事:
(1) 关闭低电检测
调用battery_set_detect_enable(0),告知低电检测模块ADC资源已被抢占。
(2) Amic Audio ADC初始化
由于使用ADC的方式和低电检测不一样,需要对ADC重新进行初始化。具体方法参考本文档“Audio”章节的介绍。
Amic Audio结束时,调用battery_set_detect_enable(1),告知低电检测模块ADC资源已经被释放。此时低电检测需要重新初始化ADC模块,然后开始进行低电检测。
如果是低电检测和其他非Amic Audio的ADC任务同时存在,其他ADC任务的处理可模仿Amic Audio的处理流程。
如果是低电检测、Amic Audio、其他ADC任务共3种任务同时存在,user可根据“ADC电路需要切换使用”的原则,参考低电检测和Amic Audio切换实现的方法,去自行实现。
OTA
关于OTA,整个流程和单连接完全一致,请参考Telink B91 BLE Single Connection SDK Handbook对应章节的介绍。
相比Telink B91 BLE Single Connection SDK ,Telink B91m BLE Multiple Connection SDK 增加一个限制:同一时刻只允许在一个peripheral connection上进行OTA。
按键扫描
这部分请参考Telink B91 BLE Single Connection SDK Handbook 对应章节的介绍。
LED管理
这部分请参考Telink B91 BLE Single Connection SDK Handbook 对应章节的介绍。
软件定时器
这部分请参考Telink B91 BLE Single Connection SDK Handbook 对应章节的介绍。
功能参考Demo
本章节将介绍 sdk/vendor/feature_test 下的各个功能的使用方法与现象,用户可以参考代码实现将所需功能添加到自己的代码中。
feature_backup
- 功能:BLE 基本功能的演示。包括广播、被动扫描、连接等。同时,该功能演示也可作为用户开发 BLE 应用的相对“干净“的基础版本(默认关闭 SMP,SDP 等功能)。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_backup 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_FEATURE_BACKUP
来激活这部分代码。代码中设置广播参数、广播内容、扫描响应内容、扫描参数,以及使能广播、使能扫描等配置,请参考初始化代码中的如下内容:
编译,将生成的固件分别烧录到两个开发板中。接电启动后,可通过扫描广播包,扫描到两个设备名为 “feature” 的广播,可以借由其他 Central 设备或者 Peripheral 设备分别进行连接,也可以使二者互联。要使二者互联,按其中一个开发板的按键 SW4 启动连接,通过 BDT 工具的 Tdebug 选项卡,读取左侧的变量值列表(具体使用方法请参考“Debug方法”章节),可以看到该开发板的 conn_master_num 的值为 1:
查看另一个开发板的conn_slave_num变量的值,其值也为1:
代表二者连接成功。
feature_2M_coded_phy
- 功能:BLE 1M/2M/Coded PHY 功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_2M_coded_phy 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_2M_CODED_PHY_CONNECTION
来激活这部分代码,做动态切换 PHY 的演示。
初始化调用 blc_ll_init2MPhyCodedPhy_feature() 来使能 PHY 切换功能。在main_loop() 中的 feature_2m_phy_test_mainloop() 中实现动态切换 PHY 的功能:
(1)Central 建立连接后,每隔 1s 给各 Peripheral 做一次 WriteCmd,有效数据长度为 8Bytes(实际发送间隔还与 Connection Interval 有关)。
(2)Peripheral 建立连接后,每隔 1s 给各个 Central 做一次 Notify,有效数据长度为8Bytes(实际发送间隔还与 Connection Interval 有关)。
(3)各个连接每隔 10s 做一次 PHY 的切换,顺序是Coded_S8 \(\rightarrow\) 2M \(\rightarrow\) 1M \(\rightarrow\) Coded_S8...
实际抓包如下:
feature_gatt_api
- 功能:BLE GATT 指令功能演示与API使用方法。Telink B91m BLE Multiple Connection SDK 中部分实例的 SDP 流程的参考实现中用到了这些指令,用户可以使用该演示代码进行单指令测试。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_gatt_api 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_GATT_API
来激活这部分代码。在 app.c 中,通过修改 TEST_API 的定义来测试不同的 GATT 指令。
编译,将生成的固件分别烧录到两个开发板中。上电,开发板上红灯每隔2s 做一次亮、灭的切换。通过按其中一个开发板(作为 Central)上的SW4 按键,触发连接,通过抓包可以看到每隔 2s,Central 发送一个测试指令,Peripheral 会进行相应的回复,回复的实现参考函数 app_gatt_data_handler()。
以下是分别定义 TEST_API 为不同的指令测试定义时的抓包:
feature_l2cap_coc
- 功能:BLE COC(Connection Oriented Channel)连接和数据发送功能演示。
- 主要硬件: B92开发板 x 2
(1)将feature_config.h中的FEATURE_TEST_MODE宏改为TEST_L2CAP_COC:
#define FEATURE_TEST_MODE TEST_L2CAP_COC
(2)编译下载和操作:
编译并将生成的固件分别烧录到两块开发板中,上电,按其中一个开发板的SW5(作为Master)按键,触发连接,连接成功Master端红灯常亮,Slave端绿灯常亮,然后按任意一个开发板的SW2触发COC连接,连接成功后两块开发板蓝灯常亮,然后按任意一块开发板的SW2发送数据给对方,对方收到数据会toggle一次白灯,同时也可以通过串口查看log,默认GPIO_PD4波特率1000000。
(3)抓包
以下是COC连接和发送数据的抓包截图,发送300字节的数据。
COC连接请求:
COC连接响应:
COC数据发送1(SDU=300,MPS=248,L2CAP层分2帧,第一帧在payload头部包含2字节SDU length): COC数据发送2(SDU=300,MPS=248,L2CAP层分2帧):
feature_ll_more_data
- 功能:BLE MD=1 的演示。MD,即 More Data,是数据通道 PDU Header 中的一个标志位 MD flag。同时,该演示也提供用户做吞吐量的测试使用。
- 主要硬件: B91 开发板 x 2
以B91为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_ll_more_data 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_LL_MD
来激活这部分代码。
编译,将生成的固件分别烧录到两个开发板中。上电,按其中一个开发板(作为 Central)的 SW4 按键,触发连接。连接成功后,红灯点亮(如果连接多个 Peripheral,分别点亮:红色、白色、绿色、蓝色灯)。按 Central 的按键 SW3,可以从抓包中看到 Central 不断向 Peripheral 发送 WriteCmd,其包中的 MD flag 被置为 1,即下一个包已经准备好,即将发送:
按 Central 的按键 SW2,停止发送。
按 Peripheral 的 SW3,可以从抓包中看到 Peripheral 不断向 Central 发送 Notify 包,其中的 MD flag 被置为1:
按 Peripheral 的按键 SW2,停止发送。
feature_dle
- 功能:BLE DLE(Data Length Extension) 和 MTU Exchange 以及L2CAP 分包组包功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_dle 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_LL_DLE
来激活这部分代码,做 DLE 和 MTU 的演示。
通过修改 DLE_LENGTH_SELECT 的定义来修改 DataLength,演示内容为:
(1)通过两个按键同时按下触发测试。
(2)Central 触发测试后,每个连接,每隔 1s 发送一个 WriteCmd。
(3)Peripheral 触发测试后,每个连接,每隔 1s 发送一个 Notify。
抓包如下:
由于 DLE 小于 MTU,可以看到分包的效果:
feature_smp
- 功能:BLE SMP (Security Manager Protocol) 功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_demo/feature_smp 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_demo/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_SMP
来激活这部分代码,默认 Central 和 Peripheral 的 SMP 功能均不使能。
说明:
(1) 由于要演示 SMP 下多个加密配置,为方便用户参考,在feature_smp/app_config.h 中定义 SMP_TEST_MODE 宏,用户仅需修改该宏的定义来实现不同加密配置的演示代码,通过全局搜索该定义的方式,得到 porting 相应 SMP 功能的最简代码。
(2) 配置 SMP 相关参数时,如果 Central 和 Peripheral 配置相同,则使用不带后缀的 API,如果不同,应按照需求使用带 _central/_periphr 后缀的 API。演示代码中为了方便用户另作配置进行测试,对 SMP 加密使能的演示,使用带后缀的 API。
(3) 为方便观察现象,代码中加入了 LED 指示灯的显示,定义如下:
a) 绿色:ON:Central connected;OFF: Central disconnected.
b) 红色:ON:Peripheral connected;OFF: Peripheral disconnected.
c) 蓝色:ON:Pair Succeeded;OFF:Disconnected,没走Pair流程也不亮。
d) 白色:ON:Encryption succeeded;OFF:Disconnected.
(3) 在 SMP 中的角色分为 Initiator 和 Responder,分别对应 BLE 中的角色 Central 和 Peripheral。
(4) 提示: Security Connections 的配对方式,要求 MTU>=65。所以,这里没有使用 default_buffer.h 中的定义。
Peripheral 和 Central 均不使能 SMP
Peripheral 和 Central 的 SMP 功能关闭的演示,需在 feature_smp/app_config.h 中定义(默认):
#define SMP_TEST_MODE SMP_TEST_NOT_SUPPORT
编译 feature_demo,分别烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),可以看到两个开发板分别亮了绿灯和红灯,代表连接成功:
抓包如下:
二者先分别发广播,建立连接后,作为 Central 的设备停止发广播。
仅使能 Peripheral SMP
仅 Peripheral 使能 SMP 功能的演示。该演示需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_ONLY_PERIPHR
编译 feature_demo,分别烧录 sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),从灯效上看,和双方均不使能 SMP 一样。但通过抓包可以看到,这里多了建立连接后 Peripheral 发送 Security Request,但由于 Central 不支持 SMP,Central 并不会做回复,如下图:
说明:
如果用户不希望 Peripheral 建立连接后发 Security Request,应在初始化代码中添加(默认注释掉):
blc_smp_configSecurityRequestSending( SecReq_NOT_SEND, SecReq_NOT_SEND, 0);
该 API 仅针对 Peripheral(只有 Peripheral 会发 Security Request),因此不存在后缀为 _periphr/_central 的相应 API。
仅使能 Central SMP
仅 Central 使能 SMP 功能的演示。该演示需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_ONLY_CENTRAL
编译 feature_demo,分别烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),从灯效上看,和双方均不使能 SMP 一样。但通过抓包可以看到,这里多了建立连接后 Central 发送 Pairing Request,但由于 Peripheral 不支持 SMP,Peripheral 回复 Pairing Failed,reason 在 SDK 中的定义为 PAIRING_FAIL_REASON_PAIRING_NOT_SUPPORTED,抓包如下图:
说明:
如果用户希望 Central 建立连接等待 Peripheral 发出 Security Request 后再发 Pairing Request,应在初始化代码中添加(默认注释掉):
blc_smp_configPairingRequestSending(PairReq_SEND_upon_SecReq, PairReq_SEND_upon_SecReq);
该 API 仅针对 Central(只有 Central 会发 Pairing Request),因此不存在后缀为 _periphr/_central 的相应 API。
Legacy Just Works
Central 和 Peripheral 使能 SMP 功能,以 Legacy Just Works 方式配对的演示。该演示需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_LEGACY_JUST_WORKS
编译 feature_demo,分别烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色、白色、蓝色灯,Peripheral 亮红色、白色、蓝色灯,代表二者 Pair 成功,抓包如下:
此时按 Central 或 Peripheral 开发板上的 SW1 按键给开发板重新上电,由于 Central 在扫描到已经绑定过的 Peripheral 设备会自动回连(参考演示代码中 central_auto_connect 变量),可以看到灯灭之后又立刻亮了起来,但这一次蓝色灯没有亮,因为回连不走 Pair 流程,直接通过 LTK 走加密流程。
说明:
有时候,我们需要不停地跑配对流程,而不希望 Central 和 Peripheral 重新上电后回连跳过 Pair 流程,只需要将 Central 或 Peripheral 任意一方的 Bonding Flag 置位为 0 即可,这里给出两种方法(需要注意,由于此前的测试已经 Bonding 过了,所以使用下面方法演示时,要先擦除 SMP Storage 区域):
- 方法1:在初始化配置 Security Parameters 的同时将 Bonding Flag 置位为 0。以 Peripheral 为例(默认注释掉):
blc_smp_setSecurityParameters_periphr(Non_Bondable_Mode, 0, LE_Legacy_Pairing, 0, 0, IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
这里设置了 Peripheral 端的 bonding_mode 为 Non-Bondable mode,其他设置为 Just Works 默认配置(Central 端同理)。
调用该 API 之后,在 Pair 时,会看到 Peripheral 回复的 Pairing Response 中的 Bonding Flags=0,这样 Central 和 Peripheral 就都不会做 Bonding,也就是不会将配对信息存在 Flash 中。当 Central 或 Peripheral 设备重启之后,他们仍然是“全新的”,并不“认识”彼此:
如果不调用该 API,默认会使用在 blc_gap_init() 中对 Peripheral 和 Central 设置 SecurityParameters 的初始值:
mode_level = Unauthenticated_Pairing_with_Encryption;
bond_mode = Bondable_Mode;
MITM_en = 0;
method = LE_Legacy_Pairing;
OOB_en = 0;
keyPress_en = 0;
ioCapablility = IO_CAPABILITY_NO_INPUT_NO_OUTPUT;
ecdh_debug_mode = non_debug_mode;
passKeyEntryDftTK = 0;
- 方法2:在初始化配置 Security Parameters 的同时单独配置 Bonding Mode。以 Central 为例:
blc_smp_setBondingMode_central(Non_Bondable_Mode);
这里设置了 Central 端的 bonding_mode 为 Non-Bondable mode (Peripheral 端同理)。抓包可以看到效果和上述 方法1 相同:
Secure Connections Just Works
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections Just Works 方式配对的演示。该演示需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_SC_JUST_WORKS
编译 feature_demo,分别烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色、白色、蓝色灯,Peripheral 亮红色、白色、蓝色灯,代表二者 Pair 成功,抓包如下:
关于 Debug Mode 参考SMP章节,用户可以根据需求配置为如下三种:
Central | Peripheral | Description |
---|---|---|
disabled | enabled | 演示代码默认配置 |
enabled | disabled | 效果与演示代码类似 |
disabled | disabled | 抓包工具无法自动解析加密包 |
注意:
Peripheral debug mode enable + Central debug mode enable 的组合是不允许的,会出现 SMP Pairing Failed 事件:
Legacy Passkey Entry CDPI
Central 和 Peripheral 使能 SMP 功能,以 Legacy Passkey Entry 配对方式,通过 CDPI 方法实现的演示。CDPI 代表 Central(Initiator) Peripheral(Responder) Inputs,这里演示的 Input 是 Keyboard Input,使用该功能需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_CDPI
演示代码中 Initiator 默认设置 Passkey 为 123456,Responder 在收到 GAP_EVT_SMP_TK_REQUEST_PASSKEY 事件10s内,自动输入 123456 的 Passkey。参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_BY_DEFAULT
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色,Peripheral 亮红色,停留几秒后二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为等待 Peripheral 输入 Passkey):
代码中在初始化时使用 blc_smp_setDefaultPinCode/_periphr/_central() 来设置 Display 设备的默认 Passkey,这里设置为 123456。如果没有调用该语句设置默认 Passkey,系统会随机生成一个十进制 000000~999999 的 6 位 Passkey。
此后通过 Input 设备在 main_loop 中轮询 blc_smp_isWaitingToSetTK() 来获取当前是否在等待输入 Passkey,当获取到该需求后,通过 blc_smp_setTK_by_PasskeyEntry() 来设置 Passkey,应与 Display 设备设置的默认 Passkey 相等。需要注意,在没有获取到 blc_smp_isWaitingToSetTK() 的需求前调用blc_smp_setTK_by_PasskeyEntry() 设置的 Passkey 是无效的。
另外,在 app_host_event_callback() 中,当获取到 GAP_EVT_SMP_TK_REQUEST_PASSKEY GAP 事件时调用 blc_smp_setTK_by_PasskeyEntry() 也可以成功设置 Input 设备的 Passkey。
Legacy Passkey Entry CIPD
Central 和 Peripheral 使能 SMP 功能,以 Legacy Passkey Entry 配对方式,通过 CIPD 方法实现的演示。CIPD 与上一个演示 CDPI 类似,只是显示和输入的设备角色互换了,CIPD 代表 Central(Initiator) Inputs Peripheral(Responder) Displays。使用该功能需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_CIPD
演示代码中 Initiator 通过 UART 输入 Passkey ,Responder 通过底层随机生成 Passkey 并显示出来,更加贴合实际使用需求,参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_MANUALLY
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。给其中一个开发板(作为 Central)的 UART 口 PA3(Tx),PA4(Rx) 接到电脑串口上,配置波特率 115200 8N1。按 Central 开发板的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色灯,Peripheral 亮红色灯,此时 Peripheral 的 Log 输出 "TK Display",将输出的数填入到 Central 的 UART 串口,SDK 默认超时为 30s,须在该时间内将 Passkey 输入成功,可以看到 Central 的 UART 串口返回该值,Log 输出 "TK set" 后面跟着用户通过UART输入的值。SMP 流程继续,白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为等待 Central 输入 Passkey):
Legacy Passkey Entry Both Input
Central 和 Peripheral 使能 SMP 功能,以 Legacy Passkey Entry 配对方式,通过 Both Input 方法实现的演示。Both Input 的方式是由用户在两端输入相同的 Pincode 来实现的。使用该功能需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_BOTH_INPUT
演示代码中 Initiator 和 Responder 使用默认 Passkey 为 123456 的方式,参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_BY_DEFAULT
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿灯,Peripheral 亮红灯,停留几秒后二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为等待双方输入 Passkey):
Secure Connections Passkey Entry
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections Passkey Entry 方式配对的演示。与 Legacy 对应,Secure Connections 的 Passkey Entry 匹配方式也有 CDPI、CIPD、Both Input 三种实现方式。为了全面演示 Passkey Entry 的使用,演示代码中与 Legacy 交叉,给出 Secure Connections 下 Passkey Entry 的 CIPD 方式以 By_Default 方法输入 Passkey,CDPI 和 BothInput 方式用 Manually 方法输入 Passkey。
- 对于 CDPI,需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_CDPI
Passkey 的输入方式参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_MANUALLY
- 对于 CIPD,需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_CIPD
Passkey 的输入参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_BY_DEFAULT
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿灯,Peripheral 亮红灯。停留几秒后二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为等待 Central 输入 Passkey):
- 对于Both Input,需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_BOTH_INPUT
Passkey 的输入参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_MANUALLY
Secure Connections Numeric Comparison
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections Numeric Comparison 方式配对的演示。根据蓝牙协议,当使用 Secure Connections 且双方皆为 DisplayYesNo 或 KeyboardDisplay 功能时,会使用 Numeric Comparison 方式进行匹配。使用该功能需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_SC_NUMERIC_COMPARISON
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿灯,Peripheral 亮红灯,此时从双方的 Log 中看到 "TK display" 的值,二者应该相等。分别在 Central 和 Peripheral 开发板上按按键 SW3,代表发送 YES,可以看到二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为分别等待 Central 和 Peripheral 进行按键确认):
Legacy OOB
Central 和 Peripheral 使能 SMP 功能,以 Legacy OOB 方式配对的演示。根据蓝牙协议,在 Legacy 配对方式下,当双方均支持 OOB,拥有对方 OOB 数据时,会使用 Legacy OOB 方式进行匹配。使用该功能需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_LEGACY_OOB
这里选择用 Manually 方式通过 UART 输入 OOB data,参考定义:
#define PASSKEY_ENTRY_METHOD PASSKEY_ENTRY_MANUALLY
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。给两个开发板的 UART 口 PA3(Tx),PA4(Rx) 接到电脑串口上,配置波特率 115200 8N1。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色灯,Peripheral 亮红色灯。此时从双方的 Log 中看到 "OOB request",在 30s 内将自定义的 OOB data 分别填入到 Peripheral 和 Central 的 UART 串口。可以看到二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下:
可以看到这里的加密内容没有被解析,解析需要用 STK,Telink 不提供获取 STK 的接口和方法。但可以通过读 RAM 得到 LTK,这样重连用 LTK 加密后,将 LTK 加载到抓包器中:
就可以解析重连后的加密内容:
Secure Connections OOB
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections OOB 方式配对的演示,SDK 中暂时不支持。
Custom Pair
关闭 Central 的 SMP 功能时,Telink 提供了一种不通过 SMP 实现设备绑定(自动回连)的方法 —— Custom Pair,因为该功能替换了 SMP,所以演示将 Central 和 Peripheral 的 SMP 都关闭,使用该功能需在 feature_smp/app_config.h 中定义:
#define SMP_TEST_MODE SMP_TEST_CUSTOM_PAIR
编译 feature_demo,分别擦除 Flash 后烧录 B91_ble_multi_conn_sdk/build/B91/feature_demo/output/feature_demo.bin 到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿灯,Peripheral 亮红灯,代表二者连接成功。此时重启 Central 或 Peripheral,可以看到另一端的灯熄灭之后,两端的灯很快点亮,代表回连成功。
异常处理
按按键没有响应
(1) 硬件原因:可能是由于按键的跳线帽没有接正确,请参考下图:
(2) 环境原因:可能由于开发板烧录后没有 Reset 使程序运行起来。
LED 灯没有点亮
(1) 硬件原因:可能由于LED的跳线帽没有接正确,请参考下图:
feature_ota
- 功能:BLE OTA 功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_ota 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_OTA
来激活这部分代码,做 OTA 的演示。
编译固件后,分别烧录到两个开发板中,并给其中一个开发板的 Flash 0x80000 位置烧录 OTA 固件(这里使用以上同一个编译固件)。重新给两个开发板上电后,连续按 Central 的按键 SW2 或 SW3 五次,触发 OTA 测试模式,可以看到蓝色灯和绿色灯三次慢闪,代表切换 OTA 测试模式成功。
按 Central 的按键 SW2 开始 OTA,可以看到两个开发板上的蓝色灯点亮,代表正在 OTA,抓包可以看到 Central 不停向 Peripheral 发送 WriteCmd:
升级完成,Peripheral 发送给 Central 一个 Notify 指令,代表升级成功,之后发送 Terminate 指令,断开连接。由于二者进行 SMP 连接, Central 存有 Peripheral 的信息,立刻又重新连上:
feature_whitelist
- 功能:BLE 白名单 功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_whitelist 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_WHITELIST
来激活这部分代码,做白名单功能的演示。
由于 SDK 中待连接 Central 白名单和待连接 Peripheral 白名单共用同一个白名单列表,所以,这里分别对连接 Central 白名单设备和连接 Peripheral 白名单设备进行演示。
先编译一个使能白名单功能但不使用白名单的主从一体设备固件(为方便后文引用,称为NoWhiteList),将初始化代码中的
blc_ll_setAdvParam(ADV_INTERVAL_30MS, ADV_INTERVAL_30MS, ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC, 0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_ALLOW_SCAN_WL_ALLOW_CONN_WL);
修改为
blc_ll_setAdvParam(ADV_INTERVAL_30MS, ADV_INTERVAL_30MS, ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC, 0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_ALLOW_SCAN_ANY_ALLOW_CONN_ANY);
再将
blc_ll_setScanParameter(SCAN_TYPE_PASSIVE, SCAN_INTERVAL_100MS, SCAN_WINDOW_100MS, OWN_ADDRESS_PUBLIC, SCAN_FP_ALLOW_ADV_WL);
修改为
blc_ll_setScanParameter(SCAN_TYPE_PASSIVE, SCAN_INTERVAL_100MS, SCAN_WINDOW_100MS, OWN_ADDRESS_PUBLIC, SCAN_FP_ALLOW_ADV_ANY);
编译,NoWhiteList.bin。
Central 设置 扫描 Peripheral 白名单
连接 Peripheral 白名单设备的演示,就是将要连接的 Peripheral 的 MAC 地址添加到白名单中,作为 Central 的Telink 设备通过对扫描到的广播进行地址过滤的方式决定是否进行连接。
基于 NoWhiteList 代码,还原扫描参数配置语句:
blc_ll_setScanParameter(SCAN_TYPE_PASSIVE, SCAN_INTERVAL_100MS, SCAN_WINDOW_100MS, OWN_ADDRESS_PUBLIC, SCAN_FP_ALLOW_ADV_ANY);
为
blc_ll_setScanParameter(SCAN_TYPE_PASSIVE, SCAN_INTERVAL_100MS, SCAN_WINDOW_100MS, OWN_ADDRESS_PUBLIC, SCAN_FP_ALLOW_ADV_WL);
编译,得到 Central 的固件,烧录到其中一块开发板上作为 Central。另一块开发板烧录 NoWhiteList.bin,并修改 MAC 地址为白名单地址33:88:99:99:99:99:
烧录完成后,通过按Central的SW4启动连接,可以看到Central和Peripheral成功连接,抓包如下:
如果此时 Peripheral 的 MAC 地址不是33:88:99:99:99:99,按 Central 的SW4 按键,是没有响应的,抓包中不会看到建立连接及建立连接之后的包。
Peripheral 设置 Central 连接白名单
连接 Central 白名单设备的演示,就是将要连接的 Central 的 MAC 地址添加到白名单中,作为 Peripheral 的 Telink 设备通过对扫描包或连接包的过滤来决定是否进行响应。
基于 NoWhiteList 代码,还原扫描参数配置语句:
blc_ll_setAdvParam(ADV_INTERVAL_30MS, ADV_INTERVAL_30MS, ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC, 0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_ALLOW_SCAN_ANY_ALLOW_CONN_ANY);
为
blc_ll_setAdvParam(ADV_INTERVAL_30MS, ADV_INTERVAL_30MS, ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC, 0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_ALLOW_SCAN_WL_ALLOW_CONN_WL);
编译,得到 Peripheral 的固件,烧录到其中一块开发板上作为 Peripheral。另一块开发板烧录 NoWhiteList.bin,并修改 MAC 地址为白名单地址 33:88:99:99:99:99。 烧录完成后,通过按 Central 的 SW4启动连接,可以看到 Central 和Peripheral 成功连接,抓包如下:
如果此时 Central 的 MAC 地址不是 33:88:99:99:99:99,按 Maste 的SW4 按键,会看到 Central 发出了Connect Request,Peripheral 却不会响应,以至于 Central 一直尝试连接:
feature_soft_timer
- 功能:Soft Timer (软件定时器) 功能演示。并作为 IO Debug 方法的演示参考。
- 主要硬件: B91 开发板,逻辑分析仪 或 示波器
以 B91 为例,应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_test/feature_soft_timer 下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_SOFT_TIMER
来激活这部分代码,并在 app_config.h 中使能 IO Debug:
#define DEBUG_GPIO_ENABLE 1
来做 Soft Timer 和 IO Debug 的功能演示。
将开发板上 PE1 接出,作为 PM IO,用于观察休眠唤醒。将 PA3、PB0、PB2、PE0 接出,分别作为Debug IO 4、5、6、7 的信号显示,参考宏定义 DEBUG_GPIO_ENABLE。
编译固件后,烧录到开发板中,上电,逻辑分析仪抓取 Debug IOs 信号,可以看到逻辑分析仪上抓到的5个IO效果如下:
从图上可以看出广播间隔为 50ms(左右,底层会有一点动态调整),PA3 每隔 23ms toggle 一次,PB0 以 7ms 和 17ms 交替间隔进行 toggle,PB2 每隔 13ms toggle 一次,PE0 每隔 100ms toggle 一次,参考宏定义 BLT_SOFTWARE_TIMER_ENABLE 所使能的代码。可以看到 Soft Timer 事件触发时,设备都有被提前唤醒,也就是说 Soft Timer 设置的事件是不受休眠影响的。
PAwR
- 功能:BLE PAwR功能演示
- 主要硬件:B91开发版 x 2
一块B91开发版作为广播者(broadcaster),另一块作为观察者(observer)。 以 B91 为例,broadcaster端应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_pawr_adv下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_PAWR_ADV
observer端应用层代码在 B91_ble_multi_conn_sdk/vendor/feature_pawr_sync下,需要修改 B91_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
#define FEATURE_TEST_MODE TEST_PAWR_SYNC
TEST_PAWR_ADV代码自定义
地址过滤
在app_le_ext_adv_report_event_handle()设置需要连接的observer端mac地址,如果需要设置多个,需同步更新mac数组和判断逻辑。
......
u8 address[6] = {0x99, 0x99, 0x99, 0x99, 0x99, 0x99};
if(central_auto_connect || (user_manual_pairing && (0 || memcmp(pExtAdvInfo->address, address, 6) == 0)))
......
PAwR参数设置
在app_periodic_adv_test()调用blc_ll_setPeriodicAdvParam_v2()
blc_ll_setPeriodicAdvParam_v2(adv_handle_t adv_handle,
u16 advInter_min,
u16 advInter_max,
perd_adv_prop_t property,
u8 numSubevents,
u8 subeventInterval,
u8 responseSlotDelay,
u8 responseSlotSpace,
u8 numResponseSlots);
- advInter_min:周期广播间隔最小值
- advInter_max:周期广播间隔最大值
- numSubevents:subevent数量
- subeventInterval:一个subevent的时间长度
- responseSlotDelay:subevent中发送完REQ到开始接收RSP的延迟时间
- responseSlotSpace:响应槽的时间长度
- numResponseSlots:一个subevent响应槽的数量
AUX_SYNC_SUBEVENT_IND包内容
设置包内容的调用接口为blc_ll_setPeriodicAdvSubeventData()。
当前程序逻辑在app_le_periodic_advertising_subevent_data_request_event_handle()进行数据设置。
当controler buffer中数据发送完成之后上报事件,用户层可根据需求在该事件回调函数中设置AUX_SYNC_SUBEVENT_IND包内容。
注意:当前AUX_SYNC_SUBEVENT_IND内的数据无具体格式,用户可参考ESL格式进行封装
AUX_CONNECT_REQ请求发送
PAwR包含了AUX_SYNC_SUBEVENT_IND和AUX_CONNECT_REQ两种请求包,在broadcaster和observer完成同步之后(处于断连状态),broadcaster端可发送AUX_CONNECT_REQ请求连接。
该请求通过按键SW2或SW3触发,调用blc_ll_extended_createConnection_v2()。
在app_ui.c中定义:
dev_info_t ESL_deviceDB[4] = {
{.set = 0, .type = BLE_ADDR_PUBLIC, .address = { 0x99, 0x99, 0x99, 0x99, 0x99, 0x99 }, },
{.set = 1, .type = BLE_ADDR_PUBLIC, .address = { 0x01, 0x99, 0x99, 0x99, 0x99, 0x99 }, },
{.set = 2, .type = BLE_ADDR_PUBLIC, .address = { 0x02, 0x99, 0x99, 0x99, 0x99, 0x99 }, },
{.set = 3, .type = BLE_ADDR_PUBLIC, .address = { 0x03, 0x99, 0x99, 0x99, 0x99, 0x99 }, },
};
举例说明,若address = { 0x99, 0x99, 0x99, 0x99, 0x99, 0x99 }的observer设备在监听第2个subevent,则对应的.set应该为2,否则该observer端不会收到连接请求。
TEST_PAWR_SYNC端代码自定义
rsp_slotIdx设置
该参数表示收到AUX_SYNC_SUBEVENT_IND包后在第几个slot中发送rsp,该参数在ESL中动态确定,该demo中直接进行指定。
在app_le_periodic_adv_report_event_handle()中进行设置:
u8 rsp_slotIdx = 2;
监听subevent设置
该参数设置observer需要监听的subevent,可以为多个。
app_le_periodic_adv_sync_transfer_received_event_handle()中进行设置:
u8 sync_subevent_num = 1;
u8 sync_subevent[1] = {2}; //now sync the subevent 2
observer响应数据设置
#define RSP_DATA_LEN 16
u8 rsp_dataBuff[RSP_DATA_LEN] = {0x00, 0x01, 0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f};
int app_le_periodic_adv_report_event_handle(u8 *p){
......
//因broadcaster端数据无具体格式,此处为用户层自定义的判断逻辑,用户可自行定义,与broadcaster端一致即可
if(pPdaReport->data[3] != 0x11 && pPdaReport->data[4] != 0x02 ){
return 1;
}
......
blc_hci_le_setPAwRsync_rspData( pPdaReport->syncHandle,
req_pawrEvtCnt, req_subevent,
rsp_subevent, rsp_slotIdx,
RSP_DATA_LEN, rsp_dataBuff);
......
}
操作流程及现象
1.上电后,首先长按broadcaster端SW4按键进行pair操作,当与observer端成功建立传统的acl连接后,broadcaster端红灯会点亮。
2.按下broadcaster端SW5按键发送PAST指令(broadcaster端与observer同步的一种方式),PAST包中包含了observer端同步的参数,发送成功后,broadcaster端绿灯带点亮。
3.observer端正常接收到PAST包,完成同步后,observer开始监听设置的subevent并进行响应。observer端绿灯会随着每次响应进行toggle。
4.完成同步后,可通过observer端的SW5进行解配对操作,该操作不会影响broadcaster和observer端的同步状态,此时broadcaster端红灯熄灭,表明连接断开。
5.按下broadcaster端SW2或SW3可发送AUX_CONNECT_REQ,与observer建立连接,observer端一旦通过该方式建立方式,会主动取消同步状态,如果需要再次同步,需要按下broadcaster端SW5按键发送PAST指令。
上述过程均伴有相关log打印,用户可根据log进一步了解PAwR执行过程。
步骤1-4完成PAST指令和AUX_SYNC_SUBEVENT_IND交互逻辑的验证,抓包过程如下图所示,每10个subevent请求收到一个RSP。
步骤5完成AUX_CONNECT_REQ交互逻辑,抓包如下图所示,收到AUX_CONNECT_REQ后马上进行响应(与ACL连接一致)。
其他模块
24MHz晶体外部电容
参考下图中的24MHz晶体匹配电容的位置C1/C4。
SDK默认使用B91m内部电容(即ana_8a<5:0>对应的cap)作为24MHz晶振的匹配电容,此时C1/C4不用焊接电容。使用该方案的优势是:在Telink治具上可以测量并调节该电容,使得最终应用产品的频点值达到最佳。
如果需要使用外部焊接电容作为24M晶振的匹配电容(C1/C4焊接电容),则只要在main函数开始的地方(一定要在sys_init函数之后,blc_app_loadCustomizedParameters()之前)调用下面API即可:
static inline void blc_app_setExternalCrystalCapEnable(u8 en)
{
blt_miscParam.ext_cap_en = en;
analog_write(0x8a, analog_read(0x8a) | 0x80);//disable internal cap
}
32KHz时钟源选择
SDK默认使用MCU内部32kHz RC振荡电路,简称32kHz RC。32kHz RC的误差比较大,所以对于suspend或者deep retention时间较长的应用,其时间准确性会差一些。目前32kHz RC默认支持的最大长连接不能超过3s,一旦超过这个时间,ble_timing会出错,造成收包时间点不准确,容易出现收发包retry,功耗增大,甚至出现断连。
如果用户需要实现更低的连接功耗,包括低功耗睡眠情况下时钟计时更加准确,可以选择使用外部32k晶体,简称32k Pad,目前SDK支持该模式。
用户只需要在main函数开始的地方(一定要在sys_init函数之前)调用下面两个API中的一个:
void blc_pm_select_internal_32k_crystal(void);
void blc_pm_select_external_32k_crystal(void);
PA
如果需要使用RF PA,B91系列请参考drivers/B91/ext_driver/software_pa.h,B92系列请参考drivers/B92/ext_driver/software_pa.h。
首先打开下面的宏,默认是关闭的。
#ifndef PA_ENABLE
#define PA_ENABLE 0
#endif
在系统初始化的时候,调用PA的初始化。
void rf_pa_init(void);
参考代码实现,该初始化里面,将PA_TXEN_PIN和PA_RXEN_PIN设为GPIO输出模式,初始状态为输出0。需要user定义TX和RX的PA对应的GPIO:
#ifndef PA_TXEN_PIN
#define PA_TXEN_PIN GPIO_PB2
#endif
#ifndef PA_RXEN_PIN
#define PA_RXEN_PIN GPIO_PB3
#endif
另外将void (*rf_pa_callback_t)(int type)注册为PA的回调处理函数,实际它处理了下面3种PA状态:PA关、开TX PA、开RX PA。
#define PA_TYPE_OFF 0
#define PA_TYPE_TX_ON 1
#define PA_TYPE_RX_ON 2
User只需要调用上面的rf_pa_init,app_rf_pa_handler被注册到底层的回调,BLE在各种状态时,都会自动调用app_rf_pa_handler的处理。
PhyTest
PhyTest即PHY test,是指对BLE controller RF性能的测试。
详情请参照《Core_v5.4》(Vol 4/Part E/7.8.28~7.8.30)和《Core_v5.4》(Vol 6/Part F “Direct Test Mode” )。
PhyTest API
PhyTest的源码被封装在library文件中,提供相关API供user使用,请参考stack/ble/controller/phy/phy_test.h文件。
void blc_phy_initPhyTest_module(void);
ble_sts_t blc_phy_setPhyTestEnable (u8 en);
bool blc_phy_isPhyTestEnable(void);
int blc_phytest_cmd_handler (u8 *p, int n);
首先调用blc_phy_initPhyTest_module初始化PhyTest模块,应用层触发PhyTest后,底层自动调用blc_phy_setPhyTestEnable(1)开启PhyTest模式。
PhyTest是一个特殊的模式,和正常的BLE功能是互斥的,一旦进入PhyTest 模式,广播和连接都不可用。所以运行正常BLE功能时不能触发PhyTest。
PhyTest结束后,要么直接重新上电,要么调用blc_phy_setPhyTestEnable(0),此时MCU会自动reboot。 使用blc_phy_isPhyTestEnable可判断当前PhyTest是否被触发。
PhyTest demo
B91m SDK demo “feature_test”的app_config.h中,测试模式修改为“TEST_BLE_PHY”,如下所示:
#define FEATURE_TEST_MODE TEST_BLE_PHY
根据物理接口和测试命令格式的不同,PhyTest可分为两种测试模式,如下所示。
#define PHYTEST_MODE_THROUGH_2_WIRE_UART 1 //Direct Test Mode through a 2-wire UART interface
#define PHYTEST_MODE_OVER_HCI_WITH_UART 2 //Direct Test Mode over HCI(UART hardware interface)
选择PhyTest的测试模式,如下定义为uart两线模式:
#define BLE_PHYTEST_MODE PHYTEST_MODE_THROUGH_2_WIRE_UART
如下定义为HCI模式UART接口(硬件接口还是uart)phytest:
#define BLE_PHYTEST_MODE PHYTEST_MODE_OVER_HCI_WITH_UART
用户需设置发送和接收回调函数:
blc_register_hci_handler (app_phyTest_rxUartCb, app_phyTest_txUartCb);
编译feature_test生成的bin文件直接测试可以通过。user可研究一下code的实现,掌握相关接口的使用。
JTAG使用
为了能够使用JTAG模块,需要在使用前确保满足以下几点条件:
- JTAG的四个GPIO需要设置成使能模式。
- 如果芯片处于低功耗模式,那么使用JTAG前芯片必须退出低功耗模式。
- 如果JTAG模式因为FLASH中有程序而不能正常使用,需要在使用前用Telink BDT工具擦除FLASH。
Diagnostic Report
(1) 在Target Manager里选择Diagnostic report。
(2) 选择V5 core,不要勾选SDP (2wires),我们的JTAG暂时不支持2线模式,address输入0。
(3) 点击OK,会生成一个Diagnostic report。
Target Configuration
(1) 右击工程文件夹选择“Target Configuration”。
(2) 确保没有勾选“SDP (2wires)”。
Flash Programming
右击工程文件夹选择“Flash Burner”。
(1) 选择IDE安装目录下的SPI_burn.exe。
(2) 选择需要下载的bin文件。
(3) 勾选“Target management”。
(4) 不要勾选“Target Burn”。
(5) 勾选“Verification”,如果需要在烧录前擦除FLASH,可以勾选“erase all”。
(6) 点击“Burn”下载bin文件,如果出现“Verify sucess”,此时表示烧录成功。
版本函数
用户可以通过以下函数来获取当前SDK版本信息。
unsigned char blc_get_sdk_version(unsigned char *pbuf,unsigned char number);
参数pbuf是指向存储版本信息位置的指针,参数number是版本信息的长度,应该在5-16之间,目前只用了5个字节表示版本信息。函数的返回值为0表示成功,为1表示失败。例如,如果调用blc_get_sdk_version函数后得到的数据为{4,1,0,0,1},它表示当前SDK版本为4.1.0.0 patch 1。
调试方法
该章节介绍几种在开发过程中常用的调试方法。
GPIO 模拟 UART 打印
为方便 B91m 用户调试,Telink B91m BLE Multiple Connection SDK 提供 GPIO 模拟 UART 串口输出调试信息的一种实现,该方法仅作为一种参考,并非官方推荐的调试信息输出方法。将例程中 app_config.h 中的宏 UART_PRINT_DEBUG_ENABLE 定义为 1 后,就可以在代码中直接使用与 C 语言语法规则一致的 printf 接口进行串口输出。各个应用例程中都有模拟 UART 的 GPIO 的默认配置,用户可根据需求进行更改:
一般而言,只修改波特率与其他每行定义中的 IO 名(图中所有PD7)。
注意:
- 波特率目前最高支持1M。
- 由于GPIO模拟UART串口的打印会被中断打断,致使模拟的UART的时序不准确,因此在实际使用过程中时而会有打印乱码的情况发生。
- 由于GPIO模拟UART串口的打印会占用CPU,所以不建议在中断中加入打印,会影响对时序要求较高的中断任务。
BDT 工具读取全局变量的值
Telink 官方提供的 BDT 工具,不仅可以用来烧录固件,也可以进行一些线上 Debug,其中 Tdebug 标签页下可以读到代码中的全局变量的值。用户可以根据需求在代码中添加全局变量,在 Tdebug 中读取。具体请参考 《BDT 用户指南》的 Debug 章节。
注意:
- 芯片处于休眠状态时,读出来的值为全 0。
- 该功能依赖于生成的 list 文件(默认 B91生成objdump.txt,B92是xxx.lst),所以用户在向官方提供调试固件时,也应尽可能提供list文件,以便 Telink 工程师通过 list 文件读取底层一些变量的值。
BDT 工具的 Memory Access 功能
Telink 官方提供的 BDT 工具,还可以通过 Memory Access 功能,读取 Flash、内存等空间指定位置的内容,也可以写入数据。需要在读取前保证SWS处于接通状态。具体请参考 《BDT 用户指南》的 Debug 章节。
BDT 工具读 PC 指针
Telink 官方提供的 BDT 工具,可以用来读取 B92 的 PC(Program Counter) 指针,这在分析死机问题时,非常有帮助。具体请参考 《BDT 用户指南》的 Debug 章节。
Debug IO
可以看到在各个例程的 app_config.h 中都有用宏 DEBUG_GPIO_ENABLE 括起来的一段 IO 相关的定义:
该功能默认不开启,是给 Telink 工程师内部使用的统一的 Debug IO 定义,通过逻辑分析仪或者示波器抓 IO 的波形进行调试。但用户可以使用类似的方式在应用层添加自己的 Debug IO,参考 common/default_config.h。
注意:
- 官方 release 的 SDK中,Stack 中的 Debug IO 调试信息以禁用的状态被包含在了库文件中。所以即使用户在应用层定义 DEBUG_GPIO_ENABLE 为 1,也不会使能 Stack 中的 Debug IO 调试信息。
- 如果使能 Debug IO,虽然 Stack 中的 Debug IO 不会起效,但是应用层的 Debug IO 会起效,比如 Telink B91m BLE Multiple Connection SDK 中 CHN14 所代表的 rf_irq_handler,CHN15 所代表的 stimer_irq_handler。
USB my_dump_str_data
在 Telink B91m BLE Multiple Connection SDK 中可以看到有多处 my_dump_str_data 这个 API 的调用,该功能是 B91 上借助 USB 接口将调试信息输出的一种实现,不是官方推荐的调试信息输出方法,仅作为一种参考,目的是解决在中断中不能通过 GPIO 模拟 UART 进行输出的问题。该功能默认不开启,需要定义 DUMP_STR_EN 为 1 才能开启。
附录
附录1:crc16 算法
unsigned shortcrc16 (unsigned char *pD, int len)
{
static unsigned short poly[2]={0, 0xa001};
unsigned short crc = 0xffff;
unsigned char ds;
int i,j;
for(j=len; j>0; j--)
{
unsigned char ds = *pD++;
for(i=0; i<8; i++)
{
crc = (crc >> 1) ^ poly[(crc ^ ds ) & 1];
ds = ds >> 1;
}
}
return crc;
}
附录2:检查stack是否溢出
原理
在cstartup_flash.S中将stack的所有内容写为0x55,在程序运行时会将使用过的栈的数据改写为其他值。通过查看栈被改写的size大小,可以判断栈的使用情况,并可以判断栈是否发生溢出。
方法
(1)打开boot/cstartup_flash.S,将_FILL_STK下的内容使能。
_FILL_STK:
#if 1
lui t0, 0x55555
addi t0, t0, 0x555
la t2, _BSS_VMA_END
la t3, _STACK_TOP
_FILL_STK_BEGIN:
bleu t3, t2, _MAIN_FUNC
sw t0, 0(t2)
addi t2, t2, 4
j _FILL_STK_BEGIN
#endif
(2)根据此手册中2.1.2.1章节的SRAM空间分配确定stack的栈底地址。
(3)使用BDT软件将程序的.bin文件下载完成后,点击Reset使程序运行。然后将slave与master进行连接配对。
(4)配对完成后,在BDT中使用“Tool -> Memory Access”来读取RAM中的数据。示例图如下。
(5)在键盘上按“tab”键可生成一个Read.bin文件将数据保存。文件地址为:BDT安装路径 -> config -> user,Read.bin。
(6)使用十六进制查看软件打开Read.bin,如果没有连续的0x55,说明stack溢出到了.bss段。 或者,更准确的方法为,在工程生成的.lst文件中找到所分配的栈顶地址,如下图所示。然后在Read.bin中查看此地址是否被改写为其他内容,若被改写则说明栈溢出。