时序逻辑的Verilog HDL描述
时序逻辑电路的输出,不仅和当前的输入状态有关,而且和原来的电路状态有关,也就是有存储记忆效应。基本的时序电路如D触发器、计数器、移位寄存器。
触发器
D触发器
触发器(Filp-Flop)是最基本的时序元件。在时钟触发边沿到来时,输出更新为前一时刻输入端的值;其他时间输出保持不变,无论输入是否变化。
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。
同步复位和异步复位
对于时序电路来说,有一个确定的初始状态是很重要的。也就是说,在系统复位时,触发器应该被赋予一个确定的值。所谓同步复位,是指复位信号到来后,并不会立即产生效果,而是要等到下一个触发沿到来时才有效。而异步复位则是立即生效,和触发沿无关。
module D_FF ( input D, input Clock, input Reset, output reg Q ); always @ (posedge Clock) if (Reset) Q <= 0; else Q <= D; endmodule
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
门控时钟和时钟使能
在一个系统中,时钟通常是多个触发器共用的,但是各个触发器往往需要独立地控制,并不希望每个时钟周期都装入数据。简单的办法是用与门控制时钟,代码如下。
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)。
FPGA内部的触发器,设计时均考虑了避免门控时钟,只要在Verilog代码中采用恰当的描述方式,综合工具就能够推断出使用时钟使能。如下。
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中用向量表示。
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。
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
计数器和移位寄存器
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
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)是在电平的作用下更新输出,称为电平触发。假设高电平触发,在触发信号维持高电平期间,输出跟随输入的变化;否则,输出维持不变,与输入无关。
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”,见下例。
case (op) 2'b00: y = a + b; 2'b01: y = a – b; 2'b10: y = a ^ b; default: y = 'bx; endcase
存储器
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块作为存储器,以节省宝贵的逻辑资源。
阻塞赋值和非阻塞赋值
在前面的例子中,已经使用了两种赋值符号“=”和“<=”,分别称为阻塞(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原来的值。
通常情况下,组合逻辑电路使用阻塞赋值,时序电路使用非阻塞赋值。但是更多的情况下,一个块中的赋值并不会像上面的例子描述的那样,写在后面的赋值语句的右值表达式使用前面赋值语句的左值,因此阻塞和非阻塞的并没有什么差别。此外应注意一个块内不能混用阻塞和非阻塞赋值。如果一个块内对同一个变量赋值多次,只有最后一个赋值语句是有效的。