尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于VHDL的FPGA计算器设计:键盘扫描、消抖与LED动态显示

基于VHDL的FPGA计算器设计:键盘扫描、消抖与LED动态显示 简介基于VHDL语言实现的简易计算器工程以键盘扫描和LED显示为主要外设系统展示了从顶层原理图设计到VHDL实体/结构体编写再到编译综合、时序仿真与下载验证的完整开发流程尤其适合数字逻辑课程设计、FPGA入门自学及电子竞赛预研。压缩包内共147个文件主要包含.vhd源文件、.bdf原理图文件、.tdf文本设计文件以及大量Quartus工程文件如.qpf工程文件、.qsf约束设置、.rpt综合报告、.summary设计摘要、.sci/.qmsg等编译信息此外还有.cdb/.hdb数据库缓存与.bak备份文件总大小仅509KB关键源码、配置与报告分层存放方便按目录追踪各阶段结果。已有303人学习下载。该资源对理解键盘消抖与编码、LED多位扫描显示、算术逻辑单元ALU、有限状态机控制等VHDL核心知识点很有帮助.pof配置文件可直接用于FPGA下载验证综合与时序报告则能辅助排查时序约束问题是集设计、仿真、实现于一体的实战参考。1. 为什么一个“键盘LED”的计算器最容易写崩很多初学者拿到这个key_led项目第一反应是“键盘输入、LED 显示无非就是读按键、点亮灯”但真正用 VHDL 把它跑在 FPGA 上就会发现问题不在算术逻辑而在时序。按键按下时产生的抖动毛刺、LED 动态扫描时的位选与段选切换、算术运算的进位传递任何一处没处理好板上现象就是“按键偶尔不响应”或“显示数字乱跳”。这个项目看似简单实际覆盖了数字系统设计的三条主线输入接口键盘扫描与消抖、输出接口LED 动态刷新、核心运算ALU 与控制状态机。如果你正在学 VHDL或者准备把课设、竞赛作品从原理图拖到 FPGA 验证这个项目能把“写代码”和“上板调试”之间的差距完整暴露出来。下面我按从工程文件到仿真调试的顺序把这个计算器的实现拆开讲清楚。2. 先拆包key_led 项目里的 VHDL 文件都在干什么2.1 从 .bdf 到 .vhdQuartus 工程的逻辑层次打开key_led.rar会看到一组混合后缀的文件.bdf是 Quartus 原理图设计文件.vhd.bak是 VHDL 源码备份.bsf是原理图符号文件.map.cdb、.pre_map.cdb这类是综合与适配生成的中间数据库文件。整个工程其实是“原理图顶层 VHDL 底层”的混合设计模式顶层用key_led.bdf把键盘扫描模块KeyScan和 LED 扫描模块ScanLED连起来而实际逻辑都在.vhd.bak里。这种结构的优势在于顶层连线可以用原理图直观看到数据流向底层逻辑用 VHDL 描述便于修改时序和算法。很多板卡参考设计都采用同样的方式。你自己重构时也可以把顶层换成纯 VHDL 的entity用component例化两个底层模块效果一样但代码可读性更好。-- 顶层例化示意对应 key_led.bdf 的结构 entity key_led_top is port ( clk_50m : in std_logic; rst_n : in std_logic; row_in : in std_logic_vector(3 downto 0); col_out : out std_logic_vector(3 downto 0); seg : out std_logic_vector(7 downto 0); sel : out std_logic_vector(3 downto 0) ); end entity;按理说工程里没有给出完整顶层代码但根据.bdf的引脚连接关系可以推断出端口信号。上面这个例程定义是标准写法clk_50m作为全局时钟rst_n低有效复位row_in读回按键行状态col_out输出列扫描信号seg与sel分别驱动七段数码管的段选和位选。2.2 KeyScan 与 ScanLED输入输出模块的边界划分从文件名KeyScan.vhd.bak和ScanLED.vhd.bak能看出模块职责划分非常清晰。KeyScan 只负责三件事产生列扫描信号、读回行电平、输出消抖后的按键编码。ScanLED 只负责三件事接收运算结果、做 BCD 到七段码的译码、产生动态扫描时序。这种划分让测试变得简单我可以单独给 KeyScan 写 testbench模拟某一行某一列同时为低来判断编码是否正确也可以单独给 ScanLED 喂一组数字观察段选波形。实际项目中我一般会把 ALU 单独拆成第三个模块但原工程只有两个模块文件说明运算逻辑被放在了某一层内部。如果你拿到的是课设源码大概率 ALU 写在 KeyScan 或顶层进程里这个后面会讲怎么优化。2.3 关键信号与引脚规划表无论是上板还是仿真先理清引脚映射关系。下面是我基于典型 4x4 矩阵键盘和 4 位七段数码管整理的信号规划表和你手里的板卡可能略有差异但思路一致信号名方向宽度功能说明对应引脚/硬件clk_50m输入150MHz 系统时钟板载晶振rst_n输入1低电平复位清零内部寄存器按键或拨码row_in[3:0]输入4键盘行输入默认上拉为高矩阵键盘行col_out[3:0]输出4列扫描输出逐列拉低矩阵键盘列key_code[3:0]输出4消抖后的按键编码内部信号seg[7:0]输出8七段码含小数点数码管段选sel[3:0]输出4数码管位选低有效数码管公共端需要注意的是键盘的行信号默认必须通过外部上拉电阻接到高电平否则扫描时读到的浮动电平会导致误触发。部分开发板内部已经做了上拉但自己画板子时别忘了在行线上加 10kΩ 排阻。3. 键盘扫描矩阵按键的消抖与编码实现3.1 行列扫描的基本原理与分时驱动矩阵键盘用 4 根行线加 4 根列线就能识别 16 个按键。识别原理是某一时刻只把一列输出为低电平其余列输出为高电平然后读取所有行线。如果有一个键按下那么该键所在行与当前低电平列之间被短路对应行线会被拉低。这样每扫描完 4 列就能得到一个 4 位行状态结合当前列号就能唯一确定按键位置。常见做法是用一个 2 位计数器产生列选信号频率在 1kHz 左右即可。不要用 50MHz 直接扫否则机械按键的抖动毛刺会被完整采进来。我一般先用计数器分频到 1kHz 作为扫描时钟每个扫描周期只切换一列保证每一列有足够时间稳定。-- KeyScan 模块部分代码列扫描计数器生成 signal col_cnt : unsigned(1 downto 0) : 00; signal scan_clk : std_logic; begin process(clk_50m, rst_n) variable cnt : integer range 0 to 49999; begin if rst_n 0 then cnt : 0; scan_clk 0; elsif rising_edge(clk_50m) then if cnt 49999 then cnt : 0; scan_clk not scan_clk; else cnt : cnt 1; end if; end if; end process; process(scan_clk, rst_n) begin if rst_n 0 then col_cnt 00; elsif rising_edge(scan_clk) then col_cnt col_cnt 1; end if; end process; -- 根据列计数器输出列扫描信号 with col_cnt select col_out 1110 when 00, 1101 when 01, 1011 when 10, 0111 when 11, 1111 when others;这段代码里scan_clk分频得到 500Hz 左右的扫描时钟col_cnt循环变化控制col_out每次只有一列输出低电平。注意顺序先分频产生低速时钟再用这个时钟做列切换避免按键电平在高速时钟下被采到多次跳变。with-select语句是 VHDL 中写译码逻辑最直接的方式比多个if嵌套更清晰综合后也能映射到查找表结构。3.2 消抖计数器的 VHDL 实现机械按键按下或释放时触点会来回弹跳 5~20ms如果不清除一次按下会被识别成多次。常见消抖方案有延时采样法和计数确认法。延时采样法实现简单检测到行线变低后等 10ms 再读一次若仍为低则确认按键生效。计数确认法更适合在 VHDL 里实现对抖动信号进行连续采样只有连续 N 次采样到同一电平才认为状态稳定。下面是使用计数确认法的消抖代码。核心思想是每当行线电平与上次稳定状态不同时计数器清 0只有计数器累计到设定值才更新稳定状态。-- 单行输入消抖对 row_in 的第 0 位做消抖 signal stable_row : std_logic : 1; signal debounce_cnt : integer range 0 to 10 : 0; begin process(clk_50m, rst_n) begin if rst_n 0 then stable_row 1; debounce_cnt 0; elsif rising_edge(clk_50m) then if row_in(0) / stable_row then -- 电平发生变化计数器重新计数 if debounce_cnt 9 then stable_row row_in(0); debounce_cnt 0; else debounce_cnt debounce_cnt 1; end if; else -- 电平稳定计数器清零 debounce_cnt 0; end if; end if; end process;参数说明debounce_cnt上限设为 9 表示连续 10 个时钟周期采样到相反电平才翻转在 50MHz 时钟下这只有 200ns远不够消抖。实际使用时要把计数目标提高到 250000也就是对应 5ms 的稳定时间。上面的简写为了展示逻辑真实工程里最好用常量定义constant DEBOUNCE_TIME_MS : integer : 5; constant CLK_FREQ_MHZ : integer : 50; constant DEBOUNCE_LIMIT : integer : DEBOUNCE_TIME_MS * CLK_FREQ_MHZ * 1000;这段代码能直观看出 VHDL 中常量定义的作用把时间参数和时钟频率参数化后换板卡时只需改两个常量不用改逻辑。3.3 按键状态到数字/运算符的映射消抖之后需要把行列坐标编码成按键值。这里用组合逻辑实现即可。假设矩阵键盘布局是[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [A] [B] [C] [D] [E] [F]row_in的某一位为低col_cnt记录当前列号用两个信号联合编码。推荐的做法是维护一个key_pressed脉冲信号当检测到有键按下且之前没有键按下时产生一个单时钟周期的高电平并把编码输出到总线。脉冲信号的意义在于让后续 ALU 能捕捉到一次“按键事件”而不是连续的高电平导致重复计算。process(col_cnt, stable_row_vec) variable row_num : integer; begin key_val 0000; -- 默认值 for i in 0 to 3 loop if stable_row_vec(i) 0 then row_num : i; end if; end loop; key_val std_logic_vector(to_unsigned(row_num * 4 to_integer(col_cnt), 4)); end process;这段代码把行号乘以 4 再加上列号得到 0~15 的按键编码。注意for循环在 VHDL 综合时会被展开成并行的比较器而不是串行执行所以速度不是问题。但这里有个坑如果stable_row_vec中有多行为低说明发生了多键同时按下上面的循环只记录最后一行。实际设计里可以加一个“多键按下无效”的判断防止误触发。4. 计算器逻辑LED 显示与 ALU 的配合4.1 七段 LED 动态刷新驱动四位七段数码管如果每位独立驱动需要 32 个引脚大多数 FPGA 板卡不会这么奢侈。动态扫描方案用 4 个位选信号 8 个段选信号含小数点共 12 个引脚通过快速轮流点亮每一位利用人眼视觉暂留效应让人看到四位数字同时显示。扫描频率要在 1kHz 以上我一般取 1~2kHz。如果频率太低会看到闪烁太高会增加功耗且段选切换可能出现拖影。一个标准的动态扫描代码如下-- ScanLED 模块4 位动态扫描 signal scan_cnt : integer range 0 to 3 : 0; signal refresh_clk : std_logic; signal seg_data : std_logic_vector(3 downto 0); begin -- 分频产生 1kHz 刷新时钟代码略参考 3.1 节 process(refresh_clk, rst_n) begin if rst_n 0 then scan_cnt 0; elsif rising_edge(refresh_clk) then scan_cnt scan_cnt 1; end if; end process; -- 位选输出低电平选通 with scan_cnt select sel 1110 when 0, 1101 when 1, 1011 when 2, 0111 when 3, 1111 when others; -- 根据当前位选数字 with scan_cnt select seg_data digit0 when 0, digit1 when 1, digit2 when 2, digit3 when 3, 0000 when others; -- BCD 到七段码译码共阴极数码管 with seg_data select seg 11000000 when 0000, -- 0 11111001 when 0001, -- 1 10100100 when 0010, -- 2 10110000 when 0011, -- 3 -- 其余编码省略 11111111 when others; -- 全灭 end;这里要注意段选信号必须在位选切换之前稳定下来否则会出现“串位”现象也就是上一位的残影跑到下一位上。解决办法是在每次位选切换后先让段选出全灭码或空操作一段时间再输出真实段码。很多初学者忽略这一步导致显示有拖影以为是数码管坏了。常见做法是增加一个空档周期或者在sel切换后的上升沿再更新seg具体时序以实际板卡为准。4.2 四则运算 ALU 与溢出处理计算器的核心是 ALU。由于是“简易计算器”通常只支持两位数字的加减乘除所以寄存器位宽用 8 位足够。运算即按即得不需要复杂的中缀表达式解析。操作数按先后顺序进入第一次按数字作为第一个操作数按运算符后输入的数字作为第二个操作数按等号输出结果。-- 简化 ALUop_a 和 op_b 为 8 位操作数op_code 为运算类型 process(clk_50m, rst_n) begin if rst_n 0 then result (others 0); overflow_flag 0; elsif rising_edge(clk_50m) then case op_code is when 00 -- 加法 result std_logic_vector(unsigned(op_a) unsigned(op_b)); if unsigned(op_a) unsigned(op_b) 255 then overflow_flag 1; end if; when 01 -- 减法 if op_a op_b then result std_logic_vector(unsigned(op_a) - unsigned(op_b)); overflow_flag 0; else result (others 0); -- 负数不做处理 overflow_flag 1; end if; when 10 -- 乘法直接截断低 8 位 result std_logic_vector(unsigned(op_a) * unsigned(op_b))(7 downto 0); when 11 -- 除法除数不为 0 if op_b / 00000000 then result std_logic_vector(unsigned(op_a) / unsigned(op_b)); else overflow_flag 1; end if; when others result (others 0); end case; end if; end process;这段代码有几个关键点。加法溢出判断使用了无符号数比较 255但 VHDL 中直接比较两个相加的结果更方便可以先把unsigned(op_a) unsigned(op_b)的结果赋值给一个 9 位中间信号再判断第 8 位是否为 1。乘法和除法的result位宽要小心VHDL 中unsigned乘法结果位宽等于两个操作数位宽之和直接截断需要像代码里那样把结果的高位丢弃。除法在 FPGA 上会消耗较多逻辑单元如果只是课设可以改用查找表或移位减法但 4 位操作数范围较小直接用/运算综合器也能处理。4.3 数据通路与寄存器时序整个计算器的数据流是键盘编码 - 输入寄存器 - ALU - 显示寄存器 - 扫描译码。这里最容易出错的环节是按键输入与运算注册的时序关系。按键消抖后产生的key_pressed脉冲只有一个时钟周期必须在脉冲有效时锁存按键数据。数字键和运算符键的处理逻辑不一样数字键作为当前输入数的累加。按下“1”再按“2”应显示“12”而不是覆盖成“2”。运算符键保存当前数为第一操作数并记录运算类型然后开始等待第二个操作数。等号键完成运算把结果送入显示寄存器。状态转换可以用一个简单的状态机实现。状态包括IDLE、INPUT_A、WAIT_OP、INPUT_B、CALC。用 VHDL 的type state_type is (...)定义枚举状态进程内用case state判断当前状态并转移。这样写不仅逻辑清晰而且综合后状态转移图便于在调试工具里查看。按下C键时回到IDLE并清零所有寄存器。这是最基础的控制单元设计但足以让整体工程更像一个完整的计算器。5. 仿真与板级调试从 Modelsim 到 Quartus 的验证技巧5.1 编写 testbench 生成按键激励仿真验证键盘扫描模块的常见方法是在 testbench 中模拟一次按键按下先输出低电平等待 3ms 释放再观察key_pressed是否只产生一个脉冲。下面是一个精简的测试代码-- testbench 键盘激励 process begin rst_n 0; row_in 1111; wait for 20 ns; rst_n 1; wait for 1 ms; -- 模拟第 0 列、第 2 行按键按下row_in(2) 为 0 row_in 1011; wait for 10 ms; -- 等待消抖稳定 row_in 1111; wait for 5 ms; assert key_pressed 1 report Key press not detected severity error; wait for 50 ms; -- 继续第二个按键 row_in 1101; wait for 10 ms; row_in 1111; wait; end process;这里的关键是wait for 10 ms要大于消抖时间否则仿真结果中stable_row还没来得及翻转。仿真的时间单位ns是在 testbench 顶部通过timescale或 VHDL 上下文设置的VHDL 不支持时间单位指令它是实体的time类型直接用wait for 10 ms即可。我习惯把按键按下时间设置成 20ms给消抖计数器留足余量这样仿真波形上能清楚看到稳定信号的建立过程。5.2 时序违例的排查与修复上板后如果出现按键偶尔不响应先看综合报告里的时序余量。Quartus 的 Compilation Report 中 Timing Analyzer 会列出最差路径。对于计算器这种低速设计最可能的问题是异步复位释放时刻与时钟上升沿过于接近导致内部寄存器进入亚稳态。常见修复办法是使用同步复位即把rst_n先经过两级触发器打拍再做复位信号。另一个坑是键盘行输入没有做同步处理。外部按键引脚直接连到 FPGA 输入可能触发 setup/hold 违例。正确做法是在模块入口加一个两级同步器-- 输入同步器避免亚稳态 signal row_sync1 : std_logic_vector(3 downto 0); signal row_sync2 : std_logic_vector(3 downto 0); begin process(clk_50m) begin if rising_edge(clk_50m) then row_sync1 row_in; row_sync2 row_sync1; end if; end process;之后所有逻辑使用row_sync2而不是直接使用row_in。这个代价很小但对稳定性提升明显尤其是板卡走线较长、按键线缆靠近电机或电源模块时。5.3 一个实用技巧用逻辑分析仪抓取键盘抖动如果没有逻辑分析仪也可以用 FPGA 内部资源。对于 Altera/Cyclone 系列Quartus 内置 SignalTap 逻辑分析仪可以在线抓取row_sync2、debounce_cnt和key_pressed信号。我一般设置触发条件为key_pressed 1采样深度 128采样时钟用 50MHz这样能看到按键按下前后 2.56us 的细节足以判断消抖是否生效。需要提醒的是SignalTap 会占用额外的逻辑资源和存储器如果工程已经接近满资源建议保留核心调试信号其他信号全部禁用。还有一种更省资源的方法把debounce_cnt的最高位引出到空闲 LED按下按键时观察 LED 闪烁频率。如果 LED 在按键按下的瞬间出现多次快速翻转说明消抖没生效如果只有一次稳定翻转说明计数器逻辑正确。最后补充一个显示相关的排错实例数码管亮度不均通常是扫描频率偏低。把refresh_clk从 500Hz 提高到 1kHz同时将位选信号的占空比调整为 25%四位数码管亮度就能均匀。如果仍然闪烁检查是否每个位选周期都插入了段选熄灭时间这是最容易忽略的“串显”根源。本文还有配套的精品资源点击获取
返回列表