尧图精选

STM32信号采集与FFT处理:电赛H题代码的DMA双缓冲与实时调试要点

🕒 发布时间:2026/9/15 13:46:57 📁 来源:尧图网络
简介针对2024年电赛H题中的信号处理任务这份资源提供了基于STM32微控制器的C语言工程代码适合参加电子设计竞赛的大学生以及嵌入式开发初学者参考。资源包约114MB内容以源代码为核心其中separate_f1文件对应数据或功能模块的第一部分便于读者按模块逐步理解滤波、频谱分析等处理流程的实现思路。已有91人加入学习可用作赛前方案搭建、代码调试和模块验证的辅助材料。通过完整的工程框架与关键代码读者可以快速把握STM32外设配置、信号采集与处理算法在竞赛场景中的落地方式减少从零起步的摸索时间也能结合描述中的版权提示合理使用资源。1. 2024年电赛H题代码真正要过的坎2024年电赛H题表面考信号处理实际拼的是“在STM32上把实时性做出来”。每年这道题都会刷掉一批“上位机做得很好看、真机一联调就崩”的队伍崩的地方往往不在算法而在DMA配置、中断优先级和缓冲切换时机。这份资源里以separate_f1为代表的代码正好是一套完整的STM32信号采集、分离与串口输出框架C语言模块怎么拆、FFT怎么调参、debug数据怎么实时看都能在里面找到对应实现。对准备电赛的学生以及想把MCU信号处理链路一次性跑通的人来说它的价值在于“可抄的工程骨架”而不是某一行的具体函数。不过网上流传的版本经常少头文件、缺DSP库配置直接编译大概率报错下面给出的补全和排错路径可以把这个坑提前填上。2. 从H题要求反推STM32资源与信号处理选型2.1 采样链路为什么必用定时器触发ADC加DMAH题这类实时信号处理场景最忌讳的就是在主循环里用HAL_ADC_GetValue()轮询采样。表面看代码简单了实际上采样间隔会被while循环里的其他逻辑抖动成不确定值后续FFT算出来的频率谱全是“虚胖”。正确做法是让定时器触发ADCADC转换完由DMA把结果搬运到内存全程不占用CPU。这样采样率和采样相位都是硬件级确定的算法拿到的是一段间隔均匀的数据。典型配置是TIM2输出TRGO事件触发ADC1注入或规则通道转换ADC1的DMA请求把转换结果写入缓冲区。STM32F103这类主频72MHz的片子12位ADC最高1Msps实际电赛信号源频率通常在几百Hz到几十kHz取100k至200kSPS采样率已经留足裕量。需要主要分辨率的时候可以用过采样把12位往16位方向抬代价是CPU占用率上去。// 定时器触发ADC采样72MHz/(TIM_Prescaler1)/(TIM_Period1)采样率 // 目标100kSPSPrescaler72-1Period10-172MHz/72/10100kHz TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_InitStructure.TIM_Prescaler 71; TIM_InitStructure.TIM_Period 9; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); TIM_Cmd(TIM2, ENABLE);上面这段把定时器优先级和ADC触发源做了绑定。TIM_Prescaler和TIM_Period共同决定采样频率修改时优先保持Prescaler固定只动Period这样采样率整除法误差更小。ADC端要设置ADC_ExternalTrigConv_T2_TRGO作为外部触发源并开启ADC_DMACmd否则定时器只管自己跑DMA拿不到数据。常见误用是忘了配置ADC_ExternalTrigConvCmd为Enable导致外部触发不生效ADC仍然处于软件触发模式。2.2 数据处理路径FFT和滤波怎么选拿到采样数据之后H题对信号的处理通常有两条路。如果目标是识别频率、幅度、相位这类全局特征FFT是首选如果目标是实时分离出某个频带并输出去控制外部设备FIR/IIR滤波更合适。从这份代码的separate_f1命名逻辑来看它对应的是“把第一路信号分量从混合信号里拆出来”本质是一个频域特征提取加分量重建的过程。STM32F103没有硬件FPU做单精度FFT会有点吃力但能跑。1024点arm_cfft_f32大约需要1ms左右配合DMA双缓冲正好做到“一段在处理、另一段在采集”。F407或G431这类带FPU和DSP指令的芯片体验就好很多2048点FFT也可以在毫秒级完成。选型上有一个容易被忽略的参数FFT频率分辨率等于采样率除以FFT点数100kSPS配1024点是97.7Hz也就是说两个频率相差不到100Hz的信号在频谱上是叠在一起的分不出来。想要分辨率高要么降低采样率要么加长FFT点数这两者又跟实时性互相制约。采样率FFT点数频率分辨率单帧时间适用场景20kSPS102419.5Hz51.2ms低频信号识别50kSPS102448.8Hz20.5ms通用音频范围100kSPS204848.8Hz20.5ms分辨率和带宽折中200kSPS409648.8Hz20.5ms高带宽多分量需要提醒的是FFT点数和采样率一起决定了一帧数据的延时这直接影响“能不能跟手”。电赛现场调试时用串口波形看一帧数据的刷新率一般10到20帧每秒是能接受的底线。低于5帧基本没法调参数。3. separate_f1模块的C语言实现与代码组织3.1 工程文件怎么拆别把代码全部堆进main.c这套代码里separate_f1应该是工程里的一个功能模块文件可能是separate_f1.c也可能是一个处理子程序。这类工程常见的组织方式是main.c只做初始化和主循环adc_driver.c负责采样signal_process.c放FFT和滤波debug_uart.c做串口输出。拆分的目的是让每个模块都能单独测试采样模块坏了不会拖累FFT模块的排查。主循环结构建议做成状态机思路不要在循环里阻塞等待数据while (1) { if (process_flag 1) // DMA半传输/全传输中断里置位 { process_flag 0; separate_f1_process(adc_buffer); // 处理当前半帧数据 send_result_uart(freq, amp); // 串口上报结果 } }这里的process_flag是中断服务函数里置位的标志位。主循环只做“有没有新数据”的判断避免轮询DMA状态导致逻辑混乱。要注意volatile关键字不能丢volatile uint8_t process_flag否则开了编译器优化后主循环可能永远看不到标志位变化。这个坑在调试时表现为“代码跑飞了”或者“有时候进不了处理”实际上是标志位被编译器优化掉了。3.2 DMA双缓冲把采集和处理时间重叠起来separate_f1模块能稳定工作的前提是采样数据不丢帧。单缓冲模式在“处理上一帧数据”期间ADC新数据无法写入缓冲区要么覆盖旧数据要么直接丢失。双缓冲的做法是准备两个长度相同的数组DMA循环写入写入前半段时触发半传输中断写入后半段时触发全传输中断两个中断轮流处理对应半段。#define N 1024 volatile uint16_t adc_buf[2][N]; // 双缓冲 volatile uint8_t buf_index 0; // 当前正在处理的缓冲索引 void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT1)) // 半传输前半段采集完成 { DMA_ClearITPendingBit(DMA1_IT_HT1); buf_index 0; process_flag 1; } if (DMA_GetITStatus(DMA1_IT_TC1)) // 全传输后半段采集完成 { DMA_ClearITPendingBit(DMA1_IT_TC1); buf_index 1; process_flag 1; } }DMA配置里要特别注意两点一是DMA_Mode必须设为DMA_Mode_Circular循环模式二是DMA_MemoryBaseAddr指向的是二维数组的首地址。前半段和后半段的分界就是adc_buf N和adc_buf 2*N。程序里拿到buf_index后实际数据起始地址是adc_buf[buf_index]处理完成后主循环清零process_flag等待下一次中断置位。3.3 FFT频谱分析与频率提取代码拿到一段完整的采样数据后接下来就是FFT和峰值查找。使用STM32官方DSP库是最省事的路径它会利用Cortex-M3/M4的DSP指令加速比自己手写基2蝶形运算快好几倍。核心代码放在separate_f1_process函数里void separate_f1_process(uint16_t *raw_data) { static float fft_input[2 * N]; static float fft_mag[N / 2]; uint16_t peak_bin 0; float peak_val 0.0f; for (int i 0; i N; i) { fft_input[2 * i] (float)raw_data[i] / 4096.0f * 3.3f; // 归一化到电压 fft_input[2 * i 1] 0.0f; // 虚部清零 } arm_cfft_f32(arm_cfft_sR_f32_len1024, fft_input, 0, 1); // 正变换 arm_cmplx_mag_f32(fft_input, fft_mag, N); // 求幅值 for (int i 2; i N / 2; i) // 跳过直流分量和近零频 { if (fft_mag[i] peak_val) { peak_val fft_mag[i]; peak_bin i; } } freq (float)peak_bin * SAMPLING_RATE / N; amp peak_val * 2.0f / N; }这段代码的fft_input是实数信号的FFT输入数组按实部虚部交替排列。arm_cfft_f32第三个参数0表示正变换最后一个参数1表示按位反转模式。arm_cmplx_mag_f32算出的是复数模值因为输入只有实部信号所以频谱关于中点对称只需要查前一半找峰值。频率计算公式里peak_bin对应FFT点数中的第几个频点乘以采样率再除以点数就是实际频率。比如100kSPS配1024点bin为50就代表约4.88kHz。幅度换算用的是峰值×2/N因为FFT结果是对称重复的要还原单边幅值必须乘2再除以点数。4. 串口调试、参数整定与常见坑的排错方法4.1 用串口把频谱和频率值实时拉出来看调参时最怕盲猜。建议在串口上同时输出两类信息一类是结构化的频率和幅度数值用于自动分析另一类是精简的波形数据用于在PC端画频谱图。发完整频谱会增加传输负担实际调试时只发送峰值附近的若干个点就够用。发送频率值本身也有讲究用ASCII码明文比二进制更直观但耗时更长115200波特率下一帧20个字符大约要1.7ms对50ms一次的处理周期影响不大。void send_result_uart(float freq, float amp, uint16_t peak_bin) { char buf[64]; int len sprintf(buf, F:%.2f A:%.3f BIN:%d\r\n, freq, amp, peak_bin); HAL_UART_Transmit(huart1, (uint8_t *)buf, len, 10); }如果发现发送期间处理周期被拉长可以把HAL_UART_Transmit换成DMA发送主循环不等待发送完成。另一种更省事的方式是用串口中断发送但要注意发送缓冲区的并发访问最简单的是在发送前关中断发送完再开。实际调参时peak_bin这个值比频率本身更有用——它直接告诉你频点在哪换算频率的误差来自采样率的整数分频精度。4.2 关键参数怎么调一点一点找到最佳工作点H题现场给的是函数信号发生器加混合信号频率不是固定的。参数调整通常按下面的顺序来先确定最高信号频率定下采样率再根据最小频率间隔定下FFT点数最后调整DMA缓冲区和FFT数组的长度。如果信号是两个相距很近的频率分量采样率设得越高频率分辨率反而越差这时要主动降采样率。调试现象可能原因调整方案频率显示频繁跳动采样数据不连续DMA缓冲切换丢帧检查双缓冲标志位时序确认中断没有被打断幅度偏大/偏小FFT归一化公式不对或者没加窗检查amp peak_val * 2.0f / N中的N是否与实际点数一致低频处频谱很高信号含直流偏置采样数据先减均值再进FFT或加高通滤波频率读数是实际两倍采样率配置是实际值的两倍检查定时器分频倍数确认TIM_Prescaler和TIM_Period计算两个靠得近的频率显示成一个FFT频率分辨率不够降低采样率或增加FFT点数二者择一4.3 DMA和中断谁先谁后优先级配错的经典踩坑很多队伍把FFT处理直接放在DMA中断里执行这是个大坑。FFT耗时毫秒级期间会阻塞其他低优先级中断更要命的是如果ADC配置为连续采样这期间DMA无法搬数据采样数据直接丢。正确做法是中断里只置标志位处理放主循环。中断优先级配置上DMA中断优先级建议设为最高串口发送中断次之定时器中断再次之。如果串口用了DMA发送发送完成中断优先级不能比DMA接收高否则可能在数据传输中抢占导致状态错乱。另一个典型问题是调试器在线仿真时程序正常脱机运行就出错。这通常跟初始化顺序有关先初始化DMA再使能ADC顺序反了会导致ADC的第一个转换结果没被DMA搬走缓冲区中原地址一直是旧数据。初始化顺序应该固定为定时器 → ADC → DMA → 串口使能DMA要在ADC开始转换之前完成。5. 把这份代码改造成自己的赛题方案5.1 从分离频率到输出控制信号separate_f1模块如果实现了频率和幅度的提取你就已经拿到了控制闭环的误差信号。电赛H题的升级方向通常是识别出信号后用DAC或PWM输出一个与输入频率同步的方波/正弦波或者根据频率值调整电机转速。STM32的DAC只支持F3/F4系列F103没有DAC需要用定时器PWM加低通滤波模拟正弦波输出。PWM频率至少设成信号频率的10倍以上再用二阶RC低通滤波把载波滤掉截止频率放在信号最高频率的1.5倍左右。如果要实时跟随输入信号频率变化输出最简单的是查表法生成正弦表只把表指针按输入频率换算的步进累加。步进计算为table_step freq / DAC_UPDATE_RATE * TABLE_SIZE累加器溢出时回绕。这个方案的优点是计算量极小100ms级别的频率跳变都能跟住。5.2 在比赛板子上验证代码是否真正可复现拿到这份代码后先不要急着跑按下面三步验证第一步把separate_f1依赖的ADC采样率和FFT点数核对清楚确认与自己的晶振配置匹配第二步用一个已知频率的正弦波信号源接到ADC输入对比串口输出的频率值误差超过1%就检查采样率配置第三步分别输入单频和双频混合信号观察peak_bin和幅度的变化是否稳定。如果双频信号无法分离先检查是否有混叠采样率必须大于最高频率的两倍。确认分离功能正常后再考虑把结果接到控制输出上否则前面的问题会传导到后续环节排错成本成倍上升。5.3 基于这份代码快速搭一套波德图扫描工具严格来说这不是赛题要求但调试滤波器或放大器时非常有用。让DAC输出一个频率可扫描的正弦信号ADC同步采样计算幅值比值就能得到系统的幅频特性曲线。扫描逻辑放在主循环里每处理完一帧数据把输出频率步进一次记录对应的幅度值。配合前面提到的串口发送协议把数据导入Python的matplotlib画图就能看到滤波器的实际响应和仿真结果的差异。这个技巧在电赛综合测评环节经常能救命因为很多评委喜欢现场改变频率看系统的跟随表现。separate_f1里已经包含了从ADC数据到频率座标的基本逻辑你只需要额外加一段DAC_SetValue之类的写值代码就能把整套系统从单纯的信号分析扩展成闭环调试工具。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →