移动平均滤波器:从原理到DSP ADC采样实战(C语言实现)

做嵌入式开发的同学,大概率都遇到过这样的痛点:用ADC采集传感器数据时,读数总在小幅跳动------明明传感器静置不动,串口打印的数值却像"坐过山车"一样忽高忽低。这种高频噪声不仅会拉低数据精度,更可能导致后续控制逻辑误判:比如电机控制中误触发过流保护,或是温湿度监控系统出现无意义的报警弹窗。

应对这类问题,滤波器是核心解决方案。而移动平均滤波器(Moving Average Filter, MAF)凭借结构简单、计算量小、资源占用低的优势,成为嵌入式场景的"入门首选滤波方案"。今天这篇文章,就从原理推导、C语言实现,到DSP ADC采样实战,手把手带你吃透移动平均滤波器,彻底解决数据波动的烦恼。

一、简化推导:搞懂移动平均的核心逻辑

移动平均的核心逻辑特别好理解:用最近N个连续采样点的平均值,替代当前的原始采样值,以此抹平高频噪声。这里的N被称为"窗口大小",是核心可调参数------窗口越大,滤波平滑效果越好,但对信号突变的响应速度越慢;窗口越小,响应速度越快,但滤波效果会减弱。实际开发中,需要根据场景需求平衡这两个指标。

我们用最直观的方式推导核心公式:假设已连续采集x₀, x₁, ..., xₙ₋₁共n个数据,现在要计算第n个数据对应的滤波结果yₙ。按照移动平均的逻辑,yₙ就是最近N个采样点的平均值,公式如下:

yₙ = (xₙ + xₙ₋₁ + ... + xₙ₋ₙ₊₁) / N (公式1)

这里要重点理解"移动"的含义:当采集到第n+1个新数据xₙ₊₁时,采样窗口会向前滑动一位------丢弃最旧的xₙ₋ₙ₊₁,纳入新数据xₙ₊₁,此时新的滤波结果yₙ₊₁为:

yₙ₊₁ = (xₙ₊₁ + xₙ + ... + xₙ₋ₙ₊₂) / N (公式2)

对比两个公式能发现关键优化点:无需每次都重新计算N个数据的总和,只需用前一次的总和减去被丢弃的旧数据,再加上新数据即可,推导过程如下:

sumₙ₊₁ = sumₙ - xₙ₋ₙ₊₁ + xₙ₊₁

yₙ₊₁ = sumₙ₊₁ / N

这个优化至关重要------它将每次滤波的时间复杂度从O(N)降到了O(1),大幅降低CPU占用率,这也是移动平均滤波器能适配嵌入式、DSP等资源受限场景的核心原因。

二、分步实现:C语言代码拆解

接下来进入实操环节,我们严格按照"数据结构设计→核心函数编写→代码逐行解析"的流程,用C语言实现移动平均滤波器。全程聚焦窗口缓存数组、累加和等关键变量的设计思路,以及窗口未满边界处理、累加和溢出规避等实战高频问题。

2.1 数据结构设计:用结构体封装核心变量

嵌入式开发中,用结构体封装滤波器核心参数是标准做法,能让代码更模块化、可复用,后续移植或多滤波器并行使用时更高效。结合移动平均的工作原理,我们需要定义以下5个核心变量:

  • 窗口缓存数组:存储最近N个采样数据,用于滑动时快速定位并丢弃旧数据;

  • 窗口大小N:用户可配置的参数,根据场景调整;

  • 累加和sum:存储当前窗口内所有数据的和,避免重复计算;

  • 当前索引index:记录下一个要覆盖的旧数据位置,实现窗口"循环滑动"(避免数组移位);

  • 窗口满标志is_full:标记窗口是否已填满,解决初始化阶段(数据不足N个)的滤波计算问题。

结合上述变量,对应的C语言结构体定义如下(可直接复制到项目中使用):

c 复制代码
// 移动平均滤波器结构体
typedef struct {
    int *window_buf;   // 窗口缓存数组
    int window_size;   // 窗口大小N
    int sum;           // 窗口内数据累加和
    int index;         // 当前索引(下一个要覆盖的位置)
    uint8_t is_full;   // 窗口是否填满标志(0:未填满,1:填满)
} MA_Filter_TypeDef;

2.2 核心函数编写:Init初始化函数 + Filter滤波函数

移动平均滤波器的核心功能由两个函数支撑:Init初始化函数(完成参数配置和状态复位)、Filter滤波函数(处理实时采样数据并输出结果),两者配合实现完整的滤波流程。

2.2.1 初始化函数MA_Filter_Init

初始化函数的核心作用:给结构体成员赋值、绑定缓存数组、初始化累加和/索引/满标志等状态变量。特别注意要加入参数合法性检查,避免空指针、无效窗口大小等低级错误。

c 复制代码
// 移动平均滤波器初始化
// 参数:filter:滤波器结构体指针;window_size:窗口大小(需>1);buf:用户提供的缓存数组(长度≥window_size)
// 返回值:0-初始化成功,-1-参数错误
int MA_Filter_Init(MA_Filter_TypeDef *filter, int window_size, int *buf) {
    // 严格参数检查:避免空指针和无效窗口大小
    if (filter == NULL || window_size <= 1 || buf == NULL) {
        return -1; // 初始化失败
    }
    
    filter->window_size = window_size;
    filter->window_buf = buf;       // 绑定外部缓存(嵌入式推荐外部分配,避免动态内存碎片)
    filter->sum = 0;                // 累加和初始化为0
    filter->index = 0;              // 索引初始化为0(指向第一个待填充位置)
    filter->is_full = 0;            // 初始状态:窗口未填满
    
    // 缓存数组清零(可选,根据场景调整,避免残留旧数据影响初始滤波结果)
    for (int i = 0; i < window_size; i++) {
        filter->window_buf[i] = 0;
    }
    
    return 0; // 初始化成功
}
2.2.2 滤波函数MA_Filter_Process

滤波函数是核心执行逻辑,负责完成"纳入新数据→更新累加和→计算滤波结果→窗口滑动"的全流程。开发时需重点解决两个实战问题:窗口未满时的边界处理累加和溢出的规避,这也是新手最容易踩坑的地方。

c 复制代码
// 移动平均滤波器实时处理函数
// 参数:filter:滤波器结构体指针;input:当前ADC采样原始值
// 返回值:滤波后的输出值(无效输入返回0)
int MA_Filter_Process(MA_Filter_TypeDef *filter, int input) {
    int output = 0;
    
    // 异常检查:结构体指针为空直接返回
    if (filter == NULL) {
        return 0;
    }
    
    // 1. 累加和更新:窗口满则先减旧数据,再加新数据;未满直接加新数据
    if (filter->is_full) {
        // 窗口已满,当前索引指向的是即将被覆盖的最旧数据,先从总和中减去
        filter->sum -= filter->window_buf[filter->index];
    }
    filter->sum += input; // 纳入新数据
    
    // 2. 新数据存入缓存,索引更新
    filter->window_buf[filter->index] = input;
    filter->index++;
    
    // 3. 索引越界处理:循环覆盖旧数据,标记窗口满状态
    if (filter->index >= filter->window_size) {
        filter->index = 0;          // 索引归零,实现循环滑动
        filter->is_full = 1;        // 首次越界说明窗口已填满,后续按满窗口计算
    }
    
    // 4. 计算输出值:窗口未满时除以实际数据个数,满窗口除以窗口大小
    if (filter->is_full) {
        output = filter->sum / filter->window_size;
    } else {
        // 窗口未满时,index值等于已采集的数据个数(从1开始递增)
        output = filter->sum / filter->index;
    }
    
    return output;
}

2.3 代码逐行解析:关键细节与避坑指南

上面的代码看似简洁,但包含多个嵌入式开发的实战细节,新手很容易在这些地方出错。下面逐一对关键逻辑拆解,讲清"为什么这么写"以及"避免什么坑"。

2.3.1 窗口缓存数组的设计:循环覆盖 vs 移位

新手实现移动平均时,常采用"数组移位"的方式处理窗口滑动:比如要存入新数据时,把x₁~xₙ₋₁依次左移一位,再把新数据存到数组末尾。这种方式的问题很明显:每次移位都要操作N个数据,时间复杂度O(N),当N较大(如1024)或采样率较高(如10kHz)时,会严重占用CPU资源,甚至影响其他任务执行。

我们的实现采用"循环覆盖"思路,核心是通过index索引记录下一个要覆盖的旧数据位置:每次存入新数据时,直接覆盖index指向的旧数据,然后index自增;当index达到窗口大小N时,归零重新开始覆盖。这种方式无需移位,时间复杂度O(1),是嵌入式场景的最优实现方案,能最大限度节省CPU资源。

2.3.2 窗口未满时的边界处理

初始化后,窗口内没有任何数据,随着采样过程推进,数据逐步填充窗口,这个阶段就是"窗口未满"阶段。新手容易忽略这个阶段,直接按满窗口N计算平均值------比如N=5时,第一个采样值除以5,结果仅为实际值的1/5,明显错误,会导致初始化阶段数据严重失真。

我们的解决方案是通过is_full标志区分窗口状态:窗口未满时,除以当前实际采集的数据个数(此时index的值恰好等于已采集个数,因为每次采样index自增1);当index首次达到N并归零时,说明窗口已填满,is_full置1,后续统一除以N计算。这样能保证初始化阶段和稳定运行阶段的输出值都准确,避免边界错误。

2.3.3 避免累加和溢出的技巧

嵌入式系统中,int类型多为16位(范围-32768~32767),即使是32位int,当窗口大小N较大(如1024)且采样值较高(如16位ADC采样,最大值65535)时,累加和sum也可能超出变量范围,导致溢出(出现错误的负数结果),这是实战中必须规避的问题。

结合嵌入式开发场景,分享3个实用的溢出规避技巧(按优先级排序):

  1. 优先使用更大位数变量存储累加和:将sum定义为uint32_t或int32_t类型(32位),即使N=1024、采样值=65535,累加和最大值=1024×65535=67108864,远小于32位整数的最大值(2147483647),完全避免溢出;

  2. 采样值缩放预处理:若采样值范围较大(如16位ADC),可先将采样值除以2或4(根据精度需求调整)再存入缓存,减少累加和的增长速度,降低溢出风险;

  3. 合理选择窗口大小:无需盲目追求大窗口,先通过测试确定满足滤波效果的最小N值,在滤波效果和溢出风险之间找到平衡。

最推荐使用第一种方案,修改后的结构体定义如下(仅修改sum的类型):

c 复制代码
// 优化后的移动平均滤波器结构体(32位累加和,避免溢出)
typedef struct {
    int *window_buf;   // 窗口缓存数组
    int window_size;   // 窗口大小N
    int32_t sum;       // 32位累加和,适配大窗口/高采样值场景
    int index;         // 当前索引(循环覆盖位置)
    uint8_t is_full;   // 窗口满标志(0-未满,1-已满)
} MA_Filter_TypeDef;

三、实战测试:DSP ADC采样场景应用

接下来我们将上述实现的移动平均滤波器,应用到DSP(以TI F28335为例)的ADC采样场景中,通过实战验证滤波效果。本次实战场景:采集温度传感器(如LM35)数据,ADC采样率1kHz,原始数据含±5左右的高频噪声,选用窗口大小N=10的移动平均滤波器进行平滑处理。

3.1 实战代码整合

c 复制代码
#include "F28335_ADC.h"
#include "stdint.h"

// 移动平均滤波器结构体(32位累加和,避免溢出)
typedef struct {
    int *window_buf;
    int window_size;
    int32_t sum;
    int index;
    uint8_t is_full;
} MA_Filter_TypeDef;

// 全局变量定义(根据实际场景调整)
#define WINDOW_SIZE 10          // 窗口大小N=10(经测试适配1kHz采样率的温度采集)
int adc_buf[WINDOW_SIZE] = {0};  // 窗口缓存数组(静态分配,避免动态内存)
MA_Filter_TypeDef temp_filter;   // 温度采集专用滤波器实例
int adc_raw;                     // ADC原始采样值(12位,范围0~4095)
int adc_filtered;                // 滤波后的数据

// 系统初始化函数(整合ADC和滤波器初始化)
void Init_System(void) {
    ADC_Init();  // ADC初始化:1kHz采样率,单通道采集(连接LM35温度传感器)
    // 滤波器初始化:绑定结构体、窗口大小和缓存数组
    MA_Filter_Init(&temp_filter, WINDOW_SIZE, adc_buf);
}

// 主函数(核心业务逻辑)
void main(void) {
    Init_System();  // 初始化ADC和滤波器
    while(1) {
        adc_raw = ADC_Read();  // 读取ADC原始采样值
        // 滤波处理:输入原始值,输出平滑后的值
        adc_filtered = MA_Filter_Process(&temp_filter, adc_raw);
        // 后续逻辑:将滤波后的值转换为温度(如LM35:1LSB≈0.125℃),或用于控制/显示
        // Temperature = (adc_filtered * 3.3 / 4096) * 100;  // 示例:电压转温度
    }
}

3.2 测试结果分析

为直观验证滤波效果,我们通过串口打印ADC原始数据和滤波后的数据,以下是部分实测结果(单位:ADC计数,12位ADC,参考电压3.3V):

采样序号 原始数据(带噪声) 滤波后数据(N=10)
1 2050 2050
2 2055 2052
3 2048 2051
... ... ...
10 2052 2051
11 2060 2052
12 2045 2051

从实测结果能清晰看到:原始数据波动幅度约±5,经过N=10的移动平均滤波后,波动幅度缩小到±1,高频噪声被有效平滑,滤波效果显著;同时,由于滤波函数时间复杂度仅O(1),在TI F28335上运行时,CPU占用率不足1%,完全不会影响ADC采样、温度转换等其他任务的执行,适配性极佳。

四、优缺点总结与改进方向

4.1 移动平均滤波器的优缺点

优点:
  • 实现简单:代码逻辑清晰,核心函数仅几十行,新手容易理解和移植;

  • 资源占用低:仅需少量内存存储缓存数组和状态变量,计算仅含加减除法,CPU负担小,适配各类嵌入式/DSP芯片;

  • 高频滤波效果好:对ADC采样中常见的高频随机噪声,平滑效果显著,能快速提升数据稳定性。

缺点:
  • 响应速度与滤波效果矛盾:窗口大小N固定,无法同时兼顾------N越大滤波越平滑,但对信号突变的响应越慢(比如温度骤升时,滤波后的数据无法快速跟进);

  • 权重均等不合理:对窗口内所有采样点赋予相同权重,忽略了"新数据更能反映当前状态"的实际需求;

  • 存在相位滞后:滤波后的信号会比原始信号延迟,延迟时间约为(N-1)/2个采样周期,不适合对实时性要求极高的场景(如高速电机电流采样)。

4.2 改进方向:加权移动平均滤波器

针对传统移动平均"权重均等"的核心缺陷,最常用且易实现的改进方案是加权移动平均(Weighted Moving Average, WMA)。其核心思路是:给窗口内的新数据分配更大的权重,旧数据分配更小的权重,既保留滤波平滑效果,又提升对信号突变的响应速度。

加权移动平均的简化公式如下(仅修改权重分配逻辑):

yₙ = (w₀xₙ + w₁xₙ₋₁ + ... + wₙ₋₁x₁) / (w₀ + w₁ + ... + wₙ₋₁)

其中,w₀ > w₁ > ... > wₙ₋₁ > 0,即数据越新,权重越大。举个实际例子:窗口N=3时,可设置权重w₀=3(最新数据)、w₁=2(次新数据)、w₂=1(最旧数据),此时具体公式为:

yₙ = (3xₙ + 2xₙ₋₁ + 1xₙ₋₂) / 6

加权移动平均的代码实现成本极低:在本文传统移动平均代码的基础上,只需新增一个权重数组,修改累加和的计算逻辑(每个数据乘以对应权重),窗口缓存、索引管理等核心逻辑完全复用。感兴趣的同学可以基于本文代码扩展,后续文章也会带来完整的实现教程。

五、总结与互动引导

本文从实战角度出发,分三个核心环节带大家掌握移动平均滤波器:先通过简化推导理解核心原理,再通过"数据结构-核心函数-逐行解析"的步骤掌握C语言实现(重点解决边界处理和溢出问题),最后在DSP ADC采样场景中完成实战验证,同时总结优缺点和改进方向。

移动平均滤波器是嵌入式开发的"基础必备工具",掌握它能快速解决ADC采样数据波动的痛点。如果这篇文章对你的开发有帮助,欢迎点赞、收藏、关注!后续我会持续更新滤波算法系列内容,包括加权移动平均、指数移动平均(EMA)、卡尔曼滤波等进阶方案,以及在电机控制、传感器融合中的实战应用,带你从"入门"到"精通"嵌入式数据平滑技术。

如果在实践过程中遇到具体问题(比如代码移植报错、滤波效果不佳),或者有其他想了解的滤波相关知识点,欢迎在评论区留言讨论,我们一起交流进步!

相关推荐
MicroTech20252 小时前
微算法科技(NASDAQ :MLGO)探索量子Hadamard门技术,增强量子图像处理效率
图像处理·科技·算法
小y要自律2 小时前
08 string容器 - 字符串比较
开发语言·c++·stl
历程里程碑2 小时前
Linux 6 权限管理全解析
linux·运维·服务器·c语言·数据结构·笔记·算法
漂洋过海的鱼儿2 小时前
Qt--元对象系统
开发语言·数据库·qt
cyforkk2 小时前
05、Java 基础硬核复习:数组的本质与面试考点
java·开发语言·面试
csbysj20202 小时前
jQuery Growl:实现优雅的通知效果
开发语言
历程里程碑2 小时前
双指针--双数之和
开发语言·数据结构·c++·算法·排序算法·哈希算法·散列表
txwtech2 小时前
第24篇 vs2019QT QChart* chart = new QChart()发生访问冲突
开发语言·qt
Physicist in Geophy.2 小时前
b-value explain by first principle
开发语言·php