FPGA 学习笔记7(状态机设计实例):因内容比较简单,而且在这篇日志中也有相关的知识点,就不写了。
该集主要知识点:
1、利用状态机实现滤除物理按键所产生的抖动波形。
2、非阻塞赋值的巧妙运用
3、将状态机与计数器功能组合使用
4、在仿真代码中利用随机数产生延时随机的时间。
5、task 运用方式、以及将仿真测试代码模块化
按键抖动的现象与状态机对应的状态:
一、源程序
/* 实验名称:按键消抖模块设计与验证
* 功能实现:滤除按键抖动的波形
*/
`define DEC_tiME_CNT ((20 * 1000 * 1000) / 20 - 1)
module mytest(clk, rst_n, key_in, key_flag, key_state);
input clk, rst_n, key_in;
output reg key_flag, key_state;
reg[3:0] state; // 状态机状态
reg key_old, key_cur; // key状态
wire pedge, nedge; // 边沿状态
// 50MHz的时钟 = 1/50M = 0.02us = 20ns
// 20ms = 20_000_000ns / 20ns = 1000_000
reg[19:0] time_cnt; // 计数器计数值
reg time_full; // 计数器已经达到指定的时间
reg time_en; // 计数器使能
localparam // 状态机几种状态的标志
IDLE = 4'b0001, // 空闲,即高电平状态,为按下状态
DING = 4'b0010, // 按下时抖动状态
DOWN = 4'b0100, // 可以确定是处于按下状态而不是抖动状态
UING = 4'b1000; // 弹起时抖动状态
// 边沿检测
always@(posedge clk, negedge rst_n)
if(!rst_n)begin
key_cur <= 1'b0;
key_old <= 1'b0;
end
else
begin
key_cur <= key_in; // 这里由于采用了非阻塞赋值
key_old <= key_cur; // 所以在同一个时钟内 key_old 采样 key_curr 的是旧的值
end
// 判断上升沿、下降沿
assign pedge = !key_old & key_cur; // 原来为低电平,现在为高电平,则表示检测到上升沿
assign nedge = key_old & !key_cur; // 原来为高电平,现在为低电平,则表示检测到下降沿
// 计数功能
always@(posedge clk, negedge rst_n)
if(!rst_n)begin
time_cnt <= 20'd0;
// time_en <= 1'b0; // 这里不能再次赋值,因为在状态机的程序块中需要对该信号赋值
// 一个信号不能在多个 always 块中赋值
end
else if(time_en)
time_cnt <= time_cnt + 1'b1;
else
time_cnt <= 20'd0;
// 检测时间是否已经到了 这里指定 20ms 的时间
always@(posedge clk, negedge rst_n)
if(!rst_n)
time_full <= 1'b0;
else if(time_cnt == `DEC_TIME_CNT)
time_full <= 1'b1;
else
time_full <= 1'b0;
// 状态机
always@(posedge clk, negedge rst_n)
if(!rst_n)begin
state <= IDLE;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
IDLE: begin // 空闲状态:按键没有被按下
key_flag <= 1'b0; // 在空闲状态 按键需要清零
if(nedge) begin // 检测到下降沿
state <= DING; // 设置状态为 DING,下个时钟上升沿将会进入另外一个分支
time_en <= 1'b1; // 启动定时器
end
else
state <= IDLE; // 依据是高电平,设置状态为 DING
end
DING: begin // 滤波抖动,按下时产生的抖动状态
if(time_full) begin // 如果指定的时间内没有上升沿
key_flag <= 1'b1; // 则表示处于稳定状态,进入按下状态
key_state <= 1'b0; // 表示按下
state <= DOWN; // 设置状态为按下
time_en <= 1'b0; // 关闭定时器
end
else if(pedge) begin // 如果指定的时间内出现上升沿说明是处于抖动状态
state <= IDLE; // 重新设置为空闲状态
time_en = 1'b0; // 并且关闭定时器
end
else // 时间未到,但也没有出现电平变化则进行维持此状态
state <= DING;
end
DOWN: begin // 按键按下状态:此时经过滤波之后处于按下状态
key_flag <= 1'b0; // 将标志位清0
if(pedge) begin // 如果出现上升沿则表示要弹起
state <= UING;
time_en = 1'b1; // 启动定时器
end
else // 如果没有出现上升沿则维持此状态
state <= DOWN;
end
UING: begin
if(time_full)begin // 到达指定时间
key_flag <= 1'b1; // 设置按键标志
key_state <= 1'b1; // 设置按键状态为弹起状态
state <= IDLE; // 将状态设置为空闲
time_en <= 1'b0; // 关闭定时器
end
else if(nedge)begin // 如果出现下降沿
time_en <= 1'b0; // 关闭定时器
state <= DOWN; // 仍设置为按下状态,即跳回之前的状态
end
else
state <= UING; // 如果时间未到并且未出现电平变化则维持此状态
end
default: begin // 如果出现其他状态,意味着***扰出现错误的状态
time_en <= 1'b0; // 关闭定时器
state <= IDLE;
key_flag <= 1'b0; // 设置按键标志
key_state <= 1'b1; // 设置按键状态为弹起状态
end
endcase
end
endmodule
源码中的知识点:
1、一个信号不允许在两个或两个以上的always 程序块中被赋值
2、利用非阻塞赋值语句实现识别上升沿下降沿
// 边沿检测 …
always@(posedge clk, negedge rst_n)
// 代码省略..
begin
key_cur <= key_in;
key_old <= key_cur;
end
// 判断上升沿、下降沿
assign pedge = !key_old & key_cur;
assign nedge = key_old & !key_cur;
3、状态机的使用
二、RTL视图(黄色是状态机模块)
三、状态机
从源码和图中可以看到状态机实际上就是利用多个标志位去对应多种状态,按照我们的逻辑进行组合,每一种状态对应一系列行为,每一种状态都依赖前一种状态的变化,从而实现模拟顺序执行的逻辑。
四、仿真测试代码
/* 实验名称:按键消抖模块的验证
* 功能实现:验证按键消抖模块是否符合设计要求
*/
`timescale 1ns/1ns
`define clock_period 20
module mytest_tb;
reg clk, rst_n, key_in;
wire key_flag, key_state;
mytest u1(clk, rst_n, key_in, key_flag, key_state);
initial clk = 1;
always #(`clock_period / 2) clk = ~clk;
initial begin
rst_n = 1'b0;
key_in = 1'b1;
#(`clock_period * 10) rst_n = 1'b1;
// 延时10时钟周期,再加1ns 错开完整的时钟这样可以更加真实的模拟
#(`clock_period * 10 + 1);
key_Event; // 模拟按键事件
#10000;
key_Event; // 模拟按键事件
#10000;
key_Event; // 模拟按键事件
#10000;
key_Event; // 模拟按键事件
#10000;
key_Event; // 模拟按键事件
#10000;
$stop;
end
reg[15:0] myrand;
// 按键事件
task key_Event;
begin
key_down; // 按键按下
key_up; // 按键弹起
end
endtask
// 按键按下
task key_down;
begin
repeat(50) begin // 模拟按下时的抖动
myrand = {$random} % 65536; // 产生 0 ~ 65535 随机数
//myrand = $random % 65536; // 产生 -65535 ~ 65535 随机数
#myrand key_in = ~key_in;
end
key_in = 0;
#50000000;
end
endtask
// 按键弹起
task key_up;
begin
repeat(50) begin // 模拟弹起时的抖动
myrand = {$random} % 65536; // 产生 0 ~ 65535 随机数
//myrand = $random} % 65536; // 产生 -65535 ~ 65535 随机数
#myrand key_in = ~key_in;
end
key_in = 1;
#50000000;
end
endtask
endmodule
源码中的知识点:
1、随机数的使用
reg[15:0] myrand;
myrand = {$random} % 65536; // 产生 0 ~ 65535 随机数
//myrand = $random % 65536; // 产生 -65535 ~ 65535 随机数
2、task的使用方法,task没有返回值。
3、初始化延时时,不延时一个完整的时钟周期可以更加真实的模拟实际电路的波形
initial begin
rst_n = 1'b0;
key_in = 1'b1;
#(`clock_period * 10) rst_n = 1'b1;
// 延时10时钟周期,再加1ns 错开完整的时钟这样可以更加真实的模拟
#(`clock_period * 10 + 1);
五、波形图
六、波形分析
全局分析
局部分析
七、仿真测试代码模块化
模拟手动按下和释放按键的仿真测试模块(这里我对代码做了一些修改)
1、模拟按键的模块代码
/* 模块名称:模拟物理按键按下释放
* 功能描述:利用 random 产生随机数来产生随机的时间模拟按键抖动
* 端口描述:
* i_key_in: 输入,连接被测试模块所检测的按键信号
* o_key_end: 输出,1:启动模拟 0:模拟完成
*/
`timescale 1ns/1ns
`define clock_period 20
// i_key_in:按键信号输入 o_key_end:启动结束标志
module key_module_tb(i_key_in, o_key_end);
output reg i_key_in;
output reg o_key_end;
initial begin
key_start;
i_key_in = 1'b1;
key_Event;
#10000;
key_Event;
#10000;
key_Event;
#10000;
key_stop;
end
task key_start;
o_key_end <= 1'b1; // 修改端口状态来通知其他模块启动测试
endtask
task key_stop;
o_key_end <= 1'b0; // 修改端口状态来通知其他模块测试结束
endtask
reg[15:0] myrand;
task key_Event;
begin
key_down; // 按键按下
key_up; // 按键弹起
end
endtask
task key_down;
begin
repeat(50) begin // 模拟按下时的抖动
myrand = {$random} % 65536; // 产生 0 ~ 65535 随机数
//myrand = $random} % 65536; // 产生 -65535 ~ 65535 随机数
#myrand i_key_in = ~i_key_in;
end
i_key_in = 0;
#50000000;
end
endtask
task key_up;
begin
repeat(50) begin // 模拟弹起时的抖动
myrand = {$random} % 65536; // 产生 0 ~ 65535 随机数
//myrand = $random} % 65536; // 产生 -65535 ~ 65535 随机数
#myrand i_key_in = ~i_key_in;
end
i_key_in = 1;
#50000000;
end
endtask
endmodule
该仿真代码并不是完全照搬视频中的代码,我觉得既然要将仿真代码模块化,那么就不应该在模块里面调用 $stop 去终止仿真执行。这样顶层仿真模块,调用这些子模块而不会因为子模块调用 $stop 终止仿真,导致之后的仿真无法继续。为此我利用自己目前所掌握的知识做了一点改动,增加了输出端口,这个端口是为了标识仿真模块启动和停止的状态。在顶层模块中只需要通过always检测下降沿就能得知模块什么时候结束。
2、顶层仿真测试代码
/* 实验名称:仿真代码模块化的验证
* 功能实现:验证按键消抖模块是否符合设计要求
*/
`timescale 1ns/1ns
`define clock_period 20
module mytest_tb;
reg clk, rst_n;
wire key_flag, key_state;
wire key_in, key_end;
mytest u1(clk, rst_n, key_in, key_flag, key_state);
key_module_tb key(key_in, key_end);
initial clk = 1;
always #(`clock_period / 2) clk = ~clk;
initial begin
rst_n = 1'b0;
#(`clock_period * 10) rst_n = 1'b1;
// 延时10时钟周期,再加1ns 错开完整的时钟这样可以更加真实的模拟
#(`clock_period * 10 + 1);
end
always@(negedge key_end)
$stop; // 检测到模拟按键的模块完成模拟,所以停止仿真
endmodule
3、波形图
|