本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文围绕“clk.rar_FPGA项目_quartus_数字钟”展开,详细介绍如何使用Altera Quartus II软件实现一个基于FPGA的数字钟系统。项目涵盖50MHz时钟分频、计数器设计、译码驱动7段数码管等核心模块,是FPGA初学者掌握数字逻辑设计与硬件编程的典型实践案例。通过本项目,学习者可掌握从逻辑设计到综合仿真的完整流程,深入理解分频、计数、译码等关键概念,并能使用Quartus II打开和运行工程文件clk.qar,进行后续开发与调试。
clk.rar_FPGA项目_quartus_数字钟

1. FPGA基础架构与可编程逻辑原理

可编程逻辑核心组成与工作原理

FPGA内部由多个可编程模块协同工作,核心包括 可配置逻辑块(CLB) 、 输入输出块(IOB) 、 Block RAM 、 DCM时钟管理单元 及丰富的 互连资源 。CLB是逻辑实现的基本单元,主要由 查找表(LUT) 和 触发器(FF) 构成。LUT通过预存真值表实现任意组合逻辑,如4输入LUT可模拟16种逻辑函数;触发器则用于构建时序逻辑,实现状态保持与同步操作。

// 示例:基于LUT的2输入异或逻辑实现(由综合器自动映射)
assign out = a ^ b; // 综合后映射至LUT4

FPGA采用SRAM工艺, 掉电后配置信息丢失 ,需外挂配置芯片(如EPCS)在上电时重新加载比特流。设计流程中,HDL代码经综合、布局布线后转化为针对特定FPGA架构的硬件映射,实现“软硬件协同设计”的闭环。

2. Quartus II开发环境配置与使用流程

在现代数字系统设计中,FPGA作为实现高度定制化硬件逻辑的核心平台,其开发效率和可靠性极大依赖于所使用的EDA(Electronic Design Automation)工具链。Altera(现为Intel PSG)推出的Quartus II软件,是业界广泛采用的FPGA综合、布局布线与编程下载一体化开发环境。该软件不仅支持完整的RTL到比特流生成流程,还集成了引脚规划器、时序分析器、SignalTap II在线逻辑分析等高级功能模块。掌握Quartus II的工程构建、源码管理、编译调试及归档机制,已成为从事FPGA开发工程师的基本技能要求。

本章将深入解析从软件安装到项目部署的完整工作流,重点聚焦于如何科学地组织工程结构、合理配置开发参数,并通过规范化的设计方法提升代码可维护性与团队协作效率。整个流程涵盖从初始环境搭建到最终编程下载的每一个关键步骤,确保开发者能够在真实项目场景中快速上手并规避常见错误。

2.1 Quartus II软件安装与工程创建

FPGA开发的第一步是建立稳定可靠的开发环境。Quartus II虽然功能强大,但其安装过程涉及版本选择、设备支持包匹配以及许可证激活等多个技术环节,稍有不慎可能导致后续无法进行综合或下载操作。因此,正确完成软件安装与首个工程初始化至关重要。

2.1.1 软件版本选择与许可证配置

选择合适的Quartus II版本是成功启动项目的前提。目前主流版本包括Quartus II 13.0/13.1(经典稳定版)、Quartus Prime Lite Edition(免费开源版),以及Professional Edition(企业级付费版)。对于大多数教学和中小型项目,推荐使用 Quartus Prime Lite Edition ,因其完全免费且支持Cyclone系列主流器件。

版本类型 支持器件范围 是否需要License 适用场景
Lite Edition Cyclone III/IV/V, MAX II/10, Arria II GX 否 教学实验、小规模项目
Standard Edition Stratix IV/V, Arria 10 是(节点锁定) 中大型商业项目
Professional Edition Stratix 10, Agilex 系列 是(网络浮动许可) 高端高速设计

安装前需确认操作系统兼容性。官方支持Windows 7/10(64位)和Linux Red Hat/CentOS。建议在独立分区预留至少20GB空间,以容纳工具链、IP库和缓存文件。

安装完成后,首次运行会提示设置许可证路径。若使用Pro版本,则需导入 .dat 格式的License文件。可通过如下命令检查License有效性:

quartus_sh --license_test

参数说明 :
- quartus_sh :Quartus Shell命令行接口;
- --license_test :用于验证当前License是否有效并列出可用产品。

若返回“Valid license found”,则表示授权成功;否则应检查环境变量 LM_LICENSE_FILE 是否指向正确的端口和服务器地址。

许可证配置常见问题与解决方案
  • 问题1:License过期
  • 解决方案:更新 .dat 文件中的 INCREMENT 字段有效期,或联系Intel获取试用许可。
  • 问题2:Host ID不匹配
  • 原因:License绑定特定MAC地址或主机名。
  • 解法:运行 quartus_64 --lsc 获取当前机器Host ID,提交给授权生成服务重新签发。

以下mermaid流程图展示了许可证加载全过程:

graph TD
    A[启动Quartus II] --> B{检测License存在?}
    B -- 否 --> C[提示用户指定路径]
    B -- 是 --> D[读取.dat文件]
    D --> E[解析Host ID与Feature]
    E --> F{匹配成功?}
    F -- 是 --> G[进入主界面]
    F -- 否 --> H[弹出错误对话框]
    H --> I[引导至License Manager]

该流程强调了License验证的关键决策点,帮助开发者理解为何某些功能灰显或不可用。

2.1.2 新建工程向导中的设备选型与引脚约束设置

创建一个新工程是进入实际设计阶段的第一步。Quartus II提供图形化“New Project Wizard”引导用户逐步完成工程目录、顶层实体命名、HDL语言选择及目标器件设定。

工程创建步骤详解
  1. 打开Quartus II → File → New Project Wizard;
  2. 设置工程路径与名称(如 digital_clock_prj );
  3. 输入顶层实体名(Top-Level Entity Name),通常与主模块一致;
  4. 添加现有HDL文件或留空后续添加;
  5. 选择目标器件系列(Device Family)与具体型号(如Cyclone IV EP4CE6E22C8);
  6. 指定封装形式、速度等级与逻辑资源需求;
  7. 完成向导并保存 .qpf 工程文件。

注: .qpf (Quartus Project File)记录所有工程属性,必须纳入版本控制系统。

设备选型关键因素分析

选择FPGA芯片时应综合考虑以下四个维度:

因素 影响说明 推荐实践
逻辑单元数量(LEs) 决定可实现的最大逻辑复杂度 预留30%余量以防后期扩展
I/O引脚数 关系到外设连接能力 根据数码管、按键等估算总数
Block RAM容量 影响存储类模块(如ROM、缓冲区)实现 至少满足显示驱动所需字符映射表
功耗与封装 影响PCB布局与散热设计 小型项目优先选用TQFP或EQFP封装

例如,在实现六位数码管动态扫描的数字钟项目中,共需约6个位选信号 + 8段选信号 = 14个GPIO;加上复位、时钟输入等控制信号,总计不超过20个I/O。Cyclone IV EP4CE6系列具备多达92个可用I/O,足以胜任此类任务。

引脚分配策略与早期约束定义

尽管引脚分配可在编译后通过Pin Planner完成,但在新建工程阶段即进行初步规划有助于避免后期冲突。可在工程创建完毕后立即打开Assignment Editor,预设关键信号绑定位置。

示例:为50MHz外部晶振输入指定专用全局时钟引脚:

set_location_assignment PIN_R8 -to clk_50mhz
set_location_assignment PIN_G9 -to rst_n
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to seg_data[*]

代码逻辑逐行解读 :
- 第1行:将物理引脚R8分配给名为 clk_50mhz 的信号;
- 第2行:将复位按钮连接至G9引脚;
- 第3行:对所有段码数据线应用3.3V低压TTL电平标准,确保驱动兼容性。

这些约束写入 .qsf (Quartus Settings File)中,属于工程配置的一部分。若未提前设定,综合阶段虽能通过,但在布局布线时报错“Failed to find fitting location for node”,导致编译失败。

此外,建议创建一张初步的引脚映射表,便于团队共享与PCB协同设计:

信号名 方向 FPGA引脚 外部连接 备注
clk_50mhz in R8 晶振输出 使用专用时钟输入引脚
rst_n in G9 按键下拉电路 低电平有效
seg_data[7:0] out J1-J8 数码管a~dp段 共阴极接法
digit_sel[5:0] out K1-K6 三极管基极驱动 动态扫描使能

此表格应在项目初期由硬件与FPGA工程师共同确认,防止出现电气不匹配或资源争用问题。

2.2 HDL源文件编写与模块化设计规范

高质量的硬件描述语言(HDL)代码不仅是功能实现的基础,更是保障可读性、可测试性和可重用性的核心要素。Verilog作为最常用的HDL之一,在Quartus II中享有完善的支持体系。遵循标准化的模块结构与接口命名约定,能显著提升开发效率。

2.2.1 Verilog/VHDL文件结构与接口定义

一个符合工业级规范的Verilog模块应包含明确的时间尺度声明、模块头、端口列表、内部信号定义及行为描述。以下是典型模板:

`timescale 1ns / 1ps

module clock_divider #(
    parameter CLK_FREQ = 50_000_000,
    parameter TARGET_FREQ = 1
)(
    input wire clk,
    input wire rst_n,
    output reg pulse_1Hz
);

    localparam COUNT_MAX = CLK_FREQ / (2 * TARGET_FREQ) - 1;
    reg [31:0] counter;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            counter <= 32'd0;
            pulse_1Hz <= 1'b0;
        end else if (counter >= COUNT_MAX) begin
            counter <= 32'd0;
            pulse_1Hz <= ~pulse_1Hz;
        end else begin
            counter <= counter + 1;
        end
    end

endmodule

逻辑分析与参数说明 :
- `timescale 1ns / 1ps :定义仿真时间精度为1纳秒,最小分辨率达皮秒级;
- parameter :允许外部传入时钟频率和目标频率,增强通用性;
- localparam :计算分频计数阈值,自动适配不同输入条件;
- always @(posedge clk ...) :敏感列表包含同步时钟上升沿与异步复位下降沿;
- 复位期间清零计数器并置脉冲为低,保证初始状态可控。

该模块实现了从50MHz降至1Hz的精确分频,将在第三章进一步展开讨论。此处仅关注其结构规范性。

接口命名建议

统一的命名规则有利于跨模块连接与自动化脚本处理。推荐采用以下风格:

  • 时钟信号: clk , clk_50mhz , sys_clk
  • 复位信号: rst_n (低有效)、 reset (高有效)
  • 使能信号: en , enable , valid
  • 数据总线: data_in[7:0] , addr_out[15:0]
  • 控制信号: start , done , busy

避免使用模糊词汇如 signal , temp 等,确保语义清晰。

2.2.2 模块划分原则与顶层实体连接策略

复杂系统应采用层次化设计(Hierarchical Design),将整体功能分解为若干子模块,每个模块职责单一、接口明确。以数字钟为例,可分为以下层级:

Top_Level
├── Clock_Divider (50MHz → 1Hz)
├── Second_Counter (0–59)
├── Minute_Counter (0–59)
├── Hour_Counter (0–23)
├── BCD_Encode (拆分十/个位)
└── Display_Driver (扫描控制+段译码)

顶层模块负责实例化各组件并连接信号通路。典型连接方式如下:

module digital_clock_top (
    input wire clk_50mhz,
    input wire rst_n,
    output wire [7:0] seg_data,
    output wire [5:0] digit_sel
);

    wire pulse_1Hz;

    // 实例化分频器
    clock_divider #(.CLK_FREQ(50_000_000), .TARGET_FREQ(1)) 
        u_div (.clk(clk_50mhz), .rst_n(rst_n), .pulse_1Hz(pulse_1Hz));

    // 待续:计数器级联...

endmodule

参数传递机制说明 :
- 在实例化时通过 #(...) 覆盖默认参数值;
- 端口映射使用 . 符号明确对应关系,提高可读性;
- 子模块间通过 wire 类型信号互联,形成数据流管道。

这种模块化架构便于独立仿真验证,也利于后期功能扩展(如加入闹钟模块)。同时,Quartus II的Project Navigator能自动生成模块依赖关系图,辅助理解系统结构。

以下为使用Mermaid绘制的模块连接拓扑图:

graph LR
    A[clk_50mhz] --> B[Clock Divider]
    B --> C[Second Counter]
    C --> D[Minute Counter]
    D --> E[Hour Counter]
    C --> F[BCD Encode]
    D --> F
    E --> F
    F --> G[Display Driver]
    G --> H[seg_data & digit_sel]
    I[rst_n] --> B
    I --> C
    I --> D
    I --> E

该图直观展示信号流向与控制依赖,是系统集成前的重要设计文档。

2.3 综合、布局布线与编程下载流程

完成代码编写后,必须经过一系列自动化处理才能转化为可在FPGA上运行的物理配置文件。Quartus II的编译流程主要包括:分析与综合(Analysis & Synthesis)、适配(Fit/Place & Route)、时序分析(Timing Analysis)和编程文件生成(Assembler)。每一步都直接影响最终性能与稳定性。

2.3.1 编译流程详解:从RTL到比特流生成

整个编译过程可通过菜单Processing → Start Compilation一键触发,或使用命令行工具实现批处理:

quartus_map digital_clock_prj --analysis_and_synthesis
quartus_fit digital_clock_prj
quartus_sta digital_clock_prj
quartus_asm digital_clock_prj

指令解释 :
- quartus_map :执行HDL语法解析与逻辑综合,生成门级网表;
- quartus_fit :将逻辑单元映射到具体LE、RAM块并完成布线;
- quartus_sta :静态时序分析,验证是否满足建立/保持时间;
- quartus_asm :生成 .sof (SRAM Object File)用于JTAG下载。

各阶段输出日志均保存在 output_files 目录下,可用于排查错误。例如,若出现“Timing requirements not met”,则需查看 digital_clock_prj.sdc 中是否缺少必要约束。

编译结果关键指标解读

编译结束后,Quartus II自动生成摘要报告,包含以下核心信息:

指标项 示例值 含义
Logic utilization 1,248 / 6,272 (19%) 逻辑单元占用率
Total memory bits 4,096 Block RAM使用总量
Number of pins 28 实际使用I/O数量
Setup slack +2.1 ns 正值表示时序收敛
Fmax for clk 108 MHz 系统最高可达频率

其中,“Setup slack”尤为关键——正值表示电路能在指定时钟频率下可靠运行;负值则意味着存在时序违例,必须优化设计或降低频率。

2.3.2 引脚分配与时序约束文件(SDC)初步应用

除了图形化Pin Planner外,推荐使用Tcl脚本或SDC(Synopsys Design Constraints)文件进行约束管理,以便版本控制和重复使用。

示例 sdc 文件内容:

create_clock -name clk_50mhz -period 20.000 [get_ports clk_50mhz]
set_input_delay -clock clk_50mhz 2.0 [all_inputs]
set_output_delay -clock clk_50mhz 2.0 [all_outputs]
set_multicycle_path -setup 2 -from [get_clocks clk_50mhz] -to [get_clocks pulse_1Hz]

逐行解析 :
- 第1行:定义输入时钟周期为20ns(对应50MHz);
- 第2-3行:设置输入/输出延迟,模拟外部器件响应时间;
- 第4行:允许1Hz脉冲信号跨越两个时钟周期传输,避免误判违例。

这些约束被Quartus STA模块读取,用于精确评估路径延迟。若不加约束,默认按最快路径分析,容易产生虚假警告。

2.3.3 使用JTAG接口进行FPGA在线编程

编译成功后,可通过USB-Blaster等JTAG下载器将 .sof 文件烧录至FPGA SRAM中。

操作步骤如下:

  1. 连接JTAG线缆至开发板JTAG接口;
  2. 打开Tools → Programmer;
  3. 添加 .sof 文件,勾选“Program/Configure”;
  4. 点击Start开始下载;
  5. 观察Progress Bar直至完成。

注意: .sof 为临时配置文件,掉电丢失;若需永久存储,须生成 .pof 并烧入EPCS串行Flash。

程序下载后,FPGA立即开始运行,此时可观察LED闪烁、数码管计时等现象验证功能。

2.4 工程归档与.qar文件管理

2.4.1 .qar文件的打包与导入操作步骤

当项目阶段性完成或需移交他人时,应使用Quartus II内置的归档功能生成 .qar (Quartus Archive)文件:

  1. Project → Archive Project;
  2. 选择归档级别(Full或Minimum);
  3. 指定输出路径与文件名;
  4. 点击OK生成压缩包。
  • Full模式包含所有中间文件,适合长期存档;
  • Minimum模式仅含必需源码与设置,适用于协作传输。

导入时只需File → Restore Project from Archive即可还原完整工程结构。

2.4.2 多人协作开发中的版本控制建议

尽管Quartus II非专为团队协作设计,但仍可通过Git等工具实现有效管理。关键在于:

  • 忽略临时文件( .qws , .ddb , *.bak );
  • 提交 .v , .sv , .qsf , .sdc , .qar 等核心文件;
  • 使用分支管理功能迭代(feature branches);
  • 定期打包发布稳定版 .qar 供测试人员使用。

建立README.md说明编译环境与引脚定义,是保障团队高效协同的重要补充措施。

3. 50MHz时钟信号分频模块设计与实现

在现代FPGA数字系统中,精确的时钟管理是确保功能正确性和系统稳定性的核心环节。大多数开发板上提供的主时钟源为50MHz或更高频率(如100MHz),而实际应用中往往需要更低频、更精准的时间基准信号——例如用于驱动秒计数器的1Hz脉冲、用于状态机节拍控制的毫秒级时钟等。因此, 高频时钟分频模块的设计与实现 成为构建可信赖数字系统的第一步。

本章将围绕“如何从一个50MHz输入时钟生成稳定、占空比准确的低频输出信号”展开深入探讨。我们将从理论推导入手,分析计数器模值与时钟周期之间的数学关系;随后进入Verilog代码层面,实现参数化整数分频器,并通过ModelSim进行波形仿真验证其精度;最后引入同步复位机制和多级分频结构,提升系统的抗干扰能力与长期运行稳定性。整个过程不仅涉及硬件逻辑建模,还包括对亚稳态传播风险的认知与规避策略,旨在建立一种兼具工程实用性与理论严谨性的设计范式。

3.1 高频时钟分频的理论依据

要实现有效的时钟分频,首先必须理解其背后的物理意义和数学模型。分频的本质是在不改变原始时钟边沿特性的前提下,通过计数方式降低有效事件的发生频率。对于FPGA而言,这一操作通常由寄存器构成的计数器完成,通过对高频频脉冲进行累加并达到预设阈值后翻转输出电平,从而生成所需低频信号。

3.1.1 时钟周期与计数器模值的关系推导

假设输入时钟频率为 $ f_{\text{in}} = 50 \, \text{MHz} $,则其周期为:

T_{\text{in}} = \frac{1}{f_{\text{in}}} = \frac{1}{50 \times 10^6} = 20 \, \text{ns}

若目标输出频率为 $ f_{\text{out}} = 1 \, \text{Hz} $,即每秒产生一次上升沿,则对应周期为:

T_{\text{out}} = 1 \, \text{s} = 10^9 \, \text{ns}

由此可知,在每一个输出周期内,需累计接收的输入时钟个数为:

N = \frac{T_{\text{out}}}{T_{\text{in}}} = \frac{10^9}{20} = 50,000,000

这表明: 每经过5千万个输入时钟周期,才能翻转一次输出信号 ,以实现1Hz的精确分频。

进一步推广,通用公式如下:

N = \left\lfloor \frac{f_{\text{in}}}{f_{\text{out}}} \right\rfloor

其中 $ N $ 称为 计数器模值 (Modulus)。当采用 偶数模分频 且使用双边沿触发(如翻转寄存器)时,可自然获得50%占空比的方波输出;若为奇数模,则需额外逻辑调整高低电平持续时间以逼近理想占空比。

下表列出了几种常见输出频率对应的计数值及资源消耗估算:

输出频率 分频比 $N$ 计数器位宽(bit) 所需寄存器数量(估计)
1 Hz 50,000,000 $\lceil \log_2(50M) \rceil = 26$ 26
100 Hz 500,000 19 19
1 kHz 50,000 16 16
10 kHz 5,000 13 13

说明 :寄存器数量等于计数器位宽,因每个比特均由D触发器实现。

graph TD
    A[50MHz 输入时钟] --> B{是否达到 N-1?}
    B -- 否 --> C[计数器+1]
    C --> B
    B -- 是 --> D[输出翻转]
    D --> E[清零计数器]
    E --> B
    D --> F[1Hz 输出时钟]

该流程图展示了基于计数器的典型分频逻辑流程:系统持续检测当前计数值是否达到预设上限 $N-1$,一旦满足条件即翻转输出并重置计数器,形成闭环控制。

值得注意的是,上述方法适用于 整数分频 场景。若需实现小数分频(如1.5Hz),则需引入更复杂的Δ-Σ调制或多模切换技术,超出本章讨论范围。

3.1.2 分频比计算方法与精度误差分析

虽然理论上可以通过精确除法得到理想的分频比,但在实际FPGA实现中仍存在若干影响精度的因素:

  1. 舍入误差 :当 $ f_{\text{in}} / f_{\text{out}} $ 不为整数时,必须取整处理,导致实际输出频率偏离设定值。
  2. 晶振漂移 :外部有源晶振可能存在±10ppm至±50ppm的频率偏差,长期累积会造成显著走时误差。
  3. 布线延迟与时钟偏斜 :片内互连延迟可能导致不同路径间出现微小相位差,影响多模块协同工作的同步性。

以1Hz输出为例,若使用 $ N = 50,000,000 $ 实现,则理论误差为0。但若由于工具限制或性能优化需求,将分频拆分为两级(如先分频至1kHz再至1Hz),则每一级都可能引入舍入误差。

考虑如下情况:
- 第一级:50MHz → 1000Hz,需计数50,000次;
- 第二级:1000Hz → 1Hz,需计数1000次;

此时总分频比仍为 $ 50,000 \times 1,000 = 50,000,000 $,无额外误差。但如果第一级仅能支持最大16位计数(即最大65,535),则无法直接实现50,000的分频,必须近似处理,带来潜在偏差。

此外,还需关注 长期累计误差 。假设分频比存在 $+1$ 的偏差(即多计一个时钟周期),则每秒钟输出延迟增加 $20\,\text{ns}$。经过一天(86,400秒)后,总误差为:

86400 \times 20 \times 10^{-9} = 1.728 \times 10^{-3} \, \text{s} \approx 1.7\,\text{ms}

虽然看似微小,但对于高精度定时系统(如通信协议定时、GPS授时辅助)而言仍不可忽视。

解决此类问题的方法包括:
- 使用更高精度的参考时钟(如TCXO温补晶振);
- 引入动态校准机制,定期与外部标准时间源对比修正;
- 在HDL代码中采用 parameter 定义分频常量,便于后期微调优化。

3.2 基于计数器的整数分频器设计

掌握了分频的基本原理后,接下来进入具体电路实现阶段。我们将使用Verilog HDL语言编写一个可复用、参数化的整数分频模块,重点突出其模块化设计思想和可移植性优势。

3.2.1 下降沿检测与翻转逻辑实现1Hz秒脉冲

传统做法中,常采用“计数到指定值后翻转输出”的方式生成对称方波。以下是一个典型的非参数化1Hz分频器示例:

module clk_div_1Hz (
    input      clk_50MHz,
    input      rst_n,
    output reg clk_1Hz
);

parameter COUNT_MAX = 25_000_000; // 半周期计数(50%占空比)

reg [25:0] counter;

always @(posedge clk_50MHz or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 26'd0;
        clk_1Hz <= 1'b0;
    end else begin
        if (counter == COUNT_MAX - 1) begin
            clk_1Hz <= ~clk_1Hz;     // 翻转输出
            counter   <= 26'd0;      // 清零计数器
        end else begin
            counter <= counter + 1'b1;
        end
    end
end

endmodule
逐行逻辑分析与参数说明:
  • input clk_50MHz : 接收来自板载晶振的50MHz主时钟,作为计数驱动源。
  • input rst_n : 低电平有效的异步复位信号,用于初始化系统状态。
  • output reg clk_1Hz : 输出1Hz方波信号,类型为 reg 因其在 always 块中被赋值。
  • parameter COUNT_MAX = 25_000_000 : 设定半周期计数值。由于每次翻转代表半个输出周期,故总周期对应 $2 \times 25M = 50M$ 个时钟周期,正好匹配1Hz需求。
  • reg [25:0] counter : 定义26位宽计数器,足以容纳最大值50,000,000。
  • always @(posedge clk_50MHz ...) : 敏感列表包含主时钟上升沿和复位下降沿,构成同步+异步混合复位结构。
  • 当计数达到 COUNT_MAX - 1 时,执行两个动作:
  • clk_1Hz <= ~clk_1Hz :翻转输出,实现自动交替;
  • counter <= 0 :立即清零,开始下一周期计数。

该设计优点在于结构清晰、易于理解,但缺点也明显: 硬编码参数难以复用 ,若需改为100Hz输出,必须手动修改 COUNT_MAX 并重新验证。

3.2.2 参数化Verilog模块设计提升复用性

为了增强模块通用性,应将其重构为参数化形式,允许用户在实例化时动态指定输入/输出频率。改进后的代码如下:

module clk_div_generic #(
    parameter FIN_HZ = 50_000_000,
    parameter FOUT_HZ = 1,
    parameter WIDTH = $clog2(FIN_HZ / FOUT_HZ / 2) + 1
)(
    input              clk_in,
    input              rst_n,
    output reg         clk_out
);

localparam CNT_MAX = FIN_HZ / FOUT_HZ / 2;

reg [WIDTH-1:0] counter;

always @(posedge clk_in or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 'b0;
        clk_out <= 1'b0;
    end else begin
        if (counter == CNT_MAX - 1) begin
            clk_out <= ~clk_out;
            counter <= 'b0;
        end else begin
            counter <= counter + 1'b1;
        end
    end
end

endmodule
关键特性解析:
  • parameter FIN_HZ , FOUT_HZ : 用户可配置的输入/输出频率(单位Hz),极大提升了灵活性。
  • WIDTH : 利用SystemVerilog内置函数 $clog2() 自动计算所需计数器位宽,避免人为估算错误。
  • localparam CNT_MAX : 根据频率比自动推导半周期计数值,确保占空比为50%。
  • 所有关键参数均可在顶层设计中覆盖:
// 示例:实例化一个50MHz→100Hz分频器
clk_div_generic #(
    .FIN_HZ(50_000_000),
    .FOUT_HZ(100)
) u_div (
    .clk_in(clk_50MHz),
    .rst_n(rst_n),
    .clk_out(clk_100Hz)
);

此模块可在多个项目中重复使用,只需更改参数即可适配不同应用场景,体现了良好的 模块封装性与工程规范性 。

3.3 分频模块的功能仿真与验证

设计完成后,必须通过仿真手段验证其行为是否符合预期。ModelSim作为业界广泛使用的HDL仿真工具,能够提供精确的波形观察与时序分析能力。

3.3.1 ModelSim仿真环境搭建与测试激励编写

首先创建测试平台(Testbench)对 clk_div_generic 模块进行功能验证:

`timescale 1ns / 1ps

module tb_clk_div;

reg        clk_50MHz;
reg        rst_n;
wire       clk_1Hz;

// 实例化被测模块
clk_div_generic #(
    .FIN_HZ(50_000_000),
    .FOUT_HZ(1)
) uut (
    .clk_in(clk_50MHz),
    .rst_n(rst_n),
    .clk_out(clk_1Hz)
);

// 生成50MHz时钟(周期20ns)
initial begin
    clk_50MHz = 0;
    forever #10 clk_50MHz = ~clk_50MHz; // 半周期10ns
end

// 复位信号控制
initial begin
    rst_n = 0;
    #100 rst_n = 1; // 100ns后释放复位
end

// 仿真结束控制
initial begin
    #200_000_000 $finish; // 运行200ms后停止
end

endmodule
仿真设置步骤:
  1. 打开Quartus II,添加Verilog源文件与Testbench;
  2. 启动EDA Simulation Tool(选择ModelSim-Altera);
  3. 编译所有模块,加载仿真;
  4. 添加信号 clk_50MHz 和 clk_1Hz 至波形窗口;
  5. 运行仿真,观察输出波形。

3.3.2 观察波形图中时钟占空比与周期准确性

成功仿真后,应重点关注以下几点:

  • 输出周期是否为1秒?
  • 在波形图中测量相邻两个上升沿之间的时间间隔,应接近1,000,000,000 ns(1秒)。
  • 占空比是否为50%?
  • 高电平持续时间为500,000,000 ns,低电平相同,表示理想方波。
  • 复位期间输出是否归零?
  • 在前100ns内, clk_1Hz 应保持低电平,直到 rst_n 拉高后才开始正常计数。
信号名称 类型 频率/周期 作用说明
clk_50MHz 输入 50 MHz / 20 ns 主时钟源
rst_n 输入 脉冲信号 控制系统初始化
clk_1Hz 输出 1 Hz / 1 s 目标分频结果,用于驱动秒计数
timeline
    title ModelSim仿真关键时间节点
    section 仿真开始
      t=0ns : clk_50MHz启动,rst_n=0
    section 复位释放
      t=100ns : rst_n → 1,计数开始
    section 第一次翻转
      t=25,000,000 × 20ns = 0.5s : clk_1Hz首次翻高
    section 第二次翻转
      t=1.0s : clk_1Hz翻低,完成第一个完整周期

通过上述验证流程,可以确认分频模块在理想条件下具备正确的逻辑行为。然而,真实硬件环境中还存在抖动、噪声、电源波动等问题,因此还需进一步实施稳定性优化措施。

3.4 抗抖动与稳定性优化措施

尽管基本分频器能在仿真中完美工作,但在实际部署中仍面临诸多挑战,尤其是 亚稳态传播 和 单级大计数带来的时序压力 。为此,需引入高级设计技巧以增强鲁棒性。

3.4.1 同步复位机制防止亚稳态传播

前述设计中采用了异步复位( negedge rst_n ),虽响应迅速,但若复位信号本身存在毛刺或与时钟不同步,极易引发亚稳态(Metastability),导致输出不确定。

推荐改用 同步复位 结构:

always @(posedge clk_in) begin
    if (!rst_n_sync) begin  // 经过两级同步的复位信号
        counter <= 'b0;
        clk_out <= 1'b0;
    end else begin
        // 正常计数逻辑...
    end
end

其中, rst_n_sync 可通过双触发器同步链生成:

reg rst1, rst2;
always @(posedge clk_in) begin
    rst1 <= rst_n;
    rst2 <= rst1;
end
assign rst_n_sync = rst2;

此举虽牺牲了复位速度,但大幅降低了跨时钟域风险,尤其适用于多时钟系统或按键复位输入场景。

3.4.2 多级分频结构降低单级计数压力

当分频比极大(如50,000,000)时,单一计数器可能导致综合工具难以满足建立/保持时间要求。为此可采用 级联分频 策略:

50MHz → ÷500 → 100kHz → ÷100 → 1kHz → ÷1000 → 1Hz

各阶段使用较小模值计数器,减轻每级逻辑负担,同时便于调试与故障隔离。

例如,先实现一个通用分频子模块:

module prescaler (
    input      clk_in,
    input      rst_n,
    output reg clk_out,
    parameter  DIV = 10
);
reg [$clog2(DIV)-1:0] cnt;
always @(posedge clk_in or negedge rst_n)
    if (!rst_n) {cnt, clk_out} <= 0;
    else if (++cnt >= DIV) begin
        clk_out <= ~clk_out;
        cnt <= 0;
    end
endmodule

然后逐级连接:

wire clk_100kHz, clk_1kHz;

prescaler #(.DIV(500))  p1 (.clk_in(clk_50MHz), .rst_n(rst_n), .clk_out(clk_100kHz));
prescaler #(.DIV(100))  p2 (.clk_in(clk_100kHz), .rst_n(rst_n), .clk_out(clk_1kHz));
prescaler #(.DIV(1000)) p3 (.clk_in(clk_1kHz),  .rst_n(rst_n), .clk_out(clk_1Hz));

这种结构不仅提升时序收敛性,也为未来扩展(如添加中间频率输出)提供了便利。

综上所述,一个健壮的分频模块不仅仅是“能跑通”,更要经得起长时间运行、温度变化和电磁干扰的考验。通过参数化设计、仿真验证与稳定性优化三者结合,方可构建真正可靠的FPGA时钟子系统。

4. 秒、分、小时计数器逻辑设计与级联

在现代数字系统中,时间的精确计量是诸多应用的基础功能。FPGA凭借其强大的并行处理能力和灵活的时序控制机制,非常适合实现高精度的实时时钟系统。本章将深入探讨如何基于前一章节生成的1Hz基准秒脉冲信号,构建完整的秒、分、小时三级计数器模块,并通过合理的级联逻辑实现自动进位与归零操作。整个设计不仅要求逻辑正确性,还需具备良好的可扩展性和调试友好性,为后续连接显示驱动模块提供稳定的数据输出。

计数器作为最基础的时序电路单元,在FPGA内部以寄存器+组合逻辑的形式存在。通过对计数状态进行判断和反馈控制,可以实现任意模值(modulo)的循环计数行为。在数字钟系统中,我们需要分别设计两个60进制计数器(用于秒和分)以及一个24进制计数器(用于小时),并通过进位信号实现跨单位的时间递增。此外,为了满足实际使用需求,还需引入复位、暂停、校准等控制功能,使系统具备用户交互能力。

本章内容从底层计数器结构出发,逐步构建复合型多级计时系统,强调模块化设计思想与同步时序控制的重要性。通过参数化Verilog编码方式提升代码复用率,并结合仿真验证手段确保各级计数器在边界条件下的稳定性。最终完成顶层模块集成,形成完整的计时核心引擎。

4.1 十进制与六十进制计数器构建

在数字钟系统中,时间单位遵循特定的进制规则:秒和分钟采用60进制(0~59),小时则采用24进制(0~23)。因此,必须针对不同单位设计具有特定上限值的计数器模块。这些计数器本质上是带有模数限制的同步递增寄存器,其输出随每个有效时钟边沿递增,达到预设最大值后自动清零并产生进位信号。

4.1.1 秒/分计数器(0-59)状态转移逻辑设计

秒和分计数器的功能完全一致,均需完成从0到59的递增计数过程。当计数值达到59并在下一个时钟上升沿到来时,应立即归零,并向高位(即“分”或“小时”)发送一个宽度为一个时钟周期的脉冲信号,表示进位发生。

该计数器的关键在于状态转移条件的准确建模。使用Verilog HDL描述如下:

module counter_60 (
    input           clk,
    input           rst_n,      // 低电平复位
    input           en,         // 使能信号
    output reg [5:0] count,     // 6位二进制表示0~63
    output reg      carry       // 进位输出
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        count <= 6'd0;
        carry <= 1'b0;
    end else begin
        carry <= 1'b0; // 默认不产生进位
        if (en) begin
            if (count == 6'd59) begin
                count <= 6'd0;
                carry <= 1'b1; // 溢出时置位进位
            end else begin
                count <= count + 1'b1;
            end
        end
    end
end

endmodule
代码逻辑逐行解读分析:
  • 第1–7行 :模块接口定义。 clk 为主时钟输入; rst_n 为异步低电平复位信号; en 为计数使能信号,可用于暂停计数; count[5:0] 为当前计数值(6位足够表示0~63); carry 为进位标志。
  • 第9–18行 :同步时序块,触发条件为 posedge clk 或 negedge rst_n ,支持异步复位。
  • 第10–12行 :复位状态下,计数器清零,进位信号也清零,保证初始状态可靠。
  • 第13行 :在非复位路径中,默认将 carry 拉低,避免毛刺传播。
  • 第14–17行 :仅当 en 有效时才允许计数。若当前值为59,则下一状态归零,并拉高 carry 一个周期;否则正常加1。

此设计采用了“先判断再赋值”的策略,符合同步设计原则,避免了组合环路风险。同时,进位信号在时钟边沿同步生成,有利于下游模块采样。

下面通过Mermaid流程图展示其状态转移逻辑:

stateDiagram-v2
    [*] --> RESET
    RESET --> IDLE : rst_n↑
    IDLE --> COUNTING : en=1
    COUNTING --> COUNTING : count < 59
    COUNTING --> CARRY_GEN : count == 59
    CARRY_GEN --> IDLE : carry=1, next count=0
    IDLE --> COUNTING : en=1

图解说明:该状态机包含四个主要状态。系统上电后进入 RESET 状态,在复位释放后转至 IDLE 。当使能信号有效时进入 COUNTING 状态持续递增。一旦检测到 count==59 ,便跳转至 CARRY_GEN 状态发出进位脉冲并重置计数器。若使能关闭,则保持在 IDLE 状态不变。

参数名称 类型 功能说明
clk input 主工作时钟,建议频率1Hz(来自分频模块)
rst_n input 异步低电平复位,优先于其他操作
en input 计数使能控制,用于暂停/恢复计时
count[5:0] output reg 当前计数值,范围0~59
carry output reg 高电平脉冲,持续一个时钟周期,指示进位

该模块已具备完整功能,可通过例化两次分别用于“秒”和“分”计数部分。

4.1.2 小时计数器(0-23)溢出归零机制

小时计数器与秒/分计数器结构相似,但其模数为24,即计数范围为0~23。当计数达到23且接收到进位信号时,应在下一时钟周期归零,并可选择是否向上级(如日期模块)发出日进位信号。

以下是其实现代码:

module counter_24 (
    input           clk,
    input           rst_n,
    input           en,
    output reg [4:0] count,     // 5位可表示0~31
    output reg      day_carry   // 日进位信号
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        count <= 5'd0;
        day_carry <= 1'b0;
    end else begin
        day_carry <= 1'b0;
        if (en) begin
            if (count == 5'd23) begin
                count <= 5'd0;
                day_carry <= 1'b1;
            end else begin
                count <= count + 1'b1;
            end
        end
    end
end

endmodule
代码逻辑分析:
  • 使用5位寄存器存储 count ,虽略大于必要位宽,但便于综合工具优化。
  • day_carry 信号仅在 count==23 且 en 有效时产生,持续一个时钟周期。
  • 复位与时钟同步处理,保障时序一致性。

与60进制计数器相比,此模块输出的是“日进位”,可用于未来拓展日历功能。

为比较两者特性,列出下表:

特性 秒/分计数器 小时计数器
模数 60 24
计数范围 0~59 0~23
输出进位 分/小时进位 日进位
数据宽度 6位 5位
典型输入时钟 1Hz(秒)、1/60Hz(分) 1/3600Hz(每小时一次)
是否可暂停 是(通过en) 是

两者均可通过参数化进一步统一结构,提高代码复用性,将在后续章节讨论。

4.2 计数器之间的级联控制逻辑

多个独立计数器必须通过精确的级联机制才能构成连贯的时间流。级联的核心问题是进位信号的生成与传递,以及各级使能信号的协调。若处理不当,可能导致进位延迟、重复计数或状态跳跃等问题。

4.2.1 进位信号生成条件与时序对齐

理想的进位传递应当满足以下要求:
1. 进位脉冲宽度严格为一个目标时钟周期;
2. 脉冲边沿与接收端时钟同步;
3. 不因竞争冒险产生亚稳态。

在前述 counter_60 模块中, carry 信号是在 count==59 且 en 有效时,在下一个时钟上升沿被置高的。由于该信号本身已在时钟域内同步生成,故可直接作为高位计数器的使能信号。

例如,将秒计数器的 carry 连接至分计数器的 en 端口,即可实现“每满60秒增加1分钟”的逻辑:

wire sec_carry;
counter_60 u_sec_counter (
    .clk(clk_1Hz),
    .rst_n(rst_n),
    .en(1'b1),           // 秒计数始终使能
    .count(sec_count),
    .carry(sec_carry)
);

counter_60 u_min_counter (
    .clk(clk_1Hz),
    .rst_n(rst_n),
    .en(sec_carry),      // 仅当秒溢出时才递增
    .count(min_count),
    .carry(min_carry)
);

注意:此处 sec_carry 作为 min_counter 的使能信号,意味着只有在秒计数完成一轮循环后,分计数器才会动作一次。这种设计天然实现了“事件驱动”式递增,避免了连续采样导致的误差累积。

我们可通过如下表格说明各级间的触发关系:

当前状态 触发事件 下一级响应
秒=59 → 0 生成carry脉冲 分计数器+1
分=59 → 0 生成carry脉冲 小时计数器+1
时=23 → 0 生成day_carry (预留日历接口)

所有进位均发生在各自模块内部,且由本地时钟同步驱动,确保了全局时序一致性。

4.2.2 异步清零与同步使能信号协调

尽管各计数器共享同一复位源( rst_n ),但由于复位信号可能为异步施加,存在引发亚稳态的风险。为此,推荐在每个计数器内部使用同步复位技术,或将外部异步复位经两级触发器同步后再分发。

改进后的同步复位结构如下:

reg rst_sync1, rst_sync2;
always @(posedge clk) begin
    rst_sync1 <= !rst_n;
    rst_sync2 <= rst_sync1;
end

然后将 rst_sync2 作为各模块的同步复位信号输入。

另一方面,使能信号的同步也至关重要。例如,在暂停模式下, en 被拉低,但必须确保该变化发生在时钟边沿之后,防止出现部分更新现象。为此,建议将控制信号(如 pause_key )也经过同步链处理:

reg pause_sync1, pause_sync2;
always @(posedge clk) begin
    pause_sync1 <= pause_key;
    pause_sync2 <= pause_sync1;
end

最终 pause_sync2 作为全局使能开关。

下面使用Mermaid绘制级联系统的信号流向图:

graph TD
    A[1Hz Clock] --> B[Second Counter]
    B -->|carry| C[Minute Counter]
    C -->|carry| D[Hour Counter]
    D -->|day_carry| E[Calendar Module *optional*]
    F[Sync Reset] --> B
    F --> C
    F --> D
    G[Pause Control] -->|.en| B
    G -->|.en| C
    G -->|.en| D

说明:该图展示了主时钟、复位、使能和进位信号在整个计数链中的传播路径。所有模块共用同一时钟域,确保无跨时钟域问题。

4.3 可控计时功能扩展

实际数字钟需要支持手动调节时间,包括复位、暂停、快调校准等功能。这要求在原有计数器基础上增加外部控制接口,并配合按键消抖电路和有限状态机(FSM)实现精准干预。

4.3.1 添加复位、暂停与校准输入端口

在顶层模块中引入以下输入信号:

  • reset_btn : 手动复位按钮,按下时秒、分、时全部归零;
  • pause_btn : 暂停/继续切换按钮;
  • set_mode_btn : 进入时间设置模式;
  • inc_btn : 在设置模式下用于快速递增当前单位。

对应地,修改计数器使能逻辑:

wire normal_en = ~pause_state & ~set_mode;  // 正常运行
wire set_min_en = set_mode & sel_min & inc_pulse;
wire set_hour_en = set_mode & sel_hour & inc_pulse;

// 实例化计数器
counter_60 u_sec (.en(normal_en), ... );
counter_60 u_min (.en(normal_en || set_min_en), ... );
counter_24 u_hour (.en(normal_en || set_hour_en), ... );

其中 inc_pulse 是由按键消抖模块产生的单周期脉冲,防止长按导致多次累加。

4.3.2 按键消抖电路与状态机配合实现精准调节

机械按键存在物理抖动,通常持续5~20ms。若直接接入逻辑电路,会造成误触发。标准解决方案是利用定时器对按键电平进行滤波。

消抖模块示例:

module debounce (
    input       clk,        // 例如50MHz
    input       btn_in,
    output reg  btn_out
);

reg [19:0] cnt;
reg btn_pre;

parameter DEBounce_TIME = 20_000; // 约20ms @ 50MHz

always @(posedge clk) begin
    btn_pre <= btn_in;
    if (btn_in != btn_pre) begin
        cnt <= DEBounce_TIME;
    end else if (cnt > 0) begin
        cnt <= cnt - 1;
    end
    if (cnt == 1) begin
        btn_out <= btn_in;
    end
end

endmodule
参数说明:
  • DEBounce_TIME :根据系统时钟计算出的等待周期数;
  • cnt :倒计时寄存器,检测到变化即重载;
  • btn_out :仅当电平稳定超过设定时间后才更新。

该输出可进一步送入边沿检测电路生成单次脉冲,供状态机使用。

状态机控制逻辑如下表所示:

当前模式 set_mode_btn 上升沿 pause_btn 上升沿 inc_btn 上升沿
Normal → Set Mode toggle pause 忽略
Set Min → Set Hour 忽略 分+1
Set Hour → Normal 忽略 时+1

通过有限状态机实现模式切换,确保用户体验流畅。

4.4 多模块集成与顶层连接

4.4.1 Top-Level模块整合各子组件

顶层模块负责统筹所有子模块,完成信号互联与资源分配。典型结构如下:

module digital_clock_top (
    input           clk_50MHz,
    input           reset_btn,
    input           pause_btn,
    input           set_mode_btn,
    input           inc_btn,
    output [7:0]    seg_data,
    output [5:0]    digit_sel
);

wire clk_1Hz;
wire rst_n;
wire pause_state;
wire set_mode;
wire inc_pulse;

// 分频模块
clock_divider u_divider (
    .clk_in(clk_50MHz),
    .clk_out(clk_1Hz)
);

// 消抖模块
debounce u_db_pause (.btn_in(pause_btn), .btn_out(pause_btn_deb));
debounce u_db_set   (.btn_in(set_mode_btn), .btn_out(set_mode_btn_deb));
debounce u_db_inc   (.btn_in(inc_btn), .btn_out(inc_btn_deb));

// 脉冲生成
single_pulse u_sp_pause (.clk(clk_1Hz), .level_in(pause_btn_deb), .pulse_out(pause_toggle));
// ... 其他脉冲生成

// 主计数器链
wire sec_carry, min_carry;
reg [5:0] sec_count, min_count;
reg [4:0] hour_count;

counter_60 u_sec (...);
counter_60 u_min (...);
counter_24 u_hour (...);

// BCD译码 & 显示驱动
bcd_to_7seg u_display (...);

endmodule

4.4.2 层次化设计带来的调试优势

采用层次化设计后,各模块职责清晰,便于独立仿真验证。例如可单独测试 counter_60 是否能在59后正确归零并发出进位。SignalTap II也可插入特定层级节点进行在线观测。

更重要的是,模块接口标准化后,易于替换或升级。例如将来可将 counter_60 替换为BCD计数器以简化显示转换逻辑。

综上所述,本章完成了数字钟核心计时系统的构建,涵盖计数器设计、级联逻辑、控制扩展与系统集成四大方面,为下一章的显示驱动奠定了坚实基础。

5. BCD编码与7段数码管译码驱动实现

在现代数字系统设计中,人机交互的直观性至关重要。FPGA虽然具备强大的并行计算和逻辑控制能力,但其输出本质上是二进制信号流,无法直接被人类有效识别。因此,在诸如数字钟、计数器等应用中,必须将内部寄存器中的数值转换为可视化的十进制数字显示。这一过程依赖于两个关键技术环节: BCD(Binary-Coded Decimal)编码 与 七段数码管译码驱动 。本章将深入剖析从二进制数据到物理显示的完整路径,涵盖编码原理、硬件结构适配、动态扫描机制以及抗干扰优化策略。

BCD编码作为连接数字逻辑与十进制认知的桥梁,使得每一位十进制数都能以4位二进制形式独立表示,极大简化了后续显示处理流程。而七段数码管作为一种成熟且成本低廉的显示器件,广泛应用于工业仪表、家用电器及教学实验平台。然而,若仅进行静态驱动,则多位显示将消耗大量I/O资源并导致功耗上升。为此,采用 动态扫描技术 成为必然选择——通过高速轮询方式分时点亮各个数码管,利用人眼视觉暂留效应实现“同时”显示效果。

此外,实际工程中还需考虑共阴极与共阳极数码管的电气特性差异、段选信号真值表的设计准确性、电流限流匹配等问题,否则可能出现亮度不均、重影或烧毁IO口的风险。整个模块不仅涉及组合逻辑设计,还融合了时序控制、电源管理与PCB布局协同等多个维度,是对FPGA综合设计能力的一次全面检验。

5.1 BCD编码原理及其在数字显示中的必要性

5.1.1 二进制与BCD码转换规则

在FPGA内部,所有数值均以纯二进制形式存储和运算。例如,秒计数器中的当前值“23”在寄存器中表现为 6'd23 ,即二进制 10111 。然而,若要将其送至两位数码管显示为“2”和“3”,则不能直接拆解该二进制数,因为其高位并非对应十位数字。此时需引入BCD编码,即将一个十进制数的每一位分别用4位二进制表示。

例如:
- 十进制数 23 → BCD 表示为: 4'b0010_4'b0011 (高4位表示十位“2”,低4位表示个位“3”)
- 而其原始二进制为 6'b10111

由此可见,BCD并非简单的二进制表示,而是按位打包的编码方式。这种编码方式牺牲了一定的数据密度(如6位可表示0~63,但BCD只能表示0~99中的部分值),却带来了极大的显示便利性。

在Verilog中实现二进制转BCD常采用 双移位算法(Double Dabble Algorithm) 或 查找表法 。对于小范围数值(如0~59),更高效的方式是使用状态机或条件判断进行拆分:

module bin_to_bcd (
    input      [5:0] bin,     // 输入6位二进制数(0~59)
    output reg [3:0] bcd_tens, // 十位 BCD
    output reg [3:0] bcd_ones  // 个位 BCD
);

always @(*) begin
    case (bin)
        6'd0:  {bcd_tens, bcd_ones} = {4'd0, 4'd0};
        6'd1:  {bcd_tens, bcd_ones} = {4'd0, 4'd1};
        ...
        6'd10: {bcd_tens, bcd_ones} = {4'd1, 4'd0};
        6'd23: {bcd_tens, bcd_ones} = {4'd2, 4'd3};
        ...
        6'd59: {bcd_tens, bcd_ones} = {4'd5, 4'd9};
        default: {bcd_tens, bcd_ones} = {4'd0, 4'd0};
    endcase
end

endmodule

代码逻辑逐行解读:
- 第1–3行:定义模块接口,输入为6位二进制数 bin ,输出为两个4位BCD码。
- 第5–22行:使用组合逻辑 always @(*) 实现无延迟映射;通过 case 语句穷举所有可能输入值,直接查表输出对应的十位与个位BCD。
- 参数说明: [5:0] 支持最大63,足够覆盖秒/分(0–59)范围; [3:0] 满足单个BCD位宽需求。
- 扩展建议:若需支持更大数值(如年份),可改用循环移位+进位判断的双移位算法,避免大规模case消耗过多LUT资源。

此方法适用于固定范围的小数值转换,具有确定性延迟和高可预测性,非常适合实时显示场景。

5.1.2 拆分十位与个位数值用于独立显示

一旦获得BCD格式的十位与个位,便可分别送往不同的数码管进行显示。假设系统包含六个数码管,分别用于显示“时十位、时个位、分十位、分个位、秒十位、秒个位”,则每个BCD输出需连接到对应位置的段译码器。

下表展示了典型时间值的BCD拆分与分配关系:

时间 原始二进制 BCD十位 BCD个位 数码管位置
秒=5 6’d5 4’d0 4’d5 SEG[1:0]
分=23 6’d23 4’d2 4’d3 SEG[3:2]
时=18 6’d18 4’d1 4’d8 SEG[5:4]

该表格清晰地反映了数据流向:原始计数器输出 → BCD转换模块 → 按位分离 → 映射至指定数码管通道。

进一步可通过参数化设计提升复用性:

parameter DIGIT_SECOND_TENS = 0;
parameter DIGIT_SECOND_ONES = 1;
parameter DIGIT_MINUTE_TENS = 2;
parameter DIGIT_MINUTE_ONES = 3;
parameter DIGIT_HOUR_TENS   = 4;
parameter DIGIT_HOUR_ONES   = 5;

这些参数可用于顶层模块中索引多路选择器或扫描控制器,确保各BCD值准确送达目标位置。

此外,考虑到未来功能扩展(如加入日期显示),推荐采用 总线化设计思想 ,将所有BCD输出整合为一个6×4位数组:

wire [3:0] bcd_digits[5:0];
assign bcd_digits[0] = sec_bcd_ones;
assign bcd_digits[1] = sec_bcd_tens;
assign bcd_digits[2] = min_bcd_ones;
assign bcd_digits[3] = min_bcd_tens;
assign bcd_digits[4] = hr_bcd_ones;
assign bcd_digits[5] = hr_bcd_tens;

该结构便于接入统一的动态扫描引擎,显著增强系统的模块化程度与可维护性。

5.2 七段数码管工作原理与驱动方式

5.2.1 共阴极与共阳极结构差异分析

七段数码管由a~g七个发光段组成,排列成“日”字形,通过不同段的亮灭组合显示0~9及少数字母。根据公共端连接方式的不同,可分为 共阴极(Common Cathode) 和 共阳极(Common Anode) 两种类型。

类型 公共端接法 段点亮条件 FPGA输出电平要求
共阴极 接GND 对应段输出高电平 高=亮
共阳极 接VCC 对应段输出低电平 低=亮

这意味着在编写译码逻辑时必须明确硬件类型,否则会导致全暗或全亮异常。

以共阴极为例,显示数字“1”只需点亮b、c两段,其段码为:

  a
 f   b
  g
 e   c
  d

→ “1” ⇒ b=1, c=1,其余为0 ⇒ 7'b0000110 (顺序:a,b,c,d,e,f,g)

而共阳极则相反,需输出低电平点亮,故段码为反码: 7'b1111001

为避免混淆,可在Verilog中定义全局参数标识类型:

`define COMMON_CATHODE
// 或 `define COMMON_ANODE

并在译码器中加入条件编译:

module seg_decoder (
    input  [3:0] bcd,
    output reg [6:0] seg
);
    always @(*) begin
        case (bcd)
            4'd0: seg = 7'b1111110;
            4'd1: seg = 7'b0110000;
            ...
            4'd9: seg = 7'b1111011;
            default: seg = 7'b0000001; // 显示 "-"
        endcase
    end

`ifdef COMMON_ANODE
    assign seg = ~seg;  // 反相输出
`endif

endmodule

代码逻辑说明:
- 使用 always @(*) 构建组合逻辑译码器;
- case 语句基于BCD输入选择标准段码(默认按共阴极设计);
- 通过 `ifdef 实现编译时极性翻转,无需修改核心逻辑;
- default 处理非法输入,防止乱码。

该设计具备良好的可移植性,仅需更改宏定义即可适配不同硬件平台。

5.2.2 段选信号真值表构建与译码器设计

完整的七段译码真值表如下所示(以共阴极为准):

BCD输入 a b c d e f g 显示字符
0 1 1 1 1 1 1 0 0
1 0 1 1 0 0 0 0 1
2 1 1 0 1 1 0 1 2
3 1 1 1 1 0 0 1 3
4 0 1 1 0 0 1 1 4
5 1 0 1 1 0 1 1 5
6 1 0 1 1 1 1 1 6
7 1 1 1 0 0 0 0 7
8 1 1 1 1 1 1 1 8
9 1 1 1 1 0 1 1 9

该表可通过组合逻辑直接实现,也可合成ROM存储于Block RAM中用于超大系统。

graph TD
    A[BCD Input 4-bit] --> B{Decoder Logic}
    B --> C[Segment Output a-g]
    C --> D[Current Limiting Resistor]
    D --> E[Seven-Segment Display]
    style A fill:#f9f,stroke:#333
    style E fill:#bbf,stroke:#333

上述流程图展示了从BCD输入到最终显示的完整信号链。值得注意的是,FPGA IO口通常驱动能力有限(约8–16mA),因此应在每段串联 限流电阻(通常220Ω–470Ω) ,防止过流损坏器件或引起电压跌落。

5.3 动态扫描显示技术实现

5.3.1 扫描频率选择与视觉暂留效应利用

静态驱动每位数码管需独立占用7+N个引脚(N为位数),对于6位显示即需42+6=48个IO,资源开销巨大。相比之下,动态扫描采用 时分复用 策略:所有数码管的段选线并联,仅公共端(COM)独立控制,通过快速轮询实现“伪并行”显示。

关键在于扫描频率的选择。人眼视觉暂留时间约为1/16秒(60ms),因此刷新率应高于60Hz,理想范围为100–200Hz。若低于此阈值,会出现明显闪烁;过高则增加CPU/FPGA负载且无实质收益。

设系统主频为50MHz,设计一个6位扫描控制器,每1ms切换一位,则总周期为6ms → 刷新率≈167Hz,满足要求。

reg [2:0] digit_sel;  // 当前选中的数码管编号(0~5)
reg [23:0] scan_counter;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        scan_counter <= 0;
    else
        scan_counter <= scan_counter + 1'b1;
end

assign update_digit = (scan_counter == 24'd50_000);  // 1ms @ 50MHz

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        digit_sel <= 3'd0;
    else if (update_digit)
        digit_sel <= digit_sel + 1'd1;
end

参数说明:
- scan_counter 计数至50,000对应1ms;
- update_digit 作为脉冲信号触发位切换;
- digit_sel 循环递增,实现轮询;
- 使用异步复位确保上电初始化可靠。

5.3.2 多位数码管轮询控制逻辑与时分复用

结合BCD数组与段译码器,构建完整扫描模块:

wire [3:0] current_bcd = bcd_digits[digit_sel];
wire [6:0] seg_data = seg_decoder_inst(seg_out);  // 已译码段信号

// 多路选择公共端
reg [5:0] anode_enable;

always @(*) begin
    anode_enable = 6'b0;
    anode_enable[digit_sel] = 1'b1;  // 激活当前位(共阴极需低电平?注意极性!)
end

// 若为共阴极,COM应接低电平表示选中
assign com[5:0] = ~anode_enable;  // 反相驱动

此时整体架构如下表所示:

信号名 位宽 方向 功能描述
clk 1 in 系统时钟(50MHz)
rst_n 1 in 低电平复位
bcd_digits [5:0][3:0] in 六个BCD输入
seg [6:0] out 段选信号(a~g)
com [5:0] out 位选信号(共阴极低有效)
flowchart LR
    subgraph Scan_Controller
        Counter --> MuxSelector
        MuxSelector --> CurrentBCD
    end
    CurrentBCD --> Decoder --> SegmentOutput
    MuxSelector --> COM_Selector --> COM_Output
    style Scan_Controller fill:#ffe4b5,stroke:#d2691e

该结构实现了完全同步的动态扫描,既能节省I/O资源(仅需7+6=13个引脚),又保证了显示稳定性。

5.4 显示模块抗干扰设计

5.4.1 消除闪烁与重影现象的方法

尽管动态扫描能有效节省资源,但在调试过程中常出现 闪烁 或 重影 问题。主要原因包括:

  • 扫描频率不足(<60Hz)
  • 段信号切换未与位选同步,产生过渡态
  • 公共端驱动能力弱,存在延迟

解决方案包括:

  1. 严格同步更新 :确保段信号在 digit_sel 更新后立即切换;
  2. 添加消隐阶段 :在切换前后短暂关闭所有COM,避免跨位串扰;
  3. 使用锁存缓冲 :在FPGA外加74HC573锁存器,稳定段信号。

改进后的控制逻辑:

always @(posedge clk) begin
    if (update_digit) begin
        seg <= seg_decoder(bcd_digits[next_digit]);
        com <= ~(1 << next_digit);
        next_digit <= (next_digit + 1) % 6;
    end
end

确保 seg 与 com 同拍更新,减少毛刺传播。

5.4.2 电流限流电阻配置与FPGA驱动能力匹配

FPGA每个IO口最大输出电流一般为±16mA,而单段LED压降约2V,工作电流5–10mA。若VCCIO=3.3V,则所需限流电阻:

$$ R = \frac{3.3V - 2V}{8mA} ≈ 162.5\Omega $$

推荐使用 180Ω 或 220Ω 贴片电阻,兼顾亮度与安全裕量。

若多位同时点亮(虽为瞬态),总电流可达 $6 \times 7 \times 8mA ≈ 336mA$,超出单电源轨承载能力。因此应:

  • 使用独立电源供电;
  • 在PCB布线上加粗电源走线;
  • 加入去耦电容(0.1μF)靠近数码管引脚。

最终系统应能在长时间运行下保持温度稳定、无亮度衰减,体现专业级设计水准。

6. 数字钟显示控制逻辑设计

6.1 整体系统架构整合与数据通路设计

在完成秒、分、小时计数器以及BCD译码和7段数码管驱动模块的设计后,下一步是将这些子系统有机地集成到一个统一的顶层控制架构中。该架构需确保从50MHz主时钟输入开始,经过分频生成1Hz秒脉冲,驱动三级计数器(秒、分、时)按正确进位规则递增,并将当前时间值以BCD格式输出至显示模块,最终通过动态扫描方式在6位共阴极数码管上稳定显示“时-分-秒”信息。

整个系统的数据通路可划分为三个层次:

  1. 时钟处理层 :由第三章设计的 clk_divider 模块实现,将50MHz输入分频为1Hz标准秒信号;
  2. 计时逻辑层 :包括第四章中的 counter_second 、 counter_minute 和 counter_hour 模块,采用同步级联方式,利用进位使能机制实现时间累加;
  3. 显示输出层 :第五章实现的 bcd_to_7seg 译码器与 display_scan 扫描控制器组成,负责将BCD数值转换为段选信号并轮询位选线。

以下为顶层模块的数据流连接示例(Verilog片段):

module digital_clock_top(
    input         clk_50m,      // 50MHz主时钟
    input         rst_n,        // 低电平复位
    output [5:0]  digit_select, // 数码管位选(低有效)
    output [7:0]  segment_data  // 段码输出(含小数点)
);

wire clk_1s;
wire [3:0] sec_unit, sec_ten;
wire [3:0] min_unit, min_ten;
wire [3:0] hr_unit,  hr_ten;

// 实例化分频模块
clk_divider #(.DIV_FACTOR(25_000_000)) u_div (
    .clk_in(clk_50m),
    .rst_n(rst_n),
    .clk_out(clk_1s)
);

// 实例化计数模块(省略详细参数传递)
counter_second u_sec (
    .clk(clk_1s), 
    .rst_n(rst_n), 
    .en(1'b1), 
    .sec_unit(sec_unit), 
    .sec_ten(sec_ten), 
    .carry_out()
);

counter_minute u_min (
    .clk(clk_1s),
    .rst_n(rst_n),
    .en(sec_unit == 4'd9 && sec_ten == 4'd5), // 秒满60触发
    .min_unit(min_unit),
    .min_ten(min_ten),
    .carry_out()
);

counter_hour u_hr (
    .clk(clk_1s),
    .rst_n(rst_n),
    .en(min_unit == 4'd9 && min_ten == 4'd5), // 分满60触发
    .hr_unit(hr_unit),
    .hr_ten(hr_ten),
    .carry_out()
);

// BCD合并成六位数组用于扫描
wire [23:0] bcd_array = {hr_ten, hr_unit, min_ten, min_unit, sec_ten, sec_unit};

// 实例化动态扫描模块
display_scan u_display (
    .clk(clk_50m),
    .rst_n(rst_n),
    .bcd_in(bcd_array),
    .digit_sel(digit_select),
    .seg_data(segment_data)
);

endmodule

上述代码实现了完整的信号流向控制: clk_50m → clk_divider → counters → bcd_array → display_scan → 输出驱动 。关键在于各模块间的时序对齐与使能信号协调,避免竞争冒险。

模块名称 输入信号 输出信号 功能描述
clk_divider clk_50m, rst_n clk_1s 生成精确1Hz秒脉冲
counter_second clk_1s, rst_n, en sec_unit, sec_ten 计数0~59,产生进位
counter_minute clk_1s, rst_n, en min_unit, min_ten 接收秒进位,计数0~59
counter_hour clk_1s, rst_n, en hr_unit, hr_ten 接收分进位,计数0~23,自动归零
display_scan clk_50m, rst_n, bcd_in digit_select[5:0], seg_data 动态扫描显示六位BCD数

此外,控制信号调度方面引入统一的同步复位机制,所有模块共享 rst_n 信号,防止异步复位引发亚稳态传播。状态监控可通过额外引出 debug_time[23:0] 总线,便于后期使用SignalTap II抓取内部计数值。

6.2 实际运行中的功能验证与问题排查

当工程编译下载至FPGA开发板后,常出现诸如“数码管不亮”、“显示乱码”或“计时不走”等问题。以下是典型故障分类及其排查方法:

常见问题分析表:

现象 可能原因 解决方案
所有数码管全灭 位选或段选极性错误 检查共阴/共阳接法,确认低电平有效
显示固定不变 分频未生效或计数器未启动 使用SignalTap II观测 clk_1s 是否跳变
出现乱码(如‘E’) BCD超出0~9范围导致译码错误 检查计数上限判断逻辑是否准确
个别位闪烁 扫描频率过低或驱动电流不足 提高扫描时钟至至少1kHz,加限流电阻
时间跳变异常 进位条件错误 在ModelSim中仿真验证进位信号建立时间
上电初始值不准 复位电路不可靠 增加上拉电阻+10kΩ与100nF电容滤波

使用SignalTap II进行片内逻辑分析的操作步骤:

  1. 在Quartus II中打开SignalTap II Logic Analyzer;
  2. 添加采样时钟(建议选择 clk_50m );
  3. 插入待观测节点: clk_1s , sec_unit , sec_ten , digit_select , segment_data ;
  4. 设置触发条件(例如 clk_1s'event and clk_1s = '1' );
  5. 编译并重新生成编程文件;
  6. 下载至FPGA后启动采集,观察波形是否符合预期。
flowchart TD
    A[启动SignalTap II] --> B[添加采样时钟clk_50m]
    B --> C[导入需监测信号列表]
    C --> D[设置触发条件为clk_1s上升沿]
    D --> E[编译工程生成.sof文件]
    E --> F[下载至FPGA并运行]
    F --> G[捕获实时波形数据]
    G --> H[分析计数跳变与时序一致性]

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文围绕“clk.rar_FPGA项目_quartus_数字钟”展开,详细介绍如何使用Altera Quartus II软件实现一个基于FPGA的数字钟系统。项目涵盖50MHz时钟分频、计数器设计、译码驱动7段数码管等核心模块,是FPGA初学者掌握数字逻辑设计与硬件编程的典型实践案例。通过本项目,学习者可掌握从逻辑设计到综合仿真的完整流程,深入理解分频、计数、译码等关键概念,并能使用Quartus II打开和运行工程文件clk.qar,进行后续开发与调试。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐