0 前言
这是一个关于FPGA的课程设计【只是一个简单的课程设计,并没有涉及很深的FPGA技术知识】,实物测试结果可以参考
FPGA课程设计-电子门锁
视频中使用的板子是睿智助学的开发板,芯片型号为EP4CE6E22C8。大家如果用的是其他开发板也可以参考本文章。除了芯片的资源,本次课设所需要的外部硬件有5个按键,3个LED、4位数码管以及一个无源蜂鸣器。硬件要求其实并不高,对于按键,如果个数过少,可以使用按键模块设计额外的功能,如单击双击和长按,以此来弥补硬件资源的短缺(实现这种功能的方法在文章对应章节有阐述)。LED的作用是指示门锁的当前状态,所以LED数量不够时,也可以根据闪烁时间的来定义不同的状态。数码管的主要作用是用于显示输入的密码数字,所以根据实际的硬件电路,可以更改为显示不同的密码位数,具体根据实际硬件电路进行修改。【当然为了实现功能,至少需要1位数码管】。最后一个无源蜂鸣器,它的作用就是发出不同的声音用于提示用户或者报警。其实使用一个有源蜂鸣器就可以了。我使用无源蜂鸣器的是为了实现蜂鸣器播放一段相应的门铃。因此并不是一定要求使用无源蜂鸣器,有源蜂鸣器也可以使用,只是无法实现门铃的功能。【关于有源蜂鸣器不能实现门铃的功能不是绝对的,在我之前的蓝桥杯的板子中,使用的是有源蜂鸣器也能实现播放音乐,但是这种跟有源蜂鸣器的发声频率有关,如果频率合适,是可以通过开关蜂鸣器的时间,来使蜂鸣器发出不同频率的声音,但是为了音色还得修改波形的占空比。如果频率不合适的话,无论怎么调整都无法发出可以辨别的音乐声音。这种情况可以对波形进行傅里叶分析,观察方波信号中有无对应音阶频率的信号,以及其波形的幅度。具体情况是挺复杂的,我也没完全搞清楚,在什么情况下有源蜂鸣器也能播放人耳可以分辨的音乐声,如果有知道的大佬,也可以在评论区进行讲解。总之,为了能播放合适的音乐,一般就使用无源蜂鸣器模块】。如果你的开发板上是有源蜂鸣器但也想实现门铃的效果,较好的办法就是去淘宝购买一个无源蜂鸣器模块(一般选3.3v那种就可以直接通过芯片供电驱动了),将其接到开发板拓展的引脚接口上,具体怎么接,就需要查询自己开发板的资料了。
课设的代码采用的Verilog语言进行书写,同时使用Intel的Quartus Prime 18.0版本软件进行工程创建,以及模块的仿真。(当然其他的软件也可以,只要能完成最终代码的下载到开发板即可)。因为硬件描述语言VHDL也是常用的语言,但是我常用的Verilog语言,VHDL只是了解过,所以VHDL的语言编写的代码还需要花时间进行修改完善。其实Verilog代码学起来跟简单,它与C语言比较类似,很多关键字都一样,所以学习VHDL的人也可以参考这篇Verilog代码,毕竟只要理解了硬件电路上的描述,换个语言去书写也并不困难。
1 电子门锁的功能介绍
电子门锁实现的功能有:密码输入、密码输入错误报警、密码输入成功提示、密码设置以及门铃功能。使用到的外设有5个按键,3个LED灯,1个无源蜂鸣器和1个四位共阳极8段数码管。
具体实现功能:首先,门锁上电保持待机模式(在代码中命名为ORIGIN状态),数码管不显示,如图1-1所示。当按下“密码输入/设置”按键时,数码管发光,并显示“待输入数字”图样,如图1-2所示(这个状态被命名为READY状态)。然后再短按“密码输入/设置”按键,数码管就会进入“密码输入显示”样式,如图1-3所示,并且当前输入位置的数字会闪烁显示。当需要改变当前位置输入的数字时,可以按下“数字增加”按键,每按一次,输入该位的数字增加1(增加到9后再次按下从0开始循环)。当前位的数字输入完成时,可以再按下“密码输入/设置”按键,切换到下一位的数字输入。当第4位数字输入完成后,如果想修改低位的数字,可以长按“密码输入/设置”按键,就会回到第1位的数字输入,然后重复之前的输入步骤。第4位数字输入完成后,可以短按“密码输入/设置”按键来确定按键输入。
如果密码输入正确,“密码正确”正确指示灯亮起,并且蜂鸣器发出中音1234567的音调;如果密码错误,“密码错误”提示灯亮起,并且蜂鸣器持续发声2s,如果连续输错五次密码,蜂鸣器持续发声时间延长至5s。
如果需要设置密码,在READY状态时,长按“密码输入/设置”按键,就会进入设置密码状态,但前提必须是门处于开启状态,设计中采用一个按键“door”的开关来表示门的开启与关闭,同时有一个LED指示灯来表示门的开启或关闭状态。如果门处于关闭时,在READY状态下长按“密码输入/设置”按键,就会进入“错误”状态(代码中用ERROR表示),同时“密码错误”提示灯亮起,蜂鸣器发出“嘀嘀嘀”的声音。进入“设置密码”状态同输入密码一样,输入完数字之后,就会返回READY状态。
当门锁处于ORIGIN或READY状态时,可以按下“门铃”按键,进入“响铃”状态(RING状态),此时蜂鸣器就会播放指定的音乐5s,音乐通过代码来编写。音乐播放完成后,返回到ORIGIN状态。
电子门锁还考虑了省电的需求,当门锁不处于ORIGIN状态时,就会进行10s计时,当十秒内没有按键操作,门锁就会进入ORIGIN状态,数码管和LED指示灯都熄灭,以此来保证省电。
图1-1 数码管的待机状态(不发光)
图1-2 数码管的准备状态
图1-3 数码管的密码输入状态
2.1 系统功能需求分析
根据开发板的原理图,我的开发板使用的是50MHz的晶振,因此我采用50MHz的晶振作为系统的输入时钟。同时设计时钟分频器产生6MHz,2KHz和16Hz的时钟频率。其中6MHz作为主模块其一的输入时钟,以及蜂鸣器模块产生不同音调的基准频率。2KHz作为按键消抖模块的输入时钟以及主模块其二的输入时钟。16Hz时钟作为控制音符时长的时钟。图2-1 显示的是板子晶振原理图。
电子门锁一共使用了5个按键(如果在实际的产品上只需要4个按键,“door”按键通常用在门锁的实际开关上),按键松开是高电平,按下是低电平。5个按键命名分别为“door”,“enter/set”,“increase”,“ring”和“rst”。图2-2显示的是板子按键的原理图。
电子门锁的显示采用了一个4位8段共阳极数码管,如图2-3所示。因为是共用段码,采用位选来选择点亮某一个数码管,所以需要设计数码管的动态扫描模块。
电子门锁使用一个无源蜂鸣器来产生不同的音调,以便根据不同的状态发出的声音提示用户,提高产品的交互性。图2-4显示的是板子无源蜂鸣器的原理图。
【因为不同的开发板,硬件的资源不同,所以大家在针对自己的开发板编写电子门锁的功能时,要注意根据硬件电路修改代码】
图2-1 晶振原理图
图2-2 按键原理图
图2-3 数码管原理图
图2-4 无源蜂鸣器原理图
2.2 时钟分频器设计
时钟分频模块对系统频率50MHz分频产生其他的频率的原理都一样,这里就以产生6MHz频率的时钟来举例说明。【注意6MHz频率在下面代码是不精确的,因为这个6MHz用于产生不同频率的音符声音,这个频率在6MHz附近即可,在本代码中,实际上是50MHz的8分频6.25MHz,如果你的晶振频率不是50MH,注意在分频器中修改分频系数,使其输出时钟频率接近6MHz】
首先对于通用的时钟分频器的设计,分为偶数分频和奇数分频,此外还有一个小数分频。这里,我采用较为简单的设计方式,分频后时钟的占空比不为50%. 分频的思想也很简单,就是删除输入信号的几个脉冲,做到分频的效果。
CLK6MHz.v代码如下:
//分频器,输出6MHz时钟(实际上是6.25MHz)
module CLK6MHz (
input clk50MHz,
output reg clk6MHz
);
reg[2:0] count;
always @(posedge clk50MHz) begin
if(count == 7) begin
count <= 1'b0;
clk6MHz <= 1'b1;
end
else begin
count <= count + 1'b1;
clk6MHz <= 1'b0;
end
end
endmodule
对于模块实际上产生的是6.25MHz的原因在2.4节进行阐述。
对于计数值 可由下式确定:
C
N
T
=
C
L
K
i
n
/
C
L
K
o
u
t
−
1
CNT\,\,=\,\,CLK_{in}/CLK_{out}-1
CNT=CLKin/CLKout−1
2.3 单次按键消抖模块的原理
机械式的按键,在按下或者松开的时候,芯片读取到该引脚的电平会有短暂的抖动(即高低电平的多次跳变),这对于主模块的按键电平读取是不利的。因为抖动的存在,会在按下按键时读取到错误的电平从而执行了错误的动作,或者某个模块的功能的多次执行。为了让主模块能正常工作就需要对按键进行消抖,去除按键按下和松开前的跳变。
借鉴在51单片机中的按键消抖代码,对于FPGA芯片的按键消抖代码,基本大同小异。设立两个寄存器,用于存储前一次读取到的按键引脚电平和当前按键的引脚电平,然后在输入时钟的每个上升沿读取按键引脚的电平,更新两个寄存器的值。比较两个寄存器的值,如果不一致,则有可能发生按键按下事件,启动计数器开始计数,当计数到一定值时,读取到的电平是低电平(因为该课设使用的开发板上的按键按下为低电平),则输出一个低电平,否则输出一个高电平,然后停止计数。如果在计数的过程中,两个寄存器的值又不一致时,清零计数器,重新计数。
Debounce2.v代码如下:
//按键消抖模块
module Debounce2 (
input clk,//这里输入2KHz
input keyIn,
output reg keyOut
);
parameter CNT = 10;//调整CNT值,得到5ms延迟
reg key_now,key_last;
reg[31:0] cnt;
always @(posedge clk) begin
{key_last,key_now} <= {key_now,keyIn};
end
always @(posedge clk) begin
if(key_last^key_now) cnt <= 0;//两次读取的值不一样,代表按键可能被按下,开始计数判断
else if(cnt == CNT) cnt <= cnt;//计数到一定值时,不再计数
else cnt <= cnt + 1;
end
//按键未被按下,默认输出高电平,判断按下后输出低电平
//本模块,只会判断是否按下按键,不管按下后有没有松开,只要判断按下都会输出一个时钟的低电平脉冲
always @(posedge clk) begin
if(cnt == CNT-1&& key_last == 1'b0) keyOut <= 0;//仅在一个时钟周期产生脉冲,达到消抖效果
else keyOut <= 1;//默认按键输出高电平
end
endmodule
2.4 长按短按按键消抖模块的原理
电子门锁系统有一个按键设置短按与长按,因此需要一个长按与短按的消抖模块。短按消抖的原理同2.3节原理类似,不同的地方在于,短按是按键松开才会判定为短按,长按是当确定按键按下后就会开始计数,如果计数值持续到一定值判定为长按。因此在该模块中存在两个计数器,一个是用于消抖的计数,另一个则是判断按键长按与短按的计数。从代码中也可以看出当保持一直按下按键时,模块会周期性的发出长按的高电平。
Debounce.v代码如下:
//按键消抖模块
module Debounce (
input clk,//这里输入2KHz
input keyIn,
output reg keySingle,
output reg keyLong
);
parameter CNT = 10;//调整CNT值,得到5ms延迟
reg key_now,key_last;
reg[31:0] cnt;
reg[31:0] cnt2;
reg down;
reg again;
always @(posedge clk) begin
{key_last,key_now} <= {key_now,keyIn};
end
always @(posedge clk) begin
if(key_last^key_now) cnt <= 0;//两次读取的值不一样,代表按键可能被按下,开始计数判断
else if(cnt == CNT) begin
if(!key_last) down <= 1'b1;//按键按下
else down <= 1'b0;
cnt <= cnt;//计数到一定值时,不再计数
end
else cnt <= cnt + 1'b1;
end
always @(posedge clk) begin
if(down)begin
if(cnt2 == 4000) begin cnt2 <= 1'b0; keyLong <= 1'b0; again <= 1'b1; end
else begin cnt2 <= cnt2 + 1'b1; keyLong <= 1'b1; end
keySingle <= 1'b1;
end
else begin
if(cnt2 == 4000) begin keyLong <= 1'b0;keySingle <= 1'b1; end
//cnt2有计数值且长按没有触发,那么松开按键触发单击
else if(cnt2 && (again == 0)) begin keyLong <= 1'b1; keySingle <= 1'b0; end
else begin keyLong <= 1'b1; keySingle <= 1'b1; end
cnt2 <= 1'b0;
again <= 1'b0;
end
end
endmodule
2.5 蜂鸣器模块的原理
蜂鸣器模块的重点在于产生不同频率的音调。因此蜂鸣器模块本质上就是一个时钟分频器,只不过它是一个动态的分频器,根据音符的不同,改变分频比,以产生不同的频率。
要让蜂鸣器发出指定的音符,就需要对音符有所了解。音乐中,每两个八度音(如
1
1
1 和
1
˙
\dot{1}
1˙)之间的频率相差一倍。两个八度音之间又可分为12个半音,每两个半音的频率之比为
2
12
\sqrt[12]{2}
122 。如表2-5就是简谱中的音名与频率的对应关系。所有不同的频率的信号都是从一个基准频率分频得到的,由于音阶的频率大部分为小数,而分频比必须为整数,所以必须要对分频比四舍五入取整,这就会导致误差的产生。为了防止误差过大,导致发出的声音跑调,选取6MHz作为基准频率,以此来分频。不过由于系统晶振的频率是50MHz,所以就选取了距离6MHz较近的6.25MHz频率来作为基准频率,因为各音符之间频率比仍保持不变,所以发出的声调听起来也不会跑调。
为了减小输出的偶次谐波分量,最后输出到蜂鸣器的波形应为对称方波,因此在扬声器之前有一个二分频器。表2-6中的分频比就是6MHz二分频得到的3MHz频率的基础上得到的。从表格中可以看出,最大的分频数位11468,故采用14位二进制计数器分频即可满足需求。除了给出分频比外,还给出了各个音符频率时的预置数,对于不同的分频系数,只要加载不同的预置数即可。对于乐曲中的休止符,只要将分频数设为0,蜂鸣器就不会发声。
有了音符的频率还不够,还需要音符的时值。也就是音符的持续时间。
对于门铃声,选取了一首东方Project里的一首音乐并截取了其中一部分作为铃声,曲子如图2-7所示。
曲谱中
1
=
E
b
4
4
♩
=
120
1=\mathrm{Eb}\frac{4}{4}\,\,♩=120
1=Eb44♩=120,表示曲子的调是Eb调,后面的
4
4
\frac{4}{4}
44指的是以四分音符为一拍,一小节4拍。
♩
=
120
♩=120
♩=120表示一分钟120拍,所以在我选的这个曲子里,一个四分音符的时值为0.5s.
在曲子中类似
2
˙
\dot{2}
2˙这种音符下面没有横线的就是四分音符,而像
6
˙
‾
\underline{\dot{6}}
6˙这样的音符就是八分音符,以此类推
6
˙
‾
‾
\underline{\underline{\dot{6}}}
6˙是十六分音符,
7
˙
‾
‾
‾
\underline{\underline{\underline{\dot{7}}}}
7˙是三十二分音符。除此之外就是形如
6
−
6-
6−的二分音符,以及
6
−
−
−
6---
6−−−的全音符。
这些音符的时值计算方法如下:
设四分音符的时值为
t
t
t,则八分音符的时值为
t
/
2
t/2
t/2,十六分音符时值
t
/
4
t/4
t/4,三十二分音符的时值为
t
/
8
t/8
t/8,二分音符时值为
2
t
2t
2t,全音符为
4
t
4t
4t .
在简谱中,除了这些音符,还有附点“.”,如
3
˙
.
\dot{3}.
3˙. 后面的小点就是附点。有附点音符,附点的时值为跟随音符时值的一半。所以
3
˙
.
\dot{3}.
3˙. 的时值为
0.5
+
0.5
/
2
=
0.75
s
0.5+0.5/2=0.75s
0.5+0.5/2=0.75s.
因为所选简谱里有三十二分音符,三十二分音符的时值为
0.0625
s
0.0625s
0.0625s ,所以需要16Hz的时钟频率产生音符的时长。
该模块的原理框图可以分解为图2-8的所示的功能模块。
表2-5 简谱中的音名与频率的对应关系
表2-6 各音阶频率对应的分频比及预置数(从3MHz频率计算得出)
图2-8 蜂鸣器的功能模块
图2-7 碎月简谱
2.6 数码管显示模块的原理
板子采用的是4位共阳极数码管,要让数码管显示数字就要采取动态扫描的方式。对于输入的4位BCD码,首先要先进行译码,将其转换为8位共阳极数码管段码,然后根据模块输入时钟,在不同时刻点亮不同的数码管位,已经对应位的数字。这里使用2KHz的时钟来实现其功能,使用一个计数器来对输入时钟进行计数,并根据计数值的不同,在不同的时刻点亮不同的数码管,由于人眼的视觉暂存,就会感觉数码管是同时点亮的。
此模块内部还有一个小的解码模块,实现的功能就是对BCD码进行译码。
LEDshow.v代码如下:
module LEDShow (
input clk,//输入时钟速度不能过快也不能过慢,过快看不到数码管闪烁,会有鬼影,太慢就不能同时显示
input[3:0]num1,num2,num3,num4,
output reg[7:0] seg,//共阳极数码管段码
output reg[3:0] sel //位选
);
reg[3:0] counter;
wire[7:0] code1,code2,code3,code4;
//解码模块
Decode myDecode1(num1,code1);
Decode myDecode2(num2,code2);
Decode myDecode3(num3,code3);
Decode myDecode4(num4,code4);
//数码管的动态扫描
always @(posedge clk) begin
if(counter == 4) counter <= 1'b0;
else counter <= counter + 1'b1;
end
always @(*) begin
case (counter)
0: begin seg <= code1; sel <= 4'b1110; end
1: begin seg <= code2; sel <= 4'b1101; end
2: begin seg <= code3; sel <= 4'b1011; end
3: begin seg <= code4; sel <= 4'b0111; end
default: begin seg <= 8'b1111_1111; sel <= 4'b1111; end
endcase
end
endmodule
Decode.v代码如下:
module Decode (
input[3:0] data,
output reg[7:0] code//共阳极数码管段码各位从高到低依次是abcdefg
);
parameter NOSHOW = 4'b1111,WAIT = 4'b1110;
//对于data只翻译0-9,E和F有其他含义
always @(*) begin
case (data)
0: code <= 8'b0000_0011;
1: code <= 8'b1001_1111;
2: code <= 8'b0010_0101;
3: code <= 8'b0000_1101;
4: code <= 8'b1001_1001;
5: code <= 8'b0100_1001;
6: code <= 8'b0100_0001;
7: code <= 8'b0001_1111;
8: code <= 8'b0000_0001;
9: code <= 8'b0000_1001;
WAIT: code <= 8'b1110_1111;
NOSHOW: code <= 8'b1111_1111;
default: code <= 8'b1111_1111;
endcase
end
endmodule
2.7 定时器模块
定时器模块的原理框图如图2-9所示
图 2-9定时器的原理框图
定时器模块采用的向下计数,当定时器使能时,在每一个时钟周期,定时器的计数值自减1。数值为0时,定时器的溢出标志位置1,同时将预置数重新装载到定时器的内部计数寄存器,并且重新开始计数。重新计数的下一个时钟周期,定时器会自动清除定时器的溢出标志位。如果定时器不使能,则自动将定时器的溢出标志清零。当定时器复位时,溢出标志清零,将预置数重新装载到内部计数寄存器。
Timer.v代码如下:
//定时器模块
module Timer (
input clk2KHz,
input timer_en,//定时器使能
input[15:0] value,//
input rst,
output reg timer_up//计时结束标志
);
reg[15:0] timer_count;//定时器计数值
always @(posedge clk2KHz,negedge rst) begin
if(!rst) begin
timer_count <= value;
timer_up <= 1'b0;
end
else if(timer_en) begin
if(timer_count == 1'b0) begin
timer_count <= value;
timer_up <= 1'b1;
end
else begin
timer_count <= timer_count - 1'b1;
timer_up <= 1'b0;
end
end
else begin
timer_count <= value;
timer_up <= 1'b0;
end
end
endmodule
2.8 主模块的说明
主模块完成的是电子门锁的主要功能,即密码输入,密码设置,门铃,密码判断。主模块采用的是状态机方法来编程,状态机各状态说明如表2-10所示。
表 2-10状态机各状态说明
Main模块的代码设计采取了高可读性的写法,每一个always语句只对一个到两个变量进行赋值。这样就使代码的逻辑清晰,各个模块的功能明确。这里对Main模块内的代码作详细说明。
首先第一个always语句是关于状态变量state切换的语句。也是状态机的关键语句,这里执行state的切换动作,
always @(posedge clk2KHz,negedge rst) begin
if(!rst) begin state <= ORIGIN; <...> end
else if(time10s_up) begin state <= ORIGIN; <....> end
else begin
case (state)
ORIGIN:<...>
READY:<...>
S0:<...>
S1:<...>
S2:<...>
S3:<...>
CHECK:<...>
SET:<...>
SUCCESS:<...>
FAILURE:<...>
ERROR:<...>
RING:<...>
default: <...>
endcase
end
end
上面的代码作了简化,采用<...>
方式来简化不重要的代码,突出主要部分。从这个always语句可以看到,该always语句一共有12个状态。系统复位是ORGIN状态。剩下的状态语句中写的就是各状态之间的切换。同时语句里有if(time10s_up)
表示是10秒倒计时结束后,就进入待机省电状态,也就是ORIGIN状态。
ORIGIN: begin
if(!enter || !set) state <= READY;
else if(!ring) state <= RING;
else state <= state;
end
这是ORIGIN状态的状态切换代码,(!enter || !set)
表示当按键enter或者按键set按下时,状态从ORGIN状态切换到READY状态。虽然这里有两个按键但在系统的实现上,采用一个按键的短按长按来实现两个按键的功能。(!ring)
表示当按键ring按下时,进入RING状态,也就是响门铃状态。这里的代码存在优先级,先会判断按键enter和set是否按下,然后才会判断ring按键会不会按下,这在实际使用也不会影响电子门锁的使用,所以采用这样的if语句无伤大雅。
READY: begin
if(!ring) state <= RING;
else begin
case ({door,enter,set})
3'b101:begin state <= S0;opening_setting <= 1'b0;end
3'b110:begin state <= ERROR;opening_setting <= 1'b0;end
3'b010:begin state <= S0;opening_setting <= 1'b1;end
default:begin state <= state;opening_setting <= 1'b0;end
endcase
end
end
这段是READY状态的切换代码,在READY状态按下按键ring也会进入门铃状态。而没有按下ring但按下按键enter或set时,则采用一个case
语句来判断。这个case
语句主要实现的功能,判断按键按下的合法性。如果门是关闭的,(这里采用door的高点电平来表示门的开启与关闭,door实际上按键的输入,当门是关闭的,door是高电平,门开启时,door就是低电平),按下按键enter时就会进入输入密码状态,否则按下按键set就会进入ERROR状态。只有门开启时,按下按键set进入设置密码状态,除此之外的状态都是不予处理。考虑到输入密码和设置密码都是输入4位数字,所以让这两个功能共用S0,S1,S2,S3四个输入数字状态,并且设置一个标志位opening_setting
来表示进入的密码输入功能还是密码设置功能。该标志位用于在S3状态的判断。
S0: begin if(!enter || !set) state <= S1;
else state <= state;
end
因为S0,S1,S2三个状态类似,所以这里只展示一个状态S0的切换代码。当处于S0状态时,如果按下按键enter或set就会切换到S1状态,也就是切换到下一位数字的输入。否则就在S0状态,等待用户第一位数字输入完成。
S3: begin
case ({opening_setting,enter,set})
3'b001: state <= CHECK;
3'b010: state <= S0;
3'b101: state <= SET;
3'b110: state <= S0;
default: state <= state;
endcase
end
S3是一个分界点,它根据不同的按键输入以及opening_setting
标志,进入不同的状态。当opening_setting=0
,即当前是输入密码状态,按下按键enter就会确认当前输入的4位密码,并进入CHECK状态进行密码的判断,而如果opening_setting=1
,即当前状态是设置密码状态,按下按键enter就会确认当前设置的密码,并进入密码设置更新状态(SET状态)。如果按下的是按键set,则不管当前是输入密码状态或是设置密码状态,都会返回第一位数字,从第一位重新开始输入数字,用于数字输入错误的更改。
CHECK:begin
//密码输入正确,密码错误次数清零
if(correct) begin state <= SUCCESS; fail_count <= 1'b0; end
else begin
//累加到5次,次数不再累加
if(fail_count == 5) fail_count <= fail_count;
//密码错误,错误次数累加
else fail_count <= fail_count + 1'b1;
state <= FAILURE;
end
end
CHECK状态是一个暂态,也就是相较于其他状态,不会停留太长时间,一个时钟周期之后就会切换到下一个状态。CHECK状态用于判断在密码输入状态,确认输入的密码正确与否。如果输入正确,就进入SUCCESS状态,如果错误就进入FAILURE状态,同时密码错误计数器fail_count
加1。之所以在此状态进行错误次数累加,一个重要的原因就是CHECK状态是暂态,在下一个时钟周期之后就会被切换。如果将fail_count
语句写在FAILURE状态里,因为FAILURE状态有蜂鸣器播放,需要延迟2s才能切换到下一状态,所以停留在FAILURE状态时,会在每一个时钟周期都会累加fail_count
,导致fail_count
计数异常,瞬间就达到了5次计数上限,最终使蜂鸣器发声时长变长。
SET:begin
if (set_OK) state <= READY;
else state <= state;
end
SET状态也是一个暂态,代码如上,只是设置完成后,set_OK
标志置1,然后返回到READY状态。
FAILURE:begin
if(fail_count == 5)begin
if(delay_5s) state <= READY;
else state <= state;
end
else begin
if(delay_2s) state <= READY;
else state <= state;
end
end
因为SUCCESS,ERROR,RING状态都与FAILURE类似,所以只针对FAILURE状态进行说明。在FAILURE状态,首先判断fail_count
的值,如果fail_count
的值达到5次,说明密码输入错误次数达到5次,因此要等待5s之后才能返回READY状态重新输入密码,在这5s的等待时间里,蜂鸣器也会持续发声。当错误次数少于五次时,需要等待2s才能返回READY状态重新输入密码,在这2s内蜂鸣器也会发声。
在RING状态,相较于其他的状态,唯一的区别就是可以在此状态按下任意按键都可以结束RING状态,回到ORIGIN状态。这里考虑到的是,门铃按键的误触,方便使用者退出门铃状态。
在上面的状态切换从可以看到,有好几个状态都需要使用延迟,所以参考了51定时器的设计,在Main模块中调用了自己设计的定时器模块。
在Main模块一共用到了4个定时器,4个模块的使用方式大同小异,所以这里只针对两个定时器模块进行描述。
//10s倒计时控制部分(无操作时就开始计数)
//time10s_up赋值部分
Timer Timer4(clk2KHz,Timer4_en,20_000,
Timer4_rst,time10s_up);
//如果不在初始状态 就启动定时器4 定时4s
always @(posedge clk6MHz,negedge rst) begin
if(!rst) Timer4_en <= 1'b0;
else begin
if(state == ORIGIN) Timer4_en <= 1'b0;
else begin
Timer4_en <= 1'b1;
//如果按下任意按钮,就复位Timer4 重新计时
if(!(key1 && enter && set)) Timer4_rst <= 1'b0;
else Timer4_rst <= 1'b1;
end
end
end
定时器4的作用是进行10s倒计时定时,当10s倒计时结束后,就会使门锁回到ORIGIN状态。当门锁处于非ORIGIN状态时,定时器4就会被启动,并且无法关闭,只有进入ORIGIN状态,定时器4才会被关闭。而在定时器4的计时过程中,任意按键按下就会使定时器4复位,复位的效果是,让定时器4的预置数重新装载到计数寄存器上,也就是让定时器4重新开始计数,这就达到了按下任意按键,让门锁的重新开始10s倒计时熄屏的功能。
定时器1的作用是计时0.5s用于数码管特定位的闪烁。
//0.5s计时
Timer Timer1(clk2KHz,Timer1_en,1000,1,Timer1_up);
//在S0 S1 S2 S3 SUCCESS FAILURE ERROR 状态 启动定时器1 定时0.5s
always @(posedge clk6MHz,negedge rst) begin
if(!rst) Timer1_en <= 1'b0;
else begin
case (state)
S0: Timer1_en <= 1'b1;
S1: Timer1_en <= 1'b1;
S2: Timer1_en <= 1'b1;
S3: Timer1_en <= 1'b1;
SUCCESS:Timer1_en <= 1'b1;
FAILURE:Timer1_en <= 1'b1;
ERROR: Timer1_en <= 1'b1;
default: Timer1_en <= 1'b0;
endcase
end
end
//根据定时器1的定时溢出标志 翻转shed 用于闪烁数码管
always @(posedge Timer1_up) begin
shed <= !shed;
end
因为定时器1在计时的过程中,溢出之后,会自动清除定时器的溢出标志位,所以可以实现连续的定时,因此根据定时器的溢出标志,shed
可以以0.5s间隔翻转电平,从而使数码管以1s为周期进行闪烁。
数码管的显示在另一个always语句中进行控制。这样的写法就体现了高可读性,各always语句分工明确,也利于代码检查,避免将多个模块的功能写在一起导致功能混杂。
//数码管根据状态输出
always @(posedge clk2KHz) begin
case (state)
//初始状态 数码管不显示
ORIGIN:begin <...> end
//就绪状态 数码管显示就绪样式
READY:begin <...> end
//S0状态 数码管的第一位数字闪烁
S0:begin <...> end
//S1状态 数码管的第二位数字闪烁
S1:begin <...> end
//S2状态 数码管的第三位数字闪烁
S2:begin <...> end
//S3状态 数码管的第四位数字闪烁
S3:begin <...> end
//密码输入正确状态 4位数字状态
SUCCESS:begin <...> end
//密码输入错误状态 就绪状态显示的数码管闪烁显示
FAILURE:begin <...> end
//错误状态同上
ERROR:begin <...> end
//默认状态不显示数码管
default:begin <...> end
endcase
end
//S0状态 数码管的第一位数字闪烁
S0:begin
if(shed) num1 <= NOSHOW;
else num1 <= data1;
num2 <= data2;
num3 <= data3;
num4 <= data4;
end
这里只展示S0状态,其他状态与此类似。S0状态,第一位数字会根据shed
的值,显示第一位数字或者不显示数字。因此借助shed
的标志很容易做到根据当前输入的不同位数字,让相应位的数字进行闪烁。
接下来就是针对数字输入设计的always语句。在此语句中,完成的功能是对datax数据的赋值,赋值方式通过,通过同步方式实现。也就是在每一个时钟上升沿读取“数字输入”按键电平,如果为低电平,即按键按下,相应的datax
的数字自加1,当值为9时,从0开始增加。Always的语句结构同上面的语句结构类似,就不再展示,这里仅展示,某一状态的执行语句。
S1:begin
if(!key1)begin
if(data2 == 9) data2 <= 1'b0;
else data2 <= data2 + 1'b1;
end
else data2 <= data2;
data1 <= data1;
data3 <= data3;
data4 <= data4;
end
下面的是密码设置与判断always语句结果,在此结构中,实现的是对于correct
和set_OK
以及存储密码的赋值。同样是根据不同的状态来执行相应的操作。以下只粘贴代码,就不再赘述。
//检查状态,检查输入的密码是否正确
//对correct,set_OK赋值
always @(posedge clk6MHz,negedge rst) begin
if(!rst)begin
correct <= 1'b0;
set_OK <= 1'b0;
pass1 <= 1'b0;
pass2 <= 1'b0;
pass3 <= 1'b0;
pass4 <= 1'b0;
end
else begin
case (state)
CHECK:begin
if({data1,data2,data3,data4} == {pass1,pass2,pass3,pass4}) correct <= 1'b1;
else correct <= 1'b0;
end
SET:begin
{pass1,pass2,pass3,pass4} <= {data1,data2,data3,data4};
set_OK <= 1'b1;
end
default: begin correct <= 1'b0; set_OK <= 1'b0; end
endcase
end
end
下面always语句,完成的功能是控制LED指示灯在对应状态的亮起,以及蜂鸣器发出相应的声音。结构也与之前一样是采用的case
语句,根据不同状态,来执行相应的动作。这里也不再赘述,只粘贴某一状态的具体执行语句。
SUCCESS:begin
warning <= 1'b1;//密码错误警告灯熄灭
buzzer_en <= 1'b1;//使能蜂鸣器
sound <= sound_success;//播放密码输入成功音频
pass <= 1'b0;//显示密码正确提示灯
end
最后一个always就比较简单,完成的是根据门的开关亮起对应的指示灯。
always @(*) begin
door_opened <= door;//door_opened显示门的开关
end
图2-11展示的就是Main模块总体的原理框图。模块之间的箭头,表示一种控制关系。文章来源:https://www.toymoban.com/news/detail-757765.html
图2-11 Main模块的原理框图
文章来源地址https://www.toymoban.com/news/detail-757765.html
到了这里,关于FPGA课程设计--电子门锁的设计的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!