简介:这份基于VHDL的4位电子密码锁设计文档面向数字电路课程设计学生及FPGA初学者,围绕DE2开发板给出从需求分析到硬件落地的完整方案。资源以docx格式提供,压缩包内仅1个文件,体积约153KB,内容为大连理工大学数字电路课程设计实验报告,涵盖设计要求、系统方案、模块划分、管脚分配、数码管真值表及VHDL源代码。读者可据此了解4位8421BCD密码输入、只取前4位、三次重试与报警锁定、密码重置、7段数码管逐位左移显示、取消键重新输入等功能的实现思路,并掌握分频模块、模4与模3计数器、密码比较和重置、LED正确/错误指示等模块设计。文档中给出清晰的模块框图和端口信号说明,便于按步骤复现与验证,对课设报告撰写与FPGA调试具有直接参考价值。目前已有178人学习下载,适合需要课设参考、VHDL入门或综合练习的读者。
1. 一个自建 clock 的 4 位密码锁,为什么比状态机更值得先学
实验室里 4 个按键、一排拨码开关、4 个七段数码管,要求做出一个能改密码、错三次报警的锁。多数人第一反应是把所有逻辑塞进一个状态机 process。真到上板才发现,最先出问题的不是密码比较,而是“每按一次键数码管左移一位”——按键给的是电平,不是时钟,数码管根本不动作。这套基于 VHDL 的 4 位电子密码锁,核心手法是自己造一个 clock:拨码开关组合经译码后拉高该信号,用它的上升沿驱动左移寄存器;同时用模 5000 分频器把 50MHz 系统时钟压到 10000Hz,保证人手拨开关的间隔远大于采样周期,边沿不会漏掉。密码为 4 位 8421BCD 码,超过 4 位只取前 4 位,错三次拉高报警,管理员用 reset 低电平解除,密码正确后可通过 set_psw 改密。目标板是 DE2,芯片 EP2C35F672C6。适合学过数字电路、手边有 FPGA 板子、想找完整案例练管脚约束和仿真的人。
2. 从拨码开关到数码管:模块划分与 DE2 管脚约束
先把系统拆成能单独仿真的小块,再拼顶层,是这个设计最省时间的地方。DE2 上可用按键只有 4 个,而密码有 10 个数字要输入,所以输入用 switch[9]~switch[0] 的独热编码来模拟数字 9~0,一个开关对应一个数字,物理上不可能同时拨两个,天然避免了编码冲突。整条数据通路是:开关 → 输入译码(8421BCD)→ 左移寄存器 → 密码比较 → LED 指示;控制通路则是:start 使能、cancel 清零、yes 确认、set_psw 改密、reset 管理员复位。
2.1 五个模块的职责边界
| 模块 | 输入 | 输出 | 关键信号 |
|---|---|---|---|
| 输入译码 | num[9:0] | q[3:0]、clock | 独热码转 BCD,产生移位时钟 |
| 显示驱动 | out_q[15:0] | out0~out3[6:0] | 共阳极段码,低电平点亮 |
| 左移控制 | clock、cnt4 | out_q[15:0] | 模 4 计数,超 4 位丢弃 |
| 比较与重置 | out_q、psw、yes | correct、out_error | 模 3 计数,三次报警 |
| 报警与解锁 | reset、alarm | alarm | reset=0 时强制清报警 |
这里最容易被忽略的是模 4 计数器的定位。设计需求写的是“多于 4 位,密码只取前 4 位”,有两种实现:一是在比较时只截取高 4 位,二是干脆不让第 5 位挤进寄存器。这份代码选了后者,用 cnt4 在计数到 4 后停止移位,输入第 5 个数字时null掉,逻辑更直接,也避免了显示上出现“第 5 位把有效位顶出去”的错觉。
2.2 顶层端口与管脚映射
顶层实体 coded_lock 的端口共 11 个:clk、start、reset、yes、cancel、set_psw、num[9:0]、out3~out0、correct、alarm、out_error。在 Quartus 里落地成约束文件,写法不是图形界面点出来的,而是直接写 qsf:
# 50MHz 系统时钟 set_location_assignment PIN_N25 -to clk # 拨码开关 num[0]~num[9],对应数字 0~9 set_location_assignment PIN_N25 -to num[0] set_location_assignment PIN_N26 -to num[1] set_location_assignment PIN_P25 -to num[2] set_location_assignment PIN_AE14 -to num[3] set_location_assignment PIN_AF14 -to num[4] set_location_assignment PIN_AD13 -to num[5] set_location_assignment PIN_AC13 -to num[6] set_location_assignment PIN_C13 -to num[7] set_location_assignment PIN_B13 -to num[8] set_location_assignment PIN_A13 -to num[9] # 功能键 set_location_assignment PIN_U4 -to start set_location_assignment PIN_G26 -to yes set_location_assignment PIN_V1 -to cancel set_location_assignment PIN_N23 -to set_psw set_location_assignment PIN_V2 -to reset # LED 指示 set_location_assignment PIN_AE22 -to correct set_location_assignment PIN_AE23 -to out_error set_location_assignment PIN_AF23 -to alarm约束里的每个 PIN 都必须与硬件手册一致,写错一个,现象往往是“功能都对,就是某个灯不亮”,很容易被误判成逻辑 bug。数码管侧同理,out0[0]~out0[6] 接到 PIN_AF10、PIN_AB12、PIN_AC12、PIN_AD11、PIN_AE11、PIN_V14、PIN_V13,out1、out2、out3 依次接 PIN_V20 起的三组、PIN_AB23 起的三组、PIN_Y23 起的七段。四组段码线一共 28 根,接线量大,建议先只连 out0 调通一位,再复制到其余三位。注意 num[0] 与 clk 不能共用同一个引脚,原文管脚表里两处都写了 PIN_N25,属于表格笔误,实际分配时要给 num[0] 另选一个空闲开关引脚,否则综合能过、上板时钟直接失效。
3. 分频、左移寄存器与密码比较的 VHDL 实现
这份源码最值得抄的不是密码比较,而是时钟域的处理方式。系统时钟 50MHz,人拨一次开关的时间动辄几十毫秒,两者差了六个数量级,直接拿 50MHz 去采样按键电平,会在一次拨动期间采到成百上千个高电平,左移寄存器一次按键就移位几十次。所以必须分频,把采样节奏降下来。
3.1 模 5000 分频:50MHz 降到 10000Hz
-- 分频模块:50MHz -> 10000Hz process(clk) variable tem : integer range 0 to 4999; begin if rising_edge(clk) then if tem = 4999 then clk_in <= '1'; tem := 0; else tem := tem + 1; clk_in <= '0'; end if; end if; end process;计满 4999 个系统时钟周期,clk_in 输出一个周期的高电平,等效分频比 5000,输出 10000Hz,周期 0.1ms。这个数值不是随便取的:人手拨动开关的最短间隔约几十毫秒,10000Hz 的采样周期远小于它,既保证不漏采,又不会在一个电平内重复移位。tem 用 variable 而非 signal,是为了在同一个 process 内立即更新,避免引入额外延迟;范围约束 0 to 4999 让综合器知道位宽,省掉默认 32 位比较器。常见改法是只降一档,比如把分频比改成 500,输出 100kHz,仿真跑得快,但上板后按键响应会变“过于灵敏”,一次拨动仍可能被采到两次。
3.2 开关译码与自建 clock 的边沿构造
process(clk_in, start, reset, enable) begin if reset = '0' then clock <= '0'; elsif rising_edge(clk_in) then if start = '1' and enable = '1' then case num is when "1000000000" => q <= "1001"; clock <= '1'; -- 开关9 -> 9 when "0100000000" => q <= "1000"; clock <= '1'; -- 开关8 -> 8 when "0010000000" => q <= "0111"; clock <= '1'; when "0001000000" => q <= "0110"; clock <= '1'; when "0000100000" => q <= "0101"; clock <= '1'; when "0000010000" => q <= "0100"; clock <= '1'; when "0000001000" => q <= "0011"; clock <= '1'; when "0000000100" => q <= "0010"; clock <= '1'; when "0000000010" => q <= "0001"; clock <= '1'; when "0000000001" => q <= "0000"; clock <= '1'; -- 开关0 -> 0 when others => q <= "1111"; clock <= '0'; end case; end if; end if; end process;这段就是整个设计的“心脏”。开关拨到某一位,case 命中,q 得到对应的 8421BCD 码,同时 clock 拉高;松开开关后 num 变成全 0,落到when others,clock 回到低电平。下一次拨动,clock 再产生一个上升沿。整个过程由 clk_in 同步,所以边沿干净、无毛刺。enable 是第三次错密后的封锁位,为 0 时 case 不再执行,键盘直接失效,这就是“报警后无法继续尝试”的实现方式,比在比较模块里判断更省资源。需要注意when others里把 q 置成 "1111",对应七段译码输出全灭,正好把松开开关的瞬间隐藏掉,不会在数码管上闪出杂码。
3.3 左移寄存器与模 4 计数器
process(clock, yes, start, reset, cancel) begin if reset = '0' then correct <= '0'; out_error <= '0'; counter <= 0; enable <= '1'; alarm <= '0'; out_q <= "1111111111111111"; elsif cancel = '1' then -- 取消本次输入 correct <= '0'; out_error <= '0'; out_q <= "0000000000000000"; cnt4 <= 0; elsif start = '0' then -- 未启动,寄存器清零 cnt4 <= 0; out_q <= "0000000000000000"; else if rising_edge(clock) then if cnt4 = 4 then null; -- 已满 4 位,第 5 位丢弃 else out_q <= out_q(11 downto 0) & q; -- 左移 4 位,新数进最低位 cnt4 <= cnt4 + 1; end if; end if; end if; end process;out_q(11 downto 0) & q是典型的拼接移位:高 12 位整体下移,新输入的 4 位 BCD 落到最低 4 位,也就是最右边的数码管 u0,旧数据依次向 u3 方向移动。cnt4 从 0 计到 4,到 4 后锁死,实现“只取前 4 位”。cancel 分支把 out_q 和 cnt4 一起清零,重新开始一轮输入;start='0' 是待机态,同样保持清零。注意这里用rising_edge(clock)而不是clock'event and clock='1',前者是标准写法,综合结果一致但可读性更好。
3.4 密码比较、三次机会与模 3 计数器
if falling_edge(yes) then if out_q = psw then correct <= '1'; out_error <= '0'; alarm <= '0'; elsif counter = 2 then -- 第三次仍错 correct <= '0'; out_error <= '1'; alarm <= '1'; enable <= '0'; -- 封锁键盘 else correct <= '0'; out_error <= '1'; alarm <= '0'; counter <= counter + 1; end if; end if;比较用out_q = psw直接做 16 位向量相等判断,综合出来是一排同或门加与门,资源很小。判断沿选的是 yes 的下降沿,也就是松开确认键的那一刻才比较,避免按住时重复触发。counter 从 0 开始,等于 2 时说明前面已经错了两回,这是第三次,于是拉高 alarm 并把 enable 清 0。默认密码 psw 初值写成"1001100001110110",四位分别是 9、8、7、6,与 8421BCD 一一对应。这里有个容易踩的坑:counter 是 integer 类型,仿真初值默认是 integer'left,必须显式赋 0,否则首次比较时counter = 2的判断行为不可预期。
3.5 密码重设与共阳极段码
process(correct, set_psw) begin if falling_edge(set_psw) then if correct = '1' then -- 只有验密通过才允许改 psw <= out_q; end if; end if; end process;改密逻辑只有三行,但顺序很关键:先输入 9876 按 yes,correct 拉高;再输入新密码,按 set_psw,新值写入 psw。correct 为 0 时按 set_psw 不产生任何效果,防止误改。段码侧用共阳极真值表,0 表示该段点亮:
| BCD | 段码 | BCD | 段码 |
|---|---|---|---|
| 0 | 1000000 | 5 | 0010010 |
| 1 | 1111001 | 6 | 0000010 |
| 2 | 0100100 | 7 | 1111000 |
| 3 | 0110000 | 8 | 0000000 |
| 4 | 0011001 | 9 | 0010000 |
其余输入统一输出1111111全灭。四位数字分别取自 out_q 的 [15:12]、[11:8]、[7:4]、[3:0],各驱动一个 decoder 元件,所以数码管的排布与移位方向严格对应,调显示时只要看 out_q 的哪一段不对,就能定位是移位还是译码的问题。
4. Quartus II 下的编译、仿真与典型报错定位
工程建好后,先综合再看仿真,顺序反了会浪费大量时间。综合阶段的报错大多是类型和端口问题,仿真阶段的波形异常则多半是初值或边沿问题,两类问题的排查手法完全不同。下面按实际操作顺序走一遍,并把这份代码最容易踩的几个点挑出来。
4.1 顶层实体与 buffer 端口的处理
原文把 correct 和 out_error 声明成buffer,因为它们既做输出又参与内部判断。buffer 在 VHDL-93 里合法,但对端口映射有限制,某些工具链会在层次化调用时报错。常见做法是改成out加一个内部 signal:内部用sig_correct参与比较,输出端correct <= sig_correct,这样既能被读数,又不会被综合器抱怨。若坚持用 buffer,必须保证该端口不被其他模块驱动,否则报“multiple drivers”。
4.2 用 Testbench 复现三次错误报警
signal clk_tb : std_logic := '0'; signal num_tb : std_logic_vector(9 downto 0) := (others => '0'); -- 50MHz 时钟:周期 20ns clk_tb <= not clk_tb after 10 ns; stim : process begin start <= '1'; -- 第一轮:8765 num_tb <= "0100000000"; wait for 1 ms; num_tb <= (others => '0'); wait for 1 ms; num_tb <= "0000100000"; wait for 1 ms; num_tb <= (others => '0'); wait for 1 ms; num_tb <= "1111000" & "000"; wait for 1 ms; num_tb <= (others => '0'); wait for 1 ms; yes <= '1'; wait for 1 ms; yes <= '0'; wait for 1 ms; -- 下降沿触发比较 -- 第二、三轮换成 9776、5432,重复上述拨码动作 wait; end process;Testbench 的关键是把每次拨码的高电平保持时间设成毫秒级,远大于 0.1ms 的采样周期,模拟真实手速;yes 用一个高脉冲再拉低,制造下降沿。观察 alarm、out_error、correct 三条波形,就能验证“前两次错只亮红灯、第三次错红灯加报警、reset 拉低后 alarm 归零”是否符合需求。如果第三轮结束 alarm 始终不拉高,先查 counter 的初值,再查 enable 是否在第一次就误清 0。
4.3 上板阶段的两类现象
按键抖动是第一个坑。cancel 和 yes 接的是按键而非开关,机械抖动会在一两个毫秒内产生多个边沿,导致“按一次确认,被判成两次比较”。这类问题的处理办法是在按键后加一级同步加计数滤波,连续采到 N 个相同的 clk_in 周期才认账。
引脚写错是第二个坑。前面提到的 num[0] 与 clk 冲突就属于这一类,现象是烧录成功、数码管全灭、任何开关操作都无反应,很容易误判成复位信号没拉高。排查时先确认 clk 引脚上确实有 50MHz 输入,可以直接在顶层把 clk 引到一个 LED 上做分频闪烁测试,灯在闪说明时钟没问题,再回头查开关。
5. 把密码位数与尝试次数参数化
这套代码把“4 位密码”和“3 次机会”写死在了 out_q 位宽和 integer range 里,想改成 6 位密码就得动多处。工程化做法是用 generic 把它们提出来,顶层实例化时再定值。移位部分改成按 4 比特步进推进,计数器上限与握手信号统一由常数推导:
entity coded_lock is generic( PWD_LEN : integer := 4; -- 密码位数 TRY_MAX : integer := 3 -- 允许尝试次数 ); port( ... ); end; architecture behave of coded_lock is signal cnt4 : integer range 0 to PWD_LEN := 0; begin -- 输入满 PWD_LEN 位后停止移位 if rising_edge(clock) then if cnt4 = PWD_LEN then null; else out_q <= out_q(15 - 4 downto 0) & q; cnt4 <= cnt4 + 1; end if; end if; -- 尝试次数改为比较 counter = TRY_MAX - 1 if falling_edge(yes) then if counter = TRY_MAX - 1 then alarm <= '1'; enable <= '0'; else counter <= counter + 1; end if; end if; end process; end behave;参数化的收益不只是少改几行。密码位数变化时,out_q 的位宽、译码器的实例个数、数码管的接线数量必须同步改,generic 至少能把逻辑侧的一致性保证住。若位数超过 4 位,显示资源会不够,这时应该把静态四位数码管改成动态扫描:用 clk_in 分频出一路几百赫兹的扫描时钟轮流选通各位,人眼利用余辉看到整体显示,代价是引入位选信号和更高频率的刷新时序。
验证仍按老三样:先只改 PWD_LEN 跑一遍综合,看 out_q 位宽和译码实例是否匹配;再用 Testbench 把密码填满并多输一位,确认第 5 位被丢弃且显示不乱;最后上板时把 TRY_MAX 临时改成 1,一秒内就能触发报警,比连按三次确认键省事得多。需要留神的是counter = TRY_MAX - 1这类表达式在 integer range 边界上容易越界,range 至少要留出 TRY_MAX 个取值,否则第三次比较时综合器会报范围溢出警告,仿真波形也会在报警前意外回绕。
本文还有配套的精品资源,点击获取