简介:面向FPGA初学者的电子密码锁完整工程,适合数字逻辑、EDA技术或FPGA课程设计及入门练习。项目实现6位十进制密码锁,由四个独立按键分别控制系统启动、密码自增、自减与确认;密码逐位输入时数码管左移显示,校验正确亮绿灯,系统运行红灯指示,默认密码可在代码中修改,方便自定义。资源共147个文件,压缩包仅3.36MB,核心文件包括tdf设计源文件、Verilog源码、bdf原理图、vwf仿真波形、sof/pof下载配置及rpt报告等,目录结构清晰,可完整复现从代码设计、综合仿真到下载验证的流程。已有1085人学习该资源。通过工程内的按键消抖、数码管动态扫描、状态机控制、密码校验等模块,读者能快速理解FPGA组合逻辑和时序逻辑的配合方法;同时提供的仿真波形和项目说明,便于对照排查问题、修改密码或扩展功能,整体是一份实用且易上手的课设参考模板。
1. 从按键到门锁,FPGA电子密码锁到底在做什么
FPGA 电子密码锁是数字逻辑课与嵌入式实战交界处最常见也最容易讲透的项目。它的技术核心不是“锁”这个物理对象,而是把按键输入、数据存储、状态比较、使能输出这一整套流程用硬件状态机描述出来。你按下的每一个键经过消抖、编码后,在时钟节拍里与预设密码逐位比较,所有位全部对齐才拉高开锁信号。相比单片机方案,FPGA 最大的优势是时序透明:状态转移可以逐拍追踪,极少出现“逻辑明明对但跑起来不工作”的黑盒子问题。因此它既适合刚接触 Verilog 的人练手,也是理解有限状态机、时钟分频和引脚约束最直观的载体,整个设计对芯片资源和时钟频率的要求都很低,入门级开发板就能跑通。
2. FPGA电子密码锁的模块划分与选型思路:从矩阵键盘到开锁驱动
2.1 独立按键还是矩阵键盘:先决定面板物理形态
电子密码锁的输入面板通常有两种形态,一种是开发板上自带的几个独立按键,另一种是 3×4 或 4×4 的矩阵键盘。独立按键的好处是省去扫描逻辑,一个按键占用一个 FPGA 引脚,适合验证状态机本身;坏处是将来要扩到 0-9、#、* 十几个键时引脚不够,也不符合真实门锁的面板形态。
我一般建议毕设或产品原型直接上矩阵键盘。矩阵扫描的常规做法是列线输出低电平、行线回读,分时点亮每一列,这样 4×4 键盘只需要 8 个引脚。这里最容易翻车的是回读毛刺:列切换瞬间的 RC 充放电会产生短暂的不确定电平,如果不在行回读前插入几个空时钟周期,一次按键会被识别成两三次。因此扫描模块至少要拆成列驱动和行读回两个环节,中间插一个固定延时窗口。
2.2 消抖与按键编码:把物理抖动变成单周期脉冲
按键按下时,机械触点在 5 到 20 毫秒内会反复通断。以 50 MHz 系统时钟计算,20 毫秒对应 100 万个时钟周期,用计数器做消抖窗口非常容易。消抖本质上是低通滤波,不是把信号“变得干净”,而是持续采样到同一个电平一段时间后才认为有效,这能避开按键抖动产生的多个边沿。
下面是一个可综合的消抖模块,加入了常见的两步同步,降低单比特信号跨时钟域时的亚稳态风险:
// 按键消抖:连续采样稳定 CNT_MAX 拍后输出稳定电平 module debounce #( parameter CNT_MAX = 1_000_000 // 20ms @ 50MHz )( input wire clk, input wire key_in, // 原始按键电平 output reg key_out // 消抖后电平 ); reg [19:0] cnt; reg [1:0] sync; always @(posedge clk) begin sync <= {sync[0], key_in}; // 两步同步 if (sync[1] != sync[0]) begin cnt <= 20'd0; // 仍在抖动,清零重新计数 end else if (cnt == CNT_MAX) begin key_out <= sync[0]; // 稳定时间足够,确认电平 end else begin cnt <= cnt + 1; end end endmodule这段代码把 key_in 先打两拍,避免直接采集外部信号时出现亚稳态。计数器只在同步后的前后两拍值相同时才累加,一旦检测到跳变就清空,所以抖动期间不会误输出。实际使用时还要在消抖输出后面再跟一个检测上升沿的组合逻辑,生成单周期脉冲 key_pulse,状态机只认脉冲不认电平,否则按键一直按住时状态机会被反复触发。
2.3 键值编码和密码存储:寄存器堆比RAM好用
消抖和扫描之后,每个按键要对应一个固定的键值码。四位二进制能表示 0-15,正好装下 0-9、#、* 和一些功能键。下表是常见编码约定:
| 按键 | key_code | 含义 |
|---|---|---|
| 0 | 4'b0000 | 数字 0 |
| 1-9 | 4'b0001 ~ 4'b1001 | 数字 1-9 |
| # | 4'b1010 | 当前密码输入结束,触发校验 |
| * | 4'b1011 | 清除当前已输入内容 |
| 功能键 A | 4'b1100 | 进入修改密码模式 |
密码位数的存储我倾向用移位寄存器而不是 RAM。4 位密码用 16 比特 reg 就能放下,每一位存成 4 比特 BCD 码,直接一个左移拼接就能完成输入缓冲。如果密码长度是 6 位就需要 24 比特,8 位需要 32 比特,对 FPGA 来说都是微不足道的寄存器资源。相比用 Block RAM 按地址写、按地址读,寄存器堆不需要地址译码,综合后的时序也更可控。
3. 用Verilog写电子密码锁核心状态机:从IDLE到OPEN再到锁定
3.1 状态定义与独热码编码:先列出所有可能状态
电子密码锁的状态机从上电复位到一次完整开锁,至少要包含 IDLE、INPUT、CHECK、OPEN、LOCKED 这五个基本状态。更完整的设计还应该加上 CHANGE 状态,用于输入旧密码通过后的在线改密流程。把状态拆细在仿真阶段收益很大,比如单独留一个 CHECK 状态,示波器或在线逻辑分析仪上能清楚看到校验结果是在哪个周期产生的。
状态编码我建议使用独热码而不是二进制编码:
| 状态 | 独热码 | 进入条件 | 退出条件 |
|---|---|---|---|
| IDLE | 5'b00001 | 复位或定时结束 | 收到有效按键脉冲 |
| INPUT | 5'b00010 | IDLE 收到按键 | 收到 # 确认键 |
| CHECK | 5'b00100 | # 键触发 | 校验结果落定 |
| OPEN | 5'b01000 | 校验通过 | 开锁定时结束 |
| LOCKED | 5'b10000 | 连续错误达到次数 | 锁定时长结束 |
独热码状态译码用一条相等比较就能完成,组合逻辑延迟最小,时序分析时不常出现列选通毛刺。缺点是状态数量多一个就多一个寄存器,但对这种几百个 LUT 的小设计完全无感。
3.2 三段式状态机模板:次态、现态和输出分开写
三段式状态机是 FPGA 开发的常见套路,分别用两个时序块寄存 state 和输出,用一个组合块计算 next_state。这样做最大的好处是检查状态跳转逻辑时不会被输出逻辑干扰,出现问题时能快速定位故障属于组合判断还是寄存器时序。
module lock_fsm( input wire clk, input wire rst_n, input wire key_pulse, // 消抖后的单周期按键脉冲 input wire [3:0] key_code, // 按键编码 output reg unlock, // 开锁信号 output reg err_ind // 错误提示,可接LED ); localparam IDLE = 5'b00001; localparam INPUT = 5'b00010; localparam CHECK = 5'b00100; localparam OPEN = 5'b01000; localparam LOCKED = 5'b10000; reg [4:0] state, next_state; reg [15:0] secret_pwd; // 预设密码,4位BCD reg [15:0] input_buf; // 当前输入缓冲 reg [1:0] err_cnt; // 错误计数,最大3 wire pwd_match = (input_buf == secret_pwd); // 第一段:现态寄存 always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end // 第二段:次态组合跳转 always @(*) begin next_state = state; case (state) IDLE: begin if (key_pulse) next_state = INPUT; end INPUT: begin if (key_pulse && key_code == 4'b1010) // # 键 next_state = CHECK; end CHECK: begin if (pwd_match) next_state = OPEN; else if (err_cnt >= 2'd2) next_state = LOCKED; // 连续错满3次 else next_state = IDLE; // 未满次数,下次重新输入 end OPEN: begin next_state = IDLE; // 开锁定时后自复位 end LOCKED: begin if (lock_timer_full) next_state = IDLE; end endcase end // 第三段:输出与错误计数 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin unlock <= 1'b0; err_cnt <= 2'd0; end else begin if (state == CHECK && !pwd_match) err_cnt <= err_cnt + 2'd1; else if (state == LOCKED) err_cnt <= 2'd0; if (state == OPEN) unlock <= 1'b1; else unlock <= 1'b0; end end endmodule注意第二个 always 块里没有遗漏默认赋值,开头先写next_state = state;再进 case,这是防止组合逻辑产生锁存器的标准写法。err_cnt 的增长没有放在组合块里,而是在第三个时序块中根据 CHECK 状态和比较结果触发,这样连续按错三次时,每次错误只会计数一次,不会因为同一轮校验重复自增。unlock 的赋值也不要做成锁存模式,除了 OPEN 状态外直接置低,综合后不会生成意外的存储单元。
3.3 错误计数与定时锁定:防暴力试探的最简实现
错误计数和锁定定时是成品电子密码锁的标配,没有这一步的密码锁在安全意识上是不合格的。上面的代码已经把错误计数放在第三段,现在要补的是 lock_timer_full 信号:一个自由运行的计数器在 LOCKED 状态下从零累加,计到 30 秒对应的周期数后拉高。注意这个计数器只在 LOCKED 时使用,其他状态要保持清零,避免重新进入 LOCKED 时有残留计数值。
调试时最常遇到的坑是错误计数提前复位。有些设计把复位条件写成state != LOCKED,结果从 CHECK 回到 IDLE 的瞬间计数被清掉,第二次按错又从零开始,永远进不了锁定。正确的做法是只在 LOCKED 状态内清零,或者干脆做成独占复位条件,IDLE 状态里只负责等待按键,不碰计数。
4. 在Vivado或Quartus里完成约束、仿真和板级调试
4.1 最小工程结构与芯片选型:LUT资源和引脚数量哪个更关键
拿到一块入门级开发板,无论上面是 Artix-7、Cyclone IV 还是国产的高云或安路芯片,电子密码锁的资源消耗都很小,几十个 LUT、几十个寄存器就够用。真正决定选型的是引脚数量:要接矩阵键盘、数码管、LED、蜂鸣器、电磁锁驱动和扩展接口,引脚少于 50 的芯片会非常紧张。因此建工程前先查板卡的引脚分配表,确认用到的外设都能映射到可用引脚上。如果工程向导里找不到板卡对应的芯片型号,通常是器件支持包没装全,需要去厂商工具网站补装对应系列,而不是随便选一个相近型号代替,不然综合后引脚和约束文件全部对不上。
4.2 约束文件怎么写给后续排错留余地
约束文件至少要覆盖主时钟、复位、按键输入和开锁输出四类信号。没有时钟约束的工程时序分析结果没有参考意义,Vivado 会默认一个非常宽松的时钟周期,综合和实现也能过,但时序报告完全失真。以 50 MHz 外部晶振为例,最少要写下面这几行:
create_clock -period 20.000 -name sys_clk [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports {clk}] set_property IOSTANDARD LVCMOS33 [get_ports {rst_n}] set_property IOSTANDARD LVCMOS33 [get_ports {key_col0}] set_property PACKAGE_PIN E3 [get_ports {clk}] set_property PACKAGE_PIN C4 [get_ports {rst_n}] set_property PACKAGE_PIN A2 [get_ports {key_col0}]IOSTANDARD 要和板卡 Bank 电压严格一致,3.3V 的管脚写成 LVCMOS25 后上板电平不确定,严重时会损伤引脚。若按键输入距离 FPGA 较远,还应补 input delay 约束,例如set_input_delay -clock sys_clk -max 5 [get_ports {key_col*}],让时序分析把走线延迟纳入计算。不写这条约束,综合工具会默认外部信号与时钟边沿对齐,板上延迟稍大就会出现偶发采样错误。
4.3 testbench这样写才能快速暴露状态机问题
仿真测试要覆盖三条核心路径:正常密码开锁、错误密码未满次数的恢复、错误满三次后的锁定。下面这个 testbench 跑的是正常开锁流程,它用一个 task 模拟每次按键的单周期脉冲:
module tb_lock_fsm; reg clk = 0; reg rst_n = 0; reg key_pulse = 0; reg [3:0] key_code = 0; wire unlock; wire err_ind; lock_fsm uut( .clk(clk), .rst_n(rst_n), .key_pulse(key_pulse), .key_code(key_code), .unlock(unlock), .err_ind(err_ind) ); always #10 clk = ~clk; // 50MHz时钟 task press(input [3:0] code); begin key_pulse = 1; key_code = code; #40; key_pulse = 0; #40; end endtask initial begin #10 rst_n = 1; press(4'd1); press(4'd2); press(4'd3); press(4'd4); press(4'd10); // 按#触发校验 #200; $display("unlock=%b", unlock); $stop; end endmodule里面 task press 把 key_pulse 拉高两个时钟周期再拉低,模拟一次完整的有效按键。如果仿真结果里 unlock 没有按预期拉高,建议在 testbench 里加一句打印 state,通过状态编码判断是卡在 INPUT 还是 CHECK。时序仿真里最容易被忽略的是复位释放后的第一个时钟沿,如果 rst_n 释放时间和 clk 上升沿靠得太近,第一拍状态可能采样不到,从而多花一到两个时钟周期才进入 IDLE。
4.4 上板调试顺序:先看最小系统再看状态跳转
上板和仿真完全是两种排错思路,仿真看不到引脚毛刺和外部干扰。我的调试顺序固定为四步:先确认时钟引脚上有正确频率的波形,再确认复位释放后 LED 有反应,然后按单键观察消抖输出,最后才连上完整状态机。如果时钟正常但按键无响应,先测按键在 FPGA 引脚上静态是高还是低,很多板载按键默认上拉到高电平,按下为低,而为高的时候 key_in 可以用逻辑分析仪里键值来对齐。遇到状态机总是来回跳的情况,直接使用 ILA 在线逻辑分析仪抓 state 信号,触发条件设为state == CHECK,立刻能看出是校验失败还是跳转条件被时序阻塞,这比一段段翻代码快得多。
5. 上生产前必调的三个参数:掉电存储、开锁脉冲和复位策略
5.1 把密码搬进EEPROM或SPI Flash才能掉电保存
FPGA 的寄存器密码一掉电就丢,演示阶段无所谓,要做成产品就必须外挂非易失存储。最省事的方案是挂一颗 I2C 接口的 EEPROM,容量 2Kbit 就足够,上电后由状态机从 EEPROM 读出密码并做一次校验,确认读回数据合法后开放按键输入。要注意 I2C 速率一般只有 100KHz 或 400KHz,FPGA 主频是 50MHz 以上,必须单独做一个 I2C 分频时序,不能让主状态机直接跨两个时钟域去操作 EEPROM。如果你的板卡上有一颗 SPI Flash,例如 W25Q16 这类常用型号,也能存密码,但 SPI Flash 是按页写入而不是按字节任意改写,写一页前必须先擦除整个扇区,做起来比 I2C EEPROM 繁琐,除非 Flash 本身闲置且想顺带实现 FPGA 在线升级,否则我更建议老老实实挂 I2C。
5.2 开锁脉冲不是越宽越好,还要避开默认上电解锁
开锁信号的持续时间要匹配电磁锁动作时间。我一般设置在 0.5 到 1 秒之间:太短锁舌还没完全复位就断电,长期使用会加速机械结构磨损;太长则白白耗电。驱动方式上,很多入门板直接让 FPGA 引脚去推三极管再驱动 MOSFET,这种做法要格外留意开漏和上拉的配合。假如你的驱动板是低电平触发,开锁输出就应该设计成默认高阻或上拉,而不是上电时为低,否则 FPGA 配置完成前输出未定状态,就可能出现上电瞬间误开锁的现象。
// 低电平有效的开锁输出示例:默认置高,避免上电误开锁 parameter OPEN_TIME_CNT = 25_000_000; // 0.5s @50MHz reg open_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin open_cnt <= 0; end else if (state == OPEN) begin if (open_cnt == OPEN_TIME_CNT) begin open_cnt <= 0; end else begin open_cnt <= open_cnt + 1; end end else begin open_cnt <= 0; end end assign lock_out = (state == OPEN) ? 1'b0 : 1'b1; // 低电平脱锁上面这个 assign 把输出定义成低电平使能方式,FPGA 配置完成前外部驱动电路的主控端默认是高电平,不会误触发开锁。如果你的电磁锁驱动板是高电平触发,就把这段逻辑取反,但务必要确认上电瞬间驱动板的主控端是被电阻拉死的,而不是悬空。
5.3 错误阈值、锁定时间与ILA抓状态验证
错误次数阈值到底选 3 次还是 5 次,取决于场景。家用门锁我倾向 5 次配 10 秒短锁定,减少老人小孩误触造成的长时间等待;仓储柜这类防暴力试探需求高的,用 3 次配 60 秒以上。这两个参数做成顶层 parameter 后,现场部署时只需要改约束或者由上位机下发配置就能调整,不需要为不同场景重新综合一版固件。
验证锁定逻辑最直接的方式是打开 ILA,把触发条件设置为state == LOCKED,然后故意输错三次密码,观察 err_cnt 是否依次自增到 3,锁定期间触发条件只命中一次。如果 err_cnt 一次跳了两个数,说明按键消抖后产生了多余脉冲,问题多半出在扫描模块而不是状态机本身。这种验证方法比示波器量引脚信号可靠,ILA 抓的是 FPGA 内部真实寄存器值,能把错误计数、状态编码、开锁信号放在同一时间轴上看因果顺序。调试通过后,记得把这几个参数收敛成一组适合批量生产的默认值,并把 ILA 的调试核从最终工程里移除,避免占用额外的 BRAM 布线资源。
本文还有配套的精品资源,点击获取