news 2026/11/5 11:59:31

超时计数器差一拍:启动、完成与到期的周期边界

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
超时计数器差一拍:启动、完成与到期的周期边界

超时计数器差一拍:启动、完成与到期的周期边界

“启动后等待 4 个时钟,没有完成就报超时”,看起来只需要一个计数器。但同样从 0 开始计数,有的实现第 4 个边沿超时,有的第 5 个边沿才超时;如果完成信号恰好在最后一拍到达,还可能同时报成功和失败。

解决这种问题,先给启动边沿一个名字。本文把接收start的上升沿称为t0,它之后的第 N 个上升沿称为tN。从 t0 到 tN 正好经过 N 个完整周期;若 tN 同拍收到完成信号,按完成处理。

之前的 FSM 文章把timer_done当作交通灯状态机的外部输入。这篇把计时模块本身展开:给出精确接口、完整 RTL、自检和两个差错对照,不再比较状态机的几种写法。

一、先画出 N 等于 4 的边沿表

假设没有完成信号,启动前模块为空闲。

图 1:按契约绘制的时序示意,输入在采样边沿前建立,输出表示边沿后的寄存器值。

下面列的是各上升沿处理后的寄存器值:

边沿本次动作已经过的完整周期elapsedbusytimed_out
t0接受start0010
t1继续等待1110
t2继续等待2210
t3继续等待3310
t4到期退出43,保留旧值01
t5保持空闲53,保留旧值00

启动边沿负责记录“从现在开始等待”,不消耗一个等待周期。内部elapsed在到期时不必继续加到 4,因为外部只需要知道操作已退出和本拍的退出原因。

测试使用 10 ns 时钟,N=4 就对应从 t0 到 t4 的 40 ns。这是仿真时间轴上的关系。工程中换成 50 MHz 时钟,同一个 N=4 对应 80 ns;模块本身只计时钟周期。

二、完成、启动和到期不能靠使用者猜顺序

端口为clk/rst/start/done,输出为busy/completed/timed_out。全部输入假定已经在同一时钟域。

边沿前状态本拍条件边沿后动作
任意状态rst=1结束当前操作,清计数与所有输出
空闲start=0保持空闲,忽略done
空闲start=1启动新操作;即使done=1也不立即完成
忙碌done=1completed=1,清busy;优先于到期
忙碌done=0且达到终点timed_out=1,清busy
忙碌done=0且未到终点继续等待

start在忙碌期间始终忽略,包括当前操作退出的那个边沿。它不会重置计数,也不会顺便启动下一项操作。如果需要重新开始,应等模块进入空闲后再给start。

这一点在边界上很重要:tN 同拍start=1、done=1,当前操作只报告完成,不接受新操作。若start一直保持高,下一次空闲上升沿会自动启动另一项操作。因此,上层最好把它当作启动脉冲使用,而不是一个长期保持高的请求电平。

这里没有独立的取消端口,也没有“忙时重启”功能。done表示外部确认完成,不能悄悄复用成另一种取消原因。同步复位可以中止当前操作,但会清除整个模块状态;若系统需要区分成功、取消和超时,应增加单独的事件定义,再补同时到达时的优先级测试。

三、为什么比较 N 减 1,却在 tN 到期

时序always_ff内的条件判断使用边沿前的寄存器值;非阻塞赋值安排的更新随后才生效。

对 N=4 而言,t3 处理完后elapsed=3。到 t4 上升沿,条件看到旧值 3,满足elapsed == N-1,因此在 t4 把timed_out置 1。这正好是四个完整周期。

若计数寄存器能够表示 N,把比较改成elapsed == N,才会出现 t4 从 3 加到 4、t5 报到期的晚一拍情况。但本文 N=4 时elapsed只有两位,根本不能表示 4:直接这样改,3 加 1 会回绕到 0,终点永远比较不到,结果是永不超时。若为了另一个计数契约改终点,也必须同步检查寄存器范围。反过来,若把启动边沿也算进已等待周期,却仍沿用原终点,同样会改变输出时刻。

所以,N与N-1没有脱离上下文的正确答案。必须连同启动装载值、第一次递增边沿和输出是否寄存一起判断。OpenTitan 的定时器说明也把 tick 产生、计数和比较器分开描述;这是阅读计时逻辑时很实用的分层方法。

另一个常见误会发生在接收端。本文timed_out在 tN 边沿处理后变高,并保持一个周期。另一个同钟always_ff若在边沿上读取它,会在 tN+1 边沿读到这个高电平。不能把模块的输出生效时刻与下一级寄存器响应时刻混为一谈。testbench 的边沿后#1只是避开仿真调度竞争,不是 RTL 里的一段硬件延迟。

四、完整 RTL

保存为rtl.sv。参数 N 必须大于等于 1;N=1 时仍给计数寄存器保留一位,避免零宽声明。正常仿真不定义MUT_宏。

`timescale 1ns/1ps // N>=1. Start at t0; timeout at tN, after N complete periods. module timeout_counter #(parameter integer N=4)( input logic clk,rst,start,done, output logic busy,completed,timed_out); localparam integer W=(N<=1)?1:$clog2(N); logic [W-1:0] elapsed; `ifdef MUT_EARLY localparam integer LIMIT=(N>1)?N-2:0; `else localparam integer LIMIT=N-1; `endif initial if(N<1) $fatal(1,"N must be >= 1"); always_ff @(posedge clk) begin if(rst) begin busy<=0; completed<=0; timed_out<=0; elapsed<=0; end else begin completed<=0; timed_out<=0; if(!busy) begin if(start) begin busy<=1; elapsed<=0; end end else begin `ifdef MUT_TIMEOUT_WINS if(elapsed==LIMIT) begin busy<=0; timed_out<=1; end else if(done) begin busy<=0; completed<=1; end `else if(done) begin busy<=0; completed<=1; end else if(elapsed==LIMIT) begin busy<=0; timed_out<=1; end `endif else elapsed<=elapsed+1'b1; end end end endmodule

completed和timed_out每个非复位边沿先安排清零,命中事件时再安排置位。这样一次操作只在退出时发出一个周期的事件,之后不会留下旧超时。

done的优先级只在忙碌分支内成立。把所有输入机械地排成rst > done > start > timeout会丢掉这个状态条件:空闲时start与done同拍,本实现会启动,忽略那个没有对应进行中操作的done。

这一版 N 是编译/展开时参数,不支持运行中改阈值;计数也不会在空闲时继续自由运行。它与 CPU 的持续时间计数器加比较寄存器属于不同接口。例如 RISC-V Machine Timer 规范定义的是比较条件触发的中断 pending,不能直接拿它的保持行为套用到本文的单周期事件输出。

五、自检从边沿期望出发

下面的 testbench 并行实例化 N=1、2、4、5,覆盖最小值、2 的幂和非 2 的幂。每次step都明确给出{busy,completed,timed_out}的期望值,不复制 DUT 内部计数器。

检查序列包括:

  • 复位与其他输入同时有效,复位必须优先
  • 空闲done忽略,空闲start+done只启动
  • 忙碌时持续给start,原计时仍在 tN 到期
  • 到期同拍done+start,只报告完成
  • 第一个等待边沿给出完成,之后不能冒出陈旧超时;N>1 时属于提前完成,N=1 时正好位于终点
  • 退出事件下一拍清零,复位能中止进行中的操作

保存为tb.sv:

`timescale 1ns/1ps module timeout_case #(parameter integer N=4)(output bit passed); logic clk=0,rst=1,start=0,done=0; wire busy,completed,timed_out; integer checks=0; always #5 clk=~clk; timeout_counter #(.N(N)) dut(.*); task automatic step(input bit reset_i,start_i,done_i, input logic [2:0] expected, input string label_i); @(negedge clk); rst=reset_i; start=start_i; done=done_i; @(posedge clk); #1; checks++; if({busy,completed,timed_out} !== expected) $fatal(1,"CHECK_FAIL TIMEOUT N=%0d %s got=%b expected=%b",N,label_i,{busy,completed,timed_out},expected); endtask initial begin passed=0; step(1,1,1,0,"reset priority"); step(0,0,1,0,"done while idle ignored"); step(0,1,1,4,"idle start wins over idle done; t0"); for(int age=1;age<N;age++) step(0,1,0,4,"busy start ignored; before tN"); step(0,1,0,1,"exactly tN timeout; busy start ignored"); step(0,0,0,0,"one-cycle timeout pulse"); step(0,1,0,4,"restart t0"); for(int age=1;age<N;age++) step(0,0,0,4,"before boundary done"); step(0,1,1,2,"done wins at tN; busy start ignored"); step(0,0,0,0,"one-cycle completed pulse"); step(0,1,0,4,"start for early done"); step(0,0,1,2,"done at first elapsed cycle"); for(int age=0;age<N+1;age++) step(0,0,0,0,"no stale timeout"); step(0,1,0,4,"start before reset"); step(1,0,0,0,"reset aborts operation"); step(0,0,0,0,"idle after reset"); $display("CASE_PASS TIMEOUT N=%0d checks=%0d",N,checks); passed=1; end endmodule module tb; wire p1,p2,p4,p5; timeout_case #(1) c1(p1); timeout_case #(2) c2(p2); timeout_case #(4) c4(p4); timeout_case #(5) c5(p5); initial begin wait(p1 && p2 && p4 && p5); $display("TEST_PASS TIMEOUT N=1,2,4,5"); $finish; end initial begin #10000; $fatal(1,"WATCHDOG TIMEOUT"); end endmodule

N=1 是特别有价值的边界:t0 启动,紧接的 t1 就到期;如果 t1 有done,则必须完成。它会把启动、到期和完成优先级压到最短序列里,减少靠波形猜测的空间。

这组测试检查的是列出的定向事件顺序,未穷举所有任意长输入历史,也没有声称覆盖全部 N。继续参数化时,可增加最大配置和更长的起停序列,但不能把未运行的用例列为通过。

六、运行命令和结果

将两份文件放在同一目录,使用配置好 Vivado 工具 PATH 的 PowerShell:

New-Item-ItemType Directory run_original|Out-NullPush-Locationrun_original xvlog-sv..\rtl.sv..\tb.svxelab work.tb-s sim-debug typical xsim sim-runallPop-Location

2026-10-13 使用 Vivado Simulator 2022.1 运行,原版编译、展开、仿真退出码均为 0,日志为:

CASE_PASS TIMEOUT N=1 checks=15 CASE_PASS TIMEOUT N=2 checks=18 CASE_PASS TIMEOUT N=4 checks=24 CASE_PASS TIMEOUT N=5 checks=27 TEST_PASS TIMEOUT N=1,2,4,5

再验证测试确实能发现问题。MUT_EARLY把 N>1 的终点提前一拍;MUT_TIMEOUT_WINS把到期判断放到完成判断之前。每次使用新的编译目录,示例如下:

New-Item-ItemType Directory run_early_bug|Out-NullPush-Locationrun_early_bug xvlog-sv-d MUT_EARLY..\rtl.sv..\tb.svxelab work.tb-s sim-debug typical xsim sim-runallPop-Location

第二次换新目录,并将宏换成MUT_TIMEOUT_WINS。实际日志分别捕获:

Fatal: CHECK_FAIL TIMEOUT N=2 busy start ignored; before tN got=001 expected=100 Fatal: CHECK_FAIL TIMEOUT N=1 done wins at tN; busy start ignored got=001 expected=010

三位输出顺序是{busy,completed,timed_out}。第一条在应该继续忙碌时提前给出超时;第二条在应该完成时给出超时。两个故障版都先成功编译、展开,再由同一 testbench 报出功能错误。

并行参数测试中的任一$fatal会终止该次故障仿真,不能把尚未完成的其他参数算作通过。本次 xsim 在这些$fatal后仍返回进程退出码 0,自动验收必须扫描CHECK_FAIL/Fatal,并要求原版出现TEST_PASS;watchdog 或编译失败也不能算成有效负对照。

七、接入状态机前最后检查四个问题

首先,谁负责产生start?如果状态机在整个等待状态里都把它置高,模块退出后的下一个边沿可能立即再次启动。

其次,done是否真的属于当前操作?本模块只有一个进行中的操作,没有事务编号;前一个操作迟到的完成脉冲若落在下一次忙碌期间,会被当成下一次操作完成。需要避免这种情况时,上层应处理迟到响应或增加身份标记。

再次,超时输出被谁读取?边沿后产生的寄存器脉冲,到下一级同步逻辑处理之间可能还有一拍,写系统时间预算时要计入。

最后,N 个周期是否等于需要的实际时长?时钟频率改变、时钟被暂停或者输入事件跨时钟域,都会改变系统观察到的时间。本文没有运行综合、时序分析或上板测试,也没有测试这些跨域与时钟管理场景。

把启动点、结束点和同拍优先级写成表,再让自检逐项对照,差一拍问题通常就能定位到一个具体边沿,而不必在比较式里反复试改常数。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 23:11:26

一次讲清五种常见BOM:工程、制造、计划、成本、服务

很多企业不是没有BOM&#xff0c;而是研发、生产、计划、财务、售后各有一套&#xff0c;真正出问题时&#xff0c;却没有一套能让所有部门都说得清。 研发按图纸定义了产品结构&#xff0c;生产拿到清单却发现缺少辅料和包装材料&#xff1b;计划根据BOM计算需求&#xff0c;结…

作者头像 李华
网站建设 2026/10/22 21:24:20

金属仓储环境无线通信技术对比——明治AKU Air星闪方案与WiFi、蓝牙、ZigBee参数差异

【对比对象】本文对比仓储场景中常见的WiFi、蓝牙、ZigBee、LoRa与星闪NearLink五类短距/长距无线技术&#xff0c;并以明治AKU Air无线组网方案作为星闪技术在仓储监测中的工程实现进行参数对照。所有数值均来源于各技术公开指标与明治官方规格书。 一、五类技术核心参数对照 …

作者头像 李华
网站建设 2026/10/23 9:28:59

LRU 缓存原理与 C++ 实践示例

文章目录LRU 缓存原理与 C 实践示例什么是 LRULRU 需要满足的操作数据结构选型C 常见使用场景完整可运行示例实际项目中的注意点开源工业级实现参考小结LRU 缓存原理与 C 实践示例 什么是 LRU LRU&#xff08;Least Recently Used&#xff09;&#xff0c;最近最少使用&#…

作者头像 李华
网站建设 2026/10/24 9:44:38

计算机组成原理第六版课后答案:版本核对与高效整理指南

简介&#xff1a;《计算机组成原理》白中英第六版课后习题答案&#xff08;PDF版&#xff09;是计算机专业核心课程的经典配套资料&#xff0c;面向本科生、考研复习者及自学者&#xff0c;用于系统巩固数据表示、运算器设计、存储层次、指令系统、CPU结构、输入输出、总线与接…

作者头像 李华
网站建设 2026/10/27 10:57:12

LangChain 上线前的安全检查:提示注入、越权和敏感数据

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录LangChain 上线…

作者头像 李华
网站建设 2026/10/23 15:47:52

第163篇 Compose 与 View 互操作:混编的边界

先把结论放在前面:Compose 与 View 的互操作不是"能不能塞进去"的问题,而是"谁持有生命周期、谁持有状态、谁负责刷新"这三个所有权问题。 官方给的答案是 AndroidView 与 ComposeView 两个入口,桥接层负责把两套世界的约定对齐——但对齐不了的部分,就…

作者头像 李华