Telink 驱动 SDK
Platform SDK 概述
SDK 简介
Telink 提供两套 Platform SDK,分别对应不同的芯片架构:
- tc_platform_sdk:面向 Telink 自研 TC32 内核的芯片系列
- tl_platform_sdk:面向 RISC-V 架构的芯片系列。
两套 SDK 均提供完整的驱动程序、示例代码和开发工具,帮助开发者快速构建基于 Telink 芯片的嵌入式应用。
根据内核架构,芯片分为 TC 系列和 TL 系列。下表汇总了所有支持芯片的关键特性分类:
| 系列 | SDK | 芯片代号 | 核数 | 内核 | RAM | Cache | Mailbox 通道 |
|---|---|---|---|---|---|---|---|
| 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 (带 FPU) | IRAM+DRAM | I-Cache + D-Cache, Cache independent space |
— |
| B92 | 1 | RISC-V D25F (带 FPU) | — | ||||
| TL321x | 1 | RISC-V D25F (无 FPU) | — | ||||
| TL322x | 2 | RISC-V D25F (带 FPU) + N22 (无 FPU) | D25F ↔ N22 | ||||
| TL323x | 1 | RISC-V D25F (带 FPU) | — | ||||
| TL721x | 1 | RISC-V D25F (带 FPU) | — | ||||
| TL751x | 3 | RISC-V D25F (带 FPU) + N22 (无 FPU) + DSP | D25F ↔ N22、D25F ↔ DSP、N22 ↔ DSP |
注意
- B91 包含 TLSR951x 与 TLSR921x,B92 包含 TLSR952x 与 TLSR922x,B80/B80B 包含 TLSR8208 与 TLSR8373,B85 包含 TLSR825x 与 TLSR8359,B87 包含 TLSR827x 与 TLSR8355。
- 多核芯片通过 Mailbox 进行核间通信,使用方法详见 Mailbox。
SDK 目录结构
tc_platform_sdk
tc_platform_sdk/
├── chip/ # 芯片相关代码
│ └── <chip>/ # 如 B80, B85, B87, TC321x
│ ├── boot/ # 启动文件
│ └── drivers/ # 驱动代码(含源码与 lib/ 预编译静态库)
├── common/ # 通用代码(基础类型、字符串等)
├── demo/vendor/ # 示例代码
└── project/ # Telink IoT Studio 工程文件
└── tlsr_tc32/
└── <chip>/ # 芯片工程,含 .project/.cproject 与 boot.link
tl_platform_sdk
tl_platform_sdk/
├── chip/ # 芯片相关代码
│ └── <chip>/ # 如 B91, B92, TL321x
│ ├── boot/ # 启动文件
│ ├── drivers/ # 驱动代码(含源码与 lib/ 预编译静态库)
│ └── link/ # 链接脚本
├── common/ # 跨芯片通用代码
├── demo/vendor/ # 示例代码
├── project/ # Telink IoT Studio 工程文件
│ └── tlsr_riscv/
│ └── <chip>/ # 芯片工程,含 .project/.cproject
└── 3rd-party/ # 第三方库
目录功能说明
boot/:启动文件,负责上电初始化:设置中断向量表、初始化栈指针、搬运数据段、跳转到 main。TC 和 TL 的启动文件不同,详见 Software Startupdrivers/:驱动代码,每个外设模块包含.h和.c源文件;部分模块(如 RF、PM)以lib/下预编译静态库(.a)形式提供,源码不公开link/(TL):链接脚本,定义程序内存布局,位于chip/<chip>/link/。TC 的链接脚本(boot.link)位于project/tlsr_tc32/<chip>/。详见 Software Startupproject/:Telink IoT Studio 工程文件目录(Eclipse 工程.project/.cproject)。TC 位于project/tlsr_tc32/<chip>/,TL 位于project/tlsr_riscv/<chip>/demo/vendor/:示例代码和参考实现common/:跨芯片共用的代码(如types.h、string.h、sdk_version.h等)3rd-party/:第三方开源组件(TL SDK 特有)reg_include/(TL):寄存器定义文件(TL SDK 特有),包含各外设的寄存器地址映射和位域定义。TC SDK 的寄存器定义直接写在register.h等头文件中
SDK 版本号
两套 SDK 在编译生成的固件(bin 文件)末尾内嵌一份版本号信息,便于烧录后通过工具或脚本识别当前固件使用的 SDK 版本。
版本号格式
版本号以带 $$$ 定界符的字符串形式存储,格式为:
$$$<sdk_name>_<version>$$$
- TC 系列示例:
$$$tc_platform_sdk_V3.4.0$$$ - TL 系列示例:
$$$tl_platform_sdk_V3.9.0$$$
实现原理
版本号机制由三部分协同实现,两套 SDK 实现方式一致:
- 版本宏定义(
common/sdk_version.h):每个芯片型号对应一个独立的版本宏(如B91_SDK_VERSION_NUM),由SDK_VERSION_NUM统一引用当前编译目标的版本。 - 版本数组存储(
common/sdk_version.c):通过SDK_VERSION/SDK_VERSION1两层宏配合 C 预处理#字符串化运算符,将版本宏拼接为完整的带定界符字符串,存入sdk_version[]数组,并利用__attribute__((section(".sdk_version")))放入名为.sdk_version的独立段:
volatile __attribute__((section(".sdk_version"))) unsigned char sdk_version[] = {SDK_VERSION(SDK_VERSION_NUM)};
- 链接脚本段放置:链接脚本通过
KEEP(*(.sdk_version))保留该段,并将其安排在 bin 文件末尾位置,段大小计入BIN_SIZE。TL SDK 位于chip/<chip>/link/*.link,TC SDK 位于project/tlsr_tc32/<chip>/boot.link。
版本号识别
由于版本信息固定出现在 bin 文件末尾且被 $$$ 定界符包裹,可通过脚本直接从二进制文件中提取。SDK 提供 tl_check_fw.sh 脚本 (../../tc_platform_sdk/tools/tl_check_fw_tool/tl_check_fw.sh) ,使用正则匹配 $$$...$$$ 模式提取并输出版本号。若编译出的 bin 文件末尾未找到版本信息,需检查 sdk_version.c 是否被正确编译并链接。
Boot ROM
Boot ROM 概述
Boot ROM 是固化在 SoC 内部的只读启动代码,是设备上电复位后 CPU 执行的第一段程序。它是整个系统启动链的起点,负责完成最基本的硬件初始化和下一阶段引导程序的加载。
Telink 芯片的启动过程分为两个阶段:
| 阶段 | 名称 | 说明 | 可修改 |
|---|---|---|---|
| 第一阶段 | Boot ROM | 固化在芯片内部的硬件启动逻辑,出厂即固化 | 否 |
| 第二阶段 | Software Startup | SDK 提供的启动文件(cstartup.S + link 文件) | 是 |
Boot ROM 作为第一阶段,在芯片上电、IO 复位或从 Deep Sleep (不包括Deep retention)唤醒后首先执行,完成必要的初始化后将控制权移交给第二阶段的 Software Startup。
Boot ROM 特点
Boot ROM 具有以下几个核心特点:
- 只读不可修改 — 固化在芯片内部 ROM 中,出厂后无法被软件擦写或修改,保证了启动代码的完整性和安全性。
- 上电首先执行 — 是 CPU 复位后执行的第一段代码。硬件复位向量固定指向 Boot ROM 的入口地址,CPU 上电后必须先执行 Boot ROM
- 芯片级安全信任根(Root of Trust) — 由于不可修改,Boot ROM 是安全启动(Secure Boot)的信任链起点。在 Secure Boot 模式下,Boot ROM 负责验证下一阶段固件镜像的签名,只有验签通过才会引导执行。
ROM 功能模式
Boot Mode
ROM 支持两种 Boot Mode:Normal Boot(常规启动)和 Secure Boot(安全启动)。Normal Boot 是 mandatory,所有芯片都具备;Secure Boot 是 optional,部分芯片具备,通过 eFuse/OTP 配置开启。一旦使能,出于安全考虑无法切回 Normal Boot。
Normal Boot(常规启动)
Normal Boot 是默认的启动模式,执行常规的固件程序启动流程,时间短,适用于一般应用场景
Secure Boot(安全启动)
Secure Boot 增加了安全相关的功能(通过eFuse或otp配置成Secure Boot模式):
- Boot ROM 在引导固件之前,会对固件程序进行签名验证
- 只有签名验证通过的固件才会被引导执行
- 签名验证失败时,芯片将拒绝启动该固件,防止未授权或篡改的代码运行 Secure Boot 为设备提供了从硬件信任根开始的全链路安全保护,适用于对安全性有较高要求的应用场景。
Firmware Load Mode
ROM 支持两种 Firmware Load Mode:Direct XIP 和 Load to RAM。这二者是互斥的,任何一款芯片只能是其中一种模式。
Direct XIP
没有从NVM (Non-Volatile Memory)搬运到sram的过程,通过xip 直接执行NVM code,本文以flash为例。
Load to RAM
从NVM对应地址搬ramcode代码到sram,搬完代码后会跳转到sram 0地址运行
芯片支持
不同芯片系列在启动模式和固件加载模式上的支持情况有所不同,详见 芯片支持。
Normal Boot 多地址启动
多地址启动原理
多地址启动(Multi-address Boot)功能允许同一个固件(firmware)在不修改 link 文件的情况下,运行在不同的 Flash 地址上。其核心原理是通过硬件地址映射机制,将 CPU 发出的访问地址重映射到实际的 Flash 物理地址。
地址映射公式如下:flash_addr = cpu_addr + (cpu_addr >= size ? 0 : offset)
其中:
| 参数 | 说明 |
|---|---|
cpu_addr |
CPU 送过来的访问地址 |
offset |
多地址启动偏移量(由寄存器配置) |
size |
多地址启动映射窗口大小(由寄存器配置) |
flash_addr |
加上 offset 之后实际送给 Flash 的地址 |
映射逻辑解读:
- 当
cpu_addr >= size时,flash_addr = cpu_addr(不做偏移,直接透传) - 当
cpu_addr < size时,flash_addr = cpu_addr + offset(在窗口范围内做地址偏移)
这样,CPU 始终从地址 0 开始取指执行,而硬件自动将地址 0 开始的 size 范围内的访问映射到 Flash 的 offset 位置,实现了同一固件在不同 Flash 地址运行的能力。
相关寄存器
多地址启动功能通过以下两个寄存器进行配置:
| 寄存器 | 说明 |
|---|---|
mspi_xip_core_offset |
配置多地址启动的偏移量(offset),即固件在 Flash 中的实际起始地址 |
mspi_xip_core_size |
配置多地址启动的映射窗口大小(size),即需要进行地址映射的地址范围 |
这两个寄存器在 Boot ROM 扫描到有效的 Telink 标记后被配置,然后 CPU 跳转到 Flash 0 地址执行时,硬件自动完成地址映射。
示例:启动地址为 256K(offset = 0x40000,size = 0x40000)
CPU 地址空间 Flash 物理地址空间
+---------------------+ +---------------------+
| | | 0x00000 |
| >= size area | ------> | (0 KB) | <- 直接透传
| (pass-through) | | ... |
| | | |
+---------------------+ +---------------------+
| | | 0x40000 |
| < size area | ========> | (256 KB) | <- 映射,固件实际存放位置
| (addr + offset) | | ... |
| | | 0x7FFFF |
+---------------------+ +---------------------+
CPU 从 0 取指执行
- CPU 访问地址
0 ~ 0x3FFFF(< size)→ 映射到 Flash0x40000 ~ 0x7FFFF - CPU 访问地址
>= 0x40000→ 直接透传到 Flash 对应地址 - 所以将固件烧入到Flash 0x40000 开始的地址,即从0x40000 开始的地址才是有效固件地址,可实现该地址启动
Direct XIP 启动加载流程
完整流程如下:
芯片上电 / 复位
|
v
Boot ROM 开始执行
|
v
按预设的启动地址列表依次扫描 Flash
(在 multiboot_offset[i] 的偏移处读取)
|
v
在偏移 +0x20 处读取 4 字节,检查是否为 Telink 标记
(Magic Number: 0x544c4e4b,即 ASCII "T L N K")
|
+--- 未找到 ---> 尝试下一个启动地址
|
+--- 找到 ---> 配置 mspi_xip_core_size 和 mspi_xip_core_offset
|
v
跳转到 Flash 0 地址执行用户固件
Telink 标记(Magic Number)说明:
- 值为
0x544c4e4b,对应 ASCII 字符TLNK - 存放在 Flash 中固件镜像的固定偏移位置(启动地址 +
0x20) - Boot ROM 通过读取该标记判断对应地址是否存在有效固件
Direct XIP伪代码实现
以下是 Boot ROM 中实现多地址启动和程序加载的核心逻辑伪代码:
unsigned int telink_flag = 0;
unsigned short multiboot_offset[N] = {addr0, addr1, ...};
for (unsigned char i = 0; i < N; i++)
{
// 在启动地址偏移 +0x20 处读取 4 字节,检查 Telink 标记
flash_read_page((multiboot_offset[i] ) + 0x20, 4, (unsigned char *)&telink_flag);
if (0x544c4e4b == telink_flag)
{
// 找到有效固件,配置多地址启动寄存器
mspi_xip_core_size(multiboot_offset[i]);
mspi_xip_core_offset(multiboot_offset[i]);
// 跳转到 Flash 0 地址执行
run_flash_start_addr(0);
}
}
代码逻辑说明:
- 定义启动地址数组
multiboot_offset[],包含 N 个预设的启动地址(不同芯片 N 值不同,详见芯片支持) - 遍历每个启动地址,在
(multiboot_offset[i] + 0x20处读取 4 字节 - 判断读取的值是否等于
0x544c4e4b(Telink 标记) - 若匹配,配置
mspi_xip_core_size和mspi_xip_core_offset寄存器 - 跳转到 Flash 0 地址,将控制权交给用户固件
Load to RAM 启动加载流程
和Direct XIP 伪代码类似 , 需要将大小为ram_code_size*16 字节 从对应flash地址搬运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++)
{
// 在启动地址偏移 +0x8 处读取 4 字节,检查 Telink 标记
flash_read_page((multiboot_offset[i] ) + 0x8, 4, (unsigned char *)&telink_flag);
if (0x544c4e4b == telink_flag)
{
// 找到有效固件,配置多地址启动寄存器
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);
//在启动地址偏移 +0x0c获取处ram_code_size
unsigned short ram_code_size = telink_info;
//在启动地址偏移 +0x0e获取处cmd
unsigned char cmd = telink_info >> 16;//在启动地址偏移 +x0e
//从启动地址搬运ram code*16 字节到sram 0地址
load_flash_to_sram(ram_code_size* 16);
// reboot from sram 0 address
write_reg8(IC_CT, cmd);
}
}
代码逻辑说明:
- 定义启动地址数组
multiboot_offset[],包含 N 个预设的启动地址(不同芯片 N 值不同,详见芯片支持) - 遍历每个启动地址,在 Flash 偏移
+0x08处读取 4 字节,检查 Telink 标记0x544c4e4b - 若匹配,配置
mspi_xip_core_size和mspi_xip_core_offset寄存器 - 在 Flash 偏移
+0x0c处读取 4 字节,低 16 位为ram_code_size,高 8 位为cmd - 调用
load_flash_to_sram将ram_code_size * 16字节从 Flash 搬运到 SRAM - 写
cmd到IC_CT寄存器,触发从 SRAM 0 地址启动
芯片支持
芯片映射
各芯片系列的支持情况如下:
| 芯片系列 | Boot Mode | Firmware Load Mode | 地址组 |
|---|---|---|---|
| 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 |
地址组定义
上表中地址组对应的启动地址列表:
| 地址组 | N | 启动地址列表 |
|---|---|---|
| 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) |
注意事项
固件文件大小限制
对于 B85 / B87/ B91 系列芯片,当使用非 0 地址启动时,固件文件大小存在上限限制:
| 启动地址 | 最大 固件 文件大小 | 计算方式 |
|---|---|---|
0x20000 (128K) |
124KB | 128K - 4K(保留区域) |
0x40000 (256K) |
252KB | 256K - 4K(保留区域) |
0x80000 (512K) |
508KB | 512K - 4K(保留区域) |
- 当固件需要在
0x20000地址启动时,固件文件编译后的大小不能超过 124KB - 当固件需要在
0x40000地址启动时,固件文件编译后的大小不能超过 252KB - 当固件需要在
0x80000地址启动时,固件文件编译后的大小不能超过 508KB
如果固件大小超出对应限制,将导致启动失败或运行异常。开发时需根据目标启动地址合理规划固件体积。
Software Startup
概述
Telink 芯片的启动过程分为两个阶段:
| 阶段 | 名称 | 说明 | 可修改 |
|---|---|---|---|
| 第一阶段 | Boot ROM | 固化在芯片内部的硬件启动逻辑,出厂即固化 | 否 |
| 第二阶段 | Software Startup | SDK 提供的启动文件(cstartup.S + link 文件) | 是 |
第一阶段 Boot ROM 完成最基本的硬件初始化和固件加载,详细说明详见Boot ROM。
本章聚焦第二阶段 Software Startup,即 cstartup.S 和 Link 文件如何配合建立 C 语言运行环境。除非完全理解启动流程,否则不要修改启动文件。
不同芯片的 RAM 架构和 Firmware Load Mode 不同,具体分类参见 Platform SDK 概述 和 Boot ROM。
S/Link 文件选择与配置
cstartup.S(S 文件)负责建立 C 语言运行环境,Link 文件定义各段在 Flash/RAM 中的内存布局。两者必须配套使用。
不同 Firmware Load Mode 的 S/Link 文件组织方式不同,请先确认你所使用的芯片属于哪种模式(参见 Boot ROM 中的芯片支持矩阵),然后按对应章节选择和配置。
S 文件与 Link 文件必须配套使用,不可跨加载模式混用。Load to RAM 的 cstartup 不能配 Direct XIP 的 link 文件,反之亦然——两者的段定义、搬运逻辑、边界符号均不同,混用会导致启动失败或运行异常。
Load to RAM
Load to RAM 芯片使用一个 link 文件(boot.link),通过 S 文件中的宏选择 Flash/SRAM 模式:
| S 文件 | 宏 | 说明 |
|---|---|---|
cstartup_flash.S |
MCU_STARTUP_FLASH |
默认启动文件,通过 ALL_SRAM_CODE 宏切换 Flash/SRAM 模式 |
cstartup_otp.S |
MCU_STARTUP_OTP |
OTP 启动 |
ALL_SRAM_CODE 宏控制两种编译模式:
ALL_SRAM_CODE |
模式 | 说明 |
|---|---|---|
| 0(默认) | Flash 模式 | S 文件在 Firmware Header 中写入 .ram_code 大小,Boot ROM 只搬 .vectors/.ram_code 到 SRAM,其余代码经 I-Cache 从 Flash 取指 |
| 1 | SRAM 模式 | S 文件在 Header 中写入整个 bin 大小,Boot ROM 把全部代码搬到 SRAM,I-Cache Tag 设到 SRAM 末尾(size=0),所有取指都从 SRAM |
SRAM 模式的核心是让 Boot ROM 搬移整个 bin 而非仅 ram_code:S 文件通过 Header offset 0x08 的配置字告诉 Boot ROM 需要搬运的大小。SRAM 模式的 bin 烧录到 Flash 或直接加载到 SRAM 均可运行。
部分芯片有 cstartup_sram.S、cstartup_copy_ramcode.S 等独立 S 文件和 boot_normal.link、boot_copy_ramcode.link 等附加 link 文件,用于特定 demo 场景,标准工程统一使用 boot.link。
用户可配置宏:
| 宏 | 默认值 | 说明 |
|---|---|---|
ALL_SRAM_CODE |
0 | 0: Flash 模式,仅 ram_code 在 SRAM;1: SRAM 模式,全部代码在 SRAM。自动派生 FLL_STK_EN/ZERO_IC_TAG_EN/COPY_DATA_EN |
SRAM_SIZE |
依芯片而定 | SRAM 大小,需按芯片 datasheet 设置(如 SRAM_32K/SRAM_48K/SRAM_64K)。设错影响效率或导致无法运行 |
WD_SET_AND_START_EN |
0 | 是否在启动文件中预设并启动看门狗。建议开启,默认未开启是为了兼容已有用户代码 |
BLC_PM_DEEP_RETENTION_MODE_EN |
1 | Deep Sleep Retention 唤醒使能,关闭后不支持 retention 唤醒但减少启动代码量 |
SKIP_RF_DRV |
未启用 | 定义后跳过 rf_sw_config(RF 初始化)。该函数必须在段搬运前执行(占用堆栈较大,放 C 中执行可能溢出覆盖 SRAM),代码较大且耗时;仅 BQB/EMI/Test 等不用 RF 的测试场景用此宏跳过(节省 Flash 约 6389 字节) |
__IRQ_STK_SIZE__ |
0x180 | IRQ 模式独立栈大小。ARM 机制下 IRQ 和 SVC 各有独立 SP。不建议修改,默认值已满足需求 |
DP_THROUGH_SWIRE_DIS |
1 | 禁用 dp_through_swire,防止 DP/DM 引脚误触发 swire 时序导致芯片被误写。详见 DP_THROUGH_SWIRE_DIS 详细说明 |
Direct XIP
Direct XIP 芯片按使用场景选择不同的 S/Link 文件对,两个文件必须配套使用,不可混用:
| 场景 | S 文件 | Link 文件 | 说明 |
|---|---|---|---|
| Flash Boot(默认) | cstartup_flash.S |
flash_boot.link |
BIN 烧录到 Flash,在 Flash 中执行(XIP),大多数 SDK demo 使用此配置 |
| Performance Testing | cstartup_flash.S |
flash_boot_ramcode.link |
除 vector table 外全部代码在 RAM 中执行,避免 Flash 延迟提升性能(用于 CoreMark/Dhrystone 等),受 IRAM 大小限制 |
| RAM-Only Execution | cstartup_ram.S |
ram_boot.link |
程序只能烧录到 SRAM 并从 SRAM 执行,不能烧录到 Flash |
多核芯片的从核启动文件命名规则和写法与主核一致,分别位于各核的 boot 子目录中。D25F 侧宏与协处理核侧 S/link 文件的配套关系详见 Multi-core。
用户可配置宏:
| 宏 | 默认值 | 说明 |
|---|---|---|
SRAM_SIZE |
依芯片而定 | SRAM 大小,需按芯片 datasheet 设置。ILM/DLM 软件共享的芯片设错会导致 ILM/DLM 划分错误、程序跑飞 |
DP_THROUGH_SWIRE_DIS |
1 | 禁用 dp_through_swire,防止 DP/DM 引脚误触发 swire 时序导致芯片被误写。详见 DP_THROUGH_SWIRE_DIS 详细说明 |
RF_CERTIFICATION_CGF_EN |
未启用 | RF 认证场景使用。正常应用不要开 |
DP_THROUGH_SWIRE_DIS>> 详细说明(此宏在 Load to RAM 和 Direct XIP 中一致):
- 功能:设为
1禁用dp_through_swire,设为0使能。 - 风险:
dp_through_swire默认使能。当 DP/DM 引脚用作 GPIO 并触发dp_through_swire时序时,可能通过 swire 误写芯片导致死机,因此启动文件中默认禁用(设为1)。 - 设为
1的副作用:若芯片只有 dp 引脚没有 sws 引脚,禁用后会导致:① 治具生产时,将禁用此功能的固件烧录到未编程芯片,会因 swire 通信失败而无法再次烧录;② 用 BDT 工具烧录禁用此功能的固件,可能因 swire 通信失败导致无法调试。 - 设为
0的注意事项:若评估上述副作用不可接受,可设为0使能dp_through_swire,但需注意不要让 DP/DM 引脚触发dp_through_swire时序。 - USB 应用:
usb_set_pin(1)会重新使能dp_through_swire,usb_set_pin(0)会禁用。
Firmware Load Mode 与段位置
Firmware Load Mode 的完整概念、多地址启动、Boot ROM 扫描流程详见 Boot ROM。
Firmware Load Mode 决定上电后代码的取指路径,进而决定启动段(.vectors / .retention_reset)在 Flash/RAM 中的位置,也决定了 Firmware Header 的字段分配。这是理解后续 Link 文件段分配和 S 文件启动流程的基础。
| Firmware Load Mode | 上电 / Deep Sleep 唤醒行为 | Retention 唤醒行为 | 入口段 |
|---|---|---|---|
| Load to RAM | 硬件把代码从 Flash 搬到 RAM,从 RAM 启动 | 直接从 RAM 启动 | .vectors |
| Direct XIP | 跳转到 Flash 起始地址取指(XIP) | 从 IRAM 启动 | .vectors(上电)/ .retention_reset(retention) |
- Load to RAM:上电和 retention 都从 RAM 启动,只需一个
.vectors启动段(位于 RAM 起始)。 - Direct XIP:上电从 Flash 启动,retention 从 IRAM 启动,需两个启动段:
.vectors(Flash 起始地址)和.retention_reset(IRAM 起始地址)。
Firmware Header
Flash 中固件文件的起始区域是硬件 Boot ROM、烧录器和 OTA 工具约定使用的固定头(Firmware Header),S 文件通过 .org 在对应偏移写入字段值。该区域不向用户开放,用户不可在此处放置自定义代码。两种 Firmware Load Mode 的头分配不同:
Load to RAM(0x00 ~ 0x1f):
| 地址范围 | S 文件中的内容 | 作用 |
|---|---|---|
| 0x00~0x03 | tj __reset |
Reset 跳转指令(Boot ROM 取第一条指令) |
| 0x04~0x05 | firmwareVersion | 固件版本号 |
| 0x06~0x07 | firmware signature flag | OTA 更新时的固件签名标志 |
| 0x08~0x0b | "TLNK" Flag | Telink 固件标识(烧录器/OTA 识别用) |
| 0x0c~0x0d | ram code 大小 | ram code 搬运大小,单位 16 byte |
| 0x0e | 0x88 |
芯片 reset 指令(Boot 写入数字寄存器 0x602) |
| 0x0f | 0x00 |
Reserved |
| 0x10~0x13 | tj __irq |
中断跳转指令 |
| 0x18~0x1b | _bin_size_ |
固件大小 |
0x0c~0x0f 这 4 字节配置字会被 Boot ROM 写入数字寄存器 0x602:
- 0x0c~0x0d(ram code 大小):ram code 搬运大小,单位为 16 byte。S 文件中实际写入
_ramcode_size_div_16_align_256_(ram code 大小先 256 对齐再除以 16)。当定义了ALL_SRAM_CODE宏时,改为写入_bin_size_div_16(整个固件大小除以 16,表示全部代码放 RAM)。 - 0x0e(
0x88):芯片 reset 指令,Boot 将其写入 0x602。0x0f 为0x00占位,无具体含义。
Direct XIP(0x00 ~ 0x27):
| 地址范围 | S 文件中的内容 | 作用 |
|---|---|---|
| 0x00~0x05 | j _START |
Reset 跳转指令(Boot ROM 取第一条指令) |
| 0x06~0x07 | firmware signature flag | OTA 更新时的固件签名标志 |
| 0x18~0x1b | BIN_SIZE | 固件大小 |
| 0x20~0x23 | "TLNK" Flag | Telink 固件标识(烧录器/OTA 识别用) |
| 0x24~0x27 | Fixed Config | 硬件配置位占用 |
Link 文件与内存布局
Link 文件定义了各段(section)在 Flash/RAM 中的位置,包括哪些段放在 Flash、哪些放在 RAM、各段的大小和对齐方式。本章介绍 Link 文件中段的划分原因、内存分配和用户使用方式。
S 文件与 Link 文件的配合关系:Link 文件通过符号(如 _start_data_)定义各段的边界和地址,S 文件(cstartup.S)根据这些符号确定搬运源和目标地址。若 Link 文件缺少某个符号,S 文件中引用该符号会编译报错。
LMA 与 VMA
Link 文件通过 AT() 为每个段指定两个地址:
- LMA(Load Memory Address,加载地址):段在固件文件中的存储地址,通常位于 Flash 中。烧录器按 LMA 把段写入 Flash。
- VMA(Virtual Memory Address,运行地址):CPU 执行时访问该段使用的地址,即段运行时实际所在的 RAM/Flash 地址。
两者关系决定启动时是否需要软件搬运:
| 情况 | 含义 | 启动处理 |
|---|---|---|
| LMA = VMA | 段存储地址 = 运行地址 | 无需软件搬运,CPU 直接访问 |
| LMA ≠ VMA | 段存在 Flash,运行时需在 RAM | cstartup 启动时把段从 LMA 搬到 VMA |
例如 .data 段:初值存在 Flash(LMA),运行时 CPU 在 RAM 中访问(VMA),启动时由 cstartup 把初值从 LMA 拷贝到 VMA。.bss 段只有 VMA(NOLOAD),不占 Flash 空间,启动时直接清零。
段总览
下表列出各段在不同 Firmware Load Mode 下的用途和位置关系,便于全局把握。每个段的具体功能和用户使用方式详见段功能详解章节。
| 段名 | 用途 | Load to RAM | Direct XIP |
|---|---|---|---|
.vectors |
上电启动代码(cstartup) | RAM 起始,硬件搬移 | Flash 起始,XIP 执行 |
.retention_reset |
Retention 唤醒启动代码 | 无 | IRAM 起始,软件搬运 |
.ram_code |
常驻 RAM 的关键函数 | RAM 起始,硬件搬移 | IRAM,软件搬运 |
.retention_data |
Retention 期间保持的变量 | RAM,软件搬运 | IRAM retention 区 |
.text |
Flash 中的函数代码 | Flash,经 I-Cache 取指 | Flash,XIP |
.rodata |
只读数据(const) | Flash,经 I-Cache 读取 | Flash |
.data |
已初始化全局变量 | RAM,软件搬运 | DRAM,软件搬运 |
.bss |
未初始化全局变量 | RAM,清零 | DRAM,清零 |
.aes_data / .my_code |
硬件专用 NOLOAD 段 | .my_code |
.aes_data |
.sdk_version |
SDK 版本号字符串 | Flash 末尾 | Flash 末尾 |
内存分配图
不同 Firmware Load Mode 对应不同的 RAM 和 Cache 设计(具体分类参见 Platform SDK 概述):
- Load to RAM:Single RAM + Cache in RAM,RAM 同时承担代码运行和数据存储,I-Cache 占用 RAM 空间。
- Direct XIP:IRAM+DRAM + Cache independent space,IRAM(代码)和 DRAM(数据)物理分离,Cache 为独立硬件模块,不占用 IRAM/DRAM 空间。
图中 <FLASH_BASE>、<SRAM_BASE>、<IRAM_BASE>、<DRAM_BASE>、<SRAM_SIZE> 等为代号,实际值因芯片而异,请查阅对应芯片的 datasheet。
Load to RAM
lst 文件中代码段的 VMA 显示在指令地址空间(从 0x0 开始),而非 RAM 物理地址,详见通过 lst 文件查看段分配章节。各段功能详见段功能详解章节。
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>
要点:
.vectors和.ram_code:由硬件 Boot ROM 搬运到 RAM,CPU 取指时直接从 RAM 读取(lst 中 VMA=0x0 起始)。.text和.rodata:不占用 RAM 空间,实际数据位于 Flash,CPU 取指时经 I-Cache 从 Flash 读取。.data和.retention_data:LMA 在 Flash,VMA 在<SRAM_BASE>+,启动时由 cstartup 软件搬运。.bss:NOLOAD 段,启动时清零。
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 和 DRAM 物理独立。.data 从 Flash 搬运到 DRAM 中的 .data 段。
段功能详解
各段的用途和用户使用方式。段的 LMA/VMA 关系和两种 RAM 架构的差异已在前文介绍,此处不再重复。具体段地址和大小请查阅对应芯片的 link 文件或编译生成的 lst 文件(见 通过lst文件查看段分配)。
.vectors / .retention_reset(启动段)
汇编启动代码(cstartup_*.S)所在的段,是芯片上电后最先执行的代码。
Single RAM(只有 .vectors):
- 位于 RAM 低地址区域,由硬件 Boot ROM 从 Flash 搬运到 RAM,CPU 取指时直接从 RAM 读取。
- 上电和 retention 唤醒都从
.vectors的 VMA 处执行(RAM 起始区域未掉电)。
IRAM+DRAM(有 .vectors 和 .retention_reset):
.vectors位于 Flash 起始,上电时 Boot ROM 直接跳转取指(XIP),无需搬运。.retention_reset位于 IRAM 起始,retention 唤醒后从此处执行(IRAM 起始区域未掉电)。LMA 在 Flash,启动时按需搬运到 IRAM。
.ram_code(常驻内存代码)
需要常驻 RAM 执行的函数。存在两个原因:
- 时序冲突规避:Flash 操作相关函数涉及 MSPI 管脚时序复用,若放在 Flash 中执行会与 Flash 读取产生时序冲突导致死机,必须常驻 RAM。
- 性能与功耗:常驻 RAM 的函数每次调用无需从 Flash 读取,执行更快。SDK 将时序敏感的函数(如中断处理)放入此段以降低功耗。
用户使用方式:在函数定义时添加关键字 __attribute__((section(".ram_code"))),编译后该函数即归入 .ram_code 段。典型场景:中断处理函数、Flash 读写函数、时序敏感的 RF 函数。
注意
.ram_code占用 RAM 空间,RAM 资源有限,应只对确有需要的函数使用此关键字。
.retention_data(保持数据)
需要在 Deep Sleep Retention 期间不掉电保持的全局变量。普通全局变量位于 .data / .bss 段,在 retention 模式下会丢失;放入 .retention_data 段的变量位于 RAM retention 区域,唤醒后仍保持原值。
用户使用方式:在变量定义时添加关键字 __attribute__((section(".retention_data")))。
.retention_data 占用 RAM retention 区域,容量有限。若超限,编译期链接会报错。处理方式:减少 retention 变量。
.text / .rodata(Flash 代码与只读数据)
.text:程序中未加ram_code关键字的函数默认归入此段,位于 Flash 中。IRAM+DRAM 下通过 XIP 执行;Single RAM 下经 I-Cache 从 Flash 取指执行。通常这是固件中最大的段。.rodata:用const关键字定义的只读数据(如查找表、ATT table 等),位于 Flash 中。IRAM+DRAM 下通过 D-Cache 读取;Single RAM 下指令和数据共用同一个 Cache,.rodata同样经 I-Cache 从 Flash 读取。两种 RAM 架构下都不占用 RAM 空间。
这两个段不占用 RAM 空间,但访问需经过 Cache,需注意缓存读旧值问题:
- 缓存读旧值问题:Flash 数据被读取后会缓存在 Cache 中。若该 Flash 区域随后被擦写更新,但 Cache 中仍缓存着旧数据,此时再次用指针访问,CPU 可能直接返回 Cache 中的旧值,而非 Flash 中的新数据。
- 正确做法:需要读取会被改写的 Flash 数据时,应使用 SDK 提供的 flash 读接口(如
flash_read_page),这些接口会绕过 Cache 直接访问 Flash,保证读到最新数据。
常驻 RAM 的代码(.ram_code 段)不经过 Cache,无此问题。
.data / .bss(全局变量)
.data:已初始化(初值非 0)的全局变量。LMA 在 Flash,VMA 在 RAM(LMA ≠ VMA),启动时由 cstartup 搬运到 RAM。.bss:未初始化或初始化为 0 的全局变量。只有 VMA,NOLOAD(不占 Flash 空间),启动时由 cstartup 直接在 RAM 中清零。
IRAM+DRAM 还有 .sdata / .sbss(small data 段),分别归入 .data / .bss。Single RAM 无此区分。
.aes_data / .my_code(硬件专用 NOLOAD 段)
部分芯片在 RAM 中保留固定区域供硬件模块使用,该段为 NOLOAD(不占用 Flash,启动时清零或保持随机值):
- IRAM+DRAM:
.aes_data,硬件 AES 模块缓存区,固定 32 字节,位于 IRAM 前 64KB 内,位置固定不可更改。 - Single RAM:
.my_code,用于 SPI slave buffer 等特殊用途。
这是芯片硬件设计决定的专用段,用户一般无需操作。
.sdk_version(版本号段)
存储 SDK 版本号字符串,位于 Flash 末尾,以特殊字符 $$$ 起始和结束,便于在固件文件中识别。详见 Platform SDK 概述。
S 文件与启动流程
S 文件(cstartup.S)是芯片上电后执行的第二段代码,它根据 Link 文件定义的段边界符号,完成段搬运、BSS 清零、Cache 初始化等操作,最终跳转到 main()。
S 文件的行为由 Firmware Load Mode 决定,两种模式的入口段和启动逻辑不同:
- Load to RAM:上电和 retention 共用一个
.vectors入口,S 文件通过ana_reg_0x7e寄存器值判断唤醒类型。 - Direct XIP:上电从
.vectors的_RESET_ENTRY入口进入,retention 从.retention_reset的_IRESET_ENTRY入口进入,两个入口完全独立。
Firmware Load Mode 的完整概念详见 Boot ROM。Firmware Header 的字段布局详见 Firmware Header。
Load to RAM
Load to RAM 各芯片的启动流程大体一致,本节只描述通用流程。流程图中涉及的宏配置详见 Load to RAM。
Load to RAM 的核心特点是上电、Deep Sleep 唤醒和 Retention 唤醒共用同一入口(__reset),在流程中通过 ana_reg_0x7e 寄存器值判断唤醒类型后选择不同路径:
ana_reg_0x7e |
唤醒类型 | 路径差异 |
|---|---|---|
0x00 |
Retention 唤醒 | 执行多地址寄存器恢复(恢复 g_pm_multi_addr 等 Flash 读命令配置);跳过 .retention_data 段搬运(retention 期间该段在 SRAM 中保留不丢失) |
!= 0x00 |
上电 / Deep Sleep 唤醒 | 搬运 .retention_data 段(Flash → SRAM);无多地址恢复 |
两条路径会合后都执行:.data 搬运 → .bss 清零 → 跳转 main()。
多地址寄存器恢复:Retention 期间 Flash 掉电,唤醒后需恢复多地址启动配置,确保唤醒后多地址启动配置与掉电前一致。上电路径无需此步骤(boot rom中会进行配置)。
Retention 前提条件:retention RAM 需大于 .retention_data + .ram_code 段总大小,否则这些段无法全部保留。
下图为各芯片共有的核心步骤:
__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
Direct XIP 芯片的上电 / Deep Sleep 唤醒从 .vectors 段的 _RESET_ENTRY 入口进入,Retention 唤醒从 .retention_reset 段的 _IRESET_ENTRY 入口进入,两个入口完全独立。
Direct XIP 启动流程在各 Direct XIP 芯片上大框架一致,分为三个阶段:硬件环境初始化 → 中断/Cache 初始化 → 段搬运与清零。
上电 / Deep Sleep 唤醒流程(.vectors)
从 .vectors 段的 _RESET_ENTRY 入口进入(位于 Flash 起始地址,Boot ROM 直接跳转取指,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()|
+-------------------+
关键配置宏:
DP_THROUGH_SWIRE_DIS、SRAM_SIZE 等宏的详细说明见 Direct XIP。
Retention 唤醒流程(.retention_reset)
Retention 唤醒从 .retention_reset 段的 _IRESET_ENTRY 入口进入(位于 IRAM 起始地址,retention 期间不掉电)。
前半段(阶段一、二)与上电路径完全一致,不再重复。与上电路径的差异仅在阶段三:
(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()|
+-------------------+
Retention 唤醒的关键差异点:
- Flash 唤醒(R1):Retention 期间 Flash 掉电,唤醒后需发送
0xAB命令唤醒 Flash 控制器,等待约 25us 才能访问 Flash。 - 多地址寄存器恢复(R2):恢复
g_pm_mspi_cfg多地址启动的相关配置,确保唤醒后多地址启动配置与掉电前一致。上电路径无需此步骤(boot rom中会进行配置)。 - 仅搬运
.data(R4):.retention_reset、.retention_data、.ram_code段在 IRAM 中保留不丢失,仅搬运.data。 - 前提条件:retention RAM 需大于
.retention_reset+.retention_data+.ram_code段总大小。
启动文件编写注意事项
.org 与 -flto 的冲突
在当前编译环境下,.org 伪指令与优化选项中的 Link Time Optimization(-flto)不能同时使用。由于 -flto 一定会被选择(用于减小 bin 文件大小),在 S 文件中使用 .org 时需要用 .option push/pop 包裹:
.option push // 保存当前 .option 配置
.option norelax // 设置为 norelax
.org 0x0 // 设置 .org
......... // 相关代码
.option pop // 恢复 .option 配置
.option push/pop 用于临时保存/恢复选项设置,不影响全局选项。
压缩指令(C Extension / CoDense)
RISC-V 的 C Extension 用 16bit 指令替换 32bit 指令;Andes 的 CoDense 技术把 32 位指令放入指令表,原位置用 16 EXEC.IT 0xxxxx 代替。
- 宏
__nds_execit在编译器中默认打开 _ITB_BASE_设置为.exec.itable段的首地址- S 文件中将
_ITB_BASE_写入uitb寄存器,作为指令表基地址 - 压缩指令存放在
.exec.itable段中
FPU 使能
带 FPU 的芯片在启动时需使能浮点单元。并非所有 D25F 核芯片都支持 FPU,是否支持请查阅 Platform SDK 概述 表格。S 文件中 FPU 相关代码由工具链宏 __riscv_flen 控制,用户不可随意修改:
li t0, 0x00006000
csrrs t0, mstatus, t0 // 设置 mstatus 的 FS 位
fscsr zero // 初始化 FCSR
通过 lst 文件查看段分配
文档中的段分配用代号表示,实际编译后可以通过 lst 文件查看真实的段地址和大小。SDK 编译时由 objdump 工具生成 .lst 文件,其中 -h 选项输出段表(Sections),记录每个段的 VMA、LMA、大小和属性。
段表示例(以 Direct XIP 为例,截取关键段):
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
如何看这个表:
Name:段名,对应 link 文件中定义的段。Size:段大小(字节,十六进制)。VMA:运行地址。例如.retention_data的 VMA 是0x00000040(IRAM 中)。LMA:加载地址,段在 Flash 中的存储地址。例如.retention_data的 LMA 是0x20000140(Flash 中)。Algn:对齐方式(2**n表示 2^n 字节对齐)。
判断是否需要软件搬运:比较 VMA 和 LMA,若不同则启动时需要 cstartup 软件搬运。例如:
.vectors:VMA = LMA =0x20000000,无需软件搬运,CPU 直接从 Flash 取指(XIP)。.ram_code:VMA =0x00000240(IRAM),LMA =0x20000340(Flash),两者不同,启动时由 cstartup 从 Flash 搬到 IRAM。.bss:只有 VMA,无 LMA(NOLOAD),启动时直接清零。
各芯片实际的段地址和大小以编译生成的 lst 文件为准,文档中的代号仅用于说明结构。
Load to RAM 的地址特殊性
Load to RAM(Single RAM)的 lst 文件中,代码段的地址显示有一个特殊点,理解它才能正确解读 lst。
现象:代码段(.vectors、.ram_code、.text、.rodata)的 VMA 都从 0x0 开始,而不是 RAM 物理地址 <SRAM_BASE>;而数据段(.retention_data、.data、.bss)的 VMA 则在 <SRAM_BASE>+(如 0x840000+)。
举例(真实 lst 片段):
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
- 代码段 VMA 在
0x0+:.vectors/.ram_code实际运行在 RAM,.text/.rodata实际运行在 Flash。 - 数据段 VMA 在
0x840000+:直接访问 RAM 物理地址。
原因:这是 MCU 取指机制决定的。CPU 取指时看到的地址不是 RAM 物理地址,而是一个独立的指令地址空间(从 0x0 开始)。MCU 内部通过 I-Cache Tag 首地址寄存器设置一个分界点:
- 地址 < 分界点:从 RAM 取指(RAM 物理地址 =
<SRAM_BASE>+ 指令地址)。 - 地址 > 分界点:经 I-Cache 从 Flash 取指。
SDK 把 .ram_code 结束地址 256 字节对齐后作为这个分界点(Cache Tag 地址要求 256 字节对齐),因此 .vectors 和 .ram_code 在 RAM 运行,.text 和 .rodata 在 Flash 运行。
解读要点:
- VMA 是 0x0,但实际在 RAM:
.vectors和.ram_code的 VMA 显示为0x0,但物理上运行在 RAM(RAM 基地址因芯片而异,如0x840000)。这是因为指令地址空间从0x0开始,CPU 取指时通过分界点机制自动映射到 RAM。 - cstartup 无搬运动作:
.vectors和.ram_code存储在 Flash,上电时由硬件 Boot ROM 搬到 RAM,cstartup 中没有对应的搬运动作。
.retention_data 和 .data 则不同:LMA 在 Flash,VMA 在 <SRAM_BASE>+,由 cstartup 软件搬运。
platform_init
概述
platform_init() 是 SDK 提供的平台初始化入口函数,负责完成芯片上电或唤醒后的基础环境配置。用户需在 main() 函数开头调用该函数,其中有一些不可更改的,有一些是根据用户自己的应用场景决定如何配置,或者如何设计逻辑。下面会详细介绍下每部分的具体作用,以便大家可以正确的使用。
各芯片的具体函数原型和参数定义请查阅对应芯片的驱动源文件。本章只介绍 platform_init() 的执行流程和各步骤的作用。
初始化流程
下图为 platform_init() 的完整步骤(全集)。其中标注「必须」的步骤所有芯片均会执行,标注「部分芯片」的步骤仅在部分芯片上存在,具体支持情况请查阅对应芯片的驱动代码。
Power On / Wakeup
|
v
+--------------------+
| 系统初始化 | cpu_wakeup_init / sys_init(必须)
+--------------------+
|
v
+--------------------+
| 关闭 32K 看门狗 | wd_32k_stop(部分芯片,硬件默认开启)
+--------------------+
|
v
+--------------------+
| 更新系统状态 | pm_update_status_info(部分芯片)
+--------------------+
|
v
+--------------------+
| 校准值读取 | 优化模块性能(必须)
+--------------------+
|
v
+--------------------+
| GPIO 初始化 | 防止漏电(必须)
+--------------------+
|
v
+--------------------+
| SWS 引脚上拉 | 防止浮空漏电(必须)
+--------------------+
|
v
+--------------------+
| 关闭 Timer 看门狗 | wd_stop(部分芯片,startup.S 中开启)
+--------------------+
|
v
+--------------------+
| Flash 写保护 | 防止程序区误擦除(必须)
+--------------------+
说明
- 各步骤的调用顺序和是否存在因芯片而异,上图仅展示完整步骤集合。实际顺序请以对应芯片的驱动代码为准。
各初始化步骤详解
系统初始化
配置晶振(内部电容/外部电容)、电源模式等系统级参数。根据架构不同,TC 系列调用 cpu_wakeup_init(),TL 系列调用 sys_init(),参数因芯片而异。
注意
- 必须在其他初始化之前调用,否则后续校准值可能不生效。
看门狗处理
SDK 中存在两个看门狗,部分芯片会默认开启,platform_init 中将其关闭仅为方便 demo 演示。实际产品中必须使能并定时喂狗。
| 看门狗 | 开启来源 | 超时复位 | platform_init 中的处理 |
|---|---|---|---|
| 32K Watchdog | 硬件默认开启 | 5 秒 | wd_32k_stop()(部分芯片) |
| Timer Watchdog | startup.S 中开启 | 10 秒 | wd_stop()(部分芯片) |
GPIO 初始化
防止漏电的关键配置。处理方式因芯片而异,主要有两类:
- 将未使用引脚设置为高阻态
- 将未使用引脚设置为高阻态并启用下拉电阻
具体采用哪种方式及涉及的 API 请查阅对应芯片的驱动代码。
SWS 引脚上拉
SWS(Single Wire Slave)是调试烧录引脚,必须配置上拉电阻:
- 原因:浮空状态可能导致异常漏电,甚至误写寄存器/SRAM 导致死机
- 配置:默认启用 1MΩ 上拉
校准值读取
从 Flash / OTP / eFuse 中读取校准值,优化模块性能,必须调用。典型校准对象包括:
- 射频(RF)性能
- ADC 精度
- 电压值精准度
注意
- 必须在系统初始化之后调用,具体位置请查阅对应芯片的驱动代码,放在其他位置可能不生效。
Flash 写保护
防止程序区被误擦除:
- 策略:程序区保护,数据区不保护(用户根据自己的固件大小,决定保护区域大小)
- 实现:具体调用的 API 因芯片而异,请查阅对应芯片的驱动代码
Clock 时钟系统
简述
系统时钟可来源于以下几种:
| 时钟源 | 说明 |
|---|---|
| PLL | 片内倍频锁相环,常见频率有 192M、240M 等(因芯片而异),输出频率最高 |
| Doubler | 倍频器,将晶振频率倍频以获得更高频率(如 24M 晶振倍频到 48M)。部分芯片(如 TC 系列)使用 doubler 替代 PLL |
| XTAL | 外部晶振,频率精度高。各芯片支持的晶振频率不同(大多数芯片使用24M,部分芯片使用48M),需根据硬件参考设计使用;晶振可使用内部电容或外部电容,使用内部电容时需做板级校准 |
| RC | 片内RC振荡器,无需外部元件,成本低;但频率误差较大,受温度/电压漂移影响,需实时校准(TL系列见 clock_cal_24m_rc(),TC系列见 rc_24m_cal()),其作为XTAL的起振时的引导钟源,越准起振越快 |
TL系列与TC系列在时钟架构和 clock_init 参数结构上存在根本差异,下面分别说明。各芯片具体的可用时钟配置、宏定义及频率上限,均以各芯片 clock.h 为准。
TL系列
TL系列通过 clock_init 一次配置即可同时设定多路时钟频率,具体路数因芯片而异(核心包括 PLL、CCLK、HCLK、PCLK、MSPI,多核芯片还包含 DSP、WT 等附加时钟)。
核心时钟概念
具体的时钟树,可查看 datasheet 手册。
| 时钟 | 说明 |
|---|---|
| pll_clk | PLL时钟,很多模块时钟的源头 |
| cclk | CPU时钟,决定程序运行速度 |
| hclk | AHB总线时钟,所有挂在AHB总线上的模块使用hclk |
| pclk | APB总线时钟,所有挂在APB总线上的模块使用pclk |
| mspi_clk | MSPI连接Flash的时钟,控制取指、读写Flash等操作 |
注意
- 多核芯片(如 TL751x)还包含 DSP、WT、N22、LSPI、GSPI 等附加时钟,具体以各芯片
clock.h为准。
clock_init 配置
驱动提供 clock_init() 函数配置系统时钟。强烈推荐使用驱动头文件中提供的宏定义,而非直接调用 clock_init():宏定义已封装正确的分频参数组合,可避免参数搭配错误。
宏命名规则: 大多数 TL 芯片的宏遵循 PLL_<PLL频率>_CCLK_<CCLK频率>_HCLK_<HCLK频率>_PCLK_<PCLK频率>_MSPI_<MSPI频率> 的命名,从宏名即可读出各路时钟频率。部分早期芯片(如 B91/B92)宏名较短(如 CCLK_96M_HCLK_48M_PCLK_24M),不含 PLL 和 MSPI 字段;多核芯片(如 TL751X)宏名还包含 DSP/WT 等附加时钟字段。
使用示例:
/* 推荐:使用宏定义配置系统时钟,直接以语句形式调用即可 */
PLL_192M_CCLK_96M_HCLK_48M_PCLK_48M_MSPI_48M;
/* 低功耗场景:使用外部晶振,绕过 PLL */
XTAL_24M_CCLK_24M_HCLK_24M_PCLK_24M_MSPI_24M;
注意
clock.h中另有标注为 internal debug purpose, users are prohibited from calling 的宏(通常位于文件中的 internal 区块),用户禁止调用。请务必以各芯片clock.h中的标注为准,某些在早期芯片上用户可用的宏,在新芯片中可能被划入 internal 区块。- MSPI 频率与 Flash 类型:内置 Flash 的 MSPI 最大速度参见各芯片
clock.h注释;外挂 Flash 的最大速度需根据板级测试确定,因为最大速度与板级走线相关,且受温度和 GPIO 电压影响,需在板级最高/最低电压、高低温长期稳定性测试中验证。 - HCLK/PCLK 分频搭配限制(部分芯片,如 B91/B92):当 HCLK = 1/2 × CCLK 时,PCLK 不能取 1/4 × HCLK,具体限制以各芯片
clock.h注释为准。
电压与频率(部分芯片)
部分 TL 芯片(如 TL721X、TL322X、TL751X)支持多个核心电压档位。电压档位决定了各时钟可支持的最大频率:高电压档位支持更高频率,低电压档位功耗更低。
- 各档位下的频率上限、可选时钟宏均按电压分组列于
clock.h中,用户应确保所选时钟配置不超过当前电压档位支持的最大频率。 - 电压档位切换通过类似于
pm_set_dvdd()接口配置,详见PM_Demo。 - 切换顺序:升压时先切电压再升频率,降压时先降频率再切电压。
- sleep 前需恢复低压:由于 deep/deep retention sleep 期间数字寄存器会丢失,唤醒后 EMA 数字寄存器与实际电压可能不匹配,导致 SRAM 使用出错。因此 进入 sleep 前必须调用类似于
pm_set_dvdd()将电压恢复到低压档位(如 0.8V)。
以下是TL751x的代码示例:
/* 升压流程:先切电压,再切频率 */
pm_set_dvdd(DVDD1_DVDD2_VOL_0P9V, 1000);
PLL_192M_D25F_DSP_192M_HCLK_96M_PCLK_48M_MSPI_48M_WT_12M;
/* 降压流程:先切频率,再切电压 */
PLL_192M_D25F_DSP_96M_HCLK_48M_PCLK_48M_MSPI_48M_WT_12M;
pm_set_dvdd(DVDD1_DVDD2_VOL_0P8V, 1000);
注意
- 电压切换是芯片级特性,具体电压值、档位数因芯片而异,请以各芯片
clock.h和pm.h为准。
TC系列
clock_init 配置
TC系列采用 SYS_CLK_TypeDef 枚举选择系统时钟源,clock_init 仅接收一个枚举参数:
clock_init(SYS_CLK_24M_RC);
枚举值命名规则为 SYS_CLK_<频率>_<时钟源>,如 SYS_CLK_24M_RC、SYS_CLK_24M_Crystal、SYS_CLK_48M_Crystal 等。其中高频选项(如 48M、32M)通过 doubler 对晶振倍频得到。各芯片支持的枚举值范围不同(如部分芯片还支持 48M 晶振等),具体以各芯片 clock.h 为准。
| 枚举值 | 时钟源 | 系统时钟频率 | 说明 |
|---|---|---|---|
SYS_CLK_24M_RC |
内部 24M RC | 24 MHz | 使用内部 RC 振荡器,无需外部晶振 |
SYS_CLK_12M_Crystal |
外部 24M 晶振 | 12 MHz | xtal → Doubler(48M)→ 1/4 分频 |
SYS_CLK_16M_Crystal |
外部 24M 晶振 | 16 MHz | xtal → Doubler(48M)→ 1/3 分频 |
SYS_CLK_24M_Crystal |
外部 24M 晶振 | 24 MHz | 直接使用外部晶振,常用配置 |
SYS_CLK_32M_Crystal |
外部 24M 晶振 | 32 MHz | xtal → Doubler(48M)→ 2/3 分频 |
SYS_CLK_48M_Crystal |
外部 24M 晶振 | 48 MHz | xtal → Doubler 倍频,最高系统时钟 |
注意
- 自动 RC 校准:
clock_init()在上电或 deep sleep 唤醒后调用时,会自动执行一次 24M RC 校准。若用户希望自行控制校准时机,可在调用前通过clock_init_calib_24m_rc_cfg(0)关闭此自动校准逻辑。 - 关于校准的前提条件:调用
rc_24m_cal()前,必须确保当前系统时钟不是 24M RC,否则会导致系统时钟波动,影响芯片正常工作。 - doubler 校准限制:使用 48M/32M 系统时钟(doubler 倍频)时,不能调用
doubler_calibration(),否则可能导致死机。USB 通信收发期间也禁止调用。
32K 时钟
32K 时钟主要用于低功耗模式下的定时唤醒(sleep timer)、32k watchdog等。用户通过 clock_32k_init() 选择 32K 时钟源:
- CLK_32K_RC:内部 32K RC,一般情况下使用 RC 即可满足需求。sleep 时间通过 tracking 方式获取,不受 32K RC 频率偏差影响,上电时校准一次即可,无需定期校准。
- CLK_32K_XTAL:外部 32K 晶振,精度高,需占用外部引脚。仅在对时间精度要求非常高的场景才需要使用。
使用 32K 晶振时,TL系列需调用 clock_kick_32k_xtal() 启动晶振振荡。
注意事项
通用:
- DMA 操作期间禁止切换时钟:时钟切换过程中系统时钟会暂停一段时间,在 DMA 收发过程中调用时钟切换接口可能导致数据丢失。
- 24M RC 校准:使用 24M RC 作为时钟源时,必须定期调用校准接口。驱动建议的定期校准间隔为 10 秒。若使用 sleep 唤醒功能,第一次进入 sleep 前必须校准 24M RC,否则唤醒后可能因 RC 频率偏差过大导致振荡器无法启动。
TL系列:
- PLL 频率需兼容多模块需求:PLL 是各模块时钟的源头,部分模块对频率有特殊要求(如 USB 需 48M、Audio 依赖默认 PLL 频率)。配置 PLL 频率时需兼顾这些模块的需求,确保 PLL 频率能分频得到各模块所需频率,否则可能导致 USB、Audio 等模块工作异常。
Power 电源管理
概述
Power 模块负责芯片上电后的系统级电源配置,包括供电模式选择、VBAT 电压配置、晶振电容选择,以及运行期的电源状态查询和软件复位。本章覆盖三个使用场景:
- 系统初始化:上电/唤醒后配置电源模式、晶振、电压等基础参数
- 电源状态查询:判断当前是从上电、reboot、deep sleep 唤醒还是 retention 唤醒恢复
- 软件复位:运行期触发芯片重启
低功耗管理(Suspend / Deep Retention / Deep Sleep / Shutdown)、唤醒源配置和使用方法,请参考 电源管理系统。
系统初始化
作用
系统初始化函数在 main() 入口最先调用,负责配置默认模块使能、供电模式(LDO/DCDC)、晶振电容、VBAT 电压等系统级参数。每次执行到 main() 都必须调用。不同系列调用接口不同,具体如下表:
TC系列:cpu_wakeup_init
| 芯片 | 函数原型 |
|---|---|
| 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系列:sys_init
| 芯片 | 函数原型 |
|---|---|
| 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) |
各芯片的具体参数定义可查阅对应芯片的驱动源文件。
供电模式选择
芯片支持 LDO 和 DCDC 两种供电模式,用户根据功耗和噪声需求选择:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| LDO | 噪声低、外围器件少,效率相对较低 | 对噪声敏感的场景 |
| DCDC | 效率高,但可能有纹波 | 对功耗敏感的场景 |
参数说明
不同芯片的函数原型略有差异,但同一类型入参的意义基本一致;以下按参数类型统一说明,各芯片具体参数和枚举值以代码定义为准。
| 参数 | 作用 |
|---|---|
power_mode_e / POWER_MODE_TypeDef |
选择供电模式(LDO/DCDC) |
vbat_type_e |
根据 VBAT 输入电压选择是否 bypass VBAT LDO |
gpio_voltage_e |
选择 GPIO 电压(3V3/1V8),需配合硬件 CFG_VIO 引脚 |
cap_typedef_e |
选择使用内部或外部晶振电容 |
cap_typedef_e :
- 需与硬件板实际电容配置一致:选内部电容时硬件板无需外接匹配电容;选外部电容时硬件板必须焊接匹配电容。若软件配置与硬件实际不一致(如选外部电容但板上未焊接),会导致晶振起振异常,可能触发反复重启。起振慢的相关处理见 起振与时序。
vbat_type_e :
- VBAT ≥ 3.6V 时,不可以 bypass;
- VBAT ≤ 2.2V 时,必须 bypass;
- 2.2V < VBAT < 3.6V 时,bypass与否均可,根据实际配置情况选择,建议选择不bypass。
- 不 bypass 时GPIO输出 ≈ 3.3V,bypass 时GPIO输出 = VBAT;
gpio_voltage_e (仅 B92):
- 需硬件 CFG_VIO 引脚配合:3V3 接 VSS,1V8 接 VDDO3。
- 1V8 模式的限制:配置为 1V8 时,ADC 最大检测电压 ≤ 1.8V;不可使用
ADC_VBAT_SAMPLE;使用 USB 时 GPIO 不可配置为 1V8。
使用注意事项
起振与时序
晶振起振慢会触发 reboot
系统初始化函数内部会等待晶振起振稳定。SDK 默认的等待时间能满足大部分晶振的需求,正常情况下不会触发 reboot。只有当使用的晶振起振非常慢(超出默认等待时间)时,调用系统初始化函数后会触发一次 reboot。这是 SDK 的正常保护机制,不是 bug。
确认是晶振起振慢导致后,在调用系统初始化函数前,先调用对应接口调长起振等待时间(建议留一定余量),调到满足晶振要求后就不会再 reboot。
| 系列 | 判断是否发生过 reboot | 调整起振等待时间的接口 |
|---|---|---|
| TL 系列(除 B91 外) | pm_update_status_info() 配合 pm_get_sw_reboot_event() |
pm_set_xtal_stable_timer_param() |
| B91 | 读取 PM_ANA_REG_POWER_ON_CLR_BUF0[bit2] |
pm_set_xtal_stable_timer_param() |
| TC 系列 | 读取 DEEP_ANA_REG0[bit1] |
pm_set_wakeup_time_param() |
各芯片接口的具体参数、默认值和含义请查阅对应芯片的 pm.h。该接口配置在 reboot 或 deep sleep 后会丢失,需重新配置。
32k RC 校准耗时 6~7ms(TC 系列)
上电或 deep sleep 唤醒后调用系统初始化函数,会耗时约 6~7ms 执行 32k RC 校准。retention 唤醒不会校准。若不需要此逻辑,可在调用系统初始化函数前调用 cpu_wakeup_init_calib_32k_rc_cfg(0) 关闭,该接口必须在系统初始化函数之前调用才生效。
电源状态查询
作用
电源状态查询用于判断芯片当前的启动原因(上电 / 软件重启 / Deep Retention 唤醒 / Deep Sleep 唤醒 / 看门狗复位等),典型用于:
- bootloader 与 app 分阶段初始化时区分启动阶段
- 异常恢复后定位复位来源(看门狗 / 软件 reboot / 上电)
实现机制
不同芯片支持的电源状态可能有差异。对于deepret唤醒的判断,是所有芯片都需要用到的,其在系统初始化接口内部就会更新,无需额外调用接口。其他更复杂的电源状态,如deep sleep唤醒或软件复位等,需要用户主动调用 pm_update_status_info 接口更新后,通过全局变量读取,目前并非所有芯片都支持;同时,deepret唤醒作为一种电源状态,也被包含在该全局变量中。这里列出各芯片电源状态更新和读取的差异:
| 芯片类型 | deepret唤醒状态 | 其他电源状态 | 读取变量 |
|---|---|---|---|
| B80 / B85 / B87 / TC321x | 支持 | 不支持 | pmParam.is_deepretn_back |
| TC122x / TC123x | 支持 | 支持 | pmParam.mcu_status |
| B91 / B92 / TL321x / TL322x / TL323x / TL721x / TL751x | 支持 | 支持 | g_pm_status_info.mcu_status |
调用pm_update_status_info时需要注意,当 clr_en=1 时,更新完会清掉相关寄存器,因此该入参下不能调用两次,否则会导致第二次获取到的状态错误并覆盖原值。若需要多次调用(如 bootloader + app 场景),前面调用时入参传 0,最后一次调用入参传 1。
各芯片具体的 MCU 状态枚举定义请查阅对应芯片的 pm.h。
软件复位
作用
软件复位用于运行时主动复位芯片,不同系列的接口不同:
| 系列 | 接口 | 说明 |
|---|---|---|
| TC | start_reboot() |
软件重启 |
| TL | sys_reboot() |
软件重启,代码位于 text 段 |
| TL | sys_reboot_ram() |
软件重启,代码位于 RAM 段 |
使用注意事项:
-
TL系列
sys_reboot()和sys_reboot_ram()的区别仅在于运行时代码存储位置。正常情况下,使用sys_reboot();当 Flash 无法访问(如正在进行 Flash 擦写)时,使用sys_reboot_ram(),因为该函数代码常驻 RAM 不依赖 Flash 取指。 -
使用
pm_get_sw_reboot_event接口(TL 系列,除 B91/B92 外)可以获取软件reboot 的原因,但需要注意:必须先调用pm_update_status_info更新电源状态,否则返回值无效。 -
B91 reboot的特殊情况: 对于 B91,当时钟源为 XTAL 或 PLL,且 hclk = 1/2 cclk 时,直接 reboot 可能产生问题。为解决此问题,
sys_init中会在检测到 reboot 后自动进入一次 deep sleep(约 100ms 后唤醒),使用MCU_STATUS_REBOOT_DEEP_BACK标识该类型复位。
中断
本章覆盖 PLIC、CLIC、IRQ 三类中断架构。用户根据芯片在下表中查找对应架构。
中断类型对照表
| 芯片 | 中断类型 |
|---|---|
| B91 / B92 / TL721x / TL321x / TL323x | PLIC |
| TL322x / TL751x | PLIC + CLIC(多核,D25 核用 PLIC,N22 核用 CLIC) |
| B80 / B80B / B85 / B87 / TC321x / TC122x / TC123x | IRQ |
三种架构特性对照
| 特性 | PLIC | CLIC | IRQ |
|---|---|---|---|
| 中断开关层级 | 三级(MSTATUS.MIE + MIE.MEIE + PLIC 使能) | 两级(MSTATUS.MIE + CLIC 使能) | 两级(irq_enable + 中断源 mask) |
| 中断入口 | 每个中断号独立入口(向量) | 每个中断号独立入口(向量) | 所有中断共用一个 irq_handler |
| 优先级 | 4 级(0-3) | 4 级(0-3) | 不支持 |
| 抢占 | 4 种模式(MODE0-3) | 一种,使能即可 | 不支持 |
| 临界区 | threshold 屏蔽低优先级 | threshold 屏蔽低优先级 | 关全局中断 |
| claim/complete | 需要(向量模式下硬件自动) | 不需要(硬件自动) | 不需要 |
| MTI(定时器) | 独立模块 PLMT | CLIC 内部管理 | 不支持 |
| MSI(软中断) | 独立模块 PLIC_SW | CLIC 内部管理 | 不支持 |
| WFI 唤醒 | 支持 | 支持 | 不支持 |
| ISR 注册 | PLIC_ISR_REGISTER |
CLIC_ISR_REGISTER |
直接在 irq_handler 中分发 |
说明
- 多核芯片 TL322X/TL751X 两个核各有一个中断控制器,D25 核使用 PLIC,N22 核使用 CLIC
- PLIC、CLIC、IRQ 三类架构的具体差异在各自章节内说明
PLIC
PLIC(Platform-Level Interrupt Controller)是 RISC-V 机器模式的外部中断控制器,管理所有外设中断源。PLIC 作为 RISC-V Core 与外部中断源之间的桥梁,接收各外设中断请求,通过优先级仲裁后将中断 ID 发送给 Core,Core 通过向量表跳转到对应 ISR。两大核心功能:中断向量化分发(每个外部中断有独立入口)和中断优先级与抢占。
RISC-V 中断体系与 trap 机制
RISC-V 机器模式定义三类中断,PLIC 架构下分别由独立模块管理:
| 中断类型 | 缩写 | CSR 使能位 | 来源 | 管理模块 | 入口 |
|---|---|---|---|---|---|
| 机器外部中断 | MEI | MIE.MEIE | 外设(UART/GPIO/Timer 等) | PLIC | 向量模式直跳 entry_irqN |
| 机器定时器中断 | MTI | MIE.MTIE | mtime 定时器 | PLMT | trap_entry 分发 |
| 机器软件中断 | MSI | MIE.MSIE | 软件触发 | PLIC_SW | trap_entry 分发 |
mcause 寄存器:trap 发生时,mcause 记录 trap 原因:
mcause[31] = 0:异常(Exception),如指令异常、栈溢出等mcause[31] = 1:中断(Interrupt),低 31 位为中断类型码:(mcause & 0x7FFFFFFF) == 3:MSI(mcause & 0x7FFFFFFF) == 7:MTI(mcause & 0x7FFFFFFF) == 11:MEI(向量模式下由 PLIC 直跳,不进入 trap_entry)
中断向量表:启动时在启动文件中构建 __vectors,并通过 csrw mtvec, __vectors 设置基址、csrsi mmisc_ctl, 2 使能硬件向量中断。同时操作 PLIC Feature 寄存器使能向量模式(reg_irq_feature = FLD_FEATURE_VECTOR_MODE_EN):
__vectors:
[0] trap_entry // 异常 / 非向量中断入口(MTI/MSI/Exception)
[1] entry_irq1 // IRQ1: 外设中断 1
...
[N] entry_irqN // IRQN: 外设中断 N
默认所有入口弱定义指向 default_irq_entry(死循环),用户通过 PLIC_ISR_REGISTER 宏注册后对应 entry_irqN 被替换为实际 ISR。
trap_entry:MTI、MSI 和异常的统一入口,定义为 weak 函数,用户可重写。内部读 mcause 分发:
__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(); // 异常:保存上下文后死循环
}
}
现场保存与恢复:trap_entry 和各 entry_irqN 均通过 __attribute__((interrupt("machine"))) 声明,编译器自动生成现场保存/恢复代码:
- 进入:硬件保存 mepc、mcause 等 CSR;编译器保存被修改的通用寄存器
- 退出:编译器恢复寄存器;
mret指令硬件恢复 mepc
用户无需手动保存/恢复寄存器现场。
异常处理:指令异常、栈溢出等非中断 trap 由 except_handler() 处理,默认保存异常上下文(mtval、mepc、mstatus、mcause、mdcause)后进入死循环,便于调试。
外部中断使用
三级中断开关:
// 第一级:RISC-V Core 全局中断(MSTATUS.MIE + MIE.MEIE)
core_interrupt_enable();
// 第二级:PLIC 中断源使能
plic_interrupt_enable(IRQ_SYSTIMER);
// 第三级:外设模块中断 mask
stimer_set_irq_mask(FLD_SYSTEM_IRQ);
常见外设的第三级 mask 配置:
- Timer:
timer_set_irq_mask()/stimer_set_irq_mask() - GPIO:配置中断触发边沿/电平时自动使能
- UART:初始化时自动配置
注册与实现 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. 清中断状态
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1MS);
gpio_toggle(LED2); // 2. 用户处理代码
}
}
PLIC_ISR_REGISTER(stimer_irq_handler, IRQ_SYSTIMER)
各芯片的中断号定义在对应 plic.h 中,以 IRQ_XXX 宏形式给出,中断号分配因芯片而异,请参考对应芯片的头文件。
优先级与抢占
优先级:PLIC 支持 4 个优先级(0-3),数字越大优先级越高。
| 优先级 | 枚举值 | 说明 |
|---|---|---|
| 0 | IRQ_PRI_LEV0 |
永不产生中断 |
| 1 | IRQ_PRI_LEV1 |
最低有效优先级(默认) |
| 2 | IRQ_PRI_LEV2 |
中等优先级 |
| 3 | IRQ_PRI_LEV3 |
最高优先级 |
规则:
- 仅当
priority > threshold时中断才能触发 - 默认
threshold = 0,默认priority = 1 - 高优先级中断可打断低优先级中断,同级不可打断
- 优先级 0 的中断源永不产生中断
抢占配置:
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);
抢占模式(控制 MEI/MSI/MTI 三类中断间的嵌套关系):
| 模式 | 枚举值 | 含义 |
|---|---|---|
| MODE0 | CORE_PREEMPT_PRI_MODE0 |
MTI 和 MSI 不能打断 MEI,MSI 和 MTI 之间可互相嵌套 |
| MODE1 | CORE_PREEMPT_PRI_MODE1 |
MTI 不能打断 MEI,MSI 和 MEI 之间可互相嵌套 |
| MODE2 | CORE_PREEMPT_PRI_MODE2 |
MSI 不能打断 MEI,MTI 和 MEI 之间可互相嵌套 |
| MODE3 | CORE_PREEMPT_PRI_MODE3 |
MEI、MSI、MTI 三者可互相嵌套 |
三种中断同时发生时,硬件处理顺序为 MEI > MSI > MTI。关闭抢占使用 plic_preempt_feature_dis()。
ISR 中禁止调用的接口(违反会导致中断卡死或 claim 失败):
| 接口 | 原因 |
|---|---|
plic_set_threshold() |
硬件有 threshold 入栈/出栈行为,出栈会覆盖软件设置的值;内部短暂关闭全局中断防竞争 |
plic_set_priority() |
claim 之前调用会使 claim 取不到值,认为中断未发生 |
plic_interrupt_enable() |
禁止在 ISR 中调用 |
plic_interrupt_disable() |
claim 后关闭中断会导致 complete 无法完成,threshold 不更新,同优先级后续中断全部卡死 |
plic_irqs_postprocess_for_wfi() |
内部会重新使能 PLIC 中断 |
必须成对使用的接口: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()。
机器定时器(PLMT)
PLMT 核心是两个 64 位寄存器:mtime(单调递增计数器)和 mtimecmp(比较值,mtime >= mtimecmp 时触发 MTI)。
使用流程:
// 1. 初始化时钟源(32K RC 需校准,32K XTAL 不需要)
clock_32k_init(CLK_32K_RC);
clock_cal_32k_rc();
mtime_clk_init(CLK_32K_RC);
// 2. 使能 MTI(属于机器模式内部中断,不通过 PLIC)
core_interrupt_enable();
core_mie_enable(FLD_MIE_MTIE);
// 3. 设置定时周期
mtime_set_interval_ms(500); // 500ms 后触发
中断处理:
_attribute_ram_code_sec_ void mtime_irq_handler(void)
{
mtime_set_interval_ms(500); // 设置下一次触发时间,否则只触发一次
gpio_toggle(LED4);
}
mtime_irq_handler 为 weak 弱函数,用户重写即可。MTI 通过 trap_entry 进入,无需 claim/complete。
软件中断(PLIC_SW)
软件中断由代码主动触发,不依赖外设硬件。
// 使能
core_interrupt_enable();
core_mie_enable(FLD_MIE_MSIE);
plic_sw_interrupt_enable();
// 触发
plic_sw_set_pending();
_attribute_ram_code_sec_ void mswi_irq_handler(void)
{
gpio_toggle(LED2);
}
mswi_irq_handler 为 weak 弱函数。MSI 通过 trap_entry 进入,SDK 默认的 trap_entry 已自动完成 claim/complete。
临界区保护
用于在 Flash 操作等需要避免中断打断的场景下保护代码执行。
// 进入临界区:仅屏蔽 priority <= threshold 的中断
unsigned int r = plic_enter_critical_sec(1, IRQ_PRI_NUM1);
// ... 临界区代码 ...
// 退出临界区:恢复到进入前的状态
plic_exit_critical_sec(1, r);
| 参数 | 含义 |
|---|---|
preempt_en = 1 |
使用 threshold,仅屏蔽 priority <= threshold 的中断 |
preempt_en = 0 |
关闭全局中断,完全禁止所有中断 |
两个函数必须成对使用。
WFI 低功耗唤醒
SoC 支持通过 WFI(Wait-For-Interrupt)指令进入低功耗模式,由中断唤醒。
两种唤醒模式(取决于进入 WFI 时 MSTATUS.MIE 的状态):
| 模式 | 全局中断状态 | 唤醒条件 | 唤醒后行为 |
|---|---|---|---|
| Interrupt Enable | MSTATUS.MIE = 1 |
中断被 CPU 接收 | 跳转到对应 ISR 执行,ISR 返回后继续执行 WFI 之后的指令 |
| Interrupt Disable | MSTATUS.MIE = 0 |
中断进入 pending 即可唤醒 | 不执行 ISR,直接从 WFI 之后的指令继续执行 |
方式一:预处理(推荐)
plic_irqs_preprocess_for_wfi(flag, mie) 一次性保存并关闭所有中断,仅保留 mie 指定的唤醒源:
// 进入 WFI 前
plic_irqs_preprocess_for_wfi(1, FLD_MIE_MEIE); // flag=1 关闭全局中断,仅保留 MEIE 唤醒
plic_interrupt_enable(IRQ_SYSTIMER); // 设置具体唤醒源(stimer)
core_entry_wfi_mode(); // 执行 WFI 指令
// 唤醒后(Interrupt Disable 模式)
stimer_clr_irq_status(FLD_SYSTEM_IRQ); // 必须先清除唤醒源的中断状态标志
plic_irqs_postprocess_for_wfi(); // 恢复所有中断配置
方式二:全局中断关闭时手动 claim/complete
// 确保 WFI 前所有 PLIC 请求已被清除,否则进不了WFI
if (plic_clr_all_request() == 0) {
return; // 清除失败:中断状态未清 或 触发电平一直存在
}
core_entry_wfi_mode();
// 唤醒后手动 claim/complete
unsigned int claim = plic_interrupt_claim();
// ... 根据 claim 值处理对应的中断源 ...
plic_interrupt_complete(claim);
注意
plic_irqs_preprocess_for_wfi和plic_irqs_postprocess_for_wfi必须成对使用- 调用
plic_irqs_postprocess_for_wfi前,必须确保对应中断的状态标志已清除,否则进不了WFI - 向量模式下硬件自动 claim,软件一般无需手动调用
plic_interrupt_claim();仅在全局中断关闭的手动处理方式中才需要 plic_clr_all_request()返回 0 表示清除失败,原因有两种:对应中断状态未清除、触发中断的电平一直存在
CLIC
CLIC(Core-Local Interrupt Controller)是 RISC-V 机器模式的中断控制器,与 PLIC 的关键区别:
| 维度 | PLIC | CLIC |
|---|---|---|
| 中断开关层级 | 三级(MSTATUS.MIE + MIE.MEIE + PLIC 使能) | 两级(MSTATUS.MIE + CLIC 使能) |
| claim/complete | 需要(MEI) | 不需要(硬件自动) |
| 抢占模式 | 4 种(MODE0-3) | 无需指定,使能后按优先级嵌套 |
| MTI/MSI 管理 | 独立模块(PLMT/PLIC_SW) | CLIC 直接管理 |
| ISR 注册宏 | PLIC_ISR_REGISTER |
CLIC_ISR_REGISTER |
CLIC 架构下,所有中断均支持向量模式,直接跳转到 entry_irqN 入口。使用前需调用 clic_init()(通常在 PLATFORM_INIT 中已完成),该函数将所有中断优先级初始化为 1。
外部中断使用
两级中断开关:
// 第一级:RISC-V Core 全局中断(MSTATUS.MIE)
core_interrupt_enable();
// 第二级:CLIC 中断源使能
clic_interrupt_enable(IRQ_SYSTIMER);
// 外设模块中断 mask
stimer_set_irq_mask(FLD_SYSTEM_IRQ);
注册与实现 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 无 claim/complete 流程,硬件自动处理中断确认。各芯片的中断号定义在对应 clic.h 中,以 IRQ_XXX 宏形式给出。
优先级与抢占
CLIC 优先级定义与 PLIC 相同(4 级 0-3,规则相同)。
抢占配置(无需指定模式):
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);
关闭抢占使用 clic_preempt_feature_dis()。
机器定时器
CLIC 架构下 MTI 由 CLIC 直接管理(中断号 IRQ_MTIMER),无需 PLMT 独立模块。
core_interrupt_enable();
clic_interrupt_enable(IRQ_MTIMER);
// 设置定时周期(接口与 PLIC 相同)
mtime_set_interval_ms(500);
ISR 通过 CLIC_ISR_REGISTER(mtime_irq_handler, IRQ_MTIMER) 注册,mtime_irq_handler 为 weak 弱函数。
软件中断
CLIC 架构下 MSI 由 CLIC 直接管理(中断号 IRQ_SOFT),无需 PLIC_SW 独立模块。
// 使能
core_interrupt_enable();
clic_interrupt_enable(IRQ_SOFT);
// 触发
clic_set_pending(IRQ_SOFT);
ISR 通过 CLIC_ISR_REGISTER(soft_irq_handler, IRQ_SOFT) 注册,无 claim/complete。
临界区保护
CLIC 临界区接口与 PLIC 相同,使用 threshold 屏蔽:
unsigned int r = plic_enter_critical_sec(1, IRQ_PRI_NUM1);
// ... 临界区代码 ...
plic_exit_critical_sec(1, r);
WFI 低功耗唤醒
CLIC 架构的 WFI 流程与 PLIC 相同,使用相同的 plic_irqs_preprocess_for_wfi / core_entry_wfi_mode / plic_irqs_postprocess_for_wfi 接口。
IRQ
IRQ 是 TC 系列采用的中断模型,基于 TC32 核心。无独立中断控制器,所有中断源共用一个入口,无优先级、无抢占、无 claim/complete。
中断使用
两级中断开关:
// 第一级:全局中断使能
irq_enable();
// 第二级:中断源 mask
irq_set_mask(FLD_IRQ_DMA); // 以 DMA 中断为例
实现 ISR(所有中断共用一个 irq_handler 入口):
_attribute_ram_code_sec_noinline_ void irq_handler(void)
{
if (dma_chn_irq_status_get(FLD_DMA_CHN_SAR_ADC))
{
// 1. 处理中断
sd_adc_rx_done_flag = 1;
adc_stop_sample_dma();
// 2. 清除中断状态
dma_chn_irq_status_clr(FLD_DMA_CHN_SAR_ADC);
}
}
通用接口:
irq_enable()/irq_disable()/irq_restore():全局中断开关irq_set_mask()/irq_disable_type():中断源使能/禁用irq_get_src()/irq_clr_sel_src()/irq_clr_src():查询/清除中断源
各芯片的中断源 mask 定义在对应 register.h 中,以 FLD_IRQ_XXX_EN 宏形式给出(如 FLD_IRQ_DMA_EN、FLD_IRQ_ZB_RT_EN、FLD_IRQ_TIMER0_EN 等),请参考对应芯片的头文件。
临界区保护
TC 系列无优先级概念,临界区直接关闭全局中断:
unsigned char r = irq_disable();
// ... 临界区代码 ...
irq_restore(r);
irq_disable() 和 irq_restore() 必须成对使用。
ISR 实现要点
以下要点适用于 PLIC、CLIC、IRQ 三类架构:
- ISR 函数必须放入
.ram_code段(PLIC/CLIC 用_attribute_ram_code_sec_,IRQ 用_attribute_ram_code_sec_noinline_),保证中断响应实时性 - ISR 内部必须先查询并清除外设中断状态标志位,再执行业务逻辑
- ISR 内代码应尽量精简,尽量避免函数调用(减少寄存器入栈,加快响应),避免关闭中断或让中断处理时间过长(BLE 协议栈中断最大延时 200us,建议 ISR 控制在 50us 内)
- 同一个中断号只能注册一次 ISR(PLIC/CLIC);若未注册,默认执行
default_irq_entry(死循环) - IRQ 架构无优先级和抢占,中断处理期间全局中断关闭,不支持嵌套
- PLIC 架构下 ISR 中禁止调用
plic_set_threshold()、plic_set_priority()、plic_interrupt_enable()、plic_interrupt_disable()、plic_irqs_postprocess_for_wfi()原因见优先级与抢占。
DMA
概述
DMA(Direct Memory Access)无需 CPU 干预即可在内存与外设之间搬运数据。
Telink 平台提供两种 DMA:
| 类型 | 通道绑定 | 传输模式 | 中断 | 链表 | Write Num | Burst |
|---|---|---|---|---|---|---|
| 增强 DMA | 软件可配 | Normal / Handshake | TC / ERR / ABT | ✓ | ✓ | 因芯片而异,见资源表 |
| 基础 DMA | 硬件固定 | — | 仅 TC | — | — | — |
- TC(Transfer Complete):传输完成中断
- ERR(Error):总线错误 / 地址未对齐 / 宽度不匹配
- ABT(Abort):软件主动中止传输
大部分外设(UART/SPI/Audio/RF 等)的 DMA 传输已由外设驱动封装,用户直接调用外设接口即可。本章重点说明用户直接配置 DMA 时需要关注的概念和注意事项。
核心概念
| 概念 | 说明 |
|---|---|
| Normal 模式 | 内存到内存传输,无需硬件握手,DMA 一次性搬完指定长度。 |
| Handshake 模式 | 内存与外设 FIFO 交互,硬件握手控制节奏——FIFO 有空位才写、有数据才读。 |
| Burst | 一次仲裁周期内连续传输的 Word 数,增加 Burst 可减少总线仲裁开销。仅 Audio/SPI 等高吞吐外设受益于大 Burst。 |
| Write Num | 接收完成后,硬件自动将实际接收长度写入目的地址前 4 字节,适合 RF 等变长接收场景。 |
| 链表 | 多节点自动顺序传输。描述符链表存于内存,当前节点完成后硬件自动加载下一节点。 |
完成判断
DMA TC 中断触发时,数据仅搬入/搬出外设 FIFO,外设未必实际收发完成。以 UART 为例:
| 方向 | 判断方式 | 说明 |
|---|---|---|
| 发送 | 外设自身标志 | 数据从 FIFO 移出才算完成,如 UART 的 TX_DONE 中断或 busy 状态,而非 DMA TC。 |
| 接收 | 外设标志 或 DMA TC | 数据进入 FIFO 即可读,可用 DMA TC 判断一批数据到齐;变长接收用外设 rx_done 判断帧结束。 |
资源总览
各芯片 DMA 能力对照如下。
| 芯片 | 类型 | 通道数 | Burst | 链表 | Write Num |
|---|---|---|---|---|---|
| B80 / B85 / B87 / B89 / TC321x / TC122x / TC123x | 基础 DMA | 8 | — | — | — |
| B80B | 基础 DMA | 12 | — | — | — |
| TC1211 | 基础 DMA | 2 | — | — | — |
| B91 | 增强 DMA | 8 | — | ✓ | — |
| B92 | 增强 DMA | 8 | 1/2/4 Word | ✓ | ✓(仅 DMA 长度=全 ff 时) |
| TL321x / TL721x / TL323x | 增强 DMA | 8 | 1/2/4/8 Word | ✓ | ✓ |
| TL322x / TL751x | 增强 DMA | 16 | 1/2/4/8 Word | ✓ | ✓ |
所有通道相互独立,可以同时使用。
增强 DMA 使用指南
增强 DMA 通道可软件配置,通过 dma_config_t 选择请求源。
使用模式:先调用外设的绑定函数(内部完成 dma_config_t 配置),再启动传输。
中断处理:支持 TC / ERR / ABT 三种中断。
当外设封装接口不满足场景时(如内存到内存传输),手动填充 dma_config_t。
关键字段含义:
| 字段 | 含义 |
|---|---|
src_req_sel / dst_req_sel |
外设请求源,取值见对应芯片 dma.h 中的 DMA_REQ_* 枚举 |
src_addr_ctrl / dst_addr_ctrl |
地址控制:INCREMENT(递增)/ FIX(固定)/ DECREMENT(递减,极少用) |
srcmode / dstmode |
传输模式:NORMAL_MODE(内存↔内存)/ HANDSHAKE_MODE(内存↔外设) |
srcwidth / dstwidth |
传输位宽:WORD_WIDTH |
与外设 FIFO 交互的规则:FIFO 端地址固定(FIX),SRAM 端地址递增(INCREMENT),模式用 Handshake。
外设传输宽度为 Word,缓冲区长度须为 4 的倍数。
链表模式
描述符链表存于内存,每个节点指定源地址、目的地址、数据长度和下一节点指针。当前节点完成后硬件自动加载下一节点。外设按自身配置切换节点,DMA 根据中断模式决定何时产生中断。
| 中断模式 | 行为 |
|---|---|
| CONTINUE_MODE | 连续传输,仅末节点完成后产生中断 |
| INTERRUPT_MODE | 每个节点完成后产生中断 |
| TERMINAL_MODE | 每个节点完成后自动停止,需软件重新触发 |
链表结构体成员必须声明为 volatile,否则编译器优化可能导致 PWM/Audio 异常。
注意事项
源地址和目的地址必须 4 字节对齐,否则触发异常。通道未完成时,必须先禁用再重新配置,否则写入无效。增强 DMA 可主动中止传输(产生 ABT 中断)。
基础 DMA 使用指南
基础 DMA 通道与外设硬件固定绑定,DMA 配置由外设驱动内部完成,用户直接调用外设接口即可。
使用模式:先使能外设的 DMA 模式,再启动外设的发送/接收接口。
中断处理:只有一个中断状态寄存器,各通道共用,写 1 清除。查询、清除分别由 API 完成,无需操作寄存器。
GPIO
概述
GPIO 模块支持通用输出与输入功能,可实现高低电平的输出和读取。同时,该模块支持引脚的外设功能复用,并具备可配置的内部上/下拉电阻。
工作模式
输出模式:推挽
默认状态下,引脚输出低电平为 0V,高电平接近 VBAT 电压。部分芯片的 I/O 域支持通过调整 VDDIO 供电,将输出高电平切换为 1.8V。
硬件不支持开漏输出模式。
输入模式
读取电平前需输入使能;不读取时应关闭。
引脚的输入电平识别采用百分比阈值判定:当输入电压低于 I/O 供电电压的 30% 时,系统识别为逻辑低电平;当输入电压高于 I/O 供电电压的 70% 时,识别为逻辑高电平;介于 30% 至 70% 之间的电压则处于不确定状态。
上下拉配置
芯片内置模拟上下拉电阻:
| 类型 | 阻值 | 适用场景 |
|---|---|---|
| 上拉 | 10K | 按键、I2C 等常规上拉 |
| 下拉 | 100K | 按键下拉、电平锚定 |
| 上拉 | 1M | 低功耗场景、SWS 调试引脚 |
| 悬空 | — | 已有外部上下拉时 |
模拟上下拉在运行及低功耗(Deep Sleep)状态下均有效。TL 系列额外提供数字上下拉,但仅在 Active 模式下有效,进入低功耗后失效。
这里的10k 100k 1M 是约数,不是准确值。
使用方法
引脚初始化(gpio_init)
GPIO 的推荐初始化方式是:在 gpio_default.h 中用宏配置每个引脚的属性,再调用 gpio_init() 一次性生效。该接口在所有芯片上均提供,是启动时配置 GPIO 的核心入口。
第一步:在应用层覆盖宏定义(以 PA0 作输出、PA1 作输入按键为例):
// app_config.h 或 gpio_default.h
#define PA0_OUTPUT_ENABLE 1 // PA0 输出使能
#define PA0_INPUT_ENABLE 0 // PA0 关输入使能
#define PA0_DATA_OUT 1 // PA0 初始电平高
#define PA0_FUNC AS_GPIO
#define PA1_INPUT_ENABLE 1 // PA1 输入使能
#define PA1_OUTPUT_ENABLE 0 // PA1 关输出
#define PA1_FUNC AS_GPIO
#define PULL_WAKEUP_SRC_PA1 GPIO_PIN_PULLUP_10K // PA1 上拉
可配置的宏包括:PXn_INPUT_ENABLE(输入使能)、PXn_OUTPUT_ENABLE(输出使能)、PXn_DATA_OUT(输出电平)、PXn_DATA_STRENGTH(驱动能力)、PXn_FUNC(功能复用)、PULL_WAKEUP_SRC_PXn(上下拉)。
第二步:调用初始化:
gpio_init(1); // 参数 1:同时初始化模拟上下拉电阻;0:仅初始化数字部分
参数说明:anaRes_init_en 控制是否初始化模拟上下拉电阻。模拟上下拉在睡眠(Deep/Deep Retention)后不丢失,无需重新设置。因此:
- 上电启动:传
1,同时初始化数字部分和模拟上下拉 - Deep/Deep Retention 唤醒后:传
0,仅初始化数字部分,跳过模拟上下拉以节省时间
运行时需动态改变某个引脚的方向或电平时,再用 3.2 的单引脚 API 操作;启动时的批量配置应优先用 gpio_init()。
基础 I/O
(1) 输出
// TL 系列
gpio_output_en(GPIO_PA0); // 1. 开启输出使能
gpio_set_high_level(GPIO_PA0); // 2. 设置电平
// TC 系列
gpio_set_output_en(GPIO_PA0, 1); // 1. 开启输出使能
gpio_set_high_level(GPIO_PA0); // 2. 设置电平
其他输出 API:gpio_set_low_level(pin)、gpio_toggle(pin)(翻转电平)。
顺序要求:先开启输出使能,再设置电平。
(2) 输入
// TL 系列
gpio_input_en(GPIO_PA0); // 1. 输入使能
unsigned char level = gpio_get_level(GPIO_PA0); // 2. 读取电平
// TC 系列
gpio_set_input_en(GPIO_PA0, 1); // 1. 输入使能
unsigned char level = gpio_get_level(GPIO_PA0); // 2. 读取电平
注意
- 顺序要求:读取电平前必须先输入使能,否则读到的值不确定。
- 关键:纯输出模式或悬空的引脚,务必关闭输入使能(
gpio_input_dis(pin)或gpio_set_input_en(pin, 0)),否则会导致漏电。
(3) 上下拉
// TL 系列
gpio_set_up_down_res(GPIO_PA0, GPIO_PIN_PULLUP_10K);
// TC 系列
gpio_setup_up_down_resistor(GPIO_PA0, GPIO_PIN_PULLUP_10K);
部分芯片还提供独立的 30k 上拉电阻接口(与上述模拟上下拉独立):
gpio_set_pullup_res_30k(GPIO_PA0); // 使能 30k 上拉
顺序要求:上下拉配置与输入/输出使能无先后依赖,但建议在使能前配置好上下拉,避免引脚悬空期间产生不确定电平。
复用功能
当 GPIO 需要连接内部外设(如 UART_TX、SPI_CLK)时,需切换为复用功能。TL 系列与 TC 系列接口不同:
// TL 系列:两步——切外设功能 + 关原生 GPIO 控制
gpio_set_mux_function(UART0_TX, UART_TX_PA3); // 1. 切换为外设功能
gpio_function_dis(UART0_TX); // 2. 关闭原生 GPIO 控制
// TC 系列:一步——gpio_set_func 直接切功能
gpio_set_func(UART0_TX_PIN, UART_TX); // 切换为外设功能
驱动能力
部分芯片提供驱动能力调节接口(如 gpio_set_data_strength(pin, value)),支持强/弱两档或更多档位。不同芯片支持的档位数量和 API 不同,具体见各芯片驱动头文件。
使用建议:默认驱动能力满足大多数应用。仅在驱动大负载电容、高速信号或长走线时需要调强;对功耗敏感的引脚可调弱。调节时注意过强驱动可能导致信号过冲和 EMI 问题。
时钟探针输出
gpio_set_probe_clk_function(PIN_CLK_OUT, CLK_32K);
可将内部时钟(32K、RC24M 等)路由到 GPIO,方便示波器抓取调试。
调试接口(JTAG / SDP)
部分芯片支持把 GPIO 切换为 JTAG(4 线:TDI/TDO/TMS/TCK)或 SDP(2 线:TMS/TCK)调试接口:
jtag_set_pin_en(); // 使能 JTAG(占用 PC4~PC7)
sdp_set_pin_en(); // 使能 SDP(占用 PC6~PC7)
注意
- JTAG/SDP 模式由上电或硬件复位时 PB0 电平决定(低电平→JTAG,高电平→SDP),软件无法改变该选择,只能通过外部电平输入。reboot 不会重新检测。
状态切换顺序
各场景的正确配置顺序,顺序错误会导致电平冲突、漏电或中断不触发。核心原则是避免中间出现错误状态(如同时使能输入和输出、或复用功能与 GPIO 同时输出):
| 场景 | 正确顺序 | 顺序错误的后果 |
|---|---|---|
| 普通 GPIO 输出 | 开输出使能 → 设置电平 | 未开输出使能直接写电平,引脚无输出 |
| 普通 GPIO 输入 | 开输入使能 → 读取电平 | 未开输入使能直接读,值不确定 |
| 纯输出引脚 | 开输出使能 →关输入使能 | 漏电 |
| 输入切输出 | 关输入使能 → 开输出使能 → 设置电平 | 输入输出同时使能,电平冲突 |
| 输出切输入 | 关输出使能 → 开输入使能 → 读取 | 输出残留驱动干扰读取 |
| 切换为复用功能 | 切 mux →关 GPIO 功能 | 电平冲突 |
| 切回普通 GPIO | 开 GPIO 功能 → 配置输入/输出 | 操作无效 |
| 中断使用 | 输入配置 → 绑定通道 → 开掩码 → PLIC 路由 → 全局使能 → ISR 清标志 | 链路不完整,中断不触发 |
| 启动防漏电 | gpio_init(1) → SWS 上拉 → 按需初始化引脚 |
引脚悬空漏电 |
| 休眠前 | 被配置为其他功能的引脚需确保固定电平(输出确定电平或上下拉) | 引脚悬空漏电 |
中断
GPIO 模块具备中断检测能力,通过合理配置 GPIO 中断,可以实现外部事件的实时响应。
中断类型
在芯片 RISC-V 架构中,根据硬件行为和寄存器控制逻辑,GPIO 中断主要分为以下4种类型:
- GPIO_IRQ
- GPIO2RISC0 ~ 7
- GPIO_GROUP_IRQ
- 特点:一旦确定了GPIO组,就只能在这个组里面挑选引脚了。接下来只需让引脚和中断号一一对应,比如选了A组,就把中断源0和PA0绑定、中断源1和PA1绑定,像这样进行对应。
- GPIO_IRQ_NUM0 ~ 7
各芯片支持的中断类型和数量:
| 芯片 | GPIO_IRQ | GPIO2RISC0 ~ 7 | GPIO_GROUP_IRQ | GPIO_IRQ_NUM0 ~ 7 | 中断个数 |
|---|---|---|---|---|---|
| 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 |
注意
- 如需跨分组的多引脚中断(如 PA0 和 PB0),改用 GPIO2RISC / RISC 内核通道或其他独立中断通道。
中断机制说明
上升沿触发:MCU 将 GPIO 的电平信号直接作为中断源,上升沿触发中断。

下降沿触发:MCU 将 GPIO 的电平信号取反后作为中断源,上升沿触发中断。

(1) 多 GPIO 共用同一中断源
- 两个 GPIO 均设为上升沿触发:MCU 将两个 GPIO 的电平信号做或运算,用结果信号作为中断源,上升沿触发。下图中仅 GPIO0 触发了中断。

- 两个 GPIO 均设为下降沿触发:MCU 分别将两个 GPIO 的电平信号取反后做或运算,用结果信号作为中断源,上升沿触发。下图中仅 GPIO0 触发了中断。

- GPIO0 上升沿 + GPIO1 下降沿:MCU 将 GPIO1 取反后与 GPIO0 做或运算,用结果信号作为中断源,上升沿触发。下图中仅 GPIO1 触发了中断。

结论: 多个 GPIO 共用同一中断源时,触发情况不确定,不推荐使用。但不同中断源之间相互独立,可同时使用。例如 GPIO0 配置为 IRQ0、GPIO1 配置为 IRQ1,均上升沿触发,两者均可正常触发中断。

各芯片实际可用的中断个数,请参见 中断类型 表格(中断个数列)。
中断示例(以 TL721x 芯片为例)
以下根据官方驱动接口,展示 TL721x(组选择源模式) 的规范配置方法:
##include "common.h"
#define IRQ_PIN GPIO_PA0 // 测试输入引脚
#define TEST_LED GPIO_LED1
volatile unsigned int gpio_src_irq0_cnt = 0;
/**
* @brief GPIO_SRC0 中断服务程序 (ISR)
*/
_attribute_ram_code_sec_noinline_ void gpio_src0_irq_handler(void)
{
gpio_src_irq0_cnt++; // 计数中断触发次数
gpio_set_high_level(TEST_LED); // 响应中断
// 清除组中断源 0 的状态标志位
gpio_clr_group_irq_status(FLD_GPIO_GROUP_IRQ0);
}
// 注册中断服务函数到 PLIC 向量表
PLIC_ISR_REGISTER(gpio_src0_irq_handler, IRQ_GPIO_SRC0)
void TL721X_gpio_irq_init(void)
{
// 1. 配置引脚基础通用属性与输入使能
gpio_function_en(IRQ_PIN);
gpio_output_dis(IRQ_PIN);
gpio_input_en(IRQ_PIN); // 开启输入使能
gpio_set_up_down_res(IRQ_PIN, GPIO_PIN_PULLUP_10K); // 配置内部 10K 上拉
// 2. 配置触发源与极性
gpio_set_src_irq_group(IRQ_PIN >> 8); // 选择引脚所在的 GPIO 组
gpio_set_src_irq(IRQ_PIN, INTR_FALLING_EDGE); // 配置为下降沿触发
// 3. 使能内核 PLIC 通道及全局中断
plic_interrupt_enable(IRQ_GPIO_SRC0);
core_interrupt_enable();
}
使用中断时建议用现成的api接口,并参考demo配置顺序,否则会因为配置顺序错误导致误触发等问题。
注意事项
数字上下拉休眠失效
部分芯片提供数字上下拉接口(如 gpio_set_digital_pullup / gpio_set_digital_pulldown),但数字上下拉仅在 Active 模式下有效,进入低功耗后失效。因此休眠唤醒源引脚、休眠期间需保持电平的引脚,必须使用模拟上下拉(gpio_set_up_down_res)。
唤醒源电平保持
部分芯片的特定引脚在 Deep Retention 睡眠模式下无法稳定保持逻辑电平(具体引脚见各芯片头文件 gpio.h 中 GPIO 枚举的注释)。这些引脚严禁用作休眠唤醒源,且外部不得连接对电平保持有严格要求的器件。
SWS 调试引脚
芯片通过 SWS 接口(通常为 PA7,部分平台为 PA3)进行程序下载与仿真。SWS 引脚若处于悬空状态,外部干扰可能使其电平跳变,导致数字电路漏电,甚至误写芯片寄存器造成死机。
处理方式:程序启动后尽早对 SWS 施加 1M 模拟上拉,使引脚保持固定电平:
gpio_set_up_down_res(GPIO_SWS, GPIO_PIN_PULLUP_1M);
// TC 系列:gpio_setup_up_down_resistor(GPIO_SWS, GPIO_PIN_PULLUP_1M);
默认上下拉配置
gpio_init(1) 会按 PULL_WAKEUP_SRC_PAx(x 为引脚号)宏的取值统一设置所有引脚的模拟上下拉。该宏在 gpio_default.h 中有默认值,部分芯片默认值为下拉(如 GPIO_PIN_PULLDOWN_100K),部分芯片默认为悬空(GPIO_PIN_UP_DOWN_FLOAT)。
宏取值含义:
| 宏值 | 含义 |
|---|---|
GPIO_PIN_UP_DOWN_FLOAT (0) |
悬空(无上下拉) |
GPIO_PIN_PULLUP_1M (1) |
1M 上拉 |
GPIO_PIN_PULLDOWN_100K (2) |
100K 下拉 |
GPIO_PIN_PULLUP_10K (3) |
10K 上拉 |
这里的10k 100k 1M 是约数,不是准确值。
注意
- 对于默认下拉的芯片,如果某个引脚实际用作其他功能(外设复用、输出驱动、连接外部上拉器件等),默认下拉会与实际功能冲突,导致电平异常或额外功耗。使用该引脚前必须把对应宏重定义为
GPIO_PIN_UP_DOWN_FLOAT取消下拉,重定义在应用配置头文件(app_config/*.h)中完成:
// 在 app_config 中取消某引脚的默认下拉
#define PULL_WAKEUP_SRC_PA0 GPIO_PIN_UP_DOWN_FLOAT
#define PULL_WAKEUP_SRC_PB3 GPIO_PIN_UP_DOWN_FLOAT
建议开发时先查阅当前芯片 gpio_default.h 的默认值,再根据实际使用的引脚在 app_config 中取消对应的下拉。
Analog
Analog概述
模拟寄存器(Analog Register)位于芯片模拟域,用于配置模拟相关参数(如 GPIO 输入使能/驱动强度、PLL、LDO 电压等)。与数字域寄存器(通过 reg_xxx 直接读写)不同,模拟寄存器需通过专用的 ALG 接口模块访问,Analog 驱动封装了这一访问过程。
驱动支持以下访问粒度:
| 粒度 | TL 系列 | TC 系列 |
|---|---|---|
| Byte(1 字节) | 支持 | 支持 |
| Halfword(2 字节) | 支持 | 不支持 |
| Word(4 字节) | 支持 | 不支持 |
| Buffer(批量) | 支持 | 不支持 |
说明
- TL 系列提供
analog_read_reg8/16/32、analog_read_buff等多粒度接口;TC 系列仅提供 byte 粒度的analog_read/analog_write(同时提供ReadAnalogReg/WriteAnalogReg宏别名)。
访问接口
接口命名
| 系列 | 读(byte) | 写(byte) |
|---|---|---|
| TL | analog_read_reg8(addr) |
analog_write_reg8(addr, data) |
| TC | analog_read(addr) |
analog_write(addr, v) |
TL 系列 byte 读写接口地址参数为 unsigned int(部分多 Bank 芯片通过地址高位选择 Bank,具体位定义见各芯片 analog.h);TC 系列地址参数为 unsigned char。
并发保护
Analog 读写接口内部已关闭中断(TL 使用 core_interrupt_disable,TC 使用 irq_disable),单次读写为原子操作,可在中断上下文中安全调用。
使用示例
/* 读-改-写:设置某 bit */
analog_write_reg8(0x3b, analog_read_reg8(0x3b) | BIT(7));
/* 直接写入 */
analog_write_reg8(0x3c, 0x5A);
存储模拟寄存器
根据几种典型的不同行为下寄存器数值是否保持,存储模拟寄存器分为两类:
| 分类 | power on | 32K watchdog | timer watchdog | reboot | deep | deep retention |
|---|---|---|---|---|---|---|
| PM_ANA_REG_WD_CLR_BUF | 恢复默认值 | 恢复默认值 | 恢复默认值 | 恢复默认值 | 保持 | 保持 |
| PM_ANA_REG_POWER_ON_CLR_BUF | 恢复默认值 | 恢复默认值 | 保持 | 保持 | 保持 | 保持 |
两类寄存器的核心差异在于 timer watchdog 和 reboot 场景:
- WD_CLR_BUF:任何复位都会恢复默认值,适合保存"所有场景都需重新初始化"的数据,避免旧数据干扰。
- POWER_ON_CLR_BUF:timer watchdog 和 reboot 时保持不恢复默认值,可用于区分"上电/32K 看门狗复位"与"timer 看门狗/软件复位"。
重要
- 各芯片的存储寄存器地址、数量、复位源、初始值均不同,使用时必须以各芯片
pm.h中PM_ANA_REG_*宏定义和注释为准,切勿跨芯片照搬地址。
使用前务必查阅 pm.h 中对应寄存器的注释说明,确认哪些 bit 可用。首次使用前建议先写入期望初始值,不要依赖默认值。
注意事项
-
DMA 模式不推荐:TL 系列 Analog 驱动提供 DMA 通道配置宏,但 DMA 配置返回后实际传输可能尚未完成,若此时中断中再次访问 Analog 寄存器会打断 DMA 传输,产生未知风险。不建议使用 DMA 读写 analog 寄存器。
-
write_buff 长度限制:TL 系列
analog_write_buff单次写入长度不能大于 8 字节。 -
bit 占用规避:访问存储模拟寄存器前,务必查阅
pm.h中对应寄存器的注释说明,确认目标 bit 未被驱动占用。 -
地址以 pm.h 为准:存储寄存器地址、复位行为、初始值均因芯片而异,切勿跨芯片照搬地址。
Flash
Flash 类型
内置 Flash
Telink IC 一般内置了多个不同厂商的 Flash,对某一个厂商的 Flash 也可能内置了不同类型(Flash Capacity 不同)。内置 Flash 型号不是静止不变的,可能会在某个时间点会增加新的类型,Platform SDK 会确保第一时间增加该型号 Flash 对应的 driver code,保障客户量产。
针对不同芯片内置 Flash 类型,用户可以按照以下方法查阅相关芯片的内置 Flash 类型,所看到的内容即是当前 SDK 所支持的所有 Flash 类型:
参考 chip/<型号>/drivers/flash/flash_type.h
Telink Platform SDK 根据 Flash datasheet 实现一些最基本功能,能够满足绝大部分用户的需求。Flash datasheet 上的内容是公开的,本文档不会重复介绍 datasheet 上的内容。所以建议客户在知道内置 Flash 型号后,主动去阅读 Flash datasheet,以了解详细的技术细节,一是可以在出现问题时具有更多的 debug 手段和信息(比如借助 Flash status 寄存器内容追查问题),二是用户可以自己开发实现一些非常规的功能(比如自定义保护策略)。
用户定制 Flash
对于所有的内置 Flash 类型,软硬件都做了充分的评估和测试。评估和测试内容除了读写擦基本功能,还有操作时间、高低温、上下电等多项数据、性能等。所以 Platform SDK 上包含的 Flash 型号都是可以放心使用的。
Telink 不建议客户增加新的 Flash 型号。如果客户一定要增加某一款 Flash,并不是简单的验证过读写擦基本功能正确就行。我们认为客户不具备对一款 Flash 进行全面评估和测试的能力。所以此时客户必须联系我们的 FAE 进行沟通,请我们原厂进行评估和测试。只有我们评估测试通过的 Flash 类型,才能在量产上使用。
芯片功能差异总表
各芯片在 Flash 功能上的差异汇总如下:
| 芯片 | 多 Slave | 四线模式 | 加密读写 |
|---|---|---|---|
| B80 / B85 / B87 | × | × | × |
| TC321X / TC122X / TC123X / TC1211 | × | × | × |
| B91 | × | √ | × |
| B92 | × | √ | √ |
| TL321X | × | √ | √ |
| TL323X | × | √ | √ |
| TL521X | × | √ | × |
| TL721X / TL322X / TL751X | √ | √ | √ |
STACK_SIZE_FOR_FLASH_DATA:buf 传入 Flash 地址时单次写入长度上限。TL 系列默认 256 字节,TC 系列默认 32 字节。
Flash 总线地址空间
通用地址映射规则:
- Flash 地址基础偏移:
FLASH_ADDR_BASE = 0x20000000 - 所有 Flash API 的
addr参数不需要加上基地址 0x20000000,范围从 0 到 Flash 存储空间大小。
多 Slave 设备地址空间:
支持最多 4 个 Flash 设备(SLAVE0~SLAVE3),总访问空间 64MB。通过 mspi_slave_device_addr_space_config() 接口划分各 Slave 的地址空间。Flash 接口的 addr 参数 = 实际 Flash 访问位置 + Slave 基地址。
注意
- Flash的总线地址空间布局图请参考basic module介绍。
各芯片多 Slave 支持情况
各芯片多 Slave 支持情况见"芯片功能差异总表"。
说明
- 以下 API 在多 Slave 芯片上均有对应的
_with_device_num版本,用于指定操作的 Slave 设备。为简洁起见,本文档以单 Slave API 为例进行介绍。
Flash 存储地址分配
Flash 存储空间是 SDK 和用户应用程序都能使用的,为确保用户在 Flash 使用上不出现冲突,对 Telink 各个 SDK 已经占用的 Flash 存储空间介绍如下。用户了解这些规则以后,尽量避开这些区域。
为了让 SDK 地址空间分配在 Telink 全系列芯片和全品类应用上更加方便统一管理,所有 Flash 地址分配规则都是基于 Flash Capacity 来划分的。
MAC Address 和 Calibration 区域的分配
Telink 所有 SDK 统一遵循以下地址分配。MAC Address 可用于 BT/BLE/Zigbee 等通用协议;由于其不重复的 ID 特性,在其他 SDK 和应用上,用户也可以根据自己的需求自行发挥。Calibration 可以参考对应章节的细节。

注意
- MAC Address 和 Calibration 功能地址分配是 Telink 全软件都遵循的标准,在所有 SDK(如 Platform SDK、BLE SDK、2.4G SDK 等)都是一样的。
Firmware Signature 功能 Flash 空间占用情况
Firmware Signature 是 Optional feature。
- 部分芯片支持硬件 Secure Boot 功能,其 Descriptor 会占用特定的存储空间,详情参考 Secure Boot 部分的介绍。
- 部分芯片上实现了软件 Firmware Signature 方案,会在特殊 Flash 地址存储 Signature 值,详情参考各 SDK 相关文档。
上层 SDK 额外占用
以上是基于通用 Platform SDK 角度了解的 Flash 占用情况。除此之外,各个通用协议 SDK(如蓝牙/Zigbee SDK)由于其功能需求(如 bootloader/OTA 功能、蓝牙配对绑定功能)会继续消耗一些空间作为它们的系统占用,用户请参考这些 SDK 的 Handbook 介绍去了解。
MID 和 UID
MID
Flash MID = Manufacturer ID(制造商 ID),是 SPI Flash 芯片的厂商识别码。
通过 flash_read_mid() 获取,包含容量和厂商信息。各字节含义如下:
| Byte | 含义 |
|---|---|
| 高 Byte | Flash 容量标识(见下表) |
| 中 Byte + 低 Byte | 厂商 + 工艺架构信息 |
例如 0x166085:
- 高 Byte
0x16:容量为 4MB - 低 16 位
0x6085:PUYA 厂商,SONOS 工艺
| MID 容量标识 | 对应 Flash 容量 |
|---|---|
| 0x10 | 64KB |
| 0x11 | 128KB |
| 0x12 | 256KB |
| 0x13 | 512KB |
| 0x14 | 1MB |
| 0x15 | 2MB |
| 0x16 | 4MB |
| 0x17 | 8MB |
| 0x18 | 16MB |
flash_vendor_e 定义
flash_vendor_e高8位是基于Mid的低16位Flash厂商和工艺架构信息,用于标识不同flash的工艺架构。以下是所有条目的汇总:
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;
注意
- 不同芯片支持的枚举条目不完全相同,具体以对应芯片的
flash.h为准。
读 mid API
unsigned int flash_read_mid(void);
使用示例:
g_flash_mid = flash_read_mid();
获取厂商 API
unsigned int flash_get_vendor(unsigned int flash_mid);
功能:根据 MID 获取 Flash 的厂商和工艺信息,返回 flash_vendor_e 类型值。该值包含了 Flash 的工艺类型(ETOX/SONOS/SST)和厂商信息。
厂商信息与软件功能关联
不同工艺 Flash 的特性差异:
| 工艺 | 特征 | 写入时间特性 |
|---|---|---|
| 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 工艺 Flash 按 byte 写入的时间也比 ETOX 大,基于 TC 系列芯片的 BLE SDK 时序设计上为了规避相关错误,需要做一些特殊处理,详情可以参考 BLE SDK 对应的 Handbook。用户的 SDK 或应用如果也存在类似情况,在设计时需要对做一些针对性的评估和处理。
Capacity 信息与软件功能关联
由 Flash 存储地址分配部分的介绍可知,MAC 地址、Calibration 功能都需要提前读到 Capacity 信息,才能去对应的地址进行相关操作。
UID
Flash UID = 闪存芯片唯一硬件标识符,是 SPI/xSPI NOR Flash 在出厂时由厂商激光/工艺固化的一串全球唯一标识码。
根据其唯一性和不可篡改的特点,可用来进行芯片身份认证、固件加密、防克隆、产品溯源、生产管控等。
读 Uid API
void flash_read_uid(unsigned char idcmd, unsigned char *buf);
功能:读取 Flash 的唯一硬件标识码(UID)。返回的 UID 长度取决于具体的 Flash 型号,通常为 16 字节。
UID 命令:不同厂商的 Flash UID 读取命令可能不同,多数厂商(PUYA、GD、ZB、TH)使用 0x4b 命令,少数厂商(XTX)使用0x5a 命令。 flash_read_uid API 需要用户显式传入 idcmd 参数,该参数区别不同厂商。
Mid 和 Uid 校验 API
int flash_read_mid_uid_with_check(unsigned int *flash_mid, unsigned char *flash_uid);
功能:同时读取 Flash 的 MID 和 UID,并校验其正确性。
返回值:
1:Flash 型号已知且 UID 读取成功0:Flash 无 UID 或非已知型号
说明:该函数遍历 SDK 支持的 Flash 列表,匹配 MID 后读取 UID,并检查 UID 是否为空(全为 0x5101 模式表示无 UID)。
在 Platform SDK 和一些上层 SDK 上(如 BLE SDK),针对部分芯片实现了软件 Firmware Signature 方案,使用到了 UID 的唯一性,作为 key 的一部分参与到 Signature 的计算。
Flash 基本操作
四线模式
Flash 四线模式相对两线模式,读写速度更快,但瞬间功耗更高。
各芯片四线功能支持情况见"芯片功能差异总表"。
使能四线模式,示例代码如下:
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(非加密)
(1) 标准读取 flash_read_page
extern flash_handler_t flash_read_page;
flash_read_page 是一个函数指针,默认指向标准读取函数,可通过 flash_change_rw_func() 切换为其他读取模式。
使用方式:
flash_read_page(addr, len, buf);
(2) 双线读取 flash_dread
_attribute_text_sec_ void flash_dread(unsigned long addr, unsigned long len, unsigned char *buf);
功能:使用双线(Dual I/O)模式从 Flash 中读取数据。
(3) 四线读取 flash_4read
_attribute_text_sec_ void flash_4read(unsigned long addr, unsigned long len, unsigned char *buf);
功能:使用四线(Quad I/O)模式从 Flash 中读取数据。
(4) 读操作注意事项
- 支持 Flash 容量内任意地址读取
- 读取长度
len不能为 0 - 最大读取长度不超过 RAM 可分配空间和 Flash 容量
Flash Write(非加密)
(1) 标准写入 flash_write_page
extern flash_handler_t flash_write_page;
与 flash_read_page 类似,flash_write_page 是一个函数指针,可通过 flash_change_rw_func() 切换。
(2) 单线页写入 flash_page_program
void flash_page_program(unsigned long addr, unsigned long len, unsigned char *buf);
功能:以标准 SPI 模式(单线)将数据写入 Flash。
(3) 四线页写入 flash_quad_page_program
void flash_quad_page_program(unsigned long addr, unsigned long len, unsigned char *buf);
功能:以四线(Quad)模式将数据写入 Flash,速度更快。
(4) 写操作注意事项
- 写入长度
len不能为 0 - 支持跨页写入,
len可以大于 256 字节(单页大小) buf指针应为 SRAM 地址,若传入 Flash 地址,驱动会自动将数据拷贝到栈上再写入,此时单次写入长度不能超过STACK_SIZE_FOR_FLASH_DATA(默认值见"芯片功能差异总表")- 不推荐使用 byte 写(<=255 字节)。如果用户想用一个或多个字节(<=255 字节)写,请按以下说明操作:
- 确保擦除后的 byte 写次数不超过 64 次
- 在一个 page 中,byte 写在 page 写后只能使用一次,并且只支持从"1"写为"0"
- 写入前必须确保目标区域已被擦除(擦除后为 0xFF),否则写入可能失败
Flash Erase
(1) 扇区擦除(4KB)
_attribute_text_sec_ void flash_erase_sector(unsigned long addr);
- 地址要求:必须是 0 或 0x1000 的倍数
- 擦除大小:4KB
(2) 32KB 块擦除
_attribute_text_sec_ void flash_erase_block_32k(unsigned long addr);
- 地址要求:必须是 0 或 0x8000 的倍数
- 擦除大小:32KB
(3) 64KB 块擦除
_attribute_text_sec_ void flash_erase_block_64k(unsigned long addr);
- 地址要求:必须是 0 或 0x10000 的倍数
- 擦除大小:64KB
注意
- 块擦除操作耗时较长,请提前注意喂狗。最大块擦除时间请参考 Flash datasheet。
加密读写
只有部分支持 Firmware Encryption 的芯片,才有加密读写功能。针对使用了 Firmware Encryption 的应用程序,读写操作必须使用加密读写相关的 API。
支持芯片:见"芯片功能差异总表"中的"加密读写"列。
(1) 加密写入
// 单线加密页写入
void flash_page_program_encrypt(unsigned long addr, unsigned long len, unsigned char *buf);
// 四线加密页写入
void flash_quad_page_program_encrypt(unsigned long addr, unsigned long len, unsigned char *buf);
功能:以加密模式将数据写入 Flash。写入的数据会被硬件加密引擎加密后再存储。
(2) 解密读取校验
// 双线解密读取校验
unsigned char flash_dread_decrypt_check(unsigned long addr, unsigned long plain_len, unsigned char *plain_buf);
// 四线解密读取校验
unsigned char flash_4read_decrypt_check(unsigned long addr, unsigned long plain_len, unsigned char *plain_buf);
返回值:
0:检查通过(读取解密后的数据与预期明文一致)1:检查失败
功能:从 Flash 读取加密数据,自动解密后与提供的明文缓冲区进行比较,用于验证加密写入的数据是否正确。
函数指针版本:
extern flash_read_check_handler_t flash_read_page_decrypt_check; // 解密读取校验函数指针
extern flash_handler_t flash_write_page_encrypt; // 加密写入函数指针
Flash 安全操作
在嵌入式系统中,Flash 擦除与写操作存在较高故障风险,因此 Flash 安全操作至关重要。Flash 采用 SPI 接口实现数据通信,尽管 SPI 协议本身具备良好的传输稳定性,但在 SoC 上下电、特殊工况运行,或是 Vbat 供电电压临近 Flash 工作阈值时,系统易出现大幅供电波动,进而引发 SPI 时钟、数据信号异常抖动,造成擦写地址或数据被非法篡改。其中 Flash 擦除属于高耗时操作,受干扰的时间窗口更长,风险也随之加剧。与此同时,若软件逻辑设计复杂、地址计算链路繁琐,再叠加开发过程中的人为疏漏,还会导致擦写地址、数据出现异常,甚至对固件分区执行非法覆写。上述问题一旦发生,将造成软件不可逆损坏,不仅导致单机功能失效,严重时还会引发产品拆机重烧固件、批量召回等问题,给企业带来高额运维成本与经济损失。
为保障应用产品 Flash 操作安全,Telink 从软硬件层面提供了多个有效的防护和规避措施。强烈建议用户使用这些措施,提升产品稳定性。
Flash 保护
(1) Flash 自身的保护功能
Flash 芯片自身提供 Status Register 保护机制,通过设置 Status Register 中的 Block Protect 位来实现不同区域的写保护。不同的 Flash 型号支持不同的锁定区域。
各 Flash 型号的保护接口遵循 flash_lock_<mid>(<lock_type>) 和 flash_unlock_<mid>() 命名规范,具体支持的 lock 类型定义在各 Flash 型号对应的头文件中(如 flash_mid146085.h)。
保护区域枚举示例:
// 不同 Flash 型号提供不同的 lock 区域选项,例如:
FLASH_LOCK_LOW_64K // 低地址 64KB 保护
FLASH_LOCK_LOW_128K // 低地址 128KB 保护
FLASH_LOCK_LOW_256K // 低地址 256KB 保护
FLASH_LOCK_ALL // 整片保护
使用示例:
// 1. 锁定 Flash
flash_lock_mid146085(FLASH_LOCK_LOW_64K_MID146085);
// 2. 验证锁定效果:擦除并写入,检查数据是否正确
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);
// 如果数据与预期一致,说明锁生效(被保护区域内容不变)
// 3. 解锁 Flash
flash_unlock_mid146085();
// 4. 验证解锁效果:擦除后读取应为 0xFF
flash_erase_sector(FLASH_ADDR);
flash_read_page(FLASH_ADDR + 0x80, FLASH_BUFF_LEN, (unsigned char *)Flash_Read_Buff);
// 如果数据为 0xFF,说明解锁成功
路径:各 Flash 型号的保护接口定义位于 chip/<型号>/drivers/flash/ 目录下的 flash_mid*.h 文件中。
(2) 芯片初始化时的 Flash 默认保护逻辑
芯片上电初始化时(platform_init),SDK 会自动执行 Flash 识别和默认保护逻辑,流程如下:
- 读取 MID,匹配 Flash 型号
hal_flash_init() 调用 flash_read_mid() 读取 Flash 的 MID,然后在 flash_list[](定义于 chip/<型号>/drivers/flash/flash_common.c)中遍历匹配。若未找到匹配项,初始化失败(返回非 0),程序进入 while(1) 死循环。
- 默认锁定一半空间
匹配成功后,hal_flash_lock() 检查当前 Flash 是否已加锁,若未加锁则调用 lock_func(lock_size) 默认锁定低地址一半空间。各容量对应的默认保护大小如下:
| Flash 容量 | 默认保护大小 |
|---|---|
| 512KB | 低 256KB |
| 1MB | 低 512KB |
| 2MB | 低 1MB |
| 4MB | 低 2MB |
| 8MB | 低 4MB |
| 16MB | 低 8MB |
注意
- 保护低地址区域即保护 Firmware 代码区,高地址区域留给用户数据读写。
- 匹配失败处理
若 flash_list[] 中无匹配的 MID,g_mid_matched 为 false,hal_flash_init() 返回 1,上层通过 while(1) 死循环指示 Flash 型号不被支持。
自定义保护大小
如需修改默认保护大小,可按以下步骤操作:
- 在
common.h中定义宏FLASH_PROTECT_MODIFY_CONFIG为 1 - 在
common.c的flash_init_list[]中按芯片型号添加目标 MID 和block_size - 仅保护 Firmware 区域,数据区不保护,避免频繁加解锁导致 Status Register 磨损
低电检测(Low Battery Detect)
电池电量检测(battery power detect/check),在 Telink SDK 和相关文档中也可能出现其他的名字,包括:电池电量检测(battery power detect/check)、低电池检测(low battery detect/check)、低电量检测(low power detect/check)、电池检查(battery detect/check)等。
本文档以"低电检测(Low Battery Detect)"这个名称进行说明。
使用电池供电的产品,由于电池电量会逐渐下降,当电压低到一定的值后会引起很多问题:
- 比如对于某个工作电压的范围为 1.8V~3.6V 的 SoC,电压低于 1.8V 时,无法保证稳定的工作。
- 当电池电压较低时,由于电源的不稳定,Flash 写和擦操作时,可能出现 SPI 总线数据抖动,造成 Flash 被异常改写。
使用电池供电产品的低电检测,主要功能点如下:
- 一般使用 ADC,在程序主循环中,周期性的进行 ADC 采样 VBAT 的值。 - 注意事项:为避免 ADC 检测受到电源抖动的影响(比如正好赶上 RF 收发),建议使用多个 ADC 采样数据进行一定的平滑滤波算法处理。
- 设定一个安全电压值(Secure Voltage),只有当电压高于这个安全电压的时候才允许 MCU 继续工作;一旦电压低于安全电压,MCU 停止运行,需要立刻被 shutdown(可以使用进入 suspend、deepsleep 等 mode 来实现)。
当前 Platform SDK 没有提供低电检测的参考设计。Telink 上层 SDK,部分 SDK 添加了低电检测的参考设计(比如 BLE SDK),用户可以参考和实现;其他没有这个参考设计的 SDK,用户可根据以上主要功能点自行开发实现。
上下电保护功能
(1) 功能背景
SoC 上下电期间 Flash 风险:
- 上电瞬间:电源未稳定、SPI 时钟/数据抖动,MCU 误发擦写命令,写坏固件/引导区。
- 掉电过程:擦除(耗时几十~几百 ms)或编程中途断电,块/页半写、数据错乱、Flash 变砖。
- 电压临界:Vbat 接近 Flash 工作阈值,SPI 信号抖动,地址/数据被篡改。
- 反复上下电:电源纹波 + 频繁复位,加剧时序错误、磨损加剧。
基于 ADC 的低电检测方案保护的是程序主循环时间内的 Flash 操作安全,无法覆盖 MCU 上下电阶段的保护。所以 Telink 逐步引入 LPC(低电比较)功能,针对 SoC 上下电进行保护,同时该保护也能兼顾程序主循环时间。
(2) 方案演进,在芯片上的差异
部分芯片提供上下电保护方案,其他老款芯片由于历史原因没有该功能,用户可以参考已有方案自主开发实现相同的功能。各芯片方案差异如下表:
| 芯片型号 | 方案类型 | 需软件介入 | 触发方式 | 行为 |
|---|---|---|---|---|
| TL321x | 软件方案 | 是 | LPC 中断 | 低压时触发中断,在中断中拉住 MCU 和 MSPI |
| TL322x | 硬件方案 | 否 | LPC → PEM → DMA | 低压时自动触发,通过 PEM 通道 + DMA 写寄存器拉住 MCU 和 MSPI |
| TL323x | 硬件方案(优化) | 否 | LPD 模块直接检测 | 新增 LPD 模块直接检测 VBAT,电压低于 1.7V 时硬件自动触发拉住 MCU 或 MSPI(可选),默认同时拉住 |
(3) 不同方案细节
TL321x(软件方案)
- 配置 PB1~7 中任一个 GPIO 输出高电平(选中后不可作为其他功能)
- 通过 LPC 检测该 PBx 引脚电平,间接检测 VBAT 电压
- 当电压低于约 2.0V 时触发 LPC 中断,在中断服务程序中拉住 MCU 和 MSPI
启用该功能的影响范围:
- 芯片供电范围:仅支持 2.1V - 4.2V。
- 中断抢占:总中断使能
core_interrupt_enable()需用户自行调用且应尽早调用,以最大限度地延长 Flash 保护的持续时间。 - 中断优先级:LPC 中断优先级(用于 Flash 下电保护功能)> Flash 操作优先级 > 其他中断优先级。
- GPIO 占用:需占用一个 GPIO,默认为 PB4,用户可根据实际应用在 PB1~7 中任意选择。一旦被选中,该 GPIO 不能用于其他功能。
TL321x该功能默认不启用,用户需根据实际应用场景,且能接受以上的影响范围,才考虑启用。
参考代码路径:\demo\vendor\Flash_Demo中的FLASH_LPC_PROTECT_MODE
TL322x(硬件方案)
- 同样配置 PB1~7 中任一个 GPIO 输出高电平(选中后不可作为其他功能)
- 通过 LPC 检测该 PBx 引脚电平,间接检测 VBAT 电压
- 当电压低于约 2.0V 时,LPC 通过 PEM 通道触发 DMA 写寄存器
- 由 DMA 硬件自动将 MCU 和 MSPI 拉住,无需软件中断介入
启用该功能的影响范围:
- 芯片供电范围:仅支持 2.1V - 4.2V。
- GPIO 占用:需占用 PB1~7 中的一个 GPIO,该 GPIO 不能用于其他功能。
- PEM 占用:需占用一个 PEM 通道,该 PEM 不能用于其他功能。
- DMA 占用:需占用 DMA0~7 中的一个通道,该 DMA 不能用于其他功能。。
TL322x该功能默认不启用,如果用户需要主频 D25F CLOCK > 96MHz 的高速应用场景,且能接受以上的影响范围,为了提高芯片在高速运行时的稳健性,必须启用该功能,请联系 Telink FAE 获得支持。
功能接口路径:\chip\tl322x\drivers\lpc.c中的lpc_pem_flash_prot_config()
TL323x(优化硬件方案)
- 新增 LPD 模块,直接检测 VBAT 电压,无需 GPIO 间接检测
- 当 VBAT 电压低于约 1.7V 时,硬件自动触发拉住 MCU 和 MSPI
- 全程无需软件介入,响应更快、更可靠
TL323x该功能默认启用,用户无需配置,即可直接使用。
注意
- 芯片工作过程中,电源端供电波动、第一次上电或睡眠唤醒时的电压毛刺,均可能导致电压低于触发阈值,误触 LPC/LPD 保护功能,使芯片卡住。使用以上方案时,用户必须启用 32K Watchdog 或 Timer Watchdog,防止误触发导致芯片长时间无法恢复。
电源管理系统
MCU正常执行程序时处于working mode,电流会在mA级别。当PM使其进入低功耗sleep mode时,电流仅会处在uA级别,这大大降低了电量的消耗。在低功耗模式下,除了唤醒需要的模块和时钟以外,其他的模块和时钟全部断电,部分需要维持电压的模块使用的也是低功耗的LDO维持电压,不能正常工作。
功能说明
五种低功耗模式
低功耗模式(low power mode)又称sleep mode,有五种:
- Suspend:暂停模式
- Deep sleep without SRAM retention(简称DeepSleep):深度睡眠无SRAM保持
- Deep sleep with SRAM retention(简称DeepSleep Retention / Retention):深度睡眠带SRAM保持
- WFI(Wait For Interrupt):等待中断模式
- Shutdown:关断模式
Suspend:
Suspend模式下,程序停止运行,类似一个暂停功能,当Suspend被唤醒后,程序继续执行。Suspend模式下,PM模块正常工作,SRAM不掉电(数据不丢失),全部的模拟寄存器不掉电,少量的数字寄存器掉电。为了节省功耗,软件可以设置把RF/USB/Audio等模块掉电,此时这几个模块相应的部分数字寄存器会丢失,例如RF在唤醒后需要重新进行初始化才能发包,其余寄存器均不丢失。如果想要在唤醒后直接能够发包,可以不设置相应模块掉电,但是相应的功耗会增加。可以通过IO、Timer等方式唤醒。
DeepSleep:
DeepSleep模式下,程序停止运行,MCU绝大部分的硬件模块都断电,当DeepSleep被唤醒后,MCU从硬件bootloader重新启动(等同于重新上电),程序重新开始初始化。DeepSleep模式下,PM模块正常工作,SRAM掉电数据丢失,大部分3.3V模拟寄存器保持,其他模拟寄存器和全部数字寄存器掉电丢失。可以通过IO、Timer等方式唤醒。
DeepSleep Retention:
DeepSleep模式电流很低,但是无法存储SRAM信息;Suspend模式SRAM和寄存器可以保持不丢,但是电流偏高。为了实现一些需要睡眠时电流很低又要能够确保睡眠唤醒后能立刻恢复状态的应用场景,增加了DeepSleep Retention模式。DeepSleep Retention模式更接近DeepSleep模式,与DeepSleep的唯一区别是可以选择保留部分SRAM,保存的SRAM越大,功耗就越大。
DeepSleep Retention模式下,程序停止运行,MCU绝大部分的硬件模块都断电,当DeepSleep Retention被唤醒后,MCU从软件bootloader开始运行(非硬件bootloader),程序重新开始初始化。DeepSleep Retention模式下,PM模块正常工作,SRAM保持部分空间不掉电,其他都掉电,大部分3.3V模拟寄存器保持,其他模拟寄存器和全部数字寄存器掉电丢失,比DeepSleep模式增加的电流值就是保持SRAM所消耗的电流值。可以通过IO、Timer等方式唤醒。相比DeepSleep模式,由于DeepSleep Retention会保存SRAM,因此醒来之后可以直接从保留SRAM启动,省去从Flash搬代码/数据到RAM的动作,唤醒更快。
WFI:
WFI(Wait For Interrupt)即等待中断模式,是最轻量的低功耗模式。MCU仅暂停指令执行(CPU时钟门控),不对任何模块做断电处理,所有供电和时钟维持不变,SRAM、数字寄存器、模拟寄存器全部保持不丢失,各外设控制器也不复位。当任意一个已使能的中断触发时,MCU立即从WFI指令之后继续执行,不需要重启,也不需要任何恢复/重新初始化操作。
WFI功耗在五种低功耗模式中最高,仍低于正常工作模式,但唤醒延迟最小。适用于需要快速响应中断的短时间空闲场景。
使用注意事项:
- 进入WFI前,必须确保至少有一个中断源已使能,否则MCU将永久挂起无法唤醒。
- 如果使能了看门狗,WFI期间无法喂狗,需确保唤醒周期小于看门狗超时时间。
Shutdown:
Shutdown(关断)模式是功耗最低的一种低功耗模式。Shutdown模式下,MCU全部模块断电,包括SRAM、全部的数字寄存器、全部的模拟寄存器均掉电丢失,PM模块自身也断电,仅保留极少数模拟唤醒电路维持工作。唤醒源仅支持PAD唤醒,其他唤醒源(Timer/MDEC/LPC/CORE)均不可用。只有部分芯片支持Shutdown模式,具体参考对应芯片的枚举 pm_sleep_mode_e 或 SleepMode_TypeDef。
当Shutdown被唤醒后,MCU等同于一次全新上电,从硬件bootloader开始重新执行,所有SRAM和寄存器都需要重新初始化。适合需要长时间极低功耗待机的场景。
使用注意事项:
- Shutdown唤醒后所有状态丢失,进入Shutdown前需将需要保留的数据持久化到外部存储(如Flash)。
- 仅PAD可唤醒,进入Shutdown前必须配置好PAD唤醒源及其上下拉。
区别:
WFI与Suspend的区别:
- WFI不调用睡眠函数,不进入PM模块的电源管理流程,不切换LDO、不断电任何模块;Suspend会通过PM模块对部分模块做断电及LDO切换。
- WFI的唤醒源是任意已使能的中断(如GPIO中断、Timer中断、UART中断等),不限于PM定义的唤醒源。
- WFI唤醒后无需任何恢复操作,直接继续执行;Suspend唤醒后可能需要恢复部分被掉电的模块(如RF)。
Shutdown与DeepSleep的区别:
- Shutdown断电范围比DeepSleep更彻底,DeepSleep模式下PM模块和大部分3.3V模拟寄存器仍保持,Shutdown则全部断电。
- Shutdown仅支持PAD唤醒,DeepSleep还支持Timer等其他唤醒源。
- 两种模式唤醒后,均从硬件 Bootloader 开始启动并重新完成初始化,但 Shutdown 模式的唤醒延迟更高。
DeepSleep与DeepSleep Retention的区别:
- DeepSleep模式下SRAM全部掉电;DeepSleep Retention模式下SRAM保持部分空间不掉电,可按核分别指定各核SRAM保持大小。
- 从 Deep Sleep 模式唤醒后,芯片从硬件 Bootloader 重新启动,并从 Flash 启动。从 Deep Sleep Retention 模式唤醒后,芯片从软件 Bootloader 开始启动,并可直接从保留的 SRAM 启动,无需重新从 Flash 加载代码和数据,从而降低唤醒延迟。
低功耗模式工作流程
不同的睡眠模式,MCU的运行流程不一致。下面详细介绍WFI、Suspend、DeepSleep、DeepSleep Retention、Shutdown五种睡眠模式被唤醒后的MCU运行流程。请参考下图。

流程图中各模块说明:
- 运行硬件bootloader:MCU硬件执行固化在ROM中的固定操作,软件无法修改,一般包含唤醒flash等操作。不同芯片系列ROM实现有所不同。
- 运行软件bootloader:硬件bootloader运行结束之后,开始运行软件bootloader,即中断向量段之后的启动汇编代码,作用是为C语言程序运行设置好内存环境,完成内存初始化。不同芯片系列启动流程有所差异:DeepSleep唤醒一般从Flash启动,DeepSleep Retention唤醒可直接从保留SRAM启动。
- 系统初始化:对应main函数中
sys_init、clock_init等各硬件模块初始化,设置各硬件模块的寄存器状态。 - 用户初始化:对应
user_init等用户初始化函数。 - main_loop:初始化完成后进入while(1)主循环,主循环中进入sleep mode之前的操作称为"Operation Set A",sleep唤醒之后的操作称为"Operation Set B"。
各睡眠模式的流程分析:
| 模式 | 唤醒后行为 | SRAM | 寄存器 | 恢复要求 |
|---|---|---|---|---|
| 正常轮询 | 循环执行Operation Set A→B | - | - | 无 |
| WFI | 中断触发后直接从WFI指令之后继续执行 | 100%保持 | 100%保持 | 无 |
| Suspend | pm_sleep_wakeup()正常返回,继续执行Operation Set B |
100%保持 | 绝大多数保持 | 几乎不需要恢复 |
| DeepSleep Retention | 从软件bootloader开始运行 | 部分保持 | 绝大多数丢失 | 几乎全部重新初始化 |
| DeepSleep | 类似重新上电,从硬件bootloader重新运行 | 100%丢失 | 绝大多数丢失 | 全部重新初始化 |
| Shutdown | 全断电,从硬件bootloader重新运行 | 100%丢失 | 100%丢失 | 全部重新初始化 |
细化上表中的寄存器部分,五种低功耗模式下SRAM、数字寄存器、模拟寄存器状态如下:
| 模式 | SRAM | 数字寄存器 | 模拟寄存器(1V) | 模拟寄存器(3V) |
|---|---|---|---|---|
| WFI | 100%保持 | 100%保持 | 100%保持 | 100%保持 |
| Suspend | 100%保持 | C1 | 100%保持 | 100%保持 |
| DeepSleep Retention | C2 | 100%丢失 | 100%丢失 | 100%保持 |
| DeepSleep | 100%丢失 | 100%丢失 | 100%丢失 | 100%保持 |
| Shutdown | 100%丢失 | 100%丢失 | 100%丢失 | 100%丢失 |
C1:大多数寄存器保持不变,只有少数特殊寄存器例外。Suspend 下可选择是否保留 RF/AUDIO/USB 等模块供电,对应寄存器随之相应保持或丢失。
C2:DeepSleep Retention模式下SRAM保持大小可选,通过pm_sleep_mode_e枚举按核分别指定各核SRAM保持大小。不同芯片可选范围不同,具体参考对应芯片的枚举定义。
多核睡眠与唤醒
适用说明:本章仅适用于支持多核的芯片,具体哪些芯片支持多核请参考 Overview 章节。单核芯片无需关注本章。
多核芯片核分类:
- 主核:D25F,主控核,运行主程序,控制全芯片电源,主核不可单独断电
- 副核:DSP、N22(RISC-V协处理器),可单独断电;不同芯片集成副核不同,具体以驱动代码为准
基本设计原则
多核电源管理只有两种使用方式:
- 全芯片一起睡眠:主核调用
pm_sleep_wakeup()统一控制,所有核一起进入低功耗。Suspend模式下副核可选保持供电或断电;Retention模式下各核可分别指定SRAM保持大小(设为NONE等同于DeepSleep);DeepSleep/Shutdown模式下所有核都断电 - 主核运行副核单独省电:主核正常工作时,可让空闲副核进入WFI等待中断,或直接给副核断电,不影响主核运行
进入全芯片深度睡眠(DeepSleep/DeepSleep Retention/Shutdown)前,所有副核必须先进入WFI状态,否则可能导致总线异常。
使用方式一:全芯片一起睡眠
整个系统空闲时,主核统一配置全芯片进入相同低功耗模式,流程:
flowchart TD
A["副核完成任务<br/>->通过 mailbox 通知主核<br/>->副核进入 WFI"]
B["主核确认所有副核就绪<br/>->配置各模式下的副核电源"]
C["主核调用 pm_sleep_wakeup()<br/>->全芯片进入低功耗"]
D["唤醒<br/>->主核检查唤醒源<br/>->检查哪些副核被断电<br/>->重新初始化断电副核"]
A --> B
B --> C
C --> D
各模式下副核的行为:
| 睡眠模式 | 副核行为 |
|---|---|
| Suspend | 通过 pm_set_suspend_power_cfg() 配置副核保持供电或断电;保持供电的副核被中断(主核mailbox或者其他中断都可以)唤醒后直接继续执行,断电的副核需要重新初始化 |
| DeepSleep | 所有副核SRAM全部掉电,唤醒后必须重新初始化 |
| DeepSleep Retention | 通过 pm_sleep_mode_e 枚举分别指定主核/副核的SRAM保持大小(如 RET_MODE_SRAM_LOW384K_LOW512K_LOW128K 表示D25F保留384 KB、N22保留512 KB、DSP保留128 KB),副核SRAM设为NONE则等同于DeepSleep;唤醒后需要重新初始化 |
| Shutdown | 全芯片断电,唤醒后全部重新初始化 |
代码示例:
// Suspend模式:配置DSP断电,N22保持供电
pm_set_suspend_power_cfg(FLD_PD_DSP_EN, 0); // 0=Suspend期间断电
pm_set_suspend_power_cfg(FLD_PD_ZB_EN, 1); // 非0=Suspend期间保持供电(ZB是基带+N22共用电源域)
pm_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_PAD | PM_WAKEUP_TIMER, PM_TICK_32K, 2 * CLOCK_32K_TIMER_TICK_1S);
// DeepSleep Retention模式:通过枚举指定各核SRAM保持大小
pm_sleep_wakeup(RET_MODE_SRAM_LOW384K_LOW512K_LOW128K, PM_WAKEUP_PAD | PM_WAKEUP_TIMER, PM_TICK_32K, 2 * CLOCK_32K_TIMER_TICK_1S);
// 唤醒后处理:检查断电的副核,重新初始化
if (pm_get_suspend_power_cfg() & FLD_PD_DSP_EN) {
sys_dsp_init(DSP_FW_DOWNLOAD_FLASH_ADDR);
sys_dsp_start();
}
注意
- 不同芯片的
pm_sleep_mode_e枚举中DeepSleep Retention可选组合不同,具体参考对应芯片的pm.h。
使用方式二:副核单独睡眠(主核保持运行)
主核正常运行时,空闲副核可单独省电:
| 方式 | 操作 | 唤醒后处理 | 适用场景 |
|---|---|---|---|
| 副核进入WFI | 副核自行执行WFI指令 | 中断触发自动继续执行 | 短时间空闲,快速响应 |
| 主核给副核断电 | 主核调用电源控制API断电 | 需要重新下载固件启动 | 长时间空闲,深度省电 |
副核断电流程:
flowchart TD
A["副核完成任务<br/>->通过 mailbox 通知主核请求断电<br/>->副核进入 WFI"]
B["主核收到请求<br/>->调用pm_set_dig_module_power_switch()<br/>->给副核断电<br/>(主核继续运行,其他任务正常处理)"]
C["需要使用副核时<br/>->主核给副核上电<br/>->重新初始化固件"]
A --> B
B --> C
代码示例:
// 主核:给DSP断电
pm_set_dig_module_power_switch(FLD_PD_DSP_EN, PM_POWER_DOWN);
// 主核:需要使用DSP时,上电重新初始化
pm_set_dig_module_power_switch(FLD_PD_DSP_EN, PM_POWER_UP);
sys_dsp_init(DSP_FW_DOWNLOAD_FLASH_ADDR);
sys_dsp_start();
多核睡眠注意事项
- 必须同步:全芯片深度睡眠前必须通过mailbox确认所有副核已进入WFI,不可直接调用睡眠函数
- 断电前WFI:主核给副核断电前,必须先通知副核进入WFI,不可直接给运行中的副核断电,否则可能导致副核控制的外设状态不确定。
- 唤醒后检查:Suspend唤醒后必须通过
pm_get_suspend_power_cfg()检查副核断电状态,断电副核必须重新初始化 - GPIO防漏电:睡眠前必须确保所有GPIO不能浮空,否则会产生漏电增加功耗
唤醒源
唤醒源分为PAD唤醒、32k timer唤醒(分内部32k rc、外部32k晶体两种时钟源)、MDEC唤醒、LPC唤醒、CORE唤醒。
PAD唤醒
PAD唤醒即通过GPIO引脚唤醒。除了专用功能引脚、默认输出引脚和特殊引脚外,大多数引脚都可以作为唤醒引脚。具体芯片哪些引脚不能作为唤醒引脚,请参考代码注释。
如果同时配置了多个引脚作为唤醒引脚,唤醒后无法读取到具体是哪个引脚触发的唤醒(部分芯片支持查询,具体参考芯片寄存器定义)。
PAD唤醒默认没有滤波功能:高电平唤醒时,引脚电压达到GPIO高电平阈值(约VDDIO的70%)立即唤醒;低电平唤醒时,引脚电压达到GPIO低电平阈值(约VDDIO的30%)立即唤醒。
如果PAD信号波动较多,可能会被误唤醒。为避免误唤醒可以打开滤波功能:打开后会在PAD边沿、32k上升沿、32k下降沿三个时刻采样,三次采样都是唤醒电平才会触发唤醒。打开滤波后,要确保唤醒电平至少维持一个32k周期(约31.25us)才能保证可靠唤醒。打开滤波功能需要开启32k时钟,功耗会增加约0.4uA。
使用注意事项:
- PAD唤醒需要配置正确的上下拉,确保睡眠初始电平正确,且维持不变直到PAD触发唤醒,避免误触发
- 睡眠期间所有GPIO不能浮空,否则会产生漏电增加功耗
Timer唤醒
Timer唤醒即配置唤醒时间,定时时间到自动唤醒。使用32k时钟源,32k rc和32k xtal均可。
每当32k计数器计数到设定的tick值,唤醒源timer标志就会置位。即使没有使能timer唤醒,到设定tick值时标志也会置位,只是不会唤醒芯片。清除标志后,下一次计数到设定值时会再次置位。
使用注意事项:
- 32k rc标称频率是32000Hz,32k xtal标称频率是32768Hz
MDEC唤醒
MDEC即曼彻斯特解码模块,用于将输入的曼彻斯特码解码为二进制数据。使用MDEC唤醒需要开启32k时钟(32k rc和32k xtal均可),当曼彻斯特输入引脚接收到的数据与配置的唤醒命令值匹配时,触发唤醒。
LPC唤醒
LPC即低功耗电压比较器(Low Power Comparator),将经过比例系数缩放后的输入电压(输入电压*比例系数)与参考电压比较,输出比较结果。LPC有两种工作模式:
- Normal mode — 精度较高,功耗大,正常工作模式
- Low power mode — 精度较低,功耗小,低功耗模式,可作为唤醒源
低功耗比较器的比较结果可以作为唤醒源触发低功耗唤醒。
使用注意事项:
- 两种模式的参考电压配置寄存器相同,但实际电压值不完全相同,可能相差100mV左右
- 配置LPC后需要等待100us才能进入睡眠:开启LPC后需要1-2个32k tick计算比较结果,之前结果寄存器值是随机的,如果此时进入睡眠可能导致无法正常睡眠
- 进入睡眠时,输入电压与参考电压之差必须大于30mV,否则比较结果随机跳变,无法正常进入睡眠导致死机
CORE唤醒
CORE唤醒包括CORE GPIO、USB等数字模块唤醒,唤醒过程要求数字部分不能断电,因此仅支持Suspend模式。
USB唤醒触发条件:USB引脚DP、DM上有电压变化(检测到USB数据)。为避免误唤醒,配置USB唤醒前需要软件设置DP上拉、DM下拉,保证电平稳定。
注意
- 上电复位、DeepSleep/DeepSleep Retention唤醒后,CORE唤醒标志位会默认置位,需要软件忽略这个无效标志;只有Suspend唤醒后该标志位才是有效的。
供电
不同低功耗模式下各模块的供电情况如下:
| 供电情况 | 模块 |
|---|---|
| 睡眠期间断电 | RF模拟部分 / ANA(PLL等) / 24M RC / 24M XTAL |
| DeepSleep/DeepSleep Retention断电,Suspend不断电 | Digital Core / SRAM(非Retention部分) |
| DeepSleep断电,Suspend/DeepSleep Retention不断电 | Retention SRAM |
| 睡眠期间都不断电 | FLASH / GPIO / PM TOP |
驱动接口
睡眠函数
函数原型:
TL系列:
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系列:
cpu_sleep_wakeup(sleep_mode, wakeup_src, wakeup_tick)
cpu_long_sleep_wakeup(sleep_mode, wakeup_src, wakeup_tick)
功能说明:
配置芯片进入低功耗模式,可支持Suspend/DeepSleep/DeepSleep Retention/Shutdown四种低功耗模式,WFI模式使用另外的接口。
TL系列和TC系列睡眠接口的实现有所不同,TL系列只提供了一个接口,采用直接调用接口的方式。TC系列采用指针的方式,指针接口没有参数wakeup_tick_type,而是分别提供了两个指针接口:cpu_sleep_wakeup 固定选择参数 PM_TICK_STIMER,和 cpu_long_sleep_wakeup 固定选择参数 PM_TICK_32K。代码如下:
#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)
睡眠期间的时钟可以选择32k rc或32k xtal。TC系列和TL系列的方式不同。
TL系列通过调用 clock_32k_init 接口参数选择 CLK_32K_RC 或者 CLK_32K_XTAL 来区分,初始化接口中更新变量 g_clk_32k_src 的值,睡眠接口通过 g_clk_32k_src 的值来判断使用32k rc还是32k xtal,是一个接口兼容两种情况。
TC系列调用方式是在初始化时调用接口 blc_pm_select_external_32k_crystal() 或者 blc_pm_select_internal_32k_crystal() 来区分。睡眠接口是两个接口,通过前面初始化调用的接口来区分使用哪个睡眠接口。内部实现如下:
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)
参数说明:
| 参数 | 说明 |
|---|---|
sleep_mode |
睡眠模式选择,Suspend/DeepSleep/DeepSleep Retention/Shutdown |
wakeup_src |
唤醒源选择,PAD/CORE/TIMER/COMPARATOR,可多选 |
wakeup_tick_type |
tick类型选择。PM_TICK_STIMER(24M/16M) / PM_TICK_32K |
wakeup_tick |
唤醒的tick值 |
sleep_mode
- DeepSleep Retention模式下,不同芯片保持的RAM可选择的大小范围不同。
- 只有部分芯片支持Shutdown模式。
具体参考对应芯片的枚举 pm_sleep_mode_e 或 SleepMode_TypeDef。
wakeup_tick_type 和 wakeup_tick
用法说明:
- 如果wakeup_tick_type为PM_TICK_STIMER:wakeup_tick为当前stimer tick值 + 睡眠tick值(stimer)
- 如果wakeup_tick_type为PM_TICK_32K:wakeup_tick为睡眠tick值(32K)
范围说明:
| tick类型 | stimer=24M | stimer=16M |
|---|---|---|
| PM_TICK_STIMER | 当前tick + (48000~ 0xe0000000),约2ms ~ 156.59s | 当前tick + (32000~ 0xe0000000),约2ms ~ 234.88s |
| PM_TICK_32K | 64~ 0xffffffff,约2ms ~ 37hours | 同左 |
内部处理:
在Timer唤醒模式下,当设置的睡眠时间大于最大值时,直接返回状态值(即唤醒源)。
当设置的睡眠时间较小,则通过清除状态值的方式,在设置的时间到了后再退出睡眠函数,效果相当于延时,并没有真正进睡眠。
返回值:
返回唤醒源标志,指示实际是哪个唤醒源触发唤醒。
使用注意事项:
在进入睡眠时,如果唤醒条件是满足的(比如当前PAD引脚电平就是唤醒电平),程序会不进入睡眠,继续往下跑。
GPIO唤醒配置
函数原型:
TL系列:
void pm_set_gpio_wakeup(gpio_pin_e pin, pm_gpio_wakeup_level_e pol, int en)
TC系列:
void cpu_set_gpio_wakeup(GPIO_PinTypeDef pin, GPIO_LevelTypeDef pol, int en)
功能说明:
PAD唤醒时使用,配置唤醒引脚和唤醒电平。
参数说明:
| 参数 | 说明 |
|---|---|
pin |
唤醒引脚 |
pol |
唤醒极性,LOW / HIGH |
en |
使能(1) / 禁止(0) |
返回值:
无
Suspend期间部分模块的电源配置
函数原型:
TL系列:
void pm_set_suspend_power_cfg(pm_pd_module_e module, unsigned char on_off)
TC系列:
void pm_set_suspend_power_cfg(pm_suspend_power_cfg_e module, unsigned char on_off)
功能说明:
配置Suspend时各模块是否断电。默认全部power down以节省电流。如果power on,Suspend电流会增加,但唤醒后不需要重新初始化该模块。
多核芯片中,此接口还可配置副核(DSP/N22)在Suspend期间是否断电:断电的副核唤醒后需要重新初始化,保持供电的副核唤醒后直接继续执行。
注意
- DeepSleep/DeepSleep Retention/Shutdown模式下所有数字模块默认都会断电,不需要也不能通过此接口配置。
参数说明:
| 参数 | 说明 |
|---|---|
module |
可配置Suspend期间保持供电/断电的模块,如FLD_PD_USB_EN(USB)、FLD_PD_AUDIO_EN(音频)、FLD_PD_DSP_EN(DSP)、FLD_PD_ZB_EN(基带/N22)等,具体参考对应芯片的pm_pd_module_e枚举 |
on_off |
0=Suspend期间断电,非0=Suspend期间保持供电 |
返回值:
无
Demo说明
Demo的流程图如下:

流程图说明:
(1) 开始加2s的延时是为了保持可以通信,因为进入睡眠后swire(单线调试通信接口)就不通了,这样使用BDT烧录工具时容易active失败,不能烧录程序。
(2) 电流测试开始前将所有的IO引脚关闭防止漏电。
(3) CORE唤醒只支持Suspend睡眠模式,不支持DeepSleep模式和DeepSleep Retention模式。
(4) Suspend模式在睡眠前和唤醒后分别控制LED2亮灭来指示状态;DeepSleep和DeepSleep Retention模式进入睡眠时芯片掉电,LED自动熄灭。(LED只是为了指示状态)
(5) 因为RC时钟是不准确的,并且会随着温度而变化,所以一般需要定时校准。给出以下建议:
a) 24M RC:
可以每10s校准一次,睡眠之前校准,这个准确度会影响睡眠后晶振的起振时间,睡眠醒来后,硬件有用24M RC的时钟去kick crystal,时间越准确,则起振时间越快。
b) 32K RC:
在上电、DeepSleep起来会校准一次。因为PM中用的是tracing的方式(用16M去数32K的固定周期的时间),所以这里对于Timer唤醒的时间准确度是没有影响的。
如果是其他模块有用到32K RC,需要根据应用需求进行处理。
c) 32K xtal:
上电后都需要再重新kick。另外使用32K xtal的时候需要外面焊接电容。
Watchdog
概述
Watchdog(看门狗)是 MCU 系统中用于检测和恢复软件异常的重要机制。当软件因死循环、死锁等原因无法正常运行时,看门狗计时器溢出将触发系统复位,使芯片恢复到正常工作状态。
Telink 平台支持以下两种 Watchdog 类型:
-
Timer Watchdog(普通看门狗):基于系统定时器的数字看门狗,工作在 active mode,时钟源为 pclk。溢出后触发数字复位,复位范围与 software reboot 相同。大多数芯片均支持,具体见下表。
-
32k Watchdog:基于 32K 时钟源的看门狗,在 active/suspend/deep retention 模式下均可工作,溢出后触发类似上电的复位效果。硬件上电默认打开,睡眠期间持续工作。部分芯片支持,具体见下表。
芯片差异说明
Watchdog 功能差异总表
| 芯片型号 | Timer Watchdog | 32k Watchdog |
|---|---|---|
| B80 | √ | √ |
| B80B | √ | √ |
| B85 | √ | ✗ |
| B87 | √ | ✗ |
| TC321x | √ | √ |
| TC1211 | X | √ |
| TC122x | ✗ | √ |
| B91 | √ | ✗ |
| B92 | √ | √ |
| TL321x | √ | √ |
| TL322x | √ | √ |
| TL323x | √ | √ |
| TL721x | √ | √ |
| TL751x | √ | √ |
普通 Watchdog(Timer Watchdog)
TL系列代码说明
Timer Watchdog 基于 pclk 时钟源,仅在 active mode 下工作。以 TL系列为例,以下接口定义于 watchdog.h与 watchdog.c。
接口列表:
| 接口 | 功能 |
|---|---|
wd_start() |
启动看门狗 |
wd_stop() |
停止看门狗 |
wd_set_interval_ms(period_ms) |
设置看门狗触发时间(单位:ms) |
wd_clear() |
喂狗,清除看门狗计时计数 |
wd_get_status() |
获取看门狗溢出状态 |
wd_clear_status() |
清除看门狗溢出状态标志 |
TC系列代码说明
Timer Watchdog 基于系统时钟源,仅在 active mode 下工作,使用的是timer2,timer2既可以充当看门狗使用,也可以充当普通定时器使用。以TC系列为例,以下接口定义于 watchdog.h 与 watchdog.c。
接口列表:
| 接口 | 功能 |
|---|---|
wd_start() |
启动看门狗 |
wd_stop() |
停止看门狗 |
wd_set_interval_ms(period_ms, tick_per_ms) |
设置看门狗触发时间(单位:ms) |
wd_clear() |
喂狗,清除看门狗计时计数 |
wd_get_status() |
获取看门狗溢出状态 |
使用示例
// 初始化并启动看门狗
wd_set_interval_ms(1000, sys_tick_per_ms); // 设置 1s 触发
wd_start();
// 主循环中周期喂狗
while (1) {
wd_clear();
// user code
}
32k Watchdog
TL系列代码说明
接口列表:
| 接口 | 功能 |
|---|---|
wd_32k_start() |
启动 32k 看门狗 |
wd_32k_stop() |
停止 32k 看门狗 |
wd_32k_set_interval_ms(period_ms) |
按毫秒设置触发时间,内部自动选择合适的时钟档位并计算计数值。 |
wd_32k_set_target_value(clk_sel, target) |
直接按档位和计数值设置触发时间,适合需要精细控制的场景;它是 wd_32k_set_interval_ms 调用的内部接口,不建议用户使用。实际触发时间 = 档位单周期宽度 × 计数值 |
wd_32k_feed() |
喂狗 |
wd_32k_get_count_ms() |
获取当前计数值(ms) |
wd_32k_get_status() |
返回溢出状态。32k 看门狗复位返回后状态为 1,需调用 wd_32k_clear_status() 清除,否则会影响后续状态判断。软件复位 / deep / deep retention / 32k 看门狗复位返回后状态保持;power cycle / reset pin / vbus detect 返回后状态丢失 |
wd_32k_clear_status() |
清除溢出状态标志 |
TC系列代码说明
接口列表:
| 接口 | 功能 |
|---|---|
wd_32k_start() |
启动 32k 看门狗 |
wd_32k_stop() |
停止 32k 看门狗 |
wd_32k_set_interval_ms(period_ms) |
按 ms 设置触发时间 |
wd_32k_get_status() |
获取溢出状态 |
wd_32k_clear_status() |
清除溢出状态标志 |
32k Watchdog方案1、2
| 芯片型号 | 32k Watchdog 方案1 | 32k Watchdog 方案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 |
方案1和方案2的差异:
方案1:
- 无法获取当前的32k watchdog count值。
- 只能通过
wd_32k_set_interval_ms(period_ms)来达到喂狗效果,且喂狗前需要先关掉watchdog,配置完成后再打开。 - 配置target值的时候,需要先关掉 watchdog,配置完成后再打开。因为模拟寄存器不能一次写 4 个字节,只能写一个,写的过程中担心出现不确定的中间值,有可能导致预期以外的一次 32k watchdog reset,所以关掉以后再写。
方案2:
- 可以通过
wd_32k_get_count_ms()获取当前计数值(ms)。 - 可以不用关掉watchdog,直接使用
wd_32k_feed()来喂狗。 - 可以通过
wd_32k_set_interval_ms(period_ms)来设置触发时间,不需要用户额外调用开关 watchdog 的操作。
配置注意事项:
- 若 sleep 状态下无timer作为唤醒源,纯pad唤醒,因为没有时钟,则 32k 看门狗计数暂停。
- timer唤醒注意watchdog复位时间要晚于唤醒时间。
- 对于 OTP 产品,如果所有代码无法在 RAM 中执行,存在崩溃风险,因此需要启用 32K Watchdog。此接口必须放在 RAM code 中以降低风险。
- 针对使用方案1的芯片,配置触发时间时必须先关闭看门狗再配置,配置完成后再打开。使用方案2的芯片,
wd_32k_set_interval_ms内部已处理该流程,可直接调用。tl321x,虽然使用的方案1,但内部也已经处理该流程,也可直接调用。
使用示例:
方案一: 喂狗和设置复位时间步骤一样
// 按 ms 设置并启动
wd_32k_stop();
wd_32k_set_interval_ms(2000); // 设置 2s 触发
wd_32k_start();
//喂狗:
wd_32k_stop();
wd_32k_set_interval_ms(2000);
wd_32k_start();
方案二:
// 按 ms 设置并启动
wd_32k_set_interval_ms(2000); // 设置 2s 触发
//喂狗:
wd_32k_feed();
常见问题
Q: 如何知道是不是从 watchdog 复位回来的?
A: 可以调用 pm_update_status_info() 获取复位原因,通过 pm_status.mcu_status 变量判断是从 watchdog 复位还是其他原因唤醒回来的。详情请参考Power电源管理章节pm_update_status_info。
RF
概述
RF 模块概述
Telink 芯片采用单硬件引擎多协议复用设计,通过软件切换 RF 工作模式,实现 BLE / Zigbee / 2.4G Proprietary / ANT 等多种无线通信协议的复用。同一颗芯片在不同应用场景下,只需调用对应的模式切换接口,即可在 BLE、Zigbee、2.4G Proprietary 等协议之间灵活切换,无需额外的硬件改动。
芯片支持模式
不同芯片系列支持的模式有所差异,以下为总体支持的模式列表,具体以对应芯片的 SDK 头文件定义为准。
| 协议类型 | 支持模式 | 调制方式 | 说明 |
|---|---|---|---|
| BLE | 1M / 2M | GFSK | 标准 BLE 1Mbps / 增强 2Mbps |
| BLE | 500K / 125K(LE Coded) | GFSK | BLE Coded S=2 / S=8 远程模式 |
| Zigbee | 250K | O-QPSK | 符合 IEEE 802.15.4 标准(部分芯片不支持) |
| Zigbee | Hybee 1M / 2M / 500K | O-QPSK | Hybee 扩展模式(部分芯片支持) |
| 2.4G Proprietary | 250K / 500K / 1M / 2M | GFSK | 支持TPLL/SB帧格式 |
| 2.4G Proprietary(Generic) | 250K / 500K / 1M / 2M | GFSK | 可自定义帧格式的私有协议 |
注意
- TC321x / TC1211 / TC122x / TC123x 系列不支持 Zigbee 和 Hybee 模式。TLSR825x / TLSR827x/ TLSR8208系列芯片及 TL 系列芯片支持 Zigbee 和 Hybee 模式。
驱动文件结构
TL 系列芯片采用模块化驱动结构:
rf_common.h— 公共设置(初始化、频点、功率、Fast Settle、CRC 等)rf_ble.h— BLE 模式相关rf_zigbee.h— Zigbee / Hybee 模式相关rf_private.h— Proprietary 模式相关rf_dma.h— DMA 相关
TC 系列芯片驱动文件:
rf_drv.h— RF 公共 API 及模式设置接口rf.h— RF 底层驱动声明
无线通信协议帧格式
本章以 BLE 模式为例详细说明发包/收包在 RAM 中的排布格式,其余协议仅列出与 BLE 的差异。
BLE 帧格式(详细示例)
空中帧格式:
| 字段 | Preamble | Access Code | PDU | CRC |
|---|---|---|---|---|
| 长度 | 1 Byte(1M)/ 2 Bytes(2M) | 4 Bytes | 2~257 Bytes | 3 Bytes |
- Preamble: 1M 模式下 1 字节(0xAA),2M 模式下 2 字节
- Access Code: 4 字节,广播 Access Code 固定为 0x8E89BED6
- CRC: CRC-24,多项式 0x65b,初始值 0x555555
发包格式(RAM 排布):
| RAM Address | Content | Description |
|---|---|---|
| addr, addr + 1 | DMA_LEN_INFO | DMA 传输长度,低字节在前 |
| addr + 2, addr + 3 | DMA_LEN_INFO | DMA 传输长度,低字节在前 |
| addr + 4 | header0 | 参考 BLE 规范 |
| addr + 5 | header (payload length) | payload 长度,不含 3 字节 CRC |
| addr + 6 | data(0) | payload |
| ... | ... | payload |
| addr + 6 + (length - 1) | data(length - 1) | payload |
收包格式(RAM 排布):
| RAM Address | Content | Description |
|---|---|---|
| rba, rba + 1 | DMA_LEN_INFO | DMA 传输长度,低字节在前 |
| rba + 2, rba + 3 | DMA_LEN_INFO | DMA 传输长度,低字节在前 |
| rba + 4 | header0 | 参考 BLE 规范 |
| rba + 5 | header1 (payload length) | payload 长度 |
| rba + 6 | data(0) | payload |
| ... | ... | payload |
| rba + 6 + length | crc(0) | CRC byte0 |
| rba + 6 + length + 1 | crc(1) | CRC byte1 |
| rba + 6 + length + 2 | crc(2) | CRC byte2 |
| rba + 6 + length + 3 | r_tstamp[7:0] | 时间戳 byte0 |
| rba + 6 + length + 4 | r_tstamp[15:8] | 时间戳 byte1 |
| rba + 6 + length + 5 | r_tstamp[23:16] | 时间戳 byte2 |
| rba + 6 + length + 6 | r_tstamp[31:24] | 时间戳 byte3 |
| rba + 6 + length + 7 | pkt_fdc[7:0] | 频率偏移值低字节 |
| rba + 6 + length + 8 | pkt_fdc[10:8] | 频率偏移值高字节 |
| rba + 6 + length + 9 | pkt_rssi | 数据包 RSSI |
| rba + 6 + length + 10 | bit[0] | CRC 错误标志 |
| 〃 | bit[1] | SFD 错误标志 |
| 〃 | bit[2] | 链路层错误标志 |
| 〃 | bit[3] | 功率错误标志 |
| 〃 | bit[7] | NoACK 指示 |
收包解析宏:
| TL 宏 | TC 宏 | 说明 |
|---|---|---|
rf_ble_packet_crc_ok(p) |
RF_BLE_PACKET_CRC_OK |
判断收包 CRC 是否正确 |
rf_ble_packet_length_ok(p) |
RF_BLE_PACKET_LENGTH_OK |
判断收包长度是否正确 |
rf_ble_dma_rx_offset_rssi(p) |
N/A | RSSI值在包中的位置偏移 |
rf_ble_dma_rx_offset_freq_offset(p) |
N/A | 频偏信息在包中的位置偏移 |
rf_ble_dma_rx_offset_time_stamp(p) |
N/A | 收包timestamp信息在包中的位置偏移 |
rf_ble_dma_rx_offset_crc24(p) |
N/A | CRC Value在包中的位置偏移 |
注意
- DMA_LEN_INFO 的计算方式因芯片系列而异。TL 系列提供宏
rf_tx_packet_dma_len(data_len)计算 DMA 长度,计算公式为((data_len + 3) / 4) | ((data_len % 4) << 22)。TC 系列直接填入实际传输数据长度(不含 DMA_LEN_INFO 自身字节数)。其他相关的宏定义可以参考对应芯片的rf_drv.h/rf.h。
2.4G Proprietary 帧格式 — 与 BLE 的差异
Proprietary 支持 TPLL(Telink Proprietary Link Layer)和 SB(Shock Burst)两种子模式。
空中帧差异:
| 参数 | BLE | Proprietary TPLL / SB |
|---|---|---|
| Sync Word | Access code(4 Bytes 固定) | 可配 3~5 Bytes |
| Preamble | 1~2 Bytes | 可配 1~16 Bytes |
| CRC | 固定 CRC-24 | CRC-16 |
RAM 排布差异:
对比 BLE 发包格式:
- TPLL:addr + 4 处存放 Payload length(BLE 此处为 header0),addr + 5 开始即为 payload 数据。 DMA_LEN_INFO字段结构一致。
- SB:addr + 4 处直接开始 payload 数据,无 Payload length 字段,需通过
rf_fix_payload_len_set()设置固定长度。
对比 BLE 收包格式:
收包尾部附加信息与 BLE 结构相同(CRC -> 时间戳 -> 频率偏移 -> RSSI -> 错误标志),只是根据不同子模式使用对应的解析宏:
| TL 宏 | TC 宏 | 适用模式 |
|---|---|---|
rf_pri_tpll_packet_crc_ok(p) |
RF_TPLL_PACKET_CRC_OK(p) |
TPLL 模式 |
rf_pri_tpll_packet_length_ok(p) |
RF_TPLL_PACKET_LENGTH_OK(p) |
TPLL 模式 |
rf_pri_sb_packet_crc_ok(p) |
RF_SB_PACKET_CRC_OK(p) |
SB 模式 |
rf_pri_sb_packet_payload_length_get(p) |
RF_SB_PACKET_PAYLOAD_LENGTH_GET(p) |
SB 模式 |
注意
- TL系列其他相关位置偏移的宏定义可以参考对应模式的h文件,TC系列相关的宏定义可以参考rf_drv.h/rf.h。
Zigbee 帧格式 — 与 BLE 的差异
注意
- Zigbee 模式仅在部分芯片上支持,请参考对应芯片的datasheet。
空中帧差异:
| 参数 | BLE | Zigbee |
|---|---|---|
| Preamble | 1~2 Bytes | 4 Bytes |
| 同步标识 | Access Address(4 Bytes) | SFD(1 Byte,固定 0xA7,硬件自动识别) |
| 帧头 | 2 Bytes Header | 1 Byte PHR(低 7 位为 PSDU 长度) |
| CRC | CRC-24 | CRC-16 CCITT(多项式 0x1021,初始值 0x0000) |
RAM 排布差异:
对比 BLE 发包格式,Zigbee 在 addr + 4 ~ addr + 12 多了一段 MAC 层头部(Frame Control、Sequence Number、PAN ID、地址字段等),addr + 13 为 Payload length,addr + 14 开始为 payload 数据。关键差异:
- Zigbee / Hybee 模式没有 Access Code 概念,初始化时无需配置 Access Code
- SFD(0xA7)由硬件自动识别
对比 BLE 收包格式,Zigbee 收包尾部附加信息结构与 BLE 相同。
解析宏:RF_ZIGBEE_PACKET_CRC_OK(p)、RF_ZIGBEE_PACKET_LENGTH_OK(p)、RF_ZIGBEE_PACKET_RSSI_GET(p) 等,用法与 BLE 宏一致。
2.4G Generic 帧格式 — 与 Proprietary 的差异
Generic 模式是 Proprietary 模式的扩展,帧格式与 Proprietary 相比更加灵活,除了可以灵活配置CRC长度,多项式,初始值等参数外还可以根据需求设置packet filter功能。具体使用方式和细节可以参考RF_Demo中的相关代码。
注意
- Generic 模式仅在部分 TL 系类芯片上支持,具体请参考对应芯片的datasheet。
RF 收发工作模式与状态机
手动模式(Manual Mode)
在 Manual 模式下,TX、RX 的所有使用过程由软件流程控制。Manual 模式的特点是硬件仅执行最基本的收发操作,不自动管理状态切换、时序和重传。Manual 模式时序精度依赖软件实现。
(1) Manual TX
通过 rf_set_txmode() 进入 TX 模式经过延时settle时间(manual模式这个时间需要软件控制)之后,可直接调用 rf_tx_pkt() 发包,具体使用方式可以参考RF_Demo中的相关代码。
注意
- Manual TX 模式下,Settle 等待仅需在首次进入 TX 模式时等待一次,后续TX状态一直拉起。
(2) Manual RX
通过 rf_set_rxmode() 进入 RX 模式,经过延时settle时间(manual模式这个时间需要软件控制)后可进入actual接收状态,该模式下可连续进行收包。
注意
- Manual RX 模式下,Settle 等待仅需在首次进入 RX 模式时等待一次。收完一个包直接进入下一个收包状态。
自动模式(Auto Mode)
与 Manual 模式不同,Auto 模式通过设置trigger tick在指定时刻触发状态机,自动模式硬件状态机自动完成 Settle -> 收发 -> 回到 IDLE 的全流程,无需 MCU 干预。
支持以下 6 种核心状态机模式:
| 序号 | 模式 | 说明 | 触发 API |
|---|---|---|---|
| 1 | STX | 单次发送,触发后进入 TX settle,发包完成自动回到 IDLE | rf_start_stx(addr, tick) |
| 2 | SRX | 单次接收,触发后进入 RX settle,收包完成或超时自动回到 IDLE | rf_start_srx(tick) |
| 3 | PTX | 周期发送,支持自动重传和 ACK 接收 | rf_start_ptx(addr, tick) |
| 4 | PRX | 周期接收,收到数据后自动回复 ACK | rf_start_prx(addr, tick) |
| 5 | TX2RX | 先发后收,发送一个包后等待进入 RX;超时则退出 | rf_start_stx2rx(addr, tick) |
| 6 | RX2TX | 先收后发,收到数据后等待并回复一个包;超时则直接退出 | rf_start_srx2tx(addr, tick) |
(1) trigger tick 说明
各 Auto 模式的 tick 参数指定硬件触发操作的时间点。当系统定时器的当前值 ≥ tick 时,硬件自动触发对应的状态机操作。获取当前系统定时器值,加上适当延时即可得到触发时刻。
// tick 计算方式: rf_start_xxx(packet, 系统tick + delay_ticks)
// delay_ticks = 延时毫秒数 × 系统定时器每毫秒计数值
注意
- tick 参数仅在当前定时器值小于 tick 值时生效,当定时器值已达到或超过 tick 值时,硬件立即触发。
(2) STX — 单次发送
void rf_start_stx(void *addr, unsigned int tick)
| 参数 | 说明 |
|---|---|
| addr | 发包地址,必须四字节对齐 |
| tick | 触发 tick 值,达到时立即触发 |
状态机流程:

STX 的硬件流程为:到达 tick 时间点 -> TX Settle -> 发送数据包 -> 自动回到 IDLE。每调用一次 rf_start_stx 触发一次完整的发送流程。
// 中断处理中的典型流程:
中断触发(FLD_RF_IRQ_TX / FLD_RF_IRQ_CMD_DONE) ->
清除中断标志 ->
装入下一包数据 ->
rf_start_stx(next_packet, trigger_tick) // 预约下次发送
(3) SRX — 单次接收
void rf_start_srx(unsigned int tick)
| 参数 | 说明 |
|---|---|
| tick | 触发 tick 值 |
状态机流程:

SRX 的硬件流程为:到达 tick 时间点 -> RX Settle-> 等待同步字(Sync Word / Access Address)-> 若同步成功,接收完整数据包并进行 CRC 校验 -> 自动回到 IDLE。
(4) TX2RX — 先发后收
void rf_start_stx2rx(void *addr, unsigned int tick)
| 参数 | 说明 |
|---|---|
| addr | 发包地址,四字节对齐 |
| tick | 触发 tick 值 |
状态机流程:

TX2RX 是一个双向单次状态机,硬件流程为:到达 tick 时间点 -> TX Settle -> 发送数据包 -> 发包结束-> RX Wait->自动切换 RX Settle -> 等待接收对方的回复包/超时 -> 自动回到 IDLE。
(5) RX2TX — 先收后发
void rf_start_srx2tx(void *addr, unsigned int tick)
| 参数 | 说明 |
|---|---|
| addr | 回复包地址,四字节对齐 |
| tick | 触发 tick 值 |
状态机流程:

RX2TX 是 TX2RX 的互补模式,硬件流程为:到达 tick 时间点 -> RX Settle -> 等待同步字 -> 收到数据包 ->收包结束/或者超时回到idle->TX Wait->自动切换 TX Settle -> 发送回复包 -> 自动回到 IDLE。
(6) PTX — 周期发送
void rf_start_ptx(unsigned char *addr, unsigned int tick)
| 参数 | 说明 |
|---|---|
| addr | 发包地址,四字节对齐 |
| tick | 触发 tick 值 |
PTX 相关配置接口:
| 接口 | 说明 |
|---|---|
rf_ptx_prx_config() |
初始化 PTX / PRX 模式配置 |
rf_set_ptx_prx_ack_en() |
使能 ACK 功能 |
rf_set_ptx_prx_ack_dis() |
禁用 ACK 功能 |
rf_set_ptx_retry(retry_count, retry_delay) |
设置重传次数和重传延迟 |
PTX 模式下,硬件自动完成数据包组装(Preamble + Access Code + Header + Payload + CRC)、自动 ACK 接收、自动重传等全部事务处理,无需 MCU 参与。一次完整的事务处理(Transaction)定义为:PTX 发送数据包 -> 收到 PRX 的 ACK 包。
PID(Packet Identification)机制:
PTX 每发送一个新数据包,PID 字段自增 1(2-bit,范围 0~3)。PRX 通过 PID + CRC 组合判断收到的包是新包还是重传包,从而避免向应用层重复投递。即使 CRC 相同时,PID 也会帮助区分。
重传延迟(retry_delay)说明:
rf_set_ptx_retry(retry_count, retry_delay) 中的 retry_delay 定义为两次发送开始时刻之间的间隔时间,而非上一次发送结束到下一次发送开始的间隔。
PTX 状态机流程:

PTX 模式下,发送端在发出数据包后,自动切换到 RX 模式等待接收端的 ACK 响应。如果在设定时间内未收到有效 ACK(CRC 校验失败或超时),则根据配置的重传次数(rf_set_ptx_retry)自动重传;当重传次数超过设定值时触发 FLD_RF_IRQ_TX_RETRYCNT 中断并回到 IDLE。PTX 支持 PRX 在 ACK 包中回传数据(payload),此时 PTX 端触发 FLD_RF_IRQ_RX_DR 中断通知 MCU 读取。
注意
- PTX / PRX 接口在不同芯片系列上名称可能略有差异。部分 TL 系列芯片使用
rf_ptx_config()/rf_prx_config()分别初始化,rf_start_ptx参数签名也可能不同(仅接受 tick 参数)。请以具体芯片 SDK 头文件为准。
(7) PRX — 周期接收
void rf_start_prx(unsigned char *addr, unsigned int tick)
| 参数 | 说明 |
|---|---|
| addr | 接收数据存储地址,四字节对齐 |
| tick | 触发 tick 值 |
收到数据后自动回复 ACK。PRX 作为主接收方,只有在收到 PTX 的有效数据包后才自动响应 ACK。
PRX PID 重复检测机制:
PRX 通过 PID + CRC 组合判断当前收到的包是否为重传包。如果 PID 与上一次成功接收的包相同且 CRC 也一致,则判定为重复包——该包将被丢弃(不再上报给应用层),但 PRX 仍会自动回复 ACK(因为 PTX 可能因之前的 ACK 丢失而重传)。
PRX TX FIFO(ACK Payload):
PRX 可以在接收数据包之前,将待回传的数据预先写入 TX FIFO。当收到 PTX 数据包后,硬件自动将 TX FIFO 中的数据作为 ACK 的 payload 发送出去。若 TX FIFO 为空,则发送空 ACK(仅包含 Preamble + Access Code + Header/PID + CRC,无 Payload)。
NO_ACK 标志处理:
当收到的数据包中 NO_ACK 标志位为 1 时,PRX 不会发送 ACK 包(此时 PTX 将继续重传直到达到最大重传次数)。
PRX 状态机流程:

PRX 模式下,接收端持续监听指定频点,当收到有效数据包后(CRC 校验通过且 PID 为新包),将 payload 存入 RX buffer 并上报应用层;随后自动切换到 TX 模式回复 ACK。如果 RX FIFO 中有预存的待发送数据,则 ACK 包携带该数据一同发出。如果在设定时间内未收到有效同步字,触发 FLD_RF_IRQ_RX_TIMEOUT 中断后回到 IDLE 状态(周期性监听模式下,下一个 tick 到来时重新进入 RX Settle)。
RF 中断系统
常用中断一览表
| 中断名称 | Code Bit 位名称 | 触发模式 | 中断产生原理和触发机制 |
|---|---|---|---|
| TX 中断 | FLD_RF_IRQ_TX | Manual TX / STX / PTX / RX2TX / TX2RX | 每发完一个包立刻产生 |
| RX 中断 | FLD_RF_IRQ_RX | Manual RX / SRX / PRX / TX2RX / RX2TX | 每收到一个包且 CRC 校验通过后产生 |
| RX Timeout 中断 | FLD_RF_IRQ_RX_TIMEOUT | SRX / PRX /TX2RX | 接收窗口内未收到同步字,超时后产生 |
| First RX Timeout 中断 | FLD_RF_IRQ_FIRST_RX_TIMEOUT | SRX / PRX /RX2TX | 首次接收超时(进入 RX 后第一次超时)即产生 |
| RX CRC Error 中断 | FLD_RF_IRQ_RX_CRC_2 | BTX / BRX / PTX / PRX | 连续两次检测到 CRC 错误产生中断 |
| TX DS 中断 | FLD_RF_IRQ_TX_DS | PTX / PRX | 发送的 payload 长度不为 0 时产生 |
| RX DR 中断 | FLD_RF_IRQ_RX_DR | PRX / PTX / SRX | 接收到的包 payload length 不为 0 时产生 |
| Invalid PID 中断 | FLD_RF_IRQ_INVALID_PID | PTX / PRX | 接收到 invalid PID 时产生 |
核心中断底层功能解析
中断操作接口因芯片系列而异,以下分别列出。
TL 系列:
| 接口 | 说明 |
|---|---|
rf_get_irq_status(status) |
获取指定中断标志位状态(读 reg_rf_irq_status) |
rf_clr_irq_status(status) |
清除指定中断标志(写 1 清除) |
rf_set_irq_mask(mask) |
设置中断 mask(reg_rf_irq_mask) |
rf_clr_irq_mask(mask) |
清除中断 mask |
TC 系列:
| 接口 | 说明 |
|---|---|
rf_irq_src_get() |
获取当前 RF 中断源状态(读 reg_rf_irq_status) |
rf_irq_clr_src(msk) |
清除指定 RF 中断源(写 1 清除) |
rf_irq_enable(msk) |
使能指定 RF 中断 |
rf_irq_disable(msk) |
禁用指定 RF 中断 |
ISR 最佳实践(参照 app_ble_mode.c 的 rf_irq_handler):
TL 系列:
void rf_irq_handler(void)
{
// 1. 读取中断状态
unsigned int irq_status = rf_get_irq_status();
// 2. 清除中断标志
rf_clr_irq_status(irq_status);
// 根据中断类型分发处理
if (irq_status & FLD_RF_IRQ_TX) {
// 处理 TX 完成
}
if (irq_status & FLD_RF_IRQ_RX) {
// 处理 RX 完成
}
if (irq_status & FLD_RF_IRQ_RX_TIMEOUT) {
// 处理 RX 超时
}
}
TC 系列:
void irq_handler(void)
{
// 1. 读取中断状态
unsigned int irq_status = rf_irq_src_get();
// 2. 清除中断标志
rf_irq_clr_src(irq_status);
// 根据中断类型分发处理
if (irq_status & FLD_RF_IRQ_TX) {
// 处理 TX 完成
}
if (irq_status & FLD_RF_IRQ_RX) {
// 处理 RX 完成
}
if (irq_status & FLD_RF_IRQ_RX_TIMEOUT) {
// 处理 RX 超时
}
}
RF DMA
RF 专用 DMA 通道
RF 收发通过专用 DMA 通道进行数据传输:
| DMA 通道 | 功能 |
|---|---|
| RF_TX_DMA(Ch0) | RF 发送 DMA 通道,从 RAM 读取数据发送至 RF 硬件 FIFO |
| RF_RX_DMA(Ch1) | RF 接收 DMA 通道,从 RF 硬件 FIFO 接收数据写入 RAM |
TX DMA 配置
发包 buffer 的前两个字节为 DMA_LEN_INFO,指示 DMA 需要传输的数据长度。DMA_LEN_INFO 的计算方式因芯片系列而异:
- TL 系列: 提供宏
rf_tx_packet_dma_len(rf_data_len),计算公式为((rf_data_len + 3) / 4) | ((rf_data_len % 4) << 22) - TC 系列: 直接填入实际传输数据长度(不含 DMA_LEN_INFO 自身的字节数),低字节在前
RX DMA 配置
TL 系列:
void rf_set_rx_dma(unsigned char *buff, unsigned char fifo_num, unsigned short fifo_byte_size)
| 参数 | 说明 |
|---|---|
| buff | 收包地址,必须四字节对齐 |
| fifo_num | RX FIFO 数量(总 FIFO 数 - 1) |
| fifo_byte_size | 每个 FIFO 的深度(字节数),为 16 的整数倍 |
TL 系列还提供 TX DMA 配置接口:
void rf_set_tx_dma(unsigned char fifo_depth, unsigned short fifo_byte_size)
- fifo_depth :FIFO的深度信息,FIFO个数=2^fifo_depth。
- fifo_byte_size:一个fifo的大小,单位是字节。
TC 系列:
void rf_rx_buffer_set(unsigned char *RF_RxAddr, int size, unsigned char PingpongEn)
| 参数 | 说明 |
|---|---|
| RF_RxAddr | 收包地址,必须四字节对齐 |
| size | 收包 buffer 大小,为 16 的整数倍 |
| PingpongEn | Ping-Pong buffer 使能:1 = 使能,0 = 禁用 |
- 当 PingpongEn = 0:接收数据存储在 RF_RxAddr 指向的单一 buffer 中
- 当 PingpongEn = 1:接收数据依次存储在 buffer0 和 buffer1 中,RAM 预留大小应为 size * 2
DMA 使用注意事项
- DMA 配置时序要求: DMA 相关配置(
rf_rx_buffer_set/rf_set_rx_dma等)必须在 RF 状态机启动前完成,DMA 先于 RF 状态机完成配置 - TX DMA 预填充与 RF STX 启动的时序配合: 调用
rf_start_stx/rf_start_ptx等函数前,TX buffer 数据必须已就绪 - RX DMA 环形 Buffer 与 FIFO 溢出的时序保护: 接收数据总量不能超过 buffer 大小,否则 FIFO 溢出导致数据丢失
- Buffer 需要四字节对齐: TX / RX buffer 必须四字节对齐,使用
__attribute__ ((aligned (4)))声明
RF 通用配置
RF 基础通用硬件配置
(1) rf_mode_init()
RF 模块初始化是使用 RF 功能的第一步。TC 系列与 TL 系列存在差异,需分别说明:
- TL 系列 & TC 新系列: 先调用
rf_mode_init()完成基础初始化,再调用具体的模式设置函数 - TLSR825x / TLSR827x 系列: 通过
rf_drv_init(RF_ModeTypeDef)一步完成初始化和模式选择
注意
rf_mode_init()或rf_drv_init()必须最先调用,且仅需在系统初始化阶段调用一次。
(2) 调制方式、空中速率底层配置
| 协议 | TL 系列 API | TC 系列 API | 说明 |
|---|---|---|---|
| BLE 1M | rf_set_ble_1M_mode() |
rf_set_ble_1M_mode() |
标准 BLE 1Mbps |
| BLE 2M | rf_set_ble_2M_mode() |
rf_set_ble_2M_mode() |
高速 BLE 2Mbps |
| 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() 等 |
rf_set_pri_250K_mode() 等 |
速率可选:250K/500K/1M/2M |
注意
- TLSR825x / TLSR827x 系列通过
rf_drv_init(RF_MODE_xxx)一步完成模式选择。例如rf_drv_init(RF_MODE_BLE_1M)设置 BLE 1M 模式,rf_drv_init(RF_MODE_ZIGBEE_250K)设置 Zigbee 模式。
(3) Channel
BLE 频道设置(推荐用于 BLE 模式):
void rf_set_ble_chn(signed char chn_num)
频率映射公式:f = 2402 + channel(MHz)。参数 chn_num 按 BLE 规范映射,例如 chn_num = 37 -> 2402MHz。
通用频道设置(适用于所有模式):
TL 系列:
void rf_set_chn(signed char chn)
| 参数 | 说明 |
|---|---|
| chn | 频点 index 值,实际频点 = 2400 + chn,范围 2~80 |
TC 系列:
void rf_set_chn(signed char chn, unsigned short set)
| 参数 | 说明 |
|---|---|
| chn | 频点 index 值,实际频点 = 2400 + chn,范围 2~80 |
| set | 频点设置内部值,通常传 0 |
注意
rf_set_chn适用于所有模式;rf_set_ble_chn仅适用于 BLE 模式。
(4) Power
VBAT / VANT 双供电模式的区别:
- VBAT 模式: RF PA 模块直接由电池(VBAT)供电,输出功率范围大,支持 +10dBm 等高功率档位,但实际输出功率随电池电压下降而降低
- VANT 模式: RF PA 模块由内部 LDO 稳压供电,输出功率稳定(与电池电压无关),适合低功耗场景,但功率上限较低
功率设置接口:
void rf_set_power_level(RF_PowerTypeDef level)
void rf_set_power_level_index(RF_PowerTypeDef level)
两者功能等价,仅在枚举类型上存在差异,可根据需要选用。
注意
- 不同芯片支持的最大发射功率不同。具体的功率枚举值以对应芯片 SDK 的
rf_drv.h或rf_common.h头文件定义为准。实际发射功率受天线匹配、PCB 走线等因素影响,量产时需进行硬件校准。
(5) CRC
rf_crc_config_t 硬件字段:
| 字段 | 说明 |
|---|---|
| init_value | CRC 初始值 |
| poly | 多项式(BLE: 0x65b,Zigbee: 0x1021) |
| xor_out | 输出异或掩码 |
| byte_order | MSB First / LSB First |
| start_cal_pos | 起始计算字节位置 |
| len | CRC 长度(0~4 Bytes,0 = 禁用) |
各协议 CRC 默认配置速查表:
| 协议 | 多项式 | 长度 | 初始值 | 说明 |
|---|---|---|---|---|
| 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 | 用户可配 | 1~4 Bytes | 用户可配 | 灵活配置 |
注意
- CRC配置已经默认在驱动层配置好,如果需要自定义配置,需要根据具体协议的要求配置。部分芯片不支持自定义,具体请参考对应芯片 SDK 文档。
(6) Access Code
| 接口 | 说明 |
|---|---|
rf_acc_code_set(pipe_id, addr) |
设置指定通道的 Access Code |
rf_access_code_comm(acc) |
设置通用 Access Code(32-bit 整型值) |
注意
- Zigbee 模式没有 Access Code 概念,其 SFD 由硬件自动识别,无需软件配置。
(7) TX / RX Wait
在 Auto 模式中,TX -> RX 或 RX -> TX 切换时需要配置等待时间:
void rf_set_rx_wait_time(unsigned short rx_wait_us) // TX -> RX 切换等待时间,单位 us
void rf_set_tx_wait_time(unsigned short tx_wait_us) // RX -> TX 切换等待时间,单位 us
这两个接口用于配置状态机在 TX -> RX 或 RX -> TX 切换时中间的等待时间,确保硬件有足够的过渡时间。
Settle 硬件时序底层配置详解
(1) Settle 硬件时序定义与底层作用
- TX Settle(TX_EN -> PA Ramp 前): 包含 PLL 锁频 + LDO 稳定 + DAC 建立阶段。此阶段 RF 硬件完成频率锁定和发射链路校准,确保发射信号质量。
- RX Settle(RX_EN -> AGC Lock 前): 包含 PLL 锁频 + LDO 稳定 + ADC 建立阶段。此阶段 RF 硬件完成频率锁定和接收链路校准,确保接收灵敏度。
时序示意:
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 输出))
%% RX Settle 流程
subgraph RX_Time [◄───────── RX Settle Time ──────────►]
direction LR
R1[RX_EN] --> R2[RX Settle]
R2 --> R3[sync status]
end
R3 ==> R4((开始接收))
%% 样式设置
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;
在 Manual 模式下,软件需自行控制 Settle 等待;在 Auto 模式下,硬件状态机自动完成 Settle 等待。
(2) 底层配置接口
void rf_set_tx_settle_time(unsigned short tx_stl_us) // TX settle 时间,单位 us
void rf_set_rx_settle_time(unsigned short rx_stl_us) // RX settle 时间,单位 us
| 参数 | 默认值 | 最小值 | 最大值 |
|---|---|---|---|
| TX Settle | 150us | 113us | 0xfff us |
| RX Settle | 150us | 85us | 0xfff us |
注意
- 这里列举的最小值适用于大部分型号的芯片,部分芯片的最小值可能存在差异,具体的可以参考Demo或者驱动注释。
Fast Settle
Fast Settle 通过跳过部分硬件校准步骤来缩短 Settle 时间,从而降低功耗并提高收发切换速度。Fast Settle 的具体时间选项、枚举定义和接口因芯片系列而异。
注意
- Fast Settle 不同的芯片可能存在差异,具体的 Fast Settle 时间可以参考对应的芯片的驱动注释。
- Fast Settle 使用方式参考 RF Demo。
RF 模块底层初始化流程
参考 RF Demo code,RF 模块初始化流程如下。
TL 系列初始化流程
graph TD
%% 节点定义
Start(["系统上电 / 唤醒"])
Init["rf_mode_init()<br/>(RF 基础初始化,仅需一次)"]
Mode["模式设置接口<br/>(例如 rf_set_ble_1M_mode())"]
Power["功率设置<br/>rf_set_power_level()"]
Access["Access Code<br/>rf_access_code_comm()"]
Channel["频点设置<br/>rf_set_ble_chn()"]
DMA["DMA 配置<br/>rf_set_tx_dma()<br/>rf_set_rx_dma()"]
Optional["可选配置<br/>rf_set_tx_settle_time() /<br/>Fast Settle / PTA 等"]
End(["开始收发<br/>(Auto / Manual Mode)"])
%% 连接关系
Start --> Init
Init --> Mode
%% 并行分支
Mode --> Power
Mode --> Access
Mode --> Channel
%% 分支汇合
Power --> DMA
Access --> DMA
Channel --> DMA
%% 后续流程
DMA --> Optional
Optional --> End
%% 样式美化
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 系列初始化流程伪代码(BLE 1M 模式):
// 步骤 1: RF 基础初始化(仅需调用一次)
rf_mode_init();
// 步骤 2: 设置通信协议模式
rf_set_ble_1M_mode();
// 步骤 3: 配置发射功率
rf_set_power_level_index(target_power_level);
// 步骤 4: 设置 Access Code
rf_access_code_comm(access_code_value);
// 步骤 5: 配置 DMA
rf_set_tx_dma(fifo_depth, fifo_size); // TX DMA FIFO 配置
rf_set_rx_dma(rx_buff, rx_fifo_num, // RX DMA FIFO 配置
fifo_byte_size);
// 步骤 6: 设置频点
rf_set_ble_chn(channel_number);
TC 系列初始化流程
TC 新系列:
graph TD
%% 节点定义
Start(["系统上电 / 唤醒"])
Init["rf_mode_init()<br/>(RF 基础初始化,仅需一次)"]
Mode["模式设置接口<br/>(例如 rf_set_ble_1M_mode())"]
Power["功率设置<br/>rf_set_power_level_index()"]
Access["Access Code<br/>rf_access_code_comm()"]
Channel["频点设置<br/>rf_set_ble_channel()"]
DMA["DMA 配置<br/>rf_rx_buffer_set()"]
Optional["可选配置<br/>rf_set_tx_settle_time() /<br/>Fast Settle / PTA 等"]
End(["开始收发<br/>(Auto / Manual Mode)"])
%% 连接关系
Start --> Init
Init --> Mode
%% 并行分支
Mode --> Power
Mode --> Access
Mode --> Channel
%% 分支汇合
Power --> DMA
Access --> DMA
Channel --> DMA
%% 后续流程
DMA --> Optional
Optional --> End
%% 样式美化
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 新系列初始化流程伪代码(BLE 1M 模式):
// 步骤 1: RF 基础初始化(仅需调用一次)
rf_mode_init();
// 步骤 2: 设置通信协议模式
rf_set_ble_1M_mode();
// 步骤 3: 配置发射功率
rf_set_power_level_index(target_power_level);
// 步骤 4: 设置 Access Code
rf_access_code_comm(access_code_value);
// 步骤 5: 设置频点
rf_set_ble_channel(channel_number);
// 步骤 6: 配置 DMA
rf_rx_buffer_set(rx_buff, buff_size, pingpong_en);
// 步骤 7: 可选配置
rf_set_tx_settle_time(tx_settle_us); // TX Settle 时间
rf_set_rx_settle_time(rx_settle_us); // RX Settle 时间
TLSR825x/TLSR827x 系列:
graph TD
%% 节点定义
Start(["系统上电 / 唤醒"])
Init["rf_drv_init(mode)<br/>(RF 初始化 + 模式选择)<br/>例如: rf_drv_init(RF_MODE_BLE_1M)<br/>(仅需调用一次)"]
Power["功率设置<br/>rf_set_power_level_index()"]
Access["Access Code<br/>rf_access_code_comm()"]
Channel["频点设置<br/>rf_set_ble_channel()"]
DMA["DMA 配置<br/>rf_rx_buffer_set()"]
End(["开始收发<br/>(Auto / Manual Mode)"])
%% 连接关系
Start --> Init
%% 并行分支
Init --> Power
Init --> Access
Init --> Channel
%% 分支汇合
Power --> DMA
Access --> DMA
Channel --> DMA
%% 后续流程
DMA --> End
%% 样式美化
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 系列初始化流程伪代码:
// 步骤 1: RF 初始化 + 模式选择(一步完成,仅需调用一次)
rf_drv_init(RF_MODE_BLE_1M);
// 步骤 2: 配置发射功率
rf_set_power_level_index(target_power_level);
// 步骤 3: 设置 Access Code
rf_access_code_comm(access_code_value);
// 步骤 4: 设置频点
rf_set_ble_channel(channel_number);
// 步骤 5: 配置 DMA
rf_rx_buffer_set(rx_buff, buff_size, pingpong_en);
RF PHY Test
概述
什么是RF PHY Test
BQB(Bluetooth Qualification Body,蓝牙资格认证)测试是蓝牙产品在上市销售前必须通过的强制性认证测试。蓝牙技术联盟(Bluetooth SIG)要求所有使用蓝牙技术的产品必须通过BQB认证,以确保产品符合蓝牙核心规范,保证不同厂商设备之间的互操作性。
RF PHY Test主要验证蓝牙设备的射频物理层(RF-PHY)性能指标是否满足蓝牙规范要求。测试内容涵盖发射机(TX)和接收机(RX)两大类指标:
发射机(TX)测试指标:
- 输出功率(Output Power)
- 调制特性(Modulation Characteristics)
- 载波频率偏移和漂移(Carrier Frequency Offset and Drift)
- 带内杂散(In-band Spurious Emissions)
接收机(RX)测试指标:
- 灵敏度(Sensitivity)
- 载干比性能(C/I Performance)
- 阻塞性能(Blocking Performance)
- 互调性能(Intermodulation Performance)
- PER报告完整性(PER Report Integrity)
Telink PHY Test方案
Telink芯片的 PHY Test 程序支持两种使用方式:
| 使用方式 | 说明 | 适用场景 |
|---|---|---|
| 串口直连仪器 | 芯片通过2-Wire UART串口直接与综测仪(如CMW500)通信,遵循标准蓝牙测试协议 | 正式认证测试 |
| BQB_Tool工具 | Telink集成在BDT(Burning and Debugging Tool)中的BQB_Tool,可修改配置项并进行简单功能验证 | 开发调试、预测试验证 |
注意
- TC系列部分芯片目前暂不支持BDT_Tool中的BQB功能,后续会逐步适配支持。
PHY Test 支持两种指令交互方式:
| 指令方式 | 说明 | 驱动支持 |
|---|---|---|
| 2-Wire | 通过 UART 串口直接与测试仪器通信,使用蓝牙标准测试协议 | 支持(当前驱动程序默认方式) |
| HCI | 通过 Host Controller Interface(HCI)协议与测试仪器通信,由 Host 端发送 HCI Command | 暂不支持,用户可参考 Telink BLE 相关 SDK 中 HCI 的实现进行移植 |
注意
- 驱动PHY Test程序目前仅支持使用2-Wire UART串口与测试仪器交互。如果使用程序默认的串口配置(波特率、TX/RX引脚等),可直接与仪器连接使用;如果用户需要修改配置参数(如串口引脚、Access Code、PA控制等),可通过BQB_Tool工具进行配置。
芯片与SDK对应关系
| 芯片系列 | SDK路径 |
|---|---|
| TL系列 | tl_platform_src\demo\vendor\RF_Certification\BQB_Demo |
| TC系列 | tc_platform_src\demo\vendor\BQB_EMI_Demo |
BQB_Tool工具介绍
工具概述
BQB_Tool是Telink集成在BDT(Burning and Debugging Tool)中的一个配置与验证工具。它的主要功能包括:
- 配置项修改:修改BQB测试程序的各项配置参数
- 简单功能验证:模拟仪器指令交互过程,验证芯片PHY Test功能是否正常
工作原理
PHY Test程序通过2-Wire UART串口与测试仪器(如CMW500)进行通信,遵循蓝牙规范定义的标准测试协议。通信流程如下:

BQB_Tool工具的作用是在不使用实际仪器的情况下:
- 修改芯片固件中的配置参数(串口引脚、功率、PA等)
- 模拟发送测试指令,验证芯片响应是否正确

支持的配置项目
BQB_Tool支持以下配置项的修改:
| 配置项 | 说明 | 默认值 |
|---|---|---|
| 串口波特率 | UART通信波特率 | 115200 |
| TX引脚 | UART发送引脚 | 芯片型号相关 |
| RX引脚 | UART接收引脚 | 芯片型号相关 |
| Access Code | BLE接入地址 | 0x29417671 |
| TX Power | 发射功率等级 | 芯片型号相关 |
| PA TX引脚 | 外部PA发射控制引脚 | GPIO_PA0 |
| PA RX引脚 | 外部PA接收控制引脚 | GPIO_PA0 |
| 芯片供电模式 | LDO/DCDC供电方式选择 | 芯片型号相关 |
| 内部电容 | 是否使用内部电容 | 使用内部电容 |
| 校准值来源 | Flash/OTP/SRAM | Flash |
| Power Slice | 功率分片模式 | 关闭 |
| Swire through USB | 通过USB进行Swire通信 | 关闭 |

简单功能验证
BQB_Tool的第二项功能是简单功能验证。它模拟了测试仪器与芯片之间的指令交互过程:
- Init 指令:配置测试参数(PHY模式、Payload长度等),芯片返回状态
- RX 指令:启动接收测试,芯片进入RX状态并统计收包数
- TX 指令:启动发射测试,芯片按配置发送测试包
- END指令:结束测试,芯片返回发包/收包计数
通过这个功能,用户可以在不使用综测仪的情况下,快速验证芯片的BQB测试固件是否正常工作。

使用说明
准备工作
硬件准备:
| 设备 | 说明 |
|---|---|
| EVK开发板 | 搭载待测芯片的开发板 |
| UART连接线 | 2-Wire(TX+RX+GND),连接EVK与仪器或PC |
| 综测仪 | 如R&S CMW500/CMW270,用于正式BQB认证测试 |
| RF连接线 | 连接EVK天线口与综测仪RF端口 |
| 直流电源 | 如需外部供电(可选) |
软件准备:
| 软件 | 说明 |
|---|---|
| BQB测试bin文件 | 编译SDK中对应芯片的BQB_Demo工程 |
| BDT工具 | Telink Burning and Debugging Tool,内含BQB_Tool |
| 综测仪控制软件 | 如CMWrun(R&S),用于自动化测试 |
通用操作流程

步骤1:编译并下载BQB测试程序
- TL系列:打开
tl_platform_src\demo\vendor\RF_Certification\BQB_Demo工程- 编译后下载到芯片
- TC系列:打开
tc_platform_src\demo\vendor\BQB_EMI_Demo工程- 在
app_config.h中确保TEST_DEMO定义为BQB_DEMO - 编译后下载到芯片
- 在
步骤2:连接硬件
方式一:直连综测仪(正式认证)
芯片 UART TX ────-> 综测仪 UART RX
芯片 UART RX ────-> 综测仪 UART TX
芯片 GND ────-> 综测仪 GND
芯片 RF ────-> 综测仪 RF Port
方式二:连接PC使用BQB_Tool(功能验证)
芯片 UART TX ────-> USB转UART RX
芯片 UART RX ────-> USB转UART TX
芯片 GND ────-> USB转UART GND
步骤3:配置参数(可选)
如果使用默认配置,可跳过此步骤。如需修改配置:
- 打开BDT工具中的BQB_Tool
- 根据需要修改配置项(串口引脚、功率、PA等)
- 保存配置并重新下载程序
步骤4:执行测试
使用综测仪:按照综测仪操作手册,选择BQB RF测试项目,仪器会自动发送SETUP/RX_TEST/TX_TEST/END指令并收集测试结果。
使用BQB_Tool验证:在工具中手动发送指令,观察芯片响应。
TX测试
测试项目与指标:
| 测试项目 | 蓝牙规范要求 | 说明 |
|---|---|---|
| 输出功率 | -20dBm ~ +20dBm(BLE) | 芯片默认功率需在校准后满足要求 |
| 调制特性 | Δf1avg ≥ 225kHz, Δf2max ≥ 185kHz(1M) | 频偏指标 |
| 载波频率偏移 | ±150kHz以内 | 频率精度 |
| 载波漂移 | ±50kHz以内 | 频率稳定性 |
| 带内杂散 | 相邻信道 ≤ -20dBm | 频谱模板 |
操作步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 编译下载 | 编译对应芯片的BQB_Demo工程并下载 |
| 2 | 连接仪器 | UART连接综测仪,RF线连接综测仪RF端口 |
| 3 | 仪器设置 | 在综测仪上选择BLE BQB RF TX测试项目 |
| 4 | 自动测试 | 仪器自动发送SETUP -> TX_TEST -> END指令序列 |
| 5 | 查看结果 | 综测仪显示各项TX指标测试结果 |
典型指令序列:
SETUP: Reset -> BLE 1M模式
TX_TEST: 信道0 (2402MHz), Payload=37字节, PRBS9
END: 获取发包计数

RX测试
测试项目与指标:
| 测试项目 | 蓝牙规范要求 | 说明 |
|---|---|---|
| 灵敏度 | ≤ -70dBm(BLE 1M, PER≤30.8%) | 接收灵敏度 |
| 载干比(C/I) | 同频C/I ≤ 21dB | 抗同频干扰能力 |
| 阻塞性能 | 依频率偏移不同,-30dBm ~ +27dBm | 抗带外干扰能力 |
| 最大输入电平 | ≥ -10dBm(PER≤30.8%) | 最大接收功率 |
操作步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 编译下载 | 编译对应芯片的BQB_Demo工程并下载 |
| 2 | 连接仪器 | UART连接综测仪,RF线连接综测仪RF端口 |
| 3 | 仪器设置 | 在综测仪上选择BLE BQB RF RX测试项目 |
| 4 | 自动测试 | 仪器发送SETUP -> RX_TEST,然后发送测试包,最后发送END |
| 5 | 查看结果 | 综测仪根据芯片返回的收包数计算PER |
典型指令序列:
SETUP: Reset -> BLE 1M模式
RX_TEST: 信道0 (2402MHz), Payload=37字节
(仪器发送1500个测试包)
END: 获取收包计数 -> 计算PER

BQB_Tool使用说明
当不使用综测仪时,可通过BQB_Tool进行简单的功能验证:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 编译下载 | 编译对应芯片的BQB_Demo工程并下载 |
| 2 | 连接工具 | UART连接PC,打开BDT中的BQB_Tool |
| 3 | 发送SETUP | 发送复位指令,确认芯片返回成功状态 |
| 4 | 发送TX_TEST | 发送TX测试指令,芯片开始发包 |
| 5 | 发送END | 发送结束指令,查看芯片返回的发包计数 |
| 6 | 发送RX_TEST | 发送RX测试指令,芯片进入接收状态 |
| 7 | 发送END | 发送结束指令,查看芯片返回的收包计数(应为0,因为没有仪器发包) |

常见问题与注意事项
UART连接问题:
- 确保TX/RX引脚连接正确(TX接对方RX,RX接对方TX)
- 默认波特率为115200,确保双方波特率一致
- 不同芯片型号的默认UART引脚不同,请参阅对应SDK中的配置文件:
- TL系列:
tl_platform_src\demo\vendor\common\common\app_config\bqb_app_config.h(BQB_UART_TX_PORT/BQB_UART_RX_PORT宏) - TC系列:
tc_platform_src\demo\vendor\BQB_EMI_Demo\BQB\bqb.h(BQB_UART_TX_PORT/BQB_UART_RX_PORT宏)
- TL系列:
Access Code配置:
- 默认Access Code为
0x29417671 - 如需修改,可通过BQB_Tool配置或直接修改代码中的
ACCESS_CODE宏
功率校准:
- 不同芯片的默认发射功率不同,测试前需确认功率等级与实际输出功率的对应关系
- 可通过BQB_Tool调整功率等级
PA配置:
- 如使用外部PA(功率放大器),需正确配置PA的TX/RX控制引脚
- PA TX引脚和RX引脚不能相同
内部电容校准:
- 芯片支持内部电容和外部电容两种方式
- 使用内部电容时,需从FLASH/OTP/SRAM读取校准值
- 校准值存储位置根据Flash大小不同,请参阅对应芯片的校准头文件:
- TL系列:
tl_platform_src\demo\vendor\common\{芯片型号}\calibration\calibration.h(同上宏定义) - TC系列:
tc_platform_src\demo\vendor\common\{芯片型号}\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_2M等宏)
- TL系列:
RF EMI
概述
什么是RF EMI测试
RF EMI(Electromagnetic Interference)测试是无线芯片在认证(如FCC、CE、SRRC等)过程中必须进行的射频指标测试。通过EMI测试程序,芯片可以输出特定的RF测试信号(单载波、调制信号、连续包、突发包等),配合频谱仪等测量设备,验证芯片的发射功率、频率偏差、杂散、谐波等射频性能指标是否满足法规要求。
测试工具
Telink芯片的EMI测试程序需要配合上位机工具使用:
| 芯片系列 | 使用工具 | 说明 |
|---|---|---|
| TL系列 | EMI_Tool(BDT) | 功能更丰富,支持更多可配置项 |
| TC系列 | Non_Signaling_Test_Tool / EMI_Test_Tool | 标准非信令测试工具 |
注意
- EMI测试程序必须与对应的上位机工具配合使用,工具通过SWire(单线)接口与EVK板通信,下发参数到芯片,芯片解析后输出对应的RF测试信号。
EMI测试工具介绍
工作原理
EMI测试工具的工作流程如下:

- 用户在上位机工具中选择测试模式和配置参数
- 上位机通过SWire(或USB转SWire)接口将参数写入芯片SRAM指定地址
- 芯片固件轮询SRAM中的命令和参数,执行对应的RF测试功能
- 测试信号通过天线口输出,由频谱仪等设备进行测量
EMI_Test_Tool/Non_Signaling_Test_Tool(TC系列芯片)
适用于TC系列芯片,支持的配置项目包括:
- RF模式选择:具体模式参考datasheet
- 频点设置:2402MHz ~ 2480MHz
- 功率设置:多级功率可调
- 测试模式:CarrierOnly、Burst(Prbs9/0x55/0x0F)、Continue(Prbs9)、RX
- 跳频使能:Continue模式下支持跳频
- PA控制:支持外部PA的TX/RX引脚配置
EMI_Tool(TL系列芯片)
适用于TL系列芯片,除支持EMI_Test_Tool/Non_Signaling_Test_Tool的全部功能外,还额外支持:
- BLE Access Code配置:可自定义BLE协议的Access Code值
- 内部电容配置:可选择关闭内部电容
- PA bypass使能:支持PA bypass模式
- SWire through USB使能:支持通过USB进行SWire通信
- Power Slice模式:支持功率分片测试
- 自适应抗干扰:支持检测环境噪声,噪声过大时暂停发包
- Packet Tone模式:支持包间插入单载波
芯片与工具对应表
| 芯片系列 | 使用工具 | SDK路径 |
|---|---|---|
| TL系列 | EMI_Tool | tl_platform_src\demo\vendor\RF_Certification\EMI_Demo |
| TC系列 | EMI_Test_Tool/Non_Signaling_Test_Tool | tc_platform_src\demo\vendor\BQB_EMI_Demo |
EMI模式与对应API介绍
Telink芯片EMI测试支持以下四种基本模式:
| 模式 | 命令ID(TC系列) | 命令ID(TL系列) | 说明 |
|---|---|---|---|
| CarrierOnly(单载波) | 0x01 | 0x01 | 产生单一频率的连续波信号 |
| Continue(连续包) | 0x02 | 0x02 | 产生连续的调制包信号 |
| RX(接收) | 0x03 | 0x03 | 接收模式,统计收包数和RSSI |
| Burst(突发包) | 0x04/0x05/0x06 | 0x04 | 产生突发的调制包信号 |
支持的无线通信模式包括:Ble1M、Ble2M、Ble125K、Ble500K、Zigbee250K、Private 2M、Private 1M。
CarrierOnly模式(单载波)
功能说明:
CarrierOnly模式用于产生单一频率的连续波信号(未调制载波)。在此模式下,可以设置频点、功率值和通信模式。该模式主要用于测试芯片的发射功率、频率精度、相位噪声等指标。
频谱仪现象:
频谱仪上显示为一个单一频率的峰值信号,无调制带宽展宽。

API接口:
TL系列:
void emicarrieronly(void);
TC系列:
void emicarrieronly(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
| 参数 | 说明 |
|---|---|
| rf_mode | RF模式:参考各芯片demo |
| pwr | 功率等级索引 |
| rf_chn | 信道号(频率范围:2402~2480MHz) |
使用示例:
// TC系列示例
emicarrieronly(RF_MODE_BLE_1M_NO_PN, power_level, 17); // BLE 1M模式,2440MHz
// TL系列示例 - 通过上位机配置后自动调用
// 上位机设置:模式=Ble1M,频点=2440MHz,功率=0dBm
// 芯片自动执行 emicarrieronly()
跳频模式:
TL系列CarrierOnly模式支持跳频(Hopping),可在多个频点间切换输出单载波信号,用于快速验证多个频点的发射性能。
Continue模式
功能说明:
Continue模式用于产生连续的调制信号。在此模式下,芯片持续不断地发送调制数据,可设置频点、功率值、通信模式以及Payload数据类型。
支持的Payload数据类型:
| pkt_type | 数据类型 | 说明 |
|---|---|---|
| 0 | Prbs9(随机数据) | 伪随机序列,模拟真实数据 |
| 1 | 0x0F | 固定数据0x0F |
| 2 | 0x55 | 固定数据0x55(0101交替) |
频谱仪现象:
频谱仪上显示为连续占据信道带宽的调制信号,信号不间断。

API接口:
TL系列:
void emi_tx_continue(void);
TC系列:
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);
跳频模式:
Continue模式支持跳频功能。使能跳频后,芯片会按照预设的跳频序列在不同频点间切换发送连续包。
Burst模式
功能说明:
Burst模式用于产生突发的调制包信号。与Continue模式不同,Burst模式发送的是不连续的包信号,包与包之间存在间隔。可设置频点、功率值、通信模式以及Payload数据类型。
支持的Payload数据类型:以工具界面支持数据类型为准
频谱仪现象:
由于Burst模式信号不连续,频谱仪需要使用 Single Sweep 或 MaxHold 设置来捕获信号:
- Single Sweep:单次扫描,可捕获到单个突发包
- MaxHold:最大保持,多次扫描叠加后可看到完整的频谱包络
Single Sweep设置下的Burst信号:

MaxHold设置下的Burst:

API接口:
TL系列:
void emi_tx_burst(void);
TC系列:
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);
自适应抗干扰模式(TL系列):
TL系列Burst模式支持自适应抗干扰功能。使能后,芯片会持续检测环境噪声:
- 当环境噪声低于-70dBm时,正常发送Burst包
- 当环境噪声高于-70dBm时,暂停发包,切换到RX模式继续监测
- 噪声降低后自动恢复发包
RX模式
功能说明:
RX模式用于接收测试。在此模式下,芯片持续处于接收状态,统计接收到的数据包数量和RSSI(接收信号强度指示)值。需要配合信号源(如综测仪)发送测试包。
频谱仪现象:
RX模式不产生RF输出信号,通过上位机工具读取收包数和RSSI值。
API接口:
TL系列:
void emirx(void);
TC系列:
void emirx(RF_ModeTypeDef rf_mode, unsigned char pwr, signed char rf_chn);
数据获取:
芯片通过SRAM向上位机回报以下数据:
| SRAM地址偏移 | 内容 | 说明 |
|---|---|---|
| +0x04 | RSSI值 | 当前接收信号强度 |
| +0x0C | 收包计数 | 累计接收到的数据包数量 |
使用说明
准备工作
-
硬件准备:
- EVK开发板(搭载待测芯片)
- SWire连接线(连接EVK与PC)
- 频谱仪(如Keysight N9020A等)
- RF连接线(连接EVK天线口与频谱仪)
- 直流电源(如需外部供电)
-
软件准备:
- 对应芯片的EMI测试bin文件(编译SDK中的EMI_Demo工程)
- 上位机工具:EMI_Tool(TL系列)或 EMI_Test_Tool/Non_Signaling_Test_Tool(TC系列)
通用操作流程
通用操作流程如下:

CarrierOnly模式测试说明
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 选择工具 | TC系列:Non_Signaling_Test_Tool;TL系列:EMI_Tool |
| 2 | 下载测试bin | 编译对应芯片的EMI_Demo工程并下载 |
| 3 | 配置参数 | 模式选择:CarrierOnly;RF模式:Ble1M;频点:2440MHz;功率:0dBm |
| 4 | 频谱仪设置 | Center Freq: 2440MHz, Span: 10MHz, RBW: 100kHz |
| 5 | 观测现象 | 频谱仪显示2440MHz处单一峰值信号 |

Continue模式测试说明
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 选择工具 | TC系列:Non_Signaling_Test_Tool;TL系列:EMI_Tool |
| 2 | 下载测试bin | 编译对应芯片的EMI_Demo工程并下载 |
| 3 | 配置参数 | 模式选择:Continue;RF模式:Ble1M;频点:2440MHz;功率:0dBm;数据类型:Prbs9 |
| 4 | 频谱仪设置 | Center Freq: 2440MHz, Span: 5MHz, RBW: 30kHz |
| 5 | 观测现象 | 频谱仪显示连续调制信号,占据约1MHz带宽 |
注意
- 如需跳频测试,在上位机工具中使能Hopping选项。

Burst模式测试说明
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 选择工具 | TC系列:Non_Signaling_Test_Tool;TL系列:EMI_Tool |
| 2 | 下载测试bin | 编译对应芯片的EMI_Demo工程并下载 |
| 3 | 配置参数 | 模式选择:Burst;RF模式:Ble1M;频点:2440MHz;功率:0dBm;数据类型:Prbs9 |
| 4 | 频谱仪设置 | Center Freq: 2440MHz, Span: 5MHz, RBW: 30kHz, Detector: MaxHold |
| 5 | 观测现象 | 使用MaxHold多次扫描后,频谱仪显示不连续的调制信号包络 |
注意
- 由于Burst信号不连续,建议使用频谱仪的MaxHold或Single Sweep模式捕获信号。

RX模式测试说明
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 选择工具 | TC系列:Non_Signaling_Test_Tool;TL系列:EMI_Tool |
| 2 | 下载测试bin | 编译对应芯片的EMI_Demo工程并下载 |
| 3 | 配置参数 | 模式选择:RX;RF模式:Ble1M;频点:2440MHz |
| 4 | 信号源设置 | 使用综测仪发送BLE 1M测试包,频点2440MHz,功率1dBm |
| 5 | 观测现象 | 上位机工具显示收包数和RSSI值 |
EMI_Tool 显示收包数和RSSI值(TL Series):

Non_Signaling_Test_Tool 显示收包数和RSSI值(TC系列):

Timer
简介
Timer 是通用硬件定时器,所有芯片均支持 TIMER_SYS_CLOCK_MODE、TIMER_GPIO_TRIGGER_MODE、TIMER_GPIO_WIDTH_MODE、TIMER_TICK_MODE 四种模式。
芯片功能差异总表
各芯片在 Timer 通道数、Input capture mode等方面存在差异,汇总如下:
| 芯片 | Timer 个数 | 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) | ✅ |
说明
- GPIO 信号接入 Timer 的硬件通道也存在差异,但已被
timer_gpio_init()封装,普通用户无需关心。
Timer 功能说明
Timer 支持 4 种工作模式,所有芯片均支持这 4 种基础模式。
System Clock Mode
时钟源:pclk
功能:周期性产生中断。计数器达到 capture value 时产生中断,自动装载 initial_tick 重新计数,循环进行。
设置步骤(以 initial_tick=0、capture value=50ms 为例):
// TC 系列
timer0_set_mode(TIMER_MODE_SYSCLK, 0, 50 * sys_clk.pclk * 1000);
timer_start(TIMER0);
// TL 系列
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);
运行结果:LED2 每 50ms 反转一次。

GPIO Trigger Mode
时钟源:GPIO 跳变沿
功能:指定个数的 GPIO 上升沿/下降沿触发中断。GPIO 每发生一个上升沿/下降沿,计数器加 1,达到设定值产生中断,清零重新计数。
设置步骤:
// TC 系列
timer0_gpio_init(SW1, POL_RISING);
timer0_set_mode(TIMER_MODE_GPIO_TRIGGER, 0, TIMER_MODE_GPIO_TRIGGER_TICK);
timer_start(TIMER0);
// TL 系列
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);
说明
TIMER_MODE_GPIO_TRIGGER_TICK为 Demo 中定义的宏(值为0x01),表示触发中断所需的 GPIO 跳变沿个数,不在驱动头文件中。
运行结果(以 capture value=0xf,GPIO_PA2 每 500ms 产生上升沿为例):GPIO_PA2 每产生 15 个上升沿,LED2 反转一次。

GPIO Pulse Width Mode
时钟源:pclk
功能:捕获 GPIO 脉冲宽度。GPIO 检测到设定极性边沿触发定时,每个 pclk 计数加 1,电平翻转时进入中断并停止计数,读取计数值计算脉冲宽度。单次触发,不循环。
设置步骤:
// TC 系列
timer0_gpio_init(SW1, POL_FALLING);
timer0_set_mode(TIMER_MODE_GPIO_WIDTH, 0, 0);
timer_start(TIMER0);
// TL 系列
timer_gpio_init(TIMER0, SW1, POL_FALLING);
timer_set_mode(TIMER0, TIMER_MODE_GPIO_WIDTH);
timer_start(TIMER0);
极性说明:设置 POL_FALLING 表示下降沿触发计时,上升沿产生中断。
运行结果(GPIO_PA2 下降沿后延时 250ms 产生上升沿进入中断):GPIO_PA2 上升沿时 LED2 反转。读取计时寄存器 0x005b8e01 = 6000129,24M 时钟下 6000129 / 24M ≈ 250ms。

Tick Mode
时钟源:pclk
功能:时间指示器,不产生中断。每个 pclk 上升沿计数器加 1,溢出后自动清零重新计数。软件可手动读取或清零计数值。
设置步骤(以 initial_tick=0、capture value=0,每 500ms 手工重置定时器为例):
// TC 系列
timer0_set_mode(TIMER_MODE_TICK, 0, 0);
timer_start(TIMER0);
// TL 系列
timer_set_mode(TIMER0, TIMER_MODE_TICK);
timer_start(TIMER0);
运行结果:LED2 每 500ms 反转一次。

Input Capture Mode
新增Input capture mode,可捕获 GPIO 边沿时刻的 Timer 计数值。支持该模式的芯片见"芯片功能差异总表"。
捕获模式:
| 模式 | 枚举值 | 说明 |
|---|---|---|
| 上升沿捕获 | TMR_CAPT_RISING_EDGE |
上升沿触发捕获 |
| 下降沿捕获 | TMR_CAPT_FALLING_EDGE |
下降沿触发捕获 |
| 双边沿捕获 | TMR_CAPT_RISING_FALLING_EDGE |
上升沿和下降沿都触发捕获 |
设置步骤:
// 1. 配置 GPIO 并使能捕获(需先选好模式 0 或 3)
timer_set_init_tick(TIMER0, 0);
timer_set_cap_tick(TIMER0, 0); // capt 不作为模式中断触发值
timer_set_mode(TIMER0, TIMER_MODE_SYSCLK); // 或 TIMER_MODE_TICK
timer_set_input_capture_mode(TIMER0, TMR_CAPT_RISING_FALLING_EDGE, GPIO_PA0);
timer_set_irq_mask(FLD_TMR0_CAPT_IRQ); // 使能捕获中断
timer_start(TIMER0);
// 2. 中断处理中读取捕获值
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);
// 计算 PWM 周期:相邻两次捕获值之差
}
}
Timer API 差异
TC 风格 API
| 函数 | 说明 |
|---|---|
timer0_set_mode(mode, init_tick, cap_tick) |
设置 Timer0 模式及参数 |
timer1_set_mode(mode, init_tick, cap_tick) |
设置 Timer1 模式及参数 |
timer2_set_mode(mode, init_tick, cap_tick) |
设置 Timer2 模式 |
timer0_gpio_init(pin, pol) |
初始化 Timer0 GPIO |
timer1_gpio_init(pin, pol) |
初始化 Timer1 GPIO |
timer_start(type) |
启动定时器 |
timer_stop(type) |
停止定时器 |
timer_set_irq_mask(mask) |
设置中断掩码(reg_irq_mask) |
timer_clr_irq_mask(mask) |
清除中断掩码 |
timer_clear_interrupt_status(status) |
清除中断状态 |
timer_get_interrupt_status(status) |
获取中断状态 |
TL 风格 API
| 函数 | 说明 |
|---|---|
timer_set_mode(type, mode) |
设置定时器模式 |
timer_gpio_init(type, pin, pol) |
初始化 GPIO |
timer_set_init_tick(type, init_tick) |
设置初始 tick |
timer_set_cap_tick(type, cap_tick) |
设置捕获 tick |
timer_start(type) / timer_stop(type) |
启停定时器 |
timer_get_irq_status(status) |
获取中断状态 |
timer_clr_irq_status(status) |
清除中断状态 |
timer_set_irq_mask(mask) / timer_clr_irq_mask(mask) |
中断掩码(reg_tmr_ctrl3) |
timer0_get_tick() / timer1_get_tick() |
读取 tick 值 |
timer_set_wrap(type) |
设置 wrap 模式 |
在 TL 系列基础 API 之上,部分芯片新增输入捕获及 DMA 相关接口:
| 函数 | 说明 |
|---|---|
timer_set_input_capture_mode(type, capt_mode, pin) |
设置输入捕获模式 |
timer_input_capture_en(type) |
使能输入捕获 |
timer_input_capture_dis(type) |
禁用输入捕获(仅 TL721x) |
timer_get_capture_value(type) |
读取捕获值(独立于 tick) |
timer_reset_tick(type, reset) |
控制捕获/比较时是否复位 tick |
timer_set_rx_dma_config(type, chn) |
配置 DMA 接收通道 |
timer_receive_dma(type, addr, rev_size) |
启动 DMA 接收 |
timer_set_dma_chain_llp(type, chn, dst, len, head) |
配置 DMA 链式头节点 |
timer_set_rx_dma_add_list_element(...) |
添加 DMA 链式节点 |
GPIO 中断路由差异
Timer 的 GPIO Trigger Mode、GPIO Pulse Width Mode 以及Input capture mode都需要将外部 GPIO 信号路由到 Timer 模块作为计数时钟源。各芯片的路由机制见"芯片功能差异总表"。
路由原理
GPIO 信号需要通过硬件路由通道才能到达 Timer。每路 Timer 固定绑定其中一个路由通道,GPIO 引脚的电平信号通过极性选择(上升沿/下降沿)后,经路由通道送至 Timer 的计数器。
原理:该信号既可作为 GPIO 中断源触发 CPU 中断,也可作为 Timer 的计数时钟。Timer 的 GPIO 模式仅使用其计数功能,不需要使能 GPIO 中断 mask,否则会产生额外的 GPIO 中断导致频繁进出中断。
各芯片的 Timer 与路由通道绑定关系如下:
| Timer 通道 | RISC 寄存器路由 | gpio2risc 路由 | PLIC GPIO_IRQ 路由(TL321x/TL322x/TL323x) | PLIC GPIO_IRQ 路由(TL751x) |
|---|---|---|---|---|
| TIMER0 | gpio_risc0 | gpio2risc0 | GPIO_IRQ1 | GPIO_IRQ0 |
| TIMER1 | gpio_risc1 | gpio2risc1 | GPIO_IRQ2 | GPIO_IRQ1 |
| TIMER2 | gpio_risc2 | — | — | — |
RISC 寄存器路由
通过 reg_gpio_irq_risc0_en(pin) / reg_gpio_irq_risc1_en(pin) / reg_gpio_irq_risc2_en(pin) 寄存器将指定 GPIO 绑定到对应 RISC 通道,通过 reg_gpio_pol(pin) 设置触发极性,通过 reg_irq_src(FLD_IRQ_GPIO_RISC0_EN / FLD_IRQ_GPIO_RISC1_EN)使能中断源。
驱动接口 timer0_gpio_init(pin, pol) / timer1_gpio_init(pin, pol) 内部已封装上述配置,无需再调用 gpio_set_risc_irq_mask() 打开 RISC mask(否则会产生 GPIO 中断)。
gpio2risc 路由
通过 gpio_set_gpio2risc0_irq(pin, pol) / gpio_set_gpio2risc1_irq(pin, pol) 将 GPIO 绑定到 gpio2risc 通道,通过 gpio_gpio2risc0_irq_en(pin) / gpio_gpio2risc1_irq_en(pin) 使能通道。
驱动接口 timer_gpio_init(type, pin, pol) 内部已封装上述配置。
PLIC GPIO_IRQ 路由
通过 PLIC 独立中断通道实现路由:gpio_set_irq(GPIO_IRQn, pin, trigger_type) 将 GPIO 绑定到对应通道,gpio_set_irq_mask(GPIO_IRQ_IRQn) 使能通道掩码。注意 TL751x 的通道号与其他芯片不同。
驱动接口 timer_gpio_init(type, pin, pol) 内部已封装上述配置。
共性:各芯片的 timer_gpio_init 接口均内部完成 GPIO 输入使能、上下拉电阻配置(下降沿用上拉、上升沿用下拉)、极性设置和路由绑定,用户无需手动配置 GPIO 寄存器。
注意事项
模式设置顺序
设置 Timer 模式时,必须先配置 init_tick / cap_tick,再调用 timer_set_mode(),最后 timer_start()。timer_set_mode() 内部会清除原有中断状态,若先调用会覆盖后续设置的 tick 值。
// 正确顺序(TL 系列)
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);
中断状态清除顺序
进入中断后必须先清除中断标志,再进行业务处理,否则可能重复进入中断:
// TL 系列
void timer_irq_handler(void) {
if (timer_get_irq_status(FLD_TMR0_MODE_IRQ)) { // 状态位
timer_clr_irq_status(FLD_TMR0_MODE_IRQ); // 1. 先清标志
// 2. 业务处理
}
}
// TC 系列
void timer_irq_handler(void) {
if (timer_get_interrupt_status(TMR_STA_TMR0)) {
timer_clear_interrupt_status(TMR_STA_TMR0); // 1. 先清标志
// 2. 业务处理
}
}
注意
FLD_TMR0_MODE_IRQ是中断状态位(timer_irq_e枚举),用于查询/清除中断状态;FLD_TMR0_MODE_MASK是中断掩码位(timer_mask_e枚举),用于timer_set_irq_mask()/timer_clr_irq_mask()使能/禁能中断,两者不可混用。
GPIO Pulse Width Mode 单次触发
GPIO Pulse Width Mode 触发一次中断后自动停止计数,不会循环。若需连续测量,必须在中断处理中重新调用 timer_start(TIMER0) 重启定时器。
Tick Mode 无中断
Tick Mode 不产生中断,只能通过轮询读取 timer0_get_tick() 判断时间。使用时无需配置中断掩码,配置了也无效。
GPIO 路由资源占用
GPIO Trigger Mode、GPIO Pulse Width Mode 以及Input capture mode均会占用对应 Timer 的 GPIO 路由通道(见第四章路由表):
- 同一路 Timer 的三种 GPIO 模式互斥,不能同时使用
- 被占用的路由通道不能再用于普通 GPIO 外部中断
- TIMER0 和 TIMER1 分别占用各自的路由通道,相互独立
Input Capture Mode 依赖基础模式
输入捕获功能(支持的芯片见"芯片功能差异总表")需先设置为 System Clock Mode 或 Tick Mode 作为计数时基,再配置捕获模式。capture tick 在此模式下不再作为中断触发值,仅用作 tick 复位点(配合 timer_reset_tick())。
DMA 接收配置顺序
使用 DMA 搬运捕获值时,必须先配置 DMA,再使能输入捕获,否则可能在 DMA 未就绪时发生捕获导致数据丢失:
// 1. 先配置 DMA
timer_set_rx_dma_config(TIMER0, DMA_CH0);
timer_receive_dma(TIMER0, (unsigned char *)capt_buff, sizeof(capt_buff));
// 2. 再使能捕获
timer_set_input_capture_mode(TIMER0, TMR_CAPT_RISING_EDGE, GPIO_PA0);
API 风格不可混用
TC 风格 API(如 timer0_set_mode(mode, init, cap))与 TL 风格 API(如 timer_set_mode(type, mode) + timer_set_init_tick() + timer_set_cap_tick())不可混用,移植时需整套替换。
System Timer(STimer)
简介
System Timer(STimer)是系统时间基准,提供基础时间服务和中断功能。
STimer 功能说明
基础时间服务 API 使用示例
STimer 提供获取系统时间、延时、超时判断功能,不同芯片系列API名略有差异,但用法相同。详细接口参考STimer API 差异。
使用示例:
// 1. 获取当前系统时间作为参考点
unsigned int ref = stimer_get_tick(); // TL 系列;TC 系列用 clock_time()
// 2. 执行一段代码或延时
delay_ms(100); // TL 系列;TC 系列用 sleep_ms(100)
// 3. 判断是否超时(单位 us)
if (clock_time_exceed(ref, 1000)) {
// 已超过 1ms
}
概述与时钟源
STimer 是 32bit 系统计时器,时钟源均来自板载 24M 晶体:部分芯片直接使用 24M,部分是24M xtal经过2/3除法器后得到16M,提供给STimer。
| 芯片系列 | 频率 |
|---|---|
| B80/B80B/B85/B87/TC321x/TC123x/B91 | 16 MHz |
| TC122x/B92/TL321x/TL721x/TL322x/TL323x/TL751x | 24 MHz |
STimer 中断触发条件
STimer 中断触发条件是各芯片系列最重要的差异,移植时务必首先确认触发条件是32bit 全匹配(tick_now == tick_capture)还是范围匹配(1/64 圈内),全匹配过时不可触发,范围匹配过时仍可触发。
说明
- 32bit 全匹配:当前 tick 与 capture 值完全相等才触发;若设置时刻已经过了 capture 值,则不会再触发。因 STimer 当前 tick 低 3bit 永远为 0,则配置capture tick的接口内部会自动将低 3bit 屏蔽为 0。
- 范围匹配:当前 tick 落在
[capture, capture + 2^26)区间内都可触发,即过了设置时间(1/64 圈以内)也可触发。该机制适用于应用 SDK 中根据远端时间计算理论 STimer tick 值的场景。
| 触发类型 | 触发条件 | 过时是否仍可触发中断 | 芯片系列 |
|---|---|---|---|
| 32bit 全匹配 | tick_now & BIT[31:3] == tick_capture |
不能 | B80/B80B/B87/TC321x/TC122x |
| 范围匹配 | (tick_now - tick_capture) & BIT[31:26] == 0 |
可以 | B85/TC123x/B91/B92/TL321x/TL721x/TL322x/TL323x/TL751x |
过时中断触发示例(范围匹配芯片):
对于范围匹配的芯片,即使 capture 值设置的是"过去"的时间(仍在 1/64 圈内),也会立即触发中断。以下示例验证该特性:
stimer_set_irq_capture(stimer_get_tick() - 10 * SYSTEM_TIMER_TICK_1MS);
// 因范围匹配机制,过时 10ms 仍在 1/64 圈内,会立即触发中断
注意
- 全匹配芯片(B80/B80B/B87/TC321x/TC122x)不具备该特性,过时不可触发中断。
中断功能与 API
STimer 通过设置 capture 值在指定时刻产生中断。
配置示例(基于 TL 系列 STimer_Demo):
// 1. 设置 1s 后触发中断的 capture 值
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1S);
// 2. 使能 STimer 中断(不同芯片 mask 宏名略有差异)
stimer_set_irq_mask(FLD_SYSTEM_IRQ_MASK);
// 3. 使能 PLIC 中断并打开全局中断
plic_interrupt_enable(IRQ_SYSTIMER);
core_interrupt_enable();
// 4. 中断服务函数(注册到 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); // 清除中断标志
stimer_set_irq_capture(stimer_get_tick() + SYSTEM_TIMER_TICK_1S); // 重新设置下一次触发
}
}
PLIC_ISR_REGISTER(stimer_irq_handler, IRQ_SYSTIMER);
TC 系列等价配置:
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 使用 FLD_IRQ_SYSTEM_TIMER
irq_enable();
// 中断处理
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 差异
| 功能 | TC系列 API | TL系列 API |
|---|---|---|
| 获取系统时间 | clock_time |
stimer_get_tick |
| 设置 tick 值 | stimer_set_tick |
stimer_set_tick |
| 延时 | sleep_us / sleep_ms |
delay_us / delay_ms |
| 超时判断 | clock_time_exceed |
clock_time_exceed |
| 设置中断捕获 | stimer_set_capture_tick |
stimer_set_irq_capture |
| 中断掩码 | stimer_set_irq_mask / stimer_clr_irq_mask |
stimer_set_irq_mask / stimer_clr_irq_mask |
| 中断状态 | stimer_get_irq_status / stimer_clr_irq_status |
stimer_get_irq_status / stimer_clr_irq_status |
PWM
PWM简介
PWM(Pulse Width Modulation,脉冲宽度调制)模块用于生成可配置占空比和周期的方波信号,可广泛应用于LED调光、电机控制、红外发射、音频DAC驱动等场景。
具备PWM模块的芯片及核心差异如下表所示:
| 芯片 | 通道数量 | 32K时钟源 | 对齐模式 | 移相功能 | 引脚配置API |
|---|---|---|---|---|---|
| B80 / B80B / B85 / B87 / TC321X / TC122X / TC123X | 6路(PWM0~PWM5) | 不支持 | 边沿对齐 | 不支持 | 无独立接口(引脚映射固定) |
| B91 | 6路(PWM0~PWM5) | 支持 | 边沿对齐 | 不支持 | pwm_set_pin |
| B92 / TL751X / TL321X / TL323X | 6路(PWM0~PWM5) | 支持 | 边沿对齐+中心对齐 | 支持 | pwm_set_pin |
| TL721X | 7路(PWM0~PWM6) | 支持 | 边沿对齐+中心对齐 | 支持 | pwm_set_pin |
| TL322X | 24路(PWM0~PWM23) | 支持 | 边沿对齐+中心对齐 | 支持 | pwm_set_pin |
通用特性: 所有芯片均支持 pclk 时钟源、5种工作模式(PWM0)+ continuous(其他通道)、IR FIFO mode、IR DMA FIFO mode、DMA链式传输、Invert/Polarity、N-channel invert。
PWM0支持的全部工作模式如下:
- continuous mode(连续模式)
- counting mode(计数模式)
- IR mode(红外模式)
- IR FIFO mode(红外FIFO模式)
- IR DMA FIFO mode(红外DMA FIFO模式)
除PWM0外的其他通道仅支持continuous mode。
时钟
PWM的时钟源有两路:PCLK和32K。
PCLK时钟源
功能: 可以进行分频,分频后的时钟作为PWM使用的时钟源。所有芯片均支持。
注意
- 请保证pclk_frequency是pwm_frequency的整数倍,否则会导致PWM输出不是期望的频率。
接口配置:
static inline void pwm_set_clk(unsigned int pwm_clk_div);
其中:pwm_clk_div = (pclk_frequency / pwm_frequency) - 1
32K时钟源
功能: 不支持分频,并且只支持continuous mode、counting mode。该配置主要是为了实现suspend模式下也能发PWM波形。所有芯片均支持。
接口配置:
// 除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);
注意
- 所有通道默认pclk时钟源,如果想使用32K时钟源,调用
pwm_32k_chn_en使能对应通道,没有使能的通道仍是pclk时钟源。 - 32K时钟源,PWM设计的时候只考虑了suspend场景,在continuous mode、counting mode下使用中断,会提前一个32K时钟周期进入中断。使用32K PWM时如果需要中断,建议可以用GPIO中断来实现。
- 在PWM时钟源为48 MHz且占空比严格为50%的理想方波场景下,PWM输出波形的理论最高频率为24 MHz,每个输出周期至少需要2个PWM时钟周期(1个周期拉高,1个周期拉低),PWM最高输出频率 = pwm_frequency / 2。
占空比
PWM的一个signal frame由两个部分组成,分别为Count status(高电平时间)和Remaining status(低电平时间),一个信号帧的具体波形如下,其中tmax是周期时间。

驱动中设置signal frame周期和占空比的函数使用tcmp和tmax作为参数,与寄存器名 TCMP/TMAX对应。
通用设置占空比接口
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);
参数说明:
id:选择哪个PWM通道。tcmp:设置高电平持续时间。tmax:设置周期。
注意
- 设置PWM的周期参数类型是short型,最小取值为1,不能为0,如果为0,则pwm处于不工作的状态,取值范围为:1~65535。
- 设置PWM的占空比参数类型是short型,最小取值可以为0(这时pwm的波形一直是低电平),最大取值可以为周期值(这时pwm的波形一直是高电平),取值范围为:0~tmax。
- 当PWM正在发送过程中调用占空比或周期设置接口时,新的值将在下一个周期开始时生效。
IR FIFO Mode、IR DMA FIFO Mode shadow接口
当使用PWM0的IR FIFO Mode、IR DMA FIFO Mode时,还会使用到另外一个函数接口:
static inline void pwm_set_pwm0_tcmp_and_tmax_shadow(unsigned short max_tick, unsigned short cmp_tick);
注意
- 参数
max_tick设定pwm0的周期,参数cmp_tick设定pwm0的高电平持续时间,max_tick的取值范围:1~65536,cmp_tick的取值范围:0~max_tick。
脉冲个数设置接口
当使用PWM0的counting mode、IR mode时,pwm0需要设定脉冲的输出个数功能,使用到的函数接口:
static inline void pwm_set_pwm0_pulse_num(unsigned short pulse_num);
pulse_num:脉冲的个数,最大支持写入寄存器14bit的数,取值范围:0~16383。
IR FIFO配置数据接口
当pwm0向fifo中写入cfg data,使用到的函数接口:
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:使用
pwm_set_pwm0_tcmp_and_tmax_shadow函数下设置的周期和占空比。 - 0:使用
pwm_set_tmax、pwm_set_tcmp函数下设置的周期和占空比。
carrier_en:
- 1:按照参数pulse_num、use_shadow的设置输出pulse。
- 0:输出低电平,持续时间按照参数pulse_num、use_shadow进行计算。
对齐模式
支持中心对齐的芯片见PWM简介里的"芯片功能差异总表"。PWM支持两种对齐模式:边沿对齐(默认)和中心对齐。仅支持边沿对齐的芯片无法使能中心对齐。
边沿对齐
在此模式下,每个周期的波形会在边沿对齐(PWM2波形)。
中心对齐
在此模式下,前后两个周期的波形会在中心点对齐(PWM1波形)。

接口配置:
static inline void pwm_set_align_en(pwm_id_e id); // 使能中心对齐
static inline void pwm_set_align_dis(pwm_id_e id); // 禁用中心对齐(恢复边沿对齐)
相位偏移功能
支持移相功能的芯片见PWM简介里的"芯片功能差异总表"。移相功能可以设置PWM的相位偏移时间,相位偏移时间仅在第一个周期前生效(下图PWM2设置相位偏移时间为25us)。

接口配置:
static inline void pwm_set_shift_time(pwm_id_e id, unsigned short shift_clk_num);
注意
-
- 移相时间必须在设置占空比和周期之前设置。
-
- 如果想重新配置移相时间,必须在重新配置移相时间后重新配置tcmp和tmax(无论tcmp值和tmax值是否需要修改)。
-
- 如果只想重新配置tcmp和tmax,只需配置tcmp和tmax,无需先配置移相时间。
Invert/Polarity
通过占空比接口设定的波形,默认情况下先输出高电平Count status,后输出低电平Remaining status。
Invert和Polarity的区别:
- Invert功能:通过
pwm_invert_en使能PWM通道的invert功能,将会翻转PWM_PIN波形,无论pwm是否启动,都会生效,所以在pwm启动前使能invert功能,初始化低电平也会被翻转为高电平(初始化电平默认为低电平且无法修改),pwm启动后按照如下规则输出:Count status输出低,Remaining status输出高。

- Polarity功能:通过
pwm_set_polarity_en使能PWM通道的polarity功能,仅在pwm启动后生效,所以不会翻转pwm启动前的初始化电平,PWM启动后按照如下规则输出:Count status输出低,Remaining status输出高。

注意
- Invert和Polarity功能不能同时使能,只能选择其中一个功能。
- 在pwm启动后,Invert和Polarity功能使能之后会马上生效,不需要等待下一个周期。
相关接口:
static inline void pwm_invert_en(pwm_id_e id); // 使能PWM输出翻转
static inline void pwm_invert_dis(pwm_id_e id); // 禁用PWM输出翻转
static inline void pwm_n_invert_en(pwm_id_e id); // 使能PWM_N输出翻转
static inline void pwm_n_invert_dis(pwm_id_e id); // 禁用PWM_N输出翻转
static inline void pwm_set_polarity_en(pwm_id_e id); // 使能极性反转
static inline void pwm_set_polarity_dis(pwm_id_e id); // 禁用极性反转
功能说明
Continuous mode(连续模式)
该模式会一直持续按照设置的占空比发送signal。如果想停止则设置stop,设置之后则立刻停止。在发送期间,可以更新占空比,占空比会在下一个frame生效。
(1) 实验结果

上图为使用逻辑分析仪抓到的实验结果:
- 通道0(LED1):是PWM输出信号。
- 通道1(LED4):是中断标识GPIO,每发送一个信号帧,产生一次中断。
红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。
(2) Stop验证
使用下面实验验证continuous mode下,执行stop后,signal会立刻停止。

由图中可以看到,LED3翻转后,PWM的signal立刻停止。
(3) 占空比更新验证
使用下面实验验证continuous mode下,在发送期间,可以更新占空比,更新占空比后会在下一个frame生效。

由图中可以看到,LED3翻转后,PWM修改占空比后,会在下一个frame生效。
配置接口:
pwm_set_pwm0_mode(PWM_NORMAL_MODE); // PWM0设置模式
pwm_start(FLD_PWM0_EN); // 启动PWM
pwm_stop(FLD_PWM0_EN); // 停止PWM
Counting Mode(计数模式)
发送设置数量的signal frame则停止。该模式下如果stop会立刻停下。该模式下发送过程中,修改占空比,不会改变占空比。
(1) COUNT_FRAME_INIT实验结果

- 通道0(LED1):是PWM输出信号。
- 通道1(LED4):是中断标识GPIO,每发送一个信号帧,产生一次中断。
红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。
(2) COUNT_PNUM_INIT实验结果

红框的具体情况如下:

- 通道0(LED1):是PWM输出信号。
- 通道1(LED4):是中断标识GPIO,指定脉冲个数发完,产生中断。
红框说明:比50us多3us,说明进入中断有一定的时延,CPU进入中断需要一定的软件和硬件处理的时间。
(3) Stop验证

由图中可以看到,LED3翻转后,PWM的signal立刻停止。
(4) 占空比验证
使用下面实验验证counting mode下,在发送期间,更改占空比,占空比不可以改变。

由图中可以看到,LED3翻转后,PWM的signal没有发生改变。
配置接口:
pwm_set_pwm0_mode(PWM_COUNT_MODE);
pwm_set_pwm0_pulse_num(PWM_PULSE_NUM); // 设置脉冲个数
pwm_start(FLD_PWM0_EN);
IR Mode(红外模式)
IR mode会连续发送pulse groups。中间可以改变占空比,会在下一个pulse group生效。如果想立刻停止,可以直接stop。IR mode与count的区别,count发送一个pulse groups就停止不再发送,而IR mode会持续不断地发送pulse groups。
(1) 实验结果

- 通道0(LED1):是PWM输出信号。
- 通道1(LED3):是中断标识GPIO,一个脉冲组发完翻转一次。
红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。
(2) Stop验证

由图中可以看到,LED3翻转后,PWM的signal立刻停止。
(3) 占空比更新验证
使用下面实验验证IR mode下,中间可以改变占空比,但会在当前pulse group执行完生效。

由图中可以看到,LED3翻转后,pwm的signal并没有立刻改变,而是在当前pulse group执行完生效。
如果想停止IR mode并想完成当前的pulse group,则可以切换到counting mode。
如果想立刻停止,可以直接stop。
注意
- 如果想停止IR mode并想完成当前的pulse group,则在中断中可以切换到counting mode,但在切换的过程中,会在当前IR mode模式下的pulse group发完,才会切换过来。
配置接口:
pwm_set_pwm0_mode(PWM_IR_MODE);
pwm_set_pwm0_pulse_num(PWM_PULSE_NUM); // 设置每个pulse group的脉冲个数
pwm_start(FLD_PWM0_EN);
IR FIFO mode(红外FIFO模式)
在没有MCU的干预下,可以发送长代码模式,IR的载波频率是由系统时钟分频得到的,可以支持通用频率,"Fifo cfg data"这个element作为IR波形的基础单元,硬件会解析cfg信息发出对应的signal。
(1) 实验结果

- 通道0(LED1):是PWM输出信号。
- 通道1(LED4):是中断标识GPIO,当FIFO里面的cfg data小于(不包括等于)设置的值(trigger_level为1)的时候翻转一次。
红框说明:产生中断的时延,CPU进入中断需要一定的软件和硬件处理的时间。
(2) Stop验证
使用下面实验验证IR FIFO mode下,执行stop后,只是停止当前cfg data的执行,不影响fifo后面的cfg data执行。

由图中可以看到,执行stop后,LED3反转,停止当前cfg data1的执行,不影响fifo后面的cfg data2的执行。
IR FIFO Mode依次取出FIFO中的cfg data并发出对应signal,直到fifo为空为止。在该模式下,可以使用stop,但只是停止当前cfg data的执行,不影响fifo后面的cfg data执行。
注意
- 在IR FIFO模式下,只要fifo有数据就会一直往外发送(自动发送),不需要start信号,同时IR DMA FIFO Mode也不需要start信号,但在其他模式下,都需要
pwm_start信号。 - 每调用函数
pwm_set_pwm0_ir_fifo_cfg_data,则FIFO的cnt会加1(如果这时FIFO为满,就会等待,直到FIFO不满,就会写入),硬件从FIFO中取出一个,则FIFO中的cnt会减1。FIFO的深度为8 bytes。数据从FIFO中取出后,执行发送signal动作,只有当前signal执行完之后,才会从FIFO中取出下一个。
FIFO状态查询接口:
static inline unsigned char pwm_get_pwm0_ir_fifo_data_num(void); // 获取FIFO中数据个数
static inline unsigned char pwm_get_pwm0_ir_fifo_is_empty(void); // 判断FIFO是否为空
static inline unsigned char pwm_get_pwm0_ir_fifo_is_full(void); // 判断FIFO是否为满
static inline void pwm_clr_pwm0_ir_fifo(void); // 清空FIFO数据
配置接口:
pwm_set_pwm0_mode(PWM_IR_FIFO_MODE);
pwm_set_pwm0_ir_fifo_cfg_data(PWM_PULSE_NUM1, 1, 1); // 写入FIFO配置数据
pwm_set_pwm0_ir_fifo_cfg_data(PWM_PULSE_NUM2, 0, 1);
IR DMA FIFO mode(红外DMA FIFO模式)
IR DMA FIFO模式,与IR FIFO模式相似,只是配置不是直接由MCU写在FIFO中,而是通过DMA写到FIFO中。所有芯片均支持。
(1) PWM_IR_FIFO_DMA实验结果

- 通道0(LED1):是PWM输出信号。
- 通道1(LED4):是中断标识GPIO。
从上图可以看出,在该模式下,当FIFO的cfg data数据全部执行完才触发中断,与IR FIFO Mode的中断机制不一样。
下图为上图中红色框的放大框:

从图中可以看出cfg data3的最后一个低电平的维持时间是105us(并不是cfg data3设置的100us),所以可以看出,在这种使用方法下,发完第一组DMA数据后,中断中重新触发DMA,在signal上是有一段时延的。
(2) PWM_CHAIN_DMA实验结果
使用链式DMA可以实现无MCU干预的连续发送,链表结构如下:

先建立头结点head_of_list,然后向循环链表添加结点。从流程图可以看出,先开始执行头指针,然后依次循环执行各结点,直到LLP设置为0才会停止。

对红框进行详细说明,指针1和指针2的cfg data在切换的过程中,没有时延的产生。


由图可得,没有延时的产生,所以使用链表的方式可以发出连续的PWM波形。
注意
- 在中断里面需要更新DMA的部分配置:源地址的更新,DMA触发等。
- 在该模式下,与IR FIFO不同的地方,并不是FIFO中的cfg data数量为空的时候触发中断,而是将fifo中的配置pwm信号帧全部执行完才会触发中断。
DMA配置接口:
void pwm_set_dma_config(dma_chn_e chn); // 配置DMA通道
void pwm_set_dma_buf(dma_chn_e chn, unsigned int buf_addr, unsigned int len); // 设置DMA缓冲区
void pwm_ir_dma_mode_start(dma_chn_e chn); // 启动DMA
DMA链表配置接口(用于链式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);
注意
- 使用链式DMA时,源地址
src_addr必须按字(4字节)对齐,否则程序会进入异常。
中断
PWM支持的中断设置说明如下(硬件不会自动清除中断标志位,需要软件手工清除)。
PWM0支持的中断
| 中断类型 | 说明 |
|---|---|
| FLD_PWM_FRAME_DONE_IRQ | 每个signal frame完成,会产生中断 |
| FLD_PWM0_PNUM_IRQ | 每发完一个pulse groups,则会产生中断 |
| FLD_PWM0_IR_FIFO_IRQ | 当FIFO里面的cfg data小于(不包括等于)设置的值(trigger_level)时,进入中断 |
| FLD_PWM0_IR_DMA_FIFO_IRQ | 当FIFO执行完DMA发送的cfg data之后,进入中断 |
其他通道支持的中断
| 中断类型 | 说明 |
|---|---|
| FLD_PWM_FRAME_DONE_IRQ | 每个signal frame完成,会产生中断 |
注意
- 所有芯片的其他通道均仅支持 frame done 中断。
中断配置接口
TC系列:
void pwm_set_interrupt_enable(PWM_IRQ irq); // 使能PWM中断
void pwm_set_interrupt_disable(PWM_IRQ irq); // 禁用PWM中断
unsigned char pwm_get_interrupt_status(PWM_IRQ status); // 获取PWM中断状态
void pwm_clear_interrupt_status(PWM_IRQ status); // 清除PWM中断状态
说明
- TC系列中断掩码和状态使用
PWM_IRQ枚举(如PWM_IRQ_PWM0_FRAME、PWM_IRQ_PWM0_PNUM、PWM_IRQ_PWM0_IR_FIFO、PWM_IRQ_PWM0_IR_DMA_FIFO_DONE),每个通道的中断类型独立枚举(如PWM_IRQ_PWM1_FRAME、PWM_IRQ_PWM2_FRAME等)。其中PWM_IRQ_PWM0_IR_FIFO需通过独立的reg_pwm0_fifo_mode_irq_mask/reg_pwm0_fifo_mode_irq_sta寄存器配置,接口内部已做区分处理。
TL系列:
void pwm_set_irq_mask(pwm_irq_e mask); // 使能PWM中断屏蔽
void pwm_clr_irq_mask(pwm_irq_e mask); // 禁用PWM中断屏蔽
unsigned char pwm_get_irq_status(pwm_irq_e status); // 获取PWM中断状态
void pwm_clr_irq_status(pwm_irq_e status); // 清除PWM中断状态
TL322x:
void pwm_set_irq_mask(pwm_id_e id, pwm_irq_type_e type); // 使能PWM中断屏蔽
void pwm_clr_irq_mask(pwm_id_e id, pwm_irq_type_e type); // 禁用PWM中断屏蔽
unsigned int pwm_get_irq_status(pwm_id_e id, pwm_irq_type_e type); // 获取PWM中断状态
void pwm_clr_irq_status(pwm_id_e id, pwm_irq_type_e type); // 清除PWM中断状态
接口差异: TC系列使用 pwm_set_interrupt_enable/disable + pwm_clear_interrupt_status(独立寄存器区分 IR FIFO 中断);TL系列使用 pwm_set_irq_mask/clr_irq_mask + pwm_clr_irq_status;TL322X需额外传入 pwm_id_e 通道参数。
注意
- 响应中断时,存在时延,时间大约在2~4us左右。
- 中断处理函数中应先清除中断标志位再处理业务,避免重复进中断。
IR FIFO模式触发级别设置
IR FIFO模式trigger_level的值可以通过以下函数接口进行配置:
static inline void pwm_set_pwm0_ir_fifo_irq_trig_level(unsigned char trig_level);
当FIFO中的数据个数小于此值时,触发中断。
注意事项
- 通道差异: PWM0支持所有5种模式,其他通道仅支持continuous mode。
- 32K时钟源: 使用32K时钟源时只能工作在continuous mode和counting mode,且不支持分频。支持的芯片见"芯片功能差异总表"。
- DMA地址对齐: 使用DMA功能时,源地址必须按字(4字节)对齐,否则程序会进入异常。
- FIFO深度: IR FIFO的深度为8 bytes。
-
占空比更新:
- continuous mode下,更新占空比会在下一个frame生效。
- counting mode下,发送过程中修改占空比不会改变占空比。
- IR mode下,中间可以改变占空比,但会在当前pulse group执行完生效。
-
Stop行为:
- continuous mode和counting mode下,stop会立刻停止signal输出。
- IR FIFO mode下,stop只是停止当前cfg data的执行,不影响fifo后面的cfg data执行。
-
高级特性: 中心对齐、相位偏移等高级特性仅部分芯片支持,详见PWM简介里的"芯片功能差异总表"。
- 32K时钟中断时延: 使用32K时钟源时,在continuous mode、counting mode下使用中断会提前一个32K时钟周期进入中断。如需中断,建议用GPIO中断实现。
I2C
简介
I2C(Inter-Integrated Circuit)是一种由数据线SDA和时钟线SCL组成的串行总线。它可以发送和接收数据,是一种半双工通信方式。时钟由主设备端控制。

I2C通信协议详细说明如下:
| 状态 | 过程 |
|---|---|
| 空闲状态 | 当I2C总线的SDA和SCL信号同时为高电平时,规定为总线的空闲状态。 |
| 起始信号 | 在SCL为高电平期间,SDA从高电平跳变为低电平。 |
| 停止信号 | 在SCL为高电平期间,SDA从低电平跳变为高电平。 |
| 应答信号 | 反馈有效应答位ACK的要求是:接收器在第9个时钟脉冲的低电平期间将SDA信号拉低,并在该时钟的高电平期间保持稳定的低电平。如果接收器是主设备,则在接收到最后一个字节后发送NACK信号,通知被控发送器结束数据传输并释放SDA信号,以便主设备接收器可以发送停止信号P。 |
| 数据有效性 | 当I2C总线用于数据传输时,数据线上的数据在时钟信号为高电平期间必须保持稳定。只有在时钟线上的信号为低电平期间,才允许数据线上的高或低状态发生变化。 |
| 数据传输 | I2C总线上传输的每一位数据都有一个对应的时钟脉冲(或同步控制),即每一位数据都在SCL串行时钟的配合下,在SDA上逐位串行传输。 |
芯片概述
I2C模块芯片之间设计原理和接口封装存在差异,分类情况如下:
| 分类 | 芯片 |
|---|---|
| 第一类 | B91/B92/TL751X/TL721X/TL321X/TL322X/TL323X |
| 第二类 | B85/B87/B80/B80B/TC321X/TC123X |
第一类I2C
| 芯片 | I2C模块 | 主/从模式 | DMA | Stretch |
|---|---|---|---|---|
| B91 | 一路I2C | 主+从 | 支持 | 有限支持 |
| B92/TL751X/TL721X/TL321X | 一路i2c + 一路i2c1_m | i2c: 主+从, i2c1_m: 仅主 | 支持 | 支持 |
| TL322X/TL323X | 二路i2c | i2c: 主+从 | 支持 | 支持 |
I2C特性
B91
- GPIO引脚选择用于SDA/SCL
- 非DMA和DMA数据传输模式
- 基本stretch功能支持
- DMA传输长度:根据协商的长度进行配置
其他芯片 I2C特性
- GPIO引脚选择用于SDA/SCL
- 非DMA和DMA数据传输模式
- 增强的stretch功能,以及支持中断
- ID阶段和数据阶段的NACK检测
- DMA write_num功能,用于上报接收数据长度
- 最大DMA传输长度:0xFFFFFC字节(B92)、任意长度(除B92其他芯片)
中断
I2C 中断用于通知 MCU 各类通信事件的发生,包括 FIFO 触发、帧传输完成、NACK 检测、Stretch 状态等。不同芯片(B91、其他芯片)支持的中断类型有所差异,本文档将详细说明各中断的触发条件、清除方式。
中断类型与触发条件
- B91 中断
| 中断 | 触发条件 | 自动/手动清除 |
|---|---|---|
| I2C_TXDONE_STATUS | 检测到停止信号时触发 | 需手动清除 |
| I2C_TX_BUF_STATUS | TX FIFO 计数 <= tx_irq_trig_lev 时触发 | 自动清除 |
| I2C_RXDONE_STATUS | 检测到停止信号时触发 | 需手动清除 |
| I2C_RX_BUF_STATUS | RX FIFO 计数 >= i2c_rx_irq_trig_cnt 时触发 | 读取数据后自动清除 |
- 其他芯片中断
| 中断 | 触发条件 | 自动/手动清除 |
|---|---|---|
| I2C_SLAVE_WR_STATUS | 从设备解析到主设备读/写命令时触发 | 需手动清除 |
| I2C_MASTER_NAK_STATUS | 主设备检测到 NACK 时触发 | 需手动清除 |
| I2C_RX_BUF_STATUS | RX FIFO 计数 >= FLD_I2C_RX_IRQ_TRIG_LEV 时触发 | 自动清除,但手动清除会复位 RX FIFO 指针 |
| I2C_TX_BUF_STATUS | TX FIFO 计数 <= FLD_I2C_TX_IRQ_TRIG_LEV 时触发 | 自动清除 |
| I2C_RX_DONE_STATUS | 检测到停止信号时触发 | 需手动清除 |
| I2C_TX_DONE_STATUS | 检测到停止信号时触发 | 需手动清除 |
| I2C_RX_END_STATUS | 接收到一帧数据时触发(停止信号已发送) | 需手动清除 |
| I2C_TX_END_STATUS | 发送完一帧数据时触发(停止信号已发送) | 需手动清除 |
| I2C_STRETCH_STATUS | 配合 Stretch 功能,TX FIFO 为空或 RX FIFO 为满时触发 | 需手动清除 |
STRETCH功能
master: 默认将stretch使能,当scl被拉低,则master端硬件状态机处于当前状态,直到scl slave端释放之后,master端硬件状态机才会继续执行。
slave端:通过是否调用i2c_slave_stretch_en() API来使能stretch功能。当slave端rxfifo为满或者txfifo为空,则触发scl拉低,当不满足触发条件之后则自动释放scl。
注意
- B91 i2c slave端发送期间,如果stretch使能的话,当i2c slave端发送完,满足stretch的条件,因此会一直拉着,发送完手动关闭,是会释放恢复的;B91之后的芯片没有问题,在i2c slave发送时,会有长度判断,达到长度后,会将stretch释放。
- B91解决方案:stretch功能一直处于使能状态,在配置tx_dma之后,将其关闭,然后在使能的地方,判断i2c slave端tx done的状态,如果置1,则将其使能。
GPIO引脚配置
引脚选项
void i2c_set_pin(gpio_func_pin_e sda_pin, gpio_func_pin_e scl_pin);
引脚配置自动完成以下操作:
- 使能SDA和SCL的输入
- 配置10K上拉电阻
- 设置mux功能为I2C
- 禁用GPIO功能
I2C工作模式
Telink I2C 驱动支持主设备(Master)和从设备(Slave)两种工作模式,同时支持非 DMA(NDMA)和 DMA 两种数据传输方式,关于nodma/dma内部设计工作机制,参考datasheet i2c章节。
- I2C Master(主设备)
a. 初始化与配置
NDMA/DMA 通用初始化:
// 1. 初始化 SDA/SCL 引脚
i2c_set_pin(sda_pin, scl_pin);
// 2. 配置 I2C 时钟频率
i2c_set_master_clk((unsigned char)(sys_clk.pclk * 1000 * 1000 / (4 * 400000)));
// 3. 使能主设备功能
i2c_master_init();
DMA初始化:
// 配置 TX/RX DMA 通道
i2c_set_tx_dma_config(DMA1);
i2c_set_rx_dma_config(DMA0);
b. 中断配置
| 模式 | B91中断配置 | 其他芯片中断配置 |
|---|---|---|
| NDMA 写 | 轮询方式,无需配置中断 | 轮询方式,无需配置中断 |
| NDMA 读 | 轮询方式,无需配置中断 | 轮询方式,无需配置中断 |
| DMA 写 | 无需配置中断 | i2c_set_irq_mask(I2C_MASTER_NAK_MASK) |
| DMA 读 | 无需配置中断 | i2c_set_irq_mask(I2C_MASTER_NAK_MASK) |
c. 中断响应(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. 数据收发
NDMA 模式:
// 主设备写(轮询)
unsigned char ret = i2c_master_write(0x5a, tx_data, len);
// ret = 0: ID 或数据阶段收到 NACK
// ret = 1: 写成功
// ret = DRV_API_TIMEOUT: 超时返回
// 主设备读(轮询)
unsigned char ret = i2c_master_read(0x5a, rx_data, len);
DMA 模式:
// 主设备 DMA 写
i2c_master_write_dma(0x5a, tx_data, len);
while (i2c_master_busy());
// 主设备 DMA 读
i2c_master_read_dma(0x5a, rx_data, len);
while (i2c_master_busy());
注意
- B91: 只是检测id阶段,若检测到NCA,发送停止信号并中止当前操作。
- 其他芯片: 在 NDMA 和 DMA 模式下均支持 ID 阶段和数据阶段的 NACK 检测。若检测到 NACK,发送停止信号并中止当前操作。
- 从模式初始化
a. Slave NDMA 模式
从设备非 DMA 模式下,MCU 直接通过 I2C 数据寄存器逐字节读写数据。接收数据时通常使用中断方式,通过 RX FIFO 触发级别中断和帧结束中断配合完成一帧数据的接收。
初始化与配置:
// 1. 初始化 SDA/SCL 引脚
i2c_set_pin(sda_pin, scl_pin);
// 2. 使能从设备功能并设置 ID
i2c_slave_init(0x5a);
中断配置:
从设备写(主设备读从设备)
| 芯片 | Stretch | 中断配置 |
|---|---|---|
| B91 | 不支持 | 无相关中断,需提前填充数据 |
| 其他芯片 | 不使用 | 无相关中断,需提前填充数据 |
| 其他芯片 | 使用 | i2c_set_irq_mask(I2C_SLAVE_WR_MASK) |
从设备读(主设备写从设备)
| 芯片 | 中断配置 |
|---|---|
| B91 | i2c_rx_irq_trig_cnt() + i2c_set_irq_mask(I2C_RX_BUF_MASK \| I2C_RX_DONE_MASK) |
| 其他芯片 | i2c_rx_irq_trig_cnt() + i2c_set_irq_mask(I2C_RX_BUF_MASK | I2C_RX_END_MASK) |
数据收发:
从设备写(响应主设备读)
不使用 Stretch:
// 需在主设备发送读命令前预先填充数据
i2c_slave_write(tx_data, len);
// FIFO 仅 8 字节,需提前准备
使用 Stretch(其他芯片):
// 在 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_write(tx_data, len);
}
}
}
Stretch 使用要点: 当从设备使用 Stretch 功能时,通过 I2C_SLAVE_WR_MASK 中断判断主设备何时读取数据,然后填充数据。当从设备不使用 Stretch 功能时,若使用 I2C_SLAVE_WR_MASK 中断判断主设备读取时机,MCU 可能来不及处理,因此需要在主设备读取数据前预先填充数据。
从设备读(响应主设备写)
B91 中断处理:
#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;
}
}
其他芯片中断处理:
#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;
}
}
数据收发(注意事项)
- B91 从设备写时,FIFO 大小仅 8 字节,需在主设备读之前预先填充数据。
- 其他芯片 不使用 Stretch 时,同样需预先填充数据;使用 Stretch 时,可在
I2C_SLAVE_WR_MASK中断中动态填充。 i2c_rx_irq_trig_cnt()推荐设置为 1 或 4,范围小于 8。
b. Slave DMA 模式
从设备 DMA 模式下,DMA 控制器自动从内存搬运数据到 I2C TX FIFO(响应主设备读)或从 I2C RX FIFO 搬运到内存(响应主设备写)。
初始化与配置:
// 1. 初始化 SDA/SCL 引脚
i2c_set_pin(sda_pin, scl_pin);
// 2. 使能从设备功能并设置 ID
i2c_slave_init(0x5a);
// 3. 配置 DMA 通道
i2c_set_tx_dma_config(DMA1);
i2c_set_rx_dma_config(DMA0);
中断配置:
从设备 DMA 写(响应主设备读)
| 芯片 | Stretch | 中断配置 |
|---|---|---|
| B91 | 不支持 | i2c_set_irq_mask(I2C_TX_DONE_MASK)(使用前清除 I2C_TX_DONE_CLR) |
| 其他芯片 | 不使用 | i2c_set_irq_mask(I2C_TX_END_MASK) |
| 其他芯片 | 使用 | i2c_set_irq_mask(I2C_SLAVE_WR_MASK) |
从设备 DMA 读(响应主设备写)
| 芯片 | 中断配置 |
|---|---|
| B91 | dma_set_irq_mask(TC_MASK) |
| 其他芯片 | dma_set_irq_mask(TC_MASK) 或 i2c_set_irq_mask(I2C_RX_END_MASK) |
数据收发:
从设备 DMA 写(响应主设备读)
B91:
// 初始化时清除 TX_DONE 状态
i2c_clr_irq_status(I2C_TX_DONE_CLR);
i2c_set_irq_mask(I2C_TX_DONE_MASK);
// 配置 DMA 写数据
i2c_slave_set_tx_dma(tx_data, len);
// 中断处理
void i2c_irq_handler(void)
{
if (i2c_get_irq_status(I2C_TXDONE_STATUS)) {
i2c_clr_irq_status(I2C_TX_DONE_CLR);
// 配置下一次 TX DMA
}
}
其他芯片使用 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);
}
}
}
从设备 DMA 读(响应主设备写)
B91:
// 配置 DMA 读数据
i2c_slave_set_rx_dma(rx_data, len);
// 通过 DMA TC 中断判断完成
dma_set_irq_mask(TC_MASK);
// 中断处理
void dma_irq_handler(void)
{
if (dma_get_tc_irq_status(I2C_RX_DMA_STATUS)) {
dma_clr_tc_irq_status(I2C_RX_DMA_STATUS);
// 配置下一次 RX DMA
}
}
其他芯片
// 配置 DMA 读数据
i2c_slave_set_rx_dma(rx_data + 4, DMA_REV_LEN);
// 方式1:通过 DMA TC 中断
dma_set_irq_mask(TC_MASK);
// 方式2:通过 I2C_RX_END_MASK 中断
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);
// 配置下一次 RX DMA
i2c_slave_set_rx_dma(rx_data + 4, DMA_REV_LEN);
}
}
注意
- buff数据缓冲区必须按字(4 字节)对齐。
- B91 使用
I2C_TX_DONE_MASK前,必须手动清除I2C_TX_DONE_CLR,否则会一直进入中断。 I2C_TX_DONE_MASK只表示数据部分已发送,不包括停止信号;其他芯片建议使用I2C_TX_END_MASK- 除B91其他芯片 DMA 读时,接收到的数据长度会写入缓冲区前 4 字节。读缓冲区大小为
BUFF_DATA_LEN_DMA + 4,前 4 字节存放实际接收长度。
- I2C1_M 模块(仅主模式,仅 NDMA)
a. 初始化与配置
// 1. 初始化 SDA/SCL 引脚
i2c1_m_set_pin(sda_pin, scl_pin);
// 2. 配置 I2C 时钟频率
i2c1_m_set_master_clk((unsigned char)(sys_clk.pclk * 1000 * 1000 / (4 * 100000)));
// 3. 使能主设备功能
i2c1_m_master_init();
b. 数据收发
// i2c1_m 写(可附带地址,将地址放入 txbuff 作为数据一起发送)
i2c1_m_master_write(0x5a, tx_data, len);
// i2c1_m 读
i2c1_m_master_read(0x5a, rx_data, len);
// i2c1_m 写后读
i2c1_m_master_write_read(0x5a, wr_data, wr_len, rd_data, rd_len);
DEMO说明
- 硬件连接
通过两块板子测试I2C通信时:
| 主设备 | 从设备 |
|---|---|
| SCL | SCL |
| SDA | SDA |
| GND | GND |
重要:
- 先给从设备上电,再给主设备上电(避免数据错误)
- 两块板子之间必须有共地连接
- NODMA
a. 配置宏
#define I2C_MASTER_DEVICE 1 // i2c master demo
#define I2C_SLAVE_DEVICE 2 // i2c slave demo
#define I2C_DEVICE I2C_SLAVE_DEVICE // 选择主设备或从设备
#if !defined(MCU_CORE_B91)
#define I2C_STRETCH_EN 0
#define I2C_STRETCH_DIS 1
#define I2C_STRETCH_MODE I2C_STRETCH_EN // 选择 Stretch 模式
#endif
#define I2C_CLK_SPEED 400000 // I2C 时钟 400K
#define SLAVE_RX_IRQ_TRIG_LEVEL 4 // 从设备 RX 中断触发级别
#define BUFF_DATA_LEN_NO_DMA 32 // 数据缓冲区长度
b. 测试说明
主设备(Master)
( I2C_DEVICE I2C_MASTER_DEVICE)
数据流:
将 bin 文件烧录到主设备
主设备向从设备写入数据,然后读回数据
比较写入和读回的数据是否一致
从设备(Slave)
(I2C_DEVICE I2C_SLAVE_DEVICE)
数据流:
将 bin 文件烧录到从设备
不使用 Stretch(I2C_STRETCH_MODE I2C_STRETCH_DIS):
从设备接收主设备数据,然后将接收到的数据写回主设备
使用 Stretch(除B91其他芯片)(I2C_STRETCH_MODE I2C_STRETCH_EN):
另一种与主设备通信的机制,从设备接收数据后写回主设备
- DMA
a. 配置宏
#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 200000 // I2C 时钟 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 与传输长度一致
// B92: DMA_REV_LEN 可设为最大值 0xFFFFFC,启用 write_num
// 除B91\B92以外的芯片: DMA_REV_LEN 为任意 4 字节对齐长度
#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. 测试说明
主设备(Master)数据流:
( I2C_DEVICE I2C_MASTER_DEVICE)
- 将 bin 文件烧录到主设备
- 主设备向从设备写入数据,然后读回数据
- 比较写入和读回的数据是否一致
从设备(Slave)数据流:
(I2C_DEVICE I2C_SLAVE_DEVICE)
- 不使用 Stretch (I2C_STRETCH_MODE I2C_STRETCH_DIS):从设备接收主设备数据,然后将接收到的数据写回主设备(提前配置rxdma,在rx dma中配置tx dma,在tx 中断中配置rxdma,demo是这样交替配置的)
- 使用 Stretch(除B91其他芯片)(I2C_STRETCH_MODE I2C_STRETCH_EN):从设备接收数据后写回主设备
- 测试结果
使用逻辑分析仪捕获时序:
通道0:SCL信号
通道1:SDA信号
通道2:LED2(错误指示)
主设备发送数据:

主设备接收数据:

特别说明
- I2C写操作时,从设备在主设备写完最后一个字节后响应ACK,然后主设备发送停止信号结束通信。
第二类I2C模块
8 系列(B85m)I2C 模块与 9 系列存在较大差异:8 系列没有真正的 DMA 传输模式,master 端数据收发由 MCU 顺序读写寄存器完成,slave 端则由硬件自动解析总线数据,MCU 无需逐字节参与。
芯片概述与特性
主要特性:
- GPIO 引脚可配置用于 SDA/SCL,引脚内部使能输入并上拉 10K
- 支持主(Master)和从(Slave)两种工作模式
- Slave 端支持两种数据组织模式:MAPPING 模式与 DMA 模式(注意:DMA 模式名称沿用历史命名,实际并非真正的 DMA 传输,详见 Slave 模式下的工作模式说明)
- Master 数据传输完全由 MCU 顺序控制(无 DMA)
- Slave 端数据收发由硬件自动完成,MCU 仅需在中断中获知读写事件
- 支持 Slave ID 阶段中断(主机读写命令到达时触发)
- 支持单字节、双字节、三字节地址访问(AddrLen 0/1/2/3)
- 最高速率:经硬件实测可稳定支持到 1 Mbps(更高速率与硬件环境相关,未做进一步验证)
中断
8 系列 I2C 仅 Slave 端产生中断,用于通知 MCU 主设备对其发起的读/写操作。中断类型如下:
| 中断 | 触发条件 | 清除方式 |
|---|---|---|
| HOST_CMD_IRQ | 主设备发起读或写命令(ID 解析完成)时触发 | 写状态寄存器手动清除 |
| HOST_READ_IRQ | 仅主设备发起读命令时触发 | 写状态寄存器手动清除 |
说明:
- 读/写事件都会触发
HOST_CMD_IRQ,读事件额外置位HOST_READ_IRQ,因此可在中断中通过HOST_READ_IRQ区分读还是写。 - 两个状态位需在处理完后一并清除(写寄存器赋值清除)。
- 中断使能需通过
irq_set_mask(FLD_IRQ_MIX_CMD_EN)打开 I2C 混合命令中断,再调用irq_enable()。
相关接口:
// 获取中断状态
unsigned char i2c_get_interrupt_status(i2c_irq_e irq_status);
// 清除中断状态
void i2c_clear_interrupt_status(i2c_irq_e irq_status);
GPIO引脚配置
8 系列使用 i2c_gpio_set() 配置 SDA/SCL 引脚,函数内部自动完成:
- 使能 SDA / SCL 引脚输入
- 配置 10K 上拉电阻
- 设置 mux 功能为 I2C
- 禁用对应 GPIO 功能
void i2c_gpio_set(GPIO_PinTypeDef sda_pin, GPIO_PinTypeDef scl_pin);
B85 使用单一参数的 i2c_gpio_set(I2C_GPIO_SDA_SCL) 形式,其他芯片使用双参数形式。
I2C工作模式
工作模式:MAPPING 模式与 DMA 模式,工作机制介绍如下:
I2C_MAPPING_MODE:
slave端:定义一个接收buff,将该buff的地址写入到寄存器中;
Master端:发送数据时无需发送地址信息,slave端会将master发送的数据存放到定义的buff地址中
当Master端读取数据时也无需发送地址信息,slave端会发送定义的BUff地址的数据
该模式有一个使用要求:slave端定义的Buff需要是128字节对齐,并且128个地址可以随意写,但读的话只能读后64个字节:
128字节对齐

所以为了保证写进去的和读出来的内容一致,所以给slave寄存器传的地址信息为:
i2c_slave_init(0x5C,I2C_SLAVE_MAP,(unsignedchar*)i2c_slave_mapping_buff+64);将i2c slave的buff地址存入到以下寄存器中,当master端读写时,slave会从该寄存器存入的地址信息存数据或者读取数据:
Note
- 虽然master端只能读取后64个字节,slave端是可以任意访问这128个字节的
I2C_DMA_MODE: 名字虽然命名为dma mode,但其实不是dma传输。
- slave端:不需要定义接收Buff
- Master端:master发送和接收时需要发送address信息,然后slave端解析到master发送过来的地址会接收的数据存放到该地址中,或者发送该地址的内容给master。
- Master 模式
初始化与配置:
// 1. 配置 SDA/SCL 引脚
i2c_gpio_set(I2C_GPIO_SDA, I2C_GPIO_SCL);
// 2. 主设备初始化:设置 Slave ID 与时钟分频
// SlaveID 最低位为读/写位(R:High W:Low),例如 0x5C 写为 0x5C,读为 0x5D
// DivClock = CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)
i2c_master_init(0x5C, (unsigned char)(CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)));
数据收发接口(MCU 顺序控制,非 DMA):
| 接口 | 用途 |
|---|---|
i2c_write_series(Addr, AddrLen, dataBuf, dataLen) |
向指定地址写一段数据 |
i2c_read_series(Addr, AddrLen, dataBuf, dataLen) |
从指定地址读一段数据 |
参数说明:
Addr:slave 内部寄存器/SRAM 地址,对应 master 时序中的地址阶段AddrLen:地址长度,0/1/2/3 字节- 0:MAPPING 模式无需发送地址
- 1/2/3:发送 1/2/3 字节地址
dataBuf:数据缓冲区dataLen:数据长度(字节)
写时序:
start + device_id(W) + [addr(0/1/2/3 byte)] + data(1..n byte) + stop
读时序:
start + device_id(W) + [addr(0/1/2/3 byte)] + restart + device_id(R) + data(1..n byte) + stop
MAPPING 模式下 master 用法示例:
// MAPPING 模式下 AddrLen=0,无需发送地址
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);
DMA 模式下 master 用法示例:
// 需要发送 slave 内部 SRAM 地址(3 字节)
#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 模式
a. MAPPING 模式
原理:
- slave 端在 SRAM 中定义一个 128 字节对齐的 buffer,将该 buffer 地址写入硬件寄存器:
- buffer 必须 128 字节对齐
- 128 字节内 master 可任意写,但但但只能读后 64 字节但但
- 为保证 master 写入与读出数据一致,slave 端初始化时传入的地址需偏移 +64:
在 MAPPING 模式下,slave 端数据由硬件自动搬运,无需 MCU 逐字节处理。
b. SDMA 模式
说明:
此处的 "DMA 模式" 是历史命名,,,实际并非真正的 DMA 传输,,。其本质是 slave 端根据 master 发送的地址字段自动解析数据存放位置(任意 SRAM 地址,地址 3 字节)。
i2c_gpio_set(I2C_GPIO_SDA, I2C_GPIO_SCL);
// slave 初始化:DMA 模式,无需传入 buffer(pMapBuf 传 0)
i2c_slave_init(0x5C, I2C_SLAVE_DMA, 0);
注意
- master 端必须发送 3 字节地址(AddrLen=3)来指定 slave SRAM 中的目标位置。
- slave 端无需定义接收 buffer,由硬件直接解析地址并存放/读取 SRAM 数据。
demo说明
demo 路径:demo/vendor/I2C_Demo,通过 app_config.h 中的 I2C_MODE 宏切换工作模式:
#define I2C_DMA_MODE 1
#define I2C_MAPPING_MODE 2
#define I2C_MODE 1 // 选择 1(DMA)或 2(MAPPING)
并通过 I2C_DEVICE 宏选择编译为 master 还是 slave:
#define I2C_MASTER_DEVICE 1
#define I2C_SLAVE_DEVICE 2
#define I2C_DEVICE I2C_MASTER_DEVICE
(1) 硬件连接
| 主设备 | 从设备 |
|---|---|
| SCL | SCL |
| SDA | SDA |
| GND | GND |
重要:
先给从设备上电,再给主设备上电(避免数据错误);两块板子必须共地。
(2) MAPPING 模式测试说明
- Master 数据流: 烧录 master 固件 → 周期性向 slave 写入数据,再读回数据 → 可对比写入与读出数据是否一致(demo 中将
i2c_tx_buff[0]++自增校验)。 - Slave 数据流: 烧录 slave 固件 → 接收主设备数据并存入
i2c_slave_mapping_buff→ 主设备读取时 slave 自动从 buffer 中返回数据。 - 中断仅用于统计读写次数并翻转 LED 指示,数据搬运完全由硬件完成。
(3) DMA 模式测试说明
- Master 数据流: 烧录 master 固件 → 通过
i2c_write_series/i2c_read_series访问 slave SRAM 中 0x48000 起始的 16 字节 → 周期性写入并读回比对。 - Slave 数据流: 烧录 slave 固件 → 中断中统计 master 读写次数并翻转 LED,数据存放位置由 master 在地址阶段指定。
UART
UART 硬件架构
概述
UART(Universal Asynchronous Receiver/Transmitter)是一种异步全双工串行通信协议,由 Tx 和 Rx 两根数据线组成。由于没有时钟参考信号,通信双方必须约定串口波特率、数据位宽、奇偶校验位、停止位等配置参数,从而按照相同的速率进行通信。
UART 模块具有以下特性:
| 特性 | 说明 |
|---|---|
| 工作模式 | NDMA / DMA / DMA LLP |
| 硬件流控 | 支持 CTS(Clear to Send)/ RTS(Request to Send) |
| FIFO 深度 | 8 字节(TX FIFO 和 RX FIFO 各 8 字节) |
| 数据位宽 | 8 位 |
| 停止位 | 1 位 / 1.5 位 / 2 位 |
| 校验位 | 无校验 / 偶校验 / 奇校验 |
| 单线模式 | 支持 RTX 引脚(单引脚复用 TX/RX,用于 S7816 协议) |
数据通信时序

UART 异步通信以一个字符为传输单位,每个字符由以下部分组成:
| 位段 | 说明 |
|---|---|
| 起始位 | 逻辑0,低电平有效,表示传输字符的开始 |
| 数据位 | 8 位逻辑0 或 1,小端传输(LSB 先发,MSB 后发) |
| 校验位 | 可选,使1 的位数为偶数(偶校验)或奇数(奇校验) |
| 停止位 | 逻辑1,1 位 / 1.5 位 / 2 位,停止位时间越长,容错能力越强 |
| 空闲位 | 逻辑1,表示当前线路上没有数据传送 |
通信原理

以 Telink SoC 中 UART 模块为例:
- 发送方向:数据由 MCU 或 DMA 写入 TX 缓冲区(TX FIFO),UART 模块通过 TX 引脚将数据串行发送到对端设备
- 接收方向:对端设备通过 UART 模块的 RX 引脚接收数据,数据被写入 RX 缓冲区(RX FIFO),然后由 MCU 或 DMA 读取
硬件流控机制:
- 若芯片 RX 缓冲区接近溢出,芯片通过 RTS 引脚发出信号,通知对端设备停止发送数据
- 若芯片通过 CTS 引脚接收到信号,表明对端设备 RX 缓冲区接近溢出,芯片应停止发送数据
接线注意: 当前设备的 TX/RX 与对端设备的 TX/RX 连接规则为 TX ↔ RX,RX ↔ TX。
芯片支持
由于芯片在设计上存在一定差异,存在不同对应的接口封装情况,分类如下:
| 分类 | 芯片 |
|---|---|
| 第一类 | B91 / B92 / TL751X / TL721X / TL321X / TL322X / TL323X |
| 第二类 | B85 / B87 / B80 / B80B / TC321X / TC123X |
UART 超时机制
| 芯片系列 | 超时机制计算 |
|---|---|
| 第一类(B91 / B92)、第二类 | rx_timeout = ((bwpc+1) * bit_cnt) * mul |
| 第一类(除 B91/B92 的其他芯片) | rx_timeout = (((bwpc+1) * bit_cnt) * mul) * 2^n (n<=255) |
其中 (bwpc+1) * bit_cnt 最大可设置为 0xff。
从当前本字节开始计算超时时间,直到达到配置的超时时间还未收到下一个字节,即可判断这笔数据包接收完成,则产生 timeout 超时中断。
RX 超时时间设置:
uart_set_rx_timeout(uart_num, bwpc, bit_cnt, mul);
bwpc:位宽,由uart_cal_div_and_bwpc()计算得出bit_cnt:传输一个字节所需的总位数(如 1 起始位 + 8 数据位 + 1 校验位 + 2 停止位 = 12)mul:超时倍率,UART_BW_MUL1= 1 字节时间,UART_BW_MUL2= 2 字节时间
超时时间 = (1 / baudrate) × bit_cnt × mul。当超过该时间未收到数据时,产生 UART_RXDONE 中断。
说明
- 什么时候需要调整 timeout 时间?当客户发送一笔包,但一笔包内存在时间间隔,希望视为同一笔包时,需要调大 timeout 时间。但调大 timeout 时间又可能导致原本两笔包被看作同一笔,因此具体数值需根据应用层场景确定。
内部 FIFO 的工作机制
UART FIFO 深度为 8 字节,通过以下计数器反映 FIFO 状态:
| 计数器 | 方向 | 说明 | 查询 API |
|---|---|---|---|
rx_bufcnt |
RX | 每接收 1 字节加 1,每读走 1 字节减 1 | uart_get_rxfifo_num() |
tx_bufcnt |
TX | 每写入 1 字节加 1,每发送 1 字节减 1 | uart_get_txfifo_num() |
rbcnt |
RX | 每读走 1 字节加 1(FLD_UART_RBCNT) |
- |
wbcnt |
TX | 每写入 1 字节加 1(FLD_UART_WBCNT) |
- |
第一类 UART
芯片实例差异
不同芯片提供的 UART 实例数量不同,使用时需根据芯片型号选择对应的 UART 编号(uart_num_e):
| 芯片系列 | UART 实例数量 | 可用编号 |
|---|---|---|
| B91 / B92 | 2 | UART0、UART1 |
| TL721X / TL321X | 3 | UART0、UART1、UART2 |
| TL7518 / TL751X | 4 | UART0 ~ UART3 |
| TL322X / TL323X | 5 | UART0 ~ UART4 |
UART 中断
(1) 中断总览
| 中断名称 | 所属模块 | 触发条件 | 清除方式 | 适用模式 |
|---|---|---|---|---|
UART_TXDONE |
UART | TX FIFO 为空,表示一帧数据发送完成 | 手动清除 | NDMA / DMA |
UART_RXDONE |
UART | RX 超时未收到数据,表示一帧数据接收完成 | 手动清除 | NDMA |
UART_RXBUF_IRQ_STATUS |
UART | RX FIFO 中数据量 ≥uart_rx_irq_trig_level 设定阈值 |
自动清除 | NDMA |
UART_TXBUF_IRQ_STATUS |
UART | TX FIFO 中数据量 ≤uart_tx_irq_trig_level 设定阈值 |
自动清除 | NDMA |
UART_RX_ERR |
UART | 接收数据错误(奇偶校验错误、停止位错误等) | 手动清除 | NDMA / DMA |
DMA TC(RX) |
DMA | DMA RX 接收长度达到配置长度 | 手动清除 | DMA / DMA LLP |
(2) 中断 mask 配置
各中断对应的 mask 配置 API:
// NDMA 模式:RX 中断 + 错误中断(UART 模块中断)
uart_set_irq_mask(UART0, UART_RX_IRQ_MASK | UART_ERR_IRQ_MASK);
// DMA 模式:TX_DONE + RX_DONE(UART 模块中断)
uart_set_irq_mask(UART0, UART_TXDONE_MASK | UART_RXDONE_MASK);
// DMA 模式:使用 DMA TC 中断(替代 RX_DONE,属于 DMA 模块中断)
dma_set_irq_mask(UART_DMA_CHANNEL_RX, TC_MASK);
// DMA LLP 模式:使用 DMA TC 中断
dma_set_llp_irq_mode(UART_DMA_CHANNEL_RX, DMA_INTERRUPT_MODE);
dma_set_irq_mask(UART_DMA_CHANNEL_RX, TC_MASK);
中断 mask 枚举(uart_irq_mask_e):
| Mask 名称 | 说明 |
|---|---|
UART_RX_IRQ_MASK |
使能 RXBUF_IRQ 中断 |
UART_TX_IRQ_MASK |
使能 TXBUF_IRQ 中断 |
UART_RXDONE_MASK |
使能 RXDONE 中断 |
UART_TXDONE_MASK |
使能 TXDONE 中断 |
UART_ERR_IRQ_MASK |
使能 RX_ERR 中断 |
(3) 中断状态查询与清除
B91 中断状态枚举(uart_irq_status_e):
| 状态名称 | 查询方式 | 清除方式 |
|---|---|---|
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) |
读取 FIFO 后自动清除 |
UART_TXBUF_IRQ_STATUS |
uart_get_irq_status(uart_num, UART_TXBUF_IRQ_STATUS) |
自动清除 |
B92 及后续芯片中断状态枚举(uart_irq_status_e):
| 状态名称 | 查询方式 | 清除方式 |
|---|---|---|
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) |
自动清除 |
(4) 芯片差异
- UART_TXDONE:B91 默认值为 1,初始化时需要先手动清除;其他芯片则默认值为 0,初始化时无需手动清除。
- UART_RXDONE:B91 只有 DMA 模式下才会触发,NDMA 没有,以及只是起到清 RXDONE 的功能;其他芯片都支持,以及当清 RXDONE 时,也起到清 RXFIFO 的功能。
- UART_RXBUF_IRQ:清 FIFO 时,B91 当 FIFO 中有数据时,清 FIFO 只是将 FIFO 的内容清除,FIFO 的指针并不会改变;其他芯片当 FIFO 中有数据时,清 FIFO 会将 FIFO 的指针也清除,指向 FIFO 的起始地址。当 FIFO 中没有数据时,所有芯片则清 FIFO 指针,指向 FIFO 的起始地址。
UART DMA 功能的迭代优化
B91: DMA write num 功能不能使用,没有硬件回写长度,导致只能通过在 RXDONE 中断中手动计算接收数据的长度。
以上工作机制会存在以下场景问题:如果两笔包挨着很近,但却产生 RXDONE 中断了,但在处理上一笔中断时,下一笔数据已经来了,会出现清上一笔中断状态位时会将下一笔的数据清除导致丢包。如果客户接收不了丢包,建议方案是两笔包的时间需要大于中断处理时间。
B92: 针对该问题做了改进,不过当 write num 功能使能时,DMA 的接收长度必须配置为全 ff 才可以 RXDONE 触发 DMA 接收完成,这样会导致当发送长度大于接收长度,导致 buff 溢出问题。是否使用 B91 的方法还是改进的方法,可以根据两者优缺点进行评估。
其他芯片(后续芯片): write_num 使能,不管 DMA 的接收长度配置为多少都可以,DMA 工作完成的条件:要么是 UART RXDONE,要么是达到 DMA 配置的长度。
UART 初始化流程
(1) 硬件复位
每次使用 UART 端口前,必须先调用 uart_hw_fsm_reset() 复位 UART 有限状态机,清除之前操作遗留的寄存器状态和 FIFO 数据,同时会清零软件读写指针。
uart_hw_fsm_reset(UART_MODULE_SEL);
(2) 引脚配置
使用 uart_set_pin() 配置 UART 的 TX/RX 引脚。
uart_set_pin(UART0_TX_PA3, UART0_RX_PA4);
该函数内部会:
- 使能引脚的 GPIO 输入功能
- 配置上拉电阻
- 将引脚功能切换为 UART 复用功能
注意
- 不同芯片的引脚枚举不同(如
UART0_TX_PA3、UART0_TX_PIN等),具体可用引脚参考各芯片uart.h中uart_tx_pin_e/uart_rx_pin_e枚举定义。
(3) 波特率配置
波特率配置分为两步:
Step 1:计算分频参数
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() 根据目标波特率和 PCLK 频率,计算出最佳的时钟分频数 div 和位宽 bwpc。计算公式为:
BaudRate × (div + 1) × (bwpc + 1) = PCLK
其中 bwpc 范围为 3-15,div 通过 bwpc 推导得出。
Step 2:初始化 UART 模块
uart_init(UART_MODULE_SEL, div, bwpc, UART_PARITY_NONE, UART_STOP_BIT_ONE);
uart_init() 将分频参数写入寄存器,同时配置校验位和停止位。
最大波特率:
芯片 UART 理论上最大支持波特率为 UART 使用时钟的 1/10,容错率见 Datasheet;但实际使用过程中受硬件环境影响,可能会达不到这个波特率。
注意
- 配置最大波特率时,尽量使用 DMA 模式,NDMA 模式可能会存在来不及从 FIFO 中取数据的风险。PCLK 是 UART 波特率上限的主要影响因素。
由于 BaudRate × (div + 1) × (bwpc + 1) = PCLK,给定波特率和 PCLK 后 (div + 1) × (bwpc + 1) 为常量,div 和 bwpc 有多种组合。bwpc 越大,时钟分频越精细,实际波特率越接近目标值。对时序要求较高的应用,可以预先计算好 div 和 bwpc 直接调用 uart_init(),因为 uart_cal_div_and_bwpc() 计算的结果可能不是最优解。
工作模式
UART 驱动支持三种运行模式,通过 UART_MODE 宏切换:
#define UART_DMA 1 // DMA 模式
#define UART_NDMA 2 // NDMA 模式(无 DMA)
#define UART_DMA_LLP 3 // DMA LLP(Linked List Pointer)模式
#define UART_MODE 2
(1) NDMA 模式
NDMA 模式下,数据的收发完全由 CPU 通过寄存器操作完成,不使用 DMA 引擎。
特点:
- CPU 直接读写 TX/RX FIFO
- 通过中断(RX_IRQ)或软件超时(RXDONE,B92 及后续芯片支持)判断数据接收完成
- 发送可通过查询
uart_tx_is_busy()或 TX_IRQ 中断判断完成 - 适合低速、小数据量场景
核心函数:
- 发送:
uart_send_byte()/uart_send_hword()/uart_send_word()/uart_send() - 接收:
uart_read_byte()
a. NDMA 模式中断配置
// 设置 RX 中断触发阈值(接收多少个字符触发一次中断)
uart_rx_irq_trig_level(UART_MODULE_SEL, 1); // 推荐设置为 1
// 设置 TX 中断触发阈值(TX FIFO 中剩余数据量 ≤ level 时触发中断)
uart_tx_irq_trig_level(UART_MODULE_SEL, 0); // 0 表示 TX FIFO 为空时触发中断
// 使能 UART 模块中断 mask
uart_set_irq_mask(UART_MODULE_SEL, UART_RX_IRQ_MASK | UART_ERR_IRQ_MASK);
// B92 以及后续芯片支持 RXDONE 中断
uart_set_irq_mask(UART_MODULE_SEL, UART_RXDONE_MASK);
// 使能 PLIC 中断源
plic_interrupt_enable(IRQ_UART0);
// 使能全局中断
core_interrupt_enable();
说明
uart_rx_irq_trig_level函数 B91 只能设置为 1,B92 只能设置为 1 或 4,其他芯片可以设置为 1-7。
b. 接收中断处理
NDMA 模式下,接收中断处理需同时考虑 RX_ERR 错误中断、RXBUF_IRQ 数据中断以及 RXDONE 超时中断(B92+)。以下按芯片分别展示处理方式:
B91 处理方式(无 RXDONE 中断):
_attribute_ram_code_sec_ void uart0_irq_handler(void)
{
// RX_ERR 错误中断:清除 RX FIFO、错误标志、硬件/软件指针
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_CLR_RX); // 清除 rx_fifo、rx_err_irq、rx_buff_irq
uart_hw_fsm_reset(UART_MODULE_SEL); // 复位硬件指针
uart_clr_rx_index(UART_MODULE_SEL); // 清零软件指针
uart_irq_cnt = 0;
uart_rx_flag = 0;
}
// RXBUF_IRQ 数据中断:从 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 及后续芯片处理方式(支持 RXDONE 中断):
_attribute_ram_code_sec_ void uart0_irq_handler(void)
{
// RX_ERR 错误中断:一步清除(B92+ 内部自动清除 FIFO 指针)
if (uart_get_irq_status(UART_MODULE_SEL, UART_RX_ERR)) {
uart_clr_irq_status(UART_MODULE_SEL, UART_RXBUF_IRQ_STATUS); // 清除 rx_fifo、硬件指针、rx_err_irq、rx_buff_irq
uart_irq_cnt = 0;
uart_rx_flag = 0;
}
// RXBUF_IRQ 数据中断:从 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 超时中断:一帧接收完成,读取 FIFO 剩余数据
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);
}
}
注意
- UART FIFO 深度为 8 字节。如果进入 RX 中断前后的时间超过接收 8 字节所需时间,FIFO 指针可能被打乱,导致接收数据异常。可通过
uart_get_rxfifo_num() > 8判断异常,若出现此异常建议改用 DMA 模式。
(2) DMA 模式
DMA 模式下,数据收发由 DMA 引擎自动完成,CPU 仅需配置 DMA 描述符并响应中断。
特点:
- DMA 在 UART FIFO 和 SRAM 之间自动搬运数据
- 发送通过
UART_TXDONE中断判断完成 - 接收可通过
UART_RXDONE中断或 DMA TC 中断判断完成 - 适合高速、大数据量场景
核心函数:
- 发送:
uart_send_dma() - 接收:
uart_receive_dma() - 接收长度计算:
uart_get_dma_rev_data_len()
a. DMA 配置
// 选择未被其他模块占用的 DMA 通道
#define UART_DMA_CHANNEL_TX DMA3
#define UART_DMA_CHANNEL_RX DMA2
// 配置 TX/RX DMA 通道
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() 和 uart_set_rx_dma_config() 内部调用 dma_config() 配置以下 DMA 参数:
- TX:源地址递增、目的地址固定、Handshake 模式、Word 传输位宽
- RX:源地址固定、目的地址递增、Handshake 模式、Word 传输位宽
b. DMA 模式中断配置
// B91 需要清除 TX_DONE,因为其默认值为 1
uart_clr_tx_done(UART_MODULE_SEL);
// 使能 TX_DONE 中断 mask(UART 模块中断)
uart_set_irq_mask(UART_MODULE_SEL, UART_TXDONE_MASK);
// B91 / B92 当 DMA 接收长度配置为定长时,需要使能 RX_DONE 中断,通过软件手动方式计算接收长度
uart_set_irq_mask(UART_MODULE_SEL, UART_RXDONE_MASK);
// B92(DMA 配置为全 ff)及后续芯片,使能 DMA TC 中断,通过硬件自动回写长度
// 注意:DMA TC 中断属于 DMA 模块,需使用 dma_set_irq_mask 配置
dma_set_irq_mask(UART_DMA_CHANNEL_RX, TC_MASK);
// 使能 PLIC 中断源
plic_interrupt_enable(IRQ_UART0);
// 使能全局中断
core_interrupt_enable();
c. 中断处理
DMA 模式下,接收完成判断有两种方式:通过 UART_RXDONE 中断(软件计算长度)或通过 DMA TC 中断(硬件自动回写长度)。
方式一:通过 UART_RXDONE 中断判断(B91 / B92 定长模式)
当通过 UART_RXDONE 中断判断接收完成时,需要通过 uart_get_dma_rev_data_len() 函数获取接收数据长度:
// B91 处理
_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);
}
// 获取接收数据长度(必须在清除中断标志之前)
rev_data_len = uart_get_dma_rev_data_len(UART_MODULE_SEL, UART_DMA_CHANNEL_RX);
// 清除 RX 中断(B91 需复位硬件状态机)
uart_hw_fsm_reset(UART_MODULE_SEL);
// DMA 访问内存需要按 word 对齐
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 处理
_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);
}
// 获取接收数据长度(必须在清除中断标志之前)
rev_data_len = uart_get_dma_rev_data_len(UART_MODULE_SEL, UART_DMA_CHANNEL_RX);
// 清除 RX 中断(B92 使用 uart_clr_irq_status)
uart_clr_irq_status(UART_MODULE_SEL, UART_RXDONE_IRQ_STATUS);
// DMA 访问内存需要按 word 对齐
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);
}
}
注意
- 在 Telink RISC-V MCU 中,清除中断状态标志的操作必须放在计算 DMA 接收数据长度之后,否则 DMA 接收数据长度计算错误。
方式二:通过 DMA TC 中断判断(B92 全 ff 模式及后续芯片)
当通过 DMA TC 中断判断接收完成时,硬件自动回写接收数据长度到缓冲区前 4 字节:
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;
}
/*
* 场景分析,有以下三种情况:
* 1. 发送长度 ≤ 接收长度:产生 TC 中断,接收数据长度为实际发送长度
* 2. 发送长度 > 接收长度:产生 TC 中断,接收数据长度为实际发送长度,
* 但缓冲区中仅配置的接收长度数据有效,DMA 不会将多余数据搬入缓冲区
* 3. 中断处理:通过 buff_rx_index 标志交替接收 rec_buff/rec_buff1,
* 在 main_loop 中通过 buff_tx_index 标志交替发送
*/
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 接收注意事项:
addr:接收缓冲区地址,必须 4 字节对齐,且实际缓冲区大小不能小于rev_sizerev_size:DMA 接收长度,必须为 4 的倍数,最大值 0xFFFFFC
- DMA 以 4 字节(Word)为单位搬运数据。例如实际接收长度为 5 字节,DMA 会搬运 2 次共 8 字节,后 3 字节为无效数据。因此接收缓冲区需预留额外空间
- 若接收数据长度 len 满足
4×(n-1) < len ≤ 4×n,建议设置rec_buff = rev_size = 4n - 接收长度不能超过 DMA 设置长度,否则超出部分会被丢弃
- 在
UART_RXDONE中断中,必须先计算接收长度,再清除中断标志
DMA TX/RX 重配置注意事项:
当 DMA 传输未完成时,若要重新配置 DMA,必须按以下顺序操作:
dma_chn_dis()关闭 DMA 通道- 重新调用
uart_send_dma()或uart_receive_dma()
说明
uart_receive_dma()内部已实现dma_chn_dis和清除中断状态的操作,调用时会自动关闭 DMA 通道并清除UART_RXBUF_IRQ_STATUS,无需手动调用。
(3) DMA LLP 模式
DMA LLP(Linked List Pointer)模式是 DMA 模式的高级用法,B92(DMA 接收长度当配置为全 ff 时)及后续芯片支持。通过链表描述符实现多缓冲区自动切换,适合需要连续接收、不丢包的场景。仅支持 RX 方向,接收完成后硬件自动跳转到下一链表节点,无需软件重新配置 uart_receive_dma()。
特点:
- 硬件自动写回接收数据长度到缓冲区前 4 字节
- 支持 Ping-Pong 双缓冲,实现无间断数据接收
- 仅支持 RX 方向;接收完成后硬件自动跳转到下一链表节点,无需软件重新配置
uart_receive_dma()
单链模式(Single Chain):
// 初始化
uart_rx_dma_chain_init(UART_MODULE_SEL, UART_DMA_CHANNEL_RX,
(unsigned char *)(rec_buff + 4), DMA_REV_LEN);
// 中断处理
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; // 硬件自动写回的接收长度
rx_done_flag = 1;
}
}
PLIC_ISR_REGISTER(dma_irq_handler, IRQ_DMA)
乒乓缓冲模式(Ping-Pong):
// 初始化
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);
乒乓缓冲中断处理:
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; // 读取 rec_buff 写回长度
pingpong_flag = 1;
} else {
rev_data_len1 = *(unsigned int *)rec_buff1; // 读取 rec_buff1 写回长度
pingpong_flag = 0;
}
rx_done_flag = 1;
}
}
PLIC_ISR_REGISTER(dma_irq_handler, IRQ_DMA)
DMA LLP 使用限制:
- B92:DMA 接收长度必须设置为
0xFFFFFC,硬件才会自动写回接收长度 - TL751X / TL721X / TL321X / TL322X / TL323X 等后续芯片:无使用限制,DMA 长度可配置为任意值(不超过 0xFFFFFC 且为 4 的倍数即可),发送长度也无限制
UART 流控
(1) CTS 配置
CTS(Clear to Send)用于控制本端发送:当 CTS 引脚检测到有效电平时,停止发送数据。
// 配置 CTS 引脚与极性
uart_cts_config(UART0_CTS_PA1, 0); // 0: 低电平停止 TX;1: 高电平停止 TX
// 使能 CTS
uart_set_cts_en(UART0);
(2) RTS 配置
RTS(Request to Send)用于通知对端:当本端 RX 缓冲区接近溢出时,通过 RTS 引脚发出信号,请求对端停止发送。RTS 支持手动和自动两种模式。
手动模式(Manual):
uart_set_rts_en(UART0); // 使能 RTS
uart_rts_manual_mode(UART0); // 设置为手动模式
uart_set_rts_level(UART0, 1); // 手动设置 RTS 引脚电平
自动模式(Auto):
自动模式下,RTS 在满足以下条件之一时被激活:
- RX FIFO 中数据量 ≥ 设定阈值(
uart_rts_trig_level_auto_mode) - 产生 RX_DONE 信号(需使能
uart_rxdone_rts_en,TL321X 及后续芯片支持)
uart_set_rts_en(UART0); // 使能 RTS
uart_rts_auto_mode(UART0); // 设置为自动模式
uart_rts_trig_level_auto_mode(UART0, 5); // RX FIFO ≥ 5 字节时激活 RTS
// uart_rxdone_rts_en(UART0); // 可选:RX_DONE 时激活 RTS(TL321X+)
说明
- B92 及后续芯片新增
uart_rts_stop_rxtimeout_en()接口,用于在 RTS 触发后停止 RX 超时计数,从而避免在 RTS 激活期间产生UART_RXDONE_IRQ_STATUS中断。当不使用 RTS 功能时,无需开启此接口,否则会影响 UART RXDONE 的产生。
(3) 流控注意事项
- CTS/RTS 引脚需通过
uart_cts_config()/uart_rts_config()配置引脚与极性后再使能 - 不同芯片可选的 CTS/RTS 引脚不同,参考各芯片
uart_cts_pin_e/uart_rts_pin_e枚举 - 自动模式下,当 RX FIFO 数据量低于阈值时,RTS 信号自动恢复
UART 错误处理与超时机制
(1) 接收错误处理
当 UART 接收数据出现奇偶校验错误或停止位错误时,产生 UART_RX_ERR 中断。错误处理方式因芯片和模式而异:
NDMA 模式错误处理:
// B91
uart_clr_irq_status(UART0, UART_CLR_RX); // 清除 RX FIFO 和错误标志
uart_hw_fsm_reset(UART0); // 复位硬件指针
uart_clr_rx_index(UART0); // 清零软件指针
// B92+
uart_clr_irq_status(UART0, UART_RXBUF_IRQ_STATUS); // 一步清除
DMA 模式错误处理:
发生接收错误时,建议重新启动 DMA 接收:
uart_receive_dma(UART0, (unsigned char *)rec_buff, BUFF_DATA_LEN);
// uart_receive_dma() 内部已实现 dma_chn_dis 和中断清除(B92+)
UART 睡眠唤醒
UART 在进入低功耗睡眠前后的处理因模式而异:
NDMA 发送:
- 进入睡眠前:判断
uart_tx_is_busy()为 0,确保数据发送完成 - 唤醒后:调用
uart_clr_tx_index()清零发送指针,清除 RAM 中发送数据
NDMA 接收:
- 唤醒后:调用
uart_clr_rx_index()清零接收指针,清除 RAM 中接收数据
DMA 发送:
- 唤醒后:重新调用
uart_send_dma()(内部已实现dma_chn_dis)
DMA 接收:
- 唤醒后:重新调用
uart_receive_dma()(内部已实现dma_chn_dis)
(1) 睡眠唤醒后异常进入 TX 中断问题
问题现象
当芯片进入低功耗睡眠模式(Suspend)并再次唤醒后,即使没有主动发送数据,UART 也会异常触发一次 TX_DONE(发送完成)中断。
原因分析
在芯片进入 Suspend 状态时,硬件会自动复位对应的总线(Bus)。复位释放后,UART 内部的 TX_DONE 寄存器状态位默认会被置为 1。此时如果系统重新开启了全局中断,MCU 就会误认为上一次的数据发送已完成,从而错误地跳转进 UART 的 TX 中断服务程序中。
芯片差异
- 第一类芯片(B91)、第二类芯片(B85\B87) :均存在此硬件设计特性,需要软件介入处理。
- 其他芯片:硬件内部已对该总线复位逻辑进行了迭代优化,唤醒后不会再出现此异常,无需特殊处理。
解决方案
针对存在该问题的芯片,标准的规避流程为:在进入睡眠前屏蔽 UART 中断 -> 唤醒后先手动清除残留的 TX_DONE 中断状态 -> 最后解除 UART 中断屏蔽。
具体参考代码如下:
// 1. 进入睡眠前:屏蔽 UART 的 TX 中断标志,防止唤醒瞬间误触发
uart_clr_tx_done(UART0);
plic_interrupt_disable(IRQ_UART0);
// 2. 调用低功耗进入函数(进入 Suspend 睡眠)
cpu_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_PAD, 0);
// 3. 睡眠唤醒后:此时总线复位导致 TX_DONE 变 1,必须先手动清除该残留状态
uart_clr_tx_done(UART0);
// 4. 清除状态后,再安全地重新解除屏蔽,恢复 UART 中断功能
plic_interrupt_enable(IRQ_UART0);
API 参考
(1) 初始化与配置 API
| API 名称 | 说明 |
|---|---|
uart_hw_fsm_reset |
复位 UART 有限状态机,清除寄存器状态和 FIFO 数据 |
uart_set_pin |
配置 UART TX/RX 引脚 |
uart_cal_div_and_bwpc |
根据波特率和 PCLK 计算分频参数div 和位宽 bwpc |
uart_init |
初始化 UART 模块(分频参数、校验位、停止位) |
uart_set_rx_timeout |
设置 RX 超时时间(B91/B92) |
uart_set_rx_timeout_with_exp |
设置 RX 超时时间,支持指数扩展(TL721X 及后续芯片) |
uart_clk_en |
使能 UART 模块时钟(多实例芯片使用 UART2+ 时需调用) |
(2) NDMA 数据收发 API
| API 名称 | 说明 |
|---|---|
uart_send_byte |
发送单个字节 |
uart_send_hword |
发送半字(2 字节,低字节先发) |
uart_send_word |
发送字(4 字节) |
uart_send |
批量发送(内部循环调用uart_send_byte) |
uart_read_byte |
读取单个字节 |
(3) DMA 数据收发 API
| API 名称 | 说明 |
|---|---|
uart_send_dma |
DMA 发送数据 |
uart_receive_dma |
DMA 接收数据 |
uart_get_dma_rev_data_len |
计算 DMA 实际接收数据长度 |
uart_set_tx_dma_config |
配置 TX DMA 通道 |
uart_set_rx_dma_config |
配置 RX DMA 通道 |
(4) 中断控制 API
| API 名称 | 说明 |
|---|---|
uart_set_irq_mask |
使能 UART 中断 mask |
uart_clr_irq_mask |
禁用 UART 中断 mask |
uart_get_irq_status |
获取中断状态 |
uart_clr_irq_status |
清除中断状态 |
uart_rx_irq_trig_level |
设置 RX 中断触发阈值 |
uart_tx_irq_trig_level |
设置 TX 中断触发阈值 |
uart_clr_tx_done |
清除 TX_DONE 标志(B91 专用) |
uart_rxdone_sel |
选择 RXDONE 功能归属(NDMA/DMA,TL321X+ 支持) |
(5) 流控 API
| API 名称 | 说明 |
|---|---|
uart_cts_config |
配置 CTS 引脚与极性 |
uart_set_cts_en / uart_set_cts_dis |
使能/禁用 CTS |
uart_rts_config |
配置 RTS 引脚与极性 |
uart_set_rts_en / uart_set_rts_dis |
使能/禁用 RTS |
uart_rts_manual_mode |
设置 RTS 为手动模式 |
uart_rts_auto_mode |
设置 RTS 为自动模式 |
uart_set_rts_level |
手动设置 RTS 引脚电平(手动模式) |
uart_rts_trig_level_auto_mode |
设置自动模式 RTS 触发阈值 |
uart_rxdone_rts_en |
使能 RX_DONE 触发 RTS(TL321X+) |
uart_rts_stop_rxtimeout_en |
RTS 激活时停止 RX 超时计数(TL321X+) |
(6) 状态查询与辅助 API
| API 名称 | 说明 |
|---|---|
uart_tx_is_busy |
查询 TX 是否正在发送 |
uart_get_rxfifo_num |
获取 RX FIFO 中数据量 |
uart_get_txfifo_num |
获取 TX FIFO 中数据量 |
uart_clr_rx_index |
清零软件接收指针 |
uart_clr_tx_index |
清零软件发送指针 |
uart_set_error_timeout |
设置错误超时时间(TL321X+) |
uart_get_error_timeout_code |
获取错误超时错误码(TL321X+) |
uart_timeout_handler |
错误超时处理函数(TL321X+,可重定义) |
(7) DMA LLP API
| API 名称 | 说明 |
|---|---|
uart_rx_dma_chain_init |
初始化 DMA LLP 单链 |
uart_set_dma_chain_llp |
初始化 DMA LLP 链首 |
uart_rx_dma_add_list_element |
添加 DMA LLP 链表节点 |
Demo 参考
Demo 路径:demo/vendor/UART_DEMO/
| 文件 | 对应模式 | 说明 |
|---|---|---|
app.c |
NDMA 模式 | 字节/半字/字发送、RX 中断接收 |
app_dma.c |
DMA 模式 | DMA 收发、TX_DONE/RX_DONE 中断 |
app_dma_llp.c |
DMA LLP 模式 | 单链 / 乒乓缓冲接收 |
(1) NDMA Demo
文件: app.c
模式选择:
#define UART_MODE UART_NDMA
#define FLOW_CTR NORMAL // 可选: BASE_TX / NORMAL / USE_CTS / USE_RTS
测试场景:
| FLOW_CTR 值 | 测试内容 |
|---|---|
| BASE_TX | TX 通过 byte/hword/word 三种方式发送数据到串口工具,验证 TX 功能 |
| NORMAL | 串口工具发送 16 字节,RX 通过中断接收后 TX 返回到串口工具 |
| USE_CTS | TX 持续发送字节,外部给 CTS 引脚高电平后停止发送 |
| USE_RTS | 串口工具发送 >5 字节,逻辑分析仪可观察到 RTS 引脚电平由低变高 |
(2) DMA Demo
文件: app_dma.c
模式选择:
#define UART_MODE UART_DMA
#define UART_DEVICE UART_SLAVE_DEVICE // 可选: UART_MASTER_DEVICE / UART_SLAVE_DEVICE
测试场景:
| UART_DEVICE 值 | 测试内容 |
|---|---|
| UART_MASTER_DEVICE | TX 通过 DMA 发送数据到串口工具,通过 TX_DONE 中断触发下一帧发送 |
| UART_SLAVE_DEVICE | 串口工具发送数据到 RX,通过 RX_DONE 中断接收,计算接收长度后通过 TX 返回 |
DMA 接收长度配置(芯片差异):
// B91 / B92(任意长度):DMA_REV_LEN = BUFF_DATA_LEN
// B92(最大长度模式):DMA_REV_LEN = 0xFFFFFC,硬件自动写回接收长度
// TL751X / TL721X / TL321X / TL322X / TL323X:DMA_REV_LEN = BUFF_DATA_LEN,硬件自动写回接收长度
(3) DMA LLP Demo
文件: app_dma_llp.c
模式选择:
#define UART_MODE UART_DMA_LLP
#define DMA_LLP_MODE DMA_LLP_PINGPONG // 可选: DMA_LLP_SINGLE_CHAIN / DMA_LLP_PINGPONG
注意
- DMA LLP 仅支持从机(slave)模式,主机(master)不支持。
测试场景:
| DMA_LLP_MODE 值 | 测试内容 |
|---|---|
| DMA_LLP_SINGLE_CHAIN | 单链模式,硬件自动写回接收长度到 rec_buff 前 4 字节 |
| DMA_LLP_PINGPONG | Ping-Pong 双缓冲模式,rec_buff 和 rec_buff1 交替接收,实现无间断数据接收 |
第二类 UART
第二类 UART 模块工作机制与第一类 UART 模块存在一定区别,以下针对芯片差异进行说明。
NODMA
第二类 NDMA 工作机制和第一类工作机制一致,这里不做详细说明,只针对第一类芯片差异进行简单介绍。
接收数据需要注意的点:
- B85/B87/B89:没有 RX_DONE 中断,只有 RX_BUFF 中断。
uart_rx_trig_level(范围 1-8)如果在不知道对方发的数据长度时,设置为 1;如果知道对方发的数据长度(这个长度必须是 level 的整数倍),可按需设置。 - B80 以及后面的芯片:有 RX_DONE 中断,RX_DONE 结合 RX_BUFF 中断一起使用,不用考虑对方发的数据长度问题,一般
uart_rx_trig_level设置为 4 即可。
DMA(无 DMA 链表功能)
第二类的 DMA 工作机制和第一类是存在一定区别的,这里关于 DMA 的工作机制进行相关讲解。
DMA 发送数据:
- DMA 允许发送的最大长度为:4075 bytes
- 发送 buff 前四个字节需填写发送的长度信息,后面再跟着需发送的数据,该 buff 需要四字节对齐
- 通过 TX_DONE 中断判断数据是否发送完
DMA 接收数据:
- DMA 允许接收数据的最大长度为:4075 bytes
- 接收 buff 需要四字节对齐,前四个字节存放的是接收长度信息
- buff 的长度包含前四个字节,DMA len 需要是 16 的倍数
- DMA 中断的产生条件:当 UART timeout 之后才会产生 DMA 中断
- 如果发送长度大于 DMA 的接收长度,DMA 还是会继续接收,只是不断地存放在 buff 的最后一个 word 中,不会出现 buff 越界问题
芯片差异:
- B85/B87/B89:只能通过 DMA 中断判断是否接收完成,不能通过 RX_DONE 中断判断是否接收完成。
- B80 以及后面的芯片:既可以用 RX_DONE 中断也可以使用 DMA 中断。
说明
- 只需要在初始化调用
uart_recbuff_init函数即可,无须在 DMA 中断中调用该函数,硬件会自动按照该配置进行接收数据。
初始化配置示例:
#define rec_buff_Len 32 //rec_buff_Len 需为 16 的倍数,且接收数据长度 < (16*n - 4)
#define trans_buff_Len 16
//DMA 收发 buff 需四字节对齐,发送 buff 前 4 字节存放发送长度信息
__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!
//配置接收 buff,DMA len 需为 16 的倍数,前 4 字节存放接收长度信息
uart_recbuff_init((unsigned char *)rec_buff, sizeof(rec_buff));
}
Multi-core
概述
Telink 平台部分芯片采用多核架构,除主核 D25F(RISC-V)外,还集成 N22(RISC-V 协处理器)和/或 DSP(数字信号处理器)。支持的芯片及核心组成详见 Platform SDK 概述 中的芯片特性总表。
各核资源与特性:
| 核 | 类型 | 存储资源 | Cache | 浮点/DSP 指令 | 流水线 | 典型用途 |
|---|---|---|---|---|---|---|
| D25F | RISC-V | Flash(XIP)/ SRAM | 有 | 支持 | 5 级 | 主核,负责系统启动和核间协调 |
| N22 | RISC-V | IRAM / DRAM | TL751x:无;TL322x:有 | 不支持 | 2 级 | 协处理核,常用于 RF / BLE 协议栈 |
| DSP | DSP | ILM(指令)/ DLM(数据) | 有 | — | — | 数字信号处理,常用于音频编解码 |
-
N22 不支持浮点和 DSP 指令,需使用
libdsp.a中的 C 语言实现版本。工程配置时需注意 N22 与 D25F 的指令集差异。 -
N22 的 Cache 配置因芯片而异:TL751x 的 N22 无 Cache,TL322x 的 N22 有 Cache。这会影响跨核 RAM 访问的 Cache 一致性处理,详见 跨核 RAM 访问。
D25F 启动后,按需加载 N22 / DSP 固件并启动对应核心。各核之间通过 Mailbox 进行消息通信,详见 Mailbox。
多核使用约束
多核协同工作有以下硬件约束和使用要求,理解这些是正确使用多核的前提。
启动时序:D25F 首启,协处理核由 D25F 启动
上电后 D25F 首先从 Flash 启动,N22 和 DSP 处于复位状态。D25F 在运行过程中通过 sys_n22_init/start、sys_dsp_init/start 按需启动协处理核。N22/DSP 何时启动、从何处加载固件,完全由 D25F 控制。
- N22/DSP 的寄存器和 RAM 需等 D25F 完成上电和复位后才能访问。
存储分配:RAM 独立,Flash 共享
各核有独立的 RAM 空间(D25F 用 SRAM、N22 用 IRAM/DRAM、DSP 用 ILM/DLM),互不干扰;Flash 为多核共享,各核固件均存放在 Flash 的不同区域,由用户通过 *_FW_DOWNLOAD_FLASH_ADDR 宏指定。
- Flash 接口不支持多核同时访问,同一时间仅允许一个核访问。多核可以同时执行 XIP(从 Flash 取指)时,硬件会对 Flash 访问进行仲裁,不过导致效率下降。因此 N22/DSP 的指令不建议放在 Flash 中运行,推荐从 RAM 运行(详见 启动模式与原理)。
外设部署
部分外设固定部署在某个核的域中,由该核管理更高效。跨域访问外设时是否经过异步桥因芯片而异:
| 核 | 管理的外设 | TL751x 是否经异步桥 | TL322x 是否经异步桥 |
|---|---|---|---|
| D25F | 除 rf / timer_n22 外的所有外设 | — | — |
| N22 | rf / timer_n22 | 是 | 否 |
| DSP | — | 否 | 否 |
所有外设寄存器三核均可访问。但访问非本域外设时,若需经过异步桥则读操作较慢。
异步桥性能影响(仅适用于有异步桥的芯片,如 TL751x):
- 典型场景:rf 寄存器由 N22 域管理,D25F 访问 rf 寄存器需经过异步桥,N22 访问则不需要
- 异步桥访问中,读操作较慢,写操作相对快
- 建议跨核大量数据传输使用 DMA(比 MCU 访问效率高,且异步桥写比读快)
跨核 RAM 访问
三核均可读写对方 RAM,但需注意以下规则:
Cache 一致性(0x80000000 规则):
有 Cache 的核访问对方 RAM 时,必须加 0x80000000 前缀绕过 Cache,否则会读到 Cache 中的旧数据,导致数据不一致。无 Cache 的核无此限制。
由于 N22 的 Cache 配置因芯片而异(TL751x 无 Cache,TL322x 有 Cache),跨核访问时是否需要加 0x80000000 也需按芯片区分:
| 访问方 | 访问目标 | TL751x 是否需加 0x80000000 | TL322x 是否需加 0x80000000 |
|---|---|---|---|
| D25F | N22 / DSP RAM | 是(D25F 有 Cache) | 是(D25F 有 Cache) |
| DSP | N22 / D25F RAM | 是(DSP 有 Cache) | 是(DSP 有 Cache) |
| N22 | D25F / DSP RAM | 否(N22 无 Cache) | 是(N22 有 Cache) |
-
通过 DMA 访问对方 RAM 时无需加
0x80000000,DMA 直接访问物理地址。 -
DSP 的 ILM 必须按 word(4 字节)访问。
协处理核启动
N22 和 DSP 的启动机制相同,都分两步:初始化(上电、开时钟、设置复位向量)和启动(释放复位/Stall,开始运行)。区别在于各核的供电域、固件格式和可用加载方式略有不同。
- 协处理核启动接口为
sys_n22_init(addr)/sys_n22_start()和sys_dsp_init(addr)/sys_dsp_start(),具体以对应sys_n22.h/sys_dsp.h为准。
启动模式与原理
N22/DSP 指令不建议放在 Flash 中运行(XIP),推荐从 RAM 运行。 原因如下:
- Flash XIP同时访问:多核同时执行 XIP 时,硬件会对 Flash 访问进行仲裁,导致取指效率下降。
- 异步桥进一步影响效率:对于有异步桥的芯片(如 TL751x),N22 域访问 Flash 还需经过异步桥,读操作更慢,XIP 性能更差。
- N22 无 Cache(TL751x)时 XIP 性能差:D25F 有 Cache 加速 Flash 取指,XIP 性能可接受;TL751x 的 N22 无 Cache,XIP 取指延迟高、时序不确定,无法满足 RF/BLE 协议栈的实时性要求。
协处理核的固件可放在 Flash 中或 RAM 中运行,推荐通过 D25F 侧的 *_BOOTLOADER_MODE 宏选择加载方式。不同加载方式决定了 D25F 是否搬运固件、协处理核从何处取指。
| 模式 | D25F 是否搬运 | 协处理核取指位置 | 适用场景 |
|---|---|---|---|
| BY_MCU | 否 | RAM(本核在S文件自己搬运) | 不推荐s文件自己搬运通过xip速度慢 |
| BY_DMA | 是(DMA 搬运) | RAM | 大固件、不阻塞 D25F(推荐) |
| BY_POINTER | 是(memcpy 搬运) | RAM | 实现简单,但会阻塞 D25F(推荐) |
| BY_NVM_MCU | 否 | NVM(RRAM 等) | 仅支持 NVM 的芯片 |
说明
- N22 固件驱动维护的,DSP是SDK维护的,具体核取指令的行为根据link/s实现为主。
工程配置与 S/link 文件配合
协处理核的启动模式由 D25F 侧宏 和 协处理核侧 S/link 文件 共同决定,两者必须配套:
D25F 侧(MULTI_CORE_Demo):
ENABLE_N22/ENABLE_DSP:选择是否启用对应协处理核N22_BOOTLOADER_MODE/DSP_BOOTLOADER_MODE:选择加载方式N22_FW_DOWNLOAD_FLASH_ADDR/DSP_FW_DOWNLOAD_FLASH_ADDR:指定协处理核固件在 Flash 中的存放地址
协处理核侧(N22_Test_Demo / DSP 专用 Demo):
- 编译时选择对应的
cstartup.S和 link 文件,决定了固件是 Flash 运行还是 RAM 运行,N22驱动的方法都是从ram运行,DSP需要参考dsp具体实现情况。
D25F 侧宏与协处理核侧 S/link 文件的配套关系(N22):
D25F 侧 *_BOOTLOADER_MODE |
协处理核侧 S / link 文件 | 固件运行位置 |
|---|---|---|
| BY_MCU | cstartup_flash.S + flash_boot_ramcode.link |
RAM(自己S中将bin搬运到SRAM,再从SRAM启动) |
| BY_DMA / BY_POINTER | cstartup_ram.S + ram_boot.link |
RAM(推荐) |
两种 link 文件的区别详见 Software Startup。协处理核固件编译后烧录到 *_FW_DOWNLOAD_FLASH_ADDR 指定的 Flash 地址。
使用示例
以 N22 为例,以下代码均在 D25F 侧 MULTI_CORE_Demo 的 user_init() 中调用。
BY_POINTER(D25F 用 memcpy 搬运到 N22 RAM):
sys_n22_init(N22_IRAM_STARTUP_ADDR);
// 从固件头读取段大小和 DLM 起始偏移
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();
BY_DMA 方式类似,用 dma_config / dma_set_address / dma_set_size 搬运 ILM 和 DLM 两段,等待 DMA 搬运完成后再 sys_n22_start()。
BY_MCU(N22 在S文件中搬运固件到SRAM,再从SRAM启动,不推荐):
// 部分芯片需用户手动上电 ZB 模块,部分芯片 sys_n22_init 内部自动处理,以 sys_n22.h 为准
pm_set_dig_module_power_switch(FLD_PD_ZB_EN, PM_POWER_UP);
sys_n22_init(N22_FW_DOWNLOAD_FLASH_ADDR); // 直接传 Flash 地址
sys_n22_start();
N22 由 ZB(baseband)域供电。部分芯片需用户在 sys_n22_init 前手动上电 ZB 模块(pm_set_dig_module_power_switch(FLD_PD_ZB_EN, PM_POWER_UP)),部分芯片在 sys_n22_init 内部自动处理,以 sys_n22.h 为准。
DSP 特有说明
本节适用于集成 DSP 的芯片,具体型号见 Platform SDK 概述。
DSP 启动与 N22 类似,但有以下差异:
sys_dsp_init后 DSP 处于 Stall 状态,需调用sys_dsp_start释放后才开始运行- DSP 的 ILM 必须按 word(4 字节)访问
// BY_DMA:搬运 DLM 段(DMA0)和 ILM 段(DMA1)后启动(推荐)
sys_dsp_init(DSP_ILM_START_ADDR);
// ... dma_config / dma_set_address / dma_set_size / dma_chn_en ...
// ... 等待两路 DMA 搬运完成 ...
sys_dsp_start();
运行控制
Stall(暂停运行)
N22 / DSP 暂停运行有两种方式,恢复后从暂停处继续执行:
| 方式 | 操作 | 特点 |
|---|---|---|
| 内部关(推荐低功耗) | D25F 通过 Mailbox 通知协处理核,协处理核自身进入 WFI,通过自身中断唤醒 | 节省功耗 |
| 外部关 | 时钟的关闭和打开 | 仅暂停时钟,功耗节省不如 WFI |
Mailbox
概述
Mailbox(邮箱)是多核芯片中用于核间通信的硬件模块。每条消息固定为两个 32 位字(共 8 字节),发送方写入消息后触发接收方中断,接收方读取消息后硬件自动清除中断标志。
支持的芯片及 Mailbox 通道详见 Platform SDK 概述 中的芯片特性总表。核心组成(D25F / N22 / DSP)亦参见该表。
后续新增多核芯片将以 SDK 对应 mailbox.h 为准。
工作机制
收发流程
Mailbox 为每对通信核心提供一组消息寄存器(word0 + word1)。通信流程:
- 发送方:先写 word0,再写 word1。硬件在 word1 写入后触发接收方中断。
- 接收方:在中断中读取 word0 和 word1。读完 word1 后硬件自动清除中断标志(这是硬件固定行为,不可配置)。
用户可自定义两个 word 的含义(如 word0 = 命令码,word1 = 参数)。
必须先写 word0 再写 word1,硬件在 word1 写入后才触发中断。消息寄存器为多核共享,发送方写入新消息前需确保接收方已读取上一条消息,否则会被覆盖。
中断配置
Mailbox 中断需通过 PLIC/CLIC 配置优先级和使能:D25F 侧走 PLIC,N22 侧走 CLIC。具体中断号请查阅对应芯片的 mailbox.h。
完整使能流程分三步:
mailbox_set_irq_mask(...); // 1. 使能 Mailbox 模块中断掩码
plic_interrupt_enable(IRQ_MAILBOX_xxx); // 2. PLIC/CLIC 使能对应 IRQ(D25F 侧)
core_interrupt_enable(); // 3. 使能 CPU 全局中断
默认中断可能已使能,若不需要 Mailbox 中断应调用对应的 mask 清除函数禁用。
中断处理函数通过 PLIC_ISR_REGISTER 宏注册(N22 侧用 CLIC 对应的注册宏):
PLIC_ISR_REGISTER(mailbox_n22_to_d25_irq_handler, IRQ_MAILBOX_N22_TO_D25)
消息寄存器自动清除(可选)
这里要注意区分两个“自动清除”:
- 中断标志自动清除(默认硬件行为,2.1 已述):接收方读 word1 后硬件自动清中断标志,不需要软件干预。
- 消息寄存器自动清除(可选功能,本节所述):接收方读 word1 后硬件自动把消息寄存器清零,防止旧消息残留。
默认情况下消息寄存器内容在读取后保留。若希望读取后自动清零,可调用:
mailbox_auto_read_clr_msg_en(); // 开启:读 word1 后自动清零消息寄存器
mailbox_auto_read_clr_msg_dis(); // 关闭
该功能作用于读消息的接收方。TL322x 开启后同时影响 D25F 和 N22 两端。
使用方法
API 命名规则
Mailbox 接口命名以操作者(当前核)为前缀,set 表示设置自己发出去的消息,get 表示获取自己收到的消息:
| 操作 | 接口格式 | 说明 |
|---|---|---|
| 发送消息 | mailbox_<me>_set_<dst>_msg(msg) |
写入后对端自动收到中断 |
| 接收消息 | mailbox_<me>_get_<src>_msg(msg) |
读取后硬件自动清除中断标志 |
<me>为操作者自己(d25f/n22/dsp)set中<dst>为消息目标(对端),get中<src>为消息来源(对端)- 例:D25F 发给 N22 用
mailbox_d25f_set_n22_msg;D25F 收来自 N22 的消息用mailbox_d25f_get_n22_msg msg为unsigned int[2]数组,msg[0] = word0,msg[1] = word1
中断接口命名差异及原因(收发消息接口在各芯片间一致,仅中断 mask/status 接口存在差异):
不同芯片的 Mailbox 中断 mask/status 接口命名不同,根本原因是寄存器布局设计的差异。
-
TL751x(统一寄存器,带参数接口):所有核的 mask 位集中定义在一个 mask 寄存器中,所有核的 status 位集中定义在一个 status 寄存器中。因此接口需要传入掩码参数来指定操作哪一位:
mailbox_set_irq_mask(mask)——mask形如FLD_MAILBOX_N22_TO_D25F_IRQmailbox_get_irq_status()—— 返回值用& FLD_MAILBOX_xxx判断来源mailbox_clr_irq_status(status)
-
TL322x 等后续芯片(分核寄存器,无参数接口):为解决多核同时访问同一个 mask/status 寄存器带来的资源冲突问题,将每个核的 mask 和 status 位分别定义在各自独立的寄存器中。因此接口无需传参,直接通过函数名后缀区分操作哪个核的寄存器:
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()
具体可用接口以芯片的 mailbox.h 为准。
中断处理
中断处理流程需根据上述寄存器布局差异选择对应的接口风格。
分核寄存器风格(TL322x,单源场景):每个核有独立 status 寄存器,直接读取并清除即可,无需传参、无需按位判断。
_attribute_ram_code_sec_noinline_ void mailbox_n22_to_d25_irq_handler(void)
{
if (mailbox_get_irq_status_d25f()) {
mailbox_clr_irq_status_d25f(); // 先清状态
mailbox_d25f_get_n22_msg(msg); // 再读消息(读 word1 后硬件自动清中断标志)
process_message(msg[0], msg[1]);
}
}
PLIC_ISR_REGISTER(mailbox_n22_to_d25_irq_handler, IRQ_MAILBOX_N22_TO_D25)
统一寄存器风格(TL751x,多源场景):所有核共享一个 status 寄存器(如 D25F 同时收 DSP 和 N22 消息),需先查询 status 按位区分来源,清除对应位后再读取消息。
_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); // 先清状态
mailbox_d25f_get_dsp_msg(msg); // 再读消息
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); // 先清状态
mailbox_d25f_get_n22_msg(msg); // 再读消息
handle_n22_message(msg[0], msg[1]);
}
}
PLIC_ISR_REGISTER(mailbox_irq_handler, IRQ_MAILBOX_N22_TO_D25)
先清状态再读消息,可避免在读取处理过程中丢失新到达消息的状态记录。统一寄存器风格在单源场景下也可按多源流程写代码,便于跨芯片复用。
完整收发示例
D25F 侧:
// 发送消息给 N22
unsigned int msg[2] = {0x12345678, 0xABCDEF00};
mailbox_d25f_set_n22_msg(msg); // N22 将收到中断
N22 侧:
// 发送消息给 D25F
unsigned int msg[2] = {0x87654321, 0x00FEDCBA};
mailbox_n22_set_d25f_msg(msg);
注意事项
- 头文件不可混用:不同芯片的 Mailbox 寄存器布局不同,必须使用对应芯片的
mailbox.h。 - 中断接口命名差异源于寄存器布局:TL751x 将所有核的 mask/status 位集中在统一寄存器中(接口带参数),需注意Mailbox 中断掩码建议统一在 D25F 中配置,避免多核各自配置产生冲突;TL322x 等后续芯片为避免多核访问同一寄存器的资源冲突,将各核 mask/status 拆分到独立寄存器(接口按核命名、无参数)。详见 API 命名规则,跨芯片移植时需注意。
SAR ADC
概述
本手册介绍 SAR ADC(逐次逼近型模数转换器)的驱动使用方法,适用于 GPIO 外部模拟电压采集、电池电压(Vbat)监测等场景。
各芯片对 SAR ADC / SD ADC 的支持情况如下,本文仅介绍 SAR ADC:
| 芯片型号 | SAR ADC | SD ADC |
|---|---|---|
| B80 / B80B | ✅ | ❌ |
| B85 / B87 | ✅ | ❌ |
| TC122X / TC123X | ✅ | ❌ |
| B91 / B92 | ✅ | ❌ |
| TL321X / TL721X / TL751X | ✅ | ❌ |
| TL322X | ✅ | ✅ |
| TC321X / TL323X | ❌ | ✅ |
支持的功能
- GPIO 电压采样:测量外部 GPIO 引脚上的模拟电压。
- Vbat 采样:通过内部分压器接入电池电压,用于电池电量监测。
- 差分输入:正相端(P)与负相端(N)均可独立选通 GPIO 或内部 GND,支持差分与单端两种接法。
- NDMA / DMA 两种采集模式:NDMA 为阻塞轮询,适合低频点采;DMA 为自动连续搬运,适合高频流采。
- 多通道硬件扫描(独立 FIFO 架构支持):M / L / R 三通道由状态机自动轮询,DMA 搬运时数据交织排列。
- 可配置参考电压:0.9V / 1.2V,出厂校准基于 1.2V。
架构划分
驱动按 FIFO 架构划分为三类,各类 API 接口差异较大。根据下表找到你的芯片所属架构,直接跳到对应章节。新增芯片时只需更新此表。
| FIFO 架构 | 包含芯片 | 分辨率 | 硬件扫描通道 | Vref |
|---|---|---|---|---|
| DFIFO 架构 | B80 / B80B / B85 / B87 / TC122X / TC123X | 14-bit | MISC | 0.9V / 1.2V |
| Audio 和 ADC 复用 FIFO架构 | B91 / B92 | 14-bit | MISC | 0.9V / 1.2V |
| ADC 专用 FIFO 架构 | TL321X / TL721X / TL751X | 12-bit | M / L / R | 1.2V |
| ADC 专用 FIFO 架构(双 ADC,每个 ADC 都有专用的 FIFO) | TL322X | 12-bit | M / L / R(各核心独立) | 1.2V |
注意
- 各架构具体的 API 接口请直接参考对应芯片的
adc.h头文件和 demo。 - 复用 Audio FIFO 架构的 ADC 不能和 audio 同时使用一个通路
TL 系列 demo 使用划分
| ADC 版本 | 对应芯片型号 |
|---|---|
| ADC_V1.0 | B91 / B92 |
| ADC_V1.1 | TL321X / TL721X / TL751X |
| ADC_V1.2 | TL322X |
工作原理
本章介绍各架构通用的原理,架构特有的 FIFO 细节、注意事项等在各架构章节中说明。
差分输入与量程
ADC 采用差分输入架构:正相端(P)接 GPIO 或 Vbat(经分压器),负相端(N)接 GPIO 或内部 GND(单端模式接 GND)。
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;
测量超过 Vref 的电压时启用预分压(pre_scale),实际可测上限:
V_max = Vref / pre_scale
例如 Vref=1.2V、pre_scale=1/4 时,Vbat 最高可测 4.8 V,GPIO 受 IO 输入能力影响最大可测 3.3 V。
量程规划:根据目标电压选择 pre_scale,确保 V_target ≤ Vref / pre_scale,以 TL721X 举例:
| 目标电压 | Vref | pre_scale | vbat divider | 可测范围 |
|---|---|---|---|---|
| 3.3V GPIO | 1.2V | 1/4 | 1 | 0~3.3V |
| 1.2V 以内信号 | 1.2V | 1 | 1 | 0~1.2V |
| Vbat | 1.2V | 1 | 1/4 | 1.9~4.3V |
外接分压电路注意:
- 当目标电压超过 Vref 量程,除使用内部 pre_scale 外,也可外接电阻分压。分压电阻选型直接影响精度:
- 推荐使用百 KΩ 级电阻分压,兼顾功耗与驱动能力。
- 避免 MΩ 级分压,高阻抗源无法在 Capture 阶段为采样电容(pF 级)充满电荷,导致测量值偏低且采样频率越高偏差越大。
- 若 GPIO 采样电路中包含外部分压电阻,用户需自行进行板级校准。
- 若必须用高阻抗源,需降低采样频率。
- pre_scale 参数对 VBAT 与 GPIO 采样均有影响;而 vbat_divider 参数仅作用于 VBAT 内部分压器,不影响 GPIO 采样。
参考电压与校准
默认 Vref 为 1.2V,建议优先选用以获得更大动态范围。出厂校准均基于 1.2V,若切换至 0.9V 需重新校准。
由于工艺偏差,实际 Vref 偏离理想值。ATE 测试写入增益校准系数到驱动中,电压计算公式:
注意
- 进行板级校准时,须基于原始代码进行,并将校准数据写入官方推荐的地址。若自行规划校准地址,则必须在驱动中移除原有的 ADC 校准接口,以防冲突。
采样状态机
每次转换经历:Set(配置通道)→ Capture(采样电容充电)→ Hold(保持)→ SAR 比较 → 数据写入 FIFO/寄存器。
- 多通道模式下,状态机按 M→L→R 顺序自动轮询,每个通道独立完成上述流程。
注意
- 对于高阻抗传感器采样,高阻抗源(MΩ 级分压)驱动能力弱,Capture 阶段无法为采样电容充满电荷,导致测量值偏低,采样频率越高偏差越大。各架构的具体解决方法见对应章节,推荐使用百 KΩ 级电阻分压。
时钟
ADC 模拟工作时钟必须分频至 4MHz(驱动在 adc_init() 中已配置,默认 24MHz/(1+5))。个别芯片有特殊时钟要求,见对应架构章节。
上电后稳定延时
ADC 上电后模拟参考 LDO 和采样电容需要时间建立稳态,首次采样前必须等待(数据来源:各芯片驱动 adc_power_on() 函数注释):
| 芯片 | 上电后延时 |
|---|---|
| TL321X | >100us |
| TL721X | >200us(GPIO)/ >300us(Vbat) |
| TL751X | >200us |
符号位判断
单端模式(负端接 GND)下,地弹噪声可能让 ADC 捕获微弱负压。若不剥离符号位,微小负压会被当作极大正压(尖峰毛刺)。取码后判断符号位,单端负电压强制归零。符号位位置:14-bit 为 BIT(13),12-bit 为 BIT(11)。
DFIFO 架构 ADC
FIFO 架构
采用 DFIFO 架构:采样前需手动分配一块 16 字节对齐的 RAM(如 adc_data_buf[8]),调用 adc_config_misc_channel_buf() 和 dfifo_enable_dfifo2(),ADC 硬件状态机会自动将数据填入。读取完毕后需禁用 DFIFO。
DFIFO 冲突注意:连续采样依赖 DFIFO(DFIFO1 或 DFIFO2),若同时使用音频业务需错开。
Demo 使用
GPIO 采样(NDMA):
adc_init();
adc_base_init(GPIO_PB2);
adc_power_on_sar_adc(1);
unsigned int mV = adc_sample_and_get_result(); // 内部完成 8 点滤波并直接返回 mV
adc_sample_and_get_result() 内部已实现 8 点插入排序滤波并返回 mV,无需手动取码转换。若不需要内部滤波,可用 adc_sample_and_get_result_manual_mode() 取单次原始码。
注意
- 手动模式时,该接口通过向
adc_data_sample_control写入NOT_SAMPLE_ADC_DATA停止数字流水线,再读硬件模拟寄存器(areg_adc_misc_l/h)拼接原始码,读完后必须恢复标志位为 0。若未停机直接读寄存器会读到乱码。
高阻抗源:测量值偏低时,降低采样频率枚举(如 96K→23K),硬件自动拉长 Capture 周期。
符号位:以 B80 举例:14-bit 分辨率,符号位为 BIT(13),单端负压需归零(见 符号位判断)。
Vbat 采样:B87 芯片通过 pre_scale=1/4 分压、vbat_div 关闭的方式采样(其他芯片pre_scale = 1,divider=1/4)。
获取电压值
adc_sample_and_get_result() 内部封装完整流程:8 点缓冲 → DFIFO 推流 → 符号位处理 → 插入排序 → 取中间第 2,3,4,5 个码值 4 点算术平均 → 校准转换为 mV。
手动模式可用 adc_calculate_voltage(adc_code) 转换。
休眠电源控制
进入 suspend、deep、deep retention 前必须调用 adc_power_on_sar_adc(0) 断电,否则待机功耗超标或唤醒后 ADC 无响应。唤醒后重新初始化,等待 >100us 再首次采样。
Audio FIFO 架构 ADC
FIFO 架构
复用 Audio FIFO(0/1),NDMA 模式直接读取,DMA 模式通过 Audio FIFO 搬运。
Demo 使用
GPIO 采样(NDMA):
adc_gpio_sample_init(ADC_M_CHANNEL, adc_gpio_cfg_m);
adc_power_on();
unsigned short code = adc_get_code(); // 取原始码
unsigned short mV = adc_calculate_voltage(code); // 转电压
DMA 采样:
adc_set_dma_config(chn);
adc_start_sample_dma(buf, len);
B92 的 adc_set_dma_config(chn, fifo_chn) 需额外指定 Audio FIFO 通道。
Vbat 采样:
| 芯片 | pre_scale | vbat_div |
|---|---|---|
| B91 | 1/4 | OFF |
| B92 | 1 | 1/4 |
B91 与 B92 配置相反:B91 通过 pre_scale=1/4 分压、vbat_div 关闭;B92 改为 vbat_div=1/4 硬件分压、pre_scale=1。驱动内部已封装。
B91 Vbat 限制:必须 sys_init() 配置为电池电压 < 3.6V 模式。精度低,仅用于电量监测。若需精确采样或电池 > 3.6V,改用 GPIO 采样加外接分压(推荐 3/4 分压、总阻值 400kΩ、无电容、采样频率 <48K)。
B92 Vbat 限制:当 sys_init() 中 GPIO 电压配置为 GPIO_VOLTAGE_1V8 时,Vbat 采样不可用,需改用外接分压。
高阻抗源:测量值偏低时,降低采样频率枚举(如 96K→23K),硬件自动拉长 Capture 周期。
获取电压值
adc_calculate_voltage(unsigned short adc_code) 返回 mV。
推荐流程:取码 → 符号位处理(BIT(13),见 2.6 节)→ 多点滤波 → adc_calculate_voltage()。
休眠电源控制
进入 PM 休眠前必须调用 adc_power_on(),防止未关闭导致唤醒后采样异常等不可预测的情况。
DEEP/DEEP RETENTION 唤醒后:需重新初始化 SAR ADC,并等待参考电压稳定
SUSPEND 唤醒恢复流程:adc_power_on() → 等待稳定 → sar_adc_sample_start()
独立 FIFO 架构 ADC
FIFO 架构
拥有 ADC 专属硬件 FIFO(独立 SAR_ADC_RX)。支持 NDMA(寄存器轮询/FIFO 单抽)和系统级 DMA(按触发阈值 trig_cnt 自动触发总线搬运)。多通道 DMA 搬运时数据在 RAM 中交织(M-L-R)。
注意
- TL322X 时钟:高速 SAR ADC 要求时钟为 48MHz,系统时钟源须选 PLL 且 PLL 配置需能被 48 整除,否则采样异常。
- TL322X 双 ADC:大部分 API 首参增加
adc_num_e sar_adc_num(ADC_SAR0/ADC_SAR1)。
Demo 使用
(1) GPIO 采样(NDMA 单通道)
// 1. 全局初始化(NDMA 模式仅 M 通道)
adc_init(NDMA_M_CHN);
// 2. 配置 M 通道引脚与参数
adc_gpio_cfg_t cfg = {
.v_ref = ADC_VREF_1P2V,
.pre_scale = ADC_PRESCALE_1F4, // 1/4 分压,量程 0~4.8V
.sample_freq = ADC_SAMPLE_FREQ_96K,
.pin = ADC_GPIO_PB0,
};
adc_gpio_sample_init(ADC_M_CHANNEL, cfg);
// 3. 上电 → 启动采样 → 取码 → 转电压
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)) { // 符号位为 1 表示负压,单端归零
code = 0;
} else {
code &= 0x7FF; // 取低 11 位有效码
}
buf[i++] = code;
}
}
// 4. 滤波 + 转电压(见 5.3 节)
FIFO 清空:adc_start_sample_nodma() 内部已调用 adc_clr_rx_fifo_cnt() 清空 FIFO。若手动操作寄存器重启采样,需显式调用 adc_clr_rx_fifo_cnt(),否则首次取码读到脏数据。
(2) DMA 多通道采样
适用于高频连续采集,支持 M/L/R 多通道硬件轮询,DMA 搬运后数据在 RAM 中交织排列(M-L-R-M-L-R...)。
// app_config.h 中切换:
// #define ADC_MODE ADC_DMA_MODE
// #define ADC_SAMPLE_CHN_CNT DMA_M_L_R_3_CHN_EN // 三通道
// 1. 配置 DMA 通道并注册中断
adc_set_dma_config(DMA6);
dma_set_irq_mask(DMA6, TC_MASK);
plic_interrupt_enable(IRQ_DMA);
core_interrupt_enable();
// 2. 全局初始化(按通道数选择枚举)
adc_init(DMA_M_L_R_CHN); // 三通道扫描
// 3. 分别配置 M / L / R 通道
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. 启动 DMA 采样
short sample_buffer[GROUP_CNT * 3] __attribute__((aligned(4)));
adc_start_sample_dma((short *)sample_buffer, (GROUP_CNT * 3) << 1);
// 5. 中断中置标志,主循环拆分数据
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. 拆分交织数据:sample_buffer[j*chn_cnt + i] → channel_buffers[i][j]
adc_code_split_dma(sample_buffer, GROUP_CNT, 3, channel_buffers);
NDMA 模式仅支持 M 通道,多通道必须用 DMA 模式。
DMA 使用注意:
- DMA 通道选择:通道号由
adc_set_dma_config(chn)指定,需选未被占用的通道。 - 触发深度:
adc_set_rx_fifo_trig_cnt(0)设为 0,FIFO 一有数据立即触发 DMA。 - 启动顺序:先
adc_set_dma_config(),再adc_start_sample_dma()。若状态机早于 DMA 就绪启动,数据顺序会漂移(M-L-R 变 R-M-L)。 - 中断处理:进入 DMA 完成中断后,第一句代码必须停止硬件推流,清除残留 FIFO 后再重新使能。
- 数据拆分:按
sample_buffer[j*chn_cnt + i]拆分到各通道缓冲。
DMA 软硬件交互时序:
sequenceDiagram
participant CPU as 业务层 (CPU)
participant ADC as SAR ADC 状态机
participant DMA as DMA 总线控制器
participant RAM as 系统 SRAM
CPU->>ADC: 初始化参数并上电启动扫描
loop 硬件自治运作
ADC->>ADC: 按 M->L->R 顺序轮询转换
ADC->>DMA: FIFO 产生数据, 发起搬运请求
DMA->>RAM: 将交织的半字数据存入目标缓冲池
end
DMA->>CPU: 设定的搬运长度达到, 触发 DMA 中断
Note over CPU: 中断内:暂停状态机 → 清标志 → 解包数据 → 重启流水线
(3) Vbat 采样
独立 FIFO 架构提供两种 Vbat 采样方式:
方式一:内部 Vbat 通道(推荐,支持 1.9~4.3V 全量程)
adc_init(NDMA_M_CHN);
adc_vbat_sample_init(ADC_M_CHANNEL); // 接入内部分压器
adc_power_on();
adc_start_sample_nodma();
// 取码后用 adc_calculate_voltage(ADC_VBAT_SAMPLE, chn, code) 转电压
驱动内部已封装:自动接通 Vbat 虚拟引脚,硬件分压器配置为 1/4 分压,预分频器设为 1 倍不分压,电压计算自动补偿 4 倍分压系数,使用 Vbat 专有校准值。采样范围 1.9~4.3V。
方式二:GPIO 间接采样 Vbat(仅适用于 1.9 V ~ 3.6 V 供电)
adc_gpio_cfg_t cfg = { .v_ref=ADC_VREF_1P2V, .pre_scale=ADC_PRESCALE_1F4, ... };
adc_gpio_sample_vbat_init(ADC_M_CHANNEL, cfg);
通过 GPIO 通道加 pre_scale 分压间接测量,适用于供电 < 3.6V 的场景。当供电电压超过 3.6V 时,应采用方式一或引入外部分压电路。。
获取电压值
adc_calculate_voltage(adc_sample_chn_e chn, unsigned short adc_code) 需传入通道号(各通道校准系数独立),返回 mV。
TL322X 双 ADC 还需传入采样类型(GPIO / Vbat)和 adc_num_e sar_adc_num(ADC_SAR0 / ADC_SAR1)。
推荐流程:取码 → 符号位判断(BIT(11),见 2.6 节)→ 多点滤波 → adc_calculate_voltage()。demo 的 adc_get_result() 已封装完整流程。
滤波建议:采样点数 ADC_SAMPLE_GROUP_CNT 需为 8 的倍数,推荐 8 或 16;过多有累加溢出风险。
高阻抗源:测量值偏低时,降低采样频率枚举(如 96K→23K),硬件自动拉长 Capture 周期。
休眠电源控制
进入 suspend、deep、deep retention 前必须调用 adc_power_off() 关闭 LDO。若未断电,轻则待机功耗超标,重则模拟 LDO 锁死导致唤醒后 ADC 完全无响应。Deep 唤醒后重新初始化,等待稳定延时(见 上电后稳定延时)再首次采样。
stateDiagram-v2
state "Active (正常工作)" as Active
state "Pre-Sleep (休眠前断电)" as PreSleep
state "Suspend / Deep (休眠)" as Sleep
state "Wakeup (唤醒)" as Wakeup
Active --> PreSleep : 系统发起休眠
note right of PreSleep
挂起 DMA,必须调用 adc_power_off()
end note
PreSleep --> Sleep : 进入低功耗
Sleep --> Wakeup : 定时器/GPIO 唤醒
note left of Wakeup
adc_power_on() + 等待稳定延时
再进行首次采样
end note
Wakeup --> Active : 恢复采集
常见问题排查
精度与误差
测量值偏低?
- 高阻抗源:测量值偏低时,降低采样频率枚举(如 96K→23K),硬件自动拉长 Capture 周期;改用百 KΩ 级分压电阻。
- 唤醒后头几次偏低:
adc_power_on()后增加延时,丢弃前几个采样值。
电压计算出现百毫伏级偏差?
可能原因:错用了 Vref 校准系数。
采样值有偶发尖峰毛刺?
符号位未处理。取码后判断符号位,单端负压强制归零。
采样值跳动大、不稳定?
增加滤波点数(8 或 16);检查电源纹波;PCB 布线模拟信号远离数字高频信号。
数据异常
数据全是 0 附近的底噪?
GPIO 未被正确切换至模拟功能,或被其他外设复用强占。
DFIFO 架构手动模式数据乱码?
未停机读寄存器。需将 adc_data_sample_control 置 1 锁死再读,读完后恢复。
独立 FIFO 架构单次采样数据乱码?
adc_start_sample_nodma() 内部已清空 FIFO。若手动操作寄存器重启采样,需显式调用 adc_clr_rx_fifo_cnt()。
DMA 模式数据顺序错乱(M-L-R 变 R-M-L)或丢包?
- DMA 通道未就绪就启动了状态机。确保
adc_set_dma_config()先于adc_start_sample_dma(),中断入口需要清除中断状态。 - 配置顺序参考 demo 代码。
功耗与休眠
休眠后唤醒 ADC 完全无响应?
进入 Sleep 前未彻底关闭模拟电源域,导致 LDO 锁死。休眠前必须调用断电接口(DFIFO 架构 adc_power_on_sar_adc(0),Audio FIFO / 独立 FIFO 架构 adc_power_off())。
待机功耗超标?
检查休眠前是否调用了 ADC 断电接口,DMA 是否已挂起。
移植与编译
换芯片后编译报错?
API 接口因架构不同。核对架构表架构划分,直接参考对应芯片的 demo。
Vbat 采样值异常?
检查 pre_scale 和 vbat_div 配置是否符合架构要求。
SD ADC
SD ADC原理
Sigma-Delta ADC(Σ-Δ ADC)通过过采样 + 噪声整形 + 数字滤波三步实现高精度模数转换:
-
过采样:以远高于奈奎斯特频率的速率对输入信号采样,将量化噪声分散到更宽的频带
-
噪声整形:Sigma-Delta调制器将量化噪声推向高频,使低频段噪声大幅降低
-
数字滤波:数字滤波器对高速调制器输出进行降采样(decimation),滤除高频噪声,输出高精度低位宽的最终结果
与SAR ADC对比
| 特性 | SD ADC | SAR ADC |
|---|---|---|
| 精度 | 高 | 中等 |
| 采样速率 | 较慢(kHz级) | 较快(MHz级) |
| 信噪比(SNR) | 高 | 中等 |
| 功耗 | 较高 | 较低 |
| 硬件复杂度 | 高(需调制器+数字滤波器) | 低(仅比较器+DAC) |
| 适用场景 | 血糖仪等高精度低频场景 | 键盘、音频等需要高速采样的场景 |
为了便于开发人员进行硬件选型与驱动架构设计,以下表格按独立芯片型号展开,明确区分了每一款芯片对 SAR ADC(逐次逼近型模数转换器) 与 SD ADC(Σ-Δ型模数转换器) 的支持情况:
| 芯片型号 | SAR ADC | SD ADC |
|---|---|---|
| TLSR820x/TLSR8373 | ✅ | ❌ |
| TLSR825x/TLSR8359 / TLSR827x/TLSR8355 | ✅ | ❌ |
| TC122x / TC123x | ✅ | ❌ |
| TLSR921x/TLSR951x / TLSR922x/TLSR952x | ✅ | ❌ |
| TL321x / TL721x / TL751x | ✅ | ❌ |
| TL322x | ✅ | ✅ |
| TC321X / TL323x | ❌ | ✅ |
核心差异:
- SD ADC:靠过采样和噪声整形换取高精度,适合对精度要求高、对速率要求不高的场景。
- SAR ADC:靠逐次逼近实现快速转换,适合对采样速率要求高、精度要求适中的场景。
芯片功能差异总表
本章集中汇总各芯片在 SD ADC 功能上的差异。
功能总览
| 特性 | TC321X | TL322x | TL323x |
|---|---|---|---|
| 采样时钟源 | sys_clk | pclk | pclk |
| 时钟分频公式 | sys_clk/2/(clk_div+1) | pclk/2/(clk_div+1) | pclk/2/(clk_div+1) |
| 采样时钟频率 | 1M / 2M | 1M / 2M | 1M / 2M |
| 降采样率 | 64 / 128 / 256 | 64 / 128 / 256 | 64 / 128 / 256 |
| 采样模式 | GPIO / VBAT | GPIO / VBAT | GPIO / VBAT |
| 数据获取方式 | NDMA Polling / NDMA Interrupt | NDMA Polling / DMA Interrupt | NDMA Polling / DMA Interrupt |
| GPIO正输入(P) | PB0 ~ PB7, PD0 ~ PD1 | PC0 ~ PC5, PB4 ~ PB7 | PB0 ~ PB7, PD0 ~ PD1 |
| GPIO负输入(N) | PB0 ~ PB7, PD0 ~ PD1, GND | PC0 ~ PC5, PB4 ~ PB7, GND | PB0 ~ PB7, PD0 ~ PD1, GND |
| 带Buffer引脚 | 仅B0: PB5, PB6 | 无 | PB5, PB6, PB7, PD0, PD1 |
| GPIO分压配置 | P/N通道独立 | 统一 gpio_div | 统一 gpio_div |
| GPIO检测范围¹ | 通用 | 通用 | 通用 |
| VBAT 1/2分压范围 | 2.0 ~ 2.4V | — | — |
| VBAT 1/4分压范围 | 2.0 ~ 3.6V | 1.7 ~ 4.5V | 2.0 ~ 4.5V |
| 上电后等待时间 | > 160us | 无需等待 | 200us |
| 出厂校准参数 | 真差分 + 假差分 | 仅假差分 | 真差分 + 假差分 |
注释:
GPIO分压检测范围(所有芯片通用):
- 1 (OFF): 0 ~ 1.2V;
- 1/2: 普通引脚 0 ~ 2.4V,带Buffer引脚 0 ~ (VBAT×60%)且≤2.4V;
- 1/4: 普通引脚 0 ~ 3.3V,带Buffer引脚 0 ~ (VBAT×60%)且≤3.3V。
- TC321X的A0/A1/A1S1版本所有引脚均为普通引脚;B0版本中PB5/PB6为Buffer引脚,其余为普通引脚。
Buffer引脚说明:带buffer的引脚能隔离内部等效分压电阻。使用外部分压电阻采样时优先选带buffer引脚,可防止内部等效电阻分压带来的测量误差。
GPIO引脚与检测范围
GPIO采样是SD ADC通过通用输入输出(GPIO)引脚采集外部模拟信号的工作模式,是SD ADC最常用的功能之一。该模式下,ADC可配置为单端(假差分)或真差分输入方式,通过选择不同的GPIO引脚组合和分压系数,实现对外部电压信号的高精度测量。适用于电池电压监测、传感器信号采集等多种场景。
各芯片支持的引脚、Buffer引脚、分压配置与检测范围请参考功能总览。
- N端配置为
SD_ADC_GNDN时为假差分(单端),N端配置为其他GPIO时为真差分。 - GPIO真差分模式下推荐只使用采样范围的10% ~ 90%。
VBAT模式
VBAT采样是SD ADC通过内部专用通道采集芯片供电电压(通常为电池电压)的工作模式,主要用于电池电量监测、低电压保护等场景。该模式无需外部GPIO引脚,直接通过芯片内部连接到电源管理模块的通路进行采样,具有硬件设计简单、抗干扰能力强的特点。
检测范围与分压选择建议请参考功能总览。
分压选择建议:推荐使用1/4分压,因为芯片出厂前的校准参数是基于1/4分压场景进行标定的,可直接获得最佳精度。若使用其他分压系数,建议重新进行校准以确保测量准确性。
硬件架构
时钟架构
采样时钟由时钟源经分频得到:支持1M和2M两种采样时钟频率。
1M 与 2M 的选择:
- 1M:功耗较低,配合下采样率 128 可获得约 7.8 kHz 采样速率,满足大多数低频高精度场景;
- 2M:功耗较高,采样速率翻倍,适合需要更快响应或更高过采样比的场景。
时钟配置宏因芯片而异,详情请参考API说明中的初始化接口。
时钟源与分频公式请参考功能总览。
数字滤波器
数字滤波器对调制器的高速输出进行降采样处理,是 SD ADC 精度的关键环节。降采样倍数(下采样率)直接影响精度和采样速率:
| 下采样率 | 采样速率(1M时钟) | 采样速率(2M时钟) | 适用场景 |
|---|---|---|---|
| 64 | ~ 15.6 kHz | ~ 31.3 kHz | 需要较高采样速率 |
| 128 | ~ 7.8 kHz | ~ 15.6 kHz | 均衡精度和速率(推荐) |
| 256 | ~ 3.9 kHz | ~ 7.8 kHz | 需要最高精度 |
FIFO 与数据获取方式
SD ADC 采样数据先进入 RX FIFO,再通过以下方式之一搬运到 SRAM:
| 方式 | 适用芯片 | 原理 | 特点 |
|---|---|---|---|
| FIFO直接读取 | TC321X | 硬件自动将FIFO数据写入指定SRAM地址,达到阈值后通过状态标志或中断通知 | 需设置FIFO基地址、深度和阈值 |
| NDMA轮询读取 | TL322x/TL323x | CPU直接读FIFO寄存器,通过查询rxfifo_cnt判断是否有数据 |
简单但占用CPU |
| DMA搬运 | TL322x/TL323x | DMA控制器自动将FIFO数据搬运到SRAM,完成后触发中断 | 高效,不占用CPU |
TC321x FIFO参数说明:
- FIFO深度:必须是2^n(n=3 ~ 10),即8/16/32/.../1024;
- 中断阈值:必须是4的倍数,最大1020,且必须比深度小至少1个word;
- 触发条件:FIFO中数据量 > 阈值。
TL322x/TL323x FIFO参数说明:
- RX FIFO深度固定为16 words;
- NDMA模式下通过
sd_adc_get_rxfifo_cnt()查询FIFO数据量,逐个读取sd_adc_get_raw_code(); - DMA 模式下触发数固定为 0(有 1 个数据就搬运),不建议修改。
资源共享与音频兼容性
SD ADC 与音频 AMIC 共享模拟前端,不能同时使用,只能分时使用;DMIC 可以与 SD ADC 同时使用。
TL323x A0 版本注意:
- 使用 ADC 时会自动启用 PLL、LPD、LPC 功能,使用期间不能操作这些模块的寄存器,使用完毕后必须调用
sd_adc_power_off()恢复。 - 芯片供电2.3V以下,不建议使用GPIO采样模式,VBAT采样模式受温度影响较大,温度每变化5度,会引入约10mv的误差。(A1版本已修复该问题)
API说明
sd_adc_init()
初始化SD ADC模块,使能数据加权平均算法(提升性能),TL系列需指定工作模式(当前仅支持SD_ADC_SINGLE_DC_MODE)。
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);
关键注意事项
- 进入PM休眠前必须调用
sd_adc_power_off(),否则唤醒后ADC可能出现采样异常等不可预测行为。 - TC321X从DEEP/DEEP RETENTION唤醒后需重新配置所有SD ADC寄存器。
sd_adc_gpio_sample_init()
初始化GPIO采样模式,配置结构体因芯片而异。关键参数差异请参考功能总览:
- 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:采样时钟分频系数,用于配置VBAT采样的时钟频率。推荐根据芯片手册选择能使采样时钟达到1M或2M的值,具体可参考时钟架构中的分频公式。 -
div:VBAT分压系数选择,类型为枚举sd_adc_vbat_div_e。推荐使用1/4分压(如SD_ADC_VBAT_DIV_1F4),因为芯片出厂校准参数基于此场景标定,可直接获得最佳精度。 -
downsample_rate:降采样率选择,类型为枚举sd_adc_downsample_rate_e。推荐使用SD_ADC_DOWNSAMPLE_RATE_128,此参数能均衡精度和采样速率,兼顾大多数应用场景需求。
sd_adc_sample_start() / sd_adc_sample_stop()
开关顺序规范:
开启:sd_adc_power_on() -> 等待稳定 -> sd_adc_sample_start()
关闭:sd_adc_sample_stop() -> sd_adc_power_off()
每次调用sd_adc_sample_start()后,前4个采样数据因内部滤波器复位而异常,必须丢弃。
sd_adc_set_irq_trig_thres()(TC321X独有)
static inline void sd_adc_set_irq_trig_thres(unsigned int threshold);
设置FIFO中断触发阈值(单位:word,必须是4的倍数,最大1020)。
- 轮询模式:查询
sd_adc_get_irq_status(),标志置位后从buffer读取数据。 - 中断模式:FIFO数据量达到阈值后触发中断,在中断服务函数中读取数据。
sd_adc_get_result()
该函数不是驱动自带的API,而是Demo中提供的参考实现,封装了"获取采样数据 -> 排序滤波 -> 转换结果"的完整流程。用户可直接参考或修改。
signed int sd_adc_get_result(sd_adc_result_type_e result_type);
参数:
| result_type | 说明 | 返回值 |
|---|---|---|
SD_ADC_VOLTAGE_10X_MV |
电压,单位0.1mV | 如33000表示3.3V |
SD_ADC_VOLTAGE_MV |
电压,单位mV | 如3300表示3.3V |
内部流程:
- 获取采样数据到buffer(TC321X从SRAM buffer读取,TL系列从FIFO或DMA buffer读取)
- 调用
sd_adc_sort_and_get_average_code()排序滤波得到平均code - 调用
sd_adc_calculate_voltage()转换为最终结果
TL323x特殊处理:GPIO模式下,sd_adc_get_result()内部会调用sd_adc_div_switch_adjust_rescale()自动调整分频器量程。当检测到电压<50mV时切换到1:1分频提高分辨率,>1000mV时切回1/4分频防止溢出。切换后会自动重新采样。
数据处理
排序滤波算法(Demo 提供)
Demo 中提供的 sd_adc_sort_and_get_average_code() 采用插入排序 + 去极值平均的方法:对原始数据升序排序后,丢弃前 N/4(最小)和后 N/4(最大),取中间 N/2 求平均,得到 code 平均值。采样数量必须是 4 的倍数,推荐 ≥ 16。
sd_adc_get_result(result_type) 封装了"取数 -> 滤波 -> 转换"完整流程,返回 SD_ADC_VOLTAGE_10X_MV(0.1mV)或 SD_ADC_VOLTAGE_MV(mV)格式的电压。该函数不是驱动自带 API,用户可直接参考或修改。
电压转换公式
基础公式(code转原始电压,未校准):
V_raw = code / OSR³ / 2 × divider
其中 OSR = 下采样率,divider = 分压系数(8/div值)
校准后公式:
V_result = V_raw × vref / 10000 + offset (单位:0.1mV)
V_result = V_raw × vref / 10000 / 10 + offset / 10 (单位:mV)
校准
SD ADC的采样精度受工艺偏差影响,需要通过两点校准来修正。校准的本质是对ADC的增益(gain/vref)和偏移(offset)进行补偿。
- 校准原理
通过采集两个已知标准电压(V1、V2)对应的ADC原始码值(code1、code2),建立线性方程求解校准参数的方法。具体步骤如下:
a. 采集标准点:分别输入高精度基准电压V1(如0.5V)和V2(如2.5V),获取对应的ADC采样码值code1和code2。
b. 计算校准参数:
- 增益系数:`vref = (V2 - V1) / (code2 - code1)`,为了尽可能保证精度,该值在ATE写入校准值时已放大1万倍,因此后续计算中需除以1万以恢复实际增益系数。
- 偏移量:`offset = V1 - gain * code1`。
c. 应用校准:实际测量时,通过V_result = vref * code / 10000 + offset计算真实电压。该方法可有效修正ADC的线性误差,适用于对精度要求较高的场景,如传感器信号采集、电池电压监测等。芯片出厂时通常通过ATE设备完成两点校准,将gain和offset存储在Efuse或Flash中,用户也可自行校准。
- 默认校准值
驱动内置了ATE大数据中位值作为默认校准值,保证ATE未校准状态下GPIO和VBAT采样误差在50mV以内。
- 校准接口
默认校准机制:各芯片在 platform_init() 中自动完成ATE校准,无需用户干预。
自行校准步骤:
-
屏蔽上述ATE校准接口的调用
-
将校准变量恢复为"不校准"初值:
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
校准子接口介绍:
| 子接口 | 适用芯片 | 说明 |
|---|---|---|
sd_adc_set_single_gpio_calib_vref() |
全部 | 单端GPIO校准 |
sd_adc_set_vbat_calib_vref() |
全部 | VBAT两点校准 |
sd_adc_set_diff_gpio_calib_vref() |
TC321X/TL323x | 差分GPIO校准 |
sd_adc_set_single_gpio_no_div_calib_vref() |
TL323x(A0版本) | 无分压(1:1)单端GPIO校准 |
sd_adc_set_vbat_4p_calib_vref() |
TL323x(A0版本) | VBAT四点校准(2.200/2.225/2.250/2.275V) |
sd_adc_set_vbat_2v2_calib_vref() |
TL323x(A0版本) | VBAT < 2.2V曲线校准 |
说明
- TL323x系列仅A0版本VBAT模式支持分段校准:2.2 ~ 2.275V使用4点分段线性插值,<2.2V使用二次曲线拟合,其余区间使用线性校准。
使用注意事项
校准与配置的关系
GPIO模式下,不同的GPIO引脚或不同的分压系数需要各自校准,无法共用同一套校准值。芯片出厂前是按驱动默认配置校准的:
- 推荐:使用驱动默认配置,直接使用出厂校准值;
- 改配置且对精度要求高:用户需自行校准;
- 改配置但对精度要求不高:可使用出厂校准值,误差可能增大。
前四个数据异常
每次调用sd_adc_sample_start()后,因内部数字滤波器复位,前4个采样code异常,必须丢弃。建议设置足够的采样数量(≥16),配合排序滤波算法消除影响。
PM休眠
进入PM休眠前必须调用sd_adc_power_off(),防止未关闭导致唤醒后采样异常等不可预测的情况。
- DEEP/DEEP RETENTION唤醒后:需重新初始化SD ADC,并等待参考电压稳定
- SUSPEND唤醒恢复流程:
sd_adc_power_on()→ 等待稳定 →sd_adc_sample_start()
音频兼容性
SD ADC与音频AMIC不能同时使用(共享模拟前端),只能分时使用,DMIC可以同时使用。
使用示例
TC321X 轮询模式
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); // 设置FIFO地址和深度
sd_adc_set_irq_trig_thres(16); // 设置阈值
sd_adc_power_on(SD_ADC_SAMPLE_MODE);
sleep_us(160); // 等待VMID稳定
sd_adc_sample_start();
}
void main_loop(void) {
if (sd_adc_get_irq_status()) {
sd_adc_sample_stop(); // 停止采样,防止数据被覆盖
// 处理buffer中的数据...
sd_adc_sample_start(); // 重新开始采样
}
}
初始化: init → sample_init → set_rx_fifo → set_thres → power_on → wait → sample_start
主循环: irq_status? ──No──→ 继续轮询 ──Yes──→ sample_stop → 读取buffer处理 → sample_start → 回到轮询
TC321x 中断模式
void user_init(void) {
// ...同轮询模式的初始化...
sd_adc_set_irq_mask(); // 使能SD ADC中断
irq_set_mask(FLD_IRQ_DAM_FIFO_EN); // 使能FIFO中断
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(); // 清除中断(内部会清wptr并stop)
flag = 1; // 置标志位,主循环处理
}
}
初始化: init → sample_init → set_rx_fifo → set_thres → set_irq_mask → irq_enable → power_on → wait → sample_start
ISR: irq触发 → clr_irq_status(清wptr+stop) → flag=1
主循环: flag? ──No──→ 等待 ──Yes──→ 读取buffer处理 → sample_start → flag=0 → 回到等待
TL322x / TL323x NDMA轮询模式
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) {
// 逐个从FIFO读取
for (int i = 0; i < 16; ) {
if (sd_adc_get_rxfifo_cnt() > 0) {
buffer[i++] = sd_adc_get_raw_code();
}
}
// 处理数据...
}
初始化: init → sample_init → power_on → wait(TL323x) → sample_start
主循环: rxfifo_cnt > 0? ──No──→ 继续等待 ──Yes──→ 读取raw_code到buffer → 采够指定数量后处理数据 → 回到循环
TL322x / TL323x DMA中断模式
void user_init(void) {
sd_adc_init(SD_ADC_SINGLE_DC_MODE);
sd_adc_gpio_sample_init(&cfg);
sd_adc_set_dma_config(DMA2); // 配置DMA通道
dma_set_irq_mask(DMA2, TC_MASK); // 使能DMA传输完成中断
plic_interrupt_enable(IRQ_DMA);
core_interrupt_enable();
sd_adc_start_sample_dma(buffer, 16 << 2); // 启动DMA采样(16个word = 64字节)
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)
初始化: init → sample_init → set_dma_config → dma_irq_enable → start_sample_dma → power_on → wait(TL323x) → sample_start
ISR: DMA传输完成 → sample_stop → clr_irq_status_dma → rx_done_flag=1
主循环: rx_done_flag? ──No──→ 等待 ──Yes──→ 读取buffer处理 → start_sample_dma → sample_start → flag=0 → 回到等待
USB
USB 公共部分
USB简介
USB (Universal Serial Bus) 即通用串行总线,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯,是应用在PC领域的接口技术。USB接口支持设备的即插即用和热插拔功能。USB是在1994年底由英特尔、康柏、IBM、Microsoft等多家公司联合提出的。如下图所示,USB由四根线组成,即VCC、GND、D-(DM)、和D+(DP),USB可选择通过主机供电也可自供电,目前大部分USB设备是通过主机供电。

USB通信是指控制器与设备间的通信,电脑主机即为控制器,Telink USB为设备,后文中引用的主机默认为控制器。USB总线是一种单向总线,通信只能由控制器发起,设备收到控制器请求时,将数据发给控制器。控制器是每隔n个单位时间向设备发送请求,n为用户配置参数。
USB有超高速(5.0Gbit/s)、高速(480Mbit/s)、全速(12Mbit/s)和低速(1.5Mbit/s)等四种工作速度,其中全速和低速USB总线的通信帧周期(连续两帧数据的发送间隔)为1ms,高速USB总线的通信帧周期为125us。USB1.1仅支持全速和低速,USB2.0支持高速、全速和低速,超高速仅在USB3.0中有支持。
USB包格式和传输过程
包(Packet)是USB数据传输的最基本单元,即每次数据传输都是以包的形式进行。而包要组成事务才能进行有效通信。根据通信的需求有各种包,用于组成各种事务(IN、OUT、SETUP)。一个或多个事务组成一个传输(控制传输、批量传输、终端传输和等时传输)。
包是USB总线上数据传输的最小单位,不能被打断或干扰,否则会引发错误。若干个数据包组成一次事务,一次事务也不能打断,即属于一次事务的几个包。
(1) USB包结构
包(Packet)是USB系统中信息传输的基本单元,所有数据都是经过打包后在总线上传输的。
如下图所示,USB包由七部分组成,即同步域(SYNC)、包标识(PID)、地址域(ADDR)、端点域(ENDP)、帧号域(FRAM)、数据域(DATA)和校验域(CRC)。注意,并不是每个USB包都包含上述的七个域,也就是说有些包只包含其中的几个域。

1) 同步域
同步域主要是通知对方数据传输开始,并提供同步时钟。对于低速设备和全速设备,同步域使用的是0000 0001(二进制数);对于高速设备使用的是00000000 00000000 00000000 00000001。
2) 包标识 (PID)
包标识主要用于标识包的类型,由8位组成:低 4位是PID编码,高4位是校验字段,是对低4位取反得到,USB中各种包是通过PID字段来区分。
3) 地址域
由于接入USB总线的设备可能有多个,因此需要引入地址域,以便于区分当前通信的设备是哪个设备。地址域包含7个数据位,最多可以指定128个地址,地址0用作缺省地址,不分配给USB设备。对于USB总线上的每个设备,地址唯一。
4) 端点域
端点域用于指定USB总线上某个设备的一个端点号,包含4个数据位;全速/高速设备最多可以含有16个端点,低速设备最多含有3个端点。所有USB设备都必须含有一个端点号为0的端点,用于主机与设备间交换基本信息。除端点0外,其余的端点都是具体USB设备所特有的。地址域和端点域组合,明确了主机与设备间通信的通道。
5) 帧号域
帧号字段用于指出当前帧的帧号,它仅在每帧/微帧开始的SOF令牌包中被发送,其数据位长度为11位,每传输一帧,主机就将其加1,当达到最大值7FFH时归零。
6) 数据域
数据字段包含主机和USB设备间需要传输的数据,以字节为单位,最大长度为1024,而实际长度取决于传输的具体情况。
7) 校验域
校验域主要是为了校验通信数据的正确性。USB令牌包和数据包中都使用了CRC。但是,CRC是发送方在进行位填充之前产生的,这样要求接收方在去除位填充之后,再对CRC字段进行译码。信息包中的PID字段本身含有校验,所以CRC计算不包含有PID部分。令牌包的CRC采用的是5位CRC,数据包中的数据字段使用的是16位CRC。
a. 令牌包
SOF包由主机发送给设备:对于full-speed总线,每隔1.00 ms±0.0005 ms发送一次;对于high-speed总线,每隔125us±0.0625us发送一次。
SOF包格式

IN,OUT,SETUP包格式

b. 数据包
数据包(DATA0, DATA1, DATA2, MDATA)

c. 握手包
PRE, ACK, NAK, STALL, NYET包格式

(2) USB传输过程
1) USB事务
在USB的一次数据接收或发送的处理过程称为事务处理(Transaction),事务通常是由一系列包组成,事务不同,组成的包也不同。USB数据传输中,常见的事务有输入(IN)事务、输出(OUT)事务和设置(SETUP)事务。注意,SOF只是指示一帧的开始,无有效数据,并不是一次事务; EOF帧发送结束后的一种电平状态,也不是事务。
事务通常由两个或者三个包组成:令牌包、数据包和握手包,令牌包启动事务,数据包传输数据,握手包的发送者通常为数据接收者,当数据正确接收后,发送握手包,设备也可以使用NACK表示数据未准备好。
2) 输入事务
输入(IN)事务是主机从USB设备的某个端点中获取数据的过程,如下图所示,一个输入事务有三种状态,即正常的输入事务(图(a))、设备忙或无数据时的输入事务(图(b))和设备出错时的输入事务(图(c)),正确的输入事务包括令牌包、数据包和握手包三个阶段。
输入事务处理流程



结合正常的输入事务实例,对正常的输入事务进行介绍和分析,如下图所示,一个正常的输入事务包含三个交互流程:(1)Host向Device发送一个IN令牌包;(2)Device收到IN令牌包后,将待发送数据发给主机;(3)主机收到数据包后,回复一个ACK包来确认数据包被正确接收。

3) 输出事务
输出(OUT)事务是主机向USB设备的某个端点中发送数据的过程,如下图所示,一个输出事务有三种状态,即正常的输出事务(图(a))、设备忙时的输出事务(图(b))和设备出错时的输出事务(图(c))。正确的输出事务包括令牌、数据和握手三个阶段。
输出事务处理流程



下面结合正常的输出事务实例,对正常的输出事务进行介绍和分析,如下图所示,一个正常的输出事务包含三个交互流程:(1)Host向Device发送一个OUT令牌包;(2)Host向Device发送数据包;(3)Device收到数据包后,回复一个ACK包来确认数据包被正确接收。

4) 设置事务
设置(SETUP)事务处理并定义了Host与Device之间的特殊的数据传输,它仅适用于USB控制传输的建立阶段。如下图所示,设置事务通常有三种状态,即正常设置事务(图(a))、设备忙设置事务(图(b))和设备出错设置事务(图(c)),正确的设置事务包括令牌、数据和握手三个阶段。
设置事务处理流程



下面结合正常的设置事务实例,对正常的设置事务进行介绍和分析, 如下图所示,一个正常的设置事务包含三个交互流程:(1)Host向Device发送一个SETUP令牌包;(2)Host向Device发送DATA0数据包;(3)Device收到数据包后,回复一个ACK包来确认数据包被正确接收。

(3) USB传输
传输由OUT、IN或SETUP等事务构成,USB标准协议中定义了4种传输类型:控制传输 (Control Transfer)、批量传输 (Bulk Transfer)、中断传输 (Interrupt Transfer)和同步传输 (Isochronous Transfer)。四种传输的优先级由高到底一次为:同步传输、中断传输、控制传输、批量传输。
1) 控制传输
控制传输(Control Transfer)是USB中最基础、最重要的传输方式,也是端口0的默认传输方式。控制传输典型地用在主机和USB外设之间的端点(Endpoint)0之间的传输,但是指定供应商的控制传输可能用在其它的端点上控制传输主要用来主要进行查询,配置和给USB设备发送通用的命令。控制传输是单向传输(端点0除外,端点0为双向传输),数据量通常较小。控制传输的数据包最大长度取决于其工作速度,低速模式最大包长固定为8字节、高速模式最大包长度为64字节,全速模式可在8、16、32和64字节中选择。
注意
- Telink USB的端点0的包长度:TC平台(B80/B80B/B85/B87/TC121X)固定为8字节;B91/B92/TL321X/TL721X/TL751x 可通过
usbhw_set_ctrl_ep_size()配置为8/16/32/64字节。 - 控制传输分为三个阶段,即建立阶段、数据阶段(可选)和状态阶段组成,每个阶段都由一次或多次(数据阶段)事务组成。
- 建立阶段: 如图建立阶段由SETUP事务组成。SETLUP事务的数据阶段总是用DATA0,且定长8字节。

数据阶段: 数据阶段可选。如果有数据阶段,则包括一个或多个IN/OUT事务。用于传输建立阶段要求的,具有USB定义格式的数据。数据阶段的事务具有相同的方向,即要么全是IN,要么全是OUT。如果要传输的数据大于一个包的长度,则主控器将其分成多个包传输,一旦数据传输方向改变,就会认为进入到状态过程,数据过程的第一个数据包必须是DATA1包,然后每次争取传输一个数据包后就在DATA0和DATA1之间交换。如果最后一个包大小等于最大包大小,则应再传一个0大小的包,以确定结束。根据数据阶段的数据传输的方向,控制传输又可分为3种类型,即控制写入(Control Write)、控制读取(Control Read)和无数据控制(No-data Control),如下图所示

状态阶段:状态阶段是控制事务处理的最后一个阶段,由一个IN或OUT事务组成,总是使用DATA1 数据包组成。状态阶段与数据阶段传输的方向相反,即如果数据阶段是IN,则状态阶段是OUT,反之亦然。用于报告建立阶段和数据阶段的传输结果。
2) 中断传输
中断传输(Interrupt Transfer)在流程上除不支持PING和NYET包之外,其他的和批量传输相同,因此其序列图可参考批量传输。中断传输与批量传输的主要区别体现在两点:(1)优先级不一样,中断传输的优先级高于批量传输,仅次于同步传输;(2)支持最大包长度不同,中断传输低速模式最大包长上限为8字节,全速模式最大包长上限为64字节,高速模式最大包长上限为1024字节。
需要注意的是,这里所说的中断和硬件上的中断是不一样的。由于USB不支持硬件的中断,所以必须靠主机周期性地轮询,以便获知是否有设备需要传送数据给主机。由此可知,中断传输也是一种轮询过程,轮询的周期由用户设备决定(全速设备的轮询间隔为1ms \~ 255ms,低速设备10ms \~ 255ms),主机只需要保证在不大于该时间间隔内,安排一次传输即可。轮询周期非常重要,如果太快,会占用太多的总线带宽,如果太低,数据可能会丢,因此用户需要根据自身数据的状况来设置。
中断传输通常用在数据量不大,但是对时间要求较严格的设备中,如人机接口设备(HID)中的键盘、鼠标等。中断传输也可用来不断检测设备状态,当条件满足时,再使用批量传输来传送大量数据。中断传输的端点类型一般为IN端点,即从Device到Host(IN事务),很少用在OUT端点上,有些电脑甚至不支持中断传输的OUT事务。

3) 同步传输
同步传输(Isochronous Transfer),又叫等时传输,是不可靠传输。同步传输只有令牌包(IN/OUT令牌包)与数据包(DATAx)两个阶段,它没有握手包,也不支持PID翻转,主机在排定传输时,同步传输有最高的优先级。同步传输数据包最大长度为全速模式上限为1023字节,高速模式上限为1024字节,低速模式不支持同步传输。

同步传输适用于必须以固定速率抵达或在指定时刻抵达,可以容忍偶尔错误的数据上。USB为其保留总线带宽,保证能在每帧/小帧内都得到服务。速率准确,传输时间可预测。但不采用差错控制和重传机制,不保证每次传输都成功,适用于音频、视频设备。
4) 批量传输
批量传输(Bulk Transfer),又称为块传输,是单向可靠传输,由一个或多个IN/OUT事务组成,事务中的数据包按照DATA0-DATA1-DATA0-…方式翻转,以保证传输端和接收端的同步,如下图所示。

USB中的错误检测和重传机制是通过硬件来完成的,若本次传输错误,DATA包不会翻转,并重新发送该包。同时,接收端接收到连续PID相同的DATA包,将视为重传包。USB允许连续3次以下的传输错误,超过3次,主机认为该端点功能错误(STALL),放弃该端点的传输任务。
USB应用 — 基本概念
本章节所讲的USB应用并不是指USB的用途,而是位于USB驱动层之上的应用层设计。本章节将从用户的角度,来详细讲解USB的基本概念和工作原理,以方便用户熟悉和掌握USB的基础知识和使用方法。
USB硬件设备和软件设备的关系为,一个USB硬件设备可以对应一个或多个软件设备,这取决于用户的枚举信息(配置描述符信息)。软件设备是PC将硬件设备中,实现同一个功能的类的接口抽象出来,并可统一操作的虚拟设备。一个软件设备包含一个或多个接口,一个接口包含一个或多个端点(端点将在下面讲解),而接口和端点是硬件设备中所有的概念。
端点(Endpoint)是USB设备中的可以进行数据收发的最小单元。除端点0(固定为双向控制传输)之外,其他所有端点仅支持单向通信,即输入端点(数据流为从设备到主机)或输出端点(数据流为从主机到设备)。设备支持端点的数量是有限制的,除默认端点0外,低速设备最多支持2 组端点(2个输入,2个输出),高速和全速设备最多支持15 组端点。
接口(Interface) 是USB设备中组成一个基本功能的端点的集合,是USB设备驱动程序控制的对象(主机会根据接口,在PC端虚拟一个可直接操作的USB设备,该虚拟设备即是一个USB设备类)。从主机的角度来看,一个USB设备可由一个或多个接口组成,如集成了鼠标和键盘的USB设备,有两个Interface,一个键盘,另一个是鼠标;如音频设备就是由一个用于命令传输的接口和一个用于数据传输的接口组成。
总结如下:
端点:端点是USB设备的唯一可识别部分,其是主机和设备之间的通信流的终点,是一个USB设备或主机上的一个数据缓冲区,用来存放和发送USB的各种数据。
接口:可以理解为一个功能。
配置:对接口的组合,在连接期间选定是那种组合。

标准描述符
描述符(Descriptor)是用来描述设备的属性的数据结,分为标准描述符和专有描述符。标准描述符是所有USB设备类通用的属性描述,包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符等,其中字符串描述符又分为序列号描述符、产品描述符、厂商描述符以及语言ID描述符。专有描述符是每个设备类所特有的描述符,如HID类特有的描述符有HID描述符、报告描述符和实体描述符等,下图为USB协议规定的标准设备请求结构。

(1) 设备描述符
设备描述符描述了有关USB设备的基本信息,设备有且只有一个设备描述符。下图给出了标准设备描述符的结构,前8个字节概括了USB的基本属性,这也是USB枚举中主机首先要获取的信息。
| 偏移量 | 域 | 大小 | 值 | 描述 |
|---|---|---|---|---|
| 0 | bLength | 1 | 数字 | 此描述表的字节数 |
| 1 | bDescriptorType | 1 | 常量 | 描述符的类型(此处应为0x01,即设备描述符) |
| 2 | bcdUSB | 2 | BCD码 | 此设备与描述表兼容的USB设备说明版本号(BCD 码) |
| 4 | bDeviceClass | 1 | 类 | 设备类码 |
| 5 | bDeviceSubClass | 1 | 子类 | 子类掩码 |
| 6 | bDeviceProtocol | 1 | 协议 | 协议码 |
| 7 | bMaxPacketSize0 | 1 | 数字 | 端点0的最大包大小(仅8,16,32,64为合法值) |
| 8 | idVendor | 2 | ID | 厂商标志(由USB-IF组织赋值) |
| 10 | idProduct | 2 | ID | 产品标志(由厂商赋值) |
| 12 | bcdDevice | 2 | BCD码 | 设备发行号(BCD 码) |
| 14 | iManufacturer | 1 | 索引 | 描述厂商信息的字符串描述符的索引值。 |
| 15 | iProduct | 1 | 索引 | 描述产品信息的字串描述符的索引值。 |
| 16 | iSerialNumber | 1 | 索引 | 描述设备序列号信息的字串描述符的索引值。 |
| 17 | bNumConfigurations | 1 | 数字 | 可能的配置描述符数目 |
注意
- idVendor(VID)和idProduct(PID)用来唯一标识一个设备,但是对于Windows系统来说,仅给定VID和PID,并不能唯一确定设备,表现为不停安装新的驱动。此时,还需要考虑序列号字符串,也就是说只有VID、PID和序列号都一致时,Windows只需要安装一次驱动。
- 三个字符串描述符的索引值应该是不同的值(0除外)。
(2) 配置描述符
配置描述符定义了设备的配置信息,一个设备可以有多个配置描述符。
| 偏移量 | 域 | 大小 | 值 | 描述 |
|---|---|---|---|---|
| 0 | bLength | 1 | 数字 | 此描述表的字节数长度。 |
| 1 | bDescriptorType | 1 | 常量 | 配置描述表类型(此处为0x02) |
| 2 | wTotalLength | 2 | 数字 | 此配置信息的总长(包括配置,接口,端点描述符) |
| 4 | bNumInterfaces | 1 | 数字 | 此配置所支持的接口个数 |
| 5 | bConfigurationValue | 1 | 数字 | 在SetConfiguration(x)请求中用作参数来选定此配置 |
| 6 | iConfiguration | 1 | 索引 | 描述此配置的字串描述表索引(0-无) |
| 7 | bmAttributes | 1 | 位图 | 配置特性: D7:保留(设为一) D6:自给电源 D5:远程唤醒 D4..0:保留(设为一) |
| 8 | MaxPower | 1 | mA | 在此配置下的总线电源耗费量,以 2mA 为一个单位 |
(3) 接口描述符
接口描述符说明了接口所提供的配置,一个配置所拥有的接口数量通过配置描述符的bNumInterfaces决定。
| 偏移量 | 域 | 大小 | 值 | 描述 |
|---|---|---|---|---|
| 0 | bLength | 1 | 数字 | 此表的字节数 |
| 1 | bDescriptorType | 1 | 常量 | 接口描述表类(此处应为0x04) |
| 2 | bInterfaceNumber | 1 | 数字 | 接口号,当前配置支持的接口数组索引(从零开始)。 |
| 3 | bAlternateSetting | 1 | 数字 | 可选设置的索引值。 |
| 4 | bNumEndpoints | 1 | 数字 | 此接口用的端点数量,端点0除外 |
| 5 | bInterfaceClass | 1 | 类 | 接口所属的类值 |
| 6 | bInterfaceSubClass | 1 | 子类 | 子类码 |
| 7 | bInterfaceProtocol | 1 | 协议 | 协议码:bInterfaceClass 和bInterfaceSubClass 域的值而定。 |
| 8 | iInterface | 1 | 索引 | 描述此接口的字串描述表的索引值。 |
(4) 端点描述符
USB设备中的每个端点都有自己的端点描述符,由接口描述符中的bNumEndpoint决定其数量。
| 偏移量 | 域 | 大小 | 值 | 描述 |
|---|---|---|---|---|
| 0 | bLength | 1 | 数字 | 此描述表的字节数长度 |
| 1 | bDescriptorType | 1 | 常量 | 端点描述表类(此处应为0x05) |
| 2 | bEndpointAddress | 1 | 端点 | 此描述表所描述的端点的地址、方向:Bit 3..0 : 端点号. 端点编号在改配置中不能重复;Bit 6..4 : 保留,为零;Bit 7: 方向,如果控制端点则略。0:输出端点(主机到设备)1:输入端点(设备到主机) |
| 3 | bmAttributes | 1 | 位图 | 端点的特性。Bit 1..0 :传送类型 00=控制传送 01=同步传送 10=批传送 11=中断传送 |
| 4 | wMaxPacketSize | 2 | 数字 | 当前配置下此端点能够接收或发送的最大数据包的大小。对于中断传输,批量传输和控制传输,端点可能发送比之短的数据包。 |
| 6 | bInterval | 1 | 数字 | 主机轮询该端点的间隔,对于批量传送和控制传送的端点忽略;对于同步传送的端点,必须为1;对于中断传输,低速模式此处为10\~ 255(ms),全速模式为1 \~ 255(ms) 。 |
(5) 字符串描述符
字符串描述符是可选的.如果不支持字符串描述符,其设备、配置、接口描述符内的所有字符串描述符索引都必须为0,语言字符串描述符的索引为0。
| 偏移量 | 域 | 大小 | 值 | 描述 |
|---|---|---|---|---|
| 0 | bLength | 1 | 数字 | 此描述表的字节数(bString域的数值N+2) |
| 1 | bDescriptorType | 1 | 常量 | 字串描述表类型(此处应为0x03) |
| 2 | bString | N | 数字 | UNICODE 编码的字串 |
USB枚举
枚举就是主机从设备端读取一些信息,知道设备是什么样的设备,如何进行通信,这样主机就可以根据这些信息加载合适的驱动程序。调试USB设备,很重的一点就是查看USB的枚举,只要枚举成功了,那么就已经成功了大半。下面结合USB枚举序列图(下图(a))和Telink 鼠标类USB枚举实例(下图(b)),详细介绍USB的枚举流程。
(1) USB枚举序列
下图(a)中给出了USB枚举序列图,从图中我们可以看出,USB枚举过程分为8步完成,其中Step 1 \~ 7为标准USB的枚举过程,Step 8 USB设备类专有的枚举流程。

- Step 1 主机检测到有设备接入后:首先,主机根据差分信号线上的电平状态,来判断该设备是低速设备,还是全速设备(高速设备在上电初始时,默认为全速设备);然后主机等待设备电源稳定(>=100ms)后,给设备发送复位信号(D+和D-全为低电平,持续>=10ms);最后,如果为高速设备,且主机(Hub)支持高速模式时,主机与设备进行高速检测和握手后,设备可切入高速模式,否则仍维持全速模式。
- Step 2 进行完Step 1后,主机会使用端点0(默认端点,控制传输),发送GetDescriptor请求(设备地址为0),设备接收到请求后,会将自己的设备描述符发给主机,主机会根据此设备描述符(bMaxPacketSize0字段)来进行下一步动作。需要注意的是:(1)只有设备在Step 1中接收到复位信号才去响应主机;(2)已经完成枚举的设备不响应该请求;(3)描述符的长度至少为8个字节(bMaxPacketSize0字段在第8个字节);(4)如果设备超时未响应或响应错误,主机会重新开始,并尝试三次,三次仍无法获取正确的响应,主机会认为该设备为无法识别的设备(下同)。
- Step 3 主机在正确完成Step 2,获取端点0的最大数据包长度后,重新复位设备,之后将按照该长度来对数据包进行拆包和组包。
- Step 4 主机给设备分配一个非0的地址,该地址与集线器上其他设备不同,用于保证定向通信的稳定性。主机完成地址设备后,主机和设备的通信将一直按照新的地址进行通信,直至设备复位或移除。
- Step 5 主机按照Step 4中设备的新地址,依次获取设备的标准描述符(设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符)。注意:(1)设备描述符的长度,主机在Step 2中就已经获得,因此主机指定长度(最大长度为设备描述符长度)获取设备描述符,其他描述符则按照最大长度255来获取,设备端只需要按照实际长度发送即可;(2)设备的接口描述符和端点描述符等,可能包含在配置描述符中,主机会根据配置描述符中的wTotalLength字段,来获取该配置的所有数据;(3)如果设备有多个配置,主机会分为多次来索要配置描述符;(4)主机会根据设备、配置、接口和端点等描述符中所包含的字符串描述符个数,按照其索引值,依次索要字符串描述符,索引值为0的是特殊字符串描述符(语音信息描述符)。
- Step 6 主机在进行完Step 5后,获取配置描述符的实际长度,依次获取配置描述符信息和配置描述符中所包含的其他信息(如接口描述符和端点描述符等)。
- Step 7 Step 1 \~ 6为标准USB枚举过程,只有当Step 1 \~ 6全部正确后,主机发出SetConfiguration的指令,来激活并使用设备的一个配置,此时设备才是真正意义上的可用状态。主机在设备配置完成后,会根据设备的标准描述符来将设备分成一个或多个虚拟设备。
- Step 8 Step 7完成后,主机上产生一个或多个虚拟设备,每个虚拟设备都有其类别标识,主机会根据其VID、PID和序列号查询对应的驱动,并安装驱动(如果主机上有备份,则直接使用,不再安装)。然后,主机根据驱动加载对应类专属描述信息。此处给出标准HID设备,主机自带驱动。
(2) USB枚举实例
下图(b)是Telink Dongle做鼠标设备时的枚举流程,图中的Step x与上图(a)中的Step x一一对应,Step 8为HID设备类专有的枚举流程,即获取报告描述符,有关报告描述符的结构可参照USB HID协议Universal Serial Bus (USB)-Device Class Definition for Human Interface Devices (HID)。

USB 类型总览
Telink 芯片根据支持的 USB 版本和速度模式,分为两类 USB 硬件模块:
- 全速USB (FS) :仅全速模式(12Mbps)。采用固定方向端点设计,使用 8+256 Bytes 专属 RAM。
- 高速USB (HS) :支持 USB 2.0 协议,同时支持高速模式(480Mbps)和全速模式(12Mbps)。采用双向端点设计,内置 DMA 引擎和 8KB FIFO。
全速USB (FS)
全速USB (Full Speed) 是 Telink 大多数芯片采用的 12Mbps USB 模式。
支持芯片见 芯片USB版本总览。
主要特点:
- 固定方向端点(IN/OUT 方向不可切换)
- TC 平台(B80/B80B/B85/B87/TC121X)端点0 固定 8 字节
- TL 平台(B91/B92/TL321X/TL721X/TL751x)端点0 可配置 8/16/32/64 字节
- 支持手动模式和自动模式
详细内容见 全速USB (FS)。
高速USB (HS)
高速USB (High Speed) 支持 480Mbps 高速模式,可向下兼容全速 12Mbps 模式。
主要特点:
- USB 2.0 协议,支持 High Speed 和 Full Speed
- Device-Only 工作模式
- 9 个双向端点(EP0-EP8),EP0 为专用控制端点
- 8KB 内部 FIFO,内置 DMA(Descriptor DMA)
- UTMI+ PHY 接口
- 支持 Control、Bulk、Interrupt、Isochronous 四种传输类型
详细内容见 高速USB (HS) — USB0。
芯片USB版本总览
| 芯片 | 速度模式 | 端点数量(EP0+EP1-EP8) | SRAM大小(EP0+EP1-EP8端点共享) |
|---|---|---|---|
| B80/B80B/B85/B87/TC121X/B91 | 全速 | 1+8 | 8+256 Bytes |
| B92 | 全速 | 1+8 | 8+1024 Bytes |
| TL321X/TL721X/TL751X | 全速 | 1+8 | 64+2048 Bytes |
| TL322X | 全速+高速 | 1+8 | 64 +8192 Bytes |
注意
- TC321X/ TL323X /TC122X 不支持USB硬件模块,无法使用USB功能。
全速USB (FS)
USB硬件介绍
Telink USB硬件模块内部固化了原始包和事务的处理,自动完成IN端点数据的保存和OUT端点数据的发送,将端点0的数据打包成标准用户数据包,这样不仅可以提高USB执行效率,而且最大程度上降低了USB开发的难度。
(1) SRAM 8+256
端点0既可输入又可输出,其余端点方向如下:
| 可配置端点类型 | 端点号 |
|---|---|
| 控制端点(既可输入又可输出) | 0 |
| 输出端点 | 5、6 |
| 输入端点 | 1、2、3、4、7和8 |
内存:8+256 字节 USB 专属 RAM。端点0 固定 8 字节,其余端点共用 256 字节。端点缓存大小为下一个端点的起始地址减去本端点的起始地址。
| 端点起始地址 | 含义 |
|---|---|
| 0x00 | Endpoint 1 起始地址 |
| 0x08 | Endpoint 2 起始地址 |
| 0x10 | Endpoint 3 起始地址 |
| 0x20 | Endpoint 6 起始地址 |
| 0xc0 | Endpoint 5 起始地址 |

注意
- 端点缓存最大值由max寄存器决定(可分配到所有缓存空间),默认为64 bytes。
- 适用芯片:B80/B80B/B85/B87/TC121X/B91(SRAM:8+256 Bytes)
(2) SRAM 8+1024
端点方向与 3.1.1 一致。较 3.1.1 新增以下特性:
- 端点地址通过
usbhw_set_ep_addr()设置,支持 16 位地址(通过reg_usb_ep_buf_addrh支持高 8 位地址) - 端点指针支持 16 位(通过
usbhw_get_ep_ptr()获取,reg_usb_ep_ptrh支持高 8 位)
适用芯片:B92(SRAM:端点0 固定 8 字节 + 共享 1024 字节)。
(3) SRAM 64+2048
端点方向与 3.1.1 一致。在 3.1.2 基础上新增:
端点0 包大小可配置(通过 usbhw_set_ctrl_ep_size() 设置为 8/16/32/64 字节)
适用芯片:TL321X/TL721X/TL751x(SRAM:端点0 64 字节 + 共享 2048 字节)。
中断
USB中断可分为三类,端点0中断、端点1-8中断和suspend/250us/reset 中断,如下表:
| 中断 | 产生条件 | 清除方式 |
|---|---|---|
| CTRL_EP_SETUP(IRQ7) | 端点0控制传输setup阶段 | 手动清status |
| CTRL_EP_DATA (IRQ8) | 端点0控制传输data阶段 | 手动清status |
| CTRL_EP_STATUS (IRQ9) | 端点0控制传输状态阶段 | 手动清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. 除同步端点:输出端点:host out事务,状态寄存器的相应位置1,产生中断,接收完回ACK。输入端点:数据填充完成后,配置ACK通知硬件,并产生中断,硬件在收到host in事务中,将数据发送给host。2. 同步端点:端点6,7可设置为同步端点,定时1ms产生中断。 | 手动清status |
| USB_IRQ_USB_SUSPEND (IRQ24) | USB总线空闲,例如拔掉USB接口,host休眠 | 手动清status |
| USB_IRQ_250us (IRQ34) | 250us定时中断 | 手动清status |
| USB_IRQ_RESET (IRQ35) | Host发送reset时序 | 手动清status |
注意
- Driver枚举过程的传输都是采用轮询的方式处理,没有使用中断方式。
自动和手动模式
Telink USB有两种模式,即自动模式和手动模式:
用户可通过设置端点0的配置寄存器,来控制是选择自动模式还是手动模式。端点0的配置寄存器默认为0xFF,即自动模式,此时与USB端点0相关的所有编解码均由Telink硬件自动驱动完成,Telink自带驱动为Print设备,使用端点8作为控制接口的端点,打印机数据是从端点0发送的;
手动模式需要用户修改EDP0CFG寄存器(下图),一般是将bit[7]和bit[5]设为0,即由用户去完成标准USB的枚举和使用用户定义的描述符。

USB软件基础
(1) USB运行流程
Telink USB的软件运行流程可以分为两个阶段,即初始化阶段和循环检测阶段,如下图所示。

初始化阶段主要完成USB的相关配置和使能USB。USB配置选项主要包括模式切换(自动模式和手动模式)、设置USB数据缓冲区和设置其他配置项;模式切换主要是将USB的工作模式切换为手动模式,此时相关的枚举过程和描述符由用户控制,设备会将用户准备好的枚举信息上报给主机;设置USB缓冲区是用户根据自己端点(端点0除外)的使用情况,分别给相关端点指定一段缓冲区(缓冲区总大小256Bytes,没有使用到的端点不用指定);设置其他配置是用来进行其他配置的操作,用户如果不想使用系统默认的配置项,可选择自行配置,如以中断形式传输数据等。
循环检测主要是不断检测数据接收和发送缓冲区是否有数据,如果有数据则进行相关操作,程序运行过程中会反复执行usb_handle_irq。端点0的操作流程可分为三个阶段,如下图所示,分别对应控制传输的SETUP阶段、DATA阶段和STATUS阶段,主要完成USB的识别和配置等相关操作,如USB的枚举,该过程是在main loop中完成,SETUP解析主机下发的指令,并根据主机的指令, 来准备相应的数据;DATA是将在数据阶段准备的数据发给主机或者接收主机发送的数据;STATUS是双方握手的过程。

(2) 数据接收与发送
A. 数据接收
Telink USB数据接收由硬件完成,硬件会将接收到的数据保存到RAM中,接收完成后硬件会产生一个中断来通知用户,用户只需要在检测到中断后,读取数据即可。数据检测和接收工作应该在usb.c中的usb_handle_irq函数中进行,结合下图,具体分析Telink USB数据接收的处理流程:
1) 用户需要检测相关的中断标识位(reg_usb_irq)是否置1,如果置1则进入数据接收阶段。
2) 一旦检测到数据后,用户需要将中断标识位清除,即reg_usb_irq = BIT((USB_EDP_CUSTUM_OUT & 0x07))。
3) 用户读取数据之前,需要使用reg_usb_ep_ptr(USB_EDP_CUSTUM_OUT)来获取接收到的数据长度。
4) 用户获取完数据长度后,即可通过反复读取usbhw_read_ep_data(USB_EDP_CUSTUM_OUT)来获取本次接收到的所有数据。
5) 用户接收完数据后,需要将调用usbhw_data_ep_ack(USB_EDP_CUSTUM_OUT)。(注意,这一步很重要,只有将OUT端点的ACK置起,硬件才会去接收主机下发到该端点的数据,并在接收完成后产生中断。)

B. 数据发送
Telink USB数据发送和数据接收一样,是由硬件完成,用户只需要将数据填充到相应的USB RAM中,并将数据ACK位置1。用户在填充数据之前,需要首先检测USB RAM中是否有待发送数据,如果有待发送数据,则需要等待发送完成之后再填充新的数据,否则会发生数据覆盖现象。
下图给出了Telink SDK中,发送数据的实例,下面结合该实例,详细分析USB数据发送流程:
1) 用户发送数据前,需要检测操作端点是否繁忙,如果繁忙(有待发送数据),则需要等待发送完成后,再填充数据。
2) 如果端点处于空闲状态,需要先重置端点计数器,即reg_usb_ep_ptr(USB_EDP_CUSTUM_CMISC_IN) = 0。
3) 重置完端点计数器后,用户就可向端点中填充数据。需要注意的时,reg_usb_ep_dat(USB_EDP_CUSTUM _CMISC_IN) = data[i]是将数据放到USB RAM中(硬件操作)。
4) 用户填充完数据后,需要调用reg_usb_ep_ctrl(USB_EDP_CUSTUM_CMISC_IN) = FLD_EP_DAT_ACK来通知硬件数据已经准备好了,硬件在收到该指令后,会在下一个主机索取数据的时候,将数据发送给主机。

USB Demo
USB应用主要介绍了USB标准设备类中的HID(Human Interface Device)设备,Audio设备,CDC(Communication Device Class)设备的简单应用,客户可根据需求自由组合。
HID类设备是USB设备中常用的设备类型,是直接与人交互的USB设备,如USB mouse,USB keyboard;
USB Audio类设备最为常见的是microphone和speaker;
USB的CDC类是USB通信设备类简称,虚拟串口设备是CDC类设备的一种类型。
(1) Demo配置
在头文件 app_config.h 中可以选择配置成不同的设备。
TC平台 (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
注意
- B80/B80B 不支持 USB_MICROPHONE 和 USB_SPEAKER。
TL平台 (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平台Demo额外支持:
USB_MIC_SPEAKER:麦克风和扬声器复合设备USB_PRINTER_ENABLE:打印机设备USB_SOMATIC_ENABLE:体感设备USB_CUSTOM_HID_REPORT:自定义HID报告USB_MASS_STORAGE_ENABLE:大容量存储设备(仅TL平台)
(2) USB Mouse
A. Mouse 处理流程
USB HID 设备是通过 report 来传输数据,一个报告描述符可以描述多个报告,不同的报告通过 ID 来识别,报告 ID 为报告的第一字节,没有规定报告 ID 时,报告没有 ID 字段,开始就是数据,详细的报告描述符资料可参考 USB HID 协议以及 HID 用途表(HID Usage Tables)。
首先 host 将 Telink USB 识别成 mouse 设备,需要经历枚举阶段,设备枚举成功后,进入数据收发阶段。根据 mouse 报告描述符的内容,报告 ID 为 USB_HID_MOUSE 的描述符中,有 4 个字节。第 1 字节的低 5 位表示按键是否按下,高 3 位为常数无用;第 2 字节为 X 轴的改变量;第 3 字节为 Y 轴的改变量;第 4 字节为滚轮的改变量。通过函数 usbmouse_hid_report(USB_HID_MOUSE, mouse, 4) 返回报告。
Demo 程序中,定义了数组 unsigned char mouse[4],其中:
mouse[0]:BIT(0) - left key; BIT(1) - right key; BIT(2) - middle key; BIT(3) - side key; BIT(4) - external key。对应的 bit 置 1,代表对应鼠标键按下mouse[1]:相对于 X 坐标的改变量mouse[2]:相对于 Y 坐标的改变量mouse[3]:滚轮的改变量
B. Mouse 测试
按下测试
Demo 程序中,报告 ID:USB_HID_MOUSE = 1,
数组 mouse 赋值:mouse[0] = BIT(1),mouse[1] = -2(补码),mouse[2] = 2,mouse[3] = 2。
将开发板上的引脚接地后再拔出,会执行函数 usbmouse_hid_report(USB_HID_MOUSE, mouse, 4)。
可以观测到桌面的鼠标右键按下,鼠标光标向左下移动,如下图所示,也可以从 USB 抓包工具 Input Report[1] 中看到:x:-2, Y:2, wheel:0, Btns=[2]。

释放测试
对另一个引脚进行同样操作,mouse 数组清零,按键释放。
(3) USB Keyboard
A. Keyboard 处理流程
根据 keyboard 报告描述符的内容,有输入和输出报告,其中输入报告规定了 8 个字节,第 1 个字节的 8 位表示特殊键是否按下:
- BYTE0:BIT(0) – 左 Ctl;BIT(1) – 左 Shift;BIT(2) – 左 Alt;BIT(3) – 左 GUI;BIT(4) – 右 Ctl;BIT(5) – 右 Shift;BIT(6) – 右 Alt;BIT(7) – 右 GUI
- BYTE1:保留值,都为 0
第 3 到 8 字节是普通键键值,当没有键按下时,全部 6 字节都为 0。这 6 个字节的第一字节值即为按键的键值,当有多个按键同时按下时,则同时返回这些按键值,键值在数组的先后顺序无关。具体键值请参考 HID 用途表文档,例如:0x59 对应数值键盘 1;0x5a 对应数值键盘 2;0x5b 对应数值键盘 3;0x39 对应大小写切换键。
B. Keyboard 测试
按下测试
Demo 程序中,定义数组 kb_data[6],赋值为 kb_data[0] = 0; kb_data[1] = 0; kb_data[2] = 0x59; kb_data[3] = 0x5a; kb_data[4] = 0x39; kb_data[5] = 0。
将开发板上的引脚接地后再拔出,会执行函数 usbkb_hid_report_normal(0x10, kb_data),其中参数 1 对应第一字节,参数 2 是对应 3\~8 字节的数组。
可以观测到在编辑窗口输入界面,会输入数字 1 和 2,右 Ctrl 键和大小写键按下。如下图,也可以从 USB 抓包工具 Input Report 中看到:Keys=[Rctrl 1 2 CapsLk]。

释放测试
对另一个引脚进行同样操作,特殊键和 kb_data 数组清零,按键释放。
(4) USB MIC
A. MIC 处理流程
以 AMIC 为例,USB microphone 设备是将 device 的 AMIC 数据通过 USB 传输到 host,需要保证整条数据通道采样率和通道数匹配。Demo 程序中,主要是将数据上传到 USB 部分,Mic 端点中断是 1ms 定时中断,也即 1ms 进一次中断。根据不同采样率,1ms 产生数据量不同,例如 16K 采样率收音,单声道数据,1 个 sample 为 2 bytes,1ms 数据为 32 bytes,将对应 audio buff 填入 USB SRAM 中。
TL平台差异: TL 平台 MIC Demo 支持双声道(MIC_CHANNEL_COUNT 2),TC 平台仅支持单声道(MIC_CHANNEL_COUNT 1)。
B. MIC Demo 测试
Audio 相关的测试,借助 Audacity 软件,如下图麦克风选择 Telink Audio16,扬声器选择 PC 扬声器。
Device Mic 收音,PC 扬声器播放,如录音的人声能不失真通过扬声器播放出来,说明 Mic 工作正常。

(5) USB Speaker
A. Speaker 处理流程
USB speaker 设备是将 host 的音频数据通过 USB 传输到 device,这个处理过程也在 1ms 中断完成的,读出 USB SRAM 数据长度,将对应的数据填充到 audio buff。
TL平台差异:TL 平台 Speaker Demo 支持双声道(SPK_CHANNEL_COUNT 2),TC 平台仅支持单声道(SPK_CHANNEL_COUNT 1)。
B. Speaker Demo 测试
在 Audacity 软件中,麦克风选择 PC 麦克风,扬声器选择 Telink Audio16。通过输出音频接口(3.5mm 耳机插口)。如录音的人声能不失真的通过耳机播放出来,说明 speaker 工作正常。

(6) USB CDC
CDC 设备有两个接口,CDC 控制接口和 CDC 数据接口,控制接口分配端点 2,作为中断输入端点传输。数据接口分配端点 5(out),端点 4(in),一定要先将端点 5 的 ACK 置起,端点才可以从 USB 主机接收数据。
A. CDC 处理流程
CDC 设备 USB 安装 host 首次识别成 CDC 设备需要手动安装 .inf 文件,如下图在 USB_Demo 路径下。
注意
- 只有 win7、win8 系统需要安装,win10 以上系统不需要安装。

数据接收 (host to device)
Demo 程序中,在函数 void usb_cdc_irq_data_process(void) 中 host 发送数据给 device,端点 5 产生中断后,函数 usb_cdc_rx_data_from_host(usb_cdc_data) 进行数据接收。
数据发送 (device to host)
在 main_loop 里当判断接收 buff 数据长度不为 0 时,通过函数 usb_cdc_tx_data_to_host() 将接收的数据发送给 host。
TL平台差异: TL 平台 CDC Demo 支持阻塞和非阻塞两种发送模式,通过 DEVICE_SEND_DATA_MODE 宏切换。非阻塞模式下,usb_cdc_tx_data_to_host_non_block() 函数填充数据后立即返回。TL 平台 CDC Demo 需要显式配置端点缓冲区大小和地址。

B. CDC Demo 测试
测试现象如下图所示,将串口助手发送的数据返回。

高速USB (HS)
高速USB硬件架构
(1) 概述
本章详细介绍高速USB 的硬件架构与驱动用法。
(2) USB0 特性
USB0 使用的 USB 2.0 Device 核具有以下特性:
| 特性 | 说明 |
|---|---|
| USB 标准 | USB 2.0,支持 High Speed 和 Full Speed |
| 工作模式 | Device-Only |
| 端点数量 | 9 个双向端点(EP0\~ EP8),EP0 为专用控制端点 |
| FIFO 总大小 | 8KB(8192 字节) |
| DMA 支持 | 内部 DMA(Descriptor DMA) |
| PHY 接口 | UTMI+ |
| 传输类型 | Control、Bulk、Interrupt、Isochronous |
(3) 系统框图
USB0 作为 CPU 的外部设备,通过 AHB 总线与系统互联,中断信号连接到中断控制器:
+-------------------+ +------------------+ +----------------+
| | AHB Bus | | UTMI+ | |
| CPU |<-------->| USB0 |<-------->| USB PHY |
| | | | | |
+-------------------+ +------------------+ +----------------+
| | |
| | IRQ_USB0 | DP/DM
| v v
| +-----------------------+ +----------------+
| | | | |
+--------------------->| Interrupt Controller | | USB Host |
| | | |
+-----------------------+ +----------------+
IO 引脚:
| 引脚 | 功能 | 说明 |
|---|---|---|
| PA3 | USB0_DM | USB D- 数据线 |
| PA4 | USB0_DP | USB D+ 数据线 |
PA3 和 PA4 只能用作 USB0_DM 和 USB0_DP 引脚或者 GPIO 功能,不能同时用作其他功能。
(4) 端点与 FIFO
A. 端点类型
USB0 支持 USB 2.0 规范定义的四种端点类型:
| 类型 | 枚举值 | 说明 |
|---|---|---|
| Control | USB0_EP_TYPE_CONTROL |
控制传输,仅 EP0 支持 |
| Isochronous | USB0_EP_TYPE_ISOCHRONOUS |
同步传输,用于音频/视频等实时数据 |
| Bulk | USB0_EP_TYPE_BULK |
批量传输,用于 CDC 等大数据量传输 |
| Interrupt | USB0_EP_TYPE_INTERRUPT |
中断传输,用于 HID 等小数据量周期性传输 |
B. FIFO 架构
USB0 内部 FIFO 总大小为 8KB,分为 RX FIFO 和 TX FIFO 两部分:
- RX FIFO :单一接收 FIFO,所有 OUT 端点共享。通过
usb0hw_set_grxfsiz()设置起始地址和大小。 - TX FIFO :每个 IN 端点拥有独立的发送 FIFO。通过
usb0hw_set_epin_size()为每个 IN 端点配置其 TX FIFO 的起始地址、大小和 FIFO 编号。
FIFO 地址分配示例(HID Mouse Demo):
+----------------------------+ 0x1FF (8KB)
| |
| EP2 TX FIFO (64x4 words) |
| |
+----------------------------+ 0x140
| |
| EP0 TX FIFO (64x4 words) |
| |
+----------------------------+ 0x100
| |
| RX FIFO (256x4 words) |
| |
+----------------------------+ 0x000
FIFO 大小以 word 为单位。实际可配置范围受 8KB 总 FIFO 大小的限制。
USB0 软件架构
(1) 分层架构
USB0 的软件栈采用分层架构设计,从上到下依次为:
+--------------------------------------------+
| USB Demo 层 |
| mouse, keyboard, cdc, audio |
+--------------------------------------------+
| USB Class 驱动层 |
| (usbd_hid, usbd_cdc, usbd_audio) |
+--------------------------------------------+
| USB 设备协议栈 |
| (标准设备请求、描述符、端点管理) |
+--------------------------------------------+
| 端口适配层 |
| (usbd_ep_open/write/read/stall + ISR) |
+--------------------------------------------+
| 驱动层 |
| (DMA 配置、FIFO 管理、中断处理) |
+--------------------------------------------+
| 硬件层(USB0 IP) |
| (DMA 引擎、PHY、端点控制) |
+--------------------------------------------+
(2) 驱动层
usb0hw.c 和 usb0hw.h 提供了 USB0 硬件的最底层操作接口,包括:
- 初始化与复位 :
usb0hw_init()、usb0hw_reset()、usb0hw_power_down() - 端点管理 :
usb0hw_ep_open()、usb0hw_ep_close() - 数据传输 :
usb0hw_write_ep_data()、usb0hw_read_ep_data() - FIFO 管理 :
usb0hw_set_grxfsiz()、usb0hw_set_epin_size()、usb0hw_flush_tx_fifo()、usb0hw_flush_rx_fifo() - 中断管理 :
usb0hw_get_gintsts()、usb0hw_clear_gintsts()、usb0hw_get_daint()、usb0hw_daintmsk_en()、usb0hw_daintmsk_dis() - 连接管理 :
usb0hw_soft_connect()、usb0hw_soft_disconnect() - 电源管理 :
usb0hw_pcgc_clk_en()、usb0hw_pcgc_clk_dis()、usb0hw_phy_pll_en()、usb0hw_phy_pll_dis()、usb0hw_remote_wakeup() - 状态查询 :
usb0hw_get_speed()、usb0hw_get_sof_fn()、usb0hw_get_timer_stamp() - 其他功能 :
usb0hw_set_address()、usb0hw_test_mode()、usb0hw_set_pwronprgdone()
(3) 端口适配层
端口适配层作为 USB0 硬件和 USB 设备协议栈之间的适配层,实现了以下功能:
- 端点操作适配 :将协议栈的
usbd_ep_open()、usbd_ep_write()、usbd_ep_read()、usbd_ep_stall()、usbd_ep_clear_stall()映射到usb0hw的对应函数 - SET_ADDRESS 适配 :
usbd_set_address()调用usb0hw_set_address() - TEST_MODE 适配 :
usbd_test_mode()调用usb0hw_test_mode() - 中断处理 :
usb0_irq_handler()通过PLIC_ISR_REGISTER注册为IRQ_USB0的中断服务程序,处理所有 USB0 全局中断
中断处理流程(usb0_irq_handler):
usb0_irq_handler()
|
+-- ENUMDONE → 速度检测完成,清除标志
+-- OEPINT → usb_irq_handler_epout()
| +-- XFERCOMPL → usbd_epout_complete_handler()
| +-- SETUP → usbd_control_request_process()
| +-- STSPHSERCVD → 状态阶段完成
+-- 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 设备协议栈
usbd_core.c 和 usbd_core.h 提供了一套轻量级 USB Device 协议栈,处理标准 USB 设备请求和描述符管理:
- 标准请求处理 :
usbd_control_request_process()处理 GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION、GET_STATUS 等标准设备请求 - 描述符管理 :通过
usbd_get_device_descriptor()、usbd_get_config_descriptor()、usbd_get_string_descriptor()等回调获取用户描述符 - 驱动注册 :
usbd_driver_register()注册 Class 驱动到指定接口 - 端点回调注册 :
usbd_endpoint_register()注册端点传输完成回调
(5) USB Class 驱动层
USB0 支持以下 USB Class 驱动:
| Class 驱动 | 说明 |
|---|---|
| HID | 人机接口设备,支持 Mouse/Keyboard/Mouse+Keyboard |
| CDC | 通信设备类,虚拟串口 |
| Audio | 音频设备类,支持 Speaker/Microphone |
Class 驱动通过 usbd_driver_register() 注册到协议栈,当主机发送 Class-Specific 请求时,协议栈会调用对应的 usbd_driver_handler 处理。
(6) 中断处理流程
USB0 使用单一中断线 IRQ_USB0,通过 PLIC_ISR_REGISTER 注册 usb0_irq_handler 为 ISR 入口。中断处理流程图如下:
USB0 中断触发
|
v
usb0_irq_handler()
|
读取 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
清除标志 EP OUT EP IN SOF回调 挂起回调 唤醒回调 复位检测 总线复位
处理 处理
USB0 初始化流程
(1) 硬件初始化
USB0 硬件初始化通过 usb0hw_init() 完成,内部执行以下操作:
- GPIO 配置 :将 PA3(USB0_DM)和 PA4(USB0_DP)设置为浮空模式,由 USB0 PHY 控制。
- 电源与时钟使能 :打开 USB 数字模块电源,使能复位和时钟
- DMA 与中断配置 :使能 AHB DMA(
GAHBCFG)、配置 IN/OUT 端点通用中断掩码(DIEPMSK、DOEPMSK)、使能 EP0 IN/OUT 中断(DAINTMSK) - 设备配置 :设置速度、描述符 DMA 模式、忽略帧号、TRDT 时序
- 全局中断使能 :使能 USB 复位、枚举完成、IN/OUT 端点、SOF、复位检测、唤醒等中断
- 软件连接 :通过
usb0hw_soft_connect()将设备连接到 USB 总线
(2) 设备枚举流程
USB 设备枚举流程由协议栈自动处理:
- 主机检测到设备连接(DP 上拉)
- 主机发送 USB 复位 → USBRST 中断 → usb0hw_reset() + usbd_bus_reset()
- 主机发送 GET_DESCRIPTOR(Device) → 协议栈返回设备描述符
- 主机发送 SET_ADDRESS → usbd_set_address() → usb0hw_set_address()
- 主机发送 GET_DESCRIPTOR(Configuration) → 协议栈返回配置描述符
- 主机发送 SET_CONFIGURATION → usbd_set_configuration_callback() 回调
(3) 描述符配置
USB 描述符由用户在各 Demo 的 *_descriptor.c 中定义,协议栈通过以下回调函数获取:
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) 设备类注册
通过 usbd_driver_register() 将 Class 驱动注册到指定接口。
该函数的参数说明如下:
bus:USB 总线编号(USB0 固定为 0)driver:驱动结构体指针,包含driver_num和usbd_driver_handlerintf:接口编号ep_addr:该接口使用的端点地址
A. 单设备注册
单设备注册,只有一个接口 0 和一个 Class-Specific 处理函数。例如 mouse 的注册流程如下:
usbd_driver_t usbd_mouse_driver;
usbd_mouse_driver.driver_num = 0;
usbd_mouse_driver.usbd_driver_handler = usbd_hid_interface_request_handler; /* hid 类处理函数 */
usbd_driver_register(0, &usbd_mouse_driver, 0, HID_MOUSE_IN_ENDPOINT_ADDRESS);
B. 复合类注册(同一个类)
同类复合设备注册,有多个接口和一个 Class-Specific 处理函数。例如 mouse + keyboard 的注册流程如下:
usbd_driver_t usbd_hid_driver;
usbd_hid_driver.driver_num = 0;
usbd_hid_driver.usbd_driver_handler = usbd_hid_interface_request_handler; /* hid 类处理函数 */
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. 复合类注册(不同类)
不同类复合设备注册,有多个接口和不同的 Class-Specific 处理函数。例如 mic + mouse 的注册流程如下:
usbd_driver_t usbd_mouse_driver;
usbd_mouse_driver.driver_num = 0;
usbd_mouse_driver.usbd_driver_handler = usbd_hid_interface_request_handler; /* hid 类处理函数 */
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) 端点注册与回调
通过 usbd_endpoint_register() 注册端点传输完成回调:
usbd_endpoint_register(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, usbd_hid_int_callback);
(6) 完整初始化示例
下面以 TL322x HID Mouse Demo 为例,展示完整的 USB0 初始化流程(其他芯片电压,时钟配置和睡眠唤醒会有差异,具体请参考代码):
void user_init(void)
{
// 1. GPIO 初始化
gpio_function_en(LED1);
gpio_output_en(LED1);
gpio_input_dis(LED1);
// 2. 注册 Class 驱动
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. 注册端点回调
usbd_endpoint_register(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, usbd_hid_int_callback);
// 4. 配置电压与时钟(USB0 要求数字电压 1.1V,HCLK 最低 48MHz)
pm_set_dig_ldo(DIG_VOL_1V1_MODE, 1000);
PLL_192M_D25F_96M_HCLK_N22_48M_PCLK_48M_MSPI_48M;
// 5. 初始化 USB0 硬件
#if USB_HIGH_SPEED_EN
usb0hw_init(USB0_SPEED_HIGH);
#else
usb0hw_init(USB0_SPEED_FULL);
#endif
// 6. 配置 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. 使能中断
core_interrupt_enable();
plic_interrupt_enable(IRQ_USB0);
// 8. 配置低功耗唤醒
pm_set_usb0_wakeup();
pm_set_suspend_power_cfg(FLD_PD_USB_EN, 1);
}
USB0 数据传输
(1) IN 传输(设备 → 主机)
通过 usbd_ep_write() 发送数据到主机:
usbd_ep_write(0, HID_MOUSE_IN_ENDPOINT_ADDRESS, hid_report_data, 5);
内部流程:
usbd_ep_write()→usb0hw_write_ep_data()- 填充 DMA 描述符(
tx_bytes、ioc、sp、l等字段) - 设置 DMA 地址并启动传输(
CNAK+EPENA) - DMA 传输完成后触发
DIEPINT_XFERCOMPL中断,需要注意的是,这里的完成中断是指 DMA 传输完成,即数据从 SRAM 搬运到 USB FIFO 完成,而不是端点数据传输完成。 - 中断处理调用
usbd_epin_complete_handler()→ 用户注册的回调函数
注意
- 对于非中断类型端点,当
len是 MPS 的整数倍时,驱动会自动设置sp(短包)标志,通知主机传输结束 - 对于中断类型端点,不会自动设置
sp标志
(2) OUT 传输(主机 → 设备)
通过 usbd_ep_read() 接收主机数据:
usbd_ep_read(0, CDC_DATA_OUT_ENDPOINT_ADDRESS, cdc_out_buffer, sizeof(cdc_out_buffer));
内部流程:
usbd_ep_read()→usb0hw_read_ep_data()- 填充 DMA 描述符(
rx_bytes、ioc、l等字段) - 设置 DMA 地址并启动传输(
CNAK+EPENA) - 传输完成后触发
DOEPINT_XFERCOMPL中断 - 中断处理调用
usbd_epout_complete_handler()→ 用户注册的回调函数
注意
usbd_ep_read()仅配置接收 DMA,实际数据接收在中断中完成- 实际接收长度通过
epout_callback中的len参数返回 len必须是相应端点 MPS 的整数倍
(3) 控制传输
EP0 的控制传输由协议栈自动处理:
- Setup 阶段 :硬件接收 Setup 包 →
DOEPINT_SETUP中断 →usbd_control_request_process()解析请求 - Data 阶段 :根据请求方向,协议栈自动调用
usbd_ep_write()或usbd_ep_read()传输数据 - Status 阶段 :
DOEPINT_STSPHSERCVD中断 → 完成标准请求处理
对于 Class-Specific 请求,协议栈会调用注册的 usbd_driver_handler 处理。
(4) 端点 Stall 管理
设置 Stall:
usbd_ep_stall(0, ep_addr);
内部根据端点方向调用 usb0hw_set_inep_stall() 或 usb0hw_set_outep_stall()。对于 EP0,设置 Stall 后会自动重新配置接收下一个 Setup 包。
清除 Stall:
usbd_ep_clear_stall(0, ep_addr);
内部根据端点方向调用 usb0hw_clear_epin_stall() 或 usb0hw_clear_epout_stall()。
USB0 电源管理
(1) 挂起与唤醒
USB0 支持 USB 挂起(Suspend)和唤醒(Resume)机制:
挂起回调:
void usbd_suspend_callback(unsigned char bus)
{
if(bus == 0) {
usb0hw_pcgc_clk_dis(); // 关闭 PCGC 时钟
usb0hw_phy_pll_dis(); // 关闭 PHY PLL
g_usb_suspend_flag = 1;
}
}
唤醒回调:
void usbd_resume_callback(unsigned char bus)
{
if(bus == 0) {
usb0hw_pcgc_clk_en(); // 使能 PCGC 时钟
usb0hw_phy_pll_en(); // 使能 PHY PLL
g_usb_suspend_flag = 0;
}
}
(2) 远程唤醒
设备可通过 usb0hw_remote_wakeup() 主动唤醒主机:
void usb0hw_remote_wakeup(void)
{
if (!(reg_usb_dsts & FLD_USB_DSTS_SUSPSTS)) {
return; // 不在挂起状态,直接返回
}
usb0hw_pcgc_clk_en();
usb0hw_phy_pll_en();
BM_SET(reg_usb_dctl, FLD_USB_DCTL_RMTWKUPSIG); // 触发远程唤醒信号
delay_ms(10);
BM_CLR(reg_usb_dctl, FLD_USB_DCTL_RMTWKUPSIG);
}
(3) 低功耗模式配置
在挂起状态下,系统可以进入低功耗模式,下面以 TL322X 为例,展示低功耗模式配置(其他芯片睡眠唤醒配置会有差异,具体请参考代码):
// 初始化时配置唤醒源
pm_set_usb0_wakeup();
pm_set_suspend_power_cfg(FLD_PD_USB_EN, 1);
// 主循环中进入低功耗
void main_loop(void)
{
if (g_usb_suspend_flag) {
pm_sleep_wakeup(SUSPEND_MODE, PM_WAKEUP_CORE, PM_TICK_STIMER, 0);
}
}
USB0 测试模式
USB0 支持 USB 2.0 规范定义的测试模式,用于电气特性测试和合规性测试:
| 测试模式 | 枚举值 | 说明 |
|---|---|---|
| Disable | USB0_TEST_MODE_DISABLE |
关闭测试模式 |
| Test_J | USB0_TEST_J_MODE |
测试 J 状态 |
| Test_K | USB0_TEST_K |
测试 K 状态 |
| Test_SE0_NAK | USB0_TEST_SE0_NAK_MODE |
测试 SE0 NAK 状态 |
| Test_Packet | USB0_TEST_PACKET_MODE |
测试包模式 |
| Test_Force_Enable | USB0_TEST_FORCE_ENABLE_MODE |
强制使能 |
通过 usbd_test_mode() 进入测试模式:
void usbd_test_mode(unsigned char bus, unsigned char test_mode)
{
delay_ms(1); // 等待状态阶段完成
usb0hw_test_mode(test_mode);
}
API 参考
(1) 驱动层 API(usb0hw)
| API | 功能 |
|---|---|
usb0hw_init(speed_sel) |
初始化 USB0,配置速度、DMA、中断 |
usb0hw_power_down() |
关闭 USB0 电源和时钟 |
usb0hw_reset() |
复位 USB0(清除地址、FIFO、NAK 默认端点) |
usb0hw_ep_open(ep_num, ep_dir, ep_type, ep_mps) |
打开指定端点,配置类型和最大包大小 |
usb0hw_ep_close(ep_num, ep_dir) |
关闭指定端点,禁用并清除配置 |
usb0hw_write_ep_data(ep_num, buf, len) |
通过 DMA 发送数据(IN 端点) |
usb0hw_read_ep_data(ep_num, buf, len) |
配置 DMA 接收数据(OUT 端点) |
usb0hw_get_epin_len(ep_num) |
获取 IN 端点已传输的数据长度 |
usb0hw_get_epout_len(ep_num) |
获取 OUT 端点已接收的数据长度 |
usb0hw_set_grxfsiz(size) |
设置 RX FIFO 大小(32-bit word 单位) |
usb0hw_set_epin_size(ep_num, addr, size) |
设置 IN 端点 TX FIFO 起始地址和大小 |
usb0hw_set_epin_fifo(ep_num, fifo_num) |
设置 IN 端点使用的 TX FIFO 编号 |
usb0hw_flush_tx_fifo(ep_num) |
刷新 TX FIFO(0x10 刷新所有 TX FIFO) |
usb0hw_flush_rx_fifo() |
刷新 RX FIFO |
usb0hw_set_address(dev_addr) |
设置设备地址 |
usb0hw_get_speed() |
获取当前枚举速度(High/Full) |
usb0hw_get_sof_fn() |
获取 SOF 帧号 |
usb0hw_get_timer_stamp() |
获取时间戳 |
usb0hw_soft_connect() |
软件连接(上拉 DP) |
usb0hw_soft_disconnect() |
软件断开(下拉 DP) |
usb0hw_remote_wakeup() |
触发远程唤醒信号 |
usb0hw_test_mode(mode) |
设置测试模式 |
usb0hw_set_pwronprgdone() |
设置 Power-On Program Done |
usb0hw_get_gintsts() |
获取全局中断状态 |
usb0hw_clear_gintsts(status) |
清除全局中断状态 |
usb0hw_get_daint() |
获取设备所有端点中断状态 |
usb0hw_daintmsk_en(mask) |
使能设备端点中断掩码 |
usb0hw_daintmsk_dis(mask) |
禁用设备端点中断掩码 |
usb0hw_get_doepint(ep_num) |
获取 OUT 端点中断状态 |
usb0hw_clear_doepint(ep_num, status) |
清除 OUT 端点中断状态 |
usb0hw_get_diepint(ep_num) |
获取 IN 端点中断状态 |
usb0hw_clear_diepint(ep_num, status) |
清除 IN 端点中断状态 |
usb0hw_set_inep_stall(ep_num) |
设置 IN 端点 STALL |
usb0hw_set_outep_stall(ep_num) |
设置 OUT 端点 STALL |
usb0hw_clear_epin_stall(ep_num) |
清除 IN 端点 STALL |
usb0hw_clear_epout_stall(ep_num) |
清除 OUT 端点 STALL |
usb0hw_pcgc_clk_en() |
使能 PCGC 时钟 |
usb0hw_pcgc_clk_dis() |
关闭 PCGC 时钟 |
usb0hw_phy_pll_en() |
使能 PHY PLL |
usb0hw_phy_pll_dis() |
关闭 PHY PLL |
(2) 端口适配层 API
| API | 功能 |
|---|---|
usbd_ep_open(bus, endpoint_desc) |
打开端点(适配 usb0hw_ep_open) |
usbd_ep_write(bus, ep_addr, buf, len) |
发送数据(适配 usb0hw_write_ep_data) |
usbd_ep_read(bus, ep_addr, buf, len) |
接收数据(适配 usb0hw_read_ep_data) |
usbd_ep_stall(bus, ep_addr) |
设置端点 STALL |
usbd_ep_clear_stall(bus, ep_addr) |
清除端点 STALL |
usbd_set_address(bus, address) |
设置设备地址 |
usbd_test_mode(bus, test_mode) |
进入测试模式 |
usb0_irq_handler() |
USB0 全局中断服务程序(ISR) |
(3) USB 设备协议栈 API(usbd_core)
| API | 功能 |
|---|---|
usbd_driver_register(bus, driver, intf, ep_addr) |
注册 Class 驱动到指定接口 |
usbd_endpoint_register(bus, ep_addr, callback) |
注册端点传输完成回调 |
usbd_control_request_process(bus, setup, setup_stage) |
处理控制请求 |
usbd_bus_reset(bus) |
总线复位处理 |
usbd_epin_complete_handler(bus, ep_index, len) |
IN 端点传输完成处理(回调用户注册函数) |
usbd_epout_complete_handler(bus, ep_index, len) |
OUT 端点传输完成处理(回调用户注册函数) |
usbd_set_configuration_callback(bus, config_num) |
SET_CONFIGURATION 回调(weak,用户可重写) |
usbd_set_interface_callback(bus, intf, alt_intf) |
SET_INTERFACE 回调(weak,用户可重写) |
(4) USB Class 驱动 API
HID Class:
| API | 功能 |
|---|---|
usbd_hid_interface_request_handler(bus, setup) |
HID Class-Specific 请求处理(GET/SET_REPORT 等) |
CDC Class:
| API | 功能 |
|---|---|
usbd_cdc_interface_request_handler(bus, setup) |
CDC Class-Specific 请求处理 |
Audio Class:
| API | 功能 |
|---|---|
usbd_audio_interface_request_handler(bus, setup) |
Audio Class-Specific 请求处理 |
(5) 弱定义回调函数(用户可重写)
| 函数 | 说明 |
|---|---|
usbd_suspend_callback(bus) |
USB 挂起回调 |
usbd_resume_callback(bus) |
USB 唤醒回调 |
usbd_resetdet_callback(bus) |
USB 复位检测回调 |
usbd_reset_callback(bus) |
USB 总线复位回调 |
usbd_sof_callback(bus) |
SOF 帧起始回调 |
usbd_set_configuration_callback(bus, config_num) |
SET_CONFIGURATION 完成回调 |
usbd_set_interface_callback(bus, intf, alt_intf) |
SET_INTERFACE 完成回调 |
Demo 参考
完整的 USB0 示例代码位于 USB0_Demo 目录中,通过 USB_DEMO_TYPE 宏切换不同演示场景。
(1) HID Mouse Demo
HID Mouse Demo 演示了一个标准的 USB HID 鼠标设备,支持自动画方块功能。
关键文件:
| 文件 | 说明 |
|---|---|
hid_mouse_app.c |
应用层代码 |
hid_mouse_descriptor.c |
设备/配置/字符串/HID 报告描述符 |
hid_mouse_descriptor.h |
描述符宏定义(VID/PID/端点地址等) |
描述符配置:
#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 定义是否启用高速模式。如需全速模式,将
USB_HIGH_SPEED_EN定义为 0。 - ID_VENDOR、ID_PRODUCT、ID_VERSION 定义设备的 VID/PID/版本号。
- STRING_VENDOR、STRING_PRODUCT、STRING_SERIAL 定义设备的供应商名、产品名和序列号。
- HID_MOUSE_IN_ENDPOINT_ADDRESS、HID_MOUDE_IN_ENDPOINT_SIZE、HID_MOUSE_IN_ENDPOINT_INTERVAL 定义鼠标 IN 端点的地址、最大包大小和间隔。
驱动注册:
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);
Mouse 驱动注册到 driver_num 0,鼠标 IN 端点地址为 HID_MOUSE_IN_ENDPOINT_ADDRESS。
FIFO 配置:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(USB0_EP2, 0x100 + 64, 64);
EP2 IN 需要分配独立的 TX FIFO,起始地址在 EP0 之后
HID 数据传输:
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 定义是否启用自动画方块功能。默认开启。如需关闭,将 USB_MOUSE_DRAW_SQUARE 定义为 0。如果需要启动画线功能,将 g_send_flag 定义为 1 即可。
(2) HID Keyboard Demo
HID Keyboard Demo 演示了标准 USB HID 键盘设备,支持自动发送按键。
关键文件:
| 文件 | 说明 |
|---|---|
hid_keyboard_app.c |
应用层代码 |
hid_keyboard_descriptor.c |
键盘描述符 |
hid_keyboard_descriptor.h |
描述符宏定义(VID/PID/端点地址等) |
描述符配置:
#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 定义是否启用高速模式。如需全速模式,将
USB_HIGH_SPEED_EN定义为 0。 - STRING_VENDOR、STRING_PRODUCT、STRING_SERIAL 定义设备的供应商名、产品名和序列号。
- HID_KEYBOARD_IN_ENDPOINT_ADDRESS、HID_KEYBOARD_IN_ENDPOINT_SIZE、HID_KEYBOARD_IN_ENDPOINT_INTERVAL 定义键盘 IN 端点的地址、最大包大小和间隔。
驱动注册:
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 配置:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(USB0_EP1, 0x100 + 64, 64);
- EP1 IN 需要分配独立的 TX FIFO,起始地址在 EP0 之后
HID 数据传输:
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
HID Mouse + Keyboard Composite Demo 演示了复合设备,同时包含鼠标(接口 0)和键盘(接口 1)两个 HID 接口。
关键文件:
| 文件 | 说明 |
|---|---|
hid_mouse_keyboard_app.c |
应用层代码 |
hid_mouse_keyboard_descriptor.c |
复合设备描述符 |
hid_mouse_keyboard_descriptor.h |
描述符宏定义 |
描述符配置:
#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
键盘使用 EP1(0x81),鼠标使用 EP2(0x82),两个端点使用不同的 IN 地址。
驱动注册:
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);
同一个 usbd_hid_driver 分别注册到接口 0(键盘)和接口 1(鼠标),每个端点注册独立的回调函数。
FIFO 配置:
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);
复合设备需要为每个 IN 端点(EP0、EP1、EP2)分别配置 TX FIFO,地址依次递增。
(4) CDC Demo
CDC Demo 演示了 USB 通信设备类(虚拟串口),支持数据回显(Echo)和批量传输。设备包含一个 CDC 控制接口和一个 CDC 数据接口。
关键文件:
| 文件 | 说明 |
|---|---|
cdc_app_app.c |
应用层代码 |
cdc_descriptor.c |
CDC 描述符(IAD + CDC ACM + Data Interface) |
cdc_descriptor.h |
描述符宏定义 |
描述符配置:
#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 设备有三个端点:通知端点(EP2 IN,
0x82,Interrupt 类型)、数据 IN 端点(EP4 IN,0x84,Bulk 类型)、数据 OUT 端点(EP5 OUT,0x05,Bulk 类型)。 - High Speed 模式下 Bulk 端点最大包大小为 512 字节(
0x200),Full Speed 下为 64 字节(0x40)。
驱动注册:
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);
- CDC 驱动分别注册到接口 0(控制接口 + 通知端点)和接口 1(数据接口 + IN/OUT 端点)。
- 每个端点均独立注册回调函数。
FIFO 配置:
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 需要分配独立的 TX FIFO,起始地址在 EP0 之后。
- CDC_DATA_IN_ENDPOINT_ADDRESS 需要分配独立的 TX FIFO,起始地址在通知端点之后。
SET_CONFIGURATION 回调与数据回显:
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));
}
}
- 在
usbd_set_configuration_callback中启动首次 OUT 接收。 main_loop中实现回显逻辑:收到数据后通过 IN 端点发回,然后继续接收下一包。
(5) Audio Speaker Demo
Audio Speaker Demo 演示了 USB Audio 1.0 扬声器设备,支持 16KHz 16-bit 立体声输出。设备包含一个 Audio Control 接口(接口 0,无端点)和一个 Audio Streaming 接口(接口 1,一个 ISO OUT 端点)。
关键文件:
| 文件 | 说明 |
|---|---|
audio_spk_app.c |
应用层代码 |
audio_spk_descriptor.c |
Audio 描述符(IAD + Audio Control + Audio Streaming) |
audio_spk_descriptor.h |
描述符宏定义 |
描述符配置:
#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
- Audio Streaming 接口使用 EP6 OUT(
0x06),Isochronous 类型,最大包大小 64 字节。 - 音频数据包大小由采样率和通道数决定:
AUDIO_OUT_PACKET = (16000 * 2 * 2) / 1000 = 64字节/ms。
驱动注册:
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);
Audio Control 接口(接口 0)仅处理 Class-Specific 控制请求(静音/音量),ep_addr 参数传 0xff 表示无端点。
FIFO 配置:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
OUT 端点共用所有 RX FIFO。
Audio 数据接收:
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));
}
- 在
usbd_audio_open回调中启动首次 OUT 接收。 - 每次接收完成后通过
usbd_audio_epout_callback继续接收下一包,形成持续的音频数据流。
Feature Unit 控制:
unsigned char usbd_audio_interface_cb(unsigned char bus, usb_control_request_t const *setup)
{
// 处理 MUTE 和 VOLUME 控制请求(CUR/MIN/MAX/RES)
}
通过 usbd_audio_interface_cb 处理主机的 Feature Unit 请求,包括静音控制和音量控制(读取 CUR/MIN/MAX/RES,设置 CUR)。
(6) Audio Microphone Demo
Audio Microphone Demo 演示了 USB Audio 1.0 麦克风设备,支持 16KHz 16-bit 立体声输入。设备包含一个 Audio Control 接口(接口 0,无端点)和一个 Audio Streaming 接口(接口 1,一个 ISO IN 端点)。
关键文件:
| 文件 | 说明 |
|---|---|
audio_mic_app.c |
应用层代码 |
audio_mic_descriptor.c |
Audio 麦克风描述符 |
audio_mic_descriptor.h |
描述符宏定义 |
描述符配置:
#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
Audio Streaming 接口使用 EP7 IN(0x87),Isochronous 类型,最大包大小 64 字节。
驱动注册:
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);
Audio Control 接口(接口 0)仅处理 Class-Specific 控制请求。
FIFO 配置:
usb0hw_set_grxfsiz(0x100);
usb0hw_set_epin_size(USB0_EP0, 0x100, 64);
usb0hw_set_epin_size(USB0_EP7, 0x100 + 64, 64);
EP7 IN 需要分配独立的 TX FIFO,起始地址在 EP0 之后。
Audio 数据发送:
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));
}
- 在
usbd_audio_open回调中启动首次 IN 发送。 - 每次发送完成后通过
usbd_audio_epin_callback填充新的音频数据并继续发送,形成持续的音频数据流。
芯片差异 — TL322x
TL322x USB0 的时钟来源于 HCLK,并且不能小于 48 MHz。
数字 LDO 的电压必须为 1.1V。
所以 demo 代码中需要设置数字 LDO 电压为 1.1V。如果 HCLK 小于 48MHz,那么需要将 HCLK 设置为 48MHz 或者更高的频率。代码如下所示
pm_set_dig_ldo(DIG_VOL_1V1_MODE, 1000); /* 设置数字 LDO 电压为 1.1V */
PLL_192M_D25F_96M_HCLK_N22_48M_PCLK_48M_MSPI_48M; /* 设置 HCLK 为 48MHz */
AES
概述
Telink 芯片提供硬件对称加密加速。实现方式有两种,不同芯片支持的加密模块如下表:
| 芯片 | 加密模块 |
|---|---|
| TLSR820x/TLSR8373 / TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC122x / TC123x / TLSR921x/TLSR951x / TLSR922x/TLSR952x | 独立的AES |
| TL321x / TL721x / TL322x / TL323x / TL751x | SKE模块中的AES |
独立的AES
- 原理
硬件 AES-128 模块,使用 16 字节密钥处理 16 字节数据块,每次调用完成一个块的加解密,默认为 ECB 模式。
- API
加密和解密使用两个API,分别为aes_encrypt和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)
- 示例使用
基本加解密:
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};
// 加密(返回 是否成功)
aes_encrypt(key, plaintext, ciphertext);
// 解密(返回 是否成功)
aes_decrypt(key, ciphertext, plaintext);
SKE模块中的AES
- 原理
AES位于SKE(Symmetric Key Engine)模块中,是独立 AES 模块的升级替代,支持 AES-128/192/256等多种算法,以及直接支持 ECB/CBC/CTR 等多种工作模式。
- API
加密和解密都使用同一个API,通过参数crypto分别指定。
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);
- 数据类型
SKE_ALG — AES算法模式:
| 枚举值 | 描述 |
|---|---|
| SKE_ALG_AES_128 | AES 128 bits key |
| SKE_ALG_AES_192 | AES 192 bits key |
| SKE_ALG_AES_256 | AES 256 bits key |
SKE_MODE — AES算法运行模式:
| 枚举值 | 描述 |
|---|---|
| SKE_MODE_ECB | ECB Mode |
| SKE_MODE_CBC | CBC Mode |
| SKE_MODE_CTR | CTR Mode |
SKE_CRYPTO — AES算法运行方向:
| 枚举值 | 描述 |
|---|---|
| SKE_CRYPTO_ENCRYPT | encrypt |
| SKE_CRYPTO_DECRYPT | decrypt |
- 示例使用
基本加解密:
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};
// 加密(返回 是否成功)
ske_lp_crypto(SKE_ALG_AES_128, SKE_MODE_ECB, SKE_CRYPTO_ENCRYPT, std_key, 0, NULL, std_in, cipher, 48);;
// 解密(返回 是否成功)
ske_lp_crypto(SKE_ALG_AES_128, SKE_MODE_ECB, SKE_CRYPTO_DECRYPT, std_key, 0, NULL, cipher, replain, 48);
说明
SKE需额外指定算法(SKE_ALG_AES_128)、模式(SKE_MODE_ECB)、加解密方向(SKE_CRYPTO_ENCRYPT/SKE_CRYPTO_DECRYPT)等。
Coremark和Dhrystone
简介
处理器的两个重要衡量指标是功耗和性能。嵌入式处理器领域中最知名和最常用的基准测试是 Dhrystone 和 CoreMark,两者均用于衡量整数运算性能:
- Dhrystone:单位 DMIPS/MHz,经典整数性能基准。
- CoreMark:单位 CoreMark/MHz,EEMBC推出的工业标准基准,旨在替代Dhrystone。
两个Demo共用相同的工程编译配置:使用 cstartup_flash.S + flash_boot_ramcode.link,除了vector table以外全部代码在RAM中执行,避免Flash延迟影响跑分结果。受IRAM大小限制,代码量较大的工程不能使用此配置。S/link文件必须在工程中配套使用,不可混用,详情请参考章节Software Startup。
支持的芯片
| 芯片系列 | 芯片型号 | Dhrystone | CoreMark |
|---|---|---|---|
| TC 系列 | TC321x | √ | √ |
| TL 系列 | TLSR921x/TLSR951x | √ | √ |
| TL 系列 | TLSR922x/TLSR952x | √ | √ |
| TL 系列 | TL321x | √ | √ |
| TL 系列 | TL721x | √ | √ |
| TL 系列 | TL322x | √ | √ |
| TL 系列 | TL323x | √ | √ |
| TL 系列 | TL751x | √ | √ |
说明
以上芯片均支持 Dhrystone 和 CoreMark 两种测试,未列出的芯片不支持。
快速上手
如果是第一次使用,按照以下步骤可以快速完成 Dhrystone 或 CoreMark 跑分:
-
确认芯片支持 — 查看支持的芯片 表格,确认芯片型号在列表中。
-
打开 Demo 工程 — Dhrystone 位于
demo/vendor/Dhrystone_Demo/,CoreMark 位于demo/vendor/Coremark_demo/。详情请参考章节Dhrystone Demo使用 和 CoreMark Demo使用。 -
配置 HAS_FLOAT 宏(仅CoreMark)— 根据芯片是否支持浮点,在
core_portme.h中设置HAS_FLOAT。详情请参考章节CoreMark配置参数。 -
编译并烧录 — 编译选项已在工程文件中预配置,直接编译烧录即可。
-
查看结果 — 通过 USB 串口查看跑分结果。详情请参考结果解读。
提示
CoreMark 测试运行时间约 10 ~ 20 秒,请耐心等待。多核芯片的 N22 核需要选择对应的工程配置。
Dhrystone
Dhrystone概念说明
Dhrystone 标准测试方法很简单,即单位时间内运行 Dhrystone 程序的次数,其度量单位为 DMIPS/MHz。MIPS 代表每秒百万条指令(Million Instructions Per Second),即每秒处理的百万机器语言指令数。DMIPS 中的 D 是 Dhrystone 的缩写,表示在 Dhrystone 标准测试方法下的 MIPS,主要用于衡量整数运算能力。
关于DMIPS有一个需要注意的点,因为历史原因,把VAX-11/780机器上的测试结果1757 Dhrystones/s定义为1 DMIPS,在其他平台测试到的每秒Dhrystones数应除以1757,才是真正的DMIPS数值,故DMIPS其实表示的是一个相对值。
Dhrystone源码地址:http://www.roylongbottom.org.uk/classic_benchmarks.tar.gz
Dhrystone算法说明
Dhrystone 程序由 Reinhold P. Weicker 在1984年提出,主要测试处理器的整数运算性能,包含以下类型的操作:
- 字符串操作(赋值、比较)
- 整数运算(算术运算、比较)
- 枚举类型操作
- 结构体赋值
- 控制流(循环、条件分支)
- 函数调用(包括带参数的函数调用)
- 指针操作
Dhrystone Demo使用
Demo 位于 demo/vendor/Dhrystone_Demo/,Dhrystone 的测试逻辑在 app.c 的 user_init() 中完成。
运行流程:
- 初始化时钟(默认使用
CLOCK_INIT宏) - 调用
dhry_main()执行 Dhrystone 基准测试 - 通过 USB / 串口输出测试结果
Dhrystone_DMIPS_Per_MHz
基础示例(大多数芯片通用):
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结果解读
- TL系列:输出浮点格式(如
1.98),单位 DMIPS/MHz; - TC系列:输出整型格式(值放大了1000倍,如
1980表示1.98 DMIPS/MHz),需除以1000得到实际值; Dhrystone_DMIPS_Per_MHz为每MHz值,可用于不同处理器之间的性能对比。
CoreMark
CoreMark概念说明
CoreMark是由EEMBC组织的Shay Gla-On于2009年提出,试图将其发展成为工业标准,从而代替陈旧的Dhrystone标准。
- CoreMark官网地址:https://www.eembc.org/coremark/index.php
- Github源代码:https://github.com/eembc/coremark
与 Dhrystone 类似,标准 CoreMark 测试方法是在一定配置参数组合下单位时间内运行 CoreMark 程序的次数,度量单位为 CoreMark/MHz。CoreMark 数值越高,表示性能越好。
CoreMark算法说明
CoreMark 程序用C语言编写,包含以下四种类型的算法:
- 数学矩阵运算(常规矩阵运算):测试CPU的整数数学运算能力
- 枚举(查找和排序):测试CPU的查找和排序处理能力
- 状态机(用于判断输入流是否包含有效数字):测试CPU的控制流处理能力
- CRC(循环冗余校验):测试CPU的校验计算能力
CoreMark Demo使用
Demo 位于 demo/vendor/Coremark_demo/。CoreMark 的初始化与测试逻辑主要在 main.c 中完成,app.c 中的 user_init() 仅包含 LED 初始化等辅助代码,用户可直接编译烧录无需修改。多核芯片的 N22 核可独立运行 CoreMark Demo,需选择 N22 核对应的工程配置。
运行流程:
- 初始化平台和时钟(
PLATFORM_INIT+CLOCK_INIT) - 获取CPU主频(SDK内部自动处理)
- 调用
main_coremark()执行CoreMark基准测试(运行时间约10~20秒) - 通过USB串口输出测试结果
int main(void)
{
PLATFORM_INIT;
CLOCK_INIT;
user_init(); // 仅初始化 LED 等外设,CoreMark 逻辑不在此函数中
printf("\r\n\r\n Core Mark Starts(wait about 10s~20s...) ...\r\n");
delay_ms(100);
cpu_mhz = sys_clk.cclk; // D25 核直接从结构体获取;N22 核通过 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配置参数
core_portme.h 中 HAS_FLOAT 宏需要根据芯片浮点支持情况配置:
| 宏定义 | TL系列默认 | TC系列默认 | 说明 |
|---|---|---|---|
HAS_FLOAT |
0 | 1 | 浮点型芯片(-mabi=ilp32f)设为1,非浮点型设为0 |
其余编译选项(-mcpu、-mabi、优化级别、S/link文件等)已在各芯片工程文件(.cproject)中预先配置,无需手动修改。
CoreMark结果解读
结果输出格式与 HAS_FLOAT 宏配置相关:
- TL系列(默认
HAS_FLOAT=0):coremark_result为int类型,输出整型格式(值放大了1000倍),coremark_result/clk(Mhz)需除以1000得到实际CoreMark/MHz值; - TC系列(默认
HAS_FLOAT=1):coremark_result为float类型,输出浮点格式,直接读取即可; coremark_result/clk(MHz)为每MHz值,可用于不同处理器之间的性能对比。
常见问题排查
编译与配置类
Q:编译时提示 HAS_FLOAT 相关错误?
A:检查 core_portme.h 中的 HAS_FLOAT 宏是否与芯片浮点支持匹配:工程文件中 -mabi=ilp32f 则设为 1,-mabi=ilp32 则设为 0。详情请参考章节CoreMark配置参数。
结果解读类
Q:Dhrystone 结果输出 1980 而不是 1.98,正常吗?
A:正常。TC 系列芯片输出的是放大 1000 倍的整型值,将输出值除以 1000 即为实际 DMIPS/MHz 值。TL 系列输出浮点格式,直接读取即可。详情请参考结果解读。
Q:CoreMark 跑分比预期低很多?
A:
-
确认
HAS_FLOAT宏配置是否正确。 -
N22 核开启 Instruction prefetch 后跑分会降低,这是正常现象。
-
确认链接文件是否正确(使用
flash_boot_ramcode.link)。
Q:Dhrystone 和 CoreMark 的结果单位分别是什么?
A:Dhrystone 单位为 DMIPS/MHz,CoreMark 单位为 CoreMark/MHz。两者都是每 MHz 的值,方便不同频率的处理器横向对比。
运行异常类
Q:CoreMark 测试运行超过 1 分钟还没结果?
A:CoreMark 正常运行时间约 10 ~ 20 秒。如果超时,检查串口是否正确连接,或尝试复位芯片重新运行。
Q:N22 核跑分比 D25 核低很多?
A:N22 核性能与 D25 核不同,跑分差异属于正常现象。具体参考数据请查阅对应芯片 Datasheet。
提示
如以上方案不能解决问题,请确认 Demo 工程是否使用最新的 SDK 版本。
数字Keyscan
概述
数字 Keyscan 是 Telink SoC 内置的硬件矩阵键盘扫描模块,用于检测行列式矩阵键盘的按键事件。
芯片支持:
- TL322x
- TC321x
- TLSR8208/TLSR8373
两种工作模式对比
| 对比项 | 硬件消抖模式 | DMA 模式 |
|---|---|---|
| 时钟源 | 32K 低速时钟 | 24M 高速时钟 |
| 扫描触发 | 有按键按下时启动扫描,无按键后自动停止 | 持续扫描,不停歇 |
| 消抖方式 | 硬件自动完成 | 软件自行实现 |
| 上报数据 | 有效的按键键值(行号+列号) | 8×32bit 原始电平位图 |
| 检测事件 | 按下事件(释放通过停止扫描推断) | 按下和释放均可检测 |
| 典型扫描周期 | 2ms(扫完 8 行×31 列一遍) | 62.5μs(单轮) |
| 低功耗支持 | 支持(无按键时模块停止,可进入睡眠) | 不支持(模块持续运行) |
| 适用场景 | 电池供电、普通键盘、功耗敏感设备 | 电竞键盘、全键无冲、低延迟场景 |
选型建议:
- 若产品对功耗敏感(如无线键盘、遥控器),优先选择硬件消抖模式
- 若需要检测按键释放事件、或要求极低的按键延迟,选择 DMA 模式
矩阵规格限制
| 参数 | 硬件消抖模式 | DMA 模式 |
|---|---|---|
| 最大行数 | 8 | 8 |
| 最大列数 | 18 | 除行之外的剩余的io个数 |
| 行+列总数 | ≤ 32(受 GPIO 编号限制) | ≤ 32(受 GPIO 编号限制) |
| 最多按键数 | 单行最大按键15个,最多31个 | 无限制,任意按键 |
注意
- 行和列使用的引脚必须从
ks_value_e枚举中选取,且所有引脚的 GPIO 编号(0~31)不能重复。即行占用的 GPIO 编号和列占用的 GPIO 编号合起来不能超过 32 个。
使用示例
硬件消抖模式
Step 1:定义行列引脚数组
#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:初始化并启动
void user_init(void)
{
// 配置矩阵引脚和上下拉方式(推荐下拉)
keyscan_set_martix(g_ks_row, ROW_CNT, g_ks_col, COL_CNT, KS_INT_PIN_PULLDOWN);
// 初始化:消抖周期 8ms,1 个空闲周期后进入 idle,2 级消抖
keyscan_init(DEBOUNCE_PERIOD_8MS, 1, DOUBLE_SCAN_TIMES);
// 使能模块和中断
keyscan_enable();
plic_interrupt_enable(IRQ_KEY_SCAN);
core_interrupt_enable();
}
Step 3:中断处理
_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 表示本轮数据结束
}
unsigned char row = key_val >> 5; // 高 3 bit = 行号
unsigned char col = key_val & 0x1f; // 低 5 bit = 列号
// 处理按键按下事件
}
}
}
PLIC_ISR_REGISTER(keyscan_irq_handler, IRQ_KEY_SCAN)
DMA 模式
Step 1:定义行列引脚和 DMA 缓冲区
#define ROW_CNT 3
#define COL_CNT 3
#define DMA_SIZE (8 * 4) // 8 个 word = 32 字节
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:初始化并启动
void user_init(void)
{
// 配置矩阵引脚和上下拉方式(推荐上拉)
keyscan_set_martix(g_ks_row, ROW_CNT, g_ks_col, COL_CNT, KS_INT_PIN_PULLUP);
// 初始化:8K 扫描速率,24M 晶振时钟源,1 无作用固定值设置为1
keyscan_init_clk_24m(KEYSCAN_8K, KS_24MXTAL, 1);
// 使能 DMA 和模块
keyscan_dma_enable();
keyscan_enable();
// 配置 DMA 链表循环模式
dma_set_llp_sof_mode(DMA0, 1);
keyscan_dma_config_llp(DMA0, dma_ks_scanning_buff, DMA_SIZE);
}
Step 3:主循环中轮询处理
void main_loop(void)
{
// 检查 DMA 传输完成中断
if (keyscan_get_rxdone_irq_status())
{
keyscan_clr_rxdone_irq_status();
// 将 DMA 缓冲区数据拷贝到当前帧缓冲区
for (int i = 0; i < 8; i++)
{
now_ks_scanning_buff[i] = dma_ks_scanning_buff[i];
}
}
else
{
return;
}
// 比较前后帧差异,检测按键事件
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++)
{
//按照当前软件的扫描顺序,判断按键是否按下
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 引脚配置
引脚分配
硬件为每个 I/O 端口分配了固定编号(0~31),可复用为 Keyscan 功能。不同芯片的引脚分配不同,具体参考 drivers/keyscan.h 中的 ks_value_e 枚举。
以 TL322x 为例:
typedef enum {
// 第一组(GPIO 编号 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,
// 第二组(GPIO 编号 32~62,编码为 BIT(5) | 编号)
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,
// ... 省略部分引脚
} ks_value_e;
注意
- 两组引脚(第一组 PA~PE 和第二组 PF~PH)不能混用。若使用第二组引脚,第一组引脚只能作为普通 GPIO。
行与列的配置规则
行(Row):
- 固定扫描 8 行,用户可选择 1~8 个引脚
- 未使用的行在驱动内部自动标记为无效,无需用户处理
列(Column):
- 硬件消抖模式:最多 18 列(受内部 FIFO 容量限制)
- DMA 模式:最多 31 列(受 GPIO 编号 0~31 限制)
- 列扫描顺序固定按 GPIO 编号 0→31 进行,与用户配置的数组顺序无关
- 软件已提供"硬件列号 → 用户列号"的转换方式
上下拉配置建议
| 工作模式 | 推荐配置 | 说明 |
|---|---|---|
| 硬件消抖模式 | KS_INT_PIN_PULLDOWN(下拉) |
低速扫描,下拉功耗更低 |
| DMA 模式 | KS_INT_PIN_PULLUP(上拉) |
高速扫描,上拉响应更快,避免时序重叠 |
重要
- 行和列的上下拉方式必须一致(全上拉或全下拉),由
keyscan_set_matrix()的最后一个参数统一配置。
硬件消抖模式详解
工作流程
flowchart LR
A[按键按下] --> B[模块从 Idle 唤醒<br>开始逐行逐列扫描]
B --> C[硬件消抖对比<br>连续 N 次结果一致]
C --> D[写入 FIFO<br>有效键值 + 结束标志 0xFF]
D --> E[触发中断<br>CPU 读取键值]
E --> F{仍有按键?}
F -- 是 --> B
F -- 否 --> G[连续 M 个周期无按键<br>回到 Idle 状态]
G -- 新按键按下 --> A
Idle 状态: 无按键时模块停止扫描,不消耗扫描电流,系统可进入睡眠。
关键参数
| 参数 | 可选值 | 说明 |
|---|---|---|
| 扫描周期 | 固定 2ms | 32K 时钟下,扫完 8 行×31 列固定耗时 2ms |
| 消抖周期 | 4 / 8 / 12 / 16 / 20 / 24 / 28 ms | 相邻两次对比的时间间隔 |
| 消抖次数 | 2 次(Double)/ 3 次(Triple) | 连续几次结果一致才认为有效 |
| 空闲周期数 | 1~N | 无按键后,经历多少个消抖周期才回 Idle |
检测时间估算:
最短检测时间 ≈ 消抖周期 × 消抖次数
例如:消抖周期 8ms + 2 次消抖 → 最快 16ms 确认按键。
注意
- 消抖周期为 4ms 时,由于扫描周期为 2ms,一个周期内只能完成 2 次扫描。因此 4ms 配置下 3 次消抖与 2 次消抖效果相同,建议 4ms 时只用 2 次消抖。
- 消抖周期可选,扫描周期是固定2ms,处理消抖时间固定4ms。
消抖原理
消抖的本质是连续多次扫描结果一致才确认按键,避免机械触点的抖动误触发。
以 8ms 消抖周期 + 2 级消抖 为例:
| 时间点 | 事件 |
|---|---|
| 0ms | 首次检测到按键,开始计时 |
| 8ms | 第二次扫描,结果与首次一致 → 确认按键,触发中断 |
| 16ms | 第三次扫描,仍有按键 → 再次触发中断 |
| ... | 持续按住,每 8ms 触发一次中断 |
| 释放后 | 连续 N 个周期无按键 → 回到 Idle,不再触发中断 |
中断触发规则:
- 从 Idle 开始首次检测,消抖未完成前不触发中断
- 消抖完成后,每个消抖周期触发一次中断,直到按键释放
- 释放后回到 Idle,不再产生中断
键值格式
FIFO 中每个键值为 1 字节:
Bit[7:5] = 行号(0~7)
Bit[4:0] = 列号(0~31)
特殊值 0xFF 表示本轮扫描数据结束。
低功耗
keyscan只能作为suspend的唤醒源,不能作为deep/deep_retation的唤醒源。
keyscan只有处于idle的时候,才能进suspend,否则进不去。
keyscan达到消抖之后才会唤醒,唤醒后按键的结果会保存下来。
//进睡眠之前,检测是否处于idle状态
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 模式详解
工作流程
flowchart LR
A[持续扫描<br>8K 速率] --> B[每轮扫描完成<br>产生 rxdone 信号]
B --> C[DMA 搬运数据<br>到内存缓冲区]
C --> D[软件比较前后帧<br>检测按下/释放]
D --> A
与硬件消抖模式的核心区别:
- 模块持续扫描,不停止,不休眠
- 每 62.5μs 完成一轮扫描,通过 DMA 直接写入内存
- 软件通过对比相邻帧的差异,自行判断按键事件
关键参数
关键参数如下表:
| 参数 | 数值 | 说明 |
|---|---|---|
| 扫描时钟 | 24M | 高速时钟,不可关闭 |
| 单轮扫描时间 | 62.5μs | 扫完 8 行×31 列 |
| 数据格式 | 8×32bit | 每行 32bit,每 bit 代表一列的电平 |
| DMA 触发 | FIFO 满 4 word | 约半轮数据即触发 DMA,保证实时性 |
| DMA 模式 | 链表循环 | 数据循环覆盖到缓冲区首地址 |
时钟分频计算:
32K 时钟时扫描周期为 2ms,DMA 模式目标周期为 62.5μs,两者相差 32 倍:
2ms / 62.5μs *32000 = 1024k
CLK_DIV = 24M/1024K ≈23
即 24M 时钟经 23 分频后得到约 1024k,作为 Keyscan 模块工作时钟。
位图数据格式
DMA 缓冲区为 8 个 32bit 整数,每个整数代表一行的 32 列电平状态:
dma_ks_scanning_buff[0] = Row0 的 Column0~31 电平(bit0=Col0, bit1=Col1...)
dma_ks_scanning_buff[1] = Row1 的 Column0~31 电平
...
dma_ks_scanning_buff[7] = Row7 的 Column0~31 电平
软件消抖
DMA 模式硬件不做消抖,软件需要自行处理机械抖动.
常见问题
Q1:硬件消抖模式能检测按键释放吗?
不能直接检测。硬件消抖模式只在确认按键按下时触发中断,按键释放时模块回到 Idle,中断停止。用户可通过"中断停止"推断按键已释放。
若必须检测释放事件,请使用 DMA 模式。
Q2:为什么 DMA 模式不能进入低功耗?
DMA 模式使用 24M 高速时钟且模块持续运行,无法关闭时钟。若需低功耗,只能切换回硬件消抖模式。
Q3:行/列数组的顺序有影响吗?
行的顺序有影响:驱动按数组索引 0~7 对应硬件 Row0~Row7。
列的顺序无影响:硬件始终按 GPIO 编号 0~31 扫描,驱动内部已完成列号映射。
Q4:可以同时使用两种模式吗?
不可以。两种模式互斥,同一时间只能使能一种。
Q5:消抖周期越长越好吗?
不是。消抖周期越长,按键响应延迟越大。建议:
- 普通应用:8ms 周期 + 2 次消抖(16ms 确认)
- 快速响应场景:4ms 周期 + 2 次消抖(8ms 确认)
Q6:DMA 模式的轮询会影响 CPU 性能吗?
main_loop() 中每次轮询只是检查一个标志位,开销极小。若担心,可改为中断方式:使能 rxdone 中断,在中断处理函数中设置标志,主循环再处理。
Audio
简介
Audio ADC
Audio ADC(Analog-to-Digital Converter,模数转换器)是 Audio CODEC 的输入部分,负责将模拟音频信号转换为数字音频数据。
工作原理:
ADC 通过采样、量化和编码三个步骤,将连续的模拟音频信号转换为离散的数字信号。采样过程以固定频率对模拟信号进行采样,量化过程将采样值映射到有限的离散电平,编码过程将量化后的值转换为二进制数字。
主要特性:
- 输入源选择:支持 AMIC(模拟麦克风)、Line In(模拟音频输入线)
- 采样率:支持 8 kHz、16 kHz、24 kHz、32 kHz、44.1 kHz、48 kHz、96 kHz、192 kHz、384 kHz、768 kHz 等多种采样率
- 数据位宽:支持 16-bit、20-bit、24-bit 多种数据格式
- 增益控制:支持模拟增益(PGA)和数字增益两级调节
- 输入模式:支持单端输入和差分输入模式
- 通道配置:支持单声道(MONO)和立体声(STEREO)模式
ADC 接口信号:
- DATA:ADC_P_IN 和 ADC_N_IN
信号链路:
模拟输入信号 -> PGA(可编程增益放大器)-> ADC -> 数字滤波器 -> 数字增益 -> 数字音频数据
各芯片 ADC 差异比较:
| 芯片系列 | 芯片型号 | ADC 路数 | 支持输入源 | 支持采样率 | 支持位宽 |
|---|---|---|---|---|---|
| TC系列 | TLSR825x/TLSR8359 | 2 路(MONO/STEREO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit |
| TC系列 | TLSR827x/TLSR8355 | 2 路(MONO/STEREO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TC系列 | TC321x | 2 路(MONO/STEREO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TC系列 | TC123x | 1 路(MONO) | AMIC、Line In | 8 kHz、16 kHz | 16-bit |
| TL系列 | TLSR921x/TLSR951x | 2 路(MONO/STEREO) | AMIC、Line In | 8 kHz ~ 192 kHz | 16-bit、20-bit、24-bit |
| TL系列 | TLSR922x/TLSR952x | 2 路(MONO/STEREO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL721x | 1 路(MONO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL321x | 1 路 (MONO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL322x | 1 路(MONO) | AMIC、Line In | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL751x | 4 路(A1/A2/B1/B2) | AMIC、Line In | 16 kHz ~ 768 kHz | 16-bit、24-bit |
Audio DAC
Audio DAC(Digital-to-Analog Converter,数模转换器)是 Audio CODEC 的输出部分,负责将数字音频数据转换为模拟音频信号,用于驱动耳机、扬声器等输出设备。
工作原理:
DAC 将数字音频数据(通常为 PCM 格式)经数模转换处理,还原出平滑连续的模拟音频信号,转换主要分为数字插值、数模转换、模拟滤波。
主要特性:
- 输出类型:支持耳机输出(Headphone)、音频线路输出(Line Out)
- 采样率:支持 8 kHz、16 kHz、24 kHz、32 kHz、44.1 kHz、48 kHz、96 kHz、192 kHz、384 kHz、768 kHz 等多种采样率
- 数据位宽:支持 16-bit、20-bit、24-bit 数据格式
- 增益控制:支持模拟增益和数字增益两级调节
- 输出模式:支持单声道(MONO)和立体声(STEREO)模式
DAC 接口信号:
- DATA:DAC_P_OUT 和 DAC_N_OUT
信号链路:
数字音频数据 -> 滤波器 -> 数字增益 -> DAC -> 模拟增益 -> 模拟输出信号
各芯片 DAC 差异比较:
| 芯片系列 | 芯片型号 | DAC 路数 | 支持采样率 | 支持位宽 |
|---|---|---|---|---|
| TL系列 | TLSR921x/TLSR951x | 2 路(支持 MONO/STEREO) | 8 kHz ~ 192 kHz | 16-bit、20-bit |
| TL系列 | TLSR922x/TLSR952x | 2 路(支持 MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL751x | 2路(DAC A1/A2) | 16 kHz ~ 768 kHz | 16-bit、24-bit |
Audio DMIC
DMIC(Digital Microphone,数字麦克风)是一种直接输出数字音频信号的麦克风,相比传统模拟麦克风(AMIC),DMIC 在抗干扰能力和系统集成度方面具有明显优势。
工作原理:
DMIC 内部集成了 MEMS 传感器和 PDM(Pulse Density Modulation,脉冲密度调制)调制器。声音振动被 MEMS 传感器转换为模拟电信号后,经过 PDM 调制器直接转换为 1-bit 的 PDM 数字信号输出。PDM 调制采用过采样技术,以远高于音频带宽的采样率(通常为 1 MHz ~ 3 MHz)对模拟信号进行调制,通过脉冲密度表示信号幅度。
主要特性:
- 数字输出:直接输出 PDM 数字信号,无需外部 ADC
- 抗干扰能力强:数字信号传输不受模拟噪声和电磁干扰影响
- 系统集成度高:省去了外部 ADC 和模拟前端电路
- 支持双声道:可同时连接左右两个 DMIC,实现立体声采集
- 时钟配置灵活:支持多种 PDM 时钟频率配置
- 增益控制:支持数字增益调节
- 降采样:通过降采样获取所需采样率的数据
PDM 与 PCM 对比:
| 特性 | PDM | PCM |
|---|---|---|
| 信号表达 | 1-bit 脉冲密度调制 | 数字编码幅值 |
| 采样率 | 过采样(1 MHz ~ 3 MHz),通过降采样获取所需采样率的数据 | 标准采样率(8 kHz ~ 48 kHz) |
| 数据量 | 高(数字抽取滤波) | 低(直接可用) |
| 抗干扰性 | 强(数字信号) | 弱(模拟信号易受影响) |
| 应用场景 | 数字麦克风 | 传统音频采集、存储 |
DMIC 接口信号:
- CLK:PDM 时钟信号,由主设备提供给 DMIC 器件
- DATA:PDM 数据信号,DMIC 器件输出给主设备
- LR 选择:支持通过DMIC的SE引脚选择左右声道
信号处理流程:
声音 -> DMIC 器件 -> PDM 调制器 -> PDM 数字信号 -> 抽取滤波器 -> PCM 音频数据
各芯片 DMIC 差异比较:
| 芯片系列 | 芯片型号 | DMIC 路数 | 支持采样率 | 支持位宽 |
|---|---|---|---|---|
| TC系列 | TLSR825x/TLSR8359 | 2 路(MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC系列 | TLSR827x/TLSR8355 | 2 路(MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC系列 | TC321x | 2 路(MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TLSR921x/TLSR951x | 2 路(MONO/STEREO) | 8 kHz ~ 192 kHz | 16-bit、20-bit、24-bit |
| TL系列 | TLSR922x/TLSR952x | 4 路(Codec0:2路;Codec1:2路) | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL721x | 2 路(MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL321x | 2 路(MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL322x | 2 路(支持 MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit、20-bit |
| TL系列 | TL751x | 6 路(Codec0:DMIC A1/A2/B1/B2;Codec1:DMIC A1/A2) | 16 kHz ~ 768 kHz | 16-bit、24-bit |
Audio I2S
I2S(Inter-IC Sound)是一种用于连接数字音频设备的串行总线接口标准,由 Philips 公司开发,广泛应用于音频编解码器、DSP、数字音频处理器等设备之间的音频数据传输。
-
接口信号:
I2S 接口通常包含以下信号线:
- SCK(Serial Clock):串行时钟信号,由主设备提供,用于同步数据传输
- WS(Word Select):字选择信号,也称为 LRCK(Left/Right Clock),用于区分左右声道数据
- SD(Serial Data):串行数据信号,用于传输实际的音频数据
- MCLK(Master Clock):主时钟信号,部分设备需要额外的系统时钟作为参考
-
工作模式:
I2S 支持多种数据格式和工作模式:
| 模式 | 特点 | 应用场景 |
|---|---|---|
| I2S 模式 | WS 在数据发送前一个时钟周期翻转,数据在 SCK 下降沿变化、上升沿采样 | 标准音频设备互联 |
| Left Justified (LJ) | WS 与数据同时开始,数据左对齐 | DSP、数字音频处理器 |
| Right Justified (RJ) | WS 与数据同时开始,数据右对齐 | 部分 ADC/DAC 设备 |
| DSP 模式 | WS 作为帧同步信号,数据连续传输 | DSP、蓝牙音频 |
| TDM 模式 | 时分复用,支持多通道数据传输 | 多麦克风阵列、专业音频 |
-
I/O 配置:
I2S 支持灵活的 I/O 配置:
- I2S_5_LINE_MODE:标准5线I/O配置,包含BIT_CLK、ADC_LR_CLK(RX)、RX_DATA、DAC_LR_CLK(TX)和TX_DATA
- I2S_4_LINE_DAC_MODE:4线模式,包含BIT_CLK、RX_DATA、DAC_LR_CLK和TX_DATA,RX和TX的字选择信号都是DAC_LR_CLK
- I2S_4_LINE_ADC_MODE:4线模式,包含BIT_CLK、ADC_LR_CLK、RX_DATA、和TX_DATA,RX和TX的字选择信号都是ADC_LR_CLK
- I2S_2_LANE_TX_MODE:5线I/O配置,但RX_DATA和TX_DATA均用作TX_DATA,两路数据线同时输出
- I2S_2_LANE_RX_MODE:5线I/O配置,但RX_DATA和TX_DATA均用作RX_DATA,两路数据线同时输入
-
TDM(Time Division Multiplexing)模式:
TDM 模式扩展了标准 I2S 的功能,支持在一个帧内传输多个通道的音频数据:
- TDM_MODE_A:WS 为单周期脉冲,数据在 SCK 上升沿采样
- TDM_MODE_B:WS 为 50% 占空比方波,数据在 SCK 上升沿采样
- TDM_MODE_C:WS 为单周期脉冲,数据在 SCK 下降沿采样
- 支持通道数:2/4/6/8 通道
- Slot 宽度:支持 16-bit、24-bit、32-bit
-
关键参数:
- 支持采样率:8 kHz ~ 192 kHz
- 支持数据位宽:16-bit、20-bit、24-bit、32-bit
- 支持主模式(Master)和从模式(Slave)
- 支持单声道(MONO)和立体声(STEREO)
各芯片 I2S 差异比较:
| 芯片系列 | 芯片型号 | I2S 通道数 | 数据位宽 | 支持采样率 | TDM 支持 | 工作模式 |
|---|---|---|---|---|---|---|
| TC系列 | TLSR825x/TLSR8359 | 1路 | 16-bit | 8 kHz ~ 192 kHz | 否 | I2S |
| TC系列 | TLSR827x/TLSR8355 | 1路 | 16/20-bit | 8 kHz ~ 192 kHz | 否 | I2S |
| TC系列 | TC321x | 1路 | 16/20-bit | 8 kHz ~ 192 kHz | 否 | I2S |
| TL系列 | TLSR921x/TLSR951x | 1路 | 16/20/24-bit | 8 kHz ~ 192 kHz | 否 | I2S/RJ/LJ/DSP |
| TL系列 | TLSR922x/TLSR952x | 2路 (I2S0/I2S1) | 16/20/24-bit | 8 kHz ~ 192 kHz | 否 | I2S/RJ/LJ/DSP |
| TL系列 | TL721x | 3路 | 16/20/24-bit | 8 kHz ~ 192 kHz | 是 (I2S2) | I2S/RJ/LJ/DSP/TDM |
| TL系列 | TL321x | 1路 | 16/20/24-bit | 8 kHz ~ 192 kHz | 是 | I2S/RJ/LJ/DSP/TDM |
| TL系列 | TL322x | 1路 | 16/20/24/32-bit | 8 kHz ~ 192 kHz | 是 | I2S/RJ/LJ/DSP/TDM |
| TL系列 | TL751x | 3路 (I2S0/I2S1/I2S2) | 16/20/24-bit | 8 kHz ~ 192 kHz | 是 (I2S0) | I2S/RJ/LJ/DSP/TDM |
Audio SDM
SDM(Sigma-Delta Modulator,Sigma-Delta 调制器)是一种用于数字音频输出的调制技术,通过过采样和噪声整形将高分辨率的数字音频信号转换为低分辨率的脉冲密度调制信号,直接驱动 Class-D 音频功放或外部低通滤波器还原为模拟音频信号。
-
主要特性:
- 直接数字输出:无需传统 DAC,直接输出调制后的数字脉冲信号
- 低功耗:相比传统 DAC + 模拟功放方案,整体功耗更低
- 通道配置:支持单声道和立体声输出模式
-
输出引脚配置:
SDM 输出通常采用差分信号对(P/N)形式:
- SDM_P:正向输出信号
- SDM_N:反向输出信号(差分模式)
-
关键参数:
- 支持采样率:8 kHz ~ 48 kHz
- 支持数据位宽:16-bit
-
信号处理流程:
数字音频数据 -> 插值滤波器 -> Sigma-Delta 调制器 -> PDM 输出 -> 滤波器 -> 模拟音频信号
各芯片 SDM 差异比较:
| 芯片系列 | 芯片型号 | SDM 通道数 | 支持采样率 | 数据位宽 |
|---|---|---|---|---|
| TC系列 | TLSR825x/TLSR8359 | 2路 (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC系列 | TLSR827x/TLSR8355 | 2路 (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TC系列 | TC321x | 2路 (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TL系列 | TL721x | 2路 (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TL系列 | TL321x | 2路 (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
| TL系列 | TL322x | 2路 (MONO/STEREO) | 8 kHz ~ 48 kHz | 16-bit |
Audio驱动和Demo文件结构
驱动文件结构
- TC 系列
| 芯片 | Demo 位置 | 驱动文件位置 | 支持的 Audio 模块 |
|---|---|---|---|
| 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 系列
| 芯片 | Demo 位置 | 驱动文件位置 | 支持的 Audio 模块 |
|---|---|---|---|
| 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 |
驱动层文件
Audio 驱动层位于各芯片目录下,提供硬件寄存器操作和底层功能接口:
| 文件 | 说明 |
|---|---|
audio.c |
Audio 驱动主文件,包含Audio各个模块初始化、时钟控制、通路设置、DMA和FIFO配置等底层实现 |
audio.h |
Audio 驱动头文件,定义枚举类型、结构体、函数声明等 |
register.h(TC系列)/audio_reg.h(TL系列) |
TC系列Audio寄存器地址放在公共的register.h头文件;TL系列Audio寄存器地址放在独立的的audio_reg.h头文件; |
驱动层主要功能模块:
- 初始化和时钟配置:音频 PLL 配置、时钟分频、Audio模块配置与使能
- 通路配置:ADC/DAC/DMIC/I2S/SDM 等模块的输入输出通路设置
- 增益控制:模拟增益(PGA)和数字增益的配置接口
- FIFO 管理:音频数据缓冲区的读写控制
- DMA 接口:与 DMA 控制器配合,实现音频数据的批量传输
- 中断管理:FIFO 中断的配置和处理
Demo文件结构
各版本 Audio Demo 的应用层文件结构如下:
- TC 系列
AUDIO_V1.0(TLSR825x/TLSR8359/TLSR827x/TLSR8355)
AUDIO_Demo/
├── app.c # 应用文件,包含音频模块使用示例
├── app_config.h # 配置文件,用于选择哪一个示例进行编译
├── audio_data.h # 包含不同采样率下的音频数据
└── main.c # 程序入口,包含平台初始化和时钟初始化
AUDIO_V1.1(TC321x)
AUDIO_Demo/
├── app_codec.c # 包含ADC、SDM和DMIC的使用示例
├── app_config.h # 配置文件,用于选择哪一个示例进行编译
├── app_i2s.c # I2S 使用示例(I2S 初始化、接收和发送)
├── app_mix.c # ADC、DMIC和I2S混合使用的示例
├── audio_common.c # 包含音频数据渐入接口
├── audio_common.h # 包含不同采样率下的音频数据
└── main.c # 程序入口,包含平台初始化和时钟初始化
AUDIO_V1.2(TC123X)
AUDIO_Demo/
├── app.c # 包含ADC的使用示例
├── app_config.h # 配置文件,用于选择哪一个示例进行编译
└── main.c # 程序入口,包含平台初始化和时钟初始化
- TL 系列
AUDIO_V1.0(TLSR921x/TLSR951x)
AUDIO_Demo/
├── app.c # 包含ADC、DMIC、DAC和I2S的使用示例
├── app_sin_data.h # 包含不同采样率下的音频数据
└── main.c # 程序入口,包含平台初始化和时钟初始化
AUDIO_V1.1(TLSR922x/TLSR952x)
AUDIO_Demo/
├── codec_0581/ # ADI 0581 编解码器支持
│ ├── codec_0581_eq.c # 0581 EQ 系数
│ ├── codec_0581_fdsp.c # 0581 FDSP 系数
│ └── codec_0581_registers.c # 0581 寄存器配置
├── ext_codec_wm/ # 外部Codec支持
│ ├── ext_codec_wm.c
│ └── ext_codec_wm.h
├── app_codec.c # 包含ADC、DMIC、DAC和I2S的使用示例
├── app_codec_0581.c # 0581 编解码器应用示例
├── app_i2s.c # I2S 使用示例(I2S 初始化、接收和发送)
├── app_sin_data.h # 包含不同采样率下的音频数据
└── main.c ## 程序入口,包含平台初始化和时钟初始化
AUDIO_V1.2(TL751x)
AUDIO_Demo/
├── app_anc.c # ANC模块的使用示例
├── app_asrc.c # ASRC模块的使用示例
├── app_codec.c # 包含ADC、DAC和DMIC的使用示例
├── app_eq.c # EQ模块的使用示例
├── app_filter_data.h # ANC滤波器系数数据、ANC和EQ模块在特定系数和输入数据下的输出数据
├── app_i2s.c # I2S 使用示例(I2S 初始化、接收和发送)
├── app_mix.c # 音频不同模块混合使用示例
├── app_sidetone.c # Sidetone模块的使用示例
├── app_sin_data.h # 包含不同采样率下的音频数据
└── main.c # 程序入口,包含平台初始化和时钟初始化
AUDIO_V1.3(TL321x/TL322x/TL721x)
AUDIO_Demo/
├── ext_codec_wm/ # 外部 Codec 支持
│ ├── ext_codec_wm.c # WM 编解码器驱动实现
│ └── ext_codec_wm.h # WM 编解码器头文件
├── app_codec.c # 包含ADC、SDM和DMIC的使用示例
├── app_i2s.c # I2S 接口应用层
├── app_mix.c # 音频不同模块混合使用示例
├── audio_common.c # 包含音频数据渐入接口
├── audio_common.h # 音含不同采样率下的音频数据
└── main.c # 程序入口,包含平台初始化和时钟初始化
Audio模块初始化与配置
Audio初始化

Audio的初始化包含Audio模块的上电和Audio时钟的初始化,Audio时钟来自系统时钟或者PLL分频,以来自PLL时钟为例(见上图),通过audio_set_audio_clk接口配置Audio时钟。各芯片的Audio时钟可参考对应芯片的Datasheet。下面代码以TL721x为例,其余芯片的初始化见对应的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);
}
// 示例:Audio初始化
audio_init();
Audio ADC使用与基础配置
Audio ADC的配置包含ADC MIC BIAS管脚设置、数据位宽、采样率、FIFO 和 DMA 通道选择、增益控制。Audio ADC的配置顺序为Audio模块初始化->时钟初始化->ADC参数和FIFO配置->DMA配置和使能,增益可随时调控。TC系列配置DFIFO后,硬件即会循环往软件buff中搬运数据,无需DMA配置。
-
Audio ADC MIC BIAS管脚设置
下图为AMIC硬件原理图,为了使AMIC能正常工作,需要给AMIC提供MIC BIAS电压。因芯片而异,部分芯片是通过外部GPIO来提供电压,则需要单独配置GPIO输出高电平;部分芯片是由CODEC来提供MIC BIAS,硬件已将AMIC的VDD与CODEC的对应管脚相连,则无需外部GPIO,只要输入源选AMIC,驱动会打开CODEC的MIC BIAS电压。

外部GPIO来提供MIC BIAS电压的示例代码如下:
/** * @brief 配置 AMIC 偏置引脚 * @param[in] amic_bias - 偏置引脚(如 GPIO_PA0) */ void audio_set_amic_bias_pin(gpio_pin_e amic_bias); // 示例:使能 AMIC 偏置 audio_set_amic_bias_pin(GPIO_PA0);
注意
若AMIC没有声音或声音异常,优先排查MIC BIAS电压是否正确。
-
Audio ADC参数配置
ADC 参数配置主要包括 采样率、数据位宽、FIFO 通道 和 DMA 通道 的选择。不同芯片的配置方式有所差异,早期芯片TLSR825x/TLSR8359和TLSR827x/TLSR8355的配置如下所示:
// TLSR825x/TLSR8359和TLSR827x/TLSR8355示例:配置ADC FIFO和AMIC采样率 audio_config_mic_buf((unsigned short*)MicBuf,MIC_BUFFER_SIZE); audio_amic_init(AUDIO_RATE_VAL);除早期芯片(TLSR825x/TLSR8359和TLSR827x/TLSR8355)在初始化接口中指定采样率外,其余芯片使用结构体封装配置参数,以TL721x为例,将配置参数整合进结构体
audio_codec_stream0_input_t中,再通过audio_codec0_input_init(input_config)完成初始化,示例代码如下。配置结构体的参数和初始化函数因芯片而异,各芯片的配置结构体详见对应的Audio Demo。audio_codec_stream0_input_t audio_codec_stream0_input = { .input_src = INPUT_SRC,//选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) // 示例:初始化audio ADC,配置ADC参数 audio_codec_stream0_input_init(&audio_codec_stream0_input); //配置链式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); //使能Codec通路和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增益配置
Audio ADC 增益配置分为 模拟增益(Analog Gain) 和 数字增益(Digital Gain) 两部分。模拟增益用于调整 PGA(Programmable Gain Amplifier)的放大倍数,数字增益用于在数字域对采样后的数据进行增益补偿。增益调节的接口函数、范围和步进因芯片而异,具体参考驱动代码实现,以下的增益控制接口以TL721x为例。
- 模拟增益(PGA):
- 接口:
audio_set_adc_pga_gain(pga_gain) - 枚举:
codec_in_pga_gain_e - 范围:0 dB ~ 45 dB(0 dB、9 dB、12 dB、15 dB...45 dB)
- 接口:
- 数字增益:
- 接口:
audio_set_stream0_dig_gain(d_gain) - 枚举:
codec_in_path_digital_gain_e - 范围:-48 dB ~ +42 dB,步进 6 dB
- 接口:
- 模拟增益(PGA):
Audio DAC使用与基础配置
Audio DAC的配置包含数据位宽、采样率、FIFO和 DMA 通道选择、增益控制。
-
Audio DAC 参数配置
DAC 参数配置主要包括 采样率、数据位宽、FIFO 通道 和 DMA 通道 的选择。目前支持 DAC 功能的均为TL系列芯片,TC系列芯片无 DAC 模块。TLSR921x/TLSR951x中通过
audio_init接口入参初始化并配置Audio DAC。Audio DAC的配置顺序为Audio模块初始化->时钟初始化->DAC参数和FIFO配置->DMA配置和使能,增益可随时调控。// 示例:初始化audio DAC通路,配置DAC参数 audio_init(BUF_TO_LINE_OUT, AUDIO_16K, MONO_BIT_16);后续的芯片都通过配置结构体封装参数,以TLSR922x/TLSR952x为例,通过
audio_codec_output_t结构体描述Audio DAC的参数,通过audio_codec_stream_output_init接口统一配置。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), }; // 示例:初始化audio DAC,配置DAC参数 audio_codec_stream_output_init(&audio_codec_output); -
Audio DAC 增益配置
DAC 增益配置分为 模拟增益(Analog Gain) 和 数字增益(Digital Gain) 两部分。增益配置接口与配置范围和步进因芯片而异,下面以TLSR922x/TLSR952x为例描述增益Audio DAC控制接口的使用。
- 模拟增益(PGA):
- 接口:
audio_set_dac_pga_l_gain(pga_gain)(左通道)/audio_set_dac_pga_r_gain(pga_gain)(右通道) - 枚举:
codec_out_pga_gain_e - 范围:-60.2 dB ~ +6 dB(非线性档位:-60.2 dB、-54.2 dB、-48.2 dB...-6 dB、-3 dB、0 dB、3 dB、6 dB)
- 步进:非线性,约 3 dB/6 dB 步进。
- 接口:
- 数字增益:
- 接口:
audio_set_dac_l_gain(d_gain)(左通道)/audio_set_dac_r_gain(d_gain)(右通道) - 枚举:
codec_out_path_digital_gain_e - 范围:-72.2 dB ~ 0 dB(非线性档位:-72.2 dB、-66.2 dB、-60.2 dB...-6 dB、-3 dB、-1 dB、0 dB)
- 步进:非线性
- 接口:
- 模拟增益(PGA):
Audio DMIC 使用与基础配置
Audio DMIC的配置包含DMIC管脚配置、数据位宽、采样率、FIFO和 DMA 通道选择、增益控制。CODEC DAC的配置顺序为Audio模块初始化->时钟初始化->DMIC参数和FIFO配置->DMA配置和使能,增益可随时调控。TC系列配置DFIFO后,硬件即会循环往软件buff中搬运数据,无需DMA配置。
-
Audio DMIC管脚设置

DMIC的电源由硬件提供,DMIC 时钟用于驱动数字麦克风的数据输出,DMIC的SE管脚接地或接VDD用于区分左右声道,DMICDAT为DMIC器件的数据输出管脚。SE 接 GND(低电平):麦克风为左声道 L,数据在 CLK 上升沿输出有效 PDM 数据;SE 接 VDD(高电平):麦克风为右声道 R,数据在 CLK 下降沿输出有效 PDM 数据。以TL721x为例,DMIC的管脚配置接口为
audio_set_stream0_dmic_pin。//DMIC管脚配置示例 audio_set_stream0_dmic_pin(GPIO_FC_PA2, GPIO_FC_PA3, GPIO_FC_PA4); -
Audio DMIC参数配置
DMIC 的参数配置主要包括采样率、位宽、FIFO 通道选择和 DMA 通道配置。不同芯片的 DMIC 参数配置方式有所区别,具体详见对应芯片的DMIC使用demo,以下以TL721x芯片为例。
audio_codec_stream0_input_t audio_codec_stream0_input = { .input_src = INPUT_SRC,//选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), }; //DMIC通道初始化示例 audio_codec_stream0_input_init(&audio_codec_stream0_input); -
Audio DMIC增益配置
由于 DMIC 是数字麦克风,DMIC 的增益配置只有 数字增益(Digital Gain) 部分。不同芯片的 DMIC 数字增益控制接口有所区别,具体详见对应芯片的驱动代码,以下以TL721x芯片为例。
- 数字增益:
- 接口:
audio_set_stream0_dig_gain(d_gain) - 枚举:
codec_in_path_digital_gain_e - 范围:-48dB ~ +42B,步进 6dB
- 接口:
- 数字增益:
Audio I2S使用与基础配置
I2S的配置顺序为Audio模块初始化->时钟初始化->I2S参数和FIFO配置->DMA配置和使能,TC系列配置DFIFO后,硬件即会循环往软件buff中搬运数据,无需DMA配置。
-
Audio I2S 管脚配置
I2S 的管脚配置包含位时钟线、字选择线和TX、RX数据线。以TL721x为例,I2S的管脚配置已封装进结构体
i2s_pin_config_t,通过i2s_set_pin接口来统一配置。其余芯片的配置参考对应芯片的Audio Demo。i2s_pin_config_t i2s_pin_config = { .bclk_pin = GPIO_FC_PD5,//位时钟 .adc_lr_clk_pin = GPIO_FC_PB3,//RX的字选择PIN .dac_lr_clk_pin = GPIO_FC_PB4,//TX的字选择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 select * @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 分频系数配置
如前面Audio模块的时钟示意图所示,I2S的时钟来自PLL的分频。I2S的分频系统由i2s_clk_config数组来统一管理。当PLL为240000000(240M)时,i2s_clk_config[5]={4,1875,0,32,32}时,i2s各个时钟的计算公式如下所示:
I2S_CLK = 240000000(i2s_clk_config[0]/i2s_clk_config[1]) = 240000000(4/1875) = 512000
注意
i2s_clk_config[1]必须大于等于两倍的i2s_clk_config[0]。
当i2s_clk_config[2]为0时, I2S_BCLK = I2S_CLK = 512000
当i2s_clk_config[2]不为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 参数配置
I2S 的参数配置主要包括采样率、位宽、FIFO 通道选择和 DMA 通道配置。不同芯片的 I2S 参数配置方式有所区别,具体详见对应芯片的I2S使用demo,以下以TL721x芯片为例。
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输入参数配置 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输出参数配置 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 = FIFO2, .dma_num = DMA1, .data_buf = AUDIO_BUFF, .data_buf_size = sizeof(AUDIO_BUFF), }; //I2S输入初始化配置示例,该接口配置I2S时管脚与时钟分频系数 audio_i2s_config_init(&audio_i2s_config); //I2S输入初始化配置示例 audio_i2s_input_init(&audio_i2s_input); //I2S输出初始化配置示例 audio_i2s_output_init(&audio_i2s_output); //I2S时钟使能 audio_i2s_clk_en(audio_i2s_config.i2s_select);
Audio SDM使用与基础配置
-
Audio SDM 管脚配置
每1路 SDM 的输出需要配置 P 和 N 两个引脚,对应通道的正负端。不同系列芯片的配置方式详见对应的Audio demo,以TL721x为例,SDM P/N引脚的配置封装在
sdm_pin_config_t结构体中,由接口audio_set_sdm_pin进行配置。
TL721x 示例:
typedef struct
{
gpio_func_pin_e sdm0_p_pin; // SDM0 正端引脚
gpio_func_pin_e sdm0_n_pin; // SDM0 负端引脚
gpio_func_pin_e sdm1_p_pin; // SDM1 正端引脚
gpio_func_pin_e sdm1_n_pin; // SDM1 负端引脚
} 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);
注意
TL721x Demo 中的 SDM0_P 使用 PA0 引脚,与默认 DEBUG 打印引脚冲突。若使用 SDM 功能,需修改 printf.h 中的 DEBUG_INFO_TX_PIN 为其他引脚。将SDM的引脚分配在EVB板子的两侧,防止信号互相干扰,产生噪音。
-
Audio SDM 参数配置
SDM 输出参数通过
audio_codec_output_t结构体进行配置,包含输出通道、采样率、数据位宽、DMA 通道等关键参数。以TL721x为例,其余芯片参考对应的驱动代码和Audio Demo。
TL721x 示例:
audio_codec_output_t 结构体定义:
typedef struct
{
void *data_buf; // 输出数据缓冲区指针
unsigned int data_buf_size; // 输出数据缓冲区大小
audio_stream_output_src_e output_src; // 输出源选择
unsigned char sample_rate; // 采样率
unsigned char data_width; // 数据位宽
dma_chn_e dma_num; // DMA 通道号
} audio_codec_output_t;
//配置示例
#define SAMPLE_RATE AUDIO_48K
#define DATA_WIDTH CODEC_BIT_16_DATA
#define TX_FIFO_NUM FIFO0 // SDM 固定使用 FIFO0
#define TX_DMA_CHN DMA1
audio_codec_output_t audio_stream_output = {
.output_src = SDM_MONO, // 或 SDM_STEREO
.sample_rate = SAMPLE_RATE,
.data_width = DATA_WIDTH,
.dma_num = TX_DMA_CHN,
.data_buf = audio_tx_buff, // 输出数据缓冲区
.data_buf_size = sizeof(audio_tx_buff),
};
//SDM输出初始化
audio_codec_stream_output_init(&audio_stream_output);
重要说明
SDM 模块仅支持 16bit 数据位宽,配置时必须使用 CODEC_BIT_16_DATA。使用其他位宽可能导致输出异常。SDM 输出固定使用 TX FIFO0。
-
Audio SDM增益配置
Audio SDM模块通过
audio_set_ascl_gain配置输出增益。以TL721x为例,其余芯片参考对应的Audio Demo和驱动文件。
TL721x 示例:
- 数字增益:
- 接口:
audio_set_ascl_gain(d_gain) - 枚举:
ascl_out_path_digital_gain_e - 范围:-48dB ~ +42B,步进 6dB
- 接口:
Audio的DMA链介绍
Audio 模块的数据传输大量依赖 DMA(Direct Memory Access,直接存储器访问)来完成音频数据在内存与 FIFO 之间的高效搬运,降低 CPU 开销。Audio DMA 采用链式传输机制,实现数据循环缓冲,保证音频流的连续性。
DMA与FIFO的位置关系如下图所示:

重要说明
TC系列芯片只要配置DFIFO后,硬件自动循环往定义的buff中搬运Audio数据,TL系列芯片需要通过DMA接口配置链式传输。
DMA链式传输原理
DMA 链式传输通过链表结构将多个 DMA 传输节点链接起来,当前节点传输完成后自动跳转到下一个节点,形成循环。
该示意图为DMA双节点链式传输,Audio Demo中一般使用单节点链式传输。
+-------------+ +-------------+ +-------------+
| DMA Node 0 | --> | DMA Node 1 | --> | DMA Node 0 | (循环)
+-------------+ +-------------+ +-------------+
| | |
Buff[0] Buff[1] Buff[0]
链式传输优势:
- 连续传输:无需 CPU 干预即可实现不间断音频数据流
- 双缓冲机制:用双节点链表循环可实现乒乓缓冲,避免数据覆盖
- 低延迟:DMA 硬件自动完成数据搬运,响应速度快
Audio DMA配置结构
Audio 驱动为 RX(接收)和 TX(发送)各自预定义了 DMA 配置:
RX DMA 配置(audio_dma_rx_config):
| 字段 | 配置值 | 说明 |
|---|---|---|
| dst_req_sel | 0 | 因为是RX请求源,该值为0 |
| src_req_sel | DMA_REQ_AUDIO_RX | 音频 RX FIFO 请求源 |
| dst_addr_ctrl | DMA_ADDR_INCREMENT | 目标地址递增(内存缓冲区) |
| src_addr_ctrl | DMA_ADDR_FIX | 源地址固定(FIFO 寄存器) |
| dstmode | DMA_NORMAL_MODE | 正常模式 |
| srcmode | DMA_HANDSHAKE_MODE | 源握手模式(等待 FIFO 请求) |
| dstwidth / srcwidth | DMA_CTR_WORD_WIDTH | 传输宽度为 Word(32bit) |
| src_burst_size | 0 | 必须为 0 |
TX DMA 配置(audio_dma_tx_config):
| 字段 | 配置值 | 说明 |
|---|---|---|
| dst_req_sel | DMA_REQ_AUDIO_TX | 音频 TX FIFO 请求源 |
| src_req_sel | 0 | 因为是TX请求源,该值为0 |
| dst_addr_ctrl | DMA_ADDR_FIX | 目标地址固定(FIFO 寄存器) |
| src_addr_ctrl | DMA_ADDR_INCREMENT | 源地址递增(内存缓冲区) |
| dstmode | DMA_HANDSHAKE_MODE | 目标握手模式 |
| srcmode | DMA_NORMAL_MODE | 正常模式 |
| dstwidth / srcwidth | DMA_CTR_WORD_WIDTH | 传输宽度为 Word(32bit) |
| src_burst_size | 0 | 必须为 0 |
Audio DMA链式传输接口
- Audio RX DMA 链初始化
/**
* @brief 初始化 Audio RX DMA 链式传输
* @param[in] rx_fifo_chn - RX FIFO 通道选择
* @param[in] chn - DMA 通道号
* @param[in] in_buff - 接收数据缓冲区指针(必须 4 字节对齐)
* @param[in] buff_size - 缓冲区大小(字节,必须为 4 的倍数,最大 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 链初始化
/**
* @brief 初始化 Audio TX DMA 链式传输
* @param[in] tx_fifo_chn - TX FIFO 通道选择
* @param[in] chn - DMA 通道号
* @param[in] out_buff - 发送数据缓冲区指针(必须 4 字节对齐)
* @param[in] buff_size - 缓冲区大小(字节,必须为 4 的倍数,最大 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 使能与禁用
// 使能 RX DMA
static inline void audio_rx_dma_en(dma_chn_e chn);
// 禁用 RX DMA
static inline void audio_rx_dma_dis(dma_chn_e chn);
// 使能 TX DMA
static inline void audio_tx_dma_en(dma_chn_e chn);
// 禁用 TX DMA
static inline void audio_tx_dma_dis(dma_chn_e chn);
Audio DMA链底层实现
RX DMA 链初始化流程:
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. 配置 DMA 通道参数
audio_rx_dma_config(chn, in_buff, buff_size, &g_audio_rx_dma_list_cfg[rx_fifo_chn]);
// 2. 添加链表元素(自循环,实现单缓冲循环传输)
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 链初始化流程:
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. 配置 DMA 通道参数
audio_tx_dma_config(chn, out_buff, buff_size, &g_audio_tx_dma_list_cfg[tx_fifo_chn]);
// 2. 添加链表元素(自循环,实现单缓冲循环传输)
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);
}
使用示例
// 输入配置
audio_rx_dma_chain_init(FIFO0, DMA0, (unsigned short *)audio_buff, sizeof(audio_buff));
audio_rx_dma_en(DMA0);
// 输出配置
audio_tx_dma_chain_init(FIFO0, DMA1, (unsigned short *)audio_buff, sizeof(audio_buff));
audio_tx_dma_en(DMA1);
Audio FIFO中断
Audio 模块为每个 FIFO 通道提供了独立的中断机制,当 FIFO 中的数据量达到预设阈值时触发中断,用户可在中断服务程序中处理音频数据或进行状态监控。
FIFO中断类型
Audio 模块支持 RX 和 TX 两种 FIFO 中断类型,以TL721x为例,TL721x有3个RX FIFO和3个TX FIFO,因此有如下6个FIFO中断:
typedef enum
{
AUDIO_RX_FIFO0_IRQ = BIT(0), // RX FIFO0 中断
AUDIO_RX_FIFO1_IRQ = BIT(1), // RX FIFO1 中断
AUDIO_RX_FIFO2_IRQ = BIT(2), // RX FIFO2 中断
AUDIO_TX_FIFO0_IRQ = BIT(3), // TX FIFO0 中断
AUDIO_TX_FIFO1_IRQ = BIT(4), // TX FIFO1 中断
AUDIO_TX_FIFO2_IRQ = BIT(5), // TX FIFO2 中断
} audio_fifo_irq_type_e;
FIFO中断触发原理
FIFO 中断基于阈值(Threshold)机制触发:
- RX FIFO 中断:当 RX FIFO 中接收到的数据量达到或超过设定的阈值时触发
- TX FIFO 中断:当 TX FIFO 中已发送的数据量达到或超过设定的阈值时触发
阈值通过以下接口配置:
/**
* @brief 设置 RX FIFO 中断阈值
* @param[in] rx_fifo_chn - RX FIFO 通道
* @param[in] threshold - 阈值(字节数),实际写入寄存器的值为 threshold >> 2
*/
static inline void audio_set_rx_fifo_threshold(audio_fifo_chn_e rx_fifo_chn, unsigned short threshold);
/**
* @brief 设置 TX FIFO 中断阈值
* @param[in] tx_fifo_chn - TX FIFO 通道
* @param[in] threshold - 阈值(字节数),实际写入寄存器的值为 threshold >> 2
*/
static inline void audio_set_tx_fifo_threshold(audio_fifo_chn_e tx_fifo_chn, unsigned short threshold);
注意事项
阈值寄存器以 Word(4 字节)为单位,因此实际阈值 = 配置值 × 4。例如配置 AUDIO_BUFF_SIZE / 2 表示当 FIFO 数据量达到缓冲区一半时触发中断。
FIFO中断控制接口
使能 FIFO 中断:
/**
* @brief 使能 RX FIFO 中断
* @param[in] rx_fifo_chn - RX FIFO 通道
*/
static inline void audio_rxfifo_irq_en(audio_fifo_chn_e rx_fifo_chn);
/**
* @brief 使能 TX FIFO 中断
* @param[in] tx_fifo_chn - TX FIFO 通道
*/
static inline void audio_txfifo_irq_en(audio_fifo_chn_e tx_fifo_chn);
查询与清除中断状态:
/**
* @brief 获取 FIFO 中断状态
* @param[in] irq_status - 中断类型
* @return 中断状态,非 0 表示对应中断触发
*/
static inline unsigned char audio_get_irq_status(audio_fifo_irq_type_e irq_status);
/**
* @brief 清除 FIFO 中断状态
* @param[in] irq_status - 中断类型
*/
static inline void audio_clr_irq_status(audio_fifo_irq_type_e irq_status);
FIFO中断的使用例子可参考TL721x的Audio Demo的AUDIO_FIFO_IRQ_TEST case。
常用FIFO指针和DMA指针查询接口
在FIFO中断处理或调试过程中,可通过以下接口获取 FIFO 和 DMA 指针位置:
| 接口 | 功能 |
|---|---|
audio_get_rx_wptr(rx_fifo_chn) |
获取 RX FIFO 写指针 |
audio_get_tx_rptr(tx_fifo_chn) |
获取 TX FIFO 读指针 |
audio_get_rx_dma_wptr(dma_chn) |
获取 RX DMA 目标地址 |
audio_get_tx_dma_rptr(dma_chn) |
获取 TX DMA 源地址 |
SPI
简介
标准SPI接口
串行外设接口(Serial Peripheral Interface)简称 SPI 接口,是一种同步串行外设接口,允许嵌入式处理器与各种外围设备通过串行的方式进行通信、数据交换等。
标准 SPI 接口一般使用 4 条线通信:

| 名称 | 含义 |
|---|---|
| CSN | 设备片选信号线,低电平有效 |
| CLK | 时钟信号线 |
| MOSI | Master 数据输出 Slave 数据输入线 |
| MISO | Master 数据输入 Slave 数据输出线 |
SPI通信过程与四种工作模式
下图是SPI通信的一个简单例子:

CSN、CLK、MOSI 信号都由 Master 产生,数据输出通过 MOSI 线。而 MISO 的信号由 Slave 产生,Master 通过该信号线读取 Slave 的数据。MOSI 与 MISO 的信号只在 CSN 为低电平的时候才有效,数据在 CLK 上升沿或下降沿时触发采样。在 CLK 的每个时钟周期 MOSI 和 MISO 传输 1 bit 数据,8 个时钟周期就可以实现 1 Byte 数据传输。
根据空闲时 CLK 时钟极性 CPOL (Clock Polarity) 和采样时刻的 CLK 时钟相位 CPHA (Clock Phase) 的不同,SPI 区分出四种工作模式:
| SPI 工作模式 | CPOL | CPHA |
|---|---|---|
| SPI_MODE0 | 0 | 0 |
| SPI_MODE1 | 0 | 1 |
| SPI_MODE2 | 1 | 0 |
| SPI_MODE3 | 1 | 1 |
- CPOL=0: CLK 在空闲时刻保持低电平;CPOL=1: CLK 在空闲时刻保持高电平。
- CPHA=0: 在 CLK 的奇数边缘触发采样;CPHA=1: 在 CLK 的偶数边缘触发采样。
主机与从机需要工作在相同的模式下才可以正常通讯。
多样化的SPI接口
在标准 SPI 的基础上为适应不同的应用场景,逐渐衍生出很多类别的 SPI 接口:
-
3-line SPI:只有 3 根线(CLK, CSN, MOSI),数据收发共用一根线,为半双工通信。
-
Dual SPI:拓展了 MOSI 和 MISO 的用法,让它们工作在半双工、同向传输数据。MOSI 变成 IO0,MISO 变成 IO1,一个时钟周期内能传输 2 bits 数据。
-
Quad SPI:拓展了 WP 和 HOLD 的用法,WP 变成 IO2、HOLD 变成 IO3,同时拥有四根数据线,一个时钟周期内可以传输 4 个 bit。

芯片SPI资源总览
SPI类型说明
不同芯片提供不同的 SPI 外设类型,以下是各类型在全平台芯片中的分布及各芯片 IO 模式与 DMA 支持情况:
| 芯片 | PSPI | HSPI | LSPI | GSPI | SPI | 总数 | IO 模式支持 | DMA 支持 |
|---|---|---|---|---|---|---|---|---|
| TLSR921x/TLSR951x | ✅ 1个 | ✅ 1个 | — | — | — | 2 | 单/双(PSPI)、单/双/四(HSPI) | 是 |
| TLSR922x/TLSR952x | — | — | ✅ 1个 | ✅ 1个 | — | 2 | 单/双/四 | 是 |
| TL721x / TL751x | — | — | ✅ 1个 | ✅ 1个 | — | 2 | 单/双/四/三线 | 是 |
| TL321x / TL323x | — | — | — | ✅ 1个 | — | 1 | 单/双/四/三线 | 是 |
| TL322x | — | — | ✅ 1个 | ✅5个 | — | 6 | 单/双/四/三线 | 是 |
| TLSR820x/TLSR8373 | — | — | — | — | ✅ 1个 | 1 | 单/双/四/三线 | 是 |
| TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC123x | — | — | — | — | ✅ 1个 | 1 | 单 | 否 |
各类型含义:
| SPI 类型 | 全称 | 总线 | 说明 |
|---|---|---|---|
| SPI | Standard SPI | System Bus | TC 系列统一 SPI 模块,TLSR820x/TLSR8373系列 支持 Single/Dual/Quad/3-line,其余仅支持 Single |
| PSPI | APB SPI | APB | TLSR921x/TLSR951x系列 专属低速 SPI,仅支持 Single/Dual |
| HSPI | AHB SPI | AHB | TLSR921x/TLSR951x系列 专属高速 SPI,支持 Single/Dual/Quad |
| LSPI | LCD SPI | AHB | 支持 特定ramless 屏幕驱动(LUT 命令表、porch 时序、帧控制) |
| GSPI | General SPI | AHB | 通用/高速 SPI,支持 XIP,通常挂 AHB 总线 |
- TLSR921x/TLSR951x 是唯一使用
PSPI/HSPI命名的芯片 - TL322x 有 5 个 GSPI 实例,是 GSPI 最多的芯片
- TC 系列所有芯片均只有 1 个 SPI 模块,统一命名为
SPI,不区分 PSPI/HSPI/LSPI/GSPI
功能说明
接口命名规则
- spi 前缀:HSPI 和 PSPI / GSPI 和 LSPI 均可以使用的接口。
- pspi 前缀:仅供 PSPI 使用。
- hspi 前缀:仅供 HSPI 使用。
- gspi 前缀:仅供 GSPI 使用。
- lspi 前缀:仅供 LSPI 使用。
- dma 后缀:DMA 模式会用到的接口。
- plus 后缀:支持更丰富的读写模式和操作指令(TLSR921x/TLSR951x/TLSR922x/TLSR952x系列和TL系列通用)。
例如:spi_master_write_read_dma_plus 接口使用 DMA 通道,先写地址到 SPI Slave,再从 SPI Slave 对应地址读出数据。
关于 "read" 和 "write_read":
- 带有 "read" 字段的接口支持硬件自动发送 address 帧,适用于使能硬件 address 帧支持的应用场景。
- 带有 "write_read" 字段的接口在没有使能硬件 address 帧时使用,地址信息先要通过 "write" 的方式写 SPI Slave,然后才能读取对应地址的数据。
各芯片 SPI 模块能力对比
各芯片的 SPI 模块对常用协议帧的硬件支持情况如下(Y=支持,N=不支持):
| SPI 模块 | 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: 硬件 cmd 帧及其格式跟随 Dual/Quad I/O 编码
- address_en/address_fmt: 硬件 address 帧及其格式跟随 Dual/Quad I/O 编码
- TL721x/TL321x/TL322x/TL323x/TL751x 系列的 GSPI/LSPI 能力与 TLSR922x/TLSR952x 系列相同
- TLSR820x/TLSR8373 系列能力与 TLSR921x/TLSR951x HSPI 相同,其他 TC 芯片(TLSR825x/TLSR8359/TLSR827x/TLSR8355/TC321x/TC123X)不支持 cmd/addr 帧硬件功能。TC122x 无 SPI 模块,仅支持低层 MSPI 接口用于 Flash 操作。
Master模式
Master 模式下,Telink SPI 提供两类接口:
| 类别 | 后缀 | 适用场景 | 特点 |
|---|---|---|---|
| 标准 Master | 无 _plus |
简单数据收发,无需 cmd/addr 帧 | 接口简洁,仅传数据 |
| Plus Master | _plus |
Flash、PSRAM 等需要 cmd/addr/dummy 帧的外设 | 硬件自动生成 cmd/addr/dummy 帧 |
两类接口均支持轮询和 DMA 两种传输方式(_dma 后缀)。
-
标准 SPI Master
无
_plus后缀,适用于仅传输纯数据的场景(如两个 MCU 之间直接通信)。初始化:TLSR921x/TLSR951x 系列使用
spi_master_config(),TLSR922x/TLSR952x 系列和 TLxx 系列(TL321x/TL323x/TL322x/TL721x/TL751x及之后的TL系列)使用spi_master_config_plus()并关闭 cmd/addr。读写:Master 直接发送/接收数据,无需指定 cmd 或地址。接口通过
spi_sel_e参数选择 SPI 模块(TLSR921x/TLSR951x 系列为HSPI_MODULE/PSPI_MODULE,TLSR922x/TLSR952x 系列和 TLxx 系列为GSPI_MODULE/LSPI_MODULE)。 -
Plus Master(高级帧格式)
_plus后缀,硬件支持自动生成 cmd 帧、address 帧、dummy 帧(空周期),适配 Flash / PSRAM 等标准 SPI 外设的命令协议。数据帧格式为:【cmd】+【address】+【dummy】+ data(【】代表可选)。
Step 1 — 配置数据帧格式。TLSR921x/TLSR951x 系列使用独立的 hspi_config_st/pspi_config_t,TLSR922x/TLSR952x 系列和 TLxx 系列使用统一的 spi_wr_rd_config_t:
// TLSR921x/TLSR951x 系列
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 系列和 TLxx 系列
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);
各字段含义:
| 字段 | 说明 |
|---|---|
spi_io_mode |
I/O 模式:Single/Dual/Quad |
spi_dummy_cnt |
cmd 和 data 之间的空 cycle 数 |
spi_cmd_en |
是否由硬件发送 cmd 帧。关闭时 _plus 接口的 cmd 参数无效 |
spi_addr_en |
是否由硬件发送 address 帧 |
spi_addr_len |
address 帧字节数(1\~4) |
spi_cmd_fmt_en |
cmd 帧格式是否跟随 I/O 模式(否则始终 Single) |
spi_addr_fmt_en |
address 帧格式是否跟随 I/O 模式 |
Step 2 — 调用 Plus 读写接口。接口以 spi_master_{动作}_plus 命名,支持轮询和 DMA(_dma 后缀):
| 接口 | 用途 |
|---|---|
spi_master_write_plus |
发送 cmd + address + 写数据 |
spi_master_read_plus |
发送 cmd + address,接收数据 |
spi_master_write_read_plus |
发送 cmd + 地址(软件拼接),接收数据 |
差异: TLSR921x/TLSR951x 系列的 _plus 接口返回 void,TLSR922x/TLSR952x 系列和 TLxx 系列返回 drv_api_status_e。TL751x 的 cmd 参数为 unsigned short(16-bit),其余芯片为 unsigned char(8-bit)。
-
读写方式
读写方式用来表示操作是否需要 dummy(空周期)帧,指令操作到底是读还是写:
typedef enum {
SPI_MODE_WR_WRITE_ONLY = 1, // 写
SPI_MODE_WR_DUMMY_WRITE = 8, // dummy + 写
} spi_wr_tans_mode_e;
typedef enum {
SPI_MODE_RD_READ_ONLY = 2, // 读(必须使能 CmdEn)
SPI_MODE_RD_DUMMY_READ = 9, // dummy + 读
} spi_rd_tans_mode_e;
typedef enum {
SPI_MODE_WR_RD = 3, // 写 + 读(必须使能 CmdEn)
SPI_MODE_WR_DUMMY_RD = 5, // 写 + dummy + 读
} spi_wr_rd_tans_mode_e;
例如在读数据时,如果 SPI Slave 要求有 dummy 空闲帧,读数据应选择 SPI_MODE_RD_DUMMY_READ 方式。
Slave模式
-
HSPI/PSPI/GSPI/LSPI Slave
TLSR921x/TLSR951x 系列的 HSPI 作为 Slave 时支持 Single、Dual 和 Quad I/O 模式;PSPI 作为 Slave 时支持 Single、Dual I/O 模式。二者均会自动解析 cmd,但 Slave 数据接收和发送需要用软件进行操作。
TLSR922x/TLSR952x 系列和 TLxx 系列的 GSPI/LSPI Slave 均支持 Single/Dual/Quad,使用统一的
spi_slave_init(spi_sel, mode)初始化。
Slave 通信帧格式:cmd + dummy (n×clock) + data,Master 和 Slave 的 I/O 模式、dummy clock 数需保持一致。
| I/O 模式 | 写/读 | 数据线映射 |
|---|---|---|
| Single | MOSI (IO0) 发 cmd + data,MISO (IO1) 仅用于读数据 | 1 bit/cycle |
| Dual | IO0/IO1 同时收/发,每 2 bit 拆到两根线上 | 2 bit/cycle |
| Quad | IO0\~IO3 同时收/发(需配置 WP/IO2 和 HOLD/IO3) | 4 bit/cycle |
HSPI/PSPI Slave 操作指令(所有系列兼容):
// Write cmd(部分示例)
SPI_WRITE_DATA_SINGLE_CMD = 0x51 // Single 写
SPI_WRITE_DATA_DUAL_CMD = 0x52 // Dual 写
HSPI_WRITE_DATA_QUAD_CMD = 0x54 // Quad 写(仅 HSPI)
// Read cmd(部分示例)
SPI_READ_DATA_SINGLE_CMD = 0x0B // Single 读
SPI_READ_DATA_DUAL_CMD = 0x0C // Dual 读
HSPI_READ_DATA_QUAD_CMD = 0x0E // Quad 读(仅 HSPI)
SPI 前缀的指令 HSPI 和 PSPI 通用,HSPI 前缀的指令仅 HSPI 可用。
-
SPI Slave 模块(专用硬件 Slave)
所有 TL 系列芯片都包含一个独立的 SPI Slave 模块(区别于 GSPI/LSPI Slave),硬件自动解析读写命令并操作对应地址,无需软件干预。仅支持 Single/Dual I/O。
注意
TLSR922x/TLSR952x 系列和 TLSR922x/TLSR952x 系列的 spi_slave_set_pin() 无参数(固定引脚 TLSR922x/TLSR952x: PA1\~PA4,TLSR922x/TLSR952x: PC0\~PC3);其他芯片通过 spi_slave_set_pin(sspi_pin_config_t *) 灵活配置。
SSPI 模块与 GSPI/LSPI Slave 对比:
| 类型 | 协议 | 数据格式 | 特点 |
|---|---|---|---|
| GSPI/LSPI Slave | cmd + dummy + data | 灵活,软件控制 | 支持 Quad,引脚可配 |
| SPI Slave 模块 (SSPI) | cmd + addr(32bit h -> l) + data | 硬件自动解析 | 仅 Single/Dual,简单场景 |
SSPI 模块的地址排列为高字节在前(addr(32bit) high -> low),与 GSPI/LSPI Slave 的 cmd+dummy+data 格式不同。
时钟设置
-
时钟源
Master 时钟源因芯片/模块而异:
| 芯片 / 模块 | 时钟源 | 配置方式 |
|---|---|---|
| TLSR921x/TLSR951x HSPI | hclk | 手动计算分频:sys_clk.hclk * 1000000 / (2 × SPI_CLK) - 1 |
| TLSR921x/TLSR951x PSPI | pclk | 手动计算分频:sys_clk.pclk * 1000000 / (2 × SPI_CLK) - 1 |
| TL751x 系列 | 可选 SRC_CLK_XTAL_48M 等 |
手动指定 |
| TLSR922x/TLSR952x 和其他 TLxx 系列 | PLL 时钟,自动选择 | sys_clk.pll_clk * 1000000 / SPI_CLK |
初始化示例:
spi_master_init(GSPI_MODULE, sys_clk.pll_clk * 1000000, 3000000);
SPI_CLK 超出配置范围可能导致通信失败。支持的最大 SPI 时钟因芯片而异,以头文件注释为准。
-
Slave 时钟
Slave 时钟由 Master 输入,无需自行配置。约束条件:
F_Master ≤ F_source_slave ÷ 4(F_source_slave为 Slave 芯片内部时钟源频率),否则 Slave 可能无法正确采样。
中断
SoC 支持多种 SPI 中断类型,使用时可以根据应用场景灵活配置。
| 模式 | 中断 | 说明 | 异常中断 | 需手动清除 | 产生方 |
|---|---|---|---|---|---|
| 非DMA | SPI_RXF_OR_INT | RX FIFO over run:接收时程序读取不够快,RX FIFO 被覆盖 | Y | Y | Slave |
| 非DMA | SPI_TXF_UR_INT | TX FIFO under run:发送时程序写入不够快,发送间断 | Y | Y | Slave |
| 非DMA | SPI_RXF_INT | RX FIFO 阈值中断:RX FIFO 数据达到/超过阈值 | N | Y | Master/Slave |
| 非DMA | SPI_TXF_INT | TX FIFO 阈值中断:TX FIFO 数据小于/达到阈值 | N | Y | Master/Slave |
| DMA/非DMA | SPI_END_INT | 数据传输结束中断,一笔数据传输完成 | N | Y | Master/Slave |
| DMA/非DMA | SPI_SLV_CMD_INT | Slave 模式时,每收到 1Byte command 触发 | N | Y | Slave |
注意
TLSR921x/TLSR951x 系列的中断状态枚举位偏移与 TLSR922x/TLSR952x 和 TLxx 系列 不同:TLSR921x/TLSR951x 系列中 SPI_END_INT = BIT(6),TLSR922x/TLSR952x 和 TLxx 系列中 SPI_END_INT = BIT(4)。跨芯片移植代码时务必检查中断状态位定义。
中断使能与状态查询:
// 使能中断
spi_set_irq_mask(SPI_MODULE_SEL, SPI_END_INT_EN | SPI_RXFIFO_INT_EN);
// 查询中断状态
u8 status = spi_get_irq_status(SPI_MODULE_SEL);
if (status & SPI_END_INT) {
spi_clr_irq_status(SPI_MODULE_SEL, SPI_END_INT);
// 处理传输完成
}
DMA 模式
TLSR921x/TLSR951x系列 使用宏定义来选择 DMA 通道:
#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 和 TLxx 系列使用 DMA 配置结构体:
spi_set_tx_dma_config(GSPI_MODULE, DMA2);
spi_set_master_rx_dma_config(GSPI_MODULE, DMA3);
DMA 模式下判断是否发送、接收完数据的方式:
- 查询方式:
spi_is_busy(spi_sel)— 返回 busy 状态 - 中断方式:
spi_set_irq_mask(SPI_MODULE_SEL, SPI_END_INT_EN)— 使能 END 中断
DMA 使用注意事项(全平台通用):
SPI_END_INT中断不代表数据传输完毕(只表示 FIFO 数据传输结束,CSN 并没有拉高),产生SPI_END_INT中断后,再查询 busy 信号,直到 IDLE 才代表结束。- 使用 DMA 进行传输时,发送/接收的结构体或数组要进行四字节对齐(Demo 中通过
__attribute__((aligned(4)))体现)。 - 使用 DMA 接收 SPI 数据到 Buffer 时,目标地址的 Buffer 大小一定要是 4 的倍数。原因是每次 DMA 都会送到 Buffer 4 个 Bytes,即使配置读取长度不够 4,也会往目的地址写 4 个 Bytes。例如定义数组 Buffer 大小为 5 Bytes,配置 DMA 从 SPI 读取 5 Bytes 到 Buffer,DMA 实际传送 2 次共 8 个 Bytes 到 Buffer,多出的 3 个 Bytes 可能覆盖其他变量。这时应配置数组大小为 8 Bytes。
- TLSR922x/TLSR952x 和 TLxx 系列中 DMA 配置分为
tx/master_rx/slave_rx三组独立配置,TLSR921x/TLSR951x系列使用tx/rx两组配置。
3-line 模式
TLSR921x/TLSR951x 系列的 HSPI/PSPI Master/Slave 以及 TLSR922x/TLSR952x 和 TLxx 系列的 GSPI/LSPI 均支持 3-line 模式。
注意
TLSR922x/TLSR952x 系列仅有 GSPI 支持 3-line,LSPI 不支持。
调用的接口为:
void spi_set_3line_mode(spi_sel_e spi_sel)
3-line 模式的读写指令兼容 HSPI/PSPI/GSPI/LSPI 的 SINGLE_CMD。
多 SPI Slave 结构
对于多 SPI Slave 的应用场景,可以为每个 Slave 都分配一个 CSN 引脚。一笔数据传输完成,CSN 会拉高,这时可以切换 CSN,达到切换 Slave 的效果。
TLSR921x/TLSR951x 系列 HSPI Master 调用接口:
void hspi_cs_pin_dis(hspi_csn_pin_def_e pin);
void hspi_cs_pin_en(hspi_csn_pin_def_e pin);
TLSR922x/TLSR952x 和 TLxx 系列 GSPI Master 支持多个 CSN(GSPI_XIP0\~3),通过 XIP 配置或直接操作 GPIO 实现多 Slave。
XIP 模式
XIP(eXecute In Place)即芯片内执行,指应用程序可以直接在外置存储设备内取指、译码、执行。通过 XIP 可以扩展 SoC 的地址空间至外置存储设备。
- XIP 支持的芯片
| 芯片 | GSPI XIP | LSPI XIP | XIP 通道数 |
|---|---|---|---|
| TLSR921x/TLSR951x HSPI | ✅ | — | 1 |
| TLSR922x/TLSR952x | ✅ | — | 4 (XIP0\~3) |
| TL721x | ✅ | ✅ | 4 |
| TL322x | ✅ | ✅ | 4 |
| TL321x | ✅ | ✅ | 4 |
| TL323x | ✅ | ✅ | 4 |
- TLSR921x/TLSR951x 系列 XIP 配置
hspi_xip_seq_mode_en(); // 使能 sequential mode
hspi_xip_page_size(4); // 设置 page size
hspi_xip_en(); // 使能 XIP
TLSR921x/TLSR951x 系列的 seq_mode(sequential mode)表示一种间隔收发模式,把数据分成一个个 \(2^{page\_size}\) Bytes 大小的块进行间隔收发,每收发完一块就会拉高一次 CS。
发送指令与数据读写:
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 系列 XIP 片内运行程序 — 通过下面两句指令切换 PC 指针到 XIP 设备对应地址(基地址 0x1000000 + 相对地址 0x00):
__asm__("li t0,0x1000000");
__asm__("jarr t0");
- TLSR922x/TLSR952x 和 TLxx 系列 XIP 配置
TLSR922x/TLSR952x 和 TLxx 系列使用统一的 spi_xip_config_t 结构体,支持更丰富的配置项:
spi_xip_config_t xip_config = {
// 读配置
.spi_xip_rd_io_mode = SPI_QUAD_MODE,
.spi_xip_rd_addr_len = 2, // 3 字节地址 (2 = 3 - 1)
.spi_xip_rd_addr_en = 1,
.spi_xip_rd_cmd_en = 1,
.spi_xip_rd_dummy_cnt = 3, // 4 个 dummy cycle (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,
// ... 写配置类似
.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);
TLSR922x/TLSR952x 和 TLxx 系列的 GSPI XIP 基地址为 0x88000000,支持 4 个 XIP 区域:
- XIP0: 0x88000000 \~ 0x88ffff00
- XIP1: 0x89000000 \~ 0x89ffff00
- XIP2: 0x8a000000 \~ 0x8affff00
- XIP3: 0x8b000000 \~ 0x8bffff00
Demo 示例
TL 系列 Demo
| Demo 版本 | 路径 | 适用芯片 |
|---|---|---|
| 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 上限 8 cycles;V1.1 dummy 上限 TLSR922x/TLSR952x 为 32 cycles,其他为 256 cycles。
TC 系列 Demo
TC 系列 SDK 提供两套 SPI Demo:
| Demo 版本 | 路径 | 适用芯片 |
|---|---|---|
| V1.0 | tc_platform_sdk/demo/vendor/SPI_Demo/SPI_V1.0/ |
TLSR820x/TLSR8373 / TC321x / TC123x |
| V1.1 | tc_platform_sdk/demo/vendor/SPI_Demo/SPI_V1.1/ |
TLSR820x/TLSR8373 |
快速上手指南
TLSR922x/TLSR952x/TL721x/TL751x/TL322x/TL323x
#include "spi.h"
// Step 1: 配置 SPI 引脚
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: 配置 Master 模式参数
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: 初始化 SPI 时钟(sys_clk=48M, 目标 SPI_CLK=3M)
spi_master_init(GSPI_MODULE, sys_clk.freq * 1000000, 3000000);
// Step 4: 发送数据(cmd_en=0 时 cmd 参数无效,可填 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 使用不同的命名: 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,
};
// 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 系列 SPI
TC 系列芯片基于 TC32 内核,使用 tc_platform_sdk。支持 SPI 的 TC 芯片均只有 1 个 SPI 模块,API 风格与 TL 系列有较大差异——接口更简洁,无 _plus 后缀分层。
Master 模式
TC 系列 SPI Master 提供两类 API:
- 通用 API(所有 TC 芯片共用):
spi_master_init()、spi_write()、spi_read()、spi_master_gpio_set() - 高级 API(仅 TLSR820x/TLSR8373):
spi_config_t配置结构体,支持 cmd/addr/dummy 帧格式及 Dual/Quad 模式
- 引脚配置
TC 系列不同芯片的引脚配置方式不同:
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 — 固定引脚组选择:
// 定义固定引脚组(枚举型)
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 引脚配置
spi_master_gpio_set(SPI_GPIO_GROUP_A2A3A4D6);
// 额外 CS 引脚选择(如果需要使用外部 CS)
spi_masterCSpin_select(GPIO_PD2);
TLSR827x/TLSR8355 — 独立引脚选择(TLSR827x/TLSR8355 额外支持):
// 四个引脚独立选择,不限于固定的 group
spi_master_gpio_set(SPI_GPIO_SCL_A4, SPI_GPIO_CS_D6,
SPI_GPIO_SDO_A2, SPI_GPIO_SDI_A3);
TLSR820x/TLSR8373 / TC321x / TC123X — 结构体引脚配置:
// TLSR820x/TLSR8373 包含 WP/HOLD 引脚(用于 Quad 模式)
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
.spi_hold_io3_pin = GPIO_PB4, // 仅 TLSR820x/TLSR8373
};
spi_set_pin(&pin_config);
// TC321x/TC123X 无 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);
- 时钟与模式初始化
TC 系列使用统一的 spi_master_init() 函数,时钟计算公式为:
SPI Clock = System Clock / ((DivClock + 1) × 2)
系统时钟通常为 24MHz,常用时钟对应的分频系数:
| SPI 频率 | DivClock | 实际频率(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 |
注意
TC321x/TC123X 的 200K/250K 分频系数与 TLSR825x/TLSR8359 / TLSR827x/TLSR8355 略有不同(0x3b/0x2f vs 0x3c/0x2e),但均可使用 SPI_24M_ClkTypeDef 枚举直接选择。
初始化代码示例:
// 以 1MHz 时钟、MODE0 初始化 SPI Master
spi_master_init(SPI_CLK_1M, SPI_MODE0);
// 也支持直接传分频系数
spi_master_init(0x0b, SPI_MODE0); // DivClock=0x0b → SPI_CLK = 24M/(12×2) = 1MHz
- 数据读写
TC 系列 SPI 的读写接口以"先发命令前缀、再读写数据"的方式工作。写数据时先发 cmd 序列,再发数据;读数据时先发 cmd 序列,再接收数据。
TLSR825x/TLSR8359 / TLSR827x/TLSR8355(需要传递 CS 引脚参数):
// 写:先发送 cmd[0..CmdLen-1],再发送 Data[0..DataLen-1]
void spi_write(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen,
GPIO_PinTypeDef CSPin);
// 读:先发送 cmd[0..CmdLen-1],再接收 DataLen 字节到 Data[]
void spi_read(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen,
GPIO_PinTypeDef CSPin);
// 使用示例
unsigned char cmd = 0x02; // 写命令
unsigned char tx_data[] = {0x01, 0x02};
spi_write(&cmd, 1, tx_data, 2, GPIO_PD2);
unsigned char rd_cmd = 0x03; // 读命令
unsigned char rx_data[4];
spi_read(&rd_cmd, 1, rx_data, 4, GPIO_PD2);
TLSR820x/TLSR8373 / TC321x / TC123x(不需要 CS 引脚参数,使用硬件 CS):
// 写(无 CSPin 参数)
void spi_write(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen);
// 读(无 CSPin 参数)
void spi_read(unsigned char *Cmd, int CmdLen,
unsigned char *Data, int DataLen);
// 使用示例
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);
注意
TLSR825x/TLSR8359 / TLSR827x/TLSR8355需要手动指定每次传输的 CS 引脚,适合多从机场景;TLSR820x/TLSR8373及之后TCxx系列芯片使用 spi_masterCSpin_select() 预先选定硬件 CS 引脚,写/读时无需再传入 CS 参数。
- TLSR820x/TLSR8373 高级帧格式(plus 模式)
TLSR820x/TLSR8373 支持与 TL 系列类似的 cmd/addr/dummy 帧配置,可适配带命令帧协议的 SPI 外设(如 Flash、PSRAM 等)。
Step 1 — 配置 spi_config_t 结构体:
typedef struct {
spi_io_mode_e spi_io_mode; // 接口模式: SPI_SINGLE_MODE / SPI_DUAL_MODE / SPI_QUAD_MODE / SPI_3LINE_MODE
unsigned char spi_dummy_cnt; // dummy cycle 数量
unsigned char spi_cmd_en; // 使能 cmd 帧
unsigned char spi_addr_en; // 使能 address 帧
unsigned char spi_addr_len; // address 长度(字节数)
unsigned char spi_cmd_fmt_en; // cmd 帧格式跟随 Dual/Quad
unsigned char spi_addr_fmt_en; // address 帧格式跟随 Dual/Quad
} spi_config_t;
Step 2 — 配置示例:
// 配置为 Quad I/O 模式,6 个 dummy cycle,使能 cmd+addr 帧
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 字节地址
.spi_cmd_fmt_en = 0, // cmd 帧保持 Single 模式
.spi_addr_fmt_en = 1, // address 帧跟随 Quad 模式
};
Step 3 — 读写方式枚举(与 TL 系列一致):
typedef enum {
SPI_MODE_WR_WRITE_ONLY = 1, // 纯写
SPI_MODE_WR_DUMMY_WRITE = 8, // dummy + 写
} spi_wr_tans_mode_e;
typedef enum {
SPI_MODE_RD_READ_ONLY = 2, // 纯读(需使能 CmdEn)
SPI_MODE_RD_DUMMY_READ = 9, // dummy + 读
} spi_rd_tans_mode_e;
typedef enum {
SPI_MODE_WR_RD = 3, // 写 + 读(需使能 CmdEn)
SPI_MODE_WR_DUMMY_RD = 5, // 写 + dummy + 读
} spi_wr_rd_tans_mode_e;
Step 4 — 完整初始化与使用示例:
// 引脚配置
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);
// 主时钟初始化
spi_master_init(SPI_CLK_2M, SPI_MODE0);
// 配置高级帧格式
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,
};
// 将配置写入寄存器(通过各字段对应的寄存器位操作函数)
spi_quad_mode_en();
// ... 其他配置 ...
// 数据传输
unsigned char cmd = 0xEB; // Quad I/O Read 命令
unsigned int addr = 0x000000;
unsigned char rx_buf[64];
// TLSR820x/TLSR8373 使用 DMA 读写接口
spi_master_read_dma_plus(/*...*/);
TLSR820x/TLSR8373 还提供 DMA 传输支持(spi_master_write_dma_plus、spi_master_read_dma_plus 等),中断支持(SPI_RXFIFO_OR_INT_EN 等),以及完整的 spi_tans_mode_e 枚举(共 10 种传输模式)。详细用法请参考 tc_platform_sdk/chip/b80/drivers/spi.h。
Slave模式
TC 系列所有芯片均支持 SPI Slave 模式:
// 初始化 Slave 模式
spi_slave_init(DivClock, SPI_ModeTypeDef Mode);
// Slave 引脚配置(TLSR825x/TLSR8359/TLSR827x/TLSR8355)
spi_slave_gpio_set(SPI_GPIO_GROUP_A2A3A4D6);
// Slave 引脚配置(TLSR820x/TLSR8373/TC321x 等)
// 使用 spi_set_pin() 即可,与 Master 共用同一配置函数
TLSR820x/TLSR8373 Slave 额外支持:
- 通过
spi_cmd_e枚举识别 Master 发来的命令:SPI_READ_DATA_SINGLE_CMD(0x0B)、SPI_READ_DATA_DUAL_CMD(0x0C)、SPI_READ_DATA_QUAD_CMD(0x0E)、SPI_WRITE_DATA_SINGLE_CMD(0x51) 等 - 获取 Slave 端收到的命令:
spi_slave_get_cmd() - 设置 Slave 端准备发送给 Master 的命令:
spi_master_set_cmd()
// TLSR820x/TLSR8373 Slave 端获取 Master 发来的指令
unsigned char cmd = spi_slave_get_cmd();
if (cmd == SPI_WRITE_DATA_SINGLE_CMD) {
// Master 要写数据
} else if (cmd == SPI_READ_DATA_SINGLE_CMD) {
// Master 要读数据
}
注意
TC 系列没有独立的 SPI Slave 模块(不同于 TL 系列的 SSPI 模块),Slave 功能直接通过主 SPI 模块配置为 Slave 模式实现。
常见注意事项
DMA 传输
- 发送/接收缓冲区必须四字节对齐。使用
__attribute__((aligned(4)))修饰数组或结构体。 - DMA 接收 Buffer 大小必须是 4 的倍数。DMA 每次写入 4 Bytes,即使配置的读取长度不足 4,仍会写入完整 4 Bytes。例如读取 5 Bytes 应定义 Buffer 大小为 8 Bytes,否则多余字节会覆盖相邻变量。
SPI_END_INT中断 ≠ 传输结束。该中断仅表示 FIFO 数据传完,CSN 尚未拉高。中断后需再查询spi_is_busy(),直到返回 IDLE 才算真正结束。- TLSR922x/TLSR952x 和 TLxx 系列 的 DMA 配置分
tx/master_rx/slave_rx三组独立配置,TLSR921x/TLSR951x 使用tx/rx两组。
Slave 时钟约束
Slave 的时钟由 Master 输入,需满足:F_Master ≤ F_source_slave ÷ 4,否则 Slave 端可能无法正确采样。其中 F_source_slave 为 Slave 芯片自身的内部时钟源频率。详情请参考时钟设置。
Slave 类型选择(TL 系列)
TL 系列有两种 Slave 模式:
| 类型 | 适用场景 | 特点 |
|---|---|---|
| GSPI/LSPI Slave | 需要 Quad I/O、灵活引脚配置 | 软件收发数据,需手动处理协议 |
| SPI Slave 模块(SSPI) | 简单地址-数据读写 | 硬件自动解析读写命令,无需软件干预 |
Master/Slave 对测接线要求
-
接线方式
TLSR825x/TLSR8359 / TLSR827x/TLSR8355 / TC321x / TC123X,在 Master 与 Slave 对测场景中,data飞线交叉连接(参考
spi_set_pin()注释): -
上电顺序
必须先启动 Slave,再启动 Master。Slave 需先完成初始化并进入等待状态,之后 Master 再发起读写时序。否则 Master 发送数据时 Slave 尚未就绪,无法响应,导致收发数据对比出错。
-
飞线与共地要求
- 飞线尽量短:SPI 为高速信号,飞线过长可能导致数据采样错误。
- Master 与 Slave 必须共地:两块芯片的 GND 需通过飞线连接在一起,确保电平参考一致,否则信号无法正确识别。
QDEC
简介
正交解码器 (Quadrature Decoder, QDEC) 硬件模块主要用于解析正交编码器(如鼠标滚轮、电机测速盘)产生的输入信号。通过实时检测 Channel A 和 Channel B 两个相位的上升沿和下降沿信号,硬件会自动累加或递减计数值,从而精准计算出设备的旋转方向与移动步数。
计步工作模式
驱动支持两种工作模式,可通过 API 配置以适应不同的精度需求:
- 普通模式 (COMMON_MODE)
- 触发条件 :仅当从 A/B 两相信号中检测到相同的上升沿或下降沿时,QDEC 计数器才会加 1 或减 1。
- 双倍精度模式 (DOUBLE_ACCURACY_MODE)
- 触发条件 :在 A/B 两相信号的每一个上升沿和下降沿,计数器都会响应并加 1 或减 1。
- 效果 :物理轮子每发生一次完整的步进滚动 (One wheel rolling),计数值将改变 2,提供更高的分辨率。
全系列芯片引脚支持矩阵
不同芯片底层使用的枚举类型 (Pin Enum) 和支持的物理引脚有明确差异。以下是根据底层驱动提取的完整引脚映射表:
| 芯片型号 | 引脚配置方式 | 支持的输入引脚枚举值 (完整列表) |
|---|---|---|
| TLSR820x/TLSR8373 | 分立 A/B 相 | A相: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A B相: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR825x/TLSR8359 | 分立 A/B 相 | A相: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A B相: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR827x/TLSR8355 | 分立 A/B 相 | A相: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A B相: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR8298 | 分立 A/B 相 | A相: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A B相: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TLSR922x/TLSR952x | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TC321X | 分立 A/B 相 | A相: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A B相: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TL321X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL721X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TC122X | 分立 A/B 相 | A相: PA0A, PA4A, PA5A, PA6A, PA7A, PB0A, PB1A, PB2A B相: PA0B, PA4B, PA5B, PA6B, PA7B, PB0B, PB1B, PB2B |
| TC123X | 分立 A/B 相 | A相: PA0A, PA4A, PA5A, PA6A, PA7A, PB0A, PB1A, PB2A B相: PA0B, PA4B, PA5B, PA6B, PA7B, PB0B, PB1B, PB2B |
| TL322X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL323X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL521X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL523X | 分立 A/B 相 | A相: PA2A, PA3A, PB6A, PB7A, PC2A, PC3A, PD6A, PD7A B相: PA2B, PA3B, PB6B, PB7B, PC2B, PC3B, PD6B, PD7B |
| TL751X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
| TL753X | 统一通道枚举 | PA2, PA3, PB6, PB7, PC2, PC3, PD6, PD7 |
核心 API 功能解析
void qdec_clk_en(void)- 功能 :初始化 32 kHz 外部/内部时钟(
rc_32k_cal等),并开启 QDEC 模块时钟门控。新架构芯片会在此函数内同步调用复位操作。
- 功能 :初始化 32 kHz 外部/内部时钟(
void qdec_reset(void)- 功能 :通过操作相关的复位寄存器(如
reg_rst0或reg_rst3)将 QDEC 内部逻辑和计数器清零。
- 功能 :通过操作相关的复位寄存器(如
void qdec_set_pin(...)- 功能 :将指定的 GPIO 映射到解码器的输入端。需依据上述引脚支持矩阵传入对应的枚举值。
void qdec_set_mode(qdec_mode_e mode)- 功能 :设置计数触发策略,可选
COMMON_MODE或DOUBLE_ACCURACY_MODE。
- 功能 :设置计数触发策略,可选
void qdec_set_debouncing(qdec_thrsh_e thrsh)- 功能 :配置硬件滤波阈值,滤除机械开关的抖动杂波。小于阈值宽度的电平变化将被丢弃。参数枚举涵盖 187.5 us 至 24000 us 的 8 个档位。
signed char qdec_get_count_value(void)- 功能 :向
reg_qdec_load发送重载指令,并读取返回的差值。正负号表示方向,数值代表自上次读取以来的累计步数。读后硬件差值状态会自动重置。
- 功能 :向
驱动配置与执行流程
以下展示了在裸机程序或 RTOS 任务中初始化并轮询 QDEC 的标准业务流:
graph TD
Start([系统启动 / 上电复位]) --> InitGPIO[配置外设通用 GPIO<br>使能 PB6 / PB7 输入]
subgraph user_init [QDEC 硬件初始化]
InitGPIO --> EnableClk[使能 QDEC 时钟<br>qdec_clk_en]
EnableClk --> SetMode[设置双倍精度模式<br>qdec_set_mode]
SetMode --> SetPin[映射 A/B 相到 PB6/PB7<br>qdec_set_pin]
SetPin --> SetDebounce[配置硬件消抖阈值<br>qdec_set_debouncing]
end
SetDebounce --> MainLoop((进入主循环))
subgraph main_loop [业务主循环]
MainLoop --> ReadValue[读取当前周期计数值<br>qdec_get_count_value]
ReadValue --> CalcTotal[累加计算总步数<br>total_count += qdec_count]
CalcTotal --> PrintOut[串口打印计数值<br>printf]
PrintOut --> Delay[阻塞延时 1000ms<br>delay_ms]
end
Delay --> MainLoop
JTAG调试指南
简介
JTAG(Joint Test Action Group),是一种硬件调试和测试技术,常被用于在集成电路中诊断和调试问题。JTAG的正式名称为IEEE 1149.1标准,是一种通过扫描链(scan chain)实现的测试方法,该方法可以在不破坏芯片的情况下,对集成电路进行测试和调试。JTAG技术广泛应用于数字集成电路、嵌入式系统和电路板等硬件开发领域。
在JTAG中,芯片上的所有测试点都被连接到一个扫描链中,这个扫描链允许对芯片进行"非入侵式"的测试和调试。通过扫描链,可以向芯片中加载测试模式或者读取芯片中的状态。除了测试和调试,JTAG还可以用于烧写程序和配置数据到芯片中,从而方便开发人员进行硬件和软件的联合调试和测试。JTAG接口通常由调试工具连接到开发板上,以便开发人员可以远程调试和测试集成电路。
ICEman可以理解为gdbserver,是gdb客户端和板子通信的一个中间件,即是下图中的openocd。

快速上手
如果是第一次使用 JTAG 调试,按照以下步骤可以快速跑通整个流程:
-
确认硬件连接 — 将 JTAG 调试器的 TDI、TCK、TMS、TDO 与板子对应引脚连接,板子供电后 JTAG 工具应只亮蓝灯。详情请参考章节硬件连接与驱动安装。
-
安装驱动 — 确保 JTAG 驱动已安装(
BDT/ice/libusb-AICE-driver/Install_driver.exe)。详情请参考章节硬件连接与驱动安装。 -
代码中添加引脚使能 — 在
gpio_init()调用之后,根据模式添加jtag_set_pin_en()(四线JTAG)或sdp_set_pin_en()(两线SDP)。详情请参考章节软件与IDE配置。 -
启动 ICEman — 四线执行
./ICEman -Z v5,两线执行./ICEman -Z v5 -I aice_sdp.cfg,记录输出的 TCP 端口号(通常为 1111)。详情请参考章节软件与IDE配置。 -
配置 IDE Debug — 新建 C/C++ Remote Application,选择 ELF 文件,GDB 设为
riscv32-elf-gdb,TCP 端口填入 ICEman 输出的端口号。详情请参考章节IDE Debug配置。 -
开始调试 — 点击 Debug 按钮,设置断点,使用 Step Over / Resume 等功能进行调试。详情请参考章节断点和单步调试。
提示
如果中途遇到问题,请参考常见问题排查 章节查找解决方案。
快速连接指南
硬件连接与驱动安装
-
板子接线
拿到板子后找到对应的原理图,通过切换开关切换两线/四线模式,原理上是上电自动检测PB0(不同芯片有所差异,以原理图为准)的默认电平,检测到GND是四线,3v3是两线。
将 JTAG 调试器的 TDI、TCK(时钟源hclk)、TMS、TDO 与板子对应引脚连接。连接好信号线后,给芯片上电,可以使用小黑盒3v3连接板子VBAT,有USB模块的可以使用5V连接VBUS。芯片输出一个电压给JTAG作为参考电平,将板子的3v3输出电压连接JTAG盒子REF,提供 JTAG 信号TCK/TMS/TDI/TDO应该工作在3.3V,避免损坏IO或者JTAG通信失败。
如果此时的JTAG工具只亮蓝灯,表明接线正常;如果有红灯亮起,说明供电有问题,或者信号线连接错误,需要排查接线和供电。
-
JTAG驱动安装
BDT/ice/libusb-AICE-driver/Install_driver.exe
安装IDE的同时默认此驱动会一起安装,不安装可能会出现红灯亮的情况,使用之前务必安装此驱动。

软件与IDE配置
-
JTAG引脚初始化
在程序执行初始化时会调用
gpio_shutdown(GPIO_ALL)/gpio_init(),该接口会将JTAG IO配置为GPIO模式,导致JTAG连接失效。因此需要在初始化完成后,重新使能JTAG引脚:- 四线模式:调用
jtag_set_pin_en()对 JTAG 引脚进行初始化; - 两线模式:调用
sdp_set_pin_en()对 SDP 引脚进行初始化。
- 四线模式:调用
注意"
如果未调用上述接口,JTAG调试器将无法正常连接目标芯片。
-
连接指令
启动ICEman,四线模式执行:
./ICEman -Z v5,两线模式执行:./ICEman -Z v5 -I aice_sdp.cfg

ICEman工具(基于OpenOCD)已经成功初始化,并显示已准备好使用。监听端口、JTAG频率、目标核心的信息等列出,表明ICEman与AICE-MINI+调试器之间的通信已经建立,且ICEman已经能够识别到目标设备(至少从JTAG接口的角度看)。1111就是需要的TCP端口号,调试过程中需要保证ICEman在运行,不能关闭终端。
ICEman位于
$IoTStudio/RDS/V5.1.1/ice/目录下,也可以自行打开ICEman。
特殊连接说明
对于支持 32k_watchdog 周期性复位(循环复位)的芯片,连接JTAG时需要关闭32k_watchdog,否则复位会将ICEman写入RAM(ICEman 本身连接时不会写指令,执行命令时,只能写 RAM)的命令清掉,导致JTAG连接后又断联。
关闭32k_watchdog的操作步骤如下:
- 下载 Telink IoT Studio 最新的安装包,解压会有以下内容:

执行 TelinkIoTStudio_V2025.2.exe 进行安装。安装完成后,必须执行 TelinkIoTStudio Updater.exe,将 IoT Studio 中的组件更新到最新版本。
确保完成更新后,若要使用 JTAG 功能,在 IoT Studio 中打开 ICEman 终端(启动之后,其他的操作和其它芯片相同):

在 Shell 中执行:
./start_ICEman.sh
即可启动 ICEman。
JTAG烧录
有两种JTAG烧录方式:Telink IoT Studio IDE和Andes IDE。
(1) Telink IoT Studio IDE
界面如下:

(2) Andes IDE
界面如下:

-
1为软件下载路径;
-
2为所要下载进去的BIN文件;
-
3为Flash的偏移地址;
-
4为下载时所要连接的地址和端口号,如果填错或不勾选复选框,那么下载代码会出错。
注意
通过JTAG烧录Flash时,需要先确认JTAG正确连接,如果不成功,可尝试将Target设置为ICE和勾选上SDP(两线)。
D25的调试
IDE Debug配置
- ICEman启动之后,点击工具栏
Debug图标下拉箭头,选择Debug Configuration。

- 选择C/C++ Remote Application,配置
Main选项卡,选择编译出的.elf文件作为gdb的输入文件,并选择Disable auto build。

- 点击下方的
Select other,选择Manual(默认为Automatic)。

- 点击
Debugger选项卡,配置相应的debugger,这里是riscv32-elf-gdb,注意不要勾选Stop on startup at。

- 点击
Connect子选项卡,将TCP端口号填写为ICEman得到的1111。

- 配置完成,点击
Debug按钮,添加断点即可开始调试。
在Startup界面中的Set breakpoint at选项可以设置在启动调试时第一个断点的位置,可以设置成main,即启动调试后会默认停留在main函数入口,完成以上配置后点击右下角的Debug按钮即可开始调试,下次进行调试时该配置会以你的命名形式存在于调试按钮下,直接点击即可再次开始调试。


断点
目前TLSR9系列SoC支持最多两个硬件断点,当你发现你的程序运行的起始地址是0x20000000时,说明运行在flash里面,需要使用硬件断点。而step in或step over等命令本身就会用到一个断点,所以用户调试时只能自定义一个断点,否则就会发生异常。这也是不建议勾选Stop on startup at的原因,因为这个选项实际上的功能是设置一个断点。
当调试时发生cannot access memory at address xx异常时,可以使用 info br命令检查一下断点数量。
进入调试模式后,在需要程序停住的位置双击行号左侧即可设置断点,断点设置后的状态如下:

注意
-
断点前必须存在对勾,否则断点无效。
-
程序停的位置可能在设置断点位置后的一两行(属于正常现象)。
点击Resume按钮,程序会执行到断点处,再次点击,程序会执行到下一断点处。

链接源文件
断点调试的过程中,可能会发生找不到源文件的错误,此时点击Edit Source Path...,如下图:

点击Add,选择Path Mapping,编辑如下:


左边需要手动输入,右边则是选择路径。
原理是将IoT Studio中的路径正确映射到本地路径,所以最简单的映射方式也可以是将 /cygdrive/c/直接映射到 C:\(因为此例中SDK放在C盘)。
上述配置完成后,即可正常调试。
单步调试
-
Step Into:单步调试遇到子函数进入子函数执行
-
Step Over:单步调试遇到子函数不进入执行
注意
若Step Into调试出错(封库等原因),建议使用Step Over或者打断点的方式调试。


调试工具栏与界面介绍

调试工具栏各功能如下:
(1) Resume:程序在断点暂停后,从此处继续全速运行,直到下一个断点 / 手动暂停;
(2) Terminate:结束当前 JTAG/GDB 调试会话;
(3) Step Into:单步跳入;
(4) Step Over:单步跳过,本行整句跑完,不进入子函数,直接跳到下一行;
(5) Step Return:单步跳出,从当前函数内部一次性跑完剩余代码、跳出回到上层调用处;
(6) Restart:重新启动调试;
(7) Debug:开启调试或打开调试相关配置。
在IDE上方的Window中点击Show View可以看到与调试相关的选项卡,最常用的是
Memory:查看内存中的值;Expressions:解析变量或变量表达式;Registers:查看核上的寄存器。

多核调试
N22的调试
-
连接
不需要选择N22或者D25F,选择工具链即可。D25F的工程用v5f,N22的工程用v5,编译器不需要知道是什么核。对于JTAG连接,如果两个核都正常运行,openocd会创建两个socket端口,上位机分别通过这两个端口控制两个核。

-
N22与D25F调试差异
a. 调试N22前需要N22能够正常运行起来。N22 和 DSP 默认处于复位状态,需要在 D25F 代码中通过 mailbox 机制将其唤醒,N22 才会从 flash 0x80000 处取指令运行(详见下方c中的描述)。
b. 采用IDE断点调试N22时,需要按下图方式进行配置,默认
Core_Configuration选项为core0,改为core1也可以正常使用。注意
- 不要勾选
Startup选项中的Reset_and_Hold,如果勾选,N22就会复位,而且此时没有外部措施打开N22,调试会出错 - 在下图
Main选框下的Program默认选项可能是错误的,需要修改。

c. D25F的Code放在flash 0地址,N22的Code放在flash 0x80000地址处,DSP的Code放在flash 0x40000地址处。三核之间加了mailbox,类似软件中断,D25F向这个地址写数据,然后会产生中断送给N22,N22读取这个地址,拿到信息。D25F是主控,N22和DSP默认处于复位状态,D25F准备好,再打开N22和DSP,此时N22和DSP才会去flash取指令运行。Telnet调试时和D25F相同,不过读写内存地址时需要注意内存地址分布。

- 不要勾选
-
N22与D25F联调
方法:先按上面的方法在IDE下调试一个核,接着继续选择另外一个核进行调试即可,在Debug选框中选择对应的core进行操作,操作方式和单核调试相同。看板只能查看当前调试核的数据。

DSP的调试
- 配置环境
注意
32-bit系统没有测试,根据《xtensa_debug_guide.pdf》中的说法,XOCD(下面要安装的一个软件)只适配64-bit系统。另外下面只介绍Windows下的安装,Linux下的安装可参考相关手册和下方步骤。
步骤1:关闭杀毒软件,以管理员身份安装"Xplorer-9.0.17-windows-installer.exe",安装路径最好默认,首次打开时会报错(没有报错也需执行解决办法)。解决办法是:软件快捷方式处右键点击属性,在目标(T)输入框中追加上 --xxtrace(--前有个空格),在license检测的弹框中选择"Install Software Keys",输入 27001@192.168.51.135。随后关闭软件,去除 --xxtrace,之后该IDE可正常使用。
步骤2:根据《xtensa_debug_guide.pdf》的7.3章节安装"xt-ocd-14.07-windows64-installer.exe",文档和安装包均在步骤1中的软件安装目录下。
步骤3:根据《xtensa_debug_guide.pdf》的7.3.2章节安装J-Link probe support,这里安装的是从segger官网处下载的V7.56版本。
步骤4:修改步骤2中软件XOCD安装路径下的文件 topology.xml,其中usbser值为JLink的SN号,可通过SEGGER J-Link Configuration软件(JLink的安装路径下的JLinkConfig.exe)查看到自己手中JLink的SN号。修改和保存文件时需要注意文件编码不变。最好使用"xml editor"等软件修改。

- 工程的导入和编译
步骤1:打开IDE,新建工作空间,将 \PCDB\prj\Onca\tst\dsp文件夹内的 boot_1028.xws拷贝到工作空间下。
步骤2:删除HelloWorld工程,在工程导航的面板下右键选择"Import" -> "Import Xtensa Xplorer Workspace",选择 boot_1028.xws,一直next,每一步都全部选择(选项标红,选择不了说明该选项在之前选过,现在不必选择,可继续next)。




步骤3:可以根据提示新建一个工程,这里演示直接导入一个已有工程。这里将 \PCDB\prj\Onca\fw\boot\dsp_rw下的 dsp_led文件夹拷贝到工作空间下,在工程导航的面板下右键选择"Import" -> "Existing Projects into Workspace",然后选择 dsp_led文件夹,将工程导入。

步骤4:按如下方式进行配置,点击编译按钮,编译成功会在控制台处打印"Build successful !!"。


步骤5:根据提示找到目标文件 dsp_led,将 dsp_led拷贝到 C:\usr\xtensa\XtDevTools\install\tools\RI-2021.7-win32\XtensaTools\bin目录下。

步骤6:Windows环境变量中添加 XTENSA_CORE,值为 hifi5_v2。在 C:\usr\xtensa\XtDevTools\install\tools\RI-2021.7-win32\XtensaTools\bin处打开cmd,执行:
xt-objcopy.exe –O binary –S dsp_led dsp_led.bin
xt-objdump.exe -D dsp_led > assemble.txt
方案二:右键工程点击"Open Command Shell",然后参考 \PCDB\prj\Onca\tst\dsp下的readme.txt。
步骤7:如果编译时没有指定链接文件则需要添加链接文件(默认是sim文件,需要修改)。
打开IDE,新建工作空间,将 \PCDB\prj\Onca\tst\dsp文件夹内的 boot_1028.xws拷贝到工作空间下(之前执行过就不必执行了)。这一步是为了方便提供后面工程中需要使用到的link文件。如果对优化选项等有特殊要求的也可以在这个界面下进行配置。



- 调试前的其他准备
步骤1:将上面生成的bin文件下载到flash地址0x40000处。
步骤2:将PB4 - PB7设置为JTAG复用功能,并通过D25F来打开DSP核,这里是通过打开wpcdb,执行 d25f_enable_dsp脚本的方式来实现的,然后复位芯片。观察实验现象,可判断程序是否正确执行。
JTAG RST后继续调试功能
./ICEman -Z v5 -H
执行该命令后程序会复位数字寄存器,不会复位模拟寄存器,能看得到在程序入口处停留,同时JTAG仍是连接上的状态。

通过选项卡查看和修改寄存器和内存
Expressions选项卡
Expressions选项卡内可以查看变量/变量表达式,可以直接在Value处修改对应的值。

Memory选项卡
Memory选项卡内查看/修改内存数据。

点击右上角放大镜图标,弹出来Monitor Memory对话框如下:

Registers选项卡
Registers选项卡内查看/修改CPU寄存器数据。




GDB常用命令的使用
下图的红色方框中输入相应命令,对寄存器/内存进行读写操作。

验证过程示例如下:
x/1w 0x20000e40
0x20000e40 <main+452>: 0x00f92223
x/1w 0x80140204
0x80140204: 0x0e0fffff
set *(unsigned int*) 0x80140204=0x0e0f55aa
x/1w 0x80140204
0x80140204: 0x0e0f55aa

读指令
| 命令 | 说明 |
|---|---|
x/1w 0x80170000 |
从0x80170000读取一个word的数据 |
x/1h 0x80170000 |
从0x80170000读取half word的数据 |
x/1b 0x80170000 |
从0x80170000读取一个byte的数据 |
其中:
x/:表示读1:读取数量w:单位(word)
写指令
| 命令 | 说明 |
|---|---|
set *(unsigned int*) 0x80140420=0x12345678 |
在地址0x80140420写一个word,数值为0x12345678 |
set *(unsigned short*) 0x80140420=0x12345678 |
在地址0x80140420写half word,数值为0x5678 |
set *(unsigned char*) 0x80140420=0x12345678 |
在地址0x80140420写1个字节,数值为0x78 |
其中:
set:代表写unsigned char:写入单位0x80140420:写入地址0x12345678:写入数值
注意
实测部分指令不支持,如:tui enable。
VS Code上使用JTAG
关于在VS Code中使用JTAG进行调试的详细说明,请参考Telink官网文档:
使用Segger调试
Segger J-Link是一款主流的调试器,支持JTAG和SWD接口,可用于程序下载、在线调试和Flash烧录等操作。本章节介绍Segger J-Link工具在Telink芯片上的基本使用方法。对于用户,相当于有两套上位机和调试器,Telink JTAG小黑盒调试器/segger调试器均可使用,功能是一样的。
J-Link驱动安装
从Segger官网下载J-Link驱动安装包:https://www.segger.com/downloads/jlink/
安装完成后,将J-Link调试器通过USB连接到PC,设备管理器中可以识别到J-Link设备。
J-Link Commander使用
J-Link Commander是J-Link自带的命令行工具,可用于连接目标芯片、读写内存和寄存器等操作。
打开J-Link Commander,输入以下命令连接目标芯片:
connect
根据提示选择目标设备(RISC-V 内核,根据实际芯片型号选择),选择接口(JTAG或SWD),选择速度(建议使用4000kHz或自动)。
连接成功后,可使用以下常用命令:
| 命令 | 说明 |
|---|---|
r |
复位目标芯片 |
g |
开始执行程序 |
h |
停止程序执行 |
mem32 <addr> <count> |
读取32位内存 |
w4 <addr> <value> |
写入32位内存 |
reg |
查看所有寄存器 |
reg <regname> <value> |
写入指定寄存器 |
loadfile <filepath> <addr> |
下载文件到指定地址 |
erase |
擦除Flash |
exit |
退出J-Link Commander |
J-Flash使用
J-Flash是Segger提供的Flash烧录工具,支持通过J-Link对目标芯片进行Flash编程。
步骤1:打开J-Flash,新建工程或打开已有工程。

步骤2:配置目标芯片型号、接口类型(JTAG/SWD)和连接速度。





步骤3:打开要烧录的HEX或BIN文件。
步骤4:点击"Program Device"按钮进行烧录,或使用快捷键F7。
J-Link GDB Server使用
J-Link GDB Server可以将J-Link作为GDB调试服务器使用,配合GDB客户端进行在线调试。以下以多核芯片为例说明。
- 安装JLINK套件
使用JLINK调试之前需要在Segger官网下载JLINK套件,包括JLINK驱动和上位机程序,经测试,推荐安装更稳定的V7.96f版本:
https://www.segger.cn/downloads/jlink/JLink_Windows_V796f_x86_64.exe
- 在命令行中启动JLinkGDBServerCL.exe
找到安装好的JLink文件夹,在资源管理器的地址栏输入cmd,回车:

在打开的CMD窗口中输入命令启动JLinkGDBServerCL.exe,若要调试D25F核,输入以下命令:
JLinkGDBServerCL.exe -select USB -device rv32 -endian little -if JTAG -speed 1000 -noir -noreset -nogui -LocalhostOnly -nologtofile -port 2331
若要调试N22核,输入以下命令:
JLinkGDBServerCL.exe -select USB -device rv32 -endian little -if JTAG -speed 1000 -noir -noreset -nogui -LocalhostOnly -nologtofile -port 2331 -jtagconf 5,1
其中,-port可以自定义,若连接成功,会出现以下log:

其中,需要注意 GDB Server Listening port 这一数字,可能与你指定的-port不同,最终的 GDB Server Listening port 以log中为准。
- 在IoT Studio中配置JLINK调试
步骤1:新建C/C++ Remote Application
编译并下载程序后,如图所示,单击选中需要调试的程序(ELF文件):

然后打开Debug Configurations,双击 C/C++ Remote Application,可以看到,新建的Debug Configuration中,可以自动填充之前选择的ELF文件。
步骤2:配置Main选项卡
单击Main选项卡,需要进行如下配置:

步骤3:配置Debugger选项
单击Debugger选项卡中的Main选项卡,进行如下改动:

- 取消勾选
Stop on startup at: - GDB Debugger:改成
riscv32-elf-gdb - GDB command file:若没有自定义gdb init command的需求,保持为空
单击Connection选项卡,将之前获取到的 GDB Server Listening port 填入 Port number 中:

完成配置之后,即可开始调试。
注意
使用Segger J-Link工具前,请确保JTAG引脚已正确初始化(参考章节软件与IDE配置中的JTAG引脚初始化),且硬件连接正常。
常见问题排查
本节汇总了 JTAG 调试过程中的高频问题及解决方案,遇到问题时可以先在这里查找。
硬件连接类
Q: JTAG 调试工具亮红灯怎么办?
A:可能原因及排查步骤:
-
供电不正常 — 检查板子是否已上电,VBAT/VBUS 电压是否正常,建议先空片进行连接。
-
REF 电平未连接 — 确保板子的 3.3V 输出已连接到 JTAG 盒子的 REF 引脚。
-
信号线接错 — 逐一核对 TDI、TCK、TMS、TDO 是否与板子原理图对应。
-
驱动未安装 — 运行
BDT/ice/libusb-AICE-driver/Install_driver.exe安装驱动。
Q: 接线正常(蓝灯亮),但 ICEman 无法识别到芯片?
A:
-
检查代码中是否调用了
jtag_set_pin_en()(四线)或sdp_set_pin_en()(两线),详情请参考章节软件与IDE配置。 -
查找板子的原理图,找到JTAG_CTR对应的引脚,一般情况下该引脚接GND时是四线模式,接3v3是两线模式。
-
确认两线/四线模式开关设置是否与 ICEman 启动参数匹配。
-
尝试重新上电复位芯片后重启 ICEman。
连接与通信类
Q: ICEman 连上后很快就断开了?
A:常见于带 32k_watchdog 周期性复位的芯片(如 TL322x、TL323x),复位会清掉 ICEman 写入 RAM 的命令。需要参考特殊连接说明 中的方法,通过最新版 IoT Studio 启动 ICEman 以关闭 32k_watchdog。
Q: 启动 ICEman 后提示端口被占用?
A:说明之前的 ICEman 进程未正常退出。在终端中执行 ./ICEman -Z v5 -k 清除残留进程后重新启动。
断点与调试类
Q: 断点设置了但程序停不住?
A:
-
在IDE Debug状态下检查断点前是否有对勾标记,没有对勾表示断点未生效。
-
硬件断点支持数量有限,使用
info br命令确认当前断点数量是否已达上限,此外,Step Into / Step Over 本身会占用一个断点。详情请参考章节断点。
Q: 调试时出现 cannot access memory at address xx 错误?
A:通常是由于断点数量超出限制导致。在 GDB 控制台输入 info br 检查当前断点数量,删除多余断点后重试。
Q: Step Into 进不去函数?
A:如果函数位于封装好的库中(无源码),Step Into 会失败。此时建议使用 Step Over 跳过,或在可调试的代码处打断点。详情请参考章节单步调试。
源文件与路径类
Q: 调试时提示找不到源文件?
A:这是因为 IoT Studio 中的编译路径与本地路径不一致。通过 Edit Source Path 添加路径映射即可解决,最简单的做法是将 /cygdrive/c/ 映射到 C:\。详情请参考章节链接源文件。
多核调试类
Q: N22 核调试时提示复位失败或连不上?
A:
-
确保 D25F 核已正常运行并开启了 N22,详情请参考章节N22的调试中的N22与D25F调试差异。
-
N22 Debug 配置中不要勾选
Reset_and_Hold选项,否则会导致 N22 复位后无法自动恢复。
Q: 多核调试时如何切换查看不同核的数据?
A:在 Debug 选框中切换对应的 core 即可,IDE 的变量/寄存器/内存窗口只会展示当前选中核的数据。详情请参考章节N22的调试中的N22与D25F联调。
GDB 命令类
Q: GDB 中如何快速查看变量或内存的值?
A:
- 在 Expressions 选项卡中直接添加变量名即可查看和修改。详情请参考章节Expressions选项卡。
- 使用 GDB 命令:读内存
x/1w 0x80170000,写内存set *(unsigned int*) 0x80140420=0x12345678。详情请参考章节GDB常用命令的使用。
LPC
简介
低功耗电压比较器(Low Power Compare,后简称LPC)将输入电压 × 缩放比例与参考电压进行比较,并输出比较结果,也可用作从低功耗模式唤醒系统的信号。
LPC 有两种工作模式:
- Normal mode:内部基准来自 Bandgap(BG),精度高、功耗大,用于芯片正常供电场景。
- Low power mode:内部基准来自 UVLO,精度低、功耗小,用于芯片睡眠场景。
芯片支持:
| 芯片 | 硬件 LPC | 支持检测的GPIO | 支持外供参考电压的GPIO | VBAT 检测 |
|---|---|---|---|---|
| TLSR825x/TLSR8359 | 有 | PB1~PB7 | PB0 / PB3 | — |
| TLSR827x/TLSR8355 | 有 | PB1~PB7 | PB0 / PB3 | — |
| TLSR921x/TLSR951x | 有 | PB1~PB7 | PB0 / PB3 | — |
| TLSR922x/TLSR952x | 有 | PB1~PB7 | PB0 / PB3 | — |
| TL321x | 有(见注) | PB1~PB7 | PB0 / PB3 | — |
| TL322x | 有(见注) | PB1~PB7 | PB0 / PB3 | — |
| TL323x | 有 | PB5~PB7, PC0~PC3 | PB4 / PB7 | 支持 |
| TL721x | 有 | PB1~PB7 | PB0 / PB3 | — |
| TL751x | 有 | PG1~PG6, PF6 | PG0 / PG3 | — |
| 其他芯片 | 无 | — | — | — |
注意
TL321x/TL322x 的 LPC 可用作 Flash 上下电保护功能,详情请参考Flash章节。启用该功能后 LPC 不能用作其他用途。
输入通道和参考电压的具体枚举值因芯片而异,请查阅对应 lpc.h 中的 lpc_input_channel_e、lpc_reference_e 定义。TC 与 TL 系列的参考电压标称值略有差异。
工作原理
LPC采用32K RC时钟源作为比较器时钟。比较结果:
-
若「输入电压 × 缩放比例」> 参考电压,输出为低(
0)。 -
若「输入电压 × 缩放比例」< 参考电压,输出为高(
1)。 -
若二者相等,或输入通道选择为float,输出不确定。
GPIO 电压检测
LPC通过配置输入通道、参考电压、缩放比例,比较「输入电压 × 缩放比例」与参考电压,结果由 lpc_get_result() 读取。适用于检测外部GPIO引脚电压。
- 缩放比例:25% / 50% / 75% / 100%(所有芯片一致),用于扩大检测范围。
- 参考电压:Normal 模式来自 BG 或外部引脚,Low power 模式来自 UVLO(精度低、功耗小,用于睡眠场景)。
- 输入通道:见上方芯片支持表,各芯片支持的引脚不同。
阈值电压 = 参考电压 / 缩放比例。输入电压低于阈值时 lpc_get_result() 返回 1,高于阈值返回 0。例如参考电压872mV、比例50%,阈值 = 872 / 0.5 = 1.744V。
使用示例
TL系列 / TLSR921x/TLSR951x / TLSR922x/TLSR952x:
lpc_gpio_vol_detect_init(LPC_NORMAL, LPC_INPUT_CHN, LPC_REF_872MV, LPC_SCALING_PER50);
lpc_power_on(); // 必须最后上电
delay_us(64); // 等待2个32K采样周期
// main_loop: lpc_get_result();
TC系列 (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(); // 必须最后上电
sleep_us(64); // 等待2个32K采样周期
// main_loop: lpc_get_result();
- 各芯片的具体参数枚举值(通道、参考电压、阈值等)请查阅对应
lpc.h。 - LPC 采样时钟为 32K RC,上电后需等待约 2 个采样周期(
delay_us(64))再读取结果。 lpc_power_on()必须在通道/参考/缩放配置完成后最后调用。
VBAT 低电压检测
VBAT低电压检测功能,可监控芯片电源电压。
通过 lpc_vbat_vol_detect_init(thres_vol) 配置,直接检测 VBAT 电压是否低于设定的下降阈值(恢复则需高于上升阈值),结果通过 lpc_get_result() 读取。
注意
- 必须调用
lpc_vbat_vol_detect_init()一键初始化,不可调用其他 LPC 配置接口。 - VBAT 检测基于 BG 参考电压,不能用于睡眠模式。
- GPIO 检测与 VBAT 检测共用比较器,
lpc_gpio_vol_detect_init()会自动关闭 VBAT 检测,二者不可同时使用。
使用示例
lpc_vbat_vol_detect_init(LPC_VBAT_FALLING_2P20V_RISING_2P30V);
lpc_power_on(); // 必须最后上电
delay_us(64); // 等待2个32K采样周期
// main_loop: lpc_get_result();
PEM (外设事件矩阵)
概述
芯片支持:PEM 模块仅部分 TL 系列芯片支持(比如:TL321X / TL322X / TL323X / TL721X),TC 系列及 B91/B92 不支持。
PEM (Peripheral Event Matrix) 是一个实现各外设之间互联的硬件模块。它将任意外设A的event信号路由给任意外设B的task输入,外设B将task信号当作enable或trigger信号。
说明
- 下图中PEM通道的配置仅为示例,展示PEM通道可以配置为一对一(如Ch0: TIMER→ADC)、一对多(如Ch0+Ch1: TIMER→ADC & DMA)、多对一(如Ch1+Ch4: TIMER & PWM→DMA)以及多对多的路由关系。

核心概念
| 概念 | 描述 |
|---|---|
| Event(事件) | 外设送出的类似中断信号。 |
| Task(任务) | 外设接收的信号,可以选择任意的event信号。task信号被当作enable或trigger信号。 |
| Channel(通道) | 每个PEM通道独立地将一个event映射到一个task。通道数因芯片而异,具体请参考各芯片 pem.h 中的 pem_chn_e 定义。 |
对比传统中断方式
以"定时器每1s触发一次ADC采样"为例,两种实现方式的差异如下:
| 不使用 PEM | 使用 PEM | |
|---|---|---|
| 工作流程 | 定时器中断 → CPU 进 ISR → 手动调用 ADC 启动 → 退出 ISR | 定时器 event → PEM 通道 → ADC task,全程硬件自动完成 |
| CPU 参与 | 每次触发需 CPU 进出 ISR | 无需 CPU 参与 |
| 延迟 | 中断响应 + 上下文切换 | 纯硬件延迟 |
| 中断嵌套风险 | ISR 可能被更高优先级中断打断 | 不受中断优先级影响 |
注意
- PEM只能路由trigger/enable信号。外设之间实际的数据传输仍然需要DMA或MCU来写入/读取。
多通道路由
- 一对多:一个event可以通过多个通道路由给多个task。
- 多对一:多个event可以通过多个通道路由给同一个task。PEM对event执行"OR"逻辑,不做仲裁。外设自身需要处理多个event同时到达的情况。
- 多对多:通过使用多个通道实现。
使用流程
使用 PEM 需要在完成外设自身初始化的基础上,再配置 PEM 通道将两者连接。完整流程如下:
1. pem_init() — 使能 PEM 模块
2. 初始化 event 源外设 — 配置外设的工作模式和中断(如配置 STIMER 定时周期、使能中断)
3. <外设名>_set_pem_event() — 将外设的某个 event 信号连接到 PEM 通道
4. 初始化 task 目标外设 — 配置外设的工作模式(如 ADC 时钟、通道、采样参数)
5. <外设名>_set_pem_task() — 将 PEM 通道连接到外设的某个 task 信号
6. pem_chn_en() — 使能通道,路由生效
7. 启动 event 源 — 开始自动触发 task
示例:STIMER 定时中断触发 ADC 单次采样
/* 1. 使能 PEM */
pem_init();
/* 2. 初始化 event 源外设:配置 STIMER 每 1s 产生一次中断 */
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() — 将 STIMER 的定时中断 event 连接到 PEM 通道 */
stimer_set_pem_event(PEM0, STIMER_EVENT_TRIG_POS);
/* 4. 初始化 task 目标外设:配置 ADC 参数 */
adc_init(ADC0, NDMA_M_CHN); // ADC 初始化(NDMA 单通道)
adc_set_clk(ADC0, ADC_CLK_4M); // 时钟配置
adc_set_sample_chn(ADC0, ADC_CHN0); // 采样通道选择
/* 5. adc_set_pem_task() — 将 ADC 的单次采样 task 连接到同一 PEM 通道 */
adc_set_pem_task(ADC0, PEM0, ADC_TASK_SINGLE_ADC_TRIG);
/* 6. 使能 PEM 通道 */
pem_chn_en(PEM0);
// 之后 STIMER 每秒产生一次中断,自动通过 PEM 触发 ADC 进行一次采样
具体外设的 API 参数和初始化方式因芯片而异,请以对应芯片的 demo 工程和头文件为准。
API 概览
核心 API
| 函数 | 说明 |
|---|---|
pem_init() |
使能 PEM 模块(复位 + 开时钟) |
pem_chn_en(chn) |
使能指定通道,event → task 路由开始工作 |
pem_chn_dis(chn) |
禁用指定通道 |
外设 PEM 接口
每个支持 PEM 的外设都提供两个接口,命名遵循统一规则:
- event 配置:
<外设名>_set_pem_event() - task 配置:
<外设名>_set_pem_task()
例如 STIMER 提供 stimer_set_pem_event() / stimer_set_pem_task(),ADC 提供 adc_set_pem_event() / adc_set_pem_task(),依次类推。
如何确认某个外设是否支持 PEM?可查看枚举pem_event_module_sel_e和pem_task_module_sel_e。可在对应外设头文件中搜索 pem_event 或 pem_task,查看相关函数声明。具体提供哪些 event/task 信号及函数参数,请查阅对应外设头文件中的枚举定义。