Telink Driver SDK
Platform SDK Overview
SDK Introduction
Telink offers two sets of Platform SDKs, each corresponding to different chip architectures:
- tc_platform_sdk: Chip series with Telink's self-developed TC32 core
- tl_platform_sdk: Chip series with RISC-V architecture.
Both SDKs provide complete drivers, sample codes, and development tools to help you quickly build embedded applications based on Telink chips.
Based on core architecture, chips are divided into TC series and TL series. The table below summarizes the key features of all supported chips:
| series | SDK | Chip codename | The number of cores | Core | RAM | Cache | Mailbox channels |
|---|---|---|---|---|---|---|---|
| TC | tc_platform_sdk | B80 | 1 | TC32 | Single RAM | Cache in RAM | — |
| B80B | |||||||
| B85 | |||||||
| B87 | |||||||
| TC321x | |||||||
| TC122x | |||||||
| TC123x | |||||||
| TL | tl_platform_sdk | B91 | 1 | RISC-V D25F (with FPU) | IRAM+DRAM | I-Cache + D-Cache, Cache independent space |
— |
| B92 | 1 | RISC-V D25F (with FPU) | — | ||||
| TL321x | 1 | RISC-V D25F (Without FPU) | — | ||||
| TL322x | 2 | RISC-V D25F (with FPU) + N22 (without FPU) | D25F ↔ N22 | ||||
| TL323x | 1 | RISC-V D25F (with FPU) | — | ||||
| TL721x | 1 | RISC-V D25F (with FPU) | — | ||||
| TL751x | 3 | RISC-V D25F (with FPU) + N22 (without FPU) + DSP | D25F ↔ N22、D25F ↔ DSP、N22 ↔ DSP |
Note
- B91 includes TLSR951x and TLSR921x, B92 includes TLSR952x and TLSR922x, B80 / B80B includes TLSR8208 and TLSR8373, B85 includes TLSR825x and TLSR8359, B87 includes TLSR827x and TLSR8355.
- Multicore chips communicate via Mailbox; for details, see Mailbox.
SDK directory structure
tc_platform_sdk
tc_platform_sdk/
├── chip/ # Chip-related code
│ └── <chip>/ # For example, B80, B85, B87, TC321x
│ ├── boot/ # Startup file
│ └── drivers/ # Driver code (including source code and precompiled static libraries in lib/)
├── common/ # General code (basic types, strings, etc.)
├── demo/vendor/ # Sample code
└── project/ # Telink IoT Studio project files
└── tlsr_tc32/
└── <chip>/ # Chip projects, including .project/.cproject and boot.link
tl_platform_sdk
tl_platform_sdk/
├── chip/ # Chip-related code
│ └── <chip>/ # For example, B91, B92, TL321x
│ ├── boot/ # Startup file
│ ├── drivers/ # Driver code (including source code and precompiled static libraries in `lib/`)
│ └── link/ # Link scripts
├── common/ # Cross-chip common code
├── demo/vendor/ # Sample code
├── project/ # Telink IoT Studio project files
│ └── tlsr_riscv/
│ └── <chip>/ # Chip engineering, including .project/.cproject
└── 3rd-party/ # Third-party library
Directory Description
boot/: Startup file, responsible for power-on initialization: setting interrupt vector tables, initializing stack pointers, transporting data segments, and jumping to main function. TC and TL have different startup files; see Software Software Startup for detailsdrivers/: drivers, each peripheral module contains.hand.csource files; some modules (such as RF, PM) are provided as precompiled static libraries (.a) underlib/, with source code not disclosedlink/(TL): Link script that defines the program's memory layout, located atchip/<chip>/link/. The TC link script (boot.link) is located atproject/tlsr_tc32/<chip>/.See Software Startup for detailsproject/:Telink IoT Studio project file directory (Eclipse project.project/.cproject). TC is located atproject/tlsr_tc32/<chip>/,TL is atproject/tlsr_riscv/<chip>/demo/vendor/: sample code and reference designcommon/: Common code across different chips (such astypes.h,string.h,sdk_version.h, etc.)3rd-party/: Third-party open-source components (TL SDK only)reg_include/(TL): Register definition file (TL SDK only), containing register address mappings and bit-field definitions for each peripheral. The register definitions in the TC SDK are written directly into header files such asregister.h
SDK version
Both sets of SDKs embed version information at the end of the compiled firmware (the bin file), making it easy for tools or scripts to identify the SDK version used after burning.
Version format
The version is stored as a string with a $$$ delimiter, in the following format:
$$$<sdk_name>_<version>$$$
- TC series example:
$$$tc_platform_sdk_V3.4.0$$$ - TL series example:
$$$tl_platform_sdk_V3.9.0$$$
Implementation principle
The version number consists of three parts, with both SDKs implementing the same approach:
- Version macro definition (
common/sdk_version.h): Each chip model corresponds to an independent version macro (e.g.,B91_SDK_VERSION_NUM), whichSDK_VERSION_NUMuniformly references the version of the current compilation target. - Version array storage (
common/sdk_version.c): By combiningSDK_VERSION/SDK_VERSION1macros with C preprocessing#stringing operators, version macros are concatenated into complete delimiter strings, stored in thesdk_version[]array, and using__attribute__((section(".sdk_version")))to place it into a separate section named.sdk_version:
volatile __attribute__((section(".sdk_version"))) unsigned char sdk_version[] = {SDK_VERSION(SDK_VERSION_NUM)};
- Link script segment placement: The link script is kept via
KEEP(*(.sdk_version)) and placed at the end of the bin file, with the segment size included in theBIN_SIZE. TL SDK is located atchip/<chip>/link/*.link, TC SDK is atproject/tlsr_tc32/<chip>/boot.link.
Version identification
Since version information is fixed at the end of the bin file and wrapped by $$$ delimiters, it can be extracted directly from the binary file via script. The SDK provides tl_check_fw.sh scripts (../../tc_platform_sdk/tools/tl_check_fw_tool/tl_check_fw.sh) that use regular expression to match the $$$...$$$ pattern to extract and output the version. If the version information is not found at the end of the bin file, check whether sdk_version.c has been properly compiled and linked.
Boot ROM
Boot ROM Overview
The Boot ROM is the read-only boot code embedded inside the SoC and is the first program executed by the CPU after the device powers on and resets. It is the starting point of the entire system boot chain, responsible for the most basic hardware initialization and loading the next-stage bootloads.
The startup process of Telink chips is divided into two phases:
| Phase | Name | Description | Modifiable |
|---|---|---|---|
| Phase One | Boot ROM | The hardware boot logic is hardwired into the chip, and it cannot be modified | No |
| Phase Two | Software Startup | The SDK provides the startup files (cstartup. S + link file) | Yes |
Boot ROM, as the first phase, is executed first after powering on, resetting IO, or waking up from Deep Sleep (excluding deep retention). After completing the necessary initialization, it hands over control to the Software Startup in the second phase.
Boot ROM Features
Boot ROM has the following features:
- Read only, non-modifiable — hardwired in the chip's ROM, it cannot be erased, written, or modified by software after manufacturing, ensuring the integrity and security of the boot codes.
- Executes first after power-on — the first segment of codes executed after CPU reset. The hardware reset vector is fixed to the entry address of the Boot ROM; after the CPU powers on, the Boot ROM must be executed first
- Chip-level Root of Trust — Because it cannot be modified, the Boot ROM serves as the starting point of the trust chain for Secure Boot. In Secure Boot mode, the Boot ROM verifies the firmware image signature in the next phase. The boot process proceeds only after the signature verification succeeds.
ROM function modes
Boot Mode
The ROM supports two Boot Modes: Normal Boot and Secure Boot. Normal Boot is mandatory for all chip support; Secure Boot is optional and available on some chips, enabled through eFuse/OTP configuration. Once enabled, for security reasons, you cannot switch back to Normal Boot.
Normal Boot
Normal Boot is the default boot mode, executing the standard firmware startup process quickly and is suitable for general application scenarios.
Secure Boot
Secure Boot adds security-related features (configured to Secure Boot mode via eFuse or OTP):
- Before booting the firmware, the Boot ROM performs signature verification on the firmware
- Only firmware that passes signature verification will be executed
- If signature verification fails, the chip will refuse to start the firmware, preventing unauthorized or tampered codes from running Secure Boot provides end-to-end security protection starting from the hardware root of trust, suitable for applications with high security requirements.
Firmware Load Mode
ROM supports two Firmware Load Modes: Direct XIP and Load to RAM. These two are mutually exclusive; any chip can only be in one mode.
Direct XIP
There is no process of copying code from NVM (Non-Volatile Memory) to SRAM; instead, code in the NVM is executed directly via XIP. This article uses flash as an example.
Load to RAM
Move ramcode from the NVM to SRAM, and after the code is moved, it will jump to SRAM 0 to run.
Chip support
Chip series support different boot modes and firmware loading modes; see Chip support.
Normal Boot Multi-address Boot
Multi-address boot principle
The Multi-address Boot allows the same firmware to run on different Flash addresses without modifying the link file. The principle is to remap the access address sent by the CPU to the actual Flash physical address through a hardware address mapping mechanism.
The address mapping formula is as follows: flash_addr = CPU_addr + (CPU_addr > = size ? 0 : offset)
Parameter description:
| Parameters | Description |
|---|---|
cpu_addr |
The access address sent by the CPU |
offset |
Multi-address boot offset (configured by register) |
size |
Multi-address boot mapping window size (configured by register) |
flash_addr |
After adding the offset, the actual address sent to Flash |
Mapping logic:
- When
cpu_addr > = size,flash_addr = cpu_addr(no offset, direct pass-through) - When
cpu_addr < size,flash_addr = cpu_addr + offset(address offset within the window range)
In this way, the CPU always fetches and executes instructions starting from address 0. At the same time, the hardware automatically maps all accesses within a size range starting from address 0 to the offset position in Flash. This enables the same firmware to run from different Flash addresses.
Related registers
The multi-address boot is configured through the following two registers:
| Register | Description |
|---|---|
mspi_xip_core_offset |
Configure the offset for multi-address boot, which is the actual start address of the firmware in Flash |
mspi_xip_core_size |
Configure the size of the mapping window for multi-address boot, which is the range of addresses to be mapped |
These two registers are configured after the Boot ROM detects a valid Telink tag. Then, when the CPU jumps to the Flash address 0 to execute, hardware automatically completes the address mapping.
Example: The boot address is 256K (offset = 0x40000, size = 0x40000).
CPU address Flash physical address
+---------------------+ +---------------------+
| | | 0x00000 |
| >= size area | ------> | (0 KB) | < - Direct transmission
| (pass-through) | | ... |
| | | |
+---------------------+ +---------------------+
| | | 0x40000 |
| < size area | ========> | (256 KB) | < - Mapping, the actual location where the firmware is stored
| (addr + offset) | | ... |
| | | 0x7FFFF |
+---------------------+ +---------------------+
CPU fetches and executes from address 0
- CPU access address
0 ~ 0x3FFFF(< size) → mapped to Flash0x40000 ~ 0x7FFFF - CPU access address
> = 0x40000→ directly transmitted to the corresponding Flash address - Therefore, flashing firmware to the address starting from Flash 0x40000, that is, the address starting from 0x40000, is the valid firmware address to enable boot
Direct XIP starts the loading process
The complete process is as follows:
Chip power-on / resets
|
v
The Boot ROM begins to execute
|
v
Scan Flash sequentially according to the preset startup address list.
(Read at the offset of multiboot_offset[i])
|
v
Read 4 bytes at offset +0x20 to check if it is a Telink tag.
(Magic Number: 0x544c4e4b, i.e., ASCII "T L N K")
|
+--- Not found ---> Try the next boot address
|
+--- Found ---> Configure mspi_xip_core_size and mspi_xip_core_offset
|
v
Jump to the Flash address 0 to execute user firmware.
Telink Marking (Magic Number) Explanation:
- The value is
0x544c4e4b, corresponding to the ASCII charactersTLNK - Stored at a fixed offset in the firmware image in Flash (base address +
0x20). - The Boot ROM reads this tag to determine whether a valid firmware is present at the corresponding address
Direct XIP pseudocode implementation
Below is the pseudocode in the Boot ROM that implements multi-address booting and program loading:
unsigned int telink_flag = 0;
unsigned short multiboot_offset[N] = {addr0, addr1, ...};
for (unsigned char i = 0; i < N; i++)
{
//Read 4 bytes at the start address offset +0x20 and check the Telink tag
flash_read_page((multiboot_offset[i] ) + 0x20, 4, (unsigned char *)&telink_flag);
if (0x544c4e4b == telink_flag)
{
//Find the valid firmware and configure the multi-address boot register.h
mspi_xip_core_size(multiboot_offset[i]);
mspi_xip_core_offset(multiboot_offset[i]);
//Jump to the Flash address 0 to execute
run_flash_start_addr(0);
}
}
Code explanation:
- Define the startup address array
multiboot_offset[], containing N preset startup addresses (N values vary by chip; see Chip support - Traverse each startup address and read 4 bytes at
(multiboot_offset[i] + 0x20 - Check whether the read value equals
0x544c4e4b(Telink mark) - If matched, configure
mspi_xip_core_sizeandmspi_xip_core_offsetregisters - Jump to the Flash address 0 and hand control over to the user's firmware
Load to RAM to start the loading process
Similar to Direct XIP pseudocode, you need to copy a size of RAM_code_size*16 bytes from the corresponding flash address to SRAM.
unsigned int telink_flag = 0;unsigned int telink_info = 0;
unsigned short multiboot_offset[N] = {addr0, addr1, ...};
#define IC_CT 0x800602
for (unsigned char i = 0; i < N; i++)
{
//Read 4 bytes at the start address offset +0x8 and check the Telink tag
flash_read_page((multiboot_offset[i] ) + 0x8, 4, (unsigned char *)&telink_flag);
if (0x544c4e4b == telink_flag)
{
//Find the valid firmware and configure the multi-address boot register.h
mspi_xip_core_size(multiboot_offset[i]);
mspi_xip_core_offset(multiboot_offset[i]);
flash_read_page((multiboot_offset[i] ) + 0x0c, 4, (unsigned char *)&telink_info);
//Get ram_code_size at the start address offset +0x0c
unsigned short ram_code_size = telink_info;
//Get cmd at the start address offset +0x0e
unsigned char cmd = telink_info >> 16; Start address offset +x0e
//Copy ram code *16 bytes from the start address to the SRAM address 0
load_flash_to_sram(ram_code_size* 16);
// reboot from sram 0 address
write_reg8(IC_CT, cmd);
}
}
Code explanation:
- Define the startup address array
multiboot_offset[], containing N preset startup addresses (N values vary by chip; see Chip support. - Traverse each startup address, read 4 bytes at the Flash offset
+0x08, and check the Telink marker0x544c4e4b - If matched, configure the
mspi_xip_core_sizeandmspi_xip_core_offsetregisters - Read 4 bytes at the Flash offset
+0x0c; the lower 16 bits areram_code_size, and the upper 8 bits arecmd - Call
load_flash_to_sramto copyram_code_size * 16bytes from Flash to SRAM - Write
cmdto theIC_CTregister to trigger startup from SRAM address 0
Chip support
Chip mapping
The support status for each chip series is as follows:
| Chip series | Boot Mode | Firmware Load Mode | Address group |
|---|---|---|---|
| TL321x / TL322x / TL323x / TL721x / TL751x | Normal + Secure | Direct XIP | A |
| B92 | Normal + Secure | Direct XIP | B |
| B91 | Normal | Direct XIP | B |
| B80 / B80B / B85 / B87 / TC321x / TC123x | Normal | Load to RAM | B |
Address group definition
List of startup addresses corresponding to the address groups in the table above:
| Address group | N | Boot address list |
|---|---|---|
| A | 9 | 0、0x10000(64K)、0x20000(128K)、0x40000(256K)、0x80000(512K)、0x100000(1M)、0x200000(2M)、0x400000(4M)、0x800000(8M) |
| B | 4 | 0、0x20000(128K)、0x40000(256K)、0x80000(512K) |
Note
Firmware size limits
For B85 / B87 / B91 series chips, when booting from a non-zero address, there is an upper limit on the size of firmware files:
| Boot address | Maximum firmware file size | Calculation method |
|---|---|---|
0x20000 (128K) |
124KB | 128K - 4K (reserved area) |
0x40000 (256K) |
252KB | 256K - 4K (reserved area) |
0x80000 (512K) |
508KB | 512K - 4K (reserved area) |
- When firmware needs to boot at the address
0x20000, the compiled firmware file size must not exceed 124KB - When firmware needs to boot at the address
0x40000, the compiled firmware file size cannot exceed 252KB - When firmware needs to boot at the address
0x80000, the compiled firmware file size cannot exceed 508KB
If the firmware size exceeds the corresponding limit, it will cause startup failure or operational abnormalities. During development, the firmware size should be planned based on the target boot address.
Software Startup
Overview
The startup process of Telink chips is divided into two phases:
| Phase | Name | Description | Modifiable |
|---|---|---|---|
| Phase One | Boot ROM | The hardware boot logic is hardwired into the chip, and it is cannot be modified | No |
| Phase Two | Software Startup | The SDK provides the startup files (cstartup. S + link file) | Yes |
The first phase, Boot ROM, completes the most basic hardware initialization and firmware loading. For details, see Boot ROM
This chapter focuses on the second phase of Software Startup, that is, cstartup.S and Link files work together to create a C language runtime environment. Do not modify the boot file unless you fully understand the startup process.
Different chips have different RAM architectures and firmware load modes. For specific classifications, see Platform SDK Overview and Boot ROM chapters.
S/Link file selection and configuration
cstartup.S (S file) is responsible for establishing the C language runtime environment, while the Link file defines the memory layout of each section in Flash/RAM. Both must be used together.
Different Firmware Load Modes have different S/Link file organizations. Confirm which mode the chip you are using (see Chip mapping in Boot ROM), then select and configure according to the corresponding section.
S files and Link files must be used as a matched set and cannot be mixed across loading modes. cstartup with Load to RAM cannot be paired with Direct XIP link files, and vice versa—the section definitions, copy logic, and boundary symbols differ, and mixing them can cause boot failures or run errors.
Load to RAM
The chip with the Load to RAM mode uses a link file (boot.link), and the Flash/SRAM mode is selected through macros in the S file:
| S files | Macro | Description |
|---|---|---|
cstartup_flash.S |
MCU_STARTUP_FLASH |
Default boot file switches Flash/SRAM mode via ALL_SRAM_CODE macro |
cstartup_otp.S |
MCU_STARTUP_OTP |
OTP launch |
ALL_SRAM_CODE controls two compilation modes:
ALL_SRAM_CODE |
Mode | Description |
|---|---|---|
| 0 (default) | Flash mode | The S file writes .ram_code to the Firmware Header, the Boot ROM only moves .vectors/.ram_code to SRAM, and the rest of the codes is fetched from Flash via I-Cache |
| 1 | SRAM mode | The S file writes the full bin size to the header file, the Boot ROM moves all codes to SRAM, the I-Cache Tag is set to the end of SRAM (size=0), and all instructions are fetched from SRAM |
The SRAM mode is to move the entire bin by the Boot ROM, not just the ram_code:S file, which tells the Boot ROM the size to be moved via the configuration word in the header offset 0x08. In SRAM mode, the bin file can be programmed to Flash or loaded directly into SRAM.
Some chips have cstartup_sram. S、cstartup_copy_ramcode. S independent S files, and additional link files like boot_normal.link and boot_copy_ramcode.link, are used for specific demo scenarios, and standard engineering uniformly uses boot.link.
User-configurable macros:
| Macro | Default values | Description |
|---|---|---|
ALL_SRAM_CODE |
0 | 0: Flash mode, only ram_code in SRAM; 1: SRAM mode, all code is in SRAM. Automatic derivation: FLL_STK_EN/ZERO_IC_TAG_EN/COPY_DATA_EN |
SRAM_SIZE |
It depends on the chips | SRAM size must be set according to the chip datasheet (e.g., SRAM_32K/SRAM_48K/SRAM_64K). Misconfigurations affect efficiency or cause operational failures |
WD_SET_AND_START_EN |
0 | Whether to preset and start the watchdog in the boot file. It is recommended to enable it; by default, it is not enabled to be compatible with existing user codes |
BLC_PM_DEEP_RETENTION_MODE_EN |
1 | Deep Sleep Retention wake-up enabled; when turned off, retention wake-up is not supported but reduces the amount of boot codes |
SKIP_RF_DRV |
Not enabled | Skip rf_sw_config (RF initialization) after definition. This function must be executed before section relocation (it occupies a large stack; executing in C may overflow and overwrite SRAM). The code is in large and time-consuming. Only BQB/EMI/Test scenarios that do not require RF can be skipped with this macro (saving about 6389 bytes of Flash). |
__IRQ_STK_SIZE__ |
0x180 | Independent stack size in the IRQ mode. Under the ARM mechanism, IRQ and SVC each have independent SPs. It is not recommended to modify it; the default value already meets the requirements |
DP_THROUGH_SWIRE_DIS |
1 | Disable dp_through_swire to prevent the DP/DM pin from accidentally triggering swire timing, which could lead to chip miswrites. See DP_THROUGH_SWIRE_DIS for detailed explanation |
Direct XIP
The chip with the Direct XIP mode selects different S/Link files according to the usage scenario. The two files must be used in pairs and cannot be mixed:
| Scenarios | S files | Link files | Description |
|---|---|---|---|
| Flash Boot (default) | cstartup_flash.S |
flash_boot.link |
BIN is flashed to Flash and executed in Flash (XIP); most SDK demos use this configurations |
| Performance Testing | cstartup_flash.S |
flash_boot_ramcode.link |
Except for vector tables, all code runs in RAM to avoid Flash latency and improve performance (used for CoreMark/Dhrystone, etc.), and is limited by IRAM size |
| RAM-Only Execution | cstartup_ram.S |
ram_boot.link |
Programs can only be programmed to and executed from SRAM, not to Flash |
The naming rules and writing of slave boot files for multi-core chips are consistent with those of the master core, located in each core's boot subdirectory. For the relationship between macros in D25F and S/link files in coprocessor-core, see Multi-core.
User-configurable macros:
| Macro | Default values | Description |
|---|---|---|
SRAM_SIZE |
It depends on the chips | SRAM size must be set according to the chip datasheet. Incorrect configuration of the ILM/DLM software sharing settings on the chip may lead to improper ILM/DLM partitioning and program execution errors (crash). |
DP_THROUGH_SWIRE_DIS |
1 | Disable dp_through_swire to prevent the DP/DM pin from accidentally triggering swire timing, which could lead to chip miswrites. See DP_THROUGH_SWIRE_DIS for detailed explanation |
RF_CERTIFICATION_CGF_EN |
Not enabled | Enabled in RF authentication scenarios. Not enabled in other scenarios |
DP_THROUGH_SWIRE_DIS Detailed Description(This macro works in both Load to RAM and Direct XIP):
- Function: Set to
1to disabledp_through_swire, set to0to enable. - Risk:
dp_through_swireenabled by default. When the DP/DM pin is used as GPIO and triggersdp_through_swiretiming, it may crash due to chip miswriting via swire, so it is disabled by default in the boot file (set to1). - Side effect of setting it to
1: If the chip only has the DP pin and no SWS pin, disabling it will cause: (1) During fixture production, programming the firmware with this function disabled to the unprogrammed chip will prevent reprogramming due to swire communication failure; (2) Programming the firmware with this function disabled using the BDT tool may prevent debugging due to swire communication failure. - Precautions set to
0: If the above side effects are unacceptable, set to0to enabledp_through_swire, but be careful not to triggerdp_through_swiretiming on the DP/DM pin. - USB applications:
usb_set_pin(1)will re-enabledp_through_swire,usb_set_pin(0)will be disabled.
Firmware Load Mode and sectiont position
For a complete concept of Firmware Load Mode, multi-address boot, and Boot ROM scanning process, refer to Boot ROM.
Firmware Load Mode determines the instruction fetch path after powering on, which in turn determines the location of the startup sections (.vectors / .retention_reset) in Flash/RAM, and also determines the field allocation for the Firmware Header. This is the foundation for understanding the subsequent Link file section assignment and the S-file startup process.
| Firmware Load Mode | Power-On / Deep Sleep wake-up behavior | Retention: wake-up behavior | Entrance section |
|---|---|---|---|
| Load to RAM | The hardware moves codes from Flash to RAM, and boots from RAM | Boot directly from RAM | .vectors |
| Direct XIP | Jump to Flash Start Address to fetch instructions (XIP) | Boot from IRAM | .vectors (power-on) / .retention_reset (retention) |
- Load to RAM: Both power-on and retention boot from RAM, requiring only one
.vectorsstartup section (located at the start of RAM). - Direct XIP: Power-on boots from Flash, retention boots from IRAM, requiring two boot sections:
.vectors(Flash start address) and.retention_reset(IRAM start address).
Firmware Header
The starting area of firmware files in Flash is the fixed header (Firmware Header) agreed upon by hardware boot ROM, the programmer, and OTA tools. The S file writes field values at the corresponding offsets with the .org. This area is not open to users, and users cannot place custom codes there. The header allocation for the two Firmware Load Modes is different:
Load to RAM(0x00 ~ 0x1f):
| Address range | Contents of the S file | Function |
|---|---|---|
| 0x00~0x03 | tj __reset |
Reset jump instruction (the first instruction fetched by Boot ROM) |
| 0x04~0x05 | firmwareVersion | Firmware version number |
| 0x06~0x07 | firmware signature flag | Firmware signature mark during OTA updates |
| 0x08~0x0b | "TLNK" Flag | Telink firmware identification (for the programmer/OTA to identify) |
| 0x0c~0x0d | RAM code size | RAM code transfer size, Unit: 16 bytes |
| 0x0e | 0x88 |
Chip reset instruction (Boot writes Digital Register 0x602) |
| 0x0f | 0x00 |
Reserved |
| 0x10~0x13 | tj __irq |
Interrupt jump instruction |
| 0x18~0x1b | _bin_size_ |
Firmware size |
0x0c~0x0f These 4-byte configuration words will be written to the Digital Register 0x602 by the Boot ROM:
- 0x0c~0x0d (RAM code size): RAM code transfer size, unit: 16 bytes.
_ramcode_size_div_16_align_256_is actually written in the S file (the RAM code size is aligned by 256 first, then divided by 16). WhenALL_SRAM_CODEmacros is defined, write_bin_size_div_16(the total firmware size divided by 16, meaning all the codes are placed in RAM). - 0x0e(
0x88): Chip reset instruction, Boot writes it to 0x602. 0x0f is a placeholder for0x00, with no meaning.
Direct XIP(0x00 ~ 0x27):
| Address range | Contents of the S file | Function |
|---|---|---|
| 0x00~0x05 | j _START |
Reset jump instruction (the first instruction fetched by Boot ROM) |
| 0x06~0x07 | firmware signature flag | Firmware signature mark during OTA updates |
| 0x18~0x1b | BIN_SIZE | Firmware size |
| 0x20~0x23 | "TLNK" Flag | Telink firmware identification (for the programmer/OTA to identify) |
| 0x24~0x27 | Fixed Config | Hardware configuration bit |
Link file and memory layout
The Link file defines the location of each section in Flash/RAM, including which sections are in Flash, which are in RAM, and the size and alignment of each section. This chapter introduces the reasons for dividing the sections in Link files, memory allocation, and the usage methods.
The relation between S files and Link files: Link files define the boundaries and addresses of each section through symbols (such as _start_data_), while S files (cstartup. S) determine the source and destination address for the transferring based on these symbols. If a Link file is missing a symbol, referencing that symbol in the S file will cause a compilation error.
LMA and VMA
The Link file specifies two addresses for each section via AT():
- LMA (Load Memory Address): The storage address of the section in the firmware file, usually located in Flash. The programmer writes sections to Flash via LMA.
- VMA (Virtual Memory Address): The address used by the CPU during execution to access the section, i.e., the RAM/Flash address where the section actually resides.
The relationship between the two determines whether software transfer is needed when boots:
| When | Meaning | Boot processing |
|---|---|---|
| LMA = VMA | Section storage address = runtime address | No software transfer required, direct CPU access |
| LMA ≠ VMA | Section stored in Flash, but in runtime, it must be in RAM | When cstartup starts, sections are moved from LMA to VMA |
For example, the .data section: the initial value exists in Flash (LMA), CPU accesses the RAM (VMA) in the runtime. When boots, cstartup copies the initial value from LMA to VMA. The .bss section only has VMA (NOLOAD), does not occupy Flash, and is reset directly when boots.
Section Overview
The table below lists the uses and position of each section in different Firmware Load Modes. For specific functions and user usage of each section, please refer to the section Detailed Explanation of Section Function.
| Section name | Function | Load to RAM | Direct XIP |
|---|---|---|---|
.vectors |
Power-on startup code (cstartup) | RAM starts, hardware copy | Flash starts, XIP executes |
.retention_reset |
Retention wake-up startup code | None | IRAM starts, software copy |
.ram_code |
The critical function resident in RAM | RAM starts, hardware copy | IRAM, software copy |
.retention_data |
Variables retained during the Retention period | RAM, software copy | IRAM retention zone |
.text |
Function code in Flash | Flash, fetch instructions via I-Cache | Flash,XIP |
.rodata |
read-only data (const) | Flash, read via I-Cache | Flash |
.data |
The global variables have been initialized | RAM, software copy | DRAM, software copy |
.bss |
The global variables have not been initialized | RAM, reset | DRAM, reset |
.aes_data / .my_code |
Hardware-specific NOLOAD section | .my_code |
.aes_data |
.sdk_version |
SDK version number string | Flash end | Flash end |
Memory allocation diagram
Different Firmware Load Modes correspond to different RAM and cache designs (see Platform SDK overview for specific classifications):
- Load to RAM: Single RAM + Cache in RAM. RAM handles both code execution and data storage, while I-Cache occupies RAM space.
- Direct XIP: IRAM + DRAM + Cache independent space, with IRAM (code) and DRAM (data) physically separated. Cache is an independent hardware module that does not occupy IRAM/DRAM space.
In the figure, the actual values of <FLASH_BASE>, <SRAM_BASE>, <IRAM_BASE>, <DRAM_BASE>, <SRAM_SIZE> vary by chip. Please refer to the corresponding chip's datasheet.
Load to RAM
The VMA of the code section in the lst file is displayed in the instruction address space (starting from 0x0), not the RAM physical address. For details, see View section assignments through lst files. For details on each section's functions, see Detailed Explanation of Section Function.
Flash (<FLASH_BASE>) RAM (<SRAM_BASE>)
+-------------------------+ +-------------------------+ <SRAM_BASE>
| .vectors | --Boot ROM move->| .vectors |
+-------------------------+ +-------------------------+
| .ram_code | --Boot ROM move->| .ram_code |
+-------------------------+ +-------------------------+ <- _ramcode_size_align_256_ = _ictag_start_ (Tag base)
| | | IC_TAG (256B) |
| .text + .rodata | XIP via I-Cache +-------------------------+ <- _ictag_end_
| .eh_frame | | IC_CACHE (2K) |
+-------------------------+ +-------------------------+
| .retention_data | --cstartup copy->| .retention_data |
+-------------------------+ +-------------------------+
| .data | --cstartup copy->| .data |
+-------------------------+ +-------------------------+
| .sdk_version | | .bss NOLOAD |
+-------------------------+ +-------------------------+
| Stack |
+-------------------------+ <- <SRAM_BASE> + <SRAM_SIZE>
Key points:
.vectorsand.ram_code: Transferred from the hardware boot ROM to RAM; when the CPU fetches instructions, it reads directly from RAM (starting from VMA=0x0 in lst)..textand.rodata: do not occupy RAM space; actual data is stored in Flash, and when the CPU fetchs the instructions, it is read from Flash via I-Cache..dataand.retention_data: LMA in Flash, VMA in<SRAM_BASE>+, and when boots, cstartup software copies them..bss: NOLOAD section, reset when boots.
Direct XIP
Flash (<FLASH_BASE>) IRAM (<IRAM_BASE>)
+-----------------------+ +-----------------------+ <IRAM_BASE>
| .vectors | XIP | |
+-----------------------+ +-----------------------+
| .retention_reset | copy-> | .retention_reset |
+-----------------------+ +-----------------------+
| .retention_data | copy-> | .retention_data |
+-----------------------+ +-----------------------+
| .ram_code | copy-> | .ram_code |
+-----------------------+ +-----------------------+
| .text + .rodata | XIP | .aes_data NOLOAD |
| .eh_frame | +-----------------------+
+-----------------------+
| | DRAM (<DRAM_BASE>)
| | +-----------------------+ <DRAM_BASE>
| .data | copy-> | .data |
+-----------------------+ +-----------------------+
| .sdk_version | | .sbss + .bss NOLOAD |
+-----------------------+ +-----------------------+
| Heap |
+-----------------------+
| Stack (_STACK_TOP) |
+-----------------------+
IRAM and DRAM are physically independent. The .data section is copied from Flash to the .data section in DRAM.
Detailed Explanation of Section Function
The purpose of each section and how to use it. The LMA/VMA relationship and the differences between the two RAM architectures were introduced. For specific section addresses and sizes,refer to the link file of the corresponding chip or the lst file generated by compilation (see View section assignments through lst files).
.vectors / .retention_reset (boot section)
Assembly startup code (cstartup_*. S) is the first code executed after the chip is powered on.
Single RAM (only .vectors):
- Located in the low address area of RAM, the hardware boot ROM moves it from Flash to RAM, and when the CPU fetches instructions, it reads directly from the RAM.
- Both power-on and retention wake-up are performed from the VMA of
.vectors(the RAM start area is not powered down).
IRAM+DRAM (with .vectors and .retention_reset):
.vectorsis located at the start of Flash; when powered on, the Boot ROM directly jumps to fetch instructions (XIP) without any relocation..retention_resetis located at the start of IRAM; after retention wake-up, execution is performed from here (the IRAM start area is not power-off). LMA is in Flash and is transferred on-demand to IRAM upon startup.
.ram_code (resident memory codes)
Functions that require resident RAM to run. There are two reasons:
- Timing Coflict Avoidance: Flash operations-related functions involve timing multiplexing of MSPI pins. If executed in Flash, it can cause timing conflicts with Flash reads and cause crashes, so they must reside in RAM.
- Performance and Power Consumption: Functions resident in RAM do not need to be fetched from Flash on each call, resulting in faster execution. The SDK places time-sensitive functions (such as interrupt handling) in this section to reduce power consumption.
Usage: Add the keyword __attribute__((section(".ram_code"))) when defining the function; after compilation, the function will be included in the .ram_code section. Typical scenarios: interrupt handling functions, Flash read/write functions, timing-sensitive RF functions.
Note
.ram_codeoccupies RAM space. RAM resources are limited, so this keyword should only be used for functions that are genuinely needed.
.retention_data (Retain Data)
Global variables that need to retain their values during Deep Sleep Retention (i.e., remain powered). Ordinary global variables are located in the .data / .bss sections and will be lost in retention mode; Variables placed in the .retention_data section are located in the RAM retention area and retain their original values after wake-up.
Usage: Add the keyword __attribute__((section(".retention_data"))) when defining variables.
.retention_data occupies RAM retention areas, limiting capacity. If the limit is exceeded, the compile link will report an error. Solution: Reduce retention variables.
.text / .rodata (Flash codes and read-only data)
.text: Functions in the program without theram_codekeyword are included in this section by default and are located in Flash. IRAM+DRAM: executed via XIP ; Single RAM: executed by fetching instructions from Flash via I-Cache. This is usually the largest section in the firmware..rodata: Read-only data defined with theconstkeyword (such as lookup table, ATT table, etc.), located in Flash. IRAM+DRAM: read via D-Cache; Single RAM: instructions and data share the same cache, and.rodatais also read from Flash via the I-Cache. Neither of the two RAM architectures consumes no RAM.
These two sections do not occupy RAM space, but access must be done via the cache.Note that cache may return stale values.
- Stale Cache Read Issue:: Flash data is cached after being read. If the Flash region is erased and updated later, but old data is still cached in the cache, and pointer access is used again, the CPU may directly return the old value in the cache instead of the new data in Flash.
- Correct approach: When reading Flash data that may be rewritten, use the SDK's flash read interface (such as
flash_read_page). These interfaces bypass the cache to access Flash directly, ensuring access to the latest data.
Codes that reside in RAM (the .ram_code section) does not pass through the cache, so does not have this issue.
.data / .bss (global variable)
.data: A global variable that has been initialized (initial value is not 0). LMA in Flash, VMA in RAM (LMA ≠ VMA), and when boots, cstartup transports to RAM..bss: A global variable that is not initialized or initialized to 0. Only VMA and NOLOAD (which do not occupy Flash space) are reset directly by cstartup in RAM when boots.
IRAM+DRAM also includes .sdata / .sbss (small data section), which are respectively included in .data / .bss Single RAM does not have this section.
.aes_data / .my_code (hardware-specific NOLOAD section)
Some chips retain fixed areas in RAM for hardware modules; this section is NOLOAD (does not occupy Flash, is reset at boot or remains random):
- IRAM+DRAM:
.aes_data, hardware AES module cache, fixed at 32 bytes, located within the first 64KB of IRAM, fixed location and unchangeable. - Single RAM:
.my_code, used for SPI slave buffers and other special purposes.
This is the dedicated section determined by chip hardware design, and users generally do not need to operate it.
.sdk_version (Version number section)
Stores the SDK version string, located at the end of Flash, starting and ending with the special character $$$ to facilitate identification in firmware files. See Platform SDK overview for details.
S file and startup process
S file (cstartup. S) is the second piece of codes executed after the chip powers on. It performs section transfer, BSS zeroing, cache initialization, and other operations based on the section boundary symbols defined in the Link file, and finally jumps to the main() function.
The behavior of the S file is determined by the Firmware Load Mode, which has different entry sections and startup logic between the two modes:
- Load to RAM: Power-on and retention share a
.vectorsentry; the S file determines wake-up type based onana_reg_0x7eregister values. - Direct XIP: Power-on enters through the
_RESET_ENTRYentry of.vectors, retention enters through the_IRESET_ENTRYentry of.retention_reset; the two entrances are completely independent.
For the concept of Firmware Load Mode, see Boot ROM. For the layout of the Firmware Header field, see Firmware Header.
Load to RAM
The boot process for load to RAM is generally the same; this section only describes the general process. For the macro configuration involved in the flowchart, see Load to RAM.
The feature of Load to RAM is that power-on, Deep Sleep wake-up, and Retention wake-up share the same entry (__reset). In the process, the wake-up type is determined by the ana_reg_0x7e register value then select different paths:
ana_reg_0x7e |
Wake-up type | Different paths |
|---|---|---|
0x00 |
Retention wake-up | Perform multi-address register restore (restore g_pm_multi_addr and other Flash read command configurations); Skip the .retention_data section (retain this section in SRAM during retention without loss) |
!= 0x00 |
Power on / Deep Sleep wakes up | Transfer the .retention_data section (Flash → SRAM); no multi-address register restore |
After both paths meet, they execute: .data transport → .bss reset → jump to main().
Multi-address register restore: During retention, if Flash loses power, after wake-up, the multi-address startup configuration must be restored to ensure the configuration matches before the power outage. The power-on path does not require this step (configuration will be done in the boot ROM).
Retention prerequisite: retention RAM must be greater than the total size of .retention_data + .ram_code sections; otherwise, all sections cannot be retained.
The diagram below shows the common steps shared by chips:
__reset (@.vectors)
|
v
+-----------------------------+
| 1. (Optional) Watchdog | <--- WD_SET_AND_START_EN
| Preset & Start |
+-------------+---------------+
|
+---------v---------+
| 2. Disable | <--- DP_THROUGH_SWIRE_DIS
| dp_through_ |
| swire |
+---------+---------+
|
+---------v---------+
| 3. (Optional) | <--- Some chips execute
| Flash Wakeup | on power-on
| | (send 0xAB cmd)
+---------v---------+
|
+---------v---------+
| 4. Init SP | <--- IRQ/SVC mode stack ptr
+---------+---------+
|
+---------v---------+
| 5. Clear .bss |
+---------+---------+
|
+---------v---------+
| 6. Init I-Cache | <--- Clear IC_TAG + set
| Tag/Cache | cache boundary
| | (_ramcode_size_align_256_)
+---------+---------+
|
+---------v---------+
| 7. Check ana_0x7e | <--- Determine wakeup type
+---------+---------+
/ \
/ 0x00 \ !=0x00
/ \
v v
+-----------+ +-----------+
| Retention | | Power-On |
| Path | | Path |
+-----------+ +-----------+
| |
v v
+-----------+ +-----------+
| 8a. Multi | | 8b. Copy |
| Address | | .retention|
| Restore | | _data |
| | | Flash-> |
| | | SRAM |
+-----------+ +-----------+
| |
v v
+-----------------------------+
| 9. Copy .data |
| Flash -> SRAM |
+-------------+---------------+
|
+---------v---------+
| 10. Jump to main()|
+-------------------+
Direct XIP
In the Direct XIP mode, chip's power-on/deep sleep wake-up enters through the _RESET_ENTRY entry of the .vectors section, while retention wake-up enters through the _IRESET_ENTRY entry of the .retention_reset section; the two entrances are completely independent.
The Direct XIP startup process follows a consistent framework, divided into three stages: hardware environment initialization→ interrupt/cache initialization, → section transfer and clearing.
Power-on / Deep Sleep Wake-up Process (.vectors)
Enter from the _RESET_ENTRY entry of the .vectors section (located at the Flash start address, Boot ROM directly jumps to fetch instructions, execute in place (XIP)).
_RESET_ENTRY (@.vectors)
|
v
+=============================================+
| Phase 1: Hardware Environment Init |
+=============================================+
|
+---------v---------+
| 1. Enable Watchdog| <--- 10s power-on guard
+---------+---------+
+---------v---------+
| 2. (Optional) | <--- Few chips support
| ILM/DLM Config | (see S file comments)
+---------+---------+
+---------v---------+
| 3. Init GP Reg | <--- __global_pointer$
+---------+---------+
+---------v---------+
| 4. Disable | <--- DP_THROUGH_SWIRE_DIS
| dp_through_ | (not SWIRE debug port)
| swire |
+---------+---------+
+---------v---------+
| 5. Init Stack | <--- _STACK_TOP
+---------+---------+
+---------v---------+
| 6. (Optional) | <--- Chip-specific
| Chip-specific | leakage/aging handling
| Init |
+---------+---------+
+---------v---------+
| 7. (Optional) | <--- If __nds_execit
| Init EXEC.IT | (Andes CoDense)
| Table |
+---------+---------+
+---------v---------+
| 8. (Optional) | <--- __riscv_flen ctrl
| Enable FPU | (check Overview)
+---------+---------+
|
v
+=============================================+
| Phase 2: Interrupt / Cache Init |
+=============================================+
|
+---------v---------+
| 9. Config Int | <--- mtvec + PLIC
| mtvec + PLIC | vectored mode
+---------+---------+
+---------v---------+
| 10. Enable I/D- | <--- mcache_ctl = 0x3
| Cache | fence.i
+---------+---------+
|
v
+=============================================+
| Phase 3: Segment Copy & Clear |
+=============================================+
|
+---------v---------+
| 11. Copy Segments | <--- Flash -> IRAM/DRAM
| .retention_reset| (LMA -> VMA)
| .retention_data |
| .ram_code |
| .data |
+---------+---------+
+---------v---------+
| 12. Clear .bss |
| Clear .aes |
+---------+---------+
+---------v---------+
| 13. Jump to main()|
+-------------------+
Key macro configuration:
For detailed explanations of macros such as DP_THROUGH_SWIRE_DIS and SRAM_SIZE, see Direct XIP.
Retention Wake-up Process (.retention_reset)
Retention wake-up is accessed from the _IRESET_ENTRY entrance of section .retention_reset (located at the IRAM starting address, which does not power off during the retention period).
The first half (Phase 1 and Phase 2) is exactly the same as the power-on and will not be repeated. The difference is only in Phase 3:
(Phase 1 & 2 identical to power-on path)
|
v
+=============================================+
| Phase 3 (Retention-specific differences) |
+=============================================+
|
+---------v---------+
| R1. Flash Wakeup | <--- Retention only
| Send 0xAB cmd | (Flash powered off
| Wait ~25us | during retention)
+---------+---------+
+---------v---------+
| R2. Multi Address | <--- Retention only
| Restore | Restore g_pm_mspi_cfg
| | (Flash read cmd config)
+---------+---------+
+---------v---------+
| R3. Clear .bss | <--- Same as power-on
| Clear .aes |
+---------+---------+
+---------v---------+
| R4. Copy .data | <--- Copy .data only
| Flash->SRAM | (other 3 segs retained)
+---------+---------+
+---------v---------+
| R5. Jump to main()|
+-------------------+
Key differences in retention wake-up:
- Flash Wake-up (R1): During retention, Flash loses power. After waking, a
0xABcommand must be sent to wake the flash controller, and it waits about 25 us before accessing Flash. - Multi-address Register Restore (R2): Restores the
g_pm_mspi_cfgconfigurations related to multi-address startup, ensuring that after wake-up, the configuration matches the pre-power-off configuration. The power-on path does not require this step (configuration will be done in the boot ROM). - Copy only
.data(R4): Retain sections.retention_reset,.retention_data, and.ram_codein IRAM without loss, and only copy.data. - Prerequisite: retention RAM must be greater than the total size of
.retention_reset+.retention_data+.ram_codesections.
Notes on Writing Startup Files
The conflicts between .org and -flto
In the current compilation environment, the .org pseudo-instructions and Link Time Optimization (-flto) in optimization options cannot be used simultaneously. Since -flto is always selected (used to reduce bin file size), when using .org in an S file, you need to wrap it with .option push/pop:
.option push // Save the current .option configuration
.option norelax // Set to norelax
.org 0x0 // Configure .org
......... // Related code
.option pop // Restore .option configuration
.option push/pop is used for temporary saving/restoring option settings and does not affect global options.
Compression Instructions (C Extension / CoDense)
RISC-V's C Extension replaces 32-bit instructions with 16-bit instructions; Andes' CoDense technology places 32-bit instructions into the instruction table, replacing the original position with 16 EXEC.IT 0xxxxx.
- The
__nds_execitmacro is enabled by default in the compiler - Set
_ITB_BASE_to the starting address of the.exec.itablesection - S file writes
_ITB_BASE_to theuitbregister as the command table base address - The compression instruction is stored in the
.exec.itablesection
FPU enabled
Chips with FPUs need to enable floating-point units at startup. Not all chips with D25F cores support FPUs. For confirmation, refer to the table in Platform SDK overview . The FPU-related codes in the S file are controlled by the toolchain macro __riscv_flen and cannot be modified arbitrarily by the user:
li t0, 0x00006000
csrrs t0, mstatus, t0 // Set the FS bit of mstatus
fscsr zero // Initialize FCSR
View section assignments through lst files
After the compilation, the actual section address and size can be viewed through the lst file. During SDK compilation, the objdump tool generates a .lst file, where the -h option outputs a list of sections, recording each section's VMA, LMA, size, and attributes.
Example of the list of sections (using Direct XIP as an example, extracting key sections):
Sections:
Idx Name Size VMA LMA File off Algn
0 .vectors 00000100 20000000 20000000 00010000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .retention_reset 00000040 00000000 20000100 00010100 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .retention_data 00000200 00000040 20000140 00010140 2**3
CONTENTS, ALLOC, LOAD, DATA
3 .ram_code 00000500 00000240 20000340 00010340 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
4 .text 00008000 20000840 20000840 00010840 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
5 .rodata 00001000 20008840 20008840 00018840 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .data 00000200 00001000 2000c840 00019840 2**3
CONTENTS, ALLOC, LOAD, DATA
7 .bss 00000300 00001200 00001200 00019a40 2**3
ALLOC
How to read this list:
Name: Sectiont name, corresponding to the section defined in the link file.Size: section size (bytes, hexadecimal).VMA: Virtual Memory Address. For example,.retention_data's VMA is0x00000040(in IRAM).LMA: Loading Memory Address, the storage address of the section in Flash. For example,.retention_data's LMA is0x20000140(in Flash).Algn: alignment method (2**nmeans 2^n byte alignment).
Determine whether software transfer is needed: Compare VMA and LMA; if different, cstartup software transfer is required during startup. For example:
.vectors:VMA = LMA =0x20000000, no software transfer required, CPU fetches instructions from Flash (XIP)..ram_code: VMA =0x00000240(IRAM), LMA =0x20000340(Flash), the two addresses are different; at startup, cstartup transfer data from Flash to IRAM..bss: Only VMA, no LMA (NOLOAD), reset directly at startup.
The actual section addresses and sizes of each chip are based on the compiled lst file; the code in the document is only used to describe the structure.
Address specialty of Load to RAM
In the lst file for Load to RAM (Single RAM), The address of the code shows a special point; understanding this allows for correct interpretation of lst.
Phenomenon: The VMAs of code sections (.vectors, .ram_code, .text, .rodata) all start from 0x0, not the RAM physical address<SRAM_BASE>; while the VMAs of data sections (.retention_data, .data, .bss) are at <SRAM_BASE>+ (e.g., 0x840000+).
Example (real a piece of the lst file):
Idx Name Size VMA LMA File off Algn
0 .vectors 00000230 00000000 00000000 00008000 2**4
1 .ram_code 00000650 00000230 00000230 00008230 2**2
2 .text 000010f8 00000900 00000900 00008900 2**2
3 .rodata 00000028 000019f8 000019f8 000099f8 2**2
4 .retention_data 00000028 00841200 00001a20 00011200 2**2
5 .data 00000084 00841228 00001a48 00011228 2**2
6 .bss 000001dc 008412b0 00001ad0 000112ac 2**4
- Code section VMAs at
0x0+:.vectors/.ram_coderun in RAM, while.text/.rodatarun in Flash. - Data section VMAs at
0x840000+: Direct access to RAM physical addresses.
Reason: This is determined by the instruction fetch mechanism of the MCU. The address the CPU sees when fetching instructions is not a RAM physical address, but rather an independent instruction address space (starting from 0x0). The MCU sets a demarcation point internally via the I-Cache Tag base address register:
- Address < the demarcation point: Fetch instructions from RAM (RAM physical address =
<SRAM_BASE>+ instruction address). - Address > the demarcation point: Fetch instructions from Flash via I-Cache.
The SDK uses the end address of the the .ram_code , aligned to 256-byte as the demarcation point (the Cache Tag address requires 256-byte alignment), so .vectors and .ram_code run in RAM, while .text and .rodata run in Flash.
Key Points:
- VMAs are 0x0, but it actually resides in RAM: The VMAs of
.vectorsand.ram_codeare shown as0x0, but physically run in RAM (the RAM's based address varies by chip, such as0x840000). Because the instruction address space starts from0x0, and when the CPU fetches instructions, it automatically maps to RAM through a demarcation point mechanism. - cstartup does not transfer:
.vectorsand.ram_codeare stored in Flash, and upon power-on, the hardware boot ROM transfers them to RAM. There is no corresponding move in cstartup.
.retention_data and .data are different: LMA is in Flash, VMA is in <SRAM_BASE>+, and is transferred by cstartup.
Platform_init
Overview
platform_init() is the platform initialization entry function provided by the SDK and is responsible for configuring the basic environment after powering on or waking up the chip. Users must call this function at the beginning of the main() function; some parts are fixed and cannot be modified, while some are configured or designed based on the user's specific application scenario. The function of each part is described in detail below to help users use it correctly.
For function prototypes and parameter definitions for each chip, refer to the driver source files for the chip. This chapter only introduces the execution process of platform_init() and the function of each step.
Initialize the process
The figure below shows the complete steps of platform_init(). Steps marked as "Mandatory" are performed on all chips, while steps marked as "For some chips" exist only on certain chips. For details, refer to the driver codes for the corresponding chip.
Power On / Wakeup
|
v
+--------------------+
| System initialization | cpu_wakeup_init / sys_init(Mandatory)
+--------------------+
|
v
+--------------------+
| Disable the 32K watchdog | wd_32k_stop(For some chips,enabled by default)
+--------------------+
|
v
+--------------------+
| Update system status | pm_update_status_info(For some chips)
+--------------------+
|
v
+--------------------+
| Calibration value reading | Optimize module performance(Mandatory)
+--------------------+
|
v
+--------------------+
| GPIO initialization | Prevent electric leakage(Mandatory)
+--------------------+
|
v
+--------------------+
| SWS pin pull-up | Prevent floating leakage(Mandatory)
+--------------------+
|
v
+--------------------+
| Disable the Timer watchdog | wd_stop(For some chips,enabled in startup.S)
+--------------------+
|
v
+--------------------+
| Flash write protection | Prevent accidental erasure of the program area(Mandatory)
+--------------------+
Info
- The call order and presence of each step vary by chip; the above figure shows the complete steps. Refer to the driver codes for the corresponding chip for the actual sequence.
Detailed explanation of each initialization step
System initialization
Configure system-level parameters such as crystal oscillators (internal capacitor/external capacitors), power mode, and more. Depending on the architecture, the TC series calls cpu_wakeup_init() while the TL series calls sys_init() with parameters varying by chip.
Note
- It must be called before other initializations; otherwise, subsequent calibration values may not take effect.
Watchdog Handling
There are two watchdogs in the SDK; some chips are enabled by default, but in platform_init, they are disabled to facilitate demonstrations. For production use, it must be enabled and regularly fed.
| Watchdog | Enable Source | Timeout Reset | Handling in platform_init() |
|---|---|---|---|
| 32K Watchdog | Hardware enabled by default | 5 seconds | wd_32k_stop() (some chips) |
| Timer Watchdog | Enabled by startup. S | 10 seconds | wd_stop() (some chips) |
GPIO initialization
Key configurations to prevent electric leakage. The handling methods vary by chip and mainly fall into two categories:
- Set unused pins to high-impedance state
- Set the unused pins to high-impedance state and enable pull-down resistors
For specific methods and APIs, refer to the driver codes of the corresponding chip.
SWS pin pull-up
SWS (Single Wire Slave) is the debugging and programming pin and must be equipped with a pull-up resistor:
- Cause: The floating state may cause abnormal leakage current, or even miswrite registers/SRAM and cause crashes
- Configuration: By default, 1MΩ pull-up enabled
Calibration value reading
Reading calibration values from Flash / OTP / eFuse to optimize module performance must be called. Typical calibration targets include:
- Radio frequency (RF) performance
- ADC accuracy
- Voltage accuracy
Note
- This must be called after system initialization. For specific locations, refer to the driver codes for the corresponding chip; placing it elsewhere may not work.
Flash write protection
Prevents accidental erasure of the program area:
- Policy: Program area protected, data area not protected (users decide the protected area size based on their firmware size)
- Implementation: The API called varies by chip; refer to the driver codes for the corresponding chip
Clock System
Overview
System clocks can be sourced from the following types:
| Clock source | Description |
|---|---|
| PLL | On-chip frequency multiplier phase-locked loops (PLL) commonly use frequencies such as 192M and 240M (depending on the chip), with the highest output frequency |
| Doubler | Frequency multiplier that doubles the crystal oscillator frequency to achieve higher frequencies (such as doubling the crystal oscillator from 24M to 48M). Some chips (such as the TC series) use a doubler instead of a PLL. |
| XTAL | External crystal oscillator with high frequency accuracy. Each chip supports different crystal oscillator frequencies (most use 24M, some use 48M), so the design should be based on hardware references; The crystal oscillator can use either internal or external capacitors; when using internal capacitors, board-level calibration is required |
| RC | On-chip RC oscillator requires no external components and is low-cost; However, the frequency error is relatively large and affected by temperature/voltage drift, requiring real-time calibration (TL series see clock_cal_24m_rc(), TC series see rc_24m_cal()). As the guide clock source for XTAL initiation, the more accurate the start, the faster the vibration |
There are fundamental differences between the TL series and TC series in clock architecture and clock_init parameter structure, which are explained separately below. The available clock configurations, macro definitions, and frequency limits for each chip are all based on each chip's clock.h standard.
TL series
The TL series allows multiple clock frequencies to be set simultaneously through clock_init configuration, with the number of channels varying by chip (including PLL, CCLK, HCLK, PCLK, MSPI; multi-core chips also include additional clocks such as DSP and WT).
Core clock concepts
For specific clock trees, refer to the datasheet.
| Clock | Description |
|---|---|
| pll_clk | PLL, the source of many module clocks |
| cclk | CPU clock determines program speed |
| hclk | AHB bus clock: All modules mounted on the AHB bus use hclk |
| pclk | APB bus clock: all modules mounted on the APB bus use pclk |
| mspi_clk | The MSPI connects to the Flash clock, and it controls instruction fetch, Flash read/write, and other operations |
Note
- Multi-core chips (such as TL751x) also include additional clocks such as DSP, WT, N22, LSPI, GSPI, etc. Refer to each chip's
clock.hfor details.
clock_init configuration
The drivers provide the clock_init() function to configure the system clock. It is highly recommended to use the macro definitions provided in the driver header files rather than calling clock_init() directly: The macro defines the correct combination of frequency division parameters, which helps avoid incorrect parameter combinations.
Macro naming rules: Most TL chip macros follow the naming of PLL_<PLL frequency>_CCLK_<CCLK frequency>_HCLK_<HCLK frequency>_PCLK_<PCLK frequency>_MSPI_<MSPI frequency>, allowing each clock frequency to be read from the macro name. Some early chips (such as B91/B92) have shorter macro names (such as CCLK_96M_HCLK_48M_PCLK_24M) and do not include PLL or MSPI fields; Multi-core chips (such as TL751x) also include additional clock fields such as DSP/WT.
Usage examples:
/* Recommended: Use macros to define and configure the system clock, and call it directly as a statement */
PLL_192M_CCLK_96M_HCLK_48M_PCLK_48M_MSPI_48M;
/* Low power scenario: Use external crystal oscillator to bypass the PLL */
XTAL_24M_CCLK_24M_HCLK_24M_PCLK_24M_MSPI_24M;
Note
clock.halso contains macros labeled as internal debug purpose, users are prohibited from calling (usually located in the internal block of the file), which users are prohibited from calling. Make sure to refer to the comments in each chip'sclock.hsection. Some macros available to users on earlier chips may be included in internal blocks on newer chips.- MSPI frequency and Flash type: For the maximum MSPI speed with built-in Flash, refer to the
clock.hcomments for each chip; The maximum speed of external Flash must be determined based on board-level testing, as the maximum speed is related to board traces and is affected by temperature and GPIO voltage, so it must be verified through long-term stability tests of the board's maximum/minimum voltage and high and low temperatures. - HCLK/PCLK divider configuration restrictions (for some chips, such as B91/B92): When HCLK = 1/2 × CCLK, PCLK cannot take 1/4 × HCLK; limits are based on each chip's
clock.hcomments.
Voltage and Frequency (for some chips)
Some TL chips (such as TL721x, TL322x, TL751x) support multiple voltage levels. The voltage level determines the maximum frequency each clock can support: the high voltage level supports higher frequencies, while the low voltage level reduces power consumption.
- Frequency limits and optional clock macros for each level are grouped by voltage and listed in
clock.h. Users should ensure that the selected clock configuration does not exceed the maximum frequency supported by the current voltage level. - Voltage level switching is configured via a
pm_set_dvdd()interface, as detailed in PM_Demo. - Switching sequence: When scaling up, first switch voltage before increasing frequency; when scaling down, lower frequency first and then switch voltage.
- Low voltage must be restored before sleep: Since digital registers are lost during deep/deep retention sleep, after wake-up, the EMA digital register may not match the actual voltage, causing errors in SRAM usage. Therefore, before entering sleep,
pm_set_dvdd()must be called to restore the voltage to the low-voltage level (such as 0.8V).
Here is a code example for TL751x:
/* Voltage ramp-up sequence: switch voltage first, then switch frequency */
pm_set_dvdd(DVDD1_DVDD2_VOL_0P9V, 1000);
PLL_192M_D25F_DSP_192M_HCLK_96M_PCLK_48M_MSPI_48M_WT_12M;
/* Voltage ramp-down sequence: switch frequency first, then switch voltage */
PLL_192M_D25F_DSP_96M_HCLK_48M_PCLK_48M_MSPI_48M_WT_12M;
pm_set_dvdd(DVDD1_DVDD2_VOL_0P8V, 1000);
Note
- Voltage switching is a chip-level feature. The specific voltage value and the number of levels vary by chip. Refer to each chip's
clock.handpm.hsettings.
TC series
clock_init configuration
The TC series adopts the SYS_CLK_TypeDef enumeration to select the system clock source; clock_init receives only one enumeration argument:
clock_init(SYS_CLK_24M_RC);
The naming rules for enumerated values are SYS_CLK_<frequency>_<clock source>, such as SYS_CLK_24M_RC, SYS_CLK_24M_Crystal, SYS_CLK_48M_Crystal, etc. High-frequency options (such as 48M, 32M) are obtained by doubling the crystal oscillator frequency through a doubler. The supported enumeration values vary by chip (for example, some chips also support 48M crystal oscillators), so refer to clock.h for details.
| Enumeration value | Clock source | System clock frequency | Description |
|---|---|---|---|
SYS_CLK_24M_RC |
Internal 24M RC | 24 MHz | Uses an internal RC oscillator, no need for an external crystal oscillator |
SYS_CLK_12M_Crystal |
External 24M crystal oscillator | 12 MHz | xtal → Doubler (48M) → 1/4 division |
SYS_CLK_16M_Crystal |
External 24M crystal oscillator | 16 MHz | xtal → Doubler (48M) → 1/3 division |
SYS_CLK_24M_Crystal |
External 24M crystal oscillator | 24 MHz | Directly using external crystal oscillators, commonly used configurations |
SYS_CLK_32M_Crystal |
External 24M crystal oscillator | 32 MHz | xtal → Doubler (48M) → 2/3 division |
SYS_CLK_48M_Crystal |
External 24M crystal oscillator | 48 MHz | xtal → Doubler frequency multiplier, maximum system clock |
Note
- Automatic RC Calibration:
clock_init()automatically performs a 24M RC calibration when called after powering on or deep sleep wake-up. If the user wants to control the calibration timing themselves, they can disable this auto-calibration logic viaclock_init_calib_24m_rc_cfg(0)before calling. - Prerequisites for calibration: Before calling
rc_24m_cal(),you must ensure that the current system clock is not 24M RC; otherwise, it will cause system clock fluctuations and affect the normal operation of the chip. - doubler calibration limit: When using a 48M/32M system clock (doubler multiplication), do not call
doubler_calibration(),otherwise it may cause crashes. It is also prohibited to call this function during USB communication (transmit/receive)
32K clock
32K clocks are mainly used for sleep timers in low-power mode, 32k watchdogs, etc. Users select a 32K clock source via clock_32k_init():
- CLK_32K_RC: Internal 32K RC, which generally meets the requirements using RC. Sleep time is obtained through tracking and is not affected by 32K RC frequency deviation; calibration only needs to be done once when powering on, with no need for regular calibration.
- CLK_32K_XTAL: External 32K crystal oscillator with high precision, occupying external pins. It is only needed in scenarios with extremely high time accuracy requirements.
When using a 32K crystal oscillator, the TL series needs to call clock_kick_32k_xtal() to start the oscillator.
Note
General:
- Clock switching is prohibited during DMA operation: The system clock pauses for a period during the switch, and calling the clock switching interface during DMA transmission may cause data loss.
- 24M RC calibration: When using 24M RC as the clock source, the calibration interface must be called regularly. The recommended interval for regular calibration of the drive is 10 seconds. If using the sleep wake-up function, the 24M RC must be calibrated before entering sleep for the first time; otherwise, excessive RC frequency deviation after waking may cause the oscillator to fail to start.
TL Series:
- PLL frequencies must be compatible with multi-module requirements: PLL is the source of each module's clock, and some modules have special frequency requirements (e.g., USB requires 48M, Audio depends on default PLL frequencies). When configuring PLL frequency, it is necessary to consider the needs of these modules to ensure the PLL frequencies can be divided to obtain the required frequencies for each module; otherwise, it may cause abnormal operation of USB, Audio, and other modules.
Power
Overview
The power module is responsible for system-level power configuration after the chip is powered on, including power mode selection, VBAT voltage configuration, crystal oscillator capacitor selection, and power status queries and software resets during operation. This chapter covers three use cases:
- System initialization: After powering on/wake-up, configure basic parameters such as power mode, crystal oscillator, and voltage
- Power status query: Determines whether the current wake-up is from power-on, reboot, deep sleep wake-up, or retention wake-up recovery
- Software Reset: Triggers chip restart during runtime
For low-power management (Suspend / Deep Retention / Deep Sleep / Shutdown), wake-up source configuration, and usage instructions, refer to PM
System initialization
Function
The system initialization function is first called at the main() entry and is responsible for configuring system-level parameters such as enabling default modules, power supply mode (LDO/DCDC), crystal oscillator capacitors, and VBAT voltage. This function must be called every time main() is executed. Different series have different call interfaces, as shown in the table below:
TC Series: cpu_wakeup_init
| Chips | Function prototype |
|---|---|
| B85 | cpu_wakeup_init(void) |
| B80 / TC122x / TC123x | cpu_wakeup_init(cap_typedef_e) |
| B87 / TC321x | cpu_wakeup_init(POWER_MODE_TypeDef, cap_typedef_e) |
TL Series: sys_init
| Chips | Function prototype |
|---|---|
| B91 / TL321x / TL322x / TL323x / TL721x | sys_init(power_mode_e, vbat_type_e, cap_typedef_e) |
| B92 | sys_init(power_mode_e, vbat_type_e, gpio_voltage_e, cap_typedef_e) |
| TL751x | sys_init(power_mode_e, vbat_type_e) |
For parameter definitions of each chip, refer to the driver source files for the corresponding chip.
Power supply mode
The chip supports both LDO and DCDC power modes, allowing users to choose based on power consumption and noise requirements:
| Mode | Features | Applicable scenarios |
|---|---|---|
| LDO | Low noise, fewer peripheral components, and relatively low efficiency | Noise-sensitive scenarios |
| DCDC | High efficiency, but may have ripple | Scenarios sensitive to power consumption |
Parameter explanation
Function prototypes may vary slightly across different chips, but the meaning of the same parameter types remains essentially consistent. The following provides a unified description by parameter type. For the exact parameters and enumeration values on each chip, please refer to the definitions in the code.
| Parameters | Function |
|---|---|
power_mode_e / POWER_MODE_TypeDef |
Select power supply mode (LDO/DCDC) |
vbat_type_e |
Select whether to bypass VBAT LDO based on the VBAT input voltage |
gpio_voltage_e |
Select GPIO voltage (3V3/1V8), which must be consistent with hardware CFG_VIO pins |
cap_typedef_e |
Select between internal or external crystal oscillators |
cap_typedef_e :
- The configuration must be consistent with the actual capacitor configuration on the hardware board: When internal capacitors are selected, no external matching capacitors are required on the board. When external capacitors are selected, matching capacitors must be populated on the board. If the software configuration does not match the actual hardware setup (e.g., external capacitors are selected in software but not populated on the board), the crystal oscillator may fail to start up properly, potentially triggering repeated system resets. For slow oscillation start, see Starting and timing.
vbat_type_e :
- When VBAT ≥ 3.6V, bypass is not allowed;
- When VBAT ≤ 2.2V, bypass is required;
- 2.2V < VBAT < 3.6V, bypassing or not is acceptable; choose based on actual configuration, but it is recommended not to bypass it.
- When not bypassed, GPIO output is ≈ 3.3V; when bypassed, GPIO output = VBAT;
gpio_voltage_e (B92 only):
- CFG_VIO pin compatibility is required: 3V3 connected to VSS, 1V8 connected to VDDO3.
- Limitations of 1V8 mode: When configured at 1V8, the ADC's maximum detection voltage is ≤ 1.8V; Unusable
ADC_VBAT_SAMPLE; When using USB, GPIO cannot be configured to 1V8.
Precautions for use
Startup and Timing
A slow crystal oscillation startup triggers reboot
The system initialization function waits internally for the crystal oscillator to start and stabilize. The SDK's default wait time meets most oscillators' needs and normally does not trigger a reboot. A reboot will be triggered after calling the system initialization function only when the crystal oscillator starts up very slowly (exceeding the default wait time). This is the SDK's protection mechanism, not a bug.
After confirming that slow oscillator startup is the cause, before calling the system initialization function, first call the corresponding interface to extend the oscillation waiting time (it is recommended to leave some margin). Once the oscillator requirements are met, it will not reboot again.
| series | Check if reboot has occurred | Interface for adjusting the crystal oscillator startup wait time |
|---|---|---|
| TL Series (except B91) | pm_update_status_info () works with pm_get_sw_reboot_event (). |
pm_set_xtal_stable_timer_param() |
| B91 | Read PM_ANA_REG_POWER_ON_CLR_BUF0[bit2]. |
pm_set_xtal_stable_timer_param() |
| TC series | Read DEEP_ANA_REG0[bit1]. |
pm_set_wakeup_time_param() |
For parameters, default values, and meanings of each chip interface, refer to the corresponding chip's pm.h. This interface configuration will be lost after reboot or deep sleep and will need to be reconfigured.
32k RC calibration takes 6~7ms (TC series).
After powering on or waking up from deep sleep, calling the system initialization function takes about 6~7ms to perform 32k RC calibration. Retention wake-up will not perform calibration. If this logic is not required, it can be disabled by calling cpu_wakeup_init_calib_32k_rc_cfg(0) before the system initialization. This interface must be called before the system initialization to take effect.
Power status inquiry
Function
Power status queries are used to determine the cause of chip startup (power-on / software restart / Deep Retention wake-up / Deep Sleep wake-up/watchdog reset, etc.), typically used for:
- When the bootloader and application are initialized in stages, the boot phase is distinguished
- Identify the reset source (watchdog/software reboot / power-on) after abnormal recovery.
Implementation mechanism
The power states supported by different chips may vary. Deepret wake-up checks are required for all chips. It is updated internally within the system initialization interface, so no additional API call is needed. Other more complex power states, such as deep sleep wake-up or software reset, require users to actively call the pm_update_status_info interface for updates and read them through global variables. Currently, not all chips support this; At the same time, deepret wake-up is also included in this global variable as a power state. Here are the differences in power status updates and reads for each chip:
| Chip type | DeepRet wake-up | Other power status | Read the variable |
|---|---|---|---|
| B80 / B85 / B87 / TC321x | Support | No support | pmParam.is_deepretn_back |
| TC122x / TC123x | Support | Support | pmParam.mcu_status |
| B91 / B92 / TL321x / TL322x / TL323x / TL721x / TL751x | Support | Support | g_pm_status_info.mcu_status |
When calling pm_update_status_info, note that when clr_en=1, the related registers will be cleared after the update, so this parameter cannot be called twice, otherwise the status will be incorrect. The original value will be overridden the second time. If multiple calls are needed (such as in bootloader + app scenarios), pass parameter 0 during earlier calls and parameter 1 during the last call.
For MCU status enumerations for each chip, refer to the corresponding chip's pm.h.
Software reset
Function
Software reset is used for actively resetting the chip during runtime, with different interfaces for different series:
| series | Interface | Description |
|---|---|---|
| TC | start_reboot() |
Software reboot |
| TL | sys_reboot() |
Software restart, code is located in the text segment |
| TL | sys_reboot_ram() |
Software reboot, code located in the RAM segment |
Precautions for use:
-
The difference between TL
series sys_reboot()andsys_reboot_ram()lies only in the runtime code storage location. Under normal circumstances, usesys_reboot(). When Flash is inaccessible (such as during Flash erase and write), usesys_reboot_ram()because the function code resides in RAM and does not depend on Flash to fetch instructions -
Using
pm_get_sw_reboot_eventinterfaces (TL series, except B91/B92) can be used to find the cause of software reboot, but note that you must first callpm_update_status_infoto update the power status; otherwise, the return value will be invalid. -
Special case for B91 reboot: For B91, when the clock source is XTAL or PLL and hclk = 1/2 cclk, direct reboot may cause problems. To resolve this issue, the
sys_initautomatically enters deep sleep once it detects a reboot (wakes up after about 100ms), usingMCU_STATUS_REBOOT_DEEP_BACKto identify this type of reset.
Interrupt
This chapter covers three interrupt architecture types: PLIC, CLIC, and IRQ. Users can find the corresponding architecture based on the chip in the table below.
Interrupt type table
| Chips | Interrupt type |
|---|---|
| B91 / B92 / TL721x / TL321x / TL323x | PLIC |
| TL322x / TL751x | PLIC + CLIC (multi-core, PLIC for D25 core, CLIC for N22 core) |
| B80 / B80B / B85 / B87 / TC321x / TC122x / TC123x | IRQ |
Comparison of three architectural characteristics
| Characteristics | PLIC | CLIC | IRQ |
|---|---|---|---|
| Interrupt switch levels | Three-level (MSTATUS.MIE + MIE. MEIE + PLIC Enabled) | Two-level (MSTATUS.MIE + CLIC enabled) | Two-level (irq_enable + interrupt source mask) |
| Interrupt entrance | Each interrupt number has its own entry (vector) | Each interrupt number has its own entry (vector) | All interrupts share one irq_handler |
| Priority | 4 levels (0-3) | 4 levels (0-3) | No support |
| Preemption | 4 Modes (MODE0-3) | One mode is to enable it | No support |
| Critical zone | threshold Shields low priority | threshold Shields low priority | Disable global interruption |
| claim/complete | Required (hardware auto in vector mode) | No need (hardware auto) | No need |
| MTI (Timer). | Standalone module PLMT | CLIC internal management | No support |
| MSI (Soft Interrupt). | Standalone modules PLIC_SW | CLIC internal management | No support |
| WFI wakes up | Support | Support | No support |
| ISR registration | PLIC_ISR_REGISTER |
CLIC_ISR_REGISTER |
Distributed directly within irq_handler |
Info
- The TL322x/TL751x multi-core chips each have an interrupt controller; the D25 core uses PLIC, and the N22 core uses CLIC
- The differences between the three types of architecture—PLIC, CLIC, and IRQ—are explained in their respective chapters
PLIC
PLIC (Platform-Level Interrupt Controller) is an external interrupt controller for RISC-V machine mode, managing all peripheral interrupt sources. The PLIC acts as a bridge between the RISC-V Core and external interrupt sources, receiving interrupt requests from various peripherals. After priority arbitration, the interrupt ID is sent to the core, which then jumps to the corresponding ISR via the vector table. Two core functions: vectorized interrupt distribution (each external interrupt has an independent entry point) and interrupt prioritization and preemption.
RISC-V interrupt system and trap mechanism
RISC-V machine mode defines three types of interrupts, each managed by independent modules under the PLIC architecture:
| Interrupt type | Abbreviation | CSR Enable Bit | Source | Management module | Entrance |
|---|---|---|---|---|---|
| Machine external interruption | MEI | MIE.MEIE | Peripherals (UART/GPIO/Timer, etc.) | PLIC | Vector mode jumps straight entry_irqN |
| Machine timer interruption | MTI | MIE.MTIE | mtime timer | PLMT | trap_entry distribution |
| Machine software interruption | MSI | MIE.MSIE | Software triggered | PLIC_SW | trap_entry distribution |
mcause register: When a trap occurs, mcause records the reason for the trap:
mcause[31] = 0: Exception, such as instruction exceptions, stack overflows, etcmcause[31] = 1: Interrupt; the lower 31 bits are the interrupt type code:(mcause & 0x7FFFFFFF) == 3:MSI(mcause & 0x7FFFFFFF) == 7:MTI(mcause & 0x7FFFFFFF) == 11: MEI (in vector mode, PLIC jumps directly, does not enter trap_entry)
Interrupt vector table: when boot, build the __vectors in the boot file and set the base address via csrw mtvec, __vectors, and enable hardware vector interrupts via csrsi mmisc_ctl, 2. Simultaneously operate the PLIC Feature register to enable vector mode (reg_irq_feature = FLD_FEATURE_VECTOR_MODE_EN):
__vectors:
[0] trap_entry // Exception / Non-vector interrupt entry (MTI/MSI/Exception)
[1] entry_irq1 // IRQ1: Peripheral interrupt 1
...
[N] entry_irqN // IRQN: Peripheral interrupt N
By default, all weak entry definitions point to default_irq_entry (a loop). After users register via PLIC_ISR_REGISTER macros, the corresponding entry_irqN is replaced with the actual ISR.
trap_entry: Unified entry point for MTI, MSI, and exceptions, defined as weak functions that users can rewrite. Internal read mcause distribution:
__attribute__((weak)) void trap_entry(void)
{
long mcause = read_csr(NDS_MCAUSE);
if ((mcause & 0x80000000UL) && ((mcause & 0x7FFFFFFFUL) == 7)) {
mtime_irq_handler(); // MTI
} else if ((mcause & 0x80000000UL) && ((mcause & 0x7FFFFFFFUL) == 3)) {
plic_sw_interrupt_claim(); // MSI
mswi_irq_handler();
plic_sw_interrupt_complete();
} else {
except_handler(); //Exception: enters infinite loop after saving context
}
}
On-site save and recovery: trap_entry and each entry_irqN are declared via __attribute__((interrupt("machine"))), and the compiler automatically generates on-site save/restore code:
- Entry: Hardware saves CSRs such as mepc, mcause, etc.; The compiler stores the modified general register
- Exit: Compiler recovery register;
mretinstruction hardware recovery mepc
Users do not need to save/restore register context manually.
Exception handling: Non-interrupt traps such as instruction exceptions and stack overflows are handled by except_handler(). By default, the exception context (mtval, mepc, mstatus, mcause, mdcause) is saved before entering an infinite loop for easier debugging.
External interrupt usage
Three-level interrupt switch:
//Level 1: RISC-V Core global interrupt (MSTATUS.MIE + MIE. MEIE)
core_interrupt_enable();
//Level 2: PLIC interrupt source enablement
plic_interrupt_enable(IRQ_SYSTIMER);
//Level 3: Peripheral module interrupt mask
stimer_set_irq_mask(FLD_SYSTEM_IRQ);
Common peripherals with the third-level mask configuration:
- Timer:
timer_set_irq_mask()/stimer_set_irq_mask() - GPIO: Automatically enabled when configured with interrupt trigger edge/level
- UART: Automatically configured during initialization
Registering and implementing ISR:
_attribute_ram_code_sec_ void stimer_irq_handler(void)
{
if (stimer_get_irq_status(FLD_SYSTEM_IRQ))
{
stimer_clr_irq_status(FLD_SYSTEM_IRQ); // 1. Clearing the interrupted state
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1MS);
gpio_toggle(LED2); // 2. User-handled code
}
}
PLIC_ISR_REGISTER(stimer_irq_handler, IRQ_SYSTIMER)
The interrupt number for each chip is defined in the corresponding plic.h and given in IRQ_XXX macro form. The assignment of interrupt numbers varies by chip; refer to the header file of the corresponding chip.
Priority and preemptive
Priority: PLIC supports four priorities (0-3), with higher priority for higher numbers.
| Priority | Enumeration value | Description |
|---|---|---|
| 0 | IRQ_PRI_LEV0 |
Never generates an interrupt. |
| 1 | IRQ_PRI_LEV1 |
Minimum effective priority (default) |
| 2 | IRQ_PRI_LEV2 |
Medium priority |
| 3 | IRQ_PRI_LEV3 |
Highest priority |
Rules:
- Interrupts can only be triggered when
priority > threshold - Default
threshold = 0, defaultpriority = 1 - High-priority interrupts can interrupt low-priority interrupts; same-level interrupts cannot interrupt
- Priority 0 interrupt sources never produce interrupts
Preemption Configuration:
core_interrupt_enable();
plic_preempt_feature_en(CORE_PREEMPT_PRI_MODE0);
plic_set_priority(IRQ_SYSTIMER, IRQ_PRI_LEV3);
plic_set_priority(IRQ_TIMER0, IRQ_PRI_LEV1);
plic_interrupt_enable(IRQ_SYSTIMER);
plic_interrupt_enable(IRQ_TIMER0);
Preemption mode (controlling nested relationships among MEI/MSI/MTI):
| Mode | Enumeration value | Meaning |
|---|---|---|
| MODE0 | CORE_PREEMPT_PRI_MODE0 |
MTI and MSI cannot interrupt MEI; MSI and MTI can nest with each other |
| MODE1 | CORE_PREEMPT_PRI_MODE1 |
MTI cannot interrupt MEI; MSI and MEI can nest with each other |
| MODE2 | CORE_PREEMPT_PRI_MODE2 |
MSI cannot interrupt MEI; MTI and MEI can nest with each other |
| MODE3 | CORE_PREEMPT_PRI_MODE3 |
MEI, MSI, and MTI can be nested with each other |
When all three interrupts occur simultaneously, the hardware processing order is MEI > MSI > MTI. Disable preemptive use plic_preempt_feature_dis().
Interfaces prohibited from being called in the ISR (violating them can cause interrupts to freeze or claim failures):
| Interface | The reason |
|---|---|
plic_set_threshold() |
Hardware has threshold in/out of the stack behavior, and out-of-stack overrides the value set by the software; Internal brief shutdown, global interruption to prevent competition |
plic_set_priority() |
Calling this before claim will cause claim to return no value, and the interrupt will be considered as not having occurred. |
plic_interrupt_enable() |
Prohibited from being called in ISR |
plic_interrupt_disable() |
Closing interrupts after claim will cause complete to fail, thresholds not updated, and subsequent interrupts of the same priority will freeze |
plic_irqs_postprocess_for_wfi() |
Internally, PLIC interrupts are re-enabled |
Interfaces that must be used in pairs: plic_interrupt_claim() / plic_interrupt_complete(), plic_enter_critical_sec() / plic_exit_critical_sec(), plic_ all_interrupt_save_and_disable() / plic_all_interrupt_restore()、plic_irqs_preprocess_for_wfi() / plic_irqs_postprocess_for_wfi()。
Machine Timer (PLMT)
The PLMT consists of two 64-bit registers: mtime (monotonically increasing counter) and mtimecmp (comparative value, triggering MTI when mtime > = mtimecmp).
Usage Process:
// 1. Initialize clock source (32K RC requires calibration, 32K XTAL does not)
clock_32k_init(CLK_32K_RC);
clock_cal_32k_rc();
mtime_clk_init(CLK_32K_RC);
// 2. Enable MTI (an internal machine mode interrupt, not via PLIC)
core_interrupt_enable();
core_mie_enable(FLD_MIE_MTIE);
// 3. Set a timer cycle
mtime_set_interval_ms(500); //Triggers after 500ms
Interrupt Handling:
_attribute_ram_code_sec_ void mtime_irq_handler(void)
{
mtime_set_interval_ms(500); //Set the next trigger time; otherwise, it will only trigger once
gpio_toggle(LED4);
}
mtime_irq_handler is a weak function, which the user can rewrite. MTI enters through trap_entry without claim/complete.
Software interrupt (PLIC_SW)
Code actively triggers software interrupts and does not rely on peripheral hardware.
//Enable
core_interrupt_enable();
core_mie_enable(FLD_MIE_MSIE);
plic_sw_interrupt_enable();
//Trigger
plic_sw_set_pending();
_attribute_ram_code_sec_ void mswi_irq_handler(void)
{
gpio_toggle(LED2);
}
mswi_irq_handler is a weak function. MSI enters via trap_entry, and the SDK's default trap_entry automatically completes claim/complete.
Critical zone protection
Used to protect code execution in scenarios like flash operations where interruptions need to be avoided.
//Entering the critical zone: only interrupts with priority < = threshold are disabled
unsigned int r = plic_enter_critical_sec(1, IRQ_PRI_NUM1);
// ... Critical Zone Code ...
//Exiting the critical zone: Reverting to the state before entering
plic_exit_critical_sec(1, r);
| Parameters | Meaning |
|---|---|
preempt_en = 1 |
Using threshold, only interrupts with priority < = threshold are disabled |
preempt_en = 0 |
Global interrupts are disabled, and all interrupts are completely prohibited |
The two functions must be used at the same time.
WFI low-power wake-up
The SoC supports entering low-power mode via WFI (Wait-For-Interrupt) commands, waking up by interrupts.
Two wake-up modes (depending on the state of MSTATUS.MIE when entering WFI:
| Mode | Global interruption status | Wake-up Condition | Behavior after wake-up |
|---|---|---|---|
| Interrupt Enable | MSTATUS.MIE = 1 |
The interrupt is received by the CPU | Jump to the corresponding ISR to execute; after the ISR returns, continue executing instructions after WFI |
| Interrupt Disable | MSTATUS.MIE = 0 |
Wake-up on the interrupt enters pending | ISR is not executed; resume execution directly from the instruction after WFI |
Method 1: Preprocessing (Recommended).
plic_irqs_preprocess_for_wfi(flag, mie) saves and closes all interrupts at once, keeping only the wake-up source specified by mie:
//Before entering WFI
plic_irqs_preprocess_for_wfi(1, FLD_MIE_MEIE); //flag=1 Close global interrupt, leaving only MEIE wake
plic_interrupt_enable(IRQ_SYSTIMER); //Set the specific wake-up source (stimer)
core_entry_wfi_mode(); //Execute the WFI command
//After wake-up (Interrupt Disable mode)
stimer_clr_irq_status(FLD_SYSTEM_IRQ); //You must first clear the interrupt status flag of the wake-up source
plic_irqs_postprocess_for_wfi(); //Restore all interrupt configurations
Method 2: Manually claim/complete when the global interrupt is closed
//Make sure all PLIC requests before WFI are cleared; otherwise, you cannot access WFI
if (plic_clr_all_request() == 0) {
return; //Clearing failure: interrupt status is not clear, or the trigger level remains unresolved
}
core_entry_wfi_mode();
//After waking up, manually claim/complete
unsigned int claim = plic_interrupt_claim();
// ... Handle the corresponding interrupt source based on the claim value ...
plic_interrupt_complete(claim);
Note
plic_irqs_preprocess_for_wfiandplic_irqs_postprocess_for_wfimust be used in pairs- Before calling
plic_irqs_postprocess_for_wfi, you must ensure that the status flag for the corresponding interrupt has been cleared; otherwise, you cannot access WFI - In vector mode, hardware automatically claims, and software generally does not need to manually call
plic_interrupt_claim();Only required in manual handling when global interrupts are closed plic_clr_all_request()returns 0 indicating a clearing failure for two reasons: the interrupt state is not cleared, or the interrupt trigger level persists.
CLIC
CLIC (Core-Local Interrupt Controller) is a RISC-V machine-mode interrupt controller, with the key differences from PLIC:
| Dimension | PLIC | CLIC |
|---|---|---|
| Interrupt switch levels | Three-level (MSTATUS.MIE + MIE. MEIE + PLIC Enabled) | Two-level (MSTATUS.MIE + CLIC enabled) |
| claim/complete | Need (MEI) | No need (hardware auto) |
| Preemption Mode | 4 modes (MODE0-3) | No specification required; once enabled, nested by priority |
| MTI/MSI management | Standalone Module (PLMT/PLIC_SW) | CLIC directly manages it |
| ISR registers macros | PLIC_ISR_REGISTER |
CLIC_ISR_REGISTER |
Under the CLIC architecture, all interrupts support vector mode and directly redirect to the entry_irqN entry. Before use, call clic_init() (usually completed in PLATFORM_INIT), which initializes all interrupt priorities to 1.
External interrupt usage
Two-level interrupt switch:
//Level 1: RISC-V Core Global Interrupt (MSTATUS.MIE)
core_interrupt_enable();
//Level 2: CLIC interrupt source enabled
clic_interrupt_enable(IRQ_SYSTIMER);
//Peripheral module interrupts mask
stimer_set_irq_mask(FLD_SYSTEM_IRQ);
Registering and implementing ISR:
_attribute_ram_code_sec_ void stimer_irq_handler(void)
{
if (stimer_get_irq_status(FLD_SYSTEM_IRQ))
{
stimer_clr_irq_status(FLD_SYSTEM_IRQ);
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1MS);
gpio_toggle(LED2);
}
}
CLIC_ISR_REGISTER(stimer_irq_handler, IRQ_SYSTIMER)
CLIC has no claim/complete process; hardware automatically handles interrupt confirmation. The interrupt number for each chip is defined in the corresponding clic.h and given in IRQ_XXX macro form.
Priority and preemptive
CLIC priority definitions are the same as PLIC (4-level 0-3, same rules).
Preemptive configuration (no need to specify mode):
core_interrupt_enable();
clic_preempt_feature_en();
clic_set_priority(IRQ_SYSTIMER, IRQ_PRI_LEV3);
clic_set_priority(IRQ_TIMER0, IRQ_PRI_LEV1);
clic_interrupt_enable(IRQ_SYSTIMER);
clic_interrupt_enable(IRQ_TIMER0);
Disable preemptive use clic_preempt_feature_dis().
Machine timer
Under the CLIC architecture, MTI is managed directly by CLIC (interrupt number IRQ_MTIMER), without the need for a standalone PLMT module.
core_interrupt_enable();
clic_interrupt_enable(IRQ_MTIMER);
//Set the timing cycle (interface is the same as PLAIC)
mtime_set_interval_ms(500);
ISRs are registered through CLIC_ISR_REGISTER(mtime_irq_handler, IRQ_MTIMER), and mtime_irq_handler is a weak function.
Software interruption
Under the CLIC architecture, MSI is managed directly by CLIC (interrupt number IRQ_SOFT), without PLIC_SW independent modules.
//Enable it
core_interrupt_enable();
clic_interrupt_enable(IRQ_SOFT);
//Trigger
clic_set_pending(IRQ_SOFT);
ISRs are registered through CLIC_ISR_REGISTER (soft_irq_handler, IRQ_SOFT), with no claim/complete.
Critical zone protection
CLIC critical zone interfaces are the same as PLIC, using threshold masking:
unsigned int r = plic_enter_critical_sec(1, IRQ_PRI_NUM1);
// ... Critical Zone Code ...
plic_exit_critical_sec(1, r);
WFI low-power wake-up
The WFI process of the CLIC architecture is the same as PLIC, using the same plic_irqs_preprocess_for_wfi / core_entry_wfi_mode / plic_irqs_postprocess_for_wfi interfaces.
IRQ
IRQ is the interrupt model adopted in the TC series, based on the TC32 core. No independent interrupt controller; all interrupt sources share a single entry, with no priority, no preemption, and no claim/complete.
Interrupt usage
Two-level interrupt switch:
//Level 1: Global interrupt enabled
irq_enable();
//Level 2: Interrupt source mask
irq_set_mask(FLD_IRQ_DMA); Take the DMA interruption as an example
Implement ISR (all interrupts share one irq_handler entry point):
_attribute_ram_code_sec_noinline_ void irq_handler(void)
{
if (dma_chn_irq_status_get(FLD_DMA_CHN_SAR_ADC))
{
// 1. Handling interrupts
sd_adc_rx_done_flag = 1;
adc_stop_sample_dma();
// 2. Clear the interrupt status
dma_chn_irq_status_clr(FLD_DMA_CHN_SAR_ADC);
}
}
Universal interfaces:
irq_enable()/irq_disable()/irq_restore():Global interrupt switchirq_set_mask()/irq_disable_type():Enable/disable interrupt sourcesirq_get_src()/irq_clr_sel_src()/irq_clr_src():Query/clear the source of the interrupt
The interrupt source mask for each chip is defined in the corresponding register.h and given in FLD_IRQ_XXX_EN macro form (such as FLD_IRQ_DMA_EN, FLD_IRQ_ZB_RT_EN, FLD_IRQ_TIMER0_EN, etc.). Refer to the header file of the corresponding chip.
Critical zone protection
TC series has no priority concept; the critical zone directly closes global interrupts:
unsigned char r = irq_disable();
// ... Critical Zone Code ...
irq_restore(r);
irq_disable() and irq_restore() must be used in pairs.
Key points of ISR implementation
The following key points apply to the three types of architectures: PLIC, CLIC, and IRQ:
- The ISR function must be placed in
.ram_codesegments (PLIC/CLIC uses_attribute_ram_code_sec_, IRQ uses_attribute_ram_code_sec_noinline_) to ensure real-time interrupt response. - The ISR must first query and clear the peripheral interrupt status flag before executing the logic.
- Codes within the ISR should be as streamlined as possible, avoiding function calls (reducing registers on the stack, speeding up response), and avoiding interrupt closures or prolonged interrupt processing times (BLE protocol stack interrupt delay maximum 200 micros; ISR is recommended to be kept within 50 micros).
- The same interrupt number can only be registered once for ISR (PLIC/CLIC); If not registered,
default_irq_entryis executed by default (an infinite loop). - IRQ architecture has no priority or preemption; during interrupt handling, global interrupts are closed, and nesting is not supported.
- Under the PLIC architecture, calls to
plic_set_threshold(),plic_set_priority(),plic_interrupt_enable(),plic_interrupt_disable(),plic_irqs_postprocess_for_wfi()are prohibited in the ISR. see Priority and preemptive.
DMA
Overview
DMA (Direct Memory Access) transfers data between memory and peripherals without CPU intervention.
Telink offers two types of DMA:
| Type | Channel binding | Transfer mode | Interrupt | Linked list | Write Num | Burst |
|---|---|---|---|---|---|---|
| Enhancing DMA | Software can be configured with | Normal / Handshake | TC / ERR / ABT | ✓ | ✓ | Varies by chip; see resource table |
| Basic DMA | Hardware fixed | — | Only TC | — | — | — |
- TC (Transfer Complete): Interrupts after transmission is complete
- ERR (Error): Bus error / address misalignment/width mismatch
- ABT (Abort): The software automatically aborts transmission
Most peripherals (UART/SPI/Audio/RF, etc.) have DMA transmission packaged by peripheral drivers, allowing users to call peripheral interfaces directly. This chapter focuses on the concepts and considerations users need to pay attention to when configuring DMAs directly.
Core concepts
| Concept | Description |
|---|---|
| Normal mode | Memory-to-memory transfer requires no hardware handshake; DMA can move the specified length in one go. |
| Handshake mode | Memory and peripheral FIFO interact, hardware shakes hands to control rhythm—FIFO only writes when there are free slots and reads data only when there is space. |
| Burst | The number of Words continuously transmitted within a single arbitration cycle, with Burst added, can reduce bus arbitration overhead. Only high-throughput peripherals like Audio/SPI benefit from large bursts. |
| Write Num | After reception is complete, the hardware automatically writes the actual received length to the first 4 bytes of the destination address, suitable for variable-length reception scenarios such as RF. |
| Linked list | Multi-node automatic sequential transmission. The descriptor linked list is stored in memory, and after the current node completes, hardware automatically loads the next node. |
Judgment complete
When a DMA TC interrupt is triggered, data is only loaded in/out of the peripheral FIFO; the peripheral may not actually complete transmission and reception. Take UART as an example:
| Direction | Method of Judgment | Description |
|---|---|---|
| Send | Peripheral self-logo | Data is only completed when it is removed from FIFO, such as a TX_DONE interrupt or busy state in UART, rather than a DMA TC. |
| Receive | Peripheral logo or DMA TC | Data can be read as soon as it enters FIFO, and DMA TC can be used to check if a batch of data has arrived; variable-length reception uses peripheral rx_done to determine frame end. |
Resource overview
The DMA capabilities of each chip are compared.
| Chip | Type | Number of channels | Burst | Linked list | Write Num |
|---|---|---|---|---|---|
| B80 / B85 / B87 / B89 / TC321x / TC122x / TC123x | Basic DMA | 8 | — | — | — |
| B80B | Basic DMA | 12 | — | — | — |
| TC1211 | Basic DMA | 2 | — | — | — |
| B91 | Enhancing DMA | 8 | — | ✓ | — |
| B92 | Enhancing DMA | 8 | 1/2/4 Word | ✓ | ✓ (When DMA length = full ff) |
| TL321x / TL721x / TL323x | Enhancing DMA | 8 | 1/2/4/8 Word | ✓ | ✓ |
| TL322x / TL751x | Enhancing DMA | 16 | 1/2/4/8 Word | ✓ | ✓ |
All channels are independent of each other and can be used simultaneously.
Enhanced DMA usage
Enhanced DMA channels can be software-configured and can be selected from request sources via dma_config_t.
Usage mode: First call the peripheral binding function (internally complete dma_config_t configuration), then start transfer.
Interrupt handling: Supports three types of interrupts: TC / ERR / ABT.
When peripheral packaging interfaces do not meet the requirements (such as memory-to-memory transfer), manually fill the dma_config_t.
Key field meanings:
| Field | Meaning |
|---|---|
src_req_sel / dst_req_sel |
Peripheral request sources, values can be found in the DMA_REQ_* enumeration in the corresponding chip's dma.h |
src_addr_ctrl / dst_addr_ctrl |
Address control: INCREMENT/ FIX/ DECREMENT (rarely used) |
srcmode / dstmode |
Transfer Modes: NORMAL_MODE (Memory ↔ Memory) / HANDSHAKE_MODE (Memory ↔Peripheral) |
srcwidth / dstwidth |
Transfer bit width: WORD_WIDTH |
Rules for interacting with peripheral FIFOs: FIFO side address fixing (FIX), SRAM side address increment (INCREMENT), and using Handshake mode.
The peripheral transmission width is Word, and the buffer length must be a multiple of 4.
Linked list mode
The descriptor linked list is stored in memory, with each node specifying the source address, destination address, data length, and pointer to the next node. After the current node completes, hardware automatically loads the next node. Peripherals switch nodes according to their own configuration, and DMA determines when an interrupt occurs based on the interrupt mode.
| Interrupt mode | Behavior |
|---|---|
| CONTINUE_MODE | Continuous transmission, interrupting only occurs after the last node completes |
| INTERRUPT_MODE | An interrupt occurs after each node completes |
| TERMINAL_MODE | After each node completes, it automatically stops and requires software to reactivate it |
Linked list structure members must be declared volatile; otherwise, compiler optimization may cause PWM/Audio exceptions.
Note
The source and destination addresses must be aligned by 4 bytes; otherwise, an exception will be triggered. If the channel is not completed, it must be disabled and then reconfigured; otherwise, the write is invalid. Enhanced DMA can proactively abort transmission (causing ABT interruptions).
Basic DMA usage
The basic DMA channel is fixed to the peripheral hardware, and the peripheral driver completes the DMA configuration internally, allowing users to call the peripheral interface directly.
Usage mode: Enable the peripheral in DMA mode first, then activate the peripheral's transmit/receive interface.
Interrupt Handling: Only one interrupt status register, shared by all channels, writes 1 to clear. Queries and clears are performed by the API separately, without registers required.
GPIO
Overview
The GPIO module supports general-purpose input and output functions, enabling the output and reading of high and low logic levels. Additionally, the module supports pin multiplexing for peripheral functions and features configurable internal pull-up and pull-down resistors.
Working Mode
Output Mode: Push-Pull
By default, the low output level is 0 V, and the high output level is close to the VBAT voltage. The I/O regions of some chips support switching the high output level to 1.8 V by adjusting the VDDIO supply voltage.
The hardware does not support open-drain output mode.
Input Mode
An enable signal must be applied before reading the voltage level; it should be turned off when not reading.
The input level of the pin is determined using a percentage threshold: when the input voltage is below 30% of the I/O supply voltage, the system recognizes it as a logic low; when the input voltage is above 70% of the I/O supply voltage, it is recognized as a logic high; voltages between 30% and 70% are considered in an undefined state.
Pull-Up and Pull-Down Configuration
Analog pull-up and pull-down resistors are built into the chip:
| Type | Resistance | Applicable Scenarios |
|---|---|---|
| Pull-up | 10Kohm | Standard pull-up resistors for keys, I2C, etc. |
| Pull-down | 100Kohm | Key press pull-down, level anchoring |
| Pull-up | 1Mohm | Low-power scenarios, SWS debug pins |
| Float | — | When external pull-up/down are already present |
Analog pull-up and pull-down are active in both the running and low-power (Deep Sleep) states. The TL series additionally provides digital pull-up and pull-down, but these are active only in Active mode and become inactive upon entering low-power mode.
The values 10k, 100k, and 1M listed here are approximate, not exact values.
Usage Method
Pin Initialization(gpio_init)
The recommended method to initialize GPIO is to configure the properties of each pin using macros in gpio_default.h, and then call gpio_init() to apply the changes all at once. This interface is available on all chips and serves as the core entry point for configuring GPIO at startup.
Step 1: Override macro definitions at the application layer (using PA0 as the output and PA1 as the input key as an example):
// app_config.h or gpio_default.h
#define PA0_OUTPUT_ENABLE 1 // PA0 output enable
#define PA0_INPUT_ENABLE 0 // PA0 disable input enable
#define PA0_DATA_OUT 1 // PA0 initial high level
#define PA0_FUNC AS_GPIO
#define PA1_INPUT_ENABLE 1 // PA1 input enable
#define PA1_OUTPUT_ENABLE 0 // PA1 disable output
#define PA1_FUNC AS_GPIO
#define PULL_WAKEUP_SRC_PA1 GPIO_PIN_PULLUP_10K // PA1 pull up
Configurable macros include:PXn_INPUT_ENABLE (input enable), PXn_OUTPUT_ENABLE (output enable),PXn_DATA_OUT (output level), PXn_DATA_STRENGTH (drive strength), PXn_FUNC (multiplexed function), PULL_WAKEUP_SRC_PXn (pull up/down).
Step 2: Call the initialization function:
gpio_init(1); // Parameter 1: Initialize both analog pull-up and pull-down resistors; 0: Initialize the digital section only
Parameter Description: anaRes_init_en controls whether to initialize the analog pull-up/pull-down resistors. The analog pull-up/pull-down settings are retained after sleep (Deep/Deep Retention) and do not need to be reset. Therefore:
- Power-on startup: Pass
1to initialize both the digital section and the analog pull-up/pull-down resistors - After waking from Deep/Deep Retention: Pass
0to initialize only the digital portion and skip the analog pull-up/pull-down to save time
If you need to dynamically change the direction or voltage level of a pin during runtime, use the single-pin API described in Section 3.2; for bulk configuration at startup, use gpio_init() instead.
Basic I/O
(1) Output
// TL series
gpio_output_en(GPIO_PA0); // 1. Enable output
gpio_set_high_level(GPIO_PA0); // 2. Set voltage level
// TC series
gpio_set_output_en(GPIO_PA0, 1); // 1. Enable output
gpio_set_high_level(GPIO_PA0); // 2. Set voltage level
Other output API:gpio_set_low_level(pin)、gpio_toggle(pin) (toggle level).
Sequence Requirements: Enable the output first, then set the level.
(2) Input
// TL series
gpio_input_en(GPIO_PA0); // 1. Enable input
unsigned char level = gpio_get_level(GPIO_PA0); // 2. Read voltage level
// TC series
gpio_set_input_en(GPIO_PA0, 1); // 1. Enable input
unsigned char level = gpio_get_level(GPIO_PA0); // 2. Read voltage level
Note
- Sequencing Requirements: The input enable must be set before reading the level; otherwise, the value read is unstable.
- Important: For pure output mode or floating pins, be sure to disable the input enable (
gpio_input_dis(pin)orgpio_set_input_en(pin, 0)), otherwise leakage current occurs.
(3) Pull-up/down
// TL series
gpio_set_up_down_res(GPIO_PA0, GPIO_PIN_PULLUP_10K);
// TC series
gpio_setup_up_down_resistor(GPIO_PA0, GPIO_PIN_PULLUP_10K);
Some chips also provide a separate 30k pull-up resistor interface (independent of the analog pull-up and pull-down circuits mentioned above):
gpio_set_pullup_res_30k(GPIO_PA0); // Enable 30k pull-up
Sequencing Requirements: There is no specific order dependency between pull-up/pull-down configuration and input/output enable, but it is recommended to configure the pull-up/pull-down settings before enabling the pin to avoid undefined voltage levels while the pin is floating.
Multiplexed Function
When GPIOs need to be connected to internal peripherals (such as UART_TX or SPI_CLK), they must be switched to multiplexed mode. The TL series and TC series have different interfaces:
// TL series: two steps : switch peripheral function + disable native GPIO control
gpio_set_mux_function(UART0_TX, UART_TX_PA3); // 1. Switch to peripheral function
gpio_function_dis(UART0_TX); // 2. Disable native GPIO control
// TC series: one step: gpio_set_func switch function directly
gpio_set_func(UART0_TX_PIN, UART_TX); // Switch to peripheral function
Drive Strength
Some chips provide an interface for adjusting drive strength (such as gpio_set_data_strength(pin, value)), supporting two levels (strong/weak) or more. The number of levels supported and the API vary by chip; refer to the respective chip driver header files for details.
Recommendations: The default drive strength is sufficient for most applications. Increase the drive strength only when driving high-capacitance loads, high-speed signals, or long traces; reduce the drive strength for power-sensitive pins. When adjusting the drive strength, be aware that excessively strong drive levels may cause signal overshoot and EMI issues.
Clock Probe Output
gpio_set_probe_clk_function(PIN_CLK_OUT, CLK_32K);
Internal clocks (such as 32K and RC24M) can be routed to the GPIO pins to facilitate debugging with an oscilloscope.
Debug Interface(JTAG / SDP)
Some chips support switching the GPIO to a JTAG (4-wire: TDI/TDO/TMS/TCK) or SDP (2-wire: TMS/TCK) debug interface:
jtag_set_pin_en(); // Enable JTAG (occupy PC4~PC7)
sdp_set_pin_en(); // Enable SDP (occupy PC6~PC7)
Note
- The JTAG/SDP mode is determined by the level of PB0 at power-on or hardware reset (low level -> JTAG, high level -> SDP). This selection cannot be changed via software; it can only be set through an external level input. A reboot does not trigger a re-detection.
State Switching Sequence
The correct configuration sequence for each scenario; an incorrect sequence can lead to logic level conflicts, leakage, or interrupts failing to trigger. The core principle is to avoid intermediate erroneous states (such as simultaneously enabling inputs and outputs, or outputting to both a multiplexed function and a GPIO):
| Scenario | Correct sequence | Consequences of incorrect sequence |
|---|---|---|
| General GPIO output | Enable output -> Set level | If the output enable is not set and a voltage level is written directly, the pin has no output. |
| General GPIO input | Enable input -> Read level | If the input enable is not set and the value is read directly, the value is undefined. |
| Pure output pin | Enable output ->Disable input | Current leakage |
| Input switches to output | Disable input -> Enable output -> Set level | Input and output enabled simultaneously, causing a logic level conflict |
| Output switches to input | Disable output -> Enable input -> Read level | Output residual drives interference reading |
| Switch to multiplexed function | Switch mux ->Disable GPIO function | Level conflict |
| Switch to general GPIO | Enable GPIO function -> Config input/output | Invalid operation |
| Interrupt usage | Input Configuration -> Channel Bindings -> Enable Mask -> PLIC Routing -> Global Enable -> ISR Clear Flags | The link is incomplete; the interrupt is not triggered. |
| Enable Leakage Protection | gpio_init(1) -> SWS pull-up -> On-demand pin initialization |
Leakage current from unconnected pins |
| Before deep sleep | Pins configured for other functions must be set to a fixed level (output high or low). | Leakage current from unconnected pins |
GPIO Interrupts
The GPIO module supports interrupt detection; by properly configuring GPIO interrupts, it is possible to respond to external events in real time.
Interrupt Type
In the RISC-V chip architecture, GPIO interrupts are primarily classified into the following four types based on hardware behavior and register control logic:
- GPIO_IRQ
- GPIO2RISC0 ~ 7
- GPIO_GROUP_IRQ
- Features: Once a GPIO group is selected, you can only choose pins from that group. Next, simply map the pins to the interrupt numbers one-to-one. For example, if you select Group A, bind interrupt source 0 to PA0 and interrupt source 1 to PA1, and continue mapping them in this manner.
- GPIO_IRQ_NUM0 ~ 7
Types and number of interrupts supported by each chip:
| Chips | GPIO_IRQ | GPIO2RISC0 ~ 7 | GPIO_GROUP_IRQ | GPIO_IRQ_NUM0 ~ 7 | Quantity of Interrupts |
|---|---|---|---|---|---|
| B91 | ✅ | ✅ | ❌ | ❌ | 3 |
| B92 | ✅ | ✅ | ✅ | ❌ | 11 |
| TL721x | ✅ | ✅ | ✅ | ❌ | 11 |
| TL321x | ❌ | ❌ | ❌ | ✅ | 8 |
| TL322x | ❌ | ❌ | ❌ | ✅ | 8 |
| TL751x | ❌ | ❌ | ❌ | ✅ | 8 |
| TL323x | ❌ | ❌ | ❌ | ✅ | 8 |
| TC321x | ✅ | ✅ | ❌ | ❌ | 5 |
| TC122x | ✅ | ✅ | ❌ | ❌ | 4 |
| TC123x | ✅ | ✅ | ❌ | ❌ | 4 |
| B80B | ✅ | ✅ | ❌ | ❌ | 8 |
| B80 | ✅ | ✅ | ✅ | ❌ | 11 |
| B85 | ✅ | ✅ | ❌ | ❌ | 3 |
| B87 | ✅ | ✅ | ❌ | ❌ | 3 |
Note
- B91 includes TLSR951x and TLSR921x, B92 includes TLSR952x and TLSR922x, B80 includes TLSR8208A/B/C/D and TLSR8373E/F, B80B includes TLSR8208E/F/G/H/J and TLSR8373A/B/C/FBR, B85 includes TLSR825x and TLSR8359, B87 includes TLSR827x and TLSR8355.
- For multi-pin interrupts that span different groups (such as PA0 and PB0), use the GPIO2RISC / RISC kernel channel or another independent interrupt channel instead.
Interrupt Mechanism
Rising Edge Trigger: The MCU uses the GPIO level signal directly as the interrupt source, triggering on the rising edge.

Falling Edge Trigger: The MCU inverts the GPIO level signal and uses it as the interrupt source, triggering on the falling edge.

(1) Multiple GPIOs Sharing One Interrupt Source:
Both GPIOs set to rising edge: The MCU ORs the two GPIO level signals and uses the result as the interrupt source, triggering on the rising edge. In the figure below, only GPIO0 triggered the interrupt.

Both GPIOs set to falling edge: The MCU inverts both GPIO level signals, ORs them, and uses the result as the interrupt source, triggering on the rising edge. In the figure below, only GPIO0 triggered the interrupt.

GPIO0 rising edge + GPIO1 falling edge: The MCU inverts GPIO1, ORs it with GPIO0, and uses the result as the interrupt source, triggering on the rising edge. In the figure below, only GPIO1 triggered the interrupt.

Conclusion: When multiple GPIOs share the same interrupt source, the trigger behavior is unpredictable — not recommended. However, different interrupt sources are independent and can be used simultaneously. For example, if GPIO0 is configured as IRQ0 and GPIO1 as IRQ1, both rising edges trigger interrupts correctly.

For the actual number of interrupts available on each chip, please see Interrupt Type。
Interrupt Example (Using TL721x as an Example)
The following demonstrates the standard configuration method for the TL721x (Group Select Source Mode) based on the official driver interface:
##include "common.h"
#define IRQ_PIN GPIO_PA0 // test input pin
#define TEST_LED GPIO_LED1
volatile unsigned int gpio_src_irq0_cnt = 0;
/**
* @brief GPIO_SRC0 Interrupt Service Routine (ISR)
*/
_attribute_ram_code_sec_noinline_ void gpio_src0_irq_handler(void)
{
gpio_src_irq0_cnt++; // Count interrupt trigger number
gpio_set_high_level(TEST_LED); // response interrupt
// Clear the status flag for group interrupt source 0
gpio_clr_group_irq_status(FLD_GPIO_GROUP_IRQ0);
}
// Register Interrupt Service Routines to the PLIC Vector Table
PLIC_ISR_REGISTER(gpio_src0_irq_handler, IRQ_GPIO_SRC0)
void TL721x_gpio_irq_init(void)
{
// 1. Configuring Basic General Properties and Input Enable for Pins
gpio_function_en(IRQ_PIN);
gpio_output_dis(IRQ_PIN);
gpio_input_en(IRQ_PIN); // Enable input
gpio_set_up_down_res(IRQ_PIN, GPIO_PIN_PULLUP_10K); // Config internal 10K pull-up
// 2. Configuring trigger source and polarity
gpio_set_src_irq_group(IRQ_PIN >> 8); // Select the GPIO group to which the pin belongs
gpio_set_src_irq(IRQ_PIN, INTR_FALLING_EDGE); // Configured for falling-edge triggering
// 3. Enable the PLIC channels and global interrupts in the kernel
plic_interrupt_enable(IRQ_GPIO_SRC0);
core_interrupt_enable();
}
When using interrupts, we recommend using the built-in API and following the configuration sequence shown in the demo; otherwise, an incorrect configuration sequence may lead to issues such as unintended triggers.
Notes
Digital Pull-up/down Deep Sleep Failure
Some chips provide digital pull-up/pull-down interfaces (such as gpio_set_digital_pullup / gpio_set_digital_pulldown), but digital pull-up/pull-down is only effective in Active mode and becomes inactive upon entering low-power mode. Therefore, pins used as wake-up sources or pins that must maintain a specific voltage level during sleep must use analog pull-up or pull-down (gpio_set_up_down_res).
Wake-up Source Level Maintain
Certain pins on some chips cannot reliably maintain their logic levels in Deep Retention sleep mode (see the comments in the GPIO enumeration in each chip’s gpio.h header file for specific pins). These pins must not be used as sleep wake-up sources, and no external devices with strict level-maintenance requirements should be connected to them.
SWS Debug Pin
The chip uses the SWS interface (typically PA7, though PA3 on some chips) for program download and simulation. If the SWS pin is left floating, external interference may cause its voltage level to fluctuate, leading to leakage in the digital circuitry or even incorrect writing to chip registers, which can result in a system crash.
Solution: Apply a 1M analog pull-up to SWS as soon as possible after the program starts to keep the pin at a fixed voltage level:
gpio_set_up_down_res(GPIO_SWS, GPIO_PIN_PULLUP_1M);
// TC series: gpio_setup_up_down_resistor(GPIO_SWS, GPIO_PIN_PULLUP_1M);
Default Pull-up/down Configuration
The gpio_init(1) uniformly sets the analog pull-up/pull-down for all pins based on the value of the PULL_WAKEUP_SRC_PAx macro (where x is the pin number). This macro has default values defined in gpio_default.h; for some chips, the default is pull-down (e.g., GPIO_PIN_PULLDOWN_100K), while for others, the default is floating (GPIO_PIN_UP_DOWN_FLOAT).
The marco value and definition:
| Macro value | Definition |
|---|---|
GPIO_PIN_UP_DOWN_FLOAT (0) |
Floating (no pull-up/down) |
GPIO_PIN_PULLUP_1M (1) |
1Mohm pull-up |
GPIO_PIN_PULLDOWN_100K (2) |
100Kohm pull-down |
GPIO_PIN_PULLUP_10K (3) |
10Kohm pull-up |
The numbers 10k, 100k, and 1M here are approximate values, not exact values.
Note
- For chips with default pull-down settings, if a pin is actually used for other functions (such as peripheral multiplexing, output driving, or connecting to external pull-up components), the default pull-down setting conflicts with the actual function, resulting in abnormal voltage levels or additional power consumption. Before using the pin, you must redefine the corresponding macro as
GPIO_PIN_UP_DOWN_FLOATto disable the pull-down. This redefinition is performed in the application configuration header files (app_config/*.h):
// Remove the default pull-down for a specific pin in app_config
#define PULL_WAKEUP_SRC_PA0 GPIO_PIN_UP_DOWN_FLOAT
#define PULL_WAKEUP_SRC_PB3 GPIO_PIN_UP_DOWN_FLOAT
When developing, we recommend first checking the default values in the chip’s gpio_default.h file, and then removing the corresponding pull-down settings in app_config based on the pins actually in use.
Analog
Analog overview
The analog register is located in the chip's analog domain and is used to configure simulation-related parameters (such as GPIO input/drive strength, PLL, LDO voltage, etc.). Unlike digital domain registers (which read and write directly via reg_xxx), analog registers require access via dedicated ALG interface modules, which the Analog driver encapsulates.
The driver supports the following access granularities:
| Granularity | TL series | TC series |
|---|---|---|
| Byte (1 byte) | Support | Support |
| Halfword (2 bytes) | Support | Not support |
| Word (4 bytes) | Support | Not support |
| Buffer (batch) | Support | Not support |
Note
- The TL series offers multi-particle interfaces such as
analog_read_reg8/16/32,analog_read_buff; The TC series only offers byte-sizedanalog_read/analog_write(withReadAnalogRegandWriteAnalogRegmacro aliases).
Access interface
Interface naming
| Series | Read (byte) | Write (byte) |
|---|---|---|
| TL | analog_read_reg8(addr) |
analog_write_reg8(addr, data) |
| TC | analog_read(addr) |
analog_write(addr, v) |
The TL series byte read/write interface address parameter is unsigned int (some multi-Bank chips select Bank via the high address position; detailed bit definitions are found in each chip's analog.h); The TC series address parameter is unsigned char.
Concurrent protection
The analog read/write interface has interrupts disabled internally (TL series uses core_interrupt_disable, TC series uses irq_disable). A single read/write is an atomic operation that can be safely called in interrupt contexts.
Use examples
/* Read-Rewrite: Set a certain bit */
analog_write_reg8(0x3b, analog_read_reg8(0x3b) | BIT(7));
/* Write directly */
analog_write_reg8(0x3c, 0x5A);
Stores analog registers
Based on whether the register values are maintained under several typical different behaviors, storage analog registers are divided into two categories:
| Type | power on | 32K watchdog | timer watchdog | reboot | deep | deep retention |
|---|---|---|---|---|---|---|
| PM_ANA_REG_WD_CLR_BUF | Restore default values | Restore default values | Restore default values | Restore default values | Maintain | Maintain |
| PM_ANA_REG_POWER_ON_CLR_BUF | Restore default values | Restore default values | Maintain | Maintain | Maintain | Maintain |
The core difference between the two types of registers lies in the timer watchdog and reboot scenarios:
- WD_CLR_BUF: Any reset will restore the default value, suitable for storing data that "all scenarios require reinitialization" to avoid interference with old data.
- POWER_ON_CLR_BUF: Keeps the default value of the timer watchdog and reboot not restored, which can be used to distinguish between "power-on/32K watchdog reset" and "timer watchdog/software reset."
Important
- Each chip's memory register address, quantity, reset source, and initial value are all different. When using, you must refer to the
PM_ANA_REG_* macro definitions and comments in eachchip's pm.hfor reference. Do not copy the address across chips.
Before use, check the comment notes for the corresponding registers in pm.h to confirm which bits are available. Before first use, it is recommended to specify the expected initial value rather than relying on the default.
Notes
-
DMA mode is not recommended: TL series analog drivers provide DMA channel configuration macros, but after DMA configuration returns, the actual transfer may not be complete. If the analog register is accessed again during the interrupt, DMA transmission may be interrupted, creating unknown risks. It is not recommended to use DMA to read and write analog registers.
-
write_buff length limit: TL series
analog_write_buffsingle write length cannot exceed 8 bytes. -
Bit Occupation Avoidance: Before accessing the storage analog register, be sure to check the comments for the corresponding register in
pm.hto confirm that the target bit is not being occupied by drivers. -
Address is based on pm.h: memory register address, reset behavior, and initial values vary by chip. Do not copy addresses across chips.
Flash
Flash type
Built-in Flash
Telink ICs generally have multiple built-in Flash from different manufacturers, and a single manufacturer's Flash may have different types (different Flash capacities). The built-in Flash model is not static; at some point, a new type may be added. The Platform SDK ensures that the driver code corresponding to that Flash model is added immediately to guarantee mass production for customers.
For different chips with built-in Flash types, users can check the embedded Flash types of the relevant chip using the following method. What you see is all the Flash types currently supported by the SDK:
Reference: chip/<model>/drivers/flash/flash_type.h
The Telink Platform SDK implements some of the most basic functions based on Flash datasheets, meeting the needs of the vast majority of users. The content on the Flash datasheet is public; this document does not repeat the content of the datasheet. Therefore, it is recommended that customers proactively read the Flash datasheet after learning the built-in Flash model to understand technical details. First, it provides more debugging methods and information when problems arise (such as using the Flash status register content to trace issues); second, users can develop and implement some unconventional features themselves (such as customizing protection policies).
User-customized Flash
For all built-in Flash types, both hardware and software have been thoroughly evaluated and tested. In addition to basic read, write, and erase functions, the evaluation and testing also include operating time, temperature fluctuations, power-on/off data, and other data and performance. Therefore, the Flash models included in the Platform SDK are safe to use.
Telink does not recommend customers add new Flash models. If a customer insists on adding a particular Flash model, verifying that the basic functions of read, write, and erase are correct is not enough. We believe that the capability to conduct a comprehensive evaluation and testing of a single Flash device is very important. Therefore, contact our FAE for communication and request our original factory to conduct evaluation and testing. Only the Flash types we evaluate and pass testing can be used in mass production.
Summary table of chip functional differences
The differences in Flash functionality among chips are summarized as follows:
| Chips | Multi slaves | Four-line mode | Encrypted read/write |
|---|---|---|---|
| B80 / B85 / B87 | × | × | × |
| TC321x / TC122x / TC123x / TC1211 | × | × | × |
| B91 | × | √ | × |
| B92 | × | √ | √ |
| TL321x | × | √ | √ |
| TL323x | × | √ | √ |
| TL521X | × | √ | × |
| TL721x / TL322x / TL751x | √ | √ | √ |
STACK_SIZE_FOR_FLASH_DATA: The maximum write length per pass for buf to a Flash address. The default is 256 bytes for the TL series, and the default for the TC series is 32 bytes.
Flash bus address space
General address mapping rules:
- Flash address base offset:
FLASH_ADDR_BASE = 0x20000000 - All
addrparameters for all Flash APIs do not require base address 0x20000000, ranging from 0 to the size of Flash storage space.
Multi-slave device address space:
Supports up to 4 Flash devices (SLAVE0~SLAVE3), with a total access space of 64MB. The address space of each slave's is divided using the mspi_slave_device_addr_space_config() interface. addr parameter for Flash interface = actual Flash access location + Slave base address.
Note
- For the Flash bus address space layout diagram, please refer to the introduction to the basic module.
Support status for each chip's multi-slave
For the multi-slave support status of each chip, see the "Chip Function Difference Summary Table."
Note
- The following APIs have corresponding
_with_device_numversions for multiple Slave chips, used to specify the Slave device for operation. For simplicity, this document uses the single Slave API as an example.
Flash storage address allocation
Flash storage space is available to both SDKs and user applications. To ensure users do not experience conflicts when using Flash, the Flash storage space already occupied by each Telink SDK is introduced below. After learning these rules, users try to avoid these areas.
To make SDK address space allocation easier and more convenient for unified management across all Telink chip series and all product categories, all Flash address allocation rules are based on Flash Capacity.
Allocation of MAC Address and Calibration areas
All Telink SDKs uniformly follow the address allocation as follows. MAC Address can be used for general protocols such as BT/BLE/Zigbee; Thanks to its unique ID feature, users can also customize other SDKs and applications according to their own needs. Calibration can refer to the details of the corresponding chapter.

Note
- MAC Address and Calibration address allocation are standards followed across all Telink software, and are the same across all SDKs (such as Platform SDK, BLE SDK, 2.4G SDK, etc.).
Firmware Signature feature Flash space usage
Firmware Signature is an Optional feature.
- Some chips support hardware Secure Boot functionality, and their Descriptor occupies specific storage space. For details, refer to the Secure Boot section.
- Some chips implement software Firmware Signature solutions, storing Signature values at special Flash addresses. For details, refer to the relevant SDK documentation.
Higher-level SDK occupies additional space
The above is the Flash usage situation understood from the perspective of the General Platform SDK. In addition, various general protocol SDKs (such as Bluetooth/Zigbee SDKs) continue to consume some space as system usage due to their functional requirements (such as bootloader/OTA functions, Bluetooth pairing and binding). Users should refer to the Handbook introductions for these SDKs for more information.
MID and UID
MID
Flash MID = Manufacturer ID, which is the manufacturer identification code for SPI Flash chips.
Obtained via flash_read_mid(), containing capacity and manufacturer information. The meanings of each byte are as follows:
| Byte | Meaning |
|---|---|
| High Byte | Flash capacity labeling (see table below) |
| Medium byte + low byte | Vendor + process architecture information |
For example, 0x166085:
- High Byte
0x16: capacity is 4MB - Low 16-bit
0x6085:P UYA manufacturer, SONOS process
| MID capacity labeling | Corresponding Flash capacity |
|---|---|
| 0x10 | 64KB |
| 0x11 | 128KB |
| 0x12 | 256KB |
| 0x13 | 512KB |
| 0x14 | 1MB |
| 0x15 | 2MB |
| 0x16 | 4MB |
| 0x17 | 8MB |
| 0x18 | 16MB |
flash_vendor_e Definition
The flash_vendor_e high 8-bit is based on Mid's low 16-bit Flash vendor and process architecture information, used to identify different Flash process architectures. Below is a summary of all entries:
typedef enum {
FLASH_ETOX_ZB = 0x0100325E, // 325E bit[24]=1:ETOX
FLASH_ETOX_GD = 0x010060C8, // 60C8/4051
FLASH_ETOX_PUYA = 0x01002085, // 2085
FLASH_SONOS_PUYA = 0x02006085, // 6085 bit[25]=1:SONOS
FLASH_SONOS_TH = 0x020060EB, // 60EB
FLASH_SST_TH = 0x040060CD, // 60CD bit[26]=1:SST
FLASH_NORD_GT = 0x100060C4, // 60C4 bit[27]=1:NORD
FLASH_NORD_TH = 0x100070CD, // 70CD/71CD/51CD bit[27]=1:NORD
} flash_vendor_e;
Note
- The enumeration entries supported by different chips may not be exactly the same; please refer to the
flash.hof the corresponding chip.
Read the mid API
unsigned int flash_read_mid(void);
Usage example:
g_flash_mid = flash_read_mid();
Obtain the vendor API
unsigned int flash_get_vendor(unsigned int flash_mid);
Function: Retrieves Flash manufacturer and process information based on MID and returns flash_vendor_e type values. This value contains the Flash process type (ETOX/SONOS/SST) and vendor information.
Vendor information is linked to software functions
Differences in Flash characteristics across different processes:
| Process | Features | Write time characteristics |
|---|---|---|
| ETOX | bit[24]=1 | Byte Program Time ≠ Page Programming Time |
| SONOS | bit[25]=1 | Byte Program Time == Page Programming Time |
| SST | bit[26]=1 | Byte Program Time ≠ Page Programming Time |
| NORD | bit[27]=1 | Byte Program Time ≠ Page Programming Time |
SONOS process Flash also takes longer to write per byte than ETOX. To avoid related errors, the timing design of BLE SDKs based on TC series chips requires special handling. For details, refer to the Handbook corresponding to the BLE SDK. If a user's SDK or application has similar issues, targeted evaluation and handling are needed during the design process.
Capacity information associated with software functions
From the introduction to the Flash storage address allocation section, it is clear that MAC addresses and calibration functions require reading capacity information in advance before performing corresponding operations at the corresponding addresses.
UID
Flash UID = The unique hardware identifier for the flash memory chip, which is a globally unique identifier that is solidified by the manufacturer's laser/process at the factory for SPI/xSPI NOR Flash.
Due to its uniqueness and immutability, it can be used for chip identity authentication, firmware encryption, anti-cloning, product traceability, production control, and more.
Read the UID APIs
void flash_read_uid(unsigned char idcmd, unsigned char *buf);
Function: Reads the unique hardware identifier (UID) of Flash. The returned UID length depends on the specific Flash model, usually 16 bytes.
UID commands: Different vendors may have different Flash UID reading commands. Most vendors (PUYA, GD, ZB, TH) use 0x4b commands, while a few vendors (XTX) use 0x5a commands. flash_read_uid The API requires users to explicitly pass in the idcmd parameter, which distinguishes between vendors.
Mid and Uid validation APIs
int flash_read_mid_uid_with_check(unsigned int *flash_mid, unsigned char *flash_uid);
Function: Reads both MID and UID of Flash simultaneously and verifies their correctness.
Return value:
1: The Flash model is known and UID reading is successful0: Flash has no UID or an unknown models
Description: This function iterates through the SDK-supported Flash list, matches MID, reads UIDs, and checks whether UIDs are empty (all in 0x5101 mode means no UIDs).
On the Platform SDK and some upper-layer SDKs (such as the BLE SDK), software Firmware Signature solutions have been implemented for certain chips, utilizing UID uniqueness as part of the key to participate in signature computation.
Basic Flash operations
Four-line mode
Compared to the two-line mode, Flash's four-line mode offers faster read/write speeds but higher instantaneous power consumption.
For the support status of each chip's four-line functionality, see the "Chip Function Difference Summary Table."
Enable four-line mode. Example code is as follows:
unsigned char flash_set_4line_read_write(unsigned int flash_mid)
{
unsigned char status = flash_4line_en(flash_mid);
if (status == 1) {
flash_read_page = flash_4read;
flash_set_xip_config(FLASH_X4READ_CMD);
flash_write_page = flash_quad_page_program;
}
return status;
}
Flash Read (non-encrypted)
(1) Standard reading flash_read_page
extern flash_handler_t flash_read_page;
flash_read_page is a function pointer, which by default points to a standard read function and can be switched to other read modes via flash_change_rw_func().
Usage method:
flash_read_page(addr, len, buf);
(2) Dual-line reading flash_dread
_attribute_text_sec_ void flash_dread(unsigned long addr, unsigned long len, unsigned char *buf);
Function: Uses dual I/O mode to read data from Flash.
(3) Four-wire reading flash_4read
_attribute_text_sec_ void flash_4read(unsigned long addr, unsigned long len, unsigned char *buf);
Function: Reads data from Flash using Quad I/O mode.
(4) Read operation precautions
- Supports reading any address within Flash capacity
- The read
length lencannot be 0 - The maximum read length does not exceed the RAM allocated space and Flash capacity
Flash Write (non-encrypted)
(1) Standard write flash_write_page
extern flash_handler_t flash_write_page;
Similar to flash_read_page, flash_write_page is a function pointer that can be switched via flash_change_rw_func().
(2) Single-line page writes flash_page_program
void flash_page_program(unsigned long addr, unsigned long len, unsigned char *buf);
Function: Writes data to Flash in standard SPI mode (single-wire).
(3) Four-line page write flash_quad_page_program
void flash_quad_page_program(unsigned long addr, unsigned long len, unsigned char *buf);
Function: Writes data to Flash in quad mode for faster speed.
(4) Write operation precautions
- The write length
lencannot be 0 - Supports cross-page writing, with
lenvalues exceeding 256 bytes (single page size) - The
bufpointer should be an SRAM address. If a Flash address is passed, the driver will automatically copy the data to the stack before writing it. At this time, the single write length cannot exceedSTACK_SIZE_FOR_FLASH_DATA(default values are shown in the "Chip Function Difference Summary Table") - It is not recommended to write with bytes (<=255 bytes). If users want to write with one or more bytes (<=255 bytes), please follow the instructions below:
- Make sure the number of written bytes after erasing does not exceed 64
- In a page, byte can only be used once after writing to a page, and only supports writing from "1" to "0"
- Before writing, you must ensure the target area has been erased (after erasure, it is 0xFF); otherwise, the write may fail.
Flash Erase
(1) Sector Erasure (4KB)
_attribute_text_sec_ void flash_erase_sector(unsigned long addr);
- Address requirements: Must be a multiple of 0 or 0x1000
- Erase size: 4KB
(2) 32KB block erasure
_attribute_text_sec_ void flash_erase_block_32k(unsigned long addr);
- Address requirements: Must be a multiple of 0 or 0x8000
- Erase size: 32KB
(3) 64KB block erased
_attribute_text_sec_ void flash_erase_block_64k(unsigned long addr);
- Address requirements: Must be a multiple of 0 or 0x10000
- Erase size: 64KB
Note
- Block erasing takes a long time, so please feed dog in advance. For the maximum block erase time, please refer to the Flash datasheet.
Encrypted read/write
Only some chips that support Firmware Encryption have encrypted read/write capabilities. For applications using Firmware Encryption, read and write operations must use APIs related to encrypted read/write.
Supported chips: See the "Encrypted Read/Write" column in the "Chip Function Difference Summary Table".
(1) Encrypted writes
//Single-line encrypted page write
void flash_page_program_encrypt(unsigned long addr, unsigned long len, unsigned char *buf);
//Four-line encrypted page writing
void flash_quad_page_program_encrypt(unsigned long addr, unsigned long len, unsigned char *buf);
Function: Writes data to Flash in encrypted mode. The data written is encrypted by a hardware encryption engine before being stored.
(2) Decryption and readout verification
//Dual-line decryption and read verification
unsigned char flash_dread_decrypt_check(unsigned long addr, unsigned long plain_len, unsigned char *plain_buf);
//Four-line decryption and read verification
unsigned char flash_4read_decrypt_check(unsigned long addr, unsigned long plain_len, unsigned char *plain_buf);
Return value:
0: Check passed (read decrypted data matches expected plaintext)1: Check failure
Function: Read encrypted data from Flash, automatically decrypt it, and compare it with the provided plaintext buffer to verify whether the encrypted data is correct.
Function pointer versions:
extern flash_read_check_handler_t flash_read_page_decrypt_check; //Decrypt and read the checksum function pointer
extern flash_handler_t flash_write_page_encrypt; //Encryption writes function pointers
Flash safe operations
In embedded systems, Flash erase and write operations carry a high risk of failure, so secure Flash operations are crucial. Flash uses the SPI interface for data communication. Although the SPI protocol itself has good transmission stability, when the SoC is powered on or off, operates under special conditions, or when the VBAT supply voltage approaches the Flash operating threshold, the system is prone to large power supply fluctuations, which can cause abnormal jitter in the SPI clock and data signals, resulting in unauthorized tampering of address erases/writes or data. Among them, Flash erasure is a time-consuming operation, with a longer window of interference and increased risk. At the same time, if the software logic design is complex, address calculation links are cumbersome, and combined with human oversight during development, it can lead to anomalies in erasing addresses and data, and even illegal overwriting of firmware partitions. Once these issues occur, they cause irreversible software damage, not only causing standalone functions to fail but, in severe cases, trigger product disassembly, firmware reburning, and batch recalls, resulting in high operational costs and economic losses for enterprises.
To ensure the safe operation of Flash application products, Telink provides multiple effective protection and avoidance measures at both the hardware and software levels. Users are strongly encouraged to adopt these measures to enhance product stability.
Flash protection
(1) Flash's own protective features
The Flash chip itself provides a Status Register protection mechanism, implementing write protection for different regions by setting the Block Protect bit in the Status Register. Different Flash models support different locking zones.
The protection interfaces for each Flash model follow the flash_lock_<mid>(<lock_type>) and flash_unlock_<mid>() naming conventions, with the specific supported lock types defined in the header files corresponding to each Flash model (e.g., flash_mid146085.h).
Example of enumeration of protected areas:
//Different Flash models offer different lock area options, such as:
FLASH_LOCK_LOW_64K // Low address 64KB protection
FLASH_LOCK_LOW_128K // Low address 128KB protection
FLASH_LOCK_LOW_256K // Low address 256KB protection
FLASH_LOCK_ALL // Whole Protection
Usage example:
// 1. Lock Flash
flash_lock_mid146085(FLASH_LOCK_LOW_64K_MID146085);
// 2. Verify the locking effect: erase and write to check if the data is correct
flash_erase_sector(FLASH_ADDR);
flash_write_page(FLASH_ADDR + 0x80, FLASH_BUFF_LEN, (unsigned char *)Flash_Write_Buff);
flash_read_page(FLASH_ADDR + 0x80, FLASH_BUFF_LEN, (unsigned char *)Flash_Read_Buff);
//If the data matches expectations, the lock is active (the protected area remains unchanged).
// 3. Unlock Flash
flash_unlock_mid146085();
// 4. Verify unlocking effect: After erasing, the read should be 0xFF
flash_erase_sector(FLASH_ADDR);
flash_read_page(FLASH_ADDR + 0x80, FLASH_BUFF_LEN, (unsigned char *)Flash_Read_Buff);
//If the data is 0xFF, it means the unlock was successful
Path: The protection interface definitions for each Flash model are located in the flash_mid*.h file under the chip/<models>/drivers/flash/ directory.
(2) Flash default protection logic during chip initialization
During the chip power-on initialization (platform_init), the SDK automatically executes Flash recognition and default protection logic. The process is as follows:
- Read MID and match the Flash model
hal_flash_init() calls flash_read_mid() to read Flash's MID, then iterates the match in flash_list[] (defined in chip/<model>/drivers/flash/flash_common.c). If no match is found, initialization fails (returns non-zero), and the program enters a while(1) deadloop.
- Half the space is locked by default
After successful matching, hal_flash_lock() checks whether the current Flash is locked; if not, it calls lock_func(lock_size) by default, locking the lower address half of the space. The default protection size for each capacity is as follows:
| Flash capacity | Default protection size |
|---|---|
| 512KB | Low 256KB |
| 1MB | Low 512KB |
| 2MB | Low 1MB |
| 4MB | Low 2MB |
| 8MB | Low 4MB |
| 16MB | Low 8MB |
Note
- Protecting the low-address area means protecting the Firmware code area; the high-address area is reserved for user data read/write.
- Match failure processing
If there is no matching MID in flash_list[], g_mid_matched is false, hal_flash_init() returns 1, and the upper layer uses the while(1) infinite loop to indicate that the Flash model is not supported.
Customize protection size
To change the default protection size, follow these steps:
- Define macro
FLASH_PROTECT_MODIFY_CONFIGincommon.has 1. - In
common.cflash_init_list[], add target MIDs andblock_sizeby chip models. - Only the firmware area is protected, not the data area, to avoid frequent unlocking that could wear the Status Register.
Low Battery Detect
Battery power detect/check may also appear under other names in the Telink SDK and related documentation, including: battery power detect/check, low battery detect/check, low power detect/check, and battery check detect/check, etc.
This document is described under the name "Low Battery Detect."
For battery-powered products, as the battery capacity gradually decreases, when the voltage drops to a certain value, many problems arise:
- For example, for an SoC with a voltage range of 1.8V~3.6V, stable operation cannot be guaranteed when the voltage is below 1.8V.
- When the battery voltage is low, due to unstable power supply, SPI bus data jitter may occur during Flash write and erase operations, causing abnormal overwriting of Flash.
The main functions of low voltage detection for battery-powered products are as follows:
- Generally, ADC is used in the main program loop, the ADC periodically samples the VBAT values.
- Notes: To avoid ADC detection being affected by power supply jitter (for example, coinciding with RF transmission and reception), it is recommended to use multiple ADC sampled data for smoothing filtering algorithms.
- Set a Secure Voltage so that the MCU is only allowed to continue operating when the voltage exceeds this secure voltage; Once the voltage falls below the safe limit, the MCU stops running and needs to be shut down immediately (this can be achieved by entering modes like suspend or deepsleep).
Currently, the Platform SDK does not provide a reference design for low battery detection. Telink upper-level SDKs, some SDKs add reference designs for low battery detection (such as BLE SDKs), allowing users to refer to and implement them; Other SDKs without this reference design can be developed and implemented by users based on the main features mentioned above.
Power-on/off protection function
(1) Functional background
Flash risks during SoC power-on/off:
- Moment of power-on: unstable power supply, SPI clock/data jitter, MCU mistakenly sends erase commands, firmware or boot area damaged.
- Power off process: erase (takes tens~hundreds of ms) or power loss during programming, block or page half-write, data disorder, Flash bricking.
- Voltage threashold: Vbat approaches the Flash operating threshold, SPI signal jitters, and address/data is modified.
- Repeated power up/down: power ripple + frequent resets, worsening timing errors and increased wear.
The ADC-based low power detection solution protects Flash operation safety during the program's main cycle time and cannot cover protection during the MCU power-on and power-off phases. Therefore, Telink is gradually introducing LPC (Low Power Comparison) to protect SoC power-on and power-up, while also considering the program's main cycle time.
(2) Solution evolution and differences in chips
Some chips offer power-on and down-power protection solutions, while other older chips lack this feature due to historical reasons. Users can refer to existing solutions to independently develop and implement the same functionality. The differences among chip solutions are shown in the table below:
| Chip model | Solution type | Software required | Trigger method | Behavior |
|---|---|---|---|---|
| TL321x | Software solutions | Yes | LPC interrupt | Triggers interrupts at low voltage, pulling the MCU and MSPI during interrupts |
| TL322x | Hardware solutions | No | LPC → PEM → DMA | Automatically triggered at low voltage, pulling the MCU and MSPI through PEM channel + DMA write register |
| TL323x | Hardware Solutions (Optimization) | No | Direct detection of LPD module | A new LPD module has been added to directly detect VBAT. When the voltage is below 1.7V, hardware automatically triggers to pull the MCU or MSPI (optional), with both simultaneously held by default. |
(3) Details of different solutions
TL321x (software solution).
- Configure any GPIO output high level among PB1~7 (cannot be used as another function after selection)
- The PBx pin level is detected via LPC, and the VBAT voltage is detected indirectly
- When the voltage drops below about 2.0V, an LPC interrupt is triggered, pulling the MCU and MSPI in the interrupt service program.
The scope of impact of enabling this feature:
- Chip power supply range: only supports 2.1V - 4.2V.
- Interrupt preemption: The total interrupt enable
core_interrupt_enable()must be called by the user themselves and should be called as early as possible to maximize the duration of Flash protection. - Interrupt priority: LPC interrupt priority (for Flash power-off protection function) > Flash operation priority> Other interrupt priority.
- GPIO usage: One GPIO is required, default is PB4. Users can freely select PB1~7 based on actual application. Once selected, the GPIO cannot be used for other functions.
TL321x is disabled by default; users need to consider enabling it based on actual application scenarios and the above range of impact.
Reference code path: FLASH_LPC_PROTECT_MODE in \demo\vendor\Flash_Demo
TL322x (hardware solution).
- Also configure any GPIO output high level from PB1~7 (cannot be used as another function after selection)
- The PBx pin level is detected via LPC, and the VBAT voltage is detected indirectly
- When the voltage drops below about 2.0V, the LPC triggers the DMA write register through the PEM channel
- DMA hardware automatically holds the MCU and MSPI without software interrupts
The scope of impact of enabling this feature:
- Chip power supply range: only supports 2.1V - 4.2V.
- GPIO Occupancy: One GPIO from PB1~7 must be occupied; this GPIO cannot be used for other functions.
- PEM Occupancy: One PEM channel must be occupied, and this PEM cannot be used for other functions.
- DMA Occupancy: One channel between DMA0~7 must be occupied; this DMA cannot be used for other functions. 。
TL322x This feature is disabled by default. If users need high-speed applications with a main frequency of D25F CLOCK > 96MHz and can accept the above impact range, this feature must be enabled to improve chip robustness during high-speed operation. Please contact Telink FAE for support.
Functional interface path: lpc_pem_flash_prot_config() in \chip\tl322x\drivers\lpc.c
TL323x (optimized hardware solution).
- A new LPD module has been added to directly detect VBAT voltage, no need for GPIO indirect detection
- When the VBAT voltage drops below about 1.7V, hardware automatically triggers the pull on the MCU and MSPI
- No software intervention required throughout the process, resulting in faster and more reliable response
TL323x This feature is enabled by default, allowing users to use it directly without any configuration.
Note
- During chip operation, power supply fluctuations, voltage glitches during the first power-on or wake-up may cause voltage to fall below the trigger threshold, accidentally triggering LPC/LPD protection and causing the chip to jam. When using the above solutions, users must enable the 32K Watchdog or Timer Watchdog to prevent false triggers that could prevent the chip from recovering for a long time.
Power Management System
When the MCU is running programs normally, it operates in working mode, and the current remains at the mA level. When PM activates it into low-power sleep mode, the current is only at the uA level, greatly reducing power consumption. In low-power mode, except for the modules and clocks needed to wake up, all other modules and clocks are powered off. Some modules that require voltage maintenance also use low-power LDO holding voltages, which cannot operate properly.
Function description
Five low-power modes
Low power mode, also known as sleep mode, has five types:
- Suspend: Pause mode
- Deep sleep without SRAM retention (abbreviated as DeepSleep): deep sleep without SRAM retention
- Deep sleep with SRAM retention (abbreviated as DeepSleep Retention / Retention): deep sleep with SRAM retention
- WFI (Wait For Interrupt): Wait for interrupt mode
- Shutdown: Shutdown mode
Suspend:
In Suspend mode, program execution is paused, similar to a pause function. After the chip wakes up from Suspend mode, the program resumes execution. In Suspend mode, the PM module operates normally. The SRAM remains powered and retains all data. All analog registers remain powered, while a small number of digital registers are powered off. To reduce power consumption, software can power down modules such as RF, USB, and Audio. In this case, the corresponding digital registers of these modules are lost. For example, the RF module must be reinitialized after waking up before it can transmit packets. All other registers are retained. To enable packet transmission immediately after wake-up, do not power down the corresponding modules. However, this results in higher power consumption. The chip can be woken up by sources such as GPIO and timers.
DeepSleep:
In DeepSleep mode, program execution stops, and most hardware modules in the MCU are powered down. When the chip is awakened from DeepSleep, it restarts from the hardware bootloader (equivalent to a power-on reset), and the program re-initializes from the beginning. In DeepSleep mode, the PM module operates normally, SRAM loses data when powering down, most 3.3V analog registers are retained, while other analog registers and all digital registers lose power. The chip can be woken up by sources such as GPIO and timers.
DeepSleep Retention:
DeepSleep mode has very low current, but cannot store SRAM information; In Suspend mode, SRAM and registers can be kept intact, but the current is relatively high. To enable scenarios where sleep current is very low but can be immediately restored after waking, a DeepSleep Retention mode has been added. DeepSleep Retention mode is more similar to DeepSleep mode. The only difference from DeepSleep is that DeepSleep Retention allows a portion of SRAM to be selectively retained. The larger the retained SRAM area, the higher the power consumption.
In DeepSleep Retention mode, program execution stops, and most hardware modules in the MCU are powered down. When the chip is awakened from DeepSleep Retention, the MCU starts running from the software bootloader (not the hardware bootloader), and the program resumes initialization. In DeepSleep Retention mode, the PM module operates normally; a portion of the SRAM is kept powered, while the rest is powered down. Most 3.3V analog registers are retained, while the remaining analog registers and all digital registers are powered down. The increase in current consumption compared to DeepSleep mode is solely due to the current consumed by the SRAM retention circuit. The chip can be woken up by sources such as GPIO and timers. Compared to DeepSleep mode, DeepSleep Retention retains SRAM content. Therefore, upon wake‑up, the chip can directly boot from the retained SRAM, eliminating the need to copy code/data from Flash to RAM, resulting in a faster wake-up.
WFI:
WFI (Wait For Interrupt) is the lightest low‑power mode. The MCU only pauses instruction execution (CPU clock gating) without powering down any modules. All power supplies and clocks remain unchanged. SRAM, digital registers, and analog registers are all retained without data loss, and no peripheral controllers are reset. When any enabled interrupt is triggered, the MCU immediately resumes execution after the WFI instruction, requiring no restart or any resumption/reinitialization operations.
WFI has the highest power consumption among the five low-power modes, still lower than normal operating mode, but has the lowest wake-up latency. Suitable for short-term idle scenarios requiring rapid response to interruptions.
Precautions:
- Before entering WFI, you must ensure that at least one interrupt source is enabled; otherwise, the MCU will permanently hang and cannot be woken up.
- If the watchdog is enabled and the dog cannot be serviced during WFI, make sure the wake-up cycle is less than the watchdog's timeout
Shutdown:
Shutdown mode is the low-power mode with the lowest power consumption. In Shutdown mode, all modules of the MCU are powered down, including SRAM, all digital registers, and all analog registers; all of which lose their contents. The PM module itself is also powered off, with only a minimal amount of analog wake‑up circuitry remaining active. Wake-up sources only support PAD wake-up; other wake-up sources (Timer/MDEC/LPC/CORE) are not available. Only some chips support Shutdown mode; refer to the enumeration pm_sleep_mode_e or SleepMode_TypeDef for the corresponding chip.
When the chip wakes up from Shutdown, the MCU behaves the same as a fresh power‑on reset. It restarts from the hardware bootloader, and all SRAM and registers must be re‑initialized. Suitable for scenarios requiring long, extremely low-power standby periods.
Precautions:
- All states are lost after wake‑up from Shutdown. Therefore, before entering Shutdown, any data that needs to be retained must be persisted to external storage (such as Flash).
- Only the PAD can wake up the chip from Shutdown. Before entering Shutdown, the PAD wake‑up source and its pull‑up/pull‑down configuration must be properly set.
Differences:
Differences between WFI and Suspend:
- WFI does not call sleep functions, does not enter the PM module's power management workflow, does not switch LDOs, and does not power down any modules. Suspend invokes the PM module to power down specific modules and perform LDO switching.
- The wake-up source of WFI is any enabled interrupt (such as GPIO interrupt, Timer interrupt, UART interrupt, etc.), not limited to the wake-up source defined by PM.
- After waking up from WFI mode, execution resumes directly without any recovery operations. After waking up from Suspend mode, some powered-down modules (such as the RF module) may need to be reinitialized.
Differences between Shutdown and DeepSleep:
- Shutdown has a more extensive power-down scope than Deep Sleep. In Deep Sleep mode, the PM module and most 3.3 V analog registers remain powered, whereas Shutdown mode powers down all of them.
- Shutdown supports only PAD wake-up, whereas Deep Sleep also supports other wake-up sources, such as timers.
- Both modes restart through the hardware bootloader after wake-up, but Shutdown has a longer wake-up latency than Deep Sleep.
Differences between DeepSleep and DeepSleep Retention:
- In Deep Sleep mode, all SRAM is powered down. In Deep Sleep Retention mode, a portion of the SRAM remains powered, and the size of the retained SRAM region can be configured independently for each core.
- After waking up from Deep Sleep mode, the chip restarts from the hardware bootloader and boots from Flash. After waking up from Deep Sleep Retention mode, the chip starts from the software bootloader and can boot directly from the retained SRAM, eliminating the need to reload code and data from Flash, thereby reducing wake-up latency.
Low-power mode workflow
Different sleep modes result in inconsistent MCU operation processes. Below is a detailed introduction to the MCU operation process after waking up in the five sleep modes: WFI, Suspend, DeepSleep, DeepSleep Retention, and Shutdown. Refer to the figure below.

The modules in the flowchart are described as follows:
- Running hardware bootloader: MCU hardware performs fixed operations fixed in the ROM, which cannot be modified by software, usually including wake-up flash and other operations. ROM implementations vary across different chip series.
- Run software bootloader: After the hardware bootloader finishes, it starts running the software bootloader, which is the assembly code that starts after interrupting the vector segment. Its purpose is to set up the memory environment for running C language programs and complete memory initialization. The boot process varies by chip series: After waking up from Deep Sleep mode, the chip typically boots from Flash, while after waking up from Deep Sleep Retention mode, it can boot directly from the retained SRAM.
- System initialization: Initialize each hardware module such as
sys_initandclock_initin the main function, and set the register status of each hardware module. - User initialization: Corresponding to
user_initand other user initialization functions. - main_loop: After initialization, enters the while(1) main loop. The operation before entering sleep mode in the main loop is called "Operation Set A"; the operation after sleep wake-up is called "Operation Set B".
Process analysis of each sleep mode:
| Mode | Behavior after wake-up | SRAM | Register | Recovery requirements |
|---|---|---|---|---|
| Normal polling | Loop Operation Set A -> B | - | - | None |
| WFI | After an interrupt is triggered, execution resumes directly from the instruction following the WFI instruction. | 100% retained | 100% retained | None |
| Suspend | pm_sleep_wakeup() returns normally, continuing to execute Operation Set B |
100% retained | mostly retained | Little or no recovery required |
| DeepSleep Retention | Starts execution from the software bootloader | Partially retained | Mostly lost | Almost complete reinitialization required |
| DeepSleep | Similar to a cold boot; restarts from the hardware bootloader | 100% lost | Mostly lost | Full reinitialization required |
| Shutdown | Complete power-down; restarts from the hardware bootloader | 100% lost | 100% lost | Full reinitialization required |
Breaking down the registers in the table above, the states of SRAM, digital registers, and analog registers in five low-power modes are as follows:
| Mode | SRAM | Digital register | Analog Register (1V) | Analog Register (3V) |
|---|---|---|---|---|
| WFI | 100% retained | 100% retained | 100% retained | 100% retained |
| Suspend | 100% retained | C1 | 100% retained | 100% retained |
| DeepSleep Retention | C2 | 100% lost | 100% lost | 100% retained |
| DeepSleep | 100% lost | 100% lost | 100% lost | 100% retained |
| Shutdown | 100% lost | 100% lost | 100% lost | 100% lost |
C1: Most registers remain unchanged, with only a few special registers as exceptions. In Suspend mode, the power supply to modules such as RF, AUDIO, and USB can be selectively retained. The corresponding register contents are retained or lost accordingly.
C2: In DeepSleep Retention mode, the SRAM retention size is selectable; the SRAM retention size for each core is specified by pm_sleep_mode_e enumeration. Different chips have different selectable ranges; refer to the enumerated definitions for each chip.
Multi-core sleep and wake-up
Applicable Notes: This chapter only applies to chips that support multi-core. For specific chips supporting multi-core, please refer to the Overview section. This chapter is not applicable to single-core chips.
Classification of multi-core chip cores:
- Main core:D25F, the main control core, runs the main program, controls the power for the entire chip; the main core cannot be powered down individually.
- Secondary cores: DSP and N22 (RISC-V coprocessor), can be powered down independently. Different chip series integrate different secondary cores. Refer to the driver code for the specific implementation.
Basic Design Principles
Multi-core power management has only two usage modes:
- Whole-chip sleep: The main core calls
pm_sleep_wakeup()to control system sleep, and all cores enter the low-power mode simultaneously. In Suspend mode, secondary cores can be configured to remain powered or be powered down independently; in the Retention mode, the retained SRAM size can be configured independently for each core (setting it to NONE is equivalent to DeepSleep); in DeepSleep and Shutdown modes, all cores are powered down. - Main core active with independent secondary core power saving: While the main core is operating normally, idle secondary cores can either enter WFI and wait for interrupts or be powered down independently without affecting the operation of the main core.
Before the chip enters a whole-chip deep sleep mode (DeepSleep, DeepSleep Retention, or Shutdown), all secondary cores must first enter the WFI state. Otherwise, bus errors may occur.
Method 1: Whole-Chip Sleep
When the entire system is idle, the main core controls the entire chip to enter the same low-power mode. The procedure is as follows:
flowchart TD
A["Secondary core completes the task<br/>-> Notifies the main core via mailbox<br/>-> Enters WFI"]
B["Main core confirms all secondary cores are ready<br/>-> Configures power modes for secondary cores"]
C["Main core calls pm_sleep_wakeup<br/>-> Entire chip enters low-power mode"]
D["Wake-up<br/>-> Main core checks wake-up source<br/>-> Checks which secondary cores were powered down<br/>-> Reinitializes powered-off secondary cores"]
A --> B
B --> C
C --> D
Secondary core behavior in each low-power mode:
| Sleep Mode | Secondary Core Behavior |
|---|---|
| Suspend | Use pm_set_suspend_power_cfg() to configure whether each secondary core remains powered or is powered down. Secondary cores that remain powered resume execution directly after being woken by an interrupt (from the main core mailbox or any other interrupt source). Powered-down secondary cores must be reinitialized after wake-up. |
| DeepSleep | All secondary core SRAM is lost. All secondary cores must be reinitialized after wake-up. |
| DeepSleep Retention | Use the pm_sleep_mode_e to configure the retained SRAM size for the main core and each secondary core (for example, RET_MODE_SRAM_LOW384K_LOW512K_LOW128K indicates that D25F retains 384 KB, N22 retains 512 KB, and DSP retains 128 KB). Setting a secondary core SRAM size to NONE is equivalent to DeepSleep. Secondary cores must be reinitialized after wake-up. |
| Shutdown | The entire chip is powered down. All secondary cores must be reinitialized after wake-up. |
Code example:
// Suspend mode: Configure the DSP to be powered down and the N22 to remain powered.
pm_set_suspend_power_cfg(FLD_PD_DSP_EN, 0); // 0 = Powered down during Suspend
pm_set_suspend_power_cfg(FLD_PD_ZB_EN, 1); // Non-zero = Remains powered during Suspend (ZB shares the same power domain as the baseband and N22)
pm_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_PAD | PM_WAKEUP_TIMER, PM_TICK_32K, 2 * CLOCK_32K_TIMER_TICK_1S);
// Deep Sleep Retention mode: Specify the retained SRAM size for each core using the enumeration.
pm_sleep_wakeup(RET_MODE_SRAM_LOW384K_LOW512K_LOW128K, PM_WAKEUP_PAD | PM_WAKEUP_TIMER, PM_TICK_32K, 2 * CLOCK_32K_TIMER_TICK_1S);
// Wake-up handling: Check which secondary cores were powered down and reinitialize them.
if (pm_get_suspend_power_cfg() & FLD_PD_DSP_EN) {
sys_dsp_init(DSP_FW_DOWNLOAD_FLASH_ADDR);
sys_dsp_start();
}
Note
- The DeepSleep Retention options vary depending on the chip's
pm_sleep_mode_elist; refer to the corresponding chip'spm.hfor details.
Method 2: Independent Secondary Core Sleep (Main Core Remains Active)
When the main core is running normally, the idle secondary core can save power independently:
| Method | Operation | Wake-up Handling | Application Scenario |
|---|---|---|---|
| Secondary core enters WFI | The secondary core executes the WFI instruction autonomously. | Execution automatically resumes when an interrupt occurs. | Short idle periods requiring a fast response. |
| Main core powers down the secondary core | The main core powers down the secondary core through the power control API. | The secondary core must be reinitialized after wake-up. | Long idle periods requiring maximum power savings. |
Secondary Core Power-Down Procedure:
flowchart TD
A["Secondary core completes the task<br/>-> Sends a power-down request to the main core via mailbox<br/>-> Enters WFI"]
B["Main core receives the request<br/>-> Calls pm_set_dig_module_power_switch()<br/>-> Powers down the secondary core <br/>(Main core continues running and handles other tasks normally)"]
C["Secondary core is required<br/>-> Main core powers up the secondary core<br/>-> Reinitializes firmware"]
A --> B
B --> C
Code example:
// Main core: Power down the DSP.
pm_set_dig_module_power_switch(FLD_PD_DSP_EN, PM_POWER_DOWN);
// Main core: Power up and reinitialize the DSP when needed.
pm_set_dig_module_power_switch(FLD_PD_DSP_EN, PM_POWER_UP);
sys_dsp_init(DSP_FW_DOWNLOAD_FLASH_ADDR);
sys_dsp_start();
Precautions for Multi-Core Sleep
- Synchronization required: Before entering a whole-chip deep sleep mode, the main core must use the mailbox to verify that all secondary cores have entered the WFI state. Do not call the sleep function directly.
- WFI before power-down: Before the main core powers down a secondary core, it must first notify the secondary core to enter the WFI state. Do not power down a running secondary core directly; otherwise, peripherals controlled by the secondary core may be left in an undefined state.
- Post-wake-up check: After waking up from Suspend mode, use
pm_get_suspend_power_cfg()to check which secondary cores were powered down. Any powered-down secondary core must be reinitialized. - GPIO leakage prevention: Before entering sleep mode, ensure that no GPIO is left floating; otherwise, leakage current may increase power consumption.
Wake-up Sources
Wake-up sources include PAD wake-up, 32k timer wake-up (internal 32k RC and external 32k crystal clock sources), MDEC wake-up, LPC wake-up, and CORE wake-up.
PAD wake-up
PAD wake-up refers to waking up the chip through GPIO pins. Besides dedicated function pins, default output pins, and special pins, most pins can serve as wake-up pins. For specific chip pins that cannot be used as wake-up pins, please refer to the code comments.
If multiple pins are configured as wakeup sources, the specific pin that triggers the wakeup event cannot be identified after wakeup. Some chips support querying the wakeup source through registers. For details, refer to the register definitions of the corresponding chip.
PAD wake-up has no filtering function by default: during high-level wake-up, when the pin voltage reaches the GPIO high-level threshold (about 70% of VDDIO), it wakes up immediately; During low-level wake-up, the pin voltage reaches the GPIO low-level threshold (about 30% of VDDIO) and immediately wakes up.
An unstable or bouncing PAD input signal may cause false wakeup events. To prevent false wakeup, the filter function can be enabled. When enabled, the system samples the PAD signal at three points: the PAD edge, the rising edge of the 32k clock, and the falling edge of the 32k clock. A wakeup event is triggered only when all three samples match the configured wakeup level. After enabling filtering, ensure the wake-up level lasts at least one 32k cycle (about 31.25 μs) to guarantee reliable wake-up. Enabling filtering requires a 32K clock, which increases power consumption by about 0.4uA.
Precautions:
- Proper pull-up/pull-down configuration is required for PAD wakeup. The PAD level must be set to the expected state before entering sleep and remain unchanged during sleep until a wakeup event occurs to prevent false triggering.
- During sleep mode, all GPIOs must not be left floating; otherwise, leakage current may occur and increase system power consumption.
Timer wake-up
Timer wake-up is implemented by configuring the wake-up time. The chip automatically wakes up when the timer expires. The 32k clock source is used as the timing reference. Both 32k RC and 32k XTAL clock sources are supported.
When the 32k counter reaches the configured tick value, the Timer wakeup flag is set. Even if Timer wake-up is not enabled, the flag is still set when the counter reaches the configured tick value, but the chip will not be woken up. After the flag is cleared, it will be set again when the counter reaches the configured value next time.
Precautions:
- The nominal frequency of 32K RC is 32000Hz, and the nominal frequency of 32K XTAL is 32768Hz.
MDEC wake-up
MDEC (Manchester Decoder) is a Manchester decoding module used to decode input Manchester-coded data into binary data. To use MDEC wakeup, the 32k clock must be enabled (both 32k RC and 32k XTAL are supported). When the data received from the Manchester input pin matches the configured wakeup command value, a wakeup event is triggered.
LPC wake-up
LPC (Low Power Comparator) is a low-power comparator that compares the scaled input voltage (input voltage × scaling factor) with the reference voltage and outputs the comparison result. LPC has two operating modes:
- Normal mode — High precision, high power consumption, normal operating mode
- Low power mode — lower precision, low power consumption, low-power mode, can serve as an wake-up source
The comparison results of the low-power comparator can be used as an wake-up source to trigger low-power wake-up.
Precautions:
- The reference voltage settings for both modes are the same, but the actual voltage values are not exactly the same, possibly differing by about 100mV.
- After configuring LPC, a delay of 100 μs is required before entering sleep mode. After LPC is enabled, it takes 1–2 32k ticks to calculate the comparison result. Before the calculation is completed, the result register value may be random. Entering sleep mode during this period may prevent the chip from entering sleep properly.
- When entering sleep, the difference between the input voltage and the reference voltage must be greater than 30mV; otherwise, the comparison results will randomly fluctuate, preventing normal sleep entry and causing the system to crash.
CORE wake-up
CORE wakeup supports wakeup sources from digital modules, including CORE GPIO and USB. Because the digital domain must remain powered during the wakeup process, this wakeup mode is supported only in Suspend mode.
USB wake-up trigger condition: Voltage changes on USB pins DP and DM (USB data detected). To avoid accidental wake-up, before configuring USB wake-up, software should set DP pull-up and DM pull-down to ensure stable levels.
Note
- After power-on reset or wakeup from DeepSleep/DeepSleep Retention mode, the CORE wakeup flag is set by default and should be ignored by software. The flag is valid only when the chip wakes up from Suspend mode.
Power Supply
The power supply conditions for each module under different low-power modes are as follows:
| Power status | Module |
|---|---|
| Powered down during sleep | RF analog domain / ANA (PLL, etc.) / 24M RC / 24M XTAL |
| Powered down in DeepSleep / DeepSleep Retention; powered on in Suspend | Digital Core / SRAM (non-retention area) |
| Powered down in DeepSleep; powered on in Suspend / DeepSleep Retention | Retention SRAM |
| Always powered on during sleep | FLASH / GPIO / PM TOP |
Driver interfaces
Sleep function
Function prototype:
TL Series
int pm_sleep_wakeup(pm_sleep_mode_e sleep_mode, pm_sleep_wakeup_src_e wakeup_src, pm_wakeup_tick_type_e wakeup_tick_type, unsigned int wakeup_tick)
TC Series
cpu_sleep_wakeup(sleep_mode, wakeup_src, wakeup_tick)
cpu_long_sleep_wakeup(sleep_mode, wakeup_src, wakeup_tick)
Function description:
The chip supports four low-power modes: Suspend, DeepSleep, DeepSleep Retention, and Shutdown. The WFI mode is configured through a separate interface.
The implementation of sleep interfaces differs between the TL series and the TC series; the TL series provides only one interface, and different low-power modes are entered by directly calling this interface. The TC series implements the sleep interface using function pointers. The function pointer interface does not include the wakeup_tick_type parameter. Instead, two function pointer interfaces are provided: cpu_sleep_wakeup, which uses the fixed parameter PM_TICK_STIMER, and cpu_long_sleep_wakeup, which uses the fixed parameter PM_TICK_32K. The code is as follows:
#define cpu_sleep_wakeup(sleep_mode, wakeup_src, wakeup_tick) cpu_sleep_wakeup_and_longsleep(sleep_mode, wakeup_src, PM_TICK_STIMER, wakeup_tick)
#define cpu_long_sleep_wakeup(sleep_mode, wakeup_src, wakeup_tick) cpu_sleep_wakeup_and_longsleep(sleep_mode, wakeup_src, PM_TICK_32K, wakeup_tick)
The sleep clock source can be configured to use either the 32K RC oscillator or the 32K crystal oscillator. The TC series and TL series operate differently.
The TL series distinguishes by calling the clock_32k_init interface parameters to select CLK_32K_RC or CLK_32K_XTAL, initializing the interface to update variable g_clk_32k_src. The sleep interface selects the 32K RC or 32K XTAL clock source according to the value of g_clk_32k_src. A single interface is compatible with both configurations.
The TC series calls interfaces blc_pm_select_external_32k_crystal() or blc_pm_select_internal_32k_crystal() during initialization to distinguish between them. Two sleep interfaces are provided. The interface called during initialization determines which sleep interface is used. The internal implementation is as follows:
blc_pm_select_external_32k_crystal(){ cpu_sleep_wakeup_and_longsleep = cpu_sleep_wakeup_32k_rc;}
int cpu_sleep_wakeup_32k_rc(SleepMode_TypeDef sleep_mode, SleepWakeupSrc_TypeDef wakeup_src, pm_wakeup_tick_type_e wakeup_tick_type, unsigned int wakeup_tick)
blc_pm_select_internal_32k_crystal(){ cpu_sleep_wakeup_and_longsleep = cpu_sleep_wakeup_32k_xtal;}
int cpu_sleep_wakeup_32k_xtal(SleepMode_TypeDef sleep_mode, SleepWakeupSrc_TypeDef wakeup_src, pm_wakeup_tick_type_e wakeup_tick_type, unsigned int wakeup_tick)
Parameter Description:
| Parameters | Description |
|---|---|
sleep_mode |
Sleep mode selection: Suspend / DeepSleep / DeepSleep Retention / Shutdown |
wakeup_src |
Wakeup source selection: PAD / CORE / TIMER / COMPARATOR (multiple selection allowed) |
wakeup_tick_type |
Tick type selection PM_TICK_STIMER(24M/16M) / PM_TICK_32K |
wakeup_tick |
Wakeup tick value |
sleep_mode
- In DeepSleep Retention mode, different chips can retain different RAM sizes.
- Only some chips support Shutdown mode.
Refer specifically to the enumeration pm_sleep_mode_e or SleepMode_TypeDef of the corresponding chip.
wakeup_tick_type and wakeup_tick
Usage Description:
- If wakeup_tick_type is PM_TICK_STIMER: wakeup_tick is the current stimer tick value + sleep tick value (stimer)
- If wakeup_tick_type is PM_TICK_32K: wakeup_tick is the sleep tick value (32K)
Scope Description:
| tick type | stimer=24M | stimer=16M |
|---|---|---|
| PM_TICK_STIMER | Current tick + (48000~ 0xe0000000), about 2ms~156.59s | Current tick + (32000~ 0xe0000000), about 2ms~234.88s |
| PM_TICK_32K | 64~ 0xffffffff, approximately 2ms ~ 37hours | Same as left |
Internal processing:
In Timer wake-up mode, when the set sleep time exceeds the maximum value, the state value (i.e., the wake-up source) is returned directly.
When the configured sleep duration is short, the system clears the status value and waits until the configured duration expires before exiting the sleep function. This operation is equivalent to a delay and does not actually enter sleep mode.
Return value:
Returns the wakeup source flag, which indicates the actual wakeup source that triggered the wakeup event.
Precautions:
When entering sleep, if the wake-up condition is met (for example, the current PAD pin level is the wake-up level), the program will not enter sleep and will continue running downward.
GPIO wake-up configuration
Function prototype:
TL Series
void pm_set_gpio_wakeup(gpio_pin_e pin, pm_gpio_wakeup_level_e pol, int en)
TC Series
void cpu_set_gpio_wakeup(GPIO_PinTypeDef pin, GPIO_LevelTypeDef pol, int en)
Function description:
Used for PAD wakeup to configure the wakeup pin and wakeup level.
Parameter Description:
| Parameters | Description |
|---|---|
pin |
wakeup pin |
pol |
Wake-up polarity: LOW / HIGH |
en |
Enable (1) / Disable (0) |
Return value:
None
Power configuration for some modules during Suspend
Function prototype:
TL Series
void pm_set_suspend_power_cfg(pm_pd_module_e module, unsigned char on_off)
TC Series
void pm_set_suspend_power_cfg(pm_suspend_power_cfg_e module, unsigned char on_off)
Function description:
Check whether each module is powered down when configuring Suspend. Default power down all to save current. When the power remains on, the Suspend current increases, but the module does not need to be reinitialized after wakeup.
For multi-core chips, this interface also supports configuring whether the secondary core (DSP/N22) is powered down during Suspend. After wakeup, the secondary core that is powered down needs to be reinitialized, while the secondary core that remains powered on can continue execution directly.
Note
- In DeepSleep/DeepSleep Retention/Shutdown mode, all digital modules will be powered down by default and cannot be configured through this interface.
Parameter Description:
| Parameters | Description |
|---|---|
module |
During Suspend, it allows configuring whether specific modules remain powered on or are powered down, including FLD_PD_USB_EN (USB), FLD_PD_AUDIO_EN (audio), FLD_PD_DSP_EN (DSP), and FLD_PD_ZB_EN (baseband/N22). For details, refer to the pm_pd_module_e enumeration defined for the corresponding chip. |
on_off |
0: Powered down during Suspend; non-zero: Remains powered on during Suspend. |
Return value:
None
Demo description
The demo flowchart is as follows:

Flowchart explanation:
(1) The 2-second delay at the beginning is added to maintain communication availability. Since the Swire (single-wire debug interface) is unavailable after entering sleep mode, the BDT may fail to activate, preventing firmware programming.
(2) All IO pins are disabled before current measurement to prevent leakage current.
(3) CORE wakeup is supported only in Suspend mode and is not supported in DeepSleep or DeepSleep Retention modes.
(4) In Suspend mode, LED2 is controlled before sleep entry and after wakeup to indicate the current status. In DeepSleep and DeepSleep Retention modes, the chip is powered down during sleep entry, and the LED is automatically turned off. (The LED is only used as a status indicator.)
(5) Since the RC clock has limited accuracy and varies with temperature, periodic calibration is generally required. The following recommendations are provided:
a) 24M RC:
It is recommended to calibrate the RC clock every 10 seconds before entering sleep. The RC clock accuracy affects the crystal oscillator startup time after wakeup. After waking up from sleep, the hardware uses the 24M RC clock to kick-start the crystal oscillator. Higher RC clock accuracy results in a shorter crystal oscillator startup time.
b) 32K RC:
The RC clock is calibrated once during power-on and once after exiting DeepSleep. Since PM uses the tracing method (using the 16M clock to measure the fixed period of the 32K clock), the RC clock accuracy does not affect the Timer wakeup timing accuracy.
If other modules use the 32K RC clock, corresponding handling is required based on application requirements.
c) 32K xtal:
It needs to be re-kicked after power-on. When using the 32K XTAL, external capacitors need to be soldered on the board.
Watchdog
Overview
Watchdog is an important mechanism in MCU systems used to detect and recover from software anomalies. When software cannot run normally due to issues such as infinite loops or deadlocks, the watchdog timer overflow triggers a system reset, allowing the chip to return to normal operation.
The Telink platform supports the following two types of Watchdogs:
-
Timer Watchdog (regular watchdog): A digital watchdog based on a system timer, operating in active mode, with a clock source of pclk. After overflow, a digital reset is triggered, with the reset range the same as software reboot. Most chips support this feature. See the table below for details.
-
32k Watchdog: A watchdog based on a 32K clock source, it can operate in active/suspend/deep retention modes, and triggers a reset effect similar to power-on after overflow. Enabled by default after hardware power-on, it continues operating during sleep. Some chips support this feature. See the table below for details.
Chip difference description
Watchdog feature differences summary
| Chip model | Timer Watchdog | 32k Watchdog |
|---|---|---|
| B80 | √ | √ |
| B80B | √ | √ |
| B85 | √ | ✗ |
| B87 | √ | ✗ |
| TC321x | √ | √ |
| TC1211 | X | √ |
| TC122x | ✗ | √ |
| B91 | √ | ✗ |
| B92 | √ | √ |
| TL321x | √ | √ |
| TL322x | √ | √ |
| TL323x | √ | √ |
| TL721x | √ | √ |
| TL751x | √ | √ |
Standard Watchdog (Timer Watchdog)
TL series code description
Timer Watchdog is based on the pclk clock source and operates only in active mode. Taking the TL series as an example, the following interfaces are defined in watchdog.h and watchdog.c.
Interface list:
| Interface | Function |
|---|---|
wd_start() |
Enable the watchdog |
wd_stop() |
Disable the watchdog |
wd_set_interval_ms(period_ms) |
Set watchdog trigger time (unit: ms) |
wd_clear() |
Feed the watchdog to clear the watchdog timer counter. |
wd_get_status() |
Obtain the watchdog overflow status |
wd_clear_status() |
Clear the watchdog overflow status flag |
TC series code description
Timer Watchdog is based on the system clock source and operates only in active mode, using timer2, which can act as both a watchdog and a regular timer. Taking the TC series as an example, the following interfaces are defined in watchdog.h and watchdog.c.
Interface list:
| Interface | Function |
|---|---|
wd_start() |
Enable the watchdog |
wd_stop() |
Disable the watchdog |
wd_set_interval_ms(period_ms, tick_per_ms) |
Set watchdog trigger time (unit: ms) |
wd_clear() |
Feed the watchdog to clear the watchdog timer counter. |
wd_get_status() |
Obtain the watchdog overflow status |
Usage Example
// Initialize and activate the watchdog
wd_set_interval_ms(1000, sys_tick_per_ms); // Set trigger after 1 s
wd_start();
// Feed the watchdog periodically in the main loop
while (1) {
wd_clear();
// user code
}
32k Watchdog
TL series code description
Interface list:
| Interface | Function |
|---|---|
wd_32k_start() |
Enable the 32k watchdog |
wd_32k_stop() |
Disable the 32k watchdog |
wd_32k_set_interval_ms(period_ms) |
Set the trigger time in milliseconds, and the clock automatically selects the appropriate clock mode and calculates the countdown value. |
wd_32k_set_target_value(clk_sel, target) |
The trigger time can be set directly using the divider level and counter value, which is suitable for scenarios requiring fine-grained control. It is an internal interface called by wd_32k_set_interval_ms() and is not recommended for users. Actual trigger time = Divider single-period width × Counter value |
wd_32k_feed() |
Feed the dog |
wd_32k_get_count_ms() |
Get the current count value (ms) |
wd_32k_get_status() |
Return to overflow state. After a 32 kHz watchdog reset, the status is set to 1 after reboot. The wd_32k_clear_status() function must be called to clear the status; otherwise, it may affect subsequent status checks. The status is retained after software reset, deep sleep, deep sleep with retention, and 32 kHz watchdog reset. The status is lost after a power cycle, a reset pin reset, and a VBUS detection reset. |
wd_32k_clear_status() |
Clear the overflow status flag |
TC series code description
Interface list:
| Interface | Function |
|---|---|
wd_32k_start() |
Enable the 32k watchdog |
wd_32k_stop() |
Disable the 32k watchdog |
wd_32k_set_interval_ms(period_ms) |
Set the trigger time by ms |
wd_32k_get_status() |
Obtain overflow status |
wd_32k_clear_status() |
Clear the overflow status flag |
32k Watchdog Solutions 1 and 2
| Chip model | 32k Watchdog Solution 1 | 32k Watchdog Solution 2 |
|---|---|---|
| B80 | √ | X |
| B80B | √ | X |
| B85 | X | X |
| B87 | X | X |
| TC321x | √ | X |
| TC1211 | √ | X |
| TC122x | ✗ | √ |
| B91 | X | X |
| B92 | √ | X |
| TL321x | √ | X |
| TL322x | √ | X |
| TL323x | X | √ |
| TL721x | √ | X |
| TL751x | √ | X |
Differences between Solution 1 and Solution 2:
Solution 1:
- Unable to obtain the current 32k watchdog count.
- The watchdog can only be fed by calling
wd_32k_set_interval_ms(period_ms). Before feeding the watchdog, it must be disabled first. After the configuration is completed, enable it again. - When configuring the target value, the watchdog must be disabled first. After the configuration is completed, enable it again. Because the analog register cannot write 4 bytes at a time, only one can be written. During writing, there is concern about uncertain intermediate values, which could lead to an unexpected 32k watchdog reset, so it is closed before writing.
Solution 2:
- The current count value (ms) can be obtained via
wd_32k_get_count_ms(). - The watchdog can be fed directly by calling
wd_32k_feed()without disabling it. - The trigger time can be set using
wd_32k_set_interval_ms(period_ms)without requiring users to perform additional watchdog enable/disable operations.
Configuration precautions:
- If there is no timer as the wake-up source in sleep mode, only pad wake-up is used. Since there is no clock, the 32k watchdog count pauses.
- When using the timer wake-up function, ensure that the watchdog reset time is later than the wake-up time.
- For OTP products, if all code cannot run in RAM, there is a risk of crashes, so the 32K Watchdog needs to be enabled. This interface must be placed in RAM code to reduce risk.
- For chips using Solution 1, when configuring the trigger time, the watchdog must be disabled before configuration, and only enabled after configuration. Using the chip from Solution 2, the
wd_32k_set_interval_mshandle this operation internally and can be called directly. TL321x, although using Solution 1, internally handles this workflow and can also be called directly.
Usage Example:
Solution 1: The watchdog feeding procedure is the same as the reset time configuration procedure.
// Set and start in milliseconds
wd_32k_stop();
wd_32k_set_interval_ms(2000); // Set 2s trigger
wd_32k_start();
// Feed the dog:
wd_32k_stop();
wd_32k_set_interval_ms(2000);
wd_32k_start();
Solution 2:
// Set and start in milliseconds
wd_32k_set_interval_ms(2000); // Set 2s trigger
// Feeding the dog:
wd_32k_feed();
FAQ
Q: How can I determine whether the system was reset by the watchdog?
A:Call pm_update_status_info() to obtain the reset reason, and check the pm_status.mcu_status variable to determine whether the system was reset by the watchdog or resumed from another wake-up source. See pm_update_status_info in the Power section for details.
RF
Overview
RF Module Overview
The Telink chip integrates a single RF hardware engine that supports multiple wireless protocols, including BLE, Zigbee, 2.4G Proprietary, and ANT. RF modes can be switched through software. In different application scenarios, the same chip can flexibly switch between protocols such as BLE, Zigbee, and 2.4G Proprietary by simply calling the corresponding mode-switching interface, without requiring any additional hardware modifications.
Chip Supported Modes
Supported modes vary by chip series. The following table lists the modes supported across all chip series. For the exact definitions of each supported mode, refer to the SDK header files for the corresponding chip.
| Protocol Type | Supported Modes | Modulation | Description |
|---|---|---|---|
| BLE | 1M / 2M | GFSK | Standard BLE 1 Mbps / Enhanced 2 Mbps |
| BLE | 500K / 125K (LE Coded) | GFSK | BLE Coded S=2 / S=8 long-range mode |
| Zigbee | 250K | O-QPSK | IEEE 802.15.4 compliant (not supported on all chips) |
| Zigbee | Hybee 1M / 2M / 500K | O-QPSK | Hybee extended mode (supported on some chips) |
| 2.4G Proprietary | 250K / 500K / 1M / 2M | GFSK | Supports TPLL/SB frame format |
| 2.4G Proprietary (Generic) | 250K / 500K / 1M / 2M | GFSK | Proprietary protocol with customizable frame format |
Note
- The TC321x / TC1211 / TC122x / TC123x series chips do not support Zigbee and Hybee modes. The TLSR825x / TLSR827x/ TLSR8208 series and TL series chips support Zigbee and Hybee modes.
Driver File Structure
TL series chips use a modular driver structure:
rf_common.h: Common settings (initialization, frequency, power, fast settle, CRC, etc.)rf_ble.h: BLE mode relatedrf_zigbee.h: Zigbee / Hybee mode relatedrf_private.h: Proprietary mode relatedrf_dma.h: DMA related
TC series chips driver files:
rf_drv.h: RF common APIs and mode setting interfacesrf.h: Low-level RF driver declarations
Wireless Protocol Frame Format
This chapter uses BLE mode as an example to describe the TX/RX frame layout in RAM. For other protocols, only the differences from BLE are listed.
BLE Frame Format (Detailed Example)
Over-the-air (OTA) frame format:
| Field | Preamble | Access Code | PDU | CRC |
|---|---|---|---|---|
| Length | 1 Byte (1M) / 2 Bytes (2M) | 4 Bytes | 2–257 Bytes | 3 Bytes |
- Preamble: 1 byte (0xAA) in 1M mode, 2 bytes in 2M mode
- Access Code: 4 bytes. The advertising Access Code is fixed at 0x8E89BED6
- CRC: CRC-24, polynomial 0x65b, initial value 0x555555
TX Frame Format (RAM Layout):
| RAM Address | Content | Description |
|---|---|---|
| addr, addr + 1 | DMA_LEN_INFO | DMA transfer length, LSB first |
| addr + 2, addr + 3 | DMA_LEN_INFO | DMA transfer length, LSB first |
| addr + 4 | header0 | Refer to the BLE specification |
| addr + 5 | header (payload length) | Payload length, excluding the 3-byte CRC |
| addr + 6 | data(0) | payload |
| ... | ... | payload |
| addr + 6 + (length - 1) | data(length - 1) | payload |
RX Frame Format (RAM Layout):
| RAM Address | Content | Description |
|---|---|---|
| rba, rba + 1 | DMA_LEN_INFO | DMA transfer length, LSB first |
| rba + 2, rba + 3 | DMA_LEN_INFO | DMA transfer length, LSB first |
| rba + 4 | header0 | Refer to the BLE specification |
| rba + 5 | header1 (payload length) | Payload length |
| rba + 6 | data(0) | payload |
| ... | ... | payload |
| rba + 6 + length | crc(0) | CRC byte 0 |
| rba + 6 + length + 1 | crc(1) | CRC byte 1 |
| rba + 6 + length + 2 | crc(2) | CRC byte 2 |
| rba + 6 + length + 3 | r_tstamp[7:0] | Timestamp byte 0 |
| rba + 6 + length + 4 | r_tstamp[15:8] | Timestamp byte 1 |
| rba + 6 + length + 5 | r_tstamp[23:16] | Timestamp byte 2 |
| rba + 6 + length + 6 | r_tstamp[31:24] | Timestamp byte 3 |
| rba + 6 + length + 7 | pkt_fdc[7:0] | Frequency offset low byte |
| rba + 6 + length + 8 | pkt_fdc[10:8] | Frequency offset high byte |
| rba + 6 + length + 9 | pkt_rssi | Packet RSSI |
| rba + 6 + length + 10 | bit[0] | CRC error flag |
| Same | bit[1] | SFD error flag |
| Same | bit[2] | Link layer error flag |
| Same | bit[3] | Power error flag |
| Same | bit[7] | NoACK indicator |
RX Packet Parsing Macros:
| TL Series Macro | TC Series Macro | Description |
|---|---|---|
rf_ble_packet_crc_ok(p) |
RF_BLE_PACKET_CRC_OK |
Check whether RX packet CRC is correct |
rf_ble_packet_length_ok(p) |
RF_BLE_PACKET_LENGTH_OK |
Check whether RX packet length is correct |
rf_ble_dma_rx_offset_rssi(p) |
N/A | Offset of the RSSI value in the packet |
rf_ble_dma_rx_offset_freq_offset(p) |
N/A | Frequency offset info offset in the packet |
rf_ble_dma_rx_offset_time_stamp(p) |
N/A | RX timestamp info offset in the packet |
rf_ble_dma_rx_offset_crc24(p) |
N/A | CRC value offset in the packet |
Note
- The calculation of DMA_LEN_INFO varies by chip series. TL series provides the macro
rf_tx_packet_dma_len(data_len)to calculate the DMA length, with the formula((data_len + 3) / 4) | ((data_len % 4) << 22). TC series directly fills in the actual data transfer length (excluding the DMA_LEN_INFO bytes). For other related macro definitions, refer to the corresponding chip'srf_drv.h/rf.h.
2.4G Proprietary Frame Format — Differences from BLE
Proprietary supports two sub-modes: TPLL (Telink Proprietary Link Layer) and SB (ShockBurst).
OTA frame differences:
| Parameter | BLE | Proprietary TPLL / SB |
|---|---|---|
| Synchronization Word | Access Code (4 bytes fixed) | Configurable 3–5 bytes |
| Preamble | 1–2 bytes | Configurable 1–16 bytes |
| CRC | Fixed CRC-24 | CRC-16 |
RAM Layout Differences:
Compared with the BLE TX frame format:
- TPLL: The payload length is stored at addr + 4 (BLE uses header0), and payload data starts at addr + 5. The DMA_LEN_INFO field structure is the same.
- SB: Payload data starts directly at addr + 4, with no payload length field. A fixed length must be set via
rf_fix_payload_len_set().
Compared with the BLE RX frame format:
The trailing information appended to the RX packet has the same structure as BLE (CRC -> Timestamp -> Frequency Offset -> RSSI -> Error Flags), with only the parsing macros differing by sub-mode:
| TL Series Macro | TC Series Macro | Applicable Mode |
|---|---|---|
rf_pri_tpll_packet_crc_ok(p) |
RF_TPLL_PACKET_CRC_OK(p) |
TPLL mode |
rf_pri_tpll_packet_length_ok(p) |
RF_TPLL_PACKET_LENGTH_OK(p) |
TPLL mode |
rf_pri_sb_packet_crc_ok(p) |
RF_SB_PACKET_CRC_OK(p) |
SB mode |
rf_pri_sb_packet_payload_length_get(p) |
RF_SB_PACKET_PAYLOAD_LENGTH_GET(p) |
SB mode |
Note
- For TL series, other related offset macros can be found in the corresponding header file for each mode. For TC series, related macros can be found in
rf_drv.h/rf.h.
Zigbee Frame Format — Differences from BLE
Note
- Zigbee mode is supported only on some chips. Refer to the corresponding chip's datasheet.
OTA frame differences:
| Parameter | BLE | Zigbee |
|---|---|---|
| Preamble | 1~2 Byte | 4 Bytes |
| Synchronization Identifier | Access Address (4 bytes) | SFD (1 byte, fixed 0xA7, auto-detected by hardware) |
| Frame Header | 2-byte Header | 1-byte PHR (lower 7 bits = PSDU length) |
| CRC | CRC-24 | CRC-16 CCITT (polynomial 0x1021, initial value 0x0000) |
RAM Layout Differences:
Compared with the BLE TX frame format, Zigbee has an additional MAC layer header (Frame Control, Sequence Number, PAN ID, address fields, etc.) at addr + 4 to addr + 12. The payload length is at addr + 13, and payload data starts at addr + 14. Key differences:
- Zigbee / Hybee modes have no access code concept, so no access code configuration is required during initialization.
- SFD (0xA7) is auto-detected by hardware.
Compared with the BLE RX frame format, the trailing information appended to the Zigbee RX packet has the same structure as BLE.
Parsing macros: RF_ZIGBEE_PACKET_CRC_OK(p), RF_ZIGBEE_PACKET_LENGTH_OK(p), RF_ZIGBEE_PACKET_RSSI_GET(p), etc. Usage is the same as the BLE macros.
2.4G Generic Frame Format — Differences from Proprietary
The Generic mode is an extension of the Proprietary mode, offering greater frame format flexibility. In addition to configurable parameters such as CRC length, polynomial, and initial value, it also supports the packet filter feature as needed. For specific usage and details, refer to the relevant code in RF_Demo.
Note
- Generic mode is supported only on some TL series chips. Refer to the corresponding chip's datasheet for details.
RF TX/RX Operating Modes and State Machine
Manual Mode
In Manual mode, all TX and RX processes are controlled by software flow. The hardware performs only basic transmit and receive operations, without automatically managing state transitions, timing, or retransmissions. The timing accuracy in Manual mode depends on the software implementation.
(1) Manual TX
After entering TX mode via rf_set_txmode() and waiting for the settle time (which must be controlled by software in Manual mode), you can call rf_tx_pkt() directly to send a packet. For specific usage, refer to the relevant code in RF_Demo.
Note
- In Manual TX mode, the settle wait is required only once when first entering TX mode. The TX state remains active thereafter.
(2) Manual RX
After entering RX mode via rf_set_rxmode() and waiting for the settle time (which must be controlled by software in Manual mode), the device enters the actual receive state. In this mode, packets can be received continuously.
Note
- In Manual RX mode, the settle wait is required only once when first entering RX mode. After receiving one packet, the device directly enters the next receive state.
Auto Mode
Unlike Manual mode, Auto mode triggers the state machine at a specified time via a trigger tick. The hardware state machine automatically completes the entire Settle -> TX/RX -> IDLE sequence without MCU intervention.
The following six core state machine modes are supported:
| No. | Mode | Description | Trigger API |
|---|---|---|---|
| 1 | STX | Single transmission. Enters TX settle after triggering, returns to IDLE automatically after sending. | rf_start_stx(addr, tick) |
| 2 | SRX | Single reception. Enters RX settle after triggering, returns to IDLE automatically after receiving or timeout. | rf_start_srx(tick) |
| 3 | PTX | Periodic transmission with auto-retransmission and ACK reception. | rf_start_ptx(addr, tick) |
| 4 | PRX | Periodic reception. Automatically replies with ACK after receiving data. | rf_start_prx(addr, tick) |
| 5 | TX2RX | Transmit then receive. Sends a packet then waits to enter RX; exits on timeout. | rf_start_stx2rx(addr, tick) |
| 6 | RX2TX | Receive then transmit. Receives data then waits and replies with a packet; exits directly on timeout. | rf_start_srx2tx(addr, tick) |
(1) Trigger Tick Description
The tick parameter of each Auto mode specifies the time point at which the hardware triggers the operation. When the current system timer value ≥ tick, the hardware automatically triggers the corresponding state machine operation. The trigger time is obtained by adding an appropriate delay to the current system timer value.
// tick calculation: rf_start_xxx(packet, system_tick + delay_ticks)
// delay_ticks = delay in milliseconds × system timer ticks per millisecond
Note
- The tick parameter takes effect only when the current timer value is less than the tick value. When the timer value reaches or exceeds the tick value, the hardware triggers immediately.
(2) STX — Single Transmission
void rf_start_stx(void *addr, unsigned int tick)
| Parameter | Description |
|---|---|
| addr | TX packet address, must be 4-byte aligned |
| tick | Trigger tick value, triggers immediately when reached |
State machine flow:

The STX hardware flow is: reach tick time -> TX Settle -> send packet -> return to IDLE automatically. Each call to rf_start_stx triggers one complete transmission sequence.
// Typical interrupt handling flow:
Interrupt triggered (FLD_RF_IRQ_TX / FLD_RF_IRQ_CMD_DONE) ->
Clear interrupt flag ->
Load next packet data ->
rf_start_stx(next_packet, trigger_tick) // Schedule next transmission
(3) SRX — Single Reception
void rf_start_srx(unsigned int tick)
| Parameter | Description |
|---|---|
| tick | Trigger tick value |
State machine flow:

The SRX hardware flow is: reach tick time -> RX Settle -> wait for sync word (Sync Word / Access Address) -> upon successful sync, receive the complete packet and perform CRC check -> return to IDLE automatically.
(4) TX2RX — Transmit Then Receive
void rf_start_stx2rx(void *addr, unsigned int tick)
| Parameter | Description |
|---|---|
| addr | TX packet address, 4-byte aligned |
| tick | Trigger tick value |
State machine flow:

TX2RX is a bidirectional single-shot state machine. The hardware flow is: reach tick time -> TX Settle -> send packet -> TX done -> RX Wait -> auto-switch to RX Settle -> wait for the peer's reply packet or timeout -> return to IDLE automatically.
(5) RX2TX — Receive Then Transmit
void rf_start_srx2tx(void *addr, unsigned int tick)
| Parameter | Description |
|---|---|
| addr | Reply packet address, 4-byte aligned |
| tick | Trigger tick value |
State machine flow:

RX2TX is the complementary mode of TX2RX. The hardware flow is: reach tick time -> RX Settle -> wait for sync word -> receive packet -> RX done or timeout -> return to IDLE -> TX Wait -> auto-switch to TX Settle -> send reply packet -> return to IDLE automatically.
(6) PTX — Periodic Transmission
void rf_start_ptx(unsigned char *addr, unsigned int tick)
| Parameter | Description |
|---|---|
| addr | TX packet address, 4-byte aligned |
| tick | Trigger tick value |
PTX configuration interfaces:
| Interface | Description |
|---|---|
rf_ptx_prx_config() |
Initializes PTX / PRX mode configuration |
rf_set_ptx_prx_ack_en() |
Enables ACK |
rf_set_ptx_prx_ack_dis() |
Disables ACK |
rf_set_ptx_retry(retry_count, retry_delay) |
Sets retry count and retry delay |
In PTX mode, the hardware automatically handles all transaction processing, including packet assembly (Preamble + Access Code + Header + Payload + CRC), ACK reception, and auto-retransmission, without MCU involvement. A complete transaction is defined as: PTX sends a data packet -> receives an ACK packet from PRX.
PID (Packet Identification) Mechanism:
Each time PTX sends a new data packet, the PID field increments by 1 (2-bit, range 0–3). PRX uses the PID + CRC combination to determine whether a received packet is new or a retransmission, thereby avoiding duplicate delivery to the application layer. Even when the CRC is the same, the PID helps distinguish them.
Retry Delay (retry_delay) Description:
The retry_delay in rf_set_ptx_retry(retry_count, retry_delay) is defined as the interval between the start times of two transmissions, rather than the interval from the end of the previous transmission to the start of the next.
PTX State Machine Flow:

In PTX mode, after sending a data packet, the transmitter automatically switches to RX mode to wait for an ACK response from the receiver. If no valid ACK is received within the specified time (CRC check failure or timeout), the transmitter automatically retransmits according to the configured retry count (rf_set_ptx_retry). When the retry count exceeds the configured value, the FLD_RF_IRQ_TX_RETRYCNT interrupt is triggered, and the transmitter returns to IDLE. PTX supports PRX sending data (payload) in the ACK packet, in which case the PTX side triggers the FLD_RF_IRQ_RX_DR interrupt to notify the MCU to read the data.
Note
- PTX / PRX interface names may vary slightly across chip series. Some TL series chips use
rf_ptx_config()/rf_prx_config()for separate initialization, andrf_start_ptxmay have a different parameter signature (accepting only the tick parameter). Refer to the specific chip's SDK header file for details.
(7) PRX — Periodic Reception
void rf_start_prx(unsigned char *addr, unsigned int tick)
| Parameter | Description |
|---|---|
| addr | RX data storage address, 4-byte aligned |
| tick | Trigger tick value |
After receiving data, an ACK is automatically replied. As the primary receiver, PRX only automatically responds with an ACK upon receiving a valid data packet from PTX.
PRX PID Duplicate Detection Mechanism:
PRX uses the PID + CRC combination to determine whether the currently received packet is a retransmission. If the PID is the same as the last successfully received packet and the CRC also matches, the packet is identified as a duplicate — it will be discarded (not reported to the application layer), but PRX still automatically replies with an ACK (because PTX may be retransmitting due to a lost ACK).
PRX TX FIFO (ACK Payload):
Before receiving a data packet, PRX can preload data to be sent back into the TX FIFO. When a PTX data packet is received, the hardware automatically sends the data in the TX FIFO as the ACK payload. If the TX FIFO is empty, an empty ACK is sent (containing only Preamble + Access Code + Header/PID + CRC, with no payload).
NO_ACK Flag Handling:
When the NO_ACK flag bit in the received data packet is 1, PRX does not send an ACK packet (in this case, PTX will continue retransmitting until the maximum retry count is reached).
PRX State Machine Flow:

In PRX mode, the receiver continuously monitors the specified channel. When a valid data packet is received (CRC check passed and PID is a new packet), the payload is stored in the RX buffer and reported to the application layer; the receiver then automatically switches to TX mode to reply with an ACK. If there is preloaded data in the RX FIFO, the ACK packet carries that data as well. If no valid sync word is received within the specified time, the FLD_RF_IRQ_RX_TIMEOUT interrupt is triggered, and the receiver returns to IDLE (in periodic monitoring mode, it re-enters RX Settle at the next tick).
RF Interrupt System
Common Interrupts Overview
| Interrupt Name | Code Bit Name | Trigger Mode | Trigger Principle and Mechanism |
|---|---|---|---|
| TX Interrupt | FLD_RF_IRQ_TX | Manual TX / STX / PTX / RX2TX / TX2RX | Generated immediately after each packet is sent |
| RX Interrupt | FLD_RF_IRQ_RX | Manual RX / SRX / PRX / TX2RX / RX2TX | Generated after each packet is received and CRC check passes |
| RX Timeout Interrupt | FLD_RF_IRQ_RX_TIMEOUT | SRX / PRX / TX2RX | Generated when no sync word is received within the RX window, upon timeout |
| First RX Timeout Interrupt | FLD_RF_IRQ_FIRST_RX_TIMEOUT | SRX / PRX / RX2TX | Generated upon the first RX timeout (the first timeout after entering RX) |
| RX CRC Error Interrupt | FLD_RF_IRQ_RX_CRC_2 | BTX / BRX / PTX / PRX | Generated when two consecutive CRC errors are detected |
| TX DS Interrupt | FLD_RF_IRQ_TX_DS | PTX / PRX | Generated when the transmitted payload length is not 0 |
| RX DR Interrupt | FLD_RF_IRQ_RX_DR | PRX / PTX / SRX | Generated when the received packet payload length is not 0 |
| Invalid PID Interrupt | FLD_RF_IRQ_INVALID_PID | PTX / PRX | Generated when an invalid PID is received |
Core Interrupt Low-Level Functional Analysis
The interrupt operation interfaces vary by chip series, as listed below.
TL Series:
| Interface | Description |
|---|---|
rf_get_irq_status(status) |
Gets the status of the specified interrupt flag (read reg_rf_irq_status) |
rf_clr_irq_status(status) |
Clears the specified interrupt flag (write 1 to clear) |
rf_set_irq_mask(mask) |
Sets the interrupt mask (reg_rf_irq_mask) |
rf_clr_irq_mask(mask) |
Clears the interrupt mask |
TC Series:
| Interface | Description |
|---|---|
rf_irq_src_get() |
Gets the current RF interrupt source status (read reg_rf_irq_status) |
rf_irq_clr_src(msk) |
Clears the specified RF interrupt source (write 1 to clear) |
rf_irq_enable(msk) |
Enables the specified RF interrupt |
rf_irq_disable(msk) |
Disables the specified RF interrupt |
ISR Best Practice (refer to rf_irq_handler in app_ble_mode.c):
TL Series:
void rf_irq_handler(void)
{
// 1. Read interrupt status
unsigned int irq_status = rf_get_irq_status();
// 2. Clear interrupt flags
rf_clr_irq_status(irq_status);
// Dispatch by interrupt type
if (irq_status & FLD_RF_IRQ_TX) {
// Handle TX completion
}
if (irq_status & FLD_RF_IRQ_RX) {
// Handle RX completion
}
if (irq_status & FLD_RF_IRQ_RX_TIMEOUT) {
// Handle RX timeout
}
}
TC Series:
void irq_handler(void)
{
// 1. Read interrupt status
unsigned int irq_status = rf_irq_src_get();
// 2. Clear interrupt flags
rf_irq_clr_src(irq_status);
// Dispatch by interrupt type
if (irq_status & FLD_RF_IRQ_TX) {
// Handle TX completion
}
if (irq_status & FLD_RF_IRQ_RX) {
// Handle RX completion
}
if (irq_status & FLD_RF_IRQ_RX_TIMEOUT) {
// Handle RX timeout
}
}
RF DMA
RF Dedicated DMA Channels
RF TX/RX data transfer is performed through dedicated DMA channels:
| DMA Channel | Function |
|---|---|
| RF_TX_DMA (Ch0) | RF TX DMA channel, reads data from RAM and sends it to the RF hardware FIFO |
| RF_RX_DMA (Ch1) | RF RX DMA channel, receives data from the RF hardware FIFO and writes it to RAM |
TX DMA Configuration
The first two bytes of the TX buffer are DMA_LEN_INFO, indicating the data length to be transferred by DMA. The calculation of DMA_LEN_INFO varies by chip series:
- TL Series: Provides macro
rf_tx_packet_dma_len(rf_data_len)with formula((rf_data_len + 3) / 4) | ((rf_data_len % 4) << 22) - TC Series: Directly fill in the actual data transfer length (excluding the DMA_LEN_INFO bytes), LSB first
RX DMA Configuration
TL Series:
void rf_set_rx_dma(unsigned char *buff, unsigned char fifo_num, unsigned short fifo_byte_size)
| Parameter | Description |
|---|---|
| buff | RX buffer address, must be 4-byte aligned |
| fifo_num | RX FIFO count (total number of RX FIFOs minus 1) |
| fifo_byte_size | Depth of each FIFO in bytes, must be a multiple of 16 |
TL Series also provides TX DMA configuration interfaces:
void rf_set_tx_dma(unsigned char fifo_depth, unsigned short fifo_byte_size)
- fifo_depth: FIFO depth information. FIFO count = 2^fifo_depth.
- fifo_byte_size: Size of a single FIFO, in bytes.
TC Series:
void rf_rx_buffer_set(unsigned char *RF_RxAddr, int size, unsigned char PingpongEn)
| Parameter | Description |
|---|---|
| RF_RxAddr | RX buffer address, must be 4-byte aligned |
| size | RX buffer size, must be a multiple of 16 |
| PingpongEn | Ping-Pong buffer enable: 1 = enabled, 0 = disabled |
- When PingpongEn = 0: received data is stored in a single buffer pointed to by RF_RxAddr
- When PingpongEn = 1: received data is stored alternately in buffer0 and buffer1, the reserved RAM size should be size * 2
DMA Usage Notes
- DMA Configuration Timing Requirement: DMA-related configurations (
rf_rx_buffer_set/rf_set_rx_dma, etc.) must be completed before the RF state machine starts. The DMA must be configured before the RF state machine. - TX DMA Pre-fill and RF STX Start Timing Coordination: Before calling functions such as
rf_start_stx/rf_start_ptx, the TX buffer data must be ready. - RX DMA Ring Buffer and FIFO Overflow Timing Protection: The total amount of received data must not exceed the buffer size; otherwise, FIFO overflow will cause data loss.
- Buffer Must Be 4-Byte Aligned: TX / RX buffers must be 4-byte aligned. Use
__attribute__ ((aligned (4)))to declare them.
RF General Configuration
RF General Hardware Configuration
(1) rf_mode_init()
RF module initialization is the first step in using RF functionality. The TC series and TL series have differences, as described below:
- TL Series & New TC Series: First call
rf_mode_init()to complete basic initialization, then call the specific mode setting function. - TLSR825x / TLSR827x Series: Use
rf_drv_init(RF_ModeTypeDef)to complete initialization and mode selection in a single step.
Note
rf_mode_init()orrf_drv_init()must be called first, and only once during the system initialization phase.
(2) Modulation and OTA Data Rate Low-Level Configuration
| Protocol | TL Series API | TC Series API | Description |
|---|---|---|---|
| BLE 1M | rf_set_ble_1M_mode() |
rf_set_ble_1M_mode() |
Standard BLE 1 Mbps |
| BLE 2M | rf_set_ble_2M_mode() |
rf_set_ble_2M_mode() |
High-speed BLE 2 Mbps |
| BLE 500K | rf_set_ble_500K_mode() |
rf_set_ble_500K_mode() |
BLE Coded S=2 |
| BLE 125K | rf_set_ble_125K_mode() |
rf_set_ble_125K_mode() |
BLE Coded S=8 |
| Zigbee | rf_set_zigbee_250K_mode() |
rf_set_zigbee_250K_mode() |
250Kbps Zigbee O-QPSK |
| Proprietary | rf_set_pri_250K_mode(), etc. |
rf_set_pri_250K_mode(), etc. |
Selectable rate: 250K/500K/1M/2M bps |
Note
- TLSR825x / TLSR827x series chips complete mode selection in a single step via
rf_drv_init(RF_MODE_xxx). For example,rf_drv_init(RF_MODE_BLE_1M)sets the BLE 1M mode, andrf_drv_init(RF_MODE_ZIGBEE_250K)sets the Zigbee mode.
(3) Channel
BLE Channel Setting (recommended for BLE mode):
void rf_set_ble_chn(signed char chn_num)
Frequency mapping formula: f = 2402 + channel (MHz). The parameter chn_num is mapped according to the BLE specification; for example, chn_num = 37 -> 2402 MHz.
General Channel Setting (applicable to all modes):
TL series:
void rf_set_chn(signed char chn)
| Parameter | Description |
|---|---|
| chn | Channel index, actual frequency = 2400 + chn, range 2–80 |
TC series:
void rf_set_chn(signed char chn, unsigned short set)
| Parameter | Description |
|---|---|
| chn | Channel index, actual frequency = 2400 + chn, range 2–80 |
| set | Internal channel setting value, typically pass 0 |
Note
rf_set_chnis applicable to all modes;rf_set_ble_chnis applicable only to BLE mode.
(4) Power
Differences Between VBAT and VANT Power Supply Modes:
- VBAT Mode: The RF PA module is powered directly by the battery (VBAT). It supports a wide output power range, including high-power levels such as +10 dBm, but the actual output power decreases as the battery voltage drops.
- VANT Mode: The RF PA module is powered by an internal LDO regulator. The output power is stable (independent of battery voltage), making it suitable for low-power scenarios, but the maximum power level is lower.
Power setting interfaces:
void rf_set_power_level(RF_PowerTypeDef level)
void rf_set_power_level_index(RF_PowerTypeDef level)
The two are functionally equivalent, differing only in the enumeration type, and can be chosen as needed.
Note
- Different chips support different maximum transmit power levels. Refer to the
rf_drv.horrf_common.hheader file of the corresponding chip's SDK for the specific power enumeration values. The actual transmit power is affected by factors such as antenna matching and PCB routing, and hardware calibration is required during mass production.
(5) CRC
rf_crc_config_t hardware fields:
| Field | Description |
|---|---|
| init_value | CRC initial value |
| poly | Polynomial (BLE: 0x65b, Zigbee: 0x1021) |
| xor_out | Output XOR mask |
| byte_order | MSB First / LSB First |
| start_cal_pos | Start byte position for calculation |
| len | CRC length (0–4 bytes, 0 = disabled) |
Quick reference for default CRC configurations by protocol:
| Protocol | Polynomial | Length | Initial Value | Description |
|---|---|---|---|---|
| BLE 1M / 2M | 0x65b | 3 Bytes | 0x555555 | CRC-24 |
| BLE Coded | 0x65b | 3 Bytes | 0x555555 | CRC-24 |
| Zigbee | 0x1021 | 2 Bytes | 0x0000 | CRC-16 |
| Proprietary | 0x1021 | 2 Bytes | 0xffffffff | CRC-16 |
| Generic | User-configurable | 1–4 Bytes | User-configurable | Flexible configuration |
Note
- The CRC configuration is pre-configured at the driver layer by default. If custom configuration is needed, it must be configured according to the requirements of the specific protocol. Some chips do not support customization; refer to the corresponding chip's SDK documentation for details.
(6) Access Code
| Interface | Description |
|---|---|
rf_acc_code_set(pipe_id, addr) |
Sets the access code for the specified pipe |
rf_access_code_comm(acc) |
Sets the common access code (32-bit integer value) |
Note
- Zigbee mode has no access code concept. Its SFD is auto-detected by hardware and requires no software configuration.
(7) TX / RX Wait
In Auto mode, the wait time must be configured when switching between TX and RX:
void rf_set_rx_wait_time(unsigned short rx_wait_us) // TX -> RX switching wait time, in µs
void rf_set_tx_wait_time(unsigned short tx_wait_us) // RX -> TX switching wait time, in µs
These two interfaces configure the intermediate wait time when the state machine switches from TX to RX or RX to TX, ensuring sufficient transition time for the hardware.
Settle Hardware Timing Low-Level Configuration
(1) Settle Hardware Timing Definition and Low-Level Role
- TX Settle (TX_EN -> Before PA Ramp): Includes the PLL frequency locking + LDO stabilization + DAC settling phase. During this phase, the RF hardware completes frequency locking and transmit chain calibration to ensure transmit signal quality.
- RX Settle (RX_EN -> Before AGC Lock): Includes the PLL frequency locking + LDO stabilization + ADC settling phase. During this phase, the RF hardware completes frequency locking and receive chain calibration to ensure receive sensitivity.
Timing diagram:
graph LR
%% TX Settle
subgraph TX_Time [◄───────── TX Settle Time ──────────►]
direction LR
T1[TX_EN] --> T2[TX Settle]
T2 --> T3[PA Ramp]
end
T3 ==> T4((PA output))
%% RX Settle
subgraph RX_Time [◄───────── RX Settle Time ──────────►]
direction LR
R1[RX_EN] --> R2[RX Settle]
R2 --> R3[sync status]
end
R3 ==> R4((Start receiving))
%% Style
classDef default fill:#f4f6f9,stroke:#34495e,stroke-width:2px,color:#2c3e50;
classDef action fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef result fill:#e8f5e9,stroke:#4caf50,stroke-width:2px;
class T1,T2,T3,R1,R2,R3 action;
class T4,R4 result;
In Manual mode, the software must control the settle wait on its own. In Auto mode, the hardware state machine automatically completes the settle wait.
(2) Low-Level Configuration Interfaces
void rf_set_tx_settle_time(unsigned short tx_stl_us) // TX settle time, in µs
void rf_set_rx_settle_time(unsigned short rx_stl_us) // RX settle time, in µs
| Parameter | Default Value | Minimum Value | Maximum Value |
|---|---|---|---|
| TX Settle | 150us | 113us | 0xfff us |
| RX Settle | 150us | 85us | 0xfff us |
Note
- The minimum values listed here apply to most chip models. Some chips may have different minimum values. Refer to the Demo or driver comments for details.
Fast Settle
Fast Settle shortens the settle time by skipping some hardware calibration steps, thereby reducing power consumption and increasing TX/RX switching speed. The specific fast settle time options, enumeration definitions, and interfaces vary by chip series.
Note
- Fast Settle may differ across chips. Refer to the corresponding chip's driver comments for the specific fast settle time.
- Refer to the RF Demo for fast settle usage.
RF Module Low-Level Initialization
Refer to the RF Demo code for the RF module initialization flow as follows.
TL Series Initialization
graph TD
%% Node
Start(["System power-on / wake-up"])
Init["rf_mode_init()<br/>(RF RF basic initialization (once only))"]
Mode["Mode setting API<br/>(e.g. rf_set_ble_1M_mode())"]
Power["Power config<br/>rf_set_power_level()"]
Access["Access Code<br/>rf_access_code_comm()"]
Channel["Channel config<br/>rf_set_ble_chn()"]
DMA["DMA config<br/>rf_set_tx_dma()<br/>rf_set_rx_dma()"]
Optional["Optional config<br/>rf_set_tx_settle_time() /<br/>Fast Settle / PTA 等"]
End(["Start TX/RX<br/>(Auto / Manual Mode)"])
%% Connection
Start --> Init
Init --> Mode
%% Parallel branches
Mode --> Power
Mode --> Access
Mode --> Channel
%% Branch convergence
Power --> DMA
Access --> DMA
Channel --> DMA
%% Following process
DMA --> Optional
Optional --> End
%% Styling
classDef default fill:#f8f9fa,stroke:#6c757d,stroke-width:2px,color:#212529;
classDef start_end fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,color:#0d47a1;
classDef func fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20;
class Start,End start_end;
class Init,Mode,Power,Access,Channel,DMA,Optional func;
TL series initialization flow pseudocode (BLE 1M mode):
// Step 1: RF basic initialization (call once only)
rf_mode_init();
// Step 2: Set the communication protocol mode
rf_set_ble_1M_mode();
// Step 3: Configure transmit power
rf_set_power_level_index(target_power_level);
// Step 4: Set access code
rf_access_code_comm(access_code_value);
// Step 5: Configure DMA
rf_set_tx_dma(fifo_depth, fifo_size); // TX DMA FIFO configuration
rf_set_rx_dma(rx_buff, rx_fifo_num, // RX DMA FIFO configuration
fifo_byte_size);
// Step 6: Set channel
rf_set_ble_chn(channel_number);
TC Series Initialization
TC New Series:
graph TD
%% Node
Start(["System power-on / wake-up"])
Init["rf_mode_init()<br/>(RF basic initialization (once only))"]
Mode["Mode setting API<br/>(e.g. rf_set_ble_1M_mode())"]
Power["Power config<br/>rf_set_power_level_index()"]
Access["Access Code<br/>rf_access_code_comm()"]
Channel["Channel config<br/>rf_set_ble_channel()"]
DMA["DMA config<br/>rf_rx_buffer_set()"]
Optional["Optional config<br/>rf_set_tx_settle_time() /<br/>Fast Settle / PTA etc."]
End(["Start TX/RX<br/>(Auto / Manual Mode)"])
%% Connection
Start --> Init
Init --> Mode
%% Parallel branches
Mode --> Power
Mode --> Access
Mode --> Channel
%% Branch convergence
Power --> DMA
Access --> DMA
Channel --> DMA
%% Following process
DMA --> Optional
Optional --> End
%% Styling
classDef default fill:#f8f9fa,stroke:#6c757d,stroke-width:2px,color:#212529;
classDef start_end fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,color:#0d47a1;
classDef func fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20;
class Start,End start_end;
class Init,Mode,Power,Access,Channel,DMA,Optional func;
TC new series initialization flow pseudocode (BLE 1M mode):
// Step 1: RF basic initialization (call once only)
rf_mode_init();
// Step 2: Set the communication protocol mode
rf_set_ble_1M_mode();
// Step 3: Configure transmit power
rf_set_power_level_index(target_power_level);
// Step 4: Set access code
rf_access_code_comm(access_code_value);
// Step 5: Set channel
rf_set_ble_channel(channel_number);
// Step 6: Configure DMA
rf_rx_buffer_set(rx_buff, buff_size, pingpong_en);
// Step 7: Optional configuration
rf_set_tx_settle_time(tx_settle_us); // TX Settle time
rf_set_rx_settle_time(rx_settle_us); // RX Settle time
TLSR825x/TLSR827x Series:
graph TD
%% Node
Start(["System power-on / wake-up"])
Init["rf_drv_init(mode)<br/>(RF init + mode selection)<br/>e.g. rf_drv_init(RF_MODE_BLE_1M)<br/>((call once only))"]
Power["Power config<br/>rf_set_power_level_index()"]
Access["Access Code<br/>rf_access_code_comm()"]
Channel["Channel config<br/>rf_set_ble_channel()"]
DMA["DMA config<br/>rf_rx_buffer_set()"]
End(["Start TX/RX<br/>(Auto / Manual Mode)"])
%% Connection
Start --> Init
%% Parallel branches
Init --> Power
Init --> Access
Init --> Channel
%% Branch convergence
Power --> DMA
Access --> DMA
Channel --> DMA
%% Following process
DMA --> End
%% Styling
classDef default fill:#f8f9fa,stroke:#6c757d,stroke-width:2px,color:#212529;
classDef start_end fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,color:#0d47a1;
classDef func fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#1b5e20;
class Start,End start_end;
class Init,Power,Access,Channel,DMA func;
TLSR825x/TLSR827x Series initialization flow pseudocode:
// Step 1: RF init + mode selection (single step, call once only)
rf_drv_init(RF_MODE_BLE_1M);
// Step 2: Configure transmit power
rf_set_power_level_index(target_power_level);
// Step 3: Set access code
rf_access_code_comm(access_code_value);
// Step 4: Set channel
rf_set_ble_channel(channel_number);
// Step 5: Configure DMA
rf_rx_buffer_set(rx_buff, buff_size, pingpong_en);
RF PHY Test
Overview
What Is RF PHY Test
BQB (Bluetooth Qualification Body) certification is a mandatory qualification test that Bluetooth products must pass before being launched to the market. The Bluetooth Special Interest Group (Bluetooth SIG) requires all products using Bluetooth technology to pass BQB certification to ensure compliance with Bluetooth Core Specifications and interoperability between devices from different manufacturers.
RF PHY Test mainly verifies whether the RF physical layer (RF-PHY) performance of Bluetooth devices meets the requirements defined in the Bluetooth specifications. The test covers two major categories: transmitter (TX) and receiver (RX) performance.
Transmitter (TX) Test Items:
- Output Power
- Modulation Characteristics
- Carrier Frequency Offset and Drift
- In-band Spurious Emissions
Receiver (RX) Test Items:
- Sensitivity
- C/I Performance
- Blocking Performance
- Intermodulation Performance
- PER Report Integrity
Telink PHY Test Solution
The Telink chip PHY Test program supports two usage modes:
| Usage Mode | Description | Application Scenario |
|---|---|---|
| Direct UART Connection to Test Equipment | The chip communicates directly with Bluetooth test equipment (such as CMW500) through a 2-wire UART interface and follows the standard Bluetooth test protocol. | Formal qualification testing |
| BQB_Tool | BQB_Tool is integrated into Telink's BDT (Burning and Debugging Tool). It allows configuration parameters to be modified and provides basic functional verification. | Development debugging and pre-test verification |
Note
- Some TC series chips currently do not support the BQB function in BDT_Tool. Support for these chips will be added gradually in future releases.
PHY Test supports two command interaction methods:
| Command Interface | Description | Driver Support |
|---|---|---|
| 2-Wire | Communicates directly with test equipment through the UART interface and uses the standard Bluetooth test protocol. | Supported (default mode of the current driver) |
| HCI | Communicates with test equipment through the Host Controller Interface (HCI) protocol. HCI Commands are sent by the Host. | Not supported currently. Users can refer to the HCI implementation in the Telink BLE SDK for porting. |
Note
- The PHY Test driver currently supports communication with test equipment only through the 2-Wire UART interface. If the default UART configuration (such as baud rate and TX/RX pins) is used, the device can be connected to the test equipment directly. If users need to modify configuration parameters (such as UART pins, Access Code, or PA control), the configuration can be performed through BQB_Tool.
Chip and SDK Mapping
| Chip Series | SDK Path |
|---|---|
| TL Series | tl_platform_src\demo\vendor\RF_Certification\BQB_Demo |
| TC Series | tc_platform_src\demo\vendor\BQB_EMI_Demo |
BQB_Tool Introduction
Tool Overview
BQB_Tool is a configuration and validation tool integrated into BDT (Burning and Debugging Tool). Its main functions include:
- Configuration Modification: Modify configuration parameters of the BQB test program.
- Basic Function Validation: Simulate the instruction interaction between the test instrument and the chip to verify whether the chip PHY Test function works properly.
Working Principle
The PHY Test program communicates with test instruments (such as CMW500) through the 2-Wire UART interface and follows the standard test protocol defined by the Bluetooth specification. The communication process is shown below:

BQB_Tool can be used without an actual test instrument to:
- Modify configuration parameters in the chip firmware (UART pins, power, PA, etc.).
- Simulate test commands and verify whether the chip responds correctly.

Supported Configuration Items
BQB_Tool supports modification of the following configuration items:
| Configuration Item | Description | Default Value |
|---|---|---|
| UART Baud Rate | UART communication baud rate | 115200 |
| TX Pin | UART transmit pin | Depends on chip model |
| RX Pin | UART receive pin | Depends on chip model |
| Access Code | BLE access address | 0x29417671 |
| TX Power | Transmit power level | Depends on chip model |
| PA TX Pin | External PA transmit control pin | GPIO_PA0 |
| PA RX Pin | External PA receive control pin | GPIO_PA0 |
| Chip Power Supply Mode | LDO/DCDC power supply mode selection | Depends on chip model |
| Internal Capacitor | Whether to enable the internal capacitor | Enabled |
| Calibration Value Source | Flash/OTP/SRAM | Flash |
| Power Slice | Power slicing mode | Disabled |
| Swire through USB | Swire communication through USB | Disabled |

Basic Function Validation
The second function provided by BQB_Tool is basic function validation. It simulates the instruction interaction between the test instrument and the chip:
- Init Command: Configures test parameters (PHY mode, payload length, etc.), and the chip returns the execution status.
- RX Command: Starts the receive test. The chip enters RX mode and counts received packets.
- TX Command: Starts the transmit test. The chip transmits test packets according to the configured parameters.
- END Command: Ends the test. The chip returns the transmitted/received packet count.
With this function, users can quickly verify whether the chip BQB test firmware operates properly without using a test instrument.

Usage Instructions
Preparation
Hardware Preparation:
| Device | Description |
|---|---|
| EVK Development Board | Development board equipped with the chip under test |
| UART Cable | 2-Wire connection (TX + RX + GND) for connecting the EVK to the test instrument or PC |
| Test Instrument | Such as R&S CMW500/CMW270, used for official BQB qualification testing |
| RF Cable | Connects the EVK antenna port to the RF port of the test instrument |
| DC Power Supply | External power supply if required (optional) |
Software Preparation:
| Software | Description |
|---|---|
| BQB Test Binary | Compile the BQB_Demo project for the corresponding chip in the SDK |
| BDT Tool | Telink Burning and Debugging Tool, which includes BQB_Tool |
| Test Instrument Control Software | Such as CMWrun (R&S), used for automated testing |
General Operation Flow

Step 1: Compile and Download the BQB Test Program
- TL Series: Open the
tl_platform_src\demo\vendor\RF_Certification\BQB_Demoproject.- Compile and download the firmware to the chip.
- TC Series: Open the
tc_platform_src\demo\vendor\BQB_EMI_Demoproject.- Ensure that
TEST_DEMOis defined asBQB_DEMOinapp_config.h. - Compile and download the firmware to the chip.
- Ensure that
Step 2: Connect Hardware
Method 1: Connect Directly to the Test Instrument (Official Qualification Testing)
Chip UART TX ────-> Test Instrument UART RX
Chip UART RX ────-> Test Instrument UART TX
Chip GND ────-> Test Instrument GND
Chip RF ────-> Test Instrument RF Port
Method 2: Connect to PC Using BQB_Tool (Function Validation)
Chip UART TX ────-> USB-to-UART RX
Chip UART RX ────-> USB-to-UART TX
Chip GND ────-> USB-to-UART GND
Step 3: Configure Parameters (Optional)
If the default configuration is used, this step can be skipped. To modify the configuration:
- Open BQB_Tool in the BDT tool.
- Modify the required configuration items (UART pins, TX power, PA, etc.).
- Save the configuration and download the firmware again.
Step 4: Perform Testing
Using a Test Instrument: Follow the test instrument operation manual, select the BQB RF test items, and the instrument automatically sends SETUP/RX_TEST/TX_TEST/END commands and collects the test results.
Using BQB_Tool for Validation: Manually send commands in the tool and observe the chip response.
TX Test
Test Items and Specifications:
| Test Item | Bluetooth Specification Requirement | Description |
|---|---|---|
| Output Power | -20 dBm to +20 dBm (BLE) | The default chip transmit power must meet the requirement after calibration |
| Modulation Characteristics | Δf1avg ≥ 225 kHz, Δf2max ≥ 185 kHz (1M) | Frequency deviation characteristics |
| Carrier Frequency Offset | Within ±150 kHz | Frequency accuracy |
| Carrier Frequency Drift | Within ±50 kHz | Frequency stability |
| In-band Spurious Emissions | Adjacent channel ≤ -20 dBm | Spectrum mask compliance |
Test Procedure:
| Step | Operation | Description |
|---|---|---|
| 1 | Compile and Download | Compile the BQB_Demo project for the corresponding chip and download the firmware |
| 2 | Connect the Instrument | Connect the UART interface to the test instrument and connect the RF cable to the RF port of the test instrument |
| 3 | Configure the Instrument | Select the BLE BQB RF TX test item on the test instrument |
| 4 | Automatic Testing | The test instrument automatically sends the SETUP -> TX_TEST -> END command sequence |
| 5 | Check Results | The test instrument displays the TX test results for each item |
Typical Command Sequence:
SETUP: Reset -> BLE 1M mode
TX_TEST: Channel 0 (2402 MHz), Payload = 37 bytes, PRBS9
END: Retrieve transmitted packet count

RX Test
Test Items and Requirements:
| Test Item | Bluetooth Specification Requirement | Description |
|---|---|---|
| Sensitivity | ≤ -70 dBm (BLE 1M, PER ≤ 30.8%) | Receiver sensitivity |
| Carrier-to-Interference Ratio (C/I) | Co-channel C/I ≤ 21 dB | Resistance to co-channel interference |
| Blocking Performance | -30 dBm to +27 dBm depending on frequency offset | Resistance to out-of-band interference |
| Maximum Input Level | ≥ -10 dBm (PER ≤ 30.8%) | Maximum received power |
Procedure:
| Step | Operation | Description |
|---|---|---|
| 1 | Compile and download | Compile the BQB_Demo project for the corresponding chip and download the firmware |
| 2 | Connect the instrument | Connect the UART interface to the tester and connect the RF cable to the tester RF port |
| 3 | Configure the Instrument | Select the BLE BQB RF RX test item on the test instrument |
| 4 | Automatic Testing | The test instrument sends the SETUP -> RX_TEST command sequence, sends test packets, and finally sends the END command |
| 5 | Check Results | The test instrument calculates the PER based on the received packet count returned by the chip |
Typical Command Sequence:
SETUP: Reset -> BLE 1M mode
RX_TEST: Channel 0 (2402 MHz), Payload = 37 bytes
(The tester sends 1500 test packets)
END: Obtain received packet count -> Calculate PER

BQB_Tool Usage
When a test instrument is not available, BQB_Tool can be used for simple functional verification:
| Step | Operation | Description |
|---|---|---|
| 1 | Compile and download | Compile the BQB_Demo project for the corresponding chip and download the firmware |
| 2 | Connect the tool | Connect the UART interface to the PC and open BQB_Tool in BDT |
| 3 | Send SETUP | Send the reset command and confirm that the chip returns a successful status |
| 4 | Send TX_TEST | Send the TX test command, and the chip starts transmitting packets |
| 5 | Send END | Send the end command and check the transmitted packet count returned by the chip |
| 6 | Send RX_TEST | Send the RX test command, and the chip enters the receiving state |
| 7 | Send END | Send the end command and check the received packet count returned by the chip (should be 0 because no test instrument is transmitting packets) |

Common Issues and Notes
UART Connection Issues:
- Ensure that the TX/RX pins are connected correctly (TX of one side connects to RX of the other side, and RX connects to TX).
- The default baud rate is 115200. Ensure that the baud rates on both sides are consistent.
- The default UART pins vary depending on the chip model. Refer to the configuration files in the corresponding SDK:
- TL series:
tl_platform_src\demo\vendor\common\common\app_config\bqb_app_config.h(BQB_UART_TX_PORT/BQB_UART_RX_PORTmacros) - TC series:
tc_platform_src\demo\vendor\BQB_EMI_Demo\BQB\bqb.h(BQB_UART_TX_PORT/BQB_UART_RX_PORTmacros)
- TL series:
Access Code Configuration:
- The default Access Code is
0x29417671. - To modify the Access Code, use BQB_Tool or directly modify the
ACCESS_CODEmacro in the source code.
Power Calibration:
- The default transmit power varies between different chips. Before testing, verify the mapping between the power level and the actual output power.
- The power level can be adjusted through BQB_Tool.
PA Configuration:
- When using an external PA (Power Amplifier), configure the PA TX/RX control pins correctly.
- The PA TX and RX pins cannot be the same.
Internal Capacitor Calibration:
- The chip supports both internal capacitor and external capacitor configurations.
- When using the internal capacitor, the calibration value must be read from FLASH/OTP/SRAM.
- The calibration value storage address varies depending on the Flash size. Refer to the calibration header file of the corresponding chip:
- TL series:
tl_platform_src\demo\vendor\common\{chip_model}\calibration\calibration.h(same macro definitions as above) - TC series:
tc_platform_src\demo\vendor\common\{chip_model}\calibration\calibration.h(FLASH_CAP_VALUE_ADDR_64K/FLASH_CAP_VALUE_ADDR_128K/FLASH_CAP_VALUE_ADDR_512K/FLASH_CAP_VALUE_ADDR_1M/FLASH_CAP_VALUE_ADDR_2Mand other macros)
- TL series:
RF EMI
Overview
What is RF EMI testing?
RF EMI (Electromagnetic Interference) testing is a required RF indicator test for wireless chips during certification processes (such as FCC, CE, SRC, etc.). The EMI test firmware enables the chip to transmit specific RF test signals, such as single-tone, modulated, continuous packet, and burst packet signals. Together with measurement equipment such as a spectrum analyzer, it can be used to verify whether RF performance metrics, including transmit power, frequency offset, spurious emissions, and harmonics, comply with regulatory requirements.
Testing tools
The EMI testing program for Telink chips needs to be used together with the host tools:
| Chip Series | Tools | Description |
|---|---|---|
| TL Series | EMI_Tool (BDT) | Provides richer functionality and supports more configurable options. |
| TC Series | Non_Signaling_Test_Tool / EMI_Test_Tool | Standard non-signaling testing tools |
Note
- EMI test programs must be used in conjunction with the corresponding host tool. The tool communicates with the EVK board via the Swire (single-wire) interface, sends parameters to the chip, and after chip parsing, outputs the corresponding RF test signal.
EMI testing tools
Working Principle
The workflow of the EMI testing tool is as follows:

- Users select test modes and configuration parameters in the host tool.
- The host tool writes parameters to the SRAM specified address on the chip via the Swire (or USB-to-Swire) interface.
- The chip firmware polls commands and parameters stored in SRAM and executes the corresponding RF test functions.
- The test signals are output through the antenna port and measured by test equipment such as a spectrum analyzer.
EMI_Test_Tool/Non_Signaling_Test_Tool (TC series chips)
Compatible with TC series chips, supported configuration items include:
- RF mode selection: For specific modes, refer to the datasheet
- Frequency point setting: 2402MHz ~ 2480MHz
- Power settings: multi-level adjustable power
- Test modes: CarrierOnly, Burst (Prbs9/0x55/0x0F), Continue (Prbs9), RX
- Frequency hopping enable: Frequency hopping is supported in Continue mode
- PA Control: Supports TX/RX pin configuration for an external PA.
EMI_Tool (TL series chips)
Applicable to TL series chips, in addition to supporting all EMI_Test_Tool/Non_Signaling_Test_Tool functions, it additionally supports:
- BLE Access Code configuration: Customizable Access Code values for the BLE protocol
- Internal capacitor configuration: Optional to turn off internal capacitors
- PA bypass enable: supports PA bypass mode
- Swire through USB enable: Supports Swire communication via USB.
- Power Slice mode: Supports power slicing testing
- Adaptive interference mitigation: Supports environmental noise detection and pauses packet transmission when the noise level is too high.
- Packet Tone mode: Supports inserting a single-tone signal between packets.
Chip and tool mapping table
| Chip Series | Tools | SDK Path |
|---|---|---|
| TL Series | EMI_Tool | tl_platform_src\demo\vendor\RF_Certification\EMI_Demo |
| TC Series | EMI_Test_Tool/Non_Signaling_Test_Tool | tc_platform_src\demo\vendor\BQB_EMI_Demo |
EMI Modes and Corresponding APIs
Telink chip EMI testing supports the following four basic modes:
| Mode | Command ID (TC series) | Command ID (TL series) | Description |
|---|---|---|---|
| CarrierOnly (Single Tone) | 0x01 | 0x01 | Generates a single-frequency continuous wave signal |
| Continue | 0x02 | 0x02 | Generates a continuous modulation packet signal |
| RX | 0x03 | 0x03 | Receive Mode, Counts received packets and measures RSSI. |
| Burst | 0x04/0x05/0x06 | 0x04 | Generates burst modulation packet signals |
Supported wireless communication modes include: BLE1M, BLE2M, BLE125K, BLE500K, Zigbee250K, Private 2M, and Private 1M.
CarrierOnly (Single Carrier)
Function description:
CarrierOnly mode is used to generate a single-frequency continuous wave signal (unmodulated carrier). In this mode, the frequency, power level, and communication mode can be configured. This mode is mainly used to test indicators such as transmit power, frequency accuracy, and phase noise of the chip.
Spectrum analyzer phenomenon:
The spectrum analyzer displays a single-frequency peak signal with no modulation bandwidth expansion.

API interfaces:
TL Series:
void emicarrieronly(void);
TC Series:
void emicarrieronly(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
| Parameters | Description |
|---|---|
| rf_mode | RF mode: refer to each chip demo |
| pwr | Power Level Index |
| rf_chn | Channel Number (Frequency Range: 2402~2480MHz) |
Usage Example:
// TC series
emicarrieronly(RF_MODE_BLE_1M_NO_PN, power_level, 17); // BLE 1M mode, 2440MHz
// TL series - automatically called after configuration by the host computer
// Host computer settings: Mode = BLE1M, Frequency Point = 2440MHz, Power = 0dBm
// Chip auto-executes emicarrieronly()
Frequency hopping mode:
The TL series CarrierOnly mode supports hopping, allowing switching between multiple frequency points to output a single carrier signal for rapid verification of transmission performance across multiple frequency points.
Continue mode
Function description:
The Continue mode is used to generate continuous modulation signals. In this mode, the chip continuously sends modulated data, allowing settings of frequency point, power value, communication mode, and payload data type.
Supported Payload Data Types:
| pkt_type | Data types | Description |
|---|---|---|
| 0 | PRBS9 (random data) | Pseudo-random sequences simulate real data |
| 1 | 0x0F | Fixed data 0x0F |
| 2 | 0x55 | Fixed Data 0x55 (0101 alternating pattern) |
Spectrum analyzer phenomenon:
The spectrum analyzer displays a modulated signal continuously occupying channel bandwidth, with the signal uninterrupted.

API interfaces:
TL Series:
void emi_tx_continue(void);
TC Series:
void emi_con_prbs9(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
void emi_con_tx55(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
void emi_con_tx0f(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
Frequency hopping mode:
Continue mode supports frequency hopping. After enabling frequency hopping, the chip switches between different frequency points to send continuous packets according to a preset frequency hopping sequence.
Burst mode
Function description:
Burst mode is used to generate burst modulation packet signals. Unlike Continue mode, Burst mode transmits non-continuous packet signals with gaps between packets. Frequency points, power values, communication modes, and payload data types can be set.
Supported Payload data types: Refer to the data types supported by the tool interface
Spectrum analyzer phenomenon:
Because the Burst mode signal is not continuous, the spectrum analyzer needs to use Single Sweep or MaxHold settings to capture the signal:
- Single Sweep: A single scan captures a single burst packet
- MaxHold: Maximum hold. The complete spectrum envelope can be observed after multiple scans are accumulated.
Burst signal under Single Sweep settings:

Burst under MaxHold settings:

API interfaces:
TL Series:
void emi_tx_burst(void);
TC Series:
void emitxprbs9(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
void emitx55(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
void emitx0f(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
Adaptive Anti-Interference Mode (TL Series):
The TL series Burst mode supports adaptive anti-interference functionality. Once enabled, the chip continuously monitors environmental noise:
- When environmental noise is below -70dBm, Burst packets are sent normally
- When the environmental noise exceeds -70 dBm, packet transmission is paused, and the device switches to RX mode for continuous monitoring.
- Packet transmission automatically resumes when the noise level decreases.
RX mode
Function description:
RX mode is used for receiving tests. In this mode, the chip remains in the receive state and counts the number of received packets and RSSI (Received Signal Strength Indicator) values. Test packets need to be sent together with signal sources (such as comprehensive testers).
Spectrum analyzer phenomenon:
RX mode does not generate RF output signals; instead, the host computer reads the number of packets received and RSSI values through the host computer tools.
API interfaces:
TL Series:
void emirx(void);
TC Series:
void emirx(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
Data acquisition:
The chip reports the following data to the controller via SRAM:
| SRAM address offset | Content | Description |
|---|---|---|
| +0x04 | RSSI values | Current received signal strength |
| +0x0C | Package counting | The cumulative number of received packets |
Usage Instructions
Preparation
-
Hardware preparation:
- EVK Development Board (equipped with chip under test)
- Swire Cable (connecting EVK to PC)
- Spectrum analyzers (such as Keysight N9020A, etc.)
- RF cable (connecting the EVK antenna port to the spectrum analyzer)
- DC power supply (if external power supply is required)
-
Software preparation:
- EMI test bin file for the corresponding chip (compiling the EMI_Demo project in the SDK)
- Host computer tools: EMI_Tool (TL series) or EMI_Test_Tool/Non_Signaling_Test_Tool (TC series)
General Operation Flow
The general operation process is as follows:

CarrierOnly mode test instructions
| Steps | Operation | Description |
|---|---|---|
| 1 | Choose the tool | TC Series: Non_Signaling_Test_Tool; TL Series: EMI_Tool |
| 2 | Download the test bin | Compiling and downloading the EMI_Demo project for the corresponding chip |
| 3 | Configuration parameters | Mode selection: CarrierOnly; RF mode: BLE1M; Frequency point: 2440MHz; Power: 0dBm |
| 4 | Spectrum instrument settings | Center Freq: 2440MHz, Span: 10MHz, RBW: 100kHz |
| 5 | Observe the phenomenon | The spectrum analyzer displays a single peak signal at 2440MHz |

Continue mode test instructions
| Steps | Operation | Description |
|---|---|---|
| 1 | Choose the tool | TC Series: Non_Signaling_Test_Tool; TL Series: EMI_Tool |
| 2 | Download the test bin | Compiling and downloading the EMI_Demo project for the corresponding chip |
| 3 | Configuration parameters | Mode selection: Continue; RF mode: BLE 1M; Frequency point: 2440MHz; Power: 0dBm; Data type: Prbs9 |
| 4 | Spectrum instrument settings | Center Freq: 2440MHz, Span: 5MHz, RBW: 30kHz |
| 5 | Observe the phenomenon | The spectrum analyzer displays a continuous modulated signal, occupying about 1MHz of bandwidth |
Note
- For frequency hopping testing, enable the Hopping option in the host computer tool.

Burst mode test instructions
| Steps | Operation | Description |
|---|---|---|
| 1 | Choose the tool | TC Series: Non_Signaling_Test_Tool; TL Series: EMI_Tool |
| 2 | Download the test bin | Compiling and downloading the EMI_Demo project for the corresponding chip |
| 3 | Configuration parameters | Mode selection: Burst; RF mode: BLE 1M; Frequency point: 2440MHz; Power: 0dBm; Data type: Prbs9 |
| 4 | Spectrum instrument settings | Center Freq: 2440MHz, Span: 5MHz, RBW: 30kHz, Detector: MaxHold |
| 5 | Observe the phenomenon | After multiple scans using MaxHold, the spectrum analyzer shows discontinuous modulation signal envelopes |
Note
- Since the burst signal is not continuous, it is recommended to use the spectrum analyzer's MaxHold or Single Sweep mode to capture the signal.

RX mode test instructions
| Steps | Operation | Description |
|---|---|---|
| 1 | Choose the tool | TC Series: Non_Signaling_Test_Tool; TL Series: EMI_Tool |
| 2 | Download the test bin | Compiling and downloading the EMI_Demo project for the corresponding chip |
| 3 | Configuration parameters | Mode selection: RX; RF mode: BLE1M; Frequency: 2440MHz |
| 4 | Signal source settings | A comprehensive tester was used to send a BLE 1M test packet, with a frequency point of 2440MHz and a power of 1dBm |
| 5 | Observe the phenomenon | The host computer tool displays the number of packages received and RSSI values |
EMI_Tool Displays the number of packages received and RSSI values (TL Series):

Non_Signaling_Test_Tool Displays the number of packages received and RSSI values (TC Series):

Timer
Introduction
Timer is a general-purpose hardware timer. All chips support the following four modes: TIMER_SYS_CLOCK_MODE, TIMER_GPIO_TRIGGER_MODE, TIMER_GPIO_WIDTH_MODE, and TIMER_TICK_MODE.
Chip Feature Differences Summary
Chips differ in terms of timer channel count, input capture mode, and other aspects, as summarized below:
| Chip | Timer Count | Input Capture Mode |
|---|---|---|
| TC321x / B80/ B80B / B85 / B87 | 3 (TIMER0/1/2) | ❌ |
| B91 / B92 / TL321x / TL751x | 2 (TIMER0/1) | ❌ |
| TL721x / TL322x / TL323x | 2 (TIMER0/1) | ✅ |
Note
- The hardware channels through which GPIO signals connect to the timer also differ across chips, but this is encapsulated by
timer_gpio_init()and requires no attention from general users.
Timer Function
Timer supports four operating modes. All chips support these four basic modes.
System Clock Mode
Clock source: pclk
Function: Generates periodic interrupts. When the counter reaches the capture value, an interrupt is triggered, and the counter automatically reloads the initial_tick and restarts counting, repeating the cycle.
Setup steps (using initial_tick = 0, capture value = 50 ms as an example):
// TC Series
timer0_set_mode(TIMER_MODE_SYSCLK, 0, 50 * sys_clk.pclk * 1000);
timer_start(TIMER0);
// TL Series
timer_set_init_tick(TIMER0, 0);
timer_set_cap_tick(TIMER0, 50 * sys_clk.pclk * 1000);
timer_set_mode(TIMER0, TIMER_MODE_SYSCLK);
timer_start(TIMER0);
Result: LED2 toggles every 50 ms.

GPIO Trigger Mode
Clock source: GPIO edge transitions
Function: A specified number of GPIO rising/falling edges triggers an interrupt. The counter increments by 1 on each rising/falling edge. When the counter reaches the set value, an interrupt is generated, and the counter resets to zero and restarts.
Setup steps:
// TC Series
timer0_gpio_init(SW1, POL_RISING);
timer0_set_mode(TIMER_MODE_GPIO_TRIGGER, 0, TIMER_MODE_GPIO_TRIGGER_TICK);
timer_start(TIMER0);
// TL Series
timer_gpio_init(TIMER0, SW1, POL_RISING);
timer_set_init_tick(TIMER0, 0);
timer_set_cap_tick(TIMER0, TIMER_MODE_GPIO_TRIGGER_TICK);
timer_set_mode(TIMER0, TIMER_MODE_GPIO_TRIGGER);
timer_start(TIMER0);
Note
TIMER_MODE_GPIO_TRIGGER_TICKis a macro defined in the Demo (value0x01), representing the number of GPIO edge transitions required to trigger an interrupt. It is not defined in the driver header file.
Result (using capture value = 0xf, GPIO_PA2 generating a rising edge every 500 ms as an example): LED2 toggles once every 15 rising edges on GPIO_PA2.

GPIO Pulse Width Mode
Clock source: pclk
Function: Captures the GPIO pulse width. When the GPIO detects an edge of the configured polarity, the timer starts, incrementing by 1 for each pclk cycle. When the signal level flips, an interrupt is triggered, and the counter stops. The pulse width is calculated from the captured count value. One-shot mode (no repeat).
Setup steps:
// TC Series
timer0_gpio_init(SW1, POL_FALLING);
timer0_set_mode(TIMER_MODE_GPIO_WIDTH, 0, 0);
timer_start(TIMER0);
// TL Series
timer_gpio_init(TIMER0, SW1, POL_FALLING);
timer_set_mode(TIMER0, TIMER_MODE_GPIO_WIDTH);
timer_start(TIMER0);
Polarity Description: Setting POL_FALLING means the timer starts on a falling edge, and the interrupt is generated on a rising edge.
Result (a rising edge is generated 250 ms after a falling edge on GPIO_PA2, triggering the interrupt): LED2 toggles on the rising edge of GPIO_PA2. The timer register reads 0x005b8e01 = 6000129. At 24 MHz, 6000129 / 24M ≈ 250 ms.

Tick Mode
Clock source: pclk
Function: A time indicator that does not generate interrupts. The counter increments by 1 on each rising edge of pclk. Upon overflow, the counter is automatically reset to zero and starts counting again. Software can manually read or clear the counter value.
Setup steps (using initial_tick = 0, capture value = 0, manually resetting the timer every 500 ms as an example):
// TC Series
timer0_set_mode(TIMER_MODE_TICK, 0, 0);
timer_start(TIMER0);
// TL Series
timer_set_mode(TIMER0, TIMER_MODE_TICK);
timer_start(TIMER0);
Result: LED2 toggles every 500 ms.

Input Capture Mode
The Input Capture Mode is newly introduced, which captures the timer count value upon a GPIO edge transition. Refer to the "Chip Feature Differences Summary" for supported chips.
Capture Modes:
| Mode | Enum Value | Description |
|---|---|---|
| Rising edge capture | TMR_CAPT_RISING_EDGE |
Captures triggered on rising edge |
| Falling edge capture | TMR_CAPT_FALLING_EDGE |
Captures triggered on falling edge |
| Both edges capture | TMR_CAPT_RISING_FALLING_EDGE |
Captures triggered on both rising and falling edges |
Setup steps:
// 1. Configure GPIO and enable capture (select mode 0 or 3 first)
timer_set_init_tick(TIMER0, 0);
timer_set_cap_tick(TIMER0, 0); // capt is not used as the mode interrupt trigger value
timer_set_mode(TIMER0, TIMER_MODE_SYSCLK); // Or TIMER_MODE_TICK
timer_set_input_capture_mode(TIMER0, TMR_CAPT_RISING_FALLING_EDGE, GPIO_PA0);
timer_set_irq_mask(FLD_TMR0_CAPT_IRQ); // Enable capture interrupt
timer_start(TIMER0);
// 2. Read capture value in interrupt handler
void timer0_irq_handler(void) {
if (timer_get_irq_status(FLD_TMR0_CAPT_IRQ)) {
timer_clr_irq_status(FLD_TMR0_CAPT_IRQ);
unsigned int capt_val = timer_get_capture_value(TIMER0);
// Calculate PWM period: difference between two consecutive capture values
}
}
Timer API Differences
TC Series APIs
| Function | Description |
|---|---|
timer0_set_mode(mode, init_tick, cap_tick) |
Sets the Timer0 mode and parameters |
timer1_set_mode(mode, init_tick, cap_tick) |
Sets the Timer1 mode and parameters |
timer2_set_mode(mode, init_tick, cap_tick) |
Sets the Timer2 mode and parameters |
timer0_gpio_init(pin, pol) |
Initializes Timer0 GPIO |
timer1_gpio_init(pin, pol) |
Initializes Timer1 GPIO |
timer_start(type) |
Starts the timer |
timer_stop(type) |
Stops the timer |
timer_set_irq_mask(mask) |
Sets the interrupt mask (reg_irq_mask) |
timer_clr_irq_mask(mask) |
Clears the interrupt mask |
timer_clear_interrupt_status(status) |
Clears the interrupt status |
timer_get_interrupt_status(status) |
Gets the interrupt status |
TL Series APIs
| Function | Description |
|---|---|
timer_set_mode(type, mode) |
Sets the timer mode |
timer_gpio_init(type, pin, pol) |
Initializes GPIO |
timer_set_init_tick(type, init_tick) |
Sets the initial tick |
timer_set_cap_tick(type, cap_tick) |
Sets the capture tick |
timer_start(type) / timer_stop(type) |
Starts the timer |
timer_get_irq_status(status) |
Gets the interrupt status |
timer_clr_irq_status(status) |
Clears the interrupt status |
timer_set_irq_mask(mask) / timer_clr_irq_mask(mask) |
Interrupt mask (reg_tmr_ctrl3) |
timer0_get_tick() / timer1_get_tick() |
Reads the tick value |
timer_set_wrap(type) |
Sets the wrap mode |
In addition to the TL series basic APIs, some chips add input capture and DMA-related interfaces:
| Function | Description |
|---|---|
timer_set_input_capture_mode(type, capt_mode, pin) |
Sets the input capture mode |
timer_input_capture_en(type) |
Enables input capture mode |
timer_input_capture_dis(type) |
Disable input capture (TL721x only) |
timer_get_capture_value(type) |
Reads the capture value (independent of tick) |
timer_reset_tick(type, reset) |
Resets tick value (capture or compare) |
timer_set_rx_dma_config(type, chn) |
Configures DMA receive channel |
timer_receive_dma(type, addr, rev_size) |
Starts DMA receive |
timer_set_dma_chain_llp(type, chn, dst, len, head) |
Configures DMA chain head node |
timer_set_rx_dma_add_list_element(...) |
Adds DMA chain element |
GPIO Interrupt Routing Differences
The Timer GPIO Trigger Mode, GPIO Pulse Width Mode, and Input Capture Mode require routing external GPIO signals to the Timer module as the counting clock source. The routing mechanisms supported by different chips are described in the "Chip Feature Differences Summary".
Routing Principle
GPIO signals must pass through hardware routing channels before reaching the Timer module. Each Timer instance is fixedly mapped to one routing channel. The GPIO pin level signal is routed to the Timer counter through the selected routing channel after polarity selection (rising edge/falling edge).
Principle: The signal can be used either as a GPIO interrupt source to trigger CPU interrupts or as the Timer counting clock. The Timer GPIO modes only use the counting function and do not require enabling the GPIO interrupt mask. Otherwise, additional GPIO interrupts will be generated, causing frequent interrupt entry and exit.
The Timer and routing channel mapping relationships for different chips are as follows:
| Timer Channel | RISC Register Routing | gpio2risc Routing | PLIC GPIO_IRQ Routing (TL321x/TL322x/TL323x) | PLIC GPIO_IRQ Routing (TL751x) |
|---|---|---|---|---|
| TIMER0 | gpio_risc0 | gpio2risc0 | GPIO_IRQ1 | GPIO_IRQ0 |
| TIMER1 | gpio_risc1 | gpio2risc1 | GPIO_IRQ2 | GPIO_IRQ1 |
| TIMER2 | gpio_risc2 | - | - | - |
RISC Register Routing
Use the reg_gpio_irq_risc0_en(pin) / reg_gpio_irq_risc1_en(pin) / reg_gpio_irq_risc2_en(pin) registers to bind the specified GPIO to the corresponding RISC channel. Use reg_gpio_pol(pin) to set the trigger polarity, and use reg_irq_src (FLD_IRQ_GPIO_RISC0_EN / FLD_IRQ_GPIO_RISC1_EN) to enable the interrupt source.
The driver interfaces timer0_gpio_init(pin, pol) / timer1_gpio_init(pin, pol) already encapsulate the above configuration. Do not call gpio_set_risc_irq_mask() again to enable the RISC mask (otherwise a GPIO interrupt will be generated).
gpio2risc Routing
Use gpio_set_gpio2risc0_irq(pin, pol) / gpio_set_gpio2risc1_irq(pin, pol) to bind the GPIO to the gpio2risc channel, and use gpio_gpio2risc0_irq_en(pin) / gpio_gpio2risc1_irq_en(pin) to enable the channel.
The driver interface timer_gpio_init(type, pin, pol) already encapsulates the above configuration.
PLIC GPIO_IRQ Routing
The routing is implemented through independent interrupt channels in the PLIC: gpio_set_irq(GPIO_IRQn, pin, trigger_type) maps the GPIO to the corresponding channel, and gpio_set_irq_mask(GPIO_IRQ_IRQn) enables the interrupt channel mask. Note that the channel numbers for TL751x differ from other chips.
The driver interface timer_gpio_init(type, pin, pol) already encapsulates the above configuration.
Commonality: The timer_gpio_init interface on all chips internally performs GPIO input enable, pull-up/pull-down resistor configuration (pull-up for falling-edge triggering and pull-down for rising-edge triggering), polarity configuration, and routing configuration. Users do not need to manually configure GPIO registers.
Notes
Mode Setting Sequence
When configuring the Timer mode, configure init_tick and cap_tick first, then call timer_set_mode(), and finally call timer_start(). The timer_set_mode() function clears the existing interrupt status internally. Calling it first will overwrite the tick values configured later.
// Correct sequence (TL series)
timer_set_init_tick(TIMER0, 0);
timer_set_cap_tick(TIMER0, 50 * sys_clk.pclk * 1000);
timer_set_mode(TIMER0, TIMER_MODE_SYSCLK);
timer_start(TIMER0);
Interrupt Status Clearing Sequence
After entering the interrupt handler, the interrupt flag must be cleared first before performing any application processing; otherwise, the interrupt may be triggered repeatedly.
// TL series
void timer_irq_handler(void) {
if (timer_get_irq_status(FLD_TMR0_MODE_IRQ)) { // status bit
timer_clr_irq_status(FLD_TMR0_MODE_IRQ); // 1. Clear the flag first
// 2. Business logic handling
}
}
// TC series
void timer_irq_handler(void) {
if (timer_get_interrupt_status(TMR_STA_TMR0)) {
timer_clear_interrupt_status(TMR_STA_TMR0); // 1. Clear the flag first
// 2. Business logic handling
}
}
Note
FLD_TMR0_MODE_IRQis an interrupt status bit (timer_irq_eenum) used to query and clear interrupt status.FLD_TMR0_MODE_MASKis an interrupt mask bit (timer_mask_eenum) used bytimer_set_irq_mask()/timer_clr_irq_mask()to enable or disable interrupts. The two bits must not be mixed.
GPIO Pulse Width Mode Single Trigger
GPIO Pulse Width Mode automatically stops counting after triggering an interrupt once and does not run cyclically. For continuous measurement, timer_start(TIMER0) must be called again in the interrupt handler to restart the timer.
Tick Mode Without Interrupt
Tick Mode does not generate interrupts and can only determine the elapsed time by polling timer0_get_tick(). Interrupt mask configuration is not required in this mode and has no effect if configured.
GPIO Routing Resource Occupancy
GPIO Trigger Mode, GPIO Pulse Width Mode, and Input Capture Mode occupy the GPIO routing channel of the corresponding Timer (see the routing table in Chapter 4):
- The three GPIO modes of the same Timer are mutually exclusive and cannot be used simultaneously.
- The occupied routing channel cannot be used for regular GPIO external interrupts.
- TIMER0 and TIMER1 occupy their own routing channels and are independent of each other.
Input Capture Mode Dependency on Basic Modes
The Input Capture function (supported chips are listed in the "Chip Feature Differences Summary") requires System Clock Mode or Tick Mode to be configured first as the counting time base, followed by the capture mode configuration. In this mode, cap_tick is no longer used as an interrupt trigger value, but only as the tick reset point (together with timer_reset_tick()).
DMA Receive Configuration Sequence
When using DMA to transfer captured values, DMA must be configured first before enabling Input Capture Mode. Otherwise, a capture event may occur before DMA is ready, resulting in data loss:
// 1. Configure DMA first
timer_set_rx_dma_config(TIMER0, DMA_CH0);
timer_receive_dma(TIMER0, (unsigned char *)capt_buff, sizeof(capt_buff));
// 2. Then enable capture
timer_set_input_capture_mode(TIMER0, TMR_CAPT_RISING_EDGE, GPIO_PA0);
API Styles Must Not Be Mixed
The TC-style APIs (such as timer0_set_mode(mode, init, cap)) and TL-style APIs (such as timer_set_mode(type, mode) + timer_set_init_tick() + timer_set_cap_tick()) must not be mixed. When porting code, replace the entire API set instead of mixing APIs from different styles.
System Timer (STimer)
Introduction
System Timer (STimer) is a system time benchmark that provides basic time services and interrupt functions.
STimer Function Description
Example of using the Basic Time Service APIs
STimer provides system time, delay, and timeout detection functions. API names vary slightly across chip series, but usage is the same. For detailed interfaces, refer to the STimer API Differences.
Usage example:
// 1. Get the current system time as a reference point
unsigned int ref = stimer_get_tick(); TL Series; TC series uses clock_time()
// 2. Execute a piece of code or a delay
delay_ms(100); TL Series; TC Series sleep_ms (100)
// 3. Check if there is a timeout (unit: us)
if (clock_time_exceed(ref, 1000)) {
// More than 1 ms has elapsed
}
Overview with clock sources
STimer is a 32-bit system timer, with clock sources all sourced from onboard 24M crystals: some chips use 24M directly, while others use a 24M xtal to obtain 16M after a 2/3 divider and are supplied to STimer.
| Chip Series | Frequency |
|---|---|
| B80/B80B/B85/B87/TC321x/TC123x/B91 | 16 MHz |
| TC122x/B92/TL321x/TL721x/TL322x/TL323x/TL751x | 24 MHz |
STimer interrupt trigger conditions
The STimer interrupt trigger condition is the most important difference among chip series. During porting, the trigger condition must be confirmed first: whether it uses 32-bit full matching (tick_now == tick_capture) or range matching (within 1/64 of a cycle). With full matching, the interrupt cannot be triggered after the scheduled time has passed; with range matching, the interrupt can still be triggered after the scheduled time has passed.
Note
- 32-bit full matching: triggered only when the current tick and capture value are exactly equal; If the set time has already passed the capture value, it will not be triggered again. Since STimer's current tick is always 0 for the lower 3 bits, the capture tick interface will automatically mask the lower 3 bits to 0.
- Range Matching: The interrupt can be triggered as long as the current tick falls within the
[capture, capture + 2^26)range. This means the interrupt can still be triggered after the configured time has passed (within 1/64 of a cycle). This mechanism applies to scenarios in the application SDK where the theoretical STimer tick value is calculated based on remote time.
| Trigger types | Trigger conditions | Whether the interrupt can still be triggered after the scheduled time has passed | Chip Series |
|---|---|---|---|
| 32-bit full matching | tick_now & BIT[31:3] == tick_capture |
No | B80/B80B/B87/TC321x/TC122x |
| Scope matching | (tick_now - tick_capture) & BIT[31:26] == 0 |
Yes | B85/TC123x/B91/B92/TL321x/TL721x/TL322x/TL323x/TL751x |
Example of Delay Triggering (Range Matching Chip):
For chips using range matching, the interrupt is triggered immediately even if the capture value is set to a time in the past (as long as it is still within 1/64 of a cycle). The following example verifies this feature:
stimer_set_irq_capture(stimer_get_tick() - 10 * SYSTEM_TIMER_TICK_1MS);
// Due to range matching, the interrupt is triggered immediately even if it is 10 ms late (still within 1/64 of a cycle)
Note
- Fully matched chips (B80/B80B/B87/TC321x/TC122x) do not have this feature and cannot trigger interrupts when the scheduled time has passed.
Interrupt function and APIs
STimer generates interrupts at specified moments by setting capture values.
Configuration example (based on TL series STimer_Demo):
// 1. Set the capture value to trigger the interrupt after 1s
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1S);
// 2. Enable the STimer interrupt (mask macro names may vary slightly depending on the chip)
stimer_set_irq_mask(FLD_SYSTEM_IRQ_MASK);
// 3. Enable the PLIC interrupt and global interrupt
plic_interrupt_enable(IRQ_SYSTIMER);
core_interrupt_enable();
// 4. Interrupt service function (registered to IRQ_SYSTIMER)
_attribute_ram_code_sec_ void stimer_irq_handler(void)
{
if (stimer_get_irq_status(FLD_SYSTEM_IRQ)) {
gpio_toggle(LED2);
stimer_clr_irq_status(FLD_SYSTEM_IRQ); // Clear the interruption flag
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1S); // Reset the next trigger
}
}
PLIC_ISR_REGISTER(stimer_irq_handler, IRQ_SYSTIMER);
TC Series Equivalent Configuration:
stimer_set_capture_tick(clock_time() + 100 * CLOCK_SYS_CLOCK_1MS);
stimer_set_irq_mask(FLD_SYSTEM_IRQ_MASK);
irq_set_mask(FLD_IRQ_SYSTEM_TIMER_EDG_EN); // TC321x uses FLD_IRQ_SYSTEM_TIMER
irq_enable();
// Interrupt handling
void irq_handler(void)
{
if (stimer_get_irq_status()) {
stimer_clr_irq_status();
stimer_set_capture_tick(clock_time() + 100 * CLOCK_SYS_CLOCK_1MS);
}
}
STimer API differences
| Function | TC series APIs | TL series APIs |
|---|---|---|
| Get system time | clock_time |
stimer_get_tick |
| Set the tick value | stimer_set_tick |
stimer_set_tick |
| Delay | sleep_us / sleep_ms |
delay_us / delay_ms |
| Timeout check | clock_time_exceed |
clock_time_exceed |
| Set interrupt capture | stimer_set_capture_tick |
stimer_set_irq_capture |
| Interrupt masks | stimer_set_irq_mask / stimer_clr_irq_mask |
stimer_set_irq_mask / stimer_clr_irq_mask |
| Interrupt status | stimer_get_irq_status / stimer_clr_irq_status |
stimer_get_irq_status / stimer_clr_irq_status |
PWM
PWM Overview
The PWM (Pulse Width Modulation) module is used to generate square-wave signals with configurable duty cycles and periods, and can be widely applied in scenarios such as LED dimming, motor control, infrared transmission, and audio DAC driving.
The differences between chips equipped with PWM modules are shown in the table below:
| Chip | Channel Number | 32K Clock Source | Align Mode | Phase-Shifting Function | Pin Configuration API |
|---|---|---|---|---|---|
| B80 / B80B / B85 / B87 / TC321x / TC122x / TC123x | 6 channels(PWM0~PWM5) | Not support | Edge align | Not support | No dedicated interface (fixed pin mapping) |
| B91 | 6 channels(PWM0~PWM5) | Support | Edge align | Not support | pwm_set_pin |
| B92 / TL751x / TL321x / TL323x | 6 channels(PWM0~PWM5) | Support | Edge align + Center align | Support | pwm_set_pin |
| TL721x | 7 channels(PWM0~PWM6) | Support | Edge align + Center align | Support | pwm_set_pin |
| TL322x | 24 channels(PWM0~PWM23) | Support | Edge align + Center align | Support | pwm_set_pin |
Note
- B91 includes TLSR951x and TLSR921x, B92 includes TLSR952x and TLSR922x, B80 includes TLSR8208A/B/C/D and TLSR8373E/F, B80B includes TLSR8208E/F/G/H/J and TLSR8373A/B/C/FBR, B85 includes TLSR825x and TLSR8359, B87 includes TLSR827x and TLSR8355.
General Features: All chips support the pclk clock source, 5 operating modes (PWM0) + continuous (other channels), IR FIFO mode, IR DMA FIFO mode, DMA chained transfer, Invert/Polarity, and N-channel invert.
Modes supported by PWM0:
- Continuous mode
- Counting mode
- IR mode
- IR FIFO mode
- IR DMA FIFO mode
Channels other than PWM0 support only continuous mode.
Clock
The PWM has two clock sources: pclk or 32kHz.
PCLK Clock Source
Function: Supports frequency division. The divided clock is used as the PWM clock source.
Note
Ensure that pclk_frequency is an integer multiple of pwm_frequency; otherwise, the PWM output is not at the expected frequency.
Interface Configuration:
static inline void pwm_set_clk(unsigned int pwm_clk_div);
Where: pwm_clk_div = (pclk_frequency / pwm_frequency) - 1
32kHz Clock Source
Function: Does not support frequency division and only supports continuous mode and counting mode. This configuration is primarily intended to allow PWM waveform output even in suspend mode.
Interface Configuration:
// Chips other than the TL322x
static inline void pwm_32k_chn_en(pwm_clk_32k_en_chn_e pwm_32K_en_chn);
static inline void pwm_32k_chn_dis(pwm_clk_32k_en_chn_e pwm_32K_en_chn);
// TL322x
static inline void pwm_32k_chn_en(pwm_id_e id);
static inline void pwm_32k_chn_dis(pwm_id_e id);
Note
- All channels default to the pclk clock source. To use the 32kHz clock source, call
pwm_32k_chn_ento enable the corresponding channel. Channels that are not enabled continue to use the pclk clock source. - The 32kHz clock source was designed only for suspend scenarios. When using interrupts in continuous mode or counting mode, the interrupt is entered one 32kHz clock cycle early. During this 32kHz clock cycle, the interrupt is exited and re-entered. If an interrupt is needed when using 32kHz PWM, it is recommended to use a GPIO interrupt instead.
- When the PWM clock source is 48 MHz and the duty cycle is strictly 50% (ideal square wave), the theoretical maximum frequency of the PWM output waveform is 24 MHz. Each output cycle requires at least 2 PWM clock cycles (1 cycle high, 1 cycle low). Maximum PWM output frequency = pwm_frequency / 2.
Duty Cycle
A PWM signal frame consists of two parts: Count status (high-level duration) and Remaining status (low-level duration). The specific waveform of a signal frame is shown below, where tmax is the cycle time.

In the driver, the functions for setting the signal frame cycle and duty cycle both use tcmp and tmax as parameters, correspond to registers TCMP/TMAX.
General Duty Cycle Configuration Interface
static inline void pwm_set_tcmp(pwm_id_e id, unsigned short tcmp);
static inline void pwm_set_tmax(pwm_id_e id, unsigned short tmax);
Parameter description:
id: Selects which PWM channel.tcmp: Sets the high-level duration.tmax: Sets the cycle period.
Note
- The parameter sets the PWM cycle, its parameter type is short. The minimum value of tmax is 1 and cannot be 0. If it is 0, the PWM is in a non-operating state. Therefore, the valid range of tmax is: 1~65535.
- The parameter sets the PWM duty cycle, its parameter type is short. The minimum value of tcmp can be 0, in which case the PWM waveform is always low. The maximum value can be tmax, in which case the PWM waveform is always high. Therefore, the valid range of tcmp is: 0~tmax.
- When
pwm_set_tcmp()orpwm_set_tmax()is called while the PWM is actively transmitting, the new value takes effect at the beginning of the next cycle.
IR FIFO Mode and IR DMA FIFO Mode Shadow Interface
When using PWM0 in IR FIFO Mode or IR DMA FIFO Mode, an additional function interface is used:
static inline void pwm_set_pwm0_tcmp_and_tmax_shadow(unsigned short max_tick, unsigned short cmp_tick);
Note
The parameter max_tick sets the cycle of PWM0, and the parameter cmp_tick sets the high-level duration of PWM0. The range of max_tick is: 1~65536, and the range of cmp_tick is: 0~max_tick.
Pulse Count Configuration Interface
When using PWM0 in counting mode or IR mode, the number of output pulses needs to be set. The function interface used is:
static inline void pwm_set_pwm0_pulse_num(unsigned short pulse_num);
pulse_num: The number of pulses. The maximum value that can be written to the register is 14 bits. Range: 0~16383.
IR FIFO Configuration Data Interface
When writing cfg data to the FIFO for PWM0, the function interfaces used are:
static inline void pwm_set_pwm0_ir_fifo_cfg_data(unsigned short pulse_num, unsigned char use_shadow, unsigned char carrier_en);
static inline unsigned short pwm_cal_pwm0_ir_fifo_cfg_data(unsigned short pulse_num, unsigned char shadow_en, unsigned char carrier_en);
use_shadow:
- 1: Use the cycle and duty cycle set by the
pwm_set_pwm0_tcmp_and_tmax_shadowfunction. - 0: Use the cycle and duty cycle set by the
pwm_set_tmaxandpwm_set_tcmpfunctions.
carrier_en:
- 1: Output pulses according to the settings of the pulse_num and use_shadow parameters.
- 0: Output low level, with the duration calculated based on the pulse_num and use_shadow parameters.
Alignment Mode
For chips that support center alignment, see the "Summary Table of Chip Feature Differences" in PWM Overview. PWM supports two alignment modes: edge alignment (default) and center alignment. Chips that support only edge alignment cannot enable center alignment.
Edge-Aligned
In this mode, the waveform of each cycle is aligned at the edge. (PWM2 waveform)
Center-Aligned
In this mode, the waveforms of two consecutive cycles are aligned at the center point (PWM1 waveform).

Interface Configuration:
static inline void pwm_set_align_en(pwm_id_e id); // Enable center alignment
static inline void pwm_set_align_dis(pwm_id_e id); // Disable center alignment (restore edge alignment)
Phase Shift Function
For chips that support the phase-shift function, see the "Summary Table of Chip Feature Differences" in PWM Overview. The PWM phase shift function allows setting the phase offset time of the PWM. The phase offset time only takes effect before the first cycle (in the figure below, PWM2 has a phase offset time of 25 \(\mu\)s).

Interface Configuration:
static inline void pwm_set_shift_time(pwm_id_e id, unsigned short shift_clk_num);
Note
- The phase shift time must be set before setting the duty cycle and cycle period.
- If you want to reconfigure the phase shift time, you must reconfigure tcmp and tmax after reconfiguring the phase shift time (regardless of whether the tcmp and tmax values need to be modified).
- If you only want to reconfigure tcmp and tmax, you only need to configure tcmp and tmax without first configuring the phase shift time.
Invert/Polarity
For the waveform set by the duty cycle interface, by default, the high level (Count status) is output first, followed by the low level (Remaining status).
Difference between Invert and Polarity:
- Invert function: Enabling the invert function of a PWM channel via
pwm_invert_eninverts the PWM_PIN waveform. It takes effect regardless of whether the PWM is started. Therefore, if the invert function is enabled before the PWM starts, the initial low level is also inverted to a high level (the initial level defaults to low and cannot be modified). After the PWM starts, the output follows this rule: Count status outputs low, Remaining status outputs high.

- Polarity function: Enabling the polarity function of a PWM channel via
pwm_set_polarity_enonly takes effect after the PWM is started, so it does not invert the initial level before the PWM starts. After the PWM starts, the output follows this rule: Count status outputs low, Remaining status outputs high.

Note
- The Invert and Polarity functions cannot be enabled simultaneously; only one of them can be selected.
- After the PWM starts, the Invert and Polarity functions take effect immediately without waiting for the next cycle.
Related Interfaces:
static inline void pwm_invert_en(pwm_id_e id); // Enable PWM output inversion
static inline void pwm_invert_dis(pwm_id_e id); // Disable PWM output inversion
static inline void pwm_n_invert_en(pwm_id_e id); // Enable PWM_N output inversion
static inline void pwm_n_invert_dis(pwm_id_e id); // Disable PWM_N output inversion
static inline void pwm_set_polarity_en(pwm_id_e id); // Enable polarity inversion
static inline void pwm_set_polarity_dis(pwm_id_e id); // Disable polarity inversion
Functional Description
Continuous mode
In this mode, signals are continuously sent according to the configured duty cycle. To stop, call stop, and it stops immediately. During transmission, the duty cycle can be updated, and the new duty cycle takes effect in the next frame.
(1) Experimental Results

The figure above shows the experimental results captured with a logic analyzer:
- Channel 0 (LED1): PWM output signal.
- Channel 1 (LED4): Interrupt indicator GPIO. An interrupt is generated each time a signal frame is sent.
Red box explanation: The delay in interrupt generation is due to the software and hardware processing time required for the CPU to enter the interrupt.
(2) Stop Verification
The following experiment verifies that in continuous mode, after executing stop, the signal stops immediately.

As shown in the figure, after LED3 toggles, the PWM signal stops immediately.
(3) Duty Cycle Update Verification
The following experiment verifies that in continuous mode, the duty cycle can be updated during transmission, and the updated duty cycle takes effect in the next frame.

As shown in the figure, after LED3 toggles, the PWM duty cycle is modified and takes effect in the next frame.
Configuration Interface:
pwm_set_pwm0_mode(PWM_NORMAL_MODE); // Set PWM0 mode
pwm_start(FLD_PWM0_EN); // Start PWM
pwm_stop(FLD_PWM0_EN); // Stop PWM
Counting Mode
Sends the configured number of signal frames and then stops. In this mode, calling stop stops it immediately. During transmission in this mode, modifying the duty cycle does not change it.
(1) COUNT_FRAME_INIT Experimental Results

- Channel 0 (LED1): PWM output signal.
- Channel 1 (LED4): Interrupt indicator GPIO. An interrupt is generated each time a signal frame is sent.
Red box explanation: The delay in interrupt generation is due to the software and hardware processing time required for the CPU to enter the interrupt.
(2) COUNT_PNUM_INIT Experimental Results

Details of the red box are as follows:

- Channel 0 (LED1): PWM output signal.
- Channel 1 (LED4): Interrupt indicator GPIO. An interrupt is generated after the specified number of pulses have been sent.
Red box explanation: The 3 \(\mu\)s excess over 50 \(\mu\)s indicates a certain delay in entering the interrupt, due to the software and hardware processing time required for the CPU to enter the interrupt.
(3) Stop Verification

As shown in the figure, after LED3 toggles, the PWM signal stops immediately.
(4) Duty Cycle Verification
The following experiment verifies that in counting mode, the duty cycle cannot be changed during transmission.

As shown in the figure, after LED3 toggles, the PWM signal does not change.
Configuration Interface:
pwm_set_pwm0_mode(PWM_COUNT_MODE);
pwm_set_pwm0_pulse_num(PWM_PULSE_NUM); // Set the number of pulses
pwm_start(FLD_PWM0_EN);
IR Mode
IR mode continuously sends pulse groups. The duty cycle can be changed in between, and it takes effect in the next pulse group. To stop immediately, you can call stop directly. The difference between IR mode and counting mode is that counting mode stops after sending one pulse group, while IR mode continuously sends pulse groups.
(1) Experimental Results

- Channel 0 (LED1): PWM output signal.
- Channel 1 (LED3): Interrupt indicator GPIO. Toggles once for each pulse group is sent.
Red box explanation: The delay in interrupt generation is due to the software and hardware processing time required for the CPU to enter the interrupt.
(2) Stop Verification

As shown in the figure, after LED3 toggles, the PWM signal stops immediately.
(3) Duty Cycle Update Verification
The following experiment verifies that in IR mode, the duty cycle can be changed in between, but it takes effect after the current pulse group finishes executing.

As shown in the figure, after LED3 toggles, the PWM signal does not change immediately, it takes effect only after the current pulse group completes executing.
To stop IR mode while completing the current pulse group, you can switch to counting mode.
To stop immediately, you can call stop directly.
Note
- To stop IR mode while completing the current pulse group, you can switch to counting mode in the interrupt. However, during the switch, the current pulse group in IR mode finishes sending before the switch takes effect.
Configuration Interface:
pwm_set_pwm0_mode(PWM_IR_MODE);
pwm_set_pwm0_pulse_num(PWM_PULSE_NUM); //set the pulse number in each pulse group
pwm_start(FLD_PWM0_EN);
IR FIFO mode
Without MCU intervention, long code patterns can be sent. The IR carrier frequency is obtained by dividing the system clock and can support common frequencies. The "Fifo cfg data" element serves as the basic unit of the IR waveform. The hardware parses the cfg information and emits the corresponding signal.
(1) Experimental Results

- Channel 0 (LED1): PWM output signal.
- Channel 1 (LED4): Interrupt indicator GPIO. Toggles once when the number of cfg data entries in the FIFO is less than (not including equal to) the configured value (trigger_level is 1).
Red box explanation: The delay in interrupt generation is due to the software and hardware processing time required for the CPU to enter the interrupt.
(2) Stop Verification
The following experiment verifies that in IR FIFO mode, after executing stop, only the execution of the current cfg data is stopped, without affecting the execution of subsequent cfg data in the FIFO.

As shown in the figure, after executing stop, LED3 toggles, stopping the execution of the current cfg data 1 without affecting the execution of the subsequent cfg data 2 in the FIFO.
IR FIFO Mode sequentially retrieves cfg data from the FIFO and emits the corresponding signal until the FIFO is empty. In this mode, stop can be used, but it only stops the execution of the current cfg data without affecting the execution of subsequent cfg data in the FIFO.
Note
- In IR FIFO mode, as long as there is data in the FIFO, it is continuously sent out (automatically) without needing a start signal. Similarly, IR DMA FIFO Mode does not need a start signal. However, in other modes, the
pwm_startsignal is required. - Each time the function
pwm_set_pwm0_ir_fifo_cfg_datais called, the FIFO count increments by 1 (if the FIFO is full at this time, it waits until the FIFO is not full before writing). Each time the hardware retrieves one entry from the FIFO, the FIFO count decrements by 1. The FIFO depth is 8 bytes. After data is retrieved from the FIFO, the signal transmission action is executed. The next entry is retrieved from the FIFO only after the current signal has finished executing.
FIFO Status Query Interfaces:
static inline unsigned char pwm_get_pwm0_ir_fifo_data_num(void); // Get the number of data entries in the FIFO
static inline unsigned char pwm_get_pwm0_ir_fifo_is_empty(void); // Check if the FIFO is empty
static inline unsigned char pwm_get_pwm0_ir_fifo_is_full(void); // Check if the FIFO is full
static inline void pwm_clr_pwm0_ir_fifo(void); // Clear FIFO data
Configuration Interface:
pwm_set_pwm0_mode(PWM_IR_FIFO_MODE);
pwm_set_pwm0_ir_fifo_cfg_data(PWM_PULSE_NUM1, 1, 1); // Write FIFO configuration data
pwm_set_pwm0_ir_fifo_cfg_data(PWM_PULSE_NUM2, 0, 1);
IR DMA FIFO Mode
IR DMA FIFO mode is similar to IR FIFO mode, except that the configuration is not directly written to the FIFO by the MCU but is written to the FIFO via DMA.
(1) PWM_IR_FIFO_DMA Experimental Results

- Channel 0 (LED1): PWM output signal.
- Channel 1 (LED4): Interrupt indicator GPIO.
As shown in the figure above, in this mode, the interrupt is triggered only after all cfg data entries in the FIFO have been executed, which differs from the interrupt mechanism in IR FIFO Mode.
The figure below is an enlarged view of the red box in the above figure:

As shown in the figure, the duration of the last low-level of cfg data 3 is 105 us (not the 100 us set by cfg data 3). Therefore, with this usage method, after sending the first set of DMA data, re-triggering the DMA in the interrupt introduces a certain delay in the signal.
(2) PWM_CHAIN_DMA Experimental Results
Using chained DMA enables continuous transmission without MCU intervention. The linked list structure is as follows:

First, create the head node head_of_list, then add nodes to the circular linked list. As shown in the flowchart, execution starts with the head pointer, and then cycles through each node in sequence until the LLP is set to 0, at which point it stops.

A detailed explanation of the red box: there is no delay during the switching of cfg data between pointer 1 and pointer 2.


As shown in the figures, there is no delay. Therefore, using the linked-list approach allows continuous PWM waveforms to be generated.
Note
- In the interrupt, some DMA configurations need to be updated: source address update, DMA trigger, etc.
- In this mode, unlike IR FIFO mode, the interrupt is not triggered when the number of cfg data entries in the FIFO is empty, but rather when all PWM signal frames configured in the FIFO have been fully executed.
DMA Configuration Interface:
void pwm_set_dma_config(dma_chn_e chn); // Configure DMA channel
void pwm_set_dma_buf(dma_chn_e chn, unsigned int buf_addr, unsigned int len); // Set DMA buffer
void pwm_ir_dma_mode_start(dma_chn_e chn); // Start DMA
DMA Linked List Configuration Interface (for chained DMA):
void pwm_set_dma_chain_llp(dma_chn_e chn, unsigned short *src_addr, unsigned int data_len, dma_chain_config_t *head_of_list);
void pwm_set_tx_dma_add_list_element(dma_chn_e chn, dma_chain_config_t *config_addr, dma_chain_config_t *llpoint, unsigned short *src_addr, unsigned int data_len);
Note
- When using chained DMA, the source address
src_addrmust be word-aligned (4 bytes); otherwise, the program enters an exception state.
Interrupts
The supported PWM interrupt settings are described below (the hardware does not automatically clear interrupt flags; they must be cleared manually by software).
Interrupts Supported by PWM0
| Interrupt Type | Description |
|---|---|
| FLD_PWM_FRAME_DONE_IRQ | Generated when each signal frame is completed |
| FLD_PWM0_PNUM_IRQ | Generated each time a pulse group is sent |
| FLD_PWM0_IR_FIFO_IRQ | Entered when the number of cfg data entries in the FIFO is less than (not including equal to) the configured value (trigger_level) |
| FLD_PWM0_IR_DMA_FIFO_IRQ | Entered after the FIFO finishes executing the cfg data sent by DMA |
Interrupts Supported by other channels
| Interrupt Type | Description |
|---|---|
| FLD_PWM_FRAME_DONE_IRQ | Generated when each signal frame is completed |
Note
- All other channels on all chips support only the "frame done" interrupt.
Interrupt Configuration Interface
TC series:
void pwm_set_interrupt_enable(PWM_IRQ irq); // Enable PWM interrupt
void pwm_set_interrupt_disable(PWM_IRQ irq); // Disable PWM interrupt
unsigned char pwm_get_interrupt_status(PWM_IRQ status); // Get PWM interrupt status
void pwm_clear_interrupt_status(PWM_IRQ status); // Clear PWM interrupt status
Note
- The TC series uses the
PWM_IRQenumeration for interrupt masks and states (e.g.,PWM_IRQ_PWM0_FRAME,PWM_IRQ_PWM0_PNUM,PWM_IRQ_PWM0_IR_FIFO,PWM_IRQ_PWM0_IR_DMA_FIFO_DONE), with the interrupt type for each channel listed separately (e.g.,PWM_IRQ_PWM1_FRAME,PWM_IRQ_PWM2_FRAME, etc.). Among these,PWM_IRQ_PWM0_IR_FIFOmust be configured via the separatereg_pwm0_fifo_mode_irq_maskandreg_pwm0_fifo_mode_irq_staregisters; the interface internally distinguishes between them.
TL series:
void pwm_set_irq_mask(pwm_irq_e mask); // Enable PWM interrupt mask
void pwm_clr_irq_mask(pwm_irq_e mask); // Disable PWM interrupt mask
unsigned char pwm_get_irq_status(pwm_irq_e status); // Get PWM interrupt status
void pwm_clr_irq_status(pwm_irq_e status); // Clear PWM interrupt status
TL322x:
void pwm_set_irq_mask(pwm_id_e id, pwm_irq_type_e type); // Enable PWM interrupt mask
void pwm_clr_irq_mask(pwm_id_e id, pwm_irq_type_e type); // Disable PWM interrupt mask
unsigned int pwm_get_irq_status(pwm_id_e id, pwm_irq_type_e type); // Get PWM interrupt status
void pwm_clr_irq_status(pwm_id_e id, pwm_irq_type_e type); // Clear PWM interrupt status
Interface Differences: The TC series uses pwm_set_interrupt_enable/disable + pwm_clear_interrupt_status (separate registers distinguish IR FIFO interrupts); The TL series uses pwm_set_irq_mask/clr_irq_mask + pwm_clr_irq_status; the TL322x requires an additional pwm_id_e channel parameter.
Note
- There is a delay of approximately 2 to 4 us when responding to an interrupt.
- In the interrupt handler, the interrupt flag should be cleared before processing the main task to prevent repeated interrupts.
IR FIFO Mode Trigger Level Configuration
The trigger_level value for IR FIFO mode can be configured via the following function interface:
static inline void pwm_set_pwm0_ir_fifo_irq_trig_level(unsigned char trig_level);
When the number of data entries in the FIFO falls below this value, an interrupt is triggered.
Notes
- Channel Differences: PWM0 supports all 5 modes, while PWM1~PWM23 only support continuous mode.
- 32kHz Clock Source: When using the 32kHz clock source, only continuous mode and counting mode are supported, and frequency division is not supported.
- DMA Address Alignment: When using the DMA function, the source address must be word-aligned (4 bytes); otherwise, the program enters an exception state.
- FIFO Depth: The depth of the IR FIFO is 8 bytes.
-
Duty Cycle Update:
- In continuous mode, updating the duty cycle takes effect in the next frame.
- In counting mode, modifying the duty cycle during transmission does not change the duty cycle.
- In IR mode, the duty cycle can be changed in between, but it takes effect after the current pulse group finishes executing.
-
Stop Behavior:
- In continuous mode and counting mode, stop immediately halts signal output.
- In IR FIFO mode, stop only halts the execution of the current cfg data without affecting the execution of subsequent cfg data in the FIFO.
-
Advanced Features: Advanced features such as center alignment and phase shift are supported only by certain chips; see the "Summary Table of Chip Feature Differences" in PWM Overview for details.
- 32kHz Clock Interrupt Delay: When using the 32kHz clock source, interrupts in continuous mode and counting mode trigger one 32kHz clock cycle earlier than expected. If an interrupt is required, it is recommended to implement it using a GPIO interrupt.
I2C
Introduction
I2C (Inter-Integrated Circuit) is a serial bus composed of the data line SDA and the clock line SCL. It can send and receive data and is a half-duplex communication method. The clock is controlled by the main device.

The detailed explanation of the I2C communication protocol is as follows:
| Status | Process |
|---|---|
| Idle state | When both the SDA and SCL signals of the I2C bus are at high levels, they are defined as the bus's idle state. |
| Start signal | During the SCL at a high level, the SDA jumps from high to low. |
| Stop signal | During the SCL at a high level, the SDA jumps from low to high. |
| Ackowledgement signal | The requirement for the feedback effective response bit ACK is: the receiver pulls the SDA signal low during the low level of the 9th clock pulse and maintains a stable low level during the high level of that clock. If the receiver is the master device, after receiving the last byte, it sends a NACK signal to notify the controlled transmitter to end data transmission and release the SDA signal, so the master device receiver can send a stop signal P. |
| Data validity | When the I2C bus is used for data transmission, the data on the data line must remain stable during high clock signals. Only when the signal on the clock line is at a low level is the high or low state on the data line allowed to change. |
| Data transmission | Each bit of data transmitted on the I2C bus has a corresponding clock pulse (or synchronization control), meaning each bit of data is transmitted bit by bit on the SDA with the help of the SCL serial clock. |
Chip overview
There are differences in design principles and interface packaging in I2C module between chips, classified as follows:
| Classification | Chips |
|---|---|
| Category 1 | B91/B92/TL751x/TL721x/TL321x/TL322x/TL323x |
| Category 2 | B85/B87/B80/B80B/TC321x/TC123x |
Note
B91 includes TLSR951x and TLSR921x, B92 includes TLSR952x and TLSR922x, B80 includes TLSR8208A/B/C/D and TLSR8373E/F, B80B includes TLSR8208E/F/G/H/J and TLSR8373A/B/C/FBR, B85 includes TLSR825x and TLSR8359, B87 includes TLSR827x and TLSR8355.
I2C in Category 1
| Chips | I2C module | Master/slave mode | DMA | Stretch |
|---|---|---|---|---|
| B91 | 1-channel I2C | Master + Slave | Support | Limited support |
| B92/TL751x/TL721x/TL321x | One i2c + one i2c1_m | i2c: master + slave, i2c1_m: master only | Support | Support |
| TL322x/TL323x | Dual I2C | i2c: Master + Slave | Support | Support |
I2C characteristics
B91
- GPIO pin selection is used for SDA/SCL
- Non-DMA and DMA data transmission modes
- Basic stretch functionality is supported
- DMA transmission length: Configured according to the negotiated length
Other chips with I2C characteristics
- GPIO pin selection is used for SDA/SCL
- Non-DMA and DMA data transmission modes
- Enhanced stretch functionality and interrupt support
- NACK detection during the ID and data phases
- DMA write_num function, used to report the length of received data.
- Maximum DMA transfer length: 0xFFFFFC bytes (B92), any length (except for other B92 chips)
Interrupt
I2C interrupts are used to notify the MCU of various communication events, including FIFO trigger, frame transfer completion, NACK detection, stretch status, and more. Different chips (B91, other chips) support different types of interrupts. This document details the trigger conditions and clearing methods for each interrupt.
Types of interrupts and trigger conditions
- B91 interrupts
| Interrupt | Trigger conditions | Automatic/manual clearing |
|---|---|---|
| I2C_TXDONE_STATUS | Triggered when a stop signal is detected | Manual clearing is required |
| I2C_TX_BUF_STATUS | TX FIFO Count < = tx_irq_trig_lev triggers | Automatically clearing |
| I2C_RXDONE_STATUS | Triggered when a stop signal is detected | Manual clearing is required |
| I2C_RX_BUF_STATUS | RX FIFO count > = i2c_rx_irq_trig_cnt triggered | Automatically clears after reading data |
- Other chip interrupts
| Interrupt | Trigger conditions | Automatic/manual clearing |
|---|---|---|
| I2C_SLAVE_WR_STATUS | Triggered when the device parses commands to the master device read/write commands | Manual clearing is required |
| I2C_MASTER_NAK_STATUS | Triggered when the main device detects NACK | Manual clearing is required |
| I2C_RX_BUF_STATUS | Triggers when RX FIFO count > = FLD_I2C_RX_IRQ_TRIG_LEV | Automatically clear, but manual clearing will reset the RX FIFO pointer |
| I2C_TX_BUF_STATUS | Triggers when TX FIFO count < = FLD_I2C_TX_IRQ_TRIG_LEV | Automatically clearing |
| I2C_RX_DONE_STATUS | Triggered when a stop signal is detected | Manual clearing is required |
| I2C_TX_DONE_STATUS | Triggered when a stop signal is detected | Manual clearing is required |
| I2C_RX_END_STATUS | Triggered when a frame of data is received (a stop signal has been sent) | Manual clearing is required |
| I2C_TX_END_STATUS | Triggered after sending a frame of data (stop signal sent) | Manual clearing is required |
| I2C_STRETCH_STATUS | With the Stretch function, triggers when TX FIFO is empty or RX FIFO is full | Manual clearing is required |
STRETCH function
Master: By default, Stretch is enabled. When SCL is pulled down, the master's hardware state machine is in the current state. Only after the SCL slave is released will the master's hardware state machine continue execution.
Slave end: Enables stretch functionality by calling the i2c_slave_stretch_en() API. When the slave rxfifo is full or txfifo is empty, SCL is triggered to pull down; if the trigger conditions are not met, SCL is automatically released.
Note
- For B91 chip, during the sending process of the i2c slave end, if stretch is enabled, once the i2c slave ends and the stretch condition is met, it remains held continuously. Manually closing after sending releases and restores it; Chips after B91 have no issues. When sending the i2c slave, there is a length check, and once the length is reached, the stretch is released.
- B91 Solution: The stretch function is always enabled. After configuring tx_dma, turn it off, then check the 'tx done' status on the I2C Slave end at the enabled area. If set to 1, enable it.
GPIO pin configuration
Pin options
void i2c_set_pin(gpio_func_pin_e sda_pin, gpio_func_pin_e scl_pin);
Pin configuration automatically completes the following operations:
- Enable input for SDA and SCL
- Configure a 10Kohm pull-up resistor
- Set mux function to I2C
- Disable GPIO function
I2C operating mode
The Telink I2C driver supports both master and slave operating modes, and supports both non-DMA (NDMA) and DMA data transmission modes. For the internal design mechanism of nodma/dma, refer to the Datasheet I2C chapter.
- I2C Master
a. Initialization and Configuration
NDMA/DMA General Initialization:
// 1. Initialize the SDA/SCL pins
i2c_set_pin(sda_pin, scl_pin);
// 2. Configure the I2C clock frequency
i2c_set_master_clk((unsigned char)(sys_clk.pclk * 1000 * 1000 / (4 * 400000)));
// 3. Enable the master device functions
i2c_master_init();
DMA Initialization:
//Configure the TX/RX DMA channels
i2c_set_tx_dma_config(DMA1);
i2c_set_rx_dma_config(DMA0);
b. Interrupt Configuration
| Mode | B91 interrupt configuration | Other chips interrupt configuration |
|---|---|---|
| NDMA writes | 'Polling method, no interrupt configuration required | 'Polling method, no interrupt configuration required |
| NDMA reads | 'Polling method, no interrupt configuration required | 'Polling method, no interrupt configuration required |
| DMA writes | No interrupt configuration required | i2c_set_irq_mask(I2C_MASTER_NAK_MASK) |
| DMA reads | No interrupt configuration required | i2c_set_irq_mask(I2C_MASTER_NAK_MASK) |
c. Interrupt Response (DMA NACK)
if (i2c_get_irq_status(I2C_MASTER_NAK_STATUS)) {
i2c_clr_irq_status(I2C_MASTER_NAK_STATUS);
reg_i2c_sct1 = FLD_I2C_LS_STOP;
while (i2c_master_busy());
dma_chn_dis(I2C_TX_DMA_CHN);
if (I2C_MASTER_WRITE == i2c_get_master_wr_status()) {
i2c_clr_irq_status(I2C_TX_BUF_STATUS);
}
}
d. Data Transmission and Reception
NDMA Mode:
//Main Device Writing (Polling)
unsigned char ret = i2c_master_write(0x5a, tx_data, len);
//ret = 0: ID or data stage receives NACK
//ret = 1: Write successfully
//ret = DRV_API_TIMEOUT: Timeout returns
//Master device reading (polling)
unsigned char ret = i2c_master_read(0x5a, rx_data, len);
DMA Mode:
//Master device DMA writes
i2c_master_write_dma(0x5a, tx_data, len);
while (i2c_master_busy());
//Master device DMA reads
i2c_master_read_dma(0x5a, rx_data, len);
while (i2c_master_busy());
Note
- B91: Only the ID detection phase; if NCA is detected, a stop signal is sent and the current operation is terminated.
- Other chips: Supports NACK detection at both the ID and data stages in both NDMA and DMA modes. If NACK is detected, a stop signal is sent and the current operation is suspended.
- Slave Mode Initialization
a. Slave NDMA mode
In non-DMA mode from slave devices, the MCU reads and writes data byte by byte directly through I2C data registers. When receiving data, an interrupt method is usually used, with the RX FIFO triggering level interrupt and frame-end interrupt working together to complete the reception of a single frame.
Initialization and configuration:
// 1. Initialize the SDA/SCL pins
i2c_set_pin(sda_pin, scl_pin);
// 2. Enable slave device functions and set IDs
i2c_slave_init(0x5a);
Interrupt configuration:
Slave write (Master Reads Slave Device)
| Chips | Stretch | Interrupt configuration |
|---|---|---|
| B91 | No supported | No related interrupts, data must be filled in in advance |
| Other chips | Not used | No related interrupts, data must be filled in in advance |
| Other chips | Used | i2c_set_irq_mask(I2C_SLAVE_WR_MASK) |
Read from Slave device (Master writes to slave device)
| Chips | Interrupt configuration |
|---|---|
| B91 | i2c_rx_irq_trig_cnt() + i2c_set_irq_mask(I2C_RX_BUF_MASK \| I2C_RX_DONE_MASK) |
| Other chips | i2c_rx_irq_trig_cnt() + i2c_set_irq_mask(I2C_RX_BUF_MASK | I2C_RX_END_MASK) |
Data transmission and reception:
Slave writes (Responds to Master Device Read)
Not using Stretch:
//Data must be pre-filled before the master device sends the read command
i2c_slave_write(tx_data, len);
//FIFO is only 8 bytes long and requires advance preparation
Using Stretch (other chips):
//Fill the data in I2C_SLAVE_WR_MASK interrupts
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_SLAVE_WR_STATUS)) {
i2c_clr_irq_status(I2C_SLAVE_WR_STATUS);
if (I2C_SLAVE_WRITE == i2c_slave_get_cmd()) {
i2c_slave_write(tx_data, len);
}
}
}
Key points for using Stretch: When a slave device uses the Stretch feature, use I2C_SLAVE_WR_MASK interrupts to determine when the master device reads data and then fill in the data. When the slave device does not use the Stretch function, if the I2C_SLAVE_WR_MASK interrupt is used to determine the master device's read timing, the MCU may not have time to handle it, so data must be pre-filled before the master device reads.
Read from Slave device (Response to Master Device Writes)
B91 Interrupt Handling:
#define SLAVE_RX_IRQ_TRIG_LEVEL 4
i2c_rx_irq_trig_cnt(SLAVE_RX_IRQ_TRIG_LEVEL);
i2c_set_irq_mask(I2C_RX_BUF_MASK | I2C_RX_DONE_MASK);
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_RX_BUF_STATUS)) {
i2c_slave_read(rx_buff + offset, SLAVE_RX_IRQ_TRIG_LEVEL);
offset += SLAVE_RX_IRQ_TRIG_LEVEL;
}
if (i2c_get_irq_status(I2C_RXDONE_STATUS)) {
if (remaining_len > 0) {
i2c_slave_read(rx_buff + offset, remaining_len);
}
i2c_clr_fifo(I2C_RX_BUFF_CLR);
rx_done_flag = 1;
}
}
Other chip interrupt handling:
#define SLAVE_RX_IRQ_TRIG_LEVEL 4
i2c_rx_irq_trig_cnt(SLAVE_RX_IRQ_TRIG_LEVEL);
i2c_set_irq_mask(I2C_RX_BUF_MASK | I2C_RX_END_MASK);
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_RX_BUF_STATUS)) {
i2c_slave_read(rx_buff + offset, SLAVE_RX_IRQ_TRIG_LEVEL);
offset += SLAVE_RX_IRQ_TRIG_LEVEL;
}
if (i2c_get_irq_status(I2C_RX_END_STATUS)) {
i2c_clr_irq_status(I2C_RX_END_STATUS);
if (i2c_get_rx_buf_cnt() > 0) {
i2c_slave_read(rx_buff + offset, i2c_get_rx_buf_cnt());
}
offset = 0;
rx_done_flag = 1;
}
}
Data transmission and reception (Precautions):
- When writing from the B91 slave device, the FIFO size is only 8 bytes, requiring data to be pre-filled before the master device reads.
- Other chips that do not use Stretch also need to pre-fill data; When using Stretch, you can dynamically fill
I2C_SLAVE_WR_MASKinterrupts. i2c_rx_irq_trig_cnt()is recommended to be set to 1 or 4, with a range less than 8.
b. Slave DMA mode
In Device DMA mode, the DMA controller automatically transfers data from memory to the I2C TX FIFO (responds to the master device read) or from the I2C RX FIFO to memory (responds to the master device write).
Initialization and configuration:
// 1. Initialize the SDA/SCL pins
i2c_set_pin(sda_pin, scl_pin);
// 2. Enable slave device functions and set IDs
i2c_slave_init(0x5a);
// 3. Configure DMA channels
i2c_set_tx_dma_config(DMA1);
i2c_set_rx_dma_config(DMA0);
Interrupt configuration:
Slave device DMA write (Response to Master Device Read)
| Chips | Stretch | Interrupt configuration |
|---|---|---|
| B91 | No supported | i2c_set_irq_mask (I2C_TX_DONE_MASK) (Clear before use I2C_TX_DONE_CLR) |
| Other chips | Not used | i2c_set_irq_mask(I2C_TX_END_MASK) |
| Other chips | Used | i2c_set_irq_mask(I2C_SLAVE_WR_MASK) |
Slave device DMA read (Response to Master Device Write)
| Chips | Interrupt configuration |
|---|---|
| B91 | dma_set_irq_mask(TC_MASK) |
| Other chips | dma_set_irq_mask (TC_MASK) or i2c_set_irq_mask (I2C_RX_END_MASK). |
Data Transmission and Reception:
Slave Device DMA write (Response to Master Device Read)
B91:
//Clear the TX_DONE state during initialization
i2c_clr_irq_status(I2C_TX_DONE_CLR);
i2c_set_irq_mask(I2C_TX_DONE_MASK);
//Configure DMA to write data
i2c_slave_set_tx_dma(tx_data, len);
//Interrupt handling
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_TXDONE_STATUS)) {
i2c_clr_irq_status(I2C_TX_DONE_CLR);
//Configure the next TX DMA
}
}
Other chips use Stretch:
i2c_slave_stretch_en();
i2c_set_irq_mask(I2C_SLAVE_WR_MASK);
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_SLAVE_WR_STATUS)) {
i2c_clr_irq_status(I2C_SLAVE_WR_STATUS);
if (I2C_SLAVE_WRITE == i2c_slave_get_cmd()) {
i2c_slave_set_tx_dma(tx_data, len);
}
}
}
Read from Slave device DMA (Respond to Master Device Write)
B91:
//Configure DMA to read data
i2c_slave_set_rx_dma(rx_data, len);
//Completed through DMA TC interrupt judgment
dma_set_irq_mask(TC_MASK);
//Interrupt handling
void dma_irq_handler(void)
{
if (dma_get_tc_irq_status(I2C_RX_DMA_STATUS)) {
dma_clr_tc_irq_status(I2C_RX_DMA_STATUS);
//Configure the next RX DMA
}
}
Other chips
//Configure DMA to read data
i2c_slave_set_rx_dma(rx_data + 4, DMA_REV_LEN);
//Method 1: Via DMA TC interrupt
dma_set_irq_mask(TC_MASK);
//Method 2: Via I2C_RX_END_MASK interrupt
i2c_set_irq_mask(I2C_RX_END_MASK);
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_RX_END_STATUS)) {
i2c_clr_irq_status(I2C_RX_END_STATUS);
//Configure the next RX DMA
i2c_slave_set_rx_dma(rx_data + 4, DMA_REV_LEN);
}
}
Note
- The buff data buffer must be aligned by word (4 bytes).
- Before using the
I2C_TX_DONE_MASKon the B91 chips, manually clearthe I2C_TX_DONE_CLR, otherwise it keeps interrupting. I2C_TX_DONE_MASKOnly indicates that the data part has been sent, excluding the stop signal; Other chips are recommended to useI2C_TX_END_MASK- Except for the B91 chip, the DMA read time received is written to the first 4 bytes of the buffer. The read buffer size is
BUFF_DATA_LEN_DMA + 4, with the first 4 bytes storing the actual receive length.
- I2C1_M Module (Master mode only, NDMA only)
a. Initialization and Configuration
// 1. Initialize the SDA/SCL pins
i2c1_m_set_pin(sda_pin, scl_pin);
// 2. Configure the I2C clock frequency
i2c1_m_set_master_clk((unsigned char)(sys_clk.pclk * 1000 * 1000 / (4 * 100000)));
// 3. Enable the master device functions
i2c1_m_master_init();
b. Data Transmission and Reception
//i2c1_m Write (can include an address, put the address into txbuff and send it together as data)
i2c1_m_master_write(0x5a, tx_data, len);
//i2c1_m Read
i2c1_m_master_read(0x5a, rx_data, len);
//i2c1_m Read after writing
i2c1_m_master_write_read(0x5a, wr_data, wr_len, rd_data, rd_len);
DEMO Description
- Hardware connection
When testing I2C communication with two boards:
| Master Device | Slave Device |
|---|---|
| SCL | SCL |
| SDA | SDA |
| GND | GND |
Important:
- Power on slave devices first, then power on master devices (to avoid data errors)
-
There must be a common-ground connection between the two boards.
-
NODMA
a. Configure Macros
#define I2C_MASTER_DEVICE 1 // i2c master demo
#define I2C_SLAVE_DEVICE 2 // i2c slave demo
#define I2C_DEVICE I2C_SLAVE_DEVICE // Select the master or slave device
#if !defined(MCU_CORE_B91)
#define I2C_STRETCH_EN 0
#define I2C_STRETCH_DIS 1
#define I2C_STRETCH_MODE I2C_STRETCH_EN // Select Stretch mode
#endif
#define I2C_CLK_SPEED 400,000 // I2C clock 400K
#define SLAVE_RX_IRQ_TRIG_LEVEL 4 // Slave device RX interrupt trigger level
#define BUFF_DATA_LEN_NO_DMA 32 // data buffer length
b. Test Description
Master Device
( I2C_DEVICE I2C_MASTER_DEVICE)
Data Flow:
Burn the bin file to the master device
The master writes data to the slave and then reads the data back
Compare whether the data written and read back are consistent
Slave Device
(I2C_DEVICE I2C_SLAVE_DEVICE)
Data Flow:
Burn the bin file to the slave device
Do not use Stretch(I2C_STRETCH_MODE I2C_STRETCH_DIS):
Receive data from the master device and then write the received data back to the master device
Using Stretch (except for B91 chips) (I2C_STRETCH_MODE I2C_STRETCH_EN):
Another communication mechanism with the master device is to receive data from the device and write it back to the master device.
- DMA
a. Configure Macros
#define I2C_TX_DMA_CHN DMA1
#define I2C_RX_DMA_CHN DMA0
#define I2C_MASTER_DEVICE 1
#define I2C_SLAVE_DEVICE 2
#define I2C_DEVICE I2C_MASTER_DEVICE
#if (I2C_DEVICE == I2C_MASTER_DEVICE)
#define I2C_CLK_SPEED 200,000 // I2C clock 200K
#define BUFF_DATA_LEN_DMA 32
##elif (I2C_DEVICE == I2C_SLAVE_DEVICE)
#if !defined(MCU_CORE_B91)
#define I2C_STRETCH_EN 1
#define I2C_STRETCH_DIS 2
#define I2C_STRETCH_MODE I2C_STRETCH_EN
#endif
#define BUFF_DATA_LEN_DMA 32
//B91: DMA_REV_LEN Matches the transmission length
//B92: DMA_REV_LEN can be set to maximum value 0xFFFFFC and enable write_num
//Chips other than B91/B92: DMA_REV_LEN is any 4-byte alignment length
#if defined(MCU_CORE_B91)
#define DMA_REV_LEN BUFF_DATA_LEN_DMA
#elif defined(MCU_CORE_B92)
#define DMA_REV_LEN 0xFFFFFC
#else
#define DMA_REV_LEN 32
#endif
#endif
b. Test Instructions
Master Device Data Stream:
( I2C_DEVICE I2C_MASTER_DEVICE)
- Burn the bin file to the master device.
- The master writes data to the slave and then reads the data back.
- Compare whether the data written and read back are consistent.
Slave Device data stream:
(I2C_DEVICE I2C_SLAVE_DEVICE)
- Dot using Stretch (I2C_STRETCH_MODE I2C_STRETCH_DIS): Receive data from the master device and then write the received data back to the master device (pre-configure rxdma, configure tx dma in rx dma, configure rxdma in tx interrupt; demo alternates configuration like this)
-
Use Stretch (except for B91 chips) (I2C_STRETCH_MODE I2C_STRETCH_EN): After receiving data from the device, write it back to the master device.
-
Test results
Using a logic analyzer to capture timing:
Channel 0: SCL signal
Channel 1: SDA signal
Channel 2: LED2 (error indicator)
Master device sends data:

Master device receives data:

Special Note
- During I2C write operations, the slave responds to ACK after the master device writes the last byte, and then the master sends a stop signal to end the communication.
I2C module in Category 2
The 8 Series (B85m) I2C module differs significantly from the 9 Series: the 8 Series does not have a true DMA transmission mode. Data transmission on the master side is handled by sequential read/write registers on the MCU, while the slave side is automatically parsed by hardware on the bus, so the MCU does not need to participate byte by byte.
Chip Overview and Features
Main features:
- GPIO pins can be configured for SDA/SCL, with internal pins enabling input and pull-up 10Kohm.
- Supports both Master and Slave modes
- The Slave side supports two data organization modes: MAPPING mode and DMA mode (Note: DMA mode names are historical and are not actual DMA transmissions; see the Slave mode operation description for details).
- Master data transmission is fully controlled sequentially by the MCU (no DMA)
- Data transmission and reception on the Slave end are automatically completed by hardware; the MCU only needs to detect read/write events during interrupts.
- Supports Slave ID phase interrupts (triggered when master device read/write commands arrive)
- Supports single-byte, double-byte, and three-byte address access (AddrLen 0/1/2/3)
- Maximum Speed: Hardware tests can stably support up to 1 Mbps (higher speeds depend on hardware environment, no further verification conducted)
Interrupt
The 8 Series I2C only generates interrupts at the slave end, which notify the MCU master device of read/write operations initiated by it. The types of interrupts are as follows:
| Interrupt | Trigger conditions | Clearing methods |
|---|---|---|
| HOST_CMD_IRQ | Triggered when the master device initiates a read or write command (ID resolution complete). | Manually clear the write status registers |
| HOST_READ_IRQ | Triggered only when the master device initiates a read command | Manually clear the write status registers |
Explanation:
- Both read/write events trigger
HOST_CMD_IRQ, and read events setHOST_READ_IRQ, so during interrupts, read or write can be distinguished byHOST_READ_IRQ. - Both status bits must be cleared together after processing (write register value to clear).
- To enable interrupts, open the I2C hybrid command interrupt via
irq_set_mask(FLD_IRQ_MIX_CMD_EN) and then callirq_enable().
Related Interfaces:
//Obtain interrupt status
unsigned char i2c_get_interrupt_status(i2c_irq_e irq_status);
//Clear the interrupt status
void i2c_clear_interrupt_status(i2c_irq_e irq_status);
GPIO pin configuration
8 Series uses i2c_gpio_set() to configure SDA/SCL pins, with the function completing it internally:
- Enable SDA / SCL pin inputs
- Configure a 10Kohm pull-up resistor
- Set the mux function to I2C
- Disable the corresponding GPIO function
void i2c_gpio_set(GPIO_PinTypeDef sda_pin, GPIO_PinTypeDef scl_pin);
The B85 uses a single-parameter i2c_gpio_set (I2C_GPIO_SDA_SCL) format, while other chips use a dual-parameter form.
I2C operating mode
Working Modes: MAPPING mode and DMA mode. The working mechanism is introduced as follows:
I2C_MAPPING_MODE:
Slave: Defines a receiving buff and writes the buff's address to the register;
Master: No address information is needed when sending data; the slave side stores the data sent by the master in a defined buff address
When the Master side reads data, it does not need to send address information; the slave side sends data from the defined Buff address
This mode has a usage requirement: the buff defined on the slave side must be 128 bytes aligned, and the 128 addresses can be written freely, but when reading, only the last 64 bytes can be read:
128 bytes aligned

Therefore, to ensure that what is written and read are consistent, the address information passed to the slave register is:
i2c_slave_init(0x5C,I2C_SLAVE_MAP,(unsignedchar*)i2c_slave_mapping_buff+64); Store the buff address of the i2c slave into the following register. When the master side reads or writes, the slave store or read data from the address information stored in this register:
Note
- Although the master side can only read the last 64 bytes, the slave side can freely access these 128 bytes.
I2C_DMA_MODE: Although it is called DMA mode, it is actually not DMA transmission.
- Slave: No need to define receiving buffs
-
Master: When sending and receiving, the master needs to send address information, then the slave side parses the address sent from the master, and the received data is stored at that address, or the contents of that address are sent to the master.
-
Master mode
Initialization and Configuration:
// 1. Configure SDA/SCL pins
i2c_gpio_set(I2C_GPIO_SDA, I2C_GPIO_SCL);
// 2. Master device initialization: Set the Slave ID and clock frequency division
//The lowest bit of a slaveID is the read/write bit (R:High W:Low), for example, 0x5C is written as 0x5C and read as 0x5D
// DivClock = CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)
i2c_master_init(0x5C, (unsigned char)(CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)));
Data transceiver interface (MCU sequential control, non-DMA):
| Interface | Purpose |
|---|---|
i2c_write_series(Addr, AddrLen, dataBuf, dataLen) |
Write a piece of data to a specified address |
i2c_read_series(Addr, AddrLen, dataBuf, dataLen) |
Read a piece of data from a specified address |
Parameter Description:
Addr: The internal register/SRAM address of the slave corresponds to the address phase in the master sequenceAddrLen: Address length, 0/1/2/3 bytes- 0: MAPPING mode does not require a sending address
- 1/2/3: Send 1/2/3 byte addresses
dataBuf: Data bufferdataLen: Data length (bytes)
Write sequence
start + device_id(W) + [addr(0/1/2/3 byte)] + data(1..n byte) + stop
Read sequence
start + device_id(W) + [addr(0/1/2/3 byte)] + restart + device_id(R) + data(1..n byte) + stop
Example of master usage in MAPPING mode:
//In MAPPING mode, AddrLen=0 is not required, no sending address required
i2c_write_series(0, 0, (unsigned char *)i2c_tx_buff, BUFF_DATA_LEN);
i2c_read_series(0, 0, (unsigned char *)i2c_rx_buff, BUFF_DATA_LEN);
Example of master usage in DMA mode:
//Needs to send the internal SRAM address of the slave (3 bytes)
#define SLAVE_DEVICE_ADDR 0x48000
#define SLAVE_DEVICE_ADDR_LEN 3
i2c_write_series(SLAVE_DEVICE_ADDR, SLAVE_DEVICE_ADDR_LEN, (unsigned char *)i2c_tx_buff, BUFF_DATA_LEN);
i2c_read_series(SLAVE_DEVICE_ADDR, SLAVE_DEVICE_ADDR_LEN, (unsigned char *)i2c_rx_buff, BUFF_DATA_LEN);
- Slave mode
a. MAPPING mode
Principle:
- The slave end defines a 128-byte aligned buffer in SRAM and writes this buffer address to the hardware register:
- buffer must be aligned at 128 bytes
- Within 128 bytes, the master can be written arbitrarily, but only the last 64 bytes of the master can be read.
- To ensure the master writes and reads the same data, the address passed in during slave initialization must be offset by +64:
In MAPPING mode, data on the slave side is automatically transferred by hardware, without the need for MCU byte processing.
b. SDMA mode
Explanation:
The "DMA mode" here is a historical name, is not actually a true DMA transmission. Essentially, the slave side automatically parses the data storage location based on the address field sent by the master (any SRAM address, address 3 bytes).
i2c_gpio_set(I2C_GPIO_SDA, I2C_GPIO_SCL);
//slave initialization: DMA mode, no buffer input required (pMapBuf passes 0)
i2c_slave_init(0x5C, I2C_SLAVE_DMA, 0);
Note
- The master side must send a 3-byte address (AddrLen=3) to specify the target location in the slave SRAM.
- The slave side does not need to define a receive buffer; hardware directly parses addresses and stores/reads SRAM data.
Demo explanation
Demo path: demo/vendor/I2C_Demo, switch working modes using the I2C_MODE macro in app_config.h:
#define I2C_DMA_MODE 1
#define I2C_MAPPING_MODE 2
#define I2C_MODE 1 // Choose 1 (DMA) or 2 (MAPPING)
And use I2C_DEVICE macros to choose whether to compile as master or slave:
#define I2C_MASTER_DEVICE 1
#define I2C_SLAVE_DEVICE 2
#define I2C_DEVICE I2C_MASTER_DEVICE
(1) Hardware connection
| Master Device | Slave Device |
|---|---|
| SCL | SCL |
| SDA | SDA |
| GND | GND |
Important:
Power on the slave device first, then power on the master device (to avoid data errors); The two boards must be on the same ground.
(2) MAPPING mode test instructions
- Master Data Stream: Programme the master firmware → periodically writes data to the slave and then reads back → to compare whether the write and read data are consistent (the demo uses
i2c_tx_buff[0]++to auto-increment checks). - Slave Data Stream: Programme the slave firmware → receiving master device data and storing it in
i2c_slave_mapping_buff→ When the master device reads, the slave automatically returns data from the buffer. - Interrupts are only used to count read/write counts and flip LED indicators; data handling is entirely hardware-controlled.
(3) DMA Mode Test Instructions
- Master Data Stream: Programme master firmware → accessing the slave SRAM via
i2c_write_series/i2c_read_series0x48000 starting at 16 bytes→ periodically writes and reads back for comparison. - Slave Data Stream: Records the master's read/write counts during the → interrupt of the slave firmware and flips the LED. The data storage location is specified by the master at the address stage.
UART
UART Hardware Architecture
Overview of UART
UART (Universal Asynchronous Receiver/Transmitter) is an asynchronous, full-duplex serial communication protocol consisting of two data lines: Tx and Rx. Since there is no dedicated clock reference signal, both communicating parties must agree on the baud rate, data width, parity, and stop bits to operate at the same data rate.
The UART module provides the following capabilities:
| Feature | Description |
|---|---|
| Operating Mode | NDMA / DMA / DMA LLP |
| Hardware Flow | CTS (Clear to Send) / RTS (Request to Send) |
| FIFO Depth | 8 bytes each for TX FIFO and RX FIFO |
| Data Width | 8 bits |
| Stop Bits | 1 / 1.5 / 2 |
| Parity | None / Even / Odd |
| Single-Wire Mode | RTX pin (shared TX/RX on a single pin, used for S7816) |
Data Communication Timing

UART asynchronous communication transmits data one character (frame) at a time. Each character consists of:
| Bit Segment | Description |
|---|---|
| Start Bit | Logic 0, active low, marks the beginning of a character |
| Data Bits | 8 bits of logic 0 or 1, transmitted LSB-first |
| Parity Bit | Optional; makes the total number of 1 bits even or odd |
| Stop Bit(s) | Logic 1, 1 / 1.5 / 2 bits; longer stop bits improve tolerance |
| Idle | Logic 1, indicates the line is free |
Communication Principle

Taking the UART module in a Telink SoC as an example:
- Transmit path: Data is written by the MCU or DMA to the TX buffer (TX FIFO); the UART module serializes and sends the data over the TX pin to the peer device.
- Receive path: The peer device sends data that arrives on the RX pin; data is written into the RX buffer (RX FIFO), then read by the MCU or DMA.
Hardware flow control mechanism:
- When the chip's RX buffer is close to overflowing, it asserts the RTS pin to signal the peer to stop transmitting.
- When the chip detects an active signal on the CTS pin, it means the peer's RX buffer is close to overflowing, and the chip must stop transmitting.
Wiring note: Connect the local TX to the peer's RX, and the local RX to the peer's TX: TX <-> RX, RX <-> TX.
Chips Supported
Due to certain differences in chip design, there are various corresponding interface packaging configurations, which are classified as follows:
| Category | Chips |
|---|---|
| Category 1 | B91 / B92 / TL751x / TL721x / TL321x / TL322x / TL323x |
| Category 2 | B85 / B87 / B80 / B80B / TC321x / TC123x |
Note
- B91 includes TLSR951x and TLSR921x, B92 includes TLSR952x and TLSR922x, B80 includes TLSR8208A/B/C/D and TLSR8373E/F, B80B includes TLSR8208E/F/G/H/J and TLSR8373A/B/C/FBR, B85 includes TLSR825x and TLSR8359, B87 includes TLSR827x and TLSR8355.
UART Timeout Mechanism
| Chip series | Timeout Mechanism Calculation |
|---|---|
| Category 1 (B91 / B92), Category 2 | rx_timeout = ((bwpc+1) * bit_cnt) * mul |
| Category 1 (chips other than B91/B92) | rx_timeout = (((bwpc+1) * bit_cnt) * mul) * 2^n (n<=255) |
in which, the maximum of (bwpc+1) * bit_cnt can be set to 0xff.
The timeout period is calculated starting from the current byte. If no subsequent byte is received by the time the configured timeout period expires, the packet is considered fully received, and a timeout interrupt is generated.
RX Timeout Settings:
uart_set_rx_timeout(uart_num, bwpc, bit_cnt, mul);
bwpc: bit width, calculated fromuart_cal_div_and_bwpc().bit_cnt: The total number of bits required to transmit one byte (e.g., 1 start bit + 8 data bits + 1 parity bit + 2 stop bits = 12)mul: Timeout multiplier:UART_BW_MUL1= 1 byte time,UART_BW_MUL2= 2 byte times
Timeout = (1 / baud rate) × bit_cnt × mul. If no data is received within this time, a UART_RXDONE interrupt is generated.
Note
- When should the timeout value be adjusted? If a client sends a single transaction but there are time intervals within that transaction, and you want those intervals to be treated as part of the same transaction, you need to increase the timeout value. However, increasing the timeout value may cause two separate transactions to be treated as one, so the specific value must be determined based on the application scenario.
Work Mechanism of Internal FIFO
The UART FIFO has a depth of 8 bytes, and its status is reflected by the following counters:
| Counter | Direction | Description | Enquiry API |
|---|---|---|---|
rx_bufcnt |
RX | Increment by 1 for every byte received; decrement by 1 for every byte read | uart_get_rxfifo_num() |
tx_bufcnt |
TX | Increment by 1 for every byte written; decrement by 1 for every byte sent | uart_get_txfifo_num() |
rbcnt |
RX | Increment by 1 for every 1 byte read (FLD_UART_RBCNT) |
- |
wbcnt |
TX | Increment by 1 for every 1 byte written (FLD_UART_WBCNT) |
- |
UART in Category 1
Chip Difference
Different chips provide different quantity of UART; when using them, you must select the corresponding UART number (uart_num_e) based on the chip model:
| Chip series | UART Quantity | Available Numbers |
|---|---|---|
| B91 / B92 | 2 | UART0、UART1 |
| TL721x / TL321x | 3 | UART0、UART1、UART2 |
| TL7518 / TL751x | 4 | UART0 ~ UART3 |
| TL322x / TL323x | 5 | UART0 ~ UART4 |
UART Interrupt
(1) Overview of UART Interrupt
| Interrupt name | Assigned module | Trigger conditions | Clearing methods | Applicable modes |
|---|---|---|---|---|
UART_TXDONE |
UART | TX FIFO is empty, indicating that one frame of data is sent completely | Manual clearing | NDMA / DMA |
UART_RXDONE |
UART | If the RX timeout and no data is received, it means the frame of data has been received | Manual clearing | NDMA |
UART_RXBUF_IRQ_STATUS |
UART | Data volume in RX FIFOs ≥uart_rx_irq_trig_level sets thresholds |
Automatically clearing | NDMA |
UART_TXBUF_IRQ_STATUS |
UART | Data volume in TX FIFOs ≤uart_tx_irq_trig_level sets thresholds |
Automatically clearing | NDMA |
UART_RX_ERR |
UART | Received data errors (parity errors, stop bit errors, etc.) | Manual clearing | NDMA / DMA |
DMA TC(RX) |
DMA | DMA RX received length reaches the configured length | Manual clearing | DMA / DMA LLP |
(2) Interrupt mask configuration
Mask configuration API for each interrupt:
//NDMA mode: RX interrupt + error interrupt (UART module interrupt)
uart_set_irq_mask(UART0, UART_RX_IRQ_MASK | UART_ERR_IRQ_MASK);
//DMA Mode: TX_DONE + RX_DONE (UART module interrupted)
uart_set_irq_mask(UART0, UART_TXDONE_MASK | UART_RXDONE_MASK);
//DMA Mode: Use DMA TC interrupts (alternative to RX_DONE, which is a DMA module interrupt)
dma_set_irq_mask(UART_DMA_CHANNEL_RX, TC_MASK);
//DMA LLP Mode: Use DMA TC interrupts
dma_set_llp_irq_mode(UART_DMA_CHANNEL_RX, DMA_INTERRUPT_MODE);
dma_set_irq_mask(UART_DMA_CHANNEL_RX, TC_MASK);
Interrupt mask enumeration (uart_irq_mask_e):
| Mask name | Description |
|---|---|
UART_RX_IRQ_MASK |
Enable RXBUF_IRQ interrupt |
UART_TX_IRQ_MASK |
Enable TXBUF_IRQ interrupt |
UART_RXDONE_MASK |
Enable RXDONE interrupt |
UART_TXDONE_MASK |
Enable TXDONE interrupt |
UART_ERR_IRQ_MASK |
Enable RX_ERR interrupt |
(3) Interrupt status query and clear
B91 Enumeration of interrupt states (uart_irq_status_e):
| State name | Inquiry method | Clearing methods |
|---|---|---|
UART_TXDONE |
uart_get_irq_status(uart_num, UART_TXDONE) |
uart_clr_tx_done(uart_num) |
UART_RX_ERR |
uart_get_irq_status(uart_num, UART_RX_ERR) |
uart_clr_irq_status(uart_num, UART_CLR_RX) |
UART_RXBUF_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_RXBUF_IRQ_STATUS) |
Automatically clears after reading FIFO |
UART_TXBUF_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_TXBUF_IRQ_STATUS) |
Automatically clearing |
B92 and subsequent chip interrupt state enumeration (uart_irq_status_e):
| State name | Inquiry method | Clearing methods |
|---|---|---|
UART_TXDONE_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_TXDONE_IRQ_STATUS) |
uart_clr_irq_status(uart_num, UART_TXDONE_IRQ_STATUS) |
UART_RXDONE_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_RXDONE_IRQ_STATUS) |
uart_clr_irq_status(uart_num, UART_RXDONE_IRQ_STATUS) |
UART_RX_ERR |
uart_get_irq_status(uart_num, UART_RX_ERR) |
uart_clr_irq_status(uart_num, UART_RXBUF_IRQ_STATUS) |
UART_RXBUF_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_RXBUF_IRQ_STATUS) |
uart_clr_irq_status(uart_num, UART_RXBUF_IRQ_STATUS) |
UART_TXBUF_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_TXBUF_IRQ_STATUS) |
Automatically clearing |
(4) Chip differences
- UART_TXDONE: B91 has a default value of 1 and must be manually cleared during initialization; Other chips default to 0, so manual clearance is not required during initialization.
- UART_RXDONE: B91 only triggers in DMA mode, not NDMA, and only serves to clear RXDONE; Other chips support it, and when clearing RXDONE, it also serves as a clearing RXFIFO.
- UART_RXBUF_IRQ: When clearing FIFO, B91 When there is data in the FIFO, clearing the FIFO only deletes the contents of the FIFO; the pointer of the FIFO does not change; When data is in the FIFO on other chips, clearing the FIFO also clears the pointer to the FIFO's starting address. When there is no data in the FIFO, all chips clear the FIFO pointer and point to the FIFO's starting address.
Iterative optimization of UART DMA functionality
B91: The DMA write num function cannot be used and has no hardware writeback length, resulting in the only option to manually calculate the received data length during the RXDONE interrupt.
The above working mechanism can have the following scenarios: if two packets are very close together but an RXDONE interrupt occurs, but the next data entry has already arrived when processing the previous interrupt, clearing the previous interrupt status bit clears the next data entry, causing packet loss. If the customer cannot accept the lost packet, the recommended solution is that the time for both packets to exceed the interrupt processing time.
B92: Improvements have been made to address this issue, but when the write num function is enabled, the DMA receive length must be set to full ff for RXDONE to trigger DMA reception completion. This can cause buff overflow when the send length exceeds the receive length. Whether to use the B91 method or an improved approach can be evaluated based on the pros and cons of both.
Other chips (subsequent chips): write_num Enable, regardless of the DMA reception length configuration, the condition for DMA to complete the work is: either UART RXDONE or reaching the DMA configured length.
UART initialization process
(1) Hardware reset
Before using the UART port each time, first call uart_hw_fsm_reset() to reset the UART finite state machine, clear the registry status and FIFO data left from previous operations, and reset the software read/write pointer.
uart_hw_fsm_reset(UART_MODULE_SEL);
(2) Pin configuration
Use uart_set_pin() to configure the TX/RX pins of UART.
uart_set_pin(UART0_TX_PA3, UART0_RX_PA4);
Internally, the function:
- Enables pin GPIO input function
- Configures a pull-up resistor
- Switches pin function to UART multiplexing function
Note
- Different chips have different pin enumerations (e.g.,
UART0_TX_PA3,UART0_TX_PIN, etc.). For specific pins, refer to theuart_tx_pin_e/uart_rx_pin_eenumeration definitions inuart.hfor each chip.
(3) Baud rate configuration
The baud rate configuration consists of two steps:
Step 1: Calculate the frequency division parameters
unsigned short div = 0;
unsigned char bwpc = 0;
uart_cal_div_and_bwpc(115200, sys_clk.pclk * 1000 * 1000, &div, &bwpc);
uart_cal_div_and_bwpc() Based on the target baud rate and PCLK frequency, calculate the optimal clock division number div and bit width bwpc. The calculation formula is:
BaudRate × (div + 1) × (bwpc + 1) = PCLK
The BWPC range is 3-15, and the div is derived from the BWPC.
Step 2: Initialize the UART module
uart_init(UART_MODULE_SEL, div, bwpc, UART_PARITY_NONE, UART_STOP_BIT_ONE);
uart_init() Writes the frequency divider parameters to the register, and simultaneously configures the check and stop bits.
Maximum baud rate:
The theoretical maximum supported baud rate of a chip UART is 1/10 of the UART clock used; fault tolerance is shown in the datasheet; However, in actual use, due to hardware environment limitations, this baud rate may not be achieved.
Note
- When configuring the maximum baud rate, try to use DMA mode; NDMA mode may carry the risk of not having enough data to retrieve data from the FIFO. PCLK is the main factor influencing the upper limit of the UART baud rate.
Since BaudRate ×(div + 1) × (bwpc + 1) = PCLK, given the baud rate and PCLK, the constants after (div + 1) × (bwpc + 1) are constants, and there are various combinations of div and bwpc. The larger the bwpc, the finer the clock division, and the closer the actual baud rate is to the target value. For applications with high timing requirements, div and bwpc can be precomputed to directly call uart_init(), because the result of uart_cal_div_and_bwpc() calculations may not be optimal.
Working mode
The UART driver supports three operating modes, switched by UART_MODE macro:
#define UART_DMA 1 // DMA mode
#define UART_NDMA 2 // NDMA mode (no DMA)
#define UART_DMA_LLP 3 // DMA LLP (Linked List Pointer) mode
#define UART_MODE 2
(1) NDMA mode
In NDMA mode, data transmission and reception are entirely handled by the CPU through registers, without using the DMA engine.
Features:
- The CPU reads and writes directly TX/RX FIFO
- Data reception is determined by interrupts (RX_IRQ) or software timeouts (RXDONE, B92 and subsequent chip support).
- Sending can be completed by querying
uart_tx_is_busy()or TX_IRQ interrupt checks - Suitable for low-speed, low-data scenarios
Core function:
- Send:
uart_send_byte()/uart_send_hword()/uart_send_word()/uart_send(). - Receive:
uart_read_byte().
a. NDMA mode interrupt configuration
//Set the RX interrupt trigger threshold (how many characters are received to trigger one interrupt)
uart_rx_irq_trig_level(UART_MODULE_SEL, 1); Recommended set to 1
//Set TX interrupt trigger threshold (interrupt triggered when the remaining data volume in the TX FIFO reaches ≤ levels)
uart_tx_irq_trig_level(UART_MODULE_SEL, 0); 0 means an interrupt is triggered when the TX FIFO is empty
//Enable UART modules to interrupt masks
uart_set_irq_mask(UART_MODULE_SEL, UART_RX_IRQ_MASK | UART_ERR_IRQ_MASK);
//B92 and subsequent chips support RXDONE interrupt
uart_set_irq_mask(UART_MODULE_SEL, UART_RXDONE_MASK);
//Enable PLIC interrupt sources
plic_interrupt_enable(IRQ_UART0);
//Enable global interrupt
core_interrupt_enable();
Note
uart_rx_irq_trig_levelfunction, B91 can only be set to 1, B92 can only be set to 1 or 4, and other chips can be set to 1-7.
b. Receive interrupt handling
In NDMA mode, receive interrupt handling must simultaneously consider RX_ERR error interrupts, RXBUF_IRQ data interrupts, and RXDONE timeout interrupts (B92+). Below is a chip-by-chip handling method:
B91 Handling Method (No RXDONE Interrupt):
_attribute_ram_code_sec_ void uart0_irq_handler(void)
{
//RX_ERR Error interrupt: Clear RX FIFO, error flags, hardware/software pointers
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_CLR_RX); // Clear rx_fifo、rx_err_irq、rx_buff_irq
uart_hw_fsm_reset(UART_MODULE_SEL); //Reset the hardware pointer
uart_clr_rx_index(UART_MODULE_SEL); //Clear the software pointer
uart_irq_cnt = 0;
uart_rx_flag = 0;
}
//RXBUF_IRQ Data interrupt: Read data from FIFO
if (uart_get_irq_status(UART_MODULE_SEL, UART_RXBUF_IRQ_STATUS)) {
gpio_set_high_level(LED3);
if (uart_rx_flag == 0) {
unsigned char fifo_cnt = uart_get_rxfifo_num(UART_MODULE_SEL);
for (int i = 0; i < fifo_cnt; i++) {
if ((uart_irq_cnt % UART_RX_IRQ_LEN == 0) && (uart_irq_cnt != 0)) {
uart_read_byte(UART_MODULE_SEL);
} else {
uart_rx_buff_byte[uart_irq_cnt++] = uart_read_byte(UART_MODULE_SEL);
}
}
if ((uart_irq_cnt % UART_RX_IRQ_LEN == 0) && (uart_irq_cnt != 0)) {
uart_rx_flag = 1;
}
} else {
unsigned char uart_fifo_cnt = uart_get_rxfifo_num(UART_MODULE_SEL);
if (uart_fifo_cnt != 0) {
for (int j = 0; j < uart_fifo_cnt; j++) {
uart_read_byte(UART_MODULE_SEL);
}
}
}
}
}
B92 and subsequent chip processing methods (supports RXDONE interrupts):
_attribute_ram_code_sec_ void uart0_irq_handler(void)
{
//RX_ERR Error interrupt: one-step clearing (B92+ internal automatic FIFO pointer clearance)
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_RXBUF_IRQ_STATUS); //Clear rx_fifo, hardware pointer, rx_err_irq, rx_buff_irq
uart_irq_cnt = 0;
uart_rx_flag = 0;
}
//RXBUF_IRQ Data interrupt: Read data from FIFO
if (uart_get_irq_status(UART_MODULE_SEL, UART_RXBUF_IRQ_STATUS)) {
gpio_set_high_level(LED3);
if (uart_rx_flag == 0) {
unsigned char fifo_cnt = uart_get_rxfifo_num(UART_MODULE_SEL);
for (int i = 0; i < fifo_cnt; i++) {
if ((uart_irq_cnt % UART_RX_IRQ_LEN == 0) && (uart_irq_cnt != 0)) {
uart_read_byte(UART_MODULE_SEL);
} else {
uart_rx_buff_byte[uart_irq_cnt++] = uart_read_byte(UART_MODULE_SEL);
}
}
if ((uart_irq_cnt % UART_RX_IRQ_LEN == 0) && (uart_irq_cnt != 0)) {
uart_rx_flag = 1;
}
} else {
unsigned char uart_fifo_cnt = uart_get_rxfifo_num(UART_MODULE_SEL);
if (uart_fifo_cnt != 0) {
for (int j = 0; j < uart_fifo_cnt; j++) {
uart_read_byte(UART_MODULE_SEL);
}
}
}
}
//RXDONE Timeout Interrupt: After receiving one frame, read the remaining FIFO data
if (uart_get_irq_status(UART_MODULE_SEL, UART_RXDONE_IRQ_STATUS)) {
gpio_set_high_level(LED4);
unsigned char uart_fifo_cnt = uart_get_rxfifo_num(UART_MODULE_SEL);
if (uart_fifo_cnt != 0) {
for (int j = 0; j < uart_fifo_cnt; j++) {
if (uart_rx_flag == 0) {
uart_rx_buff_byte[uart_irq_cnt++] = uart_read_byte(UART0);
if ((uart_irq_cnt % UART_RX_IRQ_LEN == 0) && (uart_irq_cnt != 0)) {
uart_rx_flag = 1;
uart_rx_done_flag = 1;
break;
}
}
}
}
if (uart_rx_flag == 1) {
uart_rx_done_flag = 1;
}
uart_clr_irq_status(UART_MODULE_SEL, UART_RXDONE_IRQ_STATUS);
}
}
Note
- UART FIFO has a depth of 8 bytes. If the time before and after entering the RX interrupt exceeds the time required to receive 8 bytes, the FIFO pointer may be scrambled, causing abnormal received data. Check the anomaly using
uart_get_rxfifo_num() > 8. If this abnormality occurs, it is recommended to switch to the DMA mode.
(2) DMA Mode
In DMA mode, data transmission and reception are automatically handled by the DMA engine, and the CPU only needs to configure the DMA descriptor and respond to the interrupt.
Features:
- DMA automatically transfers data between UART FIFO and SRAM
- Sending is determined to be completed by
UART_TXDONEinterrupt - Reception can be determined through
UART_RXDONEinterrupts or DMA TC interrupts - Suitable for high-speed, large data usage scenarios
Core function:
- Send:
uart_send_dma(). - Receive:
uart_receive_dma(). - Receiving length calculation:
uart_get_dma_rev_data_len().
a. DMA configuration
//Select DMA channels that are not occupied by other modules
#define UART_DMA_CHANNEL_TX DMA3
#define UART_DMA_CHANNEL_RX DMA2
//Configure the TX/RX DMA channels
uart_set_tx_dma_config(UART_MODULE_SEL, UART_DMA_CHANNEL_TX);
uart_set_rx_dma_config(UART_MODULE_SEL, UART_DMA_CHANNEL_RX);
uart_set_tx_dma_config() and uart_set_rx_dma_config() call dma_config() internally to configure the following DMA parameters:
- TX: Source address increments, target address fixed, Handshake mode, Word transmission bit width
- RX: Fixed source address, incrementing destination address, Handshake mode, Word transfer bit width
b. DMA mode interrupt configuration
//B91 needs to clear TX_DONE because its default value is 1
uart_clr_tx_done(UART_MODULE_SEL);
//Enable TX_DONE interrupt mask (UART module interrupt)
uart_set_irq_mask(UART_MODULE_SEL, UART_TXDONE_MASK);
//B91 / B92 When the DMA receive length is configured to a fixed length, it needs to enable RX_DONE interrupts and manually calculate the receive length via software
uart_set_irq_mask(UART_MODULE_SEL, UART_RXDONE_MASK);
//B92 (DMA configured to full ff) and subsequent chips, enable DMA TC interrupts and automatically write back length via hardware
Note: DMA TC interrupts belong to the DMA module and require dma_set_irq_mask configuration
dma_set_irq_mask(UART_DMA_CHANNEL_RX, TC_MASK);
//Enable PLIC interrupt sources
plic_interrupt_enable(IRQ_UART0);
//Enable global interrupt
core_interrupt_enable();
c. Interrupt handling
In DMA mode, there are two methods to check when received is complete: through UART_RXDONE interrupts (software calculates length) or through DMA TC interrupts (hardware automatically writes back length).
Method 1: Use UART_RXDONE interrupt judgment (B91 / B92 fixed length mode).
When the UART_RXDONE interrupt checks that the received is complete, the uart_get_dma_rev_data_len() function is used to obtain the length of the received data:
//B91 handling
_attribute_ram_code_sec_ void uart0_irq_handler_b91(void)
{
if (uart_get_irq_status(UART_MODULE_SEL, UART_RXDONE)) {
gpio_toggle(LED3);
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_CLR_RX);
}
//Get the length of received data (must be before clearing the interrupt flag)
rev_data_len = uart_get_dma_rev_data_len(UART_MODULE_SEL, UART_DMA_CHANNEL_RX);
//Clear RX interrupts (B91 requires resetting hardware state machine)
uart_hw_fsm_reset(UART_MODULE_SEL);
//DMA access to memory requires word alignment
uart_receive_dma(UART_MODULE_SEL, (unsigned char *)rec_buff, DMA_REV_LEN);
uart_send_dma(UART_MODULE_SEL, (unsigned char *)rec_buff, rev_data_len);
}
}
//B92 handling
_attribute_ram_code_sec_ void uart0_irq_handler_b92(void)
{
if (uart_get_irq_status(UART_MODULE_SEL, UART_RXDONE_IRQ_STATUS)) {
gpio_toggle(LED3);
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_RXBUF_IRQ_STATUS);
}
//Get the length of received data (must be before clearing the interrupt flag)
rev_data_len = uart_get_dma_rev_data_len(UART_MODULE_SEL, UART_DMA_CHANNEL_RX);
//Clearing RX Interrupts (B92 uses uart_clr_irq_status)
uart_clr_irq_status(UART_MODULE_SEL, UART_RXDONE_IRQ_STATUS);
//DMA access to memory requires word alignment
uart_receive_dma(UART_MODULE_SEL, (unsigned char *)rec_buff, DMA_REV_LEN);
uart_send_dma(UART_MODULE_SEL, (unsigned char *)rec_buff, rev_data_len);
}
}
Note
- In Telink RISC-V MCU, the operation to clear the interrupt status flag must be done after calculating the DMA received data length; otherwise, the DMA received data length calculation is incorrect.
Method 2: Use DMA TC interrupt to determine (B92 full ff mode and subsequent chips).
When the DMA TC interrupt determines that the receive is complete, the hardware automatically writes back the received data length to the first 4 bytes of the buffer:
void dma_irq_uart_rx_process(void)
{
if (buff_rx_index == 1) {
rev_data_len = *(unsigned int *)rec_buff;
if (rev_data_len > DMA_REV_LEN) {
rev_data_len = DMA_REV_LEN;
}
uart_receive_dma(UART_MODULE_SEL, (unsigned char *)(rec_buff1 + 4), DMA_REV_LEN);
buff_rx_index = 0;
} else if (buff_rx_index == 0) {
rev_data_len1 = *(unsigned int *)rec_buff1;
if (rev_data_len1 > DMA_REV_LEN) {
rev_data_len1 = DMA_REV_LEN;
}
uart_receive_dma(UART_MODULE_SEL, (unsigned char *)(rec_buff + 4), DMA_REV_LEN);
buff_rx_index = 1;
}
dma_rx_done_flag = 1;
}
/*
* Scenario analysis: There are three scenarios:
* 1. Transmission Length ≤ Receive Length: Generates a TC interrupt, and the received data length is the actual transmitted length
* 2. Transmission Length > Received Length: Generates a TC interrupt, received data length is the actual transmission length,
* However, only the receive-length data configured in the buffer is valid; DMA does not move excess data into the buffer
* 3. Interrupt handling: Alternately receives rec_buff/rec_buff1 via the buff_rx_index flag,
* Alternate sending via buff_tx_index flag in main_loop
*/
void dma_irq_handler(void)
{
if (dma_get_tc_irq_status(BIT(UART_DMA_CHANNEL_RX))) {
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_RXBUF_IRQ_STATUS);
}
dma_clr_tc_irq_status(BIT(UART_DMA_CHANNEL_RX));
dma_irq_uart_rx_process();
}
}
DMA Reception Notes:
addr: Receives buffer address, must be 4 bytes aligned, and the actual buffer size must not be less thanrev_size-
rev_size: DMA receiving length must be a multiple of 4, with a maximum value of 0xFFFFFC -
DMA transfers data in units of 4 bytes (Word). For example, if the actual received length is 5 bytes, DMA transfers 2 bytes totaling 8 bytes, with the last 3 bytes being invalid. Therefore, extra space must be reserved for the receiving buffer
- If the received data length len meets
4×(n-1) < len ≤ 4×n, it is recommended to setrec_buff = rev_size = 4n - The received length cannot exceed the DMA's set length; otherwise, the excess is discarded
- In
UART_RXDONEinterrupts, the received length must be calculated first, then the interrupt flag must be cleared
Precautions for reconfiguring DMA TX/RX:
When the DMA transfer is not complete, to reconfigure the DMA, follow these steps:
dma_chn_dis()Disable DMA channel- Recall
uart_send_dma()oruart_receive_dma().
Note
uart_receive_dma()internally performsdma_chn_disand clears interrupt status. When called, the DMA channel is automatically diabled and theUART_RXBUF_IRQ_STATUSis cleared, no manual call is required.
(3) DMA LLP model
The DMA LLP (Linked List Pointer) mode is an advanced application of the DMA model, supported by B92 (DMA receive length when configured to full ff) and subsequent chips. Enables automatic switching between multiple buffers via linked list descriptors, suitable for scenarios requiring continuous reception without packet loss. Only supports RX direction; after reception, hardware automatically jumps to the next linked list node without the need to reconfigure uart_receive_dma() by software.
Features:
- Hardware automatically writes back the received data length to the first 4 bytes of the buffer.
- Supports Ping-Pong dual buffering to achieve uninterrupted data reception
- Only RX orientation is supported; After reception is complete, the hardware automatically jumps to the next linked list node without the need for software reconfiguration
uart_receive_dma().
Single Chain Mode:
//Initialization
uart_rx_dma_chain_init(UART_MODULE_SEL, UART_DMA_CHANNEL_RX,
(unsigned char *) (rec_buff + 4), DMA_REV_LEN);
//Interrupt handling
void dma_irq_handler(void)
{
if (dma_get_tc_irq_status(BIT(UART_DMA_CHANNEL_RX))) {
dma_clr_tc_irq_status(BIT(UART_DMA_CHANNEL_RX));
rev_data_len = *(unsigned int *)rec_buff; //The received length automatically written back by hardware
rx_done_flag = 1;
}
}
PLIC_ISR_REGISTER(dma_irq_handler, IRQ_DMA)
Ping-Pong Buffer Mode:
//Initialization
uart_set_dma_chain_llp(UART_MODULE_SEL, UART_DMA_CHANNEL_RX,
(unsigned char *)(rec_buff + 4), DMA_REV_LEN, &rx_dma_list[0]);
uart_rx_dma_add_list_element(UART_MODULE_SEL, UART_DMA_CHANNEL_RX,
&rx_dma_list[0], &rx_dma_list[1],
(unsigned char *)(rec_buff1 + 4), DMA_REV_LEN);
uart_rx_dma_add_list_element(UART_MODULE_SEL, UART_DMA_CHANNEL_RX,
&rx_dma_list[1], &rx_dma_list[0],
(unsigned char *)(rec_buff + 4), DMA_REV_LEN);
dma_chn_en(UART_DMA_CHANNEL_RX);
Ping Pong buffer interrupt handling:
void dma_irq_handler(void)
{
if (dma_get_tc_irq_status(BIT(UART_DMA_CHANNEL_RX))) {
dma_clr_tc_irq_status(BIT(UART_DMA_CHANNEL_RX));
if (pingpong_flag == 0) {
rev_data_len = *(unsigned int *)rec_buff; //Read rec_buff, write back length
pingpong_flag = 1;
} else {
rev_data_len1 = *(unsigned int *)rec_buff1; //Read rec_buff1, write back length
pingpong_flag = 0;
}
rx_done_flag = 1;
}
}
PLIC_ISR_REGISTER(dma_irq_handler, IRQ_DMA)
DMA LLP usage restrictions:
- B92: DMA receive length must be set
to 0xFFFFFCfor hardware to automatically write back to receive length - TL751x / TL721x / TL321x / TL322x / TL323x and other subsequent chips: No usage restrictions, DMA length can be set to any value (not exceeding 0xFFFFFC and multiples of 4), and transmission length is also unlimited
UART flow control
(1) CTS configuration
CTS (Clear to Send) controls local transmission: stops sending data when the CTS pin detects a valid level.
//Configure CTS pins and polarity
uart_cts_config(UART0_CTS_PA1, 0); 0: Low level stops TX; 1: High-level stop TX
//Enable CTS
uart_set_cts_en(UART0);
(2) RTS configuration
RTS (Request to Send) is used to notify the peer: when the local RX buffer is close to overflow, a signal is sent via the RTS pin to request the peer to stop sending. RTS supports both manual and automatic modes.
Manual mode:
uart_set_rts_en(UART0); //Enable RTS
uart_rts_manual_mode(UART0); //Set to manual mode
uart_set_rts_level(UART0, 1); //Manually set the RTS pin level
Auto mode:
In automatic mode, RTS is activated when one of the following conditions is met:
- Data volume in RX FIFOs ≥ Set threshold (
uart_rts_trig_level_auto_mode) - Generates RX_DONE signals (requires enabling
uart_rxdone_rts_en, TL321x, and subsequent chips)
uart_set_rts_en(UART0); //Enable RTS
uart_rts_auto_mode(UART0); //Set to auto mode
uart_rts_trig_level_auto_mode(UART0, 5); //RX FIFO activates RTS at ≥ 5 bytes
// uart_rxdone_rts_en(UART0); //Optional: RTS activated at RX_DONE (TL321x+)
Note
- The B92 and subsequent chips add a
uart_rts_stop_rxtimeout_en()interface to stop RX timeout counting after RTS is triggered, thereby avoidingUART_RXDONE_IRQ_STATUSinterrupts during RTS activation. When the RTS function is not in use, this interface does not need to be enabled, otherwise it affects the generation of UART RXDONE.
(3) Flow control precautions
- CTS/RTS pins must be configured via
uart_cts_config()/uart_rts_config() pins and polarities before activation - CTS/RTS pins vary depending on the chip; refer to each chip's
uart_cts_pin_e/uart_rts_pin_eenumeration. - In automatic mode, when the RX FIFO data volume falls below the threshold, the RTS signal automatically recovers.
UART error handling and timeout mechanisms
(1) Receive error handling
A UART_RX_ERR interrupt occurs when UART receives data with parity errors or stop bit errors. Error handling varies depending on the chip and mode:
NDMA mode error handling:
// B91
uart_clr_irq_status(UART0, UART_CLR_RX); //Clear RX FIFO and error flags
uart_hw_fsm_reset(UART0); //Reset the hardware pointer
uart_clr_rx_index(UART0); //Clear the software pointer
// B92+
uart_clr_irq_status(UART0, UART_RXBUF_IRQ_STATUS); //Clear in one step
DMA mode error handling:
When a reception error occurs, it is recommended to restart the DMA receive:
uart_receive_dma(UART0, (unsigned char *)rec_buff, BUFF_DATA_LEN);
uart_receive_dma() internally implemented dma_chn_dis and interrupt clearing (B92+)
UART Sleep Wakeup
UART's handling before and after entering low-power sleep varies by mode:
NDMA sending:
- Before going to sleep: Check
uart_tx_is_busy()as 0, ensure data transmission is complete. - After wake-up: call
uart_clr_tx_index()to clear the sending pointer and clear the data sent from the RAM.
NDMA Reception:
- After wake-up: call
uart_clr_rx_index()to clear the receive pointer and clear the received data from the RAM.
DMA sending:
- After wake-up: recall
uart_send_dma()(internally implementeddma_chn_dis)
DMA reception:
- After wake-up: recall
uart_receive_dma()(internally implementeddma_chn_dis)
(1) Abnormal TX interruption after waking from sleep
Problematic phenomena
When the chip enters low-power sleep mode (Suspend) and wakes up again, even if no data is actively sent, UART anomaly triggers a TX_DONE interrupt.
Cause analysis
When the chip enters Suspend mode, the hardware automatically resets the corresponding bus. After the reset release, the status bit of the TX_DONE register inside the UART is set to 1 by default. If the system reactivates the global interrupt at this point, the MCU mistakenly believes the previous data transmission has been completed and mistakenly jump into UART's TX interrupt service program.
Chip differences
- Chips of Category 1 (B91) and Chips of Category 2 (B85/B87): Both have this hardware design feature and require software intervention.
- Other chips: The hardware has iteratively optimized the bus reset logic, so this anomaly no longer occurs after wake-up, so no special handling is needed.
Solution
For chips with this issue, the standard workaround: block UART interrupts -> before entering sleep, manually clear residual TX_DONE interrupt states after wake-up, >, and finally remove UART interrupt masking.
The detailed reference code is as follows:
// 1. Before entering sleep: Mask UART's TX interrupt flag to prevent accidental triggers during wake-up
uart_clr_tx_done(UART0);
plic_interrupt_disable(IRQ_UART0);
// 2. Call the low-power entry function (enter Suspend sleep)
cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_PAD, 0);
// 3. After waking from sleep: At this point, the bus reset causes TX_DONE to change to 1, and manually clear this residual state first
uart_clr_tx_done(UART0);
// 4. After clearing the state, safely unshield and restore the UART interrupt function
plic_interrupt_enable(IRQ_UART0);
API reference
(1) Initialize and configure the APIs
| API name | Description |
|---|---|
uart_hw_fsm_reset |
Reset the UART finite state machine, clear register states and FIFO data |
uart_set_pin |
Configure UART TX/RX pins |
uart_cal_div_and_bwpc |
Calculate the division parameters div and bit width bwpc based on baud rate and PCLK |
uart_init |
Initialize UART modules (frequency division parameters, check bits, stop bits) |
uart_set_rx_timeout |
Set RX Timeout Time (B91/B92) |
uart_set_rx_timeout_with_exp |
Set RX timeout, supports exponential scaling (TL721x and subsequent chips) |
uart_clk_en |
Enable UART module clock (called when multi-instance chips use UART2+) |
(2) NDMA data transmission and reception API
| API name | Description |
|---|---|
uart_send_byte |
Send a single byte |
uart_send_hword |
Send half a word (2 bytes, low byte first) |
uart_send_word |
Send word (4 bytes) |
uart_send |
Batch Send (Internal Loop Call uart_send_byte) |
uart_read_byte |
Read a single byte |
(3) DMA Data Transmission and Reception API
| API name | Description |
|---|---|
uart_send_dma |
DMA sends data |
uart_receive_dma |
DMA receives data |
uart_get_dma_rev_data_len |
Calculate the actual data length received by the DMA |
uart_set_tx_dma_config |
Configure TX DMA channels |
uart_set_rx_dma_config |
Configure the RX DMA channel |
(4) Interrupt Control API
| API name | Description |
|---|---|
uart_set_irq_mask |
Enable UART interrupt mask |
uart_clr_irq_mask |
Disable UART interrupt mask |
uart_get_irq_status |
Obtain interrupt status |
uart_clr_irq_status |
Clear the interrupt status |
uart_rx_irq_trig_level |
Set the RX interrupt trigger threshold |
uart_tx_irq_trig_level |
Set the TX interrupt trigger threshold |
uart_clr_tx_done |
Clear TX_DONE Label (B91 Only) |
uart_rxdone_sel |
Select RXDONE function attribution (NDMA/DMA, TL321x+ supported) |
(5) Flow Control API
| API name | Description |
|---|---|
uart_cts_config |
Configure CTS pins and polarity |
uart_set_cts_en / uart_set_cts_dis |
Enable/disable CTS |
uart_rts_config |
Configure RTS pins and polarity |
uart_set_rts_en / uart_set_rts_dis |
Enable/disable RTS |
uart_rts_manual_mode |
Set RTS to manual mode |
uart_rts_auto_mode |
Set RTS to automatic mode |
uart_set_rts_level |
Manually set RTS pin level (manual mode) |
uart_rts_trig_level_auto_mode |
Set the RTS trigger threshold for automatic mode |
uart_rxdone_rts_en |
Enable RX_DONE Trigger RTS (TL321x+) |
uart_rts_stop_rxtimeout_en |
Stops RX timeout count when RTS activates (TL321x+) |
(6) Status query and auxiliary API
| API name | Description |
|---|---|
uart_tx_is_busy |
Check if TX is sending data |
uart_get_rxfifo_num |
Obtain the amount of data in the RX FIFO |
uart_get_txfifo_num |
Get the amount of data in the TX FIFO |
uart_clr_rx_index |
Clear software receiving pointer |
uart_clr_tx_index |
Clear software sending pointer |
uart_set_error_timeout |
Set error timeout (TL321x+) |
uart_get_error_timeout_code |
Retrieve the error timeout error code (TL321x+) |
uart_timeout_handler |
Error timeout handling function (TL321x+, redefinable) |
(7) DMA LLP API
| API name | Description |
|---|---|
uart_rx_dma_chain_init |
Initialize DMA LLP single-chain |
uart_set_dma_chain_llp |
Initialize the DMA LLP chain |
uart_rx_dma_add_list_element |
Add DMA LLP linked list nodes |
Demo reference
Demo path: demo/vendor/UART_DEMO/
| File | Corresponding modes | Description |
|---|---|---|
app.c |
NDMA mode | Byte/half-word/word sending, RX interrupt reception |
app_dma.c |
DMA Mode | DMA transmission and reception, TX_DONE/RX_DONE interrupts |
app_dma_llp.c |
DMA LLP model | Single chain / ping-pong buffer reception |
(1) NDMA Demo
File: app.c
Mode selection:
#define UART_MODE UART_NDMA
#define FLOW_CTR NORMAL // Optional: BASE_TX / NORMAL / USE_CTS / USE_RTS
Test scenarios:
| FLOW_CTR value | Test content |
|---|---|
| BASE_TX | TX sends data to the serial port tool via byte, hword, or word to verify TX functionality |
| NORMAL | The serial port tool sends 16 bytes, and after the RX interrupts reception, TX returns to the serial port tool |
| USE_CTS | TX continues to send bytes, and stops sending after the external CTS pin is given a high level |
| USE_RTS | The serial port tool sends >5 bytes, and the logic analyzer can observe the RTS pin level changing from low to high |
(2) DMA Demo
File: app_dma.c
Mode selection:
#define UART_MODE UART_DMA
#define UART_DEVICE UART_SLAVE_DEVICE // Optional: UART_MASTER_DEVICE / UART_SLAVE_DEVICE
Test scenarios:
| UART_DEVICE value | Test content |
|---|---|
| UART_MASTER_DEVICE | TX sends data to the serial port tool via DMA, triggering the next frame transmission via TX_DONE interrupt |
| UART_SLAVE_DEVICE | The serial port tool sends data to the RX, interrupts reception via RX_DONE, calculates the received length, and returns via TX |
DMA Reception Length Configuration (Chip Differences):
//B91 / B92 (any length) :D MA_REV_LEN = BUFF_DATA_LEN
//B92 (maximum length mode) :D MA_REV_LEN = 0xFFFFFC, hardware automatically writes back the received length
//TL751x / TL721x / TL321x / TL322x / TL323x: DMA_REV_LEN = BUFF_DATA_LEN, hardware automatically writes back the received length
(3) DMA LLP Demo
File: app_dma_llp.c
Mode selection:
#define UART_MODE UART_DMA_LLP
#define DMA_LLP_MODE DMA_LLP_PINGPONG // Optional: DMA_LLP_SINGLE_CHAIN / DMA_LLP_PINGPONG
Note
- DMA LLP only supports slave mode; master mode does not.
Test scenarios:
| DMA_LLP_MODE value | Test content |
|---|---|
| DMA_LLP_SINGLE_CHAIN | Single-chain mode, hardware automatically writes back the received length up to the first 4 bytes of rec_buff |
| DMA_LLP_PINGPONG | Ping-Pong dual buffer mode, alternating rec_buff and rec_buff1 reception to achieve uninterrupted data reception |
UART in Category 2
The working mechanism of the UART module in Category 2 differs somewhat from that of the UART module in Category 1. The following explains the chip differences.
NODMA
The working mechanism of the second category of NDMA is consistent with that of the first category of work, so we do not go into detail here, only briefly introduce the differences between the first category of chips.
Points to note when receiving data:
- B85/B87/B89: No RX_DONE interrupts, only RX_BUFF interrupts.
uart_rx_trig_level(Range 1-8) If you don't know the data length sent by the other party, set it to 1; if you know the data length sent by the other party (which must be an integer multiple of the level), set it as needed. - B80 and later chips: RX_DONE interrupts, RX_DONE can be used together with RX_BUFF interrupts, without concerning about the data length sent by the other party. Generally, set
uart_rx_trig_levelto 4.
DMA (no DMA linked list function)
The working mechanism of the second category of DMA differs somewhat from the first category. Here, we explain the working mechanism of the DMA.
DMA sends data:
- The maximum length allowed by the DMA to be sent is: 4075 bytes
- To send a buff, fill in the length information for the first four bytes, followed by the data to be sent. The buff needs to be aligned with four bytes
- Use TX_DONE interrupt to determine whether data has been sent
DMA receives data:
- The maximum length allowed by DMA to receive data is: 4075 bytes
- Receiving buffs requires four bytes of alignment; the first four bytes store the received length information
- The length of the buff includes the first four bytes, and the DMA len needs to be a multiple of 16
- DMA interrupt occurrence: DMA interrupts only occur after UART timeout.
- If the sending length exceeds the DMA's receive length, DMA continues to receive but will keep storing it in the last word of the buff, so the buff will not go out of bounds
Chip differences:
- B85/B87/B89: Reception can only be determined by DMA interrupts, not by RX_DONE interrupts.
- B80 and subsequent chips: can be interrupted with RX_DONE or DMA.
Note
- You only need to call the
uart_recbuff_initfunction during initialization; there is no need to call this function during a DMA interrupt. The hardware automatically receives data according to the configuration.
Example of initialization configuration:
#define rec_buff_Len 32 //rec_buff_Len must be a multiple of 16, and the received data length is < (16*n - 4)
#define trans_buff_Len 16
//DMA buff sending and receiving requires four bytes of alignment, with the first 4 bytes storing the transmission length information
__attribute__((aligned(4))) unsigned char rec_buff[rec_buff_Len]={0};
__attribute__((aligned(4))) unsigned char trans_buff[trans_buff_Len] = {0x0c,0x00,0x00,0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xaa,0xbb,0xcc};
void user_init(void)
{
//note: dma addr must be set first before any other uart initialization!
//Configure receive buffs; DMA len must be a multiple of 16, and the first 4 bytes store the receive length information
uart_recbuff_init((unsigned char *)rec_buff, sizeof(rec_buff));
}
Multi-Core
Overview
Some chips on the Telink platform adopt a multi-core architecture, integrating the main core D25F (RISC-V) along with the N22 (RISC-V coprocessor) and/or DSP (Digital Signal Processor). For supported chips and core configurations, refer to the chip feature summary in Platform SDK Overview.
The resources and features of each core are as follows:
| Core | Type | Memory Resources | Cache | Floating-Point/DSP Instructions | Pipeline | Typical Usage |
|---|---|---|---|---|---|---|
| D25F | RISC-V | Flash (XIP) / SRAM | Supported | Supported | 5-stage | Main core, responsible for system boot and inter-core coordination |
| N22 | RISC-V | IRAM / DRAM | TL751x: No; TL322x: Yes | Not supported | 2-stage | Coprocessor core, commonly used for RF / BLE protocol stack |
| DSP | DSP | ILM (Instruction) / DLM (Data) | Supported | — | — | Digital signal processing, commonly used for audio codec |
-
N22 does not support floating-point and DSP instructions. The C implementation version in
libdsp.amust be used instead. Pay attention to the instruction set differences between N22 and D25F during project configuration. -
The Cache configuration of N22 varies depending on the chip: TL751x N22 does not have Cache, while TL322x N22 supports Cache. This affects Cache consistency handling during cross-core RAM access. For details, refer to Cross-Core RAM Access.
After D25F starts up, it loads the N22 / DSP firmware and starts the corresponding cores as required. The cores communicate with each other through Mailbox messages. For details, refer to Mailbox.
Multi-Core Usage Constraints
Multi-core operation has the following hardware constraints and usage requirements. Understanding these constraints is essential for proper use of the multi-core architecture.
Boot Sequence: D25F Starts First, Coprocessor Cores Are Started by D25F
After power-on, D25F boots from Flash first, while N22 and DSP remain in the reset state. During operation, D25F starts the coprocessor cores on demand through sys_n22_init/start and sys_dsp_init/start. The startup timing and firmware loading location of N22/DSP are fully controlled by D25F.
- The registers and RAM of N22/DSP can only be accessed after D25F completes the power-on initialization and reset process.
Memory Allocation: Independent RAM, Shared Flash
Each core has its own dedicated RAM space (D25F uses SRAM, N22 uses IRAM/DRAM, and DSP uses ILM/DLM), which operate independently without interfering with each other. Flash is shared among multiple cores. The firmware of each core is stored in different Flash regions, and the addresses are specified by the user through the *_FW_DOWNLOAD_FLASH_ADDR macros.
- The Flash interface does not support simultaneous access by multiple cores. Only one core is allowed to access Flash at a time. Multiple cores can execute XIP (execute-in-place) from Flash simultaneously, and hardware arbitrates Flash access. However, this reduces access efficiency. Therefore, it is not recommended to run N22/DSP instructions directly from Flash. Running from RAM is recommended (see Boot Modes and Principles).
Peripheral Deployment
Some peripherals are fixed in the domain of a specific core and are more efficiently managed by that core. Whether cross-domain peripheral access passes through an asynchronous bridge depends on the chip:
| Core | Managed Peripherals | Async Bridge in TL751x | Async Bridge in TL322x |
|---|---|---|---|
| D25F | All peripherals except rf / timer_n22 |
— | — |
| N22 | rf / timer_n22 |
Yes | No |
| DSP | — | No | No |
All peripheral registers can be accessed by all three cores. However, when accessing peripherals outside the core's own domain, read operations are slower if an asynchronous bridge is required.
Performance Impact of Asynchronous Bridge (only applicable to chips with asynchronous bridges, such as TL751x):
- Typical scenario: The
rfregisters are managed by the N22 domain. When D25F accessesrfregisters, the access must go through the asynchronous bridge, while N22 access does not require the bridge. - During asynchronous bridge access, read operations are slower, while write operations are relatively faster.
- For large amounts of cross-core data transfer, DMA is recommended (higher efficiency than MCU access, and asynchronous bridge write access is faster than read access).
Cross-Core RAM Access
All three cores can read and write the RAM of other cores. However, the following rules must be observed:
Cache Consistency (0x80000000 Rule):
When a core with Cache accesses the RAM of another core, the 0x80000000 prefix must be added to bypass the Cache. Otherwise, outdated data in the Cache may be read, resulting in data inconsistency. Cores without Cache do not have this restriction.
Since the Cache configuration of N22 varies depending on the chip (TL751x without Cache, TL322x with Cache), whether the 0x80000000 prefix is required for cross-core access also depends on the chip:
| Accessing Core | Target RAM | Need 0x80000000 in TL751x |
Need 0x80000000 in TL322x |
|---|---|---|---|
| D25F | N22 / DSP RAM | Yes (D25F has Cache) | Yes (D25F has Cache) |
| DSP | N22 / D25F RAM | Yes (DSP has Cache) | Yes (DSP has Cache) |
| N22 | D25F / DSP RAM | No (N22 has no Cache) | Yes (N22 has Cache) |
-
When accessing the RAM of another core through DMA, the
0x80000000prefix is not required because DMA directly accesses the physical address. -
DSP ILM must be accessed in word (4-byte) units.
Coprocessor Core Startup
The startup mechanisms of N22 and DSP are the same and consist of two stages: initialization (power-on, clock enable, and reset vector configuration) and startup (releasing reset/Stall and starting execution). The differences are that the power domains, firmware formats, and available loading methods vary slightly between cores.
- The coprocessor core startup APIs are
sys_n22_init(addr)/sys_n22_start()andsys_dsp_init(addr)/sys_dsp_start(). Refer to the correspondingsys_n22.h/sys_dsp.hfiles for details.
Boot Modes and Principles
Running N22/DSP instructions from Flash (XIP) is not recommended. Running from RAM is recommended. The reasons are as follows:
- Simultaneous Flash XIP access: When multiple cores execute XIP from Flash simultaneously, hardware arbitrates Flash access, reducing instruction fetch efficiency.
- Additional performance impact from asynchronous bridges: For chips with asynchronous bridges (such as TL751x), Flash access from the N22 domain must also pass through the asynchronous bridge. Read operations are slower, resulting in worse XIP performance.
- Poor XIP performance on N22 without Cache (TL751x): D25F uses Cache to accelerate Flash instruction fetch, making XIP performance acceptable. However, the N22 on TL751x does not have Cache, resulting in high instruction fetch latency and uncertain timing. Therefore, it cannot meet the real-time requirements of the RF/BLE protocol stack.
Coprocessor firmware can run from either Flash or RAM. It is recommended to select the loading method through the *_BOOTLOADER_MODE macro on the D25F side. Different loading methods determine whether D25F transfers the firmware and where the coprocessor fetches instructions from.
| Mode | Whether D25F Transfers Firmware | Coprocessor Instruction Fetch Location | Application Scenario |
|---|---|---|---|
| BY_MCU | No | RAM (the core copies firmware by itself in the S file) | Not recommended; self-copying in the S file has slow XIP speed |
| BY_DMA | Yes (DMA transfer) | RAM | Large firmware; does not block D25F (recommended) |
| BY_POINTER | Yes (memcpy transfer) |
RAM | Simple implementation, but blocks D25F (recommended) |
| BY_NVM_MCU | No | NVM (such as RRAM) | Only supported on chips with NVM |
Note
- N22 firmware is maintained by the driver, while DSP firmware is maintained by the SDK. The actual instruction fetch behavior of each core depends mainly on the linker script and startup implementation.
Coordination Between Project Configuration and S/link Files
The startup mode of the coprocessor core is jointly determined by the D25F-side macros and the S/link files on the coprocessor side. The two configurations must match.
D25F side (MULTI_CORE_Demo):
ENABLE_N22/ENABLE_DSP: Enables or disables the corresponding coprocessor core.N22_BOOTLOADER_MODE/DSP_BOOTLOADER_MODE: Selects the firmware loading mode.N22_FW_DOWNLOAD_FLASH_ADDR/DSP_FW_DOWNLOAD_FLASH_ADDR: Specifies the Flash address where the coprocessor firmware is stored.
Coprocessor side (N22_Test_Demo / DSP-specific Demo):
- The corresponding
cstartup.Sand linker file must be selected during compilation. These files determine whether the firmware runs from Flash or RAM. The N22 driver implementation runs firmware from RAM. For DSP, refer to the specific DSP implementation.
The matching relationship between the D25F-side macros and coprocessor-side S/link files (N22) is shown below:
D25F-side *_BOOTLOADER_MODE |
Coprocessor-side S / link file | Firmware execution location |
|---|---|---|
| BY_MCU | cstartup_flash.S + flash_boot_ramcode.link |
RAM (the startup assembly file copies the bin file to SRAM and starts execution from SRAM) |
| BY_DMA / BY_POINTER | cstartup_ram.S + ram_boot.link |
RAM (recommended) |
For differences between the two linker files, refer to Software Startup. After compilation, the coprocessor firmware is programmed to the Flash address specified by *_FW_DOWNLOAD_FLASH_ADDR.
Usage Example
Taking N22 as an example, the following code is called in user_init() of the D25F-side MULTI_CORE_Demo.
BY_POINTER (D25F copies firmware to N22 RAM using memcpy):
sys_n22_init(N22_IRAM_STARTUP_ADDR);
// Read the segment size and DLM start offset from the firmware header
n22_ilm_bin_size = REG_ADDR32(N22_FW_DOWNLOAD_FLASH_ADDR + 0x08);
n22_dlm_bin_size = REG_ADDR32(N22_FW_DOWNLOAD_FLASH_ADDR + 0x0c);
n22_dlm_lma_start = REG_ADDR32(N22_FW_DOWNLOAD_FLASH_ADDR + 0x10) + N22_FW_DOWNLOAD_FLASH_ADDR;
memcpy((unsigned int*)N22_IRAM_STARTUP_ADDR, (unsigned int*)N22_FW_DOWNLOAD_FLASH_ADDR, n22_ilm_bin_size);
memcpy((unsigned int*)N22_DRAM_ADDR, (unsigned int*)n22_dlm_lma_start, n22_dlm_bin_size);
sys_n22_start();
The BY_DMA mode works similarly. Use dma_config / dma_set_address / dma_set_size to transfer the ILM and DLM segments. After the DMA transfer is completed, call sys_n22_start() to start N22.
BY_MCU (N22 copies the firmware to SRAM in the S file and starts execution from SRAM; not recommended):
// Some chips require the user to manually power on the ZB module, while other chips handle it automatically inside sys_n22_init. Refer to sys_n22.h for details.
pm_set_dig_module_power_switch(FLD_PD_ZB_EN, PM_POWER_UP);
sys_n22_init(N22_FW_DOWNLOAD_FLASH_ADDR); // Pass the Flash address directly
sys_n22_start();
N22 is powered by the ZB (baseband) domain. Some chips require the user to manually power on the ZB module before calling sys_n22_init (pm_set_dig_module_power_switch(FLD_PD_ZB_EN, PM_POWER_UP)), while other chips handle this automatically inside sys_n22_init. Refer to sys_n22.h for details.
DSP-Specific Notes
This section applies to chips integrated with DSP. Refer to Platform SDK Overview for supported chip models.
The DSP startup process is similar to that of N22, but has the following differences:
- After
sys_dsp_init, the DSP remains in the Stall state.sys_dsp_startmust be called to release the Stall state before the DSP starts running. - DSP ILM must be accessed in word (4-byte) units.
// BY_DMA: Transfer the DLM segment (DMA0) and ILM segment (DMA1), then start the DSP (recommended)
sys_dsp_init(DSP_ILM_START_ADDR);
// ... dma_config / dma_set_address / dma_set_size / dma_chn_en ...
// ... wait until both DMA transfers are complete
sys_dsp_start();
Execution Control
Stall (Pause Execution)
There are two methods to pause N22 / DSP execution. After resuming, the core continues execution from the point where it was paused.
| Method | Operation | Feature |
|---|---|---|
| Internal Stall (recommended for low-power applications) | D25F notifies the coprocessor core through Mailbox. The coprocessor core enters WFI mode and is awakened by its own interrupt. | Lower power consumption |
| External Stall | Disable and enable the clock | Only pauses the clock; power saving is less effective than WFI |
Mailbox
Overview
Mailbox is a hardware module in multicore chips used for inter-core communication. Each message consists of two fixed 32-bit words (8 bytes in total). After the transmitting side writes the message, an interrupt is triggered on the receiving side. After the receiving side reads the message, the hardware automatically clears the interrupt flag.
For supported chips and mailbox channels, please refer to the summary table of chip features in the Platform SDK Overview. The core components (D25F / N22 / DSP) are also referenced in this table.
For newly added multi-core chips, refer to the corresponding mailbox.h file in the SDK.
Working mechanisms
Transmission and Reception Flow
Mailbox provides a set of message registers (word0 + word1) for each communication core. Communication process:
- Transmitting Side: Writes word0 first, followed by word1. After the hardware writes to word1, the receiving side interrupt is triggered.
- Receiving Side: Reads word0 and word1 during the interrupt. After reading Word1, the hardware automatically clears the interrupt flag (this is a fixed hardware behavior and cannot be configured).
Users can customize the meanings of the two words (e.g., word0 = command code, word1 = parameter).
First, write word0, followed by word1. The hardware triggers an interrupt only after the write operation to word1 is completed. The message register is multi-core shared. Before writing a new message, the transmitting side must ensure the receiving side has read the previous message; otherwise, it will be overwritten.
Interrupt configurations
Mailbox interrupt priority and enable configuration is performed through PLIC/CLIC: the D25F side uses PLIC, and the N22 side uses CLIC. For the specific interrupt number, please refer to the mailbox.h of the corresponding chip.
The complete enablement process consists of three steps:
mailbox_set_irq_mask(...); // 1. Enable the Mailbox module interrupt mask
plic_interrupt_enable(IRQ_MAILBOX_xxx); // 2. PLIC/CLIC enables corresponding IRQ (D25F side)
core_interrupt_enable(); // 3. Enable CPU global interrupt
The interrupt may be enabled by default. If the Mailbox interrupt is not required, the corresponding mask clear function must be called to disable it.
The interrupt handler function is registered via the PLIC_ISR_REGISTER macro (the N22 side uses the CLIC corresponding registration macro):
PLIC_ISR_REGISTER(mailbox_n22_to_d25_irq_handler, IRQ_MAILBOX_N22_TO_D25)
Message register auto-clear (optional)
Note that the following two types of "automatic clearing" must be distinguished:
- Automatic Interrupt Flag Clearing (default hardware behavior, described in 2.1): After the receiving side reads Word1, hardware automatically clears the interrupt flag without software intervention.
- Message Register Auto-Clear (optional feature described in this section): After the receiving side reads Word1, the hardware automatically clears the message registers to prevent residual old messages.
By default, message register contents are retained after reading. To enable automatic clearing after reading, call:
mailbox_auto_read_clr_msg_en(); // Enable: After reading Word1, the message register will be automatically cleared
mailbox_auto_read_clr_msg_dis(); // Disable
This function acts on the receiving side of the read message. When the TL322x chip is enabled, both the D25F and N22 sides are affected.
Usage Mode
API naming rules
The Mailbox interface naming uses the operator (current core) as the prefix. set indicates setting messages sent by the current core, while get indicates retrieving messages received by the current core:
| Operation | Interface format | Description |
|---|---|---|
| Send messages | mailbox_<me>_set_<dst>_msg(msg) |
After writing, the peer automatically receives the interrupt. |
| Receive messages | mailbox_<me>_get_<src>_msg(msg) |
After reading, the hardware automatically clears the interrupt flag. |
<me>indicates the operator itself (d25f/n22/dsp).- In
set,<dst>indicates the message destination (the peer side). Inget,<src>indicates the message source (the peer side). - Example: D25F uses
mailbox_d25f_set_n22_msgto send messages to N22; D25F usesmailbox_d25f_get_n22_msgto receive messages from N22. msgis anunsigned int[2]array, msg[0] = word0, msg[1] = word1
Differences in Interrupt Interface Naming and Reasons (Message send/receive interfaces are consistent across chips; only the interrupt mask/status interfaces differ):
The Mailbox interrupt mask/status interface names differ across chips. The fundamental reason is the difference in register layout design.
-
TL751x (Unified Register with Parameterized Interface): The mask bits of all cores are defined in a single mask register, and the status bits of all cores are defined in a single status register. Therefore, the interface needs to pass a mask parameter to specify which bit to operate on:
mailbox_set_irq_mask(mask)— Themaskparameter has the following format:FLD_MAILBOX_N22_TO_D25F_IRQ.mailbox_get_irq_status()— Use& FLD_MAILBOX_xxxto check the source indicated by the return value.mailbox_clr_irq_status(status)
-
TL322x and later chips (per-core registers, parameterless interface): To resolve resource conflicts caused by multiple cores accessing the same mask/status registers simultaneously, the mask and status bits of each core are defined in separate dedicated registers. Therefore, interfaces do not require parameter passing; they directly distinguish which core's register is being operated on by the function name extension:
mailbox_set_irq_mask_d25f()/mailbox_set_irq_mask_n22()mailbox_get_irq_status_d25f()/mailbox_get_irq_status_n22()mailbox_clr_irq_status_d25f()/mailbox_clr_irq_status_n22()
The specific available interfaces are based on the chip's mailbox.h interface.
Interrupt handling
The interrupt handling process should be based on the above register layout differences to select the corresponding interface style.
Per-Core Register Style (TL322x, Single-Source Scenario): Each core has an independent status register. The register can be directly read and cleared without passing parameters or performing bitwise checks.
_attribute_ram_code_sec_noinline_ void mailbox_n22_to_d25_irq_handler(void)
{
if (mailbox_get_irq_status_d25f()) {
mailbox_clr_irq_status_d25f(); // First, clear the status
mailbox_d25f_get_n22_msg(msg); // Then read the message (after reading Word1, hardware automatically clears interrupt flags)
process_message(msg[0], msg[1]);
}
}
PLIC_ISR_REGISTER(mailbox_n22_to_d25_irq_handler, IRQ_MAILBOX_N22_TO_D25)
Unified Register Style (TL751x, Multi-Source Scenario): All cores share a single status register (e.g., D25F receives messages from both DSP and N22 simultaneously). The status register must be checked first to identify the source by bit, then the corresponding bit must be cleared before reading the message.
_attribute_ram_code_sec_noinline_ void mailbox_irq_handler(void)
{
unsigned int msg[2];
unsigned char status = mailbox_get_irq_status();
if (status & FLD_MAILBOX_DSP_TO_D25F_IRQ) {
mailbox_clr_irq_status(FLD_MAILBOX_DSP_TO_D25F_IRQ); // First, clear the status
mailbox_d25f_get_dsp_msg(msg); // Then read the message
handle_dsp_message(msg[0], msg[1]);
}
if (status & FLD_MAILBOX_N22_TO_D25F_IRQ) {
mailbox_clr_irq_status(FLD_MAILBOX_N22_TO_D25F_IRQ); // First, clear the status
mailbox_d25f_get_n22_msg(msg); // Then read the message
handle_n22_message(msg[0], msg[1]);
}
}
PLIC_ISR_REGISTER(mailbox_irq_handler, IRQ_MAILBOX_N22_TO_D25)
Clearing the status before reading the message can prevent loss of newly arrived message status records during the reading process. The unified register style allows code writing according to multi-source workflows even in single-source scenarios, facilitating cross-chip reuse.
Complete sending and receiving examples
D25F side:
// Send a message to N22
unsigned int msg[2] = {0x12345678, 0xABCDEF00};
mailbox_d25f_set_n22_msg(msg); // N22 will receive the interruption
N22 side:
// Send a message to D25F
unsigned int msg[2] = {0x87654321, 0x00FEDCBA};
mailbox_n22_set_d25f_msg(msg);
Notes
- Header Files Must Not Be Mixed: Mailbox register layouts differ across chips. The corresponding chip-specific
mailbox.hmust be used. - The difference in interrupt interface naming comes from register layout: TL751x concentrates all core mask/status bits in a unified register (interface with parameters). Note that Mailbox interrupt masks are recommended to be uniformly configured in D25F to avoid conflicts caused by multi-core configurations. For TL322x and later chips, to avoid resource conflicts caused by multiple cores accessing the same registers, the mask/status registers are separated into independent registers for each core (interfaces are named by core and do not require parameters). See API Naming Rules for details. Pay attention to this when porting across chips.
SAR ADC
Overview of SAR ADC
This chapter describes how to use the SAR ADC (Successive Approximation Analog-to-Digital Converter) driver, which is suitable for applications such as external analog voltage acquisition via GPIO and battery voltage (VBAT) monitoring.
Support for SAR ADC and SD ADC varies by chip as follows; this chapter focuses solely on SAR ADCs:
| Chip model | SAR ADC | SD ADC |
|---|---|---|
| B80 / B80B | ✅ | ❌ |
| B85 / B87 | ✅ | ❌ |
| TC122x / TC123x | ✅ | ❌ |
| B91 / B92 | ✅ | ❌ |
| TL321x / TL721x / TL751x | ✅ | ❌ |
| TL322x | ✅ | ✅ |
| TC321x / TL323x | ❌ | ✅ |
Note
- B91 includes TLSR951x and TLSR921x, B92 includes TLSR952x and TLSR922x, B80 / B80B includes TLSR8208 and TLSR8373, B85 includes TLSR825x and TLSR8359, B87 includes TLSR827x and TLSR8355.
Functions Supported
- GPIO Voltage Sampling: Measures the analog voltage on an external GPIO pin.
- Vbat Sampling: The battery voltage is connected via an internal voltage divider for battery power monitoring.
- Differential Input: Both the positive (P) and negative (N) pins can be independently configured to connect to a GPIO or internal GND, supporting both differential and single-ended connections.
- NDMA and DMA Data Acquisition Modes: NDMA is a blocking polling mode, suitable for low-frequency point-by-point acquisition; DMA is an automatic continuous transfer mode, suitable for high-frequency stream acquisition.
- Multi-channel hardware scanning (supported by a standalone FIFO architecture): The M, L, and R channels are automatically polled by a state machine, and data is interleaved during DMA transfers.
- Configurable reference voltage: 0.9 V / 1.2 V; factory calibration is based on 1.2 V.
Architecture Category
Drivers are divided into three categories based on the FIFO architecture, and the APIs for each category differ significantly. Use the table below to find the architecture that corresponds to your chip, then skip directly to the relevant section. This table only needs to be updated when new chips are added.
| FIFO Architecture | Chips | Resolution | Hardware Scan Channels | Vref |
|---|---|---|---|---|
| DFIFO architecture | B80 / B80B / B85 / B87 / TC122x / TC123x | 14-bit | MISC | 0.9V / 1.2V |
| Audio and ADC Multiplexed FIFO Architecture | B91 / B92 | 14-bit | MISC | 0.9V / 1.2V |
| ADC-Specific FIFO Architecture | TL321x / TL721x / TL751x | 12-bit | M / L / R | 1.2V |
| ADC-Specific FIFO Architecture (Dual ADCs, with Each ADC Having Its Own Dedicated FIFO) | TL322x | 12-bit | M / L / R (each core independent) | 1.2V |
Note
- For the specific APIs of each architecture, please refer directly to the
adc.hheader file and demo for the corresponding chip. - ADCs that reuse the Audio FIFO architecture cannot share a channel with audio.
TL Series Demo Usage Breakdown
| ADC Version | Corresponding Chip Model |
|---|---|
| ADC_V1.0 | B91 / B92 |
| ADC_V1.1 | TL321x / TL721x / TL751x |
| ADC_V1.2 | TL322x |
Working Principle
This section introduces the principles common to all architectures; architecture-specific FIFO details and considerations are explained in the respective architecture sections.
Differential Input and Measurement Range
The ADC uses a differential input architecture: the positive phase (P) is connected to GPIO or Vbat (via a voltage divider), and the negative phase (N) is connected to GPIO or internal GND (connected to GND in single-ended mode).
graph LR
GPIO_P["GPIO"] --> MUX_P
Vbat["Vbat"] --> Div["Vbat Divider"] --> MUX_P
GPIO_N["GPIO"] --> MUX_N
GND["interior GND"] --> MUX_N
MUX_P{{"Positive Input<br>Channel Select"}} --> Pre_P["Pre-scale"]
MUX_N{{"Negative Input<br>Channel Select"}} --> Pre_N["Pre-scale"]
Pre_P -->|"Differential Signal"| Comp["SAR Comparator"]
Pre_N -->|"Differential Signal"| Comp
Vref["Vreference<br>0.9V / 1.2V"] -->|"Reference Voltage"| Comp
Comp --> Out["adc_code"]
classDef mux fill:#e1f5ff,stroke:#333,stroke-width:2px;
classDef comp fill:#ffe1e1,stroke:#333,stroke-width:2px;
class MUX_P,MUX_N mux;
class Comp comp;
Enable prescaling (pre_scale) when measuring voltages above Vref. The actual maximum measurable voltage is:
V_max = Vref / pre_scale
For example, when Vref = 1.2 V and pre_scale = 1/4, Vbat can be measured up to 4.8 V, while GPIO, which is most affected by the I/O input capability, can be measured up to 3.3 V.
Range Planning: Select pre_scale based on the target voltage, ensuring that V_target ≤ Vref / pre_scale. For example, using the TL721x:
| Target voltage | Vref | pre_scale | vbat divider | Measurable Range |
|---|---|---|---|---|
| 3.3V GPIO | 1.2V | 1/4 | 1 | 0~3.3V |
| Within 1.2V | 1.2V | 1 | 1 | 0~1.2V |
| Vbat | 1.2V | 1 | 1/4 | 1.9~4.3V |
Note on External Voltage Divider Circuits:
- When the target voltage exceeds the Vref range, in addition to using the internal pre_scale, you can also use an external resistor to divide the voltage. The selection of the voltage-dividing resistor directly affects accuracy:
- We recommend using a voltage divider with a resistance in the 100 kΩ range to balance power consumption and drive capability.
- Avoid using a voltage divider in the MΩ range, as a high-impedance source cannot fully charge the sampling capacitor (in the pF range) during the Capture phase, resulting in low measurement values; the higher the sampling frequency, the greater the deviation.
- If the GPIO sampling circuit includes an external voltage divider resistor, the user must perform board-level calibration.
- If a high-impedance source must be used, reduce the sampling frequency.
- The
pre_scaleparameter affects both VBAT and GPIO sampling; thevbat_dividerparameter, however, applies only to the VBAT internal divider and does not affect GPIO sampling.
Reference Voltage and Calibration
The default Vref is 1.2V; it is recommended to use this value to achieve a wider dynamic range. Factory calibration is based on 1.2V; if you switch to 0.9V, recalibration is required.
Due to process variations, the actual Vref deviates from the ideal value. The ATE testing writes gain calibration coefficients into the driver. The voltage calculation formula is:
Note
- When performing board-level calibration, use the original code and write the calibration data to the address recommended by the manufacturer. If you specify a custom calibration address, remove the original ADC calibration interface from the driver to prevent conflicts.
Sampling State Machine
The conversion process for each conversion is: Set (configure the channel) -> Capture (charge the sampling capacitor) -> Hold (hold) -> SAR comparison -> Write data to FIFO/register.
In multi-channel mode, the state machine automatically cycles through the M->L->R sequence, with each channel completing the above process independently.
Note
- For sampling high-impedance sensors,high-impedance sources (MΩ-level voltage dividers) have weak drive capability and cannot fully charge the sampling capacitor during the capture phase, resulting in low measured values; the higher the sampling frequency, the greater the deviation. For specific solutions for each architecture, refer to the corresponding sections. We recommend using a voltage divider with a resistance in the 100 kΩ range.
Clock
The ADC's analog operating clock must be divided to 4 MHz (the divisor is configured in adc_init(); the default is 24 MHz / (1 + 5)). Some chips have specific clock requirements; refer to the section on the corresponding architecture.
Power-On Stabilization Delay
After the ADC is powered on, the analog reference LDO and sampling capacitor require time to reach a steady state; wait before taking the first sample (Source: comments in the adc_power_on() function for each chip’s driver):
| Chip | Delay after power on |
|---|---|
| TL321x | >100us |
| TL721x | >200us(GPIO)/ >300us(Vbat) |
| TL751x | >200us |
Sign Bit Determination
In single-ended mode (negative terminal connected to GND), ground bounce noise may cause the ADC to capture a faint negative voltage. If the sign bit is not stripped, this faint negative voltage is interpreted as a very high positive voltage (spike). After data acquisition, check the sign bit and force the single-ended negative voltage to zero. Sign bit position: BIT(13) for 14-bit, BIT(11) for 12-bit.
ADC in DFIFO Architecture
FIFO Architecture
Uses the DFIFO architecture: Before sampling, you must manually allocate a 16-byte-aligned block of RAM (such as adc_data_buf[8]), call adc_config_misc_channel_buf() and dfifo_enable_dfifo2(), and the ADC hardware automatically fills the data into the buffer. After reading is complete, you must disable the DFIFO.
DFIFO Conflict Note: Continuous sampling relies on DFIFO (DFIFO1 or DFIFO2); if used simultaneously with audio services, they must be staggered.
Demo Usage
GPIO Sampling (NDMA):
adc_init();
adc_base_init(GPIO_PB2);
adc_power_on_sar_adc(1);
unsigned int mV = adc_sample_and_get_result(); // Performs 8-point filtering internally and returns the result directly in mV
The adc_sample_and_get_result() function internally implements an 8-point insertion-sort filter and returns the result in mV, eliminating the need for manual code conversion. If you do not require the internal filtering, you can use adc_sample_and_get_result_manual_mode() to retrieve a single raw code value.
Note
- In manual mode, this interface stops the digital pipeline by writing
NOT_SAMPLE_ADC_DATAtoadc_data_sample_control, then reads the hardware analog register (areg_adc_misc_l/h) to assemble the raw data. After reading, the flag must be reset to 0. If the pipeline is not stopped before reading the register, garbled data is read.
High-Impedance Source: When measured values are low, reduce the sampling frequency (e.g., from 96K to 23K); the hardware automatically extends the capture cycle.
Sign bit: Taking B80 as an example: 14-bit resolution, the sign bit is BIT(13), and single-ended negative voltages must be set to zero (see Sign Bit Determination).
Vbat Sampling: The B87 chip samples Vbat using a voltage divider with pre_scale=1/4 and vbat_div disabled (for other chips, pre_scale = 1 and divider=1/4).
Get Voltage Value
The adc_sample_and_get_result() function encapsulates the entire process: 8-point buffering -> DFIFO streaming -> sign bit processing -> insertion sort -> calculating the arithmetic mean of the 2nd, 3rd, 4th, and 5th code values -> calibration and conversion to mV.
Manual mode is available via the adc_calculate_voltage(adc_code) conversion.
Sleep Power Control
Before entering suspend, deep, or deep retention modes, you must call adc_power_on_sar_adc(0) to turn off the power; otherwise, standby power consumption exceeds the limit, or the ADC does not respond after waking up. After waking up, reinitialize the ADC and wait >100 μs before taking the first sample.
ADC in Audio FIFO Architecture
FIFO Architecture
Reuse the Audio FIFO (0/1); in NDMA mode, data is read directly, while in DMA mode, it is transferred via the Audio FIFO.
Demo Usage
GPIO Sampling (NDMA):
adc_gpio_sample_init(ADC_M_CHANNEL, adc_gpio_cfg_m);
adc_power_on();
unsigned short code = adc_get_code(); // get initial code
unsigned short mV = adc_calculate_voltage(code); // convert voltage
DMA Sampling:
adc_set_dma_config(chn);
adc_start_sample_dma(buf, len);
The adc_set_dma_config(chn, fifo_chn) function for the B92 chip requires that the Audio FIFO channel be specified separately.
Vbat Sampling:
| Chip | pre_scale | vbat_div |
|---|---|---|
| B91 | 1/4 | OFF |
| B92 | 1 | 1/4 |
The configurations for B91 and B92 are opposite: B91 uses voltage division via pre_scale=1/4 with vbat_div disabled; B92 uses hardware voltage division via vbat_div=1/4 with pre_scale=1. This has been encapsulated within the driver.
B91 Vbat Limit: Must be configured via sys_init() for the battery voltage < 3.6V mode. Low accuracy; intended for battery level monitoring only. For precise sampling or when the battery voltage exceeds 3.6V, use GPIO sampling with an external voltage divider (recommended: 3:4 division, total resistance of 400 kΩ, no capacitor, sampling frequency < 48 kHz).
B92 Vbat Limit: When the GPIO voltage is configured as GPIO_VOLTAGE_1V8 in sys_init(), Vbat sampling is unavailable; an external voltage divider must be used instead.
High-Impedance Source: When measured values are low, reduce the sampling rate (e.g., from 96K to 23K); the hardware automatically extends the capture cycle.
Get Voltage Value
adc_calculate_voltage(unsigned short adc_code) returns mV.
Recommended workflow: Read code -> Sign bit processing (BIT(13), see Sign Bit Determination) -> Multipoint filtering -> adc_calculate_voltage().
Sleep Power Control
Must call adc_power_on() before the PM enters sleep mode to prevent unpredictable issues, such as abnormal sampling after waking up, that could result from failing to shut down the ADC.
After DEEP/DEEP RETENTION wake-up: The SAR ADC must be reinitialized, and wait for the reference voltage to stabilize.
SUSPEND Wake-up and Resume Process: adc_power_on() -> Wait for stabilization -> sar_adc_sample_start()
ADC in Independent FIFO Architecture
FIFO Architecture
It features a dedicated ADC hardware FIFO (separate SAR_ADC_RX). Supports NDMA (register polling/FIFO single-sample) and system-level DMA (automatic bus transfer triggered by the trigger threshold trig_cnt). During multi-channel DMA transfers, data is interleaved in RAM (M-L-R).
Note
- TL322x Clock: The high-speed SAR ADC requires a 48 MHz clock; the system clock source must be a PLL, and the PLL configuration must be divisible by 48; otherwise, sampling errors occur.
- TL322x Dual ADC: The first parameter of most APIs has been expanded to include
adc_num_e sar_adc_num(ADC_SAR0/ADC_SAR1).
Demo Usage
(1) GPIO Sampling (NDMA Single-channel)
// 1. Global Initialization (NDMA Mode, M-Channel Only)
adc_init(NDMA_M_CHN);
// 2. Configuring M-Channel Pins and Parameters
adc_gpio_cfg_t cfg = {
.v_ref = ADC_VREF_1P2V,
.pre_scale = ADC_PRESCALE_1F4, // 1/4 voltage divider, range 0–4.8 V
.sample_freq = ADC_SAMPLE_FREQ_96K,
.pin = ADC_GPIO_PB0,
};
adc_gpio_sample_init(ADC_M_CHANNEL, cfg);
// 3. Power On -> Start Sampling -> Read Code -> Convert to Voltage
adc_power_on();
adc_start_sample_nodma();
unsigned short buf[8];
for (int i = 0; i < 8; ) {
if (adc_get_rxfifo_cnt() > 0) {
unsigned short code = adc_get_raw_code();
if (code & BIT(11)) { // A sign bit of 1 indicates negative voltage; single-ended returns zero.
code = 0;
} else {
code &= 0x7FF; // Take the lower 11 valid bits
}
buf[i++] = code;
}
}
// 4. Filtering + Voltage Conversion (see [Get Voltage Value](#get-voltage-value-2))
FIFO Clear: adc_start_sample_nodma() internally calls adc_clr_rx_fifo_cnt() to clear the FIFO. If you manually restart sampling by manipulating the registers, you must explicitly call adc_clr_rx_fifo_cnt(); otherwise, the first code read returns corrupted data.
(2) DMA Multi-channel Sampling
Suitable for high-frequency continuous data acquisition; supports M/L/R multi-channel hardware polling; after DMA transfer, the data is interleaved in RAM (M-L-R-M-L-R...).
// switch in app_config.h
// #define ADC_MODE ADC_DMA_MODE
// #define ADC_SAMPLE_CHN_CNT DMA_M_L_R_3_CHN_EN // 3-channel
// 1. Configure the DMA channel and register the interrupt
adc_set_dma_config(DMA6);
dma_set_irq_mask(DMA6, TC_MASK);
plic_interrupt_enable(IRQ_DMA);
core_interrupt_enable();
// 2. Global Initialization (Select Enumeration by Number of Channels)
adc_init(DMA_M_L_R_CHN); // 3-channel scanning
// 3. Configure the M, L, and R channels separately
adc_chn_cfg_t cfg_m = { .pre_scale=ADC_PRESCALE_1F4, .sample_freq=ADC_SAMPLE_FREQ_96K,
.input_p=GPIO_M_CHNP_SAMPLE_SIGNAL, .input_n=GPIO_M_CHNN_SAMPLE_SIGNAL };
adc_chn_cfg_t cfg_l = { /* ... */ };
adc_chn_cfg_t cfg_r = { /* ... */ };
adc_channel_sample_init(ADC_GPIO_SAMPLE, ADC_M_CHANNEL, &cfg_m);
adc_channel_sample_init(ADC_GPIO_SAMPLE, ADC_L_CHANNEL, &cfg_l);
adc_channel_sample_init(ADC_GPIO_SAMPLE, ADC_R_CHANNEL, &cfg_r);
// 4. Start DMA Sampling
short sample_buffer[GROUP_CNT * 3] __attribute__((aligned(4)));
adc_start_sample_dma((short *)sample_buffer, (GROUP_CNT * 3) << 1);
// 5. Clear the interrupt flag; split the data in the main loop
void dma_irq_handler(void) {
if (dma_get_tc_irq_status(BIT(DMA6))) {
adc_dma_rx_done_flag = 1;
adc_clr_irq_status_dma();
}
}
// 6. Split Interleave Data: sample_buffer[j*chn_cnt + i] -> channel_buffers[i][j]
adc_code_split_dma(sample_buffer, GROUP_CNT, 3, channel_buffers);
NDMA mode supports only the M channel; for multiple channels, DMA mode must be used.
DMA Usage Notes:
- DMA Channel Selection: The channel number is specified by
adc_set_dma_config(chn); select a channel that is not already in use. - Trigger Depth: Set
adc_set_rx_fifo_trig_cnt(0)to 0; DMA triggers immediately as soon as data arrives in the FIFO. - Start Sequence: Call
adc_set_dma_config()first, followed byadc_start_sample_dma(). If the state machine starts before the DMA is ready, the data order shifts (from M-L-R to R-M-L). - Interrupt Handling: After entering the DMA completion interrupt, the first line of code must stop hardware streaming, clear any residual FIFO data, and then re-enable it.
- Data Splitting: Split the data into the buffers for each channel according to
sample_buffer[j*chn_cnt + i].
DMA Hardware-Software Interaction Timing:
sequenceDiagram
participant CPU as business layer (CPU)
participant ADC as SAR ADC state machine
participant DMA as DMA bus controller
participant RAM as system SRAM
CPU->>ADC: Initialize the parameters and power on to start scanning
loop Hardware-Autonomous Operation
ADC->>ADC: Cycle through the modes in the order M->L->R
ADC->>DMA: FIFO generates data and initiates a transport request
DMA->>RAM: Store the interleaved half-word data in the target buffer pool
end
DMA->>CPU: When the specified transfer length is reached, a DMA interrupt is triggered.
Note over CPU: During an interrupt: Pause the state machine -> Clear flags -> Unpack data -> Restart the pipeline
(3) Vbat Sampling
The independent FIFO architecture provides two Vbat sampling methods:
Method 1: Internal Vbat Channel (Recommended; supports the full range of 1.9–4.3 V)
adc_init(NDMA_M_CHN);
adc_vbat_sample_init(ADC_M_CHANNEL); // Connect to the internal voltage divider
adc_power_on();
adc_start_sample_nodma();
// After reading the code, use `adc_calculate_voltage(ADC_VBAT_SAMPLE, chn, code)` to convert voltage.
The driver is internally encapsulated: the Vbat virtual pin is automatically connected; the hardware voltage divider is configured for a 1/4 division; the prescaler is set to 1x (no division); voltage calculations automatically compensate for a 4x division factor; and proprietary Vbat calibration values are used. The sampling range is 1.9 ~ 4.3 V.
Method 2: Indirect Vbat Sampling via GPIO (Applicable only to 1.9 V to 3.6 V power supplies)
adc_gpio_cfg_t cfg = { .v_ref=ADC_VREF_1P2V, .pre_scale=ADC_PRESCALE_1F4, ... };
adc_gpio_sample_vbat_init(ADC_M_CHANNEL, cfg);
Indirect measurement via a GPIO channel with a pre-scale voltage divider is suitable for applications with a supply voltage < 3.6V. When the supply voltage exceeds 3.6V, Method 1 should be used or an external voltage divider circuit should be implemented.
Get Voltage Value
adc_calculate_voltage(adc_sample_chn_e chn, unsigned short adc_code) requires the channel number (calibration coefficients are independent for each channel) and returns a value in mV.
For the TL322x dual ADC, you must also pass the sample type (GPIO / Vbat) and adc_num_e sar_adc_num (ADC_SAR0 / ADC_SAR1).
Recommended workflow: Retrieve code -> Check the sign bit (BIT(11), see Sign Bit Determination) -> Multipoint filtering -> adc_calculate_voltage(). The adc_get_result() function in the demo encapsulates the entire process.
Filtering Recommendation: The number of sampling points, ADC_SAMPLE_GROUP_CNT, must be a multiple of 8; 8 or 16 is recommended. Setting it too high carries a risk of accumulation overflow.
High-Impedance Sources: When measured values are low, reduce the sampling frequency (e.g., from 96K to 23K); the hardware automatically extends the capture period.
Sleep Power Control
Before entering suspend, deep, or deep retention modes, you must call adc_power_off() to turn off the LDO. If power is not turned off, the result could range from standby power consumption exceeding limits to, in the worst case, the analog LDO locking up, causing the ADC to be completely unresponsive after wake-up. After waking from deep sleep, reinitialize the system and wait for the stabilization delay (see Power on stabilization delay) before performing the first sample.
stateDiagram-v2
state "Active (Normal work)" as Active
state "Pre-Sleep (Power-off before sleep)" as PreSleep
state "Suspend / Deep (Sleep)" as Sleep
state "Wakeup" as Wakeup
Active --> PreSleep : System Initiates Sleep
note right of PreSleep
Suspend DMA, call adc_power_off()
end note
PreSleep --> Sleep : Enter low power
Sleep --> Wakeup : Timer/GPIO wakeup
note left of Wakeup
adc_power_on() + Wait for stable delay
Then take the first sample
end note
Wakeup --> Active : Resume sampling
Troubleshooting
Accuracy and Error
Measurements Are Too Low?
- High-impedance source: When measurements are too low, reduce the sampling frequency (e.g., 96K → 23K); the hardware automatically extends the capture cycle. Switch to a voltage divider resistor in the 100 kΩ range.
- Low readings during the first few samples after wake-up: Add a delay after
adc_power_on()to discard the first few sample values.
Voltage calculations show deviations in the range of 100 mV?
Possible cause: Incorrect use of the Vref calibration coefficient.
Are there occasional spikes or glitches in the sample values?
The sign bit has not been processed. Check the sign bit after code extraction; force single-ended negative voltages to zero.
Are the sample values highly volatile or unstable?
Increase the number of filtering points (8 or 16); check for power supply ripple; route analog signals on the PCB away from high-frequency digital signals.
Data Anomalies
Is the data consisting entirely of background noise around 0?
The GPIO has not been properly switched to analog mode, or it is being multiplexed and preempted by another peripheral.
Is the data garbled in manual mode for the DFIFO architecture?
You are reading the register without stopping the system. Set adc_data_sample_control to 1 to lock the system before reading, then restore it after reading.
Garbled data in a single sample in the standalone FIFO architecture?
The FIFO is cleared internally by adc_start_sample_nodma(). If you manually manipulate registers to restart sampling, you must explicitly call adc_clr_rx_fifo_cnt().
Data out of order in DMA mode (M-L-R becomes R-M-L) or packet loss?
- The state machine was started before the DMA channel was ready. Ensure that
adc_set_dma_config()is called beforeadc_start_sample_dma(), and the interrupt entry point must clear the interrupt status. - Refer to the demo code for the configuration sequence.
Power Consumption and Sleep Mode
Is the ADC completely unresponsive after waking from sleep?
The analog power domain was not completely shut down before entering sleep mode, causing the LDO to lock up. You must call the power-off interface before entering sleep mode (for DFIFO architecture: adc_power_on_sar_adc(0); for Audio FIFO / standalone FIFO architecture: adc_power_off()).
Standby power consumption exceeds limits?
Check whether the ADC power-off interface was called before entering sleep mode and whether DMA has been suspended.
Porting and Compilation
Compilation errors after changing the chip?
API interfaces vary by architecture. Check the architecture table in Architecture Category and refer directly to the demo for the corresponding chip.
Abnormal Vbat sampling values?
Check whether the pre_scale and vbat_div configurations meet the architecture requirements.
SD ADC
SD ADC principle
Sigma-Delta ADC (Σ-Δ ADC) achieves high-precision analog-to-digital conversion through three steps: oversampling + noise shaping + digital filtering:
-
Oversampling: Sampling the input signal at a rate much higher than the Nyquist frequency, dispersing quantization noise across a wider frequency band
-
Noise shaping: Sigma-Delta modulators push quantized noise to higher frequencies, significantly reducing low-frequency noise
-
Digital filtering: The digital filter decimates the output of the high-speed modulator, removes high-frequency noise, and outputs a high-precision, low-position, wide-range final result
Comparison with SAR ADC
| Characteristics | SD ADC | SAR ADC |
|---|---|---|
| Accuracy | High | Average |
| Sampling rate | Slower (kHz level) | Faster (MHz level) |
| Signal-to-Noise Ratio (SNR) | High | Average |
| Power consumption | Higher | Lower |
| Hardware complexity | High (requires modulator + digital filter) | Low (Comparator + DAC only) |
| Applicable scenarios | High-precision, low-frequency scenarios such as blood glucose meters | Scenarios requiring high-speed sampling, such as keyboards and audio |
To facilitate hardware selection and driver architecture design, the individual chip model organizes the following table, clearly distinguishing each chip's support for SAR ADC (Progressive Approximation Analog-to-Digital Converter) and SD ADC (Σ-Δ Analog-to-Digital Converter):
| Chip model | SAR ADC | SD ADC |
|---|---|---|
| TLSR820x/TLSR8373 | ✅ | ❌ |
| TLSR825x/TLSR8359 / TLSR827x/TLSR8355 | ✅ | ❌ |
| TC122x / TC123x | ✅ | ❌ |
| TLSR921x/TLSR951x / TLSR922x/TLSR952x | ✅ | ❌ |
| TL321x / TL721x / TL751x | ✅ | ❌ |
| TL322x | ✅ | ✅ |
| TC321x / TL323x | ❌ | ✅ |
Core differences:
- SD ADC: Trades oversampling and noise shaping for high precision, suitable for scenarios requiring high accuracy but not high speed requirements.
- SAR ADC: Achieves rapid conversion through sequential approximation, suitable for scenarios requiring high sampling rates and moderate accuracy.
Summary table of chip functional differences
This chapter summarizes the differences in SD ADC functionality across various chips.
Feature overview
| Characteristics | TC321x | TL322x | TL323x |
|---|---|---|---|
| Sampling clock sources | sys_clk | pclk | pclk |
| Clock crossover formula | sys_clk/2/(clk_div+1) | pclk/2/(clk_div+1) | pclk/2/(clk_div+1) |
| Sampling clock frequency | 1M / 2M | 1M / 2M | 1M / 2M |
| Reduce sampling rates | 64 / 128 / 256 | 64 / 128 / 256 | 64 / 128 / 256 |
| Sampling mode | GPIO / VBAT | GPIO / VBAT | GPIO / VBAT |
| Data acquisition methods | NDMA Polling / NDMA Interrupt | NDMA Polling / DMA Interrupt | NDMA Polling / DMA Interrupt |
| GPIO positive input (P) | PB0 ~ PB7, PD0 ~ PD1 | PC0 ~ PC5, PB4 ~ PB7 | PB0 ~ PB7, PD0 ~ PD1 |
| GPIO negative input (N) | PB0 ~ PB7, PD0 ~ PD1, GND | PC0 ~ PC5, PB4 ~ PB7, GND | PB0 ~ PB7, PD0 ~ PD1, GND |
| With Buffer pins | Only B0: PB5, PB6 | None | PB5, PB6, PB7, PD0, PD1 |
| GPIO pressure divider configuration | P/N channels are independent | Unified gpio_div | Unified gpio_div |
| GPIO detection range¹ | Generally | Generally | Generally |
| VBAT 1/2 pressure division range | 2.0 ~ 2.4V | — | — |
| VBAT 1/4 pressure division range | 2.0 ~ 3.6V | 1.7 ~ 4.5V | 2.0 ~ 4.5V |
| Wait time after powering on | > 160us | No need to wait | 200us |
| Factory calibration parameters | True difference + false difference | Only the false difference is used | True difference + false difference |
Note:
GPIO voltage division detection range (applicable to all chips):
- 1 (OFF): 0 ~ 1.2V;
- 1/2: Standard pin 0 ~ 2.4V, with buffer pin 0 ~ (VBAT×60%) and ≤ 2.4V;
- 1/4: Standard pin 0 ~ 3.3V, with buffer pin 0 ~ (VBAT×60%) and ≤ 3.3V.
- All pins in the A0/A1/A1S1 versions of the TC321x are standard pins; In the B0 version, PB5/PB6 are Buffer pins, while the others are standard pins.
Buffer Pin Description: Pins with buffer isolate internal equivalent voltage divider resistors. When using external partial-voltage resistors for sampling, preferably select buffer pins to prevent measurement errors caused by voltage division of the internal equivalent resistors.
GPIO pins and detection range
GPIO sampling is the operating mode of SD ADCs that acquire external analog signals through the General Input/Output (GPIO) pin and is one of the most commonly used functions. In this mode, the ADC can be configured for single-ended (pseudo-differential) or true differential inputs, and by selecting different GPIO pin combinations and voltage divider coefficients, it achieves high-precision measurement of external voltage signals. Suitable for various scenarios such as battery voltage monitoring and sensor signal collection.
For the pins, buffer pins, voltage division configurations, and detection range supported by each chip, please refer to Feature overview.
- When configured as
SD_ADC_GNDNon the N-end, it is called pseudo-differential (single-ended); when configured on the N-terminal as other GPIOs, it is called true differential. - In GPIO true differential mode, it is recommended to use only 10%-90% of the sampling range.
VBAT mode
VBAT sampling is the SD ADC’s mode for collecting the chip supply voltage (usually battery voltage) via an internal dedicated channel, primarily used for battery-level monitoring and low-voltage protection. This mode does not require external GPIO pins and samples directly via the chip’s internal connection to the power management module, with a simple hardware design and strong anti-interference capability.
For recommendations on detection range and pressure division, please refer to Feature overview.
Voltage division selection recommendation: 1/4 voltage division is recommended because the calibration parameters before leaving the chip are calibrated based on this scenario, allowing for the best accuracy directly. If using other voltage division coefficients, it is recommended to recalibrate them to ensure measurement accuracy.
Hardware architecture
Clock architecture
The sampling clock is obtained by dividing the clock source; it supports both 1M and 2M sampling clock frequencies.
Choosing between 1M and 2M:
- 1M: Low power consumption; combined with a downsampling rate of 128, it achieves a sampling rate of about 7.8 kHz, meeting most low-frequency, high-precision scenarios;
- 2M: Higher power consumption, double sampling rate, suitable for scenarios requiring faster response or higher oversampling ratios.
Clock configuration macros vary by chip; for details, please refer to API explanation to the API description for initialization interfaces.
For clock sources and frequency division formulas, please refer to Feature overview.
Digital filters
The digital filter downsamples the high-speed output of the modulator and is a key step in SD ADC accuracy. The downsampling factor (downsampling rate) directly affects accuracy and sampling rate:
| Downsampling rate | Sampling rate (1M clock) | Sampling rate (2M clock) | Applicable scenarios |
|---|---|---|---|
| 64 | ~ 15.6 kHz | ~ 31.3 kHz | A higher sampling rate is required |
| 128 | ~ 7.8 kHz | ~ 15.6 kHz | Balancing accuracy and rate (recommended) |
| 256 | ~ 3.9 kHz | ~ 7.8 kHz | The highest precision is required |
FIFO and data acquisition methods
SD ADC sampling data first enters the RX FIFO, then is transferred to SRAM through one of the following methods:
| Method | Applicable chips | Principle | Features |
|---|---|---|---|
| FIFO reads directly | TC321x | The hardware automatically writes FIFO data to a specified SRAM address, and upon reaching a threshold, issues status flags or interrupt notifications | The FIFO base address, depth, and threshold must be set |
| NDMA polling read | TL322x/TL323x | The CPU reads the FIFO register directly and checks whether data exists through query rxfifo_cnt |
Simple but CPU-intensive |
| DMA Transfer | TL322x/TL323x | The DMA controller automatically transfers FIFO data to SRAM, triggering an interrupt after completion | Efficient and CPU-free |
TC321x FIFO parameter description:
- FIFO depth: must be 2^n (n=3 ~ 10), i.e., 8/16/32/.../1024;
- Interrupt threshold: must be a multiple of 4, maximum 1020, and must be at least 1 word less than the depth;
- Trigger condition: Data volume in FIFO > threshold.
TL322x/TL323x FIFO parameter description:
- RX FIFO depth fixed at 16 words;
- In NDMA mode, FIFO data volumes are queried using
sd_adc_get_rxfifo_cnt()and readsd_adc_get_raw_code() one by one; - In DMA mode, the number of triggers is fixed at 0 (i.e., 1 data point is reposted), so modification is not recommended.
Resource sharing and audio compatibility
SD ADC and audio AMIC share the analog frontend; they cannot be used simultaneously and can only be used for time-sharing purposes; DMIC can be used together with SD ADC.
TL323x A0 version notes:
- When using ADC, the PLL, LPD, and LPC functions are automatically enabled. During use, the registers of these modules cannot be operated; after use,
sd_adc_power_off()must be called to recover. - If the chip power supply is below 2.3V, GPIO sampling mode is not recommended. The VBAT sampling mode is greatly affected by temperature; each 5-degree change introduces an error of about 10 mV. (A1 version has fixed this issue)
API explanation
sd_adc_init()
Initialize the SD ADC module to enable the data-weighted averaging algorithm (to improve performance); the TL series requires specifying the operating mode (currently, only SD_ADC_SINGLE_DC_MODE is supported).
sd_adc_power_on() / sd_adc_power_off()
void sd_adc_power_on(sd_adc_mode_e mode); // mode: SD_ADC_SAMPLE_MODE
void sd_adc_power_off(sd_adc_mode_e mode);
Key Notes
- Before entering PM sleep, you must call 'sd_adc_power_off()'; otherwise, the ADC may exhibit unpredictable behavior such as abnormal sampling after waking.
- After waking up from DEEP/DEEP RETENTION, TC321x requires reconfiguring all SD ADC registers.
sd_adc_gpio_sample_init()
Initialize the GPIO sampling mode and configure structures based on the chip. For key parameter differences, please refer to the Feature overview:
- TC321x:
input_p/input_n/p_div/n_div(P/N分压独立)/clk_div/downsample_rate - TL322x/TL323x:
input_p/input_n/gpio_div(统一分压)/clk_freq/downsample_rate
sd_adc_vbat_sample_init()
void sd_adc_vbat_sample_init(unsigned char clk_div, sd_adc_vbat_div_e div, sd_adc_downsample_rate_e downsample_rate);
-
clk_div: Sampling clock divider coefficient, used to configure the clock frequency for VBAT sampling. It is recommended to select a value from the chip manual that supports a sampling clock of 1M or 2M; refer to the frequency division formula in the Clock architecture for details. -
div: VBAT pressure division coefficient selection, type is enumerated sd_adc_vbat_div_e. It is recommended to use a quarter-voltage divider (such as SD_ADC_VBAT_DIV_1F4), because the chip’s factory calibration parameters are calibrated for this scenario, allowing for the best accuracy directly. -
downsample_rate: Select the downsampling rate; the type is enumeration sd_adc_downsample_rate_e. Recommended SD_ADC_DOWNSAMPLE_RATE_128; this parameter balances accuracy and sampling rate, meeting the needs of most application scenarios.
sd_adc_sample_start() / sd_adc_sample_stop()
Switch sequence specification:
On: sd_adc_power_on() -> Wait for stability -> sd_adc_sample_start()
Off: sd_adc_sample_stop() -> sd_adc_power_off()
After each call to sd_adc_sample_start(), the first four sampled data points are abnormal due to internal filter reset and must be discarded.
sd_adc_set_irq_trig_thres() (TC321x exclusive)
static inline void sd_adc_set_irq_trig_thres(unsigned int threshold);
Set the FIFO interrupt trigger threshold (unit: word, must be a multiple of 4, maximum 1020).
- Polling mode: query
sd_adc_get_irq_status(),flag set, then read data from buffer. - Interrupt mode: An interrupt is triggered when the FIFO data volume reaches a threshold, and data is read from the interrupt service function.
sd_adc_get_result()
This function is not a built-in API driver, but a reference implementation provided in the demo, encapsulating the complete workflow of "obtaining sampled data - > sorting filtering - > conversion results." Users can directly refer to or modify it.
signed int sd_adc_get_result(sd_adc_result_type_e result_type);
Parameters:
| result_type | Description | Return value |
|---|---|---|
SD_ADC_VOLTAGE_10X_MV |
Voltage, unit: 0.1mV | For example, 33000 means 3.3V |
SD_ADC_VOLTAGE_MV |
Voltage, unit: mV | For example, 3300 represents 3.3V |
Internal Process:
- Obtain sampling data to buffer (TC321x reads from SRAM buffer, TL series reads from FIFO or DMA buffer)
- Call
sd_adc_sort_and_get_average_code()to sort and filter to get the average code - Call
sd_adc_calculate_voltage()to convert to the final result
TL323x Special Processing: In GPIO mode, sd_adc_get_result() will internally call sd_adc_div_switch_adjust_rescale() to automatically adjust the crossover range. When a voltage < 50 mV is detected, it switches to a 1:1 crossover to improve resolution; when > 1000 mV, it switches back to a quarter divider to prevent overflow. After switching, the sample will be automatically resampled.
Data processing
Sorting and filtering algorithm (provided in the demo)
The sd_adc_sort_and_get_average_code() provided in the demo uses insertion sort and a depolarized average method. After sorting the original data in ascending order, discard the first N/4 (minimum) and last N/4 (maximum), then take the middle N/2 to compute the average to obtain the code mean. The sample size must be a multiple of 4, with a recommended value of ≥ 16.
sd_adc_get_result (result_type) encapsulates the complete "data extraction -> filtering - > conversion" process, returning voltages in SD_ADC_VOLTAGE_10X_MV (0.1mV) or SD_ADC_VOLTAGE_MV (mV) formats. This function is not part of the driver’s built-in API, so users cannot directly refer to or modify it.
Voltage conversion formula
Basic formula (code converted to original voltage, not calibrated):
V_raw = code / OSR³ / 2 × divider
Where OSR = downsampling rate, divider = voltage division coefficient (8/div value)
Formula after calibration:
V_result = V_raw × vref / 10000 + offset (unit: 0.1mV)
V_result = V_raw × vref / 10000 / 10 + offset / 10 (unit: mV)
Calibration
The sampling accuracy of SD ADCs is affected by process deviations and requires two-point calibration to correct them. The essence of calibration is to compensate for the gain/vref and offset** of the ADC.
- Calibration principle
A method to establish linear equations to solve calibration parameters by collecting the ADC source code values (code1, code2) corresponding to two known standard voltages (V1, V2). The specific steps are as follows:
a. Acquisition standard points: input the high-precision reference voltages V1 (e.g., 0.5V) and V2 (e.g., 2.5V) respectively to obtain the corresponding ADC sampling code values code1 and code2.
b. Calculation of calibration parameters:
- Gain factor: `vref = (V2 - V1) / (code2 - code1).` To ensure maximum accuracy, this value is amplified 10,000 times when ATE writes the calibration value, so subsequent calculations require dividing by 10,000 to restore the actual gain factor.
- Offset: `offset = V1 - gain * code1`.
c. Application calibration: During actual measurement, calculate the actual voltage using V_result = vref * code / 10000 + offset. This method can effectively correct the linear errors of ADCs and is suitable for scenarios requiring high precision, such as sensor signal acquisition and battery voltage monitoring. When chips leave the factory, two-point calibration is usually completed through ATE devices, with gains and offsets stored in eFuse or Flash, and users can also calibrate them themselves.
- Default calibration value
The driver uses the ATE big data median as the default calibration value, ensuring that GPIO and VBAT sampling errors are within 50 mV when ATE is not calibrated.
- Calibration interface
Default calibration mechanism: Each chip automatically completes ATE calibration in platform_init() without user intervention.
Self-calibration steps:
-
Shield calls to the above ATE calibration interfaces
-
Revert the calibration variable to the initial value of "not calibrated":
g_single_sd_adc_vref/g_diff_sd_adc_vref/g_sd_adc_vbat_calib_vref= 10000g_single_sd_adc_vref_offset/g_diff_sd_adc_vref_offset/g_sd_adc_vbat_calib_offset= 0
Introduction to the calibration sub-interface:
| Subinterface | Applicable chips | Description |
|---|---|---|
sd_adc_set_single_gpio_calib_vref() |
All | Single-ended GPIO calibration |
sd_adc_set_vbat_calib_vref() |
All | VBAT two-point calibration |
sd_adc_set_diff_gpio_calib_vref() |
TC321x/TL323x | Differential GPIO calibration |
sd_adc_set_single_gpio_no_div_calib_vref() |
TL323x (A0 version) | Voltage-less (1:1) single-ended GPIO calibration |
sd_adc_set_vbat_4p_calib_vref() |
TL323x (A0 version) | VBAT four-point calibration (2.200/2.225/2.250/2.275V) |
sd_adc_set_vbat_2v2_calib_vref() |
TL323x (A0 version) | VBAT < 2.2V curve calibration |
Explanation
- Only the A0 version of the TL323x series supports segmental calibration in VBAT mode: 2.2 ~ 2.275V uses 4-point linear interpolation, < 2.2V uses quadratic curve fitting, and the rest uses linear calibration.
Precautions for use
The relationship between calibration and configuration
In GPIO mode, different GPIO pins or voltage division coefficients require separate calibration and cannot share the same set of calibration values. Before leaving the factory, chips are calibrated according to the driver's default configuration:
- Recommendation: Use the driver's default configuration and directly use factory calibration values;
- Modifications with high precision requirements: users need to calibrate themselves;
- Modifications with low precision requirements: factory calibration values can be used, but errors may increase.
The first four data points are abnormal
After each call to sd_adc_sample_start(), the internal digital filter is reset, so the first four sample codes are abnormal and must be discarded. It is recommended to set a sufficient sample size (≥16) and use sorting and filtering algorithms to mitigate the impact.
PM is dormant
Before entering PM hibernation, sd_adc_power_off() must be called to prevent unpredictable behavior, such as abnormal sampling upon waking, if not turned off.
- DEEP/DEEP RETENTION wake-up: The SD ADC needs to be reinitialized, and the reference voltage must stabilize
- SUSPEND wake-up recovery process:
sd_adc_power_on()→ Wait for stabilization →sd_adc_sample_start().
Audio compatibility
SD ADC and audio AMIC cannot be used simultaneously (sharing analog frontends); they can only be used in time-sharing mode, while DMIC can be used simultaneously.
Use examples
TC321x polling mode
sd_adc_gpio_cfg_t cfg = {
.input_p = SD_ADC_GPIO_PB0P,
.input_n = SD_ADC_GNDN,
.p_div = SD_ADC_GPIO_P_CHN_DIV_1F4,
.n_div = SD_ADC_GPIO_N_CHN_DIV_1F4,
.clk_div = SD_ADC_SAPMPLE_CLK_2M_DIV(CLOCK_SYS_CLOCK_HZ),
.downsample_rate = SD_ADC_DOWNSAMPLE_RATE_128,
};
signed int buffer[32] __attribute__((aligned(4))) = {0};
void user_init(void) {
sd_adc_init();
sd_adc_gpio_sample_init(&cfg);
sd_adc_set_rx_fifo((unsigned int *)buffer, 32); // Set the FIFO address and depth
sd_adc_set_irq_trig_thres(16); // Set thresholds
sd_adc_power_on(SD_ADC_SAMPLE_MODE);
sleep_us(160); // Wait for VMID to stabilize
sd_adc_sample_start();
}
void main_loop(void) {
if (sd_adc_get_irq_status()) {
sd_adc_sample_stop(); // Stop sampling to prevent data overwrite
// Processing data in buffer...
sd_adc_sample_start(); // Restart sampling
}
}
Initialization: init → sample_init → set_rx_fifo → set_thres → power_on → wait → sample_start
Main Cycle: irq_status? ──No──→ Continue polling ──Yes──→ sample_stop → Read buffer processing → sample_start → Return to polling
TC321x interrupt model
void user_init(void) {
// ... Initialization of the same polling mode...
sd_adc_set_irq_mask(); // Enable SD ADC interrupts
irq_set_mask(FLD_IRQ_DAM_FIFO_EN); // Enable FIFO interrupts
irq_enable();
sd_adc_power_on(SD_ADC_SAMPLE_MODE);
sleep_us(160);
sd_adc_sample_start();
}
_attribute_ram_code_sec_noinline_
void irq_handler(void) {
if (sd_adc_get_irq_status()) {
sd_adc_clr_irq_status(); // Clear interrupts (internally WPTR will clear and stop)
flag = 1; // Set flag positions and handle the main cycle
}
}
Initialization: init → sample_init → set_rx_fifo → set_thres → set_irq_mask → irq_enable → power_on → wait → sample_start
ISR: irq trigger → clr_irq_status(clear wptr + stop) → flag=1
Main loop: flag? ──No──→ Wait ──Yes──→ Read buffer processing → sample_start → flag=0 → Return to waiting
TL322x / TL323x NDMA polling mode
sd_adc_gpio_cfg_t cfg = {
.clk_freq = SD_ADC_SAPMPLE_CLK_2M,
.downsample_rate = SD_ADC_DOWNSAMPLE_RATE_128,
.gpio_div = SD_ADC_GPIO_CHN_DIV_1F4,
.input_p = SD_ADC_GPIO_PB6P, .input_n = SD_ADC_GNDN,
};
signed int buffer[16] __attribute__((aligned(4))) = {0};
void user_init(void) {
sd_adc_init(SD_ADC_SINGLE_DC_MODE);
sd_adc_gpio_sample_init(&cfg);
sd_adc_power_on(SD_ADC_SAMPLE_MODE);
#if defined(MCU_CORE_TL323x)
delay_us(200);
#endif
sd_adc_sample_start();
}
void main_loop(void) {
// Read each item from the FIFO
for (int i = 0; i < 16; ) {
if (sd_adc_get_rxfifo_cnt() > 0) {
buffer[i++] = sd_adc_get_raw_code();
}
}
// Processing data...
}
Initialization: init → sample_init → power_on → wait(TL323x) → sample_start
Main Cycle: rxfifo_cnt > 0? ──No──→ Continue waiting ──Yes──→ Read raw_code to buffer → Collect the specified amount and process the data → Return to loop
TL322x / TL323x DMA interrupt mode
void user_init(void) {
sd_adc_init(SD_ADC_SINGLE_DC_MODE);
sd_adc_gpio_sample_init(&cfg);
sd_adc_set_dma_config(DMA2); // Configure DMA channels
dma_set_irq_mask(DMA2, TC_MASK); // Enable DMA transmission interrupt
plic_interrupt_enable(IRQ_DMA);
core_interrupt_enable();
sd_adc_start_sample_dma(buffer, 16 << 2); // Start DMA sampling (16 words = 64 bytes)
sd_adc_power_on(SD_ADC_SAMPLE_MODE);
#if defined(MCU_CORE_TL323x)
delay_us(200);
#endif
sd_adc_sample_start();
}
_attribute_ram_code_sec_
void dma_irq_handler(void) {
if (dma_get_tc_irq_status(BIT(DMA2))) {
sd_adc_sample_stop();
sd_adc_rx_done_flag = 1;
sd_adc_clr_irq_status_dma();
}
}
PLIC_ISR_REGISTER(dma_irq_handler, IRQ_DMA)
Initialization: init → sample_init → set_dma_config → dma_irq_enable → start_sample_dma → power_on → wait(TL323x) → sample_start
ISR: DMA transmission completed → sample_stop → clr_irq_status_dma → rx_done_flag=1
Main Cycle: rx_done_flag? ──No──→ Wait ──Yes──→ Read buffer processing → start_sample_dma → sample_start → flag=0 → Return to waiting
USB
USB Common
Introduction to USB
USB (Universal Serial Bus) is an external bus standard used to regulate the connection and communication between computers and external devices. It is an interface technology applied in the PC field. The USB interface supports plug-and-play and hot-swappable functions for devices. USB was jointly proposed at the end of 1994 by Intel, Compaq, IBM, Microsoft, and several other companies. As shown in the figure below, USB consists of four wires: VCC, GND, D-(DM), and D+ (DP). USB can be powered by the host or by itself. Currently, most USB devices are powered by the main unit.

USB communication refers to communication between controllers and devices. The computer host is the controller, and Telink USB is the device. The host referenced later is the default controller. A USB bus is a unidirectional bus; communication can only be initiated by the controller. When the device receives a request from the controller, it sends data to the controller. The controller sends requests to the device every n units, where n is the user's configuration parameter.
USB has four operating speeds: ultra-high speed (5.0 Gbit/s), high speed (480 Mbit/s), full-speed (12 Mbit/s), and low speed (1.5 Mbit/s). The communication frame cycle for full-speed and low-speed USB buses (the interval between sending data between two consecutive frames) is 1ms, while for high-speed USB buses is 125 microseconds. USB 1.1 only supports full speed and low speed, USB 2.0 supports high speed, full speed, and low speed, and ultra speed is only supported in USB 3.0.
USB packet format and transfer process
A packet is the most basic unit of USB data transmission, meaning each data transfer is carried out in the form of a packet. A packet must be composed of transactions to enable effective communication. Depending on communication needs, there are various packets used to organize different transactions (IN, OUT, SETUP). One or more transactions form a single transmission (control transmission, batch transmission, terminal transmission, and isotime transmission).
A packet is the smallest unit of data transmitted on a USB bus and cannot be interrupted or interfered with, otherwise errors occur. Several packets form a single transaction, and even a single transaction cannot be interrupted, meaning it is a few packets belonging to a single transaction.
(1) USB package structure
A packet is the basic unit for information transmission in USB systems; all data is packaged and transmitted over the bus.
As shown in the figure below, a USB packet consists of seven parts: Sync Domain (SYNC), Packet Identifier (PID), Address Domain (ADDR), Endpoint Domain (ENDP), Frame Number Domain (FRAM), Data Domain (DATA), and Validation Domain (CRC). Note that not every USB packet contains all seven domains mentioned above; in other words, some packages only contain a few of them.

1) Synchronous domain
The synchronization domain mainly notifies the other party of the start of data transmission and provides a synchronization clock. For low-speed and full-speed devices, the synchronous domain uses 0000 0001 (binary number); For high-speed equipment, the 00000001 used is 000000000000000000000.
2) Packet Identifier (PID)
Packet identifier is mainly used to identify the type of packet and consists of 8 bits: the lower 4 bits are PID codes, the upper 4 bits are the validation fields, which are inverted from the lower 4 bits. In USB, various packets are distinguished by PID fields.
3) Address field
Since there may be multiple devices connected to the USB bus, an address domain needs to be introduced to distinguish which device is currently communicating. The address field contains 7 data bits, with up to 128 addresses specified. Address 0 is used as the default address and is not assigned to USB devices. For each device on the USB bus, the address is unique.
4) Endpoint field
The endpoint field specifies an endpoint number for a device on the USB bus, containing 4 data bits; Full-speed/high-speed devices can contain up to 16 endpoints, while low-speed devices can have up to 3 endpoints. All USB devices must contain a terminal with a terminal number 0 for exchanging basic information between the host and the device. Except for endpoint 0, all other endpoints are specific to specific USB devices. The combination of address and endpoint domains clarifies the communication channel between the host and the device.
5) Frame number field
The frame number field indicates the frame number of the current frame. It is only sent in the SOF token packet at the start of each frame/microframe, with a data bit length of 11 bits. For each transmitted frame, the host increments it by 1, and it resets to zero when it reaches a maximum of 7 FFH.
6) Data field
The data field contains the data to be transferred between the host and USB devices, measured in bytes, with a maximum length of 1024, and the actual length depends on the specific transmission situation.
7) Checksum field
The checksum field is mainly used to verify the correctness of communication data. CRC is used in both USB token packages and data packets. However, CRC is generated by the sender before bit filling, so the receiver must decode the CRC field after removing bit filling. The PID field in the packet itself contains a checksum, so CRC calculations do not include the PID part. The CRC of the token package uses 5 bits, while the data fields in the packet use 16 bits of CRC.
a. Token pack
SOF packets are sent by the host to the device: for full-speed buses, they are sent every 1.00 ms ±0.0005 ms; For the high-speed bus, it is sent every 125μs ±0.0625μs.
SOF packet format

IN, OUT, SETUP packet format

b. Data packets
Data packets (DATA0, DATA1, DATA2, MDATA)

c. Handshake packet
PRE, ACK, NAK, STALL, NYET packet formats

(2) USB transfer process
1) USB transactions
The process of receiving or sending data on a USB is called transaction processing. Transactions usually consist of a series of packets, and different transactions have different packets. Common transactions in USB data transfer include input (IN) transactions, output (OUT) transactions, and SETUP transactions. Note that SOF only indicates the start of a frame; there is no valid data and it is not a transaction; A level state after the EOF frame transmission ends, and it is not a transaction.
A transaction usually consists of two or three packets: a token packet, a data packet, and a handshake packet. The token packet initiates the transaction, the data packet transmits data, and the sender of the handshake packet is usually the data receiver. After the data is correctly received, the handshake packet is sent. The device can also use NACK to indicate that the data is not ready.
2) Input transactions
An input (IN) transaction is the process by which the host retrieves data from an endpoint of a USB device. As shown in the figure below, an input transaction has three states: a normal input transaction (Figure (a)), an input transaction when the device is busy or without data (Figure (b)), and an input transaction when the device fails (Figure (c)). A correct input transaction consists of three stages: token packet, data packet, and handshake packet.
Enter the transaction processing flow

Combining a normal input transaction instance, this chapter introduces and analyzes a normal input transaction. As shown in the figure below, a normal input transaction consists of three interaction processes: (1) The Host sends an IN token packet to the Device; (2) After receiving the IN token packet, the Device sends the data to be sent to the host; (3) After the host receives the data packet, it returns an ACK packet to confirm that the packet was received correctly.
3) Output transactions
An output (OUT) transaction is the process by which the host sends data to a certain endpoint of a USB device. As shown in the figure below, an output transaction has three states: a normal output transaction (Figure (a)), an output transaction when the device is busy (Figure (b)), and an output transaction when the device fails (Figure (c)). A proper output transaction includes three stages: token, data, and handshake.
Output transaction processing flow

Below, using examples of normal output transactions, this chapter introduce and analyze normal output transactions. As shown in the figure below, a normal output transaction involves three interaction processes: (1) The Host sends an OUT token packet to the Device; (2) The Host sends data packets to the Device; (3) After receiving the data packet, the Device replies with an ACK packet to confirm that the packet was received correctly.

4) Setup transactions
SETUP transaction processing defines special data transfers between Host and Device, which are only applicable during the establishment phase of USB controlled transfers. As shown in the figure below, there are usually three states for setting up transactions: normal transaction setup (Figure (a)), busy device configuration transaction (Figure (b)), and device error transaction setup (Figure (c)). Proper transaction setup includes three stages: token, data, and handshake.
Setup transaction processing workflows


Below, we introduce and analyze normal setup transactions in conjunction with examples of normal setup transactions. As shown in the figure below, a normal setup transaction involves three interaction processes: (1) The host sends a SETUP token packet to the Device; (2) The Host sends a DATA0 packet to the Device; (3) After receiving the data packet, the Device replies with an ACK packet to confirm that the packet was received correctly.
(3) USB transfer
Transfers consist of transactions such as OUT, IN, or SETUP. The USB standard protocol defines four types of transfers: Control Transfer, Bulk Transfer, Interrupt Transfer, and Isochronous Transfer. The priority of the four transfer types from highest to lowest is: synchronous transfer, interrupt transfer, control transfer, and batch transfer.
1) Control transfer
Control Transfer is the most basic and important transmission method in USB and is the default transmission method for port 0. Control transfer is typically used for transferring between the host and the USB peripheral at the endpoint 0, but the specified vendor's control transmission may be used on other endpoints, mainly for querying, configuring, and sending general commands to USB devices. Control transfer is unidirectional (except for endpoint 0, which is bidirectional), and the data volume is usually small. The maximum length of Control Transfer's data packets depends on their operating speed: the maximum packet length for low-speed mode is fixed at 8 bytes, for high-speed mode is 64 bytes, and for full-speed mode, you can choose between 8, 16, 32, or 64 bytes.
Note
- Telink USB endpoint 0 packet length: TC platform (B80/B80B/B85/B87/TC121x) is fixed at 8 bytes; B91/B92/TL321x/TL721x/TL751x can be configured to 8/16/32/64 bytes via
usbhw_set_ctrl_ep_size(). - Control transfer consists of three stages: the setup stage, the data stage (optional), and the state stage, each consisting of one or multiple (data phase) transactions.
- Setup stage: As shown in the figure, the setup stage consists of SETUP transactions. The data stage of a SETLUP transaction is always DATA0 and has a fixed length of 8 bytes.

Data Stage: Data Stage is optional. If there is a data stage, it includes one or more IN/OUT transactions. Used to transmit data required during the setup stage, with USB definition format. Transactions in the data stage follow the same direction, meaning either all are in or all are OUT. If the data to be transmitted exceeds the length of a packet, the master controller splits it into multiple packets for transmission. Once the direction of data transmission changes, it is considered to have entered a state process. The first packet of the data process must be DATA1, and after each attempt to transmit one packet, it is exchanged between DATA0 and DATA1. If the last packet size equals the maximum size, another packet of size 0 should be passed to confirm the end. According to the direction of data transmission during the data stage, control transfer can be divided into three types: Control Write, Control Read, and No-Data Control, as shown in the figure below

State Stage: The state stage is the final stage of controlling transaction processing, consisting of an IN or OUT transaction, always using DATA1 packets. The state stage and the data stage are transmitted in opposite directions; that is, if the data stage is IN, the state phase is OUT, and vice versa. Used to report the transmission results during the setup and data stages.
2) Interrupt transfer
Interrupt Transfer is the same process as batch transmission except that it does not support PING and NYET packets, so its sequence diagram can be referenced for batch transfer. The main differences between interrupt transfer and batch transfer are two main points: (1) The priority is different—interrupt transfer has a higher priority than batch transfer and is second only to synchronous transmission; (2) Different maximum package lengths are supported: the maximum packet length for interrupt transfer is 8 bytes in low-speed mode, 64 bytes in full-speed mode, and 1024 bytes in high-speed mode.
It should be noted that the interrupt mentioned here is different from hardware interrupts. Since USB does not support hardware interrupts, the host must periodically poll to find out if any devices need to transmit data to the host. From this, it can be seen that interrupt transfer is also a polling process. The polling cycle is determined by the user's device (the polling interval for full-speed equipment is 1ms\~255ms, for low-speed devices 10ms\~255ms). The host only needs to ensure that the transmission is scheduled once within this interval. Polling cycles are very important. If they are too fast, they consume too much bus bandwidth; if too low, data may be lost. Therefore, users need to set them according to their own data conditions.
Interrupt transfer is usually used in devices with limited data volumes but strict time requirements, such as keyboards and mice in human-machine interface devices (HID). Interrupt transfer can also be used to continuously monitor device status, and when conditions are met, batch transfer is used to transfer large amounts of data. The type of endpoint for interrupt transfer is generally the IN endpoint, i.e., from Device to Host (IN transaction), and is rarely used for OUT endpoints. Some computers do not even support OUT transactions for interrupt transfer.

3) Isochronous transfer
Isochronous Transfer, also known as isochronous transmission, is unreliable transmission. Isochronous transfer only has two stages: token packets (IN/OUT token packets) and data packets (DATAx). It does not have handshake packets nor supports PID flipping. When scheduling transfers, synchronous transfer has the highest priority. The maximum length of isochronous transfer packets is 1023 bytes in full-speed mode, 1024 bytes in high-speed mode, and does not support synchronous transmission in low-speed mode.

Isochronous transfer is suitable for data that must arrive at a fixed rate or at a specified time, and can tolerate occasional errors. USB reserves bus bandwidth for it, ensuring service is received within every frame or small frame. Accurate rates and predictable transmission times. However, it does not use error control or retransmission mechanisms, so every transmission is not guaranteed. It is suitable for audio and video devices.
4) Batch transfer
Bulk Transfer, also known as block transfer, is a one-way reliable transmission consisting of one or more IN/OUT transactions, with data packets in each transaction arranged as DATA0-DATA1-DATA0-... to ensure synchronization between the transmitter and receiver, as shown in the figure below.

Error detection and retransmission mechanisms in USB are handled by hardware. If a transfer error occurs, the DATA packet will not be flipped and will be resent. At the same time, if the receiver receives a DATA packet with the same consecutive PID, it is treated as a retransmission packet. USB allows up to 3 consecutive transmission errors; if more than 3 occur, the host considers the endpoint to have a functional error (STALL) and abandons the transmission task for that endpoint.
USB applications — basic concept
The USB applications discussed in this chapter do not refer to the purpose of USB, but rather to the application layer design above the USB driver layer. This chapter explains the basic concepts and working principles of USB in detail from the user's perspective, helping users become familiar with and master the basics and usage of USB.
The relationship between USB hardware devices and software devices is that a single USB hardware device can correspond to one or more software devices, depending on the user's enumerated information (configuration descriptor information). Software devices are virtual devices that the PC abstracts from the interfaces of hardware devices that implement the same function, allowing unified operation. A software device contains one or more interfaces, and an interface contains one or more endpoints (endpoints are explained below), and interfaces and endpoints are all concepts in hardware devices.
An endpoint is the smallest unit in a USB device capable of transmitting and receiving data. Except for endpoint 0 (fixed for bidirectional control transmission), all other endpoints only support unidirectional communication, i.e., the input endpoint (data stream from device to host) or output endpoint (data stream from host to device). There is a limit to the number of endpoints supported by devices. Besides the default endpoint 0, low-speed devices can support up to 2 sets of endpoints (2 inputs, 2 outputs), while high-speed and full-speed devices can support up to 15 sets of endpoints.
An interface is a collection of endpoints that form a basic function within a USB device, and is controlled by the USB device driver (the host can, based on the interface, virtually operate a USB device on the PC side, which is a class of USB devices). From the host's perspective, a USB device can consist of one or more interfaces. For example, a USB device integrating a mouse and keyboard has two interfaces: one keyboard and the other mouse; For example, an audio device consists of an interface for command transmission and an interface for data transmission.
Summary as follows:
Endpoint: The endpoint is the only identifiable part of a USB device. It is the endpoint of communication flow between the host and the device. It serves as a data buffer on the USB device or host, used to store and transmit various USB data.
Interface: can be understood as a function.
Configuration: The combination of interfaces, selected during connection.

Standard descriptors
A descriptor is a set of data used to describe device attributes, divided into standard descriptors and proprietary descriptors. Standard descriptors are common attribute descriptions for all USB device types, including device descriptors, configuration descriptors, interface descriptors, endpoint descriptors, and string descriptors. Among them, string descriptors are further divided into serial number descriptors, product descriptors, vendor descriptors, and language ID descriptors. Proprietary descriptors are descriptors unique to each device class. For example, HID descriptors include HID descriptors, report descriptors, and entity descriptors. The diagram below shows the standard device request structure specified by the USB protocol.
(1) Device descriptors
Device descriptors describe basic information about USB devices, and each device has only one device descriptor. The diagram below shows the structure of a standard device descriptor. The first 8 bytes summarize the basic attributes of USB, which is the first information the host needs to obtain in USB enumeration.
| Offset | Domain | Size | Value | Description |
|---|---|---|---|---|
| 0 | bLength | 1 | Numbers | The byte count of this descriptive table |
| 1 | bDescriptorType | 1 | Constant | Descriptor type (here it should be 0x01, i.e., device descriptor) |
| 2 | bcdUSB | 2 | BCD code | USB device specification version number (BCD code) for this device compatible with the description table |
| 4 | bDeviceClass | 1 | Class | Device classification code |
| 5 | bDeviceSubClass | 1 | Subclass | Subclass masking |
| 6 | bDeviceProtocol | 1 | Protocol | Protocol code |
| 7 | bMaxPacketSize0 | 1 | Numbers | Maximum packet size at endpoint 0 (only 8, 16, 32, 64 are valid values) |
| 8 | idVendor | 2 | ID | Manufacturer Logo (assigned by USB-IF organization) |
| 10 | idProduct | 2 | ID | Product Logo (assigned by manufacturer) |
| 12 | bcdDevice | 2 | BCD code | Device Serial Number (BCD Code) |
| 14 | iManufacturer | 1 | Index | The index value of a string descriptor describing vendor information. |
| 15 | iProduct | 1 | Index | The index value of the string descriptor describing product information. |
| 16 | iSerialNumber | 1 | Index | The index value of the string descriptor describing the device serial number information. |
| 17 | bNumConfigurations | 1 | Numbers | Possible number of configuration descriptors |
Note
- idVendor(VID) and idProduct (PID) are used to uniquely identify a device, but for Windows, simply giving VID and PID does not uniquely identify the device, resulting in continuous driver installations. At this point, you also need to consider the serial number string. That is, when only VID, PID, and serial numbers match, Windows only needs to install the driver once.
- The index values of the three string descriptors should be different (except 0).
(2) Configuration descriptors
Configuration descriptors define the configuration information of a device, and a device can have multiple configuration descriptors.
| Offset | Domain | Size | Value | Description |
|---|---|---|---|---|
| 0 | bLength | 1 | Number | This describes the byte length of the table. |
| 1 | bDescriptorType | 1 | Constant | Configure the description table type (here it is 0x02) |
| 2 | wTotalLength | 2 | Number | The total length of this configuration information (including configuration, interfaces, and endpoint descriptors) |
| 4 | bNumInterfaces | 1 | Number | Number of interfaces supported by this configuration |
| 5 | bConfigurationValue | 1 | Number | Use this configuration |
| 6 | iConfiguration | 1 | Index | The string description table describing this configuration index (0-none) |
| 7 | bmAttributes | 1 | Bitmap | Configuration Features: D7: Reserve (set to 1) D6: Self-contained power D5: Remote wake-up D4: 0: Keep (set to one) |
| 8 | MaxPower | 1 | mA | In this configuration, the bus power consumption is measured in units of 2mA |
(3) Interface descriptors
Interface descriptors describe the configuration provided by an interface, and the number of interfaces a configuration has is determined by the bNumInterfaces of the configuration descriptor.
| Offset | Domain | Size | Value | Description |
|---|---|---|---|---|
| 0 | bLength | 1 | Number | The number of bytes in this table is |
| 1 | bDescriptorType | 1 | Constant | Interface Description Table Class (here should be 0x04) |
| 2 | bInterfaceNumber | 1 | Number | Interface number, the index of the currently configured supported interface array (starting from zero). |
| 3 | bAlternateSetting | 1 | Number | Optional index values. |
| 4 | bNumEndpoints | 1 | Number | The number of endpoints used for this interface is except for endpoint 0 |
| 5 | bInterfaceClass | 1 | Class | The class value of the interface is |
| 6 | bInterfaceSubClass | 1 | Subclass | Subclass code |
| 7 | bInterfaceProtocol | 1 | Protocol | Protocol code: depends on the values of the bInterfaceClass and bInterface SubClass fields. |
| 8 | iInterface | 1 | Index | The string describing this interface describes the index value of the table. |
(4) Endpoint descriptors
Each endpoint in a USB device has its own endpoint descriptor, and the number of these is determined by the bNumEndpoint in the interface descriptor.
| Offset | Domain | Size | Value | Description |
|---|---|---|---|---|
| 0 | bLength | 1 | Number | The byte length of this descriptive table is |
| 1 | bDescriptorType | 1 | Constant | Endpoint description table class (here should be 0x05) |
| 2 | bEndpointAddress | 1 | Endpoint | The endpoint address and direction described in this table: Bit 3..0: Endpoint number. Endpoint numbers cannot be duplicated during configuration changes; Bit 6:4: Retention, zero; Bit 7: Direction, omitted if controlling endpoints. 0: Output Endpoint (Host to Device) 1: Input Endpoint (Device to Host) |
| 3 | bmAttributes | 1 | Bitmap | Characteristics of the endpoint. Bit 1..0: Transfer type 00=Control transmission 01=Synchronous transmission 10=Batch transfer 11=Interrupt transmission |
| 4 | wMaxPacketSize | 2 | Number | The maximum size of data packets this endpoint can receive or send under the current configuration. For interrupt transfer, batch transfer, and control transfer, endpoints may send shorter packets. |
| 6 | bInterval | 1 | Number | The host polls the interval of this endpoint and ignores endpoints for batch and control transmissions; For synchronous transmission endpoints, it must be 1; For interrupted transmission, the low-speed mode is 10\~255 (ms) here, and the full-speed mode is 1\~255 (ms). |
(5) String descriptors
String descriptors are optional. If string descriptors are not supported, all indices of string descriptors in the device, configuration, and interface descriptors must be set to 0, and the language string descriptor index must be 0.
| Offset | Domain | Size | Value | Description |
|---|---|---|---|---|
| 0 | bLength | 1 | Number | The number of bytes in this describing table (the value of the bString field N+2) |
| 1 | bDescriptorType | 1 | Constant | String description table type (here should be 0x03) |
| 2 | bString | N | Number | UNICODE-encoded string |
USB enumeration
Enumeration means the host reads some information from the device side, knows what kind of device it is, and how it communicates, so the host can load the appropriate driver based on this information. A crucial part of debugging USB devices is to check the USB enumeration. As long as the enumeration succeeds, it is close to the success.. Below, we provide a detailed introduction to the USB enumeration process using a USB enumeration diagram (see diagram (a) below) and a Telink mouse-type USB enumeration example (see diagram (b)).
(1) USB enumeration sequence
Figure (a) below shows the USB enumeration sequence diagram. From the diagram, we can see that the USB enumeration process is completed in 8 steps, with steps 1~7 being standard USB enumeration steps, and Step 8 being the USB device-specific enumeration process.

- Step 1 After the host detects a device is connected: First, the host determines whether the device is low-speed or full-speed based on the level status on the differential signal line (high-speed devices default to full-speed equipment at the initial power-on); Then the host waits for the device power to stabilize (>=100ms), and sends a reset signal to the device (D+ and D- are both low, sustaining >=10ms); Finally, if the device is high-speed and the host (hub) supports high-speed mode, after high-speed detection and handshake between the host and device, the device can switch to high-speed mode; otherwise, it will remain in full-speed mode.
- Step 2: After completing Step 1, the host uses endpoint 0 (default endpoint, controlling transmission) to send a GetDescriptor request (device address is 0). After receiving the request, the device sends its device descriptor to the host, which then proceeds to the next step based on this device descriptor (bMaxPacketSize0 field). It should be noted: (1) Only when the device receives the reset signal in Step 1 will it respond to the host; (2) The device that has completed the enumeration does not respond to the request; (3) The descriptor must be at least 8 bytes long (the bMaxPacketSize0 field is at the 8th byte); (4) If the device times out and does not respond or responds incorrectly, the host will restart and try three times. If after three attempts still does not receive a correct response, the host considers the device to be unrecognized (same below).
- Step 3: After correctly completing Step 2 and obtaining the maximum packet length at endpoint 0, the host resets the device, then depackaged and bundled the packets according to this length.
- Step 4: The host assigns a non-zero address to the device, which is different from other devices on the hub and is used to ensure the stability of directional communication. After the host completes the address device, communication between the host and the device will continue to follow the new address until the device is reset or removed.
- Step 5: The host sequentially obtains the device's standard descriptor (device descriptor, configuration descriptor, interface descriptor, endpoint descriptor, and string descriptor) according to the new address in Step 4. Note: (1) The length of the device descriptor is already obtained by the host in Step 2, so the length specified by the host (maximum length is the device descriptor length) is used to get the device descriptor. Other descriptors are obtained according to the maximum length of 255. The device side only needs to send the actual length as needed; (2) Device interface descriptors, endpoint descriptors, etc., which may be included in the configuration descriptor. The host retrieves all data for that configuration based on the wTotalLength field in the configuration descriptor; (3) If the device has multiple configurations, the host will request configuration descriptors in multiple instances; (4) The host will request string descriptors in order according to the number of string descriptors contained in device, configuration, interface, and endpoint, according to their index values. A special string descriptor (speech information descriptor) with an index value of 0 is used.
- Step 6: After completing Step 5, the host obtains the actual length of the configuration descriptor, sequentially obtaining the descriptor information and other information contained in the descriptor (such as interface descriptor and endpoint descriptor, etc.).
- Step 7 Step 1 \~6 is the standard USB enumeration process. Only when steps 1 \~6 are correct will the host issue a SetConfiguration command to activate and use a configuration on the device, at which point the device is truly usable. After the device configuration is complete, the host divides the device into one or more virtual devices based on the standard device descriptors.
- Step 8 After Step 7 is completed, one or more virtual devices are generated on the host, each with its own category identifier. The host will query the corresponding driver based on its VID, PID, and serial number, and install the driver (if there is a backup on the host, use it directly and do not install it again). Then, the host loads the class-specific description information according to the driver. Here is the standard HID device, with the main unit having its own driver.
(2) USB enumeration examples
Figure (b) below shows the enumeration process for the Telink Dongle mouse device. Step x in the diagram corresponds one-to-one with Step x in Figure (a). Step 8 is the proprietary enumeration process for the HID device class, which retrieves the report descriptor. The structure of the report descriptor can refer to the USB HID protocol Universal Serial Bus (USB) - Device Class Definition for Human Interface Devices (HID)。
Overview of USB types
Telink chips are divided into two types of USB hardware modules based on supported USB versions and speed modes:
- Full-speed USB (FS): Only full-speed mode (12Mbps). Uses a fixed-direction endpoint design and 8+256 Bytes of dedicated RAM.
- High-Speed USB (HS): Supports USB 2.0 protocol, as well as high-speed mode (480Mbps) and full-speed mode (12Mbps). It features a bidirectional endpoint design, built-in DMA engine, and 8KB FIFO.
Full Speed USB (FS)
Full Speed USB is the 12Mbps USB mode used by most Telink chips.
Supported chips can be found in the Chip USB version overview.
Main features:
- Fixed directional endpoints (IN/OUT directions cannot be switched)
- TC platform (B80/B80B/B85/B87/TC121x) endpoint 0 fixed at 8 bytes
- TL platform (B91/B92/TL321x/TL721x/TL751x) endpoint 0 can be configured with 8/16/32/64 bytes
- Supports both manual and automatic modes
For details, see Full Speed USB (FS).
High-speed USB (HS)
High-Speed USB supports 480Mbps high-speed mode and is backward compatible with full-speed 12Mbps mode.
Main features:
- USB 2.0 protocol, supporting both High Speed and Full Speed
- Device-Only operating mode
- There are 9 bidirectional endpoints (EP0-EP8), with EP0 being dedicated control endpoints
- 8KB internal FIFO, built-in DMA (Descriptor DMA)
- UTMI+ PHY interface
- Supports four types of transfer: Control, Bulk, Interrupt, and Isochronous
For details, see High-Speed USB (HS).
Overview of the USB version of the chip
| Chips | Speed mode | Number of endpoints (EP0+EP1-EP8) | SRAM size (EP0+EP1-EP8 endpoints shared) |
|---|---|---|---|
| B80/B80B/B85/B87/TC121x/B91 | Full speed | 1+8 | 8+256 Bytes |
| B92 | Full speed | 1+8 | 8+1024 Bytes |
| TL321x/TL721x/TL751x | Full speed | 1+8 | 64+2048 Bytes |
| TL322x | Full-speed + high-speed | 1+8 | 64 +8192 Bytes |
Note
- TC321x/TL323x/TC122x do not support USB hardware modules and cannot use USB functionality.
Full Speed USB (FS)
Introduction to USB hardware
Telink USB hardware modules internally solidify the processing of raw packets and transactions, automatically saving IN endpoint data and sending OUT endpoint data, packaging endpoint 0 data into standard user data packets. This not only improves USB execution efficiency but also greatly reduces the complexity of USB development.
(1) SRAM 8+256
Endpoint 0 can both input and output; the other endpoints have the following directions:
| Configurable endpoint types | Endpoint number |
|---|---|
| Control Endpoint (Input/Output) | 0 |
| Output endpoint | 5, 6 |
| Input endpoint | 1, 2, 3, 4, 7, and 8 |
Memory: 8+256 bytes of dedicated USB RAM. Endpoint 0 is fixed at 8 bytes, while the other endpoints share 256 bytes. The endpoint cache size is the starting address of the next endpoint minus the starting address of this endpoint.
| Endpoint start address | Meaning |
|---|---|
| 0x00 | Endpoint 1 starting address |
| 0x08 | Endpoint 2 starting address |
| 0x10 | Endpoint 3 starting address |
| 0x20 | Endpoint 6 starting address |
| 0xc0 | Endpoint 5 starting address |

Note
- The maximum endpoint cache is determined by the max register (which can be allocated to all cache space), with a default of 64 bytes.
- Compatible chipsets: B80/B80B/B85/B87/TC121x/B91 (SRAM: 8+256 Bytes)
(2) SRAM 8+1024
The endpoint orientation is consistent with 3.1.1. The following features have been added to version 3.1.1:
- Endpoint addresses are set via
usbhw_set_ep_addr(),supporting 16-bit addresses (up to 8-bit addresses supported viareg_usb_ep_buf_addrh) - Endpoint pointers support 16 bits (obtained via
usbhw_get_ep_ptr(),reg_usb_ep_ptrhsupports up to 8 bits)
Compatible chip: B92 (SRAM: endpoint 0 fixed 8 bytes + shared 1024 bytes).
(3) SRAM 64+2048
The endpoint orientation is consistent with 3.1.1. Based on version 3.1.2, the following has been added:
Endpoint 0 Packet size configurable (set to 8/16/32/64 bytes via usbhw_set_ctrl_ep_size())
Compatible chips: TL321x/TL721x/TL751x (SRAM: Endpoint 0 64 bytes + shared 2048 bytes).
Interrupt
USB interrupts can be divided into three categories: endpoint 0 interrupts, endpoint 1-8 interrupts, and suspend/250us/reset interrupts, as shown in the table below:
| Interrupt | Conditions for generation | Clearing methods |
|---|---|---|
| CTRL_EP_SETUP(IRQ7) | Endpoint 0 controls the transmission setup stage | Manually clear status |
| CTRL_EP_DATA (IRQ8) | Endpoint 0 controls the data transfer stage | Manually clear status |
| CTRL_EP_STATUS (IRQ9) | Endpoint 0 controls the transmission state stage | Manually clear status |
| Endpoint(1-8) interrupts(IRQ11)FLD_USB_EDP8_IRQ (in)FLD_USB_EDP1_IRQ (in)FLD_USB_EDP2_IRQ (in)FLD_USB_EDP3_IRQ (in)FLD_USB_EDP4_IRQ (in)FLD_USB_EDP5_IRQ (out)FLD_USB_EDP6_IRQ (out)FLD_USB_EDP7_IRQ (in) | 1. Except for synchronous endpoints: output endpoint: host out transaction, corresponding position of the status register. 1. An interrupt occurs, and after reception, it returns to ACK. Input endpoint: After data filling is complete, configure the ACK to notify the hardware and trigger an interrupt. The hardware, upon receiving the host-in transaction, sends data to the host. 2. Synchronization endpoints: Endpoints 6 and 7 can be set as synchronization endpoints, with interrupts occurring at intervals of 1 ms. | Manually clear status |
| USB_IRQ_USB_SUSPEND (IRQ24) | The USB bus is idle, for example, if the USB port is unplugged, the host goes to sleep | Manually clear status |
| USB_IRQ_250us (IRQ34) | 250us scheduled interrupts | Manually clear status |
| USB_IRQ_RESET (IRQ35) | Host sends reset timing | Manually clear status |
Note
- The transmission of the Driver enumeration process is handled by polling without interrupts.
Automatic and manual modes
Telink USB offers two modes: automatic mode and manual mode:
Users can control whether to choose automatic or manual mode by configuring the configuration register at endpoint 0. The default configuration register for endpoint 0 is 0xFF, i.e., automatic mode. At this time, all codecs related to USB endpoint 0 are automatically driven by Telink hardware. Telink has its own driver as a Print device, using endpoint 8 as the control interface endpoint, and printer data is sent from endpoint 0;
Manual mode requires users to modify EDP0CFG registers (see below), usually setting bit[7] and bit[5] to 0, meaning the user performs the standard USB enumeration and uses user-defined descriptors.

USB software basics
(1) USB operation process
The Telink USB software operation process can be divided into two stages: initialization and loop detection, as shown in the diagram below.

The initialization stage mainly completes USB configuration and enabling USB. USB configuration options mainly include mode switching (automatic and manual), setting USB data buffers, and configuring other configuration items; Mode switching mainly switches the USB operating mode to manual mode. At this time, the enumeration process and descriptors are controlled by the user, and the device reports the user's prepared enumeration information to the host; Setting a USB buffer allows users to specify a buffer segment for each endpoint based on their usage (except for endpoint 0). The buffer total size is 256 bytes; unused endpoints do not need to be specified; Setting Other Configurations is used to perform other configuration operations. If users do not wish to use the system's default configuration items, they can choose to configure them themselves, such as transmitting data in the form of interrupts.
Loop detection mainly involves continuously checking whether there is data in the data reception and sending buffers. If there is data, it performs related operations. During program execution, it repeatedly executes USB_handle_irq. The operation process of endpoint 0 can be divided into three stages, as shown in the figure below, corresponding to the SETUP stage, DATA stage, and STATUS stage for controlling transmission. These mainly complete USB recognition and configuration operations, such as USB enumeration. This process is completed in the main loop, where SETUP parses commands issued by the host and prepares the corresponding data according to the host's commands; DATA refers to the data prepared during the data phase to be sent to or received by the host; STATUS is the process of both parties shaking hands.

(2) Data reception and transmission
A. Data reception
Telink USB data reception is completed by hardware, which saves the received data to RAM. After reception is complete, the hardware generates an interrupt to notify the user, and the user only needs to read the data after detecting the interrupt. Data detection and reception should be performed within the USB/_handle_irq functions in USB.C. Based on the diagram below, we analyze the processing flow of Telink USB data reception in detail:
1) The user needs to check whether the relevant interrupt identifier bit (reg_usb_irq) is set to 1; if set to 1, the data reception stage begins.
2) Once data is detected, the user needs to clear the interrupt identifier bit, i.e., reg_usb_irq = BIT((USB_EDP_CUSTUM_OUT & 0x07)).
3) Before reading data, users need to use reg_usb_ep_ptr (USB_EDP_CUSTUM_OUT) to obtain the received data length.
4) After obtaining the data length, users can repeatedly read the USBHW_read_ep_data (USB_EDP_CUSTUM_OUT) to obtain all the data received this time.
5) After receiving the data, the user needs to call usbhw_data_ep_ack(USB_EDP_CUSTUM_OUT). (Note: This step is very important. Only when the ACK of the OUT endpoint is set will the hardware receive data sent by the host to that endpoint, and an interrupt will occur after reception is complete.) )

B. Data transmission
Telink USB data sending and receiving are handled by hardware; users only need to fill the data into the corresponding USB RAM and set the data ACK to position 1. Before filling up data, users need to first check whether there is data to be sent in the USB RAM. If there is, they should wait until sending is complete before filling new data; otherwise, data overwriting will occur.
The diagram below shows an example of sending data in the Telink SDK. Below, we analyze the USB data sending process in detail using this example:
1) Before sending data, users need to check whether the operation endpoint is busy. If it is, it must wait for the sending to complete before filling the data.
2) If the endpoint is idle, the endpoint counter needs to be reset first, i.e., reg_usb_ep_ptr(USB_EDP_CUSTUM_CMISC_IN) = 0.
3) After resetting the endpoint counter, users can fill the endpoint with data. Note that reg_usb_ep_dat(USB_EDP_CUSTUM _CMISC_IN) = data[i] is putting data into USB RAM (hardware operation).
4) After the user fills the data, they need to call reg_usb_ep_ctrl(USB_EDP_CUSTUM_CMISC_IN) = FLD_EP_DAT_ACK to notify the hardware that the data is ready. After receiving this command, the hardware will send the data to the host when the next host requests it.

USB Demo
USB applications mainly introduce the simple applications of HID (Human Interface Device) devices, audio devices, and CDC (Communication Device Class) devices within the USB standard device category, which customers can freely combine according to their needs.
HID devices are commonly used types of USB devices, specifically those that interact directly with people, such as USB mouts and USB keyboards;
The most common USB Audio devices are microphones and speakers;
The USB CDC class is the abbreviation for USB communication devices, while virtual serial devices are a type of CDC device.
(1) Demo configuration
In the header file app_config.h, you can choose to configure different devices.
TC Platform (B80/B80B/B85/B87/TC121x):
#define USB_MOUSE 1
#define USB_KEYBOARD 2
#define USB_CDC 3
#if (!MCU_CORE_B80 && !MCU_CORE_B80B)
#define USB_MICROPHONE 4
#define USB_SPEAKER 5
#endif
#define USB_MOUSE_SLEEP 6
#define USB_DEMO_TYPE USB_MOUSE
Note
- The B80/B80B do not support USB_MICROPHONE or USB_SPEAKER.
TL Platform (B91/B92/TL321x/TL721x/TL751x):
#define USB_MOUSE 1
#define USB_KEYBOARD 2
#define USB_MICROPHONE 3
#define USB_SPEAKER 4
#define USB_CDC 5
#define USB_MIC_SPEAKER 6
#define USB_DEMO_TYPE USB_MOUSE
TL platform demo additionally supports:
USB_MIC_SPEAKER: Microphone and speaker composite deviceUSB_PRINTER_ENABLE: Printer equipmentUSB_SOMATIC_ENABLE: Motion sensing devicesUSB_CUSTOM_HID_REPORT: Custom HID reportsUSB_MASS_STORAGE_ENABLE: Large-capacity storage devices (TL platform only)
(2) USB Mouse
A. Mouse processing workflow
USB HID devices transmit data through reports. A single report descriptor can describe multiple reports, and different reports are identified by their IDs. The report ID is the first byte of the report. If there is no specified report ID, the report has no ID field; it starts with data. For detailed report descriptor information, refer to the USB HID protocol and HID Usage Tables.
First, the host recognizes the Telink USB as a mouse device and needs to go through the enumeration stage. After successful device enumeration, it enters the data transmission and reception stage. According to the content of the mouse report descriptor, the descriptor with report ID USB_HID_MOUSE contains 4 bytes. The lowest 5 bits of the first byte indicate whether the key is pressed; the higher 3 bits are constants and are useless; Byte 2 is the change in the X-axis; The 3rd byte is the change in the Y-axis; Byte 4 is the change in the scroll wheel. Returns the report via the function usbmouse_hid_report(USB_HID_MOUSE, mouse, 4).
In the demo program, the array unsigned char mouse[4] is defined, where:
mouse[0]:BIT(0) - left key; BIT(1) - right key; BIT(2) - middle key; BIT(3) - side key; BIT(4) - external key。 The corresponding bit is set to 1, which means pressing the mouse buttonmouse[1]: The change in relative to the X coordinatemouse[2]: The change in relative to the Y coordinatemouse[3]: The amount of change in the scroll wheel
B. Mouse test
Press Test
In the demo program, report ID:USB_HID_MOUSE = 1,
Array mouse assignment: mouse[0] = BIT(1), mouse[1] = -2 (complement), mouse[2] = 2, mouse[3] = 2.
After grounding the pins on the development board and then pulling them out, function usbmouse_hid_report(USB_HID_MOUSE, mouse, 4) is executed.
You can observe that when the desktop right-click the mouse is pressed, the cursor moves down to the left, as shown below. You can also see in the USB packet capture tool Input Report[1]: x:-2, Y:2, wheel:0, Btns=[2].

Release test
Repeat the same operation on the other pin: the mouse array is reset and the key is released.
(3) USB Keyboard
A. Keyboard processing workflow
According to the content of the keyboard report descriptor, there are input and output reports. The input report specifies 8 bytes, and the 8 bits of the first byte indicate whether a special key is pressed:
- BYTE0:BIT(0) – left Ctl; BIT(1) – Left Shift; BIT(2) – Left Alt; BIT(3) – left GUI; BIT(4) – Right Ctl; BIT(5) – Right Shift; BIT(6) – Right Alt; BIT(7) – Right GUI
- BYTE1: Reserved value, all set to 0
Bytes 3 to 8 are the standard key values; when no key is pressed, all 6 bytes are 0. The first byte value of these 6 bytes is the key value of the key. When multiple keys are pressed simultaneously, all these key values are returned simultaneously. The order of key values in the array is not related. For specific key values, please refer to the HID Purpose Table documentation, for example: 0x59 corresponds to numeric keyboard 1; 0x5a corresponds to numeric keyboard 2; 0x5b corresponds to numeric keypad 3; 0x39 corresponds to case-toggle keys.
B. Keyboard test
Press Test
In the Demo program, define array kb_data[6], assign the value kb_data[0] = 0; kb_data[1] = 0; kb_data[2] = 0x59; kb_data[3] = 0x5a; kb_data[4] = 0x39; kb_data[5] = 0.
After grounding the pins on the development board and then pulling them out, function usbkb_hid_report_normal(0x10, kb_data) will be executed, where parameter 1 corresponds to the first byte and parameter 2 corresponds to an array of 3\~8 bytes.
You can observe that in the input interface of the editing window, you can enter the numbers 1 and 2, and press the right Ctrl key and the capital/case key. As shown in the figure below, you can also see in the USB packet capture tool Input Report: Keys=[Rctrl 1 2 CapsLk].

Release test
Perform the same operation on another pin: the special key and kb_data array are cleared, and the key is released.
(4) USB MIC
A. MIC processing flow
Taking AMIC as an example, a USB microphone device transmits the device's AMIC data to the host via USB, ensuring the sampling rate and number of channels for the entire data channel match. In the demo program, data is mainly uploaded to the USB section. Mic endpoint interrupts are scheduled at 1ms, meaning every 1ms is interrupted. Depending on the sampling rate, the amount of data generated in 1ms varies. For example, for 16K sampling rate audio and mono data, 1 sample equals 2 bytes, 1ms data is 32 bytes, and the corresponding audio buff is filled into the USB SRAM.
TL Platform Differences: TL platform MIC Demo supports dual audio (MIC_CHANNEL_COUNT 2), while TC platform only supports mono (MIC_CHANNEL_COUNT 1).
B. MIC Demo Test
For audio-related testing, using Audacity software, as shown below, select Telink Audio 16 for microphone and PC speakers for speakers.
Device Mic picks up audio and PC speakers play it. If the recorded human voice can be played through the speaker without distortion, it means the Mic is working properly.
).
(5) USB Speaker
A. Speaker processing workflow
The USB speaker device transmits the host's audio data to the device via USB. This process is completed within 1ms interrupt, reads the length of the USB SRAM data, and fills the corresponding data into the audio buff.
TL platform differences: TL platform Speaker Demo supports dual audio (SPK_CHANNEL_COUNT 2), while TC platform only supports mono (SPK_CHANNEL_COUNT 1).
B. Speaker Demo Test
In the Audacity software, the microphone is PC microphone, and the speaker is Telink Audio 16. Through the output audio interface (3.5mm headphone jack). If the recorded vocals can be played through headphones without distortion, it means the speaker is working properly.

(6) USB CDC
CDC devices have two interfaces: CDC control interface and CDC data interface. The control interface allocates endpoint 2 as the interrupt input endpoint for transmission. The data interface assigns endpoint 5 (out) and endpoint 4 (in). You must first set the ACK of endpoint 5 before the endpoint can receive data from the USB host.
A. CDC processing flow
The first time using a CDC device on a PC, to ensure it is identified as a CDC device, you need to manually install the .inf file, as shown in the diagram below under the USB_Demo path.
Note
- Only Win7 and Win8 systems need to install the .inf file; systems with Win10 and above do not need to install it.

Data Reception (host to device)
In the demo program, the host in the function void usb_cdc_irq_data_process(void) sends data to the device. After endpoint 5 causes an interrupt, function usb_cdc_rx_data_from_host(usb_cdc_data) receives the data.
Data Transmission (Device to Host)
In main_loop, when the received buff data length is not zero, the received data is sent to the host via the function usb_cdc_tx_data_to_host().
TL Platform Differences: The TL platform CDC Demo supports both blocking and non-blocking transmission modes, switching via DEVICE_SEND_DATA_MODE macros. In non-blocking mode, the usb_cdc_tx_data_to_host_non_block() function returns immediately after filling the data. The TL platform CDC Demo requires explicit configuration of endpoint buffer size and address.

B. CDC Demo Test
The test phenomenon is shown in the figure below, returning data sent by the serial port assistant.

High-speed USB (HS)
High-speed USB hardware architecture
(1) Overview
For an overview of high-speed USB (USB0) types, please refer to High-Speed USB (HS). This chapter provides a detailed introduction to the hardware architecture and driver usage of high-speed USB.
(2) USB0 features
The USB 2.0 Device core used by USB0 has the following features:
| Characteristics | Description |
|---|---|
| USB standard | USB 2.0, supporting both High Speed and Full Speed |
| Working mode | Device-Only |
| Number of endpoints | 9 bidirectional endpoints (EP0\~EP8), with EP0 as a dedicated control endpoint |
| Total size of FIFO | 8KB (8192 bytes) |
| DMA support | Internal DMA (Descriptor DMA) |
| PHY interface | UTMI+ |
| Types of transmission | Control、Bulk、Interrupt、Isochronous |
(3) System block diagram
USB0, as an external device to the CPU, interconnects with the system via the AHB bus, with interrupt signals connected to the interrupt controller:
+-------------------+ +------------------+ +----------------+
| | AHB Bus | | UTMI+ | |
| CPU |<-------->| USB0 |<-------->| USB PHY |
| | | | | |
+-------------------+ +------------------+ +----------------+
| | |
| | IRQ_USB0 | DP/DM
| v v
| +-----------------------+ +----------------+
| | | | |
+--------------------->| Interrupt Controller | | USB Host |
| | | |
+-----------------------+ +----------------+
IO pins:
| Pins | Function | Description |
|---|---|---|
| PA3 | USB0_DM | USB D - data cable |
| PA4 | USB0_DP | USB D+ data cable |
PA3 and PA4 can only be used as USB0_DM and USB0_DP pins or GPIO functions; they cannot be used simultaneously for other functions.
(4) Endpoints and FIFOs
A. Endpoint type
USB0 supports four types of endpoints defined by the USB 2.0 specification:
| Type | Enumeration value | Description |
|---|---|---|
| Control | USB0_EP_TYPE_CONTROL |
Controlled transmission, supported only by EP0 |
| Isochronous | USB0_EP_TYPE_ISOCHRONOUS |
Isochronous transfer, used for real-time data such as audio/video |
| Bulk | USB0_EP_TYPE_BULK |
Batch transfer, used for large data volumes such as CDC |
| Interrupt | USB0_EP_TYPE_INTERRUPT |
Interrupt transfer, used for periodic transmission of small data volumes such as HID |
B. FIFO architecture
The total internal size of the USB0 FIFO is 8KB, divided into RX FIFO and TX FIFO parts:
- RX FIFO: A single receive FIFO, sharing all OUT endpoints. Set the starting address and size using
usb0hw_set_grxfsiz(). - TX FIFO: Each IN endpoint has its own independent sending FIFO. Configure the start address, size, and FIFO number of each IN endpoint for each IN endpoint via
usb0hw_set_epin_size().
FIFO Address Allocation Example (HID Mouse Demo):
+----------------------------+ 0x1FF (8KB)
| |
| EP2 TX FIFO (64x4 words) |
| |
+----------------------------+ 0x140
| |
| EP0 TX FIFO (64x4 words) |
| |
+----------------------------+ 0x100
| |
| RX FIFO (256x4 words) |
| |
+----------------------------+ 0x000
FIFO size is measured in words. The actual configurable range is limited by the total 8KB FIFO size.
USB0 software architecture
(1) Layered architecture
The USB0 software stack uses a layered architecture design, from top to bottom as follows:
+---------------------------------------------------------------+
| USB Demo layer |
| mouse, keyboard, cdc, audio |
+---------------------------------------------------------------+
| USB Class driver layer |
| (usbd_hid, usbd_cdc, usbd_audio) |
+---------------------------------------------------------------+
| USB Device Protocol Stack |
| (Standard device requests, descriptors, endpoint management) |
+---------------------------------------------------------------+
| Port adaptation layer |
| (usbd_ep_open/write/read/stall + ISR) |
+---------------------------------------------------------------+
| Drive layer |
| (DMA configuration, FIFO management, interrupt handling) |
+---------------------------------------------------------------+
| Hardware layer (USB0 IP) |
| (DMA engine, PHY, endpoint control) |
+---------------------------------------------------------------+
(2) Driver layer
usb0hw.c and usb0hw.h provide the lowest-level operating interfaces of USB0 hardware, including:
- Initialization and Reset:
usb0hw_init(),usb0hw_reset(),usb0hw_power_down(). - Endpoint management:
usb0hw_ep_open(),usb0hw_ep_close(). - Data Transmission:
usb0hw_write_ep_data(),usb0hw_read_ep_data(). - FIFO management:
usb0hw_set_grxfsiz(),usb0hw_set_epin_size(),usb0hw_flush_tx_fifo(), usb0hw_flush_rx_fifo(). - Interrupt management:
usb0hw_get_gintsts(),usb0hw_clear_gintsts(),usb0hw_get_daint(),usb0hw_daintmsk_en(),usb0hw_daintmsk_dis(). - Connection Management:
usb0hw_soft_connect(),usb0hw_soft_disconnect(). - Power management:
usb0hw_pcgc_clk_en(),usb0hw_pcgc_clk_dis(),usb0hw_phy_pll_en(),usb0hw_phy_pll_dis(), usb0hw_remote_wakeup(). - Status query:
usb0hw_get_speed(),usb0hw_get_sof_fn(),usb0hw_get_timer_stamp(). - Other features:
usb0hw_set_address(),usb0hw_test_mode(),usb0hw_set_pwronprgdone().
(3) Port adaptation layer
The port adaptation layer acts as the adaptation layer between USB0 hardware and the USB device protocol stack, achieving the following functions:
- Endpoint Operation Adaptation: Map the protocol stack's
usbd_ep_open(),usbd_ep_write(),usbd_ep_read(),usbd_ep_stall(), usbd_ep_clear_stall() to the` correspondingUSB0HW` functions - SET_ADDRESS Adaptation :
usbd_set_address()callsusb0hw_set_address(). - TEST_MODE Adaptation:
usbd_test_mode()callsusb0hw_test_mode(). - Interrupt Handling:
usb0_irq_handler()handles all USB0 global interrupts registered asIRQ_USB0throughPLIC_ISR_REGISTER
Interrupt handling process (usb0_irq_handler):
usb0_irq_handler()
|
+-- ENUMDONE → Speed check complete, clear the mark
+-- OEPINT → usb_irq_handler_epout()
| +-- XFERCOMPL → usbd_epout_complete_handler()
| +-- SETUP → usbd_control_request_process()
| +-- STSPHSERCVD → State stage completed
+-- IEPINT → usb_irq_handler_epin()
| +-- XFERCOMPL → usbd_epin_complete_handler()
+-- SOF → usbd_sof_callback()
+-- USBSUSP → usbd_suspend_callback()
+-- WKUPINT → usbd_resume_callback()
+-- RESETDET → usbd_resetdet_callback() + usb0hw_set_pwronprgdone()
+-- USBRST → usb0hw_reset() + usbd_bus_reset() + usbd_reset_callback()
(4) USB device protocol stack
usbd_core.c and usbd_core.h provide a lightweight USB Device protocol stack to handle standard USB device requests and descriptor management:
- Standard Request Processing:
usbd_control_request_process()handles standard device requests such as GET_DESCRIPTOR, SET_ADDRESS, SET_CONFIGURATION, GET_STATUS - Descriptor Management: Retrieves user descriptors through callbacks
such as usbd_get_device_descriptor(),usbd_get_config_descriptor(),usbd_get_string_descriptor(), etc. - Driver registration:
usbd_driver_register()registers the Class driver to the specified interface. - Endpoint callback registration:
usbd_endpoint_register()Registers the endpoint to complete the callback after transmission is completed.
(5) USB Class driver layer
USB0 supports the following USB Class drivers:
| Class driver | Description |
|---|---|
| HID | Human-machine interface device, supports Mouse/Keyboard/Mouse+Keyboard |
| CDC | Communication equipment category, virtual serial port |
| Audio | Audio equipment category, supports speaker/microphone |
The Class driver is registered to the protocol stack via usbd_driver_register(). When the host sends a Class-Specific request, the protocol stack calls the corresponding usbd_driver_handler for processing.
(6) Interrupt handling process
USB0 uses a single interrupt line IRQ_USB0 and registers usb0_irq_handler as the ISR entry point through PLIC_ISR_REGISTER. The interrupt handling flowchart is as follows:
USB0 interrupt trigger
|
v
usb0_irq_handler()
|
Read GINTSTS & GINTMSK
|
+---------+---------+----------+---------+----------+----------+----------+
| | | | | | | |
v v v v v v v v
ENUMDONE OEPINT IEPINT SOF USBSUSP WKUPINT RESETDET USBRST
| | | | | | | |
v v v v v v v v
Clear flag EP OUT EP IN SOF callback Hang up Wake Reset Bus reset
process process callback callback callback detection
USB0 initialization process
(1) Hardware initialization
USB0 hardware initialization is completed via usb0hw_init(), and internally the following operations are performed:
- GPIO configuration: Set PA3 (USB0_DM) and PA4 (USB0_DP) to float mode, controlled by USB0 PHY.
- Power and Clock Enable: Turn on the USB digital module to enable reset and clock.
- DMA and interrupt configuration: Enable AHB DMA (
GAHBCFG), configure IN/OUT endpoint universal interrupt masks (DIEPMSK,DOEPMSK), enable EP0 IN/OUT interrupts (DAINTMSK) - Device configuration: Set speed, descriptor DMA mode, ignore frame number, TRDT timing
- Global interrupt enable: Enable USB reset, enumeration completion, IN/OUT endpoints, SOF, reset detection, wake-up, and other interrupts
- Software connection: Connect the device to the USB bus via
usb0hw_soft_connect().
(2) Device enumeration process
The USB device enumeration process is automatically handled by the protocol stack:
- The host detects device connection (DP pull-up)
- The host sends USB reset → USBRST interrupt → usb0hw_reset() + usbd_bus_reset()
- The host sends the GET_DESCRIPTOR (Device) → protocol stack to return the device descriptor
- Host sends SET_ADDRESS → usbd_set_address() → usb0hw_set_address()
- The host sends a GET_DESCRIPTOR (Configuration) → protocol stack to return a configuration descriptor
- The host sends a SET_CONFIGURATION → usbd_set_configuration_callback() callback
(3) Descriptor configuration
USB descriptors are defined by users in *_descriptor.c for each demo, and the protocol stack is obtained through the following callback function:
unsigned char *usbd_get_device_descriptor(unsigned char bus);
unsigned char *usbd_get_config_descriptor(unsigned char bus);
unsigned char *usbd_get_string_descriptor(unsigned char bus, unsigned char string_index);
unsigned char *usbd_get_device_qualifier_descriptor(unsigned char bus);
unsigned char *usbd_get_other_speed_configuration_descriptor(unsigned char bus, unsigned char index);
(4) Device registration
Register the Class driver to the specified interface via usbd_driver_register().
The parameters of this function are described as follows:
bus: USB bus number (USB0 fixed to 0)driver: Driver structure pointer, containsdriver_numandusbd_driver_handlerintf: interface numberep_addr: The endpoint address used by this interface
A. Single device registration
Single-device registration, with only one interface 0 and one Class-Specific handler. For example, the registration process for mouse is as follows:
usbd_driver_t usbd_mouse_driver;
usbd_mouse_driver.driver_num = 0;
usbd_mouse_driver.usbd_driver_handler = usbd_hid_interface_request_handler; /* hid class handler function */
usbd_driver_register(0, &usbd_mouse_driver, 0, HID_MOUSE_IN_ENDPOINT_ADDRESS);
B. Composite Class Registration (Same Class)
Register similar composite devices, have multiple interfaces, and a Class-Specific handler. For example, the registration process for mouse + keyboard is as follows:
usbd_driver_t usbd_hid_driver;
usbd_hid_driver.driver_num = 0;
usbd_hid_driver.usbd_driver_handler = usbd_hid_interface_request_handler; /* hid class handler function */
usbd_driver_register(0, &usbd_hid_driver, 0, HID_KEYBOARD_IN_ENDPOINT_ADDRESS);
usbd_driver_register(0, &usbd_hid_driver, 1, HID_MOUSE_IN_ENDPOINT_ADDRESS);
C. Composite Registration (Different Categories)
Different class composite devices are registered, with multiple interfaces and different Class-Specific handlers. For example, the registration process for mic + mouse is as follows:
usbd_driver_t usbd_mouse_driver;
usbd_mouse_driver.driver_num = 0;
usbd_mouse_driver.usbd_driver_handler = usbd_hid_interface_request_handler; /* hid class handler function */
usbd_driver_register(0, &usbd_mouse_driver, 0, HID_MOUSE_IN_ENDPOINT_ADDRESS);
usbd_driver_t usbd_mic_driver;
usbd_mic_driver.driver_num = 1;
usbd_mic_driver.usbd_driver_handler = usbd_audio_interface_request_handler;
usbd_driver_register(0, &usbd_mic_driver, 1, 0xff);
usbd_driver_register(0, &usbd_mic_driver, 2, AUDIO_MIC_IN_ENDPOINT_ADDRESS);
(5) Endpoint registration and callbacks
Registering endpoints via usbd_endpoint_register() to complete the callback:
usbd_endpoint_register(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, usbd_hid_int_callback);
(6) Complete initialization example
Below, using the TL322x HID Mouse demo as an example, we demonstrate the complete USB0 initialization process (other chip voltages, clock configurations, and wake-up may vary; please refer to the code for details):
void user_init(void)
{
// 1. GPIO initialization
gpio_function_en(LED1);
gpio_output_en(LED1);
gpio_input_dis(LED1);
// 2. Register Class drivers
usbd_mouse_driver.driver_num = 0;
usbd_mouse_driver.usbd_driver_handler = usbd_hid_interface_request_handler;
usbd_driver_register(0, &usbd_mouse_driver, 0, HID_MOUSE_IN_ENDPOINT_ADDRESS);
// 3. Register endpoint callback
usbd_endpoint_register(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, usbd_hid_int_callback);
// 4. Configure voltage and clock (USB0 requires digital voltage 1.1V, HCLK minimum 48MHz)
pm_set_dig_ldo(DIG_VOL_1V1_MODE, 1000);
PLL_192M_D25F_96M_HCLK_N22_48M_PCLK_48M_MSPI_48M;
// 5. Initialize USB0 hardware
#if USB_HIGH_SPEED_EN
usb0hw_init(USB0_SPEED_HIGH);
#else
usb0hw_init(USB0_SPEED_FULL);
#endif
// 6. Configure the FIFO
usb0hw_set_grxfsiz(0x100); // RX FIFO: 256 words
usb0hw_set_epin_size(USB0_EP0, 0x100, 64); // EP0 TX FIFO: 64 words
usb0hw_set_epin_size(USB0_EP2, 0x100 + 64, 64); // EP2 TX FIFO: 64 words
// 7. Enable interrupt
core_interrupt_enable();
plic_interrupt_enable(IRQ_USB0);
// 8. Configure low-power wake-up
pm_set_usb0_wakeup();
pm_set_suspend_power_cfg(FLD_PD_USB_EN, 1);
}
USB0 data transfer
(1) IN Transmission (Device → Host)
Send data to the host via usbd_ep_write():
usbd_ep_write(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, hid_report_data, 5);
Internal Process:
usbd_ep_write()→usb0hw_write_ep_data()- Fill DMA descriptors (
fieldssuch as tx_bytes,ioc,sp,l, etc.) - Set the DMA address and start the transmission (
CNAK+EPENA) - After DMA transfer is completed, a
DIEPINT_XFERCOMPLinterrupt is triggered. Note that this interrupt refers to DMA transfer completion, meaning data is transferred from SRAM to USB FIFO, not endpoint data transfer. - Interrupt handling calls
usbd_epin_complete_handler()→ User-registered callback function
Note
- For non-interrupt endpoints, when
lenis an integer multiple of MPS, the driver automatically sets thesp(short packet) flag to notify the host that transmission is complete. - For interrupt-type endpoints,
the SPflag is not automatically set.
(2) OUT Transmission (Host → Device)
Receive host data via usbd_ep_read():
usbd_ep_read(0, CDC_DATA_OUT_ENDPOINT_ADDRESS, cdc_out_buffer, sizeof(cdc_out_buffer));
Internal Process:
usbd_ep_read()→usb0hw_read_ep_data()- Fill DMA descriptors (
fieldssuch as rx_bytes,ioc,l, etc.) - Set the DMA address and start the transmission (
CNAK+EPENA) - Triggers
DOEPINT_XFERCOMPLinterrupt after transmission complete - Interrupt handling calls
usbd_epout_complete_handler()→ the callback function registered by the user
Note
usbd_ep_read()is configured only to receive DMA; actual data reception is completed during the interrupt- The actual received length is returned via the
lenparameter in theepout_callback lenmust be an integer multiple of the MPS of the corresponding endpoint
(3) Control transfer
The control transmission of EP0 is automatically handled by the protocol stack:
- Setup stage: The hardware receives the Setup package →
DOEPINT_SETUPinterrupt →usbd_control_request_process()parsing request - Data phase: Depending on the request direction, the protocol stack automatically calls
usbd_ep_write()orusbd_ep_read()to transmit data - Status stage:
DOEPINT_STSPHSERCVDinterrupt → complete standard request processing
For Class-Specific requests, the protocol stack calls the registered usbd_driver_handler to handle it.
(4) Endpoint Stall management
Set Stall:
usbd_ep_stall(0, ep_addr);
Internally, call usb0hw_set_inep_stall() or usb0hw_set_outep_stall() based on endpoint direction. For EP0, after setting the Stall setup, it will automatically reconfigure to receive the next Setup package.
Clear Stall:
usbd_ep_clear_stall(0, ep_addr);
Internally, call usb0hw_clear_epin_stall() or usb0hw_clear_epout_stall() based on endpoint direction.
USB0 power management
(1) Suspend and wake-up
USB0 supports USB suspend and resume mechanisms:
Suspend callback:
void usbd_suspend_callback(unsigned char bus)
{
if(bus == 0) {
usb0hw_pcgc_clk_dis(); //Disable PCGC clock
usb0hw_phy_pll_dis(); //Disable PHY PLL
g_usb_suspend_flag = 1;
}
}
Wake-up callback:
void usbd_resume_callback(unsigned char bus)
{
if(bus == 0) {
usb0hw_pcgc_clk_en(); //Enable PCGC clocks
usb0hw_phy_pll_en(); //Enable PHY PLL
g_usb_suspend_flag = 0;
}
}
(2) Remote wake-up
The device can actively wake up the host via usb0hw_remote_wakeup():
void usb0hw_remote_wakeup(void)
{
if (!(reg_usb_dsts & FLD_USB_DSTS_SUSPSTS)) {
return; //If not in a suspended state, return directly
}
usb0hw_pcgc_clk_en();
usb0hw_phy_pll_en();
BM_SET(reg_usb_dctl, FLD_USB_DCTL_RMTWKUPSIG); //Trigger remote wake-up signal
delay_ms(10);
BM_CLR(reg_usb_dctl, FLD_USB_DCTL_RMTWKUPSIG);
}
(3) Low power mode configuration
In the suspended state, the system can enter low-power mode. Below, using the TL322x as an example, we demonstrate the low-power mode configuration (other chips may have different wake-up sleep configurations; please refer to the code for details):
//Configure the wake-up source during initialization
pm_set_usb0_wakeup();
pm_set_suspend_power_cfg(FLD_PD_USB_EN, 1);
//The main loop enters low power consumption
void main_loop(void)
{
if (g_usb_suspend_flag) {
pm_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_CORE, PM_TICK_STIMER, 0);
}
}
USB0 test mode
USB0 supports test modes defined by the USB 2.0 specification for electrical characteristic testing and compliance testing:
| Test mode | Enumeration value | Description |
|---|---|---|
| Disable | USB0_TEST_MODE_DISABLE |
Turn off test mode |
| Test_J | USB0_TEST_J_MODE |
Test the J state |
| Test_K | USB0_TEST_K |
Test the K state |
| Test_SE0_NAK | USB0_TEST_SE0_NAK_MODE |
Test SE0 NAK status |
| Test_Packet | USB0_TEST_PACKET_MODE |
Test packet mode |
| Test_Force_Enable | USB0_TEST_FORCE_ENABLE_MODE |
Forced enablement |
Enter test mode via usbd_test_mode():
void usbd_test_mode(unsigned char bus, unsigned char test_mode)
{
delay_ms(1); //Wait for the state phase to complete
usb0hw_test_mode(test_mode);
}
API reference
(1) Driver Layer API (usb0hw)
| API | Function |
|---|---|
usb0hw_init(speed_sel) |
Initialize USB0, configure speed, DMA, and interrupts |
usb0hw_power_down() |
Turn off the USB0 power and clock |
usb0hw_reset() |
Reset USB0 (clear address, FIFO, NAK default endpoints) |
usb0hw_ep_open(ep_num, ep_dir, ep_type, ep_mps) |
Open the specified endpoint, configure the type and maximum packet size |
usb0hw_ep_close(ep_num, ep_dir) |
Close the specified endpoint, disable and clear the configuration |
usb0hw_write_ep_data(ep_num, buf, len) |
Sending Data Through DMA (IN Endpoint) |
usb0hw_read_ep_data(ep_num, buf, len) |
Configure DMA to receive data (OUT endpoint) |
usb0hw_get_epin_len(ep_num) |
Retrieves the length of data transmitted by the IN endpoint |
usb0hw_get_epout_len(ep_num) |
Retrieves the length of data received by the OUT endpoint |
usb0hw_set_grxfsiz(size) |
Set RX FIFO size (32-bit word units) |
usb0hw_set_epin_size(ep_num, addr, size) |
Set the start address and size of the TX FIFO on the IN endpoint |
usb0hw_set_epin_fifo(ep_num, fifo_num) |
Set the TX FIFO number used by the IN endpoint |
usb0hw_flush_tx_fifo(ep_num) |
Refresh TX FIFOs (0x10 Refresh all TX FIFOs) |
usb0hw_flush_rx_fifo() |
Refresh RX FIFO |
usb0hw_set_address(dev_addr) |
Set the device address |
usb0hw_get_speed() |
Get the current enumeration speed (High/Full) |
usb0hw_get_sof_fn() |
Get the SOF frame number |
usb0hw_get_timer_stamp() |
Get the timestamp |
usb0hw_soft_connect() |
Software Connection (Pull-up DP) |
usb0hw_soft_disconnect() |
Software disconnect (dropdown DP) |
usb0hw_remote_wakeup() |
Trigger remote wake-up signal |
usb0hw_test_mode(mode) |
Set the test mode |
usb0hw_set_pwronprgdone() |
Set Power-On Program Done |
usb0hw_get_gintsts() |
Obtain global interrupt status |
usb0hw_clear_gintsts(status) |
Clear global interrupt status |
usb0hw_get_daint() |
Retrieves the interrupt status of all endpoints on the device |
usb0hw_daintmsk_en(mask) |
Enable device endpoint interrupt masks |
usb0hw_daintmsk_dis(mask) |
Disable device endpoint interrupt masks |
usb0hw_get_doepint(ep_num) |
Retrieves the interrupt status of the OUT endpoint |
usb0hw_clear_doepint(ep_num, status) |
Clear the interrupted state on the OUT endpoint |
usb0hw_get_diepint(ep_num) |
Retrieves the interrupt status of the IN endpoint |
usb0hw_clear_diepint(ep_num, status) |
Clear the interrupt state on the IN endpoint |
usb0hw_set_inep_stall(ep_num) |
Set the IN endpoint STALL |
usb0hw_set_outep_stall(ep_num) |
Set the OUT endpoint STALL |
usb0hw_clear_epin_stall(ep_num) |
Clear the IN endpoint STALL |
usb0hw_clear_epout_stall(ep_num) |
Clear the OUT endpoint STALL |
usb0hw_pcgc_clk_en() |
Enable PCGC clock |
usb0hw_pcgc_clk_dis() |
Turn off the PCGC clock |
usb0hw_phy_pll_en() |
Enable PHY PLL |
usb0hw_phy_pll_dis() |
Disable PHY PLL |
(2) Port Adaptation Layer API
| API | Function |
|---|---|
usbd_ep_open(bus, endpoint_desc) |
Open the endpoint (adapt usb0hw_ep_open) |
usbd_ep_write(bus, ep_addr, buf, len) |
Send data (adapt usb0hw_write_ep_data) |
usbd_ep_read(bus, ep_addr, buf, len) |
Receiving Data (Adapted usb0hw_read_ep_data) |
usbd_ep_stall(bus, ep_addr) |
Set the STALL endpoint |
usbd_ep_clear_stall(bus, ep_addr) |
Clear the STALL endpoint |
usbd_set_address(bus, address) |
Set the device address |
usbd_test_mode(bus, test_mode) |
Enter test mode |
usb0_irq_handler() |
USB0 Global Interrupt Service (ISR) |
(3) USB Device Protocol Stack API (usbd_core)
| API | Function |
|---|---|
usbd_driver_register(bus, driver, intf, ep_addr) |
Register the Class driver to the specified interface |
usbd_endpoint_register(bus, ep_addr, callback) |
Registration endpoint transmission completion callback |
usbd_control_request_process(bus, setup, setup_stage) |
Handle control requests |
usbd_bus_reset(bus) |
Bus reset processing |
usbd_epin_complete_handler(bus, ep_index, len) |
IN endpoint transfer completion processing (callback to user registration function) |
usbd_epout_complete_handler(bus, ep_index, len) |
OUT endpoint transmission completion processing (callback to user registration function) |
usbd_set_configuration_callback(bus, config_num) |
SET_CONFIGURATION Callback (weak, user can rewrite) |
usbd_set_interface_callback(bus, intf, alt_intf) |
SET_INTERFACE Callback (weak, user can rewrite) |
(4) USB Class driver API
HID Class:
| API | Function |
|---|---|
usbd_hid_interface_request_handler(bus, setup) |
HID Class-Specific Request Handling (GET/SET_REPORT, etc.) |
CDC Class:
| API | Function |
|---|---|
usbd_cdc_interface_request_handler(bus, setup) |
CDC Class-Specific request processing |
Audio Class:
| API | Function |
|---|---|
usbd_audio_interface_request_handler(bus, setup) |
Audio Class-Specific request processing |
(5) Weakly defined callback function (user-rewritable)
| Function | Description |
|---|---|
usbd_suspend_callback(bus) |
USB suspend callback |
usbd_resume_callback(bus) |
USB wake-up callback |
usbd_resetdet_callback(bus) |
USB reset detection callback |
usbd_reset_callback(bus) |
USB bus reset callback |
usbd_sof_callback(bus) |
SOF frame start callback |
usbd_set_configuration_callback(bus, config_num) |
SET_CONFIGURATION completes the callback |
usbd_set_interface_callback(bus, intf, alt_intf) |
SET_INTERFACE completes the callback |
Demo reference
The complete USB0 sample code is located in the USB0_Demo directory, allowing you to switch between different demo scenarios using USB_DEMO_TYPE macros.
(1) HID Mouse Demo
The HID Mouse demo demonstrates a standard USB HID mouse device that supports the self-animated block feature.
Key Documents:
| File | Description |
|---|---|
hid_mouse_app.c |
Application layer code |
hid_mouse_descriptor.c |
Device/configuration/string/HID report descriptor |
hid_mouse_descriptor.h |
Descriptor macro definitions (VID/PID/ENDPOINT ADDRESS, etc.) |
Descriptor configuration:
#define USB_HIGH_SPEED_EN 1
#define ID_VENDOR 0x248a
#define ID_PRODUCT 0x8006
#define ID_VERSION 0x0100
#define STRING_VENDOR "Telink Semi-conductor Ltd, Co"
#define STRING_PRODUCT "Telink Mouse"
#define STRING_SERIAL "Mouse demo"
#define HID_MOUSE_IN_ENDPOINT_ADDRESS 0x82
#define HID_MOUDE_IN_ENDPOINT_SIZE 0x08
#define HID_MOUSE_IN_ENDPOINT_INTERVAL 0x01
- USB_HIGH_SPEED_EN defines whether high-speed mode is enabled. For full-speed mode, define
USB_HIGH_SPEED_ENas 0. - ID_VENDOR, ID_PRODUCT, ID_VERSION define the device's VID/PID/version number.
- STRING_VENDOR, STRING_PRODUCT, STRING_SERIAL define the supplier, product name, and serial number of the device.
- HID_MOUSE_IN_ENDPOINT_ADDRESS, HID_MOUDE_IN_ENDPOINT_SIZE, HID_MOUSE_IN_ENDPOINT_INTERVAL Defines the address, maximum packet size, and spacing of mouse IN endpoints.
Driver registration:
usbd_mouse_driver.driver_num = 0;
usbd_mouse_driver.usbd_driver_handler = usbd_hid_interface_request_handler;
usbd_driver_register(0, &usbd_mouse_driver, 0, HID_MOUSE_IN_ENDPOINT_ADDRESS);
usbd_endpoint_register(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, usbd_hid_int_callback);
The mouse driver is registered to driver_num 0, and the mouse IN endpoint address is HID_MOUSE_IN_ENDPOINT_ADDRESS.
FIFO Configuration:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(USB0_EP2, 0x100 + 64, 64);
EP2 IN requires assigning a separate TX FIFO, with the starting address after EP0
HID Data Transmission:
hid_report_data[0] = 1;
hid_report_data[1] = 0;
hid_report_data[2] = 0;
hid_report_data[3] = 0;
hid_report_data[4] = 0;
usbd_ep_write(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, hid_report_data, 5);
USB_MOUSE_DRAW_SQUARE defines whether to enable the auto-animated block feature. Enabled by default. To disable it, define USB_MOUSE_DRAW_SQUARE to 0. If you need to enable the line drawing feature, just define g_send_flag as 1.
(2) HID Keyboard Demo
The HID Keyboard Demo demonstrates a standard USB HID keyboard device that supports automatic key sending.
Key Documents:
| File | Description |
|---|---|
hid_keyboard_app.c |
Application layer code |
hid_keyboard_descriptor.c |
Keyboard descriptors |
hid_keyboard_descriptor.h |
Descriptor macro definitions (VID/PID/ENDPOINT ADDRESS, etc.) |
Descriptor configuration:
#define USB_HIGH_SPEED_EN 1
#define ID_VENDOR 0x248a
#define ID_PRODUCT 0x8006
#define ID_VERSION 0x0100
#define STRING_VENDOR "Telink Semi-conductor Ltd, Co"
#define STRING_PRODUCT "Telink Keyboard"
#define STRING_SERIAL "Keyboard demo"
#define HID_KEYBOARD_IN_ENDPOINT_ADDRESS 0x81
#define HID_KEYBOARD_IN_ENDPOINT_SIZE 0x10
#define HID_KEYBOARD_IN_ENDPOINT_INTERVAL 0x01
- USB_HIGH_SPEED_EN defines whether high-speed mode is enabled. For full-speed mode, define
USB_HIGH_SPEED_ENas 0. - STRING_VENDOR, STRING_PRODUCT, STRING_SERIAL define the supplier, product name, and serial number of the device.
- HID_KEYBOARD_IN_ENDPOINT_ADDRESS, HID_KEYBOARD_IN_ENDPOINT_SIZE, HID_KEYBOARD_IN_ENDPOINT_INTERVAL Defines the address, maximum packet size, and spacing of the keyboard IN endpoint.
Driver registration:
usbd_keyboard_driver.driver_num = 0;
usbd_keyboard_driver.usbd_driver_handler = usbd_hid_interface_request_handler;
usbd_driver_register(0, &usbd_keyboard_driver, 0, HID_KEYBOARD_IN_ENDPOINT_ADDRESS);
usbd_endpoint_register(0, HID_KEYBOARD_IN_ENDPOINT_ADDRESS, usbd_hid_int_callback);
FIFO Configuration:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(USB0_EP1, 0x100 + 64, 64);
- EP1 IN requires the assignment of a separate TX FIFO, starting after EP0
HID Data Transmission:
hid_report_data[0] = 0;
hid_report_data[1] = 0;
hid_report_data[2] = 0;
hid_report_data[3] = 0;
hid_report_data[4] = 0;
hid_report_data[5] = 0;
hid_report_data[6] = 0;
hid_report_data[7] = 0;
usbd_ep_write(0, HID_KEYBOARD_IN_ENDPOINT_ADDRESS, hid_report_data, 8);
(3) HID Mouse + Keyboard Composite Demo
The HID Mouse + Keyboard Composite Demo demonstrates a composite device that includes two HID ports: mouse (port 0) and keyboard (interface 1).
Key Documents:
| File | Description |
|---|---|
hid_mouse_keyboard_app.c |
Application layer code |
hid_mouse_keyboard_descriptor.c |
Composite device descriptors |
hid_mouse_keyboard_descriptor.h |
Descriptor macro definition |
Descriptor configuration:
#define USB_HIGH_SPEED_EN 1
#define ID_VENDOR 0x248a
#define ID_PRODUCT 0x8006
#define ID_VERSION 0x0100
#define STRING_VENDOR "Telink Semi-conductor Ltd, Co"
#define STRING_PRODUCT "Telink KM"
#define STRING_SERIAL "KM demo"
#define HID_KEYBOARD_IN_ENDPOINT_ADDRESS 0x81
#define HID_KEYBOARD_IN_ENDPOINT_SIZE 0x10
#define HID_KEYBOARD_IN_ENDPOINT_INTERVAL 0x01
#define HID_MOUSE_IN_ENDPOINT_ADDRESS 0x82
#define HID_MOUDE_IN_ENDPOINT_SIZE 0x08
#define HID_MOUSE_IN_ENDPOINT_INTERVAL 0x01
The keyboard uses EP1 (0x81), the mouse uses EP2 (0x82), and the two endpoints use different IN addresses.
Driver registration:
usbd_hid_driver.driver_num = 0;
usbd_hid_driver.usbd_driver_handler = usbd_hid_interface_request_handler;
usbd_driver_register(0, &usbd_hid_driver, 0, HID_KEYBOARD_IN_ENDPOINT_ADDRESS);
usbd_driver_register(0, &usbd_hid_driver, 1, HID_MOUSE_IN_ENDPOINT_ADDRESS);
usbd_endpoint_register(0, HID_KEYBOARD_IN_ENDPOINT_ADDRESS, usbd_hid_keyboard_int_callback);
usbd_endpoint_register(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, usbd_hid_mouse_int_callback);
The same usbd_hid_driver registers to interface 0 (keyboard) and interface 1 (mouse), with each endpoint registering its own independent callback function.
FIFO Configuration:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(HID_KEYBOARD_IN_ENDPOINT_ADDRESS & 0x7f, 0x100 + 64, 64);
usb0hw_set_epin_size(HID_MOUSE_IN_ENDPOINT_ADDRESS & 0x7f, 0x100 + 64 + 64, 64);
The composite device needs to configure TX FIFOs for each IN endpoint (EP0, EP1, EP2), with addresses increasing in sequence.
(4) CDC Demo
The CDC Demo demonstrated the USB communication device class (virtual serial port), supporting data echo (Echo) and batch transfer. The device includes a CDC control interface and a CDC data interface.
Key Documents:
| File | Description |
|---|---|
cdc_app_app.c |
Application layer code |
cdc_descriptor.c |
CDC Descriptor (IAD + CDC ACM + Data Interface) |
cdc_descriptor.h |
Descriptor macro definition |
Descriptor configuration:
#define USB_HIGH_SPEED_EN 1
#define ID_VENDOR 0x248a
#define ID_PRODUCT 0x8002
#define ID_VERSION 0x0100
#define STRING_VENDOR "Telink Semi-conductor Ltd, Co"
#define STRING_PRODUCT "Telink CDC"
#define STRING_SERIAL "CDC demo"
#define CDC_DATA_IN_ENDPOINT_ADDRESS 0x84
#define CDC_DATA_OUT_ENDPOINT_ADDRESS 0x05
#if USB_HIGH_SPEED_EN
#define CDC_DATA_IN_ENDPOINT_SIZE 0x200
#define CDC_DATA_OUT_ENDPOINT_SIZE 0x200
#else
#define CDC_DATA_IN_ENDPOINT_SIZE 0x40
#define CDC_DATA_OUT_ENDPOINT_SIZE 0x40
#endif
#define CDC_NOTIFICATION_IN_ENDPOINT_ADDRESS 0x82
#define CDC_NOTIFICATION_IN_ENDPOINT_SIZE 0x08
- CDC devices have three endpoints: notification endpoints (EP2 IN,
0x82, Interrupt type), data IN endpoints (EP4 IN,0x84, Bulk type), and data OUT endpoints (EP5 OUT,0x05, Bulk type). - In High Speed mode, the maximum packet size for Bulk endpoints is 512 bytes (
0x200), and at Full Speed it is 64 bytes (0x40).
Driver registration:
usbd_cdc_driver.driver_num = 0;
usbd_cdc_driver.usbd_driver_handler = usbd_cdc_interface_request_handler;
usbd_driver_register(0, &usbd_cdc_driver, 0, CDC_NOTIFICATION_IN_ENDPOINT_ADDRESS);
usbd_driver_register(0, &usbd_cdc_driver, 1, CDC_DATA_OUT_ENDPOINT_ADDRESS);
usbd_driver_register(0, &usbd_cdc_driver, 1, CDC_DATA_IN_ENDPOINT_ADDRESS);
usbd_endpoint_register(0, CDC_DATA_IN_ENDPOINT_ADDRESS, usbd_cdc_epin_callback);
usbd_endpoint_register(0, CDC_DATA_OUT_ENDPOINT_ADDRESS, usbd_cdc_epout_callback);
usbd_endpoint_register(0, CDC_NOTIFICATION_IN_ENDPOINT_ADDRESS, usbd_cdc_ep_notify_callback);
- The CDC driver is registered to interface 0 (control interface + notification endpoint) and interface 1 (data interface + IN/OUT endpoint).
- Each endpoint independently registers callback functions.
FIFO Configuration:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 16);
usb0hw_set_epin_size(CDC_NOTIFICATION_IN_ENDPOINT_ADDRESS & 0x7f, 0x100 + 16, 16);
usb0hw_set_epin_size(CDC_DATA_IN_ENDPOINT_ADDRESS & 0x7f, 0x100 + 16 + 16, 64);
- CDC_NOTIFICATION_IN_ENDPOINT_ADDRESS needs to be assigned a separate TX FIFO, with the starting address after EP0.
- CDC_DATA_IN_ENDPOINT_ADDRESS requires the assignment of a separate TX FIFO, with the starting address after the notification endpoint.
SET_CONFIGURATION Callback and Data Display:
void usbd_set_configuration_callback(unsigned char bus, unsigned char config_num)
{
/* receive first cdc out buffer. */
usbd_ep_read(bus, CDC_DATA_OUT_ENDPOINT_ADDRESS, cdc_out_buffer, sizeof(cdc_out_buffer));
}
void main_loop(void)
{
/* echo */
if (cdc_out_data_len) {
cdc_epin_busy = true;
usbd_ep_write(0, CDC_DATA_IN_ENDPOINT_ADDRESS, cdc_out_buffer, cdc_out_data_len);
while (cdc_epin_busy) {
}
/* receive next cdc out buffer. */
cdc_out_data_len = 0;
usbd_ep_read(0, CDC_DATA_OUT_ENDPOINT_ADDRESS, cdc_out_buffer, sizeof(cdc_out_buffer));
}
}
- Initiate the first OUT reception in
the usbd_set_configuration_callback. main_loopimplements echo logic: after receiving data, it is sent back via the IN endpoint, then continues to receive the next packet.
(5) Audio Speaker Demo
The Audio Speaker demo showcased the USB Audio 1.0 speaker device, supporting 16KHz 16-bit stereo output. The device includes an Audio Control interface (interface 0, no endpoint) and an Audio Streaming interface (interface 1, an ISO OUT endpoint).
Key Documents:
| File | Description |
|---|---|
audio_spk_app.c |
Application layer code |
audio_spk_descriptor.c |
Audio Descriptor (IAD + Audio Control + Audio Streaming) |
audio_spk_descriptor.h |
Descriptor macro definition |
Descriptor configuration:
#define USB_HIGH_SPEED_EN 1
#define ID_VENDOR 0x248a
#define ID_PRODUCT 0x8006
#define ID_VERSION 0x0100
#define STRING_VENDOR "Telink Semi-conductor Ltd, Co"
#define STRING_PRODUCT "Telink SPK"
#define STRING_SERIAL "SPK demo"
#define AUDIO_SPK_OUT_ENDPOINT_ADDRESS 0x06
#define AUDIO_SPK_OUT_ENDPOINT_SIZE 0x0040
- The Audio Streaming interface uses EP6 OUT(
0x06), Isochronous type, with a maximum packet size of 64 bytes. - The size of the audio packet is determined by the sampling rate and number of channels:
AUDIO_OUT_PACKET = (16000 * 2 * 2) / 1000 = 64bytes/ms.
Driver registration:
usbd_spk_driver.driver_num = 0;
usbd_spk_driver.usbd_driver_handler = usbd_audio_interface_request_handler;
usbd_driver_register(0, &usbd_spk_driver, 0, 0xff);
usbd_driver_register(0, &usbd_spk_driver, 1, AUDIO_SPK_OUT_ENDPOINT_ADDRESS);
usbd_endpoint_register(0, AUDIO_SPK_OUT_ENDPOINT_ADDRESS, usbd_audio_epout_callback);
The Audio Control interface (interface 0) only handles Class-Specific control requests (mute/volume), ep_addr the parameter passes 0xff to indicate no endpoint.
FIFO Configuration:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
The OUT endpoint shares all RX FIFOs.
Audio Data Reception:
void usbd_audio_open(unsigned char bus, unsigned char intf)
{
/* receive first spk out data. */
usbd_ep_read(0, AUDIO_SPK_OUT_ENDPOINT_ADDRESS, audio_buffer, sizeof(audio_buffer));
}
void usbd_audio_epout_callback(unsigned char bus, unsigned char ep_addr, unsigned int len)
{
/* receive spk out data. */
usbd_ep_read(0, AUDIO_SPK_OUT_ENDPOINT_ADDRESS, audio_buffer, sizeof(audio_buffer));
}
- Initiate the first OUT reception in a
usbd_audio_opencallback. - After each reception, the next packet is received through the
usbd_audio_epout_callback, forming a continuous audio data stream.
Feature Unit Control:
unsigned char usbd_audio_interface_cb(unsigned char bus, usb_control_request_t const *setup)
{
//Handling MUTE and VOLUME control requests (CUR/MIN/MAX/RES)
}
Handle the host's Feature Unit requests through usbd_audio_interface_cb, including mute control and volume control (read CUR/MIN/MAX/RES, set CUR).
(6) Audio Microphone Demo
The Audio Microphone demo demonstrated a USB Audio 1.0 microphone device supporting 16KHz 16-bit stereo input. The device includes an Audio Control interface (interface 0, no endpoint) and an Audio Streaming interface (interface 1, an ISO IN endpoint).
Key Documents:
| File | Description |
|---|---|
audio_mic_app.c |
Application layer code |
audio_mic_descriptor.c |
Audio microphone descriptor |
audio_mic_descriptor.h |
Descriptor macro definition |
Descriptor configuration:
#define USB_HIGH_SPEED_EN 1
#define ID_VENDOR 0x248a
#define ID_PRODUCT 0x8006
#define ID_VERSION 0x0100
#define STRING_VENDOR "Telink Semi-conductor Ltd, Co"
#define STRING_PRODUCT "Telink MIC"
#define STRING_SERIAL "MIC demo"
#define AUDIO_MIC_IN_ENDPOINT_ADDRESS 0x87
#define AUDIO_MIC_IN_ENDPOIRT_SIZE 0x0040
The Audio Streaming interface uses EP7 IN(0x87), Isochronous type, with a maximum packet size of 64 bytes.
Driver registration:
usbd_mic_driver.driver_num = 0;
usbd_mic_driver.usbd_driver_handler = usbd_audio_interface_request_handler;
usbd_driver_register(0, &usbd_mic_driver, 0, 0xff);
usbd_driver_register(0, &usbd_mic_driver, 1, AUDIO_MIC_IN_ENDPOINT_ADDRESS);
usbd_endpoint_register(0, AUDIO_MIC_IN_ENDPOINT_ADDRESS, usbd_audio_epin_callback);
The Audio Control interface (interface 0) only handles Class-Specific control requests.
FIFO Configuration:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(USB0_EP7, 0x100 + 64, 64);
EP7 IN requires the assignment of a separate TX FIFO, starting with an address after EP0.
Audio data transmission:
void usbd_audio_open(unsigned char bus, unsigned char intf)
{
tx_flag = 1;
usbd_ep_write(0, AUDIO_MIC_IN_ENDPOINT_ADDRESS, audio_buffer, sizeof(audio_buffer));
}
void usbd_audio_epin_callback(unsigned char bus, unsigned char ep_addr, unsigned int len)
{
send_data++;
memset(audio_buffer, send_data, AUDIO_IN_PACKET);
usbd_ep_write(0, AUDIO_MIC_IN_ENDPOINT_ADDRESS, audio_buffer, sizeof(audio_buffer));
}
- Initiate the first IN send in the
usbd_audio_opencallback. - After each send, new audio data is filled with
usbd_audio_epin_callbackand continues to be sent, forming a continuous audio data stream.
Chip difference — TL322x
The TL322x USB0 clock is based on HCLK and cannot be less than 48 MHz.
The voltage of a digital LDO must be 1.1V.
Therefore, the demo code needs to set the digital LDO voltage to 1.1V. If the HCLK is less than 48MHz, then the HCLK needs to be set to 48MHz or higher. The code is as follows
pm_set_dig_ldo(DIG_VOL_1V1_MODE, 1000); /* Set digital LDO voltage to 1.1V */
PLL_192M_D25F_96M_HCLK_N22_48M_PCLK_48M_MSPI_48M; /* Set HCLK to 48MHz */
AES
Overview
Telink chips provide hardware symmetric encryption acceleration. There are two implementation methods. The encryption modules supported by different chips are shown in the table below:
| Chips | Encryption modules |
|---|---|
| TLSR820x/TLSR8373 / TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC122x / TC123x / TLSR820x/TLSR8373 / TLSR825x/TLSR8359 / TLSR827x/TLSR8355 | Independent AES |
| TL321x / TL721x / TL322x / TL323x / TL751x | AES in the SKE Module |
Independent AES
- Principle
Hardware AES-128 module, uses a 16-byte key to process 16-byte data blocks, completing encryption and decryption of one block per call, and defaults to ECB mode.
- API
Encryption and decryption use two APIs: aes_encrypt and aes_decrypt.
int aes_encrypt(unsigned char *key, unsigned char *plaintext, unsigned char *result)
int aes_decrypt(unsigned char *key, unsigned char *decrypttext, unsigned char *result)
- Example usage
Basic encryption and decryption:
unsigned char key[16] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f};
unsigned char plaintext[16] = {0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,
0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff};
unsigned char ciphertext[16] = {0};
// Encryption (Return Success Status)
aes_encrypt(key, plaintext, ciphertext);
// Decryption (Return Whether Success Was Achieved)
aes_decrypt(key, ciphertext, plaintext);
AES in the SKE Module
- Principle
AES is located within the SKE (Symmetric Key Engine) module, serving as an upgrade to the standalone AES module. It supports multiple algorithms such as AES-128/192/256, as well as direct support for multiple operating modes including ECB/CBC/CTR.
- API
Both encryption and decryption use the same API, which is specified separately through the crypto parameter.
unsigned int ske_lp_crypto(SKE_ALG alg, SKE_MODE mode, SKE_CRYPTO crypto, unsigned char *key, unsigned short sp_key_idx, unsigned char *iv, unsigned char *in, unsigned char *out, unsigned int bytes);
- Data types
SKE_ALG — AES algorithm pattern:
| Enumeration value | Description |
|---|---|
| SKE_ALG_AES_128 | AES 128-bit key |
| SKE_ALG_AES_192 | AES 192-bit key |
| SKE_ALG_AES_256 | AES 256-bit key |
SKE_MODE — AES algorithm operating mode:
| Enumeration value | Description |
|---|---|
| SKE_MODE_ECB | ECB Mode |
| SKE_MODE_CBC | CBC Mode |
| SKE_MODE_CTR | CTR Mode |
SKE_CRYPTO — AES algorithm operation direction:
| Enumeration value | Description |
|---|---|
| SKE_CRYPTO_ENCRYPT | encrypt |
| SKE_CRYPTO_DECRYPT | decrypt |
- Example usage
Basic encryption and decryption:
unsigned char std_key[16] = {0xE0,0x70,0x99,0xF1,0xBF,0xAF,0xFD,0x7F,
0x24,0x0C,0xD7,0x90,0xCA,0x4F,0xE1,0x34};
unsigned char std_in[48] = {0x81,0x70,0x99,0x44,0xE0,0xCB,0x2E,0x1D,
0xB5,0xB0,0xA4,0x77,0xD1,0xA8,0x53,0x9B,
0x0A,0x87,0x86,0xE3,0x4E,0xAA,0xED,0x99,
0x30,0x3E,0xA6,0x97,0x55,0x95,0xB2,0x45,
0x4D,0x5D,0x7F,0x91,0xEB,0xBD,0x4A,0xCD,
0x72,0x6C,0x0E,0x0E,0x5E,0x3E,0xB5,0x5E};
unsigned char cipher[48] = {0};
unsigned char replain[48] = {0};
// Encryption (Return Success Status)
ske_lp_crypto(SKE_ALG_AES_128, SKE_MODE_ECB, SKE_CRYPTO_ENCRYPT, std_key, 0, NULL, std_in, cipher, 48);;
// Decryption (Return Whether Success Was Achieved)
ske_lp_crypto(SKE_ALG_AES_128, SKE_MODE_ECB, SKE_CRYPTO_DECRYPT, std_key, 0, NULL, cipher, replain, 48);
Explanation
SKE requires additional specification of algorithm ('SKE_ALG_AES_128'), mode ('SKE_MODE_ECB'), encryption and decryption direction ('SKE_CRYPTO_ENCRYPT'/'SKE_CRYPTO_DECRYPT'), etc.
Coremark and Dhrystone
Overview
Two important processor metrics are power consumption and performance. The most well-known and widely used benchmarks in the embedded processor field are Dhrystone and CoreMark, both used to measure integer computational performance:
- Dhrystone: Unit DMIPS/MHz, classic integer performance benchmark.
- CoreMark: Unit CoreMark/MHz, an industrial standard benchmark launched by EEMBC designed to replace Dhrystone.
The two demos share the same project compilation configuration: using cstartup_flash. S + flash_boot_ramcode.link. Except for the vector table, all code is executed in RAM to avoid Flash latency affecting benchmark results. Due to IRAM size limitations, projects with large code volumes cannot use this configuration. S/link files must be used together in the project and cannot be mixed. For details, please refer to the Software Startup chapter.
Supported chips
| Chip series | Chip model | Dhrystone | CoreMark |
|---|---|---|---|
| TC series | TC321x | √ | √ |
| TL series | TLSR921x/TLSR951x | √ | √ |
| TL series | TLSR922x/TLSR952x | √ | √ |
| TL series | TL321x | √ | √ |
| TL series | TL721x | √ | √ |
| TL series | TL322x | √ | √ |
| TL series | TL323x | √ | √ |
| TL series | TL751x | √ | √ |
Explanation
All the above chips support both Dhrystone and CoreMark tests; chips not listed are not supported.
Quick get started
If it's your first time using it, you can quickly complete the Dhrystone or CoreMark benchmark by following these steps:
-
Confirm chip supported — Check the Supported chips table and confirm the chip model is listed there.
-
Open the demo project — Dhrystone is located at
demo/vendor/Dhrystone_Demo/, CoreMark is atdemo/vendor/Coremark_demo/. For details, please refer to the sections Dhrystone demo usage and CoreMark Demo. -
Configure HAS_FLOAT macros (CoreMark only) — Set
HAS_FLOATincore_portme.hdepending on whether the chip supports floating-point. For details, please refer to the section CoreMark configuration parameters. -
Compile and Burn — The compilation option is preconfigured in the project file, so compile compile and burn directly.
-
View results — View benchmark results via USB serial port. For details, please refer to Results interpretation.
Tip
The CoreMark test runtime is about 10-20 seconds, please be patient. For multi-core chips, the N22 core requires selecting the corresponding engineering configuration.
Dhrystone
Dhrystone concept explanation
The Dhrystone standard testing method is simple: the number of times the Dhrystone program runs per unit of time, measured in DMIPS/MHz. MIPS stands for Million Instructions Per Second, meaning the number of millions of machine language instructions processed per second. The D in DMIPS stands for Dhrystone, representing MIPS under the Dhrystone standard test method, mainly used to measure integer computational power.
There is a point to note about DMIPS: for historical reasons, the test result on the VAX-11/780 machine is defined as 1757 Dhrystones/s, while the number of Dhrystones per second measured on other platforms should be divided by 1757 to obtain the true DMIPS value. Therefore, DMIPS actually represents a relative value.
Dhrystone source code address: http://www.roylongbottom.org.uk/classic_benchmarks.tar.gz
Dhrystone algorithm explanation
The Dhrystone program was proposed by Reinhold P. Weicker in 1984, mainly testing the integer computation performance of processors, and includes the following types of operations:
- String operations (assignment, comparison)
- Integer operations (arithmetic operations, comparison)
- Enumeration type operations
- Struct assignment
- Control Flow (Loop, Conditional Branch)
- Function calls (including parameterized function calls)
- Pointer operation
Dhrystone demo usage
The demo is located at demo/vendor/Dhrystone_Demo/, and Dhrystone's test logic is completed in app.c's user_init().
Operation process:
- Initialize the clock (default uses
CLOCK_INITmacro) - Call
dhry_main()to perform the Dhrystone benchmark - Test results
Dhrystone_DMIPS_Per_MHzvia USB/serial port output
Basic example (universal for most chips):
void user_init(void)
{
CLOCK_INIT;
printf("\r\n\r\n Drystone Benchmark %d Starts ...", 1);
dhry_main();
printf("\r\n[dhrystone] : %6.2f\r\n", Dhrystone_DMIPS_Per_MHz);
delay_ms(100);
}
Dhrystone results interpretation
- TL series: outputs floating-point format (e.g.,
1.98), unit: DMIPS/MHz; - TC series: outputs integer format (value magnified by 1000 times, e.g.,
1980means 1.98 DMIPS/MHz), dividing by 1000 to get the actual value; Dhrystone_DMIPS_Per_MHzis the value per MHz, which can be used for performance comparison between different processors.
CoreMark
CoreMark concept explanation
CoreMark was proposed in 2009 by Shay Gla-On of EEMBC, aiming to develop it into an industrial standard to replace the outdated Dhrystone standard.
- CoreMark official website address: https://www.eembc.org/coremark/index.php
- Github source code: https://github.com/eembc/coremark
Similar to Dhrystone, the standard CoreMark testing method measures the number of times a CoreMark program runs per unit time under a given combination of configuration parameter, expressed in CoreMark/MHz. The higher the CoreMark value, the better the performance.
CoreMark algorithm explanation
The CoreMark program is written in C and includes the following four types of algorithms:
- Mathematical matrix operations (conventional matrix operations): tests the CPU's ability to perform integer mathematical operations
- Enumeration (Search and Sort): Tests the CPU's search and sorting capabilities
- State machine (used to determine if the input stream contains significant numbers): Tests the CPU's control stream processing capability
- CRC (Cyclic Redundancy Check): Tests the CPU's verification computing power
CoreMark Demo usage
The demo is located at demo/vendor/Coremark_demo/. CoreMark's initialization and testing logic is mainly completed in main.c. The user_init() in app.c contains only auxiliary code, such as LED initialization, allowing users to compile and burn without modification. The N22 cores in multi-core chips can run CoreMark Demo independently, but the engineering configuration corresponding to the N22 core must be selected.
Operation process:
- Initialize Platform and Clock (
PLATFORM_INIT+CLOCK_INIT) - Obtain CPU clock frequency (automatically processed within SDK)
- Call
main_coremark()to run the CoreMark benchmark (runtime about 10~20 seconds) - Test results are output via USB serial port
int main(void)
{
PLATFORM_INIT;
CLOCK_INIT;
user_init(); // Only initializes peripherals like LEDs; CoreMark logic is not included in this function
printf("\r\n\r\n Core Mark Starts(wait about 10s~20s...) ...\r\n");
delay_ms(100);
cpu_mhz = sys_clk.cclk; // D25 cores are obtained directly from the struct; N22 cores are calculated through n22_get_cpu_clk().
main_coremark();
#if HAS_FLOAT
printf("coremark result = %f (%dM)\r\n", coremark_result, cpu_mhz);
printf("coremark result/clk(Mhz) = %f \r\n", (coremark_result / cpu_mhz));
#else
printf("coremark result = %d\r\n", coremark_result);
printf("coremark result/clk(Mhz) = %d\r\n", (coremark_result * 1000 / (cpu_mhz / 1000000)));
#endif
while (1) {
main_loop();
}
return 0;
}
CoreMark configuration parameters
The HAS_FLOAT macro in the core_portme.h needs to be configured according to the chip's floating-point support:
| Macro definition | TL series comes by default | TC series comes by default | Description |
|---|---|---|---|
HAS_FLOAT |
0 | 1 | Floating-point chips (-mabi=ILP32f) are set to 1, non-floating-point chips are set to 0 |
Other compilation options (-mcpu, -mabi, optimization level, S/link files, etc.) are pre-configured in each chip engineering file (.cproject), so no manual modification is needed.
CoreMark result interpretation
The output format of the result depends on the HAS_FLOAT macro configuration:
- TL series (default
HAS_FLOAT=0):coremark_resultis an int type, output in integer format (value magnified by 1000x),coremark_result/clk (MHz)is divided by 1000 to get the actual CoreMark/MHz value; - TC series (default
HAS_FLOAT=1):coremark_resultis a float type, outputs in floating-point format, and can be read directly; coremark_result/clk (MHz)is the value per MHz and can be used for performance comparison between different processors.
Common problem troubleshooting
Compilation and configuration class
**Q: Is there a compilation prompt HAS_FLOAT related error? **
A: Check whether the HAS_FLOAT macro in core_portme.h matches the chip's floating-point support: set -mabi=ilp32f to 1 in the project file, and 0 for `-mabi=ilp32```. For details, please refer to the section CoreMark configuration parameters.
Result interpretation class
**Q: Is it normal for Dhrystone to output 1980 instead of 1.98? **
A: Normal. TC series chips output integer values amplified 1000 times; dividing the output by 1000 gives the actual DMIPS/MHz value. TL series outputs floating-point format, which can be read directly. For details, please refer to Results interpretation.
**Q: Is the CoreMark benchmark score much lower than expected? **
A:
-
Confirm that the
HAS_FLOATmacro configuration is correct. -
Scores drop after enabling Instruction Prefetch on the N22 core, which is normal.
-
Confirm that the link file is correct (use
flash_boot_ramcode.link).
**Q: What are the result units for Dhrystone and CoreMark respectively? **
A: Dhrystone is measured in DMIPS/MHz, CoreMark in CoreMark/MHz. Both are values per MHz, making it convenient for processors at different frequencies to compare side by side.
Operation Exception Category
**Q: CoreMark test runs for over 1 minute with no results? **
A: CoreMark has a normal operating time of about 10 ~ 20 seconds. If a timeout occurs, check whether the serial port is properly connected, or try resetting the chip and restarting the device.
**Q: Is the N22 core much lower than the D25 core? **
A: The performance of the N22 core differs from that of the D25 core, so differences in benchmark scores are normal. For specific reference data, please refer to the corresponding chip datasheet.
Tip
If the above solutions do not resolve the issue, please confirm whether the demo project uses the latest SDK version.
Digital Keyscan
Overview
Digital Keyscan is a hardware matrix keyboard scanning module built into Telink SoC, used to detect key events of row-column matrix keyboards.
The supported chips:
- TL322x
- TC321x
- TLSR8208/TLSR8373
Comparison of Two Operating Modes
| Item | Hardware Debounce Mode | DMA Mode |
|---|---|---|
| Clock Source | 32kHz low-speed clock | 24MHz high-speed clock |
| Scan Trigger | Starts scanning when a key is pressed, stops automatically when no key is pressed | Continuous scanning, never stops |
| Debounce Method | Hardware automatic | Software manual |
| Reported Data | Valid key value (row number + column number) | 8×32bit raw level bitmap |
| Detectable Events | Press event (release inferred by scan stop) | Both press and release detectable |
| Typical Scan Period | 2ms (one full scan of 8 rows × 31 columns) | 62.5\(\mu\)s (per round) |
| Low Power Support | Supported (module stops when no key, system can enter sleep) | Not supported (module runs continuously) |
| Application Scenarios | Battery-powered, standard keyboards, power-sensitive devices | Gaming keyboards, N-key rollover, low-latency scenarios |
Selection Guide:
- If the product is power-sensitive (e.g., wireless keyboards, remote controls), choose Hardware Debounce Mode
- If you need to detect key release events or require extremely low key latency, choose DMA Mode
Matrix Specification Limits
| Parameter | Hardware Debounce Mode | DMA Mode |
|---|---|---|
| Max Rows | 8 | 8 |
| Max Columns | 18 | Number of remaining IOs excluding rows |
| Row + Column Total | ≤ 32 (limited by GPIO number range) | ≤ 32 (limited by GPIO number range) |
| Max Keys | Max 15 keys per row, up to 31 total | Unlimited, any key combination |
Note
- Row and column pins must be selected from the
ks_value_eenumeration, and all GPIO numbers (0~31) must not overlap. That is, the GPIO numbers used by rows and columns combined cannot exceed 32.
Usage Example
Hardware Debounce Mode
Step 1: Define Row and Column Pin Arrays
#define ROW_CNT 3
#define COL_CNT 3
unsigned char g_ks_row[ROW_CNT] = {KS_PC0, KS_PC1, KS_PC2};
unsigned char g_ks_col[COL_CNT] = {KS_PE0, KS_PE1, KS_PE2};
Step 2: Initialize and Start
void user_init(void)
{
// Configure matrix pins and pull-up/pull-down mode (pull-down recommended)
keyscan_set_martix(g_ks_row, ROW_CNT, g_ks_col, COL_CNT, KS_INT_PIN_PULLDOWN);
// Initialize: debounce period 8ms, enter idle after 1 idle period, 2-level debounce
keyscan_init(DEBOUNCE_PERIOD_8MS, 1, DOUBLE_SCAN_TIMES);
// Enable module and interrupt
keyscan_enable();
plic_interrupt_enable(IRQ_KEY_SCAN);
core_interrupt_enable();
}
Step 3: Interrupt Handler
_attribute_ram_code_sec_noinline_ void keyscan_irq_handler(void)
{
if (keyscan_get_irq_status()) {
keyscan_clr_irq_status();
while (1) {
unsigned char key_val = keyscan_get_ks_value();
if (key_val == KEYSCAN_END_FLAG) {
break; // 0xFF indicates end of current round data
}
unsigned char row = key_val >> 5; // Upper 3 bits = row number
unsigned char col = key_val & 0x1f; // Lower 5 bits = column number
// Handle key press event
}
}
}
PLIC_ISR_REGISTER(keyscan_irq_handler, IRQ_KEY_SCAN)
DMA Mode
Step 1: Define Row/Column Pins and DMA Buffers
#define ROW_CNT 3
#define COL_CNT 3
#define DMA_SIZE (8 * 4) // 8 words = 32 bytes
unsigned char g_ks_row[ROW_CNT] = {KS_PC0, KS_PC1, KS_PC2};
unsigned char g_ks_col[COL_CNT] = {KS_PE0, KS_PE1, KS_PE2};
unsigned int now_ks_scanning_buff[8] = {0};
unsigned int last_ks_scanning_buff[8] = {0};
unsigned int dma_ks_scanning_buff[8] = {0};
Step 2: Initialize and Start
void user_init(void)
{
// Configure matrix pins and pull-up/pull-down mode (pull-up recommended)
keyscan_set_martix(g_ks_row, ROW_CNT, g_ks_col, COL_CNT, KS_INT_PIN_PULLUP);
// Initialize: 8K scan rate, 24M crystal clock source, fixed value 1 (no effect)
keyscan_init_clk_24m(KEYSCAN_8K, KS_24MXTAL, 1);
// Enable DMA and module
keyscan_dma_enable();
keyscan_enable();
// Configure DMA linked-list circular mode
dma_set_llp_sof_mode(DMA0, 1);
keyscan_dma_config_llp(DMA0, dma_ks_scanning_buff, DMA_SIZE);
}
Step 3: Polling in Main Loop
void main_loop(void)
{
// Check DMA transfer complete interrupt
if (keyscan_get_rxdone_irq_status())
{
keyscan_clr_rxdone_irq_status();
// Copy DMA buffer data to current frame buffer
for (int i = 0; i < 8; i++)
{
now_ks_scanning_buff[i] = dma_ks_scanning_buff[i];
}
}
else
{
return;
}
// Compare current and previous frames to detect key events
for (int i = 0; i < ROW_CNT; i++)
{
if (last_ks_scanning_buff[i] != now_ks_scanning_buff[i])
{
for (int k = 0; k < COL_CNT; k++)
{
//Check key state based on current software scan order
unsigned int now_bit = now_ks_scanning_buff[i] & (1 << g_ks_col[k]);
if ((last_ks_scanning_buff[i] & (1 << g_ks_col[k])) != now_bit)
{
if (now_bit)
{
printf("row=%d ,col=%d, is press\r\n", i, k);
}
else
{
printf("row=%d ,col=%d is release \r\n", i, k);
}
}
}
last_ks_scanning_buff[i] = now_ks_scanning_buff[i];
}
}
}
GPIO Pin Configuration
Pin Assignment
The hardware assigns a fixed number (0~31) to each I/O port, which can be multiplexed as Keyscan functionality. Pin assignments differ across chips; refer to the ks_value_e enumeration in drivers/keyscan.h.
Example for TL322x:
typedef enum {
// Group 1 (GPIO numbers 0~30)
KS_PD3 = 0, KS_PD4 = 1, KS_PD5 = 2, KS_PD6 = 3,
KS_PD7 = 4, KS_PE0 = 5, KS_PE1 = 6, KS_PE2 = 7,
KS_PC3 = 8, KS_PC4 = 9, KS_PC5 = 10, KS_PC6 = 11,
KS_PC7 = 12, KS_PD0 = 13, KS_PD1 = 14, KS_PD2 = 15,
KS_PB3 = 16, KS_PB7 = 20, KS_PC0 = 21, KS_PC1 = 22,
KS_PC2 = 23, KS_PA0 = 24, KS_PA1 = 25, KS_PA2 = 26,
KS_PA3 = 27, KS_PA4 = 28, KS_PB0 = 29, KS_PB1 = 30,
// Group 2 (GPIO numbers 32~62, encoded as BIT(5) | number)
KS_PH3 = BIT(5) | 0, KS_PH4 = BIT(5) | 1, KS_PH5 = BIT(5) | 2,
KS_PH6 = BIT(5) | 3, KS_PH7 = BIT(5) | 4, KS_PG3 = BIT(5) | 8,
// ... some pins omitted
} ks_value_e;
Note
- The two pin groups (Group 1: PA~PE and Group 2: PF~PH) cannot be mixed. If Group 2 pins are used, Group 1 pins can only function as regular GPIO.
Row and Column Configuration Rules
Row:
- Fixed 8-row scanning; users can select 1~8 pins
- Unused rows are automatically marked as invalid by the driver; no user action required
Column:
- Hardware debounce mode: maximum 18 columns (limited by internal FIFO capacity)
- DMA mode: maximum 31 columns (limited by GPIO number range 0~31)
- Column scan order is fixed by GPIO number 0->31, independent of user-configured array order
- The driver automatically converts "hardware column number -> user column number"
Pull-up/Pull-down Configuration Recommendations
| Operating Mode | Recommended Configuration | Description |
|---|---|---|
| Hardware Debounce Mode | KS_INT_PIN_PULLDOWN (pull-down) |
Low-speed scanning, pull-down consumes less power |
| DMA Mode | KS_INT_PIN_PULLUP (pull-up) |
High-speed scanning, pull-up responds faster, avoids timing overlap |
Important
- Row and column pull-up/pull-down must be consistent (all pull-up or all pull-down), configured uniformly by the last parameter of
keyscan_set_matrix().
Hardware Debounce Mode Details
Workflow
flowchart LR
A[Key Pressed] --> B[Module wakes from Idle<br>starts row-by-row column-by-column scanning]
B --> C[Hardware debounce comparison<br>consecutive N identical results]
C --> D[Write to FIFO<br>valid key value + end flag 0xFF]
D --> E[Trigger interrupt<br>CPU reads key value]
E --> F{Key still pressed?}
F -- Yes --> B
F -- No --> G[No key for M consecutive periods<br>return to Idle state]
G -- New key pressed --> A
Idle State: When no key is pressed, the module stops scanning, consumes no scanning current, and the system can enter sleep.
Key Parameters
| Parameter | Available Values | Description |
|---|---|---|
| Scan Period | Fixed 2ms | With 32kHz clock, scanning 8 rows × 31 columns takes a fixed 2ms |
| Debounce Period | 4 / 8 / 12 / 16 / 20 / 24 / 28 ms | Time interval between two consecutive comparisons |
| Debounce Count | 2 times (Double) / 3 times (Triple) | Number of consecutive identical results required for validation |
| Idle Period Count | 1~N | Number of debounce periods without a key before returning to Idle |
Detection Time Estimation:
Minimum detection time ≈ Debounce Period × Debounce Count
For example: 8ms debounce period + 2-level debounce -> minimum 16ms to confirm a key.
Note
- When the debounce period is 4ms, since the scan period is 2ms, only 2 scans can be completed within one period. Therefore, 3-level debounce has the same effect as 2-level debounce at 4ms; it is recommended to use only 2-level debounce for 4ms.
- The debounce period is configurable, the scan period is fixed at 2ms, and the debounce processing time is fixed at 4ms.
Debounce Principle
The essence of debouncing is confirming a key only when consecutive scan results are consistent, avoiding false triggers caused by mechanical contact bounce.
Example with 8ms debounce period + 2-level debounce:
| Time | Event |
|---|---|
| 0ms | First detection of key press, timing starts |
| 8ms | Second scan, result matches the first -> key confirmed, interrupt triggered |
| 16ms | Third scan, key still pressed -> interrupt triggered again |
| ... | Key held down, interrupt triggered every 8ms |
| After release | No key for N consecutive periods -> return to Idle, no more interrupts |
Interrupt Trigger Rules:
- Starting from Idle, no interrupt is triggered before debounce completes
- After debounce completes, one interrupt per debounce period until the key is released
- After release, the module returns to Idle and no more interrupts are generated
Key Value Format
Each key value in the FIFO is 1 byte:
Bit[7:5] = Row number (0~7)
Bit[4:0] = Column number (0~30)
The special value 0xFF indicates the end of the current scan round data.
Low Power
Keyscan can only serve as a wakeup source for suspend mode, not for deep/deep_retention mode.
Keyscan can only enter suspend when it is in idle state, otherwise it cannot.
Keyscan only wakes up after debounce is completed, and the key result is preserved after wakeup.
//Before entering sleep, check if in idle state
if((reg_ks_rptr&FLD_KS_STATE)==0x00){
pm_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_CORE, PM_TICK_STIMER, stimer_get_tick() + 4000 * SYSTEM_TIMER_TICK_1MS);
}
DMA Mode Details
Workflow
flowchart LR
A[Continuous scanning<br>at 8K rate] --> B[Each scan round completes<br>generates rxdone signal]
B --> C[DMA transfers data<br>to memory buffer]
C --> D[Software compares frames<br>detects press/release]
D --> A
Core Differences from Hardware Debounce Mode:
- The module scans continuously, never stops, never sleeps
- Completes one scan round every 62.5\(\mu\)s, writes directly to memory via DMA
- Software determines key events by comparing differences between adjacent frames
Key Parameters
The key parameters are listed below.
| Parameter | Value | Description |
|---|---|---|
| Scan Clock | 24MHz | High-speed clock, cannot be disabled |
| Single Round Scan Time | 62.5\(\mu\)s | Scans 8 rows × 31 columns |
| Data Format | 8×32bit | 32bit per row, each bit represents one column's level |
| DMA Trigger | FIFO reaches 4 words | Approximately half a round of data triggers DMA, ensuring real-time performance |
| DMA Mode | Linked-list circular | Data cyclically overwrites to buffer start address |
Clock Divider Calculation:
With a 32kHz clock, the scan period is 2ms. The target period for DMA mode is 62.5\(\mu\)s, which is 32 times faster:
2ms / 62.5us * 32000 = 1024k
CLK_DIV = 24M / 1024K ≈ 23
That is, the 24MHz clock is divided by 23 to yield approximately 1024kHz, which serves as the Keyscan module operating clock.
Bitmap Data Format
The DMA buffer consists of 8 32-bit integers, each representing the 32-column level state of one row:
dma_ks_scanning_buff[0] = Row0 Column0~31 levels (bit0=Col0, bit1=Col1...)
dma_ks_scanning_buff[1] = Row1 Column0~31 levels
...
dma_ks_scanning_buff[7] = Row7 Column0~31 levels
Software Debounce
DMA mode does not perform hardware debouncing; software must handle mechanical bounce independently.
FAQ
Q1: Can hardware debounce mode detect key release?
Not directly. Hardware debounce mode triggers an interrupt only when a key press is confirmed. When the key is released, the module returns to Idle and interrupts stop. Users can infer key release from "interrupt stopped."
If you must detect release events, use DMA Mode.
Q2: Why can't DMA mode enter low power?
DMA mode uses a 24MHz high-speed clock and the module runs continuously; the clock cannot be disabled. For low power, you must switch back to hardware debounce mode.
Q3: Does the order of row/column arrays matter?
Row order matters: the driver maps array indices 0~7 to hardware Row0~Row7.
Column order does not matter: hardware always scans by GPIO number 0~31, and the driver internally handles column number mapping.
Q4: Can both modes be used simultaneously?
No. The two modes are mutually exclusive; only one can be enabled at a time.
Q5: Is a longer debounce period always better?
No. A longer debounce period increases key response latency. Recommendations:
- General applications: 8ms period + 2-level debounce (16ms confirmation)
- Fast response scenarios: 4ms period + 2-level debounce (8ms confirmation)
Q6: Does DMA mode polling affect CPU performance?
Each poll in main_loop() only checks a single flag bit, with minimal overhead. If concerned, you can switch to interrupt mode: enable the rxdone interrupt, set a flag in the interrupt handler, and process it in the main loop.
Audio
Overview
Audio ADC
The Audio ADC (Analog-to-Digital Converter) is the input part of the Audio CODEC, responsible for converting analog audio signals into digital audio data.
Working principle:
ADCs convert continuous analog audio signals into discrete digital signals through three steps: sampling, quantization, and encoding. The sampling process samples the analog signal at a fixed frequency, the quantization process maps the sampled value to a finite set of discrete levels, and the encoding process converts the quantized value into binary digits.
Main features:
- Input source selection: Supports AMIC (analog microphone), Line In (analog audio input cable)
- Sampling rates: Supports various sampling rates including 8 kHz, 16 kHz, 24 kHz, 32 kHz, 44.1 kHz, 48 kHz, 96 kHz, 192 kHz, 384 kHz, 768 kHz
- Data bit width: supports multiple data formats including 16-bit, 20-bit, and 24-bit
- Gain control: Supports two-level adjustment of analog gain (PGA) and digital gain
- Input modes: Supports single-ended input and differential input modes
- Channel configuration: supports mono and stereo modes
ADC interface signal:
- DATA:ADC_P_IN 和 ADC_N_IN
Signal Link:
Analog input signal -> PGA (programmable gain amplifier) -> ADC -> digital filter -> digital gain -> digital audio data
Comparison of ADC differences among chips:
| Chip series | Chip model | ADC Channelsh | Supported Input Sources | Supported Sampling Rates | Supported Bit Width |
|---|---|---|---|---|---|
| TC series | TLSR825x/TLSR8359 | 2 Channels (MONO/STEREO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit |
| TC series | TLSR827x/TLSR8355 | 2 Channels (MONO/STEREO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TC series | TC321x | 2 Channels (MONO/STEREO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TC series | TC123x | 1 Channel (MONO) | AMIC, Line In | 8 kHz, 16 kHz | 16-bit |
| TL series | TLSR921x/TLSR951x | 2 Channels (MONO/STEREO) | AMIC, Line In | 8 kHz ~ 192 kHz | 16-bit, 20-bit, 24-bit |
| TL series | TLSR922x/TLSR952x | 2 Channels (MONO/STEREO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL721x | 1 Channel (MONO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL321x | 1 Channel (MONO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL322x | 1 Channel (MONO) | AMIC, Line In | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL751x | 4 Channels(A1/A2/B1/B2) | AMIC, Line In | 16 kHz ~ 768 kHz | 16-bit, 24-bit |
Audio DAC
The Audio DAC (Digital-to-Analog Converter) is the output part of the Audio CODEC, responsible for converting digital audio data into analog audio signals that drive headphones, speakers, and other output devices.
Working principle:
DACs process digital audio data (usually in PCM format) through digital-to-analog conversion to produce smooth, continuous analog audio signals. The conversion mainly includes digital interpolation, digital-to-analog conversion, and analog filtering.
Main features:
- Output Type: Supports headphone output and audio line output
- Sampling rates: Supports various sampling rates including 8 kHz, 16 kHz, 24 kHz, 32 kHz, 44.1 kHz, 48 kHz, 96 kHz, 192 kHz, 384 kHz, 768 kHz
- Data bit width: supports 16-bit, 20-bit, and 24-bit data formats
- Gain control: Supports two-level adjustment of analog gain and digital gain
- Output modes: Supports mono and stereo modes
DAC interface signal:
- DATA: DAC_P_OUT and DAC_N_OUT
Signal Link:
Digital audio data -> filter -> digital gain -> DAC -> analog gain -> analog output signal
Comparison of DAC differences among chips:
| Chip series | Chip model | DAC Channels | Supported Sampling Rates | Supported Bit Width |
|---|---|---|---|---|
| TL series | TLSR921x/TLSR951x | 2 channels (supports MONO/STEREO) | 8 kHz ~ 192 kHz | 16-bit, 20-bit |
| TL series | TLSR922x/TLSR952x | 2 channels (supports MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL751x | 2 Channels (DAC A1/A2) | 16 kHz ~ 768 kHz | 16-bit, 24-bit |
Audio DMIC
DMIC (Digital Microphone) is a microphone that outputs digital audio signals directly. Compared with traditional analog microphones (AMICs), DMIC has clear advantages in anti-interference capability and system integration.
Working principle:
DMIC integrates MEMS sensors and PDM (Pulse Density Modulation) modulators internally. After the sound vibration is converted into an analog electrical signal by the MEMS sensor, it is directly converted into a 1-bit PDM digital signal by a PDM modulator. PDM modulation uses oversampling to modulate analog signals at a sampling rate far higher than the audio bandwidth (typically 1 MHz ~ 3 MHz), representing signal amplitude via pulse density.
Main features:
- Digital output: Directly outputs PDM digital signals without the need for an external ADC
- Strong interference immunity: Digital signal transmission is not affected by analog noise or electromagnetic interference
- High system integration: eliminates the need for external ADCs and analog front-end circuits
- Supports dual audio: can connect two DMICs on the left and right simultaneously to achieve stereo capture
- Flexible clock configuration: Supports multiple PDM clock frequency configurations
- Gain control: Supports digital gain adjustment
- Downsampling: Obtain data at the required sampling rate through downsampling
Comparison between PDM and PCM:
| Characteristics | PDM | PCM |
|---|---|---|
| Signal expression | 1-bit pulse density modulation | Digital encoding amplitude |
| Sampling rate | Oversampling (1 MHz ~ 3 MHz): obtaining data at the required sampling rate through downsampling | Standard sampling rate (8 kHz ~ 48 kHz) |
| Data volume | High (digital extraction filter) | Low (directly available) |
| Interference resistance | strong (digital signal) | Weak (analog signals are easily affected) |
| Application scenarios | Digital microphones | Traditional audio capture and storage |
DMIC interface signal:
- CLK: PDM clock signal, supplied by the master device to the DMIC device
- DATA: PDM data signal, output by the DMIC device to the master device
- LR selection: Supports selecting left and right channels via the SE pin of the DMIC
Signal processing flow:
Sound -> DMIC device -> PDM modulator -> PDM digital signal -> extract filter -> PCM audio data
Comparison of DMIC differences among chips:
| Chip series | Chip model | DMIC Channels | Supported Sampling Rates | Supported Bit Width |
|---|---|---|---|---|
| TC series | TLSR825x/TLSR8359 | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC series | TLSR827x/TLSR8355 | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC series | TC321x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TLSR921x/TLSR951x | 2 Channels (MONO/STEREO) | 8 kHz ~ 192 kHz | 16-bit, 20-bit, 24-bit |
| TL series | TLSR922x/TLSR952x | 4 Channels (Codec0: 2 channels; Codec1: 2 channels) | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL721x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL321x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL322x | 2 channels (supports MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit, 20-bit |
| TL series | TL751x | 6 channels (Codec0: DMIC A1/A2/B1/B2; Codec1:DMIC A1/A2) | 16 kHz ~ 768 kHz | 16-bit, 24-bit |
Audio I2S
I2S (Inter-IC Sound) is a serial bus interface standard for connecting digital audio devices, developed by Philips, and is widely used to transmit audio data between devices such as audio codecs, DSPs, and digital audio processors.
-
Interface signal:
I2S interfaces typically include the following signal lines:
- SCK (Serial Clock): Serial clock signal, provided by the master device, used for synchronized data transmission
- WS (Word Select): Word selection signal, also known as LRCK (Left/Right Clock), used to distinguish between left and right channel data
- SD (Serial Data): Serial data signal, used to transmit actual audio data
- MCLK (Master Clock): The main clock signal; some devices require an additional system clock as a reference
-
Operating Mode:
I2S supports multiple data formats and operating modes:
| Modes | Features | Application scenarios |
|---|---|---|
| I2S mode | The WS signal toggles one SCK clock cycle before data transmission begins. Data is shifted out on the falling edge of SCK and sampled on the rising edge. | Standard audio device interconnection |
| Left Justified (LJ) | WS starts with data, aligning data to the left | DSP, digital audio processor |
| Right Justified (RJ) | WS starts simultaneously with data, aligning data to the right | Some ADC/DAC devices |
| DSP mode | WS acts as a frame synchronization signal, transmitting data continuously | DSP, Bluetooth audio |
| TDM mode | Time-division multiplexing with support for multi-channel data transmission. | Multi-microphone array, professional audio |
-
I/O configuration:
I2S supports flexible I/O configurations:
- I2S_5_LINE_MODE: Standard 5-wire I/O configuration, including BIT_CLK, ADC_LR_CLK (RX), RX_DATA, DAC_LR_CLK (TX), and TX_DATA
- I2S_4_LINE_DAC_MODE: 4-wire mode, including BIT_CLK, RX_DATA, DAC_LR_CLK, and TX_DATA, with RX and TX word selection signals DAC_LR_CLK
- I2S_4_LINE_ADC_MODE: 4-wire mode, including BIT_CLK, ADC_LR_CLK, RX_DATA, and TX_DATA, with RX and TX word selection signals ADC_LR_CLK
- I2S_2_LANE_TX_MODE: 5-wire I/O configuration, but both RX_DATA and TX_DATA are used as TX_DATA, with two data lines outputting simultaneously
- I2S_2_LANE_RX_MODE: 5-line I/O configuration, but both RX_DATA and TX_DATA are used as RX_DATA, with two data lines input simultaneously
-
TDM (Time Division Multiplexing) mode:
TDM mode extends the standard I2S functionality, supporting the transmission of multiple audio channels within a single frame:
- TDM_MODE_A: WS is a single-period pulse, with data sampled on the SCK rising edge
- TDM_MODE_B: WS is a 50% duty cycle square wave, with data sampled on the SCK rising edge
- TDM_MODE_C: WS is a single-period pulse, with data sampled on the SCK falling edge
- Supported channels: 2/4/6/8 channels
- Slot width: supports 16-bit, 24-bit, and 32-bit
-
Key parameters:
- Supported sampling rate: 8 kHz ~ 192 kHz
- Supported data bit widths: 16-bit, 20-bit, 24-bit, 32-bit
- Supports Master mode and Slave mode
- Supports mono and stereo
Comparison of I2S differences among chips:
| Chip series | Chip model | Number of I2S channels | Data bit width | Supported Sampling Rates | TDM supported | Working mode |
|---|---|---|---|---|---|---|
| TC series | TLSR825x/TLSR8359 | 1 channel | 16-bit | 8 kHz ~ 192 kHz | No | I2S |
| TC series | TLSR827x/TLSR8355 | 1 channel | 16/20-bit | 8 kHz ~ 192 kHz | No | I2S |
| TC series | TC321x | 1 channel | 16/20-bit | 8 kHz ~ 192 kHz | No | I2S |
| TL series | TLSR921x/TLSR951x | 1 channel | 16/20/24-bit | 8 kHz ~ 192 kHz | No | I2S/RJ/LJ/DSP |
| TL series | TLSR922x/TLSR952x | 2 channels(I2S0/I2S1) | 16/20/24-bit | 8 kHz ~ 192 kHz | No | I2S/RJ/LJ/DSP |
| TL series | TL721x | 3 channels | 16/20/24-bit | 8 kHz ~ 192 kHz | Yes (I2S2) | I2S/RJ/LJ/DSP/TDM |
| TL series | TL321x | 1 channel | 16/20/24-bit | 8 kHz ~ 192 kHz | Yes | I2S/RJ/LJ/DSP/TDM |
| TL series | TL322x | 1 channel | 16/20/24/32-bit | 8 kHz ~ 192 kHz | Yes | I2S/RJ/LJ/DSP/TDM |
| TL series | TL751x | 3 channels (I2S0/I2S1/I2S2) | 16/20/24-bit | 8 kHz ~ 192 kHz | Yes (I2S0) | I2S/RJ/LJ/DSP/TDM |
Audio SDM
SDM (Sigma-Delta Modulator) is a modulation technology used for digital audio output. It converts high-resolution digital audio signals into low-resolution pulse density modulation signals through oversampling and noise shaping, and then directly drives a Class-D audio amplifier or an external low-pass filter to restore the analog audio signal.
-
Main features:
- Direct digital output: No need for traditional DACs, directly outputs the modulated digital pulse signals
- Low power consumption: Compared to traditional DAC + analog amplifier solutions, overall power consumption is lower
- Channel configuration: supports mono and stereo output modes
-
Output pin configuration:
SDM output typically uses a differential signal pair (P/N) form:
- SDM_P: Positive output signal
- SDM_N: Negative output signal (differential mode)
-
Key parameters:
- Supported sampling rate: 8 kHz ~ 48 kHz
- Supported data bit width: 16-bit
-
Signal processing flow:
Digital audio data -> interpolation filter -> Sigma-Delta modulator -> PDM output -> filter -> analog audio signals
Comparison of SDM differences among chips:
| Chip series | Chip model | Number of SDM channels | Supported Sampling Rates | Data bit width |
|---|---|---|---|---|
| TC series | TLSR825x/TLSR8359 | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC series | TLSR827x/TLSR8355 | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC series | TC321x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TL series | TL721x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TL series | TL321x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TL series | TL322x | 2 Channels (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
Audio driver and demo file structure
Driver file structure
- TC series
| Chips | Demo location | Driver file location | Supported Audio modules |
|---|---|---|---|
| TLSR825x/TLSR8359 | tc_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.0 | tc_platform_src/chip/B85/drivers/audio.c tc_platform_src/chip/B85/drivers/audio.h |
ADC, SDM, I2S |
| TLSR827x/TLSR8355 | tc_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.0 | tc_platform_src/chip/B87/drivers/audio.c tc_platform_src/chip/B87/drivers/audio.h |
ADC, SDM, I2S |
| TC321x | tc_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.1 | tc_platform_src/chip/TC321x/drivers/audio.c tc_platform_src/chip/TC321x/drivers/audio.h |
ADC, SDM, I2S |
| TC123x | tc_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.2 | tc_platform_src/chip/tc123x/drivers/audio.c tc_platform_src/chip/tc123x/drivers/audio.h |
ADC |
- TL series
| Chips | Demo location | Driver file location | Supported Audio modules |
|---|---|---|---|
| TLSR921x/TLSR951x | tl_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.0 | tl_platform_src/chip/B91/drivers/audio.c tl_platform_src/chip/B91/drivers/audio.h |
ADC, DAC, I2S, DMIC |
| TLSR922x/TLSR952x | tl_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.1 | tl_platform_src/chip/B92/drivers/audio.c tl_platform_src/chip/B92/drivers/audio.h |
ADC, SDM, I2S |
| TL751x | tl_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.2 | tl_platform_src/chip/tl751x/drivers/audio.c tl_platform_src/chip/tl751x/drivers/audio.h |
ADC, DAC, I2S, ANC, Sidetone, ASRC, EQ, HAC, WT |
| TL321x | tl_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.3 | tl_platform_src/chip/TL321x/drivers/audio.c tl_platform_src/chip/TL321x/drivers/audio.h |
ADC, SDM, I2S |
| TL322x | tl_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.3 | tl_platform_src/chip/tl322x/drivers/audio.c tl_platform_src/chip/tl322x/drivers/audio.h |
ADC, SDM, I2S |
| TL721x | tl_platform_src/demo/vendor/AUDIO_Demo/AUDIO_V1.3 | tl_platform_src/chip/TL721x/drivers/audio.c tl_platform_src/chip/TL721x/drivers/audio.h |
ADC, SDM, I2S |
Driver layer files
The Audio driver layer is located within each chip directory and provides hardware register operations and low-level functional interfaces:
| Files | Description |
|---|---|
audio.c |
The main audio driver file includes low-level implementations such as initialization, clock control, channel settings, DMA, and FIFO configuration for various Audio modules |
audio.h |
Audio driver header file, defining enumeration types, structures, function declarations, etc. |
register.h (TC series) / audio_reg.h (TL series) |
The TC series Audio register address is placed in the common register.h header file; The TL series Audio register address is placed in a separate audio_reg.h file |
Main functional modules of the driver layer:
- Initialization and clock configuration: Audio PLL configuration, clock divider configuration, Audio module configuration and enablement
- Channel configuration: input and output channel settings for modules such as ADC/DAC/DMIC/I2S/SDM
- Gain control: Configuration interface for analog gain (PGA) and digital gain
- FIFO Management: Read/write control of the audio data buffer
- DMA interface: Works with DMA controllers to enable batch transmission of audio data
- Interrupt Management: Configuration and handling of FIFO interrupts
Demo file structures
The file structure of the application layer for each version of Audio Demo is as follows:
- TC series
AUDIO_V1.0(TLSR825x/TLSR8359/TLSR827x/TLSR8355)
AUDIO_Demo/
├── app.c # Application file, containing examples of audio module usage
├── app_config.h # Configuration file, used to select which example to compile
├── audio_data.h # Contains audio data at different sampling rates
└── main.c # Program entry, including platform initialization and clock initialization
AUDIO_V1.1(TC321x)
AUDIO_Demo/
├── app_codec.c # Includes examples of using ADC, SDM, and DMIC
├── app_config.h # Configuration file, used to select which example to compile
├── app_i2s.c # I2S usage examples (I2S initialization, reception, and sending)
├── app_mix.c # Example of mixed use of ADC, DMIC, and I2S
├── audio_common.c # Includes audio data introductory interface
├── audio_common.h # Contains audio data at different sampling rates
└── main.c # Program entry, including platform initialization and clock initialization
AUDIO_V1.2(TC123x)
AUDIO_Demo/
├── app.c # Includes examples of using ADCs
├── app_config.h # Configuration file, used to select which example to compile
└── main.c # Program entry, including platform initialization and clock initialization
- TL series
AUDIO_V1.0(TLSR921x/TLSR951x)
AUDIO_Demo/
├── app.c # Includes examples of using ADC, DMIC, DAC, and I2S
├── app_sin_data.h # Contains audio data at different sampling rates
└── main.c # Program entry, including platform initialization and clock initialization
AUDIO_V1.1(TLSR922x/TLSR952x)
AUDIO_Demo/
├── codec_0581/ # ADI 0581 codec supported
│ ├── codec_0581_eq.c # 0581 EQ coefficient
│ ├── codec_0581_fdsp.c # 0581 FDSP coefficient
│ └── codec_0581_registers.c # 0581 Register configuration
├── ext_codec_wm/ # External Codec support
│ ├── ext_codec_wm.c
│ └── ext_codec_wm.h
├── app_codec.c # Includes examples of using ADC, DMIC, DAC, and I2S
├── app_codec_0581.c # 0581 Codec Application Example
├── app_i2s.c # I2S usage examples (I2S initialization, reception, and sending)
├── app_sin_data.h # Contains audio data at different sampling rates
└── main.c # Program entry, including platform initialization and clock initialization
AUDIO_V1.2(TL751x)
AUDIO_Demo/
├── Example of using the app_anc.c # ANC module
├── Example of using the app_asrc.c # ASRC module
├── app_codec.c # Includes examples of using ADC, DAC, and DMIC
├── Example of using the app_eq.c # EQ module
├── app_filter_data.h # ANC filter coefficient data, output data of ANC and EQ modules under specific coefficients and input data
├── app_i2s.c # I2S usage examples (I2S initialization, reception, and sending)
├── app_mix.c # Example of mixing different audio modules
├── Example of using the app_sidetone.c # Sidetone module
├── app_sin_data.h # Contains audio data at different sampling rates
└── main.c # Program entry, including platform initialization and clock initialization
AUDIO_V1.3(TL321x/TL322x/TL721x)
AUDIO_Demo/
├── ext_codec_wm/ # External Codec support
│ ├── ext_codec_wm.c # WM codec driver implementation
│ └── ext_codec_wm.h # WM codec header file
├── app_codec.c # Includes examples of using ADC, SDM, and DMIC
├── app_i2s.c # I2S interface application layer
├── app_mix.c # Example of mixing different audio modules
├── audio_common.c # Includes audio data introductory interface
├── audio_common.h # Audio contains audio data at different sampling rates
└── main.c # Program entry, including platform initialization and clock initialization
Audio module initialization and configuration
Audio initialization

Audio initialization includes powering on the Audio module and initializing the Audio clock. The Audio clock comes from the system clock or PLL crossover. For example, using the PLL clock (see diagram above), the Audio clock is configured through audio_set_audio_clk interface. The audio clock of each chip can refer to the corresponding chip's datasheet. The following code uses the TL721x as an example; initialization of the remaining chips is shown in the corresponding Audio Demo.
/*
* @brief This function serves to initial audio.
* @return none.
*/
void audio_init(void)
{
audio_power_on();
audio_set_audio_clk(1, sys_clk.pll_clk / 24);
}
//Example: Audio initialization
audio_init();
Audio ADC usage and basic configuration
Audio ADC configuration includes ADC MIC BIAS pin settings, data bit width, sampling rate, FIFO and DMA channel selection, and gain control. The configuration sequence of the Audio ADC is: Audio module initialization - > clock initialization - > ADC parameters and FIFO configuration - > DMA configuration and enablement, with gain adjustable at any time. After configuring DFIFO on the TC series, the hardware will loop data into software buffs without DMA configuration.
-
Audio ADC MIC BIAS Pin Settings
The diagram below shows the hardware schematic of the AMIC. To ensure the AMIC operates properly, it is necessary to supply the MIC BIAS voltage. Depending on the chip, some chips provide voltage via external GPIO, so a dedicated GPIO output high level is required; Some chips use CODEC to provide MIC BIAS. The hardware has already connected the AMIC's VDD to the corresponding pin of the CODEC, so no external GPIO is needed. As long as the input source is selected as AMIC, the driver will turn on the MIC BIAS voltage of the CODEC.

Example code for using external GPIO to provide MIC BIAS voltage is as follows:
/** * @brief Configure AMIC bias pins * @param[in] amic_bias - Biased pin (e.g., GPIO_PA0) */ void audio_set_amic_bias_pin(gpio_pin_e amic_bias); //Example: Enable AMIC bias audio_set_amic_bias_pin(GPIO_PA0);
Note
If the AMIC has no sound or abnormal sound, first check whether the MIC BIAS voltage is correct.
-
Audio ADC parameter configuration
ADC parameter configuration mainly includes selection of sampling rate, data bit width, FIFO channel, and DMA channel. Different chips have different configuration methods. The early chips TLSR825x/TLSR8359 and TLSR827x/TLSR8355 series configurations are as follows:
// TLSR825x/TLSR8359 and TLSR827x/TLSR8355 Example: Configure ADC FIFO and AMIC sampling rates audio_config_mic_buf((unsigned short*)MicBuf,MIC_BUFFER_SIZE); audio_amic_init(AUDIO_RATE_VAL);Except for early chips (TLSR825x/TLSR8359 and TLSR827x/TLSR8355) which specified the sampling rate at the initialization interface, other chips use structural packaging configuration parameters. For example, TL721x integrates these parameters into the structural
audio_codec_stream0_input_t, then usesaudio_codec0_input_init( input_config)Complete initialization. Example code is as follows. The parameters and initialization functions of configuration structures vary by chip. For details on each chip's configuration structure, please refer to the corresponding Audio Demo.audio_codec_stream0_input_t audio_codec_stream0_input = { .input_src = INPUT_SRC, // choose AMIC .sample_rate = SAMPLE_RATE, .data_width = DATA_WIDTH, .fifo_chn = RX_FIFO_NUM, .dma_num = RX_DMA_CHN, .data_buf = AUDIO_BUFF, .data_buf_size = sizeof(AUDIO_BUFF), }; /** * @brief This function serves to set codec initialization. * @param[in] audio_codec - audio_codec_stream0_input_t pointer. * @return none. */ void audio_codec_stream0_input_init(audio_codec_stream0_input_t *audio_codec) // Example: Initialize the audio ADC and configure ADC parameters audio_codec_stream0_input_init(&audio_codec_stream0_input); // Configure chain-linked DMA audio_rx_dma_chain_init(audio_codec_stream0_input.fifo_chn, audio_codec_stream0_input.dma_num, (unsigned short *)audio_codec_stream0_input.data_buf, audio_codec_stream0_ input.data_buf_size); // Enable Codec pathways and DMA audio_codec_stream0_input_en(audio_codec_stream0_input.dma_num); audio_codec_input_path_en(audio_codec_stream0_input.fifo_chn); -
Audio ADC gain configuration
Audio ADC gain configurations are divided into analog gain and digital gain. Analog gain is used to adjust the amplification of the PGA (Programmable Gain Amplifier), while digital gain compensates for the sampled data in the digital domain. The gain adjustment interface functions, range, and step vary by chip. For specific implementations, refer to the driver code. The following gain control interface is taken as an example of the TL721x.
- Analog Gain (PGA):
- Interface:
audio_set_adc_pga_gain (pga_gain). - Enumerate:
codec_in_pga_gain_e - Range: 0 dB ~ 45 dB (0 dB, 9 dB, 12 dB, 15 dB... 45 dB)
- Interface:
- Digital gain:
- Interface:
audio_set_stream0_dig_gain (d_gain). - Enumerate:
codec_in_path_digital_gain_e - Range: -48 dB ~ +42 dB, stepping 6 dB
- Interface:
- Analog Gain (PGA):
Audio DAC usage and basic configuration
Audio DAC configuration includes data bit width, sampling rate, FIFO and DMA channel selection, and gain control.
-
Audio DAC configuration parameters
DAC parameter configuration mainly includes sampling rate, data bit width, selection of FIFO channels, and DMA channels. Currently, all chips supporting DAC functionality are TL series chips, while TC series chips do not have DAC modules. In TLSR921x/TLSR951x, parameter initialization and configuration of the Audio DAC are performed via the
audio_initinterface. The configuration sequence of the Audio DAC is: Audio module initialization - > clock initialization - > DAC parameters and FIFO configuration - > DMA configuration and enablement, with gain adjustable at any time.// Example: Initialize the audio DAC channel and configure DAC parameters audio_init(BUF_TO_LINE_OUT, AUDIO_16K, MONO_BIT_16);Subsequent chips configure the packaging parameters of the structure. For example, in TLSR922x/TLSR952x, the parameters of the Audio DAC are described through
the audio_codec_output_tstructure and unified through theaudio_codec_stream_output_initinterface.audio_codec_output_t audio_codec_output = { .output_src = CODEC_DAC_STEREO, .sample_rate = AUDIO_48K, .fifo_num = FIFO0, .data_width = CODEC_BIT_16_DATA, .dma_num = TX_DMA_CHN, .mode = HP_MODE, .data_buf = sin_48k_stereo, .data_buf_size = sizeof(sin_48k_stereo), }; // Example: Initialize the audio DAC and configure DAC parameters audio_codec_stream_output_init(&audio_codec_output); -
Audio DAC gain configuration
DAC gain configurations are divided into two parts: analog gain and digital gain. The gain configuration interface, configuration range, and stepper vary by chip. Below, the use of the TLSR922x/TLSR952x is used as examples to describe the use of the gain Audio DAC control interface.
- Analog Gain (PGA):
- Interfaces:
audio_set_dac_pga_l_gain (pga_gain)(left channel) /audio_set_dac_pga_r_gain (pga_gain)(right channel) - Enumerate:
codec_out_pga_gain_e - Range: -60.2 dB ~ +6 dB (nonlinear levels: -60.2 dB, -54.2 dB, -48.2 dB...-6 dB, -3 dB, 0 dB, 3 dB, 6 dB)
- Stepping: Nonlinear, about 3 dB/6 dB steps.
- Interfaces:
- Digital gain:
- Interfaces:
audio_set_dac_l_gain (d_gain)(left channel) /audio_set_dac_r_gain (d_gain)(right channel) - Enumerate:
codec_out_path_digital_gain_e - Range: -72.2 dB ~ 0 dB (nonlinear levels: -72.2 dB, -66.2 dB, -60.2 dB...-6 dB, -3 dB, -1 dB, 0 dB)
- Step: Nonlinear
- Interfaces:
- Analog Gain (PGA):
Audio DMIC usage and basic configuration
Audio DMIC configuration includes DMIC pin configuration, data bit width, sampling rate, FIFO and DMA channel selection, and gain control. The configuration sequence of the CODEC DAC is: Audio module initialization -> clock initialization -> DMIC parameters and FIFO configuration -> DMA configuration and enablement, with gain adjustable at any time. After configuring DFIFO on the TC series, the hardware will loop data into software buffs without DMA configuration.
-
Audio DMIC pin settings

The DMIC power supply is provided by hardware; the DMIC clock drives the digital microphone's data output; the SE pin of the DMIC is grounded or connected to VDD to distinguish left and right channels; and the DMICDAT is the data output pin of the DMIC device. SE connected to GND (low level): Microphone is left channel L, data outputs effective PDM data on the rising edge of CLK; SE connected to VDD (High Level): The microphone is the right channel R, and the data outputs effective PDM data on the lower edge of the CLK. Taking the TL721x as an example, the DMIC pin configuration interface is
audio_set_stream0_dmic_pin.// DMIC pin configuration example audio_set_stream0_dmic_pin(GPIO_FC_PA2, GPIO_FC_PA3, GPIO_FC_PA4); -
Audio DMIC Parameter Configuration
DMIC parameter configuration mainly includes sampling rate, bit width, FIFO channel selection, and DMA channel configuration. The DMIC parameter configuration methods vary depending on the chip. For details, see the DMIC usage demo for the corresponding chip. The following uses the TL721x chip as an example.
audio_codec_stream0_input_t audio_codec_stream0_input = { .input_src = INPUT_SRC, // Select DMIC .sample_rate = SAMPLE_RATE, .data_width = DATA_WIDTH, .fifo_chn = RX_FIFO_NUM, .dma_num = RX_DMA_CHN, .data_buf = AUDIO_BUFF, .data_buf_size = sizeof(AUDIO_BUFF), }; // Example of DMIC channel initialization audio_codec_stream0_input_init(&audio_codec_stream0_input); -
Audio DMIC gain configuration
Since the DMIC is a digital microphone, its gain configuration only includes the digital gain section. The DMIC digital gain control interface varies among different chips. For details, see the driver code for the respective chip. Below, we use the TL721x chip as an example.
- Digital gain:
- Interface:
audio_set_stream0_dig_gain (d_gain). - Enumerate:
codec_in_path_digital_gain_e - Range: -48dB ~ +42B, stepping 6dB
- Interface:
- Digital gain:
Audio I2S usage and basic configuration
The I2S configuration sequence is: Audio module initialization - > clock initialization - > I2S parameters and FIFO configuration - > DMA configuration and enable. After configuring DFIFO on the TC series, the hardware loops data into the software buff without DMA configuration.
-
Audio I2S pin configuration
The I2S pin configuration includes bit clock lines, word selection lines, and TX and RX data lines. Taking the TL721x as an example, the I2S pin configuration is packaged into the
structure i2s_pin_config_tand unified throughi2s_set_pininterfaces. For the other chip configurations, refer to the corresponding chip's Audio Demo.i2s_pin_config_t i2s_pin_config = { .bclk_pin = GPIO_FC_PD5, // clock bits .adc_lr_clk_pin = GPIO_FC_PB3,//For RX characters, select PIN .dac_lr_clk_pin = GPIO_FC_PB4,//For TX, select PIN .adc_dat_pin = GPIO_FC_PB5, // RX DATA PIN .dac_dat_pin = GPIO_FC_PB6, // TX DATA PIN }; /** * @brief This function configures i2s pin. * @param[in] i2s_select - channel selection * @param[in] config - i2s config pin struct. * @return none. */ void i2s_set_pin(audio_i2s_select_e i2s_select, i2s_pin_config_t *config) -
Audio I2S crossover factor configuration
As shown in the clock diagram of the Audio module earlier, the I2S clock comes from the PLL division. The I2S frequency division system is managed uniformly by i2s_clk_config arrays. When PLL is 2400000000 (240M), and i2s_clk_config[5] = {4,1875,0,32,32}, the calculation formulas for each i2s clock are as follows:
I2S_CLK = 240000000(i2s_clk_config[0]/i2s_clk_config[1]) = 240000000(4/1875) = 512000
Note
i2s_clk_config[1] must be greater than or equal to twice the i2s_clk_config[0].
When i2s_clk_config[2] is 0, I2S_BCLK = I2S_CLK = 512000
When i2s_clk_config[2] is not 0, I2S_BCLK = I2S_CLK / (2 *[i2s_clk_config 2])
I2S_ADC_LR_CLK = I2S_BCLK / (i2s_clk_config[3]) = 512000 / 32 = 16000
I2S_DAC_LR_CLK = I2S_BCLK / (i2s_clk_config[4]) = 512000 / 32 = 16000
-
Audio I2S parameter configuration
I2S parameter configuration mainly includes sampling rate, bit width, FIFO channel selection, and DMA channel configuration. The I2S configuration methods vary by chip. For details, see the corresponding chip's I2S usage demo. The following example uses the TL721x chip.
audio_i2s_config_t audio_i2s_config = { .i2s_select = I2S2, .i2s_mode = I2S_I2S_MODE, .pin_config = &i2s_pin_config, .data_width = I2S_BIT_24_DATA, .master_slave_mode = I2S_AS_MASTER_EN, .sample_rate = (unsigned short *)&audio_i2s_48k_config[0], }; //I2S input parameter configuration audio_i2s_input_output_t audio_i2s_input = { .i2s_select = audio_i2s_config.i2s_select, .data_width = audio_i2s_config.data_width, .i2s_ch_sel = I2S_CHANNEL_STEREO, .fifo_chn = FIFO0, .dma_num = DMA0, .data_buf = AUDIO_BUFF, .data_buf_size = sizeof(AUDIO_BUFF), }; //I2S output parameter configuration audio_i2s_input_output_t audio_i2s_output = { .i2s_select = audio_i2s_config.i2s_select, .data_width = audio_i2s_config.data_width, .i2s_ch_sel = I2S_CHANNEL_STEREO, .fifo_chn = FIFO0, .dma_num = DMA0, .data_buf = AUDIO_BUFF, .data_buf_size = sizeof(AUDIO_BUFF), }; //Example of I2S input initialization configuration, this interface configures pins and clock crossover coefficients for I2S audio_i2s_config_init(&audio_i2s_config); //Example of I2S input initialization configuration audio_i2s_input_init(&audio_i2s_input); //Example of I2S output initialization configuration audio_i2s_output_init(&audio_i2s_output); //I2S clock enabled audio_i2s_clk_en(audio_i2s_config.i2s_select);
Audio SDM usage and basic configuration
-
Audio SDM pin configuration
Each SDM output needs to be configured with P and N pins, corresponding to the channel's positive and negative terminals. For detailed configuration methods for different chip series, see the corresponding Audio demo. Taking the TL721x as an example, the configuration of the SDM P/N pins is encapsulated in the
sdm_pin_config_tstructure and configured via theaudio_set_sdm_pininterface.
TL721x example:
typedef struct
{
gpio_func_pin_e sdm0_p_pin; // SDM0 positive pin
gpio_func_pin_e sdm0_n_pin; // SDM0 negative pin
gpio_func_pin_e sdm1_p_pin; // SDM1 positive pin
gpio_func_pin_e sdm1_n_pin; // SDM1 negative pin
} sdm_pin_config_t;
sdm_pin_config_t sdm_pin_config = {
.sdm0_p_pin = GPIO_FC_PA0,
.sdm0_n_pin = GPIO_FC_PA1,
.sdm1_p_pin = GPIO_FC_PF4,
.sdm1_n_pin = GPIO_FC_PF5,
};
audio_set_sdm_pin(&sdm_pin_config);
Note
The SDM0_P in the TL721x Demo uses the PA0 pin, which conflicts with the default DEBUG print pin. If using the SDM function, you need to change the 'DEBUG_INFO_TX_PIN' in 'printf.h' to another pin. Distribute the SDM pins on both sides of the EVB board to prevent signal interference and noise.
-
Audio SDM parameter configuration
SDM output parameters are configured through the
audio_codec_output_tstructure, including key parameters such as output channels, sampling rate, data bit width, and DMA channels. Taking the TL721x as an example, the rest of the chip refers to the corresponding driver code and Audio Demo.
TL721x example:
audio_codec_output_t structure definition:
typedef struct
{
void *data_buf; // Outputs a pointer to the data buffer
unsigned int data_buf_size; // Output data buffer size
audio_stream_output_src_e output_src; // Output source selection
unsigned char sample_rate; // Sampling rate
unsigned char data_width; // Data bit width
dma_chn_e dma_num; // DMA channel number
} audio_codec_output_t;
// Configuration example
#define SAMPLE_RATE AUDIO_48K
#define DATA_WIDTH CODEC_BIT_16_DATA
#define TX_FIFO_NUM FIFO0 // SDM fixedly uses FIFO0
#define TX_DMA_CHN DMA1
audio_codec_output_t audio_stream_output = {
.output_src = SDM_MONO, // or SDM_STEREO
.sample_rate = SAMPLE_RATE,
.data_width = DATA_WIDTH,
.dma_num = TX_DMA_CHN,
.data_buf = audio_tx_buff, // Output data buffer
.data_buf_size = sizeof(audio_tx_buff),
};
// SDM output initialization
audio_codec_stream_output_init(&audio_stream_output);
!!! note Important Note
The SDM module **only supports 16-bit data bit width**; `CODEC_BIT_16_DATA` must be used during configuration. Using other bit widths may cause output abnormalities. SDM output is fixed to **TX FIFO0**.
-
Audio SDM gain configuration
The Audio SDM module configures output gain through
audio_set_ascl_gain. For TL721x as an example, the rest of the chip refers to the corresponding Audio Demo and driver files.
TL721x example:
- Digital gain:
- Interface:
audio_set_ascl_gain (d_gain). - Enumerate:
ascl_out_path_digital_gain_e - Range: -48dB ~ +42B, stepping 6dB
- Interface:
Introduction to Audio's DMA chain
The audio module's data transmission relies heavily on DMA (Direct Memory Access) to efficiently transfer audio data between memory and the FIFO, reducing CPU overhead. Audio DMA uses a chained transmission mechanism to achieve data-loop buffering and ensure the continuity of audio streams.
The positional relationship between DMA and FIFO is shown in the figure below:

Important Notes
Once DFIFO is configured, the TC series chips automatically cycle audio data to the defined buffers, whereas TL series chips require configuring chain transmission via the DMA interface.
DMA chain transmission principle
DMA-linked transfer links multiple DMA transmission nodes via a linked list structure. After the current node completes transmission, it automatically jumps to the next node, forming a loop.
This diagram shows DMA dual-node chain transmission, while Audio Demos typically use single-node chain transmission.
+-------------+ +-------------+ +-------------+
| DMA Node 0 | --> | DMA Node 1 | --> | DMA Node 0 | (Loop)
+-------------+ +-------------+ +-------------+
| | |
Buff[0] Buff[1] Buff[0]
Advantages of chain transmission:
- Continuous transmission: Enables uninterrupted audio data streaming without CPU intervention
- Dual buffering mechanism: Using dual-node linked list loops to achieve ping-pong buffering and avoid data overwrite
- Low-latency: DMA hardware automatically handles data transfer, providing fast response times
Audio DMA configuration structure
The Audio driver predefines DMA configurations for RX (receive) and TX (transmit):
RX DMA configuration (audio_dma_rx_config):
| Field | Configuration values | Description |
|---|---|---|
| dst_req_sel | 0 | Since it is the RX request source, this value is 0 |
| src_req_sel | DMA_REQ_AUDIO_RX | Audio RX FIFO request source |
| dst_addr_ctrl | DMA_ADDR_INCREMENT | Destination Address Increment (Memory Buffer) |
| src_addr_ctrl | DMA_ADDR_FIX | Source Address Fixed (FIFO Register) |
| dstmode | DMA_NORMAL_MODE | Normal mode |
| srcmode | DMA_HANDSHAKE_MODE | Source handshake mode (waiting for FIFO request) |
| dstwidth / srcwidth | DMA_CTR_WORD_WIDTH | Transfer width is Word (32bit) |
| src_burst_size | 0 | It must be 0 |
TX DMA Configuration (audio_dma_tx_config):
| Field | Configuration values | Description |
|---|---|---|
| dst_req_sel | DMA_REQ_AUDIO_TX | Audio TX FIFO request source |
| src_req_sel | 0 | Since it is the TX request source, this value is 0 |
| dst_addr_ctrl | DMA_ADDR_FIX | Fixed Target Address (FIFO Register) |
| src_addr_ctrl | DMA_ADDR_INCREMENT | Source address increases (memory buffer) |
| dstmode | DMA_HANDSHAKE_MODE | Target handshake mode |
| srcmode | DMA_NORMAL_MODE | Normal mode |
| dstwidth / srcwidth | DMA_CTR_WORD_WIDTH | Transfer width is Word (32bit) |
| src_burst_size | 0 | It must be 0 |
Audio DMA linked transmission interface
- Audio RX DMA chain initialization
/**
* @brief Initialize Audio RX DMA chain transfer
* @param[in] rx_fifo_chn - RX FIFO channel selection
* @param[in] chn - DMA channel number
* @param[in] in_buff - Receive data buffer pointer (must be 4-byte aligned)
* @param[in] buff_size - buffer size (bytes, must be multiples of 4, maximum 0x10000)
*/
void audio_rx_dma_chain_init(audio_fifo_chn_e rx_fifo_chn, dma_chn_e chn,
unsigned short *in_buff, unsigned int buff_size);
- Audio TX DMA chain initialization
/**
* @brief Initialize Audio TX DMA chain transfer
* @param[in] tx_fifo_chn - TX FIFO channel selection
* @param[in] chn - DMA channel number
* @param[in] out_buff - Send data buffer pointer (must be 4-byte alignment)
* @param[in] buff_size - buffer size (bytes, must be multiples of 4, maximum 0x10000)
*/
void audio_tx_dma_chain_init(audio_fifo_chn_e tx_fifo_chn, dma_chn_e chn,
unsigned short *out_buff, unsigned int buff_size);
- Audio DMA enables and disables
// Enable RX DMA
static inline void audio_rx_dma_en(dma_chn_e chn);
// Disable RX DMA
static inline void audio_rx_dma_dis(dma_chn_e chn);
// Enable TX DMA
static inline void audio_tx_dma_en(dma_chn_e chn);
// Disable TX DMA
static inline void audio_tx_dma_dis(dma_chn_e chn);
Audio DMA chain underlying implementation
RX DMA chain initialization process:
void audio_rx_dma_chain_init(audio_fifo_chn_e rx_fifo_chn, dma_chn_e chn,
unsigned short *in_buff, unsigned int buff_size)
{
audio_rx_fifo_chn = rx_fifo_chn;
// 1. Configure DMA channel parameters
audio_rx_dma_config(chn, in_buff, buff_size, &g_audio_rx_dma_list_cfg[rx_fifo_chn]);
// 2. Add linked list elements (self-looping, enabling single-buffered loop transmission)
audio_rx_dma_add_list_element(&g_audio_rx_dma_list_cfg[rx_fifo_chn],
&g_audio_rx_dma_list_cfg[rx_fifo_chn],
in_buff, buff_size);
}
TX DMA chain initialization process:
void audio_tx_dma_chain_init(audio_fifo_chn_e tx_fifo_chn, dma_chn_e chn,
unsigned short *out_buff, unsigned int buff_size)
{
audio_tx_fifo_chn = tx_fifo_chn;
// 1. Configure DMA channel parameters
audio_tx_dma_config(chn, out_buff, buff_size, &g_audio_tx_dma_list_cfg[tx_fifo_chn]);
// 2. Add linked list elements (self-looping, enabling single-buffered loop transmission)
audio_tx_dma_add_list_element(&g_audio_tx_dma_list_cfg[tx_fifo_chn],
&g_audio_tx_dma_list_cfg[tx_fifo_chn],
out_buff, buff_size);
}
Use examples
// Enter the configuration
audio_rx_dma_chain_init(FIFO0, DMA0, (unsigned short *)audio_buff, sizeof(audio_buff));
audio_rx_dma_en(DMA0);
// Output configuration
audio_tx_dma_chain_init(FIFO0, DMA1, (unsigned short *)audio_buff, sizeof(audio_buff));
audio_tx_dma_en(DMA1);
Audio FIFO interrupted
The Audio module provides an independent interrupt mechanism for each FIFO channel. When the FIFI's data volume reaches a preset threshold, an interrupt is triggered, allowing users to process audio data or monitor status within the interrupt service routine.
FIFO interrupt type
The Audio module supports two types of FIFO interrupts: RX and TX. For example, with the TL721x, it has 3 RX FIFOs and 3 TX FIFOs, resulting in the following 6 FIFO interrupts:
typedef enum
{
AUDIO_RX_FIFO0_IRQ = BIT(0), // RX FIFO0 interrupt
AUDIO_RX_FIFO1_IRQ = BIT(1), // RX FIFO1 interrupt
AUDIO_RX_FIFO2_IRQ = BIT(2), // RX FIFO2 interrupt
AUDIO_TX_FIFO0_IRQ = BIT(3), // TX FIFO0 interrupt
AUDIO_TX_FIFO1_IRQ = BIT(4), // TX FIFO1 interrupt
AUDIO_TX_FIFO2_IRQ = BIT(5), // TX FIFO2 interrupt
} audio_fifo_irq_type_e;
FIFO interrupt trigger principle
FIFO interrupts are triggered based on a threshold mechanism:
- RX FIFO Interrupt: Triggered when the amount of data received in the RX FIFO reaches or exceeds a set threshold
- TX FIFO Interrupt: Triggered when the amount of data sent in the TX FIFO reaches or exceeds a set threshold
The threshold is configured through the following interface:
/**
* @brief Set the RX FIFO interrupt threshold
* @param[in] rx_fifo_chn - RX FIFO channel
* @param[in] threshold - threshold (number of bytes), the actual value written to the register is threshold >> 2
*/
static inline void audio_set_rx_fifo_threshold(audio_fifo_chn_e rx_fifo_chn, unsigned short threshold);
/**
* @brief Set TX FIFO interrupt threshold
* @param[in] tx_fifo_chn - TX FIFO channel
* @param[in] threshold - threshold (number of bytes), the actual value written to the register is threshold >> 2
*/
static inline void audio_set_tx_fifo_threshold(audio_fifo_chn_e tx_fifo_chn, unsigned short threshold);
Notes
Threshold registers are in Word (4 bytes), so the actual threshold = configuration value × 4. For example, configuring 'AUDIO_BUFF_SIZE / 2' means an interrupt is triggered when the FIFO data volume reaches half of the buffer.
FIFO interrupt control interface
Enable FIFO interrupts:
/**
* @brief Enables RX FIFO interrupts
* @param[in] rx_fifo_chn - RX FIFO channel
*/
static inline void audio_rxfifo_irq_en(audio_fifo_chn_e rx_fifo_chn);
/**
* @brief Enable TX FIFO interrupts
* @param[in] tx_fifo_chn - TX FIFO channel
*/
static inline void audio_txfifo_irq_en(audio_fifo_chn_e tx_fifo_chn);
Query and clear interrupt status:
/**
* @brief Obtain the FIFO interrupt status
* @param[in] irq_status - Interrupt type
* @return Interrupt status; non-0 indicates the corresponding interrupt is triggered
*/
static inline unsigned char audio_get_irq_status(audio_fifo_irq_type_e irq_status);
/**
* @brief Clear the FIFO interrupt state
* @param[in] irq_status - Interrupt type
*/
static inline void audio_clr_irq_status(audio_fifo_irq_type_e irq_status);
For examples of FIFO interrupts usage, refer to the AUDIO_FIFO_IRQ_TEST case in the TL721x Audio Demo.
Common FIFO pointer and DMA pointer query interfaces
During FIFO interrupt handling or debugging, the positions of FIFO and DMA pointers can be obtained through the following interfaces:
| Interface | Function |
|---|---|
audio_get_rx_wptr(rx_fifo_chn) |
Get the RX FIFO write pointer |
audio_get_tx_rptr(tx_fifo_chn) |
Get the TX FIFO read pointer |
audio_get_rx_dma_wptr(dma_chn) |
Obtain the RX DMA target address |
audio_get_tx_dma_rptr(dma_chn) |
Obtain the TX DMA source address |
SPI
Overview
Standard SPI interface
The Serial Peripheral Interface (SPI) is a synchronous serial interface that allows embedded processors to communicate with peripheral devices and exchange data serially.
Standard SPI interfaces generally use 4-wire communication:

| Name | Meaning |
|---|---|
| CSN | Device chip selection signal line, low level effective |
| CLK | Clock signal line |
| MOSI | Master data output, Slave data input line |
| MISO | Master data input, Slave data output line |
SPI communication processes and four operating modes
The diagram below is a simple example of SPI communication:

CSN, CLK, and MOSI signals are all generated by the Master, with data output via MOSI lines. The MISO signal is generated by the Slave, and the Master reads the data from the Slave. MOSI and MISO signals are valid only when CSN is low, and data is triggered on the rising or falling edge of CLK. In CLK, MOSI and MISO transmit 1 bit of data per clock cycle, so 1 byte of data can be transferred in 8 clock cycles.
Based on the difference between the idle CLK clock polarity (CPOL) and the sampling time (CLK clock phase CPHA), SPI distinguishes four operating modes:
| SPI operating mode | CPOL | CPHA |
|---|---|---|
| SPI_MODE0 | 0 | 0 |
| SPI_MODE1 | 0 | 1 |
| SPI_MODE2 | 1 | 0 |
| SPI_MODE3 | 1 | 1 |
- CPOL=0: CLK remains low during idle moments; CPOL=1: CLK maintains a high level during idle moments.
- CPHA=0: Sampling is triggered at the odd edge of the CLK; CPHA=1: Sampling is triggered at the even-numbered edge of the CLK.
The Master and Slave must operate in the same mode to communicate normally.
Diverse SPI interfaces
Based on the standard SPI, to adapt to different application scenarios, many categories of SPI interfaces have gradually emerged:
-
3-line SPI: Only 3 wires (CLK, CSN, MOSI), sharing a single line for data transmission and reception, using half-duplex communication.
-
Dual SPI: Expands the use of MOSI and MISO to enable half-duplex operation, allowing data to be transmitted in both directions. MOSI becomes IO0, MISO becomes IO1, and can transmit 2 bits of data within one clock cycle.
-
Quad SPI: Expands the usage of WP and HOLD. WP becomes IO2, HOLD becomes IO3, and it has four data cables, capable of transmitting 4 bits per clock cycle.

Overview of Chip SPI Resources
SPI type description
Different chips provide different types of SPI peripherals. Below is the distribution of each type across all platforms and the support for each chip's IO mode and DMA:
| Chips | PSPI | HSPI | LSPI | GSPI | SPI | Total | IO mode | DMA |
|---|---|---|---|---|---|---|---|---|
| TLSR921x/TLSR951x | ✅ 1 | ✅ 1 | — | — | — | 2 | Single/Dual (PSPI), Single/Dual/Quad (HSPI) | Yes |
| TLSR922x/TLSR952x | — | — | ✅ 1 | ✅ 1 | — | 2 | Single/Dual/Quad | Yes |
| TL721x / TL751x | — | — | ✅ 1 | ✅ 1 | — | 2 | Single/Dual/Quad/3-line | Yes |
| TL321x / TL323x | — | — | — | ✅ 1 | — | 1 | Single/Dual/Quad/3-line | Yes |
| TL322x | — | — | ✅ 1 | ✅5 | — | 6 | Single/Dual/Quad/3-line | Yes |
| TLSR820x/TLSR8373 | — | — | — | — | ✅ 1 | 1 | Single/Dual/Quad/3-line | Yes |
| TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC123x | — | — | — | — | ✅ 1 | 1 | Single | No |
Meanings of each type:
| SPI type | Full name | Bus | Description |
|---|---|---|---|
| SPI | System Bus | TC series unified SPI modules, TLSR820x/TLSR8373 series support Single/Dual/Quad/3-Line, while the others only support Single | |
| PSPI | APB SPI | APB | TLSR921x/TLSR951x series exclusive low-speed SPI, supports only Single/Dual |
| HSPI | AHB SPI | AHB | TLSR921x/TLSR951x series Exclusive high-speed SPI, supporting Single/Dual/Quad |
| LSPI | LCD SPI | AHB | Supports specific ramless screen drivers (LUT command table, porch timing, frame control) |
| GSPI | General SPI | AHB | General/high-speed SPI, supports XIP, usually with an AHB bus |
- TLSR921x/TLSR951x are the only chips named after
PSPI/HSPI. - The TL322x has 5 GSPI instances, making it the chip with the most GSPI applications.
- All TC series chips have only one SPI module, uniformly named
SPI, with no distinction between PSPI/HSPI/LSPI/GSPI.
Function description
Interface naming rules
- spi prefix: Interfaces that HSPI and PSPI / GSPI and LSPI can all use.
- PSPI prefix: For PSPI only.
- hspi prefix: for HSPI only.
- gspi prefix: for GSPI only.
- lspi prefix: For LSPI only.
- dma suffix: interface used in DMA mode.
- plus suffix: supports a wider variety of read/write modes and operation commands (common across TLSR921x/TLSR951x/TLSR922x/TLSR952x series and TL series).
For example, the spi_master_write_read_dma_plus interface uses a DMA channel, first writing the address to the SPI Slave, then reading data from the corresponding address on the SPI Slave.
About "read" and "write_read":
- Interfaces with the "read" field support hardware automatically sending address frames, suitable for applications where hardware address frames are enabled.
- Interfaces with the "write_read" are used when no hardware address frame is enabled. The address information must first be written to the SPI Slave via “write” before the corresponding address can be read..
Comparison of SPI module capabilities for each chip
The hardware support for commonly used protocol frames of each chip's SPI modules is as follows (Y=support, N=Not supported):
| SPI module | cmd_en | cmd_fmt | address_en | address_fmt | 3-line | Dual | Quad |
|---|---|---|---|---|---|---|---|
| TLSR820x/TLSR8373 | Y | Y | Y | Y | Y | Y | Y |
| TLSR921x/TLSR951x HSPI | Y | Y | Y | Y | Y | Y | Y |
| TLSR921x/TLSR951x PSPI | Y | N | N | N | Y | Y | N |
| TLSR922x/TLSR952x GSPI | Y | Y | Y | Y | Y | Y | Y |
| TLSR922x/TLSR952x LSPI | Y | Y | Y | Y | Y | Y | Y |
- cmd_en/cmd_fmt: Hardware cmd frames and their format follow Dual/Quad I/O encoding
- address_en/address_fmt: The hardware address frame and its format follow the Dual/Quad I/O encoding
- The GSPI/LSPI capability of the TL721x/TL321x/TL322x/TL323x/TL751x series is the same as that of the TLSR922x/TLSR952x series
- The TLSR820x/TLSR8373 series has the same capabilities as the TLSR921x/TLSR951x HSPI, while other TC chips (TLSR825x/TLSR8359/TLSR827x/TLSR8355/TC321x/TC123x) do not support hardware cmd/addr frame functions. The TC122x does not have an SPI module and only supports low-level MSPI interfaces for Flash operations.
Master mode
In Master mode, Telink SPI provides two types of interfaces:
| Category | suffix | Applicable scenarios | Features |
|---|---|---|---|
| Standard Master | No _plus |
Simple data transmission and reception, no need for cmd/addr frames | Simple interface, only data is transferred |
| Plus Master | _plus |
Flash, PSRAM, and other peripherals that require cmd/addr/dummy frames | Hardware automatically generates cmd/addr/dummy frames |
Both types of interfaces support both polling and DMA transmission modes (_dma suffix).
-
Standard SPI Master
No
_plussuffix, suitable for scenarios where pure data is transmitted only (such as direct communication between two MCUs).Initialization: TLSR921x/TLSR951x series use
spi_master_config(),TLSR922x/TLSR952x series and TLxx series (TL321x/TL323x/TL322x/TL721x/TL751x and later TL series) usespi_master_config_plus()And close cmd/addr.Read/write: The Master sends/receives data directly, without specifying cmd or address. Interfaces are selected via
spi_sel_eparameters for SPI modules (TLSR921x/TLSR951x seriesare HSPI_MODULE/PSPI_MODULE, TLSR922x/TLSR952x and TLxx seriesare GSPI_MODULE/LSPI_MODULE). -
Plus Master (advanced frame format)
_plussuffix: hardware supports automatic generation of cmd frames, address frames, dummy frames (empty period), and adapts to standard SPI peripherals such as Flash/PSRAM for command protocols.The data frame format is: [cmd] + [address] + [dummy] + data ([] means optional).
Step 1 — Configure the data frame format. The TLSR921x/TLSR951x series use independent hspi_config_st/pspi_config_t, while the TLSR922x/TLSR952x and TLxx series use a unified spi_wr_rd_config_t:
// TLSR921x/TLSR951x series
hspi_config_st config = {
.hspi_io_mode = HSPI_QUAD, .hspi_dummy_cnt = 6,
.hspi_cmd_en = 1, .hspi_addr_en = 1, .hspi_addr_len = 3,
.hspi_cmd_fmt_en = 0, .hspi_addr_fmt_en = 1,
};
hspi_master_config_plus(&config);
// TLSR922x/TLSR952x series and TLxx series
spi_wr_rd_config_t config = {
.spi_io_mode = SPI_QUAD_MODE, .spi_dummy_cnt = 6,
.spi_cmd_en = 1, .spi_addr_en = 1, .spi_addr_len = 3,
.spi_cmd_fmt_en = 0, .spi_addr_fmt_en = 1,
};
spi_master_config_plus(GSPI_MODULE, &config);
Meanings of each field:
| Field | Description |
|---|---|
spi_io_mode |
I/O modes: Single/Dual/Quad |
spi_dummy_cnt |
Number of empty cycles between cmd and data |
spi_cmd_en |
Whether the hardware sends cmd frames. When closed, the cmd parameter on the _plus interface is invalid |
spi_addr_en |
Whether the hardware sends the address frame |
spi_addr_len |
address: Number of frame bytes (1\~4) |
spi_cmd_fmt_en |
Whether the cmd frame format follow I/O mode (if not, always Single) |
spi_addr_fmt_en |
Whether the address frame format follows the I/O mode |
Step 2 — Call the Plus read/write interface. The interface is named spi_master_{action}_plus and supports polling and DMA (_dma suffix):
| Interface | Purpose |
|---|---|
spi_master_write_plus |
Send cmd + address + write data |
spi_master_read_plus |
Send cmd + address to receive data |
spi_master_write_read_plus |
Send cmd + address (software splicing) to receive data |
Differences: The _plus interface of the TLSR921x/TLSR951x series returns void, while the TLSR922x/TLSR952x and TLxx series return drv_api_status_e. The cmd parameter of the TL751x is unsigned short (16-bit), while the rest of the chip is unsigned char (8-bit).
-
Reading and writing methods
Read/write mode is used to indicate whether the operation requires dummy (empty period) frames, and whether the instruction operation should be read or written:
typedef enum {
SPI_MODE_WR_WRITE_ONLY = 1, // Write
SPI_MODE_WR_DUMMY_WRITE = 8, // dummy + write
} spi_wr_tans_mode_e;
typedef enum {
SPI_MODE_RD_READ_ONLY = 2, // Read (must enable CmdEn)
SPI_MODE_RD_DUMMY_READ = 9, // dummy + read
} spi_rd_tans_mode_e;
typedef enum {
SPI_MODE_WR_RD = 3, // Write + Read (must enable CmdEn)
SPI_MODE_WR_DUMMY_RD = 5, // Write + dummy + read
} spi_wr_rd_tans_mode_e;
For example, when reading data, if the SPI Slave requires dummy-free frames, the reading method should be SPI_MODE_RD_DUMMY_READ.
Slave mode
-
HSPI/PSPI/GSPI/LSPI Slave
The HSPI in the TLSR921x/TLSR951x series supports Single, Dual, and Quad I/O modes when used as Slaves; When PSPI is used as a Slave, it supports Single and Dual I/O modes. Both automatically parse cmd, but Slave data reception and sending require software operation.
Both the TLSR922x/TLSR952x series and the TLxx series GSPI/LSPI slaves support Single/Dual/Quad, using a unified
spi_slave_init (spi_sel, mode)initialization.
Slave communication frame format: cmd + dummy (n×clock) + data. The I/O mode and dummy clock count of Master and Slave must be consistent.
| I/O mode | Write/read | Data line mapping |
|---|---|---|
| Single | MOSI (IO0) sends cmd + data, MISO (IO1) is used only for reading data | 1 bit/cycle |
| Dual | IO0/IO1 transmit/receive simultaneously, splitting every 2 bits into two wires | 2 bits/cycle |
| Quad | IO0\~IO3 simultaneous send/receive (requires WP/IO2 and HOLD/IO3 configuration) | 4 bits/cycle |
HSPI/PSPI Slave Operation Instructions (Compatible with All Series):
// Write cmd (partial examples)
SPI_WRITE_DATA_SINGLE_CMD = 0x51 // Single write
SPI_WRITE_DATA_DUAL_CMD = 0x52 // Dual writes
HSPI_WRITE_DATA_QUAD_CMD = 0x54 // Quad writes (HSPI only)
// Read cmd (partial examples)
SPI_READ_DATA_SINGLE_CMD = 0x0B // Single read
SPI_READ_DATA_DUAL_CMD = 0x0C // Dual reads
HSPI_READ_DATA_QUAD_CMD = 0x0E // Quad reads (HSPI only)
Instructions with the SPI prefix HSPI and PSPI are interchangeable; instructions with the HSPI prefix are only usable by HSPI.
-
SPI Slave Module (Dedicated Hardware Slave)
All TL series chips include an independent SPI Slave module (distinct from the GSPI/LSPI Slave), in which hardware automatically parses read/write commands and operates on the corresponding addresses without software intervention. Only Single/Dual I/O is supported. Only Single/Dual I/O is supported.
Note
The 'spi_slave_set_pin()' of the TLSR921x/TLSR951x series and TLSR922x/TLSR952x series has no parameters (fixed pins TLSR921x/TLSR951x: PA1\~PA4, TLSR922x/TLSR952x: PC0~PC3); Other chips can be flexibly configured through 'spi_slave_set_pin(sspi_pin_config_t*)'.
Comparison between SSPI modules and GSPI/LSPI Slave:
| Type | Agreement | Data format | Features |
|---|---|---|---|
| GSPI/LSPI Slave | cmd + dummy + data | Flexible and software-controlled | Supports Quad, pins can be paired |
| SPI Slave Module (SSPI) | cmd + addr(32bit h -> l) + data | Hardware auto-parsing | Only Single/Dual, simple scenarios |
The address order of SSPI modules is high-byte first (addr(32-bit) high -> low), which differs from the GSPI/LSPI Slave’s cmd+dummy+data format.
Clock settings
-
Clock source
Master clock sources vary depending on the chip/module:
| Chips / Modules | Clock source | Configuration methods |
|---|---|---|
| TLSR921x/TLSR951x HSPI | hclk | Manually calculate the divider: sys_clk.hclk * 1,000,000 / (2 × SPI_CLK) - 1 |
| TLSR921x/TLSR951x PSPI | pclk | Manually calculate the divider: sys_clk.pclk * 1,000,000 / (2 × SPI_CLK) - 1 |
| TL751x series | Optional SRC_CLK_XTAL_48M, etc |
Manual specification |
| TLSR922x/TLSR952x and other TLxx series | PLL clock, automatic selection | sys_clk.pll_clk * 1000000 / SPI_CLK |
Initialization example:
spi_master_init(GSPI_MODULE, sys_clk.pll_clk * 1000000, 3000000);
SPI_CLK going beyond the configuration range may cause communication failures. The maximum supported SPI clock varies by chip and is based on the header file comment.
-
Slave clock
The Slave clock is input by the Master and does not require manual configuration. Constraint:
F_Master ≤ F_source_slave ÷ 4(F_source_slaveis the internal clock source frequency of the Slave chip); otherwise, the Slave may not sample correctly.
interrupt
The SoC supports various SPI interrupt types and can be flexibly configured for different application scenarios.
| Modes | interrupt | Description | Abnormal interruption | Manual removal is required | Generation Party |
|---|---|---|---|---|---|
| Not DMA | SPI_RXF_OR_INT | RX FIFO over run: When received, the program read is not fast enough, and the RX FIFO is overwritten | Y | Y | Slave |
| Not DMA | SPI_TXF_UR_INT | TX FIFO under run: program writing is not fast enough during transmission, resulting in interrupted transmission | Y | Y | Slave |
| Not DMA | SPI_RXF_INT | RX FIFO Threshold Interrupt: RX FIFO data reaches/exceeds thresholds | N | Y | Master/Slave |
| Not DMA | SPI_TXF_INT | TX FIFO threshold interrupt: TX FIFO data is less than/reaches the threshold | N | Y | Master/Slave |
| DMA/Non-DMA | SPI_END_INT | Data transmission ends and is interrupted, completing a data transfer | N | Y | Master/Slave |
| DMA/Non-DMA | SPI_SLV_CMD_INT | In Slave mode, triggered every 1 Byte command received | N | Y | Slave |
Note
The interrupt state enumeration bit offset of the TLSR921x/TLSR951x series differs from TLSR922x/TLSR952x and TLxx series: in the TLSR921x/TLSR951x series, 'SPI_END_INT = BIT(6)', and in TLSR922x/TLSR952x and TLxx series, 'SPI_END_INT = BIT(4)'. When porting code across chips, be sure to check the definition of interrupt status bits.
Interrupt Enable and State Queries:
// Enable the interrupt
spi_set_irq_mask(SPI_MODULE_SEL, SPI_END_INT_EN | SPI_RXFIFO_INT_EN);
// Query the interrupt status
u8 status = spi_get_irq_status(SPI_MODULE_SEL);
if (status & SPI_END_INT) {
spi_clr_irq_status(SPI_MODULE_SEL, SPI_END_INT);
// Processing transmission complete
}
DMA mode
TLSR921x/TLSR951x series uses macro definitions to select DMA channels:
#define TX_DMA_CHN DMA2
#define RX_DMA_CHN DMA3
hspi_set_tx_dma_config(TX_DMA_CHN);
hspi_set_rx_dma_config(RX_DMA_CHN);
TLSR922x/TLSR952x and TLxx series configure structures using DMA:
spi_set_tx_dma_config(GSPI_MODULE, DMA2);
spi_set_master_rx_dma_config(GSPI_MODULE, DMA3);
Methods to determine whether data has been sent or received in DMA mode:
- Query method:
spi_is_busy(spi_sel)— returns busy status - Interrupt method:
spi_set_irq_mask(SPI_MODULE_SEL, SPI_END_INT_EN)— Enable END interrupts
DMA usage notes (universal across platforms):
SPI_END_INTinterrupt does not mean data transmission is complete (it only means FIFO data transfer is complete, CSN is not raised). After aSPI_END_INTinterrupt occurs, the busy signal is queried again until IDLE is reached, marking the end.- When using DMA for transmission, the send/receive struct or array must be aligned with four bytes (as shown in the demo by
__attribute__((aligned(4))). - When using DMA to receive SPI data into the buffer, the destination buffer size must be a multiple of 4. The reason is that each DMA sends 4 bytes to the Buffer, and even if the configured read length is less than 4, it will write 4 bytes to the destination address. For example, define the array buffer size as 5 bytes, configure the DMA to read 5 bytes from SPI into the buffer, and the DMA actually transfers 8 bytes to the buffer twice, with the extra 3 bytes possibly overwriting other variables. In this case, the array size should be configured to 8 Bytes.
- The DMA configurations in the TLSR922x/TLSR952x and TLxx series are divided into three independent configurations:
tx/master_rx/slave_rx. The TLSR921x/TLSR951x series usestx/rxconfigurations.
3-Line mode
The HSPI/PSPI Master/Slave of the TLSR921x/TLSR951x series, as well as the GSPI/LSPI of TLSR922x/TLSR952x and TLxx series, all support 3-Line mode.
Note
The TLSR922x/TLSR952x series only supports 3-Line with GSPI; LSPI does not.
The interface called is:
void spi_set_3line_mode(spi_sel_e spi_sel)
3-Line mode read/write instructions are compatible with HSPI/PSPI/GSPI/LSPI for SINGLE_CMD.
Multi-SPI Slave structure
For multi-SPI Slave applications, each Slave's CSN pin can be assigned to it. Once a data transfer is complete, the CSN will be incremented, and you can switch the CSN to simulate switching to a Slave.
TLSR921x/TLSR951x series HSPI Master calls the interface:
void hspi_cs_pin_dis(hspi_csn_pin_def_e pin);
void hspi_cs_pin_en(hspi_csn_pin_def_e pin);
TLSR922x/TLSR952x and TLxx series GSPI Master supports multiple CSNs (GSPI_XIP0\~3), enabling multiple Slaves through XIP configuration or direct GPIO operations.
XIP mode
XIP (eXecute In Place) refers to on-chip execution, allowing applications to directly fetch, decode, and execute from external storage devices. XIP can extend the SoC's address space to external storage devices.
- Chips supported by XIP
| Chips | GSPI XIP | LSPI XIP | Number of XIP channels |
|---|---|---|---|
| TLSR921x/TLSR951x HSPI | ✅ | — | 1 |
| TLSR922x/TLSR952x | ✅ | — | 4 (XIP0\~3) |
| TL721x | ✅ | ✅ | 4 |
| TL322x | ✅ | ✅ | 4 |
| TL321x | ✅ | ✅ | 4 |
| TL323x | ✅ | ✅ | 4 |
- TLSR921x/TLSR951x series XIP configuration
hspi_xip_seq_mode_en(); // Enable sequential mode
hspi_xip_page_size(4); // Set the page size
hspi_xip_en(); // Enable XIP
The seq_mode (sequential mode) in the TLSR921x/TLSR951x series refers to a sequential mode that divides data into blocks of \(2^{page\_size}\) Bytes for interval transmission. Each block is sent/sent and then sent again, increasing the CS once.
Send Instructions and Data Read/Write:
void hspi_master_write_xip_cmd_data(u8 cmd, u32 addr_offset, u8 data_in, spi_wr_tans_mode_e wr_mode);
void hspi_master_write_xip(u8 cmd, u32 addr_offset, u8 *data, u32 data_len, spi_wr_tans_mode_e wr_mode);
void hspi_master_read_xip(u8 cmd, u32 addr_offset, u8 *data, u32 data_len, spi_rd_tans_mode_e rd_mode);
TLSR921x/TLSR951x series XIP on-chip executor — switch the PC pointer to the corresponding address of the XIP device (base address 0x1000000 + relative address 0x00) via the following two commands:
__asm__("li t0,0x1000000");
__asm__("jarr t0");
- TLSR922x/TLSR952x and TLxx series XIP configurations
The TLSR922x/TLSR952x and TLxx series use a unified spi_xip_config_t structure and support a richer range of configuration options:
spi_xip_config_t xip_config = {
// Read the configuration
.spi_xip_rd_io_mode = SPI_QUAD_MODE,
.spi_xip_rd_addr_len = 2, // 3-byte address (2 = 3 - 1)
.spi_xip_rd_addr_en = 1,
.spi_xip_rd_cmd_en = 1,
.spi_xip_rd_dummy_cnt = 3, // 4 dummy cycles (3 = 4 - 1)
.spi_xip_rd_transmode = SPI_MODE_WRITE_DUMMY_READ,
.spi_xip_rd_cmd = 0x0B, // Fast Read command
.spi_xip_wr_io_mode = SPI_QUAD_MODE,
// ... Write configurations are similar
.spi_3_line_en = 0,
};
spi_master_init(GSPI_MODULE, 48000000, 3000000);
gspi_set_xip_pin(&xip_pin_config);
gspi_xip_end_addr_set();
gspi_set_xip_config(GSPI_XIP0, &xip_config);
spi_xip_en(GSPI_MODULE);
The GSPI XIP base addresses for the TLSR922x/TLSR952x and TLxx series are 0x88000000 and support 4 XIP regions:
- XIP0: 0x88000000 \~ 0x88ffff00
- XIP1: 0x89000000 \~ 0x89ffff00
- XIP2: 0x8a000000 \~ 0x8affff00
- XIP3: 0x8b000000 \~ 0x8bffff00
Demo example
TL Series Demo
| Demo version | Path | Applicable chips |
|---|---|---|
| V1.0 | demo/vendor/SPI_Demo/SPI_V1.0/ |
TLSR921x/TLSR951x |
| V1.1 | demo/vendor/SPI_Demo/SPI_V1.1/ |
TLSR922x/TLSR952x / TL721x / TL321x / TL751x / TL322x / TL323x |
V1.0 dummy cap is 8 cycles; V1.1 dummy's upper limit for TLSR922x/TLSR952x is 32 cycles, while others have 256 cycles.
TC Series Demo
The TC series SDK provides two SPI demos:
| Demo version | Path | Applicable chips |
|---|---|---|
| V1.0 | tc_platform_sdk/demo/vendor/SPI_Demo/SPI_V1.0/ |
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC123x |
| V1.1 | tc_platform_sdk/demo/vendor/SPI_Demo/SPI_V1.1/ |
TLSR820x/TLSR8373 |
Quick Getting Started
TLSR922x/TLSR952x/TL721x/TL751x/TL322x/TL323x
#include "spi.h"
// Step 1: Configure the SPI pins
gspi_pin_config_t gspi_pin = {
.spi_clk_pin = GPIO_PB2,
.spi_csn_pin = GPIO_PB3,
.spi_mosi_io0_pin = GPIO_PB4,
.spi_miso_io1_pin = GPIO_PB5,
};
gspi_set_pin(&gspi_pin);
// Step 2: Configure the Master mode parameters
spi_wr_rd_config_t config = {
.spi_io_mode = SPI_SINGLE_MODE,
.spi_dummy_cnt = 0,
.spi_cmd_en = 0,
.spi_addr_en = 0,
};
spi_master_config_plus(GSPI_MODULE, &config);
// Step 3: Initialize the SPI clock (sys_clk=48M, target SPI_CLK=3M)
spi_master_init(GSPI_MODULE, sys_clk.freq * 1000000, 3000000);
// Step 4: Send data (when cmd_en=0, cmd parameter is invalid and can be set to 0)
unsigned char tx_buf[10] = {0x01, 0x02, 0x03, 0x04, 0x05};
spi_master_write_plus(GSPI_MODULE, 0, 0, tx_buf, 5, SPI_MODE_WR_WRITE_ONLY);
TLSR921x/TLSR951x
#include "spi.h"
// TLSR921x/TLSR951x use different names: hspi (AHB SPI) / pspi (APB SPI)
hspi_config_t config = {
.hspi_io_mode = HSPI_SINGLE,
.hspi_dummy_cnt = 0,
.hspi_cmd_en = 0,
.hspi_addr_en = 0,
};
// Pins are set individually for TLSR921x/TLSR951x
hspi_set_pin_mux(HSPI_CLK_PB4);
hspi_set_pin_mux(HSPI_CSN_PB6);
hspi_set_pin_mux(HSPI_MOSI_IO0_PB3);
hspi_set_pin_mux(HSPI_MISO_IO1_PB2);
hspi_master_config(&config);
hspi_master_init(48000000, 3000000);
hspi_master_write(tx_buf, 5);
TC Series SPI
The TC series chips are based on the TC32 core and use tc_platform_sdk. All TC chips that support SPI have only one SPI module, and their API style differs significantly from the TL series—the interface is simpler, with no _plus suffix layering.
Master mode
The TC series SPI Master offers two types of APIs:
- Universal APIs (shared by all TC chips):
spi_master_init(),spi_write(),spi_read(),spi_master_gpio_set(). -
Advanced API (TLSR820x/TLSR8373 only):
spi_config_tconfiguration structure, supports cmd/addr/dummy frame formats and Dual/Quad modes. -
Pin configuration
Different TC series chips have different pin configurations:
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 — Fixed pin group selection:
// Define fixed pin sets (enumeration type)
typedef enum {
SPI_GPIO_GROUP_A2A3A4D6 = 0, // SDO=A2, SDI=A3, SCK=A4, CSN=D6
SPI_GPIO_GROUP_B6B7D2D7, // SDO=B7, SDI=B6, SCK=D7, CSN=D2
} SPI_GPIO_GroupTypeDef;
// Master pin configuration
spi_master_gpio_set(SPI_GPIO_GROUP_A2A3A4D6);
// Additional CS pin selection (if external CS is needed)
spi_masterCSpin_select(GPIO_PD2);
TLSR827x/TLSR8355 — Independent pin selection (TLSR827x/TLSR8355 additional support):
// Four pins can be selected independently, not limited to fixed groups
spi_master_gpio_set(SPI_GPIO_SCL_A4, SPI_GPIO_CS_D6,
SPI_GPIO_SDO_A2, SPI_GPIO_SDI_A3);
TLSR820x/TLSR8373 / TC321x / TC123x — Structure Pin Configuration:
// TLSR820x/TLSR8373 includes WP/HOLD pins (for Quad mode)
spi_pin_config_t pin_config = {
.spi_clk_pin = GPIO_PA4,
.spi_csn_pin = GPIO_PD6,
.spi_mosi_io0_pin = GPIO_PA2,
.spi_miso_io1_pin = GPIO_PA3,
.spi_wp_io2_pin = GPIO_PB4, // TLSR820x/TLSR8373 only
.spi_hold_io3_pin = GPIO_PB4, // TLSR820x/TLSR8373 only
};
spi_set_pin(&pin_config);
// TC321x/TC123x do not have WP/HOLD
spi_pin_config_t pin_config = {
.spi_clk_pin = GPIO_PA4,
.spi_csn_pin = GPIO_PD6,
.spi_mosi_io0_pin = GPIO_PA2,
.spi_miso_io1_pin = GPIO_PA3,
};
spi_set_pin(&pin_config);
- Clock and mode initialization
The TC series uses a unified spi_master_init() function, and the clock calculation formula is:
SPI Clock = System Clock / ((DivClock + 1) × 2)
The system clock is usually 24MHz, with the division coefficient corresponding to common clocks:
| SPI frequency | DivClock | Actual frequency (sysclk = 24M) |
|---|---|---|
| 200 KHz | 0x3c | \~200 KHz |
| 250 KHz | 0x2e | \~250 KHz |
| 500 KHz | 0x17 | \~500 KHz |
| 1 MHz | 0x0b | \~1 MHz |
| 2 MHz | 0x05 | \~2 MHz |
| 4 MHz | 0x02 | \~4 MHz |
Note
The 200K/250K crossover coefficients for TC321x/TC123x differ slightly from TLSR825x/TLSR8359 / TLSR827x/TLSR8355 ('0x3b'/'0x2f' vs '0x3c'/'0x2e'), but both can be directly selected using the 'SPI_24M_ClkTypeDef' enumeration.
Example of initialization code:
// Initialize the SPI Master with a 1MHz clock and MODE0
spi_master_init(SPI_CLK_1M, SPI_MODE0);
// Also supports direct transmission of the crossover factor
spi_master_init(0x0b, SPI_MODE0); // DivClock=0x0b → SPI_CLK = 24M/(12×2) = 1MHz
- Data read/write
The read/write interface of the TC series SPI operates on a "command prefix, then read and write data" approach. When writing data, send the cmd sequence first, then the data; when reading data, send the cmd sequence first, then receive the data.
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 (requires passing CS pin parameters):
// Write: First send cmd[0..CmdLen-1], then send data[0..DataLen-1]
void spi_write(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen,
GPIO_PinTypeDef CSPin);
// Read: First send cmd[0..CmdLen-1], then receive DataLen bytes to Data[]
void spi_read(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen,
GPIO_PinTypeDef CSPin);
// Use examples
unsigned char cmd = 0x02; // Write commands
unsigned char tx_data[] = {0x01, 0x02};
spi_write(&cmd, 1, tx_data, 2, GPIO_PD2);
unsigned char rd_cmd = 0x03; // Read the command
unsigned char rx_data[4];
spi_read(&rd_cmd, 1, rx_data, 4, GPIO_PD2);
TLSR820x/TLSR8373 / TC321x / TC123x (no CS pin parameters required, hardware CS is used):
// Write (without CSPin parameters)
void spi_write(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen);
// Read (without CSPin parameters)
void spi_read(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen);
// Use examples
unsigned char cmd = 0x02;
unsigned char tx_data[] = {0x01, 0x02};
spi_write(&cmd, 1, tx_data, 2);
unsigned char rd_cmd = 0x03;
unsigned char rx_data[4];
spi_read(&rd_cmd, 1, rx_data, 4);
Note
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 require manual specification of CS pins for each transmission, suitable for multi-slave scenes; TLSR820x/TLSR8373 and later TCxx series chips use 'spi_masterCSpin_select()' to preselect hardware CS pins, so no CS parameters are required for write/read.
- TLSR820x/TLSR8373 Advanced Frame Format (Plus Mode)
The TLSR820x/TLSR8373 supports cmd/addr/dummy frame configurations similar to the TL series and can be adapted to SPI peripherals that use command-frame protocols (such as Flash, PSRAM, etc.).
Step 1 — Configure spi_config_t structure:
typedef struct {
spi_io_mode_e spi_io_mode; // Interface modes: SPI_SINGLE_MODE / SPI_DUAL_MODE / SPI_QUAD_MODE / SPI_3LINE_MODE
unsigned char spi_dummy_cnt; // dummy cycle quantity
unsigned char spi_cmd_en; // Enable cmd frames
unsigned char spi_addr_en; // Enable address frames
unsigned char spi_addr_len; // address length (bytes)
unsigned char spi_cmd_fmt_en; // cmd frame format follows Dual/Quad
unsigned char spi_addr_fmt_en; // The address frame format follows Dual/Quad
} spi_config_t;
Step 2 — Configuration Example:
// Configured in Quad I/O mode, 6 dummy cycles, enabling cmd+addr frames
spi_config_t config = {
.spi_io_mode = SPI_QUAD_MODE,
.spi_dummy_cnt = 6,
.spi_cmd_en = 1,
.spi_addr_en = 1,
.spi_addr_len = 3, // 3-byte address
.spi_cmd_fmt_en = 0, // cmd frames remain in Single mode
.spi_addr_fmt_en = 1, // address frames follow the Quad pattern
};
Step 3 — Enumeration of read/write methods (same as TL series):
typedef enum {
SPI_MODE_WR_WRITE_ONLY = 1, // Pure writing
SPI_MODE_WR_DUMMY_WRITE = 8, // dummy + write
} spi_wr_tans_mode_e;
typedef enum {
SPI_MODE_RD_READ_ONLY = 2, // Pure read (requires enable CmdEn)
SPI_MODE_RD_DUMMY_READ = 9, // dummy + read
} spi_rd_tans_mode_e;
typedef enum {
SPI_MODE_WR_RD = 3, // Write + Read (requires enable CmdEn)
SPI_MODE_WR_DUMMY_RD = 5, // Write + dummy + read
} spi_wr_rd_tans_mode_e;
Step 4 — Complete Initialization and Usage Example:
// Pin configuration
spi_pin_config_t pin = {
.spi_clk_pin = GPIO_PA4,
.spi_csn_pin = GPIO_PD6,
.spi_mosi_io0_pin = GPIO_PA2,
.spi_miso_io1_pin = GPIO_PA3,
.spi_wp_io2_pin = GPIO_PB4,
.spi_hold_io3_pin = GPIO_PB4,
};
spi_set_pin(&pin);
// Master clock initialization
spi_master_init(SPI_CLK_2M, SPI_MODE0);
// Configure advanced frame formats
spi_config_t config = {
.spi_io_mode = SPI_QUAD_MODE,
.spi_dummy_cnt = 6,
.spi_cmd_en = 1,
.spi_addr_en = 1,
.spi_addr_len = 3,
.spi_cmd_fmt_en = 0,
.spi_addr_fmt_en = 1,
};
// Write the configuration to the register (via the register bit operation function corresponding to each field)
spi_quad_mode_en();
// ... Other configurations ...
// Data transmission
unsigned char cmd = 0xEB; Quad I/O Read command
unsigned int addr = 0x000000;
unsigned char rx_buf[64];
// The TLSR820x/TLSR8373 use a DMA read/write interface
spi_master_read_dma_plus(/*...*/);
The TLSR820x/TLSR8373 also offers DMA transport support (spi_master_write_dma_plus, spi_master_read_dma_plus, etc.), interrupt support (SPI_RXFIFO_OR_INT_EN, etc.), and full spi_tans_mode_e enumeration (10 transmission modes in total). For detailed usage, please refer to tc_platform_sdk/chip/b80/drivers/spi.h.
Slave mode
All TC series chips support SPI Slave mode:
// Initialize the Slave mode
spi_slave_init(DivClock, SPI_ModeTypeDef Mode);
// Slave pin layout (TLSR825x/TLSR8359 / TLSR827x/TLSR8355)
spi_slave_gpio_set(SPI_GPIO_GROUP_A2A3A4D6);
// Slave pin configuration (TLSR820x/TLSR8373/TC321x, etc.)
Use spi_set_pin() to share the same configuration function with the Master
TLSR820x/TLSR8373 Slave Additional Supports:
- Recognize commands sent by Master through
spi_cmd_eenumeration:SPI_READ_DATA_SINGLE_CMD(0x0B),SPI_READ_DATA_DUAL_CMD(0x0C),SPI_READ_DATA_QUAD_CMD(0x0E),SPI_WRITE_ DATA_SINGLE_CMD(0x51), etc - Retrieve the command received by the Slave end:
spi_slave_get_cmd(). - Set the command the Slave side is ready to send to the Master:
spi_master_set_cmd().
// The TLSR820x/TLSR8373 Slave side receives instructions sent by the Master
unsigned char cmd = spi_slave_get_cmd();
if (cmd == SPI_WRITE_DATA_SINGLE_CMD) {
// The Master writes data
} else if (cmd == SPI_READ_DATA_SINGLE_CMD) {
// The Master needs to read the data
}
Note
The TC series does not have a dedicated SPI Slave module (unlike the TL series' SSPI module); the Slave function is configured directly to Slave mode through the main SPI module.
Common precautions
DMA transmission
- The send/receive buffer must be aligned to 4-byte boundaries. Use
__attribute__((aligned(4)))to modify arrays or structures. - The DMA receives a Buffer size that must be a multiple of 4. DMA writes 4 bytes each time, and even if the configured read length is less than 4, it still writes the full 4 bytes. For example, to read 5 bytes, the buffer size should be defined as 8 bytes; otherwise, extra bytes will overwrite adjacent variables.
SPI_END_INTInterrupt ≠ End of transmission. This interrupt only indicates that FIFO data has been transmitted and CSN has not yet been raised. After an interruption, you need to queryspi_is_busy()again until IDLE is returned, indicating the operation is complete.- The DMA configurations for TLSR922x/TLSR952x and TLxx series are divided into three independent configurations:
tx/master_rx/slave_rx, while TLSR921x/TLSR951x usetx/rxgroups.
Slave clock constraint
The Slave clock is input by the Master and must meet F_Master ≤ F_source_slave ÷ 4; otherwise, the Slave end may not sample correctly. F_source_slave is the internal clock source frequency of the Slave chip itself. For details, please refer to the Clock settings section.
Slave Type Selection (TL Series)
The TL series offers two slave modes:
| Type | Applicable scenarios | Features |
|---|---|---|
| GSPI/LSPI Slave | Requires Quad I/O and flexible pin configuration | Software sends and receives data manually, requiring manual protocol processing |
| SPI Slave Module (SSPI) | Simple address-data read/write | Hardware automatically parses read/write commands without software intervention |
Master/Slave tests wiring requirements
- Wiring method
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC123x, in Master and Slave test scenarios, data flying wires are cross-connected (see spi_set_pin() note):
- Ordering the power transfer
You must start Slave first, then start Master. Slave must first complete initialization and enter a waiting state, after which the Master initiates read/write timing. Otherwise, when the Master sends data, the Slave is not ready to respond, resulting in errors during data comparison.
-
Flying wires and ground together
-
Keep the flying wire as short as possible: SPI is a high-speed signal, and if the flying wire is too long, it may cause data sampling errors
- Master and Slave must be grounded together: the GND of the two chips must be connected together via flying wires to ensure the level reference is consistent; otherwise, the signal cannot be correctly identified.
QDEC
Overview
The Quadrature Decoder (QDEC) hardware module is primarly used to decode input signals from quadrature encoders (such as mouse wheels and motor speed discs). By real-time detection of rising- and falling-edge signals from Channel A and Channel B phases, the hardware automatically accumulates or decrements count values to accurately calculate the device's rotation direction and number of steps.
Step counting operation mode
The driver supports two operating modes and can be configured via API to meet different precision requirements:
- Normal mode (COMMON_MODE).
- Trigger condition: The QDEC counter only increments or subtracts 1 when the same rising or falling edge is detected from both phase A and B signals.
- Double Accuracy Mode (DOUBLE_ACCURACY_MODE).
- Trigger condition: On every rising and falling edge of the A/B signal, the counter responds by incrementing or decrementing by 1.
- Effect: Each time the physical wheel makes a full step roll (One Wheel Rolling), the count value changes by 2, providing higher resolution.
The entire series of chip pins supports matrix
There are clear differences in the enumeration types (Pin Enum) used and the physical pins supported at the underlying layer of different chips. Below is the complete pin mapping table extracted from the underlying driver:
| Chip model | Pin configuration method | Supported input pin enumeration values (complete list). |
|---|---|---|
| TLSR820x/TLSR8373 | Separate A/B phases | Phase A: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A Phase B: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR825x/TLSR8359 | Separate A/B phases | Phase A: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A Phase B: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR827x/TLSR8355 | Separate A/B phases | Phase A: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A Phase B: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR8298 | Separate A/B phases | Phase A: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A Phase B: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR922x/TLSR952x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TC321x | Separate A/B phases | Phase A: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A Phase B: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TL321x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL721x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TC122x | Separate A/B phases | Phase A: PA0A, PA4A, PA5A, PA6A, PA7A, PB0A, PB1A, PB2A . Phase B: PA0B, PA4B, PA5B, PA6B, PA7B, PB0B, PB1B, PB2B |
| TC123x | Separate A/B phases | Phase A: PA0A, PA4A, PA5A, PA6A, PA7A, PB0A, PB1A, PB2A . Phase B: PA0B, PA4B, PA5B, PA6B, PA7B, PB0B, PB1B, PB2B |
| TL322x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL323x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL521x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL523x | Separate A/B phases | Phase A: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A Phase B: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TL751x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL753x | Unified corridor enumeration | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
Core API feature analysis
void qdec_clk_en(void)- Function: Initializes 32 kHz external/internal clocks (
rc_32k_cal, etc.), and enables QDEC module clock gating. The new architecture chip will synchronously call reset operations within this function.
- Function: Initializes 32 kHz external/internal clocks (
void qdec_reset(void)- Function: Clears QDEC's internal logic and counters by operating the relevant reset registers (such as
reg_rst0orreg_rst3).
- Function: Clears QDEC's internal logic and counters by operating the relevant reset registers (such as
void qdec_set_pin(...)- Function: Maps the specified GPIO to the input of the decoder. The corresponding enumeration values must be passed in according to the support matrix for the pins above.
void qdec_set_mode(qdec_mode_e mode)- Function: Set count trigger strategies, optional
COMMON_MODEorDOUBLE_ACCURACY_MODE.
- Function: Set count trigger strategies, optional
void qdec_set_debouncing(qdec_thrsh_e thrsh)- Function: Configurable hardware filtering thresholds to filter out jitter noise from mechanical switches. Level changes smaller than the threshold width will be discarded. The parameter enumeration covers 8 levels from 187.5 US to 24,000 US.
signed char qdec_get_count_value(void)- Function: Send a reload instruction to
the reg_qdec_loadand read the returned difference. The signs indicate direction, and the value represents the total number of steps since the last reading. After reading, the hardware difference status will automatically reset.
- Function: Send a reload instruction to
Drive configuration and execution processes
Below is a standard business flow for initializing and polling QDEC in bare metal programs or RTOS tasks:
graph TD
Start ([System Boot / Power Reset]) --> InitGPIO [Configure General Peripheral GPIO <br>Enable PB6 / PB7 Input]
subgraph user_init [QDEC Hardware Initialization]
InitGPIO --> EnableClk [Enabling QDEC Clock <br>qdec_clk_en]
EnableClk --> SetMode [Set double precision mode <br>qdec_set_mode]
SetMode --> SetPin [Maps A/B phase to PB6/PB7<br>qdec_set_pin]
SetPin --> SetDebounce [Configure hardware stabilization threshold <br>qdec_set_debouncing]
end
SetDebounce --> MainLoop((Enter main loop))
Subgraph main_loop [Business Main Loop]
MainLoop --> ReadValue[Read current period count <br>qdec_get_count_value]
ReadValue --> CalcTotal [Accumulate Total Steps <br>total_count += qdec_count]
CalcTotal --> PrintOut [Serial port <br>printf]
PrintOut --> Delay [Blocking Delay 1000ms<br>delay_ms]
end
Delay --> MainLoop
JTAG
Overview
JTAG (Joint Test Action Group) is a hardware debugging and testing technology commonly used in integrated circuits to diagnose and debug issues. JTAG's official name is the IEEE 1149.1 standard. It is a testing method implemented via scan chains, allowing integrated circuits to be tested and debugged without damaging the chip. JTAG technology is widely used in hardware development, including digital integrated circuits, embedded systems, and circuit boards.
In JTAG, all test points on the chip are connected to a scan chain, enabling "non-intrusive" testing and debugging. Through the scan chain, test modes can be loaded into the chip, or the chip's status can be read. In addition to testing and debugging, JTAG can also be used to program and configure data into chips, making it easier for developers to debug and test hardware and software jointly. The JTAG interface is usually connected to the development board via debugging tools, allowing developers to debug and test integrated circuits remotely.
ICEman can be understood as gdbserver, which is middleware for communication between the gdb client and the board, i.e., openocd shown in the diagram below.

Quick Start
If this is your first time using JTAG for debugging, you can quickly run the entire process by following these steps:
-
Confirm hardware connection — Connect the TDI, TCK, TMS, and TDO of the JTAG debugger to the corresponding pins on the board. After the board is powered on, the JTAG tool should only turn on with a blue light. For details, please refer to the section Hardware connection and driver installation.
-
Install driver — Make sure the JTAG driver is installed (
BDT/ice/libusb-AICE-driver/Install_driver.exe). For details, please refer to the section Hardware connection and driver installation. -
Add pin enable in code — after the
gpio_init()call, addjtag_set_pin_en()(four-line JTAG) orsdp_set_pin_en()(two-line SDP) depending on the pattern. For details, please refer to the section Software and IDE configuration. -
Start ICEman — execute the four lines
./ICEman -Z v5, and the two-lineexecute ./ICEman -Z v5 -I aice_sdp.cfg, and record the output TCP port number (usually 1111). For details, please refer to the chapteron software and IDE configuration . -
Configure IDE Debug — Create a new C/C++ Remote Application, select the ELF file, set GDB to
riscv32-elf-gdb, and enter the port number output by ICEman for TCP ports. For details, please refer to the section IDE Debug configuration. -
Start Debugging — Click the Debug button, set breakpoints, and use features like Step Over / Resume to debug. For details, please refer to the section Breakpoints and Single-step debugging.
Tip
If you encounter problems along the way, please refer to the FAQ section for solutions.
Quick Connection Guide
Hardware connection and driver installation
-
Board wiring
After receiving the board, find the corresponding schematic and use the switch to switch between two-wire/four-wire modes. In principle, it automatically detects the default level of PB0 (which varies by chip; please refer to the schematic) upon power-on. GND is detected as four-wire, and 3v3 as two-wire.
Connect the JTAG debugger's TDI, TCK (clock source hclk), TMS, and TDO to the corresponding pins on the board. After connecting the signal cables, power on the chip. You can use the small black box 3v3 to connect to the board VBAT, or, if you have a USB module, use 5V to connect to VBUS. The chip outputs a voltage to the JTAG as a reference level, connects the board's 3V3 output voltage to the JTAG box REF, and provides the JTAG signal. TCK/TMS/TDI/TDO should operate at 3.3V to avoid damaging the IO or causing JTAG communication failure.
If the JTAG tool only lights up blue at this time, it indicates the wiring is normal; If a red light appears, it indicates a power supply issue or incorrect signal cable connections. You need to check the wiring and power supply.
-
JTAG driver installation
BDT/ice/libusb-AICE-driver/Install_driver.exe
When installing the IDE, this driver will be installed by default. If not installed, a red light may appear, so be sure to install this driver before use.

Software and IDE configuration
-
JTAG pin initialization
During program initialization,
gpio_shutdown(GPIO_ALL)/gpio_init()is called, and this interface will configure JTAG IO to GPIO mode, causing the JTAG connection to fail. Therefore, after initialization, you need to re-enable the JTAG pins:- Four-wire mode: Call
jtag_set_pin_en()to initialize the JTAG pins; - Two-wire mode: Call
sdp_set_pin_en()to initialize the SDP pins.
- Four-wire mode: Call
Note
If the above interfaces are not called, the JTAG debugger will not be able to connect properly to the target chip.
-
Connection command
Start ICEman, four-line mode executes:
./ICEman -Z v5, two-line mode executes:./ICEman -Z v5 -I aice_sdp.cfg

The ICEman tool (based on OpenOCD) has been successfully initialized and shows it is ready for use. Listing information such as the listening port, JTAG frequency, and target core indicates that communication between ICEman and the AICE-MINI+ debugger has been established and that ICEman has identified the target device (at least from the perspective of the JTAG interface). 1111 is the required TCP port number. During debugging, you need to ensure ICEman is running and the terminal cannot be closed.
ICEman is located in the
$IoTStudio/RDS/V5.1.1/ice/directory, and you can also open ICEman yourself.
Special connection instructions
For chips that support 32k_watchdog periodic reset (cyclic reset), the 32k_watchdog must be turned off when connecting JTAG; otherwise, the reset will clear the command written to the RAM (ICEman itself does not write instructions when connected; only memory can be written when executed), causing the JTAG connection to be disconnected again.
The steps to disable the 32k_watchdog are as follows:
- Download the latest Telink IoT Studio installer; extracting it will reveal the following contents:

Run TelinkIoTStudio_V2025.2.exe to install. After installation, you must run TelinkIoTStudio Updater.exe to update the components in IoT Studio to the latest version.
Make sure that after the update, to use the JTAG feature, open the ICEman terminal in IoT Studio (after startup, other operations are the same as on other chips):

Executing in the shell:
./start_ICEman.sh
You can launch ICEman.
JTAG burning
There are two JTAG burning methods: Telink IoT Studio IDE and Andes IDE.
(1) Telink IoT Studio IDE
The interface is as follows:

(2) Andes IDE
The interface is as follows:
for flash programming with JTAG
-
1 is the software download path;
-
2 is the BIN file to be downloaded;
-
3 is the offset address for Flash;
-
4 is the address and port number to connect to during download. If you fill in the wrong box or fail to check the box, the download code will be incorrect.
Note
When programming Flash via JTAG, first confirm the JTAG connection is correct. If unsuccessful, try setting the target to ICE and checking SDP (dual wire).
Debugging of the D25
IDE Debugging configuration
- After ICEman starts, click the
Debugicon on the toolbar and pull down the arrow to selectDebug Configuration.

- Select C/C++ Remote Application, configure the
Maintab, choose the compiled .elf file as input for gdb, and chooseDisable auto build.

- Click
Select otherat the bottom and chooseManual(default isAutomatic).

- Click the
Debuggertab and configure the corresponding debugger. This isriscv32-elf-gdb. Be careful not to check Stop on startup at.

- Click the
Connectsubtab and enter theTCPport number as the1111obtained by ICEman.

- Once configuration is complete, click the
Debugbutton and add breakpoints to start debugging.
In the Startup interface, the Set breakpoint at option can set the position of the first breakpoint during startup debugging. You can set it to main, meaning that after debugging starts, it will default to the main function entry. After completing the above configuration, click the Debug button in the lower right corner to start debugging. Next time you debug, this configuration will appear under your name under the debug button. Just click it to restart debugging.


Breakpoint
Currently, TLSR9 series SoCs support up to two hardware breakpoints. When you find that your program's starting address is 0x20000000, it means it is running inside flash and requires hardware breakpoints. Commands like step in or step over use a breakpoint so that users can define only one breakpoint during debugging; otherwise, exceptions will occur. This is also why it's not recommended to check Stop on startup at, because this option actually sets a breakpoint.
If you encounter a CANNOT ACCESS MEMORY AT ADDRESS XX exception during debugging, you can use the info br command to check the number of breakpoints.
After entering debug mode, double-click the left side of the row number where the program needs to stop to set a breakpoint. The breakpoint status is as follows:

Note
-
A checkmark must be present before the breakpoint; otherwise, the breakpoint is invalid.
-
The program stop position may be one or two lines after setting the breakpoint (this is normal).
Click the Resume button to execute the program to the breakpoint; click again to proceed to the next breakpoint.

Link to source files
During breakpoint debugging, an error may occur where the source file cannot be found. At this point, click Edit Source Path..., as shown below:

Click Add, select Path Mapping, and edit as follows:


The left side requires manual input, while the right side selects the path.
The principle is to correctly map the path in IoT Studio to the local path; the simplest approach is to map /cygdrive/c/ to C:\ (since the SDK is on the C drive).
Once the above configuration is complete, normal debugging can proceed.
Single-step debugging
-
Step Into: Single-step debugging triggers execution of a subfunction when encountering it
-
Step Over: Single-step debugging that does not execute a child function when encountering it
Note
If an error occurs during Step Into debugging (due to database closure or other reasons), it is recommended to use Step Over or breakpoint methods instead.


Introduction to the debugging toolbar and interface

The functions of the debugging toolbar are as follows:
(1) Resume: After the breakpoint pauses, the program continues at full speed from here until the next breakpoint or manual pause;
(2) Terminate: Ends the current JTAG/GDB debugging session;
(3) Step Into: Jump in in one step;
(4) Step Over: Skip in a single step, run the entire line without entering the subfunction, skip directly to the next line;
(5) Step Return: Step out, execute the remaining code from the current function in one go, then return to the upper-level call;
(6) Restart: Restarting debugging;
(7) Debug: Enable debugging or enable related debugging configurations.
In the Windows above the IDE, click Show View to see debugging-related tabs, the most commonly used being
Memory: View values in memory;Expressions: Parse variables or variable expressions;Registers: View registers on the kernel.

Multi-core debugging
N22 debugging
-
Connection
No need to select N22 or D25F; select the toolchain. D25F uses v5f for engineering and v5 for N22 projects; the compiler doesn't need to know which core it is. For JTAG connections, if both cores are running normally, OpenOCD will create two socket ports, and the host computer controls the two cores through these ports.

-
Differences between N22 and D25F tuning
a. Before debugging N22, the N22 must be able to run normally. N22 and DSP are reset by default and need to be awakened via mailbox in the D25F code before N22 can take instructions from flash 0x80000 and run them (see description in c below).
b. When debugging N22 using IDE breakpoints, configure as shown in the diagram below. The default
Core_Configurationoption iscore0, but changing it tocore1can also work normally.Note
- Do not check 'Reset_and_Hold' in the 'Startup' option. If checked, N22 will reset, and there will be no external action to open N22, causing debugging errors
- The default option for 'Program' under the 'Main' checkbox in the image below may be incorrect and needs to be fixed.

c. The D25F code is at flash 0, the N22 code at flash 0x80000, and the DSP code at flash 0x40000. A mailbox is added between the three cores, similar to how software interrupts work. D25F writes data to this address, then generates an interrupt and sends it to N22, which reads this address and receives the information. D25F is the main controller. N22 and DSP are reset by default. When D25F is ready, open N22 and DSP, and only then will N22 and DSP fetch instructions from Flash to run. Telnet debugging is the same as with the D25F, but attention must be paid to memory address allocation when reading or writing memory.

-
N22 and D25F joint debugging
Method: First, debug one core in the IDE as described above, then continue debugging the other core. In the Debug selection, select the corresponding core to operate. The operation method is the same as single-core debugging. The Kanban board can only view data from the current debug core.

DSP debugging
- Configure the environment
Note
32-bit systems were not tested; according to xtensa_debug_guide.pdf, XOCD (software to be installed below) is only compatible with 64-bit systems. Additionally, the following only covers installation on Windows; for Linux, please refer to the relevant manuals and the steps below.
Step 1: Close the antivirus software and install "Xplorer-9.0.17-windows-installer.exe" as administrator. The installation path should be the default setting; opening it for the first time will cause errors (if no error occurs, you must follow the solution). The solution is: right-click on Properties in the software shortcut, add --xxtrace (with a space before --) in the target (T) input box, select "Install Software Keys" in the license detection popup, and enter 27001@192.168.51.135. Then close the software and remove --xxtrace, after which the IDE can work normally.
Step 2: Install "xt-ocd-14.07-windows64-installer.exe" according to Section 7.3 of "xtensa_debug_guide.pdf". Both the documentation and installation package are located in the software installation directory in Step 1.
Step 3: Install J-Link probe support according to chapter 7.3.2 of "xtensa_debug_guide.pdf". Here, you will install version V7.56 downloaded from the official segger website.
Step 4: Modify the file topology.xml in the XOCD installation path from Step 2. The usbser value is the SN number of the JLink, which can be viewed through in the SEGGER J-Link Configuration software (JLinkConfig.exe at the JLink installation path). When modifying and saving files, make sure the file encoding remains unchanged. It's best to use software like "XML Editor" to modify it.

- Import and compile the project
Step 1: Open the IDE and create a new workspace. Copy the boot_1028.xws from the \PCDB\prj\Onca\tst\dsp folder into the workspace.
Step 2: Delete the HelloWorld project. In the project navigation panel, right-click and select "Import" -> "Import Xtensa Xplorer Workspace", choose boot_1028.xws, Click Next, and select everything at every step (if the option is marked in red, it means it was previously selected. You don't need to select it now, but continue next).




Step 3: You can create a new project as prompted. Here, it demonstrates directly importing an existing project. Here, copy the dsp_led folder from \PCDB\prj\Onca\fw\boot\dsp_rw to the workspace, right-click "Import" in the project navigation panel, select "Import" -> "Existing Projects into Workspace", then select the dsp_led folder to import the project.

Step 4: Configure as follows, click the compile button, and after successful compilation, "Build successful!!" will be printed on the console.


Step 5: Follow the prompts to find the object file dsp_led and copy dsp_led to the C:\usr\xtensa\XtDevTools\install\tools\RI-2021.7-win32\XtensaTools\bin directory.

Step 6: Add XTENSA_CORE to the Windows environment variable with a value of hifi5_v2. Open cmd at C:\usr\xtensa\XtDevTools\install\tools\RI-2021.7-win32\XtensaTools\bin and execute:
xt-objcopy.exe –O binary –S dsp_led dsp_led.bin
xt-objdump.exe -D dsp_led > assemble.txt
Solution 2: Right-click the project and click "Open Command Shell", then refer to the readme.txt under \PCDB\prj\Onca\tst\dsp.
Step 7: If no link file is specified during compilation, you need to add a link file (default is a SIM file, which needs to be modified).
Open the IDE, create a new workspace, and copy boot_1028.xws from the \PCDB\prj\Onca\tst\dsp folder into the workspace (if you ran it before, you don't need to do it again). This step is to facilitate the provision of the link files needed for future projects. If you have special requirements for optimization options or other options, you can configure them in this interface.



- Other preparations before commissioning
Step 1: Download the bin file generated above to the Flash address 0x40000.
Step 2: Set PB4 - PB7 to JTAG multiplexing and use D25F to enable the DSP core. Here, this is done by opening wpcdb, executing the d25f_enable_dsp script, and then resetting the chip. By observing experimental phenomena, one can determine whether the program is executed correctly.
After JTAG RST, continue debugging the function
./ICEman -Z v5 -H
After executing this command, the program resets the digital registers but not the analog registers. You can see the program remains at the entry point, while the JTAG remains connected.

View and modify registers and memory through tabs
Expressions tab
In the Expressions tab, you can view variables and variable expressions, and directly modify the corresponding value in the Value section.

Memory tab
View/modify memory data in the Memory tab.

Click the magnifying glass icon in the upper right corner, and the Monitor Memory dialog box will pop up as follows:

Registers tab
View/modify CPU register data in the Registers tab.




GDB common commands usage
Enter the corresponding command in the red box shown below to perform read/write operations on registers/memory.

An example of the verification process is as follows:
x/1w 0x20000e40
0x20000e40 <main+452>: 0x00f92223
x/1w 0x80140204
0x80140204: 0x0e0fffff
set *(unsigned int*) 0x80140204=0x0e0f55aa
x/1w 0x80140204
0x80140204: 0x0e0f55aa

Read the command
| Command | Description |
|---|---|
x/1w 0x80170000 |
Read a Word data from the 0x80170000 |
x/1h 0x80170000 |
Read half-word data from the 0x80170000 |
x/1b 0x80170000 |
Read a byte's data from the 0x80170000 |
Among them:
x/: indicates read1: Read the quantityw: Unit (word)
Write instructions
| Command | Description |
|---|---|
set *(unsigned int*) 0x80140420=0x12345678 |
Write a word 0x80140420 address with a value of 0x12345678 |
set *(unsigned short*) 0x80140420=0x12345678 |
Write half word 0x80140420 address, with a value of 0x5678 |
set *(unsigned char*) 0x80140420=0x12345678 |
Write 1 byte 0x80140420 address with a value of 0x78 |
Among them:
set: represents writingunsigned char: writes to the units0x80140420: Write the address0x12345678: Write the value
Note
Some test instructions do not support this, such as 'tui enable'.
Use JTAG on VS Code
For detailed instructions on debugging using JTAG in VS Code, please refer to the official Telink documentation:
Telink VS Code Extension User Guide
Debugging with Segger
Segger J-Link is a mainstream debugger that supports JTAG and SWD interfaces and is suitable for program downloads, online debugging, and Flash programming. This chapter introduces the basic usage of the Segger J-Link tool on Telink chips. For users, this is equivalent to having two sets of host computers and debuggers. Telink JTAG Black Box Debugger and Segger Debugger can both be used, with the same functions.
J-Link driver installation
Download the J-Link driver installation package from the Segger official website: https://www.segger.com/downloads/jlink/
After installation, connect the J-Link debugger to your PC via USB, and the device manager will recognize it.
Used by J-Link Commander
J-Link Commander is a command-line tool built into J-Link that can be used to perform operations such as connect target chips, read and write memory and registers.
Open J-Link Commander and enter the following command to connect to the target chip:
connect
Select the target device (RISC-V core, selected according to the actual chip model) as prompted, choose the interface (JTAG or SWD), and select the speed (4000kHz or auto is recommended).
After a successful connection, you can use the following commonly used commands:
| Command | Description |
|---|---|
r |
Reset the target chip |
g |
Begin the execution procedure |
h |
Stop the execution of the procedure |
mem32 <addr> <count> |
Read 32-bit memory |
w4 <addr> <value> |
Write 32-bit memory |
reg |
View all registers |
reg <regname> <value> |
Write to the specified register |
loadfile <filepath> <addr> |
Download the file to the specified address |
erase |
Erase Flash |
exit |
Exit J-Link Commander |
J-Flash Usage
J-Flash is a flash programming tool from Segger that supports programming of target chips via J-Link.
Step 1: Open J-Flash, create a new project, or open an existing project.

Step 2: Configure the target chip model, interface type (JTAG/SWD), and connection speed.





Step 3: Open the HEX or BIN file you want to burn.
Step 4: Click the "Program Device" button to burn, or use the shortcut key F7.
J-Link GDB Server Usage
J-Link GDB Server can use J-Link as a GDB debugging server, cooperating with GDB clients for online debugging. The following example uses multi-core chips.
- Install the JLINK package
Before debugging with JLINK, you need to download the JLINK suite from the Segger official website, including the JLINK driver and host program. After testing, it is recommended to install the more stable version V7.96f:
https://www.segger.cn/downloads/jlink/JLink_Windows_V796f_x86_64.exe
- Start the JLinkGDBServerCL.exe in the command line
Find the installed JLink folder, enter cmd in the file explorer's address bar, and press Enter:

In the opened CMD window, enter the command to start JLinkGDBServerCL.exe. To debug the D25F core, enter the following command:
JLinkGDBServerCL.exe -select USB -device rv32 -endian little -if JTAG -speed 1000 -noir -noreset -nogui -LocalhostOnly -nologtofile -port 2331
To debug the N22 core, enter the following command:
JLinkGDBServerCL.exe -select USB -device rv32 -endian little -if JTAG -speed 1000 -noir -noreset -nogui -LocalhostOnly -nologtofile -port 2331 -jtagconf 5,1
-port can be customized. If the connection succeeds, the following log will appear:

Note that the number GDB Server Listening port may differ from the -port you specified. The final GDB Server Listening port is based on the log.
- Configure JLINK debugging in IoT Studio
Step 1: Create a new C/C++ Remote Application
After compiling and downloading the program, as shown in the figure, click to select the program you want to debug (ELF file):
you want to debug
Then open Debug Configurations, double-click C/C++ Remote Application, and you'll see that the newly created Debug Configuration can automatically fill in the previously selected ELF file.
Step 2: Configure the Main tab
Click the Main tab to configure the following settings:

Step 3: Configure the Debugger option
Click the Main tab in the Debugger tab to make the following changes:

- Uncheck
Stop on startup at: - GDB Debugger: Change to
riscv32-elf-gdb - GDB command file: If there is no need to customize the gdb init command, leave it empty
Click the Connection tab and fill in the port number of the GDB Server Listening port you previously obtained:

After completing the configuration, you can start debugging.
Note
Before using the Segger J-Link tool, please ensure that the JTAG pins are properly initialized (refer to the section Software and IDE Configuration for JTAG pin initialization) and that the hardware connection is normal.
FAQ
This section summarizes high-frequency issues and solutions during JTAG debugging. If you encounter any issues, you can look them up here.
Hardware connection
Q: What should I do if the JTAG debugging tool shows a red light?
A: Possible causes and troubleshooting steps:
-
Power supply abnormal — Check whether the board is powered on and whether the VBAT/VBUS voltage is normal. It is recommended to connect the blank chip first.
-
REF level not connected — Make sure the board's 3.3V output is connected to the REF pin of the JTAG box.
-
Signal line misconnection — Check each TDI, TCK, TMS, TDO to ensure they correspond to the board's schematic.
-
Driver not installed — Run
BDT/ice/libusb-AICE-driver/Install_driver.exeto install the driver.
Q: Wiring is normal (blue light on), but ICEman cannot recognize the chip?
A:
-
Check whether
the code calls jtag_set_pin_en()(four-wire) orsdp_set_pin_en()(two-wire). For details, please refer to the section software and IDE configuration. -
Find the schematic of the board and find the corresponding pins for JTAG_CTR. Generally, when connected to GND, this pin operates in four-wire mode; when connected to 3v3, it operates in two-wire mode.
-
Confirm that the two-wire/four-wire mode switch settings match the ICEman startup parameters.
-
After repowering on and resetting the chip, restart ICEman.
Connection and Communications
Q: Did ICEman disconnect quickly after connecting?
A: Commonly found in chips with a 32k_watchdog periodic reset (such as TL322x, TL323x), the reset clears the command ICEman uses to write RAM. You need to refer to the method in section Special connection instructions and launch ICEman with the latest version of IoT Studio to disable the 32k_watchdog.
Q: After starting ICEman, does it prompt that the port is already in use?
A: This indicates that the previous ICEman process did not exit properly. Run ./ICEman -Z v5 -k in the terminal to clear residual processes, then restart.
Breakpoints and debugging
Q: Breakpoints are set, but the program won't stop?
A:
-
Before checking breakpoints in IDE Debug mode, check for checkmarks; if not, breakpoints are not active.
-
The number of hardware breakpoints supported is limited. Use the
info brcommand to confirm whether the current breakpoint count has been reached. Additionally, Step Into/Step Over occupies a single breakpoint. For details, please refer to the section Breakpoints.
Q: Is there a CANNOT ACCESS MEMORY AT ADDRESS XX error during debugging?
A: This is usually caused by the number of breakpoints exceeding the limit. Enter info br in the GDB console to check the current number of breakpoints, delete any redundant ones, and then retry.
Q: Can't enter the Step Into function?
A: If the function is located in a prepackaged library (no source code), Step Into will fail. At this point, it is recommended to use Step Over to skip or break points in debuggable code. For details, please refer to the section Single-step debugging.
Source file and path
Q: Does it prompt me that the source file cannot be found during debugging?
A: This is because the compilation path in IoT Studio is inconsistent with the local path. You can solve this by adding path maps via Edit Source Path. The simplest way is to map /cygdrive/c/ to C:\. For details, please refer to the section Link to source file.
Multi-core debugging type
Q: Does N22 fail or cannot connect during nuclear debugging?
A:
-
Make sure the D25F core is running normally, and N22 is enabled. For details, please refer to the section on differences between N22 and D25F in the section on N22 debugging.
-
Do not check the
Reset_and_Holdoption in the N22 Debug configuration; otherwise, the N22 may not recover automatically after reset.
Q: How do I switch between different core data during multi-core debugging?
A: Switch the corresponding core in the Debug box. The IDE's variable/register/memory window will display only the data from the currently selected core. For details, please refer to the N22 and D25F joint debugging section on N22 debugging.
GDB command
Q: How can I quickly view the values of variables or memory in GDB?
A:
- You can view and modify variable names directly by adding them in the Expressions tab. For details, please refer to the section Expressions tab.
- Use the GDB command: Read memory
x/1w 0x80170000, write memoryset *(unsigned int*) 0x80140420=0x12345678. For details, please refer to the section GBD common commands usage.
LPC
Overview
A Low Power Compare (LPC) compares the input voltage × scaling factor with the reference voltage and outputs the comparison result. It can also be used as a signal to wake the system from low-power mode.
LPC has two operating modes:
- Normal mode: The internal reference is from Bandgap (BG), offering high accuracy and high power consumption, used for normal chip power supply scenarios.
- Low power mode: The internal reference is from UVLO, with low precision and low power consumption, used for chip sleep scenarios.
Chip support:
| Chips | Hardware LPC | GPIO supports detection | GPIO supporting external reference voltage | VBAT testing |
|---|---|---|---|---|
| TLSR825x/TLSR8359 | Yes | PB1~PB7 | PB0 / PB3 | — |
| TLSR827x/TLSR8355 | Yes | PB1~PB7 | PB0 / PB3 | — |
| TLSR921x/TLSR951x | Yes | PB1~PB7 | PB0 / PB3 | — |
| TLSR922x/TLSR952x | Yes | PB1~PB7 | PB0 / PB3 | — |
| TL321x | Yes (see note) | PB1~PB7 | PB0 / PB3 | — |
| TL322x | Yes (see note) | PB1~PB7 | PB0 / PB3 | — |
| TL323x | Yes | PB5~PB7, PC0~PC3 | PB4 / PB7 | Support |
| TL721x | Yes | PB1~PB7 | PB0 / PB3 | — |
| TL751x | Yes | PG1~PG6, PF6 | PG0 / PG3 | — |
| Other chips | None | — | — | — |
Note
The LPC of TL321x/TL322x can be used for Flash power-on and power-down protection. For details, please refer to the Flash section. Once this feature is enabled, LPC cannot be used for other purposes.
The specific enumeration values for input channels and reference voltages vary by chip. Please refer to the corresponding definitions in lpc.h lpc_input_channel_e and lpc_reference_e. The nominal reference voltage values for the TC and TL series differ slightly.
Working principle
LPC uses a 32K RC clock source as the comparator clock. Comparison results:
-
If the "Input Voltage × scaling ratio" > reference voltage, the output is low (
0). -
If the "input voltage × scaling ratio" < reference voltage, the output is high (
1). -
If the two are equal, or if the input channel is chosen as float, the output is uncertain.
GPIO voltage detection
LPC compares the "input voltage × scaling ratio" with the reference voltage by configuring the input channel, reference voltage, and scaling ratio, and the result is read by lpc_get_result(). Suitable for detecting the voltage on an external GPIO pin.
- Scaling ratio: 25% / 50% / 75% / 100% (consistent for all chips), used to expand detection range.
- Reference voltage: Normal mode comes from BG or external pins; Low power mode comes from UVLO (low precision, low power consumption, used for sleep scenarios).
- Input channels: See the chip support table above; each chip supports different pins.
Threshold voltage = reference voltage / scaling ratio. When the input voltage is below the threshold, lpc_get_result() returns 1; if it is above the threshold, it returns 0. For example, the reference voltage is 872 mV, the ratio is 50%, and the threshold is 872 / 0.5 = 1.744V.
Use examples
TL Series / TLSR921x / TLSR951x / TLSR922x / TLSR952x:
lpc_gpio_vol_detect_init(LPC_NORMAL, LPC_INPUT_CHN, LPC_REF_872MV, LPC_SCALING_PER50);
lpc_power_on(); // must power on last
delay_us(64); // Wait for two 32K sampling cycles
// main_loop: lpc_get_result();
TC Series (TLSR825x/TLSR8359 / TLSR827x/TLSR8355):
lpc_set_input_chn(LPC_INPUT_PB2);
lpc_set_input_ref(LPC_LOWPOWER, LPC_LOWPOWER_REF_810MV);
lpc_set_scaling_coeff(LPC_SCALING_PER50);
lpc_power_on(); // must power on last
sleep_us(64); // Wait for two 32K sampling cycles
// main_loop: lpc_get_result();
- For specific parameter enumerations for each chip (channel, reference voltage, threshold, etc.), please refer to the corresponding
lpc.hfile. - The LPC sampling clock is a 32K RC, and after powering on, it takes about 2 sampling cycles (
delay_us(64)) to read the result. lpc_power_on()must be called last, after the channel/reference/scaling configuration is complete.
VBAT low voltage detection
The VBAT low-voltage detection function allows monitoring of the chip power supply voltage.
Through the lpc_vbat_vol_detect_init(thres_vol) configuration, it directly detects whether the VBAT voltage is below the set drop threshold (recovery must be above the rise threshold), and the result is read via lpc_get_result().
Note
- Must call 'lpc_vbat_vol_detect_init()' for one-click initialization; do not call other LPC configuration interfaces.
- VBAT detection is based on BG reference voltage and cannot be used in sleep mode.
- GPIO detection and VBAT detection share a comparator; 'lpc_gpio_vol_detect_init()' will automatically disable VBAT detection, and the two cannot be used simultaneously.
Use examples
lpc_vbat_vol_detect_init(LPC_VBAT_FALLING_2P20V_RISING_2P30V);
lpc_power_on(); // must power on last
delay_us(64); // Wait for two 32K sampling cycles
// main_loop: lpc_get_result();
PEM (Peripheral Event Matrix)
Overview of PEM
Chips supported: The PEM module support TL series chips only (including TL321x / TL322x / TL323x / TL721x). The TC series chips and B91/B92 (TLSR951x/TLSR921x/TLSR952x/TLSR922x) chips are not supported.
PEM (Peripheral Event Matrix) is a hardware module that interconnects various peripherals. It routes the event signal from any peripheral A to the task input of any peripheral B, where peripheral B treats the task signal as an enable or trigger signal.
Note
- The PEM channel configuration in the diagram below is for illustration only, demonstrating that PEM channels can be configured for one-to-one (e.g., Ch0: TIMER->ADC), one-to-many (e.g., Ch0+Ch1: TIMER->ADC & DMA), many-to-one (e.g., Ch1+Ch4: TIMER & PWM->DMA), and many-to-many routing.

Core Concepts
| Concept | Description |
|---|---|
| Event | An interrupt-like signal sent out by a peripheral. |
| Task | A signal received by a peripheral, which can be connected to any event signal. The task signal is treated as an enable or trigger signal. |
| Channel | Each PEM channel independently maps one event to one task. The channels vary in different chips, refer the definition pem_chn_e in pem.h. |
Compared to Traditional Interrupt
Taking "the timer triggers an ADC sample once every second" as an example, the differences between the two implementation methods are as follows:
| No PEM | Use PEM | |
|---|---|---|
| Workflow | Timer interrupt -> CPU enters ISR -> Manual call to start ADC -> Exit ISR | Timer event -> PEM channel -> ADC task; the entire process is handled automatically by hardware |
| CPU Involvement | Requires the CPU to enter and exit the ISR on each trigger | No CPU involvement required |
| Latency | Interrupt response + context switch | Pure hardware latency |
| Risk of Nested Interrupts | ISRs may be interrupted by higher-priority interrupts | Not affected by interrupt priority |
Note
- PEM can only route trigger/enable signals. Actual data transfer between peripherals still requires DMA or MCU to write/read data.
Multi-Channel Routing
- One-to-many: One event can be routed to multiple tasks through multiple channels.
- Many-to-one: Multiple events can be routed to the same task through multiple channels. PEM performs an "OR" logic on events without arbitration. The peripheral itself needs to handle the case when multiple events arrive simultaneously.
- Many-to-many: Achieved by using multiple channels.
Usage Flow
To use PEM, first complete the initialization of the peripheral itself and then configure the PEM channel to connect the two. The complete process is as follows:
1. pem_init() — enable the PEM module
2. Initialize the event source peripheral — Configure the operating modes and interrupts for peripheral devices (such as setting the Stimer period and enabling interrupts)
3. <peripheral>_set_pem_event() — Connect a specific event signal from a peripheral to a PEM channel
4. Initialize the task target peripheral — Configure the operating mode of peripheral devices (such as ADC clock, channels, and sampling parameters)
5. <peripheral>_set_pem_task() — Connect the PEM channel to a task signal on the peripheral
6. pem_chn_en() — Enable the channel; the router takes effect
7. Start the event source — Start automatically triggering the task
Example: Stimer Interrupt Triggers a Single ADC Sample
/* 1. Enable PEM */
pem_init();
/* 2. Initialize the event source peripheral: Configure the timer to generate an interrupt once every second */
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1S);
stimer_set_irq_mask(FLD_SYSTEM_IRQ_MASK);
plic_interrupt_enable(IRQ_SYSTIMER);
core_interrupt_enable();
/* 3. stimer_set_pem_event() — Connect the STIMER's timer interrupt event to the PEM channel */
stimer_set_pem_event(PEM0, STIMER_EVENT_TRIG_POS);
/* 4. Initialize the target peripheral for the task: Configure ADC parameters */
adc_init(ADC0, NDMA_M_CHN); // ADC initialization(NDMA single channel)
adc_set_clk(ADC0, ADC_CLK_4M); // Clock configuration
adc_set_sample_chn(ADC0, ADC_CHN0); // sampling channel selection
/* 5. adc_set_pem_task() — Connect the ADC's single-sample task to the same PEM channel */
adc_set_pem_task(ADC0, PEM0, ADC_TASK_SINGLE_ADC_TRIG);
/* 6. Enable PEM channel */
pem_chn_en(PEM0);
// After that, the Stimer generates an interrupt once per second, automatically triggering the ADC to perform a sample via the PEM.
The specific API parameters and initialization methods for peripherals vary by chip; please refer to the demo project and header files for the corresponding chip.
Overview of API
Core API
| Function | Description |
|---|---|
pem_init() |
Enable the PEM module (reset + clock on) |
pem_chn_en(chn) |
Enable the specified channel; event-to-task routing begins. |
pem_chn_dis(chn) |
Disable specified channels |
Peripheral PEM Interface
Every PEM-compatible peripheral provides two interfaces, named according to a uniform convention:
- event configuration:
<peripheral>_set_pem_event() - task configuration:
<peripheral>_set_pem_task()
For example, the STIMER provides stimer_set_pem_event() / stimer_set_pem_task(),the ADC provides adc_set_pem_event() / adc_set_pem_task(), and so on.
How to determine whether a peripheral supports PEM? Check the enumerations pem_event_module_sel_e and pem_task_module_sel_e. You can search for pem_event or pem_task in the corresponding peripheral header file to view the relevant function declarations. For details on which event/task signals and function parameters are provided, please refer to the enumeration definitions in the corresponding peripheral header file.