STM32高精度频率计设计

AI助手已提取文章相关产品:

基于STM32F103的高精度频率计设计:ADC采样、DMA传输与FFT处理的协同实现

在音频分析、电机控制或传感器信号监测中,我们常常需要知道一个周期性信号的频率。传统做法是用定时器捕获上升沿,通过测量周期来换算频率——这种方法简单直接,但对于非方波、含噪声的正弦信号却显得力不从心:边沿抖动、占空比变化都会导致测量误差,甚至完全失效。

有没有一种方法能“看穿”复杂波形,直接找出其中隐藏的主频?答案是肯定的:借助 ADC连续采样 + DMA自动搬运 + FFT频谱分析 ,我们可以在一片STM32F103上构建出媲美示波器功能的软件频率计。它不仅能识别任意周期信号(正弦、三角、锯齿),还能在混有噪声的情况下锁定目标频率,分辨率可达亚Hz级别。

这听起来像是高端设备才有的能力,但实际上,只要合理利用MCU内部资源,就能以极低成本实现。本文将带你一步步拆解这个系统的底层逻辑,从硬件配置到算法优化,揭示如何让一颗“普通”的Cortex-M3芯片胜任频谱分析任务。


为什么选择STM32F103?

尽管现在有性能更强的H7或F4系列,但F103依然是许多工程师心中的经典。原因很简单:成本低、资料全、生态成熟。更重要的是,它的外设组合恰好满足实时信号处理的基本需求:

  • 12位ADC :足以应对大多数模拟输入场景;
  • DMA控制器 :支持多通道数据自动搬运;
  • 72MHz主频 :可运行轻量级FFT算法;
  • 丰富的定时器资源 :可用于精确触发采样。

这套组合拳让我们无需额外添加FPGA或DSP芯片,仅靠片上模块即可完成从模拟输入到数字输出的闭环。


精确采样的关键:ADC与定时器联动

要进行频域分析,第一步就是高质量的时域采样。这里的“高质量”不是指分辨率越高越好,而是强调 等时间间隔采样 。如果采样间隔不均匀,哪怕只差几个微秒,也会造成严重的频谱泄漏,使得FFT结果失真。

STM32F103的ADC本身不具备独立时钟驱动能力,必须依赖外部触发源。常见做法是使用软件启动( HAL_ADC_Start() ),但这会导致中断延迟引入抖动。更优的选择是 由定时器触发

以TIM2为例,将其配置为PWM模式,但并不输出任何波形,而是启用“更新事件触发输出”(TRGO)。当计数器溢出时,产生一个脉冲信号连接到ADC的外部触发输入端。这样,每过固定时间(比如100μs),ADC就自动开始一次转换。

// 配置TIM2作为ADC触发源
htim2.Instance = TIM2;
htim2.Init.Prescaler = 72 - 1;           // 72MHz / 72 = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 100 - 1;             // 1MHz / 100 = 10kHz 采样率
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;

// 启用主模式:更新事件触发
TIM_MasterConfigTypeDef sMasterConfig = {0};
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);

HAL_TIM_Base_Start(&htim2);

此时ADC配置为:
- 单通道、连续转换模式;
- 外部触发源设为 ADC_EXTERNALTRIGCONV_T2_TRGO
- 开启DMA请求。

这样一来,整个采样过程完全脱离CPU干预,实现了真正意义上的同步和稳定。


数据搬运的“隐形助手”:DMA的作用远不止省CPU

很多人知道DMA可以减轻CPU负担,但在实际工程中,它的价值远不止于此。尤其是在高速采集场景下, DMA决定了系统能否可靠运行

设想一下:若采用中断方式读取ADC值,每次EOC(转换结束)都进入中断服务程序,保存数据。即使中断响应很快,当中断频繁发生(如10kHz采样率),频繁进出中断会极大增加上下文切换开销,还可能因优先级冲突导致漏采。

而DMA的工作机制完全不同。每当ADC完成一次转换,硬件自动将 ADC_DR 寄存器的内容转移到内存缓冲区,整个过程无需软件参与。你只需要提前告诉DMA三件事:
- 源地址: &ADC1->DR
- 目标地址: adc_buffer[N]
- 传输数量:N

然后启动DMA,剩下的交给硬件。

#define SAMPLES_NUM 1024
uint16_t adc_buffer[SAMPLES_NUM];

// 启动ADC-DMA链式操作
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES_NUM);

这里有个重要细节: 建议使用普通模式而非循环模式 。虽然循环模式可以让DMA不断覆盖缓冲区,看似适合持续采集,但它无法明确通知“一帧数据已满”。相比之下,普通模式在传完1024个点后会产生“传输完成中断”(TC),这是我们启动FFT处理的最佳时机。

如果你担心采样与处理之间出现间隙,也可以考虑双缓冲机制(通过DMA的双缓冲模式或两个独立通道交替工作),确保当前正在处理的数据与新采集的数据物理隔离,避免竞争。


从时域到频域:FFT不是魔法,但很接近

有了干净的采样数据,下一步就是解析频率成分。这时候轮到FFT登场了。

很多人以为FFT是个黑箱函数,调用一下就能出结果。实际上,要想得到准确的频率估计,必须理解其背后的限制和优化手段。

实数FFT vs 复数FFT

由于ADC采集的是实数序列(只有幅度,没有虚部),我们可以使用专门针对实信号优化的RFFT(Real FFT),计算量约为标准FFT的一半。ARM CMSIS-DSP库提供了 arm_rfft_fast_f32() 函数,非常适合嵌入式环境。

#include "arm_math.h"

float32_t fft_input[1024];   // 归一化后的ADC数据
float32_t fft_output[1024];  // 输出为交错复数格式:[re0, im0, re1, im1, ...]
arm_rfft_fast_instance_f32 fft_inst;

// 初始化FFT实例(只需一次)
arm_rfft_fast_init_f32(&fft_inst, 1024);

// 执行变换
arm_rfft_fast_f32(&fft_inst, fft_input, fft_output, 0);

注意: fft_output 虽然是 float32_t 数组,但存储的是复数对。第k个频率点的实部和虚部分别为 fft_output[2*k] fft_output[2*k+1]

幅度计算与峰值检测

FFT的结果是一个复数数组,我们需要从中提取幅值信息:

$$
|X[k]| = \sqrt{Re^2 + Im^2}
$$

遍历前半段(0 ~ N/2),因为实数信号的频谱是对称的,后半部分无新信息。

float max_mag = 0;
uint32_t peak_k = 0;
for (int k = 1; k < 512; k++) {  // 跳过DC分量(k=0)
    float re = fft_output[2*k];
    float im = fft_output[2*k+1];
    float mag = sqrtf(re*re + im*im);
    if (mag > max_mag) {
        max_mag = mag;
        peak_k = k;
    }
}

最终频率为:

$$
f = k_{\text{peak}} \times \frac{f_s}{N}
$$

例如,采样率10kHz、1024点FFT,则分辨率约为9.77Hz。这意味着你能分辨出100Hz和110Hz的信号,但难以区分100Hz和105Hz。

⚠️ 提示:不要迷信“找到最大值就是真实频率”。由于栅栏效应(frequency binning),真实频率可能落在两个bin之间,造成±Δf/2的误差。可通过插值法(如三点辛克插值)进一步提升精度。


如何让FFT结果更靠谱?几个实用技巧

1. 去除直流偏置

绝大多数传感器信号都有一定直流分量(比如麦克风静音时输出1.65V)。如果不处理,会在频谱中形成巨大的0Hz尖峰,掩盖其他低频信号。

解决办法很简单:在FFT前减去均值。

float sum = 0;
for (int i = 0; i < 1024; i++) {
    sum += adc_buffer[i];
}
float mean = sum / 1024;
for (int i = 0; i < 1024; i++) {
    fft_input[i] = (float)(adc_buffer[i]) - mean;
}

当然,也可以直接减去理论中值(如2048对应3.3V参考电压下的中间值),前提是系统偏置稳定。

2. 加窗抑制频谱泄漏

理想情况下,信号在采样窗口内刚好包含整数个周期。但现实中几乎不可能做到,导致信号突变,产生虚假高频成分——这就是所谓的“频谱泄漏”。

解决方案是加窗函数,平滑地衰减窗口边缘的样本。常用的有汉宁窗(Hanning)、汉明窗(Hamming)等。

for (int i = 0; i < 1024; i++) {
    float window = 0.5f * (1.0f - cosf(2*M_PI*i/1023));
    fft_input[i] = ((float)adc_buffer[i] - 2048.0f) * window;
}

加窗虽好,但会降低频率分辨率(主瓣展宽),需根据应用场景权衡。

3. 抗混叠滤波不可少

根据奈奎斯特采样定理,最高可测频率不能超过采样率的一半。如果输入信号中含有更高频率的成分(如开关电源噪声),它们会被“折叠”回低频区,造成误判。

因此,在ADC前端应加入RC低通滤波器,截止频率设置为略低于 fs/2 。例如采样率为10kHz,则滤波器截止频率建议设为4~4.5kHz。


系统整合与性能考量

完整的流程如下:

  1. 初始化GPIO、ADC、TIM2、DMA、UART/LCD;
  2. 启动TIM2和ADC-DMA;
  3. 等待DMA传输完成中断(或轮询标志位);
  4. 触发FFT运算;
  5. 解析主频并输出;
  6. 重置状态,准备下一帧。

在这个过程中有几个关键点需要注意:

  • 内存占用 :1024点float数组约需8KB RAM,对于F103这类小容量型号(如CBT6仅有20KB SRAM)来说压力不小。必要时可改用Q15定点格式,节省一半空间。
  • FFT耗时 :在72MHz主频下,1024点RFFT大约耗时3~5ms。若要求每秒刷新10次频谱,是完全可以接受的。
  • 动态范围管理 :确保输入信号幅度适中。太小则信噪比差;太大则ADC饱和失真。可在前端加入可调增益放大器(PGA)或使用带PGA的ADC(如STM32G系列)。

写在最后:不只是频率计

这套“ADC+DMA+FFT”架构的意义,远不止做一个频率显示器。它是嵌入式信号处理的通用范式,可轻松扩展至更多高级应用:

  • 谐波分析 :检测电网中的3次、5次谐波含量;
  • 振动故障诊断 :识别电机轴承缺陷对应的特征频率;
  • 音频可视化 :实现迷你版音乐频谱灯;
  • LC振荡频率测试仪 :用于调试无线充电线圈匹配。

更重要的是,它教会我们如何在资源受限的环境中,通过软硬件协同设计,挖掘MCU的最大潜力。STM32F103或许已经不算“先进”,但只要掌握正确的工程思维,老平台也能焕发新生。

下次当你面对一个看似复杂的信号处理需求时,不妨想想:是否真的需要更贵的芯片?也许,只是缺了一个好的架构设计。

您可能感兴趣的与本文相关内容

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值