1. 项目概述与HWAFFT核心价值
在嵌入式数字信号处理领域,实时性是衡量系统性能的关键指标。无论是音频降噪、通信解调还是振动分析,快速傅里叶变换都是将时域信号转换到频域进行分析的核心算法。然而,在资源受限的DSP平台上,用CPU软件实现FFT往往面临计算量大、功耗高的挑战,尤其是在处理高采样率或长点数数据时,实时性难以保证。
德州仪器推出的TMS320VC5505、C5505及C5515系列DSP,其一大亮点就是集成了专用的FFT硬件加速器。这个被称为HWAFFT的模块,本质上是一个与CPU核心紧密耦合的协处理器,专门用于执行基2时域抽取算法的蝶形运算。它支持从8点到1024点(2的幂次)的复数FFT和IFFT计算,并且将512个复数旋转因子固化在硬件查找表中,省去了软件计算和存储旋转因子的开销。对于从事实时信号处理的工程师来说,掌握HWAFFT的使用,意味着能在不牺牲精度的前提下,将FFT计算速度提升数倍,同时显著降低系统功耗,这对于电池供电的便携式设备或对功耗敏感的应用场景至关重要。
2. HWAFFT硬件架构与工作原理深度解析
2.1 紧密耦合协处理器架构
HWAFFT并非一个独立的外设,而是作为CPU执行单元的一部分存在。这种“紧密耦合”的设计带来了几个关键优势。首先,HWAFFT可以直接访问CPU的B、C、D三条数据读取总线,这意味着它拥有极高的内存读取带宽,能够高效地从数据缓冲区获取待处理的复数样本。其次,它可以访问CPU内部的地址生成单元和寄存器,减少了数据搬运和地址计算的软件开销。然而,这种设计也带来了一些约束,例如HWAFFT无法直接访问内存映射寄存器或数据写入总线,其执行流程也必须遵循CPU的流水线规则,包括处理由条件执行或数据冲突引起的流水线停顿。
2.2 单级与双级蝶形运算模式
HWAFFT的核心是一个硬件实现的基2蝶形运算单元。为了提高计算效率,它支持两种工作模式:单级模式和双级模式。
在双级模式下,HWAFFT在一次运算过程中可以连续完成两个FFT级(stage)的计算。它内部会将第一级蝶形运算的输出结果,直接作为第二级运算的输入,从而在一个“pass”中完成两级运算。这种模式极大地提升了吞吐率,尤其对于大点数FFT(如1024点)效果显著。
然而,对于总级数为奇数的FFT(例如8点、32点、128点、512点),在完成若干次双级运算后,总会剩余最后一级无法配对。此时,HWAFFT会自动切换到单级模式来完成这最后一级的计算。因此,在调用hwafft_Npts函数时,我们无需手动指定模式,硬件会根据FFT点数自动规划最优的单/双级组合序列。
2.3 数据格式与精度考量
HWAFFT处理的是定点复数数据。每个复数由实部和虚部组成,各用一个16位有符号整数表示,格式为Q15(即1个符号位,15个小数位)。这意味着数值范围被限制在[-1, 1 - 2⁻¹⁵]之间。在内存中,实部和虚部交错存储,但HWAFFT函数接口使用Int32(32位)指针。具体来说,一个32位字的高16位存储实部,低16位存储虚部。
注意:在向HWAFFT传递数据前,必须确保你的源数据已经转换为此格式。如果你的原始数据是浮点数,需要进行定点化缩放和转换;如果是16位整数,则需要考虑符号扩展和定标。不正确的数据格式是导致FFT结果错误的最常见原因之一。
2.4 内置旋转因子与缩放机制
为了最大化内存带宽利用率,HWAFFT内部集成了一个包含512个复数(1024个16位值)的旋转因子查找表。对于任何不超过1024点的FFT,硬件会自动从中抽取并索引所需的旋转因子。当执行IFFT时,硬件会自动使用旋转因子的共轭,无需软件干预。
关于缩放,这是定点FFT中防止数据溢出的关键。HWAFFT提供了两种模式:
SCALE_FLAG(0):启用缩放。在每个蝶形运算的输出端,结果会自动右移一位(即除以2)。这能有效防止中间结果溢出,但会引入1/2 LSB的量化误差。只要输入数据的幅度小于1,采用此模式可保证整个FFT计算过程不会溢出。NOSCALE_FLAG(1):禁用缩放。计算速度最快,精度最高(无额外舍入误差),但极易发生溢出。仅当你能确保所有输入数据的幅度都小于1/N(N为FFT点数)时方可使用,这在实际应用中通常难以保证。
实操心得:在绝大多数实际应用中,尤其是处理动态范围较大的真实信号(如音频、振动信号)时,强烈建议启用SCALE_FLAG。虽然会损失约1/2比特每级的精度,但换来了计算的稳定性。如果对精度有极致要求,可以考虑在调用HWAFFT前,先对输入数据进行一次性的预缩放(例如除以N),然后使用NOSCALE_FLAG模式,但这需要仔细评估信号的电平。
3. 软件接口详解与工程配置实战
3.1 HWAFFT函数库概览与ROM调用
TI提供了高度优化的HWAFFT汇编函数,并已将其烧录到DSP的片内ROM中,地址因芯片版本(PG1.4或PG2.0)而异。这样做的好处是节省了宝贵的RAM空间(约4KB)。要使用这些ROM函数,你需要在工程中进行正确配置。
第一步:从工程中移除或排除hwafft.asm文件。这个文件包含了函数的源代码,如果你在编译时链接了它,链接器会使用RAM中的副本。我们的目标是使用ROM中的版本。
第二步:修改链接器命令文件(.cmd文件)。在文件的末尾(SECTIONS指令之后),添加对应你芯片版本的函数地址符号定义。例如,对于C5505 PG2.0的芯片,添加如下代码:
/*** 在链接器命令文件末尾添加以下代码以从ROM调用HWAFFT例程 ***/ /* HWAFFT Routines ROM Addresses (PG 2.0) */ _hwafft_br = 0x00ff6cd6; _hwafft_8pts = 0x00ff6cea; _hwafft_16pts = 0x00ff6dd9; _hwafft_32pts = 0x00ff6f2f; _hwafft_64pts = 0x00ff7238; _hwafft_128pts = 0x00ff73cd; _hwafft_256pts = 0x00ff75de; _hwafft_512pts = 0x00ff77dc; _hwafft_1024pts= 0x00ff7a56;第三步:重新编译工程。链接器会将这些符号解析为ROM中的绝对地址,从而正确调用函数。
重要警告:在调用ROM中的HWAFFT例程前,必须仔细阅读并满足你所用芯片勘误表(Errata)中关于数据和暂存缓冲区内存分配的限制。例如,对于C5505 PG2.0,需要参考文档SPRZ310。常见的限制是要求
data和scratch缓冲区不能位于同一块内存(如DARAM)的特定边界上,否则可能导致计算错误。忽视这一点是项目后期难以调试的“坑”。
3.2 关键函数参数与使用流程
HWAFFT的核心函数是hwafft_Npts,其中N代表点数。其函数原型和使用流程遵循一个清晰的模式。
1. 数据准备与位反转 (hwafft_br)Radix-2 DIT FFT算法要求输入数据是位反转序。HWAFFT提供了一个优化的位反转函数hwafft_br。这是一个“非原位”操作,需要源缓冲区和目标缓冲区。
// 假设进行1024点FFT #define DATA_LEN_1024 1024 Int32 data_buf[DATA_LEN_1024]; // 原始数据缓冲区 Int32 data_br_buf[DATA_LEN_1024]; // 位反转目标缓冲区 Int32 *data = data_buf; Int32 *data_br = data_br_buf; // 执行位反转 hwafft_br(data, data_br, DATA_LEN_1024); // 位反转后,使用data_br作为后续FFT的输入 data = data_br;这里有一个极易出错的强制对齐要求:data_br缓冲区的起始地址,其字节地址的最低log2(4*N)位必须为0。对于1024点FFT,log2(4*1024)=12,即地址必须是4096(2¹²)的倍数。第9章会详细讲解三种实现对齐的方法。
2. 执行FFT/IFFT (hwafft_Npts)位反转后的数据即可送入HWAFFT核心函数。
Int32 scratch_buf[DATA_LEN_1024]; // 暂存缓冲区,必须与data_br不在同一RAM块 Int32 *scratch = scratch_buf; Uint16 fft_flag, scale_flag, out_sel; Int32 *result; // 设置标志位 fft_flag = FFT_FLAG; // 0 for FFT, 1 for IFFT scale_flag = SCALE_FLAG; // 0 for scale enabled, 1 for disabled // 调用1024点FFT函数 out_sel = hwafft_1024pts(data, scratch, fft_flag, scale_flag); // 根据返回值判断结果在哪个缓冲区 if (out_sel == OUT_SEL_DATA) { result = data; // 结果在输入/输出缓冲区 } else { result = scratch; // 结果在暂存缓冲区 }参数解析:
data: 输入向量(位反转序),也可能作为输出向量。scratch: 暂存向量,用于存储中间结果,也可能作为最终输出向量。关键:data和scratch必须位于不同的物理RAM块(如不同的DARAM或SARAM)以最大化内存带宽。fft_flag: 选择FFT(0)或IFFT(1)。scale_flag: 选择是否在每级蝶形运算后缩放(0为是,1为否)。out_sel(返回值): 指示结果位置。这是一个布尔值,0表示结果在data中,1表示在scratch中。你必须检查这个返回值,否则可能访问到无效数据。
3.3 内存对齐的三种实现方法
如前所述,位反转目标缓冲区data_br有严格的对齐要求。以下是三种在工程中实现对齐的常用方法,我推荐第三种,因为它最灵活。
方法一:静态分配到合适RAM块起始处在链接器命令文件中,找到一个起始地址满足对齐要求的内存块,并将缓冲区分配给它。例如,对于1024点FFT(需12位对齐),找到一个起始地址低12位为0的块(如0x0004000)。
/* 在 .cmd 文件的 MEMORY 段 */ MEMORY { ... DARAM2_3 (RWIX): origin = 0x0004000, length = 0x004000 /* 起始地址低12位为0 */ ... } /* 在 .cmd 文件的 SECTIONS 段 */ SECTIONS { .data_br_buf : > DARAM2_3 /* 强制分配到这个块 */ ... }方法二:在链接器中使用ALIGN描述符这种方法更灵活,链接器会在指定的内存区域内寻找一个满足对齐要求的地址。
/* 在 .cmd 文件的 SECTIONS 段 */ SECTIONS { .data_br_buf : > DARAM ALIGN = 4096 /* 4096 = 2^12, 要求12位对齐 */ ... }方法三:在源代码中使用DATA_ALIGN编译指示(推荐)这是最常用且便于管理的方法。直接在定义缓冲区的C源文件中使用#pragma指令。
/* 在 main.c 或相关源文件中 */ #pragma DATA_SECTION(data_br_buf, ".data_br_buf"); // 指定段名 #pragma DATA_ALIGN(data_br_buf, 2048); // 对齐到2048字边界。注意:常数单位是“字”(16-bit) // 对于字节地址12位对齐,字地址需对齐到 2^(12-1) = 2048 字。 Int32 data_br_buf[DATA_LEN_1024];然后在链接器命令文件中,确保.data_br_buf段被分配到一个足够大的内存区域即可,无需指定具体地址。
4. 从基础到进阶:完整FFT/IFFT调用实例
4.1 基础调用:1024点FFT与IFFT
让我们结合前面的知识,看一个完整的、带错误检查的1024点FFT调用示例。这个例子包含了从数据准备、缓冲区对齐、函数调用到结果提取的全过程。
#include <stdint.h> #include “hwafft.h” // 假设头文件中定义了函数原型和常量 #define FFT_LEN 1024 #define DATA_LEN_1024 FFT_LEN /* 1. 声明并对齐缓冲区 */ #pragma DATA_SECTION(input_data, “.input_section”) Int32 input_data[DATA_LEN_1024]; // 原始时域数据(复数,Q15格式) #pragma DATA_SECTION(data_br_buf, “.data_br_section”) #pragma DATA_ALIGN(data_br_buf, 2048) // 关键:1024点对齐要求 Int32 data_br_buf[DATA_LEN_1024]; // 位反转目标缓冲区 #pragma DATA_SECTION(scratch_buf, “.scratch_section”) Int32 scratch_buf[DATA_LEN_1024]; // 暂存缓冲区 /* 2. 链接器命令文件 (.cmd) 需确保三个段位于不同的RAM块 */ /* 例如: SECTIONS { .input_section : > DARAM0 .data_br_section : > DARAM1 .scratch_section : > SARAM ... } */ void perform_1024pt_fft(void) { Int32 *data_ptr = input_data; Int32 *data_br_ptr = data_br_buf; Int32 *scratch_ptr = scratch_buf; Int32 *result_ptr = NULL; Uint16 fft_flag, scale_flag, out_sel; /* 3. 准备数据(此处省略,实际应从ADC、DMA或文件填充input_data) */ // fill_input_data(input_data, DATA_LEN_1024); /* 4. 执行位反转 */ hwafft_br(data_ptr, data_br_ptr, DATA_LEN_1024); data_ptr = data_br_ptr; // 后续FFT使用位反转后的数据 /* 5. 配置并执行FFT(启用缩放) */ fft_flag = FFT_FLAG; scale_flag = SCALE_FLAG; out_sel = hwafft_1024pts(data_ptr, scratch_ptr, fft_flag, scale_flag); /* 6. 获取结果指针 */ if (out_sel == OUT_SEL_DATA) { result_ptr = data_ptr; // scratch_ptr 现在可安全复用或丢弃 } else if (out_sel == OUT_SEL_SCRATCH) { result_ptr = scratch_ptr; // data_ptr (即 data_br_buf) 现在可安全复用 } else { // 错误处理:理论上不应发生 while(1); // 或触发错误指示灯 } /* 7. 此时 result_ptr 指向频域数据(顺序) */ // process_frequency_domain(result_ptr, DATA_LEN_1024); } /* 执行IFFT(逆变换)的流程几乎相同,仅改变fft_flag */ void perform_1024pt_ifft(Int32 *freq_data) { // ... 类似的缓冲区声明和对齐 ... Int32 *data_ptr = freq_data; // 输入是频域数据 // ... 位反转 ... fft_flag = IFFT_FLAG; // 关键区别 scale_flag = NOSCALE_FLAG; // IFFT通常可禁用缩放,但需确保输入幅度足够小 // ... 调用 hwafft_1024pts ... // result_ptr 指向恢复的时域数据 }4.2 超越1024点:大点数FFT的混合计算策略
HWAFFT硬件最大支持1024点FFT。对于需要2048点、4096点甚至更大尺寸的FFT,我们可以采用“分治”策略,结合HWAFFT和CPU计算来实现。其核心数学原理是Radix-2 DIT分解公式:
X(k) = 1/2 * [X_even(k) + W_N^k * X_odd(k)], for k = 0 to N/2-1 X(k+N/2) = 1/2 * [X_even(k) - W_N^k * X_odd(k)], for k = 0 to N/2-1这意味着一个N点FFT可以分解为两个N/2点FFT的结果,再经过一个额外的Radix-2级(称为“组合级”)进行合并。
实现2048点FFT的步骤:
- 数据拆分与位反转:将2048点的输入数据
x[n]按奇偶索引拆分为两个1024点的序列x_even[n]和x_odd[n]。有趣的是,对原始数据进行一次完整的位反转操作后,所有偶数索引的数据会自然位于前半部分,奇数索引的数据位于后半部分,一举两得。 - 并行计算子FFT:使用HWAFFT分别计算
x_even[n]和x_odd[n]的1024点FFT,得到X_even[k]和X_odd[k]。 - 计算组合级:在CPU上执行一个额外的Radix-2级。
- 生成额外的旋转因子
W_2048^k(k=0..1023)。HWAFFT内置的512点旋转因子表不够用,需要软件生成或预计算。 - 对于每个k,计算
product = complex_multiply(W_2048^k, X_odd[k])。 - 计算
X[k] = complex_add(X_even[k], product) / 2。 - 计算
X[k+1024] = complex_subtract(X_even[k], product) / 2。
- 生成额外的旋转因子
关键代码结构示意:
// 假设已定义好 complex_multiply, complex_add, complex_subtract 函数 #define N 2048 Int32 input[N], data_br[N], X_even[N/2], X_odd[N/2]; Int32 scratch_even[N/2], scratch_odd[N/2]; Int32 twiddle_2048[N/2]; // 预计算的2048点旋转因子(前一半) // 1. 位反转整个2048点数据 hwafft_br(input, data_br, N); // 2. 拆分奇偶序列 (data_br前半部分是偶数索引,后半部分是奇数索引) memcpy(X_even, data_br, (N/2)*sizeof(Int32)); memcpy(X_odd, data_br + (N/2), (N/2)*sizeof(Int32)); // 3. 使用HWAFFT计算两个1024点FFT out_sel_even = hwafft_1024pts(X_even, scratch_even, FFT_FLAG, SCALE_FLAG); if(out_sel_even == OUT_SEL_SCRATCH) X_even = scratch_even; // ... 类似处理 X_odd ... // 4. CPU执行组合级 for(k=0; k<N/2; k++) { Int32 prod = complex_multiply(twiddle_2048[k], X_odd[k]); data_br[k] = complex_add(X_even[k], prod, SCALE_FLAG); // 包含/2缩放 data_br[k + N/2] = complex_subtract(X_even[k], prod, SCALE_FLAG); } // data_br 现在包含2048点FFT结果这种方法将大部分计算量(两个1024点FFT)卸载给高效的HWAFFT,CPU仅负责一个蝶形运算级,从而在有限的硬件能力下实现了更大尺寸的FFT。
5. 性能实测、调试技巧与常见问题排查
5.1 性能基准与能效对比
TI官方文档提供了HWAFFT与纯CPU软件实现FFT的性能对比数据,这些数据基于真实的功耗和周期测量,极具参考价值。
测试条件一:核心电压1.05V,PLL 60MHz(低功耗模式)测试条件二:核心电压1.3V,PLL 100MHz(高性能模式)
下表汇总了关键数据(以条件一为例):
| FFT点数 | HWAFFT总周期(FFT+BR) | CPU总周期(FFT+BR) | 速度提升倍数 | 能效提升倍数 |
|---|---|---|---|---|
| 8点 | 130 | 291 | 2.2x | 4.0x |
| 16点 | 170 | 461 | 2.7x | 4.9x |
| 32点 | 321 | 748 | 2.3x | 3.9x |
| 64点 | 436 | 1405 | 3.2x | 5.4x |
| 128点 | 912 | 2798 | 3.1x | 5.0x |
| 256点 | 1668 | 5947 | 3.6x | 5.8x |
| 512点 | 3740 | 12736 | 3.4x | 5.4x |
| 1024点 | 7315 | 27717 | 3.8x | 6.0x |
数据解读与选型建议:
- 规模效益:FFT点数越大,HWAFFT带来的加速比和能效提升越显著。对于1024点FFT,速度提升近4倍,能效提升高达6倍。这意味着在电池供电的设备中,使用HWAFFT可以大幅延长续航时间,或在相同功耗下处理更复杂的算法。
- 固定开销:对于小点数FFT(如8点),硬件加速的优势相对较小,因为函数调用、数据搬运等固定开销占比变大。此时需要评估是否值得使用HWAFFT。
- 模式选择:在低电压/低频模式下,HWAFFT的能效优势依然保持,说明其硬件设计对功耗优化非常有效。
5.2 使用CCS进行图形化调试
Code Composer Studio (CCS) 的图形工具是验证FFT结果的利器。假设你的FFT结果存储在地址0x3000开始的存储器中(scratch缓冲区)。
- 打开图形工具:在CCS菜单栏选择
Tools -> Graph -> Single Time。 - 配置实数部分视图:
- Start Address:
0x3000(这是第一个32位数据的地址,高16位是实部) - Acquisition Buffer Size: 1024 (FFT点数)
- Display Data Size: 1024
- DSP Data Type:
32-bit signed integer - Sample Rate: 你的采样率(如48000)
- Plot Axes: 勾选
Magnitude或Real Part - Data Plot Style:
Line - 点击OK,你将看到频域信号的实部幅度谱。
- Start Address:
- 配置虚数部分视图:再打开一个图形窗口。
- Start Address:
0x3002(第一个32位数据的地址+2字节,低16位是虚部) - 其他设置同上,但需要将DSP Data Type改为
16-bit signed integer,因为虚部存储在低16位。或者,更稳妥的方法是使用一个自定义的GEL脚本来分离实部虚部并显示。
- Start Address:
实操心得:在观察频域图时,如果输入是纯实数信号(虚部全为0),其FFT结果应呈现共轭对称性。如果图形严重不对称或出现大量高频噪声,很可能是数据格式错误、缓冲区未对齐或缩放模式选择不当。
5.3 常见问题排查速查表
在实际开发中,我遇到过不少“坑”。下面这个表格总结了典型问题、可能原因和解决方法,希望能帮你快速定位问题。
| 问题现象 | 可能原因 | 排查步骤与解决方法 |
|---|---|---|
| 程序运行崩溃或进入异常 | 1. 缓冲区地址未满足对齐要求。 2. data和scratch缓冲区位于同一RAM块,违反勘误表限制。3. 函数指针错误(链接了错误的ROM地址)。 | 1. 检查data_br缓冲区地址。使用CCS Memory Browser查看其地址,确认低log2(4*N)位为0。2. 检查链接器.cmd文件,确保 data和scratch段映射到不同的DARAM/SARAM区域。3. 核对芯片版本(PG1.4/PG2.0),并使用正确的ROM地址表。 |
| FFT结果全为零或明显错误 | 1. 输入数据格式不是Q15复数交错格式。 2. 未执行位反转或位反转目标缓冲区错误。 3. 缩放模式选择不当导致数据溢出或下溢。 4. 未检查 out_sel返回值,访问了错误的输出缓冲区。 | 1. 在调用hwafft_br前,查看输入缓冲区内存,确认数据格式正确。2. 单步调试,确认 hwafft_br被调用且参数正确。3. 尝试切换 scale_flag。对于动态范围大的信号,务必使用SCALE_FLAG。4. 添加调试代码,打印 out_sel值,并据此访问result指针。 |
| IFFT结果无法恢复原始信号 | 1. FFT和IFFT的缩放标志不一致。 2. 旋转因子共轭处理错误(但HWAFFT内部已处理)。 3. 进行了额外的缩放操作。 | 1. 确保FFT和IFFT使用相同的scale_flag。通常都使用SCALE_FLAG。2. 信任HWAFFT硬件,无需对旋转因子做额外处理。 3. 记住FFT和IFFT是互逆运算,理论上 IFFT(FFT(x)) = x(可能有缩放因子)。检查最终结果是否等于原始信号乘以N或N/2。 |
| 性能远低于预期 | 1. 数据和指令缓存未使能或配置不当。 2. 缓冲区位于低速的外部存储器。 3. 频繁进行小点数FFT,函数调用开销占比大。 | 1. 在CCS中配置并使能Cache。 2. 确保所有数据缓冲区( data,data_br,scratch)和程序代码(尤其是HWAFFT调用循环)都在片内RAM(DARAM/SARAM)中。3. 考虑将多个小规模FFT批处理,或评估是否真的需要硬件加速。 |
| 计算大点数(>1024)FFT结果错误 | 1. 自行生成的旋转因子表错误。 2. 奇偶序列拆分逻辑错误。 3. CPU端的复数乘加运算函数有精度或溢出问题。 | 1. 使用MATLAB或Python生成精确的旋转因子表,并导出为C数组。用已知信号(如单频正弦波)验证。 2. 验证位反转后,前半部分是否确实是偶数索引数据。可以写一个简单的测试程序验证。 3. 实现 complex_multiply和complex_add/subtract时,使用40位累加器(long long或int40_t)进行中间计算,最后再饱和处理到32位,以防止溢出。 |
5.4 一个真实的音频滤波应用案例
TI提供了一个开源的“VC5505 FFT Filter Demo”项目,这是一个绝佳的学习范例。它实现了基于重叠相加法的实时音频低通滤波器。其工作流程是:
- 采集:通过AIC3204编解码器以48kHz采样立体声音频,DMA将数据存入乒乓缓冲区。
- 分析:对每个音频块(例如256个样本)调用HWAFFT计算FFT,转换到频域。
- 滤波:在频域与预计算的滤波器系数(低通)的FFT结果进行复数乘法。
- 合成:调用HWAFFT计算IFFT,转换回时域。
- 重叠相加:将当前块与上一个块的重叠部分相加,以消除块处理引入的边界效应,形成连续输出。
- 回放:DMA将处理后的数据送回编解码器播放。
这个案例完美展示了HWAFFT在实时流处理系统中的核心作用。通过研究这个项目的源码,你可以学到如何将孤立的FFT/IFFT调用集成到一个完整的、中断驱动的实时信号处理链中,包括DMA配置、缓冲区管理、以及防止音频断音的叠加重叠处理技巧。