跳转至

B85m系列BLE单连接SDK


SDK介绍

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

软件组织架构

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

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

"SDK文件结构"

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

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

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

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

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

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

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

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

main.c

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

_attribute_ram_code_ int main (void)    //must run in ramcode
{
    DBG_CHN0_LOW;   //debug
    blc_pm_select_internal_32k_crystal();

    #if(MCU_CORE_TYPE == MCU_CORE_825x)
        cpu_wakeup_init();
    #elif(MCU_CORE_TYPE == MCU_CORE_827x)
        cpu_wakeup_init(LDO_MODE,EXTERNAL_XTAL_24M);
    #endif

    int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();  //MCU deep retention wakeUp

    rf_drv_init(RF_MODE_BLE_1M);

    gpio_init( !deepRetWakeUp );  //analog resistance will keep available in deepSleep mode, so no need initialize again

    clock_init(SYS_CLK_TYPE);

    if(!deepRetWakeUp){//read flash size
        blc_readFlashSize_autoConfigCustomFlashSector();
    }

    blc_app_loadCustomizedParameters();  //load customized freq_offset cap value

    if( deepRetWakeUp ){
        user_init_deepRetn ();
    }
    else{
        user_init_normal ();
    }

    irq_enable();
    while (1) {
#if (MODULE_WATCHDOG_ENABLE)
        wd_clear(); //clear watch dog
#endif
        main_loop ();
    }
}

app_config.h

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

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

application file

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

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

  • 其他UI文件:如IR(红外)、battery detect(电池检测)等用户任务的处理文件。

BLE stack entry

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

(1) main.c文件irq_handler函数中BLE相关中断的处理入口irq_blt_sdk_handler。

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

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

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

Applicable IC

适用如下几种IC型号,它们属于B85m系列,均为同一内核,其中8251/8253/8258硬件模块基本一致,8238/8271/8273/8278硬件模块基本一致,只是在SRAM与flash大小上略有差异。具体请参照各芯片DataSheet。

Software Bootloader介绍

SDK V3.4.2.4之前版本介绍

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

"不同IC对应的bootloader以及boot.link路径"

每种IC对应2个software bootloader文件,分别对应启用16k deep retention、32k deep retention(deep retention的介绍可以参考“低功耗管理”章节)。其中由于8273与8278的boot文件与link文件完全相同,统一用8278的配置。

以cstartup_8258_RET_16K.S为例,第一句#ifdef MCU_STARTUP_8258_RET_16K说明了只有当user定义了MCU_STARTUP_8258_RET_16K时,该bootloader才会生效。

用户可以根据实际使用的IC以及是否使用deep retention(16K或者32k)功能选择不同的software bootloader。

B85 BLE SDK里的工程默认使用的配置是SRAM size 64K的8258 , deepsleep retention 16K SRAM,即对应使用的software bootloader和link文件分别为cstartup_8258_RET_16K.S和boot_16k_retn_8251_8253_8258.link,用户需要根据自己使用的芯片类型和Retention大小的评估情况手动修改其配置(详细分析方法可参考小节“SRAM 空间”)。

以8258_ble_remote 为例说明如何将8258的software bootloader改为deepsleep retention 32K SRAM。

Step 1 在8258_ble_remote 工程设置中定义-DMCU_STARTUP_8258_RET_32K,如下图所示。

"software bootloader设置"

注意:

  • 根据前面介绍,B85m系列中8251、8253和8258硬件相同,8271、8278硬件相同,但SRAM size是不一样,所以用户在选择不同的software bootloader文件后还需要修改SDK根目录下的boot.link文件(根据下表对应关系将其中的link文件中的内容替换到SDK根目录下的boot.link中),不同IC的software bootloader以及boot.link选用关系见下表。
IC 16kB retention 32kB retention
8251 boot_16k_retn_8251_8253_8258.link
cstartup_8251_RET_16K.S
boot_32k_retn_8251.link
cstartup_8251_RET_32K.S
8253 boot_16k_retn_8251_8253_8258.link
cstartup_8253_RET_16K.S
boot_32k_retn_8253_8258.link
cstartup_8253_RET_32K.S
8258 boot_16k_retn_8251_8253_8258.link
cstartup_8258_RET_16K.S
boot_32k_retn_8253_8258.link
cstartup_8258_RET_32K.S
8271 boot_16k_retn_8271_8278.link
cstartup_8271_RET_16K.S
boot_32k_retn_8271.link
cstartup_8271_RET_32K.S
8273 boot_16k_retn_8271_8278.link
cstartup_8278_RET_32K.S
boot_32k_retn_8278.link
cstartup_8278_RET_32K.S
8278 boot_16k_retn_8271_8278.link
cstartup_8278_RET_32K.S
boot_32k_retn_8278.link
cstartup_8278_RET_32K.S

Step 2 根据上面的例子以及映射表,software bootloader文件为cstartup_8258_RET_32K.S,需要使用SDK/boot/boot_32k_retn_8253_8258.link文件替换SDK根目录下的boot.link。 在API use_init()中blc_ll_initPowerManagement_module()后调用以下API 设置硬件的Retention区域:blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K)。

SDK v3.4.2.4及之后版本介绍

与旧版相同,software bootloader文件存放在SDK/boot/目录下:

"不同IC对应的bootloader以及boot.link路径"

825x和827x系列的IC对应不同的software bootloader文件,分别为cstartup_825x.S和cstartup_827x.S,用户通过选择不同的芯片来配置.S文件中的SRAM size。

以cstartup_825x.S为例:

#define SRAM_BASE_ADDR      (0x840000)
#define SRAM_32K            (0x8000)    //32KSRAM
#define SRAM_48K            (0xc000)    //48KSRAM
#define SRAM_64K            (0x10000)   //64KSRAM

#if (MCU_STARTUP_8258)
    #define SRAM_SIZE       (SRAM_BASE_ADDR + SRAM_64K)
#elif (MCU_STARTUP_8253)
    #define SRAM_SIZE       (SRAM_BASE_ADDR + SRAM_48K)
#elif (MCU_STARTUP_8251)
    #define SRAM_SIZE       (SRAM_BASE_ADDR + SRAM_32K)
#endif

#ifndef SRAM_SIZE
#define SRAM_SIZE           (SRAM_BASE_ADDR + SRAM_64K)
#endif

B85 BLE SDK里的工程默认使用的配置是SRAM大小为64KB的8258,用户需要根据自己使用的芯片类型手动修改其配置(详细分析方法可参考小节“SRAM 空间”)。

以8251_ble_sample为例说明如何修改bootloader中定义的SRAM大小。

在8251_ble_sample工程设置中定义-DMCU_STARTUP_8251,如下图所示。其他旧的宏,如MCU_STARTUP_8258_RET_16K、MCU_STARTUP_8258_RET_32K等不再使用。

"software bootloader设置"

Demo介绍

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

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

"BLE SDK 提供的demo code"

BLE Slave Demo

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

Demo Stack Application MCU Function
B85m hci BLE controller No BLE controller,only Advertising and one Slave connection
B85m module BLE controller + host Application在主控MCU上 BLE透传模组
B85m ble remote BLE controller + host 遥控器应用 主控MCU
B85m ble sample BLE controller + host 最简单的slave demo,广播和连接功能 主控MCU
B85m feature BLE controller + host 各种feature的集合 主控MCU

B85m hci是一个BLE slave controller,提供了基于UART的HCI,和其他MCU的host通信,形成一个完整的BLE slave系统。

B85m module只作为BLE透传模组,与主控MCU通过UART接口通信,一般应用代码写于主控MCU中。

B85m module实现了通过透传模组控制相关状态变化的功能。

B85m remote是一个基于完整slave角色的遥控器demo,包含低压检测、按键扫描、NEC格式红外发射、OTA空中升级、应用层电源管理、蓝牙控制、语音传输、红外学习等功能。用户可以根据此工程了解到一个基本的使用案例是什么结构的,以及大部分功能在应用层是如何实现的。

注意:

  • 由于语音、红外、红外学习耗费RAM资源较大,打开这些功能时,B85m remote必须更换为32k retention相关配置并编译使用。

B85m ble sample是对B85m_ble_remote的简化,可以和标准的IOS/android设备配对连接。

BLE master demo

B85m master kma dongle是BLE master single connection的demo,可以和B85m ble sample/B85m ble remote/B85m module连接并通信。

B85m ble remote/B85m ble sample对应的library提供了标准的BLE stack(master和slave共用一个library),包含了BLE controller + BLE host,用户只需要在app层添加自己的应用代码,不用再去处理BLE host的东西,完全依赖于controller和host的API即可。

新SDK的library将slave和master库合二为一了, B85m master kma dongle编译code的时候只会调用库中标准的BLE controller功能部分,library中并没有提供master的标准host功能。B85m master kma dongle demo code在app层上给出了参考的BLE Host的实现方法,包括ATT、简单的SDP(service discovery protocol)和最常用的SMP(security management protocol)等。

BLE master最复杂的功能在于对slave server的service discovery和所有service的识别,一般是在android/linux系统中才能实现。Telink B85m IC由于flash size和SRAM size的限制,无法提供完整的service discovery。但是SDK中提供了所有service discovery需要用到的ATT接口,用户可以参考B85m master kma dongle对B85m ble remote的service discovery过程,去实现自己的特定service的遍历。

Feature Demo和driver demo

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

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

B85m driver test对基本的driver给出了sample code,供用户参考并实现自己的driver功能。本文档driver部分会详细介绍各个driver。

在B85m driver test工程里面app_config.h中对宏“DRIVER_TEST_MODE”进行选择性的定义,即可切换到不同driver test的Demo。

MCU基础模块

MCU地址空间

MCU地址空间分配

以典型的64K SRAM版本为例,介绍MCU地址空间分配。如下图所示。

Telink B85m MCU的最大寻址空间为16M bytes:

  • 从0到0x7FFFFF 的8M空间为程序空间,即最大程序容量为8M bytes。
  • 0x800000到0xFFFFFF为外部设备空间:0x800000\~0x80FFFF为寄存器空间;0x840000\~0x84FFFF为64K SRAM空间。

"MCU地址空间分配"

B85m MCU 物理寻址时,地址线BIT(23)用于区别程序空间/外设空间:

  • 该地址为0时,访问程序空间;
  • 该地址为1时,访问外设空间。

寻址空间为外设空间(BIT(23)为1)时,地址线BIT(18)用于区别Register和SRAM:

  • 该地址为0时,访问Register;
  • 该地址为1时,访问SRAM。

SRAM和Firmware空间分配

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

如果不使用deepsleep retention功能,只用到suspend和普通的deepsleep功能,B85m SRAM空间分配与Telink上一代BLE IC 826x系列一样。使用过826x BLE SDK的用户可先参考《826x BLE SDK handbook》中SRAM空间分配的介绍,再和本节要介绍的B85m SRAM空间分配做对比,加深对该部分的理解。

32kB SRAM 地址空间范围为0x840000 ~ 0x848000,48kB SRAM 地址空间范围为0x840000 ~ 0x84C000,64kB SRAM 地址空间范围为0x840000 ~ 0x850000。

SDK V3.4.2.4之前版本的空间分配

下图是旧版本中8258/8278、8253/8238、8251/8271在16k retention和32k retention模式下对应的SRAM空间分配说明。需要注意的是IC为8251/8271且使用 deepsleep retention 32K SRAM模式时,其SRAM空间分配的各个段是动态调整的,具体可以参考对应的software bootloader和link文件。

"各IC在16k和32k retention对应的SRAM空间分配"

下面以SRAM size 64K的IC:8258、SDK中默认的deepsleep retention 16K SRAM模式为例详细介绍SRAM区域各个部分。如果SRAM size是其他值或者deepsleep retention 32k SRAM模式,用户类推即可。

64k SRAM对应的SRAM和Firmware空间分配如下图所示。

"SRAM空间分配& Firmware空间分配"

SDK中SRAM空间分配相关的文件有boot.link(由“software bootloader介绍”小节我们可知这里的boot.link内容同boot_16k_retn_8251_8253_8258.link文件)和cstartup_8258_RET_16K.S。(如果使用deepsleep retention 32K SRAM,则bootloader对应cstartup_8258_RET_32K.S,link文件对应boot_32k_retn_8253_8258.link。)

Flash中Firmware包括vector、ramcode、retention_data、text、Rodata和Data initial value。

SRAM中包括vector、ramcode、retention_data、Cache、data、bss、stack和unused SRAM area。

SRAM中的vector/ramcode/ retention_data是Flash中vector/ramcode/ retention_data的拷贝。

(1) vectors and ram_code

“vectors”段是汇编文件cstartup_8258_RET_16K.S对应的程序,是软件启动代码(software bootloader)。

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

_attribute_ram_code_ void flash_erase_sector(u32 addr);

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

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

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

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

Firmware中的vector和ramcode都需要在MCU上电时全部搬到RAM上,编译之后,这两部分加起来的size为_ramcode_size_。_ramcode_size_是一个编译器能够认识的变量值,它的计算实现在boot.link中,如下所示,编译的结果_ramcode_size_等于vector和ramcode所有code的size。

    . = 0x0;
        .vectors :
        {
        *(.vectors)
        *(.vectors.*)
        }
        .ram_code :
        {
        *(.ram_code)
        *(.ram_code.*)
        }
    PROVIDE(_ramcode_size_ = . );//计算实际的ramcode size(vector + ramcode)

(2) retention_data

B85m的deepsleep retention mode支持MCU进入retention后,SRAM的前16K/32K可以不掉电保持住SRAM上的数据不丢失。

程序中的全局变量如果直接编译的话,会分配在"data"段或"bss"段,这两段的内容不在前16K的retention区域,进入deepsleep retention后会掉电丢失。

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

参考下面即将要介绍的“data/bss”段可知,data段的全局变量对应的initial value需要提前存放在flash上;bss段的变量initial value为0,无需提前准备,bootloader运行时直接在SRAM上设为0即可。

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

“retention_data”段是紧跟着”ram_code”段的,即”vector + ramcode + retention_data” 3段按顺序排布在flash的前面,它们的总大小为“retention_size”。MCU上电(deepsleep wake_up)后”vector+ramcode+ retention_data”作为一个整体拷贝到SRAM的前面,此后程序执行过程中只要不进deepsleep(只有suspend /deepsleep retention),这一整块的内容就一直保持在SRAM上,MCU无须再从flash上读取。

boot.link文件中retention_data段相关的配置如下。

    . = (0x840000 + (_rstored_));
        .retention_data :
          AT ( _rstored_ )
         {
                . = (((. + 3) / 4)*4);
                PROVIDE(_retention_data_start_ = . );
                *(.retention_data)
                *(.retention_data.*)
                PROVIDE(_retention_data_end_ = . );
 }

以上配置的含义为:编译的时候看到含有“retention_data”关键字的变量,在flash firmware中分布的起始地址为“_rstored_”,在SRAM上对应的地址为0x840000 + (_rstored_)。而“_rstored_”这个值就是"ram_code” section的结尾。

使用deepsleep retention 16K SRAM mode时,“retention_size”不能超过16K,如果超过16K的限制,用户可以选择切换为deepsleep retention 32K SRAM mode。如果用户选择的配置使用的是deepsleep retention 16K SRAM mode,但定义的“retention_size”超过所定义的16K,编译时会出现如下图所示的错误。

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

a. 减少所定义的”attribute_data_retention”属性的数据;

b. 选择切换为deepsleep retention 32K SRAM,详细配置方法参考小节1.3。

当“retention_size”不超过16K时(假设为12K),flash上存在一片4K的“wasteful flash area”(无效flash区域),对应的firmware binary file上可以看到12K ~ 16K的内容全部是无效的“0”,拷贝到SRAM后,SRAM上也会有4K的“wasteful SRAM area”(无效SRAM区域)。

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

(3) Cache

Cache是MCU的指令高速缓存,且必须被配置为SRAM中的一段才可以正常运行。Cache size是固定的,包括256字节的tag和2048字节的Instructions cache,总共0x900 = 2.25K。

常驻内存的code可以直接从SRAM中读取并执行,但firmware中可以常驻SRAM的code只是一部分,剩下绝大部分都还在Flash中。根据程序的局部性原理,可以将一部分Flash code存储到Cache中,如果当前需要执行的code在Cache里,直接从Cache读取并执行;如果不在Cache中,则从Flash读取code并搬到Cache,再从Cache中读取执行。

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

Cache大小是固定的2.25K,它在SRAM中的起始地址为可配置的,旧版本中配置到SRAM 16K retention area后面,即起始地址为0x844000,结束地址为0x844900。

(4) data / bss

“data” 段是SRAM中存放程序已经初始化的全局变量,即initial value非0的全局变量。“bss” 段是SRAM中存放程序未初始化的全局变量,即initial value为0的全局变量。这两部分是连在一起的,data段后紧跟bss段,所以这里作为一个整体介绍。

“data” + “bss” 段紧跟Cache,起始地址即Cache的结束地址,在旧版本中为0x844900。下面为旧版本boot.link中的代码,直接定义SRAM上data段开始的地址:

    . = 0x844900;
        .data :

“data”段是被初始化的全局变量,其初值需要提前存储在flash中,即图中所示Firmware中的“data initial value”。

(5) data_no_init

为了节省retention部分的RAM,我们增加了这一个段。这个段的特点是:其在RAM中,却不在retention段,此部分的变量初始值为随机值。这个段是SDK做优化时使用的,不推荐用户使用。如果真的想要在应用中用到这个段,需要保证:变量在使用之前一定需赋过值,且在赋值与使用之间不能经过deep retention/deep/reboot/重新上电等。

(6) stack / unused area

对于默认64K的SRAM ,”stack”是从最高地址0x850000(48K SRAM对应地址为0x84C000,32K SRAM对应地址为0x848000)开始的,其方向为从下往上延伸,即stack指针SP在数据入栈时自减,数据出栈时自加。

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

当用户的SRAM使用较多时,需要明确知道自己的程序使用了多少stack,这个无法通过list文件来分析,只能让应用程序运行起来,确保其运行了程序中所有的可能使用stack比较深的code后,将MCU reset,读取SRAM空间去确定stack的使用量。

“unused area”即bss段结束地址与stack最深地址之间剩余的空间。只有当这个空间存在时,才说明stack没有和bss 冲突,SRAM使用没有问题。如果stack最深的地方和bss段重合了,则说明SRAM不够用了。

通过list文件可以查出bss段结束的地址,也就确定了留给stack的最大空间,用户需要分析这个空间是否足够,结合上面说的查看stack最深地址,可以知道SRAM的使用是否超出。下面demo中会给出分析方法。

(7) text

“text”段是Flash Firmware中所有非ram_code函数的集合。程序中的函数如果加了“_attribute_ram_code_”,会被编译为ram_code段,其他没有加这个关键字的函数就全部编译到了“text”段。一般情况下,”text”段是Firmware中最大的空间,远大于SRAM的size,所以需要通过Cache的缓存功能,将需要执行的code先load到Cache中再可以被执行。

(8) rodata /data init value

Flash Firmware中除了vector、ram_code和text,剩余的数据为”rodata”段和”data initial value”。

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

static const attribute_t my_Attributes[] = ……

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

前面介绍的“data”段是程序中已初始化的全局变量,比如定义全局变量如下:

int   testValue = 0x1234;

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

SDK V3.4.2.4及之后版本的空间分配

B85m SDK V3.4.2.4及之后版本的SRAM空间分配与之前版本的分配原理上是一样的,只是为了节省空间,将原来的固定16K/32K retention size修改为实际使用的retention size。此章节只针对修改和新增部分作详细介绍。

下图是新版本中8258/8278、8253/8238、8251/8271在16k retention和32k retention模式下对应的SRAM空间分配说明。SRAM空间分配的各个段是动态调整的,具体可以参考software bootloader和link文件。

"SDK v3.4.2.4及之后版本各IC在16k和32k retention对应的SRAM空间分配"

新版本中,deepsleep retention的大小可以通过API blc_app_setDeepsleepRetentionSRAMSize()自动设置,其原理为通过_retention_size_判断当前retention_data段结束所占用的RAM大小。如果小于16K,那么程序设置为deepsleep retention 16K SRAM mode;如果超过16K但小于32K,那么程序会自动切换为deepsleep retention 32K SRAM mode;如果超过32K,程序会在编译阶段报错。

blc_app_setDeepsleepRetentionSRAMSize()中判断code如下。

if (((u32)&_retention_size_) < 0x4000){
    blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW16K); //retention size < 16k, use 16k deep retention
}
else if (((u32)&_retention_size_) < 0x8000){
    blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K); //retention size < 32k and >16k, use 32k deep retention
}
else{
    /* retention size > 32k, overflow. deep retention size setting err*/
    #if (UART_PRINT_DEBUG_ENABLE)
        tlkapi_printf(APP_LOG_EN, "[APP][INI] deep retention size setting err");
    #endif
}
在boot.link文件中添加断言,用于在编译阶段判断retention size是否超过32K。
ASSERT(_retention_size_ * __PM_DEEPSLEEP_RETENTION_ENABLE < 0x8000, "Error: Retention RAM size overflow.");
其中,"__PM_DEEPSLEEP_RETENTION_ENABLE"在 vendor/common/user_config.c 中定义,等于用户在app_config.h中配置的"PM_DEEPSLEEP_RETENTTION_ENABLE"。

deepsleep retention size超过32K时错误提示为:

"deepsleep retention size超过32K时错误提示"

下面以SRAM size 64K的IC:8258为例详细介绍SRAM区域各个部分。如果SRAM size是其他值,用户类推即可。

64k SRAM对应的SRAM和Firmware空间分配如下图所示。

"SDK v3.4.2.4及之后版本SRAM空间分配& Firmware空间分配"

SDK中SRAM空间分配相关的文件有boot.link和cstartup_825x.S。

Flash中Firmware包括vector、ramcode、retention_data、text、rodata和data initial value。

SRAM中包括vector、ramcode、retention_data、Cache、data、bss、data no initial value、sdk_version、stack和unused SRAM area。

SRAM中的vector/ramcode/ retention_data是Flash中vector/ramcode/retention_data的拷贝。

(1) vectors and ram_code

“vectors”和“ram_code”段的详细介绍请参考“SDK V3.4.2.4之前版本的空间分配”小节。

(2) retention_data

“retention_data”段的详细介绍请参考“SDK V3.4.2.4之前版本的空间分配”小节。

(3) Cache

“Cache”段的详细介绍请参考“SDK V3.4.2.4之前版本的空间分配”小节。

新版本中,“Cache”段配置到retention_data后面,即起始地址为_retention_data_size_align_256_。

(4) data / bss

“data” 段和“bss” 段的详细介绍请参考“SDK V3.4.2.4之前版本的空间分配”小节。

新版本中,“data”起始地址为Cache的结束地址0x840900 + (retention_data_size_align_256)。下面为boot.link中的代码,直接定义SRAM上data段开始的地址:

    . = (0x840900 + (_retention_data_size_align_256_));
        .data :

(5) data_no_init

为了节省retention部分的RAM,我们增加了这一个段。这个段的特点是:其在RAM中,却不在retention段,此部分的变量初始值为随机值。这个段是SDK做优化时使用的,不推荐用户使用。如果真的想要在应用中用到这个段,需要保证:变量在使用之前一定赋过值,且在赋值与使用之间不能经过deep retention/deep/reboot/重新上电等。

(6) stack / unused area

对于默认64K的SRAM ,”stack”是从最高地址0x850000(48K SRAM对应地址为0x84C000,32K SRAM对应地址为0x848000)开始的,其方向为从下往上延伸,即stack指针SP在数据入栈时自减,数据出栈时自加。

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

当用户的SRAM使用较多时,需要明确知道自己的程序使用了多少stack,这个无法通过list文件来分析,用户可以使能cstartup_825x.S/cstartup_827x.S中的FLL_STK_EN,此功能是将data段和bss段到SRAM最高地址填充为0xFF,被使用的SRAM空间则会被改写。让应用程序运行起来之后,将MCU reset,读取SRAM空间去确定stack的使用量。

“unused area”即bss段结束地址与stack最深地址之间剩余的空间。只有当这个空间存在时,才说明stack没有和bss冲突,SRAM使用没有问题。如果stack最深的地方和bss段重合了,则说明SRAM不够用了。

通过list文件可以查出bss段结束的地址,也就确定了留给stack的最大空间,用户需要分析这个空间是否足够,结合上面说的查看stack最深地址,可以知道SRAM的使用是否超出。下面demo中会给出分析方法。

(7) text

“text”段的详细介绍请参考“SDK V3.4.2.4之前版本的SRAM空间分配”小节。

(8) rodata /data init value

“rodata”和“data init value”段的详细介绍请参考“SDK V3.4.2.4之前版本的SRAM空间分配”小节。

(9) sdk_version

“sdk_version”段为SDK V3.4.2.4开始新添加的一个段,主要用于存储版本号信息,在程序编译时会输出版本号信息。

list文件分析demo

这里以BLE slave最简单的demo 825x ble sample为例,结合“SDK v3.4.2.4及之后版本SRAM空间分配& Firmware空间分配”来分析。

825x ble sample的bin文件和list文件见目录“SDK”->“Demo”->“list file analyze”。

以下分析中,会出现多处截图,均来自boot.link、cstartup_825x.S、825x ble sample.bin和825x ble sample.list,请用户自行查找文件找到截图对应位置。

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

"list文件section统计"

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

(1) vectors: 从Flash 0开始,Size为0x1b0,计算出结束地址为0x1b0;

(2) ram_code: 从Flash 0x1b0开始,Size为0x29b0,计算出结束地址为0x2b60;

(3) retention_data: 从Flash 0x2b60开始,Size为0xd5c,计算出结束地址为0x38bc;

(4) text: 从Flash 0x3900开始,Size为0x8e8c,计算出结束地址为0xc78c;

(5) rodata: 从Flash 0xc78c开始,Size为0xc44,计算出结束地址为0xd3d0;

(6) data: 从SRAM 0x844200开始,Size为0x40,计算出结束地址为0x844240;

(7) bss: 从SRAM 0x844240开始,Size为0x204,计算出结束地址为0x844444。

(8) data_no_init: 从SRAM 0x844444开始,Size为0x300,计算出结束地址为0x844744。

(9) sdk_version: 从SRAM 0x844744开始,Size为0x30,计算出结束地址为0x844774。

结合前面介绍可知,剩余SRAM空间为0x850000 – 0x844774 = 0xb88c = 47244 byte,减去stack需要使用的256 byte,还剩46988 byte。

"list文件section地址"

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

(1) vector:

“vectors”段在flash firmware中起始地址为0,结束地址为0x1b0(最后一笔数据地址为0x1ae~0x1b0),size为0x1b0。上电搬移到SRAM后,在SRAM上的地址为0x840000 ~ 0x8401b0。

(2) ram_code:

“ram_code”段起始地址为0x1b0,结束地址为0x2b60(最后一笔数据地址为0x2b5c~0x2b60)。上电搬移到SRAM后,在SRAM上的地址为0x8401b0 ~ 0x842b60。

(3) retention_data:

“retention_data”在flash中起始地址“rstored”为0x2b60,也是”ram_code”的结尾。

“retention_data”在SRAM中起始地址为0x842b60,结束地址为0x8438bc(最后一笔数据地址为0x8438b8 ~ 0x8438bc)。

(4) Cache:

Cache在SRAM中地址范围为:0x843900~0x844200。Cache的相关信息在list文件中不会体现出来。

(5) text:

“text”段在flash firmware中起始地址为0x3900,结束地址为0xc78c(最后一笔数据地址为0xc78a~0xc78c),Size为0xc78a– 0x3900 = 0x8e8c,和前面Section统计中数据一致。

(6) rodata:

“rodata”段起始地址为text的结束地址0xc78c,结束地址为0xd3d0(最后一笔数据地址为0xd3cc~0xd3d0)。

(7) data:

“data”段在SRAM上起始地址为Cache的结束地址0x844200,list文件Section统计部分给出的size为0x40。 “data”段在SRAM上的结束地址为0x844240(最后一笔数据地址为0x84423c~0x844240)。

(8) bss:

“bss”段在SRAM上起始地址为“data”段的结束地址0x844240(16字节对齐),list文件Section统计部分给出的size为0x204。

“bss”段在SRAM上的结束地址为0x844444(最后一笔数据地址为0x844440~0x844444)。

(9) data_no_init:

“data_no_init”段在SRAM上起始地址为“bss”段的结束地址0x844240,list文件Section统计部分给出的size为0x300。

(10) sdk_version:

“sdk_version”段在SRAM上起始地址为“data_no_init”段的结束地址0x844744,list文件Section统计部分给出的size为0x30。 “sdk_version”段在SRAM上的结束地址为0x844774(最后一笔数据地址为0x844770~0x844774)。

剩余SRAM空间为0x850000 – 0x844774 = 0xb88c = 47244 byte,减去stack需要使用的256 byte,还剩46988 byte。

MCU地址空间访问

程序中对0x000000 - 0xFFFFFF地址空间的访问分以下两种情况。

外设空间的读写操作

外设空间(register和SRAM)的读写操作直接用指针访问实现:

    u8  x = *(volatile u8*)0x800066;  //读register 0x66的值
     *(volatile u8*)0x800066 = 0x26;  //给register 0x66赋值
    u32 y = *(volatile u32*)0x840000;      //读SRAM 0x40000-0x40003地址的值
    *(volatile u32*)0x840000 = 0x12345678; //给SRAM 0x40000-0x40003地址赋值

程序中使用函数write_reg8、write_reg16、write_reg32、read_reg8、read_reg16、read_reg32对外设空间进行读写,其实质是指针操作。更多信息,请参照drivers/8258/bsp.h。

注意程序中类似write_reg8(0x40000)/ read_reg16(0x40000)的操作,其定义如下所示,从中看出0x800000的偏移已自动加上(地址线BIT(23)为1),所以MCU能够确保访问的是Register/SRAM空间,而不会去访问flash空间。

#define REG_BASE_ADDR       0x800000
#define write_reg8(addr,v)  U8_SET((addr + REG_BASE_ADDR),v)
#define write_reg16(addr,v) U16_SET((addr + REG_BASE_ADDR),v)
#define write_reg32(addr,v) U32_SET((addr + REG_BASE_ADDR),v)
#define read_reg8(addr)     U8_GET((addr + REG_BASE_ADDR))
#define read_reg16(addr)    U16_GET((addr + REG_BASE_ADDR))
#define read_reg32(addr)    U32_GET((addr + REG_BASE_ADDR))

这里注意一个内存对齐的问题:如果使用指向2字节/4字节的指针来读写外设空间,一定要确保地址是2字节/4字节对齐的,如果不对齐的话,会发生数据读写错误。如下两种是错误的:

u16  x = *(volatile u16*)0x840001;     //0x840001没有2字节对齐
*(volatile u32*)0x840005 = 0x12345678;  //0x840005没有4字节对齐 

修改为正确的读写操作:

u16  x = *(volatile u16*)0x840000;     //0x840000  2字节对齐
*(volatile u32*)0x840004 = 0x12345678;  //0x840004  4字节对齐

Flash的操作

此部分内容详见第8章Flash介绍。

SDK Flash空间的分配

此部分内容详见第8章Flash介绍。

时钟模块

System clock & System Timer

系统时钟(system clock)是MCU执行程序的时钟。

系统定时器(System Timer)是一个只读的定时器,为BLE的时序控制提供时间基准,同时也可以提供给用户使用。

在Telink上一代IC(826x系列)上,System Timer的时钟来源于system clock,而B85m系列 IC上,System Timer和system clock是独立分开的。如下图所示,System Timer是由外部24M Crystal Oscillator经3/2分频得到的16M。

"System clock & System Timer"

图上可以看到,system clock可以由外部24M晶体振荡器经"doubler”电路倍频到48M后再分频得到16M、24M、32M、48M等,这一类clock我们称为crystal clock(如16M crystal system clock、24M crystal system clock);也可以由IC内部24M RC Oscillator处理后得到24M RC clock、32M RC clock、48M RC clock等。这一类我们称为RC clock(BLE SDK不支持RC clock)。

在BLE SDK中,我们推荐使用crystal clock。

初始化时调用下面的API配置system clock,在枚举变量SYS_CLK_TYPEDEF定义中选择时钟对应的clock即可。

void clock_init(SYS_CLK_TypeDef SYS_CLK)

由于B85m系列芯片的 System Timer与system clock不一样,用户需要了解MCU上各硬件模块的clock是来源于system clock还是System Timer。我们以system clock为24M crystal的情况来进行说明,此时system clock为24M,而System Timer是16M。

在文件app_config.h中的system clock定义如下:

#define CLOCK_SYS_CLOCK_HZ                  24000000

在文件app_common.h中对应的s、ms、us的定义如下:

enum{
        CLOCK_SYS_CLOCK_1S = CLOCK_SYS_CLOCK_HZ,
        CLOCK_SYS_CLOCK_1MS = (CLOCK_SYS_CLOCK_1S / 1000),
        CLOCK_SYS_CLOCK_1US = (CLOCK_SYS_CLOCK_1S / 1000000),
};

所有时钟源为system clock的硬件模块,在设置模块的clock时,只能使用上面CLOCK_SYS_CLOCK_HZ、CLOCK_SYS_CLOCK_1S等;换言之,如果用户看到模块中clock的设置使用的是以上几个定义,说明该模块的时钟源为system clock。

如PWM驱动中PWM周期和占空的设置如下,说明PWM的时钟源是system clock。

pwm_set_cycle_and_duty(PWM_ID, 1000 * CLOCK_SYS_CLOCK_1US, 500 * CLOCK_SYS_CLOCK_1US);

System Timer是固定的16M,所以对于这个timer,SDK code中使用如下的数值来表示s、ms和us。

/**
 * @brief   system Timer : 16Mhz, Constant
 */
enum{
    CLOCK_16M_SYS_TIMER_CLK_1S  = 16*1000*1000,
    CLOCK_16M_SYS_TIMER_CLK_1MS = 16*1000,
    CLOCK_16M_SYS_TIMER_CLK_1US = 16,
};

SDK中以下几个API都是跟System Timer相关的一些操作,所以涉及到这些API操作时,都使用上面类似”CLOCK_16M_SYS_TIMER_CLK_xxx”的方式来表示时间。

void sleep_us(unsigned long us);
unsigned long clock_time(void);
unsigned int clock_time_exceed(unsigned int ref, unsigned int us);
#define ClockTime           clock_time
#define WaitUs              sleep_us
#define WaitMs(t)           sleep_us((t)*1000)
#define sleep_ms(t)         sleep_us((t)*1000)

由于System Timer是BLE计时的基准,SDK中所有BLE时间相关的参数和变量,在涉及到时间的表达时,都是用”CLOCK_16M_SYS_TIMER_CLK_xxx”的方式。

System Timer的使用

main函数中sys_init初始化完成后,System Timer就开始工作,用户可以读取System Timer计数器的值(简称System Timer tick)。

System Timer tick每一个时钟周期加一,其长度为32bit,即每1/16us加1,最小值0x00000000,最大值0xffffffff。System Timer刚启动的时候,tick值为0,到最大值0xffffffff需要的时间为:(1/16) us * (2^32) 约等于268秒,每过268秒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 * CLOCK_16M_SYS_TIMER_CLK_1MS)

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

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

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

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

应用举例:A条件触发(只会触发一次)的2秒后,程序进行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();
        }   
}

GPIO模块

GPIO模块的说明请user对照drivers/8258/gpio.h、gpio_default.h、gpio.c来理解,所有代码都以源码形式提供。

代码中涉及到对寄存器的操作,请参考文档《gpio_lookuptable》来理解。

GPIO定义

B85m 系列芯片共有5组36个GPIO,分别为:GPIO_PA0 ~ GPIO_PA7、GPIO_PB0 ~ GPIO_PB7、GPIO_PC0 ~ GPIO_PC7、GPIO_PD0 ~ GPIO_PD7、GPIO_PE0 ~ GPIO_PE3

注意:

  • IC的core部分都是有这36个GPIO,但具体到每一颗IC的不同封装上,可能有些GPIO并没有引出,所以使用GPIO时请以IC实际封装出来的GPIO管脚为准。

程序中需要使用GPIO时,必须按照上面的写法定义,详情见drivers/8258/gpio.h。

注意:

7个GPIO比较特殊,需要注意:

1) MSPI的4个GPIO。这4个GPIO是MCU 系统总线中主SPI总线,用于读写flash操作,上电默认为spi状态,user永远不能操作它们,程序中不能使用。这个4个GPIO为PE0、PE1、PE2、PE3。

2) SWS (Single Wire Slave),用于debug和烧写firmware,上电默认为SWS状态,程序中一般不使用。B85m的SWS管脚为PA7。

3) DM和DP,上电默认为GPIO状态。当需要USB功能时,DM和DP需要使用;当不需要USB时,可以作为GPIO使用。B85m的DM、DP管脚为 PA5、PA6。

GPIO状态控制

这里只列举用户需要了解的最基本的GPIO状态。

1) func(功能配置:特殊功能/一般GPIO),如需要使用输入输出功能,需配置为一般GPIO。

void gpio_set_func(GPIO_PinTypeDef pin, GPIO_FuncTypeDef func);

pin为GPIO定义,以下一样。func可选择AS_GPIO或其他特殊功能。

2) ie(input enable):输入使能

void gpio_set_input_en(GPIO_PinTypeDef pin, unsigned int value);

value: 1和0分别表示enable和disable。

3) datai (data input):当输入使能打开时,该值为当前该GPIO管脚的电平,用于读取外部电压。

_Bool gpio_read(GPIO_PinTypeDef pin);

读到低电压返回值为0;读到高电压,返回1。

但是这里要非常注意,为了避免读到高电平时,返回值不一定是1的情况,在程序中,不能使用类似 if( gpio_read(GPIO_PA0) == 1)的写法,推荐使用方法是将读到的值取反处理,取反后只有1和0两种情况:

if( !gpio_read(GPIO_PA0) )  //判断高低电平

4) oe(output enable):输出使能

void gpio_set_output_en(GPIO_PinTypeDef pin, unsigned int value);

value 1和0分别表示enable和disable。

5) dataO (data output):当输出使能打开时,该值为1输出高电平,为0输出低电平。

void gpio_write(GPIO_PinTypeDef pin, unsigned int value);

6) 内部模拟上下拉电阻配置:有1M上拉、10K上拉、100K下拉3种模拟电阻,可配置的状态有4种:1M上拉、10K上拉、100K下拉和float状态。

void gpio_setup_up_down_resistor(GPIO_PinTypeDef gpio, GPIO_PullTypeDef up_down);

up_down的四种配置为:

typedef enum {
    PM_PIN_UP_DOWN_FLOAT    = 0,
    PM_PIN_PULLUP_1M        = 1,
    PM_PIN_PULLDOWN_100K    = 2,
    PM_PIN_PULLUP_10K   = 3,
}GPIO_PullTypeDef;

在 deepsleep和deepsleep retention状态下,GPIO的输入输出状态全部失效,但是模拟上下拉电阻仍然有效。

注意:

GPIO在进行功能改变配置的时候,需要按照如下顺序:

(A)开始就是GPIO功能,那么需要先将所需要的功能MUX配置好后,最后再将GPIO功能disable。

(B)开始是功能IO,需要改成GPIO output,先将对应的IO的output值和OEN设置对后,最后enable GPIO功能。

(C)开始功能是IO, 需要改成GPIO input并且IO上拉,先将output设置成1,OEN设置为1(对应PA和PD),其次将pullup设置为1(对应PB和PC), 最后enable GPIO功能。

(D)将pullup设置为1(对应PB和PC)并且IO不上拉,先将output设置成0,OEN设置为1(对应PA和PD),其次将pullup设置为0(对应PB和PC), 最后enable GPIO功能。

GPIO配置应用举例:

1) 将GPIO_PA4配置为输出态,并输出高电平。

gpio_set_func(GPIO_PA4, AS_GPIO) ;  // PA4默认为GPIO功能,可以不设置
gpio_set_input_en(GPIO_PA4, 0);
gpio_set_output_en(GPIO_PA4, 1);
gpio_write(GPIO_PA4, 1);

2) 将GPIO_PC6配置为输入态,判断是否读到低电平,需要开启上拉,防止float电平的影响。

gpio_set_func(GPIO_PC6, AS_GPIO) ;  // PC6默认为GPIO功能,可以不设置
gpio_setup_up_down_resistor(GPIO_PC6, PM_PIN_PULLUP_10K);
gpio_set_input_en(GPIO_PC6, 1);
gpio_set_output_en(GPIO_PC6, 0);
if(!gpio_read(GPIO_PC6)){  //是否低电平
    ......
}

3) 将PA5、PA6脚配置成USB功能。

gpio_set_func(GPIO_PA5, AS_USB ) ;
gpio_set_func(GPIO_PA6, AS_USB) ;
gpio_set_input_en(GPIO_PA5, 1);
gpio_set_input_en(GPIO_PA6, 1);

GPIO的初始化

main.c中调用gpio_init函数,会将除了MSPI 4个GPIO以外的其他32个GPIO的状态都初始化一遍。

当用户的app_config.h中没有配置GPIO参数时,这个函数会将每个IO初始化为默认状态。32个GPIO默认状态为:

1) func

除了SWS ,其他均为一般GPIO状态。

2) ie

除了SWS默认ie为1,其他所有的一般GPIO默认ie为0。

3) oe

全部为0。

4) dataO

全部为0。

5) 内部上下拉电阻配置

全部为float。

更多详情请参照drivers/8258/gpio.h、drivers/8258/gpio_default.h和drivers/8278/gpio.h、drivers/8278/gpio_default.h。

如果在app_config.h中有配置到某个或某几个GPIO的状态,那么gpio_init时不再使用默认状态,而是使用用户在app_config.h配置的状态。原因是gpio的默认状态是使用宏来表示的,这些宏的写法为(以PA0的ie为例):

#ifndef PA0_INPUT_ENABLE
#define PA0_INPUT_ENABLE        1
#endif

当在app_config.h中可以提前定义这些宏,这些宏就不再使用以上这种默认值。

在app_config.h中配置GPIO状态方法为(以PA0为例):

1) 配置func:

#define PA0_FUNC                  AS_GPIO

2) 配置ie:

#define PA0_INPUT_ENABLE           1

3) 配置oe:

#define PA0_OUTPUT_ENABLE          0

4) 配置dataO:

#define PA0_DATA_OUT               0

5) 配置内部上下拉电阻:

#define  PULL_WAKEUP_SRC_PA0              PM_PIN_UP_DOWN_FLOAT

GPIO的初始化总结:

1) 可以提前在app_config.h中定义GPIO的初始状态,在gpio_init中得以设定;

2) 可以在user_init函数中通过GPIO状态控制函数(gpio_set_input_en等)加以设定;

3) 也可以使用以上两种方式混用:在app_config.h中提前定义一些,gpio_init加以执行,在user_init中设定另外一些。

注意:

  • 在app_config.h中定义和user_init中设定同一个GPIO的某个状态为不同的值时,根据程序的先后执行顺序,最终以user_init中设定为准。

gpio_init函数的实现如下,anaRes_init_en的值决定模拟上下拉电阻是否被设置。

void gpio_init(int anaRes_init_en)
{
    // gpio digital status setting
    if(anaRes_init_en){
        gpio_analog_resistance_init();
    }
}

参考文档后面低功耗管理的介绍可知,控制GPIO模拟上下拉电阻的寄存器在deepsleep retention期间是可以保持不掉电的,所以GPIO模拟上下拉电阻的状态能在deepsleep retention mode下被维持住。

为了确保deepsleep retention唤醒之后GPIO模拟上下拉电阻的状态不被改变,在gpio_init前先要判断当前是否被deepsleep retention wake_up,根据这个状态去设置anaRes_init_en的值,如下面的code所示:

int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();
    gpio_init( !deepRetWakeUp );

GPIO数字状态在deepsleep retention mode失效

上面介绍的GPIO状态控制中,除了模拟上下拉电阻是由模拟寄存器(analog register)控制,其他所有的状态(func、ie、oe、dataO等)都是被数字寄存器(digital register)控制的。

参考文档后面低功耗管理的介绍可知,deepsleep retention期间所有digital register的状态掉电丢失。

在Telink上一代826x系列IC上,suspend期间可以用GPIO output来控制一些外围设备,但到了B85m上,如果suspend被切换为deepsleep retention mode后,GPIO output状态失效,无法在sleep期间准确地控制外围设备。此时可以使用GPIO模拟上下拉电阻的状态来代替实现:上拉10K代替GPIO output high,下拉100K代替GPIO output low。

注意:

  • deepsleep retention期间的GPIO状态控制不要使用上拉1M(上拉电压可能会比供电电压VCC低一些)。另外,上拉10K的控制中不要使用PC0~PC7的上拉10K(在deepsleep retention wake_up时会有短时间的抖动,产生毛刺),其他GPIO上拉10K都是可以的。

配置SWS上拉防止死机

Telink所有的MCU都使用SWS(single wire slave)来调试和烧录程序。在最终的应用代码上,SWS这个pin的状态为:

1) function上设为SWS,非GPIO。

2) ie =1,只有input enable时,才可以收到EVK发的各种命令,用来操作MCU。

3) 其他的配置:oe、dataO都为0。

设为以上状态后,可以随时接收EVK的操作命令,但同时也带来一个风险:当整个系统的电源抖动很厉害的时候(如发送红外时,瞬间电流可能会冲到接近100mA),由于SWS处于float状态,可能会读到一个错误的数据,误以为是EVK发来的命令,这个错误的命令可能会导致程序挂掉。

解决上面问题的方法是,将SWS的float状态修改为输入上拉。通过模拟上拉1M电阻来解决。

B85m的SWS和GPIO_PA7复用,在drivers/8258/gpio_default.h中将PA7的1M上拉开启即可:

#ifndef PULL_WAKEUP_SRC_PA7
#define PULL_WAKEUP_SRC_PA7     PM_PIN_PULLUP_1M  //sws pullup
#endif

系统中断

该文档适用IC的硬件中断具有以下两个特点:

(1)所有中断的优先级一样,MCU不具备中断嵌套的能力。

(2)所有中断共用同一个中断硬件入口,最终会触发软件irq_handler函数,在该函数里面通过读取相关中断的状态位来判断对应中断是否被触发。

上面的特点1决定了MCU响应中断时是按照先来先处理的方式。当第一个中断未处理完时,新的中断产生,无法被立刻响应,进入等待队列,直到前面的中断处理完毕后才会响应。所以,当有2个或2个以上的中断时,所有的中断都无法做到实时响应,某一个中断响应延时的时间,取决于这个中断触发时MCU是否正在处理其他中断以及处理其他中断花费的时间。如下图所示,由于IRQ2触发时IRQ1正在处理,必须等到IRQ1处理结束后才能响应,IRQ 2 delay的时间最坏情况就是IRQ1 process的最大时间。

"IRQ delay"

在BLE SDK中,使用了system timer和RF两个系统中断。如果用户没有增加新的中断,则不用考虑两个系统中断的时间问题;如果客户需要增加其他中断(比如UART、PWM等),需要考虑的细节如下:

(1) SDK中system timer和RF两个系统中断,可能的最大执行时间是200us。这意味着客户增加的中断可能无法实时响应,且理论上最大可能出现的延迟时间为200us。

(2) system timer和RF两个系统中断是为了处理BLE任务,由于BLE的时序较为严格,不能被延时太久。所以客户增加的中断,处理时间不能过长,建议在50us以内。如果时间过长,可能出现BLE时序同步出错,造成收发包效率低、功耗偏高、BLE断连等问题。

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。

(1) Telink BLE controller

Telink BLE SDK支持标准的BLE controller,包括HCI、PHY(Physical Layer)和LL(link layer)。

Telink BLE SDK包含Link Layer的五种标准状态(standby、advertising、scanning、initiating、connection),conneciton状态下的Slave role和Master role也都支持。B85m BLE Single Connection SDK中的Slave role和Master role都只是single connection,即Link Layer只能维持一个连接,无法同时多个Slave/Master或者Slave和Master同时存在。

SDK 上的B85m hci是一个BLE slave的controller,需要和另外一个运行BLE Host的MCU协调工作形成一个标准的BLE Slave系统,架构图如下。

"Telink HCI架构"

Link Layer connection状态同时支持single connection的Slave和Master,那么B85m hci实际也可以作为BLE master controller使用,但实际对于一个BLE host运行在较复杂的系统(如linux/android)来说,单一连接的Master controller只能连接一个设备,几乎是没有意义的,所以SDK在B85m hci上并没有将master role的初始化放进去。

(2) Telink BLE Slave

Telink BLE SDK在BLE host上,只对Slave部分的stack完全支持;对于Master无法做到完全支持,因为SDP(service discovery)太复杂。

当user只需要使用标准的BLE slave时,Telink BLE SDK运行Host(slave部分) + 标准的Controller,实际的协议栈架构会对上面标准的结构做一些简化处理,使得整个SDK的系统资源开销(包括SRAM、运行时间、功耗等)最小,其架构如下图所示。SDK中B85m ble sample、B85m remote、B85m 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等。

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

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

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

(3) Telink BLE master

Telink BLE master的实现方式不同于Slave,SDK上提供标准的controller封装到library里面,但在app层实现host和user自己的application,如下图所示。

"Telink BLE master架构"

SDK中B85m master kma dongle工程demo code是基于该架构实现的,host层的代码几乎都在app中实现了,SDK提供各种标准的interface供user去完成这些功能。

App层中实现了标准的l2cap、att等的处理,在SMP部分只提供了最基本的just work方式,B85m master kma dongle默认SMP是disable的,需要user自己打开这个宏才能enable SMP。由于SMP的实现比较复杂,具体的code实现还是封装在library里面,app层只需要调用相关的interface即可,user搜索BLE_HOST_SMP_ENABLE即可找到所有的code处理。

#define BLE_HOST_SMP_ENABLE                         0  
//1 for standard security management, 
// 0 for telink referenced paring&bonding(no security)

SDP是最复杂的部分,Telink BLE master不提供标准的SDP,只给出了一个简单的参考,是对B85m remote的service discovery。B85m master kma dongle默认该simple reference SDP是打开的。

#define BLE_HOST_SIMPLE_SDP_ENABLE    1  //simple service discovery

SDK提供所有的service discovery相关ATT操作的标准interface,user可以参考B85m remote的service discovery去实现自己的service discovery,或者将BLE_HOST_SIMPLE_SDP_ENABLE disable,使用和slave约定好的service ATT handle来实现数据访问。

Telink BLE master不支持Power Management,因为对Link Layer的scannning和connection master role没有做suspend处理。

BLE Controller

BLE Controller介绍

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

Telink BLE SDK对Physical Layer(对应driver文件中的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。将Conneciton状态区分为Slave Role和Master Role。

由文档前面对library的介绍,目前的Slave Role和Master Role都是基于single connection的设计,Slave Role默认为single connection的;Master Role目前提供的是single connection的,但由于将来会提供multi connection的,所以这里名为Master role single conection,和将来的Mater Role multi 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/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);

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

void        blc_ll_initAdvertising_module(u8 *public_adr);
void        blc_ll_initScanning_module(u8 *public_adr);
void        blc_ll_initInitiating_module(void);
void        blc_ll_initConnection_module(void);
void        blc_ll_initSlaveRole_module(void);
void        blc_ll_initMasterRoleSingleConn_module(void);

上面API中实参public_adr都是BLE public mac address的指针。

其中以下API用于初始化master和slave共用的module:

void        blc_ll_initConnection_module(void);

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_initAdvertising_module(mac_public);

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

(3) Idle + Scanning

如下图所示,只初始化Idle和Scanning模块,使用最基本的Scanning功能实现beacon等产品广播信息的扫描发现。

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

u8  mac_public[6] = {……};
blc_ll_initBasicMCU();
blc_ll_initStandby_module(tbl_mac);
blc_ll_initScanning_module( tbl_mac); 

Idle和Scanning状态的切换通过blc_ll_setScanEnable来实现。

"Idle + Scanning"

(4) Idle + Advertising + ConnSlaveRole

"BLE Slave LL State"

上图所示为一个基本的BLE slave应用的Link Layer状态机组合,SDK中b85m hci/b85m ble sample/b85m remote/b85m module都是基于该状态机组合。

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

u8  mac_public[6] = {……};
blc_ll_initBasicMCU();                     
blc_ll_initStandby_module(mac_public);
blc_ll_initAdvertising_module(mac_public); 
blc_ll_initConnection_module();
blc_ll_initSlaveRole_module();

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

(a) 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里面将Adv Enable,进入Advertising state。

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

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

(d) 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时设置的值。

(5) Idle + Scanning + Initiating + ConnMasterRole

"BLE Master LL State"

上图所示为一个基本的BLE master应用的Link Layer状态机组合,SDK中B85m master kma dongle是基于该状态机组合。Link Layer状态机模块初始化代码为:

u8  mac_public [6] = {……};
blc_ll_initBasicMCU();
blc_ll_initStandby_module(mac_public);
blc_ll_initScanning_module(mac_public); 
blc_ll_initInitiating_module();
blc_ll_initConnection_module();
blc_ll_initMasterRoleSingleConn_module();

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

1) MCU上电后,进入Idle state。Idle state时将Scan Enable,Link Layer切换到Scanning State;在Scanning State时将Scan Disable时,回到Idle state。

Scan Enable和Disable通过API blc_ll_setScanEnable来控制。

上电后,Link layer默认处于Idle state,一般需要在user_init里面将Scan Enable,进入Scanning state。

Link Layer处于Scanning state时,会将Scan到的adv packet通过event “HCI_SUB_EVT_LE_ADVERTISING _REPORT” report给BLE host。

2) Link Layer可以在Idle state和Scanning state通过API blc_ll_createConnection触发进入Initiating state。

blc_ll_createConnection指定了需要连接的一个或多个BLE设备的mac address。Link Layer进入Initiating state后不断Scan指定的BLE设备,在收到某个正确的可连接的adv packet后发送connection request并进入ConnMasterRole。若在一定时间内Initiating state没有Scan到指定的BLE设备,无法发起连接,将会触发create connection timeout,重新回到Idle State或者Scanning state。

注意:

  • Initiating state可以从Idle state和Scanning state进入(B85m master kma dongle就是从Scanning state直接进入),create connection timeout后回到create connection之前的Idle State或者Scanning state。

3) Link Layer处于ConnMasterRole时,有三种情况会回到Idle State:

a) slave向master 发送terminate命令,断开连接。master收到terminate命令,退出ConnMasterRole。

b) master向slave发送terminate命令,主动断开连接,退出ConnMasterRole。

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

Link layer的ConnMasterRole退出该state时,只能回到Idle state。若需要继续Scan的话必须使用API blc_ll_setScanEnable设置Link Layer再次进入Scanning state。

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

_attribute_ram_code_ void 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 reqeust,收这个包后和master建立ble连接,进入Connection 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中完成该动作,其执行时间非常短。

Scanning 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) Initiating State时序

"Initiating State时序"

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

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

Scanning state中BLE controller将收到的adv packet report给BLE host,而Initiating state不会report adv给BLE host,它只是Scan到由blc_ll_createConnection指定的设备后,发送connection_request并建立连接,然后Link Layer进入ConnMasterRole。

(5) 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个阶段:

(a) brx start阶段

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

(b) brx working阶段

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

(c) brx post阶段

收发包完成后,brx working结束,同样由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对数据包的处理包括:

(a) 数据包的解密

(b) 数据包的解析

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

(6) Conn State Master Role时序

"Conn Master Role时序"

ConnMasterRole时序图如上所示。每一个conn interval开始的时候,Link Layer进行一次BLE的RF发收包过程:先让PHY进入发包状态,向slave发送一个包后等待对方的ack包,若有more data,则继续向slave发包,这个过程简称为btx event。

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

1) btx start阶段

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

2) btx working 阶段

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

3) btx post阶段

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

上面三个阶段中btx start和btx post都是中断完成,而btx working阶段不需要软件的参与,此时UI task可以正常执行。

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

blt_sdk_main_loop对数据包的处理包括:

1) 数据包的解密

2) 数据包的解析

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

(7) Conn State Slave role时序保护

ConnSlaveRole,每个interval需要一个收发包事件,也就是上面的Brx Event。B85m SDK中,Brx Event完全是由中断触发的,所以MCU系统总中断需要一直被打开。如果user在Conn state的一些任务时间较长且必须把系统总中断关闭(比如擦除Flash),就会造成Brx Event被停掉,BLE的时序很快就乱掉,最终连接断开。 对于这种情况SDK中提供了一套保护机制,让user去停掉Brx Event却不破坏BLE的时序,user需要严格根据这个机制来操作。相关API如下:

    int     bls_ll_requestConnBrxEventDisable(void);
    void    bls_ll_disableConnBrxEvent(void);
    void    bls_ll_restoreConnBrxEvent(void);

调用bls_ll_requestConnBrxEventDisable来申请关掉Brx Event。

1) 该API返回值若为0,表示当前不接受用户的申请,即此时不能停掉Brx Event。在Conn state时的Brx working阶段,不能接受申请,返回值为0,一定要等到一个完整的Brx Event结束后,在剩余的UI task/suspend时间内才会接受申请。

2) 该API返回非0值表示可以接受申请,返回的值是允许停掉Brx Event的时间,单位为ms。该事件值有三种情况:

A. 若当前Link Layer为Advertising state或Idle state,返回值为0xffff,即没有Brx Event,用户关闭系统中断的时间随便多长都可以。
B. 若当前为Conn state,收到了master的update map或update connection parameter且还没有到更新的时间点时,返回时间为更新的时间点减去当前时间。即停掉Brx Event的时间不能超过更新的时间点,否则会造成后面所有包收不到,最终断开连接。
C. 若当前为Conn state,且没有master的更新请求,返回值为当前connection supervision timeout值的一半。比如当前timeout为1S,返回值为500ms。

user调用上面的API申请停掉Brx Event,若返回值对应的时间(ms),足够自己的任务运行时间,即可进行该任务。在该任务执行之前,调用API bls_ll_disableConnBrxEvent停掉Brx Event。任务结束后,调用API bls_ll_restoreConnBrxEvent重新开启Brx Event并修复BLE时序。

参考使用方法如下。其中具体时间的判断,以测试到的实际任务时间为准。

"Scanning in Advertising state时序"

上面BLE Link Layer状态机和工作时序介绍了最基本的几种状态,能够满足BLE slave/master等基本应用。 但是考虑到user可能会有的一些特殊的应用(比如在Conn state Slave role时还要能够advertising),Telink BLE SDK对Link Layer的状态机添加了一些特殊的扩展的功能,下面详细描述。

(1) Scanning in Advertising state

在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的使用请参考B85m_feature_test中的TEST_SCANNING_IN_ADV_AND_CONN _SLAVE_ROLE。

(2) Scanning in ConnSlaveRole

在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时序"

当前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的使用请参考B85m_feature_test中的TEST_SCANNING_IN_ADV_AND_CONN_ SLAVE_ROLE。

(3) Advertising in ConnSlaveRole

在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的使用请参考B85m_feature_test中的TEST_ADVERTISING_IN_CONN_SLAVE_ROLE。

(4) Advertising and Scanning in ConnSlaveRole

结合上面Scanning in ConnSlaveRole和Advertising in ConnSlaveRole的使用,可以在ConnSlaveRole中同时加入Scanning和Advertising。时序图如下:

"Advertising and Scanning in ConnSlaveRole时序"

当前Link Layer还是处于ConnSlaveRole(BLS_LINK_STATE_CONN),在每个Conn interval内brx event结束后,立刻执行一次adv event,然后剩余的时间用来做Scanning。

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

Advertising and Scanning in ConnSlaveRole的使用请参考B85m_feature_test中的TEST_ADVERTISING_ SCANNING_IN_CONN_SLAVE_ROLE。

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

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

Slave role和Master role的BLE TX fifo和BLE RX fifo的处理方式一致,BLE TX fifo和BLE RX fifo都在应用层定义:

MYFIFO_INIT(blt_rxfifo, 64, 8);
MYFIFO_INIT(blt_txfifo, 40, 16);

其中RX fifo size默认为64,TX fifo size默认为40,除非需要使用data length extension,否则不允许修改这两个size。

不管是TX fifo number还是RX fifo number,必须设置为2的幂,即2、4、8、16等值。User可以根据自己的需要稍作修改。

RX fifo number默认为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由于brx event Rx IRQ将往Rx fifo填数据,但是不做数据溢出检查,如果main_loop处理RX fifo太慢就会导致溢出问题,所以在使用旧SDK时,用户需要更多的注意这个风险,避免master在一个连接间隔上发太多的数据,注意用户UI task处理时间尽量短,避免阻塞问题。)

目前SDK新增了Rx overflow校验:在brx Rx IRQ中检查当前的RX fifo写指针和读指针差值是否大于Rx fifo数量,一旦发现Rx fifo被占满则让RF不去ACK对方,BLE协议会确保数据重传,此外SDK还提供了Rx overflow回调函数以便通知用户,文档后面章节“Telink defined event”会介绍这个回调。

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

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

如果设置太大(如32)会占用过多的SRAM。

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

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

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

    u8          blc_ll_getTxFifoNumber (void);

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

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

在B85m remote的语音处理中,由于已知每笔语音数据会被拆成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);

其中参数num可以设置的more data数量最大不要超过RX fifo number。

注意:

  • 需要注意的是,只有在应用层调用该API(参数num大于0)才开启限定一个连接事件上more data数据的功能。

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(b85m remote/b85m module等)上有效,即:

  • 对于BLE master,只有HCI event;

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

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

User可以根据自己的需要,在这两套event挑选一套使用,也可以两套同时使用。Telink BLE SDK中, b85m hci/b85m master kma dongle 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中定义的一致。User可参考b85m_master kma dongle中如下用法以及controller_event_callback函数的具体实现。

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 and 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,subevent code最常用的且user可能需要了解的如下,其他的不做介绍。

#define HCI_SUB_EVT_LE_CONNECTION_COMPLETE                          0x01
#define HCI_SUB_EVT_LE_ADVERTISING_REPORT                           0x02
#define HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE                   0x03
#define HCI_SUB_EVT_LE_CONNECTION_ESTABLISH                         0x20  

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"

注意:Teink 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"

d) HCI_SUB_EVT_LE_CONNECTION_ESTABLISH

HCI_SUB_EVT_LE_CONNECTION_ESTABLISH是对HCI_SUB_EVT_LE_CONNECTION_COMPLETE的补充,除了subevent不一致外,其他所有参数相同。SDK中b85m_master kma dongle使用了该event。该event是唯一的非BLE spec标准的event,属于Telink 私有定义,且只在b85m_master kma dongle中使用。

下面详细说明Telink定义该event的原因。

BLE Controller在Initiating state时,扫描到指定需要连接的device adv packet时,向它发送connection request包,此时不管对方是否有收到这个connection request,都会无条件认为Connection complete,向Host上报LE Connection Complete Event,Link Layer迅速进入Master role。由于这个包是不带ack/retry机制的,无法保证Slave一定会收到,如果Slave 丢掉这个connection request,就无法进入Slave role,后面也不会进入brx模式收发包。当这种情况发生时,Master Controller这边的处理机制是:进入Master role后,检查一下前6 ~ 10个conn interval上有没有收到任何slave的包(此时不关心CRC是否正确)。

  • 如果一个包都没收到,那么认为是Slave没有收到connection request,在前面已经上报了 LE Connection Complete Event的前提下,必须快速上报一个Disconnection Complete Event,并指出disconnect reason是0x3E(HCI_ERR_CONN_FAILED_TO_ESTABLISH)。

  • 在前6 ~ 10个conn interval上有收到Slave的包,才能确定Connection Established(连接已确立),Master后面的流程才能继续往下进行。

根据上面的描述,BLE Host的处理方法应该是:在收到Controller的LE Connection Complete Event后,不能认为connection已经Established,必须根据conn interval启动一个timer(时间设置大一些,10个interval以上, 覆盖住最长时间),在这个timer之内检查是否有disconnect reason为0x3E Disconnection Complete Event,如果没有的话才能认为connection Established。

鉴于BLE host的这个处理非常的复杂,很容易出错,所以SDK在底层定义了HCI_SUB_EVT_LE_CONNECTION _ESTABLISH,当Host收到这个event时,就表明Controller已经确定Slave端connection OK了,可以继续下面的流程。

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_ABANDOM         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

前面已经介绍,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中绝大多数都是用的这种方式;只有b85m_ module使用了方式2 “shared event entry”。

在Telink defined event的使用上,方式2 “shared event entry”请参考project “b85m_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 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(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"

a)如果使用V3.4.2.2 SDK 的用户可以参考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;
b)如果使用V3.4.2.4 SDK 的用户可以参考controller.h中tlk_contr_evt_connect_t的定义,连接请求包单元PDU从该结构体的InitA [6]开始,到hop结束。
typedef struct{
    u8  initA[6];           
    u8  advA[6];            
    u32 accessCode;         
    u8  crcinit[3];
    u8  winSize;
    u16 winOffset;
    u16 connReq_interval;   
    u16 connReq_latency;    
    u16 connReq_timeout;    
    u8  chm_map[5];         
    u8  hop_sca;            
}tlk_contr_evt_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_EXT_IND),此时触发该事件。

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

数据长度n:1。

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

7) BLT_EV_FLAG_RX_DATA_ABANDOM

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

回传指针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 wakeup时间点之前被GPIO提前唤醒时,触发BLT_EV_FLAG_GPIO _EARLY_WAKEUP事件。

数据长度n:1。

回传指针p:指向一个u8型变量wakeup_status,wakeup_status变量记录了当前suspend哪些唤醒源状态生效了,由drivers/8258(8278)/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类型的bltc.conn_chn_map[5],注意回调函数执行时p指向的bltc.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,以1.25ms为unit

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

p[4] | p[5]<<8: new connection timeout,以10ms为unit

15) BLT_EV_FLAG_SUSPEND_ENETR

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

回传指针p:空指针NULL。

数据长度n: 0。

16) BLT_EV_FLAG_SUSPEND_EXIT

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

数据长度n: 1。

回传指针p:指向一个u8型变量wakeup_status,wakeup_status变量记录了当前suspend哪些唤醒源状态生效了,由drivers/8258(8278)/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),
};

注意:

  • 实际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功能,按如下步骤设置。

Step 1 设置合适的TX & RX fifo size

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

发长包需要加大TX fifo size。TX fifo size至少比TX rf_len大12,且必须按4字节对齐。

TX rf_len = 56 bytes: MYFIFO_INIT(blt_txfifo, 68, 8);

TX rf_len = 141 bytes: MYFIFO_INIT(blt_txfifo, 156, 8);

TX rf_len = 191 bytes: MYFIFO_INIT(blt_txfifo, 204, 8);

收长包需要加大RX fifo size。RX fifo size至少比RX rf_len大24,且必须按16字节对齐。

RX rf_len = 56 bytes: MYFIFO_INIT(blt_rxfifo, 80, 8);

RX rf_len = 141 bytes: MYFIFO_INIT(blt_rxfifo, 176, 8);

RX rf_len = 191 bytes: MYFIFO_INIT(blt_rxfifo, 224, 8);

Step 2 设置合适的MTU Size

MTU即最大传输单元用于设置BLE中的L2CAP层单个数据包中的payload大小,att.h中提供了接口函数ble_sts_t blc_att_setRxMtuSize(u16 mtu_size);在BLE协议栈初始化时,用户可直接使用该函数进行传参设定MTU大小,但是MTU size有效值是在MTU exchange流程中协商后得到的,MTU size effect =min(MTU_A, MTU_B) MTU_A为设备A所支持的MTU大小,MTU_B为设备B所支持的MTU大小;另外只有MTU size大于DLE长度才能充分利用DLE的作用来增加链路层数据吞吐率。

MTU size exchange的实现,请参考本文档" ATT & GATT”部分的详细说明,也可以参考b85m_feature_test里面DLE demo的写法。

#define MTU_SIZE_SETTING                    196
blc_att_setRxMtuSize(MTU_SIZE_SETTING);

MTU大于247时,用户可以用以下API注册buffer:

ble_sts_t   blc_l2cap_initMtuBuffer(u8 *pMTU_rx_buff, u16 mtu_rx_size, u8 *pMTU_tx_buff, u16 mtu_tx_size);

Step 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包长。

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

ble_sts_t       blc_ll_exchangeDataLength (u8 opcode, u16 maxTxOct);

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

blc_ll_exchangeDataLength(LL_LENGTH_REQ , 200);

由于slave设备并不知道master会不会主动发送LL_LENGTH_REQ,推荐一个参考的方法:注册BLT_EV_FLAG_DATA_LENGTH_EXCHANGE事件回调,当connection建立后开启一个软件定时器开始计时(比如2S),如果在2S后这个回调一直没有触发,就说明master还没有主动发起LL_LENGTH_REQ,此时slave调用API blc_ll_exchangeDataLength主动发起LL_LENGTH_REQ。

Step 4 MTU size exchange

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

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

Controller API

(1) Controller API说明

在3.1.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最终采用了如上图的架构,应用层跨越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中,是分情况处理的。

1) 对于b85m_hci类的应用,Telink的IC只作为BLE controller,需要和其他家的运行BLE host的MCU协同工作,每个HCI command都会有对应的HCI command complete event或HCI command status event产生。

2) 对于b85m_master kma dongle应用,BLE Host和Controller都运行在Telink IC上,Host调用interface发送HCI command给Controller时,Controller全部都能正确收到,不会有丢失的情况,所以Controller在处理HCI command时不再回复HCI command complete event或HCI command status event。

Controller API的声明在stack/ble/ll和stack/ble/hci目录下的头文件中,其中ll目录下根据Link Layer状态机功能的分类分为ll.h、ll_adv.h、ll_scan.h、ll_ext_adv.h、ll_pm.h、ll_whitelist.h、ll_init.h、ll_resolvist.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) BLE 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的地址即可,参考文档前面的介绍,B85m 512K flash上对应的这个地址为0x76000。如果不需要random static address,上面的mac_random_static填“NULL”即可。

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

blc_ll_initStandby_module(mac_public);

如果用到Link Layer的状态机中的Advertising state或Scanning state,也需要将MAC address传入:

blc_ll_initAdvertising_modulemac_public;
blc_ll_initScanning_modulemac_public;

(4) Link Layer 状态机初始化

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

void        blc_ll_initBasicMCU (void)
void        blc_ll_initStandby_module (u8 *public_adr);
void        blc_ll_initAdvertising_module(u8 *public_adr);
void        blc_ll_initScanning_module(u8 *public_adr);
void        blc_ll_initInitiating_module(void);
void        blc_ll_initSlaveRole_module(void);
void        blc_ll_initMasterRoleSingleConn_module(void);

(5) bls_ll_setAdvData

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

"BLE协议栈广播包格式"

BLE协议栈里,广播包的格式如上图所示,前两个byte是head,后面是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 Specifcation的具体说明。

const u8    tbl_advData[] = {
        0x08, 0x09, 'f', 'e', 'a', 't', 'u', 'r', 'e',
        0x02, 0x01, 0x05,
        0x03, 0x19, 0x80, 0x01, 
        0x05, 0x02, 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 SDK中B85m_ble remote工程中定义scan response data如下,scan设备名为"Eaglerc" 。各个字段的含义请参考文档BLE Spec《CSS v6》(Core Specification Supplement v6.0)中Data Type Specifcation的具体说明。

const u8    tbl_scanRsp [] = { 0x08, 0x09, 'V', 'R', 'e', 'm', 'o', 't', 'e',};

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

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

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

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

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

(7) 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 req和Connect Request进行响应,如:第一个Connectable Undirected Event对Scan request和Connect Request都能响应,而Non-connectable Undireted Event对它们都不响应。

注意第二个Connectable Directed Event对Connect Request响应那个"YES"右上角加了“*”号,表示它只要收到匹配的Connect Request,就一定会响应,而不会被whitelist过滤。剩下的3个"YES"表示可以响应相应的请求,但实际需要依赖于whitelist的设置,根据whitelist的过滤条件来决定最终是否响应,后面的whitelist中会详细介绍。

以上四种广播事件中,Connectable Directed Event又分为Low Duty Cycle Directed Advertising和High Duty Cycle Directed Advertising,这样一共能够得到五种广播事件类型,如下定义:

/* 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,彼此间的范围检查就无法控制。

但是考虑到user对一些常用的参数可能会经常性的去修改,而又不希望每次都要调用bls_ll_setAdvParam同时设置8个参数,SDK对其中4个不会跟其他参数有耦合关系的参数,单独封装了API,以方便user的使用。单独封装3个API如下:

ble_sts_t   bls_ll_setAdvInterval(u16 intervalMin, u16 intervalMax);
ble_sts_t   bls_ll_setAdvChannelMap(u8 adv_channelMap);
ble_sts_t   bls_ll_setAdvFilterPolicy(u8 advFilterPolicy);

这3个API的参数与bls_ll_setAdvParam中一致。

返回值ble_sts_t:

1) bls_ll_setAdvChannelMap和bls_ll_setAdvFilterPolicy会无条件返回BLE_SUCCESS。

2) bls_ll_setAdvInterval返回BLE_SUCCESS或HCI_ERR_INVALID_HCI_CMD_PARAMS。

(8) 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 en);

en为1时,Enable Advertising;en为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才会生效。

(9) 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回调。

(10) blc_ll_setAdvCustomedChannel

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

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

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

blc_ll_setAdvCustomedChannel (8, 12, 22);

(11) rf_set_power_level_index

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

void rf_set_power_level_index (RF_PowerTypeDef level)

level值的设置参考drivers/8258(8278)/rf_drv.h中定义的枚举变量RF_PowerTypeDef。

该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所示。

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

(12) blc_ll_setScanParameter

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

ble_sts_t   blc_ll_setScanParameter (u8 scan_type, 
u16 scan_interval, u16 scan_window, 
own_addr_type_t ownAddrType, 
scan_fp_type_t scanFilter_policy);

参数解析:

1) scan_type

可选择passive scan和active scan,区别是active scan会在收到adv packet基础上发scan_req以获取设备scan_rsp的更多信息,scan rsp包也会通过adv report event传给BLE Host;passive scan不发scan req。

typedef enum {
        SCAN_TYPE_PASSIVE = 0x00,
        SCAN_TYPE_ACTIVE,
} scan_type_t;

2) scan_interval/scan window

scan_interval设置Scanning state时频点切换时间,单位为0.625ms,scan_window在Telink BLE SDK中暂时没有处理,实际的scan window设置为scan_interval。

3) ownAddrType

指定scan req包地址类型时,ownAddrType 4个可选的值如下:

typedef enum{
    OWN_ADDRESS_PUBLIC = 0,
    OWN_ADDRESS_RANDOM = 1,
    OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC = 2,
    OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM = 3,
}own_addr_type_t;

OWN_ADDRESS_PUBLIC表示Scan的时候使用public MAC address,实际地址来自MAC address初始化时API blc_initMacAddress(flash_sector_mac_address, mac_public, mac_random_static)的设置。

OWN_ADDRESS_RANDOM表示Scan的时候使用random static MAC address,该地址来源于下面API设定的值:

ble_sts_t   blc_ll_setRandomAddr(u8 *randomAddr);

4) scan filter policy

目前支持的scan filter policy为下面两个:

typedef enum {
    SCAN_FP_ALLOW_ADV_ANY                       =0x00,
    SCAN_FP_ALLOW_ADV_WL                        =0x01,
    SCAN_FP_ALLOW_UNDIRECT_ADV                  =0x02,
    SCAN_FP_ALLOW_ADV_WL_DIRECT_ADV_MACTH       =0x03, 
} scan_fp_type_t;   

SCAN_FP_ALLOW_ADV_ANY表示Link Layer对scan到的adv packet不做过滤,直接report到BLE Host。

SCAN_FP_ALLOW_ADV_WL则要求scan到的adv packet必须是在whitelist里面的,才report到BLE Host。

返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。

(13) blc_ll_setScanEnable

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

ble_sts_t   blc_ll_setScanEnable (scan_en_t scan_enable, dupFilter_en_t filter_duplicate);

scan_enable参数类型有如下2个可选值:

typedef enum {
    BLC_SCAN_DISABLE = 0x00,
    BLC_SCAN_ENABLE  = 0x01,
} scan_en_t;

scan_enable为1时,Enable Scanning;scan_enable为0时,Disable Scanning。

1) 在Idle state时,Enable Scanning,Link Layer进入Scanning state。

2) 在Scanning state时,Disable Scanning,Link layer进入Idle state。

filter_duplicate参数类型有如下2个可选值:

typedef enum {
    DUP_FILTER_DISABLE      = 0x00,
    DUP_FILTER_ENABLE       = 0x01,
} dupFilter_en_t;

filter_duplicate为 1 时,表示开启重复包过滤,此时对每个不同的adv packet,Controller只向Host上报一次adv report event;filter_duplicate为 0 时,不开启重复包过滤,对scan到的adv packet会一直上报给Host。

返回值ble_sts_t见下表。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
LL_ERR_CURRENT_STATE_NOT_ SUPPORTED_THIS_CMD 见SDK中定义 Link Layer处于BLS_LINK_STATE_ADV /BLS_LINK_STATE_CONN状态

当设置了scan_type为active scan、Enable Scanning后,对每个device,只读一次scan_rsp并上报给Host。因为每次Enable Scanning后,Controller会对不同设备的scan_rsp进行记录,将它们存储到scan_rsp列表里,确保后面不会再次去读该设备的scan_req。

若user需要多次上报同一个device的scan_rsp,可以通过blc_ll_setScanEnable重复设置Enable Scanning实现,因为每次Enable/Disable Scanning时,设备的scan_rsp列表都会清0。

(14) blc_ll_createConnection

详情请参照《Core_v5.0》(Vol 2/Part E/ 7.8.12 “LE Create Connection Command”)。

ble_sts_t blc_ll_createConnection (u16 scan_interval, u16 scan_window,     init_fp_type_t initiator_filter_policy, 
u8 adr_type, u8 *mac, u8 own_adr_type,
u16 conn_min, u16 conn_max,u16 conn_latency, u16 timeout, u16 ce_min, u16 ce_max)

1) scan_inetrval/scan window

scan_interval设置Initiating state中Scan频点切换时间,单位为0.625ms。

scan_window在Telink BLE SDK中暂时没有处理,实际的scan window设置为scan_interval。

2) initiator_filter_policy

指定当前连接设备的策略,可选如下两种:

typedef enum {
    INITIATE_FP_ADV_SPECIFY = 0x00,  //connect ADV specified by host
    INITIATE_FP_ADV_WL = 0x01,  //connect ADV in whiteList
} init_fp_type_t; 

INITIATE_FP_ADV_SPECIFY表示连接的设备地址是后面的adr_type/mac;

INITIATE_FP_ADV_WL表示根据whitelist里面的设备来连接,此时adr_type/mac无意义。

3) adr_type/ mac

initiator_filter_policy为INITIATE_FP_ADV_SPECIFY时,连接地址类型为adr_type(BLE_ADDR_PUBLIC或者BLE_ADDR_RANDOM)、地址为mac[5…0]的设备。

4) own_adr_type 指定建立连接的Master role使用的MAC address类型。ownAddrType 4个可选的值如下。

    typedef enum{
        OWN_ADDRESS_PUBLIC = 0,
        OWN_ADDRESS_RANDOM = 1,
        OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC = 2,
        OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM = 3,
}own_addr_type_t;

OWN_ADDRESS_PUBLIC表示连接的时候使用public MAC address,实际地址来自MAC address初始化时API blc_ll_initStandby_module (u8 *public_adr)的设置。

OWN_ADDRESS_RANDOM表示连接的时候使用random static MAC address,该地址来源于下面API设定的值:

ble_sts_t   blc_ll_setRandomAddr(u8 *randomAddr);

5) conn_min/ conn_max/ conn_latency/ timeout

这4个参数规定了建立连接后Master role的连接参数,同时这些参数也会通过connection request发给Slave,Slave也会是同样的连接参数。

conn_min/conn_max指定conn interval的范围,Telink BLE SDK中Master role Single Connection直接使用conn_min的值。单位为0.625ms。

conn_latency指定connection latency,一般设为0。 timeout指定connection supervision timeout,单位为10ms。

6) ce_min/ ce_max

SDK暂未处理ce_min/ ce_max。

返回值列表:

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_CONN_REJ_ LIMITED_RESOURCES 0x0D Link Layer已经处于Initiating state,不再接收新的create connection
HCI_ERR_CONTROLLER_BUSY 0x3A Link Layer处于Advertising state或Connection state

(15) blc_ll_setCreateConnectionTimeout

ble_sts_t   blc_ll_setCreateConnectionTimeout (u32 timeout_ms);

返回值为BLE_SUCCESS,timeout_ms单位为ms。

根据Link Layer状态机中介绍,当blc_ll_createConnection触发Idle state/Scanning state进入Initiating state后,如果长时间无法建立连接,会触发Initiate timeout,退出Initiating state。

每次调用blc_ll_createConnection时,SDK默认设置当前的Initiate timeout时间为connection supervision timeout *2。如果User不希望使用SDK默认的这个时间,可以在紧接着blc_ll_createConnection之后调用blc_ll_setCreateConnectionTimeout设置自己需要的Initiate timeout。

(16) blm_ll_updateConnection

详情请参照《Core_v5.0》(Vol 2/Part E/ 7.8.18 “LE Connection Update Command”)。

ble_sts_t blm_ll_updateConnection (u16 connHandle,
                u16 conn_min, u16 conn_max, u16 conn_latency, u16 timeout,
                              u16 ce_min, u16 ce_max);

1) connection handle

指定需要更新连接参数的connection。

2) conn_min/ conn_max/ conn_latency/ timeout

指定需要更新的连接参数,Master role single connection目前直接使用conn_min作为新的interval。

3) ce_min/ce_max

目前不处理。

返回值ble_sts_t只有BLE_SUCCESS,API不会进行参数合理性检查,user需要注意设置参数的合理性。

(17) 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_NOT_ ESTABLISH 0x3E Link Layer处于非Connection state Slave role
HCI_ERR_CONTROLLER _BUSY 0x3A Controller busy(有大量数据正在发送)暂时无法接受该命令。

(18) blm_ll_disconnect

ble_sts_t  blm_ll_disconnect (u16 handle, u8 reason);

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

跟Slave role的API bls_ll_terminateConnection功能一样,区别是多一个conn handle参数,这是因为Telink BLE SDK对Slave role的设计上最多只维持single connection,而Master role的设计上有multi connection,所以必须指定需要disconnect的connection handle。

API返回值如下表。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_UNKNOWN_CONN_ID 0x02 Handle错误,找不到对应的connection
HCI_ERR_CONTROLLER_BUSY 0x3A Controller busy(有大量数据正在发送)暂时无法接受该命令。

(19) 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。

(20) 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)

(21) 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。

(22) 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 ll_whiteList_reset(void);

Reset whitelist,返回值为BLE_SUCCESS。

ble_sts_t ll_whiteList_add(u8 type, u8 *addr);

添加一个设备到whitelist,返回值列表:

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
HCI_ERR_MEM_CAP_EXCEEDED 0x07 whitelist已满,添加失败
ble_sts_t ll_whiteList_delete(u8 type, u8 *addr);

从whitelist删除之前添加的设备,返回值为BLE_SUCCESS。

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

#define     MAX_WHITE_IRK_LIST_SIZE             2

相关API如下:

ble_sts_t  ll_resolvingList_reset(void);

Reset Resolvinglist。返回值BLE_SUCCESS。

ble_sts_t  ll_resolvingList_setAddrResolutionEnable (u8 resolutionEn);

设备地址解析使用,如果要使用Resolvinglist 解析地址,一定要打开使能。不需要解析的时候,可以关闭。

ble_sts_t  ll_resolvingList_add(u8 peerIdAddrType, u8 *peerIdAddr, 
u8 *peer_irk, u8 *local_irk);

添加使用RPA地址的设备,peerIdAddrType/ peerIdAddr和peer-irk填peer device宣称的identity address和irk,这些信息会在配对加密过程中存储到Flash中,user可以在文档SMP部分找到获取这些信息的接口。对于local_irk SDk暂时没有处理,填NULL即可。

ble_sts_t  ll_resolvingList_delete(u8 peerIdAddrType, u8 *peerIdAddr);

删除之前添加的设备。

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

(23) blc_att_setServerDataPendingTime_upon_ClientCmd

在设备中Client刚开始连接后做SDP时,此时被发现的Server在收到相关查询函数时都需要及时根据自己的服务表进行回复,此时TX buffer处于很紧张的状态。因此如果用户在此时去发数据,很容易由于RF tx_buffer满了而失败。

因此我们推荐使用一个可控的pending时间来避免这个问题,相关数据的发送动作将在SDP完成之后进行,在此之前数据被挂起,通过blc_att_setServerDataPendingTime_upon_ClientCmd(u8 num_10ms) 这个api可以修改时间,参数step为10ms。

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提供的B85 BLE SDK中,为节省SRAM,Coded PHY/2M PHY 默认是关闭的,用户如果选择使用此Feature,可以手动打开,打开方法可以参考BLE SDK提供的Demo。

  • Slave端可参考Demo “b85m_feature_test”

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

#define FEATURE_TEST_MODE   TEST_2M_CODED_PHY_CONNECTION
  • Master端可参考Demo “b85m_master_kma_dongle”

用户也可以选择使用其他厂家的设备,只要支持Coded PHY/2M PHY 即可以和Telink的Slave设备兼容互联。

如果使用Telink提供的SDK,Master端的Coded PHY和2M PHY也是默认关闭的,需要通过下面的方法打开。

在vendor/b85m_master_kma_dongle/app.c 函数void user_init(void)中添加API(SDK里默认是关闭的)

blc_ll_init2MPhyCodedPhy_feature();

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

1) API

void blc_ll_init2MPhyCodedPhy_feature(void)

用于使能2M PHY/Coded PHY。

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

3) API:

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用法去理解。

4) API blc_ll_setDefaultConnCodingIndication()

ble_sts_t   blc_ll_setDefaultConnCodingIndication(le_ci_prefer_t prefer_CI);

非BLE Spec标准接口,当Peer Device通过API blc_ll_setPhy ()主动发出PHY_Req申请时,被申请方可以通过此API设置本地设备的preferenced Encode Mode(S2/S8)。

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”)

BLE SDK中相关Demo参考。

  • Slave端参考Demo “b85m_feature_test”

在vendor/b85m_feature_test/feature_config.h 中定义宏如下

#define FEATURE_TEST_MODE           TEST_CSA2

a) 如果使用《Core_4.2》API定义的广播,用户可以选择使用或者不使用跳频算法#2, SDK中默认是不使用的,如果想要使用跳频算法#2,需要通过调用下面的API使能。

 void blc_ll_initChannelSelectionAlgorithm_2_feature(void)

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

  • Master端参考Demo “b85m_master_kma_dongle”

默认master端跳频算法#2也是关闭的,如果需要同样也需手动要在user_init()中调用同样的API使能。

void blc_ll_initChannelSelectionAlgorithm_2_feature(void)

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 PDUs的最大数据长度,从《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)。

(2) Extended Advertising Demo搭建

Extended Advertising Demo “b85m_feature_test”使用方法:

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

a) 在 vendor/b85m_feature_test/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/b85m_feature_test/feature_config.h定义宏

#define FEATURE_TEST_MODE   TEST_2M_CODED_PHY_EXT_ADV

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

注意:

  • 编译某个Demo时,如果出现下图所示的错误提示,可能是因为定义的“attribute_data_retention”属性的数据size超过了16K,而SDK默认的是deepsleep retention 16K SRAM。

"Error in compiling a demo"

可以通过下面的一种方法修改(详细分析请参考 “SRAM和Firmware空间” 章节的介绍)

减少所定义的“attribute_data_retention”属性的数据。

选择切换为deepsleep retention 32K SRAM,详细配置方法参考“software bootloader 介绍” 章节。

(3) Extended Advertising 相关的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“b85m_feature_test”中(vendor/ b85m_feature_test /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 level)来配置发送power。

3) API blc_ll_setExtScanRspData:

ble_sts_t blc_ll_setExtScanRspData(u8 advHandle, data_oper_t operation, data_fragm_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 extAdv_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 Sets,所以此API暂时不支持,只是为以后multipleAdv sets预留,不过Telink SDK根据此API功能写了一个简化的API,用来操作打开/关闭1个Adv Sets,执行效率更高。简化的API如下所示,输入参数和返回值和标准的API一样,但只用来设置1个Adv Set。

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

5) API blc_ll_setAdvRandomAddr()

ble_sts_t   blc_ll_setAdvRandomAddr(u8 advHandle, u8* rand_addr);

BLE Spec标准接口,用于设置设备的Random地址,详细请参考《Core_5.0》(Vol 2/Part E/7.8.4 “LE Set Random Address Command”),并结合SDK上枚举类型定义和demo用法去理解。

6) API blc_ll_setDefaultExtAdvCodingIndication:

void   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来选择preferenced 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

7) API blc_ll_setAuxAdvChnIdxByCustomers:

void   blc_ll_setAuxAdvChnIdxByCustomers(u8 aux_chn);

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

8) API blc_ll_setMaxAdvDelay_for_AdvEvent:

void   blc_ll_setMaxAdvDelay_for_AdvEvent(u8 max_delay_ms);

非BLE Spec标准接口,用于设置在AdvIntervel基础上的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的随机偏移量。

9) 下面的API,为Multiple Advertising Sets API所预留,本版SDK不支持,用户可以暂时忽略。

ble_sts_t   blc_ll_removeAdvSet(u8 advHandle)
ble_sts_t   blc_ll_clearAdvSets(void);

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进行设置即可。

(1) 注册L2CAP数据处理函数

在BLE SDK架构中,Controller的数据通过HCI与Host对接,从HCI到Host数据,首先会在L2CAP层处理,使用下面API注册该处理函数:

void    blc_l2cap_register_handler (void *p);

在b85m_ble_remote/b85m_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);

在b85m_master kma dongle中,应用层包含了BLE Host功能,其处理函数如下,源码提供给user参考:

int app_l2cap_handler (u16 conn_handle, u8 *raw_pkt);

初始化:

blc_l2cap_register_handler (app_l2cap_handler);

在b85m hci中,只实现了slave controller,blc_hci_sendACLData2Host函数将controller数据通过UART/USB等硬件接口传送给BLE Host设备。

int blc_hci_sendACLData2Host (u16 handle, u8 *p)

初始化:

blc_l2cap_register_handler (blc_hci_sendACLData2Host);

(2) 更新连接参数

1) 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),timeout的值为实际supervision timeout时间值除以10ms(如1 s的timeout该值为100)。

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

void    task_connect (u8 e, u8 *p, int n)
    {
        bls_l2cap_requestConnParamUpdate (6, 6, 99, 400);  
        bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(1000);
    }

"抓包显示conn para update reqeust和response"

其中API:

void bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(int time_ms)

用于设置在连接建立后,slave设备等待time_ms(单位:毫秒)后执行API:bls_l2cap_requestConnParamUpdate,来更新连接参数。用户如果在连接建立后,只调用bls_l2cap_requestConnParamUpdate,则slave设备在连接建立后1s再执行发送连接参数更新请求。

在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;
}

2) master回应更新申请

slave申请新的连接参数后,master收到该命令,回CONNECTION PARAMETER UPDATE RESPONSE命令,详情请参照《Core_v5.0》(Vol 3/Part A/ 4.20 “CONNECTION PARAMETER UPDATE RESPONSE”)。

下图所示为该命令格式及result含义。result为0x0000时表示接受该命令,result为0x0001时表示拒绝该命令。

实际的Android、iOS设备是否接受user所申请的连接参数,跟各个厂家BLE master的做法有关,基本上每一家都是不同的,这里没法提供一个统一的标准,只能靠user在平时的master兼容性测试中去慢慢总结归纳。

"BLE协议栈中conn para update rsp格式"

Telink的b85m_master kma dongle中处理slave的连接参数更新demo code如下:

"Demo code of b85m master kma dongle"

在L2CAP_CID_SIG_CHANNEL上收到L2CAP_CMD_CONN_UPD_PARA_REQ后,首先读取interval_min(将其作为最终interval)、supervision timeout和long suspend time(interval * (latency +1)),并对这些数据做一些合理性的判断:要求interval < 200ms;long suspend time<20s;supervision timeout >= 2* long suspend time。符合这些条件就接受该参数申请,不符合则拒绝。这是个简单的demo设计,user可以根据需要进行相关的修改。

不管是否接受Slave的参数申请,都使用下面API对该申请进行答复:

void    blc_l2cap_SendConnParamUpdateResponse( u16 connHandle, u8 req_id, conn_para_up_rsp result);

connHandle指定当前connection ID,result如下两个选择表示接受和拒绝。

typedef enum{
    CONN_PARAM_UPDATE_ACCEPT = 0x0000,
    CONN_PARAM_UPDATE_REJECT = 0x0001,
}conn_para_up_rsp;

如果b85m_master kma dongle接受了Slave的申请,必须在一定时间内通过API blm_ll_updateConnection向Controller发一个更新的cmd,demo code上使用host_update_conn_param_req作为标记,并在main_loop中延时50ms后发起这个update。

"Demo code of b85m master kma dongle"

3) Master在Link Layer上更新连接参数

Slave发送conn para update req,并且master回conn para update rsp接受申请后,master会发送link layer层的LL_CONNECTION_UPDATE_REQ命令,如下图所示。

"抓包显示ll conn update req"

slave收到此更新请求后,记下最后一个参数为master端的instant的值,当slave端的instant值到达这个值的时候,更新到新的连接参数,并触发回调事件BLT_EV_FLAG_CONN_PARA_UPDATE。

instant是master和slave端各自都维护的连接事件计数值,范围为0x0000~0xffff,在一个连接中,它们的值一直都是相等的。当master发送conn_req申请和slave连接后,master开始切换自己的状态(从扫描状态到连接状态),并将master端的instant清0。slave收到conn_req,从广播状态切换到连接状态,将slave端的instant清0。master和slave的每一个连接包都是一个连接事件,两端在conn_req后的第一个连接事件,instant值为1,第二个连接事件instant值为2,依次往后递增。

当master发送LL_CONNECTION_UPDATE_REQ时,最后一个参数instant是指在标号为instant的连接事件时,master将使用LL_CONNECTION_UPDATE_REQ包中前几个连接参数对应值。由于slave和master的instant值始终是相等的,它收到LL_CONNECTION_UPDATE_REQ时,在自己的instant等于master所声明的那个instant的连接事件时,使用新的连接参数。这样就可以保证两端在同一个时间点完成连接参数的切换。

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 byte 的UUID时,master设备都知道这些UUID代表的设备类型。该 BLE SDK中已经定义了一些标准的UUID,分布在以下文件中:stack/service/hids.h、stack/ble /uuid.h。

Telink私有的一些profile(OTA、MIC、SPEAKER等),标准蓝牙里面不支持,在stack/ble/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,如下所示部分代码:

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_appearanceUIID),    (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:

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

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

1) attNum

attNum有两个作用。

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

{57,0,0,0,0,0}, // ATT_END_H – 1 = 57

attNum = 57表示当前Attribute Table中共有57个Attribute。

在BLE里,Attribute Handle值从0x0001开始,往后加一递增,而数组的下标从0开始,在Attribute Table里加上上面这个虚拟的Attribute,正好使得后面每个Attribute在数据里的下标号等于其Attribute Handle的值。当定义好了Attribute Table后,数Attribute在当前Attribute Table数组中的下标号,就能知道该Attribute当前的Attribute Handle值。

将Attribute Table中所有的Attribute数完,数到最后一个的编号就是当前Attribute Table中有效Attribute的数目attNum,目前SDK 中为57,user如果添加或删除了Attribute,需要对此attNum进行修改。

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访问的权限。

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

#define ATT_PERMISSIONS_READ                 0x01 
#define ATT_PERMISSIONS_WRITE                0x02 
#define ATT_PERMISSIONS_AUTHEN_READ          0x61 
#define ATT_PERMISSIONS_AUTHEN_WRITE         0x62
#define ATT_PERMISSIONS_SECURE_CONN_READ     0xE1
#define ATT_PERMISSIONS_SECURE_CONN_WRITE    0xE2
#define ATT_PERMISSIONS_AUTHOR_READ          0x11 
#define ATT_PERMISSIONS_AUTHOR_WRITE         0x12
#define ATT_PERMISSIONS_ENCRYPT_READ         0x21
#define ATT_PERMISSIONS_ENCRYPT_WRITE        0x22 

注意:目前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] = {0x12, 0x03, 0x00, 0x00, 0x2A};
{0,1,2,5,(u8*)(&my_characterUUID), (u8*)(my_devNameCharVal), 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,0x0}
const u8 my_MicUUID[16]     = TELINK_MIC_DATA;
static u8 my_MicData        = 0x80;
{0,1,16,1,(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=35 hid Information的Attribute,相关代码:

const u8 hidInformation[] =
{
    U16_LO(0x0111), U16_HI(0x0111),   // bcdHID (USB HID version),0x11,0x01
    0x00,                           // bCountryCode
    0x01                           // Flags
};
{0,1,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 = 0x23 = 35,对应SDK中Attribute Table中的hid information。

"master读hidInformation的BLE抓包"

Attribute Handle=40 battery value的Attribute,相关代码:

u8  my_batVal[1]    = {99};
{0,1,2,1,(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)(void* p);

user如果需要定义回调写函数,须遵循上面格式。回调函数w是optional的,对某一个具体的Attribute来说,user可以设置回调写函数,也可以不设置回调(不设置回调的时候用空指针0表示)。

回调函数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)(void* p);

User如果需要定义回调读函数,须遵循上面格式。回调函数r是optional的,对某一个具体的Attribute来说,user可以设置回调读函数,也可以不设置回调(不设置回调的时候用空指针0表示)。

回调函数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  
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
const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;

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

a) attHandle从0x0001 ~ 0x0007的Attribute Group,

 Attribute Value为SERVICE_UUID_GENERIC_ACCESS (0x1800).

b) attHandle从0x0008 ~ 0x000a的Attribute Group,

 Attribute Value为SERVICE_UUID_DEVICE_INFORMATION (0x180A).

c) attHandle从0x000B ~ 0x0025的Attribute Group,

 Attribute Value为SERVICE_UUID_HUMAN_INTERFACE_DEVICE (0x1812).

d) attHandle从0x0026 ~ 0x0028的Attribute Group,

 Attribute Value为SERVICE_UUID_BATTERY (0x180F).

e) attHandle从0x0029 ~ 0x0032的Attribute Group,

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

slave将以上5个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为0x2A00的Attribute,slave中Attribute Handle为00 03的Attribute:

const u8    my_devName [] = {'t', 'S', 'e', 'l', 'f', 'i'};
#define GATT_UUID_DEVICE_NAME            0x2a00
const u16 my_devNameUUID = GATT_UUID_DEVICE_NAME;
{0,1,2, sizeof (my_devName),(u8*)(&my_devNameUUID),(u8*)(my_devName), 0},

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

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层长包数据的收发。

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

EffectiveRxMTU=min(ClientRxMTU, ServerRxMTU)

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

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

B85 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) b85 Slave GATT层发长包数据的处理。

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

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

blc_att_setRxMtuSize158;

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

ble_sts_t    blc_att_requestMtuSizeExchange (u16 connHandle, u16 mtu_size);

connHandle为Slave conection的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) 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为0x0016的Attribute写入Attribute Value为0x0001,slave收到后执行该写操作,并回Write Response。

"Write Request Write Response"

9) 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,不回复任何信息。

10) 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版本仅支持A.长属性值写入功能,长属性值最大长度小于等于244字节。如果长度大于244字节,则需要调用以下API接口,对prepare write buffer及长度进行修改:

void  blc_att_setPrepareWriteBuffer(u8 *p, u16 len)

如下图所示,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示例"

11) 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 handle, u8 *p, int len);

handle为对应Attribute的attHandle,p为要发送的连续内存数据的头指针,len指定发送的数据的字节数。该API支持自动拆包功能(根据EffectiveMaxTxOctets做分包处理,即链路层RF TX最大发送字节数,DLE可能会修改该值,默认为27,下文将介绍其替换API,见备注),可将一个很长的数据拆成多个BLE RF packet发送出去,所以len可以支持很大。

Link Layer在Conn state时,一般情况下直接调用该API可以成功push数据到底层软件fifo,但也存在一些特殊情况会导致该API调用失败,根据返回值ble_sts_t可以了解对应的错误原因。

建议应用层调用该API时,检查一下返回值是否为BLE_SUCCESS,若不为BLE_SUCCESS,则需要等待一段时间后再次push。

返回值列表如下。

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
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中定义 处于遍历服务阶段,不能发数据

备注:SDK新增了另一个替代API(使用min(EffectiveMaxTxOctets, EffectiveRxMTU)做为分包最小单元,并且master和slave都可以调用,建议用户使用新API):

ble_sts_t  blc_gatt_pushHandleValueNotify (u16 connHandle, u16 attHandle, u8 *p, int len);

调用该API时,建议用户检查返回值是否为BLE_SUCCESS,并且其与blc_gatt_pushHandleValueNotify返回值有差异:

a) 处于配对阶段时,新API返回值:SMP_ERR_PAIRING_BUSY;

b) 处于加密阶段时,新API返回值:LL_ERR_ENCRYPTION_BUSY;

c) 当len大于ATT_MTU-3时(3是ATT层的包格式长度、opcode 和handle),说明要发送的数据长度超出了ATT层支持的最大数据长度ATT_MTU,返回值为GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE。

12) 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   bls_att_pushIndicateData (u16 handle, u8 *p, int len);

handle为对应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确认

备注:SDK新增了另一个替代API(使用min(EffectiveMaxTxOctets, EffectiveRxMTU)做为分包最小单元,并且master和slave都可以调用,建议用户使用新API):

ble_sts_t  blc_gatt_pushHandleValueIndicate (u16 connHandle, u16 attHandle, u8 *p, int len);

调用该API时,建议用户检查返回值的是否为BLE_SUCCESS,并且返回值和bls_att_pushIndicateData的返回值有差异:

a) 处于配对阶段时,新API返回值:SMP_ERR_PAIRING_BUSY;

b)处于加密阶段时,新API返回值:LL_ERR_ENCRYPTION_BUSY;

c) 当len大于ATT_MTU-3时(3是ATT层的包格式长度opcode 和handle),说明要发送的数据长度PDU超出了ATT层支持的最大PDU长度ATT_MTU,返回值为GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE。

13) 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_VLAUE_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)。

"服务请求响应映射关系"

用户可以很清楚的看到:

  • 第一列跟当前连接的slave设备是否处于加密状态下有关;

  • 第二列(local Device’s Access Requirement for service)则跟用户设置的ATT表中特性的权限(Permission Access)设置有关,如下图所示;

  • 第三列又分为4个子列,这4个子列则对应当前LE安全模式1下四个级别(具体说就是当前的设备配对状态是否是如下4种中的一种:

a) No authentication and no encryption

b) Unauthenticated pairing with encryption

c) Authenticated pairing with encryption

d) Authenticated LE Secure Connections

"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安全级别的示例用户可以参b85m_feature_test/ feature_gatt_security/app.c。

(5) B85m master GATT

在b85m_master kma dongle中,提供了以下GATT API,用于做简单的service discovery或其他数据访问功能。

void    att_req_find_info(u8 *dat, u16 start_attHandle, u16 end_attHandle);

dat实际长度(byte):11 。

void    att_req_find_by_type (u8 *dat, u16 start_attHandle, u16 end_attHandle, u8 *uuid, u8* attr_value, int len);

dat实际长度(byte): 13 + attr_value长度。

void    att_req_read_by_type (u8 *dat, u16 start_attHandle, u16 end_attHandle, u8 *uuid, int uuid_len);

dat实际长度(byte): 11 + uuid长度。

void    att_req_read (u8 *dat, u16 attHandle);

dat实际长度(byte):9 。

void    att_req_read_blob (u8 *dat, u16 attHandle, u16 offset);

dat实际长度(byte):11 。

void    att_req_read_by_group_type (u8 *dat, u16 start_attHandle, u16 end_attHandle, u8 *uuid, int uuid_len);

dat实际长度(byte): 11 + uuid长度。

void    att_req_write (u8 *dat, u16 attHandle, u8 *buf, int len);

dat实际长度(byte):9 + buf数据长度。

void    att_req_write_cmd (u8 *dat, u16 attHandle, u8 *buf, int len);

dat实际长度(byte):9 + buf数据长度。

以上API,需要预先定义内存空间*dat,然后调用API进行数据组装,最后调用blm_push_fifo将dat送到Controller发送,并且注意需要判断返回值是否为TRUE。以att_req_find_info为例如下,其他接口都可使用类似方法。

u8  cmd[12];
att_req_find_info(cmd,  0x0001,  0x0003);
if( blm_push_fifo (BLM_CONN_HANDLE, cmd) ){
//cmd send OK
}

使用上面参考的方法向Slave发送了对应的find info req、read req等cmd后,很快会收到Slave回复的find info rsp、read rsp等对应的response信息,在int app_l2cap_handler (u16 conn_handle, u8 *raw_pkt)中按照如下框架处理即可:

    if(ptrL2cap->chanId == L2CAP_CID_ATTR_PROTOCOL)  //att data
    {
        if(pAtt->opcode == ATT_OP_EXCHANGE_MTU_RSP){
            //add your code
        }
        if(pAtt->opcode == ATT_OP_FIND_INFO_RSP){
            //add your code
        }
        else if(pAtt->opcode == ATT_OP_FIND_BY_TYPE_VALUE_RSP){
            //add your code
        }
        else if(pAtt->opcode == ATT_OP_READ_BY_TYPE_RSP){
            //add your code
        }
        else if(pAtt->opcode == ATT_OP_READ_RSP){
            //add your code
        }
        else if(pAtt->opcode == ATT_OP_READ_BLOB_RSP){
            //add your code
        }
        else if(pAtt->opcode == ATT_OP_READ_BY_GROUP_TYPE_RSP){
            //add your code
        }
        else if(pAtt->opcode == ATT_OP_WRITE_RSP){
            //add your code
        }
    }

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的四个级别, 详情可以参考《Core_v5.0》(Vol 3//Part C/10.2 LE SECURITY MODES)

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)

注意:本端设备设定的安全级别只表示本端设备可能达到的最高安全级别,想要达到设定的安全级别跟两个因素有关:

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_level_t;

(2) SMP参数配置

Telink BLE SDK中SMP参数配置介绍主要围绕SMP四个安全等级的配置展开,slave的SMP功能目前能支持的最高级别是LE security mode1 level4;master的SMP功能目前支持传统配对方式下的最高级别是LE security mode1 level2(传统配对Just Works方式)。

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

枚举类型io_capability_t具体定义介绍如下:

typedef enum {
    IO_CAPABILITY_UNKNOWN = 0xff,
    IO_CAPABILITY_DISPLAY_ONLY = 0,
    IO_CAPABILITY_DISPLAY_YESNO = 1,
    IO_CAPABILITY_KEYBOARD_ONLY = 2,
    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):

"根据不同IO能力映射KEY产生方法"

这部分具体映射关系可以参考《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(); //SMP参数配置必须放在该API之前

这里因为涉及到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();//SMP参数配置必须放在该API之前

C. 设备具备传统配对下Passkey Entry(PK_Init_Dsply_Resp_Input或者PK_BOTH_INPUT)的初始化配置:

blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABLITY_KEYBOARD_ONLY);
blc_smp_peripheral_init();//SMP参数配置必须放在该API之前

这里因为涉及到用户输入TK值,SDK在应用层提供了相关的GAP event给用户,请参考“GAP event”章节。提供给用户设置Passkey Entry的TK值API如下:

/**
 * @brief      This function is used to set manual pin code for debug in passkey entry mode.
 *             attention: 1. PinCode should be generated randomly each time, so this API is not standard usage for security,
 *                           it is violation of security protocols.
 *                        2. If you set manual pin code with this API in correct range(1~999999), you can neglect callback
 *                           event "GAP_EVT_MASK_SMP_TK_DISPALY", because the pin code displayed is the value you have set by this API.
 *                        3. pinCodeInput value 0 here is used to exit manual set mode, but not a Pin Code.
 * @param[in]  connHandle - connection handle
 * @param[in]  pinCodeInput - 0           :  exit  manual set mode, generated Pin Code randomly by SDK library.
 *                            other value :  enter manual set mode, pinCodeInput in range [1, 999999] will be TK's value.
 *                                           if bigger than 999999, generated Pin Code randomly by SDK library
 * @return     none.
 */
void        blc_smp_manualSetPinCode_for_debug(u16 connHandle, 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_CAPABLITY_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);
.....//参考LE security mode1 level3配置方式B、C

C. 设备具备安全连接配对下Out of Band的初始化配置:

blc_smp_enableSecureConnections(1);//enable SC
blc_smp_generateScOobData(&sc_oob_data_cb.scoob_local,&sc_oob_data_cb.scoob_local_key);//生成LTK
blc_smp_peripheral_init();//SMP参数配置必须放在该API之前
对于SC paring,如果A或者B有一方支持OOB,那么两者就会采用OOB方式进行配对,否则根据IO能力选择配对方式: "SC配对模式下MITM、OOB flag使用规则"

同level3里的OOB部分一样,在初始化配置阶段需要通过blc_smp_setSecurityParameters函数将OOB_en置1。 在该模式下,由于SC OOB的鉴权通过uart实现,所以在初始化阶段blc_smp_generateScOobData生成配对需要的值后,会通过uart方式打印出来,用户可以根据打印提示进行OOB方式的配对。

这里介绍两个API:

(a)

int         blc_smp_generateScOobData(smp_sc_oob_data_t *oob_data, smp_sc_oob_key_t *oob_key);

枚举类型smp_sc_oob_data_t和smp_sc_oob_key_t具体定义如下:

typedef struct  {
    /** Random Number. */
    u8 random[16]; //big--endian

    /** Confirm Value. */
    u8 confirm[16]; //big--endian
}smp_sc_oob_data_t;

typedef struct  {
    /** Public Key. */
    u8 public_key[64];

    /** Private Key. */
    u8 private_key[32];
}smp_sc_oob_key_t;
在本端设备向对端发送OOB数据时,此API用于生成LTK用到的随机数,即Confirm Value和Random Value.

(b)

int          blc_smp_setScOobData(u16 connHandle, const smp_sc_oob_data_t *oobd_local, const smp_sc_oob_data_t *oobd_remote);

在对端设备发送OOB数据后,本端设备则需要通过此API把收到的OOB数据存在smp中。

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

前面已经介绍了每个参数含义,这里就不再重复了。

(3) SMP安全请求配置

SMP安全请求(Security Request)只有slave可以发送,所以这部分只对slave设备来说。

我们知道配对流程阶段1有一个可选的安全请求包(Security Request),该包的目的是使slave可以主动触发配对流程的开始。SDK提供了如下API用于灵活地配置slave是否在连接后或者回连后立即还是pending_ms毫秒后再向master发送Security Request,亦或是不发送Security Request以实现不同的配对触发组合:

blc_smp_configSecurityRequestSending( secReq_cfg newConn_cfg, secReq_cfg reConn_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:用于配置新设备,reConn_cfg:用于配置回连的设备。这里SDK在回连时也提供配置是否发配对请求的目的:配对绑定过的设备,下次再连接的时候(即回连),master有时候不一定会主动发起LL_ENC_REQ来加密链路,此时如果slave发一下Security Request就会去触发master主动加密链路,所以SDK提供了reConn_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

 reConn_cfg: SecReq_NOT_SEND

 pending_ms: 此时该参数不起作用。

newConn_cfg:SecReq_NOT_SEND表示新设备slave不会主动发起Security Request,只有在对方发起配对请求时响应对方的配对请求。如果对方不发送配对请求,则不会进行加密配对。如下图,在master发送配对请求包SM_Pairing_Req时,slave会响应,但是不会主动触发master发起配对请求。

"抓包显示Pairing Peer Trigger"

reConn_cfg:SecReq_NOT_SEND表示设备已经配对,回连时slave设备不会发送Security Request。

2) newConn_cfg: SecReq_IMM_SEND

 reConn_cfg: SecReq_NOT_SEND

 pending_ms: 此时该参数不起作用。

newConn_cfg:SecReq_IMM_SEND表示新设备slave一经连接便会主动向master发Security Request,以触发master开始配对流程。如下图,slave主动发送SM_Security_Req 触发master发送配对请求:

"抓包显示Pairing Conn Trigger"

reConn_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 "b85m_ble_remote"初始化中设置direct advertising的代码。

Slave最多可以同时存储4个master的配对信息,这4个设备都可以回连成功。下面接口用于设定当前存储的最多设备数,不超过4,如果用户不设置的话,默认值也为4。

#define         SMP_BONDING_DEVICE_MAX_NUM                  4 
ble_sts_t 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为index 0,masterB为index1,因为masterB是最近的设备。接着让slave和masterA回连一次成功,这时候masterA就成为最近的一个设备,此时index 0设备变为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,再配对master E,则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为index 0, masterB为index1,接着让slave和masterA回连一次成功,此时index 0设备依然为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什么顺序回连,index 0、1、2、3依然分别对应masterA、B、C、D。

需要注意设备配对超过4的情况:若连续配对masterA、B、C、D,再配对master E,则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;  //address used in link layer connection
    u8      peer_addr[6];
    u8      peer_key_size;
    u8      peer_id_adrType; //peer identity address information in key distribution, used to identify
    u8      peer_id_addr[6];
    u8      own_ltk[16];      //own_ltk[16]
    u8      peer_irk[16];
    u8      peer_csrk[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可以解析出(参考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中合适区域。

需要注意的是,用户调用该API确保必须在blc_gap_peripheral_init这个函数之后,其他SMP API函数之前。不调用该API接口情况下,绑定信息存储在FLASH中的位置将根据flash大小自动调整。 | Flash Size | Addr | | :------------------------------- | :---- | | 512KB | 0x74000 | | 1MB | 0xFC000 | | 2MB | 0x1FC000 |

(5) master SMP

master的SMP功能目前支持传统配对方式下的最高级别是LE security mode1 level2(传统配对Just Works方式)。用户可以参考“b85m_master kma dongle”,只需要修改“b85m_master kma dongle/app_config.h”文件下的宏:

#define BLE_HOST_SMP_ENABLE                         0

该宏配置为1,则使用标准的SMP:配置master支持的最高安全级别为LE security mode1 level2,支持传统配对Just Works方式;若该宏配置为0,则表示启用非标准的自定义配对管理功能。

1) master使能SMP (设置宏BLE_HOST_SMP_ENABLE为1)

使用该安全级别配置就必须要调用如下API用于初始化SMP各参数配置,包括绑定区域FLASH的初始化配置:

int blc_smp_central_init (void);

如果在初始化阶段只调用了该API,则SDK会使用默认参数去配置SMP:

  • 默认支持的最高安全等级:Unauthenticated_Paring_with_Encryption;

  • 默认绑定模式:Bondable_Mode(存储配对加密后分发的KEY到FLASH);

  • 默认IO能力是IO_CAPABILITY_NO_INPUT_NO_OUTPUT。

在配对设备支持LE security mode1 level2时,还需要用户配置如下三个API:

void blm_smp_configParingSecurityInfoStorageAddr (int addr);
void blm_smp_registerSmpFinishCb (smp_finish_callback_t cb);
void blm_host_smp_setSecurityTrigger(u8 trigger);

下面分别介绍这三个API:

A. void blm_smp_configParingSecurityInfoStorageAddr (int addr);

该API可以用于master设备配置绑定信息存储在FLASH中的位置,其中参数addr可以根据实际需要修改。

B. void blm_smp_registerSmpFinishCb (smp_finish_callback_t cb);

该回调函数在配对第三阶段密钥分发完成后触发,用户可以在应用层注册以获取配对完成事件。

C. void blm_host_smp_setSecurityTrigger(u8 trigger);

该API主要用于配置master是否主动发起加密、回连时主动加密链路。具体参数可以选择如下:

#define SLAVE_TRIGGER_SMP_FIRST_PAIRING         0   
#define MASTER_TRIGGER_SMP_FIRST_PAIRING        BIT(0)
#define SLAVE_TRIGGER_SMP_AUTO_CONNECT          0   
#define MASTER_TRIGGER_SMP_AUTO_CONNECT         BIT(1)

具体说就是:1、第一次配对时,是master选择master主动发起配对请求还是在收到slave发送的Security Request后再开始配对;2、已经配对过的设备回连时,是master主动发送LL_ENC_REQ加密链路还是等收到slave发送的Security Request后再开始加密链路。一般我们会配置Master第一次配对主动发起配对请求,回连时主动发LL_ENC_REQ。

最终的用户初始化代码参考如下,用户可以参考“b85m_master kma dongle”:

    blm_smp_configParingSecurityInfoStorageAddr(0x78000);
    blm_smp_registerSmpFinishCb(app_host_smp_finish);
    blc_smp_central_init();
    //SMP trigger by master
    blm_host_smp_setSecurityTrigger(MASTER_TRIGGER_SMP_FIRST_PAIRING |                                  MASTER_TRIGGER_SMP_AUTO_CONNECT);

至于以下几个master端的绑定信息相关的API,是供master SMP协议底层使用的,用户不需要具体了解。

int     tbl_bond_slave_search(u8 adr_type, u8 * addr);
int     tbl_bond_slave_delete_by_adr(u8 adr_type, u8 *addr);
void    tbl_bond_slave_unpair_proc(u8 adr_type, u8 *addr);

2) 非标准的自定义配对管理(设置宏BLE_HOST_SMP_ENABLE为0)

如果用户需要使用自定义的配对管理,初始化相关API如下:

blc_smp_setSecurityLevel(No_Security);//禁用SMP功能
user_master_host_pairing_flash_init();//自定义方式

A. Flash存储方法设计

默认使用的flash数据区sector为0x78000 ~ 0x78FFF,在app_config.h中可以修改:

#define FLASH_ADR_PAIRING   0x78000

将flash 0x78000开始每8个bytes划分为一个area,称8 bytes area。每个area可以存储一个Slave的mac address,其中第一个byte是标志位,第二个byte为地址类型,后面6个为6 bytes的mac address。

typedef struct {
    u8 bond_mark;
    u8 adr_type;
    u8 address[6];
} macAddr_t;

flash存储过程中使用依次往后推8 bytes area的方法,第一个有效slave mac存储在0x78000~0x78007,将0x78000的第一byte标志位写为0x5A,表示当前地址有效;当存储第二个有效mac address时存储在0x78008~0x7800f,将0x78008打上标记0x5A;当存储第三个有效mac address时存储在0x78010~0x78017,将0x78010打上标记0x5A。

如果要某个slave设备解配对,dongle端需要擦掉这个设备的mac address,只需要将之前存储该mac address的8 bytes area的标志位写为0x00即可;如擦掉上面三个device中的第一个device,将0x78000写为0x00即可。

采用上面这种8bytes顺延方法的原因是,程序在运行过程中不能调用flash_erase_sector这个函数擦flash,因为该操作擦一个sector 4K的flash耗时在20~200ms之间,这个时间会引起BLE时序的错误。

将所有的slave mac的配对存储和解配对擦除使用0x5A和0x00标志位来表示,当8 bytes area越来越多,可能会占满整个sector 4K flash导致出错,因此在初始化的时候加了特别处理:从0x78000开始读取8 bytes area信息,将所有的有效mac address读到RAM中的slave mac table。这过程中检查8 bytes area是否太多,如果太多的话,就擦掉整个sector,然后将ram中维护的slave mac table重新写回0x78000开始的8 bytes area。

B. Slave mac table

#define USER_PAIR_SLAVE_MAX_NUM     4  //telink demo use max 4, you can change this value
typedef struct {
    u8 bond_mark;
    u8 adr_type;
    u8 address[6];
} macAddr_t;
typedef struct {
    u32 bond_flash_idx[USER_PAIR_SLAVE_MAX_NUM];  //mark paired slave mac address in flash
    macAddr_t bond_device[USER_PAIR_SLAVE_MAX_NUM];  //macAddr_t alreay defined in ble stack
    u8 curNum;
} user_salveMac_t;
user_salveMac_t user_tbl_slaveMac;

用上面结构在RAM中使用 slave mac table维护所有的配对设备,改变宏USER_PAIR_SLAVE_MAX_NUM即可定义自己想要的最多允许几只配对,Telink默认为4,指维护4个设备的配对,user可以修改这个值。

假设user将最多维护3个设备,将USER_PAIR_SLAVE_MAX_NUM改为3后,user_tbl_slaveMac中curNum表示当前flash上记录了几个有效的slave设备,bond_flash_idx数组记录有效地址在flash上的8 bytes area起始地址相对于0x78000的偏移量(当解配对这个设备时,可以通过这个偏移量找到8 bytes area的标志位,将其写为0x00),bond_device数组记录mac address。

C. 相关API说明

基于上面flash存储设计和RAM中 slave mac table的设计,分别有以下几个API可以调用。

a) user_master_host_pairing_flash_init

void    user_master_host_pairing_flash_init(void);

用户自定义配对管理flash初始化函数,启用自定义方式时需要调用该初始化函数。

b) user_tbl_slave_mac_add

int user_tbl_slave_mac_add(u8 adr_type, u8 *adr);

添加一个slave mac, return 1表示成功, 0 表示失败。当有新的设备配对上时,需要调用此函数。函数先判断当前flash和slave mac table中设备是否已经到达最大值。若没有到最大值,无条件添加到slave mac table,并在flash的一个 8 bytes area上存储。若已经到最大值。涉及到处理的策略问题:是不允许配对还是直接覆盖最老的,Telink demo的方法是直接覆盖最老的,由于telink最大配对个数为1,覆盖最老的也就是抢占当前配对设备,先使用user_tbl_slave_mac_delete_by_index(0)删掉当前设备,再往slave mac table里面加入新的。User可以根据自己的策略去修改这个函数的实现。

c) user_tbl_slave_mac_search

int user_tbl_slave_mac_search(u8 adr_type, u8 * adr)

根据adv report的设备地址搜索该设备是否已经在slave mac table中,即判断当前发广播包的设备是否之前已经和master配对上,若是已经配对过的设备可以直接连接。

d) user_tbl_slave_mac_delete_by_adr

int user_tbl_slave_mac_delete_by_adr(u8 adr_type, u8 *adr)

通过指定地址删除一个配对的设备。

e) user_tbl_slave_mac_delete_by_index

void user_tbl_slave_mac_delete_by_index(int index)

通过指定index删除配对设备。Index值反映的是设备配对的顺序。如果最大配对个数为1,配上的那个设备index永远为0;如果如果最大配对个数为2,第一个配上的设备index为0,第二个配上的设备index为1;依次类推。

f) user_tbl_slave_mac_delete_all

void user_tbl_slave_mac_delete_all(void)

删除所有配对设备。

g) user_tbl_salve_mac_unpair_proc

void user_tbl_salve_mac_unpair_proc(void)

处理解配对命令,参考代码中删除所有配对设备,是默认最大配对个数为1时的处理方法。User可以修改该函数实现。

D. 连接和配对

master收到Controller上报的广播包时,有以下两种情况会和Slave进行连接:

调用函数user_tbl_slave_mac_search来检查当前设备是否已经跟master配对过并且没有被解配对,如果已经配对过,可以自动连接。

master_auto_connect = user_tbl_slave_mac_search(pa->adr_type, pa->mac);
if(master_auto_connect) { create connection }

若当前广播设备不在slave mac table里面,不符合自动连接,检查是否满足手动配对条件。SDK中默认设置了两个手动配对方案,在当前广播设备距离足够近的前提下,一是master dongle上配对键被按下;二是当前广播数据是Telink定义的配对广播包数据。代码:

//manual paring methods 1: button triggers
user_manual_paring = dongle_pairing_enable && (rssi > -56);  //button trigger pairing(rssi threshold, short distance)
//manual paring methods 2: special paring adv data
if(!user_manual_paring){  //special adv pair data can also trigger pairing
user_manual_paring = (memcmp(pa->data,telink_adv_trigger_paring,sizeof(telink_adv_trigger_paring)) == 0)
&& (rssi > -56);
}
if(user_manual_paring) { create connection }

若是手动配对触发的建立连接,在连接成功建立后,即HCI LE CONECTION ESTABLISHED EVENT上报时,将当前设备添加到slave mac table中:

//manual paring, device match, add this device to slave mac table
if(blm_manPair.manual_pair  &&  blm_manPair.mac_type == pCon->peer_adr_type  &&
  !memcmp(blm_manPair.mac,pCon->mac, 6))
{
    blm_manPair.manual_pair = 0; 
    user_tbl_slave_mac_add(pCon->peer_adr_type, pCon->mac);
}

E. 解配对

_attribute_ram_code_void host_pair_unpair_proc (void) 
{
    //terminate and unpair proc
    static int master_disconnect_flag;
    if(dongle_unpair_enable){
        if(!master_disconnect_flag && blc_ll_getCurrentState() == BLS_LINK_STATE_CONN){
if( blm_ll_disconnect(cur_conn_device.conn_handle, HCI_ERR_REMOTE_USER_TERM_CONN)  == 
BLE_SUCCESS){
                master_disconnect_flag = 1;
                dongle_unpair_enable = 0;

                #if (BLE_HOST_SMP_ENABLE)
                    tbl_bond_slave_unpair_proc(cur_conn_device.mac_adrType, cur_conn_device.mac_addr); 
                #else
                    user_tbl_salve_mac_unpair_proc();
                #endif
            }
        }
    }
    if(master_disconnect_flag && blc_ll_getCurrentState() != BLS_LINK_STATE_CONN){
        master_disconnect_flag = 0;
    }
}

参考上面code,当解配对条件生效时,Master先调用blm_ll_disconnect断开连接,然后调用user_tbl_salve_mac_unpair_proc函数处理解配对,Demo code直接删掉所有的配对设备,由于默认的最大配对个数是1,所以也就只删掉了一个。如果user设置了比较复杂的配对多个设备,此时应该调用user_tbl_slave_mac_delete_by_adr或user_tbl_slave_mac_delete_by_index去删除某个设备。

解配对条件的生效,Demo code中给出了两种情况,一是master dongle上解配对按键被按下,二是在HID keyboard report service上收到解配对键值0xFF。

User也可以按照自己的需要去修改解配对的触发条件。

(6) SMP失败管理

当SMP失败时,可以通过回调函数控制继续保持连接或是断开。其实现方法如下:

a.注册gap层的处理函数,打开事件mask,事件为GAP_EVT_SMP_PAIRING_FAIL ,如下:

    blc_gap_registerHostEventHandler( app_host_event_callback );
    blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_FAIL );

b.修改处理函数中该mask下对应的处理:

int app_host_event_callback (u32 h, u8 *para, int n)
{
    u8 event = h & 0xFF;

    switch(event)
    {
        case GAP_EVT_SMP_PAIRING_FAIL:
        {
            gap_smp_paringFailEvt_t* p = (gap_smp_paringFailEvt_t*)para;
            //想要的操作
        }
        break;

        default:
        break;
    }

    return 0;
}

GAP

(1) GAP初始化

GAP初始化分主从角色,slave使用如下API初始化GAP:

void blc_gap_peripheral_init(void);

Master使用如下API初始化GAP:

void  blc_gap_central_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_BEAGIN                  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_DISPALY                      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_VLAUE_CONFIRM           17

callback函数原型中para和n表示event的数据和数据长度,下文将详细说明以上列出的GAP event。User可参考b85m_feature_test/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用户可以在ble/gap/gap_event.h中查到。

#define GAP_EVT_MASK_SMP_PAIRING_BEAGIN             (1<<GAP_EVT_SMP_PAIRING_BEAGIN)
#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_DISPALY                 (1<<GAP_EVT_SMP_TK_DISPALY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY         (1<<GAP_EVT_SMP_TK_REQUEST_PASSKEY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_OOB             (1<<GAP_EVT_SMP_TK_REQUEST_OOB)
#define GAP_EVT_MASK_SMP_TK_NUMERIC_COMPARE         (1<<GAP_EVT_SMP_TK_NUMERIC_COMPARE)
#define GAP_EVT_MASK_ATT_EXCHANGE_MTU               (1<<GAP_EVT_ATT_EXCHANGE_MTU)
#define GAP_EVT_MASK_GATT_HANDLE_VLAUE_CONFIRM      (1<<GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM)

若user未通过该API设置GAP event mask,那么当GAP 相应的event产生时将不会通知应用层。

注意:

  • 以下论述GAP event时,均设定注册了GAP event回调,且开启了对应的eventMask。

1) GAP_EVT_SMP_PAIRING_BEAGIN

事件触发条件:当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_paringBeginEvt_t;

connHandle表示当前连接句柄。

secure_conn为1表示使用安全加密特性(LE Secure Connections),否则将使用LE legacy pairing。

tk_method表示接下来配对使用什么样的TK值方式:例如JustWorks、PK_Init_Dsply_Resp_Input、PK_Resp_Dsply_Init_Input,Numric_Comparison等。

2) GAP_EVT_SMP_PAIRING_SUCCESS

事件触发条件:配对整个流程正确完成时产生该事件,该阶段即为LE配对阶段之密钥分发阶段3(Key Distribution, Phase 3),如果有密钥需要分发,则等待双方密钥分发完成后触发配对成功事件,否则直接触发配对成功事件。

数据长度n:4。

回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u8  bonding;
    u8  bonding_result;
} gap_smp_paringSuccessEvt_t;

connHandle表示当前连接句柄。

bonding为1表示启用bonding功能,否则不启用。

bonding_result表示bonding的结果:如果没有开启bonding功能,则为0,如果开启了bonding功能,则还需要检查加密Key是否被正确的存储在FLASH中,存储成功为1,否则为0。

3) GAP_EVT_SMP_PAIRING_FAIL

事件触发条件:由于slave或master其中一个不符合标准配对流程,或者通信中出现报错等异常原因导致配对流程终止。

数据长度n:2。

回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u8  reason;
} gap_smp_paringFailEvt_t;

connHandle表示当前连接句柄。

reason表示配对失败的原因,这里列出几个常见的配对失败原因值,其他配对失败原因值我们可以参考SDK目录下的“stack/ble/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_NUMUERIC_FAILED             0x0C
#define PAIRING_FAIL_REASON_PAIRING_TIEMOUT             0x80
#define PAIRING_FAIL_REASON_CONN_DISCONNECT             0x81

4) GAP_EVT_SMP_CONN_ENCRYPTION_DONE

事件触发条件:Link Layer加密完成时(Link Layer收到master发的start encryption response)触发。

数据长度n:3。

回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u8  re_connect;   //1: re_connect, encrypt with previous distributed LTK; 0: pairing, encrypt with STK
} gap_smp_connEncDoneEvt_t;

connHandle表示当前连接句柄。

re_connect为1表示快速回连(将使用之前分发的LTK加密链路),若该值为0则表示当前加密是第一次加密。

5) GAP_EVT_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_DISPALY

事件触发条件: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。

void blc_smp_setDefaultPinCode(u32 pinCodeInput)
用户将slave上看到的6位pincode码输入到master设备上(如手机),完成TK输入,配对流程得以继续执行。如果用户输入pincode错误或者点击取消,则配对流程失败。

关于Passkey Entry应用的实例,用户可以参考SDK提供的demo “vendor/b85m_feature_test/feature_gatt_security/app.c”

case GAP_EVT_SMP_TK_DISPALY:
{
    char pc[7];
    u32 pincode = *(u32*)para;
    sprintf(pc, "%d", pincode);
    printf("TK display:%s\n", pc);
}
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/b85m_feature_test/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(CleintRxMTU, ServerRxMTU),CleintRxMTU表示客户端的RX MTU size值,ServerRxMTU表示服务端的RX MTU size值。Master和slave交互了彼此的MTU size后,取两者最小值作为彼此交互的最大MTU size值。

11) GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM

事件触发条件:应用层每调用一次blc_gatt_pushHandleValueIndicate,向master发送indicate数据后,master会回复一个confirm,表示对这个数据的确认,slave收到该confirm时触发。

数据长度n:0。

回传指针p:NULL。

Connection-oriented Channel(CoC)

CoC是L2CAP通道支持建立面向连接的通道,它使用基于LE信令流控模式数据包的数据发送方式,主要用于在两个蓝牙设备之间传输大量的数据,其最大可允许传输64KB长度的数据。

CoC的流量控制方法

由于CoC有大数据量的传输,所以在协议层定义了一套流量控制协议,基于credit值实现流量控制。 在建立CoC连接时,双方交换初始credits。后续每当发送成功一个L2CAP包后,需要将对端的credit减1;当对端的credit值等于0时,本端不能再发送L2CAP包给对端,需要等对端上报 L2CAP_FLOW_CONTROL_CREDIT_IND 指令后才能继续发送。 本端接收一个L2CAP包后,需要将本地的credit减1,当本地的credit值小于0时需要断开该CoC通道。此SDK做了一些简化处理,当本地的credit小于初始credit的一半时,会自动上报 L2CAP_FLOW_CONTROL_CREDIT_IND。

CoC的例程在B85m_feature_test的feature_COC_slave中给出,下面对例程进行简单介绍。

CoC模块初始化

用户可调用模块初始化函数进行初始化操作:

void app_l2cap_coc_init(void)
{
    blc_coc_initParam_t regParam = {
        .MTU = COC_MTU_SIZE,
        .SPSM = 0x0080,
        .createConnCnt = 1,
        .cocCidCnt = COC_CID_COUNT,
    };
    int state = blc_l2cap_registerCocModule(&regParam, cocBuffer, sizeof(cocBuffer));
    if(state){}
}
此函数主要用于定义一些CoC的参数,包括: (1) MTU 最大传输单元,表示CoC通道中最大可支持的SDU大小,其有效范围为23~65535; (2) SPSM SPSM的值分为固定值和动态值两种。固定值范围为0x0001 ~ 0x007F,是由蓝牙官方规定的,用于固定的业务,其中当前B85m单连接SDK所支持的有IPSP(0x0023)和OTS(0x0025)。动态值范围为0x0080 ~ 0x00FF,可以被用户自己定义用于不同的业务。 (3) createConnCnt 支持在ACL连接上同时主动创建CoC连接的数量。在B85m单连接SDK中只支持单个CoC连接的同时创建。 (4) cocCidCnt 支持CoC连接的数量,最大可建立的CoC连接数为64。

CoC参数设置完成后,调用blc_l2cap_registerCocModule完成CoC模块的注册:

int blc_l2cap_registerCocModule(blc_coc_initParam_t* param, u8 *pBuffer, u16 buffLen);

CoC指令

此SDK中所支持的CoC指令为: | code | 指令 | 当前指令所支持的CID | |----- |---------------------------------------|------------ | | 0x06 | L2CAP_DISCONNECTION_REQ | 0x0005 | | 0x07 | L2CAP_DISCONNECTION_RSP | 0x0005 | | 0x14 | L2CAP_LE_CREDIT_BASED_CONNECTION_REQ | 0x0005 | | 0x15 | L2CAP_LE_CREDIT_BASED_CONNECTION_RSP | 0x0005 | | 0x16 | L2CAP_FLOW_CONTROL_CREDIT_IND | 0x0005 | | 0x17 | L2CAP_CREDIT_BASED_CONNECTION_REQ | 0x0005 | | 0x18 | L2CAP_CREDIT_BASED_CONNECTION_RSP | 0x0005 | | 0x19 | L2CAP_CREDIT_BASED_RECONFIGURE_REQ | 0x0005 | | 0x1A | L2CAP_CREDIT_BASED_RECONFIGURE_RSP | 0x0005 |

CID为0x0005表示在LE信令通道上进行传输。实际上,部分CoC指令还支持在CID为0x0001的通道(即L2CAP信令通道)上进行传输,但由于低功耗蓝牙不使用此通道,所以不在我们讨论范畴中。

下面对几个主要的指令进行介绍。

(1) 建立基于LE credit的连接请求 在BLE协议栈中,slave通过L2CAP层的 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ 命令向 master 申请建立基于LE credit的CoC连接。该命令格式如下所示,详情请参考《Core_v5.4》(Vol 3/Part A/4.22 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ (CODE 0x14))。

"BLE协议栈中 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ 格式"

其中部分字段表示为: a) Source CID 表示发送CoC连接请求方的通道端点,此通道被建立后,对端发送的数据包将发送到此CID。CID的有效范围为0x0040 ~ 0xFFFF。 注意,对于同一个ACL连接,建立的CoC通道的两端,各自的CID不能重复,即:假设设备A和设备B建立CoC通道,设备A的CID为0x0040,那么设备B的CID可以为0x0040 ~ 0xFFFF中的任意值;那么当设备A和设备B要建立第二条CoC通道(建立时第一条CoC通道未断开)时,设备A不能够再使用0x0040作为CID与设备B建立连接,只能从0x0041 ~ 0xFFFF中选择一个CID建立,同理,设备B的CID也不能与已建立连接的CoC通道的CID重复。 b) MPS 表示一个PDU指令的最大字节数,有效范围为23 ~ 65533,但要注意MPS <= MTU。 c) Initial Credits 表示在建立CoC连接之后,本端可以接收K帧的数量,有效范围为0 ~ 65535。

该SDK在L2CAP层上提供了slave主动申请建立CoC通道的API,用来向master发送上面这个 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ 命令。

ble_sts_t blc_l2cap_createLeCreditBasedConnect(u16 connHandle);

(2) 建立基于LE credit的连接回复 对端申请建立基于LE credit的连接后,本地收到命令,回复 L2CAP_LE_CREDIT_BASED_CONNECTION_RSP 命令,详情请参考《Core_v5.4》(Vol 3/Part A/4.23 L2CAP_LE_CREDIT_BASED_CONNECTION_RSP (CODE 0x15))。

此SDK中,如果收到对端发送的 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ 命令,stack会自动处理回复 L2CAP_LE_CREDIT_BASED_CONNECTION_RSP,用户无需进行其他操作。

(3) 发起断连请求 在BLE协议栈中,slave通过L2CAP层的 L2CAP_DISCONNECTION_REQ 命令向 master 申请断开CoC连接。该命令格式如下所示,详情请参考《Core_v5.4》(Vol 3/Part A/4.6 L2CAP_DISCONNECTION_REQ (CODE 0x06))。

"BLE协议栈中 L2CAP_DISCONNECTION_REQ 格式"

其中: a) Destination CID 表示接收此请求的设备上要断开连接的CID。

b) Source CID 表示发送此请求的设备上要断开连接的CID。

该SDK在L2CAP层上提供了本地主动申请断开CoC通道的API,用来向对端发送 L2CAP_DISCONNECTION_REQ 命令。

ble_sts_t blc_l2cap_disconnectCocChannel(u16 connHandle, u16 srcCID);

(4) 回复断连请求 对端申请断开连接后,本端收到命令,回复 L2CAP_DISCONNECTION_RSP 命令,详情请参考《Core_v5.4》(Vol 3/Part A/4.7 L2CAP_DISCONNECTION_RSP (CODE 0x07))。 此SDK中,如果收到对端发送的 L2CAP_DISCONNECTION_REQ 命令,stack会自动处理回复 L2CAP_DISCONNECTION_RSP,用户无需进行其他操作。

(5) 建立基于credit的连接请求与回复 在BLE协议栈中,slave通过L2CAP层的 L2CAP_CREDIT_BASED_CONNECTION_REQ 命令向 master 申请建立基于credit的CoC连接。详情请参考《Core_v5.4》(Vol 3/Part A/4.25 L2CAP_CREDIT_BASED_CONNECTION_REQ (CODE 0x17))。

该SDK在L2CAP层上提供了slave主动申请建立CoC通道的API,用来向master发送上面这个 L2CAP_CREDIT_BASED_CONNECTION_REQ 命令。

ble_sts_t blc_l2cap_createCreditBasedConnect(u16 connHandle, u8 srcCnt);
其中参数srcCnt表示单次建立CoC通道的数量,最大可以为5。但在此SDK中,不支持单次建立多个CoC连接。 如果由 master 申请建立连接,收到对端发送的 L2CAP_CREDIT_BASED_CONNECTION_REQ 命令后,stack会自动处理回复 L2CAP_CREDIT_BASED_CONNECTION_RSP。 需要注意的是,基于credit建立的连接与基于LE credit建立的连接不同之处在于,使用 L2CAP_CREDIT_BASED_CONNECTION_REQ 可以一次最多建立5条连接链路。但在此SDK中,由于只支持一次建立一条连接,所以我们建议客户使用 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ 建立连接链路。

对于其他未进行介绍的指令,在此SDK中仅限stack中使用,用户无需关注。

CoC发送数据

蓝牙官方定义了一个带协议元素的PDU用于在基于LE credit的流控模式中使用,这种在CoC通道中使用的L2CAP PDU称为K帧。K帧的基本格式如下:

"BLE协议栈中 K帧 格式"

建立连接时规定了MTU和MPS的大小,在传输数据时,Information Payload的长度不能大于MPS的值,L2CAP SDU Length的数据长度不能大于MTU的值。 该SDK在L2CAP层上提供了本地发送CoC数据的API:

ble_sts_t blc_l2cap_sendCocData(u16 connHandle, u16 srcCID, u8* data, u16 dataLen);
用户在实际使用时,只需保证dataLen小于MTU即可,底层会自动进行分包处理。

CoC Event

CoC event属于host event,所以可以使用3.3.5.2 GAP Event章节中介绍的方法注册CoC事件的回调以及打开eventMask。

blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask(   GAP_EVT_MASK_L2CAP_COC_CONNECT          |  \
                        GAP_EVT_MASK_L2CAP_COC_DISCONNECT       |  \
                        GAP_EVT_MASK_L2CAP_COC_RECONFIGURE      |  \
                        GAP_EVT_MASK_L2CAP_COC_RECV_DATA        |  \
                        GAP_EVT_MASK_L2CAP_COC_SEND_DATA_FINISH |  \
                        GAP_EVT_MASK_L2CAP_COC_CREATE_CONNECT_FINISH
                        );
对应的event mask用户可以在ble/gap/gap_event.h中查到。

(1) GAP_EVT_MASK_L2CAP_COC_CONNECT 事件触发条件:成功建立CoC连接。 回传指针p:指向一片内存数据,对应如下结构体:

typedef struct{
    u16 connHandle;
    u16 spsm;
    u16 mtu;
    u16 srcCid;
    u16 dstCid;
} gap_l2cap_cocConnectEvt_t;
connHandle表示当前连接句柄。spsm,mtu,srcCid和dstCid分别为此次建立的CoC连接的参数。

(2) GAP_EVT_MASK_L2CAP_COC_DISCONNECT 事件触发条件:CoC连接断开。 回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u16 srcCid;
    u16 dstCid;
} gap_l2cap_cocDisconnectEvt_t;
connHandle表示当前连接句柄。srcCid和dstCid为此次断开的CoC连接的CID。

(3) GAP_EVT_MASK_L2CAP_COC_RECONFIGURE 事件触发条件:CoC通道参数更新。 回传指针p:指向一片内存数据,对应如下结构体:

typedef struct {
    u16 connHandle;
    u16 srcCid;
    u16 mtu;
} gap_l2cap_cocReconfigureEvt_t;
connHandle表示当前连接句柄。srcCid和mtu分别表示srcCid所对应的CoC通道和新的MTU大小。

(4) GAP_EVT_MASK_L2CAP_COC_RECV_DATA 事件触发条件:CoC通道接收到数据。 回传指针p:指向一片内存数据,对应如下结构体:

typedef struct{
    u16 connHandle;
    u16 dstCid;
    u16 length;
    u8* data;
} gap_l2cap_cocRecvDataEvt_t;
connHandle表示当前连接句柄。dstCid用于区分来自哪条CoC通道的数据,length和data表示数据长度和数据指针。

(5) GAP_EVT_MASK_L2CAP_COC_SEND_DATA_FINISH 事件触发条件:用户调用API blc_l2cap_sendCocData成功发送数据。 回传指针p:指向一片内存数据,对应如下结构体:

typedef struct{
    u16 connHandle;
    u16 srcCid;
} gap_l2cap_cocSendDataFinishEvt_t;
connHandle表示当前连接句柄。srcCid用于区分发送给哪条CoC通道的数据。

(6) GAP_EVT_MASK_L2CAP_COC_CREATE_CONNECT_FINISH 事件触发条件:用户调用API blc_l2cap_createLeCreditBasedConnect 或 blc_l2cap_createCreditBasedConnect 建立CoC连接,发送成功即会触发事件。 回传指针p:指向一片内存数据,对应如下结构体:

typedef struct{
    u16 connHandle;
    u8 code;
    u16 result;
} gap_l2cap_cocCreateConnectFinishEvt_t;
connHandle表示当前连接句柄。 code: a) 0xFF表示ACL连接异常断开,结束流程。 b) L2CAP_COMMAND_REJECT_RSP c) L2CAP_LE_CREDIT_BASED_CONNECTION_RSP d) L2CAP_CREDIT_BASED_CONNECTION_RSP result:成功建立CoC通道则为0x00,否则返回错误码。

低功耗管理(PM)

低功耗管理(Low Power Management)也可以称为功耗管理(Power Management),本文档中会简称为PM。

低功耗驱动

低功耗模式

B85m MCU正常执行程序时处于working mode,此时工作电流在3~7mA之间。如果需要省功耗需要进入低功耗模式。

低功耗模式(low power mode)又称sleep mode,包括3种:suspend mode、deepsleep mode和deepsleep retention mode,使用者需要注意,A0版本的芯片不支持suspend mode。

Module suspend deepsleep retention deepsleep
SRAM 100% keep first 16K(or 32K) 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电流在60~70 uA之间。当suspend被唤醒后,程序继续执行。

suspend mode下所有的SRAM和analog register都能保存状态。SDK中为了降低功耗,在进入suspend低功耗处理时已经对部分模块设置了掉电模式,这时该模块的digital register也会掉电,唤醒后必须需要重新进行初始化配置,包括:

a) baseband电路中少量的digital register,user需要关注的是API rf_set_power_level_index设置的寄存器,本文档前面已经介绍,这个API需要在每次suspend醒来后都重新调用一次。此外,不建议用户在非idle状态调用cpu sleep wakeup进行睡眠,这会导致RF部分配置丢失,工作异常,此时则需要调用rf_drv_ble_init和rf_set_power_level_index重新配置RF。

b) 控制Dfifo状态的digital register。对应drivers/8258(8278)/dfifo.h中的相关API,user在使用这些API的时候,必须确保每次suspend wake_up后都要重新设置。

(2) Deepsleep mode (sleep mode 2)

此时程序停止运行,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。在deepsleep mode下IC电流小于1uA。如果内置flash的standby电流出现较大的1uA左右,可能导致测量到deepsleep为1~2uA。deepsleep mode wake_up时,MCU将重新启动,类似于上电的效果,程序会重新开始进行初始化。

Deepsleep mode下,除了analog register上有少数几个register能保存状态,其他所有SRAM、digital register、analog register全部掉电丢失。

(3) Deepsleep retention mode (sleep mode 3)

上面的deepsleep mode,电流很低,但是无法存储SRAM信息;suspend mode SRAM和register可以保持不丢,但是电流偏高。

为了实现一些需要sleep时电流很低又要能够确保sleep唤醒后能立刻恢复状态的应用场景(比如BLE长睡眠维持连接),B85m增加了一种sleep mode 3:deepsleep with SRAM retention mode,简称deepsleep retention(或deep retention)。根据SRAM retention area的大小不同,又分为deepsleep retention 16K SRAM和deepsleep retention 32K SRAM。

Deepsleep retention mode也是一种deepsleep,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。功耗是在deepsleep mode基础上增加retention SRAM消耗的电,电流在2~3uA之间。deepsleep mode wake_up时,MCU将重新启动,程序会重新开始进行初始化。

deepsleep retention mode和deepsleep mode 在register状态保存方面表现一致,几乎全部掉电。deepsleep retention mode跟deepsleep mode相比,SRAM的前16K(或前32K)可以保持不掉电,剩余的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模块的介绍。使用GPIO output属于digital register控制的状态。B85m在suspend期间可以用GPIO output来控制一些外围设备,但被切换为deepsleep retention mode后,GPIO output状态失效,无法在sleep期间准确的控制外围设备。此时可以使用GPIO模拟上下拉电阻的状态来代替实现:上拉10K代替GPIO output high,下拉100K代替GPIO output low。

b) PM模块特殊的不掉电analog regsiter

drivers/8258(8278)/pm.h文件中的DEEP_ANA_REG,如下code所示:

#define DEEP_ANA_REG0                       0x3a //initial value =0x00 [Bit1] The crystal oscillator failed to start normally.The customer cannot change!
#define DEEP_ANA_REG1                       0x3b //initial value =0x00
#define DEEP_ANA_REG2                       0x3c //initial value =0x00

需要注意的是,客户不允许使用ana_3c,该模拟寄存器留给底层stack使用,如果应用层代码有用到该寄存器,需要修改为ana_3a、ana_3b。因为不掉电模拟寄存器数量比较少,建议客户使用其每一个bit指示不同的状态位信息,具体可以参考SDK的vendor目录下的“b85m_ble_remote”。

如下几组不掉电模拟寄存器可能会因为错误的GPIO唤醒而丢掉信息,比如GPIO_PAD高电平唤醒deepsleep,但是在调用cpu_sleep_wakeup函数前GPIO已经为高电平,这就会导致错误的GPIO唤醒,那么这些模拟寄存器值将会丢失。

#define DEEP_ANA_REG6    0x35
#define DEEP_ANA_REG7    0x36
#define DEEP_ANA_REG8    0x37
#define DEEP_ANA_REG9    0x38
#define DEEP_ANA_REG10   0x39

使用者可以在这几个analog register中保存一些重要的的信息,deepsleep/deepsleep retention wake_up后还可以读到之前存储的值。

低功耗唤醒源

B85m 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(4),
    PM_WAKEUP_TIMER = BIT(6),
}SleepWakeupSrc_TypeDef;

"B85 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()中设置该唤醒源即可。

  • 唤醒源PM_WAKEUP_PAD来自GPIO模块,除MSPI 4个管脚外所有的GPIO(PAx/PBx/PCx/PDx)的高低电平都具有唤醒功能。

配置GPIO PAD唤醒sleep mode的API:

typedef enum{
    Level_Low=0,
    Level_High =1,
} GPIO_LevelTypeDef; 
void cpu_set_gpio_wakeup (GPIO_PinTypeDef pin, GPIO_LevelTypeDef pol, int en);

pin为GPIO定义。

pol为唤醒极性定义: Level_High表示高电平唤醒,Level_Low表示低电平唤醒。

en: 1表示enable,0表示disable。

举例说明:

cpu_set_gpio_wakeup (GPIO_PC2, Level_High, 1);  //GPIO_PC2 PAD唤醒打开, 高电平唤醒
cpu_set_gpio_wakeup (GPIO_PC2, Level_High, 0);  //GPIO_PC2 PAD唤醒关闭
cpu_set_gpio_wakeup (GPIO_PB5, Level_Low, 1);  //GPIO_PB5 PAD唤醒打开, 低电平唤醒
cpu_set_gpio_wakeup (GPIO_PB5, Level_Low, 0);  //GPIO_PB5 PAD唤醒关闭

低功耗模式的进入和唤醒

设置MCU进入睡眠和唤醒的API为:

int cpu_sleep_wakeup (SleepMode_TypeDef sleep_mode, SleepWakeupSrc_TypeDef wakeup_src, 
unsigned int wakeup_tick);
  • 第一个参数sleep_mode:设置sleep mode,有以下4个选择,分别表示suspend mode、deepsleep mode、deepsleep retention 16K SRAM、deepsleep retention 32K SRAM。
typedef enum {
        SUSPEND_MODE                        = 0,
        DEEPSLEEP_MODE                      = 0x80,
        DEEPSLEEP_MODE_RET_SRAM_LOW16K      = 0x43,
        DEEPSLEEP_MODE_RET_SRAM_LOW32K      = 0x07,
}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不能超过234s,若需要更长的睡眠时间,user可以调用长睡眠函数,具体可参考4.2.7章节。

cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER, clock_time() + delta_tick);

返回值为当前sleep mode的唤醒源的集合,该返回值各bit对应表示的唤醒源为:

enum {
    WAKEUP_STATUS_TIMER  = BIT(1),
    WAKEUP_STATUS_PAD    = BIT(3),

    STATUS_GPIO_ERR_NO_ENTER_PM  = BIT(8),
};

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唤醒错误:比如cpu_sleep_wakeup设置了PM_WAKEUP_PAD唤醒源,并设置为某个GPIO PAD高电平唤醒,但在调用cpu_sleep_wakeup进入suspend时GPIO 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硬件上执行一些固定的动作,这些动作固化在硬件上,软件无法修改。

举几个例子说明一下这些动作,比如:读flash的boot 启动标记,判断当前应该运行的firmware是存储在flash地址 0上的,还是在flash地址0x20000上的(跟OTA相关);读flash相应位置的值,判断当前需要从flash上拷贝多少数据到SRAM,作为常驻内存的数据(参考第2章对SRAM分配的介绍)。运行硬件bootloader部分由于涉及到flash上数据拷贝到SRAM,一般执行时间较长,比如拷贝10K数据大概耗时5ms左右。

(2) 运行软件bootloader(Run software bootloader)

hardware bootloader运行结束之后,MCU开始运行software bootloader。Software bootloader就是前面介绍过的vector段(b85m sdk的boot目录下的.S汇编程序)。

Software bootloader是为了给后面C语言程序的运行设置好内存环境,可以理解为整个内存的初始化。

(3) 系统初始化(System initialization)

System initialization对应main函数中cpu_wakeup_init到user_init之前各硬件模块初始化(包括cpu_wakeup_init、rf_drv_ble_init、gpio_init、clock_init),设置各硬件模块的数字/模拟寄存器状态。

(4) 用户初始化(User initialization)

User initialization分为User initialization normal和User initialization deep retention两种,分别对应SDK中函数user_init_normal和 user_init_deepRetn。对于不使用deep retention的工程,比如kma master dongle,只有user_init,等同于user_init_normal。

user_init和user_init_normal需要把所有的初始化都做一遍,时间较长。

user_init_deepRetn只需要做一些硬件相关的初始化,无需软件初始化,因为在设计的时候将软件初始化涉及到的变量都放到了MCU retention区域中,deep retention期间这些变量保持不变,醒来后无需重设。所以user_init_deepRetn是一个加速模式,节省了时间也就节省了功耗。

(5) main_loop

User initialization完成后,进入while(1)控制的main_loop。main_loop中进入sleep mode之前的一系列操作称为"Operation Set A”,sleep 唤醒之后一系列操作称为"Operation Set B”。

对照上图 sleep mode wakeup work flow,sleep mode流程分析:

(1) no sleep

如果没有sleep mode,MCU的运行流程为在while(1)中循环,反复执行“Operation Set A” ->“Operation Set B”。

(2) suspend

如果调用cpu_sleep_wakeup函数进入suspend mode,当suspend被唤醒后,相当于cpu_sleep_wakeup函数的正常退出,MCU运行到"Operation Set B”。

suspend是最干净的sleep mode,在suspend期间所有的SRAM数据能保持不变,所有的数字/模拟寄存器状态也保持不变(只有几个特殊的例外);suspend唤醒后,程序接着原来的位置运行,几乎不需要考虑任何SRAM和寄存器状态的恢复。suspend的缺点是功耗偏高。

(3) deepsleep

如果调用cpu_sleep_wakeup函数进入deepsleep mode,当deepsleep被唤醒后,MCU会重新回到Run hardware bootloader。

可以看出,deepsleep wake_up跟 Power on的流程是几乎一致的,所有的软硬件初始化都得重新做。

MCU进入deepsleep后,所有的SRAM和数字/模拟寄存器(只有几个模拟寄存器例外)都会掉电,所以功耗很低,MCU电流小于1uA。

(4) 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只保留前16K(或32K)不掉电,所以功耗比suspend低很多,只有2uA左右。

deepsleep wake_up后需要把所有的流程重新运行一遍,而deepsleep retention可以跳过"Run hardware bootloader”这一步,这是因为SRAM的前16K(32K)上数据是不丢的,不需要再从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。

B85m 的sleep mode分3种:suspend、deepsleep、deepsleep retention。

对于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 “b85m_ble_remote” 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;
    u16     sys_latency;
    u16     user_latency;
    u32     deepRet_advThresTick;
    u32     deepRet_connThresTick;
    u32     deepRet_earlyWakeupTick;
}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的差别有分为16K SRAM retention和32K SRAM retention。当sleep mode中deepsleep retention mode生效时,SDK会根据user的设置进入相应的deepsleep retention mode。

下面API供user选择deepsleep retention mode。

void blc_pm_setDeepsleepRetentionType(SleepMode_TypeDef sleep_type);

可选的模式只有以下两种:

typedef enum {
        DEEPSLEEP_MODE_RET_SRAM_LOW16K      = 0x43,
        DEEPSLEEP_MODE_RET_SRAM_LOW32K      = 0x07,
}SleepMode_TypeDef;
V3.4.2.4及之后版本的SDK会根据编译时计算出的_retention_size_自动设置retention的大小,这部分功能是通过API blc_app_setDeepsleepRetentionSRAMSize实现的。关于此API的详细介绍请参考章节SDK V3.4.2.4及之后版本的SRAM空间分配。

V3.4.2.4之前版本的SDK中默认的deepsleep retention mode为DEEPSLEEP_MODE_RET_SRAM_LOW16K,user如果需要retention 32K SRAM,初始化的时候调用如下code即可。

blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K);

注意:

  • 该API的调用必须在blc_ll_initPowerManagement_module之后才能生效。

参考本文档第2章可知,V3.4.2.4之前版本SDK的SRAM内存分配默认是按照deepsleep retention 16K SRAM设计的;根据第1章描述,我们知道选用不同的IC以及deep retention size值,需要选择不同的software bootloader启动文件和boot.link,具体的映射关系及修改设置方法参考“software bootloader介绍”小节。

如果当前IC为8258,使用deepsleep retention 32K SRAM,则需要以下两个步骤的修改:

Step 1 选择software bootloader文件为cstartup_8258_RET_32K.S;

Step 2 根据修改boot.link文件:将SDK/boot/boot_32k_retn_8253_8258.link文件内容替换到SDK根目录下的boot.link文件中。

其他IC的设置同上类似,用户根据实际情况修改即可。

V3.4.2.4及之后版本的SDK,其deep retention size值根据编译时计算出的_retention_size_自动设置,用户只需要在初始化阶段调用API blc_app_setDeepsleepRetentionSRAMSize即可。

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函数的逻辑实现如下所示。

注意:

  • 这里以默认的deepsleep retention16K SRAM来说明。
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_LOW16K, 
PM_WAKEUP_TIMER | bltPm.wakeup_src,T_wakeup); //suspend
//MCU被唤醒后PC值reset to 0, 将重新执行software bootloader //(cstartup_8258_16K.S)、system initialization等
}
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_LOW16K, 
PM_WAKEUP_TIMER | bltPm.wakeup_src,T_wakeup); //suspend
//MCU被唤醒后PC值reset to 0, 将重新执行software bootloader//(cstartup_8258_16K.S)、system initialization等
}
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; 
}

上面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也一样。

deepsleep retention的详细分析

引入deepsleep retention,对上面的软件处理流程继续分析。

当应用层按如下设置时,deepsleep retention mode被打开。

bls_pm_setSuspendMask( SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);

(1) 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);

以一个 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的功耗都会更低。

(2) 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略大一点,但不能小于这个值。

(3) 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。BLE SDK中,所有的user initialization都是用下面函数即可完成。该 BLE SDK中"b85m_master_kma_dongle” project也一样。

void user_init(void);

对于B85m中使用了deepsleep retention mode的应用,为了尽量降低功耗,T_userInit需要最优化。所以需要做区分,deepsleep retention wake_up时,user initialization需要尽量快。 user_init函数中所有的initialization可以被分为两类:一是硬件寄存器的初始化,二是SRAM上逻辑变量的初始化。

根据deepsleep retention mode可以保持SRAM前16K(32K)不掉电的特性,我们可以将逻辑变量定义成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 “b85m_ble_remote” user_init_deepRetn函数的实现。

_attribute_ram_code_ void user_init_deepRetn(void)
{
#if (PM_DEEPSLEEP_RETENTION_ENABLE)
    blc_app_loadCustomizedParameters();
    blc_ll_initBasicMCU();   //mandatory
    rf_set_power_level_index (MY_RF_POWER_INDEX);
    blc_ll_recoverDeepRetention();
    app_ui_init_deepRetn();
#endif
}

前3句(code blc_app_loadCustomizedParameters到rf_set_power_level_index)是BLE初始化中必不可少的相关硬件寄存器的初始化;

blc_ll_recoverDeepRetention是对Link Layer相关软硬件状态的恢复,由stack底层处理。

以上几个初始化都属于固定写法,user不要去修改。

最后app_ui_init_deepRetn是user对应用层使用到的硬件寄存器的重新初始化。SDK demo “b85m_ble_remote”中GPIO的唤醒设置、Led灯状态的设置都属于硬件初始化。SDK demo ”b85m_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_userInit在Conn state slave role的优化

待补充。

4) T_cstartup

T_cstartup是执行cstartup_xxx.S(比如cstartup_8258_RET_16K.S)所消耗的时间,请user参考SDK中boot文件。

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如下:

    tloadr      r1, DATA_I
    tloadr      r2, DATA_I+4
    tloadr      r3, DATA_I+8
COPY_DATA:
    tcmp        r2, r3
    tjge        COPY_DATA_END
    tloadr      r0, [r1, #0]
    tstorer     r0, [r2, #0]
    tadd        r1, #4
    tadd        r2, #4
    tj          COPY_DATA
COPY_DATA_END:

因为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:

    tmov      r0, #0
    tloadr    r1, DAT0 + 16
    tloadr    r2, DAT0 + 20
ZERO:
    tcmp      r1, r2
    tjge      ZERO_END
    tstorer   r0, [r1, #0]
    tadd      r1, #4
    tj        ZERO
ZERO_END:

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”段上。

5) T_init测量

根据以上介绍,对T_cstartup和T_userInit的时间做优化,将T_init优化到最小时间,然后需要测量出T_init,填到API blc_pm_setDeepsleepRetentionEarlyWakeupTiming。

T_init的起点即T_cstartup的起点。T_cstartup的起点是cstartup_8258_RET_16K.S文件中“__reset”这个点,如下code所示。

__reset:

#if 0
    @ add debug, PB4 output 1
    tloadr      r1, DEBUG_GPIO    @0x80058a  PB oen
    tmov        r0, #139      @0b 11101111
    tstorerb    r0, [r1, #0]

    tmov        r0, #16         @0b 00010000
    tstorerb    r0, [r1, #1]    @0x800583  PB output
#endif  

结合图“ T_init timing”中Debug GPIO的示意,在“__reset”这里放了Debug GPIO PB4输出高的操作,user只要将“#if 0”改成“#if 1”便可打开PB4输出高的操作。

T_cstartup结束时间是下面“tjl main”的时间点。

tjl main
END:    tj  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
    cpu_wakeup_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 = bls_calculateLatency();

在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

由于B85m的GPIO 唤醒是靠高低电平唤醒,而不是上升沿下降沿唤醒,所以当配置了GPIO PAD唤醒时,比如设置了某个GPIO PAD高电平唤醒suspend,要确保MCU在调用cpu_wakeup_sleep进入suspend时,当前的这个GPIO读到的电平不能是高电平。若当前己经是高电平了,实际进入cpu_wakeup_sleep函数里面,触发suspend时是无效的,会立刻退出来,即完全没有进入suspend。

如果出现以上情况,可能会造成意想不到的问题,比如本来想进入deepsleep后被唤醒,程序重新执行,结果MCU无法进入deepsleep,导致code继续运行,不是我们预想的状态,整个程序的flow可能会乱掉。

user在使用Telink的GPIO PAD唤醒时,要注意避免这个问题。

如果应用层没有很好的规避这个问题,在调用cpu_wakeup_sleep函数时发生了GPIO PAD唤醒源已经生效的情况,为了防止程序进入不可预知的逻辑,PM driver做了一些改善:

(1) Suspend & deepsleep retention mode

如果是suspend和deepsleep retention mode,都会很快退出函数cpu_wakeup_sleep,该函数给出的返回值可能出现两种情况:

  • 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 “b85m_ble remote”中,做了相应的处理。

在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 “b85m_ble remote” 低功耗管理的参考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) 825x工作电压的范围为1.8V~3.6V。当电压低于1.8V时,825x已经无法保证稳定的工作。

b) 当电池电压较低时,由于电源的不稳定,Flash的“write”和“erase”操作可能有出错的风险,造成program firmware和用户数据被异常修改,最终导致产品失效。根据以往的量产经验,我们将这个可能出风险的低压阀值设定为2.0V。

根据上面的描述,使用电池供电的产品,必须设定一个安全电压值(secure voltage),只有当电压高于这个安全电压的时候才允许MCU继续工作;一旦电压低于安全电压,MCU停止运行,需要立刻被shutdown(SDK上使用进入deepsleep mode来实现)。

安全电压也称为报警电压,这个电压值的选取,目前SDK默认使用2.0V。

注意:

  • 低压保护阈值数值2.0V只是示例、参考值。客户要根据实际情况评估修改这个阈值,如果user在硬件电路中出现了不合理的设计,导致电源网络稳定性降低,都要酌情提高安全阈值。

对于Telink BLE SDK开发实现的产品,只要使用了电池供电,低电检测都必须是该产品整个生命周期实时运行的任务,以保证产品的稳定性。

低电压检测对于Flash操作的保护,在Flash介绍的章节中会继续介绍。

低电检测的实现

低电检测需要使用ADC对电源电压进行测量。user请参考文档8258/8278 Datasheet相关ADC章节,先对B85m的ADC模块进行必要的了解。

低电检测的实现,结合SDK demo “b85m_ble_sample/b85m_ble_remote/b85m_module”给出的实现来说明,参考文件battery_check.h和battery_check.c。

必须确保app_config.h文件中宏“APP_BATT_CHECK_ENABLE”是被打开的,这个宏默认是关闭的,user使用低电检测功能时需要注意。

#define APP_BATT_CHECK_ENABLE                   1

低电检测的注意事项

低电检测是一个基本的ADC采样任务,在实现ADC采样电源电压时,有一些需要注意的问题,说明如下:

(1) 建议使用GPIO输入通道

825x的ADC输入通道上支持在“VCC/VBAT”输入通道上对电源电压进行ADC采样,对应下面变量ADC_InputPchTypeDef中最后一个“VBAT”。

但由于某些特殊的原因,825x的“VBAT”channel不能使用,所以Telink规定:不允许使用“VBAT”输入通道,必须使用“GPIO”输入通道。

可用的“GPIO”输入通道为PB0~PB7、PC4、PC5对应的input channel。

/*ADC analog input positive channel*/
typedef enum {
……
    B0P,
    B1P,
    B2P,
    B3P,
    B4P,
    B5P,
    B6P,
    B7P,
    C4P,
    C5P,
……
    VBAT,
}ADC_InputPchTypeDef;

使用GPIO input channel对电源电压进行ADC采样,有两种实现方式。

a) 电源连接到GPIO input channel

在硬件电路设计上,将电源直接和GPIO input channel连接。ADC初始化时,将GPIO设为高阻态(ie、oe、output全部设0),此时GPIO上的电压等于电源电压,直接进行ADC采样即可。

b) 电源不接触GPIO input channel

硬件电路上不需要电源和GPIO input channel连接。需要借助GPIO的输出高电平来测量。825x内部电路结构设计可以保证GPIO输出高电平的电压值和电源电压值永远相等。 那么GPIO输出的高电平可以作为电源电压,通过该GPIO input channel进行ADC采样。

目前低压检测选择的GPIO input channel是PB7,采用了第2种“电源不接触GPIO input channel”方式。

选择PB7为GPIO input channel,PB7作为普通GPIO功能,初始化时所有状态(ie、oe、output)使用默认状态即可,不做特殊修改。

#define GPIO_VBAT_DETECT       GPIO_PB7
#define PB7_FUNC               AS_GPIO
#define PB7_INPUT_ENABLE       0
#define ADC_INPUT_PCHN         B7P

需要进行ADC采样时,PB7输出高电平:

gpio_set_output_en(GPIO_VBAT_DETECT, 1);
gpio_write(GPIO_VBAT_DETECT, 1);
ADC采样结束后,可以将PB7的输出态关掉。由于硬件电路上PB7管脚是悬空的(没有和其他电路连接),输出高电平并不会造成任何漏电,所以SDK上并没有将PB7的输出态关掉。

(2) 只能使用差分模式

虽然825x 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中低压检测使用了差分模式,code如下。“#if 1”与“#else”分支是完全相同的功能设置,“#if 1”只是为了让code运行更快以节省时间。可以通过看“#else”来理解,adc_set_ain_channel_differential_mode API中选择了PB7作为positive input channel,GND作为negative input channel。

#if 1  //optimize, for saving time
    //set misc channel use differential_mode,
    //set misc channel resolution 14 bit,  misc channel differential mode
    //notice that: in differential_mode MSB is sign bit, rest are data,  here BIT(13) is sign bit
    analog_write (anareg_adc_res_m, RES14 | FLD_ADC_EN_DIFF_CHN_M);
    adc_set_ain_chn_misc(ADC_INPUT_PCHN, GND);
#else
    ////set misc channel use differential_mode,
    adc_set_ain_channel_differential_mode(ADC_MISC_CHN, ADC_INPUT_PCHN, GND);
    //set misc channel resolution 14 bit
    //notice that: in differential_mode MSB is sign bit, rest are data,  here BIT(13) is sign bit
    adc_set_resolution(ADC_MISC_CHN, RES14);
#endif

(3) 必须使用Dfifo模式获得ADC采样值

对于825x,Telink规定:只能使用Dfifo模式来实现ADC采样值的读取。可参考driver中如下函数的实现。

unsigned int adc_sample_and_get_result(void);

(4) 不同的ADC任务需要切换

参考文档8258/8278 Datasheet可知,ADC 状态机包括Left、Right、Misc等几个channel。 由于一些特殊的原因,这些state channel无法同时工作,Telink规定:ADC状态机中的channel必须独立运行,不能同时工作。

低压检测作为一种最基本的ADC采样,使用的是Misc channel。User如果需要除低压检测外其他的ADC的任务,也需要使用Misc channel。Amic Audio使用的是Left channel。低压检测无法与其他ADC任务同时运行,必须采用切换的方式来实现。

低电检测的单独使用

User将“b85m_ble_remote” app_config.h文件中的宏“BLE_AUDIO_ENABLE”定义为0(关闭Audio所有功能),就可以获得ADC只被低压检测使用的demo。或者直接参考“b85m_ble_sample”和“b85m_module”的低压检测demo。

(1) 低电检测初始化

参考adc_vbat_detect_init函数的实现。

ADC初始化的顺序必须满足下面的流程:先power off(掉电)sar adc,然后配置其他参数,最后power on(上电)sar adc。所有ADC采样的初始化都必须遵循这个流程。

void adc_vbat_detect_init(void) 
{
    /******power off sar adc********/
    adc_power_on_sar_adc(0);

    //add ADC configuration

    /******power on sar adc********/
    //note: this setting must be set after all other settings
    adc_power_on_sar_adc(1);
}

Sar adc power on与power off之间的配置,user尽量不要去修改,使用这些默认的设置就行。User如果选择了不同的GPIO input channel,直接修改宏“ADC_INPUT_PCHN”的定义即可。User的硬件电路如果采用了“电源连接到GPIO input channel”的设计,需要将“GPIO_VBAT_DETECT”输出高电平的操作去掉。

adc_vbat_detect_init初始化函数在app_battery_power_check中调用的code为:

if(!adc_hw_initialized){
    adc_hw_initialized = 1;
    adc_vbat_detect_init();
}

这里使用了一个变量adc_hw_initialized,只有该变量为0时调用一次初始化,并将其置1;该变量为1时不再初始化。adc_hw_initialized清零的操作调用API battery_clear_adc_setting_flag完成:

static inline void battery_clear_adc_setting_flag (void)
{
    adc_hw_initialized = 0;
}

使用了adc_hw_initialized的设计可以实现的功能有:

a) 与其他ADC任务(“ADC other task”)的切换

先不考虑sleep mode (suspend/deepsleep retention)的影响,只分析低电检测与其他ADC任务的切换。

因为需要考虑低电检测与其他ADC任务的切换使用,可能出现adc_vbat_detect_init被多次执行,所以不能写到user_init_normal中,必须在main_loop里实现。

第一次执行app_battery_power_check函数时,adc_vbat_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_vbat_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;而sleep wake_up后这个变量可以保持不变。

adc_vbat_detect_init函数里面配置的register的共同特征是:在suspend mode下不掉电,可以保存状态;在deepsleep retention mode下会掉电。

如果MCU进入suspend mode,醒来后再次执行app_battery_power_check时,adc_hw_initialized的值和suspend之前一致,不需要重新执行adc_vbat_detect_init函数。

如果MCU进入deepsleep retention mode,醒来后adc_hw_initialized为0,必须重新执行adc_vbat_detect_init,ADC相关的register状态需要被重新配置。

adc_vbat_detect_init函数中设定register的状态可以在suspend期间保持不掉电。

参考文档“低功耗管理”部分对suspend mode的说明可知,Dfifo相关的寄存器在suspend mode会掉电,所以以下两句code没有放到 adc_vbat_detect_init函数中,而是在app_battery_power_check函数中,确保每次低电检测前都重新设置。

  adc_config_misc_channel_buf((u16 *)adc_dat_buf,ADC_SAMPLE_NUM<<2);  
  dfifo_enable_dfifo2();

SDK中对adc_vbat_detect_init函数添加了关键字“_attribute_ram_code_”以设置为ram_code,最终目的是为了优化长睡眠连接态的功耗。比如对典型的 10ms * (99+1) = 1s 的长睡眠连接,每1s醒来一次,中间的长睡眠使用的是deepsleep retention mode,那么每次醒来后adc_vbat_detect_init一定会重新执行一次,加入到ram_code后执行速度会变得更快。

这个“_attribute_ram_code_”不是必须的。在产品应用中,user可以根据deepsleep retention area的使用情况,结合功耗测试的结果,来决定是否将此函数放入到ram_code中。

(2) 低电检测处理

在main_loop中,调用app_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){
        battery_clear_adc_setting_flag();   //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(VBAT_DEEP_THRES_MV);
}

lowBattDet_enable默认值为1,低电检测是默认允许的,MCU上电后立刻可以开始低电检测。该变量需要设置成retention_data,确保deepsleep retention不能修改它的状态。

只有在其他ADC任务需要抢占ADC使用权时,才能改变lowBattDet_enable的值:当其他ADC任务开始时,调用battery_set_detect_enable(0),此时main_loop中不会再调用app_battery_power_check函数;在其他ADC任务结束后,调用battery_set_detect_enable(1),交出ADC使用权,此时main_loop中又可以调用app_battery_power_check函数。

通过变量lowBattDet_tick来控制低电检测的频率,Demo中为每500ms执行一次低电检测。User可以根据自己的需求来修改这个时间值。

app_battery_power_check函数的具体实现看起来比较繁琐,涉及到低电检测的初始化、Dfifo的准备、数据的获取、数据的处理、低电报警的处理等等。

ADC采样数据的获取使用了Dfifo mode,Dfifo默认采样8笔数据,去掉最大最小值后计算平均值。

adc_vbat_detect_init函数里可以看到每个adc采样的周期为10.4us,所以获取数据过程大概83us。

可以看到Demo中宏“ADC_SAMPLE_NUM”可以被修改为4,缩短ADC采样时间到41us。推荐使用8笔数据的方法,计算结果会更加准确。

#define ADC_SAMPLE_NUM      8

#if (ADC_SAMPLE_NUM == 4)   //use middle 2 data (index: 1,2)
    u32 adc_average = (adc_sample[1] + adc_sample[2])/2;
#elif(ADC_SAMPLE_NUM == 8)  //use middle 4 data (index: 2,3,4,5)
    u32 adc_average = (adc_sample[2] + adc_sample[3] + adc_sample[4] + adc_sample[5])/4;
#endif

app_battery_power_check函数被放到ram_code上,参考上面对“adc_vbat_detect_init” ram_code的说明,也是为了节省运行时间,优化功耗。

这个“_attribute_ram_code_”不是必须的。在产品应用中,user可以根据deepsleep retention area的使用情况,结合功耗测试的结果,来决定是否将此函数放入到ram_code中。

_attribute_ram_code_ int app_battery_power_check(u16 alram_vol_mv)

(3) 低压报警

app_battery_power_check的参数alram_vol_mv指定低电检测的报警电压,单位为mV,当电源电压低于alram_vol_mv时,app_battery_power_check返回0。根据前文介绍,SDK中默认设置为2000mV。在main_loop的低压检测中,当电源电压低于2000mV时,进入低压范围。

低压报警的处理demo code如下所示。低压后必须shutdown MCU,不能再进行其他工作。

“b85m_ble_remote”使用进入deepsleep的方式来实现shutdown MCU,并且设置了按键可以唤醒遥控器。

低压报警的处理,除了必须shutdown外,user可以修改其他的报警行为。

下面code中,使用LED灯做了3次快闪,并添加printf打印提示,告知产品使用者需要充电或更换电池。

#if (UI_LED_ENABLE)  //led indicate
    gpio_set_output_en(GPIO_LED, 1);  //output enable
    for(int k = 0; k < 3; k++){
        gpio_write(GPIO_LED, LED_ON_LEVEL);
        sleep_us(200000);
        gpio_write(GPIO_LED, !LED_ON_LEVEL);
        sleep_us(200000);
    }
#endif

if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
    tlkapi_printf(APP_BATT_CHECK_LOG_EN, "[APP][BAT] The battery voltage is lower than %dmV, shut down!!!\n", (alarm_vol_mv + 200));
} else {
    tlkapi_printf(APP_BATT_CHECK_LOG_EN, "[APP][BAT] The battery voltage is lower than %dmV, shut down!!!\n", alarm_vol_mv);
}

“b85m_ble_remote”被shutdown后,进入可被唤醒的deepsleep mode。此时如果发生按键唤醒,SDK会在user initialization的时候先快速做一次低电检测,而不是等到main_loop中检测。这样处理是为了避免应用上的错误,举例说明如下:

如果低电报警时LED闪烁已经提示了产品使用者,然后进入deepsleep又被唤醒,从main_loop的处理来看,需要至少500ms的时间才会去做低电检测。在500ms之前,slave的广播包已经发很久了,很可能会跟master已经连接上了。这样的话,就出现已经低电报警的设备又继续工作的bug了。

因为这个原因,SDK必须在user initialization的时候就提前做低电检测,必须在这一步就阻止发生上面的情况。所以在user_battery_power_check中,添加低电检测:

if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
    battery_check_returnValue = app_battery_power_check(alarm_vol_mv + 200);
}
并在user initialization中调用:

#if (APP_BATT_CHECK_ENABLE)
    user_battery_power_check(VBAT_DEEP_THRES_MV);
#endif

根据USED_DEEP_ANA_REG模拟寄存器的值可以判断是否是低电报警shutdown被唤醒的情况,此时进行快速低电检测,并且将之前的2000mV报警电压提高到2200mV(称为恢复电压)。提高200mV的原因是:

低压检测会有一些误差,无法保证测量结果的准确性和一致性。比如误差在20mV,可能第一次检测到的电压是1990mV进入shutdown模式,然后唤醒后在user initialization的时候再次检测到的电压值是2005mV。如果还是以2000mV为报警电压的话,还是无法阻止上面描述的bug。

所以需要在shutdown模式唤醒后的快速低电检测时,将报警电压稍微调高一些,调高的幅度比低电检测的最大误差稍大。

只有当某次低电检测发现电压低于2000mV进入shutdown模式后,才会出现恢复电压2200mV,所以用户不用担心这个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处理的芯片,将数字信号读到827x或者825x上;

  • Amic需要使用芯片内部的codec模块,对原始的Audio信号进行采样后处理,最终转化为数字信号传输到MCU。

初始化

Amic和低电检测

参考本文档对“低电检测”的介绍可知,Amic Audio和低电检测使用ADC模块时,必须对ADC进行切换使用。

同理,如果应用上同时出现Amic Audio和其他ADC任务,这2个任务也需要对ADC进行切换使用。如果同时出现Amic Audio、低电检测、其他ADC任务,这3个任务也需要对ADC进行切换使用。

825x/827x Amic需要在Audio任务开启时设置,这样才能实现低电检测和Amic对ADC模块的切换使用。

AMIC初始化设置

参考SDK demo “b85m_8258_ble_remote”语音处理相关code。

    void ui_enable_mic (int en)
    {
        ui_mic_enable = en;

        gpio_set_output_en (GPIO_AMIC_BIAS, en); //AMIC Bias output
        gpio_write (GPIO_AMIC_BIAS, en);

        if(en){  //audio on
            audio_config_mic_buf ( buffer_mic, TL_MIC_BUFFER_SIZE);
            audio_amic_init(AUDIO_16K);
        }
        else{  //audio off
            adc_power_on_sar_adc(0);   //power off sar adc
        }

        #if (BATT_CHECK_ENABLE)
            battery_set_detect_enable(!en);
        #endif
    }

上面ui_enable_mic函数中,en=1对应Audio任务的开启,en=0对应Audio任务的结束。

Audio开始时,GPIO_AMIC_BIAS需要输出高电平来驱动Amic;Audio结束后,GPIO_AMIC_BIAS需要关闭,防止这个管脚在sleep mode漏电。

Amic初始化设置为

audio_config_mic_buf ( buffer_mic, TL_MIC_BUFFER_SIZE);
audio_amic_init(AUDIO_16K);

Audio在工作过程中,使用指定的Dfifo将数据源源不断拷贝到SRAM上。audio_config_mic_buf用于配置该Dfifo在SRAM上的起始地址和长度。

Dfifo的配置在ui_enable_mic函数中处理,相当于每次Audio开始都要重新做一遍,原因是Dfifo控制register在suspend时会掉电丢失。

Audio任务结束的时候,必须关闭SAR ADC,防止在suspend时漏电:

adc_power_on_sar_adc(0);

由于Amic和低电检测需要切换使用ADC模块,在ui_enable_mic函数里添加battery_set_detect_enable(!en),用于关闭和开启低电检测,请参考本文档低电检测部分的介绍。

语音任务的执行放在main_loop的UI entry部分。

#if (BLE_AUDIO_ENABLE)
    if(ui_mic_enable){  //audio
        task_audio();
    }
#endif

DMIC初始化设置

待补充。

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 byte的长包在slave端进行了分包处理,如果希望peer device(对端设备)收到这些包后能够重新拼装成功,就一定要通过Exchange MTU size确定对方peer device的最大ClientRxMTU,只有当ClientRxMTU大于等于128时,slave端的这个128byte长包才能被peer device正确处理。

所以当audio任务开启,需要发送128 byte长包时,会调用blc_att_requestMtuSizeExchange进行Exchange MTU size。

void voice_press_proc(void)
{
    key_voice_press = 0;
    ui_enable_mic (1);
if(ui_mtu_size_exchange_req && blc_ll_getCurrentState() == BLS_LINK_STATE_CONN){
        ui_mtu_size_exchange_req = 0;
        blc_att_requestMtuSizeExchange(BLS_CONN_HANDLE, 0x009e);
    }
}

推荐的做法是:

(1) 通过blc_gap_setEventMask(GAP_EVT_MASK_ATT_EXCHANGE_MTU)打开MUT注册开关;

(2) 然后通过blc_gap_registerHostEventHandler (gap_event_handler_t handler) 注册GAP的回调函数;

(3) 在回调函数中增加if(event==GAP_EVT_ATT_EXCHANGE_MTU)判断语句,实现MTU size Exchange的callback,在callback里去判断peer device的ClientRxMTU是否大于等于128。由于一般master设备的ClientRxMTU都比128大,SDK并没有通过callback判断实际ClientRxMTU。

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数据压缩

根据以上说明,在application/audio/audio_config.h中定义相关的宏:

    #if (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_GATT_TLEINK)
    #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]; //496 sample,992 bytes
config_mic_buffer ((u32)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中的实现。

压缩与解压缩算法

B85m单连接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数据处理流程

B85m SDK的“b85m_ble_remote”和“b85m_master_kma_dongle”工程中包含了多个模式选择,user可以在app_config.h中通过更改宏进行选择,默认为TL_AUDIO_RCU_ADPCM_GATT_TLEINK,即Telink自定义的Audio处理,其相关设定如下:

/* Audio MODE:
 * TL_AUDIO_RCU_ADPCM_GATT_TLEINK
 * 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_TLEINK

由于其中多个模式流程类似,而且默认的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三种流程进行说明。在提供的sdk中实现audio功能,slave端user可参考b85m_ble_remote工程,master端user可参考b85m_master_kma_dongle工程。

注意:

  • 若在设置不同模式时,编译提示报错XX函数或变量缺少定义,这是由于语音相关lib库没添加导致的,User在使用TL_AUDIO_RCU_ADPCM_GATT_GOOGLE、TL_AUDIO_RCU_MSBC_HID、 TL_AUDIO_RCU_SBC_HID,分别需要添加对应的库文件,其中encode标识的对应的为slave端(b85m_ble_remote工程)的编码库,decode标识的对应为master端(b85m_master_kma_dongle)的解码库,工程中库文件目录为下图所示:

"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,整个数据包显示如下:

"Google Voice 数据流程"

注意:

  • 在Dongle端没有采用发送close命令结束语音传输,采用的为超时判断的方式结束语音。具体可参考Master端dongle实现的相关code.

(3) 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组收包后开始进行解压缩还原语音信号。数据包如下:

"HID 语音数据流程"

在准备语音传输结束时,Slave向Master发送End Request,数据包如下:

"End request packet"

Master在收到End Request后发送Ack,数据包如下:

"Ack packet"

(4) 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组收包后开始进行解压缩还原语音信号。数据包如下:

"SBC解码端语音数据传输"

在准备语音传输结束时,Slave向Master发送End Request,数据包如下:

"End request packet"

Master在收到End Request后发送Ack,数据包如下:

"Ack packet"

OTA

为了实现B85m 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_big_pdu。

B85m支持Flash多地址启动:除了Flash的首地址0x00000,还支持从Flash高地址0x20000(128K)、0x40000(256K)、0x80000(512K)读取firmware运行。本文档以高地址0x20000为例来介绍OTA。

Flash架构设计和OTA流程

FLASH存储架构

使用启动地址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。

OTA更新流程

以上面的FLASH存储结构为基础,详细说明OTA程序更新的过程。

首先介绍一下多地址启动机制(只介绍前两个启动地址0x00000和0x20000):MCU上电后,默认从0地址启动,首先去读flash 0x08的内容,若该值为0x4b,则从0地址开始搬移代码到RAM,并且之后所有的取指都是从0地址开始,即取指地址 = 0+PC指针的值;若0x08的值不为0x4b,MCU直接去读0x20008的值,若该值为0x4b,则MCU从0x20000开始搬代码到RAM,并且之后所有的取指都是从0x20000地址开始,即取指地址 = 0x20000+PC指针的值。

所以只要修改0x08和0x20008标志位的值,即可指定MCU执行FLASH哪部分的代码。

SDK上某一次(2n+1或2n+2)上电及OTA过程为:

(1) MCU上电,通过读0x08和0x20008的值和0x4b作比较,确定启动地址,然后从对应的地址启动并执行代码。此功能由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.2.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区域偏移地址0x08(即ota_program_offset+0x08)写为0x4b,将之前老的firmware存储区域偏移地址0x08的地方写为0x00,表示下一次程序启动后将从新的区域搬代码执行。

(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支持修改Firmware Size和启动地址。这个启动地址指的是OTA设计中除了0地址外另一个存储New_firmware的地址(只能是0x20000、0x40000或0x80000)。

Firmware_Boot_address Firmware size (max)/K
0x20000 124
0x40000 252
0x80000 508

SDK中默认的最大firmware size为124K(由于一些特殊的原因,启动地址为0x20000的firmware size不得大于124K),对应的启动地址为0x00000和0x20000。这两个值和前文的描述一致。user可以调用API blc_ota_setFirmwareSizeAndBootAddress来进行设置最大 firmware size和启动地址:

ble_sts_t   blc_ota_setFirmwareSizeAndBootAddress(int firmware_size_k, multi_boot_addr_e boot_addr);

参数firmware_size_k表示最大firmware size,单位K Byte,必须是4K对齐。 对于825x芯片来说,参数boot_addr表示可供选择的启动地址,共有两种(注:825x不支持512 K的启动地址):

typedef enum{
    MULTI_BOOT_ADDR_0x20000     = 0x20000,  //128 K
    MULTI_BOOT_ADDR_0x40000     = 0x40000,  //256 K
}multi_boot_addr_e;

对于827x芯片来说,参数boot_addr表示可供选择的启动地址,共有三种:

typedef enum{
    MULTI_BOOT_ADDR_0x20000     = 0x20000,  //128 K
    MULTI_BOOT_ADDR_0x40000     = 0x40000,  //256 K
    MULTI_BOOT_ADDR_0x80000     = 0x80000,  //512 K
}multi_boot_addr_e;

这个API只能在main函数中cpu_wakeup_init之前调用,否则无效。原因是cpu_wakeup_init函数中需要根据firmware_size和boot_addr的值做一些设置。

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,速度会更快)。

// OTA attribute values
static const u8 my_OtaCharVal[19] = {
    CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP | CHAR_PROP_NOTIFY,
    U16_LO(OTA_CMD_OUT_DP_H), U16_HI(OTA_CMD_OUT_DP_H),
    TELINK_SPP_DATA_OTA, };
    {4,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},
    {0,ATT_PERMISSIONS_RDWR,16,sizeof(my_OtaData),(u8*)(&my_OtaUUID),  (&my_OtaData), &otaWrite, NULL},
    {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支持的功能:

    • OTA Result feedback function:该功能不可选,默认添加

    • 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

注意:

  • Use: To identify the command use in Legacy protocol、Extend protocol or both of all

  • Legacy: Only use in the Legacy protocol

  • Extend: Only use in the Extend protocol

  • All: use both in the Legacy protocol and Extend protocol

(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_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_UNCOMPLETE 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-0xff Reserved for future use /

Reserved: 保留供以后扩展使用

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 0x0020 - 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可以直接使用,如下所示,需开启如下宏:

#define OTA_FW_VERSION_EXCHANGE_ENABLE          1   //user can change
#define OTA_FW_VERSION_COMPARE_ENABLE           1   //user can change

下面将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为补齐的数据。

0xec 0x6e 0xdd 0xa9为第3个~第6个,它是整个firmware bin的CRC_32校验结果。slave在OTA升级过程中会同步计算接收到的整个bin的CRC_32校验值,在收到最后一包数据包时会将该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模式。

若用户使用bls_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提供修改的变量为:

blotaSvr.process_timeout_us = 30 * 1000000;   //default 30 s
blotaSvr.packet_timeout_us = 5 * 1000000;     //default 5 s

初始化该变量后,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 失败。

OTA安全性

OTA Service数据安全

OTA Service是一种GATT service,OTA service安全保护问题就是BLE GATT service data安全保护的问题,即数据不被非法访问。根据BLE Spec的设计,可以使用的方法包括以下:

(1) 开启SMP,建议使用尽量高的Security Level,实现的功能是:只有合法配对的设备,才有访问OTA server数据的权限。参考本文档SMP的介绍。

比如使用Security Mode 1 Level 3,使用了Authentication和MITM的配对,可以有效控制产品slave设备和特定的master才能配对加密成功以及回连,攻击者无法和slave设备加密成功。对被保护的GATT service data的读和写加入相应的安全级别设置,攻击者就无法访问到这些数据了。如果使用Mode 1 Level 4,Secure Connection + Authentication,安全级别更高了。

可能涉及到的code包括以下:

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_Pairing_with_Encryption   = BIT(1),
    LE_Security_Mode_1_Level_3 = BIT(2),  Authenticated_Pairing_with_Encryption     = BIT(2),
    LE_Security_Mode_1_Level_4 = BIT(3),  Authenticated_LE_Secure_Connection_Pairing_with_Encryption = BIT(3),
}le_security_mode_level_t;

#define ATT_PERMISSIONS_AUTHOR               0x10 //Attribute access(Read & Write) requires Authorization
#define ATT_PERMISSIONS_ENCRYPT              0x20 //Attribute access(Read & Write) requires Encryption
#define ATT_PERMISSIONS_AUTHEN               0x40 //Attribute access(Read & Write) requires Authentication(MITM protection)
#define ATT_PERMISSIONS_SECURE_CONN          0x80 //Attribute access(Read & Write) requires Secure_Connection
#define ATT_PERMISSIONS_SECURITY             (ATT_PERMISSIONS_AUTHOR | ATT_PERMISSIONS_ENCRYPT | ATT_PERMISSIONS_AUTHEN | ATT_PERMISSIONS_SECURE_CONN)

(2) 使用whitelist。 用户可以使用白名单,只和自己希望连接的master设备连接,也可以有效拦截攻击者的连接。

(3) 使用地址隐私保护,local device和peer device使用resolvable private address(RPA),有效隐藏对方或我方的身份地址,使用连接更加安全。

OTA RF传输数据完整性

由于RF是不稳定传输,需要一定的保护机制来确保OTA过程中Firmware的完整性和正确性。

参考前面的介绍可知,OTA master需要提前将Firmware按照一定大小分成多个数据包,每个数据包前2byte是包序列号,从0开始加1递增。

(1) LinkLayer数据传输机制

BLE Spec在数据传输完整性方面做了相应的设计:

一是LinkLayer发送端在传输时一笔数据时,需要看到对方的应答之后,才可以切换到下一包数据的传输,用以保证数据传输不会丢失;

二是LinkLayer接收端要对每一笔数据的包序列号进行检查,重复出现的数据会被丢掉,用来保证数据不会被重复接收。

三是对于每一包数据,发送端在尾巴上添加了CRC24校验值,接收端重新计算校验值并对比,剔除RF传输出错的数据。

Telink BLE SDK通过了Sig官方BQB认证,完全按照以上设计实现。

LinkLayer这些设计机制,可以杜绝RF传输导致的数据错误。

(2) OTA PDU CRC16校验

参考本文前面介绍,在LinkLayer数据保护的基础上,OTA协议上再增加一个CRC16校验,使得数据传输更加安全。

(3) OTA PDU序列号检查

OTA master将Firmware拆成若干个OTA PDU,每个PDU都有自己的包序列号。

为了便于说明,假设Firmware size为50K,按照OTA PDU 16Byte进行拆分,PDU数量为50*1024/16=3200,那么序列号为0 ~ 3199,即0x0 ~ 0xC7F。

OTA开始后,设置预期序列号为0。每收到一笔OTA数据,使用预期序列号和实际序列号对比,只有二者相等才认为流程正确,同时更新预期序列号+1;如果二者不等,认为失败,结束OTA。这样的设计可以保证OTA PDU的连续性和唯一性。

在OTA结束时,可以在OTA_END包上可以读到Firmware最后一个OTA PDU的序列号0xC7F,用这个序列号跟实际接收的最大序列号进行对比,可以确定OTA PDU是否存在尾巴丢失情况。如果实际接收的最大序列号为0xC7E,则说明master漏传了最后一个包,此时OTA会失败。

以上设计结合在一起,可以确保OTA master端对Firmware的正确拆分,并且每一个OTA PDU都有效发出。

Firmware CRC32校验

存在一种OTA master端主观上操作错误的情况,可能导致BLE产品的宕机。用Telink编译工具生成一个正确的binary文件后,可能因为操作失误不小心修改了这个binary文件,比如某个byte的内容被篡改。这个错误的binary文件拿到OTA master上进行OTA升级时,master无法知道这个错误,会当做正确的Firmware给slave升级,从而造成slave端的程序无法正确运行。

为了解决以上问题,SDK中增加了Firmware CRC32校验。在编译生成binary文件的最后一步,对binary文件进行CRC32计算,并将结算结果拼接上去。OTA升级过程中,server端边收数据边进行CRC32计算,OTA_END环节使用计算值和最后一个OTA PDU上的值进行比较,只有二者相等才认为Firmware没有被篡改。

OTA异常断电保护

Telink OTA设计可以确保在任意时刻对设备断电,都不会存在设备宕机的风险。

参考本章前面介绍,MCU使用了多地址启动的机制,并且使用了一个byte的标记位供MCU上电时判断Firmware从Flash哪个地址启动。为了便于说明,假设设备当前Firmware存储在Flash 0x0 ~ 0x20000这个区间,其中地址0x0008上的值标记这个Firmware有效,此时为0x4B;新的Firmware将被存储在0x20000 ~ 0x40000这个区间,其中0x20008这个地址上的值用于标记新的Firmware是否有效,初始值为0xFF。

OTA升级开始后,接收到第一笔OTA PDU的0x08地址上的值为0x4B,但是这笔PDU写入Flash时会故意将0x20008写成0xFF,不生效。 等到OTA流程全部正确且所有Flash写入都正确后,将地址0x20008的值写为0x4B。在这之前任何时刻发生断电,都没有影响0x0 ~ 0x20000的Firmware,即便断电导致0x20000 ~ 0x40000上某些Flash写入失败,重新上电后0x0 ~ 0x20000的Firmware可以运行。

最后一步,在确认0x20000 ~ 0x40000上的Firmware都处理正确后,将0x0 ~ 0x20000中的地址0x0008对应的0x4B写为0x00,表示这个区域上Firmware失效。我们可以将地址0x0008写成0x00看做一个原子操作,不管在哪一刻断电,这个操作要么成功(值被改为0x00或被误写为其他非0x4B的值),要么失败(即0x4B保持不变)。如果操作成功,下一次重新上电将运行0x20000 ~ 0x40000上的Firmware;如果失败,则还是运行0x0 ~ 0x20000上的Firmware。

Flash

Flash地址分配

FLASH存储信息以一个sector的大小(4K byte)为基本的单位,因为flash的擦除是以sector为单位的(擦除函数为flash_erase_sector),理论上同一种类的信息需要存储在一个sector里面,不同种类的信息需要在不同的sector(防止擦除信息时将其他类的信息误擦除)。所以建议user在使用FLASH存储定制信息时遵循“不同类信息放在不同sector”的原则。系统相关信息 (Customer Value,MAC address,Pair&Sec Info) 默认位置会自适应的根据flash真实大小,偏移到flash的较为偏后的位置。

如下图是512K/1M Flash中各种信息的地址分配,以默认OTA Firmware最大size不超过128K为例来说明,如果用户修改了OTA Firmware size,则相应地址发生变化,用户可以自行分析。

"512K/1M FLASH地址分配"

上图所示,其中所有的地址分配都给user提供了对应的修改接口,user可以根据自己需要去规划地址分配。下面介绍默认的地址分配方法以及对应的修改地址的接口。API blc_flash_read_mid_get_vendor_set_capacity()用于获取Flash的mid值、vendor值和size,API blc_readFlashSize_autoConfigCustomFlashSector()根据不同的Flash size自动分配各种信息的存储地址。

void blc_readFlashSize_autoConfigCustomFlashSector(void)
{
    blc_flash_read_mid_get_vendor_set_capacity();

#if (FLASH_ZB25WD40B_SUPPORT_EN || FLASH_GD25LD40C_SUPPORT_EN || FLASH_GD25LD40E_SUPPORT_EN)    //512K
    if(blc_flash_capacity == FLASH_SIZE_512K){
        flash_sector_mac_address = CFG_ADR_MAC_512K_FLASH;
        flash_sector_calibration = CFG_ADR_CALIBRATION_512K_FLASH;
        flash_sector_smp_storage = FLASH_ADR_SMP_PAIRING_512K_FLASH;
        flash_sector_master_pairing = FLASH_ADR_MASTER_PAIRING_512K;
        tlkapi_printf(APP_FLASH_INIT_LOG_EN, "[FLASH][INI] 512K Flash, MAC on 0x%x\n", flash_sector_mac_address);
    }
#endif
#if (FLASH_ZB25WD80B_SUPPORT_EN || FLASH_GD25LD80C_SUPPORT_EN || FLASH_GD25LD80E_SUPPORT_EN)        //1M
    else if(blc_flash_capacity == FLASH_SIZE_1M){
        flash_sector_mac_address = CFG_ADR_MAC_1M_FLASH;
        flash_sector_calibration = CFG_ADR_CALIBRATION_1M_FLASH;
        flash_sector_smp_storage = FLASH_ADR_SMP_PAIRING_1M_FLASH;
        flash_sector_master_pairing = FLASH_ADR_MASTER_PAIRING_1M;
        tlkapi_printf(APP_FLASH_INIT_LOG_EN, "[FLASH][INI] 1M Flash, MAC on 0x%x\n", flash_sector_mac_address);
    }
#endif
    else{
        /*This SDK do not support other flash size except what listed above
          If code stop here, please check your Flash */
        tlkapi_printf(APP_FLASH_INIT_LOG_EN, "[FLASH][INI] flash size %x do not support !!!\n", blc_flash_capacity);
        while(1);
    }
}

(1) 当使用512K Flash时0x76000~0x76FFF这个sector存储MAC地址,当使用1M Flash时为0xFF000~0x100000,实际上MAC address 6个bytes存储在0x76000~0x76005(0xFF000~0xFF005),当使用512K时,高byte的地址存放在0x76005,低byte地址存放在0x76000。比如FLASH 0x76000到0x76005的内容依次为0x11 0x22 0x33 0x44 0x55 0x66,那么MAC address为0x665544332211。泰凌的量产治具系统会将实际产品的MAC 地址烧写到0x76000(0xFF000)地址,和SDK相对应。如果user需要修改这个地址,请确保治具系统烧写的地址也做了相应的修改。SDK中在user_init_normal函数里会从FLASH的CFG_ADR_MAC读取MAC 地址,这个宏在vendor/common/ble_flash.h里面修改即可。

/**************************** 512 K Flash *****************************/
#ifndef     CFG_ADR_MAC_512K_FLASH
#define     CFG_ADR_MAC_512K_FLASH                  0x76000
#endif
/**************************** 1 M Flash *******************************/
#ifndef     CFG_ADR_MAC_1M_FLASH
#define     CFG_ADR_MAC_1M_FLASH                    0xFF000
#endif

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

校准区信息的细节,用户可参考《Telink_IC_Flash Customize Address_Spec》。以相对校准区起始地址的偏移地址对校准信息进行说明。比如偏移地址0x00指的是0x77000或0xFE000。

a) 偏移地址0x00,1 byte,存储BLE RF频偏校准值。

b) 偏移地址0x40,4 byte,存储TP值校准。B85m IC不需要TP校准,这里忽略。

c) 偏移地址0xC0,存储ADC Vref校准值。

d) 偏移地址0x180,16 byte,存储Firmware数字签名,用于防止客户Firmware被盗用。

e) 偏移地址0x1C0,2 byte,存储Flash VDDF校准值。

f) 其他,保留。

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

void    blm_smp_configPairingSecurityInfoStorageAddr (int addr);

(4) 使用0x00000 ~ 0x3FFFF 256K空间默认作为程序空间;

0x00000 ~ 0x1FFFF共128K为Firmware存储空间;0x20000 ~ 0x3FFFF 128K为OTA更新时存储新Firmware的空间,即支持最大Firmware空间为128K。

若默认的128K程序空间对user来说太大,而user希望在0x00000~0x3FFFF区域里腾出一些空间来作为数据存储空间,协议栈也提供了相应的API,修改方法见后面OTA章节的详细说明。

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

Flash操作

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

(1) Flash读写操作

flash读写操作使用flash_read_page和flash_write_page函数来实现。

_attribute_data_retention_ flash_handler_t flash_read_page = flash_read_data;
_attribute_data_retention_ flash_handler_t flash_write_page = flash_page_program;
void flash_read_data(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); //读flash 0x11000开始的6个byte到data数组。

flash_write_page函数对flash进行写操作:

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

flash_write_page函数是对page的操作,flash里面一个page为256 byte,这个函数可以进行跨page的操作。

flash_read_page也可以一次性读取超过256 byte的数据。

注意:

  • user在使用flash_write_page函数时,最多只能一次写16个byte,多了会导致BLE中断异常

  • 该限制的原理,请结合“Flash API对BLE timing的影响”部分的介绍。

(2) flash擦除操作

使用flash_erase_sector函数来擦除flash。

void flash_erase_sector(unsigned long addr);

一个sector为4096 bytes,如0x13000 ~ 0x13fff是一个完整的sector。

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

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

(3) flash读写和擦除操作对系统中断的影响

上面介绍的三个flash操作函数flash_read_page、flash_write_page、flash_erase_sector在执行时,都必须先将系统中断关掉irq_disable(),操作结束后再恢复中断irq_restore(),其目的是为了保证flash MSPI时序操作的完整性和连续性,同时防止中断里又有flash操作调用MSPI总线而造成硬件资源的重入。

这个BLE SDK RF收发包的时序全部由中断来控制的,flash操作关闭系统中断造成的后果是BLE收发包的时序会被破坏,得不到及时响应。

其中flash_read_page函数的执行时间不是太长,对BLE的中断影响很小,当使用flash_write_page时建议在BLE连接状态时最长一次写16Bytes,如果过长的话可能会对BLE时序造成影响。所以强烈建议用户在main_loop里BLE连接状态时,不要连续读写太长的地址。

flash_erase_sector函数的执行时间为几十到几百个ms,所以在主程序的main_loop里,一旦进入BLE连接状态,不允许去调用flash_erase_sector函数,否则会破坏BLE收发包的时间点,造成连接断开。如果是无法避免在BLE连接的时候擦除flash,请根据本文档后面介绍的Conn state Slave role时序保护实现方法来操作。

(4) 读flash可以使用指针访问来实现

BLE SDK的firmware存储在flash上,程序运行时,只是将flash前一部分的代码作为常驻内存代码放在RAM上执行,剩余的绝大部分代码根据程序的局部性原理,在需要的时候从flash读到RAM高速缓存cache区域(简称cache)。MCU通过自动控制内部MSPI硬件模块,读取flash上的内容。

可以使用指针的形式读取flash上的内容,指针形式读flash的原理是MCU系统总线访问数据时,当发现数据地址不在常驻内存ramcode上,系统总线就会自动切换到MSPI,通过MSCN、MCLK、MSDI和MSDO四根线去操作spi的时序来获得读取flash数据。

以下列出3种示例:

u16  x = *(volatile u16*)0x10000; //读flash 0x10000两个byte 
u8 data[16];
memcpy(data, 0x20000, 16);     //读flash 0x20000 16个byte copy到data
if(!memcmp(data, 0x30000, 16)){ //读flash 0x30000 16个byte和data比较
//……
}

user_init里面读取flash上的校准值并设定到对应的register时,都是采用指针访问flash的方式实现的,请参考SDK里的函数。

void blc_app_loadCustomizedParameters_normal(void);
_attribute_ram_code_ void blc_app_loadCustomizedParameters_deepRetn(void);

读flash可以使用指针形式,但是不能使用指针写flash(写flash只能通过flash_write_page来实现)。

需要注意指针读flash存在一个容易出问题的地方:只要是通过MCU系统总线读到的数据,MCU都会将这些数据缓存在cache里,如果cache里这个数据没有被其他内容覆盖时,又有新的访问该数据的请求发生,此时MCU会直接用cache里缓存的内容作为结果。如果user的代码出现下面这种情况:

u8  result;
result = *(volatile u16*)0x40000;    //指针读取flash
u8 data = 0x5A;
flash_write_page(0x40000, 1, &data);
result = *(volatile u16*)0x40000;    //指针读取flash
if(result != 0x5A){  ..... }

flash 地址0x40000处本来是0xff,第1次读到的result 为 0xff,然后写入0x5A,理论上第2次读到的值是0x5A,但实际程序给出的结果还是0xff,是从cache里拿到的第一次缓存的结果。

注意:

  • 如果出现这种多次读同一个地址且这个地址的值会被改写时,千万不要用指针的形式,使用API flash_read_page来实现最安全,这个函数读到的结果不会从cache里拿之前缓存的值。

改成如下实现才正确:

u8  result;
flash_read_page(0x40000, 1, &result);  //API 读取flash
u8 data = 0x5A;
flash_write_page(0x40000, 1, &data);

位置会自适应的根据flash真实大小,偏移到flash的较为偏后的位置。

Flash操作的保护

由于write flash和erase flash的过程中,需要将地址和数据通过SPI总线传递给Flash,SPI总线上的电平稳定性非常重要,这些关键的数据一旦错误就会造成不可逆的后果,比如将firmware写错或误擦都会造成firmware无法再运行,OTA功能也会失效。

在Telink芯片多年的量产经验中,出现过Flash在不稳定条件下操作导致的错误情况。不稳定的条件主要包括电源电压偏低、电源纹波过大、系统上其他模块间歇性的耗电导致电源抖动等等。为了避免后续的产品出现类似的操作风险,这里我们介绍一些相关的Flash操作保护的方法,客户认真阅读后需要尽量考虑这些问题,增加更多的安全保护机制,才能确保产品的稳定性。

低电压检测保护

结合低电保护章节的介绍,需要考虑在所有的Flash write和erase操作之前都做电压检测,避免出现过低电压下操作Flash的情况。另外为了保证系统一直在一个安全的电压下工作,在main_loop中也建议定时做低压检测,以保证系统的正常运行。

注意:

  • 关于flash低压保护,以下多处出现2.0V、2.2V等阈值,强调下这些数值只是示例、参考值。客户要根据实际情况评估修改这些阈值,比如单层板、电源波动大等因素,都要酌情提高安全阈值。

以SDK demo中的低压检测为例:

Step 1 首先在上电或从deepsleep唤醒时,在调用Flash函数之前,都要进行一次低压检测,以防止低电压造成的flash问题:

_attribute_no_inline_ void user_init_normal(void)
{
    ......
    #if (APP_BATT_CHECK_ENABLE)
        user_battery_power_check(VBAT_DEEP_THRES_MV);
    #endif
    ......
}

Step 2 在main_loop中,每隔一定500ms需要进行一次低压检测:

#if (APP_BATT_CHECK_ENABLE)
    if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
        lowBattDet_tick = clock_time();
        user_battery_power_check(VBAT_DEEP_THRES_MV);
    }
#endif

考虑MCU的工作电压及flash的工作电压情况,因此Demo设置在1.8V以下芯片将直接进入suspend,1.8~2.0V以下芯片直接进入deepsleep,并且一旦芯片检测到低于2.0V,需要等到电压升至2.2V,芯片才会恢复正常运行状况。这么设计考虑以下几点:

  • 1.8V时电压已有低于flash运行电压的风险,再进入deepsleep后唤醒时有可能造成flash异常而出现死机,因此在1.8V以下进入suspend,保证芯片安全;

  • 2.0V时当有其他模块被操作可能拉低电压造成flash无法正常工作,因此在2.0V以下需要进入deepsleep,保证芯片不再运行相关模块;

  • 当有低电压情况时,需恢复至2.2V才能使其他功能正常,这是为了保证电源电压确认在被充电且已有一定电量,此时开始恢复功能较为安全。

以上是SDK Demo中的定时检测电压及管理方式,用户可以参考来进行设计。

注意:

  • 关于flash低压保护,以上出现的阈值,只是参考值。客户要根据实际情况评估修改阈值,比如单层板、电源波动大等因素,都要酌情提高安全阈值。

Flash lock保护

除了上述的定时电压检测与管理方案,强烈建议客户做Flash的擦写保护。这是由于在某些情况下,即便做了低压检测结果为安全,但在检测之后应用层各个模块运行时也有小概率风险造成Flash电源电压的拉低,而导致Flash真正操作时其电源电压不满足条件而造成Flash的内容被篡改。因此建议客户在程序启动后便进行Flash的擦写保护,这样即使有误操作产生,Flash的内容也会更有保障。

一般建议客户只对程序部分写保护(Flash前部分),这样其余的Flash地址仍可以用于用户层的数据存储。这里以SDK Sample工程为例讲述如何计算要保护的大小与保护的方法。

(1) 初始化写保护

调用API app_flash_protection_operation()进行Flash保护的初始化,并注册Flash保护的回调。

#if (APP_FLASH_PROTECTION_ENABLE)
    app_flash_protection_operation(FLASH_OP_EVT_APP_INITIALIZATION, 0, 0);
    blc_appRegisterStackFlashOperationCallback(app_flash_protection_operation); //register flash operation callback for stack
#endif

在app_flash_protection_operation()中,flash_op_evt为FLASH_OP_EVT_APP_INITIALIZATION时进行Flash保护的初始化操作。首先调用flash_protection_init()获取blc_flash_mid来判断flash类型,并根据结果调用相关函数。其中blc_flash_mid在初始化中已通过API blc_readFlashSize_autoConfigCustomFlashSector调用 blc_flash_read_mid_get_vendor_set_capacity 获取,在这里再进行判断,以防获取失败或者被用户误删。

void flash_protection_init(void)
{
    if(!blc_flash_mid){
        blc_flash_mid = flash_read_mid();
    }

    /* According to the flash mid, execute the corresponding lock flash API. */
    switch(blc_flash_mid)
    {
        #if (FLASH_ZB25WD40B_SUPPORT_EN)
            case MID13325E:
                flash_lock_mid = (flash_lock_t)flash_lock_mid13325e;
                flash_unlock_mid = flash_unlock_mid13325e;
                flash_get_lock_status_mid = (flash_get_lock_status_t)flash_get_lock_block_mid13325e;
                flash_unlock_status = FLASH_LOCK_NONE_MID13325E;
                break;
        #endif

        #if (FLASH_ZB25WD80B_SUPPORT_EN)
            case MID14325E:
                flash_lock_mid = (flash_lock_t)flash_lock_mid14325e;
                flash_unlock_mid = flash_unlock_mid14325e;
                flash_get_lock_status_mid = (flash_get_lock_status_t)flash_get_lock_block_mid14325e;
                flash_unlock_status = FLASH_LOCK_NONE_MID14325E;
                break;
        #endif

        ``````
        default:
            break;
    }
}
当前SDK所支持的Flash型号用户可以在 drivers/8258/driver_ext/mcu_config.h 中找到,8278同理。
/*
    Flash Type  uid CMD         MID     Company
    ZB25WD40B   0x4b        0x13325E    ZB
    ZB25WD80B   0x4b        0x14325E    ZB
    GD25LD40C   0x4b        0x1360C8    GD
    GD25LD40E   0x4b        0x1360C8    GD
    GD25LD80C   0x4b(AN)    0x1460C8    GD
    GD25LD80E   0x4b(AN)    0x1460C8    GD
 */
#define FLASH_ZB25WD40B_SUPPORT_EN                  1
#define FLASH_ZB25WD80B_SUPPORT_EN                  1
#define FLASH_GD25LD40C_SUPPORT_EN                  1
#define FLASH_GD25LD40E_SUPPORT_EN                  1
#define FLASH_GD25LD80C_SUPPORT_EN                  1
#define FLASH_GD25LD80E_SUPPORT_EN                  1

如果支持OTA功能,根据OTA的地址决定要保护的大小,例如,OTA的地址为0x20000,即128K,那么Flash保护的区域应为256K,要对旧的firmware和要OTA的新的firmware区域都进行保护。如果不支持OTA,那么默认的Flash保护区域大小为256K。但需要注意的是,此大小用户可以根据自己的需求进行修改。

if(flash_op_evt == FLASH_OP_EVT_APP_INITIALIZATION)
{
    flash_protection_init();
    u32  app_lockBlock = 0;
    #if (BLE_OTA_SERVER_ENABLE)
        u32 multiBootAddress = blc_ota_getCurrentUsedMultipleBootAddress();
        if(multiBootAddress == MULTI_BOOT_ADDR_0x20000){
            app_lockBlock = FLASH_LOCK_FW_LOW_256K;
        }
        else if(multiBootAddress == MULTI_BOOT_ADDR_0x40000){
            app_lockBlock = FLASH_LOCK_FW_LOW_512K;
        }
        #if(MCU_CORE_TYPE == MCU_CORE_827x)
        else if(multiBootAddress == MULTI_BOOT_ADDR_0x80000){ //for flash capacity smaller than 1M, OTA can not use 512K as multiple boot address
            if(blc_flash_capacity < FLASH_SIZE_1M){
                blc_flashProt.init_err = 1;
            }
            else{
                app_lockBlock = FLASH_LOCK_FW_LOW_1M;
            }
        }
        #endif
    #else
        app_lockBlock = FLASH_LOCK_FW_LOW_256K; //just demo value, user can change this value according to application
    #endif
    flash_lockBlock_cmd = flash_change_app_lock_block_to_flash_lock_block(app_lockBlock);

    if(blc_flashProt.init_err){
        tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash protection initialization error!!!\n");
    }
    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] initialization, lock flash\n");
    flash_lock(flash_lockBlock_cmd);
}
但是需要注意的是,所设计的保护区域大小不一定与最终所能保护的区域大小一致,这取决于当前Flash所能提供的Flash保护的接口和其他因素。例如,flash_mid为MID13325E的Flash,其size为512K,如果选择保护低512K,那么程序中所保护的大小实际为448K,这是因为要留64K给system data和user data。再比如flash_mid为MID14325E的Flash,其size为1M,但是当用户选择保护低256K和512K时,实际上最少能选择保护768K的size。

    switch(blc_flash_mid)
    {
        #if (FLASH_ZB25WD40B_SUPPORT_EN) //512K capacity
            case MID13325E:
                if(app_lock_block == FLASH_LOCK_FW_LOW_256K){
                    flash_lock_block_size = FLASH_LOCK_LOW_256K_MID13325E;
                    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 256K block!\n");
                }
                else if(app_lock_block == FLASH_LOCK_FW_LOW_512K){
                    /* attention 1: use can change this value according to application
                     * attention 2: can not lock stack SMP data storage area
                     * attention 3: firmware size under protection is not 512K, user should calculate
                     * demo code: choose 448K, leave at 64K for system data(SMP storage data & calibration data & MAC address) and user data,
                     *            now firmware size under protection is 448K - 256K = 192K
                     * if this demo can not meet your requirement, you should change !!!*/
                    flash_lock_block_size = FLASH_LOCK_LOW_448K_MID13325E;
                    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 448K block!\n");
                }
                else{
                    blc_flashProt.init_err = 1; //can not use LOCK LOW 1M for 512K capacity flash
                }
                break;
        #endif

        #if (FLASH_ZB25WD80B_SUPPORT_EN) //1M capacity
            case MID14325E:
                if(app_lock_block == FLASH_LOCK_FW_LOW_256K || app_lock_block == FLASH_LOCK_FW_LOW_512K){
                    /* attention that :This flash type, minimum lock size is 768K, do not support 256K or other value
                     * demo code will lock 768K when user set OTA 128K or 256K as multiple boot address,
                     * system data(SMP storage data & calibration data & MAC address) is OK;
                     * user data must be stored in flash address bigger than 768K !!!
                     * if this demo code lock area do not meet your requirement, you can change it !!!*/
                    flash_lock_block_size = FLASH_LOCK_LOW_768K_MID14325E;
                    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 768K block!\n");
                }
                else if(app_lock_block == FLASH_LOCK_FW_LOW_1M){
                    /* attention 1: use can change this value according to application
                     * attention 2: can not lock stack SMP data storage area
                     * attention 3: firmware size under protection is not 1M, user should calculate
                     * demo code: choose 960K, leave 64K for system data(SMP storage data & calibration data & MAC address) and user data,
                     *            now firmware size under protection is 960K - 512K = 448K
                     * if this demo can not meet your requirement, you should change !!! */
                    flash_lock_block_size = FLASH_LOCK_LOW_960K_MID14325E;
                    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 960K block!\n");
                }
                break;
        #endif

注意:

  • 关于flash保护,以上的处理只是提供参考,用户需要根据实际情况评估此demo是否满足需求;如果不能满足,用户可根据demo自行修改。
  • 如果用户使用当前SDK中所不支持的Flash型号,请参照当前的写保护方法自行添加。

(2) OTA过程中的保护操作

在OTA中,由于需要对flash进行擦写操作,因此如果上电有写保护的操作,在OTA过程中需要将其解锁保护,并在OTA结束后再加锁保护。由于OTA结束后,无论成功或者失败,都会重新运行程序,因此在程序开始,由上一节将的flash_lock方法再次将程序写保护,形成闭环保证应用的安全性。

实现解锁保护的API为flash_unlock(),在此过程中,会对区域进行三次解锁操作以防解锁失败。

void flash_unlock(void)
{
    if(blc_flashProt.init_err){
        return;
    }
    u16 cur_lock_status = flash_get_lock_status_mid();
    if(cur_lock_status != flash_unlock_status){ //not in lock status
        for(int i = 0; i < 3; i++){ //Unlock flash up to 3 times to prevent failure.
            flash_unlock_mid();
            cur_lock_status = flash_get_lock_status_mid();

            if(cur_lock_status == flash_unlock_status){ //unlock success
                break;
            }
        }
    }
}

加锁保护的API为flash_lock(),在此操作中,首先判断当前加锁的区域是否是用户所需要的区域,如果不是,需要对当前加锁的区域进行解锁,再对所需要的区域进行加锁。为了避免失败,加锁解锁操作都重复三次。

void flash_lock(unsigned int flash_lock_cmd)
{
    if(blc_flashProt.init_err){
        return;
    }
    u16 cur_lock_status = flash_get_lock_status_mid();
    if(cur_lock_status == flash_lock_cmd){ //lock status is what we want, no need lock again
    }
    else{ //unlocked or locked block size is not what we want
        if(cur_lock_status != flash_unlock_status){ //locked block size is not what we want, need unlock first
            for(int i = 0; i < 3; i++){ //Unlock flash up to 3 times to prevent failure.
                flash_unlock_mid();
                cur_lock_status = flash_get_lock_status_mid();
                if(cur_lock_status == flash_unlock_status){ //unlock success
                    break;
                }
            }
        }
        for(int i = 0; i < 3; i++) //Lock flash up to 3 times to prevent failure.
        {
            flash_lock_mid(flash_lock_cmd);
            cur_lock_status = flash_get_lock_status_mid();
            if(cur_lock_status == flash_lock_cmd){  //lock OK
                break;
            }
        }
    }
}

在app_flash_protection_operation()中对不同的OTA阶段进行了不同的处理。

a)OTA开始擦除旧firmware,对flash进行解锁。

else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_BEGIN)
{
    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA clear old FW begin, unlock flash\n");
    flash_unlock();
}

b)OTA擦除旧firmware结束,对flash加锁。

else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_END)
{
    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA clear old FW end, restore flash locking\n");
    flash_lock(flash_lockBlock_cmd);
}

c)OTA开始写入新的firmware,对flash解锁。

else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_BEGIN)
{
    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA write new FW begin, unlock flash\n");
    flash_unlock();
}

d)OTA写入新的firmware结束,对flash加锁。

else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_END)
{
    tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA write new FW end, restore flash locking\n");
    flash_lock(flash_lockBlock_cmd);
}

当前在协议栈底层已添加了这四个OTA事件的回调。需要注意的是,用户如果有需要也可以再添加更多flash保护的操作,并且如果有自己的OTA处理流程,那么需要根据SDK所提供的demo,设计符合自己OTA逻辑的flash保护流程。

内置Flash介绍

Flash访问时序对BLE时序的影响

(1) Flash访问时序

1) Flash操作基本时序

"Flash基本操作时序"

上图所示为一个典型的MCU访问Flash时序,MSCN拉低期间,在MCLK控制下,通过MSDI和MSDO的电平状态变化,完成与Flash的数据交互。

Flash访问时序是Flash操作基本时序,MSCN拉低期间为数据交互,拉高之后结束。所有的Flash功能都是以它为基础,复杂的Flash功能可以拆分成若干个Flash操作基本时序。

每一个Flash操作基本时序都是相对独立的,必须等一个操作时序完成之后,才能进行下一轮的操作。

2) MCU硬件访问Flash

Firmware存储在Flash中,MCU执行程序需要提前从Flash中读取指令和数据, 结合2.1.2.1的介绍可知需要读取的内容为text段和“read only data”段。MCU运行过程中实时读取Flash上的指令,所以会不停的启动Flash操作基本时序,这个过程是由MCU硬件自动控制的,软件不参与。

main_loop程序运行过程中,如果突然发生中断,进入irq_handler。即使main_loop和irq_handler中的程序都在text段中,也不会出现Flash时序冲突,因为都是由MCU硬件来完成的,它会做好相关的仲裁和管控工作。

3) 软件访问Flash

MCU硬件访问Flash只解决读程序指令和“read only data”问题。如果需要对Flash手动进行读写擦等操作,使用flash driver中的flash_read_page、flash_write_page、flash_erase_sector等API。查看这几个API的具体实现,可以看出是由软件来管控Flash操作基本时序,先拉低MSCN,然后读写数据,最后拉高MSCN。

4) Flash访问时序冲突以及解决方法

由于Flash操作基本时序是一个不可分割和破坏的过程,当软件和MCU硬件同时访问Flash时,由于软件和MCU硬件不具备协调和仲裁机制,有可能出现时序冲突。

出现这个时序冲突的场景为:软件在main_loop中调用flash_read_page、flash_write_page、flash_erase_sector等API,MSCN拉低后正在进行数据读写时,发生了中断,irq_handler中有一些指令存储在text段中,MCU硬件也启动一个新的Flash操作基本时序,这个时序就和前面main_loop中的时序冲突,造成MCU死机等错误。

如下图所示,Software访问Flash结束时,发生中断并响应,MCU硬件开始访问Flash。此时Flash访问的结果必然会出错。

"中断导致的Flash时序冲突"

分析时序冲突的几个必须同时满足的条件,可以得出解决该冲突的方法包括以下几种:

a) main_loop中不要出现任何软件操作Flash时序的API。这个方法不可行,SDK和应用上都会出现flash_write_page等API的使用。

b) irq_handler函数中所有的程序都提前存储到ramcode,不依赖任何text段和”read_only_data”段。这个方法也不太好。受限于825x/827x芯片的SRAM size,如果所有的中断的code都存储到ramcode,SRAM资源不够用。另外对于user做这种限制不容易管控,无法保证user中断code写的那么严密。

c) 软件操作Flash时序的几个API中,加保护,关闭中断,不让irq_handler响应,Flash访问结束后,重新恢复中断。

目前Telink BLE SDK采用了方法3,Flash API中关中断保护。如下code所示(中间省略了若干code),使用irq_disable关中断,irq_restore恢复中断。

void flash_mspi_write_ram(unsigned char cmd, unsigned long addr, unsigned char addr_en, unsigned char *data, unsigned long data_len)
{
    unsigned char r = irq_disable();  

    …… //flash access

    irq_restore(r);
}

下图是关中断保护Flash访问时序的原理示意图。软件访问Flash时将中断关闭,中间发生了中断但是不能立刻响应(中断等待),等到软件访问Flash时序全部正确结束后,恢复开启中断,此时中断立刻响应,再由MCU硬件访问Flash。

"正确的的中断处理与flash操作"

(2) Flash API对BLE timing的影响

前面介绍了使用Flash API关中断保护来解决软件和硬件MCU访问Flash时序冲突的问题。由于关中断会使得所有的中断无法实时响应,排队等待中断恢复后延时执行,需要考虑被延时时间可能带来的副作用。

1) 关中断对BLE timing的影响

结合BLE timing的特点来介绍。这个SDK中BLE连接态的BTX、BRX状态机都是由中断任务来完成的。BTX和BRX是类似的实现,以slave role的BRX为例来说明。

BRX timing的处理比较复杂,以BLE slave BRX出现more data时RX IRQ的处理为例,如下图所示。SDK设计中要求软件对每一个RX IRQ都要响应,可以延迟响应,但不能丢掉。如果某个RX IRQ被丢掉,触发这个RX IRQ的RX packet也会丢掉,造成Linklayer丢包的错误。

"Flash操作对Link Layer风险"

图中RX1在t1触发RX IRQ 1,RX2在t2触发RX IRQ 2。如果没有关中断发生,中断会在t1和t2实时响应,软件正确处理RX packet。

t1和t2时间差为T_rx_irq,关中断持续时间为T_irq_dis,T_irq_dis > T_rx_irq。

三种情况IRQ disable case 1、IRQ disable case 和IRQ disable case 3的关中断持续时间都是T_irq_dis,但关中断的起点和t1的相对时间不一样。

IRQ disable case 1,t3关中断,t4恢复中断。t3 < t1;t4 > t2。RX IRQ 1在t1无法响应,中断排队等待。RX IRQ 2在t2触发,覆盖RX IRQ1(因为中断等待队列里只能有一个RX IRQ),RX IRQ 2排队等待,在t4被正确执行。 RX IRQ1对应的RX1丢失,如果RX1是一个有效的数据包,Linklayer就会出错。

IRQ disable case 2和IRQ disable case 3,RX IRQ 1和RX IRQ 2虽然被延迟执行,但没有丢掉,不会发生错误。

由以上的例子分析可以得到一个重要结论:

当中断关闭持续时间大于某个安全阈值,可能会发生Linklayer错误的风险。

这个安全阈值,跟SDK中Linklayer时序设计、BLE Spec时序特点都相关,比例子中的T_rx_irq复杂得多。具体细节不详细介绍,这里直接给出的安全阈值是220us。

同样是关中断持续时间T_irq_dis,上面例子中IRQ disable case 2和IRQ disable case 3,由于关中断发生的时间点不一样,RX IRQ 1或RX IRQ2被延时响应,不会发生RX IRQ 2覆盖RX IRQ1导致的丢包。即便是IRQ disable case 1,如果RX1和RX2是无关紧要的空包,发生丢包也不会造成任何错误。

中断关闭持续时间大于220us时,不是一定会出现错误,必须多个条件同时满足,才有可能触发错误,这些条件包括:关中断的时间较长、关中断的时间点和RX IRQ发生的时间点符合某种特定的关系、BTX或BRX中出现more data;连续触发RX IRQ的两个RX packet都是有效数据包而不是空包等等。所以最终结论是:

中断关闭持续时间大于220us时,会出现Linklayer出错的风险,但概率非常低。

BLE SDK Linklayer的设计,以零风险为目标,即中断关闭持续时间永远要小于220us的情况,不给任何出错的机会。

这里额外介绍上面例子RX packet丢失的问题。在Telink BLE SDK使用生产中,经常遇到客户反馈碰到这个问题:在加密开启的前提下,看到device发送一个reason为0x3D(MIC_FAILURE)的terminate包,导致断连。

以上分析可知,中断关闭时间过长会导致RX IRQ被延迟太长时间进而被覆盖,最终丢包。但SDK会正确处理好中断关闭时间的问题,文档后面会详细介绍。更有可能的原因是user用到了其他的中断(比如Uart、USB等),这些中断响应时的软件执行时间如果过长,跟中断关闭的效果是一样的,也会让RX IRQ延迟。这里我们限定一个user中断执行的最大安全时间为100us。

2) Flash API关中断保护对BLE timing的影响

为了规避软件访问Flash和MCU硬件访问Flash的时序冲突,Flash API使用了关中断的方法。当中断关闭持续时间大于220us时,Linklayer可能发生出错的风险。为了解决这二者的矛盾,需要关注Flash API关中断的最大时间。

受影响的BLE timing是connection state slave role和master role。系统初始化和mainloop中的Advertising state不受影响。在mainloop connection state中,主要关注以下三个Flash API:flash_read_page、flash_write_page、flash_erase_sector。其他Flash API一般不使用或者在初始化的时候才会用到。

a) flash_read_page

经测试验证, flash_read_page一次性读取的byte数量不超过64时,时间非常安全,在220us以内。超过这个值后会有一定的风险。

强烈建议user使用flash_read_page读Flash时最多读64 byte,如果超过64 byte,需要拆成多次调用flash_read_page来实现。

b) flash_erase_sector

flash_erase_sector的时间一般在10ms ~ 100ms这个量级,远远超过220us。所以这个SDK 要求user在BLE connection state不要调用flash_erase_sector。直接调用这个API,connection一定会出错。

我们建议user使用其他方式来取代flash_erase_sector的设计。比如一些应用是为了反复更新在Flash上存储的一些关键信息,设计上可以考虑选取一块较大的区域,使用flash_write_page不断往后延伸的方法。

BLE slave应用,对于无法避免的flash_erase_sector,如果只是偶尔会发生,可以使用Conn state Slave role时序保护机制来规避,请参考本文档的详细介绍。

注意,由于时序保护机制非常复杂,对于高频率的flash_erase_sector,不建议这么用,无法保证在BLE slave连接态时反复连接调用这套机制的稳定性。建议user尽量从设计上去避开这种情况。

c) flash_write_page

flash_write_page时间受到多个关键因素的影响,包括:Flash种类、Flash工艺、write byte number、高低温等。下面从内置Flash的几个种类来详细说明。

内置Flash API的使用

根据上一节的介绍,Flash API中flash_write_page跟内置Flash的种类相关的。本节结合825x、827x已经支持的几种内置Flash详细介绍。

注意:

  • PUYA Flash暂时不会在825x和827x上量产应用,但保留相关兼容原理的介绍,用于加深用户对BLE timing的理解。

(1) GD Flash

GD Flash属于ETOX工艺,是825x、827x最早支持的内置Flash。

flash_write_page的消耗时间跟参数len(即一次性写入的字节数量)相关,接近于一个正比的关系。经过Telink内部的详细测试和分析,发现字节数量小于等于16时,写入时间能够稳定在220us以内;字节数量如果超过16,会有风险。

对于GD Flash,要求flash_write_page写入字节数量最大值为16。如果超过16,比如32,可以拆成两次写16 byte。

在SDK设计上,两处涉及到flash_write_page的地方,一是SMP存储配置信息,使用的是每次写入16 byte;二是OTA写入新的firmware时,也使用了每次写入16 byte。OTA长包设计上,比如每包240 byte有效数据,是拆成了15次写入(16*15=240)。

强烈建议客户使用flash_write_page每次最多写入16 byte,否则会有跟BLE timing冲突的风险。

(2) Zbit Flash

Zbit Flash是ETOX工艺,flash_write_page的时间跟GD Flash类似,限制最大写入byte数量为16。

但由于Zbit Flash本身的一些特性,当温度上升时,flash_write_page消耗的时间会更长。针对这个特性,BLE SDK的处理方法为:

1) 对于有高温应用场景的产品,运营部门不会发给客户内置Zbit Flash的芯片。这一点也请客户注意。

2) SDK在涉及到flash_write_page的地方(SMP存储配对信息、OTA写入新的firmware)提升了Flash的供电电压,这样可以防止Zbit Flash写入时间太长。

上面第2点的措施非常重要,使用到Zbit Flash的产品必须确保以上措施已经生效。

该Handbook的对应的B85m单连接SDK(包括了825x、827x系列芯片)已经添加该措施,支持Zbit FLash。

对于历史825x/827x BLE单连接SDK几个重要历史版本,Telink BLE Team release了相关的patch,以支持Zbit Flash,请客户务必更新patch,确保生产无风险。

(3) PUYA Flash

825x和827x上增加的内置PUYA Flash是Sonos工艺,flash_write_page时间规律和ETOX工艺区别很大。

注意:

  • PUYA Flash暂时不会在825x和827x上量产应用,但保留相关兼容原理的介绍,用于加深用户对BLE timing的理解。

Sonos工艺Flash不支持byte烧写,只支持page烧写。

比如调用flash_write_page往 0x1000这个地址写入一个byte的数值0x5A,Flash内部的做法是先把0x1000对应的page内容(0x1000 ~ 0x10FF共256 byte)全部读出来缓存,然后将缓存上第一个byte修改为0x5A,最后将整个page缓存的值全部写到0x1000这个page。

这个机制引发的问题是:写入byte数量不管是1、2、4、8,还是200、255等,消耗的时间都跟写入一个page 256 byte的时间相似,大概消耗2ms左右。而前面我们说过关中断时间超过220us,Linklayer就有出错误的风险。

由于Sonos工艺Flash write时间2ms远大于安全阈值220us,需要在Linklayer设计上做规避方案来解决。

User在需要写Flash时,必须调用flash_write_page函数。flash_write_page在老版本SDK里是一个函数,在新版本SDK里是一个指针,这个指针默认指向flash_write_data函数,该函数时针对GD和Zbit Flash的flash_write_page的实现。

BLE SDK在初始化的时候检测Flash类型,如果发现是Sonos工艺Flash后,会将flash_write_page指针修改为另一个特殊的函数,该函数和Linklayer的时序设计进行了交互,主动去避开BTX、BRX的timing,让write flash的动作永远不要和BTX、BRX在时间上重合。这个设计在SDK底层实现中,对user不可见的,user放心使用flash_write_page即可。 下面以BRX为为例来讲解,BTX原理类似。 简化的模型如下图所示。

我们假设flash_write_page执行时间为标准的2ms(实际时间并不是这么标准,远比它复杂,SDK内部针对时间的波动做了处理)。

"Sonos工艺flash在接收窗口前后的动作"

图中write_req即软件中调用flash_write_page函数,write_wait指的是等待BRX状态机空闲时的一个安全时间点(软件中用一个while循环读取实现),write_execute是去调用Flash操作基本时序,完成flash的写入。Write_done表示写入成功。应用层看到的flash_write_pag消耗的总时间是从write_req到write_done的时间表。

Write flash case 1,write_req发生在BRX之外一个很安全的时间,直接执行wrire_execute。应用层看到的flash_write_pag时间为2ms。

Write flash case 2,write_req发生BRX事情中,此时若write_execute,可能出现破坏BRX timing导致丢包,所以先执行write_wait,等待BRX结束后再write_execute。应用层看到的flash_write_pag时间是write_wait的时间加上write_execute的时间(2ms),write wait时间跟BRX实际运行时间相关。

Write flash case 3,虽然write_req不在BRX timing中,但是由于未来有一个BRX在2ms之内即将响应,如果直接write_execute消耗2ms,会将BRX开始时间推迟,造成BLE slave收包时间错误。所以这里认为write_req是不安全的时间点,执行write_wait直到BRX结束后再write_execute。应用层看到的flash_write_pag时间是write_wait的时间加上write_execute的时间(2ms),write wait时间跟BRX实际运行时间相关。

从以上介绍可知,PUYA Flash上使用flash_write_page函数时,不是立刻就能执行write flash动作,可能需要经过等待一小段时间后才能去执行写入动作。如果大量频繁的调用flash_write_page,程序运行效率可能会降低,这是一个副作用。浪费的等待时间是write_wait消耗的时间,下图是一个比较极端的情况下的flash_write_page时间的实例,此时BRX存在大量的more data导致BRX持续时间非常大。

"more data导致的flash写动作延时"

按键扫描

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中调用下面函数完成keyscan。

u32 kb_scan_key (int numlock_status, int read_key)

第一个参数numlock_status在main_loop中调用时设为0即可;只有在deepsleep醒来的快速扫描按键时才会将其设为KB_NUMLOCK_STATUS_POWERON,后面的快速扫键中介绍(对应DEEPBACK_FAST_KEYSCAN_ENABLE)。

第二个参数read_key是keyscan函数按键的缓存处理,这个一般用不到,一直设为1即可(为0时会将按键值缓存在buffer里,不报告给上层)。

返回值用于通知user当前的按键扫描是否发现矩阵键盘有变化:有变化时,返回1;无变化时,返回0。

kb_scan_key这个函数是在main_loop中调用的,根据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自行理解。

Deepsleep唤醒快速扫描(wake_up fast keyscn)

当Slave设备在连接状态时进入deepsleep后,被按键唤醒。唤醒后,程序从头开始跑,要经过user_init后在main_loop中先发广播包,等到连上后才能将按键的值发送给ble master。

该 BLE SDK中已经做了相关的处理让deep back的回来速度尽可能的快,但这个时间还是可能会达到100 ms级别(如300 ms)。为了防止唤醒的这个按键动作丢掉,在SDK里做了快速扫键并缓存数据的处理。

快速扫键是因为按键唤醒后MCU重新从flash load程序重新初始化会耗掉一些时间,在main_loop中keyscan的时间由于防抖动滤波处理也会多一些时间,可能会导致这个按键的丢失。

缓存数据是因为如果在广播态扫描到了有效的按键数据,push 到BLE TX fifo后,进入连接态数据会被重新清掉。

相关的code在app_config.h中的宏DEEPBACK_FAST_KEYSCAN_ENABLE控制。

#define DEEPBACK_FAST_KEYSCAN_ENABLE    1
void deep_wakeup_proc(void)
{
#if(DEEPBACK_FAST_KEYSCAN_ENABLE)
    if(analog_read(DEEP_ANA_REG0) == CONN_DEEP_FLG){
        if(kb_scan_key (KB_NUMLOCK_STATUS_POWERON,1) && kb_event.cnt){
            deepback_key_state = DEEPBACK_KEY_CACHE;
            key_not_released = 1;
            memcpy(&kb_event_cache,&kb_event,sizeof(kb_event));
        }
    }
#endif
}

初始化的时候在user_init前就进行按键扫描,读取deep不掉电模拟寄存器检测到是连接状态进入deep唤醒后,调用kb_scan_key,这时不启动防抖动滤波处理,直接获取当前读到的整个矩阵的按键状态。若扫描发现有键按下(返回了按键变化并且kb_event.cnt非0),则将kb_event变量拷贝到缓存变量kb_event_cache。

main_loop的keyscan中添加deepback_pre_proc和deepback_post_proc处理:

void proc_keyboard (u8 e, u8 *p)
{
    kb_event.keycode[0] = 0;
    int det_key = kb_scan_key (0, 1);
#if(DEEPBACK_FAST_KEYSCAN_ENABLE)
    if(deepback_key_state != DEEPBACK_KEY_IDLE){
        deepback_pre_proc(&det_key);
    }
#endif
    if (det_key){
        key_change_proc();
    }
#if(DEEPBACK_FAST_KEYSCAN_ENABLE)
    if(deepback_key_state != DEEPBACK_KEY_IDLE){
        deepback_post_proc();
    }
#endif
}

deepback_pre_proc处理是等到slave和master连接上以后,在某一次kb_key_scan没有按键状态变化时,将之前缓存的kb_event_cache的值作为当前最新的按键变化,实现了快速扫键值的缓存处理。

需要注意一下按键释放的处理:手动给这个按键值时,判断一下当前矩阵按键的状态是否还有按键按着。若有键按着,不用后面加手动的release,因为实际的按键释放时会产生一个释放动作;若当前按键已经释放了,需要标记一下后面需要给一个手动的release,否则有可能会出现缓存的按键事件一直有效,无法释放。

deepback_post_proc处理就是根据deepback_pre_proc中是否留下手动release事件,来决定要不要往ble TX fifo里放一个按键release事件。

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时间。

若设为200 ms,表示当一个键被一直按着时,每过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即可。

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的优先级,优先级为0x00 < 0x01 < 0x02 < 0x04 < 0x08 < 0x10 < 0x20 < 0x40 < 0x80,以此类推。

当我们要定义一个长亮或长灭的led event时(没有时间限制,也就是repeatCount不起作用),将repeatCount的值设为255(0xff),此时onTime_ms和offTime_ms里面必须一个是0,一个非0,根据非0来判断是长亮还是长灭。

以下为几个led event的示例:

(1) 1 Hz的频率闪烁3秒:亮500ms,灭500ms,repeatCount 3次。

led_cfg_t led_event1 = {500,  500 ,  3,  0x00}; 

(2) 4 Hz的频率闪烁50秒:亮125ms,灭125ms,repeatCount 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 3次。

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中作相应的处理:

user_task_flg = ota_is_working || 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_ram_code_ void main_loop (void)
{
    tick_loop++;
#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     MAIN_LOOP_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 * CLOCK_16M_SYS_TIMER_CLK_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任务的响应时间距离现在只剩5秒(5s可以再改大一些)不到,则将该时间设为应用层提前唤醒的时间,否则关闭应用层提前唤醒。

if( (u32)(blt_timer.timer[0].t - now) < 5000 *  CLOCK_16M_SYS_TIMER_CLK_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)
{
    int i;
    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 * CLOCK_16M_SYS_TIMER_CLK_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请参考feature_test中TEST_USER_BLT_SOFT_TIMER。

int gpio_test0(void)
{
    DBG_CHN3_TOGGLE;  
    return 0;
}
int gpio_test1(void)
{
    DBG_CHN4_TOGGLE;  
    static u8 flg = 0;
    flg = !flg;
    if(flg){
        return 7000;
    }
    else{
        return 17000;
    }
}
int gpio_test2(void)
{
    DBG_CHN5_TOGGLE;        
    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每13ms toggle一次。

(4) gpio_test3每27ms toggle一次。

IR

PWM Driver

PWM相关的硬件配置很简单,基本都是直接操作寄存器来实现。操作寄存器的API大都定义在pwm.h中,使用static inline function来实现,这样可以提高运行效率,也节省code size。

PWM ID和管脚

B85共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管脚如下所示:

PWMx Pin PWMx_n Pin
PWM0 PA2/PC1/PC2/PD5 PWM0_N PA0/PB3/PC4/PD5
PWM1 PA3/PC3 PWM1_N PC1/PD3
PWM2 PA4/PC4 PWM2_N PD4
PWM3 PB0/PD2 PWM3_N PC5
PWM4 PB1/PB4 PWM4_N PC0/PC6
PWM5 PB2/PB5 PWM5_N PC7

使用void gpio_set_func(GPIO_PinTypeDef pin, GPIO_FuncTypeDef func)来设置对应管脚的PWM功能。

pin用实际PWM波形对应的GPIO;

func必须选择GPIO_FuncTypeDef定义里的AS_PWM0 ~ AS_PWM5_N,根据上面表中GPIO实际的pwm功能对应选择,如下所示。

typedef enum{
……
    AS_PWM0     = 20,
    AS_PWM1     = 21,
    AS_PWM2     = 22,
    AS_PWM3     = 23,
    AS_PWM4     = 24,
    AS_PWM5     = 25,
    AS_PWM0_N   = 26,
    AS_PWM1_N   = 27,
    AS_PWM2_N   = 28,
    AS_PWM3_N   = 29,
    AS_PWM4_N   = 30,
    AS_PWM5_N   = 31,
}GPIO_FuncTypeDef;

比如要使用PA2作为PWM0来用:

gpio_set_func(GPIO_PA2,  AS_PWM0);

PWM时钟

使用API void pwm_set_clk(int system_clock_hz, int pwm_clk)来设置PWM的clock。

system_clock_hz填当前的系统时钟CLOCK_SYS_CLOCK_HZ(这个宏是在app_config.h中定义的);

pwm_clk为要设置的clock, system_clock_hz一定要能被pwm_clk整除,才能分频得到这个正确的PWM clock。

为了让PWM波形精准,PWM clock需要尽量大,最大值不能超过系统时钟,推荐设置pwm_clk为CLOCK_SYS_CLOCK_HZ,即:

pwm_set_clk(CLOCK_SYS_CLOCK_HZ, CLOCK_SYS_CLOCK_HZ);

比如当前系统时钟CLOCK_SYS_CLOCK_HZ为16000000,上面设置的PWM clock时钟和系统时钟相等,为16M。

如果想要PWM时钟为8M,可按如下设置,不管系统时钟如何变化(CLOCK_SYS_CLOCK_HZ为16000000、24000000或32000000),PWM clock都是8M。

pwm_set_clk(CLOCK_SYS_CLOCK_HZ, 8000000);

PWM周期(cycle)和占空比(duty)

PWM波形的基本单位是PWM信号帧(Signal Frame)。

配置一个PWM Signal Frame需要设置cycle和cmp两个参数。

void pwm_set_cycle(pwm_id id, unsigned short cycle_tick)用于设置PWM cycle,单位为PWM clock的个数。

void pwm_set_cmp(pwm_id id, unsigned short cmp_tick)用于设置PWM cmp,单位为PWM clock的个数。

下面API是将上面两个API合二为一,可以提高设置的效率。

void pwm_set_cycle_and_duty(pwm_id id, unsigned short cycle_tick, unsigned short cmp_tick) 

那么对于一个PWM signal frame,计算PWM占空比(PWM duty):

PWM duty = PWM cmp/PWM cycle

下图相当于pwm_set_cycle_and_duty(PWM0_ID, 5, 2)得到的结果。一个Signal Frame的cycle为5个PWM clock,高电平时间为2个PWM clock,PWM duty为40%。

"PWM cycle & duty"

对于PWM0 ~ PWM5,硬件上会自动将高电平放前面,低电平放后面。如果想要低电平在前的话,有以下几种方法:

1) 使用对应的PWM0_N ~ PWM5_N,和PWM0 ~ PWM5相反。

2) 使用API static inline void pwm_revert(pwm_id id)将PWM0 ~ PWM5的波形直接取反。

比如当前的pwm clock为16MHz,需要设置PWM周期为1ms、占空比为50%的PWM0一个frame方法为:

pwm_set_cycle(PWM0_ID , 16000)
pwm_set_cmp (PWM0_ID , 8000)

pwm_set_cycle_and_duty(PWM0_ID, 16000, 8000);

PWM波形取反

API void pwm_revert(pwm_id id)用于将PWM0 ~ PWM5波形取反。

API void pwm_n_revert(pwm_id id)用于将PWM0_N ~ PWM5_N波形取反。

PWM开启和停止

下面两个API用于开启和停止某一路PWM:

void pwm_start(pwm_id id) ;
void pwm_stop(pwm_id id) ;

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;

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 void pwm_set_pulse_num(pwm_id id, unsigned short pulse_num) 用于设置指定的PWM波形中Signal Frame的个数。这里“pulse”和Signal Frames是同一个概念。

Normal mode(Continuous mode)不受Signal Frame个数的限制,所以此API对Normal mode无意义。只有其他4种特殊的mode才有可能用到这个API。

PWM中断

先介绍一下Telink MCU中断的一些基本概念。

中断“status”是由硬件的特定动作(也就是中断动作)产生的状态标记位,它不依赖于软件中任何设定,不管中断“mask”是否打开,只要中断动作发生,“status”必然置位(值为1)。一般通过对“status”写1的操作可以清除这个“status”(值回到0)。 定义中断响应的概念:中断响应,表示硬件中断动作产生后,软件上程序指针PC跳转到irq_handler去进行相关处理。

如果user希望中断被响应,就需要确保当前中断对应的所有“mask”全部被打开。“mask”可能有多个,多个“mask”之间是逻辑“与”的关系。只有当所有“mask”都打开了,中断“status”才会触发最终的中断响应,MCU跳转到irq_handler去执行;只要有一个“mask”没打开,中断“status”的产生都无法触发中断响应。

PWM driver中user可能需要用到的中断如下所示(code在文件register_8258.h中)。除此之外的中断是用不上的,user不用关注。

#define reg_pwm_irq_mask            REG_ADDR8(0x7b0)
#define reg_pwm_irq_sta             REG_ADDR8(0x7b1)
enum{
    FLD_IRQ_PWM0_PNUM =                 BIT(0),
    FLD_IRQ_PWM0_IR_DMA_FIFO_DONE  =    BIT(1),
    FLD_IRQ_PWM0_FRAME =                BIT(2),
    FLD_IRQ_PWM1_FRAME =                BIT(3),
    FLD_IRQ_PWM2_FRAME =                BIT(4),
    FLD_IRQ_PWM3_FRAME =                BIT(5),
    FLD_IRQ_PWM4_FRAME  =               BIT(6),
    FLD_IRQ_PWM5_FRAME =                BIT(7),
};

上面列出来的中断有8个,对应core_7b0/7b1的BIT<0:7>。core_7b0是这个8个中断的“mask”,core_7b1是8个中断的“status”。

将8个中断“status”分为3类。参考下图来说明,假设PWM0工作在IR mode,PWM Signal Frame的占空比为50%,每个IR task对应的脉冲个数pulse number(或Signal Frame number)为3。

"PWM interrupt"

1) 第一类:IRQ_PWMn_FRAME(n=0,1,2,3,4,5)

中断源后面的6个,是同一种中断,分别在PWM0~PWM5上产生。

如图所示,IRQ_PWMn_FRAME是每个PWM Signal Frame结束后都会产生的中断。PWM 5种模式里,Signal Frame都是PWM波形的基本单位。所以不管哪种PWM模式,IRQ_PWMn_FRAME都会出现。

2) 第二类:IRQ_PWM0_PNUM

IRQ_PWM0_PNUM是一组Signal Frame(个数由API pwm_set_pulse_num决定)结束时产生的中断。图中每3个Signal Frame后产生一个IRQ_PWM0_PNUM。

PWM的Counting mode、IR mode会用到API pwm_set_pulse_num。所以,只有PWM0的Counting mode、IR mode才会产生IRQ_PWM0_PNUM。

3) 第三类:IRQ_PWM0_IR_DMA_FIFO_DONE

PWM0工作在IR DMA FIFO mode时,当 DMA上所有配置好的PWM波形全部发送完毕后,触发IRQ_PWM0_IR_DMA_FIFO_DONE。

上面说到所有相关中断“mask”同时被打开时,才会触发中断响应,对于PWM中断,以FLD_IRQ_PWM0_PNUM为例,共有3层“mask”需要打开:

1) FLD_IRQ_PWM0_PNUM的“mask”

即core_7b0对应的“mask”,打开方法为:

reg_pwm_irq_mask |= FLD_IRQ_PWM0_PNUM;

一般在打开mask之前先清掉之前的status,防止中断响应被误触发:

reg_pwm_irq_sta = FLD_IRQ_PWM0_PNUM;

2) MCU系统中断上的PWM “mask”

即core_640的BIT<14>。

#define reg_irq_mask                REG_ADDR32(0x640)
enum{
        ……
        FLD_IRQ_SW_PWM_EN =         BIT(14),  //irq_software | irq_pwm
        ……
};

打开方法为:

reg_irq_mask |= FLD_IRQ_SW_PWM_EN;

3) MCU总中断使能位,即irq_enable()。

PWM phase

void pwm_set_phase(pwm_id id, unsigned short phase)

用于设置PWM开始前的延时时间。

phase为延时时间,单位为PWM clock的个数。一般不需要延时,设为0。

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后,触发中断IRQ_PWM0_IR_DMA_FIFO_DONE。

(1) DMA FIFO的配置

每个DMA FIFO上,使用2bytes (16 bits)来配置一个PWM waveform。调用下面API返回2 bytes的DMA FIFO数据。

unsigned short pwm_config_dma_fifo_waveform(int carrier_en, 
Pwm0Pulse_SelectDef pulse,  unsigned short pulse_num);

该API有三个参数:“carrier_en”、“pulse”和“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的数量。

“pulse”可选择如下两个定义。

typedef enum{
            PWM0_PULSE_NORMAL =     0,     
            PWM0_PULSE_SHADOW =     BIT(14), 
}Pwm0Pulse_SelectDef;

“pulse”为PWM0_PULSE_NORMAL时,Signal Frame来自API pwm_set_cycle_and_duty的配置;“pulse”为PWM0_PULSE_SHADOW时,Signal Frame来自PWM shadow mode的配置。

PWM shadow mode是为了增加一组Signal Frame的配置,从而为IR DMA FIFO mode的PWM waveform配置增加更多的灵活性。配置API如下,方法和API pwm_set_cycle_and_duty完全一致。

void pwm_set_pwm0_shadow_cycle_and_duty(unsigned short cycle_tick, 
unsigned short cmp_tick);

(2) 设置DMA FIFO Buffer

DMA FIFO buffer的配置完成后,调用下面API,将此buffer的首地址设置到DMA模块。

void pwm_set_dma_address(void * pdat);

(3) IR DMA FIFO Mode的开启与停止

DMA FIFO buffer准备好之后,调用下面API开启PWM waveform的发送:

void pwm_start_dma_ir_sending(void);

DMA FIFO buffer上所有PWM waveform发送结束后,PWM模块会自动停止。如果需要在此之前手动停止PWM模块,调用下面API:

void pwm_stop_dma_ir_sending(void);

IR Demo

User可参考SDK demo “ble_remote”中IR的code,将app_config.h中的宏 “REMOTE_IR_ENABLE”设为1。

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。

B85系列芯片支持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 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_ilde和第一个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)
    {
        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硬件配置

Demo code如下。

    pwm_n_revert(PWM0_ID);
    gpio_set_func(GPIO_PB3, AS_PWM0_N);
    pwm_set_mode(PWM0_ID, PWM_IR_DMA_FIFO_MODE);
    pwm_set_phase(PWM0_ID, 0);   //no phase at pwm beginning
    pwm_set_cycle_and_duty(PWM0_ID, PWM_CARRIER_CYCLE_TICK,  
                                          PWM_CARRIER_HIGH_TICK ); 
    pwm_set_dma_address(&T_dmaData_buf);
    reg_irq_mask |= FLD_IRQ_SW_PWM_EN;
    reg_pwm_irq_sta = FLD_IRQ_PWM0_IR_DMA_FIFO_DONE;

只有PWM0支持ID DMA FIFO mode,所以选用PB3对应的PWM0_N来实现。

Demo IR载波频率为38K,周期为26.3us,占空比为1/3。使用API pwm_set_cycle_and_duty配置周期和占空比。在Demo IR中,没有出现多种不同载波频率的情况,这个38K的载波足以满足所有FifoTask的配置。所以不需要使用PWM shadow模式。

DMA FIFO buffer为T_dmaData_buf。

打开系统中断mask “FLD_IRQ_SW_PWM_EN”。

清除中断状态“FLD_IRQ_PWM0_IR_DMA_FIFO_DONE”

(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_config_dma_fifo_waveform(1, PWM0_PULSE_NORMAL, 9000 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
    waveform_start_bit_2nd = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, 4500 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
 u16 waveform_stop_bit_2nd;

按照同样的方法,可以得到stop信号、repeat信号、data逻辑“1”信号、data逻辑“0”信号的配置。

FifoTask的配置

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;

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_config_dma_fifo_waveform中第一个参数carrier_en配置为0即可实现。

waveform_wait_to_repeat = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, tick_2_repeat_sysTimer/PWM_CARRIER_CYCLE_TICK);

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_config_dma_fifo_waveform(1, PWM0_PULSE_NORMAL, 9000 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
    waveform_repeat_2nd = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, 2250 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);

//repeat signal  second part,  560 us carrier, 99190 us low
    waveform_repeat_3rd = pwm_config_dma_fifo_waveform(1, PWM0_PULSE_NORMAL, 560 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
    waveform_repeat_4th = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, 99190 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);

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;

FifoTask_repeat*n and 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产生的中断时,再进行记录。实现代码如下:

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定义如下:

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_lenrn_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 。

"红外学习各阶段"

由于在红外学习状态设置IR_IN为GPIO下降沿唤醒,因此正常情况下每个下降沿都会进入中断,我们在中断中做记录的操作。在红外学习算法中,并不是将波形识别出特定的码型,而是以载波/非载波的概念记录波形。连续的载波会被认为一个载波段,而相隔时间较长的两个载波间被认为非载波。因此上述在红外学习算法中被认为如下图所示:

"载波与非载波的学习"

算法中每次执行,都会记录当前时间curr_trigger_tm_point,并与上次进入中断时间last_trigger_tm_point相减得到一个周期的时间time_interval。如果这个时间较小,则认为仍在载波中;如果这个时间超过所设阈值,则认为中间经历了一个no carrier段,而此时处在新的carrier段的第一个波形中:此时需要记录上一个载波段时间并放入buffer,其为第一次进入中断时间直到上一次中断时间的差值,如下图所示:

"红外学习算法"

按照此方法,令wave_series_cnt从0开始增加,分别对应第一个载波段、第一个非载波段、第二个载波段、第二个非载波段… 同时,将计算出的各个段的时间存入对应的位置(wave_series_buf[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常见问题

在学习过程中,有时会遇到学习成功后发送的波形频率发生变化,其可能的原因是学习的波形频率太高,导致在中断中执行算法的时间超过了载波周期。如下图所示。

"红外学习错误"

以占空比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。

软件模拟UART(Software UART)

为了方便一些user的双UART任务,除了支持硬件UART,B85m BLE SDK还提供了blt software UART demo,并且全部源码提供。user可以在理解了该demo的设计思路后直接使用,也可以自己做一些修改设计。

源代码全部在driver_ext/software_uart.c和software_uart.h文件中,若需要使用,先在feature_config.h里将宏FEATURE_TEST_MODE改为TEST_USER_BLT_SOFT_UART:

#define FEATURE_TEST_MODE                           TEST_USER_BLT_SOFT_UART

blt soft UART TX基于轮询设计,只有在广播时间和连接时间的间隔之间允许发送。blt soft UART RX是基于GPIO 中断和timer中断设计。

注意:

为了减少中断处理任务时间,软件串口工程必须使用SYS_CLK_48M_Crystal,且仅支持最高9600的波特率:

#define CLOCK_SYS_CLOCK_HZ                                  48000000    //must select 48M clock

Software UART初始化

调用下面的API进行初始化:

soft_uart_rx_handler(app_soft_rx_uart_cb);
extern int blt_send_adv();
extern void blc_ll_SoftUartisRfState();
soft_uart_sdk_adv_handler(blt_send_adv);
soft_uart_SoftUartisRfState_handler(blc_ll_SoftUartisRfState);
soft_uart_RxSetFifo(uart_rx_fifo.p, uart_rx_fifo.size);
soft_uart_init();

可以看到soft_uart_rx_handler函数将irq handler中的UART RX处理函数注册为应用层定义的回调函数app_soft_rx_uart_cb。

int app_soft_rx_uart_cb(void)//UART data send to Master,we will handler the data as CMD or DATA
{
    if (((uart_rx_fifo.wptr - uart_rx_fifo.rptr) & 255) < uart_rx_fifo.num) {
        uart_rx_fifo.wptr++;
        unsigned char* p = uart_rx_fifo.p + (uart_rx_fifo.wptr & (uart_rx_fifo.num - 1)) * uart_rx_fifo.size;
        soft_uart_RxSetFifo(p, uart_rx_fifo.size);
    }
    return 0;
}

soft_uart_RxSetFifo函数将UART接收FIFO注册为上层的定义的uart_rx_fifo。

u8          uart_rx_buf[80 * 4] = {0};
my_fifo_t   uart_rx_fifo = {
    80,
    4,
    0,
    0,
    uart_rx_buf,};

soft_uart_init函数将software UART RX和TX使用的GPIO口、RX GPIO中断、software UART相关的变量和使用到的timer0进行初始化。

void soft_uart_init(void)
{

    // set software rx io
    gpio_set_func(SOFT_UART_RX_IO, AS_GPIO);
    gpio_set_output_en(SOFT_UART_RX_IO, 0);
    gpio_set_input_en(SOFT_UART_RX_IO, 1);
    gpio_setup_up_down_resistor(SOFT_UART_RX_IO, PM_PIN_PULLUP_10K);
    gpio_set_interrupt(SOFT_UART_RX_IO, POL_FALLING);

    //set software tx io
    gpio_set_func(SOFT_UART_TX_IO , AS_GPIO);
    gpio_setup_up_down_resistor(SOFT_UART_TX_IO, PM_PIN_PULLUP_1M);
    gpio_set_output_en(SOFT_UART_TX_IO,1);//Enable output
    gpio_write(SOFT_UART_TX_IO, 1);// Add this code to fix the problem that the first byte will be error.

    soft_uart_rece.bit_num = 0x00;
    soft_uart_rece.temp_byte = 0x00;

    soft_uart_rece.stop_count = 0;
    soft_uart_rece.done_count = 0;