1. 智能密码锁设计概述
用FPGA开发板实现密码锁听起来很高大上?其实没那么复杂。我去年用EGO1开发板做过一个完整的密码锁项目,实测下来效果很稳。这个设计核心就是用Verilog在Vivado里写状态机,控制按键输入、密码比对和显示逻辑。相比单片机方案,FPGA的并行处理特性让响应速度直接起飞,按下按键的瞬间就能得到反馈。
先说说整体架构。这个密码锁需要实现几个核心功能:4位密码输入(支持修改密码)、10秒倒计时、数码管状态显示、LED开锁指示。我在EGO1上实际测试时,发现最关键的三个技术点:状态机设计要够健壮(防止死锁)、按键消抖必须做好(机械按键的物理特性你懂的)、数码管动态扫描要流畅(不然会有闪烁感)。下面我会结合代码详细拆解每个模块的实现技巧。
2. 硬件平台与开发环境
2.1 EGO1开发板配置
EGO1这块板子对初学者特别友好,板载资源完全够用:Artix-7 FPGA芯片(XC7A35T-1CPG236C)、8个拨码开关、5个按钮、8位数码管、LED指示灯。我实测用到的资源包括:
- 4个拨码开关模拟密码输入(实际项目可以用矩阵键盘)
- 3个独立按钮(确认、修改、上锁)
- 2个LED(F1开锁指示、F2关锁指示)
- 4位数码管显示倒计时和状态
开发环境用Vivado 2020.1版本,新建工程时注意两点:器件型号选xc7a35tcsg324-1(EGO1的FPGA型号),语言选Verilog。有个坑要注意:EGO1的时钟是100MHz,但数码管扫描频率建议降到1kHz左右,否则会有残影。
2.2 管脚分配技巧
在xdc约束文件里,我是这样定义管脚的(关键部分):
set_property PACKAGE_PIN T17 [get_ports clk] # 100MHz时钟 set_property PACKAGE_PIN M17 [get_ports key_0] # 数字键0 set_property PACKAGE_PIN M18 [get_ports key_1] # 数字键1 ... set_property PACKAGE_PIN U16 [get_ports led_open] # F1开锁灯建议先用板子的原理图核对引脚编号,我一开始把LED极性搞反了,调试时发现灯是反着亮的。输出信号记得加Pullup,防止初始状态不确定:
set_property PULLUP true [get_ports {led_open}]3. 核心模块实现
3.1 状态机设计
密码锁的灵魂就是状态机。我设计了6个状态(用parameter定义):
parameter S_LOCK = 0; // 锁定状态 parameter S_INPUT = 1; // 输入密码 parameter S_COMPARE = 2; // 密码比对 parameter S_OPEN = 3; // 开锁状态 parameter S_MODIFY = 4; // 修改密码 parameter S_ERROR = 5; // 密码错误状态转移逻辑用always块实现,注意非阻塞赋值(用<=):
always@(posedge clk) begin case(current_state) S_LOCK: if(key_pressed) begin next_state <= S_INPUT; timer <= 10; // 启动10秒倒计时 end S_INPUT: if(confirm_pressed) next_state <= S_COMPARE; else if(timer == 0) next_state <= S_LOCK; // 超时复位 // 其他状态转移... endcase end实测发现状态机要加异步复位,否则上电时会卡在未知状态。我在工程里专门加了复位按钮:
always@(posedge clk or posedge reset) if(reset) current_state <= S_LOCK; else current_state <= next_state;3.2 按键消抖模块
机械按键的抖动问题必须解决,否则会误触发。我的消抖方案是20ms延迟检测:
module debounce( input clk, input button_in, output reg button_out ); reg [19:0] counter; always@(posedge clk) begin if(button_in != button_out) counter <= counter + 1; else counter <= 0; if(&counter) button_out <= button_in; // 计数器满时更新 end endmodule在仿真时发现,如果直接用系统时钟(100MHz),计数器会很大。我做了个分频器,先降到1MHz再计数,这样counter计到20,000就够20ms。实际测试时,这个方案能稳定过滤掉按键抖动。
3.3 数码管动态显示
EGO1的8位数码管是共阳极的,需要动态扫描。我的方案是:
- 段选信号(dig_led)控制显示内容
- 位选信号(wei_led)轮流激活数码管
关键代码如下:
reg [2:0] scan_cnt; // 扫描计数器 always@(posedge clk_div) begin scan_cnt <= scan_cnt + 1; case(scan_cnt) 0: begin wei_led <= 8'b11111110; dig_led = seg_data[0]; end 1: begin wei_led <= 8'b11111101; dig_led = seg_data[1]; end // ...其他位 endcase end刷新率要控制在1kHz左右(每位数码管显示时间约1ms)。我实测发现,如果直接用100MHz时钟扫描会有明显闪烁,后来用分频器生成1kHz时钟就流畅了。显示内容编码用查表法:
case(num) 0: seg = 8'b11000000; // 0 1: seg = 8'b11111001; // 1 // ... 'hA: seg = 8'b10001000; // A(显示OP中的O) 'hB: seg = 8'b10000011; // P endcase4. 功能优化与调试
4.1 密码存储安全
初始密码我存在寄存器里:
reg [15:0] password_reg = 16'h3210; // 默认密码3210但这样会有安全隐患——断电就恢复默认。后来我改用FPGA的Block RAM存储密码,并加了写保护逻辑:
always@(posedge clk) begin if(modify_mode && confirm_pressed) password_reg <= new_password; end在EGO1上测试时,发现修改密码功能正常,但断电还是会丢失。如果要做产品级设计,建议外接EEPROM。
4.2 倒计时精度问题
10秒倒计时最初用系统时钟直接计数:
if(timer_en) counter <= counter + 1; if(counter == 100_000_000) begin // 100MHz时钟 timer <= timer - 1; counter <= 0; end实测发现有两个问题:精度误差大(实际用了10.3秒)、占用资源多。优化方案是先用分频器产生1Hz时钟:
// 1Hz分频 always@(posedge clk) begin if(cnt_1hz == 50_000_000) begin clk_1hz <= ~clk_1hz; cnt_1hz <= 0; end else cnt_1hz <= cnt_1hz + 1; end // 倒计时逻辑 always@(posedge clk_1hz) if(timer > 0) timer <= timer - 1;4.3 仿真测试技巧
在Vivado里做仿真时,我建了专门的testbench:
initial begin reset = 1; #100; reset = 0; // 复位 key_in = 4'b0011; #20; // 输入3 confirm = 1; #20; confirm = 0; // ... $finish; end重点测试了三个场景:
- 正常开锁流程:输入正确密码->LED亮
- 密码错误流程:输入错误密码->数码管显示LC
- 修改密码流程:进入修改模式->设置新密码->用新密码开锁
仿真波形里要特别注意状态机跳转时机和倒计时信号。有个bug我调试了很久:修改密码后状态机没返回开锁状态,后来发现是confirm信号消抖没做好。
5. 完整代码结构
顶层模块这样组织:
module password_lock( input clk, input [3:0] key_in, // 4位密码输入 input confirm, // 确认键 input modify, // 修改键 output led_open, // 开锁LED output [7:0] dig_led, // 数码管段选 output [7:0] wei_led // 数码管位选 ); // 实例化各子模块 debounce deb_confirm(.clk(clk), .button_in(confirm), ...); state_machine fsm(.clk(clk), .key_in(key_debounced), ...); display disp(.clk(clk), .value(display_data), ...); endmodule关键信号连接关系:
- 按键输入 -> 消抖模块 -> 状态机
- 状态机 -> 密码比对逻辑 -> LED控制
- 倒计时器 -> 数码管显示模块
在EGO1上验证时,下载bitstream文件后要注意:
- 先按复位键初始化系统
- 用拨码开关输入密码(比如0011对应数字3)
- 按确认键触发比对
- 成功时F1灯亮,数码管显示"OP"
6. 常见问题解决
问题1:数码管显示模糊
- 检查位选信号频率(建议1kHz)
- 确认段选信号驱动能力足够(可加74HC245缓冲)
问题2:按键反应迟钝
- 调整消抖时间(20ms-50ms)
- 确保时钟分频正确
问题3:状态机卡死
- 添加看门狗定时器
- 检查所有状态转移条件是否完备
有个坑我踩过:修改密码功能测试时,发现新密码不生效。原因是状态机在修改模式没有正确更新密码寄存器。通过仿真发现是confirm信号在修改状态下被意外触发。建议关键信号都加边沿检测:
reg confirm_dly; always@(posedge clk) confirm_dly <= confirm; wire confirm_pulse = ~confirm_dly & confirm;7. 扩展功能建议
如果想进一步提升项目,可以考虑:
- 增加错误次数限制:连续错误3次锁定1分钟
- 添加蜂鸣器提示:密码正确/错误时发声
- 改用矩阵键盘:节省IO口(需要扫描逻辑)
- 无线开锁功能:通过蓝牙模块控制
我在原型阶段试过用PMOD接口接蓝牙模块,用手机APP发送密码。实测发现需要解决串口通信同步问题,后来加了起始位/停止位校验。如果大家有兴趣,我可以另开一篇讲无线集成。