基于FPGA的电子密码锁设计:Verilog实现与仿真调试全解析
发布时间:2026/9/17 13:21:18来源:尧图网络
简介《基于FPGA的电子密码锁设计》是一份面向电子信息类本科毕业设计的中文技术文档适合正在选题、开题或需要快速搭建FPGA数字系统方案的学生参考。文档以电子密码锁为研究对象先对比机械锁与单片机电子锁的差异指出单片机方案程序容易跑飞、可靠性差的不足继而阐述用FPGA硬件电路实现算法以提高系统可靠性的设计理念并说明现场可编程带来的升级便利整体覆盖了FPGA设计流程、VHDL语言结构、自顶向下设计方法以及Quartus II平台下的工程实现等关键内容。资源包共1个doc文件大小约942KB正文包含摘要、关键词、目录、绪论、关键技术简介与系统设计等模块可作为论文写作和方案论证的完整参照。目前已有205人学习适合需要理解FPGA应用方法、EDA工具操作流程或电子密码锁实现细节的读者参考。1. 毕业设计选 FPGA 做电子密码锁到底在练什么每年毕业季都能看到《基于FPGA的电子密码锁设计》这个题目它出现的频率高到几乎成了数字逻辑课程的“期末大作业 Plus”。很多同学第一反应是密码锁用单片机做不香吗几十行 C 代码加一个矩阵键盘就能跑起来为什么要用 FPGA 和自己较劲这个反问本身恰恰是这个题目存在的意义——它根本不打算让你做一个能卖钱的产品而是让你在 8 周时间里把数字电路的核心基本功全部过一遍同步时序设计、跨时钟域处理、按键消抖、状态机编码、数码管动态扫描再加上 Modelsim 仿真和板级调试。这些东西在单片机上被库函数和中断屏蔽掉了在 FPGA 上全部暴露在你面前。如果你正在为选题纠结或者已经拿到题但不知道从哪下手这篇文就按我的习惯带你从头到尾理一遍。我会从模块划分讲到具体 Verilog 代码到仿真和上板验证最后补充几个能让答辩老师眼前一亮的进阶技巧。全程不依赖任何开发板厂商的定制 IP通用的 Xilinx 或 Altera 板子都能直接上手。2. 密码锁的功能拆解与 FPGA 内部模块划分电子密码锁听起来像个小系统但只要把它拆成“输入-判断-输出-复位”这条链路就会发现它其实是一个特别标准的数字系统设计题目非常适合用自顶向下的方法来做设计。2.1 系统整体功能需求表先搞清楚要做什么在做任何代码之前先把需求用白纸黑字固定下来。下面是电子密码锁最基础的指标描述我给它做成了需求规约表格功能项规格说明密码位数4 位也可以设计为 6 位但毕业设计按 4 位写逻辑简单且容易演示键盘输入4x4 矩阵键盘0-9 数字键 A/B/C/D 功能键开锁机制顺序输入 4 位密码后按确认键比对正确且密码有效期内触发开锁信号错误处理密码错误输出错误指示连续错误 3 次进入 10 秒锁定状态数码管显示输入位数提示用 6 位数码管左起显示当前输入的位置计数时钟频率开发板系统时钟 50MHz分频得到扫描时钟和消抖计时基准复位机制异步复位上电自动进入待输入状态这里有一个很关键的模块边界问题蜂鸣器、LED 指示灯这类设备驱动逻辑极其简单没必要单独开模块。把它们放到顶层文件的 assign 语句里即可把模块粒度控制在“复杂逻辑单独成模块”的水平。2.2 模块间信号流与数据通路设计基于上面的功能表我一般会把设计分成 6 个模块分频器clk_div、按键消抖debounce、键盘扫描key_scan、密码控制核心controller、数码管动态扫描seg_display、锁定定时器lock_timer。模块之间的关系如下。键盘扫描模块按行扫描的方式捕捉按键动作输出一个按键值 key_value 和一路数据有效脉冲 key_pulse。由于机械按键存在抖动key_pulse 不能直接送进状态机必须先过 debounce 模块。消抖输出的稳定按键值进入 controller 模块这个模块里跑着一个摩尔型状态机专门负责密码比对和状态跳转。controller 的输出包括密码正确标志、错误标志、当前输入位数它们分别去驱动 LED/蜂鸣器和数码管模块。锁定定时器只在错误次数达到 3 时被控制器启动期间控制器暂停响应所有按键输入。这个数据通路里最值得注意的设计决策是把消抖放在扫描之后。有些教材把消抖放在模块最前端也就是先消抖再扫描。实际效果差别不大但先扫描后消抖有一个好处扫描模块只负责产生“某个按键被按下”的事件消抖模块可以对同一个事件连续采样 N 次而不需要分辨具体是哪一行哪一列的电平变化这样消抖状态机可以写得很干净。2.3 为什么用三段式状态机而不是一段式controller 模块是整个设计的灵魂。密码锁的控制逻辑用状态机来描述非常自然常见实现有一段式、两段式和三段式其中三段式是 FPGA 开发中的主流推荐写法。三段式状态机在代码结构上必须分成三个 always 块。第一块负责状态寄存器的时序更新第二块负责组合逻辑的次态判断第三块负责输出赋值。它相对于一段式的最大优势是输出信号不会产生组合逻辑毛刺。在密码锁场景里开锁信号如果出现一个 10ns 的毛刺就可能让电磁锁误动作一下这在演示现场非常尴尬。使用三段式还有一个隐藏的好处便于调试。在 Modelsim 仿真里可以直接把状态寄存器的值引到波形窗口查看当前处于哪个状态。如果使用一段式状态和输出耦合在一起波形看起来会乱得多。代码结构确定之后接下来的章节会给出 core 模块的完整 Verilog 实现包括业界常用的一种“打两拍同步 计数确认”的消抖结构和状态机的详细跳转逻辑。3. 核心模块的 Verilog 实现从消抖到状态机原理讲清楚之后本节直接进入代码实现阶段。我选用 Verilog HDL 来描述整个设计开发环境以 Vivado 为例代码风格是完全可综合的寄存器传输级风格不是仿真专用代码。如果你用的是 Quartus代码可以原封不动地拿过去用只在管脚约束文件上需要做对应修改。3.1 按键消抖与键盘扫描的逻辑层次按键消抖这个问题在单片机里面一个 delay(20ms) 就解决了。在 FPGA 里任何延时都不能用阻塞式的等待来实现否则整个系统时钟都会被卡住。正确做法是使用计数器对时间进行量化。module debounce #( parameter CNT_MAX 1_000_000 // 50MHz时钟下 20ms 计数上限 )( input wire clk_50m, input wire rst_n, input wire key_in, // 来自矩阵键盘扫描模块的原始按键事件 output wire key_pulse // 消抖完成后的单周期脉冲 ); reg [19:0] cnt; reg key_dly1; reg key_dly2; wire key_negedge key_dly2 ~key_dly1; // 下降沿捕获 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt 20d0; end else if (key_in) begin cnt 20d0; // 按键仍处于按下状态持续清零 end else if (cnt CNT_MAX) begin cnt cnt 1b1; // 释放状态持续计数 end end always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin key_dly1 1b1; key_dly2 1b1; end else begin key_dly1 (cnt CNT_MAX) ? 1b0 : key_dly1; key_dly2 key_dly1; end end assign key_pulse key_negedge; endmodule逻辑说明这个消抖模块用了一个反向思维通常我们会在按键按下时计数这里改成在按键释放且保持稳定 CNT_MAX 个时钟周期后才认为一个完整的按键事件已经结束。key_dly1 和 key_dly2 构成一个双寄存器同步链提取下降沿作为单周期脉冲。CNT_MAX 的默认值是 1000000对应 50MHz 时钟下的 20ms这是机械按键从抖动到完全稳定的典型时间。参数说明CNT_MAX 的大小直接决定了按键响应速度。取 20ms 对普通轻触开关绰绰有余但如果使用薄膜键盘抖动时间可能达到 30ms 以上建议把参数改成 1_500_000对应 30ms。这个参数必须保证是整数且需要预留足够位宽的计数器。1_500_000 是 20 位二进制数上限内但 2_000_000 恰好是 2^21如果你改成这个值计数器位宽就得用 22 位。键盘扫描模块的职责是生成哪些按键被按下的组合逻辑编码。4x4 矩阵键盘的行列信号分布在两个端口组上如果直接在 always 块里读一组 wire 型数据综合器不会报错但在时序上极容易出现多列同时导通导致的误判。module key_scan ( input wire clk_50m, input wire rst_n, input wire [3:0] col_in, // 列输入 output reg [3:0] row_out, // 行驱动 output reg [3:0] key_value, // 编码后的按键值 0~15 output reg key_valid // 有效按键事件脉冲 ); reg [15:0] cnt_scan; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) cnt_scan 16d0; else cnt_scan cnt_scan 1b1; end // 扫描节拍0~3分别驱动四行 wire [1:0] scan_phase cnt_scan[15:14]; // 每 65536 周期换一行 always (*) begin case (scan_phase) 2d0: row_out 4b1110; // 第一行拉低 2d1: row_out 4b1101; 2d2: row_out 4b1011; 2d3: row_out 4b0111; default: row_out 4b1111; endcase end always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin key_value 4d0; key_valid 1b0; end else begin case ({scan_phase, col_in}) 6b00_1110: begin key_value 4h1; key_valid 1b1; end 6b00_1101: begin key_value 4h2; key_valid 1b1; end 6b00_1011: begin key_value 4h3; key_valid 1b1; end 6b00_0111: begin key_value 4hA; key_valid 1b1; end 6b01_1110: begin key_value 4h4; key_valid 1b1; end 6b01_1101: begin key_value 4h5; key_valid 1b1; end 6b01_1011: begin key_value 4h6; key_valid 1b1; end // 其他行同理 default: key_valid 1b0; endcase end end endmodule逻辑说明cnt_scan 是一个 16 位自由跑计数器最高两位作为行选信号。每个行选信号持续 65536 个时钟周期1.3ms四行一轮就是 5.2ms 完成整盘扫描。在组合逻辑中根据当前行号和列输入拼成 6 位查找表直接映射到按键编码。参数说明key_value 的高位只用到 4 位所以 A、B、C、D 功能键的编码分别是 4’hA~4’hD。在 controller 模块中如果没有对 B/C/D 做处理它们会被默认当成无效键丢弃。键盘扫描的频率也不需要太高人手指按下一次键盘至少 50ms扫描周期在 5ms 左右是合理的。如果你把 cnt_scan 位宽改成 14 位扫描周期缩到 1.3ms也能正常工作代价是更频繁地读取列信号在高速抖动环境下更容易捕到毛刺。3.2 密码控制状态机的状态跳转与安全机制controller 模块的状态定义是整个设计的核心。我设计了 5 个状态IDLE、INPUT、CHECK、UNLOCK、LOCK。IDLE 是上电初始态也作为每次输入完成后的复位态INPUT 是在收集 4 位密码的阶段CHECK 是确认键按下后打一拍比较UNLOCK 是开锁成功状态维持一定时间后自动回到 IDLELOCK 是连续错误三次后的锁死状态持续 10 秒。module controller ( input wire clk_50m, input wire rst_n, input wire key_valid, input wire [3:0] key_value, output reg lock_open, output reg error_flag, output reg [1:0] digit_pos, output reg [3:0] input_buf, input wire lock_timeout // 来自timer模块10秒计时结束信号 ); localparam IDLE 3d0; localparam INPUT 3d1; localparam CHECK 3d2; localparam UNLOCK 3d3; localparam LOCK 3d4; reg [2:0] state, next_state; reg [1:0] input_cnt; reg [3:0] pwd_reg [0:3]; // 存储用户输入 reg [3:0] pwd_default [0:3]; // 预设密码也可以parameter定义 reg [2:0] err_cnt; // 状态寄存时序块 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 次态组合逻辑 always (*) begin next_state state; case (state) IDLE: if (key_valid key_value 4hA) next_state INPUT; else if (key_valid key_value 4hC) next_state CHECK; // 直接按确认则无效 INPUT: begin if (key_valid key_value 4hC) next_state CHECK; else if (input_cnt 2d3 key_valid key_value 4hA) next_state INPUT; // 超过4位不跳转最后一位数据覆盖显示 end CHECK: next_state (lock_open) ? UNLOCK : ((err_cnt 3) ? LOCK : IDLE); UNLOCK: if (key_valid key_value 4hD) next_state IDLE; // D键手动复位 LOCK: if (lock_timeout) next_state IDLE; endcase end always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin lock_open 1b0; error_flag 1b0; err_cnt 3d0; digit_pos 2d0; input_buf 4d0; end else begin case (state) INPUT: begin if (key_valid key_value 4hA) begin pwd_reg[input_cnt] key_value; input_buf key_value; if (input_cnt 2d3) digit_pos input_cnt 1b1; end if (key_valid key_value 4hC) begin // 比对逻辑 if (pwd_reg[0]pwd_default[0] pwd_reg[1]pwd_default[1] pwd_reg[2]pwd_default[2] pwd_reg[3]pwd_default[3]) begin lock_open 1b1; error_flag 1b0; err_cnt 3d0; end else begin lock_open 1b0; error_flag 1b1; err_cnt err_cnt 1b1; end end end LOCK: begin lock_open 1b0; error_flag 1b0; end default: begin error_flag 1b0; end endcase end end endmodule逻辑说明这个模块里有两个比较隐蔽的设计点。第一pwd_reg 的赋值和比对发生在同一个时钟周期内确认键按下时最后一次输入的数字在寄存器中已经稳定存在因为输入循环里输入第四位数字是在一个周期完成的而确认键是后续周期所以直接比较不会出现少比一位的问题。第二err_cnt 在 UNLOCK 状态才清零避免在 CHECK 状态清掉导致无法进入 LOCK。参数说明state 编码使用独热码还是格雷码这里我选的是直接二进制编码3 位宽保存 5 个状态。状态数不多的情况下二进制编码最省寄存器资源而且综合器可以自动做状态转移优化。如果你用的是 Cyclone V 之类的芯片Altera 推荐使用独热码因为 FPGA 中寄存器资源丰富而组合逻辑单元相对缺乏。不过 5 个状态的差别实在有限两种写法在最终资源占用上几乎没有区别。数码管动态扫描模块属于纯组合逻辑加少量时序逻辑这里给出端口定义和核心代码片段module seg_display ( input wire clk_50m, input wire rst_n, input wire [3:0] data_in, input wire [1:0] pos_in, output reg [5:0] seg_sel, // 6位数码管位选 output reg [7:0] seg_data // 8段数据含小数点 ); reg [15:0] cnt; reg [2:0] scan_addr; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) cnt 16d0; else cnt cnt 1b1; end assign scan_addr cnt[15:14]; always (*) begin case (data_in) 4h0: seg_data 8hC0; 4h1: seg_data 8hF9; 4h2: seg_data 8hA4; // ... 共阳极数码管段码表 default: seg_data 8hFF; endcase end endmodule逻辑说明scan_addr 由计数器最高两位产生循环选中 4 个数码管示例中只用了 4 位变量实际 6 个可以增加位宽。由于人眼视觉暂留约为 20ms每位数码管点亮时间约 1.3ms四轮扫描周期 5.2ms刷新率约 192Hz不会有闪烁感。参数说明段码表数值针对共阳极数码管如果你是共阴极则取反即可。8’hC0 是共阳极下显示 0 的编码。如果你用的是 Altera DE2-115 开发板它的数码管是共阳极黑金或正点原子的板子绝大多数是共阳极。这个细节在移植时最容易出错板级调试时你会发现数码管显示乱码多半是段码表取反问题。3.3 顶层例化与时钟分频的取舍顶层文件是连线管理器它本身不包含任何逻辑计算。每个模块在这里完成端口映射。关键的时序决策是使用什么时钟作为全局时钟。50MHz 时钟直接驱动全局时钟网络是所有模块的基准不复用任何其他分频时钟。module top_password_lock ( input wire sys_clk, input wire rst_n, input wire [3:0] col, output wire [3:0] row, output wire [1:0] led_err, output wire buzzer, output wire [5:0] sel, output wire [7:0] seg ); wire [3:0] key_val; wire key_pls; wire open_flag; wire err_flag; wire lock_timeout; debounce u_debounce ( .clk_50m (sys_clk), .rst_n (rst_n), .key_in (key_pulse_raw), .key_pulse(key_pls) ); key_scan u_scan ( .clk_50m(sys_clk), .rst_n(rst_n), .col_in(col), .row_out(row), .key_value(key_val), .key_valid(key_pls) ); controller u_ctrl ( .clk_50m(sys_clk), .rst_n(rst_n), .key_valid(key_pls), .key_value(key_val), .lock_open(open_flag), .error_flag(err_flag), .lock_timeout(lock_timeout) ); lock_timer u_timer ( .clk_50m(sys_clk), .rst_n(rst_n), .enable(lock_state), .timeout(lock_timeout) ); assign buzzer err_flag | open_flag; assign led_err {2{err_flag}}; endmodule逻辑说明顶层文件把各模块的输出直接连接到端口没有插入额外逻辑。key_pulse 信号同时被 controller 使用和数码管模块使用这个多扇出没有时序收敛风险因为它的频率极低按键事件频率而且由全局时钟同步在 FPGA 中会被自动优化走全局资源。参数说明led_err 用了位拼接运算符把错误指示复制到两个 LED 上这在资源富余的 FPGA 里并没问题。如果你要控制单独的 PWM 蜂鸣器那需要另写一个发声模块一般来说直接 assign 驱动蜂鸣器会发出连续响声不够优雅。好的做法是让 buzzer 输出一个频率约 2kHz 的方波用锁存器分频即可而不是直接拉高。到这里核心代码全部给出。下一节解决的是从“仿真通过”到“板子跑起来”之间的鸿沟——也就是仿真激励的写法和板级调试时最常遇到的坑。4. Modelsim 仿真验证与 FPGA 板级调试代码写完不仿真直接上板是 FPGA 开发里最忌讳的做法。一个时序错误在仿真里可能只需要 5 分钟就能抓出来上板后可能得花一晚上。与此同时仿真通过也不等于板子能工作综合器和布局布线后的仿真结果更接近真实但时序约束又是另一个大坑。本节把从仿真到上板的全过程讲清楚。4.1 Testbench 编写方法最容易被忽视的系统级仿真Testbench 里面最容易犯的错误是给所有的输入信号同时打上非阻塞赋值导致产生毛刺。按键输入的激励模型需要考虑机械抖动特性直接在 testbench 里用随机延时模拟。timescale 1ns/1ps module tb_password_lock; reg clk_50m; reg rst_n; wire [3:0] col; reg [3:0] col_drive; wire [3:0] row; // 时钟生成 initial clk_50m 0; always #10 clk_50m ~clk_50m; // 激励序列 initial begin rst_n 0; col_drive 4b1111; #100; rst_n 1; // 输入第一个数字 1行0被拉低列0导通 #100; // 模拟按键抖动先接触再断开再接触 col_drive 4b1110; #5; col_drive 4b1111; #3; col_drive 4b1110; #100; col_drive 4b1111; #50; // 输入第二个数字 2行0列1 col_drive 4b1101; #5; col_drive 4b1111; #3; col_drive 4b1101; #100; col_drive 4b1111; #50; // 确认键 C行2列0 col_drive 4b1110; #5; col_drive 4b1111; #3; col_drive 4b1110; #100; col_drive 4b1111; #50; #1000; $finish; end // 行驱动连接 assign col col_drive; top_password_lock dut ( .sys_clk(clk_50m), .rst_n(rst_n), .col(col), .row(row), .led_err(), .buzzer(), .sel(), .seg() ); endmodule逻辑说明testbench 中 col_drive 直接驱动顶层模块的 col 端口同时注意 row 是顶层模块的 output 类型在 testbench 中不需要也不能对它赋值。这段激励模拟了完整的键盘输入过程包含了机械抖动的毛刺脉冲用来验证 debounce 模块是否正常滤除抖动。参数说明激励的时间标度是 1ns/1ps#10 表示 10ns 半个周期因此 50MHz 的时钟周期是 20ns。按键按下的时间长度只有 100ns 左右实际板级是毫秒级但仿真中为了快速看到结果可以缩短时间Debounce 模块的 CNT_MAX 在仿真时需要临时改小比如 50 个时钟周期就能验证逻辑流程。如果坚持用 1000000仿真会耗时极长这一点在跑仿真前一定要记得修改参数。4.2 仿真波形观察的 3 个关键断言点Modelsim 中把 testbench 添加到工程在 Wave 窗口观察信号有三个地方必须逐项确认。第一复位释放后状态机停在 IDLE 状态。它不会自动跳转除非有按键事件。如果复位后状态机乱跳检查 rst_n 是否同步释放或者是否存在未初始化的寄存器在 Vivado 中未初始化的 FPGA 寄存器默认接地如果你定义了 reg [2:0] state 3’bxxx综合时会给你直接拉低与你期望的状态冲突。第二确认键按下后的下一个时钟沿lock_open 信号是否变为高电平。如果比对逻辑正确锁存应该在确认键脉冲出现后的一个时钟周期内完成在这个周期中 controller 会从 CHECK 状态跳转。如果比对正确而 lock_open 没有拉高大概率是 pwd_reg 比较时少了一个时钟周期的有效数据因为 pwd_reg 的最后一位和确认键可能位于同一时刻注意检查输入计数器的时序。第三错误次数达到 3 时状态机进入 LOCK 而不是回到 IDLE。在 LOCK 状态下再按键key_valid 信号会被 controller 忽略你需要观察 key_valid 是否还能触发新的事件。如果不能问题可能出在 debounce 模块的复位逻辑没有清掉 key_dly1导致持续产生下降沿脉冲让状态机误以为按键没有释放。4.3 上板调试引脚约束、Vivado 综合与波形验证仿真通过后需要生成比特流在上板前先在约束文件 XDC 里完成所有引脚分配。这是一个勤快活但也最容易出低级错误。set_property PACKAGE_PIN AF14 [get_ports {col[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {col[0]}] set_property PACKAGE_PIN AE14 [get_ports {col[1]}] # 其他引脚同理 set_property PACKAGE_PIN N18 [get_ports sel[0]] # 数码管段选罗列全部约束会很冗长这里只展示列输入和数码管位选的写法。实际项目里 row_out、seg_data、led 等都要逐一脚引。有个提速技巧从开发板原理图里把引脚定义复制到 Excel 后用脚本生成 XDC比手打快得多。引脚约束做完后综合运行 Implementation打开 Timing Summary。电子密码锁的绝大多数路径都能满足时序要求唯一的危险路径是 debounce 模块中用于计数的进位链。如果时序报告里出现 setup time violation优先检查是否在 debounce 模块中让计数器嵌入了比较器一个 CNT_MAX 比较器如果位宽是 20 位加法器和比较器会消耗较多的组合逻辑级数。解决方案是将计数器拆成两个 10 位计数器级联或者是直接用仅仅一个计数器来判定是否到达最大值使用 if (cnt 20’hFFFFF) 而不是 if (cnt 20’hF423F)这样节省一个比较器。上板调试时最好把 led_err 和 buzzer 信号接到示波器或者逻辑分析仪上通过触发条件在确认键按下时捕捉波形。如果逻辑分析仪不方便Vivado 的 ILA IP 也可以直接在线抓取信号不过使用 ILA 会额外占用 FPGA 内部逻辑资源且对毕业设计而言通常在 LabVIEW 或逻辑分析仪环境下更直接。在板级调试中最常见的一个现象是按键按下去数码管乱跳。这通常不是扫描模块的问题而是矩阵键盘的列输入在上拉电阻不足的情况下引脚处于悬浮状态。此时需要在键盘排线连接器上外接 10kΩ 上拉电阻排或者直接在 XDC 中把引脚内部上拉开启。set_property PULLUP true [get_ports {col[*]}]上面这一句会在引脚内部使能弱上拉对解决列输入悬空导致的乱码非常有效。很多入门开发者忽略这个细节导致明明仿真很完美到板子上跑却无法按键识别。5. 复用边沿检测与看门狗定时的几个进阶技巧前四节的内容已经完全能满足毕业设计的要求但如果想让项目有更完整的工程意识和更高的仿真覆盖率还有几个点值得深入处理。这一节聚焦在三个不增加硬件成本但显著提升代码健壮性的技巧上。5.1 边沿检测的统一封装键盘扫描和消抖模块里频繁使用下降沿捕获其实在锁存器模块、定时器模块中也会反复用到。与其每次都写一个 reg 缓存变量不如直接封装成一个函数或任务。在 Verilog 中通常建议封装成独立的小模块命名为 edge_detect通过参数控制是上升沿还是下降沿触发。module edge_detect #( parameter FALLING 1 )( input wire clk, input wire rst_n, input wire signal_in, output wire pulse_out ); reg signal_dly; always (posedge clk or negedge rst_n) begin if (!rst_n) signal_dly 0; else signal_dly signal_in; end assign pulse_out FALLING ? (signal_dly ~signal_in) : (~signal_dly signal_in); endmodule逻辑说明FALLING 参数为 1 时输出信号下降沿的脉冲为 0 时输出上升沿。这个模块的好处是可以直接用 generate for 或者数组例化多路信号边沿检测大量简化代码。5.2 密码存储的时序保护在 controller 模块中密码比对使用的 pwd_reg 是寄存器类型。如果内部没有设定默认密码每次上电都是随机值这会导致用户无法预知当前密码。大多数设计中会用一个 case 语句初始化但 Verilog 中 initialize at power-up 不可综合。正确做法是在复位时单独给 pwd_default 赋值。always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin pwd_default[0] 4d1; pwd_default[1] 4d2; pwd_default[2] 4d3; pwd_default[3] 4d4; end end在 FPGA 中复位信号释放时这些寄存器会得到确定初值。有些开发者会在综合时不小心加了(* preserve *)或(* keep true *)属性这会把寄存器引到不触发复位的专用路径上导致复位失效密码永远无法匹配。排查此类问题时查看综合后的 schematic 中 pwd_default 是否连接复位信号即可。5.3 看门狗定时器的引入价值原题可以通过连续错误三次锁定 10 秒来满足安全性要求但这个设计依赖 controller 状态机自身记录错误次数。如果增加一个独立看门狗定时器当用户 30 秒内没有任何有效输入时自动清空已输密码和计数位就能够防止不完整密码长期占用输入状态。这个模块非常简洁module watchdog #( parameter TIMEOUT_CNT 3_000_000_000 // 30ns * 30亿 30秒需调整 )( input wire clk_50m, input wire rst_n, input wire key_activity, output reg reset_input ); reg [31:0] idle_cnt; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin idle_cnt 32d0; reset_input 1b0; end else if (key_activity) begin idle_cnt 32d0; reset_input 1b0; end else if (idle_cnt 32d1_500_000_000) begin reset_input 1b1; idle_cnt 32d0; end else begin idle_cnt idle_cnt 1b1; reset_input 1b0; end end endmodule逻辑说明key_activity 接 controller 输出的 key_valid每次按键都会清零计数器。当 idle_cnt 达到 15 亿对应 30 秒时输出一个高电平脉冲 reset_input这个信号接入 controller 后作为同步清零信号使用。参数说明idle_cnt 的位宽是 32 位最大计数 42 亿对于 50MHz 时钟下 30 秒的 15 亿计数来说刚好够用。如果系统时钟频率不同这个值需要重新折算。例如 100MHz 系统时钟下对应 60 秒的计数变成了 60 亿就会超出 32 位上限需要把位宽扩到 33 位。防呆优化与验证方法在板级验证看门狗时可以先输入一位密码然后等待超过 30 秒确认数码管位数指示清零。如果期待时间太长可以使用仿真缩短时间参数。此外在 UNLOCK 状态下看门狗应该被禁用或者在输出 reset_input 时不影响 UNLOCK 状态的保持这是另一个边界情况可以在 controller 中判断状态再做响应。本文还有配套的精品资源点击获取
网站建设高端定制企业官网