跳转至

实验 2:计数器实验

从本实验开始就是硬件的实验,要使用 SystemVerilog 等硬件描述语言编写程序,通过 Vivado 来综合和实现。由于 Vivado 并不支持 macOS 操作系统,在苹果电脑上,只能通过虚拟机的方式使用 Vivado。在虚拟机里面可以安装 Windows 或者 Linux,建议安装 Linux,便于后续的开发工作。

模板文件

本实验的顶层模块模板:thinpad_top.srcs/sources_1/new/lab2/lab2_top.sv

实验目的

  1. 熟悉硬件描述语言及 thinpad_top 工程模板;
  2. 熟悉开发环境 Vivado,了解硬件系统开发的基本过程;
  3. 掌握组合逻辑与时序逻辑的区别。

实验环境

  1. 硬件环境:PC 计算机(Windows 10 或 Linux 操作系统),ThinPAD 开发板或云实验平台;
  2. 软件环境:FPGA 开发工具软件 Vivado。

实验内容

  1. 根据以下需求,用 SystemVerilog 语言实现一个简单的按扭控制计数器。

    1. 计数值宽度为 4 位,实时显示在实验板的七段数码管上;
    2. 使用微动开关 push_btn 控制计数功能,每次按下时计数值 +1;
    3. 任何时刻,按下微动开关 reset_btn 均可使计数值归零;
    4. 计数值达到 0xF 之后,不再继续增长;
    5. 使用工程模板中提供的 10MHz 时钟(clk_10M)及其对应的复位信号驱动整个逻辑。
  2. 在教学计算机 ThinPAD-Cloud 上验证该计数器的功能。

实验原理

本实验通过实现一个简单的计数器硬件,帮助同学们熟悉硬件描述语言的开发,并掌握 Vivado 软件和云实验平台的基础使用方法,为接下来的实验打好基础。

设计框图

硬件描述语言通常使用模块化的方法进行设计,即将一个复杂的硬件设计,拆分为多个各自独立的模块,以便于模块的复用。首先我们画出这个实验的设计框图,框图中 FPGA 内部每个方框为一个 SystemVerilog 语言的模块,模块之间的连线为各种信号。

flowchart LR
  counter[计数器]:::seq;

  clock[计数按钮]:::periph -- push_btn --> btn1[按键检测]:::seq;
  reset[复位按钮]:::periph -- reset_btn ---> counter;

  subgraph FPGA
    btn1 -- trigger --> counter;
    counter -- "count[3:0]" --> seg7[译码器]:::comb;
  end

  seg7 -- "dpy0[7:0]" --> dpy[七段数码管]:::periph;

  classDef comb fill:#e8f5e9,stroke:#43a047;   %% green
  classDef seq fill:#ffecb3,stroke:#ffa000;    %% orange
  classDef periph fill:#e3f2fd,stroke:#2196f3; %% blue
  linkStyle default opacity:1.0

框图约定

在本文档的设计框图中,我们约定使用不同颜色表示不同类型的模块,对应关系如下:

  • 橙色:时序逻辑模块,即使用 always_ff,并且由时钟驱动的逻辑模块;
  • 绿色:组合逻辑模块,即只包含 always_combassign 等语句,不含时钟的逻辑模块;
  • 蓝色:实验板 FPGA 外部的各类外设,如按键、数码管等。

标有 FPGA 的方框表示 FPGA 芯片内的设计。在目前的小实验中,我们只会用到一组时钟和复位信号,即一个时钟域。在后续的设计中,如果涉及到多个不同的时钟域,还将在其中嵌套方框,对不同时钟域内的模块进行划分。

此外,本次实验中标出了信号的位宽(如 [7:0]),出于简洁考虑,后续的实验中通常不再标出位宽,请同学们注意。同时,后续实验中也将省略复位按钮 reset_btn,但全部逻辑依然需要复位。

原理图

在设计框图的基础上,把各个模块的的信号和连线画出来,就得到了原理图:

原理图约定

在本文档的原理图中,每个模块的输入信号写在左侧,输出信号(或者同时输入输出的信号)写在右侧。

模块右下角的小三角表示这是一个时序逻辑模块,如果没有小三角,则是组合逻辑模块。

出于简洁考虑,原理图没有绘制时钟信号,后续实验中原理图也将省略复位信号 reset_btn。当你看到小三角的时候,就可以默认这个模块还需要输入时钟信号和复位信号。

模块分析

计数器模块

计数器模块实现本实验的核心功能,即接受 triggerreset_btn 信号,输出 4 位的 count 计数值信号。同时,由于需要保存当前的状态,其中需要包含寄存器元件,因此是一个时序模块。由此,可以推导出它的模块声明类似于:

module counter (
  // 时钟与复位信号,每个时序模块都必须包含
  input wire clk,
  input wire reset,

  // 计数触发信号
  input wire trigger,

  // 当前计数值
  output wire [3:0] count
);

// Actual logic goes here ...

endmodule

初步考虑实验需求,count 信号应该由一组寄存器维护状态,并在 trigger 的每个上升沿 +1,在 reset 的上升沿归零。如果你对 SV 的语法有初步了解,可能会写出类似下方的代码:

reg [3:0] count_reg;
always_ff @ (posedge trigger or posedge reset) begin
  if (reset) begin
    count_reg <= 4'd0;
  end else begin
    count_reg <= count + 4'd1;  // 暂时忽略计数溢出
  end
end
assign count = count_reg;

这段代码将上述的需求进行了行为级描述,如果对其进行综合、上板测试,也确实可以达到计数的功能。但是,这段代码是一个典型的异步逻辑设计,即没有全局统一时钟驱动的逻辑设计。异步逻辑在复杂的项目中,会导致静态时序分析失败,造成在硬件上难以调试的时序问题,因此应尽量避免使用。一个更合理的同步设计类似于:

// 注意此时的敏感信号列表
always_ff @ (posedge clk or posedge reset) begin
    if(reset) begin
        count_reg <= 4'd0;
    end else begin
        if (trigger)   // 增加此处
            count_reg <= count_reg + 4'd1;  // 暂时忽略计数溢出
    end
end
assign count = count_reg;

在同步设计中,实际上是在全局时钟 clk 的每个上升沿,对输入的 trigger 进行采样,如果其为高电平,就执行 count_reg 自增的语句。通过这一方法,使输出的 count_reg 寄存器与全局时钟同步,避免了时序问题。关于静态时序分析的知识,同学可以查阅相关资料详细了解,在后续的实验中也有涉及。

实验需求中还提到了当计数器达到 0xF 后,不再继续增长。该部分的 HDL 描述也比较简单,请同学自己尝试实现。

按键检测模块

在上述的同步计数器设计中,带来了一个新的问题。输入的全局时钟频率很高,达到了数十 MHz 量级,而人手每次按下按键,持续的时长将远高于时钟的一个周期,导致每次按键,实际上 count_reg 被自增了上万次,显然不是我们想要的效果。因此,需要一个模块,对按键信号的上升沿进行捕捉,将每一次“按下”的事件,转换为单个时钟周期的脉冲信号,提供给 counter 模块作为 trigger

根据这一想法,可以构建一个波形图如下:

clkpush_btntriggerab

波形中,在 a 处的时钟上升沿,按键被按下,产生了一个周期的 trigger 信号。而在 b 周期,按键被松开,不产生任何影响。这样,就实现了对按键上升沿进行检测的操作,因此这样的按键检测模块,也可以称为边沿检测器,是硬件设计中很常用的一个设计。

如果要实现这一波形给出的功能,可以使用一个寄存器,维护 push_btn 在上一个周期的状态,并在每个时钟周期判断当前状态与上一个状态的关系,给出相应的输出结果。通过适当地使用组合逻辑与时序逻辑,可以实现 trigger 输出延迟一个周期或者不延迟。这一模块的具体信号列表和实现,留给同学作为练习。

译码器模块

译码器模块的功能较为简单,就是将外部给出的数值信号,转换为七段数码管每个笔段具体的亮灭状态。工程模板中已经给出了这一模块,文件名为 SEG7_LUT.v,其使用方法可以参考 thinpad_top.sv 中给出的样例,在此不再赘述。

顶层模块

前文中提到,HDL 设计通常为层次化、模块化的设计,在设计完上述的几个模块之后,需要一个顶层模块,将这些模块的逻辑包含在其中,并将信号进行正确连接,以最终实现需求的功能,这一过程称为模块的例化和连线。借助软件语言的思维,可以将顶层模块理解为 main() 函数,在其中调用其他函数。顶层模块对外的信号,将要逐一与 FPGA 芯片引脚对应,并与芯片外的外设进行通讯。

硬件思维

此处借助软件语言中的函数调用,只是为了说明顶层模块存在的意义。实际上,与软件语言函数调用时的 overhead 不同,例化模块的过程不存在任何 overhead,在最终综合、实现时,所有例化的模块也将共同参与优化。因此,无需为了避免某种额外开销,减少模块的使用数量;而是应尽量对功能进行拆分,形成更多可复用的模块,提高设计的可读性。

在之前的章节中,已经对工程模板的顶层文件 thinpad_top.sv 进行了详尽的解析,此处简单讲解一下对以上模块的例化。

module thinpad_top (
    input wire clk_50M,
    ...
);

// 删除该部分内容,在其后添加你的模块例化
always_ff @ (posedge clk_10M or posedge reset_of_clk10M) begin
    ...
end

// 内部信号声明
reg trigger;
reg [3:0] count;  // 注意位宽!

// 计数器模块例化
// counter 是模块类型,u_counter 是当前实例的名字
// 同一个模块可以多次被例化,但它们均为独立的实体,相互之间不存在关联性
counter u_counter (
  .clk     (clk_10M),
  .reset   (reset_of_clk10M),
  .trigger (trigger),
  .count   (count)
);

// 按键检测模块
// 请根据自己的设计修改
button_in u_button_in (
  .clk     (clk_10M),
  .reset   (reset_of_clk10M),

  .push_btn (push_btn),
  .trigger  (trigger)
);

// 七段数码管译码器
// thinpad_top 中提供了该部分的样例,可以将其中的 number 替换为我们的 count 信号
// 注意:先阅读理解样例的功能,移除不需要的逻辑,避免各种问题

...
endmodule

思考题目

  1. 计数器模块中提到的异步逻辑与同步逻辑有何不同?可以通过观察 Vivado 综合后的电路原理图,并且查阅相关资料回答本题。

实验报告要求

  1. 完成代码后,在实验平台上进行实验,检验你的计数器的运行结果。给出实验过程的截图。注意不是 OJ 的通过截图。

  2. 回答实验思考题。


最后更新: 2023年9月27日
作者:Jiajie Chen (23.66%), Youyou Lu (8.93%), gaoyichuan (64.73%), cuibst (2.23%), kusile (0.45%)