02 按键控制LED——ZYNQ_FPGA

一、理论分析

        对于前一章已基本分析Verilog和C语言的区别,接下来介绍Verilog的一个基本面。

        对于单片机而言,时钟和复位引脚固定,一般初始化IO为输出模式,设定一下速率等,通过GPIO_Setbits等就可以实现对一个引脚的控制了,本质上就是输出高或者低。

        对于FPGA而言,时钟和复位引脚并不固定,所以需要手动配置,也就是常说的input、output。

        FPGA不像STM32,每个引脚要么是通用IO,要么是像IIC、SPI、UART等,其底层驱动协议已经写好,只需要对引脚配置即可,FPGA所有的IO基本上都是“通用IO”,所以对于LED和KEY,相应的也要有input、output

例如如下:

    input               sys_clk ,
    input               sys_rst_n ,

    input        [1:0]  key ,
    output  reg  [1:0]  led

        sys_clk即system clock,系统时钟。sys_rst_n即system reset,系统复位。

        key即按键,[1:0]就代表了2位,对于IO而言就是两个引脚。led即灯,其为输出引脚同样有两个,reg先不讲述。

        需求:根据两个按键(KEY0 和 KEY1)的状态,在不同的 LED 状态下,分别设置 LED 的显示模式 (是同时闪烁,或者交替闪烁)。

       1、 对于Verilog而言,并行处理架构加时序逻辑结构,先看懂如下代码:

        一个时序逻辑结构的基本如图所示:always总是、posedge上升沿、or或者、negedge下降沿、begin开始、end结束。大白话就是总是在sys_clk上升沿或者sys_rst_n下降沿执行中间的代码。<=为赋值语句,右边赋值到左边,相当于C语言的=。

always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        cnt <= 25'd0;
    else if(cnt < 25'd2500_0000)  //计数500ms
        cnt <= cnt + 1'b1;
    else
        cnt <= 25'd0;
end

        2、刚才说当sys_rst_n为下降沿也可以执行代码,一般就是当复位按键按下,或系统自动复位后(不同的板载电路不一样,本人板子复位按键按下为0,不按为1,如果你恰巧反过来,那么将negedge改为posedge,!去掉)。初始上电,时钟开始、自动复位,若手动将复位按键按下由1变为0,此时捕获到sys_rst_n的下降沿,且if(!sys_rst_n)成立,运行cnt<=25'd0,也就是初始化。FPGA复位一般就是对值的刷新,状态机的回溯。

        按键弹起或系统正常工作后,if(!sys_rst_n)不再成立,每次sys_clk上升沿到来即执行else if和else对应的语句。非常简单,当cnt<25'd2500_0000就++,当cnt>=25'd2500_0000就清零,实现cnt在0-2500_0000间变化。(注:cnt可以达到2500_0000)。

        3、25'd2500_0000,25代表25位二进制,d代表十进制数,2500_0000代表实际的值。就比如:

        int a=25000000,其中int代表25,a代表cnt,C语言默认10进制赋值。对于FPGA而言,不会被int、float、uint、double等束缚,只需要计算,这个值最大需要多少位二进制,例如本次最大需要2500_0000。对于如图计算器,其中HEX为16进制,DEC为10进制,OCT为8进制,BIN为二进制,当我们输入25000000后,寻找1的最高位,即4*7-3=25位。

        对于cnt<25'd2500_0000,可以有如下表述:cnt<25'h17D7840、cnt<25'o137274100、cnt<25'b0001011111010111100001000000,无论用哪种表示,25位二进制永远不变

         利用这个特性,当sys_clk时钟为50MHz,即一秒运行50_000_000次,对于0~25_000_000(25_000_000计数),一秒可以运行两次。即完全计一次数需要0.5s。

        理论计算:t=1/f*n=1/50_000_000*2500_0000=0.5s(t为时长,f为频率,n为计数。)

        所以,我们可以在另一个always中判断当cnt == 25'd2500_0000,即需要改变LED状态,若这个状态需要被经常使用,我们可以用另一个变量表示,从而降低代码段,提示可读性。

        例如对C语言而言,你可以在定时器中不断增加i的值,你可以在main的while中判断,当i>=50,就点亮LED,也可以在定时器中同时增加:if(i>=50){LED_MODE=1;i=0}。在主函数中,判断if(LED_MODE=1){LED_MODE=0;LED0_turgo;}。对于Verilog而言也可以:

//每隔500ms就更改LED的闪烁状态
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        led_ctrl <= 1'b0;
    else if(cnt == 25'd2500_0000)
        led_ctrl <= ~led_ctrl;
    else led_ctrl <=led_ctrl;
end

        在之后,还可以创建一个always块,通过判断led_ctrl来对LED的IO进行操作,如此我们可以初步估计:cnt在0-25000000逐步增加,当到25000000时,将led_ctrl反转,当led_ctrl为0时,LED为01,否则为10。从而实现两个LED来回翻转。(led <= 2'b10;即LED低位为0,高位为1)

//根据按键的状态以及LED的闪烁状态来赋值LED
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        led <= 2'b11;
    else if(led_ctrl == 1'b0)
        led <= 2'b01;
    else
        led <= 2'b10;
end

        在此基础上,我们加入KEY实现更多的功能:先自行看代码,再看下面讲解:

//根据按键的状态以及LED的闪烁状态来赋值LED
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        led <= 2'b11;
    else case(key)
        2'b10 :
            if(led_ctrl == 1'b0)
                led <= 2'b01;
            else
                led <= 2'b10;
        2'b01 : 
            if(led_ctrl == 1'b0)
                led <= 2'b11;
            else
                led <= 2'b00;
        2'b11 : 
                led <= 2'b11;
        default: ;
    endcase
end

        通过case语句,判断KEY状态,若按键0按下,LED交替闪烁,按键1按下,LED同时闪烁,若都未按下,则全亮。


二、Verilog实现

        1、通过vivado建立工程,选好芯片信号。

        2、以下为代码的实现:


module key_led#
    (parameter cnt_max=25'd25000000)
    (
    input               sys_clk ,
    input               sys_rst_n ,

    input        [1:0]  key ,
    output  reg  [1:0]  led
);


//reg define
reg [24:0] cnt;
reg        led_ctrl;

//*****************************************************
//**                    main code
//*****************************************************

//计数器
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        cnt <= 25'd0;
    else if(cnt < cnt_max)  //计数500ms
        cnt <= cnt + 1'b1;
    else
        cnt <= 25'd0;
end

//每隔500ms就更改LED的闪烁状态
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        led_ctrl <= 1'b0;
    else if(cnt == cnt_max)
        led_ctrl <= ~led_ctrl;
end

//根据按键的状态以及LED的闪烁状态来赋值LED
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        led <= 2'b11;
    else case(key)
        2'b10 :  //如果按键0按下,则两个LED交替闪烁
            if(led_ctrl == 1'b0)
                led <= 2'b01;
            else
                led <= 2'b10;
        2'b01 :  //如果按键1按下,则两个LED同时亮灭交替
            if(led_ctrl == 1'b0)
                led <= 2'b11;
            else
                led <= 2'b00;
        2'b11 :  //如果两个按键都未按下,则两个LED都保持点亮
                led <= 2'b11;
        default: ;
    endcase
end

endmodule

        可以看到我把25'd25000000改为了cnt_max,且cnt_max是用parameter定义的。这个模块在被顶层文件定义的时候,顶层文件可以再度parameter这个cnt_max,例如改到25,这样sys_clk只需要计数25次,对应的仿真时间会大大缩短


三、tb仿真

        初学者一定要运行tb仿真和观看RTL视图,tb仿真一方面锻炼仿真文件编写能力,一方面可以快速验证你的代码是否有误,且不占用芯片的资源(后续介绍)。

        tb仿真顶层文件:1ns/1ps,就是运行是1ns级别,仿真图抽样为1ps。即解释了,always #10 sys_clk=~sys_clk,这个10的单位了,即10个1ns。

`timescale 1ns / 1ps

module tb_key_led;

reg sys_clk;
reg sys_rst_n;
reg [1:0]key;
wire [1:0]led;

//parameter cnt_max=25'd5;

always #10 sys_clk=~sys_clk;  //每10ns,sys_clk反转一次,故20ns为一个周期,即50MHz 
key_led #(
    .cnt_max(25'd5))  //实现传入parameter
    key_led_u(//对例化的模块进行二次命名,一个模块可以被多个文件例化,所以可以起名字,实现区分
    .sys_clk(sys_clk),//连接引脚,其中.后面的为原模块的变量即非tb的,()里的为调用人的变量即tb
    .sys_rst_n(sys_rst_n),
    .key(key),
    .led(led)
);
 
initial  //仿真文件的开始
begin
    sys_clk=1'b1;//时钟初始先为高电平
    sys_rst_n=1'b0;//复位初始先为低电平即复位
    key=2'b11;
    #1000//延时1000ns
    sys_rst_n=1'b1;//复位结束
    
    #200//延时100ns
    key=2'b01;
     
    #200  //延时300ns
    key=2'b10;
    
     #200  //延时300ns
    key=2'b11;   
end

endmodule

        如下为运行图:默认会添加在module tb_key_led定义的变量: sys_clk、sys_rst_n、key、led;所以,如果你还想看到cnt计数到多少,可以在源文件中加入output [24:0]cnt,在仿真文件通过wire [24:0]cnt,并进行相连,实现仿真文件中也出现。

        可以看到,仿真的效果和理论一致,在黑色区域,右键变量,可以选择数字信号还是模拟信号,信号的radix,即:二进制、八进制、十六进制、无符号十进制、有符号十进制。


四、wire、reg介绍:

        wire,即线。更多的是做一个连接的过程,reg,寄存器。有人简单称为:时序逻辑用reg、组合逻辑用wire,这么乍一看好像确实没毛病。

reg只能做输出承载仅为wire
wire做输入时承载可以时wire、reg
wire做输出时承载仅能为wire

        时序逻辑电路里可以被赋值的只有:reg、integer、time、genvar

        wire的变量,只能在非时序逻辑赋值,可以通过定义时就赋值,例如wire a=1;也可以先定义后赋值:wire a; assign a=1;不能写a=1;

        reg的变量,只能在时序逻辑赋值,通常先定义,reg a;(无法定义时赋值)。在时序逻辑电路里,令a<=1;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陌夏微秋

希望各位多多支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值