拓冰建站拓冰建站
首页 / 资讯中心 / 正文

单片机传感器数据滤波:移动平均滤波原理与C语言高效实现

1. 先搞清楚移动平均滤波到底在解决什么问题如果你在用单片机处理传感器数据比如温度、电压、AD采样值肯定会遇到一个问题数据跳得太厉害。明明温度没变读回来的数值却在上下乱蹦。这时候移动平均滤波就是最直接、最常用的“稳定器”。它不搞复杂的数学核心思想很简单用最近几次采样值的平均值来代替当前这一次的采样值。比如你连续采样5次就把这5个数加起来除以5把这个平均值作为“有效值”输出。下一次采样时丢掉最老的那个数加入最新的数再算平均值如此“移动”下去。听起来简单但在单片机上实现有几个关键点决定了它是“能用”还是“好用”速度与资源的平衡平均值窗口开多大窗口越大数据越平滑但反应越慢占用的内存数组也越多。对于51、STM32这类资源有限的单片机窗口大小是第一个要权衡的。实时性考量是每次采样都立刻计算一次平均值实时移动平均还是攒够一批数据再算这决定了系统的响应速度。整数与浮点单片机特别是8位、16位处理浮点数速度慢如何用整数运算甚至移位来高效实现平均是提升性能的关键。边界处理刚开始采样数据还没填满整个窗口时怎么算是直接输出原始值还是用已有数据计算所以“速通”移动平均滤波目标不是背公式而是快速理解原理并写出一个在资源受限的单片机上跑得快、占内存少、效果明显的C语言实现。下面我就按实际开发的顺序带你走一遍。2. 从原理到代码两种最实用的实现方案移动平均滤波主要有两种实现思路队列式和递推式。队列式直观好理解递推式计算快、省资源。我们先从最基础的队列式开始这是理解原理的最佳途径。2.1 方案一队列式移动平均适合理解原理和固定窗口这种方案模拟了“滑动窗口”的物理过程。你需要一个数组作为数据队列一个变量记录队列总和以及一个指针或索引来管理队列的更新。核心步骤初始化创建一个固定长度的数组data_buf[]一个总和变量sum一个索引index。将数组清零sum清零index指向队列起始位置。更新数据当获得一个新采样值new_sample时从sum中减去即将被覆盖的旧值data_buf[index]。将新值new_sample存入data_buf[index]。将新值加到sum中。更新索引index指向下一个位置循环队列。计算平均值平均值average sum / WINDOW_SIZE。WINDOW_SIZE就是你的窗口长度。C语言示例代码以窗口大小10为例#define WINDOW_SIZE 10 // 平均窗口大小根据需求调整 typedef struct { int buffer[WINDOW_SIZE]; // 数据缓冲区 int sum; // 缓冲区数据总和 int index; // 当前写入位置索引 } MovingAverageFilter; // 1. 初始化滤波器 void MAF_Init(MovingAverageFilter *filter) { for(int i0; iWINDOW_SIZE; i) { filter-buffer[i] 0; } filter-sum 0; filter-index 0; } // 2. 输入新数据并返回当前移动平均值 int MAF_Update(MovingAverageFilter *filter, int new_sample) { // 减去即将被覆盖的旧值 filter-sum - filter-buffer[filter-index]; // 存入新值 filter-buffer[filter-index] new_sample; // 加上新值 filter-sum new_sample; // 更新索引循环队列 filter-index (filter-index 1) % WINDOW_SIZE; // 计算并返回平均值 return filter-sum / WINDOW_SIZE; }这个方案的优缺点优点逻辑极其清晰和理论完全对应容易调试。你可以随时查看buffer数组里的历史数据。缺点每次更新需要一次减法、一次加法、一次除法和一次索引更新。除法在单片机上比较耗时尤其是窗口大小不是2的幂次方时。另外需要固定长度的数组内存占用是O(N)。注意初始化后前WINDOW_SIZE-1次调用MAF_Update时缓冲区未被填满此时计算的平均值是基于部分数据的包含了初始的0。如果这对你的应用有影响可以额外增加一个计数器只在填满后才开始输出平均值或者前几次直接输出原始值。2.2 方案二递推式移动平均适合追求速度和简单场景递推式公式来源于数学推导当前平均值 上一次平均值 (新采样值 - 最早采样值) / 窗口大小。但“最早采样值”我们通常没有保存。于是衍生出一种简化版也叫“一阶滞后滤波”或“指数平均”但它不是标准移动平均。这里介绍一种更接近标准移动平均且高效的递推式改进算法。它需要一个队列来保存最早的那个值但计算更快。核心思想维护一个总和sum。更新时sum sum - oldest new_sample。那么问题就变成了如何高效地获取oldest。我们可以用循环队列但只记录“最早值”的位置。实际上队列式已经是一种高效的实现了。真正更快的“递推”是下面这种适用于窗口大小为2的幂次方的变体它用移位代替除法速度极快。#define WINDOW_SIZE 16 // 必须是 2, 4, 8, 16, 32... 等2的幂次方 typedef struct { int buffer[WINDOW_SIZE]; int sum; int index; } MovingAverageFilter; void MAF_Init(MovingAverageFilter *filter) { /* 同上 */ } int MAF_Update_Fast(MovingAverageFilter *filter, int new_sample) { filter-sum - filter-buffer[filter-index]; filter-buffer[filter-index] new_sample; filter-sum new_sample; filter-index (filter-index 1) % WINDOW_SIZE; // 关键点用右移代替除法。WINDOW_SIZE16则右移4位。 return filter-sum 4; // 等价于 sum / 16 }这个方案的优缺点优点当WINDOW_SIZE为2的幂次方时用右移运算代替除法/在8位、32位单片机上都能获得巨大的速度提升。这是单片机滤波算法优化的经典技巧。缺点限制了窗口大小的选择必须是2的幂次方。算法主体仍然是队列式。所以“速通”的结论是在绝大多数单片机应用里使用队列式实现并将窗口大小设为2的幂次方如4, 8, 16, 32然后用右移代替除法这就是兼顾了理解性、通用性和执行效率的最佳实践。除非你的窗口大小必须是3、5、7这类质数否则强烈推荐这个组合。3. 参数选择与效果调试窗口大小和数据类型是关键代码写好了直接烧进单片机可能效果不理想。问题通常出在两个地方窗口大小和数据类型。3.1 如何选择窗口大小WINDOW_SIZE这是一个滤波强度与系统响应速度的权衡。窗口越大滤波效果越强输出曲线越平滑对单次突发噪声的抑制能力越好。但缺点是“惯性”大反应迟钝。当被测物理量真实变化时滤波后的值需要更长的时间才能跟上。窗口越小系统响应快能紧跟真实变化。但滤波效果弱可能无法有效滤除噪声。实测方法先定性测试在代码里将WINDOW_SIZE定义为宏方便修改。分别用4、8、16、32等值测试。观察现象如果窗口太小输出值可能依然抖动明显。如果窗口太大当你快速改变传感器状态比如用手握住温度传感器会发现滤波后的数值变化缓慢有明显的滞后感。定量评估如果条件允许在PC上用模拟数据或录制一段真实的传感器数据通过串口打印出来在MATLAB、Python或Excel里绘制原始数据和不同窗口大小滤波后的曲线。直观对比平滑度和滞后程度。经验值参考对于变化缓慢的信号如温度、湿度窗口可以大一些16、32甚至64。对于变化较快的信号如某些速度测量、音频预处理窗口要小4、8。对于初次尝试建议从8或16开始。这是一个比较折中的起点。3.2 数据类型与溢出问题这是单片机编程里非常实际的坑。int sum; // 危险假设你的采样值是12位的ADC结果0-4095窗口大小为32。那么sum的最大可能值是4095 * 32 131,040。对于16位的int通常范围-32768~32767来说这已经溢出了计算结果会完全错误。解决方案根据可能的最大值选择数据类型。对于WINDOW_SIZE32,ADC_MAX4095的情况sum需要至少log2(131040) ≈ 17位。所以应该使用long(32位) 或uint32_t。在STM32等32位单片机上直接使用int32_t或uint32_t最省心。在51等8位单片机上使用unsigned long通常是32位但要警惕32位运算的速度开销。在代码中明确指定。#include stdint.h // 使用标准类型 #define WINDOW_SIZE 32 #define ADC_MAX 4095 typedef struct { uint16_t buffer[WINDOW_SIZE]; // 存储ADC值uint16_t足够 uint32_t sum; // 总和可能很大用uint32_t uint8_t index; // 索引用最小的uint8_t } MovingAverageFilter;平均值计算即使sum是32位除以WINDOW_SIZE后平均值通常又能用回uint16_t或int表示。注意计算顺序先除法再赋值避免中间过程溢出。调试建议在初始化后和更新计算后通过串口打印出sum的值观察它是否在数据类型的合理范围内。这是排查滤波结果异常的第一步。4. 进阶优化与特殊场景处理基础版本跑通后可以根据实际项目需求进行优化。4.1 优化一消除初始值影响前面提到滤波器启动时缓冲区里是初始值通常是0。这会导致前几个输出平均值偏低。解决方法方法A快速填充法在初始化后或者每次系统复位后用第一次采样值快速填充整个缓冲区。void MAF_InitWithValue(MovingAverageFilter *filter, int initial_value) { for(int i0; iWINDOW_SIZE; i) { filter-buffer[i] initial_value; } filter-sum initial_value * WINDOW_SIZE; filter-index 0; } // 系统上电后第一次采样到值val调用 MAF_InitWithValue(filter, val);方法B标志位法增加一个count变量记录已存入的数据量。当count WINDOW_SIZE时直接返回原始值或当前部分平均值当count WINDOW_SIZE后再进入正常的移动平均流程。typedef struct { int buffer[WINDOW_SIZE]; int sum; int index; int count; // 新增已存储的数据个数 } MovingAverageFilter; int MAF_Update_Safe(MovingAverageFilter *filter, int new_sample) { if(filter-count WINDOW_SIZE) { // 填充期 filter-buffer[filter-count] new_sample; filter-sum new_sample; filter-count; return filter-sum / filter-count; // 或直接 return new_sample; } else { // 稳定期正常移动平均逻辑 // ... (同队列式更新逻辑) return filter-sum / WINDOW_SIZE; } }4.2 优化二加权移动平均标准移动平均给窗口内每个数据的权重是一样的。有时我们希望越新的数据越重要。这就是加权移动平均。例如给最近的数据权重为4前一个为2再前一个为1。实现时需要定义一个权重数组weights[]计算加权和weighted_sum以及权重总和weight_total。#define WINDOW_SIZE 3 const int weights[WINDOW_SIZE] {1, 2, 4}; // 权重最新数据权重最大 int weight_total 124; // 权重和 // 更新逻辑类似但sum的计算变为加权和 weighted_sum - weights[filter-index] * filter-buffer[filter-index]; filter-buffer[filter-index] new_sample; weighted_sum weights[filter-index] * new_sample; // ... 更新index return weighted_sum / weight_total;加权平均对突变响应更快但计算量稍大且需要仔细设计权重。4.3 场景滑动窗口与定长队列的取舍我们的实现是“滑动窗口”每来一个新数据就踢掉一个旧数据。还有一种“定长队列平均”即攒够N个数据算一次平均然后清空队列重新攒。后者更适用于周期性上报的场景如每10秒上报一次过去10秒的平均温度而不是实时性要求高的连续控制。根据你的需求选择。5. 集成到实际项目与调试技巧最后把滤波器嵌入你的实际传感器读取代码中。5.1 典型集成流程假设你在用STM32的ADC读取电压并通过串口打印滤波后的值。// 1. 定义并初始化滤波器 MovingAverageFilter adc_filter; MAF_Init(adc_filter); // 2. 在主循环或ADC中断中 while(1) { // 启动ADC转换并等待完成具体函数依库而定 uint16_t raw_adc_value ADC_GetValue(); // 3. 使用滤波器 uint16_t filtered_value MAF_Update(adc_filter, raw_adc_value); // 4. 使用滤波后的值 printf(Raw: %d, Filtered: %d\n, raw_adc_value, filtered_value); // 或者用于控制、显示等 // Display_ShowValue(filtered_value); HAL_Delay(10); // 假设10ms采样一次 }5.2 调试与问题排查清单当滤波效果不如预期时按以下顺序排查检查原始数据永远第一步先把滤波环节注释掉直接输出原始采样值raw_adc_value。用串口工具或逻辑分析仪查看波形。如果原始信号噪声就非常大或者有规律的干扰单纯靠软件滤波效果有限需要先检查硬件电源、接地、传感器本身、信号走线。检查窗口大小如果原始数据噪声是高频毛刺增大窗口WINDOW_SIZE应该能看到平滑效果。如果没变化跳回第1步。检查数据类型溢出打印出滤波器内部的sum变量。如果sum的值出现跳变从很大突然变成很小或负数那就是溢出了。立即将sum的类型改为更大的如uint32_t。检查初始值观察系统启动后的前WINDOW_SIZE个输出。如果出现明显不合理的低值或高值就是初始值影响。采用4.1节的“快速填充法”或“标志位法”解决。验证计算正确性用一组已知的静态数据如连续输入5个100测试滤波器。输出应该稳定在100。用一组斜坡数据如1,2,3,4,5...测试观察输出是否平滑上升。评估实时性快速改变输入比如用手捏住温度传感器使其升温观察滤波后的输出跟随速度。如果滞后太严重适当减小WINDOW_SIZE。一个重要的观念移动平均滤波是软件手段用于处理轻度、随机的噪声。如果硬件电路存在严重干扰软件滤波是治标不治本。好的电路设计如RC低通滤波、屏蔽、去耦电容加上软件滤波才能获得最稳定的信号。5.3 替代与进阶如果移动平均滤波后数据仍然不理想或者滞后无法接受可以考虑其他滤波器中值滤波取窗口内数据的中位数。对脉冲噪声突然的、幅度大的尖峰有奇效且不影响边沿响应速度。实现比移动平均稍复杂需要排序。一阶低通滤波IIRoutput alpha * input (1-alpha) * last_output。计算量极小只需一次乘法和一次加法效果类似加权移动平均但参数alpha调整需要经验。卡尔曼滤波适用于线性系统且有系统模型和噪声统计特性时是最优估计。但实现复杂参数难调在简单单片机传感器应用中常属“杀鸡用牛刀”。对于大多数单片机上的传感器信号平滑一个精心调整了窗口大小的移动平均滤波或者结合了中值滤波去脉冲噪和移动平均平滑随机噪的组合滤波已经完全够用且高效。最后给个落地建议不要追求一次调好。先在PC上模拟或者用开发板接上传感器通过串口把原始数据和滤波后数据都打印出来导入到绘图软件里对比。直观的曲线比任何理论都更能帮你理解参数的影响。当你看到那条跳跃的曲线变得平滑而紧跟趋势时你就真正“速通”了单片机上的移动平均滤波。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门