资讯动态

Verilog常见问题排查与设计优化:从语法陷阱到跨时钟域处理

发布时间:2026/8/12 21:42:27 来源:尧图企业网站定制
1. 项目概述为什么Verilog问题总在重复干了十几年数字电路设计从ASIC到FPGAVerilog这门语言就像我的老伙计熟得不能再熟。但带新人、看论坛、审代码我发现一个挺有意思的现象大家踩的坑翻来覆去就那么几个。新手在语法和仿真里打转老手在时序和架构上较劲。问题本身不复杂但一旦卡住查资料、问同事半天时间就没了。所以我决定把这些年遇到、解决、甚至“制造”过的Verilog常见问题系统地梳理一遍。这不是一本语法手册网上那种资料太多了。我想做的是结合真实的工程场景告诉你那些语法书里不会写的“潜规则”仿真器报错背后到底想说什么以及怎么从一堆看似正确的代码里把那个导致功能异常或时序违例的“幽灵”给揪出来。无论你是刚接触Verilog的学生还是已经能独立完成模块设计的工程师希望这份汇总能成为你手边一份实用的“排错指南”和“避坑备忘录”。2. 语法与编码风格从“能跑”到“可靠”写Verilog代码第一步是让工具综合器、仿真器能读懂并正确执行但更高的目标是写出清晰、健壮、易于维护的代码。很多初级问题都源于对语法细节和良好编码习惯的忽视。2.1 阻塞赋值与非阻塞赋值永恒的“坑王”这绝对是Verilog新手的第一道坎也是很多诡异仿真结果的根源。规则很简单在always块中用进行阻塞赋值用进行非阻塞赋值。但为什么阻塞赋值是“立即生效”。在同一个always块中它像C语言一样顺序执行。上一条赋值语句的结果会立刻影响下一条语句的运算。// 错误示例试图用阻塞赋值实现寄存器时序逻辑 always (posedge clk) begin a b; // 假设b是1 c a; // 这里c拿到的是a被赋值后的新值即1。这实际上是一个“连线”行为而非寄存器打拍。 end上面这个例子综合出来可能根本不是三个寄存器而是一堆组合逻辑完全违背了设计意图。非阻塞赋值是“并行生效”。在时钟沿到来时所有右侧表达式的值被同时计算并暂存在always块结束后才统一更新到左侧寄存器中。// 正确示例寄存器时序逻辑的标准写法 always (posedge clk or negedge rst_n) begin if (!rst_n) begin a 1‘b0; b 1’b0; end else begin a d_in; // d_in的值被“预定”给a b a; // 这里a取的是上一个时钟周期的旧值这是实现移位寄存器的关键。 end end关键理解b a;中的a是上一个时钟周期结束后a寄存器的值而不是本周期刚被“预定”的新值。这才正确描述了寄存器间的时序关系。实操心得我给自己和团队立了一条铁律在描述时序逻辑的always (posedge clk)块中一律使用非阻塞赋值在描述组合逻辑的always (*)块中一律使用阻塞赋值。严格遵守这条规则可以避免95%因赋值方式混淆导致的仿真与综合不一致问题。2.2 变量与网表reg与wire的误解很多人以为reg型变量就一定会被综合成寄存器wire型就一定是连线。这是错误的。reg和wire是Verilog语言中的数据类型用于声明变量的行为模式而非直接对应硬件。reg表示这个变量可以在always或initial块中被赋值。它既可以保存在always (posedge clk)中被非阻塞赋值从而综合成寄存器也可以在always (*)中被阻塞赋值综合成组合逻辑。wire表示这个变量需要被连续驱动通常由assign语句、模块输出端口或实例化连线来驱动。它不能被放在always或initial块左侧进行赋值。// 示例reg 不一定对应寄存器 wire sel, data_a, data_b; reg mux_out; // 声明为 reg 类型 // 这个 always 块描述了一个纯组合逻辑的多路选择器 always (*) begin if (sel) mux_out data_a; // 阻塞赋值 else mux_out data_b; end // 综合后mux_out 是组合逻辑不会生成寄存器。常见问题在需要wire的地方误用了reg比如模块实例化时的连线。或者在always (posedge clk)中想生成组合逻辑却用了非阻塞赋值导致多余的寄存器被插入。排查技巧当你对某个信号的硬件实现不确定时不要猜直接看综合后的网表Netlist或原理图Schematic。工具会最真实地告诉你你的代码被翻译成了什么。这是从RTL思维转向硬件思维的关键一步。2.3 敏感列表不全与锁存器推断在组合逻辑always块中敏感列表(*)或(a, b, sel)必须包含所有在块内被读取的输入信号。如果遗漏可能导致仿真行为与综合后硬件行为不一致。更严重的问题是锁存器Latch的意外推断。锁存器是电平敏感的存储单元在ASIC设计中通常要避免因为它对毛刺敏感静态时序分析复杂。// 错误示例不完整的条件判断导致锁存器 always (*) begin if (en) begin out data; end // 缺少 else 分支当 en 为 0 时out 需要保持原值这暗示了存储功能。 end // 综合工具会推断出一个锁存器来保持 out 在 en0 时的值。避免方法对组合逻辑always块使用always (*)自动敏感列表这是最安全的方式。确保if-else或case语句覆盖所有可能的输入分支。对于case语句使用default分支对于if补全else。如果设计意图就是需要寄存器请明确使用时序逻辑always (posedge clk)。3. 仿真与调试让代码“动”起来并看清一切代码写完了仿真通过是功能正确的第一道保险。但仿真本身也有很多门道。3.1 仿真时间与事件调度Verilog仿真基于离散事件模型。理解时间片Time Slot和事件队列对于调试竞争条件至关重要。虽然大多数时候我们不需要关心但当遇到“明明逻辑对但仿真结果不对”的情况时可能就是它在作祟。简单来说在一个仿真时间点如 #10仿真器会处理多个队列的事件活跃事件阻塞赋值、连续赋值等、非活跃事件#0延迟、NBA非阻塞赋值更新等。非阻塞赋值的更新发生在NBA队列这保证了在同一个时钟沿所有寄存器看起来是同时更新的。常见陷阱在测试平台Testbench中同时使用阻塞赋值和时钟驱动。// 有风险的写法 initial begin clk 0; forever #5 clk ~clk; end initial begin #10; // 在第10ns data 8‘hAA; // 阻塞赋值 (posedge clk); // 等待下一个时钟上升沿 // 问题data的赋值和时钟沿触发的事件在同一个仿真时间片内顺序可能不明确。 end更安全的做法是在时钟边沿稍后一点的时间点更新激励initial begin #10; data 8‘hAA; // 使用非阻塞赋值 (posedge clk); // 或者明确延迟一个极小量避开时钟沿 #10; data 8’hAA; (posedge clk); end3.2 文件操作$readmemh/$readmemb的正确姿势这两个系统任务用于从文本文件中读取数据并初始化存储器Memory或数组在初始化ROM、加载测试向量时非常有用。常见问题文件路径使用相对路径时路径是相对于仿真启动目录通常是项目根目录或仿真工具的工作目录而非源文件所在目录。最好使用绝对路径或通过仿真工具参数设置。数据格式$readmemh读取十六进制$readmemb读取二进制。文件中的数据必须严格符合格式每行一个数值空白行和//注释会被忽略但/* */注释可能引发错误。初始化范围可以指定初始化的起始和结束地址。reg [7:0] memory [0:1023]; // 深度1024宽度8bit的存储器 initial begin // 从文件“data.hex”中读取数据填充 memory[0] 到 memory[1023] $readmemh(“data.hex”, memory); // 或者只填充一部分从 memory[100] 到 memory[199] // $readmemh(“data.hex”, memory, 100, 199); end注意事项仿真器在initial块开始时就会执行$readmemh。确保此时存储器数组已经声明并且文件路径正确。如果文件读取失败仿真器通常会给出警告但不会停止仿真可能导致存储器内容全为X未知态使得后续仿真结果全无意义。务必检查仿真日志中的相关警告信息。3.3 高效调试波形、断言与打印波形查看这是最直观的调试方式。关键是要有选择地添加信号而不是把所有信号都加进去。重点关注控制流状态机状态、使能信号、数据流关键数据路径、以及接口信号。为信号设置有意义的显示格式如二进制、十六进制、有符号十进制、模拟波形等。SystemVerilog 断言SVA这是更高级的调试和验证手段。断言可以内嵌在RTL中实时检查设计是否违反特定的时序或逻辑关系。例如检查一个请求信号拉高后必须在3个周期内得到应答。// 一个简单的并发断言示例 req_resp_handshake: assert property ((posedge clk) disable iff (!rst_n) $rose(req) |- ##[1:3] $rose(resp) ) else $error(“Request not acknowledged in time!”);当断言失败时仿真会立即报错并指出位置能极大加速接口协议和时序错误的定位。虽然学习曲线稍陡但对于复杂设计投入是值得的。打印信息$display,$monitor在测试平台中 strategically 地插入打印语句可以跟踪程序的执行流和关键变量的值。$display在执行到时打印一次$monitor会监控参数列表中的变量任何变量变化时都会打印。注意不要滥用否则日志会变得极其庞大。4. 功能实现核心难点解析掌握了语法和仿真我们开始构建功能模块。这里集中了几个经典且容易出错的实现场景。4.1 有限状态机FSM设计安全与清晰状态机是数字逻辑的控制核心。一个健壮的状态机需要清晰的状态定义、明确的状态转换条件、安全的默认状态处理。三段式状态机是被广泛推崇的写法因为它将状态转移、状态输出和状态寄存器分开结构清晰利于综合和时序分析。// 以简单的读写状态机为例 module simple_fsm ( input wire clk, rst_n, start, done, output reg read_en, write_en, output reg [1:0] state_out ); // 第一部分状态定义 parameter S_IDLE 2‘b00, S_READ 2’b01, S_WRITE 2‘b10, S_DONE 2’b11; reg [1:0] current_state, next_state; // 第二部分状态寄存器时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state S_IDLE; else current_state next_state; end // 第三部分下一状态组合逻辑 always (*) begin next_state current_state; // 默认保持当前状态避免锁存器 case (current_state) S_IDLE: if (start) next_state S_READ; S_READ: if (done) next_state S_WRITE; S_WRITE: next_state S_DONE; S_DONE: next_state S_IDLE; default: next_state S_IDLE; // 安全默认项综合为确定硬件 endcase end // 第四部分输出组合逻辑也可以是时序输出根据需求 always (*) begin read_en 1‘b0; write_en 1’b0; case (current_state) S_READ: read_en 1‘b1; S_WRITE: write_en 1’b1; default: ; // 明确赋默认值避免隐含锁存器 endcase end assign state_out current_state; endmodule注意事项default分支至关重要。它不仅能让综合工具在状态机进入非法状态时比如由于亚稳态有机会恢复还能避免综合出不必要的锁存器。输出逻辑中所有输出信号在case语句前先赋予默认值这是避免组合逻辑输出产生锁存器的标准做法。4.2 跨时钟域处理亚稳态与同步器当信号从一个时钟域传递到另一个时钟域时如果源寄存器的输出变化时间接近目的寄存器时钟的采样边沿目的寄存器可能进入亚稳态Metastability其输出在较长时间内处于非0非1的中间电平导致后续逻辑错误。单比特信号同步最常用两级寄存器同步器双触发器。module sync_single_bit ( input wire clk_dst, // 目的时钟域时钟 input wire rst_n, input wire async_in, // 来自源时钟域的异步信号 output wire sync_out // 同步到目的时钟域的信号 ); reg reg1, reg2; always (posedge clk_dst or negedge rst_n) begin if (!rst_n) begin reg1 1‘b0; reg2 1’b0; end else begin reg1 async_in; // 第一级承受亚稳态风险 reg2 reg1; // 第二级极大降低亚稳态传播概率 end end assign sync_out reg2; endmodule关键点同步器只能降低亚稳态传播的概率不能完全消除。两级触发器通常足够要求极高的场景可用三级。同步器会引入至少两个目的时钟周期的延迟设计上层逻辑时必须考虑。这只适用于单比特、电平信号。对于连续变化的信号或计数器需要更复杂的处理如格雷码、握手、异步FIFO。4.3 异步FIFO设计数据与指针的同步异步FIFO是处理跨时钟域数据流最常用的IP。其核心难点在于如何正确、安全地比较读/写指针来判断空满状态而这两个指针分别属于不同的时钟域。解决方案格雷码与指针同步指针使用格雷码格雷码相邻数值间只有一位变化。将二进制写指针转换为格雷码然后同步到读时钟域读指针同理。这样即使同步过程中发生亚稳态也只会导致指针值相差1而不会出现二进制码跳变多位导致的灾难性误判。空满判断逻辑满标志在写时钟域产生。比较写指针和同步过来的读指针格雷码注意这个读指针是若干周期前的“旧”值。空标志在读时钟域产生。比较读指针和同步过来的写指针格雷码。判断逻辑需要将格雷码指针转换回二进制后再比较或直接比较格雷码但逻辑稍复杂。实操心得异步FIFO的深度设计有讲究。深度最好是2的幂次这样指针回环判断简单。实际有效深度会比物理深度小1因为要区分“满”和“空”状态当写指针赶上读指针时是满还是空。一个经验公式FIFO深度 (写入速率 - 读出速率) * 突发数据包长度 / 读出速率并留有一定余量。自己手写异步FIFO是对跨时钟域理解最好的锻炼但在产品中强烈建议使用经过充分验证的IP核或标准代码。4.4 存储器接口与时序以DDR3和I2C EEPROM为例I2C EEPROM读写这是一个典型的低速串行接口。关键在于严格按照I2C协议的状态机来实现起始位、设备地址读写位、应答、数据字节、停止位。Verilog实现时时钟SCL的产生和数据的采样要精确到时钟沿。// I2C发送一个字节的简化状态机片段 parameter S_START 3‘b000, S_ADDR 3’b001, S_WRITE 3‘b010, S_READ 3’b011, S_ACK 3‘b100, S_STOP 3’b101; // 需要内部计数器来计数已发送的比特8个数据位1个应答位 always (posedge clk or negedge rst_n) begin if(!rst_n) bit_cnt 4‘d0; else if (state S_WRITE || state S_READ) begin if (scl_falling_edge) // 在SCL下降沿更新比特计数 bit_cnt bit_cnt 1; end else begin bit_cnt 4’d0; end end // 根据bit_cnt控制SDA数据线的输出常见问题时序不满足I2C规范如SCL高低电平时间、建立保持时间、未正确处理从设备的应答ACK/NACK、以及多个字节读写时的地址自动递增逻辑。DDR3接口这是高速并行接口复杂度陡增。通常使用厂商提供的物理层PHYIP核和内存控制器Memory ControllerIP核。工程师的工作主要是正确例化并连接这些IP核。理解并配置IP核的参数如时序参数tCL, tRCD, tRP等这些在DDR3芯片数据手册中定义。设计用户逻辑与内存控制器之间的接口通常是AXI或自定义FIFO接口处理读写请求、数据缓冲和命令调度。进行严格的时序约束SDC文件和时序分析确保在PVT工艺、电压、温度变化下接口时序仍能收敛。注意事项对于DDR3这类高速接口强烈不建议从零开始用Verilog写控制器。使用成熟IP是唯一可靠的选择。重点应放在接口逻辑设计、时序约束、以及系统级带宽和延迟的优化上。5. 综合、时序与后端考量从RTL到硬件代码仿真正确只是万里长征第一步。综合Synthesis将RTL转换为门级网表并需要满足时序Timing要求。5.1 时序约束基础时钟、输入延迟、输出延迟综合和布局布线工具需要知道你的设计要跑多快这就是时序约束的目的。最基本的约束包括创建时钟create_clock定义主时钟的频率、占空比和端口。# SDC (Synopsys Design Constraints) 示例 create_clock -name sys_clk -period 10.0 [get_ports clk] # 100MHz时钟输入延迟set_input_delay告诉工具相对于时钟边沿输入信号在芯片端口实际到达的时间。set_input_delay -clock sys_clk -max 2.5 [get_ports data_in]输出延迟set_output_delay告诉工具相对于时钟边沿输出信号需要在芯片端口稳定下来的时间。常见问题不加约束或约束过松/过紧。不加约束工具会按默认通常很慢优化实际性能远低于芯片能力。约束过紧周期设得太小会导致时序无法收敛出现建立时间Setup Time违例。约束过松则浪费性能。5.2 建立时间与保持时间违例这是静态时序分析STA的核心概念。建立时间Tsu时钟沿到来之前数据必须保持稳定的最短时间。违例意味着数据变化太晚来不及被时钟采到。保持时间Th时钟沿到来之后数据必须继续保持稳定的最短时间。违例意味着数据变化太早冲掉了刚采样的值。解决思路建立时间违例数据路径延迟太长。解决方法降低组合逻辑级数流水线切割、优化逻辑、使用更快的逻辑单元、降低时钟频率。保持时间违例数据路径延迟太短。解决方法在数据路径上插入缓冲器Buffer增加延迟但要注意不能影响建立时间、检查时钟偏移Clock Skew。排查技巧当工具报告时序违例时不要慌。首先看违例路径的起点Launch Flip-Flop和终点Capture Flip-Flop分析中间的组合逻辑。使用工具的“关键路径报告”功能它会列出延迟最大的那些路径。优化时优先处理最差的几条路径。对于复杂的逻辑插入流水线寄存器是改善建立时间最有效的方法之一虽然它会增加一个周期的延迟。5.3 面积与功耗优化在满足时序的前提下我们需要关注面积芯片成本和功耗特别是移动设备。面积优化移除冗余逻辑使用资源共享如多个地方用的加法器合并成一个选择面积更小的IP核或编码方式比如状态机编码用二进制还是格雷码面积有差异。功耗优化动态功耗与时钟频率和电压平方成正比。降低频率、降低电压需考虑时序、减少不必要的信号翻转使用门控时钟 Clock Gating是主要手段。门控时钟可以在模块空闲时关闭其时钟树大幅降低动态功耗。静态功耗主要与工艺和温度相关。在RTL级影响有限但可以注意减少使用漏电大的单元。工具的使用现代综合工具如Design Compiler, Vivado Synthesis都提供面积和功耗优化的选项与策略。但记住工具是在你的RTL代码基础上进行优化。清晰、简洁的RTL代码是后端优化的基础。一个混乱的RTL设计即使用最强的优化策略结果也往往不尽人意。6. 工具链与开发环境工欲善其事必先利其器。高效的开发环境能极大提升生产力。6.1 编辑器与IDEVS Code配置VS Code凭借其轻量和强大的插件生态已成为很多数字设计工程师的首选编辑器。核心插件Verilog-HDL/SystemVerilog/Bluespec SystemVerilog由mshr-h提供支持语法高亮、代码片段、简单 linting。Verilog Format代码格式化工具保持代码风格统一。CTags Support配合 Universal Ctags 生成标签实现模块、实例、信号名的跳转和查找引用。实现例化名跳转的关键在项目根目录生成tags文件在终端执行ctags -R .。这会扫描所有Verilog文件生成一个tags文件。在VS Code中安装CTags Support插件。打开一个Verilog文件将光标放在一个模块实例化名上按F12或Ctrl鼠标左键即可跳转到该模块的定义处。需要定期尤其在添加新文件后重新生成tags文件以更新索引。其他配置设置.v和.sv文件的默认格式化工具配置自动保存时格式化可以保持代码整洁。6.2 仿真工具选择与协同ModelSim/QuestaSimMentor现Siemens产品行业标准功能强大调试能力强尤其是波形查看但商业版昂贵。VCSSynopsys产品编译仿真速度快在大规模SoC验证中占主导地位。XceliumCadence产品同样高性能。开源选择Icarus Verilog (iverilog)和GTKWave组合。Icarus是编译器将Verilog编译成一种中间格式然后执行仿真。GTKWave用于查看波形。对于中小型项目和学习完全够用且跨平台。工作流程通常使用Makefile或Shell脚本将编译、仿真、波形加载命令串联起来实现一键仿真。将不同的测试用例组织成不同的Testbench文件通过脚本参数选择运行哪个用例。6.3 版本控制Git for Hardware数字设计项目也必须使用版本控制。Git是最佳选择。该提交什么所有RTL代码.v, .sv、约束文件.xdc, .sdc、仿真脚本.do, .tcl、测试平台、文档。不该提交什么综合、实现过程中产生的大量中间文件如_netlist.v,.bit,.rpt,.log等这些文件体积大且可重新生成。使用.gitignore文件过滤它们。分支策略可以为主要功能开发、bug修复、实验性特性创建不同的分支。master或main分支保持为可稳定综合和仿真的版本。个人体会建立一个清晰的项目目录结构至关重要。例如project/ ├── rtl/ # 所有设计源文件 ├── sim/ # 测试平台和仿真脚本 ├── constr/ # 时序和物理约束文件 ├── ip/ # 生成的或第三方IP核 ├── doc/ # 设计文档 └── scripts/ # 综合、实现、仿真的自动化脚本这能让团队协作和项目管理变得轻松很多。7. 进阶话题与性能优化当基本功能实现后我们会追求更高的性能、更低的功耗和更好的代码质量。7.1 高性能运算单元设计例如题目中提到的“96bit全加器”。对于超宽位宽的加法器行波进位加法器Ripple Carry Adder延迟太大。需要使用更快的结构超前进位加法器Carry-Lookahead Adder, CLA通过并行计算所有进位显著减少关键路径延迟但面积和复杂度随位宽增加而快速增长。进位选择加法器Carry-Select Adder将加法器分成若干段每段同时计算“进位为0”和“进位为1”两种结果然后根据实际进位选择输出。在面积和速度间取得较好平衡。Wallace树乘法器对于乘法器使用Wallace树结构压缩部分积比阵列乘法器更快。在Verilog中我们通常不直接手动编写这些复杂结构的门级网表而是用行为级描述assign sum a b;然后依靠综合工具的优化算法如synopsys designware库自动选择最优的架构。但对于性能极其关键的路径可能需要手动实例化优化后的IP核或进行手工设计。7.2 SystemVerilog的优势接口、断言与面向对象对于复杂系统验证纯Verilog显得力不从心。SystemVerilog是Verilog的超集引入了强大特性接口Interface可以将一组相关的信号如AXI总线封装成一个接口对象简化模块端口连接提高代码可维护性和复用性。接口内可以声明modport来定义不同的访问视角主/从。interface axi_lite_if (input logic clk, rst_n); logic [31:0] awaddr; logic awvalid; logic awready; // ... 其他信号 modport master (output awaddr, awvalid, input awready, ...); modport slave (input awaddr, awvalid, output awready, ...); endinterface断言SVA如前所述用于形式化验证和动态仿真检查。面向对象编程OOP用于构建复杂的、可重用的验证组件UVM验证方法学的基础。7.3 低功耗设计技巧除了前文提到的门控时钟在RTL层面还可以操作数隔离当某个功能单元的输出在下一级不会被使用时关闭其输入防止不必要的翻转传播。存储器分区将大存储器分成多个小块每次只激活访问的那一块降低有效电容。多电压域Multi-Voltage Domain对性能要求不同的模块使用不同的供电电压。低速模块使用低电压以大幅降低功耗。这需要在架构和物理设计层面进行规划。电源门控Power Gating完全关闭空闲模块的电源。这需要特殊的电源开关单元和状态保持寄存器。这些技术大多需要综合工具和后端工具链的支持并在设计初期就纳入架构考虑。8. 问题排查心法从现象到根源的推理最后分享一套我常用的排查问题的心法当你遇到仿真失败、功能不对、时序违例时可以按这个思路来。定位与隔离首先将问题范围缩小到最小的可复现单元。是一个模块不对还是一个接口信号不对编写一个最简单的测试平台只激励出问题的部分。波形分析查看仿真波形。从出错的输出信号往回看沿着逻辑路径和时钟周期一步步追溯。检查使能信号、控制信号、数据信号在关键时钟沿的值是否符合预期。特别注意复位后的初始状态。对比预期与实际在脑子里或纸上画出时序图标明每个时钟沿应该发生什么。然后与波形对比找到第一个出现偏差的地方。检查代码聚焦出现偏差的那个逻辑点。重新阅读相关代码检查敏感列表、赋值方式阻塞/非阻塞、条件判断是否完整、位宽是否匹配隐式截断或扩展、有无符号数问题。简化与实验如果还是找不到尝试简化代码。注释掉部分逻辑用常量代替复杂输入看问题是否消失。这是一种“二分法”调试。工具辅助使用仿真工具的调试功能如设置断点、单步执行、强制信号值、查看仿真日志中的警告和错误信息。综合工具的报告也要仔细看特别是警告信息有时会提示一些潜在问题如锁存器推断、多驱动、未连接端口等。时序问题单独处理如果功能仿真对但上板不对或者静态时序分析报告违例那很可能是时序问题。检查时钟约束是否正确、时钟是否存在过大的偏移Skew、关键路径逻辑是否过于复杂。后仿真带时序信息的仿真是排查时序问题的利器。记住调试是一个需要耐心和逻辑推理的过程。最怕的是没有方法地胡乱修改代码。每一次成功的调试都会让你对硬件描述语言和数字电路的理解更深一层。

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

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

免费获取报价