这一篇在干嘛?

前两个实验的 FPGA 都只「输出」——灯自己按固定节奏闪。这一篇第一次引入输入:用 4 个按键控制 4 个 LED,按不同的键,LED 呈现不同的显示效果。你会学到多输入信号的判断、用节拍计数器驱动状态机、以及 case 语句实现多模式切换的经典写法。

实验目标与设计思路

实验任务:使用开发板上的四个按键控制四个 LED 灯。不同按键按下时,四个 LED 灯显示不同效果:

  • 按下按键 1:4 个 LED 灯显示从右向左流水灯效果;
  • 按下按键 2:4 个 LED 灯显示从左向右流水灯效果(代码注释口径);
  • 按下按键 3:4 个 LED 灯同时闪烁;
  • 按下按键 4:4 个 LED 灯同时常亮;
  • 没有按键按下:LED 全灭。

系统框图如下:


系统框图

设计的关键在于:流水灯和闪烁都需要一个时间节拍。代码里用一个 24 位计数器 cnt 计数 0.2 秒(50MHz × 0.2s = 10_000_000 个时钟,计到 9_999_999),数满一轮就让模式节拍变量 led_control(2 位,0~3 循环)加 1。然后根据当前按下的按键,用 case (led_control) 决定 LED 该显示哪种图案。这样不同模式共用同一节拍,逻辑非常清爽。

Verilog 实现

完整代码如下(原文排版把模块头部放在了代码块外,这里按原样合并还原):

module key_led(
input sys_clk , //50Mhz 系统时钟
input sys_rst_n, //系统复位,低有效
input [3:0] key, //按键输入信号
output reg [3:0] led //LED 输出信号
);
//reg define
reg [23:0] cnt;
reg [1:0] led_control;
//用于计数 0.2s 的计数器
always @ (posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
    cnt <= 24'd0;
    else if(cnt < 24'd9_999_999)
    //else if(cnt < 24'd9)    //仅用于仿真
    cnt<=cnt+1;
    else
    cnt<=0;
end
 
//用于 led 灯状态的选择
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (!sys_rst_n)
    led_control <= 2'b00;
    else if(cnt == 24'd9_999_999)
    //else if(cnt == 24'd9)    //仅用于仿真
    led_control <= led_control + 1'b1;
    else
    led_control <= led_control;
end
 
//识别按键,切换显示模式
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
    led<=4'b0000;
    end
    else if(key[0]== 0)    //按键 1 按下时,从右向左的流水灯效果
    case (led_control)
    2'b00    : led<=4'b1000;
    2'b01    : led<=4'b0100;
    2'b10    : led<=4'b0010;
    2'b11    : led<=4'b0001;
    default    : led<=4'b0000;
    endcase
    else if (key[1]==0)    //按键 2 按下时,从左向右的流水灯效果
    case (led_control)
    2'b00 : led<=4'b0001;
    2'b01 : led<=4'b0010;
    2'b10 : led<=4'b0100;
    2'b11 : led<=4'b1000;
    default : led<=4'b0000;
    endcase
    else if (key[2]==0) //按键3按下时,LED闪烁
    case (led_control)
    2'b00 : led<=4'b1111;
    2'b01 : led<=4'b0000;
    2'b10 : led<=4'b1111;
    2'b11 : led<=4'b0000;
    default : led<=4'b0000;
    endcase
    else if (key[3]==0) //按键4按下时,LED全亮
    led=4'b1111;
else
    led<=4'b0000; //无按键按下时,LED熄灭
endmodule

逐块拆解:

① 0.2 秒节拍计数器

cnt 从 0 计到 9_999_999(共 10_000_000 个时钟,即 0.2 秒)后归零。注释里保留了 else if(cnt < 24'd9) //仅用于仿真 的写法——仿真时把终值改小,几拍就能看到模式切换,不用等 0.2 秒,这与软件流程篇里缩短 T1S 是同一个技巧。

② 模式节拍 led_control

每 0.2 秒加 1。它是 2 位寄存器,从 2'b00 涨到 2'b11 后自动溢出回 2'b00,天然形成 0→1→2→3 的循环,正好对应流水灯的 4 步或闪烁的亮/灭交替。

③ 按键判断与模式输出

第三个 always 块用 if / else if 链依次判断 4 个按键(低电平表示按下,所以判断 key[n] == 0),每个按键对应一个 case (led_control)

  • 按键 1:1000 → 0100 → 0010 → 0001,亮灯位置每 0.2 秒移动一步(从右向左的流水灯,按 LED 编号从高位到低位);
  • 按键 2:0001 → 0010 → 0100 → 1000,方向相反;
  • 按键 3:1111 → 0000 → 1111 → 0000,0.2 秒亮、0.2 秒灭,整体闪烁;
  • 按键 4:直接 led = 4'b1111 常亮(注意这句用的是阻塞赋值 =,与其他分支的非阻塞赋值 <= 不一致,属于原文写法,初学阶段建议统一用 <=)。

按键是低电平有效,别把判断写反

开发板上的按键未按下时引脚被上拉为高电平,按下才接地变低。所以代码里用 key[n] == 0 判断「按下」。如果写成 == 1,现象就是「不按键灯狂跑、一按全灭」。另外 led 的 4 位中 led[0] 对应 LED1(A4 引脚),流水方向以板上实际位置为准,方向反了就把 case 里的顺序对调。

多个按键同时按下会怎样?

if / else if 链是有优先级的:key[0] 优先级最高。同时按住按键 1 和按键 3,生效的是按键 1 的流水灯模式。想让「最后按下的键生效」就需要更复杂的状态记录逻辑,这是后面可以自己挑战的扩展题。

还有一点值得注意:这个实验没有做按键消抖。机械按键按下瞬间会有几毫秒的抖动,本实验中按键只充当「模式选择开关」,抖动的影响被 0.2 秒的节拍掩盖了,看不出异常。但一旦按键用于计数(按一次加一),就必须加消抖逻辑——记住这个伏笔。

引脚约束

本实验中,系统时钟、复位按键、按键和 LED 灯的管脚如下表所示。

Anlogic IO Constraint

NameDirectionBankLocation
1key[3]inputbank3C1 按键4
2key[2]inputbank3B1 按键3
3key[1]inputbank3B2 按键2
4key[0]inputbank3A2 按键1
5led[3]outputbank0B12 LED4
6led[2]outputbank0C10 LED3
7led[1]outputbank0A3 LED2
8led[0]outputbank0A4 LED1
9sys_clkinputbank2R7 时钟
10sys_rst_ninputbank0C5 复位

注意两点:本实验中 4 个按键的引脚是 C1、B1、B2、A2(bank3),与前一篇呼吸灯里作为复位用的 A2 是同一个物理引脚——也就是说按键 1 和复位键共用 A2,不同实验的复用关系以各实验的约束表为准,不能混用;本实验的复位键是 C5,与其他实验的 A2 不同,下载前务必核对。

对应的 .adc 约束写法(以 sys_clk 为例):

set_pin_assignment { sys_clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }

其余信号按上表逐条填写即可,方法同软件流程篇。

下载测试

按标准流程编译布线、下载程序后,分别按动四组按键,观察 LED 灯的不同显示形式:

  • 按键 1:从右向左流水灯;
  • 按键 2:从左向右流水灯;
  • 按键 3:四灯同步闪烁;
  • 按键 4:四灯常亮;
  • 松开所有按键:全灭。

如果发现某个按键没反应,优先检查引脚约束是否与上表一致、IO Constraint 界面里该信号是否真的锁上了(改完 .adc 记得重新编译)。

现象与总结

本实验的三个关键收获:

  1. FPGA 第一次「听」懂了输入key[3:0] 是异步进来的电平信号,代码在时钟节拍下同步采样它,这是所有交互式 FPGA 设计的最小雏形;
  2. 节拍 + 状态机cnt(0.2s 计时)驱动 led_control(2 位循环状态),case 把状态翻译成输出图案。「计数器 → 状态寄存器 → case 输出」是数字系统里最常见的三段式结构;
  3. 优先级判断if / else if 链让多输入有了优先级,同时也意味着一次只有一个按键的模式生效。

扩展练习:把流水灯速度加快一倍(cnt 终值减半);新增一种「来回流水」模式(需要给 led_control 加方向标志)。

自测