news 2026/9/7 5:00:12

Verilog计数器必知:阻塞与非阻塞赋值底层原理解析与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Verilog计数器必知:阻塞与非阻塞赋值底层原理解析与实战

在数字逻辑开发和 FPGA 工程里,计数器可能是出现频率最高的 RTL 模块。无论是 LED 闪烁分频、UART 波特率生成、SPI 时钟分频,还是状态机里的超时判断、流水线里的地址偏移,背后都离不开计数器。很多同学第一次写计数器时,仿真波形看起来正常,一到板级验证就出现跳变、卡死甚至完全不工作;排查到最后,问题往往不在计数逻辑本身,而在 always 块里的赋值方式。本文以 Verilog 计数器为核心场景,深入剖析阻塞赋值(=)与非阻塞赋值(<=)的底层差异,并给出完整的 RTL 代码、Testbench 和仿真方法。

适合读者主要是两类:刚接触 Verilog 的初学者,以及已经能写简单模块、但还没完全搞清楚“为什么时序逻辑必须用非阻塞赋值”的开发者。学完本文后,你应该能独立写出带复位、使能、模值可配的计数器,也能解释阻塞赋值和非阻塞赋值在仿真器事件队列层面的差别,并学会用参数化计数器完成一个 LED 分频闪烁小工程。

1. 背景与核心概念

1.1 计数器在数字电路中的地位

计数器是时序逻辑的典型代表,也是最简单、最直观的时序电路。一个寄存器加一个加法器,就构成了最基础的二进制计数器;在此基础上增加复位、使能、加载初值、比较溢出等功能,就能覆盖大量工程场景。在 FPGA 工程中,计数器一般承担四类任务:分频计时、协议时序、状态统计、地址生成。无论是哪一种,都需要像寄存器一样在每个时钟沿更新数据,所以对赋值方式极其敏感。

这里需要明确一个概念:Verilog 中的 reg 类型并不一定综合成寄存器(触发器)。在组合逻辑 always 块中,reg 会被综合成 wire 或锁存器;只有当时钟沿触发的 always 块中,reg 才对应真正的 D 触发器。计数器如果希望每个时钟周期加一,就必须让它被综合成寄存器组。非阻塞赋值是这个目标最直接的语法表达。

1.2 计数器的常见分类

按计数方向分:加法计数器、减法计数器、可加可减计数器。按计数模值分:二进制计数器、模 M 计数器、十进制计数器。按复位方式分:同步复位计数器、异步复位计数器。多数课程和项目中,最常见的是“带异步复位、同步使能、可配置模值的加法计数器”。

顺便补充一个常见词:模 100 计数器。它的含义是计数范围从 0 到 99,到达 99 后下一个时钟回到 0,并可以输出一个脉冲。本文后续会用参数化方式实现模值可配的计数器,只需要把 MOD 设为 100,就得到了模 100 计数器。

1.3 为什么阻塞赋值与非阻塞赋值容易踩坑

阻塞赋值(blocking assignment,=)与非阻塞赋值(nonblocking assignment,<=)是 Verilog 里最基础也最容易被误用的语法。初学者最容易出现的问题是:写 always @(posedge clk) 时所有变量都用 =;或者在一个 always 块里混用 = 和 <=。更麻烦的是,某些代码在仿真平台里能正常运行,综合到 FPGA 后行为却可能不一致。

这个问题的底层原因与仿真事件队列(event queue)有关,也和综合工具的语义映射有关。不是说“用阻塞赋值一定编译报错”,而是它的执行语义和真实 D 触发器的行为不一致,在多语句、多进程场景下会出现竞争与偏差。后文会专门拆开讲。

2. 开发环境与版本说明

2.1 推荐开发工具

本文示例不依赖具体厂商,Verilog 代码可以在 Vivado、Quartus Prime、ModelSim/Questa、Verilator 等任意主流工具中编译仿真。如果你以 FPGA 上板为目标,建议选择一种主流通用 EDA 工具:

  • AMD/Xilinx Vivado,适合 7 系列及更新芯片;
  • Intel Quartus Prime,适合 Cyclone、MAX 等系列;
  • ModelSim/Questa、Verilator 适合纯仿真验证;
  • VS Code 搭配 Verilog 插件,适合日常编辑和轻量语法检查。

版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示代码思路,不绑定某个工具的具体版本。

2.2 语言版本与语法范围

示例采用 Verilog-2001 和少量 SystemVerilog 常用写法。Verilog-2001 是绝大多数 FPGA 工具兼容性最好的版本;SystemVerilog 是后续演进标准,可读性更好,但老工具可能需要额外配置。

本文核心语法在 Verilog-2001 下可直接运行。如果你使用'0这种 SystemVerilog 写法,需要注意工具是否支持;为了保守,也可以写成{WIDTH{1'b0}},两种写法的目标都是把寄存器清零。

2.3 示例工程结构

建议按照下面结构组织工程:

project_dir/ └── rtl/ └── led_blink.v └── tb/ └── tb_led_blink.v └── sim/ └── run.do

在实际 FPGA 工程中,还应把引脚约束文件(如 .xdc / .qsf)单独放在 constr 目录。本文重点是 RTL 与仿真,不涉及具体板卡约束。

3. 计数器核心语法拆解

3.1 计数器的最基本模型

一个 4 位二进制计数器,每个时钟上升沿加一,到达最大值后自然回卷。代码如下:

module counter_bin #( parameter WIDTH = 4 )( input wire clk, input wire rst_n, output reg [WIDTH-1:0] cnt ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= {WIDTH{1'b0}}; end else begin cnt <= cnt + 1'b1; end end endmodule

这里用<=把 cnt 更新为“当前 cnt + 1”。当 cnt = 4'b1111 也就是 15 时,再加 1 会回卷到 0,这是二进制计数器的自然溢出特性。实际项目中通常不希望计数器一直自增,会加使能或比较清零。

需要特别注意:cnt + 1'b1中使用了 1 位常量,但加法运算会把操作数扩展到 WIDTH 位,所以不会出现截断异常。为了避免仿真中出现位宽告警,我习惯写cnt + 1'b1

3.2 同步复位与异步复位

计数器作为时序逻辑,必须考虑复位。更精确地说,复位策略决定 always 块的事件列表写法。

同步复位写法:

always @(posedge clk) begin if (!rst_n) begin cnt <= {WIDTH{1'b0}}; end else if (en) begin cnt <= cnt + 1'b1; end end

异步复位写法:

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= {WIDTH{1'b0}}; end else if (en) begin cnt <= cnt + 1'b1; end end

两者区别在于:同步复位只有当 clk 上升沿到来且 rst_n 为低时才复位;异步复位只要 rst_n 变成低电平,无论 clk 是否有效都会立刻复位。异步复位的优势是复位响应快,缺点是对复位信号上的毛刺敏感,而且在释放时若靠近时钟沿,容易导致亚稳态。工程上常用“异步复位、同步释放”方案,本文不展开,建议初学者先把两种写法都掌握。计数器的使能逻辑应放在复位判断之后、计数自增之前。

3.3 可配置模值计数器

如果只做到cnt <= cnt + 1,功能太单一。工程中更多场景是“计数到 M-1 清零,并产生一个标志”。模值参数化写法如下:

module counter_mod #( parameter WIDTH = 7, parameter MOD = 100 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] cnt, output reg wrap ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= {WIDTH{1'b0}}; wrap <= 1'b0; end else if (en) begin if (cnt == MOD - 1) begin cnt <= {WIDTH{1'b0}}; wrap <= 1'b1; end else begin cnt <= cnt + 1'b1; wrap <= 1'b0; end end else begin wrap <= 1'b0; end end endmodule

把 MOD 设为 100 就是模 100 计数器,计数范围 0~99。当 en=1 时,每个时钟加一;cnt 达到 99 时下一个时钟回 0 并输出一拍 wrap 高电平。这里有多个 if/else 分支同时给 cnt 赋值,但因为分支互斥,综合结果是正常的触发器和选择器。

需要特别指出:计数器输出标志写成寄存器输出,会比组合逻辑输出多一个周期延迟;如果要求“cnt==99 时 wrap 立即为高”,则可以写成组合逻辑或用 assign。寄存器输出的好处是波形干净,没有毛刺,时序收敛更容易。

3.4 计数器使能与比较清零

保持计数器的可控制性非常重要。使能信号 en 的作用是决定每个时钟周期是否自增。测试中发现 cnt 不变化,第一个要查的就是 en 是否一直为低。一定不要把 en 写在时钟边沿条件里,那是错误的;使能应该写在 always 内部的条件分支中。同样的道理,比较清零必须与计数逻辑放在同一个时钟域中完成。

4. 阻塞赋值与非阻塞赋值的底层逻辑

4.1 两种赋值语句的基本语法

阻塞赋值用 = 表示,非阻塞赋值用 <= 表示。单看名称,“阻塞”的意思是执行当前赋值时会立即更新左边变量,并且在这条语句结束前,同一进程中的下一条语句不能执行。非阻塞赋值不会阻塞当前进程后续语句,它会先计算右边的值,等到当前仿真时间步末尾再统一更新左边变量。

看一个最直观的代码差异:

reg A, B; // 阻塞赋值 always @(posedge clk) begin A = B; B = A; end // 非阻塞赋值 always @(posedge clk) begin A <= B; B <= A; end

在阻塞版本中,执行完A = B;后 A 已经被更新为新值,紧接着B = A;使用的是新 A,最后 A 和 B 都变成原来的 B。在非阻塞版本中,两个右值 RHS 在时钟沿来临时都读取旧值,不会因为语句顺序改变结果,最终 A 变成旧 B,B 变成旧 A。这就是交换变量最常见的区别示例。

4.2 从 Verilog 仿真事件队列理解底层逻辑

为什么会有这种差异?因为 Verilog 仿真器并不是按“一行行立即执行”的直觉来运行硬件描述。IEEE 标准里,每个时间步都被划分为多个仿真区域(region),其中与本文相关的主要有:

  • Active 区:执行阻塞赋值、连续赋值、原语求值、非阻塞赋值的 RHS 计算;
  • NBA(Nonblocking Assignment Update)区:将非阻塞赋值的左值 LHS 更新为新值。

也就是说,当always @(posedge clk)触发时,仿真器首先在 Active 区计算所有非阻塞赋值右边的表达式,并将“更新事件”排到 NBA 区;当前进程可以继续执行后面的语句,不会被阻塞。等到整个 Active 区处理完,NBA 区才统一更新左变量。由于所有 RHS 都在 Active 区读取,所以同一 always 块中多个非阻塞赋值的输入都是“时钟沿到来之前的值”。这与真实 D 触发器在时钟边沿采样 D 端、然后在边沿后更新 Q 端的行为完全吻合。

对于阻塞赋值,赋值操作会立刻在当前时刻修改变量,如果多个 always 块或语句之间存在组合反馈,仿真结果就会与综合后的硬件行为产生偏差。这也是为什么硬件设计工程师把阻塞赋值理解为“过程性连续更新”,而把非阻塞赋值理解为“并行变量更新”。

4.3 非阻塞赋值在硬件中的物理含义

从电路角度看,时序逻辑的本质是触发器组。D 触发器在时钟上升沿到来时,把 D 端采样进来的值锁存到 Q 端;Q 端之前的值保持到下一个有效沿。这正好对应非阻塞赋值的两条性质:

  • RHS 读到的是一拍之前的数据;
  • LHS 更新发生在同一时刻稍后的 NBA 区。

因此,在时钟沿触发的 always 块中使用非阻塞赋值,工具会把它综合成寄存器;在组合逻辑 always 块中使用阻塞赋值,工具通常会综合成纯组合逻辑。如果组合逻辑 always 的条件分支不完整,还可能综合出锁存器(latch)。理解这一点后,就能解释一个经典疑问:用阻塞赋值写always @(posedge clk) cnt = cnt + 1'b1;在仿真中也能自增,为什么还是建议用非阻塞?答案不是“不能编译”,而是仿真行为与真实硬件并行行为存在差异风险。如果单个 always 中只有一条赋值语句,阻塞和非阻塞在简单计数器上往往看不出问题;一旦出现多条赋值、多进程交互,阻塞赋值就会放大竞争。

4.4 一个需要避免的经典错误:交换数据与竞争

下面这个模块试图在每个时钟沿交换 a 和 b,用阻塞赋值实现:

module swap_block( input wire clk, input wire rst_n, output reg a, output reg b ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin a <= 1'b0; b <= 1'b0; end else begin a = b; b = a; end end endmodule

实际仿真时,一旦 rst_n 释放,第一个时钟沿后 a 和 b 会变成相同的值。如果想要真正的交换,必须使用非阻塞。很多教程用 initial 块演示这一点,但在时钟沿上下文里演示更接近真实项目:

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin a <= 1'b0; b <= 1'b0; end else begin a <= b; b <= a; end end

这个例子背后是底层逻辑中的“并行采样”思想。如果你的设计里出现多个 always 块对同一个变量赋值,属于多驱动,综合和仿真都会告警,应该重构代码而不是依赖阻塞赋值让某个块“最后生效”。

4.5 何时用阻塞赋值,何时用非阻塞赋值

一条简洁的 RTL 设计规则:

  • 时钟沿触发的 always 块内,用非阻塞赋值<=
  • 组合逻辑 always 块内,用阻塞赋值=
  • 不要在同一个 always 块中对同一变量混合使用阻塞和非阻塞赋值;
  • 不要在多个 always 块中对同一个变量赋值。

这里给一个组合逻辑的例子:

// 组合逻辑:用阻塞赋值,且分支要补全 always @(*) begin if (sel) begin dout = din; end else begin dout = 1'b0; end end

这段代码中 dout 是 reg 类型,但由于 always 不依赖时钟,综合后会变成 LUT 等组合逻辑;只有在 if 分支不完整、且没有默认赋值时,才可能出现锁存器。

5. 完整实战:计数器实现 LED 分频闪烁

5.1 需求拆分

假设开发板上有一个系统时钟和一颗 LED。希望 LED 每隔约 0.5 秒翻转一次。如果时钟是 50MHz,0.5 秒对应 25_000_000 个时钟周期。用一个 25 位计数器计数,重点在于“计数器计数到达阈值”时翻转 LED,而不是每周期翻转。

仿真时如果真等到 25_000_000 个周期,时间太长。所以 RTL 使用参数化 MAX,仿真时将 MAX 缩小到 8 或 10,验证功能后再在顶层配置为 25_000_000。

5.2 编写 RTL:led_blink.v

文件路径:rtl/led_blink.v

module led_blink #( parameter WIDTH = 25, parameter MAX = 25_000_000 )( input wire clk, input wire rst_n, output reg led ); reg [WIDTH-1:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= {WIDTH{1'b0}}; led <= 1'b0; end else if (cnt == MAX - 1) begin cnt <= {WIDTH{1'b0}}; led <= ~led; end else begin cnt <= cnt + 1'b1; end end endmodule

关键点:两个寄存器 cnt 和 led 都在同一个时钟沿更新;cnt 到达 MAX-1 的时钟沿,不但要清 0,还同时翻转 led。使用非阻塞赋值,cnt 清零和 led 翻转用的是“旧 cnt”,不会互相影响。如果这里混用阻塞赋值,由于清零语句写在 led 翻转之后或之前,仿真结果可能出现一个时钟周期偏差,综合后的硬件行为也可能不符合预期。

检查这个例子:MAX=10 时,cnt 计数范围 0~9,共 10 个周期;cnt 到 9 后清零并翻转 LED。如果你希望“先翻转 LED 再等待 MAX 个周期”,可以调整判断条件,但应始终保持同一边沿只做一组更新。

5.3 编写 Testbench

文件路径:tb/tb_led_blink.v

`timescale 1ns/1ps module tb_led_blink; reg clk; reg rst_n; wire led; led_blink #( .WIDTH(4), .MAX (10) ) dut ( .clk (clk), .rst_n(rst_n), .led (led) ); initial begin clk = 1'b0; rst_n = 1'b0; #100; rst_n = 1'b1; #500; $finish; end always #10 clk = ~clk; initial begin $monitor("time=%0t rst_n=%b cnt=%0d led=%b", $time, rst_n, dut.cnt, led); end endmodule

Testbench 中 clk 周期为 20ns,即 50MHz。先复位 100ns,释放后运行 500ns。由于 MAX=10,仿真结束前应该能看到 cnt 从 0 变到 9 后回 0,led 每 10 个周期翻转一次。dut.cnt 是内部信号,在 tb 中通过层次路径引用,便于观察。

注意$finish会导致仿真结束;如果使用 ModelSim,可配合 do 脚本自动运行。

5.4 仿真运行与结果验证

如果使用 ModelSim/Questa,可以在 sim 目录下创建 run.do:

vlib work vlog ../tb/tb_led_blink.v ../rtl/led_blink.v vsim -voptargs=+acc work.tb_led_blink add wave -radix unsigned /tb_led_blink/dut/cnt add wave -radix binary /tb_led_blink/led run -all

然后在仿真器命令行执行:

vsim -do run.do

运行完成后,波形中 cnt 应按 0,1,2,...,9,0,1,... 循环;led 在 cnt 到 9 的时钟沿翻转。如果你的波形太多 delta 延迟或全是 X,先检查复位是否被正确释放,再检查有没有变量被多驱动,最后看是否有编译告警。

5.5 上板测试注意事项

上板前需要增加引脚约束,把 clk 约束到开发板晶振引脚,把 led 约束到 LED 引脚。不同板卡约束文件名和语法不同。如果综合工具报告 MAX 太大,可以先把参数 WIDTH 设为 4、MAX 设为 10 或 100,用低频闪烁验证代码,再改回真实参数。

时序约束不是本文重点,但必须强调:不要在综合后直接认为“没有时序违例”就能上板。计数器跨时钟域或复位释放问题,需要另外检查时序报告。

6. 常见问题与排查思路

6.1 计数器不计数

计数器不计数时,按以下顺序排查:

  • 复位是否一直被拉低:检查 rst_n 信号是否在复位后确实为 1;
  • 使能信号是否一直是 0:如果模块里有 en,优先检查 en 波形;
  • clk 是否在翻转:对于纯仿真,检查 testbench 中always #10 clk = ~clk是否执行;
  • 是否对该 reg 存在多驱动:同一个 reg 不能出现在两个 always 块的赋值语句中;
  • 综合后是否被优化掉:如果输出端口没有负载或没有输出到顶层引脚,综合工具可能把计数器优化掉。

6.2 仿真出现 X 或大量毛刺

X 通常来自未初始化的 reg、复位未生效、芯片输出悬空、多驱动竞争。计数器仿真看到 X 时,先看复位时刻是否全部清零。使用$monitor打印复位后第一个时钟沿的 cnt。如果复位释放瞬间靠近异步复位下降沿,也可能导致亚稳态仿真;工程上建议采用异步复位同步释放。不要试图通过给 reg 加initial值来掩盖复位缺失,时序硬件必须靠复位端口确定初值。

6.3 时序逻辑中使用阻塞赋值导致功能异常

这是最容易被搜索到的坑。例如两个信号在同一个 always 块中互相采样:

always @(posedge clk) begin flag = req; if (flag) cnt = cnt + 1; end

用阻塞赋值时,flag 当前拍立即变成 req,下面的判断看到的是新值;综合后由于触发器并行采样,flag 仍然是一拍前的 req,两者不一致。遇到这种情况,应把整个时钟块改成非阻塞赋值。若确实需要组合逻辑,可以拆到新的always @(*)块中。

6.4 always 块中对同一变量混用两种赋值

很多工具会对下面代码报错或告警:

always @(posedge clk) begin cnt = cnt + 1; if (cnt == MAX) cnt <= 0; end

混用阻塞和非阻塞会使事件调度变得不可预测。计数器场景中应全部改为非阻塞。如果有临时变量,也可以用 localparam 或拆分逻辑,不要在同一个时序块里混用。

6.5 计数器位数不足

模值比较大时,WIDTH 必须满足2^WIDTH >= MOD。例如模 100 计数器至少需要 7 位,因为2^6=64,不足以计数到 99。使用参数化代码后,综合工具一般会给出警告,但你应该在写代码前手动校验。

6.6 常见问题速查表

问题现象常见原因解决思路
cnt 一直为 0rst_n 未释放或 en 为 0检查复位与使能波形
cnt 跳变不正常阻塞赋值写时序逻辑改为非阻塞赋值
仿真全是 Xreg 未复位或多驱动检查复位和赋值位置
上板不工作,仿真正常引脚约束错误或异步复位问题检查约束和复位释放
数到一半卡住比较条件不完整或使能断掉检查 if/else 分支
波形中存在竞争多个 always 驱动同一 reg合并逻辑或重构

7. 最佳实践与工程建议

7.1 RTL 编码规范

团队协作时,编码规范比个人风格更重要。建议在工程中统一:

  • 时钟信号命名带 clk,低有效复位命名 rst_n;
  • 计数器变量命名 cnt、count 或具备周期语义的名称;
  • 参数名统一大写,例如 WIDTH、MOD、MAX;
  • 时钟沿 always 块统一使用非阻塞赋值;
  • 组合逻辑 always 块统一使用阻塞赋值,并补全所有分支以防锁存器;
  • 不要在 RTL 代码中写#延迟,那是仿真专用写法;
  • 每个模块尽量只保留一个时钟域和复位域,除非是跨时钟域处理模块。

7.2 复位策略选择

计数器系统受复位策略影响极大。建议优先选择异步复位、同步释放:

reg rst_n_sync0, rst_n_sync1; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rst_n_sync0 <= 1'b0; rst_n_sync1 <= 1'b0; end else begin rst_n_sync0 <= 1'b1; rst_n_sync1 <= rst_n_sync0; end end

然后用 rst_n_sync1 作为模块内部异步复位信号。这样既能快速响应外部复位,又能避免复位释放时靠近时钟沿导致亚稳态。如果项目要求全同步设计,也可以统一使用同步复位,并把异步复位信号在进模块前用两级触发器同步。

7.3 参数化设计:让计数器复用

计数器不要写死某个位宽或模值。多花一点时间把 WIDTH、MOD、INIT_VAL 参数化,后续多个 IP 和模块都能复用。参数化代码要求综合前确保参数范围合法。例如模值 MOD 必须大于 1,否则比较清零条件永假或恒真,行为不可预期。

如果想做更复杂的计数器,可以增加 load 端口实现加载初值:

if (load_en) begin cnt <= load_val; end else if (en) begin // 原有计数逻辑 end

这也是开发秒表、定时器等应用时常用的结构。

7.4 跨时钟域与计数器安全

如果计数器的时钟来自 SDRAM 时钟、PCIe 恢复时钟等,不要直接把计数结果跨时钟域读取。高频计数器跨时钟域时,可以采用:

  • 格雷码计数器(相邻计数只变化一位);
  • 异步 FIFO 实现数据缓冲;
  • 脉冲同步电路。

例如在 SPI 或 I2C 从机中,计数器常位于慢速外设时钟域,需要把外设时钟视为独立时钟域处理,不能简单用系统时钟计数代替。计数器用于分频时,如果分频输出再驱动其他逻辑,要注意生成时钟的质量;更稳妥的做法是使用厂商 PLL/MMCM 输出分频时钟,或使用时钟使能而不是直接生成时钟。

7.5 综合验证与代码审查

上板前至少完成:行为仿真、综合告警检查、时序报告检查。如果仿真正确但综合后功能不对,优先查看综合后的 RTL Schematic,确认目标寄存器是否存在、是否有意外锁存器、赋值是否被优化。对于计数器这类简单模块,还可以借助 lint 工具自动检查阻塞/非阻塞使用规范。

代码审查时应重点关注:异步复位信号是否同步、使能逻辑是否有泄漏、模值是否越界、两个时钟域的交叉点有没有同步器。

8. 总结与学习路线

围绕 Verilog 计数器,本文主要梳理了以下内容:

  • 计数器是时序逻辑的重要基础,掌握复位、使能、模值参数化后,能覆盖大多数项目场景;
  • 计数器在时钟沿触发的 always 块中应统一使用非阻塞赋值;
  • 阻塞赋值与非阻塞赋值的差异,根因来自 Verilog 仿真事件队列和硬件触发器的并行更新特性;
  • 通过 LED 分频闪烁实例,完整走通从 RTL 到 Testbench 再到波形验证的流程;
  • 遇到计数器不计数、X 状态、竞争现象时,可以从复位、使能、多驱动、赋值方式几个方向定位。

细心的读者会发现,本文反复强调同一句话:时序逻辑用非阻塞,组合逻辑用阻塞,不要混用。这不仅是应试八股,也是若干硬件 Bug 的经验沉淀。

学习完计数器后,建议按顺序继续掌握以下内容:

  • 有限状态机(FSM),配合计数器做超时判断;
  • 分频器与时钟使能设计,分频器本质也是计数器;
  • UART 发送接收模块中的波特率计数器;
  • SPI/I2C 时序中的位计数和字节计数;
  • 同步 FIFO 与异步 FIFO,会用到读写指针计数器;
  • 跨时钟域常用方法。

如果已经有 FPGA 板子,可以做一个综合练习:用计数器实现秒表,按一下按键开始计时,再按一下暂停,数码管显示毫秒或秒。这个练习能把复位、使能、按键消抖、扫描显示、参数化计数器全部串起来。

Verilog 语法标准文档可以查阅 IEEE Std 1364(Verilog)和 IEEE Std 1800(SystemVerilog)。不建议只看语法书,一定要自己写 testbench,并且故意写错赋值方式,观察仿真和综合结果的差异。把阻塞赋值和非阻塞赋值在计数器中的行为对比亲手跑一轮后,很多疑问会自然消失。如果本文对你有帮助,可以收藏备用,后面我会继续更新状态机、分频器和 UART 相关的实战笔记。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 4:59:01

PTN业务配置核心流程与故障排查实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 4:56:37

局域网NTP时间同步服务器搭建:chrony配置与客户端接入指南

简介&#xff1a;这是一套面向小型局域网的时间同步工具&#xff0c;包含服务器端与客户端程序&#xff0c;适合需要在无外部NTP条件下自行校准设备时钟的IT运维或开发人员。方案以Visual C编写客户端&#xff0c;通过自定义协议与指定服务器通信&#xff0c;完成时间请求、响应…

作者头像 李华
网站建设 2026/9/7 4:55:21

重复文件清理软件推荐:Czkawka 免费去重工具 5 步上手全解

重复文件清理软件推荐&#xff1a;Czkawka 免费去重工具 5 步上手全解 【免费下载链接】czkawka Multi functional app to find duplicates, empty folders, similar images etc. 项目地址: https://gitcode.com/GitHub_Trending/cz/czkawka 如果你正在找一款重复文件清…

作者头像 李华
网站建设 2026/9/7 4:52:48

告别低速爬行与振荡:台达ASD驱动DD马达调试要点全解析

简介&#xff1a;面向运动控制工程师与自动化调试人员的ASD驱动器简易调试手册&#xff0c;围绕ASD与DD马达联调场景&#xff0c;覆盖面板语言设置、软件中英文切换、USB通讯连接、电机参数配置与方向确认等完整流程。文档基于Akribis Motion Gallery界面&#xff0c;偏重实操路…

作者头像 李华