TC BLE Single Connection SDK
SDK Overview
This BLE SDK supplies demonstration code for BLE slave/master single connection development, based on which user can develop his own application program.
Applicable IC
The following IC models are supported:
- B85 Series: The TLSR8251, TLSR8253, and TLSR8258 hardware modules are fundamentally consistent, with only minor differences in SRAM and Flash sizes.
- B87 Series: The TLSR8238, TLSR8271, TLSR8273, and TLSR8278 hardware modules are fundamentally consistent, with only minor differences in SRAM and Flash sizes.
- TC321x Series: A newly added chip series that supports similar functionality to the B85 and B87 series but features different hardware characteristics and optimizations.
For specific details, please refer to the respective chip datasheet.
Software architecture
Software architecture for this BLE SDK includes application (APP) layer and BLE protocol stack.
Figure below shows the file structure after the SDK project is imported in IDE, which mainly contains 10 top-layer folders below: "algorithm", "application", "boot", "common", "drivers", "proj_lib", "project", "script", "stack" and "vendor".
-
Algorithm: This folder contains functions related to encryption algorithms, including general algorithm interfaces, encryption algorithms, and sub-modules such as Elliptic Curve Cryptography (ECC).
-
Application: This folder contains general application program, including:
- app: USB processing related functions
- audio: Audio processing related functions
- keyboard: Keyboard processing functions
- print: Printing functions
- usbstd: USB standard device class related functions
-
boot: This folder contains software bootloader for chip, i.e., assembly code after MCU power on or deepsleep wakeup, so as to establish environment for C program running. Includes bootloader files for the B85 series, B87 series, and TC321X series chips.
-
common: This folder contains generic handling functions across platforms, e.g. SRAM handling function, string handling function, and etc. Also includes the config subdirectory, which stores user configuration files.
-
drivers: This folder contains hardware configuration and peripheral drivers closely related to MCU, e.g. clock, flash, i2c, usb, gpio, uart. Provides corresponding drivers for different chip series (B85, B87, and TC321x).
-
proj_lib: Stores the library files required for SDK operation, including:
- liblt_2p4g.a: 2.4GHz protocol stack library
- liblt_825x.a: B85 series chip library, including BLE protocol stack
- liblt_827x.a: B87 series chip library, including BLE protocol stack
- liblt_general_stack.a: General protocol stack library
- liblt_tc321x.a: TC321x series chip library, including BLE protocol stack
- libfirmware_encrypt.a: Firmware encryption library
Files such as the BLE protocol stack, RF driver, and PM driver are encapsulated within the library files; source files are not visible to the user.
-
project: Stores project engineering files and compilation-related configurations.
-
script: Stores script files used to assist the development and build processes.
-
stack: Stores header files related to the protocol stack, including:
- 2p4g: 2.4GHz protocol stack related header files
- ble: BLE protocol stack related header files, including sub-modules such as controller, debug, device, hci, host, and service
The source files are compiled into library files and are not visible to users.
- vendor: Used to store user application layer code and various demo projects, including:
- ble_feature_test: BLE feature test demo
- ble_hci: BLE HCI demo
- ble_master_kma_dongle: BLE master device demo (not supported by some chips, such as TC321x series).
- ble_module: BLE module demo
- ble_remote: BLE remote control demo
- ble_sample: BLE simple example demo
- ble_slave_2_4g: BLE slave + 2.4GHz dual-mode demo
- 2p4g_feature: 2.4GHz feature demo
- 2p4g_genfsk_ll: 2.4GHz generic FSK demo
- 2p4g_tpll: 2.4GHz TPLL demo
- 2p4g_tpsll: 2.4GHz TPSLL demo
- common: Common code and configurations
main.c
The “main.c” file includes main function entry, system initialization functions and endless loop “while(1)”. It is recommended not to make any modifications to this file and to use the inherent implementation as provided.
_attribute_ram_code_ int main (void) //should run in ram_code
{
DBG_CHN0_LOW; //debug
blc_pm_select_internal_32k_crystal();
#if(MCU_CORE_TYPE == MCU_CORE_825x)
cpu_wakeup_init();
#else
cpu_wakeup_init(LDO_MODE,INTERNAL_CAP_XTAL24M);
#endif
int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup(); //MCU deep retention wakeUp
rf_drv_ble_init();
gpio_init(!deepRetWakeUp); //analog resistance will keep available in deepSleep mode, so no need initialize again
clock_init(SYS_CLK_TYPE);
if( deepRetWakeUp ){
user_init_deepRetn();
}
else{
user_init_normal();
}
irq_enable();
while (1) {
#if (MODULE_WATCHDOG_ENABLE)
wd_clear(); //clear watch dog
#endif
main_loop ();
}
}
app_config.h
User configuration file, used to configure various system parameters, including BLE configurations, hardware settings, PM (Power Management) low-power settings, and debug options.
In the SDK, the app_config.h file implements configuration selection for different projects through conditional compilation structures. The common/config/user_config.h file includes the specific app_config.h for the corresponding project based on defined project macros:
#if (__PROJECT_8258_BLE_REMOTE__ || __PROJECT_8278_BLE_REMOTE__|| __PROJECT_TC321X_BLE_REMOTE__)
#include "vendor/ble_remote/app_config.h"
#elif (__PROJECT_8258_BLE_SAMPLE__ || __PROJECT_8278_BLE_SAMPLE__ || __PROJECT_TC321X_BLE_SAMPLE__)
#include "vendor/ble_sample/app_config.h"
#elif (__PROJECT_8258_MODULE__ || __PROJECT_8278_MODULE__ || __PROJECT_TC321X_MODULE__)
#include "vendor/ble_module/app_config.h"
#else
#include "vendor/common/default_config.h"
#endif
This structure allows each demo project to maintain its own independent configuration file while ensuring centralized management of the overall configurations.
Detailed explanations for each parameter in app_config.h are provided later in the respective module descriptions.
Application File
-
"app.c": User file for BLE protocol stack initialization, data processing and low power management.
-
"app_att.c" of BLE slave project: configuration files for services and profiles. Contains the Telink-defined Attribute structure, which provides related Attributes such as GATT, standard HID, private OTA, and MIC. These can be referenced to add custom services and profiles.
-
UI task files: IR (Infrared Radiation), battery detect, and other user tasks.
BLE Stack Entry
There are two entry functions in BLE stack code of Telink BLE SDK.
(1) BLE related interrupt handling entry irq_blt_sdk_handler in irq_handler function of the main.c file.
_attribute_ram_code_ void irq_handler(void)
{
irq_blt_sdk_handler();
}
(2) BLE logic and data handling entry "irq_blt_sdk_handler" in "main_loop" of the application file.
_attribute_no_inline_ void main_loop(void)
{
///////////////////// BLE entry ////////////////////////////
blt_sdk_main_loop();
////////////////////// UI entry ////////////////////////////
......
////////////////////// PM process ////////////////////
......
}
Software Bootloader
The software bootloader files are stored in the SDK/boot/ directory. In version V3.4.2.4, the software bootloader files and link files have been streamlined, and automatic configuration has been added. Users experienced with older versions are advised to read the following introduction in detail.
Software Bootloader for versions prior to SDK V3.4.2.4
In older versions, the SDK/boot/ directory is as follows:

Each IC corresponds to two software bootloader files, which enable 16KB deep retention and 32KB deep retention respectively (for the introduction of deep retention, please refer to the chapter of "Low Power Management"). Since the TLSR8273 and TLSR8278 have the same boot file and link file, the TLSR8278 configuration is used uniformly.
Take cstartup_8258_RET_16K.S as an example, the first sentence #ifdef MCU_STARTUP_8258_RET_16K illustrates that the bootloader takes effect only when MCU_STARTUP_8258_RET_16K is defined by user.
Users can choose different software bootloader according to the actual IC used and whether to use the deep retention (16KB or 32KB) function.
The default configuration of the project in B85 BLE SDK is TLSR8258 with SRAM size 64KB, deepsleep retention 16KB SRAM, i.e. the corresponding software bootloader and link files are cstartup_8258_RET_16K.S and boot_16k_retn_8251_8253_8258.link respectively. Users need to manually modify their configuration according to the type of chip they use and the evaluation of Retention size.
Take 8258_ble_remote as an example to illustrate how to change the software bootloader of TLSR8258 to deepsleep retention 32KB SRAM:
Step 1 Define -DMCU_STARTUP_8258_RET_32K in the 8258_ble_remote project settings as shown in the following figure.

Note
- According to the previous introduction, the hardware of TLSR8251, TLSR8253 and TLSR8258 in the B85/B87 series is the same, and the hardware of TLSR8271, TLSR8238, TLSR8273 and TLSR8278 is the same, but the SRAM size is different, so users need to modify the
boot.linkfile in the root directory of the SDK after choosing different software bootloader files (according to the correspondence in the following table, replace the contents of the link file in the SDK root directory). The software bootloader andboot.linkselection for different ICs are shown in the following table.
| IC | 16KB retention | 32KB retention |
|---|---|---|
| TLSR8251 | boot_16k_retn_8251_8253_8258.link cstartup_8251_RET_16K.S | boot_32k_retn_8251.link cstartup_8251_RET_32K.S |
| TLSR8253 | boot_16k_retn_8251_8253_8258.link cstartup_8253_RET_16K.S | boot_32k_retn_8253_8258.link cstartup_8253_RET_32K.S |
| TLSR8258 | boot_16k_retn_8251_8253_8258.link cstartup_8258_RET_16K.S | boot_32k_retn_8253_8258.link cstartup_8258_RET_32K.S |
| TLSR8271 | boot_16k_retn_8271_8278.link cstartup_8271_RET_16K.S | boot_32k_retn_8271.link cstartup_8271_RET_32K.S |
| TLSR8238 | boot_16k_retn_8238.link cstartup_8238_RET_16K.S | boot_32k_retn_8238.link cstartup_8238_RET_32K.S |
| TLSR8273 | boot_16k_retn_8271_8278.link cstartup_8278_RET_32K.S | boot_32k_retn_8278.link cstartup_8278_RET_32K.S |
| TLSR8278 | boot_16k_retn_8271_8278.link cstartup_8278_RET_32K.S | boot_32k_retn_8278.link cstartup_8278_RET_32K.S |
Step 2 Based on the example above and the mapping table, the software bootloader file is cstartup_8258_RET_32K.S. You need to replace the boot.link in the SDK root directory with the SDK/boot/boot_32k_retn_8253_8258.link file.
Inside the user_init() API, call the following API after blc_ll_initPowerManagement_module() to configure the hardware Retention area: blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K).
Software Bootloader for SDK v3.4.2.4 and Later Versions
As with previous versions, the software bootloader files are stored in the SDK/boot/ directory:

The TLSR825x, TLSR827x, and TC321x series ICs correspond to different software bootloader files, which are cstartup_825x.S, cstartup_827x.S, and cstartup_TC321X.S respectively. Users configure the SRAM size in the .S file by selecting different chips. All B85/B87/TC321x ICs use the same link file in the new version.
Taking cstartup_825x.S as an example:
#define SRAM_BASE_ADDR (0x840000)
#define SRAM_32K (0x8000) //32K SRAM
#define SRAM_48K (0xc000) //48K SRAM
#define SRAM_64K (0x10000) //64K SRAM
#if (MCU_STARTUP_8258)
#define SRAM_SIZE (SRAM_BASE_ADDR + SRAM_64K)
#elif (MCU_STARTUP_8253)
#define SRAM_SIZE (SRAM_BASE_ADDR + SRAM_48K)
#elif (MCU_STARTUP_8251)
#define SRAM_SIZE (SRAM_BASE_ADDR + SRAM_32K)
#endif
#ifndef SRAM_SIZE
#define SRAM_SIZE (SRAM_BASE_ADDR + SRAM_64K)
#endif
The default configuration used by projects in the B85 BLE SDK is for the TLSR8258 with a 64KB SRAM size. Users need to manually modify this configuration based on the specific chip type they are using.
Using 8251_ble_sample as an example, this section explains how to modify the SRAM size defined in the bootloader.
Define -DMCU_STARTUP_8251 in the 8251_ble_sample project settings, as shown in the figure below. Other legacy flags, such as MCU_STARTUP_8258_RET_16K, MCU_STARTUP_8258_RET_32K, etc., are no longer in use.

In the new version, the Retention size can be set automatically via the API blc_app_setDeepsleepRetentionSramSize(). The mechanism works by checking _retention_size_ to determine the RAM usage at the end of the current retention_data section. If it is less than 16KB, the program sets it to Retention 16KB SRAM mode; if it exceeds 16KB but is less than 32KB, the program automatically switches to Retention 32KB SRAM mode; if it exceeds 32KB, the program reports an error during the compilation stage.
The judgment code in blc_app_setDeepsleepRetentionSRAMSize() is as follows:
if (((u32)&_retention_size_) < 0x4000){
blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW16K); //retention size < 16k, use 16k deep retention
}
else if (((u32)&_retention_size_) < 0x8000){
blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K); //retention size < 32k and >16k, use 32k deep retention
}
else{
/* retention size > 32k, overflow. deep retention size setting err*/
#if (UART_PRINT_DEBUG_ENABLE)
tlkapi_printf(APP_LOG_EN, "[APP][INI] deep retention size setting err");
#endif
}
The error prompt when the Retention size exceeds 32KB is shown below:

Demo codes
Telink BLE SDK provides users with multiple BLE and 2.4GHz demos.
Users can observe intuitive effects by running the software and hardware demo. Users can also modify the demo code to complete their own application development. Demo codes path is shown as below.

BLE Slave Demo
BLE slave demos and their differences are shown in the table below.
| Demo | Stack | Application | MCU Function |
|---|---|---|---|
| ble hci | BLE controller | No | BLE controller, only Advertising and one Slave connection |
| ble module | BLE controller + host | Application on main MCU | BLE transparent transmission module |
| ble remote | BLE controller + host | Remote control application | Main MCU |
| ble sample | BLE controller + host | Simplest slave demo, advertising and connection functions | Main MCU |
| ble feature test | BLE controller + host | Collection of various features | Main MCU |
| ble slave_2_4g | BLE controller + host | BLE slave + 2.4GHz dual-mode function | Main MCU |
| ble slave_2_4g_switch | BLE controller + host | BLE slave + 2.4GHz switchable dual-mode function | Main MCU |
The ble hci is a BLE slave controller. It provides a UART-based HCI to communicate with a host on another MCU, forming a complete BLE slave system.
The ble module functions only as a BLE pass-through module. It communicates with the main MCU via a UART interface, and application code is generally written on the main MCU.
The ble module implements functionality to control relevant status changes through the pass-through module.
The ble remote is a remote controller demo based on a complete slave role. It includes features such as Flash write protection, low voltage detection, key scanning, NEC format IR transmission, OTA updates, application-layer power management, Bluetooth control, voice transmission, and IR learning. Users can use this project to understand the structure of a basic use case and how most features are implemented at the application layer.
Note
- Due to the high RAM consumption of voice, IR, and IR learning features, for SDK versions prior to V3.4.2.4, the B85/B87 remote should be manually configured to use the 32k retention setting and recompiled when these functions are enabled.
The ble sample is a simplified version of ble remote, capable of pairing and connecting with standard BLE Master devices.
The ble slave_2_4g and ble_slave_2_4g_switch are demos featuring BLE slave and 2.4GHz dual-mode capabilities, allowing for switching between BLE and 2.4GHz modes or simultaneous operation.
BLE master demo
The ble master kma dongle is a BLE Master single-connection demo, which can establish connections and communicate with slave devices such as ble sample, ble remote, or ble module.
In slave projects like ble remote and ble sample, the corresponding Library provides a complete BLE Stack (Master and Slave share the same library), containing the BLE Controller and BLE Host. Users only need to write application code at the App layer, relying entirely on the APIs provided by the Controller and Host, without needing to concern themselves with the underlying processing of the BLE Host.
In the Library of the new SDK version, although the Slave and Master libraries have been combined into one, ble master kma dongle only invokes the standard BLE Controller functions within the library during compilation. The Library itself does not provide standard Master-side Host functions. Therefore, the ble master kma dongle Demo provides a reference BLE Host implementation at the App layer, covering ATT, a simple SDP (Service Discovery Protocol), and the most commonly used SMP (Security Manager Protocol).
The most complex functionality of a BLE Master lies in performing complete Service Discovery on Slave devices and identifying all services, which is typically only fully implemented in Android or Linux systems. Limited by the Flash and SRAM size of the Telink IC, the chip cannot provide universal full-scale Service Discovery. However, the SDK exposes all necessary ATT interfaces; users can refer to the discovery process of ble master kma dongle for ble remote to implement traversal and identification targeting specific services.
Feature Demo
The ble_feature_test provides demo code for some common BLE-related features, and users can refer to these demos to complete their own functional implementations. The BLE section of this document introduces all the features.
In feature_config.h within the ble_feature_test project, the macro "FEATURE_TEST_MODE" can be selectively defined to switch to Demos of different features.
2.4GHz Demo
The SDK also provides several demos related to 2.4GHz protocols:
- 2p4g_genfsk_ll: Generic FSK low-level protocol demo, implementing basic 2.4GHz communication functions
- 2p4g_tpll: TPLL protocol demo, implementing a specific communication protocol
- 2p4g_tpsll: TPSLL protocol demo, implementing another specific communication protocol
- 2p4g_feature: 2.4GHz feature collection demo, containing various 2.4G application scenarios
- 2p4g_feature_test: 2.4GHz feature test demo, used for testing various 2.4GHz features
These demos demonstrate the capabilities of the SDK in 2.4GHz communication, and users can select the appropriate demo as a development starting point according to their needs.
Driver Demo
The Driver demo is not provided in the new version of the SDK, users can refer to the sample code in the corresponding Platform SDK.
MCU Basic Modules
MCU Address Space
MCU Address Space Allocation
Taking the typical 64KB SRAM version as an example, the MCU address space allocation is introduced. As shown in the figure below.
The maximum addressing space of the Telink B85/B87/TC321X MCU is 16MB:
- The 8MB space from 0 to 0x7FFFFF is the program space, meaning the maximum program capacity is 8MB.
- 0x800000 to 0xFFFFFF is the external device space: 0x800000~0x80FFFF is the register space; 0x840000~0x84FFFF is the 64KB SRAM space.

When the B85/B87/TC321x MCU is physically addressed, address line BIT (23) is used to distinguish between program space/peripheral space.
- When this address is 0, access the program space.
- When this address is 1, access the peripheral space.
When the addressing space is peripheral space (BIT(23) is 1), address line BIT(18) is used to distinguish between Registers and SRAM.
- When this address is 0, access the register.
- When this address is 1, access the SRAM.
SRAM and Firmware Space Allocation
The SRAM space allocation of B85/B87/TC321X is closely related to the deepsleep retention function in the low-power management section; users are requested to first master the knowledge related to deepsleep retention.
The 32KB SRAM address space range is 0x840000 ~ 0x848000, the 48KB SRAM address space range is 0x840000 ~ 0x84C000, and the 64KB SRAM address space range is 0x840000 ~ 0x850000.
Note
- In version V3.4.2.4, the software bootloader files and link files have been streamlined, and the layout of some sections has been modified; users who have used older versions are requested to read the following introduction carefully.
Space Allocation of Versions Prior to SDK V3.4.2.4
The figure below illustrates the SRAM space allocation for 32KB, 48KB, and 64KB SRAM chips in 16KB Retention and 32KB Retention modes in the older version.
Note
- When the 32KB SRAM chip uses the Deepsleep Retention 32K SRAM mode, the various sections of its SRAM space allocation are dynamically adjusted. Please refer to the corresponding
software bootloaderandlinkfiles for details.

Below, the IC 8258 (64KB SRAM) in Deepsleep Retention 16KB SRAM mode is taken as an example to detail the various components of the SRAM area. (Note: For other SRAM sizes or the Deepsleep Retention 32KB SRAM mode, users can deduce by analogy.)
The SRAM and Firmware space allocation under this configuration is shown in the figure below:

The files related to SRAM space allocation in the SDK are boot.link (from the "software bootloader introduction" section we know that the content of boot.link here is the same as the boot_16k_retn_8251_8253_8258.link file) and cstartup_8258_RET_16K.S. (If deepsleep retention 32K SRAM is used, the bootloader corresponds to cstartup_8258_RET_32K.S, and the link file corresponds to boot_32k_retn_8253_8258.link.)
Firmware in Flash includes: vector, ram_code, retention_data, text, rodata, and Data initial value.
In SRAM it includes: vector, ram_code, retention_data, Cache, data, bss, stack, and unused SRAM area.
The vector/ram_code/retention_data in SRAM are copies of the vector/ram_code/retention_data in Flash.
(1) vectors and ram_code
The "vectors" section is the program corresponding to the assembly file software bootloader, which is the software startup code.
The "ram_code" section is the code in Flash Firmware that needs to reside in memory, corresponding to all functions in the SDK with the keyword _attribute_ram_code_ added, such as the flash erase_sector function:
_attribute_ram_code_ void flash_erase_sector(u32 addr);
Functions reside in memory mainly for the following two reasons:
First, certain functions should reside in resident memory (RAM) because they involve timing multiplexing with Flash MSPI pins. If these functions were placed in Flash, it would lead to MSPI timing conflicts and cause system crashes (e.g., all functions related to Flash operations).
Second, functions executed in RAM do not need to be re-fetched from Flash for every invocation, which saves time. Therefore, functions with strict execution time constraints can be kept resident in RAM to improve execution efficiency. The SDK places frequently executed BLE timing-related functions in RAM to reduce execution time, ultimately achieving power conservation.
If users need to make specific functions resident in RAM, they can refer to the implementation of flash_erase_sector by adding the _attribute_ram_code_ keyword before the function declaration. After compilation, it can be confirmed through the .list file that the function has been allocated to the ram_code section.
During the MCU power-on initialization phase, both vector (vector table) and ram_code in the Firmware need to be completely copied to RAM. After compilation, the total size of these two parts is represented by _ramcode_size_. _ramcode_size_ is a linker symbol whose calculation logic is implemented in the boot.link file (as shown below), and its final value equals the sum of the sizes of all instructions in the vector section and the ram_code section.
. = 0x0;
.vectors :
{
*(.vectors)
*(.vectors.*)
}
.ram_code :
{
*(.ram_code)
*(.ram_code.*)
}
PROVIDE(_ramcode_size_ = . );//Calculate actual ramcode size(vector + ram_code)
(2) retention_data
The deepsleep retention mode of B85/B87/TC321X maintains continuous power to the first 16K/32K of SRAM after the MCU enters sleep, thereby ensuring data within this area is not lost due to power loss.
In the program, ordinary global variables are allocated to the .data section or .bss section during default compilation. Since these two sections do not belong to the retention powered area, the contents are lost due to power loss after entering deepsleep retention mode.
If it is necessary to ensure that specific variables can retain their values during deepsleep (especially deepsleep retention mode), they should be explicitly allocated to the retention_data section. The implementation method is to add the _attribute_data_retention_ keyword when defining the variable. The following are application examples:
_attribute_data_retention_ int AA;
_attribute_data_retention_ unsigned int BB = 0x05;
_attribute_data_retention_ int CC[4];
_attribute_data_retention_ unsigned int DD[4] = {0,1,2,3};
Referring to the subsequent explanations regarding the data and bss sections, it can be seen that: the initial values of global variables in the data section should be pre-stored in Flash; while the initial values of variables in the bss section default to 0, requiring no reserved space in Flash, and the Bootloader directly clears them to zero in SRAM during runtime.
In contrast, the processing mechanism of the retention_data section is different: regardless of whether the initial value of the variable is 0, the compiler unconditionally reserves initial values for it in the retention_data area of Flash. When the system restarts or wakes up from Deep Sleep, this data is copied as a whole to the retention_data area of SRAM.
In terms of memory layout, the retention_data section follows immediately after the ram_code section. That is, the three parts vector, ram_code, and retention_data are stored sequentially at the beginning of Flash, and their total size is defined by the linker symbol _retention_size_.
When the MCU powers on or wakes up from Deep Sleep, vector + ram_code + retention_data is copied as a whole to the starting address of SRAM. During subsequent program execution, as long as the system does not enter a complete power-down mode (i.e., only in Suspend or Deep Sleep Retention mode), this part of the SRAM content remains resident, and the MCU does not need to read from Flash again.
The configuration regarding the retention_data section in the boot.link file is shown as follows:
. = (0x840000 + (_rstored_));
.retention_data :
AT ( _rstored_ )
{
. = (((. + 3) / 4)*4);
PROVIDE(_retention_data_start_ = . );
*(.retention_data)
*(.retention_data.*)
PROVIDE(_retention_data_end_ = . );
}
The meaning of the above configuration is: when compiling, we see that the variable with the keyword "retention_data" is distributed in the flash firmware with the starting address "_rstored_", and the corresponding address in SRAM is 0x840000 + (_rstored_). The value of "_rstored_" is the end of the "ram_code" section.
When using deepsleep retention 16KB SRAM mode, "retention_size" cannot exceed 16KB, if it exceeds the 16KB limit, user can choose to switch to deepsleep retention 32KB SRAM mode. If the user selects a configuration that uses deepsleep retention 16KB SRAM mode, but the defined "retention_size" exceeds the 16KB limit, the compilation results in the error as shown in the figure below.

Users can correct the error through any of the following ways:
a. Reduce the amount of data defined with the _attribute_data_retention_ attribute;
b. Choose to switch to Deep Sleep Retention 32KB SRAM mode; for detailed configuration methods, please refer to Section 1.3.
In this version of the SDK, when _retention_size_ does not exceed 16KB (assuming it is 12KB), there is a 4KB "wasteful flash area" (invalid Flash area) on the Flash. In the corresponding Firmware Binary file, it can be seen that the content of the 12KB ~ 16KB area consists entirely of invalid "0"s. After copying to SRAM, there is also a 4KB "wasteful SRAM area" (invalid SRAM area) on the SRAM.
If users do not wish to waste too much Flash/SRAM space, they can appropriately increase custom ram_code and retention_data, migrating functions or variables that were not previously placed in ram_code/retention_data into ram_code/retention_data by adding the corresponding keywords.
(3) Cache
Cache is the Instruction Cache of the MCU and should be mapped to a specific address segment in SRAM to function properly. The size of the Cache is fixed, containing a 256-byte Tag and a 2048-byte Instruction Cache, totaling 0x900 (2.25K).
The code residing in RAM (ram_code) can be directly read and executed from SRAM. However, only a small portion of the code in the Firmware resides in SRAM, while the vast majority of the code remains stored in Flash. Based on the principle of locality of programs, the Cache mechanism can dynamically cache parts of the Flash code:
- When the code the CPU needs to execute already exists in the Cache (Cache Hit), it is read and executed directly from the Cache;
- If it is not in the Cache, the code is read from Flash, moved to the Cache, and then executed.
The text section in the Firmware does not reside in SRAM; this part of the code conforms to the principle of locality and should be loaded into the Cache during runtime to be executed by the CPU.
The size of the Cache is fixed at 2.25KB, but its starting address in SRAM is configurable. In this version of the SDK, the Cache is configured after the SRAM 16K Retention Area, meaning the starting address is 0x844000 and the ending address is 0x844900.
(4) data / bss
The data segment is used to store initialized global variables in SRAM (i.e., global variables with non-zero initial values), while the bss segment is used to store uninitialized global variables in SRAM (i.e., global variables with an initial value of 0). These two segments are physically contiguous, with the bss segment immediately following the data segment; therefore, they are introduced here as a single unit.
The data and bss segments are placed immediately after the Cache, meaning their starting address corresponds to the Cache's ending address, which is 0x844900 in this version. In the boot.link code, the starting address of the data segment in SRAM is directly defined:
. = 0x844900;
.data :
The "data" segment is a global variable that is initialized and its initial value needs to be stored in flash in advance, i.e. the "data initial value" in the Firmware.
(5) data_no_init
To save Retention RAM space, this new data section has been added. The characteristics of this section are: it is located in RAM but does not belong to the Retention area, therefore the initial values of variables are random. This section is dedicated to internal SDK optimization and is not recommended for direct use by users. If the application layer involves the use of this section, it should be strictly guaranteed that: variables have completed explicit assignment before use, and no operations such as Deep Sleep Retention, Deep Sleep, Reboot, or re-powering have occurred between the assignment and use.
(6) stack / unused area
For 64KB SRAM, the stack starts from the highest address 0x850000 (the corresponding address for 48K SRAM is 0x84C000, and for 32K SRAM is 0x848000). Its growth direction extends from high address to low address, meaning the stack pointer SP decrements when data is pushed onto the stack and increments when data is popped from the stack.
By default, the stack usage of the SDK library does not exceed 600 bytes. However, since the actual size of the stack depends on the "deepest" position during runtime, the final usage is closely related to the user's upper-layer program design. If the user uses complex recursive function calls, large local array variables, or enables Secure Connection (the 825x series does not support Public Key Engine, requiring software calculation of Public Key, which consumes more stack space), as well as other situations that may cause deep stack calls, it results in a significant increase in the final stack usage.
When the user's SRAM usage is high, the actual stack usage of the program should be determined. This cannot be statically analyzed solely through the list file; the application should be actually run—ensuring that all code paths in the program that may lead to deep stack calls are covered—to determine the peak usage of the stack by reading the SRAM space.
The unused area is the remaining space between the end address of the data_no_init section and the deepest address of the stack. Only when this space exists (greater than 0) does it indicate that the stack has not conflicted with data_no_init, and the SRAM usage is within a safe range. If the deepest position of the stack overlaps with the bss section (or data_no_init section), it indicates that the SRAM has overflowed.
The end address of the bss section can be found through the list file, thereby determining the maximum available space left for the stack. Users need to analyze whether this space is sufficient and combine it with the above method to check the deepest stack address during runtime to judge whether the SRAM usage exceeds the standard. Detailed analysis methods are provided in the subsequent demo.
(7) text
The text section is a collection of all non-ram_code functions in the Flash Firmware. If a function in the program has the _attribute_ram_code_ declaration added, it is compiled into the ram_code section; other functions without this keyword are all compiled into the text section.
Generally, the text section is the part occupying the largest space in the Firmware, far exceeding the capacity of SRAM. Therefore, it is necessary to utilize the Cache buffering mechanism to pre-load the code to be executed into Cache before it can be executed.
(8) rodata /data init value
Except for vector, ram_code and text, the remaining data in Flash Firmware are rodata segment and data initial value.
The rodata segment is the readable and uneditable data defined in the program, and is a variable defined by the keyword const. For example, the ATT table in Slave.
static const attribute_t my_Attributes[] = ……
The user can see in the corresponding list file that my_Attributes is in the rodata segment.
The data segment introduced earlier is a global variable that has been initialized in the program, for example, the global variable is defined as follows.
int testValue = 0x1234;
Then the compiler stores the initial value 0x1234 in the data initial value, and when running the bootloader, it copies the initial value to the memory address corresponding to the testValue.
Space Allocation for SDK V3.4.2.4 and Later Versions
The SRAM space allocation of SDK V3.4.2.4 and later versions is the same in principle as the previous versions, except that in order to save space, the original fixed 16KB/32KB retention size is modified to the actual used retention size. This chapter introduces the modified and added parts in the new version in detail.
The figure below illustrates the SRAM space allocation corresponding to 32KB SRAM chips, 48KB SRAM chips, and 64KB SRAM chips in 16k retention and 32k retention modes in the new version. The various segments of SRAM space allocation are dynamically adjusted; for details, please refer to the software bootloader and link files.

In the new version, the size of deepsleep retention can be automatically set via the API blc_app_setDeep sleepRetentionSRAMSize(), which determines the RAM size occupied by the end of the current retention_data section through _retention_size_. If it is less than 16KB, it is automatically set to deepsleep retention 16K SRAM mode; if it exceeds 16KB but is less than 32KB, it is automatically set to deepsleep retention 32K SRAM mode; if it exceeds 32K, an error occurs during the compilation stage.
The judgment code in blc_app_setDeepsleepRetentionSRAMSize() is as follows:
if (((u32)&_retention_size_) < 0x4000){
blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW16K); //retention size < 16k, use 16k deep retention
}
else if (((u32)&_retention_size_) < 0x8000){
blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K); //retention size < 32k and >16k, use 32k deep retention
}
else{
/* retention size > 32k, overflow. deep retention size setting err*/
#if (UART_PRINT_DEBUG_ENABLE)
tlkapi_printf(APP_LOG_EN, "[APP][INI] deep retention size setting err");
#endif
}
Add an assertion in the boot.link file to determine during the compilation stage whether the retention size exceeds 32K.
ASSERT(_retention_size_ * __PM_DEEPSLEEP_RETENTION_ENABLE < 0x8000, "Error: Retention RAM size overflow.");
Among them, __PM_DEEPSLEEP_RETENTION_ENABLE is defined in vendor/common/user_config.c, which equals PM_DEEPSLEEP_RETENTION_ENABLE configured by the user in app_config.h.
The error message when deepsleep retention size exceeds 32KB is:

The following details the SRAM area sections using the IC 8258 with 64KB SRAM size as an example. If the SRAM size is a different value, the user can deduce the details by analogy.
The SRAM and firmware space allocation corresponding to 64KB SRAM is shown in the figure below.

The files related to SRAM space allocation in the SDK are boot.link and cstartup_825x.S.
The firmware in Flash includes vector, ram_code, retention_data, text, rodata, and data initial value.
In the SRAM it includes vector, ram_code, retention_data, Cache, data, bss, data no initial value, sdk_version, stack, and unused SRAM area.
The vector/ram_code/retention_data in SRAM are copies of vector/ram_code/retention_data in Flash.
(1) vectors and ram_code
The vectors and ram_code sections remain unchanged compared to the previous version. For detailed information, please refer to the section "Space Allocation for SDK Versions Prior to V3.4.2.4".
(2) retention_data
The retention_data section remains unchanged compared to the previous version. For detailed information, please refer to the section "Space Allocation for SDK Versions Prior to V3.4.2.4".
(3) Cache
In the new version, the Cache section is configured after retention_data, with the starting address at _retention_data_size_align_256_. Other parts of the Cache section remain consistent with the previous version. For detailed information, please refer to the section "Space Allocation for SDK Versions Prior to V3.4.2.4".
(4) data / bss
In the new version, the starting address of the data section immediately follows the end address of the Cache, which is 0x840900 + (_retention_data_size_align_256_). The following code from boot.link directly defines the starting address of the data section in SRAM:
. = (0x840900 + (_retention_data_size_align_256_));
.data :
The other parts of the data and bss sections remain unchanged compared to the previous version. For detailed information, please refer to the section "Space Allocation for SDK Versions Prior to V3.4.2.4".
(5) data_no_init
The data_no_init section remains unchanged compared to the previous version. For detailed information, please refer to the section "Space Allocation for SDK Versions Prior to V3.4.2.4".
(6) stack / unused area
In the new version, assertions have been added to the boot.link file to determine whether the stack overflows during the compilation stage. The system detects a default size of 600 bytes, which customers can modify according to the actual stack usage in their specific applications.
ASSERT(_ram_use_end_<(__SRAM_SIZE - 600), "Error: RAM size maybe overflow.");
Where “ram_use_end” is the end address of data_no_init.
When a stack overflow occurs, the error message displayed is:

The remaining portions of the stack/unused area remain unchanged from previous versions. For detailed information, please refer to the subsection “SRAM Space Allocation in SDK Versions Prior to V3.4.2.4”.
(7) text
The text section remains unchanged compared to the previous version. For detailed information, please refer to the section "SRAM Space Allocation for SDK Versions Prior to V3.4.2.4".
(8) rodata / data init value
The rodata and data init value sections remain unchanged compared to the previous version. For detailed information, please refer to the section "SRAM Space Allocation for SDK Versions Prior to V3.4.2.4".
(9) sdk_version
The sdk_version section is a new section introduced in SDK V3.4.2.4, specifically used to store SDK version information within the bin file.
List File Analysis Demo
Here taking the 825x ble sample as an example and analyze it with "SRAM space allocation & Firmware space allocation for SDK v3.4.2.4 and later versions".
In the following analysis, there are several screenshots, all from boot.link, cstartup_825x.S, 825x ble sample.bin and 825x ble sample.lst, please find the file to find the corresponding location of the screenshots by yourself.
The distribution of each section in the list file is shown in the following figure (note the Algn byte alignment):

Based on the "list file section analysis" figure above, the analysis is as follows:
(1) vector:
The start address of the vector section in flash firmware is 0, the end address is 0x1b0, and the size is 0x1b0. After power-up relocation to SRAM, the address range in SRAM is 0x840000 ~ 0x8401b0.
(2) ram_code:
The start address of the ram_code section is 0x1b0, and the end address is 0x2b60. After power-up relocation to SRAM, the address range in SRAM is 0x8401b0 ~ 0x842b60.
(3) retention_data:
The start address _rstored_ of retention_data in flash is 0x2b60, which is the end of ram_code. Its start address in SRAM is 0x842b60, and the end address is 0x8438bc.
(4) Cache:
The address range of Cache in SRAM is 0x843900 ~ 0x844200. Note that the information regarding Cache is not reflected in the list file.
(5) text:
The start address of the text section in flash firmware is 0x3900, the end address is 0xc78c, and the size is 0x8e8c, which is consistent with the section statistics mentioned above.
(6) rodata:
The start address of the rodata section is 0xc78c (the end address of text), and the end address is 0xd3d0.
(7) data:
The start address of the data section in SRAM is 0x844200 (the end address of Cache). The size given in the Section statistics part of the list file is 0x40.
The end address of this section in SRAM is 0x844240.
(8) bss:
The start address of the bss section in SRAM is 0x844240 (the end address of the data section, 16-byte aligned). The size given in the Section statistics part of the list file is 0x204.
The end address of the bss section in SRAM is 0x844444.
(9) data_no_init:
The start address of the data_no_init section in SRAM is 0x844444 (the end address of the bss section). The size given in the Section statistics part of the list file is 0x300.
(10) sdk_version:
The start address of the sdk_version section in SRAM is 0x844744 (the end address of the data_no_init section). The size given in the Section statistics part of the list file is 0x30.
The end address of the sdk_version section in SRAM is 0x844774. Since sdk_version has no substantial function during actual program execution, this area can be occupied.
The remaining SRAM space is 0x850000 – 0x844744 = 0xb8bc = 47292 Bytes. Subtracting the 600 Bytes required for the stack (this value is for example only; the actual size needs to be confirmed by the customer based on the actual application), 46692 Bytes remain.
MCU Address Space Access
Access to the 0x000000 - 0xFFFFFF address space in the program is divided into the following two situations.
Peripheral Space R/W Operation
Read and write operations in the peripheral space (register and SRAM) are implemented directly with pointer access.
u8 x = *(volatile u8*)0x800066; //read value of register 0x66
*(volatile u8*)0x800066 = 0x26; //assign value to register 0x66
u32 y = *(volatile u32*)0x840000; //read value of SRAM 0x40000-0x40003
*(volatile u32*)0x840000 = 0x12345678; //assign value to SRAM 0x40000-0x40003
The program uses the functions write_reg8, write_reg16, write_reg32, read_reg8, read_reg16, read_reg32 to read and write to the peripheral space, which are essentially pointer operations. For more information, please refer to drivers/8258/bsp.h.
Note the operation similar to write_reg8(0x40000)/ read_reg16(0x40000) in the program, which is defined as shown below, from which the 0x800000 offset is automatically added (address line BIT(23) is 1), so the MCU can ensure that it is accessing the Register/SRAM space and not going to flash space.
#define REG_BASE_ADDR 0x800000
#define write_reg8(addr,v) U8_SET((addr + REG_BASE_ADDR),v)
#define write_reg16(addr,v) U16_SET((addr + REG_BASE_ADDR),v)
#define write_reg32(addr,v) U32_SET((addr + REG_BASE_ADDR),v)
#define read_reg8(addr) U8_GET((addr + REG_BASE_ADDR))
#define read_reg16(addr) U16_GET((addr + REG_BASE_ADDR))
#define read_reg32(addr) U32_GET((addr + REG_BASE_ADDR))
Note here a memory alignment problem: If you use a pointer to 2 bytes/4 bytes to read or write peripheral space, make sure the address is 2 bytes/4 bytes aligned, if not aligned, data read/write errors occur. The following two are errors.
u16 x = *(volatile u16*)0x840001; //0x840001 is not 2-byte aligned
*(volatile u32*)0x840005 = 0x12345678; //0x840005 is not 4-byte aligned
Modify to the correct read/write operation.
u16 x = *(volatile u16*)0x840000; //0x840000 is 2-byte aligned
*(volatile u32*)0x840004 = 0x12345678; //0x840004 is 4-byte aligned
Flash Operation
This section is detailed in Chapter 8, Flash.
SDK Flash Space Allocation
This section is detailed in Chapter 8, Flash.
Clock Module
System clock & System Timer
The system clock is the clock used by the MCU to execute the program.
The system timer is a read-only timer that provides a time reference for timing control of the BLE and is also available to the user.
On the B85, B87, and TC321x series ICs, the System Timer and system clock are independent and separate.

As you can see, the system clock can be multiplied to 48M by the external 24MHz crystal oscillator through the "doubler" circuit and then divided to get 16MHz, 24MHz, 32MHz, 48MHz, etc. This type of clock is called crystal clock (such as 16MHz crystal system clock, 24M crystal system clock). It can also be processed by the IC internal 24MHz RC Oscillator to get 24MHz RC clock, 32MHz RC clock, 48MHz RC clock and so on. This category is called RC clock (BLE SDK does not support RC clock). In the BLE SDK we recommend to use crystal clock.
To configure the system clock, call the following API during initialization and select the clock corresponding to the clock in the enumeration variable SYS_CLK_TYPEDEF definition.
void clock_init(SYS_CLK_TYPEDEF SYS_CLK)
Since the System Timer of B85/B87/TC321x series chips is different from the system clock, users need to know whether the clock of each hardware module on MCU is from system clock or System Timer. Let's take the case where the system clock is 24M crystal to illustrate, at this time the system clock is 24MHz and the System Timer is 16MHz.
In the file app_config.h, the system clock and the corresponding s, ms and us are defined as follows.
#define CLOCK_SYS_CLOCK_HZ 24000000
s, ms and us:
enum{
CLOCK_SYS_CLOCK_1S = CLOCK_SYS_CLOCK_HZ,
CLOCK_SYS_CLOCK_1MS = (CLOCK_SYS_CLOCK_1S / 1000),
CLOCK_SYS_CLOCK_1US = (CLOCK_SYS_CLOCK_1S / 1000000),
};
All hardware modules whose clock source is system clock can only use the above CLOCK_SYS_CLOCK_HZ, CLOCK_SYS_CLOCK_1S, etc. when setting the clock of the module; in other words, if the user sees that the clock setting in the module uses the above definitions, it means that the clock source of the module is system clock.
If the PWM driver PWM period and duty cycle are set as follows, it means that the PWM clock source is system clock.
pwm_set_cycle_and_duty(PWM_ID, 1000 * CLOCK_SYS_CLOCK_1US, 500 * CLOCK_SYS_CLOCK_1US);
The System Timer is a fixed 16MHz, so for this timer, the SDK code uses the following values for s, ms and us.
/**
* @brief system Timer : 16Mhz, Constant
*/
enum{
CLOCK_16M_SYS_TIMER_CLK_1S = 16*1000*1000,
CLOCK_16M_SYS_TIMER_CLK_1MS = 16*1000,
CLOCK_16M_SYS_TIMER_CLK_1US = 16,
};
The following APIs in SDK are some operations related to System Timer, so when it comes to these API operations, they all use the above similar to "CLOCK_16M_SYS_TIMER_CLK_xxx" to represent the time.
void sleep_us(unsigned long us);
unsigned long clock_time(void);
unsigned int clock_time_exceed(unsigned int ref, unsigned int us);
#define ClockTime clock_time
#define WaitUs sleep_us
#define WaitMs(t) sleep_us((t)*1000)
#define sleep_ms(t) sleep_us((t)*1000)
Since the System Timer is the reference for BLE timing, all BLE time-related parameters and variables in the SDK are expressed as CLOCK_16M_SYS_TIMER_CLK_xxx when it comes to the time.
System Timer Usage
After the initialization of sys_init in the main function is completed, the System Timer starts to work, and the user can read the value of the System Timer counter (referred to as System Timer tick).
The System Timer tick is incremented by one every clock cycle, and its length is 32bit, that is, every 1/16 us plus 1, the minimum value is 0x00000000, and the maximum value is 0xffffffff. When the System Timer starts, the tick value is 0, and the time required to reach the maximum value of 0xffffffff is: (1/16) us * (2^32) approximately equal to 268 seconds, and the System Timer tick makes one cycle every 268 seconds.
The system tick does not stop when the MCU is running the program.
The reading of System Timer tick can be obtained through the clock_time() function:
u32 current_tick = clock_time();
The entire BLE timing of the BLE SDK is designed based on the System Timer tick. This System Timer tick is also used extensively in the program to complete various timing and timeout judgments. It is strongly recommended that users use this System Timer tick to implement some simple timing and timeout judgments.
For example, to implement a simple software timing. The realization of the software timer is based on the query mechanism. Because it is implemented through query, it cannot guarantee real-time performance and readiness. It is generally used for applications that are not particularly demanding on error. Implementation:
(1) Start timer: set a u32 variable, read and record the current System Timer tick.
u32 start_tick = clock_time(); // clock_time() returns System Timer tick value
(2) Constantly inquire whether the difference between the current System Timer tick and start_tick exceeds the time value required for timing in the program. If it exceeds, consider that the timer is triggered, perform corresponding operations, and clear the timer or start a new round of timing according to actual needs.
Assuming that the time to be timed is 100 ms, the way to query whether the time is reached is:
if( (u32) ( clock_time() - start_tick) > 100 * CLOCK_16M_SYS_TIMER_CLK_1MS)
Since the difference is converted to the u32 type, the limit of the system clock tick from 0xffffffff to 0 is solved.
In fact, in order to solve the problem of conversion to u32 caused by different system clocks, the SDK provides a unified calling function. Regardless of the system clock, the following functions can be used to query and judge:
if( clock_time_exceed(start_tick, 100 * 1000)) //unit of the second parameter is us
Please be noted: since the 16MHz clock takes 268 seconds for one cycle, this query function is only applicable to the timing within 268 seconds. If it exceeds 268 seconds, you need to add a counter to accumulate in the software (not introduced here).
Application example: after 2 seconds when A condition is triggered (only once), the program performs B() operation.
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 Module
Please refer to gpio.h, gpio_default.h, and gpio.c to understand the GPIO module, as all code is provided in source format.
Regarding the register operations involved in the code, please refer to the GPIO Lookup Table in the datasheet.
GPIO Definition
The B85/B87 series chips have a total of 5 groups with 36 GPIOs: GPIO_PA0 ~ GPIO_PA7, GPIO_PB0 ~ GPIO_PB7, GPIO_PC0 ~ GPIO_PC7, GPIO_PD0 ~ GPIO_PD7, and GPIO_PE0 ~ GPIO_PE3.
The GPIO configuration for the TC321x chip is as follows: there are 6 groups of GPIOs, consisting of GPIO_PA0 ~ GPIO_PA7, GPIO_PB0 ~ GPIO_PB7, GPIO_PC0 ~ GPIO_PC7, GPIO_PD0 ~ GPIO_PD7, GPIO_PE0 ~ GPIO_PE1, and GPIO_PF0 ~ GPIO_PF3.
Note
- There are 36 GPIOs in the core part of the IC, however some GPIOs may not be pinned out in the different packages of each IC, therefore please refer to the actual GPIO pins in the package of the IC when using GPIOs.
When you need to use GPIO in your program, define it as written above, see gpio.h for details.
Note
There are 7 special GPIOs that require attention when used:
(1) 4 GPIOs for MSPI. These 4 GPIOs belong to the Master SPI bus in the MCU system bus, specifically used for Flash read/write operations. They default to the SPI state after power-up. Users are strictly prohibited from operating these pins, and they should not be used in the program.
- B85/B87: PE0, PE1, PE2, PE3
- TC321X: PF0, PF1, PF2, PF3
(2) SWS (Single Wire Slave). This pin is used for Debugging and programming Firmware. It defaults to the SWS state after power-up and is usually not used as a regular GPIO in the program.
- The SWS pin for B85/B87 is PA7
- The SWS pin for TC321x is PA3
(3) DM and DP. They default to GPIO state after power-up. When USB function is required, DM and DP should be occupied; if USB function is not required, they can be used as normal GPIOs. The DM and DP pins for B85/B87 are PA5 and PA6 respectively, while TC321x does not support USB function.
Note on Special Pins for TC321x Series:
In addition to the special pins mentioned above, the TC321x series has the following pin limitations to note:
- PB0, PB1, PB3, PD4: These pins are not recommended as wake-up sources because they default to output functions, and pull-up/pull-down configurations may be ineffective.
- PF0-PF3: As MSPI pins, they cannot be used as normal GPIOs.
GPIO State Control
Only the most basic GPIO states that users need to know are listed here.
(1) func (function configuration: special function/general GPIO), if you need to use the input and output function, you need to configure it as general GPIO.
void gpio_set_func(GPIO_PinTypeDef pin, GPIO_FuncTypeDef func);
The pin is defined for GPIO, the same as below. For func you can choose AS_GPIO or other special functions.
(2) ie (input enable)
void gpio_set_input_en(GPIO_PinTypeDef pin, unsigned int value);
value: 1 and 0 means enable and disable respectively.
(3) datai (data input): When the input enable is on, this value is the current level of this GPIO pin, which is used to read the external voltage.
_Bool gpio_read(GPIO_PinTypeDef pin);
Reading a low voltage returns a value of 0; reading a high voltage returns a non-zero value. Be very careful here, when reading high, the return value is not necessarily 1, it is a non-0 value.
Therefore, in the program, you should avoid using code like if( gpio_read(GPIO_PA0) == 1). The recommended approach is to perform inverse processing on the read value, ensuring the result is strictly either 1 or 0:
if( !gpio_read(GPIO_PA0) ) //determine high or low level
(4) oe (output enable)
void gpio_set_output_en(GPIO_PinTypeDef pin, unsigned int value);
value: 1 and 0 means enable and disable respectively.
(5) dataO (data output): When the output enable is on, the value is 1 to output high, 0 to output low.
void gpio_write(GPIO_PinTypeDef pin, unsigned int value)
(6) Internal analog pull-up/pull-down resistor configuration: There are 3 types of analog resistors available: 1 M\(\Omega\) pull-up, 10 k\(\Omega\) pull-up, and 100 k\(\Omega\) pull-down. There are 4 configurable states: 1 M\(\Omega\) pull-up, 10 k\(\Omega\) pull-up, 100 k\(\Omega\) pull-down, and float state.
void gpio_setup_up_down_resistor( GPIO_PinTypeDef gpio, GPIO_PullTypeDef up_down);
The four configurations of 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;
In the deepsleep and deepsleep retention states, the GPIO input and output states are all disabled, but the analog pull-up and pull-down resistors are still valid.
Note
- The following sequence is required when making GPIO function change configurations.
(A)The beginning function is GPIO, then you need to configure the required function MUX first, and then disable the GPIO function.
(B)The beginning function is IO, you need to change to GPIO output, first set the corresponding IO output value and OEN, and then finally enable GPIO function.
(C)The beginning function is IO, you need to change to GPIO input and IO pullup, first set output to 1, OEN to 1 (corresponding to PA and PD), second set pullup to 1 (corresponding to PB and PC), and finally enable GPIO function.
(D)Set pullup to 1 (corresponding to PB and PC) and IO not pull up, first set output to 0, OEN to 1 (corresponding to PA and PD), then set pullup to 0 (corresponding to PB and PC), and finally enable GPIO function.
GPIO configuration application examples:
(1) Configure GPIO_PA4 as output state and output high level.
gpio_set_func(GPIO_PA4, AS_GPIO) ; // PA4 defaults to GPIO functionality and requires no configuration.
gpio_set_input_en(GPIO_PA4, 0);
gpio_set_output_en(GPIO_PA4, 1);
gpio_write(GPIO_PA4, 1);
(2) Configure GPIO_PC6 as input state to determine whether it reads low and needs to turn on pull-up to prevent the effect of float level.
gpio_set_func(GPIO_PC6, AS_GPIO) ; // PC6 is GPIO by default, you can leave it
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)){ //whether low level
......
}
(3) Configure PA5 and PA6 pins for USB function.
gpio_set_func(GPIO_PA5, AS_USB ) ;
gpio_set_func(GPIO_PA6, AS_USB) ;
gpio_set_input_en(GPIO_PA5, 1);
gpio_set_input_en(GPIO_PA6, 1);
Note
- The above USB function configurations are only applicable to B85/B87 series chips; the TC321x series does not support USB function.
GPIO Initialization
Calling the gpio_init function in main.c initializes the state of all 32 GPIOs except for the 4 GPIOs of the MSPI.
This function initializes each IO to its default state when no GPIO parameters are configured in the user's app_config.h. The default states of the 32 GPIOs are:
(1) func
Except SWS, all other states are general GPIOs.
(2) ie
Except the default ie for SWS is 1, the default ie for all other general GPIOs is 0.
(3) oe
All is 0.
(4) dataO
All is 0.
(5) Internal pull up/down resistors
All is float.
For more details, please refer to drivers/8258/ gpio_8258.h, drivers/8258/ gpio_default_8258.h and drivers/8278/ gpio_8278.h, drivers/8278/ gpio_default_8278.h.
If there is a state configured in app_config.h to one or more GPIOs, then gpio_init no longer uses the default state, but the state configured by the user. The reason for this is that the default state of gpio is represented using macros that are written (using PA0's ie as an example) as follows:
#ifndef PA0_INPUT_ENABLE
#define PA0_INPUT_ENABLE 1
#endif
When these macros can be defined in advance in app_config, these macros no longer use such default values as above.
The method to configure the GPIO state in app_config.h is (using PA0 as an example):
(1) Configure func:
#define PA0_FUNC AS_GPIO
(2) Configure ie:
#define PA0_INPUT_ENABLE 1
(3) Configure oe:
#define PA0_OUTPUT_ENABLE 0
(4) Configure dataO:
#define PA0_DATA_OUT 0
(5) Configure internal pull up/down resistors:
#define PULL_WAKEUP_SRC_PA0 PM_PIN_UP_DOWN_FLOAT
Summary of GPIO initialization:
(1) The initial state of GPIOs can be pre-defined in the header file and is configured in gpio_init.
(2) It can be set in user_init function by GPIO state control function (gpio_set_input_en, etc.).
(3) A mix of the above two ways can also be used: define some in app_config.h in advance, implement them in gpio_init, and set the rest in user_init.
Note
- If a state of the same GPIO is set to a different value in the header file and
user_init, the setting inuser_initprevails according to the order of program execution.
The gpio_init function is implemented as follows. The value of anaRes_init_en determines whether the analog pull-up and pull-down resistors are set.
void gpio_init(int anaRes_init_en)
{
// gpio digital status setting
if(anaRes_init_en){
gpio_analog_resistance_init();
}
}
As indicated in the low-power management section, the registers controlling GPIO analog pull-up/pull-down resistors remain powered during deepsleep retention. Consequently, the state of the GPIO analog pull-up/pull-down resistors is maintained in deepsleep retention mode, and their configuration can be skipped upon wakeup from deepsleep retention.
In order to ensure that the state of the GPIO analog pull-up and pull-down resistors is not changed after the deepsleep retention wakeup, it is necessary to determine whether the current deepsleep retention wake_up before gpio_init, and set the value of anaRes_init_en according to this state, as shown in the following code.
int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();
gpio_init( !deepRetWakeUp );
GPIO Digital States Fail in Deepsleep Retention Mode
In the GPIO state control described above, all the states (func, ie, oe, dataO, etc.) are controlled by the digital register, except for the analog pull-down resistor which is controlled by the analog register.
Referring to the introduction of low-power management later in the document, it is clear that all digital register states are lost during deepsleep retention.
On B85/B87/TC321x, if suspend is switched to deepsleep retention mode, GPIO output state is disabled and cannot accurately control peripheral devices during sleep. At this point, the state of GPIO analog pull-up and pull-down resistors can be used instead: pull-up 10K instead of GPIO output high, pull-down 100K instead of GPIO output low.
Note
- Do not use pull-up 1M for GPIO state control during deepsleep retention (the pull-up voltage may be lower than the supply voltage VCC). In addition, do not use the pull-up 10K of PC0~PC7 in the pull-up 10K control (there is a short time jitter in the deepsleep retention wake_up, generating glitches), pull-up 10K for other GPIO is fine.
Configure SWS Pull-ups to Prevent Crashes
All of Telink's MCUs use SWS (single wire slave) to debug and burn in programs. On the final application code, the state of the pin SWS is:
(1) function set to SWS, not GPIO.
(2) ie =1, only when input enable, it can receive various commands sent by EVK, which is used to operate MCU.
(3) Other configurations: oe, dataO are 0.
After setting to the above state, it can receive operation commands from EVK at any time, but it also brings a risk: when the power supply of the whole system is very jittered (such as when sending IR, the instantaneous current may rush to nearly 100mA), as SWS is in float state, it may read a wrong data and mistake it for a command from EVK, and this wrong command may cause the program stuck.
The solution to the above problem is to modify the float state of the SWS to input pull-up. This is solved by an analog pull-up 1M resistor.
The SWS and GPIO_PA7 are multiplexed on the B85/B87 series. Simply enable the 1M ohm pull-up for PA7 in drivers/B85/gpio_default.h or drivers/B87/gpio_default.h.
#ifndef PULL_WAKEUP_SRC_PA7
#define PULL_WAKEUP_SRC_PA7 PM_PIN_PULLUP_1M //sws pullup
#endif
The SWS and GPIO_PA3 are multiplexed on the TC321x series. Simply enable the 1M ohm pull-up for PA3 in drivers/TC321X/gpio_default.h.
#ifndef PULL_WAKEUP_SRC_PA3
#define PULL_WAKEUP_SRC_PA3 PM_PIN_PULLUP_1M //sws pullup
#endif
System Interrupt
This document applies to hardware interrupts of ICs with the following two characteristics.
(1)All interrupts have the same priority, and the MCU does not have the ability to nest interrupts;
(2)All interrupts share the same interrupt hardware entry, which eventually triggers the software irq_handler function, in which the function reads the status bits of the relevant interrupt to determine whether the corresponding interrupt is triggered.
The feature 1 above determines that the MCU responds to interrupts on a first-come, first-served basis. When the first interrupt is not processed, a new interrupt is generated and cannot be responded to immediately and enters the waiting queue until the previous interrupts are processed. Therefore, when there are 2 or more interrupts, all interrupts cannot be responded in real time. The response delay of a particular interrupt depends on whether the MCU is processing other interrupts when this interrupt is triggered and how long it takes to process the other interrupts. As shown in the figure below, since IRQ1 is processing when IRQ2 is triggered, it should wait until IRQ1 is finished processing before responding. The worst case of IRQ2 delay time is the maximum time of IRQ1 process.

In the BLE SDK, two system interrupts, system timer and RF, are used. If the user does not add new interrupts, there is no need to consider the timing of the two system interrupts; if the customer needs to add other interrupts (e.g. UART, PWM, etc.), the details to be considered are as follows.
(1) For the two system interrupts system timer and RF in the SDK, the maximum possible execution time is 200us. This means that the customer added interrupts may not be able to respond in real time, and the theoretical maximum possible delay time is 200us.
(2) The two system interrupts system timer and RF are for processing BLE tasks, due to the BLE timing is more strict, can not be delayed too long. Therefore, the processing time of the interrupts added by the customer should not be too long, and it is recommended to be within 50us. If the time is too long, there may be BLE timing synchronization errors, resulting in low efficiency of sending and receiving packets, high power consumption, BLE disconnection and other problems.
BLE Module
BLE SDK Software Architecture
Standard BLE SDK Architecture
Figure below shows standard BLE SDK software architecture compliant with BLE spec.

As shown above, BLE protocol stack includes Host and Controller.
-
As BLE bottom-layer protocol, the “Controller” contains Physical Layer (PHY) and Link Layer (LL). Host Controller Interface (HCI) is the sole communication interface for all data transfer between Controller and Host.
-
As BLE upper-layer protocol, the “Host” contains protocols including Logic Link Control and Adaption Protocol (L2CAP), Attribute Protocol (ATT), Security Manager Protocol (SMP), as well as Profiles including Generic Access Profile (GAP) and Generic Attribute Profile (GATT).
-
The “Application” (APP) layer contains user application codes and Profiles corresponding to various Services. User controls and accesses Host via “GAP”, while Host transfers data with Controller via “HCI”, as shown below.

(1) BLE Host will use HCI cmd to operate and set Controller. Controller API corresponding to each HCI cmd will be introduced in this chapter.
(2) Controller will report various HCI Events to Host via HCI.
(3) Host will send target data to Controller via HCI, while Controller will directly load data to Physical Layer for transfer.
(4) When Controller receives RF data in Physical Layer, it will first check whether the data belong to Link Layer or Host, and then process correspondingly: If the data belong to LL, the data will be processed directly; if the data belong to Host, the data will be sent to Host via HCI.
Telink BLE SDK Architecture
Telink BLE Controller
Telink BLE SDK supports standard BLE controllers, including HCI, PHY (Physical Layer) and LL (Link layer).
Telink BLE SDK includes five standard states of Link Layer (standby, advertising, scanning, initiating, connection), and both Slave role and Master role are supported in the connection state. In TC BLE Single Connection SDK, Slave role and Master role are only single connection, that is, Link Layer can only maintain one connection, not multiple Slave/Master or Slave and Master at the same time.
The ble HCI in the SDK is a BLE slave controller, which needs to coordinate with another MCU running BLE Host to form a standard BLE Slave system, the architecture diagram is as follows.

Link Layer connection status supports both Slave and Master of single connection, then ble HCI can actually be used as BLE master controller. However, actually for a BLE host running on more complex systems (such as Linux/Android), a single connection master controller can only connect to one device, which is almost meaningless, so the SDK does not include the initialization of the master role in the ble HCI.
Telink BLE Slave
Telink BLE SDK in BLE host fully supports stack of Slave; for the Master, it can not fully support, because SDP (service discovery) is too complex.
When user only needs to use standard BLE Slave, and Telink BLE SDK runs Host (Slave part) + standard Controller, the actual stack architecture will be simplified based on the standard architecture, so as to minimize system resource consumption of the whole SDK (including SRAM, running time, power consumption, and etc.). Following shows Telink BLE Slave architecture. In the SDK, ble sample, ble remote and ble module are based on this architecture.

In figure above, solid arrows indicate data transfer controllable via user APIs, while hollow arrows indicate data transfer within the protocol stack not involved in user.
Controller can still communicate with Host (L2CAP layer) via HCI; however, the HCI is no longer the only interface, and the APP layer can directly exchange data with Link Layer of the Controller. Power Management (PM) Module is embedded in the Link Layer, and the application layer can invoke related PM interfaces to set power management.
Considering efficiency, data transfer between the APP layer and the Host is not controlled via GAP; the ATT, SMP and L2CAP can directly communicate with the APP layer via corresponding interface. However, the event of the Host should be communicated with the APP layer via the GAP layer.
Generic Attribute Profile (GATT) is implemented in the Host layer based on Attribute Protocol. Various Profiles and Services can be defined in the APP layer based on GATT. Basic Profiles including HIDS, BAS, AUDIO and OTA are provided in demo code of this BLE SDK.
Following sections explain each layer of the BLE stack according to the structure above, as well as user APIs for each layer.
Physical Layer is totally controlled by Link Layer, since it does not involve the application layer, it is not covered in this document.
Though HCI still implements part of data transfer between Host and Controller, it is basically implemented by the protocol stack of Host and Controller with little involvement of the APP layer. User only needs to register HCI data callback handling function in the L2CAP layer.
Telink BLE Master
The implementation of Telink BLE master is different from that of Slave. The SDK provides standard controller packed inside the library, but the app layer implements host and user's own application, as shown in the figure below.

In the ble master kma dongle project of the SDK, the demo code is implemented based on this architecture, the host layer code is almost all implemented in the app layer. The SDK provides a variety of standard interfaces for users to complete these functions.
The App layer implements standard L2CAP, ATT, and other processing. In the SMP part, it only provides the most basic LE legacy pairing - Just Works method. Due to the complexity of SMP implementation, the specific code implementation is encapsulated in the library; the App layer only needs to call the relevant interfaces. Users can find all the code processing by searching for BLE_HOST_SMP_ENABLE.
#define BLE_HOST_SMP_ENABLE 1 //Master SMP strongly recommended enabled
The SDP is the most complex part, Telink BLE master does not provide a standard SDP, only a simple reference is given to the ble remote service discovery. The ble master kma dongle default this simple reference SDP is on.
#define ACL_CENTRAL_SIMPLE_SDP_ENABLE 1 //simple service discovery
The SDK provides standard interfaces for all service discovery related ATT operations, users can refer to ble remote's service discovery to implement their own service discovery, or disable BLE_HOST_SIMPLE_SDP_ ENABLE and use the agreed service ATT handle with the slave to achieve data access.
The Telink BLE master does not support power management; the scanning and connection master roles at the link layer are not suspended.
BLE Controller
BLE Controller Introduction
BLE Controller contains Physical Layer, Link Layer, HCI and Power Management.
Telink BLE SDK fully packs Physical Layer in the library (corresponding to c file of rf.h in driver file), and user does not need to learn about it. Power Management will be introduced in detail in section 4 Low Power Management (PM).
This section will focus on Link Layer, and also introduce HCI related interfaces to operate Link Layer and obtain data of Link Layer.
Link Layer State Machine
Figure below shows Link Layer state machine in BLE spec. For more information, please refer to Bluetooth Core Specification v5.3 [Vol 6] Part B, Section 1.1 LINK LAYER STATES.

Telink BLE SDK Link Layer state machine is shown as below.

Telink BLE SDK Link Layer state machine is consistent with BLE spec, and it contains five basic states: Idle (Standby), Scanning, Advertising, Initiating, and Connection. Connection state contains Slave Role and Master Role.
From the introduction of library earlier in the document, the current Slave Role and Master Role are both designed based on single connection. To distinguish multi connection, it is named Slave role single connection and Mater Role single connection.
In this document, Slave Role is marked as "ConnSlaveRole" for short; while Master Role is marked as "ConnMasterRole" for short.
The "Power Management" in figure above is not a state of LL, but a functional module which indicates the SDK only implements low power processing for Advertising and ConnSlaveRole. If Idle state needs low power, user can invoke related APIs in the APP layer. For the other states, the SDK does not contain low power management, and user cannot implement low power in the APP layer.
Based on the five states above, corresponding state machine names are defined in "stack/ble/controller/ll/ll.h". "ConnSlaveRole" and "ConnMasterRole" correspond to the state name "BLS_LINK_STATE_CONN".
#define BLS_LINK_STATE_IDLE 0
#define BLS_LINK_STATE_ADV BIT(0)
#define BLS_LINK_STATE_SCAN BIT(1)
#define BLS_LINK_STATE_INIT BIT(2)
#define BLS_LINK_STATE_CONN BIT(3)
Switch of Link Layer state machine is automatically implemented in BLE stack bottom layer. Therefore, user cannot modify state in APP layer, but can obtain current state by invoking the API below. The return value will be one of the five states.
u8 blc_ll_getCurrentState(void);
Link Layer State Machine Combined Application
Link Layer State Machine Initialization
The Telink BLE SDK Link Layer fully supports all states, but features a flexible design where each state is encapsulated as a module. By default, only the basic Idle module is included; users establish their own state machine combinations by adding modules to implement different applications. For example, for a BLE Slave application, the user only needs to add the Advertising module and ConnSlaveRole module, while the remaining Scanning/Initiating modules do not need to be configured. This design aims to save code size and ram_code, as code for unused states is not compiled.
The MCU initialization is mandatory, and the initialization API is as follows:
void blc_ll_initBasicMCU (void);
The API below serves to add the basic Idle module. This API is also necessary for all BLE applications.
void blc_ll_initStandby_module (u8 *public_adr);
The initialization APIs of the corresponding modules for several other states (Scanning, Alerting, Initiating, Slave Role, Master Role Single Connection) are as follows.
void blc_ll_initAdvertising_module(u8 *public_adr);
void blc_ll_initScanning_module(u8 *public_adr);
void blc_ll_initInitiating_module(void);
void blc_ll_initConnection_module(void);
void blc_ll_initSlaveRole_module(void);
void blc_ll_initMasterRoleSingleConn_module(void);
The real parameter public_adr in the above API is a pointer to the BLE public Mac Address.
The following API is used to initialize the module shared by master and slave.
void blc_ll_initConnection_module(void);
Users can use the above APIs to combine the Link Layer state machine. The following are some common combinations and corresponding application scenarios, but they are not limited to these examples; users can configure their own combinations.
Idle + Advertising

As shown above, only Idle and Advertising module are initialized, and it applies to applications which use basic advertising function to advertise product information in single direction, e.g. beacon.
Following is module initialization code of Link Layer state machine.
u8 mac_public[6] = {……};
blc_ll_initBasicMCU();
blc_ll_initStandby_module(mac_public);
blc_ll_initAdvertising_module(mac_public);
State switch of Idle and Advertising is implemented via the “bls_ll_setAdvEnable”.
Idle + Scanning
As shown in the figure below, only the Idle and Scanning modules are initialized, and the most basic Scanning function is used to achieve the scanning and discovery of beacons and other product broadcast information.
The Link Layer state machine module initialization code is:
u8 mac_public[6] = {……};
blc_ll_initBasicMCU();
blc_ll_initStandby_module(mac_public);
blc_ll_initScanning_module(mac_public);
The switching of Idle and Scanning states is implemented by "blc_ll_setScanEnable".

Idle + Advertising + ConnSlaveRole

The figure above shows the Link Layer state machine combination for a basic BLE slave application. The ble HCI/ble sample/ble remote/ble module in the SDK are all based on this state machine combination.
The Link Layer state machine module is initialized with the following code.
u8 mac_public[6] = {……};
blc_ll_initBasicMCU();
blc_ll_initStandby_module(mac_public);
blc_ll_initAdvertising_module(mac_public);
blc_ll_initConnection_module();
blc_ll_initSlaveRole_module();
State switch in this combination is shown as below:
(1) After power on, the MCU enters Idle state. In Idle state, when Advertising is enabled, Link Layer switches to Advertising state; when Advertising is disabled, it will return to Idle state.
The API “bls_ll_setAdvEnable” serves to enable/disable ADV.
After power on, Link Layer is in Idle state by default. Typically it’s needed to enable ADV in the “user_init” so as to enter Advertising state.
(2) When Link Layer is in Idle state, Physical Layer does not take any RF operation including packet transmission and reception.
(3) When Link Layer is in Advertising state, advertising packets are transmitted in ADV channels. Master will send Connection Indication if it receives ADV packet. After Link Layer receives this Connection Indication, it will respond, establish connection and enter ConnSlaveRole.
(4) When Link Layer is in ConnSlaveRole, it will return to Idle State or Advertising state in any of the following cases:
-
Master sends “terminate” command to Slave and requests disconnection. Slave will exit ConnSlaveRole after it receives this command.
-
By sending “terminate” command to Master, Slave actively terminates the connection and exits ConnSlaveRole.
-
If Slave fails to receive any packet due to Slave RF Rx abnormity or Master Tx abnormity until BLE connection supervision timeout is triggered, Slave will exit ConnSlaveRole.
When ConnSlaveRole of Link layer exits this state, it switches to a different state depending on whether ADV is enabled or not: if ADV is enabled, Link Layer goes back to Advertising state; if ADV is Disable, Link Layer goes back to Idle state. Whether ADV is Enable or Disable depends on the value set by the user when bls_ll_setAdvEnable was last called by the application layer.
Idle + Scanning + Initiating + ConnMasterRole

The above figure shows the Link Layer state machine combination for a basic BLE master application, the ble master kma dongle in the SDK is based on this state machine combination. The Link Layer state machine module initialization code is.
u8 mac_public [6] = {……};
blc_ll_initBasicMCU();
blc_ll_initStandby_module(mac_public);
blc_ll_initScanning_module(mac_public);
blc_ll_initInitiating_module();
blc_ll_initConnection_module();
blc_ll_initMasterRoleSingleConn_module();
The state change under this state machine combination is described as follows.
(1) After the MCU is powered on, it enters the Idle state; Scan is enabled in the Idle state, and the Link Layer is switched to the Scanning state; when Scan is disabled in the Scanning state, it returns to the Idle state.
The Scan Enable and Disable are controlled by API blc_ll_setScanEnable.
After power on, the Link layer is in Idle state by default. It is generally necessary to set Scan Enable inside user_init to enter Scanning state.
When the Link Layer is in the Scanning state, it reports the scanned Advertising packets to the BLE Host via the HCI event "HCI_SUB_EVT_LE_ADVERTISING_REPORT".
(2) In Idle state and Scanning state, Link Layer can trigger API blc_ll_createConnection to enter Initiating state.
The blc_ll_createConnection specifies the Mac Address of one or more BLE devices that need to be connected. After the Link Layer enters the Initiating state, it continuously scans the specified BLE device, sends a connection request and enters the ConnMasterRole after receiving a correct ADV packet that can be connected. If the initiating state does not scan the specified BLE device within a certain period of time and cannot initiate a connection, it will trigger a create connection timeout and revert to Idle State or Scanning state.
Note
- Initiating state can enter from Idle state and Scanning state (ble master kma dongle enters directly from Scanning state), create connection timeout and then return to the Idle State or Scanning state before create connection.
(3) When the Link Layer is in ConnMasterRole, it returns to Idle State in one of three ways:
a) The slave sends a terminate command to the master to disconnect. The master receives the terminate command and exits ConnMasterRole. b) The master sends a terminate command to the slave, actively disconnects, and exits ConnMasterRole. c) The master's RF packet receiving exception or the slave's packet sending exception causes the master to receive no packet for a long time, triggering the BLE's connection supervision timeout and exiting the ConnMasterRole.
If the Link layer's ConnMasterRole exits this state, it can only return to the Idle state. if it needs to continue scanning, it should use the API blc_ll_setScanEnable to set the Link Layer to enter the Scanning state again.
Link Layer Timing Sequence
In this section, Link Layer timing sequence in various states will be illustrated combining with irq_handler and main_loop of this BLE SDK.
_attribute_ram_code_ void irq_handler(void)
{
……
irq_blt_sdk_handler ();
……
}
void main_loop (void)
{
///////////////////// BLE entry ////////////////////////////
blt_sdk_main_loop();
////////////////////// UI entry ////////////////////////////
……
}
The “blt_sdk_main_loop” function at BLE entry serves to process data and events related to BLE protocol stack. UI entry is for user application code.
Timing Sequence in Idle State
When Link Layer is in Idle state, no task is processed in Link Layer and Physical Layer; the “blt_sdk_main_loop” function does not act and does not generate any interrupt, i.e. the whole timing sequence of main_loop is occupied by UI entry.
Timing Sequence in Advertising State

As shown in the figure above, an ADV event is triggered by the Link Layer during each ADV interval. A typical ADV event with three active ADV channels sends an advertising packet in channel 37, 38, and 39, respectively. After an ADV packet is sent, the Slave enters the Rx state and waits for a response from the Master:
-
If the Slave receives a scan request from the Master, it sends a scan response to the Master.
-
If the Slave receives a connect request from the Master, it establishes a BLE connection with the Master and enters the Connection state Slave Role.
The code for the UI entry in main_loop executes during the UI task/suspend period shown in the figure above. This duration can be used for UI tasks only, or the MCU can enter sleep (suspend or deep sleep retention) during the redundant time to reduce power consumption.
In the Advertising state, the blt_sdk_main_loop function does not process many tasks; only some callback events related to ADV are triggered, including BLT_EV_FLAG_ADV_DURATION_TIMEOUT, BLT_EV_FLAG_SCAN_RSP, BLT_EV_FLAG_CONNECT, etc.
Timing Sequence in Scanning State

Scan interval is configured by the API “blc_ll_setScanParameter”. During a whole Scan interval, packet reception is implemented in one channel, and Scan window is not designed in the SDK. Therefore, the SDK does not process the setting of Scan window in the “blc_ll_setScanParameter”.
After the end of each Scan interval, it will switch to the next receiving channel, and enters next Scan interval. Channel switch action is triggered by interrupt, and it’s executed in irq which takes very short time.
In Scanning interval, PHY Layer of Scan state is always in RX state, and it depends on MCU hardware to implement packet reception. Therefore, all timing in software are for UI task.
After correct BLE packet is received in Scan interval, the data are first buffered in software RX fifo (corresponding to “my_fifo_t blt_rxfifo” in code), and the “blt_sdk_main_loop” function will check whether there are data in software RX fifo. If correct ADV data are discovered, the data will be reported to BLE Host via the event “HCI_SUB_EVT_LE_ADVERTISING_REPORT”.
Timing Sequence in Initiating State

The timing sequence of the Initiating state is shown above, and is the same as that of the Scanning state, with the difference that the Scan interval is configured by the API "blc_ll_createConnection". Packet reception is performed on one channel during the entire Scan interval. Similarly, the SDK does not process the setting of the Scan window in "blc_ll_createConnection".
After the end of each Scan interval, it will switch to the next listening channel, and start a new Scan interval. Channel switch action is triggered by interrupt, and it’s executed in irq which takes very short time.
In the Scanning state, the BLE Controller reports the received ADV packet to the BLE Host; however, in the Initiating state, ADV packets are not reported to the BLE Host, and it only scans for the device specified by "blc_ll_createConnection". If the specific device is scanned, it sends a connection_request and establishes a connection, then the Link Layer enters the ConnMasterRole.
Timing Sequence in Conn State Slave Role

As shown in the above figure, each connection interval starts with a brx event, i.e. transfer process of BLE RF packets by Link Layer: PHY enters Rx state, and an ack packet will be sent to respond to each received data packet from Master. If there is more data, then continue to receive master packets and reply, this process is called brx event for short.
In this BLE SDK, each brx process consists of three phases according to the assignment of hardware and software.
(1) brx start phase
When Master is about to send packet, an interrupt is triggered by system tick irq to enter brx start phase. During this interrupt, MCU sets BLE state machine of PHY to enter brx state, hardware in bottom layer prepares for packet transfer, and then MCU exits from the interrupt irq.
(2) brx working phase
After brx start phase ends and MCU exits from irq, hardware in bottom layer enters Rx state first and waits for packet from Master. During the brx working phase, all packet reception and transmission are implemented automatically without involvement of software.
(3) brx post phase
After packet transfer is finished, the brx working phase is completed. System tick irq triggers an interrupt to switch to the brx post phase. During this phase, protocol stack will process BLE data and timing sequence according to packet transfer in the brx working phase.
During the three phases, brx start and brx post are implemented in interrupt, while brx working phase does not need the involvement of software, and UI task can be executed normally (Note that during brx working phase, UI task can be executed in the time slots except RX, TX, and System Timer interrupt handler). During the brx working phase, MCU can’t enter sleep (suspend or deep sleep retention) since hardware needs to transfer packets.
Within each connection interval, the duration except for brx event can be used for UI task only, or MCU can enter sleep (suspend or deep sleep retention) for the redundant time to reduce power consumption.
In the ConnSlaveRole, the "blt_sdk_main_loop" processes the data received during the brx process. During the brx working phase, the data packet received from the Master is copied out during the RX interrupt IRQ handler; this data is not processed immediately but is buffered in the software RX FIFO (corresponding to my_fifo_t blt_rxfifo in the code). The "blt_sdk_main_loop" function checks whether there is data in the software RX FIFO and processes the detected data packet accordingly.
The processing of packets by blt_sdk_main_loop includes:
(1) Decryption of data packet
(2) Parsing of data packet
If the parsed data belongs to the control command sent by Master to Link Layer, this command will be executed immediately; if it’s the data sent by Master to Host layer, the data will be transferred to L2CAP layer via HCI interface.
Timing Sequence in Conn State Master Role

The ConnMasterRole timing sequence is shown above. At the beginning of each conn interval, the Link Layer performs a BLE RF packet sending and receiving process: first the PHY enters the packet sending state, sends a packet to the slave and then waits for the other party's ack packet, if there is more data, it continues to send packets to the slave, this process is referred to as btx event.
In this BLE SDK, each brx process consists of three phases according to the assignment of hardware and software.
(1) btx start phase
When the time for master to send packets is approaching, it will be triggered by system tick irq to enter the btx start phase, in which the MCU sets the BLE state machine of the PHY to enter the btx state, and the bottom layer hardware prepares to send and receive packets, and then exits the interrupt irq.
(2) btx working phase
After btx start, the MCU exits irq and the bottom layer hardware enters the transmitting state and does all the work of sending and receiving packets automatically, without any software involvement, this process is called the btx working phase.
(3) btx post phase
After the packet is sent and received, btx working is finished and the system tick irq triggers the btx post phase. This phase is mainly for the protocol stack to process some data and timing of the BLE according to the btx working phase.
The btx start and btx post in the above three phases are both interrupted, while the btx working phase requires no software involvement and the UI task can be executed normally at this point.
At ConnMasterRole, blt_sdk_main_loop needs to process the data received by the btx process. The btx working process actually copies the slave packets received by the hardware during the RX receive interrupt irq processing, this data is not immediately processed in real time, but cached in the software RX fifo. The blt_sdk_main_loop function will check if there is data in the software RX fifo and process it as soon as it is available.
The processing of packets by blt_sdk_main_loop includes:
1) Decryption of data packet
2) Parsing of data packet
If the parsed data is found to belong to a control command sent by the slave to the Link Layer, the command will be executed immediately. If it is data sent by the master to the Host Layer, the data will be dropped to the L2CAP layer for processing through the HCI interface.
Timing Protect for Conn State Slave role
In ConnSlaveRole, each interval requires a send/receive packet event, which is the Brx Event above. In the B85m SDK, the Brx Event is triggered entirely by interrupts, so the MCU main system interrupt needs to be turned on all the time. If the user is in the Conn state for a long time and has to turn off the main system interrupt (e.g. to erase the Flash), the Brx Event will be stopped and the BLE timing will soon be messed up and eventually the connection will be disconnected.
In this situation, the SDK provides a protection mechanism that allows the user to disable the Brx Event without breaking the BLE timing, and the user needs to strictly follow this mechanism. The relevant API is as follows.
int bls_ll_requestConnBrxEventDisable(void);
void bls_ll_disableConnBrxEvent(void);
void bls_ll_restoreConnBrxEvent(void);
Call bls_ll_requestConnBrxEventDisable to request that the Brx Event be switched off.
(1) If the return value of this API is 0, it means that the user's application is not currently accepted, i.e. the Brx Event cannot be stopped at this time. During the Brx working phase at Conn state, the application cannot be accepted and the return value is 0. It should wait until the end of a full Brx Event to accept the application for the remaining UI task/suspend time.
(2) This API returns a non-zero value to indicate that the request can be accepted and the value returned is the time in ms allowed to stop the Brx Event. There are three cases for this event value:
a) If the current Link Layer is Alerting state or Idle state, the return value is 0xffff, that is, there is no Brx Event, and the user is allowed to turn off the system interrupt for any length of time. b) If the current Conn state receives an update map or update connection parameter from the master and has not yet reached the update time point, the return time is the update time point minus the current time. In other words, the time to stop the Brx Event cannot exceed the update time, otherwise all the packets is not received and the connection will be disconnected. c) If the current state is Conn state and there is no update request from master, the return value is half of the current connection supervision timeout value. For example, if the current timeout is 1s, the return value is 500ms.
The user calls the above API to request to disable the Brx Event, and if the return value corresponds to enough time for the task to run itself, the task can be performed. Before the task is executed, API bls_ll_disableConnBrxEvent is called to disable the Brx Event. After the task is finished, API bls_ll_restoreConnBrxEvent is called to re-enable the Brx Event and repair the BLE timing.
The reference usage is as follows. Where the specific time is judged by the actual time of tested task.

Link Layer State Machine Extension
The above BLE Link Layer state machine and working timings introduce the most basic states, which can satisfy the basic applications such as BLE slave/master. However, considering potential special user applications (such as the ability to perform advertising while in the ConnSlaveRole), the Telink BLE SDK adds special extension functions to the Link Layer state machine, which are introduced in detail below.
Scanning in Advertising state
When Link Layer is in Advertising state, the Scanning feature can be added.
The API to add Scanning feature:
ble_sts_t blc_ll_addScanningInAdvState(void);
The API to remove Scanning feature:
ble_sts_t blc_ll_removeScanningFromAdvState(void);
For the above two API, the return value of ble_sts_t type are both BLE_SUCCESS.
Combining the timing sequence of the Advertising state and Scanning state, the timing sequence is as follows when the Scanning feature is added to the Advertising state.

The current Link Layer is still in the Advertising state (BLS_LINK_STATE_ADV) and the remaining time in each ADV interval, excluding the ADV event, is used for Scanning.
At each Set Scan, it is determined whether the current time exceeds a Scan interval (set by blc_ll_setScanParameter) since the last Set Scan, and if so, the Scan channel is switched (channel 37/38/39).
For the usage of Scanning in Advertising state, please refer to the TEST_SCANNING_IN_ADV_AND_CONN_SLAVE_ROLE in the ble_feature_test.
Scanning in ConnSlaveRole
When the Link Layer is in ConnSlaveRole, the Scanning feature can be added.
The API to add Scanning feature:
ble_sts_t blc_ll_addScanningInConnSlaveRole(void);
The API to remove Scanning feature:
ble_sts_t blc_ll_removeScanningFromConnSLaveRole(void);
For the above two API, the return value of ble_sts_t type are both BLE_SUCCESS.
Combining the timing sequence of Scanning state and ConnSlaveRole, when the Scanning feature is added to ConnSlaveRole, the timing sequence is as follows.

The current Link Layer is still in ConnSlaveRole (BLS_LINK_STATE_CONN) and the remaining time in each Conn interval, excluding the brx event, is used for Scanning.
At each Set Scan, it is determined whether the current time exceeds a Scan interval (set by blc_ll_setScanParameter) since the last Set Scan, and if so, the Scan channel is switched (channel 37/38/39).
For the usage of Scanning in ConnSlaveRole, please refer to the TEST_SCANNING_IN_ADV_AND_CONN_ SLAVE_ROLE in ble_feature_test.
Advertising in ConnSlaveRole
When the Link Layer is in ConnSlaveRole, the Advertising feature can be added.
The API to add Advertising feature:
ble_sts_t blc_ll_addAdvertisingInConnSlaveRole(void);
The API to remove Advertising feature:
ble_sts_t blc_ll_removeAdvertisingFromConnSLaveRole(void);
ble_sts_t blc_ll_setAdvParamInConnSlaveRole( u8 *adv_data, u8 advData_len, u8 *scanRsp_data, u8 scanRspData_len, adv_type_t advType, own_addr_type_t ownAddrType, adv_chn_map_t adv_channelMap, adv_fp_type_t advFilterPolicy)
The return value of type ble_sts_t for the above three APIs is BLE_SUCCESS.
Combining the timing sequence of Advertising and ConnSlaveRole, when the Advertising feature is added to ConnSlaveRole, the timing sequence is as follows.

The current Link Layer is still in ConnSlaveRole (BLS_LINK_STATE_CONN) and executes an ADV event immediately after the brx event in each Conn interval, and then leaves the rest of the time for the UI task or goes into sleep (suspend/ deepsleep retention) to save power.
For the usage of Advertising in ConnSlaveRole, please refer to the TEST_ADVERTISING_IN_CONN_SLAVE_ROLE in ble_feature_test.
Advertising and Scanning in ConnSlaveRole
Combined with the use of Scanning in ConnSlaveRole and Advertising in ConnSlaveRole above, it is possible to add both Scanning and Advertising to ConnSlaveRole. The timing sequence is as follows.

The current Link Layer is still in ConnSlaveRole (BLS_LINK_STATE_CONN) and an ADV event is executed immediately after the brx event in each Conn interval, and then the rest of the time is used for Scanning.
At each Set Scan, it is determined whether the current time exceeds a Scan interval (set by blc_ll_setScanParameter) since the last Set Scan, and if so, the Scan channel is switched (channel 37/38/39).
For the usage of Advertising and Scanning in ConnSlaveRole, please refer to the TEST_ADVERTISING_SCANNING_IN_CONN_SLAVE_ROLE in ble_feature_test.
Link Layer TX fifo & RX fifo
All data from the application layer and the BLE Host eventually needs to be sent through the Link Layer of the Controller to complete the RF data. A BLE TX fifo is designed in the Link Layer, which can be used to cache the incoming data and to send the data after the brx/btx has started.
All data received from the peer device during Link Layer brx/btx is first stored in a BLE RX fifo before being uploaded to the BLE Host or application layer for processing.
The BLE TX fifo and BLE RX fifo for the Slave role and Master role are handled in the same way. Both the BLE TX fifo and BLE RX fifo are defined at the application layer.
MYFIFO_INIT(blt_rxfifo, 64, 8);
MYFIFO_INIT(blt_txfifo, 40, 16);
The RX fifo size is 64 by default and the TX fifo size is 40 by default, and these two sizes are not allowed to be modified unless a Data Length Extension(DLE) is required.
Both TX fifo number and RX fifo number should be set to the power of 2, i.e. 2, 4, 8, 16, etc. Users can modify them slightly to suit their needs.
RX fifo number is 8 by default, which is a reasonable value and can ensure that the bottom layer of the Link Layer can buffer up to 8 packets. If the setting is too large, it will take up too much SRAM. If the setting is too small, there may be a risk of data overwriting: in the brx event, the Link Layer is likely to word under More Data (MD) mode on an interval, and continue to receive multiple packets, if you set 4, it is likely that there will be five or six packets in an interval (such as OTA, playing master voice data, etc.), and the upper layer's response to these data is too long to process due to the long decryption time, then it is possible some data is overflowed.
Here is an example of RX overflow, we have the following assumptions:
a) The number of RX fifo is 8;
b) Before brx_event(n) is turned on, the read and write pointers of RX fifo are 0 and 2 respectively;
c) In the brx_event(n) and brx_event(n+1) stages, the main_loop has task blockage, and the RX fifo is not taken in time;
d) Both brx_event stages are multi-packet situations.
From the description in the “Conn state Slave role timing” section above, we know that the BLE data packets received in the brx_working stage will only be copied to the RX fifo (RX fifo write pointer++), and the RX fifo data is actually taken out for processing. In the main_loop stage (RX fifo read pointer++), we can see that the sixth data will cover the read pointer 0 area. It should be noted here that the UI task time slot in the brx working stage is the time except for interrupt processing such as RX, TX, and system timer.

Relative to the extreme case above with long task blockade duration due to one connection interval, the case below is more likely to occur: During one brx_event, since Master writes multiple packets (e.g. 7/8 packets) into Slave, Slave fails to process the received data in time. As shown below, the rptr (read pointer) is increased by two, but the wptr (write pointer) is also increased by eight, which thus causes data overflow.

Once there is a data loss problem caused by overflow, for the encryption system, there will be a MIC failure disconnection problem. (For old SDK, as brx event Rx IRQ will fill data to Rx fifo but not do data overflow check, if main_loop is too slow to process RX fifo it will lead to overflow problem, so when using old SDK, user need to pay more attention to this risk, avoid master to send too much data on one connection interval, pay attention to user UI that the task processing time is as short as possible to avoid blocking problems.)
Rx overflow checks have now been added to the SDK. Check whether the current RX fifo write pointer and read pointer difference is greater than the number of Rx fifo in brx/btx event Rx IRQ. Once the Rx fifo is found to be full, the RF does not ACK the other party. BLE protocol Data retransmission is ensured. In addition, the SDK also provides the Rx overflow callback function to notify users. This callback will be introduced in the chapter "Telink defined event" later in the document.
Similarly, if there may be more than 8 valid packets in an interval, the default 8 is not enough.
The TX fifo number is 16 by default, which is able to handle the larger data volume of the voice remote control function. Users can modify it to 8 if they do not use such a large fifo.
If set too large (e.g. 32) it will take up too much sram.
In the TX fifo, the SDK bottom layer stack needs to use 2, and the rest is used exclusively by the application layer; for a TX fifo of 16, the application layer can only use 14; for 8, the application layer can only use 6.
When sending data from the application layer (e.g. calling blc_gatt_pushHandleValueNotify), the user should first check how many TX fifo's are currently available in the Link Layer.
The following API is used to determine how many TX fifo's are currently occupied, not how many are left.
u8 blc_ll_getTxFifoNumber (void);
For example, if the TX fifo number defaults to 16, there are 14 users available, so the value returned by the API is available as long as it is less than 14: a return of 13 means there is 1 available, and a return of 0 means there are 14 available.
When using TX fifo, if the user first looks at how many are left before deciding whether to push the data directly, a fifo should be left in place to prevent various boundary issues from occurring.
In the voice processing of the ble remote, as each voice data is known to be split into 5 packets, 5 TX fifo's are required and no more than 9 occupied fifo's can be used. In order to avoid exceptions caused by some boundary conditions when the TX fifo is used (e.g. just in time for the BLE stack to reply to the master's command and insert a data into the TX fifo), the final code is written as follows: the voice data is only pushed to the TX fifo when there are no more than 8 occupied TX fifo's.
if (blc_ll_getTxFifoNumber() < 9)
{
……
}
As discussed above, the SDK provides the following API for limiting the amount of more data received on an interval (if the user wants to limit the data even if the RX fifo is sufficient), in addition to the automatic data overflow handling mechanism.
void blc_ll_init_max_md_nums(u8 num);
In which, the maximum number of More Data set by parameter num should not to exceed the RX fifo number.
Note
- Note that the ability to qualify more data on a connection event is only enabled if the API is called at the application level (parameter num is greater than 0).
Controller Event
To meet user requirements for recording and processing key actions of the BLE stack bottom layer at the application layer, the Telink BLE SDK provides three types of events:
(1) Standard HCI events defined by the BLE Controller;
(2) A set of events defined by Telink, referred to as "Telink defined events" (both of the first two types belong to Controller events);
(3) Event notification-type GAP events for protocol stack interaction flows defined by the BLE Host (can also be considered Host events; for details, please refer to the "GAP event" section of this document).
The BLE SDK event architecture is illustrated in the figure below. It can be seen that HCI events and Telink defined events belong to Controller events, while GAP events belong to BLE Host events. The following two subsections mainly introduce Controller events.

Controller HCI Event
HCI event is designed according to BLE Spec; Telink defined event only applies to BLE Slave (ble remote/ble module etc).
-
BLE Master only supports HCI event.
-
BLE Slave supports both HCI event and Telink defined event.
For BLE Slave, basically the two sets of event are independent of each other, except for the connect and disconnect event of Link Layer, which will be introduced in detail later.
User can select one set or use both as needed.
As shown in the “Host + Controller” architecture below, Controller HCI event indicates all events of Controller are reported to Host via HCI.

For details regarding the definition of Controller HCI events, please refer to the Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7 Events. Specifically, Section 7.7.65 "LE Meta Event" refers to HCI LE (Low Energy) Events, while the others are standard HCI events. According to the definition in the specification, the Telink BLE SDK also categorizes Controller HCI events into two types: HCI Events and HCI LE Events. As the Telink BLE SDK mainly supports the Bluetooth Low Energy protocol stack, it supports only a few basic HCI Events, while supporting the majority of HCI LE Events.
For the definition of macros and interfaces related to Controller HCI event, please refer to head files under “stack/ble/hci”.
To receive Controller HCI event in Host or APP layer, user should register callback function of Controller HCI event, and then enable mask of corresponding event.
Following are callback function prototype and register interface of Controller HCI event:
typedef int (*hci_event_handler_t) (u32 h, u8 *para, int n);
void blc_hci_registerControllerEventHandler(hci_event_handler_t handler);
In the callback function prototype, “u32 h” is a mark which will be frequently used in bottom-layer stack, and user only needs to know the following:
#define HCI_FLAG_EVENT_TLK_MODULE (1<<24)
#define HCI_FLAG_EVENT_BT_STD (1<<25)
The “HCI_FLAG_EVENT_TLK_MODULE” will be introduced in “Telink defined event”, while “HCI_FLAG_EVENT _BT_STD” indicates current event is Controller HCI event.
In the callback function prototype, “para” and “n” indicate data and data length of event. The data is consistent with the definition in BLE spec. User can refer to the following usage in the ble_master kma dongle and the specific implementation of the controller_event_callback function.
blc_hci_registerControllerEventHandler(controller_event_callback);
HCI event
Telink BLE SDK supports a few HCI events. Following lists some events for user.
#define HCI_EVT_DISCONNECTION_COMPLETE 0x05
#define HCI_EVT_ENCRYPTION_CHANGE 0x08
#define HCI_EVT_READ_REMOTE_VER_INFO_COMPLETE 0x0C
#define HCI_EVT_ENCRYPTION_KEY_REFRESH 0x30
#define HCI_EVT_LE_META 0x3E
a) HCI_EVT_DISCONNECTION_COMPLETE
Please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7.5 Disconnection Complete Event. Total data length of this event is 7, and 1-byte "param len" is 4, as shown below. Please refer to BLE spec for data definition.

b) HCI_EVT_ENCRYPTION_CHANGE and HCI_EVT_ENCRYPTION_KEY_REFRESH
Please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7.8 & 7.7.39. The two events are related to Controller encryption, and the processing is assembled in library.
c) HCI_EVT_READ_REMOTE_VER_INFO_COMPLETE
Please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7.12. When Host uses the “HCI_CMD_READ_REMOTE_VER_INFO” command to exchange version information between Controller and BLE peer device, and version of peer device is received, this event will be reported to Host. Total data length of this event is 11, and 1-byte "param len" is 8, as shown below. Please refer to BLE spec for data definition.

d) HCI_EVT_LE_META
It indicates current event is HCI LE event, and event type can be judged according to sub event code. Except for HCI_EVT_LE_META, other HCI events should use the API below to enable corresponding event mask.
ble_sts_t blc_hci_setEventMask_cmd(u32 evtMask);
Definition of 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
If the user does not set the HCI event mask via this API, the SDK will only turn on the mask corresponding to HCI_CMD_DISCONNECTION_COMPLETE by default, i.e. to ensure that the Controller disconnect event is reported.
HCI LE event
When event code in HCI event is “HCI_EVT_LE_META” to indicate HCI LE event, common sub-event code are shown as below:
#define HCI_SUB_EVT_LE_CONNECTION_COMPLETE 0x01
#define HCI_SUB_EVT_LE_ADVERTISING_REPORT 0x02
#define HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE 0x03
#define HCI_SUB_EVT_LE_CONNECTION_ESTABLISH 0x20
a) HCI_SUB_EVT_LE_CONNECTION_COMPLETE
Please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7.65.1 LE Connection Complete Event. When connection is established between Link Layer and peer device, this event will be reported. Total data length of this event is 22, and 1-byte "param len" is 19, as shown below. Please refer to BLE spec for data definition.

b) HCI_SUB_EVT_LE_ADVERTISING_REPORT
Please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7.65.2 LE Advertising Report Event. When Link Layer scans right ADV packet, it will be reported to Host via "HCI_SUB_EVT_LE_ADVERTISING_REPORT". Data length of this event is not fixed and it depends on payload of ADV packet, as shown below. Please refer to BLE spec for data definition.

Note: In Telink BLE SDK, each "LE Advertising Report Event" only reports an ADV packet, i.e. "i" in figure above is 1.
c) HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE
Please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.7.65.3 LE Connection Update Complete Event. When "connection update" in Controller takes effect, the "HCI_SUB_EVT_LE_CONNECTION_UPDATE_COMPLETE" will be reported to Host. Total data length of this event is 13, and 1-byte "param len" is 10, as shown below. Please refer to BLE spec for data definition.

d) HCI_SUB_EVT_LE_CONNECTION_ESTABLISH
The “HCI_SUB_EVT_LE_CONNECTION_ESTABLISH” is a supplement to the “HCI_SUB_EVT_LE_CONNECTION _COMPLETE”, so all the parameters except for subevent is the same. This event is used by the b85m_master kma dongle in the SDK. It is the only non-BLE spec standard event, is privately defined by Telink and is only used in the b85m_master kma dongle.
Following illustrates the reason for Telink to define this event.
When BLE Controller in Initiating state scans ADV packet from specific device to be connected, it will send connection request packet to peer device; no matter whether this connection request is received, it will be considered as “Connection complete”, “LE Connection Complete Event” will be reported to Host, and Link Layer immediately enters Master role. Since this packet does not support ack/retry mechanism, Slave may miss the connection request, thus it cannot enter Slave role, and does not enter brx mode to transfer packets. In this case, Master Controller will process according to the mechanism below: After it enters Master role, it will check whether there’s any packet received from Slave during the beginning 6 ~ 10 conn intervals (CRC check is negligible).
-
If no packet is received, it’s considered that Slave does not receive connection request; suppose “LE Connection Complete Event” has already been reported, it should report a “Disconnection Complete Event” quickly, and indicate disconnect reason is “0x3E (HCI_ERR_CONN_FAILED_TO_ESTABLISH)”.
-
If there’s packet received from Slave, it can confirm that Connection is established, thus Master can continue rest of the flow.
According to the description above, the processing method of BLE Host should be: After it receives “LE Connection Complete Event” of Controller, it cannot confirm that connection has already been established, but instead, starts a timer based on conn interval (timing value should be configured as 10 intervals or above to cover the longest time). After the timer is started, it will check whether there is “Disconnection Complete Event” with disconnect reason of 0x3E; if there is no such event, it will be considered as “Connection Established”.
Considering this processing of BLE Host is very complex and error prone, the SDK defines the “HCI_SUB_EVT_LE_CONNECTION_ESTABLISH” in the bottom layer. When Host receives this event, it indicates that Controller has confirmed connection is OK on Slave side and can continue rest of the flow.
“HCI LE event” needs the API below to enable mask.
ble_sts_t blc_hci_le_setEventMask_cmd(u32 evtMask);
Following lists some evtMask definitions. User can view the other events in the “hci_const.h”.
#define HCI_LE_EVT_MASK_CONNECTION_COMPLETE 0x00000001
#define HCI_LE_EVT_MASK_ADVERTISING_REPORT 0x00000002
#define HCI_LE_EVT_MASK_CONNECTION_UPDATE_COMPLETE 0x00000004
#define HCI_LE_EVT_MASK_CONNECTION_ESTABLISH 0x80000000
If HCI LE event mask is not set via this API, mask of all HCI LE events in the SDK are disabled by default.
Telink Defined Event
Besides standard Controller HCI event, the SDK also provides Telink defined event. Up to 20 Telink defined events are supported, which are defined by using macros in the “stack/ble/ll/ll.h”.
Current SDK version supports the following callback events. The “BLT_EV_FLAG_CONNECT / BLT_EV_FLAG_ TERMINATE” has the same function as the “HCI_SUB_EVT_LE_CONNECTION_COMPLETE” / “HCI_EVT_ DISCONNECTION_COMPLETE” in HCI event, but data definition of these events are different.
#define BLT_EV_FLAG_ADV 0
#define BLT_EV_FLAG_ADV_DURATION_TIMEOUT 1
#define BLT_EV_FLAG_SCAN_RSP 2
#define BLT_EV_FLAG_CONNECT 3
#define BLT_EV_FLAG_TERMINATE 4
#define BLT_EV_FLAG_LL_REJECT_IND 5
#define BLT_EV_FLAG_RX_DATA_ABANDON 6
#define BLT_EV_FLAG_PHY_UPDATE 7
#define BLT_EV_FLAG_DATA_LENGTH_EXCHANGE 8
#define BLT_EV_FLAG_GPIO_EARLY_WAKEUP 9
#define BLT_EV_FLAG_CHN_MAP_REQ 10
#define BLT_EV_FLAG_CONN_PARA_REQ 11
#define BLT_EV_FLAG_CHN_MAP_UPDATE 12
#define BLT_EV_FLAG_CONN_PARA_UPDATE 13
#define BLT_EV_FLAG_SUSPEND_ENTER 14
#define BLT_EV_FLAG_SUSPEND_EXIT 15
#define BLT_EV_FLAG_VERSION_IND_REV 16
#define BLT_EV_FLAG_SCAN_REQ 17
#define BLT_EV_FLAG_ADV_TX_EACH_CHANNEL 18
Telink defined event is only triggered in BLE slave applications. There are two ways to implement callback of Telink defined event in BLE slave application.
(1) The first method, which is called “independent registration”, is to independently register callback function for each event.
Prototype of callback function is shown as below:
typedef void (*blt_event_callback_t)(u8 e, u8 *p, int n);
Where “e”: event number. “p”: It’s the pointer to the data transmitted from the bottom layer when callback function is executed, and it varies with the callback function. “n”: length of valid data pointed by pointer.
API to register callback function:
void bls_app_registerEventCallback (u8 e, blt_event_callback_t p);
Whether each event will respond depends on whether corresponding callback function is registered in APP layer.
(2) The second method, which is called “shared event entry”, is that all event callback functions share the same entry. Whether each event will respond depends on whether its event mask is enabled. This method uses the same API as HCI event to register event callback:
typedef int (*hci_event_handler_t) (u32 h, u8 *para, int n);
void blc_hci_registerControllerEventHandler(hci_event_handler_t handler);
Although registered callback function of HCI event is shared, they are different in implementation. In HCI event callback function:
h = HCI_FLAG_EVENT_BT_STD | hci_event_code;
While in Telink defined event “shared event entry”:
h = HCI_FLAG_EVENT_TLK_MODULE | e;
Where “e” is event number of Telink defined event.
Telink defined event “shared event entry” is similar to mask of HCI event; the API below serves to set the mask to determine whether each event will be responded.
ble_sts_t bls_hci_mod_setEventMask_cmd(u32 evtMask);
Relationship between evtMask and event number is:
evtMask = BIT(e);
The two methods for Telink defined event are exclusive to each other. The first method is recommended and is adopted by most demo code of the SDK; only “ble_ module” uses the “shared event entry” method.
For the use of Telink defined event, please refer to the demo code of project “b85m_module” for 2 “shared event entry”.
The following takes the connect and terminate event callbacks as examples to describe the code implementation methods of these two methods.
(1) The first method: “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) The second method: “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) );
Following will introduce details about all events, event trigger condition and parameters of corresponding callback function for Controller.
(1) BLT_EV_FLAG_ADV
This event is not used in current SDK.
(2) BLT_EV_FLAG_ADV_DURATION_TIMEOUT
Event trigger condition: If the API “bls_ll_setAdvDuration” is invoked to set advertising duration, a timer will be started in BLE stack bottom layer. When the timer reaches the specified duration, advertising is stopped, and this event is triggered. In the callback function of this event, user can modify ADV event type, re-enable advertising, re-configure advertising duration, and etc.
Pointer p: null pointer.
Data length “n”: 0.
Note: This event is not triggered in “advertising in ConnSlaveRole” which is an extended state of Link Layer.
(3) BLT_EV_FLAG_SCAN_RSP
Event trigger condition: When Slave is in advertising state, this event will be triggered if Slave responds with scan response to the scan request from Master.
Pointer p: null pointer.
Data length “n”: 0.
(4) BLT_EV_FLAG_CONNECT
Event trigger condition: When Link Layer is in advertising state, this event will be triggered if it responds to connect request from Master and enters ConnSlaveRole role.
Data length “n”: 34.
Returned pointer p: p points to a RAM area of 34 bytes. Please refer to the definition of tlk_contr_evt_connect_t in controller.h.
typedef struct{
u8 initA[6];
u8 advA[6];
u32 accessCode;
u8 crcinit[3];
u8 winSize;
u16 winOffset;
u16 connReq_interval;
u16 connReq_latency;
u16 connReq_timeout;
u8 chm_map[5];
u8 hop_sca;
}tlk_contr_evt_connect_t;
(5) BLT_EV_FLAG_TERMINATE
Event trigger condition: This event will be triggered when Link Layer state machine exits from ConnSlaveRole role in any of the three specific cases.
Returned pointer p: p points to a u8 type variable terminate_reason, which indicates the reason for the Link Layer disconnection. Please refer to the definition of tlk_contr_evt_terminate_t in controller.h.
typedef struct{
u8 terminate_reason;
}tlk_contr_evt_terminate_t;
Data length “n”: 1.
Three cases to exit ConnSlaveRole and corresponding reasons are listed as below:
A. If Slave fails to receive packet from Master for a duration due to RF communication problem (e.g. bad RF or Master is powered off), and “connection supervision timeout” expires, this event will be triggered to terminate connection and return to disconnected state. The terminate reason is HCI_ERR_CONN_TIMEOUT (0x08).
B. If Master sends “terminate” command to actively terminate connection, after Slave responds to the command with an ack, this event will be triggered to terminate connection and return to disconnected state. The terminate reason is the Error Code in the “LL_TERMINATE_IND” control packet received in Slave Link Layer. The Error Code is determined by Master. Common Error Codes include HCI_ERR_REMOTE_USER_TERM_CONN (0x13), HCI_ERR_CONN_TERM_MIC_FAILURE (0x3D), and etc.
C. If Slave invokes the API “bls_ll_terminateConnection(u8 reason)” to actively terminate connection, this event will be triggered. The terminate reason is the actual parameter “reason” of this API.
(6) BLT_EV_FLAG_LL_REJECT_IND
Event trigger condition: When Master sends a “LL_ENC_REQ” (encryption request) in the Link Layer and it’s declared to use the pre-allocated LTK, if Slave fails to find corresponding LTK and responds with a “LL_REJECT_IND” (or “LL_REJECT_EXT_IND”), this event will be triggered.
Returned pointer p: p points to a u8 type variable which stores the transmitted command (LL_REJECT_IND or LL_REJECT_EXT_IND).
Data length “n”: 1.
For more information, please refer to Bluetooth Core Specification v5.3 [Vol 6] Part B, Section 2.4.2.
(7) BLT_EV_FLAG_RX_DATA_ABANDON
Event trigger condition: This event will be triggered when BLE RX fifo overflows (see section Link Layer TX fifo & RX fifo), or the number of More Data received in an interval exceeds the preset threshold (Note: User needs to invoke the API “blc_ll_init_max_md_nums” with non-zero parameter, so that SDK bottom layer will check the number of More Data.)
Pointer p: null pointer.
Data length “n”: 0.
(8) BLT_EV_FLAG_PHY_UPDATE
Event trigger condition: This event will be triggered after the update succeeds or fails when the slave or master proactively initiates LL_PHY_REQ; Or when the slave or master passively receives LL_PHY_REQ and meanwhile PHY is updated successfully, this event will be triggered.
Data length “n”: 1.
Pointer p: p points to an u8-type variable indicating the current connection of PHY mode.
typedef enum {
BLE_PHY_1M = 0x01,
BLE_PHY_2M = 0x02,
BLE_PHY_CODED = 0x03,
} le_phy_type_t;
(9) BLT_EV_FLAG_DATA_LENGTH_EXCHANGE
Event trigger condition: Triggered when Slave and Master exchange Link Layer maximum data length, i.e. one side sends LL_LENGTH_REQ and the other side replies with LL_LENGTH_RSP. If Slave initiates LL_LENGTH_REQ, it is triggered when LL_LENGTH_RSP is received; if Master initiates LL_LENGTH_REQ, it is triggered immediately after Slave replies with LL_LENGTH_RSP.
Data length “n”: 12.
Pointer p: Points to a memory area corresponding to the following structure.
typedef struct {
u16 connEffectiveMaxRxOctets; //effective maximum RX Octets
u16 connEffectiveMaxTxOctets; //effective maximum TX Octets
u16 connMaxRxOctets; //local maximum RX Octets
u16 connMaxTxOctets; //local maximum TX Octets
u16 connRemoteMaxRxOctets; //remote maximum RX Octets
u16 connRemoteMaxTxOctets; //remote maximum TX Octets
}tlk_contr_evt_dataLenExg_t;
The “connEffectiveMaxRxOctets” and “connEffectiveMaxTxOctets” are max RX and TX data length finally allowed in current connection; “connMaxRxOctets” and “connMaxTxOctets” are max RX and TX data length of the device; “connRemoteMaxRxOctets” and “connRemoteMaxTxOctets” are max RX and TX data length of peer device.
connEffectiveMaxRxOctets = min(supportedMaxRxOctets,connRemoteMaxTxOctets);
connEffectiveMaxTxOctets = min(supportedMaxTxOctets, connRemoteMaxRxOctets);
(10) BLT_EV_FLAG_GPIO_EARLY_WAKEUP
Event trigger condition: Before the BLE slave enters sleep (suspend or deepsleep retention), the next wake-up time is calculated. After entering sleep, the device wakes up when this time point is reached (implemented by a timer in the sleep state).
In this scenario, a potential issue arises: if the sleep duration is too long, user tasks can only be processed after waking up, which causes response latency issues for applications with real-time requirements.
For example, in keyboard scanning, user key presses can be very fast. To ensure no keystrokes are missed while handling debouncing, a scan interval of 10~20ms is preferred. However, if the sleep duration is long, such as 400ms or 1s (which can occur when the advertising interval is large or latency is enabled in the connection state), keystrokes will be missed.
Therefore, it is necessary to check the current sleep duration before the MCU enters sleep. If the duration is too long, the suspend/deepsleep retention mode should be configured to allow early wake-up by user key actions (this will be introduced in detail in the "PM module" section).
The event “BLT_EV_FLAG_GPIO_EARLY_WAKEUP” will be triggered if MCU is woke up from sleep (suspend or deepsleep) by GPIO in advance before wakeup timer expires.
Data length “n”: 1.
Pointer p: p points to a u8 type variable wakeup_status. This variable records which wake-up sources are effective in the current suspend state. Wake-up source status values are defined in drivers/8258(8278)/lib/include/pm.h, as shown below.
enum {
WAKEUP_STATUS_COMPARATOR = BIT(0),
WAKEUP_STATUS_TIMER = BIT(1),
WAKEUP_STATUS_CORE = BIT(2),
WAKEUP_STATUS_PAD = BIT(3),
......
STATUS_GPIO_ERR_NO_ENTER_PM = BIT(8),
STATUS_ENTER_SUSPEND = BIT(30),
};
For parameter definition above, please refer to “Power Management” section.
(11) BLT_EV_FLAG_CHN_MAP_REQ
Event trigger condition: When Slave is in ConnSlaveRole, if Master needs to update current connection channel list, it will send a “LL_CHANNEL_MAP_REQ” command to Slave; this event will be triggered after Slave receives this request from Master and has not processed the request yet.
Data length “n”: 5.
Pointer p: p points to the starting address of the following channel list array, as shown below.
typedef struct {
u8 chn_map[5]; //old channel map before update take effect
}tlk_contr_evt_chnMapRequest_t;
Note: When the callback function executes, the chn_map[5] pointed to by p is the old channel map which has not yet been updated.
The chn_map uses 5 bytes to represent the current channel list. It employs a mapping method where each bit represents a channel:
- Bit 0-7 of
chn_map[0]represent channel 0-7 respectively. - Bit 0-7 of
chn_map[1]represent channel 8-15 respectively. - Bit 0-7 of
chn_map[2]represent channel 16-23 respectively. - Bit 0-7 of
chn_map[3]represent channel 24-31 respectively. - Bit 0-4 of
chn_map[4]represent channel 32-36 respectively.
(12) BLT_EV_FLAG_CHN_MAP_UPDATE
Event trigger condition: When Slave is in ConnSlaveRole state, this event will be triggered if Slave has updated channel map after it receives the “LL_CHANNEL_MAP_REQ” command from Master.
Returned pointer p: p points to the start address of chn_map[5], corresponding to the following structure. At this point, chn_map is the new map after update.
typedef struct {
u8 chn_map[5]; //new channel map after update take effect
}tlk_contr_evt_chnMapUpdate_t;
Data length “n”: 5.
(13) BLT_EV_FLAG_CONN_PARA_REQ
Event trigger condition: When Slave is in ConnSlaveRole state (Conn state Slave role), if Master needs to update current connection parameters, it will send a “LL_CONNECTION_UPDATE_IND” command to Slave; this event will be triggered after Slave receives this request from Master and has not processed the request yet.
Data length “n”: 11.
Pointer p: p points to the PDU of the LL_CONNECTION_UPDATE_IND, as shown below
typedef struct {
u8 winSize;
u16 winOffset;
u16 interval;
u16 latency;
u16 timeout;
u16 instant;
}tlk_contr_evt_connParaReq_t;
(14) BLT_EV_FLAG_CONN_PARA_UPDATE
Event trigger condition: When Slave is in ConnSlaveRole state, this event will be triggered if Slave has updated connection parameters after it receives the “LL_CONNECTION_UPDATE_IND” from Master.
Data length “n”: 6.
Pointer p: p points to the new connection parameters after update, as shown below.
typedef struct {
u16 conn_interval; //new connection interval after update take effect, 1.25 mS unit
u16 conn_latency; //new connection latency after update take effect
u16 conn_timeout; //new connection timeout after update take effect, 10 mS unit
}tlk_contr_evt_connParaUpdate_t;
(15) BLT_EV_FLAG_SUSPEND_ENTER
Event trigger condition: Triggered before the slave enters sleep (suspend or deepsleep retention) while executing the "blt_sdk_main_loop" function.
Pointer p: Null pointer.
Data length “n”: 0.
(16) BLT_EV_FLAG_SUSPEND_EXIT
Event trigger condition: When Slave executes the function “blt_sdk_main_loop”, this event will be triggered after Slave is woke up from suspend.
Data length “n”: 1.
Pointer p: Points to a u8 type variable wakeup_status. The wakeup_status variable records which wake-up source statuses are effective in the current suspend state. Wake-up source status values are defined in drivers/8258(8278)/lib/include/pm.h, as shown below.
enum {
WAKEUP_STATUS_COMPARATOR = BIT(0),
WAKEUP_STATUS_TIMER = BIT(1),
WAKEUP_STATUS_CORE = BIT(2),
WAKEUP_STATUS_PAD = BIT(3),
......
STATUS_GPIO_ERR_NO_ENTER_PM = BIT(8),
STATUS_ENTER_SUSPEND = BIT(30),
};
Note
- This callback is executed after SDK bottom layer executes “cpu_sleep_wakeup” and Slave is woke up, and this event will be triggered no matter whether the actual wakeup source is GPIO or timer. If the event “BLT_EV_FLAG_GPIO_EARLY_WAKEUP” occurs at the same time, for the sequence to execute the two events, please refer to pseudo code in “Power Management – PM Working Mechanism”.
Data Length Extension
The Bluetooth Core Specification v4.2 and above supports Data Length Extension (DLE).
Link Layer in this BLE SDK supports data length extension to max rf_len of 251 bytes per BLE Spec.
Please refer to Bluetooth Core Specification v5.3 [Vol 6] Part B, Section 2.4.2.21 LL_LENGTH_REQ and LL_LENGTH_RSP.
Following steps explains how to use data length extension.
Step 1 Configure suitable TX & RX fifo size
To receive and transmit long packet, bigger Tx & Rx fifo size is required and thus occupies large SRAM space. So be cautious when setting fifo size to avoid waste of SRAM space.
Tx fifo size should be increased to transmit long packet. Tx fifo size should be larger than Tx rf_len by 10, and should be aligned by 4 bytes.
#define TLK_RF_TX_EXT_LEN (10) //10 = 4(DMA_len) + 2(BLE header) + 4(MIC)
#define CAL_LL_ACL_TX_BUF_SIZE(maxTxOct) (((maxTxOct + TLK_RF_TX_EXT_LEN) + 3) / 4 *4)
rf_len, and it should be 16-byte aligned. Please refer to the following macro definition.
#define TLK_RF_RX_EXT_LEN (22) //4(DMA_len) + 2(BLE header) + ISORxOct + 4(MIC) + 3(CRC) + 8(ExtraInfo)
#define CAL_LL_ACL_RX_BUF_SIZE(maxRxOct) (((maxRxOct + TLK_RF_RX_EXT_LEN) + 15) / 16 *16)
Note
- When the 825x series chip acts as a Master, the RX FIFO size needs to be at least 29 bytes larger than the RX
rf_len, and it should also be 16-byte aligned.
Step 2 Set proper MTU size
MTU, the maximum transmission unit, is used to set the size of the payload in a single packet of the L2CAP layer in BLE. THe att.h provides the API ble_sts_t blc_att_setRxMtuSize(u16 mtu_size). during the initialization of the BLE stack, users can directly use this function to pass the parameter to set the MTU. However, the MTU size effect is negotiated in the MTU exchange process, and valid value for MTU size is the minimum of MTU_A and MTU_B. MTU_A is the MTU size supported by device A, MTU_B is the MTU size supported by device B; in addition, only MTU size greater than the DLE length can make full use of the DLE to increase the link layer data throughput rate.
For the implementation of MTU size exchange, please refer to the detailed description in the "ATT & GATT" section of this document, or refer to the DLE demo in ble_feature_test.
#define MTU_SIZE_SETTING 196
blc_att_setRxMtuSize(MTU_SIZE_SETTING);
For MTU greater than ATT_MTU_MAX_SDK_DFT_BUF, the user can register buffer with the following API.
ble_sts_t blc_l2cap_initMtuBuffer(u8 *pMTU_rx_buff, u16 mtu_rx_size, u8 *pMTU_tx_buff, u16 mtu_tx_size);
Step 3 data length exchange
Before transfer of long packets, please make sure the “data length exchange” flow has already been completed. Data length exchange is an interactive process in Link Layer by LL_LENGTH_REQ and LL_LENGTH_RSP. Either master or slave can initiate the process by sending LL_LENGTH_REQ, while the peer responds with LL_LENGTH_RSP. Through this interaction, master and slave obtain the max Tx and Rx packet size from each other, and adopt the minimum of the two as the max Tx and Rx packet size in current connection.
Regardless of which side initiates the LL_LENGTH_REQ, upon completion of the data length exchange procedure, the SDK will generate a BLT_EV_FLAG_DATA_LENGTH_EXCHANGE event if the user has registered the corresponding callback. Please refer to the "Telink defined event" section for the meaning of each parameter in the event callback function.
The final max Tx and Rx packet size can be obtained from the BLT_EV_FLAG_DATA_LENGTH_EXCHANGE event callback function.
When B85m chip acts as slave device in actual applications, master may or may not initiate LL_LENGTH_REQ. If master does not initiate it, slave should initiate LL_LENGTH_REQ by the following API in the SDK:
ble_sts_t blc_ll_exchangeDataLength (u8 opcode, u16 maxTxOct);
In the API, set the opcode to LL_LENGTH_REQ and maxTxOct to the maximum TX packet length supported by the current device. For example, when the maximum TX packet length is 200 bytes, set it as follows:
blc_ll_exchangeDataLength(LL_LENGTH_REQ , 200);
Since the slave device does not know whether the master will initiate LL_LENGTH_REQ, we recommend a method for your reference: register the BLT_EV_FLAG_DATA_LENGTH_EXCHANGE event callback, turn on a software timer to start timing when the connection is established (e.g. 2S), if this callback has not been triggered after 2s, it means that master has not initiated LL_LENGTH_REQ, at this time slave can calls API blc_ll_exchangeDataLength to initiate LL_LENGTH_REQ.
Step 4 MTU size exchange
In addition to data length exchange, MTU size exchange flow should also be executed to ensure configured MTU size takes effect, so that the peer can process long packet in BLE L2CAP layer. MTU size should be equal or larger than max packet size of Tx & Rx. Please refer to “ATT & GATT” section or the demo of the B85m_feature for the implementation of MTU size exchange.
Step 5 Transmission/Reception of long packet
Please refer to “ATT & GATT” section for illustration of Handle Value Notification, Handle Value Indication, Write request and Write Command.
Transmission and reception of long packet can start after correct completion of the steps above.
The APIs corresponding to “Handle Value Notification” and “Handle Value Indication” can be invoked in ATT layer to transmit long packet. As shown below, fill in the address and length of data to be sent to the parameters “*p” and “len”, respectively:
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);
To receive long packets, it is only needed to handle the write callback functions corresponding to Write Request and Write Command; inside the callback function, reference the data pointed to by the formal parameter pointer.
Controller API
Controller API Introduction
In the standard BLE protocol stack architecture shown in Section 3.1.1, the Application Layer cannot interact directly with the Link Layer of the Controller; data should be passed down through the Host, and ultimately, the Host transmits control commands to the Link Layer via HCI. All Controller control commands issued by the Host through the HCI interface are strictly defined in the Bluetooth Core Specification v5.3. For details, please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E Host Controller Interface Functional Specification.
Telink BLE SDK based on standard BLE architecture can serve as a Controller and work together with Host system. Therefore, all APIs to operate Link Layer strictly follow the data format of Host commands in the spec.
The detailed API descriptions below will provide the corresponding Host commands from the Spec. Users can refer to the specific explanations in the Spec for further understanding.
In BLE spec, all HCI commands to operate Controller have corresponding “HCI command complete event” or “HCI command status event” in response to Host layer. However, in Telink BLE SDK, it is handled case by case.
(1) For ble_hci class applications, Telink IC only acts as a BLE controller and needs to work with other MCU's running BLE hosts, a corresponding HCI command complete event or HCI command status event will be generated for each HCI command.
(2) For ble_master kma dongle application, both BLE Host and Controller are running on Telink IC, when Host calls API to send HCI command to Controller, all of them are received correctly by Controller and there is no loss, so the Controller no longer replies to the HCI command complete event or HCI command status event when processing the HCI command.
The Controller API is declared in the header files in the stack/ble/controller and stack/ble/hci directories, where the controller directory is classified according to Link Layer state machine functions into ll.h, ll_adv.h, ll_scan.h, ll_ext_adv.h, ll_pm.h, ll_whitelist.h, ll_init.h, ll_resolvlist.h, ll_conn.h, etc., the user can look for Link Layer functions accordingly, for example, the APIs for functions related to advertising are declared in ll_adv.h.
API Return Type ble_sts_t
An enum type “ble_sts_t” defined in the “stack/ble/ble_common.h” is used as return value type for most APIs in the SDK. When API invoking with right parameter setting is accepted by the protocol stack, it will return “0” to indicate BLE_SUCCESS; if any non-zero value is returned, it indicates a unique error type. All possible return values and corresponding error reason will be listed in the subsections below for each API.
The “ble_sts_t” applies to both APIs in Link Layer and some APIs in Host layer.
BLE MAC Address Initialization
In this document, “BLE MAC Address” includes both “public address” and “random static address”.
In this BLE SDK, the API below serves to obtain public address and random static address:
void blc_initMacAddress(int flash_addr, u8 *mac_public, u8 *mac_random_static);
The “flash_addr” is the flash address to store MAC Address. As explained earlier, this address in B85m chip 512KB flash is 0x76000. If random static address is not needed, set “mac_random_static” as “NULL”.
After the BLE public MAC Address has been successfully obtained, the API for Link Layer initialization is called and the MAC Address is passed into the BLE protocol stack.
blc_ll_initStandby_module(mac_public);
If you use the Advertising state or Scanning state in the Link Layer's state machine, you also need to pass in the MAC Address.
blc_ll_initAdvertising_module(mac_public);
blc_ll_initScanning_module(mac_public);
Link Layer state machine initialization
In conjunction with the previous detailed description of the Link Layer state machine, the following APIs are used to configure the initialisation of the individual modules when building the BLE state machine.
void blc_ll_initBasicMCU (void)
void blc_ll_initStandby_module (u8 *public_adr);
void blc_ll_initAdvertising_module(u8 *public_adr);
void blc_ll_initScanning_module(u8 *public_adr);
void blc_ll_initInitiating_module(void);
void blc_ll_initSlaveRole_module(void);
void blc_ll_initMasterRoleSingleConn_module(void);
bls_ll_setAdvData
For details, please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.8.7 LE Set Advertising Data Command.

As shown above, an ADV packet in BLE stack contains 2-byte header, and Payload (PDU). The maximum length of Payload is 31 bytes.
The API below serves to set PDU data of ADV packet:
ble_sts_t bls_ll_setAdvData(u8 *data, u8 len);
The “data” pointer points to the starting address of the PDU, while the “len” indicates data length.
The table below lists possible results for the return type “ble_sts_t”.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_INVALID_HCI_ CMD_PARAMS | 0x12 | Len exceeds the maximum length 31 |
This API can be invoked during initialization to set ADV data, or invoked in main_loop to modify ADV data when firmware is running.
The ADV PDU defined in the feature_backup project in this BLE SDK is as follows, for the specific meanings of the fields, please refer to the document Supplement to the Bluetooth Core Specification v12 Part A DATA TYPES SPECIFICATION.
const u8 tbl_advData[] = {
0x08, DT_COMPLETE_LOCAL_NAME, 'f', 'e', 'a', 't', 'u', 'r', 'e',
0x02, DT_FLAGS, 0x05,
0x03, DT_APPEARANCE, 0x80, 0x01,
0x05, DT_INCOMPLETE_LIST_16BIT_SERVICE_UUID, 0x12, 0x18, 0x0F, 0x18,
};
As shown in the ADV data above, the ADV device name is set as "feature".
bls_ll_setScanRspData
For details, please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.8.8 LE Set Scan response Data Command.
Similar to the ADV PDU setting above, the scan response PDU setting uses the following API:
ble_sts_t bls_ll_setScanRspData(u8 *data, u8 len);
The "data" pointer points to the starting address of the PDU, while the “len” indicates data length.
The table below lists possible results for the return type "ble_sts_t".
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_INVALID_HCI_ CMD_PARAMS | 0x12 | Len exceeds the maximum length 31 |
Users can call this API during initialization to set Scan response data, or call this API at any time in the main_loop during runtime to modify Scan response data. The scan response data defined in the ble remote project in this BLE SDK is as follows, where the scan device name is "VRemote", for the specific meanings of the fields, please refer to the document Supplement to the Bluetooth Core Specification v12 Part A DATA TYPES SPECIFICATION.
const u8 tbl_scanRsp [] = { 0x08, 0x09, 'V', 'R', 'e', 'm', 'o', 't', 'e',};
Since the device name is set in both the advertising data and the scan response data above, and they are different, the device name actually displayed when the master device scans the Bluetooth device may differ:
a) Some devices only display the device name "feature" in the advertising packet;
b) Some devices send a scan request after scanning the advertisement, and read the scan response packet, so the displayed device name might be "VRemote".
The user can also write the same device name in these two packages, and two different names is not displayed when scanned.
In fact, after the device is connected by the master, when the master reads the Attribute Table of the device, it will obtain the gap device name of the device. After connecting to the device, it may also display the device name according to the gap device name.
bls_ll_setAdvParam
For details, please refer to Bluetooth Core Specification v5.3 [Vol 4] Part E, Section 7.8.5 LE Set Advertising Parameters Command.

The figure above shows Advertising Event (ADV Event in brief) in BLE stack. It indicates during each T_advEvent, Slave implements one advertising process, and sends one packet in three advertising channels (channel 37, 38 and 39) respectively.
The API below serves to set parameters related to 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
The two parameters serve to set the range of advertising interval in integer multiples of 0.625ms. The valid range is from 20ms to 10.24s, and intervalMin should not exceed intervalMax.
As required by BLE spec, it’s not recommended to set ADV interval as fixed value; in Telink BLE SDK, the eventual ADV interval is random variable within the range of intervalMin ~ intervalMax. If intervalMin and intervalMax are set as same value, ADV interval will be fixed as the intervalMin.
ADV packet type has limits to the setting of intervalMin and intervalMax. For details, please refer to “Core 5.0” (Vol 6/Part B/ 4.4.2.2 “Advertising Events”).
If the user expects to set an interval of less than 20ms when not in ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY, the interval check mechanism can be disabled via the following API.
void blc_ll_setAdvIntervalCheckEnable(u8 enable);
(2) advType
Refer to Bluetooth Core Specification v5.3, the five basic advertising types are as follows:

In the figure above, ADV_IND is a Connectable and Scannable Undirected event, which allows Scan Requests and Connect Indications from other devices; ADV_DIRECT_IND is a Connectable Directed event, which only allows the corresponding initiator to initiate a Connect Indication; ADV_SCAN_IND is a Scannable Undirected event, which allows Scan Requests from other devices; ADV_NONCONN_IND is a Non-Connectable and Non-Scannable Undirected event, which does not allow responses from other devices.
Among the above five advertising types, ADV_DIRECT_IND is further divided into Low Duty Cycle Directed Advertising and High Duty Cycle Directed Advertising.
The advType is defined in the SDK as follows:
/* 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;
By default, the most common ADV event type is “ADV_TYPE_CONNECTABLE_UNDIRECTED”.
(3) ownAddrType
There are four optional values for “ownAddrType” to specify ADV address type.
/* 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;
First two parameters are explained herein.
The "OWN_ADDRESS_PUBLIC" indicates that public MAC address is used during advertising. Actual address is the setting from the API “blc_ll_initAdvertising_module(u8 *public_adr)” during MAC address initialization.
The "OWN_ADDRESS_RANDOM" indicates random static MAC address is used during advertising, and the address comes from the setting of the API below:
ble_sts_t blc_ll_setRandomAddr(u8 *randomAddr);
"OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC": Indicates that during advertising, the Controller generates a Resolvable Private Address (RPA) based on the local IRK in the resolving list. If no matching entry is found in the resolving list, the Public MAC Address is used.
"OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM": Indicates that during advertising, the Controller generates a Resolvable Private Address (RPA) based on the local IRK in the resolving list. If no matching entry is found in the resolving list, the Random Static MAC Address is used.
(4) peerAddrType & *peerAddr
When "ownAddrType" is 2 or 3, the "*peerAddr" parameter contains the peer's "Identity Address", and the "peerAddrType" parameter is the peer's "Identity Type" (i.e. 0x00 or 0x01). These parameters are used to locate the corresponding "local IRK" in the "resolving list"; this "IRK" is used to generate the own address used in advertising. Detailed introduction in the subsequent "Whitelist & Resolving list" chapter.
When "advType" is set to directed advertising packet type "directed adv" ("ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY" or "ADV_TYPE_CONNECTABLE_DIRECTED_LOW_DUTY"), "peerAddrType" and "*peerAddr" are used to specify the type and address of the "peer device MAC Address".
In other cases, the values of "peerAddrType" and "*peerAddr" are invalid, can be set to 0 and "NULL".
(5) adv_channelMap
The “adv_channelMap” serves to set advertising channel. It can be selectable from channel 37, 38, 39 or combination.
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
The “advFilterPolicy” serves to set filtering policy for scan request/connect request from other device when ADV packet is transmitted. Address to be filtered needs to be pre-loaded in whitelist.
Filtering type options are shown as below. The “ADV_FP_NONE” can be selected if whitelist filter is not needed.
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
The table below lists possible values and reasons for the return value “bls_ll_setAdvParam”.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_UNSUPPORTED_FEATURE_PARAM_VALUE | 0x11 | "interval" or "ownAddrType" does not conform to the BLE protocol regulations. |
According to Host command design in HCI part of BLE spec, eight parameters are configured simultaneously by the “bls_ll_setAdvParam” API. This setting also takes some coupling parameters into consideration. For example, the “advType” has limits to the setting of intervalMin and intervalMax, and range check depends on the advType; if advType and advInterval are set in two APIs, the range check is uncontrollable.
However, considering that the user may modify some common parameters frequently and does not want to call bls_ll_setAdvParam every time to set 8 parameters at the same time, the SDK wraps 4 of the parameters that are not coupled with other parameters separately to facilitate the use of the user. The three separately wrapped APIs are as follows.
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);
These 3 API parameters are the same as in bls_ll_setAdvParam.
Return value ble_sts_t:
1) bls_ll_setAdvChannelMap and bls_ll_setAdvFilterPolicy will return BLE_SUCCESS unconditionally.
2) bls_ll_setAdvInterval will return BLE_SUCCESS or HCI_ERR_INVALID_HCI_CMD_PARAMS.
bls_ll_setAdvEnable
Please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.9 LE Set Advertising Enable Command.
ble_sts_t bls_ll_setAdvEnable(int en);
en”: BLC_ADV_ENABLE - Enable Advertising; BLC_ADV_DISABLE - Disable Advertising.
a) In Idle state, by enabling Advertising, Link Layer will enter Advertising state.
b) In Advertising state, by disabling Advertising, Link Layer will enter Idle state.
c) In other states, Link Layer state is not influenced by enabling or disabling Advertising.
Note:
- Note that at any time this function is called, ble_sts_t unconditionally returns BLE_SUCCESS, which means that the ADV-related parameters will be turned on or off internally, but only if they are in idle or ADV state.
bls_ll_setAdvDuration
ble_sts_t bls_ll_setAdvDuration (u32 duration_us, u8 duration_en);
After the “bls_ll_setAdvParam” is invoked to set all ADV parameters successfully, and the “bls_ll_setAdvEnable(BLC_ADV_ENABLE)” is invoked to start advertising, the API “bls_ll_setAdvDuration” can be invoked to set duration of ADV event, so that advertising will be automatically disabled after this duration.
“duration_en”: 1-enable timing function; 0-disable timing function. “duration_us”: The “duration_us” is valid only when the “duration_en” is set as 1, and it indicates the advertising duration in unit of us.
The program starts timing from the set time point, once exceeding the preset time, stops advertising, advertising enable becomes invalid, in "Advertising state", switches to "Idle State", at this time triggers "Link Layer" event "BLT_EV_FLAG_ADV_DURATION_TIMEOUT".
As specified in BLE spec, for the ADV type “ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY”, the duration time is fixed as 1.28s, i.e. advertising will be stopped after the 1.28s duration. Therefore, for this ADV type, the setting of “bls_ll_setAdvDuration” does not take effect.
The return value “ble_sts_t” is shown as below.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_INVALID_HCI_ CMD_PARAMS | 0x12 | Duration Time cannot be configured for “ADV_TYPE_CONNECTABLE_DIRECTED_HIGH_DUTY” type. |
When ADV Duration Time expires, advertising is stopped, if user needs to re-configure ADV parameters (such as AdvType, AdvInterval, AdvChannelMap), first the parameters should be set in the callback function of the event “BLT_EV_FLAG_ADV_DURATION_TIMEOUT”, then the “bls_ll_setAdvEnable (1)” should be invoked to start new advertising.
To trigger the “BLT_EV_FLAG_ADV_DURATION_TIMEOUT”, a special case should be noted:
Suppose the “duration_us” is set as “2000000” (i.e. 2s).
If Slave stays in advertising state, when ADV time reaches the preset 2s timeout, the “BLT_EV_FLAG_ADV_ DURATION_TIMEOUT” will be triggered to execute corresponding callback function.
If Slave is connected with Master when ADV time is less than the 2s timeout (suppose ADV time is 0.5s), the timeout timing is not cleared but cached in bottom layer. When Slave stays in connection state for 1.5s (i.e. the preset 2s timeout moment is reached), since Slave does not check ADV event timeout in connection state, the callback of “BLT_EV_FLAG_ADV_DURATION_TIMEOUT” is not triggered.
blc_ll_setAdvCustomedChannel
The API below serves to customize special advertising channel/scanning channel, and it only applies some special applications such as BLE mesh. It’s not recommended to use this API for other conventional application cases.
void blc_ll_setAdvCustomedChannel (u8 chn0, u8 chn1, u8 chn2);
chn0/chn1/chn2: customized channel. Default standard channel is 37/38/39. For example, to set three advertising channels as 2420MHz, 2430MHz and 2450MHz, the API below should be invoked:
blc_ll_setAdvCustomedChannel (8, 12, 22);
bls_ll_continue_adv_after_scan_req
The following API is used to set whether to continue sending advertising packets when receiving a scan request within the current "advertising interval".
void bls_ll_continue_adv_after_scan_req(u8 enable);
By default, assuming a "SCAN REQ" is received on channel 37, subsequent channel 38 and channel 39 advertising packets are not sent, if the user wishes to continue sending, can call "bls_ll_continue_adv_after_scan_req(1)" to enable.
bls_set_advertise_prepare
The following API is used to register a callback function before the start of the current advertising, used to determine whether to send advertising packets.
void bls_set_advertise_prepare (void *p);
p is as follows.
typedef int (*advertise_prepare_handler_t) (rf_packet_adv_t * p);
If the user wishes to process some tasks before each advertising transmission or stop a specific advertising transmission, implement it by registering this callback function. When the return value of the callback function is 0, it stops the transmission of this advertising. When the return value of the callback function is non-zero, this advertising transmits normally.
rf_set_power_level_index
This BLE SDK provides the API to set output power for BLE RF packet, as shown below.
void rf_set_power_level_index (RF_PowerTypeDef level)
The setting of the "level" value refers to the enumeration variable "RF_PowerTypeDef" defined in "drivers/8258(8278)/lib/include/rf_drv.h".
The Tx power configured by this API will take effect for both ADV packet and conn packet, and it can be set freely in firmware. The actual Tx power will be determined by the latest setting. Please note that the “rf_set_power_level_index” configures registers related to MCU RF. Once MCU enters sleep (suspend/deepsleep retention), these registers’ values will be lost, so they should be reconfigured after each wakeup. For example, SDK demo employs the event callback “BLT_EV_FLAG_SUSPEND_EXIT” to guarantee RF power is recovered after wakeup from sleep.
_attribute_ram_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);
blc_ll_setScanParameter
Please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.10 LE Set Scan Parameters Command.
ble_sts_t blc_ll_setScanParameter (u8 scan_type,
u16 scan_interval, u16 scan_window,
own_addr_type_t ownAddrType,
scan_fp_type_t scanFilter_policy);
Parameter analysis:
(1) scan_type
The user can select "passive scan" and "active scan", the difference is that "active scan" sends "SCAN_REQ" when receiving "ADV packet" to acquire more information of device "SCAN_RSP", the received "SCAN_RSP" packet is also transmitted to "BLE Host" through "ADV report event"; while "passive scan" does not send "SCAN_REQ".
/* LE_Scan_Type */
typedef enum {
SCAN_TYPE_PASSIVE = 0x00,
SCAN_TYPE_ACTIVE = 0x01,
} scan_type_t;
(2) scan_interval/scan_window
"scan_interval" sets the frequency switching time in "Scanning state", the unit is 0.625ms, "scan_window" is temporarily not processed in this "BLE SDK", the actual "scan window" is set to "scan_interval".
(3) ownAddrType
When specifying the SCAN_REQ packet address type, the 4 optional values for ownAddrType are as follows.
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 means that the public MAC Address is used for Scan, the actual address comes from the settings during the MAC Address initialisation API blc_initMacAddress(flash_sector_mac_address, mac_public, mac_ random_static).
OWN_ADDRESS_RANDOM indicates that a random static MAC Address is used for Scan, which is derived from the value set by the following API.
ble_sts_t blc_ll_setRandomAddr(u8 *randomAddr);
OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC means that the Controller generates a Resolvable Private Address based on the local IRK in the resolving list for Scan. If no matching entry is found in the resolving list, the public MAC Address is used.
OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM indicates that the Controller generates a Resolvable Private Address based on the local IRK in the resolving list for Scan. If no matching entry is found in the resolving list, the random static MAC Address is used.
(4) scan filter policy
The current supported scan filter policy are as follows:
typedef enum {
SCAN_FP_ALLOW_ADV_ANY = 0x00,
SCAN_FP_ALLOW_ADV_WL = 0x01,
SCAN_FP_ALLOW_UNDIRECTED_ADV = 0x02,
SCAN_FP_ALLOW_ADV_WL_DIRECT_ADV_MATCH = 0x03,
} scan_fp_type_t;
SCAN_FP_ALLOW_ADV_ANY means that the Link Layer does not filter the ADV packets from scan and reports them directly to the BLE Host.
SCAN_FP_ALLOW_ADV_WL requires that the ADV packets scanned should be in the whitelist before they are reported to the BLE Host.
When using the above two "scan filter policy" modes, unless the following conditions are met, "directed advertising PDU" will be ignored:
- The "TargetA" field is the same as the scanning device address, or
- The "TargetA" field is "RPA", address resolution is enabled, and address resolution is successful.
"SCAN_FP_ALLOW_UNDIRECTED_ADV" is basically the same as "SCAN_FP_ALLOW_ADV_ANY", "SCAN_FP_ALLOW_ADV_WL_DIRECT_ADV_MATCH" is basically the same as "SCAN_FP_ALLOW_ADV_WL", except for the conditions for ignoring "directed advertising PDU".
When using "SCAN_FP_ALLOW_UNDIRECTED_ADV" or "SCAN_FP_ALLOW_ADV_WL_DIRECT_ADV_MATCH" modes, unless the following conditions are met, "directed advertising PDU" will be ignored:
- The "TargetA" field is the same as the scanning device address, or
- The "TargetA" field is "RPA".
The return value of "blc_ll_setScanParameter" is always "BLE_SUCCESS"; as the "API" does not perform parameter validity checks, the "user" should ensure the validity of the parameters.
blc_ll_setScanEnable
Please refer to "Bluetooth Core Specification v5.3" [Vol 2] Part E, Section 7.8.11 LE Set Scan Enable Command.
ble_sts_t blc_ll_setScanEnable (scan_en_t scan_enable, dupFilter_en_t filter_duplicate);
The scan_enable parameter type has the following 2 optional values.
typedef enum {
BLC_SCAN_DISABLE = 0x00,
BLC_SCAN_ENABLE = 0x01,
} scan_en_t;
When scan_enable is BLC_SCAN_ENABLE, Enable Scanning; when scan_enable is BLC_SCAN_DISABLE, Disable Scanning.
(1) In Idle state, Enable Scanning, Link Layer enters Scanning state.
(2) In Scanning state, Disable Scanning, Link layer enters Idle state.
The filter_duplicate parameter type has 2 optional values as follows.
typedef enum {
DUP_FILTER_DISABLE = 0x00,
DUP_FILTER_ENABLE = 0x01,
} dupFilter_en_t;
When filter_duplicate is DUP_FILTER_ENABLE, duplicate packet filtering is enabled, and the Controller will only report the ADV report event to the Host once for each different ADV packet; when filter_duplicate is DUP_FILTER_DISABLE, duplicate packet filtering is not enabled, and the ADV packet scanned to the Host will will always be reported to the Host.
The return value ble_sts_t is as below.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CURRENT_STATE_ NOT_SUPPORTED_THIS_CMD | 0x83 | "Link Layer" is in "BLS_LINK_STATE_ADV"/"BLS_LINK_STATE_CONN" state |
| LL_ERR_INVALID_ PARAMETER | 0x84 | "Link Layer" has not initialized "SCAN" module |
When "scan_type" is set to "active scan", after "Enable Scanning", for each device, reads "SCAN_RSP" only once and reports to "Host". Because after each "Enable Scanning", the "Controller" records "SCAN_RSP" of different devices, stores them into "SCAN_RSP" list, ensures not to send "SCAN_REQ" to this device again later.
If the "user" needs to report "SCAN_RSP" of the same device multiple times, can set whether to enable the filter policy of sending "SCAN_REQ" through "API" "blc_ll_scanReq_filter_en". When calling "blc_ll_scanReq_filter_en(0)", for scannable advertising conforming to "scan filter policy", regardless of whether "SCAN_REQ" has been sent to this advertising, actively sends "SCAN_REQ", and reports "SCAN_RSP".
blc_ll_createConnection
Please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.12 LE Create Connection Command.
ble_sts_t blc_ll_createConnection( scan_inter_t scanInter, scan_wind_t scanWindow, init_fp_t initiator_fp, u8 peerAdrType, u8 *peerAddr, own_addr_type_t ownAdrType, conn_inter_t conn_min, conn_inter_t conn_max, u16 conn_latency, conn_tm_t timeout, u16 ce_min, u16 ce_max );
(1) scan_interval/scan window
scanInter sets the Scan frequency switching time in the Initiating state, in 0.625ms.
scanWindow is not handled in the Telink BLE SDK at the moment, the actual scan window is set to scanInter.
(2) initiator_fp
Specify the policy for the currently connected device, either of the following two options.
typedef enum {
INITIATE_FP_ADV_SPECIFY = 0x00, //connect ADV specified by host
INITIATE_FP_ADV_WL = 0x01, //connect ADV in whiteList
} init_fp_type_t;
"INITIATE_FP_ADV_SPECIFY" indicates the connected device address is the following "peerAdrType"/"peerAddr";
"INITIATE_FP_ADV_WL" indicates connecting to the devices in the "whitelist"; at this time, "peerAdrType"/"peerAddr" are meaningless.
(3) peerAdrType/peerAddr
When "initiator_filter_policy" is "INITIATE_FP_ADV_SPECIFY", a connection is established to the device whose address type is "peerAdrType" and address is "peerAddr".
(4) ownAdrType
Specifies the MAC Address type used by the Master role to establish a connection; the available values are as follows.
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 means that a public MAC address is used when connecting, the actual address is from the API blc_ll_initStandby_module (u8 *public_adr) set during MAC address initialization.
OWN_ADDRESS_RANDOM indicates that a random static MAC address is used when connecting, which is derived from the value set by the following API.
ble_sts_t blc_ll_setRandomAddr(u8 *randomAddr);
"OWN_ADDRESS_RESOLVE_PRIVATE_PUBLIC" means that the "Controller" generates a "Resolvable Private Address" based on the "local IRK" in the "resolving list" for connection. If no matching entry is found in the "resolving list", the "public MAC Address" is used.
"OWN_ADDRESS_RESOLVE_PRIVATE_RANDOM" indicates that the "Controller" generates a "Resolvable Private Address" based on the "local IRK" in the "resolving list" for connection. If no matching entry is found in the "resolving list", the "random static MAC Address" is used.
(5) conn_min/conn_max/conn_latency/timeout
These 4 parameters specify the connection parameters for the Master role once the connection is established, and these parameters are also sent to the Slave via the connection request, which will also have the same connection parameters.
conn_min/conn_max specifies the range of conn interval. The Telink BLE SDK for Master role Single Connection uses the value of conn_min directly. "conn_max" is not used. The unit is 0.625ms.
conn_latency specifies the connection latency, usually set to 0. timeout specifies the connection supervision timeout, in 10ms.
(6) ce_min/ce_max
The SDK does not handle ce_min/ce_max yet.
The return value table of "blc_ll_createConnection" is as below.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_CMD_DISALLOWED | 0x0C | The "Link Layer" is already in the "Initiating state" and does not accept a new "create connection" command |
| HCI_ERR_INVALID_HCI _CMD_PARAMS | 0x12 | "Own address type" is "OWN_ADDRESS_RANDOM", but "random static MAC Address" is not initialized |
| HCI_ERR_CONTROLLER _BUSY | 0x3A | "Link Layer" is in "Advertising state" or "Connection state" |
blc_ll_setCreateConnectionTimeout
ble_sts_t blc_ll_setCreateConnectionTimeout (u32 timeout_ms);
The return value is BLE_SUCCESS and the timeout_ms unit is ms.
According to the Link Layer state machine, when blc_ll_createConnection triggers the Idle state/Scanning state to enter the Initiating state, if the connection cannot be established for a long time, it will trigger Initiate timeout and exit the Initiating state.
Each time blc_ll_createConnection is called, the SDK defaults to the current Initiate timeout time of connection supervision timeout *2. If the User does not want to use the SDK default timeout, they can call blc_ll_createConnection immediately after the blc_ll_setCreateConnectionTimeout immediately after createConnection to set the desired Initiate timeout.
blc_ll_createConnectionCancel
Please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.13 LE Create Connection Cancel command.
ble_sts_t blc_ll_createConnectionCancel (void);
According to the introduction in the "Link Layer" state machine, when "blc_ll_createConnection" triggers the transition from the "Idle state"/"Scanning state" to the "Initiating state", if the "user" wishes to cancel the connection, the "API" "blc_ll_createConnectionCancel" can be used.
List of return values for "blc_ll_createConnectionCancel":
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_CMD_DISALLOWED | 0x0C | The "Link Layer" is already in the "Connection state" and cannot be cancelled |
blm_ll_updateConnection
Please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.18 LE Connection Update Command.
ble_sts_t blm_ll_updateConnection (u16 connHandle,
u16 conn_min, u16 conn_max, u16 conn_latency, u16 timeout,
u16 ce_min, u16 ce_max);
(1) connection handle
Specify the connection whose parameters need to be updated.
(2) conn_min/ conn_max/ conn_latency/ timeout
Specify the connection parameter to be updated. Master role single connection currently uses conn_min directly as the new interval.
(3) ce_min/ce_max
Not currently processed.
The return value ble_sts_t is only BLE_SUCCESS, the API does not check the reasonableness of the parameters, the user needs to pay attention to the reasonableness of the set parameters.
bls_ll_terminateConnection
ble_sts_t bls_ll_terminateConnection (u8 reason);
This API is used for BLE Slave device, and it only applies to Connection state Slave role.
In order to actively terminate connection, this API can be invoked by APP Layer to send a “Terminate” to Master in Link Layer. “reason” indicates reason for disconnection. Please refer to “Core_v5.0” (Vol 2/Part D/2 “Error Code Descriptions”).
If connection is not terminated due to system operation abnormity, generally APP layer specifies the “reason” as:
HCI_ERR_REMOTE_USER_TERM_CONN = 0x13
bls_ll_terminateConnection(HCI_ERR_REMOTE_USER_TERM_CONN);
In bottom-layer stack of Telink BLE SDK, this API is invoked only in one case to actively terminate connection: When data packets from peer device are decrypted, if an authentication data MIC error is detected, the “bls_ll_terminateConnection(HCI_ERR_CONN_TERM_MIC_FAILURE)” will be invoked to inform the peer device of the decryption error, and connection is terminated.
After Slave invokes this API to actively initiate disconnection, the event “BLT_EV_FLAG_TERMINATE” is triggered.
In Connection state Slave role, generally connection will be terminated successfully by invoking this API; however, in some special cases, the API may fail to terminate connection, and the error reason will be indicated by the return value. It’s recommended to check whether the return value is “BLE_SUCCESS” when this API is invoked by APP layer.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_CONN_NOT_ ESTABLISH | 0x3E | Link Layer is not in Connection state |
| HCI_ERR_CONTROLLER _BUSY | 0x3A | Controller busy (mass data are being transferred), this command cannot be accepted for the moment. |
blm_ll_disconnect
ble_sts_t blm_ll_disconnect (u16 handle, u8 reason);
This API is used for BLE Master devices and is only available for the Connection Master role.
This function is identical to the "Slave role" "API" "bls_ll_terminateConnection", except for the inclusion of an additional "conn handle" parameter. As the "Master role" in this "BLE SDK" is designed to maintain at most a single connection, the "handle" should be specified as "BLM_CONN_HANDLE".
The API returns the following values.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_UNKNOWN_CONN_ID | 0x02 | "Handle" error; the corresponding "connection" cannot be found |
Get Connection Parameters
The following APIs serves to obtain current connection paramters including Connection Interval, Connection Latency and Connection Timeout (only apply to Slave role).
u16 bls_ll_getConnectionInterval(void);
u16 bls_ll_getConnectionLatency(void);
u16 bls_ll_getConnectionTimeout(void);
a) If the "Link Layer" is currently in a non-connected state, meaning there are no connection parameters, the return value from calling the above "API" is 0.
b) If the "Link Layer" is currently in a connected state, the return value represents the parameter value:
-
Actual conn interval divided by 1.25ms will be returned by the API “bls_ll_getConnectionInterval”. Suppose current conn interval is 10ms, the return value should be 10ms/1.25ms=8.
-
Actual Latency value will be returned by the API “bls_ll_getConnectionLatency”.
-
Actual conn timeout divided by 10ms will be returned by the API “bls_ll_getConnectionTimeout”. Suppose current conn timeout is 1000ms, the return value would be 1000ms/10ms=100.
blc_ll_getCurrentState
The API below serves to obtain current Link Layer state.
u8 blc_ll_getCurrentState(void);
The user determines the current state at the application level, e.g.
if(blc_ll_getCurrentState() == BLS_LINK_STATE_ADV)
if(blc_ll_getCurrentState() == BLS_LINK_STATE_CONN)
blc_ll_getLatestAvgRSSI
The API serves to obtain latest average RSSI of connected peer device after Link Layer enters Slave role or Master role.
u8 blc_ll_getLatestAvgRSSI(void)
The return value is u8-type rssi_raw, and the real RSSI should be: rssi_real = rssi_raw- 110. Suppose the return value is 50, rssi = -60 dB.
Whitelist & Resolving list
As mentioned earlier, the "filter_policy" in the "Advertising"/"Scanning"/"Initiating state" involves the "Whitelist" and "Resolving list"; corresponding operations are performed based on the devices in the "Whitelist" and "Resolving list".
User can check whether address type of peer device is RPA (Resolvable Private Address) via “peer_addr_type” and “peer_addr”. The API below can be invoked directly.
#define IS_NON_RESOLVABLE_PRIVATE_ADDR(type, addr) ( (type)==BLE_ADDR_RANDOM && (addr[5] & 0xC0) == 0x00 )
#define IS_RESOLVABLE_PRIVATE_ADDR(Type, Addr) ( (Type)==BLE_ADDR_RANDOM && (Addr[5] & 0xC0) == 0x40 )
Only non-RPA address can be stored in whitelist. In current SDK, whitelist can store up to four devices.
The introduction to the "Whitelist"-related "APIs" is as follows.
(1) blc_ll_clearWhiteList
ble_sts_t blc_ll_clearWhiteList(void);
The return value of reset whitelist is “BLE_SUCCESS”.
(2) blc_ll_addDeviceToWhiteList
ble_sts_t blc_ll_removeDeviceFromWhiteList(u8 type, u8 *addr);
Add a device into whitelist, the return value is shown as below.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_MEM_CAP_EXCEEDED | 0x07 | Whitelist is already full, add failure |
| HCI_ERR_INVALID_HCI _CMD_PARAMS | 0x12 | "Address type" is "RPA", which is an invalid parameter |
(3)blc_ll_removeDeviceFromWhiteList
ble_sts_t blc_ll_removeDeviceFromWhiteList(u8 type, u8 *addr);
Delete a device from whitelist, the return value is “BLE_SUCCESS”.
RPA (Resolvable Private Address) device needs to use Resolving list. To save RAM space, “Resolving list” can store up to two devices in current SDK.
The introduction to the "Resolving list"-related "APIs" is as follows.
blc_ll_clearResolvingList:
ble_sts_t blc_ll_clearResolvingList(void);
Reset Resolving list, the return value is “BLE_SUCCESS”.
blc_ll_setAddressResolutionEnable:
ble_sts_t blc_ll_setAddressResolutionEnable (u8 resolutionEn);
To use the "Resolving list" for address resolution, address resolution should be enabled. It can be disabled when resolution is not required.
blc_ll_addDeviceToResolvingList:
ble_sts_t blc_ll_addDeviceToResolvingList(u8 peerIdAddrType, u8 *peerIdAddr,
u8 *peer_irk, u8 *local_irk);
To add a device using an "RPA", "peerIdAddrType", "peerIdAddr", and "peer_irk" should be populated with the "identity address type", "identity address", and "IRK" distributed by the "peer device", respectively. This information is stored in "Flash" during the pairing and encryption process; the "user" can find the "APIs" to obtain this information in the "SMP" introduction section.
When the "local device" needs to use an "RPA", "local_irk" needs to be populated with the "IRK" distributed by the "local device". This information is stored in "Flash" during the pairing and encryption process; the "user" can find the "APIs" to obtain this information in the "SMP" introduction section.
blc_ll_removeDeviceFromResolvingList:
ble_sts_t blc_ll_removeDeviceFromResolvingList(u8 peerIdAddrType, u8 *peerIdAddr);
This API serves to delete a RPA device from Resolvinglist.
blc_ll_setResolvablePrivateAddressTimeout:
ble_sts_t blc_ll_setResolvablePrivateAddressTimeout (u16 rpa_timeout_s);
Used to set the time interval for "RPA" updates in the "Controller"; the current default value is 900s. When the device uses a "Local RPA" and the timeout expires, the "controller" updates the "RPA". For specific details, please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.45 LE Set Resolvable Private Address Timeout command.
List of return values:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| HCI_ERR_INVALID_HCI _CMD_PARAMS | 0x12 | Invalid timeout value |
For the usage of "Whitelist"/"Resolving list" to implement address filtering, please refer to "TEST_WHITELIST" in the "SDK feature test demo".
For the usage of "Resolving list" to implement "peer device" "RPA" resolution, please refer to the "ble sample" or "ble remote" in the "SDK".
For the usage of "Resolving list" to implement "local device" "RPA" generation, please refer to "TEST_LL_PRIVACY_SLAVE" and "TEST_LL_PRIVACY_MASTER" in the "SDK feature test demo".
Coded PHY/2M PHY
Coded PHY/2M PHY Introduction
"Coded PHY" and "2M PHY" are new "Features" added in "Bluetooth Core Specification v5.0", significantly expanding the application scenarios of "BLE". "Coded PHY" includes "S2" (500 kb/s) and "S8" (125 kb/s) to accommodate longer-range applications, while "2M PHY" (2 Mb/s) greatly improves "BLE" bandwidth. "2M PHY"/"Coded PHY" can be used in "advertising channels" and also in "data channels" in the "connected state". The usage method in the "connected state" is introduced in this section, while the usage method in "advertising channels" is introduced in the "Extended Advertising" chapter.
Coded PHY/2M PHY Demo Introduction
In the "B85 BLE SDK" provided by "Telink", "Coded PHY"/"2M PHY" are disabled by default to save "SRAM". If the "user" chooses to use this "Feature", it can be enabled manually. For the method to enable it, please refer to the "Demo" provided by the "BLE SDK".
- For the "Slave" side, please refer to "TEST_2M_CODED_PHY_CONNECTION" in the "SDK feature test demo".
Define the macro in vendor/ble_feature_test/feature_config.h
#define FEATURE_TEST_MODE TEST_2M_CODED_PHY_CONNECTION
- Master end reference Demo “ble_master_kma_dongle”
Users can also choose to use other manufacturers' devices, as long as they support Coded PHY/2M PHY, they can interconnect with Telink's Slave devices.
If using Telink's SDK, Coded PHY and 2M PHY are also disabled by default on the Master end and need to be enabled by the following method.
Add API to the function void user_init(void) in vendor/ble_master_kma_dongle/app.c (disabled by default in SDK).
blc_ll_init2MPhyCodedPhy_feature();
Coded PHY/2M PHY API Introduction
(1) API
void blc_ll_init2MPhyCodedPhy_feature(void)
is used to enable Code PHY and 2M PHY.
(2) A new event - BLT_EV_FLAG_PHY_UPDATE is introduced to Telink Defined Event in order to support Coded and 2M PHY, the detail implementation could refer to section “Controller Event”.
(3) API:
ble_sts_t blc_ll_setPhy (u16 connHandle,le_phy_prefer_mask_t all_phys, le_phy_prefer_type_t tx_phys, le_phy_prefer_type_t rx_phys, le_ci_prefer_t phy_options);
This is a standard "BLE Spec" interface. Please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.49 LE Set PHY Command for details.
connHandle:slave mode: it should set to BLS_CONN_HANDLE; master mode: it should set to BLM_CONN_HANDLE.
For other parameters, please refer to Spec’s definition along with SDK’s enumeration definition.
(4) API blc_ll_setDefaultConnCodingIndication()
ble_sts_t blc_ll_setDefaultConnCodingIndication(le_ci_prefer_t prefer_CI);
This is not a standard "BLE Spec" interface. When the "Peer Device" actively initiates a "PHY_Req" using the "API" blc_ll_setPhy(), the recipient can use this "API" to set the "preferred Encode Mode" ("S2"/"S8") of the "local device".
Channel Selection Algorithm #2
"Channel Selection Algorithm #2" is a new "Feature" added in "Bluetooth Core Specification v5.0" that provides improved interference resilience. For specific details regarding the algorithm, please refer to "Bluetooth Core Specification v5.3" [Vol 6] Part B, Section 4.5.8.3 Channel Selection Algorithm #2.
The corresponding demo reference in BLE SDK.
- For the Slave side, please refer to TEST_CSA2 in the SDK feature test demo.
Define macro in vendor/ble_feature_test/feature_config.h as below:
#define FEATURE_TEST_MODE TEST_CSA2
a) If "Legacy Advertising" is used, "Channel Selection Algorithm #2" is optional and is disabled by default in the "SDK". If the "user" chooses to use "Channel Selection Algorithm #2", it should be enabled by calling the following "API".
void blc_ll_initChannelSelectionAlgorithm_2_feature(void)
b) If "Extended Advertising" is used and a connection needs to be initiated via "Extended Advertising", "Channel Selection Algorithm #2" should be enabled using the above "API". This is a requirement in the "Spec", as connections initiated via "Extended Adv" default to using "Channel Selection Algorithm #2" upon successful connection. If only the "Advertising" function is used, it is recommended not to enable "Channel Selection Algorithm #2" to save "SRAM".
- For the "Master" side, please refer to the "Demo" "ble_master_kma_dongle".
By default the master end frequency hopping algorithm #2 is also disabled, if needed the same API has to be enabled manually in the user_init() call.
void blc_ll_initChannelSelectionAlgorithm_2_feature(void);
Extended Advertising
Extended Advertising Introdcution
"Extended Advertising" is a new "Feature" added in "Bluetooth Core Specification v5.0".
Due to the expansion of the "Advertising" section in "Bluetooth Core Specification v5.0", several new "APIs" have been added to the "SDK" to implement "Legacy Advertising" and "Extended Advertising" functions. However, the three "APIs" introduced in the "Controller API" chapter—bls_ll_setAdvData(), bls_ll_setScanRspData(), and bls_ll_setAdvParam()—can only implement "Legacy Advertising" and cannot implement "Extended Advertising" functions.
The main features of "Extended Advertising" are as follows:
(1) Increased the maximum data length of "Advertising PDUs" from the "Legacy Advertising PDU" maximum length of 37 bytes in "Bluetooth Core Specification v4.2" to the "Extended Advertising PDU" maximum length of 255 bytes (length of a single "PDU" packet) in "Bluetooth Core Specification v5.0". If the "Advertising Data" length exceeds the "ADV PDU", it will be automatically fragmented into multiple "Advertising PDUs" for distribution.
(2) Allows for the flexible selection of different "PHYs" (1Mb/s, 2Mb/s, 125kb/s, 500kb/s) depending on the application scenario.
Extended Advertising Demo
For the "Extended Advertising Demo", please refer to "TEST_EXTENDED_ADVERTISING" in the "SDK feature test demo".
Enable the corresponding macro according to the type of packet intended to be sent. The "Demo" can cover all advertising packet types supported by "Bluetooth Core Specification v5.0", as shown below.
/* Advertising Event Properties type*/
typedef enum{
ADV_EVT_PROP_LEGACY_CONNECTABLE_SCANNABLE_UNDIRECTED = 0x0013,
ADV_EVT_PROP_LEGACY_CONNECTABLE_DIRECTED_LOW_DUTY = 0x0015,
ADV_EVT_PROP_LEGACY_CONNECTABLE_DIRECTED_HIGH_DUTY = 0x001D,
ADV_EVT_PROP_LEGACY_SCANNABLE_UNDIRECTED = 0x0012,
ADV_EVT_PROP_LEGACY_NON_CONNECTABLE_NON_SCANNABLE_UNDIRECTED = 0x0010,
ADV_EVT_PROP_EXTENDED_NON_CONNECTABLE_NON_SCANNABLE_UNDIRECTED = 0x0000,
ADV_EVT_PROP_EXTENDED_CONNECTABLE_UNDIRECTED = 0x0001,
ADV_EVT_PROP_EXTENDED_SCANNABLE_UNDIRECTED = 0x0002,
ADV_EVT_PROP_EXTENDED_NON_CONNECTABLE_NON_SCANNABLE_DIRECTED = 0x0004,
ADV_EVT_PROP_EXTENDED_CONNECTABLE_DIRECTED = 0x0005,
ADV_EVT_PROP_EXTENDED_SCANNABLE_DIRECTED = 0x0006,
ADV_EVT_PROP_EXTENDED_MASK_ANONYMOUS_ADV = 0x0020,
ADV_EVT_PROP_EXTENDED_MASK_TX_POWER_INCLUDE = 0x0040,
}advEvtProp_type_t;
Extended Advertising Related API
The "Extended Advertising" section adopts a modular design. Additionally, considering the uncertainty of the "ADV data length"/"scan response data" length, it is necessary to support data lengths of over 1000 bytes. However, we do not want fixed buffers in the underlying layer to cause "SRAM" waste, as most customers may use very short data lengths. Therefore, all "SRAM" required by the underlying layer is left to the "user layer" to define.
Current SDK only support one Advertising set, but with the design that has flexibility to support multiple ADV set for future as well. So you could see the APIs’ parameters are all designed in the way to support multiple ADV sets for future.
With that design, following are the APIs.
(1) Initialization stage, you would need to call the following APIs to allocate the SRAM.
blc_ll_initExtendedAdvertising_module(app_adv_set_param, app_primary_adv_pkt, APP_ADV_SETS_NUMBER);
blc_ll_initExtSecondaryAdvPacketBuffer(app_secondary_adv_pkt, MAX_LENGTH_SECOND_ADV_PKT);
blc_ll_initExtAdvDataBuffer(app_advData, APP_MAX_LENGTH_ADV_DATA);
blc_ll_initExtScanRspDataBuffer(app_scanRspData, APP_MAX_LENGTH_SCAN_RESPONSE_DATA);
According to above API, the memory allocation is shown as below:

-
APP_MAX_LENGTH_ADV_DATA:Advertising Set length, developer could adjust the macro to define the size based on the needs in order to save the DeepRetention space.
-
APP_MAX_LENGTH_SCAN_RESPONSE_DATA: Scan response data length, developer could adjust the macro to define the size based on the needs in order to save the DeepRetention space.
-
app_primary_adv_pkt:Primary Advertising PDU data length, the size is allocated as 44 bytes, user layer can’t change it.
-
app_secondary_adv_pkt:Secondary Advertising PDU data length, the size is allocated as 264 bytes, user layer can’t change it.
In the demo of "ble_feature_test", (vendor/ble_feature_test/feature_extend_adv/app.c), users can use the following macro to allocate the SRAM based on your requirement in order to best use the SRAM.
#define APP_ADV_SETS_NUMBER 1
#define APP_MAX_LENGTH_ADV_DATA 1024
#define APP_MAX_LENGTH_SCAN_RESPONSE_DATA 31
(2) API blc_ll_setExtAdvParam:
ble_sts_t blc_ll_setExtAdvParam(……);
This is a standard "BLE Spec" interface used to set advertising parameters. For details, please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.53 LE Set Extended Advertising Parameters Command, and understand it in conjunction with the "enum" type definitions and "demo" usage in the "SDK".
Note
- The parameter adv_tx_pow does not support the selection of the send power value at the moment, you need to call the API void rf_set_power_level_index (rf_power_level_index_e level) separately to configure the send power.
(3) API blc_ll_setExtScanRspData:
ble_sts_t blc_ll_setExtScanRspData(u8 advHandle, data_oper_t operation, data_fragment_t fragment_prefer, u8 scanRsp_dataLen, u8 *scanRspData);
This is a standard "BLE Spec" interface used to set "Scan Response Data". For details, please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.55 LE Set Extended Scan Response Command, and understand it in conjunction with the "enum" type definitions and "demo" usage in the "SDK".
(4) API blc_ll_setExtAdvEnable:
Currently, the "SDK" supports only one "ADV Set". Based on "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.56 LE Set Extended Advertising Enable Command, the "Telink SDK" provides a simplified "API" to enable/disable one "ADV Set" with higher execution efficiency. The simplified "API" is shown below; its "input parameters" and "return values" are identical to the standard, but it is used to set only a single "ADV Set".
ble_sts_t blc_ll_setExtAdvEnable_1(u32 extAdv_en, u8 sets_num, u8 advHandle, u16 duration, u8 max_extAdvEvt);
(5) API blc_ll_setAdvRandomAddr()
ble_sts_t blc_ll_setAdvRandomAddr(u8 advHandle, u8* rand_addr);
This is a standard "BLE Spec" interface used to set the "Random address" of the device. For details, please refer to "Bluetooth Core Specification v5.3" [Vol 4] Part E, Section 7.8.4 LE Set Random Address Command, and understand it in conjunction with the "enum" type definitions and "demo" usage in the "SDK".
(6) API blc_ll_setDefaultExtAdvCodingIndication:
void blc_ll_setDefaultExtAdvCodingIndication(u8 advHandle, le_ci_prefer_t prefer_CI);
This is a Non-BLE Spec standard interface, when setting advertising parameters with BLE standard API blc_ll_setExtAdvParam(), if set to Coded PHY (contains S2 and S8) but does not specify which Encode mode, SDK defaults to S2, to facilitate user selection, this API is defined to select preferred Encode mode(S2/S8).
The user can pass a reference via prefer_CI for S2/S8 mode selection, as enumerated below.
typedef enum {
CODED_PHY_PREFER_NONE = 0,
CODED_PHY_PREFER_S2 = 1,
CODED_PHY_PREFER_S8 = 2,
} le_ci_prefer_t; //LE coding indication prefer
(7) API blc_ll_setAuxAdvChnIdxByCustomers:
void blc_ll_setAuxAdvChnIdxByCustomers(u8 aux_chn);
This is a Non-BLE Spec standard interface, the user can set the channel value of the Auxiliary Advertising channel through this function, commonly used for debug, if the user does not call this function to define, the Auxiliary Advertising channel value will be generated randomly.
(8) API blc_ll_setMaxAdvDelay_for_AdvEvent:
void blc_ll_setMaxAdvDelay_for_AdvEvent(u8 max_delay_ms);
This is a non BLE Spec standard interface,used to configure the AdvDelay timing based on the ADV Interval, the input range is from 0, 1, 2, 4, 8 in the unit of ms.
advDelay(unit: us) = Random() % (max_delay_ms*1000);
T_advEvent = advInterval + advDelay
If max_delay_ms = 0, T_advEvent is accurate on the advInterval timing;
If max_delay_ms = 8, T_advEvent is based on the advInterval with a random offset in between 0-8ms.
BLE Host
BLE Host Introduction
BLE Host consists of L2CAP, ATT, SMP, GATT and GAP,etc. User-layer applications are implemented on the basis of the Host layer.
L2CAP
The "Logical Link Control and Adaptation Protocol", commonly abbreviated as "L2CAP", connects to the upper "APP layer" and the lower "Controller layer". By acting as an adaptor between the Host and the Controller, the L2CAP makes data processing details of the Controller become negligible to the upper-layer application operations.
The L2CAP layer of BLE is a simplified version of classical Bluetooth. In basic mode, it does not implement segmentation and re-assembly, has no involvement of flow control and re-transmission, and only uses fixed channels for communication. The figure below shows simple L2CAP structure: Data of the APP layer are sent in packets to the BLE Controller. The BLE Controller assembles the received data into different CID data and report them to the Host layer.

As specified in BLE Spec, L2CAP is mainly used for data transfer between Controller and Host. Most work are finished in stack bottom layer with little involvement of user. User only needs to invoke the following APIs to set correspondingly.
Register L2CAP Data Processing Function
In the BLE SDK architecture, the Controller's data is interfaced with the Host via the HCI, and the data from the HCI to the Host is first processed at the L2CAP layer, using the following API to register this processing function.
void blc_l2cap_register_handler (void *p);
In BLE Slave applications such as ble_remote/ble_module, the functions in the SDK L2CAP layer that process Controller data are:
int blc_l2cap_packet_receive (u16 connHandle, u8 * p);
This function has been implemented in the protocol stack and it will parse the received data and transmit it upwards to ATT, SMP or L2CAP Signaling.
Initialization:
blc_l2cap_register_handler (blc_l2cap_packet_receive);
In the ble_master kma dongle, the application layer contains the BLE Host function with the following processing functions, the source code of which is provided for user reference.
int app_l2cap_handler (u16 conn_handle, u8 *raw_pkt);
Initialization:
blc_l2cap_register_handler (app_l2cap_handler);
In the ble HCI, only the slave controller is implemented. The blc_hci_sendACLData2Host function transmits the controller data to the BLE Host device via a hardware interface such as UART/USB.
int blc_hci_sendACLData2Host (u16 handle, u8 *p)
Initialization:
blc_l2cap_register_handler (blc_hci_sendACLData2Host);
Update connection parameters
(1) Slave requests for connection parameter update
In BLE stack, Slave can actively apply for a new set of connection parameters by sending a “CONNECTION PARAMETER UPDATE REQUEST” command to Master in L2CAP layer. The figure below shows the command format. For details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part A, Section 4.20 L2CAP_CONNECTION_PARAMETER_UPDATE_REQ (CODE 0x12).

The BLE SDK provides an API for slaves to actively apply to update connection parameters on the L2CAP layer to send the above CONNECTION PARAMETER UPDATE REQUEST command to the master.
void bls_l2cap_requestConnParamUpdate (u16 min_interval, u16 max_interval, u16 latency, u16 timeout);
The four parameters of this API correspond to the parameters in the “data” field of the “CONNECTION PARAMETER UPDATE REQUEST”. The “min_interval” and “max_interval” are the actual interval time divided by 1.25ms (e.g. for 7.5ms connection interval, the value should be 6); the “timeout” is actual supervision timeout divided by 10ms (e.g. for 1s timeout, the value should be 100).
Application example: Slave requests for new connection parameters when connection is established.
void task_connect (u8 e, u8 *p, int n)
{
bls_l2cap_requestConnParamUpdate (6, 6, 99, 400);
bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(1000);
}

The API:
void bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(int time_ms)
serves to make the Slave wait for time_ms milliseconds after connection is established, and then invoke the API “bls_l2cap_requestConnParamUpdate” to update connection parameters. After connection is established, if user only invokes the “bls_l2cap_requestConnParamUpdate”, the Slave will wait for 1s to execute this request command.
In the "slave application", the "SDK" provides an interface to register a callback function to obtain the "L2CAP_CONNECTION_PARAMETER_UPDATE_RSP" result, used to notify the "user" whether the connection parameter request initiated by the "slave" was rejected or accepted by the "master". As shown in the figure above, the "master" accepted the "slave's" "L2CAP_CONNECTION_PARAMETER_UPDATE_REQ".
void blc_l2cap_registerConnUpdateRspCb(l2cap_conn_update_rsp_callback_t cb);
Please refer to the use case of Slave initialization:
blc_l2cap_registerConnUpdateRspCb(app_conn_param_update_response)
Following shows the reference for the callback function "app_conn_param_update_response".
int app_conn_param_update_response(u8 id, u16 result)
{
if(result == CONN_PARAM_UPDATE_ACCEPT){
//the LE master Host has accepted the connection parameters
}
else if(result == CONN_PARAM_UPDATE_REJECT){
//the LE master Host has rejected the connection parameter
}
return 0;
}
Note
- The "master" accepting the "slave's" "L2CAP_CONNECTION_PARAMETER_UPDATE_REQ" does not imply that the "connection parameters" will be updated immediately. The specific update timing is determined by the "LL_CONNECTION_UPDATE_IND" sent by the "master".
(2) Master responds to connection parameter update request
After Master receives the “CONNECTION PARAMETER UPDATE REQUEST” command from Slave, it will respond with a “CONNECTION PARAMETER UPDATE RESPONSE” command. For details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part A, Section 4.21 L2CAP_CONNECTION_PARAMETER_UPDATE_RSP (CODE 0x13).
The figure below shows the command format: if “result” is “0x0000”, it indicates the request command is accepted; if “result” is “0x0001”, it indicates the request command is rejected.
Whether actual Android/iOS device will accept or reject the connection parameter update request is determined by corresponding BLE Master. User can refer to Master compatibility test.

Telink's ble_master kma dongle handles the connection parameter update demo code for slave as follows.

After “L2CAP_CMD_CONN_UPD_PARA_REQ” is received in “L2CAP_CID_SIG_CHANNEL”, it will read interval_min (used as eventual interval), supervision timeout and long suspend time (interval * (latency +1)), and check the rationality of these data. If interval < 200ms, long suspend time<20s and supervision timeout >= 2* long suspend time, this request will be accepted; otherwise this request will be rejected. User can modify as needed based on this simple demo design.
No matter whether parameter request of Slave is accepted, the API below can be invoked to respond to this request.
void blc_l2cap_SendConnParamUpdateResponse( u16 connHandle, u8 req_id, conn_para_up_rsp result);
“connHandle” indicates current connection ID. “result” has two options to indicate “accept” and “reject”, respectively.
typedef enum{
CONN_PARAM_UPDATE_ACCEPT = 0x0000,
CONN_PARAM_UPDATE_REJECT = 0x0001,
}conn_para_up_rsp;
If the ble_master kma dongle accepts the Slave's request, it should send an update cmd to the Controller via API blm_ll_updateConnection within a certain amount of time, using host_update_conn_param_req on the demo code as a flag and initiates this update after a 50ms delay in the main_loop.

(3) Master updates connection parameters in Link Layer
After Master responds with “conn para update rsp” to accept the “conn para update req” from Slave, Master will send a “LL_CONNECTION_UPDATE_IND” command in Link Layer.

Slave will mark the final parameter as the instant value of Master after it receives the update request. When the instant value of Slave reaches this value, connection parameters are updated, and the callback of the event “BLT_EV_FLAG_CONN_PARA_UPDATE” is triggered.
The “instant” indicates connection event count value maintained by Master and Slave, and it ranges from 0x0000 to 0xffff. During a connection, Master and Slave should always have consistent “instant” value. When Master sends “conn_req” and establishes connection with Slave, Master switches state from scanning to connection, and clears the “instant” of Master to “0”. When Slave receives the “conn_req”, it switches state from advertising to connection, and clears the instant of Slave to “0”. Each connection packet of Master and Slave is a connection event. For the first connection event after the “conn_req”, the instant value is “1”; for the second connection event, the instant value is 2, and so on.
When Master sends a “LL_CONNECTION_UPDATE_IND”, the final parameter “instant” indicates during the connection event marked with “instant”, Master will use the values corresponding to the former connection parameters of the “LL_CONNECTION_UPDATE_IND” packet. After the “LL_CONNECTION_UPDATE_IND” is received, the new connection parameters will be used during the connection event when the instant of Slave equals the declared instant of Master, thus Slave and Master can finish switch of connection parameters synchronously.
ATT & GATT
GATT basic unit “Attribute”
GATT defines two roles: Server and Client. In this BLE SDK, Slave is Server, and corresponding Android/iOS device is Client. Server needs to supply multiple Services for Client to access.
Each Service of GATT consists of multiple Attributes, and each Attribute contains certain information. When multiple Attributes of different kinds are combined together, they can reflect a basic service.

The basic contents of Attribute are shown as below:
(1) Attribute Type: UUID
The UUID is used to identify Attribute type, and its total length is 16 bytes. In BLE standard protocol, the UUID length is defined as two bytes, since Master devices follow the same method to transform 2-byte UUID into 16 bytes.
When standard 2-byte UUID is directly used, Master should know device types indicated by various UUIDs. 8x5x BLE stack defines some standard UUIDs in “stack/service/hids.h” and “stack/ble /uuid.h”.
Telink proprietary profiles (OTA, MIC, SPEAKER, and etc.) are not supported in standard Bluetooth. The 16-byte proprietary device UUIDs are defined in “stack/ble/uuid.h”.
(2) Attribute Handle
Slave supports multiple Attributes which compose an Attribute Table. In Attribute Table, each Attribute is identified by an Attribute Handle value. After connection is established, Master will analyze and obtain the Attribute Table of Slave via “Service Discovery” process, then it can identify Attribute data via the Attribute Handle during data transfer.
(3) Attribute Value
Attribute Value corresponding to each Attribute is used as request, response, notification and indication data. In 8x5x BLE stack, Attribute Value is indicated by one pointer and the length of the area pointed by the pointer.
Attribute and ATT Table
To implement GATT Service on Slave, an Attribute Table is defined in this BLE SDK and it consists of multiple basic Attributes. Attribute definition is shown as below.
typedef struct attribute
{
u16 attNum;
u8 perm;
u8 uuidLen;
u32 attrLen;
u8* uuid;
u8* pAttrValue;
att_readwrite_callback_t w;
att_readwrite_callback_t r;
} attribute_t;
Below is Attribute Table given by the BLE SDK to illustrate the meaning of the above items. See app_att.c for the Attribute Table code, as shown below:
static const attribute_t my_Attributes[] = {
{ATT_END_H - 1, 0,0,0,0,0}, // total num of attribute
// 0001 - 0007 gap
{7,ATT_PERMISSIONS_READ,2,2,(u8*)(&my_primaryServiceUUID), (u8*)(&my_gapServiceUUID), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof(my_devNameCharVal),(u8*)(&my_characterUUID), (u8*)(my_devNameCharVal), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof(my_devName), (u8*)(&my_devNameUUID), (u8*)(my_devName), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof(my_appearanceCharVal),(u8*)(&my_characterUUID), (u8*)(my_appearanceCharVal), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof (my_appearance), (u8*)(&my_appearanceUUID), (u8*)(&my_appearance), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof(my_periConnParamCharVal),(u8*)(&my_characterUUID), (u8*)(my_periConnParamCharVal), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof (my_periConnParameters),(u8*)(&my_periConnParamUUID), (u8*)(&my_periConnParameters), 0},
// 0008 - 000b gatt
{4,ATT_PERMISSIONS_READ,2,2,(u8*)(&my_primaryServiceUUID), (u8*)(&my_gattServiceUUID), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof(my_serviceChangeCharVal),(u8*)(&my_characterUUID), (u8*)(my_serviceChangeCharVal), 0},
{0,ATT_PERMISSIONS_READ,2,sizeof (serviceChangeVal), (u8*)(&serviceChangeUUID), (u8*)(&serviceChangeVal), 0},
{0,ATT_PERMISSIONS_RDWR,2,sizeof (serviceChangeCCC),(u8*)(&clientCharacterCfgUUID), (u8*)(serviceChangeCCC), 0},
};
Note: The key word “const” is added before Attribute Table definition:
const attribute_t my_Attributes[] = { ... };
By adding the “const”, the compiler will store the array data to flash rather than RAM, while all contents of the Attribute Table defined in flash are read only and not modifiable.
(1) attNum
The “attNum” supports two functions.
The “attNum” can be used to indicate the number of valid Attributes in current Attribute Table, i.e. the maximum Attribute Handle value. This number is only used in the invalid Attribute item 0 of Attribute Table array.
{ATT_END_H - 1,0,0,0,0,0},
"attNum = ATT_END_H - 1" indicates that there are a total of "ATT_END_H - 1" "Attributes" in the current "Attribute Table".
In BLE, Attribute Handle value starts from 0x0001 with increment step of 1, while the array index starts from 0. When this virtual Attribute is added to the Attribute Table, each Attribute index equals its Attribute Handle value. After the Attribute Table is defined, Attribute Handle value of an Attribute can be obtained by counting its index in current Attribute Table array.
Traversing through all "Attributes" in the "Attribute Table" to the last one yields the number of valid "Attributes" in the current "Attribute Table", "attNum", which is "ATT_END_H - 1". If the user adds or deletes an "Attribute", "attNum" will automatically change along with "ATT_END_H".
The “attNum” can also be used to specify Attributes constituting current Service.
The UUID of the first Attribute for each Service should be “GATT_UUID_PRIMARY_SERVICE(0x2800)”; the first Attribute of a Service sets “attNum” and it indicates following “attNum” Attributes constitute current Service.
As shown in code above, for the gap service, the Attribute with UUID of “GATT_UUID_PRIMARY_SERVICE” sets the “attNum” as 7, it indicates the seven Attributes from Attribute Handle 1 to Attribute Handle 7 constitute the gap service.
Except for Attribute item 0 and the first Attribute of each Service, attNum values of all Attributes should be set as 0.
(2) perm
The “perm” is the simplified form of “permission” and it serves to specify access permission of current Attribute by Client.
The “perm” of each Attribute should be configured as one or combination of following 10 values.
#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
Note: Current SDK version does not support PERMISSION READ and PERMISSION WRITE yet.
(3) uuid and uuidLen
As introduced above, UUID supports two types: BLE standard 2-byte UUID, and Telink proprietary 16-byte UUID. The “uuid” and “uuidLen” can be used to describe the two UUID types simultaneously.
The “uuid” is an u8-type pointer, and “uuidLen” specifies current UUID length, i.e. the uuidLen bytes starting from the pointer are current UUID. Since Attribute Table and all UUIDs are stored in flash, the “uuid” is a pointer pointing to flash.
a) BLE standard 2-byte UUID:
For example, the Attribute “devNameCharacter” with Attribute Handle of 2, related code is shown as below:
#define GATT_UUID_CHARACTER 0x2803
static const u16 my_characterUUID = GATT_UUID_CHARACTER;
static const u8 my_devNameCharVal[5] = {
CHAR_PROP_READ,
U16_LO(GenericAccess_DeviceName_DP_H), U16_HI(GenericAccess_DeviceName_DP_H),
U16_LO(GATT_UUID_DEVICE_NAME), U16_HI(GATT_UUID_DEVICE_NAME)
};
{0,ATT_PERMISSIONS_READ,2,sizeof(my_devNameCharVal),(u8*)(&my_characterUUID), (u8*)(my_devNameCharVal), 0, 0},
“UUID=0x2803” indicates “character” in BLE and the “uuid” points to the address of “my_devNameCharVal” in flash. The “uuidLen” is 2. When Master reads this Attribute, the UUID would be “0x2803”.
b) Telink proprietary 16-byte UUID:
For example, the Attribute MIC of audio, related code is shown as below:
#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},
The “uuid” points to the address of “my_MicData” in flash, and the “uuidLen” is 16. When Master reads this Attribute, the UUID would be “0x000102030405060708090a0b0c0d2b18”.
(4) pAttrValue and attrLen
Each Attribute corresponds to an Attribute Value. The “pAttrValue” is an u8-type pointer which points to starting address of Attribute Value in RAM/Flash, while the “attrLen” specifies the data length. When Master reads the Attribute Value of certain Attribute from Slave, the “attrLen” bytes of data starting from the area (RAM/Flash) pointed by the “pAttrValue” will be read by this BLE SDK to Master.
Since UUID is read only, the “uuid” is a pointer pointing to flash; while Attribute Value may involve write operation into RAM, so the “pAttrValue” may points to RAM or flash.
For example, HID Information's Attribute, relevant code:
const u8 hidInformation[] =
{
U16_LO(0x0111), U16_HI(0x0111), // bcdHID (USB HID version)
0x00, // bCountryCode
0x01 // Flags
};
{0,ATT_PERMISSIONS_READ,2, sizeof(hidInformation),(u8*)(&hidInformationUUID), (u8*)(hidInformation), 0, 0},
In actual application, the key word “const” can be used to store the read-only 4-byte hid information “0x01 0x00 0x01 0x11” into flash. The “pAttrValue” points to the starting address of hidInformation in flash, while the “attrLen” is the actual length of hidInformation. When Master reads this Attribute, “0x01000111” will be returned to Master correspondingly.
Figure below shows a packet example captured by BLE sniffer when Master reads this Attribute. Master uses the “ATT_Read_Req” command to set the target AttHandle as 0x23 (35), corresponding to the hid information in Attribute Table of SDK.

For the Attribute “battery value” with Attribute Handle of 40, related code is as shown below:
u8 my_batVal[1] = {99};
{0,ATT_PERMISSIONS_READ,2,sizeof(my_batVal),(u8*)(&my_batCharUUID), (u8*)(my_batVal), 0, 0},
In actual application, the “my_batVal” indicates current battery level and it will be updated according to ADC sampling result; then Slave will actively notify or Master will actively read to transfer the “my_batVal” to Master. The starting address of the “my_batVal” stored in RAM will be pointed by the “pAttrValue”.
(5) Callback function w
The callback function w is write function with prototype as below:
typedef int (*att_readwrite_callback_t)(void* p);
User should follow the format above to define callback write function. The callback function w is optional, i.e. for an Attribute, user can select whether to set the callback write function as needed (null pointer indicates not setting callback write function).
The trigger condition for callback function w is: When Slave receives any Attribute PDU with Attribute Opcode as shown below, Slave will check whether the callback function w is set.
-
opcode = 0x12, Write Request.
-
opcode = 0x52, Write Command.
-
opcode = 0x18, Execute Write Request.
After Slave receives a write command above, if the callback function w is not set, Slave will automatically write the area pointed by the “pAttrValue” with the value sent from Master, and the data length equals the “l2capLen” in Master packet format minus 3; if the callback function w is set, Slave will execute user-defined callback function w after it receives the write command, rather than writing data into the area pointed by the “pAttrValue”. Note: Only one of the two write operations is allowed to take effect.
By setting the callback function w, user can process Write Request, Write Command, and Execute Write Request in ATT layer of Master. If the callback function w is not set, user needs to evaluate whether the area pointed by the “pAttrValue” can process the command (e.g. If the “pAttrValue” points to flash, write operation is not allowed; or if the “attrLen” is not long enough for Master write operation, some data will be modified unexpectedly.)



The void-type pointer p of the callback function w points to the value of Master write command. Actually p points to a memory area, the value of which is shown as the following structure.
typedef struct{
u8 type;
u8 rf_len;
u16 l2cap;
u16 chanid;
u8 att;
u16 handle;
u8 dat[20];
}rf_packet_att_data_t;
p points to "type", valid length of data is l2cap minus 3, and the first valid data is pw->dat[0].
Note
- The maximum length of 'dat' in this structure potentially exceeds 20 and depends on the effective MTU length, so the '20' in the structure holds no practical significance. The actual accessible data length is 'l2cap - 3', as shown below.
int my_WriteCallback (void *p)
{
rf_packet_att_data_t *pw = (rf_packet_att_data_t *)p;
int len = pw->l2cap - 3;
//add your code
//valid data is pw->dat[0] ~ pw->dat[len-1]
return 1;
}
The structure “rf_packet_att_data_t” above is available in the “stack/ble/ble_format.h”.
The return value of the callback function 'w' is not processed by the protocol stack by default and requires handling by the user at the upper layer. If the user requires the protocol stack to process the return value, the following API is available to enable this, where a return value of 0 indicates execution success, and other return values indicate execution failure (For Error Code details, refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.1.1 ATT_ERROR_RSP).
void blc_att_enableWriteReqReject (u8 WriteReqReject_en);
(6) Callback function r
The callback function r is read function with prototype as below:
typedef int (*att_readwrite_callback_t)(void* p);
If the user defines a callback read function, the format above should be followed. The callback function 'r' is optional. For a specific 'Attribute', the user sets a callback read function or leaves it unset (indicated by the null pointer '0' when no callback is set).
The trigger condition for callback function r is: When Slave receives any Attribute PDU with Attribute Opcode as shown below, Slave will check whether the callback function r is set.
-
opcode = 0x0A, Read Request.
-
opcode = 0x0C, Read Blob Request.
After Slave receives a read command above,
a) If the callback read function is set, Slave will execute this function, and determine whether to respond with “Read Response/Read Blob Response” according to the return value of this function.
-
If the return value is 1, Slave does not respond with “Read Response/Read Blob Response” to Master.
-
If the return value is not 1, Slave will automatically read “attrLen” bytes of data from the area pointed by the “pAttrValue”, and the data will be responded to Master via “Read Response/Read Blob Response”.
If the user requires the protocol stack to process the return value, the following API is available to enable this. When the return value is 0, the slave does not reply with a 'Read Response'; for other return values, it replies with an 'Error Response' (for 'Error Code' details, refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.1.1 ATT_ERROR_RSP).
void blc_att_enableReadReqReject (u8 ReadReqReject_en);
b) If the user does not set a callback read function, the slave reads 'attrLen' values from the area pointed to by the 'pAttrValue' pointer and replies to the master with a 'Read Response'/'Read Blob Response'.
If the user intends to modify the content of the 'Read Response'/'Read Blob Response' to be sent after receiving a 'Read Request'/'Read Blob Request' from the master, the user registers the corresponding callback function 'r' and modifies the content of the RAM pointed to by the 'pAttrValue' pointer inside the callback function.
(7) Attribute Table layout
Figure below shows Service/Attribute layout based on Attribute Table. The “attnum” of the first Attribute indicates the number of valid Attributes in current ATT Table; the remaining Attributes are assigned to different Services, the first Attribute of each Service is the “declaration”, and the following “attnum” Attributes constitute current Service. Actually the first item of each Service is a Primary Service.
#define GATT_UUID_PRIMARY_SERVICE 0x2800
const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;

(8) ATT table Initialization
GATT & ATT initialization only needs to transfer the pointer of Attribute Table in APP layer to protocol stack, and the API below is supplied:
void bls_att_setAttributeTable (u8 *p);
p is the pointer of Attribute Table.
Attribute PDU and GATT API
As required by BLE spec, the following Attribute PDU types are supported in current SDK.
-
Requests:Data request sent from Client to Server.
-
Responses:Data response sent by Server after it receives request from Client.
-
Commands:Command sent from Client to Server.
-
Notifications:Data sent from Server to Client.
-
Indications:Data sent from Server to Client.
-
Confirmations:Confirmation sent from Client after it receives data from Server.
The following is an analysis of all ATT PDUs at the ATT layer in conjunction with the Attribute structure and Attribute Table structure described previously.
(1) Read by Group Type Request, Read by Group Type Response
Please refer to “Core_v5.0” (Vol 3/Part F/3.4.4.9 and 3.4.4.10) for details about the “Read by Group Type Request” and “Read by Group Type Response” commands.
The “Read by Group Type Request” command sent by Master specifies starting and ending attHandle, as well as attGroupType. After the request is received, Slave will check through current Attribute Table according to the specified starting and ending attHandle, and find the Attribute Group that matches the specified attGroupType. Then Slave will respond to Master with Attribute Group information via the “Read by Group Type Response” command.

As shown above, Master requests from Slave for Attribute Group information of the “primaryServiceUUID” with UUID of 0x2800.
#define GATT_UUID_PRIMARY_SERVICE 0x2800
const u16 my_primaryServiceUUID = GATT_UUID_PRIMARY_SERVICE;
The following groups in Slave Attribute Table meet the requirement according to current demo code.
a) Attribute Group with attHandle from 0x0001 to 0x0007,
Attribute Value is SERVICE_UUID_GENERIC_ACCESS (0x1800).
b) Attribute Group with attHandle from 0x0008 to 0x000a,
Attribute Value is SERVICE_UUID_DEVICE_INFORMATION (0x180A).
c) Attribute Group with attHandle from 0x000B to 0x0025,
Attribute Value is SERVICE_UUID_HUMAN_INTERFACE_DEVICE (0x1812).
d) Attribute Group with attHandle from 0x0026 to 0x0028,
Attribute Value is SERVICE_UUID_BATTERY (0x180F).
e) Attribute Group with attHandle from 0x0029 to 0x0032,
Attribute Value is TELINK_AUDIO_UUID_SERVICE(0x11,0x19,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06, 0x05,0x04,0x03,0x02,0x01,0x00).
Slave responds to Master with the attHandle and attValue information of the five Groups above via the “Read by Group Type Response” command. The final ATT_Error_Response indicates end of response. When Master receives this packet, it will stop sending “Read by Group Type Request”.
(2) Find by Type Value Request, Find by Type Value Response
Please refer to "Core_v5.0" (Vol 3/Part F/3.4.3.3 and 3.4.3.4) for details on 'Find by Type Value Request' and 'Find by Type Value Response'.
The “Find by Type Value Request” command sent by Master specifies starting and ending attHandle, as well as AttributeType and Attribute Value. After the request is received, Slave will check through current Attribute Table according to the specified starting and ending attHandle, and find the Attribute that matches the specified AttributeType and Attribute Value. Then Slave will respond to Master with the Attribute via the “Find by Type Value Response” command.

(3) Read by Type Request, Read by Type Response
Please refer to "Core_v5.0" (Vol 3/Part F/3.4.4.1 and 3.4.4.2) for details on 'Read by Type Request' and 'Read by Type Response'.
The “Read by Type Request” command sent by Master specifies starting and ending attHandle, as well as AttributeType. After the request is received, Slave will check through current Attribute Table according to the specified starting and ending attHandle, and find the Attribute that matches the specified AttributeType. Then Slave will respond to Master with the Attribute via the “Read by Type Response”.

As shown above, Master reads the Attribute with attType of 0x2A00, i.e. the Attribute with Attribute Handle of 00 03 in Slave.
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},
In the “Read by Type response”, attData length is 8, the first two bytes are current attHandle “0003”, followed by 6-byte Attribute Value.
(4) Find information Request, Find information Response
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.3.1 and 3.4.3.2 for details on 'Find Information Request' and 'Find Information Response'.
The master sends a "Find information request", specifying the starting and ending attHandle. After receiving the command, the slave replies to the master through "Find information response" the UUIDs of all the starting and ending attHandle corresponding Attributes. As shown in the figure below, the master requires information of three Attributes with attHandle of 0x0016~0x0018, and Slave responds with corresponding UUIDs.

(5) Read Request, Read Response
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.3 and 3.4.4.4 for details on 'Read Request' and 'Read Response'.
The “Read Request” command sent by Master specifies certain attHandle. After the request is received, Slave will respond to Master with the Attribute Value of the specified Attribute via the “Read Response” command (If the callback function r is set, this function will be executed), as shown below.

(6) Read Blob Request, Read Blob Response
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.5 and 3.4.4.6 for details on 'Read Blob Request' and 'Read Blob Response'.
If some Slave Attribute corresponds to Attribute Value with length exceeding MTU_SIZE (It’s set as 23 in current SDK), Master needs to read the Attribute Value via the “Read Blob Request” command, so that the Attribute Value can be sent in packets. This command specifies the attHandle and ValueOffset. After the request is received, Slave will find corresponding Attribute, and respond to Master with the Attribute Value via the “Read Blob Response” command according to the specified ValueOffset. (If the callback function r is set, this function will be executed.)
As shown below, when Master needs the HID report map of Slave (report map length largely exceeds 23), first Master sends “Read Request”, then Slave responds to Master with part of the report map data via “Read response”; Master sends “Read Blob Request”, and then Slave responds to Master with data via “Read Blob Response”.

(7) Exchange MTU Request, Exchange MTU Response
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.2.1 and 3.4.2.2 for details on 'Exchange MTU Request' and 'Exchange MTU Response'.
As shown below, Master and Slave obtain MTU size of each other via the “Exchange MTU Request” and “Exchange MTU Response” commands.

During data access process of Telink BLE Slave GATT layer, if there’s data exceeding a RF packet length, which involves packet assembly and disassembly in GATT layer, Slave and Master need to exchange RX MTU size of each other in advance. Transfer of long packet data in GATT layer can be implemented via MTU size exchange.
a) User can register callback of GAP event and enable the eventMask “GAP_EVT_MASK_ATT_EXCHANGE_MTU” to obtain EffectiveRxMTU.
EffectiveRxMTU=min(ClientRxMTU, ServerRxMTU)。
The “GAP event” section of this document will introduce GAP event in detail.
b) Processing of long Rx packet data in Slave GATT layer
In this SDK, the Slave 'ServerRxMTU' defaults to 23, but the actual maximum 'ServerRxMTU' supports up to 'ATT_MTU_MAX_SDK_DFT_BUF' by default, meaning that fragmented data of 'ATT_MTU_MAX_SDK_DFT_BUF' bytes from the master correctly completes packet reassembly on the Slave side. When the application requires the use of packet reassembly from the master, use the following API to modify the Slave's RX size first:
ble_sts_t blc_att_setRxMtuSize(u16 mtu_size);
The return value is shown as below:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| ATT_ERR_INVALID_PARAMETER | 0xB0 | 'mtu_size' unavailable |
When Master GATT layer needs to send long packet data to Slave, Master will actively initiate “ATT_Exchange_MTU_REQ”, and Slave will respond with “ATT_Exchange_MTU_RSP”. “ServerRxMTU” is the configured value of the API “blc_att_setRxMtuSize”. If user has registered GAP event and enabled the eventMask “GAP_EVT_MASK_ATT_EXCHANGE_MTU”, “EffectiveRxMTU” and “ClientRxMTU” of Master can be obtained in the callback function of GAP event.
c) Processing of long Tx packet data in Slave GATT layer
When Slave needs to send long packet data in GATT layer, it should obtain Client RxMTU of Master first, and the eventual data length should not exceed ClientRxMTU.
First Slave should invoke the API “blc_att_setRxMtuSize” to set its ServerRxMTU. Suppose it’s set as 158.
blc_att_setRxMtuSize(158);
Then the API below should be invoked to actively initiate an “ATT_EXCHANGE_MTU_REQ”.
ble_sts_t blc_att_requestMtuSizeExchange (u16 connHandle, u16 mtu_size);
“connHandle” is ID of Slave connection, i.e. “BLS_CONN_HANDLE”, while “mtu_size” is ServerRxMTU.
blc_att_requestMtuSizeExchange(BLS_CONN_HANDLE, 158);
After the “ATT_EXCHANGE_MTU_REQ” is received, Master will respond with “ATT_EXCHANGE_MTU_RSP”. After receiving the response, the SDK will calculate EffectiveRxMTU. If user has registered GAP event and enabled the eventMask “GAP_EVT_MASK_ATT_EXCHANGE_MTU”, “EffectiveRxMTU” and “ClientRxMTU” will be reported to user.
(8) Write Request, Write Response
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.5.1 and 3.4.5.2 for details on 'Write Request' and 'Write Response'.
The “Write Request” command sent by Master specifies certain attHandle and attaches related data. After the request is received, Slave will find the specified Attribute, determine whether to process the data by using the callback function w or directly write the data into corresponding Attribute Value depending on whether the callback function w is set by user. Finally Slave will respond to Master via “Write Response”.
As shown in below, by sending “Write Request”, Master writes Attribute Value of 0x0001 to the Slave Attribute with the attHandle of 0x0016. Then Slave will execute the write operation and respond to Master via “Write Response”.

(9) Write Command
Please refer to “Core_v5.0” (Vol 3/Part F/3.4.5.3) for details about the “Write Command”.
The “Write Command” sent by Master specifies certain attHandle and attaches related data. After the command is received, Slave will find the specified Attribute, determine whether to process the data by using the callback function w or directly write the data into corresponding Attribute Value depending on whether the callback function w is set by user. Slave does not respond to Master with any information.
(10) Queued Writes
Queued Writes include ATT protocols such as Prepare Write Request/Response and Execute Write Request/Response. Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.5.3 for details on 'Write Command'.
“Prepare Write Request” and “Execute Write Request” can implement the two functions below.
a) Provide write function for long attribute value.
b) Allow to write multiple values in an atomic operation that is executed separately.
Similar to “Read_Blob_Req/Rsp”, “Prepare Write Request” contains AttHandle, ValueOffset and PartAttValue. That means Client can prepare multiple attribute values or various parts of a long attribute value in the queue. Thus, before executing the prepared queue indeed, Client can confirm that all parts of some attribute can be written into Server.
Note: Current SDK version only supports the write function of long attribute value with the maximum length not exceeding 244 bytes. If the length is greater than 244 bytes, the following API interface needs to be called to make changes to the prepare write buffer and its length.
void blc_att_setPrepareWriteBuffer(u8 *p, u16 len)
The figure below shows the case when Master writes a long character string “I am not sure what a new song” (byte number is far more than 23, and use the default MTU) into certain characteristic of Slave. First Master sends a “Prepare Write Request” with offset of 0x0000, to write the data “I am not sure what” into Slave, and Slave responds to Master with a “Prepare Write Response”. Then Master sends a “Prepare Write Request” with offset of 0x12, to write the data “ a new song” into Slave, and Slave responds to Master with a “Prepare Write Response”. After the write operation of the long attribute value is finished, Master sends an “Execute Write Request” to Slave. “Flags=1” indicates write result takes effect immediately. Then Slave responds with an “Execute Write Response” to complete the whole Prepare Write process.
As we can see, “Prepare Write Response” also contains AttHandle, ValueOffsetand PartAttValue in the request, so as to ensure reliable data transfer. Client can compare field value of Response with that of Request, to ensure correct reception of the prepared data.

(11) Handle Value Notification
Please refer to “Core_v5.0” (Vol 3/Part F/3.4.7.1).

The figure above shows the format of “Handle Value Notification” in BLE Spec.
This BLE SDK provides an API for 'Handle Value Notification' of a specific 'Attribute', callable by both the master and the slave. The user calls this API to push the data requiring notification to the underlying BLE software FIFO. The protocol stack pushes the data from the software FIFO to the hardware FIFO during the nearest packet transmission/reception interval, and finally transmits it via RF.
ble_sts_t blc_gatt_pushHandleValueNotify (u16 handle, u8 *p, int len);
'handle' corresponds to the 'attHandle' of the specific 'Attribute', 'p' serves as the head pointer to the continuous memory data to be sent, and 'len' specifies the byte count of the data to send. This API supports automatic packet fragmentation, using min('EffectiveMaxTxOctets', 'EffectiveRxMTU') as the minimum unit for fragmentation. It splits very long data into multiple BLE RF packets for transmission; therefore, 'len' supports large values.
When Link Layer is in Conn state, generally data will be successfully pushed into bottom-layer software FIFO by invoking this API; however, some special cases may result in invoking failure, and the return value “ble_sts_t” will indicate the corresponding error reason.
When this API is invoked in APP layer, it’s recommended to check whether the return value is “BLE_SUCCESS”. If the return value is not “BLE_SUCCESS”, a delay is needed to re-push the data.
The return value is shown as below:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| GATT_ERR_DATA_PENDING_DUE_TO_SERVICE_DISCOVERY_BUSY | 0xC4 | Service discovery phase active, cannot send data |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
Note
- The API "bls_att_pushNotifyData" in the old version is deprecated, and the user is recommended to use the above API.
(12) Handle Value Indication
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.7.2 for details on "Handle Value Indication".

The figure above shows the format of "Handle Value Indication" in the BLE Spec.
This BLE SDK provides an API for "Handle Value Indication" of a specific "Attribute", callable by both the "master" and the "slave". The user calls this API to push the data requiring indication to the underlying BLE software FIFO. The protocol stack pushes the data from the software FIFO to the hardware FIFO during the nearest packet transmission/reception interval, and finally transmits it via RF.
ble_sts_t blc_gatt_pushHandleValueIndicate (u16 connHandle, u16 attHandle, u8 *p, int len);
The BLE Spec stipulates that each indicated datum waits for the Master's confirmation to be considered successful, and the next indicate data is not sent until then.
When the Link Layer is in the "Conn state", calling this API directly generally successfully pushes data to the underlying software FIFO, but special circumstances cause API failure. The return value "ble_sts_t" indicates the corresponding error reason. It is recommended that the application layer check whether the return value is "BLE_SUCCESS" when calling this API. If not "BLE_SUCCESS", it requires waiting for a period before pushing again. The return value list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| GATT_ERR_DATA_PENDING_DUE_TO_SERVICE_DISCOVERY_BUSY | 0xC4 | Service discovery phase active, cannot send data |
| GATT_ERR_PREVIOUS_INDICATE_DATA_HAS_NOT_CONFIRMED | 0xC1 | Previous indicate data has not been confirmed by the master |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
Note
- The API "bls_att_pushIndicateData" in the old version is deprecated, and the user is recommended to use the above API.
(13) Handle Value Confirmation
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.7.3 for details on "Handle Value Confirmation".
Each time the application layer calls "blc_gatt_pushHandleValueIndicate" to send "indicate" data to the "master", the "master" replies with a "confirm", indicating confirmation of this data; only then can the "slave" proceed to send the next "indicate" data.

As seen in the figure above, the "Confirmation" does not specify which specific "handle" it confirms; instead, a single "Confirmation" is uniformly replied for "indicate" data on all different "handles".
To allow the application layer to know whether the transmitted "indicate data" has been "Confirmed", users can register a "GAP event" callback and enable the corresponding "eventMask": "GAP_EVT_GATT_HANDLE_VALUE_CONFIRM" to obtain the "Confirm" event. The "GAP event" section of this document provides a detailed introduction to "GAP events".
Additionally, this BLE SDK provides an API for "Handle Value Confirmation" of a specific "Attribute", callable by both the "master" and the "slave". The user calls this API to push the required "Confirm" to the underlying BLE software FIFO. The protocol stack pushes the data from the software FIFO to the hardware FIFO during the nearest packet transmission/reception interval, and finally transmits it via RF.
ble_sts_t blc_gatt_pushConfirm(u16 connHandle);
When the "Link Layer" is in the "Conn state", generally calling this API directly can successfully push data to the underlying software FIFO, but special circumstances may cause this API call to fail. The corresponding error reason can be understood based on the return value "ble_sts_t". It is recommended that the application layer check whether the return value is "BLE_SUCCESS" when calling this API. If it is not "BLE_SUCCESS", it is necessary to wait for a period before pushing again.
The return value list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
(14) Multiple Handle Value Notification
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.7.4 for details on "Multiple Handle Value Notification".

The figure above shows the format of "Multiple Handle Value Notification" in the "BLE Spec".
This "BLE SDK" provides an API for "Multiple Handle Value Notification", callable by both the "master" and the "slave". The user calls this API to push the data requiring notification to the underlying "BLE" software FIFO. The protocol stack pushes the data from the software FIFO to the hardware FIFO during the nearest packet transmission/reception interval, and finally transmits it via "RF".
ble_sts_t blc_gatt_pushMultiHandleValueNotify (u16 connHandle, atts_mulHandleNtf_t* lists, u8 listNum);
typedef struct {
u16 handle;
u16 length;
u8* value;
} atts_mulHandleNtf_t;
Here, "handle" corresponds to the "attHandle" of the corresponding "Attribute", "value" serves as the head pointer to the memory data to be sent, and "length" specifies the byte count of the data to send.
This API supports automatic packet fragmentation, using min("EffectiveMaxTxOctets", "EffectiveRxMTU") as the minimum unit for fragmentation. It splits very long data into multiple "BLE RF packets" for transmission; therefore, "len" supports large values. When the "Link Layer" is in the "Conn state", generally calling this API directly can successfully push data to the underlying software FIFO, but special circumstances may cause this API call to fail. The corresponding error reason can be understood based on the return value "ble_sts_t".
It is recommended that the application layer check whether the return value is "BLE_SUCCESS" when calling this API. If it is not "BLE_SUCCESS", it is necessary to wait for a period before pushing again.
The return value list follows.
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| GATT_ERR_DATA_PENDING_DUE_TO_SERVICE_DISCOVERY_BUSY | 0xC4 | Service discovery phase active, cannot send data |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(15) Error Response
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.1.1 for details on "Error Response".

As seen in the figure above, the "Error Response" is used to state that a specific request cannot be executed and to provide the reason.
This "BLE SDK" provides an API for the "Error Response" of a specific "Request", callable by both the "master" and the "slave". The user calls this API to push the required "Error Response" to the underlying "BLE" software FIFO. The protocol stack pushes the data from the software FIFO to the hardware FIFO during the nearest packet transmission/reception interval, and finally transmits it via "RF".
ble_sts_t blc_gatt_pushErrResponse(u16 connHandle, u8 reqOpcode, u16 attHdlInErr, u8 ErrorCode);
Here, the "reqOpcode" parameter should be set to the "Opcode" that generated this error request.
The "attHdlInErr" parameter should be set to the "attribute handle" in the original request that generated this error. If the original request has no "attribute handle", or if the request is not supported, the value of this field should be 0x0000.
For the "ErrorCode" parameter, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.1.1.
When the "Link Layer" is in the "Conn state", generally calling this API directly can successfully push data to the underlying software FIFO, but special circumstances may cause this API call to fail. The corresponding error reason can be understood based on the return value "ble_sts_t". It is recommended that the application layer check whether the return value is "BLE_SUCCESS" when calling this API. If it is not "BLE_SUCCESS", it is necessary to wait for a period before pushing again.
The return value list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
blc_att_setServerDataPendingTime_upon_ClientCmd
When the "Client" performs "SDP" immediately after connecting to the device, the discovered "Server" needs to reply promptly based on its service table upon receiving relevant query functions. At this time, the "TX buffer" is under heavy load. Therefore, if the user attempts to send data at this moment, the operation is likely to fail because the "RF tx_buffer" is full.
Consequently, we recommend using a controllable "pending" time to avoid this issue. The transmission of relevant data will be executed after "SDP" completion; until then, the data is suspended. The time can be modified via the API "blc_att_setServerDataPendingTime_upon_ClientCmd(u8 num_10ms)", where the parameter "step" is 10ms.
GATT Service Security
Before reading “GATT Service Security”, user can refer to section 3.3.4 SMP to learn basic knowledge related to SMP including LE pairing method, security level, and etc.
The figure below shows the mapping relationship between "GATT" service security levels and service requests provided by the "BLE spec". For details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part C, Section 10.3 "AUTHENTICATION PROCEDURE".

As shown in the figure above:
-
The first column marks whether currently connected Slave device is in encryption state;
-
The second column (local Device’s Access Requirement for service) is related to Permission Access setting for attributes in ATT table;
-
The third column includes four sub-columns corresponding to four levels of LE secuiry mode1 for current device pairing state:
a) No authentication and no encryption
b) Unauthenticated pairing with encryption
c) Authenticated pairing with encryption
d) Authenticated LE Secure Connections

The final implementation of GATT Service Security is related to parameter settings during SMP initialization, including the highest security level, permission access of attributes in ATT table. It is also related to Master, for example, suppose Slave sets the highest security level supported by SMP as “Authenticated pairing with encryption”, but the highest level supported by Master is “Unauthenticated pairing with encryption”; if the permission for some write attribute in ATT table is “ATT_PERMISSIONS_AUTHEN_WRITE”, when Master writes this attribute, an error will be responded to indicate “encryption level is not enough”.
User can set permission of attributes in ATT table to implement the application below:
Suppose the highest security level supported by Slave is “Unauthenticated pairing with encryption”, but it’s not hoped to trigger Master pairing by sending “Security Request” after connection, user can set the permission for CCC (Client Characteristic Configuration) attribute with nofity attribute as “ATT_PERMISSIONS_ENCRYPT_WRITE”. Only when Master writes the CCC, will Slave respond that security level is not enough and trigger Master to start pairing encryption.
Note:
- Security level set by user only indicates the highest security level supported by device, and GATT Service Secuiry can be used to realize control as long as ATT Permission does not exceed the highest level that takes effect indeed. For LE security mode1 level 4, if use only sets the level “Authenticated LE Secure Connections”, the setting supports LE Secure Connections only.
For the example of GATT security level, please refer to "b85m_feature_test/ feature_gatt_security/app.c".
ble master GATT
In the ble_master kma dongle, the following GATT API is provided for doing simple service discovery or other data access functions.
(1) Find Information Request
ble_sts_t blc_gatt_pushFindInformationRequest(u16 connHandle, u16 start_attHandle, u16 end_attHandle);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.3.1 for details.
The return value list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
(2) Find By Type Value Request
ble_sts_t blc_gatt_pushFindByTypeValueRequest (u16 connHandle, u16 start_attHandle, u16 end_attHandle, u16 uuid, u8* attr_value, int len);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.3.3 for details.
The return value list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_INVALID_PARAMETER | 0xC0 | "attr_value" or "len" is 0, invalid |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(3) Read By Type Request
ble_sts_t blc_gatt_pushReadByTypeRequest (u16 connHandle, u16 start_attHandle, u16 end_attHandle, u8 *uuid, int uuid_len);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.1.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_INVALID_PARAMETER | 0xC0 | "uuid" or "uuid_len" is 0, invalid |
(4) Read Request
ble_sts_t blc_gatt_pushReadRequest (u16 connHandle, u16 attHandle);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.3.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
(5) Read Blob Request
ble_sts_t blc_gatt_pushReadBlobRequest (u16 connHandle, u16 attHandle, u16 offset);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.5.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
(6) Read by Group Type Request
ble_sts_t blc_gatt_pushReadByGroupTypeRequest (u16 connHandle, u16 start_attHandle, u16 end_attHandle, u8 *uuid, int uuid_len);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.9.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_INVALID_PARAMETER | 0xC0 | "uuid" or "uuid_len" is 0, invalid |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(7) Read Multiple Request
ble_sts_t blc_gatt_pushReadMultiRequest(u16 connHandle, u8 numHandles, u16 *pHandle);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.7.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_INVALID_PARAMETER | 0xC0 | "uuid" or "uuid_len" is 0, invalid |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(8) Read Multiple Variable Request
ble_sts_t blc_gatt_pushReadMultiVariableRequest(u16 connHandle, u8 numHandles, u16 *pHandle);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.4.11.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_INVALID_PARAMETER | 0xC0 | "uuid" or "uuid_len" is 0, invalid |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(9) Write Request
ble_sts_t blc_gatt_pushWriteRequest (u16 connHandle, u16 attHandle, u8 *p, int len);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.5.1.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(10) Write Command
ble_sts_t blc_gatt_pushWriteCommand (u16 connHandle, u16 attHandle, u8 *p, int len);
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_DATA_LENGTH_EXCEED_MTU_SIZE | 0xC5 | Data length exceeds effective MTU size |
(11) Prepare Write Request
ble_sts_t blc_gatt_pushPrepareWriteRequest (u16 connHandle, u16 attHandle, u16 valOffset,u8 *data, int data_len);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.6.1.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
| GATT_ERR_INVALID_PARAMETER | 0xC0 | "uuid" or "uuid_len" is 0, invalid |
(12) Execute Write Request
ble_sts_t blc_gatt_pushExecuteWriteRequest(u16 connHandle,u8 value);
Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part F, Section 3.4.6.3.
The return values list follows:
| ble_sts_t | Value | ERR Reason |
|---|---|---|
| BLE_SUCCESS | 0 | Success |
| LL_ERR_CONNECTION_NOT_ ESTABLISH | 0x80 | Link Layer is in a non-connected state |
| LL_ERR_ENCRYPTION_BUSY | 0x82 | Encryption phase active, cannot send data |
| LL_ERR_TX_FIFO_NOT_ENOUGH | 0x81 | Large data task running, software Tx FIFO insufficient |
| SMP_ERR_PAIRING_BUSY | 0xA1 | Pairing phase active, cannot send data |
After sending corresponding commands such as ATT_FIND_INFORMATION_REQ or ATT_READ_REQ to the Slave using the methods referenced above, corresponding response information such as ATT_FIND_INFORMATION_RSP or ATT_READ_RSP will be received from the Slave. These can be processed within the following framework in "int app_l2cap_handler (u16 conn_handle, u8 *raw_pkt)":
if(ptrL2cap->chanId == L2CAP_CID_ATTR_PROTOCOL) //att data
{
if(pAtt->opcode == ATT_OP_EXCHANGE_MTU_RSP){
//add your code
}
if(pAtt->opcode == ATT_OP_FIND_INFO_RSP){
//add your code
}
else if(pAtt->opcode == ATT_OP_FIND_BY_TYPE_VALUE_RSP){
//add your code
}
else if(pAtt->opcode == ATT_OP_READ_BY_TYPE_RSP){
//add your code
}
else if(pAtt->opcode == ATT_OP_READ_RSP){
//add your code
}
else if(pAtt->opcode == ATT_OP_READ_BLOB_RSP){
//add your code
}
else if(pAtt->opcode == ATT_OP_READ_BY_GROUP_TYPE_RSP){
//add your code
}
else if(pAtt->opcode == ATT_OP_WRITE_RSP){
//add your code
}
}
Note
- The following APIs in the old version are deprecated, and the user is recommended to use the above APIs as replacements:
att_req_read, att_req_read_blob, att_req_read_multi, att_req_write, att_req_signed_write_cmd, att_req_prep_write, att_req_write_cmd, att_req_read_by_type, att_req_read_by_group_type, att_req_find_info, att_req_find_by_type.
SMP
Security Manager (SM) in BLE is mainly used to provide various encryption keys for LE device to ensure data security. Encrypted link can protect the original contents of data in the air from being intercepted, decoded or read by any attacker. Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part H Security Manager Specification for details on SMP.
SMP Security Level
BLE V4.2 Spec adds a new pairing method “LE Secure Connections” which further strengthens security. The pairing method in earlier version is called “LE legacy pairing”.
Recalling the section "GATT service Security", the following types of pairing status are available for local devices.

These four states correspond to the four levels of LE Security Mode 1. Please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part C, Section 10.2 LE SECURITY MODES for details.
-
No authentication and no encryption (LE security mode1 level1)
-
Unauthenticated pairing with encryption (LE security mode1 level2)
-
Authenticated pairing with encryption (LE security mode1 level3)
-
Authenticated LE Secure Connections (LE security mode1 level4)
Note
- Security level set by local device only indicates the highest security level that local device may reach. However, to reach the preset level indeed, the two factors below are important:
a) The supported highest security level set by peer Master device >= the supported highest security level set by local Slave device.
b) Both local device and peer device complete the whole pairing process (if pairing exsits) correctly as per the preset SMP parameters.
For example, even if the highest security level supported by Slave is set as “mode1 level3” (Authenticated pairing with encryption), when the highest security level supported by peer Master is set as “mode1 level1” (No authentication and no encryption), after connection Slave and Master does not execute pairing, and indeed Slave uses security mode1 level 1.
User can use the API below to set the highest security level supported by SM:
void blc_smp_setSecurityLevel(le_security_mode_level_t mode_level);
Following shows the definition for the enum type 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_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;
SMP Parameter Configuration
SMP parameter configuration In Telink BLE SDK is introduced according to the configuration of four SMP security levels. For Slave, SMP function currently can support the highest security level “LE security mode1 level4”; for master, currently the SMP function in the traditional pairing method can support the highest security level "LE security mode1 level2" (traditional pairing Just Works method).
(1) LE security mode1 level1
Level 1 indicates device does not support encryption pairing. If it’s needed to disable SMP function, user only needs to invoke the function below during initialization:
blc_smp_setSecurityLevel(No_Security);
It means the device does not implement pairing encryption for current connection. Even if the peer requests for pairing encryption, the device will reject it. It generally applies to the device that does not support encryption pairing process. As shown in the figure below, Master sends a pairing request, and Slave responds with “SM_Pairing_Failed”.

(2) LE security mode1 level2
Level 2 indicates device supports the highest security level “Unauthenticated_Pairing_with_Encryption”, e.g. “Just Works” pairing mode in legacy pairing and secure connection pairing method.
A. As introduced earlier, SMP supports legacy encryption and secure connection pairing. The SDK provides the API below to set whether the new encryption feature in BLE4.2 is supported.
void blc_smp_setPairingMethods (pairing_methods_t method);
Following shows the definition for the enum type paring_methods_t:
typedef enum {
LE_Legacy_Pairing = 0, // BLE 4.0/4.2
LE_Secure_Connection = 1, // BLE 4.2/5.0/5.1
}pairing_methods_t;
B. When using security level other than LE security mode1 level1, the API below should be invoked to initialize SMP parameter configuration, including flash initialization setting of bonded area.
int blc_smp_peripheral_init (void);
If only this API is invoked during initialization, the SDK will use default parameters to configure SMP:
-
The highest security level supported by default: Unauthenticated_Pairing_with_Encryption.
-
Default bonding mode: Bondable_Mode (store KEY that is distributed after pairing encryption into flash).
-
Default IO capability: IO_CAPABILITY_NO_INPUT_NO_OUTPUT.
The default parameters above follow the configuration of legacy pairing “Just Works” mode. Therefore invoking this API only is equivalent to configure LE security mode1 level2. LE security mode1 level2 has two types of setting:
A. Device supports initialization setting of “Just Works” in legacy pairing.
blc_smp_peripheral_init();
B. Device supports initialization setting of “Just Works” in secure connections.
blc_smp_setPairingMethods(LE_Secure_Connection);
blc_smp_peripheral_init();
(3) LE security mode1 level3
Level 3 indicates device supports the highest security level “Authenticated pairing with encryption”, e.g. “Passkey Entry” / “Out of Band” in legacy pairing mode.
As required by this level, device should support Authentication, i.e. legal identity of two pairing sides should be ensured. The three Authentication methods below are supported in BLE:
-
Method 1 with involvement of user, e.g. device has button or display capability, so that one side can display TK, while the other side can input the same TK (e.g. Passkey Entry).
-
Method 2: The two pairing sides can exchange information using the method of non-BLE RF transfer to implement pairing (e.g. Out of Band which transfers TK via NFC generally).
-
Method 3: Use the TK negotiated and agreed by two device sides (e.g. Just Works with TK 0 used by two sides). Since this method is Unauthenticated, the security level of “Just Works” corresponds to LE security mode1 level2.
Authentication can ensure the legality of two pairing sides, and this protection method is called MITM (Man-in-the-Middle) protection.
A. Device with Authentication should set its MITM flag or OOB flag. The SDK provides the two APIs below to set MITM flag and OOB flag.
void blc_smp_enableAuthMITM (int MITM_en);
void blc_smp_enableOobAuthentication (int OOB_en);
“MITM_en”/“OOB_en”: 1 - enable; 0 - disable.
B. As introduced earlier, SM provides three Authentication methods selectable depending on IO capability of two sides. The SDK provides the API below to set IO capability for current device.
void blc_smp_setIoCapability (io_capability_t ioCapability);
Following shows the definition for the enum type io_capability_t:
typedef enum {
IO_CAPABILITY_UNKNOWN = 0xff,
IO_CAPABILITY_DISPLAY_ONLY = 0,
IO_CAPABILITY_DISPLAY_YES_NO = 1,
IO_CAPABILITY_KEYBOARD_ONLY = 2,
IO_CAPABILITY_NO_INPUT_NO_OUTPUT= 3,
IO_CAPABILITY_KEYBOARD_DISPLAY = 4,
} io_capability_t;
C. The figure below shows the rule to use MITM flag and OOB flag in legacy pairing mode.

The OOB and MITM flag of local device and peer device will be checked to determine whether to use OOB method or select certain KEY generation method as per IO capability. As shown in the figure below, the SDK will select different KEY generation methods according to IO capability (Row/Column parameter type io_capability_t):

For details about the mapping relationship, please refer to “core5.0” (Vol3/Part H/2.3.5.1 Selecting Key Generation Method).
LE security mode1 level 3 supports the methods below to configure initial values:
A. Initialization setting of OOB for device with legacy pairing:
blc_smp_enableAuthMITM(1);
blc_smp_enableOobAuthentication(1);
blc_smp_peripheral_init(); // SMP parameter configuration should be placed before this API
Since the OOB transmission of the TK value is involved, the SDK provides relevant GAP events to the user at the application layer. Please refer to the "GAP event" chapter. The API provided for the user to set the Legacy OOB pairing TK value follows:
void blc_smp_setTK_by_OOB (u8 *oobData);
The parameter “oobData” indicates the head pointer for the array of 16-digit TK value to be set.
B. Initialization setting of Passkey Entry (PK_Resp_Dsply_Init_Input) for device with legacy pairing:
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_DISPLAY_ONLY);
blc_smp_peripheral_init();
Since displaying the TK value is involved, the protocol stipulates that the TK value should be randomly generated. However, to meet the debugging needs of some customers, the SDK provides an API at the application layer for the user to set a fixed TK value. The API follows:
void blc_smp_manualSetPinCode_for_debug(u16 connHandle, u32 pinCodeInput);
The parameter "pinCodeInput" indicates the set "PinCode" value, ranging from "1" to "999999". This is used in "Passkey Entry" mode where the "slave" displays the TK, the "master" needs to input the TK, and a fixed TK value is required for debugging purposes.
Note
- Since the protocol stipulates that a "PinCode" should be randomly generated each time, this API is not a secure standard usage and violates the security protocol. Customers are not recommended to use this API in actual applications.
- If the "user" uses this API to manually set the pin code within the correct range (1–999999), the callback event "GAP_EVT_SMP_TK_DISPLAY" can be ignored because the displayed pin code is the value set by this API.
- If the "pinCodeInput" value is 0 or greater than 999999, it serves to exit manual setting mode, and the "Pin Code" will be randomly generated by the protocol stack.
C. Initialization setting of Passkey Entry (PK_Init_Dsply_Resp_Input or PK_BOTH_INPUT) for device with legacy pairing:
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_KEYBOARD_ONLY);
blc_smp_peripheral_init(); // SMP parameter configuration should be placed before this API
Considering TK value input by user, the SDK provides related GAP event in the APP layer (see section 3.3.5.2 GAP event).
The API below serves to set TK value of Passkey Entry:
int blc_smp_isWaitingToSetPasskeyEntry(void);
A return value of 1 indicates that entering a TK value is required, and the customer needs to set the Passkey Entry TK value as soon as possible. A return value of 0 indicates that entering a TK value is not required.
The API provided for the user to set the Passkey Entry TK value follows:
int blc_smp_setTK_by_PasskeyEntry (u32 pinCodeInput);
The parameter “pinCodeInput” indicates the pincode value to be set and its range is 0~999999. It applies to the case of Passkey Entry method in which Master displays TK and Slave needs to input TK.
The KEY generation method finally adopted is related to SMP security level supported by two pairing sides. If Master only supports LE security mode1 level1, since Master does not support pairing encryption, Slave does not enable SMP function.
(4) LE security mode1 level4
Level 4 indicates device supports the highest security level “Authenticated LE Secure Connections”, e.g. Numeric Comparison/Passkey Entry/Out of Band in secure connection pairing mode.
LE security mode1 level4 supports the methods below to configure initial values:
A. Initialization setting of Numeric Comparison for device with secure connection pairing:
blc_smp_setPairingMethods(LE_Secure_Connection);
blc_smp_enableAuthMITM(1);
blc_smp_setIoCapability(IO_CAPABILITY_DISPLAY_YES_NO);
Considering display of numerical comparison result to user, the SDK provides related GAP event in the APP layer (see section 3.3.5.2 GAP event).
The API below serves to set numerical comparison result as “YES” or “NO”.
int blc_smp_isWaitingToCfmNumericComparison(void);
The API provided for the user to set the Numeric Comparison result "YES" or "NO" value follows:
void blc_smp_setNumericComparisonResult(bool YES_or_NO);
B. Initialization setting of Passkey Entry for device with secure connection pairing:
User initialization code of this part is almost the same with that of the configuration mode B/C (Passkey Entry in legacy pairing) in LE security mode1 level3, except that pairing method herein should be set as “secure connection pairing” at the start of initialization.
blc_smp_setPairingMethods(LE_Secure_Connection);
.....//Refer to configuration method B/C in LE security mode1 level3
C. Initialization setting of Out of Band for device with secure connection pairing:
blc_smp_enableSecureConnections(1);
blc_smp_setSecurityParameters(Bondable_Mode, 1, 1, 0, IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
blc_smp_generateScOobData(&sc_oob_data_cb.scoob_local,&sc_oob_data_cb.scoob_local_key);
blc_smp_peripheral_init();//SMP parameter configuration should be placed before this API.

Similar to the OOB part in Level 3, the "OOB_en" parameter needs to be set to 1 via the "blc_smp_setSecurityParameters" function during the initialization configuration phase. In this mode, since SC OOB authentication is implemented via UART, after the values required for pairing are generated by "blc_smp_generateScOobData" during the initialization phase, they will be printed out via UART. The user can perform OOB pairing according to the printed prompts.
Two APIs are introduced here:
(a) blc_smp_generateScOobData
int blc_smp_generateScOobData(smp_sc_oob_data_t *oob_data, smp_sc_oob_key_t *oob_key);
The specific definitions of the enumeration types smp_sc_oob_data_t and smp_sc_oob_key_t follow:
typedef struct {
u8 random[16];
u8 confirm[16];
}smp_sc_oob_data_t;
typedef struct {
u8 public_key[64];
u8 private_key[32];
}smp_sc_oob_key_t;
int blc_smp_setScOobData(u16 connHandle, const smp_sc_oob_data_t *oobd_local, const smp_sc_oob_data_t *oobd_remote);
(5) Several APIs related to SMP parameter configuration:
A. The API below serves to set whether to enable bonding function::
void blc_smp_setBondingMode(bonding_mode_t mode);
Following shows the enum type bonding_mode_t:
typedef enum {
Non_Bondable_Mode = 0,
Bondable_Mode = 1,
}bonding_mode_t;
For device with security level other than mode1 level1, bonding function should be enabled. Since the SDK has enabled bonding function by default, generally user does not need to invoke this API.
B. The API below serves to set whether to enable Key Press function:
void blc_smp_enableKeypress (int keyPress_en);
It indicates whether it’s supported to provide some necessary input status information for KeyboardOnly device during Passkey Entry.
C. The API below serves to set whether to enable key pairs for ECDH (Elliptic Curve Diffie-Hellman) debug mode:
void blc_smp_setEcdhDebugMode(ecdh_keys_mode_t mode);
Following shows the definition for the enum type 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;
This API is strictly used for secure connections pairing. Since secure connections utilize elliptic curve cryptography, which effectively prevents eavesdropping and renders sniffers unable to capture over-the-air (OTA) BLE packets, debugging can become challenging. To address this, the BLE Specification defines a set of Debug Curve private/public key pairs. By enabling this mode, BLE sniffer tools can decrypt the link using these predefined, known keys.
D. Following is a unified API to set whether to enable bonding, whether to enable MITM flag, whether to support OOB, whether to support Keypress notification, as well as to set supported IO capability(The previous documents are all separate configuration APIs. For the convenience of user settings, the SDK also provides a unified configuration API).
void blc_smp_setSecurityParameters (bonding_mode_t mode, int MITM_en, int OOB_en, int keyPress_en, io_capability_t ioCapability);
Definition for each parameter herein is consistent with the same parameter in the corresponding independent API.
Security Request Configuration
Only Slave can send SMP Security Request, so this part only applies to Slave device.
During phase 1 of pairing process, there’s an optional Security Request packet which serves to enable Slave to actively trigger pairing process to start. The SDK provides the API below to flexibly set whether Slave sends Security Request to Master immediately after connection/re-connection, or delay for pending_ms miliseconds before sending Security Request, or does not send Security Request, so as to implement different pairing trigger combination.
void blc_smp_configSecurityRequestSending( secReq_cfg newConn_cfg, secReq_cfg re_conn_cfg, u16 pending_ms);
Following shows the definition for the enum type secReq_cfg:
typedef enum {
SecReq_NOT_SEND = 0,
SecReq_IMM_SEND = BIT(0),
SecReq_PEND_SEND = BIT(1),
}secReq_cfg;
The meaning of each parameter is introduced as below:
-
SecReq_NOT_SEND:After connection is established, Slave does not send Security Request actively.
-
SecReq_IMM_SEND:After connection is established, Slave will send Security Request immediately.
-
SecReq_PEND_SEND:After connection is established, Slave will wait for pending_ms miliseconds and then determine whether to send Security Request.
(1) For the first connection, if the Pairing_request packet from the Master is received within pending_ms milliseconds, the Security Request will not be sent;
(2) For re-connection, if Master has already sent LL_ENC_REQ before pending_ms miliseconds to encrypt reconnection link, Slave does not send Security Request.
The parameter “newConn_cfg” serves to configure new device, while the parameter “reConn_cfg” serves to configure device to be reconnected. During reconnection, the SDK also supports the configuration whether to send purpose of pairing request: During reconnection for a bonded device, Master may not actively initiate LL_ENC_REQ to encrypt link, and Security Request sent by Slave will trigger Master to actively enrypt the link. Therefore, the SDK provides reConn_cfg configuration, and user can configure it as needed.
Note
- This API should be invoked before connection. It’s recommended to invoke it during initialization.
The input parameters for the API “blc_smp_configSecurityRequestSending” supports the nine combinations below:
| Parameter | SecReq_NOT_SEND | SecReq_IMM_SEND | SecReq_PEND_SEND |
|---|---|---|---|
| SecReq_NOT _SEND Not se reconnection (the (the | nd SecReq after the first connection or Not send para pending_ms is invalid). immediately send S para pending_ms is invalid). after reconnecti | ecReq after the first connection, and Not send ecReq after reconnection wait for pending_ms mil on. | ecReq after the first connection, and iseconds to send SecReq |
| SecReq_IMM _SEND Immedi and not send SecRe pending_ms is inva | ately send SecReq after the first connection, Immedi q after reconnection (the para or reconnection (t lid). after reconnection. | ately send SecReq after the first connection Immedi he para pending_ms is invalid). and wait for pendi | ately send SecReq after the first connection, ng_ms miliseconds to send SecReq |
| SecReq_PEND _SEND Wait f after the first co after reconnection | or pending_ms miliseconds to send SecReq Wait nnection, and not send SecReq after the first c . send SecReq after reconnection. | for pending_ms miliseconds to send SecReq Wait f onnection, and immediately after the first conne | or pending_ms miliseconds to send SecReq ction or reconnection. |
Following shows two examples:
(1) newConn_cfg: SecReq_NOT_SEND
reConn_cfg: SecReq_NOT_SEND
pending_ms: This parameter does not take effect.
When newConn_cfg is set as SecReq_NOT_SEND, it means new Slave device does not actively initiate Security Request, and it will only respond to the pairing request from the peer device. If the peer device does not send pairing request, encryption pairing is not executed. As shown in the figure below, when Master sends a pairing request packet “SM_Pairing_Req”, Slave will respond to it, but does not actively trigger Master to initiate pairing request.

When reConn_cfg is set as SecReq_NOT_SEND, it means device pairing has already been completed, and Slave does not send Security Reqeust after reconnection.
(2) newConn_cfg: SecReq_IMM_SEND
reConn_cfg: SecReq_NOT_SEND
pending_ms: This parameter does not take effect.
When newConn_cfg is set as SecReq_IMM_SEND, it means new Slave device will immediately send Security Request to Master after connection, to trigger Master to start pairing process. As shown in the figure below, Slave actively sends a SM_Security_Req to trigger Master to send pairing request.

When reConn_cfg is set as SecReq_NOT_SEND, it means Slave does not send Security Reqeust after reconnection.
The SDK also provides an API to send Security Request packet only for special use case. The APP layer can invoke this API to send Security Request at any time.
int blc_smp_sendSecurityRequest (void);
If user invokes the “blc_smp_configSecurityRequestSending” to control secure pairing request packet, the “blc_smp_sendSecurityRequest” should not be invoked.
SMP Bonding info
The SMP bonding information discussed here is for the Slave device. Users can refer to the code for setting advertising in the initialization of the SDK demo ble_remote.
The Slave can store pairing information for up to 8 Masters simultaneously, and all 8 devices can successfully reconnect. The following interface is used to set the maximum number of devices currently stored, which cannot exceed 8. If the user does not set it, the default value is 8.
ble_sts_t blc_smp_param_setBondingDeviceMaxNumber(int device_num);
If using blc_smp_param_setBondingDeviceMaxNumber (4) to set the max number as 4, after four devices have been paired, excuting pairing for the fifth device will automatically delete the pairing info of the earliest connected (first) device, so as to store the pairing info of the fifth device.
If using blc_smp_param_setBondingDeviceMaxNumber (2) to set the max number as 2, after two devices have been paired, excuting pairing for the third device will automatically delete the pairing info of the earliest connected (first) device, so as to store the pairing info of the third device.
The API below serves to obtain the number of currently bonded Master devices (successfully paired with Slave) stored in the flash.
u8 blc_smp_param_getCurrentBondingDeviceNumber(void);
Assuming a return value of 3, this means that there are currently 3 successfully paired devices stored on the flash, and that all 3 devices can be connected back successfully.
(1) Storage sequence for bonding info
Index is a concept related to BondingDeviceNumber. If current BondingDeviceNumber is 1, there’s only one bonding device whose index is 0; if BondingDeviceNumber is 2, there’re two bonding devices with index 0 and 1.
The SDK provides two methods to update device index, Index_Update_by_Connect_Order and Index_Update_by_Pairing_Order, i.e. update index as per the time sequence of lastest connection or pairing for devices. The API below serves to select index update method.
void bls_smp_setIndexUpdateMethod(index_updateMethod_t method);
Following shows the enum type index_updateMethod_t:
typedef enum {
Index_Update_by_Pairing_Order = 0, //default value
Index_Update_by_Connect_Order = 1,
} index_updateMethod_t;
Two index update methods are introduced below:
A. Index_Update_by_Connect_Order
If BondingDeviceNumber is 2, device index stored in Slave flash includes 0 and 1. Index sequence is updated by the order of the latest successful connection rather than the latest pairing. Suppose Slave is paired with MasterA and MasterB in sequence, since MasterB is the latest connected device, the index for MasterA is 0, while the index for MasterB is 1. Then reconnect Slave with MasterA. Now MasterA becomes the latest connected device, so the index for MasterB is 0, and the index for MasterA is 1.
If BondingDeviceNumber is 3, device index includes 0, 1 and 2. The index for the latest connected device is 2, and index for the earliest connected device is 0.
If BondingDeviceNumber is 4, device index includes 0, 1, 2 and 3. The index for the latest connected device is 3, and index for the earliest connected device is 0. Suppose Slave is paired with MasterA, MasterB, MasterC and MasterD in sequence, the index for the latest connected MasterD is 3. If Slave is reconnected with MasterB, the index for the latest connected MasterB is 3.
Since the upper limit for bonding devices is 4, please note the case when more than four Master devices are paired: When Slave is paired with MasterA, MasterB, MasterC and MasterD in sequence, pairing Slave with MasterE will make Slave delete the pairing info for MasterA; however, if Slave is reconnected with MasterA before pairing Slave with MasterE, since the sequence changes to B-C-D-A, the latest pairing operation between Slave and MasterE will delete the pairing info for MasterB.
B. Index_Update_by_Pairing_Order
If BondingDeviceNumber is 2, device index stored in Slave flash includes 0 and 1. Index sequence is updated by the order of the latest pairing. Suppose Slave is paired with MasterA and MasterB in sequence, since MasterB is the latest paired device, the index for MasterA is 0, while the index for MasterB is 1. Then reconnect Slave with MasterA. Now the index sequence for MasterA and MasterB is not changed.
If BondingDeviceNumber is 4, device index includes 0, 1, 2 and 3. The index for the latest paired device is 3, and the index for the earliest paired device is 0. Suppose Slave is paired with MasterA, MasterB, MasterC and MasterD in sequence, the index for the latest paired MasterD is 3. No matter how Slave is reconnected with MasterA/B/C/D, the index sequence is not changed.
Note
- When Slave is paired with MasterA, MasterB, MasterC and MasterD in sequence, pairing Slave with MasterE will make Slave delete the pairing info for MasterA; if Slave is reconnected with MasterA before pairing Slave with MasterE, since the sequence is still A-B-C-D, the latest pairing operation between Slave and MasterE will delete the pairing info for MasterA.
(2) Format for bonding info and related APIs
Bonding info of Master device is stored in flash with the format below:
typedef struct {
u8 flag;
u8 peer_addr_type;
u8 peer_addr[6];
u8 peer_key_size;
u8 peer_id_adrType;
u8 peer_id_addr[6];
u8 own_ltk[16];
u8 peer_irk[16];
u8 peer_csrk[16];
}smp_param_save_t;
Bonding info includes 64 bytes.
-
peer_addr_type and peer_addr indicate Master connection address in the Link Layer.
-
"peer_id_adrType" and "peer_id_addr" are the "identity address type" and "identity address" distributed by the "Master" during the "key distribution phase", respectively;
-
"peer_irk" is the "IRK" distributed by the "Master" during the "key distribution phase".
If the peer distributed an "IRK", the user needs to add the above information to the "resolving list" so that the "Slave" can resolve the peer's "RPA". For details, please refer to the code for setting advertising in the initialization of the "SDK demo ble_remote", as follows.
if(blc_app_isIrkValid(bondInfo.peer_irk)){
blc_ll_addDeviceToResolvingList(bondInfo.peer_id_adrType, bondInfo.peer_id_addr, bondInfo.peer_irk, NULL);
blc_ll_setAddressResolutionEnable(1);
}
The API "blc_app_isIrkValid" is used to determine whether the "IRK" is valid. A return value of 1 indicates that the "IRK" is valid, and a return value of 0 indicates that the "IRK" is invalid.
Users do not need to pay attention to other parameters in "smp_param_save_t".
The following API uses "index" to retrieve device information from "flash".
u32 bls_smp_param_loadByIndex(u8 index, smp_param_save_t* smp_param_load);
A return value of 0 indicates that retrieving information failed, while a non-zero value represents the start address of the "Flash" area for that information. For example, when the number of current "bonded devices" is 3, to retrieve the relevant information of the most recently connected device:
bls_smp_param_loadByIndex(2, …)
The API below serves to obtain bonding device info from flash by using Master address (connection address in the Link Layer).
u32 bls_smp_param_loadByAddr(u8 addr_type, u8* addr, smp_param_save_t* smp_param_load);
If the return value is 0, it indicates failure to get info; non-zero return value indicates starting flash address to store the info.
The API below is used for Slave device to erase all pairing info stored in local flash.
void bls_smp_eraseAllPairingInformation(void);
Note: Before invoking this API, please ensure the device is in non-connection state.
The API below is used for Slave device to configure address to store pairing info in flash.
void bls_smp_configPairingSecurityInfoStorageAddr(int addr);
User can set the parameter “addr” as needed, and please refer to the section 2.1.4 SDK flash space partition so as to determine a suitable flash area for bonding info storage.
Note
- The user should ensure that this API is called after the function "blc_gap_peripheral_init" and before other "SMP API" functions. In the case where this API interface is not called, the location where bonding information is stored in "FLASH" will be automatically adjusted based on the "flash" size.
| Flash Size | Addr |
|---|---|
| 512KB | 0x74000 |
| 1MB | 0xFC000 |
| 2MB | 0x1FC000 |
master SMP
The highest level of the master SMP function currently supported in the traditional pairing method is LE security mode1 level2 (traditional pairing Just Works method). The user can refer to the "ble_master kma dongle" and simply modify the macros in the "ble_master kma dongle/app_config.h" file as follows
#define BLE_HOST_SMP_ENABLE 0
If this macro is configured to 1, standard SMP is used: the highest security level supported by the configured master is LE security mode1 level2, which supports the traditional pairing Just Works method; if this macro is configured to 0, it means that the non-standard custom pairing management function is enabled.
(1) master enable SMP (set macro BLE_HOST_SMP_ENABLE as 1)
To use this security level configuration, the following API calls should be made to initialize the SMP parameters, including the initial configuration of the bound area FLASH:
int blc_smp_central_init (void);
If only this API is called during the initialization phase, the SDK will use the default parameters to configure SMP:
-
The highest security level supported by default: Unauthenticated_Pairing_with_Encryption.
-
The default bonding mode: Bondable_Mode (stores the KEY distributed after pairing encryption to FLASH).
-
Default IO capability is IO_CAPABILITY_NO_INPUT_NO_OUTPUT.
When the paired device supports LE security mode1 level2, the user is also required to configure the following three APIs.
void blm_smp_configPairingSecurityInfoStorageAddr (int addr);
void blm_smp_registerSmpFinishCb (smp_finish_callback_t cb);
void blm_host_smp_setSecurityTrigger(u8 trigger);
Three APIs are described below:
A. void blm_smp_configPairingSecurityInfoStorageAddr (int addr);
This API can be used to master the location of the device configuration binding information stored in FLASH, where the parameter addr can be modified according to actual needs.
B. void blm_smp_registerSmpFinishCb (smp_finish_callback_t cb);
This callback function is triggered after the key distribution in the third phase of the pairing is completed and the user can register at the application layer to get the pairing completion event.
C. void blm_host_smp_setSecurityTrigger(u8 trigger);
This API is mainly used to configure whether master initiates encryption and encrypts the link actively when connecting back. The specific parameters can be selected as follows.
#define SLAVE_TRIGGER_SMP_FIRST_PAIRING 0
#define MASTER_TRIGGER_SMP_FIRST_PAIRING BIT(0)
#define SLAVE_TRIGGER_SMP_AUTO_CONNECT 0
#define MASTER_TRIGGER_SMP_AUTO_CONNECT BIT(1)
Specifically: 1) When pairing for the first time, whether the master choose to initiate the pairing request or start pairing after receiving the Security Request from the slave; 2) When connecting back to a device that has already been paired, whether the master initiate the LL_ENC_REQ encrypted link or wait until it receives the Security Request from the slave. Generally, we will configure the master to initiate the pairing request for the first time and send LL_ENC_REQ when reconnecting.
The final user initialisation code reference is as follows and the user can refer to the "ble_master kma dongle".
blm_smp_configPairingSecurityInfoStorageAddr(0x78000);
blm_smp_registerSmpFinishCb(app_host_smp_finish);
blc_smp_central_init();
//SMP trigger by master
blm_host_smp_setSecurityTrigger(MASTER_TRIGGER_SMP_FIRST_PAIRING | MASTER_TRIGGER_SMP_AUTO_CONNECT);
As for the following APIs related to binding information on the master end, they are for use by the bottom layer master SMP protocol.
int tbl_bond_slave_search(u8 adr_type, u8 * addr);
int tbl_bond_slave_delete_by_adr(u8 adr_type, u8 *addr);
void tbl_bond_slave_unpair_proc(u8 adr_type, u8 *addr);
(2) Non-standard self-defined pairing management (set the macro “BLE_HOST_SMP_ENABLE” as 0)
When using self-defined pairing management, initialization related APIs are shown as below:
blc_smp_setSecurityLevel(No_Security);//disable SMP function
user_master_host_pairing_flash_init();//custom method
A. Design flash storage method
The default flash sector used for pairing is 0x78000 ~ 0x78FFF, and it’s modifiable in the “app_config.h”.
#define FLASH_ADR_PAIRING 0x78000
Starting from flash address 0x78000, every eight bytes form an area (named 8 bytes area). Each area can store MAC address of one Slave, and includes 1-byte bonding mark, 1-byte address type and 6-byte MAC address.
typedef struct {
u8 bond_mark;
u8 adr_type;
u8 address[6];
} macAddr_t;
All valid Slave MAC addresses are stored in 8 bytes areas successively: The first valid Slave MAC address is stored in 0x78000~0x78007, and the mark in 0x78000 is set as “0x5A” to indicate current address is valid. The second valid Slave MAC address is stored in the next 8 bytes area 0x78008~ 0x7800f and the mark in 0x78008 is set as “0x5A”.The third valid Slave MAC address is stored in the next 8 bytes area 0x78010~ 0x78017 and the mark in 0x78010 is set as “0x5A”.
To un-pair certain Slave device, it’s needed to erase its MAC address in the Dongle side by setting the mark of the corresponding 8 bytes area as “0x00”. For example, to erase the MAC addres of the first Slave device as shown above, user should set 0x78000 as “0x00”.
The reason to adopt this design is: During execution of program, the SDK cannot invoke the function “flash_erase_sector” to erash flash, since this operation takes 20~200ms to erase a 4kB sector of flash and thus will result in BLE timing error.
Mark of “0x5A” and “0x00” are used to indicate pairing storage and un-pairing erasing of all Slave MAC addresses. Considering 8 bytes areas may occupy the whole 4kB sector of flash and thus result in error, a special processing is added during initialization: Read info of 8 bytes areas starting from address 0x78000, and store all valid MAC addresses into Slave MAC table of RAM. During this process, it will check whether there’re too many 8 bytes areas. If yes, erase the whole sector and then write the contents of Slave MAC table in RAM back to 8 bytes areas starting from 0x78000.
B. Slave mac table
#define USER_PAIR_SLAVE_MAX_NUM 4
typedef struct {
u8 bond_mark;
u8 adr_type;
u8 address[6];
} macAddr_t;
typedef struct {
u32 bond_flash_idx[USER_PAIR_SLAVE_MAX_NUM];
macAddr_t bond_device[USER_PAIR_SLAVE_MAX_NUM];
u8 curNum;
} user_salveMac_t;
user_salveMac_t user_tbl_slaveMac;
The structure above serves to use Slave MAC table in RAM to maintain all paired devices. The macro “USER_PAIR_SLAVE_MAX_NUM” serves to set the max allowed number of maintainable paired devices, and the default value is 4 which indicates four paired device is maintainable. User can modify this value as needed.
Suppose the “USER_PAIR_SLAVE_MAX_NUM” is set as 3 to indicate up to three paired devices can be maintained. In the “user_tbl_slaveMac”, the “curNum” indicates the number of current valid Slave devices in flash, the array “bond_flash_idx” records offset relative to 0x78000 for starting address of each valid 8 bytes area in flash (When un-pairing certain device, based on corresponding offset, user can locate the mark of the 8 bytes area, and then write the mark as 0x00), while the array “bond_device” records MAC address.
C. Related APIs
Based on the design of flash storage and Slave MAC table above, user can invoke the APIs below.
a) user_master_host_pairing_flash_init
void user_master_host_pairing_flash_init(void);
This API should be invoked to implement flash initialization when enabling user-defined pairing management.
b) user_tbl_slave_mac_add
int user_tbl_slave_mac_add(u8 adr_type, u8 *adr);
Adds a "slave mac". A return value of 1 indicates success, and 0 indicates failure. This function needs to be called when a new device is paired. The function first determines whether the number of devices in the current "flash" and "slave mac table" has reached the maximum value. If the maximum value has not been reached, it unconditionally adds it to the "slave mac table" and stores it in an "8 bytes area" on "flash". If the maximum value has been reached, a strategy issue regarding processing is involved: whether to disallow pairing or directly overwrite the earliest one. The "Telink demo" method is to directly overwrite the earliest one. Since the "Telink" maximum pairing count is 1, overwriting the earliest one essentially means preempting the current paired device; it first uses "user_tbl_slave_mac_delete_by_index(0)" to delete the current device, and then adds the new one into the "slave mac table". Users can modify the implementation of this function according to their own strategies.
c) user_tbl_slave_mac_search
int user_tbl_slave_mac_search(u8 adr_type, u8 * adr)
This API serves to check whether the device is already available in Slave MAC table according to device address reported by ADV, i.e. whether the device sending ADV packet currently has already been paired with Master. The device that has already been paired can be directly reconnected.
d) user_tbl_slave_mac_delete_by_adr
int user_tbl_slave_mac_delete_by_adr(u8 adr_type, u8 *adr)
This API serves to delete MAC addr of certain paired device from Slave MAC table by specified address.
e) user_tbl_slave_mac_delete_by_index
void user_tbl_slave_mac_delete_by_index(int index)
This API serves to delete MAC addr of certain paired device from Slave MAC table by specified index. The parameter “index” indicates device pairing sequence. If the max pairing number is 1, the index for the paired device is always 0; if the max pairing number is 2, the index for the first paired device is 0, and the index for the second paired device is 1……
f) user_tbl_slave_mac_delete_all
void user_tbl_slave_mac_delete_all(void)
This API serves to delete MAC addr of all the paired devices from Slave MAC table.
g) user_tbl_salve_mac_unpair_proc
void user_tbl_salve_mac_unpair_proc(void)
This API serves to process un-pairing. The demo code adopts the processing method using the default max pairing number (1) to delete all paired devices. User can modify the implementation of the API.
D. Connection and pairing
When Master receives ADV packet reported by Controller, it will establish connection with Slave in the two cases below:
Invoke the function “user_tbl_slave_mac_search” to check whether current Slave device has already been paired with Master and un-pairing has not been executed. If yes, Master can automatically establish connection with the device.
master_auto_connect = user_tbl_slave_mac_search(pa->adr_type, pa->mac);
if(master_auto_connect) { create connection }
If current ADV device is not available in Slave MAC table, auto connection is not initiated, and it’s needed to check whether manual pairing condition is met. The SDK provides two manual pairing solutions by default. Premise: Current ADV device is close enough. Solution 1: The pairing button on Master Dongle is pressed. Solution 2: Current ADV data is pairing ADV packet data defined by Telink.
//manual pairing methods 1: button triggers
user_manual_pairing = dongle_pairing_enable && (rssi > -56); //button trigger pairing(rssi threshold, short distance)
//manual pairing methods 2: special pairing ADV data
if(!user_manual_pairing){ //special ADV pair data can also trigger pairing
user_manual_pairing = (memcmp(pa->data,telink_adv_trigger_pairing,sizeof(telink_adv_trigger_pairing)) == 0)
&& (rssi > -56);
}
if(user_manual_pairing) { create connection }
After connection triggered by manual pairing is established successfully, the current device is added into Slave MAC table when reporting “HCI_SUB_EVT_LE_CONNECTION_ESTABLISH”.
//manual pairing, device match, add this device to slave mac table
if(blm_manPair.manual_pair && blm_manPair.mac_type == pCon->peer_adr_type &&
!memcmp(blm_manPair.mac,pCon->mac, 6))
{
blm_manPair.manual_pair = 0;
user_tbl_slave_mac_add(pCon->peer_adr_type, pCon->mac);
}
E. Un-pairing
_attribute_ram_code_void host_pair_unpair_proc (void)
{
//terminate and unpair proc
static int master_disconnect_flag;
if(dongle_unpair_enable){
if(!master_disconnect_flag && blc_ll_getCurrentState() == BLS_LINK_STATE_CONN){
if( blm_ll_disconnect(cur_conn_device.conn_handle, HCI_ERR_REMOTE_USER_TERM_CONN) ==
BLE_SUCCESS){
master_disconnect_flag = 1;
dongle_unpair_enable = 0;
#if (BLE_HOST_SMP_ENABLE)
tbl_bond_slave_unpair_proc(cur_conn_device.mac_adrType, cur_conn_device.mac_addr);
#else
user_tbl_salve_mac_unpair_proc();
#endif
}
}
}
if(master_disconnect_flag && blc_ll_getCurrentState() != BLS_LINK_STATE_CONN){
master_disconnect_flag = 0;
}
}
As shown in the code above, when un-pairing condition is triggered, Master first invokes the “blm_ll_disconnect” to terminate connection, and then invokes the “user_tbl_salve_mac_unpair_proc” to process un-pairing. The demo code will directly delete all paired devices. In the default case, the max pairing number is 1, so only one device will be deleted. If user sets the max number larger than 1, the “user_tbl_slave_mac_delete_by_adr” or “user_tbl_slave_mac_delete_by_index” should be invoked to delete specified device.
The demo code provides two conditions to trigger un-pairing:
- The un-pairing button on Master Dongle is pressed.
- The un-pairing key value “0xFF” is received in “HID keyboard report service”.
User can modify un-pairing trigger method as needed.
SMP Failure Management
When SMP fails, a callback function can be used to control whether the connection is maintained or disconnected. This is implemented as follows.
a) Register the handler function for the gap layer and open the event mask with the event GAP_EVT_SMP_PAIRING_FAIL , as follows.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PAIRING_FAIL );
b) Modify the corresponding processing under this mask in the processing function.
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_pairingFailEvt_t* p = (gap_smp_pairingFailEvt_t*)para;
//operation wanted
}
break;
default:
break;
}
return 0;
}
GAP
GAP initialization
GAP initialization for Master and Slave is different. Slave uses the API below to initialize GAP.
void blc_gap_peripheral_init(void);
The Master initialises the GAP using the following API.
void blc_gap_central_init(void);
As introduced earlier, data transfer between the APP layer and the Host is not controlled via GAP; the ATT, SMP and L2CAP can directly communicate with the APP layer via corresponding interface. In current SDK version, the GAP layer mainly serves to process events in the Host layer, and GAP initialization mainly registers processing function entry for events in the Host layer.
GAP Event
GAP event is generated during the communication process of Host protocol layers such as ATT, GATT, SMP and GAP. As introduced earlier, current SDK supports two types of event: Controller event, and GAP (Host) event. Controller event also includes two sub types: HCI event, and Telink defined event.
GAP event processing is added in current BLE SDK, which enables the protocol stack to layer events more clearly and to process event communication in the user layer more conveniently. SMP related processing, such as Passkey input and notification of pairing result to user, is also included.
If user wants to receive GAP event in the APP layer, it’s needed to register the corresponding callback function, and then enable the corresponding mask.
Following shows the prototype and register interface for callback function of GAP event.
typedef int (*gap_event_handler_t) (u32 h, u8 *para, int n);
void blc_gap_registerHostEventHandler (gap_event_handler_t handler);
The “u32 h” in the callback function prototype is the mark of GAP event which will be frequently used in the bottom layer protocol stack.
Following lists some events which user may use.
#define GAP_EVT_SMP_PAIRING_BEGIN 0
#define GAP_EVT_SMP_PAIRING_SUCCESS 1
#define GAP_EVT_SMP_PAIRING_FAIL 2
#define GAP_EVT_SMP_CONN_ENCRYPTION_DONE 3
#define GAP_EVT_SMP_SECURITY_PROCESS_DONE 4
#define GAP_EVT_SMP_TK_DISPLAY 8
#define GAP_EVT_SMP_TK_REQUEST_PASSKEY 9
#define GAP_EVT_SMP_TK_REQUEST_OOB 10
#define GAP_EVT_SMP_TK_NUMERIC_COMPARE 11
#define GAP_EVT_SMP_KEYPRESS_NOTIFY 12
#define GAP_EVT_SMP_TK_SEND_SC_OOB_DATA 13
#define GAP_EVT_SMP_TK_REQUEST_SC_OOB 14
#define GAP_EVT_ATT_EXCHANGE_MTU 16
#define GAP_EVT_GATT_HANDLE_VALUE_CONFIRM 17
In the callback function prototype, “para” and “n” indicate data and data length of event. User can refer to the usage below in the “b85m_feature_test/feature_smp_security/app.c” and the implementation of the function “app_host_event_callback”.
blc_gap_registerHostEventHandler( app_host_event_callback );
The API below serves to enable the mask for GAP event.
void blc_gap_setEventMask(u32 evtMask);
Following lists the definition for some common eventMasks. For other event masks, user can refer to the “ble/gap/gap_event.h”.
#define GAP_EVT_MASK_SMP_PAIRING_BEGIN (1<<GAP_EVT_SMP_PAIRING_BEGIN)
#define GAP_EVT_MASK_SMP_PAIRING_SUCCESS (1<<GAP_EVT_SMP_PAIRING_SUCCESS)
#define GAP_EVT_MASK_SMP_PAIRING_FAIL (1<<GAP_EVT_SMP_PAIRING_FAIL)
#define GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE (1<<GAP_EVT_SMP_CONN_ENCRYPTION_DONE)
#define GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE (1<<GAP_EVT_SMP_SECURITY_PROCESS_DONE)
#define GAP_EVT_MASK_SMP_TK_DISPLAY (1<<GAP_EVT_SMP_TK_DISPLAY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY (1<<GAP_EVT_SMP_TK_REQUEST_PASSKEY)
#define GAP_EVT_MASK_SMP_TK_REQUEST_OOB (1<<GAP_EVT_SMP_TK_REQUEST_OOB)
#define GAP_EVT_MASK_SMP_TK_NUMERIC_COMPARE (1<<GAP_EVT_SMP_TK_NUMERIC_COMPARE)
#define GAP_EVT_MASK_SMP_KEYPRESS_NOTIFY (1<<GAP_EVT_SMP_KEYPRESS_NOTIFY)
#define GAP_EVT_MASK_SMP_TK_SEND_SC_OOB_DATA (1<<GAP_EVT_SMP_TK_SEND_SC_OOB_DATA)
#define GAP_EVT_MASK_SMP_TK_REQUEST_SC_OOB (1<<GAP_EVT_SMP_TK_REQUEST_SC_OOB)
#define GAP_EVT_MASK_ATT_EXCHANGE_MTU (1<<GAP_EVT_ATT_EXCHANGE_MTU)
#define GAP_EVT_MASK_GATT_HANDLE_VALUE_CONFIRM (1<<GAP_EVT_GATT_HANDLE_VALUE_CONFIRM)
If user does not set GAP event mask via this API, the APP layer does not receive notification when corresponding GAP event is generated.
Note
- For the description about GAP event below, it’s supposed that GAP event callback has been registered, and corresponding eventMask has been enabled.
(1) GAP_EVT_SMP_PAIRING_BEGIN
Event trigger condition: When entering connection state, Slave sends a SM_Security_Request command, and Master sends a Pairing Request to request for pairing. When Slave receives the pairing request, this event will be triggered to indicate that pairing starts.

Data length “n”: 4.
Pointer p: p points to data of a memory area, corresponding to the structure below.
typedef struct {
u16 connHandle;
u8 secure_conn;
u8 tk_method;
} gap_smp_pairingBeginEvt_t;
“connHandle”: current connection handle.
“secure_conn”: If it’s 1, secure encryption feature (LE Secure Connections) will be used; otherwise LE legacy pairing will be used.
“tk_method”: It indicates the method of TK value to be used in the subsequent pairing, e.g. JustWorks, PK_Init_Dsply_Resp_Input, PK_Resp_Dsply_Init_Input, Numric_Comparison.
(2) GAP_EVT_SMP_PAIRING_SUCCESS
Event trigger condition: This event will be generated when the whole pairing process is completed correctly. This phase is called “Key Distribution, Phase 3” of LE pairing phase. If there’s key to be distributed, the pairing success event will be triggered after the two sides have completed key distribution; otherwise the pairing success event will be triggered directly.
Data length “n”: 4.
Pointer p: p points to data of a memory area, corresponding to the structure below.
typedef struct {
u16 connHandle;
u8 bonding;
u8 bonding_result;
} gap_smp_pairingSuccessEvt_t;
“connHandle”: current connection handle.
“bonding”: If it’s 1, bonding function is enabled; otherwise bonding function is disabled.
“bonding_result”: It indicates bonding result. If bonding function is disabled, the result value should be 0. If bonding function is enabled, it’s also needed to check whether encryption Key is correctly stored in flash; if yes, the result value is 1; otherwise the result value is 0.
(3) GAP_EVT_SMP_PAIRING_FAIL
Event trigger condition: If Slave or Master does not conform to standard pairing flow, or pairing process is terminated due to abnormity such as error report during communication, this event will be triggered.
Data length “n”: 2.
Pointer p: p points to data of a memory area, corresponding to the structure below.
typedef struct {
u16 connHandle;
u8 reason;
} gap_smp_pairingFailEvt_t;
“connHandle”: current connection handle.
“reason”: It indicates the reason for pairing failure. Following lists some common reason values, and for other values, please refer to the file “stack/ble/smp/smp_const.h”.
The specific meaning of "pairing failed values" can be referred to in "Bluetooth Core Specification v5.3" [Vol 3] Part H, Section 3.5.5 Pairing Failed.
#define PAIRING_FAIL_REASON_CONFIRM_FAILED 0x04
#define PAIRING_FAIL_REASON_PAIRING_NOT_SUPPORTED 0x05
#define PAIRING_FAIL_REASON_DHKEY_CHECK_FAIL 0x0B
#define PAIRING_FAIL_REASON_NUMERIC_FAILED 0x0C
#define PAIRING_FAIL_REASON_PAIRING_TIMEOUT 0x80
#define PAIRING_FAIL_REASON_CONN_DISCONNECT 0x81
(4) GAP_EVT_SMP_CONN_ENCRYPTION_DONE
Event trigger condition: When Link Layer encryption is completed (the LL receives “start encryption response” from Master), this event will be triggered.
Data length “n”: 3.
Pointer p: p points to data of a memory area, corresponding to the structure below.
typedef struct {
u16 connHandle;
u8 re_connect;
} gap_smp_connEncDoneEvt_t;
“connHandle”: current connection handle.
“re_connect”: If it’s 1, it indicates fast reconnection (The LTK distributed previously will be used to encrypt the link); if it’s 0, it indicates current encryption is the first encryption.
(5) GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE
Event trigger condition: when pairing for the first time, it is triggered after the GAP_EVT_SMP_PAIRING_SUCCESS event, and in the fast reconnection, triggered after GAP_EVT_SMP_CONN_ENCRYPTION_DONE event.
Data length “n”: 3.
Pointer p: p points to data of a memory area, corresponding to the structure below.
typedef struct {
u16 connHandle;
u8 re_connect;
} gap_smp_securityProcessDoneEvt_t;
“re_connect”: If it’s 1, it indicates fast reconnection (The LTK distributed previously will be used to encrypt the link); if it’s 0, it indicates current encryption is the first encryption.
(6) GAP_EVT_SMP_TK_DISPLAY
Event trigger condition: After Slave receives a Pairing_Req from Master, as per the pairing parameter configuration of the peer device and the local device, the method of TK (pincode) value to be used for pairing will be known. If the method “PK_Resp_Dsply_Init_Input” is enabled, which means Slave displays 6-digit pincode and Master inputs 6-digit pincode, this event will be triggered.
Data length “n”: 4.
Pointer p: p points to an u32-type variable “tk_set”. The value is 6-digit pincode that Slave needs to inform the APP layer, and the APP layer needs to display the pincode.
During user debugging, if the 6-digit "PinCode" randomly generated by the underlying layer is not required, a user-specified "PinCode" can be manually set, for example "123456", by adopting the following API.
void blc_smp_manualSetPinCode_for_debug(u16 connHandle, u32 pinCodeInput);
User should get the 6-digit pincode from Slave and input the pincode on Master side (e.g. Mobile phone), to finish TK input and continue pairing process. If user has input wrong pincode, or has clicked “cancel”, the pairing process fails.
The demo “vendor/ble_feature/feature_smp_security/app.c” provides an example for Passkey Entry application.
case GAP_EVT_SMP_TK_DISPLAY:
{
char pc[7];
u32 pinCode = *(u32*)para;
sprintf(pc, "%d", pinCode);
tlkapi_printf(APP_LOG_EN, "TK display:%s\n", pc);
}
break;
(7) GAP_EVT_SMP_TK_REQUEST_PASSKEY
Event trigger condition: When the slave device enables the Passkey Entry mode and the PK_Init_Dsply_Resp_Input or PK_BOTH_INPUT pairing mode is used, this event will be triggered to notify the user that the TK value needs to be input. After receiving the event, the user needs to input the TK value through the IO input port (if the timeout is 30s, the pairing fails). The API for inputting the TK value: blc_smp_setTK_by_PasskeyEntry is explained in the "SMP parameter configuration" chapter.
Data length “n”: 0.
Pointer p: NULL.
(8) GAP_EVT_SMP_TK_REQUEST_OOB
Event trigger condition: When Slave device enables the OOB method of legacy pairing, this event will be triggered to inform user that 16-digit TK value should be input by the OOB method. After this event is received, user needs to input 16-digit TK value within 30s via IO input capability, otherwise pairing will fail due to timeout. For the API “blc_smp_setTK_by_OOB” to input TK value, please refer to section 3.3.4.2 SMP parameter configuration.
Data length “n”: 0.
Pointer p: NULL.
(9) GAP_EVT_SMP_TK_NUMERIC_COMPARE
Event trigger condition: After Slave receives a Pairing_Req from Master, as per the pairing parameter configuration of the peer device and the local device, the method of TK (pincode) value to be used for pairing will be known. If the method “Numeric_Comparison” is enabled, this event will be triggered immediately. For “Numeric_Comparison”, a method of SMP4.2 secure encryption, dialog window will pop up on both Master and Slave to show 6-digit pincode, “YES” and “NO”; user needs to check whether pincodes on the two sides are consistent, and decide whether to click “YES” to confirm TK check result is OK.
Data length “n”: 4.
Pointer p: p points to an u32-type variable “pinCode”. The value is 6-digit pincode that Slave needs to inform the APP layer. The APP layer needs to display the pincode, and supplies “YES or “NO” confirmation mechanism.
The demo “vendor/b85m_feature/feature_smp_security/app.c” provides an example for Numeric_Comparison application.
(10) GAP_EVT_ATT_EXCHANGE_MTU
Event trigger condition: This event will be triggered in either of the two cases below.
- Master sends “Exchange MTU Request”, and Slave responds with “Exchange MTU Response”.
- Slave sends “Exchange MTU Request”, and Master responds with “Exchange MTU Response”.
Data length “n”: 6.
Pointer p: p points to data of a memory area, corresponding to the structure below.
typedef struct {
u16 connHandle;
u16 peer_MTU;
u16 effective_MTU;
} gap_gatt_mtuSizeExchangeEvt_t;
"connHandle": current connection handle.
"peer_MTU": RX MTU value of the peer device.
"effective_MTU" = min(ClientRxMTU, ServerRxMTU). “ClientRxMTU” and “ServerRxMTU” indicate RX MTU size value of Client and Server respectively. After Master and Slave exchanges MTU size of each other, the minimum of the two values is used as the maximum MTU size value for mutual communication between them.
(11) GAP_EVT_GATT_HANDLE_VALUE_CONFIRM
Event trigger condition: Whenever the APP layer invokes the “blc_gatt_pushHandleValueIndicate” to send indicate data to Master, Master will respond with a confirmation for the data. This event will be triggered when Slave receives the confirmation.
Data length “n”: 0.
Pointer p: Null pointer.
(12) GAP_EVT_SMP_KEYPRESS_NOTIFY
Event trigger condition: During the "Passkey Entry" protocol, a peer device with "KeyboardOnly" IO capability sends a "Keypress Notification" to notify when a key is being entered or erased, triggering this event.
Data length "n": 3.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct {
u16 connHandle;
u8 ntfType;
} gap_smp_keypressNotifyEvt_t;
"connHandle" indicates the current connection handle.
"ntfType" indicates "Notification Type"; for details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part H, Section 3.5.8 Pairing Keypress Notification.
(13) GAP_EVT_SMP_TK_SEND_SC_OOB_DATA
Event trigger condition: After receiving the "Pairing Request" from the peer device, if "SC OOB" is adopted, this event is triggered to inform the user layer to decide whether to send "SC OOB" related information to the peer device (via "OOB" transmission) based on the "OOB" support status of both sides.
Data length "n": 3.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct {
u16 connHandle;
bool sendScOobData2RemoteFlag;
} gap_smp_TkSendScOobDataEvt_t;
"sendScOobData2RemoteFlag" indicates whether it is necessary to send "SC OOB" data; for details, please refer to the demo vendor/ble_feature_test/feature_smp_security/app.c provided by the "SDK".
case GAP_EVT_SMP_TK_SEND_SC_OOB_DATA:
{
gap_smp_TkSendScOobDataEvt_t* pEvt = (gap_smp_TkSendScOobDataEvt_t*)para;
if(pEvt->sendScOobData2RemoteFlag){
tlkapi_printf(APP_LOG_EN, "[APP][SMP] need to send SC OOB data to remote device\n");
tlkapi_printf(APP_LOG_EN, " Local SC OOB data-c(be) (by UART) %s\n", hex_to_str(sc_oob_data_cb.scoob_local.confirm, 16));
tlkapi_printf(APP_LOG_EN, "[APP][SMP] Send Local SC OOB data-r(be) (by UART) %s\n", hex_to_str(sc_oob_data_cb.scoob_local.random, 16));
}
else{
tlkapi_printf(APP_LOG_EN, "[APP][SMP] not need to send SC OOB data to remote device\n");
}
}
break;
(14) GAP_EVT_SMP_TK_REQUEST_SC_OOB
Event trigger condition: After receiving the "Pairing Random" sent by the peer, if "SC OOB" is adopted, this event is triggered to inform the user layer that, based on the "OOB" support status of both parties, it is necessary to transmit the "SC OOB" related information obtained via "OOB" to the "SMP layer".
Data length "n": 4.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct {
u16 connHandle;
u8 scOobLocalUsed;
u8 scOobRemoteUsed;
} gap_smp_TkRequestScOobDataEvt_t;
"connHandle" indicates the current connection handle.
"scOobLocalUsed" indicates whether the "Local device" has used "SC OOB", and "scOobRemoteUsed" indicates whether the "Remote device" has used "SC OOB".
For details, please refer to the demo vendor/ble_feature_test/feature_smp_security/app.c provided by the "SDK".
case GAP_EVT_SMP_TK_REQUEST_SC_OOB:
{
gap_smp_TkRequestScOobDataEvt_t* pEvt = (gap_smp_TkRequestScOobDataEvt_t*)para;
tlkapi_printf(APP_LOG_EN, "[APP][SMP] SC OOB scOobLocalUsed %d\n", pEvt->scOobLocalUsed);
tlkapi_printf(APP_LOG_EN, "[APP][SMP] SC OOB scOobRemoteUsed %d\n", pEvt->scOobRemoteUsed);
sc_oob_data_cb.scoob_remote_used = pEvt->scOobRemoteUsed;
sc_oob_data_cb.scoob_local_used = pEvt->scOobLocalUsed;
if(pEvt->scOobRemoteUsed || pEvt->scOobLocalUsed){
blc_smp_setScOobData(pEvt->connHandle, &sc_oob_data_cb.scoob_local, &sc_oob_data_cb.scoob_remote);
}
}
break;
Connection-oriented Channel(CoC)
"CoC" is an "L2CAP" channel that supports the establishment of connection-oriented channels. It uses a data transmission method based on "LE signaling flow control mode" packets, mainly used for transmitting large amounts of data between two "Bluetooth" devices, allowing for a maximum data length of 64KB.
The "CoC" example routine is provided in feature_COC_slave under ble_feature_test. A brief introduction to the routine is given below.
CoC Flow Control Method
Due to the large volume of data transmission in "CoC", a set of flow control protocols is defined at the protocol layer to implement flow control based on "credit" values.
When establishing a "CoC" connection, both parties exchange initial "credits". Subsequently, whenever an "L2CAP" packet is successfully sent, the peer's "credit" needs to be decremented by 1; when the peer's "credit" value equals 0, the local end cannot send any more "L2CAP" packets to the peer and should wait for the peer to report the "L2CAP_FLOW_CONTROL_CREDIT_IND" instruction before continuing to send. After the local end receives an "L2CAP" packet, it needs to decrement the local "credit" by 1. When the local "credit" value is less than 0, the "CoC" channel needs to be disconnected. This "SDK" performs some simplified processing: when the local "credit" is less than half of the initial "credit", it will automatically report "L2CAP_FLOW_CONTROL_CREDIT_IND".
CoC Module Initialization
Users can refer to the following module initialization function to perform initialization operations:
void app_l2cap_coc_init(void)
{
blc_coc_initParam_t regParam = {
.MTU = COC_MTU_SIZE,
.SPSM = 0x0080,
.createConnCnt = 1,
.cocCidCnt = COC_CID_COUNT,
};
int state = blc_l2cap_registerCocModule(®Param, cocBuffer, sizeof(cocBuffer));
if(state){}
}
The structure "blc_coc_initParam_t" is mainly used to define some "CoC" parameters, including:
(1) MTU
Maximum Transmission Unit, representing the maximum supported "SDU" size in the "CoC" channel. Its valid range is 23~65535;
(2) SPSM
The values of "SPSM" are divided into fixed values and dynamic values. The range of fixed values is 0x0001 ~ 0x007F, which are stipulated by the Bluetooth official for fixed services. Among them, the current "B85m single connection SDK" supports "IPSP" (0x0023) and "OTS" (0x0025). The range of dynamic values is 0x0080 ~ 0x00FF, which can be defined by the user for different services.
(3) createConnCnt
Supports the number of "CoC" connections actively created simultaneously on an "ACL" connection. The "B85m single connection SDK" only supports the simultaneous creation of a single "CoC" connection.
(4) cocCidCnt
The number of supported "CoC" connections. The maximum number of "CoC" connections that can be established is 64.
typedef struct{
u16 SPSM;
u16 MTU;
u16 createConnCnt;
u16 cocCidCnt;
u16 eattCidCnt;
}blc_coc_initParam_t;
After the "CoC" parameter settings are completed, call "blc_l2cap_registerCocModule" to complete the registration of the "CoC" module:
int blc_l2cap_registerCocModule(blc_coc_initParam_t* param, u8 *pBuffer, u16 buffLen);
CoC Commands
The "CoC" commands supported in this "SDK" are:
| code | Command | CID supported by current command |
|---|---|---|
| 0x06 | L2CAP_DISCONNECTION_REQ | 0x0005 |
| 0x07 | L2CAP_DISCONNECTION_RSP | 0x0005 |
| 0x14 | L2CAP_LE_CREDIT_BASED_CONNECTION_REQ | 0x0005 |
| 0x15 | L2CAP_LE_CREDIT_BASED_CONNECTION_RSP | 0x0005 |
| 0x16 | L2CAP_FLOW_CONTROL_CREDIT_IND | 0x0005 |
| 0x17 | L2CAP_CREDIT_BASED_CONNECTION_REQ | 0x0005 |
| 0x18 | L2CAP_CREDIT_BASED_CONNECTION_RSP | 0x0005 |
| 0x19 | L2CAP_CREDIT_BASED_RECONFIGURE_REQ | 0x0005 |
| 0x1A | L2CAP_CREDIT_BASED_RECONFIGURE_RSP | 0x0005 |
A "CID" of 0x0005 indicates transmission on the "LE signaling channel". In fact, some "CoC" commands also support transmission on the channel with "CID" 0x0001 (i.e., the "L2CAP signaling channel"), but since "Bluetooth Low Energy" does not use this channel, it is not within the scope of our discussion.
The following introduces several main commands.
(1) Request to establish an LE credit-based connection
In the "BLE protocol stack", the "slave" requests the "master" to establish an "LE credit-based CoC connection" through the "L2CAP" layer's "L2CAP_LE_CREDIT_BASED_CONNECTION_REQ" command. The format of this command is shown below; for details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part A, Section 4.22 L2CAP_LE_CREDIT_BASED_CONNECTION_REQ (CODE 0x14).

Some of the fields are described as follows: a) Source CID
Indicates the channel endpoint of the party sending the "CoC" connection request. After this channel is established, data packets sent by the peer will be sent to this "CID". The valid range of the "CID" is 0x0040 ~ 0xFFFF.
Note that for the same "ACL" connection, the "CIDs" of the two ends of the established "CoC" channel cannot be duplicated. That is: assume "Device A" and "Device B" establish a "CoC" channel, and "Device A's" "CID" is 0x0040, then "Device B's" "CID" can be any value from 0x0040 ~ 0xFFFF; then, when "Device A" and "Device B" want to establish a second "CoC" channel (while the first "CoC" channel is not disconnected), "Device A" cannot use 0x0040 again as the "CID" to establish a connection with "Device B", but can only choose a "CID" from 0x0041 ~ 0xFFFF. Similarly, "Device B's" "CID" cannot duplicate the "CID" of an already established "CoC" channel.
b) MPS
Indicates the maximum number of bytes for a "PDU" instruction. The valid range is 23 ~ 65533, but note that "MPS" <= "MTU".
c) Initial Credits
Indicates the number of frames that the local end can receive after the "CoC" connection is established. The valid range is 0 ~ 65535.
The "SDK" provides an "API" at the "L2CAP" layer for the "Slave" to actively request the establishment of a "CoC" channel. This "API" is used to send the aforementioned "L2CAP_LE_CREDIT_BASED_CONNECTION_REQ" command to the "Master".
ble_sts_t blc_l2cap_createLeCreditBasedConnect(u16 connHandle);
(2) LE Credit Based Connection Response
After the peer requests to establish an "LE credit" based connection, the local side receives the command and replies with the "L2CAP_LE_CREDIT_BASED_CONNECTION_RSP" command. For details, please refer to "Bluetooth Core Specification v5.3" (Vol 3/Part A/4.23 L2CAP_LE_CREDIT_BASED_CONNECTION_RSP (CODE 0x15)).
In this "SDK", if the "L2CAP_LE_CREDIT_BASED_CONNECTION_REQ" command sent by the peer is received, the "stack" will automatically process the reply "L2CAP_LE_CREDIT_BASED_CONNECTION_RSP", and the user does not need to perform other operations.
(3) Initiate Disconnection Request
In the "BLE protocol stack", the "slave" requests the "master" to disconnect the "CoC" connection through the "L2CAP_DISCONNECTION_REQ" command of the "L2CAP" layer. The command format is shown below; for details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part A, Section 4.6 L2CAP_DISCONNECTION_REQ (CODE 0x06).

Where:
a) Destination CID
Indicates the "CID" to be disconnected on the device receiving this request.
b) Source CID
Indicates the "CID" to be disconnected on the device sending this request.
The "SDK" provides an "API" on the "L2CAP" layer for the local side to actively request the disconnection of the "CoC" channel, used to send the "L2CAP_DISCONNECTION_REQ" command to the peer.
ble_sts_t blc_l2cap_disconnectCocChannel(u16 connHandle, u16 srcCID);
(4) Disconnection Response
After the peer requests to disconnect, the local side receives the command and replies with the "L2CAP_DISCONNECTION_RSP" command. For details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part A, Section 4.7 L2CAP_DISCONNECTION_RSP (CODE 0x07).
In this "SDK", if the "L2CAP_DISCONNECTION_REQ" command sent by the peer is received, the "stack" will automatically process the reply "L2CAP_DISCONNECTION_RSP", and the user does not need to perform other operations.
(5) Credit Based Connection Request and Response
In the "BLE protocol stack", the "Slave" requests the "Master" to establish a "credit-based CoC connection" through the "L2CAP_CREDIT_BASED_CONNECTION_REQ" command of the "L2CAP" layer. For details, please refer to "Bluetooth Core Specification v5.3" [Vol 3] Part A, Section 4.25 L2CAP_CREDIT_BASED_CONNECTION_REQ (CODE 0x17).
The "SDK" provides an "API" on the "L2CAP" layer for the "Slave" to actively request the establishment of a "CoC" channel, used to send the aforementioned "L2CAP_CREDIT_BASED_CONNECTION_REQ" command to the "Master".
ble_sts_t blc_l2cap_createCreditBasedConnect(u16 connHandle, u8 srcCnt);
The parameter "srcCnt" indicates the number of "CoC" channels to be established at a single time, with a maximum of 5. However, in this "SDK", establishing multiple "CoC" connections at a single time is not supported.
If the connection is requested by the "Master", after receiving the "L2CAP_CREDIT_BASED_CONNECTION_REQ" command sent by the peer, the "stack" will automatically process the reply "L2CAP_CREDIT_BASED_CONNECTION_RSP".
It should be noted that the difference between a connection established based on "credit" and a connection established based on "LE credit" is that using "L2CAP_CREDIT_BASED_CONNECTION_REQ" allows establishing up to 5 connection links at a time. However, in this "SDK", since only establishing one connection at a time is supported, we recommend that customers use "L2CAP_LE_CREDIT_BASED_CONNECTION_REQ" to establish the connection link.
For other commands not introduced here, they are restricted to use within the "stack" in this "SDK", and users do not need to pay attention to them.
CoC Data Transmission
The "Bluetooth" official specification defines a "PDU" with protocol elements for use in "LE credit" based flow control mode. This "L2CAP PDU" used in "CoC" channels is called a "K-frame". The basic format of a "K-frame" is as follows:

The sizes of "MTU" and "MPS" are specified when establishing the connection. When transmitting data, the length of the "Information Payload" cannot be greater than the value of "MPS", and the data length of "L2CAP SDU Length" cannot be greater than the value of "MTU".
This "SDK" provides an "API" on the "L2CAP" layer for locally sending "CoC" data:
ble_sts_t blc_l2cap_sendCocData(u16 connHandle, u16 srcCID, u8* data, u16 dataLen);
When actually using it, the user only needs to ensure that "dataLen" is less than "MTU", and the underlying layer will automatically perform packet fragmentation.
CoC Event
The "CoC event" belongs to the "host event", so the methods introduced in section 3.3.5.2 "GAP Event" can be used to register callbacks for "CoC" events and enable the "eventMask".
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_L2CAP_COC_CONNECT | \
GAP_EVT_MASK_L2CAP_COC_DISCONNECT | \
GAP_EVT_MASK_L2CAP_COC_RECONFIGURE | \
GAP_EVT_MASK_L2CAP_COC_RECV_DATA | \
GAP_EVT_MASK_L2CAP_COC_SEND_DATA_FINISH | \
GAP_EVT_MASK_L2CAP_COC_CREATE_CONNECT_FINISH
);
The corresponding "event mask" can be found by users in ble/gap/gap_event.h.
(1) GAP_EVT_MASK_L2CAP_COC_CONNECT
Event trigger condition: Successful establishment of a "CoC" connection.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct{
u16 connHandle;
u16 spsm;
u16 mtu;
u16 srcCid;
u16 dstCid;
} gap_l2cap_cocConnectEvt_t;
"connHandle" indicates the current connection handle. "spsm", "mtu", "srcCid", and "dstCid" are the parameters of the "CoC" connection established this time.
(2) GAP_EVT_MASK_L2CAP_COC_DISCONNECT
Event trigger condition: "CoC" connection is disconnected.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct {
u16 connHandle;
u16 srcCid;
u16 dstCid;
} gap_l2cap_cocDisconnectEvt_t;
"connHandle" indicates the current connection handle. "srcCid" and "dstCid" are the "CIDs" of the "CoC" connection disconnected this time.
(3) GAP_EVT_MASK_L2CAP_COC_RECONFIGURE
Event trigger condition: "CoC" channel parameters are updated.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct {
u16 connHandle;
u16 srcCid;
u16 mtu;
} gap_l2cap_cocReconfigureEvt_t;
"connHandle" indicates the current connection handle. "srcCid" and "mtu" represent the "CoC" channel corresponding to the "srcCid" and the new "MTU" size, respectively.
(4) GAP_EVT_MASK_L2CAP_COC_RECV_DATA
Event trigger condition: Data is received on the "CoC" channel.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct{
u16 connHandle;
u16 dstCid;
u16 length;
u8* data;
} gap_l2cap_cocRecvDataEvt_t;
"connHandle" indicates the current connection handle. "dstCid" is used to distinguish which "CoC" channel the data comes from, and "length" and "data" represent the data length and data pointer, respectively.
(5) GAP_EVT_MASK_L2CAP_COC_SEND_DATA_FINISH
Event trigger condition: The user successfully sends data by calling the "API" blc_l2cap_sendCocData.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct{
u16 connHandle;
u16 srcCid;
} gap_l2cap_cocSendDataFinishEvt_t;
"connHandle" indicates the current connection handle. "srcCid" is used to distinguish which "CoC" channel the data is sent to.
(6) GAP_EVT_MASK_L2CAP_COC_CREATE_CONNECT_FINISH
Event trigger condition: The event is triggered when the user calls the "API" blc_l2cap_createLeCreditBasedConnect or blc_l2cap_createCreditBasedConnect to establish a "CoC" connection and the transmission is successful.
Pointer p: Points to a memory data area, corresponding to the following structure:
typedef struct{
u16 connHandle;
u8 code;
u16 result;
} gap_l2cap_cocCreateConnectFinishEvt_t;
"connHandle" indicates the current connection handle.
"code":
a) 0xFF indicates that the "ACL" connection was disconnected abnormally, terminating the process.
b) L2CAP_COMMAND_REJECT_RSP
c) L2CAP_LE_CREDIT_BASED_CONNECTION_RSP
d) L2CAP_CREDIT_BASED_CONNECTION_RSP
"result": 0x00 indicates the "CoC" channel was successfully established; otherwise, an error code is returned.
Low Power Management
Low Power Management is also called Power Management, or PM as referred by this document.
Low Power Driver
Low Power Mode
"Low Power Mode" is also known as "sleep mode".
B85/B87 Low Power Mode
When the "B85/B87 MCU" is executing programs normally, it is in "working mode", with a working current between 3~7mA. If power saving is required, it is necessary to enter "low power mode".
"B85/B87" includes 3 types of "sleep mode": "suspend mode", "deepsleep mode", and "deepsleep retention mode". Users should note that the "A0" version chips do not support "suspend mode".
| Module | suspend | deepsleep retention | deepsleep |
|---|---|---|---|
| SRAM | 100% keep | first 32K(or 64K) keep, others lost | 100% lost |
| digital register | 99% keep | 100% lost | 100% lost |
| analog register | 100% keep | 99% lost | 99% lost |
TC321x Low Power Mode
"TC321x" includes 4 types of "sleep mode": "suspend mode", "deepsleep mode", "deepsleep retention mode", and "shutdown mode".
| Module | suspend | deepsleep retention | deepsleep | shutdown |
|---|---|---|---|---|
| SRAM | 100% keep | first 32K(or 64K) keep, others lost | 100% lost | 100% lost |
| digital register | 99% keep | 100% lost | 100% lost | 100% lost |
| analog register | 100% keep | 99% lost | 99% lost | 99% lost |
TC321X Specific Note:
- SHUTDOWN_MODE: A newly added "low power mode" in "TC321X", offering the lowest power consumption.
The table above provides a statistical summary of the status retention for "SRAM", "digital registers", and "analog registers" under the "sleep modes".
(1) Suspend mode (sleep mode 1)
In this mode, program execution pauses, most hardware modules of MCU are powered off, and the PM module still works normally. In this mode, IC current is about 60~70uA. Program execution continues after wakeup from suspend mode.
In suspend mode, data of the SRAM and all analog registers are maintained. In order to reduce power consumption, the SDK has set the power-down mode for some modules when entering the suspend low-power processing, at which time the digital register of the module will also be powered down, and should be re-initialized and configured after waking up. Involving:
a) A small number of digital registers in the baseband circuit. User should pay close attention to the registers configured by the API rf_set_power_level_index. This API needs to be invoked after each wakeup from suspend mode. In addition, it is not recommended for users to call cpu_sleep_wakeup to enter sleep mode in a non-idle state. This will cause the configuration of the "RF" part to be lost, resulting in abnormal operation. In this case, rf_drv_ble_init and rf_set_power_level_index should be called to reconfigure the "RF".
b) The digital register that controls the state of the Dfifo. Corresponding to the related APIs in drivers/8258(8278)/dfifo.h. When using these APIs, the user should ensure that they are reset after each suspend wake_up.
(2) Deepsleep mode (sleep mode 2)
In this mode, program execution pauses, vast majority of hardware modules are powered off, and the PM module still works. In this mode, IC current is less than 1uA, but if flash standby current comes up at 1uA or so, total current may reach 1~2uA. After wakeup from deepsleep mode, similar to power on reset, MCU will restart, and program will reboot and re-initialize.
In deepsleep mode, except a few retention analog registers, data of all registers (analog & digital) and SRAM are lost.
(3) Deepsleep retention mode (sleep mode 3)
In deepsleep mode, the current is very low, but cannot store SRAM data; while in suspend mode, though SRAM and most registers are non-volatile, but the current is high.
The deepsleep with SRAM retention (deepsleep retention or deep retention) mode is designed in the B85m family, so as to achieve application scenes with low sleep current and quick wakeup to restore state, e.g. maintain BLE connection during long sleep. Corresponding to 16K or 32K SRAM retention area, deepsleep retention 16K SRAM and deepsleep retention 32K SRAM are introduced.
The deepsleep retention mode is also a kind of deepsleep. Most of the hardware modules of the MCU are powered off, and the PM hardware modules remain working. Power consumption is the power consumed by retention SRAM plus that of deepsleep mode, and the current is between 2~3uA. When deepsleep mode wake up, the MCU will restart and the program will restart to initialize.
The deepsleep retention mode and deepsleep mode are consistent in register state, almost all of them are powered off. Compare with in deepsleep mode, in deepsleep retention mode, the first 32K (or the first 64K) of SRAM can be kept without power-off, and the remaining SRAM is powered off.
In deepsleep mode and deepsleep retention mode, there are very few analog registers that can be kept without power-down. These non-power-down analog registers include:
a) Analog registers to control GPIO pull-up/down resistance
When configured via the API “GPIO_setup_up_down_resistor” or the following method in the app_config.h, GPIO pull-up/down resistance are non-volatile:
#define PULL_WAKEUP_SRC_PD5 PM_PIN_PULLDOWN_100K
Please refer to the introduction of the GPIO module. Using GPIO output belongs to the state controlled by the digital register. B85m can use GPIO output to control some peripherals during suspend, but after being switched to deepsleep retention mode, the GPIO output status becomes invalid and it cannot accurately control peripherals during sleep. At this point, you can use GPIO to simulate the state of the pull-up and pull-down resistors instead: pull-up 10K ohm instead of GPIO output high, and pull-down 100K ohm instead of GPIO output low.
b) Special retention analog registers of the PM module:
The DEEP_ANA_REG in the pm.h file, as shown in the code below:
#define DEEP_ANA_REG0 0x3a
#define DEEP_ANA_REG1 0x3b
#define DEEP_ANA_REG2 0x3c
It should be noted that Bit1 of ana_3a in "B85" and "B87" is already utilized by the driver, and customers are not permitted to use it. Additionally, ana_3c in "B85" and ana_3b in "B87" are reserved for the underlying layer; if the application layer code makes use of these registers, modifications are required. Due to the limited number of retention analog registers, it is recommended that customers utilize each individual bit to indicate different status flag information. For specific details, please refer to ble_remote in the vendor directory of the "SDK".
The following groups of non-drop analog registers may lose information due to wrong GPIO wakeup. For example, GPIO_PAD wakes up deepsleep at high level, but GPIO is already at high level before calling cpu_sleep_wakeup function. It will cause wrong GPIO wakeup, then these analog register values will be lost.
#define DEEP_ANA_REG6 0x35
#define DEEP_ANA_REG7 0x36
#define DEEP_ANA_REG8 0x37
#define DEEP_ANA_REG9 0x38
#define DEEP_ANA_REG10 0x39
The user can save some important information in these analog register and read the previously stored values after deepsleep/deepsleep retention wake_up.
Low Power Wake-up Source
The low-power wake-up source diagram of B85m MCU is shown below, suspend/deepsleep/deepsleep retention can all be awakened by GPIO PAD and timer. In the BLE SDK, only two types of wake-up sources are concerned, as shown below (note that the two definitions of PM_TIM_RECOVER_START and PM_TIM_RECOVER_END in the code are not wake-up sources):
typedef enum {
PM_WAKEUP_PAD = BIT(4),
PM_WAKEUP_TIMER = BIT(6),
}SleepWakeupSrc_TypeDef;

As shown above, there are two hardware wakeup sources: TIMER and GPIO PAD.
-
The “PM_WAKEUP_TIMER” comes from 32k HW timer (32k RC timer or 32k Crystal timer). Since 32k timer is correctly initialized in the SDK, no configuration is needed except setting wakeup source in the “cpu_sleep_wakeup ()”.
-
The “PM_WAKEUP_PAD” comes from GPIO module. Except 4 MSPI pins, all GPIOs (PAx/PBx/PCx/PDx) support high or low level wakeup .
The API below serves to configure GPIO PAD as wakeup source for sleep mode.
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 pin
- “pol”: wakeup polarity, Level_High: high level wakeup, Level_Low: low level wakeup
- “en”: 1 indicates enable, 0 indicates disable.
Examples:
cpu_set_GPIO_wakeup (GPIO_PC2, Level_High, 1); //Enable GPIO_PC2 PAD high level wakeup
cpu_set_GPIO_wakeup (GPIO_PC2, Level_High, 0); //Disable GPIO_PC2 PAD wakeup
cpu_set_GPIO_wakeup (GPIO_PB5, Level_Low, 1); //Enable GPIO_PB5 PAD low level wakeup
cpu_set_GPIO_wakeup (GPIO_PB5, Level_Low, 0); //Disable GPIO_PB5 PAD wakeup
Sleep and Wake-up from Low Power Mode
The API below serves to configure MCU sleep and wakeup.
int cpu_sleep_wakeup (SleepMode_TypeDef sleep_mode, SleepWakeupSrc_TypeDef wakeup_src,
unsigned int wakeup_tick);
- sleep_mode: This parameter serves to set sleep mode as suspend mode, deepsleep mode, deepsleep retention 16K SRAM or deepsleep retention 32K SRAM.
typedef enum {
SUSPEND_MODE = 0,
DEEPSLEEP_MODE = 0x80,
DEEPSLEEP_MODE_RET_SRAM_LOW16K = 0x43,
DEEPSLEEP_MODE_RET_SRAM_LOW32K = 0x07,
}SleepMode_TypeDef;
-
wakeup_src: This parameter serves to set wakeup source for suspend/deep retention/deepsleep as one or combination of PM_WAKEUP_PAD and PM_WAKEUP_TIMER. If set as 0, MCU wakeup is disabled for sleep mode.
-
“wakeup_tick”: if PM_WAKEUP_TIMER is assigned as wakeup source, the “wakeup_tick” serves to set MCU wakeup time. If PM_WAKEUP_TIMER is not assigned, this parameter is negligible.
The “wakeup_tick” is an absolute value, which equals current value of System Timer tick plus intended sleep duration. When System Timer tick reaches the time defined by the wakeup_tick, MCU wakes up from sleep mode. The value of wakeup_tick needs to be based on the current System Timer tick value, plus an absolute time converted from the time to be slept, in order to effectively control the sleep time. If the wakeup_tick is set directly without taking into account the current System Timer tick, the wake-up time point cannot be controlled.
Since the wakeup_tick is an absolute time, it follows the max range limit of 32bit System Timer tick. In current SDK, 32bit max sleep time corresponds to 7/8 of max System Timer tick. Since max System Timer tick is 268s or so, max sleep time is 268*7/8=234s, which means the “delta_Tick” below should not exceed 234s. If a longer sleep time is needed, user can call the long sleep function, as described in section 4.2.7.
cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER, clock_time() + delta_tick);
The return value is an ensemble of current wakeup sources. Following shows wakeup source for each bit of the return value.
enum {
WAKEUP_STATUS_TIMER = BIT(1),
WAKEUP_STATUS_PAD = BIT(3),
STATUS_GPIO_ERR_NO_ENTER_PM = BIT(8),
};
a) If WAKEUP_STATUS_TIMER bit = 1, wakeup source is Timer.
b) If WAKEUP_STATUS_PAD bit = 1, wakeup source is GPIO PAD.
c) If both WAKEUP_STATUS_TIMER and WAKEUP_STATUS_PAD equal 1, wakeup source is Timer and GPIO PAD.
d) STATUS_GPIO_ERR_NO_ENTER_PM is a relatively special status, indicating that a "GPIO" wake-up error has occurred: for instance, cpu_sleep_wakeup sets PM_WAKEUP_PAD as the wake-up source and configures a specific "GPIO PAD" to wake up on a high level, but the "GPIO PAD" is already at a high level when cpu_sleep_wakeup is called to enter "suspend". At this time, it is impossible to enter "suspend", and the "MCU" immediately exits the cpu_sleep_wakeup function, returning STATUS_GPIO_ERR_NO_ENTER_PM.
Sleep time is typically set in the following way:
cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_TIMER, clock_time() + delta_Tick);
The “delta_Tick”, a relative time (e.g. 100* CLOCK_16M_SYS_TIMER_CLK_1MS), plus “clock_time()” becomes an absolute time.
Some examples on cpu_sleep_wakeup:
cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_PAD, 0);
When it’s invoked, MCU enters suspend, and wakeup source is GPIO PAD.
cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_TIMER, clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1MS;
When it’s invoked, MCU enters suspend, wakeup source is timer, and wakeup time is current time plus 10ms, so the suspend duration is 10ms.
cpu_sleep_wakeup (SUSPEND_MODE , PM_WAKEUP_PAD | PM_WAKEUP_TIMER,
clock_time() + 50* CLOCK_16M_SYS_TIMER_CLK_1MS);
When it’s invoked, MCU enters suspend, wakeup source includes timer and GPIO PAD, and timer wakeup time is current time plus 50ms.
If GPIO wakeup is triggered before 50ms expires, MCU will be woke up by GPIO PAD in advance; otherwise, MCU will be woke up by timer.
cpu_sleep_wakeup (DEEPSLEEP_MODE, PM_WAKEUP_PAD, 0);
When it’s invoked, MCU enters deepsleep, and wakeup source is GPIO PAD.
cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K , PM_WAKEUP_TIMER, clock_time() + 8* CLOCK_16M_SYS_TIMER_CLK_1S);
When it’s invoked, MCU enters deepsleep retention 32K SRAM mode, wakeup source is timer, and wakeup time is current time plus 8s.
cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K , PM_WAKEUP_PAD | PM_WAKEUP_TIMER,clock_time() + 10* CLOCK_16M_SYS_TIMER_CLK_1S);
When it’s invoked, MCU enters deepsleep retention 32K SRAM mode, wakeup source includes GPIO PAD and Timer, and timer wakeup time is current time plus 10s. If GPIO wakeup is triggered before 10s expires, MCU will be woke up by GPIO PAD in advance; otherwise, MCU will be woke up by timer.
Low Power Wake-up Procedure
When user calls the API cpu_sleep_wakeup, the MCU enters the sleep mode; when the wake-up source triggers the MCU to wake up, the MCU software operation flow is inconsistent for different sleep modes.
The following is a detailed description of the MCU operating process after the suspend, deepsleep, and deepsleep retention three sleep modes are awakened. Please refer to the figure below.

Detailed process after the MCU is powered on is introduced as following:
(1) Run hardware bootloader
It is pure MCU hardware operation without involvement of software.
Couple of examples:
Read the boot flag of flash to determine whether the firmware that should be run currently is stored on flash address 0 or on flash address 0x20000 (related to OTA); read the value of the corresponding location of flash to determine how much data currently needs to be copied from flash to SRAM as resident memory data (refer to Chapter (refer to the introduction of SRAM allocation in Chapter 2). The part of running the hardware bootloader involves copying data from flash to sram, which generally takes a long time to execute. For example, it takes about 5ms to copy 10K data.
(2) Run software bootloader
After the "hardware bootloader" finishes execution, the "MCU" starts running the "software bootloader". The "software bootloader" is the "vector" section introduced earlier (the .S assembly program in the boot directory of the "B85m SDK").
Software bootloader serves to set up memory environment for C program execution, so it can be regarded as memory initialization.
(3) System initialization
System initialization corresponds to the initialization of each hardware module (including cpu_wakeup_init, rf_drv_ble_init, GPIO_init, clock_init) from cpu_wakeup_init to user_init in the main function, and sets the digital/analog register status of each hardware module.
(4) User initialization
User initialization is divided into User initialization normal and User initialization deep retention, which correspond to the functions user_init_normal and user_init_deepRetn in the SDK respectively. For projects that do not use deep retention, such as kma master dongle, there is only user_init, which is equivalent to user_init_normal.
The user_init and user_init_normal require all the initialization to be done, which takes longer time.
The user_init_deepRetn only needs to do some hardware related initialization, no software initialization is needed because the variables involved in software initialization are put into the MCU retention area during design time, and these variables remain unchanged during deep retention and do not need to be reset after waking up. So user_init_deepRetn is an accelerated mode, saving time and therefore power.
(5) main_loop
After User initialization, program enters main_loop inside while(1). The operation is called “UI Task Set A” before main_loop enters sleep mode, and called “UI Task Set B” after wakeup from sleep.
As shown in figure above, sleep mode process is detailed as following:
(1) no sleep
Without sleep mode, MCU keeps looping inside while(1) between “UI Task Set A” -> “UI Task Set B”.
(2) suspend
MCU enters suspend mode by invoking cpu_sleep_wakeup, wakes up from suspend after exiting from cpu_sleep_wakeup, and then executes “UI Task Set B”.
Suspend can be regarded as the most “clean” sleep mode, in which data of SRAM, digital and analog registers are retained (a few special exceptions). After wakeup from suspend, program continues from the breakpoint, with almost no need to recover SRAM or registers. However, the disadvantage of "suspend" is its relatively high power consumption.
(3) deepsleep
MCU can also enter deepsleep by invoking cpu_sleep_wakeup. After wakeup from deepsleep, MCU restarts from “Running hardware bootloader”. Almost the same as power on reset, all hardware and software initialization are required after deepsleep wakeup. Since SRAM and registers - except a few retention analog registers - will lose their data in deepsleep, MCU current is decreased to less than 1uA.
(4) deepsleep retention
MCU can also enter deepsleep retention mode by invoking cpu_sleep_wakeup. After wakeup from deepsleep retention, MCU restarts from “Running software bootloader”.
The deepsleep retention is an intermediate sleep mode between suspend and deepsleep. In suspend mode, both SRAM and most registers need to retain data, which thus ends up with higher current. In deepsleep retention, it’s only needed to maintain states of a few retention analog registers, as well as data of first 16K or 32K SRAM, so current is largely decreased to 2uA or so.
After deepsleep wake_up, MCU needs to restart the whole flow. Since first 16K or 32K SRAM are non-volatile in deepsleep retention, there’s no need to re-load from flash to SRAM after wake_up, and thus “Running hardware bootloader” is skipped. Due to limited SRAM retention size, “Running software bootloader” cannot be skipped. Since deepsleep retention does not keep register state, system initialization should also be executed to re-initialize registers.
The user initialization after deepsleep retention wake_up can actually be optimized to differentiate from MCU power on and deepsleep wake_up.
API pm_is_MCU_deepRetentionWakeup
According to the figure “sleep mode wakeup work flow” above, MCU power on, deepsleep wake_up and deepsleep retention wake_up all need to go through “Running software bootloader”, “system initialization”, and “user initialization”.
While running system initialization and user initialization, user needs to know whether MCU is woke up from deepsleep retention, so as to differentiate from power on and deepsleep wake_up. The following API in the PM driver serves to make this judgement.
int pm_is_MCU_deepRetentionWakeup(void);
Return value: 1 indicates deepsleep retention wake_up; 0 indicates power on or deepsleep wake_up.
BLE Low Power Management
BLE PM Initialization
For applications with low power mode, BLE PM module needs to be initialized by following API.
void blc_ll_initPowerManagement_module(void);
If low power is not required, DO NOT use this API, so as to skip compiling of PM related code and variables into program and thus save firmware and SRAM space.
BLE PM for Link Layer
In this BLE SDK, PM module manages power consumption in BLE Link Layer. It would be helpful referring to introduction to Link Layer in earlier chapter.
Current SDK only applies low power management to Advertising state and Connection state Slave role with a set of APIs for user. It’s not applicable yet to Scanning state, Initiating state and Connection state Master role.
The SDK does not apply low power management to Idle state either. In Idle state, since there is no RF activity, i.e. the “blt_sdk_main_loop” function is not valid, user can use PM driver for certain low power management. E.g. In the demo code below, when Link Layer is in Idle state, every main_loop would suspend for 10ms.
void main_loop (void)
{
////////////////////// BLE entry ////////////////////////
blt_sdk_main_loop();
///////////////////// UI entry //////////////////////////
// add user task
//////////////////// PM configuration ////////////////////////
if(blc_ll_getCurrentState() == BLS_LINK_STATE_IDLE ){ //Idle state
cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_TIMER,
clock_time() + 10*CLOCK_16M_SYS_TIMER_CLK_1MS);
}
else{
blt_pm_proc(); //BLE ADV & Conn state
}
}
The figure below shows timing of sleep mode when Link Layer is in Advertising state or ConnSlaveRole with connection latency = 0.

(1) In Advertising state, during each ADV Internal, ADV Event is mandatory; MCU can enter sleep mode (suspend/deepsleep retention) during the rest time other than UI task.
In figure above, the starting time of ADV event at first ADV interval is defined as T_advertising, and the time for MCU to wake up from sleep is defined as T_wakeup.T_wakeup is also the start of ADV event at next ADV interval. Both these two parameters will be elaborated in later section.
(2) When in ConnSlaveRole, within each Conn interval, the brx Event (brx start + brx working + brx post) time is mandatory. Excluding the time occupied by UI task, the MCU can enter sleep mode (suspend/deepsleep retention) during the remaining time.
The starting time of of Brx event at first Connection interval is defined as T_brx, and the time for MCU to wake up from sleep is T_wakeup. T_wakeup is also the start of BRx event at next Connection interval. Both these two parameters will be elaborated in later section.
BLE PM is basically the sleep mode management in Advertising state or ConnSlaveRole. User can select sleep mode and set related time parameters: enter sleep, enter suspend mode, or enter deepsleep retention mode.
The "sleep mode" of "B85/B87" is divided into 3 types: "suspend", "deepsleep retention", and "deepsleep". The "sleep mode" of "TC321X" is divided into 4 types: "suspend", "deepsleep retention", "deep sleep", and "shut down".
For suspend and deepsleep retention, since the blt_sdk_main_loop of the SDK includes low PM in BLE stack according to Link Layer state, to configure low power management, user only needs to invoke corresponding APIs instead of the “cpu_sleep_wakeup”.
For "deepsleep"/"shut down", "BLE" low power management does not include handling for them, so the user needs to manually call the "API" "cpu_sleep_wakeup" at the application layer to enter "deepsleep"/"shut down". For the use of manual "deepsleep mode", please refer to the handling of "deepsleep" in the "blt_pm_proc" function of the "SDK" project "ble_remote".
Following sections illustrate details of low power management in Advertising state and Connection state Slave role.
BLE PM Variables
The variables in this section are helpful to understand BLE PM software flow.
The struct “st_ll_pm_t” is defined in BLE SDK. Following lists some variables of the struct which will be used by PM APIs.
typedef struct {
u8 suspend_mask;
u8 wakeup_src;
u16 sys_latency;
u16 user_latency;
u32 deepRet_advThresTick;
u32 deepRet_connThresTick;
u32 deepRet_earlyWakeupTick;
}st_ll_pm_t;
Following struct is defined in the file “ll_pm.c” for understanding purpose.
st_ll_pm_t bltPm;
Please note that this file is assembled in library, and user is not allowed to make any operation on this struct variable.
There will be a lot of variables like the “bltPm. suspend_mask” in later sections.
API bls_pm_setSuspendMask
The APIs below serve to configure low power management in Link Layer at “Advertising state” and “ConnSlaveRole”.
void bls_pm_setSuspendMask (u8 mask);
u8 bls_pm_getSuspendMask (void);
The “bltPm.suspend_mask” is set by the “bls_pm_setSuspendMask” and its default value is SUSPEND_DISABLE.
Following shows source code of the 2 APIs.
void bls_pm_setSuspendMask (u8 mask)
{
bltPm.suspend_mask = mask;
}
u8 bls_pm_getSuspendMask (void)
{
return bltPm.suspend_mask;
}
The “bltPm.suspend_mask” can be set as any one or the “or-operation” of following values:
#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)
The “SUSPEND_DISABLE” means sleep is disabled which allows MCU to enter neither suspend nor deepsleep retention.
The “SUSPEND_ADV” and “DEEPSLEEP_RETENTION_ADV” decide whether MCU at Advertising state can enter suspend and deepsleep retention.
The “SUSPEND_CONN” and “DEEPSLEEP_RETENTION_CONN” decide whether MCU at ConnSlaveRole can enter suspend and deepsleep retention.
In low power sleep mode design of the SDK, deepsleep retention is a substitute of suspend mode to reduce sleep power consumption.
Take Conn state slave role as an example:
The SDK will first check whether SUSPEND_CONN is enabled in the “bltPm.suspend_mask”, and MCU can enter suspend only when SUSPEND_CONN is enabled. Further on, based on the value of the DEEPSLEEP_RETENTION_CONN, MCU can decide whether it will enter suspend mode or deepsleep retention mode.
Therefore, to enable MCU to enter suspend, user only needs to enable SUSPEND_ADV/SUSPEND_CONN. To enable MCU to enter deepsleep retention mode, both SUSPEND_CONN and DEEPSLEEP_RETENTION_CONN should be enabled.
Following shows 3 typical use cases:
bls_pm_setSuspendMask(SUSPEND_DISABLE);
MCU will not enter sleep mode (suspend/deepsleep retention).
bls_pm_setSuspendMask(SUSPEND_ADV | SUSPEND_CONN);
At Advertising state and ConnSlaveRole, MCU can only enter suspend mode, and it’s not allowed to enter deepsleep retention.
bls_pm_setSuspendMask(SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV
|SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);
At Advertising state and ConnSlaveRole role, MCU can enter both suspend and deepsleep retention, but the sleep mode to enter depends on sleeping time which will be explained later.
There may be some special applications, for example:
bls_pm_setSuspendMask(SUSPEND_ADV)
Only at Advertising state can MCU enter suspend, and at ConnSlaveRole it’s not allowed to enter sleep mode.
bls_pm_setSuspendMask(SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN)
Only at ConnSlaveRole, can MCU enter suspend or deepsleep retention, and at Advertising state it’s not allowed to enter sleep mode.
API bls_pm_setWakeupSource
User can set the bls_pm_setSuspendMask to enable MCU to enter sleep mode (suspend or deepsleep retention), and use the following API to set wakeup source.
void bls_pm_setWakeupSource(u8 source);
“source”: Wakeup source, can be set as PM_WAKEUP_PAD.
This API sets the bottom-layer variable “bltPm.wakeup_src”. Following shows source code in the SDK.
void bls_pm_setWakeupSource (u8 src)
{
bltPm.wakeup_src = src;
}
When MCU enters sleep mode at Advertising state or ConnSlaveRole, its actual wakeup source is:
bltPm.wakeup_src | PM_WAKEUP_TIMER
So PM_WAKEUP_TIMER is mandatory, not depending on user setup. This guarantees that MCU will wake up at specified time to handle ADV Event or Brx Event.
Every time wakeup source is set by the “bls_pm_setWakeupSource”, after MCU wakes up from sleep mode, the bltPm.wakeup_src is set to 0.
Note
- It is not recommended for users to use the API "pm_get_wakeup_src" to obtain the wake-up source after waking up. This function is used internally by the protocol stack, and it only assigns a value to "pmParam.wakeup_src" when waking up from "deepsleep retention"/"deepsleep". If the customer needs to obtain the wake-up source, it can be obtained directly through the analog registers. For details, please refer to the chip "DataSheet".
API blc_pm_setDeepsleepRetentionType
Deepsleep retention further separates into 16K SRAM retention or 32K SRAM retention. When entering deepsleep retention mode, the following API can be set to decide which sub-mode to enter:
void blc_pm_setDeepsleepRetentionType(SleepMode_TypeDef sleep_type);
Only two options are available:
typedef enum {
DEEPSLEEP_MODE_RET_SRAM_LOW16K = 0x43,
DEEPSLEEP_MODE_RET_SRAM_LOW32K = 0x07,
}SleepMode_TypeDef;
"SDK" versions V3.4.2.4 and later will automatically set the "retention" size based on the "retention_size" calculated at compile time. This functionality is implemented through the API "blc_app_setDeepsleepRetentionSRAMSize". For a detailed introduction to this API, please refer to the section "Space Allocation for SDK V3.4.2.4 and Later Versions".
In "SDK" versions prior to V3.4.2.4, the default "deepsleep retention mode" is "DEEPSLEEP_MODE_RET_SRAM_LOW16K". If the "user" needs "retention 32K SRAM", the following "code" can be called during initialization.
blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K);
Note
- The call to this "API" should be made after "blc_ll_initPowerManagement_module" to be effective.
Referring to previous chapter of this document, it is evident that the "SRAM" memory allocation in "SDK" versions prior to V3.4.2.4 is designed by default for "deepsleep retention 16K SRAM". According to the description in Chapter 1, selecting different "ICs" and "deep retention size" values requires selecting different "software bootloader" startup files and "boot.link". For specific mapping relationships and configuration methods, please refer to the "software bootloader introduction" subsection.
PM software processing flow
Both actual code and pseudo-code are used herein to explain the flow details.
blt_sdk_main_loop
As shown below, the “blt_sdk_main_loop” is repetitively executed in while (1) loop of the SDK.
while(1)
{
////////////////////// BLE entry ////////////////////////
blt_sdk_main_loop();
////////////////////// UI entry ////////////////////////
//UI task
////////////////////// user PM config ////////////////////////
//blt_pm_proc();
}
The blt_sdk_main_loop function is executed continuously in while(1), and the code for BLE low-power management is in the blt_sdk_main_loop function, so the code for low-power management is also executed all the time.
Following shows the implementation of BLE PM logic inside the “blt_sdk_main_loop”.
int blt_sdk_main_loop (void)
{
……
if(bltPm. suspend_mask == SUSPEND_DISABLE) // SUSPEND_DISABLE, can not
{ // enter sleep mode
return 0;
}
if( (Link Layer State == Advertising state) || (Link Layer State == ConnSlaveRole) )
{
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) When the “bltPm.suspend_mask” is SUSPEND_DISABLE, the SW directly exits without executing the “blt_brx_sleep” function. So when using the “bls_pm_setSuspendMask(SUSPEND_DISABLE)”, PM logic is completely ineffective; MCU will never enter sleep and the SW always execute while(1) loop.
(2) When the SW is executing ADV Event at Advertising State or Brx Event at ConnSlaveRole, the “blt_brx_sleep” won’t be executed either due to RF task ongoing. The SDK needs to guarantee completion of ADV Event/Brx Event before MCU enters sleep mode.
Only when both cases above are invalid, the blt_brx_sleep will be executed.
blt_brx_sleep
Following shows logic implementation of the “blt_brx_sleep” function in the case of default deepsleep retention 16K SRAM.
void blt_brx_sleep (void)
{
if( (Link Layer state == ADV state)&& (bltPm. suspend_mask &SUSPEND_ADV) )
{ // Current advertising state, suspend is allowed
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);
// After MCU wakeup, PC value resets to 0, will re-execute software bootloader, system initialization, etc.
}
else
{
cpu_sleep_wakeup (SUSPEND_MODE, PM_WAKEUP_TIMER | bltPm.wakeup_src, T_wakeup);
}
“BLT_EV_FLAG_SUSPEND_EXIT ” event callback execution
if(suspend is woken up by GPIO PAD)
{
“BLT_EV_FLAG_GPIO_EARLY_WAKEUP” event callback execution
}
}
else if((Link Layer state == ConnSlaveRole)&& (SuspendMask&SUSPEND_CONN) )
{ // Current Conn state, enter 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);
// After MCU wakeup, PC value resets to 0, will re-execute software bootloader, system initialization, etc.
}
else
{
cpu_sleep_wakeup (SUSPEND_MODE, PM_WAKEUP_TIMER | bltPm.wakeup_src, T_wakeup);
}
“BLT_EV_FLAG_SUSPEND_EXIT” event callback execution
if(suspend is woken up by GPIO PAD)
{
”BLT_EV_FLAG_GPIO_EARLY_WAKEUP” event callback execution
Adjust BLE timing related processing
}
}
bltPm.wakeup_src = 0;
bltPm.user_latency = 0xFFFF;
}
To simplify the discussion, let’s begin with an easy case: conn_latency =0, only suspend mode, no deepsleep retention. This is the case when setting suspend mask in APP layer via the “bls_pm_setSuspendMask(SUSPEND_ADV | SUSPEND_CONN)”.
Referring to controller event introduced earlier, please pay close attention to the timing of these suspend related events and callback functions: BLT_EV_FLAG_SUSPEND_ENTER, BLT_EV_FLAG_SUSPEND_EXIT, BLT_EV_FLAG_GPIO_EARLY_WAKEUP.
When Link Layer is in Advertising state with “bltPm. suspend_maskis” set to SUSPEND_ADV, or at Conn state slave role with “bltPm. suspend_mask” set to SUSPEND_CONN, MCU can enter suspend mode.
In suspend mode, the API “cpu_sleep_wakeup” in the driver is finally invoked.
cpu_sleep_wakeup (SUSPEND_MODE, PM_WAKEUP_TIMER | bltPm.wakeup_src, T_wakeup);
This API sets wakeup source as PM_WAKEUP_TIMER | bltPm.wakeup_src, so Timer wakeup is mandatory to guarantee MCU wakeup before next ADV Event or Brx Event. For wakeup time “T_wakeup”, please refer to earlier “sleep timing for Advertising state & ConnSlaveRole” diagram.
When exiting the “blt_brx_sleep” function, both the “bltPm.wakeup_src” and the “bltPm.user_latency” are reset. So the API “bls_pm_setWakeupSource” and “bls_pm_setManualLatency” are only effective for current sleep mode.
Analysis of deepsleep retention
Introduce deepsleep retention, and continue to analyze the above software processing flow. When the application layer is set as follows, deepsleep retention mode is enabled.
bls_pm_setSuspendMask( SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);
API blc_pm_setDeepsleepRetentionThreshold
At Advertising state and Conn state slave role, suspend can switch to deep retention only when following conditions are met, respectively:
if( bltPm. suspend_mask & DEEPSLEEP_RETENTION_ADV &&T_sleep > bltPm.deepRet_advThresTick )
if( bltPm. suspend_mask & DEEPSLEEP_RETENTION_CONN &&T_sleep > bltPm.deepRet_connThresTick )
Firstly, the “bltPm. suspend_mask” should be set to DEEPSLEEP_RETENTION_ADV or DEEPSLEEP_RETENTION _CONN, as explained before.
Secondly, for T_sleep > bltPm.deepRet_advThresTick or T_sleep > bltPm.deepRet_connThresTick ,T_sleep, sleep duration time, equals Wakeup time “T_wakeup” minus current time “clock_time()”. It means that sleep duration should exceed certain threshold so that MCU can switch sleep mode from suspend to deepsleep retention.
Here is the API to set the two threshold in unit of ms for Advertising state and Conn state slave role.
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 is used to set the time threshold when suspend is switched to deepsleep retention trigger condition. This design is to pursue lower power consumption.
Refer to the description of the "Run Process After Sleep Wake_up" section above. After suspend mode wake_up, you can immediately return to the environment before suspend to continue running. In the above software flow, after T_wakeup wakes up, it can immediately start executing the ADV Event/Brx Event task.
After deepsleep retention wake_up, you need to return to the place where "Run software bootloader" started. Compared with suspend wake_up, you need to run 3 more steps (Run software bootloader + System initialization + User initialization) before you can return to main_loop to execute ADV Event again. /Brx Event task.
Taking Conn state slave role as an example, the following figure shows the timing (sequence) & power (power consumption) comparison when sleep mode is suspend and deep sleep retention respectively.
The time difference T_cycle between two adjacent Brx events is the current time period. Average the power consumption of Brx Event, the equivalent current is I_brx, and the duration is t_brx (the name t_brx here is to distinguish it from the previous concept T_brx). The bottom current of Suspend is I_suspend, and the bottom current of deep retention is I_deepRet.
The average current in the process of "Run software bootloader + System initialization + User initialization" is equivalent to I_init, and the total duration is T_init. In actual applications, the value of T_init needs to be controlled and measured by the user, and how to implement it will be introduced later.

The following is the description of terms in the figure.
-
T_cycle: the time difference between two adjacent Brx events
-
I_brx: average the power consumption of Brx Event, the equivalent current is I_brx
-
t_brx: l_brx duration
-
l_suspend: suspend bottom current
-
l_deepRet: bottom current of deep retention
-
l_init: Software bootloader + System initialization + User initialization process equivalent average current
-
T_init: the total duration of l_init
Average Brx current with suspend mode is:
I_avgSuspend = I_brx*t_brx + I_suspend* (T_cycle – t_brx)
Simplified by T_cycle >> t_brx, (T_cycle – t_brx) can be regarded as T_cycle.
I_avgSuspend = I_brx*t_brx + I_suspend* T_cycle
Average Brx current with deepsleep retention mode is:
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
Comparing I_avgSuspend and I_avgDeepRet, removing the same "I_brx*t_brx", the final part of the comparison is
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)
For application program with correct power debugging on both HW circuit and SW, the “(I_suspend - I_ deepRet)” and “(T_init*I_init)” can be regarded as fixed value.
Suppose I_suspend=30uA, I_deepRet=2uA, (I_suspend - I_ deepRet) = 28uA; I_init=3mA, T_init=400 us, (T_init*I_init)=1200 uA*us:
I_avgSuspend – I_avgDeepRet = Tcycle (28 – 1200/Tcycle)
I_avgSuspend – I_avgDeepRet
>0 when Tcycle > (1200/28) = 43ms, DeepRet consumes less power;
<0 when Tcycle < 43ms, Suspend mode consumes less power.
Mathematically, when Tcycle < 43 ms, suspend mode is more power efficient; when Tcycle > 43 ms, deepsleep retention mode is a better choice.
Note
- As you can see in the PM software processing flow section, the suspend is automatically switched to deepsleep retention only when T_sleep > 43ms. We generally consider the MCU working time (Brx Event + UI task) to be relatively short, and when T_cycle is large, we can consider T_sleep to be approximately equal to T_cycle.
By using the threshold setting API below, MCU will automatically switch suspend to deepsleep retention for T_sleep more than 43mS, and maintain suspend for T_sleep less than 43mS.
blc_pm_setDeepsleepRetentionThreshold(43, 43);
Take a long connection of 10ms connection interval * (99 + 1) = 1s as an example:
During the Conn state slave role, due to the tasks of the application layer, manual latency settings, etc., it may lead to time values such as 10ms, 20ms, 50ms, 100ms, 1s, etc. when the MCU suspend. According to the 43ms threshold setting, the MCU will automatically switch the 50ms, 100ms, 1s etc. suspend to deepsleep retention, while the 10ms, 20ms etc. suspend will still maintain suspend, such processing can ensure an optimal power consumption.
Since the power consumption of deepsleep retention is lower than that of suspend, and the presence of the 3 steps "Run software bootloader + System initialization + User initialization" results in some additional power consumption. Based on the above analysis, it should be the case that deepsleep retention will be more power efficient when T_cycle is greater than a certain threshold. The values in the above example are just a simple demo, the user needs to measure the corresponding values in the above equation according to certain methods when implementing power optimisation, and only then can the threshold value be determined.
In practice, following demos in the SDK, as long as user initialization does not incorrectly run across extended time, for T_cycle larger than 100ms, deepsleep retention mode should end up with lower power in most application scenarios.
blc_pm_setDeepsleepRetentionEarlyWakeupTiming
According to the “suspend & deepsleep retention timing & power”, suspend wake_up time “T_wakeup” is exactly the starting point of next Brx Event, or the time point when BLE master starts sending packet.
For deepsleep retention, wake_up time needs to start earlier than T_wakeup to allow T_init: running software bootloader + system initialization + user initialization, or it will miss Brx Event, i.e., the time when BLE master starts sending packet. So MCU wake_up time should be pulled in to T_wakeup’:
T_wakeup’ = T_wakeup – T_init
When applying to:
cpu_sleep_wakeup (DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_TIMER | bltPm.wakeup_src,
T_wakeup - bltPm.deepRet_earlyWakeupTick);
The T_wakeup is automatically calculated by the BLE stack, while the “bltPm.deepRet_earlyWakeupTick” can be assigned to the measured T_init (or slightly larger) by following API:
void blc_pm_setDeepsleepRetentionEarlyWakeupTiming(u32 earlyWakeup_us)
{
bltPm.deepRet_earlyWakeupTick = earlyWakeup_us * CLOCK_16M_SYS_TIMER_CLK_1US;
}
User can directly set the measured value of T_init to the above API, or set a value slightly larger than T_init, but not less than this value.
Optimization and measurement of T_init
For SRAM concept to be discussed in this section such as ram_code, retention_data, deepsleep retention area, please refer to SRAM space partition.
(1) T_init timing
From the figure "suspend & deepsleep retention timing & power", combined with the previous analysis, we can see that for the larger T_cycle, the sleep mode uses deepsleep retention with lower power consumption, but in this mode the T_init time is mandatory. In order to minimize the power consumption of long sleep, the time of T_init needs to be optimized to the minimum. The value of I_init is basically stable and does not need to be optimized.
The T_init is the sum of the time consumed by the 3 steps of Run software bootloader + System initialization + User initialization. The 3 steps are disassembled and analyzed, and the time of each step is defined first.
-
T_cstatup is the time of running software bootloader, i.e. executing assembly file cstartup_xxx.S.
-
T_sysInit is system initialization time.
-
T_userInit is user initialization time.
T_init = T_cstatup + T_sysInit + T_userInit
Following is a complete timing diagram of T_init:

Based on earlier definition, T_wakeup is the starting point of next ADV Event/Brx Event, and T_wakeup’ is MCU early wake_uptime.
After wake_up, MCU will execute cstatup_xxx.S, jump to main() to start system initialization followed by user initialization, and then enter main_loop. Once getting in main_loop, it can start processing of ADV Event/Brx Event. The end of T_userInit is the starting point of ADV Event/Brx Event, or T_brx/T_advertising as shown in above diagram. “irq_enable” in the diagram is the separation between T_userInit and main_loop, matching the code in the SDK.
In the SDK, T_sysInit includes execution time of cpu_wakeup_init, rf_drv_init, GPIO_init and clock_init. These timing parameters have been optimized in the SDK by placing the associated code into the ram_code.
The T_cstatup and T_userInit in the SDK are elaborated herein.
(2) T_userInit
User initialization is executed at power on, deepsleep wake_up, and deepsleep retention wake_up.
For applications without deepsleep retention mode, user initialization does not need to differentiate between deepsleep retention wake_up and power on/ deepsleep wake_up. In the BLE SDK, all user initialization can be completed with the following functions. The same applies to the "b85m_master_kma_dongle" project in the BLE SDK.
void user_init(void);
For applications with deepsleep retention mode, to reduce power, T_userInit needs to be as short as possible as explained earlier, so deepsleep retention wake_up would be different from power on / deepsleep wakeup.
The initialization tasks in the user_init falls into 2 categories: initialization of hardware registers, and initialization of logic variables in SRAM.
Since in deepsleep retention mode first 16K or 32K SRAM is non-volatile, logic variables can be defined as retention_data to save time for initialization. Since registers cannot retain data across deepsleep retention, re-initialization is required for registers.
In summary, for deepsleep retention wake_up, user_init_deepRetn applies; while for power on and deepsleep wake_up, user_init_normal function applies, as shown in following code:
int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();
if( deepRetWakeUp ){
user_init_deepRetn ();
}
else{
user_init_normal ();
}
The user can compare the implementation of these two functions. The following is the implementation of the user_init_deepRetn function in the SDK demo "ble_remote".
_attribute_ram_code_ void user_init_deepRetn(void)
{
#if (PM_DEEPSLEEP_RETENTION_ENABLE)
blc_app_loadCustomizedParameters();
blc_ll_initBasicMCU(); //mandatory
rf_set_power_level_index (MY_RF_POWER_INDEX);
blc_ll_recoverDeepRetention();
app_ui_init_deepRetn();
#endif
}
First 3 lines (from code blc_app_loadCustomizedParameters to rf_set_power_level_index) are mandatory BLE initialization of hardware registers.
The blc_ll_recoverDeepRetention() is to recover software and hardware state at Link Layer by low level stack.
User is not recommended to modify these lines.
Finally, app_ui_init_deepRetn is the user's re-initialization of the hardware registers used by the application layer. The GPIO wakeup configuration and LED state setting in the demo “ble_remote” are hardware initialization. The UART hardware register state in the demo “ble_module” needs to re-initialize.
On top of SDK demo, if additional items are added to user initialization, following judgement is recommended:
-
If it is SRAM variable, put it to the “retention_data” section by adding the keyword “attribute_data_retention”, so as to save re-initialization time after deepsleep retention wake_up. Then it can be run at user_init_normal function.
-
If it is hardware register, it should be placed inside user_init_deepRetn function to ensure the correct hardware status.
With above implementation, after deepsleep retention wake_up, T_userInit is execution time of user_init_deepRetn. The SDK also tries to place these functions inside ram_code to save time. If deepsleep retention area allows, user should place added register initialization functions inside ram_code as well.
(3) T_userInit Optimization for Conn state slave role
TBD
(4) T_cstartup
T_cstartupis the execution time of cstartup_xxx.S, e.g. cstartup_825x.S in the SDK. Please refer to the boot file in the SDK.
T_cstartup has 4 components, in time sequence:
T_cstartup = T_cs_1 + T_cs_bss + T_cs_data + T_cs_2
T_cs_1 and T_cs_2 are fixed timing which user is not allowed to modify.
The T_cs_data is initialization of “data” sector in SRAM. The “data” is already initialized global variables with initial values stored in “data initial value” sector of flash. Therefore, T_cs_data is the time transferring “data” from flash “data initial value” sector to SRAM “data” sector. Corresponding assembly code is:
tloadr r1, DATA_I
tloadr r2, DATA_I+4
tloadr r3, DATA_I+8
COPY_DATA:
tcmp r2, r3
tjge COPY_DATA_END
tloadr r0, [r1, #0]
tstorer r0, [r2, #0]
tadd r1, #4
tadd r2, #4
tj COPY_DATA
COPY_DATA_END:
Data transferring from flash is slow. As a reference, 16 bytes would take 7us. So more data are in “data” sector, the longer T_cs_data and T_init would be, or vice versa.
User can use method explained earlier to check size of "data" sector in list file.
If “data” sector is too big and there is enough space in deepsleep retention area, user can add the keyword “attribute_data_retention” to place some of the variables in “data” sector into “retention_data” sector, so as to reduce T_cs_data and T_init.
T_cs_bss is time to initialize SRAM “bss” sector. Initial values of “bss” sector are all 0s. It’s only need to reset SRAM “bss” sector to 0, and no flash transferring is needed. Corresponding assembly code is:
tmov r0, #0
tloadr r1, DAT0 + 16
tloadr r2, DAT0 + 20
ZERO:
tcmp r1, r2
tjge ZERO_END
tstorer r0, [r1, #0]
tadd r1, #4
tj ZERO
ZERO_END:
Resetting each word (4 byte) to 0 can be very fast. So when “bss” is small, T_cs_bss is very small. But if “bss” sector is large, for example when a huge global data array is defined (int AAA[2000] = {0}),T_cs_bss can also be very long. So it is worth paying attention to “bss” size in list file.
To optimize T_cs_bss when “bss” sector is large, if retention area allows, some of them can also be defined as “attribute_data_retention” to place in “retention_data” sector.
(5) T_init measurement
After T_cstartup and T_userInit are optimized to minimize T_init, it’s also needed to measure T_init, and apply to API: blc_pm_setDeepsleepRetentionEarlyWakeupTiming
T_init starts at the timing as T_cstartup, which is the “_reset” point in cstartup_8258_RET_16K.S file as shown below:
__reset:
#if 0
@ add debug, PB4 output 1
tloadr r1, DEBUG_GPIO @0x80058a PB oen
tmov r0, #239 @0b 11101111
tstorerb r0, [r1, #0]
tmov r0, #16 @0b 00010000
tstorerb r0, [r1, #1] @0x800583 PB output
#endif
Combined with the Debug GPIO indication in the picture "T_init timing", the Debug GPIO PB4 output high operation is placed in "__reset". The user only needs to change "#if 0" to "#if 1" to enable the PB4 output high operation.
T_cstartup finishes at “tjl main”.
tjl main
END: tj END
Since main function starts almost at the end of T_cstartup, PB4 can be set to output low at beginning of main function as shown below. Please note that DBG_CHN0_LOW requires enabling “DEBUG_GPIO_ENABLE” in app_config.h.
_attribute_ram_code_ int main (void) //should run in ram_code
{
DBG_CHN0_LOW; //debug
cpu_wakeup_init();
……
}
By scoping signal of PB4, T_cstartup is obtained.
Adding PB4 output high at end of T_userInit inside user_init_deepRetn will generate same timing diagram as Debug GPIO as shown above. T_init and T_cstartup can be measured by oscilloscope or logic analyzer. Following understanding of GPIO operation, user can modify the Debug GPIO code as needed, so as to get other timing parameters as well, e.g. T_sysInit, T_userInit etc.
Connection Latency
Sleep timing with non-zero connection latency
The previous introduction to the sleep mode of Conn state slave role (refer to the figure "sleep timing for Advertising state & ConnSlaveRole") is based on the premise that connection latency (conn_latency for short) does not take effect.
In the PM software processing flow, T_wakeup = T_brx + conn_interval, the corresponding code is as follows.
if(conn_latency != 0)
{
latency_use = bls_calculateLatency();
T_wakeup = T_brx + (latency_use +1) * conn_interval;
}
else
{
T_wakeup = T_brx + conn_interval;
}
When the BLE slave goes through the connection parameters update process and conn_latency takes effect, the sleep wake_up time is:
T_wakeup = T_brx + (latency_use +1) * conn_interval;
Following diagram illustrates sleep timing with non-zero conn_latency when latency_use= 2.

When conn_latency is not effective, the sleep duration is no more than 1 connection interval (generally small). After conn_latency becomes effective, the sleep time may have a relatively large value, such as 1S, 2S, etc., and the system power consumption can become very low. It makes sense to use deepsleep retention mode with lower power consumption during long sleep.
latency_use calculation
At effective conn_latency, T_wakeup is determined by latency_use, so it is not necessarily equal to conn_latency.
latency_use = bls_calculateLatency();
In the calculation of latency_use, user_latency is involved. This is the value that the user can set. The API to be called and its source code are:
void bls_pm_setManualLatency(u16 latency)
{
bltPm.user_latency = latency;
}
Initial value of bltPm.user_latency is 0xFFFF, and at the end of blt_brx_sleep function it will be reset to 0xFFFF, which means the value set by the API bls_pm_setManualLatency is only valid for latest sleep, so it needs to be set on every sleep event.
The calculation process of latency_use is as follows.
First calculate the system latency:
(1) If connection latency is 0,system latency is 0
(2) If connection latency is not 0:
-
If system task is not done in current connection interval, MCU needs to wake up on next connection interval to continue the task such as transfer packet not completely sent out, or handle data from master not fully processed yet, and under this scenario, system latency is 0.
-
If no task is left over, system latency = connection latency. However, if slave receives master’s update map request or update connection parameter request, and its updated timing is before (connection latency+1)*interval, then the actual system latency would force MCU to wake up before the updated timing point to ensure correct BLE timing sequence.
Combining user_latency and system_latency:
latency_use = min(system latency, user_latency)
Accordingly, if user_latency set by the API bls_pm_setManualLatency is less than system latency, user_latency would be the final latency_use; otherwise, system latency is the final latency_use.
API bls_pm_getSystemWakeupTick
Following API is used to obtain wakeup time out of suspend (System Timer tick), or T_wakeup:
u32 bls_pm_getSystemWakeupTick(void);
According to blt_brx_sleep function in PM software process flow, T_wakeup is calculated fairly late, almost next to cpu_sleep_wakeup. Application layer can only get an accurate T_wakeup by BLT_EV_FLAG_SUSPEND_ENTER event callback function.
Following keyscan example explains usage of BLT_EV_FLAG_SUSPEND_ENTER event callback function and bls_pm_getSystemWakeupTick.
bls_app_registerEventCallback(BLT_EV_FLAG_SUSPEND_ENTER, &ble_remote_set_sleep_wakeup);
void ble_remote_set_sleep_wakeup (u8 e, u8 *p, int n)
{
if( blc_ll_getCurrentState() == BLS_LINK_STATE_CONN && ((u32)(bls_pm_getSystemWakeupTick() - clock_time())) >
80 * CLOCK_SYS_CLOCK_1MS){
bls_pm_setWakeupSource(PM_WAKEUP_PAD);
}
}
Above callback function is meant to prevent loss of key press.
A normal key press lasts for a few hundred ms, or at least 100~200ms for a fast press. When Advertising state and Conn state are configured by bls_pm_setSuspendMask to enter sleep mode, without conn_latency in effect, as long as ADV Interval or conn_interval is not very long, typically less than 100ms, sleep time will not exceed ADV Interval or conn_interval, in other words, sleep time is less than 100ms or a fast key press time, loss of key press can be prevented and there is no need to enable GPIO wakeup.
With conn_latency ON, for example, with conn_interval = 10ms, connec_latency = 99, sleep time may last 1s, obviously key loss may occur. If current state is Conn state and wakeup time of suspend to be entered is more than 80ms from current time as determined by BLT_EV_FLAG_SUSPEND_ENTER callback function, key loss can be prevented by using GPIO level trigger to wake up MCU for keyscan process in case timer wakeup is too late.
Issues in GPIO Wake-up
Fail to enter sleep mode when wake-up level is valid
In B85m, GPIO wakeup is level triggered instead of edge triggered, so when GPIO PAD is configured as wakeup source, for example, suspend wakeup triggered by GPIO high level, MCU needs to make sure when MCU invokes cpu_wakeup_suspend to enter suspend, that the wakeup GPIO is not at high level. If the current level is already high, the actual entry into the cpu_wakeup_sleep function will be invalid when the suspend is triggered, and it will exit immediately, i.e. it will not enter the suspend at all.
If the above situation occurs, it may cause unexpected problems, for example, it was intended to enter deepsleep and be woken up and the program re-executed, but it turns out that the MCU cannot enter deepsleep, resulting in the code continuing to run, not in the state we expected, and the whole flow of the program may be messed up.
User should pay attention to avoid this problem when using Telink's GPIO PAD to wake up.
If the APP layer does not avoid this problem, and GPIO PAD wakeup source is already effective at invoking of cpu_wakeup_sleep, PM driver makes some improvement to avoid flow mess:
(1) Suspend & deepsleep retention mode
For both suspend and deepsleep retention mode, the SW will fast exit cpu_wakeup_sleep with two potential return values:
-
Return WAKEUP_STATUS_PAD if the PM module has detected effective GPIO PAD state.
-
Return STATUS_GPIO_ERR_NO_ENTER_PM if the PM module has not detected effective GPIO PAD state.
(2) deepsleep mode
For deepsleep mode, PM diver will reset MCU automatically in bottom layer (equivalent to watchdog reset). The SW restarts from “Run hardware bootloader”.
To prevent this problem, following is implemented in the SDK demo “ble remote”.
In BLT_EV_FLAG_SUSPEND_ENTER, it is configured that only when suspend time is larger than a certain value, can GPIO PAD wakeup be enabled.
void ble_remote_set_sleep_wakeup (u8 e, u8 *p, int n)
{
if( blc_ll_getCurrentState() == BLS_LINK_STATE_CONN && ((u32)(bls_pm_getSystemWakeupTick() - clock_time())) >
80 * CLOCK_SYS_CLOCK_1MS){
bls_pm_setWakeupSource(PM_WAKEUP_PAD);
}
}
When key is pressed, manually set latency to 0 or a small value (as shown in below code), so as to ensure short sleep time, e.g. shorter than 80ms as set in above code. Therefore, the high level on drive pin due to a pressed key will never become a high-level GPIO PAD wakeup trigger.

There are 2 scenarios that will make MCU enter deepsleep.
-
First one is if there’s no event for 60s, MCU will enter deepsleep. The events here include keys being pressed, so there is no drive pin high at this point to make deepsleep inaccessible.
-
The other scenario is if a key is stuck for more than 60s, MCU will enter deepsleep. Under the second scenario, the SDK will invert polarity from high level trigger to low level trigger to solve the problem.
BLE System Low Power Management
Based upon understanding of PM principle of this BLE SDK, user can configure PM under different application scenarios, referring to the demo “ble remote” low power management code as explained below.
Function blt_pm_proc is added in PM configuration of main_loop. This function should be placed at the end of main_loop to ensure it is immediate to blt_sdk_main_loop in time, since blt_pm_proc needs to configure low power management according to different UI entry tasks.
Summary of highlights in blt_pm_proc function:
(1) When UI task requires turning off sleep mode, such as audio (ui_mic_enable) and IR, set bltm.suspend_mask to SUSPEND_DISABLE.
(2) After advertising for 60s in Advertising state, MCU enters deepsleep with wakeup source set to GPIO PAD in user initialization. The 60s timeout is determined by software timer using advertise_begin_tick variable to capture advertising start time.
The design of 60s into deepsleep is to save power, prevent slave wasting power on advertising even when not connected with master. User can justify 60s setting based on different applications.
(3) At Conn state slave role, under conditions of no key press, no audio or LED task for over 60s from last task, MCU enters deepsleep with GPIO PAD as wakeup source, and at the same time set DEEP_ANA_REG0 label in deepsleep register, so that once after wakeup slave will connect quickly with master.
The design of 60s into deepsleep is to save power. Actually if power consumption under connected state is tuned low enough as with deepsleep retention, it is not absolutely necessary to enter deepsleep.
To enter deepsleep at Conn state slave role, slave first issues a TERMINATE command to master by calling bls_ll_terminateConnection, after receiving ack which triggers BLT_EV_FLAG_TERMINATEcallback function, slave will enter deepsleep. If slave enters deepsleep without sending any request, since master is still at connected state and would constantly try to synchoroniz with slave till connection timeout. The connection timeout could be a very large value, e.g. 20s. If slave wakes up before 20s, slave would send advertising packet attempting to connect with master. But since master would assume it is already in connected state, it would not be able to connect to slave, and user experience is therefore very slow reconnection.
(4) If certain task can not be disrupt by long sleep time, user_latency can be set to 0, so latency_use is 0.
Under applications such as key_not_released, or DEVICE_LED_BUSY, call API bls_pm_setManualLatency to set user_latency to 0. When conn_interval is 10ms, sleep time is no more than 10ms.
(5) For scenario as in item 4, with latency set to 0, slave will wakeup at every conn interval, power might be unnecessarily too high since key scan and LED task does not repeat on every conn interval. Further power optimization can be done as following:
When LONG_PRESS_KEY_POWER_OPTIMIZE=1, once key press is stable (key_matrix_same_as_last_cnt > 5), manually set latency. With bls_pm_setManualLatency (3), sleep time will not exceed 4 *conn_interval. If conn_interval=10 ms, MCU will wake up every 40ms to process LED task and keyscan.
User can tweak this approach toward different conn intervals and task response time requirements.
Timer Wake-up by Application Layer
At Advertising state or ConnSlaveRole, without GPIO PAD wakeup, once MCU enters sleep mode, it only wakes up at T_wakeup pre-determined by BLE SDK. User can not wake up MCU at an earlier time which might be needed at certain scenario. To provide more flexibility, application layer wakeup and associated callback function are added in the SDK:
Application layer timer wakeup API:
void bls_pm_setAppWakeupLowPower(u32 wakeup_tick, u8 enable);
“wakeup_tick” is wakeup time at System Timer tick value.
“enable”: 1-wakeup is enabled; 0-wakeup is disabled.
Registered call back function bls_pm_registerAppWakeupLowPowerCb is executed at application layer wakeup:
typedef void (*pm_appWakeupLowPower_callback_t)(int);
void bls_pm_registerAppWakeupLowPowerCb(pm_appWakeupLowPower_callback_t cb);
Take ConnSlaveRole as an example:
When the user uses bls_pm_setAppWakeupLowPower to set the app_wakeup_tick for the application layer to wake up regularly, the SDK will check whether app_wakeup_tick is before T_wakeup before entering sleep.
-
If app_wakeup_tick is before T_wakeup, as shown in the figure below, it will trigger sleep in app_wakeup_tick to wake up early;
-
If app_wakeup_tick is after T_wakeup, MCU will still wake up at T_wakeup.

Low Battery Detect
Battery power detect/check, which may also appear in the Telink BLE SDK and related documentation under other names, includes: battery power detect/check, low battery detect/check low power detect/check), battery detect/check, etc. For example, the relevant files and functions in the SDK are named battery_check, battery_detect, battery_power_check, etc.
This document is unified under the name of "low battery detect".
The Importance of Low Battery Detect
For battery-powered products, as the battery power will gradually drop, when the voltage is low to a certain value, it will cause many problems.
a) The operating voltage range is 1.8V~3.6V. When the voltage is lower than 1.8V, stable operation cannot be guaranteed.
b) When the battery voltage is low, due to the unstable power supply, the "write" and "erase" operations of Flash may have the risk of error, causing the program firmware and user data to be modified abnormally, and eventually causing the product to fail. Based on our previous mass production experience, we set the low voltage threshold for this risk to 2.0V.
According to the above description, for battery-powered products, a secure voltage should be set, and the MCU is allowed to continue working only when the voltage is higher than this secure voltage; once the voltage falls below the secure voltage, the MCU stops running and needs to be shutdown immediately (this is achieved by entering deepsleep mode on the SDK).
The secure voltage is also called alarm voltage, and the value of this voltage is 2.0 V by default in the SDK.
Note
- The low voltage protection threshold 2.0V is an example and reference value. Customers should evaluate and modify this threshold according to the actual situation. If users have unreasonable designs in the hardware circuit, which leads to a decrease in the stability of the power supply network, the safety thresholds should be increased as appropriate.
For the product developed and implemented using Telink BLE SDK, as long as the use of battery power, low power detection should be a real-time operation of the task for the product's entire life cycle to ensure the stability of the product.
The protection of low voltage detection for flash operation will be introduced further in the chapter on flash.
The Implementation of Low Battery Detect
The low battery detect requires the use of ADC to measure the power supply voltage. Users can refer to the 8258/8278 datasheet and Driver SDK Developer Handbook chapter on ADC to get the necessary understanding of the B85m ADC module first.
The implementation of the low battery detect is described in the SDK demo "ble_sample", refer to the files battery_check.h and battery_check.c.
Make sure the macro "APP_BATT_CHECK_ENABLE" is enabled in app_config.h. This macro is disabled by default, and users need to pay attention to it when using the low battery detect function.
#define APP_BATT_CHECK_ENABLE 1
Notes on Low Battery Detect
Low battery detect is a basic ADC sampling task, and there are a number of issues that need attention when implementing an ADC to sample the supply voltage, as described below.
GPIO Input Channel Recommended
The ADC input channels support ADC sampling of the supply voltage on the "VCC/VBAT" input channel, which corresponds to the last "VBAT" in the variable ADC_InputPchTypeDef below.
However, for some special reasons, the 825x "VBAT" channel cannot be used. Telink stipulates that the "VBAT" input channel is not allowed, and the GPIO input channel should be used.
The available GPIO input channels are the input channels corresponding to PB0~PB7, PC4, and PC5.
/*ADC analog input positive channel*/
typedef enum {
……
B0P,
B1P,
B2P,
B3P,
B4P,
B5P,
B6P,
B7P,
C4P,
C5P,
……
VBAT,
}ADC_InputPchTypeDef;
ADC sampling of the supply voltage using the GPIO input channel can be implemented in two ways:
a) Power supply connected to the GPIO input channel
In the hardware circuit design, the power supply is directly connected to the GPIO input channel, and the ADC is initialized by setting the GPIO to high resistance (ie, oe, output all set to 0), at which time the voltage on the GPIO is equal to the power supply voltage, and ADC sampling can be performed directly.
b) Power supply does not touch the GPIO input channel
There is no need for power supply and GPIO input channel connection on the hardware circuit. It needs to be measured with the output high level of GPIO. The 825x internal circuit structure is designed to ensure that the voltage value of GPIO output high level and power supply voltage value are always equal. The high level of the GPIO output can be used as the power supply voltage, and ADC sampling is performed through the GPIO input channel.
Currently, the GPIO input channel selected for low voltage detection is PB7, utilizing the second method: "power supply not connected to GPIO input channel".
Choose PB7 as GPIO input channel, PB7 as ordinary GPIO function, initialize all states (ie, oe, output) using the default state, no special modification.
#define GPIO_VBAT_DETECT GPIO_PB7
#define PB7_FUNC AS_GPIO
#define PB7_INPUT_ENABLE 0
#define ADC_INPUT_PCHN B7P
When ADC sampling is required, PB7 outputs high level:
gpio_set_output_en(GPIO_VBAT_DETECT, 1);
gpio_write(GPIO_VBAT_DETECT, 1);
The output state of PB7 can be turned off after the ADC sampling is finished. Since the PB7 pin is floating (not connected to other circuits), the high output level does not cause any leakage, so the output state of PB7 is not turned off on the SDK.
Differential Mode Only
Although the 8x5x ADC input mode supports both Single Ended Mode and Differential Mode, for some specific reasons, Telink specifies that only Differential Mode can be used, and Single Ended Mode is not allowed.
The differential mode input channel is divided into positive input channel and negative input channel, the measured voltage is the voltage difference obtained by subtracting the negative input channel voltage from the positive input channel voltage.
If the ADC sample has only one input channel, when using differential mode, set the current input channel as the positive input channel and GND as the negative input channel, so that the voltage difference between the two is equal to the positive input channel voltage.
The differential mode is used in SDK low battery detect, the interface function is as follows. The "#if 1" and "#else" branches are the exact same function settings, the "#if 1" is just to make the code run faster to save time. It can be understood by looking at "#else", the adc_set_ain_channel_differential_mode API selects PB7 as the positive input channel and GND as the negative input channel.
#if 1 //optimize, for saving time
//set misc channel use differential_mode,
//set misc channel resolution 14 bit, misc channel differential mode
//notice that: in differential_mode MSB is sign bit, rest are data, here BIT(13) is sign bit
analog_write (anareg_adc_res_m, RES14 | FLD_ADC_EN_DIFF_CHN_M);
adc_set_ain_chn_misc(ADC_INPUT_PCHN, GND);
#else
////set misc channel use differential_mode,
adc_set_ain_channel_differential_mode(ADC_MISC_CHN, ADC_INPUT_PCHN, GND);
//set misc channel resolution 14 bit
//notice that: in differential_mode MSB is sign bit, rest are data, here BIT(13) is sign bit
adc_set_resolution(ADC_MISC_CHN, RES14);
#endif
Should Use Dfifo Mode to Obtain ADC Sampling Value
For 825x, Telink stipulates that only Dfifo mode can be used to read ADC sampling values. Refer to the following function.
unsigned int adc_sample_and_get_result(void);
Need to Switch Different ADC Tasks
According to the "825x/827x datasheet", the "ADC state machine" includes several channels such as "Left", "Right", and "Misc". Due to specific reasons, these "state channels" cannot operate simultaneously. "Telink" stipulates that the channels within the "ADC state machine" should operate independently and cannot function at the same time.
The Misc channel is used for low battery detect as the most basic ADC sampling. Users need to use the Misc channel if they need other ADC tasks besides low battery detect. The Amic Audio uses Left channel. The low battery detect cannot run simultaneously with other ADC tasks and should be implemented by switching.
Stand-Alone Use of Low Battery Detect
Users define the macro "BLE_AUDIO_ENABLE" in the "ble_remote" app_config.h file to 0 (turn off all functions of Audio) to get a demo of the ADC being used only by low voltage detection. Or refer to the "ble_module" demo and for "ble_sample" demo low voltage detection.
Low Battery Detect Initialization
Refer to the implementation of the adc_vbat_detect_init function.
The order of ADC initialization should satisfy the following procedure: first power off sar adc, then configure other parameters, and finally power on sar adc. All initialization of ADC sampling should follow this flow.
void adc_vbat_detect_init(void)
{
/******power off sar adc********/
adc_power_on_sar_adc(0);
//add ADC configuration
/******power on sar adc********/
//note: this setting should be set after all other settings
adc_power_on_sar_adc(1);
}
For the configuration before sar adc power on and power off, the user try not to modify, and use the default settings. If users choose a different GPIO input channel, directly modify the ADC_INPUT_PCHN related macro definition described earlier. If user adopts the hardware circuit design "power supply connected to GPIO input channel", the operation of "GPIO_VBAT_DETECT" output high level needs to be removed
The adc_vbat_detect_init initialization function is called in app_battery_power_check with the following code:
if(!adc_hw_initialized){
adc_hw_initialized = 1;
adc_vbat_detect_init();
}
Here, a variable "adc_hw_initialized" is used. Initialization is performed once and the variable is set to 1 only when it is 0; when the variable is 1, initialization is not performed again. The operation to clear "adc_hw_initialized" is completed by calling the API "battery_clear_adc_setting_flag":
static inline void battery_clear_adc_setting_flag (void)
{
adc_hw_initialized = 0;
}
The functions that can be implemented by a design using adc_hw_initialized are:
a) Switching with other ADC task
The effect of sleep mode (suspend/deepsleep retention) is not considered first, and only the switching between low battery detect and other ADC tasks is analyzed.
Because of the need to consider the switch between low battery detect and other ADC tasks, adc_vbat_detect_init may be executed several times, so it cannot be written to user_init_normal and should be implemented in main_loop.
The first time the app_battery_power_check function is executed, adc_vbat_detect_init is executed and will not be executed repeatedly.
Once the "ADC other task" needs to be executed, it will take away the ADC usage and make sure that the "ADC other task" should call battery_set_detect_enable(0) when it is initialized, which will clear adc_hw_initialized to 0.
When the "ADC other task" is finished, the right to use the ADC is handed over. The app_battery_power_check is executed again, and since the value of adc_hw_initialized is 0, adc_vbat_detect_init should be executed again. This ensures that the low battery detect is reinitialized each time it is switched back.
b) Adaptive handling of suspend and deepsleep retention
Take sleep mode into account.
The adc_hw_initialized variable should be defined as a variable on the "data" or "bss" segment, not on the retention_data. Defining it on the "data" segment or "bss" ensures that this variable is used after each deepsleep retention wake_up when the software bootloader is executed (i.e., cstartup_xxx. S) will be re-initialized to 0; after sleep wake_up, this variable can be left unchanged.
The common feature of the register configured inside the adc_vbat_detect_init function is that it does not power down in suspend mode and can maintain the state; it will power down in deepsleep retention mode.
If the MCU enters into suspend mode, when it wakes up and executes app_battery_power_check again, the value of adc_hw_initialized is the same as before suspend, so there is no need to re-execute the adc_vbat_detect_init function.
If the MCU enters deepsleep retention mode and wakes up with adc_hw_initialized to 0, adc_vbat_detect_init should be re-executed and the ADC-related register state needs to be reconfigured.
The state of register set in the adc_vbat_detect_init function can be kept from powering down during the suspend.
Refer to the "Low Power Management" section that the Dfifo related registers will be powered down in suspend mode, so the following two codes are not put in the adc_vbat_detect_init function, but in the app_ battery_power_check function, to ensure that it is reset before each low power detection.
adc_config_misc_channel_buf((u16 *)adc_dat_buf,ADC_SAMPLE_NUM<<2);
dfifo_enable_dfifo2();
The keyword "attribute_ram_code" is added to the adc_vbat_detect_init function in the SDK to set it to ram_code, with the ultimate goal of optimizing power consumption for long sleep connection states. For example, for a typical long sleep connection of 10ms * (99+1) = 1s, waking up every 1s and using deepsleep retention mode during long sleep, adc_vbat_detect_init should be executed again after each wake-up, and the execution speed will become faster after adding to ram_code.
This "_attribute_ram_code_" is not required. In the product application, the user can decide whether to put this function into ram_code based on the usage of the deepsleep retention area and the results of the power test.
Low Battery Detect Processing
In main_loop, the app_battery_power_check function is called to implement the processing of low battery detect, and the related code is as follows.
_attribute_data_retention_ u8 lowBattDet_enable = 1;
u8 adc_hw_initialized = 0;
void battery_set_detect_enable (int en)
{
lowBattDet_enable = en;
if(!en){
battery_clear_adc_setting_flag(); //need initialized again
}
}
int battery_get_detect_enable (void)
{
return lowBattDet_enable;
}
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(VBAT_DEEP_THRES_MV);
}
The default value of lowBattDet_enable is 1. Low battery detect is allowed by default, and the MCU can start low battery detect immediately after powering up. This variable needs to be set to retention_data to ensure that deepsleep retention cannot modify its state.
The value of lowBattDet_enable can only be changed when other ADC tasks need to seize ADC usage: when other ADC tasks start, battery_set_detect_enable(0) is called, at this time app_battery_power_check is not called again in main_loop; After the other ADC tasks are finished, call battery_set_detect_enable(1) to surrender the right to use ADC, then the app_battery_power_check function can be called again in main_loop.
The frequency of low battery detect is controlled by the variable lowBattDet_tick, which is executed every 500ms in the demo. Users can modify this time according to their needs.
The specific implementation of the app_battery_power_check function seems to be cumbersome, involving the initialization of low-power detection, Dfifo preparation, data acquisition, data processing, low-power alarm processing, etc.
The ADC sampling data is acquired using Dfifo mode. Dfifo samples 8 strokes of data by default and calculates the average value after removing the maximum and minimum values.
The adc_vbat_detect_init function shows that the period of each adc sample is 10.4us, so the data acquisition process is about 83us.
The macro "ADC_SAMPLE_NUM" in the demo can be modified to 4, which shortens the ADC sampling time to 41 us. it is recommended to use the method of 8 data strokes for more accurate calculation results.
#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
The app_battery_power_check function is put on ram_code, refer to the above description of "adc_vbat_detect_init" ram_code, also to save running time and optimize power consumption.
The "_attribute_ram_code_" is not necessary. In the product application, the user can decide whether to put this function into ram_code based on the usage of the deepsleep retention area and the results of the power test.
_attribute_ram_code_ int app_battery_power_check(u16 alarm_vol_mv)
Low Voltage Alarm
The parameter alarm_vol_mv of app_battery_power_check is to specify the alarm voltage in mV for low battery detect. When the power supply voltage is lower than alarm_vol_mv, app_battery_power_check returns 0. According to the previous content, the default setting in SDK is 2000 mV. In the low voltage detection of main_loop, when the power supply voltage is lower than 2000 mV, it enters low voltage range.
The demo code for handling low voltage alarm is shown below. The MCU should be shutdown after low voltage, and no other work can be done.
The "ble_remote" uses the way of entering deepsleep to shut down the MCU, and a button is set to wake up the remote control.
In addition to shutdown, user can modify other alarm behaviors for low voltage alarm processing.
In the code below, 3 quick flashes are made using LED lights and add a printf print prompt to inform the product user that the battery needs to be charged or replaced.
#if (UI_LED_ENABLE) //led indicate
gpio_set_output_en(GPIO_LED, 1); //output enable
for(int k = 0; k < 3; k++){
gpio_write(GPIO_LED, LED_ON_LEVEL);
sleep_us(200000);
gpio_write(GPIO_LED, !LED_ON_LEVEL);
sleep_us(200000);
}
#endif
if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
tlkapi_printf(APP_BATT_CHECK_LOG_EN, "[APP][BAT] The battery voltage is lower than %dmV, shut down!!!\n", (alarm_vol_mv + 200));
} else {
tlkapi_printf(APP_BATT_CHECK_LOG_EN, "[APP][BAT] The battery voltage is lower than %dmV, shut down!!!\n", alarm_vol_mv);
}
After "ble_remote" is shutdown, they enter the deepsleep mode where they can be woken up. If a key wake-up occurs, the SDK will do a quick low battery detect during user initialization instead of waiting until the main_loop. The reason for this process is to avoid application errors, as illustrated by the following example.
If the product user has been alerted by the flashing LED during the low power alarm and then wakes up again by entering deepsleep, it takes at least 500ms to do the low battery detect from the processing of main_loop. Before 500ms, the slave's broadcast packet has been sent for a long time, and it is likely to be connected to the master already. In this case, there is a bug that the device already having low power alarm continues to work again.
For this reason, the SDK should do the low battery detect in advance during user initialization, and should prevent the above situation from happening at this step. So add low battery detect during user_battery_power_check, and the function interface in the SDK is:
if(analog_read(USED_DEEP_ANA_REG) & LOW_BATT_FLG){
battery_check_returnValue = app_battery_power_check(alarm_vol_mv + 200);
}
#if (APP_BATT_CHECK_ENABLE)
user_battery_power_check(VBAT_DEEP_THRES_MV);
#endif
Based on the value of the USED_DEEP_ANA_REG analog register, it can be determined whether the wake-up was caused by a low-battery alarm shutdown. At this time, a rapid low-battery detection is performed, and the previous 2000mV alarm voltage is increased to 2200mV (referred to as the "recovery voltage"). The reason for the 200mV increase is:
According to the value of DEEP_ANA_REG2 analog register can determine whether the low power alarm shutdown is woken up, at this time, a fast low power detection is performed and the previous 2000mV alarm voltage is increased to 2200mV (called recovery voltage). The reason for the 200mV increase is:
Low voltage detection will have some errors, can not guarantee the accuracy and consistency of the measurement results. For example, if the error is 20mV, the voltage detected for the first time may be 1990mV to enter shutdown mode, and then the voltage value detected again after waking up in user initialization is 2005mV. If the alarm voltage is still 2000mV, it still can't stop the bug described above.
Therefore, it is necessary to adjust the alarm voltage slightly higher than the maximum error of low power detection during the fast low power detection after the shutdown mode wakeup.
Only when a certain low power detection found that the voltage is lower than 2000mV into shutdown mode, the recovery voltage 2200mV will appear, so user does not have to worry about this 2200mV will misreport low voltage to the actual voltage 2V~2.2V products. Product users see the low voltage alarm indication, after charging or replacing the battery to meet the requirements of recovery voltage, the product back to normal use.
Low Battery Detect and AMIC Audio
Referring to the detailed introduction in Low Battery Detect Stand-alone Use mode, for products that need to implement Amic Audio, just switch between Low Battery Detect and Amic Audio.
According to the low battery detection stand-alone use mode, after the program starts running, the default low battery detection is enabled first. When Amic Audio is triggered, do the following two things.
(1) Disable low battery detection
Call battery_set_detect_enable(0) to inform the low battery detect module that the ADC resources have been seized.
(2) Amic Audio ADC initialization
Since the ADC is used in a different way than the low battery detection, the ADC needs to be initialized again. For details, refer to the "Audio" section of this document.
At the end of Amic Audio, battery_set_detect_enable(1) is called to inform the low battery detect module that the ADC resources have been released. At this point the low battery detection needs to reinitialize the ADC module and then start the low battery detection.
If it is low battery detection and other non-Amic Audio ADC tasks at the same time, the processing of other ADC tasks can imitate the processing flow of Amic Audio.
If there are three kinds of tasks at the same time: low battery detection, Amic Audio and other ADC tasks, user can refer to the method of switching between low battery detection and Amic Audio to implement them according to the principle of "switching if ADC circuit needs".
Low Battery Detection in ble_sample Reference Design
In the "ble_sample reference design", the "low battery detection" function is disabled by default. Users need to manually enable this function:
(1) In the app_config.h file, set the APP_BATT_CHECK_ENABLE macro definition to 1:
#define APP_BATT_CHECK_ENABLE 1
(2) In the main loop of the app.c file, the low battery detection code is already included:
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick,500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(VBAT_DEEP_THRES_MV);
}
(3) In the user initialization function, rapid low battery detection is already included:
#if (APP_BATT_CHECK_ENABLE)
user_battery_power_check(VBAT_DEEP_THRES_MV);
#endif
(4) In the TC321x series chips, the low battery detection will be restricted when the audio function is enabled:
#if (MCU_CORE_TYPE == MCU_CORE_TC321X)
if(!ui_mic_enable){ //If audio is not enabled, then perform low battery detection.
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(VBAT_DEEP_THRES_MV);
}
}
#endif
Low Battery Detection in ble_remote Reference Design
In the ble_remote reference design, the "low battery detection" function is enabled by default. The relevant configuration and implementation are as follows:
(1) In the app_config.h file, set the APP_BATT_CHECK_ENABLE macro definition to 1:
#define APP_BATT_CHECK_ENABLE 1
(2) In the main loop of the app.c file, the low battery detection code is already included:
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(VBAT_DEEP_THRES_MV);
}
(3) In the user initialization function, rapid low battery detection is already included:
#if (APP_BATT_CHECK_ENABLE)
user_battery_power_check(VBAT_DEEP_THRES_MV);
#endif
(4) In the TC321x series chips, the low battery detection is restricted when the audio function is enabled:
#if (MCU_CORE_TYPE == MCU_CORE_TC321X)
if(!ui_mic_enable){ //Low-voltage detection is performed only if audio is disabled.
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(VBAT_DEEP_THRES_MV);
}
}
#endif
Configuration Parameters for Low Battery Detection
The main configuration parameters for low battery detection are defined in the battery_check.h file:
#define VBAT_DEEP_THRES_MV 2000 // 2000 mV Low voltage alarm threshold
Users can adjust this threshold based on actual requirements, but the following should be noted:
(1) The low voltage protection threshold value of 2.0V is merely an example or reference value. Customers should evaluate and modify this threshold based on actual conditions.
(2) If unreasonable hardware circuit design leads to reduced power network stability, the safety threshold should be increased accordingly.
Relationship Between Low Battery Detection and Flash Protection
A critical purpose of low battery detection is to protect Flash operations. When the battery voltage is low, power instability creates a risk of errors during Flash "write" and "erase" operations. This can cause the program firmware and user data to be abnormally modified, ultimately leading to product failure.
Therefore, in mass production applications, users should monitor battery voltage to prevent abnormal Flash writing or erasing at low voltages. The SDK already provides relevant protection mechanisms, but users should ensure that the low battery detection function is correctly enabled and configured.
Audio
The audio source can be AMIC or DMIC. Currently, the SDK demo "ble_remote" supports only AMIC Audio.
AMIC needs to use the codec module inside the chip to sample and post-process the original Audio signal, and finally convert it into a digital signal and transmit it to the MCU.
Audio Mode Overview
The SDK supports multiple audio modes, configured via the TL_AUDIO_MODE macro. Audio modes are mainly categorized into RCU projects (Remote Control Unit) and Dongle projects (Adapter). Each project type supports different codecs and transmission methods.
Audio Mode Classification
According to the definitions in audio_common.h, audio modes are combined using the following masks:
// Codec Type Mask
#define TL_AUDIO_MASK_SBC_MODE (0x00000001) // SBC encoding
#define TL_AUDIO_MASK_MSBC_MODE (0x00000002) // mSBC encoding
#define TL_AUDIO_MASK_ADPCM_MODE (0x00000004) // ADPCM encoding
// Service Channel Type Mask
#define TL_AUDIO_MASK_HID_SERVICE_CHANNEL (0x00000100) // HID Service Channel
#define TL_AUDIO_MASK_GATT_SERVICE_TELINK_CHANNEL (0x00000200) // GATT Telink Service Channel
#define TL_AUDIO_MASK_GATT_SERVICE_GOOGLE_CHANNEL (0x00000400) // GATT Google Service Channel
// Project Type Mask
#define RCU_PROJECT (0x01000000) // RCU Project (Remote Control Unit)
#define DONGLE_PROJECT (0x02000000) // Dongle Project
// Special Function Mask
#define TL_AUDIO_MASK_DONGLE_TO_STB (0x00010000) // Special mode for Dongle to STB
Audio Modes Supported by RCU Projects
The RCU project (Remote Control Unit) supports the following audio modes:
(1) TL_AUDIO_RCU_ADPCM_GATT_TLEINK: ADPCM encoding, transmitted via GATT Telink service.
(2) TL_AUDIO_RCU_ADPCM_GATT_GOOGLE: ADPCM encoding, transmitted via GATT Google service.
(3) TL_AUDIO_RCU_ADPCM_HID: ADPCM encoding, transmitted via HID service.
(4) TL_AUDIO_RCU_SBC_HID: SBC encoding, transmitted via HID service.
(5) TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB: ADPCM encoding, HID service, Dongle to STB (Set-Top Box) mode.
(6) TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB: SBC encoding, HID service, Dongle to STB mode.
(7) TL_AUDIO_RCU_MSBC_HID: mSBC encoding, transmitted via HID service.
Audio Modes Supported by Dongle Projects
The Dongle project (Adapter) supports audio modes similar to the RCU project, with the main difference being the role.
Initialization
AMIC and Low Power Detect
Refer to the introduction of "low-power detection" in this document, when Amic Audio and low-power detection use the ADC module, ADC should be switched.
Similarly, if the two tasks of Amic Audio and other ADC task, ADC need to be switched. If the three tasks of Amic Audio, low-power detect and other ADC task, ADC need to be switched.
The 825x/827x Amic needs to be set when the Audio task is on so that low power detection and Amic to ADC module switching can be used.
AMIC Initialization
Refer to the SDK demo "ble_remote" speech processing related code.
void ui_enable_mic (int en)
{
ui_mic_enable = en;
gpio_set_output_en (GPIO_AMIC_BIAS, en); // AMIC Bias output
gpio_write (GPIO_AMIC_BIAS, en);
if(en){ // audio on
audio_config_mic_buf ( buffer_mic, TL_MIC_BUFFER_SIZE);
// Initialize AMIC according to different MCU types
#if (MCU_CORE_TYPE == MCU_CORE_827x)
audio_amic_init(AUDIO_16K);
#elif (MCU_CORE_TYPE == MCU_CORE_825x)
audio_amic_init(AUDIO_16K);
#elif (MCU_CORE_TYPE == MCU_CORE_TC321X)
// TC321x uses a different initialization method
audio_codec_stream0_input_t audio_codec_stream0_input = {
.input_src = AMIC_STREAM0_MONO_L,
.sample_rate = AUDIO_16K,
.data_width = CODEC_BIT_16_DATA,
.fifo_chn = FIFO0,
.data_buf = buffer_mic,
.data_buf_size = TL_MIC_BUFFER_SIZE,
};
audio_codec_stream0_input_init(&audio_codec_stream0_input);
audio_set_mute_mic(1);
audio_codec_clr_input_pop(20);
audio_set_codec_en(1);
audio_dfifo_config(FIFO0,(u16*)buffer_mic,TL_MIC_BUFFER_SIZE);
#endif
}
else{ // audio off
// Disable audio according to different MCU types
#if (MCU_CORE_TYPE == MCU_CORE_827x)
audio_codec_and_pga_disable(); // Disable codec and PGA
#elif (MCU_CORE_TYPE == MCU_CORE_825x)
adc_power_on_sar_adc(0); // Disable SAR ADC
#elif (MCU_CORE_TYPE == MCU_CORE_TC321X)
audio_codec_adc_power_down();
audio_power_down();
#endif
amic_gpio_reset();
}
#if (BATT_CHECK_ENABLE)
battery_set_detect_enable(!en); // Switch low battery detection status
#endif
}
In the ui_enable_mic function above, en=1 corresponds to enabling the Audio task, and en=0 corresponds to ending the Audio task.
At the beginning of Audio, GPIO_AMIC_BIAS needs to output a high level to drive Amic; after Audio ends, GPIO_AMIC_BIAS needs to be turned off to prevent this pin from leaking in sleep mode.
Following shows AMIC initialization setting:
audio_config_mic_buf ( buffer_mic, TL_MIC_BUFFER_SIZE);
audio_amic_init(AUDIO_16K);
In the working process of Audio, the data is continuously copied to SRAM through designated Dfifo. audio_config_mic_buf is used to configure the starting address and length of the Dfifo on the SRAM.
The configuration of Dfifo is handled in the ui_enable_mic function, which is equivalent to doing it again every time Audio starts. The reason is that the Dfifo control register will be lost during suspend.
After the Audio task is over, the SAR ADC should be closed to prevent leakage during suspend:
adc_power_on_sar_adc(0);
Since Amic and low power detect need to switch between using the ADC module, add battery_set_detect_enable(!en) to the ui_enable_mic function to turn off and on the low power detect. Please refer to the introduction in the low power detect section of this document.
The execution of the Audio task is placed in the UI entry part of the main_loop.
#if (BLE_AUDIO_ENABLE)
if(ui_mic_enable){ //audio
task_audio();
}
#endif
Audio Data Processing
Audio Data Volume and RF Transfer
The raw data sampled by AMIC adopt pcm format. The demo currently provides three compression algorithms, sbc, msbc and adpcm, with adpcm using the pcm-to-adpcm algorithm to compress the raw data into adpcm format with compression ratio of 25%, thus BLE RF data volume will be decreased largely. Master will decompress the received adpcm-format data back to pcm format.
AMIC sampling rate is 16K x 16bits, corresponding to 16K samples of raw data per second, i.e. 16 samples per millisecond (16*16bits=32bytes per ms).
For every 15.5ms, 496-byte (15.5*16=248 samples) raw data are generated. Via pcm-to-adpcm conversion with compression ratio of 1/4, the 496-byte data are compressed into 124 bytes.
The 128-byte data, including 4-byte header and 124-byte compression result, will be disassembled into five packets, and sent to Master in L2CAP layer; since the maximum length of each packet is 27 bytes, the first packet should contain 7-byte L2CAP information, including: 2-byte l2caplen, 2-byte chanid, 1-byte opcode and 2-byte AttHandle.
The figure below shows the RF data captured over-the-air. It can be seen that the first packet contains 7 bytes of additional information, followed immediately by 20 bytes of audio data. The subsequent packets contain 27 bytes of pure audio data.
The first packet carries only 20 bytes of audio data. Since the following 4 packets are fragmented packets, they do not require L2CAP header information, allowing each packet to carry 27 bytes: 20 + 27*4 = 128 bytes.

According to “Exchange MTU size” in ATT & GATT (section 3.3.3 ATT & GATT), since 128-byte long audio data packet are disassembled on Slave side, if peer device needs to re-assemble these received packets, we should determine maximum ClientRxMTU of peer device. Only when “ClientRxMTU” is 128 or above, can the 128-byte long packet of Slave be correctly processed by peer device.
Therefore, when the audio task is enabled and a 128-byte long packet needs to be sent, "blc_att_ requestMtuSizeExchange" is called to perform the MTU size exchange.
void voice_press_proc(void)
{
key_voice_press = 0;
ui_enable_mic (1);
if(ui_mtu_size_exchange_req && blc_ll_getCurrentState() == BLS_LINK_STATE_CONN){
ui_mtu_size_exchange_req = 0;
blc_att_requestMtuSizeExchange(BLS_CONN_HANDLE, 0x009e);
}
}
The recommended practice is:
(1) Turn on the MUT registration switch through blc_gap_setEventMask (GAP_EVT_MASK_ATT_EXCHANGE_MTU);
(2) Then register the GAP callback function through "blc_gap_registerHostEventHandler (gap_event_handler_t handler)";
(3) Add if(event==GAP_EVT_ATT_EXCHANGE_MTU) judgment statement in the callback function to implement the MTU size Exchange callback, and in the callback to determine whether the ClientRxMTU of the peer device is greater than or equal to 128. Since the master device's ClientRxMTU is generally larger than 128, the SDK does not determine the actual ClientRxMTU through the callback.

The second Attribute above is used to transfer audio data. This Attribute uses “Handle Value Notification” to send Data to Master. After Master receives Handle Value Notification, the Attribute Value data corresponding to the five successive packets will be assembled into 128 bytes, and then decompressed back to the pcm-format audio data.
Audio Data Compression
Related macros are defined in the application/audio/audio_config.h, as shown below:
#if (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_GATT_TLEINK)
#define ADPCM_PACKET_LEN 128
#define TL_MIC_ADPCM_UNIT_SIZE 248
#define TL_MIC_BUFFER_SIZE 992
#elif (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_GATT_GOOGLE)
#define GOOGLE_AUDIO_V0P4 1
#define GOOGLE_AUDIO_V1P0 2
#define GOOGLE_AUDIO_VERSION GOOGLE_AUDIO_V1P0
#if(GOOGLE_AUDIO_VERSION == GOOGLE_AUDIO_V1P0)
#define TL_MIC_ADPCM_UNIT_SIZE 240
#define ADPCM_PACKET_LEN 120
#define TL_MIC_BUFFER_SIZE 960
#define ADPCM_BUFFER_SIZE (GOOGLE_V1P0_ADPCM_PACKET_LEN * GOOGLE_V1P0_ADPCM_PACKET_NUM)
#define GOOGLE_AUDIO_DLE 0
#else
#define ADPCM_PACKET_LEN 136 //(128+6+2)
#define TL_MIC_ADPCM_UNIT_SIZE 256
#define TL_MIC_BUFFER_SIZE 1024
#endif
#elif (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB)
#define ADPCM_PACKET_LEN 120
#define TL_MIC_ADPCM_UNIT_SIZE 240
#define TL_MIC_BUFFER_SIZE 960
#elif (TL_AUDIO_MODE == TL_AUDIO_RCU_ADPCM_HID)
#define ADPCM_PACKET_LEN 120
#define TL_MIC_ADPCM_UNIT_SIZE 240
#define TL_MIC_BUFFER_SIZE 960
#elif (TL_AUDIO_MODE == TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB)
#define ADPCM_PACKET_LEN 20
#define MIC_SHORT_DEC_SIZE 80
#define TL_MIC_BUFFER_SIZE 320
#elif (TL_AUDIO_MODE == TL_AUDIO_RCU_SBC_HID)
#define ADPCM_PACKET_LEN 20
#define MIC_SHORT_DEC_SIZE 80
#define TL_MIC_BUFFER_SIZE 320
#elif (TL_AUDIO_MODE == TL_AUDIO_RCU_MSBC_HID)
#define ADPCM_PACKET_LEN 57
#define MIC_SHORT_DEC_SIZE 120
#define TL_MIC_BUFFER_SIZE 480
Each compression needs to process 248-sample, i.e. 496-byte data. Since AMIC continuously samples audio data and transfers the processed pcm-format data into buffer_mic, considering data buffering and preservation, this buffer should be pre-configured so that it can store 496 samples for two compressions. If 16K sampling rate is used, then 496 samples correspond to 992 bytes, i.e. “TL_MIC_BUFFER_SIZE” should be configured as 992.
The “buffer_mic” is defined as below:
s16 buffer_mic[TL_MIC_BUFFER_SIZE>>1]; //496 sample,992 bytes
config_mic_buffer ((u32)buffer_mic, TL_MIC_BUFFER_SIZE);
Following shows the mechanism of data filling into buffer_mic via HW control.
Data sampled by AMIC are transferred into memory starting from buffer_mic address with 16K speed; once the maximum length 992 is reached, data transfer returns to the buffer_mic address, the old data will be replaced directly without checking whether it’s read. It’s needed to maintain a write pointer when transferring data into RAM; the pointer is used to indicate the address in RAM for current newest audio data.
The “buffer_mic_enc” is defined to store the 128-byte compression result data; the buffer number is configured as 4 to indicate result of up to four compressions can be buffered.
int buffer_mic_enc[BUFFER_PACKET_SIZE];
Since “BUFFER_PACKET_SIZE” is 128, and “int” occupies four bytes, it’s equivalent to 128*4 signed char.

The figure above shows data compression processing method:
The buffer_mic automatically maintains a write pointer by hardware, and maintains a read pointer by software.
Whenever SW detects there are 248 samples between the two pointers, the compression handler is invoked to read 248-sample data starting from the read pointer and compress them into 128 bytes; the read pointer moves to a new location to indicate following data are new and not read. The buffer_mic is continuously checked whether there are enough 248-sample data; if so, the data are compressed and transferred into the buffer_mic_enc.
Since 248-sample data are generated for every 15.5ms, the firmware should check the buffer_mic with maximum frequency of 1/15.5ms. The FW only executes the task_audio once during each main_loop, so the main_loop duration should be less than 15.5ms to avoid audio data loss. In Conn state, the main_loop duration equals connection interval; so for applications with audio task, connection interval should be less than 15.5ms. It’s recommended to configure connection interval as 10ms.
The buffer_mic_enc maintains a write pointer and a read pointer by software: after the 248-sample data are compressed into 128 bytes, the compression result are copied into the buffer address starting from the write pointer, and the buffer_mic_enc is checked whether there’s overflow; if so, the earliest 128-byte data are discarded and the read pointer switches to the next 128 bytes.
The compression result data are copied into BLE RF Tx buffer as below:
The buffer_mic_enc is checked if it’s non-empty (when writer pointer equals read pointer, it indicates “empty”, otherwise it indicates “non-empty); if the buffer is non-empty, the 128-byte data starting from the read pointer are copied into the BLE RF Tx buffer, then the read pointer moves to the new location.
The function proc_mic_encoder is used to process Audio data compression.
Compression and Decompression Algorithm
The B85m single connection SDK provides sbc, msbc and adpcm compression and decompression algorithms, the following mainly takes adpcm to explain the entire compression and decompression algorithm. About sbc and msbc, the user can refer to the project implementation to understand.
The function below is used to invoke the adpcm compression algorithm:
void mic_to_adpcm_split (signed short *ps, int len, signed short *pds, int start);
-
"ps" points to the starting storage address for data before compression, which corresponds to the read pointer location of the buffer_mic as shown in figure above;
-
"len" is configured as “TL_MIC_ADPCM_UNIT_SIZE (248)”, which indicates 248 samples;
-
"pds" points to the starting storage address for compression result data, which corresponds to the write pointer location of the buffer_mic_enc as shown in figure above.

After compression, the data space stores 2-byte predict, 1-byte predict_idx, 1-byte length of current valid adpcm-format audio data (i.e. 124), and 124-byte data compressed from the 496-byte raw data with compression ratio of 1/4.
The function below is used to invoke the decompression algorithm:
void adpcm_to_pcm (signed short *ps, signed short *pd, int len);
-
"ps" points to the starting storage address for data to be decompressed (i.e. 128-byte adpcm-format data). This address needs user to define a buffer to store 128-byte data copied from BLE RF.
-
"pd" points to the starting storage address for 496-byte pcm-format audio data after decompression. This address needs user to define a buffer to store data to be transferred when playing audio.
-
"len" is 248, same as the "len" during compression.
As shown in figure above, during decompression, the data read from the buffer are two-byte predict, 1-byte predict_idx, 1-byte valid audio data length "124", and the 124-byte adpcm-format data which will be decompressed into 496-byte pcm-format audio data.
Audio Data Processing Flow
SDK's “ble_remote” and “ble_master_kma_dongle” contains a number of mode options, the user can select by changing the macro in app_config.h, the default is TL_AUDIO_RCU_ADPCM_GATT_TLEINK, that is, Telink custom Audio processing, its related settings are as follows.
/* Audio MODE:
* TL_AUDIO_RCU_ADPCM_GATT_TLEINK
* TL_AUDIO_RCU_ADPCM_GATT_GOOGLE
* TL_AUDIO_RCU_ADPCM_HID
* TL_AUDIO_RCU_SBC_HID
* TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB
* TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB
* TL_AUDIO_RCU_MSBC_HID
*/
#define TL_AUDIO_MODE TL_AUDIO_RCU_ADPCM_GATT_TLEINK
Since the workflows for several of these modes are similar, and the default Telink custom mode involves a relatively straightforward process of solely compressing and transmitting voice data, the overall procedure is simple.
The TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB and TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB modes are functionally similar, differing only in encoding. Therefore, this chapter will primarily explain the following three flows: TL_AUDIO_RCU_ADPCM_GATT_GOOGLE, TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB, and TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB.
To implement audio functionality in the provided SDK, users on the Slave side can refer to the ble_remote project, while users on the Master side can refer to the ble_master_kma_dongle project.
Note
- If in setting different modes, compile prompt error that XX function or variable lack of definition, this is due to the voice related lib library is not added, Users in the use of TL_AUDIO_RCU_ADPCM_GATT_GOOGLE, TL_AUDIO_RCU_MSBC_HID, TL_AUDIO_RCU_SBC_HID, respectively, need to add the corresponding library file, The code identified by encode is the encoding library on the slave side (b85m_ble_remote project), and the decoding library identified by decode is the decoding library on the master side (b85m_master_kma_dongle). The library file directory in the project is as shown in the figure below:

For example, if using SBC mode, the setting method is shown as below.

TL_AUDIO_RCU_ADPCM_GATT_GOOGLE
Audio demo refers Google Voice V0.4 Spec for implementation, the user can use this demo and google TV box for voice-related product development. Google's Service UUID is also set in accordance with the Spec provisions, as follows.

Initialization

Initialization is mainly the slave end to obtain the configuration information of the master end, the entire packet interaction information is as follows.

Voice Data Transmission

After the initialization is completed, the Slave end will send Search_KEY to the Master end, and the packet is as follows.

Then the Slave end will send Search to the Master end with the following packet.

Then the Master end will send MIC_Open to the Slave end, and the packet is as follows.

Then the Slave end sends Start to the Master end with the following packet.

According to Google Voice's Spec, the voice data transmission implemented in the program is 134 bytes per frame, and the entire packet is displayed as follows.

Note
- On the Dongle side, we do not send a close command to end the voice transmission, but use a timeout judgment to end the voice. For details, please refer to the code of Dongle implementation on Master end.
TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB
This mode uses Service for the HID service specified in the Bluetooth Spec, through which the service can achieve communication with the Dongle connected devices, provided that the Dongle and the master computer device support the HID service method of interaction.

At the beginning, the Slave sends start_request to the Master with the following packet.

After the Master receives the start_request, it sends the Ack,packet as follows.

Slave starts to send Audio voice data, the decompression and compression of voice data are operated in 480Bytes size, the voice data is first compressed to 120 bytes by ADPCM compression algorithm, then split into 6 groups of packets and sent to Master end in turn, each group packet size is 20 bytes. In order to ensure the sequence of voice packets, use every three groups of packets are changed in turn for a fixed handle value. The receiver side starts to decompress and restore the voice signal after completing 6 groups of packets. The packets are as follows.

At the end of the voice transmission, the Slave sends an End Request to the Master with the following packet.

The Master sends an Ack after receiving the End Request with the following packet.

TL_AUDIO_RCU_SBC_HID_DONGLE_TO_STB
This mode and TL_AUDIO_RCU_ADPCM_HID_DONGLE_TO_STB, the same use of Service for the HID service specified in the Bluetooth Spec, through the service can achieve the communication among the Dongle connected, the premise is that the Dongle and the master computer device support the HID service interaction.

At the beginning, the Slave sends start_request to the Master with the following packet.

After the Master receives the start_request, it sends the Ack,packet as follows.

Slave starts to send Audio voice data, voice data decompression and compression are operated in 160 bytes size, voice data is first compressed to 20 bytes by SBC compression algorithm, and then sent to Master end, each group of packet size is 20 bytes. In order to ensure the sequence of voice packets, use every three groups of packets for fixed handle value. The receiver end starts to decompress and restore the voice signal after each group of packets is completed. The packets are as follows.

At the end of the voice transmission, the Slave sends an End Request to the Master with the following packet.

The Master sends an Ack after receiving the End Request with the following packet.

OTA
In order to realize the OTA function of the B85m BLE slave, a device is required as a BLE OTA master.
The OTA master can be a Bluetooth device actually used with the slave (you need to implement OTA in the APP), or you can use Telink's BLE master kma dongle. The following uses Telink's BLE master kma dongle as the ota master to introduce OTA in detail. The related code implementation can also be found in feature_ota_big_pdu under the Multi-Connection SDK.
B85m supports booting from multiple Flash addresses. In addition to the Flash base address 0x00000, the B85 supports executing firmware from high Flash addresses 0x20000 (128KB) and 0x40000 (256KB), while the B87 supports executing firmware from high Flash addresses 0x20000 (128KB), 0x40000 (256KB), and 0x80000 (512KB). This document uses the high address 0x20000 as an example to describe OTA.
Flash Architecture and OTA Procedure
FLASH Storage Architecture
When booting address is 0x20000, size of firmware compiled by the SDK should not exceed 128kB, i.e. the flash area 0~0x20000 serves to store firmware. If you’re using boot address as 0x0 and 0x20000, the firmware size shouldn’t be larger than 124K. if your firmware size is larger than 124K, then you would need to use 0x0 and 0x40000 as boot address, the firmware size shouldn’t be larger than 252K. If more than 252K should be upgraded alternately using boot address 0 and 0x80000, the maximum firmware size should not exceed 508K.

(1) OTA Master burns new firmware2 into the Master flash area from 0x20000 to 0x40000.
(2) OTA for the first time:
-
When power on, Slave starts booting and executing firmware1 from flash 0~0x20000.
-
When firmware1 is running, the area of Slave flash starting from 0x20000 (i.e. flash 0x20000~0x40000) is cleared during initialization and will be used as storage area for new firmware.
-
OTA process starts, Master transfers firmware2 into Slave flash area from 0x20000 to 0x40000 via RF. Then slave reboot (Restart, similar to a power outage and power on again).
(3) For subsequent OTA updates, OTA Master first burns new firmware3 into the Master flash area from 0x20000 to 0x40000.
(4) OTA for the second time:
-
When power on, Slave starts booting and executing firmware2 from flash 0x20000~0x40000.
-
When firmware2 is running, the area of Slave flash starting from 0x0 (i.e. flash 0~0x20000) is cleared during initialization and will be used as storage area for new firmware.
-
OTA process starts, Master transfers firmware3 into Slave flash area 0~0x20000 via RF. Then slave reboot (Restart, similar to a power outage and power on again).
(5) Subsequent OTA process repeats steps 1)~4): 1)~2) represents OTA of the (2n+1)-th time, while 3)~4) represents OTA of the (2n+2)-th time.
OTA Update Procedure
Based on the flash storage structure introduced, the OTA update procedure is illustrated as below:
First introduce the multi-address booting mechanism (only the first two booting addresses 0x00000 and 0x20000 will be introduced here): after MCU is powered on, it boots from address 0 by default. First, read the content of flash 0x08. If the value is 0x4b, the code starting from 0 are transferred to RAM, and the following instruction fetch address equals 0 plus PC pointer value; if the value of 0x08 is not 0x4b, the MCU directly reads the value of 0x20008, if the value is 0x4b, the MCU moves the code from 0x20000 to RAM, and all subsequent fetches start from the 0x20000 address, that is, the fetch address = 0x20000+PC pointer value.
So as long as you modify the value of the 0x08 and 0x20008 flag bits, you can specify which part of the FLASH code that the MCU executes.
The power-on and OTA process of a certain SDK (2n+1 or 2n+2) is:
(1) The MCU is powered on, and the values of 0x08 and 0x20008 are read and compared with 0x4b to determine the booting address, and then boots from the corresponding address and execute the code. This function is automatically completed by the MCU hardware.
(2) During the program initialization process, read the MCU hardware register to determine which address the MCU boots from:
If boots from 0, set ota_program_offset to 0x20000, and erase all non-0xff content in the 0x20000 area to 0xff, which means that the new firmware obtained by the next OTA will be stored in the area starting at 0x20000;
If boots from 0x20000, set ota_program_offset to 0x0, and erase all the non-0xff content in the 0x0 area to 0xff, which means that the new firmware obtained by the next OTA will be stored in the area starting from 0x0.
(3) Slave MCU executes the firmware after booting; OTA Master is powered on and establishes BLE connection with Slave.
(4) Trigger OTA Master to enter OTA mode by UI (e.g. button press, write memory by PC tool, etc.). After entering OTA mode, OTA Master needs to obtain Handle value of Slave OTA Service Data Attribute (The handle value can be pre-appointed by Slave and Master, or obtained via “read_by_type”.)
(5) After the Attribute Handle value is obtained, OTA Master may need to obtain version number of current Slave Flash firmware, and compare it with the version number of local stored new firmware.
Note
- If legacy protocol is used, user needs to implement the version number; if extend protocol is used, the operation related to version number acquisition has been implemented. For the difference between legacy and extend protocol, user can refer to section 7.2.2.
(6) To enable OTA upgrade, OTA Master will send an OTA_start command to inform Slave to enter OTA mode.
(7) After the OTA_start command is received, Slave enters OTA mode and waits for OTA data to be sent from Master.
(8) Master reads the firmware stored in the flash area starting from 0x20000, and continuously sends OTA data to Slave until the entire firmware is sent.
(9) Slave receives OTA data and stores it in the area starting with ota_program_offset.
(10) After the master sends all the OTA data, check whether the data is received correctly by the slave (call the relevant function of the underlying BLE to determine whether the data of the link layer is correctly acknowledged).
(11) After the master confirms that all OTA data has been correctly received by the slave, it sends an OTA_END command.
(12) Slave receives the OTA_END command and writes the offset address of the new firmware area 0x08 (that is, ota_program_offset+0x08) as 0x4b, and writes the offset address of the old firmware storage area 0x08 as 0x00, which means it will Move code execution from the new area.
(13) Slave reports the results of OTA to master through Handle Value Notification.
(14) Reboot the slave, the new firmware takes effect.
During the whole OTA upgrade process, Slave will continuously check whether there’s packet error, packet loss or timeout (A timer is started when OTA starts). Once packet error, packet loss or timeout is detected, Slave will determine the OTA process fails. Then Slave reboots, and executes the old firmware.
The OTA related operations on Slave side described above have been realized in the SDK and can be used by user directly. On Master side, extra firmware design is needed and it will be introduced later.
Modify FW Size and Booting Address
API blc_ota_setFirmwareSizeAndBootAddress supports modification of the boot address and Firmware Size. Herein booting address means the address except 0 to store New_firmware, so it should be one of 0x20000, 0x40000 or 0x80000 (Only B87 supports 0x80000).
| Firmware_Boot_address | Firmware size (max)/K |
|---|---|
| 0x20000 | 124 |
| 0x40000 | 252 |
| 0x80000 | 508 |
The default maximum firmware size in the SDK is 124KB (due to some special reasons, the firmware size of the startup address 0x20000 should not be greater than 124KB), and the corresponding startup addresses are 0x00000 and 0x20000. These two values are consistent with the previous description. The user can call the API blc_ota_setFirmwareSizeAndBootAddress to set the maximum firmware size and boot address:
ble_sts_t blc_ota_setFirmwareSizeAndBootAddress(int firmware_size_k, multi_boot_addr_e boot_addr);
The parameter firmware_size_k indicates the maximum firmware size in KBytes, which should be 4K-aligned.
For 825x series chips, the parameter boot_addr indicates the available boot addresses, of which there are two options (Note: The 825x does not support the 512KB boot address):
typedef enum{
MULTI_BOOT_ADDR_0x20000 = 0x20000, //128 K
MULTI_BOOT_ADDR_0x40000 = 0x40000, //256 K
}multi_boot_addr_e;
For 827x series chips, the parameter boot_addr indicates the available boot addresses, of which there are three options:
typedef enum{
MULTI_BOOT_ADDR_0x20000 = 0x20000, //128 K
MULTI_BOOT_ADDR_0x40000 = 0x40000, //256 K
MULTI_BOOT_ADDR_0x80000 = 0x80000, //512 K
}multi_boot_addr_e;
This API can only be called before cpu_wakeup_init in main function, otherwise it is invalid. The reason is that the cpu_wakeup_init function needs to do some settings according to the values of firmware_size and boot_addr.
The API blc_ota_getCurrentUsedMultipleBootAddress is used to retrieve the currently configured boot address.
RF Data Processing for OTA Mode
OTA Processing in Attribute Table
OTA related contents needs to be added in the Attribute Table on slave end. The “att_readwrite_callback_t r” and “att_readwrite_callback_t w” of the OTA data Attribute should be set as otaRead and otaWrite, respectively; the attribute should be set as Read and Write_without_Rsp (Telink Master KMA Dongle sends data via “Write Command” by default, with no need of ack from Slave to enable faster speed).
// OTA attribute values
static const u8 my_OtaCharVal[19] = {
CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP | CHAR_PROP_NOTIFY,
U16_LO(OTA_CMD_OUT_DP_H), U16_HI(OTA_CMD_OUT_DP_H),
TELINK_SPP_DATA_OTA, };
{4,ATT_PERMISSIONS_READ, 2,16,(u8*)(&my_primaryServiceUUID), (u8*)(&my_OtaServiceUUID), 0},
{0,ATT_PERMISSIONS_READ, 2, sizeof(my_OtaCharVal),(u8*)(&my_characterUUID), (u8*)(my_OtaCharVal), 0},
{0,ATT_PERMISSIONS_RDWR,16,sizeof(my_OtaData),(u8*)(&my_OtaUUID), (&my_OtaData), &otaWrite, NULL},
{0,ATT_PERMISSIONS_RDWR,2,sizeof(otaDataCCC),(u8*)(&clientCharacterCfgUUID), (u8*)(otaDataCCC), 0},
{0,ATT_PERMISSIONS_READ, 2,sizeof (my_OtaName),(u8*)(&userdesc_UUID), (u8*)(my_OtaName), 0},
When Master sends OTA data to Slave, it actually writes data to the second Attribute as shown above, so Master needs to know the Attribute Handle of this Attribute in the Attribute Table. To use the Attribute Handle value pre-appointed by Master and Slave, user can directly define it on Master side.
OTA Protocol
For the OTA Server, the following API needs to be called during the initialization phase to initialize the OTA Server module.
void blc_ota_initOtaServer_module(void);
The current OTA architecture extends the functionality and is compatible with previous versions of the protocol. The entire OTA protocol consists of two parts: the Legacy protocol and the Extend protocol.
| OTA Protocol | - |
|---|---|
| Legacy protocol | Extend protocol |
Note
- Functions supported by OTA protocol are:
- OTA Result feedback function: this function is not optional, added by default.
- Firmware Version Compare function and Big PDU function: This function is optional and can not be added, it should be noted that the version number comparison function is different in Legacy protocol and Extend protocol, please refer to the following OTA_CMD section for details.
The following introductions are all focused on Legacy and Extend protocols.
OTA_CMD composition
The PDUs of OTA's CMD are as follows.
| OTA Command Payload | - |
|---|---|
| Opcode (2 octet) | Cmd_data (0-18 octet) |
Opcode
| Opcode | Name | Use* |
|---|---|---|
| 0xFF00 | CMD_OTA_VERSION | Legacy |
| 0xFF01 | CMD_OTA_START | Legacy |
| 0xFF02 | CMD_OTA_END | All |
| 0xFF03 | CMD_OTA_START_EXT | Extend |
| 0xFF04 | CMD_OTA_FW_VERSION_REQ | Extend |
| 0xFF05 | CMD_OTA_FW_VERSION_RSP | Extend |
| 0xFF06 | CMD_OTA_RESULT | All |
Note
- Use:To identify the command use in Legacy protocol、Extend protocol or both of all
- Legacy: Only use in the Legacy protocol
- Extend: Only use in the Extend protocol
- All: use both in the Legacy protocol and Extend protocol
(1) CMD_OTA_VERSION
It is a command to get the current firmware version number of the slave, and the user can choose to use it if he adopts OTA Legacy protocol for OTA upgrade. It is Optional. This command can be used to pass the firmware version number through the callback function reserved on the slave end.
void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);
The server side will trigger this callback function when it receives the CMD_OTA_VERSION command.
(2) CMD_OTA_START
This command is the OTA update start command. The master sends this command to the slave to officially start the OTA update. This command is only for Legacy Protocol, if user uses OTA Legacy protocol, this command should be used.
(3) CMD_OTA_END
This command is the end command, which is used by both legacy and extend protocol in OTA. When Master confirms all OTA data are correctly received by Slave, it will send this command, which can be followed by four valid bytes to re-confirm Slave has received all data from Master.
| - | CMD_data | - |
|---|---|---|
| Adr_index_max (2 octets) | Adr_index_max_xor (2 octets) | Reserved (16 octets) |
-
Adr_index_max: the maximum adr_index value
-
Adr_index_max_xor: the anomaly value of Adr_index_max for verification
-
Reserved: Reserved for future function extension
(4) CMD_OTA_START_EXT
This command is the OTA update start command in the extend protocol. master sends this command to slave to officially start the OTA update. User should use this command as the start command if using OTA extend protocol.
| - | CMD_data | - |
|---|---|---|
| Length (1 octets) | Version_compare (1 octets) | Reserved (16 octets) |
-
Length: PDU length
-
Version_compare: 0x01: enable version compare 0x00: disable version compare
-
Reserved: Reserved for future extension
(5) CMD_OTA_FW_VERSION_REQ
This command is the version comparison request command in the OTA upgrade process. This command is initiated by client to Server side to request for version number and upgrade permission.
| - | CMD_data | - |
|---|---|---|
| version_num (2 octets) | version_compare (1 octets) | Reserved (16 octets) |
-
Version num: the firmware version number to be upgraded on the client side
-
Version compare: 0x01: Enable version compare 0x00: Disable version compare
-
Reserved: Reserved for future extensions
(6) CMD_OTA_FW_VERSION_RSP
This command is a version response command, the server side will compare the existing firmware version number with the version number requested by the client side after receiving the version comparison request command (CMD_OTA_FW_VERSION_REQ) from the client side to determine whether to upgrade, and the related information will be sent back to the client via this command.
| - | CMD_data | - |
|---|---|---|
| version_num (2 octets) | version_accept (1 octets) | Reserved (16 octets) |
-
Version num: the firmware version number that Server side is currently running
-
Version_accept: 0x01: accept client side upgrade request, 0x00: reject client side upgrade request
-
Reserved: Reserved for future extensions
It should be noted that the existing firmware version number can be set using the following API.
void blc_ota_setFirmwareVersionNumber(u16 version_number);
(7) CMD_OTA_RESULT
This command is the OTA result return command, the slave will send the result information to the master after the OTA is finished. In the whole OTA process, no matter success or failure, the OTA_result will only be reported once, the user can judge whether the upgrade is successful according to the returned result.
| CMD_data | - |
|---|---|
| Result (1 octets) | Reserved (16 octets) |
Result: OTA result information, all possible return results are shown in the following table.
| Value | Type | info |
|---|---|---|
| 0x00 | OTA_SUCCESS success | |
| 0x01 SEQ_ER | OTA_DATA_PACKET_ OTA data pac R | ket sequence number error: repeated OTA PDU or lost some OTA PDU |
| 0x02 | OTA_PACKET_INVALID invalid | OTA packet: 1. invalid OTA command; 2. addr_index out of range; 3.not standard OTA PDU length |
| 0x03 | OTA_DATA_CRC_ERR packet | PDU CRC err |
| 0x04 | OTA_WRITE_FLASH_ERR write O | TA data to flash ERR |
| 0x05 | OTA_DATA_INCOMPLETE lost la | st one or more OTA PDU |
| 0x06 | OTA_FLOW_ERR peer de | vice send OTA command or OTA data not in correct flow |
| 0x07 | OTA_FW_CHECK_ERR firmwar | e CRC check error |
| 0x08 COMPAR | OTA_VERSION_ the version num E_ERR | ber to be update is lower than the current version |
| 0x09 | OTA_PDU_LEN_ERR PDU len | gth error: not 16*n, or not equal to the value it declare in "CMD_OTA_START_EXT" packet |
| 0x0a MARK_E | OTA_FIRMWARE_ firmware mark RR | error: not generated by Telink's BLE SDK |
| 0x0b | OTA_FW_SIZE_ERR firmwar | e size error: no firmware_size; firmware size too small or too big |
| 0x0c TIMEOU | OTA_DATA_PACKET_ time interva T | l between two consequent packet exceed a value(user can adjust this value) |
| 0x0d | OTA_TIMEOUT OTA flo | w total timeout |
| 0x0e CONNEC | OTA_FAIL_DUE_TO_ OTA fail due to TION _TERMINATE | current connection terminate(maybe connection timeout or local/peer device terminate connection) |
| 0x0f | OTA_MCU_NOT_SUPPORTED MCU does not | support this OTA mode |
| 0x10 | OTA_LOGIC_ERR Software logic err | or |
| 0x0f-0xff | Reserved for future use / |
Reserved: Reserved for future extensions
OTA Packet structure composition
When the Master sends commands and data to the Slave using WriteCommand or WriteResponse, the value of the Attribute Handle of the ATT layer is the handle_value of the OTA data on the slave side. According to the specification of the Ble Spec L2CAP layer regarding the PDU format, Attribute Value length is defined as the OTA_DataSize part in the following figure.

-
DLE Size: CID + Opcode + Att_Handle + Adr_index + OTA_PDU + CRC
-
MTU_Size: Opcode + Att_Handle + Adr_index + OTA_PDU +CRC
-
OTA_Data_Size: Adr_index + OTA_PDU + CRC
OTA_Data introduction
| Type | Length |
|---|---|
| Default + BigPDU | 16octets -240octets(n*16,n=1..15) |
Note
-
Default: OTA PDU length fixed default size is 16 octets
-
BigPDU: OTA PDU length can be changed in the range of 16octets - 240 octets, and is an integer multiple of 16 bytes. It requires special mention that due to special limitations of the B85 chip, the maximum supported OTA PDU length is 80 octets. If it exceeds 80 octets, an
OTA_MCU_NOT_SUPPORTEDerror is reported.
OTA_PDU Format
When user adopts Extend protocol in OTA and supports Big PDU, it can support long packet for OTA upgrade operation and reduce the time of OTA upgrade. User can customize the PDU size at the client side according to the need. The last two bytes are a CRC_16 calculation of the previous Adr_Index and Data to get the first CRC value, the slave will do the same CRC calculation after receiving the OTA data, and only when the CRC calculated by both matches, it will be considered a valid data.
| - | OTA PDU | - |
|---|---|---|
| Adr_Index (2 octets) | Data(n*16 octets) n=1..15 | CRC (2 octets) |
(1) PDU packet length: n=1
Data : 16 octets
Mapping of Adr_Index to Firmware address.
| Adr_Index | Firmware_address |
|---|---|
| 0x0001 | 0x0000 - 0x000F |
| 0x0002 | 0x0010 - 0x001F |
| ……. | …… |
| XXXX | (XXXX -1)*16 - (XXXX)*16+15 |
(2) PDU packet length: n=2
Data : 32 octets
Mapping of Adr_Index to Firmware address.
| Adr_Index | Firmware_address |
|---|---|
| 0x0001 | 0x0000 - 0x001F |
| 0x0002 | 0x0020 - 0x003F |
| ……. | …… |
| XXXX | (XXXX -1)*32 - (XXXX)*32+31 |
(3) PDU packet length: n=15
Data : 240 octets
Mapping of Adr_Index to Firmware address.
| Adr_Index | Firmware_address |
|---|---|
| 0x0001 | 0x0000 - 0x00EF |
| 0x0002 | 0x0010 - 0x01DF |
| ……. | …… |
| XXXX | (XXXX -1)*240 - (XXXX)*240+239 |
Note
- In the OTA upgrade process, each packet of PDU length sent needs to be aligned with 16 bytes, that is, when the valid OTA data in the last packet is less than 16 bytes, the 0xFF data is added to make up the alignment, as listed below.
a) the current PDU length is set to 32, the last packet of valid data PDU is 4octets, then you need to add 12octets of 0xFF for alignment

b) the current PDU length is set to 48, the last packet of valid data PDUs is 20octets, then you need to add 12octets of 0xFF for alignment

c) the current PDU length is set to 80, the last packet of valid data PDUs is 52octets, then you need to add 12octets of 0xFF for alignment

- For the packet capture records corresponding to different PDU sizes, users can contact Telink technical support to obtain.
RF Transfer Processing on Master Side
The master end sends commands and data to the slave via Write Command or Write Request in the L2CAP layer, and Spec specifies that it should return Write Response after receiving Write Request. For the introduction of ATT layer about Write Command and Write Request, please refer to Ble Spec or section 3.3.3.2 for its specific composition user. Telink Ble master Dongle uses Write Command to send data and commands by default, in this way, during OTA data transfer, Master won’t check whether each OTA data is acknowledged. In other words, after sending an OTA data via write command, Master won’t check if there’s ack response from Slave by software, but will directly push the following data into hardware TX buffer which yet does not have enough data to be sent.
The following will introduce the process of Legacy Protocol and Extend Protocol, and Version Compare of OTA, respectively, to explain the interaction process of Salve and Master in the whole RF Transform. The Server side shown below is the Slave side, and the Client side is the Master side, which will not be distinguished later.
OTA Legacy Protocol Process
OTA Legacy is compatible with the previous version of Telink's OTA protocol. To better explain the whole interaction process between Slave and Master, the following example is used to illustrate.
Note
- The default PDU length of 16 octets is used, which does not involve the operation of DLE long packets.
- Firmware compare function is not selected.
The specific operation flow is shown in the following figure.

Client side will first send CMD_OTA_START command to Server side, Server side will start to prepare to receive OTA data after receiving the command, then Client side will start to send OTA_Data. If there is any interaction failure during the process, Server side will send CMD_OTA_Result to Client side, that is, return an error message and re-run the original program but will not enter reboot, the client side will stop the OTA data transfer when receiving this message. If the Client side and Server side successfully complete the OTA_Data transfer, the Client side will send CMD_OTA_END to the Server side, and the Server side will send CMD_OTA_Result to the Client side after receiving the result information, and enter reboot and run the new firmware.
OTA Extend Protocol Process
As mentioned above, there are some differences between the interaction commands of OTA Extend and Legacy introduced above. To better illustrate the whole interaction process between Slave and Master, the following example is used.
Note
- PDU length adopts 64 octets size, which involves the operation of DLE long packets.
- Firmware compare function is not selected.

Due to the DLE long packet function, the Client side first needs to interact with the Server side for MTU and DLE, then the next process is similar to the previous Legacy. The Client side sends CMD_OTA_START_EXT command to the Server side, the Server side starts to prepare to receive OTA data after receiving the command, then client side starts sending OTA_Data. If there is any interaction failure during the process, the Server side will send CMD_OTA_Result to the Client side, which returns the error message and re-runs the original program but will not enter reboot. If the Client side and Server side successfully complete the OTA_Data transfer, the Client side will send CMD_OTA_END to the Server side, and the Server side will send CMD_OTA_Result to the Client side after receiving the result information, and enter reboot and run the new firmware.
OTA Version Compare Process
In the Slave side, both Extend and Legacy Protocol have version comparison function, where Legacy reserved the interface, need to be implemented by the user, while Extend has implemented the version comparison function, the user can directly use, as follows, need to enable the following macro.
#define OTA_FW_VERSION_EXCHANGE_ENABLE 1 //user can change
#define OTA_FW_VERSION_COMPARE_ENABLE 1 //user can change
The following is an example of the interaction flow in Extend with version comparison.
Note
- PDU length is 16 octets size, no operation of DLE long packet is involved.
- Firmware compare function selection (OTA to be upgraded version number is 0x0001, enable version compare enable)

After enabling the version comparison function, the Client side first sends the CMD_OTA_FW_VERSION_REQ version comparison request command to the Server side, where the PDU sent includes the Firmware version number of the Client side (new_fw_version = 0x0001), and the Server side gets the version number information of the Client side and compares it with the local version number (local_version).
If the received version number (new_fw_version = 0x0001) is not greater than the local version number (local version = 0x0001), the Server side will reject the Client side OTA upgrade request and send the Client side version response command (CMD_OTA_FW_VERSION_RSP). The information sent includes the receiving parameter (accept = 0) and the local version number (local_version = 0x0001), and the Client will stop the OTA related operation after receiving it, that is, the current version upgrade is not successful.
If the received version number (new_fw_version = 0x0001) is larger than the local version number (local version = 0x0000), the Server side will receive the OTA upgrade request from the Client side and send the version response command (CMD_OTA_FW_VERSION_RSP) to the Client side. The message sent includes the acceptance parameter (accept = 1) and the local version number (local_version = 0x0000), which the Client receives to start preparing the OTA upgrade related operations.The process is similar to the previous content, that is, first send the CMD_OTA_START command to the Server side, and then the Server side starts to prepare to receive the OTA data after receiving the command, client side starts sending OTA_data. If there is any interaction failure during the process, the Server side will send CMD_OTA_Result to the Client side, which will return the error message and re-run the original program but will not enter reboot, and the Client side will stop OTA data transmission immediately after receiving it. If the Client side and Server side successfully complete the OTA_Data transfer, the Client side will send CMD_OTA_END to the Server side, and the Server side will send CMD_OTA_Result to the Client side after receiving the result information, and enter reboot and run the new firmware.
OTA Implementation
The above describes the entire OTA interaction process, the following example illustrates the specific data interaction between Master and Slave.
Note
- OTA Protocol: Legacy Protocol
- The PDU length is 16 octets, which does not involve the operation of long DLE packets.
- The Master side enables Firmware compare function.
(1) Check if there’s any behavior to trigger entering OTA mode. If so, Master enters OTA mode.
(2) To send OTA commands and data to Slave, Master needs to know the Attribute Handle value of current OTA data Attribute on Slave side. User can decide to directly use the pre-appointed value or obtain the Handle value via “Read By Type Request”.
UUID of OTA data in Telink BLE SDK is always 16-byte value as shown below:
#define TELINK_SPP_DATA_OTA {0x12,0x2B,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00}
In “Read By Type Request” from Master, the “Type” is set as the 16-byte UUID. The Attribute Handle for the OTA UUID is available from “Read By Type Rsp” responded by Slave. In the figure below, the Attribute Handle value is shown as “0x0031”.

(3) Obtain the current firmware version number of the slave and decide whether to continue the OTA update (if the version is already the latest, no update is required). This step is for the user to choose whether to do it or not. The BLE SDK does not provide a specific version number acquisition method, users can play by themselves. In the current BLE SDK, legacy protocol does not implement version number transmission. The user can use write cmd or write response to send a request to obtain the OTA version to the slave through the OTA version cmd, but the slave side only provides a callback function when receiving the OTA version request, and the user finds a way to set the slave side in the callback function. The version number is sent to the master (such as manually sending a NOTIFY/INDICATE data).
(4) Start a timing at the beginning of the OTA, and then continue to check whether the timing exceeds 30 seconds (this is only a reference time, and the actual evaluation will be made after the normal OTA required by the user test).
If it takes more than 30 seconds to consider the OTA timeout failure, because the slave side will check the CRC after receiving the OTA data. Once the CRC error or other errors (such as programming flash errors) occur, the OTA will be considered as a failure and the program will be restarted directly. The layer cannot ack the master, and the data on the master side has not been sent out, resulting in a timeout.
(5) Read the four bytes of Master flash 0x20018~0x2001b to determine the size of the firmware.
This size is implemented by our compiler. Assuming the size of the firmware is 20k = 0x5000, then the value of 0x18~0x1b of the firmware is 0x00005000, so the size of the firmware can be read from 0x20018~0x2001b.
In the bin file shown in the figure below, the content of 0x18 ~ 0x1b is 0x0000cf94, so the size is 0xcf94 = 53140Bytes, from 0x0000 to 0xcf96.


(6) Master sends an OTA start command “0xff01” to Slave, so as to inform it to enter OTA mode and wait for OTA data from Master, as shown below.

(7) Read 16 bytes of firmware each time starting from Master flash 0x20000, assemble them into OTA data packet, set corresponding adr_index, calculate CRC value, and push the packet into TX FIFO, until all data of the firmware are sent to Slave.
The data sending method is described above, using the OTA data format: 20-byte valid data contains 2-byte adr_index, 16-byte firmware data and 2-byte CRC value to the former 18 bytes.
Note: If firmware data for the final transfer are less than 16 bytes, the remaining bytes should be complemented with “0xff” and need to be considered for CRC calculation.
Below illustrates how to assemble OTA data.
Data for first transfer: “adr_index” is “0x00 00”, 16-byte data are values of addresses 0x0000 ~ 0x000f. Suppose CRC calculation result for the former 18 bytes is “0xXYZW”, the 20-byte data should be:
0x00 0x00 0xf3 0x22 .... (12 bytes not listed)..... 0x60 0x15 0xZW 0xXY
Data for second transfer:
0x01 0x00 0x21 0xa8 ....(12 bytes not listed)..... 0x00 0x00 0xJK 0xHI
Data for third transfer:
0x02 0x00 0x4b 0x4e ....(12 bytes not listed)..... 0x81 0x7d 0xNO 0xLM
........
Data for penultimate transfer:
0xf8 0x0c 0x20 0xa1 ....(12 bytes not listed)..... 0xff 0xff 0xST 0xPQ
Data for final transfer:
0xf9 0x0c 0xec 0x6e 0xdd 0xa9 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xWX 0xUV
12 “0xff” are added to complement 16 bytes.
0xec 0x6e 0xdd 0xa9 is the third to sixth, which is the CRC_32 calculation result of the entire firmware bin. The slave will synchronously calculate the CRC_32 check value of the entire bin received during the OTA upgrade process, and compare it with 0xec 0x6e 0xdd 0xa9 at the end.
The CRC calculation result for a total of 18 bytes from 0xf9 to 0xff is 0xUVWX.
The above data is shown in the figure below:


(8) After the firmware data is sent, check whether the data of the BLE link layer has been completely sent (because only when the data of the link layer is acked by the slave, the data is considered to be sent successfully). If it is completely sent, the master sends an ota_end command to notify the slave that all data has been sent.
The packet effective bytes of the OTA end are set to 6, the first two are 0xff02, and the middle two bytes are the maximum adr_index value of the new firmware (this is for the slave to confirm again that the last or several OTA data is not lost) , The last two bytes are the inverse of the largest adr_index value in the middle, which is equivalent to a simple check. OTA end does not require CRC check.
Take the bin shown in the above figure as an example, the largest adr_index is 0x0cf9, and its inverse value is 0xf306, and the final OTA end package is shown in the figure above.
(9) Check if link-layer TX FIFO on Master side is empty: If it’s empty, it indicates all data and commands in above steps are sent successfully, i.e. OTA task on Master succeeds.
Please refer to Appendix for CRC_16 calculation function.
As introduced above, Slave can directly invoke the otaWrite and otaRead in OTA Attribute. After Slave receives write command from Master, it will be parsed and processed automatically in BLE stack by invoking the otaWrite function.
In the otaWrite function, the 20-byte packet data will be parsed: first judge whether it’s OTA CMD or OTA data, then process correspondingly (respond to OTA cmd; check CRC to OTA data and burn data into specific addresses of flash).
The OTA related operations on Slave side are shown as below:
(1) OTA_FIRMWARE_VERSION command is received: Master requests to obtain Slave firmware version number.
In this BLE SDK, after Slave receives this command, it will only check whether related callback function is registered and determine whether to trigger the callback function correspondingly.
The interface in ble_ll_ota.h to register this callback function is shown as below:
typedef void (*ota_versionCb_t)(void);
void blc_ota_registerOtaFirmwareVersionReqCb(ota_versionCb_t cb);
(2) OTA start command is received: Slave enters OTA mode.
If the “bls_ota_registerStartCmdCb” function is used to register the callback function of OTA start, then the callback function is executed to modify some parameter states after entering OTA mode (e.g. disable PM to stabilize OTA data transfer).
And the slave also starts and maintains a slave_adr_index to record the adr_index of the latest correct OTA data. The slave_adr_index is used to check whether there’s packet loss in the whole OTA process, and its initial value is -1. Once packet loss is detected, OTA fails, Slave MCU exits OTA and reboots; since Master cannot receive any ack from Slave, it will discover OTA failure by software after timeout.
The following interface is used to register the callback function of OTA start:
typedef void (*ota_startCb_t)(void);
void blc_ota_registerOtaStartCmdCb(ota_startCb_t cb);
User needs to register this callback function to carry out operations when OTA starts, for example, configure LED blinking to indicate OTA process.
After Slave receives “OTA start”, it enters OTA and starts a timer (The timeout duration is set as 30s by default in current SDK). If OTA process is not finished within 30s, it’s regarded as OTA failure due to timeout. User can evaluate firmware size (larger size takes more time) and BLE data bandwidth on Master (narrow bandwidth will influence OTA speed), and modify this timeout duration accordingly via the variable as shown below.
blotaSvr.process_timeout_us = 30 * 1000000; //default 30s
blotaSvr.packet_timeout_us = 5 * 1000000; //default 5s
The user can use the following API to modify the timeout for the completion of the entire OTA process, ranging from 5s to 1000s.
ble_sts_t blc_ota_setOtaProcessTimeout(int timeout_second);
After initializing this variable, the user can call the following function to handle timeout judgment.
void blt_ota_procTimeout(void);
The other is the timeout period of the receive packet. It will be updated every time an OTA data packet is received. The timeout period is 5s, that is, if the next data is not received within 5s, the OTA_RF_PACKET_TIMEOUT is considered as a failure. The user can use the following API to modify this timeout, ranging from 1s to 20s.
ble_sts_t blc_ota_setOtaDataPacketTimeout(int timeout_second);
(3) Valid OTA data are received (first two bytes are 0~0x1000):
Whenever Slave receives one 20-byte OTA data packet, it will first check if the adr_index equals slave_adr_index plus 1. If not equal, it indicates packet loss and OTA failure; if equal, the slave_adr_index value is updated.
Then carry out CRC_16 check to the former 18 bytes. If not matched, OTA fails; if matched, the 16-byte valid data are written into corresponding flash area (ota_program_offset+adr_index16 ~ ota_program_offset+adr_index16 + 15). During flash writing process, if there’s any error, OTA also fails.
In order to ensure the integrity of the firmware after the OTA is completed, a CRC_32 check will be performed on the entire firmware at the end, and it will be compared with the check value calculated by the same method sent by the master. If it is not equal, it means there is a data error in the middle, and the OTA is considered a failure.
(4) “OTA end” command is received:
Check whether adr_max in OTA end packet and the inverted check value are correct. If yes, the adr_max can be used to double check whether maximum index value of data received by Slave from Master equals the adr_max in this packet. If equal, OTA succeeds; if not equal, OTA fails due to packet loss.
After successful OTA, Slave will set the booting flag of the old firmware address in flash as 0, set the booting flag of the new firmware address in flash as 0x4b, then reboot MCU.
(5) The slave sends the OTA result back to the master:
Once the OTA is started on the slave side, regardless of whether the OTA succeeds or fails, the slave will finally send the result to the master. The following is an example of the result information sent by the slave after the OTA is successful (the length is only 3 bytes):

(6) Slave supplies OTA state callback function:
After Slave starts OTA, MCU will finally reboot when OTA is successful.
If OTA succeeds, Slave will set flag before rebooting so that MCU executes the New_firmware.
If OTA fails, the incorrect new firmware will be erased before rebooting, so that MCU still executes the Old_firmware.
Before rebooting, user can judge whether the OTA state callback function is registered and determine whether to trigger it correspondingly.
The corresponding codes are as following:
typedef void (*ota_resIndicateCb_t)(int result);
void blc_ota_registerOtaResultIndicationCb (ota_resIndicateCb_t cb);
After the callback function is set, the enum of the parameter result of the callback function is the same as the result reported by the OTA. The first 0 is OTA success, and the rest are different reasons for failure.
The OTA upgrade success or failure will trigger the callback function, the actual code can be debugged by the result of the function to return parameters. When the OTA is unsuccessful, you can read the above result and stop the MCU with while(1) to understand what causes the OTA failure.
OTA Security
OTA Service Data Security
OTA Service is a kind of GATT service, and the problem of OTA service security protection is the problem of BLE GATT service data security protection, that is, data cannot be accessed illegally. According to the design of BLE Spec, user can used the following methods:
(1) To enable SMP, it is recommended to use the Security Level as high as possible to achieve the function that only legally paired devices have access to OTA server data. Refer to the introduction of SMP in this document.
For example, using Security Mode 1 Level 3, the pairing of Authentication and MITM can effectively control the product slave device and the specific master to pair encryption success and back connection, the attacker cannot successfully encrypt with the slave device. Add the corresponding security level settings to the read and write of the protected GATT service data, and the attacker will not be able to access these data. If you use Mode 1 Level 4, Secure Connection + Authentication, the security level is even higher.
The codes that may be involved include the following:
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) Use whitelist. Users can use the whitelist to connect only to the master device they want to connect to, or they can effectively intercept the attacker's connection.
(3) Use address privacy protection, local device and peer device use resolvable private address (RPA), which effectively hides the identity address of the other party or us and maks the connection more secure.
OTA RF Transmission Data Integrity
Since RF is an unstable transmission, a certain protection mechanism is needed to ensure the integrity and correctness of the firmware during the OTA process.
Refer to the previous introduction, the OTA master needs to divide the Firmware into multiple data packets according to a certain size in advance. The first 2 bytes of each data packet is the packet sequence number, starting from 0 and increasing by 1.
LinkLayer Data Transfer Mechanism
BLE Spec has been made corresponding design in terms of data transmission integrity:
(a) When the LinkLayer sender is transmitting a piece of data, it needs to see the other party's response before switching to the next packet of data transmission to ensure that the data transmission will not be lost;
(b) The LinkLayer receiver needs to check the packet sequence number of each piece of data, and the repeated data will be discarded to ensure that the data will not be received repeatedly;
(c) For each packet of data, the sender adds a CRC24 check value at the end, and the receiver recalculates the check value and compares it to eliminate the data with RF transmission errors.
Telink BLE SDK has passed the official Sig BQB certification and is implemented in full accordance with the above design.
These design mechanisms of LinkLayer can prevent data errors caused by RF transmission.
OTA PDU CRC16 Check
Refer to the previous introduction, on the basis of LinkLayer data protection, add a CRC16 checksum to the OTA protocol to make data transmission more secure.
OTA PDU Serial Number Check
The OTA master splits the Firmware into several OTA PDUs, each PDU has its own package serial number.
For the convenience of explanation, assume the Firmware size is 50K, split according to OTA PDU 16Byte, the number of PDUs is 50*1024/16=3200, then the serial number is 0 ~ 3199, i.e. 0x0 ~ 0xC7F.
After the OTA starts, set the expected serial number to 0. For each OTA data received, use the expected serial number and the actual serial number to compare, only when the two are equal, the process is considered correct and the expected serial number is updated +1. If the two are not equal, the process is considered failed and the OTA is ended. This design can ensure the continuity and uniqueness of OTA PDUs.
At the end of OTA, you can read the serial number 0xC7F of the last OTA PDU of Firmware on the OTA_END packet, and use this serial number to compare with the actual maximum serial number received, you can determine whether the OTA PDU loss a number. If the actual received maximum serial number is 0xC7E, it means the master missed the last packet, and the OTA will fail at this time.
The combination of the above designs can ensure that the OTA master splits the firmware correctly, and each OTA PDU is effectively sent out.
Firmware CRC32 Check
There is a subjectively incorrect operation of the OTA master, which may cause the BLE product to be down. After generating a correct binary file with Telink compiler tool, the binary file may be accidentally modified due to operation error, for example, the content of a byte is tampered with. When this wrong binary file is taken to the OTA master for OTA upgrade, the master cannot know the error and will be used as the correct firmware to upgrade the slave, causing the program on the slave side to not run correctly.
To solve the above problems, Firmware CRC32 checksum is added in SDK. In the last step of compiling and generating the binary file, CRC32 calculation is performed on the binary file and the result is spliced in. During the OTA upgrade process, the server performs CRC32 calculation while receiving data. The OTA_END link uses the calculated value to compare with the value on the last OTA PDU. Only when the two are equal can the Firmware be considered to have not been tampered with.
OTA Abnormal Power Failure Protection
The Telink OTA design can ensure that the device is powered off at any time without the risk of device downtime.
Refer to the previous introduction in this chapter, the MCU uses a multi-address boot mechanism, and uses a byte marker for the MCU to determine which address the firmware starts from when the MCU is powered on. For the convenience of explanation, assume that the current firmware of the device is stored in the Flash 0x0 ~ 0x20000 range, where the value at address 0x0008 marks the Firmware as valid, which is 0x4B at this time; the new Firmware will be stored in the 0x20000 ~ 0x40000 range, where the value at address 0x20008 is used to mark the validity of the new Firmware, and the initial value is 0xFF.
After the OTA upgrade starts, the value on the 0x08 address of the first OTA PDU received is 0x4B, but this PDU will be intentionally written to 0x20008 as 0xFF when it is written to Flash, which is not effective. When all OTA processes are correct and all Flash writes are correct, the value of address 0x20008 will be written as 0x4B. Any power failure occurs at any time before this, it does not affect the Firmware of 0x0 ~ 0x20000, even if the power failure causes some Flash writes on 0x20000 ~ 0x40000 to fail, after re-powering, the Firmware of 0x0 ~ 0x20000 can be written to 0x40000. The Firmware of 0x20000 can be operated after re-powering.
In the last step, after confirming that the firmware on 0x20000 ~ 0x40000 is processed correctly, write 0x4B corresponding to the address 0x0008 in 0x0 ~ 0x20000 to 0x00, which means that the firmware on this area is invalid. We can write address 0x0008 to 0x00 as an atomic operation, no matter at which moment the power is turned off, this operation either succeeds (the value is changed to 0x00 or mistakenly written as a value other than 0x4B) or fails (i.e. 0x4B remains unchanged). If the operation succeeds, the next time the power is turned on, it will run the Firmware on 0x20000 ~ 0x40000; if it fails, it will run the Firmware on 0x0 ~ 0x20000.
Flash
Flash address allocation
The basic unit of flash storage information is the size of a sector (4K byte), because the flash erase is based on the sector (the erase function is flash_erase_sector). Theoretically the same kind of information needs to be stored in one sector, and different kinds of information need to be stored in different sectors (to prevent other types of information from being erased by mistake when erasing information). Therefore, it is recommended that users follow the principle of "different types of information in different sectors" when using flash to store customized information. The default location of system related information (Customer Value, MAC Address, Pair&Sec Info) will be adaptively shifted to a later position of the flash according to the actual size of the flash.
The following figure shows the address allocation of various information in 512K/1M flash. Take the default OTA Firmware maximum size not exceeding 128K as an example to illustrate, if the user modifies the OTA Firmware size.

As shown in the figure above, all address assignments provide users with corresponding modification interfaces, and users can plan address assignments according to their needs. The following introduces the default address allocation method and the corresponding interface to modify the address. The API blc_flash_read_mid_get_vendor_set_capacity() is used to retrieve the MID, vendor information, and capacity of the flash. The API blc_readFlashSize_autoConfigCustomFlashSector() automatically configures storage addresses for various information based on the flash size.
void blc_readFlashSize_autoConfigCustomFlashSector(void)
{
blc_flash_read_mid_get_vendor_set_capacity();
#if (FLASH_ZB25WD40B_SUPPORT_EN || FLASH_GD25LD40C_SUPPORT_EN || FLASH_GD25LD40E_SUPPORT_EN) //512K
if(blc_flash_capacity == FLASH_SIZE_512K){
flash_sector_mac_address = CFG_ADR_MAC_512K_FLASH;
flash_sector_calibration = CFG_ADR_CALIBRATION_512K_FLASH;
flash_sector_smp_storage = FLASH_ADR_SMP_PAIRING_512K_FLASH;
flash_sector_master_pairing = FLASH_ADR_MASTER_PAIRING_512K;
tlkapi_printf(APP_FLASH_INIT_LOG_EN, "[FLASH][INI] 512K Flash, MAC on 0x%x\n", flash_sector_mac_address);
}
#endif
#if (FLASH_ZB25WD80B_SUPPORT_EN || FLASH_GD25LD80C_SUPPORT_EN || FLASH_GD25LD80E_SUPPORT_EN) //1M
else if(blc_flash_capacity == FLASH_SIZE_1M){
flash_sector_mac_address = CFG_ADR_MAC_1M_FLASH;
flash_sector_calibration = CFG_ADR_CALIBRATION_1M_FLASH;
flash_sector_smp_storage = FLASH_ADR_SMP_PAIRING_1M_FLASH;
flash_sector_master_pairing = FLASH_ADR_MASTER_PAIRING_1M;
tlkapi_printf(APP_FLASH_INIT_LOG_EN, "[FLASH][INI] 1M Flash, MAC on 0x%x\n", flash_sector_mac_address);
}
#endif
else{
/*This SDK do not support other flash size except what listed above
If code stop here, please check your Flash */
tlkapi_printf(APP_FLASH_INIT_LOG_EN, "[FLASH][INI] flash size %x do not support !!!\n", blc_flash_capacity);
while(1);
}
}
(1) When using 512K flash, the sector 0x76000~0x76FFF stores the MAC address. When using 1M flash, it is 0xFF000~0x100000. In fact, the 6 bytes of MAC address are stored in 0x76000~0x76005 (0xFF000~0xFF005). In fact, the 6 bytes of MAC address are stored in 0x76000~0x76005 (0xFF000~0xFF005). When using 512K flash, the high byte address is stored in 0x76005, and the low byte address is stored in 0x76000. For example, the contents of flash 0x76000 to 0x76005 are 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, then the MAC address is 0x665544332211. Telink’s mass production fixture system will burn the actual product’s MAC address to 0x76000 (0xFF000) address, which corresponds to the SDK. If the user needs to modify this address, please make sure that the address programmed by the fixture system is also modified accordingly. In the SDK, the MAC address will be read from the CFG_ADR_MAC of flash in the user_init_normal function. This macro can be modified in vendor/common/ble_flash.h.
/**************************** 512 K Flash *****************************/
#ifndef CFG_ADR_MAC_512K_FLASH
#define CFG_ADR_MAC_512K_FLASH 0x76000
#endif
/**************************** 1 M Flash *******************************/
#ifndef CFG_ADR_MAC_1M_FLASH
#define CFG_ADR_MAC_1M_FLASH 0xFF000
#endif
(2) For the sector storage of 512K flash 0x77000~0x77FFF, Telink MCU needs to calibrate the customized information, which is 0xFE000~0xFEFFF for 1M flash. Only this part of the information does not follow the principle of "different types of information are placed in different sectors". Divide the 4096 bytes of this sector into different units for each 64 bytes, and each unit stores one type of calibration information. The calibration information can be placed in the same sector, because the calibration information is burned to the corresponding address during the fixture burning process. The actual firmware can only read the calibration information when it is running, and it is not allowed to write or erase it.
For details of calibration area information, please refer to "Telink_IC_Flash Customize Address_Spec". The calibration information is described with the offset address relative to the start address of the calibration area. For example, the offset address 0x00 refers to 0x77000 or 0xFE000.
a) Offset address 0x00, 1 byte, stores the BLE RF frequency offset calibration value.
b) Offset address 0x40, 4 byte, stores the TP value calibration. B85m IC does not require TP calibration, which is ignored here.
c) Offset address 0xC0, stores the ADC Vref calibration value.
d) Offset address 0x180, 16 byte, stores the Firmware digital signature, which is used to prevent theft of the client Firmware.
e) Offset address 0x1C0, 2 byte, stores the flash VDDF calibration value.
f) Others, reserved.
(3) 512K flash 0x74000 ~ 0x75FFF these two sectors are occupied by the BLE protocol stack system. For 1M flash, they are 0xFC000~0xFDFFF, which are used to store pairing and encryption information. User can also modify the location of these two sectors. The size is fixed at two sectors 8K and cannot be modified. User can call the following function to modify the starting address of the paired encryption information storage:
void blm_smp_configPairingSecurityInfoStorageAddr (int addr);
(4) Use 0x00000 ~ 0x3FFFF 256K space as program space by default;
0x00000 ~ 0x1FFFF total 128K is Firmware storage space; 0x20000 ~ 0x3FFFF 128K is the space to store new Firmware when OTA update, i.e. the maximum Firmware space supported is 128K.
If the default 128K program space is too large for the user and the user wants to free up some space in the 0x00000 ~ 0x3FFFF area for data storage, the protocol stack also provides the corresponding API, the modification method is described in the OTA chapter.
(5) All the remaining flash space is used as data storage space for USER.
Flash operation
flash space read/write operations use flash_read_page and flash_write_page functions, and flash erase uses flash_erase_sector function.
(1) Flash read/write operations
Flash read/write operations use flash_read_page and flash_write_page functions.
_attribute_data_retention_ flash_handler_t flash_read_page = flash_read_data;
_attribute_data_retention_ flash_handler_t flash_write_page = flash_page_program;
void flash_read_data(unsigned long addr, unsigned long len, unsigned char *buf);
void flash_page_program(unsigned long addr, unsigned long len, unsigned char *buf);
The flash_read_page function reads the contents of the flash:
u8 data[6] = {0};
flash_read_page(0x11000, 6, data); //Read 6 bytes starting from flash 0x11000 to the data array.
The flash_write_page function writes the flash:
u8 data[6] = {0x11,0x22,0x33,0x44,0x55,0x66};
flash_write_page(0x12000, 6, data); //Write 0x665544332211 to the 6 bytes starting at flash 0x12000.
The flash_write_page function performs page-based write operations. Each flash page is 256 bytes, and this function supports cross-page operations.
The flash_read_page can read more than 256 bytes of data at one time.
Note
-
When using the flash_write_page function, user can only write up to 16 bytes at a time, more than that will cause a BLE interrupt exception.
-
For the principle of this limitation, please refer to the introduction in the section "The Impact of Flash API on BLE Timing".
(2) flash erase operation
Use the flash_erase_sector function to erase the flash.
void flash_erase_sector(unsigned long addr);
A sector is 4096 bytes, e.g. 0x13000 ~ 0x13fff is a complete sector.
addr should be the first address of a sector, and this function erases the entire sector each time.
It takes a long time to erase a sector. When the system clock is 16M, it takes about 30 ~ 100ms or even longer.
(3) Impact of flash read/write and erase operations on system interrupts
The three flash operation functions flash_read_page, flash_write_page, and flash_erase_sector introduced above should first turn off the system interrupt irq_disable() when executing, and then restore the interrupt irq_restore() after the operation is completed, the purpose of which is to ensure the integrity and continuity of the flash MSPI timing operation, and to prevent the reentry of hardware resources caused by another flash operation calling the MSPI bus in the interrupt.
The timing of the BLE SDK RF receiving and sending packets is all controlled by interrupts. The consequence of turning off the system interrupt during the flash operation is that the timing of BLE receiving and sending packets will be destroyed and no timely response.
The execution time of the flash_read_page function is not too long and has little impact on the interruption of BLE. When using flash_write_page, it is recommended to write up to 16 Bytes at a time when BLE is connected, if it is too long, it may affect the BLE timing. Therefore, it is strongly recommended that users do not continuously read and write too long addresses in the main_loop when BLE is connected.
The execution time of the flash_erase_sector function is tens to hundreds of ms, so in the main_loop of the main program, once the BLE connection state is entered, it is not allowed to call the flash_erase_sector function, otherwise it will destroy the time point of BLE receiving and sending packets, causing the connection to be disconnected. If it is unavoidable to erase the flash when BLE is connected, please follow the ConnSlaveRole timing protection implementation method described later in this document to operate.
(4) Use pointer access to read flash
The firmware of the BLE SDK is stored on the flash, and when the program runs, only the first part of the flash is placed on the RAM for execution as resident memory code, and the vast majority of the remaining code is read from the flash to the RAM cache area (cache for short) when needed according to the program's locality principle. MCU reads the content on the flash by automatically controlling the internal MSPI hardware module.
User can use the pointer form to read the content on the flash. The principle of the pointer form to read the flash is that when the MCU system bus accesses the data, when it finds that the data address is not on the resident memory ram_code, the system bus will automatically switch to the MSPI, and the four lines MSCN, MCLK, MSDI and MSDO will operate the timing of the spi to get to read the flash data.
List three examples below:
u16 x = *(volatile u16*)0x10000; //Read flash 0x10000 2 byte
u8 data[16];
memcpy(data, 0x20000, 16); //Read flash 0x20000 16 byte copy to data
if(!memcmp(data, 0x30000, 16)){ //Read flash 0x30000 16 bytes and compare with data
//……
}
When reading the calibration value on the flash in user_init and setting it to the corresponding register, it is implemented by using pointers to access the flash. Please refer to the functions in the SDK.
void blc_app_loadCustomizedParameters_normal(void);
_attribute_ram_code_ void blc_app_loadCustomizedParameters_deepRetn(void);
Read flash with pointer, but can't write flash with pointer (write flash can only be achieved by flash_write_page).
It should be noted that there is a problem with pointer read flash: as long as the data is read through the MCU system bus, the MCU will cache the data in the cache, if the data in the cache is not covered by other content, and there is a new request to access the data, the MCU will directly use the cached content in the cache as the result. If the following situation occurs in the user's code.
u8 result;
result = *(volatile u16\*)0x40000; //Pointer read flash
u8 data = 0x5A;
flash_write_page(0x40000, 1, &data);
result = \*(volatile u16\*)0x40000; //Pointer read flash
if(result != 0x5A){ ..... }
The flash address 0x40000 was originally 0xff, the first read result is 0xff, then write 0x5A, theoretically the second read value is 0x5A, but the actual program gives the result is still 0xff, which was the first cache taken from the cache.
Note
- If this happens when the same address is read multiple times and the value of this address will be rewritten, do not use the pointer form, use API flash_read_page to achieve the safest, this function reads the result without taking the previously cached value from the cache.
It is correct to implement it as follows:
u8 result;
flash_read_page(0x40000, 1, &result); //API read flash
u8 data = 0x5A;
flash_write_page(0x40000, 1, &data);
The position will be adaptively shifted to a later position of the flash according to the actual size of the flash.
Flash operation protection
Since the process of writing flash and erasing flash requires to transfer the address and data to flash through the SPI bus, the level stability on the SPI bus is very important. Any error in these critical data will cause irreversible consequences, such as writing the firmware wrong or erasing it by mistake will cause the firmware to no longer work and the OTA function will be disabled.
In the years of mass production experience of Telink chips, there have been errors caused by flash operation under unstable conditions. Unstable conditions mainly include low power supply voltage, excessive power supply ripple, and intermittent power consumption of other modules on the system causing power supply jitter, and so on. In order to avoid similar operational risks in subsequent products, here we introduce some related flash operation protection methods. After reading carefully, customers need to consider these issues as much as possible and add more security protection mechanisms to ensure product stability.
Low voltage detection protection
Combine with the introduction of low power protection chapter, it is necessary to consider doing voltage detection before all flash write and erase operations to avoid the situation of operating flash at too low voltage. In addition, in order to ensure that the system is always working at a safe voltage, it is also recommended to do low voltage detection in main_loop at regular intervals to ensure the normal operation of the system.
Note
- About flash low-voltage protection, the following many places appear 2.0V, 2.2V and other thresholds, emphasize that these values are only examples, reference values. Customers have to assess the actual situation to modify these thresholds, such as single-layer boards, power supply fluctuations and other factors, are to improve the safety threshold as appropriate.
Take the low voltage detection in the SDK demo as an example:
Step 1 First, when powering on or waking up from deepsleep, before calling the flash function, a low voltage test should be performed to prevent flash problems caused by low voltage:
_attribute_no_inline_ void user_init_normal(void)
{
......
#if (APP_BATT_CHECK_ENABLE)
user_battery_power_check(VBAT_DEEP_THRES_MV);
#endif
......
}
Step 2 In the main_loop, low-voltage detection is required every 500ms:
#if (APP_BATT_CHECK_ENABLE)
if(battery_get_detect_enable() && clock_time_exceed(lowBattDet_tick, 500000) ){
lowBattDet_tick = clock_time();
user_battery_power_check(VBAT_DEEP_THRES_MV);
}
#endif
Considering the operating voltage requirements of both the MCU and the flash, if the demo voltage is set below 1.8 V, the chip will directly enter suspend mode, and the chip below 1.8~2.0V will directly enter deepsleep, and once the chip is detected to be below 2.0V, it needs to wait until the voltage rises to 2.2V, the chip will resume normal operation. Consider the following points in this design:
-
At 1.8V, there is a risk that the voltage is lower than the flash operating voltage. When you wake up after entering deepsleep, it may cause the flash to be abnormal and crash, so enter the suspend below 1.8V to ensure the safety of the chip;
-
At 2.0V, when other modules are operated, the voltage may be pulled down and the flash will not work normally. Therefore, it is necessary to enter deepsleep below 2.0V to ensure that the chip no longer runs related modules;
-
When there is a low voltage situation, need to restore to 2.2V in order to make other functions normal, this is to ensure that the power supply voltage is confirmed in the charge and has a certain amount of power, then Starting to restore the function is safer.
The above is the timing detection voltage and management method in SDK Demo, users can refer to it for design.
Note
- About flash low-voltage protection, the threshold values that appear above are only reference values. Customers have to assess the actual situation to modify these thresholds, such as single-layer boards, power supply fluctuations and other factors, are to improve the safety threshold as appropriate.
Flash lock protection
In addition to the above-mentioned timing voltage detection and management solutions, it is strongly recommended that customers do flash erase and write protection. This is because, in some cases, even if the low-voltage detection indicates the system is safe, there remains a slight risk that the operation of various application-layer modules afterward could cause the flash supply voltage to drop. Consequently, the flash contents may be corrupted if the supply voltage is insufficient for proper operation. Therefore, it is recommended that customers perform flash erasing protection after the program is started, so that even if there is a mis-operation, the content of the flash will be more secure.
Generally, it is recommended that customers only write-protect the part of the program (the front part of flash), so that the remaining flash addresses can still be used for user-level data storage. Here we take the SDK Sample project as an example to describe how to calculate the protection size and the protection method.
Initialize write protection
Call the API app_flash_protection_operation() to initialize flash protection and register the flash protection callback.
#if (APP_FLASH_PROTECTION_ENABLE)
app_flash_protection_operation(FLASH_OP_EVT_APP_INITIALIZATION, 0, 0);
blc_appRegisterStackFlashOperationCallback(app_flash_protection_operation); //register flash operation callback for stack
#endif
In the app_flash_protection_operation() function, the initialization of flash protection occurs when flash_op_evt is set to FLASH_OP_EVT_APP_INITIALIZATION. First, flash_protection_init() is called to obtain the blc_flash_mid for determining the flash type, and the corresponding function is invoked based on the result. The blc_flash_mid was previously acquired during initialization via the API blc_readFlashSize_autoConfigCustomFlashSector, which calls blc_flash_read_mid_get_vendor_set_capacity. This value is rechecked here to guard against acquisition failures or accidental deletion by the user.
void flash_protection_init(void)
{
if(!blc_flash_mid){
blc_flash_mid = flash_read_mid();
}
/* According to the flash mid, execute the corresponding lock flash API. */
switch(blc_flash_mid)
{
#if (FLASH_ZB25WD40B_SUPPORT_EN)
case MID13325E:
flash_lock_mid = (flash_lock_t)flash_lock_mid13325e;
flash_unlock_mid = flash_unlock_mid13325e;
flash_get_lock_status_mid = (flash_get_lock_status_t)flash_get_lock_block_mid13325e;
flash_unlock_status = FLASH_LOCK_NONE_MID13325E;
break;
#endif
#if (FLASH_ZB25WD80B_SUPPORT_EN)
case MID14325E:
flash_lock_mid = (flash_lock_t)flash_lock_mid14325e;
flash_unlock_mid = flash_unlock_mid14325e;
flash_get_lock_status_mid = (flash_get_lock_status_t)flash_get_lock_block_mid14325e;
flash_unlock_status = FLASH_LOCK_NONE_MID14325E;
break;
#endif
``````
default:
break;
}
}
The flash models supported by the TLSR825x in the current SDK can be found in drivers/8258/driver_ext/mcu_config.h. The same applies to the TLSR827x.
/*
Flash Type uid CMD MID Company
ZB25WD40B 0x4b 0x13325E ZB
ZB25WD80B 0x4b 0x14325E ZB
GD25LD40C 0x4b 0x1360C8 GD
GD25LD40E 0x4b 0x1360C8 GD
GD25LD80C 0x4b(AN) 0x1460C8 GD
GD25LD80E 0x4b(AN) 0x1460C8 GD
*/
#define FLASH_ZB25WD40B_SUPPORT_EN 1
#define FLASH_ZB25WD80B_SUPPORT_EN 1
#define FLASH_GD25LD40C_SUPPORT_EN 1
#define FLASH_GD25LD40E_SUPPORT_EN 1
#define FLASH_GD25LD80C_SUPPORT_EN 1
#define FLASH_GD25LD80E_SUPPORT_EN 1
If OTA functionality is supported, the protected size is determined by the OTA address. For example, if the OTA address is 0x20000 (128K), the protected flash area should be 256K, covering both the old firmware and the new firmware area to be OTA-updated. If OTA is not supported, the default protected flash area size is 256K. Note that users can modify this size according to their specific requirements.
if(flash_op_evt == FLASH_OP_EVT_APP_INITIALIZATION)
{
flash_protection_init();
u32 app_lockBlock = 0;
#if (BLE_OTA_SERVER_ENABLE)
u32 multiBootAddress = blc_ota_getCurrentUsedMultipleBootAddress();
if(multiBootAddress == MULTI_BOOT_ADDR_0x20000){
app_lockBlock = FLASH_LOCK_FW_LOW_256K;
}
else if(multiBootAddress == MULTI_BOOT_ADDR_0x40000){
app_lockBlock = FLASH_LOCK_FW_LOW_512K;
}
#if(MCU_CORE_TYPE == MCU_CORE_827x)
else if(multiBootAddress == MULTI_BOOT_ADDR_0x80000){ //for flash capacity smaller than 1M, OTA can not use 512K as multiple boot address
if(blc_flash_capacity < FLASH_SIZE_1M){
blc_flashProt.init_err = 1;
}
else{
app_lockBlock = FLASH_LOCK_FW_LOW_1M;
}
}
#endif
#else
app_lockBlock = FLASH_LOCK_FW_LOW_256K; //just demo value, user can change this value according to application
#endif
flash_lockBlock_cmd = flash_change_app_lock_block_to_flash_lock_block(app_lockBlock);
if(blc_flashProt.init_err){
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash protection initialization error!!!\n");
}
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] initialization, lock flash\n");
flash_lock(flash_lockBlock_cmd);
}
However, it is important to note that the designed size of the protected area may not necessarily match the final size. This depends on the flash protection interfaces provided by the current flash and other factors. For example, with a flash_mid of MID13325E and a size of 512K, if the lower 512K is selected for protection, the actual protected size within the program is 448K. This is because 64K should be reserved for system data and user data. As another example, with a flash_mid of MID14325E and a size of 1M, when a user selects to protect the lower 256K or 512K, the minimum size that can actually be selected for protection is 768K.
switch(blc_flash_mid)
{
#if (FLASH_ZB25WD40B_SUPPORT_EN) //512K capacity
case MID13325E:
if(app_lock_block == FLASH_LOCK_FW_LOW_256K){
flash_lock_block_size = FLASH_LOCK_LOW_256K_MID13325E;
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 256K block!\n");
}
else if(app_lock_block == FLASH_LOCK_FW_LOW_512K){
/* attention 1: use can change this value according to application
* attention 2: can not lock stack SMP data storage area
* attention 3: firmware size under protection is not 512K, user should calculate
* demo code: choose 448K, leave at 64K for system data(SMP storage data & calibration data & MAC Address) and user data,
* now firmware size under protection is 448K - 256K = 192K
* if this demo can not meet your requirement, you should change !!!*/
flash_lock_block_size = FLASH_LOCK_LOW_448K_MID13325E;
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 448K block!\n");
}
else{
blc_flashProt.init_err = 1; //can not use LOCK LOW 1M for 512K capacity flash
}
break;
#endif
#if (FLASH_ZB25WD80B_SUPPORT_EN) //1M capacity
case MID14325E:
if(app_lock_block == FLASH_LOCK_FW_LOW_256K || app_lock_block == FLASH_LOCK_FW_LOW_512K){
/* attention that :This flash type, minimum lock size is 768K, do not support 256K or other value
* demo code will lock 768K when user set OTA 128K or 256K as multiple boot address,
* system data(SMP storage data & calibration data & MAC Address) is OK;
* user data should be stored in flash address bigger than 768K !!!
* if this demo code lock area do not meet your requirement, you can change it !!!*/
flash_lock_block_size = FLASH_LOCK_LOW_768K_MID14325E;
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 768K block!\n");
}
else if(app_lock_block == FLASH_LOCK_FW_LOW_1M){
/* attention 1: use can change this value according to application
* attention 2: can not lock stack SMP data storage area
* attention 3: firmware size under protection is not 1M, user should calculate
* demo code: choose 960K, leave 64K for system data(SMP storage data & calibration data & MAC Address) and user data,
* now firmware size under protection is 960K - 512K = 448K
* if this demo can not meet your requirement, you should change !!! */
flash_lock_block_size = FLASH_LOCK_LOW_960K_MID14325E;
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] flash lock low 960K block!\n");
}
break;
#endif
Note
- Regarding flash protection, the above handling serves only as a reference. Users should evaluate whether this demonstration meets their actual requirements. If not, users may modify it accordingly.
- If using a flash model not supported by the current SDK, please add the write protection method manually based on the current approach.
Additionally, to prevent BLE disconnections caused by prolonged flash locking and unlocking during the connection process, the SDK now uses blc_ll_write_flash_status instead of flash_write_status. This function provides appropriate protection for the BRX timing during such operations.
Protection operations in the OTA process
During OTA, since flash erase and write operations are required, any write protection enabled at power-on should be unlocked during the OTA process and relocked after the OTA completes. Because the program restarts regardless of OTA success or failure, the flash_lock method introduced in the previous section re-enables write protection at program startup, forming a closed-loop mechanism to ensure application security.
The API for unlocking protection is flash_unlock(). During this process, the area is unlocked three times to prevent unlock failure.
void flash_unlock(void)
{
if(blc_flashProt.init_err){
return;
}
u16 cur_lock_status = flash_get_lock_status_mid();
if(cur_lock_status != flash_unlock_status){ //not in lock status
for(int i = 0; i < 3; i++){ //Unlock flash up to 3 times to prevent failure.
flash_unlock_mid();
cur_lock_status = flash_get_lock_status_mid();
if(cur_lock_status == flash_unlock_status){ //unlock success
break;
}
}
}
}
The API for lock protection is flash_lock(). During this operation, it first determines whether the currently locked area is the one the user requires. If not, it should unlock the current locked area before locking the desired area. To prevent failure, both locking and unlocking operations are repeated three times.
void flash_lock(unsigned int flash_lock_cmd)
{
if(blc_flashProt.init_err){
return;
}
u16 cur_lock_status = flash_get_lock_status_mid();
if(cur_lock_status == flash_lock_cmd){ //lock status is what we want, no need lock again
}
else{ //unlocked or locked block size is not what we want
if(cur_lock_status != flash_unlock_status){ //locked block size is not what we want, need unlock first
for(int i = 0; i < 3; i++){ //Unlock flash up to 3 times to prevent failure.
flash_unlock_mid();
cur_lock_status = flash_get_lock_status_mid();
if(cur_lock_status == flash_unlock_status){ //unlock success
break;
}
}
}
for(int i = 0; i < 3; i++) //Lock flash up to 3 times to prevent failure.
{
flash_lock_mid(flash_lock_cmd);
cur_lock_status = flash_get_lock_status_mid();
if(cur_lock_status == flash_lock_cmd){ //lock OK
break;
}
}
}
}
The app_flash_protection_operation() function handles different OTA phases in different ways.
a) The OTA begins erasing the old firmware and unlocking the flash memory.
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_BEGIN)
{
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA clear old FW begin, unlock flash\n");
flash_unlock();
}
b) OTA erases old firmware successfully and locks the flash.
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_CLEAR_OLD_FW_END)
{
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA clear old FW end, restore flash locking\n");
flash_lock(flash_lockBlock_cmd);
}
c) OTA begins writing new firmware and unlocks the flash memory.
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_BEGIN)
{
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA write new FW begin, unlock flash\n");
flash_unlock();
}
d) OTA writes the new firmware successfully and locks the flash.
else if(flash_op_evt == FLASH_OP_EVT_STACK_OTA_WRITE_NEW_FW_END)
{
tlkapi_printf(APP_FLASH_PROT_LOG_EN, "[FLASH][PROT] OTA write new FW end, restore flash locking\n");
flash_lock(flash_lockBlock_cmd);
}
Currently, callbacks for these four OTA events are implemented at the lower layers of the protocol stack. Note that users may add additional flash protection operations as needed. If users have their own OTA processing workflow, they should design a flash protection process aligned with their OTA logic based on the SDK's provided demonstration.
Internal Flash introduction
Impact of Flash access timing on BLE timing
Flash access timing
(1) Flash Operation Basic Timing

The figure above shows a typical MCU accessing flash timing. During MSCN pull-down, the data interaction with flash is completed through the level state change of MSDI and MSDO under the control of MCLK.
Flash access timing is the basic timing of flash operations. The period of MSCN being pulled down is data interaction, and it ends after being pulled up. All flash functions are based on it, and complex flash functions can be divided into several basic sequence of flash operations.
The basic timing of each flash operation is relatively independent, and the next round of operations can only be performed after one operation timing is completed.
(2) MCU hardware access to flash
Firmware is stored in flash, and the MCU execution program needs to read instructions and data from flash in advance. Combining with the introduction of section 2.1.2.1, we can see that the content that needs to be read is the text segment and the "read only data" segment. The MCU reads the instructions on the flash in real time during the running process, so it will start the basic sequence of the flash operation continuously. This process is automatically controlled by the MCU hardware and the software does not participate.
If an interrupt occurs during the main_loop program, it enters irq_handler. Even if the programs in main_loop and irq_handler are both in the text segment, there will be no flash timing conflict because it is done by the MCU hardware, which will do the relevant arbitration and control work.
(3) Software access to flash
MCU hardware access to flash only solves the problem of reading program instructions and "read only data". If you need to manually read, write, and erase the flash, use the flash_read_page, flash_write_page, flash_erase_sector and other APIs in the flash driver. Looking at the specific implementation of these APIs, it can be seen that the software controls the basic timing of flash operations, first pulling down MSCN, then reading and writing data, and finally pulling up MSCN.
(4) Flash access timing conflicts and solutions
Since the basic timing of flash operation is an indivisible and destructive process, when software and MCU hardware access flash at the same time, there is a possibility of timing conflicts because software and MCU hardware do not have coordination and arbitration mechanisms.
The scenario where this timing conflict occurs is that the software calls flash_read_page, flash_write_page, flash_erase_sector and other APIs in main_loop, and when the MSCN is pulled low and data is being read or written, an interrupt occurs and some instructions in the irq_handler are stored in the text segment, the MCU hardware also starts a new basic timing for flash operation, and this timing conflicts with the previous timing in main_loop, causing errors such as MCU crash.
As shown in the figure below, when Software access to flash ends, an interrupt occurs and responds, and MCU hardware starts to access flash. At this time, the result of flash access will inevitably be wrong.

Analyzing the conditions that should be met at the same time for timing conflicts, it can be concluded that the methods for resolving the conflicts include the following:
a) Do not show any APIs for software manipulation of flash timings in main_loop. this approach is not feasible and the use of APIs such as flash_write_page will appear on both the SDK and the application.
b) All procedures in the irq_handler function are stored in the ram_code in advance, without relying on any text segment or "read_only_data" segment. This method is not good either. It is limited by the SRAM size of TLSR825x/TLSR827x chips, if all the interrupt codes are stored in ram_code, the SRAM resources are not enough. In addition, it is not easy to control this restriction for users, and it is not possible to ensure that the user interrupt code is written so tightly.
c) In the several APIs of the software operating flash timing, add protection, close the interrupt, and prevent the irq_handler from responding. After the flash access is over, the interrupt is resumed.
The Telink BLE SDK currently uses method 3, the flash API to turn off interrupt protection. As shown in the code below (several codes are omitted in between), use irq_disable to turn off interrupts and irq_restore to restore them.
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);
}
The following diagram shows the principle of turning off the interrupt to protect the flash access timing. The interrupt is turned off when the software accesses flash, and the interrupt occurs in the middle but does not respond immediately (interrupt wait). When the software accesses flash timing is all finished correctly, the interrupt is turned back on, and the interrupt responds immediately at this time, and then the MCU hardware accesses flash.

Impact of Flash API on BLE timing
Previously introduced the use of flash API to turn off the interrupt protection to solve the problem of timing conflicts between software and hardware MCU access to flash. Since turning off the interrupt will make all interrupts unable to respond in real time, queuing to wait for the interrupt to resume and delay execution, you need to consider the possible side effects of the delayed time.
(1) Impact of off interrupt on BLE timing
Combine the characteristics of BLE timing to introduce. The BTX and BRX state machines in the BLE connection state in this SDK are all completed by interrupt tasks. BTX and BRX are similar implementations. Take BRX of slave role as an example.
The processing of BRX timing is more complicated. Take the processing of RX IRQ when more data appears in the BLE slave BRX as an example, as shown in the figure below. The SDK design requires the software to respond to every RX IRQ. The response may be delayed, but it should not be disabled. If a certain RX IRQ is lost, the RX packet that triggered this RX IRQ will also be lost, causing Link layer packet loss errors.

In the figure, RX1 triggers RX IRQ 1 at t1, and RX2 triggers RX IRQ 2 at t2. If no close interrupt occurs, the interrupt will respond in real time at t1 and t2, and the software correctly processes the RX packet.
The time difference between t1 and t2 is T_rx_irq, the off interrupt duration is T_irq_dis, T_irq_dis> T_rx_irq.
The off interrupt duration for all three cases IRQ disable case 1, IRQ disable case and IRQ disable case 3 is T_irq_dis, but the starting point of the off interrupt and the relative time of t1 are not the same.
IRQ disable case 1, t3 turns off the interrupt, t4 resumes the interrupt. t3 < t1; t4 > t2. RX IRQ 1 fails to respond at t1 and the interrupt is queued. RX IRQ 2 is triggered at t2, overwriting RX IRQ1 (because there can be only one RX IRQ in the interrupt wait queue), and RX IRQ 2 is queued and executed correctly at t4. RX IRQ 1 corresponding to RX1 is lost and Link layer errors out if RX1 is a valid packet.
IRQ disable case 2 and IRQ disable case 3, RX IRQ 1 and RX IRQ 2 are delayed, but they are not lost and no error occurs.
An important conclusion can be drawn from the analysis of the above examples:
When the interrupt closing duration is greater than a certain safety threshold, there may be a risk of Link layer error.
This safety threshold is related to the timing design of the Link layer in the SDK and the timing characteristics of the BLE Spec. The T_rx_irq in the scale is much more complicated. The specific details will not be introduced in detail, and the safety threshold is directly given here as 220us.
The same is the off-interrupt duration T_irq_dis. In the above example, IRQ disable case 2 and IRQ disable case 3 are different. Because the off interrupt occurs at different time points, RX IRQ 1 or RX IRQ2 will be delayed in response, and RX IRQ 2 will not overwrite packet loss caused by RX IRQ1. Even with IRQ disable case 1, if RX1 and RX2 are irrelevant empty packets, packet loss will not cause any errors.
When the interrupt off duration is greater than 220us, it is not certain that an error will occur. Multiple conditions should be met at the same time to trigger an error. These conditions include: a long time to turn off the interrupt, and the time point of the RX IRQ occurrence match a specific relationship, more data appears in BTX or BRX, the two RX packets that continuously trigger RX IRQ are valid data packets rather than empty packets, and so on. So the final conclusion is:
There is a risk of Link layer errors when the interrupt off duration is greater than 220us, and the probability is very low.
The design of BLE SDK Link layer aims at zero risk, that is, the interrupt closing duration is always less than 220us, and no chance of error is given.
Here is an additional introduction to the problem of RX packet loss in the above example. In the production of Telink BLE SDK, we often encounter this problem with customer feedback: under the premise that encryption is turned on, we see that the device sends a terminate packet with a reason of 0x3D (MIC_FAILURE), which leads to disconnection.
The above analysis shows that a long interrupt off time will cause the RX IRQ to be delayed for too long and then overwritten, and eventually lost packets. However, the SDK will handle the interrupt shutdown time correctly, which will be described in detail later in the document. The more likely reason is that the user uses other interrupts (such as Uart, USB, etc.), and the software execution time for these interrupts to respond is too long, which will have the same effect as the interrupt shutdown, and will also delay the RX IRQ. Here we limit the maximum safe time for a user interrupt execution to 100us.
(2) Impact of flash API off interrupt protection on BLE timing
In order to avoid the timing conflict between software access to flash and MCU hardware access to flash, the flash API uses a method of turning off interrupts. When the interrupt closing duration is greater than 220us, there is a risk of error in Link layer. In order to solve the contradiction between the two, it is necessary to pay attention to the maximum time for the flash API to close the interrupt.
The affected BLE timing is connection state slave role and master role. System initialization and Advertising state are not affected. In the connection state, the following three flash APIs are mainly concerned: flash_read_page, flash_write_page, flash_erase_sector. Other flash APIs are generally not used or used during initialization.
a) flash_read_page
It has been tested and verified that when the number of bytes read by flash_read_page at a time does not exceed 64, the time is very safe, within 220us. After this value is exceeded, there will be a certain risk.
It is strongly recommended that users read up to 64 bytes when using flash_read_page to read flash. If it exceeds 64 bytes, it needs to be split into multiple calls to flash_read_page to achieve.
b) flash_erase_sector
The time of flash_erase_sector is generally in the order of 10ms ~ 100ms, which is far more than 220us. So this SDK requires users not to call flash_erase_sector in the BLE connection state. If you call this API directly, the connection will definitely go wrong.
We recommend that users use other methods to replace the design of flash_erase_sector. For example, some applications are designed to repeatedly update some key information stored in flash. In the design, you can consider selecting a larger area and using flash_write_page to continuously extend back.
For BLE slave applications, if the unavoidable flash_erase_sector occurs occasionally, you can use the ConnSlaveRole timing protection mechanism to avoid it. Please refer to the details of this document.
Note that because the timing protection mechanism is very complicated, it is not recommended to use the high-frequency flash_erase_sector as it cannot guarantee the stability of the mechanism of repeated connection calls when the BLE slave is connected. It is recommended that users avoid this situation as much as possible by design.
c) flash_write_page
The flash_write_page time is affected by many key factors, including: Flash type, Flash technology, write byte number, high and low temperature, etc. The following is a detailed description from several types of internal flash.
Use of internal Flash API
According to the previous section, flash_write_page in flash API is related to the type of internal flash. This section describes in detail with several kinds of internal flash already supported by 825x and 827x.
GD Flash
GD flash belongs to the ETOX process and is the earliest internal flash supported by 825x and 827x.
The consumption time of flash_write_page is related to the parameter len (i.e. the number of bytes written at once), which is close to a positive relationship. After detailed testing and analysis within Telink, it is found that when the number of bytes is less than or equal to 16, the writing time can be stabilized within 220us; if the number of bytes exceeds 16, there will be risks.
For GD flash, the maximum number of bytes written by flash_write_page is required to be 16. If it exceeds 16, such as 32, it can be split into two and write 16 bytes.
In the SDK design, there are two places involving flash_write_page. One is SMP storage configuration information, which uses 16 bytes per write; the other is when OTA writes new firmware, it also uses 16 bytes per write. In the design of OTA long package, for example, each package of 240 bytes of valid data is divided into 15 writes (16*15=240).
It is strongly recommended that customers use flash_write_page to write at most 16 bytes each time, otherwise there will be a risk of conflict with BLE timing.
Zbit Flash
Zbit flash is ETOX process, flash_write_page time is similar to GD flash, and the maximum number of bytes written is limited to 16.
However, due to some characteristics of Zbit flash itself, when the temperature rises, flash_write_page consumes longer time. For this characteristic, the BLE SDK handles it as follows:
(1) For products with high-temperature application scenarios, the operations department will not send customers chips with internal Zbit flash. Please note this point as well.
(2) The SDK increases the power supply voltage of flash where flash_write_page is involved (SMP stores pairing information, OTA writes new firmware), which can prevent Zbit Flash write time from being too long.
The measures in point 2 above are very important. Products that use Zbit flash should ensure that the above measures have taken effect.
The corresponding B85/B87 single-connection SDK of the Handbook (including TLSR825x and TLSR827x series chips) has added this measure to support Zbit flash.
For several important historical versions of the historical TLSR825x/TLSR827x BLE single connection SDK, the Telink BLE Team released related patches to support Zbit flash. Customers should update the patch to ensure risk-free production.
PUYA Flash
The internal PUYA flash added on 825x and 827x is Sonos process, flash_write_page time law and ETOX process is very different.
Note
- PUYA flash will not be mass-produced on the 825x and 827x for the time being, but it retains the introduction of related compatibility principles to deepen users’ understanding of BLE timing.
The flash with Sonos process does not support byte programming, only page programming.
For example, call flash_write_page to write a byte value of 0x5A to the address 0x1000. the internal practice of flash is to first read all the contents of the page corresponding to 0x1000 (0x1000 ~ 0x10FF total 256 byte) out of the cache, then modify the first byte on the cache to 0x5A, and finally write all the values of the entire page cache to the page 0x1000.
The problem caused by this mechanism is that whether the number of bytes written is 1, 2, 4, 8, or 200, 255, etc., the time consumed is similar to that of writing a page 256 byte, which takes about 2ms. But as we said before, if the interrupt time exceeds 220us, the Link layer is at risk of error.
Since the Sonos process flash write time of 2ms is much larger than the safety threshold of 220us, it needs to be solved by doing circumvention solutions in the Link layer design.
When users need to write flash, they should call the flash_write_page function. flash_write_page is a function in the old version of the SDK, and a pointer in the new version of the SDK. This pointer points to the flash_write_data function by default, which is aimed at the implementation of flash_write_page of GD and Zbit flash.
BLE SDK detects the flash type during initialization, and if it is found to be Sonos process flash, it will modify the flash_write_page pointer to another special function that interacts with Link layer timing design to actively avoid the BTX, BRX timings, so that the write flash action never coincides with BTX and BRX in time. This design is not visible to the user in the underlying SDK implementation, so the user can use flash_write_page without worry.
The following is an example to explain BRX as an example, BTX principle is similar. The simplified model is shown in the figure below.
Assume that the execution time of flash_write_page is the standard 2ms (the actual time is not so standard, it is far more complicated than it, and the SDK internally deals with time fluctuations).

In the figure, write_req refers to calling the flash_write_page function in the software, write_wait refers to a safe time point when the BRX state machine is idle (a while loop is used in the software to read), write_execute is to call the basic timing of the flash operation to complete the flash write. Write_done indicates a successful write. The total time consumed by flash_write_pag as seen by the application layer is the schedule from write_req to write_done.
Write flash case 1, write_req occurs at a very safe time outside of BRX, execute write_execute directly. The flash_write_pag time seen by the application layer is 2ms.
Write flash case 2, write_req is in the event of BRX, if write_execute at this time, BRX timing may be broken and packet loss may occur, so execute write_wait first, and then write_execute after BRX ends. The flash_write_pag time seen by the application layer is the write_wait time plus the write_execute time (2ms), and the write wait time is related to the actual running time of the BRX.
Write flash case 3. Although write_req is not in BRX timing, because there is a BRX that will respond within 2ms in the future, if write_execute directly consumes 2ms, the BRX start time will be postponed, causing the BLE slave to receive the packet time error. Therefore, it is considered that write_req is an unsafe point in time, execute write_wait until the end of BRX and then write_execute. The flash_write_pag time seen by the application layer is the write_wait time plus the write_execute time (2ms), and the write wait time is related to the actual running time of the BRX.
From the above introduction, when using the flash_write_page function on PUYA flash, the write flash action is not executed immediately, and it may take a short time to wait before the write action is executed. If flash_write_page is called frequently and frequently, the efficiency of the program may decrease, which is a side effect. The wasted waiting time is the time consumed by write_wait. The figure below is an example of flash_write_page time in an extreme case. At this time, there is a large amount of more data in BRX, which leads to a very long BRX duration.

Flash Manufacturer Model Support and Configuration
Supported Flash Manufacturer Models
The SDK supports Flash chips from multiple manufacturers. Each manufacturer's Flash has a specific MID (Manufacturer ID) and characteristic parameters. Below is the list of Flash models currently supported by the SDK:
| Manufacturer | Model | Capacity | MID | UID Command | Sector Erase Time |
|---|---|---|---|---|---|
| ZB | ZB25WD40B | 512KB | 0x1360eb | 0x4B | 60ms |
| GD | GD25LD40C | 512KB | 0x1460c8 | 0x4B | 50ms |
| GD | GD25LD40E | 512KB | 0x1460c9 | 0x4B | 50ms |
| ZB | ZB25WD80B | 1MB | 0x1360ec | 0x4B | 80ms |
| GD | GD25LD80C | 1MB | 0x146014 | 0x4B | 70ms |
| GD | GD25LD80E | 1MB | 0x146015 | 0x4B | 70ms |
| PUYA | P25Q40L | 512KB | 0x146085 | 0x4B | 45ms |
| PUYA | P25Q80L | 1MB | 0x146086 | 0x4B | 65ms |
| TH | TH25D40LA | 512KB | 0x1360eb | 0x4B | 55ms |
| TH | TH25D40UA | 512KB | 0x1360eb | 0x4B | 55ms |
Flash Protection Mechanism Implementation
TH25D40LA/TH25D40UA Protection Area Configuration
For TH25D40LA and TH25D40UA Flash models, multiple protection block size configurations are supported:
typedef enum {
FLASH_LOCK_LOW_496K_MID1360EB = 0x00, // Lock the lower 496K area
FLASH_LOCK_LOW_384K_MID1360EB = 0x01, // Lock the lower 384K area
FLASH_LOCK_LOW_320K_MID1360EB = 0x02, // Lock the lower 320K area
FLASH_LOCK_LOW_256K_MID1360EB = 0x03, // Lock the lower 256K area
FLASH_LOCK_LOW_192K_MID1360EB = 0x04, // Lock the lower 192K area
FLASH_LOCK_LOW_128K_MID1360EB = 0x05, // Lock the lower 128K area
FLASH_LOCK_LOW_64K_MID1360EB = 0x06, // Lock the lower 64K area
FLASH_LOCK_NONE_MID1360EB = 0x07, // No lock
FLASH_LOCK_HIGH_496K_MID1360EB = 0x08, // Lock the upper 496K area
FLASH_LOCK_HIGH_384K_MID1360EB = 0x09, // Lock the upper 384K area
FLASH_LOCK_HIGH_320K_MID1360EB = 0x0A, // Lock the upper 320K area
FLASH_LOCK_HIGH_256K_MID1360EB = 0x0B, // Lock the upper 256K area
FLASH_LOCK_HIGH_192K_MID1360EB = 0x0C, // Lock the upper 192K area
FLASH_LOCK_HIGH_128K_MID1360EB = 0x0D, // Lock the upper 128K area
FLASH_LOCK_HIGH_64K_MID1360EB = 0x0E, // Lock the upper 64K area
FLASH_LOCK_ALL_MID1360EB = 0x0F, // Lock all areas
} mid1360eb_lock_block_e;
OTP Security Register Support
Certain Flash models support OTP (One-Time Programmable) security registers for storing permanent information:
typedef enum {
FLASH_OTP_0x001000_512B_MID1360EB = 0x001000, // OTP area start address
FLASH_OTP_0x001200_512B_MID1360EB = 0x001200, // Second OTP area
FLASH_OTP_0x001400_512B_MID1360EB = 0x001400, // Third OTP area
FLASH_OTP_0x001600_512B_MID1360EB = 0x001600, // Fourth OTP area
} mid1360eb_otp_block_e;
OTP Register Operation APIs:
// // Read OTP register
int flash_read_otp_mid1360eb(u32 addr, u32 len, u8 *buf);
// Write to OTP register (one-time programming)
int flash_write_otp_mid1360eb(u32 addr, u32 len, u8 *buf);
// Erase OTP register
int flash_erase_otp_mid1360eb(u32 addr);
// Lock OTP register (permanently locked, cannot be modified again)
int flash_lock_otp_mid1360eb(u32 addr);
Flash Status Register Operations
Status Register Bit Mask Definition
Flash status register bit definitions may vary across manufacturers. Below are universal bit masks:
// Status register bit definitions
#define FLASH_STATUS_BUSY (1 << 0) // Busy flag
#define FLASH_STATUS_WEL (1 << 1) // Write enable flag
#define FLASH_STATUS_BP0 (1 << 2) // Block protection bit 0
#define FLASH_STATUS_BP1 (1 << 3) // Block protection bit 1
#define FLASH_STATUS_BP2 (1 << 4) // Block protection bit 2
#define FLASH_STATUS_TB (1 << 5) // Top/Bottom protection selection
#define FLASH_STATUS_SEC (1 << 6) // Sector protection
#define FLASH_STATUS_SRP0 (1 << 7) // Status register protection bit 0
Status Register Operation APIs
// Read status register
u8 flash_read_status_mid1360eb(void);
// Write status register (with mask control)
int flash_write_status_mid1360eb(u8 status, u8 mask);
Flash Operation Safety Requirements
Power Supply Voltage Check
Before any Flash operation, ensure the power supply voltage is within safe limits. Performing Flash operations under low voltage may cause the following risks:
- Firmware corruption
- Data write errors
- Permanent damage to the Flash chip
Important Note: Before calling any Flash operation API, check if the power supply voltage meets requirements. If voltage is too low, postpone Flash operations until voltage recovers.
Safe Operation Example
// Example of a safe Flash write operation
void safe_flash_write(u32 addr, u32 len, u8 *buf)
{
// Check power supply voltage
if (!is_power_voltage_safe()) {
// Voltage unsafe, postpone operation
return;
}
// Execute Flash write
flash_write_page(addr, len, buf);
}
Advanced Flash Protection Mechanisms
Flash Protection During OTA
Flash protection is critical during OTA updates. The SDK provides a comprehensive OTA Flash protection workflow:
(1) Before OTA Initiation: Unlock flash areas requiring updates
(2) During OTA: Lock completed areas as needed
(3) After OTA Completion: Relock all necessary areas
Application Layer Flash Protection Strategies
The following flash protection strategies are recommended for the application layer:
(1) Partition Protection: Store different data types in separate sectors
(2) Backup Mechanism: Critical data should have dedicated backup regions
(3) Version Management: Use version numbers to manage data updates
(4) Error Recovery: Implement data checksum and error recovery mechanisms
Key Scan
Telink provides a keyscan architecture based on row/column scan to detect and process key state update (press/release). User can directly use the demo code, or realize the function by developing his own code.
Key Matrix
Figure shows a 5*6 Key matrix which supports up to 30 buttons. Five drive pins (Row0~Row4) serve to output drive level, while six scan pins (CoL0~CoL5) serve to scan for key press in current column.

The Telink EVK board is a 2*2 keyboard matrix. In the actual product application, more keys may be needed, such as remote control switches, etc. The following is an example of Telink's demo board providing remote control. Combined with the above figure, the keyscan related configuration in app_config.h is explained in detail as follows.
According to the real hardware circuit, on Telink demo board, Row0~Row4 pins are PE2, PB4, PB5, PE1 and PE4, CoL0~CoL5 pins are PB1, PB0, PA4, PA0, PE6 and PE5.
Define drive pin array and scan pin array:
#define KB_DRIVE_PINS {GPIO_PE2, GPIO_PB4, GPIO_PB5, GPIO_PE1, GPIO_PE4}
#define KB_SCAN_PINS {GPIO_PB1, GPIO_PB0, GPIO_PA4, GPIO_PA0, GPIO_PE6, GPIO_PE5}
Keyscan adopts analog pull-up/pull-down resistor of GPIO: drive pins use 100K pull-down resistor, and scan pins use 10K pull-up resistor. When no button is pressed, scan pins act as input GPIOs and read high level due to 10K pull-up resistor. When key scan starts, drive pins output low level; if low level is detected on a scan pin, it indicates there’s button pressed in current column (Note: Drive pins are not in float state, if output is not enabled, scan pins still detect high level due to voltage division of 100K and 10K resistor.)
Define valid voltage level detected on scan pins when drive pins output low level in Row/Column scan:
#define KB_LINE_HIGH_VALID 0
Define pull-up resistor for scan pins and pull-down resistor for drive pins:
#define MATRIX_ROW_PULL PM_PIN_PULLDOWN_100K
#define MATRIX_COL_PULL PM_PIN_PULLUP_10K
#define PULL_WAKEUP_SRC_PE2 MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PB4 MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PB5 MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PE1 MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PE4 MATRIX_ROW_PULL
#define PULL_WAKEUP_SRC_PB1 MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PB0 MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PA4 MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PA0 MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PE6 MATRIX_COL_PULL
#define PULL_WAKEUP_SRC_PE5 MATRIX_COL_PULL
Since “ie” of general GPIOs is set as 0 by default in gpio_init, to read level on scan pins, corresponding “ie” should be enabled.
#define PB1_INPUT_ENABLE 1
#define PB0_INPUT_ENABLE 1
#define PA4_INPUT_ENABLE 1
#define PA0_INPUT_ENABLE 1
#define PE6_INPUT_ENABLE 1
#define PE5_INPUT_ENABLE 1
When MCU enters sleep mode, it’s needed to configure PAD GPIO wakeup. Set drive pins as high level wakeup; when there’s button pressed, drive pin reads high level, which is 10/11 VCC. To read level state of drive pins, corresponding “ie” should be enabled.
#define PE2_INPUT_ENABLE 1
#define PB4_INPUT_ENABLE 1
#define PB5_INPUT_ENABLE 1
#define PE1_INPUT_ENABLE 1
#define PE4_INPUT_ENABLE 1
Keyscan and Keymap
Keyscan
After configuration as shown above, the function below is invoked in main_loop to implement keyscan.
u32 kb_scan_key (int numlock_status, int read_key)
numlock_status: Generally set as 0 when invoked in main_loop. Set as “KB_NUMLOCK_STATUS_POWERON” only for fast keyscan after wakeup from deepsleep (corresponding to DEEPBACK_FAST_KEYSCAN_ENABLE).
read_key: Buffer processing for key values, generally not used and set as 1 (if it’s set as 0, key values will be pushed into buffer and not reported to upper layer).
The return value is used to inform user whether matrix keyboard update is detected by current scan: if yes, return 1; otherwise return 0.
The “kb_scan_key” is invoked in main_loop. As in BLE timing sequence, each main_loop is an adv_interval or conn_interval. In advertising state (suppose adv_interval is 30ms), key scan is processed once for each 30ms; in connection state (suppose conn_interval is 10ms), key scan is processed once for each 10ms.
In theory, when button states in matrix are different during two adjacent key scans, it’s considered as an update.
In actual code, a debounce filtering processing is enabled: It will be considered as a valid update, only when button states stay the same during two adjacent key scans, but different with the latest stored matrix keyboard state. “1” will be returned by the function to indicate valid update, matrix keyboard state will be indicated by the structure “kb_event”, and current button state will be updated to the newest matrix keyboard state. Corresponding code in keyboard.c is shown as below:
unsigned int key_debounce_filter( u32 mtrx_cur[], u32 filt_en );
The newest button state means press or release state set of all buttons in the matrix. When power on, initial matrix keyboard state shows all buttons are “released” by default, and debounce filtering processing is enabled. As long as valid update occurs to the button state, “1” will be returned, otherwise “0” will be returned.
For example: press a button, a valid update is returned; release a button, a valid update is returned; press another button with a button held, a valid update is returned; press the third button with two buttons held, a valid update is returned; release a button of the two pressed buttons, a valid update is returned……
Keymap & kb_event
If a valid button state update is detected by invoking the “kb_scan_key”, user can obtain current button state via a global structure variable “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;
The “kb_event” consists of 8 bytes:
-
“cnt” serves to indicate valid count number of pressed buttons currently;
-
“ctrl_key” is not used generally except for standard USB HID keyboard (user is not allowed to set keycode in keymap as 0xe0~0xe7).
-
keycode[6] indicates keycode of up to six pressed buttons can be stored (if more than six buttons are pressed actually, only the former six can be reflected).
Keycode definition of all buttons in the “app_config.h” is shown as below:
#define KB_MAP_NORMAL {\
VK_B, CR_POWER, VK_NONE, VK_C, CR_HOME, \
VOICE, VK_NONE, VK_NONE, CR_VOL_UP, CR_VOL_DN, \
VK_2, VK_RIGHT, CR_VOL_DN, VK_3, VK_1, \
VK_5, VK_ENTER, CR_VOL_UP, VK_6, VK_4, \
VK_8, VK_DOWN, VK_UP, VK_9, VK_7, \
VK_0, CR_BACK, VK_LEFT, CR_VOL_MUTE, CR_MENU, }
The key map follows the format of 5*6 matrix structure. The keycode of pressed button can be configured accordingly, for example, the keycode of the button at the cross of Row0 and CoL0 is “VK_B”.
In the “kb_scan_key” function, the “kb_event.cnt” will be cleared before each scan, while the array “kb_event.keycode[]” won’t be cleared automatically. Whenever “1” is returned to indicate valid update, the “kb_event.cnt” will be used to check current valid count number of pressed buttons.
a) If current kb_event.cnt = 0, previous valid matrix state “kb_event.cnt” should be uncertain non-zero value; the update should be button release, but the number of released button is uncertain. Data in kb_event.keycode[] (if available) is invalid.
b) If current kb_event.cnt = 1, the previous kb_event.cnt indicates button state update. If previous kb_event.cnt is 0, it indicates the update is one button is pressed; if previous kb_event.cnt is 2, it indicates the update is one of the two pressed buttons is released; if previous kb_event.cnt is 3, it indicates the update is two of the three pressed buttons are released……kb_event.keycode[0] indicates the key value of currently pressed button. The subsequent keycodes are negligible.
c) If current kb_event.cnt = 2, the previous kb_event.cnt indicates button state update. If previous kb_event.cnt is 0, it indicates the update is two buttons are pressed at the same time; if previous kb_event.cnt is 1, it indicates the update is another button is pressed with one button held; if previous kb_event.cnt is 3, it indicates the update is one of the three pressed buttons is released……kb_event.keycode[0] and kb_event.keycode[1] indicate key values of the two pressed buttons currently. The subsequent keycodes are negligible.
User can manually clear the “kb_event.keycode” before each key scan, so that it can be used to check whether valid update occurs, as shown in the example below.
In the sample code, when kb_event.keycode[0] is not zero, it’s considered a button is pressed, but the code won’t check further complex cases, such as whether two buttons are pressed at the same time or one of the two pressed buttons is released.
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
When “kb_scan_key” is invoked, a basic keyscan flow is shown as below:
(1) Initial full scan through the whole matrix.
All drive pins output drive level (0). Meanwhile read all scan pins, check for valid level, and record the column on which valid level is read. (The scan_pin_need is used to mark valid column number.)
If row-by-row scan is directly adopted without initial full scan through the whole matrix, each time all rows should be scanned at least, even if no button is pressed. To save scan time, initial full scan through the whole matrix can be added, thus it will directly exit keyscan if no button press is detected on any column.
The first full scan codes:
scan_pin_need = kb_key_pressed (gpio);
In the “kb_key_pressed” function, all rows output low level, and stabilized level of scan pins will be read after 20us delay. A release_cnt is set as 6; if a detection shows all pressed buttons in the matrix are released, it won’t consider no button is pressed and stop row-by-row scan immediately, but buffers for six frames. If six successive detections show buttons are all released, it will stop row-by-row scan. Thus key debounce processing is realized.
(2) Scan row by row according to full scan result through the whole matrix.
If button press is detected by full scan, row-by-row scan is started: Drive pins (ROW0~ROW4) output valid drive level row by row; read level on columns, and find the pressed button. Following is related code:
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;
}
}
The following methods are used to optimize code execution time for row-by-row scan.
-
When a row outputs drive level, it’s not needed to read level of all columns (CoL0~CoL5). Since the scan_pin_need marks valid column number, user can read the marked columns only.
-
After a row outputs drive level, a 20us or so delay is needed to read stabilized level of scan pins, and a buffer processing is used to utilize the waiting duration.
The array variable “u32 pressed_matrix[5]” (up to 40 columns are supported) is used to store final matrix keyboard state: pressed_matrix[0] bit0~bit5 mark button state on CoL0~CoL5 crossed with Row0, ……, pressed_matrix[4] bit0~bit5 mark button state on CoL0~CoL5 crossed with Row4.
(3) Debounce filtering for pressed_matrix[].
Corresponding codes:
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);
During fast keyscan after wakeup from deepsleep, “numlock_status” equals “KB_NUMLOCK_STATUS_POWERON”; the “filt_en” is set as 0 to skip filtering and fast obtain key values.
In other cases, the “filt_en” is set as 1 to enable filtering. Only when pressed_matrix[] stays the same during two adjacent key scans, but different from the latest valid pressed_matrix[], will the “key_changed” set as 1 to indicate valid update in matrix keyboard.
(4) Buffer processing for pressed_matrix[].
Push pressed_matrix[] into buffer. When the “read_key” in “kb_scan_key (int numlock_status, int read_key)” is set as 1, the data in the buffer will be read out immediately. When the “read_key” is set as 0, the buffer stores the data without notification to the upper layer; the buffered data won’t be read until the read_key is 1.
In current SDK, the “read_key” is fixed as 1, i.e. the buffer does not take effect actually.
(5) According to pressed_matrix[], look up the KB_MAP_NORMAL table and return key values.
Corresponding functions are “kb_remap_key_code” and “kb_remap_key_row”.
Deepsleep wake_up fast keyscan
When the Slave device enters deepsleep while it is connected, it is woken up by a keystroke. After waking up, the program starts from the beginning and has to send broadcast packets in main_loop after user_init and wait until it is connected before sending the value of the key to ble master.
The BLE SDK has already done the relevant processing to make the deep back as fast as possible, but this time may still reach the 100 ms level (e.g. 300 ms). In order to prevent the wake up keystroke from being lost, a fast keyscan and data caching process is done in the SDK.
The fast keyscan is because the MCU will consume some time to re-initialize from the flash load program after the key wakes up, and the time of keyscan in main_loop will also take some more time due to the anti-jitter filter processing, which may lead to the loss of this key.
The data is cached because if valid key data is scanned in the broadcast state, after pushing to the BLE TX fifo, the data in the connected state will be cleared again.
The relevant code is controlled by the macro DEEPBACK_FAST_KEYSCAN_ENABLE in app_config.h.
#define DEEPBACK_FAST_KEYSCAN_ENABLE 1
void deep_wakeup_proc(void)
{
#if(DEEPBACK_FAST_KEYSCAN_ENABLE)
if(analog_read(DEEP_ANA_REG0) == CONN_DEEP_FLG){
if(kb_scan_key (KB_NUMLOCK_STATUS_POWERON,1) && kb_event.cnt){
deepback_key_state = DEEPBACK_KEY_CACHE;
key_not_released = 1;
memcpy(&kb_event_cache,&kb_event,sizeof(kb_event));
}
}
#endif
}
When initializing, scan the key before user_init, read the deep non-power-off analog register to detect that it is connected state into deep wakeup, call kb_scan_key. Then do not start the anti-jitter filtering process, and directly get the key state of the whole matrix currently read. If the scan finds that a key is pressed (the key change is returned and kb_event.cnt is not 0), the kb_event variable is copied to the cache variable kb_event_cache.
Add deepback_pre_proc and deepback_post_proc processing to the keyscan of main_loop.
void proc_keyboard (u8 e, u8 *p)
{
kb_event.keycode[0] = 0;
int det_key = kb_scan_key (0, 1);
#if(DEEPBACK_FAST_KEYSCAN_ENABLE)
if(deepback_key_state != DEEPBACK_KEY_IDLE){
deepback_pre_proc(&det_key);
}
#endif
if (det_key){
key_change_proc();
}
#if(DEEPBACK_FAST_KEYSCAN_ENABLE)
if(deepback_key_state != DEEPBACK_KEY_IDLE){
deepback_post_proc();
}
#endif
}
The deepback_pre_proc processing is to wait until the slave and master are connected, and when there is no key state change in a certain kb_key_scan, the value of the previously cached kb_event_cache is used as the current latest key change, which realizes the cache processing of fast sweep key value.
Pay attention to the processing of key release: When manually setting the key value, determine whether the current matrix key is still pressed. If a key is pressed, there is no need to add a manual release, because a release action will be generated when the actual key is released; if the current key has been released, mark a manual release later, otherwise there may be a cached key event is always valid and cannot be released.
The deepback_post_proc processing is to decide whether to put a key release event in the ble TX fifo according to whether there is a manual release event left in the deepback_pre_proc.
Repeat Key Processing
The most basic keyscan described above only generates a change event when the state of a key is changed and reads the current key value via kb_event, but it is not possible to implement the repeat key function. When a key is pressed all the time, a key value needs to be sent at regular intervals.
The “repeat key” function is masked by default. By configuring related macros in the “app_config.h”, this function can be controlled correspondingly.
#define KB_REPEAT_KEY_ENABLE 0
#define KB_REPEAT_KEY_INTERVAL_MS 200
#define KB_REPEAT_KEY_NUM 1
#define KB_MAP_REPEAT {VK_1, }
(1) KB_REPEAT_KEY_ENABLE
This macro serves to enable or mask the repeat key function. To use this function, first set “KB_REPEAT_KEY_ENABLE” as 1.
(2) KB_REPEAT_KEY_INTERVAL_MS
This macro serves to set the repeat interval time. For example, if it’s set as 200ms, it indicates when a button is held, kb_key_scan will return an update with the interval of 200ms. Current button state will be available in kb_event.
(3) KB_REPEAT_KEY_NUM & KB_MAP_REPEAT
The two macros serve to define current repeat key values: KB_REPEAT_KEY_NUM specifies the number of keycodes, while the KB_MAP_REPEAT defines a map to specify all repeat keycodes. Note that the keycodes in the KB_MAP_REPEAT should be the values in the KB_MAP_NORMAL.
Following example shows a 6*6 matrix keyboard: by configuring the four macros, eight buttons including UP, DOWN, LEFT, RIGHT, V+, V-, CHN+ and CHN- are set as repeat keys with repeat interval of 100ms, while other buttons are set as non-repeat keys.

User can search for the four macros in the project to locate the code about repeat key.
Stuck Key Processing
Stuck key processing is used to save power when one or multiple buttons of a remote control/keyboard is/are pressed and held for a long time unexpectedly, for example a RC is pressed by a cup or ashtray. If keyscan detects some button is pressed and held, without the stuck key processing, MCU won’t enter deepsleep or other low power state since it always considers the button is not released.
Following are two related macros in the app_config.h:
#define STUCK_KEY_PROCESS_ENABLE 0
#define STUCK_KEY_ENTERDEEP_TIME 60//in s
By default the stuck key processing function is masked. User can set the “STUCK_KEY_PROCESS_ENABLE” as 1 to enable this function.
The “STUCK_KEY_ENTERDEEP_TIME” serves to set the stuck key time: if it’s set as 60s, it indicates when button state stays fixed for more than 60s with some button held, it’s considered as stuck key, and MCU will enter deepsleep.
User can search for the macro “STUCK_KEY_PROCESS_ENABLE” to locate related code in the keyboard.c, as shown below:
#if (STUCK_KEY_PROCESS_ENABLE)
u8 stuckKeyPress[ARRAY_SIZE(drive_pins)];
#endif
An u8-type array stuckKeyPress[5] is defined to record row(s) with stuck key in current key matrix. The array value is obtained in the function “key_debounce_filter”.
Upper-layer processing is shown as below:
kb_event.keycode[0] = 0;
int det_key = kb_scan_key (0, 1);
if (det_key){
if(kb_event.cnt){ //key press
stuckKey_keyPressTime = clock_time() | 1;;
}
.......
}
For each button state update, when button press is detected (i.e. kb_event.cnt is non-zero value), the “stuckKey_keyPressTime” is used to record the time for the latest button press state.
Processing in the “blt_pm_proc” is shown as below:
#if (STUCK_KEY_PROCESS_ENABLE)
if(key_not_released && clock_time_exceed(stuckKey_keyPressTime, STUCK_KEY_ENTERDEEP_TIME*1000000)){
u32 pin[] = KB_DRIVE_PINS;
for (u8 i = 0; i < ARRAY_SIZE(pin); i ++)
{
extern u8 stuckKeyPress[];
if(stuckKeyPress[i])
continue;
cpu_set_gpio_wakeup (pin[i],0,1);
}
…… if(sendTerminate_before_enterDeep == 1){ //sending Terminate and wait for ack before enter deepsleep
if(user_task_flg){ //detect key Press again, can not enter deep now
sendTerminate_before_enterDeep = 0;
bls_ll_setAdvEnable(BLC_ADV_ENABLE); //enable ADV again
}
}
else if(sendTerminate_before_enterDeep == 2){ //Terminate OK
cpu_sleep_wakeup(DEEPSLEEP_MODE, PM_WAKEUP_PAD, 0); //deepSleep
}}#endif
Determine whether the time of the most recent key press has exceeded 60s continuously. If it exceeds, it is considered that the stuck key processing has occurred. According to the stuckKeyPress[] of the bottom layer, all the row numbers where the stuck key occurs are obtained, and the original high-level PAD wake-up deepsleep is changed to the low-level PAD wake-up deepsleep.
The reason for the modification is that when the key is pressed, the drive pin on the corresponding line reads a high level of 10/11 VCC. At this time, it is impossible to enter deepsleep because it is already high. As long as you enter deepsleep, it will immediately Wake up by this high level; after modifying it to low level, you can enter deepsleep normally, and when the button is released, the level of the drive pin on the row changes to a low level of 100K pull-down, releasing the button can wake up the entire MCU.
LED Management
LED Management Overview
The source code about LED management is available in vendor/common/blt_led.c of this BLE SDK for user reference. Users can directly include the vendor/common/blt_led.h into their C file.
The LED management module provides a simple yet effective LED control mechanism, supporting multiple blinking modes and priority management, suitable for status indication in various application scenarios.
LED Management Data Structures
led_cfg_t Structure
The structure used to configure LED event parameters:
typedef struct{
unsigned short onTime_ms; // LED on duration, unit: milliseconds
unsigned short offTime_ms; // LED off duration, unit: milliseconds
unsigned char repeatCount; // Number of repeats; 0xff indicates always on/always off
unsigned char priority; // Priority: 0x00 < 0x01 < 0x02 < ... < 0x80
} led_cfg_t;
device_led_t Structure
LED device status structure, for internal use:
typedef struct {
unsigned char isOn; // Current LED state: 1 indicates on, 0 indicates off
unsigned char polar; // LED polarity
unsigned char repeatCount; // Remaining repeat count
unsigned char priority; // Current event priority
unsigned short onTime_ms; // Current event duration (on)
unsigned short offTime_ms; // Current event duration (off)
unsigned int gpio_led; // GPIO connected to LED
unsigned int startTick; // Start timestamp
}device_led_t;
LED Management API Functions
device_led_init()
LED initialization function to set the corresponding GPIO and polarity.
void device_led_init(u32 gpio, u8 polarity);
Parameter Description:
gpio: GPIO pin connected to the LEDpolarity: LED polarity; 1 indicates the LED is on at high level; 0 indicates the LED is off at low level.
Usage Example:
// Initialize the LED connected to PA5, and it turns on at a high level.
device_led_init(GPIO_PA5, 1);
During initialization, use device_led_init(u32 gpio, u8 polarity) to set the GPIO and polarity for the current LED. Polarity set to 1 indicates that the GPIO output is high level to turn on the LED; polarity set to 0 indicates that the GPIO output is low level to turn on the LED.
device_led_setup()
Create a new LED event and configure the LED blinking mode.
int device_led_setup(led_cfg_t led_cfg);
Parameter Description:
led_cfg: LED event configuration parameters
Return Values:
- 0: New LED event priority is not higher than currently executing events; creation failed
- 1: New LED event created successfully
Usage Example:
// Create an LED event that blinks 3 times at 1Hz
led_cfg_t led_event = {500, 500, 3, 0x01};
device_led_setup(led_event);
device_led_process()
This is the LED processing function, which should be called within the main loop to manage LED states.
static inline void device_led_process(void);
Usage Example:
// Called within the main loop
int main(void) {
// ... Initialization code ...
while(1) {
// ... Other processing ...
device_led_process(); // Process LED state
// ... Other processing ...
}
}
The “device_led_process” function is added at UI Entry of main_loop. It’s used to check whether LED task is not finished (DEVICE_LED_BUSY). If yes, MCU will carry out corresponding LED task operation.
device_led_on_off()
This is an internal function that directly controls the LED's on/off state.
void device_led_on_off(u8 on);
Parameter Description:
on: Indicates LED status; 1 means on, 0 means off
LED Event Configuration
LED Event Definition
The following structure serves to define a LED event.
typedef struct{
unsigned short onTime_ms;
unsigned short offTime_ms;
unsigned char repeatCount;
unsigned char priority;
} led_cfg_t;
The unsigned short type “onTime_ms” and “offTime_ms” specify light on and off time (unit: ms) for current LED event, respectively. The two variables can reach the maximum value 65535.
The unsigned char type “repeatCount” specifies blinking times (i.e. repeat times for light on and off action specified by the “onTime_ms” and “offTime_ms”). The variable can reach the maximum value 255.
The priority indicates the priority level of the current LED event, with the hierarchy being 0x00 < 0x01 < 0x02 < 0x04 < 0x08 < 0x10 < 0x20 < 0x40 < 0x80, and so on.
To define a LED event when the LED always stays on/off, set the “repeatCount” as 255(0xff), set “onTime_ms”/“offTime_ms” as 0 or non-zero correspondingly.
LED event examples:
(1) Blink for 3s with 1Hz frequency: keep on for 500ms, stay off for 500ms, and repeat for 3 times.
led_cfg_t led_event1 = {500, 500, 3, 0x00};
(2) Blink for 50s with 4Hz frequency: keep on for 125ms, stay off for 125ms, and repeat for 200 times.
led_cfg_t led_event2 = {125, 125, 200, 0x00};
(3) Always on: onTime_ms is non-zero, offTime_ms is zero, and repeatCount is 0xff.
led_cfg_t led_event3 = {100, 0, 0xff, 0x00};
(4) Always off: onTime_ms is zero, offTime_ms is non-zero, and repeatCount is 0xff.
led_cfg_t led_event4 = {0, 100, 0xff, 0x00};
(5) Keep on for 3s, and then turn off: onTime_ms is 1000, offTime_ms is 0, and repeatCount is 0x3.
led_cfg_t led_event5 = {1000, 0, 3, 0x00};
The “device_led_setup” can be invoked to deliver a led_event to LED task management.
device_led_setup(led_event1);
LED Event Priority
User can define multiple LED events in the SDK, however, only a LED event is allowed to be executed at the same time.
No task list is set for the simple LED management: When LED is idle, LED will accept any LED event delivered by invoking the “device_led_setup”. When LED is busy with a LED event (old LED event), if another event (new LED event) comes, MCU will compare priority level of the two LED events; if the new LED event has higher priority level, the old LED event will be discarded and MCU starts to execute the new LED event; if the new LED event has the same or lower priority level, MCU continues executing the old LED event, while the new LED event will be completely discarded, rather than buffered.
By defining LED events with different priority levels, user can realize corresponding LED indicating effect.
Since inquiry scheme is used for LED management, MCU should not enter long suspend (e.g. 10ms * 50 = 500ms) with latency enabled and LED task ongoing (DEVICE_LED_BUSY); otherwise LED event with small onTime_ms value (e.g. 250ms) won’t be responded in time, thus LED blinking effect will be influenced.
#define DEVICE_LED_BUSY (device_led.repeatCount)
The corresponding processing is needed to add in blt_pm_proc, as shown below:
user_task_flg = ota_is_working || scan_pin_need || key_not_released || DEVICE_LED_BUSY;
if(user_task_flg){
bls_pm_setManualLatency(0); // manually disable latency
}
LED Management Enable Control
LED management functions can be enabled/disabled via macro definitions:
#ifndef BLT_APP_LED_ENABLE
#define BLT_APP_LED_ENABLE 0
#endif
When BLT_APP_LED_ENABLE is set to 0, all LED-related code is optimized away by the compiler and does not consume system resources.
Software Timer
Software Timer Overview
The Telink BLE SDK provides a complete software timer solution, delivered as source code that users can utilize directly or modify as needed. The software timer is suitable for simple timing tasks, particularly for applications requiring integration with low-power modes.
The source code resides in the files vendor/common/blt_soft_timer.c and vendor/common/blt_soft_timer.h. Before use, set the macro definition BLT_SOFTWARE_TIMER_ENABLE to 1:
#define BLT_SOFTWARE_TIMER_ENABLE 1 //enable or disable
The software timer is implemented based on the system clock (System Tick) and operates in an polling mode. While its precision is not as high as that of the hardware timer, it is sufficient for most application scenarios. It is recommended for scenarios where the timing interval is greater than 5ms and the requirement for time accuracy is not particularly stringent.
The key feature of the software timer is its seamless integration with low-power modes. It not only gets polled within the main loop but also ensures timely wake-up and execution of scheduled tasks after the system enters suspend state. This capability is achieved through the “application-layer periodic wake-up” mechanism.
The current design supports up to four timers running concurrently. Users can adjust the supported timer count by modifying the macro MAX_TIMER_NUM:
#define MAX_TIMER_NUM 4 //timer max number
Software Timer-Related Data Structures
blt_time_event_t Structure
This is the timer event structure used to store information about a single timer:
typedef struct blt_time_event_t {
blt_timer_callback_t cb; // Timer callback function
u32 t; // Next trigger time
u32 interval; // Timing interval
} blt_time_event_t;
blt_soft_timer_t Structure
This is the software timer management structure used to manage all timers:
typedef struct blt_soft_timer_t {
blt_time_event_t timer[MAX_TIMER_NUM]; // Timer array
u8 currentNum; // Number of currently active timers
} blt_soft_timer_t;
Timer Callback Function Type
typedef int (*blt_timer_callback_t)(void);
Callback function return value meanings:
- Return value < 0: Delete this timer
- Return value = 0: Maintain the current timing interval
- Return value > 0: Use the return value as the new timing interval (unit: microseconds)
Software Timer API Functions
blt_soft_timer_init()
This is the software timer initialization function, registering the timer handler as a callback function for early wake-up at the application layer.
void blt_soft_timer_init(void);
Usage Example:
// Call during application initialization
blt_soft_timer_init();
Source Implementation:
void blt_soft_timer_init(void)
{
bls_pm_registerAppWakeupLowPowerCb(blt_soft_timer_process);
}
blt_soft_timer_process()
This is the software timer processing function, which should be called within the main loop and also serves as the low-power wake-up callback function.
void blt_soft_timer_process(int type);
Parameter Description:
type: Call type, can be one of the following values:MAINLOOP_ENTRY(0): Called within the main loopCALLBACK_ENTRY(1): Called as a low-power wake-up callback
Usage Example:
_attribute_ram_code_ void main_loop(void)
{
tick_loop++;
#if (FEATURE_TEST_MODE == TEST_USER_BLT_SOFT_TIMER)
blt_soft_timer_process(MAINLOOP_ENTRY);
#endif
blt_sdk_main_loop();
}
Process Flow:
- Check if any timer tasks exist. If none, disable low-power wake-up and return.
- Check if the nearest timer has reached its trigger time.
- Iterate through all timers and execute the callback function for timers that have reached their trigger time.
- Update timer status based on callback function return values.
- Reorder the timer list (if changes occur)
- Set the next low-power wake-up time
blt_soft_timer_add()
This function is used to add new software timer tasks.
int blt_soft_timer_add(blt_timer_callback_t func, u32 interval_us);
Parameter Description:
func: Timer callback functioninterval_us: Timer interval in microseconds
Return Values:
- 0: Timer already full, addition failed
- 1: Addition successful
Usage Example:
// Add a 10ms timer
int my_timer_callback(void) {
// Timer processing logic
return 0; // Maintain current interval
}
blt_soft_timer_add(my_timer_callback, 10000); // 10ms = 10000us
blt_soft_timer_delete()
This function is used to delete the specified software timer task.
int blt_soft_timer_delete(blt_timer_callback_t func);
Parameter Description:
func: The timer callback function to delete
Return Values:
- 0: Deletion failed
- 1: Deletion successful
Usage Example:
// Delete the previously added timer
blt_soft_timer_delete(my_timer_callback);
blt_soft_timer_delete_by_index()
This function is used to delete a soft timer task by index.
int blt_soft_timer_delete_by_index(u8 index);
Parameter Description:
index: Index of the timer to be deleted
Return Value:
- 0: Deletion failed
- 1: Deletion successful
blt_soft_timer_get_first_tick()
This function is used to retrieve the trigger time of the first (most recent) timer in the timer list.
u32 blt_soft_timer_get_first_tick(void);
Return Value:
- 0: No timers
- Other values: Trigger time of the first timer
Software Timer Operation Principles
Timer Sorting Mechanism
Software timers employ a bubble sort algorithm to ensure the timer list is always sorted by trigger time from earliest to latest. This allows processing to check only the first timer to determine if any tasks require execution.
int blt_soft_timer_sort(void)
{
if(blt_timer.currentNum < 1 || blt_timer.currentNum > MAX_TIMER_NUM){
return 0;
}
else{
//BubbleSort
int n = blt_timer.currentNum;
u8 temp[sizeof(blt_time_event_t)];
for(int i=0;i<n-1;i++)
{
for(int j=0;j<n-i-1;j++)
{
if(TIME_COMPARE_BIG(blt_timer.timer[j].t, blt_timer.timer[j+1].t))
{
//swap
memcpy(temp, &blt_timer.timer[j], sizeof(blt_time_event_t));
memcpy(&blt_timer.timer[j], &blt_timer.timer[j+1], sizeof(blt_time_event_t));
memcpy(&blt_timer.timer[j+1], temp, sizeof(blt_time_event_t));
}
}
}
}
return 1;
}
Time Comparison Macros
Software timers use specialized time comparison macros to handle system clock overflows:
//if t1 < t2 return 1
#define TIME_COMPARE_SMALL(t1,t2) ( (u32)((t2) - (t1)) < BIT(30) )
// if t1 > t2 return 1
#define TIME_COMPARE_BIG(t1,t2) ( (u32)((t1) - (t2)) < BIT(30) )
Timer Expiration Check
static inline int blt_is_timer_expired(u32 t, u32 now) {
return ((u32)(now + BLT_TIMER_SAFE_MARGIN_PRE - t) < BLT_TIMER_SAFE_MARGIN_POST);
}
This function employs safety margins to handle timer expiration checks, preventing misjudgments caused by system clock inaccuracies.
Software Timers and Low-Power Management
A key feature of software timers is their tight integration with low-power modes. Through application-layer periodic wake-up mechanisms, timer tasks execute on schedule even when the system enters low-power states.
Low-Power Wakeup Configuration
When timer tasks exist and the trigger time of the nearest timer falls within 5 seconds, the system configures application-layer periodic wakeups:
if( (u32)(blt_timer.timer[0].t - now) < 5000 * SYSTEM_TIMER_TICK_1MS){
bls_pm_setAppWakeupLowPower(blt_timer.timer[0].t, 1);
}
else{
bls_pm_setAppWakeupLowPower(0, 0); //disable
}
Timer Handling in Low-Power Mode
The feature_misc example demonstrates how to use software timers in low-power mode:
#if (FEATURE_TEST_MODE == TEST_USER_BLT_SOFT_TIMER)
if(blc_ll_getCurrentState() == BLS_LINK_STATE_IDLE &&
(clock_time_exceed(blt_soft_timer_get_first_tick(),(clock_time()+ 3 *SYSTEM_TIMER_TICK_1MS))) )
{
// Enter SUSPEND_MODE and set timer wakeup
cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_PAD|PM_WAKEUP_TIMER, blt_soft_timer_get_first_tick());
// Reinitialize BLE after wakeup
rf_drv_ble_init();
}
#endif
Complete Usage Example
The following demonstrates the complete usage of the software timer based on the sample code in feature_misc:
Define Timer Callback Function
/**
* @brief Timer callback function example 0
* @return 0 - Maintain current timing interval
*/
int gpio_test0(void)
{
// Toggle GPIO 0; waveform observable via oscilloscope
DBG_CHN3_TOGGLE;
return 0; // Maintain current timer interval
}
/**
* @brief Timer callback function example 1
* @return 7000 or 17000 - Dynamically change timer interval
*/
_attribute_data_retention_ static u8 timer_change_flg = 0;
int gpio_test1(void)
{
// Toggle GPIO 1
DBG_CHN4_TOGGLE;
// Dynamically change timing interval
timer_change_flg = !timer_change_flg;
if(timer_change_flg){
return 7000; // 7ms
}
else{
return 17000; // 17ms
}
}
/**
* @brief Timer callback function example 2
* @return 0 - Maintain current timing interval
*/
int gpio_test2(void)
{
// Toggle GPIO 2
DBG_CHN5_TOGGLE;
// Timer deletion logic can be added here
// if(clock_time_exceed(0, 5000000)){
// return -1; // Delete timer after 5 seconds
// }
return 0; // Maintain current timing interval
}
/**
* @brief Timer callback function example 3
* @return 0 - Maintain current timing interval
*/
int gpio_test3(void)
{
// Toggle GPIO 3
DBG_CHN6_TOGGLE;
return 0; // Maintain current timing interval
}
Initialize Software Timer
void user_init_normal(void)
{
// ... Other initialization code ...
// Initialize software timer
blt_soft_timer_init();
// Add timer tasks
blt_soft_timer_add(&gpio_test0, 23000); // 23ms
blt_soft_timer_add(&gpio_test1, 7000); // Initial 7ms, later cycles between 7ms and 17ms
blt_soft_timer_add(&gpio_test2, 13000); // 13ms
blt_soft_timer_add(&gpio_test3, 27000); // 27ms
// ... Other initialization code ...
}
Handle Timers in Main Loop
void main_loop(void)
{
// Process software timers
blt_soft_timer_process(MAINLOOP_ENTRY);
// BLE protocol stack processing
blt_sdk_main_loop();
// Other application logic
// ...
}
Advanced Application Scenarios
One-Shot Timer
int one_shot_timer_callback(void) {
// Execute one-time task
do_something();
// Return negative value to automatically delete timer
return -1;
}
// Add one-shot timer
blt_soft_timer_add(one_shot_timer_callback, 500000); // Execute once after 500ms
Periodic Task Execution
int periodic_task_callback(void) {
// Execute periodic task
do_periodic_task();
// Return 0 to maintain current cycle
return 0;
}
// Add periodic timer
blt_soft_timer_add(periodic_task_callback, 1000000); // Execute once per second
Dynamically Adjust Timer Period
int adaptive_timer_callback(void) {
// Dynamically adjust next execution interval based on conditions
if(condition_met) {
return 500000; // 500ms
} else {
return 2000000; // 2s
}
}
// Add adaptive timer
blt_soft_timer_add(adaptive_timer_callback, 1000000); // Initialized to 1s
Notes
-
Time Accuracy: Software timers rely on the system clock, with precision limited by the system timer's resolution. They are unsuitable for high-precision timing requirements.
-
Resource Usage: Each timer occupies RAM space. The maximum number of timers can be adjusted via the
MAX_TIMER_NUMmacro. -
Low-Power Compatibility: Software timers are compatible with low-power modes, but frequent timer wake-ups increase power consumption.
-
Callback Execution Time: Timer callback functions should be kept brief to avoid blocking other tasks for extended periods.
-
Interrupt Safety: Timer callback functions are safe to call within interrupt contexts.
-
System Clock Overflow: Software timers use specialized time comparison macros to handle system clock overflow conditions.
-
Timer Quantity Limit: Supports up to 4 timers by default; adjustable by modifying the
MAX_TIMER_NUMmacro. -
Timer Deletion: Returning a negative value in the callback function automatically deletes the timer; manual deletion is also possible via
blt_soft_timer_delete. -
Low-Power Wake-Up: When a timer triggers within 5 seconds, the system initiates low-power wake-up to ensure timely execution.
-
Sorting Mechanism: Timer lists are always sorted by trigger time to ensure efficient processing.
PWM and IR
PWM Driver
By operating registers, hardware configurations for PWM are very simple. To improve execution efficiency and save code size, related APIs, implemented via “static inline function”, are defined in the “pwm.h”.
PWM ID and Pin
The B85/B87/TC321x chips support up to 12-channel PWM: PWM0 ~ PWM5 and PWM0_N ~ PWM5_N.
Six-channel PWM is defined in driver:
typedef enum {
PWM0_ID = 0,
PWM1_ID,
PWM2_ID,
PWM3_ID,
PWM4_ID,
PWM5_ID,
}pwm_id;
Only six channels PWM0~PWM5 are configured in software, while the other six channels PWM0_N~PWM5_N are inverted output of PWM0~PWM5 waveform. For example: PWM0_N is inverted output of PWM0 waveform. When PWM0 is high level, PWM0_N is low level; When PWM0 is low level, PWM0_N is high level. Therefore, as long as PWM0~PWM5 are configured, PWM0_N~PWM5_N are also configured.
IC pins corresponding to 12-channel PWM are shown as below:
PWM Pin Assignments for B85/B87 Series Chips
| PWMx | Pin | PWMx_n | Pin |
|---|---|---|---|
| PWM0 | PA2/PC1/PC2/PD5 | PWM0_N | PA0/PB3/PC4/PD5 |
| PWM1 | PA3/PC3 | PWM1_N | PC1/PD3 |
| PWM2 | PA4/PC4 | PWM2_N | PD4 |
| PWM3 | PB0/PD2 | PWM3_N | PC5 |
| PWM4 | PB1/PB4 | PWM4_N | PC0/PC6 |
| PWM5 | PB2/PB5 | PWM5_N | PC7 |
PWM Pin Assignments for TC321x Series Chips
| PWMx | Pin | PWMx_n | Pin |
|---|---|---|---|
| PWM0~5 | PA0/PA2/PA5/PA7/PB2/PB4/PB6/PC0/ PC2/PC4/PC6/PD0/PD2/PD6 |
PWM0~5_N | PA1/PA4/PA6/PB0/PB5/PB7/PC1/ PC3/PC5/PC7/PD1/PD3/PD5/PD7 |
The “void gpio_set_func(GPIO_PinTypeDef pin, GPIO_FuncTypeDef func)” serves to set specific pin as PWM function.
“pin”: GPIO pin corresponding to actual PWM channel
“func”: should be selected from AS_PWM0 ~ AS_PWM5_N (B85/B87) or PWM0 ~ PWM5_N (TC321x) as defined in GPIO_FuncTypeDef, according to the actual PWM functionality of the GPIO shown in the table above, as illustrated below.
For example, to use PA2 as PWM0:
gpio_set_func(GPIO_PA2, AS_PWM0); // The B85/87 series chips use PA2 as PWM0.
// or
gpio_set_func(GPIO_PA2, PWM0); // The TC321x series chips use PA2 as PWM0.
PWM Clock
Use API void pwm_set_clk (int system_clock_hz, int pwm_clk) to set the PWM clock.
The system_clock_hz fills in the current system clock CLOCK_SYS_CLOCK_HZ (this macro is defined in app_config.h);
The pwm_clk is the clock to be set, system_clock_hz should be divisible by pwm_clk to get the correct PWM clock.
The PWM clock needs to be as large as possible to make the PWM waveform accurate, the maximum value cannot exceed the system clock. It is recommended to set pwm_clk to CLOCK_SYS_CLOCK_HZ, that is:
pwm_set_clk(CLOCK_SYS_CLOCK_HZ, CLOCK_SYS_CLOCK_HZ);
For example, the current system clock CLOCK_SYS_CLOCK_HZ is 16000000, and the PWM clock set above is equal to the system clock, which is 16M.
If you want the PWM clock to be 8M, you can set it as follows: no matter how the system clock changes (CLOCK_SYS_CLOCK_HZ is 16000000, 24000000 or 32000000), the PWM clock is 8M.
pwm_set_clk(CLOCK_SYS_CLOCK_HZ, 8000000);
PWM Cycle and Duty
The basic unit of the PWM waveform is the PWM Signal Frame.
Configuring a PWM Signal Frame requires setting both the cycle and cmp parameters.
void pwm_set_cycle (pwm_id id, unsigned short cycle_tick) is used to set the PWM cycle, the unit is the number of PWM clocks.
void pwm_set_cmp (pwm_id id, unsigned short cmp_tick) is used to set the PWM cmp, the unit is the number of PWM clock.
The following API combines the above two APIs into one, which can improve the efficiency of settings.
void pwm_set_cycle_and_duty(pwm_id id, unsigned short cycle_tick, unsigned short cmp_tick)
Then for a PWM signal frame, calculate the PWM duty:
PWM duty = PWM cmp/PWM cycle
The figure below is equivalent to the result obtained by pwm_set_cycle_and_duty (PWM0_ID, 5, 2). The cycle of a Signal Frame is 5 PWM clocks, the high level time is 2 PWM clocks, and the PWM duty is 40%.

For PWM0 ~ PWM5, the hardware will automatically put the high level in the front and the low level in the back. If you want the low level first, there are several ways:
1) Use the corresponding PWM0_N ~ PWM5_N, which is the negative of PWM0 ~ PWM5.
2) Use the API static inline void pwm_revert (pwm_id id) to invert the PWM0 ~ PWM5 waveforms directly.
For example, the current pwm clock is 16MHz, you need to set the PWM period of 1ms, duty cycle of 50% of the PWM0 a frame method as follows:
pwm_set_cycle(PWM0_ID , 16000)
pwm_set_cmp (PWM0_ID , 8000)
or
pwm_set_cycle_and_duty(PWM0_ID, 16000, 8000);
PWM Revert
The following API serves to invert PWM0~PWM5 waveform.
void pwm_revert (pwm_id id)
The following API serves to invert PWM0_N ~ PWM5_N waveform.
void pwm_n_revert (pwm_id id)
PWM Start and Stop
The following 2 APIs serve to enable/disable specified PWM.
void pwm_start(pwm_id id) ;
void pwm_stop(pwm_id id) ;
PWM Mode
PWM supports 5 modes: Normal mode (Continuous mode), while only PWM0 supports Counting mode, IR mode, IR FIFO mode and IR DMA FIFO mode, defined as following:
typedef enum{
typedef enum{
PWM_NORMAL_MODE = 0x00,
PWM_COUNT_MODE = 0x01,
PWM_IR_MODE = 0x03,
PWM_IR_FIFO_MODE = 0x07,
PWM_IR_DMA_FIFO_MODE = 0x0F,
}pwm_mode;
PWM0 supports all 5 modes, Normal mode (Continuous mode), while only PWM0 supports Counting mode, IR mode, IR FIFO mode and IR DMA FIFO mode, PWM1~PWM5 supports only normal mode.
PWM Pulse Number
The API below serves to set pulse number, i.e. number of Signal Frames, for output waveform of specified PWM channel.
void pwm_set_pulse_num(pwm_id id, unsigned short pulse_num)
This API is only used for Counting mode, IR mode, IR FIFO mode and IR DMA FIFO mode, but not applies to Normal mode with continuous pulses. Normal mode (Continuous mode) is not limited by the number of Signal Frames, so this API is not relevant for Normal mode.
PWM Interrupt
First introduce some basic concepts of Telink MCU interrupt.
The interrupt "status" is a status marker bit generated by a specific action of the hardware (i.e. interrupt action). It does not depend on any software setting, no matter whether the interrupt "mask" is on or not, as soon as the interrupt action occurs, "status" will be set (value is 1). This "status" can be cleared by writing a 1 to "status" (the value returns to 0).
Define the concept of interrupt response: interrupt response means that after the hardware interrupt action is generated, the software program pointer PC jumps to irq_handler for related processing.
If the user wants the interrupt to be responded to, he needs to make sure that all the "masks" corresponding to the current interrupt are turned on. There may be more than one "mask", and the relationship between multiple "masks" is a logical "with" relationship. Only when all "masks" are turned on, the interrupt "status" will trigger the final interrupt response and the MCU will jump to irq_handler to execute; as long as one "mask "is not turned on, the interrupt "status" will not be generated to trigger the interrupt response.
The interrupts that users may need to use in the PWM driver are as follows (code is in the file register_8258.h). Other interrupts are not needed, and users don't need to pay attention.
#define reg_pwm_irq_mask REG_ADDR8(0x7b0)
#define reg_pwm_irq_sta REG_ADDR8(0x7b1)
enum{
FLD_IRQ_PWM0_PNUM = BIT(0),
FLD_IRQ_PWM0_IR_DMA_FIFO_DONE = BIT(1),
FLD_IRQ_PWM0_FRAME = BIT(2),
FLD_IRQ_PWM1_FRAME = BIT(3),
FLD_IRQ_PWM2_FRAME = BIT(4),
FLD_IRQ_PWM3_FRAME = BIT(5),
FLD_IRQ_PWM4_FRAME = BIT(6),
FLD_IRQ_PWM5_FRAME = BIT(7),
};
The above 8 interrupts correspond to BIT<0:7> of core_7b0/7b1. core_7b0 is the "mask" of these 8 interrupts, and core_7b1 is the "status" of the 8 interrupts.
Divide the 8 interrupt "status" into 3 types. Refer to the figure below, assume PWM0 is operating in IR mode, the duty cycle of PWM Signal Frame is 50%, and the pulse number (or Signal Frame number) of each IR task is 3.

1) First type: IRQ_PWMn_FRAME (n=0,1,2,3,4,5)
The latter 6 interrupt sources are the same interrupt, which are generated on PWM0~PWM5 respectively.
As shown in the figure, IRQ_PWMn_FRAME is an interrupt generated after each PWM Signal Frame ends. In the five modes of PWM, Signal Frame is the basic unit of PWM waveform. So no matter which PWM mode, IRQ_PWMn_FRAME will appear.
2) Second type: IRQ_PWM0_PNUM
IRQ_PWM0_PNUM is an interrupt generated at the end of a Signal Frame (the number is determined by API pwm_set_pulse_num). In the figure, one IRQ_PWM0_PNUM is generated after every three Signal Frames.
The Counting mode and IR mode of PWM will use API pwm_set_pulse_num. Therefore, only the counting mode and IR mode of PWM0 will generate IRQ_PWM0_PNUM.
3) Third type:IRQ_PWM0_IR_DMA_FIFO_DONE
When PWM0 is operating in IR DMA FIFO mode, IRQ_PWM0_IR_DMA_FIFO_DONE is triggered when all configured PWM waveforms on the DMA have been sent.
As mentioned above, the interrupt response will be triggered when all related interrupt "masks" are turned on at the same time. For PWM interrupts, taking FLD_IRQ_PWM0_PNUM as an example, there are 3 layers of "masks" that need to be turned on:
1) “mask” of FLD_IRQ_PWM0_PNUM
That is the "mask" corresponding to core_7b0, can be opened as follows:
reg_pwm_irq_mask |= FLD_IRQ_PWM0_PNUM;
Generally, clear the previous status before opening the mask to prevent the interrupt response from being triggered by mistake:
reg_pwm_irq_sta = FLD_IRQ_PWM0_PNUM;
2) PWM “mask” on MCU system interrupt
That is, BIT<14> of core_640.
#define reg_irq_mask REG_ADDR32(0x640)
enum{
……
FLD_IRQ_SW_PWM_EN = BIT(14), //irq_software | irq_pwm
……
};
Open as follows:
reg_irq_mask |= FLD_IRQ_SW_PWM_EN;
3) MCU total interrupt enable bit, irq_enable().
PWM phase
void pwm_set_phase(pwm_id id, unsigned short phase)
It is used to set the delay time before the PWM starts.
The phase is the delay time, and the unit is the number of PWM clocks. Generally, there is no need to delay, set to 0.
IR DMA FIFO mode
IR DMA FIFO mode is to write configuration data to FIFO through DMA. Each FIFO uses 2 bytes to represent a PWM waveform. When the DMA data buffer takes effect, the PWM hardware module will sent out PWM waveform 1, waveform 2 Waveform n in chronological order continuously, when fifo finishes executing the cfg_data sent by DMA, it triggers the interrupt IRQ_PWM0_IR_DMA_FIFO_DONE.
Configuration for DMA FIFO
On each DMA FIFO, use 2bytes (16 bits) to configure a PWM waveform. Call the following API to return 2 bytes of DMA FIFO data.
unsigned short pwm_config_dma_fifo_waveform(int carrier_en,
Pwm0Pulse_SelectDef pulse, unsigned short pulse_num);
The API has three parameters: "carrier_en", "pulse" and "pulse_num". The configured PWM waveform is a collection of "pulse_num" PWM signal frames.
BIT(15) determines the format of Signal Frame, the basic unit of the current PWM waveform, corresponding to the "carrier_en" in the API:
-
When "carrier_en" is 1, the high pulse in the Signal Frame is effective;
-
When "carrier_en" is 0, the signal frame is all 0 data, and the high pulse is invalid.
"Pulse_num" is the number of Signal Frames in the current PWM waveform.
"pulse" can choose the following two definitions.
typedef enum{
PWM0_PULSE_NORMAL = 0,
PWM0_PULSE_SHADOW = BIT(14),
}Pwm0Pulse_SelectDef;
When "pulse" is PWM0_PULSE_NORMAL, the Signal Frame comes from the configuration of API pwm_set_cycle_and_duty; when "pulse" is PWM0_PULSE_SHADOW, the Signal Frame comes from the configuration of PWM shadow mode.
The purpose of PWM shadow mode is to add a set of signal frame configuration, thereby adding more flexibility to the PWM waveform configuration of IR DMA FIFO mode. The configuration API is as follows, and the method is exactly the same as API pwm_set_cycle_and_duty.
void pwm_set_pwm0_shadow_cycle_and_duty(unsigned short cycle_tick,
unsigned short cmp_tick);
Set DMA FIFO Buffer
After DMA FIFO buffer is configured, the API below should be invoked to set the starting address of the buffer to DMA module.
void pwm_set_dma_address(void * pdat);
Start and Stop for IR DMA FIFO Mode
After DMA FIFO buffer is prepared, the API below should be invoked to start sending PWM waveforms.
void pwm_start_dma_ir_sending(void);
After all PWM waveforms in DMA FIFO buffer are sent, the PWM module will be stopped automatically. The API below can be invoked to manually stop the PWM module in advance.
void pwm_stop_dma_ir_sending(void);
PWM Differences Across Chip Series
| Feature | B85 Series | B87 Series | TC321x Series |
|---|---|---|---|
| PWM Channels | 6 | 6 | 6 |
| IR Mode Support | Supported | Supported | Supported |
| Shadow Register | Supported | Supported | Supported |
| DMA FIFO | Supported | Supported | Enhanced |
| Interrupt Type | Basic | Basic | Extended |
Enhanced PWM Features in TC321x Series
The TC321x series adds the following enhanced features to the original PWM functionality:
(1) Precision Clock Control: Supports more accurate clock division to improve PWM output precision
(2) Enhanced DMA FIFO:
- Supports PingPong DMA mode for continuous, uninterrupted PWM waveform output
- Added a DMA address configuration API to support larger data buffers
void pwm_set_dma_address(void * pdat);
void pwm_set_dma_address_pingpong(void * pdat1, void * pdat2);
void pwm_dma_pingpong_chain_start(void);
(3) Optimized interrupt handling: Enhanced interrupt response mechanism for improved real-time performance
(4) Analog output control:
void pwm_set_pwm0_output_to_ana_ir_en(void);
void pwm_set_pwm0_output_to_ana_ir_dis(void);
Demonstration Notes
User can refer to the code of IR in SDK demo "ble_remote" and set the macro REMOTE_IR_ENABLE in app_config.h to 1.
PWM mode selection
As required by IR transmission, PWM output needs to switch at specific time with small error tolerance of switch time accuracy to avoid incorrect IR.
As described in Link Layer timing sequence (section 3.2.4), Link Layers uses system interrupt to process brx event. (In the new SDK, ADV event is processed in the main_loop and does not occupy system interrupt time.) When IR is about to switch PWM output soon, if brx event related interrupt comes first and occupies MCU time, the time to switch PWM output may be delayed, thus to result in IR error. Therefore IR cannot use PWM Normal mode.
The B85 family introduces an extra IR DMA FIFO mode. In IR DMA FIFO mode, since FIFO can be defined in SRAM, more FIFOs are available, which can effectively solve the shortcoming of PWM IR mode above.
The IR DMA FIFO mode supports pre-storage of multiple PWM waveforms into SRAM. Once DMA is started, no software involvement is needed. This can save frequent SW processing time, and avoid PWM waveform delay caused by simultaneous response to multiple IRQs in interrupt system.
Only PWM0 with IR DMA FIFO mode can be used to implement IR. Therefore, in HW design, IR control GPIO should be PWM0 pin or PWM0_n pin.
Demo IR Protocol
The figure below shows demo IR protocol in the SDK.

IR Timing Design
The figure below shows basic IR timing abased demo IR protocol and feature of IR DMA FIFO mode.
In IR DMA FIFO mode, a complete task is defined as FifoTask. Herein the processing of IR repeat signal adopts the method of “add repeat one by one”, i.e. the macro below is defined as 1.
#define ADD_REPEAT_ONE_BY_ONE 1

When a button is pressed to trigger IR transmission, IR is disassembled to FifoTasks as shown in the figure above.
(1) After IR is started, run FifoTask_data to send valid data. The duration of FifoTask_data, marked as T_data, is not certain due to the uncertainty of data. After FifoTask_data is finished, trigger IRQ_PWM0_IR_DMA_FIFO_DONE.
(2) In interrupt function of IRQ_PWM0_IR_DMA_FIFO_DONE, start FifoTask_idle phase to send signal without carrier and it lasts for a duration of (110ms – T_data). This phase is designed to guarantee the time point the first FifoTask_repeat is 110ms later after IR is started. After FifoTask_idle is finished, trigger IRQ_PWM0_IR_DMA_FIFO_DONE.
(3) In interrupt function of IRQ_PWM0_IR_DMA_FIFO_DONE, start the first FifoTask_repeat. Each FifoTask_repeat lasts for 110ms. By adding FifoTask_repeat in corresponding interrupt function, IR repeat signals can be sent continuously.
(4) The time point to stop IR is not certain, and it depends on the time to release the button. After the APP layer detects key release, as long as FifoTask_data is correctly completed, IR transmission is finished by manually stopping IR DMA FIFO mode.
Following shows some optimization steps for the IR timing design above.
(1) Since FifoTask_repeat timing is fixed, and there are many DMA fifos in IR DMA FIFO mode, multiple FifoTask_repeat of 110ms can be assembled into one FifoTask_repeat*n, so as to reduce the number of times to process IRQ_PWM0_IR_DMA_FIFO_DONE in SW. Corresponding to the processing of “ADD_REPEAT_ONE_BY_ONE” macro defined as 0, the Demo herein assembles five IR repeat signals into one FifoTask_repeat*5. User can further optimize it according to the usage of DMA fifos.
(2) Based on step 1, combine FifoTask_idle and the first “FifoTask_repeat*n” to form “FifoTask_idle_repeat*n”.
The figure below shows IR timing after optimization.

As per the IR timing design above, corresponding code in SW flow is shown as below:
At IR start, invoke the function “ir_nec_send”, enable FifoTask_data, and use interrupt to control the following flow. In the interrupt when FifoTask_data is finished, enable FifoTask_idle. In the interrupt when FifoTask_idle is finished, enable FifoTask_repeat. Before manually stopping IR DMA FIFO mode, FifoTask_repeat is executed continually.
void ir_nec_send(u8 addr1, u8 addr2, u8 cmd)
{
if(ir_send_ctrl.last_cmd != cmd)
{
if(ir_sending_check())
{
return;
}
ir_send_ctrl.last_cmd = cmd;
// set waveform input in sequence //////
T_dmaData_buf.data_num = 0;
//waveform for start bit
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_2nd;
//add data
u32 data = (~cmd)<<24 | cmd<<16 | addr2<<8 | addr1;
for(int i=0;i<32;i++){
if(data & BIT(i)){
//waveform for logic_1
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_2nd;
}
else{
//waveform for logic_0
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_2nd;
}
}
//waveform for stop bit
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_2nd;
T_dmaData_buf.dma_len = T_dmaData_buf.data_num * 2;
if(1){ //need repeat
ir_send_ctrl.repeat_enable = 1; //need repeat signal
}
else{ //no need repeat
ir_send_ctrl.repeat_enable = 0; //no need repeat signal
}
reg_pwm_irq_sta = FLD_IRQ_PWM0_IR_DMA_FIFO_DONE; //clear dma fifo mode done irq status
reg_pwm_irq_mask |= FLD_IRQ_PWM0_IR_DMA_FIFO_DONE; //enable dma fifo mode done irq mask
ir_send_ctrl.is_sending = IR_SENDING_DATA;
ir_send_ctrl.sending_start_time = clock_time();
pwm_start_dma_ir_sending();
}
}
IR Initialization
rc_ir_init
IR initialization function is shown as below. Please refer to demo code in the SDK.
void rc_ir_init(void)
IR Hardware Configuration
Following shows the demo code.
pwm_n_revert(PWM0_ID);
gpio_set_func(GPIO_PB3, AS_PWM0_N);
pwm_set_mode(PWM0_ID, PWM_IR_DMA_FIFO_MODE);
pwm_set_phase(PWM0_ID, 0); //no phase at pwm beginning
pwm_set_cycle_and_duty(PWM0_ID, PWM_CARRIER_CYCLE_TICK,
PWM_CARRIER_HIGH_TICK );
pwm_set_dma_address(&T_dmaData_buf);
reg_irq_mask |= FLD_IRQ_SW_PWM_EN;
reg_pwm_irq_sta = FLD_IRQ_PWM0_IR_DMA_FIFO_DONE;
Only PWM0 supports ID DMA FIFO mode, so choose PB3 corresponding to PWM0_N.
The Demo IR carrier frequency is 38K, the cycle is 26.3uS, and the duty is 1/3. Use API pwm_set_tmax and pwm_set_tcmp and configure the cycle and duty. In Demo IR, there are no multiple different carrier frequencies. This 38K carrier is sufficient for all FifoTask configurations. So there is no need to use PWM shadow mode.
DMA FIFO buffer is T_dmaData_buf。
Turn on the system interrupt mask "FLD_IRQ_SW_PWM_EN".
Clear interrupt status "FLD_IRQ_PWM0_IR_DMA_FIFO_DONE".
IR Variable Initialization
Related variables in the SDK demo includes waveform_start_bit_1st, waveform_start_bit_2nd, and etc.
As introduced in IR timing design, FifoTask_data and FifoTask_repeat should be configured.
Start signal = 9ms carrier signal + 4.5ms low level signal (no carrier). The “pwm_config_dma_fifo_waveform” is invoked to configure the two corresponding DMA FIFO data.
//start bit, 9000 us carrier, 4500 us low
waveform_start_bit_1st = pwm_config_dma_fifo_waveform(1, PWM0_PULSE_NORMAL, 9000 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
waveform_start_bit_2nd = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, 4500 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
The method also applies to configure stop signal, repeat signal, data logic “1” signal, and data logic “0” signal.
FifoTask Configuration
FifoTask_data
As per demo IR protocol, to send a cmd (e.g. 7), first send start signal, i.e. 9ms carrier signal + 4.5ms low level signal (no carrier); then send “address+ ~address+ cmd + ~cmd”. In the demo code, address is 0x88.
When sending the final bit of “~cmd”, logical “0” or logical “1” always contains some non-carrier signals at the end. If “~cmd” is not followed by any data, there may be a problem on Rx side: Since there’s no boundary to differentiate carrier, the FW does not know whether the non-carrier signal duration of the final bit is 560us or 1690us, and fails to recognize whether it’s logical “0” or logical “1”.
To solve this problem, the Data signal should be followed by a “stop” signal which is defined as 560us carrier signal + 500us non-carrier signal.
Thus, the FifoTask_data mainly contains the three parts below:
(1) start signal: 9ms carrier signal + 4.5ms low level signal (no carrier)
(2) data signal: address+ ~address+ cmd + ~cmd
(3) stop signal: 560us carrier signal + 500us non-carrier signal
According to the above 3 signals, configure the Dma FIfo buffer to start the IR transmission, which is partially implemented in the ir_nec_send function, where part of the relevant code is as follows.
// set waveform input in sequence
T_dmaData_buf.data_num = 0;
//waveform for start bit
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_start_bit_2nd;
//add data
u32 data = (~cmd)<<24 | cmd<<16 | addr2<<8 | addr1;
for(int i=0;i<32;i++){
if(data & BIT(i)){
//waveform for logic_1
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_1_2nd;
}
else{
//waveform for logic_0
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_logic_0_2nd;
}
}
//waveform for stop bit
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_stop_bit_2nd;
T_dmaData_buf.dma_len = T_dmaData_buf.data_num * 2;
FifoTask_idle
As introduced in IR timing design, FifoTask_idle lasts for the duration “110mS – T_data”. Record the time when FifoTask_data starts:
ir_send_ctrl.sending_start_time = clock_time();
Then calculate FifoTask_idle time in the interrupt triggered when FifoTask_data is finished:
110mS – (clock_time() - ir_send_ctrl.sending_start_time)
Demo code:
u32 tick_2_repeat_sysClockTimer16M = 110*CLOCK_16M_SYS_TIMER_CLK_1MS - (clock_time() ir_send_ctrl.sending_start_time);
u32 tick_2_repeat_sysTimer = (tick_2_repeat_sysClockTimer16M*CLOCK_PWM_CLOCK_1US>>4);
Please pay attention to the switch of time unit. As introduced in Clock module, System Timer frequency used in software timer is fixed as 16MHz. Since PWM clock is derived from system clock, user needs to consider the case with system clock rather than 16MHz (e.g. 24MHz, 32MHz).
FifoTask_idle does not send PWM waveform, which can be considered to continually send non-carrier signal. It can be implemented by setting the first parameter “carrier_en” of the API “pwm_config_dma_fifo_waveform” as 0.
waveform_wait_to_repeat = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, tick_2_repeat_sysTimer/PWM_CARRIER_CYCLE_TICK);
FifoTask_repeat
As per Demo IR protocol, repeat signal is 9ms carrier signal + 2.25ms non-carrier signal.
Similar to the processing of FifoTask_data, the end of repeat signal should be followed by 560us carrier signal as stop signal.
As introduced in IR timing design, repeat signal lasts for 110ms, so the duration of non-carrier signal after the 560us carrier signal should be:
110ms – 9ms – 2.25ms – 560us = 99190us
The code below shows the configuration for a complete repeat signal.
//repeat signal first part, 9000 us carrier, 2250 us low
waveform_repeat_1st = pwm_config_dma_fifo_waveform(1, PWM0_PULSE_NORMAL, 9000 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
waveform_repeat_2nd = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, 2250 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
//repeat signal second part, 560 us carrier, 99190 us low
waveform_repeat_3rd = pwm_config_dma_fifo_waveform(1, PWM0_PULSE_NORMAL, 560 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
waveform_repeat_4th = pwm_config_dma_fifo_waveform(0, PWM0_PULSE_NORMAL, 99190 * CLOCK_SYS_CLOCK_1US/PWM_CARRIER_CYCLE_TICK);
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_1st;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_2nd;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_3rd;
T_dmaData_buf.data[T_dmaData_buf.data_num ++] = waveform_repeat_4th;
FifoTask_repeat*n and FifoTask_idle_repeat*n
By simple superposition in DMA Fifo buffer, “FifoTask_repeat*n” and “FifoTask_idle_repeat*n” can be implemented on the basis of FifoTask_idle and FifoTask_repeat.
Check IR Busy Status in APP Layer
In the Application layer, user can use the variable “ir_send_ctrl.is_sending” to check whether IR is busy sending data or repeat signal.
The following shows the determination of whether IR is busy in power management. When IR is busy, MCU cannot enter suspend.
if( ir_send_ctrl.is_sending)
{
bls_pm_setSuspendMask(SUSPEND_DISABLE);
}
IR Learn
IR Learn Introduction
The IR Learn function is designed to receive and analyze infrared remote control signals, enabling the learning and replication capabilities of infrared remote controllers. In the B85/B87/TC321x series chips, the IR Learn module can precisely capture the timing characteristics of infrared signals, providing support for infrared remote control applications. After learning, the relevant data is stored in RAM/FLASH. Subsequently, the infrared transmitter utilizes its transmission capabilities to send out the learned waveforms.
IR Learn Hardware Principle
The hardware circuit of IR learn is shown as below.

When in the IR learning state, IR_OUT and IR_CONTROL pin should be set to GPIO function and pulled low at the same time, then Q2 and Q3 will be in the cutoff state, IR_IN level is high when there is no waveform, and then will follow the waveform received by the triode and change: when the input waveform is high, IR_IN is pulled low, on the contrary IR_IN back to high. IR learning also takes advantage of this feature, using GPIO low level trigger to complete the learning algorithm, which will be described in detail later. As shown in the figure below, the transmitter is using NEC format IR, and the waveform of IR_IN is captured as shown in the figure below.

The dark part is the carrier waveform and the white part is the non-carrier waveform. The waveform of the amplified carrier part is shown in the figure below, which is high when no IR signal is received in front, and IR_IN is pulled down when a signal is received. The IR_IN low level is 9.35us and the period is 26.4us, which is converted to a carrier frequency of 37.88kHz. This matches the NEC protocol carrier of 38kHz with a duty cycle of 1/3.

IR Learn software principle
During IR learning, the chip will set and enable the IR_IN trigger interrupt at falling edge. Every time it receives an IR carrier waveform from its device, IR_IN will be pulled low and trigger the interrupt. In the interrupt, the timing, number of waveforms, and carrier period of the carrier and non-carrier waveforms will be recorded by the algorithm, and the waveforms will be copied and sent out according to the above information when sending.
The following figure shows the order of recording during interrupt processing, the duration of the carrier/non-carrier part shown in the previous 1, 2... will be recorded in the buff. At the same time, the duration of a certain number of single carriers constituting 1 is recorded, and the carrier frequency fc of the waveform is obtained by averaging. When the waveform is sent after the recording is completed, the carrier frequency fc is fixed with a duty cycle of 1/3, and the time corresponding to 1 and 2 is sent out in the carrier/non-carrier order to complete the IR learning process.

IR Learn Software Description
To quickly complete the IR learning and sending function, the following steps are required.
(1) Initialize with ir_learn_init(void).
(2) Add the relevant part of the ir_learn_irq_handler(void) interrupt handling function to the interrupt function.
(3) Add the ir_learn_detect(void) section to the program to determine the learning results.
(4) Modify the relevant macro definitions in rc_ir_learn.h.
(5) Add the ir_learn_start(void) function to the appropriate location in the UI layer to start learning.
(6) After judging the result by the judgment function set in step 3, use get_ir_learn_state(void) to check the IR learning status and do UI layer operations according to the success or failure of learning: if successful, continue steps 7~9 to finish sending, if failure, you can re-execute step 5 or perform other custom UI actions.
(7) After successful learning, the learning result can be sent. The first step of sending is to initialize the IR transmission, using ir_learn_send_init(void). Be noted that after calling this function IR_OUT will be changed to PWM output pin, if you want to re-enter the IR learning state, you should re-execute step 1 to re-initialize the pin function.
(8) The second step of sending is to copy the useful parameters of the learning result to a fixed area, RAM/Flash are suitable, use the ir_learn_copy_result(ir_learn_send_t* send_buffer) function to copy to the structure defined for sending the IR learning result.
(9) The final step of sending is to call the ir_learn_send(ir_learn_send_t* send_buffer) function to send the learning results.
At this point, the entire functionality of IR learning has been implemented. In the following section, we will specify how to add the functions mentioned in the steps, one by one, in the order of the above steps.
IR_Learn initialization
When using the IR Learn function, after copying rc_ir_learn.c and rc_ir_learn.h into the project, the first step is to call the initialization function.
void ir_learn_init(void)
This function finds its entity in rc_ir_learn.c. It first clears the structure used, then sets IR_OUT and IR_CONTROL to GPIO and outputs 0. Then it sets the GPIO interrupt enable and clears the interrupt flag bit.
IR_Learn interrupt handling
Since the IR Learn function is implemented based on interrupts, the second step requires adding interrupt handling functions to the interrupts. As the protocol stack structure frequently triggers interrupts, the B85/B87 first reads the interrupt flag to distinguish whether it is a GPIO interrupt. Only when the interrupt is generated by GPIO does it proceed with recording. The TC321x series implements a similar function by detecting the IR_LEARN_CYCLE interrupt instead. The implementation code is as follows:
Note
- The current B85/B87 implements IR Learning using the GPIO interrupt method, while the TC321x utilizes a dedicated hardware IR Learning module.
void ir_learn_irq_handler(void)
{
if ((g_ir_learn_ctrl -> ir_learn_state != IR_LEARN_WAIT_KEY) && (g_ir_learn_ctrl -> ir_learn_state != IR_LEARN_BEGIN))
{
return;
}
#if(MCU_CORE_TYPE!=MCU_CORE_TC321X)
u32 src = reg_irq_src;
if ((src & IR_LEARN_INTERRUPT_MASK))
{
reg_irq_src = IR_LEARN_INTERRUPT_MASK;
ir_record(clock_time()); // IR Learning
}
#else
if( ir_learn_get_irq_status(IR_LEARN_CYCLE_IRQ))
{
ir_learn_clr_irq_status(IR_LEARN_CYCLE_IRQ);
ir_learn_clr_irq_status(IR_LEARN_HIGH_IRQ);
ir_record(clock_time()); // IR Learning
}
if( ir_learn_get_irq_status(IR_LEARN_TIMEOUT_IRQ))
{
/**
* When a timeout interrupt occurs, there is still the last piece of data in the high register.
* Therefore, in the timeout interrupt, we check the high interrupt to retrieve the last value from the high register.
*/
ir_learn_clr_irq_status(IR_LEARN_TIMEOUT_IRQ);
}
#endif
}
Where ir_record() is the specific learning algorithm, the function pre_attribute_ram_code_ is put into the RAM in order to speed up the learning and avoid errors caused by long execution time.
IR_Learn result processing function
The main role of the result processing function is to change the state of IR learning in time according to the current IR learning situation, and each loop needs to be executed to complete the detection in time. The function can be called in the main_loop().
void ir_learn_detect(void)
As can be seen from the function entity, when the time after the start of learning exceeds IR_LEARN_ OVERTIME_THRESHOLD, the waveform is still not received and it is a timeout failure; after learning has started and has received the signal, the set threshold time passed but no new signal received, it is considered to have completed the learning state, at this time, if the received carrier and non-carrier part exceeds the set number (default is 15), the learning is considered successful, otherwise it is considered failed.
IR_Learn macro definition
To increase extensibility, some macro definitions are added to rc_ir_learn.h.
#define GPIO_IR_OUT PWM_PIN // GPIO_PE3
#define GPIO_IR_CONTROL GPIO_PE0
#define GPIO_IR_LEARN_IN GPIO_PE1
The first three define the GPIO pins, IN/OUT/CONTROL, which change according to the specific design.
IR_Learn start function
The IR Learn start function is called where needed in the UI layer to start the IR learning process. The function is as follows.
ir_learn_start();
IR_Learn state query
Users can call the status query function to query the learning results, the function is as follows.
unsigned char get_ir_learn_state(void)
{
if(g_ir_learn_ctrl -> ir_learn_state == IR_LEARN_SUCCESS)
return 0;
else if(g_ir_learn_ctrl -> ir_learn_state < IR_LEARN_SUCCESS)
return 1;
else
return (g_ir_learn_ctrl -> ir_learn_state);
}
Return value = 0: IR learning is successful.
Return value = 1: IR learning is in progress or not started.
Return value > 1: IR learning failed, the return value is the reason for failure, which corresponds to the reason for failure known in ir_learn_states. The ir_learn_states is defined as follows.
enum {
IR_LEARN_DISABLE = 0x00,
IR_LEARN_WAIT_KEY,
IR_LEARN_KEY,
IR_LEARN_BEGIN,
IR_LEARN_SAMPLE_END,
IR_LEARN_SUCCESS,
IR_LEARN_FAIL_FIRST_INTERVAL_TOO_LONG,
IR_LEARN_FAIL_TWO_LONG_NO_CARRIER,
IR_LEARN_FAIL_WAIT_OVER_TIME,
IR_LEARN_FAIL_WAVE_NUM_TOO_FEW,
IR_LEARN_FAIL_FLASH_FULL,
IR_LEARN_FAIL,
}ir_learn_states;
IR_Learn_Send initialization
After the UI layer determines that the learning is successful, the send initialization function needs to be called before sending the learned waveform, and the function is as follows.
void ir_learn_send_init(void)
The initialization function mainly sets PWM-related parameters, interrupt-related parameters, and sets IR_OUT as the output port of PWM, noting that the IR learning function stops after this function is used, and the initialization function described in 11.3.4.1 needs to be called again if it needs to be enabled again.
IR_Learn result copy function
In the design, there are often cases where several keys need to have IR learning functions, so the UI layer wants to be able to copy the learning results to a location in RAM/Flash for later transmission after successful learning, and to start the learning process for other keys. Therefore, a result copy function is provided to copy the necessary parameters for sending. The function is as follows.
void ir_learn_copy_result(ir_learn_send_t* send_buffer)
The send_buffer is the structure needed for IR learning to send, which contains the clock_tick value for one carrier cycle, the total number of carriers and non-carriers (counting from 0), and the buffer of carriers and non-carriers already to be sent.
typedef struct{
unsigned int ir_learn_carrier_cycle;
unsigned short ir_learn_wave_num;
unsigned int ir_lenrn_send_buf[MAX_SECTION_NUMBER];
}ir_learn_send_t;
IR_Learn send function
After the learning is successful and the pre-send operation is done, the send function can be called to send the learning result. The function is as follows.
void ir_learn_send(ir_learn_send_t* send_buffer);
where send_buffer is the structure used in the previous function. The send function does not carry the repeat function, each call to the function will send the learned waveform, if you need to repeat the user can use the timer in the UI layer to design their own repeated calls to the function.
IR Learn algorithm details
To facilitate understanding the code, the principle of the IR learning algorithm is explained in detail here. The following is a simulated waveform, which simulates a complete packet of IR data. The data contains Start carrier, Start No carrier, bit 1 carrier, bit 1 no carrier, bit 0 carrier, bit 0 no carrier, End carrier, End no carrier.

Since IR_IN is set in the IR learning state to wake up on the falling edge of the GPIO, normally every falling edge goes to an interrupt where we do the recording operation. In the IR learning algorithm, instead of identifying the waveform to a specific code type, the waveform is recorded with the concept of carrier/non-carrier. Consecutive carriers are considered as one carrier segment, while two carriers separated by a long time are considered as non-carriers. Thus the above is considered in the IR learning algorithm as follows.

Each time the algorithm is executed, the current time curr_trigger_tm_point is recorded, and the last_trigger_tm_point is subtracted from the last interrupt time to get a time_interval of one cycle. If this time is relatively small, it is considered to be still in the carrier; if this time exceeds the set threshold, it is considered to be in the middle of a no carrier segment, and at this time it is in the first waveform of the new carrier segment: at this time, it is necessary to record the last carrier time and put it into the buffer, which is the difference between the first interrupt entry time and the last interrupt time, as shown below.

According to this method, let wave_series_cnt increase from 0, corresponding to the first carrier segment, the first non-carrier segment, the second carrier segment, the second non-carrier segment... At the same time, the calculated time of each segment is stored in the corresponding location (wave_series_buf[wave_series_cnt]) in wave_series_buf[0], wave_series_buf[1], and wave_series_buf[2]. All the way to the end of the waveform, wave_series_cnt represents the total number of segments, and wave_series_buf is loaded with the length of each segment.
In addition, during the first N (settable) interrupts, N times are recorded, and the smallest one of them is taken as the carrier period, which is used when sending after the learning is finished, and the duty cycle is 1/3 (settable) by default.
After the IR learning process is finished, the learning result can be sent. When sending the learning result, it is also sent according to the concept of carrier and non-carrier. Using PWM DMA_FIFO mode, after putting the learned carrier frequency, duty cycle, and duration of each segment into DMA buffer, enable DMA, the chip will automatically send out the learned waveform until all the sending is finished, and generate FLD_IRQ_PWM0_IR_DMA_FIFO_DONE interrupt.
IR Learn learning parameter adjustment
Some parameters related to IR learning are defined in rc_ir_learn.h. When setting the parameter mode to USER_DEFINE is selected and set by yourself, it will have different effects on the learning effect, which will be described in detail here.
#define IR_LEARN_MAX_FREQUENCY 40000
#define IR_LEARN_MIN_FREQUENCY 30000
#define IR_LEARN_CARRIER_MIN_CYCLE 16000000/IR_LEARN_MAX_FREQUENCY
#define IR_LEARN_CARRIER_MIN_HIGH_TICK IR_LEARN_CARRIER_MIN_CYCLE/3
#define IR_LEARN_CARRIER_MAX_CYCLE 16000000/IR_LEARN_MIN_FREQUENCY
#define IR_LEARN_CARRIER_MAX_HIGH_TICK IR_LEARN_CARRIER_MAX_CYCLE/3
The above parameters set the frequencies supported by IR learning. The default value is set to 30k~40k. The following parameters are the maximum and minimum values of the sys_tick value per carrier cycle, default 1/3 duty cycle high level to sys_tick value, calculated from the frequency parameters for later parameter calculation. Other parameters that affect the learning results are described below, and each parameter is defined in rc_ir_learn.h using macros.
#define IR_LEARN_INTERVAL_THRESHOLD (IR_LEARN_CARRIER_MAX_CYCLE*3/2)
#define IR_LEARN_END_THRESHOLD (30*SYSTEM_TIMER_TICK_1MS)
#define IR_LEARN_OVERTIME_THRESHOLD 10000000 // 10s
#define IR_CARR_CHECK_CNT 10
#define CARR_AND_NO_CARR_MIN_NUMBER 15
#define MAX_SECTION_NUMBER 100
(1) IR_LEARN_INTERVAL_THRESHOLD.
For carrier period threshold, the default value is 1.5 times the IR_LEARN_CARRIER_MAX_CYCLE value, when the time to enter the interrupt twice is more than this threshold is considered at the carrier side.
(2)IR_LEARN_END_THRESHOLD
For IR learn end threshold, when the time to enter interrupt twice exceeds this threshold, or the threshold is exceeded without entering the next interrupt, the IR learning process is considered to be finished.
(3) IR_LEARN_OVERTIME_THRESHOLD
For timeout time, after the start of IR learning process, if the threshold value is exceeded and the received waveform enters interrupt, the learning process is considered to be finished and failed.
(4) IR_CARR_CHECK_CNT
Set the number of packets to be collected to determine the carrier cycle time, the default is set to 10, which means the smallest of the time_interval of the first 10 interrupts will be taken as the carrier time and used to calculate the carrier cycle when sending learning results.
(5) CARR_AND_NO_CARR_MIN_NUMBER
The minimum threshold of carrier and non-carrier segments. When the IR learning process is completed, if the total number of recorded carrier and non-carrier segments is less than this threshold, the entire waveform is considered not learned and the IR learning fails.
(6) MAX_SECTION_NUMBER
The maximum threshold value of carrier and non-carrier section, which will be used when setting the buffer size. If setting to 100, the IR learning process will record at most 100 carrier and non-carrier sections; if it exceeds, the IR learning will be considered failed.
IR Learn common issues
During the learning process, sometimes it encounters that the frequency of the waveform sent after successful learning changes. The possible cause is that the frequency of the learned waveform is too high, resulting in the execution of the algorithm in the interrupt for more than the carrier period. This is shown in the figure below.

Take the IR signal with duty cycle 1/3 and transmitting frequency 38K as an example, one carrier cycle is about 26.3us, high level accounts for 1/3 about 8.7us. At the moment of t0, the external waveform carrier end point is pulled low from high, the chip GPIO triggers an interrupt, and the interrupt needs to execute several instructions in the assembly to save the site to enter the interrupt, after testing at t1 after about 4us to enter the interrupt function to start executing the operation. Due to the long execution time in the interrupt, the interrupt execution ends at t2, and it also takes about 4us to restore the site. In the process of restoring the site at t3 moment, as the next falling edge of the transmit waveform arrives, the interrupt flag bit is cleared at this time and the hardware will trigger the interrupt again. The interrupt has been triggered again after restoring the site about 4us after t2, so the chip saves the site again to enter the interrupt at t4 after 4us in entering the interrupt for operation, after which the above process will be repeated. As seen by the waveform executed by the interrupt, its time is completely deformed and the time to enter the interrupt twice is also larger than the time of one carrier cycle of the original waveform. Since the IR learning is done exactly according to the time recorded in the interrupt, the abnormal time of entering the interrupt will lead to abnormal IR learning results.
There are several ways to solve this problem.
One is to put the IR learning algorithm into the ram_code to reduce the execution time, by default this operation is already performed and does not need to be modified.
The second is to make sure to reduce other processing of interrupts. BLE needs to be disabled in IR learning because it takes up a lot of time in interrupts during non-IDLE states, and the UI layer also tries to prohibit other interrupt sources from causing interrupts during IR learning to prevent exceptions.
Demo description
The feature_IR of the BLE SDK contains the normal IR sending function and IR learning function, and the IR encoding method used is NEC encoding. The switch between the different modes is shown in the following code.
void key_change_proc(void)
{
switch(key0)
{……
if(switch_key == IR_mode){……
}
else if(switch_key == IR_Learn_mode){……
}
else{……
}
}
}
Each mode can be switched to a different mode by pressing a key to perform the corresponding initialization operation, the specific code implementation can be referred to the BLE SDK.
IR Learn Driver
Since the IR Learn implementation on B85/B87 series chips does not use the driver module, the content of this chapter applies only to the TC321x series.
IR Learn Operating Principle
The IR Learn module captures level transitions on the infrared receiver pin and records the durations of high and low levels, thereby reconstructing the original infrared signal waveform. These timing data can be used to analyze infrared protocols or to replicate infrared signals.
IR Learn Modes
IR Learn supports multiple operating modes to accommodate different application scenarios:
typedef enum
{
RISING_EDGE_START_CNT = 0, // Start counting on rising edge
IL_EN_HIGH_STATUS_START_CNT = 1, // Start counting on high-level status after enable
FALLING_EDGE_START_CNT = 2, // Start counting on falling edge
IL_EN_LOW_STATUS_START_CNT = 3, // Start counting on low-level status after enable
} ir_learn_start_cnt_mode_e; // TC321x series
IR Learn Interrupts
IR Learn supports multiple interrupt types to capture different signal events:
typedef enum{
IR_LEARN_HIGH_IRQ = BIT(0), // High-level interrupt
IR_LEARN_CYCLE_IRQ = BIT(1), // Cycle interrupt
IR_LEARN_TIMEOUT_IRQ = BIT(2), // Timeout interrupt
} ir_learn_irq_e; // TC321x series
IR Learn Timeout Settings
IR Learn supports multiple timeout settings to detect signal termination:
typedef enum{
TICK_VALUE_3, // tick value: 0x000003
TICK_VALUE_15, // tick value: 0x00000f
TICK_VALUE_63, // tick value: 0x00003f
TICK_VALUE_255, // tick value: 0x0000ff
TICK_VALUE_1023, // tick value: 0x0003ff
TICK_VALUE_4095, // tick value: 0x000fff
TICK_VALUE_16383, // tick value: 0x003fff
TICK_VALUE_65535, // tick value: 0x00ffff
TICK_VALUE_262143, // tick value: 0x03ffff
TICK_VALUE_1048575, // tick value: 0x0fffff
TICK_VALUE_4194303, // tick value: 0x3fffff
} ir_learn_timeout_tick_e; // TC321x series
IR Learn API
Basic Control API
// Enable IR Learn functionality
static inline void ir_learn_en(void)
// Disable IR Learn functionality
static inline void ir_learn_dis(void)
Interrupt Control API
// Set interrupt mask
static inline void ir_learn_set_irq_mask(ir_learn_irq_e mask) // TC321x series
// Clear interrupt mask
static inline void ir_learn_clr_irq_mask(ir_learn_irq_e mask) // TC321x series
// Get interrupt status
static inline unsigned char ir_learn_get_irq_status(ir_learn_irq_e status) // TC321x series
// Clear interrupt status
static inline void ir_learn_clr_irq_status(ir_learn_irq_e status) // TC321x series
Data Acquisition API
// Get high-level duration
static inline unsigned int ir_learn_get_high(void)
// Get cycle time
static inline unsigned int ir_learn_get_cycle(void)
Configuration API
// Set timeout threshold
static inline void ir_learn_set_timeout(ir_learn_timeout_tick_e timeout_value) // TC321x series
// Set operating mode
void ir_learn_rx_init(ir_learn_rx_t *ir_learn_rx) // TC321x series
// Set reception pin
void ir_learn_set_dig_rx_pin(ir_learn_rx_pin_e ir_rx_pin, GPIO_PullTypeDef pull_type) // TC321x series
Enhanced Features of the TC321x Series IR Learn Functionality
The TC321x series introduces the following enhancements to the existing IR Learn functionality:
(1) Analog/Digital Receive Mode:
typedef enum
{
ANALOG_RX_MODE, // Analog receive mode
DIGITAL_RX_MODE, // Digital receive mode
} ir_learn_rx_e;
void ir_learn_rx_mode(ir_learn_rx_e rx_mode);
(2) Analog/Digital Transmission Mode:
typedef enum
{
DIGITAL_TX_MODE, // Digital transmission mode
ANALOG_TX_MODE, // Analog transmission mode
} ir_learn_tx_e;
void ir_learn_tx_mode(ir_learn_tx_e tx_mode);
(3) Structured Configuration:
typedef struct
{
ir_learn_start_cnt_mode_e cnt_mode;
ir_learn_rx_e rx_mode;
ir_learn_timeout_tick_e timeout_cnt;
} ir_learn_rx_t;
typedef struct
{
ir_learn_tx_e tx_mode;
} ir_learn_tx_t;
(4) Analog Transmit/Receive Control:
// Enable analog transmit
static inline void ir_learn_ana_tx_en(void)
// Disable analog transmission
static inline void ir_learn_ana_tx_dis(void)
// Enable analog reception
static inline void ir_learn_ana_rx_en(void)
// Disable analog reception
static inline void ir_learn_ana_rx_dis(void)
(5) Additional GPIO Pin Support:
typedef enum{
IR_RX_PA1 = GPIO_PA1,
IR_RX_PA2 = GPIO_PA2,
IR_RX_PB0 = GPIO_PB0,
IR_RX_PC2 = GPIO_PC2,
IR_RX_PD4 = GPIO_PD4,
IR_RX_PE0 = GPIO_PE0,
IR_RX_PE1 = GPIO_PE1,
}ir_learn_rx_pin_e;
IR Learn Application Example
The following is a simple IR Learn application example:
// Initialize IR Learn
void ir_learn_init_example(void)
{
// Set receive pin
ir_learn_set_dig_rx_pin(IR_RX_PA1, GPIO_PullUp); // TC321x series
// Set operating mode
ir_learn_rx_t rx_cfg = {
.cnt_mode = RISING_EDGE_START_CNT,
.rx_mode = DIGITAL_RX_MODE,
.timeout_cnt = TICK_VALUE_16383
};
ir_learn_rx_init(&rx_cfg); // TC321x series
// Set timeout
ir_learn_set_timeout(TICK_VALUE_16383);
// Enable interrupts
ir_learn_set_irq_mask(RISING_EDGE_IRQ | FALLING_EDGE_IRQ | TIMEOUT_IRQ);
// Enable IR Learn
ir_learn_en();
}
// IR Learn interrupt handler
void ir_learn_irq_handler(void)
{
if (ir_learn_get_irq_status(RISING_EDGE_IRQ)) {
// Handle rising edge interrupt
unsigned int cycle_time = ir_learn_get_cycle();
// Handle cycle time...
ir_learn_clr_irq_status(RISING_EDGE_IRQ);
}
if (ir_learn_get_irq_status(FALLING_EDGE_IRQ)) {
// Handle falling edge interrupt
unsigned int high_time = ir_learn_get_high();
// Handle high level time...
ir_learn_clr_irq_status(FALLING_EDGE_IRQ);
}
if (ir_learn_get_irq_status(TIMEOUT_IRQ)) {
// Handle timeout interrupt
// Signal reception complete...
ir_learn_clr_irq_status(TIMEOUT_IRQ);
}
}
Feature Demo Introduction
The ble_feature_test provides demo codes for some commonly used BLE-related features. Users can refer to these demos to complete their own function implementation. See code for details. Select the macro "FEATURE_TEST_MODE" in feature_config.h in the ble_feature_test project to switch to the demo of different feature test.

Test methods of each demo are described below.
Broadcast Power Consumption Test
This item mainly tests the power consumption during broadcasting of different broadcasting parameters. Users can measure the power consumption with an external multimeter during the test. Need to modify FEATURE_TEST_MODE to TEST_POWER_ADV in feature_config.h.
#define FEATURE_TEST_MODE TEST_POWER_ADV
The demo provides different broadcast types and broadcast parameters. Users can modify the definition of APP_ADV_POWER_TEST_TYPE to select different broadcast modes based on their requirements.
//TEST_POWER_ADV config start
#define CONNECT_12B_1S_1CHANNEL 0
#define CONNECT_12B_1S_3CHANNEL 1
#define CONNECT_12B_500MS_3CHANNEL 2
#define CONNECT_12B_30MS_3CHANNEL 3
#define UNCONNECTED_16B_1S_3CHANNEL 4
#define UNCONNECTED_16B_1_5S_3CHANNEL 5
#define UNCONNECTED_16B_2S_3CHANNEL 6
#define UNCONNECTED_31B_1S_3CHANNEL 7
#define UNCONNECTED_31B_1_5S_3CHANNEL 8
#define UNCONNECTED_31B_2S_3CHANNEL 9
#define APP_ADV_POWER_TEST_TYPE UNCONNECTED_31B_2S_3CHANNEL
Unconnected Broadcast Power Consumption Test
In feature_adv_power, the default test is unconnected broadcast power consumption. In the demo, the default broadcast data length is 31 bytes. Users can modify it according to their requirements.
//ADV data length: max 31 byte
u8 tbl_advData[] = {
0x1E, DT_COMPLETE_LOCAL_NAME, 't', 'e', 's', 't', 'a', 'd', 'v', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D'
};
The demo provides broadcast parameters including 1S_3CHANNEL, 1_5S_3CHANNEL and 2S_3CHANNEL, with broadcast packet lengths of 16 bytes and 31 bytes. Users can select the corresponding test items based on their requirements.
Connectable Broadcast Power Consumption Test
In feature_adv_power, users need to modify APP_ADV_POWER_TEST_TYPE to test unconnected broadcast power consumption. By default, the demo uses a broadcast data length of 31 bytes, which users can modify as needed.
//ADV data length: 12 byte
u8 tbl_advData[12] = {
0x08, DT_COMPLETE_LOCAL_NAME, 't', 'e', 's', 't', 'a', 'd', 'v',
0x02, DT_FLAGS, 0x05,
};
The demo provides broadcast parameters including 1S_1CHANNEL, 1S_3CHANNEL, 500MS_3CHANNEL and 30MS_3CHANNEL. Users may select the corresponding test items based on their requirements.
SMP Test
SMP test mainly tests the process of pairing encryption, mainly divided into the following ways:
(1) LE_Security_Mode_1_Level_1, no authentication and no encryption.
(2) LE_Security_Mode_1_Level_2, unauthenticated pairing with encryption.
(3) LE_Security_Mode_1_Level_3, authenticated pairing with encryption-legacy.
(4) LE_Security_Mode_1_Level_4, authenticated pairing with encryption-sc.
Users need to set FEATURE_TEST_MODE to TEST_SMP_SECURITY in feature_config.h.
#define FEATURE_TEST_MODE TEST_SMP_SECURITY
LE pairing is a three-phase process. The primary difference between LE Legacy Pairing and LE Secure Connections lies in Phase 2:
- Phase 1: Exchange pairing characteristics
- Phase 2 (LE Legacy Pairing): STK generation
- Phase 2 (LE Secure Connections): LTK generation
- Phase 3: Key distribution

Below is a brief introduction to each pairing mode.
LE_Security_Mode_1_Level_1
LE_Security_Mode_1_Level_1 is the simplest pairing method, neither authentication nor encryption. The user changes the SMP_TEST_MODE of feature_security.c to SMP_TEST_NO_SECURITY.
#define SMP_TEST_MODE SMP_TEST_NO_SECURITY
LE_Security_Mode_1_Level_2
The LE_Security_Mode_1_Level_2 mode is Just Work, only encryption but not authentication. Just work is divided into Legacy Just Work and SC Just Work. The user changes the SMP_TEST_MODE of feature_security.c to SMP_TEST_LEGACY_PAIRING_JUST_WORKS or SMP_TEST_SC_PAIRING_JUST_WORKS as required. Introduced separately below.
SMP_TEST_LEGACY_PAIRING_JUST_WORKS
The user makes the following modifications:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PAIRING_JUST_WORKS
The process is shown as following:

SMP_TEST_SC_PAIRING_JUST_WORKS
The user makes the following modifications:
#define SMP_TEST_MODE SMP_TEST_SC_PAIRING_JUST_WORKS
Different from Legacy Pairing, Phase 2 of LE Secure Connections consists of public key exchange, authentication stage 1, LTK calculation, and authentication stage 2. Except for authentication stage 1, all SC modes follow the same procedure.
The SC public key exchange process is illustrated below:

The SC just work authentication stage 1 process is illustrated below:

The SC LTK calculation process is illustrated below:

The SC authentication stage 2 process is illustrated below:

LE_Security_Mode_1_Level_3
LE_Security_Mode_1_Level_3 is a legacy pairing method that provides both authentication and encryption. Based on pairing parameter settings, it is categorized into OOB and PassKey Entry.
The current demo provides three sample codes for PassKey Entry: SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI, SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI, and SMP_TEST_LEGACY_PASSKEY_ENTRY_MISI. Users may select based on requirements.
Below is a brief introduction to these methods.
SMP_TEST_LEGACY_PASSKEY_ENTRY
The basic flowchart for Legacy Passkey Entry pairing is as follows:

1) SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI
The user needs to modify as follows in feature_security.c:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_SDMI
During the pairing process, the slave side needs to display the TK and the master side needs to enter the TK. During initialization, a gap event related to pairing is registered. The pairing information will be notified to the app layer.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_DISPLAY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);
The user needs to print the current TK information when receiving the GAP_EVT_MASK_SMP_TK_DISPLAY message.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_DISPLAY:
{
char pc[7];
u32 pinCode = *(u32*)para;
sprintf(pc, "%d", pinCode);
tlkapi_printf(APP_LOG_EN, "TK display:%s\n", pc);
}
break;
…
}
}
2) SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI
The only difference from above is that the master displays the TK while the slave inputs it. Users need to modify the code:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_MDSI
During initialization, the pairing-related gap event is registered. When a TK input is required, the app layer is notified.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_DISPLAY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY | \
GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);
Users should enter the TK information displayed by the master as soon as possible after receiving the GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY message.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_REQUEST_PASSKEY:
{
tlkapi_printf(APP_LOG_EN, "TK Request passkey\n");
}
…
}
}
3) SMP_TEST_LEGACY_PASSKEY_ENTRY_MISI
The only difference from above is that both master and slave input the TK simultaneously. Users need to modify the code:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_MISI
During initialization, the pairing-related gap event is registered. When a TK input is required, the app layer is notified.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_DISPLAY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY | \
GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);
Users should enter the TK information displayed by the master as soon as possible after receiving the GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY message.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_REQUEST_PASSKEY:
{
tlkapi_printf(APP_LOG_EN, "TK Request passkey\n");
}
…
}
}
SMP_TEST_LEGACY_PASSKEY_ENTRY_OOB
Users need to modify feature_security.c as follows:
#define SMP_TEST_MODE SMP_TEST_LEGACY_PASSKEY_ENTRY_OOB
During pairing, the slave and master should exchange the TK via OOB. During initialization, the pairing-related gap event is registered. The pairing information is notified to the application layer.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_REQUEST_OOB | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);
Users should configure the current TK information after receiving the GAP_EVT_SMP_TK_REQUEST_OOB event.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_REQUEST_OOB:
{
tlkapi_printf(APP_LOG_EN, "TK Request OOB\n");
blc_smp_setTK_by_OOB(OOB_TK_key);
tlkapi_printf(APP_LOG_EN, "[APP][SMP]set TK data %s\n", hex_to_str(OOB_TK_key, 16));
}
break;
…
}
}
The process flowchart is shown below:

LE_Security_Mode_1_Level_4
LE_Security_Mode_1_Level_4 is both an authentication and encryption SC pairing method. According to the pairing parameter settings, it is divided into OOB, PassKey Entry, and Numeric Comparison. The current demo provides sample code for all pairing parameters, namely SMP_TEST_SC_PASSKEY_ENTRY_SDMI, SMP_TEST_SC_PASSKEY_ENTRY_MDSI, SMP_TEST_SC_PASSKEY_ENTRY_MISI, SMP_TEST_SC_NUMERIC_COMPARISON, and SMP_TEST_SC_PASSKEY_ENTRY_OOB. Users may select the appropriate method as needed. Below is a brief overview of these methods.
SMP_TEST_SC_NUMERIC_COMPARISON
The user needs to modify as follows in feature_security.c:
#define SMP_TEST_MODE SMP_TEST_SC_NUMERIC_COMPARISON
This pairing method is numeric comparison, that is, during the pairing process, both the master and slave will display a six-digit PinCode. If the user compares the numbers for the same, if they are the same, click to confirm and agree to the pairing. During initialization, the pairing-related gap event is registered. Pairing information is communicated to the application layer.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_NUMERIC_COMPARE | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE );
Users should verify numerical equality after receiving GAP_EVT_SMP_TK_NUMERIC_COMPARE.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_NUMERIC_COMPARE:
{
char pc[7];
u32 pinCode = *(u32*)para;
sprintf(pc, "%d", pinCode);
tlkapi_printf(APP_LOG_EN, "TK numeric comparison:%s\n", pc);
}
break;
…
}
}
The demo sends YES or NO via button presses. Sample code follows:
if(blc_smp_isWaitingToCfmNumericComparison()){
if(consumer_key == MKEY_VOL_DN){
blc_smp_setNumericComparisonResult(1);// YES
tlkapi_printf(APP_KEYBOARD_LOG_EN, "[APP][KEY] confirmed YES\n");
}
else if(consumer_key == MKEY_VOL_UP){
blc_smp_setNumericComparisonResult(0);// NO
tlkapi_printf(APP_KEYBOARD_LOG_EN, "[APP][KEY] confirmed NO\n");
}
}
The SC numeric comparison authentication stage 1 process is shown below:

SMP_TEST_SC_PASSKEY_ENTRY
Users need to modify feature_security.c as follows:
The basic SC Passkey Entry pairing process is shown below:

1) SMP_TEST_SC_PASSKEY_ENTRY_SDMI
Users need to modify feature_security.c as follows:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_SDMI
During the pairing process, the slave side needs to display the TK and the master side needs to enter the TK. During initialization, a gap event related to pairing is registered. The pairing information will be notified to the app layer.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_DISPLAY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);
The user needs to print the current TK information when receiving the GAP_EVT_MASK_SMP_TK_DISPLAY message.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_DISPLAY:
{
char pc[7];
u32 pinCode = *(u32*)para;
sprintf(pc, "%d", pinCode);
tlkapi_printf(APP_LOG_EN, "TK display:%s\n", pc);
}
break;
…
}
}
2) SMP_TEST_SC_PASSKEY_ENTRY_MDSI
The only difference from above is that the master displays the TK while the slave inputs it. Users need to modify the code:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_MDSI
During initialization, the pairing-related gap event is registered. When TK input is required, the app layer is notified.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE );
Users should enter the TK information displayed by the master as soon as possible after receiving the GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY message.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_REQUEST_PASSKEY:
{
tlkapi_printf(APP_LOG_EN, "TK Request passkey\n");
blc_smp_sendKeypressNotify(BLS_CONN_HANDLE, KEYPRESS_NTF_PKE_START);
}
…
}
}
3) SMP_TEST_LEGACY_PASSKEY_ENTRY_MISI
The only difference from above is that both master and slave input TK simultaneously. Users need to modify the code:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_MISI
During initialization, the pairing-related gap event is registered. When TK input is required, the app layer is notified.
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_DISPLAY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY | \
GAP_EVT_MASK_SMP_SECURITY_PROCESS_DONE);
Users should enter the TK information displayed by the master as soon as possible after receiving the GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY message.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_REQUEST_PASSKEY:
{
tlkapi_printf(APP_LOG_EN, "TK Request passkey\n");
blc_smp_sendKeypressNotify(BLS_CONN_HANDLE, KEYPRESS_NTF_PKE_START);
}
…
}
}
SMP_TEST_SC_PASSKEY_ENTRY_OOB
Users need to modify feature_security.c as follows:
#define SMP_TEST_MODE SMP_TEST_SC_PASSKEY_ENTRY_OOB
``
This pairing method uses OOB communication, meaning the confirm value between the master and slave is transmitted via OOB during pairing. Both sides verify whether the confirm values match. During initialization, the pairing-related gap event is registered. Pairing information is notified to the application layer.
```C
blc_gap_registerHostEventHandler( app_host_event_callback );
blc_gap_setEventMask( GAP_EVT_MASK_SMP_PARING_BEGIN | \
GAP_EVT_MASK_SMP_PARING_SUCCESS | \
GAP_EVT_MASK_SMP_PARING_FAIL | \
GAP_EVT_MASK_SMP_TK_REQUEST_PASSKEY | \
GAP_EVT_MASK_SMP_CONN_ENCRYPTION_DONE | \
GAP_EVT_MASK_SMP_TK_SEND_SC_OOB_DATA | \
GAP_EVT_MASK_SMP_TK_REQUEST_SC_OOB);
During initialization, the user should generate local OOB data.
blc_smp_generateScOobData(&sc_oob_data_cb.scoob_local,&sc_oob_data_cb.scoob_local_key);
After receiving GAP_EVT_SMP_TK_SEND_SC_OOB_DATA, if the peer requires OOB data, the user can send the generated local OOB data to the peer device via OOB.
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_SEND_SC_OOB_DATA:
{
gap_smp_TkSendScOobDataEvt_t* pEvt = (gap_smp_TkSendScOobDataEvt_t*)para;
if(pEvt->sendScOobData2RemoteFlag){
tlkapi_printf(APP_LOG_EN, "[APP][SMP] need to send SC OOB data to remote device\n");
tlkapi_printf(APP_LOG_EN, " Local SC OOB data-c(be) (by UART) %s\n", hex_to_str(sc_oob_data_cb.scoob_local.confirm, 16));
tlkapi_printf(APP_LOG_EN, "[APP][SMP] Send Local SC OOB data-r(be) (by UART) %s\n", hex_to_str(sc_oob_data_cb.scoob_local.random, 16));
}
else{
tlkapi_printf(APP_LOG_EN, "[APP][SMP] not need to send SC OOB data to remote device\n");
}
}
break;
…
}
}
After receiving GAP_EVT_SMP_TK_REQUEST_SC_OOB, the user should send both local and remote OOB data to the lower layer. The sample code is as follows:
int app_host_event_callback (u32 h, u8 *para, int n)
{
u8 event = h & 0xFF;
switch(event)
{
…
case GAP_EVT_SMP_TK_REQUEST_SC_OOB:
{
gap_smp_TkRequestScOobDataEvt_t* pEvt = (gap_smp_TkRequestScOobDataEvt_t*)para;
tlkapi_printf(APP_LOG_EN, "[APP][SMP] SC OOB scOobLocalUsed %d\n", pEvt->scOobLocalUsed);
tlkapi_printf(APP_LOG_EN, "[APP][SMP] SC OOB scOobRemoteUsed %d\n", pEvt->scOobRemoteUsed);
sc_oob_data_cb.scoob_remote_used = pEvt->scOobRemoteUsed;
sc_oob_data_cb.scoob_local_used = pEvt->scOobLocalUsed;
if(pEvt->scOobRemoteUsed || pEvt->scOobLocalUsed){
blc_smp_setScOobData(pEvt->connHandle, &sc_oob_data_cb.scoob_local, &sc_oob_data_cb.scoob_remote);
}
}
break;
…
}
}
The SC OOB authentication stage 1 process is shown below:

GATT Security Test
As known from the BLE module 3.3.3 ATT&GATT chapter, each Attribute in the service list defines read and write permissions, that is, the pairing mode should reach the corresponding level to read or write. For example, in the SPP service of Demo:
// client to server RX
{0,ATT_PERMISSIONS_READ,2,sizeof(TelinkSppDataClient2ServerCharVal),(u8*)(&my_characterUUID), (u8*)(TelinkSppDataClient2ServerCharVal), 0, 0}, //prop
{0,SPP_C2S_ATT_PERMISSIONS_RDWR,16,sizeof(SppDataClient2ServerData),(u8*)(&TelinkSppDataClient2ServerUUID), (u8*)(SppDataClient2ServerData), &module_onReceiveData, 0}, //value
{0,ATT_PERMISSIONS_READ,2,sizeof(TelinkSPPC2SDescriptor),(u8*)&userdesc_UUID,(u8*)(&TelinkSPPC2SDescriptor), 0, 0},
The read and write permissions of the second Attribute are defined as: SPP_C2S_ATT_PERMISSIONS_RDWR.
This read and write permission is up to the user to choose, you can choose one of the following:
##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
No matter which one you choose, the current pairing mode should be higher than or equal to this level of read and write permissions to read and write services correctly.
The user needs to modify feature_config.h as follows:
##define FEATURE_TEST_MODE TEST_GATT_SECURITY
SMP test encryption levels are LE_SECURITY_MODE_1_LEVEL_1, LE_SECURITY_MODE_1_LEVEL_2, LE_SECURITY_MODE_1_LEVEL_3, LE_SECURITY_MODE_1_LEVEL_4. The user needs to select app_config.h according to the needs of the corresponding pairing mode.
##define SMP_TEST_MODE LE_SECURITY_MODE_1_LEVEL_3
For example, the current pairing mode is LE_SECURITY_MODE_1_LEVEL_3, that is, there are both authentication and encryption Legacy pairing modes. So the current read and write permissions can be selected as follows.
##define SPP_C2S_ATT_PERMISSIONS_RDWR ATT_PERMISSIONS_AUTHEN_RDWR
The process is shown as following:

DLE Test
The DLE test mainly tests the long package. The demo is divided into master and slave. Users need to compile and burn to two EVB boards respectively for testing.
For the code at master side, users can refer to feature_DLE_master:
##define FEATURE_TEST_MODE TEST_MDATA_LENGTH_EXTENSION
For the code at slave side, users can refer to feature_DLE_slave:
##define FEATURE_TEST_MODE TEST_SDATA_LENGTH_EXTENSION
After burning, they are reset respectively, and the master is triggered to establish a connection. After the connection is successful, the MTU and DLE are exchanged respectively.
blc_att_requestMtuSizeExchange(BLS_CONN_HANDLE, MTU_SIZE_SETTING);
blc_ll_exchangeDataLength(LL_LENGTH_REQ , DLE_TX_SUPPORTED_DATA_LEN);
After the exchange is successful, the slave will send a long packet of data to the master every 3.3s, or the master will trigger via a button press, the mater will write a long packet of data to the slave, and the slave will send the same data to the master after receiving it.
The test process is as follows:

In this new version, the feature now includes an automatic calculation mechanism for RX FIFO and TX FIFO. If the user sets ACL_CONN_MAX_RX_OCTETS or ACL_CONN_MAX_TX_OCTETS in app_config.h (corresponding to the maximum DLE length the local device can accept), the actual required RX_FIFO_SIZE or TX_FIFO_SIZE will be automatically calculated in vendor/common/app_buffer.h.
Soft Timer Test
Please refer to the chapter of Software Timer.
WhiteList Test
If the whitelist is set, only the devices in the whitelist are allowed to establish connections. The user needs to modify feature_config.h as follows:
##define FEATURE_TEST_MODE TEST_WHITELIST
When the slave has no binding information, any other device is allowed to connect. After the connection is successful, the slave will add the current master's information to the whitelist, and then only the current device can connect with the slave.
The test process is as follows:

Extended Advertising Test
The Extended advertising demo primarily tests various combinations of 1M/2M/Coded PHY for extended broadcasting. You need to modify FEATURE_TEST_MODE to TEST_EXTENDED_ADVERTISING in feature_config.h.
##define FEATURE_TEST_MODE TEST_EXTENDED_ADVERTISING
The relevant codes are in vendor/ble_feature_test/feature_extend_adv, and the slave demo is provided.
Set the maximum length of broadcast data as follows:
##define APP_ADV_SETS_NUMBER 1 // Number of Supported Advertising Sets
##define APP_MAX_LENGTH_ADV_DATA 1024 // Maximum Advertising Data Length
##define APP_MAX_LENGTH_SCAN_RESPONSE_DATA 1024 // Maximum Scan Response Data Length
In user_init_normal, different types of extended broadcast packets based on 1M PHY configuration have been reserved.
##if 0 //Legacy, non_connectable_non_scannable
##elif 0 //Legacy, connectable_scannable
##elif 0 // Extended, None_Connectable_None_Scannable undirected, without auxiliary packet
#if 1 // ADV_EXT_IND: 1M PHY
#elif 1 // ADV_EXT_IND: Coded PHY(S2)
#elif 0 // ADV_EXT_IND: Coded PHY(S8)
#endif
##elif 0 // Extended, None_Connectable_None_Scannable undirected, with auxiliary packet
#if 1 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S2)
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
#elif 0 // ADV_EXT_IND: Coded PHY(S2) AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
#elif 0 // ADV_EXT_IND: Coded PHY(S8) AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
#elif 0 // ADV_EXT_IND: Coded PHY(S2) AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
#elif 0 // ADV_EXT_IND: Coded PHY(S8) AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
#elif 0 // ADV_EXT_IND: Coded PHY(S2); AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S2)
#elif 0 // ADV_EXT_IND: Coded PHY(S8); AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
#else
#endif
#if 1 //AdvData: 100 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 100
#elif 0 //AdvData: 251 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 300
#elif 0 //AdvData: 300 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 300
#elif 0 //AdvData: 600 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 600
#elif 1 //AdvData: 1010 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 1010
#endif
##elif 0 // Extended, None_Connectable_None_Scannable directed, without auxiliary packet
##elif 0 // Extended, None_Connectable_None_Scannable Directed, with auxiliary packet
#if 1 //AdvData: 600 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 600
#elif 1 //AdvData: 1010 bytes, check that APP_MAX_LENGTH_ADV_DATA should be bigger than 1010
#endif
##elif 0 // Extended, Scannable, Undirected
#if 1 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
#elif 0 // ADV_EXT_IND: Coded PHY(S8) AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
#elif 0 // ADV_EXT_IND: Coded PHY(S8) AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
#elif 1 // ADV_EXT_IND: Coded PHY(S8); AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
#else
#endif
#if 1
#else //ExtScanRspData: 1010 bytes, check that APP_MAX_LENGTH_SCAN_RESPONSE_DATA should be bigger than 1010
#endif
##elif 1 // Extended, Connectable, Undirected
#if 1 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: 1M PHY
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: 2M PHY
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
#elif 0 // ADV_EXT_IND: 1M PHY; AUX_ADV_IND/AUX_CHAIN_IND: Coded PHY(S8)
#endif
##endif
Users can refer to the demo to combine the types of extended broadcast packages they need.
Users need to use mobile phones or protocol analysis devices that support Bluetooth 5 Low Energy Advertising Extension, Bluetooth 5 Low Energy 2Mbps and Bluetooth 5 Low Energy Coded (Long Range) functions to see the data broadcast by the above various types of extensions.
Note
- API blc_ll_init2MPhyCodedPhy_feature() is used to enable 2M PHY/Coded PHY.
2M/Coded PHY used on Legacy advertising and Connection Test
The 2M/Coded PHY used on Legacy advertising and Connection demo is mainly to test that after establishing a connection based on Legacy advertising, switch to 1M/2M/Coded PHY in the connected state, and change FEATURE_TEST_MODE to TEST_2M_CODED_PHY_CONNECTION in feature_config.h.
##define FEATURE_TEST_MODE TEST_2M_CODED_PHY_CONNECTION
The relevant codes are in vendor/ble_feature_test/feature_phy_conn, and the slave demo is provided.
Initially open 2M Phy and Coded Phy:
blc_ll_init2MPhyCodedPhy_feature(); // mandatory for 2M/Coded PHY
After the connection is successful, the mainloop will use the API blc_ll_setPhy() to initiate a PHY change request in a 2-second cycle, 1M –> Coded PHY(S2) –> 2M –> Coded PHY(S8) –> 1M. The process is shown in the figure below:

if(phy_update_test_tick && clock_time_exceed(phy_update_test_tick, 2000000)){
phy_update_test_tick = clock_time() | 1;
int AAA = phy_update_test_seq%4;
if(AAA == 0){
blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_CODED, PHY_PREFER_CODED, CODED_PHY_PREFER_S2);
}
else if(AAA == 1){
blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_2M, PHY_PREFER_2M, CODED_PHY_PREFER_NONE);
}
else if(AAA == 2){
blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_CODED, PHY_PREFER_CODED, CODED_PHY_PREFER_S8);
}
else{
blc_ll_setPhy(BLS_CONN_HANDLE, PHY_TRX_PREFER, PHY_PREFER_1M, PHY_PREFER_1M, CODED_PHY_PREFER_NONE);
}
phy_update_test_seq ++;
}
Peer Master Device can use Demo “ble_master_kma_dongle”, but also need to use API blc_ll_init2MPhyCodedPhy _feature() to open 2M Phy and Coded Phy.
Users can also choose to use other manufacturers' Master devices or mobile phones that support Bluetooth 5 Low Energy 2Mbps and Bluetooth 5 Low Energy Coded (Long Range) functions.
CSA #2 Test
CSA #2 demo mainly uses Channel Selection Algorithm #2 (Channel Selection Algorithm #2) for frequency hopping when testing the connection state. You need to modify FEATURE_TEST_MODE to TEST_CSA2 in feature_config.h.
##define FEATURE_TEST_MODE TEST_CSA2
The relevant codes are all in vendor/ble_feature_test/feature_misc, and the slave demo is provided.
Initial CSA #2:
blc_ll_initChannelSelectionAlgorithm_2_feature()
After enabling CSA #2, the ChSel field in the broadcast packet of Slave has been set to 1. If the CONNECT_IND PDU of the Peer Master Device has also set the ChSel field to 1, the channel selection algorithm #2 is used after the connection is successful. Otherwise, channel selection algorithm #1 should be used.
Peer Master Device can use SDK Demo “ble_master_kma_dongle”, but also need to use API blc_ll_initChannelSelectionAlgorithm_2_feature() to open CSA #2.
Users can also choose to use Master devices or mobile phones from other manufacturers that support Bluetooth 5 Low Energy CSA #2.
BLE_PHY Test
The BLE_PHY test operates in Direct Test Mode to evaluate the LE PHY layer and provide reports to the test side. For detailed specifications, refer to the "Bluetooth Core Specification v5.3 [Vol 6] Part F".
In feature_config.h, modify FEATURE_TEST_MODE to TEST_BLE_PHY.
##define FEATURE_TEST_MODE TEST_BLE_PHY
All relevant code is located in vendor/ble_feature_test/feature_phy_test, offering the following two communication options:
(1) HCI (2) 2-wire UART interface
Either method can be selected in app_config.h.
##define BLE_PHYTEST_MODE PHYTEST_MODE_OVER_HCI_WITH_UART
or
##define BLE_PHYTEST_MODE PHYTEST_MODE_THROUGH_2_WIRE_UART
To perform BLE_PHY testing, first initialize the PHY test module and enable PHY test:
blc_phy_initPhyTest_module();
blc_phy_setPhyTestEnable( BLC_PHYTEST_ENABLE );
After connecting to the host computer via UART, LE PHY testing can be performed.
Multi-Device Address Testing
Overview
Multi-Device Address Testing (TEST_MULTI_LOCAL_DEV) demonstrates how to simultaneously emulate multiple independent BLE devices on a single BLE chip, each with its own unique MAC address and broadcast data. This feature is highly useful for application scenarios requiring support for multiple BLE devices concurrently, such as gateway devices or multi-role devices.
Configuration Method
Set the following in the feature_config.h file:
##define FEATURE_TEST_MODE TEST_MULTI_LOCAL_DEV
Test Content
Multi-device address testing includes the following:
(1) Multi-device initialization: Configure multiple local devices (2) Device switching: Switch between different devices (3) Independent broadcasting: Each device broadcasts independently (4) Independent connection: Each device handles connections independently
Code Implementation
Multi-Device Initialization
void multi_local_dev_init(void)
{
// Enable multi-device address functionality
blc_ll_setMultipleLocalDeviceEnable(1);
// Set unique MAC addresses for each device
u8 dev_addr[4][6] = {
{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
{0x11, 0x22, 0x33, 0x44, 0x55, 0x67},
{0x11, 0x22, 0x33, 0x44, 0x55, 0x68},
{0x11, 0x22, 0x33, 0x44, 0x55, 0x69}
};
// Set MAC address for each device index
for(int i = 0; i < 4; i++) {
blc_ll_setLocalDeviceIndexAndIdentityAddress(i, dev_addr[i]);
}
// Set current device index
blc_ll_setCurrentLocalDevice_by_index(0);
}
Device Switching
void switch_device(u8 dev_index)
{
// Switch to the specified device index
blc_ll_setCurrentLocalDevice_by_index(dev_index);
// Configure the broadcast parameters for this device
bls_ll_setAdvParam(ADV_INTERVAL_100MS, ADV_INTERVAL_100MS,
ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_RANDOM,
0, NULL,
BLT_ENABLE_ADV_ALL, ADV_FP_NONE);
// Set broadcast data
u8 adv_data[] = {
0x02, 0x01, 0x06, // Flags
0x09, 0x09, 'D', 'e', 'v', '0' + dev_index, ' ', 'T', 'e', 's', 't' // Device name
};
bls_ll_setAdvData(adv_data, sizeof(adv_data));
}
Independent Broadcasting
void start_advertising(void)
{
// Start broadcasting
bls_ll_setAdvEnable(1);
}
Usage Instructions
- Compile and download the code to the development board
- The device auto-initializes multiple device addresses
- Use buttons to switch between different device index entries
- Observe broadcast data from various devices
- Attempt connecting to different devices
Notes
- Multi-device address functionality requires hardware support
- Each device requires independent resources
- Pay attention to state management when switching devices
- Configure broadcast intervals and parameters appropriately
SOFT_UART Testing
Software UART Emulation
To facilitate dual-UART tasks for users, the B85m BLE SDK provides a blt software UART demo alongside hardware UART support. Full source code is available. Users may directly utilize this demo after understanding its design principles or modify it for custom implementations.
The source code resides entirely in the driver_ext/software_uart.c and software_uart.h files. To use it, first modify the macro FEATURE_TEST_MODE to TEST_USER_BLT_SOFT_UART in feature_config.h:
##define FEATURE_TEST_MODE TEST_USER_BLT_SOFT_UART
The blt soft UART TX operates on a polling design, permitting transmission only during the interval between broadcast and connection times. The blt soft UART RX is designed using GPIO interrupts and timer interrupts.
Note
- To minimize interrupt handling time, the software serial project should use SYS_CLK_48M_Crystal and supports a maximum baud rate of 9600:
##define CLOCK_SYS_CLOCK_HZ 48000000 //should select 48M clock
Software UART Initialization
Call the following API for initialization:
soft_uart_rx_handler(app_soft_rx_uart_cb);
extern int blt_send_adv();
extern void blc_ll_SoftUartisRfState();
soft_uart_sdk_adv_handler(blt_send_adv);
soft_uart_SoftUartisRfState_handler(blc_ll_SoftUartisRfState);
soft_uart_RxSetFifo(uart_rx_fifo.p, uart_rx_fifo.size);
soft_uart_init();
The soft_uart_rx_handler function registers the UART RX processing function from the IRQ handler as the application-layer defined callback function 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;
}
The soft_uart_RxSetFifo function registers the UART receive FIFO as the upper-layer defined uart_rx_fifo.
u8 uart_rx_buf[80 * 4] = {0};
my_fifo_t uart_rx_fifo = {
80,
4,
0,
0,
uart_rx_buf,};
The soft_uart_init function initializes the GPIO pins used for software UART RX and TX, the RX GPIO interrupt, software UART-related variables, and the timer0 used.
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 Processing
The blt software UART TX processing is implemented using the soft_uart_send function. Users can refer to the demo for sending data only via the simulated serial port. Configuration is as follows:
##define TEST_RX_TX_RUN 1
##define TEST_ONLY_TX_RUN 2
##define TEST_SOFT_UART_RUN_MODEL TEST_ONLY_TX_RUN
Call the soft_uart_send function within the main_loop at the location shown below to handle timer tasks.
u8 send_buf[10] = {0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55};
void main_loop (void)
{
...
soft_uart_send(send_buf, 10);
...
}
The 10-byte data sent can be observed using the serial port host computer tool. Additionally, the results can also be viewed using an external logic analyzer.

The specific implementation method for the blt software UART TX is as follows:
(1) First, send data byte by byte. Before transmission, check the current state machine's state. If the current state is advertising mode, proceed to (2); if connection mode, proceed to (3); if standby mode, proceed to (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) If the current state is advertising, call blc_sdk_adv to check if the broadcast time is arrived. If so, send the broadcast first, then enable TX and jump to (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) When in connection state, call blc_ll_SoftUartisRfState to check if the current RF task affects the TX task. If the time remaining until the next connection event exceeds the duration required to transmit one byte via UART (SOFT_UART_SEND_ONE_BYTE), TX is permitted and execution jumps to (4). Otherwise, wait until the next connection event finishes.
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) When UART TX is enabled, calling the soft_uart_putchar function transmits the i-th byte.
void soft_uart_send(unsigned char * buf, unsigned char len) {
...
soft_uart_putchar(buf[i]);
...
}
Software UART RX Processing
Software UART RX processing uses GPIO interrupts and timer interrupts, with RX operations handled within the irq handler. Users can refer to the demo for simulating serial port data transmission/reception, configured as follows:
##define TEST_RX_TX_RUN 1
##define TEST_ONLY_TX_RUN 2
##define TEST_SOFT_UART_RUN_MODEL TEST_RX_TX_RUN
Process received RX data within the main_loop at the location shown below, then transmit it back via the soft_uart_send function.
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++;
}
...
}
Send 10-byte data via the serial port host computer tool, and the data sent back by the slave device can be received in the receiving window. In addition, the effect can also be observed with an external logic analyzer.

The specific implementation method for the blt software UART RX is as follows:
(1) When data is received from the host computer, a GPIO interrupt is triggered. If the software UART state is SOFT_UART_WAIT, the GPIO interrupt is disabled. Instead, a timer interrupt tick is set to trigger an interrupt. The GPIO input value is read during the next RX interval, and the current software UART state is set to 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) When the timer interrupt is triggered, clear the timer interrupt status. Then check the current software UART state and perform corresponding actions. If the state is SOFT_UART_WORK, jump to (3); if the state is SOFT_UART_STOP_CHECK, jump to (4); if the state is SOFT_UART_DONE_CHECK, jump to (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) When the software UART state is SOFT_UART_WORK, store the current bit value. If 8 bits are already received, set the software UART state to 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) When the software UART state is SOFT_UART_STOP_CHECK, save the previously transmitted byte value. Subsequently, set the software UART state to SOFT_UART_DONE_CHECK and SOFT_UART_WAIT, call soft_uart_RxHandler for processing, and re-enable the RX GPIO interrupt.
_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) When the software UART state is SOFT_UART_DONE_CHECK, clear the SOFT_UART_DONE_CHECK state and call soft_uart_RxHandler for processing, then disable the timer interrupt.
_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;
}
}
...
}
Demo Description
The feature_soft_uart is used for software-based serial port emulation to facilitate TX and RX communication. In feature_config.h, modify FEATURE_TEST_MODE to TEST_USER_BLT_SOFT_UART.
In app_config.h, select the baud rate and TX/RX ports:
/**
* @brief software uart enable and setting
* (The maximum baud rate is 9600)
*/
##define SOFT_UART_ENABLE 1
##define SOFT_UART_BAUD_RATE 9600
##define SOFT_UART_TX_IO GPIO_PA4
##define SOFT_UART_RX_IO GPIO_PB4
The feature_soft_uart provides two demos. Users can select different modes by defining TEST_SOFT_UART_RUN_MODEL:
##define TEST_RX_TX_RUN 1
##define TEST_ONLY_TX_RUN 2
##define TEST_SOFT_UART_RUN_MODEL TEST_RX_TX_RUN
RX_TX_RUN
The soft_uart immediately transmits received data via the TX port when the RX port receives data.
ONLY_TX_RUN
The soft_uart transmits a data packet via the TX port every 5 seconds.
L2CAP COC Test
Overview
The L2CAP COC test (TEST_L2CAP_COC) demonstrates how to use the L2CAP connection-oriented channels introduced in BLE 5.0 to establish reliable data transmission channels between BLE devices.
Configuration Method
Set the following in the feature_config.h file:
##define FEATURE_TEST_MODE TEST_L2CAP_COC
Test Content
The L2CAP COC test includes the following steps:
- COC Initialization: Configure COC parameters
- Channel Establishment: Establish a COC connection
- Data Transmission: Transfer data via COC
- Channel Management: Manage COC connection state
Code Implementation
COC Initialization
void app_l2cap_coc_init(void)
{
blc_coc_initParam_t regParam = {
.MTU = COC_MTU_SIZE,
.SPSM = 0x0080,
.createConnCnt = 1,
.cocCidCnt = COC_CID_COUNT,
};
int state = blc_l2cap_registerCocModule(®Param, cocBuffer, sizeof(cocBuffer)); //
if(state){}
}
Channel Establishment
void app_createLeCreditBasedConnect(void)
{
ble_sts_t state = blc_l2cap_createLeCreditBasedConnect(BLS_CONN_HANDLE);
if(state){}
}
Data Transmission
void app_sendCocData(void)
{
for(unsigned int i=0; i<ARRAY_SIZE(app_cocCid); i++)
{
if(app_cocCid[i].connHandle)
{
ble_sts_t state = blc_l2cap_sendCocData(app_cocCid[i].connHandle, app_cocCid[i].srcCid, coc_test_Data, min(sizeof(coc_test_Data), app_cocCid[i].mtu));
if(state){}
}
}
}
COC Event Handling
int app_host_coc_event_callback (u32 h, u8 *para, int n)
{
(void)h;(void)para;(void)n;
u8 event = h & 0xFF;
switch(event) {
case GAP_EVT_L2CAP_COC_CONNECT:
// Handle COC connection request
break;
case GAP_EVT_L2CAP_COC_DISCONNECT:
// Handle COC disconnection
break;
case GAP_EVT_L2CAP_COC_SEND_DATA_FINISH:
// Handle COC data transmission completion
break;
// ......
default:
break;
}
return 0;
}
Usage Instructions
- Compile and download the code to the development board
- Establish a BLE connection
- Use the button to trigger COC connection establishment
- Transfer data via COC
- Monitor the data transmission status
Notes
- COC requires BLE 5.0 or higher support
- MTU size should be configured appropriately
- Data transmission requires consideration of the credit mechanism
- COC events should be handled correctly
USB CDC Test
Overview
The USB CDC test (TEST_USB_CDC) demonstrates how to implement virtual serial port functionality using the USB communication device class. This is particularly useful for applications requiring USB communication.
Note
- The TC321x doesn't have a USB module and therefore doesn't support this feature.
Configuration Method
Set the following in the feature_config.h file:
##define FEATURE_TEST_MODE TEST_USB_CDC
##define USB_CDC_ENABLE 1
RPA Test
Overview
The RPA test (TEST_LL_PRIVACY_SLAVE) demonstrates how to use the BLE RPA feature to prevent device tracking by periodically changing the device address. This is crucial for applications requiring user privacy protection.
Configuration Method
Set the following in the feature_config.h file:
##define FEATURE_TEST_MODE TEST_LL_PRIVACY_SLAVE
##define BLE_PRIVACY_ENABLE 1
Other Modules
24MHz Crystal External Capacitor
Refer to the position C19/C20 of the 24MHz crystal matching capacitor in the figure below.
The SDK defaults to use internal capacitance (that is, the cap corresponding to ana_8a\<5:0>) as the matching capacitance of the 24MHz crystal oscillator. At this time, C19/C20 does not need to be soldered. The advantage of using this solution is that the capacitance can be measured and adjusted on the Telink fixture to make the frequency value of the final application product reach the best.

If you need to use an external welding capacitor as the matching capacitor (C19/C20 welding capacitor) of the 24MHz crystal oscillator, just call the following API at the beginning of the main function (should be before the blc_app_loadCustomizedParameters() function and after the cpu_wakeup_init function):
static inline void blc_app_setExternalCrystalCapEnable(u8 en)
{
blt_miscParam.ext_cap_en = en;
WriteAnalogReg (0x8a, ReadAnalogReg(0x8a)|0x80);//close internal cap
}
As long as the API is called before cpu_wakeup_init, the SDK will automatically handle all the settings, including disabling the internal matching capacitor, no longer reading the frequency bias correction value, etc.
32KHz Clock Source Selection
The SDK supports the use of either the MCU's internal 32kHz RC oscillator circuit (referred to as 32kHz RC) or an external 32kHz RC oscillator circuit (referred to as 32kHz Pad). The error of 32kHz RC is relatively large, so for applications with long suspend or deep retention time, the time accuracy will be worse. At present, the maximum long connection supported by 32kHz RC by default cannot exceed 3s. Once exceeding this time, ble_timing will have errors, resulting in inaccurate packet receiving time points, prone to receiving and sending packets retry, increased power consumption, and even disconnection. The error is much smaller when using the 32kHz Pad.
The user only needs to call the following API at the beginning of the main function (should be before the cpu_wakeup_init function):
Call 32kHz RC:
void blc_pm_select_internal_32k_crystal(void);
Call 32kHz Pad:
void blc_pm_select_external_32k_crystal (void);
Special Notes for TC321x:
- The 32kHz clock only supports 32kHz RC, not 32kHz Pad.
Firmware Digital Signature
There is a method of malicious copying of products in the market. For example, if customer A develops a product using Telink's chip and SDK, customer A's competitor customer B, who also uses Telink's chip, gets the product and can copy the same hardware circuit design. If the product's data burn-in bus is not disabled, it is possible for customer B to read the complete firmware on the product's Flash, and customer B can copy the product using the same hardware and software.
To address these security risks, the SDK supports a software digital signature function. The principle is to take advantage of the fact that the chip's internal Flash has a unique UID. The product reads the 16 byte UID from the internal Flash during the fixture burn-in process and then performs a complex encryption operation with the contents of the Firmware to produce a set of checksum values called Signature, which are stored at the corresponding address in the Flash calibration area. Which is:
Signature = Encryption_function (Firmware, Flash_UID)
Signature is related to both Firmware and Flash_UID. The same calculation is done when the program is initialized on the SDK, the result is compared with the Signature burned on the fixture and if it does not match, the program is not legal and is prohibited from running.
It is important to emphasize that this feature involves a number of technical aspects, including the fitment of the jig, the corresponding configuration on the SDK, etc. Customers should confirm the details with Telink FAE in advance if required.
Below are some technical details of the implementation of this feature.
(1) The jig end should be correctly matched, including file configuration, writing scripts, etc. Please refer to the Telink testbench documentation and instructions for details.
(2) The Signature memory address is the Flash Calibration area offset address 0x180 continuous 16 byte.
(3) This feature is disabled by default on the SDK, to use it, enable the following macro in app_config.h.
#define FIRMWARES_SIGNATURE_ENABLE 1 //firmware check
Note
- There are only a few projects on the SDK that add Firmware digital signature verification to the initialization of the main function (see FIRMWARES_SIGNATURE_ENABLE by searching for it). If the customer is using a project that does not have this feature, please make sure to merge from another project to your own.
The code in the SDK is shown below and the program needs to be disabled when the digital signature does not match. The SDK uses the simplest while(1) to disable the program when the digital signature does not match, this is just a sample writeup, please evaluate for yourself if this method meets the requirements, if not you can use other methods such as putting the MCU into deepsleep, modifying various data, bss, stack segments etc. stack segments, etc.
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) The calculation method for the digital signature, Encryption_function, is defined by Telink and takes into account both the Firmware content and the Flash_UID, using AES 128 encryption. The details of the calculation are not publicly available and are packaged in a sealed library. The above firmware_encrypt_based_on_uid function is implemented in libfirmware_encrypt.a.
If customers feel that the generic encryption algorithm is not secure enough and need to use their own encryption algorithm, they can contact Telink FAE to discuss the solution.
Firmware Integrity Self-check
The Firmware Integrity Self-check function is used to check the integrity of the current firmware during initialization to prevent errors in the firmware from causing problems.
This function is disabled by default on the SDK, to use it, enable the following macro in app_config.h.
#define FIRMWARES_CHECK_ENABLE 1 //firmware check
Note
- There are only a few projects on the SDK that add the Firmware self-check function to the initialization of the main function (you can see this by searching for FIRMWARES_CHECK_ENABLE). If the customer is using a project that does not have this feature, please make sure to merge from another project to your own.
Referring to the introduction of Firmware CRC32 checksum in OTA chapter, the SDK adds the checksum value at the end when generating the Firmware, the last step of OTA upgrade can confirm whether the Firmware is complete by the checksum value. Here the Firmware integrity self-check is done with the help of CRC32 checksum value, during initialization the Firmware is read to calculate the CRC32 and compared, if it does not meet, the Firmware is considered corrupted.
The code is as follows, the simplest while(1) is used on the SDK to disable the run when the checksum fails, this is just an example write up, users need to modify this write up to suit their own UI design, such as LED indication errors etc.
#if FIRMWARE_CHECK_ENABLE
if(flash_fw_check(0xffffffff)){ //return 0, flash fw crc check ok. return 1, flash fw crc check fail
while(1); //Users can process according to the actual application.
}
#endif
It is clear from the above description of the principle and details that the Firmware self-check cannot detect 100% of program abnormalities, for example, if the Firmware is extensively damaged, the program may not be initialized properly and will not run here with the self-check function. Therefore, the self-check function can only be effective when the Firmware is less damaged.
Debug
TLKAPI_DEBUG Method Introduction
To facilitate printing information during debugging, the SDK provides the TLKAPI_DEBUG method. It uses GPIO to simulate UART printing with a fixed baud rate of 1Mbps. Users should define TX_PIN as needed and configure related information in app_config.h as follows:
#ifndef UART_PRINT_DEBUG_ENABLE
#define UART_PRINT_DEBUG_ENABLE 1
#endif
/////////////////////////////////////// PRINT DEBUG INFO ///////////////////////////////////////
#if (UART_PRINT_DEBUG_ENABLE)
#define DEBUG_INFO_TX_PIN GPIO_PB1
#define PULL_WAKEUP_SRC_PB1 PM_PIN_PULLUP_10K
#define PB1_OUTPUT_ENABLE 1
#define PB1_DATA_OUT 1
#endif
The master_kma_dongle supports USB printing mode, which requires the BDT tool to view the printing information. Note that in USB mode, initialization-related logs cannot be printed. Therefore, users are recommended to use the GPIO-emulated UART printing mode.
Related API Introduction
API tlkapi_debug_init
This is the initialization function, with the baud rate fixed at 1M. To use the TLKAPI_DEBUG method for log printing, users should call the tlkapi_debug_init interface in user_init_normal for initialization.
void tlkapi_debug_init(void)
{
#if (PRINT_BAUD_RATE != 1000000)
#error "GPIO simulate UART, support 1000000 baud rate only!!!*"
#endif
tlkDbgCtl.dbg_en = 1;
}
API blc_debug_enableStackLog
The SDK adds stack logging at the protocol stack lower layer for user debugging. The blc_debug_enableStackLog interface is used to set the stkLog_mask (default value is STK_LOG_DISABLE) to specify which log types the user needs to print.
/**
* @brief for user to configure which type of stack print information they want
* @param[in] mask - debug information combination
* @return none
*/
void blc_debug_enableStackLog(stk_log_msk_t mask);
Users can also use the blc_debug_addStackLog and blc_debug_removeStackLog APIs to add or remove specific mask types:
/**
* @brief for user to add some type of stack print information they want
* @param[in] mask - debug information combination
* @return none
*/
void blc_debug_addStackLog(stk_log_msk_t mask);
/**
* @brief for user to remove some type of stack print information they want
* @param[in] mask - debug information combination
* @return none
*/
void blc_debug_removeStackLog(stk_log_msk_t mask);
The stkLog_mask can be set to one of the following values, or a combination of multiple values using the "OR" operation.
/**
* @brief stack log
*/
typedef enum {
STK_LOG_DISABLE = 0,
STK_LOG_LL_CMD = BIT(0),
STK_LOG_LL_RX = BIT(11),
STK_LOG_LL_TX = BIT(12),
STK_LOG_SMP_RX = BIT(14),
STK_LOG_SMP_TX = BIT(15),
STK_LOG_ATT_RX = BIT(20),
STK_LOG_ATT_TX = BIT(21),
STK_LOG_OTA_FLOW = BIT(24),
STK_LOG_OTA_DATA = BIT(25),
STK_LOG_ALL = 0xFFFFFFFF,
}stk_log_msk_t;
STK_LOG_DISABLEis used to disable stack logging, preventing the printing of lower-layer log information.STK_LOG_LL_CMDis used to control log information at the LL layer, such as broadcasts and scans.STK_LOG_ATT_RXandSTK_LOG_ATT_TXare used to control the printing of ATT layer RX and TX information.STK_LOG_OTA_FLOWandSTK_LOG_OTA_DATAare used to control the printing of log information related to the OTA process.STK_LOG_ALLis used to print all lower-layer log information.
API tlkapi_send_string_data
This API is used to print a hexadecimal array and provides a 32-byte buffer to store the printed results. If the user prints an array exceeding 32 bytes, they can modify TLKAPI_DEBUG_DATA_MAX_LEN accordingly.
#define tlkapi_send_string_data(en, str, pData, len) if(en){tlkapi_send_str_data(str":%s\n",(u8*)(pData), len);}
/**
* @brief Send debug log to log FIFO, character string and data mixed mode.
* attention: here just send log to FIFO, can not output immediately, wait for "tlkapi debug_handler" to output log.
* @param[in] str - character string
* @param[in] pData - pointer of data
* @param[in] len - length of data
* @return none
*/
void tlkapi_send_str_data (char *str, u8 *pData, u32 data_len)
{
if(tlkDbgCtl.dbg_en){
/* user can change this size if "data_len" bigger than 32 */
#define TLKAPI_DEBUG_DATA_MAX_LEN 32
unsigned char hex[] = "0123456789abcdef";
unsigned char temp_str[TLKAPI_DEBUG_DATA_MAX_LEN * 3 + 1];
const u8 *b = pData;
u8 i;
u8 len = min(data_len, TLKAPI_DEBUG_DATA_MAX_LEN);
for (i = 0; i < len; i++) {
temp_str[i*3] = ' ';
temp_str[i * 3 + 1] = hex[b[i] >> 4];
temp_str[i * 3 + 2] = hex[b[i] & 0xf];
}
temp_str[i * 3] = '\0';
u_printf(str, temp_str);
}
}
API tlkapi_send_string_u32s
This API is used to print a string or multiple u32-type data values, with a variable number of data items.
/**
* @brief print debug log, character string and data mixed mode, with variable length data, data in "unsigned int" format
* @param[in] en - send log enable, 1: enable; 0: disable
* @param[in] str - character string
* @param[in] ... - variable length data, maximum length is 8
* @param[in] data_len - length of data
* @return none
*/
#define tlkapi_send_string_u32s(en, str, ...) if(en){tlkapi_send_str_u32s(str, COUNT_ARGS(__VA_ARGS__), ##__VA_ARGS__);}
API tlkapi_send_string_u8s
This API is used to print strings or multiple u8-type data, with a variable number of data items.
/**
* @brief Send debug log, character string and data mixed mode, with variable length data, data in "unsigned char" format
* @param[in] en - send log enable, 1: enable; 0: disable
* @param[in] str - character string
* @param[in] ... - variable length data, maximum length is 16
* @param[in] data_len - length of data
* @return none
*/
#define tlkapi_send_string_u8s(en, str, ...) if(en){tlkapi_send_str_u8s(str, COUNT_ARGS(__VA_ARGS__), ##__VA_ARGS__);}
API tlkapi_printf
This API provides printf output functionality.
/**
* @brief print log with GPIO simulate UART or USB
* @param[in] en - print log enable, 1: enable; 0: disable
* @param[in] fmt - the string will be printed
* @return none
*/
#define tlkapi_printf(en, fmt, ...) if(en){tlk_printf(fmt, ##__VA_ARGS__);}
These interfaces provide usage examples at the application layer for user reference.
Note
- Printing is not permitted within interrupts; it is only allowed in the main loop.
Q&A
Q: When compiling a project of the SDK, the following error is generated, how to solve it?

A: This situation is due to the default 16KB retention used in the SDK versions prior to V3.4.2.4, and some projects need to switch to a 32KB retention configuration due to the complexity of the project or the need for a larger buffer and the RAM in the retention_code section exceeds 16KB. The modification process is as follows.
Step 1 Right-click on the project, select Properties -> C/C++ Build -> Settings -> General, in Other GCC Flags change the configuration from 16K to 32KB.
Step 2 Paste the contents of the boot_32k_retn_....link file corresponding to the required boot in the boot directory into the boot.link in the subfolder, then clean the project and recompile it.
Q: How to create my own project in the SDK?
A: Generally, to ensure that the various settings in the project are in place, we usually build new projects based on a particular demo. For example, we use ble_sample as a base to complete a new project.
Step 1 Copy and paste the code and rename it. As shown below, we copy the code for ble_sample and newly name it Test_Demo.

Step 2 Right-click on the project, Properties -> Settings -> Manage Configurations, create a new project, for example: Test_Demo.

After clicking OK, you can see the new project in the project list, as shown below.

Step 3 Right-click on the Test_Demo folder, Resource Configurations -> Exclude from Build, tick all the items in Test_Demo's settings except for itself. And tick Test_Demo in the same settings for its copy source. This is shown as below.


Step 4 Change the Setting-TC32 Compiler-symbols in the Test_Demo property to the new symbols and click Apply after the change, as shown below:

Step 5 In vender/common/user_config.h, add corresponding to the settings for the new code. As shown below:

At this point, the new project has been built. You can now select the new project, clean it and build it for use.
Appendix
Appendix 1: crc16 Algorithm
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;
}
Appendix 2: BLE Module Instruction Set
| Field | Bytes | Description |
|---|---|---|
| cmdID | 2 | command ID |
| ParaLen | 2 | parameter length |
| Parameters | n | parameter payload |
| Field | Bytes | Description |
|---|---|---|
| Token | 1 | Always be 0xFF |
| ParaLen | 2 | parameter length |
| eventID | 2 | Event ID |
| Parameters | n | parameter payload |
BLE SPP commands represent instructions sent from the Host to the Telink BLE Module, while BLE SPP events represent synchronous or asynchronous events sent from the Telink BLE Module to the Host.
The SPP command set sent by the Host to the BLE Module is shown in the table below. The corresponding events are all command completion events.
| CmdFunction | CmdID | ParaLen | Parameters | Format |
|---|---|---|---|---|
| Set broadcast interval | 0xFF01 | 0x0002 | interval: e.g., 0x0050; Broadcast interval = 80 * 0.625ms = 50ms |
Example: 01 FF 02 00 50 00 |
| Set broadcast data | 0xFF02 | <=0x0010 | Set data, e.g., 01 02 03 04 | Example: 02 FF 04 00 01 02 03 04 |
| Enable/Disable broadcast | 0xFF0A | 0x0001 | Enable:0x01; Disable:0x00 |
Example: 0A FF 01 00 01 |
| Get available data buffer of the module | 0xFF0C | 0x0000 | N/A | 0C FF 00 00 |
| Set broadcast type | 0xFF0D | 0x0001 | 0x00:connectable undirected ADV 0x01:connectable directed adv 0x02:scannable undirected adv 0x03:non-connectable adv |
Example: 0D FF 01 00 00 |
| Set direct broadcast address | 0xFF0E | 0x0007 | parameter1:dirAddrType 0x00:public address 0x01:random address parameter2:dirAddr Example: 01 02 03 04 05 06 |
Example: 0E FF 07 00 00 01 02 03 04 05 06 |
| Add to white list | 0xFF0F | 0x0007 | parameter1:addrType 0x00:public address 0x01:random address parameter2: address Example: 01 02 03 04 05 06 |
Example: 0F FF 07 00 00 01 02 03 04 05 06 |
| Delete white list | 0xFF10 | 0x0007 | parameter1:addrType 0x00:public address 0x01:random address parameter2: address Example: 01 02 03 04 05 06 |
Example: 10 FF 07 00 00 01 02 03 04 05 06 |
| Reset white list | 0xFF11 | 0x0000 | N/A | 11 FF 00 00 |
| Set filtering policy | 0xFF12 | 0x0001 | 00:all device 01:connReq from all device scanReq from white list 02:scanReq from all device connReq from white list 03:scanReq and connReq from white list |
Example: 12 FF 01 00 00 |
| Set device name | 0xFF13 | <=0x0012 | Device name in hexadecimal format Example: 0A 09 08 07 06 05 04 |
Example: 13 FF 07 00 0A 09 08 07 06 05 04 |
| Get connection parameters | 0xFF14 | 0x0000 | N/A | 14 FF 00 00 |
| Set connection parameters | 0xFF15 | 0x0008 | parameters: u16 intervalMin; Example: 0x00A0 is the current device's minimum connection interval: 0xA0*1.25ms=200ms u16 intervalMax; Example: 0x00A2 is the current device's maximum connection interval: 0xA2*1.25ms=202.5ms u16 connLatency; Example: 0x0000 is the current device's desired new latency 0x00 u16 connTimeout; Example: 0x012C is the current device's desired new timeout value 0x12C*10ms=3000ms |
15 FF 08 00 A0 00 A2 00 00 00 2C 01 |
| Get module's current working status | 0xFF16 | 0x0000 | N/A | 16 FF 00 00 |
| Terminate connection | 0xFF17 | 0x0000 | N/A | 17 FF 00 00 |
| Restart Module | 0xFF18 | 0x0000 | N/A | 18 FF 00 00 |
| Enable or disable MAC binding function | 0xFF19 | 0x0001 | 0x01: Enable MAC binding function. After enabling, only MAC addresses in the MAC address table can connect to the module 0x00: Disable MAC binding function |
19 FF 01 00 01/00 |
| Add device MAC address to MAC binding table | 0xFF1A | 0x0006 | MacAddr Support public MAC addresses only Example: B4 CE BF 01 E7 60 |
Example: 1A FF 06 00 B4 CE BF 01 E7 60 |
| Remove a MAC address from the MAC address table | 0xFF1B | 0x0006 | MacAddr Example: B4 CE BF 01 E7 60 |
Example: 1B FF 06 00 B4 CE BF 01 E7 60 |
| Send data | 0xFF1C | <=0x0016 | Handle(2 bytes) of the Attribute "service to client" Example: 0x000D data payload (macLen: 20 bytes) Example: 01 02 03 04 05 |
Example: 1C FF 07 00 0D 00 01 02 03 04 05 |
Some SPP events are not implemented in the SDK module demo. Users may implement these functions in their own upper-layer code if required.
| Event Name | Type | Token | ParaLen | EventID | Parameters | Format |
|---|---|---|---|---|---|---|
| Command completion event | Synchronous | 0xFF | 0x0003 | Rule: eventID=(cmdID & 0x03FF) | 0x0400 Example: 0x0701 (cmdID=0xFF01, corresponding to “Set Broadcast Interval” command) |
Display status information Success: 0x00 Others: Refer to ble_sts_t |
Example: FF 03 00 01 07 00 |
| Data receive event | Asynchronous | 0xFF | n+5 | 0x07A0 | data(indicates received data, n bytes) Example: 01 02 03 04 05 06 |
Example: FF 0B 00 A0 07 52 15 00 01 02 03 04 05 06 |
| Get available buffer count event | Asynchronous | 0xFF | 0x0004 | 0x070C | 0x0004 | Example: FF 04 00 0C 07 00 04 |
| Connection event | Asynchronous | 0xFF | 0x0002 | 0x0783 | N/A | FF 02 00 83 07 |
| Disconnection event | Asynchronous | 0xFF | 0x0002 | 0x0784 | N/A | FF 02 00 84 07 |
| Channel map change event | Asynchronous | 0xFF | 0x0002 | 0x078A | N/A | FF 02 00 8A 07 |
| Connection parameter update event | Asynchronous | 0xFF | 0x0002 | 0x078B | N/A | FF 02 00 8B 07 |