这一篇在干嘛?

前面几个实验我们一直在「输出」——点灯、数数、亮数码管,这一篇终于轮到「输入」。开发板上的 4 个机械按键和 4 位拨码开关都会接到这个实验里,按下、拨动什么,LED 就实时反映什么。实验代码只有十几行,但它藏着 FPGA 输入处理的两个基本功:电平极性和打两拍。

实验目的

了解和检测开发板上的按键及开关特性。

听起来简单,其实这一步非常值得做:拿到一块新板子,先弄清楚「按键按下到底是高还是低」「拨码开关拨上是 0 还是 1」,后面所有用到输入的实验(计数器的复位/加载、多路选择器的选通、消抖实验)都建立在这个常识上。搞反了极性,现象会完全对不上号。

实验原理

图 1.1 开发板上的按键是按下去是接触为低电平,松开是断开为高电平。

图 1.2 中 SW1-SW4 是作为与 FPGA 的 IO 输入信号,拨上为「0」,拨下为「1」。


图 1.1


图 1.2

为什么按键是「低有效」?

机械按键一端接 FPGA 引脚,另一端接地。FPGA 内部(或约束里 PULLTYPE = PULLUP 配置的上拉电阻)把引脚平时拉到高电平;按下按键时引脚被直接接到地,变成低电平。所以:

  • 松开 = 高电平(1)
  • 按下 = 低电平(0)

这就是「低有效」的含义——信号起作用的状态是低电平。回看上一篇计数器代码里 negedge reset_n 复位,正是利用了这个特性:按下键 1 才产生复位。

拨码开关为什么拨上是 0?

拨码开关的极性纯粹由硬件电路接线决定,本板上 SW1-SW4 拨上为「0」、拨下为「1」。记住这个对应关系即可,不同厂家的板子可能相反——这也是为什么拿到任何板子都值得先跑一遍本实验验证极性。

输入引脚都要配上拉

看后面的引脚约束会发现,8 个输入全部配了 PULLTYPE = PULLUP。对按键来说上拉提供松开时的高电平;对拨码开关来说,上拉保证开关断开一侧时引脚不悬空(悬空的输入会随机跳变,造成误动作)。

程序里的「打两拍」是什么?

程序对输入信号连做两级寄存再输出。这个习惯做法有两个理由:

  1. 亚稳态防护:异步输入(人手的动作随时可能发生)直接进入时钟域可能违反寄存器的建立/保持时间,导致输出处于既不是 0 也不是 1 的亚稳态。第一级寄存器允许它「短暂不确定」,第二级寄存器在下一个时钟沿采样时,亚稳态大概率已经稳定下来,从而把不确定性隔离在一级之内。
  2. 对齐时序:打一拍让输入信号与系统时钟同步,后续逻辑使用起来时序才可控。

本实验的输入只是点亮 LED,即使偶发抖动毛刺也观察不出来(真正的抖动治理要等到消抖实验)。但「输入先打两拍」应该从今天起成为肌肉记忆。

实验程序

原文程序分两部分给出(模块头与主体因排版断开,连续阅读):

module key_test
input clk, //时钟 50Mhz 板
input [7:0] key, //输入 8 组信号
output[7:0] led //LED 8 组
);
reg[7:0] led_r; //定义第一阶段寄存器,生成四个 D 触发器
reg[7:0] led_r1; //定义第二级寄存器,生成四个 D 触发器
always@(posedge clk)
begin
led_r <= ~key; // 第一级锁存数据
end
always@(posedge clk)
begin
led_r1 <= led_r; // 第二级锁存数据
end
assign led = led_r1;
endmodule

逐行解读:

  • input [7:0] key —— 8 位输入把按键和拨码开关一起接进来(4 个按键 + 4 个开关 = 8 路信号);
  • led_r <= ~key; —— 取反是点睛之笔。按键按下输入变低,取反后 LED 寄存器变高、灯点亮;于是「按下亮、松开灭」,符合直觉。LED 是低电平点亮的设计,若不取反,现象就会变成平时全亮、按下才灭;
  • 两级 always@(posedge clk) 就是上面说的打两拍:key 先进 led_r,再进 led_r1,最后输出;
  • 整个设计是纯同步逻辑,没有任何组合反馈,综合出来的就是 16 个 D 触发器(每级 8 个)加少量布线。

为什么现象可能偶尔「闪一下」

机械按键在按下/松开的几毫秒内触点会弹跳,电平反复抖动。本实验只做同步化、不做消抖,所以按键对应的那颗 LED 可能快速闪烁几下才稳定——这是正常现象,不是板子坏了。要「治好」它,请看消抖动电路设计那一篇。

引脚约束

set_pin_assignment { clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[0] } { LOCATION = C8; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[1] } { LOCATION = C7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[2] } { LOCATION = C6; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[3] } { LOCATION = C5; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[4] } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[5] } { LOCATION = B2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[6] } { LOCATION = B1; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[7] } { LOCATION = C1; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { led[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[7] } { LOCATION = A6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }

这份约束表本身就是一张「板级地图」:

  • key[4]~key[7](A2、B2、B1、C1)就是计数器实验里出现过的键 1~3 那一批引脚——同一个物理按键,不同实验只是换了信号名;
  • key[0]~key[3](C8、C7、C6、C5)与上一篇的 SW[0]~SW[3] 引脚相同,对应拨码开关 SW1-SW4;
  • led[0]~led[7](C15、C16、B6、A5、B5、A8、A7、A6)与数码管 DIG 引脚重叠——LED 和数码管段线是复用的引脚,靠板上的 DIG/LED 拨动开关选择通向哪一路,这解释了为什么做数码管实验时要把开关拨到 DIG 一侧。

下载测试

分别按动四组按键与拨码开关,测试按键与 LED 灯的效果。

建议按下面的清单逐项验收:

  1. 4 个机械按键:按下对应 LED 点亮,松开熄灭;快速连按能看到闪烁(抖动未消除的正常表现);
  2. 4 个拨码开关:拨下(逻辑 1)对应 LED 点亮,拨上(逻辑 0)熄灭——开关是电平保持型输入,不取反的话现象相反,正好可以用取反代码对照理解;
  3. 同时操作:8 路输入互相独立,可以同时按住多个键、拨多个开关,验证 8 位向量是并行处理的;
  4. 速度感受:手指再快,LED 的响应也不会「丢」——因为输入在每个 50MHz 时钟沿都被采样,人的动作相对时钟太慢了。

现象与总结

这个实验小到不像话,却是输入处理的地基,记住三件事:

  1. 极性是板级属性:本板按键按下为低、开关拨上为 0,写代码前先确认;
  2. 异步输入先进两级寄存器:防亚稳态、同步时钟域,成本只有两级触发器;
  3. LED 与数码管引脚复用:引脚冲突时靠板上开关切换通路,做实验前记得检查开关位置。

再往前想一步:本实验只处理了「电平」和「同步」,还没处理「抖动」和「事件」。消抖动电路设计一篇会补上抖动治理,而在计数器实验里你已经见过了「把按键变成控制信号」的用法。输入处理的三板斧——同步、消抖、边沿检测——凑齐之后,任何按键开关相关的需求都不再可怕。

自测