十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Cyclone II的FPGA四位电子密码锁设计与Verilog实现

基于Cyclone II的FPGA四位电子密码锁设计与Verilog实现 简介这是一份基于 Cyclone II FPGA 的 4 位电子密码锁设计工程采用 Verilog 编写并带数码管显示适合 FPGA 初学者、数字逻辑课程学生以及电子设计爱好者作为入门到进阶的练习项目。压缩包内共 283 个文件涵盖 Quartus II 13.0 工程配置qsf/qpf、Verilog 源文件v、仿真波形vwf、原理图模块bsf/bdf、时序与综合报告rpt/sdo以及最终烧录文件sof/pof等压缩后仅 1.45MB方便快速下载与部署。已有 896 人浏览学习。通过该项目可掌握密码锁的模块划分、按键输入处理、密码比较、状态控制及数码管动态显示等关键设计方法同时工程中附带的测试平台与波形文件能帮助读者理解仿真验证流程从源码、编译到下载形成完整 FPGA 开发链路是一份具有实操参考价值的数字系统设计案例。1. 用 Cyclone II 和 Quartus 13.0 复刻 4 位电子密码锁为什么值得做4 位电子密码锁是 FPGA 入门最常见的小工程按键输入、密码比对、结果反馈逻辑不复杂却把时序电路最常用的积木全用上了。标题里的 cyclone2 指 Altera Cyclone II 器件EP2C5、EP2C8配合 Quartus 13.0Verilog 编码、引脚约束、ModelSim 仿真在一个软件里走完。真正的收获不在密码锁本身而在把 50MHz 分频、按键消抖、矩阵扫描、状态机、七段数码管动态显示这条信号链亲手打通。适合练第一个完整工程的初学者也适合准备数字逻辑面试、想把状态机落到工程上的工程师。2. 信号链与时钟方案分频、按键消抖、矩阵扫描的分工先把模块边界划清楚比急着写代码重要。常见做法是拆成四块分频、键盘、锁控、显示顶层只做实例化和连线。分频负责把 50MHz 主时钟变成 1kHz 的节拍脉冲键盘扫描负责逐行驱动并读回列状态叠加消抖后输出键值和单拍脉冲锁控是状态机主体负责缓冲输入、比对密码、计数错误次数显示模块把锁控给出的 4 位数据动态扫到七段数码管上。各自时钟域的依赖尽量单一方便后面单独仿真和替换。模块输入输出职责clk_tickclk, rst_ntick(1kHz)产生节拍使能脉冲key_matrixclk, tick, colrow, key_num, key_en行扫描 消抖 译码lock_ctrlclk, tick, key_en, key_numdisp_data, lock_on状态机 密码校验seg_dispclk, tick, disp_dataseg, sel段码译码 位选动态扫描2.1 为什么用 1kHz 时钟使能而不是直接分频出慢时钟很多新手会写一个always (posedge clk_1k)把分频输出当成时钟用。这在 FPGA 里是典型的门控时钟误区分频寄存器产生的信号没有走全局时钟网络扇出和偏斜都不可控时序约束也不好加综合器还会报 Gated clock 提示。常规做法是把分频做成计数器溢出脉冲也就是时钟使能所有寄存器仍然统一用 50MHz 主时钟只在使能拍那一周期执行逻辑。下面的 clk_tick 就是标准写法。2.2 分频模块 clk_tick.v一个计数器解决所有节拍module clk_tick #( parameter SYS_CLK_HZ 50_000_000, // 晶振频率 parameter TICK_HZ 1_000 // 目标节拍频率 ) ( input wire clk, input wire rst_n, output reg tick ); localparam CNT_MAX SYS_CLK_HZ / TICK_HZ - 1; // 49999 reg [15:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 16d0; tick 1b0; end else if (cnt CNT_MAX) begin cnt 16d0; tick 1b1; // 每满 50000 个时钟拉高一拍 end else begin cnt cnt 1b1; tick 1b0; // 其余周期都保持低电平 end end endmodule参数SYS_CLK_HZ和TICK_HZ在实例化时可以覆盖改晶振频率不用动逻辑。tick 每 1ms 出现一次高电平脉冲后续所有模块都判断tick而不是posedge tick。注意计数器位宽50MHz / 1kHz 50000需要 16 位不要照抄 8 位计数器的模板。键扫描和显示刷新都复用这一个节拍整个工程只有一条时间基准。提示如果开发板晶振是 24MHz实例化时把SYS_CLK_HZ改成 24_000_000 即可下面所有模块不用改。2.3 矩阵键盘扫描与 10ms 消抖行列译码的键位表4x4 矩阵键盘用 4 根行线加 4 根列线覆盖 16 个键行线由 FPGA 驱动、每次只拉低一行列线接上拉电阻作为输入。行列交叉的位置决定键值把键值定义为{行号, 列号}的组合译码就变成纯拼接不用写一长串 case。这里采用的键位表如下行\列列0(1110)列1(1101)列2(1011)列3(0111)行0(00)0123行1(01)4567行2(10)8910 确认11 清除行3(11)12131415 预留key_matrix.v 把行扫描、列打拍、译码和消抖放在同一个模块里输出key_en是经过 10ms 稳定窗口后的单拍脉冲module key_matrix #( parameter STABLE_CNT 10 // 1kHz 节拍下 10 拍约 10ms ) ( input wire clk, input wire rst_n, input wire tick, input wire [3:0] col, // 列输入无键按下为 4b1111 output reg [3:0] row, // 行驱动低有效 output reg [3:0] key_num, // 0~9 数字10确认11清除 output reg key_en ); reg [1:0] row_idx; reg [3:0] col_r; // 列打一拍避开换行瞬间毛刺 reg [3:0] val_r; reg [15:0] cnt_stable; function [1:0] col_enc; // 列电平 - 列号 input [3:0] c; begin case (c) 4b1110: col_enc 2d0; 4b1101: col_enc 2d1; 4b1011: col_enc 2d2; default: col_enc 2d3; endcase end endfunction always (posedge clk or negedge rst_n) begin if (!rst_n) begin row_idx 2d0; row 4b1110; end else if (tick) begin row_idx row_idx 2d1; case (row_idx) 2d0: row 4b1110; 2d1: row 4b1101; 2d2: row 4b1011; default: row 4b0111; endcase end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin col_r 4b1111; val_r 4d0; end else begin col_r col; val_r (col_r 4b1111) ? 4d0 : {row_idx, col_enc(col_r)}; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_stable 16d0; key_en 1b0; key_num 4d0; end else begin key_en 1b0; if (col_r 4b1111) begin // 无键按下清窗口 cnt_stable 16d0; end else if (val_r key_num) begin // 与上一次同一个键 if (cnt_stable STABLE_CNT) begin key_en 1b1; // 抖动窗口结束上报按键 cnt_stable 16d0; end else begin cnt_stable cnt_stable 1b1; end end else begin // 换键重新计时 key_num val_r; cnt_stable 16d0; end end end endmodulecol_enc是个纯组合函数把列线上的低电平位置翻译成 2 位列号。行切换后的第一个节拍采到的还是上一行状态这个瞬态噪声正好被消抖窗口吸收。按键长按时key_en会周期性重复输出这个行为在第 5 章用一个标志位处理。3. Verilog 实现核心密码锁状态机、校验与七段数码管动态显示3.1 状态机设计与编码为什么需要 6 个状态锁控模块是整颗芯片里唯一有记忆的地方用一段式状态机把输入缓冲、错误计数、开锁输出放在一个 always 里维护。状态设计为六态IDLE 等待第一位输入ENTER 持续收数字CHECK 做一次性比对UNLOCK 开锁保持WRONG 提示错误等待清除键LOCKED 锁定 30 秒。状态编码用二进制顺序编码3 位足够面试手撕状态机时这也是最常见的八股拆法。状态进入条件主要动作IDLE复位/清除清错误计数和输入缓冲ENTER第一位数字到来左移缓冲集满 4 位跳 CHECKCHECK第 4 位输入完成比对后跳 UNLOCK 或 WRONGUNLOCK密码正确点亮 LED显示正确密码WRONG密码错误且未达上限等清除键或超时回 IDLELOCKED错误次数达上限等待LOCK_TICKS节拍后解锁恢复3.2 lock_ctrl.v密码缓冲、比对与 3 次错误锁定4 位输入用 16 位寄存器 buf 存下每来一个数字左移一次digit_cnt数到 3 表示已集满 4 位自动进入比对。比对把默认密码和 buf 直接比较正确则lock_on拉高错误则错误计数加一。连续错 3 次进入 LOCKED之后按键全部无效必须等计时结束。module lock_ctrl #( parameter [3:0] PWD0 4d1, PWD1 4d2, PWD2 4d3, PWD3 4d4, parameter [2:0] MAX_ERR 3d3, parameter [14:0] LOCK_TICKS 15d30000 // 30 秒 1kHz ) ( input wire clk, input wire rst_n, input wire tick, input wire key_en, input wire [3:0] key_num, output reg [15:0] disp_data, // 4 位显示数据 output reg lock_on ); localparam IDLE 3d0, ENTER 3d1, CHECK 3d2, UNLOCK 3d3, WRONG 3d4, LOCKED 3d5; reg [2:0] state; reg [15:0] buf; reg [3:0] digit_cnt, err_cnt; reg [15:0] lock_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; buf 16d0; digit_cnt 4d0; err_cnt 4d0; lock_on 1b0; disp_data 16hFFFF; lock_cnt 16d0; end else begin case (state) IDLE: begin disp_data 16hFFFF; // 全灭 if (key_en key_num 4d10) begin buf {4d0, key_num}; digit_cnt 4d1; state ENTER; end end ENTER: begin if (key_en key_num 4d10) begin buf {buf[11:0], key_num}; // 左移最新在最高位 digit_cnt digit_cnt 1b1; if (digit_cnt 4d3) state CHECK; end else if (key_en key_num 4d11) begin buf 16d0; state IDLE; // 清除键 end disp_data buf; end CHECK: begin if ({PWD0, PWD1, PWD2, PWD3} buf) begin lock_on 1b1; disp_data {PWD0, PWD1, PWD2, PWD3}; state UNLOCK; end else begin err_cnt err_cnt 1b1; disp_data 16h0000; // 错误提示显示 0000 state (err_cnt 1 MAX_ERR) ? LOCKED : WRONG; end end UNLOCK: begin disp_data {PWD0, PWD1, PWD2, PWD3}; if (key_en key_num 4d11) begin lock_on 1b0; disp_data 16hFFFF; state IDLE; end end WRONG: begin if (key_en key_num 4d11) begin disp_data 16hFFFF; state IDLE; end end LOCKED: begin disp_data 16hFFFF; // 显示熄灭表示锁定 if (tick) begin if (lock_cnt LOCK_TICKS) begin lock_cnt 16d0; err_cnt 4d0; state IDLE; end else begin lock_cnt lock_cnt 1b1; end end end endcase end end endmoduleLOCK_TICKS的单位是节拍数默认 30000 对应 30 秒。四个密码值做成 parameter 而不是写死在比较表达式里是为了第 5 章验证时能通过实例化参数快速改密码。WRONG 状态只等清除键实际产品里一般再加一个超时自动复位这里从简不引入额外计时器。注意CHECK里err_cnt 1 MAX_ERR的判断错误已经发生先加一再比较避免丢最后一次错误计数。这是状态机实现里最容易漏的边界条件。3.3 七段数码管动态扫描位选轮询加段码译码数码管显示必须处理两个物理限制一是 4 位共用一组段线不可能同时亮只能轮流点亮并靠视觉暂留形成稳定画面二是共阳共阴的段码完全相反。下面的 seg_disp 每 1ms 切换一位4 位一轮约 4ms刷新率 250Hz高于人眼闪烁阈值。先看段码表以共阳数码管、低电平点亮为例显示段码(a~g)显示段码(a~g)01000000500100101111100160000010201001007111100030110000800000004001100190010000module seg_disp #( parameter REFRESH_TICK 1 // 每 1 个 tick 切换一位 ) ( input wire clk, input wire rst_n, input wire tick, input wire [15:0] disp_data, output reg [6:0] seg, output reg [3:0] sel ); reg [1:0] pos; reg [3:0] digit; reg [6:0] seg_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pos 2d0; end else if (tick) begin pos pos 2d1; case (pos) 2d0: begin sel 4b1110; digit disp_data[3:0]; end 2d1: begin sel 4b1101; digit disp_data[7:4]; end 2d2: begin sel 4b1011; digit disp_data[11:8]; end 2d3: begin sel 4b0111; digit disp_data[15:12]; end endcase end end always (*) begin case (digit) 4d0: seg_r 7b1000000; 4d1: seg_r 7b1111001; 4d2: seg_r 7b0100100; 4d3: seg_r 7b0110000; 4d4: seg_r 7b0011001; 4d5: seg_r 7b0010010; 4d6: seg_r 7b0000010; 4d7: seg_r 7b1111000; 4d8: seg_r 7b0000000; 4d9: seg_r 7b0010000; default: seg_r 7b1111111; // 其余值熄灭 endcase end assign seg seg_r; endmodulepos是 2 位轮询指针每个 tick 指向一位同时把对应 nibble 送去译码。段码 7 位按{a,b,c,d,e,f,g}排列如果板子原理图把段线接到 dp 或者其他顺序需要按原理图重排这是移植到不同开发板时唯一要动的显示代码。4. Quartus 13.0 工程搭建、引脚约束与 ModelSim 仿真验证4.1 Quartus 13.0 建工程器件选型与顶层文件组织Quartus 13.0 对 Cyclone II 支持很完整安装时勾选 Cyclone 系列器件库即可。新建工程走一遍 New Project Wizard工程名建议用lock_top路径不要出现中文和空格器件型号按板子选144 脚小板的典型型号是 EP2C5T144C8N资源多一点的板子用 EP2C8Q208C8N。把 clk_tick.v、key_matrix.v、lock_ctrl.v、seg_disp.v 和顶层一起加入工程顶层模块只需要十几行实例化连线module lock_top ( input wire clk, input wire rst_n, input wire [3:0] col, output wire [3:0] row, output wire [6:0] seg, output wire [3:0] sel, output wire lock_on ); wire [3:0] key_num; wire key_en, tick; wire [15:0] disp_data; clk_tick u_tick (.clk(clk), .rst_n(rst_n), .tick(tick)); key_matrix u_key (.clk(clk), .rst_n(rst_n), .tick(tick), .col(col), .row(row), .key_num(key_num), .key_en(key_en)); lock_ctrl u_lock (.clk(clk), .rst_n(rst_n), .tick(tick), .key_en(key_en), .key_num(key_num), .disp_data(disp_data), .lock_on(lock_on)); seg_disp u_disp (.clk(clk), .rst_n(rst_n), .tick(tick), .disp_data(disp_data), .seg(seg), .sel(sel)); endmodule顶层模块名必须和工程 top-level entity 一致否则 Quartus 默认把文件名作为顶层编译时会报Cant elaborate top-level hierarchy。另外 Cyclone II 的每个 I/O bank 电压由板子决定引脚约束前先在 Assignment Editor 里确认 I/O Standard 是 3.3-V LVTTL。4.2 用 TCL 脚本做引脚约束一个可版本化的管脚文件手工在 Assignment Editor 里一个个点引脚很慢而且换板子就要重来。常见做法是写一个 TCL 脚本在 Quartus 里 Tools Run TCL Script 执行或者把内容存成 .tcl 后通过 Assignments Import Assignments 导入。下面是一份示意引脚必须对照自己板子的原理图替换set_location_assignment PIN_23 -to clk set_location_assignment PIN_25 -to rst_n set_location_assignment PIN_49 -to row[0] set_location_assignment PIN_50 -to row[1] set_location_assignment PIN_51 -to row[2] set_location_assignment PIN_52 -to row[3] set_location_assignment PIN_98 -to col[0] set_location_assignment PIN_99 -to col[1] set_location_assignment PIN_100 -to col[2] set_location_assignment PIN_101 -to col[3] set_location_assignment PIN_68 -to seg[0] set_location_assignment PIN_69 -to seg[1] set_location_assignment PIN_70 -to seg[2] set_location_assignment PIN_71 -to seg[3] set_location_assignment PIN_72 -to seg[4] set_location_assignment PIN_73 -to seg[5] set_location_assignment PIN_74 -to seg[6] set_location_assignment PIN_78 -to sel[0] set_location_assignment PIN_79 -to sel[1] set_location_assignment PIN_80 -to sel[2] set_location_assignment PIN_81 -to sel[3] set_location_assignment PIN_82 -to lock_on-to后面跟的是信号名总线信号要用[0]、[1]这样的下标逐位写不能写row[*]。TCL 脚本的好处是跟着工程一起走版本换工程、换机器不用重新记忆管脚。导入后跑一次全编译重点看两处是否有引用了但没约束的输入管脚以及Ignored assignment提示是否和你板子的实际连接冲突。提示列输入 col 在原理图上通常已经有上拉电阻Cyclone II 的管脚本身也能开弱上拉。如果按键按下后读不到低电平优先检查上拉再怀疑逻辑。4.3 ModelSim 仿真验证 key_en 单脉冲与 CHECK 比对时序Quartus 13.0 自带 ModelSim-Altera也可以在工程外独立跑 ModelSim。仿真的核心不是看功能对不对而是看三个时间点tick 是否每 50000 个时钟一拍、key_en 是否在按键稳定窗口后只出现单拍、CHECK 后一个时钟内 lock_on 是否拉起。用下面的 tb 直接模拟 4 次按键输入module tb_lock; reg clk 1b0; reg rst_n 1b0; reg [3:0] col 4b1111; wire [3:0] row; wire [3:0] key_num; wire key_en; wire [15:0] disp_data; wire lock_on; always #10 clk ~clk; // 50MHz lock_top u_p(.clk(clk), .rst_n(rst_n), .col(col), .row(row), .seg(), .sel(), .lock_on(lock_on), .key_num(key_num), .key_en(key_en), .disp_data(disp_data)); task press(input [1:0] c); // 拉低某一列模拟键按下 begin col ~(4b0001 c); repeat (20000) (posedge clk); // 保持 20ms col 4b1111; repeat (20000) (posedge clk); // 释放 20ms end endtask initial begin #100 rst_n 1b1; press(0); // 键 1 press(1); // 键 2 press(2); // 键 3 press(1); // 键 5错误密码 repeat (10000) (posedge clk); $stop; end endmoduletb 里的press任务只操作列线行线由 key_matrix 自动轮询所以按一个键至少要等一个完整扫描周期4 个 tick才能被采到。ModelSim 里跑 200ms 仿真波形重点看key_en是否为宽度一个时钟的脉冲disp_data是否在 CHECK 后变成全 0错误提示lock_on是否一直为低。命令行跑仿真的完整流程vlib work vlog clk_tick.v key_matrix.v lock_ctrl.v seg_disp.v lock_top.v tb_lock.v vsim -voptargsacc work.tb_lock add wave -hex /tb_lock/clk /tb_lock/col /tb_lock/row add wave -hex /tb_lock/key_num /tb_lock/key_en add wave -hex /tb_lock/u_p/u_lock/state add wave -hex /tb_lock/disp_data /tb_lock/lock_on run 200ms把 lock_ctrl 内部 state 信号也加进波形可以在 ModelSim 里直接看到 IDLE - ENTER - CHECK 的跳变沿时序问题基本一眼定位。5. 上板调试4 个易错点与一个 30 秒锁定参数化技巧5.1 上板前先自查的三个坑编译通过不等于能跑按顺序排查这三个高频问题。第一综合报告里出现 Gated clock 或 latch 提示检查是不是有always (posedge tick)或漏写 default 分支统一改成时钟使能写法。第二数码管乱码或某一位永远不亮对照原理图确认共阳共阴。若板子是共阴把 seg_r 全部按位取反位选 sel 极性也要同步反转。第三按键怎么按都没反应但数码管显示正常问题多半在列输入上拉Cyclone II 的管脚可以在 Assignment Editor 里把 Weak Pull-Up 打开或外接 10k 电阻验证。5.2 长按去重一个标志位解决自动连发第 2 章的消抖代码在按键持续按住时会周期性重复上报 key_en这在密码锁里是致命的一次长按可能变成两次输入。加一个 key_ready 标志上报一次后立即屏蔽直到按键释放才恢复reg key_ready; always (posedge clk or negedge rst_n) if (!rst_n) key_ready 1b1; else if (key_en) key_ready 1b0; // 已上报一次屏蔽后续重复 else if (col_r 4b1111) key_ready 1b1; // 按键释放后重新允许 wire key_pulse key_en key_ready; // 把 key_pulse 接进 lock_ctrl注意 col_r 是 key_matrix 内部的打拍信号实际工程里要把这个释放条件引到顶层或者把这 3 行直接补进 key_matrix 输出端。测试方法很简单按住确认键不动观察 ModelSim 波形里 key_pulse 应该只出现一次。5.3 用实例化参数和拨码开关加速验收30 秒锁定时间在调试时很难等把锁定时长和密码都做成可覆盖参数上板验收就快很多lock_ctrl #( .PWD0(4d9), .PWD1(4d8), .PWD2(4d7), .PWD3(4d6), .MAX_ERR(3d3), .LOCK_TICKS(15d5000) // 临时改为 5 秒验收后再改回 30000 ) u_lock ( .clk(clk), .rst_n(rst_n), .tick(tick), .key_en(key_pulse), .key_num(key_num), .disp_data(disp_data), .lock_on(lock_on) );更进一步的做法是把 PWD0~PWD3 从常量换成板载拨码开关输入锁控模块比较时读拨码电平而不是 parameter这样改密码不用重新编译适合在教室里轮流动手演示。验收顺序固定为按 1、2、3、4 观察数码管从暗到依次显示、LED 拉高按清除键回暗故意错 3 次数到 LED 熄灭且按键全部失效等锁定结束确认错误计数清空。把LOCK_TICKS从 30000 改回 5000 再全编译一次下载 .sof 确认锁定恢复最后再改回 30000 出最终版本。本文还有配套的精品资源点击获取
返回列表