☰
FPGA智能门锁课程设计:Verilog状态机与仿真避坑指南
2026/10/15 6:46:45 网站建设 项目流程

简介:这份资源是面向电子信息工程、FPGA初学者及课程设计需求者的智能门锁仿真项目资料,基于Verilog HDL与Xilinx Vivado实现电子密码锁控制电路,解决数字逻辑课程设计中从设计输入到功能仿真的完整实践问题。压缩包内共1个docx文件,约1.76MB,内容为EDA技术课程设计报告,涵盖课题任务、Vivado环境配置、总体设计方案、按键比较电路、计时与密码存储模块、正确与错误密码输入的功能仿真测试,以及三次输错自锁、5秒超时复位和20秒报警机制等关键逻辑。报告还给出xc7a100tcsg324-1芯片选型、自顶向下模块划分思路与信号定义说明,便于读者理解FPGA开发流程与错误处理机制。目前已有359人学习,适合需要参考完整课设结构、仿真验证方法与模块化设计思路的读者。

1. 从一份课程设计拆出来的 FPGA 智能门锁:它到底能跑通什么

翻到这份基于 FPGA 的电子密码锁课程设计时,我第一反应不是“又一个学生作业”,而是它把时序约束、状态机、按键消抖、计时器、自锁逻辑这几块 FPGA 入门阶段最容易翻车的东西全塞进了一个能跑通的闭环里。芯片选的是 xc7a100tcsg324-1,开发环境 Vivado,语言 Verilog HDL,仿真用 Vivado Simulator,输入时钟 1MHz。功能上要求 6 位可修改密码、第一位按下后 5 秒内必须完成输入、允许三次输错、超时或三次错满后自锁并拉高 20 秒报警。这套需求放在实际门禁产品里当然偏简单,但作为 FPGA 课程设计,它的价值在于:每一个模块都有明确的输入输出信号定义,每一段逻辑都能在仿真波形里找到对应跳变,不是那种“跑个流水灯就算完”的水项目。如果你正在找一份能直接复现、能改参数、能拿去答辩的 FPGA 实战素材,这份源码包值得拆开看。

2. 模块拆解与信号流:从按键到报警的完整链路

2.1 顶层集成的信号定义与模块例化

这份设计的顶层文件叫Main_all.v,里面例化了按键比较模块Password_cmp,并通过 wire 把计时、报警、状态输出串起来。先看顶层端口定义,这是理解整个数据流的关键:

module main_all( input clk, // 1MHz 外部时钟 input key_start, // 开始开锁信号,高电平有效 input password_update, // 密码修改信号,与 key_start 同时拉高进入改密模式 input key_iqr, // 按键中断信号,检测上升沿 input [3:0] key_value, // 键值 0~9,无按键时为 0xf output [3:0] result, // 状态输出:0 空闲 / 1 成功 / 2 重输 / 3 自锁 output alarm // 报警输出,高电平持续 20 秒 );

顶层内部用 wire 声明了模块间握手信号:password_success、password_error由按键比较电路产生;time_5s_start、time_20s_start分别由按键比较和顶层发出;time_5s_end、time_20s_end由计时电路回传;reset直接由time_5s_end驱动,用于同步复位按键比较电路。这种“顶层只做连线和状态仲裁,功能下沉到子模块”的写法,是 FPGA 课程设计里比较规范的自顶向下思路。

提示:key_value默认值设为 0xf 而不是 0,是为了区分“没有按键”和“按下了数字 0”。这个细节在仿真里如果不注意,很容易把空闲状态误判成输入了 0。

2.2 按键比较电路的状态机设计

按键比较电路是整个设计的核心,它内部维护一个 24 位寄存器存储 6 位密码,每位密码占 4 位。状态寄存器state的编码如下:0 表示等待开始信号,1~6 表示正在等待第 x 位密码,7 表示六位全部正确,0xf 表示密码错误。这个状态机在 1MHz 时钟下运行,每检测到key_iqr上升沿就读取一次key_value并与对应位的存储密码比较。

// 状态机核心逻辑(示意) always @(posedge clk) begin if (reset) begin state <= 4'd0; time_5s_start <= 1'b0; end else if (key_start && !password_update) begin state <= 4'd1; // 进入等待第一位密码 time_5s_start <= 1'b1; // 发出 5 秒计时启动脉冲 end else if (key_iqr) begin case (state) 4'd1: if (key_value == password_reg1) state <= 4'd2; else state <= 4'hf; // ... 后续位依次比较 4'd6: if (key_value == password_reg6) begin state <= 4'd7; password_success <= 1'b1; end else state <= 4'hf; endcase end end

这里有几个参数需要留意:time_5s_start只在第一位密码按下时拉高一个时钟周期,后续位不再重复触发;password_success和password_error都是脉冲信号,顶层需要做边沿检测或状态锁存,否则会漏掉。仿真时用 50us 代替 5s、20us 代替 20s,通过宏定义切换实际计数值,这个做法在课程设计里很常见,方便波形观察。

2.3 计时电路的双定时器结构

计时电路负责两个独立计时:5 秒输入超时和 20 秒报警时长。5 秒计时可以被time_5s_reset同步复位,20 秒计时一旦启动就不复位,直到计满自动拉高time_20s_end。信号定义如下表:

信号名位宽方向含义
clk1Input1MHz 时钟
time_5s_start1Input5 秒计时启动脉冲
time_20s_start1Input20 秒计时启动脉冲
time_5s_reset1Input5 秒计时复位
time_5s_end1Output5 秒计时结束脉冲
time_20s_end1Output20 秒计时结束脉冲

实际写代码时,计数器位宽要算清楚:1MHz 时钟下 5 秒需要 5,000,000 个周期,至少 23 位;20 秒需要 20,000,000 个周期,至少 25 位。仿真时用宏定义把计数值改小,比如define SIM_5S 50,综合前再切回真实值。这个宏切换的坑后面会细说。

3. 仿真验证:四类场景的波形判读与参数调整

3.1 正确输入密码的仿真流程

在 Vivado Simulator 里跑正确密码场景,testbench 需要模拟按键中断和键值输入。核心步骤是:先给key_start一个高脉冲,然后在每个时钟周期检查key_iqr,拉高一个周期同时给出对应key_value。六位密码 123456 依次输入后,result应该在最后一个按键周期后变为 0001,alarm保持低电平。

// testbench 片段:输入正确密码 123456 initial begin key_start = 0; key_iqr = 0; key_value = 4'hf; #100 key_start = 1; #20 key_start = 0; // 启动脉冲 // 输入第 1 位:1 #100 key_iqr = 1; key_value = 4'h1; #20 key_iqr = 0; key_value = 4'hf; // 输入第 2 位:2 #100 key_iqr = 1; key_value = 4'h2; #20 key_iqr = 0; key_value = 4'hf; // ... 依次输入 3、4、5、6 #500 $stop; end

仿真波形里重点看三个点:state是否从 1 逐步跳到 7;password_success是否在第六位匹配后出现一个时钟周期的高脉冲;result是否在password_success之后变为 0001。如果state在中间某位跳到了 0xf,说明该位密码比较失败,需要检查password_regx的初始值是否与 testbench 输入一致。

3.2 三次输错与自锁报警的触发条件

错误密码场景的 testbench 改动很小,把第三位密码从 3 改成 2,然后重复三次完整的六位输入流程。每次输错时password_error应该产生一个高脉冲,result变为 0010。三次错误后,顶层电路收到第三个password_error,向计时电路发出time_20s_start,同时alarm拉高,result锁定为 0011。

这里有个容易翻车的点:顶层对password_error的计数必须用边沿检测,不能用电平判断。因为password_error是一个时钟周期的脉冲,如果顶层用if (password_error) cnt <= cnt + 1这种写法,在脉冲持续期间每个时钟周期都会加一,三次错误可能被算成几十次。正确做法是打两拍做上升沿检测:

reg password_error_d1, password_error_d2; always @(posedge clk) begin password_error_d1 <= password_error; password_error_d2 <= password_error_d1; end wire error_pulse = password_error_d1 && !password_error_d2;

3.3 超时复位与密码修改的仿真差异

超时场景的 testbench 在按下第一位密码后不再给后续按键,等待 5 秒计时结束。仿真里用 50us 代替 5s,所以time_5s_end应该在 50us 附近拉高,reset随之拉高,state清零,同时顶层触发 20 秒报警,result变为 0011。

密码修改场景需要key_start和password_update同时拉高。此时按键比较电路不再输出password_success或password_error,而是把key_value依次写入password_reg1到password_reg6。仿真波形里可以看到寄存器值从 123456 逐步变成 654321。改密完成后,下一次正常开锁应该用新密码验证。

注意:改密模式下time_5s_start不应该被触发,否则改密过程中会误触发超时复位。代码里需要在key_start && password_update的分支里屏蔽计时启动信号。

4. 避坑排查:从仿真波形到上板验证的五个血泪教训

4.1 仿真通过但上板按键无反应

现象:Vivado Simulator 里所有波形都正确,下载到 FPGA 后按键按下没有任何反应。原因通常是真实按键存在机械抖动,key_iqr在按下瞬间产生多个上升沿,状态机误判为多次输入。解决方式是在按键输入路径上加消抖模块,用 20ms 计数器滤除抖动,或者至少用两级寄存器同步后再做边沿检测。仿真里 testbench 给的理想脉冲不会暴露这个问题,所以上板前一定要补消抖。

4.2 计时器宏定义切换后综合报错

现象:仿真时用define SIM_5S 50跑得好好的,切回真实值 5_000_000 后综合报位宽不匹配。原因是计数器声明时位宽写死了reg [15:0] cnt,50 能放下,5_000_000 放不下。解决方式是计数器位宽按最大计数值声明,5 秒用 23 位,20 秒用 25 位,或者直接用$clog2函数自动计算。这个坑在课程设计里非常常见,因为仿真阶段为了波形好看会把计数值改得很小,综合前忘了改位宽。

4.3 密码寄存器初值被综合优化掉

现象:上板后输入正确密码 123456 却一直报错,仿真里同样的密码能通过。原因是password_reg1到password_reg6的初值在代码里用initial块赋值,而部分综合工具会忽略initial块,导致寄存器上电后为 0。解决方式是用复位逻辑给密码寄存器赋初值,或者在综合属性里保留初值。更稳妥的做法是把默认密码存在 ROM 里,上电后从 ROM 读取。

4.4 报警信号拉高后无法自动关闭

现象:三次输错后alarm拉高,但 20 秒后没有自动拉低。原因是time_20s_end脉冲没有被顶层正确捕获,或者alarm的复位条件写成了time_20s_start而不是time_20s_end。检查顶层代码里alarm的赋值逻辑,确保它由time_20s_end清零,而不是由time_20s_start或reset清零。另外time_20s_end也是一个脉冲,需要边沿检测后再用。

4.5 状态输出 result 在空闲时不稳定

现象:仿真波形里result在空闲状态偶尔跳到 0010 或 0011。原因是result用组合逻辑直接由password_error和password_success译码,而这些信号在状态切换瞬间可能有毛刺。解决方式是给result加寄存器打一拍,用时钟同步输出。这样虽然延迟一个周期,但输出稳定,不会在状态边界产生误判。

5. 进阶改造:把 6 位密码锁扩展成可配置多用户门禁

这份课程设计的密码是硬编码在寄存器里的 6 位数字,实际门禁场景往往需要多用户、可配置、带管理员权限。在不改变整体架构的前提下,可以做几个低成本扩展。第一个扩展是把密码存储从寄存器改成 Block RAM,用双端口 RAM 实现管理员写入和用户读取分离。第二个扩展是增加用户 ID 输入,在按键比较之前先匹配用户 ID,再匹配该用户的密码。第三个扩展是把result输出从 4 位扩展到 8 位,低 4 位保留原有状态,高 4 位输出剩余尝试次数。

具体改法上,密码存储模块可以这样写:

// 用 Block RAM 存储 4 组密码,每组 6 位 reg [23:0] password_ram [0:3]; initial begin password_ram[0] = 24'h123456; // 用户 0 默认密码 password_ram[1] = 24'h654321; // 用户 1 默认密码 password_ram[2] = 24'h111111; password_ram[3] = 24'h222222; end

读取时用user_id作为 RAM 地址,比较逻辑不变。管理员改密时通过password_update信号配合user_id写入对应地址。这个改动在 Vivado 里综合后资源占用增加很少,因为 Block RAM 是 FPGA 的专用资源,不消耗额外 LUT。

验证扩展功能时,我一般会跑一个覆盖四组用户的 testbench,每组用户分别测试正确密码、错误密码、改密后重新开锁三个场景。波形里重点看password_ram的读写地址是否与user_id一致,以及改密后旧密码是否立即失效。如果旧密码还能开锁,说明改密逻辑只写了 RAM 但没有更新比较电路的当前密码寄存器,需要在改密完成后同步刷新。

从那以后我每次做带计时和状态机的 FPGA 设计,都会先把仿真计数值和真实计数值的切换检查一遍,再确认所有脉冲信号都做了边沿检测,最后上板前补按键消抖。这三步走完,基本不会再出现“仿真全对、上板全废”的情况。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询