简介:本资源围绕EDA技术中基于Quartus II的8位电子密码锁设计与仿真展开,面向从事FPGA、Verilog数字逻辑设计的工程师、电子工程专业师生及技术爱好者,可用于教学案例展示或实际密码锁控制系统的设计参考。资源包内共1个docx文档,压缩包约720KB,以文字与图表相结合的形式呈现完整设计流程。内容涵盖密码输入、清除、更改、开锁与三次错误警报等核心功能,采用主、从双状态机架构完成8位密码输入与控制,并结合4x4矩阵键盘键值扫描、按键消抖、时序产生、BCD至七段译码及数码管显示等电路模块逐项讲解原理。文中附有Verilog程序清单与系统总体框图、状态机状态图等丰富图示,便于对照理解设计思路;同时指出当前仅停留在软件仿真层面、尚未制作实物的问题,并提出增加实物环节、管理员模式与改进显示方式等改善措施,为后续实践提供可延伸的方向。目前已有151人学习下载。
1. 从八次按键到一把锁:Quartus II 里做 8 位电子密码锁的真实工作量
很多人对 EDA 课程设计的第一印象是画一张原理图,接几个 74 系列比较器,波形跑出来就算交差。真坐到电脑前把 Quartus II 打开才会发现,8 位电子密码锁的难点不在锁本身,而在两件事:八位输入要按时序一位一位挤进移位寄存器,再和预设密码逐位比对;仿真波形必须干净,一旦出现仿真发散或者 ModelSim 波形全红线,后面的引脚分配和上板全是空谈。
这里说的「8 位」,指的是 8 个十进制密码位,每位用 4 bit BCD 表示,合起来是一个 32 bit 的常数,而不是 8 位二进制。这条定义决定了移位寄存器、数码管显示和比对逻辑的全部写法,也是后面所有参数的计算基准。
内容围绕 Quartus II 工程建立、Verilog 模块划分、Testbench 编写和引脚约束四条线展开,器件默认取 Cyclone IV 这类课程板常见型号,工具版本差异覆盖 Quartus II 13.1 到 18.1。适合正在做 EDA 课程设计的学生,也适合想把一套密码锁逻辑复用成门禁、保险箱控制器雏形的工程师。
2. 8 位电子密码锁的功能边界与 Quartus II 工程搭建
2.1 先定接口再谈代码:8 位密码锁的输入输出清单
动手写 Verilog 之前,一定要先把顶层端口钉死。密码锁最容易返工的地方不是状态机写错,而是中途发现少了一个报警输出、或者数码管位选的数量和板上对不上,于是从顶层一路改到子模块。我一般会在纸上把下面这张表先填完,再打开 Quartus II 新建工程。
| 信号名 | 方向 | 位宽 | 含义 |
|---|---|---|---|
| clk | 输入 | 1 | 板级 50 MHz 有源晶振 |
| rst_n | 输入 | 1 | 低电平复位,接独立按键 |
| key_valid | 输入 | 1 | 键盘扫描模块给出的单周期有效脉冲 |
| key_val | 输入 | 4 | 键值 0~9,无按键时为 4'hF |
| confirm | 输入 | 1 | 确认键,按下即启动比对 |
| clear | 输入 | 1 | 清除键,清空已输入的移位寄存器 |
| unlock | 输出 | 1 | 电磁锁驱动,高电平开锁 |
| alarm | 输出 | 1 | 连续错误达到阈值后拉高 |
| seg_sel | 输出 | 8 | 8 位数码管位选,低有效 |
| seg_data | 输出 | 7 | 段选 a~g,共阴极高有效 |
矩阵键盘扫描单独做成一个模块,密码锁主模块只吃「有效脉冲 + 4 位键值」。这样分层的好处是,后面换 4x4 矩阵键盘还是换 PS/2 键盘,主逻辑一行都不用改,只需要在顶层换掉扫描模块。同理,数码管如果从 8 位共阴换成 6 位共阳,改动也只落在显示模块里。
2.2 Quartus II 新建工程时那几个不能随手选的参数
New Project Wizard 里前几页看着都是例行公事,但有两个选项选错了,后面综合和仿真都会出问题。
第一个是器件型号。课程板常见 Cyclone IV EP4CE6E22C8 或 EP4CE10F17C8,家庭、器件名、封装、速度等级四项必须和板子一致。选错器件不会立刻报错,但引脚分配页面里的可用引脚会完全不同,Pin Planner 会因为找不到 PIN_xx 而标红。另外速度等级不同,时序分析里 Fmax 的估算也会变,看到不合理的余量时先回头确认这一页。
第二个是 EDA Tool Settings 里的仿真工具。Quartus II 13.1 之后的版本不再自带波形仿真器,Tools → Options → EDA Tool Options 里要把 ModelSim-Altera 的安装路径填进去,NativeLink 才能识别。如果版本是 13.0 或更早,还能用自带的 Simulation Waveform Editor 画激励,路径就选 Quartus II 自带仿真器。
# 在 Quartus II Tcl Console 里直接建工程,避免向导来回点 project_new pwd_lock -overwrite set_global_assignment -name FAMILY "Cyclone IV E" set_global_assignment -name DEVICE EP4CE6E22C8 set_global_assignment -name TOP_LEVEL_ENTITY password_lock_top set_global_assignment -name VERILOG_FILE rtl/password_lock.v set_global_assignment -name VERILOG_FILE rtl/clk_div.v set_global_assignment -name VERILOG_FILE rtl/debounce.v set_global_assignment -name VERILOG_FILE rtl/seg_display.v set_global_assignment -name EDA_SIMULATION_TOOL "ModelSim-Altera (Verilog)" set_global_assignment -name EDA_TEST_BENCH_FILE tb/tb_password_lock.v project_close这段脚本等价于走一遍向导,好处是工程配置可复现、可进版本库,换台电脑执行一次就能重建。TOP_LEVEL_ENTITY必须和顶层模块名严格一致,大小写敏感;EDA_TEST_BENCH_FILE填了之后,Tools → Run Simulation Tool → RTL Simulation 才会自动带上 Testbench。
2.3 自上而下的模块划分与文件组织
一个能上板的密码锁,我一般拆成五个文件:clk_div.v负责把 50 MHz 降到 1 kHz 节拍;debounce.v做按键消抖;keypad_scan.v做矩阵键盘扫描;password_lock.v是状态机和密码比对的核心;seg_display.v负责 8 位数码管动态扫描。顶层password_lock_top.v只做例化和连线,不写任何逻辑。
module password_lock_top ( input wire clk_50m, input wire rst_n, input wire [3:0] row, output wire [3:0] col, output wire unlock, output wire alarm, output wire [7:0] seg_sel, output wire [6:0] seg_data ); wire clk_1k; wire key_valid; wire [3:0] key_val; clk_div #(.CNT_MAX(24_999)) u_div ( .clk (clk_50m), .rst_n (rst_n), .clk_1k(clk_1k) ); keypad_scan u_scan ( .clk (clk_1k), .rst_n (rst_n), .row (row), .col (col), .key_valid(key_valid), .key_val (key_val) ); password_lock #( .PASSWORD (32'h12345678), .LOCK_TIME(16'd30000), .MAX_ERR (4'd3) ) u_lock ( .clk (clk_1k), .rst_n (rst_n), .key_valid(key_valid), .key_val (key_val), .confirm (key_val == 4'hE), .clear (key_val == 4'hD), .unlock (unlock), .alarm (alarm), .seg_sel (seg_sel), .seg_data (seg_data) ); endmodule顶层把key_val等于4'hE、4'hD直接当作确认和清除,这是一种偷懒但实用的做法,前提是键盘扫描模块保留两个非数字键值。如果板子只有 0~9 十个数字键,就得在扫描模块里额外编码长按或组合键,那时把confirm、clear从组合逻辑改回独立端口即可。
3. 密码锁核心逻辑的 Verilog 实现与关键参数设定
3.1 时钟分频:把 50 MHz 变成 1 kHz 节拍
整个设计只用一个时钟域,其余节拍全部由分频产生,这是避免跨时钟域亚稳态最省事的做法。50 MHz 分频到 1 kHz,计数器每 25000 个周期翻转一次输出。
module clk_div #( parameter CNT_MAX = 24_999 // 50MHz / (2*(CNT_MAX+1)) = 1kHz )( input wire clk, // 50MHz 输入 input wire rst_n, output reg clk_1k // 1kHz 输出,占空比 50% ); reg [14:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 15'd0; clk_1k <= 1'b0; end else if (cnt == CNT_MAX) begin cnt <= 15'd0; clk_1k <= ~clk_1k; end else begin cnt <= cnt + 1'b1; end end endmoduleCNT_MAX取 24999,翻转周期是 2×25000 个 50 MHz 周期,也就是 1 ms,得到 1 kHz、占空比 50% 的方波。计数器位宽取 15 位,能表示到 32767,留有余量,如果想把节拍降到 500 Hz,只要把参数改成 49999 并确认位宽不溢出即可。
提示:数码管动态扫描希望刷新率不低于 200 Hz,用 1 kHz 分频出来的节拍做 8 位扫描,每位停留 1 ms,整屏刷新 125 Hz,肉眼看不到闪烁。如果发现数码管有轻微抖动,优先怀疑分频参数被改成了过低的值。
3.2 按键消抖模块的计数窗口怎么定
机械按键的抖动时间典型值在 5~20 ms 之间,所以消抖窗口取 20 ms 足够覆盖绝大多数轻触开关。在 1 kHz 节拍下,20 ms 对应 20 个计数周期。
module debounce #( parameter CNT_MAX = 5'd20 // 20ms @1kHz )( input wire clk, // 1kHz 节拍 input wire key_in, // 原始按键,低有效 output reg key_pulse // 消抖后单周期脉冲,低有效 ); reg [4:0] cnt; reg key_d0, key_d1; always @(posedge clk) begin key_d0 <= key_in; key_d1 <= key_d0; if (key_d0 == key_d1) begin if (cnt < CNT_MAX) cnt <= cnt + 1'b1; end else begin cnt <= 5'd0; // 电平跳变,计数清零 end end always @(posedge clk) begin key_pulse <= (cnt == CNT_MAX) && (key_d1 == 1'b0) ? 1'b0 : 1'b1; end endmodule第一段用两级寄存同步输入并比较前后值,只要不一致就把计数清零,连续 20 个周期一致才认为电平稳定。第二段在稳定且为低电平时输出一个低脉冲。CNT_MAX的大小直接决定手感:调小了会重复触发,一次按键识别成好几次;调大了响应迟钝,输密码时明显拖沓。20 这个值在多数板子上都能兼顾。
3.3 移位寄存器式密码比对与状态机
密码比对有两种思路:一是把 8 个输入位分别和 8 个常量逐位比,二是把输入序列左移进一个 32 位寄存器,确认时整体比对。第二种代码短、易参数化,改密码只要动一个 parameter。
module password_lock #( parameter [31:0] PASSWORD = 32'h12345678, // 8 位 BCD,先输入的在高位 parameter [15:0] LOCK_TIME = 16'd30000, // 错误锁定 30s @1kHz parameter [3:0] MAX_ERR = 4'd3 // 连续错误阈值 )( input wire clk, // 1kHz 节拍 input wire rst_n, input wire key_valid, input wire [3:0] key_val, input wire confirm, input wire clear, output reg unlock, output reg alarm, output reg [7:0] seg_sel, output reg [6:0] seg_data ); reg [31:0] pwd_shift; reg [3:0] digit_cnt; reg [1:0] state; reg [15:0] wait_cnt; reg [3:0] err_cnt; localparam S_IDLE = 2'd0, S_OPEN = 2'd1, S_LOCKED = 2'd2; wire pwd_match = (pwd_shift == PASSWORD) && (digit_cnt == 4'd8); // 输入移位:左移,最新键值进最低 4 位 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pwd_shift <= 32'd0; digit_cnt <= 4'd0; end else if (clear || confirm) begin pwd_shift <= 32'd0; digit_cnt <= 4'd0; end else if (key_valid && key_val < 4'd10 && digit_cnt < 4'd8) begin pwd_shift <= {pwd_shift[27:0], key_val}; digit_cnt <= digit_cnt + 1'b1; end end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; wait_cnt <= 16'd0; err_cnt <= 4'd0; unlock <= 1'b0; alarm <= 1'b0; end else begin case (state) S_IDLE: begin unlock <= 1'b0; if (confirm) begin if (pwd_match) begin state <= S_OPEN; wait_cnt <= 16'd5000; // 开锁保持 5s err_cnt <= 4'd0; end else if (err_cnt + 1'b1 >= MAX_ERR) begin state <= S_LOCKED; wait_cnt <= LOCK_TIME; err_cnt <= 4'd0; alarm <= 1'b1; end else begin err_cnt <= err_cnt + 1'b1; end end end S_OPEN: begin unlock <= 1'b1; if (wait_cnt == 16'd0) begin state <= S_IDLE; unlock <= 1'b0; end else begin wait_cnt <= wait_cnt - 1'b1; end end S_LOCKED: begin unlock <= 1'b0; if (wait_cnt == 16'd0) begin state <= S_IDLE; alarm <= 1'b0; end else begin wait_cnt <= wait_cnt - 1'b1; end end default: state <= S_IDLE; endcase end endpwd_match是组合逻辑,用的是当前周期的pwd_shift,而清零动作发生在同一个时钟沿的时序块里,所以confirm拉高的那一拍比较用的仍是完整输入值,不会因为清零把结果吃掉。三个状态语义清晰:S_IDLE负责收数和判定,S_OPEN维持开锁 5 秒,S_LOCKED锁定 30 秒并点亮报警。把MAX_ERR从 3 改成 5,或者把LOCK_TIME从 30000 改成 60000,都不用动状态机结构。
3.4 错误计数锁定与 8 位数码管扫描
显示部分用计数器循环选通 8 位,每位对应pwd_shift里的一段 4 bit。扫描顺序和输入顺序保持一致,用户看到的就是从左往右依次填入。
reg [2:0] scan_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) scan_cnt <= 3'd0; else scan_cnt <= scan_cnt + 1'b1; end function [6:0] seg7; input [3:0] bcd; begin case (bcd) 4'd0: seg7 = 7'b1111110; 4'd1: seg7 = 7'b0110000; 4'd2: seg7 = 7'b1101101; 4'd3: seg7 = 7'b1111001; 4'd4: seg7 = 7'b0110011; 4'd5: seg7 = 7'b1011011; 4'd6: seg7 = 7'b1011111; 4'd7: seg7 = 7'b1110000; 4'd8: seg7 = 7'b1111111; 4'd9: seg7 = 7'b1111011; default: seg7 = 7'b0000000; // 未输入位熄灭 endcase end endfunction always @(*) begin seg_sel = ~(8'b1 << scan_cnt); seg_data = seg7(pwd_shift[scan_cnt*4 +: 4]); endseg7函数按共阴极接法编码,位序是{a,b,c,d,e,f,g},如果板上是共阳数码管,把整个 case 的赋值按位取反即可,不用动扫描逻辑。位选seg_sel用移位后取反,保证同一时刻只有一位被拉低。这里scan_cnt*4 +: 4是 Verilog-2001 的位选语法,仿真工具版本过老时可能不支持,改成pwd_shift[scan_cnt*4 + 3 -: 4]或直接展开成 case 也可以。
4. 基于 Quartus II 与 ModelSim 的仿真验证流程
4.1 Testbench 怎么写才能覆盖 8 位输入全流程
功能仿真的价值在于,正确密码、错误密码、连续错误锁定这三条路径能在几分钟内跑完,而不是上板按到手酸。Testbench 用 task 封装按键动作,主流程只写语义。
`timescale 1ns/1ps module tb_password_lock; reg clk, rst_n, key_valid, confirm, clear; reg [3:0] key_val; wire unlock, alarm; wire [7:0] seg_sel; wire [6:0] seg_data; password_lock #( .PASSWORD (32'h12345678), .LOCK_TIME(16'd100), // 仿真里缩短锁定时间 .MAX_ERR (4'd3) ) u_dut ( .clk(clk), .rst_n(rst_n), .key_valid(key_valid), .key_val(key_val), .confirm(confirm), .clear(clear), .unlock(unlock), .alarm(alarm), .seg_sel(seg_sel), .seg_data(seg_data) ); initial begin clk = 1'b0; forever #500 clk = ~clk; // 1us 周期,等效 1kHz 节拍 end task press_key(input [3:0] v); begin key_val = v; key_valid = 1'b1; @(posedge clk); key_valid = 1'b0; repeat (2) @(posedge clk); end endtask task hit_confirm; begin confirm = 1'b1; @(posedge clk); confirm = 1'b0; repeat (2) @(posedge clk); end endtask initial begin rst_n = 1'b0; key_valid = 1'b0; key_val = 4'hF; confirm = 1'b0; clear = 1'b0; repeat (10) @(posedge clk); rst_n = 1'b1; press_key(4'd1); press_key(4'd2); press_key(4'd3); press_key(4'd4); press_key(4'd5); press_key(4'd6); press_key(4'd7); press_key(4'd8); hit_confirm; repeat (5) @(posedge clk); if (unlock !== 1'b1) $display("[FAIL] correct pwd not unlocked"); else $display("[PASS] correct pwd unlocked"); // 连续三次错误,验证进入锁定态 repeat (3) begin press_key(4'd9); press_key(4'd9); press_key(4'd9); press_key(4'd9); press_key(4'd9); press_key(4'd9); press_key(4'd9); press_key(4'd9); hit_confirm; repeat (5) @(posedge clk); end if (alarm !== 1'b1) $display("[FAIL] alarm not asserted"); else $display("[PASS] locked after 3 errors"); $stop; end endmoduleLOCK_TIME在仿真里从 30000 缩到 100,否则跑完锁定态要等 30 万个时钟周期,波形文件会大到打不开。实际综合时上层重新传参即可,不影响硬件行为。press_key之间插入两拍空闲,是为了让消抖模块的计数有回落空间。
命令行编译仿真:
vlib work vlog ../rtl/clk_div.v ../rtl/debounce.v ../rtl/password_lock.v ../tb/tb_password_lock.v vsim -voptargs="+acc" -c -do "run -all; quit -f" work.tb_password_lockvlib work建库,vlog编译,vsim -c走命令行模式不弹图形界面,适合脚本化回归。-voptargs="+acc"保留信号可见性,否则优化后波形窗口里只能看到少数几根线。老版本 ModelSim 用-novopt替代这一项。
4.2 仿真发散与波形全红线的排查顺序
波形一片红(X 态)或者仿真跑到某个时间点卡死,是密码锁这类时序电路最常见的两类问题。按下面顺序查,比漫无目的地翻代码快得多。
第一,看复位。所有寄存器和状态机都必须有确定的复位值,initial块里rst_n拉低的时间要覆盖至少一个时钟沿。如果rst_n一直是 X,状态机会停在 X,case语句匹配不上任何分支,波形整条全红。Testbench 里rst_n = 1'b0必须显式赋值,不能依赖默认值。
第二,看 timescale。DUT 写了`timescale 1ns/1ps而 Testbench 写了`timescale 1ps/1ps,时钟周期会被解释成完全不同的物理时间,wait、#延迟和采样点全部错位。统一在 Testbench 顶部声明一次,子模块不必重复声明。
第三,看多驱动和组合环。同一个reg在两个always块里被赋值,综合报 multiple drivers;组合逻辑的输出经一段线绕回自己的输入,会形成振荡。这两种情况在 RTL 仿真里表现为反复跳变或红线,在综合里表现为无法收敛。
第四,看位宽和初始化。key_val < 4'd10这类比较,如果key_val里有 X 位,结果是 X 而不是 false,if分支不执行,寄存器保持旧值。密码移位寄存器初值没清零时,pwd_match的比较结果一定是 X。
// 排查用的暴力初始化,确认是复位问题后删掉 initial begin pwd_shift = 32'd0; digit_cnt = 4'd0; state = 2'd0; err_cnt = 4'd0; wait_cnt = 16'd0; end加上这段之后波形恢复正常,说明问题出在复位路径而不是逻辑本身,回去检查rst_n的极性、复位按键的引脚约束和顶层连线即可。
4.3 关键节点观测清单与断言式自检
波形窗口里加信号不是越多越好。我一般只留下面这几组,跑一次仿真对着表看一遍就能定位到具体模块。
| 观测信号 | 所在模块 | 期望现象 |
|---|---|---|
| clk_1k | clk_div | 周期 1 ms 的方波,占空比稳定 |
| cnt(消抖) | debounce | 按键抖动期间反复清零,稳定后涨到 20 |
| key_pulse | debounce | 一次按键只出现一个单周期低脉冲 |
| digital_cnt | password_lock | 每输一位加 1,输满 8 位后保持 8 |
| pwd_shift | password_lock | 每拍左移 4 位,新键值进最低位 |
| state | password_lock | 0→1(正确)或 0→2(三次错误) |
| unlock | password_lock | 正确密码后拉高并保持 5000 拍 |
比看波形更省事的是在 Testbench 里写自检。前面$display("[PASS]")和$display("[FAIL]")就是最轻量的断言,跑完直接在控制台看结果,不用盯着波形找。如果要用$assert或 SVA,注意 Quartus II 自带的仿真流程对 SystemVerilog 断言支持有限,跑 SVA 建议直接在 ModelSim 里独立建工程。
5. 引脚约束、SignalTap 抓波形与三个能省半天时间的技巧
引脚分配阶段最容易出的事是编号抄错板子。下面这段 Tcl 只能在 Quartus II 的 Tcl Console 里执行,编号是占位示例,务必对着自己板的原理图逐个核对,尤其是clk一定要接在专用时钟引脚上,接普通 IO 会导致时钟树走线劣化。
# 占位编号,按自己板子的原理图替换 set_location_assignment PIN_23 -to clk_50m set_location_assignment PIN_25 -to rst_n set_location_assignment PIN_28 -to unlock set_location_assignment PIN_30 -to alarm set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to clk_50m set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to rst_nIO_STANDARD不写也能通过编译,但默认电平和板上外设不匹配时,表现是「逻辑仿真全对、上板完全没反应」,这类现象优先怀疑 IO 标准。按键输入加弱上拉,能省掉一部分外部电阻。
上板之后逻辑仿真过不了的路径,用 SignalTap II 抓实时波形。采样时钟选 50 MHz 系统时钟而不是 1 kHz 节拍,否则采样深度很快被耗尽;触发条件设成state == 2'd2,能直接抓到锁定瞬间前后的完整信号。采样深度 1K、仅上升沿触发,是抓密码输入过程的常用配置。
三个技巧:一是把密码做成 parameter 而不是硬编码在case里,换密码只改一行;二是综合前先跑 Analysis & Synthesis,检查 Warning 里有没有 latch 推断,always @(*)分支不全会生成锁存器,上板后行为诡异;三是逻辑验证通过后如果要落到自制 PCB,会转到立创 EDA 或嘉立创 EDA 做原理图与布局布线,这时常见的坑是 EDA 元件的引脚与焊盘未对应,封装选错会导致数码管段选和位选全部错位,画板前先拿万用表把封装和实际器件对一遍。
本文还有配套的精品资源,点击获取