简介:这份资源面向数字信号处理初学者与嵌入式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 必须用定点 FFT
TMS320F28035 是 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段。这样每次蝶形读表都是单周期访问,不会拖慢中断。
// 旋转因子表,N=256 时只需前 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 表的第一项是 0,cos 表第一项是 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.8,150Hz 接近 0.2。如果峰的位置对但幅值差一倍,检查归一化除数;如果峰的位置偏移,检查采样率和点数是否匹配;如果满屏都是杂波,大概率是 Q15 溢出或者位反转错了。
提示:第一次跑通前,先把输入数据固定成已知正弦,别直接接 ADC。ADC 的噪声和偏置会让频谱看起来像一锅粥,分不清是算法问题还是信号问题。
4. 避坑与排查:F28035 FFT 最常见的五个翻车点
4.1 现象:频谱里 0Hz 处有一个巨大峰值,交流成分被压得很低
原因:ADC 原始值没减偏置,或者减了偏置但没做 Q15 归一化,导致输入信号里混入了很大的直流分量。FFT 的 0 号输出就是所有输入之和,直流越大,这个值越夸张。
解决:在ADC_To_Q15里确认ADC_OFFSET等于 ADC 满量程的一半。12 位 ADC 是 2048,16 位是 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 每级蝶形让幅值乘 2,N 点 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)(*(b+1)) * wi; long ti = (long)(*b) * wi + (long)(*(b+1)) * wr; tr >>= 15; // Q15 乘法后右移 ti >>= 15; int16 ar = *a; int16 ai = *(a+1); *a = SATURATE(ar + (int16)tr); *(a+1) = SATURATE(ai + (int16)ti); *b = SATURATE(ar - (int16)tr); *(b+1) = SATURATE(ai - (int16)ti); }这段里wr和wi是旋转因子的实部和虚部,a和b指向两个复数点。注意tr和ti右移 15 位之前是long,不会溢出。饱和宏在加减之后立即生效,保证写回的都是合法 Q15。如果你发现频谱底噪比预期高,先把饱和去掉跑一次,对比一下是不是钳位引入的谐波。
5.3 验证 FFT 结果的一个笨办法
拿信号发生器给一个 1kHz 正弦,幅值 1V,ADC 参考 3.3V,那么 Q15 输入幅值大概是1/3.3*32767 ≈ 9929。跑完 FFT 后,1kHz 对应的 bin 是1000/12800*256 = 20,看第 20 个fft_mag是不是接近 9929。如果差得远,先查 ADC 前端有没有衰减或放大,再查归一化除数。这个办法虽然土,但能一次性把信号链和算法链都串起来验证。
从那以后我每次调 FFT,都先拿信号发生器打一个单频正弦,确认 bin 位置和幅值都对,再上真实信号。希望帮到你。
本文还有配套的精品资源,点击获取