
简介基于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 20d1_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 2b11; else key_sync {key_sync[0], key_in}; end // 消抖计数两级同步值一致才累加不一致说明还在抖清零重计 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_stable 1b1; end else if (key_sync[0] ! key_sync[1]) begin cnt 20d0; end else if (cnt CNT_MAX) begin cnt cnt 1b1; 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 1b1; key_pulse 1b0; 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扩到21bitCNT_MAX改成21d2_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 4d0; else if (k1_pulse) digit_value 4d1; else if (k2_pulse) digit_value 4d2; else if (k3_pulse) digit_value 4d3; else if (k4_pulse) digit_value 4d4; else if (k5_pulse) digit_value 4d5; else if (k6_pulse) digit_value 4d6; else if (k7_pulse) digit_value 4d7; else if (k8_pulse) digit_value 4d8; else if (k9_pulse) digit_value 4d9; else digit_value 4d0; 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 4d0; localparam S_INPUT 4d1; localparam S_CHECK 4d2; localparam S_UNLOCK 4d3; localparam S_ERROR 4d4; 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 2d0; unlock 1b0; disp_data 24h0_0000_0; end else begin case (state) S_IDLE: begin unlock 1b0; if (key_pulse) begin state S_INPUT; digit_cnt 2d1; disp_data[19:16] key_value; end end S_INPUT: begin if (key_pulse) begin if (digit_cnt 2d3) begin // 第4位输入完成进入校验 disp_data[7:4] key_value; digit_cnt 2d0; state S_CHECK; end else begin // 非第4位存入对应位置并递增计数 case (digit_cnt) 2d0: disp_data[19:16] key_value; 2d1: disp_data[15:12] key_value; 2d2: disp_data[11:8] key_value; endcase digit_cnt digit_cnt 2d1; end end else if (conf_pulse) begin // 未满4位按确认进入校验交给错误状态兜底 state S_CHECK; end end S_CHECK: begin digit_cnt 2d0; if (disp_data[19:16] 4d1 disp_data[15:12] 4d2 disp_data[11:8] 4d3 disp_data[7:4] 4d4) begin state S_UNLOCK; unlock 1b1; end else begin state S_ERROR; end end S_UNLOCK: begin if (key_pulse) begin state S_IDLE; unlock 1b0; end end S_ERROR: begin if (key_pulse) begin state S_IDLE; disp_data 24h0_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 16d0; else if (scan_cnt CLK_FREQ / SCAN_HZ - 1) scan_cnt 16d0; else scan_cnt scan_cnt 16d1; end // 扫描位置0~5循环 always (posedge clk or negedge rst_n) begin if (!rst_n) scan_pos 3d0; else if (scan_cnt CLK_FREQ / SCAN_HZ - 1) begin if (scan_pos 3d5) scan_pos 3d0; else scan_pos scan_pos 3d1; end end // 取出当前扫描位对应的4位显示数据 always (*) begin case (scan_pos) 3d0: cur_digit disp_data[23:20]; 3d1: cur_digit disp_data[19:16]; 3d2: cur_digit disp_data[15:12]; 3d3: cur_digit disp_data[11:8]; 3d4: cur_digit disp_data[7:4]; 3d5: cur_digit disp_data[3:0]; endcase end // 共阳数码管段码表0~9 always (*) begin case (cur_digit) 4d0: seg 8hC0; 4d1: seg 8hF9; 4d2: seg 8hA4; 4d3: seg 8hB0; 4d4: seg 8h99; 4d5: seg 8h92; 4d6: seg 8h82; 4d7: seg 8hF8; 4d8: seg 8h80; 4d9: seg 8h90; default: seg 8hFF; endcase end // 位选低电平选通sel中的一位为0对应点亮一个数码管 always (*) sel ~(6b000001 scan_pos); endmodule扫描节奏是1ms切换一位6位一轮6ms刷新率大约166Hz远高于人眼能感知的闪烁频率。段码表给的是共阳数码管的编码如果EGO1的数码管是共阴的把所有段码按位取反即可。这里最关键的一个习惯是段码用组合逻辑直接跟随当前扫描位位选也是组合输出两者同步变化没有寄存器的额外延迟不容易出现残影。真正的残影往往出在段码寄存器比位选慢一拍的设计里这部分在第5章展开。5. EGO1上板避坑下载、按键、显示三类常见翻车点5.1 硬件管理器识别不到EGO1四条排查路径现象打开Vivado的Hardware Manager点Auto ConnectJTAG设备列表是空的或者下载时报错找不到目标器件。原因这个问题我遇到过不止一次九成是下面四类。第一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_LOCKEDS_LOCKED里用一个32位计数器计时30秒时间到自动回到待机并清零错误计数。// 在S_ERROR分支里增加错误计数 S_ERROR: begin if (err_cnt 3) begin state S_LOCKED; err_cnt 3d0; unlock 1b0; end else if (key_pulse) begin state S_IDLE; disp_data 24h0_0000_0; end end // S_LOCKED状态中计数到30秒后回待机 S_LOCKED: begin if (lock_cnt 32d1_500_000_000) begin // 50MHz * 30s state S_IDLE; lock_cnt 32d0; end else begin lock_cnt lock_cnt 32d1; end end这个扩展只动状态机和计数器消抖和显示模块完全不动这就是当初分层设计留下来的红利。锁定期间最好在数码管上显示一个特定的错误码让用户知道系统不是死机了。6.2 用行为仿真给状态机把关testbench喂什么、看什么上板之前跑一遍行为仿真能省下大量下载调试的时间。仿真里不需要模拟真实的机械抖动直接给时钟和复位然后按时间顺序拉高数字键脉冲和确认键脉冲。要看的信号有三个state_out状态跳变是否按预期走、disp_data的4位密码是否逐位写入、unlock在密码正确时是否拉高。initial begin rst_n 1b0; #100; rst_n 1b1; // 依次输入 1、2、3、4 #20 key_pulse 1b1; key_value 4d1; #20 key_pulse 1b0; #20 key_pulse 1b1; key_value 4d2; #20 key_pulse 1b0; #20 key_pulse 1b1; key_value 4d3; #20 key_pulse 1b0; #20 key_pulse 1b1; key_value 4d4; #20 key_pulse 1b0; // 按确认键 #20 conf_pulse 1b1; #20 conf_pulse 1b0; // 观察 unlock 是否拉高 end仿真里给的脉冲宽度可以很短因为状态机本来就按时钟沿采样和真实按键的20ms消抖没有关系。消抖模块的行为单独验证两者混在一起反而不好定位问题。6.3 我上板前固定走三分钟检查的习惯做过几次上板调试后我给自己定了一个固定检查清单每次生成比特流之前过一遍三分钟能省下半小时的排错时间顶层模块名和set_property top是否一致xdc里所有管脚号是否对照原理图确认过create_clock的周期是否和板载时钟频率匹配消抖CNT_MAX的位宽是否覆盖了待消抖时长数码管位选顺序和板子丝印方向是否一致下载用的USB线是不是数据线。这六项里管脚和时钟约束是翻车率最高的两项。希望这篇笔记能帮你少踩几个坑一次把EGO1上的电子密码锁跑起来。本文还有配套的精品资源点击获取