尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

单片机信号处理:移动平均滤波原理、优化实现与工程实践

单片机信号处理:移动平均滤波原理、优化实现与工程实践 你是不是也遇到过这种情况用单片机采集传感器数据时数值总是跳来跳去像心电图一样不稳定明明温度没变ADC读回来的值却在±5个LSB之间波动电机转速恒定但测速脉冲计数却忽高忽低。这些“毛刺”和“噪声”不仅让显示界面闪烁更会直接导致控制算法误判让整个系统变得不可靠。问题的根源往往不是传感器坏了而是无处不在的环境电磁干扰、电源纹波以及传感器自身的量化误差。直接使用这些“原始数据”进行逻辑判断或控制输出无异于在沙地上盖楼。今天要讨论的移动平均滤波就是解决这个问题的第一道也是最常用、最有效的“防线”。与那些听起来高大上但实现复杂的卡尔曼滤波、IIR滤波不同移动平均滤波的核心思想极其朴素用历史数据的“集体智慧”来平滑当前数据的“个体波动”。它不涉及复杂的数学推导计算量极小特别适合51、STM32这类资源有限的单片机。但朴素不等于简单窗口长度怎么选数据类型用int还是float如何避免累积误差这些细节才是决定滤波效果和系统稳定性的关键。本文将带你彻底搞懂单片机上的移动平均滤波。我们不只讲“是什么”更聚焦“怎么用”和“为什么这么用”。你会看到从最基本的算法推导到针对单片机优化的队列式实现再到防止溢出的定点数技巧以及如何根据信号特性科学选择窗口大小。最后我们将用一个完整的、可复制的51单片机ADC采集滤波实例把理论变成你屏幕上的稳定波形。1. 移动平均滤波为什么它是单片机信号处理的“第一课”在深入代码之前我们必须先建立正确的认知移动平均滤波不是一个“高级”算法而是一个工程必需品。它的首要目标不是追求最优的数学性能而是在有限的CPU资源和内存下快速、稳定地提升信号质量。它解决了什么问题抑制随机噪声对于叠加在真实信号上的高频随机噪声如白噪声平均运算能有效将其幅度降低。噪声是随机的有正有负多次平均后倾向于相互抵消。平滑数据抖动使输出曲线变得平滑便于后续的阈值判断、显示或记录。这对于制作数码管显示、波形绘制等场景至关重要。降低误触发概率在按键检测、边缘触发等场景中未经滤波的信号可能因噪声产生多次误触发平均滤波可以提供一个稳定的判断基准。它的局限性是什么先知道坑在哪滞后性相位延迟这是移动平均滤波最核心的代价。输出是过去N个点的平均因此输出变化永远比输入真实变化“慢半拍”。窗口越大滞后越严重。这对于实时性要求高的控制系统如无人机姿态控制可能是致命的。对周期性干扰抑制有限对于工频干扰50/60Hz这种有规律的周期性噪声普通移动平均滤波效果不佳需要根据干扰周期专门设计窗口长度或使用其他滤波方法。无法区分噪声与突变如果真实信号本身突然发生大幅度阶跃滤波器会把这个突变“平滑”成一个缓慢上升的过程从而扭曲了真实信息。所以移动平均滤波的适用场景非常明确处理变化相对缓慢但对瞬时噪声敏感的信号。例如温度、湿度、压力、电池电压、低速下的转速测量等。2. 核心原理从“算术平均”到“移动窗口”我们先从最基础的算术平均说起。假设我们采集了10个数据data[0]到data[9]。它们的算术平均值是average (data[0] data[1] ... data[9]) / 10这叫做一次平均。但我们的数据是持续不断来的第11个、第12个……。最笨的方法是每次收到新数据都重新计算最近10个数的和再求平均。计算量是O(N)每次都需要进行N-1次加法、1次除法对于单片机来说当N较大时比如50在中断服务程序里这么干很可能导致中断处理超时。移动平均滤波的优化精髓在于它维护了一个“数据窗口”和一个“窗口和”。当新数据data_new到来时从“窗口和”中减去最早的那个旧数据data_old。将新数据data_new加入“窗口和”。将新数据放入窗口并淘汰最早的数据通常是循环队列。用新的“窗口和”除以窗口长度N得到当前输出。这样每次更新只需要一次加法、一次减法和一次除法计算量是O(1)与窗口大小N无关这是它能应用于实时单片机的关键。我们可以用一个表格来对比两种思路特性每次重算的笨办法移动窗口的优化法计算复杂度O(N)O(1)所需存储空间可只存数组和临时计算需存数组窗口和一个累加和变量实时性差随N增大而变差极好恒定时间完成适合场景非实时、后处理单片机实时采样3. 环境与思维准备单片机的资源约束在电脑上写算法我们可以随意使用float、动态数组。在单片机上我们必须“斤斤计较”。数据类型选择int还是float优先使用整数int16_t,int32_t。大多数8位或16位单片机没有硬件浮点单元FPU浮点运算靠软件模拟速度慢百倍。ADC采集的结果通常是整数如0-4095用整数处理天经地义。决定数据范围根据ADC位数和可能的最大累加值选择。例如12位ADC最大值4095窗口长度N10则最大窗口和约为40950仍在16位整数-32768~32767范围内吗不溢出了所以必须使用uint32_t或int32_t来存储累加和。窗口数据结构循环队列是标准答案用一个固定长度的数组buffer[N]和一个指向当前写入位置的索引index来实现。新数据覆盖最老的数据。避免使用链表动态内存分配在单片机中是大忌容易产生碎片且效率低。除法运算优化如果窗口长度N是2的幂次如2,4,8,16,32那么除法可以用右移运算来代替。average sum 3;等价于average sum / 8;速度极快。如果N不是2的幂次对于性能极其苛刻的场景可以考虑使用定点数或近似算法。4. 两种经典实现从基础版到优化版4.1 基础实现易懂但低效我们先看一个最直观但低效的实现用于理解流程// 低效的移动平均滤波示例仅用于理解不推荐实际使用 #define N 10 uint16_t buffer[N]; uint8_t count 0; uint16_t Moving_Average_Simple(uint16_t new_value) { uint32_t sum 0; uint8_t i; // 将新值存入缓冲区 if(count N) { buffer[count] new_value; count; } else { // 缓冲区已满整体前移一位效率最低的操作 for(i 0; i N-1; i) { buffer[i] buffer[i1]; } buffer[N-1] new_value; } // 计算总和 for(i 0; i count; i) { sum buffer[i]; } // 返回平均值 return (uint16_t)(sum / count); }问题分析每次插入新数据如果缓冲区满了都需要进行N-1次数据搬移for循环这是一个O(N)的操作。在高速采样的中断里这是不可接受的。4.2 优化实现循环队列法这是单片机中的标准实现务必掌握。// 高效的移动平均滤波实现循环队列 #define FILTER_WINDOW_SIZE 10 // 窗口长度建议为2的幂次以便优化 typedef struct { uint16_t buffer[FILTER_WINDOW_SIZE]; // 数据窗口 uint32_t sum; // 窗口内数据的和 uint8_t index; // 当前写入位置索引 uint8_t is_full; // 窗口是否已填满标志 } MovingAverageFilter; // 初始化滤波器 void MovingAverageFilter_Init(MovingAverageFilter* filter) { uint8_t i; for(i 0; i FILTER_WINDOW_SIZE; i) { filter-buffer[i] 0; } filter-sum 0; filter-index 0; filter-is_full 0; } // 输入新数据并返回滤波后的值 uint16_t MovingAverageFilter_Update(MovingAverageFilter* filter, uint16_t new_value) { uint16_t old_value; // 如果窗口已满需要从总和中减去即将被覆盖的旧值 if(filter-is_full) { old_value filter-buffer[filter-index]; filter-sum filter-sum - old_value new_value; } else { // 窗口未满直接累加 filter-sum new_value; } // 将新值存入窗口 filter-buffer[filter-index] new_value; // 更新索引实现循环 filter-index; if(filter-index FILTER_WINDOW_SIZE) { filter-index 0; filter-is_full 1; // 索引回到0说明窗口第一次被填满 } // 计算并返回平均值 if(filter-is_full) { // 窗口已满除数固定为窗口大小 // 如果 FILTER_WINDOW_SIZE 是2的幂次这里可以改为return (uint16_t)(filter-sum LOG2(FILTER_WINDOW_SIZE)); return (uint16_t)(filter-sum / FILTER_WINDOW_SIZE); } else { // 窗口未满除数为当前有效数据个数 return (uint16_t)(filter-sum / filter-index); } }代码精讲使用结构体封装将滤波器状态数组、和、索引、标志位打包方便管理多个滤波通道。循环队列index指针在到达数组末尾后回到0逻辑上形成一个环避免了数据搬移。is_full标志位这是关键技巧。它用来区分窗口“填充阶段”和“稳定运行阶段”。在填充阶段平均值分母是递增的index填满后分母固定为N计算更规整。O(1)更新无论窗口多大每次Update只做一次减法、一次加法、一次赋值、两次判断和一次除法效率极高。5. 项目实战51单片机ADC采集与滤波显示让我们结合一个具体的场景用STC89C52或类似51单片机采集电位器电压通过ADC0804或单片机自带的ADC经移动平均滤波后将稳定的电压值显示在LCD1602上。5.1 硬件与工程概览MCU: STC89C52RCADC: ADC0804并行接口或 STC单片机内置的ADC需具体型号支持显示: LCD1602信号源: 电位器分压模拟传感器电压变化开发环境: Keil C515.2 核心代码集成我们假设你已经完成了ADC驱动和LCD1602驱动的底层函数。uint16_t Read_ADC_Value(void): 读取一次ADC原始值0-255或0-4095。LCD_ShowNum(uint8_t x, uint8_t y, uint16_t num, uint8_t len): 在LCD指定位置显示数字。主程序滤波流程#include reg52.h #include intrins.h // ... 其他头文件和驱动函数声明 ... #define ADC_FILTER_WINDOW_SIZE 8 // 使用8方便用移位代替除法 MovingAverageFilter adc_filter; void main() { uint16_t raw_adc, filtered_adc; // 初始化 Init_ADC(); Init_LCD1602(); MovingAverageFilter_Init(adc_filter); // 初始化滤波器结构体 LCD_ShowString(0, 0, Raw:); LCD_ShowString(0, 1, Filtered:); while(1) { // 1. 读取ADC原始值 raw_adc Read_ADC_Value(); // 2. 使用移动平均滤波器处理 filtered_adc MovingAverageFilter_Update(adc_filter, raw_adc); // 3. 显示结果可以转换为电压值显示此处简化为显示数字 LCD_ShowNum(5, 0, raw_adc, 4); LCD_ShowNum(10, 1, filtered_adc, 4); // 4. 延时控制采样率。例如每100ms采样一次。 Delay_ms(100); } }5.3 优化使用移位代替除法由于我们选择了窗口大小N82的3次方可以将除法优化为右移3位。修改MovingAverageFilter_Update函数中的返回部分// 计算并返回平均值 - 优化版仅当N为2的幂次时可用 if(filter-is_full) { // 窗口已满使用右移代替除法。8 2^3所以右移3位。 return (uint16_t)(filter-sum 3); } else { // 窗口未满无法简单移位仍需除法。但此阶段很短。 return (uint16_t)(filter-sum / filter-index); }注意filter-sum必须是uint32_t类型确保右移前有足够的位数防止溢出信息丢失。6. 效果验证与调试技巧如何知道你的滤波器是否在工作光看LCD上数字变“稳定”了还不够。1. 串口绘图验证强烈推荐将原始数据raw_adc和滤波后数据filtered_adc通过串口发送到电脑使用串口绘图工具如SerialPlot、匿名上位机、VOFA查看波形。预期现象原始信号波形充满高频毛刺滤波后的信号是一条紧贴原始信号趋势但平滑得多的曲线。调整窗口大小N你会直观地看到N越大曲线越平滑但滞后也越明显。如果信号本身变化快滞后会导致滤波后的曲线“跟不上”。2. 注入测试信号用程序模拟一个“阶跃信号”加“随机噪声”来测试滤波器性能。// 在循环中模拟一个带噪声的阶跃信号 static uint16_t simulate_true_value 1000; if(some_condition) { // 例如按键按下 simulate_true_value 2000; // 信号发生阶跃 } // 在真实值上叠加一个随机噪声-20 ~ 20 raw_adc simulate_true_value (rand() % 41) - 20; // 然后将raw_adc送入滤波器...观察滤波器输出是否能平滑地过渡到新的稳定值以及过渡过程的时间滞后性。7. 常见问题与深度优化指南7.1 问题排查表问题现象可能原因排查思路解决方案滤波后输出为0或恒定值1. 累加和sum溢出后归零。2. 窗口长度N为0或除数错误。3. 数据全部为0。1. 检查sum的数据类型是否足够大uint32_t。2. 单步调试查看is_full标志和index是否正确更新。3. 检查ADC读取函数是否正常。1. 使用更大类型uint64_t或减小N。2. 仔细检查循环索引和满标志逻辑。3. 先验证原始数据是否正确。滤波效果不明显仍有抖动1. 窗口长度N太小。2. 噪声幅度过大超出了平均的抑制能力。3. 噪声不是白噪声而是周期性干扰。1. 增大N值观察效果。2. 通过串口绘图观察噪声频率和幅度。3. 观察波形看是否有规律波动。1. 适当增加N。2. 结合硬件滤波如RC低通。3. 考虑使用中值滤波先去除脉冲再用移动平均。输出响应严重滞后感觉“很肉”窗口长度N太大。测量系统阶跃响应时间与采样周期*N进行对比。减小N值。必须在“平滑度”和“实时性”之间权衡。系统运行一段时间后异常1. 内存越界index超出数组范围。2. 在中断和非中断中同时调用滤波器导致数据错乱。1. 检查数组大小和index自增逻辑。2. 检查是否在中断和主循环都调用了Update。1. 确保index在0到N-1之间循环。2. 避免共享数据或使用临界区保护。7.2 高级优化技巧1. 加权移动平均普通移动平均对所有历史数据一视同仁。有时我们希望“越新的数据越重要”。这就是加权移动平均公式为output (w0*new w1*data[-1] ... w(N-1)*data[-(N-1)]) / sum(wi)其中w0 w1 ... w(N-1)。这能在平滑噪声和快速跟踪信号之间取得更好平衡但计算量稍大。2. 一阶滞后滤波低通滤波这是一种特殊的“窗口大小为无穷大”的加权平均但计算极其简单output (1-alpha) * output alpha * new_input其中alpha是一个介于0和1之间的系数通常很小如0.1。它不需要存储历史数组内存占用极小但滞后性随alpha减小而增大。它是移动平均的一种近似和变体。3. 移动平均 中值滤波 组合对于含有突发性尖峰脉冲噪声的信号如开关抖动先使用中值滤波取窗口内数据的中位数去除脉冲再使用移动平均平滑随机噪声效果极佳。8. 工程实践与选择建议如何科学选择窗口大小N经验法则N应远大于噪声的主要周期但小于你关心的信号变化周期。通常从4、8、16、32开始尝试。采样定理的考虑如果你的采样频率是Fs那么移动平均滤波大致相当于一个截止频率为Fc ≈ Fs / N的低通滤波器。你可以根据你想保留的信号最高频率来反推N。实时性约束在控制系统中滞后时间T_lag ≈ N * T_samT_sam为采样周期。确保这个滞后时间在你的系统允许范围内。单片机资源评估表资源类型基础移动平均滤波 (N10)注意事项RAM数组: 10 * sizeof(data_type)变量: 1个sum, 1个index, 1个flag对于51单片机128B RAM如果数据是uint16_t数组就占20B需谨慎。STM32则通常无压力。ROM/Flash约50-100字节代码空间非常小。CPU时间每次更新约10-20个指令周期整数运算在MHz级别的单片机中几乎可忽略不计。非常适合在定时器中断中调用。最后的建议移动平均滤波是你单片机信号处理工具箱中的“瑞士军刀”——简单、可靠、无处不在。掌握它的优化实现和理解其滞后性的本质比你盲目追求更复杂的滤波算法更重要。在大多数单片机应用场景中一个正确实现和参数调优的移动平均滤波足以解决80%的信号噪声问题。下次当你面对跳动的传感器数据时不要只想着换更贵的传感器或抱怨电路设计。首先请熟练地敲出那个循环队列的滤波结构体它可能就是你项目从“不稳定”走向“可靠”的关键一步。
返回列表