☰
TMS320F28035定点FFT实战:Q15频谱分析与避坑指南
2026/11/5 4:17:16 网站建设 项目流程

简介:面向数字信号处理初学者与嵌入式开发者的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)是 2,sizeof(long)是 4,Q15 的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.5KB,F28035 的 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 点数或窗函数,都强制先用一个已知频率的正弦跑一遍峰值位置和幅值,确认无误再接入实际信号,这个习惯帮我挡掉了好几次表没更新、移位量不对的低级错误。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询