
简介面向数字信号处理初学者与嵌入式开发者的TMS320F28035 FFT实现代码包聚焦在TI C28x浮点DSP上完成快速傅里叶变换这一核心任务。内容围绕蝶形运算结构、位反转排序、旋转因子预计算与复数乘法等关键环节展开并涉及CCS开发环境下的工程配置与浮点数学库调用可服务于频谱分析、滤波等实时信号处理场景的学习与移植参考。压缩包共约2000个文件、10.33MB以528个C源文件与237个头文件为主体辅以大量CCS工程配置cdtbuild、project、ccsproject、链接命令文件cmd、GEL调试脚本及汇编与CLA相关文件另含PDF文档与少量图片资料工程结构完整、层次分明。已有552人学习下载适合希望从源码层面理解FFT落地细节、对照调试并积累DSP工程组织经验的读者。1. 从一段跑不动的 FFT 说起TMS320F28035 上的定点频谱分析代码很多人第一次在 DSP 上做频谱分析都是把 PC 上跑得好好的浮点 FFT 直接搬到板子上结果编译能过、下载能跑串口吐出来的数据却是一堆乱码或者全零。问题不在算法本身而在 TMS320F28035 这颗芯片的定位它是定点 DSP主频 60MHz片上 RAM 只有十几 KB没有硬件浮点单元。这份 FFT 代码就是围绕这些约束写出来的包含定点蝶形运算、旋转因子表、位反转重排和窗函数处理目标是在有限的算力和内存里把一段 ADC 采样变成可用的幅频谱。适合正在用 C2000 系列做电机控制、电源谐波检测、音频特征提取的开发者尤其是那些被浮点移植坑过一次、想找一份能直接对照改的定点实现的人。2. 定点 FFT 的架构拆解从采样到幅频谱的完整链路2.1 为什么 F28035 上必须用定点实现TMS320F28035 属于 C2000 系列CPU 是 32 位定点内核指令集里没有单精度浮点的硬件加速。用软件浮点库当然能跑 FFT但一次 256 点浮点 FFT 的耗时可能是定点版本的十几倍而且浮点库会额外占用宝贵的 Flash 和 RAM。这份代码选择 Q15 定点格式也就是用 16 位有符号整数表示 [-1, 1) 区间的小数乘加运算直接走芯片的硬件乘法器一个蝶形运算几个周期就能完成。Q15 的核心约定是数值 32767 代表接近 1.0-32768 代表 -1.0。两个 Q15 数相乘结果是 Q30 格式需要右移 15 位再饱和回 Q15。这个移位和饱和处理是整份代码里最容易翻车的地方后面避坑章节会专门讲。代码里旋转因子表也是 Q15 格式预先算好的存在 Flash 的 const 数组里运行时直接查表避免在中断里调用三角函数。常见做法是用脚本在 PC 上生成一张 256 点的 cos/sin 表按 Q15 量化后粘贴进头文件这样既省算力又保证精度可控。2.2 目录结构与模块划分这份代码的组织方式比较朴素没有复杂的工程框架核心文件大致如下文件作用fft_q15.cFFT 主流程包含蝶形运算和位反转fft_q15.h函数声明、点数宏定义、Q15 类型定义twiddle_q15.h旋转因子表const 数组window_q15.h汉宁窗或汉明窗系数表main.c初始化、ADC 采样触发、结果输出这种扁平结构对新手友好打开就能看到全部逻辑不需要在多层目录里找入口。实际工程里我一般会把 ADC 驱动和 SCI 输出单独拆成 bsp 层但这份代码为了突出 FFT 本身把外设操作压缩在 main.c 里读起来反而更直接。2.3 蝶形运算的核心实现FFT 采用基 2 时域抽取点数通过宏FFT_SIZE配置常见取值 128、256、512。下面这段是蝶形运算的核心循环做了 Q15 乘法和饱和处理/* 基2 DIT 蝶形每级 group 个蝶形跨距 half */ for (stage 1; stage log2n; stage) { half 1 (stage - 1); /* 本级蝶形跨距的一半 */ for (group 0; group FFT_SIZE; group (half 1)) { for (k 0; k half; k) { /* 查表取旋转因子索引按本级步长抽取 */ wr twiddle_cos[k * (FFT_SIZE / (half 1))]; wi -twiddle_sin[k * (FFT_SIZE / (half 1))]; idx1 group k; idx2 idx1 half; /* Q15 复数乘法实部 ar*wr - ai*wi */ tr (int32_t)real[idx2] * wr - (int32_t)imag[idx2] * wi; ti (int32_t)real[idx2] * wi (int32_t)imag[idx2] * wr; /* Q30 右移回 Q15并做饱和 */ tr sat_q15(tr 15); ti sat_q15(ti 15); real[idx2] real[idx1] - tr; imag[idx2] imag[idx1] - ti; real[idx1] real[idx1] tr; imag[idx1] imag[idx1] ti; } } }这段代码有几个参数需要说清楚。log2n是点数的以 2 为底对数256 点就是 8循环跑 8 级。half控制每级蝶形的跨距第一级是 1最后一级是 128。旋转因子索引里的FFT_SIZE / (half 1)是步长保证每一级取到正确间隔的表项。sat_q15是一个内联函数把超出 16 位范围的中间结果钳到 [-32768, 32767]。没有这个饱和累加溢出会让频谱出现莫名其妙的杂散而且这种错误在时域波形上看不出来只有对比才知道。2.4 位反转与窗函数的配合时域抽取的 FFT 要求输入序列按位反转顺序排列输出才是自然顺序。代码里用一个独立的位反转函数在蝶形之前重排数组/* 位反转重排把输入按二进制倒序搬移 */ for (i 0; i FFT_SIZE; i) { j bit_reverse(i, log2n); if (j i) { tmp real[i]; real[i] real[j]; real[j] tmp; tmp imag[i]; imag[i] imag[j]; imag[j] tmp; } }bit_reverse用移位和掩码实现不查表省一张 256 字节的表。j i的判断避免重复交换这个细节很多人会漏导致数据被换回去。窗函数在采样之后、位反转之前施加。汉宁窗系数同样是 Q15 表逐点相乘后右移 15 位。加窗的目的是压低频谱泄漏代价是主瓣变宽。如果做谐波检测主瓣宽度要能容忍相邻谐波间隔256 点在 10kHz 采样率下频率分辨率约 39Hz汉宁窗主瓣大约占 2 个 bin够用。3. 把代码跑起来工程配置、采样与结果验证3.1 建立工程与关键编译选项在 Code Composer Studio 里新建 C2000 工程器件选 TMS320F28035编译器版本按手头安装的选。把 fft_q15.c、main.c 加入工程头文件路径指向当前目录。几个编译选项值得注意优化等级先用-O0或-O1调试确认结果正确后再开-O2高优化下某些中间变量可能被重排影响断点观察。确认sizeof(int)是 2sizeof(long)是 4Q15 的int16_t和中间结果的int32_t类型定义要对得上。如果工程里同时有浮点库检查链接时没有把浮点运行时拖进来否则 Flash 占用会明显变大。常见做法是在fft_q15.h里用typedef signed short q15_t;和typedef signed long q31_t;明确位宽不依赖编译器默认值。3.2 ADC 采样与数据搬运main.c 里的流程是配置 ADC 为连续采样或由 ePWM 触发采样率由定时器决定采满FFT_SIZE个点后置标志主循环调用 FFT。下面是一个简化的采样搬运片段/* ADC 中断里搬运一个点采满一帧后置位 */ __interrupt void adc_isr(void) { adc_buf[sample_idx] AdcResult.ADCRESULT0 - 2048; /* 去直流偏置 */ if (sample_idx FFT_SIZE) { sample_idx 0; frame_ready 1; } AdcRegs.ADCST.bit.INT_SEQ1_CLR 1; PieCtrlRegs.PIEACK.all PIEACK_GROUP1; }ADCRESULT0是 12 位结果范围 0 到 4095减去 2048 把零点移到中间再左移若干位映射到 Q15 的动态范围。具体左移几位要看信号幅度移位太多会削顶太少则信噪比不够。我一般先用示波器看 ADC 输入摆幅保证峰值不超过满量程的 80%再定移位量。frame_ready是主循环和中断之间的握手标志主循环看到置位后把adc_buf拷进 FFT 的工作数组清标志再算。工作数组和采样数组分开避免计算期间新采样覆盖数据。3.3 幅值计算与串口输出FFT 输出是复数实际看频谱要算幅值。代码里用近似求模避免开方/* 近似幅值max 0.5*min比 sqrt 快很多 */ for (i 0; i FFT_SIZE / 2; i) { a abs(real[i]); b abs(imag[i]); mag[i] (a b) ? (a (b 1)) : (b (a 1)); }只取前一半点因为实序列 FFT 的频谱对称。mag数组通过 SCI 以文本或二进制形式发到上位机波特率 115200 足够 256 点每秒刷新几次。验证时先给一个已知频率的正弦看峰值 bin 是否落在预期位置再给方波看谐波分布两步都对上基本可以确认整条链路没问题。提示第一次跑建议在 RAM 里调试把FFT_SIZE设成 64 点单步看蝶形前后数值确认 Q15 移位没有溢出再放大到 256 点。4. 避坑与排查定点 FFT 最容易翻车的五个地方4.1 频谱全是零或只有直流一个峰现象是串口输出的幅值数组除了第 0 点其余全是 0 或极小值。原因通常是 ADC 数据没有正确映射到 Q15或者去直流偏置后整体幅度太小右移次数过多把有效位全移没了。解决方法是先把adc_buf的原始值打印出来确认有变化再检查映射时的移位量保证信号峰值落在 Q15 的 1/4 到 1/2 满量程之间。4.2 峰值位置偏移一个 bin给 1kHz 正弦峰值却出现在相邻 bin。原因多半是采样率和FFT_SIZE的对应关系没算对或者窗函数施加位置错了。频率分辨率等于采样率除以点数1kHz 在 10kHz 采样、256 点下对应第 25.6 个 bin不会正好落在整数上出现泄漏是正常的。要精确对齐可以调整采样率让目标频率落在 bin 中心或者用插值修正。先确认frame_ready后搬运的是完整一帧没有半帧新旧混合。4.3 高优化等级下结果和低优化不一致-O0正常、-O2出错常见于中间变量被优化掉或者 Q15 移位依赖了未定义行为。检查所有int32_t中间结果是否显式声明移位前是否保证非负饱和函数是否用了volatile或内联汇编防止被重排。我一般会在关键蝶形里加一个volatile的调试变量确认优化后数值仍然对再删掉。4.4 中断里算 FFT 导致采样丢点把 FFT 放进 ADC 中断256 点计算时间超过采样间隔后续中断被阻塞。FFT 必须放在主循环中断只做搬运和置标志。如果主循环还有其他耗时任务可以用双缓冲一帧在算另一帧在采算完交换指针。RAM 够的话这是最稳的做法。4.5 旋转因子表点数不够或索引越界改FFT_SIZE后忘了重新生成旋转因子表或者索引公式里的步长算错会读到表外数据频谱出现规律性杂散。每次改点数用脚本重新生成twiddle_q15.h并在代码里加一句编译期断言确保表长度等于FFT_SIZE。索引计算建议单独写一个宏所有地方统一调用避免手写公式不一致。5. 二次开发换点数、换窗、换输出方式的改造路径拿到这份代码后最常见的改造是换 FFT 点数。把FFT_SIZE从 256 改成 512需要同步做三件事重新生成 512 点的旋转因子表和窗表确认 RAM 里real、imag、mag三个数组放得下512 点复数各占 2 字节加上幅值约 2.5KBF28035 的 RAM 够用但要检查链接映射确认主循环计算时间仍小于一帧采样时间必要时提高主频或降低采样率。改完先用 64 点小规模验证逻辑再上目标点数这个习惯能省很多调试时间。换窗函数更简单把window_q15.h里的系数表替换掉施加窗的循环不用动。汉宁窗适合一般频谱观察汉明窗旁瓣更低适合强弱信号并存如果只关心某个频点的幅值平顶窗幅度精度更好但主瓣更宽。选窗本质是主瓣宽度和旁瓣抑制的取舍按实际信号特征定。输出方式上串口文本适合调试但 256 点每帧发 ASCII 会占不少时间。改成二进制打包每点两个字节一帧 512 字节115200 波特率下几十毫秒发完。上位机用 Python 的struct.unpack解析画图用 matplotlib比在 IDE 里看数组直观得多。下面是一个解析片段import serial, struct import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200) raw ser.read(512) # 256 点每点 int16 mag struct.unpack(256h, raw) # 小端有符号短整型 plt.plot(mag) plt.show()256h里的表示小端h是 16 位有符号和 DSP 端int16_t对应。如果 DSP 是大端模式要改成C2000 是小端一般不用改。还有一个值得做的改造是把 FFT 结果用于实时判断比如检测某几个谐波 bin 的幅值是否超过阈值超了就置故障标志。这时候不需要输出整帧频谱只算目标 bin 的幅值省掉串口和上位机环节响应更快。阈值要留滞回避免在边界反复跳变。从那以后我每次改 FFT 点数或窗函数都强制先用一个已知频率的正弦跑一遍峰值位置和幅值确认无误再接入实际信号这个习惯帮我挡掉了好几次表没更新、移位量不对的低级错误。希望帮到你。本文还有配套的精品资源点击获取