泰凌 tl_ble_sdk 开发手册
SDK介绍
tl_ble_sdk 提供 BLE 多连接应用的参考代码,用户可以在此基础上开发自己的应用程序。多连接指多个(大于等于1)Central 或 Peripheral 角色共存,比如自身同时作为 4 个 Central 和 3 个 Peripheral(简称C4P3)。目前协议栈支持到最多4个Central,和最多4个Peripheral,不支持角色自由转化。
从 tl_ble_sdk V4.0.4.4 版本开始,SDK 与 Handbook 同时 Release,该版 Handbook 的内容对应 tl_ble_sdk V4.0.4.4。
从 tl_ble_sdk V4.0.4.2 版本开始,发布 SDK 从官网 Wiki 移到了Gitee和GitHub上,代码下载或Clone不需要登录账号。为了及时收到SDK的更新,建议登录后,关注该仓库。
适用IC
当前 tl_ble_sdk 适用 B91(TLSR921x 与 TLSR951x)、B92(TLSR922x 与 TLSR952x)、TL321X、TL721X、TL322x 系列的MCU。
软件组织架构
在 Telink IoT Studio 中导入 tl_ble_sdk 后,显示的文件组织结构如下图所示(以 B91 为例)。顶层文件夹有8个:algorithm,application,boot,common,drivers,proj_lib,stack,vendor。
algorithm: 提供一些通用的算法,如aes_ccm。大多数算法对应的C文件被封装在库文件中,只留对应的头文件。
application: 提供一些通用的应用处理程序,如usb,keyboard等。
boot: 提供 MCU 的 software bootloader和链接脚本.link文件。cstartup_
common: 提供一些通用的跨平台定义,如assert, BIT(x)等。
drivers: 提供MCU外设驱动程序,如Clock、Flash、I2C、USB、GPIO、UART等,与 tl_platform_sdk 的 release 版本对应,可参考 doc/tl_platform_sdk_Release_Note.md。
proj_lib: 存放SDK运行所必需的库文件。BLE协议栈、RF驱动、PM驱动等文件,被封装在库文件中。不同的芯片对应使用不同的库文件。
stack: 存放BLE协议栈相关的头文件。源文件被编译到库文件里面,对于用户不可见。
vendor: 用于存放示例代码,或者用户的应用层代码。
Vendor
tl_ble_sdk 提供了3个基础demo:
-
acl_peripheral_demo: 广播,自身作为 Peripheral 被其他 Central 连接的参考代码,默认配置为 C0P4,使能 SMP,deep retention 休眠。
-
acl_central_demo: 扫描,自身作为 Central 与其他 Peripheral 建立连接的参考代码,默认配置为 C4P0,使能 SMP,没有配置休眠。
-
acl_connection_demo: 同时广播和扫描,自身可同时作为 Peripheral 或 Central 与其他设备连接的参考代码,默认配置为 C4P4,使能 SMP,Suspend 休眠。
feature_test/ 下是各个功能的参考代码,使用方式请参考章节 Feature Demo。
common/ 下是应用层通用模块,如
-
boards/: 该文件夹下是该SDK所支持适配的所有开发板通用配置。
-
battery_check.c/.h: 提供了低供电保护的处理方案,详情请参考章节 低电检测。
-
ble_flash.h: 声明了Flash相关接口、以及定义,如Flash Map。
-
blt_soft_timer.c/.h: 提供了软件定时器的实现方案。
-
default_config.h: 对应用层配置宏做了默认的定义,用户可以在app_config.h中做定义,覆盖默认定义。
-
device_manage.c/.h: 连接设备信息的管理(例如:connection handle, attribute handle, BLE device address, address type等)。
-
simple_sdp.c/.h: 该文件提供了Central role简单的SDP(Service Discovery Protocol)实现方案。
-
tlkapi_debug.c/.h: 提供了Debug日志的接口实现。
下面以acl_connection_demo为例来讲解demo文件结构,文件构成如下图所示:
main.c
main.c 文件中包含 main 函数和中断处理函数。
main函数是程序执行的入口,包含了系统正常工作所需的配置,建议用户不要对它进行任何修改。其中 tl_ble_sdk 支持主频(CCLK)最低为 32MHz。
中断处理函数是系统触发中断时候的入口函数。
app_config.h
这是用户配置文件,用于对整个系统的相关参数(例如:BLE参数,GPIO配置,低功耗使能/失能,加密使能/失能 等)进行配置。
下文介绍各个模块时会对app_config.h中的各个参数的含义进行详细说明。
Application Files
app.c/.h: 用户主文件,用于完成BLE系统初始化、数据处理、低功耗处理等。
app_att.c/.h: 提供了GATT service表和profile文件,GATT service表中已提供了标准的GATT服务、标准的GAP服务、标准的HID服务以及一些私有服务等。用户可以参考这些添加自己的service和profile。
app_ui.c/.h: 主要提供按键处理接口和逻辑。
app_buffer.c/.h: 该文件用于定义stack各层使用的buffer,例如:LinkLayer TX & RX buffer、L2CAP layer MTU TX & RX buffer、HCI TX & RX buffer等。
app_freertos.c/.h: 当使能FREERTOS_ENABLE宏时,将会调用该文件内的接口,运行FreeRTOS操作系统。
BLE Stack Entry
BLE 中断处理入口函数是 blc_sdk_irq_handler()。
BLE 逻辑和数据处理入口函数是 blc_sdk_main_loop (),它负责处理BLE协议栈相关的数据处理和事件上报。
版本号
在旧版本的 tl_ble_sdk 中,使用sdk_version.txt对版本号进行记录。该方法已经废除。
用户可以通过以下函数来获取当前SDK版本信息,该API在V4.0.4.4版本中功能进行了调整。
unsigned char blc_get_sdk_version(unsigned char *pbuf,unsigned char number);
参数pbuf是指向存储版本信息数组的指针,参数number是该数组的长度,返回值为版本信息字符串实际需要的长度。如果返回值为0,代表传入的数组长度不够,需要扩大。
使用时,用户需先定义一个大数组,用以获取版本信息,将该数组及其大小作为参数传入函数,之后进行返回值和字符串的打印,参考代码如下:
u8 sdk_ver[180] = {0};
u8 sdk_ver_len = blc_get_sdk_version(sdk_ver, 180);
tlkapi_printf(1, "Version Info[%d]:%s\n", sdk_ver_len, sdk_ver);
运行后,打印内容类似 "Version Info[95]:V4.0.4.4_P0001 C0.0 Develop 105b2c862 Thu Jun 19 21:09:30 2025 +0800 Dirty 2025-06-19 23:29:58",则实际版本信息所需的字符串数组大小为 95 bytes,代码可以修改为:
u8 sdk_ver[95] = {0};
u8 sdk_ver_len = blc_get_sdk_version(sdk_ver, 95);
tlkapi_printf(1, "Version Info[%d]:%s\n", sdk_ver_len, sdk_ver);
MCU基础模块
MCU地址空间
Flash
各款MCU所支持的 Flash 大小,请参考对应的Datasheet。
程序将被烧录在Flash中,起始位置为0x0。
Flash 读、写的基本单位为 byte,用户通过调用 flash_write_page()/flash_read_page(),实现读写操作。
说明
- 受 Flash 设计的限制,Flash 读、写驱动层面的基本单位为 page(256 bytes),所以这里的接口名称为xx_page。
Flash 擦除的基本单位为 sector(4K bytes),通过调用 flash_erase_sector() 来实现整个sector的擦除。
注意
- 写之前必须要先擦除,Flash里面一个page为256 byte,flash_write_page函数支持跨page的写操作。
tl_ble_sdk 在初始化时通过调用 blc_readFlashSize_autoConfigCustomFlashSector() 读取Flash的MID,得到Flash大小,进行底层Flash Map的自动配置,可参考下图及vendor/common/ble_flash.h的定义(注意替换下文中的[Flash Size]):
- CFG_ADR_MAC_[Flash Size]_FLASH:BLE MAC地址保存的位置,使用方式参考 blc_initMacAddress()。
- 对于B91,如果Flash中这段内容为全0xFF,会随机生成一段以Telink Company ID开头的 MAC地址。
- 对于B92、TL321x、TL721x、TL322x,如果Flash中这段内容为全0xFF,会从eFuse/OTP中对应位置读取内置的MAC地址。
- CFG_ADR_CALIBRATION_[Flash Size]_FLASH:RF 校准参数保存起始位置,相关代码参考 user_calib_freq_offset()。
- FLASH_ADR_SMP_PAIRING_[Flash Size]_FLASH:SMP配对信息存储起始位置,占16KB,其存储结构参考stack/ble/host/smp/smp_storage.h 中的 smp_param_save_t,细节请参考章节SMP。
- FLASH_SDP_ATT_ADDRESS_[Flash Size]_FLASH:作为Client,做了simple SDP后,将对端Server的ATT信息存储在Flash中的位置。
注意
- 以上定义的区域都是占用了整个sector,不允许用户使用这些sector做其他用途。
另外,如果用户在 B92、TL321x 或 TL721x 上配置了Secure Boot,一块固定的Flash区域将用于Secure Boot的配置,见上图。用户如果不需要Secure Boot,可以自定义这几个sector的存储空间。
SRAM
各款MCU所支持的 SRAM 大小,请参考对应的Datasheet。I-SRAM可以存放指令和数据,D-SRAM只能存放数据。
对于B92、TL721x 系列 MCU 的不同型号会有SRAM资源的差异,需要用户针对性地根据实际的I-SRAM和D-SRAM配置boot/[Chip Name]/cstartup_[Chip Name].S文件中对IRAM和DRAM大小的定义。以B92为例:
##define SRAM_SIZE SRAM_128K
##if (SRAM_SIZE == SRAM_256K)
.equ __IRAM_2_EN, 1
.equ __DRAM_1_EN, 0
.equ __DRAM_2_EN, 0
.equ __DRAM_DIS, 1
##elif (SRAM_SIZE == SRAM_384K)
.equ __IRAM_2_EN, 1
.equ __DRAM_1_EN, 1
.equ __DRAM_2_EN, 0
.equ __DRAM_DIS, 0
##elif (SRAM_SIZE == SRAM_512K)
.equ __IRAM_2_EN, 1
.equ __DRAM_1_EN, 1
.equ __DRAM_2_EN, 1
.equ __DRAM_DIS, 0
##else
.equ __IRAM_2_EN, 0
.equ __DRAM_1_EN, 0
.equ __DRAM_2_EN, 0
.equ __DRAM_DIS, 1
##endif
注意
- I-SRAM和D-SRAM配置只有cstartup_[Chip Name].S中列出的固定几种配置,并不是用户可以自由组合的。
MCU地址空间分配
各款MCU的Memory Map,请参考对应的Datasheet,以B91的TLSR9218A为例,在Datasheet的第4.1.1 SRAM章节,介绍了它的Memory Map。SDK主要关注以下几个地址空间:
- CPU访问I-SRAM的地址范围(ILM_CPU):0x00000000~0x00020000
- CPU访问D-SRAM的地址范围(DLM_CPU):0x00080000~0x000A0000
- Flash空间的地址范围(FLASH):0x20000000~0x21000000
- 寄存器的地址范围(USB、AUDIO、ZB等):0x80100000~0x82000000
- 总线访问I-SRAM的地址范围(ILM):0xC0000000~0xC0020000
- 总线访问D-SRAM的地址范围(DLM):0xC0200000~0xC0220000
说明
- 从上图看到,I-SRAM或D-SRAM可以通过CPU访问,也可以通过总线(如DMA, Swire)访问,通过不同的访问地址来区分访问的方式。从上图可知,CPU也可以通过总线访问。
Flash和SRAM空间分配
tl_ble_sdk会通过调用blc_app_setDeepsleepRetentionSramSize()
根据实际SRAM要使用的retention大小,来配置deepsleep retention的模式(如 DEEPSLEEP_MODE_RET_SRAM_LOW32K)。下图是以B91为例,在deepsleep retention 32K模式下,I-SRAM和D-SRAM都同时使用的情况下对应的SRAM和Firmware空间分配说明。
上图的SRAM空间分配相关的文件有cstartup_B91.S和boot_general.link。
编译生成的Firmware下载到Flash中,其内容包括vectors、retention_reset、retention_data、ram_code、text、rodata和data initial value。
上电启动后,会将Flash中的部分内容搬运到I-SRAM和D-SRAM。
I-SRAM中包括retention_reset、aes_data、retention_data、ram_code、和unused I-SRAM area。I-SRAM中的retention_reset / retention_data / ram_code是Flash中retention_reset / retention_data / ram_code的拷贝。
D-SRAM中包括data、sdata、bss、sbss、heap、unused D-SRAM area和stack。D-SRAM中data的初始值是Flash中data initial value。
各个段的介绍
data、sdata 和 data initial value
“data” 或 “sdata” 段存放初值非0的全局变量。sdata是small data的简写。其初值存储在Flash Firmware中的“data initial value”段,在运行bootloader时,会将初值拷贝到变量对应的SRAM地址。
sbss and bss
“sbss”段和“bss”段存放程序未初始化或初始化为0的全局变量,即initial value为0的全局变量。sbss是small bss的简写。在运行bootloader时,会直接将该段SRAM全设为0。
text
“text”段是程序中的函数默认编译到的段。访问“text”段的指令需要通过I-Cache,将需要执行的指令先加载到I-Cache中才可以被执行。
rodata
“rodata”段存放的是程序中定义的可读、不能改写的变量,位于Flash中,如用关键字“const”定义的变量。
ram_code
“ram_code”段是Firmware中需要常驻内存的code,上电后MCU会从Flash拷贝到I-SRAM的ram_code area上,内容包含SDK中加了关键字_attribute_ram_code_xxx
的函数,比如rf_irq_handler函数:
_attribute_ram_code_ void rf_irq_handler(void);
函数常驻内存有三个原因:
(1). 某些函数由于涉及到Flash MSPI四根管脚的时序复用,必须常驻内存,如果放到Flash中就会出现时序冲突,造成死机,如Flash操作相关所有函数;
(2). 放到ram中执行的函数每次被调用时不需要从Flash重新读取,可以节省时间,所以对于一些执行时间有要求的函数可以放到常驻内存,提高执行效率。SDK中将BLE时序相关的一些经常要执行的函数常驻到内存,大大降低执行时间,节省功耗;
注意
- tl_ble_sdk支持中断嵌套的功能,详细内容可以请参考章节中断嵌套,如果用户新增了LEV3优先级的中断入口函数,就必须将它放到“ram_code”段。
用户如果需要将某个函数常驻内存,可以仿照上面的rf_irq_handler,在函数上添加关键字_attribute_ram_code_
,编译之后就能在objdump文件中看到该函数在ram_code段了。
retention_data
“retention_data”段是Firmware中需要常驻内存的数据,上电或normal deepsleep唤醒后MCU会从Flash拷贝到I-SRAM的retention_data area上,deepsleep retention休眠后,I-SRAM的指定retention区域数据不掉电保持(参考定义 pm_sleep_mode_e)。
程序中的全局变量默认会分配在“data”段、“sdata”段、“sbss”段或“bss”段,并未限定存放在I-RAM的retention区域,进入deepsleep retention会丢失。
如果希望一些特定的变量在deepsleep retention mode期间能够保持,需要将它们分配到“retention_data”段,方法是在定义变量时添加关键字_attribute_data_retention_
。以下为几个示例:
_attribute_data_retention_ int AA;
_attribute_data_retention_ unsigned int BB = 0x05;
_attribute_data_retention_ int CC[4];
_attribute_data_retention_ unsigned int DD[4] = {0,1,2,3};
vectors and retention_reset
“vectors”段和“retention_reset”段都是汇编文件cstartup_
aes_data
“aes_data”段存放硬件AES模块的缓存数据,aes_data段在I-SRAM上,并且长度固定为32 bytes,用户不可更改。在运行bootloader时,会将这段区域全设为0。
_retention_size_
I-SRAM的“retention_reset + aes_data + retention_data + ram_code”共4段按顺序排布在I-SRAM的前面,它们的总大小为“_retention_size_”。MCU上电或normal deepsleep 醒来后,程序在执行过程中只要不进normal deepsleep(只有suspend/deepsleep retention),“_retention_size_”的内容就一直保持在I-SRAM上,MCU无须再从Flash上读取。
评估“_retention_size_”的方法是根据objdump文件开头的‘Sections’,使用“ram_code”段的‘Size’与‘VMA’相加,就是实际的“_retention_size_”大小,比如下图的“_retention_size_”大小为0x5b02 + 0xf00,约26.5KB。
如果用户选择的配置使用的是deepsleep retention 32K mode,但定义的“_retention_size_”超过所定义的32K,比如下图的“_retention_size_”大小为0x5b02 + 0x3700,约36.5KB,超出32K编译检查会报错。
用户可以通过下面的方式修改:
(1). 减少所定义“_attribute_data_retention_”段或“_attribute_ram_code_”段属性的内容。
(2). 根据芯片支持情况,选择切换为deepsleep retention 更大Size的模式。
Cache
Cache为高速缓存,分为I-Cache和D-Cache两块,大小各是固定的8KB,访问地址用户不可见。
默认Cache是打开的,在cstartup_
/* Enable I/D-Cache */
csrr t0, mcache_ctl
ori t0, t0, 1 #/I-Cache
ori t0, t0, 2 #/D-Cache
csrw mcache_ctl, t0
fence.i
常驻内存的code可以直接从SRAM中读取并执行,但Firmware中可以常驻SRAM的code只是一部分,剩下绝大部分都还在Flash中。根据程序的局部性原理,可以将一部分Flash code加载到Cache中,如果当前需要执行的code在Cache里,直接从Cache读取并执行;如果不在Cache中,则从Flash读取code加载到Cache再执行。
Firmware的”text”和“rodata”段是没有放到SRAM中,这部分code符合程序局部性原理,需要一直被load到Cache中才能被执行。
由于Cache比较大,所以不允许用户使用指针形式读Flash,因为指针形式读Flash的数据被缓存在Cache里,如果Cache里这个数据没有被其他内容覆盖时,即使该位置处的Flash数据已经被改写,又有新的访问该数据的请求发生,此时MCU会直接用Cache里缓存的内容作为结果。
heap
“heap”区域是分配给堆,堆是向上增长的,一般我们设置在bss后面不用的空间,如果调用了sprintf/malloc/free这类内存管理函数,这些函数会调用_sbrk函数进行堆内存的分配,_sbrk会通过_end符号确定从哪里开始分配堆空间,在link文件的定义如下。
PROVIDE (_BSS_VMA_END = .);
...
. = ALIGN(8);
/* end is the starting address of the heap, the heap grows upward */
_end = .;
PROVIDE (end = .);
stack
对于128KB的D-SRAM,“stack”是从最高地址0x000A0000开始的,其方向为从下往上延伸,即stack指针SP在数据入栈时自减,数据出栈时自加。
如果stack使用过大,那么可能会出现栈溢出的情况,与.bss段发生重合,导致程序运行错误。关于查看stack的原理和方法请参考章节附录2。
以B91为例,boot_general.link文件中定义了栈顶位置_STACK_TOP:
PROVIDE (_STACK_TOP = 0x00a0000);/*Need to prevent stack overflow*/
在cstartup_B91.S文件里初始化了堆栈指针sp寄存器:
/* Initialize stack pointer */
la t0, _STACK_TOP
mv sp, t0
如果用户希望128KB的D-SRAM空间全部留给用户使用或者D-SRAM不使用,可以将SDK占用的数据和指令都放到I-SRAM。
时钟模块
时钟模块参考各MCU的Datasheet中的Clock章节。
初始化时调用API blc_app_system_init() 配置pll_clk/cclk/hclk/pclk/clk_mspi,以TL321x为例:
注意
- 多连接SDK的CCLK至少配置24MHz系统时钟,其他时钟无法满足多连接SDK的运行。
- TL_BLE_SDK中TL321x默认主频(cclk)支持到最高48MHz,如果需要支持48MHz以上主频,需要参考tl_platform_sdk中LPC_demo中的写法做相应修改。
- TL_BLE_SDK中TL322x默认主频(cclk)支持到最高96MHz,如果需要支持96MHz以上主频,需要参考tl_platform_sdk中LPC_demo中的写法做相应修改。
B91的System Timer的频率是固定的16MHz,B92、TL321x、TL721x、TL322x的System Timer的频率是固定的24MHz。由于System Timer是BLE计时的基准,SDK中所有BLE时间相关的参数和变量,在涉及到时间的表达时,都是用“SYSTEM_TIMER_TICK_xxx”的方式,如下的数值来表示s、ms和us(以TL321x为例):
enum
{
SYSTEM_TIMER_TICK_1US = 24,
SYSTEM_TIMER_TICK_1MS = 24000,
SYSTEM_TIMER_TICK_1S = 24000000,
SYSTEM_TIMER_TICK_625US = 15000, //625*24
SYSTEM_TIMER_TICK_1250US = 30000, //1250*24
};
SDK中以下几个API都是跟System Timer相关的一些操作,这些API内部已经使用上面类似“ xxx_TIMER_TICK_xxx”的方式来表示时间,用户操作这些API时,根据形参提示输入us或ms就可以。
void delay_us(unsigned int microsec);
void delay_ms(unsigned int millisec);
_Bool clock_time_exceed(unsigned int ref, unsigned int us)
System Timer的使用
main函数中sys_init初始化完成后,System Timer就开始工作,用户可以读取System Timer计数器的值(简称System Timer tick)。
System Timer tick每一个时钟周期加一,其长度为32bit,最小值0x00000000,最大值0xffffffff。System Timer刚启动的时候,tick值为0。B91每1/16 us加1,到最大值0xffffffff需要的时间为:(1/16) us * (2^32) 约等于268秒,每过268秒System Timer tick转一圈。B92每1/24 us加1,到最大值0xffffffff需要的时间为:(1/24) us * (2^32) 约等于178秒,每过178秒System Timer tick转一圈。
MCU在运行程序过程中system tick不会停止。
System Timer tick的读取可以通过clock_time()函数获得:
u32 current_tick = clock_time();
中断嵌套
中断嵌套功能简述
tl_ble_sdk包含的芯片支持中断嵌套功能,先说明下三个概念:中断优先级,中断阈值,中断抢占。
(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已经规划了3种类型优先级的中断,用户必须按照这样的分类来使用。中断优先级LEV1的中断等级最低,分配给用户定义的APP普通中断。中断优先级LEV2的中断等级在中间,强制分配给BLE中断,用户定义的中断不能使用LEV2。中断优先级LEV3的中断等级最高,一般情况下不建议使用,只有某些特殊场合需要实时响应的情况才使用,也是分配给用户定义的APP高级中断。
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
-
中断处理函数执行时间小于50 us
在执行Flash空间的擦除、读写操作函数时,会将中断阈值设置为1,执行完Flash操作函数后再将中断阈值设置为0,因此在读写Flash操作过程中允许BLE中断和用户APP高级中断插入。如果BLE中断和用户APP高级中断函数存放在Flash中,Flash预取值操作和读写Flash操作会出现时序冲突,造成死机。如果BLE中断和用户APP高级中断函数中存在读写Flash的操作,多个读写Flash操作也会出现时序冲突,造成死机。因此需将BLE中断和用户APP高级中断函数放在ram_code段,以及函数内禁止访问Flash。由于用户APP高级中断会抢占BLE中断和APP普通中断,所以用户也必须限制高级中断函数执行时间小于50 us以免影响到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中断,还是用户关闭全局中断,都限制最长时间为50 us,需要用户特别注意。
BLE模块
本手册以Bluetooth Core Specification 6.0版本为参考。
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。
(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
tl_ble_sdk支持标准的BLE controller,包括HCI、PHY(Physical Layer)和LL(Link Layer)。
tl_ble_sdk包含Link Layer的五种标准状态(standby、advertising、scanning、initiating、connection),connection状态下同时支持最多 4 个 Central role 和 4 个 Peripheral role。
controller架构图如下:
Telink BLE Multiple Connection Whole Stack (Controller+Host)
tl_ble_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
tl_ble_sdk 将Connection Central role最大数量称为supportedMaxCentralNum,将Connection Peripheral role最大数量称为supportedMaxPeripheralNum,他们是由library决定的,如下表所示:
Table: 支持最大主从数与library对应关系
IC library supportedMaxCentral-Num supportedMaxPeripheralNum
B91 liblt_9518 4 4
B92 liblt_9528 4 4
TL721X liblt_TL721X 4 4
TL321X liblt_TL321X 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。
而对于tl_ble_sdk ,由于要同时支持多个Central和Peripheral,Link Layer无法做到在某一时刻只处于某一种状态,必须是几种状态的组合。
tl_ble_sdk 的Link Layer状态机比较复杂,只做一个大致的介绍,能够满足用户对底层的基本理解以及相应API的使用。
Link Layer状态机初始化
tl_ble_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这个短暂的临时状态。
tl_ble_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种,如下表所示:
Table: C1P1 Link Layer组合状态
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种,如下表所示:
Table: C4P4 Link Layer组合状态
/ | 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 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中的1I2F状态。
此时只有连接任务,按照各自连接的时序进行任务分配。如果发生了冲突,则以优先级高低进行分配,高优先级任务抢占,被放弃的任务会自动增加优先级,增加下次冲突时抢占的几率。
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的讲解,请参考章节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为例,其他芯片是同样的原理,类推即可。
(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[48 * 8 * 4] = {0};
u8 app_acl_per_tx_fifo[48 * 8 * 4] = {0};
图示如下:
每一个connection都对应一个tx fifo,并且每一个connection fifo的数量都是8(0 ~ 7),0 ~ 7的 size都是一样的(48B):
(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[272 * 8 * 4] = {0};
u8 app_acl_per_tx_fifo[48 * 8 * 4] = {0};
从图可以看出,Central 和 Peripheral 每个 connection 的 fifo数量是一样的,都是8个(0~7)。但是 Central 每个 fifo size是272B,而Peripheral的fifo size是48B。
(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)。
tl_ble_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”部分的详细说明---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 可以使用在广播通道,也可以用在连接状态下的数据通道。
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。
tl_ble_sdk 中,Channel Selection Algorithm #2默认是关闭的,用户如果选择使用此Feature,可以手动打开,需要在user_init()调用下面的API使能。
void blc_ll_initChannelSelectionAlgorithm_2_feature(void);
只有local device和peer device都同时支持Channel Selection Algorithm #2,Channel Selection Algorithm #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。
tl_ble_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的地址即可,相关介绍请参考章节Flash和SRAM空间分配。如果不需要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 Undirected 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的状态机变化请参考章节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_setAdvCustomizedChannel
该API用于定制特殊的advertising channel & scanning channel,只对一些非常特殊的应用有意义,如BLE mesh。
void blc_ll_setAdvCustomizedChannel(u8 chn0, u8 chn1, u8 chn2);
chn0/chn1/chn2填需要定制的频点,默认的标准频点是37/38/39,比如设置3个advertising channel分别为 2420 MHz、2430 MHz、2450 MHz,可如下调用:
blc_ll_setAdvCustomizedChannel (8, 12, 22);
常规BLE应用使用该API可以实现这样的功能,如果user在一些使用场景希望用到单通道广播&单通道扫描,比如将advertising channel & scanning channel固定为39,可如下调用:
blc_ll_setAdvCustomizedChannel (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_MATCH=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的状态机变化请参考章节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(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_interval/scan window
scan_interval/scan_window在该API中暂时没有处理。如果需要设置可以使用“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
tl_ble_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。
tl_ble_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
tl_ble_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,其余芯片可依次类推。
该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中,目前tl_ble_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使用,目前tl_ble_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可以在“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软件架构
tl_ble_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接口,tl_ble_sdk 目前只提供了UART接口的HCI Transport。Telink BLE HCI transport的软件实现可以查看SDK中的vendor/common/hci_transport文件夹。
HCI UART Transport 支持两种协议,H4和H5,这两种协议tl_ble_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提供 |
!!! note "说明" 该函数用户一般不需要调用,它由HCI Transport Control层调用。
void HCI_Tr_H4RxHandler(void)
功能: 该函数实现了H4 PDU的解析和处理功能。
参数: 无。
!!! note "说明" 该函数用户一般不需要调用,它由HCI Transport Control层调用。
void HCI_Tr_H4IRQHandler(void)
功能: 该函数实现了UART RX/TX中断处理。
参数: 无。
!!! note "说明" 该函数用户一般不需要调用,它由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的转义序列表如下图所示。
tl_ble_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。
tl_ble_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,当收到SYNC_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版本。
tl_ble_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层调用。 |
!!! note "说明" 该函数用户一般不需要调用,它由HCI Transport Control层调用。
void HCI_H5_Poll(void)
功能: 该函数用于管理H5包的解析、发送、resend和流控。
参数: 无。
!!! note "说明" 该函数用户一般不需要调用,它由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 协议。tl_ble_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信息 |
!!! note "说明" 该函数被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大部分是一样的,在tl_ble_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。tl_ble_sdk也将Controller HCI event分为两类:HCI event和HCI LE event。由于tl_ble_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
tl_ble_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。
注意
- tl_ble_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数据处理函数
tl_ble_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)。
tl_ble_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。
tl_ble_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的做法有关,各家标准并不统一。
tl_ble_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;
tl_ble_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。tl_ble_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 bytes 的UUID时,Central设备都知道这些UUID代表的设备类型。SDK中已经定义了一些标准的UUID,分布在以下文件中:stack/ble/service/uuid.h。
Telink私有的一些profile(OTA、SPP、MIC等),标准蓝牙里面不支持,在stack/ble/service/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
注意
- 目前tl_ble_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指向otaOutUuid在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。写过来的数据有效长度为l2capLen - 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的指针。
(9) ATT HANDLE
每一个attribute都对应一个handle,handle的具体枚举位于app_att.h文件的ATT_HANDLE
中, 如GAP service对应att handle枚举如下:
static const attribute_t my_Attributes[] = {
...
// 0001 - 0007 gap
{7, ATT_PERMISSIONS_READ, 2, 2, (u8 *)(size_t)(&my_primaryServiceUUID), (u8 *)(size_t)(&my_gapServiceUUID), 0, 0},
{0, ATT_PERMISSIONS_READ, 2, sizeof(my_devNameCharVal), (u8 *)(size_t)(&my_characterUUID), (u8 *)(size_t)(my_devNameCharVal), 0, 0},
{0, ATT_PERMISSIONS_READ, 2, sizeof(my_devName), (u8 *)(size_t)(&my_devNameUUID), (u8 *)(size_t)(my_devName), 0, 0},
{0, ATT_PERMISSIONS_READ, 2, sizeof(my_appearanceCharVal), (u8 *)(size_t)(&my_characterUUID), (u8 *)(size_t)(my_appearanceCharVal), 0, 0},
{0, ATT_PERMISSIONS_READ, 2, sizeof(my_appearance), (u8 *)(size_t)(&my_appearanceUUID), (u8 *)(size_t)(&my_appearance), 0, 0},
{0, ATT_PERMISSIONS_READ, 2, sizeof(my_periConnParamCharVal), (u8 *)(size_t)(&my_characterUUID), (u8 *)(size_t)(my_periConnParamCharVal), 0, 0},
{0, ATT_PERMISSIONS_READ, 2, sizeof(my_periConnParameters), (u8 *)(size_t)(&my_periConnParamUUID), (u8 *)(size_t)(&my_periConnParameters), 0, 0},
...
}
typedef enum
{
ATT_H_START = 0,
//// Gap ////
/**************************************************/
GenericAccess_PS_H, //UUID: 2800, VALUE: uuid 1800
GenericAccess_DeviceName_CD_H, //UUID: 2803, VALUE: Prop: Read | Notify
GenericAccess_DeviceName_DP_H, //UUID: 2A00, VALUE: device name
GenericAccess_Appearance_CD_H, //UUID: 2803, VALUE: Prop: Read
GenericAccess_Appearance_DP_H, //UUID: 2A01, VALUE: appearance
CONN_PARAM_CD_H, //UUID: 2803, VALUE: Prop: Read
CONN_PARAM_DP_H, //UUID: 2A04, VALUE: connParameter
...
}
(10) 如何添加一个新的att service,下面是一个简单示例
a) 在app_att.c 中的my_Attributes数组里加入需要新添加的service信息,例子如下:
/////////////// CUSTOM DEFINE EXAMPLE //////////////////
##define TELINK_CUSTOM_DEFINE_EXAMPLE 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23
static const u8 TelinkCustomDefineExampleUUID[16] = WRAPPING_BRACES(TELINK_CUSTOM_DEFINE_EXAMPLE);
static const u8 TelinkBrithday[] = "20100630";
static const u8 mood_of_today[] = {'E', 'x', 'c', 'i', 't', 'i', 'n', 'g'};
static const attribute_t my_Attributes[] = {
......
// 0040 - 0042 custom_define_example
{3, ATT_PERMISSIONS_READ, 2, 16, (u8 *)(size_t)(&my_primaryServiceUUID), (u8 *)(size_t)(&TelinkCustomDefineExampleUUID), 0, 0},
{0, ATT_PERMISSIONS_RDWR, 2, sizeof(TelinkBrithday), (u8 *)(size_t)(&my_characterUUID), (u8 *)(size_t)(TelinkBrithday), 0, 0},
{0, ATT_PERMISSIONS_RDWR, 2, sizeof(mood_of_today), (u8 *)(size_t)(&userdesc_UUID), (u8 *)(size_t)(&mood_of_today), 0, 0},
};
b) 在app_att.h中的ATT_HANDLE里添加新加service的枚举
typedef enum
{
...
//// CUSTOM DEFINE ////
/************************************************/
CUSTOM_DEFINE_PS_H, //UUID: TELINK_CUSTOM_DEFINE_EXAMPLE, VALUE: TELINK CUSTOM DEFINE UUID
TELINK_BRITHDAY_CD_H, //UUID: 2803, VALUE:TelinkBrithday Prop: Read | write
MOOD_OF_TODAY_DP_H, //UUID: 2901, VALUE: mood_of_today
...
} ATT_HANDLE;
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,tl_ble_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(目前tl_ble_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。
注意
- tl_ble_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的格式。
tl_ble_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的格式。
tl_ble_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
tl_ble_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初始化
tl_ble_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。
tl_ble_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);
如果设置值为0,或者没有调用以上API进行设置,则Pincode值是随机的。
用户将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);
说明
-
在 tl_ble_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();
这里涉及到请求 TK 的 GAP event:GAP_EVT_SMP_TK_REQUEST_PASSKEY,请参考章节“GAP event”。
用户调用以下 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 Connection的安全配对方式,需要MTU>=65。
-
Secure Connections Passkey Entry 方式的初始化配置:
与 Legacy Pairing Passkey Entry 基本一致,唯一不同的是需要在初始化最开始的地方设置配对方式为“安全连接配对”:
blc_smp_setSecurityLevel(Authenticated_LE_Secure_Connection_Pairing_with_Encryption);
blc_smp_setPairingMethods(LE_Secure_Connection);
...//参考 Mode 1 Level 3 配置方式
- Secure Connections Numeric Comparison 的初始化配置:
blc_smp_setSecurityLevel(Authenticated_LE_Secure_Connection_Pairing_with_Encryption);
blc_smp_setPairingMethods(LE_Secure_Connection);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_DISPLAY_YES_NO);
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_CAPABILITY_DISPLAY_YES_NO);
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 区域。
对于 tl_ble_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);
最大绑定数量
对于 tl_ble_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);
- Central_max_bonNum: 自身作为 Central 角色的最大 peer Peripheral 绑定数量,最大为8,传入参数超过8时,返回错误 0xA0 (SMP_ERR_INVALID_PARAMETER)。
- Peripheral_max_bondNum: 自身作为 Peripheral 角色的最大 peer Central 绑定数量,最大为4,传入参数超过4时,返回错误 0xA0 (SMP_ERR_INVALID_PARAMETER)。
达到最大绑定数量时,下一个绑定的设备将会顶替掉当前同角色有效设备中最早绑定的设备。具体而言,会将新的设备的 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 的绑定数量:
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达到最大数量时,新的Bonding Info的处理方式
void blc_smp_setDevExceedMaxStrategy(dev_exceed_max_strategy_t strategy);
Device Manage & Simple SDP
如前面对于 GATT 的描述,在 BLE 中,Peripheral 充当 GATT Server 角色时,会维护一个 GATT Service 的表,表中的每条 Attribute 都对应有个一 Attribute handle 值。而对于 Central,想要获取 Peripheral 的这些信息,需要通过 SDP 过程获取,并进行维护,以供需要时使用。
为了便于用户使用,tl_ble_sdk 为用户提供了一个连接设备管理方案的实现 Device Manage 以及一个 Central 做 SDP 获取 peer Peripheral 的 GATT Service 表的简单实现。不仅可以为 Central 管理 peer Peripheral 的 GATT Service 表,还可以用于随时通过对端设备的部分信息,调取该设备的其他相关信息。该方案全部以源码的形式提供,用户可以参考 SDK 中 vendor/common/device_manage.* 文件及 vendor/common/simple_sdp.* 文件。
tl_ble_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 是一个很复杂的部分,对于 tl_ble_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 sync等。这些功能的引入也为后面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_ADV_IND 加上所有对应的AUX_CHAIN_IND的有效数据不超过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。
扩展广播包含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字段进行区分。
注意:
- 目前tl_ble_sdk支持创建最大广播集的数量是4个。
Extended Advertising 相关的 API 介绍
ble_sts_t blc_ll_initExtendedAdvModule_initExtendedAdvSetParamBuffer(u8 *pBuff_advSets, int num_advSets);
如果使用扩展广播,需使用该API进行初始化。目的是初始化extended advertising module及对应广播参数所需要的buffer空间。只有进行了相应模块的初始化,相应的feature才会生效,对应的功能函数才会链接到执行(bin)文件中。
pBuff_advSets:底层控制extended advertising使用的buffer空间首地址。stack会使用该buffer来存储extended advertising的运行时所需的各种控制变量。每个广播集需要分配一个这样的buffer空间。之所以交由上层用户来管控,是因为用户可以根据实际使用的广播集数量来进行分配,以节约RAM size。
num_advSets:用户实际使用的广播集数量。note:底层最大支持4个广播集。
void blc_ll_initExtendedAdvDataBuffer(u8 *pExtAdvData, int max_len_advData);
设置存储extended advertising data的buffer,该buffer用来存储blc_ll_setExtAdvData设置的数据。
pExtAdvData:buffer首地址。
max_len_advData:buffer size。blc_ll_setExtAdvData设置的数据长度不能大于该值。
void blc_ll_initExtendedScanRspDataBuffer(u8 *pScanRspData, int max_len_scanRspData);
设置存储scan response data的buffer,该buffer用来存储blc_ll_setExtScanRspData设置的数据。
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 Spec 标准接口,用于设置extended advertising参数,详细请参考《Core_5.4》 (Vol 4/Part E/7.8.53 “LE Set Extended Advertising Parameters Command”), 并结合SDK该API的注释来理解。
ble_sts_t blc_ll_setExtAdvData (u8 adv_handle, int advData_len, u8 *advData);
BLE Spec 标准接口,用于设置extended advertising发送的数据,详细参考《Core_5.4》 (Vol 4/Part E/7.8.54 “LE Set Extended Advertising Data command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_setExtScanRspData(u8 adv_handle, int scanRspData_len, u8 *scanRspData);
BLE Spec 标准接口,用于设置extended scan response的数据,详细参考《Core_5.4》 (Vol 4/Part E/7.8.55 “LE Set Extended Scan Response Data command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_setExtAdvEnable(adv_en_t enable, u8 adv_handle, u16 duration, u8 max_extAdvEvt);
BLE Spec 标准接口,用于打开/关闭 Extended Advertising,详细可参考《Core_5.4》 (Vol 4/Part E/7.8.56 “LE Set Extended Advertising Enable Command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_setAdvRandomAddr(u8 adv_handle, u8* rand_addr);
BLE Spec 标准接口,用来设置设备的random address。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.52 “LE Set Advertising Set Random Address command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_removeAdvSet(u8 adv_handle);
BLE Spec 标准接口,用来移除对应的广播集。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.59 “LE Remove Advertising Set command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_clearAdvSets(void);
BLE Spec 标准接口,用来移除所有的广播集。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.60 “LE Clear Advertising Sets command”),并结合SDK该API的注释来理解。
周期广播(Periodic Advertising)
周期广播也是core 5.0引入的概念,可以以固定周期向所有监听该广播的设备发送数据。周期广播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,如下图所示。
注意:
- 目前tl_ble_sdk支持创建最大2个周期广播 (4个广播集中只有2个可以引导出周期广播)。
Periodic Advertising 相关的 API 介绍
PA 广播 (Advertising) 相关的 API:
void blc_ll_initPeriodicAdvModule_initPeriodicdAdvSetParamBuffer(u8 *pBuff, int num_periodic_adv);
初始化periodic advertising module,只有进行了相应模块的初始化,相应的feature才会生效,对应的功能函数才会链接到执行(bin)文件中。
初始化periodic advertising module所需参数的buffer空间。stack会使用该buffer来存储periodic advertising运行时所需的各种控制变量。每个广播集需要分配一个这样的buffer空间。之所以交由上层用户来管控,是因为用户可以根据实际使用的广播集数量来进行分配,以节约RAM size。
pBuff:periodic advertising 参数使用的buffer空间地址。
num_periodic_adv:用户实际使用的周期广播集数量。note:底层最大支持的周期广播集数量是2个。
ble_sts_t blc_ll_setPeriodicAdvParam(adv_handle_t adv_handle, u16 advInter_min, u16 advInter_max, perd_adv_prop_t property);
BLE Spec 标准接口,用于设置periodic advertising参数。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.61 “LE Set Periodic Advertising Parameters command”),并结合SDK该API的注释来理解。
void blc_ll_initPeriodicAdvDataBuffer(u8 *perdAdvData, int max_len_perdAdvData);
设置存储periodic advertising data的buffer,该buffer用来存储blc_ll_setPeriodicAdvData设置的数据。
ble_sts_t blc_ll_setPeriodicAdvData(adv_handle_t adv_handle, u16 advData_len, u8 *advdata);
BLE Spec 标准接口,用于设置periodic advertising发送的数据,详细参考《Core_5.4》 (Vol 4/Part E/7.8.62 “LE Set Periodic Advertising Data command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_setPeriodicAdvEnable(u8 per_adv_enable, adv_handle_t adv_handle);
BLE Spec 标准接口,用于打开/关闭 periodic Advertising,详细参考《Core_5.4》 (Vol 4/Part E/7.8.63 “LE Set Periodic Advertising Enable command”),并结合SDK该API的注释来理解。
扩展扫描(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);
初始化extended scan module,只有进行了相应模块的初始化,相应的feature才会生效,对应的功能函数才会链接到执行(bin)文件中。
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 Spec 标准接口,用于设置extended scan参数。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.64 “LE Set Extended Scan Parameters command”),并结合SDK该API的注释来理解。
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);
BLE Spec 标准接口,用于打开/关闭extended scan。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.65 “LE Set Extended Scan Enable command”),并结合SDK该API的注释来理解。
周期同步 (Periodic Sync)
扫描设备可以有以下两种方式中与周期广播序列 (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 Sync相关的 API 介绍
void blc_ll_initPeriodicAdvertisingSynchronization_module(void);
初始化periodic sync module,只有进行了相应模块的初始化,相应的feature才会生效,对应的功能函数才会链接到执行(bin)文件中。
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 Spec 标准接口,用于和周期广播同步,开始接收周期广播包。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.67 “LE Periodic Advertising Create Sync command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_periodicAdvertisingCreateSyncCancel (void);
BLE Spec 标准接口,在使用blc_ll_periodicAdvertisingCreateSync准备同步相应periodic advertising,但是还未同步上的时候,调用该API可以取消同步,但是如果已经同步上,调用该API不会有作用,会返回command disallowed。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.68 “LE Periodic Advertising Create Sync Cancel command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_periodicAdvertisingTerminateSync (u16 sync_handle);
BLE Spec 标准接口,停止与sync_handle指定的periodic advertising同步。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.69 “LE Periodic Advertising Terminate Sync command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_addDeivceToPeriodicAdvertiserList (u8 adv_adrType, u8 *adv_addr, u8 adv_sid);
BLE Spec 标准接口,添加指定的设备及广播集ID到Periodic Advertiser List,类似whitelist。详细内容可参考(Vol 4/Part E/7.8.70 “LE Add Device To Periodic Advertiser List command”),并结合SDK该API的注释来理解。
关于Periodic sync establishment filter policy可参考《Core_5.4》 (Vol 6/Part B/4.3.5 Periodic sync establishment filter policy)
ble_sts_t blc_ll_removeDeivceFromPeriodicAdvertiserList (u8 adv_adrType, u8 *adv_addr, u8 adv_sid);
BLE Spec 标准接口,从Periodic Advertiser List删除指定内容。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.71 “LE Remove Device From Periodic Advertiser List command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_clearPeriodicAdvertiserList (void);
BLE Spec 标准接口,删除Periodic Advertiser List中所有内容。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.72 “LE Clear Periodic Advertiser List command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_readPeriodicAdvertiserListSize (u8 *perdAdvListSize);
BLE Spec 标准接口,读取Periodic Advertiser List可以存储信息的最大数量。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.73 “LE Read Periodic Advertiser List Size command”),并结合SDK该API的注释来理解。
ble_sts_t blc_ll_periodicAdvertisingReceiveEnable (u16 sync_handle, sync_adv_rcv_en_msk enable);
BLE Spec 标准接口,打开或关闭接收到的periodic advertising上报给host。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.88 “LE Set Periodic Advertising Receive Enable command”),并结合SDK该API的注释来理解。
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
如上图,smart phone已经与TV进行了周期广播同步,可以收到周期广播数据包AUX_SYNC_IND。同时,smart phone可与smart watch建立ACL连接。在没有PAST的情况下,如果smart watch希望从TV处获得周期广播数据包,那么smart watch需要自行扫描并与TV进行周期广播同步。smart watch在完成这一过程时需要耗费额外的时间和电能,然而此类设备往往电量有限。
在有PAST的情况下,针对相同的情景,smart phone能够通过LE ACL将周期广播同步信息通过LL_PERIODIC_SYNC_IND传输到smart watch,smart watch可通过同步信息与TV进行周期广播同步。PAST能够简化同步过程并帮助电量有限的设备节省电能。
PAST 模式2
PAST涉及三方设备:广播设备(TV)、辅助器(smart phone,作为 Central)、接收设备 (smart watch,作为 Peripheral),其中广播设备(TV)和辅助器(smart phone)在实现上可以是同一个设备,也可以独立存在。当辅助器(central)自身就是广播源时即为PAST模式2,如下图所示。
PAST相关API介绍
void blc_ll_initPAST_module(void);
初始化PAST module,只有进行了相应模块的初始化,相应的feature才会生效,对应的功能函数才会链接到执行(bin)文件中。
ble_sts_t blc_ll_periodicAdvSyncTransfer(u16 connHandle, u16 serviceData, u16 syncHandle);
BLE Spec 标准接口,指示controller发送LL_PERIODIC_SYNC_IND给到与自己ACL连接的设备,具体设备有API参数指定。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.89 “LE Periodic Advertising Sync Transfer command”),并结合SDK该API的注释来理解。
注意:
- 这个API应用场景是PAST模式1,即广播设备、辅助设备不是同一个设备。syncHandle是LE Periodic Advertising Sync Established event上报上来的,作为periodic adv ID。
ble_sts_t blc_ll_periodicAdvSetInfoTransfer(u16 connHandle, u16 serviceData, u8 advHandle);
BLE Spec 标准接口,指示controller发送LL_PERIODIC_SYNC_IND给到与自己ACL连接的设备,具体设备有API参数指定。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.90 “LE Periodic Advertising Set Info Transfer command”),并结合SDK该API的注释来理解。
注意:
- 这个API应用场景是PAST模式2,即广播设备、辅助设备是同一个设备。同一个设备,不会有LE Periodic Advertising Sync Established event,就不会有syncHandle来标记的periodic advertising。但是设备自身是知道advertising set ID的,因此可以通过advertising set ID来指定periodic advertising。
ble_sts_t blc_ll_setPeriodicAdvSyncTransferParams(u16 connHandle, u8 mode, u16 skip, u16 syncTimeout, u8 cteType);
BLE Spec 标准接口,指示接收设备(模式1/2中的Peripheral)接收到LL_PERIODIC_SYNC_IND时,controller该如何处理。可以根据LL_PERIODIC_SYNC_IND包含的信息同步到相应的周期广播,也可以忽略LL_PERIODIC_SYNC_IND。
详细可参考《Core_5.4》 (Vol 4/Part E/7.8.91 “LE Set Periodic Advertising Sync Transfer Parameters command”),并结合SDK该API的注释来理解。
接收设备可能存在多个ACL连接,可使用connHandle来指定某个ACL连接。
ble_sts_t blc_ll_setDftPeriodicAdvSyncTransferParams(u8 mode, u8 skip, u16 syncTimeout, u8 cteType);
BLE Spec 标准接口,作用同blc_ll_setPeriodicAdvSyncTransferParams。不同之处是:该API对所有的ACL连接都起作用。如果某个ACL连接需要不同于其他的连接,可以使用blc_ll_setPeriodicAdvSyncTransferParams来设定特定的ACL connect(connHandle)。详细可参考《Core_5.4》 (Vol 4/Part E/7.8.92 “LE Set Default Periodic Advertising Sync Transfer Parameters command”),并结合SDK该API的注释来理解。
Periodic Advertising with Response (PAwR)
PAwR 是《Core_V5.4》新增加的特性。用于通过周期性广播(具体内容可参考周期广播章节)将数据和命令发送给特定的同步设备,同时可以接收同步设备的响应信息,目前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连接请求
AUX_SYNC_SUBEVENT_IND发送完成后,等待一定的延迟并进入接收状态。可以看到接收的时序中被分为多个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);
自定义函数,初始化PAwR广播者模块和分配的数据空间。
//不通过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 spec标准接口,对PAwR的周期广播参数进行设置。详情请参照《core5.4》(vol4/Part E/7.8.61 "LE Set Periodic Advertising Parameters command")。
ble_sts_t blc_ll_setPeriodicAdvEnable(u8 per_adv_enable, adv_handle_t adv_handle);
BLE spec标准接口,使能周期广播,PAwR复用该接口。详情请参照《core5.4》(vol4/Part E/7.8.63 "LE Set Periodic Advertising Enable command")。
//不通过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)
BLE spec标准接口,设置subevent中AUX_SYNC_SUBEVENT_IND包中数据。详情请参照《core5.4》(vol4/Part E/7.8.125 "LE Set Periodic Advertising Subevent Data command")。
//不通过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);
BLE spec标准接口,在对应subevent发送AUX_CONNECT_REQ以建立ACL连接。详情请参照《core5.4》(vol4/Part E/7.8.66 "LE Extended Create Connection command")。
观察端API
ble_sts_t blc_ll_initPAwRsync_module(int num_pawr_sync);
ble_sts_t blc_ll_initPAwRsync_rspDataBuffer(u8 *pdaRspData, int maxLen_pdaRspData);
自定义函数,初始化PAwR观察者模块和分配的响应数据空间。
ble_sts_t blc_hci_le_setPeriodicSyncSubevent(u16 sync_handle,
u16 pda_prop,
u8 num_subevent,
u8* pSubevent)
BLE spec标准接口,观察者设置自身需要监听的subevent,可以为多个。详情请参照《core5.4》(vol4/Part E/7.8.127 "LE Set Periodic Sync Subevent command")。
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标准接口,设置response数据。详情请参照《core5.4》(vol4/Part E/7.8.126 "LE Set Periodic Advertising Response Data command")。
低功耗管理
低功耗管理(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模块维持正常工作。以B91为例,此时IC电流在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长睡眠维持连接),tl_ble_sdk增加了一种sleep mode 3:deepsleep with Sram retention mode,简称deepsleep retention(或deep retention)。
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全部掉电。
低功耗唤醒源
tl_ble_sdk MCU的低功耗唤醒源示意图如下,suspend/deepsleep/deepsleep retention都可以被GPIO PAD和timer唤醒。tl_ble_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
{
WAKEUP_LEVEL_LOW = 0,
WAKEUP_LEVEL_HIGH = 1,
} pm_gpio_wakeup_level_e;
void pm_set_gpio_wakeup(gpio_pin_e pin, pm_gpio_wakeup_level_e pol, int en);
pin为GPIO定义。
pol为唤醒极性定义: Level_High表示高电平唤醒,Level_Low表示低电平唤醒。
en: 1表示enable,0表示disable。
举例说明:
cpu_set_gpio_wakeup (GPIO_PC2, WAKEUP_LEVEL_HIGH, 1); //GPIO_PC2 PAD唤醒打开, 高电平唤醒
cpu_set_gpio_wakeup (GPIO_PC2, WAKEUP_LEVEL_HIGH, 0); //GPIO_PC2 PAD唤醒关闭
cpu_set_gpio_wakeup (GPIO_PB5, WAKEUP_LEVEL_HIGH, 1); //GPIO_PB5 PAD唤醒打开, 低电平唤醒
cpu_set_gpio_wakeup (GPIO_PB5, WAKEUP_LEVEL_HIGH, 0); //GPIO_PB5 PAD唤醒关闭
低功耗模式的进入和唤醒
在tl_ble_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(8),
......
}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 initialization必须执行,寄存器的初始化需要重新设置。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
tl_ble_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去做一些低功耗管理。
注意
- 用户需调用API blc_ll_isBleTaskIdle检查BLE协议栈是否处于Idle状态!
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软件处理流程部分会出现很多变量,用户有必要了解这些变量。
tl_ble_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;
下面API供user选择deepsleep retention mode,鉴于目前SDK都是默认使用最大retention sram size,用户基本上用不到。
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
tl_ble_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可以根据实际情况来调整。
这里只是提供了一个接口,客户根据实际情况来决定是否使用。
API blc_pm_setDeepsleepRetentionEarlyWakeupTiming
该 API 用于设置 Deep Retention 模式下的提前唤醒时间。
芯片在 Deep Retention 模式下唤醒后,从启动文件的 _IRESET_ENTRY
入口开始启动,执行必要的初始化代码,从 _IRESET_ENTRY
到 user_init_deepRetn
函数中的开启中断(irq_enable
)操作,这部分代码消耗的时间称为 Deep Retention 模式的唤醒时间。
唤醒时间会影响 BLE 的时序,且会受到以下因素影响:
(1). 用户在开启中断操作前添加的代码;
(2). 必要的初始化代码;
(3). 主频。
设置提前唤醒时间是为了补偿唤醒时间,通常等于唤醒时间加上 50 us 的阈值,唤醒时间的测量主要借助逻辑分析仪来进行测量,可以参考以下步骤:
(1). 打开 _ISTART
中的 DEBUG IO,该 IO 置高电平时,为 _IRESET_ENTRY
的执行时间点;
(2). 打开 app_config.h
中的 DEBUG_GPIO_ENABLE
开关,这会打开开启中断(irq_enable
)操作前的 DEBUG IO,该 IO 置高电平时,为开启中断(irq_enable
)操作的执行时间点;
(3). 使用逻辑分析仪连接以上两个 IO,并设置芯片进入 Deep Retention 模式,可以在逻辑分析仪的页面上观察到唤醒时间。
将唤醒时间加上 50 us 的阈值作为参数,通过 blc_pm_setDeepsleepRetentionEarlyWakeupTiming API 来进行设置。
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) tl_ble_sdk 工作电压的范围为1.8V~4.3V。当电压低于1V,tl_ble_sdk 已经无法保证稳定地工作。
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章节,先对tl_ble_sdk的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输入通道
tl_ble_sdk的采样方式可采用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”的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
只能使用差分模式
虽然tl_ble_sdk 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
battery_get_detect_enable()返回lowBattDet_enable值,底层低电检测是默认使能,即该值默认为1,如果用户层面对BATT_CHECK_ENABLE进行置位,MCU上电后立刻可以开始低电检测。该变量需要设置成retention_data,确保deepsleep retention不能修改它的状态。
只有在其他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时才会执行唤醒。
tl_ble_sdk 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切换实现的方法,去自行实现。
Flash写保护
Flash写保护主要是用于保护Flash中的用户代码、用户数据和用户配置信息被非授权方篡改。
Flash写保护的重要性
当电压较低或电源的不稳定,对Flash进行操作可能存在出错的风险(尤其是“write”和“erase”操作),造成firmware和用户数据被异常修改,最终导致产品失效。根据以往的量产经验,推荐客户将Flash写保护默认开启。客户在使用Flash写保护时,必须设定一个安全保护区域大小,一方面不能影响正常的协议栈存储信息的写入和擦除(如SMP信息存储区域),另一方面需要尽可能的保护到程序和用户数据。
对于Telink BLE SDK开发实现的产品,只要使用了电池供电,Flash写保护功能最好在该产品整个生命周期都是开启的,以保证产品的稳定性和用户数据的安全性。
Flash写保护的使能
Flash 写保护需要保证电源电压⼤于安全电压阈值。因此使用Flash写保护功能时也需要将低压检测功能打开。
Flash写保护的实现,结合 tl_ble_sdk/acl_central_demo 给出的实现来说明,参考文件flash_prot.h和flash_prot.c。
必须确保app_config.h文件中宏“APP_FLASH_PROTECTION_ENABLE”是被打开的,user使用Flash写保护功能时需要注意。
##define APP_FLASH_PROTECTION_ENABLE 1
Flash写保护的初始化
初始化调用app_flash_protection_operation函数,第一个参数传入FLASH_OP_EVT_APP_INITIALIZATION事件表示初始化Flash写保护,初始化不会使用后面两个参数传入0即可,下面是app_flash_protection_operation函数中初始化部分。
在初始化Flash写保护过程中,样例代码默认对OTA过程需要的区域进行加锁(针对B92,为000000h-07FFFFh 512K的空间)
void app_flash_protection_operation(u8 flash_op_evt, u32 op_addr_begin, u32 op_addr_end)
{
if(flash_op_evt == FLASH_OP_EVT_APP_INITIALIZATION) //判断是否为初始化事件
{
////读取Flash的mid信息,根据mid信息判断是哪一款Flash并调用对应Flash加锁的API
flash_protection_init();
#if (BLE_OTA_SERVER_ENABLE)
//如果ota功能使能,根据ota启动地址确定Flash加锁区域
......
#else
//用户可根据实际情况设置Flash上锁区域大小
......
#endif
//根据上层需要Flash加锁区域大小,映射到不同Flash型号上的命令,并对Flash进行加锁
......
}
......
}
注意:对于要修改的Flash区域,需要先解锁,修改完成后,再上锁。比如将Flash全部上锁后,如果需要擦写Flash,需要先做解锁。如果用户要频繁操作用户数据区域,则可以考虑部分区域上锁的方式。
demo中初始化时会将app_flash_protection_operation函数注册为协议栈里的回调函数,会在OTA过程触发不同事件进行处理,主要是清除旧固件以及写入新固件。
blc_appRegisterStackFlashOperationCallback(app_flash_protection_operation);
Flash写保护处理
在Telink BLE SDK中Flash写保护的处理都集中在app_flash_protection_operation函数中,包括应用程序中的所有Flash加锁和解锁的情况。如果用户有更多的flash操作建议在此函数中进行添加。下面对app_flash_protection_operation函数进行介绍:
void app_flash_protection_operation(u8 flash_op_evt, u32 op_addr_begin, u32 op_addr_end)
{
......
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_BEGIN)
{ //OTA清除旧固件开始事件
......
}
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_END)
{
//OTA清除旧固件结束事件
......
}
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_BEGIN)
{
//OTA写入新固件开始事件
......
}
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_END)
{
//OTA写入新固件结束事件
......
}
//如果需要用户应用程序需要添加更多的Flash保护操作可以在后面继续添加。
}
参数op_addr_begin表示Flash保护区域的起始地址。
参数op_addr_end表示Flash保护区域的结束地址。
参数flash_op_evt表示Flash写保护的事件,包括应用层动作和协议栈事件(OTA写和擦除),共包含5个事件:
##define FLASH_OP_EVT_APP_INITIALIZATION 1
##define FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_BEGIN 10
##define FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_END 11
##define FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_BEGIN 12
##define FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_END 13
(1) FLASH_OP_EVT_APP_INITIALIZATION为Flash写保护初始化事件,对Flash进行加锁。
(2) FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_BEGIN为OTA清除旧固件开始事件,需要将Flash进行解锁。OTA清除旧固件开始事件由协议栈底层触发,在"blc_ota_initOtaServer_module"中,在OTA中成功后的芯片重启阶段。软件会将擦除整个旧固件,以备下一个新OTA流程,如果从“op_addr_begin”到“op_addr_end”内存地址的任何部分处于锁定区域,则需要解锁Flash。在demo示例代码中,我们为新旧固件保护整个Flash区域,所以这里我们不需要判断“op_addr_begin”和“op_addr_end”,并且必须要进行Flash解锁。
(3) FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_END为OTA清除旧固件结束事件,需要将Flash进行加锁。OTA清除旧固件结束事件由堆栈触发,在“blc_OTA_initOtaServer_module”中,擦除旧固件数据完成。在结束事件中忽略“op_addr_begin”和“op_addr_end”,在demo示例代码中,我们需要再次锁定Flash,因为我们已经在清除旧固件的开始事件时解锁了它。
(4) FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_BEGIN为OTA写入新固件开始事件,需要将Flash进行解锁。当接收到第一个OTA数据PDU时,OTA写入新固件开始事件由堆栈触发,软件将数据写入存放新固件的Flash区域,如果从“op_addr_begin”到“op_addr_end”的Flash地址的任何部分处于锁定区域,则需要进行Flash解锁。在demo示例代码中,我们为新旧固件整个Flash区域进行了保护,所以这里我们不需要判断“op_addr_begin”和“op_addr_end”,直接进行Flash解锁。
(5) FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_END为OTA写入新固件结束事件,需要将Flash进行加锁。OTA写入新固件结束事件由堆栈触发,在OTA结束、发生OTA错误后或写入新固件数据完成。在结束事件中忽略“op_addr_begin”和“op_addr_end”,在demo示例代码中,我们需要再次锁定Flash,因为我们在写入新固件的开始事件时已经解锁了它。
在SDK demo中针对Flash写保护部分只涉及ota过程中新旧程序的写入和擦除,用户后续可根据应用情况自行添加其他事件进行Flash写保护操作。
目前代码逻辑仅针对固定地址的空间进行写保护,op_addr_begin和op_addr_end参数目前无效。后续为方便客户使用,会添加针对该参数的使用逻辑。
加锁和解锁操作
//加锁函数, flash_lock_cmd控制加锁区域,以B92为例,完整定义参考flash_mid156085.h中mid156085_lock_block_e
void flash_lock(unsigned int flash_lock_cmd)
//解锁函数
void flash_unlock(void)
flash写保护仅支持一段特定、连续区域的保护,加锁函数过程中如果新加锁区域和旧加锁区域不同,首先执行旧加锁区域的解锁操作,再对新加锁区域进行加锁。
用户在使用过程中,可以直接调用加锁和解锁函数进行操作,但是为方便代码管理,建议仅调用app_flash_protection_operation进行操作:
1) 在flash_port.h中自定义操作码
2) app_flash_protection_operation函数中结合操作码判断编写相应的操作逻辑。
OTA
tl_ble_sdk 支持设备作为OTA Server或OTA Client进行升级,默认Peripheral作为OTA Server,Central作为OTA Client。同一时刻只允许在一个Peripheral链路上进行OTA。
OTA Client的参考代码请参考章节feature_ota。OTA Server的实现代码在协议栈中,不予开放。
tl_ble_sdk支持Flash多地址启动:除了Flash的首地址0x00000,还支持从Flash高地址0x20000(128K)、0x40000(256K)、0x80000(512K)读取firmware运行。本文档以高地址0x20000为例来介绍OTA。
Flash存储架构
Flash存储架构分为传统架构(Secure Boot功能未使能时)和Secure Boot架构(Secure Boot功能使能时)。在两种架构中,如使用启动地址0x20000时,SDK编译出来的firmware size均应不大于128K,即Flash的0~0x20000之间的区域存储Firmware_1,但是由于一些特殊的原因,如果使用启动地址为0和0x20000交替OTA升级,其firmware size不得超过124K(高地址空间最后的4KB都不能使用);如果超过124K必须使用启动地址0和0x40000交替升级,此时最大firmware size不得超过252K,如果超过252K必须使用启动地址0和0x80000交替升级,此时最大firmware size不得超过508K。
Secure Boot相关说明参考:Secure Boot Application Note
传统存储架构
(1) OTA Client将新的firmware_2烧写到0x20000~0x40000的区域。
(2) 第1次OTA:
-
Server上电时从flash的0~0x20000区域读程序启动,运行firmware_1;
-
firmware_1运行,初始化的时候将0x20000~0x40000区域清空,该区域将作为新的Firmware的存储区。
-
启动OTA,Client通过蓝牙交互将firmware_2空运到Server的0x20000~0x40000区域。升级成功后,Server重启。
(3) 将新的firmware_3烧写到OTA Client的0x20000~0x40000的区域。
(4) 第2次OTA:
-
Server上电时从flash的0x20000~0x40000区域读程序启动,运行firmware_2;
-
firmware_2运行,初始化的时候将0~0x20000区域清空,该区域将作为新的Firmware的存储区。
-
启动OTA,Client通过空中包将firmware_3空运到Server的0~0x20000区域。升级成功后,Server重启。
(5) 后面的OTA过程重复上面(1)~ (4)过程,可理解为(2)代表第2n+1次OTA,(3)代表第2n+2次OTA。
注意
- 若B92芯片使能了firmware Encryption,则软件必须调用如下接口配置了Secure Boot OTA功能,否则无法进行正常的firmware Encryption OTA。
ble_sts_t blc_ota_enableFirmwareEncryption(void);
Secure Boot存储架构
如果使用Secure Boot功能,OTA模块需参考feature_test/feature_ota进行特殊设计。作为Client端,在feature_ota/app_config中定义:
##define OTA_CLIENT_SEND_SECURE_BOOT_SIGNATURE_ENABLE 1
Client端会向Server端发送Secure Boot Application Note中Secure Boot章节描述的public key和signature等内容,使Server端进行相关验证过程。
作为Server端,在feature_ota/app_config中定义:
##define APP_HW_FIRMWARE_ENCRYPTION_ENABLE 1
初始化过程将调用API blc_ota_enableFirmwareEncryption使能firmware加密,MCU读写Flash时自动完成加解密过程,功能对应Secure Boot Application Note中 firmware Encryption章节。
在feature_ota/app_config中定义:
##define APP_HW_SECURE_BOOT_ENABLE 1
初始化过程将调用API blc_ota_enableSecureBoot使能Secure Boot,MCU在上电和OTA过程中加入对firmware的验证过程,保证程序不被篡改,功能对应Secure Boot Application Note中Secure Boot章节。
当芯片使能了Secure Boot,则软件必须调用如下接口配置Secure Boot OTA功能,否则无法进行正常的Secure Boot OTA。
ble_sts_t blc_ota_enableSecureBoot(void);
以下以1MB Flash为例介绍,1MB Flash中默认的描述符位置为0xF8000与0xFA000,每个描述符占用8kB空间。
(1) OTA Client将新的firmware_2和描述符分别烧写到0x20000~0x40000、0x60000的区域。
(2) 第1次OTA:
-
Server上电时从flash的描述符地址0xF8000中读取程序的地址参数(图中以0xF8000中描述符对应程序地址0x00000为例);
-
从对应0x00000区域读程序启动,运行firmware_1;
-
firmware_1运行,初始化的时候将0x20000\~0x40000中程序区域,与另一描述符区域0xFA000\~0xFC000清空,这两个区域将分别作为新firmware和新描述符的存储区。
-
启动OTA,Client通过蓝牙将firmware_2、描述符空运到Server的0x20000~0x40000、0xFA000区域。升级成功后,Server重启。
(3) 将新的firmware_3烧写到OTA Client的0x20000~0x40000的区域。
(4) 第2次OTA:
-
Server上电时从flash的描述符地址0xFA000中读取程序的地址参数(图中以0xFA000中描述符对应程序地址0x20000为例);
-
从对应0x20000区域读程序启动,运行firmware_2;
-
firmware_2运行,初始化的时候将0x00000\~0x20000中程序区域和另一描述符区域0xFA000\~0xFC000清空,这两个区域将分别作为新firmware和新描述符的存储区。
-
启动OTA,Client通过蓝牙将firmware_3、描述符空运到Server的0x00000~0x20000、0xF8000区域。升级成功后,Server重启。
(5) 后面的OTA过程重复上面(1)~ (4)过程,可理解为(2)代表第2n+1次OTA,(3)代表第2n+2次OTA。
OTA更新流程
以上面的Flash存储结构为基础,详细说明OTA程序更新的过程。 与Flash存储架构相对应,OTA更新流程分为传统流程和Secure Boot流程。
下面首先介绍下传统流程。
传统OTA更新流程
仍以高地址0x20000为例,MCU上电后,默认从0地址启动,首先去读flash 0x20的内容,若该值为0x544C4E4B,则从0地址开始搬移代码到RAM,并且之后所有的取值都是从0地址开始,即取值地址 = 0+PC指针的值;若0x20的值不为0x544C4E4B,MCU直接去读0x20020的值,若该值为0x544C4E4B,则MCU从0x20000开始搬代码到RAM,并且之后所有的取值都是从0x20000地址开始,即取值地址 = 0x20000+PC指针的值。
所以只要修改0x20和0x20020标志位的值,即可指定MCU执行FLASH哪部分的代码。
SDK上某一次(2n+1或2n+2)上电及OTA过程为:
(1) MCU上电,通过读0x20和0x20020的值和0x544C4E4B作比较,确定启动地址,然后从对应的地址启动并执行代码。此功能由MCU硬件自动完成。
(2) 程序初始化过程中,读MCU硬件寄存器判断MCU是从哪个地址启动:
若从0启动,将ota_program_offset设为0x20000,并将0x20000区域非0xff的内容全部擦除为0xff,表示下一次OTA获得的新firmware会存入0x20000开始的区域;
若从0x20000启动,将ota_program_offset设为0x0,并将0x0区域非0xff的内容全部擦除为0xff,表示下一次OTA获得的新firmware会存入0x0开始的区域。
(3) Server程序正常运行,OTA Client上电运行,二者建立BLE连接。
(4) 在OTA Client端,OTA Client通过UI触发进入OTA模式(可以是按键、PC 工具写内存等)。OTA Client进入OTA模式后,首先需要获取Server端 OTA service数据Attribute的Attribute Handle的值。
(5) OTA Client获取了Server端 OTA service数据Attribute的Attribute Handle值后,获取当前Server端firmware版本号。
注意
- 若采用legacy protocol则获取版本号需要用户自行实现;若采用extend protocol则获取版本号相关操作已实现。关于legacy与extend protocol的区别用户请参考RF Transfer处理方法。
(6) Client确定要做OTA更新后,先发一个OTA_start命令通知Server进入OTA模式。
(7) Server收到OTA_start命令后,进入OTA模式,等待Client发OTA数据。
(8) Client从0x20000开始的区域读预先存储好的firmware,不间断的向Server发送OTA数据,直至整个firmware都发过去。
(9) Server接收OTA 数据,向ota_program_offset开始的区域存储。
(10) Client端发完所有的OTA数据后,检查这些数据Server是否都正确收到(调用底层BLE的相关函数判断link layer的数据是否都被正确ack)。
(11) Client确定所有的OTA数据都被Server正确收到后,发送一个OTA_END命令。
(12) Server收到OTA_END命令,将新firmware区域偏移地址0x20(即ota_program_offset+0x20)写为0x544C4E4B,将之前老的firmware存储区域偏移地址0x20的地方改写为0x00000000,表示下一次程序启动后将从新的区域搬代码执行。
(13) Server通过Handle Value Notification将OTA的结果上报给Client。
(14) 将Server reboot,新的firmware生效。
在整个OTA更新过程中,Server会不断检查是否有错包和丢包,同时也会不断检查是否超时(OTA开始的时候启动一个计时),一旦有错包、丢包或超时,Server会认为更新失败,并向对方发送失败的原因,使用之前的firmware。
以上流程Server端相关操作在SDK上已经实现,用户不需要添加任何东西,Client端需要额外的程序设计,后面会详细介绍。
Secure Boot OTA更新流程
首先Secure Boot中多地址启动机制为(以1MB Flash为例,两个描述符分别对应前两个启动地址0x00000和0x20000):MCU上电后,默认从0xF8000读取描述符,首先去读flash 0xF8000的内容,若该值为0x544C4E4B且描述符内容校验通过,则从0地址开始搬移代码到RAM,并且之后所有的取值都是从0地址开始,即取值地址 = 0+PC指针的值;若0xF8000的值不为0x544C4E4B,MCU直接去读0xFA000的值,若该值为0x544C4E4B且描述符内容校验通过,则MCU从0x20000开始搬代码到RAM,并且之后所有的取值都是从0x20000地址开始,即取值地址 = 0x20000+PC指针的值。
所以只要修改0xF8000和0xFA000标志位的值,即可指定MCU执行FLASH哪部分的代码。
SDK上某一次(2n+1或2n+2)上电及OTA过程为:
(1) MCU上电,通过读0xF8000和0xFA000的值和0x544C4E4B作比较,确定启动地址,完成程序校验,然后从对应的地址启动并执行代码。此功能由MCU硬件自动完成。
(2) 程序初始化过程中,读MCU硬件寄存器判断MCU是从哪个地址启动:
若从0启动,将ota_program_offset设为0x20000,并将0x20000区域非0xff的内容全部擦除为0xff,表示下一次OTA获得的新firmware会存入0x20000开始的区域;
若从0x20000启动,将ota_program_offset设为0x0,并将0x0区域非0xff的内容全部擦除为0xff,表示下一次OTA获得的新firmware会存入0x0开始的区域。
(3) Server程序正常运行,OTA Client上电运行,并与Server建立BLE连接。
(4) 在OTA Client端,OTA Client通过UI触发进入OTA模式(可以是按键、PC 工具写内存等)。OTA Client进入OTA模式后,首先需要获取Server端 OTA service数据Attribute的Attribute Handle的值(可以Server事先和Client约定好,也可以通过Read_by_type获取这个handle值)。
(5) OTA Client获取了Server OTA service数据Attribute的Attribute Handle值后,获取当前Server FLASH程序的firmware版本号。
注意
- 若采用legacy protocol则获取版本号需要用户自行实现;若采用extend protocol则获取版本号相关操作已实现。关于legacy与extend protocol的区别用户可参考7.3.2小节。
(6) Client确定要做OTA更新后,先发一个OTA_start命令通知Server进入OTA模式。
(7) Server收到OTA_start命令后,进入OTA模式,等待Client发OTA数据。
(8) Client从0x60000开始的区域读预先存储好的描述符,将描述符中的public key和signature发送给Server。
(9) Server收到public key和signature后,会对public key进行校验,如果校验失败,则退出OTA,如果校验成功将public key、signature、新的firmware运行地址信息等写入到新描述符区域对应位置。
(10) Client从0x20000开始的区域读预先存储好的firmware,不间断的向Server发送OTA数据,直至整个firmware都发过去。
(11) Server接收OTA 数据,向ota_program_offset开始的区域存储。
(12) Client端发完所有的OTA数据后,检查这些数据Server是否都正确收到(调用底层BLE的相关函数判断link layer的数据是否都被正确ack)。
(13) Client确定所有的OTA数据都被Server正确收到后,发送一个OTA_END命令。
(14) Server收到OTA_END命令,对新firmware进行验签,验签通过后将新描述符存储区域偏移地址0x0写为0x544C4E4B,将之前老的描述符存储区域偏移地址0x0的地方改写为0x00000000,表示下一次程序启动后将从新的描述符区域读取执行。
(15) Server通过Handle Value Notification将OTA的结果上报给Client。
(16) 将Server reboot,新的firmware生效。
在整个OTA更新过程中,Server会不断检查是否有错包、丢包、超时(OTA开始的时候启动一个计时),同时也会检查是否公钥错误、验签错误。一旦有错包、丢包、公钥错误、验签错误或超时,Server会认为更新失败,并向对方发送失败的原因,使用之前的firmware。
以上流程Server端相关操作在SDK上已经实现,用户不需要添加任何东西,Client端需要额外的程序设计,后面会详细介绍。
修改Firmware Size和Boot Address
API blc_ota_setFirmwareSizeAndBootAddress支持修改启动地址。这个启动地址指的是OTA设计中除了0地址外另一个存储New_firmware的地址(只能是0x20000、0x40000或0x80000)。
Firmware_Boot_address | Firmware size (max)/K |
---|---|
0x20000 | 124 |
0x40000 | 252 |
0x80000 | 508 |
SDK中默认的最大firmware size为252K(由于一些特殊的原因,启动地址为0x40000的firmware size不得大于252K),对应的启动地址为0x00000和0x40000。这两个值和前文的描述一致,用户在设置时需要遵循表7-1启动地址与firmware_size大小的约束关系,如果最大firmware_size发生变化,超过了124K,此时需要将启动地址挪到0x40000(size最大不得超过252K),同理如果firmware_size超过252K,需要将启动地址挪到0x80000(size最大不得超过508K),比如最大firmware size可能到200K 用户可以调用API blc_ota_setFirmwareSizeAndBootAddress来进行设置:
ble_sts_t blc_ota_setFirmwareSizeAndBootAddress(int firmware_size_k, multi_boot_addr_e boot_addr);
注意
- 该API的调用必须在sys_init函数前。
参数multi_boot_addr_e表示可供选择的启动地址,共有三种选项:
typedef enum{
MULTI_BOOT_ADDR_0x20000 = 0x20000, //128 K
MULTI_BOOT_ADDR_0x40000 = 0x40000, //256 K
MULTI_BOOT_ADDR_0x80000 = 0x80000, //512 K
};
返回值ble_sts_t表示设置的状态,关于该类型的定义可参考SDK中ble_common.h。
若成功成功,则返回BLE_SUCCESS;否则返回SERVICE_ERR_INVALID_PARAMETER。
OTA模式RF数据处理
Attribute Table中OTA的处理
Server端在Attribute Table中添加OTA的相关内容,其中OTA数据Attribute的att_readwrite_callback_t中的参数w设为otaWrite,将属性设为Read和Write_without_Rsp。OTA Client默认采用Write Command发数据,不需要Server返回ack(速度会更快)。若Client采用Write Request发数据,需要更改gatt的特性权限为允许Server端响应(CHAR_PROP_WRITE_WITHOUT_RSP更改为CHAR_PROP_WRITE)。
// OTA attribute values
static const u8 my_OtaCharVal[19] = {
CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP,
U16_LO(OTA_CMD_OUT_DP_H), U16_HI(OTA_CMD_OUT_DP_H),
TELINK_SPP_DATA_OTA, };
{5,ATT_PERMISSIONS_READ, 2,16,(u8*)(&my_primaryServiceUUID), (u8*)(&my_OtaServiceUUID), 0},
{0,ATT_PERMISSIONS_READ, 2, sizeof(my_OtaCharVal),(u8*)(&my_characterUUID), (u8*)(my_OtaCharVal), 0}, //prop
{0,ATT_PERMISSIONS_RDWR,16,sizeof(my_OtaData),(u8*)(&my_OtaUUID), (&my_OtaData), &otaWrite, NULL}, //value
{0,ATT_PERMISSIONS_RDWR,2,sizeof(otaDataCCC),(u8*)(&clientCharacterCfgUUID), (u8*)(otaDataCCC), 0},
{0,ATT_PERMISSIONS_READ, 2,sizeof (my_OtaName),(u8*)(&userdesc_UUID), (u8*)(my_OtaName), 0},
Client向Server发送OTA数据时,实际是向上面第3个Attribute写数据,Client需要知道这个Attribute在整个Attribute Table中的Attribute Handle。
OTA Protocol
目前OTA架构对功能进行了扩展并且兼容以前旧版本的协议,整个OTA协议包含了Legacy protocol和Extend protocol两个部分:
OTA Protocol | - |
---|---|
Legacy protocol | Extend protocol |
注意
- OTA protocol支持的功能:
(1) OTA Result feedback function:该功能不可选,默认添加;
(2) FirmWare Version Compare function和Big PDU function:该功能可选,可不添加。需要注意一点其中的版本号比较功能在Legacy protocol和Extend protocol中实现有所区别,具体可参考下文OTA_CMD部分介绍。
下面的介绍均围绕Legacy和Extend protocol进行介绍。
OTA_CMD组成
OTA中CMD的PDU如下:
OTA Command Payload | - |
---|---|
Opcode (2 octet) | Cmd_data (0-18 octet) |
Opcode
Opcode | Name | Use* |
---|---|---|
0xFF00 | CMD_OTA_VERSION | Legacy |
0xFF01 | CMD_OTA_START | Legacy |
0xFF02 | CMD_OTA_END | All |
0xFF03 | CMD_OTA_START_EXT | Extend |
0xFF04 | CMD_OTA_FW_VERSION_REQ | Extend |
0xFF05 | CMD_OTA_FW_VERSION_RSP | Extend |
0xFF06 | CMD_OTA_RESULT | All |
0xFF10~0xFF17 | CMD_OTA_SB_PUBKEY_SIGN | All |
注意
- Use:识别在Legacy protocol、Extend protocol或两者中均可使用的命令;
- Legacy:只在Legacy protocol中使用;
- Extend:只在Extend protocol中使用;
- All: 在Legacy protocol和Extend protocol中均可使用。
- CMD_OTA_SB_PUBKEY_SIGN:仅在Secure Boot使能时使用
(1) CMD_OTA_VERSION
该命令为获得Server当前firmware版本号的命令,用户若采用OTA Legacy protocol进行OTA升级,可以选择使用,在使用该命令时,可通过Server端预留的回调函数来完成firmware版本号的传递。
void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);
Server端在收到CMD_OTA_VERSION命令时会触发该回调函数。
(2) CMD_OTA_START
该命令为OTA升级开始命令,Client发这个命令给Server,用来正式启动OTA更新。该命令仅供Legacy Protocol进行使用,用户若采用OTA Legacy protocol,则必须使用该命令。
(3) CMD_OTA_END
该命令为结束命令,OTA 中的legacy和extend protocol均采用该命令为结束命令,当Client确定所有的OTA数据都被Server正确接收后,发送OTA end命令。为了让Server再次确定已经完全收到了Client所有数据(double check,确保正确性),OTA end命令后面带4个有效的bytes,后面详细介绍。
- | CMD_data | - |
---|---|---|
Adr_index_max (2 octets) | Adr_index_max_xor (2 octets) | Reserved (16 octets) |
-
Adr_index_max:最大的adr_index值
-
Adr_index_max_xor:Adr_index_max的异或值,供校验使用
-
Reserved:保留供以后功能扩展使用
(4) CMD_OTA_START_EXT
该命令为extend protocol中的OTA升级开始命令,Client发这个命令给Server,用来正式启动OTA更新。用户若采用OTA extend protocol则必须采用该命令作为开始命令。
- | CMD_data | - |
---|---|---|
Length (1 octets) | Version_compare (1 octets) | Reserved (16 octets) |
-
Length:PDU length。
-
Version_compare:0x01:开启版本比较功能;0x00:关闭版本比较功能。
-
Reserved:保留供以后扩展使用。
(5) CMD_OTA_FW_VERSION_REQ
该命令为OTA升级过程中的版本比较请求命令,该命令由Client发起给Server端,请求获取版本号和升级许可。
- | CMD_data | - |
---|---|---|
version_num (2 octets) | version_compare (1 octets) | Reserved (16 octets) |
-
Version_num:Client端待升级的Firmware版本号。
-
Version_compare:0x01:开启版本比较功能;0x00:关闭版本比较功能。
-
Reserved:保留供以后扩展使用。
(6) CMD_OTA_FW_VERSION_RSP
该命令为版本响应命令,Server端在收到Client发来的版本比较请求命令(CMD_OTA_FW_VERSION_REQ)后,会将已有的firmware版本号与Client端请求升级的版本号进行对比,确定是否升级,相关信息通过该命令返回发送给Client。
- | CMD_data | - |
---|---|---|
version_num (2 octets) | version_accept (1 octets) | Reserved (16 octets) |
-
Version_num:Server端当前运行的firmware版本号。
-
Version_accept:0x01:接受Client端升级请求;0x00:拒绝Client端升级请求。
-
Reserved:保留供以后扩展使用。
(7) CMD_OTA_SB_PUBKEY_SIGN
该命令为public key和signature传输命令,Server端在收到Client发来的版本public key和signature传输命令(CMD_OTA_SB_PUBKEY_SIGN)后,会将已有的public key与Client端发送的public key进行对比,确定是否允许OTA。该命令总共分为8包发送,每包发送16 bytes。
CMD_data |
---|
data (16 octets) |
(8) CMD_OTA_RESULT
该命令为OTA结果返回命令,OTA结束后Server会将结果信息发送给Client,在整个OTA过程中,无论成功或失败,OTA_result只会上报一次,用户可根据返回的结果来判断升级是否成功。
CMD_data | - |
---|---|
Result (1 octets) | Reserved (16 octets) |
Result:OTA结果信息,所有可能的返回结果如下表所示:
Table: OTA所有可能的返回结果
Value Type info
0x00 OTA_SUCCESS success
0x01 OTA_DATA_PACKET_ OTA data packet sequence number error: repeated OTA PDU or lost some OTA PDU SEQ_ERR
0x02 OTA_PACKET_INVALID invalid OTA packet: 1. invalid OTA command; 2. addr_index out of range; 3.not standard OTA PDU length
0x03 OTA_DATA_CRC_ERR packet PDU CRC err
0x04 OTA_WRITE_FLASH_ERR write OTA data to flash ERR
0x05 OTA_DATA_INCOMPLETE lost last one or more OTA PDU
0x06 OTA_FLOW_ERR peer device send OTA command or OTA data not in correct flow
0x07 OTA_FW_CHECK_ERR firmware CRC check error
0x08 OTA_VERSION_ the version number to be update is lower than the current version COMPARE_ERR
0x09 OTA_PDU_LEN_ERR PDU length error: not 16*n, or not equal to the value it declare in "CMD_OTA_START_EXT" packet
0x0a OTA_FIRMWARE_ firmware mark error: not generated by telink's BLE SDK MARK_ERR
0x0b OTA_FW_SIZE_ERR firmware size error: no firmware_size; firmware size too small or too big
0x0c OTA_DATA_PACKET_ time interval between two consequent packet exceed a value(user can adjust this value) TIMEOUT
0x0d OTA_TIMEOUT OTA flow total timeout
0x0e OTA_FAIL_DUE_TO_ OTA fail due to current connection terminate(maybe connection timeout or local/peer device terminate connection) CONNECTION _TERMIANTE
0x0f OTA_MCU_NOT MCU does not support this OTA mode _SUPPORTED
0x10 OTA_LOGIC_ERR software logic error, please contact FAE of Telink
0x80 OTA_SECBOOT_HW_ERR OTA server device hardware error
0x81 OTA_SECBOOT OTA server device system error _SYSTEM_ERR
0x82 OTA_SECBOOT_FUNC OTA server device do not enable secure boot function _NOT_ENABLE
0x83 OTA_SECBOOT_PUBKEY OTA public key & signature sequence number error: repeated or lost _SIGN_SEQ_ERR
0x84 OTA_SECBOOT_PUBKEY OTA public key & signature data packet length error _SIGN_LEN_ERR
0x85 OTA_SECBOOT_PUBLIC OTA client public key not match OTA server device local hash _KEY_ERR
0x86 OTA_SECBOOT_SIGN OTA signature verification fail _VERIFY_FAIL
0x87 OTA_SECBOOT_WRITE write secure boot descriptor fail _DESC_FAIL
0x88 OTA_SECBOOT_NEW_FW secure boot function: new firmware not match old firmware _NOT_MATCH_OLD_FW 1. old firmware enable secure boot, but new firmware do not enable 2. old firmware do not enable secure boot, but new firmware enable
0x89 OTA_FWENC_NEW_FW firmware encryption function: new firmware not match old firmware _NOT_MATCH_OLD_FW 1. old firmware enable firmware encryption, but new firmware do not enable 2. old firmware do not enable firmware encryption, but new firmware enable
Other Reserved for future use /
注意
- 0x80~0x89只适用于Secure Boot模式中。
OTA Packet结构组成:
Client在采用Write Command或Write Request向Server端发命令和数据时,ATT层有关的Attribute Handle的值为Server端OTA数据的handle_value。根据BLE Spec L2CAP 层有关PDU format的规范,Attribute Value长度定义为下图中的OTA_DataSize部分。
-
DLE Size: CID + Opcode + Att_Handle + Adr_index + OTA_PDU + CRC
-
MTU_Size: Opcode + Att_Handle + Adr_index + OTA_PDU +CRC
-
OTA_Data_Size: Adr_index + OTA_PDU + CRC
OTA_Data介绍:
Type | Length |
---|---|
Default + BigPDU | 16octets -240octets(n*16,n=1..15) |
注意
- Default:OTA PDU长度固定,默认大小为16 octets。
- BigPDU:OTA PDU 长度可更改范围为16 octets – 240 octets,且为16字节整数倍。
OTA_PDU Format:
当用户采用OTA 中的Extend protocol,支持Big PDU,即可支持长包进行OTA升级操作,减少OTA升级的时长, 用户可根据需要在Client端自定义设置PDU大小。最后两个byte是将前面的Adr_Index和Data进行一个CRC_16计算得到第一个CRC的值,Server收到OTA data后,会进行同样的CRC计算,只有两者计算的CRC吻合时,才认为这是一个有效数据。
- | OTA PDU | - |
---|---|---|
Adr_Index (2 octets) | Data(n*16 octets) n=1..15 | CRC (2 octets) |
(1) PDU 包长度: n = 1;
Data:16 octets;
Adr_Index与firmware address的映射关系:
Adr_Index | Firmware_address |
---|---|
0x0001 | 0x0000 - 0x000F |
0x0002 | 0x0010 - 0x001F |
……. | …… |
XXXX | (XXXX -1)*16 - (XXXX)*16+15 |
(2) PDU 包长度:n = 2;
Data : 32 octets;
Adr_Index与firmware address的映射关系:
Adr_Index | Firmware_address |
---|---|
0x0001 | 0x0000 - 0x001F |
0x0002 | 0x0010 - 0x003F |
……. | …… |
XXXX | (XXXX -1)*32 - (XXXX)*32+31 |
(3) PDU 包长度:n=15;
Data : 240 octets
Adr_Index与firmware address的映射关系:
Adr_Index | Firmware_address |
---|---|
0x0001 | 0x0000 - 0x00EF |
0x0002 | 0x0010 - 0x01DF |
……. | …… |
XXXX | (XXXX -1)240 - (XXXX)240+239 |
注意
- 在OTA升级过程中,发送的每包PDU length需16字节对齐,即当最后一包中的OTA有效数据小于16字节时,采用添加0xFF数据进行补全对齐,例如:
a) 当前的PDU length设置为32,最后一包的有效数据PDU为4 octets,则需要添加12 octets的0xFF进行对齐;
b) 当前的PDU length设置为48,最后一包的有效数据PDU为20 octets,则需要添加12 octets的0xFF进行对齐;
c) 当前的PDU length设置为80,最后一包的有效数据PDU为52 octets,则需要添加12octets的0xFF进行对齐。
- 关于不同PDU大小对应的抓包记录,用户可联系Telink技术支持进行获取。
RF Transfer处理方法
Client端通过L2CAP层的Write Command或Write Request向Server发命令和数据,Spec规定在收到Write Request后必须返回Write Response。
下面将分别对Legacy Protocol和Extend Protocol、以及OTA的Version Compare的流程进行介绍,阐述整个RF Transform中Server和Client的交互过程。
OTA Legacy Protocol流程:
OTA Legacy兼容Telink上一版的OTA协议,为更好说明Server端和Client端整个交互过程,下面采用举例说明:
注意
- PDU 长度采用默认的16 octets大小,不涉及DLE长包的操作。
- Firmware compare功能不选择。
具体的的操作流程如下图所示:
Client端首先给Server端发送CMD_OTA_START命令,Server端在收到命令后开始准备接收OTA数据,接着Client端开始发送OTA_Data,该过程中若出现任何交互失败,Server端会向Client端发送CMD_OTA_Result即返回错误信息,重新运行原程序但不会进入reboot,Client端收到后会立即停止OTA数据传输。若Client端和Server端成功完成了OTA_Data传输,则Client端会向Server端发送CMD_OTA_END,Server端在收到后返回结果信息发送CMD_OTA_Result给Client端,并且进入reboot,运行新的firmware。
OTA Extend Protocol流程:
如前文所述,OTA Extend与上面介绍的Legacy的交互命令存在部分区别,为更好说明Server端和Client端整个交互过程,下面采用举例说明:
注意
- PDU 长度采用64 octets大小,涉及DLE长包的操作。
- Firmware compare功能不选择。
由于采用了DLE长包功能,Client端首先需要与给Server端进行MTU和DLE交互,然后接下来的流程和前面Legacy类似,Client端向Server端发送CMD_OTA_START_EXT命令,Server端在收到命令后开始准备接收OTA数据,接着Client端开始发送OTA_Data,该过程中若出现任何交互失败,Server端会向Client端发送CMD_OTA_Result即返回错误信息,重新运行原程序但不会进入reboot,Client端收到后会立即停止OTA数据传输。若Client端和Server端成功完成了OTA_Data传输,则Client端会向Server端发送CMD_OTA_END,Server端在收到后返回结果信息发送CMD_OTA_Result给Client端,并且进入reboot,运行新的firmware。
OTA Version Compare流程:
在Server端,Extend和 Legacy Protocol都具有版本比较功能,其中Legacy预留了接口,需要用户自行实现,而Extend中已经实现了版本比较的功能,用户可以直接使用。
下面将Extend中具有版本比较功能的交互流程进行举例说明:
注意
- PDU 长度采用16 octets大小,不涉及DLE长包的操作。
- Firmware compare功能选择 (OTA待升级版本号为0x0001,使能版本比较功能)。
在使能版本比较功能后,Client端首先给Server端发送CMD_OTA_FW_VERSION_REQ版本比较请求命令,其中发送的PDU中包括Client端firmware版本号(new_fw_version = 0x0001),Server端获取Client端的版本号信息并与本地版本号(local_version)进行对比:
若接收的版本号(new_fw_version = 0x0001)不大于本地版本号(local version = 0x0001),则Server端会拒绝Client端OTA升级请求,发送给Client端版本号响应的命令(CMD_OTA_FW_VERSION_RSP),发送的信息包括接收参数(accept = 0)和本地版本号(local_version = 0x0001),Client在收到后停止OTA相关操作,即当前版本升级不成功。
若接收的版本号(new_fw_version = 0x0001)大于本地版本号(local version = 0x0000),则Server端会接收Client端OTA升级请求,发送给Client端版本号响应的命令(CMD_OTA_FW_VERSION_RSP),发送的信息包括接收参数(accept = 1)和本地版本号(local_version = 0x0000),Client在收到后开始准备OTA升级相关操作,过程与前文类似,即首先向Server端发送CMD_OTA_START命令,Server端在收到命令后开始准备接收OTA数据,接着Client端开始发送OTA_Data,该过程中若出现任何交互失败,Server端会向Client端发送CMD_OTA_Result即返回错误信息,重新运行原程序但不会进入reboot,Client端收到后会立即停止OTA数据传输。若Client端和Server端成功完成了OTA_Data传输,则Client端会向Server端发送CMD_OTA_END,Server端在收到后返回结果信息发送CMD_OTA_Result给Client端,并且进入reboot,运行新的firmware。
OTA具体实现:
上面介绍了整个OTA交互流程,下面举例说明一下Client和Server具体的数据交互实现:
注意
- OTA Protocol:Legacy Protocol;
- PDU 长度采用16octets大小,不涉及DLE长包的操作;
- Client端开启firmware compare功能。
(1) 检测是否有触发进入OTA模式的行为,一旦检测到该行为,进入OTA模式。
(2) Client向Server传送OTA命令和数据,需要知道Server端当前OTA数据的Attribute的Attribute Handle值。
若用户采用事先约定好的方式,直接定义该值;
若没有事先约定好,采用Read By Type Request的方式获得这个Attribute Handle值。
Telink所有BLE SDK 的OTA data的UUID都是16bytes,且永远都是下面这个值:
##define TELINK_SPP_DATA_OTA {0x12,0x2B,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00}
在Client的Read By Type Request中将Type设置为这16个bytes的UUID,Server端回复的Read By Type Rsp中可以查到OTA UUID所在的这个Attribute Handle,如下图所示,Client可以查到Attribute Handle的值为0x0031。
(3) 获取Server当前firmware版本号,决定是否要继续做OTA更新(若版本已经最新,不需要更新)。这一步为用户自己选择是否要做。tl_ble_sdk Legacy protocol并没有实现版本号的传送。用户可以使用write cmd或write response的形式通过OTA version cmd向Server传送一个获取OTA version的请求,Server端在收到OTA version请求的时候在回调函数里将Server端的版本号传送给Client(如手动送一个NOTIFY/INDICATE的数据)。
(4) 启动OTA开始的一个计时,后面要不断检测该计时是否超过30秒(这只是个参考时间,实际所需时间根据用户测试的正常OTA需要多少时间进行评估)。
如果超过30秒认为OTA超时失败,因为Server端收到OTA数据后会校验CRC,一旦CRC错误或者出现其他错误(如烧写flash错误),就会认为OTA失败。
(5) 读取Client Flash 0x20018~0x2001b四个字节,确定firmware的size。
这个size是由编译器实现的,假设firmware的size为20k = 0x5000,那么firmware的0x18 ~ 0x1b的值为0x00005000,所以在0x20018~0x2001b可以读到firmware的大小。
如下图所示的bin文件,0x18 ~ 0x1b内容为0x0000cf94,所以大小为0xcf94 = 53140Bytes,从0x0000 到 0xcf96。
(6) Client向Server发一个OTA start命令,通知Server进入OTA模式,等待Client端的OTA数据,如下图所示。
(7) 从Client flash 0x20000区域开始每次读16个byte的firmware,填入OTA data packet,设置对应的adr_index,并计算CRC值,将packet push到TX fifo,一直到firmware size最后一个16 bytes为止,将firmware所有的数据全部发送给Server。
数据发送方法如上文介绍,使用OTA data的格式,有效数据为20 bytes,前两个bytes放adr_index,紧跟16个有效的firmware数据,最后两个是前18个数据的CRC计算值。
注意,如果firmware最后一组数据不是16字节对齐,需要将剩余的部分按0xff补对齐,计算CRC的时候需要将补充的数据计算进去。
结合上图所示的bin文件来详细介绍OTA数据如何拼装。
第一组数据:adr_index为0x00 00,16个数据为0x0000 ~0x000f地址的值,然后这18个数据计算CRC,假设CRC结果为 0xXYZW,那么20 bytes排列为:
0x00 0x00 0xf3 0x22 ......省略12个bytes...... 0x60 0x15 0xZW 0xXY
第二组数据:
0x01 0x00 0x21 0xa8 ......省略12个bytes...... 0x00 0x00 0xJK 0xHI
第三组数据:
0x02 0x00 0x4b 0x4e ......省略12个bytes...... 0x81 0x7d 0xNO 0xLM
........
倒数第二组数据:
0xf8 0x0c 0x20 0xa1 ......省略12个bytes...... 0xff 0xff 0xST 0xPQ
最后一组数据:
0xf9 0x0c 0xec 0x6e 0xdd 0xa9 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xWX 0xUV
12 “0xff” are added to complement 16 bytes.
12个0xff为补齐的数据。
0xec 0x6e 0xdd 0xa9为第3个~第6个,它是整个firmware bin的CRC_32校验结果。Server在OTA升级过程中会同步计算接收到的整个bin的CRC_32校验值,在结束时会与0xec 0x6e 0xdd 0xa9进行比较。
0xf9 ~0xff 共18个bytes的CRC计算结果为 0xUVWX。
上面的数据如下图所示:
(8) Firmware数据发送完毕后,检查BLE link layer的数据是否已经完全发送出去(因为只有当link layer的数据被Server ack了,才会认为该数据发送成功)。若完全发送出去,Client发送一个OTA_end命令,通知Server所有数据已发送完毕。
OTA_end的packet有效字节设为6个,前两个为0xff02;中间的两个bytes为新的firmware最大的adr_index值(这个是为了让Server端再次确认没有丢掉最后一条或几条OTA数据);最后两个bytes为中间最大的adr_index值的取反,相当于一个简单的校验。OTA_end不需要CRC校验。
以上图所示的bin为例,最大的adr_index为0x0cf9,其取反值为0xf306,最终的OTA end包如上图所示。
(9) 检查Client端link layer TX fifo是否为空。若为空,说明之前所有的数据和命令都已成功发送出去,即Client端的OTA任务已经全部完成。 CRC_16计算函数见本文档后面的“附录1:crc16算法”。
按照前面所述,Server端在OTA Attribute中直接调用otaWrite和otaRead即可,Client端发送过来的write command命令,BLE协议栈会自动解析并最终调用到otaWrite函数进行处理。
在otaWrite函数里对packet 20 bytes的数据进行解析,首先判断是OTA CMD还是OTA data。对OTA CMD进行相应的响应,对OTA数据进行CRC校验并烧写到Flash对应位置。
Server端OTA相关的操作为:
(1) 收到OTA version命令(OTA_FIRMWARE_VERSION命令):
Client要求获得Server firmware版本号,该BLE SDK收到这个命令时,不做处理,只是根据用户是否注册了收到version的回调函数,判断是否触发回调函数。
在ota.h中看到注册该回调函数的接口为:
typedef void (*ota_versionCb_t)(void);
void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);
(2) 收到OTA_start命令:
此时Server进入OTA模式。
若用户使用blc_ota_registerStartCmdCb函数注册了OTA start时的回调函数,则执行此函数,这个函数的目的是让用户在进入OTA模式后,修改一些参数状态等,比如将PM关掉(使得OTA数据传输更加稳定)。另外Server启动并维护一个slave_adr_index,初值为-1,记录最近一次正确OTA data的adr_index,用于判断整个OTA过程中是否有丢包。一旦丢包,认为OTA失败,退出OTA,上报结果,MCU重启。Client端由于收不到Server的ack包,也会由于OTA任务超时使得软件发现OTA失败。
注册OTA start的回调函数:
typedef void (*ota_startCb_t)(void);
void blc_ota_registerOtaStartCmdCb(ota_startCb_t cb);
用户需要注册这个回调,以便在OTA start时进行操作,比如配置LED灯的特殊闪烁方式来指示OTA正在进行。
Server这端一旦收到OTA_start命令,开始OTA后,也会启动两个计时。一个是OTA整个过程完成的超时时间,目前SDK中默认是30s,如果30s之内OTA还没有完成,就认为OTA_TIMEOUT失败。实际操作时,用户最后需要根据自己的firmware大小(越大越耗时)和Client端BLE数据带宽(太窄的话会影响OTA速度)修改这个默认的30s。SDK提供修改的变量为:
ble_sts_t blc_ota_setOtaProcessTimeout(int timeout_second);
ble_sts_t blc_ota_setOtaDataPacketTimeout(int timeout_second);
函数blc_ota_setOtaProcessTimeout的参数timeout_second的单位为秒,默认为30,范围是5-1000;
函数blc_ota_setOtaDataPacketTimeout的参数timeout_second的单位为秒,默认为5,范围是1-20;
初始化该变量后,用户可调用下面的的超时函数来进行超时判断处理:
void blt_ota_procTimeout(void);
另外一个是receive packet的超时时间,每收到一次OTA数据包都会更新一次,超时时间为5s,即5s内没有收到下一笔数据则认为OTA_RF_PACKET_TIMEOUT失败。
(3) 收到有效的OTA数据(前两bytes为0~0x1000):
这个范围的值表示具体的OTA data。
每次Server收到一个20 bytes 的OTA data packet,先看adr_index是否等于slave_adr_index的值加1。若不等,说明丢包,OTA失败;若相等,更新slave_adr_index的值。
然后对前18 byte的内容进行CRC_16的校验。若不匹配,OTA失败;若匹配,则将16 byte的有效数据写到flash对应位置ota_program_offset+adr_index*16 ~ ota_program_offset+adr_index*16 + 15。在写flash的过程中,如果出错,OTA也失败。
为了保证OTA完成后firmware的完整性,在OTA最后,会对整个firmware进行CRC_32校验。并与Client发送过来同样方法计算得到的校验值进行比较,不相等的话说明中间有数据出错,认为OTA失败。
(4) 收到OTA end:
检查OTA end包中的adr_max和其取反校验值是否正确。若正确,则adr_max可以用来做double check。double check的时候,判断Server之前收到的Client的数据index最大值与该包中的adr_max是否相等。若相等,认为OTA成功,若不等,认为丢掉了最后一笔或几笔数据,OTA不完整。当OTA成功的时候,Server将老的firmware所在地址的flash启动标志设为0,将新的firmware所在地址的flash启动标志设为0x4b,将MCU reboot。
(5) Server 发送OTA结果反馈给Client:
Server端一旦启动OTA,不管OTA是成功还是失败,最后Server都会将结果发送给Client。如下是OTA成功后Server发送结果信息示例(长度只有3个byte):
(6) Server提供OTA状态的回调函数:
Server端一旦启动OTA,在OTA成功时会将MCU reboot:
若成功,会在reboot前设置flag通知MCU再次启动后运行New_firmware;
若OTA失败,会将错误的新程序擦掉后重新启动,运行Old_firmware。
在MCU reboot前,根据用户是否注册了OTA状态回调函数,来决定是否触发该函数。
以下是相关code:
void blc_ota_registerOtaResultIndicationCb (ota_resIndicateCb_t cb);
设置了回调函数后,回调函数参数result的enum与OTA上报的结果一样,第一个0是OTA成功,其余是不同的失败原因。
OTA升级成功或失败均会触发该回调函数,实际代码中可以通过该函数的结果返回参数来进行debug。当OTA不成功时,用户可以在读到上面的result后,通过执行while(1)使MCU停止运行,便于了解当前OTA失败的原因。
注意
- 1) 如果固件大小超过256KB,需在sys_init()前调用API blc_ota_setFirmwareSizeAndBootAddress设置固件大小和多地址启动地址。
- 2) 如果当前程序使能Flash protection功能,则不可升级为没有使能Flash protection的程序,因为Flash已经被加锁,更新后的程序没有解锁操作。
Q&A
-
Q: 在编译完成时,从编译日志中看到 bin size is greater 256KB, please refer to handbook!
-
A: OTA 功能与多地址启动功能相关,Telink B91 系列芯片的默认启动地址是 0 地址和 256 KB 偏移地址,因此,当编译生成的 Bin Size 超过 256 KB 且需要支持 OTA 功能时,应进行以下配置:
(1). 在 main 函数的开头(sys_init
之前),调用 blc_ota_setFirmwareSizeAndBootAddress
来设置最大固件大小和多地址启动地址;
(2). 关注 .link
文件中的 BIN_SIZE
(即实际的固件大小),避免超过设置的最大固件大小;
(3). 若 BIN_SIZE
超过最大固件大小,则应根据用户实际使用的 Flash 和 Flash 规划进行调整,避免出现内存覆盖问题。
按键扫描
Telink提供了一套基于行列式扫描的keyscan架构,用于按键扫描处理,使用者可以直接使用,也可以自己去实现。
键盘矩阵
定义drive pin数组和scan pin数组:
##define KB_DRIVE_PINS {GPIO_PB3, GPIO_PB5}
##define KB_SCAN_PINS {GPIO_PB6, GPIO_PB7}
keyscan使用的上下拉电阻都使用GPIO的模拟电阻:drive pin选取下拉100K ohm,scan pin选取上拉10K ohm。那么当没有按键按下时,scan pin作为输入,GPIO读到的是被10K ohm上拉的高电平。当扫描开始时,在drive pin上输出低电平,scan pin读到低电平,就表示当前列上有按键按下。(注意:此时drive pin不是float态,若output没打开,scan pin读到的是100K ohm和10K ohm的分压电平,仍是高电平。)
定义行列式扫描中,drive pin输出低电平时scan pin扫描到的有效电平。
##define KB_LINE_HIGH_VALID 0
定义Row和COL的上下拉:
##define MATRIX_ROW_PULL PM_PIN_PULLDOWN_100K
##define MATRIX_COL_PULL PM_PIN_PULLUP_10K
//A<3:0>
analog_write_reg8(0x17, PULL_WAKEUP_SRC_PA0 | (PULL_WAKEUP_SRC_PA1 << 2) | (PULL_WAKEUP_SRC_PA2 << 4) | (PULL_WAKEUP_SRC_PA3 << 6));
//A<7:4>
analog_write_reg8(0x18, PULL_WAKEUP_SRC_PA4 | (PULL_WAKEUP_SRC_PA5 << 2) | (PULL_WAKEUP_SRC_PA6 << 4) | (PULL_WAKEUP_SRC_PA7 << 6));
//B<3:0>
analog_write_reg8(0x19, PULL_WAKEUP_SRC_PB0 | (PULL_WAKEUP_SRC_PB1 << 2) | (PULL_WAKEUP_SRC_PB2 << 4) | (PULL_WAKEUP_SRC_PB3 << 6));
//B<7:4>
analog_write_reg8(0x1a, PULL_WAKEUP_SRC_PB4 | (PULL_WAKEUP_SRC_PB5 << 2) | (PULL_WAKEUP_SRC_PB6 << 4) | (PULL_WAKEUP_SRC_PB7 << 6));
//C<3:0>
analog_write_reg8(0x1b, PULL_WAKEUP_SRC_PC0 | (PULL_WAKEUP_SRC_PC1 << 2) | (PULL_WAKEUP_SRC_PC2 << 4) | (PULL_WAKEUP_SRC_PC3 << 6));
//C<7:4>
analog_write_reg8(0x1c, PULL_WAKEUP_SRC_PC4 | (PULL_WAKEUP_SRC_PC5 << 2) | (PULL_WAKEUP_SRC_PC6 << 4) | (PULL_WAKEUP_SRC_PC7 << 6));
//D<3:0>
analog_write_reg8(0x1d, PULL_WAKEUP_SRC_PD0 | (PULL_WAKEUP_SRC_PD1 << 2) | (PULL_WAKEUP_SRC_PD2 << 4) | (PULL_WAKEUP_SRC_PD3 << 6));
//D<7:4>
analog_write_reg8(0x1e, PULL_WAKEUP_SRC_PD4 | (PULL_WAKEUP_SRC_PD5 << 2) | (PULL_WAKEUP_SRC_PD6 << 4) | (PULL_WAKEUP_SRC_PD7 << 6));
//E<3:0>
analog_write_reg8(0x1f, PULL_WAKEUP_SRC_PE0 | (PULL_WAKEUP_SRC_PE1 << 2) | (PULL_WAKEUP_SRC_PE2 << 4) | (PULL_WAKEUP_SRC_PE3 << 6));
//E<7:4>
analog_write_reg8(0x20, PULL_WAKEUP_SRC_PE4 | (PULL_WAKEUP_SRC_PE5 << 2) | (PULL_WAKEUP_SRC_PE6 << 4) | (PULL_WAKEUP_SRC_PE7 << 6));
在gpio_init时,ie的状态会被默认设为0,由于scan pin需要读取电平,因此需要打开ie:
##define PB1_INPUT_ENABLE 1
##define PB0_INPUT_ENABLE 1
##define PA4_INPUT_ENABLE 1
##define PA0_INPUT_ENABLE 1
##define PE6_INPUT_ENABLE 1
##define PE5_INPUT_ENABLE 1
当MCU进入sleep mode时,需要设置PAD GPIO唤醒。设置drive pin高电平唤醒,按下按键时,drive pin读到100K和10K的分压电平,为10/11 VCC的高电平。需要打开drive pin的ie读取其电平状态:
##define PE2_INPUT_ENABLE 1
##define PB4_INPUT_ENABLE 1
##define PB5_INPUT_ENABLE 1
##define PE1_INPUT_ENABLE 1
##define PE4_INPUT_ENABLE 1
Keyscan and Keymap
Keyscan
按照上面的配置完成后,在main_loop的proc_keyboard函数中调用下面函数完成keyscan。
u32 kb_scan_key (int numlock_status, int read_key)
第一个参数numlock_status在proc_keyboard中调用时设为0即可;只有在deepsleep醒来的快速扫描按键时才会将其设为KB_NUMLOCK_STATUS_POWERON,后面的快速扫键中介绍(对应DEEPBACK_FAST_KEYSCAN_ENABLE)。
第二个参数read_key是keyscan函数按键的缓存处理,这个一般不使用,默认设为1(为0时会将按键值缓存在buffer里,不报告给上层)。
返回值用于通知user当前的按键扫描是否检测矩阵键盘有变化:有变化时,返回1;无变化时,返回0。
kb_scan_key这个函数是在proc_keyboard中调用的,根据BLE时序可知,main_loop的运行时间为adv_interval或conn_interval。广播状态时 (假设adv_interval为30ms),每30ms做一次key scan;连接状态时(假设conn_interval = 10ms),每10ms 做一次key scan。
理论上,当前key scan检测到矩阵上按键的状态和上次key scan的状态不一样时,就认为有变化。
实际代码中开启了一个防抖动滤波处理:只有检测连续两次key scan的按键状态一样,且和上一次存储的最新矩阵按键状态不一样时,才认为是一个有效的按键变化。这时返回1表示按键有变化,并将矩阵按键的状态通过kb_event结构体反映出来,同时将更新当前的按键状态为最新的矩阵按键状态。这部分对应的代码为keyboard.c中的:
unsigned int key_debounce_filter( u32 mtrx_cur[], u32 filt_en );
上面所说的最新按键状态指的是矩阵上所有按键的按下或松开的状态的集合。上电时,默认的第一次矩阵按键状态为所有按键都是松开的。只要经过防抖动滤波处理后的矩阵按键的状态发生任何变化,返回值都会为1,否则返回0。如:按下一个按键返回一个变化;松开一个按键返回一个变化;按下一个键时再按下第二个键返回一个变化;按下两个键时再按下第三个键返回一个变化;按下两个键时松开其中一个键返回一个变化……
Keymap & kb_event
user在调用kb_scan_key看到一个按键变化时,通过一个全局的结构体变量kb_event来获取当前的按键状态。
##define KB_RETURN_KEY_MAX 6
typedef struct{
u8 cnt;
u8 ctrl_key;
u8 keycode[KB_RETURN_KEY_MAX];
}kb_data_t;
kb_data_t kb_event;
kb_event由8个byte构成:
第一个cnt用于指示当前有几个有效的按键被按下;
第二个ctrl_key一般不会用到,只有在做标准的USB HID keyboard时才会用到(keymap中的keycode设为0xe0-0xe7时会触发,所以user千万不要设这8个值)。
keycode[6]用于存储当前最多6个被按下按键的keycode(如果实际按下的键超过6个,只有前6个能反应出来)。
所有按键对应的keycode在app_config.h中定义:
##define KB_MAP_NORMAL { \
{BTN_UNPAIR, BTN_PAIR }, \
{CR_VOL_UP, CR_VOL_DN}, \
}
在kb_scan_key函数内部,每次扫描前会将kb_event.cnt清0,而kb_event.keycode[]这个数组是不清除的。所以当每次返回1表示有变化时,用kb_event.cnt判断当前矩阵按键上有几个有效的按键。
a) kb_event.cnt = 0时,上一次有效矩阵状态kb_event.cnt肯定是不等于0的,但不确定是1、2还是3,这个变化一定是按键释放,不确定是一个键释放还是同时好几个键释放。此时kb_event.keycode[]里面即使有数据,也是无效的,忽略不看。
b) kb_event.cnt = 1,可能上次kb_event.cnt =0,那么按键变化是一个键被按下;可能上次 kb_event.cnt = 2,那么按键变化是两个键中一个被释放;也还有其他可能性,如三个键中两个被同时释放。此时kb_event.keycode[0]表示当前被按下的这个键的键值,后面的keycode忽略不看。
user可以每次在key scan前自己将kb_event.keycode清0,这时就可以根据kb_event.keycode来判断是否有按键变化发生,如下所示。
这个示例只是简单的处理单个按键按下的情况,所以当kb_event.keycode[0]非0时,就认为是一个按键被按下,并不去判断是否两个键同时按下或者两个键中的一个释放等复杂的情况。
int det_key = kb_scan_key(0, 1);
if (det_key) {
key_change_proc();
}
Keyscan Flow
调用kb_scan_key时,一个最基本的keyscan的流程如下:
(1) 第一次全矩阵通扫。
将drive pin全部输出drive电平(0),同时读取所有的scan pin,检查是否能读到有效的电平,并记录哪一列上读到了有效电平(用scan_pin_need标记有效的列号)。
若不使用第一次全矩阵通扫,直接逐行扫的话,至少要进行所有行的扫描,即使没有按键按下也要每次都逐行扫描,比较耗时间。加入了第一次全矩阵通扫后,若没发现任何列上有按键按下,就可以直接退出keyscan,在没有按键按下时会节省很多时间。
第一次全矩阵通扫的code对应如下:
scan_pin_need = kb_key_pressed (gpio);
在kb_key_pressed函数中将所有的行输出低电平,延时20us后(延时是为了等待电平稳定后才读scan pin),设置了一个release_cnt为6。当检测到矩阵上的按键按下并全部释放后,并不是立刻就认为没有按键而不去逐行扫描了,而是最终缓冲6帧,直到发现连续6次都是检测到按键全部释放后不再去逐行扫描。实现了一个key debouce防抖动的处理。
(2) 根据全矩阵通扫的结果,逐行扫描。
全矩阵通扫发现有按键按下时,开始逐行扫描,从ROW0 ~ ROW4逐行输出有效drive电平,读取列上的电平值,找出按键按下的位置。
对应的代码为:
unsigned int pressed_matrix[ARRAY_SIZE(drive_pins)] = {0};
##if (KB_STANDARD_KEYBOARD)
kb_k_mp = kb_p_map[0];
##else
kb_k_mp = (kb_k_mp_t *)&kb_map_normal[0];
##endif
kb_scan_row(0, gpio);
for (unsigned int i = 0; i <= ARRAY_SIZE(drive_pins); i++) {
unsigned int r = kb_scan_row(i < ARRAY_SIZE(drive_pins) ? i : 0, gpio);
if (i) {
pressed_matrix[i - 1] = r;
}
}
在做逐行扫描时使用了一些方法来优化代码执行时间:
一、当对某行drive进行扫描时,并不需要读取全部的列CoL0 ~ CoL5,根据之前通扫的scan_pin_need可以知道哪些列上能够读到有效电平,此时只读取已经被标记的列即可。
二、当对每一行drive进行扫描时,需要20us左右的等待稳定时间,做了一个缓冲处理,把20us的等待时间转化到执行code中,节省了这个时间。具体怎么实现不介绍,请user自行理解。
最终的矩阵按键状态使用u32 pressed_matrix[5](可看出最多支持40列)来存储,pressed_matrix[0]的bit0~bit5标记Row0上CoL0 ~ CoL5是否有按键,......,pressed_matrix[4]的bit0~bit5标记Row4上CoL0 ~ CoL5是否有按键。
(3) 对pressed_matrix[]进行防抖动滤波处理
对应代码为:
unsigned int key_debounce_filter( u32 mtrx_cur[], u32 filt_en);
u32 key_changed = key_debounce_filter( pressed_matrix, (numlock_status & KB_NUMLOCK_STATUS_POWERON) ? 0 : 1);
当在deepsleep状态被唤醒后进行快速按键检测时,numlock_status = KB_NUMLOCK_STATUS_POWERON,此时filt_en = 0,不进行滤波,是为了最快速的获取键值。
其他情况下,filt_en = 1,需要滤波处理。滤波处理的思路是:最近的连续两次pressed_matrix[]一致,且和上一次有效的pressed_matrix[]不一样,才认为是按键矩阵发生了有效的变化,key_changed = 1。
(4) 对pressed_matrix[]进行缓存处理
将pressed_matrix[]存入到缓冲区,当kb_scan_key (int numlock_status, int read_key)中的read_key为1时,立刻读出缓冲区的数据,当read_key为0时,缓冲区数据保存起来,不通知上层,只有等到read_key为1时,才能读出之前缓存的数据。
由于本demo的read_key永远为1,这部分可以忽略不计,相当于缓冲区没有起到作用。具体代码不介绍。
(5) 根据pressed_matrix[],查表KB_MAP_NORMAL,返回键值。
对应的函数为kb_remap_key_code和kb_remap_key_row,不具体介绍,user自行理解。
Repeat Key处理
以上介绍的最基本的keyscan只有在按键状态变化时产生一个变化事件,通过kb_event来读取当前key 值,就无法实现repeat key功能:一个按键一直按着时,需要定时发送一个按键值。
加入repeat key处理,在keyboard.c中配置相关的宏如下:KB_REPEAT_KEY_ENABLE用来打开或关闭repeat key功能,默认这个功能是关闭的。
##ifndef KB_REPEAT_KEY_ENABLE
#define KB_REPEAT_KEY_ENABLE 0
##endif
##ifndef KB_REPEAT_KEY_INTERVAL_MS
#define KB_REPEAT_KEY_INTERVAL_MS 200
##endif
##ifndef KB_REPEAT_KEY_NUM
#define KB_REPEAT_KEY_NUM 4
##endif
(1) KB_REPEAT_KEY_ENABLE
用来打开或关闭repeat key功能。
若要实现repeat key,首先要将KB_REPEAT_KEY_ENABLE设为1。
(2) KB_REPEAT_KEY_INTERVAL_MS
定义repeat key的repeat时间。
若设为200ms,表示当一个键被一直按着时,每过200 ms,kb_key_scan会返回一个变化,并且在kb_event里面给出当前这个按键状态。
(3) KB_REPEAT_KEY_NUM & KB_MAP_REPEAT
定义当前需要repeat的键值。
KB_REPEAT_KEY_NUM定义数量;KB_MAP_REPEAT定义一个map,给出所有需要repeat的keycode,注意这个map中keycode一定要是KB_MAP_NORMAL中的值。
应用举例:
如下所示的一个6*6的矩阵按键,四个宏定义实现的功能是:8个按键UP、DOWN、LEFT、RIGHT、V+、V-、CHN+、CHN-支持repeat,每100ms repeat一次;其他的按键都不支持repeat key。
repeat key代码的实现这里不介绍,在工程上搜索以上四个宏就可以找到所有代码了。
LED管理
LED任务相关调用函数
该BLE SDK提供了一个LED管理的参考代码,以源码提供,user可以直接使用这部分的code或参考其实现方法自己设计。代码在vendor/common/blt_led.c中,user在自己的C file中include vendor/common/blt_led.h即可。若需要使用,先将下面宏改为1:
##define BLT_APP_LED_ENABLE 0
user需要调用的三个函数为:
void device_led_init(u32 gpio,u8 polarity);
int device_led_setup(led_cfg_t led_cfg);
static inline void device_led_process(void);
在初始化的时候使用device_led_init(u32 gpio,u8 polarity)设置当前LED对应的GPIO和极性。极性设为1,表示GPIO输出高电平点亮LED;极性设为0,表示低电平点亮LED。
在main_loop的UI Entry部分添加device_led_process函数,该函数每次检查是否有LED任务没有完成(DEVICE_LED_BUSY),若有任务,去执行相应的操作。
LED任务的配置和管理
定义LED Event
使用如下结构体定义一个LED event:
typedef struct{
unsigned short onTime_ms;
unsigned short offTime_ms;
unsigned char repeatCount;
unsigned char priority;
} led_cfg_t;
onTime_ms和offTime_ms表示当前的LED event保持亮起的时间(ms)和熄灭的时间(ms)。注意它们是用unsigned short定义的,最大65535。
repeatCount表示onTime_ms和offTime_ms定义的一亮一灭的动作持续重复多少次。注意它是用unsigned char定义的,最大255。
priority表示当前LED event的优先级。
当我们要定义一个长亮或长灭的LED event时(没有时间限制,也就是repeatCount不起作用),将repeatCount的值设为255(0xff),此时onTime_ms和offTime_ms里面必须一个是0,一个非0,根据非0来判断是长亮还是长灭。
以下为几个LED event的示例:
(1) 1 Hz的频率闪烁3秒:亮500ms,灭500ms,repeat 3次。
led_cfg_t led_event1 = {500, 500, 3, 0x00};
(2) 4 Hz的频率闪烁50秒:亮125ms,灭125ms,repeat 200次。
led_cfg_t led_event2 = {125, 125, 200, 0x00};
(3) 长亮:onTime_ms非0,offTime_ms为0,repeatCount为0xff。
led_cfg_t led_event3 = {100, 0, 0xff, 0x00};
(4) 长灭:onTime_ms为0,offTime_ms非0,repeatCount为0xff。
led_cfg_t led_event4 = {0, 100, 0xff, 0x00};
(5) 亮3秒后熄灭:onTime_ms为1000,offTime_ms为0,repeatCount为0x3。
led_cfg_t led_event5 = {1000, 0, 3, 0x00};
调用device_led_setup将一个led_event发送给LED任务管理:
device_led_setup(led_event1);
LED Event的优先级
user可以在SDK里定义多个LED event,LED在一个时间点只能执行一个LED event。
这个简单的LED管理没有设置任务列表,当LED空闲时,LED接受user调用device_led_setup建立的任何LED event;当LED busy时(前一个old LED event还没有结束),对于new LED event,对两个LED event的优先级进行比较。若new LED event的优先级高于old LED event的优先级,将old LED event抛弃,开始执行new LED event;若new LED event的优先级低于或等于old LED event的优先级,继续执行old LED event,将new LED event抛弃(注意:是彻底抛弃,并不会将这个LED event 缓存起来后面再处理)。
user可以根据以上的LED event优先级的原则,在自己的应用里定义不同优先级的LED event,实现自己的LED指示效果。
另外,由于LED的管理采用了查询的机制,当DEVICE_LED_BUSY时,不能进入latency生效时的long suspend,如果进入了一个long suspend(比如10ms * 50 = 500ms),会导致onTime_ms较小的值(如250ms)无法得到及时响应,从而影响了LED闪烁的效果。
##define DEVICE_LED_BUSY (device_led.repeatCount)
针对以上问题,需要在blt_pm_proc中作相应的处理:
int user_task_flg = scan_pin_need || key_not_released || DEVICE_LED_BUSY;
if(user_task_flg){
...
bls_pm_setManualLatency(0); // manually disable latency
...
}
软件定时器(Software Timer)
为了方便user做一些简单的定时器任务,Telink BLE SDK提供了blt software timer demo,并且全部提供源码。user可以在理解了该timer的设计思路后直接使用,也可以自己做一些修改设计。
源代码全部在vendor/common/blt_soft_timer.c和blt_soft_timer.h文件中,若需要使用,先将下面宏改为1:
##define BLT_SOFTWARE_TIMER_ENABLE 0 //enable or disable
blt soft timer是基于system tick设计的查询式timer,其准确度无法达到硬件timer那么准,且需要保证在main_loop中一直被查询。
我们约定:blt soft timer的使用场景为定时时间大于5ms、小于44739ms(BIT(30)/SYSTEM_TIMER_TICK_1MS)、且对于时间误差要求不是特别高的情况。
blt soft timer的最大特点是不仅在main_loop中会被查询,也能确保在进入suspend后能够及时唤醒并执行timer的任务,该设计是基于低功耗唤醒部分介绍的“应用层定时唤醒”实现的。
目前设计上最多同时支持4个timer运行,实际user可以修改下面的宏来实现更多或者更少的timer:
##define MAX_TIMER_NUM 4 //timer max number
Timer初始化
调用下面的API进行初始化:
void blt_soft_timer_init(void);
可以看到源码上初始化只是将blt_soft_timer_process注册为应用层提前唤醒的回调函数。
void blt_soft_timer_init(void)
{
bls_pm_registerAppWakeupLowPowerCb(blt_soft_timer_process);
}
Timer的查询处理
blt soft timer的查询处理使用blt_soft_timer_process函数来实现:
void blt_soft_timer_process(int type);
一方面需要确保在main_loop中如下所示位置一直被调用;另一方面被注册为应用层提前唤醒的回调函数,那么每次在suspend中发生定时提前唤醒时,也会快速执行该函数,去处理各timer任务。
int main_idle_loop(void)
{
blc_sdk_main_loop();
##if (FEATURE_TEST_MODE == TEST_USER_BLT_SOFT_TIMER)
blt_soft_timer_process(MAINLOOP_ENTRY);
##endif
}
blt_soft_timer_process参数中的type有如下两种情况:MAINLOOP_ENTRY表示在main_loop中查询进入该函数,CALLBACK_ENTRY表示发生了timer提前唤醒时进入该函数。
##define MAINLOOP_ENTRY 0
##define CALLBACK_ENTRY 1
blt_soft_timer_process的具体实现比较复杂,基本思路如下:
(1) 首先检查当前timer table中是否还有user定义的timer:若没有则直接退出,并关掉应用层定时唤醒;若有timer任务,继续往下运行。
if(!blt_timer.currentNum){
bls_pm_setAppWakeupLowPower(0, 0); //disable
return;
}
(2) 检查时间上最近的一个timer任务是否到达:若没有到达,则退出,否则继续往下运行。在设计层面会保证timer在任何时候都是按照时间排序的,所以这里只要看时间上最近的timer即可。
if( !blt_is_timer_expired(blt_timer.timer[0].t, now) ){
return;
}
(3) 轮询当前所有的timer任务,只要时间达到了就执行timer对应的任务。
for(int i=0; i<blt_timer.currentNum; i++){
if(blt_is_timer_expired(blt_timer.timer[i].t ,now) ){ //timer trigger
if(blt_timer.timer[i].cb == NULL){
}
else{
result = blt_timer.timer[i].cb();
if(result < 0){
blt_soft_timer_delete_by_index(i);
}
else if(result == 0){
change_flg = 1;
blt_timer.timer[i].t = now + blt_timer.timer[i].interval;
}
else{ //set new timer interval
change_flg = 1;
blt_timer.timer[i].interval = result * SYSTEM_TIMER_TICK_1US;
blt_timer.timer[i].t = now + blt_timer.timer[i].interval;
}
}
}
}
这里面可以看到对timer任务函数的处理:若该函数返回值小于0,这个timer任务会被删掉,后面不再响应;若返回值为0,则保持上一次的定时值;若返回值大于0,则以该返回值作为新的定时周期(单位us)。
(4) 在上面的第3步中,如果timer任务表中的任务发生了变化,则之前的时间顺序可能会被破坏,这里再重新排序。
if(change_flg){
blt_soft_timer_sort();
}
(5) 若最近的timer任务的响应时间距离现在只剩3秒(3s可以再改大一些)不到,则将该时间设为应用层提前唤醒的时间,否则关闭应用层提前唤醒。
if( (u32)(blt_timer.timer[0].t - now) < 3000 * SYSTEM_TIMER_TICK_1MS){
bls_pm_setAppWakeupLowPower(blt_timer.timer[0].t, 1);
}
else{
bls_pm_setAppWakeupLowPower(0, 0); //disable
}
添加定时器任务
使用如下API添加定时器任务:
typedef int (*blt_timer_callback_t)(void);
int blt_soft_timer_add(blt_timer_callback_t func, u32 interval_us);
func为定期执行的任务函数;interval_us为定时时间,单位为us。
对于定时任务func的int类型返回值,可采用以下三种处理方式:
a) 返回值小于0,则该任务执行后被自动删除。可以使用这个特性来控制定时器执行的次数。
b) 返回0,则一直使用之前的interval_us来定时。
c) 返回值大于0,则使用该返回值作为新的定时周期,单位us。
int blt_soft_timer_add(blt_timer_callback_t func, u32 interval_us)
{
u32 now = clock_time();
if(blt_timer.currentNum >= MAX_TIMER_NUM){ //timer full
return 0;
}
else{
blt_timer.timer[blt_timer.currentNum].cb = func;
blt_timer.timer[blt_timer.currentNum].interval = interval_us * SYSTEM_TIMER_TICK_1US;
blt_timer.timer[blt_timer.currentNum].t = now + blt_timer.timer[blt_timer.currentNum].interval;
blt_timer.currentNum ++;
blt_soft_timer_sort();
bls_pm_setAppWakeupLowPower(blt_timer.timer[0].t, 1);
return 1;
}
}
在代码实现中,若定时器数量超过最大值,则添加失败。每添加一个新的timer任务,必须重新做一下排序,以确保定时器任务在任何时候都是按照时间排序的,时间上最近的那个timer任务对应的index为0。
删除定时器任务
除了使用上面返回值小于0来自动删除定时器任务,还可以使用下面API来指定要删除的定时器任务。
int blt_soft_timer_delete(blt_timer_callback_t func);
Demo
blt soft timer的Demo code请参考B91m (B91 and B92) feature中TEST_USER_BLT_SOFT_TIMER。
int gpio_test0(void)
{
//GPIO toggle to see the effect
gpio_toggle(GPIO_LED_BLUE);
return 0;
}
_attribute_ble_data_retention_ static u8 timer_change_flg = 0;
int gpio_test1(void)
{
//GPIO toggle to see the effect
gpio_toggle(GPIO_LED_GREEN);
timer_change_flg = !timer_change_flg;
if (timer_change_flg) {
return 7000;
} else {
return 17000;
}
}
int gpio_test2(void)
{
//GPIO toggle to see the effect
gpio_toggle(GPIO_LED_WHITE);
//timer last for 5 second
if (clock_time_exceed(0, 5000000)) {
//return -1;
//blt_soft_timer_delete(&gpio_test2);
} else {
}
return 0;
}
int gpio_test3(void)
{
//GPIO toggle to see the effect
gpio_toggle(GPIO_LED_RED);
;
return 0;
}
初始化:
##if (BLT_SOFTWARE_TIMER_ENABLE)
blt_soft_timer_init();
blt_soft_timer_add(&gpio_test0, 23000); //23ms
blt_soft_timer_add(&gpio_test1, 7000); //7ms <-> 17ms
blt_soft_timer_add(&gpio_test2, 13000); //13ms
blt_soft_timer_add(&gpio_test3, 100000); //100ms
##endif
定义了4个任务,这4个定时任务各有特点:
(1) gpio_test0每23ms toggle一次。
(2) gpio_test1使用了7ms/17ms两个时间的切换定时。
(3) gpio_test2在系统运行5s后将自己删掉。代码中有两种方式可以实现这个功能:一是调用blt_soft_timer_delete(&gpio_test2);二是return -1。
(4) gpio_test3每100ms toggle一次。
功能参考Demo
本章节将介绍 sdk/vendor/feature_test 下的各个功能的使用方法与现象,用户可以参考代码实现将所需功能添加到自己的代码中。
feature_backup
- 功能:BLE 基本功能的演示。包括广播、被动扫描、连接等。同时,该功能演示也可作为用户开发 BLE 应用的相对“干净“的基础版本(默认关闭 SMP,SDP 等功能)。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_backup 下,需要修改 tl_ble_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_FEATURE_BACKUP
来激活这部分代码。代码中设置广播参数、广播内容、扫描响应内容、扫描参数,以及使能广播、使能扫描等配置,请参考初始化代码中的如下内容:
编译,将生成的固件分别烧录到两个开发板中。接电启动后,可通过扫描广播包,扫描到两个设备名为 “feature” 的广播,可以借由其他 Central 设备或者 Peripheral 设备分别进行连接,也可以使二者互联。要使二者互联,按其中一个开发板的按键 SW4 启动连接,通过 BDT 工具的 Tdebug 选项卡,读取左侧的变量值列表(具体使用方法请参考章节“Debug方法”),可以看到该开发板的 acl_conn_central_num 的值为 1:
查看另一个开发板的acl_conn_periphr_num变量的值,其值也为1:
代表二者连接成功。
feature_2M_coded_phy
- 功能:BLE 1M/2M/Coded PHY 功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_2M_coded_phy 下,需要修改 tl_ble_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使用方法。tl_ble_sdk 中部分实例的 SDP 流程的参考实现中用到了这些指令,用户可以使用该演示代码进行单指令测试。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_gatt_api 下,需要修改 tl_ble_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_ll_more_data
- 功能:BLE MD=1 的演示。MD,即 More Data,是数据通道 PDU Header 中的一个标志位 MD flag。同时,该演示也提供用户做吞吐量的测试使用。
- 主要硬件: B91 开发板 x 2
以B91为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_ll_more_data 下,需要修改 tl_ble_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_LL_MD
来激活这部分代码。
编译,将生成的固件分别烧录到两个开发板中。上电,按其中一个开发板(作为 Central)的 SW4 按键,触发连接。连接成功后,红灯点亮(如果连接多个 Peripheral,分别点亮:红色、白色、绿色、蓝色灯)。连接成功后同时按下 Central 的两个按键,可以从抓包中看到 Central 不断向 Peripheral 发送 WriteCmd,其包中的 MD flag 被置为 1,即下一个包已经准备好,即将发送:
同时按下 Peripheral 的两个按键,可以从抓包中看到 Peripheral 不断向 Central 发送 Notify 包,其中的 MD flag 被置为1:
feature_dle
- 功能:BLE DLE(Data Length Extension) 和 MTU Exchange 以及L2CAP 分包组包功能演示。
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_dle 下,需要修改 tl_ble_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 为例,应用层代码在 tl_ble_sdk/vendor/feature_demo/feature_smp 下,需要修改 tl_ble_sdk/vendor/feature_demo/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_SMP
来激活这部分代码,默认 Central 和 Peripheral 的 SMP 功能均不使能。
说明
(1) 由于要演示 SMP 下多个加密配置,为方便用户参考,在feature_smp/app.c 中定义 SMP_TEST_MODE 宏,用户仅需修改该宏的定义来实现不同加密配置的演示代码,通过全局搜索该定义的方式,得到 porting 相应 SMP 功能的最简代码。
(2) 配置 SMP 相关参数时,如果 Central 和 Peripheral 配置相同,则使用不带后缀的 API,如果不同,应按照需求使用带 _central/_periphr 后缀的 API。演示代码中为了方便用户另作配置进行测试,对 SMP 加密使能的演示,使用带后缀的 API。
(3) 为方便观察现象,代码中加入了 LED 指示灯的显示,定义如下:
- 绿色:ON:Central connected;OFF: Central disconnected.
- 红色:ON:Peripheral connected;OFF: Peripheral disconnected.
- 蓝色:ON:Pair Succeeded;OFF:Disconnected,没走Pair流程也不亮。
- 白色:ON:Encryption succeeded;OFF:Disconnected.
(4) 在 SMP 中的角色分为 Initiator 和 Responder,分别对应 BLE 中的角色 Central 和 Peripheral。
(5) 目前 feature_smp 中 UART 功能,仅做了 B91,B92 的适配,如需适配其他芯片,请参考 tl_platform_sdk。
注意
- LE Security Connections 的配对方式,要求 MTU>=65。所以,这里没有使用 default_buffer.h 中的定义。
Peripheral 和 Central 均不使能 SMP
Peripheral 和 Central 的 SMP 功能关闭的演示,需在 feature_smp/app.c 中定义(默认):
##define SMP_TEST_MODE SMP_TEST_NOT_SUPPORT
编译,分别擦除 Flash 后烧录到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),可以看到两个开发板分别亮了绿灯和红灯,代表连接成功:
抓包如下:
Legacy Just Works
Central 和 Peripheral 使能 SMP 功能,以 Legacy Just Works 方式配对的演示。该演示需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LEGACY_JW
说明
如果用户不希望 Peripheral 建立连接后发 Security Request,应在初始化代码中添加(默认注释掉):
blc_smp_configSecurityRequestSending( SecReq_NOT_SEND, SecReq_NOT_SEND, 0);
该 API 仅针对 Peripheral(只有 Peripheral 会发 Security Request),因此不存在后缀为 _periphr/_central 的相应 API。
说明
如果用户希望 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。
编译,分别擦除 Flash 后烧录到两个开发板中。按其中一个开发板(作为 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;
ioCapability = 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.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LESC_JW
编译,分别擦除 Flash 后烧录到两个开发板中。按其中一个开发板(作为 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 Input
Central 和 Peripheral 使能 SMP 功能,以 Legacy Passkey Entry 配对方式,通过 Input 方法实现的演示。这里演示的 Input 是 Keyboard Input,使用该功能需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LEGACY_PKI
演示代码中 Initiator 默认设置 Passkey 为 123456,Responder 在收到 GAP_EVT_SMP_TK_REQUEST_PASSKEY 事件10s内,自动输入 123456 的 Passkey。
编译,分别擦除 Flash 后烧录到两个开发板中。按其中一个开发板(作为 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 Display
Central 和 Peripheral 使能 SMP 功能,以 Legacy Passkey Entry 配对方式,通过 Display 方法实现的演示。Display 与上一个演示 Input 类似,只是显示和输入的设备角色互换了。使用该功能需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LEGACY_PKD
演示代码中 Initiator 通过 UART 输入 Passkey ,Responder 通过底层随机生成 Passkey 并显示出来,更加贴合实际使用需求。
编译,分别擦除 Flash 后烧录到两个开发板中。给其中一个开发板(作为 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 OOB
Central 和 Peripheral 使能 SMP 功能,以 Legacy OOB 方式配对的演示。根据蓝牙协议,在 Legacy 配对方式下,当双方均支持 OOB,拥有对方 OOB 数据时,会使用 Legacy OOB 方式进行匹配。使用该功能需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LEGACY_OOB
这里选择用 Manually 方式通过 UART 输入 OOB data。
编译,分别擦除 Flash 后烧录到两个开发板中。给两个开发板的 UART 口 PA3(Tx),PA4(Rx) 接到电脑串口上,配置波特率 115200 8N1。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色灯,Peripheral 亮红色灯。此时从双方的 Log 中看到 "OOB request",在 30s 内将自定义的 OOB data 分别填入到 Peripheral 和 Central 的 UART 串口。可以看到二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下:
Secure Connections Passkey Entry
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections Passkey Entry 方式配对的演示。与 Legacy 对应,Secure Connections 的 Passkey Entry 匹配方式也有 Input 和 Display 两种实现方式。
- 对于 Input,需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LESC_PKI
编译,分别擦除 Flash 后烧录到两个开发板中。给其中一个开发板(作为 Peripheral)的 UART 口 PA3(Tx),PA4(Rx) 接到电脑串口上,配置波特率 115200 8N1。按 Central 开发板的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色灯,Peripheral 亮红色灯。此时 Central 的 Log 输出 "TK display",将输出的数在 30s 内填入到 Peripheral 的 UART 串口,可以看到 UART 串口返回输入值,Log 显示 "TK set" 后面跟着输入的 passkey,SMP 流程继续,白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(高亮部分为等待 Peripheral 输入 Passkey):
- 对于 Display,需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LESC_PKD
编译,分别擦除 Flash 后烧录到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿灯,Peripheral 亮红灯。停留几秒后二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为等待 Central 输入 Passkey):
Secure Connections Numeric Comparison
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections Numeric Comparison 方式配对的演示。根据蓝牙协议,当使用 Secure Connections 且双方皆为 DisplayYesNo 或 KeyboardDisplay 功能时,会使用 Numeric Comparison 方式进行匹配。使用该功能需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LESC_NC
编译,分别擦除 Flash 后烧录到两个开发板中。按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿灯,Peripheral 亮红灯,此时从双方的 Log 中看到 "TK display" 的值,二者应该相等。分别在 Central 和 Peripheral 开发板上按按键 SW3,代表发送 YES,可以看到二者的白色、蓝色灯相继亮起,代表二者 Pair 成功,抓包如下(框起部分为分别等待 Central 和 Peripheral 进行按键确认):
Secure Connections OOB
Central 和 Peripheral 使能 SMP 功能,以 Secure Connections OOB 方式配对的演示。根据蓝牙协议,在 SC 配对方式下,当双方均支持 OOB,拥有对方 OOB 数据时,会使用 SC OOB 方式进行匹配。使用该功能需在 feature_smp/app.c 中定义:
##define SMP_TEST_MODE SMP_TEST_LESC_OOB
编译 feature_test,分别擦除 Flash 后烧录B92_ble_multi_conn_sdk/build/B92/feature_test/feature_test.bin 到两个开发板中。给两个开发板的 UART 口 PA3(Tx),PA4(Rx) 接到电脑串口上,配置波特率115200 8N1。
下载后,开发板初始化会生成random value 和confirm value,按其中一个开发板(作为 Central)的按键 SW4(启动连接),连接成功后,可以看到 Central 亮绿色灯,Peripheral 亮红色灯。
两端的OOB功能可以通过UART发命令方式将其打开或关闭,打开后即可进行sc oob方式的配对。
这里将Central的oob flag置1,Peripheral置0,此时将peripheral的confirm值和value值一起通过UART串口发送给central,Central和Peripheral均会回复“pairing success”。
异常处理
按键没有响应
(1) 硬件原因:可能是由于按键的跳线帽没有接正确,请参考下图:
(2) 环境原因:可能由于开发板烧录后没有 Reset 使程序运行起来。
LED 灯没有点亮
(1) 硬件原因:可能由于LED的跳线帽没有接正确,请参考下图:
feature_whitelist
- 功能:BLE 白名单 功能演示
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_whitelist 下,需要修改 tl_ble_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_WHITELIST
来激活这部分代码,做白名单功能的演示。编译并下载到 Central 和 Peripheral 中。烧录完成后,通过按Central的SW4启动连接,可以看到Central和Peripheral成功连接,抓包如下:
通过 BDT 修改 Central 或 Peripheral 的 MAC 地址为其他地址。
复位,并重新点击 Central 的SW4 按键,会看到 Central 发出了 Connect Request,Peripheral 却不会响应,以至于 Central 一直尝试连接:
因为,Central 和 Peripheral 已经建立过连接,且已将对方的 MAC 地址加入到自己的白名单中。
feature_soft_timer
- 功能:Soft Timer (软件定时器) 功能演示。并作为 IO Debug 方法的演示参考。
- 主要硬件: B91 开发板,逻辑分析仪 或 示波器
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_soft_timer 下,需要修改 tl_ble_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 设置的事件是不受休眠影响的。
feature_ota
- 功能:BLE OTA 功能演示
- 主要硬件: B91 开发板 x 2
以 B91 为例,应用层代码在 tl_ble_sdk/vendor/feature_test/feature_ota 下,需要修改 tl_ble_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_OTA
来激活这部分代码,做 OTA 的演示。
在app_config.h中,使能BLE_OTA_CLIENT_ENABLE,并且可以选用两种传输协议:extended protocol 和 legacy protocol。extended protocol是传输速率更快的协议。
##define BLE_OTA_CLIENT_ENABLE 1
##if (BLE_OTA_CLIENT_ENABLE)
//0: OTA extended protocol; 1: OTA legacy protocol
#define OTA_LEGACY_PROTOCOL 1
##endif
编译,得到 Central 的固件,烧录到其中一块开发板上作为 Central。将Peripheral的新固件(上电白灯常亮)烧录到Central的OTA文件地址(demo中有两个地址,分别是0x80000和0xC0000)。Peripheral对应的开发板烧录老固件,上电绿灯常亮。
按下Central的SW5按键,建立连接。连接后按下SW2或者SW4(SW2对应0x80000,SW对应0xC0000),开始OTA流程。Peripheral接收新固件,传输完成后,Peripheral自动重启运行新固件,可观察LED从绿灯常亮变化为白灯常亮。
抓包可以看到 Central 不停向 Peripheral 发送 WriteCmd:
升级完成,Peripheral 发送给 Central 一个 Notify 指令,代表升级成功,之后发送 Terminate 指令,断开连接。由于二者进行 SMP 连接, Central 存有 Peripheral 的信息,立刻又重新连上:
feature_l2cap_coc
- 功能:BLE COC(Connection Oriented Channel)连接和数据发送功能演示。
- 主要硬件: B91开发板 x 2
(1) 将feature_config.h中的FEATURE_TEST_MODE宏改为TEST_L2CAP_COC:
##define FEATURE_TEST_MODE TEST_L2CAP_COC
(2) 编译下载和操作:
编译并将生成的固件分别烧录到两块开发板中,上电,按其中一个开发板的SW5(作为Master)按键,触发连接,连接成功Central端红灯常亮,Peripheral端绿灯常亮,然后按任意一个开发板的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_ext_adv
- 功能:扩展广播
- 主要硬件: B92 开发板×1
应用层代码在 B92_ble_multi_conn_sdk/vendor/feature_test/feature_ext_adv 下,需要修改 B92_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_EXT_ADV
demo打开了TLKAPI_DEBUG_CHANNEL_GSUART,默认使用GPIO_PD4进行模拟DEBUG输出,可通过连接串口工具RX查看打印信息。
##define TLKAPI_DEBUG_CHANNEL TLKAPI_DEBUG_CHANNEL_GSUART
在ext_adv_set.c中,app_ext_adv_set_test()函数实现了多种extend advertising。通过按键SW5循环切换发送的扩展广播内容。
feature_ext_scan
- 功能:扩展扫描
- 主要硬件: B92 开发板×2
应用层代码在 B92_ble_multi_conn_sdk/vendor/feature_test/feature_ext_scan 下,需要修改 B92_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_EXT_SCAN
demo打开了TLKAPI_DEBUG_CHANNEL_GSUART,默认使用GPIO_PD4进行模拟DEBUG输出,可通过连接串口工具RX查看打印信息。
##define TLKAPI_DEBUG_CHANNEL TLKAPI_DEBUG_CHANNEL_GSUART
在app_config.h中可以配置扫描的目标phy类型:
##define APP_EXT_SCAN_1M 0x01
##define APP_EXT_SCAN_CODED 0x02
##define APP_EXT_SCAN_1M_CODED 0x04
##define EXT_SCAN_TEST_MODE APP_EXT_SCAN_CODED
两块开发板一块烧录ext_adv固件作为peripheral,另一块烧录ext_scan固件作为central。按下central的SW5按键,可以连接到正在发出扩展广播的peripheral。
feature_per_adv
- 功能:周期广播扫描
- 主要硬件: B92 开发板×1
应用层代码在 B92_ble_multi_conn_sdk/vendor/feature_test/feature_per_adv 下,需要修改 B92_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_PER_ADV
demo打开了TLKAPI_DEBUG_CHANNEL_GSUART,默认使用GPIO_PD4进行模拟DEBUG输出,可通过连接串口工具RX查看打印信息。
##define TLKAPI_DEBUG_CHANNEL TLKAPI_DEBUG_CHANNEL_GSUART
编译固件烧录到开发板,上电运行,可看到周期广播
feature_per_adv_sync
- 功能:周期广播同步
- 主要硬件: B92 开发板×2
应用层代码在 B92_ble_multi_conn_sdk/vendor/feature_test/feature_per_adv_sync 下,需要修改 B92_ble_multi_conn_sdk/vendor/feature_test/feature_config.h 中的定义:
##define FEATURE_TEST_MODE TEST_PER_ADV_SYNC
demo打开了TLKAPI_DEBUG_CHANNEL_GSUART,默认使用GPIO_PD4进行模拟DEBUG输出,可通过连接串口工具RX查看打印信息。
##define TLKAPI_DEBUG_CHANNEL TLKAPI_DEBUG_CHANNEL_GSUART
在app.c中,app_controller_event_callback,可以判断HCI事件进入不同的事件callback,在扫描到周期广播后,可以建立同步,可以通过打印观察事件
其他模块
24MHz晶体外部电容
参考下图中的24MHz晶体匹配电容的位置C1/C4。
SDK默认使用内部电容(即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。
用户只需要在main函数开始的地方(一定要在sys_init函数之前)调用下面API:
void blc_pm_select_internal_32k_crystal(void);
上述API是选择32k RC API,SDK默认调用blc_pm_select_internal_32k_crystal选择的32k RC。
PA
如果需要使用RF PA,B91请参考drivers/B91/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
tl_ble_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);
app_phyTest_rxUartCb实现上位机下发的cmd的解析和执行,app_phyTest_txUartCb实现将相应的结果和数据反馈给上位机。
编译feature_test生成的bin文件直接测试可以通过。user可研究一下code的实现,掌握相关接口的使用。
注意
- 在验证PHY test功能时,需保证没有其他模块使用RF功能。
调试方法
该章节介绍几种在开发过程中常用的调试方法。
GPIO 模拟 UART 打印
为方便用户调试,tl_ble_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,其余芯片是xxx.lst),所以用户在向官方提供调试固件时,也应尽可能提供list文件,以便 Telink 工程师通过 list 文件读取底层一些变量的值。
BDT 工具的 Memory Access 功能
Telink 官方提供的 BDT 工具,还可以通过 Memory Access 功能,读取 Flash、内存等空间指定位置的内容,也可以写入数据。需要在读取前保证SWS处于接通状态。具体请参考 《BDT 用户指南》的 Debug 章节。
BDT 工具读 PC 指针
Telink 官方提供的 BDT 工具,可以用来读取 PC(Program Counter) 指针(B91不支持),这在分析死机问题时,非常有帮助。具体请参考 《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 会起效,比如 tl_ble_sdk 中 CHN14 所代表的 rf_irq_handler,CHN15 所代表的 stimer_irq_handler。
USB my_dump_str_data
在 tl_ble_sdk 中可以看到有多处 my_dump_str_data 这个 API 的调用,该功能是 B91 上借助 USB 接口将调试信息输出的一种实现,不是官方推荐的调试信息输出方法,仅作为一种参考,目的是解决在中断中不能通过 GPIO 模拟 UART 进行输出的问题。该功能默认不开启,需要定义 DUMP_STR_EN 为 1 才能开启。
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 success”,此时表示烧录成功。
附录
附录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_中将stack的所有内容写为0x55,在程序运行时会将使用过的栈的数据改写为其他值。通过查看栈被改写的size大小,可以判断栈的使用情况,并可以判断栈是否发生溢出。
方法
(1)打开boot/cstartup_,将_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中查看此地址是否被改写为其他内容,若被改写则说明栈溢出。