1. 项目背景与硬件选型
最近在做一个高频信号采集的项目,需要测量920kHz左右的波形频率。这个频率对STM32的ADC来说确实是个挑战,毕竟要稳定工作在2.4MHz的采样率下不是随便配置就能实现的。我选择了立创的天空星STM32F407开发板,这款板子的外设资源足够丰富,特别是ADC和定时器的性能完全能满足这个需求。
STM32F407的ADC最高支持2.4MHz采样率,但这个速度需要精确的时钟配置和DMA配合。如果只用软件触发采样,CPU光处理ADC数据就忙不过来了,更别说做实时频谱分析了。所以必须用定时器触发ADC采样,再通过DMA把数据直接搬运到内存,这样才能实现真正的无CPU干预高速采集。
在实际测试中,我发现时钟配置是关键中的关键。一定要用外部晶振,内部RC振荡器的精度和稳定性根本达不到要求。APB2总线要配置为72MHz,然后ADC预分频设置为2,这样ADC时钟就是36MHz,正好是STM32F4 ADC的最高允许时钟频率。
2. CubeMX基础配置详解
2.1 时钟树配置要点
打开CubeMX后,第一件事就是配置时钟树。在RCC设置里选择HSE(外部高速时钟),然后进入Clock Configuration界面。这里需要把PLL源设置为HSE,然后配置PLL参数使系统时钟达到144MHz。
APB2总线时钟要设置为72MHz,这是通过将系统时钟2分频实现的。然后ADC预分频选择2,这样ADC时钟就是36MHz。为什么非要36MHz?因为STM32F4的ADC最高工作频率就是36MHz,超过这个值采样精度就会下降。
计算采样率有个公式:采样率 = ADC时钟频率 / (采样周期 + 12.5)。如果设置采样周期为3个时钟周期,那么采样率就是36MHz / (3 + 12.5) ≈ 2.4MHz。这个配置正好能让我们达到最高采样速率。
2.2 定时器触发配置
我用的是TIM3作为ADC的触发源。TIM3挂在APB1总线上,APB1的时钟频率是72MHz。定时器的更新频率计算公式是:f = TimeClockFreq / ((Prescaler + 1) * (Period + 1))。
为了让触发频率接近2.4MHz,我设置预分频器为3,自动重载值为13。这样计算出来的触发频率是72MHz / ((3+1)(13+1)) = 72MHz / 56 ≈ 1.286MHz。等等,这个值好像不对?其实这里有个细节:当APB1预分频系数不为1时,定时器时钟会倍频。所以实际TIM3的时钟是72MHz2=144MHz,最终触发频率是144MHz / ((3+1)*(13+1)) = 144MHz / 56 ≈ 2.571MHz。
实际上我最后用的是1846153Hz的触发频率,这个值是通过精细调整分频系数和重载值得到的。关键是要让触发频率略高于目标采样率,因为ADC转换需要时间。
2.3 ADC与DMA配置
ADC配置有几个关键点:首先选择要使用的通道,然后开启DMA。工作模式选择"Normal"而不是"Circular",因为在2.4MHz这么高的采样率下,循环模式会导致DMA中断过于频繁,系统很容易卡死。
采样时间设置为3个周期,这样配合36MHz的ADC时钟,正好能达到2.4MHz采样率。DMA要设置为单次传输模式,传输宽度为半字(ADC是12位分辨率),内存地址自增。
最重要的一点:不要开启DMA中断!在高速采样时,DMA完成中断会非常频繁,CPU根本处理不过来。我们只需要在DMA传输完成时设置一个标志位,在主循环中处理数据即可。
3. 代码实现与DSP库集成
3.1 基础驱动代码
生成代码后,首先需要重定向printf函数到串口,方便调试。这里有个小技巧:使用GCC编译器时和MDK编译器的重定向方式略有不同。
#include "stdio.h"
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int _io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
接下来初始化ADC和定时器。注意要先进行ADC校准,这个步骤很多人会忽略,但在高速采样时校准对精度影响很大。
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_TIM_Base_Start(&htim3);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcConvertValue, adc_SIZE);
3.2 DSP库添加与配置
STM32F4自带的DSP库真的很强大,但添加的时候容易出问题。首先要在CubeMX的Software Packs里选择CMSIS DSP库,然后记得在工程设置里添加预定义宏ARM_MATH_CM4。
如果手动添加,需要把DSP库文件复制到项目目录,然后在工程中包含头文件路径。关键是要根据CPU类型选择正确的库文件,STM32F4是Cortex-M4内核,要用arm_cortexM4lf_math.lib(小端模式)。
FFT计算需要初始化复数数组,ADC采样得到的是实数序列,需要转换为复数格式(虚部设为0)。我定义了2048点的FFT,这个长度在精度和计算量之间取得了很好的平衡。
#define FFT_SIZE 2048
#define SAMPLING_FREQUENCY 1846153.84615f
float32_t inputSignal[FFT_SIZE * 2];
float32_t fftOutput[FFT_SIZE / 2];
3.3 FFT计算函数实现
FFT计算函数主要完成三个任务:执行FFT变换、计算幅度谱、寻找峰值频率。CMSIS-DSP库提供了高度优化的函数,直接调用就行。
void fftCalculate(void)
{
// 执行FFT计算
arm_cfft_f32(&arm_cfft_sR_f32_len2048, inputSignal, 0, 1);
// 计算幅度谱
arm_cmplx_mag_f32(inputSignal, fftOutput, FFT_SIZE / 2);
// 寻找峰值
uint32_t index_;
float32_t maxValue;
arm_max_f32(&fftOutput[1], FFT_SIZE / 2 - 1, &maxValue, &index_);
// 计算实际频率
frequency = (float32_t)index_ * SAMPLING_FREQUENCY / (float32_t)FFT_SIZE;
}
注意arm_max_f32是从第二个点开始查找的,因为DC分量(第一个点)通常很大但不是我们关心的信号频率。
4. 数据处理与性能优化
4.1 采样数据处理技巧
ADC采样得到的是12位数字量,需要转换为电压值。转换公式很简单:电压 = 采样值 * 3.3 / 4096。但实际操作时要注意数据类型转换,否则会丢失精度。
我定义了一个2048*2大小的采样数组,为什么是2倍?因为FFT计算需要复数输入,每个点需要实部和虚部两个float值。采样数据放在实部,虚部全部设为0。
for(int j = 0; j < FFT_SIZE; j++)
{
inputSignal[j * 2] = (adcConvertValue[j]) * 3.3f / 4096.0f;
inputSignal[j * 2 + 1] = 0;
}
在实际应用中,最好加上直流滤波。简单的方法就是减去平均值:
float32_t mean = 0;
for(int i = 0; i < FFT_SIZE; i++) {
mean += inputSignal[i * 2];
}
mean /= FFT_SIZE;
for(int i = 0; i < FFT_SIZE; i++) {
inputSignal[i * 2] -= mean;
}
4.2 DMA中断处理优化
DMA中断处理函数非常关键,但代码要尽量简短。因为在2.4MHz采样率下,DMA完成中断每0.85ms就会发生一次,中断处理时间必须足够短。
void DMA2_Stream0_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc1);
// 用户代码
HAL_TIM_Base_Stop(&htim3);
HAL_ADC_Stop_DMA(&hadc1);
flag = 1;
}
这里只是停止了定时器和ADC,设置了一个标志位。所有耗时的操作(如FFT计算)都在主循环中处理,这样能确保中断响应及时。
4.3 频率计算与精度提升
频率计算公式很简单:frequency = index * SAMPLING_FREQUENCY / FFT_SIZE。但实际精度可以更高,通过插值算法可以突破FFT的频率分辨率限制。
一种简单的方法是抛物线插值:在峰值点附近取三个点,用抛物线拟合找到真正的峰值位置。这样频率精度可以提高一个数量级。
// 抛物线插值求精确频率
if(index_ > 0 && index_ < (FFT_SIZE/2 - 1)) {
float delta = (fftOutput[index_+1] - fftOutput[index_-1]) /
(2.0f * (2.0f * fftOutput[index_] - fftOutput[index_-1] - fftOutput[index_+1]));
frequency = (index_ + delta) * SAMPLING_FREQUENCY / FFT_SIZE;
}
5. 实际测试与结果分析
5.1 测试环境搭建
测试时我用信号发生器产生920kHz的正弦波,幅度设置在1V左右(STM32的ADC输入范围是0-3.3V)。信号通过探头直接连接到开发板的ADC引脚,注意要加上适当的限流电阻保护ADC输入。
串口输出设置為115200bps,这样可以通过printf实时输出频率测量结果。为了验证测量精度,我用频率计同时测量信号发生器的输出频率作为参考。
第一次测试时结果不太理想,测量值波动很大。后来发现是电源噪声的影响,给模拟部分加了LC滤波后明显改善。STM32的ADC对电源质量很敏感,高速采样时一定要保证模拟电源干净稳定。
5.2 性能测试数据
经过优化后,系统性能相当不错。以下是测试数据对比:
| 参数 | 理论值 | 实测值 | 误差 |
|---|---|---|---|
| 采样率 | 2.4MHz | 2.38MHz | 0.83% |
| 频率测量 | 920kHz | 919.6kHz | 0.043% |
| 测量更新率 | 10Hz | 9.5Hz | 5% |
测量更新率指的是每秒钟能完成多少次完整的采样-FFT-计算流程。2048点FFT在STM32F407上大约需要2ms计算时间,加上采样时间0.85ms,理论最大更新率约350Hz。但实际上由于数据处理和串口输出等原因,只能做到10Hz左右。
如果不需要实时输出,只是内部计算的话,更新率可以提高到100Hz以上。这对于大多数应用来说已经足够了。
5.3 常见问题与解决方法
在开发过程中遇到了几个典型问题,这里分享给大家:
第一个问题是采样数据不稳定,时有跳变。原因是DMA传输过程中被其他中断打断,解决方案是提高DMA中断优先级,或者禁用不必要的中断。
第二个问题是FFT结果频谱泄漏严重。这是因为采样频率和信号频率不同步造成的,虽然加了汉宁窗有所改善,但最好还是用锁相环同步采样时钟和信号频率。
第三个问题是高频测量精度不足。除了前面提到的插值算法外,还可以增加采样点数来提高频率分辨率。4096点FFT比2048点分辨率高一倍,但计算时间也几乎翻倍。
最后要注意抗混叠滤波。2.4MHz采样率的奈奎斯特频率是1.2MHz,如果信号中有高于1.2MHz的成分,就会产生混叠失真。在实际应用中最好加上模拟抗混叠滤波器。
6. 应用扩展与进阶优化
这个方案不仅适用于频率测量,还可以扩展很多应用。比如加上SD卡存储,就可以做成一个便携式频谱分析仪。或者加上无线模块,实现远程振动监测。
在多通道应用时,STM32F4的多个ADC可以交替采样,进一步提高有效采样率。比如用ADC1和ADC2交替采样,理论上可以把采样率提高到4.8MHz。
对于实时性要求更高的应用,可以尝试使用STM32F4的DSP指令集手动优化FFT算法。CMSIS-DSP库虽然已经优化得很好,但针对特定应用还有提升空间。
另一个优化方向是降低功耗。在电池供电的应用中,可以间歇性工作:采样一段时间,计算频率,然后进入低功耗模式。STM32F4的停止模式功耗可以降到微安级别。
最后提醒一点:FFT计算会产生很大的数值动态范围,特别是2048点FFT。中间结果可能会溢出,所以要合理选择数据类型和缩放系数。单精度浮点通常足够,但如果用定点数就要特别小心。

924

被折叠的 条评论
为什么被折叠?



