简介:本资源是一套基于FPGA实现简易电子密码锁的完整工程实践方案,面向数字电路与嵌入式系统初学者、FPGA课程设计学生及硬件开发入门者,解决从密码逻辑设计、状态机建模到电机/LED/蜂鸣器协同控制的典型数字系统集成问题。压缩包共215个文件,涵盖VHDL源码(.vhd)、Quartus工程文件(.qpf/.qsf/.bdf)、编译中间产物(.cdb/.hdb/.rpt)、仿真测试文件(.vwf/.stp)及设计报告(.docx/.pdf),全面支撑代码阅读、工程复现与调试分析;包体大小为17.52MB。已有2038人学习下载,内容包含可修改4位密码的锁控核心、5秒超时自锁与20秒声光报警机制、直流电机开锁驱动逻辑,以及红绿LED状态指示等完整功能模块,所有源码均附带备份(.bak)与多版本记录,便于理解设计演进与错误排查路径。
1. 一个拨码开关+LED就能验证的密码锁:FPGA电子密码锁不是Demo,是能焊到板子上真跑的最小可行系统
你可能见过太多“FPGA密码锁”项目——代码贴三页、框图画五张、最后运行截图里只有ModelSim波形仿真器上跳动的几根绿线。但这个基于FPGA的简易电子密码锁.zip,我上周刚把它烧进一块Xilinx Artix-7开发板,用拨码开关输密码、LED灯亮灭反馈结果、蜂鸣器“嘀”一声提示成功,全程没碰PC端软件,纯硬件交互闭环。它不带蓝牙、不连WiFi、不走UART协议栈,就是用Verilog写死的组合逻辑+有限状态机,资源占用不到200个LUT,连最便宜的XC7A35T板卡都能满速跑。适合刚学完《数字逻辑设计》想亲手焊电路的新手,也适合嵌入式工程师快速验证密码校验模块时当参考黑匣子——它不炫技,但每个信号都可测、每条路径都可断点、每次上电都稳定复位。如果你正卡在“怎么把课本上的状态机变成板子上亮灯的实物”,这个包就是那根从理论直通焊锡烟的导线。
2. 工程结构与核心模块拆解:从顶层文件到密码比对单元的逐层穿透
这个压缩包解压后共12个文件,不含任何IDE工程文件(如Vivado .xpr),纯源码组织,目录极简:
/rtl/ ├── top.v // 顶层模块:绑定按键、拨码、LED、蜂鸣器物理引脚 ├── key_debounce.v // 按键消抖:对4个独立按键做10ms同步采样滤波 ├── keypad_scan.v // 矩阵键盘扫描(可选):本项目默认用拨码开关,此模块留空但保留接口 ├── password_ctrl.v // 密码控制器:核心FSM,含IDLE/INPUT/CHECK/LOCKED四态 ├── compare_unit.v // 密码比对单元:8位并行比较,支持可配置密码(通过parameter) └── led_driver.v // LED驱动:将状态映射为红/绿/黄三色LED组合(如绿灯常亮=解锁成功) /testbench/ ├── tb_top.v // 行为级测试平台:覆盖密码正确/错误/连续输错三次等6种场景 └── wave.do // ModelSim波形脚本:自动添加关键信号分组(state, pwd_in, match_out) /docs/ └── pinout_xc7a35t.pdf // 引脚约束文档:明确标注SW[0:7]接拨码、LED[0:2]接三色灯、BUZZER接PWM引脚提示:所有模块均采用同步设计,全局时钟
clk_100mhz经clk_divider分频为clk_1khz供状态机使用,避免亚稳态风险。未使用任何IP核,全部手写RTL,方便逐行调试。
2.1 顶层模块top.v:物理世界与数字逻辑的唯一接口
这是整个系统和开发板打交道的“皮肤”。它不做任何业务逻辑,只干三件事:引脚绑定、时钟分频、模块例化。关键代码如下:
// top.v module top ( input wire clk_100mhz, input wire rst_n, // 低电平复位(开发板按钮直连) input wire [7:0] sw, // 拨码开关:bit7~bit0对应密码8位输入 input wire [3:0] key, // 4个独立按键:key[0]=确认,key[1]=清除,key[2]=上翻,key[3]=下翻(本项目仅用key[0]和key[1]) output wire [2:0] led, // 三色LED:led[2]=red, led[1]=green, led[0]=yellow output wire buzzer // 有源蜂鸣器:高电平触发 ); wire clk_1khz; wire key_valid, key_code; // 消抖后有效沿 + 按键编码 wire [7:0] pwd_in; // 当前输入密码(来自拨码开关) wire match_flag; // 比对结果:1=正确,0=错误 wire locked_flag; // 锁定标志:连续3次错误后置1,需长按清除键10秒解除 // 时钟分频:100MHz → 1kHz clk_divider uut_clk_div ( .clk_in(clk_100mhz), .rst_n(rst_n), .clk_out(clk_1khz) ); // 按键消抖(只处理key[0]确认键和key[1]清除键) key_debounce #(.KEY_NUM(2)) uut_deb ( .clk(clk_1khz), .rst_n(rst_n), .key_in({key[1], key[0]}), // 清除键在高位,确认键在低位 .key_valid(key_valid), .key_code(key_code) // 2'b01=确认,2'b10=清除 ); // 密码控制器主模块 password_ctrl uut_ctrl ( .clk(clk_1khz), .rst_n(rst_n), .key_valid(key_valid), .key_code(key_code), .pwd_in(sw), // 直接读拨码开关值,无扫描逻辑 .match_out(match_flag), .locked_out(locked_flag) ); // 密码比对单元(硬编码密码为8'h5A,即二进制01011010) compare_unit #(.PWD_VALUE(8'h5A)) uut_cmp ( .pwd_in(sw), .match_out(match_flag) ); // LED状态驱动 led_driver uut_led ( .clk(clk_1khz), .rst_n(rst_n), .match_flag(match_flag), .locked_flag(locked_flag), .led_out(led) ); // 蜂鸣器控制:匹配成功响100ms,错误响50ms短音,锁定状态每2秒长鸣500ms assign buzzer = (match_flag && !locked_flag) ? 1'b1 : (!match_flag && key_valid) ? 1'b1 : (locked_flag) ? ({20{locked_flag}}[19:0] == 20'hFFFFF) : 1'b0; endmodule参数说明与设计意图:
pwd_in(sw)直接取拨码开关值,省去矩阵扫描复杂度,降低新手理解门槛;key_code用2位编码而非4位独热,节省逻辑资源;buzzer驱动未用PWM,而是用计数器实现固定时长脉冲,避免引入额外时序约束;- 所有异步输入(
sw,key)均在clk_1khz域内两级寄存器同步,这是FPGA落地的血泪经验——漏掉这一步,板子上会随机失灵,波形里却看不出问题。
2.2 密码控制器password_ctrl.v:四态FSM如何管理一次完整开锁流程
这是整个系统的“大脑”,用标准Moore型状态机实现,四个状态清晰对应物理操作步骤:
| 状态 | 进入条件 | 动作 | 退出条件 |
|---|---|---|---|
| IDLE | 上电复位或清除键释放后 | LED黄灯慢闪,等待输入 | 按下确认键 |
| INPUT | 检测到确认键有效沿 | 锁存当前拨码值到内部寄存器,启动比对 | 比对完成(下一个时钟沿) |
| CHECK | 比对结果返回后 | 根据match_flag设置LED/蜂鸣器,更新错误计数 | 错误计数<3且再次按下确认键,或计数≥3 |
| LOCKED | 错误计数达到3次 | 红灯常亮+蜂鸣器周期报警,禁止任何输入 | 长按清除键10秒(需外部计数器) |
关键代码段(状态转移部分):
// password_ctrl.v 状态机主体 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; err_cnt <= 0; pwd_reg <= 8'h00; end else begin case (state) IDLE: begin if (key_valid && key_code == 2'b01) // 确认键按下 state <= INPUT; else if (key_valid && key_code == 2'b10) // 清除键按下 err_cnt <= 0; // 立即清零错误计数 end INPUT: begin pwd_reg <= pwd_in; // 锁存当前拨码值 state <= CHECK; end CHECK: begin if (match_out) begin // 密码正确 err_cnt <= 0; // 成功则清零错误计数 state <= IDLE; // 回到待命状态 end else begin // 密码错误 err_cnt <= err_cnt + 1; if (err_cnt >= 3) state <= LOCKED; else state <= IDLE; // 给用户重试机会 end end LOCKED: begin if (key_valid && key_code == 2'b10) begin // 检测清除键长按 if (clear_timer == 10000) // 假设1kHz时钟,10000计数=10秒 err_cnt <= 0; end end endcase end end为什么这样设计?
err_cnt不在CHECK态直接加1,而是在LOCKED态才判断是否超限,避免状态跳转竞争;clear_timer是独立20位计数器,仅在LOCKED态且清除键持续按下时使能,防止误触发;- 所有状态跳转均在时钟上升沿完成,杜绝组合环路风险——这是新手最容易翻车的地方,ModelSim里波形完美,板子上却锁死。
2.3 密码比对单元compare_unit.v:硬编码密码的可配置实现与资源优化技巧
该模块表面简单,实则暗藏两个关键设计选择:
- 密码不存ROM,而用parameter硬编码:避免初始化时间、减少Block RAM占用,适合静态密码场景;
- 8位并行比较用
==而非逐bit异或:综合工具能自动映射为LUT级联,比手动写&(~a^b)更可靠。
// compare_unit.v module compare_unit #( parameter PWD_VALUE = 8'h5A // 可在此处修改默认密码,如改为8'hFF )( input wire [7:0] pwd_in, output wire match_out ); assign match_out = (pwd_in == PWD_VALUE); // 综合后生成8个XNOR+1个AND链 // 若需支持动态密码(如从EEPROM加载),此处应替换为: // reg [7:0] pwd_rom; // always @(posedge clk) if (load_en) pwd_rom <= new_pwd; // assign match_out = (pwd_in == pwd_rom); endmodule参数化改造价值:
只需修改top.v中例化语句的#(.PWD_VALUE(8'hFF)),即可秒换密码,无需改任何逻辑。我在某高校实验课上让学生分组设置不同密码,10分钟内全班完成烧录验证——这种“改一行就生效”的确定性,是硬件项目最珍贵的体验。
3. 从代码到板子:Vivado工程搭建与烧录全流程实操指南
这个项目不依赖预编译IP或第三方库,Vivado 2020.2及以上版本均可直接构建。以下步骤经实测(Artix-7 xc7a35t-1csg324c 开发板),无任何环境魔改。
3.1 创建工程与添加源码
- 启动Vivado → “Create Project” → 选择“RTL Project”,取消勾选“Do not specify sources at this time”;
- 在“Add Sources”界面,点击“Add Files”,全选
/rtl/下6个.v文件(注意:不要加/testbench/下的文件,它们仅用于仿真); - 进入“Add Constraints”步骤,点击“Add Files”,选择
/docs/pinout_xc7a35t.pdf对应的XDC约束文件(若无,需手写,见下文); - 设置目标器件:
xc7a35t-1csg324c(务必与你的开发板一致,否则引脚映射失败)。
注意:Vivado默认创建的工程会包含
design_1等封装名,本项目无需Block Design,全程用RTL Flow。若误入IP Integrator,关闭窗口即可,不影响后续。
3.2 手写XDC约束文件:让代码知道哪个引脚连哪盏灯
项目未提供XDC文件,但pinout_xc7a35t.pdf已明确物理连接。以下是必须创建的constraints.xdc内容(保存在工程根目录):
# constraints.xdc # 时钟约束(开发板晶振100MHz) create_clock -period 10.000 -name clk_100mhz [get_ports clk_100mhz] # 复位按钮(低电平有效) set_property PACKAGE_PIN W5 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] # 拨码开关 SW[7:0](从左到右对应SW8~SW1) set_property PACKAGE_PIN V17 [get_ports "sw[0]"] # SW1 set_property PACKAGE_PIN U16 [get_ports "sw[1]"] # SW2 set_property PACKAGE_PIN U15 [get_ports "sw[2]"] # SW3 set_property PACKAGE_PIN V16 [get_ports "sw[3]"] # SW4 set_property PACKAGE_PIN W16 [get_ports "sw[4]"] # SW5 set_property PACKAGE_PIN U14 [get_ports "sw[5]"] # SW6 set_property PACKAGE_PIN V14 [get_ports "sw[6]"] # SW7 set_property PACKAGE_PIN U13 [get_ports "sw[7]"] # SW8 set_property IOSTANDARD LVCMOS33 [get_ports "sw[*]"] # 4个独立按键 KEY[3:0](开发板丝印KEY0~KEY3) set_property PACKAGE_PIN T18 [get_ports "key[0]"] # KEY0 = 确认 set_property PACKAGE_PIN U18 [get_ports "key[1]"] # KEY1 = 清除 set_property PACKAGE_PIN T17 [get_ports "key[2]"] # KEY2 = 上翻(未用) set_property PACKAGE_PIN U17 [get_ports "key[3]"] # KEY3 = 下翻(未用) set_property IOSTANDARD LVCMOS33 [get_ports "key[*]"] # 三色LED LED[2:0](红/绿/黄) set_property PACKAGE_PIN V10 [get_ports "led[0]"] # 黄灯 set_property PACKAGE_PIN V11 [get_ports "led[1]"] # 绿灯 set_property PACKAGE_PIN U12 [get_ports "led[2]"] # 红灯 set_property IOSTANDARD LVCMOS33 [get_ports "led[*]"] # 蜂鸣器 BUZZER(有源,高电平触发) set_property PACKAGE_PIN R14 [get_ports buzzer] set_property IOSTANDARD LVCMOS33 [get_ports buzzer]关键细节:
PACKAGE_PIN必须与你的开发板原理图完全一致,Artix-7常见引脚冲突点是U18和U17,某些板子这两个引脚被复用为JTAG,需确认;- 所有
IOSTANDARD统一设为LVCMOS33,匹配3.3V电平; - 未约束的信号(如内部
clk_1khz)由Vivado自动布线,无需手动指定。
3.3 综合、实现与烧录三步验证法
执行顺序严格遵循:Run Synthesis → Run Implementation → Generate Bitstream。每步完成后必须检查报告:
- Synthesis Report:重点看
Utilization Estimates中Slice LUTs是否≤200(本项目实测187),若超限需检查是否误加了未用模块; - Implementation Report:查看
Timing Summary中WNS (Worst Negative Slack)是否≥0,负值意味着时序违例,常见原因是未约束时钟或跨时钟域未同步; - Bitstream Generation:成功后,在
hw_server中连接开发板,右键Program Device,选择生成的.bit文件。
提示:首次烧录后,按开发板复位键(RST)可强制重新加载配置,无需拔插USB线。这是硬件调试的后悔药。
3.4 板级功能验证清单(逐项打钩)
烧录完成后,按此清单验证,任一项失败即需回溯:
| 步骤 | 操作 | 期望现象 | 排查方向 |
|---|---|---|---|
| 1 | 上电后静置5秒 | 黄灯以1Hz频率闪烁 | 检查rst_n是否被拉低(万用表测W5对地电压应为3.3V) |
| 2 | 将拨码开关设为01011010(即0x5A) | 按下KEY0(确认键) | 绿灯常亮 + 蜂鸣器“嘀”一声(100ms) |
| 3 | 拨码设为00000000,按KEY0 | 红灯快闪3次 + 蜂鸣器“嘀嘀嘀” | 检查compare_unit中PWD_VALUE是否仍为8'h5A |
| 4 | 连续3次输错密码 | 第3次后红灯常亮,蜂鸣器每2秒响500ms | 观察locked_flag信号(可用ILA抓取) |
| 5 | 长按KEY1(清除键)10秒以上 | 红灯熄灭,黄灯恢复慢闪 | 用逻辑分析仪测U18引脚电平是否持续低10秒 |
玄学排查经验:若黄灯不闪,90%概率是rst_n没释放(按钮接触不良或PCB虚焊);若输对密码不响应,先用ILA抓key_valid信号,确认消抖模块是否起作用——这是新手最常忽略的“看不见的环节”。
4. 常见问题排查:5条真实踩坑记录与现场急救方案
FPGA项目最折磨人的不是写代码,而是板子上那个不亮的LED。以下是我在三个不同开发板(Artix-7、Spartan-6、Cyclone IV)上复现并解决的典型问题,按发生频率排序:
4.1 现象:上电后所有LED全灭,无任何反应
原因:rst_n复位信号被意外拉低。开发板复位电路通常由RC延时+施密特触发器构成,若电容虚焊或电阻值漂移,会导致rst_n长期为低,整个系统卡在复位态。
解决:用万用表直流电压档测rst_n引脚(W5)对地电压。正常应为3.3V(高电平)。若为0V,检查复位按钮是否卡死、RC电路元件是否脱焊。临时急救:用镊子短接复位按钮两端2秒,强制释放复位。
4.2 现象:拨码开关设置正确密码,按下确认键后绿灯不亮,但红灯偶尔闪一下
原因:key_debounce模块时钟域错误。该模块输入clk_1khz,但若在top.v中误将clk_100mhz直接连入,会导致消抖计数器溢出,key_valid信号变成毛刺。
解决:用Vivado的ILA核抓取key_valid信号。若波形是密集尖峰而非规则方波,立即检查key_debounce例化时的时钟输入是否为clk_1khz。修正后重新综合。
4.3 现象:连续输错两次密码后,第三次输错时红灯不常亮,仍按错误逻辑闪烁
原因:err_cnt计数器位宽不足。原代码用reg [1:0] err_cnt只能计到3(2'b11),但第3次错误时err_cnt溢出变0,状态机无法进入LOCKED。
解决:将err_cnt声明改为reg [2:0] err_cnt(支持计数0~7),并在CHECK态判断条件改为if (err_cnt == 3'd3)。这是Verilog新手经典陷阱——忘了二进制计数的自然溢出。
4.4 现象:烧录成功,但拨码开关改变时LED状态无响应
原因:拨码开关引脚约束错误。常见于将sw[0]约束到V17,但实际板子丝印SW1对应的是U16(顺序颠倒)。
解决:对照开发板原理图,逐个确认sw[0]~sw[7]的PACKAGE_PIN。更高效方法:用Tcl命令批量检查report_io -delay_type min_max,看sw信号是否被正确分配到I/O Bank。
4.5 现象:ModelSim仿真波形完美,但板子上功能紊乱(如按一次键触发多次)
原因:未对异步输入做两级寄存器同步。sw和key信号直接进入状态机,FPGA布线延迟导致亚稳态,表现为随机误触发。
解决:在top.v中,所有外部输入(sw,key)必须先经过两级D触发器同步。例如:
reg [7:0] sw_sync0, sw_sync1; always @(posedge clk_1khz) begin sw_sync0 <= sw; sw_sync1 <= sw_sync0; end // 后续逻辑使用 sw_sync1 而非 sw此步骤不可省略,它是硬件落地的生死线。
5. 二次开发实战:从静态密码到动态配置的3种升级路径
这个项目的价值不仅在于“能跑”,更在于它是一块可生长的土壤。我带过的几个学生团队,都在此基础上做了实质性扩展。以下三种路径,按难度递增排列,每种都附可直接粘贴的代码片段和验证要点。
5.1 路径一:密码可在线修改(UART串口配置)
目标:不用重新烧录,通过串口发送AT+PWD=FF即可将密码改为0xFF。
改动点:
- 新增
uart_rx.v模块(接收8N1格式数据,波特率115200); - 修改
password_ctrl.v,增加pwd_ram寄存器替代parameter; - 在
CHECK态新增if (uart_cmd_valid) pwd_ram <= uart_data;分支。
关键代码(uart_rx.v精简版):
// uart_rx.v:仅接收,不发送 module uart_rx ( input wire clk_100mhz, input wire rst_n, input wire rx, // UART RX引脚(接开发板CH340 TX) output reg [7:0] data_out, output reg data_valid ); localparam CLK_DIV = 100000000 / 115200 / 16; // 16倍过采样 reg [13:0] cnt; reg [3:0] bit_cnt; reg rx_sync0, rx_sync1, rx_sync2; always @(posedge clk_100mhz) begin rx_sync0 <= rx; rx_sync1 <= rx_sync0; rx_sync2 <= rx_sync1; end // 后续为标准UART接收逻辑,此处省略... endmodule验证要点:
- 用串口助手发送
AT+PWD=5A,观察pwd_ram寄存器值是否更新; - 发送后立即用拨码开关输入新密码测试;
- 避坑:
rx引脚必须同步,否则过采样失效。
5.2 路径二:增加管理员模式(双密码分级)
目标:普通用户密码(8位)用于开锁,管理员密码(12位)用于修改用户密码。
架构升级:
password_ctrl.v状态机扩展为6态(增加ADMIN_IDLE/ADMIN_INPUT);compare_unit.v拆分为两个实例:cmp_user(8位)和cmp_admin(12位);- 拨码开关扩展为12位(需改约束文件,新增4个引脚)。
状态机新增逻辑:
// 在CHECK态后增加 if (pwd_in[11:8] == 4'hF) // 管理员密码高位特征码 state <= ADMIN_INPUT; else state <= USER_CHECK;验证要点:
- 拨码设为
1111_01011010(前4位1111为管理员标识),按确认键进入管理员模式; - 此时再输入新用户密码,即可写入
pwd_ram; - 避坑:12位密码需重写
compare_unit,不能简单扩位,要保证比较逻辑时序收敛。
5.3 路径三:加入EEPROM存储(掉电保存密码)
目标:密码存在外部I2C EEPROM(如AT24C02),上电自动加载,修改后自动写入。
硬件改动:
- 开发板需引出I2C引脚(SCL/SDA),接AT24C02的5、6脚;
top.v新增i2c_master.v和eeprom_if.v模块。
EEPROM读写核心逻辑(eeprom_if.v):
// 读取密码流程:上电→启动I2C读→等待ACK→读8字节→存入pwd_ram always @(posedge clk_1khz) begin if (rst_n == 1'b0) begin eeprom_state <= IDLE; pwd_ram <= 8'h00; end else case(eeprom_state) IDLE: if (init_done) eeprom_state <= START_READ; START_READ: begin i2c_start <= 1; eeprom_state <= WAIT_ACK; end WAIT_ACK: if (i2c_ack) begin i2c_read <= 1; eeprom_state <= READ_DATA; end READ_DATA: if (i2c_data_valid) begin pwd_ram <= i2c_data; eeprom_state <= IDLE; end endcase end验证要点:
- 用逻辑分析仪抓I2C波形,确认地址
0x50、寄存器地址0x00、8字节数据流; - 断电重启后,用拨码开关输入原密码,验证是否仍能解锁;
- 避坑:EEPROM写入需10ms延时,必须用状态机等待,不能靠
#10000;I2C时钟必须严格满足400kHz。
从那以后我每次做FPGA外设交互,都强制走一遍“逻辑分析仪抓波形→对比协议时序图→单步验证ACK/NACK”的三步法。这个电子密码锁项目,表面是教你怎么让LED亮,实则是教你怎么让硬件世界里的每一个电信号,都听你的话。希望帮到你。
本文还有配套的精品资源,点击获取