ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SystemVerilog双向信号建模:从tranif1到rtranif0的传输门原语详解

SystemVerilog双向信号建模:从tranif1到rtranif0的传输门原语详解 1. 双向信号传输为什么绕不开tranif1做硬件验证和RTL设计的朋友对导线赋值、三态缓冲器应该都不陌生。可一旦碰到真正的双向信号——比如I2C的SDA、1-Wire的总线、或者自定义的调试接口——不少人第一反应就是写inout端口然后靠assign语句配合高阻z去模拟开漏或推挽行为。这套做法在RTL级仿真里确实能跑但到了需要精确建模芯片引脚行为、验证驱动冲突、或者做门级仿真时RTL级的assign就有点不够看了。SystemVerilog其实把Verilog时代的开关级原语完整保留了下来其中专门负责双向传输的tranif1、tranif0、rtranif1、rtranif0以及不带控制端的tran和rtran才是真正从晶体管级别去描述双向信号通路的原语。很多人学SystemVerilog时要么把这些原语直接跳过要么只在绿皮书《SystemVerilog验证》里看到一句“开关级原语主要用于门级建模”就翻过去了很少真正搞明白这些原语能干什么、什么时候该用、以及和RTL级写法相比到底有什么优势。这篇内容就围绕双向信号传输这条主线从tranif1到rtranif0完整过一遍这些原语各自的语义是什么、在仿真里怎么建模真实的双向引脚、驱动方向和驱动强度是怎么参与的、实际工程中怎么用才能避免踩坑。适合正在做接口级验证、需要搭双向总线测试平台的同学也适合想补全SystemVerilog语法版图的RTL工程师。先说一个最容易忽略的点tranif1这类原语建模的不是“逻辑功能”而是“物理连接”。它不是一个会参与always块调度的事件驱动逻辑而是一个持续导通的开关——控制端条件满足两个端口之间就是导线直连控制端条件不满足两个端口之间就是断路。这种语义和MOS晶体管导通/截止的行为非常接近所以用tranif1去建模芯片的真实引脚比assign语句更贴近物理实现。2. 六个传输门原语一次讲透2.1 从tran到rtranif0控制端和延迟的两种维度SystemVerilog延续了Verilog的开关级原语体系双向传输门一共有6个按两个维度划分有没有控制端、有没有延迟参数。先看不带控制端的两个tran两个端口之间永久导通就是一根理想导线。rtran也是永久导通但带延迟参数可以模拟信号通过传输门的延迟。再看带控制端的四个tranif1控制端为1时导通为0时断开。tranif0控制端为0时导通为1时断开。rtranif1同tranif1但支持延迟参数。rtranif0同tranif0但支持延迟参数。这里的r前缀不是“复位”的意思而是resistive——带电阻特性的。为什么要单独区分出带电阻的版本因为在真实芯片里传输门导通后并不是零电阻信号经过它会有衰减、有延迟。rtran系列可以给这个导通路径指定延迟时间更精确地模拟信号从一端传到另一端的耗时。注意rtran的延迟和大规模反标SDF不是一回事。SDF反标是门级网表层面的时序标注而rtran的延迟是原语自带的惯性延迟模型两者的精度粒度不同。在行为级仿真中用rtran系列做粗略时序估计、验证时序方向逻辑时够用但真要签核级精度就得靠门级网表SDF。如果做数字电路设计可能觉得开关级原语很“模拟”但实际上在验证环境里它们最常用的场景是建模芯片的双向引脚——也就是Pad。芯片的IO Pad内部就是一个传输门或三态缓冲器结构往外看是inout引脚往里看是输入通路和输出通路的汇合点。用tranif1可以非常优雅地建模这种结构。2.2 tranif1和tranif0的导通逻辑差异tranif1和tranif0的控制逻辑正好相反。一个在控制端为高时接通一个在控制端为低时接通。它们的完整真值表如下原语控制端1控制端0控制端X/Ztranif1导通断开不确定/断开tranif0断开导通不确定/断开rtranif1导通带延迟断开不确定/断开rtranif0断开导通带延迟不确定/断开注意控制端出现X或Z时的行为按仿真器的处理规则控制端为未知态时传输门的输出也变成未知态——因为物理上你不知道这个开关到底通没通。这一条在实际调试中特别容易踩坑。如果控制端信号来自一个未复位的寄存器仿真波形里双向信号很容易出现X排查起来会让人怀疑人生。tranif1和tranif0这种“一个高有效、一个低有效”的互补特性在实际芯片里对应的是CMOS传输门的互补控制——一个NMOS和一个PMOS并联需要一对互补的控制信号。所以你会发现很多模拟/混合信号模型里会同时用tranif1和tranif0来建模同一个传输门的上下半部分。2.3 rtranif1和rtranif0的延迟参数怎么给rtranif1和rtranif0的延迟参数写法和门级原语一致支持1个、2个、3个参数rtranif1 #(rise_delay, fall_delay, turn_off_delay) (io1, io2, ctrl);三个延迟分别对应信号上升沿从一端传到另一端的延迟、下降沿延迟、传输门关闭时输出变为高阻的延迟。如果不写延迟参数rtranif1的行为其实退化和tranif1几乎一样——这也是一些人用了rtranif1却感觉和tranif1没区别的原因。给延迟参数的时候要区分一个概念rtran的延迟是“信号经过传输门的时间”不是“控制端到导通的时间”。控制端的变化到传输门真正导通/断开这个开关动作本身也有延迟但在原语参数里没有独立的建模项只能靠外围逻辑去模拟。从实操角度说我一般只在两种场景用rtran系列做芯片引脚级建模时需要粗略模拟Pad的输入输出延迟。做总线竞争分析时需要让两个驱动源的信号经过不同延迟路径到达总线上制造出真实的建立时间/保持时间关系。2.4 双向传输门的驱动强度语义tranif1导通时两个端口之间是wire级连接这就涉及一个很多RTL工程师没细想的问题驱动强度怎么定在使用assign语句的三态缓冲器模型里输出端的驱动强度取决于驱动源。比如assign pad (oe) ? data : 1bz;当oe为高时pad被data强驱动当oe为低时pad是高阻。这种写法实际上只关心逻辑值不关心驱动强度。但tranif1不一样。它建模的是一个物理开关开关导通后两端直接相连——如果两端都被驱动而且是相反的值那么导线上的最终值取决于两个驱动源的驱动强度竞争而不是简单地由某一个逻辑表达式决定。这就是为什么要用tran类原语而不是assignassign是单向的赋值语义明确规定了方向tran是无方向的连接语义方向由外部驱动源决定。在SystemVerilog的仿真中驱动强度从强到弱依次是supplystrongpullweakhighz。两个strong驱动源同时驱动一根线一个为0一个为1最终线值是X并产生竞争警告。这个行为对总线协议仿真尤其重要——比如I2C总线主机和从机同时拉低SDA会产生X这在真实芯片里对应的就是总线冲突。3. 双向信号传输的完整建模实战3.1 场景设定I2C的SDA引脚建模下面用一个真实的工程场景来演示tranif1和rtranif0的用法I2C总线中的SDA引脚。I2C的SDA是典型的开漏双向信号。芯片内部有一个NMOS管输出数据时通过控制NMOS的栅极来决定是否把SDA拉低读取数据时则释放NMOS让外部上拉电阻把SDA拉高。芯片对SDA的操作本质上是可以拉低不可以强推高。用tranif1来建模这个开漏结构非常贴切。NMOS导通时SDA引脚和地之间是低阻连接NMOS截止时SDA引脚和内部读通路之间是高阻隔离。module i2c_pad ( inout wire sda_pad, output wire sda_in, input wire sda_out, input wire sda_oe ); // 输出通路sda_oe为高时sda_pad被拉低 // 用tranif1建模NMOS控制端为1时导通到地 tranif1 (sda_pad, gnd_net, sda_oe); // 但tranif1两个端口都不能是常量 // 所以需要用一个wire来承接地电平 wire gnd_net; assign gnd_net 1b0; // 输入通路sda_pad的值直接送给内部逻辑 assign sda_in sda_pad; // sda_out在这里其实没被使用 // 因为开漏结构不存在强推高的操作 endmodule这里有个细节值得注意tranif1的两个信号端口必须都是wire类型不能直接接常量1b0。我一开始在图省事的时候直接写过tranif1 (sda_pad, 1b0, sda_oe)编译直接报错。后来才意识到传输门的端口本质上是节点不是值。更规范的写法是直接用tranif1建模芯片内部“读通路”的隔离而不是建模“到地的通路”module i2c_pad_better ( inout wire sda_pad, output wire sda_in, input wire sda_out, input wire sda_oe ); wire pull_down; wire internal_node; // 内部驱动oe有效时把internal_node拉低 assign internal_node sda_oe ? 1b0 : 1bz; // 双向传输门把内部节点和外部引脚连接起来 // rtranif0建模传输门的互补控制低有效控制 rtranif0 (internal_node, sda_pad, sda_oe_inv); // 输入通路 assign sda_in sda_pad; // 生成互补控制 wire sda_oe_inv; assign sda_oe_inv ~sda_oe; endmodule3.2 一个更完整的双向IO建模实际芯片的双向IO往往比I2C的SDA更复杂。输出通路有推挽结构既能拉低也能推高输入通路要隔离整个Pad还要考虑ESD结构和上拉/下拉电阻。用tranif1和rtranif0可以搭建一个相对完整的双向IO模型module bidirectional_pad #( parameter int PULL_UP 0, parameter int PULL_DOWN 0 ) ( inout wire pad, input wire data_out, input wire oe, output wire data_in ); // 输出驱动oe有效时驱动data_out到pad // 这里不用tran直接assign即可 // 因为推挽输出本质上是单向驱动高阻控制 assign pad oe ? data_out : 1bz; // 输入通路用rtranif1精确建模传输门延迟 wire pad_filtered; rtranif1 #(0.1, 0.1, 0.2) (pad, pad_filtered, 1b1); // 输入缓冲 assign data_in pad_filtered; // 可选的内部上拉/下拉 generate if (PULL_UP) begin : pull_up_gen pullup (pad); end if (PULL_DOWN) begin : pull_down_gen pulldown (pad); end endgenerate endmodule这个例子展示了一个关键思路tran类原语不是用来代替三态驱动器的而是用来建模“物理连接通道”的。推挽输出部分用assign做逻辑级建模没问题但输入通路从Pad到内部逻辑之间那段物理传输门的延迟用rtranif1建模比用纯assign更准确。3.3 仿真测试平台验证双向传输的正确性建模了双向IO之后下一个问题自然是怎么验证它。下面是测试平台的思路module tb_bidirectional_pad; wire pad; reg data_out; reg oe; wire data_in; // 实例化被测Pad bidirectional_pad #(.PULL_UP(1)) dut ( .pad(pad), .data_out(data_out), .oe(oe), .data_in(data_in) ); // 模拟外部驱动一块外部芯片通过传输门连接 reg ext_drive_en; reg ext_drive_data; wire ext_drive ext_drive_en ? ext_drive_data : 1bz; // 用tranif1模拟外部芯片的输出级 tranif1 (pad, ext_node, ext_drive_en); wire ext_node; assign ext_node ext_drive; // 上拉电阻 pullup (pad); initial begin // 测试1内部驱动外部高阻 oe 1; data_out 1b0; ext_drive_en 0; ext_drive_data 1b0; #10; $display(Test1: pad%b data_in%b (expect pad0, data_in0), pad, data_in); // 测试2内部驱动高外部驱动低 总线竞争 oe 1; data_out 1b1; ext_drive_en 1; ext_drive_data 1b0; #10; $display(Test2: pad%b (expect padX, bus contention), pad); // 测试3内部高阻外部驱动 oe 0; data_out 1b1; ext_drive_en 1; ext_drive_data 1b1; #10; $display(Test3: pad%b data_in%b (expect pad1, data_in1), pad, data_in); // 测试4外部也高阻靠上拉 oe 0; data_out 1b0; ext_drive_en 0; #10; $display(Test4: pad%b data_in%b (expect pad1 due to pullup), pad, data_in); end endmodule测试2里故意制造了总线竞争。在oe1且data_out1b1时内部推挽试图把pad拉高同时外部驱动把pad拉低两个strong驱动源冲突最终pad会变成X。这在真实芯片里就是总线冲突有可能损坏器件。对这个场景的仿真验证能帮助在测试用例设计阶段就发现潜在的驱动方向冲突问题。4. 易错点排查与仿真器注意事项4.1 控制端为X时的隐性故障前面提过tranif1控制端如果出现X或Z输出就变成未知态。这个行为在实际调试中最容易迷惑人。比如设计里有一个使能信号来自跨时钟域逻辑没有做同步处理仿真中如果这个使能信号在某段时间处于X那么与之关联的所有双向信号都会变成X——而且波形的X看起来像是总线冲突很容易让人误判为驱动竞争。排查这类问题的思路是先看控制端波形再看传输门输出。如果控制端出现X问题根源在处理使能信号的上游逻辑上而不是双向信号本身。我在实际项目中遇到过一次SDA在仿真中途出现X排查了一下午最后发现是寄存器没有复位控制信号从X开始把所有开漏通路都拖进了未知态。4.2 tranif1和rtranif1在仿真器里的支持度差异不同仿真器对开关级原语的支持程度是有差异的。主流仿真器都能仿真tran、tranif0、tranif1因为这些原语从Verilog时代就存在。但对rtran系列个别仿真器的处理方式不一样——有些仿真器直接把rtran的延迟忽略掉退化成一个普通tran有些支持得很好。所以如果你的代码里用到了rtranif1的延迟参数并且需要在多个仿真器之间跑一致性测试建议先查每个仿真器的版本说明或者在小型测试用例里专门验证延迟行为。4.3 不要和force/release混用的原因force和release可以强制改变一个信号的值这在调试时非常常用。但force到一个传输门连接的信号上时行为是仿真器相关的。有些仿真器能把force值穿透传输门传递到另一端有些则不能。我在一个项目里想用force快速把SDA拉低来模拟从机的ACK信号结果发现在VCS里force能穿过去在Modelsim里穿不过去导致测试环境的跨平台兼容性问题。后来改成用带控制端的tranif0来做这种临时驱动行为就完全可控了。5. 从传输门到SystemVerilog验证环境的进阶用法5.1 bind语法把传输门接入验证环境《SystemVerilog绿皮书》里讲验证方法学时会反复强调“受约束随机激励”、“功能覆盖率”、“断言”这些概念。但课本里有个很少被展开的实操痛点被测设计DUT是网表或者RTL无法直接修改但你又需要监控内部双向信号。SystemVerilog的bind语法就是解决这类问题的标准姿势。你可以把带传输门的监控逻辑bind到DUT内部节点上在不改动RTL源码的情况下观测双向信号的行为。interface sda_monitor_bfm ( inout wire sda ); logic sda_capture; logic sample_clk; always_ff (posedge sample_clk) begin sda_capture sda; end // 用tranif1建模的可切换观测通路 wire obs_node; tranif1 (sda, obs_node, sample_en); logic sample_en; endinterface然后在测试平台里bind i2c_master dut_sda_monitor : sda_monitor_bfm ( .sda(dut.sda_pad) );这种用法本质上是在验证环境里做非侵入式观测——DUT只管正常工作监控逻辑通过bind挂上去不影响也不改动DUT本身的任何逻辑。如果是做芯片验证的同学这个概念应该不陌生。5.2 队列在双向总线测试中的实测应用bind之外SystemVerilog的队列也经常配合双向总线测试场景一起出现。比如验证I2C控制器需要在仿真中持续采样SDA上的数据并且按字节存起来方便后续做比对。class i2c_transaction_monitor; byte data_queue[$]; virtual task monitor_sda(virtual interface.sda_monitor_ports vif); forever begin (posedge vif.scl); // 采样SDA bit sample vif.sda; // 根据SCL组合出字节示意实际需要移位逻辑 // 这里只是展示队列的存储方式 if (sample) begin // ...字节组装逻辑... // data_queue.push_back(assembled_byte); end end endtask endclass队列在这里比定长数组好用的地方在于你不知道一次传输会有多少字节队列可以动态增长而且push_back和pop_front都是O(1)级别的操作完全满足实时采样的性能要求。5.3 绿皮书中关于信号建模的补充建议《SystemVerilog验证》这本书也就是大家常说的绿皮书在信号建模方面着墨不多但它的核心方法论——验什么比怎么验更重要——完全适用于双向信号验证。在写传输门模型之前先把验证目标列清楚你需要验证的是DUT的双向引脚时序还是仅仅是逻辑功能你需要覆盖哪些驱动方向组合你需要仿真总线竞争吗这些问题决定了你是用tranif1做精确建模还是用assign做逻辑级建模就够了。不要把简单问题复杂化——如果不需要时序细节纯RTL的assign三态写法更快更稳如果需要精确引脚行为才引入开关级原语。6. 实操心得什么时候该用哪一级的抽象做了几年验证一个越来越深的体会是抽象层次的选择往往比语法本身的掌握更决定项目的成败。对于双向信号建模我现在的取舍标准是纯功能验证用assign三态就够不需要引入开关级原语。仿真速度快调试简单。芯片引脚模型Pad Model用tranif1/rtranif1/rtranif0建模输入输出通路配合pullup/pulldown可以比较真实地模拟芯片引脚行为。混合信号验证/AMS开关级原语是连接数字仿真的数字逻辑和模拟世界的桥梁之一。虽然现在主流AMS平台有自己的建模语言比如Verilog-AMS但纯数字仿真中用开关级原语做粗略的模拟行为建模依然是最轻量级的选择。门级仿真由后端工具生成的门级网表里根本看不到tranif1——工具会用具体的标准单元比如TIEH、TIEHI、三态缓冲器来替换传输门。只有在RTL级行为建模里你才需要手工使用这些原语。最后分享一个实用小技巧如果用的是VCSdefineSVT这类开关级原语相关的编译选项可以帮你开启/关闭某些传输门优化如果用ModelSim/Questa注意vopt优化有可能会把某些开关级原语优化掉——如果你的tranif1模型仿真行为突然变得不对劲先检查是不是优化选项在作怪。双向信号传输的建模说到底就是一个用合适的抽象层次描述物理连接的问题。tranif1到rtranif0这套原语从Verilog时代到SystemVerilog时代一直保留说明它在数字验证的某些角落确实不可替代。把这一小块知识真正吃透遇到I2C、SPI、1-Wire这类开放式总线协议的验证需求时你建模引脚的底气会完全不一样。
返回列表