跳转至

B80 BLE单连接SDK


SDK介绍

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

软件组织架构

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

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

SDK文件结构

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

  • Application:提供一些通用的应用处理程序。

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

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

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

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

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

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

main.c

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

#if (PM_DEEPSLEEP_RETENTION_ENABLE)
_attribute_ram_code_sec_noinline_
#endif
int main(void)
{
    #if (BLE_APP_PM_ENABLE)
        blc_pm_select_internal_32k_crystal();
    #endif

    #if (BLE_OTA_SERVER_ENABLE && (FLASH_SIZE_OPTION == FLASH_SIZE_OPTION_128K))
        blc_ota_setFirmwareSizeAndBootAddress(48, MULTI_BOOT_ADDR_0x10000);
    #endif

    cpu_wakeup_init(EXTERNAL_XTAL_24M);

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

    rf_ble_1m_param_init();

    clock_init(SYS_CLK_TYPE);

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

    /* load customized freq_offset CAP value and TP value. */
    blc_app_loadCustomizedParameters();

    #if FIRMWARES_SIGNATURE_ENABLE
        blt_firmware_signature_check();
    #endif

    #if (PM_DEEPSLEEP_RETENTION_ENABLE)
        if( deepRetWakeUp ){
            user_init_deepRetn ();
        }
        else
    #endif
        {
        #if(BATT_CHECK_ENABLE)
            blc_app_loadADCParameters();
        #endif
            user_init_normal ();
        }

    #if (MODULE_WATCHDOG_ENABLE)
        wd_set_interval_ms(WATCHDOG_INIT_TIMEOUT,CLOCK_SYS_CLOCK_1MS);
        wd_start();
    #endif

    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协议栈初始化、数据处理、低功耗处理等。

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

  • app_buffer.c:用于配置ACL RX FIFO、ACL TX FIFO、L2CAP RX Buffer等。

  • app_ui.c:按键、OTA等用户任务的处理文件。

BLE Stack Entry

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

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

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

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

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

Applicable IC

适用如下几种IC型号,它们属于B80系列,均为同一内核,8208A/8208B/8208C硬件模块基本一致,只是在flash方面略有差异。具体如下表所示。

IC Flash size
8208A 无内置Flash
8208B 内置128 kB
8208C 内置512 kB

因为上面3颗IC的差异主要是Flash,其他部分是一致的,SDK文件架构除了SDK/boot/启动脚本(即software bootloader文件)和boot.link文件有差异外,其他部分完全共用。

Software Bootloader介绍

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

bootloader以及boot.link路径

Demo介绍

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

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

BLE SDK 提供的demo code

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

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

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

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

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

8208 ble sample是一个简化的slave demo,可以和标准的IOS/android设备配对连接。

Feature Demo和Driver Demo

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

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

MCU基础模块

MCU地址空间

MCU地址空间分配

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

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

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

MCU地址空间分配

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

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

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

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

SRAM空间分配

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

(1) SRAM和Firmware空间

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

16kB Sram 地址空间范围为0x840000 ~ 0x844000,对应的Sram和Firmware空间分配如下图所示。

Sram空间分配& Firmware空间分配

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

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

Sram中包括vector、ramcode、Cache、data、data_reload、bss、stack和unused sram area。

Sram中的vector/ramcode是Flash中vector/ramcode的拷贝。

(a) vectors and ram_code

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

“ramcode”段是Flash Firmware中需要常驻Ram的code,对应SDK中所有加了关键字“attribute_ram_code”或者“attribute_ram_code_sec_noinline”的函数,比如flash_send_cmd 函数:

_attribute_ram_code_sec_noinline_ static void flash_send_cmd(unsigned char cmd);

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

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

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

用户如果需要将某个函数常驻内存,可以仿照上面flash_send_cmd,在自己的函数上添加关键字,编译之后就能在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)

(b) 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中的起始地址为可配置的。

(c) data

“data” 段是Sram中存放程序已经初始化的全局变量,即initial value非0的全局变量。该部分只会在上电或者deep sleep唤醒后重新初始化,在deepsleep retention和suspend期间均保持睡眠前的值。

“data”段紧跟Cache,起始地址即Cache的结束地址。下面为boot.link中的代码,直接定义Sram上“data”段开始的地址:

     . = (0x840000 + ((0x900 + _ramcode_size_align_256_) * __LOAD_FLASH) + (0x400 * __LOAD_DUT) + (_dstored_ * __LOAD_RAM));     
    .data :

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

(d) data_reload

为了节省retention的唤醒起来的boot时间,8208中“data” 段和“bss” 段被设置为retention保留,retention起来不再初始化。另一方面,有些变量就是retention回来是有初始化需要的,因此我们增加了“data_reload”段。这个段的特点是:其在retention回来后会重新初始化,不再保留睡眠之前的值。

“data_reload”段紧跟“data”段,起始地址即“data”段的结束地址。下面为boot.link中的代码,直接定义Sram上“data_reload”段开始的地址:

    PROVIDE(_rstored_ = _dstored_ + _end_data_ - _start_data_); 

    .data_reload :
    AT ( _rstored_ )

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

(e) bss

“bss” 段是Sram中存放程序未初始化的全局变量,即initial value为0的全局变量。该部分只会在上电或者deep sleep唤醒后重新初始化,在deepsleep retention和suspend期间均保持睡眠前的值。

“bss” 段紧跟“data_reload”段,起始地址即“data_reload”段的结束地址。下面为boot.link中的代码,直接定义Sram上“bss”段开始的地址:

    PROVIDE(_data_reload_end_ = . );     
    ……
    .bss :
    {
    . = (((. + 3) / 4)*4);
    PROVIDE(_start_bss_ = .);
    ……

(f) stack / unused area

对于16K的Sram ,“stack”是从最高地址0x844000开始的,其方向为从下往上延伸,即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中会给出分析方法。

(g) text

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

(h) rodata /data init value /data_reload init value

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

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

static const attribute_t my_Attributes[] = ……

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

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

int   testValue = 0x1234;

那么编译器就会将初值0x1234存放到“data initial value”中,在上电或者deepsleep唤醒时,会将该初值拷贝到testValue对应的内存地址。

前面介绍的“data_reload”段是程序中定义为retention回来重新初始化的全局变量,比如定义全局变量如下:

_attribute_data_reload_ int   reloadValue = 0x1234;

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

(2) list文件分析demo:

这里以8208 ble sample的默认配置为例,结合“Sram空间分配& Firmware空间分配”来分析。

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

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

list文件section统计

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

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

(b) ram_code: 从Flash 0x210开始,Size为0x1e5c,计算出结束地址为0x2100;

(c) text: 从Flash 0x2100开始,Size为0x7098,计算出结束地址为0x9198;

(d) rodata: 从Flash 0x9198开始,Size为0x800,计算出结束地址为0x9998;

(e) data: 从Sram 0x842a00开始,Size为0x174,计算出结束地址为0x842b74;

(f) bss: 从Sram 0x842b80开始(因为对齐原因不是从0x842b74开始),Size为0x980,计算出结束地址为0x843500。

结合前面介绍可知,剩余Sram空间为0x844000 –0x843500= 0xb00 = 2816 byte,减去stack需要使用的256 byte,还剩2560 byte。

list文件section地址

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

(a) vector:

“vectors”段在flash firmware中起始地址为0,结束地址为0x210,size为0x210。上电搬移到Sram后,在Sram上的地址为0x840000 ~ 0x840210。

(b) ram_code:

“ram_code”段在flash firmware中起始地址为0x210,结束地址为0x2100。上电搬移到Sram后,在Sram上的地址为0x840210 ~ 0x842100。

(c) Cache:

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

(d) text:

“text”段在flash firmware中起始地址为0x2100,结束地址为0x9198,Size为0x9198– 0x2100 = 0x7098,和前面Section统计中数据一致。

(e) rodata:

“rodata”段起始地址为text的结束地址0x9198,结束地址为0x9998。

(f) data:

“data”段在Sram上起始地址为Cache的结束地址0x842a00,list文件Section统计部分给出的size为0x174。“data”段在Sram上的结束地址为0x842b74。

(g) bss:

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

“bss”段在Sram上的结束地址为0x843500。

剩余Sram空间为0x844000 – 0x843500 = 0xb00 = 2816 byte,减去stack需要使用的256 byte,还剩2560 byte。

MCU地址空间访问

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

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

外设空间(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/bsp.h。

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

#define REG_BASE_ADDR           0x800000

#define REG_ADDR8(a)            (*(volatile unsigned char*) (REG_BASE_ADDR | (a)))
#define REG_ADDR16(a)           (*(volatile unsigned short*)(REG_BASE_ADDR | (a)))
#define REG_ADDR32(a)           (*(volatile unsigned long*) (REG_BASE_ADDR | (a)))

#define write_reg8(addr,v)      (*(volatile unsigned char*)  (REG_BASE_ADDR | (addr)) = (unsigned char)(v))
#define write_reg16(addr,v)     (*(volatile unsigned short*) (REG_BASE_ADDR | (addr)) = (unsigned short)(v))
#define write_reg32(addr,v)     (*(volatile unsigned long*)  (REG_BASE_ADDR | (addr)) = (v))

#define read_reg8(addr)         (*(volatile unsigned char*) (REG_BASE_ADDR | (addr)))
#define read_reg16(addr)        (*(volatile unsigned short*)(REG_BASE_ADDR | (addr)))
#define read_reg32(addr)        (*(volatile unsigned long*) (REG_BASE_ADDR | (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字节对齐

(2) Flash的操作

此部分内容详见Flash章节。

SDK Flash空间的分配

此部分内容详见Flash章节。

时钟模块

System Clock & System Timer

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

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

在Telink上一代IC(826x系列)上,System Timer的时钟来源于system clock,而B80系列 IC上,System Timer和system clock是独立分开的。如下图所示,System Timer是由外部24M Crystal Oscillator经分频得到的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)

由于B80系列芯片的 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以及对应的s、ms、us的定义如下:

#define CLOCK_SYS_CLOCK_HZ      24000000
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(PWM0_ID, (u16) (1000 * CLOCK_SYS_CLOCK_1US),  (u16) (500 * CLOCK_SYS_CLOCK_1US) );

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

//system timer clock source is constant 16M, never change
enum{
        CLOCK_16M_SYS_TIMER_CLK_1S =  16000000,
        CLOCK_16M_SYS_TIMER_CLK_1MS = 16000,
        CLOCK_16M_SYS_TIMER_CLK_1US = 16,
};

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

void sleep_us (unsigned long microsec);
unsigned int clock_time(void);
int clock_time_exceed(unsigned int ref, unsigned int span_us);
#define ClockTime           clock_time
#define WaitUs              sleep_us
#define WaitMs(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/gpio.h、gpio_default.h、gpio.c来理解,所有代码都以源码形式提供。

代码中涉及到对寄存器的操作,请参考datasheet来理解。

GPIO定义

B80 系列芯片最多支持5组38个GPIO,分别为:GPIO_PA0 ~ GPIO_PA7、GPIO_PB0 ~ GPIO_PB7、GPIO_PC0 ~ GPIO_PC7、GPIO_PD0 ~ GPIO_PD7、GPIO_PE0 ~ GPIO_PE3和GPIO_PF0 ~ GPIO_PF1。

注意:

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

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

注意:

7个GPIO比较特殊,需要注意:
a)MSPI的4个GPIO。这4个GPIO是MCU 系统总线中主SPI总线,用于读写flash操作,上电默认为spi状态,user永远不能操作它们,程序中不能使用。这个4个GPIO为PE0、PE1、PE2、PE3。
b)SWS (Single Wire Slave),用于debug和烧写firmware,上电默认为SWS状态,程序中一般不使用。B80的SWS管脚为PA3。
c)DM和DP,上电默认为GPIO状态。当需要USB功能时,DM和DP需要使用;当不需要USB时,可以作为GPIO使用。B80的DM、DP管脚为 PA1、PA2。

GPIO状态控制

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

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

void gpio_set_func(GPIO_PinTypeDef pin, gpio_func_e 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管脚的电平,用于读取外部电压。

static inline _Bool gpio_read(GPIO_PinTypeDef pin);

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

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

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

static inline void gpio_set_output_en(GPIO_PinTypeDef pin, unsigned int value);

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

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

static inline 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_PA2, 0);//DP
    gpio_set_func(GPIO_PA1, 0);//DM
    gpio_set_input_en(GPIO_PA2|GPIO_PA1,1);//DP/DM must set input enable

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/gpio.h、drivers/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中可以提前定义这些宏,这些宏就不再使用以上这种默认值。

在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来控制一些外围设备,但到了B80上如果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电阻来解决。

B80的SWS和GPIO_PA3复用,在drivers/gpio_default.h中将PA3的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。

Telink BLE Controller:

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

B80 BLE Single Connection SDK包含Link Layer的3种标准状态(standby、advertising、connection),conneciton状态仅支持Slave role。B80 BLE Single Connection SDK中的Slave role只是single connection,即Link Layer只能维持一个连接,无法同时多个Slave同时存在。

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

Telink HCI架构

Telink BLE Slave:

B80 BLE Single Connection SDK在BLE host上,只支持Slave部分。

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

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

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

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

BLE Controller

BLE Controller介绍

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

Telink BLE SDK对Physical Layer(对应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_v4.2》(Vol 6/Part B/1.1 “LINK LAYER STATES”)。

BLE Spec中Link Layer状态机

B80 BLE Single Connection SDK Link Layer状态机如下图所示。

Telink Link Layer状态机

B80 BLE Single Connection SDK由于仅支持Slave Role,状态机仅拥有3种基本状态:Idle(Standby)、Advertising、Connection(Slave Role)。Connection(Slave Role)在后面会简称为ConnSlaveRole。

图中Power Management并不是LL的一种状态,而是功能模块,表示的是SDK只对Advertising和ConnSlaveRole进行了低功耗处理;Idle state如果需要低功耗,user在应用层调用相关API可以完成。

基于上面3种状态,stack/ble/ll/ll.h中定义了状态机的命名。

#define BLS_LINK_STATE_IDLE         0
#define BLS_LINK_STATE_ADV          BIT(0)
#define BLS_LINK_STATE_CONN         BIT(3)

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

u8      blc_ll_getCurrentState(void);

(1) Link Layer状态机初始化

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

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

void        blc_ll_initBasicMCU (void);

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

void        blc_ll_initStandby_module (u8 *public_adr);

Advertising、ConnSlaveRole对应模块的初始化API分别如下:

void        blc_ll_initAdvertising_module(void);
void        blc_ll_initSlaveRole_module(void);

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

User通过以上几个API去配合组合Link Layer状态机,下面给出两个常用的组合方式和对应的应用场景。

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

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

(3) Idle + Advertising + ConnSlaveRole

BLE Slave LL State

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

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

u8  mac_public[6] = {……};
blc_ll_initBasicMCU();                     
blc_ll_initStandby_module(mac_public);
blc_ll_initAdvertising_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时设置的值。

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

_attribute_ram_code_ void irq_handler(void)
{
    ……
    blc_sdk_irq_handler ();
    ……
}

void main_loop (void)
{
///////////////////// BLE entry ////////////////////////////
    blc_sdk_main_loop();
////////////////////// UI entry ////////////////////////////
    ……
}

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

Idle State时序:

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

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,blc_sdk_main_loop函数没有太多要处理的任务,只是触发几个跟Adv相关的几个回调事件。包括BLT_EV_FLAG_ADV_DURATION_TIMEOUT、BLT_EV_FLAG_SCAN_RSP、BLT_EV_FLAG_CONNECT等。

Conn State Slave Role时序:

Conn State Slave Role时序

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

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

(1) brx start阶段

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

(2) brx working阶段

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

(3) 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,blc_sdk_main_loop需要处理brx过程收到的数据。brx working过程中,实际是在RX接收中断irq处理中将硬件收到的master数据包拷贝出来,这些数据并不会立刻实时处理,而是将数据缓存到软件RX fifo中(对应code中的app_acl_rxfifo)。blc_sdk_main_loop函数会检查软件RX fifo中是否有数据,只要有数据就去处理。

blc_sdk_main_loop对数据包的处理包括:

(1) 数据包的解密

(2) 数据包的解析

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

Conn State Slave role时序保护:

ConnSlaveRole,每个interval需要一个收发包事件,也就是上面的Brx Event。B80 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基本应用。

但是考虑到user可能会有的一些特殊的应用,B80 BLE SDK增加了Conn state Slave role时还要能够advertising,下面详细描述。

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的使用请参考8208_feature中的TEST_ADVERTISING_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或应用层处理。

BLE TX fifo和BLE RX fifo都在应用层app_buffer.c中定义:

u8  app_acl_rxfifo[ACL_RX_FIFO_SIZE * ACL_RX_FIFO_NUM] = {0};
u8  app_acl_txfifo[ACL_TX_FIFO_SIZE * ACL_TX_FIFO_NUM] = {0};

之后在app.c中配置到底层stack中:

blc_ll_initAclConnTxFifo(app_acl_txfifo, ACL_TX_FIFO_SIZE, ACL_TX_FIFO_NUM);
blc_ll_initAclConnRxFifo(app_acl_rxfifo, ACL_RX_FIFO_SIZE, ACL_RX_FIFO_NUM);

其中ACL_TX_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默认为4,这是一个比较合理的值,能够确保Link Layer底层最多缓存4个数据包。如果设置的太大,会占用过多的Sram,如果设置的太小,可能出现数据覆盖的风险:在brx event时,Link Layer很可能在一个interval上出现more data(MD)模式,连续收多个包,如果设置太少的话,很可能在一个interval上出现多个包(比如OTA),而上层对这些数据的响应由于解密时间较长来不及处理,那么就有可能有一些数据被overflow。

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

(1) RX fifo数量为8;

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

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

(4) 两个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默认为8。如果设置太大(如16)会占用过多的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,防止发生各种边界问题。

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架构

Controller HCI Event:

HCI event是按BLE Spec标准设计的,对于BLE slave,HCI event和Telink defined event同时可用。

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

User可以根据自己的需要,在这两套event挑选一套使用,也可以两套同时使用。

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

HCI Event

Controller HCI event的定义,详情请参照《Core_v4.2》 (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中定义的一致。

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

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

(2) 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中完成了,这里不介绍细节。

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

(4) 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的上报。

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  

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

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

(3) HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE

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

LE Connection Update Complete Event

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

ble_sts_t   blc_hci_le_setEventMask_cmd(u32 evtMask);

evtMask的定义也对应上面给出一些,其他的event用户可以在hci_const.h中查到。

#define HCI_LE_EVT_MASK_CONNECTION_COMPLETE             0x00000001
#define HCI_LE_EVT_MASK_ADVERTISING_REPORT              0x00000002
#define HCI_LE_EVT_MASK_CONNECTION_UPDATE_COMPLETE      0x00000004

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

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中绝大多数都是用的这种方式;只有8208_ module使用了方式2 “shared event entry”。

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

(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,
} 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/lib/include/pm.h可看到如下唤醒状态。

enum {
     WAKEUP_STATUS_PAD              = BIT(0),
     WAKEUP_STATUS_CORE             = BIT(1),
     WAKEUP_STATUS_TIMER            = BIT(2),

     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执行blc_sdk_main_loop函数时,进入suspend之前触发该事件。

回传指针p:空指针NULL。

数据长度n: 0。

(16) BLT_EV_FLAG_SUSPEND_EXIT

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

回传指针p:空指针NULL。

数据长度n: 0。

注意:

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

Data Length Extension

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

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

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

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

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

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

发长包需要加大TX fifo size。TX fifo size至少比TX rf_len大12,且必须按4字节对齐,在app_buffer.h中直接修改ACL_CONN_MAX_TX_OCTETS,code中会自动处理对齐。

#define ACL_CONN_MAX_TX_OCTETS          27

收长包需要加大RX fifo size。RX fifo size至少比RX rf_len大24,且必须按16字节对齐,在app_buffer.h中直接修改ACL_CONN_MAX_RX_OCTETS,code中会自动处理对齐。

#define ACL_CONN_MAX_TX_OCTETS          27

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”部分的详细说明,也可以参考8208_feature里面DLE demo的写法。

#define MTU_SIZE_SETTING                    247
blc_att_setRxMtuSize(MTU_SIZE_SETTING);

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

在实际应用中,当8207作为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”部分的详细说明,也可以参考8208_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

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) 对于8208_hci类的应用,Telink的IC只作为BLE controller,需要和其他家的运行BLE host的MCU协同工作,每个HCI command都会有对应的HCI command complete event或HCI command status event产生。

(2) 对于8208_sample应用,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_slave.h、ll_pm.h,user可以根据Link Layer的功能去寻找,比如跟advertising相关功能的API就应该都在ll_adv.h中声明。

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

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的地址即可,参考文档前面的介绍,8208 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状态机初始化:

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

void        blc_ll_initBasicMCU (void)
void        blc_ll_initStandby_module (u8 *public_adr);
void        blc_ll_initAdvertising_module(void);
void        blc_ll_initSlaveRole_module(void);

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[] = {
     8,  DT_COMPLETE_LOCAL_NAME,                'f','e','a','t','u','r','e',
     2,  DT_FLAGS,                              0x05, 
     3,  DT_APPEARANCE,                         0x80, 0x01,
     5,  DT_INCOMPLT_LIST_16BIT_SERVICE_UUID,   0x12, 0x18, 0x0F, 0x18,
};

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

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中8208_ble_sample工程中定义scan response data如下,scan设备名为"eSample" 。各个字段的含义请参考文档BLE Spec《CSS v6》(Core Specification Supplement v6.0)中Data Type Specifcation的具体说明。

const u8    tbl_scanRsp [] = {
     8,  DT_COMPLETE_LOCAL_NAME,        'e', 'S', 'a', 'm', 'p', 'l', 'e',
};

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

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

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

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

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

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。

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。

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

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

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

注意:

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

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回调。

blc_ll_setAdvCustomedChannel:

下面API用于定制特殊的advertising 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);

rf_set_power_level_index:

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

void rf_set_power_level_index (RF_PowerTypeDef level);

level值的设置参考drivers/lib/include/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所示。

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

bls_ll_terminateConnection:

ble_sts_t   bls_ll_terminateConnection (u8 reason);

应用层可以调用此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(有大量数据正在发送)暂时无法接受该命令。

Get Connection Parameters:

获取当前连接参数的Connection Interval、Connection Latency、Connection Timeout的API如下:

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

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

(2) 若返回为非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。

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)

blc_ll_getLatestAvgRSSI:

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

u8      blc_ll_getLatestAvgRSSI(void)

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

Whitelist & Resolvinglist:

前面介绍过,Advertising的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);

删除之前添加的设备。

2M PHY

2M PHY介绍:

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

2M PHY Demo介绍:

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

  • Slave端可参考Demo “b80_feature_test”

在vendor/8208_feature/feature_config.h中定义宏

#define FEATURE_TEST_MODE   TEST_PHY_CONN

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

2M PHY API介绍:

(1) API

void blc_ll_init2MPhy_feature(void)

用于使能2M 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);

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

connHandle:BLS_CONN_HANDLE。

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

BLE Host

BLE Host介绍

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

L2CAP

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

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

BLE L2CAP结构以及ATT组包模型

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

注册L2CAP数据处理函数:

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

void    blc_l2cap_register_handler (void *p);

在8208_ble_sample/8208_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);

在8208 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);

更新连接参数:

(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再执行发送连接参数更新请求。

(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格式

(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 and GATT

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/ble/service/hids.h、stack/service/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用指针和指针所指区域的长度来描述。

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上的地址。

此外,当att表存在CHARACTERISTIC_UUID_HID_REPORT_MAP时,很多设备都要识别操作系统,然后根据操作系统去发送不同的report map,所以底层在处理时候可以将对应的pAttrValue和attrLen重新注册,API接口如下。

ble_sts_t bls_att_setHIDReportMap(u8* p,u32 len);

如果需要恢复Attribute Table中定义的预设值,调用以下API接口。

ble_sts_t bls_att_resetHIDReportMap();

(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的写操作会越界,导致其他数据被错误的改写)。另外,回调函数w的返回值为0,代表写成功,其他返回值请参考枚举att_err_t或者按照协议要求user自行定义。

BLE 协议栈中Write Request

BLE 协议栈中Write Command

协议栈中Execute Write Request

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

typedef struct{
    u8  type;
    u8  rf_len;
    u16 l2capLen;
    u16 chanId;
    u8  opcode;
    u16 handle;
    u8  dat[20];
}rf_packet_att_data_t;

p指向type。写过来的数据有效长度为l2cap - 3,第一个有效数据为p->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 0;
}   

上面这个结构体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的指针。

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

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

ble_sts_t   blc_att_setRxMtuSize(u16 mtu_size);

返回值列表:

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

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

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

当b80 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
SMP_ERR_PAIRING_BUSY 见SDK中定义 处于配对阶段,不能发送数据
LL_ERR_ENCRYPTION_BUSY 见SDK中定义 处于加密阶段,不能发送数据
LL_ERR_TX_FIFO_NOT_ ENOUGH 见SDK中定义 有大数据量任务在运行,软件Tx fifo不够用
GATT_ERR_DATA_LENGTH_ EXCEED_MTU_SIZE 见SDK中定义 len大于ATT_MTU-3时,发送的数据长度超出了ATT层支持的最大数据长度
GATT_ERR_DATA_PENDING_ DUE_TO_SERVICE_DISCOVERY _BUSY 见SDK中定义 处于遍历服务阶段,不能发数据

(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   blc_gatt_pushHandleValueIndicate(u16 connHandle, u16 attHandle, u8 *p, int len);

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

BLE Spec里规定了每一个indicate的数据,都要等到Master的confirm才能认为indicate成功,未成功时不能发送下一个indicate数据。

Link Layer在Conn state时,一般情况下直接调用该API可以成功push数据到底层软件fifo,但也存在一些特殊情况会导致该API调用失败,根据返回值ble_sts_t可以了解对应的错误原因。建议应用层调用该API时,检查一下返回值是否为BLE_SUCCESS,若不为BLE_SUCCESS,则需要等待一段时间后再次push。返回值列表如下:

ble_sts_t Value ERR Reason
BLE_SUCCESS 0 添加成功
LL_ERR_CONNECTION_NOT_ ESTABLISH 见SDK中定义 Link Layer处于None Conn state
SMP_ERR_PAIRING_BUSY 见SDK中定义 处于配对阶段,不能发送数据
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确认
GATT_ERR_DATA_LENGTH_EXCEED_ MTU_SIZE 见SDK中定义 len大于ATT_MTU-3时,发送的数据长度超出了ATT层支持的最大数据长度

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

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种中的一种:

(1) No authentication and no encryption

(2) Unauthenticated pairing with encryption

(3) Authenticated pairing with encryption

(4) 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安全级别的示例用户可以参8208_feature/ feature_gatt_security/app.c。

SMP

Security Manager(SM)在BLE中的主要目的是为LE设备提供加密所需要的各种Key,确保数据的机密性。加密链路可以确保避免第三方“攻击者”拦截、破译或者读取空中数据原始内容。SMP详细内容请用户参考《Core_v5.0》(Vol 3/Part H/ Security Manager Specification)。

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)

(1) No authentication and no encryption (LE security mode1 level1)

(2) Unauthenticated pairing with encryption (LE security mode1 level2)

(3) Authenticated pairing with encryption (LE security mode1 level3)

(4) 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;

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如下:

void blc_smp_setTK_by_PasskeyEntry (u32 pinCodeInput);

参数pinCodeInput表示设置的pincode值,范围在“0~999999”。在Passkey Entry方式下,master显示TK,slave需要输入TK的情况下使用。

最终设备使用何种Key产生方式是基于配对连接的两端设备支持什么样的SMP安全等级的,如果master只支持安全等级LE security mode1 level1,那么最终slave是不会启用SMP功能的,因为master不支持配对加密。

(4) LE security mode1 level4

安全级别4表示设备最高支持Authenticated LE Secure Connections,如安全连接配对模式下的Numeric Comparison、Passkey Entry、Out of Band等。

根据以上介绍,我们知道LE security mode1 level4有如下几种初始值配置方式:

a) 设备具备安全连接配对下Numeric Comparison的初始化配置:

blc_smp_setParingMethods(LE_Secure_Connection);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_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的初始化配置:

该部分目前SDK并未实现,所以这里就不再做具体介绍。

(5) 下面再介绍几个额外的SMP参数配置相关的API:

a) SDK提供是否需要开启绑定功能的API:

void blc_smp_setBondingMode(bonding_mode_t mode);

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

typedef enum {
    Non_Bondable_Mode = 0,
    Bondable_Mode     = 1,
}bonding_mode_t;

配置安全级别非mode1 level1的设备,必须要使能绑定功能,SDK已经默认开启了,所以一般情况下用户不需要再调用该API。

b) SDK提供是否需使能Key Press功能的API:

void blc_smp_enableKeypress (int keyPress_en);

表示Passkey Entry期间是否支持为KeyboardOnly设备提供一些必要的输入状态信息,因为目前SDK不支持该功能,所以参数必须设置为0。

c) 在安全连接方式下是否启用Debug椭圆加密密匙对:

void blc_smp_setEcdhDebugMode(ecdh_keys_mode_t mode);

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

typedef enum {
    non_debug_mode = 0,//ECDH distribute private/public key pairs
    debug_mode = 1,//ECDH use debug mode private/public key pairs
} ecdh_keys_mode_t;

该API仅在安全连接配对情况下使用,由于安全连接配对情况下使用了椭圆加密算法,可以有效避免窃听,这对调试开发就不那么友好了,用户无法通过sniffer工具抓取BLE空中包,进而进行数据分析调试,所以BLE spec也规定给出了一组用于Debug的椭圆加密私钥/公钥对,只要开启这个模式,BLE sniffer工具就可以用已知的密钥去解密链路。

d) 通过如下API设置SM是否绑定、是否开启MITM flag、是否支持OOB、是否支持Keypress notification,以及支持的IO能力(文档前面给的都是单独的配置API,为了用户设置方便,SDK也提供了统一配置API):

void blc_smp_setSecurityParameters (bonding_mode_t mode,int MITM_en,int OOB_en, int keyPress_en,
io_capability_t ioCapablility);

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

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函数。

SMP绑定信息说明:

这里讨论的SMP绑定信息是对slave设备来说的。用户可以参考SDK demo "8208_ble_sample"初始化中设置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中合适区域。

SMP失败管理:

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

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

    blc_gap_registerHostEventHandler( app_host_event_callback );
    blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_FAIL );

(2) 修改处理函数中该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

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层事件处理函数入口。

GAP Event:

GAP event则是ATT、GATT、SMP、GAP等host协议层交互过程中产生的事件。从前文我们可以知道,目前SDK事件主要分为两大类:Controller event和GAP(host) event,其中controller event又分为HCI event和Telink defined event。

Telink BLE SDK中新增了GAP event处理,主要是协议栈事件分层更加清晰,协议栈处理用户层交互事件更加便捷,特别是SMP相关的处理,如Passkey的输入,配对结果通知用户等。

如果user需要在App层接收GAP event,首先需要注册GAP event的callback函数,其次需要将对应event的mask打开。

GAP event的callback函数原型和注册接口分别为:

typedef int (*gap_event_handler_t) (u32 h, u8 *para, int n);
void blc_gap_registerHostEventHandler (gap_event_handler_t  handler);

callback函数原型中的u32 h是GAP event标记,底层协议栈多处会用到。

下面列出几个用户可能会用到的事件:

#define GAP_EVT_SMP_PAIRING_BEGIN                                    0
#define GAP_EVT_SMP_PAIRING_SUCCESS                                  1
#define GAP_EVT_SMP_PAIRING_FAIL                                     2
#define GAP_EVT_SMP_CONN_ENCRYPTION_DONE                             3
#define GAP_EVT_SMP_SECURITY_PROCESS_DONE                            4
#define GAP_EVT_SMP_TK_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可参考8208_feature/feature_gatt_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_NONE                     0x00000000
#define GAP_EVT_MASK_SMP_PAIRING_BEGIN        (1<<GAP_EVT_SMP_PAIRING_BEGIN)
#define GAP_EVT_MASK_SMP_PAIRING_SUCCESS      (1<<GAP_EVT_SMP_PAIRING_SUCCESS)
#define GAP_EVT_MASK_SMP_PAIRING_FAIL         (1<<GAP_EVT_SMP_PAIRING_FAIL)
#define GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE (1<<GAP_EVT_SMP_CONN_ENCRYPTION_DONE)
#define GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE     (1<<GAP_EVT_SMP_SECURITY_PROCESS_DONE)
#define GAP_EVT_MASK_SMP_TK_DISPALY           (1<<GAP_EVT_SMP_TK_DISPALY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY   (1<<GAP_EVT_SMP_TK_REQUEST_PASSKEY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_OOB       (1<<GAP_EVT_SMP_TK_REQUEST_OOB)
#define GAP_EVT_MASK_SMP_TK_NUMERIC_COMPARE   (1<<GAP_EVT_SMP_TK_NUMERIC_COMPARE)
#define GAP_EVT_MASK_ATT_EXCHANGE_MTU         (1<<GAP_EVT_ATT_EXCHANGE_MTU)
#define GAP_EVT_MASK_GATT_HANDLE_VLAUE_CONFIRM  (1<<GAP_EVT_GATT_HANDLE_VLAUE_CONFIRM)

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

注意:

以下论述GAP event时,均设定注册了GAP event回调,且开启了对应的eventMask。

(1) GAP_EVT_SMP_PAIRING_BEGIN

事件触发条件:当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_manualSetPinCode_for_debug(u16 connHandle, u32 pinCodeInput);

用户将slave上看到的6位pincode码输入到master设备上(如手机),完成TK输入,配对流程得以继续执行。如果用户输入pincode错误或者点击取消,则配对流程失败。

关于Passkey Entry应用的实例,用户可以参考SDK提供的demo “vendor/8208_feature/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/8208_feature/feature_gatt_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。

低功耗管理(PM)

低功耗管理(Low Power Management)也可以称为功耗管理(Power Management),本文档中会简称为PM。

低功耗驱动

低功耗模式

8208 MCU正常执行程序时处于working mode,此时工作电流在3~7mA之间。如果需要省功耗需要进入低功耗模式。

低功耗模式(low power mode)又称sleep mode,包括3种:suspend mode、deepsleep mode和deepsleep retention mode。

Module suspend deepsleep retention deepsleep
Sram 100% keep 100% keep 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 init和rf_set_power_level_index重新配置RF。

b) 控制Dfifo状态的digital register。对应drivers/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长睡眠维持连接),增加了一种sleep mode 3:deepsleep with Sram retention mode,简称deepsleep retention(或deep retention)。

Deepsleep retention mode也是一种deepsleep,MCU绝大部分的硬件模块都断电,PM硬件模块维持工作。功耗是在deepsleep mode基础上增加retention Sram消耗的电,电流在2uA左右。deepsleep mode wake_up时,MCU将重新启动,程序会重新开始进行初始化。

deepsleep retention mode和deepsleep mode 在register状态保存方面表现一致,几乎全部掉电。deepsleep retention mode跟deepsleep mode相比,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控制的状态。8208在suspend期间可以用gpio output来控制一些外围设备,但被切换为deepsleep retention mode后,gpio output状态失效,无法在sleep期间准确的控制外围设备。此时可以使用GPIO模拟上下拉电阻的状态来代替实现:上拉10K代替gpio output high,下拉100K代替gpio output low。

b) PM模块特殊的不掉电analog regsiter

drivers/lib/include/pm.h文件中的DEEP_ANA_REG,如下code所示:

#define DEEP_ANA_REG0                       0x3a //initial value =0x00
#define DEEP_ANA_REG1                       0x3b //initial value =0x00,system used, user can not use
#define DEEP_ANA_REG2                       0x3c //initial value =0x0f

需要注意的是,客户不允许使用ana_3b,该模拟寄存器留给底层stack使用,如果应用层代码有用到该寄存器,需要修改。因为不掉电模拟寄存器数量比较少,建议客户使用其每一个bit指示不同的状态位信息。

如下几组不掉电模拟寄存器可能会因为错误的GPIO唤醒而丢掉信息,比如GPIO_PAD高电平唤醒deepsleep,但是在调用cpu_sleep_wakeup函数前gpio已经为高电平,这就会导致错误的GPIO唤醒,那么这些模拟寄存器值将会丢失。

#define DEEP_ANA_REG6                       0x35 //initial value =0x20
#define DEEP_ANA_REG7                       0x36 //initial value =0x00
#define DEEP_ANA_REG8                       0x37 //initial value =0x00
#define DEEP_ANA_REG9                       0x38 //initial value =0x00
#define DEEP_ANA_REG10                      0x39 //initial value =0xff

使用者可以在这几个analog regsiter中保存一些重要的的信息,deepsleep/deepsleep retention wake_up后还可以读到之前存储的值。

低功耗唤醒源

8208 MCU的低功耗唤醒源示意图如下,suspend/deepsleep/deepsleep retention都可以被GPIO PAD和timer唤醒。该 BLE SDK中,只关注2种唤醒源,如下所示(注意code中PM_TIM_RECOVER_START和PM_TIM_RECOVER_END两个定义不是唤醒源):

typedef enum {
     //available wake-up source for customer
     PM_WAKEUP_PAD              = BIT(0),
     PM_WAKEUP_TIMER            = BIT(2),
}SleepWakeupSrc_TypeDef;

B80 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外所有的GPIO的高低电平都具有唤醒功能。

配置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,有以下3个选择,分别表示suspend mode、deepsleep mode、deepsleep retention 16K Sram。
typedef enum {
        SUSPEND_MODE                        = 0x00,
        DEEPSLEEP_MODE                      = 0x30,
        DEEPSLEEP_MODE_RET_SRAM_LOW16K      = 0x01,
}SleepMode_TypeDef;
  • 第二个参数wakeup_src:设置当前的suspend/deepsleep的唤醒源,参数只能是PM_WAKEUP_PAD、PM_WAKEUP_TIMER中的一个或者多个。如果wakeup_src为0,那么进入低功耗sleep mode后,无法被唤醒。

  • 第三个参数“wakeup_tick”:当wakeup_src中设置了PM_WAKEUP_TIMER时,需要设置wakeup_tick来决定timer在何时将MCU唤醒。如果没有设置PM_WAKEUP_TIMER唤醒,该参数无意义。

wakeup_tick的值是一个绝对值,按照本文档前面介绍的System Timer tick来设置,当System Timer tick的值达到这个设定的wakeup_tick后,sleep mode被唤醒。wakeup_tick的值需要根据当前的System Timer tick的值,加上由需要睡眠的时间换算成的绝对时间,才可以有效地控制睡眠时间。如果没有考虑当前的System Timer tick,直接对wakeup_tick进行设置,唤醒的时间点就无法控制。

由于wakeup_tick是绝对时间,必须在32bit的System Timer tick能表示的范围之内,所以这个API能表示的最大睡眠时间是有限的。目前的设计是最大睡眠时间为32bit能表示的最大System Timer tick对应时间的7/8。System Timer tick最大能表示大概268s,那么最长sleep时间时间为268*7/8=234 s,即下面delta_Tick不能超过234 s。

cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER, clock_time() + delta_tick);

返回值为当前sleep mode的唤醒源的集合,该返回值各bit对应表示的唤醒源为:

enum {
     WAKEUP_STATUS_PAD              = BIT(0),
     WAKEUP_STATUS_TIMER            = BIT(2),

     STATUS_GPIO_ERR_NO_ENTER_PM    = BIT(7),
};

(1) WAKEUP_STATUS_TIMER这个bit为1,说明当前sleep mode是被Timer唤醒。

(2) WAKEUP_STATUS_PAD这个bit为1,说明当前sleep mode是被GPIO PAD唤醒。

(3) WAKEUP_STATUS_TIMER和WAKEUP_STATUS_PAD同时为1时,表示Timer和GPIO PAD两个唤醒源同时生效了。

(4) STATUS_GPIO_ERR_NO_ENTER_PM是一个比较特殊的状态,表示当前发生了GPIO唤醒错误:比如当设置了某个GPIO PAD高电平唤醒,而在这个GPIO为高电平的时候尝试调用cpu_sleep_wakeup进入suspend,且设置了PM_WAKEUP_PAD唤醒源。此时会出现无法进入suspend,MCU立刻退出cpu_sleep_wakeup函数,给出返回值STATUS_GPIO_ERR_NO_ENTER_PM。

一般采用如下的形式来控制睡眠时间:

cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_TIMER,  clock_time() + delta_Tick);

delta_Tick是一个相对的时间(比如100* CLOCK_16M_SYS_TIMER_CLK_1MS),加上当前的clock_time()就变成了绝对时间。

举例说明cpu_sleep_wakeup的用法:

cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_PAD, 0);

程序执行该函数时进入suspend mode,只能被GPIO PAD唤醒。

cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_TIMER, clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1MS);

程序执行该函数时进入suspend mode,只能被Timer唤醒,唤醒时间为当前时间加上10 ms,所以suspend时间为10 ms。

cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_PAD | PM_WAKEUP_TIMER, 
clock_time() + 50* CLOCK_16M_SYS_TIMER_CLK_1MS);

程序执行该函数时进入suspend模式,可被GPIO PAD和Timer唤醒,Timer唤醒的时间设置为50ms。如果在50ms结束之前触发了GPIO 的唤醒动作,MCU会被GPIO PAD唤醒;如果50ms内无GPIO动作,MCU会被Timer唤醒。

cpu_sleep_wakeup (DEEPSLEEP_MODE, PM_WAKEUP_PAD, 0);

程序执行该函数时进入deepsleep mode,可被GPIO PAD唤醒。

cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K , PM_WAKEUP_TIMER, clock_time() + 8* CLOCK_16M_SYS_TIMER_CLK_1S);

程序执行该函数时进入deepsleep retention 32K Sram mode,可被Timer唤醒,唤醒时间为执行该函数的8s后。

cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K , PM_WAKEUP_PAD | PM_WAKEUP_TIMER,clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1S);

程序执行该函数时进入deepsleep retention 32K Sram mode,可被GPIO PAD和Timer唤醒,Timer唤醒时间为执行该函数的10s后。如果在10s结束之前触发了GPIO动作,MCU会被GPIO PAD唤醒;如果10s内无GPIO动作,MCU会被Timer唤醒。

低功耗唤醒后运行流程

当user调用API cpu_sleep_wakeup后,MCU进入sleep mode;当唤醒源触发MCU唤醒后,对于不同的sleep mode,MCU的软件运行流程不一致。

下面详细介绍suspend、deepsleep、deepsleep retention 3种sleep mode被唤醒后的MCU运行流程。请参考下图。

Sleep Mode Wakeup Work Flow

MCU上电(Power on)之后,各流程的介绍:

(1) 运行硬件bootloader(Run hardware bootloader)

MCU硬件上执行一些固定的动作,这些动作固化在硬件上,软件无法修改。

举几个例子说明一下这些动作,比如:读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端(b80 sdk的boot目录下的.s汇编程序)。

Software bootloader是为了给后面C语言程序的运行设置好内存环境,可以理解为整个内存的初始化。

(3) 系统初始化(System initialization)

System initialization对应main函数中cpu_wakeup_init到user_init之前各硬件模块初始化(包括cpu_wakeup_init、rf_drv_init、gpio_init、clock_init),设置各硬件模块的数字/模拟寄存器状态。

(4) 用户初始化(User initialization)

User initialization分为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流程分析:

(6) no sleep

如果没有sleep mode,MCU的运行流程为在while(1)中循环,反复执行“Operation Set A” ->“Operation Set B”。

(7) suspend

如果调用cpu_sleep_wakeup函数进入suspend mode,当suspend被唤醒后,相当于cpu_sleep_wakeup函数的正常退出,MCU运行到"Operation Set B”。

suspend是最干净的sleep mode,在suspend期间所有的Sram数据能保持不变,所有的数字/模拟寄存器状态也保持不变(只有几个特殊的例外);suspend唤醒后,程序接着原来的位置运行,几乎不需要考虑任何sram和寄存器状态的恢复。suspend的缺点是功耗偏高。

(8) deepsleep

如果调用cpu_sleep_wakeup函数进入deepsleep mode,当deepsleep被唤醒后,MCU会重新回到Run hardware bootloader。

可以看出,deepsleep wake_up跟 Power on的流程是几乎一致的,所有的软硬件初始化都得重新做。

MCU进入deepsleep后,所有的Sram和数字/模拟寄存器(只有几个模拟寄存器例外)都会掉电,所以功耗很低,MCU电流小于1uA。

(9) deepsleep retention

如果调用cpu_sleep_wakeup函数进入deepsleep retention mode,当deepsleep retention被唤醒后,MCU会重新回到Run software bootloader。

deepsleep retention是介于suspend和deepsleep之间的一种sleep mode。

suspend因为要保存所有的sram和寄存器状态而导致电流偏高;deepsleep retention不需要保存寄存器状态,Sram只保留前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调用和配置。

对于Idle state,SDK也不提供任何低功耗管理。由于此状态不涉及BLE RF任何动作(即blc_sdk_main_loop函数完全无效),user可以自行调用PM driver去做一些低功耗管理。 下面code为一个简单的demo:当Link Layer处于Idle state时,每个main_loop suspend 10ms。

void main_loop (void)
{
    ////////////////////// BLE entry ////////////////////////
    blc_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。

8208 的sleep mode分3种:suspend、deepsleep、deepsleep retetnion。

对于sleep mode中的suspend和deepsleep retention,user不需要调用API cpu_sleep_wakeup来实现。SDK根据Link Layer的状态和低功耗模式,在BLE协议栈部分加了一套低功耗管理机制(对应的代码在blc_sdk_main_loop中实现)。user只需要调用相应的API即可对低功耗进行配置。

对于deepsleep,BLE低功耗管理不包括对它的处理,所以user需要手动在应用层调用API cpu_sleep_wakeup来进入deepsleep。手动deepsleep mode的使用,可以参考SDK project “8208_ble_sample” blt_pm_proc函数中对deepsleep的处理。

下面开始对Advertising state和Connection state Slave role的低功耗管理做详细的介绍。

相关变量

BLE PM软件处理流程部分会出现很多变量,用户有必要了解这些变量。

BLE SDK中定义了以下结构体和变量,下面只列出结构体中部分变量(API介绍时需要用到的变量)。

typedef struct {
    u8      suspend_mask;
    u8      wakeup_src;
    u16     user_latency;
}st_ll_pm_t;

extern  u32     deepRet_advThresTick;
extern  u32     deepRet_connThresTick;
extern  u32     deepRet_earlyWakeupTick;

在文件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。

PM软件处理流程

低功耗管理的软件处理流程,下面将使用代码与伪代码相结合的方式来说明,目的是为了让user了解处理流程的所有逻辑细节。

blc_sdk_main_loop:

SDK中,blc_sdk_main_loop在一个while(1)的结构中被反复调用。

while(1)
{
    ////////////////////// BLE entry ////////////////////////
    blc_sdk_main_loop();
////////////////////// UI entry ////////////////////////
    //UI  task
////////////////////// user PM config ////////////////////////
    //blt_pm_proc();
}

blc_sdk_main_loop函数在while(1)中不断被执行,BLE低功耗管理的code在blc_sdk_main_loop函数中,所以低功耗管理的code也是一直在被执行。

下面是blc_sdk_main_loop函数中低功耗管理逻辑的实现。

int blc_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函数。

blt_brx_sleep:

blt_brx_sleep函数的逻辑实现如下所示。

void    blt_brx_sleep (void)
{
    if( (Link Layer state == Adv state)&& (bltPm. suspend_mask &SUSPEND_ADV) )
    {   //当前广播状态,允许进suspend
        T_wakeup = T_advertising + advInterval;
        BLT_EV_FLAG_SUSPEND_ENTER event callback execution
        T_sleep = T_wakeup  clock_time();
        if( bltPm. suspend_mask & DEEPSLEEP_RETENTION_ADV && T_sleep > bltPm.deepRet_advThresTick )
        {   //suspend is automatically switched to deepsleep retention 
            cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_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);

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的功耗都会更低。

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略大一点,但不能小于这个值。

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都是用下面函数即可完成。

void user_init(void);

对于使用了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 “8208_ble_sample” user_init_deepRetn函数的实现。

_attribute_ram_code_ void user_init_deepRetn(void)
{
    blc_ll_initBasicMCU();
    rf_set_power_level_index (MY_RF_POWER_INDEX);
    blc_ll_recoverDeepRetention();
    irq_enable();
}

前2句code是BLE初始化中必不可少的相关硬件寄存器的初始化;

blc_ll_recoverDeepRetention是对Link Layer相关软硬件状态的恢复,由stack底层处理。

最后irq_enable是初始化完成,打开系统中断。

以上几个初始化都属于固定写法,user不要去修改。

在SDK demo基础上,user initialization如果增加了其他功能,这些新增功能的initialization节省时间的原则是:对每一条initialization的code进行分析,判断出是确定需要retention回来重新初始化,还是硬件寄存器的操作。

  • 如果是retention回来需要重新初始化的全局变量,将相应的变量添加关键字“attribute_data_reload”定义到 “data_reload”段,就可以保证deepsleep retention wake_up后重新初始化,不需要该操作放到user_init_deepRetn函数中。

  • 如果是硬件寄存器的操作,那么必须放到user_init_deepRetn函数中,确保硬件状态的正确性。

deepsleep retention wake_up 后的T_userInit就是user_init_deepRetn函数的执行时间,SDK中也尽量将这部分的函数放到ram_code中以节省运行时间。

在deepsleep retention area空间足够的前提下,user也需要将增加的硬件初始化相关函数放到ram_code中。在B80 BLE SDK中我们提供了以下API用于将deepsleep retention wake_up后需要初始化的寄存器操作放到ram中,节省唤醒时间和平均功耗,当然刚API也会导致ram占用增加,user需要自行评估是否需要调用该API。

void blc_ll_initDeepsleepRetention_module(void)

(3) 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_data_reload + T_cs_2

T_cs_1和T_cs_2两个时间是固定的,user无法修改它们,不需要去关注。

T_cs_data_reload是Sram中“data_reload”段的初始化时间。“data_reload”段是retention醒来重新初始化的全局变量,它们的初始值存储在flash的“data_reload init value”区域上。“data_reload”段初始化的时间就是MCU从flash “data_reload init value”区域上的初值拷贝到Sram “data_reload”段的过程。对应的汇编code如下:

COPY_DATA_RELOAD:
    tloadr      r1, DATA_I+12
    tloadr      r2, DATA_I+16
    tloadr      r3, DATA_I+20
COPY_DATA_RELOAD_BEGIN:
    tcmp        r2, r3
    tjge        COPY_DATA_RELOAD_END
    tloadr      r0, [r1, #0]
    tstorer     r0, [r2, #0]
    tadd        r1, #4
    tadd        r2, #4
    tj          COPY_DATA_RELOAD_BEGIN

因为flash数据拷贝速度相对比较慢(给个参考:16 byte数据大概需要7us时间),如果“data_reload”段的数据较多,就会造成T_cs_data_reload时间偏大,最终导致T_init偏大。

SDK中“data_reload”段数据越少越好。user可以参考文档前面介绍的方法去查看list文件中”data_reload”段的大小。

(4) 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_CHN4_LOW依赖于app_config.h中“ DEBUG_GPIO_ENABLE”的打开。

_attribute_ram_code_ int main (void)    //must run in ramcode
{
    DBG_CHN4_LOW;   //debug
    ……
}

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

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才变得有意义。

latency_use的计算:

当conn_latency生效时,T_wakeup的值是由latency_use决定的。说明latency_use并不是直接等于conn_latency。

在latency_use计算中,涉及到一个user_latency,这个是user可以设置的值,调用的API及其源码为:

void bls_pm_setManualLatency(u16 latency)
{
    bltPm.user_latency = latency;
}

bltPm.user_latency这个变量的初值为0xFFFF。注意PM软件处理流程中blt_brx_sleep函数最后会强制将它再次复位为0xFFFF,说明API bls_pm_setManualLatency设置的user_latency只对最近一次sleep管用,每次不同的sleep都需要重新设置。

latency_use的计算过程如下。

首先计算system latency:

(1) 若当前连接参数中connection latency为0,system latency为0。

(2) 若当前连接参数中connection latency非0:

  • 若当前系统还有一些任务没有处理完,必须在下一个connection interval醒来收发包继续处理(比如还有数据没有发送完、收到master的数据还没处理完等等),system latency为0。

  • 若当前系统已经没有任务需要处理了,则system latency等于connection latency。但是有一个例外,如果收到了master的update map request或update connection parameter request且实际的更新时间点在(connection latency+1)个interval之前,则实际的system latency会强制MCU在实际更新时间点之前那个interval醒来,确保BLE时序的正确。

然后

latency_use = min(system latency, user_latency)

即latency_use取system latency和user_latency中的较小值。

以上逻辑可以看出:如果user调用API bls_pm_setManualLatency设置的user_latency比system latency小,user_latency将会作为最终的latency_use,否则system latency将作为最终的latency_use。

API bls_pm_getSystemWakeupTick

下面的API用于获取低功耗管理计算的suspend醒来的时间点(System Timer tick),即T_wakeup。

u32 bls_pm_getSystemWakeupTick(void);

从PM软件处理流程blt_brx_sleep函数中可以看到,T_wakeup的计算比较晚,已经很接近cpu_sleep_wakeup函数了,应用层只能在BLT_EV_FLAG_SUSPEND_ENTER事件回调函数里才能得到准确的T_wakeup。

下面以按键扫描的应用为例,说明BLT_EV_FLAG_SUSPEND_ENTER事件回调函数和bls_pm_getSystemWakeupTick的用法。

bls_app_registerEventCallback (BLT_EV_FLAG_SUSPEND_ENTER, &app_set_kb_wakeup);  

void app_set_kb_wakeup(u8 e, u8 *p, int n)
{
    #if (BLE_APP_PM_ENABLE)
    if( blc_ll_getCurrentState() == BLS_LINK_STATE_CONN
        && ((u32)(bls_pm_getSystemWakeupTick() - clock_time())) > 80 *CLOCK_16M_SYS_TIMER_CLK_1MS ){  //suspend time > 30ms.add gpio wakeup
        bls_pm_setWakeupSource(PM_WAKEUP_PAD);  //gpio CORE wakeup suspend
    }
    #endif
}

以上这个回调函数要实现的作用是防止按键丢失。

一个正常的人为机械按键动作大概会持续几百毫秒,按的快的时候也会有一两百毫秒。当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

由于8208的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 “8208_ble_sample”中,做了相应的处理。

在BLT_EV_FLAG_SUSPEND_ENTER中设置了只有当suspend时间超过某个时间时,才会开启GPIO PAD唤醒。

void app_set_kb_wakeup(u8 e, u8 *p, int n)
{
    #if (BLE_APP_PM_ENABLE)
    if( blc_ll_getCurrentState() == BLS_LINK_STATE_CONN
        && ((u32)(bls_pm_getSystemWakeupTick() - clock_time())) > 80 *CLOCK_16M_SYS_TIMER_CLK_1MS ){  //suspend time > 30ms.add gpio wakeup
        bls_pm_setWakeupSource(PM_WAKEUP_PAD);  //gpio CORE wakeup suspend
    }
    #endif
}

当按键没有释放时,通过手动设置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 “8208_ble_sample” 低功耗管理的参考code。下面做一些解释。

在main_loop的PM configuration部分添加blt_pm_proc函数,注意这个函数要放在main_loop的最后面,以保证运行时间上最接近blc_sdk_main_loop。因此blt_pm_proc函数里面低功耗管理的配置,需要根据UI entry部分各种任务的处理情况来做相应设置。

blt_pm_proc函数低功耗管理配置几个要点总结如下:

(1) 某些任务需要关闭sleep mode时,设置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) 8208工作电压的范围为1.8V~3.6V。当电压低于1.8V时,8208已经无法保证稳定的工作。

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请参考文档8208 Datasheet相关ADC章节,先对B80的ADC模块进行必要的了解。

低电检测的实现,结合SDK demo “8208_ble_sample”给出的实现来说明,参考文件vendor/common/battery_check.h和vendor/common/battery_check.c。

必须确保app_config.h文件中宏“BATT_CHECK_ENABLE”是被打开的,这个宏默认是关闭的,user使用低电检测功能时需要注意。

#define BATT_CHECK_ENABLE                   1

低电检测的注意事项

低电检测是一个基本的ADC采样任务,在实现ADC采样电源电压时,有一些需要注意的问题,说明如下:

(1) 建议使用VBAT输入通道

8208的ADC输入通道上支持在“VCC/VBAT” 输入通道上对电源电压进行ADC采样,对应下面变量ADC_InputPchTypeDef中最后一个“VBAT”。

可用的GPIO输入通道为PB0~PB7、PC4、PA3对应的input channel。

/*ADC analog positive input channel selection enum*/
typedef enum {
……
    B0P,
    B1P,
    B2P,
    B3P,
    B4P,
    B5P,
    B6P,
    B7P,
    C4P,
    A3P,
……
    VBAT,
}ADC_InputPchTypeDef;

目前“8208_ble_sample”选择的是VBAT输入通道采样。

(2) 只能使用差分模式

虽然8208 ADC input mode同时支持单端模式(Single Ended Mode)和差分模式(Differential Mode),但由于某些特定的原因,Telink规定:只能使用差分模式,单端模式不允许使用。

差分模式的input channel分为positive input channel(正端输入通道)和negative input channel(负端输入通道), 被测量的电压值为 positive input channel电压减去negative input channel电压得到的电压差。

如果ADC采样的input channel只有1个,使用差分模式时,将当前input channel设置为positive input channel,将GND设为negative input channel。这样二者的电压差和positive input channel电压相等。

SDK中低压检测使用了差分模式,在adc_init()函数中调用adc_set_input_mode_chn_misc函数进行差分模式选择,code如下。

void adc_init(void){
...
    adc_set_input_mode_chn_misc(DIFFERENTIAL_MODE);
...
}

(3) 必须使用Dfifo模式获得ADC 采样值

对于8208,Telink规定:只能使用Dfifo模式来实现ADC采样值的读取。可参考dirver中如下函数的实现。

unsigned int adc_sample_and_get_result(void);

(4) 不同的ADC任务需要切换

参考《8208 Datasheet》可知,ADC 状态机仅有Misc channel。

(5) 低电检测初始化

参考app_battery_power_check函数里的初始化实现。

ADC初始化的顺序必须满足下面的流程:先power off(掉电)sar adc,然后配置其他参数,最后power on(上电)sar adc。所有ADC采样的初始化都必须遵循这个流程。

adc_init();
adc_vbat_channel_init();
adc_power_on_sar_adc(1);

Sar adc power on与power off之前的配置,user尽量不要去修改,使用这些默认的设置就行。

app_battery_power_check初始化函数在app_battery_power_check中调用的code为:

if(!adc_hw_initialized){
        adc_hw_initialized = 1;
        adc_init();
        adc_vbat_channel_init();
        adc_power_on_sar_adc(1);
    }

这里使用了一个变量adc_hw_initialized,只有该变量为0时调用一次初始化,并将其置1;该变量为1时不再初始化。adc_hw_initialized在下面 API中也会被操作。

void battery_set_detect_enable (int en)
{
    lowBattDet_enable = en;
    if(!en){
        adc_hw_initialized = 0;   //need initialized again
    }
}

使用了adc_hw_initialized的设计可以实现的功能有:

a. 与其他ADC任务(“ADC other task”)的切换

先不考虑sleep mode (suspend/deepsleep retention)的影响,只分析低电检测与其他ADC任务的切换。

因为需要考虑低电检测与其他ADC任务的切换使用,可能出现adc init被多次执行,所以不能写到user initialization中,必须在main_loop里实现。

第一次执行app_battery_power_check函数时,adc 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_reload段”上的变量,不能定义到“data”段或“bss”段上。定义在“data_reload段”上可以保证每次deepsleep retention wake_up后在执行software bootloader(即cstartup_xxx.S)时这个变量会被重新初始化为0;而sleep wake_up后这个变量可以保持不变。

adc init函数里面配置的register的共同特征是:在suspend mode下不掉电,可以保存状态;在deepsleep retention mode下会掉电。

如果MCU进入suspend mode,醒来后再次执行app_battery_power_check时,adc_hw_initialized的值和suspend之前一致,不需要重新执行adc init函数。

如果MCU进入deepsleep retention mode,醒来后adc_hw_initialized为0,必须重新执行adc init,ADC相关的register状态需要被重新配置。

adc init函数中设定register的状态可以在suspend期间保持不掉电。

参考文档“低功耗管理”部分对suspend mode的说明可知,Dfifo相关的寄存器在suspend mode会掉电,所以以下两句code没有放到adc init函数中,而是在app_battery_power_check函数中,确保每次低电检测前都重新设置。

  adc_config_misc_channel_buf((u16 *)adc_dat_buf,ADC_SAMPLE_NUM<<2);  
  dfifo_enable_dfifo2();

(6) 低电检测处理

在main_loop中,调用app_battery_power_check函数实现低电检测的处理,相关code如下:

u8      lowBattDet_enable = 1;
_attribute_data_reload_ u8      adc_hw_initialized = 0;   //note: can not be retention variable
void battery_set_detect_enable (int en)
{
    lowBattDet_enable = en;

    if(!en){
        adc_hw_initialized = 0;   //need initialized again
    }
}
int battery_get_detect_enable (void)
{
    return lowBattDet_enable;
}
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
    lowBattDet_tick = clock_time();
    app_battery_power_check(VBAT_ALRAM_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_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

(7) 低压报警

app_battery_power_check的参数alram_vol_mv指定低电检测的报警电压,单位为mV。根据前文介绍,SDK中默认设置为2000mV。在main_loop的低压检测中,当电源电压低于2000mV时,进入低压范围。

低压报警的处理demo code如下所示。低压后必须shutdown MCU,不能再进行其他工作。

“8208_ble_sample”使用进入deepsleep的方式来实现shutdown MCU,并且设置了按键可以唤醒。

低压报警的处理,除了必须shutdown外,user可以修改其他的报警行为。

下面code中,使用LED灯做了3次快闪,告知产品使用者需要充电或更换电池。

u8 battery_check_returnVaule = 0;
if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
    battery_check_returnVaule = app_battery_power_check(VBAT_ALRAM_THRES_MV + 200);  //2.2 V
}
else{
    battery_check_returnVaule = app_battery_power_check(VBAT_ALRAM_THRES_MV);  //2.0 V
}
if(battery_check_returnVaule){
    analog_write(USED_DEEP_ANA_REG,  analog_read(USED_DEEP_ANA_REG)&(~LOW_BATT_FLG));  //clr
}
else{
    #if (UI_LED_ENABLE)  //led indicate
        for(int k=0;k<3;k++){
            gpio_write(GPIO_LED_BLUE, LED_ON_LEVAL);
            sleep_us(200000);
            gpio_write(GPIO_LED_BLUE, !LED_ON_LEVAL);
            sleep_us(200000);
        }
    #endif
    analog_write(USED_DEEP_ANA_REG,  analog_read(USED_DEEP_ANA_REG) | LOW_BATT_FLG);  //mark
    GPIO_WAKEUP_FEATURE_LOW;

    cpu_set_gpio_wakeup (GPIO_WAKEUP_FEATURE, Level_High, 1);  //drive pin pad high wakeup deepsleep

    cpu_sleep_wakeup(DEEPSLEEP_MODE, PM_WAKEUP_PAD, 0);  //deepsleep
}

“8208_ble_sample”被shutdown后,进入可被唤醒的deepsleep mode。此时如果发生按键唤醒,SDK会在user initialization的时候先快速做一次低电检测,而不是等到main_loop中检测。这样处理的原因是为了避免应用上的错误,举例说明如下:

如果低电报警时LED闪烁已经提示了产品使用者,然后进入deepsleep又被唤醒,从main_loop的处理来看,需要至少500ms的时间才会去做低电检测。在500ms之前,slave的广播包已经发很久了,很可能会跟master已经连接上了。这样的话,就出现已经低电报警的设备又继续工作的bug了。

因为这个原因,SDK必须在user initialization的时候就提前做低电检测,必须在这一步就阻止发生上面的情况。所以在user initialization的时候,添加低电检测:

if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
            battery_check_returnVaule = app_battery_power_check(VBAT_ALRAM_THRES_MV + 200);  //2.2 V
        }
        else{
            battery_check_returnVaule = app_battery_power_check(VBAT_ALRAM_THRES_MV);  //2.0 V
        }

根据USED_DEEP_ANA_REG模拟寄存器的值可以判断是否低电报警shutdown被唤醒的情况,此时进行快速低电检测,并且将之前的2000mV报警电压提高到2200mV(称为恢复电压)。提高200mV的原因是:

低压检测会有一些误差,无法保证测量结果的准确性和一致性。比如误差在20mV,可能第一次检测到的电压是1990mV进入shutdown模式,然后唤醒后在user initialization的时候再次检测到的电压值是2005mV。如果还是以2000mV为报警电压的话,还是无法阻止上面描述的bug。

所以需要在shutdown模式唤醒后的快速低电检测时,将报警电压稍微调高一些,调高的幅度比低电检测的最大误差稍大。

只有当某次低电检测发现电压低于2000mV进入shutdown模式后,才会出现恢复电压2200mV,所以user不用担心这个2200mV会对实际电压2V~2.2V的产品误报低压。产品使用者看到低压报警指示后,进行充电或更换电池后,满足恢复电压的要求,产品恢复正常使用。

OTA

为了实现8208 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。

8208支持Flash多地址启动:除了Flash的首地址0x00000,128K Flash还支持从Flash高地址0x10000(64K)读取firmware运行,512K Flash还支持从Flash高地址0x10000(64K)、0x20000(128K)、0x40000(256K)读取firmware运行。本文档以高地址0x20000为例来介绍OTA。

Flash架构设计和OTA流程

FLASH存储架构

使用启动地址0x20000时,SDK编译出来的firmware size应不大于128K,即flash的0~0x20000之间的区域存储firmware;如果超过128K必须使用启动地位0和0x40000交替升级,此时最大firmware size不得超过240K,这是由于0x7C000 ~ 0x7FFFF需要存储一些SDK相关的信息,详细可以参考Flash章节的介绍。另外,对于128K Flash,使用启动地址为0和0x10000交替OTA升级,其firmware size不得超过48K。

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,(4)代表第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和0x20020的值和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版本号。

注意:

获取版本号需要user自行实现。

(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 reboot,新的firmware生效。

在整个OTA更新过程中,slave会不断检查是否有错包和丢包,同时也会不断检查是否超时(OTA开始的时候启动一个计时),一旦有错包、丢包或超时,slave会认为更新失败,程序reboot,使用之前的firmware。

以上流程slave端相关操作在SDK上已经实现,user不需要添加任何东西,master端需要额外的程序设计,后面会详细介绍。

修改Firmware size和boot address

API blc_ota_setFirmwareSizeAndBootAddress同时支持修改启动地址和最大firmware size。这个启动地址指的是OTA设计中除了0地址外另一个存储New_firmware的地址(只能是0x10000、0x20000或0x40000)。

Flash size Firmware_Boot_address Firmware size (max)/K
128 0x10000 48
512 0x10000 64
512 0x20000 128
512 0x40000 240

SDK中默认的最大firmware size为240K,对应的启动地址为0x00000和0x40000。user可以调用API blc_ota_setFirmwareSizeAndBootAddress来进行设置最大 firmware size:

ble_sts_t blc_ota_setFirmwareSizeAndBootAddress(int firmware_size_k, multi_boot_addr_e new_fw_addr);

firmware_size_k的设置一定要4K byte对齐,比如size为97K时需要设为100K。

参数multi_boot_addr_e表示可供选择的启动地址,共有三种:

typedef enum{
    MULTI_BOOT_ADDR_0x10000     = 0x10000,  //64 K
    MULTI_BOOT_ADDR_0x20000     = 0x20000,  //128 K
    MULTI_BOOT_ADDR_0x40000     = 0x40000,  //256 K
}multi_boot_addr_e;

这个API只能在main函数中cpu_wakeup_init之前调用,否则无效。原因是cpu_wakeup_init函数中需要根据firmware_size和boot_addr的值做一些设置。

如果固件不需要这么大的空间,例如firmware size不超过60kB,则只使用两个128kB空间(0x00000 ~ 0x20000, 0x20000 ~ 0x40000)的一部分。要使用冗余空间作为数据存储区域,可以执行以下设置。

blc_ota_setFirmwareSizeAndBootAddress(60, 0x20000);

通过上面的配置,两个60kB Flash区域0x00000 ~ 0x0F000和0x20000 ~ 0x2F000可以用作固件的存储空间,而两个68kB的Flash区域0x0F000 ~ 0x20000和0x2F000 ~ 0x40000可以作为用户数据的存储空间。

128kB Flash OTA中的情况与512kB基本相同,只是将偏移量需配置为0x10000。 1)对于128kB Flash, SDK的默认最大支持的固件大小为48kB。 2)对于128kB Flash,通过修改SDK配置,如下图,固件的最大大小可以达到56kB,但不会有额外的Flash空间来存储用户数据。

128kB Flash存储结构

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,
    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_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

Master端通过L2CAP层的Write Command向slave发命令和数据。

OTA_CMD组成:

OTA的CMD的PDU如下:

OTA Command Payload
Opcode (2 octet) invalid data

Opcode:

Opcode Name
0xFF00 CMD_OTA_VERSION
0xFF01 CMD_OTA_START
0xFF02 CMD_OTA_END

(1) CMD_OTA_VERSION

该命令为获得slave当前firmware版本号的命令,user可以选择使用。在使用该命令时,可通过slave端预留的回调函数来完成firmware版本号的传递。

void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);

server端在收到CMD_OTA_VERSION命令时会触发该回调函数。

(2) CMD_OTA_START

该命令为OTA升级开始命令,master发这个命令给slave,用来正式启动OTA更新。

(3) CMD_OTA_END

该命令为结束命令,当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
  • Adr_index_max: 最大的adr_index值

  • Adr_index_max_xor: Adr_index_max的异或值,供校验使用

  • Reserved: 保留供以后功能扩展使用

OTA_Data介绍:

- OTA PDU -
Adr_Index (2 octets) Data(16 octets) CRC (2 octets)

注意:

OTA PDU长度固定大小为16octets

OTA_PDU Format:

前两个byte的范围在firmware_size_k之内时,表示一个OTA数据。由于firmware size不超过128K(0x20000),OTA data packet中每次传送16 byte的firmware数据,使用的adr_index为实际firmware地址除以16的值。adr_index=0,表示OTA数据是firmware地址0x0 ~ 0xF的值;adr_index=1,表示OTA数据是firmware地址0x10 ~ 0x1F的值。最后两个byte是将前面的Adr_Index和Data进行一个CRC_16计算得到第一个CRC的值,slave收到OTA data后,会进行同样的CRC计算,只有两者计算的CRC吻合时,才认为这是一个有效数据。

RF Transfer处理方法

基于BLE link layer RF数据自动ack确保所有数据包不丢的前提,OTA的数据 transform不检查每一个OTA数据是否被ack,即master通过write command发一个ota 数据后,不在软件上检查对方是否有ack信息回复,只要master端硬件TX buffer缓存的待发送数据未达到一定数量,直接将下一笔数据丢进TX buffer。

下面将对OTA具体实现流程进行介绍,阐述整个RF Transform中Salve和Master的交互过程。

OTA具体实现:

master端OTA相关的操作为:

(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可以自行发挥。目前的B80 BLE SDK中并没有实现版本号的传送。user可以使用write cmd的形式通过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内容为0x0000A164,所以大小为0xa164 = 41316Bytes,从0x0000 到 0xa164。

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 0x58 0x80 ....省略12个bytes..... 0x88 0x00 0xZW 0xXY

第二笔数据:

0x01 0x00 0xE6 0x80 ....省略12个bytes..... 0x00 0x00 0xJK 0xHI

第三笔数据:

0x02 0x00 0x0C 0x64 ....省略12个bytes..... 0xC0 0x06 0xNO 0xLM

........

倒数第二笔数据:

0x15 0x0a 0x06 0x00 ....省略12个bytes..... 0xff 0xff 0xST 0xPQ

最后一笔数据:

0x16 0x0a 0xad 0xcb 0x7a 0xde 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xWX 0xUV

12个0xff为补齐的数据。

0xad 0xcb 0x7a 0xde为第3个~第6个,它是整个firmware bin的CRC_32校验结果。slave在OTA升级过程中会同步计算接收到的整个bin的CRC_32校验值,在收到最后一包数据包时会将该CRC_32校验值与0xad 0xcb 0x7a 0xde进行比较。

0x16 ~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为0x0a16,其取反值为0xf5e9,最终的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还没有完成,就认为超时失败。实际user最后需要根据自己的firmware大小(越大越耗时)和master端BLE数据带宽(太窄的话会影响OTA速度)来修改这个默认的30s,SDK提供修改的接口为:

ble_sts_t blc_ota_setOtaProcessTimeout(int timeout_second);

该接口支持的timeout时间范围为4 ~ 1000s,单位为s。

(3) 收到有效的OTA数据:

这个范围的值表示具体的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也失败。

(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状态的回调函数:

slave端一旦启动OTA,在OTA成功时会将MCU reboot:

若成功,会在reboot前设置flag告诉MCU再次启动后运行New_firmware;

若OTA失败,会将错误的新程序擦掉后重新启动,还是运行Old_firmware。

在MCU reboot前,根据user是否注册了OTA状态回调函数,来决定是否触发该函数。

以下是相关code:

enum{
    OTA_SUCCESS             = 0,            //success
    OTA_PACKET_LOSS,                        //lost one or more OTA PDU
    OTA_DATA_CRC_ERR,                       //packet PDU CRC err
    OTA_WRITE_FLASH_ERR,                    //write OTA data to flash ERR
    OTA_DATA_UNCOMPLETE,                    //lost last one or more OTA PDU
    OTA_TIMEOUT,                            //OTA flow total timeout
    OTA_FW_CHECK_ERR,                       //firmware CRC check error
    OTA_STEP_ERR,
};

typedef void (*ota_resIndicateCb_t)(int result);

void blc_ota_registerOtaResultIndicationCb (ota_resIndicateCb_t cb);

设置了回调函数后,回调函数的参数result的6个值如上面enum描述,第一个是OTA成功,其余是不同的失败原因。

由于回调函数执行完后,一定会触发MCU reboot,实际代码中看到这个状态指示的结果时,并没有太多功能性用途。OTA升级成功或失败均会触发该回调函数,user可以通过该函数的结果返回参数来进行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) OTA PDU CRC16校验

参考本文前面介绍,在LinkLayer数据保护的基础上,OTA协议上再增加一个CRC16校验,使得数据传输更加安全。

(2) 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都有效发出。

(3) OTA期间需要关闭低功耗

为了保证OTA过程不受低功耗的影响,OTA开始后需要调用以下API关闭低功耗。

bls_pm_setSuspendMask(SUSPEND_DISABLE)

Flash

Flash地址分配

FLASH存储信息以一个sector的大小(4K byte)为基本的单位,因为flash的擦除是以sector为单位的(擦除函数为flash_erase_sector),理论上同一种类的信息需要存储在一个sector里面,不同种类的信息需要在不同的sector(防止擦除信息时将其他类的信息误擦除)。所以建议user在使用FLASH存储定制信息时遵循“不同类信息放在不同sector”的原则。系统相关信息 (Customed Value,MAC address,Pair&Sec Info) 默认位置会自适应的根据flash真实大小,偏移到flash的较为偏后的位置。

如下图是128K/512K Flash中各种信息的地址分配,以默认OTA Firmware最大size不超过128K为例来说明,如果用户修改了OTA Firmware size,则相应地址发生变化,用户可以自行分析。

128K/512K FLASH地址分配

上图所示,其中所有的地址分配都给user提供了对应的修改接口,user可以根据自己需要去规划地址分配。user可以根据相对应的Flash size定义以下宏定义。

#define     FLASH_SIZE_OPTION_128K                              0x20000
#define     FLASH_SIZE_OPTION_512K                              0x80000

#define     FLASH_SIZE_OPTION                       FLASH_SIZE_OPTION_128K

(1) 当使用512K Flash时0x7F000~0x80000这个sector存储MAC地址,当使用128K Flash时为0x1F000~0x20000,实际上MAC address 6个bytes存储在0x7F000\~0x7F005(0x1F000\~0x1F005),当使用512K时,高byte的地址存放在0x7F005,低byte地址存放在0x7F000。比如FLASH 0x7F000到0x7F005的内容依次为0x11 0x22 0x33 0x44 0x55 0x66,那么MAC address为0x665544332211。泰凌的量产治具系统会将实际产品的MAC 地址烧写到0x7F000(0x1F000)地址,和SDK相对应。如果user需要修改这个地址,请确保治具系统烧写的地址也作了相应的修改。SDK中在user_init函数里会从FLASH的CFG_ADR_MAC读取MAC 地址,这个宏在stack/ble/blt_config.h里面修改即可。

/**************************** 512 K Flash *****************************/
#ifndef     CFG_ADR_MAC_512K_FLASH
#define     CFG_ADR_MAC_512K_FLASH                              0x7F000
#endif
/**************************** 128 K Flash *******************************/
#ifndef     CFG_ADR_MAC_128K_FLASH
#define     CFG_ADR_MAC_128K_FLASH                              0x1F000
#endif

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

校准区信息的细节,用户可参考《Telink_IC_Flash Customize Address_Spec》。以相对校准区起始地址的偏移地址对校准信息进行说明。比如偏移地址0x00指的是0x7E000或0x1E000。

a) 偏移地址0x00,1 byte,存储BLE RF频偏校准值。

b) 偏移地址0x40,4 byte,存储TP值校准。B80 IC不需要TP校准,这里忽略。

c) 偏移地址0xC0,存储ADC Vref校准值。

d) 偏移地址0x180,16 byte,存储Firmware数字签名,用于防止客户Firmware被盗用。

e) 其他,保留。

(3) 512K Flash 0x7C000 ~ 0x7DFFF这两个sector被BLE协议栈系统占用,对于128K Flash为0x1C000~0x1DFFF,用来存储配对和加密信息。user也可以修改这两个sector的位置,size固定为两个sector 8K,无法修改,可以调用下面函数修改配对加密信息存储的起始地址,如果不调用该函数,默认存储区域为0x74000:

void    bls_smp_configpairingSecurityInfoStorageAddr (int addr);

(4) 对于512K Flash0x00000 ~ 0x40000这段区域为程序空间,128K Flash时为0x00000~0x0FFFF。以512K Flash举例,0x00000 ~ 0x1FFFF共128K为Firmware存储空间;0x20000 ~ 0x3FFFF 128K为OTA更新时存储新Firmware的空间,即支持最大Firmware空间为128K。

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

Flash操作

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

(1) Flash读写操作

flash读写操作使用flash_read_page和flash_write_page函数指针来实现,默认指向flash_read_data和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读写函数可以通过以下API更改。

void flash_change_rw_func(flash_hander_t read, flash_hander_t write);

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,这个函数操作的地址大小最大为256 byte,不能跨越两个不同page范围。

当被操作的地址是一个page的首地址时,最大地址为256 byte,flash_write_page(0x12000, 256 , data)操作正确,而flash_write_page(0x12000, 257 , data)错误,因为最后一个地址不属于0x12000所在的page了,写操作会失败。

当被操作的地址不是一个page的首地址时,更要注意不能出现跨page的问题,如flash_write_page(0x120f0, 20 , data)就错了,前16个地址在0x12000这个page,而后4个地址在0x12100这个page。

flash_read_page不存在上面说的跨page的问题,可以一次性读取超过256 byte的数据。

注意:

  • user在使用flash_write_page函数时,最多只能一次写16个byte,多了会导致BLE中断异常。
  • 该限制的原理,请结合“Flash API对BLE timing的影响”部分的介绍。

(2) flash擦除操作

使用flash_erase_sector函数来擦除flash。

void  flash_erase_sector(u32 addr);

一个sector为4096 byte,如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里函数。

static inline void blc_app_loadCustomizedParameters(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问题:

void user_init_normal(void)
{

    ……

    #if (BATT_CHECK_ENABLE) 
        u8 battery_check_returnVaule = 0;
        if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
            battery_check_returnVaule = app_battery_power_check(VBAT_ALRAM_THRES_MV + 200);
        }
        else{
            battery_check_returnVaule = app_battery_power_check(VBAT_ALRAM_THRES_MV);
        }
        if(battery_check_returnVaule){
            ……
        }
        else{
            ……
        }
    #endif

    ……

}

Step 2:在main_loop中,每隔一定500ms需要进行一次低压检测:

if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
    lowBattDet_tick = clock_time();
    u8 battery_check_returnVaule;
    if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
        battery_check_returnVaule=app_battery_power_check(VBAT_ALRAM_THRES_MV + 200);
    }
    else{
        battery_check_returnVaule=app_battery_power_check(VBAT_ALRAM_THRES_MV);
    }
    if(battery_check_returnVaule){
        ……
    }
    else{
        ……
    }
}

考虑mcu的工作电压及flash的工作电压情况,因此Demo设置在2.0V以下芯片直接进入deepsleep,并且一旦芯片检测过低于2.0V,需要等到电压升至2.2V,芯片才会恢复正常运行状况。这么设计考虑以下几点:

  • 2.0V时当有其他模块被操作可能拉低电压造成flash无法正常工作,因此在2.0V以下需要进入deepsleep,保证芯片不再运行相关模块;

  • 当有低电压情况时,需恢复至2.2V才能使其他功能正常,这是为了保证电源电压确认在被充电且已有一定电量,此时开始恢复功能可以较为安全。

以上是SDK Demo中的定时检测电压及管理方式,用户可以参考来进行设计。

注意:

关于flash低压保护,以上出现的阈值,只是参考值。客户要根据实际情况评估修改阈值,比如单层板、电源波动大等因素,都要酌情提高安全阈值。

Flash lock保护

除了上述的定时电压检测与管理方案,强烈建议客户做Flash的擦写保护。这是由于在某些情况下,即便做了低压检测结果为安全,但在检测之后应用层各个模块运行也小概率风险造成Flash电源电压的拉低,而导致Flash真正操作时其电源电压不满足条件而产生Flash的内容被篡改。因此建议客户在程序启动后便进行Flash的擦写保护,这样即使有误操作产生,Flash的内容也会更有保障。

一般建议客户只对程序部分写保护(Flash前部分),这样其余的Flash地址仍可以用于用户层的数据存储。这里以SDK Sample工程为例讲述如何计算要保护大小与保护的方法。

初始化写保护:

(1) 计算保护大小:在初始化前,计算要写保护的flash地址大小。

(2) 调用flash_read_mid判断flash类型,根据结果调用相关函数,并根据要保护大小传入对应参数。mid值对应的相关函数可以在drivers/flash目录下找到。

OTA过程中的保护操作:

在OTA中,由于需要对flash进行擦写操作,因此如果上电有写保护的操作,在OTA过程中需要将其解锁保护。可以在OTA_START回调中进行flash解锁保护,步骤如下:

Step 1:首先在初始化函数中如以下方法注册回调函数;

blc_ota_registerOtaStartCmdCb (&flash_ota_start);

Step 2:在回调函数中,根据之前上电拿到的flash类型,调用对应函数进行解锁保护:

void flash_ota_start(void)
{
switch(flash_lock_mid)
{
case mid值:
    对应的unlock函数;
    break;

        
}
}

由于OTA结束后,无论成功或者失败,都会重新运行程序,因此在程序开始,由上一节将的flash_lock方法再次将程序写保护,形成闭环保证应用的安全性。

内置Flash介绍

Flash访问时序对BLE时序的影响

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”段。这个方法也不太好。受限于8208芯片的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操作

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时,会出现linlayer出错的风险,概率非常低。

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中的Advertiisng 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的种类相关的。本节结合8208已经支持的内置Flash详细介绍。

GD Flash:

GD Flash属于ETOX工艺。

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冲突的风险。

按键扫描

Telink提供了一套基于行列式扫描的keyscan架构,用于按键扫描处理,user可以直接使用这部分的code,也可以自己去实现。

键盘矩阵

如下图所示,这是一个2*2的Key matrix(键盘矩阵)。Row0,Row1是2个drive pin(驱动管脚),用来输出驱动电平;CoL0,CoL1是2个scan pin(扫描管脚),用来扫描当前列上是否有按键被按下。

行列式键盘结构

Telink EVK板上为2*2键盘矩阵。在实际产品应用中可能需要更多的按键,请用户自行添加设计,下面以Telink提供板载2*2矩阵键盘为例来进行说明。结合上图,对app_config.h中keyscan相关的配置进行详细说明如下。

根据实际的硬件电路,EVK板上Row0,Row1为GPIO_PF0, GPIO_PF1。CoL0,CoL1为GPIO_PA0, GPIO_PD4。

定义drive pin数组和scan pin数组:

#define KB_DRIVE_PINS   {GPIO_PF0, GPIO_PF1}
#define KB_SCAN_PINS    {GPIO_PA0, GPIO_PD4}

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
//drive pin need 100K pulldown
#define PULL_WAKEUP_SRC_PF0     MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PF1     MATRIX_ROW_PULL 
//scan  pin need 10K pullup
#define PULL_WAKEUP_SRC_PA0     MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PD4     MATRIX_COL_PULL

由于在gpio_init时将ie的状态会默认设为0,scan pin需要读电平,打开ie:

//drive pin open input to read gpio wakeup level
#define PF0_INPUT_ENABLE        1
#define PF1_INPUT_ENABLE        1

当MCU进入sleep mode时,需要设置PAD GPIO唤醒。设置drive pin高电平唤醒,按下按键时,drive pin读到100K和10K的分压电平,为10/11 VCC的高电平。需要打开drive pin的ie读取其电平状态:

//scan pin open input to read gpio level
#define PA0_INPUT_ENABLE        1
#define PD4_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   {   {CR_VOL_UP,     VK_1},   \
                                {CR_VOL_DN,     VK_2}, }

这个keymap的格式和2*2矩阵结构一致,可以对应设置按键按下后的keycode,如按下Row0和CoL0两条线交叉的按键,出来的keycode为CR_VOL_UP。

在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,ROW1逐行输出有效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,CoL1,根据之前通扫的scan_pin_need可以知道哪些列上能够读到有效电平,此时只读取已经被标记的列即可。

二是每一行drive时,需要20us左右的等待稳定时间,做了一个缓冲处理,把20us的等待时间转化到执行code中,节省了这个时间。具体怎么实现不介绍,请user自行理解。

最终的矩阵按键状态使用u32 pressed_matrix[5](可看出最多支持40列)来存储,pressed_matrix[0]的bit0~bit2标记Row0上CoL0 ~ CoL2是否有按键,......,pressed_matrix[4]的bit0~bit2标记Row4上CoL0 ~ CoL2是否有按键。

(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自行理解。

软件定时器(Software Timer)

为了方便user一些简单的定时器任务,Telink BLE SDK提供了blt software timer demo,并且全部源码提供。user可以在理解了该timer的设计思路后直接使用,也可以自己做一些修改设计。

源代码全部在vendor/common/blt_soft_timer.c和blt_soft_timer.h文件中,若需要使用,先在feature_config.h里将宏FEATURE_TEST_MODE改为TEST_USER_BLT_SOFT_TIMER:

#define FEATURE_TEST_MODE                           TEST_USER_BLT_SOFT_TIMER

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任务。

void main_loop (void)
{
    blt_soft_timer_process(MAINLOOP_ENTRY);
    blc_sdk_main_loop();
    #if (UI_KEYBOARD_ENABLE)
        proc_keyboard (0, 0, 0);
    #endif
    blt_pm_proc();
}

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任务的响应时间距离现在只剩3秒(3s可以再改大一些)不到,则将该时间设为应用层提前唤醒的时间,否则关闭应用层提前唤醒。

if( (u32)(blt_timer.timer[0].t - now) < 3000 *  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返回值三种处理方法为:

(1) 返回值小于0,则该任务执行后被自动删除。可以使用这个特性来控制定时器执行的次数。

(2) 返回0,则一直使用之前的interval_us来定时。

(3) 返回值大于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中feature_soft_timer。

int gpio_test0(void)
{
    DBG_CHN3_TOGGLE;  
    return 0;
}
static u8 timer_change_flg = 0;
int gpio_test1(void)
{
    DBG_CHN4_TOGGLE;  
    timer_change_flg = !timer_change_flg;
    if(timer_change_flg){
        return 7000;
    }
    else{
        return 17000;
    }
}
int gpio_test2(void)
{
    DBG_CHN5_TOGGLE;        
    if(clock_time_exceed(0, 5000000)){
        //return  -1;
        blt_soft_timer_delete(&gpio_test2);
    }
    return 0;
}
int gpio_test3(void)
{
    DBG_CHN6_TOGGLE;
    return 0;
}

初始化:

blt_soft_timer_init();
blt_soft_timer_add(&gpio_test0, 23000);//23ms
blt_soft_timer_add(&gpio_test1, 7000); //7ms <-> 17ms
blt_soft_timer_add(&gpio_test2, 13000);//13ms
blt_soft_timer_add(&gpio_test3, 27000);//27ms

定义了4个任务,这4个定时任务各有特点:

(1) gpio_test0每23ms toggle一次。

(2) gpio_test1使用了7ms/17ms两个时间的切换定时。

(3) gpio_test2在5s后将自己删掉。代码中有两种方式都可以实现这个功能:一是调用blt_soft_timer_delete (&gpio_test2);二是return -1。

(4) gpio_test3每27ms toggle一次。

软件模拟UART(Software UART)

为了方便一些user的双uart任务,除了支持硬件UART,B80 BLE SDK还提供了blt software UART demo,并且全部源码提供。user可以在理解了该demo的设计思路后直接使用,也可以自己做一些修改设计。

源代码全部在drivers/ext_driver/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。

Software UART初始化

调用下面的API进行初始化:

soft_uart_rx_handler(app_soft_rx_uart_cb);
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;

    soft_uart_rece.state = SOFT_UART_WAIT;

    soft_uart_rece.mutex_flag = 0;

    soft_uart_rece.time_interval = (1000000 / SOFT_UART_BAUD_RATE) * CLOCK_SYS_CLOCK_1US + SOFT_UART_OFFSET;
    //SET TIME
    timer0_set_mode(TIMER_MODE_SYSCLK, 0, SOFT_UART_INTERVAL * CLOCK_SYS_CLOCK_1US);
    timer_stop(TIMER0);
}

Software UART TX处理

blt software UART TX处理使用soft_uart_send函数来实现。user可以参考模拟串口只发数据的Demo,配置如下。

#define    TEST_RX_TX_RUN                           1
#define    TEST_ONLY_TX_RUN                         2

#define    TEST_SOFT_UART_RUN_MODEL                 TEST_ONLY_TX_RUN

在main_loop中如下图所示位置调用soft_uart_send函数,去处理各timer任务。

u8 send_buf[10] = {0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55};

void main_loop (void)
{
    ...
    soft_uart_send(send_buf, 10);
    ...
}

通过串口上位机工具可以看到发送的10 Byte数据。另外,外接逻辑分析仪也可以看到效果。

广播态下模拟串口只发数据

blt software UART TX的具体实现思路如下:

(1) 首先按Byte发送数据,发送前检查当前状态机状态。如果当前状态为advertising态,跳转到(2);如果当前状态为connection态,跳转到(3);如果当前状态为standby态,跳转到(4)。

void soft_uart_send(unsigned char * buf, unsigned char len) {

    unsigned char i;
    for (i = 0; i < len; i++) {

        unsigned char s;
        extern u8 blc_ll_getCurrentState(void);
        s = blc_ll_getCurrentState();
        ...
    }

}

(2) 当前状态为advertising态,调用blc_sdk_adv检查是否已经到达广播时间,如果到达,先发送广播,之后允许TX,跳转到(4)。

void soft_uart_send(unsigned char * buf, unsigned char len) {
    ...
    /*
    #define     BLS_LINK_STATE_ADV                  BIT(0)
    */
    if (s == BIT(0)) {
        extern void blc_sdk_adv(void);
        blc_sdk_adv();
    }
    ...
}

(3) 当前状态为connection态,调用blc_ll_SoftUartisRfState检查当前RF任务是否影响TX任务,如果当前时刻到下一次连接事件大于UART TX一个Byte数据的时间(SOFT_UART_SEND_ONE_BYTE),允许TX,跳转到(4),否则需要等待下一次连接事件结束。

void soft_uart_send(unsigned char * buf, unsigned char len) {
    ...
    /*
    #define         BLS_LINK_STATE_CONN                 BIT(3)
    */
    if (s == BIT(3)) {
        extern void blc_ll_SoftUartisRfState(int acl_margin_us);
        blc_ll_SoftUartisRfState(SOFT_UART_SEND_ONE_BYTE);
    } 
    ...
}

(4) 在允许UART TX的情况下,调用soft_uart_putchar函数将第i个Byte发送。

void soft_uart_send(unsigned char * buf, unsigned char len) {
    ...
    soft_uart_putchar(buf[i]);
    ...
}

Software UART RX处理

blt software UART RX处理使用gpio 中断和timer中断来实现,RX动作在irq handler中进行。user可以参考模拟串口收发数据的Demo,配置如下。

#define    TEST_RX_TX_RUN                           1
#define    TEST_ONLY_TX_RUN                         2

#define    TEST_SOFT_UART_RUN_MODEL                 TEST_RX_TX_RUN

在main_loop中如下图所示位置去处理收到的RX数据,并通过soft_uart_send函数再TX回去。

void main_loop (void)
{
    ...
    if (uart_rx_fifo.wptr != uart_rx_fifo.rptr) {
        u8 *p = uart_rx_fifo.p + (uart_rx_fifo.rptr & (uart_rx_fifo.num - 1))
                * uart_rx_fifo.size;

        soft_uart_send(&p[4], p[0]);

        uart_rx_fifo.rptr++;
    }
    ...
}

通过串口上位机工具发送10 Byte数据,可以在接受窗口收到下位机发回的数据。另外,外接逻辑分析仪也可以看到效果。

广播态下模拟串口收发数据

blt software UART RX的具体实现思路如下:

(1) 首先收到上位机发送的数据时,会触发GPIO irq,如果software uart state为SOFT_UART_WAIT,则关闭GPIO irq,通过设置timer irq tick来触发中断,读取下一次RX interval时的GPIO输入值,并将当前software uart state设置为SOFT_UART_WORK。

_attribute_ram_code_ void soft_uart_irq_handler(void)
{
    ...
    if ((reg_irq_src & FLD_IRQ_GPIO_EN) == FLD_IRQ_GPIO_EN) {
        reg_irq_src |= FLD_IRQ_GPIO_EN; // clear the relevant irq
        if ((gpio_read(SOFT_UART_RX_IO) == 0)&&(SOFT_UART_WAIT & soft_uart_rece.state)) {
                BM_CLR(reg_gpio_irq_wakeup_en(SOFT_UART_RX_IO), SOFT_UART_RX_IO & 0xff); // close GPIO irq
                soft_uart_rece.bit_num = 0x00;
                soft_uart_rece.temp_byte = 0x00;
                soft_uart_rece.state &= ~SOFT_UART_DONE_CHECK;
                soft_uart_rece.state &= ~SOFT_UART_WAIT;
                soft_uart_rece.state |= SOFT_UART_WORK;
                soft_uart_rece.done_count = 0;
                timer0_set_mode(TIMER_MODE_SYSCLK, 0, soft_uart_rece.time_interval);
                timer_start(TIMER0);
        }
    }
    ...
}

(2) 当触发timer irq时,清完timer irq status,之后判断当前software uart state,并进行相应操作。如果state为SOFT_UART_WORK,跳转到(3);如果state为SOFT_UART_STOP_CHECK,跳转到(4);如果state为SOFT_UART_DONE_CHECK,跳转到(5)。

_attribute_ram_code_ void soft_uart_irq_handler(void)
{
    ...
    //time irq
    if (timer_get_interrupt_status(FLD_TMR_STA_TMR0)) {
        timer_clear_interrupt_status(FLD_TMR_STA_TMR0); //clear irq status

        if (soft_uart_rece.state & SOFT_UART_WORK) {
            ...
        } else if (soft_uart_rece.state & SOFT_UART_STOP_CHECK) {
            ...
        } else if (soft_uart_rece.state & SOFT_UART_DONE_CHECK) {
            ...
        }
    }
    ...
}

(3) software uart state为SOFT_UART_WORK,暂存当前位的值,如果已经接收到8位的值,将software uart state置为SOFT_UART_STOP_CHECK。

_attribute_ram_code_ void soft_uart_irq_handler(void)
{
    ...
    if (soft_uart_rece.state & SOFT_UART_WORK) {
        if (1 == gpio_read(SOFT_UART_RX_IO)) { //
            soft_uart_rece.temp_byte |= BIT(soft_uart_rece.bit_num);
        }
        soft_uart_rece.bit_num++;
        if (8 == soft_uart_rece.bit_num) {
            soft_uart_rece.bit_num = 0x00;
            soft_uart_rece.state |= SOFT_UART_STOP_CHECK; //change state
            soft_uart_rece.state &= ~SOFT_UART_WORK;
        }
    }
    ...
}

(4) software uart state为SOFT_UART_STOP_CHECK,保存之前传输的Byte值,之后将software uart state置SOFT_UART_DONE_CHECK和SOFT_UART_WAIT,并调用soft_uart_RxHandler进行一些处理,重新打开RX GPIO irq。

_attribute_ram_code_ void soft_uart_irq_handler(void)
{
    ...
    if (soft_uart_rece.state & SOFT_UART_STOP_CHECK) {
        soft_uart_rece.state &= ~SOFT_UART_STOP_CHECK;
        if (1 == gpio_read(SOFT_UART_RX_IO)) { //
            soft_uart_rece.data[soft_uart_rece.data_count + 4] = soft_uart_rece.temp_byte; //len + buf
            soft_uart_rece.temp_byte = 0x00;
            soft_uart_rece.data_count++;
            if (soft_uart_rece.data_count >= soft_uart_rece.data_size) { //over flow
                soft_uart_rece.data[0] = soft_uart_rece.data_count;
                if (soft_uart_RxHandler)
                    soft_uart_RxHandler();
            }
        }
        soft_uart_rece.state |= SOFT_UART_DONE_CHECK;
        soft_uart_rece.state |= SOFT_UART_WAIT;
        reg_irq_src |= FLD_IRQ_GPIO_EN; // clear the relevant irq
        BM_SET(reg_gpio_irq_wakeup_en(SOFT_UART_RX_IO), SOFT_UART_RX_IO & 0xff); //start io irq
    } 
    ...
}

(5) software uart state为SOFT_UART_DONE_CHECK,清SOFT_UART_DONE_CHECK状态并调用soft_uart_RxHandler进行一些处理,关闭timer irq。

_attribute_ram_code_ void soft_uart_irq_handler(void)
{
    ...
    if (soft_uart_rece.state & SOFT_UART_DONE_CHECK) {
        soft_uart_rece.done_count++;
        if (UART_RECE_DONE_NUM <= soft_uart_rece.done_count) {
            timer_stop(TIMER0);
            if(soft_uart_rece.data_count > 0){
                soft_uart_rece.data[0] = soft_uart_rece.data_count;
                if (soft_uart_RxHandler)
                    soft_uart_RxHandler();
            }
            soft_uart_rece.state &= ~SOFT_UART_DONE_CHECK;
        } 
    }
    ...
}

Feature Demo介绍

B80_feature_test针对一些常用的BLE相关的feature给出了demo code,用户可参考这些demo完成自己的功能实现,详情见code。在B80_feature_test工程里面app_config.h中对宏“FEATURE_TEST_MODE”进行选择性的定义,即可切换到不同feature test的Demo。

feature test的测试Demo选择

下面介绍每个Demo测试方法。

广播功耗测试

此项主要测试不同广播参数广播时的功耗,用户在测试时可外接万用表测量功耗。在feature_config.h需要修改FEATURE_TEST_MODE 为TEST_POWER_ADV。

#define FEATURE_TEST_MODE             TEST_POWER_ADV

在feature_adv_power中根据需求修改广播类型及广播参数,app_config.h中提供了多种广播类型供用户选择,分别为可连接广播及非可连接广播。

ADV_POWER_TEST_TYPE的选择

可连接广播功耗测试

在feature_adv_power中,以下代码是可连接广播类型。

#define CONNECT_12B_1S_1CHANNEL         0
#define CONNECT_12B_1S_3CHANNEL         1
#define CONNECT_12B_500MS_3CHANNEL      2
#define CONNECT_12B_30MS_3CHANNEL       3

Demo 默认广播数据长度为12byte,用户可根据需求修改。

//ADV data length: 12 byte
u8 tbl_advData[12] = {0x08, 0x09, 't', 'e', 's', 't', 'a', 'd', 'v',0x02, 0x01, 0x05,};

Demo提供了1s_1channel,1s_3channel,500ms_3channel,30ms_3channel的广播参数,用户根据自己需求选择对应的测项即可。

非可连接广播功耗测试

在feature_adv_power中,以下代码是非可连接广播类型。

#define UNCONNECT_16B_1S_3CHANNEL       4
#define UNCONNECT_16B_1_5S_3CHANNEL     5
#define UNCONNECT_16B_2S_3CHANNEL       6

#define UNCONNECT_31B_1S_3CHANNEL       7
#define UNCONNECT_31B_1_5S_3CHANNEL     8
#define UNCONNECT_31B_2S_3CHANNEL       9

Demo 提供了两种广播数据长度分别为16byte和31byte,用户可根据需求选择。

u8 tbl_advData[] = {
                 15, 0x09, 't', 'e', 's', 't', 'a', 'd', 'v', '8', '9', 'A', 'B', 'C', 'D', 'E',
                };//ADV data length: 16 byte
u8 tbl_advData[] = {
                 30, 0x09, 't', 'e', 's', 't', 'a', 'd', 'v', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D'
            };//ADV data length: max 31 byte

Demo提供了1s_3channel,1.5s_3channel,2s_3channel的广播参数,用户根据自己需求选择对应的测项即可。

GATT Security测试

用户需要在feature_config.h中将FEATURE_TEST_MODE 为TEST_GATT_SECURITY。

#define FEATURE_TEST_MODE         TEST_GATT_SECURITY

由BLE模块ATT&GATT章节可知,服务列表中每个Attribute都定义了读写权限,即配对模式也要达到相应的等级才能去读或者写。如Demo的SPP服务中:

// client to server RX
{0,ATT_PERMISSIONS_READ,2,sizeof(TelinkSppDataClient2ServerCharVal),(u8*)(&my_characterUUID),     (u8*)(TelinkSppDataClient2ServerCharVal), 0},    //prop
{0,SPP_C2S_ATT_PERMISSIONS_RDWR,16,sizeof(SppDataClient2ServerData),(u8*)(&TelinkSppDataClient2ServerUUID), (u8*)(SppDataClient2ServerData), &module_onReceiveData},    //value
{0,ATT_PERMISSIONS_READ,2,sizeof(TelinkSPPC2SDescriptor),(u8*)&userdesc_UUID,(u8*)(&TelinkSPPC2SDescriptor)},

第二个Attribute的读写权限定义为:SPP_C2S_ATT_PERMISSIONS_RDWR。

此读写权限由用户去选择,可以选择如下其中一个:

#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_RDWR
#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_ENCRYPT_RDWR
#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_AUTHEN_RDWR
#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_SECURE_CONN_RDWR

无论选择哪个,当前配对模式要高于或者等于此读写权限等级才能正确的读写服务。

GATT测试主要测试配对加密的流程,主要分为以下几种方式:

  • LE_Security_Mode_1_Level_1, no authentication and no encryption.

  • LE_Security_Mode_1_Level_2, unauthenticated paring with encryption.

  • LE_Security_Mode_1_Level_3, authenticated paring with encryption-legacy.

  • LE_Security_Mode_1_Level_4, authenticated paring with encryption-sc.

用户需要将app_config.h根据需求选择相应的配对模式。

// LE security mode select
#define     SMP_TEST_MODE                       LE_SECURITY_MODE_1_LEVEL_1

下面对每种配对模式进行简要介绍。

LE_Security_Mode_1_Level_1

// LE_Security_Mode_1_Level_1, no authentication and no encryption
#define     SMP_TEST_NO_SECURITY                1

LE_Security_Mode_1_Level_1为最简单的配对方式,既不认证也不加密。用户将app_config.h的LE_SECURITY_MODE_1_LEVEL_1更改为SMP_TEST_NO_SECURITY。

#define     LE_SECURITY_MODE_1_LEVEL_1          SMP_TEST_NO_SECURITY

SMP_TEST_MODE更改为LE_SECURITY_MODE_1_LEVEL_1。

#define     SMP_TEST_MODE                       LE_SECURITY_MODE_1_LEVEL_1

LE_Security_Mode_1_Level_2

// LE_Security_Mode_1_Level_2, unauthenticated pairing with encryption
#define     SMP_TEST_LEGACY_PAIRING_JUST_WORKS  2 //JustWorks
#define     SMP_TEST_SC_PAIRING_JUST_WORKS      3 //JustWorks

LE_Security_Mode_1_Level_2模式为just work,只加密不认证。Just work 又分为legacy just work, sc just work。GATT Security测试中app_config.h的LE_SECURITY_MODE_1_LEVEL_2默认为SMP_TEST_LEGACY_PARING_JUST_WORKS,SMP_TEST_SC_PARING_JUST_WORKS未做处理,user可自行按照API配置。下面介绍SMP_TEST_LEGACY_PARING_JUST_WORKS。

SMP_TEST_LEGACY_PARING_JUST_WORKS:

用户作如下修改:

#define     LE_SECURITY_MODE_1_LEVEL_2       SMP_TEST_LEGACY_PARING_JUST_WORKS
#define     SMP_TEST_MODE                   LE_SECURITY_MODE_1_LEVEL_2

示意流程图:

Legacy Just Work流程示意图

LE_Security_Mode_1_Level_3

// LE_Security_Mode_1_Level_3, authenticated pairing with encryption
#define     SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI  4 //PK_Resp_Dsply_Init_Input
#define     SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI  5 //PK_Init_Dsply_Resp_Input
#define     SMP_TEST_LEGACY_PASSKEY_ENTRY_MISI  6 //PK_BOTH_INPUT, not test
#define     SMP_TEST_LEGACY_PASSKEY_ENTRY_OOB   7 //OOB_Authentication, not test

LE_Security_Mode_1_Level_3为既认证又加密Legacy配对方式,根据配对参数设置分为OOB,PassKey Entry, Numeric Comparison。目前demo仅提供了SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI示例代码。下面简单介绍下这种方式。

SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI:

用户需要在app_config.h中如下修改:

#define     LE_SECURITY_MODE_1_LEVEL_3       SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI
#define     SMP_TEST_MODE       LE_SECURITY_MODE_1_LEVEL_3

配对过程中需要slave端显示密钥,master端输入密钥。初始化时,注册了配对相关gap event。配对信息会通知到app层。

blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PAIRING_BEGIN            |  \
                        GAP_EVT_MASK_SMP_PAIRING_SUCCESS        |  \
                        GAP_EVT_MASK_SMP_PAIRING_FAIL               |  \
                        GAP_EVT_MASK_SMP_TK_DISPALY             |  \
                        GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE     |  \
                        GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);

用户需要在收到GAP_EVT_SMP_TK_DISPALY消息时打印下当前的密钥信息。

int app_host_event_callback (u32 h, u8 *para, int n)
{
    u8 event = h & 0xFF;
    switch(event)
    {
    
        case GAP_EVT_SMP_TK_DISPALY:
        {
            char pc[7];
            u32 pinCode = *(u32*)para;
            sprintf(pc, "%d", pinCode);
            printf("TK display:%s\n", pc);
        }
    
    }
}

基本流程示意图如下:

Legacy Just Work SDMI流程示意图

LE_Security_Mode_1_Level_4

// LE_Security_Mode_1_Level_4, authenticated pairing with encryption
#define     SMP_TEST_SC_NUMERIC_COMPARISON      8 //Numric_Comparison
#define     SMP_TEST_SC_PASSKEY_ENTRY_SDMI      9 //PK_Resp_Dsply_Init_Input
#define     SMP_TEST_SC_PASSKEY_ENTRY_MDSI      10//PK_Init_Dsply_Resp_Input
#define     SMP_TEST_SC_PASSKEY_ENTRY_MISI      11//PK_BOTH_INPUT, not test
#define     SMP_TEST_SC_PASSKEY_ENTRY_OOB       12//OOB_Authentication, not test

LE_Security_Mode_1_Level_4为既认证又加密SC配对方式,根据配对参数设置分为OOB,PassKey Entry, Numeric Comparison。目前demo 仅提供了SMP_TEST_SC_PASSKEY_ENTRY_SDMI的示例代码。下面简单介绍下这种方式。

SMP_TEST_SC_PASSKEY_ENTRY_SDMI:

用户需要在feature_security.c中如下修改:

#define     LE_SECURITY_MODE_1_LEVEL_4       SMP_TEST_SC_PASSKEY_ENTRY_SDMI
#define     SMP_TEST_MODE       LE_SECURITY_MODE_1_LEVEL_4

配对过程中需要slave端显示密钥,master端输入密钥。初始化时,注册了配对相关gap event。配对信息会通知到app层。

    blc_gap_registerHostEventHandler( app_host_event_callback );
    blc_gap_setEventMask( GAP_EVT_MASK_SMP_PAIRING_BEGIN            |  \
                          GAP_EVT_MASK_SMP_PAIRING_SUCCESS          |  \
                          GAP_EVT_MASK_SMP_PAIRING_FAIL             |  \
                          GAP_EVT_MASK_SMP_TK_DISPALY               |  \
                          GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE     |  \
                          GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);

用户需要在收到GAP_EVT_MASK_SMP_TK_DISPLAY消息时打印下当前的密钥信息。

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

        case GAP_EVT_SMP_TK_DISPALY:
        {
            char pc[7];
            u32 pinCode = *(u32*)para;
            sprintf(pc, "%d", pinCode);
            printf("TK display:%s\n", pc);
        }
        break;

    }
}

基本流程示意图如下:

SC SDMI配对流程示意图

GATT Security测试流程

如当前的配对模式为LE_SECURITY_MODE_1_LEVEL_3,即既有认证又有加密Lagacy配对模式。所以当前读写权限可以如下选择。

#define     SPP_C2S_ATT_PERMISSIONS_RDWR        ATT_PERMISSIONS_AUTHEN_RDWR

流程示意图如下:

Gatt Security示意图

DLE测试

DLE测试主要测试长包,对应的feature_config.h选择,代码如下:

#define FEATURE_TEST_MODE          TEST_SDATA_LENGTH_EXTENSION

烧录完之后按下reset,B80作为slave端选择与master建立连接,连接成功后会进行MTU及DataLength交换。

blc_att_requestMtuSizeExchange(BLS_CONN_HANDLE, MTU_SIZE_SETTING);
blc_ll_exchangeDataLength(0x14 , ACL_CONN_MAX_TX_OCTETS);//LENGTH_REQ opcode: 0x14

交换成功后,slave会每隔3.33s向master发送长包数据。

测试流程示意图如下:

DLE测试流程示意图

注意:

需要注意的是,在打开deepsleep retention时候,如果将RX FIFO size和TX FIFO size设置为200以上,可能会存在ram不够用的情况,user需要评估修改。

Soft Timer测试

可参考软件定时器(Software Timer)一章。

EMI测试

feature_emi用于产生所需的EMI测试信号,该例程需配合EMI_Tool” 和 “Non_Signaling_Test_Tool”工具使用。

协议

通信协议请参考《Telink SoC EMI Test User Guide》。

Demo说明

B80中EMI测试支持carrieronly模式、continue模式、burst模式、收包模式。

支持的无线通信方式包括Ble1M、Ble2M、Ble125K、Ble500K、Zigbee250K。

关于各个模式和功能函数的介绍,user 可参考《Telink Driver SDK Developer Handbook》。

其他模块

24MHz晶体外部电容

参考下图中的24MHz晶体匹配电容的位置C3/C4。

SDK默认使用B80内部电容(即ana_8a<5:0>对应的cap)作为24MHz晶振的匹配电容,此时C3/C4不用焊接电容。使用该方案的优势是:在Telink治具上可以测量并调节该电容,使得最终应用产品的频点值达到最佳。

24M晶体电路

如果需要使用外部焊接电容作为24M晶振的匹配电容(C3/C4焊接电容),则只要在main函数开始的地方(一定要在cpu_wakeup_init()之后,blc_app_loadCustomizedParameters()之前)调用下面API即可:

static inline void blc_app_setExternalCrystalCapEnable(u8  en)
{
    blt_miscParam.ext_cap_en = en;
}

只要按要求调用该API,SDK会自动处理所有的设置,包括关掉内部匹配电容、不再读取频偏校正值等。

32KHz时钟源选择

SDK支持使用MCU内部32k RC振荡电路(简称32k RC) 或者外部32k RC振荡电路(简称 32k Pad)。使用32k RC时,由于误差比较大,所以对于suspend或者deep retention时间较长的应用,其时间准确性会差一些。目前32k RC默认支持的最大长连接不能超过3s,一旦超过这个时间,ble_timing会出错,造成收包时间点不准确,容易出现收发包retry,功耗增大,甚至出现断连。而使用32k Pad时,误差则会小很多。

用户只需要在main函数开始的地方(一定要在cpu_wakeup_init函数之前)调用下面API:

32k RC调用:

void blc_pm_select_internal_32k_crystal(void);

32k Pad调用:

void blc_pm_select_external_32k_crystal (void);

Firmware数字签名

市场上存在一种产品恶意抄袭的方法。比如A客户使用了Telink的芯片和SDK研发了一款产品。A客户的竞争对手B客户也使用Telink的芯片,拿到该产品后,可以复制同样的硬件电路设计。如果该产品的数据烧录总线未被禁用的话,B客户有可能读到产品Flash上完整的Firmware。B客户使用同样的软硬件就可以抄袭出这个产品。

为了解决以上安全风险,SDK支持了软件数字签名功能。其原理是利用了芯片内置Flash具有唯一的UID。产品在治具烧录环节读取内置Flash的16 byte UID,然后和Firmware上的内容进行复杂的加密运算,生产一组校验值称为Signature,存储在Flash校准区对应的地址。即:

Signature = Encryption_function (Firmware, Flash_UID)

Signature跟Firmware和Flash_UID同时相关。SDK上程序初始化的时候读取Firmware和Flash_UID也做相同的计算,得到的结果和治具烧录的Signature进行对比验证,如不吻合则认为程序不合法,禁止运行。

需要强调,此功能涉及到的技术环节较多,包括治具上的配合、SDK上相应的配置等,客户如需要,必须提前和Telink FAE沟通确认细节。

下面介绍此功能实现的一些技术细节。

(1) 治具端必须正确配合,包括文件配置、写脚本等,详细内容请参考Telink testbench的相关文档和说明,这里不具体介绍。

(2) Signature存储地址为Flash Calibration area的偏移地址0x180连续16 byte。

(3) SDK上默认此功能是关闭的,如需使用,在app_config.h中打开下面的宏:

#define FIRMWARES_SIGNATURE_ENABLE          1   //firmware check

注意:

SDK上只有少数几个project在main函数初始化里添加了Firmware数字签名校验功能(通过搜索FIRMWARES_SIGNATURE_ENABLE可以看到)。如果客户使用的project上没有添加该功能,请务必从其他project merge到自己的project。

SDK中code如下所示,当数字签名不匹配时需要禁止程序运行。SDK上使用了最简单的while(1)来禁止运行,这只是一个示例写法,请用户自己评估该方法是否满足需求,如不满足可以自行使用其他方法,比如让MCU进deepsleep睡眠、修改各种data、bss、stack段等。

void blt_firmware_signature_check(void)
{
        unsigned int flash_mid;
        unsigned char flash_uid[16];
        unsigned char signature_enc_key[16];
        int flag = flash_read_mid_uid_with_check(&flash_mid, flash_uid);

        if(flag==0){  //reading flash UID error
            while(1);
        }

        firmware_encrypt_based_on_uid (flash_uid, signature_enc_key);

          //signature not match
        if(memcmp(signature_enc_key, (u8*)(flash_sector_calibration + CALIB_OFFSET_FIRMWARE_SIGNKEY), 16)){ 
            while(1);   //user can change the code here to stop firmware running
        }
}

(4) 数字签名的计算方法Encryption_function是由Telink定义的,会同时兼顾Firmware内容和Flash_UID,使用了AES 128加密。具体计算细节不公开,打包封库。上面firmware_encrypt_based_on_uid函数在libfirmware_encrypt.a中实现。

如果客户觉得通用的加密算法不够安全,需要使用自己的加密算法,可联系Telink FAE沟通解决。

SDK版本信息

为了方便用户获取当前SDK和library的版本信息,增加了版本信息获取函数。

unsigned char blc_get_sdk_version(unsigned char *pbuf,unsigned char number);

用户调用该接口应该得到至少5个字节,前5个字节显示SDK版本,其余为未来保留。

例如,如果调用该API后得到的数字为{3,4,0,0,1},则表示SDK版本为3.4.0.0补丁1。

Debug

GPIO模拟UART_TX打印方法介绍

为方便用户调试时打印信息,B80支持gpio模拟打印printf(const char *format, ...),sprintf(char *buff, const char *format, ...),需要在drivers/printf.h中定义相关信息如下所示:

#define DEBUG_MODE 1

#define  DEBUG_BUS      1

#if(DEBUG_BUS==DEBUG_IO)
#define PRINT_BAUD_RATE                     115200      //1M baud rate,should Not bigger than 1Mb/s
#define DEBUG_INFO_TX_PIN                   GPIO_PD3
#define TX_PIN_GPIO_EN()                    gpio_set_func(DEBUG_INFO_TX_PIN , AS_GPIO);
#define TX_PIN_PULLUP_1M()                  gpio_setup_up_down_resistor(DEBUG_INFO_TX_PIN, PM_PIN_PULLUP_1M);
#define TX_PIN_OUTPUT_EN()                  gpio_set_output_en(DEBUG_INFO_TX_PIN,1)
#define TX_PIN_OUTPUT_REG                   (0x503+((DEBUG_INFO_TX_PIN>>8)<<3))
#endif

此处默认的波特率为115200,TX_PIN 为GPIO_PD3,用户根据自己实际需求更改波特率及TX_PIN。

如果用户想用更高的波特率(大于115200,最高支持1M),需要提高cclk,至少更改为24MHz以上,在app_config.h更改cclk:

/////////////////// Clock  /////////////////////////////////
/**
 *  @brief  MCU system clock
 */
#define CLOCK_SYS_CLOCK_HZ      24000000

注意:

printf打印的信息可能会被中断打断出现乱码,不要在中断里打印!

Q&A

Q.如何在SDK中新建自己的工程?

A.一般为了保证工程中的各种设置,我们一般会以某一个demo为基础,建新的工程。比如我们以8208_ble_sample为基础来完成一个项目的新建。

Step 1:将code复制粘贴,并重命名。如下图所示,我们复制8208_ble_sample的代码,新命名为Test_Demo.

为工程重命名

Step 2:在项目右键的Properties -> Settings –> Manage Configurations中建立新的工程,比如新建Test_Demo。

为工程新建配置

点击OK之后,可以在项目列表中看到我们新建的工程,如下图。

工程列表中新建的工程

Step 3:在Test_Demo文件夹右键的Resource Configurations -> Exclude from Build中,将Test_Demo的设置中除了自己,将其他所有项目打勾。且在其复制源的相同设置中把Test_Demo打勾。如下图所示:

Exclude Test_Demo from build

Exclude source project from build

Step 4:将Test_Demo属性中的Setting-TC32 Compiler-symbols修改为新的标志,修改后需点Apply,如下图所示。

修改编译器标志

Step 5:在vender/common/user_config.h中,增加新代码对应的设置包含。如图所示:

增加新代码对应的设置

至此,新项目已经建好了。可以选择新的工程,进行clean和build使用了。

附录

附录1:crc16 算法

unsigned short crc16 (unsigned char *pD, int len)
{
static unsigned short poly[2]={0, 0xa001};
unsigned short crc = 0xffff;
unsigned char ds;
int i,j;

for(j=len; j>0; j--)
    {
unsigned char ds = *pD++;
for(i=0; i<8; i++)
        {
            crc = (crc >> 1) ^ poly[(crc ^ ds ) & 1];
            ds = ds >> 1;
        }
    }

return crc;
}