第15章:计数器

本章对应原书第 359~362 页,属于「第四篇 数字电路篇」。

本章在讲什么?(先看这个)

上一章的寄存器只会"存",这一章让它学会"数"。计数器 = 寄存器 + 加法器,是逻辑设计中使用频率最高的时序电路之一,可以实现对脉冲个数的计数,进而实现测量、计数、分频和控制等功能——后面章节的流水灯、呼吸灯、频率计,全都建立在计数器之上。本章先用 3 个 D 触发器搭出一个 3 位计数器,从电路层面理解"每来一个时钟上升沿就加 1"到底是怎么实现的,再用 Verilog 设计一个 3bit 计数器并仿真验证。

核心概念拆解

计数器简介

计数是最简单最基本的运算,计数器就是实现这种运算的逻辑电路。下面以一个 3 位计数器为例,从电路结构入手学习。

图15.1.1 3位计数器电路图

这个电路由 3 个 D 触发器(F1、F2、F3)、2 个异或门(G1、G2)和 1 个与门(G3)组成(触发器的复位信号图中没有画出)。上电复位后 Q0、Q1、Q2 的初始状态是 000。

上电后各触发器的输入分析

  • F1:输入 D 由"Q 非"反馈回来。Q 当前是 0,"Q 非"就是 1,反馈给 F1 的输入 D,所以 F1 的输入是 1;
  • F2:输入是 Q0 和 Q1 经过异或门后的结果。Q0=0、Q1=0,异或门"两个输入相同输出 0、不同输出 1",所以 F2 的输入是 0;
  • F3:输入是 Q0 与 Q1 经过与门后的输出,再与 Q2 异或。Q0 和 Q1 相与还是 0,这个 0 再与 Q2=0 异或,输出 0,所以 F3 的输入是 0。

来了第一个时钟上升沿:3 个 D 触发器同时触发,输入值被锁存,下一刻输出变为 Q2Q1Q0 = 001。如果把 Q2 当作二进制最高位、Q0 当作最低位,计数器输出的就是二进制 001,即十进制 1。

第二个上升沿到来时输出 010(十进制 2)……每多来一个时钟上升沿,计数器就加 1。计到第 8 个脉冲时,电路状态由 111 变回 000,完成一个循环周期——所以该电路也称为模 8 同步加法计数器

所谓"同步",是指电路中 3 个边沿型 D 触发器共用同一个时钟脉冲 CLK,时钟上升沿到来时同时触发(而不是前一级的输出当后一级的时钟)。

3bit 计数器的真值表:

时钟CLK 当前 Q2 Q1 Q0 下一刻 Q2 Q1 Q0
上升沿 0 0 0 0 0 1
上升沿 0 0 1 0 1 0
上升沿 0 1 0 0 1 1
上升沿 0 1 1 1 0 0
上升沿 1 0 0 1 0 1
上升沿 1 0 1 1 1 0
上升沿 1 1 0 1 1 1
上升沿 1 1 1 0 0 0

从真值表可以看出:每个时钟上升沿之后计数器值加 1,计满 8 个时钟周期后从 111 溢出回 000,循环往复。

实验任务

使用 Verilog 语言设计一个 3bit 的计数器电路。

程序设计——模块设计

模块命名为 counter。因为是时序逻辑,输入肯定有时钟和复位;输出为一个 3bit 的计数值。

图15.3.1 模块框图与端口

信号名 方向 端口说明
sys_clk 输入 系统时钟,50MHz
sys_rst_n 输入 系统复位按键,低电平有效
cnt 输出 3bit的计数器

绘制波形图

每当一个时钟周期到来,寄存器就累加 1:

图15.3.2 计数器模块波形图

cnt 循环从 0 计数到 7,每当计到最大值时,就从零开始重新累加。

编写代码

module counter(
    input               sys_clk,    //系统时钟
    input               sys_rst_n,  //系统复位

    output  reg [2:0]   cnt         //输出计数器
   );

//*****************************************************
//**                    main code
//*****************************************************

//描述计数器
always@(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        cnt <= 3'd0;
    else
        cnt <= cnt + 3'd1;
end

endmodule

代码解读:

  • 第 5 行定义了一个 3bit 的 cnt;
  • always@(posedge sys_clk or negedge sys_rst_n):时钟上升沿触发、低电平复位信号异步复位;
  • 系统复位信号 sys_rst_n 为低电平时,cnt 清零;否则每个时钟上升沿执行 cnt <= cnt + 3'd1 循环累加。

对比上一章的电路图你会发现:电路版的 3 位计数器需要 3 个触发器加 3 个门电路,而 Verilog 一句 cnt <= cnt + 1 就描述完了——综合工具会自动生成对应的加法器和寄存器。这就是硬件描述语言的威力。

仿真验证——编写 TB 文件

计数器模块的输入只有时钟和复位,TB 只需对这两个信号激励:

`timescale 1ns / 1ns //仿真单位/仿真精度

module tb_counter();

//reg define
reg              sys_clk  ; //系统时钟
reg              sys_rst_n; //系统复位

//wire define
wire [2:0]   cnt          ; //cnt计数器

//信号初始化
initial begin
    sys_clk = 1'b1;
    sys_rst_n <= 1'b0;
    #201
    sys_rst_n <= 1'b1;
end

//生成50MHz系统时钟
always #10 sys_clk <= ~sys_clk;

//例化计数器模块
counter  counter_inst(
    .sys_clk    (sys_clk  ),
    .sys_rst_n  (sys_rst_n),

    .cnt        (cnt      )
    );

endmodule

TB 结构与上一章相同:初始复位 201ns 后释放;always #10 sys_clk <= ~sys_clk; 生成 50MHz 时钟。

仿真验证

打开 Modelsim 运行 10us:

图15.3.3 计数器模块仿真波形

仿真波形与我们绘制的波形图一致:cnt 从 0 逐次累加到 7,然后归零重新开始,说明 3bit 计数器设计正确。

本章总结

本章讲解了后续学习和设计中最常用的计数器:从 3 个 D 触发器组成的模 8 同步计数器电路理解了计数原理,再掌握了用 Verilog 描述计数器的标准写法——"复位清零 + 时钟沿累加"。计数器是后面所有延时、分频、状态机的基础,务必通过后续章节的训练熟练掌握。

图片对照

本章原文图片包括 3 位计数器电路图、模块框图、波形图和仿真波形,均已插入对应小节。

初学者容易踩的坑

  • 计数器位宽不够:3bit 最大只能计到 7,如果计数范围更大要相应加宽位宽,否则会提前溢出(有时候溢出正是我们要的,比如本章的模 8 循环,但要"有意为之"而不是"意外发生")。
  • 忘加复位:计数器没有复位的话,上电后 cnt 的初值不确定(可能是 x 态),上板和仿真都会出问题。
  • 复位分支漏写或写错极性if(!sys_rst_n) 对应低电平有效的复位,写成 if(sys_rst_n) 逻辑就反了。
  • 误以为计数器是"异步"的:本章电路是同步计数器,所有触发器共用一个时钟;还有一种行波(异步)计数器把前级输出当后级时钟,各级延迟会累积,FPGA 设计中一般用同步计数器。
  • 混淆"计数值"和"计时值":cnt 是对时钟脉冲的计数,不是秒数;要变成时间得知道时钟周期(见拓展训练的换算)。

小结 & 下一步

本章掌握了计数器的电路原理和 Verilog 标准写法:复位清零、时钟沿循环累加。建议动手拓展:把现有例程稍作修改,设计一个"1 秒钟"的计数器。提示:板载晶振是 50MHz,一个时钟周期为 1/(50×10^6) s = 0.000_000_02s,所以 1 秒钟需要 50_000_000 个脉冲周期——计数器需要在 50MHz 时钟下计 50_000_000 次,想想计数器位宽要多少位才装得下。