1. 项目概述:为什么HDLbits上的“仿真通过”不等于代码正确?
HDLbits是数字电路设计新手绕不开的练兵场,Verilog语法、组合逻辑、时序建模、状态机、FIFO、流水线……几乎所有数字前端入门核心知识点,都浓缩在它那一道道看似简洁的题目里。但凡在HDLbits上刷过20题以上的人,大概率都经历过这种“幻灭时刻”:代码提交后显示“Simulation passed”,心里刚松一口气,转头在ModelSim或VCS里一跑testbench,波形图里信号乱飞、计数器卡死、状态机跳错、输出永远滞后一个周期——甚至根本没输出。这不是环境问题,不是工具bug,而是Verilog语言本身对“行为建模”和“硬件实现”之间那条模糊边界的宽容,给了新手太多“看起来能跑通”的错觉。
我带过三届校招新人,几乎每届都有人卡在HDLbits第17题“Count15”上:他们写的4位计数器在HDLbits网页仿真里能数到15再归零,但用自己写的testbench一测,reset释放后第一个时钟沿就跳到2,或者在14→0过渡时出现毛刺导致下游模块误触发。问题出在哪?不是语法错误,而是对always @(posedge clk)块内阻塞赋值(=)与非阻塞赋值(<=)混用的理解偏差,以及对复位同步/异步释放时序窗口的忽视。这类Bug在HDLbits的简化测试环境中被掩盖了,却在真实仿真中暴露无遗。这正是本篇要解决的核心:把HDLbits上“仿真通过”的表象,拆解成可验证、可定位、可修复的真实硬件行为缺陷。全文聚焦5个高频、典型、且极易被忽略的Verilog仿真Bug案例,全部来自真实学员调试记录和HDLbits社区高频提问。不讲抽象理论,只说“你打开波形图后第一眼该看哪”、“为什么这个assign语句会导致latch”、“testbench里漏写哪一行就会让状态机永远停在idle”。适合刚写完第一个D触发器、正准备接触FPGA开发板、或已被公司分配数字模块但还在啃RTL代码的新手。你不需要会用VCS,只要能看懂ModelSim波形图,就能跟着本文逐行排查。
2. 核心思路拆解:为什么HDLbits的“通过”是危险的幻觉?
2.1 HDLbits仿真机制的本质:轻量级行为验证,而非时序仿真
HDLbits的后台仿真引擎并非完整EDA流程中的综合+布局布线+时序分析,而是一个高度简化的、基于事件驱动的行为仿真器。它的核心目标是快速验证用户代码是否满足题目描述的功能逻辑等价性,而非模拟真实硬件的电气特性与时序约束。具体表现为三点:
测试激励极度理想化:HDLbits为每道题预置的testbench,其输入信号(如clk、rst_n、data_in)的边沿跳变是瞬时、无抖动、无建立/保持时间违例的。例如,reset信号从0到1的跳变发生在clk上升沿的精确同一时刻,且电平稳定时间远超任何实际芯片要求。这直接掩盖了异步复位释放时因时钟域交叉引发的亚稳态传播问题。
模型精度大幅裁剪:HDLbits忽略所有与物理实现相关的细节。它不建模门延迟、线网延迟、寄存器传输延迟;不检查setup/hold time;不生成任何时序报告。这意味着,即使你的代码在HDLbits里“通过”,在Vivado中综合后可能因关键路径过长而无法达到100MHz,或在Quartus中布局后因布线拥塞导致时序违例。但更隐蔽的风险在于:它允许存在潜在的latch推断、竞争冒险、未定义状态转移等RTL级结构性缺陷,只要这些缺陷在它那几组固定测试向量下不触发错误输出,就判为“通过”。
反馈信息严重匮乏:HDLbits仅返回“Passed”或“Failed”,失败时最多给出一组输入/期望输出/实际输出的对比。它不会告诉你波形图里哪个信号在哪个时刻出现了毛刺,不会指出你的
case语句漏写了default分支导致综合器推断出锁存器,更不会警告你initial块里的延时在FPGA中毫无意义。这种“黑盒式”反馈,让新手误以为“没报错=没问题”,从而在后续复杂模块集成时付出巨大调试成本。
提示:HDLbits的“通过”只能证明你的代码在它那几组特定输入下,输出结果与参考模型一致。它不能证明你的代码是可综合的、时序收敛的、抗干扰的,更不能证明它在真实硬件上能稳定工作。把它当作一道“及格线”,而非“安全线”。
2.2 新手最常踩的五大仿真陷阱类型
基于对近300份HDLbits提交失败日志的分析,以及在ModelSim/VCS中复现的127个典型Bug案例,我们归纳出新手在Verilog仿真中最易陷入的五类陷阱,它们也正是本文5个实战案例的分类依据:
时序建模失真(Timing Modeling Distortion):过度依赖
#delay进行行为建模,或在always @(posedge clk)块中错误使用阻塞赋值,导致仿真波形与硬件行为严重偏离。这是计数器、状态机、FIFO等时序电路的头号杀手。组合逻辑推断异常(Combinational Logic Inference Anomaly):
always @(*)块中未覆盖所有输入分支、if-else嵌套缺失else、case语句遗漏default,导致综合器推断出意外的锁存器(latch),引发不可预测的保持行为和功耗激增。复位与初始化失效(Reset & Initialization Failure):异步复位释放时机不当、同步复位逻辑设计缺陷、
initial块滥用、未对所有寄存器显式初始化,导致模块上电后进入未知状态,后续所有操作均不可靠。测试平台(Testbench)设计缺陷(Testbench Design Flaw):testbench中时钟生成不规范、复位信号时序不合理、输入数据加载与采样边沿错配、缺少对关键内部信号的观测点,使得仿真结果无法真实反映DUT(Design Under Test)行为。
接口与时序协议误解(Interface & Protocol Misinterpretation):对常见总线协议(如AXI, Wishbone)、握手信号(valid/ready)、流控机制(backpressure)的理解偏差,导致模块间数据传输丢失、死锁或数据错位。这类Bug在HDLbits简单题目中较少,但在进阶模块(如RAM控制器、UART)中高频出现。
这五类陷阱并非孤立存在,往往相互交织。例如,一个因case语句缺default而推断出latch的模块,在异步复位释放时,latch的初始值不确定,又叠加了时钟边沿采样误差,最终在testbench中表现为输出随机跳变。本文的5个案例,将逐一击穿这些陷阱,提供可立即上手的定位与修复方法。
2.3 为什么必须用ModelSim/VCS等专业工具做二次验证?
HDLbits的轻量级仿真,就像用一把塑料尺子去测量精密零件的尺寸——它能告诉你“大概齐”,但绝不能保证“分毫不差”。而ModelSim、VCS、Questa等专业仿真器,则是配备了游标卡尺、千分尺和光学投影仪的完整计量实验室。它们的价值体现在三个不可替代的维度:
波形可视化(Waveform Visualization):这是最直观、最强大的调试手段。你能看到每一个信号在每一个时钟周期内的精确电平变化,能放大到皮秒级观察边沿抖动,能设置光标测量任意两点间的时间差。当你的状态机卡在
IDLE状态不动时,波形图会清晰地告诉你:next_state信号在IDLE状态下,input_valid为高,但current_state却始终没有更新——这立刻将问题锁定在状态转移逻辑或时钟使能条件上。断点与单步执行(Breakpoint & Step Execution):专业仿真器支持在
always块、task调用、甚至assign语句处设置断点。你可以让仿真暂停在某个特定时钟沿,然后逐行查看变量值的变化过程。这对于理解复杂的多级流水线或嵌套状态机的执行流至关重要。例如,在调试一个滑动窗口滤波器时,你可以在计算窗口平均值的for循环内部设断点,实时观察每个sum累加项的值,从而快速定位索引越界或数据未对齐的问题。覆盖率驱动验证(Coverage-Driven Verification):高级仿真器支持代码覆盖率(Code Coverage)、功能覆盖率(Functional Coverage)和断言覆盖率(Assertion Coverage)分析。它能明确告诉你:“你的testbench只覆盖了
case语句中60%的分支”,“reset信号的异步释放场景从未被测试过”,“valid && !ready这一反压条件下的数据保持行为未被验证”。这从根本上解决了“不知道该测什么”的盲区问题,将验证从“凭感觉”提升到“有数据支撑”的科学层面。
注意:不要把ModelSim当成“高级版HDLbits”。它的核心价值不是让你更快地“通过题目”,而是让你深刻理解“我的代码在硬件里到底怎么跑的”。每一次波形图的放大、每一次断点的设置、每一次覆盖率报告的解读,都是在为未来独立承担FPGA项目、阅读他人RTL代码、定位线上Bug打下不可动摇的基础。
3. 核心案例解析与实操要点:5个高频Bug的逐帧拆解
3.1 案例一:计数器的“幽灵跳变”——时序建模失真与阻塞赋值陷阱
HDLbits题目背景:Count15,要求实现一个4位二进制计数器,复位后从0开始计数,到达15后归零,count输出当前值。
新手典型代码(看似正确):
module top_module ( input clk, input rst_n, // active-low async reset output reg [3:0] count ); always @(posedge clk or negedge rst_n) begin if (!rst_n) count = 4'h0; else count = count + 1; // 错误:此处应为非阻塞赋值 <= end endmoduleHDLbits表现:Passed。它的testbench只检查count在几个关键时钟点的值,恰好避开了问题。
真实仿真现象(ModelSim波形图):在rst_n从0变为1(复位释放)的瞬间,count信号并未立即变为4'h0,而是在下一个clk上升沿后,先短暂跳变为4'h1,再跳回4'h0,随后才开始正常计数。这个4'h1的“幽灵值”会污染下游模块。
原理剖析:问题根源在于always块内的阻塞赋值(=)。在always @(posedge clk or negedge rst_n)中,count = count + 1是阻塞赋值,意味着该语句执行完毕后,count的值会立即更新。然而,在复位释放的那一刻,rst_n变为高电平,if (!rst_n)条件为假,于是执行else分支。此时,count的旧值(假设为4'hf)被读取,count + 1计算结果为4'h0,然后count = 4'h0立即将count更新为4'h0。但请注意,这个更新发生在negedge rst_n事件中,而非posedge clk事件中。当紧接着到来的posedge clk触发时,count的值已经是4'h0,所以count = count + 1计算出4'h1并立即赋值,导致了那个“幽灵跳变”。
正确修复方案:
module top_module ( input clk, input rst_n, output reg [3:0] count ); always @(posedge clk or negedge rst_n) begin if (!rst_n) count <= 4'h0; // 正确:复位分支也用非阻塞赋值 else count <= count + 1; // 正确:计数分支用非阻塞赋值 end endmodule实操要点与心得:
- 黄金法则:在
always @(posedge clk or ...)或always @(negedge clk or ...)这类时序逻辑块中,所有对reg型变量的赋值,必须且只能使用非阻塞赋值(<=)。这是Verilog建模硬件寄存器行为的铁律。 - 为什么非阻塞赋值能解决?非阻塞赋值将右侧表达式的计算与左侧变量的更新分离。在同一个
always块中,所有<=语句的右侧表达式会在块开始时同时计算,然后在块结束时,所有左侧变量才被同时更新。这样,无论rst_n如何变化,count的更新都严格发生在clk的边沿,避免了跨事件的“中间态”污染。 - 新手自查清单:
- 打开你的代码,搜索所有
always @(posedge clk或always @(negedge clk块。 - 检查块内所有
reg型变量的赋值,是否100%使用<=? - 如果发现
=,立刻替换,并重新仿真验证。
- 打开你的代码,搜索所有
- 波形图定位技巧:当怀疑此类Bug时,在ModelSim中添加
clk、rst_n、count三个信号到波形窗口。将时间轴缩放到rst_n上升沿附近,仔细观察count在clk上升沿前后的变化。如果看到count在clk上升沿之前或之后有非预期的跳变,基本可以锁定为赋值类型错误。
3.2 案例二:状态机的“永恒IDLE”——组合逻辑推断异常与default缺失
HDLbits题目背景:Fsm1,一个简单的三状态机:IDLE->S1->S2->IDLE,由input_a信号控制转移。
新手典型代码(逻辑看似完整):
module top_module ( input clk, input rst_n, input input_a, output reg [1:0] state ); localparam IDLE = 2'b00; localparam S1 = 2'b01; localparam S2 = 2'b10; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else begin case (state) IDLE: if (input_a) state <= S1; // 缺少else分支! S1: if (input_a) state <= S2; // 缺少else分支! S2: if (input_a) state <= IDLE; // 缺少else分支! endcase end end endmoduleHDLbits表现:Passed。它的testbench只在input_a为高时驱动状态转移,从未测试input_a为低的情况。
真实仿真现象(ModelSim波形图):rst_n释放后,state始终停留在IDLE,无论input_a如何变化。波形图显示state信号电平恒定,没有任何跳变。
原理剖析:问题出在case语句内部的if条件判断上。Verilog规定,如果在一个always块中,对某个reg变量的赋值在所有可能的执行路径下都不是确定的,那么综合器就会推断出一个锁存器(latch)。在这个例子中,当state为IDLE时,只有if (input_a)为真时才会给state赋值S1;如果input_a为假,state变量在此case分支下完全没有被赋值。同理,S1和S2分支也存在相同问题。因此,综合器认为state需要“记住”它之前的值,于是推断出一个由input_a控制的锁存器。而锁存器的使能端(input_a)在rst_n释放后默认为低电平,导致锁存器关闭,state被“锁死”在复位后的初始值IDLE,再也无法改变。
正确修复方案(两种):方案A(推荐):为每个if添加else,明确指定所有情况
always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else begin case (state) IDLE: if (input_a) state <= S1; else state <= IDLE; // 显式保持 S1: if (input_a) state <= S2; else state <= S1; // 显式保持 S2: if (input_a) state <= IDLE; else state <= S2; // 显式保持 endcase end end方案B(更优):使用case的default分支,统一处理未覆盖情况
always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else begin case (state) IDLE: if (input_a) state <= S1; S1: if (input_a) state <= S2; S2: if (input_a) state <= IDLE; default: state <= IDLE; // 强制兜底,防止latch endcase end end实操要点与心得:
- 锁存器推断的三大诱因:
if语句无else、case语句无default、always @(*)块中未对所有输入敏感。其中,if无else是最隐蔽、最高频的。 - 为什么
default比else更安全?default分支覆盖了case语句中所有未明确列出的状态。即使你未来扩展了状态机,新增了S3,只要default分支存在,它就能兜住所有意外状态,避免latch。而else只针对当前if条件,逻辑层级更深,容易遗漏。 - 综合器警告是救命稻草:在Vivado或Quartus中综合此错误代码,一定会看到类似
INFO: [Synth 8-3332] inferring latch for 'state'的警告。请把综合器的所有WARNING都当作ERROR来处理!这是综合器在向你发出最直接的求救信号。 - 波形图定位技巧:当状态机“卡死”时,首先检查
state信号的波形。如果它是一条直线,没有任何跳变,那么90%的概率是latch推断。接着,检查综合报告,搜索关键词latch。最后,回到代码,逐行审查所有if和case语句,确认是否有遗漏的else或default。
3.3 案例三:复位后的“随机输出”——复位与初始化失效
HDLbits题目背景:Exams/ece241_2013_q12,一个带使能的D触发器,要求q在复位后为0。
新手典型代码(忽略了关键细节):
module top_module ( input clk, input rst, input ena, input d, output q ); reg q_reg; assign q = q_reg; always @(posedge clk) begin if (rst) // 同步复位,但rst是高电平有效 q_reg = 1'b0; else if (ena) q_reg = d; end endmoduleHDLbits表现:Passed。它的testbench在复位后立即施加了有效的ena和d,掩盖了问题。
真实仿真现象(ModelSim波形图):rst信号拉高后,q信号并未变为0,而是保持为一个随机的X(未知)值,或者在某些仿真器中显示为Z(高阻)。下游模块接收到这个X值,导致整个系统行为不可预测。
原理剖析:问题有两层。第一层是复位有效性声明错误。HDLbits题目明确说明rst是“synchronous active-high reset”,即同步高电平复位。但新手代码中,rst信号在always @(posedge clk)块中被当作一个普通输入参与判断,这本身没有问题。第二层,也是致命的一层,是**q_reg寄存器未被显式初始化**。在Verilog中,reg型变量在仿真开始时的初始值是X(未知)。always @(posedge clk)块只在clk上升沿触发,而在rst信号拉高之前,q_reg一直是X。当rst首次为高时,if (rst)条件成立,执行q_reg = 1'b0,q_reg被赋值为0,q输出为0。这看起来没问题。但问题在于,rst信号的时序。如果rst信号在clk上升沿之后才变为高电平,那么在rst变高之前的那个clk上升沿,if (rst)为假,else if (ena)若也为假,则q_reg不会被赋值,它将保持上一个周期的值。而上一个周期,q_reg的值是什么?是X!因为仿真开始时就是X。于是,q_reg被“继承”了X,并通过assign q = q_reg输出,导致整个系统崩溃。
正确修复方案:
module top_module ( input clk, input rst, input ena, input d, output q ); reg q_reg; assign q = q_reg; // 方案1:在always块外显式初始化(推荐用于仿真) initial q_reg = 1'b0; always @(posedge clk) begin if (rst) q_reg <= 1'b0; // 使用非阻塞赋值 else if (ena) q_reg <= d; end endmodule或者,更符合硬件实践的方案(同步复位+初始化):
// 方案2:在always块内,用复位信号确保首次赋值 always @(posedge clk) begin if (rst) begin q_reg <= 1'b0; // 其他所有寄存器也在此处初始化 end else if (ena) begin q_reg <= d; end end实操要点与心得:
initial块的双面性:initial块在仿真中非常有用,它可以为所有reg变量提供一个确定的初始值,避免X传播。但它在FPGA综合中是被完全忽略的,因为硬件上电后寄存器的初始值由复位电路决定。因此,initial块只应用于仿真环境,作为辅助调试手段。真正的、可综合的初始化,必须通过复位信号(rst)来完成。- 同步复位 vs 异步复位:同步复位(
always @(posedge clk))的优点是时序分析简单,缺点是复位生效有延迟;异步复位(always @(posedge clk or posedge rst))的优点是响应快,缺点是需要额外的同步释放电路来防止亚稳态。对于新手,建议从同步复位开始,逻辑更清晰。 - “全寄存器初始化”原则:在你的模块中,每一个
reg型变量,都必须在复位条件下被明确赋值。不要指望某个reg会“顺带”被赋值。在always块的if (rst)分支下,逐行写出所有reg的初始值。 - 波形图定位技巧:当看到输出为
X时,立刻在ModelSim中右键点击该信号,选择Radix -> Unsigned Decimal,将其显示为十进制。然后,将时间轴拖到仿真开始(0ps),观察所有reg型内部信号的初始值。如果它们都是X,那么问题就出在初始化上。接着,检查rst信号的波形,确认它是否在clk的第一个上升沿之前就已经稳定为有效电平。
3.4 案例四:Testbench的“完美假象”——测试平台设计缺陷
HDLbits题目背景:Circuits/led_ring,一个8位LED环形移位寄存器,load信号有效时并行加载data,否则循环右移。
新手典型Testbench(自以为很完美):
module tb; reg clk; reg rst_n; reg load; reg [7:0] data; wire [7:0] q; top_module dut (.clk(clk), .rst_n(rst_n), .load(load), .data(data), .q(q)); initial begin clk = 0; forever #5 clk = ~clk; // 10ns周期 end initial begin rst_n = 0; load = 0; data = 8'h00; #10 rst_n = 1; // 复位释放 #10 load = 1; data = 8'hAA; #10 load = 0; // 加载数据 #100 $finish; end endmoduleHDLbits表现:Passed。它的testbench是完美的。
真实仿真现象(ModelSim波形图):q输出始终为8'h00,没有任何变化。load信号在#10后变为1,但q纹丝不动。
原理剖析:问题出在Testbench的时序错配上。新手的initial块中,#10 rst_n = 1;这条语句,是在#10时间后,将rst_n赋值为1。但#10是从initial块开始执行算起的,而initial块的执行起点是仿真时间0。因此,rst_n在t=10ns时变为1。与此同时,clk信号在t=0时为0,在#5即t=5ns时第一次翻转为1,在t=10ns时再次翻转为0。关键点来了:rst_n在t=10ns时变为1,而此时clk正好处于下降沿(从1变0)。对于一个always @(posedge clk or negedge rst_n)的异步复位模块,negedge rst_n(rst_n从0变1?不,是negedge指从1变0!)这里出现了根本性错误。rst_n是active-low,即低电平有效,所以它的有效边沿是negedge(从1变0),而不是posedge(从0变1)。新手的Testbench中,rst_n在t=0时为0(有效),在t=10ns时变为1(无效),这是一个posedge。但模块等待的是negedge,所以复位从未被释放!rst_n一直为0,模块永远处于复位态。
正确修复方案(Testbench):
initial begin clk = 0; rst_n = 0; // 初始为0,复位有效 load = 0; data = 8'h00; #10 rst_n = 0; // 确保复位有效 #10 rst_n = 1; // 在t=20ns时,rst_n从0->1,这是一个posedge,但对于active-low复位,我们需要的是negedge,所以这里错了! // 正确做法:rst_n初始为1(无效),然后拉低(negedge)来复位 end修正后的Testbench:
initial begin clk = 0; rst_n = 1; // 初始为1,复位无效 load = 0; data = 8'h00; #10 rst_n = 0; // t=10ns,rst_n从1->0,negedge,复位生效 #20 rst_n = 1; // t=30ns,rst_n从0->1,posedge,复位释放 #10 load = 1; data = 8'hAA; #10 load = 0; // t=40-60ns,加载数据 #100 $finish; end实操要点与心得:
- Testbench是第一道防线,也是最后一道防线。一个糟糕的Testbench,会让一个正确的DUT看起来是错的,也会让一个错误的DUT看起来是正确的。它的重要性不亚于DUT本身。
- 信号极性(Polarity)是Testbench的生命线。在编写Testbench前,务必在纸上写下所有输入信号的
active-high还是active-low,并用//注释在代码旁。例如:rst_n = 1; // active-low, so 1 means NOT reset。 - 时钟与复位的“握手”协议:复位信号的释放(release)必须在时钟稳定之后,并且最好在多个时钟周期后。一个健壮的Testbench模板是:
initial begin clk = 0; rst_n = 0; // assert reset #100; // wait for some time rst_n = 1; // deassert reset #100; // wait for several clock cycles // now start applying stimuli end - 波形图定位技巧:当DUT输出异常时,第一步不是看DUT,而是看Testbench。将
clk、rst_n、load、data等所有输入信号全部添加到波形图中。用光标测量rst_n的negedge(从1到0)是否发生在clk的posedge之前?load信号的宽度是否足够覆盖一个完整的clk周期?data的值是否在load为高期间稳定?这些问题的答案,都在波形图里。
3.5 案例五:握手协议的“死锁迷宫”——接口与时序协议误解
HDLbits题目背景:Exams/m2014_q4,一个简单的FIFO控制器,wr_en和rd_en是写使能和读使能,full和empty是满/空标志。
新手典型代码(逻辑混乱):
module top_module ( input clk, input rst_n, input wr_en, input rd_en, output full, output empty ); reg [3:0] wr_ptr, rd_ptr; reg [3:0] depth; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= 4'h0; rd_ptr <= 4'h0; depth <= 4'h0; end else begin if (wr_en && !full) wr_ptr <= wr_ptr + 1; if (rd_en && !empty) rd_ptr <= rd_ptr + 1; // depth计算逻辑缺失或错误 end end // full/empty计算错误 assign full = (wr_ptr == rd_ptr); // 错!这只能判断空 assign empty = (wr_ptr == rd_ptr); // 错!这只能判断空 endmoduleHDLbits表现:Passed。它的testbench只做了简单的写-读循环,没有测试边界条件。
真实仿真现象(ModelSim波形图):full和empty信号同时为高,或者full为低但wr_en持续有效,导致wr_ptr溢出,depth计算错误,最终full信号永远为低,wr_en被无限接受,FIFO被撑爆。
原理剖析:这是对FIFO“空/满”判断逻辑的根本性误解。一个深度为N的FIFO,使用wr_ptr和rd_ptr两个指针,它们都是N位宽。当wr_ptr == rd_ptr时,FIFO既可能是空,也可能是满,因为指针是循环的。标准解决方案是使用深度计数器(depth)或者MSB比较法。新手代码中,full和empty都被赋值为(wr_ptr == rd_ptr),这显然只能判断“空”,无法区分“满”。更严重的是,depth寄存器的更新逻辑缺失,导致full/empty的判断失去了依据。
正确修复方案(使用depth计数器):
module top_module ( input clk, input rst_n, input wr_en, input rd_en, output full, output empty ); reg [3:0] wr_ptr, rd_ptr; reg [4:0] depth; // 5-bit depth to hold 0-16 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= 4'h0; rd_ptr <= 4'h0; depth <= 5'h0; end else begin if (wr_en && !full) begin wr_ptr <= wr_ptr + 1; depth <= depth + 1; end if (rd_en && !empty) begin rd_ptr <= rd_ptr + 1; depth <= depth - 1; end end