ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Quartus II的FPGA简易时钟设计:Verilog分频与数码管显示实现

2026/9/1 7:45:43 拓冰建站 浏览量
基于Quartus II的FPGA简易时钟设计:Verilog分频与数码管显示实现 简介面向计算机组成原理课程的FPGA大作业这套基于Quartus II设计的简易时钟工程包完整覆盖从HDL编写到下载验证的实践链路。资源以小时、分钟、秒钟三级显示为框架采用模24与模60同步计数器实现进位逻辑并附带编译、仿真、适配等过程产生的工程文件适合数字逻辑与计算机硬件方向学生对照学习。包内共61个文件压缩包约194KB主要包含bdf顶层原理图、qsf/qpf工程配置、vwf仿真波形、rpt与summary报告、qmsg编译信息及hdb/cdb中间数据库文件便于复现整个设计流程并查看时序与适配结果。目前已有1584人学习说明该案例在教学场景中具有较好的参考价值。通过打开工程学习者既能查看计数器模块的HDL实现也能从仿真波形、适配报告中理解分频、进位与显示译码的配合关系还能掌握Quartus II的项目组织方式与FPGA设计的基本排错思路是一份贴近课程要求、可直接运行和改写的完整样例。 计算机组成原理的大作业十个组里有八个组在做时钟这话一点都不夸张。而用Quartus II做简易时钟几乎是数字逻辑和组成原理课程的经典保留项目。我当年做这个题目的时候最开始也是一头雾水觉得“时钟”不就是显示时间吗有什么难的真正动手才发现从晶振的50MHz信号折腾出秒脉冲再到时分秒的进位和数码管扫描显示每一步都在考察你对时序逻辑的理解。这篇博客就把我当时做这个项目的完整思路、模块划分、Verilog代码思路和踩过的坑全部梳理出来给正在为这个作业头秃的同学参考。1. 项目概述与整体设计思路1.1 大作业到底在考察什么先别急着打开Quartus II写代码想清楚这门课的老师想让你从大作业里学会什么比闷头干重要得多。计算机组成原理这门课核心讲的是CPU怎么工作、数据怎么流动、控制信号怎么产生。但所有这些东西落到物理实现上都离不开一个基础——时序逻辑。时钟信号是整个数字系统的“心跳”寄存器靠它更新状态机靠它跳转CPU里的PC计数器靠它加一。而“简易时钟”这个题目恰好把时序逻辑里最核心的几个知识点全串起来了分频、计数、进位、译码、显示。老师出这个题不是真缺一个看时间的钟而是想看到你对以下几个概念的理解时钟分频板载晶振给的信号频率太高怎么得到1Hz的秒脉冲。模N计数器秒和分是六十进制小时是二十四进制怎么写归零和进位。译码显示BCD码或二进制怎么转换成数码管的段码。动态扫描为什么六个数码管可以只用一组段选线靠什么让它们“同时”亮。模块化设计分频、计数、显示各自独立成模块最后在顶层例化连接。把这些点吃透了不仅大作业能拿高分后续学到单周期CPU、状态机设计时你会觉得顺很多。很多学软件的同学觉得计算机组成原理没用但只要你做过一次这种硬件描述的项目再看“时钟周期”“中断”“寄存器”这些概念时感受完全不一样。1.2 功能需求拆解与技术选型我当时的任务要求并不复杂总结下来就三点正常走时上电后从00:00:00开始每秒自动加一。时间可调通过按键可以分别调整小时、分钟和秒或者至少能校时。数码管显示六位数码管分别显示时、分、秒。这个功能集看起来简单实际做的时候要选的技术路线却有好几条。我当时在方案上纠结了挺久这里分享下我的选型思路。先说开发工具。Quartus II是Intel原Altera官方出的FPGA开发软件学校里配套的FPGA开发板绝大多数是Cyclone系列所以用Quartus II基本是没得选的。版本上有人用13.1有人用15.0还有人用更新的版本。我的建议是能用13.1或15.0就别追求新版这两个版本稳定网上教程多破解教程一搜一大把而且对老开发板的器件支持很全。我有同学装了最新版Quartus结果发现板子上的Cyclone IV器件型号在器件列表里反而找不到折腾半天。再说开发方式。Quartus II支持原理图输入和Verilog/VHDL硬件描述语言输入两种方式。原理图输入对新手很友好逻辑门、触发器、计数器都是现成的元件库拖出来连上线就行。但缺点也很明显一旦要改功能或者排查问题那叫一个痛苦。我亲眼见过室友用原理图搭的时钟一根线没连对仿真波形怎么都不对在密密麻麻的连线里找问题找到崩溃。Verilog则干净得多。每个功能模块就是一个文件逻辑清晰改起来容易仿真和调试也方便。更重要的是用Verilog写的模块化代码后期如果想拓展功能比如加闹钟、倒计时往顶层加例化就行。所以我的结论很明确除非老师强制要求原理图否则一定用Verilog。这是个能让你少吃很多苦的选择。2. 核心原理从晶振到时间显示2.1 分频50MHz到1Hz的距离开发板上一般有一颗50MHz的有源晶振也就是说FPGA的全局时钟引脚上每秒会来50,000,000个上升沿。对CPU来说这个速度刚合适但对我们做时钟来说太块了——我们需要的是每秒钟只来一个脉冲的信号。这里就要用到分频了。分频的思路很朴素用计数器数晶振的脉冲数够5000万个就输出一个脉冲然后清零重新数。分频系数的计算公式很简单N F_clk / F_out也就是 N 50,000,000 / 1 50,000,000。那计数器位宽需要多少我们需要能数到50,000,000而2^25 33,554,432不够2^26 67,108,864够了。所以计数器最少得26位也就是用一个26位的寄存器做分频计数。我当时第一次写到这里就踩了个坑寄存器位宽只给了25位结果仿真时发现分频输出隔很久才翻转一次当时还没意识到是位宽不够以为是代码逻辑问题查了半天。后来算了一下才反应过来N50,000,000超出了25位能表示的最大值。这个细节看起来小但真的会让你的时钟“走不准”。分频的实现方式有两种常见写法一种是计数到N-1时清零同时产生一个高电平脉冲另一种是计数到N/2-1时让输出翻转这种主要用于产生50%占空比的对称方波做秒脉冲的话用第一种就行我们只需要每个周期有一个时钟宽的脉冲来触发秒计数器不需要50%占空比。2.2 六十进制和二十四进制的进位逻辑有了1Hz的秒脉冲接下来就是让计数器动起来。秒计数器是六十进制但我们不能直接用一个可以数到59的计数器就完事——因为后面要显示而数码管一位只能显示0到9。所以常规做法是把“秒”拆成两个数字个位和十位。个位是十进制0到9十位是六进制0到5。当个位到9后再来一个秒脉冲个位归零十位加一。当个位等于9且十位等于5时说明当前是59秒下一秒秒整体归零同时向分钟进位。这个过程在数字电路里本质上就是一个状态机只是状态比较规整用两个计数器拼接起来就实现了。你可以把它理解成一个“逢十进一”加“逢六进一”的复合进位关键是搞清楚什么时候产生进位信号。分钟的进位数逻辑和秒一模一样的毕竟分也是六十进制。小时则是二十四进制只有小时那个模块的规则不同小时个位是十进制0到9小时十位是0到2而且十位为2时个位最大只能到3对应23点。也就是说小时计数器的归零条件是“十位为2且个位为3”。这里有个新手容易犯的错把小时的逻辑想得太简单直接让十位在0到2之间循环个位在0到9之间循环结果会出现24点、25点这种不存在的时刻。归零条件必须写成hour_ten 2 hour_unit 3。2.3 数码管动态扫描一个人眼欺骗术时钟做完了怎么显示出来六个数码管如果每个都单独接一组段选线那FPGA的引脚根本不够用。所以实际电路里几乎都采用动态扫描的方式。动态扫描的原理特别像人眼的视觉暂留在任何时刻六个数码管里其实只有一只是在亮的但是我们让六只管子以极快的速度轮流点亮比如先亮第1只显示小时的十位过1ms后切换到第2只显示小时的个位再过1ms切到第3只……当这个循环跑得足够快一般刷新频率要在100Hz以上最好到200Hz人眼就分辨不出来哪只管子是“轮流亮”的看到的效果就是六只管子全部稳定显示。这里有两个参数很关键扫描周期每只管子分配多少时间。我用的是1ms六只管子一轮就是6ms刷新率约167Hz肉眼完全看不到闪烁。消隐处理如果切换位选时没有先把段码清掉前一位的数字会“拖影”到后一位看起来就是重影加亮度不均。动态扫描这个知识点在计算机组成原理里可能不会专门讲但在数字电路实验里一定会碰到。理解了它你后面学存储器刷新、DRAM的刷新机制时也会有似曾相识的感觉。3. 模块化设计与Verilog实现3.1 分频模块掐着5000万次算一秒我用Verilog写了整个工程习惯是每类功能一个文件。下面是分频模块的核心代码module clk_div_1hz( input clk_50m, input rst_n, output reg clk_1hz ); parameter N 50_000_000; reg [25:0] cnt; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt 26d0; clk_1hz 1b0; end else if (cnt N - 1) begin cnt 26d0; clk_1hz 1b1; end else begin cnt cnt 1b1; clk_1hz 1b0; end end endmodule这段代码的逻辑很简单参数N设为50,000,000计数器从0数到N-1当计满时输出一个时钟周期宽的高脉冲。注意几个细节。第一复位用了异步复位negedge rst_n低电平有效这是FPGA开发板的常见配置。第二reg [25:0] cnt是26位这是根据2^26 50,000,000算出来的。第三clk_1hz 1b0写在else分支里确保它平时是低电平只有计满的那一拍是高脉冲。我在仿真时发现一个技巧仿真50M个时钟周期来验证这个模块虽然仿真软件能跑完但实在太浪费时间了。更好的办法是先把N临时改成一个小值比如500验证分频逻辑正确后再改回50M做整体仿真。这个是实际项目中常用的加速仿真手段不是走捷径偷懒而是仿真本来就该分模块验证。3.2 时分秒计数器BCD的妙处计数器模块我用了BCD码也就是每个十进制位用4位二进制表示。为什么不用一个6位二进制数直接数到59因为后面显示的时候每一位数码管要独立译码BCD写法省去了除法取商和取余的操作电路更简洁。秒计数器部分的实现思路如下秒个位为例reg [3:0] sec_unit; reg [3:0] sec_ten; always (posedge clk_1hz or negedge rst_n) begin if (!rst_n) begin sec_unit 4d0; end else if (sec_unit 4d9) begin sec_unit 4d0; end else begin sec_unit sec_unit 1b1; end end always (posedge clk_1hz or negedge rst_n) begin if (!rst_n) begin sec_ten 4d0; end else if (sec_unit 4d9) begin if (sec_ten 4d5) sec_ten 4d0; else sec_ten sec_ten 1b1; end end这样写的妙处在于两个always块都受同一个clk_1hz驱动当秒个位从9变成0的那一拍秒十位同步加一不存在竞争冒险问题。如果你试图用组合逻辑去做进位反而容易出现毛刺。分钟的计数器写法和秒完全一致。小时的计数器需要多一个判断条件// 小时归零条件22 - 23 - 00 always (posedge clk_1hz or negedge rst_n) begin if (!rst_n) begin hour_ten 4d0; hour_unit 4d0; end else if (min_inc) begin if (hour_ten 4d2 hour_unit 4d3) begin hour_ten 4d0; hour_unit 4d0; end else if (hour_unit 4d9) begin hour_unit 4d0; hour_ten hour_ten 1b1; end else begin hour_unit hour_unit 1b1; end end end这里的min_inc是分钟计数器的进位信号也就是秒计数器出现59后产生的那个进位脉冲。这里要注意的是所有计数器模块的驱动时钟都是clk_1hz而进位信号只是作为条件判断不是作为时钟。这个设计原则是时序电路里非常重要的一个点尽量用同一个时钟沿驱动所有状态避免用进位信号当时钟否则时序分析会变得非常不可控。3.3 显示驱动段码表和动态扫描数码管显示分为两部分一是把BCD码翻译成七段码二是动态扫描。七段码的翻译用case语句实现。我用的开发板是共阴极数码管段码表如下always (*) begin case (data) 4d0: seg 7b1000000; // 0 4d1: seg 7b1111001; // 1 4d2: seg 7b0100100; // 2 4d3: seg 7b0110000; // 3 4d4: seg 7b0011001; // 4 4d5: seg 7b0010010; // 5 4d6: seg 7b0000010; // 6 4d7: seg 7b1111000; // 7 4d8: seg 7b0000000; // 8 4d9: seg 7b0010000; // 9 default: seg 7b1111111; endcase end注意段码表里的0和1不同板子可能用的是共阳极段码恰好是共阴极按位取反。所以拿到新板子第一件事是查原理图确认数码管是共阴还是共阳别想当然。动态扫描部分需要一个扫描计数器和位选信号。我习惯用一个3位的计数器在1kHz左右的时钟驱动下循环0到5依次选中第0到第5个数码管// 位选信号低电平有效共阴时通常高电平选中 always (posedge clk_1khz or negedge rst_n) begin if (!rst_n) scan_cnt 3d0; else scan_cnt scan_cnt 1b1; end always (*) begin seg_data 7b1111111; // 默认灭 sel_output 6b000000; case (scan_cnt) 3d0: begin sel_output 6b000001; seg_data display_data[0]; end 3d1: begin sel_output 6b000010; seg_data display_data[1]; end // ... 依次类推 endcase end这里我特别强调一个细节就是组合逻辑块里的默认赋值。每次切换显示位之前先把段码置成全灭也就是消隐再选中下一位然后给段码。如果不这样做扫描切换的瞬间会看到上一只数码管残留的数字表现在实际效果上就是数字有“拖影”亮度也不均匀。3.4 顶层模块把积木拼起来顶层模块做的事情就三件例化分频模块、例化计数器模块、例化显示模块把信号连起来。我的顶层大概长这样module top_clock( input clk_50m, input rst_n, input key_hour, input key_min, output [6:0] seg, output [5:0] sel ); wire clk_1hz; wire [3:0] sec_unit, sec_ten; wire [3:0] min_unit, min_ten; wire [3:0] hour_unit, hour_ten; clk_div_1hz u_clk_div( .clk_50m(clk_50m), .rst_n(rst_n), .clk_1hz(clk_1hz) ); counter_60 u_sec( .clk(clk_1hz), .rst_n(rst_n), .inc(1b1), .unit(sec_unit), .ten(sec_ten) ); counter_60 u_min( .clk(clk_1hz), .rst_n(rst_n), .inc(sec_inc), .unit(min_unit), .ten(min_ten) ); // 小时模块和显示模块略 endmodule模块化最大的好处是每个文件短小精悍出错了定位快。比如显示出问题只需要看显示模块和顶层连接不用在几千行的代码里找。4. 实操流程从新建工程到板级调试4.1 Quartus II工程创建与配置打开Quartus II之后新建工程的流程基本上是固定的File New Project Wizard填工程名和路径选器件型号。器件型号一定得跟你手上的开发板对得上比如Cyclone IV系列的EP4CE6E22C8。选错了后面编译会报一堆看不懂的错误。这里有个我印象很深的坑工程路径不能有中文也不能有空格。当时我把工程放在了“D:\大作业\clock”下面Quartus直接报错一度让我以为软件装坏了。后来改成纯英文路径才正常。这个习惯建议从第一天就养成所有跟开发相关的文件路径都保持英文能避免很多莫名其妙的问题。文件命名也建议用英文小写加下划线比如clk_div_1hz.v、counter_60.v、display.v。对初学者来说Verilog文件里模块名最好跟文件名一致这样Quartus的自动识别会更友好。版本选型方面如果你是初学者或者板子比较老13.1和15.0都是非常稳的选择。15.0在Windows 10上运行比较顺畅13.1则兼容性更强很多学校机房甚至还在用。装15.0的话注意破解步骤跟13.1略有不同用Quartus自带工具的License Setup指定好license文件就行。4.2 功能仿真上板之前的最后一道防线仿真这一步很多人会跳过尤其是赶作业的时候总觉得“代码写完直接下载到板子上不就行了”。这是大忌。板子上出了问题你只能看到现象不对但看不到内部信号排查效率极低。而仿真是白盒调试每个内部信号都能看波形逻辑对不对一目了然。我做仿真用的是ModelSimQuartus II自带集成。如果需要模拟1Hz走秒完整仿真50M个时钟周期会很慢所以我的做法是写一个单独的testbench先把分频模块的N参数临时改成10验证分频思路正确后再单独验证计数器模块。计数器模块直接用理想1Hz时钟来驱动不用经过分频这样仿真时间压缩到几毫秒之内。还有一个小技巧ModelSim里可以用force命令直接给内部信号赋初值或者用initial块把复位拉低再拉高。波形窗口里放大缩小、加信号光标这些操作做一次仿真基本上就熟练了。4.3 引脚分配与下载调试仿真通过之后再上板需要做引脚分配。不同开发板的引脚定义完全不同必须查板子附带的原理图。我拿着自己板子的原理图找到50MHz晶振接的是哪个引脚数码管的段选和位选接的是哪些引脚然后依次填入Quartus的Pin Planner。引脚分配最怕的就是手抖填错。填完引脚建议做一次全编译Quartus会重新做布线。如果有引脚冲突或者频率约束问题编译时的报告里会提示。下载的时候在Quartus里打开Programmer选择USB-Blaster下载器加载生成的.sof文件点Start等进度条走完板子上数码管应该就开始跳了。如果板子上没反应先看下载器驱动装没装好再看电源有没有给FPGA供电最后再怀疑代码。这三个因素里低级问题占了大头。5. 调试实录常见问题与排查技巧5.1 上板之后时钟不走怎么排查这是群里被问得最多的一个问题。代码编译没问题仿真也对下载到板子上一动不动。我的排查顺序是这样的第一步确认复位信号。很多开发板的按键按下是低电平复位信号要用rst_n接按键按下才复位。如果你高电平和低电平时序弄反了板子可能一直处于复位状态自然不走。最直接的验证方法是用板子上的LED灯看一个分频后的慢速信号会不会闪。第二步确认时钟引脚。50MHz晶振通常固定在某个专用时钟引脚上你在Pin Planner里填的是那个引脚吗有同学图省事随便填了个普通IO口FPGA内部的时钟网络根本进不去所有时序逻辑全废。第三步确认分频参数。如果秒表走得太快很可能是N写成了50,000而不是50,000,000或者计数器位宽不够导致溢出。这个在仿真阶段就该发现但如果你跳过了仿真直接上板就只能靠直觉去猜了。5.2 数码管显示闪烁、重影、亮度不均闪烁基本上是扫描频率太低了。每只数码管分配的时间太长刷新率掉到50Hz以下人眼就能明显感觉到闪。解决办法是把扫描时钟频率提高让每位驻留时间缩短到1ms左右。重影和亮度不均则大概率是消隐没做或者做早了。正确的时序是关闭所有位选 - 切换段码数据 - 打开目标位选。如果段码还没切好就把位选打开了就会出现上一个数字的残影。亮度不均还有一种情况某一位显示的时间比其他位长。比如扫描信号写错导致第0位被选中了2ms而其他位只有1ms那第0位就会明显更亮。5.3 按键校时不稳定去抖是必修课如果加按键校时功能十有八九会遇到按键抖动的问题。机械按键按下和松开的时候信号会来回抖动十几毫秒直接拿来当计数器的加一信号会出现按一下加好几次或者乱跳。最原始也最有效的办法是计数去抖检测到按键电平变化后用一个计数器延时20ms左右再读取一次电平如果电平稳定了才认为按键真的被按下。这个延时在1kHz时钟下就是20个周期代码量很小实测效果很好。当时我偷懒没做去抖直接在板子上按按键调小时结果按一下跳了三四次调个时间比登录系统还费劲。后来老老实实把去抖加进去世界清静了。还有一个细节如果按键是按下去拉低去抖之后要判断的是下降沿也就是“之前是高电平现在是低电平”才算一次按下而不是“现在是低电平”就算按下。很多人把电平判断当成边沿判断用结果按键按住不放时钟就一路狂跳。6. 做完这个项目的几点沉淀这个项目做完之后我个人最大的感受是纸上谈兵看再多书不如亲手跑通一次仿真和上板。分频、进位、扫描、去抖这些概念在课本上可能就几行字但你真的在波形图里看到它们如何一个节拍一个节拍地推进时理解深度完全是另一回事。如果你做完基础功能还有余力可以试一试这些扩展方向增加闹钟功能到点让蜂鸣器响这就涉及到比较电路和状态控制。增加秒表功能用另一个按键控制时钟暂停和清零背后是数据选择器的应用。把整个设计改成状态机风格体验一下Moore型和Mealy型状态机的差别这对后续学期做CPU设计有直接帮助。最后再分享一个小技巧所有工程文件包括Verilog源码、Quartus工程、仿真脚本、引脚分配报告都放到一个文件夹里打包存好。不光是交作业要用后面你可能会回头翻这个工程看自己当时怎么写进位的——相信我你会感谢当时的自己留了这些文件。本文还有配套的精品资源点击获取