[文档].艾米电子 - 二进制计数器及其变体,Verilog
0赞对读者的假设
已经掌握:
内容
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; 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所示。
图1 通用计数器的前仿真波形
图2 通用计数器的前仿真波形片段
参考
1 Pong P. Chu.FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version.Wiley