MATLAB 5 点滑动平均:filter 与 conv 的输出对齐,以及 2 样本延时的含义
filter(ones(1,5)/5,1,x)和conv(x,ones(1,5)/5,'same')都能得到一条平滑曲线,长度也都与 x 相同,但同一数组下标上的结果不一定相等。另一方面,5 点对称 FIR 的群延时是 2 个样本,也不代表输出就是把输入原样向后挪两格。
这篇用冲激、阶跃和两个频率叠加的信号,分别检查输出截取、启动暂态和频率相关增益。代码只用基础 MATLAB,不调用freqz、grpdelay或filtfilt。
1. 运行方式与实测结果
把第 3 节完整代码保存为example03_average.m,在 MATLAB 中切换到该文件所在目录后执行:
example03_average终端配置好 MATLAB 命令行路径后,也可执行:
matlab -batch "example03_average"本次实际测试版本为25.2.0.2998904 (R2025b)。所有输入在函数内构造,没有外部数据。正常运行会在后台导出output03/moving_average.png,并保存数值指标到output03/average_metrics.csv。
同一输出目录重跑会覆盖示例文件。需要保留不同运行时,可以执行example03_average('output03_run2',false)。
2026 年 10 月 14 日正向 batch 退出码为 0,10 项断言通过。关键结果如下。
| 检查项 | 实测结果 |
|---|---|
| filter 与 full 卷积前 N 项的最大绝对误差 | 2.22044604925e-16 |
| N=100 时 full 卷积长度 | 104 |
| 单位阶跃前五个输出 | 0.2、0.4、0.6、0.8、1 |
| 稳态两频信号与解析式的最大绝对误差 | 4.60742555219e-15 |
| 稳态输出与单位增益纯延时的最大差异 | 0.810372220831 |
| 0.08π rad/sample 对应的有符号增益 | 0.937955936469 |
| 0.6π rad/sample 对应的有符号增益 | -0.2472135955 |
| 避开频响零点后计算的群延时范围 | 1.99999999996 至 2.00000000001 样本 |
很小的数值误差反映 double 浮点运算的舍入;0.81037 的差异则说明“只做两样本纯延时”的模型与平均滤波器并不相同。
2. 先说明时间坐标和边界
五点因果平均定义为:
y[n] = (x[n] + x[n-1] + x[n-2] + x[n-3] + x[n-4])/5。
每个输出使用当前样本和前四个样本。本文令记录前的输入为零,采用filter的默认零初始状态。这种初始化和输出同长规则见 filter R2025b 文档。
长度 N 的 x 与长度 5 的 b 做 full 卷积,共有 N+4 项。filter(b,1,x)输出前 N 项;full 卷积还保留输入记录结束后、历史数据逐步离开窗口的四项尾部。
same取的是 full 卷积的中心 N 项。对本例的五点核,相当于 full 结果的第 3 到第 N+2 项,而因果记录取第 1 到第 N 项。因此,把两者都画在原来的n=0:N-1上,会产生两样本的对齐差异,末尾处理也不相同。same的定义可查 conv R2025b 文档。
如果把居中结果放在原输入时刻上理解,它需要使用该时刻之后的两个样本;用于在线处理时,需要明确等待这些样本的时间。
3. 完整可运行代码
以下为实际测试文件。x 和 b 均为行向量,避免比较时混入行列方向问题。
functionexample03_average(outDir,negativeMode)% Causal 5-point moving average, zero initial state. Base MATLAB only.ifnargin<1,outDir=fullfile(fileparts(mfilename('fullpath')),'output03');endifnargin<2,negativeMode=false;endif~isfolder(outDir),mkdir(outDir);endfprintf('MATLAB_VERSION=%s\nNEGATIVE_MODE=%d\n',version,negativeMode);tol=1e-10;b=ones(1,5)/5;N=100;n=0:N-1;w1=0.08*pi;w2=0.6*pi;x=cos(w1*n)+0.6*cos(w2*n);y=filter(b,1,x);fullY=conv(x,b,'full');pureDelay=[00x(1:end-2)];ifnegativeMode,y=pureDelay;end% Intentionally incorrect "only a delay".convError=max(abs(y-fullY(1:N)));ck(convError<tol,'Convolution','Causal filter equals first N terms of full convolution');ck(numel(fullY)==N+4,'FullLength','Full convolution includes the four-sample tail');sameY=conv(x,b,'same');ck(max(abs(y-sameY))>0.1,'NotSame','Centered same convolution is not the causal record');impulse=filter(b,1,[1zeros(1,11)]);ck(max(abs(impulse-[bzeros(1,7)]))<tol,'Impulse','Impulse response has five taps of 0.2');step=filter(b,1,ones(1,12));ck(max(abs(step(1:5)-(1:5)/5))<tol,'Transient','Zero-state step front is a ramp');ck(max(abs(step(5:end)-1))<tol&&abs(sum(b)-1)<tol,'DC','Steady-state DC gain is one');% H(w)=exp(-j*2*w)*A(w), A real, possibly negative.gain=@(w)sin(5*w/2)./(5*sin(w/2));predicted=gain(w1)*cos(w1*(n-2))+0.6*gain(w2)*cos(w2*(n-2));steadyError=max(abs(y(5:end)-predicted(5:end)));ck(steadyError<tol,'SteadyTone','Steady sinusoids have frequency-dependent signed gain');delayError=max(abs(y(5:end)-pureDelay(5:end)));ck(delayError>0.1,'NotPureDelay','Mixed frequencies are not a unit-gain pure shift');% Compute phase derivative analytically, without freqz or grpdelay.omega=linspace(0.001,pi,2000)';E=exp(-1i*omega*(0:4));H=E*b';Hprime=-1i*E*((0:4).*b)';away=abs(H)>1e-3;gd=-imag(Hprime(away)./H(away));ck(max(abs(gd-2))<1e-9,'GroupDelay','Group delay is two away from response zeros');zeroH=exp(-1i*[2*pi/5;4*pi/5]*(0:4))*b';ck(max(abs(zeroH))<tol,'Zeros','Response zeros: group delay is undefined there');fprintf('CONV_MAX_ERROR=%.12g STEADY_MAX_ERROR=%.12g PURE_DELAY_MAX_ERROR=%.12g\n',convError,steadyError,delayError);fprintf('GAIN_LOW=%.12g GAIN_HIGH=%.12g GROUP_DELAY_RANGE=[%.12g %.12g]\n',gain(w1),gain(w2),min(gd),max(gd));fprintf('STEP_FRONT=%s\nIMPULSE=%s\n',mat2str(step(1:5),8),mat2str(impulse,8));fprintf('LIMIT: group delay only where H is nonzero; startup and frequency-dependent gains prevent generic pure shifts.\n');fig=figure('Visible','off','Color','w','Position',[1001001100760]);cleanup=onCleanup(@()close(fig));tiledlayout(2,2,'TileSpacing','compact');nexttile;stem(0:11,impulse,'filled');grid on;ylim([00.25]);title('Impulse: five coefficients of 0.2');xlabel('Sample n');ylabel('h[n]');nexttile;stairs(0:11,step,'LineWidth',1.5);grid on;ylim([01.1]);title('Zero-state step: startup ramp then unity');xlabel('Sample n');ylabel('Output');nexttile;plot(n,y,n,pureDelay,'--','LineWidth',1.1);xlim([045]);grid on;legend('5-point average','Pure delay of 2','Location','best');title('Same nominal delay does not mean same waveform');xlabel('Sample n');ylabel('Amplitude');nexttile;plot(omega/pi,abs(H),'LineWidth',1.3);grid on;xlabel('Normalized frequency (x pi rad/sample)');ylabel('|H|');title('Frequency-dependent gain; zeros at 0.4 and 0.8');exportgraphics(fig,fullfile(outDir,'moving_average.png'),'Resolution',150);metrics=table(convError,steadyError,delayError,min(gd),max(gd),...'VariableNames',{'conv_error','steady_error','pure_delay_error','gd_min','gd_max'});writetable(metrics,fullfile(outDir,'average_metrics.csv'));fprintf('RESULT=PASS example03_average\n');endfunctionck(ok,id,msg)assert(ok,['lessonMA:'id],msg);fprintf('ASSERT_PASS=%s\n',id);end4. 从冲激和阶跃读懂初始状态
左上图中,单位冲激只有第一个输入为 1,后面全部为 0。这个 1 在五个连续窗口中各贡献 1/5,因此冲激响应前五项都是 0.2,之后为 0。这也直接对应代码里的五个滤波系数。
右上图输入是从记录开始就为 1 的单位阶跃。由于记录前四个输入按零处理,第一项是(1+0+0+0+0)/5=0.2,之后依次为 0.4、0.6、0.8,到第五个输出才达到 1。
图中的横轴 n 从 0 开始,所以第五个输出位于 n=4。它与 MATLAB 数组里的step(5)是同一个点,别在读图时多算一拍。
系数和为 1,说明稳态直流增益为 1;启动阶段没有填满窗口,仍会有这段暂态。如果希望从第一项就符合另一种历史输入,需要明确初始条件,不能只把曲线开头的偏低判成算法错误。
默认movmean(x,5)也不等价于这里的零初态因果平均:它采用居中窗口,端点默认缩小窗口。movmean 的端点说明可用于核对这类差别。
5. 为什么群延时是 2,波形却没有原样平移
五点平均的频率响应可写为:
H(ω) = exp(-j*2*ω) * sin(5*ω/2)/(5*sin(ω/2))。
其中 ω 的单位是 rad/sample;ω=0 时按极限得到 H(0)=1。前面的指数项对应两样本延时,后面的实因子随频率变化,并且可能为负。
在 H 不为零的连续频段上,这个实因子的符号不会突然改变,相位对 ω 的斜率为 -2,因此群延时为 2。跨过零点会发生相位跳变,零点处输出分量被消除,群延时没有可用定义。群延时与相位延时的概念可参阅 Group Delay and Phase Delay。
本例不用数值差分相位,而是直接对有限 FIR 和求导:计算 H、H′,再由-imag(Hprime./H)得到群延时。为了避开零点附近的除法放大,检查范围限定为abs(H)>1e-3。实测范围非常接近 2,同时另有断言检查 0.4π、0.8π 两个零点。
5.1 两个频率会受到不同衰减
输入由下面两部分相加而成:
- 低频余弦:ω₁=0.08π,输入幅度为 1
- 高频余弦:ω₂=0.6π,输入幅度为 0.6
进入稳态后,低频项的有符号增益约为 0.937956,高频项约为 -0.247214。负号表示相对于共同的两样本延时形式,还存在符号反转;它不代表幅频响应为负。右下图画的是abs(H),因此始终非负。
两个分量经历不同的衰减和符号变化,叠加出来的形状自然会改变。左下图蓝线是平均滤波器输出,虚线是把原始输入仅向后移两格,两条曲线明显不同。代码从第五项开始比较稳态解析式,最大误差约为 4.61e-15;与纯延时的最大差异约为 0.81037。
因此,群延时描述的是相位随频率变化的性质。只有在相关频带的幅度响应也足够接近所需增益、并妥善处理暂态时,才能把某些输入近似理解为延后而形状基本保持。
5.2 换算成秒时要用采样率
两样本的算法群延时对应2/Fs秒。假设采样率为 1 kHz,这个换算值是 2 ms;这是单位换算举例,本文代码没有指定 1 kHz 的硬件采样率。
如果实现到 FPGA,额外寄存器流水线通常还会引入系统时钟周期延迟。采样间隔与系统时钟周期可能不同,不能把“2 个样本”直接说成“2 个 FPGA 时钟周期”。
6. 让“只是延时”的错误模型失败
执行:
example03_average('output03_negative',true)负对照把 y 故意替换为两样本纯延时,再与 full 卷积的前 N 项比较。本次实际在lessonMA:Convolution失败,batch 退出码为 1。
这个负对照验证的是错误的数学模型能否被检查抓住;它没有运行硬件,也没有证明任何固定点位宽、溢出策略或 RTL 实现正确。
7. 使用范围和参考资料
本例只验证了零初态、固定五点系数、double 浮点输入。平均会削弱快速变化,也会让边沿展开。不能只凭平滑后的曲线更顺眼,就认为有效信号全部保留了。
分块处理时需要传递滤波器状态,否则每块开头都会重新产生零初态暂态。官方filter文档给出了zi/zf用法;本文代码没有运行分块状态实验,也没有把它列入上述 10 项断言。
选题参考 Convolution in Digital Signal Processing 固定版本 README中的卷积、LTI 与平均滤波概念。完整课程使用 Signal Processing Toolbox,部分案例另需图像处理和深度学习工具箱。
该版本同时包含 BSD 3-Clause 文本的 LICENSE.md,以及带 MathWorks 产品及服务使用限制的 license.txt。本文仅作概念参考,实验代码和图片独立生成,不复制仓库素材。
需要进一步区分算法延时和硬件流水线延时,可以阅读 流水线设计 Pipeline:提升时序性能的核心技巧。
最后留一个具体问题:如果把同一段输入分成两块,第二块仍直接调用没有初始状态参数的filter,它的前四个输出为什么可能与整段处理不同?这与本篇阶跃前沿的四个过渡点有什么联系?