简介:本资源为基于FPGA的智能门锁仿真课程设计资料,面向电子信息工程、微电子等专业学生及FPGA初学者,帮助完成EDA技术课程设计或数字电路综合实践。内容围绕Verilog HDL硬件描述语言与Xilinx Vivado开发环境展开,涵盖电子密码锁设计原理、总体设计方案、按键比较电路、计时与自锁逻辑、功能仿真测试等核心模块,并给出xc7a100tcsg324-1芯片的完整开发流程。压缩包内共1个docx文档,约1.76MB,为结构完整的课程设计报告,包含课题任务、设计平台、方案分析、仿真测试与结论等章节,可直接参考电路结构、模块划分与代码实现思路。目前已有359人学习下载,适合需要快速搭建FPGA密码锁实验、理解状态机与错误处理机制、对照完成仿真验证的读者。
1. 开源!基于FPGA的智能门锁仿真:从课程设计到可上板验证的完整路径
智能门锁这个题目在 FPGA 课程设计里出现频率极高,但真正能跑通仿真、还能往板子上落的人并不多。多数人卡在两个地方:一是把「智能」理解成了堆功能,密码、指纹、刷卡全往上塞,结果状态机写成一团乱麻;二是仿真波形跑出来了,却不知道哪些时序是必须卡的、哪些约束是上板才需要补的。这篇笔记就围绕「基于 FPGA 的智能门锁仿真」这条线,把模块划分、状态机设计、仿真激励、上板前的时序检查讲清楚。适合正在做 FPGA 课程设计的学生,也适合想拿这个题目练手状态机和时序约束的工程师。整套方案用纯 RTL 实现,不依赖软核,仿真平台用常见的 Verilog 仿真器即可复现。
2. 智能门锁的模块划分:为什么不能把密码校验塞进状态机
2.1 先定边界:哪些逻辑放 FPGA,哪些放外部
做课程设计最容易犯的错,是把所有功能都往一个 always 块里写。智能门锁的核心交互其实只有几件事:接收输入(按键或矩阵键盘)、比对密码、控制开锁信号、超时自动上锁、错误次数锁定。这些逻辑里,密码比对和错误计数是纯组合或简单时序逻辑,开锁控制是状态迁移,输入采集涉及消抖。把它们拆成独立模块,仿真时才能单独给激励,上板时也方便定位问题。
我一般会分成五个模块:key_debounce(按键消抖)、password_compare(密码比对)、lock_fsm(主状态机)、timer(超时与锁定计时)、seg_display(数码管或 LED 显示)。顶层只做例化和连线。这样拆的好处是,仿真时你可以先单独验证消抖模块的输出是否在稳定后只跳变一次,再验证状态机在正确密码下的迁移路径,最后做整体联调。
模块之间的接口要提前定死。比如password_compare输出一个match脉冲,lock_fsm只在match为高且当前处于INPUT状态时才迁移到UNLOCK。这种「脉冲 + 状态」的握手方式比直接用电平信号更稳,避免状态机在同一个时钟周期内反复判断。
2.2 状态机设计:五个状态够用,别再加了
主状态机我通常只设五个状态:IDLE(待机)、INPUT(输入密码)、CHECK(比对)、UNLOCK(开锁)、LOCKED(错误锁定)。迁移条件如下表:
| 当前状态 | 条件 | 下一状态 |
|---|---|---|
| IDLE | 任意按键按下 | INPUT |
| INPUT | 按下确认键 | CHECK |
| INPUT | 超时未按确认 | IDLE |
| CHECK | 密码匹配 | UNLOCK |
| CHECK | 密码不匹配且错误次数 < 3 | IDLE |
| CHECK | 密码不匹配且错误次数 = 3 | LOCKED |
| UNLOCK | 开锁计时到 | IDLE |
| LOCKED | 锁定计时到 | IDLE |
这个表看起来简单,但写代码时要注意:CHECK状态只停留一个时钟周期,比对结果必须在这个周期内稳定。所以password_compare不能是纯组合逻辑直接输出,最好打一拍,让match信号在CHECK状态生效时已经稳定。
// lock_fsm.v 核心迁移逻辑片段 localparam IDLE = 3'd0, INPUT = 3'd1, CHECK = 3'd2, UNLOCK = 3'd3, LOCKED = 3'd4; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; err_cnt <= 2'd0; end else begin case (state) IDLE: if (key_press) state <= INPUT; INPUT: begin if (confirm) state <= CHECK; else if (timeout) state <= IDLE; end CHECK: begin if (match) state <= UNLOCK; else if (err_cnt == 2'd2) begin state <= LOCKED; err_cnt <= 2'd0; end else begin state <= IDLE; err_cnt <= err_cnt + 1'b1; end end UNLOCK: if (unlock_timeout) state <= IDLE; LOCKED: if (lock_timeout) state <= IDLE; default: state <= IDLE; endcase end end这段代码里err_cnt的复位和累加时机很关键。错误次数在CHECK状态不匹配时累加,但进入LOCKED后要清零,否则锁定解除后错误计数还是满的,下一次错误直接又锁。这个坑我在第一次做的时候踩过,仿真波形上表现为锁定解除后按一次错误密码立刻又锁,查了半天才发现是计数器没清。
2.3 消抖模块:20ms 不是随便定的
按键消抖的常见做法是计数到 20ms 再采样。但课程设计里时钟频率不一定是 50MHz,如果时钟是 24MHz,计数值就要重新算。我一般把消抖时间做成参数,顶层例化时根据板子时钟改。
module key_debounce #( parameter CLK_FREQ = 50_000_000, parameter DEBOUNCE_MS = 20 )( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); localparam CNT_MAX = CLK_FREQ / 1000 * DEBOUNCE_MS; reg [31:0] cnt; reg key_sync0, key_sync1; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync0 <= 1'b1; key_sync1 <= 1'b1; cnt <= 32'd0; key_out <= 1'b1; end else begin key_sync0 <= key_in; key_sync1 <= key_sync0; if (key_sync1 != key_out) begin if (cnt == CNT_MAX) begin key_out <= key_sync1; cnt <= 32'd0; end else begin cnt <= cnt + 1'b1; end end else begin cnt <= 32'd0; end end end endmodule这里key_sync0和key_sync1是两级同步器,防止亚稳态。key_out是消抖后的输出,默认高电平,按下为低。注意CNT_MAX的计算用CLK_FREQ / 1000 * DEBOUNCE_MS,如果时钟频率不是 1000 的整数倍,会有截断误差,但 20ms 量级下这点误差无所谓。
3. 仿真平台怎么搭:从单模块激励到全流程用例
3.1 单模块仿真:先让消抖模块自己跑起来
仿真第一步不是直接跑顶层,而是单独给key_debounce写 testbench。激励里模拟按键抖动:在 5ms 内随机翻转key_in几次,然后保持稳定,看key_out是否只在稳定后跳变一次。
// tb_key_debounce.v initial begin clk = 0; forever #10 clk = ~clk; // 50MHz end initial begin rst_n = 0; key_in = 1; #100 rst_n = 1; // 模拟抖动:5ms 内翻转 #1000 key_in = 0; #200 key_in = 1; #300 key_in = 0; #500 key_in = 1; #1000 key_in = 0; // 最终按下 #30_000_000; // 等待 30ms 看输出 $stop; end跑完看波形,key_out应该在最后一次key_in变低后约 20ms 才变低,之前的高频翻转不应该影响输出。如果key_out跟着抖动跳,说明同步器或计数逻辑有问题。
3.2 全流程用例:正确密码、错误密码、超时、锁定
顶层仿真要覆盖四条路径:正确密码开锁、错误密码一次后重试成功、连续三次错误锁定、输入超时回待机。每条路径用一个task封装,方便复用。
task press_key(input [3:0] key_val); begin key_in = key_val; #20_000_000; // 按下 20ms key_in = 4'hF; #20_000_000; // 松开 20ms end endtask initial begin // 初始化 rst_n = 0; #100 rst_n = 1; // 用例1:正确密码 1-2-3-4 press_key(4'h1); press_key(4'h2); press_key(4'h3); press_key(4'h4); press_key(4'hA); // 确认键 #1000; if (unlock != 1'b1) $display("FAIL: correct password"); // 用例2:错误密码后重试 press_key(4'h5); press_key(4'hA); #1000; if (unlock != 1'b0) $display("FAIL: wrong password should not unlock"); // 用例3:连续三次错误 press_key(4'h5); press_key(4'hA); press_key(4'h5); press_key(4'hA); press_key(4'h5); press_key(4'hA); #1000; if (locked != 1'b1) $display("FAIL: should be locked"); $stop; end这里press_key的 20ms 按下和 20ms 松开是为了匹配消抖模块的时序。如果消抖参数改了,这里也要跟着改。仿真时间会因此变长,但这是必要的,否则消抖模块还没输出稳定,状态机就已经采样了。
3.3 仿真波形要看什么:三个关键信号
跑完仿真,波形上重点看三个信号:state、match、err_cnt。state的迁移必须和状态表一致,不能出现IDLE直接跳到UNLOCK的情况。match应该在CHECK状态期间为高,且只持续一个周期。err_cnt在每次错误后加一,进入LOCKED后清零。
如果match在CHECK状态之外也为高,说明password_compare没有做状态限定,这会导致状态机在IDLE时误判。常见做法是在password_compare里加一个en输入,只在CHECK状态使能比对。
4. 上板前的时序检查与约束:仿真过了不代表板子能跑
4.1 时钟约束:先确认板子时钟频率
仿真时用的时钟周期是 20ns(50MHz),但实际板子可能是 24MHz、50MHz 或 100MHz。上板前第一件事是查板子手册,确认时钟频率,然后改CLK_FREQ参数和 testbench 里的时钟周期。如果时钟频率变了但消抖参数没改,消抖时间会偏,表现为按键反应过快或过慢。
约束文件里至少要有时钟周期约束:
# 假设板子时钟 50MHz create_clock -period 20.000 -name sys_clk [get_ports clk]如果板子有多个时钟域,比如数码管扫描用另一个时钟,还要加set_clock_groups或set_false_path。课程设计里一般只有一个时钟,但如果你用了 PLL 分频,就要把 PLL 输出也约束上。
4.2 输入输出延迟:按键和 LED 的约束
按键输入和 LED 输出属于外部接口,需要设set_input_delay和set_output_delay。但课程设计里按键是机械开关,LED 是低速外设,这些延迟对时序影响很小,可以不加。真正要加的是如果用了矩阵键盘扫描,扫描信号和回读信号之间有时序关系,需要约束。
我一般会先不加 IO 约束,跑一遍时序报告,看有没有 setup/hold 违例。如果没有,就不加;如果有,再根据板子手册补。这样比一上来就写一堆约束更省事。
4.3 复位信号:异步复位同步释放
复位信号如果直接用电平,释放时可能不满足时序。常见做法是异步复位同步释放:
reg rst_sync0, rst_sync1; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rst_sync0 <= 1'b0; rst_sync1 <= 1'b0; end else begin rst_sync0 <= 1'b1; rst_sync1 <= rst_sync0; end end wire rst_n_sync = rst_sync1;然后所有模块用rst_n_sync作为复位。这样复位释放和时钟同步,避免亚稳态。仿真时如果复位释放时机不对,状态机可能停在非法状态,波形上表现为state是xxx。
5. 避坑与常见问题:仿真通过但上板翻车的五个原因
5.1 现象:仿真波形正常,上板按键没反应
原因:消抖模块的CLK_FREQ参数没改,实际板子时钟是 24MHz,但参数还是 50MHz,消抖时间变成 40ms 以上,按键按一下根本不够。解决:查板子手册,改顶层例化时的CLK_FREQ,同时改 testbench 时钟周期。
5.2 现象:上板后数码管显示乱码
原因:数码管扫描频率和消抖模块共用时钟,但扫描逻辑里用了阻塞赋值导致竞争。解决:扫描模块用非阻塞赋值,且扫描频率控制在 1kHz 左右,太高会闪,太低会抖。我一般用clk / 50000作为扫描时钟。
5.3 现象:错误次数到了但不锁定
原因:err_cnt的位宽不够,2 位最大是 3,但判断条件是err_cnt == 2'd2时锁定,实际第三次错误时err_cnt从 2 变成 0 的同时进入LOCKED,但LOCKED状态里又判断了err_cnt,导致逻辑冲突。解决:在CHECK状态判断err_cnt == 2'd2时直接进LOCKED,不要在LOCKED里再判断。
5.4 现象:仿真时match信号一直是高
原因:password_compare没有使能信号,组合逻辑一直比对,输入密码寄存器默认值恰好和预设密码相同。解决:加en输入,只在CHECK状态使能,且密码寄存器复位值设为一个不可能的值,比如全 1。
5.5 现象:上板后开锁信号抖动
原因:开锁信号直接由状态机输出,没有打拍。解决:开锁信号在UNLOCK状态寄存一拍再输出,或者加一个lock_ctrl模块做电平保持。
6. 进阶技巧:把仿真用例做成可配置的回归测试
6.1 用参数化 testbench 覆盖多组密码
上面的 testbench 里密码是写死的,改密码就要改激励。更好的做法是把密码做成参数,用$random生成多组测试用例,跑回归。
parameter [15:0] PASSWORD = 16'h1234; reg [15:0] input_pwd; integer i; initial begin for (i = 0; i < 10; i = i + 1) begin input_pwd = $random; // 依次输入 input_pwd 的四个 nibble press_key(input_pwd[15:12]); press_key(input_pwd[11:8]); press_key(input_pwd[7:4]); press_key(input_pwd[3:0]); press_key(4'hA); #1000; if (input_pwd == PASSWORD) begin if (unlock != 1'b1) $display("FAIL: case %0d", i); end else begin if (unlock != 1'b0) $display("FAIL: case %0d", i); end end end这样跑一次能覆盖多组随机密码,比手写用例效率高。注意$random生成的密码可能和预设密码相同,概率很低但存在,可以在比对时加一个判断。
6.2 用 SystemVerilog 断言检查状态迁移
如果仿真器支持 SystemVerilog,可以加断言检查非法迁移:
property p_no_idle_to_unlock; @(posedge clk) disable iff (!rst_n) (state == IDLE) |=> (state != UNLOCK); endproperty assert property (p_no_idle_to_unlock) else $error("illegal transition");断言能在仿真时自动抓非法迁移,比看波形快。课程设计里如果仿真器不支持,可以用$display在always块里打印状态迁移,跑完看日志。
6.3 上板验证:先跑通开锁,再调显示
上板时不要一次把所有功能都打开。先烧一个只包含消抖和状态机的版本,用 LED 指示状态,确认按键能正常迁移。然后再加数码管显示,最后加错误锁定。这样出问题时能快速定位是哪个模块。
我自己的习惯是,每次上板前先在仿真里跑一遍完整用例,把波形存下来,上板时如果现象不对,先对比波形和实际现象,看是时序问题还是逻辑问题。这个习惯帮我省了很多来回烧板子的时间。
希望帮到你。
本文还有配套的精品资源,点击获取