资讯动态

FPGA计算器设计:矩阵键盘、状态机与LCD1602显示全解析

发布时间:2026/9/12 1:15:38 来源:尧图企业网站定制
简介基于Verilog实现的4位整数计算器设计工程面向FPGA入门学习者与数字逻辑课程设计人群完整演示矩阵键盘输入、运算控制、数码管/LCD1602显示输出的一体化流程。工程包含顶层模块、按键扫描、LCD控制器、译码显示等核心源码并配有Quartus工程配置、引脚约束与编译报告便于直接打开工程进行仿真与下载验证。资源共81个文件以.v源码、.qsf/.qpf工程文件、.sof配置文件、.rpt/.summary编译报告为主另有大量Quartus自动生成的过程文件包体仅211KB轻量易获取。已有512人学习下载适合需要参考完整代码结构或快速搭建同类运算系统的人群。压缩包内目录包含大量工程中间产物但关键源码与报告齐全结构与常规Quartus工程一致便于按需定位。1. 一款接住矩阵键盘、四位整数运算和双显示输出的计算器计算器是 FPGA 入门里少有的“输入、运算、输出”三链路齐全的实验矩阵键盘负责输入运算电路处理四位整数的加、减、乘、除结果既可以用七段数码管显示也能送到 LCD1602 上。这个实验的价值不在于那几条算术公式而在于它迫使你把异步按键事件转成干净的内部状态再让状态机去驱动算数和显示。很多人在点亮数码管后不知道下一步该做什么真正能卡住人的不是 9999 乘 9999而是按键抖动带来的重复触发、状态机漏掉一次键盘事件、以及 LCD1602 上电初始化时序不对导致整屏乱码。这套源码目录里已经包含了 key_scan.v、SEG7_LUT.v、LCD_Controller.v、Reset_Delay.v 以及顶层 calculator.v下面从模块边界开始拆把键盘扫描、状态机、ALU 和显示一条路走通。2. 从 calculator.v 往下看顶层模块划分与数据通路设计拿到工程先不要急着读某一个.v文件先把calculator.qsf里收录的模块名圈出来工程边界就已经清楚了key_scan.v负责 4x4 矩阵键盘扫描与消抖SEG7_LUT.v做七段数码管译码LCD_Controller.v负责 LCD1602 的命令和数据时序LCD_TEST.v用于测试字符显示Reset_Delay.v处理上电复位延迟顶层calculator.v把这些模块粘合成一个完整系统。.bak文件通常是调试时留下的备份说明这些模块都被实际修改过可以直接当参考。2.1 子模块职责与接口定位整体功能按“取数、算数、出数”三段划分下面是这个工程里最主要的几个模块。模块文件职责对外接口要点key_scan.v矩阵键盘逐列扫描消抖后输出键值和有效脉冲row 输入col 输出key_valid 单周期脉冲calculator.v状态机控制输入寄存操作数调用运算送显示键盘接口与显示接口都在此汇总SEG7_LUT.v4 位 hex 输入到 7 段码的译码查找表hex[3:0] 输入seg[6:0] 输出LCD_Controller.vLCD1602 初始化、写命令、写数据时序rs/rw/en/data 总线Reset_Delay.v上电后延迟释放复位输入 clk、rst_n输出 reset_out这种安排的好处是键盘扫描不关心操作数是什么显示模块也不关心结果是怎么来的。键盘模块输出键值和一个单周期脉冲顶层状态机拿到这个脉冲后决定把数据放进 op1 还是 op2运算结束后顶层把结果锁存到显示寄存器SEG7_LUT 和 LCD_Controller 各自去取。2.2 顶层接口定义与模块实例化在 Quartus II 工程里顶层模块一般命名为calculator接口需要同时覆盖时钟、复位、矩阵键盘行列线、数码管段选位选和 LCD 数据控制线。一个常规的顶层声明如下。module calculator( input sys_clk, // 50MHz 板载时钟 input rst_n, // 外部复位按键低有效 input [3:0] key_row, // 矩阵键盘行输入 output [3:0] key_col, // 矩阵键盘列扫描 output [7:0] lcd_db, // LCD1602 数据总线 output lcd_rs, output lcd_rw, output lcd_en, output [6:0] seg_code, // 数码管 7 段码 output [3:0] seg_sel // 数码管位选 );键盘的 4 根行线必须设为输入4 根列线设为输出靠输出低电平来逐列扫描。key_scan实例化时把行、列信号直接连到顶层端口seg_code与seg_sel接数码管LCD 的 rs、rw、en 和 8 位数据总线独立引出。密钥矩阵键盘并没有锁存器需求所以全部逻辑都建在同一个全局时钟域内消除了异步采样问题。2.3 内部数据通路与时钟分配顶层内部的数据流可以概括为矩阵键盘扫描得到key_value和key_valid状态机根据当前状态决定把键值当作数字、运算符还是等号操作数寄存器 op1、op2 用移位加累加方式接收多位数运算电路把结果写进 32 位的result显示部分再从result的高低位拆出各个四位 BCD 码交给 SEG7_LUT 或 LCD_Controller。时钟方面50MHz 主时钟直接驱动状态机和按键消抖计数不需要做跨时钟域处理。对 10ms 级别的消抖时间和 LCD 写命令时的微秒级延时用计数器产生使能信号即可。若想进一步确认综合后的资源占用可以直接看工程目录里的calculator.fit.rpt和calculator.map.summary里面会列出每个模块综合后的 LE 数与寄存器数。3. verilog 矩阵键盘扫描、20ms 消抖与三段式状态机实现这一章是整个系统的核心。四位整数运算本身不难难的是怎样让一次按键只产生一个键值以及怎样在按键事件之间把两个操作数完整接住。矩阵键盘的物理结构决定了不能同时读所有按键必须分时扫描而按键抖动决定了必须在检测到低电平后等待约 20ms 再做确认否则一次按下会被当成多次按下。3.1 4x4 矩阵键盘的扫描原理4x4 矩阵键盘有 4 根行线、4 根列线共 16 个交叉点。行线接 FPGA 输入列线接 FPGA 输出。扫描时每一时刻只把一列拉低其余列输出高电平然后读整组行线如果某一行变成低电平说明该列与该行的交叉点被按下。例如扫描第 0 列时col 4b1110若row从4b1111变成4b1101则按键位于第 1 行第 0 列。键值映射关系如下这个工程里通常用 A 到 F 表示运算符。键值含义键值含义0-9数字输入A加法B减法C乘法D除法E等号F清除/复位其他保留扫描的关键在于列扫描速度不能太快也不能太慢。太快时人手的电容效应和信号竞争会让行线读到毛刺太慢则可能漏掉短促按键。常见做法是用 1kHz 左右的频率循环扫描四列也就是每列停留 1ms这样一轮扫描周期约 4ms结合 20ms 消抖时间刚好能过滤掉机械抖动。3.2 按键消抖与防连续触发的关键逻辑机械按键按下时金属簧片接触会产生 5ms 到 10ms 量级的电平抖动。为了得到干净的单周期脉冲我在 key_scan 内部用一个计数器来确认电平稳定。下面的代码片段是消抖与单脉冲生成的常用写法。reg [19:0] debounce_cnt; // 20ms 50MHz最大计数 1_000_000 reg key_pressed_reg; reg key_pressed_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin debounce_cnt 20d0; key_pressed_reg 1b0; end else if (key_row_stable ! 4b1111) begin // 检测到有键按下启动 20ms 计数 if (debounce_cnt 20d999_999) begin debounce_cnt 20d0; key_pressed_reg 1b1; // 20ms 后确认按键有效 end else debounce_cnt debounce_cnt 1b1; end else begin debounce_cnt 20d0; key_pressed_reg 1b0; // 按键释放立即复位 end end // 在 key_pressed_reg 上升沿产生单周期脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) key_valid 1b0; else begin key_valid 1b0; key_pressed_d key_pressed_reg; if (key_pressed_reg !key_pressed_d) key_valid 1b1; end end代码里key_row_stable是经过打拍对齐后的行线状态key_pressed_reg是高频时钟下的电平锁存。只有电平稳定 20ms 后才置高key_valid只在高电平的上升沿产生一个时钟周期的脉冲状态机只消费这个脉冲就不会出现按一次键跳两个数字的问题。此外这段代码在按键释放时立即清零计数器配合“确认后还要等释放完毕才允许下一轮扫描”的外围逻辑可以做到按住不放不会连续触发。3.3 三段式状态机管理操作数输入流程有了键值脉冲接下来要考虑的是用户按的到底是第一个操作数、运算符、第二个操作数还是等号引入有限状态机是最直接的做法。状态定义如下。localparam IDLE 3d0; // 等待第一个操作数 localparam INPUT_OP1 3d1; // 接收 op1 数字 localparam INPUT_OP2 3d2; // 接收 op2 数字 localparam CALC 3d3; // 执行运算占用一个周期 localparam DISPLAY 3d4; // 显示结果等待清除三段式状态机把状态转移、次态判定和输出逻辑拆开逻辑清晰且不容易出现 latch。状态转移核心逻辑如下。always (posedge clk or negedge rst_n) begin if (!rst_n) current_state IDLE; else current_state next_state; end always (*) begin next_state current_state; case (current_state) IDLE: begin if (key_valid is_digit(key_value)) next_state INPUT_OP1; end INPUT_OP1: begin if (key_valid is_operator(key_value)) next_state INPUT_OP2; else if (key_valid key_value 4hF) next_state IDLE; end INPUT_OP2: begin if (key_valid key_value 4hE) next_state CALC; else if (key_valid key_value 4hF) next_state IDLE; end CALC: begin next_state DISPLAY; end DISPLAY: begin if (key_valid key_value 4hF) next_state IDLE; end default: next_state IDLE; endcase end在 INPUT_OP1 状态下数字键并不会改变状态而是让操作数寄存器做移位累加。例如 op1 先存 1再按 2 时op1 op1 * 10 2变成 12。其他运算符键在 INPUT_OP2 状态同样用移位累加方式接收 op2。等号键触发 CALC运算在一个周期内完成然后进入 DISPLAY 状态把结果锁存到显示寄存器。将 F 设计为清除键这样在任何状态下都可以重新开始。3.4 关键参数调整对照参数推荐值说明列扫描周期1ms/列4 列循环一遍约 4ms消抖时间20ms覆盖绝大多数机械开关抖动键值脉冲宽度1 个系统时钟key_valid 只在 clk 上升沿产生系统时钟50MHz计数 1_000_000 即为 20ms操作数位宽16bit最大 9999留出余量如果把列扫描周期缩短到 0.1ms消抖时间也需要相应拉长否则毛刺更容易通过反过来如果消抖时间放大到 50ms快速连续按两个不同的键时可能会有输入丢失的感觉。手里没有示波器时可以先用 signaltap 或 ModelSim 抓key_row_stable和debounce_cnt确认抖动波形长度再定消抖值。4. 四位整数加减乘除 ALU、BCD 转换与 LCD1602 显示驱动状态机把输入数据准备好之后运算电路只负责一件简单的事情把 op1、op2 和运算符翻译成结果。这个工程的运算对象是四位整数也就是 0 到 9999加法结果可能超过 9999减法可能出现负数乘法最大可以得到 9999 × 9999 99980001结果位宽必须做到 32 位才安全。除法则需要专门作除零保护否则综合后的硬件上会得到一个不可预测的商。4.1 算数逻辑单元设计补码、溢出与除零保护运算部分放在一个由cal_start触发的 always 块里。cal_start由 CALC 状态产生持续一个时钟周期下面的代码以加法、减法、乘法、除法四类运算为例。always (posedge clk or negedge rst_n) begin if (!rst_n) begin result 32d0; calc_err 1b0; end else if (cal_start) begin calc_err 1b0; case (op_mode) 2b00: begin if (op1 op2 16d9999) begin calc_err 1b1; // 超出四位整数显示范围 end else result {16d0, op1 op2}; end 2b01: begin if (op1 op2) begin result[15:0] op1 - op2; result[31] 1b0; // 非负标志 end else begin result[15:0] op2 - op1; result[31] 1b1; // 负号标志 end end 2b10: begin result op1 * op2; // 32 位足够容纳 9999*9999 end 2b11: begin if (op2 16d0) begin calc_err 1b1; // 除零保护 result 32d0; end else result op1 / op2; // 商送显示余数丢弃 end endcase end end加法的溢出判断要看业务定义如果这个计算器只允许显示四位整数那么结果超过 9999 时直接报错比截断显示更合理。减法用补码思想处理result[31]当作符号位显示模块检测到这个位时输出一个负号。乘法的result是 32 位无符号数综合时 Quartus 会自动推断出硬件乘法器。除法直接使用/运算符会让综合器生成组合除法网络代价是逻辑资源较多四位十进制数范围内的除法用恢复余数法或者查表法会更节省资源但课程设计阶段用行为级/描述完全足够。4.2 二进制结果转 BCD 的移位加三法运算结果是二进制数而七段数码管和 LCD1602 需要按位显示十进制数字因此中间必须经过二进制到 BCD 的转换。对于 16 位二进制数最常用的可综合写法是移位加 3 法也叫 Double Dabble 算法。核心思想是将二进制逐位移入 BCD 寄存器每移动一位前检查各位 BCD 码是否超过 4超过则先加 3再移位。reg [15:0] bcd_buf; integer i; always (*) begin bcd_buf 16d0; for (i 0; i 16; i i 1) begin if (bcd_buf[3:0] 4d5) bcd_buf[3:0] bcd_buf[3:0] 4d3; if (bcd_buf[7:4] 4d5) bcd_buf[7:4] bcd_buf[7:4] 4d3; if (bcd_buf[11:8] 4d5) bcd_buf[11:8] bcd_buf[11:8] 4d3; if (bcd_buf[15:12] 4d5) bcd_buf[15:12] bcd_buf[15:12] 4d3; bcd_buf {bcd_buf[14:0], result_bin[15 - i]}; end end这段代码是组合逻辑仿真时可以看到 bcd_buf 从 0 开始逐步变成与 result_bin 对应的 BCD 码。四位整数最多需要四位 BCD 才能显示所以 bcd_buf 拆开后的高 4 位对应结果的最高位低 4 位对应个位。若运算结果是 8 位十进制则把 bcd_buf 扩展到 36 位循环次数也要相应增加到 32。相比之下用$display在仿真里看十进制值很方便但它不可综合不能替代这个转换电路。4.3 数码管刷新与显示锁存SEG7_LUT 是典型的查找表模块输入 4 位二进制数输出 7 段码。一般把数码管的段选和位选分开处理段选接 SEG7_LUT 的输出位选由扫描计数器决定当前点亮哪一位。多位数码管必须用动态扫描刷新频率至少要高于 75Hz 才不会明显闪烁。显示数据要在运算完成那一刻锁存一次不能在 DISPLAY 状态下让 bcd_buf 由于组合逻辑的中间过程而跳变否则数码管会闪现错误数字。常见做法是增加一组 display_reg只在cal_start或状态进入 DISPLAY 时更新。4.4 LCD1602 初始化命令与写时序LCD1602 相比数码管多了控制命令初始化顺序错误时屏幕上全是方块。上电后至少要等 40ms 才能发第一条命令这就是工程里Reset_Delay.v存在的必要性它用计数器把复位释放时间延迟到约 100ms确保 LCD 控制器完成自检。带 Reset_Delay 后整个系统复位不再由按键直接控制而是等延迟结束再放行。初始化命令序列按标准流程执行先 Function Set 0x38 设置 8 位总线、两行显示、5x8 点阵接着 Display ON 0x0C 开显示不显示光标再 Clear Display 0x01 清屏最后 Entry Mode 0x06 设置为写入数据后地址自动加一。每条命令之间需要延时写数据时的使能脉冲宽度不能低于 450ns。LCD_Controller.v 通常用一个状态机顺序执行这些命令执行过程中顶层只要把要显示字符的 ASCII 码放到数据总线上控制器内部会处理 RS、RW、EN 的时序。// 写命令状态片段 LCM_WR_CMD: begin lcd_rs 1b0; lcd_rw 1b0; lcd_db lcd_cmd; lcd_en 1b1; state LCM_DLY; end写命令时 RS 拉低写数据时 RS 拉高RW 一直为低表示只写不读。LCD_TEST.v 可以用来单独跑字符显示测试比如先让屏幕显示 “KEY ”再接上键盘输入键值这样调试输入问题时能直接看到按键是否进入系统。5. modelsim 仿真验证与上板后调时序的具体技巧把这套计算器烧进板子之前先用 ModelSim 做一次定向仿真能省下大量下载 bit 文件、插拔下载线的时间。仿真的重点不是看最终结果对不对而是看按键脉冲、状态机转移和 LCD 初始化时序这三个内部信号是否符合预期。在 ModelSim 里建立 testbench 后先跑通“按数字、按加号、按数字、按等号”的完整链路。为了让 tb 里的虚拟键盘产生按键事件直接对key_row和key_col赋值是不够的因为扫描模块一直在改变 col 输出。正确做法是由 testbench 生成列扫描信号对应的行输入也就是在 tb 里模仿真实键盘的行为当检测到某列为低时同行输出低电平。initial begin clk 0; forever #10 clk ~clk; // 50MHz end initial begin rst_n 0; #100 rst_n 1; // 模拟按下数字键 1对应行 0 列 0 wait (u_dut.key_col 4b1110); key_row 4b1110; #10; key_row 4b1111; // 松键 end这段代码里u_dut.key_col是层次化路径引用可以直接在 testbench 中观察 DUT 内部信号。仿真时如果等真实的 20ms 消抖时间跑一次完整按键输入会非常慢可以把 debounce_cnt 的目标值临时改小比如把20d999_999改成20d99确认逻辑正确后再改回。同样的技巧也适用于 LCD 初始化延时用localparam定义延时值仿真时改成很小的数上板前恢复避免反复下载 bit 文件。测试用例建议至少覆盖下面几类。用例输入期望输出关注点加法正常123 45168多位数移位累加是否正确加法溢出9999 1溢出标志边界值不做截断减法负数3 - 8显示 -5符号位处理乘法边界99 × 99980132 位结果中取低 16 位除法除零5 ÷ 0错误标志除零保护触发乘法最大9999 × 999999980001验证 32 位结果位宽足够上板后最容易出现的三个问题依次是按键跳数、LCD 乱码、数码管闪烁。按键跳数大多是消抖不完全或没有等待释放改善手段是加长消抖计数并把 key_valid 改为只在按键释放后产生LCD 乱码优先检查Reset_Delay.v的延迟时间是否足够再用单独模块验证每一条初始化命令之间延时是否完整数码管闪烁则把位选刷新时间缩短到 8ms 以内。最后一个常被忽视的技巧是当乘法结果超过四位时显示模块要决定是抹掉低位还是截断高位工程里建议把calc_err信号引到 LED超出显示范围时亮灯告警这样比屏幕上显示一段残缺数字更符合真实计算器行为。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价