DSP28335上搞定FFT频谱分析:从原理到工程落地
发布时间:2026/10/5 4:30:57 作者:尧图编辑部 阅读量:1,286

在电力电子和电机控制的现场项目里我遇到过一个特别典型的诉求系统已经在用DSP28335做双闭环控制突然客户要求“顺便”把电流波形、振动波形做一套FFT频谱分析出来。DSP28335做FFT傅里叶变换这件事听起来像是大学数字信号处理课的课后作业但真正放到工程现场牵扯到ADC采样链路、定点格式、库函数选型、频谱泄漏校准一堆问题。这篇就把我在28335上把FFT真正跑起来的完整过程写出来包括芯片资源怎么用、TI官方库怎么调、采样率怎么定、相位为什么对不上以及那些文档里不会写的坑。如果你手头也是28335这类C28x控制类MCU又不想为了FFT额外加一片STM32或者FPGA这篇文章应该能帮你省掉至少一周的摸索时间。1. DSP28335做FFT到底香在哪从控制芯片到频谱分析的角色延伸1.1 什么场景下会盯上28335而不是别人我最早接触28335是因为电机控制片内12位ADC、PWM模块、正交编码接口全都有一块芯片就能把电流环、速度环、位置环全包了。但后来项目加了需求要监测定子电流的谐波含量或者把减速箱的振动信号做实时频谱。这时候摆在面前的无非三条路再加一片专门做采集分析的MCU/FPGA或者在现有28335上硬塞一个FFT。第一条路硬件成本、通信时序、同步问题都来了划不来。第二条路的核心矛盾是28335没有硬件浮点单元CPU主频150MHz片内SRAM也就几十KB量级做1024点FFT到底扛不扛得住我的实测结论是扛得住而且余量不小。用TI官方针对C28x优化的32位定点FFT库1024点实数FFT单次计算时间大约在1~2ms这个量级具体和编译优化等级、数据放在哪个内存段有关对50Hz工频谐波分析或者几百赫兹的机械振动分析来说这个速度完全够用。1.2 28335的硬件底子对FFT的加成很多人以为FFT是“纯数学问题”但在定点DSP上跑FFT硬件资源直接决定算法能不能落地。单周期乘累加MAC指令FFT的蝶形运算核心就是复乘加C28x有配套的流水线指令比普通MCU靠C编译器生成的乘加代码快得多。片内大块SRAMFFT需要把整帧时域数据一次性喂进去1024点实数FFT如果用32位定点格式输入和中间结果要占几十KB。28335的SRAM足够放两帧甚至三帧数据做乒乓缓冲不用去等外部RAM。片内12位ADC配合EPWM硬件触发FFT最怕采样间隔抖动用软件while循环一路采过去脉冲抖动会造成频谱底噪抬升。28335的EPWM模块可以在特定时刻自动触发ADC采样间隔准到纳秒级这是做FFT非常关键的一个条件。自带DMA连续采样时可以让ADC结果直接通过DMA搬进内存CPU只在整帧采完后参与FFT计算。所以28335虽然定位是“控制芯片”它做FFT需要的所有拼图其实都是齐的。1.3 一个反直觉的结论点数不是越多越好接触FFT的新手最容易一上来就选4096点、8192点觉得分辨率高、看得细。但工程上点数、采样率、实时性、内存四者是互相制约的。我在28335上的经验是电力谐波分析用1024点已经能覆盖51次以内的谐波在5.12kHz采样率下机械振动分析用512点配高采样率看细节效果反而更好。后面第4章会具体算这笔账。2. 先搞清楚FFT在28335上是怎么“变快”的2.1 DFT为什么慢FFT快在哪离散傅里叶变换DFT的定义式每个做信号处理的都见过X[k] Σ x[n] * e^(-j2πkn/N)直接按这个式子算N个输入点要算N个输出点每个输出点要做N次复数乘加总计算量是N²量级。1024点直接算就是一百多万次复数乘加28335虽然主频不低但这么干也扛不住实时性更是谈不上。FFT的核心思想就是把N点DFT按奇偶拆分一层层二分下去。以最常用的基2时间抽取法为例1024点可以先拆成两个512点512再拆成两个256点一直拆到2点蝶形总的复数乘法次数从N²降到(N/2)log₂N。1024点大约变成5120次复数乘法差了差不多两百倍。关键就在旋转因子W e^(-j2π/N)的对称性和周期性上。比如W的(N/2)次方等于-1那有些乘法就能变成加减法这就是蝶形运算里“旋转因子合并”的来源。2.2 实数输入的FFT还能再省一半现实中ADC采回来的信号都是实数序列但标准FFT输入输出都是复数。实序列傅里叶变换有个天然性质频谱关于N/2共轭对称也就说前一半谱线包含了全部信息。这么做有两个层面的优化空间第一层只算N/2个频点计算量直接砍半。第二层更狠把前后两段实数数据拼成一个复数序列分别当作实部和虚部算一次复数FFT再拆开还原出两段实数序列的频谱。TI针对C28x的DSPLIB里的实数FFTRFFT类函数基本都用了这个打包技巧所以同样的点数实数FFT比复数FFT快接近一倍。这也是为什么在28335上尽量不要把ADC数据填成复数FFT的实部、虚部填0那样等于浪费了一半计算资源。2.3 为什么点数必须是2的幂基2 FFT要求N 2^M。1024是2的10次方这也是工程上最常见的点数。28335的DSPLIB直接支持128、256、512、1024这些固定点数有的还支持非基2数的混合基FFT但从工程稳妥性看无脑选2的幂是最省心的。点数的对数M就是FFT分解的级数每一级N/2个蝶形整体流水很清晰。3. 工程搭建TI官方FSFT库和IQ格式的正确打开方式3.1 自己写FFT还是调官方库网上关于“手写FFT算法”的教程很多我自己刚入门时也照着C语言代码抄过一版。但放到28335项目里我的选择很明确直接用TI的C28x DSPLIB。原因有三个。第一性能差距明显。TI的库函数是针对C28x指令集手工汇编优化的编译器自动生成的C代码根本没法比同样是1024点RFFT我实测过时间差两三倍。第二稳定性有保证。蝶形运算的溢出处理、旋转因子表、位逆序排列这些细节自己写特别容易出隐蔽bug而库函数已经经过长期验证。第三版权和升级省心。用官方库不用担心维护问题。当然自写FFT用于理解原理或者点数很特殊的场合还是有价值的。工程上时间紧、要出东西直接调库。3.2 IQ格式28335没有FPU小数怎么算28335和后来的2837x不一样它没有硬件浮点单元。如果你直接写float a 0.5 * sin(2PIf*n/Fs)编译器会悄悄调用软件浮点库一条乘法变成几十条指令FFT这种成千上万次运算的算法会被拖慢到一个不可接受的程度。正确的做法是用定点格式TI生态里最常用的是IQ格式。IQ格式的本质就是约定一个数的小数点位置。以Q31为例一个32位整数最高位是符号位剩余31位表示小数1.0对应0x7FFFFFFF-1.0对应0x80000000。两个Q31数相乘结果要右移31位把小数点“挪回来”否则直接溢出。但在调用TI DSPLIB的FFT函数时有个细节要特别留意库内部对输入数据的格式有约定。我常用的做法是先把ADC的12位原始值减掉中点2048变成有符号数再左移20位映射到Q31的-1.0到1.0范围。代码长这样// 假设adcBuf[]是12位ADC采样原始值范围0~4095 // 先移除直流偏置再映射到Q31格式 for (i 0; i FFT_SIZE; i) { long tmp (long)adcBuf[i] - 2048; // -2048 ~ 2047 fftIn[i] tmp 20; // Q31格式约-1.0 ~ 1.0 }这里左移20位是因为12位ADC满幅2048对应Q31的1.0而1.0在Q31里是2^312^31 / 2^11 2^20。这样映射可以保证信号幅度不会超过FFT的输入范围又尽可能用满32位动态范围。3.3 TI DSPLIB核心调用流程DSPLIB里的RFFT类函数大体分三步正向FFT、拆包unpack、取幅值mag。以TI官方示例程序的写法为模板大致是#include dsp28x_fft.h // 具体头文件名以你的DSPLIB版本为准 #define FFT_SIZE 1024 RFFT32_1024_OBJ fftObj; long fftIn[FFT_SIZE]; // 输入时域数据IQ格式 long fftOut[FFT_SIZE]; // 输出频谱数据 long fftMag[FFT_SIZE/2]; // 幅度谱 // 初始化旋转因子表 RFFT32_1024_init(fftObj, fftIn, fftOut, fftMag); // 在采集完一帧数据、填好fftIn[]后调用 RFFT32_1024_calc(fftObj, fftIn); RFFT32_1024_unpack(fftObj); RFFT32_1024_mag(fftObj);需要注意不同版本DSPLIB的函数名可能略有差异有的叫RFFT32_1024、有的版本直接叫rfft32_1024_calc但只要找到你安装版本里的“RFFT 点数 calc”这类接口套路是一致的。旋转因子表一般作为系数数组放在Flash里init时把指针传给FFT对象。我建议先把TI自带例程原样编译过一遍确认库函数和CCS版本匹配再往自己工程里搬。4. ADC采样链路设计频谱能不能看一半取决于采样窗口4.1 采样率、点数、频率分辨率的三角关系FFT输出的频率间隔也就是“一根谱线的宽度”由两个量决定Δf Fs / N其中Fs是采样率N是FFT点数。这个公式一旦定下来整个分析带宽和分辨率就框死了。举个例子我要分析50Hz工频信号的谐波想看到20次谐波1000Hz按奈奎斯特定律采样率至少得2000Hz以上留点余量取5120Hz。Fs 5120HzN 1024时Δf 5Hz。50Hz正好落在第10根谱线1024点中k10对应50Hz整数周期采样频谱泄漏最小。采样时间窗口 N / Fs 1024 / 5120 0.2秒正好是10个完整的50Hz周期。这是最理想的情况。如果把采样率提到20kHz1024点只能看200Hz以内的频率间隔意味着两个只差10Hz的信号就分不开了。所以先定“我到底要分辨多少Hz”再倒推采样率这个顺序不能反。常见参数组合我整理了一个表分析场景采样率FsFFT点数N频率分辨率Δf分析频率范围工频谐波50Hz基波5120 Hz10245 Hz0~2560 Hz电机振动分析25600 Hz204812.5 Hz0~12.8 kHz开关电源纹波100000 Hz102497.7 Hz0~50 kHz低速机械振动1000 Hz10240.98 Hz0~500 Hz这个表里每组参数都是先确定带宽和分辨率再反推出的Fs实际项目里照着这个思路微调就行。4.2 EPWM触发ADC别用while循环苦等很多初学者习惯这么写采样代码while(1) { adcBuf[i] AdcResult.ADCRESULT0; i; if(i FFT_SIZE) break; DELAY_US(10); // 靠延时“粗调”采样率 }这种写法最大的问题是采样间隔不准。for循环里ADC读取、变量自增、延时函数本身的执行时间都在变哪怕每个周期只差几十纳秒积累到1024个点等效时基抖动会让频谱底噪飙升谐波附近会出现一堆不明不白的旁瓣。28335的正确做法是用EPWM模块产生周期性的SOC触发信号每到PWM周期匹配点自动启动一次ADC转换转换完成再触发ADC中断或者DMA搬运。整个采样过程的节奏完全由硬件控制。EPWM1配置成定时触发模式的寄存器设置大致如下// 假设EPWM1的时钟TBCLK SYSCLKOUT / 2 75MHz // 若想得到5120Hz采样率则PWM频率5120Hz // 选择up-down计数模式时TBPRD值 TBCLK / (2 * 采样率) EPwm1Regs.TBCTL.bit.CTRMODE 2; // up-down模式 EPwm1Regs.TBPRD 7324; // 75MHz / (2*5120) ≈ 7324 EPwm1Regs.ETSEL.bit.SOCAEN 1; // 使能ADC SOC触发 EPwm1Regs.ETSEL.bit.SOCASEL 2; // 周期匹配时触发 EPwm1Regs.ETPS.bit.SOCAPRD 1; // 每个周期触发一次 EPwm1Regs.TBCTL.bit.HSPCLKDIV 0; // 高速时钟不分频 EPwm1Regs.TBCTL.bit.CLKDIV 0; // 系统时钟不分频这里具体分频系数和TBPRD值要根据你的系统时钟仔细算关键是把“每个PWM周期触发一次ADC”这条链路打通。ADC中断里只做一件事把结果存进环形缓冲区攒够N点后置标志位主循环看到标志位再做FFT避免把FFT硬塞进中断里导致中断服务时间过长。4.3 采样窗口的“短时平稳”原则FFT处理的是有限长数据它默认这段时间窗口内的信号是平稳的也就是说频率和幅值不能突变太多。如果你让28335边做斩波控制边采电流而电流本身每个PWM周期都在大幅波动FFT结果会特别难看。解决思路是把采样触发点固定到PWM周期的某个稳定位置比如在PWM载波谷底开始连续采一小段再去分析而不是在开关纹波里乱抓。5. 从时域波形到频谱一个工频谐波分析完整例子5.1 测试输入怎么来先说明一下我这里分析的信号是用函数发生器产生的50Hz正弦波幅值约0.5V叠加在1.65V直流偏置上直接送到ADCINA0引脚。这样做的目的是先验证FFT链路本身是准的再去接真实的传感器。板子上电后先用示波器看ADC引脚波形确认信号没有削顶、没有明显噪声再开始采样。5.2 整帧采集与FFT计算主循环里等待采样完成标志然后执行FFT// 采样完成标志由ADC中断置位 if (adcDoneFlag 1) { adcDoneFlag 0; // 1. ADC原始值转IQ格式 for (i 0; i FFT_SIZE; i) { long tmp (long)adcBuf[i] - 2048; fftIn[i] tmp 20; } // 2. 执行1024点实数FFT RFFT32_1024_calc(fftObj, fftIn); RFFT32_1024_unpack(fftObj); RFFT32_1024_mag(fftObj); // 3. 遍历有效谱线0 ~ N/2-1换算频率和幅值 for (k 0; k FFT_SIZE/2; k) { float freq (float)k * SAMPLING_RATE / FFT_SIZE; long mag fftMag[k]; // 幅值换算见下文 } }DSPLIB的RFFT32_mag函数计算的是复频谱的模值但它是定点整数形式还要再除以一个缩放因子才能得到真实幅值。对TI库里的32位RFFT实现一般会在函数头文件的注释里给出mag输出和理论幅值的关系。在没有加窗、信号刚好落在整根谱线上的情况下频谱峰值的理论关系是峰值幅值 fftMag[k] * 缩放系数 / N这个缩放系数和库内部右移位数有关不同版本不同。我自己的做法是先注入一个已知幅值的正弦波标定出一个“工程缩放因子”。比如我注入1V峰值相对ADC满量程折算的50Hz正弦落点正好在第10根谱线实测fftMag[10]输出某个值K那么之后所有谱线的幅值都除以K再乘以1V得到以伏为单位的真实幅值。这个方法比纠结库内部的移位算法省心得多。5.3 频域结果长什么样谱线k对应的频率是 k * 5120 / 1024 5k Hz。所以在k10处看到一个大峰就是50Hz基波k20处如果有峰对应100Hz也就是二倍频分量。用这个方式可以很直观地检查整流桥、逆变器输出是否存在整倍数谐波。直流分量落在k0这根谱线上反映的是信号里的直流偏置。前面映射IQ格式时已经减掉了2048所以正常输入波形时k0附近的能量应该很小否则说明ADC输入信号本身就带偏置没清干净。5.4 提取相位信息FFT输出的复数谱里不仅有幅值还有相位。计算方式是对第k根谱线取atan2(虚部, 实部)// 假设unpack后的频谱数据是实部偶下标、虚部奇下标 long realPart fftOut[2*k]; long imagPart fftOut[2*k 1]; float phase atan2f((float)imagPart, (float)realPart);这给出的是信号在采样序列起点处的相位。如果信号是余弦函数x(t)Acos(2πftφ)那么对应谱线的初相读出来基本就是φ。如果信号源给的是正弦函数直接读出来的值会差90°因为sin和cos之间本来就差一个90°相移。这个细节在下一章展开讲。6. 相位修正与几个特别容易翻车的坑6.1 用FFT读相位为什么总是“差一点”我最初做三相电流相位差分析时连续采了三相电流分别做FFT发现计算出的相位差和实测值对不上误差不是固定的几十毫度而是和信号的频率、幅值都有关系。后来排查下来有两个原因第一模拟链路有固定的群延迟。信号从传感器出来经过放大滤波电路再经过ADC采样保持每一级都会引入相移。这个相移在FFT读相位时会被叠加进去。解决办法很土但很有效注入一路已知相位关系的标准信号标定出从模拟输入到FFT输出之间的固定相移量然后软件补偿。比如输入一个严格的cos信号读出来初相是−3.2°就把这个值记作系统固有相移后续都减掉。第二FFT读cos初相是直接读读sin初相要加90°。公式要从定义式推导不在这里展开但结论很实用。很多人都栽在这一条上对着sin信号愣是不知道相位为什么要偏移。第三加窗会影响相位。用Hanning窗等非矩形窗时频谱主瓣的相位响应会变平缓靠近谱峰的两个bin的相位关系可以用来做插值但直接用窗后的相位去代表信号初相误差会增大。所以如果相位精度要求高要么保证整周期采样、分析时不用窗要么用专门的三参数正弦拟合算法而不是靠FFT相位输出。6.2 坑一非整周期采样导致的频谱泄漏这是FFT应用里最大的坑没有之一。FFT默认把这一段有限长信号“周期延拓”成一个无限长周期信号来处理。如果采样窗口长度不是信号周期的整数倍延拓后波形首尾不衔接会在边界上产生阶跃频谱上就表现为能量从真实频率处散落到所有相邻谱线上这就是频谱泄漏。50Hz信号如果采样率取6000Hz1024点窗口对应0.1707秒是8.533个50Hz周期频谱就不是干净的谱线基波旁边会出现一堆旁瓣甚至把附近的微弱分量淹没掉。应对策略按优先级排列第一优先设计Fs和N保证窗口是目标信号周期的整数倍。就是我上一章那个Fs5120、N1024的例子窗口正好10个50Hz周期。第二优先用合适的窗函数压制旁瓣。工程上最常用Hanning窗主瓣宽一点旁瓣衰减很快。加窗后幅度恢复系数大约是2.0也就是频谱峰值要乘以2才接近真实幅值如果关心总能量能量恢复系数是sqrt(8/3)≈1.633。第三优先对于频率不是固定整倍数的情况用双谱线插值或者FFT细化算法把峰值频率和幅值估计得准一点。6.3 坑二ADC配置的隐藏坑28335的ADC模块和后来2837x系列不一样配置上很绕。两个最常见的坑一个是级联模式和同步模式的选择。如果只采一个通道用级联模式SEQ_CASC1就行采样结果顺序往ADCRESULT0到15里填。但如果想同时采两路信号做相位比较千万别以为把两个通道配置上就自动同步采了级联模式下两个通道是顺序轮询转换不是同时采样。要真正同步得用双排序器模式用两个转换序列分别触发或者后续对结果做时域延迟补偿。另一个是采样保持时间ACQ_PS配置太短。ADC引脚外部如果接了高阻信号源或者滤波电容较大采样电容还没充满就被切断转换出来的值会系统性偏小这种误差在时域上看不出来但FFT的幅值精度会差一截。我习惯把ACQ_PS配成15即采样窗口约16个ADC时钟牺牲一点点采样率换来稳定的转换结果。ADCCLK也要注意不超过芯片手册允许的最大值。6.4 坑三定点FFT的溢出和动态范围定点FFT最怕中间过程溢出。蝶形运算本质是复数乘加某一级如果输入幅度接近满幅加法就可能超过32位整数范围。TI DSPLIB内部会做scale处理但如果你直接把满幅信号左移20位塞进去仍然可能踩线。我在做动态范围测试时发现输入信号幅值在满量程的70%以下FFT结果很稳定接近满量程时某些谐波分量会出现“冒头”现象要么是饱和失真导致的真实谐波要么是中间溢出造成的虚假谱线。区分办法很简单把输入幅值减小3dB再跑一次FFT如果谱线幅值也跟着线性下降说明是真实信号如果下降不线性优先怀疑定点溢出。另外IQ格式转换时不要一味追求放大倍数。FFT库内部有自己一致的标定你输入信号缩放多少输出幅度就成比例变化真正的幅值可以在最后标定环节统一找回不需要在前级拼命放大。7. 实测性能、优化思路和后续玩法7.1 28335上1024点RFFT的实际表现我用CCS的profile tool测过几次在150MHz主频、编译器优化等级开到最高、数据放在片内RAM的条件下TI DSPLIB的1024点32位实数FFT单次耗时大致在1.5ms这个量级。如果把这段代码放到中断里跑加上ADC采集时间0.2秒FFT只占用其中极小一部分实时性完全没问题。但这有个前提不要用C语言默认的math库去做sqrt、atan2这些浮点函数在28335上很贵。取幅值阶段如果每个点都调用标准sqrtf1024个点的总耗时可能比FFT本身还长。我一般用查表法求平方根倒数或者干脆把幅度值以对数形式输出到上位机少做浮点运算。7.2 流水线设计连续分析不丢帧如果要做连续频谱显示最忌讳的做法是采完一帧 - 算FFT - 采下一帧。因为FFT算的时候ADC还在跑数据会被覆盖或者丢失。正确的做法是双缓冲ADC中断把数据写入buffer A采满A后切换ADC中断目标到buffer B主循环对buffer A做FFT采满B后再切回A主循环处理B。这样采集和计算互相不打断效率翻倍。28335的ADC结果寄存器配合DMA可以做到后台搬运CPU只在必要时刻拷贝一个指针。7.3 优化和后级扩展频谱算出来后后级处理还能玩出很多花活频率细分对峰附近的几根谱线做抛物线插值可以把频率分辨率从Δf提高到0.1Hz甚至更高精度。谐波畸变率THD把前N次谐波幅值平方和开根号再除以基波幅值就是总谐波畸变率。这个指标在电能质量分析里很常用。转频分析电机转速和振动频谱的基频有对应关系通过FFT找到振动基频再反推转速比装编码器省事。加窗补偿算法如果加了Hanning窗又不想损失幅值精度可以把幅度乘以2.0频率位置用相邻双峰加权平均来细化这就是双谱线插值的雏形。另外28335的SCI或者SPI可以把FFT结果发到上位机做成实时频谱曲线。帧率不需要太高每秒5~10帧就足够观察趋势。上位机收到的数据是频点、幅值、相位三个数组用串口助手先存成CSV文件再用Python或者Matlab画图调试效率会高很多。最后分享一个我自己学这套东西时觉得最有效的方法先用函数发生器注入一个已知频率、已知幅值的干净正弦波把FFT链路的频点、幅值、相位全部标定准了再去接真实信号。真实信号一旦出现问题你至少能确定是FFT前面的采集链问题还是后面的数据解释问题不用两边一起猜。这个思路听起来简单但真遇到问题的时候能帮你省出好几个晚上的排查时间。