组合逻辑存在竞争冒险
第11讲:寄存器
寄存器具有存储功能,一般是由D触发器构成,由时钟脉冲控制,每个D触发器能够存储一位二进制码。
D触发器的工作原理:在一个脉冲信号(一般为晶振产生的时钟脉冲)上升沿或下降沿的作用下,将信号从输入端D送到输出端Q,如果时钟脉冲的边沿信号未出现,即使输入信号改变,输出信号仍然保持原值,且寄存器拥有复位清零功能,其复位又分为同步复位和异步复位。
模块设计
异步复位解决毛刺的影响,提高了可靠性
异步复位有延迟一拍的效果
波形图绘制
flip_flop.v
`timescale 1ns/1ns
module flip_flop
(
input wire sys_clk , //系统时钟50Mhz,后面我们都是设计的时序电路,所以一定要有时钟,时序电路中几乎所有的信号都是伴随着时钟的沿(上升沿或下降沿,习惯上用上升沿)进行工作的
input wire sys_rst_n , //全局复位,复位信号的主要作用是在系统出现问题是能够回到初始状态,或一些信号的初始化时需要进行复位
input wire key_in , //输入按键
output reg led_out //输出控制led灯
);
//同步复位
//led_out:led灯输出的结果为key_in按键的输入值
always@(posedge sys_clk) //当always块中的敏感列表为检测到sys_clk上升沿时执行下面的语句
if(sys_rst_n == 1'b0) //sys_rst_n为低电平时复位,但是这个复位有个大前提,那就是当sys_clk的上升沿到来时,如果检测到sys_rst_n为低电平则复位有效。
led_out <= 1'b0; //复位的时候一定要给寄存器变量赋一个初值,一般情况下赋值为0(特殊情况除外),在描述时序电路时赋值符号一定要使用“<=”
else
led_out <= key_in;
/*
//异步复位
//led_out:led灯输出的结果为key_in按键的输入值
always@(posedge sys_clk or negedge sys_rst_n) //当always块中的敏感列表为检测到sys_clk上升沿或sys_rst_n下降沿时执行下面的语句
if(sys_rst_n == 1'b0) //sys_rst_n为低电平时复位,且是检测到sys_rst_n的下降沿时立刻复位,不需等待sys_clk的上升沿来到后再复位
led_out <= 1'b0;
else
led_out <= key_in;
*/
endmodule
同步复位RTL视图:
异步复位RTL视图
tb_flip_flop.v
`timescale 1ns/1ns
module tb_flip_flop();
//wire define
wire led_out ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg key_in ;
//初始化系统时钟、全局复位和输入信号
initial begin
sys_clk = 1'b1; //时钟信号的初始化为1,且使用“=”赋值,其他信号的赋值都是用“<=”
sys_rst_n <= 1'b0; //因为低电平复位,所以复位信号的初始化为0
key_in <= 1'b0; //输入信号按键的初始化,为0和1均可
#20
sys_rst_n <= 1'b1; //初始化20ns后,复位释放,因为是低电平复位,所示释放时,把信号拉高,电路开始工作
#210
sys_rst_n <= 1'b0; //为了观察同步复位和异步复位的区别,在复位释放后电路工作210ns后再让复位有效。之所以选择延时210ns而不是200ns或220ns,是因为能够使复位信号在时钟下降沿时复位,能够清晰的看出同步复位和异步复位的差别
#40
sys_rst_n <= 1'b1; //复位40ns后再次让复位释放掉
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk; //使用always产生时钟信号,让时钟每隔10ns反转一次,即一个时钟周期为20ns,换算为频率为50Mhz
//key_in:产生输入随机数,模拟按键的输入情况
always #20 key_in <= {$random} % 2; //取模求余数,产生非负随机数0、1,每隔20ns产生一次随机数(之所以每20ns产生一次随机数而不是之前的每10ns产生一次随机数,是为了在时序逻辑中能够保证key_in信号的变化的时间小于等于时钟的周期,这样就不会产生类似毛刺的变化信号,虽然产生的毛刺在时序电路中也能被滤除掉,但是不便于我们观察波形)
initial begin
$timeformat(-9, 0, "ns", 6);
$monitor("@time %t: key_in=%b led_out=%b", $time, key_in, led_out);
end
//------------- flip_flop_inst -------------
flip_flop flip_flop_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.key_in (key_in ), //input key_in
.led_out (led_out ) //output led_out
);
endmodule
第12讲:阻塞赋值与非阻塞赋值
阻塞赋值的赋值号用“=”表示,对应的电路结构往往与触发沿没有关系,只与输入电平的电话有关系。它的操作可以认为是只有一个步骤的操作,即计算赋值号右边的语句并更新赋值号左边的语句,此时不允许有来自任何其他Verilog语句的干扰,直到现行得到赋值完成,才允许下一条的赋值语句的执行。
串行块(begin-end)中,各条阻塞赋值语句将以它们在顺序块中的排列次序依次执行。
非阻塞赋值的赋值号用“<=”表示,对应的电路结构往往与触发沿有关系,只有在触发沿的时刻才能进行非阻塞赋值。
它的操作可以看作为两个步骤的过程:在赋值开始时刻,计算赋值号右边的语句。在赋值结束时刻,更新赋值号左边的语句。
在计算非阻塞语句赋值号右边的语句和更新赋值号左边的语句期间,允许其他的Verilog语句同时进行操作。
非阻塞操作只能用于对寄存器类型变量进行赋值,因此只能用于“initial”和“always”块中,不允许用于连续赋值“assign”
阻塞赋值例子
blocking.v
`timescale 1ns/1ns
module blocking
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
input wire [1:0] in , //输入信号
output reg [1:0] out //输出信号
);
reg [1:0] in_reg;
//in_reg:给输入信号打一拍
//out:输出控制一个LED灯
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
in_reg = 2'b0;
out = 2'b0;
end
else
begin
in_reg = in;
out = in_reg;
end
endmodule
tb_blocking.v
`timescale 1ns/1ns
module tb_blocking();
wire [1:0] out ;
reg sys_clk ;
reg sys_rst_n ;
reg [1:0] in ;
//初始化系统时钟、全局复位和输入信号
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
in <= 2'b0;
#20;
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//in:产生输入随机数,模拟按键的输入情况
always #20 in <= {$random} % 4; //取模求余数,产生非负随机数0、1,2,3,每隔20ns产生一次随机数
//------------------------ blocking_inst ------------------------
blocking blocking_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.in (in ), //input [1:0] in
.out (out ) //output [1:0] out
);
endmodule
非阻塞赋值例子
non_blocking.v
`timescale 1ns/1ns
module non_blocking
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
input wire [1:0] in , //输入按键
output reg [1:0] out //输出控制led灯
);
reg [1:0] in_reg;
//in_reg:给输入信号打一拍
//out:输出控制一个LED灯
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
in_reg <= 2'b0;
out <= 2'b0;
end
else
begin
in_reg <= in;
out <= in_reg;
end
endmodule
非阻塞的仿真代码与阻塞的一样
第13讲:计数器
计数是一种最简单基本的运算,计数器就是实现这种运算的逻辑电路,计数器在数字系统中主要是对脉冲的个数进行计数,以实现测量、计数和控制的功能,同时兼有分频功能。
计数器在数字系统中应用广泛,如电子计算机的控制器中对指令地址进行计数,以便顺序取出下一条指令,在运算器中作乘法、除法运算时记下加法、减法次数,又如在数字仪器中对脉冲的计数等等。
模块设计
波形绘制:每个周期前0.5秒点亮LED,后0.5秒熄灭
方法1实现:不带标志信号的计数器
`timescale 1ns/1ns
//方法1实现:不带标志信号的计数器
module counter
#(
parameter CNT_MAX = 25'd24_999_999 //这是我们第一次使用参数的方式定义常量,使用参数的方式定义常量有很多好处,如:我们在RTL代码中实例化该模块时,如果需要两个不同计数值的计数器我们不必设计两个模块,而是直接修改参数的值即可;另一个好处是在编写Testbench进行仿真时我们也需要实例化该模块,但是我们需要仿真至少0.5s的时间才能够看出到led_out效果,这会让仿真时间很长,也会导致产生的仿真文件很大,所以我们可以通过直接修改参数的方式来缩短仿真的时间而看到相同的效果,且不会影响到RTL代码模块中的实际值,因为parameter定义的是局部参数,所以只在本模块中有效。为了更好的区分,参数名我们习惯上都要大写
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
output reg led_out //输出控制led灯
);
//reg define
reg [24:0] cnt; //经计算得需要25位宽的寄存器才够500ms
//cnt:计数器计数,当计数到CNT_MAX的值时清零
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX) //新一轮的计数开始
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
//led_out:输出控制一个LED灯,每当计数满标志信号有效时取反
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out <= 1'b0;
else if(cnt == CNT_MAX)
led_out <= ~led_out;
endmodule
tb_counter.v
`timescale 1ns/1ns
module tb_counter();
//wire define
wire led_out ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
//初始化系统时钟、全局复位
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
initial begin
$timeformat(-9, 0, "ns", 6);
$monitor("@time %t: led_out=%b", $time, led_out);
end
//------------- counter_inst --------------
counter
#(
.CNT_MAX (25'd24 ) //实例化带参数的模块时要注意格式,当我们想要修改常数在当前模块的值时,直接在实例化参数名后面的括号内修改即可
)
counter_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.led_out (led_out ) //output led_out
);
endmodule
方法2实现:带标志信号的计数器
//方法2实现:带标志信号的计数器
module counter
#(
parameter CNT_MAX = 25'd24_999_999 //这是我们第一次使用参数的方式定义常量,使用参数的方式定义常量有很多好处,如:我们在RTL代码中实例化该模块时,如果需要两个不同计数值的计数器我们不必设计两个模块,而是直接修改参数的值即可;另一个好处是在编写Testbench进行仿真时我们也需要实例化该模块,但是我们需要仿真至少0.5s的时间才能够看出到led_out效果,这会让仿真时间很长,也会导致产生的仿真文件很大,所以我们可以通过直接修改参数的方式来缩短仿真的时间而看到相同的效果,且不会影响到RTL代码模块中的实际值,因为parameter定义的是局部参数,所以只在本模块中有效。为了更好的区分,参数名我们习惯上都要大写
)
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
output reg led_out //输出控制led灯
);
reg [24:0] cnt; //经计算得需要25位宽的寄存器才够500ms
reg cnt_flag;
//cnt:计数器计数,当计数到CNT_MAX的值时清零
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX)
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
//cnt_flag:计数到最大值产生的标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt == CNT_MAX - 1'b1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
//led_out:输出控制一个LED灯,每当计数满标志信号有效时取反
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out <= 1'b0;
else if(cnt_flag == 1'b1)
led_out <= ~led_out;
endmodule
第14讲:分频器:偶分频
分频器是数字系统设计中最常见的基本电路之一。所谓“分频”,就是把输入信号的频率变成成倍地低于输入频率的输出信号。
分频器分为偶数分频器和奇数分频器,和计数器非常类似,有时候甚至可以说就是一个东西。
模块设计
方法1实现:仅实现分频功能
波形绘制
divider_six.v
`timescale 1ns/1ns
//方法1实现:仅实现分频功能
module divider_six
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
output reg clk_out //对系统时钟6分频后的信号
);
//reg define
reg [1:0] cnt; //用于计数的寄存器
//cnt:计数器从0到2循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 2'b0;
else if(cnt == 2'd2)
cnt <= 2'b0;
else
cnt <= cnt + 1'b1;
//clk_out:6分频50%占空比输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk_out <= 1'b0;
else if(cnt == 2'd2)
clk_out <= ~clk_out;
else
clk_out <= clk_out;
endmodule
`timescale 1ns/1ns
module tb_divider_six();
//wire define
wire clk_out;
//reg define
reg sys_clk;
reg sys_rst_n;
//初始化系统时钟、全局复位
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//------------- divider_six_inst -------------
divider_six divider_six_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.clk_out (clk_out ) //output clk_out
);
endmodule
方法2实现:实用的降频方法
//方法2实现:实用的降频方法
module divider_six(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
output reg clk_flag //指示系统时钟6分频后的脉冲标志信号
);
reg [2:0] cnt; //用于计数的寄存器
//cnt:计数器从0到5循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 3'b0;
else if(cnt == 3'd5)
cnt <= 3'b0;
else
cnt <= cnt + 1'b1;
//clk_flag:脉冲信号指示6分频
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk_flag <= 1'b0;
else if(cnt == 3'd4)
clk_flag <= 1'b1;
else
clk_flag <= 1'b0;
endmodule
第15讲:分频器:奇分频
模块设计
方法1实现:仅实现分频功能
模型绘制
divider_five.v
`timescale 1ns/1ns
//方法1实现:仅实现分频功能
module divider_five
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
output wire clk_out //对系统时钟5分频后的信号
);
//reg define
reg [2:0] cnt;
reg clk1;
reg clk2;
//cnt:上升沿开始从0到4循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 3'b0;
else if(cnt == 3'd4)
cnt <= 3'b0;
else
cnt <= cnt + 1'b1;
//clk1:上升沿触发,占空比高电平维持2个系统时钟周期,低电平维持3个系统时钟周期
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk1 <= 1'b0;
else if(cnt == 3'd2)
clk1 <= 1'b1;
else if(cnt == 3'd4)
clk1 <= 1'b0;
else
clk1 <= clk1;
//clk2:下降沿触发,占空比高电平维持2个系统时钟周期,低电平维持3个系统时钟周期
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk2 <= 1'b0;
else if(cnt == 3'd2)
clk2 <= 1'b1;
else if(cnt == 3'd4)
clk2 <= 1'b0;
else
clk2 <= clk2;
//clk_out:5分频50%占空比输出
assign clk_out = clk1 | clk2;
endmodule
`timescale 1ns/1ns
module tb_divider_five();
//wire define
wire clk_out;
//reg define
reg sys_clk;
reg sys_rst_n;
//初始化系统时钟、全局复位
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//------------- divider_five_inst --------------
divider_five divider_five_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.clk_out (clk_out ) //output clk_out
);
endmodule
方法2实现:实用的降频方法
//方法2实现:实用的降频方法
module divider_five
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
output reg clk_flag //指示系统时钟5分频后的脉冲标志信号
);
reg [2:0] cnt; //用于计数的寄存器
//cnt:计数器从0到4循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 3'b0;
else if(cnt == 3'd4)
cnt <= 3'b0;
else
cnt <= cnt + 1'b1;
//clk_flag:脉冲信号指示5分频
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk_flag <= 1'b0;
else if(cnt == 3'd3)
clk_flag <= 1'b1;
else
clk_flag <= 1'b0;
endmodule
第16讲:按键消抖
按键是最为常见的电子元器件之一,在电子设计中应用广泛;在日常生活中,遥控器、玩具、计算器等等电子产品都使用按键。
在FPGA的实验工程中,我们可以使用其作为系统复位信号或者控制信号的外部输入。
按键消抖主要针对的是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。
因而在闭合及断开的瞬间均伴随着一连串的抖动,为了保证系统能正确识别按键的开关,就必须对按键的抖动进行处理,这就是按键消抖。
模块设计
波形图绘制
key_filter.v
`timescale 1ns/1ns
module key_filter
#(
parameter CNT_MAX = 20'd999_999 //计数器计数最大值
)
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //全局复位
input wire key_in , //按键输入信号
output reg key_flag //key_flag为1时表示消抖后检测到按键被按下
//key_flag为0时表示没有检测到按键被按下
);
//reg define
reg [19:0] cnt_20ms ; //计数器
//cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key_in == 1'b1)
cnt_20ms <= 20'b0;
else if(cnt_20ms == CNT_MAX) //为低电平且已经达到最大值,不再进行清零
cnt_20ms <= CNT_MAX;
else
cnt_20ms <= cnt_20ms + 1'b1;
//key_flag:当计数满20ms后产生按键有效标志位
//且key_flag在999_999时拉高,维持一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX - 1'b1)
key_flag <= 1'b1;
else
key_flag <= 1'b0;
endmodule
tb_key_filter.v
`timescale 1ns/1ns
module tb_key_filter();
//parameter define
//为了缩短仿真时间,我们将参数化的时间值改小
//但位宽依然定义和参数名的值保持一致
//也可以将这些参数值改成和参数名的值一致
parameter CNT_1MS = 8'd19 ,
CNT_11MS = 8'd69 ,
CNT_41MS = 8'd149 ,
CNT_51MS = 8'd199 ,
CNT_60MS = 8'd249 ;
//wire define
wire key_flag ; //消抖后按键信号
//reg define
reg sys_clk ; //仿真时钟信号
reg sys_rst_n ; //仿真复位信号
reg key_in ; //模拟按键输入
reg [7:0] tb_cnt ; //模拟按键抖动计数器
//初始化输入信号
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
key_in <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//tb_cnt:按键过程计数器,通过该计数器的计数时间来模拟按键的抖动过程
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tb_cnt <= 8'b0;
else if(tb_cnt == 8'd249)
//计数器计数到CNT_60MS完成一次按键从按下到释放的整个过程
tb_cnt <= 8'b0;
else
tb_cnt <= tb_cnt + 1'b1;
//key_in:产生输入随机数,模拟按键的输入情况
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_in <= 1'b1; //按键未按下时的状态为为高电平
else if((tb_cnt >= CNT_1MS && tb_cnt <= CNT_11MS) || (tb_cnt >= CNT_41MS && tb_cnt <= CNT_51MS))
//在该计数区间内产生非负随机数0、1来模拟10ms的前抖动和10ms的后抖动
key_in <= {$random} % 2;
else if(tb_cnt >= CNT_11MS && tb_cnt <= CNT_41MS)
key_in <= 1'b1;
//按键经过10ms的前抖动后稳定在低电平,持续时间需大于CNT_MAX
else
key_in <= 1'b0;
//------------------------ key_filter_inst ------------------------
key_filter
#(
.CNT_MAX (20'd24 )
//修改的CNT_MAX值一定要小于(CNT_41MS - CNT_11MS)
//否则就会表现为按键一直处于“抖动状态”而没有“稳定状态”
//无法模拟出按键消抖的效果
)
key_filter_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.key_in (key_in ), //input key_in
.key_flag (key_flag ) //output key_flag
);
endmodule