VHDL仿真三阶段与事件调度:延迟模型、测试基准及波形排查
简介:围绕电子设计自动化中的系统仿真,这份PPT课件面向学习VHDL与FPGA/CPLD设计流程的在校学生及初级硬件工程师,帮助他们在动手制作硬件板卡之前,借助计算机模型验证电路逻辑,从而降低硬件调试成本。内容从把仿真看作一种间接检测方法讲起,按顺序介绍VHDL源程序仿真的完整流程:文本与图形输入、编译入库、仿真输出数据与波形,并用与门、三态缓冲器等实例说明实体与结构体的具体写法;随后介绍两类激励信号产生方式,即用仿真器波形命令直接施加激励,以及用VHDL编写波形发生器、搭建顶层测试平台完成自动比对。压缩包内仅有1个ppt文件,约654KB,篇幅精炼、示例集中,便于课堂讲授或自学时对照查阅。目前已有169人学习,可作为数字逻辑与EDA课程的配套上机材料。
1. 仿真不是“跑一遍看看”:为什么 EDA 把验证切成三段而不是一段
课件里给系统仿真下的定义只有一句话:用计算机对整个硬件系统做模拟检测,却不接触具体硬件。这句话很容易被读成“省事”。但真踩过坑的人知道,仿真波形对了,不代表设计对;波形错了,一定说明你某一层的假设崩了。同一份 VHDL,要被行为仿真、综合后功能仿真、适配后时序仿真各拷问一遍,三段问的根本不是同一个问题:逻辑写对了吗、综合器把它理解成什么了、布线延时叠上去还成不成立。搞清这三段的分工,以及仿真器内核到底怎么调度事件,你才有资格判断屏幕上那条波形该不该信。
2. VHDL 仿真的流水线:编译、细化与事件调度在内核里怎么排
2.1 从文本到设计库:编译阶段固定了什么
课件给出的流程是:VHDL 文本与图形输入先翻译成文本,再经 VHDL 编译进入设计库,然后由仿真器读库执行,输出数据和波形,中间还有一层设计库管理。这条链上最容易被忽略的是“设计库”这一步,很多人把它当成一个存放文件的目录,其实它是已分析过的编译单元集合。
VHDL 的编译单元是分层的:实体(ENTITY)描述端口这层“外壳”,结构体(ARCHITECTURE)描述内部行为,此外还有包(PACKAGE)、配置(CONFIGURATION)。编译器只做语法与类型检查,把每个单元分析成中间形式放进库;库里的单元之间靠名字绑定,LIBRARY IEEE;和USE IEEE.STD_LOGIC_1164.ALL;这两行就是在告诉编译器去哪个库、取哪个包里的哪些声明。工作库通常叫 work,结构体写ARCHITECTURE one OF and1时,它和实体and1的绑定就发生在这一步。
-- 例12-1:一个最简的与门实体 LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; -- 引入 std_logic 类型与逻辑运算重载 ENTITY and1 IS PORT( aaa, bbb : IN STD_LOGIC; -- 两个输入 ccc : OUT STD_LOGIC -- 一个输出 ); END and1; ARCHITECTURE one OF and1 IS BEGIN ccc <= aaa AND bbb; -- 并发信号赋值,不是顺序执行 END;这段代码里唯一需要警惕的是最后一行。ccc <= aaa AND bbb;写在结构体里,属于并发信号赋值语句,它不在“程序从上往下跑”的语义里。细化(elaboration)阶段,仿真器会把这条语句展开成一个隐式进程,敏感表就是等号右边出现过的所有信号,这里是aaa和bbb。也就是说,只要aaa或bbb中任意一个发生变化,这个隐式进程就被唤醒一次,重新计算右边并给ccc排一次更新。你把它写成ccc <= aaa AND bbb;还是ccc <= bbb AND aaa;,仿真结果完全一样,因为并发语句之间没有先后关系。这个性质是后面理解事件调度的前提。
常见的实现思路是:编译(analysis)只产出库里的中间表示,不做任何求值;真正的“接线”发生在细化阶段。细化要做三件事——给每个信号分配初始值(STD_LOGIC的初始值是'U',不是'0')、给每个进程建好敏感表、把元件例化的层次关系建立起来。很多“波形一片 U 或 X”的问题,根子就在这一步没想清楚。
2.2 仿真内核:事件队列、信号更新与 delta 周期
仿真器不是解释器逐行跑代码,它维护的是一个事件驱动的调度循环。每个信号有两个东西:当前值,以及一个按时间排序的事务队列。仿真主循环大致是这样转的:
- 把仿真时间推进到队列里最近的一个待处理时刻;
- 取出该时刻所有到期的信号更新,把信号值真正改掉;
- 找出敏感表里包含这些信号的进程,把它们标成“可运行”;
- 依次执行这些进程,进程里的信号赋值不立即生效,而是生成新的事务,排进队列;
- 如果新事务的时间戳等于当前时间,就产生一个delta 周期,回到第 2 步再算一轮,但不推进物理时间;
- 直到当前时间没有任何待处理事务,才推进到下一个时刻。
delta 周期的存在是为了解决“零延时反馈”的语义问题。看下面这段交换赋值的写法:
-- 两个信号互换:仿真里必须借助 delta 周期才不会互相覆盖 process(clk) begin if rising_edge(clk) then a <= b; -- 读的是 b 的旧值 b <= a; -- 读的是 a 的旧值,不是刚被赋的新值 end if; end process;如果信号赋值立即生效,第二句读到的就会是第一句写进去的新值,交换变成覆盖。仿真器用“赋值只排队、周期末统一提交”的方式绕开了这个问题:同一个 delta 里所有进程读到的都是上一轮提交的旧值,写入的新值统一在周期末生效。这也解释了一个常被当成玄学的现象——把两个并发赋值语句调换顺序,输出波形纹丝不动。
这套机制还有一个副作用:如果设计里存在组合环路(输出直接或间接反馈回自己的输入,中间没有任何寄存器),仿真器会在这个 delta 里反复唤醒同一批进程,物理时间不推进,delta 计数却一路飙升。成熟一点的仿真器会在 delta 数超过阈值时直接报“零延时迭代超限”并终止,否则你看到的就是仿真卡死、进度条不动。排查这类问题的经验是:看仿真时间有没有前进,如果时间戳定住不动但 CPU 满负荷,八成是组合环,而不是机器慢。
2.3 波形只是辅助视图:数据流和文件才是可回归的资产
课件把仿真结果写成“数据和波形”,这两者地位并不对等。波形是给人眼看的,缩放、拖动、比对全靠手工,一旦设计改了一版,上一版波形就失去了参照意义。数据是给工具看的:把关键信号按时间戳打成的文本,可以直接做 diff,可以进版本库,可以在回归脚本里自动判定通过与否。
这也是为什么后面要专门讲 TEXTIO 落盘。仿真器自己生成的波形文件(各家格式不同)通常只有仿真器能读,而你用write/writeline写出来的纯文本表格,任何脚本都能处理。设计越往后走,你越会依赖“能自动比对”这件事,而不是“我再肉眼扫一遍波形”——后者在信号超过二三十根之后基本失效。
3. 延迟语义:AFTER子句、TRANSPORT与三态缓冲器到底在建模什么
3.1AFTER不是“睡一会”,而是往事务队列里插事件
初学者看到y <= 'Z' AFTER ttxz;很容易类比成软件里的 sleep,以为进程会在这里挂起一段时间。不是。这条语句执行时立刻返回,进程继续往下走,只是往y的事务队列里插入了一个“在 ttxz 之后把值改成 Z”的预约。进程本身完全不阻塞。
这个区别决定了 VHDL 描述硬件的风格:延时是信号自身的属性,不是控制流的一部分。也正因如此,同一个信号可以在不同时刻被排入多个事务,仿真器按时间顺序逐个兑现,先到的事务先生效,后到的事务如果时间戳更早则会覆盖。所有关于“毛刺”“脉冲被吞”“输出晚了一拍”的困惑,本质上都是在问:这个信号的事务队列里到底排了什么。
3.2 例 12-2 三态缓冲器:逐行拆开看
课件里的三态缓冲器例子,是理解延迟模型最好的样本。
-- 例12-2:带延迟参数的三态缓冲器 LIBRARY IEEE; USE IEEE.std_logic_1164.all; ENTITY TRIBUF_and1 IS GENERIC( ttri : TIME := 1 ns; -- 透明传输延迟 ttxz : TIME := 1 ns; -- 输出使能到高阻的延迟 ttzx : TIME := 1 ns -- 高阻到输出有效的延迟 ); PORT( in1 : IN std_logic; oe : IN std_logic; -- 输出使能,低电平为高阻 y : OUT std_logic ); END TRIBUF_and1; ARCHITECTURE behavior OF TRIBUF_and1 IS BEGIN PROCESS(in1, oe) BEGIN IF oe'EVENT THEN -- oe 刚刚发生变化 IF oe = '0' THEN y <= TRANSPORT 'Z' AFTER ttxz; ELSIF oe = '1' THEN y <= TRANSPORT in1 AFTER ttzx; END IF; ELSIF oe = '1' THEN -- oe 稳定在高电平,in1 变化 y <= TRANSPORT in1 AFTER ttri; ELSIF oe = '0' THEN y <= TRANSPORT 'Z' AFTER ttxz; END IF; END PROCESS; END behavior;GENERIC里的三个时间参数不是装饰,它们是这个模块对外暴露的“时序接口”:调用方可以在例化时把它们改成更贴近实际器件的数值,而不必改模块内部。端口上oe是低有效使能,所以oe='0'时输出'Z'(高阻),oe='1'时输出跟随in1。
进程的敏感表是(in1, oe),两个信号任一变化都会唤醒它。唤醒之后分两条路:如果这次唤醒是因为oe发生了跳变(oe'EVENT为真),那就走使能切换的时序,用ttxz或ttzx;如果oe没跳变、是in1变了,那就走透明传输路径,用ttri。三态总线的核心行为——使能打开的时候数据直通、使能关闭的时候让出总线——就靠这个分支表达。
这里有一个值得单独拎出来说的实现细节:IF oe'EVENT THEN这个判断没有限定方向,上升沿和下降沿都会进第一个分支,代码是靠在分支内部再读oe的新值来决定走哪条路的。它能跑通,是因为oe跳变后的新值恰好和你要做的动作一一对应;但它的语义是脆的,如果同一次唤醒里oe的跳变方向与你预期的动作不匹配,或者同一个 delta 内oe出现多次变化,'EVENT只反映“变过”,不反映“变了几次、朝哪个方向变”。更稳的写法是把方向写进去:IF oe'EVENT AND oe='1' THEN ... ELSIF oe'EVENT AND oe='0' THEN ...。课件这种写法在用仿真器跑一遍时看不出问题,属于典型的“能跑但不严谨”,血泪经验是:凡是没有明确写出边沿方向的'EVENT,都要在评审时被拎出来问一句。
3.3TRANSPORT与默认的INERTIAL:脉冲要不要被吞掉
延迟模型只有两种,TRANSPORT和INERTIAL,不写的时候默认是后者。
TRANSPORT的语义是“所有事务一律保留”,新事务不会取消旧事务,输出波形等于输入波形整体平移一个延迟量,任何宽度的脉冲都能传过去。INERTIAL的语义是“输出有惯性”,如果新事务在前一个事务兑现之前到达,且两者的时间差小于延迟值,那么前一个事务会被撤销——翻译成人话就是:比延迟还窄的毛刺,直接吞掉。
三态缓冲器这里必须用TRANSPORT,道理很直接:总线切换的瞬间本来就会产生真实的窄脉冲,这是硬件上客观存在的现象,你要是让仿真器把它当成“毛刺”吞掉,等于蒙上眼睛做验证。凡是描述传输线、总线、延迟线这类“输入什么样输出就什么样,只是晚一点”的行为,选TRANSPORT;凡是描述门电路这类“输入变化太快输出跟不上就不响应”的行为,用默认的INERTIAL更贴近物理。
代价也得说清楚。TRANSPORT保留全部事务,意味着信号的事务队列长度随翻转频率线性增长,一个高频时钟信号如果全程用TRANSPORT建模,仿真器的内存和调度开销都会上去。INERTIAL靠撤销旧事务省掉了这部分开销,所以它既是默认值,也是更“便宜”的选择。这个取舍在纯功能验证里几乎无感,一旦设计规模上去、仿真要跑几十毫秒的物理时间,差别就出来了。
还有一个高频误解:行为仿真里写的AFTER 1 ns,跟芯片里真实的 1 ns 完全是两回事。综合工具会忽略这些延时值,因为延时是工艺和布线决定的,不是你能在 RTL 里许愿出来的。真实延时来自适配后的时序信息,通过延迟反标(back-annotation)注入到时序仿真里。所以行为仿真里的AFTER只有两个作用:让波形看起来像回事、让依赖时序的建模逻辑(比如三态切换的先后)跑得通。把它当成“性能预估”是彻头彻尾的翻车起点。
4. 激励从哪来:force命令与 VHDL 波形发生器,两条路的成本完全不同
4.1force:即时注入,快但不可回溯
课件给出的第一种方法是“利用仿真器的波形设置命令施加激励信号”,语法形式是:
force <信号名> <值> [<时间>][, <值> <时间> ...] [-repeat <周期>]具体到例子里:
force a 0 -- 把信号 a 的当前值强制为 0 force b 0 0, 1 10 -- 时刻 0 为 0,时刻 10 变 1 force clk 0 0, 1 15 -repeat 20 -- 周期信号,周期 20,占空比 15:5 force a 10 0, 5 200, 8 400 -- 给加法器输入铺一段激励 force b 3 0, 4 100, 6 300 run 500 -- 推进仿真 500 个时间单位参数的含义要逐个咬清楚:信号名后面的第一个值是起点值,值 时间成对出现表示在指定时刻改成该值,-repeat后面的周期表示整段波形循环播放,run则是让仿真时间前进指定长度。上面这段跑起来,a在 0、200、400 三个时刻分别取 10、5、8,b在 0、100、300 取 3、4、6,正好覆盖了 4 位整数加法器的几组典型输入。
这条路的好处是快:不用改任何源码,交互式地一条条敲,边看波形边调。坏处同样明显:命令只存在于你的终端历史里,关掉会话就没了;换个人接手,他看到的是一份没有任何激励的设计;想跑回归,你得把命令录成脚本,而脚本和源码分处两个体系,版本一多必然对不上。我一般把它定位成“调试期的临时探针”,而不是验证手段。
4.2SIGGEN:把激励写成 VHDL,让它进版本库
第二种方法是“用 VHDL 写一个波形信号发生器”。课件里先给了一个被测算的加法器:
-- 例12-3:被测算的 4 位加法器 LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; ENTITY ADDER4 IS PORT( a, b : IN INTEGER RANGE 0 TO 15; c : OUT INTEGER RANGE 0 TO 15 ); END ADDER4; ARCHITECTURE one OF ADDER4 IS BEGIN c <= a + b; -- 行为级描述,交给综合器去映射成加法电路 END one;然后是激励源本身:
-- 例12-4:用并发赋值直接描述波形 ENTITY SIGGEN IS PORT( sig1 : OUT INTEGER RANGE 0 TO 15; sig2 : OUT INTEGER RANGE 0 TO 15 ); END; ARCHITECTURE Sim OF SIGGEN IS BEGIN -- 时刻 0 取 10,200ns 后取 5,400ns 后取 8 sig1 <= 10, 5 AFTER 200 ns, 8 AFTER 400 ns; -- 时刻 0 取 3,100ns 后取 4,300ns 后取 6 sig2 <= 3, 4 AFTER 100 ns, 6 AFTER 300 ns; END;一个信号赋值里塞进一串“值 + AFTER”,是 VHDL 里描述波形的紧凑写法:它等价于在 0、200ns、400ns 三个时刻各排一个事务,第一个值不带AFTER表示就在当前时刻。这段代码没有任何进程,靠的就是 2.2 节讲的并发赋值展开机制。
它比force强在哪?强在它是源码。激励波形跟着设计一起进版本库,改动有记录,可以被别人 review,可以在不同的仿真器上重放。这个实体的端口方向全是 OUT,因为它不接收任何东西,只负责往外吐波形——它是纯粹的信号源。这类“只为了仿真存在、不打算综合”的代码,业界习惯叫它非可综合代码,写的时候心里要有数,别哪天顺手把它塞进综合列表里。
4.3BENCH顶层:端口映射把两边接起来
激励源和被测算计是两个独立的实体,得有个东西把它们焊在一起,这就是顶层测试程序:
-- 例12-5:把 SIGGEN 和 ADDER4 连起来的顶层 ENTITY BENCH IS END ENTITY BENCH; -- 注意:没有端口 ARCHITECTURE one OF BENCH IS COMPONENT ADDER4 PORT( a, b : IN INTEGER RANGE 0 TO 15; c : OUT INTEGER RANGE 0 TO 15 ); END COMPONENT; COMPONENT SIGGEN PORT( sig1 : OUT INTEGER RANGE 0 TO 15; sig2 : OUT INTEGER RANGE 0 TO 15 ); END COMPONENT; SIGNAL a, b, c : INTEGER RANGE 0 TO 15; -- 内部连线 BEGIN U1 : ADDER4 PORT MAP (a, b, c); -- 位置映射 U2 : SIGGEN PORT MAP (sig1 => a, sig2 => b); -- 命名映射 END;COMPONENT声明是“我要用这个东西”的占位符,PORT MAP才是真正的连线动作。两种映射风格在这里都出现了:ADDER4用的是位置映射,实参顺序必须和形参声明顺序严格一致,少一个多一个都报错;SIGGEN用的是命名映射sig1 => a,明确写出谁接谁,可读性好,也不怕以后端口顺序被调整。规模一上去,我一律建议用命名映射,位置映射埋的雷太隐蔽——某次改端口顺序,编译照样过,波形全错。
SIGNAL a, b, c这三根内部信号是关键:它们既不是任何东西的输入输出端口,也不是变量,而是连接两个元件实例的物理连线。仿真器在细化阶段为它们分配存储、建立驱动关系,a同时被U2的sig1驱动、被U1的a端口读取,这个“一驱动多负载”的关系就靠信号来承载。
4.4 TEXTIO 落盘:把波形变成能 diff 的文本
课件例 12-7 的测试基准里,有一个容易被跳过的部分——它没有只靠波形窗口,而是把每次采样的结果写进了文件:
-- 摘自例12-7:把采样结果写成文本表格 File RESULTS: TEXT open WRITE_MODE is "results.txt"; procedure WRITE_RESULTS( CLK, CE, LOAD, RESET : STD_LOGIC; DIN, COUNT : INTEGER ) is variable V_OUT : LINE; begin write(V_OUT, now, right, 16, ps); -- 当前仿真时间,右对齐,16 字符宽,单位 ps write(V_OUT, CLK, right, 2); write(V_OUT, RESET,right, 2); write(V_OUT, CE, right, 2); write(V_OUT, LOAD, right, 2); write(V_OUT, DIR, right, 2); write(V_OUT, DIN, right, 257); write(V_OUT, COUNT,right, 257); -- 被观察的输出 writeline(RESULTS, V_OUT); -- 一行写完,落盘 end WRITE_RESULTS;write的四个参数分别是:写到哪个行缓冲区、写什么值、对齐方式(right右对齐)、字段宽度。now是仿真器的内置函数,返回当前仿真时刻,配合单位ps把它格式化成带单位的时间戳。LINE是 std.textio 里定义的行类型,相当于一个可增长的字符串缓冲区,攒够一行用writeline一次性刷到文件。
赋值顺序和字段宽度这两个细节值钱:字段宽度 2 是给单比特信号留的('0'/'1'加空格),257 是给整数留的宽字段,宽得离谱但保证了对齐。之所以要这么干,是为了让results.txt用任何文本工具打开都是一张规整的表,可以直接拿diff比对两个版本的输出,或者喂给脚本做断言。这套东西在课件年代是手工活,放到今天就是自动回归测试的雏形——你不需要肉眼看波形,只需要看两份文本有没有差异。
5. 避坑与排查:仿真波形和预期不一样时,先动哪一颗螺丝
5.1 波形一片 U 或 X,时间却在前进了
现象:仿真跑完了,波形窗口里所有输出从头到尾都是'U',或者零星几处'X',但仿真时间是正常推进的。
原因:STD_LOGIC是九值逻辑,未初始化的信号默认是'U'(未定),多驱动冲突或类型不匹配才会出现'X'(强未知)。如果某个进程压根没被唤醒过,它驱动的信号就永远停在初始值上。最常见的漏法是敏感表写漏:进程体里读了in1,敏感表只写了(oe),那么in1变化不会唤醒它,输出当然不动。
解决:先把'U'和'X'分开看——'U'是“没人驱动过”或“进程没跑过”,'X'是“多个驱动打架”或“类型转换出错”。前者查敏感表、查例化是否真的发生了、查复位有没有生效;后者查同一个信号是不是被两个进程同时驱动,或者INTEGER和STD_LOGIC_VECTOR之间有没有做显式转换。还有一个低级但高发的坑:顶层实体忘了写END ENTITY 名字;而写成了END;,某些工具会容忍,某些会静默跳过细化,症状同样是全'U'。
5.2 仿真 0 ns 就结束,波形窗口一片空白
现象:下了run 1000 ns命令,仿真器瞬间返回,时间戳还停在 0,什么都没算。
原因:仿真器的退出条件是“队列里没有待处理事务”。如果你的测试基准里既没有延时赋值、也没有wait语句、也没有显式的结束指令,那么细化完成之后队列就是空的,仿真器认为“没事可做”,直接收工。激励源忘了例化、PORT MAP连错了信号名、SIGGEN的并发赋值被注释掉了,都会造成这个结果。
解决:先在测试基准里放一条“保底”的推进语句,比如wait for 1 us;,让队列里至少有东西;然后检查顶层是不是真的例化了所有元件、映射的信号名有没有拼错(命名映射拼错通常会报错,位置映射连错则完全静默)。如果用的是“断言触发失败来终止仿真”的老写法,注意那条断言只能在某个条件下触发,条件永远不成立时仿真同样会跑到时间上限才停。
5.3 行为仿真一路绿灯,时序仿真直接挂掉
现象:RTL 级行为仿真波形干净漂亮,综合、适配之后再跑时序仿真,输出开始出现宽度只有几个 ns 的窄脉冲,下游逻辑被误触发。
原因:行为仿真里所有门都是零延时或理想延时,组合逻辑的竞争冒险根本不存在;时序仿真把布线延时反标进来之后,同一个逻辑功能的两个输入到达时间不同,输出端就会产生瞬间的毛刺。这不是仿真器算错了,是电路本来就会这样,只是行为仿真把这个问题藏起来了。
解决:不要在时序仿真里试图“消除”毛刺,那是物理事实。要么在架构上解决——跨时钟域和异步输入前面加两级寄存器打拍,用同步采样把毛刺过滤掉;要么确认下游逻辑对毛刺不敏感(比如下游是边沿触发且毛刺不落在采样窗口内)。真正的教训是:只跑行为仿真就宣布验证通过,等于把最贵的那类问题留到板子上发作。
5.4 明明加了延时,输出脉冲却凭空少了一个
现象:输入给了一串窄脉冲,仿真出来的输出里,宽度比延迟参数小的那些脉冲全不见了。
原因:默认延迟模型是INERTIAL,它的规则就是“窄于延迟宽度的脉冲被抑制”。如果你的本意是建模一条传输线或一段纯延时,却忘了写TRANSPORT,仿真器就会按门电路的惯性语义把毛刺吞掉,波形上看起来像“信号丢了”。
解决:先问自己这段逻辑到底在建模什么——传输线、总线、延迟单元用TRANSPORT,普通门电路保持默认INERTIAL。改完之后再跑一遍,如果脉冲回来了但形状仍然不对,接着查是不是多个事务的时间戳排反了(后插入的事务时间戳更早,会覆盖前面的),这类问题只能靠打印信号的事务队列或者逐段缩短仿真时间来定位。
6. 设计取舍:精度、时间与可信度之间的三角
把三段仿真摆在一起看,取舍就清楚了。行为仿真快,几分钟能跑完几十毫秒的物理时间,但它对延时是瞎的;时序仿真准,能把布线和工艺的延时代进去,代价是仿真时间随规模指数级上涨,跑一次可能要看一晚上;功能仿真夹在中间,验证的是综合器有没有把你的 RTL 理解错。三种仿真不是“哪个更好”的关系,而是各自负责排除一类特定的怀疑。
| 仿真阶段 | 输入 | 延时来源 | 能发现的问题 | 主要成本 |
|---|---|---|---|---|
| 行为仿真 | RTL 源码 | 代码里的AFTER(仅建模用) | 逻辑错误、状态机死锁、协议时序 | 极低,秒到分钟级 |
| 功能仿真 | 综合后网表 | 门级单元延时(通常为零或典型值) | 综合器误解、不可综合写法 | 中,分钟到小时级 |
| 时序仿真 | 适配后网表 | 延迟反标(布线 + 工艺) | 竞争冒险、建立/保持违例 | 高,小时级 |
延迟模型的选择是同一套逻辑的微观版本:TRANSPORT保真但贵,INERTIAL便宜但会吞脉冲;force命令上手快但不可回溯,SIGGEN加测试基准前期麻烦但能进版本库、能自动比对。所有这些选择背后是同一个问题——你愿意为“可信”付多少仿真时间。
我自己的习惯是:任何一次改动,先在行为仿真里跑通逻辑,再看综合报告有没有警告,最后只在涉及跨时钟域或异步接口的模块上跑时序仿真,不全量跑。全量时序仿真当然最保险,但它的单位时间产出太低,逼着人偷懒跳过,最后连该跑的那部分也不跑了。从那以后,我每次新建测试基准,都会强制先写三样东西:一个无端口的顶层实体、一份能落盘的 TEXTIO 输出、一条保底的wait for。这三样东西花不了十分钟,却能保证这个基准三个月后还能被别人一键重放。希望帮到你。
本文还有配套的精品资源,点击获取