前言在全国大学生电子设计竞赛中只要你不是纯做机械结构就绝对绕不开一个东西——ADC模数转换器。小到读取光敏电阻的分压大到电力电子题目中的交流电压电流采样、信号类题目中的频谱分析FFTADC 就是单片机感知真实世界的唯一窗口。但现实往往很骨感为什么我读到的 ADC 数据疯狂跳动为什么我跑的 FFT 算出来的频率根本不对为什么采样一快单片机就卡死本文将从硬件抗干扰、ADCDMA 黄金架构、数字滤波算法到 FFT 频谱分析带你彻底打通电赛信号处理的任督二脉TOC一、 硬件先决条件不要让噪声毁了你的源头“垃圾进垃圾出Garbage in, garbage out”这是信号处理界的铁律。如果硬件送给单片机的信号本身就是脏的软件算法再强也是徒劳。1. 致命基准VREF 的玄学很多新手默认 STM32 的 ADC 满量程是 3.3V。错ADC 的满量程取决于 VDDA 和 VREF 引脚的电压。如果你直接把开发板的 3.3V 连到 VDDA由于 LDO 芯片和电路板的压降实际可能只有 3.21V。你还在代码里乘 3.3/4096算出来的数据从起跑线就错了。对策如果题目要求高精度测量必须使用外部高精度基准电压源如 TL431、REF3033 等并用高精度万用表实测基准电压写进代码里2. 阻抗匹配ADC 也是要“吃”电流的STM32 内部 ADC 是基于**逐次逼近型SAR和开关电容阵列的。在采样瞬间ADC 会向外部电路抽取极小的一点电流。如果你的外部信号源输出阻抗很高比如串了个 100k 的电阻电压瞬间就会被拉低导致采样值偏小。对策在 ADC 引脚前加一级电压跟随器运放**进行阻抗变换这是电赛仪器仪表的标配并在 ADC 引脚并联一个 1nF ~ 10nF 的 C0G 材质小电容作为电荷补充。二、 软件架构革命彻底告别轮询拥抱 TIM ADC DMA很多同学采集 ADC 的代码是这样的while(1) { val HAL_ADC_GetValue(); delay_ms(1); }这种代码只能用来点灯如果用来做信号处理会面临两个灾难CPU 被彻底榨干没时间干别的。采样率Fs极度不抖动。由于各种中断的打断你的 delay_ms(1) 有时是 1ms有时是 1.2ms。采样频率不恒定后续做 FFT 绝对算不出正确的频率 黄金架构定时器触发 DMA 搬运零 CPU 消耗这是电赛高速采样的终极方案。让定时器TIM像节拍器一样每隔绝对精准的时间触发一次 ADC 转换转换完成后 DMA直接内存访问自动把数据搬到数组里。整个过程 CPU 0 参与直到数组装满才触发一次中断核心配置思路基于 STM32CubeMXTIM 配置设置定时器触发更新事件TRGO频率设为你想要的采样率例如 10kHz。ADC 配置外部触发源选择对应的 TIM TRGO开启 DMA 模式Circular 循环模式。DMA 配置方向 Peripheral to Memory开启 Half Word16位。实战代码直接调用codeC#define FFT_LENGTH 1024 uint16_t ADC_Buffer[FFT_LENGTH]; // DMA搬运的目的地 // 在 main.c 中启动 // 1. 启动定时器触发 HAL_TIM_Base_Start(htim2); // 2. 启动 ADC 的 DMA 模式目标数组长度 HAL_ADC_Start_DMA(hadc1, (uint32_t*)ADC_Buffer, FFT_LENGTH); // 3. 当装满 1024 个数据后会自动进入回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { // 在这里设置标志位通知主循环去处理这 1024 个点的数据 fft_ready_flag 1; } }三、 数字滤波算法消除毛刺的“三大法宝”如果只是测直流电压、温度等缓慢变化的物理量读出来的值一直乱跳怎么办千万别直接用单次采样值一定要上滤波算法1. 均值滤波最无脑但也最实用连采 20 个数去掉一个最大值去掉一个最小值剩下 18 个取平均。适合滤除偶尔出现的随机脉冲干扰。2. 软件一阶 RC 低通滤波最优雅不需要保存数组极度节省内存还能起到像电容一样的平滑效果。非常适合电池电压监测、传感器数据读取。C语言实现codeC// a 决定滤波系数范围 0~1。 // a 越小滤波效果越强但响应越慢滞后。 float First_Order_Filter(float new_val, float last_val, float a) { return (a * new_val) ((1.0f - a) * last_val); } // 使用示例 float current_voltage 0; current_voltage First_Order_Filter(Read_ADC_Vol(), current_voltage, 0.1f);3. 滑动窗口滤波兼顾平滑与实时性维护一个长度为 N 的队列每次来一个新数据挤掉最老的数据求当前 N 个数据的平均值。适合需要平滑且对实时性有一定要求的场景。四、 频谱分析大杀器FFT快速傅里叶变换避坑指南遇到题目要求“测量输入信号的频率分量”、“分析电压的谐波THD”直接上 FFT很多同学对 FFT 有深深的恐惧其实在 STM32 中使用 CMSIS-DSP 库调用 FFT 极其简单难的是参数怎么选1. 必须死记硬背的奈奎斯特定理Nyquist采样频率Fs必须大于信号最高频率的 2 倍工程上通常取 5~10 倍。如果你要分析最高 10kHz 的音频信号你的 ADC 采样率至少要设为 20kHz建议 50kHz。如果不满足就会出现“频率混叠”高频信号会变成低频幽灵出现在你的结果里2. 频率分辨率能测得多准公式频率分辨率 Fs / NFs 是采样率N 是 FFT 点数。比如采样率 10240Hz采集 1024 个点。分辨率 10240 / 1024 10 Hz。这意味着FFT 算出来的频率只能是 0Hz, 10Hz, 20Hz... 也就是 10 的整数倍。你永远测不出 15Hz提分秘籍如果题目要求精确到 1Hz要么降低采样率但不能违反奈奎斯特要么增加点数 N比如加到 4096 点但这吃单片机内存 RAM。3. STM32 DSP库 FFT 核心调用流程准备好带有 FPU硬件浮点运算的芯片如 STM32F4/F3/G4勾选 CMSIS-DSP。codeC#include arm_math.h #define FFT_PTS 1024 float fft_input[FFT_PTS * 2]; // FFT输入数组实部虚部所以长一倍 float fft_output[FFT_PTS]; // 存放计算后的幅值 // 1. 将 ADC 采集到的数据装填给 FFT // 注意偶数索引存实部(ADC数据)奇数索引存虚部(填0) for(int i 0; i FFT_PTS; i) { fft_input[i*2] (float)ADC_Buffer[i] * (3.3f / 4096.0f); // 实部转成实际电压 fft_input[i*2 1] 0; // 虚部填0 } // 2. 初始化 FFT 实例并执行 arm_cfft_radix4_instance_f32 scfft; arm_cfft_radix4_init_f32(scfft, FFT_PTS, 0, 1); arm_cfft_radix4_f32(scfft, fft_input); // 3. 计算各个频率点的幅值模 arm_cmplx_mag_f32(fft_input, fft_output, FFT_PTS); // 4. 寻找最大峰值基波频率 float max_value; uint32_t max_index; // 注意通常忽略索引0直流分量从索引1开始找 arm_max_f32(fft_output[1], FFT_PTS/2 - 1, max_value, max_index); // 5. 算出真实频率 float real_frequency (max_index 1) * (Fs / FFT_PTS);五、 电赛 ADC 玄学故障 QAQ1为什么我多路 ADC 采集通道 1 的电压总是影响通道 2原因STM32 的多个 ADC 通道共用一个内部采样电容。通道切换太快电容里的电荷还没放完就带到了下一个通道串扰。对策在 CubeMX 中将 ADC 的Sampling Time采样周期拉长例如从 3 Cycles 加到 239.5 Cycles让电容有充足的时间充放电或者在通道外部加运放跟随器。Q2FFT 算出来的频率总是差那么一点点不准原因这就是著名的“栅栏效应”和“频谱泄露”。对策在做 FFT 之前给原始数据乘上一个窗函数如 Hanning 汉宁窗 或 汉明窗可以让两端的突变平滑掉极大减少频谱泄露或者采用信号插值算法高阶选手的加分项。结语在电赛的四天三夜里能够精准、高速、稳定地采集现实世界的数据你的队伍就已经击败了全国 50% 的对手。ADC是感官DMA是神经滤波和FFT是大脑。熟练掌握这套“感知与处理”体系不管是做微弱信号检测仪还是做逆变器谐波分析你都能手到擒来预祝各位电赛人ADC数据稳如泰山FFT频谱一柱擎天轻松斩获国一觉得干货满满点赞 ⭐收藏写代码遇到“波形跳舞”时随时拿出来看你在使用 ADC 或者 FFT 时踩过什么神坑或者对某种滤波算法有独到见解欢迎在评论区留言交流我将在评论区随机解答大家的技术问题