FPGA课程设计--电子门锁的设计

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状态。
  当门锁处于ORIGINREADY状态时,可以按下“门铃”按键,进入“响铃”状态(RING状态),此时蜂鸣器就会播放指定的音乐5s,音乐通过代码来编写。音乐播放完成后,返回到ORIGIN状态。
  电子门锁还考虑了省电的需求,当门锁不处于ORIGIN状态时,就会进行10s计时,当十秒内没有按键操作,门锁就会进入ORIGIN状态,数码管和LED指示灯都熄灭,以此来保证省电。
图1-1 数码管的待机状态(不发光)

图1-1 数码管的待机状态(不发光)

图1-2 数码管的准备状态

图1-2 数码管的准备状态

图1-3 数码管的密码输入状态

图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-1 晶振原理图

图2-2 按键原理图

图2-2 按键原理图

图2-3 数码管原理图

图2-3 数码管原理图

图2-4 无源蜂鸣器原理图

图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/CLKout1

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频率计算得出)
各音阶频率对应的分频比及预置数(从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语句结果,在此结构中,实现的是对于correctset_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模块总体的原理框图。模块之间的箭头,表示一种控制关系。

图2-11 Main模块的原理框图
Main模块的原理框图

  • 6
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值