欢迎光临
我们一直在努力

MATLAB ADC 码流触顶诊断:命中数、缺口分段与削顶不可辨识性

采集日志里连续出现最大码,并不等于已经证明模拟前端削顶。整数最大码可能来自正常量化、数字限幅、协议解析错误,也可能确实来自过量程。工程分析首先应该回答“观察到了什么”,再讨论“是什么造成的”。

本文把问题收敛为一套可复用的离线诊断:统计上下轨命中与邻轨命中,列出不跨缺口的同轨样本段,并把越界数据和缺失数据单独留存。输出不是“已确认削顶率”,而是观测轨码命中比例。

1. 环境与证据边界

代码仅使用基础 MATLAB 函数,不需要 Signal Processing Toolbox、Statistics and Machine Learning Toolbox 或 Fixed-Point Designer。入口建议在 MATLAB R2020a 及以后版本执行,因为出图采用 exportgraphics;这只是接口最低版本设计目标,不是已验证兼容性声明。

当前准备环境为 Linux / Python 3.12.14;已实际执行独立 Python 数学参考检查,枚举 3125 组五样本序列通过。当前没有可用的已授权 MATLAB 运行环境,所以下文数值是手算及 Python 参考核对的预期结果,不是 MATLAB 实测结果。MATLAB 版本、原生测试日志、PNG、FIG、MAT 与 CSV 均待原生运行后产生;本稿处于待验证状态,不应据此声称 MATLAB 验收通过。

2. 先约定数据含义

示例使用有符号 4 位 ADC 已解码码值,低轨 lo=-8、高轨 hi=7,邻轨宽度 margin=1 个码。实际接入 12 位二补码时应先完成符号解码,再使用 lo=-2048, hi=2047;偏移二进制不能直接套用同一零点。

输入时间 t 为严格递增的有限 double 秒数;x 为 double 码值,NaN 表示缺失,Inf 拒绝。这里只分析记录值,不承担位宽、整数性和帧校验检查;导入真实 ADC 日志时,应在调用前检查有效码是否为整数。浮点工程量可以使用相同算法,但“精确等于轨值”必须确实符合其编码约定,不应盲目加容差后仍叫精确轨码。

  • 有效记录:非 NaN 且处于 [lo,hi]。越界码独立计数,绝不先钳位,否则会把坏包伪造成触顶。
  • 精确轨码:有效且等于 lo 或 hi。
  • 邻轨命中:有效且 x<=lo+margin 或 x>=hi-margin,包含精确轨码。因此两个计数不可相加。限制 margin<(hi-lo)/2 避免两侧邻域重叠。
  • 两项比例都除以有效记录数;没有有效记录时返回 NaN,不返回误导性的 0。
  • 同轨样本段:原始记录相邻、同一条轨、相邻时间差不超过 maxGap。NaN、越界、正常中间值、换轨和过长时间缺口均打断。
  • 观测跨度:t(end)-t(start)。单个命中跨度为 0,而不是一个采样周期。它不是连续处于饱和状态的真实时长;即使时间差很短,两个采样点之间的行为仍未被观察。

maxGap 是采集链路允许的最大相邻记录间隔,由采样周期、时间戳抖动和丢包策略确定,不是信号自身的物理保持时间。边界采用 <=;有量化误差的时间戳应先统一单位和比较策略,不能事后为了连成一段随意放宽。

3. 一组能手算的日志

t = [0 1 2 3 4 8 9 10 11 12 13 14 15]';
x = [0 7 7 6 NaN 7 7 –8 –8 –7 9 –8 0]';
[summary,runs,samples] = rail_diagnostics(t,x,–8,7,1,1.5);

预期有 13 条记录,其中 1 条 NaN、1 条越界码 9、11 条有效记录。低轨命中 3 次,高轨命中 4 次,总轨码 7 次;邻轨包含码 6 和 -7,合计 9 次。轨码比例为 7/11≈0.636364,邻轨比例为 9/11≈0.818182。这些是按记录数加权的比例,不是不等间隔采样下的时间占比。

轨方向起始索引结束索引样本数首末观测跨度/s
上轨 +1 2 3 2 1
上轨 +1 6 7 2 1
下轨 -1 8 9 2 1
下轨 -1 12 12 1 0

码 9 不会进入峰值和比例统计。ObservedAbsCodePeak=8 表示有效码值相对码零点的最大绝对值,既不是已标定物理幅值,也不是被削顶前峰值。偏移二进制或带直流偏置时,必须先明确零点和标定口径。

4. 为什么不能从轨码恢复真实峰值

只考虑已知理想钳位 y=min(7,max(-8,u)):输入 [0,7,7,0] 与 [0,12,20,0] 得到同一输出 [0,7,7,0],但输入峰值分别为 7 和 20。轨码序列相同,真实峰值不同,因此仅凭该输出不存在唯一反推。

这里的结论只针对“已知钳位模型”。真实 ADC 还存在量化阈值、参考电压和前端非线性,不能把数字轨值直接当作模拟电压的精确下界。进一步确认应结合 ADC 的 overrange 标志、原理图、量程配置、参考电压测量及降低输入幅度/增益后的对照采集。

5. 完整实现与复现

把下面三个代码块分别保存为同名 .m 文件,放在同一目录。在该目录执行:

matlab -batch "run_rail_demo('native_results_01')"

也可以在 MATLAB 命令窗口运行 run_rail_demo('native_results_01')。输出目录必须不存在;复跑换一个名字,避免覆盖失败记录。入口先跑断言测试,成功后输出 summary.csv、rail_runs.csv、samples.csv、rail_demo.mat、rail_demo.png、rail_demo.fig 及 native_testlog.txt。图只画离散点,不跨缺口补线。CSV 对 NaN 的导出表现应以实际版本与导入约定核验;MAT 文件保留原始 NaN 与逻辑掩码。

只有日志出现 NATIVE_RUN_COMPLETE、命令正常退出且全部文件能读取,才完成原生验收。PASS 测试行本身不代表后续出图/保存成功。失败保留日志和已有产物,用新输出目录复测。

rail_diagnostics.m

function [summary,runs,samples] = rail_diagnostics(t,x,lo,hi,margin,maxGap)
% Discrete observed rail hits, NOT proof of analog clipping.
% t: finite strictly increasing double seconds; x: double codes, NaN missing.
% Out-of-range values are flagged, never silently clamped.
validateattributes(t,{'double'},{'vector','real','finite','nonempty'});
validateattributes(x,{'double'},{'vector','real','nonempty'});
validateattributes(lo,{'double'},{'scalar','real','finite'});
validateattributes(hi,{'double'},{'scalar','real','finite','>',lo});
validateattributes(margin,{'double'},{'scalar','real','finite','nonnegative','<',(hi–lo)/2});
validateattributes(maxGap,{'double'},{'scalar','real','finite','positive'});
t=t(:); x=x(:);
assert(numel(t)==numel(x),'rail:length','Length mismatch.');
assert(all(diff(t)>0),'rail:time','Timestamps must strictly increase.');
assert(~any(isinf(x)),'rail:inf','Inf is not a missing-value marker.');
missing=isnan(x); outside=~missing & (x<lo | x>hi);
valid=~missing & ~outside;
lower=valid & x==lo; upper=valid & x==hi;
near=valid & (x<=lo+margin | x>=hi–margin); % inclusive, includes exact rail
side=zeros(size(x)); side(lower)=–1; side(upper)=1;
% A run requires adjacent records, same exact rail, and gap <= maxGap.
starts=zeros(0,1); ends=zeros(0,1); sides=zeros(0,1);
i=1;
while i<=numel(x)
if side(i)==0, i=i+1; continue; end
j=i;
while j<numel(x) && side(j+1)==side(i) && t(j+1)–t(j)<=maxGap
j=j+1;
end
starts(end+1,1)=i; ends(end+1,1)=j; sides(end+1,1)=side(i); %#ok<AGROW>
i=j+1;
end
count=ends–starts+1; span=t(ends)–t(starts);
runs=table(sides,starts,ends,count,t(starts),t(ends),span, …
'VariableNames',{'Side','StartIndex','EndIndex','Count','Start_s','End_s','ObservedSpan_s'});
samples=table(t,x,missing,outside,valid,lower,upper,near, …
'VariableNames',{'Time_s','Code','Missing','Outside','Valid','LowerRail','UpperRail','NearRailInclusive'});
nValid=sum(valid); railCount=sum(lower|upper); nearCount=sum(near);
railFraction=NaN; nearFraction=NaN;
if nValid>0, railFraction=railCount/nValid; nearFraction=nearCount/nValid; end
peak=NaN; if nValid>0, peak=max(abs(x(valid))); end
summary=table(numel(x),sum(missing),sum(outside),nValid,sum(lower),sum(upper), …
railCount,nearCount,railFraction,nearFraction,peak, …
'VariableNames',{'Records','Missing','Outside','Valid','LowerRail','UpperRail', …
'RailCount','NearCount','RailFraction','NearFraction','ObservedAbsCodePeak'});
end

test_rail_diagnostics.m

function test_rail_diagnostics
% Hand-calculated fixtures and independent per-start maximality checks.
t=[0 1 2 3 4 8 9 10 11 12 13 14 15]';
x=[0 7 7 6 NaN 7 7 –8 –8 –7 9 –8 0]';
[s,r,p]=rail_diagnostics(t,x,–8,7,1,1.5);
assert(isequal([s.Records s.Missing s.Outside s.Valid],[13 1 1 11]));
assert(isequal([s.LowerRail s.UpperRail s.RailCount s.NearCount],[3 4 7 9]));
assert(s.RailFraction==7/11 && s.NearFraction==9/11 && s.ObservedAbsCodePeak==8);
assert(isequal(r{:,{'Side','StartIndex','EndIndex','Count','ObservedSpan_s'}}, …
[1 2 3 2 1; 1 6 7 2 1; –1 8 9 2 1; –1 12 12 1 0]));
assert(sum(p.NearRailInclusive & ~p.LowerRail & ~p.UpperRail)==2);
[~,g]=rail_diagnostics([0 1 4 5],[7 7 7 7],–8,7,0,1);
assert(isequal(g.Count,[2;2])); % equality maxGap accepted, long gap split
[~,g]=rail_diagnostics([0 1 2],[7 NaN 7],–8,7,0,3);
assert(isequal(g.Count,[1;1]));
[s,g]=rail_diagnostics([0 1],[NaN 9],–8,7,0,1);
assert(s.Valid==0 && isnan(s.RailFraction) && isnan(s.ObservedAbsCodePeak) && isempty(g));
[s,g]=rail_diagnostics(0,7,–8,7,0,1);
assert(s.RailFraction==1 && g.ObservedSpan_s==0);
[~,g]=rail_diagnostics([0 1],[–8 7],–8,7,0,1);
assert(isequal(g.Count,[1;1]));
for value=[0 NaN 9]
[~,emptyRuns]=rail_diagnostics(0,value,–8,7,0,1);
assert(height(emptyRuns)==0 && width(emptyRuns)==7);
end
mustFail(@() rail_diagnostics([0 1],0,–8,7,0,1));
mustFail(@() rail_diagnostics([1 0],[0 0],–8,7,0,1));
mustFail(@() rail_diagnostics(0,0,–8,7,0,0));
mustFail(@() rail_diagnostics([0 0],[0 1],–8,7,0,1));
mustFail(@() rail_diagnostics([0 1],[0 Inf],–8,7,0,1));
mustFail(@() rail_diagnostics(0,0,–8,7,7.5,1));
% Deterministic exhaustive 5-record sequences. Reference = enumerate all
% start/end intervals and accept only maximal constant-rail valid intervals.
alphabet=[–8 0 7 NaN 9]; t=[0 1 2 5 6]';
for k=0:5^5–1
q=k; x=zeros(5,1);
for a=1:5, x(a)=alphabet(mod(q,5)+1); q=floor(q/5); end
[s,r]=rail_diagnostics(t,x,–8,7,1,1);
valid=~isnan(x) & x>=–8 & x<=7;
assert(s.Valid==sum(valid) && s.Missing==sum(isnan(x)));
assert(s.Outside==sum(~isnan(x) & ~valid));
assert(s.LowerRail==sum(x==–8) && s.UpperRail==sum(x==7));
assert(s.NearCount==sum(valid & (x<=–7 | x>=6)));
expected=zeros(0,3);
for a=1:5
for b=a:5
rail=(x(a)==–8 || x(a)==7);
ok=rail && all(x(a:b)==x(a)) && all(diff(t(a:b))<=1);
left=a>1 && x(a–1)==x(a) && t(a)–t(a–1)<=1;
right=b<5 && x(b+1)==x(a) && t(b+1)–t(b)<=1;
if ok && ~left && ~right, expected(end+1,:)=[a b b–a+1]; end %#ok<AGROW>
end
end
assert(isequal(r{:,{'StartIndex','EndIndex','Count'}},expected));
end
fprintf('PASS: hand fixtures + 3125 exhaustive sequences (native MATLAB).\\n');
end
function mustFail(f)
caught=false; try, f(); catch, caught=true; end
assert(caught,'Expected input rejection.');
end

run_rail_demo.m

function run_rail_demo(outdir)
% Run from this directory: matlab -batch "run_rail_demo('native_results')"
if nargin==0, outdir='native_results'; end
% Never overwrite an earlier execution record.
assert(~exist(outdir,'file'),'Output path already exists; choose a new directory.');
mkdir(outdir); diary(fullfile(outdir,'native_testlog.txt'));
cleanup=onCleanup(@() diary('off')); %#ok<NASGU>
fprintf('MATLAB version: %s\\n',version); disp(ver);
fprintf('Run time: %s\\n',datestr(now,30));
try
test_rail_diagnostics;
t=[0 1 2 3 4 8 9 10 11 12 13 14 15]';
x=[0 7 7 6 NaN 7 7 –8 –8 –7 9 –8 0]';
[summary,runs,samples]=rail_diagnostics(t,x,–8,7,1,1.5);
writetable(summary,fullfile(outdir,'summary.csv'));
writetable(runs,fullfile(outdir,'rail_runs.csv'));
writetable(samples,fullfile(outdir,'samples.csv'));
save(fullfile(outdir,'rail_demo.mat'),'t','x','summary','runs','samples');
f=figure('Visible','off','Color','white');
closeFig=onCleanup(@() close(f)); %#ok<NASGU>
plot(t,x,'o','DisplayName','Observed code'); hold on;
yline(7,'–','Upper rail'); yline(–8,'–','Lower rail');
plot(t(samples.Outside),x(samples.Outside),'rx','LineWidth',2,'DisplayName','Out of range');
xlabel('Time (s)'); ylabel('Code'); grid on; legend('Location','best');
title('Discrete observations: no interpolation across missing data');
exportgraphics(gca,fullfile(outdir,'rail_demo.png'),'Resolution',160);
savefig(f,fullfile(outdir,'rail_demo.fig'));
fprintf('NATIVE_RUN_COMPLETE\\n');
catch err
fprintf('NATIVE_RUN_FAILED\\n%s\\n',getReport(err,'extended'));
rethrow(err);
end
end

6. 如何把它接进采集工程

  • 保存原始数据与采集配置,先检查字节序、二补码/偏移二进制、帧序号与时间戳单位,不要把异常值清洗成轨值。
  • 每个通道分别调用;不要把多个通道交错码流当作一条时序。跨设备时钟先完成明确的时间同步。
  • 保留 NaN 的记录位置。若日志完全丢掉一行,只有丢行后相邻时间差超过 maxGap 才会打断;不超过时无法仅凭当前输入识别,必须另外检查帧序号或缺失掩码;若前端已经插值补齐,函数无法凭补齐后的值重建缺口,需要额外保留缺失掩码并将这些点设 NaN。
  • 导出样本掩码与段表,让异常回到原始索引;同时检查有效率、越界计数,不孤立比较两个设备的轨码比例。
  • 用已知输入和量程对照验证。若用于自动报警,应另外设计滞回与持续时间状态机,不能直接把本函数的首末跨度解释为连续超限时长。
  • 测试文件包含手算例子、单样本、全无效、NaN 断段、跨缺口、换轨、时间差恰等阈值、重复时间戳及 Inf/非法 margin。穷举部分对五种码值组合的全部 5^5=3125 条序列,通过枚举所有候选区间寻找极大同轨段,和顺序扫描实现交叉比较。Python 参考也独立枚举了这些序列;这验证数学约定一致,不能替代 MATLAB 语法、表类型和图形接口的原生执行。

    7. 官方资料与延伸阅读

    以下 MathWorks 文档于 2026-10-27 核查;当前文档页面显示 R2026b,未据此冒称 R2025b 已运行。示例使用基础语法,而不是依赖新版本 table 差分能力:

    • diff:相邻元素作差,用于时间间隔检查。
    • validateattributes:输入类型及属性检查。
    • writetable:导出表格。
    • exportgraphics:导出坐标轴图像。
    • savefig:保存可重新打开的 FIG。
    • diary:记录命令窗口输出。

    这里不复用 GitHub 第三方实现。上一篇定点量化误差实验:半个 LSB 为什么挡不住削顶失真讨论误差界;FPGA ADC 码流窗口统计关注硬件窗口与码型;本文补上离线日志的缺口分段和证据边界。需要把诊断接入状态机,可继续看滞回加持续时间报警。

    你的采集链路会保留 ADC overrange 标志和丢包位置吗?如果日志只有连续最大码,没有这些辅助证据,你会用哪次增益对照采集来区分真正过量程与正常轨码量化?

    赞(0)
    未经允许不得转载:171主机测评 » MATLAB ADC 码流触顶诊断:命中数、缺口分段与削顶不可辨识性
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址