☰
EGO1开发板实现电子密码锁:FPGA状态机与按键消抖实战
2026/10/16 19:17:05 网站建设 项目流程

简介:基于EGO1开发板与Vivado 2020.1的电子密码锁完整工程包,面向数字逻辑与数字系统设计课程学生及FPGA入门开发者,适合作为大作业参考。工程实现状态机控制、错误报警、倒计时警告与十进制滚动输入等核心功能,覆盖从按键扫描到LED/LCD反馈的完整交互链路,有助于理解数字系统的模块化划分、时序控制与仿真验证全流程。压缩包共306个文件,仅1.7MB,以Vivado工程文件为主,包含.xpr工程、.v源码、.xdc约束、.tcl脚本、.bat批处理脚本、仿真日志与综合实现报告,并附生成好的.bit配置文件和实验报告文档,目录分类清晰,便于逐模块查阅。项目还包含丰富的工程中间文件,如.runs、.srcs、.sim等目录中的日志与缓存,可完整复盘从代码编写到bit流生成的每一步。已有4835人学习,尤其适合需要快速完成同类课程设计或系统学习Xilinx FPGA开发流程的同学。

1. EGO1开发板配Vivado 2020.1做电子密码锁:从点灯到完整小系统的第一道坎

EGO1开发板在Vivado 2020.1上实现一个电子密码锁,听起来不过是按按键、亮数码管,但真正上手你会发现,这个项目把FPGA入门的三个坎一次性踩遍了:按键消抖、状态机设计、多外设时序配合。它不只是一个课程设计,更是一条从单模块点灯走向完整小系统的练手路径。密码锁的代码量不大,难在模块怎么分、状态怎么跳、信号怎么接,这些恰恰是后面做复杂项目时最容易翻车的地方。本文按一次完整的上板调通思路展开,新手可以照着搭,熟手可以直接抄走状态机和xdc里的经验。

2. 电子密码锁的设计拆解:状态机与模块划分先于代码

2.1 功能清单先定死:4位密码、确认键、错误提示与解锁保持

做硬件和写软件最大的区别是:软件可以先写再改,硬件一上板,时序错了很难调。所以第一件事不是写代码,而是把功能边界定清楚。我这里按最常见的课程设计要求来定:4位数字密码,默认密码1234;用0~9共10个按键逐位输入;按确认键触发校验;密码正确点亮解锁LED并保持,等待任意按键回到待机;密码错误数码管显示错误状态,按任意键重新输入。

为什么定4位而不是6位?因为EGO1板载6位数码管,我用其中1位显示当前状态、4位显示已输入的数字,刚好放下。位数越多,按键输入和数码管反馈的复杂度都会上升,4位是功能完整性和实现难度之间的平衡点。密码位数可以做成参数,但第一版不建议贪多。数字键用独立按键而不是矩阵键盘,EGO1板载按键数量足够,独立按键的消抖逻辑更直观,矩阵扫描留给后面做进阶。

2.2 状态图与状态转移:五个状态撑起整个密码锁行为

密码锁的本质是一个有限状态机,所有行为都可以用状态转移来描述。我在设计时把它们拆成五个状态:待机、输入中、校验中、解锁、错误。待机状态等第一个数字键;输入状态接收后续数字,每按一位存一个;校验状态对比缓冲区里的4位密码;解锁状态点亮LED;错误状态给出提示并等待重新开始。

现态转移条件次态动作
待机数字键脉冲输入中存入第1位,输入计数置1
输入中数字键脉冲,计数<4输入中存入当前位,计数加1
输入中数字键脉冲,计数=4校验中存入第4位并触发校验
输入中确认键脉冲校验中按已输入位数校验
校验中密码匹配解锁点亮解锁LED
校验中密码不匹配错误清空输入,显示错误
解锁任意数字键待机回到待机,LED熄灭
错误任意数字键待机重新开始

这个状态表看起来简单,但有一个容易被忽略的边界:如果用户只输入了2位就按确认,状态机要不要处理?我的做法是允许进入校验,让错误状态来兜底。这样比卡死在输入状态更好,因为用户至少有一个明确的反馈路径。状态机的每个转移条件都要能对应用户的一个真实操作,如果某个状态下所有输入都不响应,用户就会觉得"死机了"。

2.3 模块划分与接口约定:消抖、主控、显示三层各管一摊

我习惯把密码锁拆成三个模块:按键消抖、密码锁状态机、数码管扫描驱动。按键消抖负责把物理按键的机械抖动过滤成单脉冲;状态机负责密码的存储、校验和状态流转;数码管扫描负责把显示数据刷到6位数码管上。顶层模块做例化和信号连接。

模块之间用简单的信号约定。消抖模块输出一个时钟宽度的脉冲,状态机看到脉冲就采样一次键值;状态机输出6位显示数据和1位解锁标志;数码管扫描只负责显示,不知道密码是什么。这样分层的好处是:以后想加蜂鸣器报警,只需要在状态机里多拉一个输出;想改显示内容,不用动状态机。消抖单独成模块还有一个实际原因——10个数字键加1个确认键,共11个按键都要消抖,参数化模块例化11次,比在状态机里揉成一团清晰得多。

3. 在Vivado 2020.1里搭EGO1工程:从新建到约束到出比特流

3.1 新建RTL工程:器件型号、源文件与顶层设置

Vivado 2020.1新建工程有两种路径,GUI点点点或者TCL命令。第一次用建议走GUI,熟悉一下界面;后面改引脚、换源文件,TCL效率高很多。创建工程时最关键的一步是器件选择,要选对EGO1主芯片对应的Artix-7型号。不同批次的开发板芯片封装可能不一样,以你手里开发板资料里的型号为准,常见的是Artix-7系列中的xc7a35tcsg324-1,如果找不到,就按手册把封装和速度等级对上。

# 创建工程:工程名 ego1_lock,器件按EGO1资料实际型号填写 create_project ego1_lock ./ego1_lock -part xc7a35tcsg324-1 # 添加RTL源文件 add_files -norecurse ./rtl/top_ego1_lock.v ./rtl/key_debounce.v ./rtl/lock_fsm.v ./rtl/seg_scan.v # 添加约束文件 add_files -fileset constrs_1 -norecurse ./xdc/ego1_lock.xdc # 指定顶层模块 set_property top top_ego1_lock [current_fileset]

这段TCL命令做完,工程骨架就起来了。-part参数指定芯片型号,这个必须和你实际板子一致,选错了后续综合布线都会出问题。源文件我习惯放在rtl目录,约束放xdc目录,在工程里也保持同名,后面多人协作时不会乱。set_property top指定顶层模块名字,Vivado会自动识别,但显式指定更稳妥,避免它把测试平台当成顶层。

3.2 引脚约束xdc:把EGO1的按键、数码管和LED映射到端口

xdc约束文件是上板前最不能省的一步。Vivado只知道你的RTL里有clk、key、seg这些端口,不知道它们对应芯片的哪个物理引脚,全靠xdc来指定。写xdc的第一步是把时钟约束上,这是唯一能让时序分析真正跑起来的约束。

# ---------- 时钟 ---------- # 管脚号务必对照EGO1原理图,下面P4仅为格式示例 set_property PACKAGE_PIN P4 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -period 20.000 -name sys_clk [get_ports clk] # ---------- 数字按键0~9 ---------- # 按键按下为低电平,具体管脚号逐个对照原理图替换 set_property PACKAGE_PIN P5 [get_ports key[0]] set_property IOSTANDARD LVCMOS33 [get_ports key[0]] # key[1]~key[9] 同法添加 # ---------- 确认键 ---------- set_property PACKAGE_PIN P6 [get_ports conf_key] set_property IOSTANDARD LVCMOS33 [get_ports conf_key] # ---------- 数码管位选与段选 ---------- set_property PACKAGE_PIN P7 [get_ports seg[0]] set_property IOSTANDARD LVCMOS33 [get_ports seg[0]] # seg[1]~seg[7]、sel[0]~sel[5] 同法添加

xdc里容易踩的坑有两个。一个是IOSTANDARD忘了写,Vivado会按默认值处理,EGO1的外部接口基本都是3.3V电平,统一写LVCMOS33不会错。另一个是管脚号抄错或者抄了网上的模板没核对原理图,这种错最隐蔽,因为编译不会报错,上板后某个按键没反应才暴露出来。我每次新建工程都会把开发板自带的约束模板找出来,和原理图对照一遍再填,不直接抄。

3.3 综合、实现、出比特流:跑通三个阶段要盯住的报告

源文件和约束齐了,就进入编译流程。Vivado里可以点Run Synthesis、Run Implementation、Generate Bitstream三步,也可以用TCL命令一次跑完。我一般用TCL,因为命令行能清楚看到每一步的日志输出。

# 综合:把RTL转成网表 synth_design -top top_ego1_lock # 逻辑优化与布局布线 opt_design place_design route_design # 生成比特流 write_bitstream -force ./ego1_lock.bit

跑完综合先看Warnings,重点看两类:一类是"unconnected port",说明顶层有信号没接,通常是例化时的疏漏;另一类是"inferred latch",说明组合逻辑里某个分支没赋值,状态机代码里漏了default常常导致这个。实现阶段主要看Timing Summary,密码锁这种低速设计一般不会违例,但如果看到时序不满足,先检查create_clock的周期是不是和板载时钟对上了,时钟约束写错是时序违例最常见的来源。

4. 核心代码落地:消抖、状态机、数码管扫描三块硬骨头

4.1 按键消抖:20ms计数去抖与单脉冲输出

机械按键按下时,触点会抖动几毫秒到几十毫秒,期间电平反复跳变,不处理的话状态机会把一次按下当成多次输入。消抖的思路是:对按键信号持续采样,只有电平稳定保持足够长的时间,才认为是一次有效按下。下面这个模块按50MHz系统时钟设计,CNT_MAX对应20ms。

module key_debounce #( parameter CNT_MAX = 20'd1_000_000 // 50MHz时钟下对应20ms )( input wire clk, input wire rst_n, input wire key_in, // 按键原始电平,按下为低 output reg key_pulse // 按下有效,输出一个时钟周期脉冲 ); reg [1:0] key_sync; // 两级同步,消除亚稳态 reg [19:0] cnt; reg key_stable; reg key_stable_dly; // 两级同步:把异步输入打两拍再使用 always @(posedge clk or negedge rst_n) begin if (!rst_n) key_sync <= 2'b11; else key_sync <= {key_sync[0], key_in}; end // 消抖计数:两级同步值一致才累加,不一致说明还在抖,清零重计 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 20'd0; key_stable <= 1'b1; end else if (key_sync[0] != key_sync[1]) begin cnt <= 20'd0; end else if (cnt < CNT_MAX) begin cnt <= cnt + 1'b1; end else begin key_stable <= key_sync[1]; end end // 边沿检测:key_stable 从1跳0视为一次按下 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_stable_dly <= 1'b1; key_pulse <= 1'b0; end else begin key_stable_dly <= key_stable; key_pulse <= key_stable_dly & (~key_stable); end end endmodule

代码里key_sync是两级同步寄存器,它先把异步的按键电平同步到系统时钟域,避免亚稳态传播。消抖的核心是cnt计数:只有当两级同步值相同时才累加,一旦检测到不一致就清零,这说明电平还在抖动。key_stable表示最终确认的电平。最后的边沿检测把稳定的电平转成单脉冲,key_pulse只持续一个时钟周期。CNT_MAX的计算公式是:消抖时间乘以时钟频率,50MHz乘20ms等于1,000,000。如果你的板载时钟是100MHz,同样的20ms需要2,000,000,这时候参数位宽20bit就不够了,要把cnt扩到21bit,CNT_MAX改成21'd2_000_000。

注意:这里假设按键按下为低电平。如果EGO1的按键按下为高,把复位值和边沿检测逻辑取反即可,判断沿的条件从key_stable_dly & (~key_stable)换成key_stable & (~key_stable_dly)。

4.2 密码锁状态机:输入锁存、确认比对与状态输出

状态机模块接收消抖后的脉冲和键值。10个数字键的脉冲先做或运算得到统一的digit_pulse,再用一条组合逻辑编码出当前按下的数字值。这样写的好处是,无论按哪个键,状态机只需要看一个脉冲信号和一个4位值。

// 11个按键各自例化key_debounce后,在顶层做脉冲合并与键值编码 assign digit_pulse = k0_pulse | k1_pulse | k2_pulse | k3_pulse | k4_pulse | k5_pulse | k6_pulse | k7_pulse | k8_pulse | k9_pulse; // 编码当前按键值,优先级写法简单直接 always @(*) begin if (k0_pulse) digit_value = 4'd0; else if (k1_pulse) digit_value = 4'd1; else if (k2_pulse) digit_value = 4'd2; else if (k3_pulse) digit_value = 4'd3; else if (k4_pulse) digit_value = 4'd4; else if (k5_pulse) digit_value = 4'd5; else if (k6_pulse) digit_value = 4'd6; else if (k7_pulse) digit_value = 4'd7; else if (k8_pulse) digit_value = 4'd8; else if (k9_pulse) digit_value = 4'd9; else digit_value = 4'd0; end

这段编码逻辑放在顶层,不在状态机内部。原因很实际:状态机关注的是"有没有按键输入"和"按了什么值",不关心物理按键的映射关系。以后想改成拨码开关输入或矩阵键盘,只需要改顶层这一段,状态机一行不用动。

下面是状态机主体的核心代码。显示数据我用一个24位输出,高4位是状态码,中间16位存4位密码,最低4位留空,数码管驱动直接从这24位里拆,避免数组端口在顶层例化时出问题。

module lock_fsm ( input wire clk, input wire rst_n, input wire key_pulse, // 数字键有效脉冲 input wire [3:0] key_value, // 当前按下的数字 input wire conf_pulse, // 确认键有效脉冲 output reg unlock, // 解锁指示,驱动LED output reg [3:0] state_out, // 状态编码,供数码管显示 output reg [23:0] disp_data // 6位数码管显示数据 ); localparam S_IDLE = 4'd0; localparam S_INPUT = 4'd1; localparam S_CHECK = 4'd2; localparam S_UNLOCK = 4'd3; localparam S_ERROR = 4'd4; reg [3:0] state; reg [1:0] digit_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; digit_cnt <= 2'd0; unlock <= 1'b0; disp_data <= 24'h0_0000_0; end else begin case (state) S_IDLE: begin unlock <= 1'b0; if (key_pulse) begin state <= S_INPUT; digit_cnt <= 2'd1; disp_data[19:16] <= key_value; end end S_INPUT: begin if (key_pulse) begin if (digit_cnt == 2'd3) begin // 第4位输入完成,进入校验 disp_data[7:4] <= key_value; digit_cnt <= 2'd0; state <= S_CHECK; end else begin // 非第4位,存入对应位置并递增计数 case (digit_cnt) 2'd0: disp_data[19:16] <= key_value; 2'd1: disp_data[15:12] <= key_value; 2'd2: disp_data[11:8] <= key_value; endcase digit_cnt <= digit_cnt + 2'd1; end end else if (conf_pulse) begin // 未满4位按确认,进入校验交给错误状态兜底 state <= S_CHECK; end end S_CHECK: begin digit_cnt <= 2'd0; if (disp_data[19:16] == 4'd1 && disp_data[15:12] == 4'd2 && disp_data[11:8] == 4'd3 && disp_data[7:4] == 4'd4) begin state <= S_UNLOCK; unlock <= 1'b1; end else begin state <= S_ERROR; end end S_UNLOCK: begin if (key_pulse) begin state <= S_IDLE; unlock <= 1'b0; end end S_ERROR: begin if (key_pulse) begin state <= S_IDLE; disp_data <= 24'h0_0000_0; end end endcase end end always @(*) state_out = state; endmodule

状态转移的逻辑读一遍就能对上第2章的状态表。有一个细节值得注意:S_INPUT状态下,第4位数字的存入和状态跳转发生在同一个周期。disp_data[7:4] <= key_value这条非阻塞赋值在时钟沿写入,state也在同一时刻被赋值为S_CHECK,所以进入校验状态时,数码管缓冲区已经是完整的4位密码了。校验用的默认密码1234就在S_CHECK的比较条件里,想改密码直接改这四个数。S_INPUT里的确认键分支是刻意的设计选择:用户只输了2位就按确认,状态机不会卡死,而是进入校验、由错误状态给出反馈,这比什么都不响应更符合直觉。

4.3 数码管扫描:共阳段码表、位选刷新与显示内容编排

EGO1的6位数码管是动态扫描方式驱动的,同一时刻只有一位被点亮,靠快速轮换产生"所有位都亮着"的视觉效果。seg_scan模块的任务是:接收状态机给的24位显示数据,按1kHz的频率轮换点亮6位中的一位。

module seg_scan #( parameter CLK_FREQ = 50_000_000, parameter SCAN_HZ = 1_000 )( input wire clk, input wire rst_n, input wire [23:0] disp_data, // 高4位到低4位对应左到右6个数码管 output reg [7:0] seg, output reg [5:0] sel ); reg [15:0] scan_cnt; reg [2:0] scan_pos; reg [3:0] cur_digit; // 分频:产生SCAN_HZ频率的扫描节拍 always @(posedge clk or negedge rst_n) begin if (!rst_n) scan_cnt <= 16'd0; else if (scan_cnt == CLK_FREQ / SCAN_HZ - 1) scan_cnt <= 16'd0; else scan_cnt <= scan_cnt + 16'd1; end // 扫描位置:0~5循环 always @(posedge clk or negedge rst_n) begin if (!rst_n) scan_pos <= 3'd0; else if (scan_cnt == CLK_FREQ / SCAN_HZ - 1) begin if (scan_pos == 3'd5) scan_pos <= 3'd0; else scan_pos <= scan_pos + 3'd1; end end // 取出当前扫描位对应的4位显示数据 always @(*) begin case (scan_pos) 3'd0: cur_digit = disp_data[23:20]; 3'd1: cur_digit = disp_data[19:16]; 3'd2: cur_digit = disp_data[15:12]; 3'd3: cur_digit = disp_data[11:8]; 3'd4: cur_digit = disp_data[7:4]; 3'd5: cur_digit = disp_data[3:0]; endcase end // 共阳数码管段码表,0~9 always @(*) begin case (cur_digit) 4'd0: seg = 8'hC0; 4'd1: seg = 8'hF9; 4'd2: seg = 8'hA4; 4'd3: seg = 8'hB0; 4'd4: seg = 8'h99; 4'd5: seg = 8'h92; 4'd6: seg = 8'h82; 4'd7: seg = 8'hF8; 4'd8: seg = 8'h80; 4'd9: seg = 8'h90; default: seg = 8'hFF; endcase end // 位选:低电平选通,`sel`中的一位为0对应点亮一个数码管 always @(*) sel = ~(6'b000001 << scan_pos); endmodule

扫描节奏是1ms切换一位,6位一轮6ms,刷新率大约166Hz,远高于人眼能感知的闪烁频率。段码表给的是共阳数码管的编码,如果EGO1的数码管是共阴的,把所有段码按位取反即可。这里最关键的一个习惯是:段码用组合逻辑直接跟随当前扫描位,位选也是组合输出,两者同步变化,没有寄存器的额外延迟,不容易出现残影。真正的残影往往出在段码寄存器比位选慢一拍的设计里,这部分在第5章展开。

5. EGO1上板避坑:下载、按键、显示三类常见翻车点

5.1 硬件管理器识别不到EGO1:四条排查路径

现象:打开Vivado的Hardware Manager,点Auto Connect,JTAG设备列表是空的,或者下载时报错找不到目标器件。

原因:这个问题我遇到过不止一次,九成是下面四类。第一,Vivado安装到最后一步的Cable Drivers没有安装,很多人装完软件就急着用,驱动这一步跳过了;第二,USB线是纯充电线,里面没有数据线芯;第三,开发板没上电,或者JTAG相关的跳线帽没插;第四,JTAG链路频率太高导致握手失败。

解决:按顺序排查。先在设备管理器里看有没有识别出Xilinx USB Cable设备,没有就去Vivado安装目录下的data/xicom/cable_drivers/nt64运行安装脚本。然后换一根短的数据线,普通的手机充电线很多是不带数据传输的。确认板子上电指示灯亮、JTAG跳线帽在位。最后在Hardware Manager里把Target Frequency从默认值降到5MHz左右再试一次连接。这四条路径走完,绝大多数连接问题都能解决。

5.2 按一次键数字跳两位:消抖参数与沿检测的双重问题

现象:按一次数字键,输入计数前进两位,本来输4位密码,按了3次键就进入校验了。

原因:两个可能。一个是消抖时间太短,比如CNT_MAX只对应1ms,机械抖动还没结束,电平的多个沿都被当成了有效输入;另一个是消抖后没有做边沿检测,直接把稳定的电平信号接给了状态机,电平保持期间每个时钟周期都被当成一次按下。

解决:把CNT_MAX调到对应20ms以上,同时确认状态机接收的是key_pulse单脉冲而不是key_in原始电平。还有一个容易踩的坑是:消抖模块本身输出对了,但顶层例化时两个按键的脉冲信号接反了,或者把key_in和key_pulse搞混。这类问题用仿真看波形最直观,把按键脉冲和状态计数同时拉出来看,是消抖的问题还是接线的问题一目了然。

5.3 数码管残影和亮度不均:位选段码的配合顺序

现象:数码管显示数字时有拖影,某个位明显更亮或更暗,甚至能看到"扫描光标"在移动。

原因:残影的典型原因是段码更新的时机不对。如果用寄存器输出段码,且位选和段码在同一次时钟沿更新,切换位选的瞬间,新位选已经生效,但段码还是上一个位置的旧值,这一瞬间就会显示错误内容,视觉上就成了拖影。亮度不均通常是每位停留时间不一致,或者限流电阻阻值偏大导致驱动电流不足。

解决:第一选择是像我第4章那样用组合逻辑输出段码,位选和段码同步切换,从本质上消除延迟失配。如果一定要用寄存器段码,就分两拍:一拍锁存当前位的段码,下一拍再切换位选。亮度不均的问题,检查扫描节拍是否均匀,另外不要把数码管限流电阻选得太大,EGO1板载一般都已经配好,外接模块再接电阻容易过暗。

5.4 确认键按下没反应:输入计数与确认脉冲的时序错位

现象:四位密码输完,按确认键什么反应都没有,或者必须再按一个数字键才会触发校验。

原因:最常见的错误是把确认键的脉冲也混进了数字键脉冲里,或者状态机在S_INPUT状态下只判断了key_pulse,没有单独判断conf_pulse。还有一个更隐蔽的时序问题:确认键消抖后的脉冲只有几个时钟周期宽,如果它恰好出现在状态机还在处理最后一位数字的同一个周期,可能被状态机当成无效输入丢掉。

解决:确认键必须独立走一条脉冲信号,在状态机里用独立的conf_pulse分支判断。如果发现确认键偶发失灵,把确认键的脉冲经过两级打拍再送进状态机,让状态机的采样窗口更宽。另外,要确认消抖模块输出的key_pulse确实是单脉冲而不是持续电平——确认键的电平如果一直保持高,状态机会在每个周期都进入确认分支,表现又变成"一按就跳好几个状态"。

6. 把密码锁做成完整小系统:锁定惩罚、行为仿真与上板清单

6.1 加三次错误锁定与30秒解锁惩罚

真实门锁都有试错惩罚,这个功能加上后,密码锁才像一个完整小系统。思路是在状态机里加一个错误计数器和锁定状态:进入S_ERROR时错误计数加1,计数达到3次跳进S_LOCKED;S_LOCKED里用一个32位计数器计时30秒,时间到自动回到待机并清零错误计数。

// 在S_ERROR分支里增加错误计数 S_ERROR: begin if (err_cnt >= 3) begin state <= S_LOCKED; err_cnt <= 3'd0; unlock <= 1'b0; end else if (key_pulse) begin state <= S_IDLE; disp_data <= 24'h0_0000_0; end end // S_LOCKED状态中,计数到30秒后回待机 S_LOCKED: begin if (lock_cnt == 32'd1_500_000_000) begin // 50MHz * 30s state <= S_IDLE; lock_cnt <= 32'd0; end else begin lock_cnt <= lock_cnt + 32'd1; end end

这个扩展只动状态机和计数器,消抖和显示模块完全不动,这就是当初分层设计留下来的红利。锁定期间最好在数码管上显示一个特定的错误码,让用户知道系统不是死机了。

6.2 用行为仿真给状态机把关:testbench喂什么、看什么

上板之前跑一遍行为仿真,能省下大量下载调试的时间。仿真里不需要模拟真实的机械抖动,直接给时钟和复位,然后按时间顺序拉高数字键脉冲和确认键脉冲。要看的信号有三个:state_out状态跳变是否按预期走、disp_data的4位密码是否逐位写入、unlock在密码正确时是否拉高。

initial begin rst_n = 1'b0; #100; rst_n = 1'b1; // 依次输入 1、2、3、4 #20 key_pulse = 1'b1; key_value = 4'd1; #20 key_pulse = 1'b0; #20 key_pulse = 1'b1; key_value = 4'd2; #20 key_pulse = 1'b0; #20 key_pulse = 1'b1; key_value = 4'd3; #20 key_pulse = 1'b0; #20 key_pulse = 1'b1; key_value = 4'd4; #20 key_pulse = 1'b0; // 按确认键 #20 conf_pulse = 1'b1; #20 conf_pulse = 1'b0; // 观察 unlock 是否拉高 end

仿真里给的脉冲宽度可以很短,因为状态机本来就按时钟沿采样,和真实按键的20ms消抖没有关系。消抖模块的行为单独验证,两者混在一起反而不好定位问题。

6.3 我上板前固定走三分钟检查的习惯

做过几次上板调试后,我给自己定了一个固定检查清单,每次生成比特流之前过一遍,三分钟能省下半小时的排错时间:顶层模块名和set_property top是否一致;xdc里所有管脚号是否对照原理图确认过;create_clock的周期是否和板载时钟频率匹配;消抖CNT_MAX的位宽是否覆盖了待消抖时长;数码管位选顺序和板子丝印方向是否一致;下载用的USB线是不是数据线。这六项里,管脚和时钟约束是翻车率最高的两项。希望这篇笔记能帮你少踩几个坑,一次把EGO1上的电子密码锁跑起来。

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

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

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

立即咨询