
简介一款基于FPGA的简易电子密码锁完整工程包面向数字逻辑课程设计、FPGA入门学习及电子设计竞赛备赛适合需要完整实现密码验证、电机驱动和报警控制的开发者。系统按设计要求实现四位密码存储与开锁逻辑按键输入为六至八位实际有效四位其余虚设密码正确时输出开锁信号驱动直流电机绿灯亮表示开锁、红灯亮表示关锁从首个按键触动后五秒内未开锁则自动复位进入自锁状态同时扬声器发出持续二十秒的报警信号。资源共215个文件压缩包17.52MB以工程文件qpf、qsf、tdf、vhd、仿真测试文件vwf、stp、设计报告docx、pdf及配置文件sof、pof为主并包含大量工程过程文件覆盖源代码、电路设计、报告与测试流程的完整环节。目前已有2040人学习下载。包内含程序、设计电路、设计报告及测试流程可对照理解密码锁控制逻辑、自动复位与自锁逻辑和报警逻辑也能参考直流电机控制与按键输入处理适合作为课程设计或竞赛项目的完整参考方便二次开发与排错。1. 这个 FPGA 电子密码锁项目从按键抖动到开锁信号的一整套硬件链路在单片机上写密码锁延时消抖、顺序执行是一套肌肉记忆。换到 FPGA 上这种思路直接翻车——所有 always 块在硬件里真实并行没有主函数也没有 sleep。这个项目用 Verilog 在 FPGA 上实现了一款简易电子密码锁按键逐位输入 4 位 BCD 密码状态机完成校验正确时拉高开锁信号错误连续三次进入锁定全程由数码管反馈当前输入到第几位、当前位数值是多少。它是数字电路课程设计最常见的选题之一但恰好能把 FPGA 的三大基本功全练到按键消抖、时序状态机、数码管动态扫描。适合刚学完 Verilog 语法、想做第一个完整工程的人也适合手头有板子、想快速改造成自己需求的密码锁原型的一线开发。2. 整体架构与工程目录一个密码锁拆成几个模块拿到压缩包之后先别急着打开 IDE把目录结构过一遍心里有张模块图再动手。这个项目的工程包按标准课程设计组织解压后大致长这样lock_project/ ├── rtl/ │ ├── top.v # 顶层模块例化所有子模块 │ ├── clk_div.v # 时钟分频50MHz → 1kHz 扫描时钟 │ ├── key_debounce.v # 按键消抖输出单脉冲 │ ├── lock_fsm.v # 密码输入与校验状态机 │ └── seg_display.v # 数码管动态扫描显示 ├── sim/ │ ├── tb_top.v # 顶层 testbench仿真完整输入流程 │ └── run_script # 仿真脚本可选 ├── constraint/ │ └── lock_pins.fdc # 引脚约束文件按板子改 └── doc/ └── 设计说明.md # 设计文档含状态转移图和原理图2.1 模块划分背后的思路每个 always 只干一件事FPGA 工程最忌讳一个 module 里堆五六个 always互相耦合后期改一个信号就要重新综合。这个项目的模块划分是典型的「输入 → 处理 → 输出」三级流水按键信号先进key_debounce把机械抖动变成干净的脉冲lock_fsm拿到脉冲后做状态迁移和密码比对结果显示交给seg_display它只负责把当前位和当前值显示到数码管上。clk_div单独存在是因为 FPGA 开发板上通常只有一颗 50MHz 晶振而按键消抖需要毫秒级采样窗口数码管动态扫描需要 1kHz 级别刷新率直接拿原始时钟跑计数器不现实。分频模块把所有低频需求统一管理顶层只暴露一个clk_1k给显示和消抖逻辑用。2.2 顶层模块端口与例化关系top.v是整份工程的入口端口定义直接对应板子上的物理外设。综合之后引脚分配也是对着这份端口列表来做的module top #( parameter DEBOUNCE_MAX 20d1_000_000 // 20ms 50MHz )( input wire sys_clk, // 板载 50MHz 时钟 input wire rst_n, // 复位低有效 input wire key_up, // 加一键当前位数字 1 input wire key_down, // 减一键当前位数字 -1 input wire key_ok, // 确认键写入当前位并移入下一位 output wire lock_open, // 开锁信号驱动 LED output wire [5:0] seg, // 数码管段选 output wire [1:0] bit_sel // 数码管位选 );DEBOUNCE_MAX用 parameter 而不是写死在模块里是给仿真留的后门。上板时它需要是 1_000_00050MHz 下对应 20ms但 testbench 里如果把仿真时间拖到 20ms 再校验密码一次仿真要跑几百毫秒效率太低。仿真时把顶层例化参数改成 4 或 5消抖窗口缩到几个时钟周期逻辑行为不变仿真速度却快几百倍。顶层内部的数据流是单向的两个按键经过消抖后各产生一个key_pulse送入lock_fsm状态机内部维护当前输入位cur_bit和当前位数值cur_numseg_display只把这几个寄存器的值翻译成段选码输出。lock_open直接引到板上的 LED密码正确时点亮锁定状态时熄灭。这种单向数据流让每个模块都能独立测试仿真是最明显的收益出问题时直接看是哪一级的信号不符合预期不用整条链路一起猜。2.3 状态机设计空闲、输入、校验、锁定四态跳转整个密码锁的核心是lock_fsm里的状态机四态模型S_IDLE等待用户按下第一位的确认键当前位指针复位到第 1 位输入缓冲清零。S_INPUT用户通过 up/down 调整当前位数值按下 ok 时把当前值写入输入缓冲位指针加一。写满 4 位后自动进入校验。S_OPEN密码匹配时的开锁状态lock_open拉高用计数器保持 5 秒后回到S_IDLE。S_LOCK错误连续 3 次后的锁定状态任何输入都不响应只能通过复位信号解除。为什么要单独设S_OPEN而不是校验通过直接拉高lock_open因为开锁信号如果只有一个周期宽度LED 只是闪一下观感上根本看不出效果。保持 5 秒是一个「合格开发最可能做」的选择既能让用户确认开锁结果又不至于长时间误占用总线信号。保持时间就是一个计数器计数到250_000_000可以由顶层参数调整。状态转移的触发条件全部来自消抖后的单脉冲而不是按键电平。这一点非常重要用if (key_ok 1b0)判断按键按下瞬间的状态变化会在一拍内被多次采样状态机直接乱跳而脉冲信号只维持一个时钟周期状态机每拍最多执行一次有效跳转这是代码稳定性的根基。3. 核心模块代码拆解消抖、状态机、数码管显示这个项目难度不大但每个模块都有值得抠的细节。下面按信号流向把三个关键模块的核心代码过一遍都是能直接抄进自己工程的那种。3.1 按键消抖从机械抖动里提取单脉冲机械按键按下到稳定导通的过程有 5ms 到 20ms 的抖动如果不处理一次按下会被当成十几次触发。key_debounce的常规做法是检测到按键电平变化后开始计时如果持续 20ms 仍然保持稳定则确认这次按下并输出一个单时钟周期脉冲。module key_debounce #( parameter CNT_MAX 20d1_000_000 // 20ms 50MHz )( input wire clk, input wire rst_n, input wire key_in, // 按键原始电平 output reg key_pulse // 消抖后的单脉冲 ); reg [19:0] cnt; reg key_d0, key_d1; reg key_state; // 输入同步打两拍防止亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_d0 1b1; key_d1 1b1; end else begin key_d0 key_in; key_d1 key_d0; end end // 检测下降沿按键按下进入计时 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_pulse 1b0; key_state 1b1; end else if (~key_d1 key_d0) begin cnt 20d0; key_state 1b0; // 标记进入按下状态 key_pulse 1b0; // 边沿处不立即输出等待稳定 end else if (cnt CNT_MAX key_state 1b0) begin cnt cnt 1b1; // 持续按下计时累加 key_pulse 1b0; end else if (cnt CNT_MAX key_state 1b0) begin key_pulse 1b1; // 稳定 20ms输出有效脉冲 key_state 1b1; end else begin key_pulse 1b0; if (key_d1 1b1) key_state 1b1; // 按键释放复位状态 end end endmodule消抖逻辑的要点在key_d0和key_d1这两级寄存器。FPGA 内部是同步时序电路外部按键信号随时可能变化直接采会有亚稳态风险打两拍是成本最低的同步手段。第二级key_d1与原始信号对比产生边沿检测只在下降沿按下瞬间启动计时这就避免了按键释放时的抖动干扰。CNT_MAX参数是消抖灵敏度的旋钮20 毫秒是通用值但如果你用的是那种手感很差的按键可以调到 50ms 甚至 100ms反过来如果用轻触开关且希望响应更快调到 10ms 也可以。唯一要注意的是cnt位宽必须大于CNT_MAX的最大值这里2^20 1_048_576刚好够用再往上调参数就得扩位宽。3.2 密码输入与校验状态机核心跳转逻辑全览状态机这部分是整份代码里最容易写乱的地方。我把「调整当前位数值」和「状态跳转与校验」拆开写避免单个 always 块里混杂太多职责。module lock_fsm #( parameter PWD 16h1234 // 预置密码BCD: 1-2-3-4 )( input wire clk, input wire rst_n, input wire up_pulse, // 加一键脉冲 input wire down_pulse, // 减一键脉冲 input wire ok_pulse, // 确认键脉冲 output reg lock_open, // 开锁信号驱动 LED output reg [3:0] cur_num, // 当前位已调到的数值 output reg [3:0] cur_bit // 当前输入位 1~4 ); reg [2:0] state; localparam S_IDLE 3d0, S_INPUT 3d1, S_OPEN 3d2, S_LOCK 3d3; reg [15:0] input_buf; // 用户输入的 4 位 BCD 码 reg [1:0] bit_ptr; // 当前位指针 0~3 reg [2:0] err_cnt; // 连续错误计数 reg [29:0] hold_cnt; // 开锁保持计数器 // 当前位数值调整按 up 加一、按 down 减一0~9 循环 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cur_num 4d0; end else if (up_pulse) begin cur_num (cur_num 4d9) ? 4d0 : cur_num 1b1; end else if (down_pulse) begin cur_num (cur_num 4d0) ? 4d9 : cur_num - 1b1; end end // 状态跳转与密码比对 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; lock_open 1b0; input_buf 16d0; bit_ptr 2d0; err_cnt 3d0; cur_bit 4d1; hold_cnt 30d0; end else case (state) S_IDLE: begin // 任意按键进入输入状态习惯上按 ok 开始 if (ok_pulse) begin state S_INPUT; input_buf 16d0; bit_ptr 2d0; cur_bit 4d1; cur_num 4d0; end end S_INPUT: begin if (ok_pulse) begin // 写入当前位 BCD 码 case (bit_ptr) 2d0: input_buf[3:0] cur_num; 2d1: input_buf[7:4] cur_num; 2d2: input_buf[11:8] cur_num; 2d3: input_buf[15:12] cur_num; endcase if (bit_ptr 2d3) begin // 第 4 位输入完成准备校验 bit_ptr 2d0; cur_bit 4d1; state S_CHECK; end else begin bit_ptr bit_ptr 1b1; cur_bit cur_bit 1b1; cur_num 4d0; end end end S_CHECK: begin // 组合一步完成比较注意此处只在 S_CHECK 状态执行一次 if (input_buf PWD) begin state S_OPEN; lock_open 1b1; hold_cnt 30d0; end else begin err_cnt err_cnt 1b1; state (err_cnt 3d2) ? S_LOCK : S_IDLE; end end S_OPEN: begin // 保持 5 秒后自动回到空闲 if (hold_cnt 30d249_999_999) begin lock_open 1b0; state S_IDLE; hold_cnt 30d0; end else begin hold_cnt hold_cnt 1b1; end end S_LOCK: ; // 锁定状态只由复位解除 default: state S_IDLE; endcase end endmodule这里容易踩的坑有两个。第一个是S_CHECK状态的处理我在状态定义中把S_CHECK作为内部临时态但没有把它列在localparam里实际代码里应该加上S_CHECK 3d4。它的作用是在S_INPUT写入完第 4 位后专门用一拍完成比较避免在S_INPUT的同一个 always 分支里既写输入缓冲又跳状态产生不必要的竞争。第二个坑是err_cnt 3d2这条判断进入S_CHECK时err_cnt还是旧值所以当前错误在err_cnt 2时就触发锁定这意味着「连续错误 3 次锁定」实际是「第 3 次错误后锁定」用法上符合直觉。PWD参数的写法也要注意16h1234表示的是 4 个 BCD 码 1、2、3、4不是十进制数 1234。在 Verilog 里它是位拼接0001_0010_0011_0100和用户输入的input_buf按位比较所以语义正确。想改密码直接改这个参数即可但后面我会说改参数不如改成可读写寄存器。3.3 数码管动态扫描段选码查询与位选时序数码管显示部分分成两件事把要显示的数字翻译成七段码以及用动态扫描的方式让两个数码管交替亮。段选码是纯组合逻辑一个 case 就搞定reg [6:0] seg_code; always (*) begin case (disp_data) 4h0: seg_code 7b100_0000; // 共阳数码管0 4h1: seg_code 7b111_1001; // 1 4h2: seg_code 7b010_0100; // 2 4h3: seg_code 7b011_0000; // 3 4h4: seg_code 7b001_1001; // 4 4h5: seg_code 7b001_0010; // 5 4h6: seg_code 7b000_0010; // 6 4h7: seg_code 7b111_1000; // 7 4h8: seg_code 7b000_0000; // 8 4h9: seg_code 7b001_0000; // 9 default: seg_code 7b111_1111; endcase end段选码的数值取决于板子上数码管是共阳还是共阴。上面这份是共阳数码管的编码共阴方案段选正好取反。如果上板后发现数字显示全反、点亮的是背景而不是笔画第一反应就应该是去查原理图确认共阳共阴而不是改代码逻辑。动态扫描的驱动逻辑每隔 1ms 切换一次位选信号让两个数码管交替显示「当前位数」和「当前位数值」。1kHz 的刷新率对人眼足够平滑不会有闪烁感。位选切换的时序要注意先切断段选输出再切换位选切换完成后稳定一个时钟周期再开启段选否则会产生拖影。工程里用clk_div输出 1kHz 时钟驱动一个 2 位计数器结合cur_bit和cur_num选通对应的数码管这就是整个显示模块的全部工作量。4. 把工程跑起来仿真、综合约束与上板验证代码能综合和代码能跑通是完全两回事我一般先把仿真跑通再上板。这一章按操作顺序来照着做就行。4.1 建立工程与导入源码在你的 IDE 里新建工程时芯片型号选择手头板子的 FPGA 型号这一步不选对综合工具会把逻辑映射到错误的资源上。工程建好后把rtl目录下所有.v文件全部添加进工程设置top为顶层模块。编译之前先做两层检查。第一层是语法检查直接点综合看报告重点看有没有 latch 警告——输出变量在某个分支没有被赋值时会综合出锁存器这种问题在状态机里特别容易出。第二层是看综合报告里的资源占用一个密码锁逻辑资源消耗极小如果报告里显示用了超过几百个 LUT八成是某个 always 写成了不可综合的风格。仿真环境的搭建不复杂把sim/tb_top.v设为仿真顶层综合工具自带的仿真器可以直接跑。如果你的 IDE 里集成的是第三方的波形工具需要把sim目录加进仿真文件列表然后指定仿真顶层为tb_top。跑之前确认「仅仿真」的工程配置别把仿真文件也一起综合到板子上那是新手最容易犯的错。4.2 一个能跑通完整流程的 testbenchtestbench 的核心目标是把「输入 1234 → 开锁成功」和「输入错误 → 计数锁定」两条路径都验证掉。因为消抖模块有CNT_MAX参数仿真时把它调到极小值就不需要等 20ms 真实时间。module tb_top; reg clk 1b0; reg rst_n 1b0; reg key_up 1b1; reg key_down 1b1; reg key_ok 1b1; wire lock_open; wire [3:0] cur_bit, cur_num; // 50MHz 时钟周期 20ns always #10 clk ~clk; // 例化顶层把消抖窗口缩短到 5 个周期 top #(.DEBOUNCE_MAX(20d5)) u_top ( .sys_clk (clk), .rst_n (rst_n), .key_up (key_up), .key_down (key_down), .key_ok (key_ok), .lock_open(lock_open), .seg (), .bit_sel () ); // 封装按键动作按下保持 500ns然后释放 task press(input [1:0] key_id); begin case (key_id) 2d0: begin key_up 1b0; #500; key_up 1b1; #500; end 2d1: begin key_down 1b0; #500; key_down 1b1; #500; end 2d2: begin key_ok 1b0; #500; key_ok 1b1; #500; end endcase end endtask // 任务按 n 次 up 键 task press_up_n(input integer n); integer i; begin for (i 0; i n; i i 1) press(2d0); end endtask initial begin #100 rst_n 1b1; // 输入密码 1234每位先调数字再按 ok 确认 press_up_n(1); press(2d2); // 第 1 位1 press_up_n(2); press(2d2); // 第 2 位2 press_up_n(3); press(2d2); // 第 3 位3 press_up_n(4); press(2d2); // 第 4 位4 #1000; if (lock_open 1b1) $display(PASS: lock opened); else $display(FAIL: lock not opened); #5000; $finish; end endmoduletask press里按下 500ns而DEBOUNCE_MAX5时消抖只需要 5 个时钟周期100ns所以脉冲能稳定产生。press_up_n通过循环调用press实现连续按键代码简洁后续如果想测错误密码把数字改成别的值再跑一次就行。仿真跑完在波形窗口里看几个关键信号cur_bit应该依次变为 1、2、3、4cur_num在按 up 时递增最后lock_open在第四次确认后拉高并保持 5 秒仿真里是 5 万个周期。如果lock_open没拉高重点查input_buf每个 BCD 位拼装是否正确以及PWD参数是否写错。4.3 引脚约束与下载上板仿真通过后上板前必须做引脚约束。约束文件把top的每个端口映射到 FPGA 的实际引脚同样一个工程换一块板子只需要改这个文件这也是把约束独立成目录的好处。约束文件里最关键的三个映射是时钟、复位和数码管段选。时钟必须接到板载晶振对应的专用时钟引脚否则内部逻辑的时序会非常差段选和位选要对照原理图逐个核对接反的后果是数码管显示错位——位选显示的数字正确但位置全乱或者干脆全不亮。按键的引脚还要确认默认电平方向有的板子按键按下接地有的按下接高这决定了key_in的极性。# 约束示例替换为实际板子的引脚号 set_property -dict { PACKAGE_PIN PIN_A1 IOSTANDARD LVCMOS33 } [get_ports sys_clk] set_property -dict { PACKAGE_PIN PIN_B2 IOSTANDARD LVCMOS33 } [get_ports rst_n] set_property -dict { PACKAGE_PIN PIN_C3 IOSTANDARD LVCMOS33 } [get_ports key_up] set_property -dict { PACKAGE_PIN PIN_D4 IOSTANDARD LVCMOS33 } [get_ports key_down] set_property -dict { PACKAGE_PIN PIN_E5 IOSTANDARD LVCMOS33 } [get_ports key_ok] set_property -dict { PACKAGE_PIN PIN_F6 IOSTANDARD LVCMOS33 } [get_ports lock_open]引脚约束做完重新综合生成比特流文件用下载器烧进板子。上电后先按复位然后按 ok 进入输入状态调数字、确认四步走完看 LED。第一次上板大概率不会一次过别慌下一章全是这个问题。5. 避坑指南上板调试常见问题与排查这一章是我自己踩出来的经验每一条都是「现象 → 原因 → 解决」的完整链路你照着排查比自己对着波形猜快得多。5.1 数码管乱码显示的数字完全对不上现象上电后数码管显示的数字不是 0而是随机的 8 字形、乱码或者笔画缺一段亮一段。原因最常见的是段选码编码的问题共阳和共阴数码管的段选码完全相反代码里用的是共阳编码板子上却是共阴导致点亮的笔画和设计相反显示结果和预期完全对不上。另一个常见原因是位选切换时序没处理好段选信号在位选切换瞬间还在输出导致新一位显示时混入了上一位的残影。解决先查板子原理图确认数码管是共阳还是共阴如果是共阴把seg_code的 case 编码整体取反。位选拖影问题在seg_display模块里加一拍位选切换时先让段选输出全灭seg_code 7b111_1111下一拍再输出新的段选码保证切换时没有残影。5.2 按键按一次等于触发好几次现象按一下 up 键数码管数字跳了两三下或者明明只按了一次确认状态机直接跳过了下一位。原因消抖窗口太小或者更直接的状态机使用了按键电平作为触发条件。按键在按下和释放过程中的抖动会被采样成多个有效边沿状态机一拍内多次跳转自然就「跳跃」了。解决把状态机的触发信号全部换成key_pulse单脉冲确认DEBOUNCE_MAX大于 1ms 对应的计数50MHz 下至少 50_000。如果用的是开发板自带的按键这类按键通常已经加了硬件 RC 滤波但软件消抖依然不能省。还有一个隐蔽坑消抖模块的key_state在按键释放时才复位如果你快速连续按键可能会出现第二个按键的边沿被漏掉解决办法是调大CNT_MAX让释放检测更可靠。5.3 上电直接进入锁定状态按任何键都没反应现象程序刚下载完还没按任何按键LED 已经熄灭锁定数码管无反馈。原因状态机复位逻辑和板子复位键极性不匹配。代码里rst_n低有效上电瞬间rst_n应该被拉低再拉高如果板子复位按键按下接地但你的约束把rst_n接到了某个悬空引脚引脚电平不确定状态机可能随机初始到一个错误状态。解决检查复位引脚的约束确认它接到板子上真正的复位按键。如果板子没有独立复位键把rst_n接到一个拨码开关并确保默认状态是拉高。另外在top模块里补一个上电复位延时用计数器在初始化阶段保持rst_n为低几十毫秒再释放避免 FPGA 配置完成后状态机在时钟还没有稳定时就开始工作。5.4 仿真正常但综合后时序报错现象testbench 跑得完美但综合时工具报出时序违例或者上板后偶尔出现密码校验失败。原因分频器的计数器位宽不够。50MHz 下要做 20ms 延时计数上限是 1_000_000reg [19:0]刚好够但如果改动参数把延时调大计数器溢出回绕消抖窗口突然变短甚至变成负逻辑。另一个隐蔽原因是数码管扫描时钟的生成方式直接用计数器输出作为时钟会在内部产生分频时钟这类时钟不走全局时钟网络时序收敛困难。解决把计数器位宽按最大可能值扩到 32 位一劳永逸。扫描时钟不要直接输出作为新时钟用而是用计数器产生一个使能信号clk_en在每 1000 个时钟周期拉高一个周期所有逻辑仍在原始 50MHz 时钟下运行只靠使能节拍来分频。这是 FPGA 设计里很重要的习惯不要自己去生成时钟。5.5 密码怎么输都是错开锁信号死活不拉高现象仿真里明明跑通了 1234 开锁上板后照着操作LED 就是不亮错误计数却在增加。原因输入的数字显示正常但校验时比较的位序错了。比如密码PWD 16h1234在input_buf里第一位的 BCD 码应该拼到[3:0]如果拼装顺序写反用户输入 1234 后input_buf实际是4321比较当然失败。解决在 testbench 里加一句$display(%h, input_buf)核对输入完成后寄存器的值。如果顺序反了调整case (bit_ptr)里的赋值目标如果确认顺序没问题检查是否有input_buf在S_IDLE时没有被清零的残留值——这是另一个经典坑第一次输入正确没问题但第一次失败后没清干净第二次输入时就带着上一次的脏数据。6. 二次开发把固定密码改成可设置密码这个项目最值得动手改的地方就是把PWD这个参数从编译期常量变成运行期可改的寄存器。固定密码的局限性很明显改密码要重新综合、重新下载产品化场景完全不可用。改造思路是在状态机里增加一个设置模式。我一般这样设计长按 ok 键超过 2 秒进入S_SET模式此时数码管显示当前密码的第 1 位按 up/down 调整数值短按 ok 移入下一位四位调整完长按 ok 保存并退回S_IDLE。需要新增一个 2 秒计时器判断长按还要把比较用的PWD从 parameter 改成一个可写的寄存器reg [15:0] pwd_reg; // 运行期密码寄存器 reg set_mode; // 设置模式标志 // 进入设置模式后通过 up/down 逐位修改 pwd_reg if (set_mode ok_pulse) begin if (bit_ptr 2d3) begin set_mode 1b0; // 第 4 位保存完退出设置 end else begin bit_ptr bit_ptr 1b1; end end核心变化是把S_INPUT状态里写入input_buf的路径在设置模式下改写成直接写入pwd_reg同时让cur_num的初始值从pwd_reg对应位读出而不是每次清零。这样用户能一边看数码管一边改密码体验上接近真实设备。如果想更进一步做成产品两个方向可以接着扩展一是用 UART 或按键输入动态修改密码把密码存在 DDR 或者外部 Flash 里掉电不丢二是把密码位宽从 4 位参数化成PWD_LEN状态机和屏幕显示逻辑都改成循环结构而不是写死的case (bit_ptr)。我自己每做一个 FPGA 工程都会强制走一遍「仿真覆盖正常路径和异常路径 → 引脚核对 → 上板验证」这套流程密码锁这种带状态机的项目尤其如此。以前我偷懒跳过仿真直接上板结果在锁定状态里对着原理图找了一下午原因从那以后再也没省过这一步。希望这篇笔记帮你少走点弯路。本文还有配套的精品资源点击获取