FPGA实战——按键控制蜂鸣器
实验任务:
复位后蜂鸣器发声,按下按键后停止发声,再次按下继续发声。
蜂鸣器
有源无源的判断:
1.将蜂鸣器引脚朝上,可以看出有绿色电路板的一种是无源蜂鸣器,没有电路板而用黑胶封闭的一种是有源蜂鸣器。
2.万用表电阻档Rxl档: 用黑表笔接蜂鸣器 "+"引脚,红表笔在另一引脚上来回碰触,如果触发出咔、咔声的且电阻只有8Ω(或16Ω)的是无源蜂鸣器;如果能发出持续声音的,且电阻在几百欧以上的,是有源蜂鸣器。
本次实验使用有源蜂鸣器
硬件设计
当BUZZER引脚输出低电平时,PNP导通,蜂鸣器发声工作。
程序设计
rtl文件
这次我们用到了按键消抖,需要单独写一个模块比较方便,所以这节开始,我们就开始用多个.V文件,然后写顶层模块调用例化的底层模块这种写法了。回顾一下例化的知识(伪代码):
比如我已经定义了
<底层led模块>
module pled(
input sys_clk,
output led_value
);
那么我在顶层模块例化他的时候:
<底层模块>
首先定义顶层模块:
module top_flow_led(
input sys_clk,
output led
);
例化led模块时:
pled pled_u(
.sysclk(sys_clk),
.led_value(led)
);
在例化模块中,.sysclk后的()表示的是输入,也就是说,
因为在原来的pled模块中,sysclk是input,所以()中的变量为一个输入,
现在,只需要将开发板上的晶振连接到顶层文件的input sysclk,
然后这个顶层文件的sysclk再输入到led 的sysclk就实现了对底层模块的时钟的赋值。
***********************
而底层模块pled中 led_value是个output,
所以这里.led_value(led)中led是作为led_value的输出接收信号,
接收底层模块的输出,然后通过顶层模块,连接到开发板的LED灯上。
***********************
这就是通过例化连接到顶层模块的写法。具体后面的例子变量多,看起来可能更容易理解。
按键消抖
思想:当检测到按键值发生变化,也就是被按下时,将按键值保存,保存之后,每隔一段时间取按键值与原来值进行比较,如果持续20ms不变,就认为按键确实按下了。
(实际上,只需要检测到按键按下之后,启动计数器(不管怎么抖,都以变化后为计时起点重新计时),只要计数器在计数到20ms之前 ,按键值没有发生变化,就认为按键被按下)
那么我们写一个按键消抖模块,输入key(只用一个按键)输出一个key_flag表示按键被按下的标志,再输出一个key_value来存储key的值。
(一些疑问在最后的 下载与debug解决部分解决)
**************按键消抖模块**************
module key_debounce( //消抖模块
input sys_clk,
input sys_rst_n,
input key,
output reg key_value,
output reg key_flag
);
reg key_reg; //存储key的值,相当于temp的作用
reg [19:0] delay_cnt; //计数器,计数20ms内key未变即按键被按下 20ms / 20ns = 100_0000,20位
//按键延时计数器
always @ (posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
key_reg <= 1'b1; //1为未按下
delay_cnt <= 20'd0; //计数器值清零
end
else begin
key_reg <= key; //把key的值存储到寄存器中去
if(key_reg != key) //说明按键值有变化!!!!!(不管怎么抖,都以变化后为计时起点重新计时)
delay_cnt <= 20'd100_0000; //一旦检测到key值变化,就把计数器设到倒计时最大值。
else if(key_reg == key)begin //说明此时按键还是按下的状态
if(delay_cnt > 20'd0)
delay_cnt <= delay_cnt - 1'b1;
else //else 它要等于自己!!!!!
delay_cnt <= delay_cnt;
end
end
end
//按键抖动判断
always @ (posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
key_value <= 1'b1;
key_flag <= 1'b0;
end
else if(delay_cnt == 20'd1) begin //从100_0000到了1,说明持续了20ms
key_flag <= 1'b1;
key_value <= key; //寄存此时的按键值
end
else begin //计数器减到0
key_flag <= 1'b0; //这是个flag 标志位,不需要一直为1,只需要一个周期即可。
key_value <= key_value;
end
end
endmodule
这里有一点需要讲解一下:
我们在key_reg <= key;之后,马上就判断key_reg和key是否相等,而且FPGA是并行的,这又是非阻塞赋值,那这不是肯定相等吗,其实不然。
always语句块是在posedge sys_clk的时候执行,其他时候不执行,而key_reg <= key; 这句虽然执行了,但是并不是马上把key的值给key_reg,而是需要等下一个clk上升沿才真正赋值,此时key_reg是上一clk的值,key就是实时的按键值,此时可以进行比较。
**************蜂鸣器模块**************
module beep_ctrl(
input sys_clk,
input sys_rst_n,
input key_value,
input key_flag,
output reg beep
);
always @ (posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
beep <= 1'b0;
else if(key_flag && (~key_value))
beep <= ~beep;
end
endmodule
**************顶层模块**************
module top_key_beep(
input sys_clk,
input sys_rst_n,
input key,
output beep
);
//只需要作为导线把两个模块连接到一起即可,不需要是reg型
wire key_value;
wire key_flag;
key_debounce key_debounce_u(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n),
.key (key) ,
.key_value (key_value),
.key_flag (key_flag)
);
beep_ctrl beep_ctrl_u(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.key_value (key_value) ,
.key_flag (key_flag) ,
.beep (beep)
);
endmodule
发现顶层模块并不是top_key_beep,所以我们设置一下,
如果设置完之后没有变化,说明我们的代码有错误,一定要好好检查。比如endmodule会忘记加。(如果代码添加进ise之后发现全编译的绿色按钮灰色,下方工具栏很多没有显示,也可能使endmodule未加)
这样就OK了。
ucf文件
NET sys_clk TNM_NET = sys_clk_pin;
TIMESPEC TS_sys_clk_pin = PERIOD sys_clk_pin 20ns HIGH 50%;
NET sys_clk LOC = T8 | IOSTANDARD = LVCMOS33;
NET sys_rst_n LOC = L3 | IOSTANDARD = LVCMOS33;
################## KEY ############################
NET key LOC = C3 | IOSTANDARD = LVCMOS33;
################## BEEP ############################
NET beep LOC = J11 | IOSTANDARD = LVCMOS33;
编译
编译报错:
Line 29: Target of concurrent assignment or output port connection should be a net type.
原因是:
例化时输出必须为wire类型的!!!!!
RTL图
上次我们用了第一种,是需要我们手动添加的,这次我们用第二个,就不需要手动添加了。
首先是这样的,但是我们还想看内部的图,那么我们可以双击这个rtl图,就可以看到内部
补充——例化模块 的软件操作:
首先两个底层模块加入进ISE,然后创建一个顶层模块。接下来,对于底层模块的例化,我们可以
将这段代码复制进顶层文件即可。
下载及debug
生成bit流文件,就可以下载进开发板了。
发现不能按一下就一直响,响了按一下不能停,问题出在下图那
我们先分析下逻辑,如果计数器从1百万减到1了,就可以把flag变成1,此时让key_value等于0,就是按下了。但是我们忽略了一点,就是按键松开的时候,也需要消抖!!!!那这时候,是不是消抖完还让他等于0呢,不是应该是等于稳定后的key值也就是把这里改成:key_value <=key;
如果按我们刚才那种,那没错,按下去是正常的,但是松开的时候,因为我们flag =1 ,但是按键值key_vlaue却还是0,此时,在下图的逻辑中相当于又按下了一次!!!!
另外,这个bug 也解决了了另外一个问题,就是为什么key_flag只持续了一个clk周期,不能一直等于1,这样对于后面的按键松开消抖不利,实验证明,这样按键会及其不灵敏!!
仿真
tb很简单,思想就是模拟按键抖动即可。
`timescale 1ns / 1ns
module tb_top_key_beep;
// Inputs
reg sys_clk;
reg sys_rst_n;
reg key;
// Outputs
wire beep;
// Instantiate the Unit Under Test (UUT)
top_key_beep u_top_key_beep (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.key(key),
.beep(beep)
);
initial begin
// Initialize Inputs
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
key <= 1'b1;
// Wait 100 ns for global reset to finish
#100;
sys_rst_n <= 1'b1;
// Add stimulus here
#150 key <= 1'b0; // 在第250ns按下按键
#1_000_000 key <= 1'b1; //模拟按键抖动1ms
#1_000_000 key <= 1'b0;
#1_000_000 key <= 1'b1;
#1_000_000 key <= 1'b0;
#21_000_000 key <= 1'b1; //20ms之后松开按键
#1_000_000 key <= 1'b0;
#1_000_000 key <= 1'b1;
#1_000_000 key <= 1'b0;
#1_000_000 key <= 1'b1;
#17_000_000 key <= 1'b0; //开始按下
#1_000_000 key <= 1'b1;
#1_000_000 key <= 1'b0;
#1_000_000 key <= 1'b1;
#1_000_000 key <= 1'b0;
#17_000_000 key <= 1'b1; //松开
#1_000_000 key <= 1'b0;
#1_000_000 key <= 1'b1;
#1_000_000 key <= 1'b0;
#1_000_000 key <= 1'b1;
#20_000_000 key <= 1'b0; //松开
end
//生成时钟
always #10 sys_clk = ~sys_clk;
endmodule
模拟了100ms之后的结果,我设置的是按键按下的延迟先20ms,后两个17ms,再一个20ms,可以看到符合我们的预期,之后前后两个才改变了beep的状态。