基于EP4CE6F17C8的FPGA键控灯实例

一、电路模块

1、LED

开发板板载了4个用户LED发光二极管,其原理图如下所示,当 FPGA的引脚输出为逻辑 0时,LED会熄灭。输出为逻辑1时,LED被点亮。

其实物图如下所示。

LED的引脚分配见下表。

2、时钟晶振

开发板板载了一个50MHz的有源晶振,为系统提供时钟。

其实物图如下所示。

时钟输出引脚分配见下表。

3、按键

开发板板载了4个独立按键,其中有3个用户按键(KEY1~KEY3),1个功能按键(RESET)。按键按下为低电平(0),释放为高电平(1),4个按键的原理图如下图所示。

其实物图如下所示。

按键的引脚分配见下表。

 

二、实验代码

本例实现通过3个按键控制3个LED的亮灭,按键按下对应的LED点亮,再次按下LED熄灭,如此循环。模块名称为key_led,文件名称为key_led.v,设置为顶层模块,代码如下。

module key_led(
            input               clk,                  //板载50HMz系统时钟
            input               rst,                  //复位按键
            input [2:0]         key_h,                //三个用户按键
            output reg[2:0]     led                   //三个LED
            );

//按键抖动判断逻辑
wire    key = key_h[0] & key_h[1] & key_h[2];         //定义三个按键相与

reg[1:0] keyr;                                        //定义按键存储变量
always@(posedge clk or negedge rst)                   //敏感信号为时钟上沿或复位下沿
begin
    if(!rst)                                          //低电平复位
        keyr <= 2'b11;                                //复位时按键值为全1
    else
      keyr <= {keyr[0], key};                         //并位,相当于每个时钟之后用key值向左填充keyr
end

wire    key_neg = ~keyr[0] & keyr[1];                 //按键按下之后判定有下降沿
wire    key_pos = keyr[0] & ~keyr[1];                 //按键释放之后判定有上升沿

//定时计数20ms时间,用于对按键的消抖判断
reg[19:0]    cnt;
always@(posedge clk)
begin
     if(!rst)                                         //低电平复位时计数值清零
            cnt <= 20'd0;
    if(key_pos || key_neg)                            //如果有上升沿或下降沿发生,计数值清零
            cnt <= 20'd0;
    else if(cnt < 20'd999_999)                        //如果未计到20ms,则继续加1计数
            cnt <= cnt + 20'd1;
    else
            cnt <= 20'd0;                             //到20ms,计数值清零        
end

//定时采集按键值
reg[2:0]    key_halue[1:0];                           //定义两个健值存储变量
always@(posedge clk or negedge rst)                   //敏感信号为时钟上沿或复位下沿
begin
    begin
         if(!rst)                                     //低电平复位时健值变量全部置1
        begin
            key_halue[0] <= 3'b111;
            key_halue[1] <= 3'b111;
        end
        else
        begin
        key_halue[1] <= key_halue[0];                 //两次键值相差一个时钟节拍,用于在不相同时产生一个变化脉冲
        if(cnt == 20'd999_999)
                key_halue[0] <= key_h;                //到20ms后,获取外部按键值
        end
    end
end
/*key_halue[1] <= key_halue[0]一个时钟执行一次,当到20ms时,执行了key_halue[0] <= key_h,此时
key_halue[1]与key_halue[0]的值是不同的,直到下一个时钟到来时,才相同。因此,通过下面语句会产生
出一个时钟周期的窄脉冲,依靠这个窄脉冲,就可判断是哪个按键被按下了。由于些窄脉冲的宽度只有一个时钟
周期,所以本次时钟过后,又恢复低电平状态,因此不会被反复触发。且在释放按键时,不会产生此窄脉冲。 */
wire[2:0]    key_press = key_halue[1] & ~key_halue[0];    //按键值按下时产生一个变化脉冲
//wire[3:0]    key_press = ~key_halue[1] & key_halue[0];  //按键值释放时产生一个变化脉冲

//LED控制
always@(posedge clk or negedge rst)                   //敏感信号为时钟上沿或复位下沿
begin
     if(!rst)                                         //低电平复位时LED全灭
            led <= 3'b000;
    else if(key_press[0])                             //若按键0按下,则LED0取反
            led[0] <= ~led[0];
    else if(key_press[1])                             //若按键1按下,则LED1取反
            led[1] <= ~led[1];
    else if(key_press[2])                             //若按键2按下,则LED2取反
            led[2] <= ~led[2];
end
endmodule

三、代码说明

1、本例主要讨论按键消抖的设计方法,其主要思想为,当按键按下时,进行20毫秒的延时,若在20毫秒内检测到按键的边沿,则计数清零,重新开始延时,直到20毫秒结束,则获取键。
2、为了判断是否有键按下,先把三个按键值相与,若结果key为0,则表明有键被按下。
3、为了产生电平跳变的边沿,先进行keyr <= {keyr[0], key}操作,相当于每个时钟来时用key值向左填充keyr。由于keyr的初始值为11,当key等于1时,相当于没有变化,即没有按键按下。当key等于0时,并位操作后keyr的值为10,此时只要把前后两位的值进行~keyr[0] & keyr[1]的操作,其结果为1,表示有下降沿产生。而当下一个时钟来时,keyr的值变为了00,进行~keyr[0] & keyr[1]操作后的结果为0,表明没有边沿产生(因为按键一直处于按下状态)。当按键释放时,key等于1,并位后keyr的值为01,此时进行keyr[0] & ~keyr[1]的操作,其结果为1,表示有上升没产生。而当下一个时钟来时,keyr的值变为了11,进行keyr[0] & ~keyr[1]操作后的结果为0,表明没有边沿产生(因为按键一直处于释放状态)。
4、根据以上第3步,就可得到按键的下降沿或上升沿(代码中的key_neg或key_pos),且其高电平的维持时间只有一个时钟周期,故可以作为按键边沿信号来使用。这样做的好处在于,无论按下(或释放)哪个按键,都统一做一个边沿量来处理。
5、然后进行20毫秒的循环计数,在计数过程中,如果遇到前面的边沿量(key_neg或key_pos)有效,则计数清零重新计数,直到溢出。
6、20毫秒溢出后,表示肯定有按键按下了,此时需要计算出键值,即哪个按键被按下。先定义两个健值存储变量key_halue[1:0],初始值都为111,然后一个时钟执行一次key_halue[1] <= key_halue[0]。当计数到了20毫秒后,执行key_halue[0] <= key_h,即获取外部按键。由于已经是在20毫秒之后才执行的该语句,所以说明有键被按下,即key_halue[0]的值不为全1了(key_halue[1]的值仍没变,为全1,因为两个值相差一个时钟节拍),此时进行key_halue[1] & ~key_halue[0]的处理之后,其结果就是被按下的那一位的值为1,其余为0,这样就能找出按下的键值(代码中key_press的值)。同上面第3步中的情况一样,在下一个是时钟来时,key_press的值恢复为全0,即按键的键值只存在一个时钟周期。
7、若需要检测的是按键的释放,则换成~key_halue[1] & key_halue[0]的处理方式,其余一样。
8、综合以上步骤,最终获得的按键值key_press,是经过消抖处理之后,按位排列的键值(即一个按键占用一个位),当某位为1时表明该按键被触发(可能是按下或是释放),且只存在一个时钟的高电平宽度。
9、这样的按键设计,不仅能消除抖动引起的误触发,还解决了按键按下(或释放)一次,却得到多次键值的弊病。即一次按键操作,只产生一个键值信号,保证了按键操作的有效性。
10、为了容易理解,上述在获取按键并进行并位操作时,没有进行时钟打拍操作。在实际应用时,为了消除FPGA的亚稳态,一般需要再打两拍时钟,再进行其他操作(即keyr <= {keyr[2:0], key}),可自行分析。

四、实验步骤

FPGA开发的详细步骤请参见“基于EP4CE6F17C8的FPGA开发流程(以半加器为例)”一文,本例只对不同之处进行说明。

本例工程放在D:\EDA_FPGA\Exam_7文件夹下,工程名称为Exam_7。模块文件名称为key_led.v,并设置为顶层实体。其余步骤与“基于EP4CE6F17C8的FPGA开发流程”中的一样。

接下来看管脚约束,本例中使用了3个按键和3个LED,一共6个引脚,再加上复位和时钟晶振,一共8个。具体的端口分配如下图所示。

对于未用到的引脚设置为三态输入方式,多用用途引脚全部做为普通I/O端口,电压设置为3.3-V LVTTL(与”基于EP4CE6F17C8的FPGA开发流程“中的一样)。需要注意,程序中的每个端口都必须为其分配管脚,如果系统中存在未分配的I/O,软件可能会进行随机分配,这将造成不可预料的后果,存在烧坏FPGA芯片的风险。

接下来对工程进行编译,编译完成后,可查看一下逻辑器件的消耗情况,如下图所示。

另外,还可以点击菜单Tools->Netlist Viewers->RTL Viewer,查看一下生成的RTL电路图,如下图所示。

最后进行程序下载,并查看结果。下图是按下key1时点亮led1的图片。

下图为按下key2时点亮led2的图片。

下图为按下key3时点亮led3的图片。

当按下reset键时,所有led均熄灭,如下图。

posted @ 2024-04-05 16:42  fxzq  阅读(111)  评论(0编辑  收藏  举报