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所示。