简介:一份基于Verilog的FPGA时钟设计工程包,面向数字电路初学者和FPGA实践者,适合希望用硬件描述语言完成综合小项目的读者。资源围绕时钟模块展开,包含秒、分、小时计数器及进位逻辑,并实现闹铃准点比较、整点/半点报时与机械按键去抖等实用功能,可帮助理解同步时序电路的条件检测和状态控制。工程文件共101个,以Quartus工程配置与编译下载文件(.qpf/.qsf/.sof/.pof)为主,另有Verilog源码(.v)、文本说明和辅助脚本,整体约3.83MB,结构清晰且目录便于按功能模块检索,可直接打开工程查看逻辑或重新编译下载。已有1526人学习下载,通过阅读源码和工程结构,可以掌握时钟分频、闹铃比较器设计、按键消抖电路等典型模块写法,为后续更复杂的数字系统开发打好实践基础。
1. 从“仿真全对、上板不亮”说起:Verilog时钟到底在做什么
很多玩FPGA的第一块上板作品,就是时钟:六个数码管显示时分秒,加个按键能调时,设个闹铃到点就响。这个项目标题看着简单,但背后牵出分频、BCD计数、扫描显示、按键消抖、状态机、闹铃比较一整条数字逻辑链。我见过不少人在testbench里把波形跑得干干净净,一上板秒不走、显示乱跳、闹铃不响,最后卡在某个玄学细节上。这篇笔记把这条链路拆开讲:为什么这么设计、代码怎么写、参数怎么调、坑在哪。新手能照着搭,熟手可以拿去对边界,顺带省掉一轮上板翻车的时间。
2. 秒分时计数器:用BCD和脉冲使能把时间“数”出来
2.1 为什么不用二进制计数器
初学者最容易想到的是用一个大计数器,从0数到86399,再拆成时分秒。这个思路没错,但直接落地会别扭:六个数码管要显示的数字是十进制,纯二进制计数器得频繁做取模和除法才能得到每一位。这种运算在FPGA里组合逻辑开销大,还会让进位链变长,影响时序收敛。更关键的是,闹铃比较、调时装载都发生在“某个十进制位”上,二进制表示会让比较逻辑变得很绕。
所以工程上普遍用BCD(二进制编码十进制):秒个位、秒十位、分个位、分十位、时个位、时十位各用4位寄存器,秒和分做模60计数,时做模24计数。每个4位寄存器的取值范围0-9,直接把BCD码给到数码管的七段译码表,连转换都省了。代价是多用几个触发器,但FPGA的寄存器资源最不缺,用面积换清晰和易维护,这笔账划算。
BCD的进位规则也直观:个位加到9再加1就归0并向十位进1,十位加到5再加1就归0并向上一级进1。这条规则完全贴着一个人类读时间的习惯,调时、闹铃比较都不需要额外的转换模块,而且写进RTL之后,仿真波形里每一位是什么一目了然。
2.2 模60/模24计数器:带装载的可用代码
下面是我常用的带装载功能的模60计数器。之所以加load端口,是因为调时的时候需要直接把当前值改成设定值,而不是从0按几百次才能调到目标时间。没有装载端口的话,调时逻辑会非常痛苦。
module counter60 ( input wire clk, // 系统时钟 input wire rst_n, // 异步复位,低有效 input wire en, // 秒脉冲使能,高电平计数一次 input wire load_en, // 装载使能,高电平写入设定值 input wire [3:0] load_ones, // 设定的个位 BCD 值 input wire [3:0] load_tens, // 设定的十位 BCD 值 output reg [3:0] ones, // 个位 output reg [3:0] tens, // 十位 output reg carry // 进位脉冲,持续一个时钟周期 ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ones <= 4'd0; tens <= 4'd0; carry <= 1'b0; end else if (load_en) begin ones <= load_ones; tens <= load_tens; carry <= 1'b0; end else if (en) begin carry <= 1'b0; // 默认不产生进位 if (ones == 4'd9) begin ones <= 4'd0; if (tens == 4'd5) begin tens <= 4'd0; carry <= 1'b1; // 59 -> 00,输出进位脉冲 end else begin tens <= tens + 1'b1; end end else begin ones <= ones + 1'b1; end end else begin carry <= 1'b0; end end endmodule这个模块的关键点有三个。第一,en是电平使能而不是时钟,整个设计只用一个系统时钟,避免多时钟域带来的亚稳态和综合警告。第二,carry是单周期脉冲,它在en有效的那一拍产生高电平,下一拍自动归0,正好作为下一级计数器的en。第三,load_en优先级高于en,调时状态下按下add键不会同时触发计数。
模24计数器写法一样,只是进位条件从“个位9且十位5”换成“个位3且十位2”,也就是23:59:59之后要翻到00:00:00:
module counter24 ( input wire clk, input wire rst_n, input wire en, input wire load_en, input wire [3:0] load_ones, input wire [3:0] load_tens, output reg [3:0] ones, output reg [3:0] tens, output reg carry ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ones <= 4'd0; tens <= 4'd0; carry <= 1'b0; end else if (load_en) begin ones <= load_ones; tens <= load_tens; carry <= 1'b0; end else if (en) begin carry <= 1'b0; if (ones == 4'd3 && tens == 4'd2) begin // 23 -> 00 ones <= 4'd0; tens <= 4'd0; carry <= 1'b1; end else if (ones == 4'd9) begin ones <= 4'd0; tens <= tens + 1'b1; end else begin ones <= ones + 1'b1; end end else begin carry <= 1'b0; end end endmodule使用时按秒→分→时串起来:秒计数器的carry接分计数器的en,分计数器的carry接时计数器的en。这样秒跑满60次,分加1;分跑满60次,时加1。这里的级联是“脉冲链”,不是每个模块各用一个独立时钟,综合工具能正确约束路径。
2.3 秒脉冲产生:分频参数决定仿真和上板速度
有了一秒的基准信号才能让秒计数器动起来。常见做法是把开发板晶振频率分频得到1Hz。有人直接分频产生方波送给计数器当时钟,我一般不这么做——方波是时钟信号,会引入第二个时钟域,而且门控时钟在FPGA上容易惹出毛刺。我更习惯产生“每秒一个时钟周期宽的高脉冲”,用它做使能。
module pulse_gen #( parameter CNT_MAX = 50_000_000 - 1 // 默认适配 50MHz 晶振 )( input wire clk, input wire rst_n, output reg pulse // 每 CNT_MAX+1 个时钟周期输出一个高脉冲 ); reg [25:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= 26'd0; else if (cnt == CNT_MAX) cnt <= 26'd0; else cnt <= cnt + 1'b1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) pulse <= 1'b0; else pulse <= (cnt == CNT_MAX); // 到达边界时拉高一拍,随后自动拉低 end endmoduleCNT_MAX这个参数是整篇设计的“速度开关”。上板时50MHz晶振写成49_999_999;如果你的板子是100MHz或12MHz,改成对应的值减1即可。仿真时为了不真等一秒,把它改成99甚至9,秒脉冲就会在100个或10个时钟周期里产生一次,一整天的逻辑几毫秒就跑完了。后面验证技巧章节还会细说这个参数怎么配合testbench用。
注意计数器位宽。CNT_MAX最大到50_000_000-1约23位,我写成26位留了余量,即使换100MHz晶振也够用。用parameter而不是localparam,是为了能在例化时合法覆盖。
3. 六位数码管扫描显示:刷新率、消影与段码表
3.1 扫描显示原理与刷新率选择
时钟的时间是六个数字:时十位、时个位、分十位、分个位、秒十位、秒个位。FPGA开发板通常不会同时给每个数码管配独立的段驱动,而是把六个数码管的相同段引脚并在一起,靠位选轮流点亮。这种“分时复用”利用人眼的视觉暂留:某一瞬间只有一位数码管在亮,但只要每一轮刷新够快,看起来就像六个数字同时在显示。
刷新率怎么选?人眼能感知的临界闪烁频率大概在50Hz左右,LED数码管的扫描频率一般不低于60Hz。六位扫描,每位点亮1ms的话,一轮是6ms,每秒约167轮,完全够用。如果把每位点亮时间拉长到2ms,一轮12ms,整体约83Hz,也能接受。再慢就会出现明显的闪动,拍照时尤其明显。实践中我习惯把扫描时钟分频到1kHz,也就是每毫秒切换一位,既简单又稳定。
扫描显示的代价是平均亮度只有常亮的1/6,但数码管本身亮度余量大,人眼感觉不明显。另一个代价是切换瞬间会发生“拖影”:位选已经切到下一位,但段码还是上一位的数字,会短暂点亮一个错误的位置。这个问题的解决手段叫消影,后面代码里会落到实处。
3.2 扫描模块代码:共阴共阳怎么改
module seg_scan ( input wire clk, // 1kHz 扫描时钟 input wire rst_n, input wire [23:0] bcd_in, // 六位BCD:{时十,时个,分十,分个,秒十,秒个} output reg [5:0] sel, // 位选 output reg [7:0] seg // 段选 ); reg [2:0] pos; // 当前扫描位置 0~5 reg [3:0] cur_digit; reg [7:0] seg_code; // 扫描位置计数器 always @(posedge clk or negedge rst_n) begin if (!rst_n) pos <= 3'd0; else if (pos == 3'd5) pos <= 3'd0; else pos <= pos + 1'b1; end // 根据位置取出对应的BCD位 always @(*) begin case (pos) 3'd0: cur_digit = bcd_in[3:0]; 3'd1: cur_digit = bcd_in[7:4]; 3'd2: cur_digit = bcd_in[11:8]; 3'd3: cur_digit = bcd_in[15:12]; 3'd4: cur_digit = bcd_in[19:16]; 3'd5: cur_digit = bcd_in[23:20]; default: cur_digit = 4'd0; endcase end // 位选:低电平有效(共阴极板常见接法) always @(*) begin case (pos) 3'd0: sel = 6'b011111; 3'd1: sel = 6'b101111; 3'd2: sel = 6'b110111; 3'd3: sel = 6'b111011; 3'd4: sel = 6'b111101; 3'd5: sel = 6'b111110; default: sel = 6'b111111; endcase end // 七段码译码表(共阴极,段码高电平点亮) always @(*) begin case (cur_digit) 4'd0: seg_code = 8'h3F; 4'd1: seg_code = 8'h06; 4'd2: seg_code = 8'h5B; 4'd3: seg_code = 8'h4F; 4'd4: seg_code = 8'h66; 4'd5: seg_code = 8'h6D; 4'd6: seg_code = 8'h7D; 4'd7: seg_code = 8'h07; 4'd8: seg_code = 8'h7F; 4'd9: seg_code = 8'h6F; default: seg_code = 8'h00; endcase end // 输出寄存器:用打一拍的方式避开组合竞争,切换瞬间自然消影 always @(posedge clk or negedge rst_n) begin if (!rst_n) seg <= 8'h00; else seg <= seg_code; end endmodule这段代码有三个地方要按你的板子改。第一是位选极性,上面的sel是低电平有效,如果你的板子是共阳极数码管或者位选用PNP三极管驱动,sel的1/0含义要反过来。第二是段码表,共阳极的0-9段码是共阴极的按位取反,比如0是8'hC0。第三是扫描时钟,clk这里应该是1kHz,需要配合第2章的pulse_gen分频产生,或者单独再写一个分频模块。
消影在代码里是怎么实现的?位选是组合逻辑即时切换,而段码经过了一拍寄存器。位选切换的那一拍,seg还维持旧值,从硬件上看就是位选和段码错开了一拍,旧段码会落在新位置上一个很短的时间。因为扫描时钟是1kHz,这一拍只有1ms,实际显示基本看不出问题。追求完美的话,可以在位选切换的边界把seg强制清零,方法是额外增加一个使能逻辑,这里不再展开。
至此显示链路通了:bcd_in来自三个计数器的ones/tens拼接,每毫秒扫描一位,反复循环。接线时建议在顶层模块用assign把bcd_in拼好,保持计数器模块不被显示模块污染。
4. 闹铃、调时与整点报时:把功能键嵌进状态机
4.1 按键消抖:20ms窗口与单脉冲产生
机械按键按下和释放的瞬间,金属触点会抖动几毫秒到十几毫秒,如果不处理,一次按下会被电路识别成多次。常见做法是检测电平连续稳定一段时间后才认为按键有效。我习惯用20ms窗口,覆盖绝大多数按键的抖动范围。
module key_debounce #( parameter CLK_FREQ = 1_000_000 // 输入时钟频率,默认 1MHz )( input wire clk, input wire rst_n, input wire key_in, // 按键输入,按下为低电平 output reg key_pulse // 按下瞬间产生的高脉冲,持续一个时钟周期 ); localparam MAX_CNT = CLK_FREQ / 1000 * 20 - 1; // 20ms 对应的时钟周期数 reg [19:0] cnt; reg key_stable; // 电平稳定判断 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 20'd0; key_stable <= 1'b1; end else if (key_in == key_stable) begin cnt <= 20'd0; // 状态没变化,计数清零 end else begin cnt <= cnt + 1'b1; if (cnt >= MAX_CNT) begin key_stable <= key_in; // 20ms 后仍处于相反电平,认为稳定 cnt <= 20'd0; end end end // 边沿检测:key_stable 从 1 变 0,说明按键按下 reg key_stable_d; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_stable_d <= 1'b1; key_pulse <= 1'b0; end else begin key_stable_d <= key_stable; key_pulse <= key_stable_d & ~key_stable; end end endmodule这个模块输出的是单周期脉冲,不是电平。按键按住不放,只在按下沿产生一个脉冲,适合“按一下加一”的调时逻辑。如果你想要长按连续加,可以再加一个计数逻辑:超过1秒后每隔200ms再产生一个脉冲,这个留作扩展。注意CLK_FREQ参数,如果直接用50MHz给这个模块,MAX_CNT会变成一百万,20位计数器刚好够;我把时钟频率设计成可配置的,仿真时可以降到1kHz加速跑。
4.2 调时状态机:mode键循环切换可调对象
一个不带状态机的时钟,功能键多了就乱套。我常用的状态划分是五态:正常显示、调时、调分、调秒、设闹铃。mode键每按一次切到下一个状态,add键在当前状态下把对应寄存器加1,还能顺手处理闹铃的时和分设定。
localparam S_NORMAL = 3'd0; // 正常走时 localparam S_SET_H = 3'd1; // 调时 localparam S_SET_M = 3'd2; // 调分 localparam S_SET_S = 3'd3; // 调秒 localparam S_SET_ALM= 3'd4; // 设闹铃 reg [2:0] state; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S_NORMAL; else begin case (state) S_NORMAL: if (mode_pulse) state <= S_SET_H; S_SET_H: if (mode_pulse) state <= S_SET_M; S_SET_M: if (mode_pulse) state <= S_SET_S; S_SET_S: if (mode_pulse) state <= S_SET_ALM; S_SET_ALM: if (mode_pulse) state <= S_NORMAL; default: state <= S_NORMAL; endcase end end调时动作分成两步:先决定调哪个对象,再决定加多少。对象的选中逻辑很简单,add按键的脉冲在S_SET_H状态下拉高counter24的load_en,并把当前值加1后从load_ones/load_tens送回;S_SET_S和S_SET_M同理。这一步有一个容易被忽略的细节:调秒的时候,最好能把秒计数器一次性归零,让秒从0开始走,否则要从当前秒值按一下加一秒,太费事。
闹铃设定状态同理。闹铃寄存器和走时计数器是两套寄存器,设闹铃时改的是alm_hour、alm_min、alm_sec,不影响当前走时。显示模块需要一个显示源选择:正常态显示当前时间,设闹铃态显示闹铃时间。我喜欢在顶层用一个2选1的mux,把bcd_in的来源切一下,这样显示模块完全不用改。
4.3 闹铃比较与响铃控制:精确到分钟还是秒
闹铃比较的写法取决于你想要“秒级准点”还是“分钟级准点”。精确到秒的写法是时、分、秒三组值全相等,触发点是设定的那个秒数;精确到分钟的写法是时、分相等且当前秒为0,触发点是设定分钟的第0秒。桌面闹钟大多用后者,响铃条件少一个寄存器比较,也更好解释。
// 秒级精确匹配 wire alm_match_sec = (cur_hour == alm_hour) && (cur_min == alm_min) && (cur_sec == alm_sec); // 分钟级匹配 wire alm_match_min = (cur_hour == alm_hour) && (cur_min == alm_min) && (cur_sec == 4'd0);响铃不能无限响下去,不然没人按停会一直吵。我加了一个30秒的响铃计时器:alm_match触发后拉起alm_flag,同时开始计数,30秒后自动关断;期间任意按键按下也直接关断。这里要注意alm_match在30秒窗口内会反复变化,所以用“触发后锁存”的方式处理,不能每次都重新判断。
reg alm_flag; reg [4:0] alm_cnt; // 0~30 秒 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin alm_flag <= 1'b0; alm_cnt <= 5'd0; end else if (alm_flag) begin if (any_key_pulse) begin // 任意键停止 alm_flag <= 1'b0; alm_cnt <= 5'd0; end else if (alm_cnt == 5'd30) begin // 自动停止 alm_flag <= 1'b0; alm_cnt <= 5'd0; end else begin alm_cnt <= alm_cnt + 1'b1; end end else if (alm_match_min) begin alm_flag <= 1'b1; alm_cnt <= 5'd0; end end蜂鸣器驱动要看硬件是有源还是无源。有源蜂鸣器给电平就响,可以直接用alm_flag驱动;无源蜂鸣器需要PWM方波,得另做一个分频器产生音频信号,再和alm_flag做与逻辑。上板前先查开发板原理图,确认是高电平驱动还是低电平驱动,否则很可能出现“逻辑对了声音没有”的尴尬。
4.4 整点报时:直接复用分进位
整点报时比闹铃简单得多,它不需要独立比较器。分计数器从59:59翻到00:00时产生carry脉冲,这个脉冲原本就是时计数器的使能信号,同时也标志着“整点刚刚到达”。直接在顶层把分计数器的carry引出来,作为报时的触发信号。
// 顶层接线示意 wire en_hour; counter60 u_min (... .carry(en_hour) ...); counter24 u_hour (... .en(en_hour) ...); // 整点报时:en_hour 每 60 分钟拉高一拍 assign chime_trigger = en_hour;报时的具体动作可以是让蜂鸣器响几秒、闪烁一个LED、或者驱动数码管显示整点时间。常见做法是响一秒左右的提示音,避免和闹铃响30秒重叠。如果你的报时要求是“响N次”,可以再做一个小计数器,把en_hour脉冲扩展成1秒窗口,这些逻辑都很直观。注意整点报时和闹铃可能在同一个整点撞车,设计时要先判闹铃还是先判报时,我习惯让闹铃优先,毕竟叫醒需求比仪式感重要。
5. 上板调试避坑记录:5个必踩的坑
5.1 仿真全对、上板不亮:先查复位和时钟
现象:testbench里波形完美,秒计数、进位、显示全部正常,烧到板子上数码管一个不亮,或者隔几秒才有微弱反应。
原因:最常见的是时钟频率和复位极性搞混。开发板的晶振频率不一定是50MHz,有的是12MHz、24MHz、100MHz,分频参数写错的话,秒脉冲周期会变成实测周期的若干倍,看起来就是“不走”。另外异步复位是低有效还是高有效,每个板子约定不同,复位管脚悬空或者反接会导致模块一直处在复位状态。
解决:先用最简单的blinky验证通路。写一个分频到1Hz的脉冲,直接接一个LED翻转,确认开发板时钟频率和复位极性。这一步通过了再做时钟项目。上板调试一定要留出可观测信号,仿真漂亮不是终点,LED闪烁能证明时钟树正确。下面这几条都是时钟项目里出现频率最高的翻车现场,几乎每条我都见人踩过。
5.2 数码管乱闪带拖影:刷新率和消影没做好
现象:六个数码管同时显示,但亮度不均,或者某个数字“挂”在旁边的位上,整体看有鬼影。
原因:扫描频率太低,人眼能看出每位轮流亮的节奏;或者位选切换时没有消影,旧段码残留到新位选上。还有可能是1kHz扫描时钟没分出来,直接用了系统时钟,导致每位点亮时间短到几乎不亮。
解决:第一步确认扫描时钟频率,用示波器或让扫描时钟驱动LED看看闪烁节奏。第二步确认消影,检查段码输出是否打拍,以及位选切换时是否先关闭段码。第三步尝试把每位点亮时间从1ms调整到2ms,拖影会增加但亮度更足,属于工程权衡。
5.3 秒走快或走慢:分频参数和计数器位宽
现象:和手机对表,一小时能差几秒甚至几分钟。分频参数差1,理论上每个50M周期差1个周期,一小时累计差约70微秒,肉眼不可见。真正差得多的是参数位数不够,计数到最大值就溢出翻转了,或者CNT_MAX写成了整除后的商而不是周期数减1。
原因:常见的两个错误,一是把CNT_MAX直接写成晶振频率数值,忘了减1;二是计数器位宽不够,50MHz需要26位,用了16位信号,数值到65535就溢出。两个错误都不会在仿真里暴露,因为仿真时间短、频率参数一致。
解决:把分频参数单独定义为parameter,位宽固定在26位,CNT_MAX=CLK_FREQ-1这样写。有条件的话用频率计或者逻辑分析仪量一下pulse管脚的频率,精确对时。不要靠看数码管秒表走字来估,误差太小看不见。
5.4 按键按一次跳好几个数:缺边沿检测
现象:调时的时候按一下add,数字连跳两三个,或者跳完又自己跳一下。这是按键抖动没消干净,或者是消抖输出还是电平信号,直接驱动计数器累加。
原因:消抖模块只做了电平稳定,但输出是电平而不是单脉冲。按键按住期间,add信号一直为高,只要en条件满足就会持续计数;抖动期间也可能出现多次边沿。这在仿真里很难还原,因为testbench里按键没有机械抖动模型。
解决:消抖之后一定做边沿检测,只输出一个时钟周期的脉冲。如果已经这样做了还跳,把消抖窗口从20ms加到30ms,并检查add_pulse是否被多个模块同时使用。用单脉冲驱动状态机和计数器,是这次项目里最值得养成的习惯。
5.5 闹铃不响或一直响:先查硬件驱动再查逻辑
现象:设定的时间到了,蜂鸣器没声音,或者上电就一直在响。仿真里闹铃触发一切正常,上板后表现和仿真完全对不上。
原因:蜂鸣器分有源和无源两类,有源的给电平就响,无源的必须给一定频率的方波。驱动电路如果用了三极管,还有个高电平导通还是低电平导通的问题。很多板子的蜂鸣器是低电平有效,你拿alm_flag直接驱动,逻辑上响铃条件为真反而是关闭蜂鸣器。
解决:先做一个固定频率的输出,比如直接用分频后的1kHz方波去驱动蜂鸣器,确认硬件通路和有效电平。再把alm_flag和使能结合,无源蜂鸣器需要把音频方波和alm_flag做与运算。上板调试时给闹铃设成当前时间后一两分钟,别设到几小时后,否则等出问题的耐心会耗尽。
6. 快速验证技巧:把仿真时间“压缩”到秒级
时钟项目最难受的验证点是“真实时间太长”。一个完整小时进位要3600秒,真实跑一遍谁等得起?但仿真里如果分频参数保持50MHz,模拟一天需要8.6亿个时钟周期,testbench也扛不住。解决办法在前面埋了伏笔:pulse_gen的CNT_MAX是可覆盖的parameter。
仿真testbench里例化时直接改参数,把一秒钟压缩成100个系统时钟周期,甚至10个。这样验证“08:59:59→09:00:00”只需要几十个时钟周期,整段测试瞬间跑完,波形上能清清楚楚看到个位归0、十位归0、进位拉高一拍。
pulse_gen #(.CNT_MAX(99)) u_pulse ( .clk(clk), .rst_n(rst_n), .pulse(sec_pulse) );边界用例不用穷举,盯住这几个进位点就够了:
| 用例 | 关注点 |
|---|---|
| 23:59:59 → 00:00:00 | 小时十位 2→0,跨天翻转 |
| 09:59:59 → 10:00:00 | 小时十位 0→1,首次越过 9 |
| 19:59:59 → 20:00:00 | 小时十位 1→2 |
| 08:59:59 → 09:00:00 | 小时个位 9→0,十位保持 0 |
每个边界检查都确认三件事:个位BCD不出现A-F,十位不越过5(小时十位不越过2),carry只拉高一拍。想偷懒又想严谨,可以写一个自动检查的仿真脚本,每来一个秒脉冲就比对一次BCD值是否落在合法集合里,不合法立刻报错,几秒钟跑完几万拍。我这个项目里吃过一点亏:当时只顾着手动看波形,漏测了23:59:59到00:00:00的跨天,结果上板后每到午夜时刻显示就乱一下。后来养成习惯,把边界用例写进testbench,验证做扎实了,上板才敢谈玄学。希望这个技巧帮到你。
本文还有配套的精品资源,点击获取