第一次接触STM32F407做信号处理时,我完全低估了系统架构的重要性。直到亲眼看到FFT结果出现毛刺,才意识到实时信号处理是个系统工程。这个项目的核心在于让ADC采样、DMA传输、FFT运算和图形显示像精密齿轮一样咬合运转。
整个系统的工作流程是这样的:TIM3定时器像节拍器一样触发ADC采样,DMA像传送带把数据运到内存,DSP库的FFT函数变身信号翻译官,最后Emwin把频域图谱画在LCD上。关键是要保证数据流不断流,我在ucos里给DSP任务最高优先级,就像给急诊科医生开绿色通道。
硬件上需要特别注意时钟树配置。STM32F407的168MHz主频看着很充裕,但FFT运算会吃掉大量时钟周期。我的经验是把ADC时钟配在21MHz(APB2四分频),TIM3挂在APB1上(84MHz),这样既能保证采样精度,又给FFT留足算力。
很多教程教的是软件触发ADC,但在实时系统里这就是个灾难。我用TIM3触发ADC时踩过坑:当采样率设为10kHz时,用示波器抓信号发现实际只有9.8kHz。后来发现是没关闭TIM3的ARR预装载缓冲,导致定时器更新有延迟。
正确的配置应该是这样的:
c复制TIM_TimeBaseStructure.TIM_Period = 8400-1; //84MHz/10000Hz
TIM_TimeBaseStructure.TIM_Prescaler = 0; //不分频
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_ARRPreloadConfig(TIM3, DISABLE); //关键配置!
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);
直接搬运ADC数据到FFT输入数组会有个致命问题——FFT计算期间新数据会覆盖旧数据。我采用的解决方案是双缓冲机制:DMA循环填充两个缓冲区,当Buffer1满时触发中断,在中断里切换FFT计算Buffer2,同时DMA继续填充Buffer1。
具体实现时要注意内存对齐:
c复制__attribute__((at(0x20001000), aligned(8))) float adc_buf1[256];
__attribute__((at(0x20002000), aligned(8))) float adc_buf2[256];
DMA_InitStructure.DMA_Memory0BaseAddr = (u32)adc_buf1;
DMA_InitStructure.DMA_BufferSize = 256;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
最初我直接对原始数据做FFT,频谱泄露严重得像喷泉。后来发现必须加窗函数,但不同场景要选不同的窗:
在DSP库中加汉宁窗的代码:
c复制float hanning_window[256];
arm_hanning_f32(hanning_window, 256);
arm_mult_f32(adc_buffer, hanning_window, fft_input, 256);
FFT结果数组下标和实际频率的换算让很多人头疼。其实公式很简单:
code复制实际频率 = (下标×采样率)/FFT点数
但要注意三点:
我在工程里封装了个标定函数:
c复制void freq_calibration(float *fft_out, float fs, int N, float *freq_axis) {
for(int i=0; i<N/2; i++){
freq_axis[i] = i*fs/N;
}
}
直接刷波形会导致屏幕闪烁,GUI_MEMDEV内存设备是救星。但要注意:
我的波形刷新代码结构:
c复制static GUI_MEMDEV_Handle hMem;
hMem = GUI_MEMDEV_Create(0,50,320,180);
GUI_MEMDEV_Select(hMem);
// 在这里绘制网格和坐标轴
GUI_MEMDEV_WriteAt(hMem, 0, 50);
当信号幅值变化剧烈时,固定量程显示会丢失细节。我实现了自动量程算法:
核心算法:
c复制float adaptive_scale(float *data, int len) {
float max_val;
arm_max_f32(data, len, &max_val, NULL);
return (max_val == 0) ? 1.0 : (max_val * 1.2f);
}
在ucos中我把任务分成四个等级:
关键是要处理好任务同步。我用信号量控制数据流:
任务创建代码要注意栈大小:
c复制#define DSP_TASK_STK_SIZE 512 //FFT需要较大栈空间
#define GUI_TASK_STK_SIZE 1024 //Emwin很吃内存
OSTaskCreate(dsp_task, NULL,
(OS_STK*)&DSP_TASK_STK[DSP_TASK_STK_SIZE-1], 6);
PCB布局不当会引入噪声,我的血泪教训:
推荐使用STM32F407的专用VDDA引脚,并注意:
用信号发生器输入1kHz正弦波,测试系统极限:
如果发现性能瓶颈,可以尝试:
关键的编译器优化选项:
code复制-mfpu=fpv4-sp-d16 -mfloat-abi=hard -O3 -ffast-math
这个项目最让我自豪的是,最终成品能稳定显示到30kHz的频谱,成本不到商用频谱仪的百分之一。虽然FFT幅值精度只有5%,但对于定性分析已经完全够用。