1. 从零开始:秒表进阶实验到底要做什么?
如果你已经跟着实验五,用Multisim和Basys3做出了一个基础的秒表,看着数码管上的数字随着时钟信号跳动,那种成就感肯定很棒。但那个秒表可能还有点“傻”:上电就跑,想停都停不下来,更别说记录多个时间了。这次的“秒表进阶”实验,就是要给你的作品注入灵魂,让它变成一个真正智能、实用的工具。
简单来说,我们要实现两个核心目标。第一个是状态控制,让秒表能听我们指挥。我们设计一个只有三个状态(清零A、计时B、停止C)的简单状态机,用一个核心按键(比如Basys3上的BTNC)来循环切换。想象一下你手里的实体秒表:按一下开始,再按一下暂停,再按一下归零,就是这个逻辑。第二个是数据记忆,让秒表能记住历史。我们不仅要能计时,还要能把精彩的瞬间(比如三次不同的成绩)存下来,并且能随时翻看,就像手机相册一样,而当前正在进行的计时还不会被打断。
这个实验的妙处在于,它把数字电路里几个非常核心的概念串了起来:状态机、按键消抖与检测、寄存器组和数据选择器。你不再是孤立地学习一个个芯片,而是亲手把它们组装成一个协同工作的系统。我当年学数电的时候,就是做完这个实验,才真正搞明白了“控制流”和“数据流”是怎么在硬件里流淌的。我们会先在Multisim里用仿真把逻辑跑通,看着信号波形一切正常,然后再把代码烧录到Basys3这块真实的FPGA开发板上,看着硬件按你的设计精准运行,这个过程非常过瘾。
实验环境还是老伙计:Multisim用来做前期的电路设计和仿真验证,这是我们的“数字沙盘”;Xilinx ISE(或者现在更多人用Vivado)负责把我们的设计变成比特流文件;最后,Digilent Basys3开发板就是我们的实战舞台,上面的FPGA芯片、按键、数码管都是待我们调遣的士兵。准备好了吗?我们一步步来,我保证把每一步的“为什么”和“怎么做”都讲清楚。
2. 核心控制:设计一个听话的三状态秒表
基础秒表只会闷头计数,我们现在要给它装上大脑,这个大脑就是一个有限状态机。别看名字唬人,其实它就像一个有严格流程的自动售货机:投币(按键) -> 选择商品(状态切换) -> 出货(执行动作)。我们的秒表状态机只有三个状态,形成一个循环:A(清零) -> B(计时) -> C(停止) -> A(清零)...
2.1 如何准确捕捉一次按键动作?
要让状态机响应按键,第一步是必须准确识别出“一次按键”。这里有个大坑:机械按键在按下和弹起时,会产生一段时间的抖动,在电路看来就是一连串快速的开通关断信号。如果你直接用这个信号当时钟,状态可能会乱跳好几次。
原始文章里提到用4位串入并出移位寄存器来检测按键上升沿,这个方法非常经典且有效。我来拆解一下它的工作原理。我们把这个寄存器看成一条4个位置的流水线(Q3, Q2, Q1, Q0)。每个时钟周期(我们用系统主时钟,比如100MHz),流水线里的数据向右移动一位,同时把当前按键的电平(1表示按下,0表示松开)塞到最左边Q3的位置。
关键逻辑在于:我们判断Out = Q1 AND (NOT Q0)。这是什么意思呢?想象一下按键从0变到1(按下)的过程。在某个时钟沿,按键刚按下,电平“1”被移入Q3,此时Q1和Q0还是之前的历史值(都是0)。下一个时钟沿,这个“1”被移到Q2,Q1得到0,Q0还是0,输出为0。再下一个时钟沿,“1”移到Q1,Q0还是0,此时Q1=1, Q0=0,我们的输出Out瞬间变为一个时钟周期的高电平!之后,只要按键保持按下,“1”会继续移到Q0,输出又会变回0。
所以,这个电路的精髓在于,它只在检测到按键电平从0跳变到1后的那个特定时刻,输出一个单时钟周期宽度的脉冲。这个脉冲就是我们所认可的“一次有效的按键动作”,完美避开了抖动期。在Multisim里搭建这个电路时,记得把寄存器的异步复位端接好,确保上电时所有输出是确定的0。
2.2 构建状态切换的“指挥中心”
检测到按键脉冲后,我们需要一个电路来记住当前处于哪个状态,并根据脉冲切换状态。原始文章用了一个十进制计数器加2-4线译码器的组合,这是一个非常巧妙的“计数器型状态机”实现。
我更喜欢用更直观的思路来解释:我们需要一个能循环输出00, 01, 10这三个代码的电路。一个模3计数器(0,1,2循环)正好可以。用十进制计数器改造也很简单:把计数器的输出QA和QB拿出来(它们代表00,01,10,11),然后我们设计一个逻辑,当它数到3(二进制11)时,立刻让它清零回到0。这样,它的有效循环就是00(A), 01(B), 10(C)。
然后,用一个2-4译码器。把计数器的QA、QB接译码器的地址输入端。译码器的输出Y0、Y1、Y2就分别对应了A、B、C三个状态。当计数器是00时,只有Y0输出高电平,表示“清零状态”;计数器是01时,Y1为高,表示“计时状态”;计数器是10时,Y2为高,表示“停止状态”。这个高电平信号,就是我们控制后续计时器的“尚方宝剑”。
2.3 让计数器服从状态指挥
现在,我们有了三个状态信号:Clear(清零)、Hold(保持/运行)。注意,Hold信号在状态B时为高(允许计数),在状态C时为低(停止计数)。接下来就要改造我们实验五里的计时模块了。
计时模块的核心是一个或多个级联的十进制计数器(比如74LS160)。每个计数器都有几个关键控制引脚:CLK(时钟)、ENT/ENP(计数使能)、CLR(同步清零)。
Clear信号的处理:它需要具有最高优先级。无论计数器在做什么,一旦Clear有效,计时器必须立刻归零。我们可以将Clear信号直接连接到每个计数器的CLR异步清零端(如果支持异步清零),或者与计数器原有的清零逻辑进行“或”运算。确保在状态A时,数码管稳稳地显示“00.00”。Hold信号的处理:它控制计时器是否“走动”。我们把Hold信号连接到所有级联计数器的ENT和ENP使能端。当Hold=1(状态B),使能有效,计数器在每个时钟上升沿正常递增;当Hold=0(状态C),使能被禁止,计数器保持当前数值不变,实现“暂停”功能。
在Multisim里连接好这部分后,一定要做仿真测试。创建一个测试波形,模拟连续点击BTNC按键,观察状态译码器的输出、Hold和Clear信号,以及最终计数器输出值的变化。你会看到,随着你的“按键”动作,电路严格地在“清零->计时->暂停->清零”之间循环,计时器也乖乖地跟随动作,这就说明你的状态控制核心搭建成功了。
3. 功能升级:给秒表加上“记忆”功能
能控制开始暂停,这已经是个合格的秒表了。但进阶要求让它变成了一个“运动教练秒表”——可以连续记录多个运动员的成绩,并随时回顾。这就需要我们设计一个数据存储与读取系统。
3.1 搭建存储数据的“仓库”
我们要存储最近的三组时间数据。每组数据对应4位数码管显示(假设是两位秒和两位百分秒),也就是4个BCD码(每个BCD码4位二进制)。所以,总共需要存储 3组 * 4位/组 = 12个BCD码数字。
用什么存?4位双向移位寄存器(如74LS194)是绝佳选择。我们可以把它配置为“并行输入、并行输出”的模式。想象我们并排放置三组寄存器(每组4个,分别存一个BCD码),它们像三排储物柜。
存储(BTNU按键)的逻辑是:每按一次BTNU,我们就把当前计时器显示的四位BCD码,整体存入第一排“储物柜”。同时,原来第一排的数据被推到第二排,第二排的被推到第三排,第三排的旧数据就被丢弃了。这就实现了“保留最近三次记录”的功能。这个“整体推入”的动作,可以通过将BTNU检测到的脉冲作为所有寄存器的时钟信号,并将寄存器的S1S0模式控制端设置为“并行加载”来实现。
3.2 设计读取数据的“调度员”
存好了数据,怎么读呢?用另一个按键BTND来控制读取。要求是按顺序读:第一次按读最早存的(即第三排),第二次按读第二个存的(第二排),第三次按读最新的(第一排),第四次按则切换回显示当前计时值。
这里需要一个四进制计数器来记录我们按了几次BTND(1,2,3,4循环)。同时,我们需要一个数据选择器网络来充当调度员。我们为每一位数码管驱动信号都配备一个4选1数据选择器(比如74LS153的扩展应用)。
数据选择器的四个数据输入端分别接:当前计时值、第一排寄存器数据、第二排寄存器数据、第三排寄存器数据。数据选择器的地址选择端,就由那个四进制计数器的输出(00,01,10,11)来控制。通过精心设计计数器输出与地址的映射关系,我们就能实现:计数器为00时,选通当前计时值;为01时,选通最新记录(第一排);为10时,选通次新记录(第二排);为11时,选通最早记录(第三排)。
3.3 整合存储与读取系统
整个数据通路的设计需要仔细规划。存储通路和读取通路是独立的,但共享寄存器组。BTNU脉冲来时,只触发寄存器的并行加载和移位,不影响读取计数器。BTND脉冲来时,只触发读取计数器加一,从而改变数据选择器的通道,不影响寄存器内存储的数据。
在Multisim中搭建这个部分时,建议分层设计。把“存储寄存器堆”打包成一个子电路,“读取控制逻辑”(计数器+数据选择器)打包成另一个子电路,然后在顶层将它们与计时器、状态机连接起来。这样电路图会清晰很多,也便于调试。
仿真时,设计复杂的测试序列:先计时一段时间,按BTNU存储;再计时,再存储;再计时,再存储。然后在不干扰当前计时的情况下,反复按BTND,观察输出显示是否按照“当前值->记录3->记录2->记录1->当前值…”的顺序循环变化。这个过程能让你深刻理解数据流与控制流是如何分离又协同的。
4. 从仿真到实战:在Basys3开发板上跑起来
Multisim仿真成功,只意味着我们的逻辑设计是正确的。接下来要把这个设计“铸造”到真实的硬件——Basys3 FPGA开发板中。这个过程是把原理图或硬件描述语言变成芯片内部实际连接的过程。
4.1 硬件描述语言实现关键模块
虽然Multisim是图形化设计,但最终在ISE/Vivado中,我们更常用Verilog或VHDL来描述电路。这里我用一些简化的Verilog代码片段来说明核心模块的思路,你可以根据这些思路去完善。
首先是按键边沿检测模块,这比用移位寄存器更常用:
module key_edge_detector ( input clk, // 系统时钟(如100MHz) input key_i, // 原始按键输入 output pulse_o // 检测到的上升沿脉冲 ); reg [2:0] key_r; // 3级寄存器用于同步和延时 always @(posedge clk) begin key_r <= {key_r[1:0], key_i}; end // 检测上升沿:前一刻是0,这一刻是1 assign pulse_o = (~key_r[2]) & key_r[1]; endmodule这个模块通过三级寄存器同步外来按键信号,消除亚稳态,并比较相邻两个时钟周期的值来产生脉冲。
其次是三状态机模块,用清晰的case语句描述:
module state_machine ( input clk, input rst_n, // 复位信号 input key_pulse, // 来自BTNC的按键脉冲 output reg [1:0] state_o // 状态输出:00-A, 01-B, 10-C ); localparam S_A = 2'b00, S_B = 2'b01, S_C = 2'b10; always @(posedge clk or negedge rst_n) begin if (!rst_n) state_o <= S_A; // 上电复位到A状态 else if (key_pulse) begin // 有按键脉冲才切换状态 case (state_o) S_A: state_o <= S_B; S_B: state_o <= S_C; S_C: state_o <= S_A; default: state_o <= S_A; endcase end end // 根据状态产生控制信号 assign clear = (state_o == S_A); assign hold = (state_o == S_B); endmodule存储和读取控制模块逻辑类似,也需要用状态机或计数器配合数据选择器来实现。将各个模块在顶层文件中实例化并连接起来,就构成了完整的系统。
4.2 引脚约束与上板调试
代码写完后,需要告诉工具你的输入输出信号对应Basys3板子上的哪个物理引脚。这就是创建约束文件(.xdc文件)。例如:
set_property PACKAGE_PIN W5 [get_ports {clk}] # 系统时钟接100MHz晶振 set_property PACKAGE_PIN U18 [get_ports {btnc}] # BTNC中心按键 set_property PACKAGE_PIN T18 [get_ports {btnu}] # BTNU上键 set_property PACKAGE_PIN W17 [get_ports {btnd}] # BTND下键 set_property PACKAGE_PIN W7 [get_ports {seg[0]}] # 数码管段选a ... # 其他段选和位选引脚 set_property IOSTANDARD LVCMOS33 [get_ports *] # 所有端口电压标准为3.3V综合、实现、生成比特流文件后,用线连接电脑和Basys3,将比特流下载到板载FPGA中。
上板调试是最后一步,也是问题最多的一步。如果数码管不亮,先检查约束引脚是否正确,段选和位选的极性(共阴/共阳)是否搞反。如果按键没反应,用示波器或者写个简单的LED测试程序,先确认按键硬件和去抖代码是否正常工作。如果存储读取顺序错乱,回头检查你的计数器状态转换逻辑和数据选择器地址映射。耐心地定位问题,每一次解决问题的过程,都是对数字系统理解的一次深化。
当你最终看到板子上的秒表完全按照你的设计,精准地开始、停止、存储、读取时,你会觉得之前所有的调试和思考都是值得的。这个项目虽然不大,但它麻雀虽小五脏俱全,涵盖了FPGA开发从设计、仿真到实现、调试的全流程,是通向更复杂数字系统设计的一块坚实跳板。