尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA实战:用Verilog实现多功能数字时钟(分频、动态扫描)

FPGA实战:用Verilog实现多功能数字时钟(分频、动态扫描) 简介基于FPGA实验板的多功能数字时钟设计是一份完整的Quartus课程设计资料包适合数字电子技术课程学习者、FPGA入门者及毕业设计/工程实训用户。资源围绕时钟计时、12/24小时切换、时分秒校时、秒表及暂停复位等功能展开帮助读者掌握多路选择器、N进制计数器、显示译码电路等常见模块的设计与调试方法。压缩包共358个文件约22.59MB包含51个cdb、49个hdb、31个bdf、38个jpg等Quartus工程、原理图、仿真波形和报告文档各类文件对应设计源文件、编译信息、操作截图与说明文档便于按目录查阅和复用。目前已有584人学习下载适合按项目流程从搭建工程到仿真验证逐步实践。资料还配有实验报告和完整工程文件可直接对照学习综合、布线、仿真等FPGA开发关键环节为后续数字系统设计打下基础。1. 数字时钟的设计定位为什么用 FPGA 而不是单片机一个数字时钟听起来并不复杂——无非是分频、计数、显示。但把它放在 FPGA 实验板上用 Verilog 去实现问题就变得不一样了你不再像单片机那样靠循环和中断“挤出”时间而是用纯组合逻辑和时序逻辑同时描述多个并发进程。秒计数、分计数、时计数、动态扫描显示还有按键消抖、时间校准、进位脉冲的产生这些东西在 CPU 上是一个任务排队问题在 FPGA 上是并行硬件问题。敲定这个设计既能检验你对 Quartus 工程的熟悉程度又能把同步复位、时钟使能、边沿检测、扫描显示这些基本功一次练完。这篇博文适合两类人一是正在选课程设计题目需要找一个“能讲清楚、能写报告、能上板演示”项目的在校生二是工作几年后想用一个小项目快速回忆 Quartus 开发流程、找回硬件描述语言手感的工程师。我以 Quartus II 13.1 和 Quartus Prime 16.0 及以上版本的操作为基准适配 DE2-115、国产 AX301、EP4CE6 等常见实验板给出业内最常走的实现路径和自己调试时的习惯做法。2. 模块划分与核心逻辑计数器、分频器与扫描显示FPGA 设计的第一步永远是画模块图而不是打开 Quartus 写代码。多功能数字时钟从功能上拆至少要分四块分频器、时间计数器、显示驱动、按键/功能控制。如果加了整点报时或者闹钟还要多出蜂鸣器驱动模块。模块划分得清不清楚直接决定后面的仿真激励好不好写、实验报告的小节好不好排。2.1 时钟源与分频器先解决 50MHz 到 1Hz 的问题实验板上通常只有 50MHz 或 100MHz 的有源晶振而数字时钟需要 1Hz 的秒脉冲所以第一步必须分频。分频有两条路用 PLL 硬核或者用计数器直接分频。PLL 能精确产生低频时钟但 FPGA 的 PLL 对最小输出频率有限制而且把低频信号送进全局时钟网络容易产生时钟偏斜问题。更常见的做法是保持系统时钟始终为 50MHz用一个计数器产生 1Hz 的使能脉冲clk_en只有这个脉冲为高时秒计数器才加一。这个“时钟使能”设计是课程设计的核心考点也是面试常问的。它避免了多时钟域之间同步的麻烦所有模块仍然工作在同一个 50MHz 时钟域里只是通过使能信号控制计数节奏。这样做还有一个额外好处ModelSim 仿真时波形不会出现两个频率相差悬殊的时钟读波形要容易得多。2.1.1 分频参数的计算方式与参数化写法50MHz 除以 1Hz结果是 50000000。计数器从 0 数到 49999999总共经历 50000000 个周期输出一个高电平脉冲。这个上限用 26 位二进制足够表示。写成分频模块时应该把分频系数做成可配置参数方便仿真时改小、上板时改回module clk_div #( parameter DIV_COUNT 50000000 - 1 ) ( input wire clk, input wire rst_n, output reg clk_en ); reg [25:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 26d0; clk_en 1b0; end else if (cnt DIV_COUNT) begin cnt 26d0; clk_en 1b1; end else begin cnt cnt 1b1; clk_en 1b0; end end endmodule代码里有三个关键点要读明白。第一复位使用异步复位优先级最高rst_n拉低时所有寄存器立刻清零。第二clk_en不是一个占空比 50% 的时钟它只保持一个周期的高电平其余时间都是低电平所以它是“脉冲”而不是“时钟”。第三计数到上限后立即清零并拉高使能这样每 50000000 个周期只出现一个周期的使能高脉冲。实际工程里我建议把DIV_COUNT设成参数而不是硬编码原因只有一个仿真效率。如果 50MHz 时钟跑满 50000000 个周期才能看到一次秒脉冲ModelSim 要跑一秒仿真时间耗时极长。把参数临时改成 9 或者 99仿真就能在几微秒内看到秒进位跑完整个时钟模块的验证只需要几毫秒仿真时间。提示有些同学的实验板晶振是 25MHz或者用锁相环倍频到了 100MHz参数都要相应修改。设计报告里如果写了“分频参数可配置适配不同晶振频率”是一个容易被认可的细节。2.2 时、分、秒计数器与进位逻辑同步进位计数器模块是数字时钟的核心数据通路。秒计数器是 60 进制分计数器也是 60 进制时计数器是 24 进制。三个计数器级联秒计到 59 再收到秒使能时归零同时产生一个进位脉冲送给分计数器分计数器同理产生进位给时计数器。写这类计数器有三个常见误区一是复位后初值不对上电不是从 00:00:00 开始二是把组合逻辑的输出直接接到下一级时钟端用“异步级联”的方式产生进位三是不区分 BCD 编码和二进制编码显示时还要额外做转换。2.2.1 可配置模值计数器的 Verilog 实现实际工程中不建议为秒、分、时分别写三个几乎一样的计数器模块而是写一个可配置进制的计数器用参数传递模值。同时直接用 BCD 编码方式存储计数结果高 4 位是十位、低 4 位是个位这样输出到数码管段码驱动之前不需要再做二进制到 BCD 的转换节省组合逻辑也方便阅读module bcd_counter #( parameter MAX 59 ) ( input wire clk, input wire rst_n, input wire en, // 上级进位脉冲或秒脉冲 output reg [7:0] count, // BCD: 高4位十位, 低4位个位 output reg carry ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin count 8d0; carry 1b0; end else if (en) begin if (count MAX) begin count 8d0; carry 1b1; end else begin count count 1b1; carry 1b0; end end else begin carry 1b0; end end endmodule这个模块里MAX参数是关键秒和分计数器传 59时计数器传 23。复位后count从 0 开始收到使能脉冲就加 1。当计到最大值时下一拍归零并输出carry高电平这一拍结束之后carry被拉低完成一次完整的进位握手。使用 BCD 编码后count[7:4]永远只会出现 0 到 5 的值count[3:0]永远只会出现 0 到 9 的值。如果没有用 BCD 而直接用二进制数到 59count[7:4]在 9 到 15 之间时数码管驱动会显示乱码这是很多同学上板调试时遇到“显示 9 后面跟奇怪符号”的根本原因。2.3 动态扫描显示模块为什么是 1kHz 而不是 1Hz实验板上的七段数码管通常是共阳极多位共用一个段选总线位选由三极管扩展或直接由 FPGA 引脚驱动。要让 6 位数码管“同时”显示不同的数字必须用动态扫描方式在某一时刻只点亮一位以足够高的频率轮流点亮所有位利用人眼视觉暂留现象形成稳定显示。扫描频率的选择有讲究。每一位的刷新率至少要 60Hz 才不会明显闪烁6 位轮流扫描就意味着扫描总频率至少 360Hz。工程上常用 1kHz 到 2kHz对应每位刷新率 166Hz 到 333Hz。低于 500Hz 会看到闪动高于 4kHz 时位选切换时间短如果段码建立时间不够会出现亮度不均匀。扫描显示模块的内部结构分成三部分分频器产生 1kHz 的扫描使能脉冲、一个位选计数器循环选择当前点亮位、一个数据选择器把当前位的 BCD 码取出送到段码查找表。2.3.1 段码查找表与共阳极共阴极兼容写法七段数码管的段码映射是纯组合逻辑最直接的方式是case语句。但不同实验板的数码管极性不同DE2-115 的七段数码管是共阳极低电平点亮段而部分国产板是共阴极高电平点亮段。为了代码在不同板子上都能用把极性做成参数module seg_lut #( parameter IS_COMMON_ANODE 1 ) ( input wire [3:0] bcd, output reg [6:0] seg ); always (*) begin case (bcd) 4d0: seg 7b1000000; 4d1: seg 7b1111001; 4d2: seg 7b0100100; 4d3: seg 7b0110000; 4d4: seg 7b0011001; 4d5: seg 7b0010010; 4d6: seg 7b0000010; 4d7: seg 7b1111000; 4d8: seg 7b0000000; 4d9: seg 7b0011000; default: seg 7b1111111; endcase if (!IS_COMMON_ANODE) seg ~seg; end endmodule段码表里 0 表示点亮1 表示熄灭这是共阳极数码管的编码方式。如果是共阴极就把所有位取反。参数 IS_COMMON_ANODE 的作用就是让这一个模块在不同实验板之间可移植而不用改内部逻辑。位选信号sel的编码同样与硬件有关。DE2-115 的位选是高电平有效直接用 6 位独热码有些板子用低电平有效则需要取反。建议在顶层模块里用一个简单的assign适配硬件极性不要改 RTL 核心逻辑。3. Quartus 工程搭建与 RTL 实现模块划分和逻辑设计完成后进入正式工具操作。Quartus 工程搭建的流程是固定的但几个细节没做对后面会花大量时间排查。这里以 Quartus II 13.1 为基准描述Prime 版本的菜单名称略有差异但路径一致。3.1 新建工程与器件选型打开 Quartus 后选择File → New Project Wizard。工程名不要用中文和数字开头路径也不要带中文和空格否则后面 ModelSim 仿真时工作库路径解析会出错。这一点几乎每年都有同学踩坑报的错误是Error: Cant find library但真正原因是路径编码问题。器件选型取决于实验板DE2-115 使用 Cyclone IV 的 EP4CE115F29C7国产 AX301 使用 Cyclone IV 的 EP4CE6F17C8有些老实验板用 Cyclone II 系列。注意 Quartus II 13.1 之后的 Prime 版本不再支持 Cyclone II如果使用老实验板就必须用 13.1 或更低版本。选错器件会在编译时报出器件不匹配的错误Pin Planner 里的引脚也无法正确显示。仿真工具设置在新建工程向导的EDA Tool Settings页把仿真工具选为 ModelSim-Altera语言选 Verilog。安装 ModelSim 时要注意必须安装 Altera 定制版ModelSim-Altera而不是通用版 ModelSim SE否则 Quartus 调用仿真时找不到仿真器。安装路径不要有空格建议直接装在D:\altera\modelsim_ase。3.2 分频器、计数器与顶层模块的实例化将前面写的clk_div、bcd_counter、seg_lut分别保存为独立.v文件每个文件只有一个模块文件名与模块名一致。这是 Verilog 编码规范的基本要求也让 Quartus 自动添加文件时不容易出错。顶层模块的作用是把所有子模块用结构化描述连接起来。顶层不写逻辑只做实例化和连线。整个数字时钟的顶层大致如下module top_digital_clock ( input wire clk_50m, input wire rst_n, output wire [5:0] sel, output wire [6:0] seg ); wire clk_1hz_en; wire carry_sec; wire carry_min; wire [7:0] sec; wire [7:0] min; wire [7:0] hour; clk_div #(.DIV_COUNT(49999999)) u_clk_div ( .clk (clk_50m), .rst_n (rst_n), .clk_en (clk_1hz_en) ); bcd_counter #(.MAX(59)) u_sec ( .clk (clk_50m), .rst_n (rst_n), .en (clk_1hz_en), .count (sec), .carry (carry_sec) ); bcd_counter #(.MAX(59)) u_min ( .clk (clk_50m), .rst_n (rst_n), .en (carry_sec), .count (min), .carry (carry_min) ); bcd_counter #(.MAX(23)) u_hour ( .clk (clk_50m), .rst_n (rst_n), .en (carry_min), .count (hour), .carry () ); // 显示模块动态扫描 段码查找表 wire disp_en; clk_div #(.DIV_COUNT(49999)) u_disp_div ( .clk (clk_50m), .rst_n (rst_n), .clk_en (disp_en) ); // 位选计数、BCD选择、段码输出的具体实现见下文 endmodule顶层实例化代码中每个实例的信号连接采用命名端口方式尽量不要用位置端口。命名端口的好处是即使子模块端口顺序改变只要端口名不变例化依然有效。信号命名也直接体现出数据流方向carry_sec是秒计数器的进位输出同时也是分计数器的使能输入这在读代码时一目了然。clk_div被例化了两次一次产生秒脉冲一次产生显示扫描脉冲。这两个实例的参数不同如果你把它重构成一个频率可配的模块会发现代码复用性非常好。课程设计要求“多功能”多例化一个模块不会增加多少资源但报告里可以写“扫描时钟与秒脉冲独立分频互不干扰”。3.3 显示驱动与状态控制的 RTL 设计显示驱动部分需要实现动态扫描过程一个scan_cnt计数器循环从 0 到 5选中对应位同时把该位的 BCD 码送入段码查找表。reg [2:0] scan_cnt; reg [7:0] current_bcd; reg [6:0] seg_out; reg [5:0] sel_out; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) scan_cnt 3d0; else if (disp_en) scan_cnt scan_cnt 1b1; end always (*) begin case (scan_cnt) 3d0: current_bcd sec[3:0]; 3d1: current_bcd sec[7:4]; 3d2: current_bcd min[3:0]; 3d3: current_bcd min[7:4]; 3d4: current_bcd hour[3:0]; 3d5: current_bcd hour[7:4]; default: current_bcd 4d0; endcase end seg_lut #(.IS_COMMON_ANODE(1)) u_seg_lut ( .bcd (current_bcd), .seg (seg_out) ); always (*) begin case (scan_cnt) 3d0: sel_out 6b000001; 3d1: sel_out 6b000010; 3d2: sel_out 6b000100; 3d3: sel_out 6b001000; 3d4: sel_out 6b010000; 3d5: sel_out 6b100000; default: sel_out 6b000000; endcase end assign seg seg_out; assign sel sel_out;这里的current_bcd是一个组合逻辑多路选择器根据scan_cnt当前值选出对应位的 BCD 码。位选信号sel_out也是组合逻辑采用独热码方式。两个always (*)块都是纯组合逻辑没有时钟沿综合后会变成两套多路选择器。需要特别注意的是组合逻辑输出不能有锁存器暗示。case语句必须写default分支否则综合工具会推断出锁存器编译报告里会出现Warning: Latch inferred。这个警告很容易被忽略但从综合结果来看锁存器会带来毛刺和时序问题。4. ModelSim 仿真验证与实验板上板调试代码写完之后排在烧录前面的下一步永远是行为仿真。仿真能验证计数器进位关系、显示扫描时序、复位行为是否符合预期还能在实验报告里留下波形截图。上板调试的问题大多出在引脚约束和硬件极性上仿真阶段无法覆盖。4.1 编写高效仿真激励文件用 ModelSim 做仿真有两种启动方式在 Quartus 里直接调用 RTL Simulation或者单独打开 ModelSim 手动加载文件。课程设计场景建议用前者Quartus 会自动建立工程库并调用仿真器省去很多环境配置。仿真最大的问题是时间尺度。完整工程里DIV_COUNT是 49999999如果直接仿真运行 1 秒仿真时间才能看到秒计数变化等待时间非常久。解决方法是使用defparam在 testbench 中覆盖分频参数timescale 1ns / 1ps module tb_clock; reg clk_50m; reg rst_n; wire [5:0] sel; wire [6:0] seg; top_digital_clock u_top ( .clk_50m (clk_50m), .rst_n (rst_n), .sel (sel), .seg (seg) ); // 覆盖分频参数每5个时钟周期产生一个秒使能 defparam u_top.u_clk_div.DIV_COUNT 4; defparam u_top.u_disp_div.DIV_COUNT 4; always #10 clk_50m ~clk_50m; initial begin rst_n 0; #100; rst_n 1; #5000; $display(sec %d, min %d, hour %d, u_top.sec, u_top.min, u_top.hour); $stop; end endmoduledefparam的作用是穿透模块层级直接修改实例内部参数。testbench 里把秒脉冲分频参数改成 4意味着每 5 个时钟周期产生一次秒脉冲5 秒的走时在 250ns 仿真时间内就能完成。这不是用来验证真实时间精度的而是用来验证计数器进位逻辑。验证进位逻辑时要重点观察carry_sec信号当sec从 59 跳到 00 时carry_sec应该在同一拍或者下一拍拉高一个周期。如果carry_sec持续多个周期为高分计数器会连续加多次出现“分跳变”的严重错误。4.2 引脚约束与 Quartus 编译排错仿真通过后进入引脚分配环节。打开Assignments → Pin Planner根据实验板原理图填写每个信号的引脚位置。以 DE2-115 为例时钟信号必须分配到专用时钟引脚PIN_Y2复位按键根据原理图分配数码管段选和位选按手册逐一填写。分配引脚时容易犯的一个错误是把普通 IO 信号分配到专用时钟引脚或者把一对差分信号拆成两个单端信号使用Quartus 编译时会报Error (170025)。另一个常见错误是引脚分配了同一个编号报Error (12006)两个信号不能使用同一个引脚。完整的编译应该在Processing → Start Compilation中运行。编译完成后查看Compilation Report重点关注两个部分一是 Fitter 报告里没有违规的 Fan-out 或时序路径二是资源占用率。数字时钟六个模块加显示扫描在 EP4CE6 上资源占用不到 5%如果资源占用超过 20%说明综合结果里面有意外生成的锁存器或者冗余逻辑。4.3 下载烧录与常见硬件故障定位编译成功后用Tools → Programmer下载.sof文件到实验板。下载前确认 JTAG 模式选的是 JTAG而不是 Active Serial。对于课程设计每次上电重新下载可以接受如果做成了最终作品应该把配置文件转换为.jic烧写到配置芯片里这样上电即运行。上板后最常见的现象是时间不走。排查步骤用示波器或者逻辑分析仪看clk_1hz_en是否正常如果实验板没有引出测试点就检查晶振和复位引脚如果数码管完全无显示先检查位选信号有没有接反再检查段码极性参数设置。另一个常见问题是 6 位数码管显示错乱比如秒钟十位走到了 9 以上。这个问题像前文所述大概率是计数器没有用 BCD 编码二进制 10 在 BCD 编码里被seg_lut的 default 分支映射成了全灭于是出现“秒个位正常、十位时不时灭一段”的诡异现象。5. 课程设计里的隐藏加分项按键消抖、校准模式与上板验证技巧实验报告里的“功能扩展”章节是最能拉开分数差距的地方。基础时钟功能人人都能做但能从“能走”做到“能用”靠的是按键消抖、时间校准和边界条件的验证技巧。这一部分是把设计的可用性和工程素养体现出来的关键。按键在 FPGA 上的核心问题是机械抖动。不消抖的按键直接接计数器使能按一下可能产生几十毫秒的随机抖动出现“按一次加了 3 秒”甚至“按一次没反应”的情形。专业做法是采样加计时对按键电平连续采样 20ms只有 20ms 内电平保持不变才认为按键按下有效module key_debounce #( parameter DELAY_CNT 1000000 ) ( input wire clk, input wire key_in, output wire key_pulse ); reg [19:0] cnt; reg [1:0] key_sync; reg key_prev; wire key_stable; // 两级同步寄存器消除亚稳态 always (posedge clk) begin key_sync[0] key_in; key_sync[1] key_sync[0]; end // 20ms 稳定计时 always (posedge clk) begin if (key_sync[0] ! key_sync[1]) cnt 0; else if (cnt DELAY_CNT) cnt cnt 1b1; end assign key_stable key_sync[1] (cnt DELAY_CNT); // 输出单周期脉冲 always (posedge clk) begin key_pulse key_stable ~key_prev; key_prev key_stable; end endmodule这个模块里key_sync是两级触发器第一级消除亚稳态第二级给后续逻辑提供干净信号。cnt计数到DELAY_CNT表示按键电平稳定。key_stable拉高后key_pulse在一个时钟周期内产生高脉冲完成“边沿检测”。DELAY_CNT的计算方式是 20ms 除以时钟周期50MHz 下是 1000000仿真时可以改小。校准模式的状态机是另一个加分点。用key_set在正常运行、校准分钟、校准小时三种状态之间切换用key_add在选中状态下加一。状态机应该用参数化定义便于波形调试中显示状态名字。上板验证有一个高效技巧用defparam在综合前把分频参数改为 500让时钟走秒速度加快 100000 倍。这样只需要几十秒就能观察小时进位是否正确不需要真正等一小时。验证结束后改回 49999999 再重新编译这不影响最终功能但能大幅压缩调试验证周期。课程设计报告里把这个“加速验证”的方法写上体现的正是工程效率意识比大篇幅贴代码更有说服力。本文还有配套的精品资源点击获取
返回列表