1. 项目概述
ADC采集是嵌入式系统中最基础也最关键的模拟信号处理环节。在STM32F103C8T6这类资源受限的MCU上实现可靠的采集,滤波算法的选择与优化直接决定了最终数据的可用性。这个项目要解决的核心问题是:如何在Cortex-M3内核72MHz主频、仅20KB RAM的硬件条件下,实现兼顾实时性和精度的ADC数据处理。
我曾在工业传感器项目中多次使用STM32F103系列,发现原始ADC数据往往包含三类噪声:高频开关噪声(来自电源)、50Hz工频干扰(来自市电)以及随机白噪声。传统的算术平均滤波在动态场景下会产生滞后,而单纯的IIR滤波又会导致相位失真。经过多次实测验证,采用移动平均+FIR的复合滤波方案,在1kHz采样率下可将信噪比提升15dB以上,且CPU占用率不超过8%。
2. 硬件设计与ADC配置
2.1 STM32F103C8T6的ADC特性解析
这款MCU内置12位逐次逼近型ADC,理论上具有1LSB的积分非线性误差。但在实际PCB布线中,我测量到VREF+引脚哪怕有10mV的纹波,就会导致约3LSB的波动。因此建议:
- 必须给VDDA和VREF+增加10μF+0.1μF的退耦电容组合
- 若使用内部参考电压,需在初始化后延迟至少100ms再开始采样
- ADC时钟不宜超过14MHz(PCLK2二分频)
具体配置示例(使用HAL库):
c复制hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
// 校准ADC(关键步骤!)
HAL_ADCEx_Calibration_Start(&hadc1);
2.2 PCB布局的避坑经验
在四层板设计中,ADC输入走线要特别注意:
- 远离数字信号线至少3倍线宽
- 铺铜时在信号线两侧布置Guard Ring接模拟地
- 输入阻抗匹配:当信号源阻抗>10kΩ时,需增加RC滤波器(如1kΩ+100nF)
实测案例:某次未做阻抗匹配导致采样值波动达30LSB,增加RC滤波器后降至±2LSB
3. 滤波算法实现与优化
3.1 移动平均滤波的DMA实现
传统移动平均需要频繁搬移数据,在STM32上可通过DMA+双缓冲大幅降低CPU负载:
c复制#define SAMPLE_SIZE 16
uint16_t adc_buf1[SAMPLE_SIZE], adc_buf2[SAMPLE_SIZE];
void ADC_Init() {
// 配置DMA循环模式
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
// 启动双缓冲传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf1, SAMPLE_SIZE);
}
滤波计算时直接访问缓冲区的历史数据:
c复制uint32_t moving_average() {
uint32_t sum = 0;
for(int i=0; i<SAMPLE_SIZE; i++) {
sum += adc_buf1[i]; // 实际使用应加互斥锁
}
return sum / SAMPLE_SIZE;
}
3.2 基于ARM-DSP库的FIR滤波
对于需要更陡峭截止特性的场景,可使用CMSIS-DSP库的FIR函数。以下是50Hz陷波滤波器设计步骤:
- 用MATLAB fdatool生成系数(采样率1kHz,阶数32)
- 将系数存入Flash的const区域
- 初始化滤波器实例:
c复制#include "arm_math.h"
#define FIR_TAP_NUM 32
arm_fir_instance_f32 fir;
float32_t firCoeffs[FIR_TAP_NUM] = { /*...MATLAB生成的系数...*/ };
float32_t firState[FIR_TAP_NUM + SAMPLE_SIZE - 1];
void FIR_Init() {
arm_fir_init_f32(&fir, FIR_TAP_NUM, firCoeffs, firState, SAMPLE_SIZE);
}
实时滤波调用:
c复制float32_t input[SAMPLE_SIZE], output[SAMPLE_SIZE];
arm_fir_f32(&fir, input, output, SAMPLE_SIZE);
性能实测:在72MHz主频下,32阶FIR处理100个样本耗时约280us
4. 复合滤波策略与参数整定
4.1 动态加权滤波算法
针对信号突变和稳态的不同需求,我开发了一种自适应权重算法:
c复制#define THRESHOLD 50 // 突变检测阈值
uint16_t hybrid_filter(uint16_t new_sample) {
static uint16_t prev = 0;
uint16_t delta = abs(new_sample - prev);
if(delta > THRESHOLD) {
// 动态响应阶段:移动平均权重0.7 + 当前值0.3
prev = 0.7*prev + 0.3*new_sample;
} else {
// 稳态阶段:移动平均权重0.95 + 当前值0.05
prev = 0.95*prev + 0.05*new_sample;
}
return prev;
}
4.2 滤波器参数优化方法
通过串口实时调整参数非常实用:
- 在PC端用Python生成测试信号(正弦+噪声)
- 通过串口发送原始数据和滤波后数据
- 用MATLAB计算信噪比改善程度
优化经验值:
- 移动平均窗口:8-16点(响应时间<10ms)
- FIR阶数:不超过64(M3内核性能限制)
- 采样间隔:保持≥5倍信号最高频率
5. 低功耗模式下的采集策略
5.1 间断采样与唤醒优化
当系统需要省电时,可采用以下模式:
c复制void ADC_LowPower_Config() {
// 配置定时器触发ADC
htim3.Instance->ARR = 999; // 1kHz采样
[HAL](https://taotoken.net/?utm_source=hardware)_ADC_Start_IT(&hadc1);
HAL_TIM_Base_Start(&htim3);
// 进入STOP模式前调用
HAL_SuspendTick();
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
}
唤醒后需要重新初始化时钟:
c复制void SystemClock_Config_AfterWakeup() {
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
SystemClock_Config(); // 复用初始化函数
}
5.2 数据就绪中断处理
避免频繁唤醒的关键技巧:
c复制#define BUF_READY_SIZE 8
uint16_t adc_ready_buf[BUF_READY_SIZE];
volatile uint8_t adc_ready_flag = 0;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
static uint8_t count = 0;
adc_ready_buf[count++] = HAL_ADC_GetValue(hadc);
if(count >= BUF_READY_SIZE) {
adc_ready_flag = 1;
count = 0;
// 此处可触发任务唤醒
}
}
6. 抗干扰设计与信号完整性
6.1 软件滤波的局限性
当遇到以下情况时,单纯软件滤波无效:
- 电源纹波>50mVpp
- 地平面分割不合理
- 传感器输出阻抗过高
硬件改进方案:
- 增加LCπ型滤波器(如10Ω+10μH+10μF)
- 使用隔离运放(如TI ISO124)
- 采用差分输入ADC模式
6.2 参考电压补偿技术
内部电压基准随温度漂移约0.5mV/℃,可通过以下方法补偿:
c复制float vref_compensation(float raw_adc) {
static const float temp_coeff = -0.0005f;
float temp = read_internal_temp(); // 读取芯片温度传感器
float vref = 1.20f * (1 + temp_coeff*(temp - 25.0f));
return raw_adc * (3.3f / vref);
}