FPGA密码锁程序从零到上板:Verilog状态机与按键消抖完整实战
2026/9/16 16:50:41 网站建设 项目流程

简介:面向FPGA入门与进阶学习者的密码锁实践工程,基于Altera Quartus II开发,采用VHDL描述数字逻辑,适合课程设计、实验教学或自学硬件描述语言的读者参考。项目把按键输入、密码预设与存储、逐位比较、错误计数报警以及密码更改等模块落到可综合代码中,呈现了从功能定义到硬件实现的完整思路。压缩包共83个文件,约515KB,以vhd源码、qpf与qsf工程配置、vwf仿真波形、pof编程文件以及rpt、summary和多种编译数据库为主,覆盖设计、仿真、综合、适配与下载各阶段。已有863人学习,读者可据此理解4×4键盘扫描时序、密码比较与错误阈值报警机制,结合波形图和编译报告排查引脚约束与时序问题,掌握Quartus II工程组织与VHDL调试方法;源码按功能模块组织,便于按需查阅与二次开发,对课程设计与自学均较友好。

1. 为什么密码锁程序是FPGA入门最该完整走一遍的项目

FPGA 密码锁程序这个标题听起来像课程设计,但它牵出的东西比本身值钱得多:按键消抖、矩阵扫描、状态机、定时器、比对逻辑、仿真验证、引脚约束、烧录调试,正好是 FPGA 开发的完整闭环,用 Quartus 从零到上板跑通一遍,后面做串口解析、倒车雷达、协议控制都是在复制这套结构。我见过不少让 FPGA 做密码锁的方案,有纯组合逻辑硬拼的,有拿单片机思路写个超长 always 块的,这些都能亮灯,但仿真和上板对不上,问题几乎都出在输入侧没有做时序处理,或者状态机把“比较”和“状态”揉在一起。这篇就按我实际做这个项目的顺序写:先解决按键怎么进得来,再把核心状态机写稳,然后说清楚 Quartus 从建工程到仿真、约束、烧录的操作细节,最后给上板后的验证手段。适合刚学完 Verilog 语法想做第一个完整项目的人,也适合有经验的工程师当作可复用的外设模板。

2. 按键输入侧:4x4矩阵扫描与Verilog消抖的必调参数

2.1 机械抖动和亚稳态是密码锁的第一道坎

绝大多数密码锁程序第一次上板翻车,不是状态机写错,而是按键输入没处理好。轻触开关的簧片从接触到稳定,电平会在 5~20ms 内来回跳变,对 50MHz 时钟来说这 20ms 里能采到上百万个边沿。如果不做处理,一次物理按下会被逻辑层理解成好几次,密码位数直接错乱,表现为“按一下 1 进来三个 1”。

更隐蔽的是亚稳态。按键信号的变化恰好落在时钟沿附近时,触发器输出会在一段时间内既不是 0 也不是 1,这个不稳定值如果被下一级直接采走,你会看到完全不符合按键矩阵逻辑的键值,比如按 1 进来的是 3。处理方法是固定的:先用两级触发器同步,再用计数器做连续多次采样确认。两级同步把亚稳态收敛到确定电平,计数确认把机械抖动过滤掉。这两步做完,按键才能安全地进入状态机。

2.2 4x4矩阵键盘扫描与键值译码模块

开发板上常见的是 4x4 矩阵键盘,8 根线解决 16 个按键:4 根列线做输出,4 根行线做输入。扫描原理是让一列输出低电平,其余列输出高电平,然后读行线,哪一行读到低,就说明这个交点的按键被按下。扫描每 1ms 换一列,完整扫一轮 4ms,人手指按一个键至少保持几十毫秒,时间上完全来得及。

// keypad_scan.v — 4x4矩阵键盘扫描 module keypad_scan #( parameter CLK_HZ = 50_000_000 )( input wire clk, input wire rst_n, output reg [3:0] col, // 列驱动, 低电平扫描 input wire [3:0] row, // 行回读, 低电平表示按下 output reg [3:0] key, // 键值 0~F output reg valid // 有键按下时高电平 ); reg [15:0] cnt; reg [1:0] col_idx; wire scan_tick = (cnt == CLK_HZ/1000 - 1); // 1ms 扫描节拍 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 16'd0; col_idx <= 2'd0; end else if (scan_tick) begin cnt <= 16'd0; col_idx <= col_idx + 2'd1; // 换下一列 end else begin cnt <= cnt + 16'd1; end end always @(*) begin case (col_idx) 2'd0: col = 4'b1110; // 第0列拉低 2'd1: col = 4'b1101; 2'd2: col = 4'b1011; default: col = 4'b0111; endcase end always @(*) begin key = 4'hF; // 无按键时返回F valid = 1'b0; case ({col_idx, row}) 6'b00_1110: begin key = 4'h1; valid = 1'b1; end 6'b00_1101: begin key = 4'h2; valid = 1'b1; end 6'b00_1011: begin key = 4'h3; valid = 1'b1; end 6'b00_0111: begin key = 4'hA; valid = 1'b1; end 6'b01_1110: begin key = 4'h4; valid = 1'b1; end 6'b01_1101: begin key = 4'h5; valid = 1'b1; end 6'b01_1011: begin key = 4'h6; valid = 1'b1; end 6'b01_0111: begin key = 4'hB; valid = 1'b1; end 6'b10_1110: begin key = 4'h7; valid = 1'b1; end 6'b10_1101: begin key = 4'h8; valid = 1'b1; end 6'b10_1011: begin key = 4'h9; valid = 1'b1; end 6'b10_0111: begin key = 4'hC; valid = 1'b1; end 6'b11_1110: begin key = 4'hE; valid = 1'b1; end 6'b11_1101: begin key = 4'h0; valid = 1'b1; end 6'b11_1011: begin key = 4'hD; valid = 1'b1; end default: begin key = 4'hF; valid = 1'b0; end endcase end endmodule

注意{col_idx, row}的 6 位拼接是组合逻辑译码,某个按键在哪个行列取决于开发板原理图,上板前要对着板子丝印和管脚映射表核对。key的默认值设成4'hF是为了让“无按键”成为一个可过滤状态,后面状态机里遇到 F 就直接忽略。扫描节拍CLK_HZ/1000是分频计数值,不是把时钟真正切成 1kHz 方波,这样避免在 FPGA 里产生除主时钟以外的第二时钟域,综合和时序约束都更干净。

2.3 两级同步加计数确认的消抖模块

扫描给了键值,但还没解决抖动。行线的抖动在扫描周期内一样存在,正确的做法是把每个行输入信号做两级同步加计数消抖,消抖后的电平才进入译码。这里给出一个可直接复用的消抖模块,采用单时钟加采样使能的结构:

// key_debounce.v — 两级同步 + 计数确认 module key_debounce #( parameter STABLE_CNT = 20 // 连续采样确认次数 )( input wire clk, input wire rst_n, input wire sample_en, // 1kHz 采样使能脉冲 input wire key_in, // 按键电平, 按下为低 output reg key_level, // 消抖后的稳定电平 output reg key_press // 按下瞬间的单周期脉冲 ); reg [5:0] cnt; // 计数到20, 6位足够 reg in_d1, in_d2; // 两级同步触发器 reg level_prev; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin in_d1 <= 1'b1; in_d2 <= 1'b1; key_level <= 1'b1; // 复位为高 = 未按下 cnt <= 6'd0; end else if (sample_en) begin in_d1 <= key_in; in_d2 <= in_d1; // 同步后的按键电平 if (in_d2 == key_level) cnt <= 6'd0; // 与当前稳定值一致, 计数清零 else if (cnt == STABLE_CNT - 1) begin key_level <= in_d2; // 连续20次采样相同才翻转 cnt <= 6'd0; end else begin cnt <= cnt + 6'd1; end end end always @(posedge clk or negedge rst_n) begin if (!rst_n) level_prev <= 1'b1; else if (sample_en) level_prev <= key_level; end assign key_press = level_prev & ~key_level; // 高到低跳变 endmodule

采样使能的产生很简单,主时钟分频一个 1ms 节拍即可:计数器从 0 数到CLK_HZ/1000 - 1时输出一个高电平脉冲。这个模块有三个参数直接影响手感,列出来方便调整:

参数推荐值作用调整方向
采样使能周期1ms决定采样间隔间隔越小越灵敏,消抖窗口 = 周期 × STABLE_CNT
STABLE_CNT20连续相同次数机械按键 20 次即 20ms;薄膜按键可降到 5~8
同步级数2 级消除亚稳态时钟高于 100MHz 建议加一级成 3 级

key_press输出的是单周期脉冲,只在检测到高电平变低电平的那一个时钟沿出现一次。状态机只认这个脉冲,就不会出现“按住一个键被连续采样”的问题。可以看到这里避免了一个常见误用:用always里的条件去判断按键值而不是判断脉冲,结果一次按键变成了多次触发。

2.4 扫描输出与消抖的接法:先消抖还是先扫描

这个顺序容易踩坑。我一般建议对每个行信号单独调用key_debounce实例,8 路线里行线接了弱上拉,列线本来由扫描驱动,不存在按键抖动问题。每个行线消抖后再进keypad_scan的译码,译码结果valid天然就是稳定的。另一种常见做法是先扫描得到valid,再对valid做一次边沿检测,好处是例化少,但按下和释放瞬间的临界片段如果恰好被扫描捕捉到,仍然可能漏键或重键。开发板资源紧张时用这种简化的接法可以,工整起见还是每根行线一个消抖实例,Cyclone IV 的资源量对 4 个消抖模块完全没有压力。

提示:行线输入务必在引脚约束里打开弱上拉,否则悬空时电平不确定,消抖模块会随机翻转,表现为开机后自动输入密码。

3. 密码锁核心:FSM状态机与4位密码逐位比对

3.1 五个状态的定义与转移条件

密码锁的行为可以归纳成五个状态:等待输入、接收密码、比对、解锁保持、错误锁定。我见过有人把比对结果写进组合逻辑然后直接驱动 LED,看起来简单,但“错误次数累计”和“锁定倒计时”没有地方放,后续加功能必然重写。状态机才是这个项目该有的结构。

状态进入条件离开条件输出表现
IDLE复位完成或比对失败后重置收到第一个有效数字键全部 LED 灭
INPUT收到数字键输满 4 位或者按确认键数码管显示已输入位数
CHECK输入完成比对结束蜂鸣器短暂提示
OPEN比对通过保持时间到绿灯常亮、继电器吸合
LOCK错误次数达到上限30 秒倒计时结束红灯常亮、蜂鸣器报警

把 CHECK 单独设为一个状态,而不是让 IDLE 或 INPUT 直接判断结果,好处是错误计数和转移条件集中在同一段case里,仿真时波形上一眼能看到比对发生的位置。OPEN 和 LOCK 都靠同一个定时器计数,只是到期时间不同,这也是状态机的常见优化:定时资源尽量复用。

3.2 FSM主模块代码与逐位比对

下面这段是顶层密码锁状态机的核心代码,密码默认存成 1-2-3-4,按键值来自前文扫描译码后的key_valuekey_valid必须是单周期脉冲:

// top_pwd_lock.v — 密码锁状态机主模块 module top_pwd_lock #( parameter CLK_HZ = 50_000_000, parameter MAX_WRONG = 3, // 允许错误次数 parameter LOCK_SEC = 30 // 锁定秒数 )( input wire clk, input wire rst_n, input wire [3:0] key_value, // 键值 0~F input wire key_valid, // 单周期有效脉冲 output wire led_open, output wire led_lock, output wire buzzer ); localparam S_IDLE = 3'd0; localparam S_INPUT = 3'd1; localparam S_CHECK = 3'd2; localparam S_OPEN = 3'd3; localparam S_LOCK = 3'd4; reg [2:0] state; reg [3:0] pwd_in [0:3]; // 本次输入缓冲 reg [3:0] pwd_set [0:3]; // 存储密码 reg [1:0] idx; // 输入位数索引 reg [3:0] wrong_cnt; reg [31:0] timer; integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pwd_set[0] <= 4'd1; pwd_set[1] <= 4'd2; pwd_set[2] <= 4'd3; pwd_set[3] <= 4'd4; end end // 纯组合比较, 不依赖状态 wire match = (pwd_in[0]==pwd_set[0]) && (pwd_in[1]==pwd_set[1]) && (pwd_in[2]==pwd_set[2]) && (pwd_in[3]==pwd_set[3]); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; idx <= 2'd0; wrong_cnt <= 4'd0; end else begin case (state) S_IDLE: begin if (key_valid && key_value != 4'hF) begin pwd_in[0] <= key_value; idx <= 2'd1; state <= S_INPUT; end end S_INPUT: begin if (key_valid) begin if (key_value == 4'hC) begin // C 键确认 state <= S_CHECK; end else if (key_value == 4'hB) begin // B 键清空 idx <= 2'd0; state <= S_IDLE; end else if (key_value != 4'hF) begin pwd_in[idx] <= key_value; if (idx == 2'd3) // 第4位输入完毕 state <= S_CHECK; else idx <= idx + 2'd1; end end end S_CHECK: begin if (match) begin state <= S_OPEN; wrong_cnt <= 4'd0; end else begin wrong_cnt <= wrong_cnt + 4'd1; if (wrong_cnt + 4'd1 >= MAX_WRONG) state <= S_LOCK; // 错误满3次锁定 else state <= S_IDLE; end end S_OPEN: begin if (timer >= CLK_HZ) // 保持1秒 state <= S_IDLE; end S_LOCK: begin if (timer >= LOCK_SEC * CLK_HZ) // 锁定到时间 state <= S_IDLE; end endcase end end always @(posedge clk or negedge rst_n) begin if (!rst_n) timer <= 32'd0; else if (state == S_OPEN || state == S_LOCK) timer <= timer + 32'd1; else timer <= 32'd0; end assign led_open = (state == S_OPEN); assign led_lock = (state == S_LOCK); assign buzzer = (state == S_CHECK) && !match; endmodule

几个关键点。pwd_inpwd_set是 reg 数组,对应 Verilog 里的存储器语义,综合后会被映射成寄存器堆,比用 4 个单独的 reg 拼接可读性好。match是纯组合比较,不依赖 state,这样它既可以在 CHECK 状态里当跳转条件,也可以直接驱动蜂鸣器,避免重复写一遍比较逻辑。错误计数wrong_cntwrong_cnt + 4'd1 >= MAX_WRONG判断,是因为进入 CHECK 状态时wrong_cnt还是旧的计数值,必须先加再比,否则第三次错误会延后一个周期才锁定。如果按确认键时输入不满 4 位,缓冲里的残留值会参与比较,这属于设计取舍,实际产品里可以在 S_INPUT 里加一个idx == 2'd3的前置判断,让未满 4 位按 C 无效。

3.3 锁定倒计时:计数器位宽是隐藏雷区

定时器的位宽是密码锁项目里最容易翻车的细节。LOCK_SEC * CLK_HZ在 50MHz 时钟下是 30 × 5000 万 = 15 亿,这个数用二进制表示需要 31 位,因为 2 的 30 次方只有 10.7 亿。很多入门教程喜欢写reg [24:0] counter,在这个场景里计数器到 3355 万就会回绕,大约 0.67 秒,表现为“锁定状态自己跳回 IDLE”,锁了个寂寞。

计数目标数值所需位宽
1 秒 @50MHz5×10^726 位
30 秒 @50MHz1.5×10^931 位
60 秒 @50MHz3×10^932 位

工程上的习惯是位宽统一给 32 位,不够了再扩,不会为省几百个 LUT 去精算。逻辑综合工具通常会对未定义的截断报 Warning,别只当没看见,把LOCK_SEC * CLK_HZ的最大值代入$clog2算一次,心里就有底了。仿真时把LOCK_SEC覆写成 1,上板再改回 30,这条经验在后面 Quartus 流程里还会用到。

4. Quartus II 编译仿真与引脚约束的完整操作

4.1 新建工程时的器件和EDA工具设置

无论你用 Quartus Prime Lite 还是 Quartus II 13.1,工程创建流程一样。New Project Wizard 里要特别注意三点:工程目录和工程名必须全英文、不要带空格;器件型号按开发板实际芯片选,Cyclone IV 还是 MAX 10 都行,不用刻意挑贵的;最后一个页面 EDA Tool Settings 里把 Simulation 选成 ModelSim-Altera,语言设为 Verilog。这一步不做,后面想启动功能仿真时会发现 NativeLink 是灰的。

Quartus II 13.1 自带配套的 ModelSim-Altera 10.1d,安装时会提示是否装 ModelSim,很多人一跳过后面就启动不了。Quartus Prime Lite 18.1 以后的版本不带 ModelSim 了,分两条路:要么单独装 Modelsim Intel FPGA Edition,要么直接用 Prime 自带的 QSIM。QSIM 对几百行的密码锁项目完全够用,波形界面朴素一点而已。我一般先新建工程,接着写 RTL,然后在 Assignments → Settings → EDA Tool Settings → Simulation 里确认 Tool name 和 Testbench 编译路径,避免编译到一半才发现选错了工具链。

4.2 Testbench里模拟“按下1234”的波形验证

功能仿真的核心是让状态机快速跑通“输入正确密码→解锁”和“输入错误密码→锁定”两条路径。写 Testbench 时用 task 封装按键动作是标准做法,避免在每个时序点重复写key_valid的拉高拉低:

`timescale 1ns/1ps module tb_pwd_lock; reg clk = 0, rst_n = 0; reg [3:0] key_value; reg key_valid; wire led_open, led_lock, buzzer; always #10 clk = ~clk; // 50MHz // 仿真时把 CLK_HZ 覆写成 1000, 让1秒保持变成20us仿真时间 top_pwd_lock #(.CLK_HZ(1000)) dut ( .clk(clk), .rst_n(rst_n), .key_value(key_value), .key_valid(key_valid), .led_open(led_open), .led_lock(led_lock), .buzzer(buzzer) ); task press_key(input [3:0] k); begin key_value = k; @(posedge clk); key_valid = 1'b1; @(posedge clk); key_valid = 1'b0; repeat(10) @(posedge clk); // 间隔10个时钟模拟手指抬起 end endtask initial begin rst_n = 0; repeat(5) @(posedge clk); rst_n = 1; press_key(4'd1); // 正确密码 1 2 3 4 press_key(4'd2); press_key(4'd3); press_key(4'd4); // 观察 led_open 拉高一个周期, 随后回到 IDLE repeat(2000) @(posedge clk); press_key(4'd1); // 错误密码 1 2 3 5 press_key(4'd2); press_key(4'd3); press_key(4'd5); // 观察 led_lock 拉高, 开始 30s 倒计时 repeat(2000) @(posedge clk); $stop; end endmodule

press_key里的@(posedge clk); key_valid = 1'b1;是先等一个时钟沿再拉高,保证赋值不是写在时钟沿上,避免仿真器出现建立时间警告。关键波形看三处:第一次输完 4 位后led_open拉高,说明 match 判定正确;state从 S_CHECK 走到 S_IDLE,说明定时结束;错误密码后wrong_cnt从 1 递增到 3,第三次触发 S_LOCK。把CLK_HZ在仿真里覆写成 1000 是常见技巧,仿真时钟仍按#10跑 50MHz,但逻辑里的“1 秒”变成了 1000 个时钟周期,整个用例不会跑出几百万个 cycle。

4.3 ModelSim启动失败的常见原因与对应处理

搜索“quartus ii cannot launch modelsim”能看出这是高频问题,基本就是下面四种情况,排查顺序也按此来。

现象原因处理
启动仿真提示 cannot launch工程路径或系统用户名含中文把工程复制到纯英文目录,重新 Open Project
找不到 vsim 可执行文件ModelSim-Altera 没装或没配套版本Quartus II 13.1 配 10.1d;Prime Lite 用 QSIM 兜底
NativeLink 设置失效EDA Tool Settings 里没选 Tool nameAssignments → Settings → Simulation 选 ModelSim-Altera
启动后波形窗口空白32/64 位混用检查 Quartus 和 ModelSim 安装目录里的 bin 是 win32 还是 win64,保持一致

其中路径带中文是最坑的,Quartus 对 Unicode 路径支持一直不算好,报错信息还不直接。排查时分两步:先看 Assignments → Settings → EDA Tool Settings 里的 Tool name 和 Testbench 是不是都选对,再打开 ModelSim 手动跑一遍vsim work.tb_pwd_lock,能起来就说明工具链没问题,是 Quartus 的启动配置问题;起不来就先解决路径和位数。

4.4 引脚约束、弱上拉设置与烧录

编译通过只是第一步,引脚约束不对,下载到板子上一样不工作。建议直接改 .qsf 文件,比在 Pin Planner 里一格格点快,内容格式如下:

set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_L1 -to led_open set_location_assignment PIN_L2 -to led_lock set_location_assignment PIN_R8 -to col[0] set_location_assignment PIN_T8 -to col[1] set_location_assignment PIN_R9 -to col[2] set_location_assignment PIN_T9 -to col[3] set_location_assignment PIN_U9 -to row[0] set_location_assignment PIN_V9 -to row[1] set_location_assignment PIN_AB3 -to row[2] set_location_assignment PIN_AB4 -to row[3] set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to row[0] set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to row[1] set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to row[2] set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to row[3] set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to *

上面的引脚编号是示意,实际要看开发板原理图。弱上拉这句别漏,否则行线悬空时消抖模块会乱翻。IO 电平统一设成 3.3-V LVTTL 是大多数廉价开发板的默认,如果板上有电平转换芯片就按芯片手册改。全部约束后重新 Analysis & Synthesis 和 Fitter,看 Assignment 相关 Warning,没有红色错误才能进 Programmer。Cyclone IV 下载.sof到 SRAM,断电就丢,MAX 10 用.pof可以固化到内部 flash,这点在产品化时很重要。

5. 上板验证:用状态编码、SignalTap和仿真短参数抓问题

5.1 让每颗LED报告状态机的当前位置

板子上没有屏幕时,LED 就是最便宜的状态指示。把状态机的 3 位state直接引到 3 颗 LED 上,比你用数码管逐位显示密码更直观,因为你看到的是逻辑内部,而不是经过译码的间接结果。

状态state 编码LED 显示
IDLE000全灭
INPUT001输入几位就亮几盏
CHECK010全部熄灭一瞬
OPEN0113 盏全亮
LOCK100每秒快闪

按错密码到第三次时,如果 LOCK 灯闪起来了,说明错误计数和跳转正常,问题就锁定在定时器位宽或者时钟频率参数上,这时候去看timer是不是提前回绕。

5.2 SignalTap II直接观察FSM内部寄存器

LED 只能看状态跳不跳,看不到具体数值,比如wrong_cnt是 2 还是 3。Quartus 自带的 SignalTap II Logic Analyzer 可以实时抓内部信号,而且不用改 RTL。操作路径是 Tools → SignalTap II Logic Analyzer,新建一个 instance,把时钟设为clk,把statewrong_cntkey_validled_open加进信号列表,采样深度设 2K 条目,触发条件设为state == 4也就是 LOCK 状态,然后重新编译并下载。

有一点要注意:SignalTap 只能抓到综合网表里存在的信号。如果某个寄存器被优化掉了,信号列表会显示不可用,解决办法是在 RTL 里加综合属性:

(* keep *) reg [2:0] state; (* keep *) reg [3:0] wrong_cnt;

keep属性告诉综合工具保留这个寄存器,不要因为它在综合过程中看起来不会被使用就删除。抓到的波形里能清楚看到wrong_cnt递增长到 3 后state跳到 LOCK,比你在 ModelSim 里对着仿真波形猜上板行为准确得多。

5.3 把20ms消抖变成仿真短参数

上板验证到只剩消抖手感的问题时,你大概率已经受够了仿真里那 20ms。功能仿真时 20ms 意味着 100 万个时钟周期,一个用例跑下来几分钟就没了。我的做法是给顶层加一个仿真宏,让消抖计数和定时参数在仿真和综合两种配置下分别取值:

`ifdef SIM localparam STABLE_CNT = 2; // 仿真: 2ms 就认 `else localparam STABLE_CNT = 20; // 上板: 20ms 才认 `endif

在 ModelSim 里通过命令vlog +define+SIM或 Quartus 的 EDA Tool Settings 里加宏定义来启用。这样 RTL 仿真在几毫秒内就能跑完整个开锁流程,而综合时宏关闭,芯片里的消抖窗口仍然是真实的 20ms。同样的宏还可以缩短 OPEN 保持和 LOCK 倒计时,等全部用例通过再把宏删掉做一次完整综合,最后用 SignalTap 验证宏关闭后的时序行为,这一套下来密码锁程序的验证就闭环了。

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

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

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

立即咨询