1 free-running二进制计数器
自由运行二进制计数器就是按照二进制形式不断循环计数。例如,4位的二进制计数器的从0000数到1111,然后翻回来重新数。
代码1 free-runing二进制计数器
module free_run_bin_counter
#(parameter N=8)
(
// global clock and asyn reset
input clk,
input rst_n,
// counter interface
output max_tick,
output [N-1:0] q
);
// signal declaration
reg [N-1:0] r_reg;
wire [N-1:0] r_next;
// body
// register
always@(posedge clk, negedge rst_n)
if(!rst_n)
r_reg <= 0; // {N{1'b0}}
else
r_reg <= r_next;
// next-state logic
assign r_next = r_reg + 1'b1;
//output logic
assign q = r_reg;
assign max_tick = (r_reg == 2**N-1) ? 1'b1 : 1'b0;
// r_reg == {N{1'b1}}
endmodule
次态逻辑是一个自增器,即给寄存器的当前值加1。由于使用了“+”运算符,因此也暗示了当r_reg到达1111的时候之后,会翻回来变成0000。这个电路也包括一个输出状态信号,max_tick。每当计数器到达最大值——1111(等同于2^N-1),就会插入一个max_tick,即max_tick变为高电平。
所谓tick即一个时刻,比方说我们把1分钟可以分为60个tick,那么每一秒都会产生一个tick。此处的max_tick正是这种意义的信号,相应的,具有同类属性的信号我们都会加上_tick这个后缀。tick信号常用于连接不同频率的时序电路。
2 Universal二进制计数器
通用二进制计数器,可递增或递减计数,亦可载入指定的值,也可被异步清零。其查找表如表1所示。注意rst_n和syn_clr信号的区别,前者是异步复位,且仅应该用于系统的初始化;后者为同步复位,只在时钟的上升沿被采样,可被用于一般的同步设计中。
表1 通用二进制计数器的查找表
syn_clr | load | en | up | q次态 | 操作 |
1 | - | - | - | 00…00 | 异步清零 |
0 | 1 | - | - | d | 并行载入 |
0 | 0 | 1 | 1 | q+1 | 递增计数 |
0 | 0 | 1 | 0 | q-1 | 递减计数 |
0 | 0 | 0 | - | q | 暂停 |
代码2 通用二进制计数器
module univ_bin_counter
#(parameter N=8)
(
// global clock and asyn reset
input clk,
input rst_n,
// counter interface
input syn_clr,
input load,
input en,
input up,
input [N-1:0] d,
output max_tick,
output min_tick,
output [N-1:0] q
);
// signal declaration
reg [N-1:0] r_reg, r_next;
// body
// register
always@(posedge clk, negedge rst_n)
if(!rst_n)
r_reg <= 0; // {N{1'b0}}
else
r_reg <= r_next;
// next-state logic
always@*
if(syn_clr)
r_next = 0;
else if(load)
r_next = d;
else if(en & up)
r_next = r_reg + 1'b1;
else if(en & ~up)
r_next = r_reg - 1'b1;
else
r_next = r_reg;
//output logic
assign q = r_reg;
assign max_tick = (r_reg == 2**N-1) ? 1'b1 : 1'b0;
assign min_tick = (r_reg == 0) ? 1'b1 : 1'b0;
endmodule
按照查找表设计的次态逻辑,被放在一个always块内,并且使用if-else-if来控制所需优先性的操作。
3 模-m计数器
模-m计数器,从0计数到m-1,然后翻过来重新计数。代码3所示的参数化的模-m计数器有两个参数:M,指定计数的范围为[0, M-1];N,指定M个数需要多少位宽来存储,其值为大于或等于log2(M)的整数。
代码3 模-m计数器(缺省为模-10)
module mod_m_bin_counter
#(
parameter N=4 // number of bits in counter
parameter M=10 // mod-M
)
(
// global clock and asyn reset
input clk,
input rst_n,
// counter interface
output max_tick,
output min_tick,
output [N-1:0] q
);
// signal declaration
reg [N-1:0] r_reg;
wire [N-1:0] r_next;
// body
// register
always@(posedge clk, negedge rst_n)
if(!rst_n)
r_reg <= 0;
else
r_reg <= r_next;
// next-state logic
assign r_next = (r_reg == (M-1)) ? 0 : r_reg + 1'b1;
//output logic
assign q = r_reg;
assign max_tick = (r_reg == (M-1)) ? 1'b1 : 1'b0;
assign min_tick = (r_reg == 0) ? 1'b1 : 1'b0;
endmodule
次态逻辑由一个条件语句组成:如果计数器数到M-1,那么新的值就会被清零;否则它将自增。
虽然N的值取决于M的值,但是有时计算N的值有点烦人,以后我们通过加入function来解决这个问题
4 时序电路的testbench
所谓testbench即模仿物理实验平台的程序。下面我们写一个万用计数器的testbench,当然也做作为其他时序电路的testbench的模板。关于testbench的更加复杂的话题以后会讨论。
代码4 通用计数器的testbench
`timescale 1ns/10 ps
// the `timescale specifies that
// the simulation time unit is 1 ns and
// the simulalor timestep is 10 ps
module univ_bin_counter_tb;
// declaration
localparam T = 20; // clock period
reg clk, rst_n;
reg syn_clr, load, en, up;
reg [2:0] d;
wire max_tick, min_tick;
wire [2:0] q;
// univ_bin_counter instaniation
univ_bin_counter #(.N(3)) univ_bin_counter_inst
(
//
.clk(clk),
.rst_n(rst_n),
//
.syn_clr(syn_clr),
.load(load),
.en(en),
.up(up),
.d(d),
.max_tick(max_tick),
.min_tick(min_tick),
.q(q)
);
// clock
// 20 ns clock running forever
always
begin
clk = 1'b1;
#(T/2);
clk = 1'b0;
#(T/2);
end
// reset for the first half cycle
initial
begin
rst_n = 1'b0;
#(T/2);
rst_n = 1'b1;
end
// other stimulus
initial
begin
// ==== initial input ====
syn_clr = 1'b0;
load = 1'b0;
en = 1'b0;
up = 1'b1; // count up
d = 3'b000;
@(posedge rst_n); // wait reset to deassert
@(negedge clk); // wait for one clock
// ===== test load ====
load = 1'b1;
d = 3'd011;
@(negedge clk);
load = 1'b0;
repeat(2) @(negedge clk); // wait for two clock
// ==== test syn_clear ====
syn_clr = 1'b1; // assert clear
@(negedge clk);
syn_clr = 1'b0;
// ==== test up counter and pause ====
en = 1'b1; // enable counter
up = 1'b1; // count up
repeat(10) @(negedge clk);
en = 1'b0; // pause
repeat(2) @(negedge clk);
en = 1'b1;
repeat(2) @(negedge clk);
// ==== test down counter ====
up = 1'b0;
repeat(10) @(negedge clk);
// ==== wait statement ====
// continue wait until q=20
wait(q==2);
@(negedge clk);
up = 1'b1;
// continue until min_tick becomes 10
@(negedge clk);
wait(min_tick);
@(negedge clk);
up = 1'b0;
// ==== absolute delay ====
#(4*T); // wait for 80 ns
en = 1'b0; // pause
#(4*T);
// ==== stop simulation ====
// return to interactive simulation mode
$stop;
end
endmodule
上述代码包括创建一个3位的计数器的例化模块表达式,以及生成时钟、复位和寄存器输入的三段表达式。
通过一个always块来生成指定时钟:
always
begin
clk = 1'b1;
#(T/2);
clk = 1'b0;
#(T/2);
end
T代表每个时钟周期包含多少时间单元。可使用下面的表达式定义。
localparam T = 20; // clock period
注意时钟生成always块没事敏感列表,及永远重复执行下去。clk信号被交替地断言为1或0,且每个值持续半个周期。
通过initial块来模拟复位信号:
initial
begin
rst_n = 1'b0;
#(T/2);
rst_n = 1'b1;
end
在仿真的开始,initial块会被执行一次。上面的语句表示rst_n信号被初始设置为0;然后在半个时钟周期后变为1。这个initial块代表“上电”情况,即上电后复位信号会被立即插入,以清零及初始化系统。注意:缺省情况下,变量值为未定态。使用复位短脉冲是一个很好的初始化系统的机制。
第二个initial块用于生成其他输入信号的模拟。我们首先测试的是载入(load)和同步清零(syn_clr)操作,然后测试的是两个方向的计数。$stop用于强制仿真器停止仿真。
在带有上升沿触发的触发器的同步电路中,输入信号必须在时钟的上升沿附近保持稳定,以满足建立时间和保持时间的时序约束。一个简单的方法可以实现在保持信号稳定的情况下变换信号:在时钟的1到0跳变情况下,改变信号的值。我们通过使用下面的语句来可以等待这个跳变。注意分号不要落下。
@(negedge clk);
negedge用于指定时钟信号的下降沿。注意:每一条这样的语句都带吧一个新的下降沿。在我们的模版中,我们一般使用这样的表达式来指定时间进程。比方说多个时钟周期,可以使用repeat表达式:
repeat(10) @(negedge clk); // repeat 10 times
同时我们还是用了一个类似的语句,来等待上电异步复位完成,即解除异步复位信号。之所以此处为posedge,因为代码中的异步复位信号是低电平有效,我们所等待的是其恢复为高电平的跳变。
@(posedge rst_n); // wait reset to deassert
在第二个initial块的后面,还有一些时序控制语句。我们可以等待指定的条件。比方说,等到“q=2”的时候。注意一条语句结尾,分号不要落下。
wait(q==2);
再比如等待一个信号跳变:
wait(min_tick);
抑或等待一段绝对时间,比如
#(4*T); // wait for 80 ns
如果修改了输入信号的值,我们需要确认输入的改变不是在时钟的上升沿发生的,因此我们应该在其后附加下面的语句。
@(negedge clk);
完成的上述testbench的前仿真波形如图1和图2所示。