实测说明(2026-10-17):本文代码已在 Windows 11(构建 26200)、MATLAB 25.2.0.2998904(R2025b)、PCWIN64 中实际执行,主实验及独立边界补测均通过。下列表格和配图来自真实 MATLAB 输出;requiredFilesAndProducts 实际分析仅返回基础 MATLAB,不需要 Signal Processing Toolbox。另保留 Python 3.12.14、NumPy 2.3.5、SciPy 1.17.0 的先期参考检查,作为独立补充证据。
最短运行:将后文完整代码保存为block_fir_state_demo.m,在文件所在目录执行results = block_fir_state_demo;。真实关键结果:保留状态的分块输出与整段输出最大差为 2.22×10⁻¹⁶;每块清零时最大误差为 1.149161978。
从 FPGA 经 DMA、串口或网络收到一块数据,就调用一次filter(b,1,xBlock),很容易得到一种迷惑性的结果:块中间看起来正常,块头总有一段异常。改变块长以后,异常出现的位置也跟着变化。
先检查每次调用是否保留了滤波器状态。对同一条连续采样流,软件收包边界通常没有物理含义。如果每块都从零状态开始,算法会把每个包都当成一次新的开机采样。
1. 一条记录切成几块,应该得到同一组输出
本例固定采样率Fs=2000 Hz,记录长度N=4097,使用 33 抽头对称 FIR。系数由理想低通冲激响应乘 Hamming 形式的窗口后归一化生成,全部按公式编写,不调用fir1、hamming或sinc。
这里的 180 Hz 是构造理想低通响应时的截止参数。有限长度加窗后,实际通带纹波、过渡带和阻带衰减要另行测量;本例不把它当作经过指标设计的产品滤波器。
输入包含 41 Hz 主信号、647 Hz 分量、一次直流阶跃和一个孤立脉冲。分块长度循环取:
pattern=[176432571289];这组长度故意包含单点块、比 FIR 记忆长度更短的块,以及不能整除总记录长度的块。共得到 62 块。它比只测试“每块 1024 点”更容易暴露状态和最后一块的处理问题。
我们比较三条路径:
- 整段输入一次
filter - 分块输入,把上一块
zf传给下一块zi - 分块输入,每次清零状态,作为故意错误的对照
前两条输出应在浮点误差范围内相同。第三条在块头反复丢失历史信息。
2. zi 保存的是滤波器内部状态
FIR 的因果关系为:
y [ n ] = ∑ k = 0 L − 1 b [ k ] x [ n − k ] y[n]=\sum_{k=0}^{L-1}b[k]x[n-k]y[n]=k=0∑L−1b[k]x[n−k]
当L=33时,当前输出可能依赖前 32 个输入样本。块头没有这些历史输入,直接重新计算就等价于把它们设为零。
MATLAB 提供的接口为:
[yBlock,zf]=filter(b,1,xBlock,zi,1);zi=zf;对这里的 FIR,状态长度为L-1=32。MathWorks 文档明确说明了初始状态、最终状态和沿指定维度过滤的用法。MathWorks filter 文档
需要留意:zi是filter所用实现结构的延迟状态,不能直接用“最近 32 个原始采样值”替换。若在自己的 FPGA FIR 中保存输入移位寄存器,那是另一种状态表示;要么比较两边的输出,要么推导结构之间的状态映射。
独立补测还在 MATLAB 中手写转置直接 II 型 FIR 状态递推,逐样本核对 filter 输出,输出最大差为 4.44×10⁻¹⁶,末状态最大差为 1.11×10⁻¹⁶。先期 Python 参考模型也做过同类核对,两种环境的记录分别保存,未把 Python 输出当作 MATLAB 实测。
3. 多通道时,单点块最容易触发维度问题
约定数据始终为N×C:每行一个时刻,每列一个通道。两路信号对应两套独立状态:
zi=zeros(numel(b)-1,C);[yBlock,zi]=filter(b,1,xBlock,zi,1);最后的1明确指定沿时间维处理。特别是xBlock只有一行时,它的形状是1×C,不能再依赖“第一个非单一维度”的默认规则。否则,本应彼此独立的通道有可能被当成同一条序列的相邻样本。
同样不能为了修复维度,直接对多通道矩阵调用xBlock(:)。这种写法会改变样本与通道的组织关系。单通道行向量可以在输入接口处转成列向量,多通道数据则必须明确原始布局。
4. 16 点群延迟和 32 点历史记忆是两件事
这组系数对称,长度为 33,因此其线性相位群延迟为(33-1)/2=16个采样点,也就是 8 ms。该结论用于滤波器响应不为零、相位有定义的频段。
为了把有限记录的完整线性卷积输出取完,需要在真实流结束后继续送入L-1=32个零。16 和 32 分别对应群延迟与冲激响应尾部长度,不能互相替代。
代码先比较N个因果输出,再用保存的状态处理 32 个零:
[yTail,zEnd]=filter(b,1,zeros(L-1,1),zf,1);yFull=[yStream;yTail];yFull应与conv(x,b)的完整结果一致。conv的完整输出长度为两个输入长度之和减一。MathWorks conv 文档
连续采样过程中不要每收完一个包就冲刷 32 个零。那相当于人为往物理时间线上插入零输入。只有真正结束一条有限流、并且明确要输出尾部时,才做这一步。
5. 本次 MATLAB 实测结果
| 检查项 | 本次 MATLAB 结果 |
|---|---|
| 变长块保留状态与整段滤波最大差值 | 2.22×10⁻¹⁶ |
| 保留状态与直接卷积前 N 点最大差值 | 8.88×10⁻¹⁶ |
| 每块清零与整段滤波最大差值 | 1.149161978 |
| 分块与整段滤波的最终状态差值 | 0 |
| 最后一次零冲刷后的完整卷积差值 | 8.88×10⁻¹⁶ |
| 多通道结果最大差值 | 2.22×10⁻¹⁶ |
| 六种不同分块方式中的最大差值 | 2.22×10⁻¹⁶ |
| 独立补测:手写逐点递推与 filter 最大差值 | 4.44×10⁻¹⁶ |
| 冲激输入与 FIR 系数序列差值 | 0 |
MATLAB 独立补测验证了长度为 1、2、7、31、32、33、34 的记录,每种长度分别输入零、常量和确定性信号,共 21 组;每组分别检查整段输出、末状态、完整卷积尾部和补尾后状态。还测试了每次只输入一个样本的双通道记录,以及错误清零方法的指定反例。上述检查全部通过。
本文主程序的 9 处断言均实际执行一次。与同批联合拟合实验合计,两个主程序有 20 次成功断言执行,独立补测有 103 个通过检查;这些计数不是 103 个独立数据集,也不包括额外的两次 MAT 保存回读核验。浮点运算顺序不同,末位误差可以变化;本例采用1e-12作为该组双精度数据的数值容差,并不把它作为任意幅度、任意长度数据的通用标准。
图中灰色竖线是分块起点。第一幅可以看到错误输出在部分边界突然回到零附近;第二幅直接画误差;第三幅验证整条有限流结束后的输出尾部。它们均为本次 MATLAB 实际导出的图。
6. 完整 MATLAB 示例与运行方法
保存下面代码为block_fir_state_demo.m,在文件所在目录运行:
results=block_fir_state_demo;函数会创建matlab_results目录,执行断言并输出 PNG、MAT 文件。本次实际日志已打印BLOCK MATLAB PASS; blocks=62; taps=33; group_delay_samples=16,主程序批次进程退出码为 0。复现时可先运行version、ver和which filter -all记录环境,排查同名函数遮蔽;使用新的输出目录保存每次结果。
functionresults=block_fir_state_demo(outDir)% Base-MATLAB FIR streaming demo with independent channel states.% Compares retained states, reset states, partitions, and finite-stream tails.ifnargin<1outDir=fullfile(fileparts(mfilename('fullpath')),'matlab_results');endif~exist(outDir,'dir'),mkdir(outDir);endFs=2000;N=4097;L=33;fc=180;n=(0:N-1)';t=n/Fs;b=make_lowpass(L,fc/Fs);x=0.8*sin(2*pi*41*t+0.2)+0.2*cos(2*pi*647*t);x(n>=1550)=x(n>=1550)+0.35;x(2501)=x(2501)+1.1;pattern=[176432571289];[yWhole,zWhole]=filter(b,1,x,zeros(L-1,1),1);[yKeep,zKeep,starts]=run_blocks(x,b,pattern,true);[yReset,~]=run_blocks(x,b,pattern,false);yConv=conv(x,b(:));keepError=max(abs(yKeep-yWhole));convError=max(abs(yKeep-yConv(1:N)));resetError=max(abs(yReset-yWhole));stateError=max(abs(zKeep-zWhole));assert(keepError<1e-12,'BLOCK:Continuity','Block continuity failed.');assert(convError<1e-12,'BLOCK:Convolution','Causal convolution differs.');assert(stateError<1e-12,'BLOCK:State','Final states differ.');assert(resetError>0.1,'BLOCK:NegativeControl','Reset error not exposed.');% Flush exactly L-1 zero inputs only when the finite stream really ends.[yTail,zAfterTail]=filter(b,1,zeros(L-1,1),zKeep,1);yFull=[yKeep;yTail];tailError=max(abs(yFull-yConv));assert(tailError<1e-12,'BLOCK:Tail','Full convolution tail differs.');assert(max(abs(zAfterTail))<1e-12,'BLOCK:Flush','State did not clear.');% Every column is a separate channel. dim=1 also covers 1-by-C blocks.x2=[x,0.4*cos(2*pi*73*t)-0.15];y2Whole=filter(b,1,x2,zeros(L-1,2),1);[y2Keep,~]=run_blocks(x2,b,pattern,true);channelError=max(abs(y2Keep(:)-y2Whole(:)));assert(channelError<1e-12,'BLOCK:Channels','Channel states mixed.');% Same input, different partitions; include one sample per call.patterns={1,[235],64,1024,[313233],N};partitionError=0;fork=1:numel(patterns)yTest=run_blocks(x,b,patterns{k},true);partitionError=max(partitionError,max(abs(yTest-yWhole)));endassert(partitionError<1e-12,'BLOCK:Partitions','Partition changed output.');% An impulse at sample zero must yield the tap sequence, followed by zeros.xImpulse=[1;zeros(2*L-1,1)];yImpulse=run_blocks(xImpulse,b,[1311],true);impulseExpected=[b(:);zeros(L,1)];impulseError=max(abs(yImpulse-impulseExpected));assert(impulseError<1e-12,'BLOCK:Impulse','Impulse check failed.');fig=figure('Visible','off','Color','w','Position',[1001001050760]);subplot(3,1,1);plot(n,yWhole,'k-',n,yReset,'r-');grid on;xlim([0650]);ylabel('Output');legend('Continuous state','Reset every block');title('FIR block boundaries: same samples, different state policy');subplot(3,1,2);plot(n,yReset-yWhole,'r-');hold on;plot(n,yKeep-yWhole,'b-');grid on;xlim([0650]);ylabel('Error');legend('Reset - whole','Keep - whole');ylim([-1.41.4]);fork=1:numel(starts)ifstarts(k)-1>650,break;endplot([starts(k)-1starts(k)-1],[-1.41.4],':',...'Color',[0.70.70.7],'HandleVisibility','off');endsubplot(3,1,3);plot(0:numel(yFull)-1,yConv,'k-',0:numel(yFull)-1,yFull,'b--');xlim([N-45N+L-2]);grid on;xlabel('Zero-based sample index');ylabel('Output');legend('Full convolution','Stream + final zero flush');title('Only append the FIR tail at the true end of a finite record');print(fig,fullfile(outDir,'block_fir_state_matlab.png'),'-dpng','-r150');close(fig);results=struct('Fs',Fs,'N',N,'taps',L,'groupDelaySamples',(L-1)/2,...'blocks',numel(starts),'keepError',keepError,'convError',convError,...'resetError',resetError,'stateError',stateError,'tailError',tailError,...'channelError',channelError,'partitionError',partitionError,...'impulseError',impulseError);save(fullfile(outDir,'block_fir_state_matlab.mat'),'results','b');fprintf('BLOCK keep=%.16g; conv=%.16g; reset=%.16g; state=%.16g\n',...keepError,convError,resetError,stateError);fprintf('BLOCK tail=%.16g; channels=%.16g; partitions=%.16g; impulse=%.16g\n',...tailError,channelError,partitionError,impulseError);fprintf('BLOCK MATLAB PASS; blocks=%d; taps=%d; group_delay_samples=%g\n',...numel(starts),L,(L-1)/2);endfunctionb=make_lowpass(L,cutoffCyclesPerSample)% Windowed-sinc FIR written directly; no fir1, hamming, or sinc dependency.m=(-(L-1)/2:(L-1)/2)';h=zeros(L,1);z=(m==0);h(z)=2*cutoffCyclesPerSample;h(~z)=sin(2*pi*cutoffCyclesPerSample*m(~z))./(pi*m(~z));w=0.54-0.46*cos(2*pi*(0:L-1)'/(L-1));h=h.*w;b=(h/sum(h)).';endfunction[y,z,starts]=run_blocks(x,b,pattern,keepState)% x is N-by-C, including C=1; do not flatten a multi-channel matrix.y=zeros(size(x));z=zeros(numel(b)-1,size(x,2));starts=zeros(0,1);pos=1;k=1;whilepos<=size(x,1)count=pattern(mod(k-1,numel(pattern))+1);last=min(size(x,1),pos+count-1);if~keepState,z(:)=0;end[part,z]=filter(b,1,x(pos:last,:),z,1);y(pos:last,:)=part;starts(end+1,1)=pos;%#ok<AGROW>pos=last+1;k=k+1;endend7. 接到实际采集链路时,先固定这几条规则
- 初始化属于“流”,不要放进每次 DMA 回调。每个通道有自己的状态,状态要与采样流 ID 一起管理。
- 只有接收完成、样本顺序确定后再推进滤波状态。重传包不能重复滤波,丢包也不能假装未发生。缺样本以后,补零、插值或重新启动都是明确的信号模型选择,需要标记数据质量。
- 系数改变、通道数改变或采样流重新开始时,要定义状态如何迁移。直接沿用旧系数下的
zf未必符合预期,产品中常需要显式清零、过渡或交叉渐变。 - 初次零状态仍然代表“记录之前的输入为零”。保留状态只消除了软件分块引入的重复启动,不会恢复采集开始前未知的真实历史。
- 与 FPGA 对齐时,先对齐采样序号、16 点算法群延迟和 RTL 流水线延迟,再讨论误差。硬件系数量化、舍入、饱和与握手停顿要单独建模。
- 先用冲激、常量和几种不规则块长跑回归,再上真实传感器波形。真实波形很难让人凭目视判断每个边界都正确。
FIR 连续处理的验收标准很直接:同一条输入流,仅改变软件分块方式,输出不应发生有意义的变化。把这个不变量写成自动测试,比长期观察边界附近是否“看起来平滑”更可靠。
延伸阅读:MATLAB Hann 相干增益与补零标度对照。对时域状态连续性验收通过后,再检查后续 FFT 的窗口与幅值标度,避免把两个处理阶段的问题混在一起。
工程问题:如果你的 DMA 数据流中间丢了一个包,你会选择补零后继续保留状态,还是明确标记断流并重新初始化?可以先用本文的不规则分块测试比较两种策略在丢包点附近的输出,再按采集系统的数据质量要求决定。