ARTICLE DETAIL

资讯详情

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

EDA(2)多功能数字钟:Verilog时钟树、消抖与动态扫描实战

EDA(2)多功能数字钟:Verilog时钟树、消抖与动态扫描实战 简介这份资源是南京理工大学EDA(2)课程的多功能数字钟实验报告面向电子信息、电子工程等专业正在学习数字电路与EDA设计的学生。报告以QuartusII7.0为工具围绕24小时计时、保持、清零、快速校时校分、整点报时及动态显示等功能的实现展开既可作为课程实验的参考范例也适合自学Verilog/原理图设计的读者对照练习。资源包内共1个doc文档约688KB内容涵盖设计要求、实验原理、各子模块电路设计、总电路图、电路下载与实验感想等章节其中脉冲发生电路、计时电路、清零电路、校时校分电路、保持电路、整点报时电路与译码显示电路均单独成节附有波形图仿真验证过程与问题解决记录结构完整、层次清晰。目前已有302人学习下载便于读者借鉴模块划分思路、比对仿真波形并积累排错经验。1. EDA(2)实验报告里多功能数字钟真正被考的是什么很多人做这块题目的顺序是反的先把数码管点亮、看到数字在跳再回头补分频和消抖答辩时被问一句「1Hz 是怎么来的」「按键抖动的几个毫秒怎么处理」就卡住。多功能数字钟在 EDA 课程设计里的定位其实是一块综合板——时基分频、BCD 级联计数、按键消抖、数码管动态扫描、校时状态机、整点报时与闹钟比较最后全部收敛到一颗 CPLD 或 FPGA 里用同一套时钟跑顺。它考的不是「做个钟」而是把数字系统设计的几个必经环节串起来时钟树怎么分、时序逻辑怎么写才不会跑飞、人机接口按键和数码管怎么跟 1Hz 的慢时基共存。EDA(2) 这类实验报告的技术含量七成压在这三件事上剩下三成才是显示效果好不好看。适合三类人正在跟 EDA(2) 课程设计、要把实验报告写成能复现的文档的同学在准备蓝桥杯 EDA 赛道、需要在有限时间里把功能做全的选手以及刚上手 Verilog、想找一个规模适中的完整项目练手的工程师。下面按「定向时钟树 → 计数与校时 → 仿真与上板 → 落到 PCB」的顺序走一遍。2. 多功能数字钟的时钟树从 50MHz 分频出 1Hz 与 1kHz多功能数字钟最容易翻车的地方不是计数器写错而是时钟树一开始就设计歪了。开发板上给的是 50MHz 或 24MHz 的板级时钟而计时需要 1Hz、数码管动态扫描需要几百 Hz 到 1kHz 的刷新节拍、按键消抖需要毫秒级的时间窗。这几路节拍必须从一个源时钟派生出来否则后面每加一个功能都要动一次全局时钟。2.1 为什么用「单周期使能」而不是分频时钟新手最常见的写法是reg clk_1hz; always (posedge clk_50m) clk_1hz ~clk_1hz;然后拿clk_1hz去当时钟always (posedge clk_1hz)。这在仿真里看着能跑上板后遇到时序约束、时钟树资源分配就开始出问题——FPGA 的全局时钟网络数量有限随手派生出来的时钟会被工具塞进普通布线资源偏斜大、容易出亚稳态。我一般的做法是全程单时钟分频器只产生宽度为一个源时钟周期的使能脉冲en_1hz所有时序逻辑都挂在clk_50m上用if (en_1hz)控制节奏。这样整份设计只有一个时钟域约束简单SignalTap 抓波形也直观。提示如果非要用派生时钟至少用 PLL 的专用输出并把它写进 SDC 约束。手写的分频reg当时钟是答辩里最容易被追问的点。2.2 参数化分频器的 Verilog 实现把分频系数做成参数是为了让同一份代码既能跑 50MHz 的开发板也能在仿真里把系数改小、几微秒跑完一天。// clk_gen.v —— 单周期使能式分频全程单时钟域不生成门控时钟 module clk_gen #( parameter integer CNT_1HZ 50_000_000, // 50MHz / 50M 1Hz计时节拍 parameter integer CNT_1KHZ 50_000, // 50MHz / 50k 1kHz扫描节拍 parameter integer CNT_1MS 50_000 // 50MHz / 50k 1ms消抖基准 )( input wire clk_50m, input wire rst_n, output reg en_1hz, output reg en_1khz, output reg en_1ms ); reg [25:0] cnt_hz; reg [15:0] cnt_khz; reg [15:0] cnt_ms; // 1Hz计满拉高一个源时钟周期只当使能用绝不当 always 的时钟 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_hz 26d0; en_1hz 1b0; end else if (cnt_hz CNT_1HZ - 1) begin cnt_hz 26d0; en_1hz 1b1; end else begin cnt_hz cnt_hz 1b1; en_1hz 1b0; end end // 1kHz6 位数码管轮流点亮单管频率约 166Hz人眼不闪 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_khz 16d0; en_1khz 1b0; end else if (cnt_khz CNT_1KHZ - 1) begin cnt_khz 16d0; en_1khz 1b1; end else begin cnt_khz cnt_khz 1b1; en_1khz 1b0; end end // 1ms给按键消抖模块做计数时基 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_ms 16d0; en_1ms 1b0; end else if (cnt_ms CNT_1MS - 1) begin cnt_ms 16d0; en_1ms 1b1; end else begin cnt_ms cnt_ms 1b1; en_1ms 1b0; end end endmodule逻辑说明三段结构完全一样都是「计满清零 输出一个源时钟周期的高脉冲」。把参数改成CNT_1HZ 50配合CNT_1KHZ 5仿真里 100ns 就能看到秒进位省掉几十毫秒的波形。参数说明CNT_1HZ决定计时精度改错会直接导致「走快/走慢」CNT_1KHZ太小会闪、太大则每位点亮时间不足导致亮度偏低一般取 1kHz 附近CNT_1MS是消抖的时间基准不参与计时。2.3 顶层模块划分与信号连线模块粒度定得太细会让顶层连线写到手酸定得太粗又不好单独仿真。我的划分习惯是「一个功能一个模块、一个外设一个模块」顶层只做例化和连线不写任何逻辑。模块名主要输入主要输出职责clk_genclk_50m, rst_nen_1hz / en_1khz / en_1ms生成全部使能脉冲key_debounceclk, en_1ms, key_inkey_pulse消抖并输出单周期脉冲time_coreclk, en_1hz, 校时脉冲sec/min/hour BCD时/分/秒 60 进制与 24 进制计数alarm_cmp当前时间, 闹钟设定buzz_en定时比较驱动蜂鸣器seg_scanclk, en_1khz, BCD 数据seg_sel, seg_data动态扫描与段码译码top全部管脚输出只做例化连线顶层越薄出问题时定位越快。调试阶段我会在 top 里给每个子模块的输出都留一组测试管脚用逻辑分析仪或者 SignalTap 一次抓全。2.4 复位与跨时钟域校时按键不能直接进 1Hz 逻辑按键是异步输入如果用 1Hz 的使能去采它一次按下只持续几十毫秒很可能落在两次en_1hz之间被整体丢掉。常见做法是按键先在clk_50m下过两级触发器做同步再由消抖模块用 1ms 节拍确认稳定最后输出一个和clk_50m同域的单周期脉冲key_pulse。这个脉冲送到时间核心由核心自己决定是「加一小时」还是「进入设置模式」。复位同理如果是按键复位也要同步后再用且内部统一用低有效同步复位rst_n避免异步复位释放时踩在时钟沿附近。3. 计时、校时与动态扫描的 Verilog 实现时钟树搭好之后逻辑部分就变成纯粹的「写对状态转移」。这一章是本设计的肉三个模块写清楚了功能基本就齐了。3.1 秒、分、时的 60/60/24 级联 BCD 计数用二进制计数再转 BCD 是最容易想到的写法但显示时要不断做除法取余综合出来的除法器面积不小实验箱上资源紧的时候很吃亏。直接用 BCD 计数更省每一位单独维护一个 09 的寄存器进位条件是当前位为 9。// cnt60.v —— BCD 计数ones 是个位、tens 是十位carry 提前一拍给出 module cnt60 ( input wire clk, input wire rst_n, input wire en, // 1Hz 使能或上一级进位 input wire load, // 校时装载 input wire [7:0] load_val, // {tens, ones} output reg [3:0] ones, output reg [3:0] tens, output wire carry ); // 组合逻辑给出进位供上一级在同一拍判断 assign carry (tens 4d5) (ones 4d9) en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ones 4d0; tens 4d0; end else if (load) begin tens load_val[7:4]; // 校时直接写入 ones load_val[3:0]; end else if (en) begin if (ones 4d9) begin ones 4d0; if (tens 4d5) tens 4d0; // 59 - 00同时 carry 拉高 else tens tens 1b1; end else begin ones ones 1b1; end end end endmodule逻辑说明load优先级高于en这样校时按键按下时不会和正常计时打架。参数说明60 进制靠tens 4d5 ones 4d9判定24 小时制不能直接复用这个模块——需要单独写一个cnt24进位条件改成tens 2 ones 3并且小时十位只能是 0/1/2、个位在十位为 2 时到 3 就回零。很多人图省事把 24 进制硬塞进 60 进制模块里改常数最后小时位会跑到 29这是实验报告里最典型的错误。3.2 按键消抖与校时状态机机械按键按下和松开时会有 520ms 的抖动直接进计数器会一次加好几位。消抖思路是先同步、再计数稳定、再输出单周期脉冲。// key_debounce.v —— 两级同步 持续稳定 20ms 才认账 module key_debounce #( parameter integer STABLE_MS 20 // 需要稳定 20 个 1ms 节拍 )( input wire clk, input wire rst_n, input wire en_1ms, // 来自 clk_gen input wire key_in, // 原始按键低有效 output reg key_pulse // 稳定按下时输出一个 clk 周期脉冲 ); reg key_s0, key_s1; // 同步链 reg [4:0] cnt; // 20 够用5 位足够计到 31 reg key_stable; always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_s0 1b1; key_s1 1b1; end else begin key_s0 key_in; // 第一级抗亚稳态 key_s1 key_s0; // 第二级给后级用 end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 5d0; key_stable 1b0; key_pulse 1b0; end else if (en_1ms) begin if (key_s1 1b0) begin // 采样到按下 if (cnt STABLE_MS - 1) begin cnt cnt; // 保持 key_stable 1b1; end else cnt cnt 1b1; end else begin cnt 5d0; // 松开立即清零 key_stable 1b0; end // 只在「上一次未稳定、这一次稳定」时给脉冲保证一次按下只加一位 key_pulse (key_s1 1b0) (cnt STABLE_MS - 1) !key_stable; end else key_pulse 1b0; end endmodule逻辑说明key_s0/key_s1是典型的两级同步器把异步按键拉进clk_50m域cnt只在按下时累加计满 20 个 1ms 才认为稳定key_pulse的判定里带了!key_stable避免按住不放时每个时钟周期都吐脉冲。参数说明STABLE_MS取 20 对应 20ms如果实验箱按键特别硬可以调到 30调太小会一次跳两位调太大会感觉「按了没反应」20 到 30 之间比较合适。校时状态机本身很简单四个状态运行、设时、设分、设秒或者三个用按键脉冲做state state 1被编辑的那一位用闪烁提示——把该位对应的en_1khz取反后与段码做与运算就能做出 1Hz 闪烁效果。3.3 数码管动态扫描与段码译码6 位数码管共用同一组数据线靠位选信号轮流点亮。刷新节拍用前面生成的en_1khz实际每位的点亮时长是 1ms6 位扫完一轮 6ms刷新率约 166Hz。显示位段码来源段码示例共阴dp 未点亮0时十位0x3F显示 01时个位0x06显示 12分十位0x5B显示 23分个位0x4F显示 34秒十位0x66显示 45秒个位0x6D显示 5// seg_scan.v —— 6 位动态扫描位选高有效段码共阴 module seg_scan ( input wire clk, input wire rst_n, input wire en_1khz, input wire [23:0] disp_val, // 6 位 × 4bit 的 BCD output reg [5:0] seg_sel, // 位选one-hot output reg [7:0] seg_data // {dp,g,f,e,d,c,b,a} ); reg [2:0] idx; // 0~5 当前点亮位 reg [3:0] cur_bcd; reg [7:0] seg_code; // 段码查表用 case 综合成 LUT比除法取余省资源 always (*) begin case (cur_bcd) 4d0: seg_code 8h3F; 4d1: seg_code 8h06; 4d2: seg_code 8h5B; 4d3: seg_code 8h4F; 4d4: seg_code 8h66; 4d5: seg_code 8h6D; 4d6: seg_code 8h7D; 4d7: seg_code 8h07; 4d8: seg_code 8h7F; 4d9: seg_code 8h6F; default: seg_code 8h00; endcase end // 1kHz 换位轮流转 always (posedge clk or negedge rst_n) begin if (!rst_n) idx 3d0; else if (en_1khz) idx (idx 3d5) ? 3d0 : idx 1b1; end // 按 idx 取当前位的 BCD并译码到位选 always (*) begin case (idx) 3d0: begin cur_bcd disp_val[23:20]; seg_sel 6b000001; end 3d1: begin cur_bcd disp_val[19:16]; seg_sel 6b000010; end 3d2: begin cur_bcd disp_val[15:12]; seg_sel 6b000100; end 3d3: begin cur_bcd disp_val[11:8]; seg_sel 6b001000; end 3d4: begin cur_bcd disp_val[7:4]; seg_sel 6b010000; end default: begin cur_bcd disp_val[3:0]; seg_sel 6b100000; end endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) seg_data 8h00; else seg_data seg_code; // 段码与位选同拍输出 end endmodule逻辑说明idx每 1ms 递增seg_sel用 one-hot 直接给出不需要额外译码器段码用case查表综合器会把它映射成小 LUT比用%和/做 BCD 转换省很多资源。参数说明共阴和共阳的段码表是反的共阳 0 显示 0xC0实验箱上是哪种一定要先确认接反会出现「该亮的不亮、不该亮的全亮」。disp_val的位宽按 6 位 × 4bit 定义实际只显示 时分秒 的话也可以只接 24 位。4. 仿真验证与上板调试Testbench、SignalTap 与引脚约束代码写完到上板之间有两件事必须做一是仿真里把一天跑完二是约束文件写对。跳过前者你会在实验箱前花两小时找那个本该五秒发现的问题。4.1 一个能自动跑完 24 小时翻转的 Testbench常规做法是把分频参数改小让仿真能在几百微秒内跑完一整天CNT_1HZ 10、CNT_1KHZ 2、CNT_1MS 2这样「1 秒」在仿真里只有 200ns。timescale 1ns/1ps // tb_time_core.v —— 用缩小参数跑完 23:59:59 - 00:00:00 翻转 module tb_time_core; reg clk 0; reg rst_n 0; reg en_1hz 0; wire [3:0] s_ones, s_tens, m_ones, m_tens, h_ones, h_tens; wire carry_s; // 200ns 一个「秒」模拟缩小后的使能节拍 always #5 clk ~clk; always (posedge clk) begin if (!rst_n) en_1hz 1b0; else begin en_1hz ~en_1hz; // 每 10ns 出现一次等效 20ns 周期 end end time_core u_core ( .clk(clk), .rst_n(rst_n), .en_1hz(en_1hz), .load_h(1b0), .load_m(1b0), .load_s(1b0), .load_val(8d0), .s_ones(s_ones), .s_tens(s_tens), .m_ones(m_ones), .m_tens(m_tens), .h_ones(h_ones), .h_tens(h_tens), .carry_s(carry_s) ); integer i; initial begin #100 rst_n 1; // 直接预置到 23:59:50 太麻烦用循环跑到翻转 for (i 0; i 90000; i i 1) (posedge en_1hz); // 观察 h_tens/h_ones 是否从 2/3 回到 0/0 #1000 $stop; end endmodule逻辑说明en_1hz用posedge触发循环比数时钟周期更贴近真实行为for循环跑 90000 次覆盖 25 小时足以看到 24 小时回零。参数说明仿真里把三个分频参数都设成个位数千万不要用真实的 50_000_000否则仿真会卡住断言的重点是h_tens只能出现 0/1/2h_ones在h_tens 2时只能到 3。4.2 引脚约束与管脚分配Quartus 用.qsfVivado/ISE 用.xdc/.ucf写法不同但内容一样把顶层端口绑到物理管脚把时钟周期告诉工具。# clock.qsf —— Quartus 约束片段管脚号按实际开发板替换 set_global_assignment -name FAMILY Cyclone IV E create_clock -name clk_50m -period 20.000 [get_ports {clk_50m}] derive_pll_clocks derive_clock_uncertainty # 时钟与复位 set_location_assignment PIN_E1 -to clk_50m set_location_assignment PIN_M15 -to rst_n # 数码管位选与段选示意务必对照实验箱原理图 set_location_assignment PIN_A3 -to seg_sel[0] set_location_assignment PIN_A4 -to seg_sel[1] set_location_assignment PIN_A5 -to seg_sel[2] set_location_assignment PIN_A6 -to seg_sel[3] set_location_assignment PIN_A7 -to seg_sel[4] set_location_assignment PIN_A8 -to seg_sel[5] set_location_assignment PIN_B3 -to seg_data[0]逻辑说明create_clock这句比所有管脚分配都重要缺了它工具不知道时钟频率时序报告全是红的或者干脆不报。参数说明-period 20.000对应 50MHz24MHz 要改成 41.667管脚号必须和实验箱原理图一一对应把段选接到位选上现象是「整屏一起闪」而不是逐位扫描。上板前值得过的清单时钟约束写了没有、复位是低有效还是高有效、数码管是共阴还是共阳、按键是上拉还是下拉。这四项错一个现象都很像「板子坏了」。4.3 上板四类异常与排查顺序全灭先看时钟约束和管脚是否绑对再用示波器量位选脚有没有 1kHz 的方波。没有方波说明扫描模块没跑起来多半是复位一直有效。全亮且不扫典型是段码和位选搞反或者共阴共阳接错。把段码表整体取反先试一遍能排除一半。走快或走慢CNT_1HZ和板级时钟频率对不上。24MHz 的板子用了 50MHz 的参数秒会走得飞快反之会慢得看不出来。用手机秒表对 60 秒核一次比盯着看靠谱。按键一次跳好几位或没反应消抖窗口太大或太小或者按键极性搞反上拉按键低有效被当成高有效采。先把消抖模块的key_pulse单独引到一个 LED 上验证确认每次按下只闪一下再接到时间核心。5. 从 FPGA 验证到嘉立创EDA画数字钟板卡的进阶技巧跑通仿真、上板显示正常之后很多同学会把功能再往前推一步——做一块自己的数字钟 PCB把 FPGA/CPLD 换成单片机或者干脆用分立逻辑这时候 EDA 工具就从 Quartus 换成了嘉立创 EDA 这类原理图加 PCB 一体的工具EDA 布局、引脚与焊盘对应、位号图这些问题会集中冒出来。5.1 原理图阶段的封装核对与网表联动从 HDL 转到 PCB第一个坑不是布线而是元件封装。数码管的引脚编号在不同厂家之间不统一常见的有 10 脚双排和 12 脚双排段码顺序也不一样。嘉立创 EDA 里放好符号之后一定要点开封装看焊盘编号符号的A/B/C...和封装的1/2/3...必须映射正确否则会出现「eda 元件的引脚与焊盘未对应」这类问题——现象是原理图完全正确PCB 里网络却是乱的。原理图画完后用「设计 → 更新/转换到 PCB」把网表同步过去之后回到原理图改网络名PCB 那边要用同样的入口再同步一次不要手动改 PCB 上的网络名否则两边会不一致后面查错很痛苦。给数码管位选加驱动三极管时基极电阻的位置尽量靠近三极管这一段是低电平有效还是高电平有效要和 HDL 里的极性保持一致。5.2 六层板还是两层板扫描电流回路与星型接地的取舍数字钟这种规模的板子两层板就够了。是否上六层板判断标准不是「看起来高级」而是有没有高速信号和密集的电源分割。动态扫描的电流集中在位选线上6 位数码管峰值电流可能到几百毫安重点是给每条位选线足够的线宽1A 大约需要 20mil 以上按实际电流折算以及让扫描电流的回流路径尽量短、尽量贴着信号线走。嘉立创 EDA 里做接地小信号部分晶振、复位、按键尽量集中到一块区域用大面积铺铜并在上面打过孔到背面地平面形成就近回流大电流的数码管驱动回路单独走最后在电源入口处汇合这就是常说的星型接地的实际做法。如果板子只有两层、铺铜完整不必强求严格的分区把回流路径画短比画漂亮更重要。电源入口加一颗 100uF 电解并一颗 0.1uF 瓷片数码管附近再各加一个 0.1uF能压掉不少扫描带来的电源纹波。5.3 位号图导出与实物比对样板回来之后焊接前的最后一步是对位号。嘉立创 EDA 里可以导出位号图装配图把元件位号、极性标记、一脚位置都打出来和实物板逐块比对电解电容的正极、数码管的方向、排针的一脚。焊接完成后先不上电用万用表量电源和地之间的电阻确认没有短路再上电。调试阶段建议把按键、蜂鸣器、数码管的位选各留一个测试点用示波器或者逻辑分析仪直接夹比一个个拔元件快得多。走时不准的话把晶振附近的负载电容按 10pF 到 22pF 之间微调同时检查晶振下方是不是被铺铜包住了——晶振底下要挖空否则寄生电容会让频率偏掉一天差出十几秒都算正常范围。本文还有配套的精品资源点击获取
返回列表