FPGA实战(07): Verilog 实现带符号输出的 0~99 循环计数器(tops 模块)设计与仿真
前言
在 FPGA 或数字 IC 设计中,计数器是最基础的时序逻辑电路之一。本文将分享一个简洁实用的 Verilog 计数器模块——tops,它能够输出带符号的 10 位宽信号,并在0~99之间循环计数。文章将结合设计代码、测试平台以及仿真分析,帮助读者快速理解异步复位、自增循环以及$signed的使用方法。代码风格规整、注释分区清晰,非常适合初学者学习和工程参考。
1. 设计功能与创新点
功能点
- 0~99 循环计数:每个时钟上升沿计数值加 1,计到 99 后自动归零,形成模 100 循环。
- 异步复位:复位信号
i_rst高电平有效,一旦有效立即将输出置 0,不受时钟控制。 - 带符号输出:输出端口
o_cout声明为signed [9:0],即便计数值始终为正,保留符号位可方便后续进行有符号扩展运算。 - 代码结构清晰:使用
param / reg / wire / assign / FSM / inst / combine_Logic / always等分区注释,便于维护和团队协作。
创新点
- 显式符号处理:在自增表达式
ro_cout + $signed('d1)中使用了$signed系统函数,明确将常量转换为有符号数,避免混合符号类型引发的综合警告,提升代码可移植性。 - 预留扩展接口:代码中虽然尚未使用状态机(
FSM)和实例化(inst)部分,但通过注释提前规划了模块结构,为后续扩展为多模式计数器或添加子模块留下清晰框架。 - 条件全覆盖写法:
always块内通过else if(1)保证所有条件分支无遗漏,同时明确最后一个else永不被执行,可从代码层面展示条件优先级的设计思想(实际综合工具会优化冗余逻辑)。
2. 模块代码与详解
下面是tops模块的完整代码:
module tops( input i_clk , input i_rst , output signed [ 9: 0] o_cout ); //**************param*****************// //**************reg*******************// reg signed [ 9: 0] ro_cout ; //**************wire******************// //**************assign****************// assign o_cout = ro_cout ; //**************FSM*******************// //**************inst******************// //**************combine_Logic*********// //**************always****************// always @(posedge i_clk or posedge i_rst )begin if(i_rst) ro_cout <= ('d0) ; else if(ro_cout == 'd99) ro_cout <= ('d0) ; else if(1) ro_cout <= (ro_cout + $signed('d1)) ; else ro_cout <= ro_cout ; end endmodule代码解析
- 端口列表
i_clk:时钟输入;i_rst:异步复位,高有效;o_cout:带符号 10 位输出,连接到内部寄存器ro_cout。
- 内部寄存器
ro_cout声明为signed [9:0],与输出端口类型一致,存储当前计数值。
- 连续赋值
assign o_cout = ro_cout;将寄存器值直接输出。
- 时序逻辑(always 块)
- 敏感列表为
posedge i_clk or posedge i_rst,表示时钟上升沿或复位上升沿触发(异步复位)。 - 复位处理:
if(i_rst)条件优先级最高,复位时将ro_cout清零。 - 计数上限判断:当
ro_cout == 99时,下一时钟沿也清零,实现循环。 - 自增操作:
else if(1)恒为真,其余情况(即计数值不为 99 且非复位)执行ro_cout + $signed('d1)。这里$signed('d1)将无符号常量1强制转为有符号数,与ro_cout类型匹配,避免综合器报警。 - 最后的 else:作为保底写法,保持当前值,但永远不会执行(因为
else if(1)已覆盖全部未复位、未达到上限的情况)。
- 敏感列表为
3. 测试平台与仿真
为了验证计数器功能,我们编写如下 testbench:
`timescale 1ns / 1ps module test_tops; reg i_clk; reg i_rst; wire signed[9:0] o_cout; tops tops_u( .i_clk (i_clk), .i_rst (i_rst), .o_cout (o_cout) ); initial begin i_clk = 1'b1; i_rst = 1'b1; #100 i_rst = 1'b0; end always #5 i_clk = ~i_clk; // 时钟周期 10ns endmodule激励说明
- 时钟:初始电平为高,每隔 5ns 翻转一次,产生周期 10ns(频率 100MHz)的方波。
- 复位:初始为高电平,持续 100ns 后拉低,模拟上电复位过程。
- 顶层例化:将
tops模块实例化,并连接激励与观测信号。
4. 仿真结果与分析
在 ModelSim 或 Vivado 仿真器中运行上述 testbench,得到波形如下(文字描述):
- 0~100ns:
i_rst为高,输出o_cout保持为0,无论时钟如何变化。 - 100ns 之后:复位释放,在第一个时钟上升沿(105ns 处)
o_cout由 0 变为 1;随后每个时钟周期递增,1→2→3 … →99。 - 当计数值达到 99 时,下一个时钟上升沿归零,变为 0,然后继续 0→1→2…,形成模 100 循环。
整个过程符合设计预期,异步复位与循环计数功能均正确实现。
5. 总结与扩展
本文通过tops模块展示了一个规范的 Verilog 计数器设计,重点在于:
- 异步复位与循环计数的组合逻辑;
$signed在有符号运算中的规范使用;- 分区注释与条件全覆盖的代码习惯。
扩展建议
若希望计数范围可配置,可引入参数parameter MAX = 99,将比较条件和复位后的初始值用MAX替代。修改后的片段如下:
parameter MAX = 99; ... else if(ro_cout == MAX) ro_cout <= 'd0; else ro_cout <= ro_cout + $signed('d1);这样只需修改一个参数即可改变计数模值,进一步提升了设计的复用性。
希望这篇博客对正在学习 Verilog 的读者有所帮助。如有疑问或建议,欢迎在评论区交流!
