跳转至

B91 BLE 单连接 SDK 开发手册


SDK介绍

该BLE SDK提供BLE slave Single Connection开发demo,用户可以在这些demo 基础上开发自己的应用程序。

软件组织架构

该BLE SDK软件架构包括APP应用层和BLE stack协议栈部分。

在IDE中导入sdk工程后,显示的文件组织结构如下图所示。顶层文件夹有9个:algorithm、application、boot、common、drivers、proj_lib、stack、vendor、3rd-party。

SDK文件结构

  • Algorithm:提供加密算法相关的函数。

  • Application:提供一些通用的应用处理程序,如print、keyboard等。

  • boot:提供芯片的software bootloader,即MCU上电启动或deepsleep唤醒后的汇编处理过程,为后面C语言程序的运行搭建好环境。

  • common:提供一些通用的跨平台的处理函数,如内存处理函数、字符串处理函数等。

  • drivers:提供与MCU紧密相关的硬件设置和外设驱动程序,如clock、flash、i2c、usb、gpio、uart等。

  • proj_lib: 存放SDK运行所必需的库文件(如libB91_ble_lib.a)。BLE协议栈、RF驱动、PM驱动等文件,被封装在库文件里,用户无法看到源文件。

  • stack:存放BLE协议栈相关的头文件。源文件被编译到库文件里面,对于用户是不可见的。

  • vendor:用于存放用户应用层代码。

  • 3rd-party:用于存放第三方开源代码。

main.c

包括main函数入口,系统初始化的相关函数,以及无限循环while(1)的写法,建议不要对此文件进行任何修改,直接使用固有写法。

_attribute_ram_code_ int main (void)   //must on ramcode
{
    blc_pm_select_internal_32k_crystal();
    #if (MCU_CORE_TYPE == MCU_CORE_B91)
        sys_init(DCDC_1P4_DCDC_1P8,VBAT_MAX_VALUE_GREATER_THAN_3V6);
    #elif (MCU_CORE_TYPE == MCU_CORE_B92)
        sys_init(LDO_1P2_LDO_2P0,VBAT_MAX_VALUE_GREATER_THAN_3V6);
    #endif
    /* detect if MCU is wake_up from deep retention mode */
    int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();  //MCU deep retention wakeUp

    CCLK_24M_HCLK_24M_PCLK_24M;

    rf_drv_ble_init();

    gpio_init(!deepRetWakeUp);
    #if (MCU_CORE_TYPE == MCU_CORE_B92)
        wd_32k_stop();
    #endif

    if(!deepRetWakeUp){//read flash size
        #if (BATT_CHECK_ENABLE)
            user_battery_power_check(2000);
        #endif

        blc_readFlashSize_autoConfigCustomFlashSector();

        #if (FLASH_FIRMWARE_CHECK_ENABLE)
            blt_firmware_completeness_check();
        #endif

        #if FIRMWARES_SIGNATURE_ENABLE
            blt_firmware_signature_check();
        #endif
    }

    /* load customized freq_offset cap value. */
    blc_app_loadCustomizedParameters();

    if( deepRetWakeUp ){ //MCU wake_up from deepSleep retention mode
        user_init_deepRetn ();
    }
    else{ //MCU power_on or wake_up from deepSleep mode
        user_init_normal();
    }

#if (FREERTOS_ENABLE)
    extern void vPortRestoreTask();
    if( deepRetWakeUp ){
        tlkapi_send_string_data(APP_LOG_EN,"enter restor work.",0,0);
        vPortRestoreTask();
    }
    else{

        #if(!TEST_CONN_CURRENT_ENABLE)
            xTaskCreate( led_task, "tLed", configMINIMAL_STACK_SIZE, (void*)0, (tskIDLE_PRIORITY+1), 0 );
        #endif
        xTaskCreate( proto_task, "tProto", 2*configMINIMAL_STACK_SIZE, (void*)0, (tskIDLE_PRIORITY+2), &hProtoTask );
        //xTaskCreate( ui_task, "tUI", configMINIMAL_STACK_SIZE, (void*)0, tskIDLE_PRIORITY + 1, 0 );

        /* !!! must */
        extern void os_give_sem(void);
        os_give_sem();

        vTaskStartScheduler();
    }
#else
    irq_enable();

    while (1) {
        main_loop ();
    }
    return 0;
#endif
}

app_config.h

用户配置文件,用于对整个系统的相关参数进行配置,包括BLE相关参数、GPIO的配置、PM低功耗管理的相关配置等。

后面介绍各个模块时会对app_config.h中的各个参数的含义进行详细说明。

application file

  • app.c:用户主文件,用于完成BLE协议栈初始化、数据处理、低功耗处理等。

  • BLE sample工程的app_att.c:service和profile的配置文件,有Telink定义的Attribute结构,根据该结构,已提供GATT、标准HID和私有的OTA等相关Attribute,用户可以参考这些添加自己的service和profile。

  • 其他UI文件:如按键处理、OTA等用户任务的处理文件。

BLE stack entry

Telink BLE SDK中BLE stack部分code的入口函数有三个:

(1) rf_irq_handler函数中BLE相关中断的处理入口irq_blt_sdk_handler。

_attribute_ram_code_ void rf_irq_handler (void)
{
……
irq_blt_sdk_handler ();
……
}

(2) stimer_irq_handler函数中BLE相关中断的处理入口irq_blt_sdk_handler。

_attribute_ram_code_ void stimer_irq_handler (void)
{
……
irq_blt_sdk_handler ();
……
}

(3) application file main_loop中BLE逻辑和数据处理的函数入口blt_sdk_main_loop。

void main_loop (void)
{
///////////////////// BLE entry ////////////////////////////
blt_sdk_main_loop();
////////////////////// UI entry ////////////////////////////
……
////////////////////// PM process ////////////////////
……
}

Software Bootloader介绍

software bootloader文件存放在SDK/boot/目录下:

bootloader以及boot.link路径

默认运行的是cstartup_B9X.S及boot_general.link文件,

此时SDK会占用I-SRAM及D-SRAM空间,I-SRAM中包括retention_reset、aes_data、retention_data、ramcode、和unused I-SRAM area。D-SRAM中包括data、sbss、bss、heap、unused D-SRAM area和stack。

如果想把128K的D-SRAM空间全部留给用户使用,需要做如下更改。

(1) 将cstartup_B91.S文件开始的 #if 1 改成 #if 0。

(2) 将cstartup_B91_DLM.S文件开始的 #if 0 改成 #if 1。

(3) 将boot_DLM.link的内容拷贝到工程的boot.link文件中。

link文件位置示意图

关于I-SRAM及D-SRAM更多信息请参考“2.1.2 SRAM空间分配” 章节。

SDK默认运行的是deepsleep retention 32K,用户想切换到deepsleep retention 64K,需要在user_init_normal()函数Power Management initialization的地方加上如下代码:

blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW64K);

Demo介绍

Telink BLE SDK 给用户提供了多个BLE demo。

用户通过软硬件demo的运行,可以观察到直观的效果。用户也可以在demo code上进行修改,完成自己的应用开发。

BLE SDK 提供的demo code

BLE Slave Demo

BLE slave的demo及区别如下表所示。

Demo Stack Application MCU Funciton
B91m module BLE controller + host Application在主控MCU上 BLE透传模组(UART)
B91m ble sample BLE controller + host 最简单的slave demo,广播和连接功能 主控MCU

B91m module是Telink提供的完整的BLE slave stack。B91m module只作为BLE透传模组,与主控MCU通过UART接口通信,一般应用代码写在对方主控MCU。

B91m ble sample也是Telink提供的完整的BLE slave stack。可以和标准的IOS/android设备配对连接。

Feature Demo

B91m_feature针对一些常用的BLE相关的feature给出了demo code,用户可参考这些demo完成自己的功能实现,详情见code。该文档BLE部分会介绍所有的feature。

在B91m_feature工程里面feature_config.h中对宏“FEATURE_TEST_MODE”进行选择性的定义,即可切换到不同feature的Demo。

该文档“Feature Demo 介绍”章节会介绍各个demo的简单使用。

工程配置说明

一般情况下user使用我们BLE_SDK是已经默认配置好的,不需要重新更改,如果想要自己新加工程的话,可以按照如下推荐进行配置,以B91为例。

Tool Setting

(1) Symbols Define

该步骤是定义SDK中所需要的宏,需要增加的配置如下:

CHIP_TYPE=CHIP_TYPE_B91

__PROJECT_B91_BLE_SAMPLE__=1

具体配置如下图:

Symbol Define Config

需要注意的是,我们在配置的时候需要确保选中对应的工程,如上图步骤2所示,也可以同时选中所有的工程,选中[All configurations]即可。

(2) Directories

文件路径添加:

"${workspace_loc:/${ProjName}}"

Directiories Config

(3) Optimization Config

编译器优化选项,使用者需要勾选如下图的三个选项:

Optimization Config

(4) Miscellaneous config

Other flags中需要添加如下:

-c -fmessage-length=0 -fno-builtin -fomit-frame-pointer -fno-strict-aliasing -fshort-wchar -fuse-ld=bfd -fpack-struct

(5) Libraries set

使用BLE_SDK都需要添加我们的stack库文件,链接库文件的配置方法如下:

Libraries中需要添加:

B91_ble_lib

Library search path:

"${workspace_loc:/${ProjName}/proj_lib}"

具体配置,如下图:

Libraries Set

(6) Objcopy General

该项是为了取消自动生成bin文件,配置如下图:

Objcopy Config

Build Steps

该项步骤中需要在Post-build steps的Command中添加如下命令:

"${workspace_loc:/${ProjName}}/tl_check_fw.bat" ${ConfigName}

在上一小节中我们介绍了需要取消自动生成bin文件,那么该项的目的在于post build阶段调用tl_check_fw.bat脚本来生成bin文件并在末尾添加CRC域。

Build Steps Config

Build Artifact

在Build Artifact的Artifact extension中选择elf选项。

Build Artifact Config

MCU基础模块

MCU地址空间

MCU地址空间分配

默认支持的Flash程序存储空间为1 MB。

B91最大支持256 KB SRAM,包括128 KB的ILM(instruction local memory)和128 KB的DLM(data local memory)。

B92最大支持512 KB SRAM,包括256 KB的ILM(instruction local memory)和256 KB的DLM(data local memory)。

ILM全文用I-SRAM表示,DLM全文用D-SRAM表示。

B91地址空间分配如下图所示。

MCU地址空间分配

  • 访问程序空间的地址范围:0x20000000~0x21000000

  • 访问Register的地址范围:0x80100000~0x82000000

  • 总线访问ILM的地址范围:0xC0000000~0xC0020000

  • 总线访问DLM的地址范围:0xC0200000~0xC0220000

  • CPU访问ILM的地址范围:0x00000000~0x00020000

  • CPU访问DLM的地址范围:0x00080000~0x000A0000

B92地址空间分配如下图所示。

MCU地址空间分配

  • 访问程序空间的地址范围:0x20000000~0x23FFFF00

  • 访问Register的地址范围:0x80100000~0xA4000000

  • 总线访问ILM的地址范围:0xC0000000~0xC0040000

  • 总线访问DLM的地址范围:0xC0200000~0xC0240000

  • CPU访问ILM的地址范围:0x00000000~0x00040000

  • CPU访问DLM的地址范围:0x00080000~0x000C0000

SRAM空间分配

B91芯片最大支持256 KB SRAM,分别为I-SRAM(128 KB)和D-SRAM(128 KB)。 B92芯片最大支持512 KB SRAM,分别为I-SRAM(256 KB)和D-SRAM(256 KB)。I-SRAM可以放指令和数据,D-SRAM只能放数据。

SRAM的空间分配和低功耗管理部分的deepsleep retention功能密切相关,请用户先掌握了解deepsleep retention相关知识。

(1) SRAM和Firmware空间

对于不同SRAM大小的B92芯片,用户可以在cstartup_B92.S文件中控制所使用的SRAM大小(受限于芯片所支持的最大SRAM)。下表为各大小的SRAM参数配置:

SRAM size 128KB 256KB 384KB 512KB
__IRAM_2_EN 0 1 1 1
__DRAM_1_EN 0 0 1 1
__DRAM_2_EN 0 0 0 1
__DRAM_DIS 1 1 0 0
  • 若配置为128KB的SRAM,则可以使用128KB的I-SRAM空间;
  • 若配置为256KB的SRAM,则可以使用256KB的I-SRAM;
  • 若配置为384KB的SRAM,则可以使用128KB的D-SRAM和256KB的I-SRAM;
  • 若配置为512KB的SRAM,则可以使用256KB的D-SRAM和256KB的I-SRAM。

用户可根据需求配置所使用的SRAM空间,但不能超过硬件限制的SRAM大小。例如用户当前的B92芯片最大支持256KB的SRAM,那么在使用时可以选择配置为128KB或者256KB。128KB和512KB的B92芯片同理。配置128KB SRAM的代码如下所示:

    .equ __IRAM_2_EN,   0
    .equ __DRAM_1_EN,   0
    .equ __DRAM_2_EN,   0
    .equ __DRAM_DIS,    1

对MCU 地址空间中SRAM空间的分配做进一步说明。

128KB的I-SRAM CPU访问地址范围为0x00000000~0x00020000,256KB的I-SRAM CPU访问地址范围为0x00000000~0x00040000,128KB的D-SRAM CPU访问地址范围为0x00080000~0x000A0000,256KB的D-SRAM CPU访问地址范围为0x00080000~0x000C0000。

下图是以B91为例,在deepsleep retention 32K模式下,I-SRAM和D-SRAM都同时使用的情况下对应的SRAM和Firmware空间分配说明。

SRAM空间分配&Firmware空间分配

上图的SRAM空间分配相关的文件有cstartup_B91.S和boot.link(由“1.2 software bootloader介绍”小节我们可知这里的boot.link内容同boot_general.link文件)。(如果使用deepsleep retention 64K模式,上图左上方deepsleep retention area的范围为0x00000000~0x00010000。)

Flash中Firmware包括vectors、retention_reset、retention_data、ram_code、text、rodata和data initial value。

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、sbss、bss、heap、unused D-SRAM area和stack。

D-SRAM中data的初始值是Flash中data initial value。

根据上图对各个段的介绍如下:

1) vectors and retention_reset

“vectors”段和“retention_reset”段都是汇编文件cstartup对应的程序,是软件启动代码(software bootloader)。其中vectors段是在Flash起始地址,retention_reset段是在I-SRAM的起始地址。芯片上电或normal deepsleep醒来后跳转到Flash的起始地址(0x20000000)开始执行。如果是deepsleep retention醒来会从I-SRAM的起始地址(0x00000000)开始执行。

2) aes_data

硬件AES模块的缓存数据被放到aes_data段,aes_data段在I-SRAM上,并且长度固定为32Bytes,用户不可更改。在运行bootloader时,会直接在I-SRAM上全设为0。

3) retention_data

B91的deepsleep retention mode支持MCU进入retention后,I-SRAM的前32K/64K可以不掉电保持住I-SRAM上的数据不丢失。B92除支持前32K/64K,还支持前96K进入retention后不掉电保持。

程序中的全局变量如果直接编译的话,会分配在“data”段、“sbss”段或“bss”段,进入deepsleep retention后会掉电丢失。若D-SRAM使用的情况下,这三段的内容都存放在D-SRAM。

如果希望一些特定的变量在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};

“retention_data”段的全局变量不管initial value是否为0,都会无条件准备好它们的initial value,存放在flash的retention_data area上。上电或normal deepsleep唤醒后MCU会从Flash拷贝到I-SRAM的retention_data area上。

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。

_retention_size_大小

如果用户不希望浪费太多的“_retention_size_”,可以将之前不在retention_data/ram_code的variable(变量)/fucntion(函数)通过添加对应的关键字,适当增加切换到retention_data/ram_code中。放在retention_data中的variable也可以节省初始化时间以降低功耗(具体原因请参考低功耗管理部分的介绍)。

如果用户选择的配置使用的是deepsleep retention 32K mode,但定义的“_retention_size_”超过所定义的32K,比如下图的“_retention_size_”大小为0x5b02 + 0x3700,约36.5KB,超出32K部分的ram_code会因为进入deepsleep retention模式而丢失内容。

_retention_size_大小

用户可以通过下面的一种方式修改:

一是减少所定义“_attribute_data_retention_”段或“_attribute_ram_code_”段属性的内容。

二是选择切换为deepsleep retention 64K或者deepsleep retention 96K(仅B92支持)模式,详细配置方法参考“1.2 software bootloader介绍”小节。

4) ram_code

“ram_code”段是Flash Firmware中需要常驻内存的code,对应SDK中所有加了关键字“_attribute_ram_code_”的函数,比如rf_irq_handler函数:

_attribute_ram_code_ void rf_irq_handler(void);

函数常驻内存有三个原因:

一是某些函数由于涉及到和Flash MSPI四根管脚的时序复用,必须常驻内存,如果放到Flash中就会出现时序冲突,造成死机,如Flash操作相关所有函数;

二是放到ram中执行的函数每次被调用时不需要从Flash重新读取,可以节省时间,所以对于一些执行时间有要求的函数可以放到常驻内存,提高执行效率。SDK中将BLE时序相关的一些经常要执行的函数常驻到内存,大大降低执行时间,最终达到节省功耗;

三是B91m基于Risc-V Platform,支持中断嵌套的功能,详细内容可以先参考“2.3中断嵌套”章节,如果用户新增了LEV3优先级的中断入口函数,就必须将它放到“ram_code”段。因为目前的SDK支持正在读写操作Flash的时候允许LEV2(BLE中断)和LEV3优先级的中断响应,如果新增的LEV3优先级中断入口函数不在“ram_code”段,就会导致Flash预取指操作和读写Flash操作出现时序冲突,造成死机。

用户如果需要将某个函数常驻内存,可以仿照上面的rf_irq_handler,在函数上添加关键字“_attribute_ram_code_”,编译之后就能在objdump文件中看到该函数在ram_code段了。

上电后MCU会从Flash拷贝到I-SRAM的ram_code area上。

5) Cache

Cache为高速缓存,分为I-Cache和D-Cache两块,大小各是固定的8KB,访问地址用户不可见。

默认Cache是打开的,在cstartup_B91.S或者cstartup_B92.S文件里配置。

  /* 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,再从Cache中读取执行。

Firmware的”text”和“rodata”段是没有放到SRAM中,这部分code符合程序局部性原理,需要一直被load到Cache中才能被执行。

由于Cache比较大,所以不允许用户使用指针形式读Flash,因为指针形式读Flash的数据被缓存在Cache里,如果Cache里这个数据没有被其他内容覆盖时,即使该位置处的Flash数据已经被改写,又有新的访问该数据的请求发生,此时MCU会直接用Cache里缓存的内容作为结果。

6) text

“text”段是Flash Firmware中所有非ram_code函数的集合。程序中的函数如果加了“_attribute_ram_code_”,会被编译为ram_code段,其他没有加这个关键字的函数就全部编译到了“text”段。“text”段的code访问需要通过I-Cache的缓存功能,将需要执行的code先load到I-Cache中才可以被执行。

7) rodata

Flash Firmware中“rodata”段是程序中定义的可读、不能改写的数据,是用关键字“const”定义的变量。比如Slave中的ATT table:

static const attribute_t my_Attributes[] = ……

用户可以在对应的objdump文件中看到“my_Attributes”是在“rodata”段之内。

My Attributes

8) data和data initial value

“data” 段存放程序已经初始化的全局变量,即initial value非0的全局变量,其初值需要提前存储在Flash Firmware中的“data initial value”段。

比如定义全局变量如下:

int   testValue = 0x1234;

那么编译器就会将变量testValue放到D-SRAM的“data”段,初始值0x1234存放到Flash Firmware中的“data initial value”段中,在运行bootloader时,会将该初值拷贝到testValue对应的SRAM内存地址。

9) sbss and bss

“sbss”段和“bss”段存放程序未初始化的全局变量,即initial value为0的全局变量。sbss是small bss的简写。这两部分是连在一起的,初始值无需提前准备。在运行bootloader时,会直接在SRAM上全设为0。

10) 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 = .);

11) stack

对于128K的D-SRAM,“stack”是从最高地址0x000A0000开始的,其方向为从下往上延伸,即stack指针SP在数据入栈时自减,数据出栈时自加。

默认情况下,SDK library使用了stack的size不超过384 byte,但由于stack的使用size取决于stack最深位置的地址,所以最终stack的使用量跟用户上层的程序设计是有关的。如果用户使用了比较麻烦的递归函数调用,或者在函数里使用了比较大的局部数组变量,或者其他有可能造成stack比较深的情况,都会导致最终stack的使用size变大。如果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

如果用户希望128K的D-SRAM空间全部留给用户使用或者D-SRAM不使用,可以将SDK占用的数据和指令都放到I-SRAM,更改方法见“1.2 software bootloader介绍。

下图是以B91为例,在deepsleep retention 32K模式下,SDK只使用I-SRAM情况下对应的SRAM和Firmware空间分配说明。

SRAM空间分配&Firmware空间分配(SDK只使用I-SRAM)

上图的SRAM空间分配相关的文件有cstartup_B91_DLM.S和boot_DLM.link。(如果使用deepsleep retention 64K模式,上图deepsleep retention area的范围为0x00000000~0x00010000。)

Flash中Firmware包括vectors、retention_reset、retention_data、ram_code、text、rodata、dlm_data initial value和data initial value。

I-SRAM中包括retention_reset、aes_data、retention_data、ram_code、unused I-SRAM area、data、sbss、bss、heap和stack。

I-SRAM中的retention_reset / retention_data / ram_code是Flash中retention_reset / retention_data / ram_code的拷贝。

I-SRAM中data的初始值是Flash中data initial value。

D-SRAM中只存放dlm_data或者不存放数据。

D-SRAM中dlm_data的初始值是Flash中dlm_data initial value。

各种段的定义参考上文的描述,这里只单独介绍dlm_data、dlm_data initial value和ram_code。

“dlm_data” 段是D-SRAM中存放程序已经初始化的全局变量,即initial value非0的全局变量,其初值需要提前存储在Flash Firmware中的“dlm_data initial value”段。

如果用户将变量定义到D-SRAM,只要将它们分配到“dlm_data”段即可,方法是在定义变量时添加关键字“_attribute_data_dlm_”。

比如定义全局变量如下:

_attribute_data_dlm_    int   dlm_testValue = 0x12345;

那么编译器就会将变量dlm_testValue放到D-SRAM的“dlm_data”段,初始值0x12345存放到Flash Firmware中的“dlm_data initial value”段中,在运行bootloader时,会将该初值拷贝到dlm_testValue对应的D-SRAM内存地址。

对于128K的I-SRAM,“stack”是从最高地址0x00020000开始的,其方向为从下往上延伸,即stack指针SP在数据入栈时自减,数据出栈时自加。

在boot_DLM.link文件中定义了栈顶位置_STACK_TOP。

PROVIDE (_STACK_TOP = 0x0020000);/*Need to prevent stack overflow*/

(2) objdump文件分析demo

这里以BLE slave最简单的demo B91m_ble_sample为例,结合“图 2-2 SRAM空间分配&Firmware空间分配(SDK使用I-SRAM和D-SRAM)”来分析。

B91m_ble_sample的bin文件和objdump文件在B91 SDK中见目录“SDK”->“B91_ble_sample”->“B91_ble_sample.bin”和“B91_ble_sample.lst”,在B92 SDK中见目录“SDK”->“B92_ble_sample” ->“B92_ble_sample.bin”和“B92_ble_sample.lst”,。

以下分析以B91为例,出现的多处截图,均来自boot_general.link、cstartup_B91.S、B91_ble_sample.bin和objdump.txt,请用户自行查找文件找到截图对应位置。

objdump文件中各个section的分布情况如下图所示(注意Align字节对齐):

objdump文件section统计

根据section统计先将需要了解的信息列出来,和后面具体的section介绍都会一一对应。

1) vectors:

从Flash 0x20000000开始,Size为0x166,计算出结束地址为0x166;

2) retention_reset:

从Flash 0x20000168开始,Size为0x126,计算出结束地址为0x2000028E;

从I-SRAM 0x00000000开始,Size为0x126,计算出结束地址为0x00000126;

3) aes_data:

从I-SRAM 0x00000128开始,Size为0x20,计算出结束地址为0x00000148;

4) retention_data:

从Flash 0x20000290开始,Size为0xda0,计算出结束地址为0x20001030;

从I-SRAM 0x00000148开始,Size为0xda0,计算出结束地址为0x00000EE8;

5) ram_code:

从Flash 0x20001030开始,Size为0x5b02,计算出结束地址为0x20006B32;

从I-SRAM 0x00000f00开始,Size为0x5b02,计算出结束地址为0x00006A02;

6) text:

从Flash 0x20006b38开始,Size为0x4c6e,计算出结束地址为0x2000B7A6;

7) rodata:

从Flash 0x2000b7a8开始,Size为0x881,计算出结束地址为0x2000C029;

8) data:

从Flash 0x2000c040开始,Size为0x11c,计算出结束地址为0x2000C15C;

从D-SRAM 0x00080000开始,Size为0x11c,计算出结束地址为0x0008011C;

9) sbss:

从Flash 0x2000c160开始,Size为0x80,计算出结束地址为0x2000C1E0;

从D-SRAM 0x00080120开始,Size为0x80,计算出结束地址为0x000801A0;

10) bss:

从Flash 0x2000c1e0开始,Size为0x20,计算出结束地址为0x2000C200;

从D-SRAM 0x000801a0开始,Size为0x20,计算出结束地址为0x000801C0;

objdump文件section地址

上图为在objdump文件中搜索“section”查到的部分section的起始地址后的拼图,结合该图和上面的“图 2-4 objdump文件section统计”,分析如下:

1) vector:

“vectors”段在Flash Firmware中起始地址为0x20000000,结束地址为0x20000168(最后一笔数据地址为0x20000162~0x20000165),size为0x166。

“vectors”段不会搬到SRAM。

2) retention_reset:

“retention_reset”段在Flash中起始地址为0x20000168,结束地址为0x20000290(最后一笔数据地址为0x2000028A~0x2000028D),size为0x126。上电搬移到I-SRAM。

“retention_reset”段在I-SRAM中起始地址为0x00000000,结束地址为0x00000128(最后一笔数据地址为0x00000122~0x00000125),size为0x126。

3) aes_data:

“aes_data”段不会存到Flash。

“aes_data”段在I-SRAM中起始地址为0x00000128,结束地址为0x00000148(最后一笔数据地址为0x00000144~0x00000147),size为0x20。

4) retention_data:

“retention_data”段在Flash中起始地址为0x20000290,结束地址为0x20001030(最后一笔数据地址为0x2000102D~0x2000102F),size为0xda0。上电搬移到I-SRAM。

“retention_data”段在I-SRAM中起始地址为0x00000148,结束地址为0x00000f00(最后一笔数据地址为0x00000EE4~0x00000EE7),size为0xda0。I-SRAM的0x00000EE8~0x00000EFF为无效I-SRAM区域。

5) ram_code:

“ram_code”段在Flash中起始地址为0x20001030,结束地址为0x20006b38(最后一笔数据地址为0x20006B2E~0x20006B31),size为0x5b02。上电搬移到I-SRAM。

“ram_code”段在I-SRAM中起始地址为0x00000f00,最后一笔数据地址为0x000069FE~0x00006A01,size为0x5b02,理论上结束地址可以使用到0x00020000。

6) Cache:

I-Cache和D-Cache的相关信息在objdump文件中不会体现出来。

7) text:

“text”段在Flash中起始地址为0x20006b38,结束地址为0x2000b7a8(最后一笔数据地址为0x2000B7A2~0x2000B7A5),size为0x4c6e。

8) rodata:

“rodata”段在Flash中起始地址为0x2000b7a8,结束地址为0x2000c040(最后一笔数据地址为0x2000C025~0x2000C028),size为0x881。

9) data:

“data”段在Flash中起始地址为0x2000c040,结束地址为0x2000c160(最后一笔数据地址为0x2000C158~0x2000C15B),size为0x11c。

“data”段在D-SRAM中起始地址为0x00080000,结束地址为0x00080120(最后一笔数据地址为0x00080118~0x0008011B),size为0x11c。

10) sbss:

“sbss”段在Flash中起始地址为0x2000c160,结束地址为0x2000c1e0(最后一笔数据地址为0x2000C1DC~0x2000C1DF),size为0x80。

“sbss”段在D-SRAM中起始地址为0x00080120,结束地址为0x000801a0(最后一笔数据地址为0x0008019C~0x0008019F),size为0x80。

11) bss:

“bss”段在Flash中起始地址为0x2000c1e0,最后一笔数据地址为0x2000C1FC~0x2000C1FF,size为0x20。

“bss”段在D-SRAM中起始地址为0x000801a0,最后一笔数据地址为0x000801BC~0x000801BF,size为0x20。

MCU地址空间访问

程序中对地址空间的访问分为外设空间和Flash空间两种情况。

(1) 外设空间的读写操作

程序中使用函数write_reg8(addr,v)、write_reg16(addr,v)、write_reg32(addr,v)、read_reg8(addr)、read_reg16(addr)、read_reg32(addr)对外设空间的Register进行读写,其实质是指针操作。更多信息,请参照drivers/B91/reg_include/sos.h或drivers/B92/reg_include/sos.h。

#define REG_RW_BASE_ADDR    0x80000000
#define write_reg8(addr,v)          (*(volatile unsigned char*)(REG_RW_BASE_ADDR | (addr)) = (unsigned char)(v))
#define write_reg16(addr,v)         (*(volatile unsigned short*)(REG_RW_BASE_ADDR | (addr)) = (unsigned short)(v))
#define write_reg32(addr,v)         (*(volatile unsigned long*)(REG_RW_BASE_ADDR | (addr)) = (unsigned long)(v))
#define read_reg8(addr)             (*(volatile unsigned char*)(REG_RW_BASE_ADDR | (addr)))
#define read_reg16(addr)            (*(volatile unsigned short*)(REG_RW_BASE_ADDR | (addr)))
#define read_reg32(addr)            (*(volatile unsigned long*)(REG_RW_BASE_ADDR | (addr)))

注意类似write_reg32(0x140824)/read_reg16(0x140300)的操作,其定义如上所示,可以看到是自动加上了0x80000000的偏移,所以MCU能够确保访问的是Register空间,而不会去访问Flash空间。

程序中使用函数write_sram8(addr,v)、write_sram16(addr,v)、write_sram32(addr,v)、read_sram8(addr)、read_sram16(addr)、read_sram32(addr)对外设空间的I-SRAM和D-SRAM进行读写,其实质也是指针操作,没有自动添加偏移量,所以对I-SRAM或D-SRAM的操作时注意地址是不同的。更多信息,请参照drivers/B91/reg_include/sos.h或drivers/B92/reg_include/sos.h。

#define write_sram8(addr,v)         (*(volatile unsigned char*)( (addr)) = (unsigned char)(v))
#define write_sram16(addr,v)            (*(volatile unsigned short*)( (addr)) = (unsigned short)(v))
#define write_sram32(addr,v)            (*(volatile unsigned long*)( (addr)) = (unsigned long)(v))

#define read_sram8(addr)            (*(volatile unsigned char*)((addr)))
#define read_sram16(addr)           (*(volatile unsigned short*)((addr)))
#define read_sram32(addr)           (*(volatile unsigned long*)((addr)))

注意:

对外设空间的I-SRAM和D-SRAM进行读写的函数一般只在少数debug模式下使用,而且必须使用的是多余sram空闲空间,确保不会破坏正常程序的执行。

(2) Flash空间的操作

Flash空间的读写操作使用flash_read_page和flash_write_page函数,Flash的擦除使用flash_erase_sector函数。

注意:

使用Flash操作函数时地址参数使用的起始地址是0x0。

1) Flash擦除操作

使用flash_erase_sector函数来擦除Flash。

void flash_erase_sector(unsigned long addr);

一个sector为4096 byte,如0x13000~0x13FFF是一个完整的sector。

addr必须是一个sector的首地址,该函数每次擦除整个sector。

擦除一个sector的时间会比较长,16M 系统时钟时,大约需要10~20ms甚至更长时间。

2) Flash读写操作

Flash读写操作只能使用flash_read_page和flash_write_page来实现,flash_read_page和flash_write_page为函数指针,默认分别指向flash_dread和flash_page_program。

void flash_dread(unsigned long addr, unsigned long len, unsigned char *buf);
void flash_page_program(unsigned long addr, unsigned long len, unsigned char *buf);

flash_read_page函数读取Flash上的内容:

u8 data[6] = {0 };
flash_read_page(0x11000, 6, data); //read 6 bytes from Flash 0x20011000 into data array.

flash_write_page函数对Flash进行写操作:

flash_write_page(u32 addr, u32 len, u8 *buf);
u8 data[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66 };
flash_write_page(0x12000, 6, data); //向Flash 0x20012000开始的6个byte写入0x665544332211。

注意:

写之前必须要先擦除,Flash里面一个page为256 byte,flash_write_page函数支持跨page的写操作。

3) 中断对Flash操作的影响

B91m系列的芯片支持中断嵌套功能,参考“2.3 中断嵌套”小节。上面介绍的三个Flash操作函数flash_read_page、flash_write_page、flash_erase_sector在执行时,会将中断阈值设置为1,允许中断优先级高于1的中断打断Flash操作函数,响应完改中断后再继续执行Flash操作函数,执行完Flash操作函数后再将中断阈值设置为0。

SDK Flash空间的分配

Flash存储信息以一个sector的大小(4K byte)为基本的单位,因为Flash的擦除是以sector为单位的(擦除函数为flash_erase_sector),理论上同一种类的信息需要存储在一个sector里面,不同种类的信息需要在不同的sector(防止擦除信息时将其他类的信息误擦除)。所以建议user在使用Flash存储定制信息时遵循“不同类信息放在不同sector”的原则。

B91m芯片默认支持1MB的Flash作为程序存储空间,SDK在文件boot.link的最后定义“FLASH_SIZE”为1MB,并且对“BIN_SIZE” <= “FLASH_SIZE”做了限制的判断,如果用户使用大于1MB的Flash,描述需要做对应的修改。

PROVIDE (FLASH_SIZE = 0x0100000);
ASSERT((BIN_SIZE)<= FLASH_SIZE, "BIN FILE OVERFLOW");

B91 1MB Flash地址分配

B91m默认使用的Flash为1MB,上图所示为B91 Flash地址分配,user可以根据自己需要去规划地址分配。下面以B91为例介绍默认的地址分配方法以及对应的修改地址的接口。

(1) 0xFF000~0xFFFFF这个sector存储MAC地址,实际上MAC address 6个bytes存储在0xFF000 ~0xFF005,高byte的地址存放在0xFF005,低byte地址存放在0xFF000。比如FLASH 0xFF000到0xFF005的内容依次为0x11 0x22 0x33 0x44 0x55 0x66,那么MAC address为0x665544332211。

泰凌的量产治具系统会将实际产品的MAC地址烧写到0xFF000这个地址,和SDK相对应。如果user需要修改这个地址,请确保治具系统烧写的地址也作了相应的修改。SDK中在user_init函数里会从Flash的CFG_ADR_MAC_1M_FLASH地址处读取MAC 地址,这个宏在/vendor/common/blt_common.h里面修改即可。

#ifndef     CFG_ADR_MAC_1M_FLASH
#define     CFG_ADR_MAC_1M_FLASH          0xFF000
#endif

(2) 0xFE000~0xFEFFF这个sector存储telink MCU需要校准定制的信息。只有这部分的信息不遵循“不同类信息放在不同sector”的原则,将这个sector 4096 bytes按照每64 bytes划分为不同的单元,每个单元存储一类校准信息。校准信息可以放在同一个sector,是因为校准信息在治具烧录的过程中烧到对应地址,实际Firmware在运行时只能读这些校准信息,而不允许去写,更不允许去擦除。具体的分配为:

  • 第一个64bytes存储频偏校准信息,实际校准值只有一个byte,存储在0xFE000。

  • 第二个64bytes存储TP值校准:B91m系列芯片不需要。

  • 第三个64bytes用来存储外部32k crystal的电容校准值,存储在0xFE080。

  • 第四个64bytes用来存储内部ADC校准值,存储在0xFE0C0。

  • 后面的空间留着做其他可能需要的校准值。

(3) 在B91中,0xFC000~0xFDFFF这两个sector被BLE协议栈系统占用,用来存储配对和加密信息。user也可以修改这两个sector的位置,size固定为两个sector 8K,无法修改,可以调用下面函数修改配对加密信息存储的起始地址:

void    bls_smp_configParingSecurityInfoStorageAddr (int addr);

(4) 0x00000~0x3FFFF 256KB空间默认作为程序空间。0x00000\~0x3FFFF共256KB为Firmware存储空间,0x40000~0x7FFFF 256KB为OTA更新时存储新Firmware的空间,即支持的Firmware空间理论上为256KB。

若默认的256K程序空间对user来说太小,而user希望Firmware空间为512KB,协议栈也提供了相应的API,修改方法见后面OTA章节的详细说明。

(5) 剩余的Flash空间全部作为user的数据存储空间。

B92 1MB Flash地址分配

上图所示为B92 Flash地址分配,user可以根据自己需要去规划地址分配。B92默认的地址分配方法以及对应的修改地址的接口为:

(1) 0xFF000~0xFFFFF这个sector存储MAC地址。

(2) 0xFE000~0xFEFFF这个sector存储telink MCU需要校准定制的信息。

(3) 0x00000~0x3FFFF 256KB空间默认作为程序空间。

以上部分B92与B91相同,这里不多作介绍,请参考上面B91的例子。B92与B91地址分配不同之处为:

(1) 0xFC000~0xFDFFF这两个sector暂时保留,用于未来其他用途。

(2) 0xF8000~0xFBFFF这四个sector用于存储Secure Boot描述符。用户如果不需要Secure Boot,可以自定义这四个sector的存储空间。

(3) 0xF6000~0xF7FFF这两个sector出于安全角度保留,防止配对和加密信息出现bug超出规定范围,导致secure boot破坏。

(4) 0xF4000~0xF5FFF这两个sector被BLE协议栈系统占用,用来存储配对和加密信息。user也可以修改这两个sector的位置,size固定为两个sector 8K,无法修改,可以调用下面函数修改配对加密信息存储的起始地址:

void    bls_smp_configParingSecurityInfoStorageAddr (int addr);

(5) 剩余的Flash空间全部作为user的数据存储空间。

时钟模块

Clock简述

B91m的Clock相对比较复杂,如下图所示为部分时钟的时钟树。这里介绍几个比较重要的时钟,pll_clk/cclk/hclk/pclk/clk_mspi/clk_stimer。

B91 Clock Tree

B92 Clock Tree

  • clk_pll:即图中的PLL,它是很多模块时钟源的源头,包括sys_clk一般使用的也是从PLL分频过来的。

  • cclk:即cpu clk,程序运行的速度是由该时钟决定的,同时他也是hclk和pclk的唯一的时钟源头。

  • hclk:所有挂在AHB总线上的模块均使用hclk。

  • pclk:所有挂在APB总线上的模块均使用pclk。

  • clk_mspi:mspi连接Flash,进行Flash的相关操作(包括取指,读写Flash等)均是受该时钟控制。

  • clk_stimer:系统定时器(System Timer)是一个只读的定时器,为BLE的时序控制提供时间基准,同时也可以提供给用户使用,B91 System Timer是由外部24M Crystal Oscillator经2/3分频得到的16MHz,B92 System Timer是由外部24M Crystal Oscillator直接输出的24MHz。

图上可以看到,sys_clk(system clock)一般由外部24M晶体振荡器倍频/分频得到。

初始化时调用API clock_init()配置pll_clk/cclk/hclk/pclk/clk_mspi,以B91为例,SDK已经定义了常用的一些时钟。

//PCLK can't larger than 24MHz, HCLK can't larger than 48MHz
#define     CCLK_16M_HCLK_16M_PCLK_16M      clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV12_TO_CCLK, CCLK_DIV1_TO_HCLK, HCLK_DIV1_TO_PCLK, PLL_DIV4_TO_MSPI_CLK)
#define     CCLK_24M_HCLK_24M_PCLK_24M      clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV8_TO_CCLK, CCLK_DIV1_TO_HCLK, HCLK_DIV1_TO_PCLK, PLL_DIV4_TO_MSPI_CLK)
#define     CCLK_32M_HCLK_32M_PCLK_16M      clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV6_TO_CCLK, CCLK_DIV1_TO_HCLK, HCLK_DIV2_TO_PCLK, PLL_DIV4_TO_MSPI_CLK)
#define     CCLK_48M_HCLK_48M_PCLK_24M      clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV4_TO_CCLK, CCLK_DIV1_TO_HCLK, HCLK_DIV2_TO_PCLK, PLL_DIV4_TO_MSPI_CLK)
#define     CCLK_64M_HCLK_32M_PCLK_16M      clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV3_TO_CCLK, CCLK_DIV2_TO_HCLK, HCLK_DIV2_TO_PCLK, PLL_DIV4_TO_MSPI_CLK)
#define     CCLK_96M_HCLK_48M_PCLK_24M      clock_init(PLL_CLK_192M, PAD_PLL_DIV, PLL_DIV2_TO_CCLK, CCLK_DIV2_TO_HCLK, HCLK_DIV2_TO_PCLK, PLL_DIV4_TO_MSPI_CLK)

B91的System Timer的频率是固定的16MHz,B92的System Timer的频率是固定的24MHz。以下以B91为例,对于这个timer,SDK code中使用如下的数值来表示s、ms和us。由于System Timer是BLE计时的基准,SDK中所有BLE时间相关的参数和变量,在涉及到时间的表达时,都是用“SYSTEM_TIMER_TICK_xxx”的方式。

enum{
    SYSTEM_TIMER_TICK_1US       = 16,//B92: 24
    SYSTEM_TIMER_TICK_1MS       = 16000,//B92: 24000
    SYSTEM_TIMER_TICK_1S        = 16000000,//B92: 24000000

    SYSTEM_TIMER_TICK_625US     = 10000,  //625*16, B92: 625 * 24
    SYSTEM_TIMER_TICK_1250US    = 20000,  //1250*16, B92: 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/16us加1,到最大值0xffffffff需要的时间为:(1/16) us * (2^32) 约等于268秒,每过268秒System Timer tick转一圈。B92每1/24us加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();

BLE SDK整个BLE时序都是基于System Timer tick设计的,程序中也大量使用这个System Timer tick来完成各种计时和超时判断,强烈推荐user使用这个System Timer tick来实现一些简单的定时和超时判断。

比如要实现一个简单的软件定时。软件定时器的实现基于查询机制,由于是通过查询来实现,不能保证非常好的实时性和准备性,一般用于对误差要求不是特别苛刻的应用。实现方法:

(1) 启动计时:设置一个u32的变量,读取并记录当前System Timer tick。

u32 start_tick = clock_time();   // clock_time() returns System Timer tick value

(2) 在程序的某处不断查询当前System Timer tick和start_tick的差值是否超过需要定时的时间值。若超过,认为定时器触发,执行相应的操作,并根据实际的需求清除计时器或启动新一轮的定时。

假设需要定时的时间为100 ms,查询计时是否到达的写法为:

if( (u32) ( clock_time() - start_tick) >  100 * SYSTEM_TIMER_TICK_1MS)

由于将差值转化为u32型,解决了system clock tick从0xffffffff到0这个极限情况。

实际上SDK中为了解决系统时钟不同导致和u32转换的问题,提供了统一的调用函数,不管系统时钟多少,都可以下面函数进行查询判断:

if( clock_time_exceed(start_tick, 100 * 1000))  //第二个参数单位为us

需要注意的是:由于System Timer转一圈,B91为268秒,B92为178秒,这个查询函数只适用于该范围以内的定时。如果超过,需要在软件上加计数器累计实现(这里不介绍)。

应用举例:A条件触发(只会触发一次)的2 second后,程序进行B()操作。

u32  a_trig_tick;
int  a_trig_flg = 0;
while(1)
{
        if(A){
            a_trig_tick = clock_time();
            a_trig_flg = 1;
        }
        if(a_trig_flg && clock_time_exceed(a_trig_tick,2 *1000 * 1000)){
            a_trig_flg = 0;
            B();
        }   
}

中断嵌套

中断嵌套功能简述

B91m系列的芯片支持中断嵌套功能,先说明下三个概念:中断优先级,中断阈值,中断抢占。

(1) 中断优先级是每个中断的等级,在初始化中断的时候需要配置;

(2) 中断阈值是指响应中断的阈值,只有中断优先级高于中断阈值的中断才会被触发;

(3) 中断抢占是指当两个中断的优先级都高于中断阈值,如果当前较低优先级的中断正在被响应,较高优先级的中断可以被触发,抢占较低优先级的中断,执行完较高优先级的中断后再继续执行较低优先级的中断。

注意:

中断嵌套功能默认是打开的,并且中断阈值默认为0。

中断优先级可以设置的范围1~3,中断优先级目前只能支持最高设置到3,数字越大优先级越高,优先级的枚举如下:

typedef enum{
    IRQ_PRI_LEV0,//Never interrupt
    IRQ_PRI_LEV1,
    IRQ_PRI_LEV2,
    IRQ_PRI_LEV3,
}irq_priority_e;

BLE SDK中断嵌套示意图

如上图所示,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,并且code必须全部放到ram_code段,它会抢占BLE中断和APP普通中断,所以用户必须限制执行时间小于50us以免影响到BLE中断。

在这里解释一下为什么APP高级中断的code必须全部放到ram_code。在执行Flash空间的擦除、读和写操作函数时,会将中断阈值设置为1,执行完Flash操作函数后再将中断阈值设置为0。这样的话在执行Flash操作函数的过程中,BLE中断和APP高级中断的优先级都是大于中断阈值1,他们都有可能在执行Flash操作函数的过程中被触发,如果触发的中断函数没有放到ram_code段,就会导致Flash预取指操作和读写Flash操作出现时序冲突,造成死机。BLE中断的相关code都已经放入ram_code段,所以也要求APP高级中断的相关code必须全部放到ram_code段。

中断嵌套的使用

(1) App普通中断

比如用户想设置一个PWM的APP普通中断,在配置中断的时候定义中断优先级为IRQ_PRI_LEV1,方法如下。

plic_set_priority(IRQ16_PWM, IRQ_PRI_LEV1);

中断响应函数类型不限。

void pwm_irq_handler(void)
{
        ……
}

(2) App高级中断

比如用户想设置一个Timer0的APP高级中断,在配置中断的时候定义中断优先级为IRQ_PRI_LEV3,注意控制Timer0的中断执行时间小于50us,方法如下。

plic_set_priority(IRQ4_TIMER0, IRQ_PRI_LEV3);

中断响应函数必须定义为ram_code段,方法如下。

_attribute_ram_code_ void timer0_irq_handler(void)
{
        ……
}

BLE模块

BLE SDK软件架构

标准BLE SDK 软件架构

根据BLE spec,一个比较标准的BLE SDK架构如下图所示。

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完成数据交互,如下图所示。

Host和Controller的HCI数据交互

(1) BLE Host通过HCI cmd去操作设置Controller,这些HCI cmd对应本章后面将要介绍的controller API。

(2) Controller通过HCI向host上报各种HCI event,本章也会具体介绍。

(3) Host将需要发送给对方设备的数据通过HCI传送到Controller,Controller将数据直接丢到Physical Layer进行发送。

(4) Controller在Physical Layer收到的RF数据,先判断是属于Link Layer的数据还是Host的数据:如果是Link Layer的数据,直接处理数据;如果是Host的数据,则通过HCI将数据传到Host。

B91m Single Connection SDK 只支持slave,关于Master和Controller软件架构,具体内容和实现方法可参考BLE Multi Connection SDK Handbook,下面将讲述Slave role内容:

Telink BLE Slave

Telink BLE SDK在BLE host上,只对Slave部分的stack完全支持。

当user只需要使用标准的BLE slave时,Telink BLE SDK运行Host(slave部分) + 标准的Controller,实际的协议栈架构会对上面标准的结构做一些简化处理,使得整个SDK的系统资源开销(包括sram、运行时间、功耗等)最小,其架构如下图所示。SDK中B91m ble sample、B91m module都是基于该架构。

Telink BLE Slave架构

图中实心箭头所示的数据交互是user可以通过各种接口来操作控制的,会提供user API。空心箭头是协议栈内部完成的数据交互,user无法参与。

Controller部分HCI仍然是与Host的数据通信接口(和L2CAP层对接),但不是唯一的接口,APP应用层也可以直接与Link Layer进行数据交互。Power Management(PM)低功耗管理单元被内嵌到Link layer,应用层可以调用PM相关接口进行功耗管理的设置。

考虑到效率,应用层与Host的数据交互不通过GAP来访问控制,协议栈在ATT、SMP和L2CAP都提供了相关接口,可以和应用层直接交互。但是Host的事件需要通过GAP层和应用层交互。

Host层以Attribute Protocol为基础,实现了Generic Attribute Profile(GATT)。应用层基于GATT,定义user自己需要的各种profile和service。该BLE SDK demo code提供几个基本的profile,包括HIDS、BAS、AUDIO、OTA等。

下面基于这个架构对B91m BLE协议栈各部分做一些基本的介绍,并给出各层的user API。

其中Physical Layer完全由Link Layer控制,且不需要应用层任何的参与,这部分不介绍。

虽然Host与Controller的部分数据交互还是靠HCI来完成,但基本都是Host和Controller协议栈完成,应用层几乎不参与,只需要在L2CAP层注册HCI数据回调处理函数就行了,所以对HCI部分也不做介绍。

BLE Controller

BLE Controller介绍

BLE controller包括Physical Layer、Link Layer、HCI和Power Management。

Telink BLE SDK对Physical Layer(对应drivers文件中的rf.h的c文件)完全封装到library中,user不需要了解。Power Management将在本文档后面的章节中详细介绍,本章只稍微提一下,不做过多介绍。

本章只介绍Link Layer和HCI,且由于HCI主要是interface,其实现的功能也都是为了操作Link Layer和获取Link Layer的数据,所以重点详细介绍Link layer,并在介绍Link Layer和API的过程中对HCI相关interface一一描述。

下图为BLE spec中Link Layer状态机。

更多信息请参照《Core_v5.0》(Vol 6/Part B/1.1 “LINK LAYER STATES”)。

BLE Spec中Link Layer状态机

Telink BLE SDK Link Layer状态机如下图所示。

Telink Link Layer状态机

Telink BLE SDK Link Layer状态机和BLE spec定义的一致,拥有5种基本状态:Idle(Standby)、Scanning、Advertising、Initiating、Connection。将Connection状态区分为Slave Role和Master Role。

由文档前面对library的介绍,目前的Slave Role和Master Role都是基于single connection的设计,Slave Role默认为single connection的;但由于会提供multi connection的,所以这里名为Master role single connection。

本文档中将Slave Role后面称为Conn state Slave role或ConnSlaveRole/Connection Slave Role或简写为ConnSlaveRole。

本文档中将Master Role后面称为Conn state Master role或ConnMasterRole/Connection Master Role或简写为ConnMasterRole。

图中Power Management并不是LL的一种状态,而是功能模块,表示的是SDK只对Advertising和Connection Slave Role进行了低功耗处理;Idle state如果需要低功耗,user在应用层调用相关API可以完成;其他几个state,SDK没有做低功耗管理,user也无法在应用层实现低功耗。

基于上面5种状态,stack/ble/controller/ll/ll.h中定义了状态机的命名。其中ConnSlaveRole和ConnMasterRole时状态名都是BLS_LINK_STATE_CONN。

#define BLS_LINK_STATE_IDLE         0
#define BLS_LINK_STATE_ADV          BIT(0)
#define BLS_LINK_STATE_SCAN         BIT(1)
#define BLS_LINK_STATE_INIT         BIT(2)
#define BLS_LINK_STATE_CONN         BIT(3)

Link Layer状态机的切换都在BLE stack底层自动完成,所以user在应用层无法去修改状态,只能获取当前状态,调用下面API即可,返回值为上面5种状态中的1种。

u8      blc_ll_getCurrentState(void);

(1) Link Layer状态机初始化

Telink BLE SDK Link Layer完整的支持所有状态,但在设计上很灵活,每一个状态都封装成为一个模块,默认情况下只有最基本的Idle模块,user通过添加模块的方式去搭建自己的状态机组合,从而实现不同的应用。比如user需要的应用是BLE slave,那么只需要添加Advertising模块和ConnSlaveRole模块就可以了,剩下的Scanning/Initiating等模块没有被配置进来。这样设计的目的是节省code size和ram_code,不需要使用的状态的相关代码不会被编译进来。

MCU的初始化是必须的,API如下:

void        blc_ll_initBasicMCU (void);

Idle模块的添加API如下,这个是必须的,所有的BLE应用都需要初始化。

void        blc_ll_initStandby_module (u8 *public_adr);

其他几个状态(Advertising、Connection、Slave Role)对应模块的初始化API分别如下:

void        blc_ll_initLegacyAdvertising_module(void);
void        blc_ll_initConnection_module(void);
void        blc_ll_initSlaveRole_module(void);

其中:

void        blc_ll_initConnection_module(void);

用于初始化master和slave共用的module。

User通过以上几个API去配合组合Link Layer状态机,下面给出几个常用的组合方式和对应的应用场景,但不仅限于以下几种组合,user可以自行配置组合。

(2) Idle + Advertising

Idle + Advertising

上图所示,只初始化Idle和Advertising模块,使用最基本的Advertising功能实现的应用如beacon等,单方向广播产品信息。

Link Layer状态机模块初始化代码为:

u8  mac_public[6];
blc_ll_initBasicMCU();  
blc_ll_initStandby_module(mac_public);
blc_ll_initLegacyAdvertising_module();

Idle和Advertising状态的切换通过bls_ll_setAdvEnable来实现。

(3) Idle + Advertising + ConnSlaveRole

BLE Slave LL State

上图所示为一个基本的BLE slave应用的Link Layer状态机组合,SDK中B91m_hci/B91m_ble_sample/B91m_module都是基于该状态机组合。

Link Layer状态机模块初始化代码为:

u8  mac_public[6];
blc_ll_initBasicMCU();                     
blc_ll_initStandby_module(mac_public);
blc_ll_initLegacyAdvertising_module();
blc_ll_initConnection_module();
blc_ll_initSlaveRole_module();

该状态机组合下,状态变化情况描述如下。

1) MCU上电后,进入Idle state。Idle state时将Adv Enable,Link Layer切换到Advertising state;Adv Disable时,回到Idle state。

Adv Enable和Disable通过下面API bls_ll_setAdvEnable来控制。

上电后,Link layer默认处于Idle状态,一般需要在user_init_normal里面将Adv Enable,进入Advertising state。

2) Link Layer处于Idle state时,Physical Layer不进行任何RF相关动作,不收包也不发包。

(3) Link Layer处于Advertising state时,在广播channel上发送广播包。若master收到广播包,并发送connection request,Link Layer收到这个connection request后,响应并建立连接,Link Layer进入ConnSlaveRole。

4) Link Layer处于ConnSlaveRole时,有三种情况会回到Idle State或者Advertising state:

  • master向slave发送terminate命令,断开连接。slave收到terminate命令,退出ConnSlaveRole。

  • slave向master发送terminate命令,主动断开连接,退出ConnSlaveRole。

  • slave的RF收包异常或master发包异常,导致slave长时间收不到包,触发BLE的connection supervision timeout,退出ConnSlaveRole。

Link Layer的ConnSlaveRole退出该state时,根据Adv是否被Enable切换到不同state:若Adv被Enable,Link Layer重新回到Advertising state;若Adv被Disable,Link Layer回到Idle state。Adv处于Enable或者Disable取决于user在应用层最后一次调用bls_ll_setAdvEnable时设置的值。

结合该BLE SDK的rf_irq_handler,stimer_irq_handler和main_loop来说明Link layer在各状态的工作时序。

_attribute_ram_code_ void rf_irq_handler(void)
{
    ……
    irq_blt_sdk_handler ();
    ……
}
_attribute_ram_code_ void stimer_irq_handler(void)
{
    ……
    irq_blt_sdk_handler ();
    ……
}
void main_loop (void)
{
///////////////////// BLE entry ////////////////////////////
    blt_sdk_main_loop();
////////////////////// UI entry ////////////////////////////
    ……
}

BLE entry部分blt_sdk_main_loop函数负责处理BLE协议栈相关的数据和事件。UI entry是user写自己的应用代码的地方。

(1) Idle State时序

当Link Layer处于Idle state时,Link Layer和Physical Layer没有任何任务要处理,blt_sdk_main_loop 函数完全不起作用,也不会产生任何中断。可以认为UI entry占据了整个main_loop的时序。

(2) Advertising State时序

Advertising State时序

Advertising state时序图如上所示。每一个Adv interval时间内Link Layer触发一个Adv event。一个典型的3个adv channel都发包的Adv event会在channel 37、38、39上都发送一个广播包。每发一个广播包之后都进入收包状态,等待master可能的回包,回包有两种:一个是scan request,收到这个包后再发送一个scan response作为回复;另一个是connect request,收这个包后和master建立ble连接,进入Conn state Slave role。

user在main_loop的UI entry部分的code会在图中所示的UI task/suspend部分执行,这部分时间全部可以用来做UI task,如果需要低功耗的话,可以将多余的时间用来sleep(suspend/deepsleep retention)以降低功耗。

在Advertising state,blt_sdk_main_loop函数没有太多要处理的任务,只是触发几个跟Adv相关的回调事件。包括BLT_EV_FLAG_ADV_DURATION_TIMEOUT、BLT_EV_FLAG_SCAN_RSP、BLT_EV_FLAG_CONNECT等。

(3) Scanning State时序

Scanning State时序

Scanning state时序图如上所示,Scan interval是由API blc_ll_setScanParameter设置。整个Scan interval对一个channel进行收包,没有将Scan window设计到SDK里,所以blc_ll_setScanParameter中对Scan window的设置SDK是不处理的。

每个Scan interval结束后,切换到下一个收包的channel,开始新一轮的Scan interval。切换channel的动作是interrupt触发的,在irq中完成该动作,其执行时间非常短。

Scan interval上,Scan状态PHY层一直处于RX状态,靠MCU硬件去实现收包操作,所以软件上的timing都留给了UI task。

在Scan interval上收到正确的BLE packet后,将收包数据先缓存在软件RX fifo中(对应code中的my_fifo_t blt_rxfifo),blt_sdk_main_loop函数会检查软件RX fifo中是否有数据,如果发现正确的adv数据会通过event“HCI_SUB_EVT_LE_ADVERTISING_REPORT”将其report给BLE host。

(4) Conn State Slave Role时序

Conn State Slave Role时序

ConnSlaveRole时序图如上所示。每一个conn interval开始的时候,Link Layer进行一次BLE的RF包收发过程:先让PHY进入收包状态,收到master的包后发送一个ack包回复,若有more data,则继续收master包并回复,这个过程简称为brx event。

在该BLE SDK中,根据软硬件的工作分配,每个brx过程分为3个阶段:

1) brx start阶段

当master发包的时间快要到来时,会由reg_system_tick_irq触发进入brx start阶段,在这个中断里MCU设置PHY的BLE状态机进入brx状态,底层硬件做好收发包的相关准备,然后退出中断irq。

2) brx working阶段

brx start结束后,MCU退出irq后,底层硬件进入收包状态,并自动完成收发包的所有工作,不需要软件任何的参与,这个过程称为brx working阶段。

3) brx post阶段

收发包完成后,brx working结束,同样由reg_system_tick_irq触发进入中断执行brx post阶段。这个阶段主要是协议栈根据brx working阶段收发包的情况对BLE的一些数据和时序进行相关的处理。

上面三个阶段中brx start和brx post都是中断完成,而brx working阶段不需要软件的参与,此时UI task可以正常执行(注意brx working阶段RX、TX、系统定时器中断等处理函数以外的时隙是可以跑UI task的)。brx working时间内,硬件需要进行收发包,所以不能进sleep(suspend/deepsleep retention)。

整个conn interval内除去brx过程的时间,可以用来做UI task,如果需要低功耗的话,可以将多余的时间用来sleep(suspend/deepsleep retention)以降低功耗。

在ConnSlaveRole,blt_sdk_main_loop需要处理brx过程收到的数据。brx working过程中,实际是在RX接收中断irq处理中将硬件收到的master数据包拷贝出来,这些数据并不会立刻实时处理,而是将数据缓存到软件RX fifo中(对应code中的my_fifo_t blt_rxfifo)。blt_sdk_main_loop函数会检查软件RX fifo中是否有数据,只要有数据就去处理。

blt_sdk_main_loop对数据包的处理包括:

1) 数据包的解密

2) 数据包的解析

解析的数据若发现是属于master发给Link Layer的控制命令,立即执行该命令,若是master发给Host层的数据,则会通过HCI接口将数据丢到L2CAP层处理。

上⾯ BLE Link Layer 状态机和⼯作时序介绍了最基本的⼏种状态,能够满⾜ BLE slave/master 等基本应⽤。但是考虑到 user 可能会有的⼀些特殊的应⽤(⽐如在 Conn state Slave role 时还要能够 advertising), Telink BLE SDK 对 Link Layer 的状态机添加了⼀些特殊的扩展的功能。下面对状态机扩展的状态:ADVERTISING_IN_CONN_SLAVE_ROLE、SCANNING_IN_ADV 和 SCAN_IN_CONN_SLAVE_ROLE 进行详细描述。

(1) ADVERTISING_IN_CONN_SLAVE_ROLE

在 Link Layer 处于 ConnSlaveRole 时,可以添加 Advertising feature。

添加 Advertising feature 的 API 为:

ble_sts_t   blc_ll_addAdvertisingInConnSlaveRole(void);

去掉 Advertising feature 的 API 为:

ble_sts_t   blc_ll_removeAdvertisingFromConnSLaveRole(void);

以上两个 API ble_sts_t 类型的返回值都是 BLE_SUCCESS。

结合Advertising 和 ConnSlaveRole 的时序图,当加⼊ Advertising feature 到 ConnSlaveRole 后,时序图如下

Advertising in ConnSlaveRole时序

当前 Link Layer 还是处于 ConnSlaveRole(BLS_LINK_STATE_CONN),在每个 Conn interval 内 brx event 结束后,⽴刻执⾏⼀次 adv event,然后剩余的时间留给 UI task 或进⼊ sleep(suspend/deepsleep retention)节省功耗。

Advertising in ConnSlaveRole 的使⽤请参考 B91m_feature_test 中的 TEST_ADVERTISING_IN_CONN_SLAVE_ROLE。

(2) ADVERTISING_IN_CONN_SLAVE_ROLE

在 Link Layer 处于 Advertising state 时,可以添加 Scanning feature。

添加 Scanning feature 的 API 为:

ble_sts_t   blc_ll_addScanningInAdvState(void);

去掉 Scanning feature 的 API 为:

ble_sts_t   blc_ll_removeScanningFromAdvState(void);

以上两个 API ble_sts_t 类型的返回值都是 BLE_SUCCESS。

结合 Advertising state 和 Scanning state 的时序图,当加⼊ Scanning feature 到 Advertising state 后,时序图如下。

Scanning in Advertising state时序

当前 Link Layer 还是处于 Advertising state(BLS_LINK_STATE_ADV),在每个 Adv interval 内,除去 Adv event,剩余的时间全部⽤来做 Scanning。

在每个 Set Scan 的时候,会判断⼀下当前时间距离上次 Set Scan 时间点是否超过⼀个 Scan interval(来⾃ blc_ll_setScanParameter 的设定),若超过则切换 Scan 的 channel(channel 37/38/39)。

Scanning in Advertising state 的使⽤请参考 B91m_feature_test 中的 TEST_SCANNING_IN_ADV_AND_CONN_SLAVE_ROLE。

(3) SCAN_IN_CONN_SLAVE_ROLE

在 Link Layer 处于 ConnSlaveRole 时,可以添加 Scanning feature。

添加 Scanning feature 的 API 为:

ble_sts_t   blc_ll_addScanningInConnSlaveRole(void);

去掉 Scanning feature 的 API 为:

ble_sts_t   blc_ll_removeScanningFromConnSLaveRole(void);

以上两个 API ble_sts_t 类型的返回值都是 BLE_SUCCESS。

结合 Scanning state 和 ConnSlaveRole 的时序图,当加⼊ Scanning feature 到 ConnSlaveRole 后,时序图如下。

Scanning in ConnSlaveRole state时序

当前 Link Layer 还是处于 ConnSlaveRole(BLS_LINK_STATE_CONN),在每个 Conn interval 内,除去 brx event 剩余的时间全部⽤来做 Scanning。

在每个 Set Scan 的时候,会判断⼀下当前时间距离上次 Set Scan 时间点是否超过⼀个 Scan interval(来⾃ blc_ll_setScanParameter 的设定),若超过则切换 Scan 的 channel(channel 37/38/39)。

Scanning in ConnSlaveRole 的使⽤请参考 B91m_feature_test 中的 TEST_SCANNING_IN_ADV_AND_CONN_SLAVE_ROLE。

Slave role的BLE TX fifo和BLE RX fifo的处理方式一致。

(1) Link Layer RX fifo介绍

Link Layer brx时收到的所有peer device的数据都会先存放在一个BLE RX fifo中,然后才上传给BLE Host或应用层处理。

BLE RX fifo在应用层定义:

#define ACL_RX_FIFO_SIZE    CAL_LL_ACL_RX_FIFO_SIZE(ACL_CONN_MAX_RX_OCTETS)
#define ACL_RX_FIFO_NUM     8
_attribute_data_retention_  u8  app_acl_rxfifo[ACL_RX_FIFO_SIZE * ACL_RX_FIFO_NUM] = {0};
blc_ll_initAclConnRxFifo(app_acl_rxfifo, ACL_RX_FIFO_SIZE, ACL_RX_FIFO_NUM);

ACL_RX_FIFO_SIZE必须使用CAL_LL_ACL_RX_FIFO_SIZE来计算,用户可通过修改ACL_CONN_MAX_RX_OCTETS来改变ACL_RX_FIFO_SIZE。

ACL_RX_FIFO_NUM必须设置为2的幂,即2、4、8、16等值。User可以根据自己的需要稍作修改。

ACL_RX_FIFO_NUM默认为8,这是一个比较合理的值,能够确保Link Layer底层最多缓存8个数据包。如果设置的太大,会占用过多的Sram,如果设置的太小,可能出现数据覆盖的风险:在brx event时,Link Layer很可能在一个interval上出现more data(MD)模式,连续收多个包,如果设置4的话,很可能在一个interval上出现五六个包(比如OTA、播放master语音数据等情况),而上层对这些数据的响应由于解密时间较长来不及处理,那么就有可能有一些数据被overflow。

下面举一个RX overflow的示例,我们有如下假设:

a) RX fifo数量为8;

b) 在brx_event(n)开启前RX fifo的读、写指针分别为0和2;

c) 在brx_event(n)和brx_event(n+1)阶段main_loop存在任务阻塞,没有及时去取RX fifo;

d) 两个brx_event阶段都是多包情况。

由上文“Conn State Slave Role时序”小节描述我们知道,在brx working阶段收到的BLE数据包只会拷贝到RX fifo中(RX fifo写指针++),真正取出RX fifo数据进行处理操作是放在main_loop阶段的(RX fifo读指针++),我们可以看到第6笔数据会覆盖读指针0区域。这里需要注意的是在brx working阶段的UI task时隙是在RX、TX、系统定时器等中断处理除外的时间。

RX overflow图示1

上面的例子由于间隔了1个连接间隔,任务阻塞时间得要足够长,有点极端,下面这个RX overflow情形则相对而言出现的概率更高:在一个brx event期间,master向slave写入多笔数据,比如多包数量7、8个,这种情况下由于master一下子发送了很多数据,slave来不及进行处理。如下图,读指针只移动了2笔,但是写指针移动了8笔也会造成数据溢出。

RX overflow图示2

一旦出现overflow导致的数据丢失问题,对加密系统而言,则会出现MIC failure断连问题。SDK 具有Rx overflow校验:在irq_blc_ll_rx中检查当前的RX fifo写指针和读指针差值是否大于Rx fifo数量,一旦发现Rx fifo被占满则让RF不去ACK对方,BLE协议会确保数据重传,此外SDK还提供了Rx overflow回调函数以便通知用户,文档后面章节“Telink defined event”会介绍这个回调。

同理如果可能出现一个interval多于8个有效数据包的话,默认的8也就不够用了。

(2) Link Layer TX fifo介绍

应用层和BLE Host所有的数据最终都需要通过Controller的Link Layer完成RF数据的发送,在Link Layer中设计了一个BLE TX fifo,可以用来缓存传过来的数据,并在brx开始后进行数据发送。

BLE TX fifo在应用层定义:

#define ACL_TX_FIFO_SIZE    CAL_LL_ACL_TX_FIFO_SIZE(ACL_CONN_MAX_TX_OCTETS)
#define ACL_TX_FIFO_NUM     17
_attribute_data_retention_  u8  app_acl_txfifo[ACL_TX_FIFO_SIZE * ACL_TX_FIFO_NUM] = {0};
blc_ll_initAclConnTxFifo(app_acl_txfifo, ACL_TX_FIFO_SIZE, ACL_TX_FIFO_NUM); 

ACL_TX_FIFO_SIZE必须使用CAL_LL_ACL_TX_FIFO_SIZE来计算,用户可通过修改ACL_CONN_MAX_TX_OCTETS来改变ACL_TX_FIFO_SIZE。

ACL_TX_FIFO_NUM必须设置为2的幂加1,即3、5、9、17等值。User可以根据自己的需要稍作修改。

TX fifo number默认为17,能够处理数据量较大的语音数据功能。User如果用不到这么大的fifo,可以修改为9。

需要注意的是(ACL_TX_FIFO_SIZE * (ACL_TX_FIFO_NUM - 1))不能超过0xFFF (4096)。

TX fifo中,SDK底层stack需要用到3个,剩下的才完全由应用层使用,TX fifo为17时,应用层只能用14个;为9时应用层只能用6个。

User在应用层发送数据时(比如调用blc_gatt_pushHandleValueNotify),应该先检查一下当前Link Layer还有多少TX fifo可用。

下面API用于判断当前TX fifo被占用了多少个,注意不是剩余多少个可用。

u8      blc_ll_getTxFifoNumber (void);

比如TX fifo number默认为17时,user可用为14个,所以该API返回的值只要小于14,就是可用的:返回13表示还有1个可用,返回0则表示还有14个可用。

TX fifo使用上,如果客户先看多少个剩余,再决定是否直接push数据时,要留一个fifo,防止发生各种边界问题。

在B91m Audio的语音处理中,由于已知每笔语音数据会被拆成5个包,需要5个TX fifo,被占用的fifo不能超过9个。为了避免TX fifo使用时的一些边界条件导致的异常(比如正好赶上BLE stack要回复master的command,往TX fifo里插入了一个数据),最终code写法如下:当已被占用的TX fifo不超过8个时,才将语音数据push到TX fifo。

if (blc_ll_getTxFifoNumber() < 9)
    {
        ……
    }

上面讨论过数据溢出问题,SDK底层除了提供数据快要overflow自动处理机制外,还提供了下面的API用于限定一个interval上more data接收数量(如果客户希望RX fifo足够用的情况下也对数据进行限制,则可以使用)。

void        blc_ll_init_max_md_nums(u8 num);

Controller Event

为了满足user在应用层对BLE stack底层一些关键动作的记录和处理,Telink BLE SDK提供了三种类型的event:一是BLE Controller定义的标准的HCI event;二是Telink自己定义的一套event,称为Telink defined event(前两种类型均属于Controller event);三是BLE host定义的一些协议栈流程交互的事件通知型GAP event(也可以认为是host event,这部分具体介绍请参考本文档“GAP event”章节)。

BLE SDK event架构说明如下图所示,可以看到HCI event和Telink defined event均属于Controller event,而GAP event属于BLE host event。下面两小节内容主要介绍Controller event。

BLE SDK Event架构

(1) Controller HCI Event

HCI event是按BLE Spec标准设计的,而Telink defined event只在BLE slave(B91m_module等)上有效,即:

  • 对于BLE master,只有HCI event;

  • 对于BLE slave,HCI event和Telink defined event同时可用。

在BLE slave上,这两套event基本相互独立,只有两个event重复定义了:Link Layer的connect和disconnect event,后面会具体介绍。

User可以根据自己的需要,在这两套event挑选一套使用,也可以两套同时使用。Telink BLE SDK中,B91m_module等使用了Telink defined event。

如下图Host + Controller架构所示,Controller HCI event是通过HCI将Controller所有的event报告给Host。

HCI Event

Controller HCI event的定义,详情请参照《Core_v5.0》 (Vol 2/Part E/7.7 “Events”)。其中7.7.65“LE Meta Event”指HCI LE(low energy) Event,其他的都是普通的HCI event。根据Spec的定义,Telink BLE SDK也将Controller HCI event分为两类:HCI Event和HCI LE event。由于Telink 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打开。

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_TLK_MODULE       (1<<24)
#define         HCI_FLAG_EVENT_BT_STD           (1<<25)

HCI_FLAG_EVENT_TLK_MODULE在后面的Telink defined event会再介绍,HCI_FLAG_EVENT_BT_STD这个标志表示当前event为Controller HCI event。

callback函数原型中para和n表示event的数据和数据长度,该数据和BLE spec中定义的一致。

blc_hci_registerControllerEventHandler(controller_event_callback);

(2) HCI event

Telink BLE SDK中支持了少部分的HCI event,下面列出了几个user可能希望了解的event。

#define HCI_EVT_DISCONNECTION_COMPLETE                  0x05
#define HCI_EVT_ENCRYPTION_CHANGE                       0x08
#define HCI_EVT_READ_REMOTE_VER_INFO_COMPLETE       0x0C
#define HCI_EVT_ENCRYPTION_KEY_REFRESH                  0x30
#define HCI_EVT_LE_META                                 0x3E

a) HCI_EVT_DISCONNECTION_COMPLETE

详情请参照《Core_v5.0》(Vol 2/Part E/7.7.5 “Disconnection Complete Event”)。该event的总体数据长度为7,param len为4,如下所示,具体数据含义请直接参考BLE spec。

Disconnection Complete Event

b) HCI_EVT_ENCRYPTION_CHANGE 和 HCI_EVT_ENCRYPTION_KEY_REFRESH

详情请参照《Core_v5.0》(Vol 2/Part E/7.7.8 & 7.7.39)。跟Controller加密相关,具体的处理封装到library中完成了,这里不介绍细节。

c) HCI_EVT_READ_REMOTE_VER_INFO_COMPLETE

详情请参照《Core_v5.0》(Vol 2/Part E/7.7.12)。当Host使用了HCI_CMD_READ_REMOTE_VER_INFO命令Controller和BLE peer device交互version信息,并且收到了peer device的version后,向Host上报该event。该event的总体数据长度为11,param len为8,如下所示,具体数据含义请直接参考BLE spec。

Read Remote Version Information Complete Event

d) HCI_EVT_LE_META

表示当前是HCI LE event,根据后面的sub event code判断具体的event类型。HCI event中除了HCI_EVT_LE_META,其他都要通过下面API来打开event mask。

ble_sts_t   blc_hci_setEventMask_cmd(u32 evtMask);

event mask的定义如下所示:

#define HCI_EVT_MASK_DISCONNECTION_COMPLETE                                 0x0000000010     
#define HCI_EVT_MASK_ENCRYPTION_CHANGE                                      0x0000000080
#define HCI_EVT_MASK_READ_REMOTE_VERSION_INFORMATION_COMPLETE               0x0000000800 

若user未通过该API设置HCI event mask,SDK默认只打开HCI_CMD_DISCONNECTION_COMPLETE对应的mask,即保证Controller disconnect event的上报。

(3) HCI LE event

当HCI event中event code为HCI_EVT_LE_META,就是HCI LE event,sub event 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_CONNECTION_ESTABLISH                         0xFF  

a) HCI_SUB_EVT_LE_CONNECTION_COMPLETE

详情请参照《Core_v5.0》(Vol 2/Part E/7.7.65.1 “LE Connection Complete Event”)。当controller Link Layer和peer device建立connection后,上报该event。该event的总体数据长度为22,param len为19,如下所示,具体数据含义请直接参考BLE spec。

LE Connection Complete Event

b) HCI_SUB_EVT_LE_ADVERTISING_REPORT

详情请参照《Core_v5.0》(Vol 2/Part E/7.7.65.2 “LE Advertising Report Event”)。当controller的Link Layer scan到正确的adv packet后,通过HCI_SUB_EVT_LE_ADVERTISING_REPORT上报给Host。该event的数据长度不定(取决于adv packet的payload),如下所示,具体数据含义请直接参考BLE spec。

LE Advertising Report Event

注意:

Telink BLE SDK中的LE Advertising Report Event每次只报一个adv packet,即上图中的i为1。

c) HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE

详情请参照《Core_v5.0》(Vol 2/Part E/7.7.65.3 “LE Connection Update Complete Event”)。当Controller上的connection update生效时,向Host上报HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE。该event的总体数据长度为13,param len为10,如下所示,具体数据含义请直接参考BLE spec。

LE Connection Update Complete Event

HCI LE event需要通过下面的API来打开mask。

ble_sts_t   blc_hci_le_setEventMask_cmd(u32 evtMask);

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
#define HCI_LE_EVT_MASK_CONNECTION_ESTABLISH                0x80000000 

若user未通过该API设置HCI LE event mask,SDK默认所有HCI LE event都不打开。

(4) Telink Defined Event

除了标准的Controller HCI event,SDK提供了Telink defined event。Telink defined event最多支持20个,在stack/ble/ll/ll.h中用宏来定义。

目前SDK中支持以下回调事件。其中BLT_EV_FLAG_CONNECT/BLT_EV_FLAG_TERMINATE可理解为和HCI event中的HCI_SUB_EVT_LE_CONNECTION_COMPLETE /HCI_EVT_DISCONNECTION_COMPLETE功能上是重复的,只是event数据部分定义不同。

#define         BLT_EV_FLAG_ADV                     0
#define         BLT_EV_FLAG_ADV_DURATION_TIMEOUT    1
#define         BLT_EV_FLAG_SCAN_RSP                2
#define         BLT_EV_FLAG_CONNECT                 3
#define         BLT_EV_FLAG_TERMINATE               4
#define         BLT_EV_FLAG_LL_REJECT_IND           5
#define         BLT_EV_FLAG_RX_DATA_ABANDON         6
#define         BLT_EV_FLAG_PHY_UPDATE              7
#define         BLT_EV_FLAG_DATA_LENGTH_EXCHANGE    8
#define         BLT_EV_FLAG_GPIO_EARLY_WAKEUP       9
#define         BLT_EV_FLAG_CHN_MAP_REQ             10
#define         BLT_EV_FLAG_CONN_PARA_REQ           11
#define         BLT_EV_FLAG_CHN_MAP_UPDATE          12
#define         BLT_EV_FLAG_CONN_PARA_UPDATE        13
#define         BLT_EV_FLAG_SUSPEND_ENTER           14
#define         BLT_EV_FLAG_SUSPEND_EXIT            15
#define         BLT_EV_FLAG_VERSION_IND_REV         16

前面已经介绍,Telink defined event只在BLE slave相关应用中才会被触发。Telink defined event在BLE slave应用上的回调实现,有两种方式。

1) 第一种方式是每个event 的回调函数单独注册。这种方式我们称为“independent registration”。

回调函数的原型说明为:

typedef void (*blt_event_callback_t)(u8 e, u8 *p, int n);

其中e是event number;p为回调函数执行时,底层传上来相关数据的指针,不同回调函数传上来的指针数据都不一样;n是回传指针所指的有效数据长度。

注册回调函数的API为:

void    bls_app_registerEventCallback (u8 e, blt_event_callback_t p);

每一个event是否响应,取决于应用层是否有对应的回调函数被注册。没有被注册回调函数的event,不会响应。

2) 第二种方式是:所有event的回调函数共用同一个入口,每个事件是否响应取决于该event对应的event mask是否被打开。这种方式我们称为“shared event entry”。“shared event entry”方式注册event回调是使用和HCI event一样的API:

typedef int (*hci_event_handler_t) (u32 h, u8 *para, int n);
void  blc_hci_registerControllerEventHandler(hci_event_handler_t  handler);

虽然这里共用了HCI event的注册回调函数,但二者在实现上有一些区别。HCI event回调函数里面:

h = HCI_FLAG_EVENT_BT_STD  |  hci_event_code;

Telink defined event “shared event entry”方式里面:

h = HCI_FLAG_EVENT_TLK_MODULE  |  e;

其中e是Telink defined event的event number。

Telink defined event “shared event entry”方式,类似于HCI event的mask方法,每一个event是否被响应,由下面的API来设置其mask:

ble_sts_t   bls_hci_mod_setEventMask_cmd(u32 evtMask);

evtMask的值和event number的对应关系为:

evtMask = BIT(e);

Telink defined event的两种实现方式,是互斥独立的,只能使用一种。推荐使用方式1 “independent registration”,SDK中绝大多数都是用的这种方式;只有B91m_module使用了方式2 “shared event entry”。

在Telink defined event的使用上,方式2 “shared event entry”请参考project “B91m_module”的demo code。

下面以connect和terminate事件回调为例,描述这两种方式的code实现的方法。

1) 方式一 “independent registration”

void    task_connect (u8 e, u8 *p, int n)
{
        // add connect callback code here
}

void    task_terminate (u8 e, u8 *p, int n)
{
        // add terminate callback code here
}
bls_app_registerEventCallback (BLT_EV_FLAG_CONNECT, &task_connect);
bls_app_registerEventCallback (BLT_EV_FLAG_TERMINATE, &task_terminate);

2) 方式二“shared event entry”

int controller_event_handler(u32 h, u8 *para, int n)
{
    if( (h&HCI_FLAG_EVENT_TLK_MODULE)!= 0 ) //module event
    {
        ······
        switch(event)
        {
            case BLT_EV_FLAG_CONNECT:
            {
                // add connect callback code here
            }
            break;
            case BLT_EV_FLAG_TERMINATE:
            {
                // add terminate callback code here
            }
            break;
            default:
            break;
        }
    }
}
blc_hci_registerControllerEventHandler(controller_event_handler);   
bls_hci_mod_setEventMask_cmd( BIT(BLT_EV_FLAG_CONNECT) | BIT(BLT_EV_FLAG_TERMINATE) );

下面对Controller所有的事件、事件触发条件、对应的回调函数的参数进行详细的说明。

1) BLT_EV_FLAG_ADV

该事件目前没有使用。

2) BLT_EV_FLAG_ADV_DURATION_TIMEOUT

事件触发条件:如果user调用API bls_ll_setAdvDuration设置了广播时间限制,BLE协议栈底层启动一个计时,在这个限时达到后,停止广播,同时触发该事件,user可以在该事件回调函数中进行修改广播事件类型、重新打开广播使能、再次设置广播时间限制等操作。

回传指针p:空指针NULL。

数据长度n:0。

注意:该event在Link Layer扩展状态中的advertising in ConnSlaveRole不触发。

3) BLT_EV_FLAG_SCAN_RSP

事件触发条件:slave处于广播状态,收到master的scan request并响应,回scan response时触发该事件。

回传指针p:空指针NULL。

数据长度n:0。

4) BLT_EV_FLAG_CONNECT

事件触发条件:Link Layer处于广播状态时收到master的连接请求包单元,响应这个请求,进入Conn state Slave role,触发该事件。

数据长度n:34。

回传指针p:p指向长度为34 byte的一片ram区域,对应如下图所示的连接请求包单元PDU。

连接请求包单元PDU

可以参考ble_format.h中rf_packet_connect_t的定义,连接请求包单元PDU从该结构体的InitA [6]开始,到hop结束。

typedef struct{
    u32 dma_len;
    u8  type   :4;
    u8  rfu1   :1;
    u8  chan_sel:1;
    u8  txAddr :1;
    u8  rxAddr :1;
    u8  rf_len;
    u8  initA[6];
    u8  advA[6];
    u8  accessCode[4];
    u8  crcinit[3];
    u8  winSize;
    u16 winOffset;
    u16 interval;
    u16 latency;
    u16 timeout;
    u8  chm[5];
    u8  hop;
}rf_packet_connect_t;

5) BLT_EV_FLAG_TERMINATE

事件触发条件:Link Layer状态机中从Conn state Slave role退出时触发。

回传指针p:p指向一个u8类型的变量terminate_reason,该变量表明当前是什么reason导致的Link Layer断开连接。

数据长度n:1。

Conn state Slave role退出的三种情况对应的reason分别如下:

A. slave和master的RF通信出现问题(RF不好或者master断电等),slave连续一段时间收不到master的包,连接超时(connection supervision timeout)时触发该事件,连接断开,回到None Conn state。terminate reason为HCI_ERR_CONN_TIMEOUT(0x08)。

B. Master发送terminate主动断开连接,slave收到terminate命令,对这个命令进行ack后,触发该事件,连接断开,回到None Conn state。 terminate reason是slave在Link Layer上收到的LL_TERMINATE_IND控制包中的Error Code,该Error Code是由master决定的。常见的Error code有HCI_ERR_REMOTE_USER_TERM_CONN(0x13)、HCI_ERR_CONN_TERM_MIC_FAILURE(0x3D)等。

C. Slave端调用了API bls_ll_terminateConnection(u8 reason),主动断开连接。terminate reason是该API的实参reason。

6) BLT_EV_FLAG_LL_REJECT_IND

事件触发条件:Master在Link Layer发送LL_ENC_REQ(encryption request),且声明了使用之前已经分配好的LTK,slave无法找到对应的LTK,发送LL_REJECT_IND(or LL_REJECT_IND_EXT),此时触发该事件。

回传指针p:指向发送的command(LL_REJECT_IND or LL_REJECT_IND_EXT)。

数据长度n:1。

更多信息请参照《Core_v5.0》(Vol 6/Part B/2.4.2 )。

7) BLT_EV_FLAG_RX_DATA_ABANDON

事件触发条件:当BLE RX fifo overflow时(参考前文“Link Layer TX fifo & RX fifo”小节),或者在一个连接间隔里收到多包数量>设定的多包数量阈值(用户需要调用API:blc_ll_init_max_md_nums且参数不为0,SDK底层才会做多包数量的检查),触发BLT_EV_FLAG_RX_DATA_ABANDON事件。

回传指针p:空指针NULL。

数据长度n:0。

8) BLT_EV_FLAG_PHY_UPDATE

事件触发条件:当slave或者master主动发起LL_PHY_REQ,更新成功或者失败后触发;或者当slave或者master被动收到LL_PHY_REQ,并且PHY更新成功后触发。

数据长度n:1

回传指针p:指向一个u8类型的变量,指示当前连接的PHY mode。

typedef enum {
    BLE_PHY_1M          = 0x01,
    BLE_PHY_2M          = 0x02,
    BLE_PHY_CODED       = 0x03,
} le_phy_type_t;

9) BLT_EV_FLAG_DATA_LENGTH_EXCHANGE

事件触发条件:Slave和Master交互Link Layer最大数据长度时触发,即一方发送LL_LENGTH_REQ,另一方回复LL_LENGTH_RSP。如果Slave主动发送LL_LENGTH_REQ,需要等收到LL_LENGTH_RSP时触发;如果Master发起LL_LENGTH_REQ,Slave回复LL_LENGTH_RSP后立刻触发。

数据长度n:12。

回传指针p:指向一片内存数据,对应如下结构体的前6个u16的变量。

typedef struct {
    u16     connEffectiveMaxRxOctets;
    u16     connEffectiveMaxTxOctets;
    u16     connMaxRxOctets;
    u16     connMaxTxOctets;
    u16     connRemoteMaxRxOctets;
    u16     connRemoteMaxTxOctets;
            ……
}ll_data_extension_t;

connEffectiveMaxRxOctets和connEffectiveMaxTxOctets是当前连接上最终允许的RX和TX最大数据长度;connMaxRxOctets和connMaxTxOctets是设备自己RX和TX最大数据长度;connRemoteMaxRxOctets和connRemoteMaxTxOctets是对方设备RX和TX最大数据长度。

connEffectiveMaxRxOctets = min(supportedMaxRxOctets, connRemoteMaxTxOctets);

connEffectiveMaxTxOctets = min(supportedMaxTxOctets, connRemoteMaxRxOctets);

10) BLT_EV_FLAG_GPIO_EARLY_WAKEUP

事件触发条件:BLE slave进入sleep(suspend或deepsleep retention)之前,计算好下一次醒来的时间点,到该时间点后醒来(这是由sleep状态下的timer计时实现的),那么会存在一个问题:如果sleep的时间过长,user的任务必须到sleep醒来后才能处理,对于一些实时性要求比较严的应用,可能会出问题。如对于键盘扫描,使用者按键的动作可能很快,为了保证按键不丢同时又要处理按键去抖动,按键扫描的时间间隔在10~20ms比较好,此时如果sleep时间较大,如400ms、1s等(sleep时间在启用latency的时候会达到这些值),按键就会丢掉。那么需要在MCU进入sleep之前判断当前睡眠的时间,如果时间过长,设置suspend/deepsleep retention可以被使用者的按键动作提前唤醒(后面PM模块详细介绍)。

当suspend/deepsleep retention在timer wake up时间点之前被GPIO提前唤醒时,触发BLT_EV_FLAG_GPIO_EARLY_WAKEUP事件。

数据长度n:1。

回传指针p:指向一个u8型变量wakeup_status,wakeup_status变量记录了当前suspend哪些唤醒源状态生效了,由drivers/B91/pm.h可看到如下唤醒状态。

enum {
     WAKEUP_STATUS_COMPARATOR       = BIT(0),
     WAKEUP_STATUS_TIMER            = BIT(1),
     WAKEUP_STATUS_CORE             = BIT(2),
     WAKEUP_STATUS_PAD              = BIT(3),
     WAKEUP_STATUS_MDEC             = BIT(4),
     STATUS_GPIO_ERR_NO_ENTER_PM    = BIT(7),
     STATUS_ENTER_SUSPEND           = BIT(30),
};

以上参数的含义请参考“低功耗管理”部分下的详细说明。

11) BLT_EV_FLAG_CHN_MAP_REQ

事件触发条件:slave在Conn state,master需要更新当前连接的频点列表,发送一个LL_CHANNEL_MAP_REQ,slave收到这个请求后触发该事件,注意是slave收到该请求的时候,还并没有处理。

数据长度n:5。

回传指针p:p指向下面频点列表数组的首地址。

unsigned char类型的blttcon.conn_chn_map[5],注意回调函数执行时p指向的blttcon.conn_chn_map是还没有更新的老channel map。

conn_chn_map用5个byte表示当前频点列表,采用映射的方法,每个bit代表一个channel:

conn_chn_map[0]的bit0-bit7分别表示channel0-channel7。

conn_chn_map[1]的bit0-bit7分别表示channel8-channel15。

conn_chn_map[2]的bit0-bit7分别表示channel16-channel23。

conn_chn_map[3]的bit0-bit7分别表示channel24-channel31。

conn_chn_map[4]的bit0-bit4分别表示channel32-channel36。

12) BLT_EV_FLAG_CHN_MAP_UPDATE

事件触发条件:slave在连接状态,收到LL_CHANNEL_MAP_REQ命令后到了更新的时间点,将channel map进行更新,触发该事件。

回传指针p:p指向conn_chn_map[5]的首地址,此时的conn_chn_map是更新以后新的map。

数据长度n:5。

13) BLT_EV_FLAG_CONN_PARA_REQ

事件触发条件:Slave设备处于连接态(Conn state Slave role),master设备需要更新当前连接的参数,发送LL_CONNECTION_UPDATE_REQ命令,Slave设备收到这个请求后触发该事件,此时还没有处理这个请求。

数据长度n:11。

回传指针p:p指向LL_CONNECTION_UPDATE_REQ的PDU,如下图所示的11个byte。

LL_CONNECTION_UPDATE REQ Format in BLE Stack

14) BLT_EV_FLAG_CONN_PARA_UPDATE

事件触发条件:slave在连接状态,收到LL_CONNECTION_UPDATE_REQ后到了该更新的时间点,对连接参数进行更新,触发该事件。

数据长度n:6。

回传指针p:p指向连接参数更新后的新参数,如下

p[0] | p[1]<<8: new connection interval in unit of 1.25ms.

p[2] | p[3]<<8: new connection latency.

p[4] | p[5]<<8: new connection timeout in unit of 10ms.

15) BLT_EV_FLAG_SUSPEND_ENTER

事件触发条件:slave执行blt_sdk_main_loop函数时,进入suspend之前触发该事件。

回传指针p:空指针NULL。

数据长度n: 0。

16) BLT_EV_FLAG_SUSPEND_EXIT

事件触发条件:slave执行blt_sdk_main_loop函数时, suspend唤醒之后触发该事件。

回传指针p:空指针NULL。

数据长度n: 0。

注意:

实际SDK底层执行cpu_sleep_wakeup唤醒后执行该回调,且不管是被gpio唤醒还是被timer唤醒,都会无条件触发该事件。如果同时发生了BLT_EV_FLAG_GPIO_EARLY_WAKEUP事件,这两个事件的先后执行顺序请参考“低功耗管理—低功耗管理工作机制”部分伪代码的描述。

Data Length Extension

BLE Spec从core_4.2开始增加了data length extension(DLE)。

该BLE SDK Link Layer上支持data length extension,且rf_len长度支持到BLE spec上最大长度251 bytes。

详情请参照《Core_v5.0》(Vol 6/Part B/2.4.2.21 “LL_LENGTH_REQ and LL_LENGTH_RSP”)。

User如果需要使用data length extension功能,按如下步骤设置。

(1) 设置合适的TX & RX fifo size

收发长包都需要更大的TX & RX fifo size,考虑到这些fifo会占据大量的Sram空间,在设置fifo size时应选取最合适的值,避免Sram的浪费。

发长包需要加大TX fifo size。TX fifo size至少比TX rf_len大12,且必须按16字节对齐。用户可调用如下API进行设置:

blc_ll_initAclConnTxFifo(app_acl_txfifo, ACL_TX_FIFO_SIZE, ACL_TX_FIFO_NUM);
用户只需要关注ACL_TX_FIFO_SIZE和ACL_TX_FIFO_NUM对应的宏即可,也可参考“Link Layer TX fifo介绍”部分的说明来了解各个参数的含义。

收长包需要加大RX fifo size。RX fifo size至少比RX rf_len大24,且必须按16字节对齐。用户可调用如下API进行设置:

blc_ll_initAclConnRxFifo(app_acl_rxfifo, ACL_RX_FIFO_SIZE, ACL_RX_FIFO_NUM);
用户同样只需要关注ACL_RX_FIFO_SIZE和ACL_RX_FIFO_NUM对应的宏即可,也可参考“Link Layer RX fifo介绍”部分的说明来了解各个参数的含义。

(2) 设置合适的MTU Size

MTU即最大传输单元,用于设置BLE中的L2CAP层单个数据包中的payload大小,att.h中提供了接口函数ble_sts_t blc_att_setRxMtuSize(u16 mtu_size);在BLE协议栈初始化时,用户可直接使用该函数进行传参设定MTU大小,设置成功返回值为BLE_SUCCESS,具体使用可参考B91m_feature的DLE Demo,可选择替换不同的宏进行设置MTU大小。

#define DLE_LENGTH_SELECT           DLE_LENGTH_200_BYTES

#if (DLE_LENGTH_SELECT == DLE_LENGTH_27_BYTES)
    #define ACL_CONN_MAX_RX_OCTETS          27  //user set value
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_52_BYTES)
    #define ACL_CONN_MAX_RX_OCTETS          52  //user set value
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_56_BYTES)
    #define ACL_CONN_MAX_RX_OCTETS          56  //user set value
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_100_BYTES)
    #define ACL_CONN_MAX_RX_OCTETS          100 //user set value
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_200_BYTES)
    #define ACL_CONN_MAX_RX_OCTETS          200 //user set value
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_251_BYTES)
    #define ACL_CONN_MAX_RX_OCTETS          251 //user set value
#else
    #define ACL_CONN_MAX_RX_OCTETS          27  //user set value
#endif

blc_att_setRxMtuSize(MTU_SIZE_SETTING);

(3) data length exchange

在收发长包前,一定要确保在BLE connection上data length exchange这个流程已经完成。data length exchange流程是Link Layer上LL_LENGTH_REQ和LL_LENGTH_RSP两个包的交互过程,slave和master任何一方都可以主动发起LL_LENGTH_REQ,另一方回应LL_LENGTH_RSP。通过这两个包的交互后,master和slave都能知道彼此TX和RX最大包长,然后取两个最大包长中的较小值,即可确定当前connection收发包最大包长。

不管是哪一端发起的LL_LENGTH_REQ,data length exchange流程结束时,SDK都会产生BLT_EV_FLAG_DATA_LENGTH_EXCHANGE事件回调(前提是注册了该事件的回调),user可参考“Telink defined event”部分的说明来了解该事件回调函数各个参数的含义。

在这个BLT_EV_FLAG_DATA_LENGTH_EXCHANGE事件回调函数里,可以获得最终的最大TX包长和RX包长。

在实际应用中,当B91m作为BLE slave设备时,master端可能会主动发起LL_LENGTH_REQ,也可能不会主动发起。如果master端没有主动发起LL_LENGTH_REQ,就需要由slave端主动发起。SDK提供主动发起LL_LENGTH_REQ的API如下:

ble_sts_t   blt_ll_exchangeDataLength (u8 opcode, u16 maxTxOct);

API中opcode填"LL_LENGTH_REQ”,maxTxOct填当前设备支持的最大TX包长,比如TX最大包长为200 bytes时,设置如下:

blt_ll_exchangeDataLength(LL_LENGTH_REQ , 200);

Telink Ble SDK中,当slave端在主函数调用blc_att_setRxMtuSize设置Rx MTU大小时,若size大于23则会主动进行上报MTU和更新DLE,其中会触发两个Event,对应的code是task_dle_exchange和app_host_event_callback,user可自行添加标志位来对触发事件进行判断,若发现没触发代表设置参数MTU和DLE没更新,则进行手动设置,其中函数包括两个:

ble_sts_t  blc_att_requestMtuSizeExchange (u16 connHandle, u16 mtu_size);
ble_sts_t  blt_ll_exchangeDataLength (u8 opcode, u16 maxTxOct);

(4) MTU size exchange

除了上面data length exchange流程,MTU size exchange的流程必须也要执行,确保大的MTU size生效,防止对方设备在BLE L2CAP层无法处理长包。MTU size的值需要大于等于TX & RX最大包长。MTU size exchange的实现,请参考本文档" ATT & GATT”部分的详细说明,也可以参考B91m_feature里面demo的写法。

(5) 收发长包的操作

请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”即可,在回调函数里,引用形参指针指向的数据。

注意:

Telink Ble协议栈会主动更新MTU和DLE,应用层可以不需要手动调用接口函数进行设置。

Controller API

(1) Controller API说明

在图3-1所示的标准BLE协议栈架构中,应用层是无法与Controller的Link Layer直接数据交互的,必须通过Host把数据往下发,最终由Host通过HCI把控制命令传送给Link Layer。Host通过HCI接口下发的所有Controller控制命令都在BLE spec《Core_v5.0》中严格定义了,详情请参照《Core_v5.0》(Vol 2/Part E/ Host Controller Interface Functional Specification)。

Telink BLE SDK按照标准的BLE架构设计,可以作为一个单独的Controller与另外一个运行Host协议的系统级联工作,所以所有的操作设置Link Layer的API都是严格按照Spec上 Host command的数据格式来定义实现的。

虽然Telink BLE SDK最终采用了Figure26的架构,应用层跨越Host直接操作设置Link Layer,但使用的API还是标准的HCI部分的API。下面的API具体介绍中会给出Spec上对应的Host command,user可以参考Spec上的具体说明加以理解。

BLE spec上所有的HCI command,Controller的处理都会有对应的HCI command complete event或HCI command status event作为对Host层的应答,但在Telink BLE SDK中,是分情况处理的。

Controller API的声明在stack/ble/controller/ll和stack/ble/hci目录下的头文件中,其中ll目录下根据Link Layer状态机功能的分类分为ll.h、ll_adv.h、ll_scan.h、ll_ext_adv.h、ll_pm.h,user可以根据Link Layer的功能去寻找,比如跟advertising相关功能的API就应该都在ll_adv.h中声明。

(2) API返回类型ble_sts_t

在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也适用。

(3) MAC address初始化

本文档中的BLE MAC address最基本的类型包括public address和random static address。

该BLE SDK中,调用如下接口获得public address和random static address。

void blc_initMacAddress(int flash_addr, u8 *mac_public, u8 *mac_random_static);

flash_addr填flash上存储MAC address的地址即可,参考文档前面的介绍,B91m 1M flash上对应的这个地址为0xFF000。如果不需要random static address,上面的mac_random_static填“NULL”即可。

BLE public MAC address成功获取后,调用Link Layer初始化的API,将MAC address传入BLE协议栈:

blc_ll_initStandby_module(mac_public);

(4) Link Layer 状态机初始化

结合前面对Link Layer状态机的详细介绍,以下几个API用于配置搭建BLE状态机时各个模块的初始化。

blc_ll_initBasicMCU();                    
blc_ll_initStandby_module(mac_public);      
blc_ll_initLegacyAdvertising_module();  
blc_ll_initConnection_module();         
blc_ll_initSlaveRole_module();          

(5) bls_ll_setAdvData

详情请参照《Core_v5.0》(Vol 2/Part E/ 7.8.7 “LE Set Advertising Data Command”)。

BLE协议栈广播包格式

BLE协议栈里,广播包的格式如上图所示,前两个byte是header,后面是Payload(PDU),最多31 byte。

下面的API用于设置PDU部分的数据:

ble_sts_t   bls_ll_setAdvData(u8 *data, u8 len);

data指针指向PDU的首地址,len为数据长度。

返回类型ble_sts_t可能返回的结果如下表所示。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_INVALID_HCI_ CMD_PARAMS 0x12 Len超过最大长度31

user可以在初始化的时候调用该API设置广播数据,也可以于程序运行时在main_loop里随时调用该API来修改广播数据。

该BLE SDK中feature_backup工程中定义的Adv PDU如下,其中各个字段的含义请参考文档BLE Spec《CSS v6》(Core Specification Supplement v6.0)中Data Type Specification的具体说明。

const u8    tbl_advData[] = {
     0x08, DT_COMPLETE_LOCAL_NAME, 'f', 'e', 'a', 't', 'u', 'r', 'e',
     0x02, DT_FLAGS, 0x05,
     0x03, DT_APPEARANCE, 0x80, 0x01,
     0x05, DT_INCOMPLETE_LIST_16BIT_SERVICE_UUID, 0x12, 0x18, 0x0F, 0x18,
};

上面广播数据里,设置广播设备名为"feature"。

(6) bls_ll_setScanRspData

详情请参照《Core_v5.0》(Vol 2/Part E/ 7.8.8 “LE Set Scan response Data Command”)。

类似于上面广播包PDU的设置,scan response PDU的设置使用API:

ble_sts_t   bls_ll_setScanRspData(u8 *data, u8 len);

data指针指向PDU的首地址,len为数据长度。返回类型ble_sts_t可能返回的结果如下表所示。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_INVALID_HCI_ CMD_PARAMS 0x12 Len超过最大长度31

user可以在初始化的时候调用该API设置Scan response data,也可以于程序运行时在main_loop里随时调用该API来修改Scan response data。各个字段的含义请参考文档BLE Spec《CSS v6》(Core Specification Supplement v6.0)中Data Type Specification的具体说明。

const u8    tbl_scanRsp [] = { 0x08, DT_COMPLETE_LOCAL_NAME, 'E', 'a', 'g', 'l', 'e', 'r', 'c',};

上面在advertising data和scan response data中都设置了设备名称且不一样,那么在手机或IOS系统上扫描蓝牙设备时,看到的设备名称可能会不一样:

a) 一些设备只看广播包,那么显示的设备名称为"feature";

b) 一些设备看到广播后,发送scan request,并读取回包scan response,那么显示的设备名称可能就会是"Eaglerc"。

user也可以在这两个包中将设备名称写为一样,被扫描时就不会显示两个不同的名字了。

实际上设备被master连接后,master在读设备的Attribute Table时,会获取设备的gap device name,连上设备后也可能会根据那里的设置来显示设备名称。

(7) bls_ll_continue_adv_after_scan_req

void bls_ll_continue_adv_after_scan_req(u8 enable);

用于广播时添加和删除扫描请求,用于设置在收到扫描请求时,连续发送广播包。

(8) bls_ll_setAdvParam

详情请参照《Core_v5.0》(Vol 2/Part E/ 7.8.5 “LE Set Advertising Parameters Command”)。

BLE协议栈里Advertising Event

BLE协议栈里Advertising Event(简称Adv Event)如上图所示,指的是在每一个T_advEvent,slave进行一轮广播,在三个广播channel(channel 37、channel 38、channel 39)上各发一个包。

下面的API对Adv Event相关的参数进行设置。

ble_sts_t bls_ll_setAdvParam( u16 intervalMin, u16 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.625ms为基本单位,范围在20ms ~ 10.24s之间,并且intervalMin小于等于intervalMax。

BLE spec要求adv interval不要设一个固定的值,需要有一些随机的变化。Telink BLE SDK通过intervalMin和intervalMax设置为不同的值来实现,最终的adv interval是在intervalMin ~ intervalMax之间随机变化。若设置的intervalMin和intervalMax相等,adv interval会等于固定的intervalMin,不会变化。

根据不同广播包的类型,intervalMin和intervalMax的值有一些限定,请参照(Vol 6/Part B/ 4.4.2.2 “Advertising Events”)。

2) advType

参考BLE Spec,四种基本的广播事件类型如下:

BLE协议栈四种广播事件

上图中Allowable response PDUs for advertising event部分用YES和NO说明了各种类型广播事件是否对其他设备的Scan Request和Connect Request进行响应,如:第一个Connectable Undirected Event对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,这样一共能够得到五种广播事件类型,如下定义:

/* 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个可选的值如下:

/* Own Address Type */
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的值可设置如下:

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; //adv_filterPolicy_type_t

返回值ble_sts_t可能出现的值和原因如下表所示:

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_INVALID_HCI_ CMD_PARAMS 0x12 intervalMin或 intervalMax的值不符合BLE spec的规定

按照BLE spec HCI部分Host command的设计,Set Advertising parameters同时设置了上面的8个参数。同时设置的思路也是合理的,因为一些不同的参数之间是有耦合关系的,比如advType和advInterval,在不同的advType下,对intervalMin和intervalMax的范围限定会不一样,所以会有不同的范围检查,如果将set advType和set advInterval拆成两个不同的API,彼此间的范围检查就无法控制。

(9) bls_ll_setAdvEnable

详情请参照《Core_v5.0》(Vol 2/Part E/ 7.8.9 “LE Set Advertising Enable Command”)。

ble_sts_t   bls_ll_setAdvEnable(int adv_enable);

adv_enable为1时,Enable Advertising;adv_enable为0时,Disable Advertising。

a) 在Idle state时,Enable Advertising,Link Layer进入Advertising state。

b) 在Advertising state时,Disable Advertising,Link layer进入Idle state。

c) 在其他state,Enable Advertising或Disable Advertising都不影响Link Layer的state。

注意:

需要注意的是,在任何时候调用该函数,ble_sts_t无条件返回BLE_SUCCESS,也就是adv相关参数内部会打开或关闭,但只有处于idle或adv state才会生效。

(10) bls_ll_setAdvDuration

ble_sts_t   bls_ll_setAdvDuration (u32 duration_us, u8 duration_en);

使用bls_ll_setAdvParam对广播所有参数设置成功后,使用bls_ll_setAdvEnable(1)开始广播。若希望对设置好的广播事件进行时间限定,让它持续发送一段时间后就自动关闭,可以调用上面的API。

duration_en设为1表示开启计时功能,设为0表示关闭计时功能。只有在计时功能开启的情况下,duration_us(单位:us)的设置才有意义。

程序从设置的时间点开始计时,一旦超出预设的时间时,停止广播,广播使能(AdvEnable)失效,在None Conn state下,会切换到Idle State。此时会触发Link Layer事件BLT_EV_FLAG_ADV_DURATION_TIMEOUT。

BLE Spec中规定ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY广播类型, Duration Time固定为1.28s,到1.28s之后将停止广播。所以对于这种广播类型,调用bls_ll_setAdvDuration设置将无效。返回值ble_sts_t见下表。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_INVALID_HCI_ CMD_PARAMS 0x12 ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY广播类型不能被设置Duration Time

如果user需要在Adv Duration Time到达广播停止后,重新设置广播参数(AdvType、AdvInterval、AdvChannelMap等),需要在BLT_EV_FLAG_ADV_DURATION_TIMEOUT事件的回调函数里设置,并且需要再次调用bls_ll_setAdvEnable(1)开始新的广播。

触发BLT_EV_FLAG_ADV_DURATION_TIMEOUT需要注意一种特殊的情况:

假设设定了duration_us为2000000,即2s。

如果Slave一直在广播,那么广播时间到达2s时会触发timeout,执行BLT_EV_FLAG_ADV_DURATION_TIMEOUT回调。

如果广播时间不到2s的情况下(假设在0.5s的时候),Slave和Master连接上了,这个timeout的计时在底层并没有被清除掉,而是被缓存起来。在进入连接状态1.5s的时候,也就是到达实际设定的timeout时间点正好2s时,由于此时已经处于连接状态,不会去检查广播事件是否超时,也就不会触发BLT_EV_FLAG_ADV_DURATION_TIMEOUT回调。

(11) blc_ll_setAdvCustomizedChannel

下面API用于定制特殊的advertising channel/scanning channel,只对一些非常特殊的应用有意义,如BLE mesh,其他常规BLE应用不要使用该API。

void  blc_ll_setAdvCustomizedChannel (u8 chn0, u8 chn1, u8 chn2);

chn0/chn1/chn2填需要定制的频点,默认的标准频点是37/38/39,比如设置3个advertising channel分别为 2420MHz、2430MHz、2450MHz,可如下调用:

blc_ll_setAdvCustomizedChannel (8, 12, 22);

(12) rf_set_power_level_index

该 BLE SDK提供了BLE RF packet能量设定的API:

void rf_set_power_level_index(rf_power_level_index_e idx);

idx值的设置参考drivers/B91/lib/include/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都被重新设置一遍,如下code所示。用户需注意在较老版本的程序中task_suspend_exit可能被命名为user_set_rf_power。

void task_suspend_exit (u8 e, u8 *p, int n)
{
    rf_set_power_level_index (MY_RF_POWER_INDEX);
}
bls_app_registerEventCallback (BLT_EV_FLAG_SUSPEND_EXIT, &task_suspend_exit);

(13) bls_ll_terminateConnection

ble_sts_t   bls_ll_terminateConnection (u8 reason);

该API用于BLE Slave设备,只对Connection state Slave role适用。

应用层可以调用此API在Link Layer上发送一个Terminate给master,主动断开连接,reason为需要指定的断开原因,reason的设置详请参照《Core_v5.0》(Vol 2/Part D/2 “Error Code Descriptions”)。

若不是系统运行异常导致的terminate,应用层一般指定reason为:

HCI_ERR_REMOTE_USER_TERM_CONN  = 0x13
bls_ll_terminateConnection(HCI_ERR_REMOTE_USER_TERM_CONN);

Telink BLE SDK底层stack中,只有一个地方会调用该API主动terminate:解密对方设备的数据包,如发现认证数据MIC错误,调用bls_ll_terminateConnection(HCI_ERR_CONN_TERM_MIC_FAILURE),告知对方解密错误,断开连接。

Slave调用该API主动发起断开连接后,一定会触发BLT_EV_FLAG_TERMINATE事件,在该事件的回调函数里可以看到对应的terminate reason和这个手动设置的reason是一样的。

在Connection state Slave role时,一般情况下直接调用该API可以成功发送terminate并断连,但也存在一些特殊情况会导致该API调用失败,根据返回值ble_sts_t可以了解对应的错误原因。建议应用层调用该API时,检查一下返回值是否为BLE_SUCCESS。返回值列表如下。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_CONN_FAILED_TO_ ESTABLISH 0x3E Link Layer处于非Connection state Slave role
HCI_ERR_CONTROLLER _BUSY 0x3A Controller busy(有大量数据正在发送)暂时无法接受该命令。

(14) Get Connection Parameters

获取当前连接参数的Connection Interval、Connection Latency、Connection Timeout的API如下(只对Slave role适用):

u16         bls_ll_getConnectionInterval(void); 
u16         bls_ll_getConnectionLatency(void);  
u16         bls_ll_getConnectionTimeout(void);

a) 若返回值为0,表示当前Link Layer处于None Conn state,也就没有连接参数。

b) 若返回为非0值表示参数值:

  • bls_ll_getConnectionInterval返回的是实际conn interval除以1.25ms单位值,比如当前conn interval为10ms,则返回值为8。

  • bls_ll_getConnectionLatency返回实际Latency值。

  • bls_ll_getConnectionTimeout返回的是实际conn timeout除以10ms的单位值,比如当前conn timeout为1000ms,则返回值为100。

(15) blc_ll_getCurrentState

下面API用于获取当前Link Layer所处的状态。

u8  blc_ll_getCurrentState(void);

user在应用层判断当前状态,如:

if(  blc_ll_getCurrentState() == BLS_LINK_STATE_ADV)
if(  blc_ll_getCurrentState() == BLS_LINK_STATE_CONN )

(16) blc_ll_getLatestAvgRSSI

当Link Layer进入Slave role或Master role后,通过下面API能得到跟自己连接的peer device过去一段时间的平均RSSI。

u8      blc_ll_getLatestAvgRSSI(void);

返回值u8的rssi_raw要做一定的转换,rssi_real = rssi_raw - 110,假设返回值为50,则rssi = -60 db。

(17) 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中,目前SDK whitelist最多存储4个设备:

#define     MAX_WHITE_LIST_SIZE         4

相关接口:

ble_sts_t       blc_ll_clearWhiteList(void);

Clear whitelist,在较老版本的程序中函数可能命名为ll_whiteList_reset。返回值为BLE_SUCCESS。

ble_sts_t       blc_ll_addDeviceToWhiteList(u8 adr_type, u8 *addr);

添加一个设备到whitelist,在较老版本的程序中函数可能命名为ll_whiteList_add。函数返回值列表:

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_MEM_CAP_EXCEEDED 0x07 whitelist已满,添加失败
HCI_ERR_INVALID_HCI_CMD_PARAMS 0x12 HCI命令参数无效

ble_sts_t       blc_ll_removeDeviceFromWhiteList(u8 adr_type, u8 *addr);
从whitelist删除之前添加的设备,在较老版本的程序中函数可能命名为ll_whiteList_delete。返回值为BLE_SUCCESS。

RPA(resolvable private address)设备,需要使用Resolvinglist。为了节省ram使用,目前SDK Resolvinglist最多存储2个设备:

#define     MAX_RESOLVING_LIST_SIZE             2

相关API如下:

ble_sts_t   blc_ll_clearResolvingList(void);
Clear Resolvinglist,在较老版本的程序中函数可能命名为ll_resolvingList_reset。返回值BLE_SUCCESS。

ble_sts_t   blc_ll_setAddressResolutionEnable (addr_res_en_t resolution_en);
设备地址解析使用,在较老版本的程序中函数可能命名为ll_resolvingList_setAddrResolutionEnable。如果要使用Resolvinglist解析地址,一定要打开使能。不需要解析的时候,可以关闭。

ble_sts_t   blc_ll_addDeviceToResolvingList(ida_type_t peerIdAddrType, u8 *peerIdAddr, u8 *peer_irk, u8 *local_irk);
添加使用RPA地址的设备,在较老版本的程序中函数可能命名为ll_resolvingList_add。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);
删除之前添加的设备。在较老版本的程序中函数可能命名为ll_resolvingList_delete。

Whitelist/Resolvinglist实现地址过滤的使用,请参考SDK feature test demo中的TEST_WHITELIST.

Coded PHY/2M PHY

(1) Coded PHY/2M PHY介绍

Coded PHY 和2M PHY是《Core_5.0》新增加的Feature,很大程度上扩展了BLE的应用场景,Coded PHY包含S2(500kbps)和S8(125kbps)以适应更远距离的应用,2M PHY(2Mbps)大大提高了BLE带宽。2M PHY/Coded PHY 可以使用在广播通道,也可以用在连接状态下的数据通道。连接状态下的使用方法在本小节中中介绍,广播通道下使用方法在“Extended Advertising”章节中涉及到。

(2) Coded PHY/2M PHY Demo介绍

Telink提供的BLE SDK中,为节省Sram,Coded PHY/2M PHY 默认是关闭的,用户如果选择使用此Feature,可以手动打开,打开方法可以参考BLE SDK提供的Demo。

  • Slave端可参考Demo “B91m_feature_test”

在vendor/B91m_feature/feature_config.h中定义宏

#define FEATURE_TEST_MODE   TEST_2M_CODED_PHY_CONNECTION

(3) Coded PHY/2M PHY API介绍

1) API: blc_ll_init2MPhyCodedPhy_feature

void blc_ll_init2MPhyCodedPhy_feature(void)

用于使能2M PHY/Coded PHY。

2) 在”Telink defined event”中增加了一个“BLT_EV_FLAG_PHY_UPDATE”Event,具体细节请参考前面“Controller Event”章节介绍。

3) 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);

BLE Spec标准接口,详细请参考《Core_v5.0》(Vol 2/PartE/7.8.49 “LE Set PHY Command”)。

connHandle:slave应用填BLS_CONN_HANDLE; master应该填BLM_CONN_HANDLE。

其他参数请参考Spec定义,结合SDK上枚举类型定义和demo用法去理解。

Channel Selection Algorithm #2

Channel Selection Algorithm #2 是《Core_5.0》中新添加的Feature,拥有更强的抗干扰能力,有关算法的具体说明请参考《Core_5.0》(Vol 6/Part B/4.5.8.3 “Channel Selection Algorithm #2”)

a) 若使用跳频算法#2,需要通过调用下面的API使能。

 void blc_ll_initChannelSelectionAlgorithm_2_feature(void);

b) 如果使用《Core_4.2》API定义的广播,用户可以选择使用或者不使用跳频算法#2,SDK中默认是不使用的。

c) 如果使用《Core_5.0》API定义的Extended Advertising,并且需要通过Extend Adv发起连接,就必须通过上面的API使能跳频算法#2,这是Spec中的规定,必须要支持(因为通过Extended Adv发起的连接,连接成功后默认使用跳频算法#2),如果只是使用Advertising功能,为节省Sram,建议不使能跳频算法#2。

Extended Advertising

(1) Extended Advertising介绍

Extended Advertising 是《Core_5.0》新添加的Feature。

由于《Core_5.0》对Advertising部分的扩展,SDK中新添加了几个API可以发送《Core_4.2》中定义的广播功能和《Core_5.0》新添加的广播功能,这部分API我们会在以后的章节中描述为《Core_5.0》API(下面小节后引用这个名称),而《Core_4.2》API指“Controller API”章节中介绍的bls_ll_setAdvData()、bls_ll_setScanRspData()、bls_ll_setAdvParam(),这3个API只能实现《Core_4.2》定义的广播功能,不能够实现《Core_5.0》的广播功能。

Extended Advertising主要特点如下:

1) 增加了Advertising PDU的最大数据长度,从《core_4.2》的Advertising PDU长度6~37 bytes到《core_v5.0》Extended Advertising PDU的最大长度0~255 bytes(单个包PDU的长度),如果Advertising Data数据长度大于Adv PDU,会自动将分包为N个Advertising PDU分发。

2) 可根据应用场景灵活选择不同的PHYs(1Mbps,2Mbps,125kbps,500kbps)。

Extended Advertising Demo搭建

Extended Advertising Demo “B91m_feature”使用方法:

Demo1:用于说明《Core_5.0》支持的所有的基础广播功能用法。

a) 在 vendor/B91m_feature/feature_config.h定义宏

#define FEATURE_TEST_MODE   TEST_EXTENDED_ADVERTISING

b) 根据想要发送包的的类型使能对应的宏, Demo可以遍历《Core_5.0》所支持的所有广播包类型,所支持类型如下所示。

/* Advertising Event Properties type*/
typedef enum{
  ADV_EVT_PROP_LEGACY_CONNECTABLE_SCANNABLE_UNDIRECTED              = 0x0013,
  ADV_EVT_PROP_LEGACY_CONNECTABLE_DIRECTED_LOW_DUTY                 = 0x0015,
  ADV_EVT_PROP_LEGACY_CONNECTABLE_DIRECTED_HIGH_DUTY                = 0x001D,
  ADV_EVT_PROP_LEGACY_SCANNABLE_UNDIRECTED                          = 0x0012,
  ADV_EVT_PROP_LEGACY_NON_CONNECTABLE_NON_SCANNABLE_UNDIRECTED      = 0x0010,
  ADV_EVT_PROP_EXTENDED_NON_CONNECTABLE_NON_SCANNABLE_UNDIRECTED    = 0x0000,
  ADV_EVT_PROP_EXTENDED_CONNECTABLE_UNDIRECTED                      = 0x0001,
  ADV_EVT_PROP_EXTENDED_SCANNABLE_UNDIRECTED                        = 0x0002,
  ADV_EVT_PROP_EXTENDED_NON_CONNECTABLE_NON_SCANNABLE_DIRECTED      = 0x0004,
  ADV_EVT_PROP_EXTENDED_CONNECTABLE_DIRECTED                        = 0x0005,
  ADV_EVT_PROP_EXTENDED_SCANNABLE_DIRECTED                          = 0x0006,
  ADV_EVT_PROP_EXTENDED_MASK_ANONYMOUS_ADV                          = 0x0020,
  ADV_EVT_PROP_EXTENDED_MASK_TX_POWER_INCLUDE                       = 0x0040,
}advEvtProp_type_t;

Demo2:在Demo1广播功能的基础上,添加了Coded PHY/2M PHY的选择。

a) 在 vendor/B91m_feature/feature_config.h定义宏

#define FEATURE_TEST_MODE   TEST_2M_CODED_PHY_EXT_ADV

b) 根据需要的包类型和PHY mode选择相应的宏使能相关功能。

(2) Extended Advertising Related API

Extended Advertising部分采用模块化的设计,另外考虑到adv data length/scan response data长度的不确定性,需要支持最长1000多bytes的数据长度,而我们又不希望底层写死的code导致Sram的浪费(因为大部分客户可能使用的数据长度很短),所以将底层需要用到的Sram全部交给用户层去定义。

当前SDK只支持1个Advertising set,不支持Multiple Advertising sets,但是为了方便以后扩展为multiple adv sets,code中的API都是兼容Multiple Advertising sets而设计的,用户可暂时忽略multiple adv sets的参数设置。

根据以上的设计思想,设计了如下的API。

1) 初始化时需要调用如下API来分配Extended Advertising所使用的Sram。

blc_ll_initExtendedAdvertising_module(app_adv_set_param, app_primary_adv_pkt, APP_ADV_SETS_NUMBER);
blc_ll_initExtSecondaryAdvPacketBuffer(app_secondary_adv_pkt, MAX_LENGTH_SECOND_ADV_PKT);
blc_ll_initExtAdvDataBuffer(app_advData, APP_MAX_LENGTH_ADV_DATA);
blc_ll_initExtScanRspDataBuffer(app_scanRspData, APP_MAX_LENGTH_SCAN_RESPONSE_DATA);

根据上面API调用其内存分配如下所示:

Extended Advertising 初始化内存分配

  • APP_MAX_LENGTH_ADV_DATA:一个Advertising Set的数据长度,用户需要根据实际应用,调节宏定义大小,以节省deepRetention空间。

  • APP_MAX_LENGTH_SCAN_RESPONSE_DATA: 一个Advertising Set的scanResponse数据长度,用户需要根据实际应用,调节宏定义大小,以节省deepRetention空间。

  • app_primary_adv_pkt:一个Primary Advertising PDU数据长度的大小,固定分配长度为44 bytes,用户层不能随意修改。

  • app_secondary_adv_pkt:一个Secondary Advertising PDU数据长度的大小,固定分配长度为264 bytes,用户层不能随意修改。

在SDK提供的demo“B91m_feature”中(vendor/B91m_feature/feature_extend_adv/app.c)用户可以根据自己的需求来定义以下的宏来分配Sram,以达到节省Sram的目的。

#define APP_ADV_SETS_NUMBER                     1           
#define APP_MAX_LENGTH_ADV_DATA                1024     
#define APP_MAX_LENGTH_SCAN_RESPONSE_DATA       31  

2) API: blc_ll_setExtAdvParam

ble_sts_t   blc_ll_setExtAdvParam(……);

BLE Spec标准接口,用于设置广播参数,详细请参考《Core_5.0》(Vol 2/Part E/7.8.53 “LE Set Extended Advertising Parameters Command”),并结合SDK上枚举类型定义和demo用法去理解。

注意:

参数中adv_tx_pow暂不支持选择发送power值,需要另外调用API void rf_set_power_level_index (rf_power_level_index_e idx)来配置发送power。

3) API: blc_ll_setExtScanRspData

ble_sts_t   blc_ll_setExtScanRspData(u8 advHandle, data_oper_t operation, data_fragment_t fragment_prefer, u8 scanRsp_dataLen, u8 *scanRspData);

BLE Spec标准接口,用于设置Scan Response Data,详细可参考《Core_5.0》(Vol 2/Part E/7.8.53 “LE Set Extended Scan Response Command”),并结合SDK上枚举类型定义和demo用法去理解。

4) API: blc_ll_setExtAdvEnable_n

ble_sts_t   blc_ll_setExtAdvEnable_n(u32 ext_adv_en, u8 sets_num, u8 *pData);

BLE Spec标准接口,用于打开/关闭Extended Advertising,详细可参考《Core_5.0》(Vol 2/Part E/7.8.56 “LE Set Extended Advertising Enable Command”),并结合SDK上枚举类型定义和demo用法去理解。

但目前SDK只支持1个Adv Set,所以此API暂时不支持,只是为以后multiple Adv sets预留,不过Telink SDK根据此API功能写了一个简化的API,用来操作打开/关闭1个Adv Set,执行效率更高。简化的API如下所示,输入参数和返回值和标准的API一样,但只用来设置1个Adv Set。

ble_sts_t   blc_ll_setExtAdvEnable_1(u32 ext_adv_en, u8 sets_num, u8 advHandle,      u16 duration,    u8 max_extAdvEvt);

5) API: blc_ll_setDefaultExtAdvCodingIndication

ble_sts_t   blc_ll_setDefaultExtAdvCodingIndication(u8 advHandle, le_ci_prefer_t prefer_CI);

非BLE Spec标准接口,用BLE标准API blc_ll_setExtAdvParam()设置广播参数时,如果设置为Coded PHY(包含S2和S8),但并没有指出具体的哪种Encode mode。SDK默认为S2,为方便用户选择,定义了此API来选择preference Encode mode S2/S8。

user可通过prefer_CI传参来进行S2/S8 mode选择,具体枚举如下:

typedef enum {
    CODED_PHY_PREFER_NONE   = 0,
    CODED_PHY_PREFER_S2     = 1,
    CODED_PHY_PREFER_S8     = 2,
} le_ci_prefer_t;   //LE coding indication prefer

6) API: blc_ll_setAuxAdvChnIdxByCustomers

void   blc_ll_setAuxAdvChnIdxByCustomers(u8 aux_chn);

非BLE Spec标准接口,用户可以通过此函数设置Auxiliary Advertising channel的通道值,常用于debug,如果用户没有调用此函数来定义,Auxiliary Advertising channel值会随机生成(随机数范围0~31)。

7) API: blc_ll_setMaxAdvDelay_for_AdvEvent

void   blc_ll_setMaxAdvDelay_for_AdvEvent(u8 max_delay_ms);

非BLE Spec标准接口,用于设置在AdvInterval基础上的AdvDelay时间,输入参数范围0,1,2,4,8,单位为ms。

advDelay(unit: us) = Random()  %  (max_delay_ms * 1000);
T_advEvent = advInterval + advDelay

如果max_delay_ms = 0 ,T_advEvent的时间是精确的advInterval时间;

如果max_delay_ms = 8, T_advEvent的时间在advInterval基础上有0 ~ 8ms的随机偏移量。

BLE Host

BLE Host介绍

BLE host包括L2CAP、ATT、SMP、GATT、GAP等层,用户层的应用都是基于host层实现的。

L2CAP

逻辑链路控制与适配协议通常简称为L2CAP(Logical Link Control and Adaptation Protocol),它向上连接应用层,向下连接控制器层,发挥主机与控制器之间的适配器的作用,使上层应用操作无需关心控制器的数据处理细节。

BLE的L2CAP层是经典蓝牙L2CAP层的简化版本,它在基础模式下,不执行分段和重组,不涉及流程控制和重传机制,仅使用固定信道进行通信。L2CAP的简化结构如下图所示,简单说就是将应用层数据分包发给BLE controller,将BLE controller收到的数据打包成不同CID数据上报给host层。

BLE L2CAP结构以及ATT组包模型

L2CAP根据BLE Spec设计,主要功能是完成Controller和Host的数据对接,绝大部分都在协议栈底层完成,需要user参与的地方很少。user根据以下几个API进行设置即可。

void    blc_l2cap_register_handler (void *p);

在B91m module等BLE Slave应用中,SDK L2CAP层处理Controller数据的函数为:

int     blc_l2cap_packet_receive (u16 connHandle, u8 * p);

该函数已经在协议栈中实现,它会对接收到的数据进行解析后向上传输给ATT、SIG或SMP。

初始化:

blc_l2cap_register_handler (blc_l2cap_packet_receive);

Slave请求更新连接参数

在BLE协议栈中,slave通过L2CAP层的CONNECTION PARAMETER UPDATE REQUEST命令向master申请一组新的连接参数。该命令格式如下所示,详情请参照《Core_v5.0》(Vol 3/Part A/ 4.20 “CONNECTION PARAMETER UPDATE REQUEST”)。

BLE协议栈中Connection Para update Req格式

该 BLE SDK在L2CAP层上提供了slave主动申请更新连接参数的API,用来向master发送上面这个CONNECTION PARAMETER UPDATE REQUEST命令。

void  bls_l2cap_requestConnParamUpdate (u16 min_interval, u16 max_interval, u16 latency, u16 timeout);

以上四个参数跟CONNECTION PARAMETER UPDATE REQUEST的data区域中四个参数对应。注意min_interval和max_interval的值是实际interval时间值除以1.25 ms(如申请7.5ms的连接,该值为6),latency的值是实际latency的时间除以10ms再减1(如1s的latency该值为99),timeout的值为实际supervision timeout时间值除以10ms(如1s的timeout该值为100)。

应用举例:在connection建立的时候,申请更新连接参数。

void    task_connect (u8 e, u8 *p, int n)
{
    ···
    bls_l2cap_requestConnParamUpdate (8, 8, 99, 400);
    ···
}

抓包显示conn para update request和response

bls_l2cap_requestConnParamUpdate用来更新连接参数,slave设备默认在连接建立后1s再执行发送连接参数更新请求。 用户也可以在bls_l2cap_requestConnParamUpdate后使用API:

void bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(int time_ms)
用于设置连接建立后slave设备等待time_ms(单位:毫秒)再执行发送连接参数更新请求。

在slave应用中,SDK提供了获取Conn_UpdateRsp结果的注册回调函数接口,用于通知用户slave申请的连接参数请求被master拒绝还是接受,如上图所示,master接受了slave的Connection_Param_Update_Req参数。

void blc_l2cap_registerConnUpdateRspCb(l2cap_conn_update_rsp_callback_t cb);

参考slave初始化用例:

blc_l2cap_registerConnUpdateRspCb(app_conn_param_update_response)

其中回调函数app_conn_param_update_response函数参考如下:

int app_conn_param_update_response(u8 id, u16  result)
{
    if(result == CONN_PARAM_UPDATE_ACCEPT){
        //the LE master Host has accepted the connection parameters
    }
    else if(result == CONN_PARAM_UPDATE_REJECT){
        //the LE master Host has rejected the connection parameter
    }
    return 0;
}

ATT & GATT

(1) GATT基本单位Attribute

GATT定义了两种角色:Server和Client。该BLE SDK中,Slave是Server,对应的Android、iOS设备是Client。Server需要提供多个service供Client访问。

GATT的service实质是由多个Attribute构成,每个Attribute都具有一定的信息量,当多个不同种类的Attribute组合在一起时,就能够反映出一个基本的service。

Attribute构成GATT service

一个Attribute的基本内容和属性包括以下:

1) Attribute Type: UUID

UUID用来区分每一个attribute的类型,其全长为16个bytes。BLE标准协议中UUID长度定义为2个bytes,这是因为master设备都遵循同一套转换方法,将2个bytes的UUID转换成16 bytes。

user直接使用蓝牙标准的2 bytes的UUID时,master设备都知道这些UUID代表的设备类型。该BLE SDK中已经定义了一些标准的UUID,分布在以下文件中:stack/ble/service/hids.h、stack/ble/service/uuid.h。

Telink私有的一些profile(OTA、MIC、SPEAKER等),标准蓝牙里面不支持,在stack/ble/service/uuid.h中定义这些私有的UUID,长度为16 bytes。

2) Attribute Handle

slave拥有多个Attribute,这些Attribute组成一个Attribute Table。在Attribute Table中,每一个Attribute都有一个Attribute Handle值,用来区分每一个不同的Attribute。slave和master建立连接后,master通过Service Discovery过程解析读取到slave的Attribute Table,并根据Attribute Handle的值来对应每一个不同的Attribute,这样它们后面的数据通信只要带上Attribute Handle,对方就知道是哪个Attribute的数据了。

3) Attribute Value

每个Attribute都有对应的Attribute Value,用来作为request、response、notification和indication的数据。在该BLE SDK中,Attribute Value用指针和指针所指区域的长度来描述。

(2) Attribute and ATT Table

为了实现slave端的GATT service,该BLE 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;

结合目前该BLE SDK给出的参考Attribute Table来说明以上各项的含义。Attribute Table代码见相应工程目录下的app_att.c或者default_att.c,如下所示部分代码:

static const attribute_t my_Attributes[] = {
    {ATT_END_H - 1, 0,0,0,0,0}, // total num of attribute
    // 0001 - 0007  gap
    {7,ATT_PERMISSIONS_READ,2,2,(u8*)(&my_primaryServiceUUID),  (u8*)(&my_gapServiceUUID), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof(my_devNameCharVal),(u8*)(&my_characterUUID), (u8*)(my_devNameCharVal), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof(my_devName), (u8*)(&my_devNameUUID), (u8*)(my_devName), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof(my_appearanceCharVal),(u8*)(&my_characterUUID), (u8*)(my_appearanceCharVal), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof (my_appearance), (u8*)(&my_appearanceUUID),    (u8*)(&my_appearance), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof(my_periConnParamCharVal),(u8*)(&my_characterUUID), (u8*)(my_periConnParamCharVal), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof (my_periConnParameters),(u8*)(&my_periConnParamUUID),  (u8*)(&my_periConnParameters), 0},

    // 0008 - 000b gatt
    {4,ATT_PERMISSIONS_READ,2,2,(u8*)(&my_primaryServiceUUID),  (u8*)(&my_gattServiceUUID), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof(my_serviceChangeCharVal),(u8*)(&my_characterUUID),         (u8*)(my_serviceChangeCharVal), 0},
    {0,ATT_PERMISSIONS_READ,2,sizeof (serviceChangeVal), (u8*)(&serviceChangeUUID),     (u8*)(&serviceChangeVal), 0},
    {0,ATT_PERMISSIONS_RDWR,2,sizeof (serviceChangeCCC),(u8*)(&clientCharacterCfgUUID), (u8*)(serviceChangeCCC), 0},
};

请注意,Attribute Table的定义前面加了const:

static const attribute_t my_Attributes[] = { ... };

const关键字会让编译器将这个数组的数据最终都存储到flash,以节省ram空间。这个Attribute Table定义在flash上的所有内容是只读的,不能改写。

1) attNum

attNum有两个作用。

attNum第一个作用是表示当前Attribute Table中有效Attribute数目,即Attribute Handle的最大值,该数目只在Attribute Table数组的第0项无效Attribute中使用:

{ATT_END_H - 1, 0,0,0,0,0},

attNum = ATT_END_H - 1表示当前Attribute Table中共有ATT_END_H - 1个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在ATT_HANDLE中自动计数ATT_END_H,目前在B91m_ble_sample中所使用的ATT_END_H为46,也就是有45个attNum。user如果添加或删除了Attribute,需要在ATT_HANDLE中进行相应的修改以保证ATT_END_H正确。

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 1 ~ Attribute Handle 7这7个Attribute是属于gap service的描述。

除了第0项Attribute和每一个service首个Attribute外,其他所有的Attribute的attNum的值都必须设为0。

2) perm

perm是permission的简写。

perm用于指定当前Attribute被Client访问的权限。

权限有以下15种,每个Attribute的权限都必须为下面的值或它们的组合。

#define ATT_PERMISSIONS_READ                 0x01 //!< Attribute is Readable
#define ATT_PERMISSIONS_WRITE                0x02 //!< Attribute is Writable
#define ATT_PERMISSIONS_RDWR                 (ATT_PERMISSIONS_READ | ATT_PERMISSIONS_WRITE)   //!< Attribute is Readable & Writable

#define ATT_PERMISSIONS_ENCRYPT_READ         (ATT_PERMISSIONS_READ  | ATT_PERMISSIONS_ENCRYPT)      //!< Read requires Encryption
#define ATT_PERMISSIONS_ENCRYPT_WRITE        (ATT_PERMISSIONS_WRITE | ATT_PERMISSIONS_ENCRYPT)      //!< Write requires Encryption
#define ATT_PERMISSIONS_ENCRYPT_RDWR         (ATT_PERMISSIONS_RDWR  | ATT_PERMISSIONS_ENCRYPT)      //!< Read & Write requires Encryption

#define ATT_PERMISSIONS_AUTHEN_READ          (ATT_PERMISSIONS_READ  | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN)         //!< Read requires Authentication
#define ATT_PERMISSIONS_AUTHEN_WRITE         (ATT_PERMISSIONS_WRITE | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN)         //!< Write requires Authentication
#define ATT_PERMISSIONS_AUTHEN_RDWR          (ATT_PERMISSIONS_RDWR  | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN)         //!< Read & Write requires Authentication

#define ATT_PERMISSIONS_SECURE_CONN_READ     (ATT_PERMISSIONS_READ  | ATT_PERMISSIONS_SECURE_CONN | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN)   //!< Read requires Secure_Connection
#define ATT_PERMISSIONS_SECURE_CONN_WRITE    (ATT_PERMISSIONS_WRITE | ATT_PERMISSIONS_SECURE_CONN | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN)  //!< Write requires Secure_Connection
#define ATT_PERMISSIONS_SECURE_CONN_RDWR     (ATT_PERMISSIONS_RDWR  | ATT_PERMISSIONS_SECURE_CONN | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN)   //!< Read & Write requires Secure_Connection

#define ATT_PERMISSIONS_AUTHOR_READ          (ATT_PERMISSIONS_READ | ATT_PERMISSIONS_AUTHOR)        //!< Read requires Authorization
#define ATT_PERMISSIONS_AUTHOR_WRITE         (ATT_PERMISSIONS_WRITE | ATT_PERMISSIONS_AUTHEN)       //!< Write requires Authorization
#define ATT_PERMISSIONS_AUTHOR_RDWR          (ATT_PERMISSIONS_RDWR | ATT_PERMISSIONS_AUTHOR)        //!< Read & Write requires Authorization

注意:目前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 = 2的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_serviceChangeCharVal),(u8*)(&my_characterUUID),         (u8*)(my_serviceChangeCharVal), 0},

UUID=0x2803在BLE中表示character,uuid指向my_devNameCharVal在flash中的地址,uuidLen为2,master来读这个Attribute时,UUID会是0x2803。

b) Telink私有的16 bytes UUID:

如audio中MIC的Attribute,相关代码:

#define TELINK_MIC_DATA     0x18,0x2B,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00
const u8 my_MicUUID[16]     = TELINK_MIC_DATA;
static u8 my_MicData        = 0x80;
{0,ATT_PERMISSIONS_READ,16,sizeof(my_MicData),(u8*)(&my_MicUUID),   (u8*)(&my_MicData), 0},

uuid指向my_MicData在flash中的地址,uuidLen为16,master来读这个Attribute时,UUID会是0x000102030405060708090a0b0c0d2b18。

4) pAttrValue and attrLen

每一个Attribute都会有对应的Attribute Value。pAttrValue是一个u8型指针,指向Attribute Value所在RAM/Flash的地址,attrLen用来反映该数据在RAM/Flash上的长度。当master读取slave某个Attribute的Attribute Value时,该BLE SDK从Attribute的pAttrValue指针指向的区域(RAM/Flash)开始,取attrLen个数据回给master。

UUID是只读的,所以uuid是指向flash的指针;而Attribute Value可能会涉及到写操作,如果有写操作必须放在RAM上,所以pAttrValue可能指向RAM,也可能指向Flash。

Attribute Handle=39 hid Information的Attribute,相关代码:

static const u8 hidInformation[] =
{
  U16_LO(0x0111), U16_HI(0x0111),             // bcdHID (USB HID version)
  0x00,                                       // bCountryCode
  0x01                                        // Flags
};
{0,ATT_PERMISSIONS_READ,2, sizeof(hidInformation),(u8*)(&hidInformationUUID),   (u8*)(hidInformation), 0},

在实际应用中,hid information 4个byte 0x01 0x00 0x01 0x11是只读的,不会涉及到写操作,所以定义时可以使用const关键字存储在flash上。pAttrValue指向hidInformation在flash上的地址,此时attrLen以hidInformation实际的长度取值。当master读该Attribute时,会根据pAttrValue和attrLen返回0x01000111给master。

master读该Attribute时BLE抓包如下图,master使用ATT_Read_Req命令,设置要读的AttHandle = 0x27 = 39,对应SDK中Attribute Table中的hid information。

master读hidInformation的BLE抓包

Attribute Handle=44 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采样到的电量而改变,然后通过slave主动notify或者master主动读的方式传输给master,所以my_batVal应该放在内存上,此时pAttrValue指向my_batVal在RAM上的地址。

5) 回调函数w

回调函数w是写函数。函数原型:

typedef int (*att_readwrite_callback_t)(u16 connHandle, void* p);

user如果需要定义回调写函数,须遵循上面格式。回调函数w是optional的,对某一个具体的Attribute来说,user可以设置回调写函数,也可以不设置回调(不设置回调的时候用空指针0表示),connHandle为master和slave之间的连接句柄,slave应用填BLS_CONN_HANDLE;master应该填BLM_CONN_HANDLE。

回调函数w触发条件为:当slave收到的Attribute PDU的Attribute Opcode为以下三个时,slave会检查回调函数w是否被设置:

a) opcode = 0x12, Write Request.

b) opcode = 0x52, Write Command.

c) opcode = 0x18, Execute Write Request.

slave收到以上写命令后,如果没有设置回调函数w,slave会自动向pAttrValue指针所指向的区域写master传过来的值,写入的长度为master数据包格式中的l2capLen-3;如果user设置了回调函数w,slave收到以上写命令后执行user的回调函数w,此时不再向pAttrValue指针所指区域写数据。这两个写操作是互斥的,只能有一个生效。

user设置回调函数w是为了处理master在ATT层的Write Request、Write Command和Execute Write Request命令,如果没有设置回调函数w,需要评估pAttrValue所指向的区域是否能够完成对以上命令的处理(如pAttrValue指向flash无法完成写操作;或者attrLen长度不够,master的写操作会越界,导致其他数据被错误地改写)。

BLE 协议栈中Write Request

BLE 协议栈中Write Command

协议栈中Execute Write Request

回调函数w的void型p指针指向master写命令的具体数值。实际p指向一片内存,内存上的值如下面结构体所示。

typedef struct{
    u8  type;
    u8  rf_len;
    u16 l2cap;
    u16 chanid;

    u8  att;
    u16 handle;

    u8  dat[20];

}rf_packet_att_data_t;

p指向type。写过来的数据有效长度为l2cap - 3,第一个有效数据为pw->dat[0]。

int my_WriteCallback (void *p)
{
    rf_packet_att_data_t *pw = (rf_packet_att_data_t *)p;
    int len = pw->l2cap - 3;
    //add your code
    //valid data is pw->dat[0] ~ pw->dat[len-1]
    return 1;
}   

上面这个结构体rf_packet_att_data_t所在位置为stack/ble/ble_format.h。

6) 回调函数r

回调函数r是读函数。函数原型:

typedef int (*att_readwrite_callback_t)(u16 connHandle, void* p);

User如果需要定义回调读函数,须遵循上面格式。回调函数r是optional的,对某一个具体的Attribute来说,user可以设置回调读函数,也可以不设置回调(不设置回调的时候用空指针0表示),connHandle为master和slave之间的连接句柄,slave应用填BLS_CONN_HANDLE;master应该填BLM_CONN_HANDLE。

回调函数r触发条件为:当slave收到的Attribute PDU的Attribute Opcode为以下两个时,slave会检查回调函数r是否被设置:

a) opcode = 0x0A, Read Request.

b) opcode = 0x0C, Read Blob Request.

slave收到以上读命令后,

a) 如果user设置了回调读函数,执行该函数,根据该函数的返回值决定是否回复Read Response/Read Blob Response:

  • 若返回值为1,slave不回复Read Response/Read Blob Response给master。

  • 若返回值为其他值,slave从pAttrValue指针所指向的区域读attrLen个值用Read Response/Read Blob Response回复给master。

b) 如果user没有设置回调读函数,slave从pAttrValue指针所指向的区域读attrLen个值用Read Response/Read Blob Response回复给master。

如果user想在收到master的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  
static const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;

Service Attribute Layout

8) ATT table Initialization

GATT & ATT初始化只需要将应用层的Attribute Table的指针传到协议栈即可,提供的API:

void        bls_att_setAttributeTable (u8 *p);

p为Attribute Table的指针。

(3) Attribute PDU and GATT API

根据BLE Spec,该BLE SDK目前支持的Attribute PDU有以下几类:

  • Requests:client发送给server的数据请求。

  • Responses:server收到client的request后发送的数据回应。

  • Commands:client发送给server的命令,无应答。

  • Notifications:server发送给client的数据,无应答。

  • Indications:server发送给client的数据。

  • Confirmations:client对server数据的确认。

下面结合之前介绍的Attribute结构和Attribute Table结构,对ATT层所有的ATT PDU进行分析。

1) Read by Group Type Request, Read by Group Type Response

Read by Group Type Request和Read by Group Type Response详请参照《Core_v5.0》(Vol 3/Part F/3.4.4.9 and 3.4.4.10)。

master发送Read by Group Type Request,在该命令中指定起始和结束的attHandle,指定attGroupType。slave收到该Request后,遍历当前Attribute table,在指定的起始和结束的attHandle中找到符合attGroupType的Attribute Group,通过Read by Group Type Response回复Attribute Group信息。

Read by Group Type Request Read by Group Type Response

上图所示,master查询slave的UUID为0x2800的primaryServiceUUID的Attribute Group信息:

#define GATT_UUID_PRIMARY_SERVICE        0x2800
static const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;

参考当前demo code,slave的Attribute table中有以下几组符合该要求:

a) Attribute Group with attHandle from 0x0001 to 0x0007,

 Attribute Value为SERVICE_UUID_GENERIC_ACCESS (0x1800).

b) Attribute Group with attHandle from 0x0008 to 0x000b,

 Attribute Value为SERVICE_UUID_GENERIC_ATTRIBUTE (0x1801).

c) Attribute Group with attHandle from 0x000c to 0x000e,

 Attribute Value为SERVICE_UUID_DEVICE_INFORMATION (0x180A).

d) Attribute Group with attHandle from 0x000f to 0x0029,

 Attribute Value为SERVICE_UUID_HUMAN_INTERFACE_DEVICE (0x1812).

e) Attribute Group with attHandle from 0x002a to 0x002d,

 Attribute Value为SERVICE_UUID_BATTERY (0x180F).

f) Attribute Group with attHandle from 0x002e to 0x0032,

 Attribute Value为TELINK_OTA_UUID_SERVICE(0x12,0x19,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00).

slave将以上6个GROUP的attHandle和attValue的信息通过Read by Group Type Response回复给master,最后一个ATT_Error_Response表明所有的Attribute Group都已回复完毕,Response结束,master看到这个包也会停止发送Read by Group Type Request。

2) Find by Type Value Request, Find by Type Value Response

Find by Type Value Request和Find by Type Value Response详请参照《Core_v5.0》(Vol 3/Part F/3.4.3.3 and 3.4.3.4)。

master发送Find by Type Value Request,在该命令中指定起始和结束的attHandle,指定AttributeType和Attribute Value。slave收到该Request后,遍历当前Attribute table,在指定的起始和结束的attHandle中找到AttributeType和Attribute Value相匹配的Attribute,通过Find by Type Value Response回复Attribute。

Find by Type Value Request Find by Type Value Response

3) Read by Type Request, Read by Type Response

Read by Type Request和Read by Type Response详请参照《Core_v5.0》(Vol 3/Part F/3.4.4.1 and 3.4.4.2)。

master发送Read by Type Request,在该命令中指定起始和结束的attHandle,指定AttributeType。slave收到该Request后,遍历当前Attribute table,在指定的起始和结束的attHandle中找到符合AttributeType的Attribute,通过Read by Type Response回复Attribute。

Read by Type Value Request Find by Type Value Response

上图所示,master读attType为0x2803的Attribute,slave中Attribute Handle为0x0002的Attribute:

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)
};
#define GATT_UUID_CHARACTER              0x2803
static const u16 my_characterUUID = GATT_UUID_CHARACTER;
{0,ATT_PERMISSIONS_READ,2,sizeof(my_devNameCharVal),(u8*)(&my_characterUUID), (u8*)(my_devNameCharVal), 0},

Read by Type response中length为8,attData中前两个byte为当前的attHandle 0003,后面6个bytes为对应的Attribute Value。

4) Find information Request, Find information Response

Find information request和Find information response详请参照《Core_v5.0》(Vol 3/Part F/3.4.3.1 and 3.4.3.2)。

master发送Find information request,指定起始和结束的attHandle。slave收到该命令后,将起始和结束的所有attHandle对应Attribute的UUID通过Find information response回复给master。如下图所示,master要求获得attHandle 0x0016 ~ 0x0018三个Attribute的information,slave回复这三个Attribute的UUID。

Find Information Request Find Information Response

5) Read Request, Read Response

Read Request和Read Response详请参照《Core_v5.0》(Vol 3/Part F/3.4.4.3 and 3.4.4.4)。

master发送Read Request,指定某一个attHandle,slave收到后通过Read Response回复指定的Attribute的Attribute Value(若设置了回调函数r,执行该函数),如下图所示。

Read Request Read Response

6) Read Blob Request, Read Blob Response

Read Blob Request和Read Blob Response详请参照《Core_v5.0》(Vol 3/Part F/3.4.4.5 and 3.4.4.6)。

当slave某个Attribute的Attribute Value值的长度超过MTU_SIZE(目前SDK中为23)时,master需要启用Read Blob Request来读取该Attribute Value,从而使得Attribute Value可以分包发送。master在Read Blob Request指定attHandle和ValueOffset,slave收到该命令后,找到对应的Attribute,根据ValueOffset值通过Read Blob Response回复Attribute Value(若设置了回调函数r,执行该函数)。

如下图所示,master读slave的HID report map(report map很大,远远超过23)时,首先发送Read Request,slave回Read response,将report map前一部分数据回给master。之后master使用Read Blob Request,slave通过Read Blob Response回数据给master。

Read Blob Request Read Blob Response

7) Exchange MTU Request, Exchange MTU Response

Exchange MTU Request和Exchange MTU Response详请参照《Core_v5.0》(Vol 3/Part F/3.4.2.1 and 3.4.2.2)。

如下面所示,master和slave通过Exchange MTU Request和Exchange MTU Response获知对方的MTU size。

Exchange MTU Request Exchange MTU Response

当Telink BLE Slave GATT层的的数据访问过程中出现超过一个RF包长度的数据,涉及到GATT层分包和拼包时,需要提前和master交互双方的RX MTU size,也就是MTU size exchange的过程。MTU size exchange的目的是为了实现GATT层长包数据的收发。前面3.2.8小节描述的Telink Ble SDK中,当slave端在主函数调用blc_att_setRxMtuSize()设置Rx MTU大小时,若size大于23则会主动进行上报MTU和更新DLE。

a) 用户可以通过注册GAP event回调并开启eventMask: GAP_EVT_MASK_ATT_EXCHANGE_MTU来获取EffectiveRxMTU,其中:

EffectiveRxMTU=min(ClientRxMTU, ServerRxMTU)

本文档 “GAP event”小节会详细介绍GAP event。

b) Slave GATT层收长包数据的处理。

Slave ServerRxMTU默认为23,实际最大ServerRxMTU可以支持到250,即master端250个byte的分包数据在Slave端可以正确地完成数据包重拼。当应用中需要使用到master的分包重拼时,使用下面API先修改Slave端的RX size:

ble_sts_t   blc_att_setRxMtuSize(u16 mtu_size);

返回值列表:

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
GATT_ERR_INVALID_ PARAMETER 见SDK中定义 mtu_size大于最大值250

如果Master GATT层有长包数据需要发送给Slave,Master端会主动发起ATT_Exchange_MTU_req,此时Slave回复ATT_Exchange_MTU_rsp,其中ServerRxMTU为上面API: blc_att_setRxMtuSize设置的值。如果用户注册了GAP event,并且开启了eventMask: GAP_EVT_MASK_ATT_EXCHANGE_MTU,用户可以在GAP event回调函数中获取EffectiveRxMTU和Master端的ClientRxMTU。

c) Slave GATT层发长包数据的处理。

当Slave需要在GATT层发送长包数据时,需要先获取到Master的Client RxMTU,最终的数据长度不能大于ClientRxMTU。

Slave先使用API blc_att_setRxMtuSize设置自己的ServerRxMTU,假设设为158。

blc_att_setRxMtuSize(158);

再调用下面API主动发起一个ATT_Exchange_MTU_req:

ble_sts_t    blc_att_requestMtuSizeExchange (u16 connHandle, u16 mtu_size);

connHandle为Slave connection的ID,为BLS_CONN_HANDLE,mtu_size为ServerRxMTU。

blc_att_requestMtuSizeExchange(BLS_CONN_HANDLE,  158);

Master收到ATT_Exchange_MTU_req后,回复ATT_Exchange_MTU_rsp,SDK收到rsp后,计算EffectiveRxMTU。如果用户注册了GAP event并且开启了eventMask: GAP_EVT_MASK_ATT_EXCHANGE_MTU,则EffectiveRxMTU和ClientRxMTU会上报给用户。

8) Prepare Write Request

当User采用Prepare Write命令来传输数据时,若传输数据大于260 Bytes,Slave端可能出现接收数据不完整的情况,这是由于Telink Ble SDK协议栈默认设置的Buffer大小为260 Bytes,最后的3 Bytes用于保存其他有用的信息,User可以针对实际应用,调用API blc_att_setPrepareWriteBuffer来自定义设置所需的Buffer大小。

void    blc_att_setPrepareWriteBuffer(u8 *p, u16 len);

参数p为指向设定buffer的指针, len为设定Buffer的长度。

9) Write Request, Write Response

Write Request和Write Response详请参照 《Core_v5.0》(Vol 3/Part F/3.4.5.1 and 3.4.5.2)。

master发送Write Request,指定某个attHandle,并附带相关数据。slave收到后,找到指定的Attribute,根据user是否设置了回调函数w决定数据是使用回调函数w来处理还是直接写入对应的Attribute Value,并回复Write Response。

下图所示为master向attHandle为0x0015的Attribute写入Attribute Value为0x0001,slave收到后执行该写操作,并回Write Response。

Write Request Write Response

10) Write Command

Write Command详请参照《Core_v5.0》(Vol 3/Part F/3.4.5.3)。

master发送Write Command,指定某个attHandle,并附带相关数据。slave收到后,找到指定的Attribute,根据user是否设置了回调函数w决定数据是使用回调函数w来处理还是直接写入对应的Attribute Value,不回复任何信息。

11) Queued Writes

Queued Writes包含Prepare Write Request/Response和Execute Write Request/Response等ATT协议,详情内容可以参照《Core_v5.0》(Vol 3/Part F/3.4.6/Queued Writes)。

Prepare Write Request和Execute Write Request可以实现如下两种功能:

a) 提供长属性值的写入功能。

b) 允许在一个单独执行的原子操作中写入多个值。

Prepare Write Request包含AttHandle、ValueOffset和PartAttValue,这和Read_Blob_Req/Rsp类似。这说明Client既可以在队列中准备多个属性值,也可以准备一个长属性值的各个部分。这样,在真正执行准备队列之前,Client可以确定某属性的所有部分都能写入Server。

备注:当前SDK版本仅支持长属性值写入功能,长属性值最大长度小于等于244字节。

如下图所示,master向slave某个特性写很长的字符串:“I am not sure what a new song”(字节数远远超过23,使用默认MTU情况下)时,首先发送Prepare Write Request,偏移0x0000,将“I am not sure what”部分数据写给slave,slave向master回Prepare Write Response。之后master发送Prepare Write Request,偏移0x12,将“ a new song”部分数据写给slave,slave向master回Prepare Write Response。当master将长属性值全部写完成后,发送Execute Write Request给slave,Flags为1,表示写立即生效,slave回复Execute Write Response,整个Prepare Write过程结束。

这里我们可以看到Prepare Write Response也包含请求中的AttHandle、ValueOffset和PartAttValue。这样做的目的为了数据传递的可靠性。Client可以对比Response和Request的字段值,确保准备的数据被正确接收。

Write Long Characteristic Values示例

12) Handle Value Notification

Handle Value Notification详请参照《Core_v5.0》(Vol 3/Part F/3.4.7.1)。

BLE Spec中Handle Value Notification

上图所示为BLE Spec中Handle Value Notification的格式。

该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);

13) Handle Value Indication

Handle Value Indication详请参照《Core_v5.0》(Vol 3/Part F/3.4.7.2)。

Handle Value Indication in BLE Spec

上图所示为BLE Spec中Handle Value Indication的格式。

该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);

调用该API时,建议用户检查返回值的是否为BLE_SUCCESS:1. 处于配对阶段时,新API返回值:SMP_ERR_PAIRING_BUSY;2.处于加密阶段时,新API返回值:LL_ERR_ENCRYPTION_BUSY;3. 当len大于ATT_MTU-3时(3是ATT层的包格式长度opcode 和handle),说明要发送的数据长度PDU超出了ATT层支持的最大PDU长度ATT_MTU,API返回值:GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE。

第一个参数connHandle 为对应GATT服务的connHandle,第二个参数attHandle为对应Attribute的attHandle,第三个参数p为要发送的连续内存数据的头指针,第四个参数len指定发送的数据的字节数。该API支持自动拆包功能(根据EffectiveMaxTxOctets做分包处理,即链路层RF TX最大发送字节数,DLE可能会修改该值,默认为27,下文将介绍其替换API,见备注),可将一个很长的数据拆成多个BLE RF packet发送出去,所以len可以支持很大。

BLE Spec里规定了每一个indicate的数据,都要等到Master的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 添加成功
LL_ERR_CONNECTION_NOT_ ESTABLISH 见SDK中定义 Link Layer处于None Conn state
LL_ERR_ENCRYPTION_BUSY 见SDK中定义 处于配对或加密阶段,不能发送数据
LL_ERR_TX_FIFO_NOT_ENOUGH 见SDK中定义 有大数据量任务在运行,软件Tx fifo不够用
GATT_ERR_DATA_PENDING_DUE _TO_SERVICE_DISCOVERY_BUSY 见SDK中定义 处于遍历服务阶段,不能发数据
GATT_ERR_PREVIOUS_INDICATE_ DATA_HAS_NOT_CONFIRMED 见SDK中定义 前一个indicate数据还没有被master确认

14) Handle Value Confirmation

Handle Value Confirmation详请参照《Core_v5.0》(Vol 3/Part F/3.4.7.3)。

应用层每调用一次bls_att_pushIndicateData(或者调用blc_gatt_pushHandleValueIndicate),向master发送indicate数据后,master会回复一个confirm,表示对这个数据的确认,然后slave才可以继续发送下一个indicate数据。

BLE Spec中Handle Value Confirmation

从上图中可以看出,Confirmation并不指定是对哪一个具体handle的确认,对所有不同handle上的indicate数据都统一回复一个Confirmation。

为了让应用层了解发送出去的indicate data是否已经被Confirm,用户可以通过注册GAP event回调,并开启相应的eventMask:GAP_EVT_GATT_HANDLE_VALUE_CONFIRM来获取Confirm事件,本文档“GAP event”小节会详细介绍GAP event。

(4) GATT Service Security

在介绍GATT Service Security前,用户可以先了解一下SMP相关的内容。

请参考“SMP”章节相关的详细介绍,了解LE配对方式、加密等级等基础知识。

下图是BLE spec给出的GATT服务安全等级服务请求之间映射关系,详细可以参考《core5.0》(Vol3/Part C/10.3 AUTHENTICATION PROCEDURE)。

服务请求响应映射关系

用户可以很清楚地看到:

  • 第一列(Link Encryption State)跟当前连接的slave设备是否处于加密状态下有关;

  • 第二列(Local Device’s Access Requirement for Service)则跟用户设置的ATT表中特性的权限(Permission Access)设置有关,如下图所示;

  • 第三列(Local Device Pairing Status)又分为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

ATT Permission定义

最终GATT service security的实现跟SMP初始化时的参数配置,包括支持的最高安全级别设置、ATT表中的特性权限设置等都有关系,而且跟master也有关系,比如我们slave设置的SMP能支持的最高等级是Authenticated pairing with encryption,但是master具备的最高安全等级是Unauthenticated pairing with encryption,此时如果ATT表中某个写特性的权限是ATT_PERMISSIONS_AUTHEN_WRITE,那么master在写该特性时,我们会回复加密等级不够的错误。

用户可以设定ATT表中特性权限实现如下应用:

比如slave设备支持的最高安全级别是Unauthenticated pairing with encryption,但是不想连接后使用发送Security Request这种方式去触发master开始配对,那么客户可以将某些具备notify属性的客户端特性配置(Client Characteristic Configuration,简称CCC)属性的权限设置为ATT_PERMISSIONS_ENCRYPT_WRITE,那么master只有写该CCC后,slave会回复其安全级别不够,这会触发master开启配对加密流程。

注意:

用户设置的安全级别只表示设备能支持的最高安全级别,只要ATT表中特性的权限(ATT Permission)不超过实际生效的最高级别就可以通过GATT service security管控。对于LE安全模式1中的等级4来说,如果用户只设置Authenticated LE Secure Connections一种级别,则代表当前设置支持LE Secure Connections only。

GATT安全级别的示例用户可以参B91m_feature/feature_gatt_security/app.c。

SMP

Security Manager(SM)在BLE中的主要目的是为LE设备提供加密所需要的各种Key,确保数据的机密性。加密链路可以确保避免第三方“攻击者”拦截、破译或者读取空中数据原始内容。SMP详细内容请用户参考《Core_v5.0》(Vol 3/Part H/ Security Manager Specification)。

(1) SMP安全等级

BLE 4.2 Spec新增了一种称作安全连接(LE Secure Connections)配对方式,新配对方式在安全性方面得到进一步增强,而BLE4.2以前的配对方式我们统称传统配对(LE legacy pairing)。

回顾“GATT service Security”小节,可知本地设备的配对状态类型如下:

本地设备配对状态

这四个状态分别对应LE安全模式1的四个级别:

a) No authentication and no encryption (LE security mode1 level1)

b) Unauthenticated pairing with encryption (LE security mode1 level2)

c) Authenticated pairing with encryption (LE security mode1 level3)

d) Authenticated LE Secure Connections (LE security mode1 level4)

详情可以参考《Core_v5.0》(Vol 3//Part C/10.2 LE SECURITY MODES)。

注意:本端设备设定的安全级别只表示本端设备可能达到的最高安全级别,想要达到设定的安全级别跟两个因素有关:

a) master对端设定能支持的最高安全级别 >= slave本端设定能支持的最高安全级别;

b) 本端和对端按照各自设定的SMP参数正确处理完配对整个流程(如果存在配对的话)。

举例来说,用户即便是设置slave端能够支持的最高安全等级是mode1 level3,但是连接slave的master设置为不支持配对加密(最高只支持mode1 level1),那么连接后slave和master不会进行配对流程,slave实际使用的安全级别是mode1 level1。

用户可以通过如下API设置SM能支持的最高安全等级:

void blc_smp_setSecurityLevel(le_security_mode_level_t  mode_level);

枚举类型le_security_mode_level_t 具体定义介绍如下:

typedef enum {
    LE_Security_Mode_1_Level_1 = BIT(0),  No_Authentication_No_Encryption           = BIT(0), No_Security = BIT(0),
    LE_Security_Mode_1_Level_2 = BIT(1),  Unauthenticated_Paring_with_Encryption    = BIT(1),
    LE_Security_Mode_1_Level_3 = BIT(2),  Authenticated_Paring_with_Encryption      = BIT(2),
    LE_Security_Mode_1_Level_4 = BIT(3),  Authenticated_LE_Secure_Connection_Paring_with_Encryption = BIT(3),

    LE_Security_Mode_2_Level_1 = BIT(4),  Unauthenticated_Paring_with_Data_Signing  = BIT(4),
    LE_Security_Mode_2_Level_2 = BIT(5),  Authenticated_Paring_with_Data_Signing    = BIT(5),

    LE_Security_Mode_1 = (LE_Security_Mode_1_Level_1 | LE_Security_Mode_1_Level_2 | LE_Security_Mode_1_Level_3 | LE_Security_Mode_1_Level_4)
}le_security_mode_level_t;

(2) SMP参数配置

Telink BLE SDK中SMP参数配置介绍主要围绕SMP四个安全等级的配置展开,slave的SMP功能目前能支持的最高级别是LE security mode1 level4。

1) LE security mode1 level1

安全级别1表示设备不支持加密配对,如果需要禁用SMP功能,用户只需要在初始化的地方调用如下函数:

blc_smp_setSecurityLevel(No_Security);

表示设备端不会对当前连接进行配对加密过程,即使对方请求配对加密,设备端也会拒绝配对加密。一般用于当前设备不支持设备加密配对的过程。如下图,master发起配对请求,slave回复SM_Pairing_Failed。

抓包显示Pairing Disable

2) LE security mode1 level2

安全级别2表示设备最高支持Unauthenticated_Paring_with_Encryption,如传统配对和安全连接配对方式下的Just Works配对模式。

A. 通过前文SMP基本概念的描述,我们知道SM配对方法包括传统加密配对和安全连接配对,SDK提供了如下API用于设置是否支持BLE4.2新加密特性:

void blc_smp_setParingMethods (paring_methods_t  method);

枚举类型paring_methods_t具体定义介绍如下:

typedef enum {
    LE_Legacy_Paring     = 0,   // BLE 4.0/4.2
    LE_Secure_Connection = 1,   // BLE 4.2/5.0/5.1
}paring_methods_t;

B. 使用LE security mode1 level1以外的安全级别配置就必须要调用如下API用于初始化SMP各参数配置,包括绑定区域FLASH的初始化配置:

int blc_smp_peripheral_init (void);

如果在初始化阶段只调用了该API,则SDK会使用默认参数去配置SMP:

  • 默认支持的最高安全等级:Unauthenticated_Paring_with_Encryption;

  • 默认绑定模式:Bondable_Mode(存储配对加密后分发的KEY到FLASH);

  • 默认IO能力是IO_CAPABILITY_NO_INPUT_NO_OUTPUT。

以上默认参数是按照传统配对Just Works模式配置的,所以用户只调用该API,相当于配置了LE security mode1 level2,通过A和B我们知道LE security mode1 level2有两种配置:

A. 设备具备在传统配对下Just works的初始化配置:

blc_smp_peripheral_init();

B. 设备具备在安全连接下Just works的初始化配置:

blc_smp_setParingMethods(LE_Secure_Connection);
blc_smp_peripheral_init();

3) LE security mode1 level3

安全级别3表示设备最高支持Authenticated pairing with encryption,如传统配对模式下的Passkey Entry、Out of Band等。

该级别需要设备支持Authentication,也就是需要通过某种方法确保配对双方身份的合法性,BLE给出了如下三种Authentication方式:

  • 有人参与的方式,如设备具备按键或者显示能力,通过一方显示TK,另一方输入相同的TK(比如Passkey Entry);

  • 配对的双方通过非BLE RF传输方式交互一些信息,进行后续的配对操作(如Out of Band,一般通过NFC传输TK);

  • 设备自行协商TK(如Just Works,两端设备均使用TK:0)。需要注意的是第3种方式属于Unauthenticated,所以Just works的安全级别对应LE security mode1 level2。

Authentication能确保配对双方身份的合法性,提供这种方式的保护又可以称为MITM(Man in the Middle)中间人保护。

A. 具备Authentication的设备需要设置其MITM flag或OOB flag,SDK提供如下两个API用于设置MITM和OOB flag的值:

void blc_smp_enableAuthMITM (int MITM_en);
void blc_smp_enableOobAuthentication (int OOB_en);

其中参数MITM_en、OOB_en的值为0或者1:0对应失能,1对应使能。

B. 根据Authentication方式的介绍,SM提供了三类鉴权方式,这三类方式的选择依赖于配对双方具备的IO能力,我们SDK提供了如下接口用于配置当前设备具备的IO能力:

void        blc_smp_setIoCapability (io_capability_t  ioCapability);

枚举类型io_capability_t具体定义介绍如下:

typedef enum {
    IO_CAPABILITY_UNKNOWN           = 0xff,
    IO_CAPABILITY_DISPLAY_ONLY      = 0,
    IO_CAPABILITY_DISPLAY_YES_NO    = 1,    IO_CAPABILITY_DISPLAY_YESNO = 1,
    IO_CAPABILITY_KEYBOARD_ONLY     = 2,
    IO_CAPABILITY_NO_INPUT_NO_OUTPUT= 3,    IO_CAPABILITY_NO_IN_NO_OUT  = 3,
    IO_CAPABILITY_KEYBOARD_DISPLAY  = 4,
} io_capability_t;

C. 传统配对模式下MITM、OOB flag使用规则:

传统配对模式下MITM、OOB flag使用规则

设备会根据本地设备和对端设备的OOB以及MITM flag决定使用OOB方式还是根据IO能力决定选择什么样的KEY产生方式。下图是SDK根据IO能力映射关系选择不同的KEY产生方法(行、列参数类型io_capability_t):

Mapping Relationship for KEY Generation Method and IO Capability

这部分具体映射关系可以参考《core5.0》(Vol3/Part H/2.3.5.1 Selecting Key Generation Method),文档不再展开介绍。

根据以上介绍,我们知道LE security mode1 level3有如下几种初始值配置方式:

A. 设备具备传统配对下OOB的初始化配置:

blc_smp_enableOobAuthentication(1);
blc_smp_peripheral_init();

这里因为涉及到OOB传输TK值,SDK在应用层提供了相关的GAP event给用户,请参考“GAP event”章节。提供给用户设置OOB TK值的API如下:

void blc_smp_setTK_by_OOB (u8 *oobData);

参数oobData表示需要设置的16位TK值数组的头指针。

B. 设备具备传统配对下Passkey Entry(PK_Resp_Dsply_Init_Input)的初始化配置:

blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_DISPLAY_ONLY);
blc_smp_peripheral_init();

C. 设备具备传统配对下Passkey Entry(PK_Init_Dsply_Resp_Input或者PK_BOTH_INPUT)的初始化配置:

blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_KEYBOARD_ONLY);
blc_smp_peripheral_init();

这里因为涉及到用户输入TK值,SDK在应用层提供了相关的GAP event给用户,请参考“GAP event”章节。提供给用户设置Passkey Entry的TK值API如下:

int         blc_smp_setTK_by_PasskeyEntry (u32 pinCodeInput);

参数pinCodeInput表示设置的pincode值,范围在“0~999999”。在Passkey Entry方式下,master显示TK,slave需要输入TK的情况下使用。

最终设备使用何种Key产生方式基于配对连接的两端设备支持什么样的SMP安全等级,如果master只支持安全等级LE security mode1 level1,那么最终slave是不会启用SMP功能的,因为master不支持配对加密。

4) LE security mode1 level4

安全级别4表示设备最高支持Authenticated LE Secure Connections,如安全连接配对模式下的Numeric Comparison、Passkey Entry、Out of Band等。

根据以上介绍,我们知道LE security mode1 level4有如下几种初始值配置方式:

A. 设备具备安全连接配对下Numeric Comparison的初始化配置:

blc_smp_setParingMethods(LE_Secure_Connection);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_DISPLAY_YESNO);

这里因为涉及到向用户显示数值比较值,SDK在应用层提供了相关的GAP event给用户,请参考“GAP event”章节。提供给用户设置数值比较结果“YES”或“NO”值的API如下:

void blc_smp_setNumericComparisonResult(bool YES_or_NO);

参数YES_or_NO:在数值比较配对方式下,用于给用户确认比较两端显示的数值是否一致。当用户确认显示的6位数值和对端一致时,可以输入1:“YES”,不一致则输入0:“NO”。

B. 设备具备安全连接配对下Passkey Entry的初始化配置:

这部分用户初始化代码和LE security mode1 level3配置方式B、C(传统配对Passkey Entry)基本一致,唯一不同的是需要在初始化最开始的地方设置配对方式为“安全连接配对”:

blc_smp_setParingMethods(LE_Secure_Connection);
.....//Refer to configuration method B/C in LE security mode1 level3

C. 设备具备安全连接配对下Out of Band的初始化配置:

该部分目前SDK并未实现,所以这里就不再做具体介绍。

5) 下面再介绍几个额外的SMP参数配置相关的API:

A. SDK提供是否需要开启绑定功能的API:

void blc_smp_setBondingMode(bonding_mode_t mode);

枚举类型bonding_mode_t具体定义介绍如下:

typedef enum {
    Non_Bondable_Mode = 0,
    Bondable_Mode     = 1,
}bonding_mode_t;

配置安全级别非mode1 level1的设备,必须要使能绑定功能,SDK已经默认开启了,所以一般情况下用户不需要再调用该API。

B. SDK提供是否需使能Key Press功能的API:

void blc_smp_enableKeypress (int keyPress_en);

表示Passkey Entry期间是否支持为KeyboardOnly设备提供一些必要的输入状态信息,因为目前SDK不支持该功能,所以参数必须设置为0。

C. 在安全连接方式下是否启用Debug椭圆加密密匙对:

void blc_smp_setEcdhDebugMode(ecdh_keys_mode_t mode);

枚举类型ecdh_keys_mode_t具体定义介绍如下:

typedef enum {
    non_debug_mode  = 0,  // ECDH distribute private/public key pairs
    debug_mode      = 1,  // ECDH use debug mode private/public key pairs
} ecdh_keys_mode_t;

该API仅在安全连接配对情况下使用,由于安全连接配对情况下使用了椭圆加密算法,可以有效避免窃听,这对调试开发就不那么友好了,用户无法通过sniffer工具抓取BLE空中包,进而进行数据分析调试,所以BLE spec也给出了一组用于Debug的椭圆加密私钥/公钥对,只要开启这个模式,BLE sniffer工具就可以用已知的密钥去解密链路。

D. 通过如下API设置SM是否绑定、是否开启MITM flag、是否支持OOB、是否支持Keypress notification,以及支持的IO能力(文档前面给的都是单独的配置API,为了用户设置方便,SDK也提供了统一配置API):

void blc_smp_setSecurityParameters (bonding_mode_t mode,int MITM_en,int OOB_en, int keyPress_en,
io_capability_t ioCapability);

前面已经介绍了每个参数含义,这里就不再重复了。

(3) SMP安全请求配置

SMP安全请求(Security Request)只有slave可以发送,所以这部分只对slave设备来说。

我们知道配对流程阶段1有一个可选的安全请求包(Security Request),该包的目的是使slave可以主动触发配对流程。SDK提供了如下API用于灵活地配置slave在连接后或者回连后是立即还是pending_ms毫秒后再向master发送Security Request,亦或是不发送Security Request以实现不同的配对触发组合:

void        blc_smp_configSecurityRequestSending( secReq_cfg newConn_cfg,  secReq_cfg re_conn_cfg, u16 pending_ms);

枚举类型secReq_cfg具体定义介绍如下:

typedef enum {
    SecReq_NOT_SEND = 0,  
    SecReq_IMM_SEND = BIT(0),  
    SecReq_PEND_SEND = BIT(1), 
}secReq_cfg;

每个参数的意义介绍如下:

  • SecReq_NOT_SEND:连接建立后,slave不会主动发送Security Request;

  • SecReq_IMM_SEND:连接建立后,slave会立即发送Security Request;

  • SecReq_PEND_SEND:连接建立后,slave等待pending_ms(单位毫秒)后再决定是否发送Security Request。

1) 首次连接,slave在pending_ms毫秒前就收到master的Pairing_request包也不会再发送Security Request;

2) 在回连阶段,pending_ms毫秒之前如果master已经发送LL_ENC_REQ加密回连链路,则不再发送Security Request。

newConn_cfg用于配置新设备,re_conn_cfg用于配置回连的设备。这里SDK在回连时也提供配置是否发配对请求的目的:配对绑定过的设备,下次再连接的时候(即回连),master有时候不一定会主动发起LL_ENC_REQ来加密链路,此时如果slave发一下Security Request就会去触发master主动加密链路,所以SDK提供了re_conn_cfg配置,客户可以根据实际需要配置。

注意:当前函数只能在连接之前调用。建议在初始化的时候调用。

函数blc_smp_configSecurityRequestSending的输入参数有如下9种组合:

Parameter SecReq_NOT_SEND SecReq_IMM_SEND SecReq_PEND_SEND
SecReq_NOT _SEND 第一次连接或者回连都不发SecReq (参数pending_ms无效) 第一次连接不发SecReq,回连立即发SecReq(参数pending_ms无效) 第一次连接不发SecReq,回连pending_ms毫秒后发SecReq (*见前面参数说明)
SecReq_IMM _SEND 第一次连接立即发SecReq,回连不发SecReq(参数pending_ms无效) 第一次连接或者回连都立即发SecReq (参数pending_ms无效) 第一次连接立即发SecReq,回连pending_ms毫秒后发SecReq (*见前面参数说明)
SecReq_ PEND_SEND 第一次连接pending_ms毫秒后发SecReq (*见前面参数说明),回连不发SecReq 第一次连接pending_ms毫秒后发SecReq (*见前面参数说明),回连立即发SecReq 第一次连接或者回连都pending_ms毫秒后发SecReq(*见前面参数说明)

我们挑其中两组做一下详细说明,其他组合类似,不做具体介绍。

1) newConn_cfg: SecReq_NOT_SEND

 re_conn_cfg: SecReq_NOT_SEND

 pending_ms: 此时该参数不起作用。

newConn_cfg:SecReq_NOT_SEND表示新设备slave不会主动发起Security Request,只有在对方发起配对请求时才响应对方的配对请求。如果对方不发送配对请求,则不会进行加密配对。如下图,在master发送配对请求包SM_Pairing_Req时,slave会响应,但是不会主动触发master发起配对请求。

抓包显示Pairing Peer Trigger

re_conn_cfg:SecReq_NOT_SEND表示设备已经配对,回连时slave设备不会发送Security Request。

2) newConn_cfg: SecReq_IMM_SEND

 re_conn_cfg: SecReq_NOT_SEND

 pending_ms: 此时该参数不起作用。

newConn_cfg:SecReq_IMM_SEND表示新设备slave一经连接便会主动向master发Security Request,以触发master开始配对流程。如下图,slave主动发送SM_Security_Req 触发master发送配对请求。

抓包显示Pairing Conn Trigger

re_conn_cfg:SecReq_NOT_SEND表示回连的时候slave不会发送Security Request。

此外SDK还提供了一个单独发送Security Request包的API,用于特殊应用场合,应用层可以随时调用该API发送Security Request包:

int blc_smp_sendSecurityRequest (void);

这里需要注意的是,用户如果使用blc_smp_configSecurityRequestSending管控安全配对请求包的话,就不要再调用blc_smp_sendSecurityRequest函数。

(4) SMP绑定信息说明

这里讨论的SMP绑定信息是对slave设备来说的。用户可以参考SDK demo “feature_gatt_security/feature_smp_security”初始化中设置direct advertising的代码。

Slave最多可以同时存储4个master的配对信息,这4个设备都可以回连成功。下面接口用于设定当前存储的最多设备数,不能超过4,如果用户不设置的话,默认值也为4。

void        blc_smp_param_setBondingDeviceMaxNumber ( int device_num);

如果设置了blc_smp_param_setBondingDeviceMaxNumber(4),配对4个设备后,一旦配对第5个,就会自动将最老的那个(第1个)设备的配对信息删除,然后存储第5个设备的配对信息。

如果设置了blc_smp_param_setBondingDeviceMaxNumber(2),配对2个设备后,一旦配对第3个,就会自动将最老的那个(第1个)设备的配对信息删除,然后存储第3个设备的配对信息。

下面API用于获取当前slave在flash上存储的配对成功的master设备数量。

u8 blc_smp_param_getCurrentBondingDeviceNumber(void);

假设返回值为3,就说明flash上目前存储了3个配对成功的设备,这3个设备都可以回连成功。

1) 绑定信息存储顺序

和BondingDeviceNumber相关的一个概念叫index。如果当前BondingDeviceNumber为1,那么只有1个bonding设备,它的index为0;如果BondingDeviceNumber为2,两个设备的index分别为0和1。

SDK提供了两种index更新顺序方式:1、根据设备最近连接时间的顺序;2、根据设备配对时间的顺序。可以通过如下API设置index更新方式:

void    bls_smp_setIndexUpdateMethod(index_updateMethod_t method);

枚举类型index_updateMethod_t 具体定义介绍如下:

typedef enum {
    Index_Update_by_Pairing_Order = 0,     //default value
    Index_Update_by_Connect_Order = 1,
} index_updateMethod_t;

下面分别介绍两种index更新方式:

A. 根据设备最近连接时间的顺序(Index_Update_by_Connect_Order)

如果BondingDeviceNumber为2,两个设备的index分别为0和1。Index顺序是以最近成功的一次连接为准的,并不是最近一个配对为准。假设slave先和masterA配对成功,再和masterB配对成功,此时slave的flash存储上masterA为index0,masterB为index1,因为masterB是最近的设备。接着让slave和masterA回连一次成功,这时候masterA就成为最近的一个设备,此时index0设备变为masterB,index1设备变为masterA。

如果BondingDeviceNumber为3,三个设备的index分别为0、1、2,0是最老连接的,2是最新连接的。

如果BondingDeviceNumber为4,四个设备的index分别为0、1、2、3,此时3是最新连接的设备。根据上面的描述,若slave连续配对masterA、B、C、D,此时masterD是index3设备,若slave和masterB回连一次,则master B成为最新的index3设备。

由于index是以最近连接时间为顺序的,所以也要注意设备配对超过4的情况:若连续配对masterA、B、C、D,再配对masterE,则slave会删除最老的masterA;但如果在配对masterA、B、C、D后,先和masterA回连一次,此时顺序变为B、C、D、A,再配对masterE 的话slave就会删除masterB的配对信息。

B. 根据设备配对时间的顺序(Index_Update_by_Pairing_Order)

如果BondingDeviceNumber为2,两个设备的index分别为0和1。Index顺序是以配对时间先后为准。假设slave先和masterA配对成功,再和masterB配对成功,此时slave的flash存储上masterA为index0, masterB为index1,接着让slave和masterA回连一次成功,此时index0设备依然为masterA,index1设备为masterB。

如果BondingDeviceNumber为4,四个设备的index分别为0、1、2、3,0是最老配对的设备,3是最新配对的设备。根据上面的描述,若slave连续配对masterA、B、C、D,此时masterD是index3设备,期间不管slave和master A、B、C、D什么顺序回连,index0、1、2、3依然分别对应masterA、B、C、D。

需要注意设备配对超过4的情况:若连续配对masterA、B、C、D,再配对masterE,则slave会删除最老的masterA;如果在配对masterA、B、C、D后,先和masterA回连一次,此时顺序依旧A、B、C、D,再配对masterE的话slave会删除masterA的配对信息。

2) 绑定信息格式及相关API说明

Master设备绑定信息存储在flash上,其格式为:

typedef struct {  
    u8      flag;
    u8      peer_addr_type;  
    u8      peer_addr[6];

    comb_flg_t  cflg_union;
    u8      peer_id_adrType;
    u8      peer_id_addr[6];

    u8      own_ltk[16];
    u8      peer_irk[16];
    u8      local_irk[16];
}smp_param_save_t;

绑定信息共64byte。

  • peer_addr_type和peer_addr是link layer上master的连接地址,设备direct adv时使用这个地址。

  • peer_id_adrType/peer_id_addr和peer_irk是master在key distribution阶段宣称的identity address和irk。

只有当peer_addr_type和peer_addr是可解析的私有地址(resolvable private addr,简称RPA),且用户需要使用地址过滤时,才需要将相关的信息添加到resolving list中,以便slave可以解析出(参考B91m_feature_test中TEST_WHITELIST的用法)。

其他参数user不用关注。

下面API使用index从flash上获取设备的信息。

u32  bls_smp_param_loadByIndex(u8 index, smp_param_save_t* smp_param_load);

返回值为0表示获取信息失败,非0的值为该信息区域flash首地址。比如当前bonding设备为3时,获取最近的那个连接设备的相关信息:

bls_smp_param_loadByIndex(2, ···);

下面接口使用master的地址(link layer上的连接地址)从flash上获取bonding的设备的信息。

u32  bls_smp_param_loadByAddr(u8 addr_type, u8* addr, smp_param_save_t* smp_param_load);

返回值为0表示获取信息失败,非0的值为该信息区域flash首地址。

下面API用于slave设备清除本地flash上存储的所有配对绑定信息:

void    bls_smp_eraseAllParingInformation(void);

需要注意的是,用户调用该API前需要确保设备处于非连接态。

下面API可以用于slave设备配置绑定信息存储在flash中的位置:

void bls_smp_configParingSecurityInfoStorageAddr(int addr);

其中参数addr可以根据实际需要修改,用户配置前可以参考文档中“SDK FLASH空间的分配”章节,决定绑定信息放置在flash中合适的区域。

GAP

(1) GAP初始化

GAP初始化分主从角色,slave使用如下API初始化GAP:

void blc_gap_peripheral_init(void);

由前文我们知道,应用层与host的数据交互不通过GAP来访问控制,协议栈在ATT、SMP和L2CAP都提供了相关接口,可以和应用层直接交互。目前SDK的GAP层主要处理host层上的事件,GAP初始化主要是注册host层事件处理函数入口。

(2) GAP Event

GAP event则是ATT、GATT、SMP、GAP等host协议层交互过程中产生的事件。从前文我们可以知道,目前SDK事件主要分为两大类:Controller event和GAP(host) event,其中controller event又分为HCI event和Telink defined event。

Telink 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_SECURITY_PROCESS_DONE                            4  
#define GAP_EVT_SMP_TK_DISPLAY                                       8
#define GAP_EVT_SMP_TK_REQUEST_PASSKEY                               9
#define GAP_EVT_SMP_TK_REQUEST_OOB                                   10
#define GAP_EVT_SMP_TK_NUMERIC_COMPARE                               11
#define GAP_EVT_ATT_EXCHANGE_MTU                                     16
#define GAP_EVT_GATT_HANDLE_VALUE_CONFIRM                            17

callback函数原型中para和n表示event的数据和数据长度,下文将详细说明以上列出的GAP event。User可参考B91m feature/feature_smp_security/app.c中如下用法以及app_host_event_callback函数的具体实现。

blc_gap_registerHostEventHandler( app_host_event_callback ); 

GAP event需要通过下面的API来打开mask。

void blc_gap_setEventMask(u32 evtMask);

eventMask的定义也对应上面给出的一些,其他的event mask用户可以在stack/ble/host/gap/gap_event.h中查到。

#define GAP_EVT_MASK_NONE                                            0x00000000
#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_SECURITY_PROCESS_DONE                       (1<<GAP_EVT_SMP_SECURITY_PROCESS_DONE)
#define GAP_EVT_MASK_SMP_TK_DISPLAY                                  (1<<GAP_EVT_SMP_TK_DISPLAY)
#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_VALUE_CONFIRM                       (1<<GAP_EVT_GATT_HANDLE_VALUE_CONFIRM)

若user未通过该API设置GAP event mask,那么当GAP相应的event产生时将不会通知应用层。

注意:

以下论述GAP event时,均设定注册了GAP event回调,且开启了对应的eventMask。

1) GAP_EVT_SMP_PAIRING_BEGIN

事件触发条件:当slave和master刚刚连接进入connection state,slave发送SM_Security_Req命令后,master发送SM_Pairing_Req请求开始配对,slave收到这个配对请求命令时,触发该事件,表示配对开始。

master发起Pairing_Req

数据长度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、Numeric_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

事件触发条件:由于slave或master其中一个不符合标准配对流程,或者通信中出现报错等异常原因导致配对流程终止。

数据长度n:3。

回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u8  reason;
} gap_smp_pairingFailEvt_t;

connHandle表示当前连接句柄。

reason表示配对失败的原因,这里列出几个常见的配对失败原因值,其他配对失败原因值我们可以参考SDK目录下的“stack/ble/host/smp/smp.h”文件。

配对失败值具体含义可以参照《Core_v5.0》(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_NUMERIC_FAILED          0x0C
#define PAIRING_FAIL_REASON_PAIRING_TIMEOUT         0x80
#define PAIRING_FAIL_REASON_CONN_DISCONNECT         0x81

4) GAP_EVT_SMP_CONN_ENCRYPTION_DONE

事件触发条件:Link Layer加密完成时(Link Layer收到master发的start encryption response)触发。

数据长度n:3。

回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u8  re_connect;   //1: re_connect, encrypt with previous distributed LTK;   0: paring , encrypt with STK
} gap_smp_connEncDoneEvt_t;

connHandle表示当前连接句柄。

re_connect为1表示快速回连(将使用之前分发的LTK加密链路),若该值为0则表示当前加密是第一次加密。

5) GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE

事件触发条件:第一次配对时,在GAP_EVT_SMP_PAIRING_SUCCESS事件之后触发,快速回连时,在GAP_EVT_SMP_CONN_ENCRYPTION_DONE事件之后触发。

数据长度n:3。

回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u8  re_connect;   //1: re_connect, encrypt with previous distributed LTK;   0: paring , encrypt with STK
} gap_smp_securityProcessDoneEvt_t; 

re_connect为1表示快速回连(将使用之前分发的LTK加密链路),若该值为0则表示当前加密是第一次加密。

6) GAP_EVT_SMP_TK_DISPLAY

事件触发条件:slave收到master发送的Pairing_Req后,根据对端设备的配对参数和本地设备的配对参数配置,我们就可以知道接下来配对使用什么样的TK(pincode)值方式。如果启用的是PK_Resp_Dsply_Init_Input(即:slave端显示6位pincode码,master端负责输入6位pincode码)方式,则会立即触发。

数据长度n:4。

回传指针p:指向一个u32型变量tk_set,该值即为slave需要通知应用层的6位pincode码,应用层需要显示该6位码值。

用户debug时如需不使用底层随机生成的6位pincode码,可以手动设置一个用户指定的pincode码,例如“123456”,采用以下API。

用户需要在/stack/ble/host/smp/smp.h里加上以下声明:

extern void blc_smp_setDefaultPinCode(u32 pinCodeInput);

用户将slave上看到的6位pincode码输入到master设备上(如手机),完成TK输入,配对流程得以继续执行。如果用户输入pincode错误或者点击取消,则配对流程失败。

关于Passkey Entry应用的实例,用户可以参考SDK提供的demo “vendor/B91m_feature/feature_smp_security/app.c”。

case GAP_EVT_SMP_TK_DISPLAY:
{
    char pc[7];
    u32 pinCode = MAKE_U32(para[3], para[2], para[1], para[0]);
    sprintf(pc, "%d", pinCode);
    tlkapi_printf(1,"[APP][SMP]TK display:%d\n", pinCode);
}
break;

7) GAP_EVT_SMP_TK_REQUEST_PASSKEY

事件触发条件:当slave设备启用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。

8) GAP_EVT_SMP_TK_REQUEST_OOB

事件触发条件:当slave设备启用传统配对OOB方式时,会触发该事件,通知用户需要通过OOB方式输入16位TK值。用户在收到该事件后就需要通过IO输入能力输入16位TK值(超时30s如果还未输入则配对失败),输入TK值的API:blc_smp_setTK_by_OOB在“SMP参数配置”章节有说明。

数据长度n:0。

回传指针p:NULL。

9) GAP_EVT_SMP_TK_NUMERIC_COMPARE

事件触发条件:slave收到master发送的Pairing_Req后,根据对端设备的配对参数和本地设备的配对参数配置,我们就可以知道接下来配对使用什么样的TK(pincode)值方式,如果启用的是Numeric_Comparison方式,则会立即触发。(Numeric_Comparison方式即数值比较,属于smp4.2安全加密,master和slave设备均会弹出显示6位pincode码以及“YES”和“NO”对话框,用户需要检查两端设备显示的pincode是否一致,并需要两端分别确认,点击“YES”以确认TK校验通过)。

数据长度n:4。

回传指针p:指向一个u32型变量pinCode,该值即为slave需要通知应用层的6位pincode码,应用层需要显示该6位码值,并提供“YES”和“NO”的确认机制。

关于数值比较应用的实例,用户可以参考SDK提供的demo “vendor/B91m_feature/feature_smp_security/app.c”。

10) GAP_EVT_ATT_EXCHANGE_MTU

事件触发条件:无论是master端发送Exchange MTU Request,slave回复Exchange MTU Response,还是slave端发送Exchange MTU Request,master回复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(ClintRxMTU, ServerRxMTU),ClintRxMTU表示客户端的RX MTU size值,ServerRxMTU表示服务端的RX MTU size值。Master和slave交互了彼此的MTU size后,取两者最小值作为彼此交互的最大MTU size值。

11) GAP_EVT_GATT_HANDLE_VALUE_CONFIRM

事件触发条件:应用层每调用一次blc_gatt_pushHandleValueIndicate,向master发送indicate数据后,master会回复一个confirm,表示对这个数据的确认,slave收到该confirm时触发。

数据长度n:0。

回传指针p:NULL。

低功耗管理(PM)

低功耗管理(Low Power Management)也可以称为功耗管理(Power Management),本文档中会简称为PM。

低功耗驱动

低功耗模式

B91m MCU正常执行程序时处于working mode,此时工作电流在3~7mA之间。如果需要省功耗需要进入低功耗模式。

低功耗模式(low power mode)又称sleep mode,包括3种:suspend mode、deepsleep mode和deepsleep retention mode。各个模式的IC电流详细参考datasheet。

Module suspend deepsleep retention deepsleep
Sram 100% keep first 32K(or 64K) keep, others lost 100% lost
digital register 99% keep 100% lost 100% lost
analog register 100% keep 99% lost 99% lost

上表为3种sleep mode下Sram、数字寄存器(digital register)、模拟寄存器(analog register)状态保存的统计说明。

(1) Suspend mode (sleep mode 1)

此时程序停止运行,类似一个暂停功能。MCU大部分硬件模块断电,PM模块维持正常工作。此时IC电流在几十微安左右。当suspend被唤醒后,程序继续执行。

suspend mode下所有的Sram和analog register都能保存状态。SDK中为了降低功耗,在进入suspend低功耗处理时已经对部分模块设置了掉电模式,这时该模块的digital register也会掉电,唤醒后必须需要重新进行初始化配置,user需要关注的是API rf_set_power_level_index设置的寄存器,本文档前面已经介绍,这个API需要在每次suspend醒来后都重新调用一次。

(2) Deepsleep mode (sleep mode 2)

此时程序停止运行,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。在deepsleep mode下IC电流在几微安左右。如果内置flash的standby电流出现较大的1uA左右,可能导致测量到deepsleep电流稍大一些。deepsleep mode wake_up时,MCU将重新启动,类似于上电的效果,程序会重新开始进行初始化。

Deepsleep mode下,除了analog register上有少数几个register能保存状态,其他所有Sram、digital register、analog register全部掉电丢失。

(3) Deepsleep retention mode (sleep mode 3)

上面的deepsleep mode,电流很低,但是无法存储Sram信息;suspend mode Sram和register可以保持不丢,但是电流偏高。

为了实现一些需要sleep时电流很低又要能够确保sleep唤醒后能立刻恢复状态的应用场景(比如BLE长睡眠维持连接),B91m支持sleep mode 3:deepsleep with Sram retention mode,简称deepsleep retention(或deep retention)。根据Sram retention area的大小不同,B91又分为deepsleep retention 32K Sram和deepsleep retention 64K Sram, B92又分为deepsleep retention 32K Sram、deepsleep retention 64K Sram和deepsleep retention 96K Sram。

Deepsleep retention mode也是一种deepsleep,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。功耗是在deepsleep mode基础上增加retention Sram消耗的电,IC电流会比deepsleep mode大1-2uA。deepsleep mode wake_up时,MCU将重新启动,程序会重新开始进行初始化。

deepsleep retention mode和deepsleep mode 在register状态保存方面表现一致,几乎全部掉电。deepsleep retention mode跟deepsleep mode相比,Sram的前32K(或前64K)可以保持不掉电,剩余的Sram全部掉电。

deepsleep mode和deepsleep retention mode中在analog register部分都有极少数可以保持不掉电,这些不掉电的analog register包括:

a) 控制GPIO模拟上下拉电阻的analog register

通过API gpio_setup_up_down_resistor设置的GPIO模拟上下拉电阻,或者在app_config.h中使用类似如下方式配置的GPIO模拟上下拉电阻,可以保持状态。

#define PULL_WAKEUP_SRC_PD5     PM_PIN_PULLDOWN_100K

使用gpio output属于digital register控制的状态。B91m在suspend期间可以用gpio output来控制一些外围设备,但被切换为deepsleep retention mode后,gpio output状态失效,无法在sleep期间准确的控制外围设备。此时可以使用GPIO模拟上下拉电阻的状态来代替实现:上拉10K代替gpio output high,下拉100K代替gpio output low。

b) PM模块特殊的不掉电analog regsiter

drivers/B91/lib/include/pm.h文件中的DEEP_ANA_REG,如下code所示:

#define PM_ANA_REG_POWER_ON_CLR_BUF1    0x3a // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF2    0x3b // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF3    0x3c // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF4    0x3d // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF5    0x3e // initial value 0x00
#define PM_ANA_REG_POWER_ON_CLR_BUF6    0x3f // initial value 0x0f

以上寄存器只有在掉电时才会恢复初始值,需要注意的是,客户不允许使用ana_39,该模拟寄存器的留给底层stack使用,如果应用层代码有用到该寄存器,需要修改为ana_3a~ana_3f。因为不掉电模拟寄存器数量比较少,建议客户使用其每一个bit指示不同的状态位信息。

#define PM_ANA_REG_POWER_ON_CLR_BUF0    0x39 // initial value 0x00. [Bit0][Bit1] is already occupied. The customer cannot change!

以上0x38寄存器,在硬件/软件reset、掉电和watchdog三种情况下会被初始化,需要注意的是bit0位已经被stack使用,使用者在使用时需要避免该bit位。

使用者可以在sys_init(power_mode_e power_mode)后使用API pm_get_mcu_status(void)的返回值来判断cpu是从哪种状态返回,返回值如下:

typedef enum{
    MCU_STATUS_POWER_ON         = BIT(0),
    MCU_STATUS_REBOOT_BACK      = BIT(2),   //the user will not see the reboot status.
    MCU_STATUS_DEEPRET_BACK     = BIT(3),
    MCU_STATUS_DEEP_BACK        = BIT(4),
    MCU_STATUS_REBOOT_DEEP_BACK = BIT(5),   //reboot + deep
}pm_mcu_status;

低功耗唤醒源

B91m MCU的低功耗唤醒源示意图如下,suspend/deepsleep/deepsleep retention都可以被GPIO PAD和timer唤醒。该 BLE SDK中,只关注2种唤醒源,如下所示(注意code中PM_TIM_RECOVER_START和PM_TIM_RECOVER_END两个定义不是唤醒源):

typedef enum {
     PM_WAKEUP_PAD   = BIT(3),
     PM_WAKEUP_TIMER = BIT(5),
}SleepWakeupSrc_TypeDef;

B91 MCU硬件唤醒源

如上图所示,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()中设置唤醒源即可。cpu_sleep_wakeup是一个函数指针,使用内部32k RC时,用户在main函数中调用blc_pm_select_internal_32k_crystal,使cpu_sleep_wakeup指向cpu_sleep_wakeup_32k_rc;使用外部32k Crystal时,用户在main函数中调用blc_pm_select_external_32k_crystal,使cpu_sleep_wakeup指向cpu_sleep_wakeup_32k_xtal。

  • 唤醒源PM_WAKEUP_PAD来自GPIO模块,除MSPI 4个管脚外所有的GPIO(B91:PAx/PBx/PCx/PDx/PEx,B92:PAx/PBx/PCx/PDx/PEx/PFx)的高低电平都具有唤醒功能。

配置GPIO PAD唤醒sleep mode的API:

typedef enum{
    Level_Low=0,
    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);
#define cpu_set_gpio_wakeup             pm_set_gpio_wakeup

pin为GPIO定义。

pol为唤醒极性定义: Level_High表示高电平唤醒,Level_Low表示低电平唤醒。

en: 1表示enable,0表示disable。

举例说明:

cpu_set_gpio_wakeup (GPIO_PC2, Level_High, 1);  //Enable GPIO_PC2 PAD high level wakeup
cpu_set_gpio_wakeup (GPIO_PC2, Level_High, 0);  //Disable GPIO_PC2 PAD wakeup
cpu_set_gpio_wakeup (GPIO_PB5, Level_Low, 1);  //Enable GPIO_PB5 PAD low level wakeup
cpu_set_gpio_wakeup (GPIO_PB5, Level_Low, 0);  //Disable GPIO_PB5 PAD wakeup

低功耗模式的进入和唤醒

设置MCU进入睡眠和唤醒的API为:

typedef int (*cpu_pm_handler_t)(SleepMode_TypeDef sleep_mode,  SleepWakeupSrc_TypeDef wakeup_src, unsigned int  wakeup_tick);
cpu_pm_handler_t         cpu_sleep_wakeup;

以B91为例对API各个参数进行介绍:

  • sleep_mode:设置sleep mode,有以下几个选择,分别表示suspend mode、deepsleep mode、deepsleep retention 32K Sram、deepsleep retention 64K Sram、deepsleep retention 96K Sram(仅B92支持)。
typedef enum {
    //available mode for customer
    SUSPEND_MODE                        = 0x00,
    DEEPSLEEP_MODE                      = 0x30,
    DEEPSLEEP_MODE_RET_SRAM_LOW32K      = 0x21,  //for boot from sram
    DEEPSLEEP_MODE_RET_SRAM_LOW64K      = 0x03,  //for boot from sram
    DEEPSLEEP_MODE_RET_SRAM = 0x21,
    //not available mode
    DEEPSLEEP_RETENTION_FLAG            = 0x0F,
}SleepMode_TypeDef;
  • 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=234 s,即下面delta_Tick不能超过234 s,若需要更长的睡眠时间,user可以调用长睡眠函数,具体可参考4.2.7章节。

cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER, clock_time() + delta_tick);

返回值为当前sleep mode的唤醒源的集合,该返回值各bit对应表示的唤醒源为:

typedef enum {
    WAKEUP_STATUS_COMPARATOR        = BIT(0),
    WAKEUP_STATUS_TIMER             = BIT(1),
    WAKEUP_STATUS_CORE              = BIT(2),
    WAKEUP_STATUS_PAD               = BIT(3),
    WAKEUP_STATUS_MDEC              = BIT(4),

    STATUS_GPIO_ERR_NO_ENTER_PM     = BIT(7),
    STATUS_ENTER_SUSPEND            = BIT(30),
}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 (SUSPEND_MODE , PM_WAKEUP_TIMER, clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1MS;

程序执行该函数时进入suspend mode,只能被Timer唤醒,唤醒时间为当前时间加上10 ms,所以suspend时间为10 ms。

cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_PAD | PM_WAKEUP_TIMER, 
clock_time() + 50* CLOCK_16M_SYS_TIMER_CLK_1MS);

程序执行该函数时进入suspend模式,可被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唤醒。

cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K , PM_WAKEUP_TIMER, clock_time() + 8* CLOCK_16M_SYS_TIMER_CLK_1S);

程序执行该函数时进入deepsleep retention 32K Sram mode,可被Timer唤醒,唤醒时间为执行该函数的8s后。

cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K , PM_WAKEUP_PAD | PM_WAKEUP_TIMER,clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1S);

程序执行该函数时进入deepsleep retention 32K Sram mode,可被GPIO PAD和Timer唤醒,Timer唤醒时间为执行该函数的10s后。如果在10s结束之前触发了GPIO动作,MCU会被GPIO PAD唤醒;如果10s内无GPIO动作,MCU会被Timer唤醒。

低功耗唤醒后运行流程

当user调用API cpu_sleep_wakeup后,MCU进入sleep mode;当唤醒源触发MCU唤醒后,对于不同的sleep mode,MCU的软件运行流程不一致。

下面详细介绍suspend、deepsleep、deepsleep retention 3种sleep mode被唤醒后的MCU运行流程。请参考下图。

Sleep Mode Wakeup Work Flow

MCU上电(Power on)之后,各流程的介绍:

(1) 运行硬件bootloader(Run hardware bootloader)

MCU硬件上执行一些固定的动作,这些动作固化在硬件上,软件无法修改。

举几个例子说明一下这些动作,比如:芯片上电/deep回来:通过读取flash的boot启动标记“TLNK”,判断当前应该运行的firmware存储地址(偏移地址0x00000/0x20000/0x40000/0x80000),然后跳转到Flash的对应地址(基础地址0x20000000+偏移地址0x00000/0x20000/0x40000/0x80000)开始执行软件bootloader。(可以按照这样处理的原因是B91m系列的芯片支持直接从Flash取指执行。)。

(2) 运行软件bootloader(Run software bootloader)

hardware bootloader运行结束之后,MCU开始运行software bootloader。Software bootloader就是前面介绍过的vector端(对应cstartup_B91.S里面的汇编程序)。

Software bootloader是为了给后面C语言程序的运行设置好内存环境,可以理解为整个内存的初始化。

(3) 系统初始化(System initialization)

System initialization对应main函数中sys_init到user_init之前各硬件模块初始化(包括sys_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”。

对照Figure 4-2 sleep mode wakeup work flow,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电流较小。

(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)不掉电,所以功耗比suspend低很多,仅比deep mode高1-2uA。

deepsleep wake_up后需要把所有的流程重新运行一遍,而deepsleep retention可以跳过"Run hardware bootloader”这一步,这是因为Sram的前32K(64K)上数据是不丢的,不需要再从flash上重新拷贝一次。但由于Sram上retention area有限,"run software bootloader”无法跳过,必须得执行;由于deepsleep retention无法保存寄存器状态,所以system initilization必须执行,寄存器的初始化需要重新设置。deepsleep retention wake_up后的user initilization可以做一些优化改进,和MCU power on/deepsleep wake_up后的user initilization做区分处理,参考本文档后面的介绍。

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 SDK低功耗管理是对BLE Link Layer功耗的管理,请参考本文档前面对Link Layer的介绍。

SDK中暂时只对Advertising state和Connection state Slave role做了低功耗管理,并且提供了一套API供user调用和配置。

对于Scanning state、Initiating state和Connection state Master role,SDK暂时不提供低功耗管理。

对于Idle state,SDK也不提供任何低功耗管理。由于此状态不涉及BLE RF任何动作(即blt_sdk_main_loop函数完全无效),user可以自行调用PM driver去做一些低功耗管理。 下面code为一个简单的demo:当Link Layer处于Idle state时,每个main_loop suspend 10ms。

void main_loop (void)
{
    ////////////////////// BLE entry ////////////////////////
    blt_sdk_main_loop();

    ///////////////////// UI entry //////////////////////////
    // add user task
    //////////////////// PM configuration ////////////////////////
    if(blc_ll_getCurrentState() == BLS_LINK_STATE_IDLE ){  //Idle state
        cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER, 
clock_time() + 10*CLOCK_16M_SYS_TIMER_CLK_1MS);
    }
    else{
        blt_pm_proc();  //BLE Adv & Conn state 
    }
}

当Link Layer处于Advertising state或Conn state Slave role时,下图所示为sleep mode的时序。

注意:

图中Conn state Slave role为connection latency = 0的情况。

Sleep Timing for Advertising State and Conn State Slave Role

(1) 处于Advertising state时,每个Adv Interval里,Adv Event时间是必须的,除去UI task所占用的时间,剩余时间MCU可以进入sleep mode (suspend/ deepsleep retention)。

图中,第一个Adv interval上Adv event开始的时间我们定义为T_advertising;sleep需要唤醒的时间我们定义为T_wakeup,T_wakeup也是下一个Adv interval上Adv event的开始。T_advertising和T_wakeup在本文档后面的介绍中需要使用到。

(2) 处于Conn state Slave role时,每个Conn interval内,brx Event(brx start+brx working+brx post)时间是必须的,除去UI task占用的时间,剩余时间MCU可以进入sleep mode (suspend/deepsleep retention)。

图中,第一个Connection interval上Brx event开始的时间我们定义为T_brx;sleep需要唤醒的时间我们定义为T_wakeup,T_wakeup也是下一个Connection interval上Brx event的开始。T_brx和T_wakeup在本文档后面的介绍中需要使用到。

BLE低功耗管理的实质是对上面两个状态的sleep时间进行管理,user可以决定如何使用这些时间:不进入sleep、进入suspend mode或进入deepsleep retention mode。

B91m 的sleep mode分3种:suspend、deepsleep、deepsleep retetnion。

对于sleep mode中的suspend和deepsleep retention,user不需要调用API cpu_sleep_wakeup来实现。SDK根据Link Layer的状态和低功耗模式,在BLE协议栈部分加了一套低功耗管理机制(对应的代码在blt_sdk_main_loop中实现)。user只需要调用相应的API即可对低功耗进行配置。

对于deepsleep,BLE低功耗管理不包括对它的处理,所以user需要手动在应用层调用API cpu_sleep_wakeup来进入deepsleep。手动deepsleep mode的使用,可以参考SDK project “B91m_ble_sample” blt_pm_proc函数中对deepsleep的处理。

下面开始对Advertising state和Connection state Slave role的低功耗管理做详细的介绍。

相关变量

BLE PM软件处理流程部分会出现很多变量,用户有必要了解这些变量。

BLE SDK中定义了“st_ll_pm_t” 的结构体,下面只列出该结构体部分变量(API介绍时需要用到的变量)。

typedef struct {
    u8      suspend_mask;
    u8      wakeup_src;
    u8      deepRet_type;
    ...
}st_ll_pm_t;

在文件ll_pm.c中定义了如下结构体变量。

st_ll_pm_t  bltPm;

注意:

该文件被封装在library中,这里给出定义只是为了方便后面的介绍,用户不允许对这个结构体变量进行任何操作。

下面的介绍中会经常出现类似 “bltPm. suspend_mask” 的变量。

API bls_pm_setSuspendMask

用于配置Link Layer Advertising state和Conn state Slave role低功耗管理的API:

void    bls_pm_setSuspendMask (u8 mask);
u8      bls_pm_getSuspendMask (void);

使用bls_pm_setSuspendMask设置bltPm.suspend_mask(默认值为SUSPEND_DISABLE)。

这两个API的源码为:

void  bls_pm_setSuspendMask (u8 mask)
{
        bltPm.suspend_mask = mask;
}
u8  bls_pm_getSuspendMask (void)
{
        return bltPm.suspend_mask;
}

bltPm.suspend_mask的设置,可以选择下面几个值中的一个,或者选择多个值的“或操作”。

#define         SUSPEND_DISABLE             0
#define         SUSPEND_ADV                 BIT(0)
#define         SUSPEND_CONN                BIT(1)
#define         DEEPSLEEP_RETENTION_ADV     BIT(2)
#define         DEEPSLEEP_RETENTION_CONN    BIT(3)

SUSPEND_DISABLE表示sleep disable,不允许MCU进入suspend和deepsleep retention。

SUSPEND_ADV和DEEPSLEEP_RETENTION_ADV分别用于控制Advertising state时MCU进入suspend和deepsleep retention。

SUSPEND_CONN和DEEPSLEEP_RETENTION_CONN分别用于控制Conn state Slave role时MCU进入suspend和deepsleep retention。

SDK低功耗sleep mode的设计上,deepsleep retention是suspend的替代模式,目的是降低sleep mode的功耗。

以Conn state slave role为例,SDK首先得看到bltPm.suspend_mask中SUSPEND_CONN是否生效,才可以进入suspend。在可以进入suspend的基础上,根据实际情况再结合bltPm.suspend_mask中DEEPSLEEP_RETENTION_CONN是否生效,才能决定此时suspend mode是否被切换为deepsleep retention mode。

所以如果user希望MCU进入suspend,打开SUSPEND_ADV/SUSPEND_CONN即可;如果希望MCU进入deepsleep retention mode,必须同时打开SUSPEND_CONN和DEEPSLEEP_RETENTION_CONN。

该API最常用的3种情况如下:

bls_pm_setSuspendMask(SUSPEND_DISABLE);

MCU不允许进入sleep mode。

bls_pm_setSuspendMask(SUSPEND_ADV | SUSPEND_CONN);

MCU在Advertising state和Conn state Slave role只允许进入suspend,但是不允许进入deepsleep retention。

bls_pm_setSuspendMask(SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV 
                    |SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);

MCU在Advertising state和Conn state Slave role允许进入suspend和deepsleep retention,具体进入哪种sleep mode由当前sleep的时间长度决定,后面会详细介绍。

除了上面3种常用的情况,也可以出现一些特殊的用法,如:

bls_pm_setSuspendMask(SUSPEND_ADV)

只有Advertising state可以进入suspend,Conn state Slave role不允许进入sleep mode。

bls_pm_setSuspendMask(SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN)

只有Conn state Slave role可以进入suspend或deepsleep retention,Advertising state不允许进入sleep mode。

API bls_pm_setWakeupSource

user通过上面的bls_pm_setSuspendMask设置MCU进入sleep mode(suspend或deepsleep retention),通过下面的API可设置sleep mode的唤醒源。

void        bls_pm_setWakeupSource(u8 source);

source可以选择唤醒源PM_WAKEUP_PAD。

该API设置底层变量bltPm.wakeup_src,SDK中源码为:

void    bls_pm_setWakeupSource (u8 src)
{
    bltPm.wakeup_src = src;
}

MCU在Advertising state或Conn state Slave role进入sleep mode,实际的唤醒源为:

bltPm.wakeup_src | PM_WAKEUP_TIMER

即PM_WAKEUP_TIMER是一定会有的,不依赖于user的设定,这是为了保证MCU一定要在特定的时间点唤醒去处理接下来的Adv Event或Brx Event。

每次调用bls_pm_setWakeupSource设置唤醒源后,一旦MCU进入sleep mode被唤醒后,bltPm.wakeup_src会被清0。

API blc_pm_setDeepsleepRetentionType

前面介绍了deepsleep retention根据retention sram size的差别有分为32K sram retention、64K sram retention和96K sram retention(B92)。当sleep mode中deepsleep retention mode生效时,SDK会根据user的设置进入相应的deepsleep retention mode。

下面API供user选择deepsleep retention mode。

void blc_pm_setDeepsleepRetentionType(SleepMode_TypeDef sleep_type);

以B91为例,可选的模式有以下两种:

typedef enum {
        DEEPSLEEP_MODE_RET_SRAM_LOW32K      = 0x21,
        DEEPSLEEP_MODE_RET_SRAM_LOW64K      = 0x03,
}SleepMode_TypeDef;

B91m SDK中默认的deepsleep retention mode为DEEPSLEEP_MODE_RET_SRAM_LOW32K,user如果需要retention 64K sram/retention 96K sram(只B92具有),初始化的时候调用如下code即可。

blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW64K);

注意:

该API的调用必须在blc_ll_initPowerManagement_module之后才能生效。

API cpu_long_sleep_wakeup_32k_rc

该API主要用于设定唤醒CPU的时间点:

int cpu_long_sleep_wakeup_32k_rc(SleepMode_TypeDef sleep_mode,  SleepWakeupSrc_TypeDef wakeup_src, unsigned int  wakeup_tick);

其中该API与cpu_sleep_wakeup_32k_rc的主要区别在于睡眠时长,原函数cpu_sleep_wakeup_32k_rc设置的睡眠时长最大不能超过5分钟,而该函数可支持睡眠时长超过5分钟。调用该函数的前提是断开连接和关闭广播,并且suspend mask和sleep mask都要设为disable。

第一个参数sleep_mode为低功耗模式,一共有四种模式可供选择:

typedef enum {
    SUSPEND_MODE                        = 0x00,
    DEEPSLEEP_MODE                      = 0x30, 
    DEEPSLEEP_MODE_RET_SRAM_LOW32K      = 0x21, 
    DEEPSLEEP_MODE_RET_SRAM_LOW64K      = 0x03,
};

第二个参数wakeup_src为唤醒源,一共有5种唤醒源可供选择:

typedef enum {
     PM_WAKEUP_PAD          = BIT(3),
     PM_WAKEUP_CORE         = BIT(4),
     PM_WAKEUP_TIMER        = BIT(5),
     PM_WAKEUP_COMPARATOR   = BIT(6),
     PM_WAKEUP_MDEC         = BIT(7),
};

第三个参数wakeup_tick为唤醒计数值,其单位为31.25us,当睡眠时长与该计数值相等时唤醒CPU,返回值0代表唤醒成功,否则为失败。

PM软件处理流程

低功耗管理的软件处理流程,下面将使用代码与伪代码相结合的方式来说明,目的是为了让user了解处理流程的所有逻辑细节。

(1) blt_sdk_main_loop

SDK中,blt_sdk_main_loop在一个while(1)的结构中被反复调用。

while(1)
{
    ////////////////////// BLE entry ////////////////////////
    blt_sdk_main_loop();
    ////////////////////// UI entry ////////////////////////
    //UI  task
    ////////////////////// user PM config ////////////////////////
    //blt_pm_proc();
}

blt_sdk_main_loop函数在while(1)中不断被执行,BLE低功耗管理的code在blt_sdk_main_loop函数中,所以低功耗管理的code也是一直在被执行。

下面是blt_sdk_main_loop函数中低功耗管理逻辑的实现。

int blt_sdk_main_loop (void)
{
    ……
    if(bltPm. suspend_mask == SUSPEND_DISABLE) // SUSPEND_DISABLE, can not enter sleep mode
    {
        return  0;
    }

    if( (Link Layer State == Advertising state)  ||  (Link Layer State == Conn state Slave role) )
    {
        if(Link Layer is in Adv Event or Brx Event) //RF is working, can not enter sleep mode
        {
            return  0;
        }
        else
        {
            blt_brx_sleep (); //process sleep & wakeup
        }
    }
    return 0;
}

1) 当bltPm. suspend_mask为SUSPEND_DISABLE时,直接退出,不会执行blt_brx_sleep函数。所以user使用bls_pm_setSuspendMask(SUSPEND_DISABLE)时,低功耗管理的逻辑就会完全失效,MCU不会进入低功耗,while(1)的loop一直在执行。

2) 如果Advertising State的Adv Event或Conn state Slave role的Brx Event正在执行,blt_brx_sleep函数也不会被执行,这是因为此时RF的任务正在运行,SDK需要保证Adv Event/Brx Event结束之后才能进sleep mode。

当以上两个条件都不满足时,才去执行blt_brx_sleep函数。

(2) blt_brx_sleep

blt_brx_sleep函数的逻辑实现如下所示。

void    blt_brx_sleep (void)
{
    if( (Link Layer state == Adv state)&& (bltPm. suspend_mask &SUSPEND_ADV) )
    {   //当前广播状态,允许进suspend
        T_wakeup = T_advertising + advInterval;
        BLT_EV_FLAG_SUSPEND_ENTER event callback execution
        T_sleep = T_wakeup  clock_time();
        if(  bltPm. suspend_mask & DEEPSLEEP_RETENTION_ADV &&   T_sleep  >  bltPm.deepRet_advThresTick )
        {   //suspend is automatically switched to deepsleep retention
            cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_TIMER | bltPm.wakeup_src,T_wakeup); //suspend
            //MCU被唤醒后PC值reset to 0, 将重新执行software bootloader 
        }
        else
        {
            cpu_sleep_wakeup (  SUSPEND_MODE, PM_WAKEUP_TIMER | bltPm.wakeup_src, T_wakeup);
        }
        BLT_EV_FLAG_SUSPEND_EXIT  event callback execution

        if(suspend是被GPIO PAD 唤醒)
        {
            BLT_EV_FLAG_GPIO_EARLY_WAKEUP event callback execution
        }
    }
    else if((Link Layer state == Conn state Slave role)&& (SuspendMask&SUSPEND_CONN) )
    {       //当前Conn state,进suspend
        if(conn_latency != 0)
        {
            latency_use = bls_calculateLatency();
            T_wakeup = T_brx + (latency_use +1) * conn_interval;
        }
        else
        {
            T_wakeup = T_brx + conn_interval;
        }
        BLT_EV_FLAG_SUSPEND_ENTER event callback execution
        T_sleep = T_wakeup  clock_time();
        if(  bltPm. suspend_mask & DEEPSLEEP_RETENTION_CONN && T_sleep  >  bltPm.deepRet_connThresTick )
        {   //suspend is automatically switched to deepsleep retention 
            cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_TIMER | bltPm.wakeup_src,T_wakeup); //suspend
            //MCU被唤醒后PC值reset to 0, 将重新执行software bootloader
        }
        else
        {
            cpu_sleep_wakeup (  SUSPEND_MODE, PM_WAKEUP_TIMER | bltPm.wakeup_src, T_wakeup);
        }

         BLT_EV_FLAG_SUSPEND_EXIT event callback execution
        if(suspend是被GPIO PAD 唤醒)
        {
            BLT_EV_FLAG_GPIO_EARLY_WAKEUP event callback execution
            //调整BLE时序相关的处理
        }
    }
    bltPm.wakeup_src = 0;
    bltPm.user_latency = 0xFFFF;
}

注意:

这里以默认的deepsleep retention 32K Sram来说明。

上面blt_brx_sleep函数的流程看起来比较复杂,我们的分析从最简单的情况开始:首先conn_latency方面只考虑conn_latency =0的情况。其次暂时不考虑deepsleep retention生效的情况,此时只有suspend。当应用层设置suspend mask时使用的是bls_pm_setSuspendMask(SUSPEND_ADV | SUSPEND_CONN)时,就对应这种情况。

结合文档前面介绍的controller event,这里看到几个suspend相关event 回调函数的执行的时机:BLT_EV_FLAG_SUSPEND_ENTER、BLT_EV_FLAG_SUSPEND_EXIT、BLT_EV_FLAG_GPIO_EARLY_WAKEUP。

Link Layer Advertising state时bltPm. suspend_mask中SUSPEND_ADV生效,或者Link Layer Conn state slave role时bltPm. suspend_mask中SUSPEND_CONN生效,可以进入suspend。

在suspend模式下,最终调用了driver中的API cpu_sleep_wakeup:

cpu_sleep_wakeup (SUSPEND_MODE, PM_WAKEUP_TIMER | bltPm.wakeup_src, T_wakeup);

唤醒源为PM_WAKEUP_TIMER | bltPm.wakeup_src,Timer无条件生效,是为了保证MCU一定要在下一个Adv Event、Brx Event到来前唤醒。唤醒时间T_wakeup请参考本文档前面的图"sleep timing for Advertising state & Conn state Slave role”。

blt_brx_sleep函数退出时将bltPm.wakeup_src和bltPm.user_latency的值复位,所以需要注意API bls_pm_setWakeupSource设置唤醒源的生命周期,每次设置的值只对最近一次要进入的sleep mode有效。同理API bls_pm_setManualLatency也一样。

(3) deepsleep retention的详细分析

引入deepsleep retention,对上面的软件处理流程继续分析。

当应用层按如下设置时,deepsleep retention mode被打开。

bls_pm_setSuspendMask(  SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);

(4) API blc_pm_setDeepsleepRetentionThreshold

Advertising state/Conn state slave role中,满足以下条件,suspend才会被自动切换为deep retention:

if(  bltPm. suspend_mask & DEEPSLEEP_RETENTION_ADV &&T_sleep  > bltPm.deepRet_advThresTick )
if(  bltPm. suspend_mask & DEEPSLEEP_RETENTION_CONN &&T_sleep  >  bltPm.deepRet_connThresTick )

第一个条件bltPm. suspend_mask中DEEPSLEEP_RETENTION_ADV需要生效,前面已经介绍过。

第二个条件T_sleep > bltPm.deepRet_advThresTick或T_sleep > bltPm.deepRet_connThresTick中,T_sleep = T_wakeup – clock_time()表示唤醒时间减去实时时间,即sleep的持续时间。这个条件的意义是:当sleep的持续时间超过特定的时间阀值时,MCU的sleep mode才会被切换为deep retention。

我们先介绍两个时间阀值设置的API,如下所示为源码,设置时间单位为ms。

void blc_pm_setDeepsleepRetentionThreshold( u32 adv_thres_ms, u32 conn_thres_ms)
{
    bltPm.deepRet_advThresTick = adv_thres_ms * CLOCK_16M_SYS_TIMER_CLK_1MS;
    bltPm.deepRet_connThresTick = conn_thres_ms * CLOCK_16M_SYS_TIMER_CLK_1MS;
}

API blc_pm_setDeepsleepRetentionThreshold用于设置 suspend切换到deepsleep retention触发条件中的时间阀值,这个设计是为了追求更低的功耗。

参考前文"sleep wake_up后运行流程”部分的说明可知,suspend mode wake_up后,可以立刻回到suspend前的环境继续运行。上面软件流程中在T_wakeup醒来之后,可以立刻开始执行Adv Event/Brx Event任务。

而deepsleep retention wake_up后需要回到"Run software bootloader”开始的地方,和suspend wake_up相比,需要多运行3个步骤(Run software bootloader + System initialization + User initialization),才能再次回到main_loop中执行Adv Event/Brx Event任务。

以Conn state slave role为例,下图表示sleep mode分别为suspend和deepsleepretention时的timing(时序)& power(功耗)对比。

两个相邻的Brx event之间的时间差值T_cycle即当前时间周期。将Brx Event的功耗平均化,等效电流为I_brx,持续时间为t_brx(这里取名t_brx是为了和前面已有概念T_brx做区分)。Suspend的底电流为I_suspend,deep retention的底电流为I_deepRet。

“Run software bootloader + System initialization + User initialization”的过程平均电流等效为I_init,持续的总时间为T_init。实际的应用中,T_init的值需要user去把控和测量,后面会介绍如何实现。

Suspend Deep sleep Retention Timing Power

以下是图中几个名词说明。

  • T_cycle:两个相邻的Brx event之间的时间差值

  • I_brx:将Brx Event的功耗平均化,等效电流为I_brx

  • t_brx:l_brx 持续时间

  • l_suspend:suspend 底电流

  • l_deepRet:deep retention的底电流

  • l_init:Software bootloader + System initialization + User initialization 过程的等效平均电流

  • T_init:l_init 持续的总时间

那么,使用了suspend mode的Brx平均功耗为:

I_avgSuspend = I_brx*t_brx + I_suspend* (T_cycle – t_brx)

由于T_cycle远大于t_brx,(T_cycle – t_brx) 约等于T_cycle。

I_avgSuspend = I_brx*t_brx + I_suspend* T_cycle

使用了deepsleep retention mode的Brx平均功耗为

I_avgDeepRet = I_brx*t_brx + I_init*T_init + I_deepRet* (T_cycle – t_brx)

             = I_brx*t_brx + I_init*T_init + I_ deepRet * T_cycle

比较I_avgSuspend和I_avgDeepRet,去掉相同的“ I_brx*t_brx”,最终比较的部分为:

I_avgSuspend – I_avgDeepRet = I_suspend* T_cycle – I_init*T_init – I_ deepRet * T_cycle

               = T_cycle( (I_suspend – I_ deepRet) – (T_init*I_init)/T_cycle)

对于功耗调试正确的应用程序(硬件电路和软件的功耗调试都正确),最终(I_suspend - I_ deepRet)是一个固定的值,比如suspend为30uA,deepsleep retention为2uA时,(I_suspend - I_ deepRet) = 28uA;(T_init*I_init)最终也是固定的值,比如I_init为3mA,T_init为400 uS,(T_init*I_init)为1200 uA*us

I_avgSuspend – I_avgDeepRet = Tcycle (28 – 1200/Tcycle)

可以看到,当T_cycle的值较小时(例子中T_cycle < 43 ms),使用suspend mode最终的平均功耗更低;当T_cycle较大时(T_cycle > 43 ms),使用deepsleep retention mode最终的平均功耗会更低。

注意,PM软件处理流程部分可以看到,当T_sleep > 43ms的时候才会将suspend自动切换为deepsleep retention。一般我们认为MCU working时间(Brx Event + UI task)比较短,当T_cycle较大时,可以认为T_sleep约等于T_cycle。

那么初始化的时候按照如下设置的话,MCU对于T_sleep大于43mS的suspend自动切换为deepsleep retention,对于T_sleep小于43ms的suspend还是保持不变。

blc_pm_setDeepsleepRetentionThreshold(43, 43);  //43ms为建立在上述条件下的示例时间

以一个 10ms connection interval * (99 + 1) = 1s的长连接为例进行说明:

在Conn state slave role时,由于应用层的任务、手动latency的设置等,会导致MCU suspend时可能出现10ms、20ms、50ms、100ms、1s等时间值。根据43ms的阀值设置,MCU会自动将50ms、100ms、1s等suspend切换为deepsleep retention,而10ms、20ms等suspend还是维持suspend,这样的处理可以保证一个最优的功耗。

由于deepsleep retention的功耗比suspend低,并且“Run software bootloader + System initialization + User initialization” 3个步骤的存在会多出一部分的功耗,根据以上分析,一定是T_cycle大于某个临界值之后,使用deepsleep retention才会更省功耗。上面例子中的数值只是简单的demo,user在实现功耗优化时需要根据一定的方法先测出上面公式中对应的数值,最终才可以确定临界值。

只要程序设计上参考SDK中的demo,且在user initialization这部分没有错误的增加很大的时间消耗,上面的T_cycle临界值都不会太大。一般超过100ms以上的T_cycle,使用deepsleep retention mode的功耗都会更低。

(5) blc_pm_setDeepsleepRetentionEarlyWakeupTiming

参考上面的图“suspend & deepsleep retention timing & power”,可以看到,suspend wake_up的时间点T_wakeup正好是下一个Brx Event开始的时间点,对应BLE master端开始发包的时间。

deepsleep retention wake_up的时间点如果也设置为T_wakeup的话,就会有问题:此时MCU醒来,还需要经过T_init的时间(Run software bootloader + System initialization + User initialization 3个步骤消耗的时间)才能开始Brx Event,已经错过了BLE master端发包的时间点。

为了解决这个问题,MCU wake_up的时间点需要提前到T_wakeup’。

  T_wakeup’ = T_wakeup – T_init

以Conn state slave role为例(Advertising state的处理完全相同),上面PM软件处理流程对blt_brx_sleep函数中deepsleep retention wake_up的时间点进行优化改善,如下:

cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_TIMER | bltPm.wakeup_src,T_wakeup - bltPm.deepRet_earlyWakeupTick);

T_wakeup是BLE stack自动计算的时间点,user只需要知道T_init的值,就可以通过下面API来设置deepsleep retention wake_up的提前量。

void blc_pm_setDeepsleepRetentionEarlyWakeupTiming(u32 earlyWakeup_us)
{
    bltPm.deepRet_earlyWakeupTick = earlyWakeup_us *    CLOCK_16M_SYS_TIMER_CLK_1US;
}

User将测量到的T_init的值直接设置到上面API即可,或者设置的值比T_init略大一点,但不能小于这个值。

(6) T_init的优化和测量

这部分的介绍大量用到ram_code、retention_data、deepsleep retention area等Sram相关的概念,请user先参考Sram空间分配的详细介绍。

1) T_init timing

由图"suspend & deepsleep retention timing & power”,结合上面已有的分析可知,对于T_cycle较大的情况,sleep mode使用deepsleep retention功耗更低,但这种模式下T_init的时间是必须的,无法避开。为了尽量降低长时间睡眠的功耗,需要将T_init的时间尽量优化到最小。I_init的值基本上是保持稳定的,不需要去做优化。

T_init是Run software bootloader + System initialization + User initialization 3个步骤消耗的时间总和,将这3个步骤拆开分析,先定义各步骤的时间。

  • T_cstatup为Run software bootloader的时间(Run software bootloader就是执行汇编文件cstartup_xxx.S)。

  • T_sysInit为执行system initialization的时间。

  • T_userInit为执行user initialization的时间。

T_init = T_cstatup + T_sysInit + T_userInit

下图为T_init的时序图。

T_init Timing

结合前面已有的图和概念继续分析。

T_wakeup是下一个Adv Event/Brx Event开始的时间点,T_wakeup’是MCU提前wake_up的时间。

MCU wake_up后,执行cstatup_xxx.S,然后跳转到main函数,执行System initialization + User initialization,之后进入main_loop。一旦进入main_loop便可以处理Adv Event/Brx Event,所以T_userInit结束的时间对应Adv Event/Brx Event开始的时间T_brx/T_advertising。图中irq_enable这个函数是T_userInit和main_loop分界线,和SDK上code是对应的。

SDK上,T_sysInit时间包括sys_init、rf_drv_init、gpio_init、clock_init执行的时间,这些时间SDK已经做了优化,运行时间达到最低。所以T_sysInit是一个固定的值,user不用关注这个时间。SDK优化这部分时间的方法是将这些初始化函数全部放到ram_code中运行。

下面只对SDK中T_cstatup和T_userInit进行详细的介绍。

2) T_userInit

由前面的介绍可知,user initialization在power on、deepsleep wake_up、deepsleep retention wake_up的时候都需要被执行。

对于没有使用deepsleep retention mode的应用来说,user initialization不需要区分deepsleep retention wake_up和power on/deepsleep wake_up。

void user_init(void);

对于B91m中使用了deepsleep retention mode的应用,为了尽量降低功耗,T_userInit需要最优化。所以需要做区分,deepsleep retention wake_up时,user initialization需要尽量快。 user_init函数中所有的initialization可以被分为两类:一是硬件寄存器的初始化,二是Sram上逻辑变量的初始化。

根据deepsleep retention mode可以保持Sram前32K(64K)不掉电的特性,我们可以将逻辑变量定义成retention_data,这样的话deepsleep retention wake_up时就可以省掉逻辑变量初始化的时间。由于寄存器的状态无法被保持,deepsleep retention wake_up时寄存器的初始化必须重新执行。

最终的实现方法是deepsleep retention wake_up时,执行优化过的user_init_deepRetn函数,power on和deepsleep wake_up时执行user_init_normal。如下code所示:

int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();
if( deepRetWakeUp ){
    user_init_deepRetn ();
}
else{
    user_init_normal ();
}

user可以对比一下这两个函数的实现,下面是SDK demo “B91m_ble_sample” user_init_deepRetn函数的实现。

_attribute_ram_code_ void user_init_deepRetn(void)
{
#if (PM_DEEPSLEEP_RETENTION_ENABLE)
    blc_ll_initBasicMCU();   //mandatory
    rf_set_power_level_index (MY_RF_POWER_INDEX);
    blc_ll_recoverDeepRetention();

    irq_enable();
    ...
#endif
}

前3句(blc_ll_initBasicMCU到blc_ll_recoverDeepRetention();)是BLE初始化中必不可少的相关硬件寄存器的初始化;

blc_ll_recoverDeepRetention是对Link Layer相关软硬件状态的恢复,由stack底层处理。

以上几个初始化都属于固定写法,user不要去修改。

SDK demo “B91m_ble_sample”中GPIO的唤醒设置、Led灯状态的设置都属于硬件初始化。SDK demo “B91m_module”中UART硬件寄存器的状态都需要重新初始化。

在SDK demo基础上,user initialization如果增加了其他功能,这些新增功能的initialization节省时间的原则是:对每一条initialization的code进行分析,判断出是纯Sram变量还是硬件寄存器的操作。

  • 如果是纯Sram变量的操作,将相应的Sram变量添加关键字“attribute_data_retention”定义到 “retention_data”段,就可以保证deepsleep retention wake_up后不需要重新初始化,只需要该操作放到user_init_normal函数中就行了。

  • 如果是硬件寄存器的操作,那么必须放到user_init_deepRetn函数中,确保硬件状态的正确性。

deepsleep retention wake_up 后的T_userInit就是user_init_deepRetn函数的执行时间,SDK中也尽量将这部分的函数放到ram_code中以节省运行时间。

在deepsleep retention area空间足够的前提下,user也需要将增加的硬件初始化相关函数放到ram_code中。

3) T_cstartup

T_cstartup是执行cstartup_xxx.S(比如cstartup_B91.S)所消耗的时间,请user参考SDK中cstartup_B91.S文件。

T_cstartup按照时间顺序可以被拆成4个时间组成:

T_cstartup = T_cs_1 + T_cs_bss + T_cs_data + T_cs_2

T_cs_1和T_cs_2两个时间是固定的,user无法修改它们,不需要去关注。

T_cs_data是Sram中“data”段的初始化时间。“data”段是已初始化的全局变量,它们的初始值存储在flash的“data initial value”区域上。“data”段初始化的时间就是MCU从flash “data initial value”区域上的初值拷贝到Sram “data”段的过程。对应的汇编code如下:

/* Move Data from flash to sram */
_DATA_INIT:
    la     t1, _DATA_LMA_START
    la     t2, _DATA_VMA_START
    la     t3, _DATA_VMA_END
_DATA_INIT_BEGIN:
    bleu   t3, t2, _ZERO_BSS
    lw     t0, 0(t1)
    sw     t0, 0(t2)
    addi   t1, t1, 4
    addi   t2, t2, 4
    j      _DATA_INIT_BEGIN

因为flash数据拷贝速度相对比较慢(给个参考:16 byte数据大概需要7uS时间),如果“data”段的数据较多,就会造成T_cs_data时间偏大,最终导致T_init偏大。

SDK中“data”段数据越少越好。user可以参考文档前面介绍的方法去查看list文件中”data”段的大小。

如果“data”段较大,优化方法为:在deepsleep retention area空间足够的前提下,将原先属于“data”段的变量加关键字“attribute_data_retention”定义到“retention_data”段上。

T_cs_bss是Sram中“bss”段的初始化时间。“bss”段的初值为0,不需要从flash上去拷贝data,只需要将”bss”段对应的Sram全部清0即可。下面为”bss”段 Sram清0操作对应的汇编code:

/* Zero .bss section in sram */
_ZERO_BSS:
    lui    t0, 0
    la     t2, _BSS_VMA_START
    la     t3, _BSS_VMA_END
_ZERO_BSS_BEGIN:
    bleu   t3, t2, _ZERO_AES
    sw     t0, 0(t2)
    addi   t2, t2, 4
    j      _ZERO_BSS_BEGIN

T_cs_bss是“bss”段数据清0操作的时间,每个word (4 byte)清0的速度非常快,当“bss”较小时,T_cs_bss很小。但如果“bss”段很大(比如程序中定义了一个很大的全局数组 int AAA[2000] = {0}),T_cs_bss的时间也会变大很多,所以user还是需要注意,可以在list文件中查看“bss”段的大小。

如果“bss”段偏大,需要优化T_cs_bss。优化方法和“data”段一样,在deepsleep retention area空间足够的前提下,将原先属于“bss”段的变量加关键字“attribute_data_retention”定义到“retention_data”段上。

4) T_init测量

根据以上介绍,对T_cstartup和T_userInit的时间做优化,将T_init优化到最小时间,然后需要测量出T_init,填到API blc_pm_setDeepsleepRetentionEarlyWakeupTiming。

T_init的起点即T_cstartup的起点。T_cstartup的起点是cstartup_B91.S文件中“__START”这个点,如下code所示。

_START:
#if 0
    // add debug, PB4 output 1
    lui     t0,0x80140          //0x8014030a
    li      t1, 0xef
    li      t2, 0x10
    sb      t1 , 0x30a(t0)       //0x8014030a  PB oen     =  0xef
    sb      t2 , 0x30b(t0)       //0x8014030b  PB output  =  0x10
#endif  

结合图“ T_init timing”中Debug gpio的示意,在“__start”这里放了Debug GPIO PB4输出高的操作,user只要将“#if 0”改成“#if 1”便可打开PB4输出高的操作。

T_cstartup结束时间是下面“tjl main”的时间点。

_MAIN_FUNC:
    nop
    la     t0, main
    jalr   t0
    nop
    nop
    nop
    nop
    nop
_END:

那么main函数开始的时间与T_cstartup结束的时间几乎是相等的。在main函数一开始的地方让PB4输出低,如下所示。注意这个DBG_CHN0_LOW依赖于app_config.h中“ DEBUG_GPIO_ENABLE”的打开。

_attribute_ram_code_ int main (void)    //must run in ramcode
{
    DBG_CHN0_LOW;   //debug
    sys_init();
    ……
}

PB4的一高一低可以测量出T_cstartup的持续时间。

在user_init_deepRetn函数里面T_userInit结束地方添加PB4输出高的操作,这样就可以实现图中Debug gpio的效果。User可以通过示波器、逻辑分析仪等设备测量出T_init、T_cstartup的时间。User在理解GPIO操作的基础上,可根据自己的需要,对Debug gpio的code进行修改,以得到更多时间参数测量的结果,如T_sysInit、T_userInit等。

Connection Latency

(1) connection latency生效时的Sleep时序

前面关于Conn state slave role的sleep mode的介绍(参考图“sleep timing for Advertising state & Conn state Slave role”所示),都是基于connection latency(简称conn_latency)没有生效时的前提。

PM软件处理流程上,T_wakeup = T_brx + conn_interval,对应的code如下。

if(conn_latency != 0)
    {   
        latency_use = bls_calculateLatency();   
        T_wakeup = T_brx + (latency_use +1) * conn_interval;
    }
    else
    {   
        T_wakeup = T_brx + conn_interval;
    }

当BLE slave经过 connection parameters update(连接参数更新)流程,conn_latency生效后,sleep wake_up的时间为

T_wakeup = T_brx + (latency_use +1) * conn_interval;

下图所示为一个conn_latency生效时的sleep时序,此时latency_use= 2。

Sleep Timing for Valid Conn_latency

conn_latency没有生效时,sleep的时间最长不超过1个connection interval (一般都比较小)。由于conn_latency的生效,sleep的时间可能会出现一个比较大的值,如1s、2s等,系统功耗可以变得非常低。长sleep期间使用功耗更小的deepsleep retention mode才变得有意义。

(2) latency_use的计算

当conn_latency生效时,T_wakeup的值是由latency_use决定的。说明latency_use并不是直接等于conn_latency。

在latency_use计算中,涉及到一个user_latency,这个是user可以设置的值,调用的API及其源码为:

void bls_pm_setManualLatency(u16 latency)
{
    bltPm.user_latency = latency;
}

bltPm.user_latency这个变量的初值为0xFFFF。注意PM软件处理流程中blt_brx_sleep函数最后会强制将它再次复位为0xFFFF,说明API bls_pm_setManualLatency设置的user_latency只对最近一次sleep管用,每次不同的sleep都需要重新设置。

latency_use的计算过程如下。

首先计算system latency:

1) 若当前连接参数中connection latency为0,system latency为0。

2) 若当前连接参数中connection latency非0:

  • 若当前系统还有一些任务没有处理完,必须在下一个connection interval醒来收发包继续处理(比如还有数据没有发送完、收到master的数据还没处理完等等),system latency为0。

  • 若当前系统已经没有任务需要处理了,则system latency等于connection latency。但是有一个例外,如果收到了master的update map request或update connection parameter request且实际的更新时间点在(connection latency+1)个interval之前,则实际的system latency会强制MCU在实际更新时间点之前那个interval醒来,确保BLE时序的正确。

然后

latency_use = min(system latency, user_latency)

即latency_use取system latency和user_latency中的较小值。

以上逻辑可以看出:如果user调用API bls_pm_setManualLatency设置的user_latency比system latency小,user_latency将会作为最终的latency_use,否则system latency将作为最终的latency_use。

API bls_pm_getSystemWakeupTick

下面的API用于获取低功耗管理计算的suspend醒来的时间点(System Timer tick),即T_wakeup。

u32 bls_pm_getSystemWakeupTick(void);

从PM软件处理流程blt_brx_sleep函数中可以看到,T_wakeup的计算比较晚,已经很接近cpu_sleep_wakeup函数了,应用层只能在BLT_EV_FLAG_SUSPEND_ENTER事件回调函数里才能得到准确的T_wakeup。

下面以按键扫描的应用为例,说明BLT_EV_FLAG_SUSPEND_ENTER事件回调函数和bls_pm_getSystemWakeupTick的用法。

bls_app_registerEventCallback(BLT_EV_FLAG_SUSPEND_ENTER, &ble_remote_set_sleep_wakeup);             
void ble_remote_set_sleep_wakeup (u8 e, u8 *p, int n)
{
    if( blc_ll_getCurrentState() == BLS_LINK_STATE_CONN &&              ((u32)(bls_pm_getSystemWakeupTick() - clock_time())) >
                80 * CLOCK_SYS_CLOCK_1MS){
        bls_pm_setWakeupSource(PM_WAKEUP_PAD);
    }
}

以上这个回调函数要实现的作用是防止按键丢失。

一个正常的人为机械按键动作大概会持续几百毫秒,按的快的时候也会有一两百毫秒。当user通过bls_pm_setSuspendMask设置了Advertising state和Conn state都要进入sleep mode,在conn_latency没有生效的前提下,只要Adv interval和conn_interval的值不是特别大(一般设置在100ms以内),sleep的时间不会超过Adv interval和conn_interval,能够确保按键扫描的频率,就不会丢键。此时不设置GPIO唤醒,不让按键动作唤醒MCU。

但是当conn_latency生效后(比如conn_interval为10ms,conn_latency为99),可能某次Conn state时的sleep会持续1s。这个过程中按键可能会丢掉。在BLT_EV_FLAG_SUSPEND_ENTER回调里判断如果当前状态为Conn state,并且当前要进入的suspend的唤醒时间点距离当前时间大于80 ms,那么将GPIO PAD的唤醒添加进去。如果timer的唤醒时间点还没到,有按键按下导致GPIO上电平发生变化,触发MCU提前唤醒,去处理按键的扫描任务,按键就不会丢失。

GPIO唤醒的注意事项

唤醒电平有效时无法进入sleep mode

由于B91m的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”开始重新运行。

针对以上问题,在SDK demo中,做了相应的处理。

在BLT_EV_FLAG_SUSPEND_ENTER中设置了只有当suspend时间超过某个时间时,才会开启GPIO PAD唤醒。

void ble_remote_set_sleep_wakeup (u8 e, u8 *p, int n)
{
    if( blc_ll_getCurrentState() == BLS_LINK_STATE_CONN && ((u32)(bls_pm_getSystemWakeupTick() - clock_time())) >
                80 * CLOCK_SYS_CLOCK_1MS){
        bls_pm_setWakeupSource(PM_WAKEUP_PAD);
    }
}

当按键没有释放时,通过手动设置latency为0或者一个很小的值,使得sleep时间较短,确保sleep时间不会超过80ms,那么就不会发生按键按着的时候(drive pin上有高电平)开启了GPIO PAD高电平唤醒。如下代码所示。

低功耗代码

MCU进入deepsleep 的两种情况:

  • 一是连续60s没有任何事件会进入deepsleep,这里的事件包括按键被按下,所以此时不会有drive pin 高电平导致deepsleep无法进入;

  • 二是卡键60s后进入deepsleep,这时候虽然有drive pin上的高电平,SDK会将卡键的drive pin唤醒电平极性取反,设为低电平唤醒,同样避免了这个问题(参考按键扫描章节的卡键处理)。

BLE系统低功耗管理参考

在了解了该 BLE SDK低功耗管理的实现原理基础上,user可以很灵活地配置自己的低功耗管理,请参考SDK demo “B91m ble sample” 低功耗管理的参考code。下面做一些解释。

在main_loop的PM configuration部分添加blt_pm_proc函数,注意这个函数要放在main_loop的最后面,以保证运行时间上最接近blt_sdk_main_loop。因此blt_pm_proc函数里面低功耗管理的配置,需要根据UI entry部分各种任务的处理情况来做相应设置。

blt_pm_proc函数低功耗管理配置几个要点总结如下:

(1) 某些任务需要关闭sleep mode时,如语音(ui_mic_enable)、红外等任务运行时,设置bltm.suspend_mask为SUSPEND_DISABLE。

(2) Advertising state下连续广播时间达到60s,设置MCU进入deepsleep,唤醒源为GPIO PAD(需要在user initialization部分提前设置按键GPIO PAD)。判断是否广播超过60s的方法是用软件定时器,用变量advertise_begin_tick记录广播开始的System Timer tick。

设置连续60s无广播进入deepsleep的目的是为了省功耗,防止slave设备没有被master连接时还一直在广播。user需要根据自己的需求,对功耗进行评估后,决定如何处理advertising state的时间问题。

(3) Conn state slave role时,所有的按键都已经释放、没有音频任务、LED任务等,超过最近一次有效的任务时间60S以上,设置MCU进入deepsleep,唤醒源为GPIO PAD,并且在deepsleep记忆寄存器DEEP_ANA_REG0标记当前是在连接状态下进入deepsleep(deepsleep唤醒后,可以设置快速广播包尽快跟master连上)。

设置连续60S无有效任务进入deepsleep的目的是为了省功耗。实际只要将维持连接的功耗调到很小,也可以不进入deepsleep。user需要根据自己的需求和功耗状况决定如何实现。

Conn state slave role时要进入deepsleep,先调用bls_ll_terminateConnection向master发送一个TERMINATE命令,等到这个命令被ack后(此时会触发BLT_EV_FLAG_TERMINATE事件回调函数)再进入deepsleep。这样做是为了确保master收到salve主动断连的请求后立刻断开。如果slave没有发送断连请求就进入deepsleep,master仍然处于连接状态并一直尝试去和slave同步,直到connection timeout触发。这个connection timeout时间可能很大(比如20s),如果在20s connection timeout之前slave被唤醒并发广播尝试和master建立连接,由于master还处于上一次的连接状态中,会导致无法立刻和slave建立连接。应用上的体验就是回连速度很慢。

(4) 当有一些任务不能被长时间的sleep破坏时,可以手动设置user_latency为0。如key_not_released、DEVICE_LED_BUSY时调用API bls_pm_setManualLatency将user_latency设为0,那么latency_use就是0,conn_interval为10ms时sleep时间不超过10ms。

(5) 在上面第4步的基础上,手动关闭latency后,每个conn_interval都要醒来,功耗稍微有点高,且按键扫描和LED任务的处理并不需要每个conn_interval都做一次,此时可以再做一些功耗优化。

LONG_PRESS_KEY_POWER_OPTIMIZE为1时,当按键已经稳定后(key_matrix_same_as_last_cnt > 5),可以手动设置latency的值,调用bls_pm_setManualLatency (3)后,sleep的时间不会超过4个conn_interval。 conn_interval为10 ms时,每40 ms醒来一次处理LED和按键扫描。

user在使用这个优化时,需要自行根据conn_interval的值和任务响应时间来评估。

应用层定时唤醒

在Advertising state和Conn state Slave role,不考虑GPIO PAD唤醒前提下,一旦进入sleep mode,只能在BLE SDK计算好的时间点T_wakeup唤醒,user无法在某一个特定的时间点将sleep提前唤醒。为了增加PM的灵活性,SDK增加了应用层定时唤醒的API和它的回调函数。

应用层定时唤醒API:

void  bls_pm_setAppWakeupLowPower(u32 wakeup_tick, u8 enable);

wakeup_tick为定时唤醒的System Timer tick值;

enable为1时打开该唤醒功能,enable为0时关闭。

应用层定时唤醒发生时,执行bls_pm_registerAppWakeupLowPowerCb注册的回调函数,其原型和API如下:

typedef     void (*pm_appWakeupLowPower_callback_t)(int);
void        bls_pm_registerAppWakeupLowPowerCb(pm_appWakeupLowPower_callback_t cb);

以Conn state Slave role为例:

当user使用bls_pm_setAppWakeupLowPower设置了应用层定时唤醒的app_wakeup_tick,SDK在进入sleep前,会检查app_wakeup_tick是否在T_wakeup之前。

  • 如果app_wakeup_tick在T_wakeup之前,如下图所示,就会在app_wakeup_tick触发sleep提前唤醒;

  • 如果app_wakeup_tick在T_wakeup之后,MCU还是会在T_wakeup唤醒。

EarlyWake_upatapp_wakup_tick

低电检测

电池电量检测(battery power detect/check),在Telink BLE SDK和相关文档中也可能出现其他的名字,包括:电池电量检测(battery power detect/check)、低电池检测(low battery detect/check)、低电量检测(low power detect/check)、电池检查(battery detect/check)等。比如SDK中相关文件和函数出现battery_check、battery_detect、battery_power_check等命名。

本文档统一以“低电检测(low battery detect)”这个名称进行说明。

低电检测的重要性

使用电池供电的产品,由于电池电量会逐渐下降,当电压低到一定的值后会引起很多问题:

a) B91m 工作电压的范围为1.8V~4.3V。当电压低于1.8V时,B91m已经无法保证稳定地工作。

b) 当电池电压较低时,由于电源的不稳定,Flash的“write”和“erase”操作可能有出错的风险,造成program firmware和用户数据被异常修改,最终导致产品失效。根据以往的量产经验,我们将这个可能出风险的低压阀值设定为2.0V。

根据上面的描述,使用电池供电的产品,必须设定一个安全电压值(secure voltage),只有当电压高于这个安全电压的时候才允许MCU继续工作;一旦电压低于安全电压,MCU停止运行,需要立刻被shutdown(SDK上使用进入deepsleep mode来实现)。

安全电压也称为报警电压,这个电压值的选取,目前SDK默认使用2.0V。如果user在硬件电路中出现了不合理的设计,导致电源网络稳定性的恶化,安全电压值还需要继续提高,比如2.1V、2.2V等。

对于Telink BLE SDK开发实现的产品,只要使用了电池供电,低电检测都必须是该产品整个生命周期实时运行的任务,以保证产品的稳定性。

低电检测的实现

低电检测需要使用ADC对电源电压进行测量。user请参考文档Data sheet和Driver SDK Developer Handbook相关ADC章节,先对B91m的ADC模块进行必要的了解。

低电检测的实现,结合SDK demo “B91m_ble_sample”给出的实现来说明,参考文件battery_check.h和battery_check.c。

必须确保app_config.h文件中宏“BATT_CHECK_ENABLE”是被打开的,user使用低电检测功能时需要注意。

#define BATT_CHECK_ENABLE                   1

低电检测的注意事项

低电检测是一个基本的ADC采样任务,在实现ADC采样电源电压时,有一些需要注意的问题,说明如下:

(1) 建议使用GPIO输入通道

B91m的采样方式可采用Vbat或GPIO模拟信号输入的方式进行采样,但Vbat通道采样精度较差,对采样精度要求高的场合建议通过外部GPIO方式采样。

  • 3 / 4外部电阻分压器(总阻值400k,无任何电容)

  • 1.2V Vref基准电压

  • 1 / 4的pre_scale预分压系数

  • 采样频率低于48K

可用的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可通过B91m sample的app_config.h中的宏切换GPIO input channel,选择PB7为GPIO input channel,PB7作为普通GPIO功能,初始化时所有状态(ie、oe、output)使用默认状态即可,不做特殊修改。demo中默认选择PB0为GPIO input channel:

    #define GPIO_BAT_DETECT                 GPIO_PB0
    #define PB0_FUNC                        AS_GPIO
    #define PB0_INPUT_ENABLE                0
    #define PB0_DATA_OUT                    0
    #define ADC_INPUT_PIN_CHN               ADC_GPIO_PB0

(2) 只能使用差分模式

虽然B91m ADC input mode同时支持单端模式(Single Ended Mode)和差分模式(Differential Mode),但由于某些特定的原因,Telink规定:只能使用差分模式,单端模式不允许使用。

差分模式的input channel分为positive input channel(正端输入通道)和negative input channel(负端输入通道),被测量的电压值为positive input channel电压减去negative input channel电压得到的电压差。

如果ADC采样的input channel只有1个,使用差分模式时,将当前input channel设置为positive input channel,将GND设为negative input channel。这样二者的电压差和positive input channel电压相等。

SDK中低压检测使用了差分模式,函数接口如下:

adc_set_diff_input(ADC_INPUT_PIN_CHN >> 12, GND);

(3) 不同的ADC任务需要切换

低压检测无法与其他ADC任务同时运行,必须采用切换的方式来实现。

低电检测的单独使用

在SDK demo中,B91m_ble_sample和B91m_module工程中均实现了低电检测功能,user需要在app_config.h中开启低电检测的功能进行使用。

(1) 低电检测初始化

参考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中。

(2) 低电检测处理

在main_loop中,调用user_battery_power_check函数实现低电检测的处理,相关code如下:

_attribute_data_retention_  u8      lowBattDet_enable = 1;
                            u8      adc_hw_initialized = 0;
void battery_set_detect_enable (int en)
{
    lowBattDet_enable = en;

    if(!en){
        adc_hw_initialized = 0;   //need initialized again
    }
}
int battery_get_detect_enable (void)
{
    return lowBattDet_enable;
}
    if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
        lowBattDet_tick = clock_time();
        user_battery_power_check(BAT_DEEP_THRESHOLD_MV);
    }

lowBattDet_enable默认值为1,低电检测是默认允许的,MCU上电后立刻可以开始低电检测。该变量需要设置成retention_data,确保deepsleep retention不能修改它的状态。

只有在其他ADC任务需要抢占ADC使用权时,才能改变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);

(3) 低压报警

user_battery_power_check的参数为阈值电压,单位为mV。根据前文介绍,SDK中默认设置deepsleep为2000mV,在main_loop的低压检测中,当电源电压低于2000mV时,进入deepsleep模式;当电源电压高于2200mV时才会执行唤醒。

“B91m_ble_sample”和“B91m_ble_module”使用进入deepsleep的方式来实现shutdown MCU,并且设置了按键可以唤醒。

“B91m_ble_sample”和“B91m_ble_module”被shutdown后,进入可被唤醒的deepsleep mode。此时如果发生按键唤醒,SDK会在user initialization的时候先快速做一次低电检测,而不是等到main_loop中检测。这样处理的原因是为了避免应用上的错误,举例说明如下:

如果低电报警时LED闪烁已经提示了产品使用者,然后进入deepsleep又被唤醒,从main_loop的处理来看,需要至少500ms的时间才会去做低电检测。在500ms之前,slave的广播包已经发很久了,很可能跟master已经连接上了。这样的话,就出现已经低电报警的设备又继续工作的bug了。

因为这个原因,SDK必须在user initialization的时候就提前做低电检测,必须在这一步就阻止发生上面的情况。所以在user initialization的时候,添加低电检测,SDK中函数接口为:

#if (BATT_CHECK_ENABLE)
    user_battery_power_check(2000);
#endif

在user_battery_power_check函数中采用低电检测的固定值2000mV再提高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切换实现的方法,去自行实现。

Audio

Audio的来源可以是Amic或Dmic。

  • Dmic是直接使用外围audio处理的芯片,将数字信号读到B91m上;

  • Amic需要使用B91m内部的codec模块,对原始的Audio信号进行采样后处理,最终转化为数字信号传输到MCU。

初始化

Amic和低电检测

目前版本暂不支持此功能,未来版本会支持。

AMIC初始化设置

参考SDK demo B91m feature下的“feature_audio”语音处理相关code。

    void ui_enable_mic (int en)
    {
        ui_mic_enable = en;

        #if (BLT_APP_LED_ENABLE)
            device_led_setup(led_cfg[en ? LED_AUDIO_ON : LED_AUDIO_OFF]);
        #endif
        gpio_write(GPIO_LED_BLUE,en);
        if(en){  //audio on
            ///////////////////// AUDIO initialization///////////////////
            #if (MICROPHONE_SELECT == BLE_DMIC_SELECT)  //Dmic config
                audio_dmic_init();
            #else  //Amic config
                audio_amic_init();
            #endif
        }
        else{  //audio off
            #if (MICROPHONE_SELECT == BLE_DMIC_SELECT)  //Dmic config
                audio_mic_off();
            #else  //audio off
                audio_mic_off();
            #endif
        }

        #if (BATT_CHECK_ENABLE)
            battery_set_detect_enable(!en);
        #endif
    }

上面ui_enable_mic函数中,en=1对应Audio任务的开启,en=0对应Audio任务的结束。

Audio开始时,TL_MICBIAS管脚已经在audio_amic_init()中默认配置输出高电平驱动Amic,用户可以直接使用而无需重新配置。

Amic初始化设置为

void audio_amic_init(void)
{
    audio_set_codec_in_path_a_d_gain(CODEC_IN_D_GAIN_20_DB,CODEC_IN_A_GAIN_0_DB);//recommend setting dgain:20db,again 0db
    audio_init(AMIC_IN_TO_BUF_TO_LINE_OUT ,AUDIO_16K,MONO_BIT_16);
    audio_rx_dma_chain_init(DMA2,(u16*)buffer_mic,TL_MIC_BUFFER_SIZE);
}

Audio在工作过程中,通过DMA将codec中的数据源源不断地搬到sram中。audio_rx_dma_chain_init用于配置sram中存放的语音数据的缓存和长度,同时将其配置成循环链表的结构来存放语音数据,用户在自行定义buffer_mic的时候可以参考目前sdk的做法对buffer_mic四字节对齐。

audio_set_codec_in_path_a_d_gain用来配置语音的增益,codec_in_path_digital_gain_e的设置范围为0-43db,用户可以根据需要进行配置。

Buffer_mic的配置在ui_enable_mic函数中处理,相当于每次Audio开始都要重新做一遍,原因是配置的register在sleep时会掉电丢失。

Audio任务结束后,必须关闭codec adc,防止漏电:

audio_codec_adc_power_down ();

语音任务的执行放在main_loop的Audio entry部分。

    if(ui_mic_enable){
        if(audio_start || (audio_stick && clock_time_exceed(audio_stick, 380*1000))){
            audio_start = 1;
            task_audio();
        }
    }
    else{
        audio_start = 0;
    }

DMIC初始化设置

将宏“MICROPHONE_SELECT”设置成BLE_DMIC_SELECT,DMIC初始化配置如下:

void audio_dmic_init()
{
    audio_set_codec_in_path_a_d_gain(CODEC_IN_D_GAIN_20_DB,CODEC_IN_A_GAIN_0_DB);
    audio_set_dmic_pin(DMIC_D4_DAT_D5_CLK);
    audio_init(DMIC_IN_TO_BUF ,AUDIO_16K,MONO_BIT_16);
    audio_rx_dma_chain_init(DMA2,(u16*)&buffer_mic,TL_MIC_BUFFER_SIZE);
}

Mic_buffer和语音增益部分可参考Amic讲解,用户需要通过audio_set_dmic_pin来配置dmic的clk引脚和dat引脚,共有DMIC_GROUPB_B2_DAT_B3_B4_CLK、DMIC_GROUPC_C1_DAT_C2_C3_CLK和DMIC_GROUPD_D4_DAT_D5_D6_CLK这三组引脚供选择使用。

Audio数据处理

Audio数据量和RF传送方法

Amic采样出来的原声数据是pcm格式的,目前demo中提供了sbc,msbc和adpcm三种压缩算法,其中adpcm采用pcm to adpcm算法将其压缩为adpcm格式,压缩率为25%,用以减低BLE RF数据量,master端收到的adpcm格式数据解压缩还原为pcm格式。

Amic采样率为16K*16bit,每秒钟16K个sample,每ms 16个sample,即每ms 16*16bit = 32byte。

每15.5ms,产生15.5*16=248个sample 共496 bytes的原声数据。对这496 bytes进行pcm到adpcm转换:1/4压缩为124 bytes,同时加上4个bytes的头信息,得到128个bytes的数据。

128 bytes的数据,在L2cap层上发送给master,会分成5个packet进行,因为每个包最大长度是27,第一个包必须带7个bytes的l2cap的说明信息:

l2caplen: 2 bytes,chanid:2 bytes,opcode:1 byte,AttHandle:2 bytes

下图所示为空中抓到的RF数据,可以看到第一个包中有7个额外的信息,后面紧跟20 bytes的audio数据,后面的包27 bytes全是audio数据。第一个包只放20 bytes的audio数据,后面4个包由于是分包, 不需要再带l2cap说明信息,每个包可以放27个bytes:20 + 27*4 = 128 bytes。

Audio数据抓包

结合前面BLE模块ATT & GATT部分对Exchange MTU size部分的说明可知,这里audio数据属于128 bytes的长包在slave端进行了分包处理,如果希望peer device(对端设备)收到这些包后能够重新拼装成功,就一定要确定对方peer device的最大ClientRxMTU,只有当ClientRxMTU大于等于128时,slave端的这个128byte长包才能被peer device正确处理。前面3.2.8小节描述的Telink Ble SDK中,当slave端在主函数调用blc_att_setRxMtuSize()设置Rx MTU大小时,若size大于23则会主动进行上报MTU和更新DLE。

audio service在Attribute Table中的描述为:

MIC Service in Attribute Table

图上第2个Attribute是负责audio数据传送的Attribute。在这个Attribute上使用Handle Value Notification将数据发送给master。master收到Handle Value Notification后,可以将连续5个分包对应的Attribute Value数据进行拼包成为128个bytes,对其进行解压缩还原为pcm格式的audio数据。

Audio数据压缩

根据以上说明,在audio_config.h中定义相关的宏:

    #if (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_GATT_TELINK)
        #define ADPCM_PACKET_LEN                128
        #define TL_MIC_ADPCM_UNIT_SIZE          248
        #define TL_MIC_BUFFER_SIZE              992
    #elif (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_GATT_GOOGLE)
        #define ADPCM_PACKET_LEN                136     //(128+6+2)
        #define TL_MIC_ADPCM_UNIT_SIZE          256
        #define TL_MIC_BUFFER_SIZE              1024
    #elif (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB)
        #define ADPCM_PACKET_LEN                120
        #define TL_MIC_ADPCM_UNIT_SIZE          240
        #define TL_MIC_BUFFER_SIZE              960
    #elif (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_HID)
        #define ADPCM_PACKET_LEN                120
        #define TL_MIC_ADPCM_UNIT_SIZE          240
        #define TL_MIC_BUFFER_SIZE              960
    #elif (TL_AUDIO_MODE == TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB)
        #define ADPCM_PACKET_LEN                20
        #define MIC_SHORT_DEC_SIZE              80
        #define TL_MIC_BUFFER_SIZE              320
    #elif (TL_AUDIO_MODE == TL_AUDIO_RCU_SBC_HID)
        #define ADPCM_PACKET_LEN                20
        #define MIC_SHORT_DEC_SIZE              80
        #define TL_MIC_BUFFER_SIZE              320
    #elif (TL_AUDIO_MODE == TL_AUDIO_RCU_MSBC_HID)
        #define ADPCM_PACKET_LEN                57
        #define MIC_SHORT_DEC_SIZE              120
        #define TL_MIC_BUFFER_SIZE              480

每一笔adpcm压缩数据量为248个sample,496个bytes。由于Amic一直在进行采样并把处理过的pcm格式数据放到事先设置好的buffer上(buffer_mic)。将这个buffer设置为能够存储2笔压缩数据,也就是496个sample,以实现数据的缓冲和保存。使用16K采样,496个sample为992个bytes,TL_MIC_BUFFER_SIZE为992。

定义buffer_mic:

s16     buffer_mic[TL_MIC_BUFFER_SIZE>>1] __attribute__((aligned(4)));
audio_rx_dma_chain_init(DMA2,(u16*)buffer_mic,TL_MIC_BUFFER_SIZE);

硬件控制数据填充到buffer_mic的机制说明如下:

Amic采样的数据按照16K的速度匀速放入从buffer_mic地址开始的内存,向后移动,并且最大长度为992,一旦到最大长度,重新回到buffer_mic地址开始放数据。这个过程不对内存上的数据进行任何是否已经被读走的判断,直接覆盖老的数据。向RAM放数据的过程中,维护一个写指针用于记录当前最新的audio数据已经到RAM的哪个地址了。

软件上定义一个buffer_mic_enc,用来存放压缩后的128个bytes的数据,将buffer_mic_enc的number设为4,最多可以缓存4笔压缩后的数据。

int buffer_mic_enc[BUFFER_PACKET_SIZE];

BUFFER_PACKET_SIZE为128,由于int占4个bytes,等同于128*4个signed char。

数据压缩处理

上图所示为数据压缩处理的方法。

buffer_mic自动维护一个硬件写指针,同时在软件上维护一个读指针。

当软件上检测到写指针与读指针中间的差值已经满足248个sample时,就开始调用压缩处理函数,从读指针开始取出248个sample的数据压缩为128个bytes,同时将读指针移到图上新的位置,表示最新的未读的数据从新的位置开始。如此循环往复,不断检测是否是有足够的248个sample的数据,只要达到这个数据量,就开始做压缩处理。

由于248个sample的产生时间为15.5ms,需要保证程序至少15.5 ms才查询一次。由前面的介绍可知,程序在每个main_loop只执行一次task_audio,那么main_loop的时间必须小于15.5 ms才能保证音频数据不丢。在连接状态,main_loop的时间等于connection interval,所以有音频任务的应用,connection interval一定要小于15.5 ms。实际应用中推荐10 ms。

buffer_mic_enc在软件上维护写指针和读指针,当248 sample数据压缩为128 bytes后,将这个128个bytes拷贝到写指针开始的地方,拷贝完之后检查一下这个buffer是否溢出。若溢出,将最老的一笔数据放弃(将读指针向后移动128 bytes即可)。

将压缩后的数据拷贝到BLE RF数据发送缓冲区的方法为:

检查buffer_mic_enc是否为非空(写指针和读指针相等时为空,不等为非空)。若非空,从读指针开始的地址拿出128 bytes拷贝到BLE RF数据发送缓冲区,然后将读指针移到图上所示新的位置。

Audio数据压缩处理对应的函数为proc_mic_encoder,请参考SDK中的实现。

压缩与解压缩算法

B91m单连接SDK中提供了sbc、msbc和adpcm压缩与解压缩算法,下面主要以adpcm来讲解整个压缩与解压缩算法,关于sbc和msbc,user可参考工程实现进行理解。

adpcm压缩算法调用的函数为:

void mic_to_adpcm_split (signed short *ps, int len, signed short *pds, int start);
  • ps指向压缩前数据内存的首地址,对应上图中buffer_mic的读指针的位置。

  • len取TL_MIC_ADPCM_UNIT_SIZE(248),表示248个sample。

  • pds指向压缩后数据内存的首地址,对应上图中buffer_mic_enc写指针的位置。

压缩算法对应数据

如上图所示:压缩后的数据内存的前两个bytes存predict;第三个byte存predict_idx;第4个byte为当前adpcm格式的audio数据的有效数据量,也就是124;后面的124个bytes由496 bytes的原声数据1/4压缩而来,压缩的具体算法不介绍,只要能根据这个方法对应解压缩即可。

解压缩算法对应函数为:

void adpcm_to_pcm (signed short *ps, signed short *pd, int len);
  • ps指向需要解压缩的数据内存开始的地址,也就是指向128 bytes的adpcm格式数据,这个地址需要user定义buffer,从BLE RF收到的128 bytes数据拷贝到该buffer;

  • pd指向解压缩后还原的496 bytes pcm格式音频数据内存开始的地址,这个需要user定义buffer,播放声音时直接从该buffer拿数据;

  • len与压缩端长度一样,为248。

解压缩的时候,对应上图所示,从前两个bytes读到的数据为predict,第三个byte为predict_idx,第4个为audio数据有效长度124,后面的124 bytes对应转换为496bytes pcm格式audio数据。

Audio数据处理流程

B91m SDK的B91m feature 中的feature_audio工程中包含了多个模式选择,user可以在app_config.h中通过更改宏进行选择,默认为TL_AUDIO_RCU_ADPCM_GATT_TELINK,即Telink自定义的Audio处理,其相关设定如下:

/* Audio MODE:
 * TL_AUDIO_RCU_ADPCM_GATT_TELINK
 * TL_AUDIO_RCU_ADPCM_GATT_GOOGLE
 * TL_AUDIO_RCU_ADPCM_HID
 * TL_AUDIO_RCU_SBC_HID
 * TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB
 * TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB
 * TL_AUDIO_RCU_MSBC_HID
 */
    #define TL_AUDIO_MODE                   TL_AUDIO_RCU_ADPCM_GATT_TELINK

由于其中多个模式流程类似,而且默认的Telink自定义的只是单一的将语音数据压缩进行传输,整个流程相对简单。TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB和TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB两种模式实现功能类似只是编码不同,因此本章接下来主要对其中TL_AUDIO_RCU_ADPCM_GATT_GOOGLE、TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB和TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB三种流程进行说明。需要注意的是B91m SDK中只提供Slave端的demo程序,master的程序user可以参考Vulture BLE SDK中的master_kma_dongle工程,本章讲述的master端相关操作均参考于Vulture BLE SDK。

注意:

若在设置不同模式时,编译提示报错XX函数或变量缺少定义,这是由于语音相关lib库没添加导致的,User在使用TL_AUDIO_RCU_ADPCM_GATT_GOOGLE、TL_AUDIO_RCU_MSBC_HID、 TL_AUDIO_RCU_SBC_HID,分别需要添加对应的库文件,其中对应的库文件为下图所示:

Corresponding library files

  • 举例说明,若使用SBC模式,则设置方法如下图所示:

SBC mode setting method

TL_AUDIO_RCU_ADPCM_GATT_GOOGLE

Audio的demo参考Google Voice的V0.4的Spec进行实现,user可以采用该demo与google电视盒子等进行语音相关产品的开发,Google的Service UUID也按照Spec规定进行设定,如下:

Google的Service UUID设定

(1) 初始化

Google语音初始化流程

初始化主要是slave端获取master端的配置信息,整个数据包交互信息如下:

数据包交互信息

(2) 语音数据传输

语音数据传输

在初始化完成后,Slave端会向Master端发送Search_KEY,数据包如下:

Search_KEY packet

接着Slave端会向Master端发送Search,数据包如下:

Search packet

接着Master端会向Slave端发送MIC_Open,数据包如下:

MIC_Open packet

Slave端接着向Master端发送Start,数据包如下:

Start packet

根据Google Voice的Spec,程序中实现的语音数据传输每一帧的数据为134Byte,整个数据包显示如下:

134-byte Audio frame

注意:

在Dongle端没有采用发送close命令结束语音传输,而采用超时判断的方式结束语音。具体可参考Master端dongle实现的相关code.

TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB

该模式采用Service为蓝牙Spec中规定的HID服务,通过该服务可实现与Dongle连接的设备之间的通信,前提是Dongle和上位机设备支持HID服务方式交互。

ADPCM_HID_DONGLE_TO_STB模式下语音数据交互

开始时,Slave向Master发送start_request,数据包如下:

Start_request packet

Master收到start_request后,发送Ack,数据包如下:

Ack packet

Slave开始发送Audio语音数据,语音数据的解压和压缩都是以480Bytes大小进行操作的,语音数据首先经过ADPCM压缩算法压缩为120Bytes,然后拆分成6组数据包依次发送给Master端,每组包大小为20Bytes,为了确保语音包的先后顺序,采用每三组包为固定handle值轮流改变。接收端在完成6组收包后开始进行解压缩还原语音信号。数据包如下:

Audio packet

在准备语音传输结束时,Slave向Master发送End Request,数据包如下:

End request packet

Master在收到End Request后发送Ack,数据包如下:

Ack packet

TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB

该模式和TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB一样,同样采用Service为蓝牙Spec中规定的HID服务,通过该服务可实现与Dongle连接的设备之间的通信,前提是Dongle和上位机设备支持HID服务方式交互。

SBC_HID_DONGLE_TO_STB模式下语音数据交互

开始时,Slave向Master发送start_request,数据包如下:

Start_request packet

Master收到start_request后,发送Ack,数据包如下:

Ack packet

Slave开始发送Audio语音数据,语音数据的解压和压缩都是以160Bytes大小进行操作的,语音数据首先经过SBC压缩算法压缩为20Bytes,然后发送给Master端,每组包大小为20Bytes,为了确保语音包的先后顺序,采用每三组包为固定handle值轮流改变。接收端在每完成1组收包后开始进行解压缩还原语音信号。数据包如下:

Audio packet

在准备语音传输结束时,Slave向Master发送End Request,数据包如下:

End request packet

Master在收到End Request后发送Ack,数据包如下:

Ack packet

Flash写保护

Flash写保护主要是用于保护Flash中的用户代码、用户数据和用户配置信息被非授权方读取或篡改。

Flash写保护的重要性

当电压较低或电源的不稳定,对Flash进行操作可能存在出错的风险(尤其是“write”和“erase”操作),造成firmware和用户数据被异常修改,最终导致产品失效。根据以往的量产经验,推荐客户将Flash写保护默认开启。客户在使用Flash写保护时,必须设定一个安全保护区域大小,一方面不能影响正常的协议栈存储信息的写入和擦除(如SMP信息存储区域),另一方面需要尽可能的保护到程序和用户数据。

对于Telink BLE SDK开发实现的产品,只要使用了电池供电,Flash写保护最好是该产品整个生命周期实时运行的任务,以保证产品的稳定性和用户数据的安全性。

Flash写保护的实现

Flash写保护需要在电源电压大于安全电压阈值。因此使用Flash写保护功能时也需要将低压检测功能打开。

Flash写保护的实现,结合SDK demo “B91m_ble_sample”给出的实现来说明,参考文件flash_prot.h和flash_prot.c。

必须确保app_config.h文件中宏“APP_FLASH_PROTECTION_ENABLE”是被打开的,user使用Flash写保护功能时需要注意。

#define APP_FLASH_PROTECTION_ENABLE                 1

Flash写保护的使用

在SDK demo中,B91m_ble_sample和B91m_module工程中均实现了Flash写保护功能,user需要在app_config.h中开启Flash写保护功能进行使用。

(1) Flash写保护的初始化

初始化调用app_flash_protection_operation函数,第一个参数传入FLASH_OP_EVT_APP_INITIALIZATION事件表示初始化Flash写保护,初始化不会使用后面两个参数传入0即可,下面是app_flash_protection_operation函数中初始化部分。

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容量为1M,不能将1M区域都上锁,应保留一些Flash区域留给系统数据和用户数据使用。

demo中初始化时会将app_flash_protection_operation函数注册为协议栈里的回调函数,会在OTA过程触发不同事件进行处理,主要是清除旧固件以及写入新固件。

blc_appRegisterStackFlashOperationCallback(app_flash_protection_operation);

(2) 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写保护操作。

OTA

为了实现B91m BLE slave的OTA功能,需要一个设备作为BLE OTA master。

OTA master可以是实际与slave配套使用的蓝牙设备(需要在APP里实现OTA),也可以使用Telink的BLE master kma dongle,下面以Telink的BLE master kma dongle作为ota master来详细介绍OTA,相关code实现也可参考多连接SDK下的feature_ota。

B91m支持Flash多地址启动:除了Flash的首地址地址0x00000,还支持从Flash高地址0x20000(128K)、0x40000(256K)、0x80000(512K)读取firmware运行。本文档以高地址0x20000为例来介绍OTA。

FLASH存储架构

Flash存储架构分为传统架构(B91、B92 Secure Boot功能未使能时)和Secure Boot架构(仅B92 Secure Boot功能使能时)。在两种架构中,如使用启动地址0x20000时,SDK编译出来的firmware size均应不大于128K,即flash的0~0x20000之间的区域存储firmware,但是由于一些特殊的原因,如果使用启动地址为0和0x20000交替OTA升级,其firmware size不得超过124K(高地址空间最后的4KB都不能使用);如果超过124K必须使用启动地位0和0x40000交替升级,此时最大firmware size不得超过252K,如果超过252K必须使用启动地址0和0x80000交替升级,此时最大firmware size不得超过508K。

下面首先介绍下传统架构。

传统存储架构

传统Flash存储结构

(1) ota master将新的firmware2烧写到0x20000~0x40000的区域。

(2) 第1次OTA:

  • slave上电时从flash的0~0x20000区域读程序启动,运行firmware1;

  • firmware1运行,初始化的时候将0x20000~0x40000区域清空,该区域将作为新的firmware的存储区。

  • 启动OTA,master通过RF将firmware2空运到slave的0x20000~0x40000区域。slave reboot重新启动,类似一次断电并重新上电。

(3) 将新的firmware3烧写到ota master的0x20000~0x40000的区域。

(4) 第2次OTA:

  • slave上电时从flash的0x20000~0x40000区域读程序启动,运行firmware2;

  • firmware2运行,初始化的时候将0~0x20000区域清空,该区域将作为新的firmware的存储区。

  • 启动OTA,master通过RF将firmware3空运到slave的0~0x20000区域。slave reboot重新启动,并重新上电。

(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,则软件必须调用如下接口配置了Secure Boot OTA功能,否则无法进行正常的Secure Boot OTA。

ble_sts_t   blc_ota_enableSecureBoot(void);

Secure Boot Flash存储结构

以下以1MB Flash为例介绍,1MB Flash中默认的描述符位置为0xF8000与0xFA000,每个描述符占用8kB空间。

(1) ota master将新的firmware2和描述符分别烧写到0x20000~0x40000、0x60000的区域。

(2) 第1次OTA:

  • slave上电时从flash的描述符0xF8000中的程序地址参数(图中以0xF8000中描述符对应程序地址0x00000为例);

  • 从对应0x00000区域读程序启动,运行firmware1;

  • firmware1运行,初始化的时候将0x20000~0x40000中程序区域和另一描述符区域0xFA000~0xFC000清空,这两个区域将分别作为新firmware和新描述符的存储区。

  • 启动OTA,master通过RF将firmware2、描述符空运到slave的0x20000~0x40000、0xFA000区域。slave reboot重新启动,类似一次断电并重新上电。

(3) 将新的firmware3烧写到ota master的0x20000~0x40000的区域。

(4) 第2次OTA:

  • slave上电时从flash的描述符0xFA000中的程序地址参数(图中以0xFA000中描述符对应程序地址0x20000为例);

  • 从对应0x20000区域读程序启动,运行firmware2;

  • firmware2运行,初始化的时候将0x00000~0x20000中程序区域和另一描述符区域0xFA000~0xFC000清空,这两个区域将分别作为新firmware和新描述符的存储区。

  • 启动OTA,master通过RF将firmware3、描述符空运到slave的0x00000~0x20000、0xF8000区域。slave reboot重新启动,类似一次断电并重新上电。

(5) 后面的OTA过程重复上面(1)~ (4)过程,可理解为(2)代表第2n+1次OTA,(3)代表第2n+2次OTA。

OTA更新流程

以上面的FLASH存储结构为基础,详细说明OTA程序更新的过程。 与Flash存储架构相对应,OTA更新流程分为非Secure Boot流程(B91、B92 Secure Boot功能未使能时)和Secure Boot流程(仅B92 Secure Boot功能使能时)。

下面首先介绍下非Secure Boot流程。

非Secure Boot OTA更新流程

首先介绍一下非Secure Boot中的多地址启动机制(只介绍前两个启动地址0x00000和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) Slave程序正常运行,OTA master上电运行,并与slave建立BLE连接。

(4) 在OTA master端UI触发进入OTA模式(可以是按键、PC 工具写内存等)。OTA master进入OTA模式后,首先需要获取slave OTA service数据Attribute的Attribute Handle的值(可以slave事先和master约定好,也可以通过Read_by_type获取这个handle值)。

(5) OTA master获取了slave OTA service数据Attribute的Attribute Handle值后,获取当前slave FLASH程序的firmware版本号。

注意:

若采用legacy protocol则获取版本号需要user自行实现;若采用extend protocol则获取版本号相关操作已实现。关于legacy与extend protocol的区别user可参考7.3.2小节。

(6) master确定要做OTA更新后,先发一个OTA_start命令通知slave进入OTA模式。

(7) Slave收到OTA start命令后,进入OTA模式,等待master发OTA数据。

(8) Master从0x20000开始的区域读预先存储好的firmware,不间断的向slave发送OTA数据,直至整个firmware都发过去。

(9) Slave接收OTA 数据,向ota_program_offset开始的区域存储。

(10) master端发完所有的OTA数据后,检查这些数据slave是否都正确收到(调用底层BLE的相关函数判断link layer的数据是否都被正确ack)。

(11) master确定所有的OTA数据都被slave正确收到后,发送一个OTA_END命令。

(12) Slave收到OTA_END命令,将新firmware区域偏移地址0x20(即ota_program_offset+0x20)写为0x544C4E4B,将之前老的firmware存储区域偏移地址0x20的地方改写为0x00000000,表示下一次程序启动后将从新的区域搬代码执行。

(13) Slave通过Handle Value Notification将OTA的结果上报给master。

(14) 将Slave reboot,新的firmware生效。

在整个OTA更新过程中,Slave会不断检查是否有错包和丢包,同时也会不断检查是否超时(OTA开始的时候启动一个计时),一旦有错包、丢包或超时,slave会认为更新失败,并向对方发送失败的原因,使用之前的firmware。

以上流程slave端相关操作在SDK上已经实现,user不需要添加任何东西,master端需要额外的程序设计,后面会详细介绍。

Secure Boot OTA更新流程

首先介绍一下Secure Boot中多地址启动机制(以1M 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) Slave程序正常运行,OTA master上电运行,并与slave建立BLE连接。

(4) 在OTA master端UI触发进入OTA模式(可以是按键、PC 工具写内存等)。OTA master进入OTA模式后,首先需要获取slave OTA service数据Attribute的Attribute Handle的值(可以slave事先和master约定好,也可以通过Read_by_type获取这个handle值)。

(5) OTA master获取了slave OTA service数据Attribute的Attribute Handle值后,获取当前slave FLASH程序的firmware版本号。

注意:

若采用legacy protocol则获取版本号需要user自行实现;若采用extend protocol则获取版本号相关操作已实现。关于legacy与extend protocol的区别user可参考7.3.2小节。

(6) master确定要做OTA更新后,先发一个OTA_start命令通知slave进入OTA模式。

(7) Slave收到OTA start命令后,进入OTA模式,等待master发OTA数据。

(8) Master从0x60000开始的区域读预先存储好的描述符,将描述符中的public key和signature发送给slave。

(9) Slave收到public key和signature后,会对public key进行校验,如果校验失败,则退出OTA,如果校验成功将public key、signature、新的firmware运行地址信息等写入到新描述符区域对应位置。

(8) Master从0x20000开始的区域读预先存储好的firmware,不间断的向slave发送OTA数据,直至整个firmware都发过去。

(9) Slave接收OTA 数据,向ota_program_offset开始的区域存储。

(10) master端发完所有的OTA数据后,检查这些数据slave是否都正确收到(调用底层BLE的相关函数判断link layer的数据是否都被正确ack)。

(11) master确定所有的OTA数据都被slave正确收到后,发送一个OTA_END命令。

(12) Slave收到OTA_END命令,对新firmware进行验签,验签通过后将新描述符存储区域偏移地址0x0写为0x544C4E4B,将之前老的描述符存储区域偏移地址0x0的地方改写为0x00000000,表示下一次程序启动后将从新的描述符区域读取执行。

(13) Slave通过Handle Value Notification将OTA的结果上报给master。

(14) 将slave reboot,新的firmware生效。

在整个OTA更新过程中,slave会不断检查是否有错包、丢包、超时(OTA开始的时候启动一个计时),同时也会检查是否公钥错误、验签错误,一旦有错包、丢包、公钥错误、验签错误或超时,slave会认为更新失败,并向对方发送失败的原因,使用之前的firmware。

以上流程slave端相关操作在SDK上已经实现,user不需要添加任何东西,master端需要额外的程序设计,后面会详细介绍。

修改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。这两个值和前文的描述一致,user在设置时需要遵循表7-1启动地址与firmware_size大小的约束关系,如果最大firmware_size发生变化,超过了124K,此时需要将启动地址挪到0x40000(size最大不得超过252K),同理如果firmware_size超过252K,需要将启动地址挪到0x80000(size最大不得超过508K),比如最大firmware size可能到200K user可以调用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的处理

Slave端在Attribute Table中添加OTA的相关内容,其中OTA数据Attribute的att_readwrite_callback_t r和att_readwrite_callback_t w分别设为otaRead和otaWrite,将属性设为Read和Write_without_Rsp(Telink的Master KMA Dongle默认采用Write Command发数据,不需要slave回ack,速度会更快),需要注意master若采用Write Response发数据,需要更改gatt的特性权限为允许slave端响应(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}, 

master向slave发送OTA数据时,实际是向上面第3个Attribute写数据,master需要知道这个Attribute在整个Attribute Table中的Attribute Handle。如果user使用master和slave事先约定好Attribute Handle值的方法,可以直接在master端定义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

该命令为获得slave当前firmware版本号的命令,user若采用OTA Legacy protocol进行OTA升级,可以选择使用,在使用该命令时,可通过slave端预留的回调函数来完成firmware版本号的传递。

void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);

server端在收到CMD_OTA_VERSION命令时会触发该回调函数。

(2) CMD_OTA_START

该命令为OTA升级开始命令,master发这个命令给slave,用来正式启动OTA更新。该命令仅供Legacy Protocol进行使用,user若采用OTA Legacy protocol,则必须使用该命令。

(3) CMD_OTA_END

该命令为结束命令,OTA 中的legacy和extend protocol均采用该命令为结束命令,当master确定所有的OTA数据都被slave正确接收后,发送OTA end命令。为了让slave再次确定已经完全收到了master所有数据(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升级开始命令,master发这个命令给slave,用来正式启动OTA更新。user若采用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 byte。

CMD_data
data (16 octets)

(8) CMD_OTA_RESULT

该命令为OTA结果返回命令,OTA结束后slave会将结果信息发送给master,在整个OTA过程中,无论成功或失败,OTA_result只会上报一次,user可根据返回的结果来判断升级是否成功。

CMD_data -
Result (1 octets) Reserved (16 octets)

Result:OTA结果信息,所有可能的返回结果如下表所示:

Value Type info
0x00 OTA_SUCCESS success
0x01 OTA_DATA_PACKET_SEQ_ERR OTA data packet sequence number error: repeated OTA PDU or lost some OTA PDU
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_COMPARE_ERR the version number to be update is lower than the current version
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_MARK_ERR firmware mark error: not generated by telink's BLE SDK
0x0b OTA_FW_SIZE_ERR firmware size error: no firmware_size; firmware size too small or too big
0x0c OTA_DATA_PACKET_TIMEOUT time interval between two consequent packet exceed a value(user can adjust this value)
0x0d OTA_TIMEOUT OTA flow total timeout
0x0e OTA_FAIL_DUE_TO_CONNECTION _TERMIANTE OTA fail due to current connection terminate(maybe connection timeout or local/peer device terminate connection)
0x0f OTA_MCU_NOT_SUPPORTED MCU does not support this OTA mode
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_SYSTEM_ERR OTA server device system error
0x82 OTA_SECBOOT_FUNC_NOT_ENABLE OTA server device do not enable secure boot function
0x83 OTA_SECBOOT_PUBKEY_SIGN_SEQ_ERR OTA public key & signature sequence number error: repeated or lost
0x84 OTA_SECBOOT_PUBKEY_SIGN_LEN_ERR OTA public key & signature data packet length error
0x85 OTA_SECBOOT_PUBLIC_KEY_ERR OTA client public key not match OTA server device local hash
0x86 OTA_SECBOOT_SIGN_VERIFY_FAIL OTA signature verification fail
0x87 OTA_SECBOOT_WRITE_DESC_FAIL write secure boot descriptor fail
0x88 OTA_SECBOOT_NEW_FW_NOT_MATCH_OLD_FW secure boot function: new firmware not match old firmware
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_NOT_MATCH_OLD_FW firmware encryption function: new firmware not match old firmware
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结构组成:

Master在采用WirteCommand或WriteResponse向Slave端发命令和数据时,ATT层有关的Attribute Handle的值为slave端OTA数据的handle_value。根据Ble Spec L2CAP 层有关PDU format的规范,Attribute Value长度定义为下图中的OTA_DataSize部分。

L2CAP PDU 中对应OTA packet

  • 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长度固定默认大小为16octets
  • BigPDU:OTA PDU 长度可更改范围为16octets – 240 octets,且为16字节整数倍。

OTA_PDU Format

当user采用OTA 中的Extend protocol,支持Big PDU,即可支持长包进行OTA升级操作,减少OTA升级的时长, user可根据需要在client端自定义设置PDU大小。最后两个byte是将前面的Adr_Index和Data进行一个CRC_16计算得到第一个CRC的值,slave收到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 为4octets,则需要添加12octets的0xFF进行对齐;
PDU length 32

b) 当前的PDU length设置为48,最后一包的有效数据PDU 为20octets,则需要添加12octets的0xFF进行对齐;
PDU length 48

c) 当前的PDU length设置为80,最后一包的有效数据PDU 为52octets,则需要添加12octets的0xFF进行对齐。
PDU length 80

  • 关于不同PDU大小对应的抓包记录,user可联系Telink技术支持进行获取。

RF Transfer处理方法

Master端通过L2CAP层的Write Command或Write Request向slave发命令和数据,Spec规定在收到Write Request后必须返回Write Response。关于ATT层有关Write Command和Write Request的介绍,关于其具体组成user可参考Ble Spec或3.3.3.2小节内容。Telink Ble master Dongle默认采用Write Command来发送数据和命令,在该方式下,OTA的数据 transform不检查每一个OTA数据是否被ack,即master通过write command发一个ota 数据后,不在软件上检查对方是否有ack信息回复,只要master端硬件TX buffer缓存的待发送数据未达到一定数量,直接将下一笔数据丢进TX buffer。

下面将分别对Legacy Protocol和Extend Protocol、以及OTA的Version Compare的流程进行介绍,阐述整个RF Transform中Salve和Master的交互过程。下文中图示出现的Server端即为Slave端, Client:即为Master端,后面将不作区分。

OTA Legacy Protocol流程

OTA Legacy兼容Telink上一版的OTA协议,为更好说明Slave端和Master端整个交互过程,下面采用举例说明:

注意:

  • PDU 长度采用默认的16 octets大小,不涉及DLE长包的操作。
  • Firmware compare功能不选择。

具体的的操作流程如下图所示:

OTA Legacy protocol流程

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的交互命令存在部分区别,为更好说明Slave端和Master端整个交互过程,下面采用举例说明:

注意:

  • PDU 长度采用64octets大小,涉及DLE长包的操作。
  • Firmware compare功能不选择。

OTA Extend Protocol流程

由于采用了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流程

在Slave端,Extend和 Legacy Protocol都具有版本比较功能,其中Legacy预留了接口,需要user自行实现,而Extend中已经实现了版本比较的功能,user可以直接使用。

下面将Extend中具有版本比较功能的交互流程进行举例说明:

注意:

  • PDU 长度采用16octets大小,不涉及DLE长包的操作。
  • Firmware compare功能选择 (OTA待升级版本号为0x0001,开启版本比较使能)。

OTA Version Compare流程

在使能版本比较功能后,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交互流程,下面举例说明一下Master和Slave具体的数据交互实现:

注意:

  • OTA Protocol: Legacy Protocol;
  • PDU 长度采用16octets大小,不涉及DLE长包的操作;
  • Master端开启Firmware compare功能。

(1) 检测查询是否有触发进入OTA模式的行为,一旦检测到该行为,进入OTA模式。

(2) master向slave传送OTA命令和数据,需要知道slave端当前OTA数据的Attribute的Attribute Handle值。

若user采用事先约定好的方式,直接定义该值;

若没有事先约定好,采用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}

在master的Read By Type Request中将Type设置为这16个bytes的UUID,slave端回复的Read By Type Rsp中可以查到OTA UUID所在的这个Attribute Handle,如下图所示,master可以查到Attribute Handle的值为0x0031。

master通过Read By Type Request获取OTA的Attribute Handle

(3) 获取slave当前firmware版本号,决定是否要继续做OTA更新(若版本已经最新,不需要更新)。这一步为user自己选择是否要做。该BLE SDK不提供具体的版本号获取办法,user可以自行发挥。目前的 BLE SDK中,Legacy protocol并没有实现版本号的传送。user可以使用write cmd或write response的形式通过OTA version cmd向slave传送一个获取OTA version的请求,但是slave那端在收到OTA version请求的时候只提供一个回调函数,user自己在回调函数里想办法将slave端的版本号传送给master(如手动送一个NOTIFY/INDICATE的数据)。

(4) 启动OTA开始的一个计时,后面要不断检测该计时是否超过30秒(这只是个参考时间,实际根据user测试的正常OTA需要多少时间后再做评估)。

如果超过30秒认为OTA超时失败,因为slave端收到OTA数据后会校验CRC,一旦CRC错误或者出现其他错误(如烧写flash错误),就会认为OTA失败,直接程序重启,此时link layer无法ack master,master端的数据一直发不出去导致超时。

(5) 读取Master 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。

firmware示例-开头部分

firmware示例-结尾部分

(6) 向slave发一个OTA start命令,通知slave进入OTA模式,等待master端的OTA数据,如下图所示。

master发 OTA start

(7) 从Master flash 0x20000区域开始每次读16个byte的firmware,填入OTA data packet,设置对应的adr_index,并计算CRC值,将packet push到TX fifo,一直到firmware size最后一个16 byte为止,将firmware所有的数据全部发送给slave。

数据发送方法如前面介绍,使用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,那么20bytes排列为:

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校验结果。slave在OTA升级过程中会同步计算接收到的整个bin的CRC_32校验值,在结束时会与0xec 0x6e 0xdd 0xa9进行比较。

0xf9 ~0xff 共18个bytes的CRC计算结果为 0xUVWX。

上面的数据如下图所示:

master OTA数据1

master OTA数据2

(8) firmware数据发送完毕后,检查BLE link layer的数据是否已经完全发送出去(因为只有当link layer的数据被slave ack了,才会认为该数据发送成功)。若完全发送出去,master发送一个ota_end命令,通知slave所有数据已发送完毕。

OTA end的packet有效字节设为6个,前两个为0xff02,中间的两个bytes为新的firmware最大的adr_index值(这个是为了让slave端再次确认没有丢掉最后一条或几条OTA数据),最后两个bytes为中间最大的adr_index值的取反,相当于一个简单的校验。OTA end不需要CRC校验。

以上图所示的bin为例,最大的adr_index为0x0cf9,其取反值为0xf306,最终的OTA end包如上图所示。

(9) 检查master端link layer TX fifo是否为空。若为空,说明之前所有的数据和命令都已成功发送出去,即master端的OTA任务已经全部完成。 CRC_16计算函数见本文档后面的“附录1:crc16算法”。

按照前面所述,Slave端在OTA Attribute中直接调用otaWrite和otaRead即可,master端发送过来的write command命令,BLE协议栈会自动解析并最终调用到otaWrite函数进行处理。

在otaWrite函数里对packet 20 byte的数据进行解析,首先判断是OTA CMD还是OTA data,对OTA cmd进行相应的响应,对OTA数据进行CRC校验并烧写到flash对应位置。

slave端OTA相关的操作为:

(1) 收到OTA version命令(OTA_FIRMWARE_VERSION命令):

master要求获得slave firmware版本号,该BLE SDK收到这个命令时,不做处理,只是根据user是否注册了收到version的回调函数,判断是否触发回调函数。

在ota.h中看到注册该回调函数的接口为:

typedef void (*ota_versionCb_t)(void);
void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);

(2) 收到OTA start命令:

此时slave进入OTA模式。

若用户使用blc_ota_registerStartCmdCb函数注册了OTA start时的回调函数,则执行此函数,这个函数的目的是让用户在进入OTA模式后,修改一些参数状态等,比如将PM关掉(使得OTA数据传输更加稳定)。另外slave启动并维护一个slave_adr_index,初值为-1,记录最近一次正确OTA data的adr_index,用于判断整个OTA过程中是否有丢包。一旦丢包,认为OTA失败,退出OTA,上报结果,MCU重启,master端由于收不到slave的ack包,也会由于OTA任务超时使得软件发现OTA失败。

注册OTA start的回调函数:

typedef void (*ota_startCb_t)(void);
void blc_ota_registerOtaStartCmdCb(ota_startCb_t cb);

user需要注册这个回调,以便在OTA start的时候做一些操作,比如配置LED灯的特殊闪烁方式来指示OTA正在进行。

另外slave这端一旦收到OTA start开始OTA后,也会启动两个计时,一个是OTA整个过程完成的超时时间,目前SDK中默认是30s。如果30s之内OTA还没有完成,就认为OTA_TIMEOUT失败。实际user最后需要根据自己的firmware大小(越大越耗时)和master端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;

初始化该变量后,user可调用下面的的超时函数来进行超时判断处理。

void blt_ota_procTimeout(void)

另外一个是receive packet的超时时间,每收到一次OTA数据包都会更新一次,超时时间为5s,即5s内没有收到下一笔数据则认为OTA_RF_PACKET_TIMEOUT失败。

(3) 收到有效的OTA数据(前两bytes为0~0x1000):

这个范围的值表示具体的OTA data。

每次slave收到一个20 byte 的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的完整性,在最后还会对整个firmware进行CRC_32校验,与master发送过来同样方法计算得到的校验值进行比较,不相等的话说明中间有数据出错,认为OTA失败。

(4) 收到OTA end:

检查OTA end包中的adr_max和其取反校验值是否正确。若正确,则adr_max可以用来做double check。double check的时候,判断slave之前收到的master的数据index最大值与该包中的adr_max是否相等。若相等,认为OTA成功,若不等,认为丢掉了最后一笔或几笔数据,OTA不完整。当OTA成功的时候,slave将老的firmware所在地址的flash启动标志设为0,将新的firmware所在地址的flash启动标志设为0x4b,将MCU reboot。

(5) slave 发送OTA结果反馈给master:

slave端一旦启动OTA,不管OTA是成功还是失败,最后slave都会将结果发送给master。如下是OTA成功后slave发送结果信息示例(长度只有3个byte):

slave将OTA成功的结果发送给master

(6) slave提供OTA状态的回调函数:

slave端一旦启动OTA,在OTA成功时会将MCU reboot:

若成功,会在reboot前设置flag告诉MCU再次启动后运行New_firmware;

若OTA失败,会将错误的新程序擦掉后重新启动,还是运行Old_firmware。

在MCU reboot前,根据user是否注册了OTA状态回调函数,来决定是否触发该函数。

以下是相关code:

void blc_ota_registerOtaResultIndicationCb (ota_resIndicateCb_t cb);

设置了回调函数后,回调函数的参数result的enum与OTA上报的结果一样,第一个0是OTA成功,其余是不同的失败原因。

OTA升级成功或失败均会触发该回调函数,实际代码中可以通过该函数的结果返回参数来进行debug,在OTA不成功时,可以读到上面的result后,将MCU用while(1)停住,来了解当前是何种原因导致的OTA 失败。

按键扫描

Telink提供了一套基于行列式扫描的keyscan架构,用于按键扫描处理,user可以直接使用这部分的code,也可以自己去实现。

键盘矩阵

如下图所示,这是一个5*6的Key matrix(键盘矩阵),最多支持30个按键。Row0 ~ Row4是5个drive pin(驱动管脚),用来输出驱动电平;CoL0 ~ CoL5是6个scan pin(扫描管脚),用来扫描当前列上是否有按键被按下。

行列式键盘结构

Telink EVK板上为2*2的键盘矩阵。在实际产品应用中可能需要更多的按键,例如遥控器开关等等,下面以Telink提供遥控器的demo板为例来进行说明。结合上图,对app_config.h中keyscan相关的配置进行详细说明如下。

根据实际的硬件电路,demo板上Row0 ~ Row4为PE2、PB4、PB5、PE1、PE4。CoL0 ~ CoL5为PB1、PB0、PA4、PA0、PE6、PE5。

定义drive pin数组和scan pin数组:

#define KB_DRIVE_PINS   {GPIO_PE2, GPIO_PB4, GPIO_PB5, GPIO_PE1, GPIO_PE4}
#define KB_SCAN_PINS    {GPIO_PB1, GPIO_PB0, GPIO_PA4, GPIO_PA0, GPIO_PE6, GPIO_PE5}

keyscan使用的上下拉电阻都使用GPIO的模拟电阻:drive pin选取下拉100K,scan pin选取上拉10K。那么当没有按键按下时,scan pin作为输入GPIO读到的是被10K上拉的高电平。当扫描开始时,在drive pin上输出低电平,scan pin读到低电平,就表示当前列上有按键按下(注意此时drive pin不是float态,若output没打开,scan pin读到的是100K和10K的分压电平,还是高)。

定义行列式扫描中,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
#define PULL_WAKEUP_SRC_PE2     MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PB4     MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PB5     MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PE1     MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PE4     MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PB1     MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PB0     MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PA4     MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PA0     MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PE6     MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PE5     MATRIX_COL_PULL

由于在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 {\
VK_B,       CR_POWER,       VK_NONE,        VK_C,               CR_HOME,     \
VOICE,       VK_NONE,       VK_NONE,        CR_VOL_UP,          CR_VOL_DN,   \
VK_2,       VK_RIGHT,       CR_VOL_DN,      VK_3,               VK_1,    \
VK_5,       VK_ENTER,       CR_VOL_UP,      VK_6,               VK_4,    \
VK_8,       VK_DOWN,        VK_UP ,         VK_9,               VK_7,    \
VK_0,       CR_BACK,        VK_LEFT,        CR_VOL_MUTE,        CR_MENU,     }

这个keymap的格式和5*6矩阵结构一致,可以对应设置按键按下后的keycode,如按下Row0和CoL0两条线交叉的按键,出来的keycode为VK_B。

在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忽略不看。

c) kb_event.cnt = 2,可能上次kb_event.cnt = 0,变化是两个键同时按下;可能上次kb_event.cnt = 1,一个键被按下时另一个键被按下;可能上次kb_event.cnt = 3,三个键被按下时,其中一个被释放;其他可能性等等。此时kb_event.keycode[0]和kb_event.keycode[1]表示当前被按下的两个键的键值,后面的keycode忽略不看。

user可以每次在key scan前自己将kb_event.keycode清0,这时就可以根据kb_event.keycode来判断是否有按键变化发生,如下所示。

这个示例只是简单的处理单个按键按下的情况,所以当kb_event.keycode[0]非0时,就认为是一个按键被按下,并不去判断是否两个键同时按下或者两个键中的一个释放等复杂的情况。

kb_event.keycode[0] = 0;//clear keycode[0]
int det_key = kb_scan_key (0, 1);
if (det_key)
{
    key_not_released = 1;
    u8 key0 = kb_event.keycode[0];
    if (kb_event.cnt == 2)   //two key press, do  not process
    { 
    } 
    else if(kb_event.cnt == 1)
    {           
        key_buf[2] = key0;
         //send key press
        bls_att_pushNotifyData (HID_NORMAL_KB_REPORT_INPUT_DP_H, key_buf, 8);
}
    else  //key release
    {
        key_not_released = 0;
        key_buf[2] = 0;
        //send key release
        bls_att_pushNotifyData (HID_NORMAL_KB_REPORT_INPUT_DP_H, key_buf, 8);
}
}

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电平,读取列上的电平值,找出按键按下的位置。

对应的代码为:

u32 pressed_matrix[ARRAY_SIZE(drive_pins)] = {0};
kb_scan_row (0, gpio);
for (int i=0; i<=ARRAY_SIZE(drive_pins); i++) {
    u32 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时,才能读出之前缓存的数据。

由于我们的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处理,在app_config.h中配置相关的宏如下。KB_REPEAT_KEY_ENABLE用来打开或关闭repeat key功能,默认这个功能是关闭的。

#define KB_REPEAT_KEY_ENABLE            0
#define KB_REPEAT_KEY_INTERVAL_MS       200
#define KB_REPEAT_KEY_NUM               1
#define KB_MAP_REPEAT                   {VK_1, }

(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 应用举例

repeat key代码的实现这里不介绍,user自行理解,只要在工程上搜索以上四个宏就可以找到所有代码了。

卡键处理

卡键处理(Stuck Key process)指的是当一个遥控器/键盘在不用的时候,用户不小心用一些东西把其中一个键或多个键压住了,比如家里的茶杯/烟灰缸等压住了遥控器。此时正常的keyscan会发现一直有一些按键被按着没有释放,code上若不做相应的卡键处理,就会一直认为是按键按着没有释放,永远进不了deepsleep或其他低功耗状态。

app_config.h中相关的两个宏为:

#define STUCK_KEY_PROCESS_ENABLE        0
#define STUCK_KEY_ENTERDEEP_TIME        60//in s

默认卡键处理是关着的,将STUCK_KEY_PROCESS_ENABLE设为1即打开卡键处理。

STUCK_KEY_ENTERDEEP_TIME定义卡键的时间,设为60s表示当一个或多个按键被按着,状态一直没有改变的连续时间超过60s,就认为是卡键发生了,此时我们会让MCU进入deepsleep。

搜索STUCK_KEY_PROCESS_ENABLE宏,可以在keyboard.c中找到相关代码如下:

#if (STUCK_KEY_PROCESS_ENABLE)
    u8 stuckKeyPress[ARRAY_SIZE(drive_pins)];
#endif

定义一个u8的数组stuckKeyPress[5],用于记录当前按键矩阵上哪一行或多行上有卡键。该值的获取是在key_debounce_filter函数中实现的,请user自行理解。

上层的相关处理为:

kb_event.keycode[0] = 0;
int det_key = kb_scan_key (0, 1);
if (det_key){
    if(kb_event.cnt){  //key press
        stuckKey_keyPressTime = clock_time() | 1;;
    }
    .......
}

对于每一个按键状态的变化,当发现是按键按下时(kb_event.cnt非0),记录这个最近的按键按下状态的时间stuckKey_keyPressTime。

然后在blt_pm_proc中处理如下:

#if (STUCK_KEY_PROCESS_ENABLE)
if(key_not_released && clock_time_exceed(stuckKey_keyPressTime, STUCK_KEY_ENTERDEEP_TIME*1000000)){
    u32 pin[] = KB_DRIVE_PINS;
    for (u8 i = 0; i < ARRAY_SIZE(pin); i ++)
    {
        extern u8 stuckKeyPress[];
        if(stuckKeyPress[i])
               continue;
            cpu_set_gpio_wakeup (pin[i],0,1);       
}

……  if(sendTerminate_before_enterDeep == 1){ //sending Terminate and wait for ack before enter deepsleep
        if(user_task_flg){  //detect key Press again,  can not enter deep now
            sendTerminate_before_enterDeep = 0;
            bls_ll_setAdvEnable(BLC_ADV_ENABLE);   //enable adv again
        }
    }
    else if(sendTerminate_before_enterDeep == 2){  //Terminate OK
        cpu_sleep_wakeup(DEEPSLEEP_MODE, PM_WAKEUP_PAD, 0);  //deepSleep
    }}#endif

判断最近的一次按键被按下的时间是否已经连续超过60s。若超过,则认为是发生了卡键处理,根据底层的stuckKeyPress[]获取发生卡键的所有行号,将这些行原来高电平PAD唤醒deepsleep改为低电平PAD唤醒deepsleep。

修改的原因是本来按键按下时,对应的行上drive pin读到的是10/11 VCC高电平,此时是无法进入deepsleep的,因为已经是高电平了,只要进入deepsleep就会立刻被这个高电平唤醒;修改为低电平唤醒后,可以正常进入deepsleep,且按键被释放时,行上的drive pin的电平变为100K下拉的低电平,释放按键可以唤醒整个MCU。

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、且对于时间误差要求不是特别高的情况。

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任务。

_attribute_no_inline_ void main_loop (void)
{
#if (FEATURE_TEST_MODE == TEST_USER_BLT_SOFT_TIMER)
    blt_soft_timer_process(MAINLOOP_ENTRY);
#endif
    blt_sdk_main_loop();
}

blt_soft_timer_process的参数中type有如下两种情况:0表示在main_loop中查询进入,1表示发生了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 feature中TEST_USER_BLT_SOFT_TIMER。

int gpio_test0(void)
{
    DBG_CHN3_TOGGLE;  
    return 0;
}
int gpio_test1(void)
{
    DBG_CHN4_TOGGLE;
    timer_change_flg = !timer_change_flg;
    if(timer_change_flg){
        return 7000;
    }
    else{
        return 17000;
    }
}
int gpio_test2(void)
{
    DBG_CHN5_TOGGLE;        
    if(clock_time_exceed(0, 5000000)){
        //return  -1;
        blt_soft_timer_delete(&gpio_test2);
    }
    return 0;
}
int gpio_test3(void)
{
    DBG_CHN6_TOGGLE;
    return 0;
}

初始化:

blt_soft_timer_init();
blt_soft_timer_add(&gpio_test0, 23000);
blt_soft_timer_add(&gpio_test1, 7000);
blt_soft_timer_add(&gpio_test2, 13000);
blt_soft_timer_add(&gpio_test3, 27000);

定义了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每27ms toggle一次。

IR

PWM Driver

PWM相关的硬件配置很简单,基本都是直接操作寄存器来实现。操作寄存器的API大都定义在pwm.h中,使用static inline function来实现,这样可以提高运行效率,也节省code size。

PWM ID和管脚

B91m共12路PWM,分别为PWM0 ~ PWM5和PWM0_N ~ PWM5_N。

驱动上定义了6路PWM为:

typedef enum {
        PWM0_ID = 0,
        PWM1_ID,
        PWM2_ID,
        PWM3_ID,
        PWM4_ID,
        PWM5_ID,
}pwm_id;

软件上只设置6路PWM0\~PWM5,另外6路PWM0_N\~PWM5_N是对应PWM的波形取反。比如PWM0_N和PWM0相反,当 PWM0为高时PWM0_N为低,而PWM0为低时PWM0_N为高。所以只要设置了PWM0,PWM0_N就被设置了。同理其他几路也一样。

这12路PWM对应的IC管脚请参考对应芯片的driver SDK demo。

PWM时钟、周期(cycle)和占空比(duty)

这部分内容不作详细介绍,详情请参考对应芯片的data sheet或driver SDK handbook。

PWM波形取反

PWM0 ~ PWM5波形取反:

static inline void pwm_invert_en(pwm_id_e id)

PWM0_N ~ PWM5_N波形取反:

static inline void pwm_n_invert_en(pwm_id_e id)

PWM开启和停止

下面两个API用于开启和停止某一路PWM:

static inline void pwm_start(pwm_en_e en);
static inline void pwm_stop(pwm_en_e en);

PWM模式

PWM共5种模式:Normal mode(也称Continuous mode)、Counting mode、IR mode、IR FIFO mode和IR DMA FIFO mode。如下定义:

typedef enum{
    PWM_NORMAL_MODE         = 0x00,
    PWM_COUNT_MODE          = 0x01,
    PWM_IR_MODE             = 0x03,
    PWM_IR_FIFO_MODE        = 0x07,
    PWM_IR_DMA_FIFO_MODE    = 0x0F,
}pwm_mode_e;

PWM0具有Normal mode、Counting mode、IR mode、IR FIFO mode和IR DMA FIFO mode 所有的5种模式;而PWM1 ~ PWM5都只有normal mode。

也就是说,只有PWM0具有除normal mode外的4种特殊的模式。

PWM脉冲数(pulse number)

如下API用于设置指定的PWM波形中Signal Frame的个数。这里“pulse”和Signal Frames是同一个概念。

static inline void pwm_set_pwm0_pulse_num(unsigned short pulse_num)

Normal mode(Continuous mode)不受Signal Frame个数的限制,所以此API对Normal mode无意义。只有其他4种特殊的mode才有可能用到这个API。

PWM中断

PWM支持的中断设置说明如下(硬件不会自动清除中断标志位,需要软件手工清除):

PWM0:

a) FLD_PWM0_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

b) FLD_PWM0_PNUM_IRQ:每发完一个pulse groups,会产生中断。

c) FLD_PWM0_IR_FIFO_IRQ:当FIFO里面的cfg data小于(不包括等于)设置的值(trigger_level)时,进入中断。

d) FLD_PWM0_IR_DMA_FIFO_IRQ:当FIFO执行完DMA发送的cfg data之后,进入中断。

PWM1:

a) FLD_PWM1_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM2:

a) FLD_PWM2_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM3:

a) FLD_PWM3_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM4:

a) FLD_PWM4_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

PWM5:

a) FLD_PWM5_FRAME_DONE_IRQ:每个signal frame完成,会产生中断。

一个脉冲组(pluse groups)包含几个frame可以通过pwm_set_pwm0_pulse_num函数接口进行配置:

static inline void pwm_set_pwm0_pulse_num(unsigned short pulse_num);

IR FIFO模式trigger_level的值可以通过pwm_set_pwm0_ir_fifo_irq_trig_level函数接口进行配置:

static inline void pwm_set_pwm0_ir_fifo_irq_trig_level(unsigned char trig_level);

IR DMA FIFO mode

IR DMA FIFO模式是将配置数据通过DMA写到FIFO中,其中每个FIFO 2个bytes用来表示一个PWM波形,当DMA data buffer生效后,PWM硬件模块会按照时间顺序将PWM waveform 1、waveform 2、waveform n连续发送出去,当FIFO执行完DMA发送的cfg_data后,触发中断FLD_PWM0_IR_FIFO_IRQ。

(1) DMA FIFO的配置

每个DMA FIFO上,使用2bytes (16 bits)来配置一个PWM waveform。调用下面API返回2 bytes的DMA FIFO数据。

static inline unsigned short pwm_cal_pwm0_ir_fifo_cfg_data(unsigned short pulse_num, unsigned char shadow_en, unsigned char carrier_en)

该API有三个参数:“carrier_en”、“shadow_en”和“pulse_num”,配置出来的PWM waveform是“pulse_num”个PWM Signal Frame的集合。

BIT(15)决定当前PWM waveform的基本单位Signal Frame的格式,对应API中的“carrier_en”:

  • “carrier_en”为1时,Signal Frame中高脉冲是生效的;

  • “carrier_en”为0时,Signal Frame是全0数据,高脉冲无效。

“pulse_num”为当前PWM waveform中Signal Frame的数量。

“shadow_en”可选择为1或0: - “shadow_en”为0时,为PWM normal mode,Signal Frame来自pwm_set_tcmp和pwm_set_tmax的配置; - “shadow_en”为1时,为PWM shadow mode,Signal Frame来自pwm_set_pwm0_tcmp_and_tmax_shadow的配置。

PWM shadow mode的目的是为了增加一组Signal Frame的配置,从而为IR DMA FIFO mode的PWM waveform配置增加更多的灵活性。

(2) 设置DMA FIFO Buffer

DMA FIFO buffer的配置完成后,调用下面的接口,将此buffer的首地址设置到DMA模块,同时设定DMA搬运数据的长度,在设置buffer的时候需要四字节对齐。

void pwm_set_dma_buf(dma_chn_e chn,unsigned int buf_addr,unsigned int len);

(3) IR DMA FIFO Mode的开启与停止

DMA FIFO buffer准备好之后,调用下面API开启PWM waveform的发送:

void pwm_ir_dma_mode_start(dma_chn_e chn);

DMA FIFO buffer上所有PWM waveform发送结束后,PWM模块会自动停止。如果需要在此之前手动停止PWM模块,调用下面API:

void pwm_stop_dma_ir_sending(void);

IR Demo

User可参考Ble SDK demo“vendor / B91m_feature / feature_ir”,将feature_config.h中的宏FEATURE_TEST_MODE设置为TEST_IR。

PWM模式的选择

IR的发送需要在特定的时间切换PWM的输出,对切换时间的准确性要求比较高,误差稍微大一点就会引起IR的错误。

根据本文档BLE部分对Link Layer时序的介绍,Link Layer使用了系统中断来处理brx event(最新的SDK已经将adv event放到main_loop中处理,不再占用系统中断的时间)。如果IR某个切换PWM输出的时间点快要到来时,brx event相关的中断先来了,这个中断会占用MCU的时间,可能会导致PWM输出的切换时间被延迟,IR发生错误,所以IR不能使用PWM Normal mode。

B91m系列芯片支持IR DMA FIFO mode。IR DMA FIFO mode由于存储FIFO可以定义在Sram上,使得FIFO的数量显著提高,可以有效解决上面描述的PWM IR mode的缺陷。

IR DMA FIFO mode可以提前将多组PWM waveform存储在Sram上,一旦启动DMA就不需要软件的参与,这样既可以节省软件频繁处理的时间,又可以防止中断系统上多个中断同时响应导致的PWM waveform被延迟。

由于只有PWM0具有IR DMA FIFO mode,IR的实现只能通过PWM0来实现,硬件电路设计时IR的控制GPIO必须选择PWM0(或者PWM0_N)对应的管脚。

Demo IR协议

SDK上demo IR协议如下图所示。

Demo IR Protocol

IR协议数据包括起始位、数据位,其中数据位又包括客户码和数据码。在IR Demo里使用的脉冲调制时间为

  • 起始位:位宽为13.5ms,脉冲时间9ms
  • 数据位:
    1. Logic “1” 位宽为2.25ms,脉冲时间560us
    2. Logic “0” 位宽为1.12ms,脉冲时间560us
  • 重复码:位宽为11.25ms,脉冲时间9ms。

例如:当按下一个按键触发IR发送开始后,可以看到红外分析仪上显示如下图,其中0x0100是客户码,0x00FF是数据码。 Demo IR

IR时序设计

首先需要设计IR时序。根据demo IR的协议,结合IR DMA FIFO mode的特点,我们得到下面图示的IR时序。

IR DMA FIFO mode一个完整的任务定义为FifoTask。先按照SDK demo中对IR repeat信号的处理采用“add repeat one by one(逐个添加repeat)”的方式介绍,即下面的宏定义为1。

#define ADD_REPEAT_ONE_BY_ONE           1

IR Timing 1

当一个按键按下触发IR发送开始后,将IR拆分成图上的FifoTask。

(1) IR start后,运行FifoTask_data,发送有效数据。FifoTask_data持续时间为T_data,由于数据的不确定性,T_data也不确定。FifoTask_data结束后,触发中断IRQ_PWM0_IR_DMA_FIFO_DONE。

(2) 在IRQ_PWM0_IR_DMA_FIFO_DONE中断函数里,开启FifoTask_idle,这个阶段发送无载波信号,时间为110ms – T_data。FifoTask_idle存在的意义是为了控制第1个FifoTask_repeat时间点正好在IR start后的110mS。FifoTask_idle结束后,触发中断IRQ_PWM0_IR_DMA_FIFO_DONE。

(3) 在IRQ_PWM0_IR_DMA_FIFO_DONE中断函数里,开启第1个FifoTask_repeat。每个FifoTask_repeat的持续时间都是110ms,只要在其对应的IRQ_PWM0_IR_DMA_FIFO_DONE中断函数继续添加下一个FifoTask_repeat,就可以控制IR repeat信号的持续发送。

(4) IR stop的时间点是不确定的,取决于按键release的时间。应用层检测到按键release后,在确保FifoTask_data正确完成的前提下,手动停止IR DMA FIFO mode即可结束IR的发送。

对上面时序设计进行一些优化。优化的步骤包括:

(1) 由于FifoTask_repeat是固定的时序,而IR DMA FIFO mode中DMA fifo数量比较大,可以将多个110ms的FifoTask_repeat合为1个FifoTask_repeat*n,这样可以减少软件中处理IRQ_PWM0_IR_DMA_FIFO_DONE的次数。对应Demo中宏“ADD_REPEAT_ONE_BY_ONE”为0的处理,此时Demo使用了5个IR repeat signal合成一个FifoTask_repeat*5。User可以根据Dma Fifo的使用情况,继续做优化。

(2) 在步骤1优化基础上,将FifoTask_idle和第一个FifoTask_repeat*n合一起,组成FifoTask_idle_repeat*n。

优化后的IR时序如下图所示:

IR Timing 2

根据上面IR时序设计,软件流程上对应如下code:

IR start时调用ir_nec_send函数,开启FifoTask_data,后面全部用中断去控制。FifoTask_data结束的中断里,开启FifoTask_idle。FifoTask_idle结束的中断里,开启FifoTask_repeat。在手动停止IR DMA FIFO mode之前,FifoTask_repeat连续执行。

void ir_nec_send(u8 addr1, u8 addr2, u8 cmd)
{
    if(ir_send_ctrl.last_cmd != cmd)
    {
        DBG_CHN2_TOGGLE;
        if(ir_sending_check())
        {
            return;
        }
        ir_send_ctrl.last_cmd = cmd;

        // set waveform input in sequence
        T_dmaData_buf.data_num = 0;

        //waveform for start bit
        T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_1st;
        T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_2nd;

        //add data
        u32 data = (~cmd)<<24 | cmd<<16 | addr2<<8 | addr1;
        for(int i=0;i<32;i++){
            if(data & BIT(i)){
                //waveform for logic_1
                T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_1st;
                T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_2nd;
            }
            else{
                //waveform for logic_0
                T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_1st;
                T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_2nd;
            }
        }

        //waveform for stop bit
        T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_1st;
        T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_2nd;
        T_dmaData_buf.dma_len = T_dmaData_buf.data_num * 2;
        ir_send_ctrl.repeat_enable = 1;  //need repeat signal
        ir_send_ctrl.is_sending = IR_SENDING_DATA;
//dma init
        pwm_set_dma_config(PWM_DMA_CHN);
        pwm_set_dma_buf(PWM_DMA_CHN, (u32) &T_dmaData_buf ,T_dmaData_buf.dma_len);
        pwm_ir_dma_mode_start(PWM_DMA_CHN);
        pwm_set_irq_mask(FLD_PWM0_IR_DMA_FIFO_IRQ);
        pwm_clr_irq_status(FLD_PWM0_IR_DMA_FIFO_IRQ );
        core_interrupt_enable();//
        plic_interrupt_enable(IRQ16_PWM);
        ir_send_ctrl.sending_start_time = clock_time();
    }
}

IR初始化

(1) rc_ir_init

IR初始化函数如下,user请参考SDK上code。

void rc_ir_init(void)

(2) IR硬件配置

以B91 demo为例。

PWM相关部分配置:

pwm_n_invert_en(PWM0_ID);
pwm_set_clk((unsigned char) (sys_clk.pclk*1000*1000/PWM_CLK_SPEED-1));//use pclk is ok
pwm_set_pin(PWM_PIN);
pwm_set_pwm0_mode(PWM_IR_DMA_FIFO_MODE);
pwm_set_tcmp(PWM_ID, PWM_CARRIER_HIGH_TICK);
pwm_set_tmax(PWM_ID, PWM_CARRIER_CYCLE_TICK);

DMA相关配置:

pwm_set_dma_config(PWM_DMA_CHN);
pwm_set_dma_buf(PWM_DMA_CHN, (u32) &T_dmaData_buf ,T_dmaData_buf.dma_len);
pwm_ir_dma_mode_start(PWM_DMA_CHN);

中断相关配置:

pwm_set_irq_mask(FLD_PWM0_IR_DMA_FIFO_IRQ);
pwm_clr_irq_status(FLD_PWM0_IR_DMA_FIFO_IRQ );
core_interrupt_enable();
plic_interrupt_enable(IRQ16_PWM);

Demo IR载波频率为38K,周期为26.3us,占空比为1/3。使用API pwm_set_tmax和pwm_set_tcmp配置周期和占空比。

pwm_set_tcmp(PWM_ID, PWM_CARRIER_HIGH_TICK);
pwm_set_tmax(PWM_ID, PWM_CARRIER_CYCLE_TICK);

在Demo IR中,没有出现多种不同载波频率的情况,这个38K的载波足以满足所有FifoTask的配置。所以不需要使用PWM shadow模式,关于该模式介绍可参考11.1.9.1部分内容。

(3) IR变量初始化

对应SDK demo中变量waveform_start_bit_1st、waveform_start_bit_2nd等。

结合IR时序设计的介绍,需要配置出FifoTask_data、FifoTask_repeat。

Start信号是9ms的载波信号+4.5ms无载波信号,对应的两个DMA FIFO数据的配置调用pwm_config_dma_fifo_waveform实现如下:

//start bit, 9000 us carrier,  4500 us low
waveform_start_bit_1st = pwm_cal_pwm0_ir_fifo_cfg_data(9000 * CLOCK_PWM_CLOCK_1US/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 1);
waveform_start_bit_2nd = pwm_cal_pwm0_ir_fifo_cfg_data(4500 * CLOCK_PWM_CLOCK_1US/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 0);

按照同样的方法,可以得到stop信号、repeat信号、data逻辑“1”信号、data逻辑“0”信号的配置。

FifoTask的配置

(1) FifoTask_data

根据demo IR的协议,如果要发送一个cmd(比如7),先发start信号(9ms载波信号+4.5ms无载波信号),然后是address + ~address + cmd + ~cmd。SDK Demo code中我们取address为0x88。

当发送~cmd的最后一个bit时,logical “0”或logical “1”的后面都是一段无载波信号。如果~cmd后面不再有任何数据,接收端可能会出现一个问题:由于没有载波的边界作为区分,不知道最后一个bit的无载波信号时间是560us还是1690us,导致无法识别这是一个logical “0”还是logical “1”。

为了解决这个问题,我们在Data信号尾巴上添加一个stop信号,stop信号的构成是:560us载波信号+500us无载波信号。

根据上面的描述,FifoTask_data主要分为3部分:

1) start信号:9ms载波信号+4.5ms无载波信号

2) data信号:address + ~address + cmd + ~cmd

3) stop:自定义的560us载波信号+500us无载波信号

根据以上3段信号,配置Dma FIfo buffer启动IR的发送,该部分在ir_nec_send函数中进行实现,其中部分相关code如下:

// set waveform input in sequence
    T_dmaData_buf.data_num = 0;
    //waveform for start bit
    T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_1st;
    T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_2nd;

    //add data
    u32 data = (~cmd)<<24 | cmd<<16 | addr2<<8 | addr1;
    for(int i=0;i<32;i++){
        if(data & BIT(i)){
            //waveform for logic_1
            T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_1st;
            T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_2nd;
        }
        else{
            //waveform for logic_0
            T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_1st;
            T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_2nd;
        }
    }

    //waveform for stop bit
    T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_1st;
    T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_2nd;
    T_dmaData_buf.dma_len = T_dmaData_buf.data_num * 2;

(2) FifoTask_idle

参考IR时序设计,FifoTask_idle的持续时间为110ms – T_data。FifoTask_data开始时记录时间:

ir_send_ctrl.sending_start_time = clock_time();

那么在FifoTask_data结束触发的中断里,计算FifoTask_idle的时序时间为:

110mS  (clock_time() - ir_send_ctrl.sending_start_time)

对应code如下:

u32 tick_2_repeat_sysClockTimer16M = 110*CLOCK_16M_SYS_TIMER_CLK_1MS - (clock_time() - ir_send_ctrl.sending_start_time);
u32 tick_2_repeat_sysTimer = (tick_2_repeat_sysClockTimer16M*CLOCK_PWM_CLOCK_1US>>4);

这里要注意两个时间单位的切换问题。参考本文档“时钟模块”的介绍可知,软件计时使用的System Timer频率是固定的16M。而PWM clock的来源是PCLK,需要考虑当system clock频率为非16M(24M、32M)时候的情况。

FifoTask_idle不发送PWM waveform,也可以认为是一直在发送无载波信号。将API pwm_cal_pwm0_ir_fifo_cfg_data中第三个参数carrier_en配置为0即可实现。

waveform_wait_to_repeat = pwm_cal_pwm0_ir_fifo_cfg_data(tick_2_repeat_sysTimer/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 0);

(3) FifoTask_repeat

根据demo IR的协议,repeat信号的组成是9ms的载波信号+2.25ms无载波信号。

类似于FifoTask_data的处理,需要在repeat信号尾巴上添加一小段载波信号作为结束判断标志,时间设为560us。

由IR时序设计可知,repeat信号要求持续时间为110ms,那么560us载波信号之后的无载波信号持续时间为:

  110ms – 9ms – 2.25ms – 560us = 99190us

一个完整的repeat信号的配置如下code所示:

//repeat signal  first part,  9000 us carrier, 2250 us low
    waveform_repeat_1st = pwm_cal_pwm0_ir_fifo_cfg_data(9000 * CLOCK_PWM_CLOCK_1US/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 1);
    waveform_repeat_2nd = pwm_cal_pwm0_ir_fifo_cfg_data(2250 * CLOCK_PWM_CLOCK_1US/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 0);

//repeat signal  second part,  560 us carrier, 99190 us low(110 ms - 9000us - 2250us - 560us = 99190 us)
    waveform_repeat_3rd = pwm_cal_pwm0_ir_fifo_cfg_data(560 * CLOCK_PWM_CLOCK_1US/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 1);
    waveform_repeat_4th = pwm_cal_pwm0_ir_fifo_cfg_data(99190 * CLOCK_PWM_CLOCK_1US/PWM_CARRIER_CYCLE_TICK, PWM0_PULSE_NORMAL, 0);

T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_2nd;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_3rd;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_4th;

(4) FifoTask_repeat*n 和 FifoTask_idle_repeat*n

以FifoTask_idle、FifoTask_repeat为基础,在Dma Fifo buffer上简单的叠加即可实现FifoTask_repeat*n 和 FifoTask_idle_repeat*n。

应用层判断IR busy

user在应用层通过变量“ir_send_ctrl.is_sending”来判断当前IR是否在发送数据或repeat信号。

如下所示为功耗管理中对IR是否busy的判断。当IR busy时,MCU不能进suspend。

if(ir_send_ctrl.is_sending)
{
    bls_pm_setSuspendMask(SUSPEND_DISABLE);
}

IR Learn

IR功能介绍

红外学习是利用红外管具备发送和接收红外信号的特性,利用放大电路将接收到的微弱信号放大并转换为数字信号,从而完成对红外波形的学习。学习之后将相关数据存至RAM/FLASH,之后再利用红外管的发送特性,将已学习的波形发送出去。

IR Learn硬件原理介绍

红外学习的硬件电路如下图所示。

IR Learn hardware circuit

当为红外学习状态时,IR_OUT与IR_CONTROL引脚应设置为GPIO功能同时拉低,此时Q2与Q3将处于截止态,IR_IN电平在没有波形时为高电平,随后将跟随三极管接收到的波形而变化:当输入波形为高电平时,IR_IN被拉低,相反则IR_IN恢复为高电平,红外学习也正是利用这个特性,使用GPIO低电平触发来完成学习算法,后面会详细介绍。发送端使用NEC格式的红外,而抓到的IR_IN的波形如下图所示。

IR_IN waveform of NEC protocol

其中深色的部分为载波,白色的部分为非载波。放大载波部分波形如下图所示,前面没有接收红外信号时为高,接收到信号时IR_IN被拉低,由图中可知IR_IN低电平为9.35us,周期为26.4us,换算载波频率为37.88kHz。这与NEC协议载波38kHz,1/3的占空比相匹配。

IR_IN waveform of NEC carrier

IR Learn软件原理概述

在红外学习时,芯片将设置并使能IR_IN下降沿触发中断。每次接收到其设备发送的红外载波,IR_IN都会被拉低并触发中断,在中断中通过算法记录载波与非载波的时间、波形数量、载波周期,在发送时根据以上信息将波形复制并发出。

如下图所示是中断处理时记录的顺序,前面1、2…所示的载波/非载波部分的持续时间将被记录在buff中。同时记录一定数量构成1的单载波的持续时间,经过平均值求得波形的载波频率fc。在记录完成后发送波形时,将按照fc的载波频率,固定1/3的占空比,按顺序将1、2对应的时间用载波/非载波的顺序发送出来,完成红外学习的过程。

Carrier and non-carrier

IR Learn软件说明

想要快速完成红外学习及发送功能,需要以下几个步骤:

(1) 使用ir_learn_init(void)进行初始化;

(2) 在中断函数添加ir_learn_irq_handler(void)中断处理函数相关部分;

(3) 在程序中增加ir_learn_detect(void)部分以判断学习结果;

(4) 修改rc_ir_learn.h中相关宏定义;

(5) 在UI层适当位置添加ir_learn_start(void)函数开始学习;

(6) 由步骤3中设置的判断函数判断结果后,使用get_ir_learn_state(void)查看红外学习状态,根据学习成功或失败做UI层操作:若成功继续步骤7~9完成发送,若失败可以重新执行步骤5或执行其他自定义UI动作;

(7) 学习成功后,可以将学习的结果进行发送。发送的第一步是进行红外发送的初始化,使用ir_learn_send_init(void)实现,注意,调用该函数后IR_OUT将改为PWM输出引脚,若想重新进入红外学习状态,必须重新执行步骤1来初始化引脚功能;

(8) 发送的第二步是将学习结果中有用的参数拷贝至固定区域,RAM和FLASH均可,使用ir_learn_copy_result(ir_learn_send_t* send_buffer)函数拷贝至定义的红外学习结果发送的结构体中;

(9) 发送的最后一步是调用ir_learn_send(ir_learn_send_t* send_buffer)函数,将学习结果发送。

至此,红外学习的整个功能就已实现。在下面的部分将按照上面的步骤按顺序逐一具体说明步骤中所提函数的添加方法。

IR_Learn初始化

在使用IR Learn功能时,将rc_ir_learn.c与rc_ir_learn.h拷入工程后,第一步需要调用初始化函数:

void ir_learn_init(void)

该函数在rc_ir_learn.c中找到实体,其首先将使用的结构体清零,接着将IR_OUT与IR_CONTROL设为GPIO并输出0,然后设置了GPIO中断使能并清除中断标志位。

IR_Learn中断处理

由于IR Learn功能是基于中断实现,第二步需在中断中添加中断处理函数。由于协议栈的构造会多次进入中断,为分辨是GPIO中断,先读取中断标志位,当为GPIO产生的中断时,再进行记录。实现代码如下:

_attribute_ram_code_ void ir_learn_irq_handler(void)
{
    gpio_clr_irq_status(FLD_GPIO_IRQ_CLR);
    if ((g_ir_learn_ctrl -> ir_learn_state != IR_LEARN_WAIT_KEY) && (g_ir_learn_ctrl -> ir_learn_state != IR_LEARN_BEGIN))
    {
        return;
    }
    ir_record(clock_time());  // IR Learning
}

其中ir_record()为具体的学习算法,为加快学习速度,避免执行时间长引起的误差,该函数前置_attribute_ram_code_被放至ram中。

IR_Learn结果处理函数

结果处理函数的主要作用是根据当前红外学习的情况来及时更改红外学习的状态,需要每个loop都执行来及时完成检测。可以在如main_loop()中调用函数:

void ir_learn_detect(void)

由函数实体可知,当学习开始后时间超过IR_LEARN_OVERTIME_THRESHOLD时,仍未收到波形超时失败;在学习开始并已收到信号后,超过设定的阈值时间未收到新的信号则认为学习状态已完成,此时如果收到的载波与非载波部分超过设定的数量(默认为15)则认为学习成功,否则会认为失败。

IR_Learn宏定义

为增加扩展性,在rc_ir_learn.h中增加了一些宏定义。

#define  GPIO_IR_OUT                    PWM_PIN   // GPIO_PE3
#define  GPIO_IR_CONTROL                GPIO_PE0
#define  GPIO_IR_LEARN_IN               GPIO_PE1

前三个定义了GPIO引脚,分别为IN/OUT/CONTROL,根据具体设计而改变。

IR_Learn启动函数

在UI层需要的地方调用IR Learn启动函数开始红外学习过程。函数如下:

ir_learn_start();

IR_Learn学习状态查询

user可以调用状态查询函数查询学习结果,函数如下:

unsigned char get_ir_learn_state(void)
{
    if(g_ir_learn_ctrl -> ir_learn_state == IR_LEARN_SUCCESS)
        return 0;
    else if(g_ir_learn_ctrl -> ir_learn_state < IR_LEARN_SUCCESS)
        return 1;
    else
        return (g_ir_learn_ctrl -> ir_learn_state);
}

返回值为0:红外学习成功。

返回值为1:红外学习进行中或未开始。

返回值 > 1:红外学习失败,返回值为失败原因,对应ir_learn_states可知失败原因,ir_learn_states定义如下:

typedef enum {
    IR_LEARN_DISABLE = 0x00,
    IR_LEARN_WAIT_KEY,
    IR_LEARN_KEY,
    IR_LEARN_BEGIN,
    IR_LEARN_SAMPLE_END,
    IR_LEARN_SUCCESS,
    IR_LEARN_FAIL_FIRST_INTERVAL_TOO_LONG,
    IR_LEARN_FAIL_TWO_LONG_NO_CARRIER,
    IR_LEARN_FAIL_WAIT_OVER_TIME,
    IR_LEARN_FAIL_WAVE_NUM_TOO_FEW,
    IR_LEARN_FAIL_FLASH_FULL,
    IR_LEARN_FAIL,
}ir_learn_states;

IR_Learn_Send初始化

在UI层判断学习成功后,在发送学习到的波形前需调用发送初始化函数,函数如下:

void ir_learn_send_init(void)

初始化函数中主要设置PWM相关参数和中断相关参数,并把IR_OUT设为PWM的输出口,注意此函数使用后红外学习功能停止,如需再打开需重新调用11.3.4.1描述的初始化函数。

IR_Learn结果复制函数

在设计中经常会遇到几个按键需要有红外学习功能的情况,因此UI层希望可以在学习成功后将学习结果复制到RAM/FLASH某个位置,供之后发送使用,同时可以开始其他按键的学习过程。因此提供结果复制函数,将发送中必须的参数复制。函数如下:

void ir_learn_copy_result(ir_learn_send_t* send_buffer)

其中send_buffer为红外学习发送需要的结构体,该结构体包含一个载波周期所占clock_tick值、载波与非载波的总数(从0开始计)、已经要发送的载波与非载波buffer。

typedef struct{
    unsigned int   ir_learn_carrier_cycle;
    unsigned short ir_learn_wave_num;
    unsigned int   ir_learn_send_buf[MAX_SECTION_NUMBER];
}ir_learn_send_t;

IR_Learn发送函数

在学习成功并做好发送前的操作后,可以调用发送函数将学习结果发送。函数如下:

void ir_learn_send(ir_learn_send_t* send_buffer);

其中send_buffer为上一个函数使用的结构体。该发送函数不带repeat功能,每次调用该函数会发送一次学习到的波形,如需repeat,用户可在UI层用定时器自行设计重复调用该函数。

IR Learn算法详解

为便于理解代码,这里详解红外学习算法原理。以下为一段模拟波形,模拟一包完整的红外数据。该数据包含Start Carrier、Start No Carrier、bit 1 carrier、bit 1 no carrier、bit 0 carrier、bit 0 no carrier、End carrier、End no carrier 。

A frame of IR code

由于在红外学习状态设置IR_IN为GPIO下降沿唤醒,因此正常情况下每个下降沿都会进入中断,我们在中断中做记录的操作。在红外学习算法中,并不是将波形识别出特定的码型,而是以载波/非载波的概念记录波形。连续的载波会被认为一个载波段,而相隔时间较长的两个载波间被认为非载波。因此上述在红外学习算法中被认为如下图所示:

Carrier and no carrier in IR Learn

算法中每次执行,都会记录当前时间curr_trigger_tm_point,并与上次进入中断时间last_trigger_tm_point相减得到一个周期的时间time_interval。如果这个时间较小,则认为仍在载波中;如果这个时间超过所设阈值,则认为中间经历了一个no carrier段,而此时处在新的carrier段的第一个波形中,此时需要记录上一个载波段时间并放入buffer,其为第一次进入中断的时间与上一次中断时间的差值,如下图所示:

IR learn algorithm

按照此方法,令wave_series_cnt从0开始增加,分别对应第一个载波段、第一个非载波段、第二个载波段、第二个非载波段······同时,将计算出的各个段的时间存入对应的位置(wave_series_buf[g_ir_learn_ctrl->wave_series_cnt])wave_series_buf[0]、wave_series_buf[1]、wave_series_buf[2]中,一直到波形结束,wave_series_cnt代表了总的段数,wave_series_buf中装载了各段的长度。

另外,在前N次(可设定)中断时,会记录N次时间,取其中最小的一个时间,作为载波周期,在学习结束后发送时使用,占空比默认为1/3(可设定)。

红外学习过程结束后,可进行学习结果发送。在发送学习结果时,也是按照载波与非载波的概念发送。利用PWM DMA_FIFO模式,将学习得到的载波频率、占空比、以及各个段的时长放入DMA buffer后,开启DMA,芯片将自动发送学习的波形至全部发送完成,并产生FLD_IRQ_PWM0_IR_DMA_FIFO_DONE中断。

IR Learn学习参数调整

在rc_ir_learn.h中定义了一些与红外学习相关的参数,当选择设置参数模式为USER_DEFINE并自行设置时,其会对学习效果产生不同的影响,这里将详细介绍这些参数。

#define         IR_LEARN_MAX_FREQUENCY          40000
#define         IR_LEARN_MIN_FREQUENCY          30000

#define         IR_LEARN_CARRIER_MIN_CYCLE      16000000/IR_LEARN_MAX_FREQUENCY
#define         IR_LEARN_CARRIER_MIN_HIGH_TICK  IR_LEARN_CARRIER_MIN_CYCLE/3
#define         IR_LEARN_CARRIER_MAX_CYCLE      16000000/IR_LEARN_MIN_FREQUENCY
#define         IR_LEARN_CARRIER_MAX_HIGH_TICK  IR_LEARN_CARRIER_MAX_CYCLE/3

以上参数设置了红外学习支持的频率。默认值设置为30k~40k。下面的参数是根据频率参数计算出的每个载波周期占sys_tick值、默认为1/3占空比的高电平占sys_tick值的最大值与最小值,以供后面参数计算使用。下面将介绍其他影响学习结果的参数,各参数均采用宏定义在rc_ir_learn.h中:

#define         IR_LEARN_INTERVAL_THRESHOLD         (IR_LEARN_CARRIER_MAX_CYCLE*3/2)
#define         IR_LEARN_END_THRESHOLD              (30*SYSTEM_TIMER_TICK_1MS)
#define         IR_LEARN_OVERTIME_THRESHOLD         10000000                        // 10s
#define         IR_CARR_CHECK_CNT                   10
#define         CARR_AND_NO_CARR_MIN_NUMBER         15
#define         MAX_SECTION_NUMBER                  100

(1) IR_LEARN_INTERVAL_THRESHOLD

载波周期阈值,默认值为IR_LEARN_CARRIER_MAX_CYCLE值的1.5倍,当两次进入中断的时间小于该阈值则认为在载波端。

(2)IR_LEARN_END_THRESHOLD

红外学习结束阈值,当两次进入中断时间超过该阈值,或超过该阈值没有进入下一次中断,则认为红外学习过程结束。

(3) IR_LEARN_OVERTIME_THRESHOLD

超时时间,如果红外学习过程开始后超过该阈值认为接收到波形进入中断,则认为学习过程结束且失败。

(4) IR_CARR_CHECK_CNT

设定用来判断载波周期时间而采集的包个数,默认设为10,代表将取前10次中断的time_interval中的最小值作为载波时间,在发送学习结果时用来计算载波周期。

(5) CARR_AND_NO_CARR_MIN_NUMBER

载波与非载波段的最小阈值,当红外学习过程完成后,如果记录的载波与非载波段的总数小于该阈值,则认为没有学习到整个波形而此次红外学习失败。

(6) MAX_SECTION_NUMBER

载波与非载波段的最大阈值,在设置buffer大小时使用,如设置为100,则此次红外学习过程最多记录100个载波与非载波段,超过时则认为此次红外学习失败。

IR Learn常见问题

在学习过程中,有时会遇到学习成功后发送的波形频率发生变化,可能的原因是学习的波形频率太高,导致在中断中执行算法的时间超过了载波周期。如下图所示。

IR learn error

以占空比1/3、发送频率38K的红外信号为例,一个载波周期大约为26.3us,高电平占1/3约为8.7us。在t0时刻,外部波形载波结束电平从高拉低,芯片GPIO触发中断,而进入中断需要执行汇编中几句指令保存现场进入中断,经测试在4us左右之后的t1进入中断函数开始执行操作。由于在中断中执行时间较长,在t2时中断执行结束,而恢复现场也需要4us左右。在恢复现场的过程中t3时刻,由于发送波形下一个下降沿到达,此时中断标志位已清除,硬件又将触发中断。在t2后4us左右恢复完现场后中断已再触发,因此芯片再次保存现场进入中断,在4us后的t4再进入中断进行操作,之后将重复上述过程。由中断执行的波形可见,其时间完全变形,两次进入中断的时间也比原波形一个载波周期的时间大。由于红外学习完全依照在中断中记录的时间来完成学习,进入中断的时间异常,会导致红外学习结果异常。

解决此问题的方法有几个:

一是将红外学习算法放入ram_code中减少执行时间,这个默认已经执行该操作,无需修改;

二是务必减少中断的其他处理,BLE由于在非IDLE状态时在中断中占用大量时间,因此在红外学习中需要关掉,UI层也尽量在红外学习期间禁止其他中断源引起中断,防止造成异常。

Demo说明

Ble SDK的feature_IR包含了普通的红外发送功能和红外学习功能,采用的红外编码方式为NEC编码。不同的模式之间切换如下code所示:

void key_change_proc(void)
{
    ···
    switch(key0)
    {
        ···
        if(switch_key == IR_mode){···
        }
        else if(switch_key == IR_Learn_mode){···
        }
        else{···
        }
    }
}

各个模式之间通过按键切换到不同模式执行相应的初始化操作,具体代码实现可参考BLE SDK。

Feature Demo介绍

B91m_feature_test针对一些常用的BLE相关的feature给出了demo code,用户可参考这些demo完成自己的功能实现,详情见code。在B91m_feature_test工程里面feature_config.h中对宏“FEATURE_TEST_MODE”进行选择性的定义,即可切换到不同feature test的Demo。

feature test的测试Demo选择

下面介绍每个Demo测试方法。

广播功耗测试

此项主要测试不同广播参数广播时的功耗,用户在测试时可外接万用表测量功耗。在feature_config.h需要修改FEATURE_TEST_MODE 为TEST_POWER_ADV。

#define FEATURE_TEST_MODE                   TEST_POWER_ADV

在app.c中使用APP_ADV_POWER_TEST_TYPE选择不同的广播类型及广播参数,用户可以根据需求修改。

#define APP_ADV_POWER_TEST_TYPE        UNCONNECTED_31B_2S_3CHANNEL
Demo中提供了两种广播类型,分别为可连接广播及非可连接广播。

可连接广播功耗测试

在app.c中,提供了四种基于可连接广播的组合供用户选择,如下代码所示。

#define CONNECT_12B_1S_1CHANNEL             0
#define CONNECT_12B_1S_3CHANNEL             1
#define CONNECT_12B_500MS_3CHANNEL          2
#define CONNECT_12B_30MS_3CHANNEL           3

在app.c中的user_init_normal函数中,定义可连接广播数据长度为12byte,用户可根据需求修改。

//ADV data length: 12 byte
u8 tbl_advData[12] = {  0x08, DT_COMPLETE_LOCAL_NAME, 't', 'e', 's', 't', 'a', 'd', 'v',
            0x02, DT_FLAGS, 0x05,};
在user_init_normal中通过bls_ll_setAdvParam函数配置广播参数:
ble_sts_t   bls_ll_setAdvParam( u16 intervalMin,  u16 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);

Demo提供了1s1channel,1s3channel,500ms3channel和30ms3channel的广播参数,用户根据自己需求选择对应的测项即可。用户也可以根据自己需求修改bls_ll_setAdvParam函数的参数来自由配置广播参数。

非可连接广播功耗测试

在app.c中,提供了六种基于非可连接广播的组合供用户选择,如下代码所示。

#define UNCONNECTED_16B_1S_3CHANNEL     4
#define UNCONNECTED_16B_1_5S_3CHANNEL       5
#define UNCONNECTED_16B_2S_3CHANNEL         6

#define UNCONNECTED_31B_1S_3CHANNEL         7
#define UNCONNECTED_31B_1_5S_3CHANNEL       8
#define UNCONNECTED_31B_2S_3CHANNEL         9

在user_init_normal函数中,定义非可连接广播数据长度为16byte和31byte,用户可根据需求选择。

#if APP_ADV_POWER_TEST_TYPE < UNCONNECTED_31B_1S_3CHANNEL
    //ADV data length: 16 byte
    u8 tbl_advData[] = {
         0x0F, DT_COMPLETE_LOCAL_NAME, 't', 'e', 's', 't', 'a', 'd', 'v', '8', '9', 'A', 'B', 'C', 'D', 'E',
    };
#else   //ADV data length: max 31 byte
    u8 tbl_advData[] = {
         0x1E, DT_COMPLETE_LOCAL_NAME, 't', 'e', 's', 't', 'a', 'd', 'v', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D'
    };
#endif

Demo提供了1s3channel,1.5s3channel,2s3channel的广播参数,用户根据自己需求选择对应的测项即可。用户也可以根据自己需求修改bls_ll_setAdvParam函数的参数来自由配置广播参数。

连接功耗测试

此项主要测试不同连接参数连接时的功耗,用户在测试时可外接万用表测量功耗。在feature_config.h需要修改FEATURE_TEST_MODE 为TEST_POWER_CONN。

#define FEATURE_TEST_MODE                       TEST_POWER_CONN

User可以在feature_conn_power工程目录下app.c中根据需求在task_connect回调函数中修改连接参数:

void    task_connect (u8 e, u8 *p, int n)
{
    bls_l2cap_requestConnParamUpdate (8, 8, 99, 400);  // 1 S
}

连接参数主要是通过bls_l2cap_requestConnParamUpdate函数进行修改,demo中默认设置1s的连接间隔:

void bls_l2cap_requestConnParamUpdate (u16 min_interval, u16 max_interval, u16 latency, u16 timeout)

关于该函数的具体描述user可参考BLE模块的3.3.2.1更新连接参数章节。

SMP测试

Smp测试主要测试配对加密的流程,主要分为以下几种方式:

(1) LE_Security_Mode_1_Level_1, no authentication and no encryption.

(2) LE_Security_Mode_1_Level_2, unauthenticated paring with encryption.

(3) LE_Security_Mode_1_Level_3, authenticated paring with encryption-legacy.

(4) LE_Security_Mode_1_Level_4, authenticated paring with encryption-sc.

用户需要在feature_config.h中将FEATURE_TEST_MODE 为TEST_SMP_SECURITY。

#define FEATURE_TEST_MODE                       TEST_SMP_SECURITY

下面对每种配对模式进行简要介绍。

LE_Security_Mode_1_Level_1

LE_Security_Mode_1_Level_1为最简单的配对方式,既不认证也不加密。用户将app_config.h的SMP_TEST_MODE更改为SMP_TEST_NO_SECURITY。

#define     SMP_TEST_MODE                   SMP_TEST_NO_SECURITY

LE_Security_Mode_1_Level_2

LE_Security_Mode_1_Level_2模式为just work,只加密不认证。Just work 又分为legacy pairing just work,secure connections just work。用户按需求将app_config.h的SMP_TEST_MODE更改为SMP_TEST_LEGACY_PARING_JUST_WORKS或者SMP_TEST_SC_PARING_JUST_WORKS。下面分别介绍。

SMP_TEST_LEGACY_PARING_JUST_WORKS

用户作如下修改:

#define     SMP_TEST_MODE           SMP_TEST_LEGACY_PARING_JUST_WORKS

示意流程图:

Legacy Just Work流程示意图

SMP_TEST_SC_PAIRING_JUST_WORKS

用户作如下修改:

#define     SMP_TEST_MODE                       SMP_TEST_SC_PAIRING_JUST_WORKS

示意流程图:

SC Just Work流程示意图

LE_Security_Mode_1_Level_3

LE_Security_Mode_1_Level_3为既认证又加密Legacy配对方式,根据配对参数设置分为OOB,PassKey Entry, Numeric Comparison。目前demo 提供了PassKey Entry两种示例代码,即SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI和SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI,用户根据需要选择。下面简单介绍下这两种方式。

SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI

用户需要在app_config.h中作如下修改:

#define     SMP_TEST_MODE                       SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI

配对过程中需要slave端显示密钥,master端输入密钥。初始化时,注册了配对相关gap event。配对信息会通知到app层。

blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEAGIN            |  \
                          GAP_EVT_MASK_SMP_PARING_SUCCESS           |  \
                          GAP_EVT_MASK_SMP_PARING_FAIL              |  \
                          GAP_EVT_MASK_SMP_TK_DISPALY               |  \
                          GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE );

用户需要在收到GAP_EVT_MASK_SMP_TK_DISPLAY消息时打印下当前的密钥信息。

int app_host_event_callback (u32 h, u8 *para, int n)
{
    u8 event = h & 0xFF;
    switch(event)
    {

        case GAP_EVT_SMP_TK_DISPALY:
        {
            char pc[7];
            u32 pinCode = *(u32*)para;
            sprintf(pc, "%d", pinCode);
            tlkapi_printf(1,"[APP][SMP]TK display:%d\n", pinCode);
        }
        break;

    }
}

基本流程示意图如下:

Legacy Just Work SDMI流程示意图

SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI

和上面的区别只是在master显示密钥,slave输入密钥。用户需要修改代码:

#define     SMP_TEST_MODE                       SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI

基本流程图如下:

Legacy Just Work SIMD流程示意图

LE_Security_Mode_1_Level_4

LE_Security_Mode_1_Level_4为既认证又加密SC配对方式,根据配对参数设置分为OOB,PassKey Entry, Numeric Comparison。目前demo 提供了SC PassKey Entry及SC Numeric Comparison三种示例代码,即SMP_TEST_SC_PASSKEY_ENTRY_SDMI、SMP_TEST_SC_PASSKEY_ENTRY_MDSI及SMP_TEST_SC_NUMERIC_COMPARISON,用户根据需要选择。下面简单介绍下这几种方式。

SMP_TEST_SC_NUMERIC_COMPARISON

用户需要在app_config.h中作如下修改:

#define     SMP_TEST_MODE                       SMP_TEST_SC_NUMERIC_COMPARISON

此配对方式为数字比较,即在配对的过程中,master和slave端均会显示六位PIN码,用户比较数字是否相同,如果相同,则分别点击确认同意配对。Demo是以按键的形式发送YES或者NO。示例代码如下:

if(consumer_key == MKEY_VOL_DN){
    blc_smp_setNumericComparisonResult(1);// YES
    /*confirmed YES*/
}
else if(consumer_key == MKEY_VOL_UP){
    blc_smp_setNumericComparisonResult(0);// NO
    /*confirmed NO*/
}

流程示意图如下:

Numeric Comparison配对示意图

SMP_TEST_SC_PASSKEY_ENTRY_SDMI

用户需要在app_config.h中作如下修改:

#define     SMP_TEST_MODE                       SMP_TEST_SC_PASSKEY_ENTRY_SDMI

配对过程中需要slave端显示密钥,master端输入密钥。初始化时,注册了配对相关gap event。配对信息会通知到app层。

blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask(    GAP_EVT_MASK_SMP_PARING_BEAGIN             |  \
                          GAP_EVT_MASK_SMP_PARING_SUCCESS           |  \
                          GAP_EVT_MASK_SMP_PARING_FAIL              |  \
                          GAP_EVT_MASK_SMP_TK_DISPALY               |  \
                          GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE );

用户需要在收到GAP_EVT_MASK_SMP_TK_DISPLAY消息时打印下当前的密钥信息。

int app_host_event_callback (u32 h, u8 *para, int n)
{
    u8 event = h & 0xFF;
    switch(event)
    {

        case GAP_EVT_SMP_TK_DISPLAY:
        {
            char pc[7];
            u32 pinCode = *(u32*)para;
            sprintf(pc, "%d", pinCode);
            tlkapi_printf(1,"[APP][SMP]TK display:%d\n", pinCode);
        }
        break;

    }
}

基本流程示意图如下:

SC SDMI配对流程示意图

SMP_TEST_SC_PASSKEY_ENTRY_MDSI

用户需要在app_config.h中作如下修改:

#define     SMP_TEST_MODE                       SMP_TEST_SC_PASSKEY_ENTRY_MDSI

配对过程中需要master端显示密钥,slave端输入密钥。初始化时,注册了配对相关gap event。配对信息会通知到app层。

    blc_gap_registerHostEventHandler( app_host_event_callback );
    blc_gap_setEventMask( GAP_EVT_MASK_SMP_PAIRING_BEGIN            |  \
                          GAP_EVT_MASK_SMP_PAIRING_SUCCESS          |  \
                          GAP_EVT_MASK_SMP_PAIRING_FAIL             |  \
                          GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY       |  \
                          GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE );

slave会在收到GAP_EVT_SMP_TK_REQUEST_PASSKEY时打印请求密钥的log。

        case GAP_EVT_SMP_TK_REQUEST_PASSKEY:
        {
            tlkapi_printf(1,"[APP][SMP]TK Request passkey\n");
        }
        break;

用户通过BDT将master显示的密钥输入后,slave会在这个API里进行验证密钥并设置TK。

int  blc_smp_setTK_by_PasskeyEntry (u32 pinCodeInput)
TK设置完成后,设备继续执行配对流程,使用设置的TK进行后续的加密和认证操作。 基本流程示意图如下:

SC SDMI配对流程示意图

GATT Security测试

由BLE模块3.3.3 ATT&GATT章节已知,服务列表中每个Attribute都定义了读写权限,即配对模式也要达到相应的等级才能去读或者写。如Demo的SPP服务中:

// client to server RX
{0,ATT_PERMISSIONS_READ,2,sizeof(TelinkSppDataClient2ServerCharVal),(u8*)(&my_characterUUID),       (u8*)(TelinkSppDataClient2ServerCharVal), 0},       //prop
    {0,SPP_C2S_ATT_PERMISSIONS_RDWR,16,sizeof(SppDataClient2ServerData),(u8*)(&TelinkSppDataClient2ServerUUID), (u8*)(SppDataClient2ServerData), &module_onReceiveData},    //value
    {0,ATT_PERMISSIONS_READ,2,sizeof(TelinkSPPC2SDescriptor),(u8*)&userdesc_UUID,(u8*)(&TelinkSPPC2SDescriptor)},

第二个Attribute的读写权限定义为:SPP_C2S_ATT_PERMISSIONS_RDWR。

此读写权限由用户去选择,可以选择如下其中一个:

#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_RDWR
#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_ENCRYPT_RDWR
#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_AUTHEN_RDWR
#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_SECURE_CONN_RDWR

无论选择哪个,当前配对模式要高于或者等于此读写权限等级才能正确的读写服务。

用户需要将feature_config.h修改如下:

#define FEATURE_TEST_MODE                               TEST_GATT_SECURITY

由SMP测试知道加密等级有LE_SECURITY_MODE_1_LEVEL_1、LE_SECURITY_MODE_1_LEVEL_2、LE_SECURITY_MODE_1_LEVEL_3、LE_SECURITY_MODE_1_LEVEL_4。用户需要在app_config.h中根据需求选择相应的配对模式。

#define     SMP_TEST_MODE                       LE_SECURITY_MODE_1_LEVEL_3

如当前的配对模式为LE_SECURITY_MODE_1_LEVEL_3,即既有认证又有加密Legacy配对模式。所以当前读写权限可以如下选择。

#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_AUTHEN_RDWR

流程示意图如下:

Gatt Security示意图

DLE测试

DLE测试主要测试长包,Demo分为master和slave,用户需要分别编译烧录到两块EVB板子,关于master端的代码user可参考B91m多连接SDK,slave端对应的feature_config.h选择,代码如下:

#define FEATURE_TEST_MODE           TEST_SDATA_LENGTH_EXTENSION

用户可在app_config.h中设置DLE长度:

#define DLE_LENGTH_SELECT               DLE_LENGTH_200_BYTES

设置DLE长度之后会自动配置合适大小的MTU。但实际数据传送过程中所使用的MTU大小要取master和slave中的较小值。

#if (DLE_LENGTH_SELECT == DLE_LENGTH_27_BYTES)
    #define MTU_SIZE_SETTING             23
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_52_BYTES)
    #define MTU_SIZE_SETTING             48
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_56_BYTES)
    #define MTU_SIZE_SETTING             52
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_100_BYTES)
    #define MTU_SIZE_SETTING             96
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_200_BYTES)
    #define MTU_SIZE_SETTING             196
#elif (DLE_LENGTH_SELECT == DLE_LENGTH_251_BYTES)
    #define MTU_SIZE_SETTING             247
#else
    #define MTU_SIZE_SETTING             23
#endif

烧录完之后分别reset,master通过GPIO或者按键触发(具体方式及接口由使用的master程序决定)建立连接。连接成功后,分别进行MTU及DataLength交换,MTU和DataLength分别取slave和master中的较小值用于后续发送数据。

以slave为例,简要说明MTU和DataLength的作用。用户在APP层设置要发送的长包数据,通过blc_gatt_pushHandleValueNotify()将数据和包头等一系列信息推到ATT层,其内部判断当前要发送的数据长度是否超过有效的MTU_SIZE - 3(这里 - 3 是因为ATT的包头占了3 bytes),如果超过则返回错误码GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE。之后ATT层将要发送的数据推到LL层,如果MTU的大小超过了LL层规定的DataLength,那么进行分包处理后再发出。

交换成功后,slave会每隔3.3s向master发送长包数据,或者master端每次触发配对键,mater会向slave端写一段长包数据。

测试流程示意图如下:

DLE测试流程示意图

Soft Timer测试

可参考软件定时器(Software Timer)一章。

WhiteList测试

Whitelist测试即白名单测试,设置了白名单则只允许白名单内的设备建立连接。用户需要如下修改feature_config.h:

#define FEATURE_TEST_MODE                               TEST_WHITELIST

当slave没有绑定信息时,则允许其他任意设备进行连接,连接成功后slave会将当前master的信息加入到白名单中,后续则只有当前设备能与slave进行连接。

测试示意图如下:

Whitelist测试示意图

1M Extended Advertising测试

1M Extended advertising demo主要是测试1M PHY的扩展广播,在feature_config.h需要修改FEATURE_TEST_MODE 为TEST_EXTENDED_ADVERTISING。

#define FEATURE_TEST_MODE                               TEST_EXTENDED_ADVERTISING

相关code都在vendor/feature_extend_adv,提供的是Slave端demo。

设置广播数据最大长度如下:

#define APP_ADV_SETS_NUMBER         1       // Number of Supported Advertising Sets
#define APP_MAX_LENGTH_ADV_DATA         1024    // Maximum Advertising Data Length
#define APP_MAX_LENGTH_SCAN_RESPONSE_DATA   31  // Maximum Scan Response Data Length

在app.c的user_init_normal()里已经预留基于1M PHY配置不同类型的扩展广播包。

#if 1  //Legacy, non_connectable_non_scannable
……
#elif 0 //Legacy, connectable_scannable
……
#elif 0 // Extended, None_Connectable_None_Scannable undirected, without auxiliary packet
……
#elif 0 // Extended, None_Connectable_None_Scannable directed, without auxiliary packet
……
#elif 0 // Extended, None_Connectable_None_Scannable undirected, with auxiliary packet
……
#elif 0 // Extended, None_Connectable_None_Scannable Directed, with auxiliary packet
……
#elif 0 // Extended, Scannable, Undirected
……
#elif 0 // Extended, Connectable, Undirected
……
#endif

用户需要使用支持Bluetooth 5 Low Energy Advertising Extension功能的手机或协议分析设备,才能看到扩展广播的数据。

2M/Coded PHY Used on Extended Advertising测试

2M/Coded PHY used on Extended advertising demo主要是测试1M/2M/Coded PHY各种组合的扩展广播,在feature_config.h需要修改FEATURE_TEST_MODE 为TEST_2M_CODED_PHY_EXT_ADV。

#define FEATURE_TEST_MODE                       TEST_2M_CODED_PHY_EXT_ADV

相关code都在vendor/feature_phy_extend_adv,提供的是Slave端demo。

设置广播数据最大长度如下:

#define APP_ADV_SETS_NUMBER         1   // Number of Supported Advertising Sets
#define APP_MAX_LENGTH_ADV_DATA         1024    // Maximum Advertising Data Length
#define APP_MAX_LENGTH_SCAN_RESPONSE_DATA   31  // Maximum Scan Response Data Length

在feature_ext_adv_init_normal里已经预留基于1M PHY / Coded PHY(S2) / Coded PHY(S8)各种组合的配置不同类型的扩展广播包。

#if 0 // Extended, None_Connectable_None_Scannable undirected, without auxiliary packet
    #if 0     // ADV_EXT_IND: 1M PHY
    #elif 1    // ADV_EXT_IND: Coded PHY(S2)
    #elif 0     // ADV_EXT_IND: Coded PHY(S8)
    #endif#
#elif 0 // Extended, None_Connectable_None_Scannable undirected, with auxiliary packet
    #if 1       // ADV_EXT_IND: 1M PHY;          AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
    #elif 0      // ADV_EXT_IND: 1M PHY;         AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
    #elif 0      // ADV_EXT_IND: 1M PHY;         AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S2)
    #elif 0      // ADV_EXT_IND: 1M PHY;         AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
    #elif 0      // ADV_EXT_IND: Coded PHY(S2);  AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
    #elif 0      // ADV_EXT_IND: Coded PHY(S8);  AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
    #elif 0      // ADV_EXT_IND: Coded PHY(S2);  AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
    #elif 0      // ADV_EXT_IND: Coded PHY(S8);  AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
    #elif 0      // ADV_EXT_IND: Coded PHY(S2);  AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S2)
    #elif 0      // ADV_EXT_IND: Coded PHY(S8);  AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
    #endif
#elif 1 // Extended, Scannable, Undirected
    #if 1       // ADV_EXT_IND: 1M PHY;          AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
    #elif 0      // ADV_EXT_IND: 1M PHY;         AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
    #elif 0      // ADV_EXT_IND: 1M PHY;         AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
    #elif 0      // ADV_EXT_IND: 1M PHY;         AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S2)
    #elif 0      // ADV_EXT_IND: Coded PHY(S8);  AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
    #elif 0      // ADV_EXT_IND: Coded PHY(S8);  AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
    #elif 0      // ADV_EXT_IND: Coded PHY(S8);  AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
    #endif
#endif

用户可以参照demo自行组合需要的扩展广播包类型。

用户需要使用支持Bluetooth 5 Low Energy Advertising Extension,Bluetooth 5 Low Energy 2Mbps和Bluetooth 5 Low Energy Coded(Long Range)功能的手机或协议分析设备,才能看到以上各种类型扩展广播的数据。

注意:

API blc_ll_init2MPhyCodedPhy_feature()用于使能2M PHY/Coded PHY。

2M/Coded PHY used on Legacy advertising and Connection测试

2M/Coded PHY used on Legacy advertising and Connection demo主要是测试在基于Legacy advertising 建立连接后,在连接态切换1M/2M/Coded PHY,在feature_config.h需要修改FEATURE_TEST_MODE 为TEST_2M_CODED_PHY_CONNECTION。

#define FEATURE_TEST_MODE           TEST_2M_CODED_PHY_CONNECTION

相关code都在vendor/feature_phy_conn,提供的是Slave端demo。

初始化打开2M Phy和Coded Phy:

blc_ll_init2MPhyCodedPhy_feature();     // mandatory for 2M/Coded PHY

连接成功后,在mainloop会以10秒的周期使用API blc_ll_setPhy()发起PHY变更请求,1M –> Coded PHY(S2) –> 2M –> Coded PHY(S8) –> 1M。

if(phy_update_test_tick && clock_time_exceed(phy_update_test_tick, 10000000)){
        phy_update_test_tick = clock_time() | 1;
        int AAA = phy_update_test_seq%4;
        if(AAA == 0){
            blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_CODED, PHY_PREFER_CODED, CODED_PHY_PREFER_S2);
        }
        else if(AAA == 1){
            blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_2M,    PHY_PREFER_2M,    CODED_PHY_PREFER_NONE);
        }
        else if(AAA == 2){
            blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_CODED, PHY_PREFER_CODED, CODED_PHY_PREFER_S8);
        }
        else{
            blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_1M,    PHY_PREFER_1M,    CODED_PHY_PREFER_NONE);
        }
        phy_update_test_seq ++;
    } 

Peer Master Device可以使用B91m多连接SDK的Demo “B91m master kma dongle”,但是也需要用API blc_ll_init2MPhyCodedPhy_feature()打开2M Phy和Coded Phy。

用户也可以选择使用支持Bluetooth 5 Low Energy 2Mbps和Bluetooth 5 Low Energy Coded(Long Range)功能的其他厂家Master设备或手机。

其他模块

24MHz晶体外部电容

参考下图中的24MHz晶体匹配电容的位置C1/C4。

SDK默认使用B91m内部电容(即ana_8a<5:0>对应的cap)作为24MHz晶振的匹配电容,此时C1/C4不用焊接电容。使用该方案的优势是:在Telink治具上可以测量并调节该电容,使得最终应用产品的频点值达到最佳。

24M晶体电路

如果需要使用外部焊接电容作为24M晶振的匹配电容(C1/C4焊接电容),则只要在main函数开始的地方(一定要在cpu_wakeup_init函数之前)调用下面API即可:

static inline void blc_app_setExternalCrystalCapEnable(u8  en)
{
    blt_miscParam.ext_cap_en = en;
    analog_write_reg8(0x8a,analog_read_reg8(0x8a)|0x80);//close internal cap
}

只要在cpu_wakeup_init之前调用该API,SDK会自动处理所有的设置,包括关掉内部匹配电容、不再读取频偏校正值等。

32KHz时钟源选择

SDK默认使用MCU内部32kHz RC振荡电路,简称32kHz RC。32kHz RC的误差比较大,所以对于suspend或者deep retention时间较长的应用,其时间准确性会差一些。目前32kHz RC默认支持的最大长连接不能超过3s,一旦超过这个时间,ble_timing会出错,造成收包时间点不准确,容易出现收发包retry,功耗增大,甚至出现断连。

如果用户需要实现更低的连接功耗,包括低功耗睡眠情况下时钟计时更加准确,可以选择使用外部32k晶体,简称32k Pad,目前SDK支持该模式。

用户只需要在main函数开始的地方(一定要在cpu_wakeup_init函数之前)调用下面两个API中的一个:

void blc_pm_select_internal_32k_crystal(void);
void blc_pm_select_external_32k_crystal(void);

他们分别是选择32k RC和32k Pad的API,SDK默认调用blc_pm_select_internal_32k_crystal选择的32k RC,如果需要使用32k Pad,将其替换成blc_pm_select_external_32k_crystal即可。

软件PA

如果需要使用RF PA的话,请参考drivers/B91/ext_driver/software_pa.c和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 app_rf_pa_handler(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.0》(Vol 2/Part E/7.8.28~7.8.30)和《Core_v5.0》(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);

//user for PhyTest 2 wire uart mode
int      blc_phyTest_2wire_rxUartCb (void);
int      blc_phyTest_2wire_txUartCb (void);
// user for PhyTest 2 wire hci mode
int blc_phyTest_hci_rxUartCb (void);

void blc_phyTest_Uart_num_Set (uart_num_e uart_num);

初始化的时候,调用blc_phy_initPhyTest_module将PhyTest模块设置好。调用blc_phyTest_Uart_num_Set设置PhyTest的uart num。

应用层触发PhyTest后,调用blc_phy_setPhyTestEnable(1)开启PhyTest模式。

SDK demo “B91m_feature_test”中初始化时就直接触发phytest开始;

PhyTest是一个特殊的模式,和正常的BLE功能是互斥的,一旦进入PhyTest 模式,广播和连接都不可用了。所以运行正常BLE功能时不能触发PhyTest。

PhyTest结束后,要么直接重新上电,要么调用blc_phy_setPhyTestEnable(0),此时MCU会自动reboot。

使用blc_phy_isPhyTestEnable判断当前PhyTest是否被触发了,可以看到代码中使用该API来实现低功耗管理,PhyTest模式不能进入低功耗。

当PhyTest使用uart两线模式(PHYTEST_MODE_THROUGH_2_WIRE_UART)时,初始化的时候按如下设置:

blc_register_hci_handler (blc_phyTest_2wire_rxUartCb, blc_phyTest_2wire_txUartCb);

blc_phyTest_2wire_rxUartCb实现上位机下发的cmd的解析和执行,blc_phyTest_2wire_txUartCb实现将相应的结果和数据反馈给上位机。

当PhyTest使用uart两线模式(PHYTEST_MODE_OVER_HCI_WITH_UART)时,初始化的时候按如下设置:

blc_register_hci_handler (blc_phyTest_hci_rxUartCb, blc_phyTest_2wire_txUartCb); 

blc_phyTest_hci_rxUartCb实现上位机下发的cmd的解析和执行,blc_phyTest_2wire_txUartCb实现将相应的结果和数据反馈给上位机。

PhyTest demo

Demo: B91m_feature_test

SDK demo “B91m_feature_test”的app_config.h中,测试模式修改为“TEST_BLE_PHY”,如下所示:

#define FEATURE_TEST_MODE               TEST_BLE_PHY

根据物理接口和测试命令格式的不同,PhyTest可分为三种测试模式,如下所示,“PHYTEST_MODE_DISABLE”表示PhyTest禁用。

#ifndef     PHYTEST_MODE_DISABLE
#define     PHYTEST_MODE_DISABLE                        0
#endif

#ifndef     PHYTEST_MODE_THROUGH_2_WIRE_UART
#define         PHYTEST_MODE_THROUGH_2_WIRE_UART        1
#endif

#ifndef     PHYTEST_MODE_OVER_HCI_WITH_USB
#define         PHYTEST_MODE_OVER_HCI_WITH_USB          2 
#endif

#ifndef     PHYTEST_MODE_OVER_HCI_WITH_UART
#define         PHYTEST_MODE_OVER_HCI_WITH_UART         3 
#endif

选择PhyTest的测试模式:

#if (FEATURE_TEST_MODE == TEST_BLE_PHY)
        #define BLE_PHYTEST_MODE        PHYTEST_MODE_THROUGH_2_WIRE_UART
#endif

如下定义为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

注意:HCI模式USB接口,暂时不支持。

按照以上定义,编译B91m_feature_test生成的bin文件直接测试可以通过。user可研究一下code的实现,掌握相关接口的使用。

EMI

EMI Test

EMI Test Tool

关于工具和使用,user可从Telink的官网或联系telink技术支持进行获取。

JTAG使用

为了能够使用JTAG模块,需要在使用前确保满足以下几点条件:

  • JTAG的四个GPIO需要设置成使能模式。
  • 如果芯片处于低功耗模式,那么使用JTAG前芯片必须退出低功耗模式。
  • 如果JTAG模式因为FLASH中有程序而不能正常使用,需要在使用前用Telink BDT工具擦除FLASH。

JTAG连线说明

Diagnostic Report

(1) 在Target Manager里选择Diagnostic report。

Target Manager

(2) 选择V5 core,不要勾选SDP (2wires),我们的JTAG暂时不支持2线模式,address输入0。

Diagnostic report option

(3) 点击OK,会生成一个Diagnostic report。

Diagnostic report

Target Configuration

(1) 右击工程文件夹选择“Target Configuration”。

Target Configuration Option

(2) 确保没有勾选“SDP (2wires)”。

Target Configuration

Flash Programming

右击工程文件夹选择“Flash Burner”。

Flash Burner Option

(1) 选择IDE安装目录下的SPI_burn.exe。

(2) 选择需要下载的bin文件。

(3) 勾选“Target management”。

(4) 不要勾选“Target Burn”。

(5) 勾选“Verification”,如果需要在烧录前擦除FLASH,可以勾选“erase all”。

Flash Programming

(6) 点击“Burn”下载bin文件,如果出现“Verify sucess”,此时表示烧录成功。

Verify sucess

Application Debug

(1) 右击工程文件夹选择“Debug As-> Debug Configurations”。

Debug Configurations option

(2) 右击“MCU Program”,选择new,创建一个Debug Configuration。

New Debug Configurations option

(3) 在startup里勾选Reset and Hold,然后点击“Apply"和"Debug"。

Debug Configurations Startup

(4) 点击“Apply”之后,IDE会自动调出debug视图,在工具栏里可以看到“Variables”、“Expressions”等内容。

“Variables”:列出了堆栈区相关的变量信息,当程序突然停止时会突出显示变量值的更改。

“Expressions”:列出了全局变量,用黄色突出有更改的部分。

Debug perspective

版本函数

用户可以通过以下函数来获取当前SDK版本信息。

unsigned char blc_get_sdk_version(unsigned char *pbuf,unsigned char number);

参数pbuf是指向存储版本信息位置的指针,参数number是版本信息的长度,应该在5-16之间,目前只用了5个字节表示版本信息。函数的返回值为0表示成功,为1表示失败。例如,如果调用blc_get_sdk_version函数后得到的数据为{3,4,0,0,1},它表示当前SDK版本为3.4.0.0 patch 1。

Debug

TLKAPI_DEBUG打印方法介绍

为方便用户调试时打印信息,B91m支持USB Dump、GPIO simulate UART、hardware UART三种通道打印:

/**
 * @brief   Debug log channel select
 *          user can select one type in app_config.h
 */
#define TLKAPI_DEBUG_CHANNEL_UDB                    1   //USB Dump
#define TLKAPI_DEBUG_CHANNEL_GSUART                 2   //GPIO simulate UART
#define TLKAPI_DEBUG_CHANNEL_UART                   3   //hardware UART
用户需要在app_config.h中定义相关信息如下所示,默认使用模拟UART通道:

#define TLKAPI_DEBUG_ENABLE                 1
#define TLKAPI_DEBUG_CHANNEL                TLKAPI_DEBUG_CHANNEL_GSUART
#define TLKAPI_DEBUG_GSUART_BAUDRATE        1000000

#define APP_LOG_EN                          1
#define APP_SMP_LOG_EN                      0
#define APP_KEY_LOG_EN                      1
#define APP_CONTR_EVENT_LOG_EN              1  //controller event log
#define APP_HOST_EVENT_LOG_EN               1  //host event log
#define APP_OTA_LOG_EN                      1
#define APP_FLASH_INIT_LOG_EN               1
#define APP_FLASH_PROT_LOG_EN               1

配置说明

(1)TLKAPI_DEBUG_CHANNEL_UDB

PM模式下不支持UDB通道打印。

(2)TLKAPI_DEBUG_CHANNEL_GSUART 程序默认使用GPIO模拟UART打印,用户可以自己定义打印TX口TLKAPI_DEBUG_GPIO_PIN和波特率TLKAPI_DEBUG_GSUART_BAUDRATE。

    /**
     * @brief   some default setting for "GPIO simulate UART" log channel
     */
    /* default GPIO, user can change it in app_config.h */
    #ifndef TLKAPI_DEBUG_GPIO_PIN
        #if (MCU_CORE_TYPE == MCU_CORE_B91)
            #define TLKAPI_DEBUG_GPIO_PIN               GPIO_PD2
        #elif (MCU_CORE_TYPE == MCU_CORE_B92)
            #define TLKAPI_DEBUG_GPIO_PIN               GPIO_PE5
        #endif
    #endif

    /* default UART baudrate, user can change it in app_config.h */
    #ifndef TLKAPI_DEBUG_GSUART_BAUDRATE
    #define TLKAPI_DEBUG_GSUART_BAUDRATE        1000000
    #endif

(3)选择TLKAPI_DEBUG_CHANNEL_UART

用户可以自行修改TLKAPI_DEBUG_UART_TX_PIN、TLKAPI_DEBUG_UART_RX_PIN、TLKAPI_DEBUG_UART_BAUDRATE等参数。

相关API的介绍

API tlkapi_debug_init

初始化函数。

/**
 * @brief       Debug log initialization when MCU power on or wake_up from deepSleep mode
 * @param[in]   none
 * @return      none
 */
int tlkapi_debug_init(void)
{
    tlkDbgCtl.dbg_en = 1;

    if (!tlkapi_print_fifo){
        tlkapi_print_fifo = &print_fifo;
    }
    tlkapi_print_fifo->wptr = tlkapi_print_fifo->rptr = 0;

    #if (DUMP_STR_EN)
        extern my_fifo_t   *myudb_print_fifo;
        myudb_print_fifo = tlkapi_print_fifo;
    #endif

    #if (TLKAPI_DEBUG_CHANNEL == TLKAPI_DEBUG_CHANNEL_UDB)
        #if (BLE_APP_PM_ENABLE)
            #error "can not use USB debug when PM enable !!!"
        #endif
        tlkDbgCtl.dbg_chn = TLKAPI_DEBUG_CHANNEL_UDB;
        tlkDbgCtl.fifo_format_len = 12;  //todo
        myudb_usb_init(0x120, NULL);
        usb_set_pin_en();
    #elif (TLKAPI_DEBUG_CHANNEL == TLKAPI_DEBUG_CHANNEL_GSUART)
        tlkDbgCtl.dbg_chn = TLKAPI_DEBUG_CHANNEL_GSUART;
        tlkDbgCtl.fifo_format_len = 8;  //todo
        sTlkApiDebugBitIntv = SYSTEM_TIMER_TICK_1S/TLKAPI_DEBUG_GSUART_BAUDRATE;
        gpio_set_gpio_en(TLKAPI_DEBUG_GPIO_PIN);
        gpio_set_up_down_res(TLKAPI_DEBUG_GPIO_PIN, GPIO_PIN_PULLUP_1M);
        gpio_write(TLKAPI_DEBUG_GPIO_PIN, 1);
        gpio_set_output_en(TLKAPI_DEBUG_GPIO_PIN, 1);
    #elif (TLKAPI_DEBUG_CHANNEL == TLKAPI_DEBUG_CHANNEL_UART)
        tlkDbgCtl.dbg_chn = TLKAPI_DEBUG_CHANNEL_UART;
        tlkDbgCtl.fifo_format_len = 8;  //todo
        tlkDbgCtl.uartSendIsBusy = 0;
        uart_debug_init();
    #endif

    tlkDbgCtl.fifo_data_len = tlkapi_print_fifo->size - tlkDbgCtl.fifo_format_len;

    return 0;
}

用户在使用TLKAPI_DEBUG打印之前需要对其进行初始化。在tlkapi_debug_init函数中,会根据TLKAPI_DEBUG_CHANNEL的选择来初始化一些相关信息,比如比特率、TX_PIN等等。而由于GPIO的初始化信息会在进入deep_ret时掉电,所以需要在user_init_deepRetn()函数中调用tlkapi_debug_deepRetn_init()函数来重新初始化一些内容。

API tlkapi_debug_deepRetn_init

/**
 * @brief       Debug log initialization when MCU wake_up from deepSleep_retention mode
 * @param[in]   none
 * @return      none
 */
int tlkapi_debug_deepRetn_init(void)
{

    #if (TLKAPI_DEBUG_CHANNEL == TLKAPI_DEBUG_CHANNEL_GSUART)
        gpio_set_gpio_en(TLKAPI_DEBUG_GPIO_PIN);
        gpio_write(TLKAPI_DEBUG_GPIO_PIN, 1);
        gpio_set_output_en(TLKAPI_DEBUG_GPIO_PIN, 1);
    #elif (TLKAPI_DEBUG_CHANNEL == TLKAPI_DEBUG_CHANNEL_UART)
        uart_debug_init();
    #endif

    return 0;
}

API blc_debug_enableStackLog

用于配置客户需要打印哪种类型的log。

/**
 * @brief      for user to configure which type of stack print information they want
 * @param[in]  mask - debug information combination
 * @return     none
 */
void blc_debug_enableStackLog(stk_log_msk_t mask);

使用blc_debug_enableStackLog设置stkLog_mask(默认值为 STK_LOG_NONE)。 其他还可以使用blc_debug_addStackLog和blc_debug_removeStackLog API来添加和删除某个类型的mask:

/**
 * @brief      for user to add some type of stack print information they want
 * @param[in]  mask - debug information combination
 * @return     none
 */
void blc_debug_addStackLog(stk_log_msk_t mask);
/**
 * @brief      for user to remove some type of stack print information they want
 * @param[in]  mask - debug information combination
 * @return     none
 */
void blc_debug_removeStackLog(stk_log_msk_t mask);
stkLog_mask的设置可以选择下面几个值中的一个,或者多个值的“或操作”。

/**
 *  @brief stack log
 */
typedef enum {
    STK_LOG_NONE                = 0,

    STK_LOG_LL_CMD              = BIT(0),


    STK_LOG_ATT_RX              = BIT(20),
    STK_LOG_ATT_TX              = BIT(21),

    STK_LOG_OTA_FLOW            = BIT(24),
    STK_LOG_OTA_DATA            = BIT(25),

    STK_LOG_ALL                 = 0xFFFFFFFF,
}stk_log_msk_t;

STK_LOG_NONE 表示stack log disable,不打印底层的log信息。 STK_LOG_LL_CMD 用于控制比如广播、扫描之类LL层的log信息。 STK_LOG_ATT_RX和STK_LOG_ATT_TX用于控制打印ATT层RX和TX的信息。 STK_LOG_OTA_FLOW和 STK_LOG_OTA_DATA用于控制打印OTA过程相关的log信息。 STK_LOG_ALL表示打印底层所有的log信息。

API tlkapi_debug_isBusy

tlkapi_debug_isBusy用于判断是否还有debug信息在FIFO里未发送完

/**
 * @brief       check if debug log busy
 * @param[in]   none
 * @return      1: debug log busy, some log pending in FIFO, not all finish;
 *              0: debug log empty
 */
bool tlkapi_debug_isBusy(void)
{
    #if (TLKAPI_DEBUG_CHANNEL == TLKAPI_DEBUG_CHANNEL_UART)
        if(tlkDbgCtl.uartSendIsBusy){
             return TRUE;
        }
    #endif

    return (tlkapi_print_fifo->wptr != tlkapi_print_fifo->rptr);
}

API tlkapi_send_string_data

/**
 * @brief       Send debug log to log FIFO, character string and data mixed mode.
 *              attention: here just send log to FIFO, can not output immediately, wait for "tlkapi debug_handler" to output log.
 * @param[in]   en - send log enable, 1: enable;  0: disable
 * @param[in]   str - character string
 * @param[in]   pData - pointer of data
 * @param[in]   len - length of data
 * @return      1: send to FIFO success; 0: send to FIFO fail
 */
#define tlkapi_send_string_data(en, str, pData, len)                if(en){tlkapi_send_str_data(str,(u8*)(pData), len);}

用来传输需要打印的内容,可以把字符和数据一起传到FIFO里,len需要传输的u8类型数据表示长度。

API tlkapi_send_string_u32s

/**
 * @brief       Send debug log to log FIFO, character string and data mixed mode, with variable length data, data in "unsigned int" format
 *              attention: here just send log to FIFO, can not output immediately, wait for "tlkapi debug_handler" to output log.
 * @param[in]   en - send log enable, 1: enable;  0: disable
 * @param[in]   str - character string
 * @param[in]   ... - variable length data, maximum length is 8
 * @param[in]   data_len - length of data
 * @return      1: send to FIFO success; 0: send to FIFO fail
 */
#define tlkapi_send_string_u32s(en, str, ...)               if(en){tlkapi_send_str_u32s(str, COUNT_ARGS(__VA_ARGS__), ##__VA_ARGS__);}

用于传输字符串和多个u32类型的数据,数据个数可变。

API tlkapi_send_string_u8s

/**
 * @brief       Send debug log to log FIFO, character string and data mixed mode, with variable length data, data in "unsigned char" format
 *              attention: here just send log to FIFO, can not output immediately, wait for "tlkapi debug_handler" to output log.
 * @param[in]   en - send log enable, 1: enable;  0: disable
 * @param[in]   str - character string
 * @param[in]   ... - variable length data, maximum length is 16
 * @param[in]   data_len - length of data
 * @return      1: send to FIFO success; 0: send to FIFO fail
 */
#define tlkapi_send_string_u8s(en, str, ...)                if(en){tlkapi_send_str_u8s(str, COUNT_ARGS(__VA_ARGS__), ##__VA_ARGS__);}

用于传输字符串和多个u8类型的数据,数据个数可变。

API tlkapi_printf

/**
 * @brief       Send debug log to log FIFO, printf mode
 *              attention: here just send log to FIFO, can not output immediately, wait for "tlkapi debug_handler" to output log.
 * @param[in]   en - send log enable, 1: enable;  0: disable
 * @param[in]   fmt -
 * @return      none
 */
#define tlkapi_printf(en, fmt, ...)                     if(en){my_printf(fmt, ##__VA_ARGS__);}

printf输出功能。

这些函数在app层均有调用示例可供用户参考。

注意:

(1)这些函数只是把要打印的log信息放到FIFO里,并不是立即打印,需要等到tlkapi_debug_handler函数执行完才能打印出来。

(2)用户在使用以上函数时需要使用Telink串口上位机,这是因为Telink上位机可以混合显示ASCII和Hex。

API tlkapi_debug_handler

/**
 * @brief       Debug log process in mainloop, output log form log FIFO if FIFO not empty
 * @param[in]   none
 * @return      none
 */
void tlkapi_debug_handler(void);

tlkapi_debug_handler函数会跟随着main_loop循环执行,如果FIFO里有用户存入的数据,那么就会打印出来。

附录

附录1:crc16 算法

unsigned short crc16 (unsigned char *pD, int len)
{
static unsigned short poly[2]={0, 0xa001};
unsigned short crc = 0xffff;
unsigned char ds;
int i,j;

for(j=len; j>0; j--)
    {
unsigned char ds = *pD++;
for(i=0; i<8; i++)
        {
            crc = (crc >> 1) ^ poly[(crc ^ ds ) & 1];
            ds = ds >> 1;
        }
    }

return crc;
}

附录2:检查stack是否溢出

原理

在cstartup_flash.S中将stack的所有内容写为0x55,在程序运行时会将使用过的栈的数据改写为其他值。通过查看栈被改写的size大小,可以判断栈的使用情况,并可以判断栈是否发生溢出。

方法

(1)打开boot/cstartup_flash.S,将_FILL_STK下的内容使能。

_FILL_STK:
#if 1
    lui    t0, 0x55555
    addi   t0, t0, 0x555
    la     t2, _BSS_VMA_END
    la     t3, _STACK_TOP
_FILL_STK_BEGIN:
    bleu   t3, t2, _MAIN_FUNC
    sw     t0, 0(t2)
    addi   t2, t2, 4
    j      _FILL_STK_BEGIN
#endif

(2)根据此手册中2.1.2.1章节的SRAM空间分配确定stack的栈底地址。

(3)使用BDT软件将程序的.bin文件下载完成后,点击Reset使程序运行。然后将slave与master进行连接配对。

(4)配对完成后,在BDT中使用“Tool -> Memory Access”来读取RAM中的数据。示例图如下。

使用BDT读取RAM数据的设置

(5)在键盘上按“tab”键可生成一个Read.bin文件将数据保存。文件地址为:BDT安装路径 -> config -> user,Read.bin。

(6)使用十六进制查看软件打开Read.bin,如果没有连续的0x55,说明stack溢出到了.bss段。 或者,更准确的方法为,在工程生成的.lst文件中找到所分配的栈顶地址,如下图所示。然后在Read.bin中查看此地址是否被改写为其他内容,若被改写则说明栈溢出。

通过.lst查看栈顶地址