资讯动态

TMS320F28035定点FFT实战:从Q15精度到内存布局的完整指南

发布时间:2026/10/14 18:26:11 来源:尧图企业网站定制
简介这份资源面向数字信号处理初学者与嵌入式DSP开发者聚焦在TI TMS320F28035浮点DSP上实现快速傅里叶变换的完整代码工程可用于频谱分析、滤波等实时信号处理场景的学习与移植参考。压缩包共约2000个文件整体10.33MB以528个C源码与237个头文件为核心配合304套CCS工程与构建配置、104个链接命令文件、49个汇编文件及若干GEL调试脚本、PDF文档与库文件完整保留了Code Composer Studio下的工程结构便于直接导入编译与调试。内容涵盖蝶形运算、位反转、旋转因子预计算、复数乘法优化以及循环缓冲等关键环节读者可据此理解FFT算法在28035上的落地方式并借助C28x浮点数学库对比验证结果。目前已有548人学习下载适合希望从原理到工程实践打通FFT实现路径的开发者参考。1. TMS320F28035 上跑 FFT从「算得动」到「算得准」的分水岭很多人第一次在 TMS320F28035 上写 FFT 代码都会经历同一个反直觉的瞬间代码编译通过、烧录成功、串口也能打印出频谱但把信号源换成标准正弦波一看幅值对不上、频率有偏移、旁瓣一塌糊涂。问题往往不在 FFT 算法本身而在这颗定点 DSP 的「数据表示」和「内存布局」上。TMS320F28035 属于 C2000 系列主频常见配置到 60MHz没有硬件浮点单元所有运算靠定点或 IQmath 库完成这就决定了 FFT 代码的核心矛盾不是「怎么调库」而是「怎么在有限位宽和有限 RAM 里把精度守住」。这篇笔记面向正在用这颗芯片做电机控制、电力监测或音频采集的工程师把 FFT 代码从选型、定点化、内存分配到实测验证的完整路径拆开讲新手能照着复现熟手能对照边界参数。2. 定点 FFT 的数学底子与库选型为什么不能直接抄浮点代码2.1 定点 Q 格式与溢出边界TMS320F28035 是 32 位定点内核做 FFT 时数据通常用 Q15 或 Q31 表示。Q15 把 1.0 映射成 32767动态范围约 90dB适合 16 位 ADC 采进来的原始数据Q31 精度更高但乘法后容易溢出需要 64 位中间结果。FFT 的蝶形运算里每一级都有加法和乘法加法会让位宽增长乘法会让位宽收缩如果不做缩放N256 的 FFT 中间结果可能超出 32 位。常见做法是每级蝶形后右移 1 位代价是损失约 0.5bit 信噪比换来的是绝不溢出。这个「每级缩放」是定点 FFT 和浮点 FFT 最大的代码差异也是很多人抄浮点代码后结果发散的根本原因。2.2 C2000 上可用的 FFT 实现路径在 F28035 上做 FFT实际可选的路子有三条一是 TI 官方 C28x FPU 不适用这颗没 FPU所以走 C28x IQmath 库配合手写蝶形二是用 C2000 的 RFFT 库部分版本对定点有支持三是自己用查表法写旋转因子。我一般推荐 IQmath 加手写基 2 蝶形原因是 IQmath 把 Q 格式的乘加封装好了_IQmpy、_IQsin这些函数在 ROM 里有查表执行周期可预测调试时也能单步看 Q 值。RFFT 库虽然省事但版本兼容和内存对齐要求比较苛刻换芯片或换编译器版本时容易翻车。2.3 旋转因子的存储与精度取舍旋转因子 $W_N^k \cos(2\pi k/N) - j\sin(2\pi k/N)$ 如果用 Q15 存N256 时需要 256 个 16 位字占 512 字节用 Q31 存则翻倍。F28035 的 RAM 通常只有几 KB 到十几 KB 可用所以旋转因子一般放 Flash 里用const修饰运行时通过memcpy搬到 RAM 或直接寻址。精度上Q15 旋转因子在 N256 时理论信噪比约 80dB够电力谐波分析用如果做音频或需要 90dB 以上动态范围就得用 Q31 旋转因子配合 Q31 数据但 RAM 和周期都要重新算。2.4 最小可运行工程的文件结构一个能跑通的最小工程通常包含这几个文件fft.c放蝶形和位反转fft.h放宏定义和函数声明twiddle_q15.c放旋转因子表main.c做初始化和调用。下面是一个 Q15 基 2 FFT 的核心蝶形代码片段N 固定为 256输入输出都是 Q15。// fft_q15.c - 256点基2 FFTQ15格式每级缩放1位 #include fft.h // 旋转因子表Q15格式实部虚部交替存放 extern const int16_t twiddle_q15[256]; void fft_256_q15(int16_t *real, int16_t *imag) { uint16_t i, j, k, m; uint16_t le, le2; int16_t tr, ti, ur, ui; int32_t acc_r, acc_i; // 第一步位反转重排 j 0; for (i 0; i FFT_N - 1; i) { if (i j) { tr real[i]; real[i] real[j]; real[j] tr; ti imag[i]; imag[i] imag[j]; imag[j] ti; } k FFT_N 1; while (k j) { j - k; k 1; } j k; } // 第二步蝶形运算共 log2(N) 级 for (m 1; m LOG2_N; m) { le 1 m; // 当前级的蝶形跨度 le2 le 1; // 半跨度 for (j 0; j le2; j) { // 取旋转因子索引按级和位置计算 uint16_t tw_idx j * (FFT_N / le); int16_t wr twiddle_q15[2 * tw_idx]; int16_t wi twiddle_q15[2 * tw_idx 1]; for (i j; i FFT_N; i le) { // 蝶形t w * x[ile2] acc_r (int32_t)wr * real[i le2] - (int32_t)wi * imag[i le2]; acc_i (int32_t)wr * imag[i le2] (int32_t)wi * real[i le2]; tr (int16_t)(acc_r 15); // Q15乘法后右移15位 ti (int16_t)(acc_i 15); ur real[i]; ui imag[i]; // 每级缩放1位防止溢出 real[i] (ur tr) 1; imag[i] (ui ti) 1; real[i le2] (ur - tr) 1; imag[i le2] (ui - ti) 1; } } } }这段代码里FFT_N和LOG2_N在头文件里定义为 256 和 8。位反转部分用标准的 Gold-Rader 算法蝶形部分用三层循环最外层m控制级数中间j控制旋转因子索引最内层i做同址运算。关键参数是acc_r 15这个右移它把 Q15 乘 Q15 的 Q30 结果转回 Q15以及每级末尾的 1这是定点 FFT 的缩放策略。如果去掉 1N256 时输出会溢出成噪声如果改成每两级缩放一次中间结果需要 34 位以上32 位寄存器扛不住。2.5 旋转因子表的生成方式旋转因子表不建议手写用脚本生成最稳。下面这段 Python 生成 Q15 格式的 256 点旋转因子输出成 C 数组。# gen_twiddle.py - 生成Q15旋转因子表 import math N 256 scale 32767.0 print(const int16_t twiddle_q15[%d] { % (2 * N)) for k in range(N): angle 2.0 * math.pi * k / N re int(round(math.cos(angle) * scale)) im int(round(-math.sin(angle) * scale)) # 限幅防止round后超出int16范围 re max(-32768, min(32767, re)) im max(-32768, min(32767, im)) print( %d, %d, % (re, im)) print(};)生成时注意-sin的符号因为 FFT 正变换的旋转因子是负指数。如果符号搞反频谱会镜像很多人第一次调的时候看到频率轴反了就是这个原因。生成后把数组放进twiddle_q15.c用const修饰链接时编译器会把它放到 Flash 的只读段。3. 在 F28035 上把 FFT 跑起来的完整步骤3.1 工程配置与内存分配在 Code Composer Studio 里新建 C2000 工程后第一件事是改链接器命令文件.cmd把 FFT 的输入输出缓冲和旋转因子放到合适的内存段。F28035 的 RAM 分 M0、M1、L0、L1 几块L0 和 L1 通常各 1KBM0/M1 各 1KB。256 点 FFT 需要实部虚部各 256 个 int16共 1KB加上旋转因子 1KB至少要 2KB 连续空间。常见做法是把输入输出放 L0旋转因子放 Flash 的.const段运行时不用搬。下面是一个典型的 cmd 片段。MEMORY { PAGE 0: FLASH: origin 0x3E8000, length 0x10000 PAGE 1: L0L1RAM: origin 0x008000, length 0x0800 } SECTIONS { .fft_buffer : L0L1RAM, PAGE 1 .const : FLASH, PAGE 0 }然后在 C 代码里用#pragma DATA_SECTION把数组指到.fft_buffer。#pragma DATA_SECTION(fft_real, .fft_buffer) int16_t fft_real[FFT_N]; #pragma DATA_SECTION(fft_imag, .fft_buffer) int16_t fft_imag[FFT_N];这样链接器会把两个数组紧挨着放避免跨段访问带来的额外周期。如果 RAM 不够可以把虚部数组省掉用实序列 FFT 的打包技巧但代码复杂度会上升新手不建议一上来就这么做。3.2 ADC 采样与数据搬移FFT 的输入通常来自 ADC。F28035 的 ADC 是 12 位结果左对齐或右对齐可配。做 Q15 FFT 时把 12 位结果左移 4 位变成 16 位有符号数再减去直流偏置。采样率要和 FFT 点数匹配如果分析 50Hz 工频谐波想分辨到 5 次谐波250Hz采样率至少 1kHz256 点对应 256ms 窗口频率分辨率约 3.9Hz。下面是一段 ADC 中断里填缓冲的代码。// ADC中断服务函数每次转换完成调用 __interrupt void adc_isr(void) { static uint16_t idx 0; int16_t raw; // 读取12位ADC结果减去偏置2048左移4位转Q15 raw (int16_t)(AdcResult.ADCRESULT0 4) - 2048; raw raw 4; if (idx FFT_N) { fft_real[idx] raw; fft_imag[idx] 0; // 实序列虚部清零 idx; } else { // 缓冲满置标志主循环处理 fft_ready 1; idx 0; } AdcRegs.ADCINTFLGCLR.bit.ADCINT1 1; PieCtrlRegs.PIEACK.all PIEACK_GROUP1; }这里 4是因为 12 位结果在 16 位寄存器里左对齐右移 4 位得到实际值减 2048 是去掉 1.65V 左右的直流偏置再左移 4 位是为了让信号占满 Q15 的动态范围。如果信号幅度小可以少移几位但要注意别溢出。3.3 主循环调用与结果读取主循环里检测fft_ready标志调用fft_256_q15然后计算幅值。幅值是实部虚部平方和开根号定点开根号用牛顿迭代或查表。下面是一段计算幅值并找峰值的代码。// 主循环中处理FFT if (fft_ready) { fft_ready 0; fft_256_q15(fft_real, fft_imag); // 计算前N/2个点的幅值找最大值 uint16_t peak_idx 0; int32_t peak_mag 0; for (uint16_t i 1; i FFT_N / 2; i) { // 幅值平方Q15*Q15Q30右移15位回Q15 int32_t mag_sq ((int32_t)fft_real[i] * fft_real[i] (int32_t)fft_imag[i] * fft_imag[i]) 15; if (mag_sq peak_mag) { peak_mag mag_sq; peak_idx i; } } // peak_idx 对应频率 peak_idx * sample_rate / FFT_N g_peak_freq (uint32_t)peak_idx * SAMPLE_RATE / FFT_N; }注意这里只算前 N/2 个点因为实序列 FFT 的频谱对称后半部分是镜像。mag_sq用 Q30 表示比较时不用开根号省周期。如果要真实幅值再对peak_mag做一次开根号即可。3.4 用信号源验证从时域到频域的对照验证时不要一上来就接复杂信号先用信号发生器给一个 50Hz、1Vpp 的正弦波采样率设 1kHz看 FFT 峰值是否落在 bin 1350*256/1000≈12.8取整 13。如果峰值在 bin 13 但幅值偏小检查 Q15 缩放级数如果峰值在 bin 243镜像位置检查旋转因子符号如果整个频谱是噪声检查 ADC 数据是否真的填进了缓冲。我一般会在fft_256_q15调用前后各设一个 GPIO 翻转用示波器量执行时间256 点 Q15 FFT 在 60MHz 下大约 200~300 微秒如果超过 1 毫秒说明编译器优化没开或内存没放对。4. 避坑与排查定点 FFT 最容易翻车的五个地方4.1 频谱镜像旋转因子符号搞反现象是峰值出现在 N-k 的位置比如 50Hz 应该在第 13 根谱线结果出现在第 243 根。原因是旋转因子生成时用了sin而不是-sin或者蝶形里虚部乘法的符号写错。解决方法是重新生成旋转因子表确认im -sin(angle)并检查蝶形里acc_i的表达式是wr*imag wi*real而不是减号。这个坑我见过至少三次每次都是抄代码时没注意符号。4.2 输出溢出缩放策略没对齐现象是频谱底部整体抬高或者大信号时出现平顶。原因是蝶形运算没有每级缩放中间结果超出 int16。解决方法是确认每级蝶形后都有 1并且输入信号幅度不要超过 Q15 的 1/4留足余量。如果做 N1024 的 FFT每级缩放 1 位后总缩放 10 位输出幅值只有输入的 1/1024需要在最后乘回来否则看起来信号很弱。4.3 执行时间超标内存没放对或优化没开现象是 FFT 跑一次要几毫秒导致主循环卡顿。原因是输入输出数组放在了 Flash 或跨 RAM 块每次访问都有等待周期。解决方法是用#pragma DATA_SECTION把数组放到 L0/L1 连续 RAM并在工程设置里开-o3优化和--opt_for_speed。另外旋转因子如果放 RAM 会比 Flash 快但 RAM 紧张时放 Flash 也能接受因为旋转因子访问有规律缓存命中率高。4.4 频率分辨率不够采样率和点数没匹配现象是 50Hz 和 55Hz 分不开或者峰值落在两个 bin 之间。原因是频率分辨率等于采样率除以点数1kHz 采样 256 点只有 3.9Hz 分辨率。解决方法是提高点数到 512 或 1024或者降低采样率但降低采样率会限制最高分析频率。如果必须用 256 点可以在 FFT 后做插值但插值只能改善显示不能真正提高分辨率。4.5 直流偏置没去掉0 号 bin 淹没有用信号现象是频谱最左边一根谱线特别高其他谱线被压制。原因是 ADC 有 1.65V 左右的偏置没减掉就做 FFT直流分量会泄漏到相邻 bin。解决方法是在填缓冲时减去偏置或者在 FFT 后把 0 号 bin 置零。更彻底的做法是加一个简单的高通滤波器但会引入相位失真看应用是否在意。5. 进阶技巧用实序列打包和查表开根号再省一半周期5.1 实序列 FFT 的打包技巧如果输入是实信号可以把 2N 个实数打包成 N 个复数做一次 N 点 FFT再用旋转因子分离出 2N 点实序列的频谱。这样 256 点实序列只需要 128 点复数 FFT周期直接减半。代价是分离步骤需要额外的乘加但总体还是划算。下面是一个简化的分离代码框架。// 假设已对打包数据做完N/2点复数FFT // 分离出原实序列的频谱 for (k 0; k FFT_N / 4; k) { // 取对称点 uint16_t k2 (FFT_N / 2) - k; int16_t xr fft_real[k], xi fft_imag[k]; int16_t yr fft_real[k2], yi -fft_imag[k2]; // 共轭 // 用旋转因子组合具体公式略核心是加减和乘 // ... }这个技巧在 RAM 和周期都紧张时特别有用但调试难度比直接做 256 点高建议先把直接版跑通再改。5.2 定点开根号的查表实现幅值计算如果每个 bin 都开根号256 点要 128 次每次牛顿迭代十几次乘法周期很可观。实际做峰值检测时不需要开根号比较平方值即可。如果一定要幅值可以用查表加线性插值把 Q15 的平方值分成 256 段每段存一个近似根号值查表后插值。下面是一个查表开根号的示例。// 查表开根号输入Q15平方值输出Q15根号值 int16_t sqrt_q15(int32_t sq) { // sq范围0~2^30取高8位做索引 uint16_t idx (uint16_t)(sq 22); int32_t base sqrt_table[idx]; int32_t next sqrt_table[idx 1]; // 线性插值 int32_t frac (sq 14) 0xFF; return (int16_t)(base ((next - base) * frac 8)); }sqrt_table有 257 个条目每个 16 位占 514 字节。精度在 Q15 下约 1%够大多数监测应用。如果嫌占空间可以减到 65 个条目精度降到 3% 左右。5.3 用 GPIO 和定时器做周期测量调 FFT 时最怕「感觉慢但不知道慢在哪」。我的习惯是在 FFT 函数入口拉高一个 GPIO出口拉低用示波器量高电平宽度。F28035 的 GPIO 翻转只要几个周期对测量影响很小。如果手头没有示波器可以用 CPU 定时器读计数算差值。下面是一段用定时器测周期的代码。// 测FFT执行周期 CpuTimer0Regs.TCR.bit.TSS 1; // 停定时器 CpuTimer0Regs.TIM.all 0xFFFFFFFF; // 重置 CpuTimer0Regs.TCR.bit.TSS 0; // 启动 GpioDataRegs.GPASET.bit.GPIO0 1; // 拉高 fft_256_q15(fft_real, fft_imag); GpioDataRegs.GPACLEAR.bit.GPIO0 1; // 拉低 CpuTimer0Regs.TCR.bit.TSS 1; // 停 uint32_t cycles 0xFFFFFFFF - CpuTimer0Regs.TIM.all;cycles就是 FFT 占用的 CPU 周期数除以主频就是时间。60MHz 下 256 点 Q15 FFT 如果超过 20000 周期约 333 微秒就要检查优化和内存布局了。5.4 从 256 点扩到 1024 点的参数调整如果应用需要更高分辨率把 N 从 256 改成 1024需要同步改这几个地方FFT_N和LOG2_N宏、旋转因子表长度、位反转循环的FFT_N 1、蝶形级数LOG2_N、内存分配大小实部虚部各 2KB共 4KB。F28035 的 RAM 可能不够这时要把旋转因子放 Flash输入输出用实序列打包技巧省一半空间。另外每级缩放 1 位后总缩放 10 位输出幅值只有输入的 1/1024需要在最后乘 1024 或调整 Q 格式。我一般会在 1024 点版本里把输入左移 6 位而不是 4 位补偿一部分缩放损失。5.5 一个我常犯的错误忘了清虚部实序列 FFT 要求虚部初始为零但如果在中断里填缓冲时只填了实部虚部保留了上一轮的值频谱就会混入莫名其妙的成分。这个 bug 很隐蔽因为第一轮结果是对的第二轮开始才出错。后来我养成了习惯在fft_ready置位后、调用 FFT 前用memset把虚部数组清零或者干脆在填缓冲时显式写fft_imag[idx] 0。多这一条语句省下几个小时的排查。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑