资讯动态

TMS320F28035定点FFT实战:从Q15格式到频谱避坑指南

发布时间:2026/10/13 19:45:41 来源:尧图企业网站定制
简介这份资源面向数字信号处理初学者与嵌入式DSP开发者聚焦在TI TMS320F28035浮点DSP上实现快速傅里叶变换的完整工程代码可用于频谱分析、滤波等实时信号处理场景的学习与移植。压缩包共约2000个文件整体10.33MB以528个C源码与237个头文件为核心配合304套CCS工程与构建配置、104个链接命令文件、49个汇编文件及若干GEL调试脚本、数学库与说明文档完整保留了Code Composer Studio下的工程结构。代码覆盖位反转、蝶形运算、旋转因子预计算、复数乘法与数据缓冲等FFT关键环节并涉及CLA协处理器与浮点数学库的调用方式。已有548人学习下载适合希望理解FFT算法落地细节、对照工程调试与验证结果的读者参考。1. 从一份 TMS320F28035 FFT 代码说起它到底能解决什么问题如果你正在用 TMS320F28035 做电机控制、数字电源或者并网逆变器大概率会遇到一个绕不开的需求把 ADC 采回来的时域波形转成频域看谐波分布、做锁相或者分析振动。这时候你需要的不是 MATLAB 里那条fft(x)而是一份能在 C2000 定点 DSP 上跑起来、不依赖操作系统、RAM 占用可控的 FFT 实现。这份 TMS320F28035 FFT 代码就是干这个的——它把 TI 官方 C28x 定点库里的 RFFT 例程做了裁剪和适配直接对着 F28035 的存储映射和 IQmath 风格的数据格式来写拿来就能往工程里塞。它适合两类人一类是刚接手 F28035 项目、被 16 位定点 Q 格式和位反转折腾得够呛的新手另一类是已经用过 C28x 库、但想搞清楚旋转因子表怎么生成、蝶形运算怎么对齐、结果怎么换算回真实幅值的熟手。代码本身不复杂坑都在细节里——采样点数必须是 2 的幂、输入要按 Q15 归一化、输出要除以 N/2 才是单边谱幅值。下面我按“先跑通、再调参、最后避坑”的顺序拆一遍。2. F28035 上跑 FFT 的底层逻辑定点、Q 格式与存储布局2.1 为什么 F28035 必须用定点 FFTTMS320F28035 是 C28x 内核加 CLA 协处理器的定点芯片主频 60MHz没有硬件浮点单元。你当然可以用软件浮点库但一次 256 点浮点 FFT 在 60MHz 上跑下来要几毫秒电机控制环路根本等不起。定点 FFT 把三角函数和乘法都压到 Q15 格式里用__IQmpy或者直接(long)a * b 15来做256 点 RFFT 大概 30 到 50 微秒就能出结果这才是能塞进 10kHz 控制中断里的量级。常见做法是直接用 TI 的C28x_FPU_Lib或者C28x_IQmath_Lib里的 RFFT 函数但那些库默认按 32 位数据宽度和特定内存段来组织。F28035 的 RAM 只有 16KB 左右留给 FFT 的缓冲区往往只有 1KB 到 2KB所以必须把输入输出都压成 16 位旋转因子表也存成 Q15。这份代码就是按这个约束来写的。2.2 Q15 格式与位反转两个最容易翻车的地方Q15 的意思是用一个 16 位有符号整数表示 -1 到 1 之间的小数具体是实际值 整数 / 32768。ADC 采回来的 12 位无符号数要先减 2048 偏置再左移 4 位变成 Q15。这一步如果忘了减偏置频谱里会冒出一个巨大的直流分量把交流谐波全淹了。位反转是另一个玄学点。FFT 的输入必须是位反转顺序比如 8 点 FFT 的输入索引顺序是 0,4,2,6,1,5,3,7。TI 的库函数RFFT_f32内部会做位反转但如果你自己写蝶形就得手动排。我一般会先用一个小数组验证位反转表对不对再往大点数上套。// 将 ADC 原始值转为 Q15 格式N 为点数 #define ADC_OFFSET 2048 #define ADC_SCALE 16 // 左移 4 位12 位变 16 位 void ADC_To_Q15(int16 *adc_buf, int16 *q15_buf, Uint16 N) { Uint16 i; for(i 0; i N; i) { // 减偏置后左移注意结果可能溢出实际用 long 中转 long tmp ((long)adc_buf[i] - ADC_OFFSET) 4; // 饱和处理防止超过 Q15 范围 if(tmp 32767) tmp 32767; if(tmp -32768) tmp -32768; q15_buf[i] (int16)tmp; } }这段代码里ADC_OFFSET和ADC_SCALE要根据实际 ADC 位数改。12 位 ADC 满量程 4096偏置 2048左移 4 位刚好把 ±2048 映射到 ±32768。如果你用的是 16 位外部 ADC偏置和移位都要重算。饱和处理不能省否则电机突加负载时 ADC 打满Q15 溢出会变成反相的大负数频谱直接乱掉。2.3 旋转因子表存 RAM 还是算实时旋转因子就是cos(2πk/N)和sin(2πk/N)的 Q15 值。256 点 FFT 需要 128 组 cos/sin 对每组两个 16 位一共 512 字节。F28035 的 RAM 够放但如果你同时跑多个 FFT 或者点数更大就得考虑放 Flash 里用const修饰运行时用memcpy搬到 RAM 或者直接寻址。我一般会把旋转因子表放在 Flash 的.const段链接器会自动处理。但要注意 C28x 的 Flash 访问速度比 RAM 慢如果 FFT 在中断里跑最好在初始化时把表拷到 RAM 里的一个专用数组用#pragma DATA_SECTION指定到FFT_TABLE段。这样每次蝶形读表都是单周期访问不会拖慢中断。// 旋转因子表N256 时只需前 128 组 #pragma DATA_SECTION(fft_twiddle, FFT_TABLE) const int16 fft_twiddle[256] { // cos 表 128 个sin 表 128 个均为 Q15 // 具体数值由 MATLAB 或 Python 生成后粘贴 };生成这张表的时候Python 里用int(round(math.cos(2*math.pi*k/N) * 32767))就行注意正负号别搞反。sin 表的第一项是 0cos 表第一项是 32767如果发现频谱相位不对先查这两个值。3. 把代码跑起来从工程配置到第一次出频谱3.1 工程里要加的源文件和链接器配置拿到这份代码后别急着全塞进现有工程。先建一个最小测试工程只加三个文件fft.c、fft_twiddle.c和main.c。fft.c里是蝶形运算和位反转fft_twiddle.c放旋转因子表main.c里造一个 256 点的正弦波加三次谐波跑完 FFT 后把幅值打到数组里用 CCS 的 Graph 功能看频谱。链接器文件里要加一段// 在 .cmd 文件里定义 FFT 专用段 MEMORY { PAGE 0: FLASH : origin 0x3E8000, length 0x008000 PAGE 1: RAM : origin 0x000400, length 0x000400 } SECTIONS { FFT_TABLE : RAM, PAGE 1 .text : FLASH, PAGE 0 }这里FFT_TABLE段被放到 RAM 的 0x400 起始处长度 0x400 刚好 1KB够放 256 点旋转因子表加输入输出缓冲。如果你的工程 RAM 更紧张可以把表留在 Flash只把输入输出缓冲放 RAM。3.2 调用 FFT 函数的完整流程假设你已经把 ADC 数据转成了 Q15 存在fft_input数组里接下来就是调 FFT 和算幅值。代码里一般会提供FFT_Run(int16 *input, int16 *output, Uint16 N)这样的接口内部先做位反转再做 log2(N) 级蝶形。#define FFT_SIZE 256 int16 fft_input[FFT_SIZE]; int16 fft_output[FFT_SIZE]; // 实部在前虚部在后各 128 个 int16 fft_mag[FFT_SIZE/2]; // 单边谱幅值 void main(void) { Uint16 i; // 1. 造测试数据50Hz 基波 150Hz 三次谐波采样率 12.8kHz for(i 0; i FFT_SIZE; i) { float t (float)i / 12800.0; float sig 0.8 * sin(2*3.14159*50*t) 0.2 * sin(2*3.14159*150*t); fft_input[i] (int16)(sig * 32767); } // 2. 执行 FFT FFT_Run(fft_input, fft_output, FFT_SIZE); // 3. 计算单边谱幅值sqrt(re^2 im^2) / (N/2) for(i 0; i FFT_SIZE/2; i) { long re fft_output[i]; long im fft_output[i FFT_SIZE/2]; long mag sqrt(re*re im*im); // 实际用 IQsqrt 或查表 fft_mag[i] (int16)(mag / (FFT_SIZE/2)); } while(1); }这段代码里FFT_Run的第二个参数fft_output是打包格式前 128 个是实部后 128 个是虚部。这是 TI C28x 库的惯例不是标准复数交错格式别搞混。算幅值时除以N/2是因为定点 FFT 每级蝶形都会让幅值翻倍最后要归一化。如果你只关心相对大小不除也行但要做谐波百分比就得除。3.3 用 CCS Graph 验证频谱对不对跑完上面这段在 CCS 里把fft_mag加到 Graph 窗口设置 Q15 格式采样率填 12800点数填 128。正确的波形应该在 50Hz 和 150Hz 处各有一个峰50Hz 的幅值接近 0.8150Hz 接近 0.2。如果峰的位置对但幅值差一倍检查归一化除数如果峰的位置偏移检查采样率和点数是否匹配如果满屏都是杂波大概率是 Q15 溢出或者位反转错了。提示第一次跑通前先把输入数据固定成已知正弦别直接接 ADC。ADC 的噪声和偏置会让频谱看起来像一锅粥分不清是算法问题还是信号问题。4. 避坑与排查F28035 FFT 最常见的五个翻车点4.1 现象频谱里 0Hz 处有一个巨大峰值交流成分被压得很低原因ADC 原始值没减偏置或者减了偏置但没做 Q15 归一化导致输入信号里混入了很大的直流分量。FFT 的 0 号输出就是所有输入之和直流越大这个值越夸张。解决在ADC_To_Q15里确认ADC_OFFSET等于 ADC 满量程的一半。12 位 ADC 是 204816 位是 32768。减完之后再左移左移位数要保证最大交流幅值不超过 32767。如果 ADC 参考电压不是 3.3V偏置值不变但实际信号幅值要重新折算。4.2 现象FFT 结果每次都不一样同一路信号跑两次频谱形状变了原因输入缓冲区被其他中断改写或者 FFT 函数内部用了全局变量做临时存储而中断里又调了同一个函数。F28035 的中断嵌套很常见如果 FFT 没做可重入保护数据就会串。解决给 FFT 函数加一个忙标志或者把输入输出缓冲都定义成局部静态数组确保每次调用前数据已经稳定。更稳妥的做法是在 ADC 中断里只做采样把数据存到乒乓缓冲主循环里再跑 FFT。如果非要在中断里跑把 FFT 的临时数组也放到栈上别用全局。4.3 现象编译通过但运行到 FFT 函数就进ILLEGAL_ISR或跑飞原因旋转因子表被链接到了 Flash 但没初始化或者FFT_TABLE段和别的段重叠了。C28x 的 Flash 里const数据默认是初始化过的但如果你手动指定了段又没在.cmd里正确映射表里的值就是 0xFFFF蝶形运算直接溢出。解决在.cmd里确认FFT_TABLE段有明确的LOAD和RUN地址。如果表放 RAM要确保启动代码里有从 Flash 到 RAM 的拷贝循环。简单办法是先不指定段让编译器自动分配跑通后再优化。4.4 现象256 点 FFT 跑一次要 200 多微秒中断周期不够用原因旋转因子表在 Flash 里每次蝶形读表都要等 Flash 周期或者用了软件浮点做幅值计算sqrt和除法拖慢了整体。解决把旋转因子表拷到 RAM用memcpy在初始化时做一次。幅值计算改用 IQmath 的IQsqrt或者查表法别用math.h里的sqrt。如果还嫌慢把 FFT 拆成两级第一级在中断里做第二级在主循环做但这样会引入延迟锁相环参数要重调。4.5 现象频谱峰值位置对但幅值比理论值小一半或大一倍原因归一化除数搞错了。定点 FFT 每级蝶形让幅值乘 2N 点 FFT 共 log2(N) 级所以最终幅值要除以 N。但单边谱只取前 N/2 个点能量等效于除以 N/2。如果你用了 TI 库的RFFT_f32它内部已经做了 1/N 缩放你再除就多除了。解决先确认你用的 FFT 实现有没有内部缩放。自己写的蝶形一般没有要手动除 N/2。TI 的定点 RFFT 有些版本会在最后一级做 1/2 缩放累计缩放因子是 1/N这时候单边谱幅值直接取模就行不用再除。最笨也最可靠的办法输入一个幅值为 1 的直流所有点都是 32767看 0 号输出是多少反推缩放系数。5. 进阶技巧用 CLA 分担 FFT 计算与定点溢出保护5.1 把 FFT 拆到 CLA 上跑F28035 的 CLA 是一个独立浮点协处理器虽然主频也是 60MHz但它不占用 C28x 的流水线。如果你的 FFT 点数不大128 点以内可以把蝶形运算搬到 CLA 上C28x 只负责 ADC 采样和结果读取。CLA 支持浮点所以旋转因子可以直接用float省掉 Q15 转换的麻烦。具体做法是在 CLA 里定义一个__interrupt void cla_fft_task(void)把输入数据通过CLAtoCPU消息 RAM 传进去算完再传回来。CLA 的 FFT 代码可以直接用 TI 的CLA_FPU_Lib里的RFFT_f32但要注意 CLA 的 RAM 更小128 点 FFT 的输入输出加旋转因子表大概要 1.5KB得仔细分配。// CLA 任务里调用浮点 FFT 的片段 __interrupt void cla_fft_task(void) { // 从消息 RAM 读输入 float *in (float *)0x0000A000; float *out (float *)0x0000A400; // 调用 CLA 浮点 RFFT点数 128 RFFT_f32(in, out, 128, 0); // 通知 C28x 算完了 Cla1Regs.MIFRC.bit.INT1 1; }这段代码里0x0000A000是 CLA 消息 RAM 的地址具体值查 F28035 手册的 CLA 数据 RAM 映射。RFFT_f32的第四个参数是缩放标志0 表示不缩放1 表示除以 N。CLA 跑浮点 FFT 的速度和 C28x 跑定点差不多但省了 Q15 转换和溢出判断代码更干净。5.2 定点溢出保护饱和与缩放策略定点 FFT 最怕中间级溢出。256 点 Q15 输入如果信号幅值接近满量程第一级蝶形后实部虚部就可能超过 32767。常见做法有两种一是每级蝶形后右移 1 位相当于整体除以 N但会损失精度二是只在最后一级做饱和判断中间用 32 位累加。我一般用第二种蝶形里的乘加用long做结果存回int16前判断是否超过 ±32767超了就钳位。这样只在最后一级损失一点精度中间过程不会溢出。代价是每次蝶形多两条比较指令256 点 FFT 大概多 10 微秒换来的稳定性很值。// 带饱和的蝶形运算核心 #define SATURATE(x) ((x) 32767 ? 32767 : ((x) -32768 ? -32768 : (x))) void butterfly(int16 *a, int16 *b, int16 wr, int16 wi) { long tr (long)(*b) * wr - (long)(*(b1)) * wi; long ti (long)(*b) * wi (long)(*(b1)) * wr; tr 15; // Q15 乘法后右移 ti 15; int16 ar *a; int16 ai *(a1); *a SATURATE(ar (int16)tr); *(a1) SATURATE(ai (int16)ti); *b SATURATE(ar - (int16)tr); *(b1) SATURATE(ai - (int16)ti); }这段里wr和wi是旋转因子的实部和虚部a和b指向两个复数点。注意tr和ti右移 15 位之前是long不会溢出。饱和宏在加减之后立即生效保证写回的都是合法 Q15。如果你发现频谱底噪比预期高先把饱和去掉跑一次对比一下是不是钳位引入的谐波。5.3 验证 FFT 结果的一个笨办法拿信号发生器给一个 1kHz 正弦幅值 1VADC 参考 3.3V那么 Q15 输入幅值大概是1/3.3*32767 ≈ 9929。跑完 FFT 后1kHz 对应的 bin 是1000/12800*256 20看第 20 个fft_mag是不是接近 9929。如果差得远先查 ADC 前端有没有衰减或放大再查归一化除数。这个办法虽然土但能一次性把信号链和算法链都串起来验证。从那以后我每次调 FFT都先拿信号发生器打一个单频正弦确认 bin 位置和幅值都对再上真实信号。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑