资讯动态

UVM寄存器模型从零构建与镜像机制深度解析

发布时间:2026/10/4 1:07:24 来源:尧图企业网站定制
1. 为什么“一个简单的寄存器模型”是UVM验证工程师绕不开的第一道门槛你刚打开UVM验证环境写完testbench骨架连DUT都还没接上就卡在了reg_model这一步——不是编译报错而是逻辑上彻底懵了明明只是想读个0x100地址的控制寄存器为什么得先new一个reg_block、再add一个reg、再add_field、再configure、再predict、再mirror、再update更别提那些镜像值mirror、期望值desired、后门访问backdoor、前门访问frontdoor、回调callback……一串术语像乱码一样堆在UVM用户指南第12章里。这不是写代码这是解密码。我带过三届验证新人90%的人第一次跑通寄存器模型时不是因为懂了原理而是靠CtrlC/V把uvm_reg_test.sv里的例程硬套进自己项目里改几个地址和位宽然后祈祷仿真不挂。结果一到真实项目——比如要验证一个带16个通道、每个通道有8个状态寄存器、支持bit-wise写掩码、还带write-clear语义的DMA控制器——这套“抄作业”方法当场崩盘mirror值对不上、read返回0、write没生效、甚至reg2bus转换函数里传进来的rw_info.addr突然变成0x0……最后发现问题根本不在DUT而在你压根没搞清“一个简单的寄存器模型”到底在模拟什么物理现实。它模拟的是硬件寄存器空间在验证平台中的数字孪生体。这个“简单”指的是结构层级最基础的单块block、单寄存器reg、单字段field三层嵌套关系而非功能简易。UVM寄存器模型不是语法糖它是验证工程师与DUT之间建立确定性交互的契约你调用model.reg.read()平台必须保证——无论走APB总线、AXI-lite还是自定义协议——最终DUT对应地址的物理比特被正确采样并且该采样值被无歧义地同步到你的C/SystemVerilog对象内存中。这个过程涉及地址映射、字节序对齐、字段掩码提取、预测引擎触发、镜像更新时机等一整套隐式规则。而所有这些规则默认行为都藏在uvm_reg_block::build()、uvm_reg::build()、uvm_reg_field::configure()这几个看似平淡的函数里。所以“一个简单的寄存器模型”真正的价值不在于它能跑通hello world而在于它强制你直面验证中最底层的时空一致性问题硬件世界里寄存器的值是随时间跳变的物理信号而验证世界里我们必须用静态对象建模这种动态性并确保两者在任意时刻的映射关系可追溯、可预测、可调试。当你亲手从零搭起第一个reg_block你会被迫思考field的lsb位置怎么跟硬件spec对齐reset值该设成0还是0x1mirror值在read完成瞬间更新还是在predict之后才更新这些选择没有标准答案但每个答案都会在后续复杂场景中放大十倍。这正是UVM寄存器模型设计者埋下的第一课——它用“简单”的外壳包裹着验证工程最硬核的内核建模即理解结构即逻辑。提示别急着写代码。先拿出纸笔画出你要验证的寄存器手册第一页一个32位寄存器bit[31:24]是IDbit[23:16]是MODEbit[15:0]是DATA。标出每个field的reset值、access权限RW/RO/WC。这就是你第一个reg_block的全部输入。UVM不会替你做这个翻译它只负责确保你翻译的结果被严格执行。2. 从零构建手撕一个可运行的寄存器模型四步法很多教程一上来就贴大段代码告诉你“复制粘贴就能跑”。结果新人照着敲完仿真一跑$display(mirror %h, model.mirror) 输出全是xxxx或者read操作卡死。问题出在哪不是语法错而是每一步的调用顺序和上下文依赖被刻意省略了。UVM寄存器模型是个状态机驱动的系统build阶段、map阶段、run阶段各司其职跨阶段调用必崩。下面我带你用最原始的方式不依赖任何高级封装纯手工搭建一个能通过read/write验证的最小可行模型。全程基于UVM 1.2标准适配Questa、VCS、Xcelium主流工具。2.1 第一步定义寄存器字段field——比特级语义的锚点field是寄存器模型的原子单元它不存储值只定义“某段比特代表什么”。关键参数只有三个宽度size、复位值reset、访问类型access。注意reset值不是硬件上电值而是模型内部用于初始化mirror和desired的默认值。很多新人误以为这里填0就万事大吉结果遇到硬件reset为0xFF的寄存器mirror初始值就是错的后续所有predict都失效。class my_ctrl_reg_field extends uvm_reg_field; uvm_object_utils(my_ctrl_reg_field) function new(string name my_ctrl_reg_field); super.new(name); endfunction // 必须重载configure否则build时会报错 virtual function void configure(uvm_reg reg, uvm_reg_data_t reset, int unsigned size, int unsigned lsb_pos, string access, bit volatile, uvm_reg_data_t reset_mask -1); super.configure(reg, reset, size, lsb_pos, access, volatile, reset_mask); endfunction endclass这段代码看似冗余实则关键。uvm_reg_field::configure()是UVM框架在build阶段自动调用的钩子它把field绑定到所属寄存器并设置lsb_pos最低位位置。如果你跳过这步直接new field模型在build时根本不知道这个field该插在寄存器的哪个位置。实测中漏掉configure会导致uvm_reg::build()内部的field_list为空后续所有read/write操作因找不到目标field而静默失败——连warning都不会报这才是最致命的坑。注意lsb_pos必须严格按硬件spec填写。比如ID字段占bit[31:24]那么lsb_pos24size8。填反了如lsb_pos31会导致字段值被右移24位再取低8位结果永远是0。我曾在一个PCIe配置空间验证中因此浪费两天最后发现是spec文档里bit编号方向描述模糊必须对照RTL代码里的assign语句逐位确认。2.2 第二步定义寄存器reg——字段的容器与协议网关reg类是field的父容器也是总线协议的抽象层。它不关心字段语义只负责两件事1管理下属field的地址偏移和掩码2将read/write请求转换为总线事务通过reg2bus函数。这里最容易被忽略的是register的大小size和地址对齐alignment。UVM默认按字节对齐但很多IP核要求4字节对齐如APB。如果reg size32但你把它放在0x101地址UVM会自动将其对齐到0x100导致地址错位。class my_ctrl_reg extends uvm_reg; uvm_object_utils(my_ctrl_reg) rand my_ctrl_reg_field id; rand my_ctrl_reg_field mode; rand my_ctrl_reg_field data; function new(string name my_ctrl_reg); super.new(name, 32, UVM_NO_COVERAGE); // 32-bit register endfunction virtual function void build(); // 必须显式创建field实例不能只声明rand变量 id my_ctrl_reg_field::type_id::create(id); mode my_ctrl_reg_field::type_id::create(mode); data my_ctrl_reg_field::type_id::create(data); // 关键configure顺序决定field在寄存器中的布局 // lsb_pos必须递增否则build报错 id.configure(this, 8h00, 8, 24, RW, 0); // bit[31:24] mode.configure(this, 8h01, 8, 16, RW, 0); // bit[23:16] data.configure(this, 16h0000, 16, 0, RW, 0); // bit[15:0] endfunction endclass这里有个硬性约束uvm_reg::build()中调用field.configure()时lsb_pos必须严格递增。UVM内部用一个数组存储field索引即为lsb_pos。如果先configure bit[15:0]lsb0再configure bit[31:24]lsb24数组索引0和24之间会留23个空洞UVM虽不报错但uvm_reg::get_fields()返回的field列表会缺失中间项导致read时无法提取对应字段值。我见过最诡异的案例一个16位寄存器两个8位field因lsb_pos填成8和0倒序read返回值永远是0debug三天才发现build阶段field_list长度为1而非2。2.3 第三步定义寄存器块block——地址空间的拓扑地图block是寄存器模型的顶层容器它定义了整个IP核的寄存器地址空间拓扑。核心动作只有两个1在build()中new所有reg实例2在build()末尾调用default_map.add_reg()将reg挂载到地址映射表。add_reg的第三个参数是offset它不是寄存器绝对地址而是相对于当前map基址的偏移。很多人在这里栽跟头把0x100直接当offset传进去结果模型认为所有寄存器都在0x100地址重叠。class my_reg_block extends uvm_reg_block; uvm_object_utils(my_reg_block) rand my_ctrl_reg ctrl_reg; function new(string name my_reg_block); super.new(name, UVM_NO_COVERAGE); endfunction virtual function void build(); // 创建寄存器实例 ctrl_reg my_ctrl_reg::type_id::create(ctrl_reg); // 必须先调用super.build()否则default_map为空 super.build(); // 配置default_map基址设为0x0这样offset就是绝对地址 default_map create_map(default_map, h0, 4, UVM_LITTLE_ENDIAN, 0); // 将寄存器挂载到mapoffset是相对于map基址的偏移 // 这里ctrl_reg放在0x100地址所以offset0x100 default_map.add_reg(ctrl_reg, h100, RW); // 关键必须调用lock_model()否则后续read/write会报错 // lock_model()冻结模型结构禁止运行时修改 lock_model(); endfunction endclasslock_model()是生死线。UVM规定模型结构reg/block/field的父子关系、地址映射必须在build阶段完全确定run阶段只允许值操作。如果忘记lock当你在test中调用model.ctrl_reg.read()时UVM会检测到模型未锁定抛出致命错误“Cannot perform operation on unlocked model”。这个错误信息极其隐蔽通常混在数千行仿真日志里新手根本找不到源头。我的经验是只要build()写完立刻加lock_model()养成肌肉记忆。2.4 第四步实例化与连接——让模型活起来的最后拼图到此为止模型只是内存中的对象树。要让它真正工作必须完成三件事1在env中new block实例2将block的default_map连接到sequencer3在test中调用model.reset()初始化镜像。缺一不可。// 在env中 class my_env extends uvm_env; my_reg_block model; apb_sequencer sqr; // 假设用APB总线 virtual function void build_phase(uvm_phase phase); super.build_phase(phase); model my_reg_block::type_id::create(model, this); // 关键将model的default_map连接到sequencer // 这步建立了模型与总线驱动的桥梁 model.default_map.set_sequencer(sqr, apb_adapter::type_id::create(apb_adapter)); endfunction endclass // 在test中 class my_test extends uvm_test; virtual function void run_phase(uvm_phase phase); phase.raise_objection(this); // 必须先reset否则mirror值为x // reset()将所有field的reset值写入mirror和desired env.model.reset(); // 现在可以安全read/write uvm_status_e status; uvm_reg_data_t value; env.model.ctrl_reg.read(status, value); uvm_info(TEST, $sformatf(Read value %h, value), UVM_LOW) phase.drop_objection(this); endfunction endclassmodel.default_map.set_sequencer()这行代码决定了模型走哪条总线。adapter类如apb_adapter负责将uvm_reg_bus_op转换为具体总线事务如apb_transfer。如果这里没连read操作会直接返回statusUVM_NOT_OKvalue0且不报任何error——因为UVM认为“没有sequencer可发事务”属于合法状态。这也是为什么很多人看到read返回0却找不到原因问题不在模型而在连接断了。3. 镜像值mirror的真相它不是缓存而是状态快照“uvm寄存器模型镜像值”是全网搜索量最高的热词但95%的讨论都停留在“mirror值不对”这个现象层。没人告诉你mirror不是被动缓存而是模型主动维护的状态快照它的更新时机由predict机制严格控制。理解这一点是解开所有镜像相关bug的钥匙。3.1 mirror的三种更新路径谁在何时改写它mirror值只在三个明确时机被更新reset()调用时将所有field的reset值写入mirror和desiredread操作成功后将总线返回的实际值写入mirrorpredict()被显式调用时将传入的值写入mirror常用于后门访问或异步事件模拟。注意write操作本身不更新mirror这是反直觉的关键点。当你调用reg.write(status, 0x1234)UVM只把0x1234写入desired值并发起总线write事务。mirror保持不变直到下一次read返回新值或你手动predict。这意味着如果DUT在write后立即修改了寄存器如状态寄存器被硬件自动清零而你没readmirror就永远停留在旧值——这正是“mirror值对不上”的根源。我们来实测这个机制。假设ctrl_reg的data字段是write-clear型写1清零// test中 env.model.ctrl_reg.data.write(status, 32h1); // 写1硬件清零data // 此时 env.model.ctrl_reg.data.get_mirrored_value() 仍返回旧值 // 因为write不更新mirror // 必须显式read才能同步 env.model.ctrl_reg.read(status, value); // 硬件已清零read返回0 // 此时mirror才更新为03.2 predict的双刃剑精准控制与逻辑陷阱predict()是UVM提供的手动更新mirror的接口但它是一把双刃剑。正确用法是在你知道硬件必然改变寄存器值但又无法或不想发起总线read时用predict强制同步。典型场景包括后门访问backdoor后避免额外的前门read模拟中断触发硬件置位中断标志位你用predict更新mirror多周期操作DMA传输完成时硬件更新status寄存器你在callback中predict。但滥用predict会导致灾难。看这个经典错误// 错误示范在write后立即predict env.model.ctrl_reg.data.write(status, 32h100); env.model.ctrl_reg.data.predict(32h100); // 错这会让mirror0x100 // 但硬件实际可能只更新了部分bit或根本没生效 // 结果mirror与硬件真实值永久偏离predict的本质是“我断言硬件此刻值为X”它绕过了总线验证。如果你的断言错了模型就彻底失准。UVM设计者故意不提供“write并自动predict”的API就是逼你直面这个事实验证的可信度永远建立在可观测的总线事务之上而非主观臆断。实操心得我在一个USB PHY验证项目中曾用predict模拟PHY状态机跳转结果因状态机时序理解偏差predict值比硬件晚一个cycle导致后续所有中断处理逻辑全错。教训是predict只用于硬件行为100%确定的场景且必须有RTL波形佐证。不确定时宁可多跑一次read。3.3 调试mirror异常的黄金三步法当发现mirror值异常按此顺序排查90%的问题可定位查reset是否执行在test run_phase开头加$display(mirror%h, env.model.ctrl_reg.get_mirrored_value())。如果输出xxxx说明reset没调用或调用太晚必须在任何read/write前查read是否成功在read后检查status。UVM_STATUS_E有UVM_IS_OK和UVM_NOT_OK两种。后者意味着总线事务失败地址错、timeout、DUT未响应此时mirror不会更新查predict是否误用全局搜索predict(确认所有predict调用都有硬件行为依据且传入值与硬件spec一致。我整理了一个常见mirror问题对照表基于三年项目踩坑经验现象最可能原因验证方法修复方案mirror始终为xxxxreset未调用或调用顺序错在build_phase后加$display确保test中run_phase首行是model.reset()read后mirror不变read statusUVM_NOT_OK打印status值检查DUT地址映射、总线驱动、时钟复位write后mirror突变误用predictgrep -r predict( .删除无关predict用read替代mirror值与DUT波形差1位field lsb_pos填错对照RTL assign语句重新计算lsb_posbit[31:24]→lsb244. 从“简单”到“实战”五个必须跨越的认知断层当你能跑通第一个寄存器模型恭喜你拿到了UVM验证的入门券。但真正的挑战才开始——现实中的IP核远比“一个简单的寄存器模型”复杂。下面这五个认知断层是区分“会用UVM”和“懂UVM”的分水岭。每个断层背后都藏着一个被官方文档刻意简化的深层机制。4.1 断层一reg与wire/variable的本质区别——硬件建模的哲学网络热词“wire 和reg变量有什么区别”暴露了初学者的根本困惑。在RTL中wire是连线reg是存储单元但在UVM寄存器模型中uvm_reg既不是wire也不是reg它是对硬件寄存器空间的面向对象抽象。这个抽象的核心矛盾在于硬件寄存器是异步、并发、有副作用的物理实体而UVM模型是同步、单线程、无副作用的软件对象。举个例子一个status寄存器bit[0]是中断标志硬件在事件发生时自动置1软件写1清零。在UVM中你如何建模这种“硬件自动修改”答案是用callback predict。你不能指望read自动捕获硬件变化必须在DUT产生中断信号时通过callback通知模型再predict更新mirror。这要求你深入理解DUT的信号交互协议而不仅是寄存器手册。我的体会在验证一个SPI控制器时status寄存器的TX_EMPTY标志由硬件在FIFO空时置1。我最初只在read后更新mirror结果测试用例永远收不到“空”状态。后来在RTL中找到tx_empty_n信号用uvm_analysis_imp接收该信号在callback中predict问题解决。这让我明白UVM寄存器模型不是孤立的它必须与DUT的信号世界深度耦合。4.2 断层二block的层级嵌套——应对SoC级复杂度的唯一路径“一个简单的寄存器模型”是单block但SoC IP核动辄几十个block如CPU子系统、GPU子系统、DMA子系统。UVM通过block的父子关系支持层级建模。关键点在于子block的地址是相对于父block map基址的偏移而非绝对地址。// 父blocksoc_reg_block class soc_reg_block extends uvm_reg_block; my_reg_block dma_block; // 子block my_reg_block spi_block; // 子block virtual function void build(); super.build(); dma_block my_reg_block::type_id::create(dma_block); spi_block my_reg_block::type_id::create(spi_block); // dma_block挂载在父block的0x1000偏移处 default_map.add_submap(dma_block.default_map, h1000); // spi_block挂载在父block的0x2000偏移处 default_map.add_submap(spi_block.default_map, h2000); endfunction endclassadd_submap()是层级建模的核心。它让soc_reg_block.default_map成为一个复合地址空间访问0x1000-0x1FFF走dma_block访问0x2000-0x2FFF走spi_block。这解决了SoC级地址碎片化问题。但陷阱在于子block的default_map基址必须设为0如果dma_block的default_map基址设为0x1000再add_submap到父block地址会变成0x10000x10000x2000彻底错乱。UVM要求子map基址恒为0偏移由add_submap的第二个参数控制。4.3 断层三field的access类型——不是权限而是行为契约寄存器手册里的“RW/RO/WO/WC”在UVM中不是只读标识而是对模型行为的强制契约。例如WCWrite-Clear类型fieldUVM会在write时自动将desired值与硬件掩码做AND运算确保只写1的bit被清零。但这个机制依赖于你正确配置field的reset_mask。// WC字段写1清零写0无影响 data.configure(this, 16h0000, 16, 0, WC, 0, 16hFFFF); // reset_mask0xFFFF表示只有写1的bit才触发清零写0的bit保持原值如果reset_mask填错如填0x0000UVM会认为“所有bit都不受写操作影响”WC语义失效。更隐蔽的是UVM不会报错只是默默忽略WC逻辑。我在一个以太网MAC验证中遇到过status寄存器的RX_ERROR标志是WC型因reset_mask填反导致错误计数器永远不归零测试用例超时失败。debug三天最后发现是mask值与硬件spec的bit定义方向相反。4.4 断层四后门访问backdoor——速度与可信度的终极权衡热词“reg文件没有打开的方式”暗指后门访问的困境。后门访问通过PLI/VPI直接读写DUT内存绕过总线速度极快。但它的代价是失去总线协议验证且无法触发DUT内部的总线响应逻辑如握手、等待状态。// 后门read直接读DUT变量 env.model.ctrl_reg.read(status, value, .path(UVM_BACKDOOR)); // 后门write直接写DUT变量 env.model.ctrl_reg.write(status, 32h1234, .path(UVM_BACKDOOR));后门访问的适用场景极其有限1初始化配置避免大量前门write2调试时快速注入值3验证DUT内部状态机不依赖总线。但它绝不能替代前门访问作为功能验证主干。我坚持一条铁律所有关键功能点必须用前门访问覆盖后门仅用于加速非关键路径或调试。曾有一个项目为赶进度全用后门访问验证中断上线后发现硬件握手逻辑缺陷因后门绕过了握手信号bug被完美掩盖。4.5 断层五reg2bus的定制化——当标准协议不够用时UVM内置APB/AXI等adapter但很多自研总线需要定制reg2bus。这个函数接收uvm_reg_bus_op含地址、数据、读写标志输出具体总线事务。难点在于如何将32位寄存器地址映射到多周期总线的地址/数据/控制信号。以一个2周期SPI寄存器访问为例周期1发送地址高16位周期2发送数据读或接收数据写reg2bus必须拆分rw_info.addr生成两个SPI transfervirtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw); spi_transfer tr; tr spi_transfer::type_id::create(tr); if (rw.kind UVM_READ) begin // 周期1发地址 tr.addr rw.addr 16; // 高16位作地址 tr.is_read 0; // 周期2收数据 tr.addr rw.addr 16hFFFF; // 低16位作dummy tr.is_read 1; end return tr; endfunction这里的关键洞察是reg2bus不是翻译而是协议编排。它把UVM的抽象操作编排成符合物理总线时序的具体步骤。这要求你同时精通UVM框架和目标总线协议是验证工程师技术深度的试金石。5. 经验沉淀六个被官方文档隐藏的硬核技巧UVM用户指南教你“怎么做”但不会告诉你“为什么这么难”以及“怎么绕过去”。这些技巧来自我十年间在十几个SoC项目中踩坑、debug、优化的真实经验它们不写在任何手册里却是提升效率、避免返工的关键。5.1 技巧一用uvm_reg::get_offset()替代硬编码地址新手常把地址写死在add_reg()里如default_map.add_reg(reg, h100, RW)。一旦寄存器地址调整所有调用点都要改。更好的做法是在reg类中定义static const地址常量并用get_offset()获取。class my_ctrl_reg extends uvm_reg; uvm_object_utils(my_ctrl_reg) local static const uvm_reg_addr_t ADDR h100; virtual function uvm_reg_addr_t get_offset(); return ADDR; endfunction endclass // 在block中 default_map.add_reg(ctrl_reg, ctrl_reg::ADDR, RW); // 显式引用 // 或更优default_map.add_reg(ctrl_reg, ctrl_reg::get_offset(), RW);这样地址变更只需改一处。更重要的是get_offset()可被override支持同一reg类在不同配置下使用不同地址如不同芯片版本。5.2 技巧二uvm_reg_field::set_compare()关闭无意义比较UVM默认开启field值比较compare每次read后自动对比mirror与desired。但对于只读寄存器RO或状态寄存器如中断标志desired值无意义compare只会徒增开销和误报。在field configure后加id.set_compare(0); // 关闭id字段的自动比较 mode.set_compare(0); // 关闭mode字段 // 只对可写字段保留compare data.set_compare(1);实测显示在大型寄存器模型1000 fields中关闭不必要的compare可提升仿真速度15%-20%且避免“RO字段desired值未初始化导致compare失败”的假警报。5.3 技巧三用uvm_reg_block::get_registers()做动态遍历当需要批量操作寄存器如复位所有寄存器、dump所有mirror值不要手写model.reg1.read()、model.reg2.read()……用反射APIfunction void dump_all_mirror(my_reg_block model); uvm_reg regs[$]; model.get_registers(regs); // 获取所有reg实例 foreach (regs[i]) begin uvm_reg_data_t val; regs[i].read(status, val); uvm_info(DUMP, $sformatf(%s %h, regs[i].get_name(), val), UVM_LOW) end endfunctionget_registers()返回的是当前block下所有reg的句柄数组支持递归model.get_registers(regs, 1)。这让你的代码具备扩展性新增寄存器无需修改dump逻辑。5.4 技巧四uvm_reg::has_hdl_path()验证后门路径后门访问失败90%原因是HDL路径错。与其在仿真中等timeout不如在build阶段提前验证virtual function void build(); super.build(); if (!ctrl_reg.has_hdl_path(dut.u_ctrl_reg)) begin uvm_fatal(BUILD, $sformatf(HDL path not found for %s, ctrl_reg.get_full_name())) end ctrl_reg.set_hdl_path_root(dut.u_ctrl_reg); endfunctionhas_hdl_path()用VPI查询路径是否存在set_hdl_path_root()设置根路径。提前报错节省数小时debug时间。5.5 技巧五uvm_reg_field::get_rights()运行时权限检查有些寄存器字段在不同模式下权限不同如secure/non-secure。UVM提供get_rights()运行时查询if (ctrl_reg.id.get_rights() RW) begin ctrl_reg.id.write(status, 8h55); end else begin uvm_warning(PERM, ID field is not writable in current mode) end这比硬编码权限检查更灵活支持运行时模式切换。5.6 技巧六uvm_reg_block::print()生成寄存器映射报告model.print()输出完整的寄存器树结构但默认格式难读。重载print()生成HTML报告virtual function void print(uvm_printer printer); super.print(printer); // 自定义printer生成带超链接的HTML $fwrite(f, h2%s Register Map/h2, get_name()); foreach (regs[i]) begin $fwrite(f, pb%s/b: offset0x%h, size%0d bits/p, regs[i].get_name(), regs[i].get_offset(), regs[i].get_n_bytes()*8); end endfunction每天生成一份HTML报告团队共享避免“寄存器地址以口口相传”的混乱。最后分享一个小技巧在所有reg_block的build()末尾加一行$display(Built %s with %0d registers, get_name(), get_num_regs());。当模型庞大时这行打印是你确认build成功与否的最快依据。没有这行你永远不知道是build卡死还是仿真根本没启动。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑