1. 项目背景与需求分析
在嵌入式系统开发中,ADC采集数据的准确性直接影响整个系统的可靠性。STM32F103C8T6作为一款经典的Cortex-M3内核微控制器,其内置的12位ADC模块在工业控制、环境监测等领域广泛应用。然而实际应用中,ADC采集到的数据往往伴随着各种噪声干扰,就像我在水质监测项目中遇到的情况——当附近电机启动时,ADC读数会出现剧烈波动,严重影响监测结果的准确性。
面对这种情况,滤波算法就成了解决问题的关键。经过多次实测对比,我发现卡尔曼滤波和中位值滤波这两种算法各有特点:中位值滤波对脉冲干扰有奇效,而卡尔曼滤波则擅长处理连续变化信号中的随机噪声。本文将详细解析这两种算法的实现原理、参数调优技巧以及实际应用效果对比。
2. 硬件平台与基础配置
2.1 STM32F103C8T6 ADC模块特性
STM32F103C8T6的ADC模块主要参数如下:
- 12位分辨率
- 0-3.6V输入电压范围
- 1μs采样时间(在56MHz ADC时钟下)
- 规则组和注入组两种转换模式
- 支持外部触发和DMA传输
在实际水质监测项目中,我使用ADC1的通道0(PA0引脚)连接pH传感器,采用TIM3定时触发采样,配置为239.5个ADC时钟周期的采样时间,这对高阻抗传感器信号获取尤为重要。
2.2 关键外设初始化代码解析
ADC初始化代码需要特别注意时钟配置和触发设置:
c复制void ADC1_Init(void)
{
// 使能ADC1时钟(位于APB2总线)
RCC->APB2ENR |= 1<<9;
// CR2配置:外部触发使能、右对齐、连续转换关闭
ADC1->CR2 = 0x13000000;
// 规则组序列长度设为1(仅转换通道0)
ADC1->SQR1 &= ~(0xF<<20);
// 通道0采样时间239.5周期(保证采样充分)
ADC1->SMPR2 |= 0x07;
// TIM3触发配置(每100ms触发一次采样)
TIM3->ARR = 7200-1; // 72MHz/7200=10kHz
TIM3->PSC = 100-1; // 10kHz/100=100Hz
TIM3->CR2 |= 0x0020; // 更新事件触发输出
TIM3->CR1 |= 0x01; // 启动定时器
}
注意:当使用定时器触发ADC时,务必确保定时器时钟已正确使能,且触发信号极性配置与ADC期望的一致。我曾遇到过因TIM3的CR2寄存器配置错误导致ADC无法触发的问题。
3. 中位值滤波算法实现与优化
3.1 基础算法实现
中位值滤波的核心思想是通过排序取中间值来消除异常采样点。我实现的5点中位值滤波代码如下:
c复制#define MEDIAN_WINDOW 5
uint16_t MedianFilter(uint16_t new_val)
{
static uint16_t buffer[MEDIAN_WINDOW] = {0};
static uint8_t index = 0;
// 循环填充缓冲区
buffer[index++] = new_val;
if(index >= MEDIAN_WINDOW) index = 0;
// 冒泡排序实现
for(uint8_t i=0; i<MEDIAN_WINDOW-1; i++){
for(uint8_t j=0; j<MEDIAN_WINDOW-1-i; j++){
if(buffer[j] > buffer[j+1]){
uint16_t temp = buffer[j];
buffer[j] = buffer[j+1];
buffer[j+1] = temp;
}
}
}
return buffer[MEDIAN_WINDOW/2]; // 返回中间值
}
3.2 性能优化技巧
在实际使用中发现几个优化点:
-
窗口大小选择:窗口越大滤波效果越好,但延迟和计算量也越大。对于STM32F103,5-7点窗口是性价比最高的选择。在水质监测中,我最终采用5点窗口,计算耗时约28μs(72MHz主频)。
-
排序算法优化:冒泡排序虽然简单,但对于固定窗口大小,可以改用更高效的插入排序。优化后版本计算时间缩短到18μs:
c复制// 优化版插入排序实现
void InsertSort(uint16_t arr[], uint8_t n)
{
uint8_t i, j;
uint16_t key;
for(i=1; i<n; i++){
key = arr[i];
j = i-1;
while(j>=0 && arr[j]>key){
arr[j+1] = arr[j];
j--;
}
arr[j+1] = key;
}
}
- 内存访问优化:将buffer数组定义为
static __IO类型可以加快访问速度:
c复制static __IO uint16_t buffer[MEDIAN_WINDOW];
3.3 实际应用效果
在电机干扰测试中,中位值滤波表现出色:
- 对100ms宽度的脉冲干扰抑制率:98%
- 信号延迟:约300ms(5点窗口@10Hz采样率)
- CPU占用率:约5%(72MHz主频)
经验分享:当遇到周期性强干扰时,可以尝试将采样率调整为干扰频率的非整数倍,这样干扰不会固定落在某个采样点上,中位值滤波效果会更好。
4. 卡尔曼滤波算法深度解析
4.1 算法原理与实现
卡尔曼滤波是一种基于状态空间模型的最优估计算法。在ADC滤波应用中,我们可以建立如下模型:
- 状态方程:x_k = x_{k-1} + w_k
- 观测方程:z_k = x_k + v_k
其中w_k和v_k分别是过程噪声和观测噪声。实现代码如下:
c复制typedef struct {
float Q; // 过程噪声方差
float R; // 观测噪声方差
float x_hat; // 状态估计值
float P; // 估计误差协方差
} KalmanFilter;
uint16_t KalmanProcess(KalmanFilter* kf, uint16_t z)
{
// 预测步骤
float x_hat_minus = kf->x_hat;
float P_minus = kf->P + kf->Q;
// 更新步骤
float K = P_minus / (P_minus + kf->R); // 卡尔曼增益
kf->x_hat = x_hat_minus + K*(z - x_hat_minus);
kf->P = (1 - K) * P_minus;
return (uint16_t)kf->x_hat;
}
4.2 参数调优方法论
卡尔曼滤波的性能很大程度上取决于Q和R参数的设置:
-
Q(过程噪声方差):表示我们对系统模型的信任程度。Q值越小,滤波器越相信预测模型。在水质监测中,pH值通常变化缓慢,我设置为0.01。
-
R(观测噪声方差):反映测量噪声水平。可以通过采集静止状态下的ADC数据计算方差得到。实测传感器噪声方差约为4-6,因此设为5.0。
调试技巧:
- 先设置R=测量方差,Q=0.01
- 观察滤波效果,如果响应太慢,适当增大Q
- 如果滤波后仍有明显噪声,适当减小R
4.3 定点数优化实现
考虑到STM32F103没有FPU,浮点运算效率较低,可以采用Q15定点数格式优化:
c复制#include <arm_math.h>
typedef struct {
q15_t Q; // Q15格式(0.01→32768*0.01=327)
q15_t R; // Q15格式(5.0→32768*5.0=163840)
q15_t x_hat;
q15_t P;
} KalmanFilter_Q15;
uint16_t KalmanProcess_Q15(KalmanFilter_Q15* kf, uint16_t z)
{
// 预测
q15_t x_hat_minus = kf->x_hat;
q15_t P_minus = __SSAT(kf->P + kf->Q, 16);
// 更新
q31_t K_tmp = (q31_t)P_minus * 32768 / (P_minus + kf->R);
q15_t K = (q15_t)(K_tmp >> 15);
kf->x_hat = x_hat_minus + ((q31_t)K * (z - x_hat_minus)) >> 15;
kf->P = ((q31_t)(32768 - K) * P_minus) >> 15;
return (uint16_t)kf->x_hat;
}
优化后性能对比:
- 浮点版本:约45μs/次
- Q15定点版本:约15μs/次
- 精度损失:<0.5%
5. 组合滤波策略与性能对比
5.1 级联滤波方案
结合两种算法的优势,我设计了中位值+卡尔曼的级联滤波方案:
c复制// 全局滤波器实例
KalmanFilter adc_filter = {0.01, 5.0, 0.0, 1.0};
// 主循环处理
while(1)
{
// 获取原始ADC值(12位右对齐)
uint16_t raw_adc = ADC1->DR & 0xFFF;
// 第一级:中位值滤波
uint16_t median_val = MedianFilter(raw_adc);
// 第二级:卡尔曼滤波
uint16_t kalman_val = KalmanProcess(&adc_filter, median_val);
// 三路DAC输出用于示波器观察
DacOutput(0, raw_adc>>4); // 原始信号
DacOutput(1, median_val>>4); // 中位值
DacOutput(2, kalman_val>>4); // 卡尔曼
// 控制采样间隔
Delay_ms(10);
}
5.2 性能对比测试
在电机启停干扰测试中,三种数据对比结果如下:
| 指标 | 原始信号 | 中位值滤波 | 卡尔曼滤波 | 级联滤波 |
|---|---|---|---|---|
| 信噪比(dB) | 24.5 | 38.2 | 42.7 | 46.3 |
| 延迟时间(ms) | 0 | 300 | 100 | 350 |
| CPU占用率(%) | 0 | 5 | 8 | 12 |
| 内存占用(Byte) | 0 | 10 | 16 | 26 |
5.3 波形对比分析
通过DAC输出到示波器观察,可以明显看到:
- 原始信号:呈现明显的50Hz工频干扰和随机脉冲(电机干扰)
- 中位值滤波:有效消除了脉冲干扰,但信号呈阶梯状变化,存在滞后
- 卡尔曼滤波:输出平滑,跟随性好,但对强脉冲抑制不足
- 级联滤波:兼具两者的优点,既平滑又稳定
实测技巧:当发现卡尔曼滤波输出出现"过平滑"现象(丢失真实信号变化)时,可以尝试以下调整:
- 增大Q值(如从0.01调到0.05)
- 减小R值(如从5.0调到3.0)
- 降低中位值滤波的窗口大小(如从5降到3)
6. 常见问题与解决方案
6.1 中位值滤波相关问题
问题1:滤波后信号出现明显阶梯状
- 原因:窗口大小过大导致响应迟缓
- 解决:减小窗口尺寸(3-5点),或改用加权中值滤波
问题2:偶发脉冲干扰仍能通过
- 原因:干扰持续时间超过窗口采样间隔
- 解决:增加采样率,或采用动态窗口大小(异常时自动增大窗口)
6.2 卡尔曼滤波相关问题
问题1:滤波输出发散不稳定
- 原因:数值计算溢出(特别是定点数实现)
- 解决:增加范围检查,或改用浮点实现
c复制// 在卡尔曼更新步骤中加入饱和处理
kf->x_hat = __SSAT(x_hat_minus + K*(z - x_hat_minus), 16);
kf->P = __SSAT((1 - K) * P_minus, 16);
问题2:参数调优困难
- 解决:实现参数自整定功能,根据信号方差动态调整Q和R
c复制// 简单的R值自适应算法
float estimate_R(uint16_t *samples, uint8_t n)
{
float mean = 0, var = 0;
for(uint8_t i=0; i<n; i++) mean += samples[i];
mean /= n;
for(uint8_t i=0; i<n; i++) var += (samples[i]-mean)*(samples[i]-mean);
return var/n;
}
6.3 系统级优化建议
- DMA传输优化:使用DMA将ADC数据直接传输到内存,减少CPU干预
c复制// 启用ADC DMA
ADC1->CR2 |= 0x0100;
DMA1_Channel1->CCR = 0x000025A0; // 循环模式,16位,内存增量
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;
DMA1_Channel1->CMAR = (uint32_t)adc_buffer;
DMA1_Channel1->CNDTR = BUFFER_SIZE;
DMA1_Channel1->CCR |= 0x1; // 开启DMA
-
定时器触发优化:使用硬件定时器精确控制采样间隔,避免软件延时误差
-
多传感器处理:对于多通道ADC采样,可以分时复用同一个滤波器实例,节省内存
7. 扩展应用与进阶优化
在实际项目中,我还尝试了以下几种进阶方案:
-
自适应混合滤波:根据信号变化率自动切换滤波算法
- 当检测到快速变化时,使用较小窗口的中位值滤波
- 当信号稳定时,切换到卡尔曼滤波
-
频域分析辅助:通过FFT分析干扰频率,针对性设计滤波器参数
- 使用STM32的DSP库进行实时频谱分析
- 根据主要干扰频率调整采样率
-
机器学习增强:采集大量数据训练简单模型预测噪声特征
- 使用线性回归预测噪声变化趋势
- 动态调整卡尔曼参数
-
硬件滤波配合:
- 在ADC输入端增加RC低通滤波(截止频率≈采样率/5)
- 使用TVS二极管抑制高压脉冲
- 优化PCB布局减少串扰
经过这些优化后,系统在复杂工业环境下的ADC采集稳定性得到显著提升。特别是在同时存在周期性干扰和随机脉冲噪声的场景下,级联滤波方案的信噪比相比单一算法提高了65%。