拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32实战:差分ADC+2048点FFT实时频谱分析系统全解析

简介STM32搭配差分ADC实现2048点FFT频谱采集的完整工程面向嵌入式开发者、信号处理学习者和需要做频谱分析的项目人员。资源基于STM32F10x平台给出ADC采样配置、FFT蝶形运算及频谱图生成的纯C实现涵盖从时域采集到频域幅值、dB转换的完整链路可用于音频分析、振动监测、电力谐波检测等场景。包体共193个文件6.38MB以c、h源文件为主包含stm32f10x_adc.c、stm32f10x_tim.c等驱动以及o、d、crf等编译中间文件、uvprojx工程文件、hex/axf烧录文件另含keilkill.bat清理脚本便于直接在Keil工程中打开编译。已有2798人学习下载。资源提供可直接运行的工程同时体现了差分ADC抗共模噪声的采集思路、2048点FFT的算法组织方式和频谱数据后处理步骤对理解嵌入式数字信号处理有实际参考价值。 搞嵌入式信号处理绕不开FFT。最近我把一套基于STM32的信号采集频谱分析小系统调通了差分ADC做前端输入纯C实现2048点FFT串口把频谱数据发给上位机再用QCustomPlot画出实时频谱图。整套流程从硬件链路、采样参数到算法实现全部自己控制非常适合想深入理解“单片机FFT”完整闭环的朋友参考。先说结论STM32跑2048点FFT完全可行即使是F103级别的芯片也能做只是要处理好定点数或时间分配真正决定频谱质量的是ADC前端、采样率设计和窗函数选择算法本身反而是最机械的部分。这篇文章我会从整体设计、硬件接入、纯C实现、上位机显示到踩坑实录完整拆一遍。1. 项目到底在做什么配置目标与整体拆解1.1 一条信号链路看懂系统构成这个项目的本质是把一段连续模拟信号变成频谱图。我常用的完整链路是这样传感器或信号源输出模拟量 → 差分ADC采样量化 → DMA把数据搬进内存 → 纯C的FFT算法处理2048点数据 → 串口发送幅频结果 → 上位机QCustomPlot绘制实时频谱图。中间最容易被忽视的是“采样率”和“FFT点数”这两个参数。我这次定的采样率是10kHzFFT点数2048点对应的频率分辨率是fs/N 10000/2048 ≈ 4.88Hz单帧采样时长T N/fs 204.8ms。也就是说频谱图上每个频点间隔约4.88Hz能分辨的最小频率差就是这个值100Hz和104Hz能区分开但100Hz和102Hz就分不开了。如果要做更细的频率分辨要么降低采样率要么增加FFT点数两者选哪个后面细说。这套系统适合谁想用单片机做振动监测、音频分析、电力谐波检测、电机故障诊断的工程师和学生都适用。硬件成本很低核心就是一块STM32开发板加一点前端电路算法部分完全开源可改改完就能用到自己的项目里。1.2 为什么是“差分ADC”而不是普通单端ADC这是第一个需要在设计阶段就想清楚的问题。普通单端ADC只有一个输入端信号相对GND测量一旦现场有共模噪声比如电机驱动、开关电源带来的地弹噪声会直接叠加到信号上。差分ADC有正负两个输入端测量的是两端的差值共模噪声在两端同时出现时会被直接抵消抗干扰能力明显更强。在STM32家族里能直接支持差分输入的有G431、F303等型号内置可编程增益放大器PGA能把毫伏级传感器信号放大后再采样。如果是F103/F407这种没有差分模式的型号可以通过外部仪表放大器比如INA128把差分信号转换为单端信号再接进普通ADC。我的做法是用一个带差分输入的ADC通道方案做主线同时把INA128前端作为兼容方案两种路线后面会分别说明。选2048点不是随便定的它是2的11次方FFT蝶形运算的级数是log2(2048)11级。基2 FFT要求点数必须是2的幂如果用1024点频率分辨率会翻倍到9.76Hz对于100Hz附近的振动分析来说偏粗用4096点分辨率能到2.44Hz但内存占用翻倍计算时间也翻倍。2048点是分辨率、内存、计算速度三者比较折中的选择。2. 硬件输入链路差分接入与抗混叠滤波2.1 直接差分模式 vs 仪表放大器前端两个方案方案A使用STM32G431这类内置差分ADC的型号。配置时要点在于开启通道的DIFSEL位把通道设为差分模式然后选择正负输入引脚如INP0和INN1。G431内部还带PGA增益可选1/2/4/8/16/32/64倍小信号可以直接放大不用外部运放。HAL库配置大概是hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; hadc1.Init.DMAContinuousRequests DISABLE; hadc1.Init.Resolution ADC_RESOLUTION_12B; sConfig.Channel ADC_CHANNEL_0; sConfig.DifferentialMode ENABLE; // 关键开差分 sConfig.PgaGain ADC_PGAGAIN_4; // 差分PGA增益 sConfig.SamplingTime ADC_SAMPLETIME_247CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig);有的库函数里没有直接暴露DifferentialMode也可以直接操作寄存器ADC1-DIFSEL | (DIFSEL_DIFSEL_0 | DIFSEL_DIFSEL_1);作用是把通道0和1配对成差分模式。这种方式适合电桥传感器、生物电信号这类本身就差分输出的场景。方案B用F103/F407这类常见芯片外接INA128仪表放大器。INA128的增益由1、8引脚之间的电阻RG决定公式是G 1 50kΩ / RG。想要10倍增益RG取5.6kΩ标准电阻即可实际计算是1 50000/5600 ≈ 9.93倍。输出端是单端信号直接接ADC输入引脚。这种方式需要注意给INA128提供双电源或做电平抬升否则负半周信号会被截掉。对单电源系统我会在输出端加一个VCC/2的偏置电压把信号抬到ADC的中间量程。2.2 抗混叠滤波与采样率定标混叠是FFT最容易踩的坑。根据奈奎斯特采样定理采样率fs最多只能无混叠地分析到fs/2的频率。如果输入信号里有超过fs/2的高频分量它会“折叠”到低频区域在频谱图上显示成一个根本不存在的峰。所以输入到ADC之前必须加低通滤波器。我的采样率是10kHz理论最大分析频率5kHz抗混叠滤波器截止频率设在5kHz附近。最简单是一阶RC低通比如R1kΩ、C33nF截止频率fc 1/(2πRC) ≈ 4.82kHz。这个组合成本几乎为零做演示和一般工程验证足够。如果要求更陡的衰减可以改成二阶巴特沃斯或有源滤波器但要考虑运放带宽和相位延迟。采样率定标也很关键。我用定时器触发ADC采样定时器频率就是实际采样率比如TIM2的更新频率配置成10kHz。实际跑下来晶振会有微小偏差导致FFT计算的频率和真实频率有几Hz的偏移这在后面实测中确认过。对精度有要求的话可以用已知频率的标准信号去校准修正系数存下来。3. 纯C实现2048点FFT算法与代码拆解3.1 DMA乒乓缓冲边采集边计算FFT需要一帧完整的数据最省事的做法是ADC转换完N个点后触发中断在中断里开始做FFT。但这样采204.8ms的数据要等完才能算期间信号一直在产生白白浪费算力。我采用DMA“乒乓缓冲”机制在内存里开两个2048点的缓冲区A和BDMA半传输和全传输中断交替触发。前半段数据填满A时触发半传输中断CPU开始算A后半段填满B时触发全传输中断CPU开始算B。这样采集和计算完全重叠FFT做完了下一帧数据也采了一半能实现接近实时的频谱刷新。关键配置代码片段#define FFT_SIZE 2048 uint16_t adcBufA[FFT_SIZE]; uint16_t adcBufB[FFT_SIZE]; // DMA配置为循环模式数据长度4096即半满2048点 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adcBufA, FFT_SIZE * 2); // 在DMA半传输和全传输回调中分别处理 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { process_fft_frame(adcBufA, FFT_SIZE); // 处理前半 } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { process_fft_frame(adcBufB, FFT_SIZE); // 处理后半这里按DMA循环写 }这里有个细节DMA缓冲地址我直接用了adcBufA长度设为FFT_SIZE * 2借助DMA的半满中断机制把一块连续内存虚拟成了两个缓冲区。这样省一次memcpy对实时性帮助很大。3.2 窗函数、位反转与蝶形运算直接拿2048个原始采样点做FFT会产生频谱泄漏。因为采样窗口不是信号周期的整数倍相当于截断信号时引入了跳变导致本应是一根线的频谱变成一片“裙边”。解决办法是在FFT之前把时域数据乘以窗函数。我用汉宁窗Hanning公式是w[n] 0.5 × (1 - cos(2πn/(N-1)))。工程上最常用主瓣和旁瓣折中好通用性强。预处理代码float fftInRe[FFT_SIZE]; float fftInIm[FFT_SIZE]; for (uint16_t i 0; i FFT_SIZE; i) { float win 0.5f * (1.0f - cosf(2.0f * PI * i / (FFT_SIZE - 1))); fftInRe[i] ((float)adcBuf[i] - 2048.0f) * win; // ADC 12位减中值变双极性 fftInIm[i] 0.0f; }如果输入是12位ADC量化范围0~4095我会先减2048转成双极性信号再乘窗函数。对仪表放大器方案如果信号已经偏置到中间电平这一步不能省否则频谱上会多出一个很大的直流分量。FFT核心是基2时间抽取法分为位反转和蝶形运算两步。位反转的作用是把输入序列按二进制位倒序重排这一步做错整个频谱都是乱的。我的实现void bit_reverse(float *re, float *im, uint16_t n) { for (uint16_t i 1, j 0; i n; i) { uint16_t bit n 1; for (; j bit; bit 1) j ^ bit; j ^ bit; if (i j) { float tr re[i]; re[i] re[j]; re[j] tr; float ti im[i]; im[i] im[j]; im[j] ti; } } }蝶形运算是FFT的核心循环2048点FFT一共有11级每级有1024个蝶形运算。完整的浮点实现如下我这里偷懒用了每次蝶形旋转因子累乘的方式省去了查表计算的所有cos/sin调用void fft_calc(float *re, float *im, uint16_t n) { bit_reverse(re, im, n); for (uint16_t len 2; len n; len 1) { float ang -2.0f * PI / len; float wRe cosf(ang), wIm sinf(ang); for (uint16_t i 0; i n; i len) { float curRe 1.0f, curIm 0.0f; for (uint16_t k 0; k len / 2; k) { float uRe re[i k], uIm im[i k]; float vRe re[i k len/2] * curRe - im[i k len/2] * curIm; float vIm re[i k len/2] * curIm im[i k len/2] * curRe; re[i k] uRe vRe; im[i k] uIm vIm; re[i k len/2] uRe - vRe; im[i k len/2] uIm - vIm; float nextRe curRe * wRe - curIm * wIm; curIm curRe * wIm curIm * wRe; curRe nextRe; } } } }这里必须提醒len每增加一级旋转因子的初始角度都在变所以内层循环里curRe/curIm由wRe/wIm累乘更新。这个写法的缺点是累乘会引入微小误差但对2048点工程应用完全够用。追求极致精度就在外层for的len循环里直接查预计算的cos/sin表。3.3 频率映射与幅值校正FFT算完得到复数组第k个点的幅值是sqrt(re[k]² im[k]²)对应的实际频率是f k × fs / N。因为输入是实数信号频谱关于N/2对称我们只需要看0到N/2共1025个点。第0点是直流分量第k点的物理意义是k×4.88Hz处的正弦波幅值。加汉宁窗之后信号能量会被窗函数“摊薄”直接读出的峰值比真实幅值小一半左右。工程上最简单粗暴的做法是幅值乘2作为近似还原然后用法标定实际设备。严谨一点可以用窗函数的相干增益校准汉宁窗的幅度恢复系数≈2具体表现在单频信号加窗后峰值幅度变为原信号的约0.5倍所以乘2还原。我用1kHz标准正弦验证过还原后的幅值误差在5%以内对频谱分析足够了。4. 数据通路与上位机可视化4.1 串口发送协议设计频谱算出来是要给人看的不能只留在单片机里。我用UART2做数据回传波特率921600。协议自定义最简单可靠的格式帧头数据长度频谱数据校验字节。为了降低上位机解析难度我把1025个浮点幅值数据转成16位无符号整数整数部分直接是幅值的百分比或电压值分度。格式如下表字节段内容长度帧头0xAA 0x552字节点数FFT点数/2110252字节数据幅值数组uint162050字节校验累加和低字节1字节串口发送建议用DMA不要在main里死等发送完成否则会拖慢FFT循环。4.2 用QCustomPlot绘制实时频谱上位机我用Qt QCustomPlotQCustomPlot画曲线简单直接实时性也不错。每个串口帧到达后用graph-setData(xData, yData)更新数据再调用rescaleAxes()和replot()刷新画面。这里要特别注意replot()调用太频繁会造成界面卡顿可以加一个简单的帧数控制比如每5帧刷新一次界面刷新率大约1Hz左右人眼看起来已经很流畅了。QCustomPlot横轴刻度直接用频率值纵轴标成幅值如果传感器标定了物理单位比如加速度值mV/g就可以直接显示物理量。振动分析场景里我习惯把纵轴切换成dB刻度20 * log10(mag)这样能同时看到很小的谐波分量和主峰动态范围更清晰。4.3 性能预算2048点FFT要算多久这个可能是很多人最关心的数字。实测下来在STM32F407 168MHz带FPU上2048点单精度浮点纯C FFT从位反转到最后一轮蝶形结束耗时大约在4~6ms之间。比想象中快很多对吧加上窗函数预处理和幅值计算总共不超过10ms。而一次采样耗时是204.8msCPU占用率不到5%剩余时间完全可以干别的甚至可以跑多帧平均来降噪。如果是STM32F103 72MHz没有FPU单精度浮点运算全靠软件模拟同样2048点FFT实测大概在80~120ms接近采样周期的一半。这时有两个优化方向一是把数据转成Q15定点格式用整数蝶形运算替代浮点速度能提升5~10倍二是降低采样率或FFT点数比如1024点换取实时性。我做F103版本时直接改成Q15定点虽然写起来繁琐但性能提升非常明显。5. 实测结果与波形验证5.1 用信号发生器验证频谱线位置我用手头的DDS信号发生器输出1kHz、幅值1V的正弦波接入差分输入。频谱图上最明显的峰落在约第205个频点上205×4.88Hz ≈ 1001Hz和设定频率一致。峰旁边的底噪大概在-60dB以下说明前端和FFT处理的噪声底比较低。这验证了三件事采样率配置正确、FFT算法正确、频率映射公式正确。再测试一个容易出问题的场景输入1.5kHz正弦同时叠加一个2.7kHz的小幅正弦。只要两个频率间隔大于频率分辨率4.88Hz谱图上能清晰看到两个独立的峰。如果信号发生器精度不高最右侧峰的频点会在几个频点之间跳变这是信号源本身频率漂移引起的不是FFT的问题。5.2 泄漏现象与加窗对比为了演示窗函数的作用我特意做过对比输入一个不是频率分辨率整数倍的信号比如1234Hz正弦分别做不加窗和加汉宁窗的FFT。不加窗时1234Hz的谱线能量向两侧扩散最高峰旁边出现一串幅度不小的旁瓣主峰的峰值也偏低。加窗之后虽然主峰宽度变宽了一点但旁瓣被明显压下去幅值读数更稳定、更接近真实值。用这个对比想说明一个道理窗函数不是数学游戏。实际信号频率极少正好等于频率分辨率的整数倍不加窗直接算频谱峰值和幅值都是不可信的。汉宁窗能解决绝大多数通用频谱分析需求但如果要更窄的主瓣可以换平顶窗或凯塞窗不过旁瓣抑制和幅值精度两者要取舍具体选型根据测量场景来。6. 踩坑记录与常见问题排查6.1 报错、乱码与波形异常“error: no stm32 target found! if your product embeds debug authentication...”这是我调STM32时最烦的报错之一。多数情况不是代码问题而是调试器接线或目标板供电异常。先检查SWDIO/SWCLK/GND三条线是否牢靠再确认目标板供电正常最后在STM32 ST-LINK Utility里点Connect试一下能连上再回IDE调试。如果Connect都连不上大概率是板子处于低功耗模式或调试口被复用了。串口乱码也很好排查先看波特率是否一致再看GND是否与上位机共地如果这些都没问题检查单片机时钟配置。HSE起振失败导致倍频不对串口输出的所有字符就会变成烂码。G431这类带内部PLL的芯片还要确认VCO范围配置在数据手册规定的区间内超范围会导致主频不准。频谱图上出现大量无规律毛刺优先怀疑电源噪声。我给ADC供电线串了磁珠和10uF100nF去耦电容后噪声底明显下降。如果信号源本身就带开关噪声差分输入能扛掉一大部分但前提是前端地的处理要干净地环路越少越好。6.2 FFT结果对不上号的定位思路如果出现“明明输入1kHz谱峰却不在预期位置”的情况按顺序排查先算频率分辨率确认采样率配置是否匹配然后用单频信号验证接着看FFT输出是不是只取了前N/2个点最后确认位反转正确性。位反转写错会表现为整个频谱完全错乱没有规律可言。还有一个常见问题MCU主频较高时FFT结果偶发抖动尤其是在Cortex-M7内核上。这是因为CPU和DMA操作同一块数据时存在缓存一致性问题需要调用SCB_CleanDCache()或在DMA传输完成后做数据同步。STM32F4系列没有这个问题但H7系列一定要处理不然FFT输入数组里可能混入旧数据结果看起来就是“偶尔冒尖”。我个人调试这套系统时最终的稳定做法是先用固定频率正弦逐步验证每个环节再接入真实传感器信号。这样一旦频谱异常能立刻判断是前端采样问题还是算法问题。如果你第一次做类似项目也建议按这个顺序推进不要一上来就接实际传感器否则问题叠加在一起会非常难定位。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门