数字IC经典电路设计
经典电路设计是数字IC设计里基础中的基础,盖大房子的第一部是打造结实可靠的地基,每一篇笔者都会分门别类给出设计原理、设计方法、verilog代码、Testbench、仿真波形。然而实际的数字IC设计过程中考虑的问题远多于此,通过本系列希望大家对数字IC中一些经典电路的设计有初步入门了解。能力有限,纰漏难免,欢迎大家交流指正。快速导航链接如下:
一、可复位/置数计数器
1.1 可复位/置数计数器
根据要求可以知道有四个输入和两个输出,主要包括累加计数、置位、指示信号,此题较简单,分开设计即可。
模块的接口信号图如下:
1.2 Verilog代码
要求:设计一个位宽为4的带复位端和置位端的计数器,并且计数器输出信号递增每次到达0,指示信号zero拔高至“1”,当置位信号set 有效时,将当前输出置为输入的数值set_num。
//可复位可置位的简易计数器
module count_bin01(
input clk,
input rst_n,
input set,
input [3:0] set_num,
output reg [3:0]number,
output reg zero
);
reg [3:0] cnt;
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
cnt <= 0;
end
else if(set) begin //置数
cnt <= set_num;
end
else begin
cnt <= (cnt==15)?0:cnt+1; //累加计数,到顶清零
end
end
//计数圈数的指示信号
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
zero <= 0;
end
else begin
zero <= (cnt==0)?1:0;
end
end
//时序逻辑打一拍子输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
number <= 0;
end
else begin
number <= cnt;
end
end
endmodule
1.3 Testbench
`timescale 1ns/1ps;仿真时间单位1ns 仿真时间精度1ps
module count_bin01_tb();
//信号申明
reg clk;
reg rst_n;
reg set;
reg [3:0] set_num;
wire [3:0] number;
wire zero;
//模块实例化(将申明的信号连接起来即可)
count_bin01 u_count_bin01(
.clk (clk),
.rst_n (rst_n),
.set (set),
.set_num (set_num),
.number (number),
.zero (zero)
);
//生成时钟信号
always #5 clk =~ clk;
//为输入数据赋值
initial begin
clk = 0;
set = 0;
set_num = 4'b1010;
#5 rst_n = 1;
#5 rst_n =0;
#5 rst_n = 1;
#400 set = 1;
#100 set = 0;
#1000;
$finish;
end
endmodule
1.4 仿真结果
二、双向(可加可减)计数器
2.1 双向(可加可减)计数器
一个十进制计数器模块,当mode信号为1,计数器输出信号递增,当mode信号为0,计数器输出信号递减。每次到达0,给出指示信号zero。此题较简单,分开设计即可。
模块的接口信号图如下:
2.2 Verilog代码
要求:设计一个双向计数器,分别实现从0 ~ 9加法计数和9 ~ 0减法计数,并且计数器输出信号每次到达0,指示信号zero拔高至“1”。
//设计双向(可加可减)计数器
module count_bin02(
input clk,
input rst_n,
input mode,
output [3:0]number,
output reg zero
);
//双向计数模块
reg[3:0] number_r; //定义中间寄存器
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
number_r<=0;
end
else if(mode)begin//选择加计数器
number_r<=(number_r==9) ? 0 : number_r + 1; //累加计数,到顶清零
end
else if(~mode) begin //选择减计数器
number_r<=(number_r==0) ? 9 : number_r - 1; //累加计数,到底置9
end
else begin
number_r<=number_r;
end
end
assign number =number_r; //组合逻辑输出
//计数圈数的指示信号
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
zero<=0;
end
else if(number==0) begin
zero<=1;
end
else begin
zero<=0;
end
end
endmodule
2.3 Testbench
`timescale 1ns/1ps; //仿真时间单位1ns 仿真时间精度1ps
module count_bin02_tb();
//信号申明
reg clk;
reg rst_n;
reg mode;
wire [3:0] number;
wire zero;
//模块实例化(将申明的信号连接起来即可)
count_bin02 u_count_bin02(
.clk (clk),
.rst_n (rst_n),
.mode (mode),
.number (number),
.zero (zero)
);
always #5 clk =~ clk; //生成时钟信号
//为输入数据赋值
initial begin
clk = 0;
mode = 1;
#5 rst_n = 1;
#5 rst_n = 0;
#5 rst_n = 1;
#150 mode = 0;
#1000;
$finish;
end
endmodule
2.4 仿真结果
三、格雷码计数器
3.1 格雷码计数器
- 此处详细可以查看上篇自然二进制数与格雷码转换。
什么是格雷码?
格雷码(Gray code)是一种二进制数码系统,格雷码的特点是从一个数变为相邻的一个数时,只有一个数据位发生跳变,由于这种特点,就可以避免二进制编码计数组合电路中出现的亚稳态。在某些应用中,格雷码具有排除歧义和减少数据传输错误的功能。四位格雷码和自然二进制数关系如下图所示:
自然二进制如何转换成格雷码?
从自然二进制码到格雷码的转换具体方法是:从二进制的最低位起,依次起与相邻左边的一位数进行异或逻辑运算,并且作为对应格雷码该位的值,最高位保持不变。简而言之就是,将二进制码与逻辑右移的二进制码进行异或可得到格雷码。具体原理图如下所示:
格雷码有哪些优点和应用场合?
避免计数器状态的冗余转换,在格雷码中,两个连续的数值仅仅只有一位不同,而在二进制码中两个连续的数值可能会有多位不同,这会导致在计数器发生器中产生大量的冗余状态转换。格雷码可以通过降低状态转换次数来设计出更简单的计数器。
降低传输干扰和误差。在数据传输过程中,如果使用二进制码,由于两个相邻的数值可能会有多位不同,数据在传输过程中可能会因为电磁干扰等原因而发生错误。而使用格雷码则能够避免这种情况,因为任何相邻的两个数值之间只有一位不同。格雷码广泛应用在FIFO、跨时钟域的通信(CDC)、RAM地址寻址计数器、数据纠错等电路设计中。
格雷码的特点决定了它适用于数据传输,比如在异步时钟域之间传递计数结果而用到的计数器。常见的异步FIFO空满信号的信号就是用格雷码进行比较的(因为格雷码计数器计数时相邻的数之间只有一个bit发生了变化,例如:000-001-011-010-110-111-101-100)。
也常用在状态机的状态编码。
而由于格雷码是一种变权码,每一位码没有固定的大小,很难直接进行比较大小和算术运算,因此在实际的数据运算中并不使用格雷码,如异步FIFO中读写地址仍然是使用二进制编码。
格雷码计数可以用三种方式实现
- 一种是状态机,但是如果计数器的位数很大,比如6位,就得用至少64个状态,非常麻烦。
- 一种是设计一个二进制计数器,通过它来计数,然后利用binary-gray的编码就可以得到对应的格雷码计数器。
- 还有一种方法,不是很常用,也很复杂,是通过组合逻辑直接产生格雷码的,而不需要先产生二进制,再进行转换。但是较为复杂,应用场合没前两者多。
本文采用简单易操作的的第二种方法。二进制码转格雷码的基本思路:从最右边一位起,依次将每一位与左边一位异或(XOR),作为对应格雷码该位的值,最左边一位不变。详情可以查看自然二进制数与格雷码转换。
模块的接口信号图如下:
3.2 Verilog代码
要求:实现4bit位宽的格雷码计数器。
//设计四位格雷码计数器
module counter_gray(
input clk,
input rst_n,
output reg [3:0] cnt_gray,
output reg [3:0] cnt_bin
);
//自然二进制计数器
reg [3:0] cnt_bin_r;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_bin_r <= 0;
end
else begin
cnt_bin_r <= cnt_bin_r + 1'b1;
end
end
//自然二进制码转格雷码
wire [3:0] cnt_gray_r;
assign cnt_gray_r = (cnt_bin_r >> 1) ^ cnt_bin_r;
//时序逻辑打一拍子输出
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_gray <= 0;
cnt_bin <= 0;
end
else begin
cnt_gray <= cnt_gray_r;
cnt_bin <= cnt_bin_r;
end
end
endmodule
3.3 Testbench
`timescale 1ns/1ps;//仿真时间单位1ns 仿真时间精度1ps
module counter_gray_tb();
//信号申明
reg clk;
reg rst_n;
wire [3:0] cnt_gray;
wire [3:0] cnt_bin;
//模块实例化(将申明的信号连接起来即可)
counter_gray u_counter_gray(
.clk (clk),
.rst_n (rst_n),
.cnt_gray (cnt_gray),
.cnt_bin (cnt_bin)
);
always #5 clk =~ clk;//生成时钟信号
//为输入数据赋值
initial begin
clk = 0;
#5 rst_n = 1;
#5 rst_n = 0;
#5 rst_n = 1;
#1000;
$finish;
end
endmodule
3.4 仿真结果
四、环形计数器和扭环形计数器
4.1 环形计数器和扭环形计数器
什么是环形计数器?
环形计数器是基于移位寄存器的计数器,对于n个移位寄存器构成的计数器,只有n个有效状态。设置一个初始状态,通过移位即可进行循环。如下图所示为环形计数器的电路结构以及循环的有效和无效编码。
Tips:实际中,因为该计数器有 2 n − n 2^n-n 2n−n 个无效状态,因此存在自锁的问题,这可以通过设计可以自启动(自动从无效状态转移到有效状态,进入有效循环)的电路来解决。自启动的设计可通过修改状态逻辑实现,本质是改变无效状态的次态,使其为有效状态。
什么是独热码?
所谓的独热码是指对任意给定的状态,状态向量中只有1位为1,其余位都是为0。独热码经常用在状态机的状态编码中。n状态的状态机需要n个触发器。当状态机的状态增加时,如果使用二进制编码,那么状态机速度会明显下降,且由于翻转的寄存器较多容易出编码错误。而采用独热码,虽然多用了触发器,但由于状态译码简单,节省和简化了组合逻辑电路。独热编码还具有设计简单、修改灵活、易于综合和调试等优点。对于寄存器数量多、而门逻辑相对缺乏的FPGA器件,采用独热编码可以有效提高电路的速度和可靠性,也有利于提高器件资源的利用率。独热编码有很多无效状态,应该确保状态机一旦进入无效状态时,可以立即跳转到确定的已知状态。通过独热码可是实现简单的有限状态机。
one-hot(独热码)计数器与环形移位计数器实际上相同
独热码只有一位为1,也就是下面的环形计数器产生的计数序列。如4bit one-hot计数器的计术序列即为:0001-0010-0100-1000循环。
这种计数器的优点是速度快,且每次只有两个bit发生跳变,而且不需外加译码电路,可以直接以各个触发器输出端的1状态表示计数。主要缺点是没有有效利用电路的状态,对于 n bit,有 2 n − n 2^n-n 2n−n 个状态没有利用。
应用:在状态机的状态编码时,经常用到。实际上,大多情况下这种独热码计数器不被称作计数器,而是状态编码的一种。
什么是扭环形计数器?
扭环形计数器又称约翰逊计数器,是基于移位寄存器的计数器,是对环形计数器的改进,对于n个移位寄存器构成的计数器,有 2n 个有效状态。如下图所示为扭环形计数器的电路结构以及循环的有效和无效编码。
Tips:与环形计数器类似,实际中,因为该计数器有 2 n − 2 n 2^n-2n 2n−2n 个无效状态,因此存在自锁的问题,这可以通过设计可以自启动(自动从无效状态转移到有效状态,进入有效循环)的电路来解决。自启动的设计可通过修改状态逻辑实现,本质是改变无效状态的次态,使其为有效状态。
扭环形计数器仍然有很多状态是无效的,一旦计数器进入这些状态就会陷入死循环,无法正常工作。计数器的初始状态必须位于有效循环的几种状态之中才能启动。通过添加门电路可以拆掉无效循环,也就是可以自启动的扭环形计数器。
设置一个初始状态,将最高位取反,作为最低位的输入,通过移位即可得到。下面的代码仅仅是简单的实现,模拟环形计数器和扭环形计数器的工作方式,并没有过多的考虑自启动的问题。
4.2 Verilog代码
要求:实现4bit位宽的计数器,可实现环形计数器独热码输出和扭环形计数器(约翰逊计数器)输出。
//设计环形计数器和扭环形计数器
module counter_circle #(
parameter WIDTH = 4 //定义数据位宽
)(
input clk,
input rst_n,
output [WIDTH - 1 : 0] counter_circle,
output [WIDTH - 1 : 0] counter_john
);
//环形计数器(独热码计数器)模块
reg [WIDTH - 1 : 0] counter_circle_r; //中间寄存器
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
counter_circle_r <= 4'b0001;
end
else begin
counter_circle_r <= {counter_circle_r[0],counter_circle_r[WIDTH - 1 : 1]};
end
end
//扭环形计数器(约翰逊计数器)模块
reg [WIDTH - 1 : 0] counter_john_r; //中间寄存器
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
counter_john_r <= 4'b0000;
end
else begin
counter_john_r <= {~counter_john_r[0],counter_john_r[WIDTH - 1 : 1]};
end
end
//组合逻辑输出
assign counter_circle = counter_circle_r;
assign counter_john = counter_john_r;
endmodule
4.3 Testbench
`timescale 1ns/1ps //仿真时间单位1ns 仿真时间精度1ps
module counter_circle_tb #(
parameter WIDTH = 4 //定义数据位宽
);
//信号申明
reg clk;
reg rst_n;
wire [WIDTH - 1 : 0] counter_circle;
wire [WIDTH - 1 : 0] counter_john;
//模块实例化(将申明的信号连接起来即可)
counter_circle u_counter_circle(
.clk (clk),
.rst_n (rst_n),
.counter_circle (counter_circle),
.counter_john (counter_john)
);
always #5 clk = ~clk; //生成时钟信号
//为输入数据赋值
initial begin
clk = 0;
rst_n = 1;
#5 rst_n = 0;
#5 rst_n = 1;
#100 $finsh;
end
endmodule
4.4 仿真结果
五、线性反馈移位寄存器LFSR计数器
5.1 斐波那契LFSR
5.1.1 斐波那契LFSR
斐波那契LFSR为多到一型LFSR,即多个触发器的输出经过异或逻辑来驱动一个触发器的输入。反馈多项式为 f ( x ) = x 3 + x 2 + 1 f(x)=x^3 + x^2 +1 f(x)=x3+x2+1 ,即 x 1 x_1 x1 的输入为 x 3 x_3 x3 和 x 2 x_2 x2 的输出异或后的结果,电路图如下所示:
输出序列的顺序为:111-110-100-001-010-101-011-111
5.1.2 verilog代码
//三级斐波那契LFSR设计
//反馈多项式为 f(x)=x^3 + x^2 +1
module lfsr_fibonacci(
input clk,
input rst_n,
output reg [2:0] q
);
//时序逻辑LFSR移位模块
always @(posedge clk or rst_n) begin
if (!rst_n) begin
q <= 3'b111; //种子值为111
end
else begin
q <= {q[1],q[0],q[1]^q[2]}; //根据三级斐波那契LFSR电路拼接输出
end
end
endmodule
5.1.3 Testbench
`timescale 1ns/1ps //仿真时间单位1ns 仿真时间精度1ps
module lfsr_fibonacci_tb();
//信号申明
reg clk;
reg rst_n;
wire [2:0] q;
//模块实例化(将申明的信号连接起来即可)
lfsr_fibonacci u_lfsr_fibonacci(
.clk (clk),
.rst_n (rst_n),
.q (q)
);
always #5 clk = ~clk; //生成时钟信号
//为输入数据赋值
initial begin
clk = 1;
rst_n = 1;
#5 rst_n = 0;
#5 rst_n = 1;
#1000
$stop;
end
endmodule
5.1.4 仿真结果
5.2 伽罗瓦LFSR
5.2.1 伽罗瓦LFSR
伽罗瓦LFSR为一到多型LFSR,即一个触发器的输出经过异或逻辑来驱动多个触发器的输入。对于同样的反馈多项式 x 3 + x 2 + 1 x^3+x^2+1 x3+x2+1 而言:触发器 x 1 x_1 x1 的输入通常来源于触发器 x 2 x_2 x2 的输出, x 3 x_3 x3 (最高项)的输入通常来自于 x 1 x_1 x1 的输出,此多项式中剩余触发器的输入是 x 1 x_1 x1 的输出与前级输出异或的结果, x 2 x_2 x2 的输入由 x 1 x_1 x1 的输出与 x 3 x_3 x3 的输出通过异或运算得到。其电路图如下所示:
输出序列的顺序为:111-101-100-010-001-110-011-111
5.2.2 verilog代码
//三级伽罗瓦LFSR设计
//反馈多项式为 f(x)=x^3 + x^2 +1
module lfsr_galois(
input clk,
input rst_n,
output reg [2:0] q
);
//时序逻辑LFSR移位模块
always @(posedge clk or rst_n) begin
if (!rst_n) begin
q <= 3'b111; //种子值为111
end
else begin
q <= {q[0],q[2]^q[0],q[1]}; //根据三级伽罗瓦LFSR电路拼接输出
end
end
endmodule
5.2.3 Testbench
`timescale 1ns/1ps //仿真时间单位1ns 仿真时间精度1ps
module lfsr_galois_tb();
//信号申明
reg clk;
reg rst_n;
wire [2:0] q;
//模块实例化(将申明的信号连接起来即可)
lfsr_galois u_lfsr_galois(
.clk (clk),
.rst_n (rst_n),
.q (q)
);
always #5 clk = ~clk; //时钟信号生成
//为输入数据赋值
initial begin
clk = 1;
rst_n = 1;
#5 rst_n = 0;
#5 rst_n = 1;
#1000
$stop;
end
endmodule
5.2.4 仿真结果
六、简易时分秒数字秒表
6.1 简易时分秒数字秒表
其实本质还是二进制计数器,只不过判断逻辑稍微多一些。首先是秒:复位后可以开始计数,当计数器到达最大值即59后清零;其次是分:复位后且只能在秒到达最大值后才能计数,当计数器到达最大值即59后清零。最后是时,复位后且只能在分到达最大值后才能计数,当计数器到达最大值即23后清零。
6.2Verilog代码
要求:实现一个时分秒的简易秒表。
//实现时分秒简易数字秒表
module count_calendar(
input clk,
input rst_n,
output [4:0] hour,
output [5:0] minute,
output [5:0] second
);
//时分秒最大计数
parameter SECOND_MAX = 59;
parameter MINUTE_MAX = 59;
parameter HOUR_MAX = 23;
//秒计数器
reg [5:0] second_r;
always@(posedge clk or negedge rst_n) begin
if (!rst_n) begin
second_r <= 6'd0;
end
else if (second_r == SECOND_MAX) begin
second_r <= 6'd0;
end
else begin
second_r <= second_r + 1'b1;
end
end
//分计数器
reg [5:0] minute_r;
always@(posedge clk or negedge rst_n) begin
if (!rst_n) begin
minute_r <= 6'd0;
end
else if ( (minute_r == MINUTE_MAX) && (second_r == SECOND_MAX) ) begin
minute_r <= 6'd0;
end
else if ( second_r >= SECOND_MAX) begin
minute_r <= minute_r + 1'b1;
end
else begin
minute_r <= minute_r;
end
end
//时计数器
reg [5:0] hour_r;
always@(posedge clk or negedge rst_n) begin
if (!rst_n) begin
hour_r <= 5'd0;
end
else if ( (hour_r == HOUR_MAX) && (minute_r == MINUTE_MAX) && (second_r == SECOND_MAX) ) begin
hour_r <= 5'd0;
end
else if ( (minute_r == MINUTE_MAX) && (second_r == SECOND_MAX) ) begin
hour_r <= hour_r + 1'b1;
end
else begin
hour_r <= hour_r;
end
end
//组合逻辑输出
assign hour = hour_r;
assign minute = minute_r;
assign second = second_r;
endmodule
6.3Testbench
`timescale 1ns/1ps; //仿真时间单位1ns 仿真时间精度1ps
module count_calendar_tb();
//信号申明
reg clk;
reg rst_n;
wire [4:0] hour;
wire [5:0] minute;
wire [5:0] second;
//模块实例化(将申明的信号连接起来即可)
count_calendar u_count_calendar(
.clk (clk),
.rst_n (rst_n),
.hour (hour),
.minute (minute),
.second (second)
);
//调用系统命令——监视
initial begin
$monitor ("hour = %b, minute = %b, second = %b",
hour, minute, second);
end
always #5 clk =~ clk;//生成时钟信号
//为输入数据赋值
initial begin
clk = 0;
#5;
rst_n = 1;
#5;
rst_n=0;
#5;
rst_n = 1;
#100000;
$finish;
end
endmodule
6.4 仿真结果
七、总结
可以将计数器大致分为这几类:
- 普通二进制计数器:大致包括可复位/置数计数器、双向(可加可减)计数器等,这类计数器代码逻辑简单,直观易懂。
- 格雷码计数器:格雷码因翻转的次数少,因此具有减少数据传输错误的作用。格雷码广泛应用在FIFO、跨时钟域的通信(CDC)、RAM地址寻址计数器、数据纠错等电路设计中。产生格雷码有状态机法、自然二进制转格雷码、组合逻辑产生这三种方法。
- 移位寄存器类计数器:包括环形计数器、扭环形计数器(又称约翰逊计数器)、LFSR计数器,三种都可归结于由寄存器与一个反馈回路组成,只不过对于环形计数器,没有反馈回路;对于扭环形计数器,反馈回路只是将最低位取反,作为最高位的输入;而LFSR的反馈回路比较复杂,对于不同的位数,由不同的生成多项式指定。
一定程度上说,生成可循环序列的过程就是计数的过程!
更多可查看个人主页链接
软件版本:Modelsim 10.6c
不定期纠错补充,欢迎随时交流
最后修改日期:2023.6.2