ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA密码锁中的VHDL状态机设计与矩阵键盘扫描实现

FPGA密码锁中的VHDL状态机设计与矩阵键盘扫描实现 简介一份基于FPGA的密码锁程序采用Altera Quartus II配合VHDL语言实现适合FPGA入门与进阶学习者研究数字系统设计。资源压缩包约515KB共83个文件以.vhd源文件、.qpf工程文件、.qsf配置文件为主另有.rpt综合/适配/时序报告、.vwf仿真波形、.pof编程文件及.readme说明完整呈现Quartus工程从建项、编译到下载的各个阶段。项目使用4×4键盘作为输入在FPGA内部实现密码存储、逐位比较、错误次数计数与报警、以及通过旧密码验证完成修改等核心功能同时涉及按键消抖、状态机控制等典型时序逻辑设计。资源内容结构清晰便于对照代码与报告理解VHDL硬件描述方法、Quartus II调试流程以及外设接口设计。已有863人学习对于想动手实践FPGA密码锁或类似按键交互系统的开发者具有较好的参考价值。1. 套件里藏着的密码锁一次VHDL状态机拆解拿到这套Quartus工程先别急着编译——mimasuo.vhd、jianpan.vhd、yima.vhd、xiaodou.vhd、seg_sweep0.vhd这五个源文件刚好对应了密码锁的五个核心模块主控逻辑、键盘扫描、译码、按键消抖和数码管段选扫描。这种按功能拆分文件的结构本身就是一次值得抄的模块化设计示范。常见的密码锁Demo大多只做“输入几位密码、比对、开锁”的直通逻辑而这套工程把键盘扫描、动态扫描和主状态机分开写意味着你可以单独仿真任何一个子模块而不需要搭整个系统——这对调试来说价值比“能跑”大得多。核心逻辑藏在mimasuo.vhd里它用VHDL描述了一个典型的状态机空闲等待、输入密码、逐位比较、错误计数、报警、改密确认。这里值得注意的不是“实现了密码锁”这件事而是它怎么用有限个状态去管理“多位输入 错误累计 修改密码”三种并发需求。下面先把状态机和键盘模块的配合逻辑说透再给出一套可以直接改用的代码结构。2. VHDL状态机设计从真值表到密码锁主控逻辑2.1 为什么用状态机而不是“顺序执行”的思路VHDL不同于C语言没有“先读键盘、再比较、再判断”这种天然的执行顺序。你要做的是把系统的所有可能处境枚举出来然后用状态寄存器显式地表达当前处于哪个阶段。密码锁的核心困境在于输入密码是一个时序过程密码比较是组合逻辑报警计数又要跨多个时钟周期——这三者如果用一堆if嵌套去写综合后的电路会变得不可预测时序约束也没法给。状态机的好处是让每个阶段的输入条件、输出动作、跳转条件都摊在台面上综合器能明确推断出寄存器的翻转条件布线后的时序也比较容易收敛。以这套工程为例主状态机的状态至少包含S_IDLE等待第一次按键、S_INPUT接收8位密码输入、S_CHECK比对密码、S_ALARM连续错误触发报警、S_CHANGE验证旧密码后允许输入新密码。状态编码用的是独热码Quartus默认会帮你选编码方式但如果设备资源紧张可以在State信号上显式声明独热码以减少组合逻辑的扇出。下面是一段从mimasuo.vhd主干逻辑提炼出的状态跳转骨架type state_type is (S_IDLE, S_INPUT, S_CHECK, S_ALARM, S_CHANGE); signal state : state_type : S_IDLE; signal key_buf : std_logic_vector(7 downto 0); signal cur_pwd : std_logic_vector(7 downto 0) : 00010010; -- 预设密码 signal key_count : integer range 0 to 7 : 0; signal err_count : integer range 0 to 3 : 0; signal alarm_sig : std_logic;这段代码定义了一个单进程状态机最核心的骨架。key_buf用来缓存当前输入的按键值key_count指示已经输入了几位cur_pwd是预设密码——注意这里直接用信号量做密码存储对于教学级项目完全够用但如果你想防止综合器把密码优化成常量导致改密功能失效可以在综合属性里加上keep约束。err_count每个错误周期加1达到3次后进入S_ALARM。2.2 密码比较逻辑的两种写法与边界问题密码比较看起来简单——每一位输入和预设值比对就行——但这里有个陷阱如果每按一位就立刻比较那么输入到第3位时前面两位已经被比对通过了这时若第3位错误逻辑必须能定位到“错在哪一位”同时不能让之前的状态残留影响下一次输入。我见过不少学生作品在这个环节出问题错误标记没有及时清除导致下一次输入即使全对也永远无法开锁。常见做法是用一个移位寄存器把按键值逐位推入然后跟预设值做整体比较process(clk) begin if rising_edge(clk) then if key_pulse 1 and key_count 8 then key_buf key_buf(6 downto 0) key_in; key_count key_count 1; end if; end if; end process; process(key_buf, cur_pwd) begin match_flag 0; if key_buf cur_pwd then match_flag 1; end if; end process;第一段是时序逻辑负责在按键脉冲到来时把当前按键值拼接到移位寄存器尾部第二段是纯组合逻辑持续比较移位寄存器的内容与预设密码。用这种写法你要对齐的是“第几个时钟沿按键值被采入”而不是“按键按下的瞬间是否等于某位密码”——这能避免按键释放与边沿检测之间差一个周期导致漏判。注意key_buf(6 downto 0) key_in的方向如果你是从高位开始移入那么最终比较时要确认预设密码的位序和移入方向一致否则会出现“输入12345却被当成54321”的经典错位。边界情况是如果用户输入了9位数字第9位到达时key_count已经是8那个key_count 8条件会阻止第9位进入移位寄存器但这个按键事件本身已经被消耗掉了——也就是说用户多按的这第9位不会被计入下一次输入。工程里的处理方式是每个完整周期后自动回到S_IDLE或S_INPUT但具体怎么回取决于状态机在key_count 7时是否直接跳转到S_CHECK。3. 4×4矩阵键盘扫描与VHDL消抖实现3.1 矩阵扫描的时序拆解行扫描、列回读、边沿检测在后续内容中继续展开矩阵键盘省引脚的本质是“分时复用”——4根行线逐行拉低4根列线轮询回读16个按键只需要8个引脚。但FPGA引脚状态在未按下时是悬空的这对CMOS输入结构来说是致命的必须用内部上拉电阻。Quartus的Pin Planner中可以直接给引脚分配Weak Pull-Up属性但更可控的做法是在mimasuo.qsf文件里手写set_location_assignment PIN_AB7 -to row[0] set_location_assignment PIN_AA9 -to row[1] set_location_assignment PIN_AB9 -to row[2] set_location_assignment PIN_AA11 -to row[3] set_location_assignment PIN_AC12 -to col[0] set_location_assignment PIN_AB12 -to col[1] set_location_assignment PIN_AC9 -to col[2] set_location_assignment PIN_AB8 -to col[3]引脚分配后扫描时序通常是四个节拍为一轮每个节拍内只拉低一行读四列然后换下一行。这个循环频率不能太快——按键按下的物理抖动通常在5~20ms内如果扫描频率高于抖动频率同一个按键会被识别成多次触发。常用做法是每行扫描周期设在1~2ms左右四行一轮扫描周期约8ms这个节奏恰好能避开大部分机械抖动窗口。扫描逻辑的VHDL实现从jianpan.vhd中的设计思路整理process(clk_1k) begin if rising_edge(clk_1k) then case scan_cnt is when 0 row 1110; col_read col; when 1 row 1101; col_read col; when 2 row 1011; col_read col; when 3 row 0111; col_read col; when others scan_cnt 0; end case; end if; end process;clk_1k通常由开发板上的50MHz时钟分频而来分频系数50000得到1kHz的扫描基频。row输出低电平驱动行线col信号实时回读四位列线的电平状态。case when 0/1/2/3四个节拍覆盖了四根行线每个节拍读一次列线共16个组合。判键就在回读的瞬间发生——当某一行拉低时某一列读到低电平说明该行列交叉点的按键被按下。3.2 消抖的工程取舍延时计数器与边沿捕获在后续内容中继续展开按键消抖是FPGA入门项目里最容易忽略但影响体验最大的细节。机械开关的抖动模式不是固定的按键按下瞬间触点反弹会产生一系列亚毫秒级的高频脉冲这会让边沿检测电路误判成多次按键。大部分教学demo用一段固定延时如10ms来跨越抖动窗口但这段延迟同时意味着你的系统对“快速连续按键”的响应上限被锁死了——用户必须在10ms窗口后才能按第二个键。从这套工程的xiaodou.vhd来看它用了计数延时消抖的思路检测到电平跳变后启动一个计数器计数到预设阈值由时钟频率决定消抖时间时再次读取按键电平如果和跳变后的电平一致认定按键稳定。这个消抖时间窗口一般设在10~20ms比如50MHz时钟下计数50万次就是10ms。下面是一段可直接复用的消抖模板signal key_raw : std_logic; signal key_deb : std_logic; signal cnt_deb : integer range 0 to 500000 : 0; process(clk) begin if rising_edge(clk) then if key_raw key_deb then cnt_deb 0; else if cnt_deb 500000 then key_deb key_raw; cnt_deb 0; else cnt_deb cnt_deb 1; end if; end if; end if; end process;这段逻辑里key_raw是引脚直接采到的电平key_deb是消抖后的稳定电平。cnt_deb的计数上限500000对应50MHz时钟下的10ms延迟数值要是换成25MHz时钟计数上限就要减半到250000——这个换算关系务必记住很多人移植代码到别的板子后按键失灵就是没改这个数。另外一个关键点是key_deb要用于后续边沿检测不推荐直接用key_deb做电平触发因为如果密码输入期间用户一直按着键电平触发会让移位寄存器反复工作。正确做法是检测key_deb从高到低的跳变沿生成一个单脉冲key_pulse。3.3 译码模块把行列坐标映射到按键值yima.vhd负责把行列坐标翻译成实际的数字。最常见的映射是4×4键盘的16个键位分别对应十六进制数字0~F密码锁场景里通常只启用0~9这十个键。译码的核心是一个case语句——行列四位数据组合成8位std_logic_vector按照你的硬件接线顺序查出对应键值process(row, col) variable key_row : integer; variable key_col : integer; begin case row col is when 11100001 key_out 0000; -- 0 when 11100010 key_out 0001; -- 1 when 11010010 key_out 0010; -- 2 -- ... 按实际接线对应 when others key_out 1111; -- 无效键 end case; end process;这里有个一致性陷阱如果键盘是共阳/共阴结构或者模块板卡上的丝印方向不同行与列的组合可能与上表不一致。调试时不要想当然——用万用表量出每个按键按下时是哪根行线与哪根列线短接再反推case分支。key_out 1111作为无效键的兜底值能避免未定义状态导致的译码毛刺这个习惯值得保留。4. 密码锁整体仿真与Quartus时序验证4.1 用Waveform.vwf搭一套完整仿真激励在后续内容中继续展开仿真才是这套工程最有分量的部分。压缩包里的Waveform.vwf和Waveform1.vwf是Quartus II的波形激励文件对应不同测试场景——前者大概率验证基础按键输入到开锁的路径后者可能是带错误次数触顶后报警的路径。如果你刚接触Quartus可以直接打开这两个文件看激励是怎么建立的时钟周期设多少、按键脉冲插在哪个时间点、复位信号的释放时机。自己动手搭激励时建议用一段模板化的VHDL testbench而不是完全依赖波形编辑器。原因在于波形编辑器的一次次手动拖拽难以复现而testbench可以用循环语句批量生成按键序列。下面是一个针对主控模块的仿真激励骨架entity tb_mimasuo is end; architecture sim of tb_mimasuo is signal clk : std_logic : 0; signal rst : std_logic : 0; signal key_pulse : std_logic : 0; signal key_in : std_logic_vector(3 downto 0) : 0000; signal lock_open : std_logic; begin clk not clk after 10 ns; -- 50MHz process begin rst 1; wait for 100 ns; rst 0; -- 输入“12345678” for i in 0 to 7 loop key_in key_values(i); key_pulse 1; wait for 20 ns; key_pulse 0; wait for 200 ns; end loop; wait; end process; end sim;wait for 200 ns的意义是模拟两次按键之间的间隔现实中人的按键间隔远大于20ns仿真给20~200ns只是为了快速跑完——真正仿真时你的按键脉冲宽度要参考消抖模块的时钟分频否则消抖会把仿真中的按键脉冲统统滤掉导致testbench里明明有按键输入而密码锁毫无反应。这是初学者最容易掉的坑仿真和实板的时钟频率不一致导致时延相关逻辑的行为天差地别。4.2 从仿真波形反推状态时序仿真的好处在你能同时看到内部信号和外部输出。Quartus的Waveform Editor里可以添加内部节点观察——右键插入mimasuo_inst|state这类层次化信号名就能看状态寄存器跳变。如果你加了信号却看不到检查mimasuo.qsf里是否设置了global assignment部分版本需要在Settings - Compiler Settings - Advanced Settings里勾选“Generate simulation files”相关选项。从Waveform.vwf的波形中读状态跳转时注意两个关键时间点第一个是key_pulse结束后的下一拍状态是否从S_IDLE跳到了S_INPUT第二个是第8位输入结束后的下一拍状态是否跳到了S_CHECK且match_flag拉高继而lock_open在下一拍拉高。这三拍的延迟关系就是状态机能正常工作与否的“心电图”。4.3 时序报告里值得看的几个指标工程里有一批mimasuo.sta.summary、mimasuo.fit.rpt、mimasuo.timing.rpt之类的报告文件很多人编译完成从来看都不看。对于密码锁这种低复杂度设计最值得检查的是Fmax和Setup Slack两个指标。Fmax低不代表设计错——但如果你密码锁里有高频分频器或者异步逻辑Fmax会异常低这时候优先怀疑是不是把组合逻辑写在了未约束的时钟域里。Setup Slack如果是负值说明时序不收敛可能出现在板卡高温或电压偏低时偶发按键失灵。对该工程来说50MHz时钟下做到50ns级的Setup Slack并不难如果你看到几百ns级别恭喜但说明你给的时钟约束太松了真实硬件可能没你想的那么稳定。5. 动态数码管显示与改密流程的状态扩展在后续内容中继续展开数码管显示是密码锁和用户交互的“面子工程”但它的电路原理和键盘扫描是镜像关系——都利用了人眼的视觉暂留效应。seg_sweep0.vhd就是干这个的8位数码管不可能每个都分配独立的数据线必须分时点亮每位数码管在极短的时间内单独亮一次扫描完整一圈后人眼看到的是一组同时亮着的数字。扫描频率的设计规则和键盘消抖不一样数码管动态扫描一般取1kHz~2kHz的扫描频率也就是每位点亮时间在0.5~1ms之间。低于500Hz会出现明显闪烁高于5kHz且位数多时占空比太低会亮度不足。该工程里用seg_sweep0.vhd配合一个BCD到七段码的译码逻辑完成显示刷新。如果你要显示密码的输入位数比如输入到第几位对应第几位数码管闪烁需要额外加一个位选寄存器和闪烁控制信号——闪烁本质上是对位选信号做周期性的使能翻转可以用一个低频分频时钟的toggle信号来门控位选。改密流程是密码锁功能完整性的最后一环也是状态机里最容易失控的部分。工程中改密状态S_CHANGE的处理逻辑是在密码验证通过后额外等待一个“功能键”事件如果该事件到来状态机转入改密模式接下来的8次按键值被写入新的密码寄存器如果改密中掉了某一位必须有超时退出机制——否则用户只输入了6位新密码就停顿状态机会一直卡在S_INPUT改密分支此时无论按哪个键都会被认为是新的密码位而不是“取消改密”。一个稳妥做法是增加一个独立的idle计数5秒无按键自动回到S_IDLE并丢弃不完整的改密输入。6. 容易忽略但影响实板的几个验证技巧在后续内容中继续展开编译烧录见到“成功”两个绿字只是起点。实际跑起来之前有几个验证步骤比看仿真波形更能说明问题。首先检查引脚锁定。Quartus编译时如果某个引脚分配无效通常会给warning而不是error——这很操蛋因为你的设计可能在仿真里完美烧进板子后却“键盘没反应、数码管全灭”。建议在mimasuo.fit.rpt里搜Fitter Input逐项核对行线、列线、数码管的位选与段选是否映射到预期的FPGA引脚。如果你发现自己用的开发板和工程原始板的引脚定义不同直接改mimasuo.qsf文件里对应行不要试图在GUI里一个个点——用文本编辑器批量替换更快。其次要验证报警逻辑的实板响应。工程里报警的触发条件是连续错误3次报警表现形式可能是LED闪烁或数码管显示特定字符。这里有个验证技巧输入错码时要有意让系统进入报警状态然后观察复位是否必须断电重来还是按某个特定键就能退出报警。如果你希望报警后必须等待10秒才能重新输入需要在S_ALARM状态里加一个计数器这个计数器要独立于键盘扫描频率——否则如果报警逻辑里依赖key_pulse来计时用户在报警期间按键会干扰计数。最后是拨码开关或跳线导致的按键映射错乱。4×4键盘模块的输入输出方向在不同开发板上定义不同甚至同一块板子的两个键盘接口方向都是反的。如果你发现按键值错乱先不要怀疑VHDL——用Quartus的Chip Planner或者SignalTap II直接观察row和col引脚的实时电平按下每个键对比行列状态是否与yima.vhd中的case分支一致。SignalTap II是排查这类问题最快的工具但需要额外占用片上RAM这在这个工程剩余的存储资源里一般没问题。还有一个操作技巧密码锁烧录后如果硬件段选显示“88”或全灭大概率是数码管的共阴共阳接法搞反了——seg_sweep0.vhd里的七段码是按共阴数码管写的换成共阳的模块板上就是反码。你可以在源文件里加一个特性参数来切换共阴共阳而不是为两种数码管分别维护两份逻辑constant USE_COMMON_ANODE : boolean : false; seg_out not seg_cathode when USE_COMMON_ANODE else seg_cathode;这种“用常量开关切换硬件属性”的写法比复制整个显示模块要干净得多——后期如果你要换板卡改一行常量就能适配所有模块的段选逻辑不用去翻哪几个case分支要取反。本文还有配套的精品资源点击获取
返回列表