去年参加电子设计竞赛时,我和队友遇到了一个棘手的问题:需要设计一个能同时测量低频和高频信号的系统。传统输入捕获法在低频段表现尚可,但遇到高频信号时就力不从心了。经过反复尝试,我们发现结合ADC采样、DMA传输和FFT分析的方案,能够实现从10Hz到50kHz的宽范围频率测量。
这个方案的核心优势在于三点:硬件资源占用少(仅需1个ADC通道)、测量精度高(12位ADC分辨率)、实时性强(DMA自动搬运数据)。比如在测量电机转速时,既能捕捉低速转动的低频振动,也能分析PWM驱动产生的高频谐波。
我们使用的STM32F103C8T6最小系统板,核心配置如下:
实际接线时要注意:
采样系统的性能取决于三个关键参数:
提示:测量低频信号时需权衡采样率和采样时间。例如要分辨1Hz信号,采样时间至少需要1秒(Fs=1024Hz)
先初始化ADC为定时器触发模式:
c复制void ADC1_Init(void) {
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
}
DMA配置采用循环缓冲模式:
c复制#define NPT 1024
uint16_t ADC_Value[NPT];
void DMA_Config(void) {
DMA_InitTypeDef DMA_InitStructure;
DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_Value;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = NPT;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE);
}
使用STM32 DSP库需完成三步操作:
arm_math.h和对应CMSIS-DSP库c复制#define FFT_SIZE 1024
arm_cfft_radix4_instance_f32 S;
float32_t fft_input[FFT_SIZE*2]; // 实部+虚部
float32_t fft_output[FFT_SIZE];
c复制arm_cfft_radix4_init_f32(&S, FFT_SIZE, 0, 1);
通过查找频谱峰值确定主频率:
c复制void ProcessFFT(void) {
// 1. 数据格式转换
for(int i=0; i<FFT_SIZE; i++) {
fft_input[2*i] = (float32_t)ADC_Value[i] - 2048; // 去除直流分量
fft_input[2*i+1] = 0;
}
// 2. 执行FFT
arm_cfft_radix4_f32(&S, fft_input);
// 3. 计算幅值
arm_cmplx_mag_f32(fft_input, fft_output, FFT_SIZE);
// 4. 寻找峰值
float32_t maxValue;
uint32_t maxIndex;
arm_max_f32(fft_output, FFT_SIZE/2, &maxValue, &maxIndex);
// 5. 计算实际频率
float signalFreq = (float)maxIndex * (SAMPLING_RATE) / (float)FFT_SIZE;
}
在实验室调试时,我们遇到过几个典型问题:
c复制for(int i=0; i<NPT; i++) {
float window = 0.5 * (1 - cos(2*PI*i/(NPT-1)));
fft_input[2*i] *= window;
}
通过以下改动将处理时间从15ms降至6ms:
arm_cfft_q15定点数运算实测数据显示优化前后对比:
| 优化项 | 处理时间 | 内存占用 |
|---|---|---|
| 原始方案 | 15.2ms | 8KB |
| 启用硬件FPU | 9.8ms | 8KB |
| 改用Q15格式 | 6.1ms | 4KB |
在某直流电机监测项目中,我们通过此方案成功识别出:
调整采样率至44.1kHz后,实现了:
记得第一次成功捕捉到440Hz标准音叉信号时,示波器上清晰的频谱峰让我们整个团队都欢呼起来。这种从底层硬件到上层算法全栈掌控的成就感,正是嵌入式开发的魅力所在。