在FPGA设计中,几乎没人会主动使用锁存器Latch,但有时候不知不觉中你的设计莫名其妙地就生成了一堆Latch,而这些Latch可能会给你带来巨大的麻烦。
Latch,锁存器,一种可以存储电路状态信息的组合逻辑元件,和同样可以保存电路状态的时序逻辑元件–触发器(Flip-Flop,FF)不同,锁存器只在其使能端口有效时,将输入传递给输出;而在其使能端口无效时,输出则保持不变,就像被“锁住储存”起来了一样。
下图是一个典型的Latch的门电路结构。当使能信号E无效时,两个与门的输出均为0,对后面的SR锁存器即或非门无影响,所以无论输入D的值为1或0,输出Q的值都不会改变,就像被“锁住”了。
当使能信号E有效且输入D为1时,两个与门的输出分别为0和1、此时的输出Q值为1,与输入D一致。
当使能信号E有效且输入D为0时,两个与门的输出分别为1和0、此时的输出Q值为0,同样与输入D一致。
当Latch的使能信号有效时,直接让输入信号通过,使得输出完全等于输入。这一特性无疑会很容易地引入毛刺,使得设计不稳定;其次,Latch没有时钟信号,是一个异步电路元件,这就使得综合工具对Latch的静态时序分析十分困难。
而同步逻辑所使用的触发器FF,它的典型结构是这样的,由2个Latch+1个反相器组成。
当时钟信号Clock为高电平时,第1个Latch的使能有效,输入数据直接通过;由于反向器的存在,第2个Latch的使能端无效,所以其输出保持不变。
当时钟信号Clock为低电平时,第1个Latch使能无效,其输出保持不变,而这个输出也就是在时钟信号处于下降沿时的输入信号;由于反向器的存在,第2个Latch的使能端有效,输入直接传递到输出,此时最终输出的是在时钟下降沿时采集的输入信号。
可以看到,在同步电路中,FF的输出只在时钟边沿(这里是下降沿)变化,而在其他时间是保持不变的,这样一来,即使某些时刻在输入上出现了毛刺,但只要这个毛刺不发生在时钟边沿,那它就不会传递到输出进而影响后面的电路。
显然,同一个时钟周期比起来,时钟边沿持续的时间实在是微不足道,所以这大大降低了毛刺影响电路的概率。这也是同步电路相对于异步电路的一个大优点。
下面的一些代码风格是会生成Latch的:
在组合逻辑中,如果if-else语句没有写完整的话,是会生成Latch的(不管你是有意还是无意的),因为缺失掉的条件会被综合工具理解成不变,即锁存。
module test(
input data,
input en,
output reg q
);
always@(*)begin
if(en)
q = data;
end
endmodule
Vivado中生成的RTL电路如下–一个Latch。
生成Latch的时候,Vivado也会报警告,所以设计的时候请务必多多关注这些信息!
有些时候,尽管你的if-else语句已经很完整了,也还是会生成Latch,就像这样:
module test(
input data,
input en,
output reg q
);
always@(*)begin
if(en)
q = data;
else
q = q; //输出反馈给了输入,相当于没变
end
endmodule
这是因为输出直接反馈给了输入,使得输出等于输入,相当于没变化,依然是被锁存住了。
消除Latch的方法有两种:
module test(
input data,
input en,
output reg q
);
always@(*)begin
if(en)
q = data;
else
q = 1'b0; //q = 1'b1也可以
end
endmodule
module test(
input data,
input en,
output reg q
);
always@(*)begin
q = 1'b0; //q = 1'b1也可以
if(en)
q = data;
end
endmodule
两种方法中推荐第一种方法,因为比较规范,代码风格容易统一。这样的代码不会生成Latch,而是生成一个组合逻辑用的2选1MUX:
与if-else句类似,如果case语句的分支没有列举完全,也是会生成Latch的,比如这样:
module test(
input data0,data1,
input sel,
output reg q
);
always@(*)begin
case(sel)
1'b0: q = data0;
endcase
end
endmodule
Vivado中生成的RTL电路会有Latch:
类似的,有些时候尽管你的case语句已经很完整了,但因为出现了反馈语句(即输出反馈给输入)也还是会生成Latch,就像这样:
module test(
input data0,data1,
input sel,
output reg q
);
always@(*)begin
case(sel)
1'b0: q = data0;
1'b1: q = q; //反馈语句
endcase
end
endmodule
消除锁存器的方法有三种:
module test(
input data0,data1,
input sel,
output reg q
);
always@(*)begin
case(sel)
1'b0: q = data0;
1'b1: q = data1;
endcase
end
endmodule
module test(
input data0,data1,
input sel,
output reg q
);
always@(*)begin
q = data1;
case(sel)
1'b0: q = data0;
endcase
end
endmodule
module test(
input data0,data1,
input sel,
output reg q
);
always@(*)begin
case(sel)
1'b0: q = data0;
default: q = data1;
endcase
end
endmodule
在实践中,最推荐第三种写法。风格比较规范和统一。有时候编码的时候可能会把case语句的所有分支都列举出来,这种情况下依然建议保留default语句。
这样的代码就不会生成Latch了,而是生成一个组合逻辑用的2选1MUX:
如果以上出现Latch的写法转换成时序逻辑,是否也会生成Latch?比如这样:
module test(
input data,
input en,
input clk,
output reg q
);
always@(posedge clk)begin
if(en)
q <= data;
end
endmodule
答案是不会。只会生成一个寄存器reg(实际上就是一个FF)。
因为在时序逻辑中,即使不补全if-else语句,由于FF/REG的特性,它本身就可以在非时钟边沿存储数据,所以像这样的语句:
else q <= q; //保存值
写或不写在功能上都是一样的。但是在实践中,还是建议补全,目的是养成风格统一、良好的编码习惯。
网上有种说法:在FPGA设计中不使用Latch的一个很大原因是FPGA中没有现成的Latch资源,如果要用,则需要消耗非常多的资源来构建Latch。
**这种说法是错的。**以Xilinx的7系列FPGA为例,Latch实际上是其内部存在的固定资源。一个Slice中有一半的逻辑资源可以被配置为Latch或FF,另一半只能被配置成FF。但是需要注意的是,如果Silice中的FF被配置成了Latch,那么剩余的4个FF就不能用了,这样确实是会造成资源的浪费。
Xilinx的7系列FPGA中的FF可以被配置成两种类型的Latch: LDCE和LDPE。
首先Latch是被做成了和部分FF二选一的一个器件,而不是独立器件,这确实也说明在FPGA设计中确实需求不高,不然肯定就做成了FF这种固定的底层器件了。但这样一来,也增加了FPGA底层资源的灵活性,保留了更多的设计可能。
其次,Latch在某些特定应用还是需要的,比如IC设计中的time borrowing来提高时序性能、门控时钟、某些总线接口用来锁存数据等。
总之,Latch的用武之地确实有,但不多。 不是特殊需求,请尽量避免使用Latch!