时序逻辑的Verilog HDL描述

时序逻辑电路的输出,不仅和当前的输入状态有关,而且和原来的电路状态有关,也就是有存储记忆效应。基本的时序电路如D触发器、计数器、移位寄存器。

触发器

D触发器

触发器(Filp-Flop)是最基本的时序元件。在时钟触发边沿到来时,输出更新为前一时刻输入端的值;其他时间输出保持不变,无论输入是否变化。

例 1. 上升沿触发的D触发器
module D_FF
(
  input D,
  input Clock,
  output reg Q
);

always @ (posedge Clock)
  Q <= D;

endmodule

与描述组合逻辑相比,明显的区别是敏感列表。D触发器的敏感列表是时钟事件,posedge Clock表示时钟上升沿。很容易理解always块描述的是,当时钟上升沿到来时,always块内的表达式被重新计算,即输出Q被更新。特别需要注意的是,输入D一定不能出现在敏感列表中,否则当输入D变化时,即使没有时钟上升沿事件,输出Q也被重新计算,失去了时序电路的记忆特性。

如果需要时钟下降沿触发,用关键字negedge代替posedge

T触发器

T触发器的功能是,在时钟触发边沿到来时,若输入T为1,触发器翻转;否则,触发器保持不变。

例 2. 具有互补输出的T触发器
module T_FF
(
  input T,
  input Clock,
  output reg Q,
  output Q_n
);

always @ (posedge Clock)
if (T == 1)
  Q <= ~Q;
assign Q_n = ~Q;

endmodule

本例的T触发器具有反相输出端口Q_n,它和Q是互补输出。和多个assign语句类似,assign语句和always语句也是并发的,它们描述的硬件逻辑是同时工作的。

同步复位和异步复位

对于时序电路来说,有一个确定的初始状态是很重要的。也就是说,在系统复位时,触发器应该被赋予一个确定的值。所谓同步复位,是指复位信号到来后,并不会立即产生效果,而是要等到下一个触发沿到来时才有效。而异步复位则是立即生效,和触发沿无关。

例 3. 具有同步复位的D触发器
module D_FF
(
  input D,
  input Clock,
  input Reset,
  output reg Q
);

always @ (posedge Clock)
  if (Reset)
    Q <= 0;
  else
    Q <= D;

endmodule
例 4. 具有异步复位的D触发器
module D_FF
(
  input D,
  input Clock,
  input Reset,
  output reg Q
);

always @ (posedge Clock or posedge Reset)
  if (Reset)
    Q <= 0;
  else
    Q <= D;

endmodule

门控时钟和时钟使能

在一个系统中,时钟通常是多个触发器共用的,但是各个触发器往往需要独立地控制,并不希望每个时钟周期都装入数据。简单的办法是用与门控制时钟,代码如下。

例 5. 门控时钟
module D_FF
(
  input D,
  input Clock,
  input Load,
  output reg Q
);

wire gateClock;
assign gateClock = (Load & Clock);
always @ (posedge gateClock)
  Q <= D;

endmodule

这种方法称为门控时钟(Gated Clock),但是会带来毛刺glitches,增加时钟延迟clock delay、时钟偏差clock skew等不希望的效果。在ASIC设计中,为了避免门控时钟,可以在数据端增加一个多路器,选择数据来自输入端还是当前输出,图 1(a);或者采用JK触发器,如图 1(b)。

image
图 1. 避免门控时钟的解决方案

FPGA内部的触发器,设计时均考虑了避免门控时钟,只要在Verilog代码中采用恰当的描述方式,综合工具就能够推断出使用时钟使能。如下。

例 6. 具有时钟使能控制的D触发器
module D_FF
(
  input D,
  input Clock,
  input Load,
  output reg Q
);

always @ (posedge Clock)
if (Load == 1)
  Q <= D;

endmodule

数据寄存器

寄存器和D触发器可以看作同义词,触发器(flip-flop)侧重于表达逻辑实现,而寄存器(register)侧重于表达功能;在计算机硬件中,寄存器这个术语比触发器更常用。因此,寄存器的逻辑描述和D触发器的逻辑描述是一样的;寄存器通常是多位的,比如8位、16位等,在Verilog中用向量表示。

例 7. 异步复位的16位数据寄存器
module REG16
(
  input [15:0] D,
  input Clock, Reset,
  output reg [15:0] Q
);

always @ (posedge Clock or posedge Reset)
  if (Reset)
    Q <= 0;
  else
    Q <= Data;

endmodule

上面的例子,数据输入和输出是分开的端口(D和Q),下面给出一个双向输入输出端口的数据寄存器的例子,结构如图 2

20100223212645
图 2. 数据寄存器和三态缓冲器
例 8. 具有双向输入输出端口的16位寄存器
module Reg16
(
  inout [15:0] Data,
  input Clock, Reset, OE
);

reg [15:0] Q;
always @ (posedge Clock or posedge Reset)
  if (Reset)
    Q <= 0;
  else
    Q <= Data;

assign Data = OE ? Q : 16'bz;

endmodule

计数器和移位寄存器

例 9. 具有使能控制、可异步复位的加法计数器
module CountUp
(
  input Clock,
  input Reset,
  input Enable,
  output reg [7:0] Q
);

always @ (posedge Clock or posedge Reset)
  if (Reset == 1)
    Q <= 0;
  else if (Enable)
    Q <= Q + 1;

endmodule
例 10. 可预置数的移位寄存器(右移)
module Shifter
(
  input Clock, Load,
  input [7:0] D,
  output reg [7:0] Q
);

always @ (posedge Clock)
  if (Load)
    Q <= D;
  else
    Q <= {1'b0, Q[7:1]};

endmodule

锁存器

前面介绍的触发器都是在时钟边沿的作用下更新输出,称为边沿触发。而锁存器(Latch)是在电平的作用下更新输出,称为电平触发。假设高电平触发,在触发信号维持高电平期间,输出跟随输入的变化;否则,输出维持不变,与输入无关。

例 11. 锁存器
module Latch
(
  input D, En,
  output reg Q
);

always @ (En or D)
  if (En)
    Q <= D;

endmodule

注意敏感列表的形式和触发器不同,没有时钟边沿posedge或negedge的关键字,和组合逻辑的敏感列表形式相同。分析always块内的if-else语句,如果En为1,输出Q等于输入D;因为En和D都出现在敏感列表中,所以D的变化将引起重新计算块内的输出结果,从而使得输出Q跟随输入D变化。if-else语句省略了else分支,相当于En为0时,Q保持不变,表达了锁存器的存储特性。

综合工具正是根据Verilog的表达方式,推断设计者的意图是描述组合逻辑,还是触发器或者锁存器,所以采用正确的描述方式是非常重要的。对于组合逻辑的always块,if语句应该有else分支,case语句应该有default,否则可能会造成锁存器推断。如果并不需要else或default情况下的输出,可以赋值为“x”,见下例。

例 12. 组合逻辑中对不关心的输出赋值为x
case (op)
  2'b00: y = a + b;
  2'b01: y = a – b;
  2'b10: y = a ^ b;
  default: y = 'bx;
endcase

存储器

例 13. 存储器
module RAM
(
  output [7:0] Q,
  input [7:0] Data,
  input [3:0] Addr,
  input WR, Clk
);

reg [7:0] MEM [0:15];
always @ (posedge Clk) begin
  if (WR) mem[Addr] <= Data ;
end
assign Q = mem[Addr];

endmodule

存储器可以用数组来描述,如果存储容量较大,这样设计将占用大量的逻辑资源。需要说明的是, FPGA器件中都具备一定数量的RAM块,它们可以实现为单端口、双端口存储器、FIFO等,但是不能用作逻辑资源。所以在FPGA设计中应优先使用RAM块作为存储器,以节省宝贵的逻辑资源。

例 13的描述是基于触发器的,基于锁存器的存储器描述见后面[exa-25]

阻塞赋值和非阻塞赋值

在前面的例子中,已经使用了两种赋值符号“=”和“<=”,分别称为阻塞(Blocking)赋值和非阻塞(Nonblocking)赋值。

对于阻塞赋值,Verilog 编译器按照语句出现的顺序计算其值。如果一个变量被阻塞赋值,那么后续语句的计算使用这个变量的新值。这也是“阻塞”的含义,前面的语句阻塞了后面语句的计算。例如:

begin
  a = 1;
  b = a;
  c = b;
end

其结果是:c=b=a=1。

而所有非阻塞赋值语句的计算,是采样输入变量进入过程块时的值。那么某个变量的值对块中所有的语句来说都是同样的。每个赋值语句都是在过程块结束时更新输出值。块内所有的非阻塞赋值都是并行的。例如:

begin
  a <= 1;
  b <= a;
  c <= b;
end

其结果是:

a = 1;
b = a原来的值;
c = b原来的值。

通常情况下,组合逻辑电路使用阻塞赋值,时序电路使用非阻塞赋值。但是更多的情况下,一个块中的赋值并不会像上面的例子描述的那样,写在后面的赋值语句的右值表达式使用前面赋值语句的左值,因此阻塞和非阻塞的并没有什么差别。此外应注意一个块内不能混用阻塞和非阻塞赋值。如果一个块内对同一个变量赋值多次,只有最后一个赋值语句是有效的。