FPGA时序逻辑设计入门:从Verilog计数器到Quartus II全流程实践
2026/6/22 21:22:03 网站建设 项目流程

1. 项目概述与核心价值

这次我们来聊聊FPGA入门路上一个绕不开的坎儿:时序逻辑电路的设计。很多朋友刚接触FPGA时,都是从组合逻辑开始的,比如做个简单的与或非门、译码器,感觉还挺直观。但一旦涉及到需要“记忆”和“同步”的时序逻辑,比如计数器、分频器、状态机,就有点懵了。这个实验,说白了,就是带你从组合逻辑的“静态世界”,一脚迈进时序逻辑的“动态世界”,理解时钟、寄存器这些核心概念到底是怎么在硬件描述语言(HDL)里活起来的。

我当年学这块的时候,最大的困惑不是语法,而是脑子里没有那个“硬件电路图”和“代码行为”之间的映射关系。写出来的代码要么综合不了,要么仿真结果和预想的完全两码事。这个实验的核心价值,就在于通过一个具体的、简单的时序电路设计实例(比如一个基础的计数器),让你亲手在Quartus II里走完从代码编写、功能仿真、综合布局布线到硬件下载测试的全流程。你会深刻体会到,用HDL(无论是Verilog还是VHDL)来描述一个时序电路,比用原理图拖拽寄存器、连线要高效、灵活得多,尤其是在设计需要频繁修改或规模较大的电路时。这不仅仅是学一个工具,更是建立一种“用代码思维设计硬件”的工程思想,这对后续做复杂的数字系统,比如通信协议处理、图像处理流水线,是至关重要的基础。

2. 实验环境准备与工具链解析

工欲善其事,必先利其器。做FPGA开发,环境搭对了,就成功了一半。这个实验我们聚焦在Intel(原Altera)的Quartus II Prime Lite Edition上,这是免费的版本,对于学习和小型项目完全够用。

2.1 Quartus II安装与器件库配置

首先,你得去Intel官网下载Quartus Prime Lite的安装包。下载时注意选择对应你操作系统的版本(Windows/Linux)。安装过程比较直接,但有一个关键点:器件支持。安装程序会让你选择安装哪些FPGA系列的器件库。对于大多数入门级开发板(比如Cyclone IV E系列的EP4CE6/10/22),你需要确保勾选了“Cyclone IV E”器件库。如果漏了,后续创建工程时可能找不到你的芯片型号,那就得回头重新安装支持包,比较麻烦。我的建议是,如果你硬盘空间不是特别紧张,可以把常见的Cyclone IV/V、MAX 10这些系列的库都装上,以备不时之需。

安装完成后,第一次启动可能会比较慢,这是正常现象。建议为你的工程单独建立一个文件夹,路径最好全英文,避免任何中文或特殊字符,这是为了避免工具链在综合、仿真时可能出现的各种诡异问题,算是一个老鸟的经验之谈。

2.2 两种设计方法:HDL输入与原理图输入

这个实验强调的“两种基本方法”,指的就是HDL文本输入和原理图(Block Diagram/Schematic)输入。原理图输入很直观,就像在纸上画电路图一样,从库里面拖出来与门、或门、D触发器,然后用线连起来。这种方法对于验证非常简单的逻辑、或者向不熟悉代码的硬件工程师展示设计意图时,有一定优势。但是,它的缺点极其明显:效率低下、难以维护、无法描述复杂行为、可移植性差。你想想,一个几十个触发器的状态机,用原理图连线会是什么灾难现场。

因此,现代FPGA设计,几乎百分之百以HDL输入为主流和首选。本次实验的核心目的,也是让你通过对比,切身感受到HDL的优越性。我们将用Verilog HDL(因其语法相对简洁,在业界应用更广泛)来完成核心设计。你需要准备的,就是一个文本编辑器(Quartus自带的就行,或者用VS Code等第三方编辑器搭配插件),以及理解待设计时序电路的基本功能要求。

2.3 实验目标电路分析

实验通常会要求设计一个经典的时序电路,例如一个4位二进制同步计数器,附带一个异步清零端和一个使能端。我们以此为例展开。

  • 功能:在时钟上升沿,如果使能有效且清零无效,则计数值加1;计满(1111)后自动归零。异步清零端一旦有效,立即将计数值清零,不受时钟控制。
  • 端口
    • clk:系统时钟输入。
    • rst_n:低电平有效的异步复位信号输入。
    • en:计数使能信号输入,高电平有效。
    • cnt_out[3:0]:4位计数结果输出。
  • 设计要点:这个电路包含了时序逻辑最核心的几个元素:时钟敏感(always @(posedge clk))、异步复位(always @(posedge clk or negedge rst_n))、同步使能控制。理解如何用Verilog准确地描述这些行为,是本次实验成败的关键。

3. 使用Verilog HDL设计计数器

现在,我们进入实操环节,用Verilog来实现上面分析的4位同步计数器。我会逐行解释代码,并说明背后的硬件含义。

3.1 模块定义与端口声明

首先,我们定义一个Verilog模块,并声明其输入输出端口。这相当于给我们的“电路黑盒子”画好接口。

module sync_counter_4bit ( input wire clk, // 系统时钟, wire类型表示物理连线 input wire rst_n, // 低电平有效的异步复位信号 input wire en, // 高电平有效的计数使能信号 output reg [3:0] cnt_out // 4位计数输出, reg类型因为在always块中被赋值 );

关键点解析

  • moduleendmodule是模块定义的开始和结束。
  • 端口方向有input(输入)、output(输出)、inout(双向,本例未用)。
  • 信号类型常用wirereg。简单理解:在assign语句或作为模块输入输出的连线,通常用wire;在alwaysinitial等过程块中被赋值的信号,必须声明为reg。注意,这里的reg不绝对等同于硬件寄存器,它只是一种描述方式。cnt_out最终会被综合成触发器(寄存器),因为它是在时钟沿触发的 always 块中被赋值的。

3.2 核心时序逻辑过程块

计数器的心脏是一个对时钟和复位敏感的always过程块。

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin // 异步复位:当rst_n为低电平时,立即执行,独立于时钟 cnt_out <= 4'b0000; // 使用非阻塞赋值 ‘<=‘ end else begin // 当时钟上升沿到来,且复位无效时 if (en) begin // 同步使能:只有en为高电平时,才执行计数操作 cnt_out <= cnt_out + 4'b0001; end else begin // 使能无效时,保持当前计数值不变 cnt_out <= cnt_out; end end end

代码深度解读与避坑指南

  1. 敏感列表@(posedge clk or negedge rst_n)

    • 这是描述时序逻辑的经典形式。它告诉综合工具:这个 always 块的行为由时钟clk的上升沿 (posedge) 或者复位rst_n的下降沿 (negedge) 触发。
    • 为什么是negedge rst_n因为我们定义rst_n低电平有效。当复位信号从高变低(下降沿)时,触发复位操作。虽然复位是异步的,但在这个描述风格中,我们将其边沿也放入敏感列表,是一种通用且安全的写法,综合工具能正确识别并生成异步复位逻辑。
  2. 异步复位与同步复位

    • 上述代码描述的是异步复位if (!rst_n)的判断独立于else后的时钟域,只要rst_n变低,立即清零。
    • 如果要改成同步复位,敏感列表应只写@(posedge clk),并且复位判断放在时钟沿之下:
      always @(posedge clk) begin if (!rst_n) begin // 此时复位信号也需要与时钟同步 cnt_out <= 4‘b0000; end else if (en) begin cnt_out <= cnt_out + 1‘b1; end end
    • 选择建议:异步复位设计简单,复位响应快,但要注意复位释放时是否与时钟沿对齐,否则可能导致亚稳态。同步复位更安全,但会消耗额外的组合逻辑资源。初学者项目用异步复位问题不大。
  3. 非阻塞赋值<=

    • 在描述时序逻辑的 always 块中,必须使用非阻塞赋值<=
    • 它与阻塞赋值=有本质区别。非阻塞赋值意味着块内所有语句的右值计算是同时进行的(在时钟沿到来瞬间),赋值操作在所有计算完成后“同时”更新。这精确模拟了寄存器在时钟沿同时捕获新值的行为。
    • 绝对禁忌:在同一个时钟沿触发的 always 块中混合使用阻塞和非阻塞赋值,或者对时序逻辑错误地使用阻塞赋值,这会导致综合前后仿真结果不一致,是常见的大坑。
  4. 使能信号en的处理

    • 使能en是同步的,它的判断发生在else分支下,即复位无效后的时钟沿有效区间。
    • else分支下的cnt_out <= cnt_out;这一行,明确描述了使能无效时输出保持原值。虽然有些编码风格会省略这一行(因为寄存器默认会保持值),但显式地写出来逻辑更清晰,可读性更强,尤其对初学者理解“保持”这一状态有帮助。

3.3 完整代码示例与测试激励

一个完整的设计文件通常还包括测试激励(Testbench),用于仿真验证。这里给出计数器的完整设计文件sync_counter_4bit.v和一个简单的测试平台tb_sync_counter_4bit.v

设计文件 (sync_counter_4bit.v):

`timescale 1ns / 1ps // 时间单位/精度 module sync_counter_4bit ( input wire clk, input wire rst_n, input wire en, output reg [3:0] cnt_out ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_out <= 4'b0000; end else begin if (en) begin cnt_out <= cnt_out + 4'b0001; end else begin cnt_out <= cnt_out; end end end endmodule

测试激励文件 (tb_sync_counter_4bit.v):

`timescale 1ns / 1ps module tb_sync_counter_4bit(); // 声明与被测模块连接的信号 reg clk; reg rst_n; reg en; wire [3:0] cnt_out; // 实例化被测模块 sync_counter_4bit uut ( .clk(clk), .rst_n(rst_n), .en(en), .cnt_out(cnt_out) ); // 生成时钟信号,周期20ns (50MHz) initial begin clk = 0; forever #10 clk = ~clk; // 每10ns翻转一次,形成周期20ns的时钟 end // 生成测试激励序列 initial begin // 初始化信号 rst_n = 0; // 初始处于复位状态 en = 0; #100; // 等待100ns,让全局稳定 // 测试1:释放复位,但使能无效,计数器应保持0 rst_n = 1; #50; if (cnt_out != 0) $display("Error at time %t: cnt_out should be 0 after reset release.", $time); // 测试2:使能有效,开始计数 en = 1; #200; // 让计数器运行10个时钟周期(200ns / 20ns) // 此时计数器应该从0加到9(1001),但因为我们只运行了200ns,即10个周期,从0开始加,第10个上升沿后值应为9(1001)? 需要精确计算。 // 更严谨的做法是在每个时钟沿检查,这里为简化,我们观察波形。 // 测试3:使能无效,计数器应停止并保持 en = 0; #100; // 记录下当前值,再过几个周期,值应不变。 // 测试4:再次使能有效,从保持值继续计数 en = 1; #80; // 再运行4个周期 // 测试5:异步复位有效,应立即清零 rst_n = 0; #15; // 复位后不久检查,注意复位是异步的,不需要等时钟沿 if (cnt_out != 0) $display("Error at time %t: cnt_out should be 0 immediately after async reset.", $time); #5; rst_n = 1; // 释放复位 // 测试6:让计数器计满一个循环(0-15) en = 1; #320; // 16个时钟周期 * 20ns = 320ns $display("Simulation finished."); $stop; // 停止仿真 end endmodule

注意:这个测试平台比较简单,主要靠观察波形判断功能是否正确。更严谨的测试会使用自动化的检查语句(assert)在每次时钟沿比较输出值。对于初学者,先学会看仿真波形是关键。

4. Quartus II 工程创建与综合实现

代码写好了,接下来就要在Quartus II里把它变成实实在在的硬件配置。

4.1 创建新工程与器件选择

打开Quartus II,选择File -> New Project Wizard

  1. 目录、名称、顶层实体:工程目录选择之前建好的英文路径。工程名和顶层实体名通常保持一致,例如sync_counter_4bit。记住,顶层实体名必须与你的主Verilog模块名完全一致,包括大小写。
  2. 添加设计文件:将写好的sync_counter_4bit.v添加进去。
  3. 选择器件:这是关键一步。根据你手头的FPGA开发板型号选择。例如,如果用的是EP4CE10F17C8(Cyclone IV E系列),就在Family里选Cyclone IV E,然后在Available devices里筛选找到它。务必选对,否则后续引脚分配会找不到对应位置。
  4. EDA工具设置:仿真工具选择ModelSim-Altera(如果你安装了的话),格式选择Verilog HDL。综合和布局布线工具就用Quartus自带的。
  5. 完成向导。

4.2 分析与综合(Analysis & Synthesis)

点击工具栏上的蓝色三角形(Start Analysis & Synthesis)或直接按Ctrl+K。这一步编译器会检查你的Verilog语法,并将其转换为基本的逻辑门和触发器组成的网表(Netlist)。

  • 常见错误
    • 语法错误:拼写错误、缺少分号、模块端口连接错误等。编译器会给出详细的行号和错误信息。
    • 未声明信号:在always块里用了未在模块中声明的变量。
    • 多重驱动:同一个信号在多个always块或assign语句中被赋值。
  • 关键报告:综合完成后,查看Processing -> Compilation Report。关注Analysis & Synthesis -> Resource Utilization,可以看到你的设计占用了多少个逻辑单元(LEs)、寄存器(Registers)。对于这个4位计数器,应该只占用极少的资源(几个LEs和4个寄存器)。

4.3 引脚分配(Pin Planner)

综合通过后,需要告诉Quartus你的输入输出信号对应到FPGA芯片的哪个物理引脚上。这需要参考你的开发板原理图。

  1. 打开Assignments -> Pin Planner
  2. Node Name列,你会看到clk,rst_n,en,cnt_out[0]cnt_out[3]
  3. Location列,为每个信号分配具体的引脚号。例如:
    • clk-> 连接到开发板晶振输出的引脚,如PIN_23
    • rst_n-> 连接到按键的引脚(通常按键按下为低电平),如PIN_24
    • en-> 连接到另一个按键或拨码开关,如PIN_25
    • cnt_out[3:0]-> 连接到4个LED灯对应的引脚,如PIN_40, PIN_41, PIN_42, PIN_43
  4. 电平标准:通常还需要设置I/O Standard,对于3.3V LVTTL的开发板,选择3.3-V LVTTL。这个信息也在原理图上。

实操心得:引脚分配一定要对照原理图仔细核对。一个常见的坑是,有些引脚有特殊功能(如专用时钟输入、配置引脚),不能随意分配。最好养成习惯,将分配好的引脚信息导出或记录下来,方便后续复用和调试。

4.4 全编译(Full Compilation)

引脚分配好后,点击紫色三角形(Start Compilation)进行全编译。这个过程包括综合、布局布线、时序分析和生成编程文件(.sof文件)。

  • 布局布线:工具将逻辑网表映射到FPGA内部具体的逻辑单元和互连线上。
  • 时序分析:工具会分析设计是否满足时序要求(建立时间、保持时间)。对于这个低速计数器,在几十MHz的时钟下通常都能通过。但如果看到“时序要求未满足”的警告,对于复杂设计就需要关注了。
  • 编译报告:全编译后,再次查看编译报告,确认没有严重警告(Critical Warning),并查看最终的资源占用和时序性能。

5. 功能仿真与硬件调试

在把设计下载到板子之前,仿真是验证逻辑功能是否正确的最重要环节。

5.1 使用ModelSim进行仿真

  1. 设置仿真工具:在Assignments -> Settings -> EDA Tool Settings -> Simulation中,确保Tool nameModelSim-AlteraFormat for output netlistVerilog HDL
  2. 生成测试文件列表:将之前写好的测试激励文件tb_sync_counter_4bit.v添加到工程中(作为仿真源文件,不参与综合)。
  3. 运行RTL仿真:在Tools -> Run Simulation Tool -> RTL Simulation。Quartus会自动启动ModelSim,编译设计文件和测试平台,并运行仿真。
  4. 查看波形:在ModelSim的Wave窗口,添加需要观察的信号(clk,rst_n,en,cnt_out)。运行一段时间后,你就可以看到信号随时间变化的波形。
    • 验证点
      • 复位期间,cnt_out是否为0?
      • 复位释放后,en为高时,cnt_out是否在每个时钟上升沿加1?
      • en变低后,cnt_out是否保持不变?
      • 异步复位rst_n变低时,cnt_out是否立即清零(无需等待时钟沿)?
      • 计数值从15(1111)回到0时,是否平滑?

5.2 硬件下载与在线调试

仿真通过后,就可以下载到FPGA开发板了。

  1. 连接硬件:用USB-Blaster或其他下载器连接电脑和开发板,给开发板上电。
  2. 编程器配置:打开Tools -> Programmer。确保硬件被识别(左上角显示USB-Blaster或你的下载器型号)。
  3. 添加文件:点击Add File,选择全编译生成的.sof文件(位于工程目录的output_files文件夹下)。
  4. 编程:勾选.sof文件对应的Program/Configure选项,然后点击Start。进度条走完,设计就下载到FPGA的SRAM中了(掉电丢失)。
  5. 功能验证
    • 按下复位按键(对应rst_n),观察所有LED(对应cnt_out)是否熄灭(全0)。
    • 松开复位,按下使能按键(对应en),观察LED是否开始以二进制形式循环闪烁(计数)。
    • 松开使能按键,观察LED是否停止在某个状态。
    • 验证异步复位:在计数过程中,随时按下复位键,LED应立即全灭。

5.3 常见问题与排查技巧

即使按照步骤操作,第一次也难免遇到问题。这里记录几个我踩过的坑和排查思路:

现象可能原因排查方法
编译失败,语法错误Verilog代码有拼写、格式错误。仔细阅读Quartus的错误信息,定位到具体行。检查模块名、端口名、信号名是否一致,是否缺少begin/end,是否误用中文标点。
仿真波形全为红色(不定态X)信号未初始化,或存在组合逻辑环路。1. 检查测试激励中是否对所有输入信号(clk,rst_n,en)赋予了初始值。
2. 检查设计代码中,是否在复位状态下对所有寄存器进行了明确赋值。对于计数器,复位时必须给cnt_out赋初值0。
3. 检查是否存在自己给自己赋值又无时钟控制的组合逻辑(如assign a = ~a;)。
仿真时计数器不计数使能信号en未有效拉高,或时钟信号未产生。1. 在ModelSim波形中检查en信号在复位释放后是否为高电平。
2. 检查测试激励中时钟生成逻辑是否正确,时钟周期是否合理。
3. 检查设计代码中en的判断逻辑是否正确(if (en))。
下载后LED无反应或常亮/常灭引脚分配错误;时钟未连接;复位电平不对。1.最可能的原因:引脚分配错误。重新核对原理图,确认每个信号分配的引脚号是否正确,特别是时钟和复位引脚。
2. 检查开发板上的时钟晶振是否工作。
3. 确认复位按键的电平:代码中是低电平复位(!rst_n),那么按键按下时,对应引脚应该是低电平。用万用表测量一下。
4. 检查LED是低电平点亮还是高电平点亮,你的代码输出逻辑是否与之匹配。
计数器速度过快,LED看起来常亮时钟频率太高(如50MHz),计数器从0到15只需要320ns,人眼无法分辨。这是预期行为,验证了功能正确。如果想看到可视化的计数效果,需要设计一个分频器,将高速时钟分频成例如1Hz或几Hz的低速时钟,再用这个低速时钟驱动计数器。这正好引出了下一个进阶实验内容。
Quartus无法识别下载器驱动未安装;USB线松动;下载器损坏。1. 检查设备管理器中是否有未识别的设备,重新安装USB-Blaster驱动。
2. 换一个USB口或USB线试试。
3. 确认下载器型号,并选择正确的编程器硬件设置。

硬件调试黄金法则:当硬件行为不符合预期时,首先怀疑引脚分配,其次是电源和时钟,最后再回头审视代码逻辑。用最笨但最有效的方法——分段验证:先写一个最简单的程序(比如只让一个LED闪烁),确保下载流程和基础硬件是好的,再逐步增加功能。

6. 从原理图输入看HDL的优越性

作为对比,我们可以看看如果用Quartus的原理图工具来实现同样的4位计数器,会是多么繁琐。

  1. 元件库调用:你需要从元件库(Symbol Tool)里找到4个D触发器(DFF),一个一个拖到图纸上。
  2. 连线:你需要手动连接时钟clk、复位rst_n到每个DFF的对应端口。需要用一个与门(AND)来处理使能信号en。最关键的是,你需要用4个加法器(或者更底层地用与非门搭出加法逻辑)来实现“加1”功能,并将低位的进位输出连接到高位的输入。
  3. 输出连接:将每个DFF的Q输出连接到输出端口,并命名网络为cnt_out[0]cnt_out[3]
  4. 修改与调试:如果你想将计数器改为从某个特定值开始计数,或者改为十进制计数器,你需要大幅修改原理图,重新连线,极易出错。

整个过程可视化,但效率极低,且图纸会随着电路复杂度增加而变得异常混乱。而使用Verilog,我们只需要修改几行代码:

  • 改位宽?output reg [7:0] cnt_outcnt_out <= cnt_out + 8‘b1;
  • 改计数模式? 把cnt_out + 1改成cnt_out - 1cnt_out + 2
  • 加一个同步加载功能? 只需增加一个输入端口loadload_data,然后在always块里加一个判断分支else if (load) cnt_out <= load_data;

这种修改的便捷性、代码的可读性和可维护性,是原理图无法比拟的。通过这个简单的实验,你应该能深刻体会到,HDL才是进行复杂、可重用数字系统设计的正确工具。原理图输入或许能帮你理解底层连接,但在实际工程中,它基本只用于顶层模块的简单连接(如果确实需要)或者查看综合后的网表。

7. 实验总结与进阶思考

走完这个完整的流程,你应该已经掌握了使用Quartus II和Verilog进行FPGA时序逻辑设计的基本技能。从理解时序概念,到编写可综合的Verilog代码,再到仿真验证、引脚分配、全编译和硬件下载,这是一套标准的FPGA开发流程。

我个人在带新手时发现,最容易出问题的环节往往不是代码本身,而是工程设置硬件连接。比如器件选错、引脚分配张冠李戴、下载器驱动异常。这些看似“低级”的问题,恰恰是工程实践的第一步。多踩几次坑,印象就深刻了。

这个计数器虽然简单,但它是一个“种子”。基于它,你可以轻松地扩展出更多功能:

  • 分频器:修改代码,让计数器在计到特定值时翻转一个信号,就能生成任意整数分频的时钟。但记住,在FPGA中,尽量避免用计数器分频后的时钟去驱动其他模块,推荐使用时钟使能(Clock Enable)方案,这涉及到同步设计思想,是下一个重要的知识点。
  • 移位寄存器:将加法的逻辑改为移位操作(cnt_out <= {cnt_out[2:0], cnt_out[3]};循环左移),就变成了一个移位寄存器。
  • 状态机:计数器的每个状态(0-15)可以看作是一个状态,结合一些组合逻辑判断状态转移条件,一个简单的状态机就诞生了。实际上,计数器本身就是状态机的一种特例(线性顺序状态机)。

最后再分享一个小技巧:养成给关键信号写注释的习惯,并且在仿真波形中多分组、多配色。比如把cnt_out用二进制和十进制两种格式显示,把时钟、复位、使能信号放在一起并用不同颜色区分,这样在调试时能极大提升效率。FPGA设计是一个迭代和调试的过程,清晰的代码和仿真环境是你最得力的助手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询