这一篇在干嘛?
前两个实验的 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
| Name | Direction | Bank | Location | |
|---|---|---|---|---|
| 1 | key[3] | input | bank3 | C1 按键4 |
| 2 | key[2] | input | bank3 | B1 按键3 |
| 3 | key[1] | input | bank3 | B2 按键2 |
| 4 | key[0] | input | bank3 | A2 按键1 |
| 5 | led[3] | output | bank0 | B12 LED4 |
| 6 | led[2] | output | bank0 | C10 LED3 |
| 7 | led[1] | output | bank0 | A3 LED2 |
| 8 | led[0] | output | bank0 | A4 LED1 |
| 9 | sys_clk | input | bank2 | R7 时钟 |
| 10 | sys_rst_n | input | bank0 | C5 复位 |
注意两点:本实验中 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 记得重新编译)。
现象与总结
本实验的三个关键收获:
- FPGA 第一次「听」懂了输入:
key[3:0]是异步进来的电平信号,代码在时钟节拍下同步采样它,这是所有交互式 FPGA 设计的最小雏形; - 节拍 + 状态机:
cnt(0.2s 计时)驱动led_control(2 位循环状态),case把状态翻译成输出图案。「计数器 → 状态寄存器 → case 输出」是数字系统里最常见的三段式结构; - 优先级判断:
if / else if链让多输入有了优先级,同时也意味着一次只有一个按键的模式生效。
扩展练习:把流水灯速度加快一倍(cnt 终值减半);新增一种「来回流水」模式(需要给 led_control 加方向标志)。
自测
自测
- 代码里判断按键用
key[n] == 0,为什么是等于 0 才算按下?如果写反会出现什么现象?- 0.2 秒的节拍是怎么算出来的?写出 50MHz 时钟下计 0.2 秒需要的计数终值。
led_control为什么定义成 2 位就够?它从2'b11再加 1 会变成什么?- 同时按下按键 1 和按键 4,LED 会显示什么?为什么?
- 本实验为什么可以不做按键消抖?在什么场景下按键消抖就变成必需的了?
- 本实验的复位引脚是哪个?它和呼吸灯实验的复位引脚相同吗?为什么每次实验都要核对约束表?