一、组合逻辑和时序逻辑
数字电路可以分成两大类,一类叫组合逻辑电路,另一类叫做时序逻辑电路。
组合逻辑电路:由门电路组成,其某一时刻的输出状态只与该时刻的输入状态有关,而与电路原来的状态无关,并没有记忆功能。
时序逻辑电路:由锁存器、触发器和寄存器等单元组成,其某一时刻的输出状态不仅与该时刻的输入状态有关,而且与电路原来的状态有关,具有记忆功能。
而组合逻辑电路和时序逻辑在FPGA中并行执行这是毋庸置疑的,唯一不同的就是组合逻辑只要信号发生改变就随便改变,时序逻辑则需要随着时钟的上升沿或下降沿的到来而改变。
assign result1 = a & b;
always @(*) begin
if (~reset_n) begin
result2 = 0;
end
else begin
result2 = a & b;
end
end
always @(posedge clk or negedge reset_n) begin
if (~reset_n) begin
result3 <= 0;
end
else begin
result3 <= a & b;
end
end
在第一个clk时reset_n低电平有效,因此result2和result3被置为0,而result1则因为assign语句被置为1;
在第一个clk结束后reset_n变为高电平,因此result2被置为1,result3则因为没有检测到clk上升沿仍被置为0;
在第二个clk结束后a从1变为0,此时组合逻辑的result1和result2随之变化,而时序逻辑result3则在第三个上升沿clk时被置为0。
二、阻塞赋值和非阻塞赋值
阻塞赋值(=):在赋值时,先计算等号右手部分的值,再赋值给左边变量,直到该语句赋值完成,后面的语句才能执行,会阻塞后面的语句。
非阻塞赋值(<=):执行赋值语句右边,然后将begin-end之间的所有赋值语句同时赋值到赋值语句的左边,但是左边的变量的值不会立即更新,直到always块所有语句执行完,才将左边变量的值更新。
always @(posedge clk or negedge reset_n) begin
if (~reset_n) begin
b <= 0;
c <= 0;
end
else begin
b <= a;
c <= b;
end
end
always @(*) begin
e = d;
f = e;
end
可以简单这么理解:阻塞赋值时顺序执行,前一句执行完后才会执行后一句;非阻塞赋值时并行执行。
这样也就很好解释了为什么什么是打拍。如下图仿真所示
非阻塞赋值:
在第一个clk时, reset_n为低电平有效,因此b、c被置为0;
在第二个clk时,执行赋值语句,因为是非阻塞赋值,将b的值置为a、将c的值置为b(注意此时b不等2,b的值仍为0),赋值结束后b=2、c=0
在第三个clk时,执行赋值语句,此时b=2,将c的值置为b,因此赋值结束后c=2
阻塞赋值:
先执行e=d,执行完毕后e=3;
再执行f=e,执行完毕后f=3。
通过RTL图也可以理解,非阻塞赋值被综合成了两个D触发器,因此当一个clk到来时寄存器的值通过Q输出到c中,即b原来的0值,而a的值通过寄存器的D存进b里,;第二个clk到来时才会将b后来更新的值送到c寄存器。而阻塞赋值则被综合成了连线哈哈^_^
针对阻塞赋值和非阻塞赋值的使用:
1、时序逻辑,使用非阻塞赋值;
2、锁存器建模,使用非阻塞赋值;
3、组合逻辑,使用阻塞赋值;
4、当在同一个always块里面既为组合逻辑又为时序逻辑时,使用非阻塞赋值;
5、组合逻辑输出时,为消除毛刺会在输出端加一个触发器,即使用非阻塞赋值。
三、组合逻辑和时序逻辑中的if语句
3.1 if else语句
if-else具有优先级,只有if或者当前一级else if的条件不满足才会进行后面的判断,否则就直接执行当前条件下的语句。
always @(posedge clk or negedge reset_n) begin
if (~reset_n) begin
result <= 0;
end
else if(a==0) begin
result <= 0;
end else if(a==1) begin
result <= 1;
end else begin
result <= 2;
end
end
还需要注意的是if-else不能执行多个条件,如果所有的condition1、condition2、condition3都为真,程序会首先判断condition1为真,执行statement1,然后就会退出执行。
always @(posedge clk or negedge reset_n) begin
if (condition1) begin
state1;
end
else if(condition2) begin
state2;
end else if(condition3) begin
state3;
end else begin
state2;
end
end
3.2 多if语句
首先要再次明确一点的就是FPGA是并行执行的,在FPGA里是没有顺序执行这个概念的,但按照Verilog语法规定always里面的代码是顺序的,即按照代码顺序推理逻辑并综合。经常看到有文章说多个always块并行执行、always块内顺序,这个说法并不完全对的。
而当always块中有多个if语句时,程序是并行执行的,可以看到在组合逻辑中当三个sel信号有效时,三个result分别同时被a、b、c赋值,从RTL图中也能看出代码被编译器综合成为三个独立的二选一多路选择器,说明三个if语句确实是并行执行的。
always @(*) begin
result1 = 0; //避免产生latch
result2 = 0;
result3 = 0;
if (sel1)
result1 = a;
if (sel2)
result2 = b;
if (sel3)
result3 = c;
end
而在时序逻辑中生成的RTL图,因为需要用到时钟,因此在二选一多路选择器后多了一个D触发器,但仍然是并行运行了三个if语句。
always @(posedge clk) begin
result1 <= 0;
result2 <= 0;
result3 <= 0;
if (sel1)
result1 <= a;
if (sel2)
result2 <= b;
if (sel3)
result3 <= c;
end
但是有一种情况需要注意,一些文章说多if语句也具有优先级,这种说法是错误的。因为在他们的代码中错误的写法形成了多重驱动。比如以下多if写法,如果多个if语句相互独立,那么被赋值的变量只能出现在一个if语句里,否则就会产生多重驱动。但是这样写仍然会被编译器综合成功,因为编译器按代码顺序推理逻辑,result=c最后被推理综合,因此优先级最高,最后综合成顺序执行的硬件电路。
always @(*) begin
result = 0;
if (sel1)
result = a;
if (sel2)
result = b;
if (sel3)
result = c;
end
从仿真的结果也能看出,当三个sel同时有效时,result被优先级最高的c赋值。但这样的写法时错误的,应尽量避免。
3.3 if语句和赋值语句
if语句和赋值语句单独拎开其实没什么好说的,但是如果放一块就需要注意一些细节地方。如下面代码,always块中的count计数在每个CLK的上升沿加一,有两个if语句的判断,这里再次提醒FPGA是并行执行的,并且赋值语句和if语句没有优先级关系,因此加一和判断是同时执行的。
always @(posedge clk or negedge reset_n) begin
if (~reset_n) begin
count <= 0;
result1 <= 0;
result2 <= 0;
end
else begin
count <= count + 1'b1;
if (count == 8'd0) begin
result1 <= 8'd66;
end
if (count == 8'd5) begin
result2 <= 8'd88;
end
end
end
在第一个clk时,reset_n低电平有效,因此count、result1、result2都被置为0;
在第二个clk时,count=count+1=0+1、同时if(count==8'd0)有效(此时count仍为0),赋值和if语句判断并行执行
直到第六个clk,count=count+1=5+1、同时if(count==8'd5)有效(此时count仍为5),赋值和if语句判断并行执行
从RTL图中也能看出,编译器综合出了三个没有优先级关系的的D触发器,如果这方便还不理解的话可以去看FPGA时序分析与时序约束(一)-CSDN博客中建立时间和保持时间的概念去加深理解。
3.4 case语句
再浅说一个case语句,case语句条件选项可以有多个,而且这些条件选项不要求互斥。虽然这些条件选项是并发比较的,但执行效果是谁在前且条件为真谁被执行。
always@(*) begin
case(sel)
2'd0 : result = a;
2'd1 : result = b;
2'd1 : result = c;
2'd2 : result = d;
default : result = 0; //在组合电路中,如果所有分支没有列出,且不使用default,则会生成latch
endcase
end
如仿真所示
当sel=0时,程序执行2'd0 : result = a = ;
当sel=3时,程序执行default : result = 0;
当sel=1时,由于2'd1 : result = b比2'd1 : result = c在程序中的位置更靠前,因此执行2'd1 : result = b = 2。
四、 时序逻辑中的清零及保持
在时序逻辑电路中,如果没有对信号进行赋值更改,那么信号就会一直保持上一个周期的值,因此尽量将每个信号在每个分支都写出赋值。
always @(posedge clk or negedge reset_n) begin
if (~reset_n) begin
start <= 1'd0;
finish <= 1'd0;
count <= 8'd0;
end
else if (start_en) begin
start <= 1'd1;
end else if (start) begin //start一直为1因此一直计数
count <= count + 1'd1;
end else if (count==10) begin
start <= 1'd0;
finish <= 1'd1;
count <= 8'd0;
end else begin
start <= 1'd0;
finish <= 1'd0;
count <= 8'd0;
end
end
如以下代码。
当start_en高电平时,将start置为1;
在下一个clk到来时start为高电平进入计数,但是在这个分支仅对count进行了自增赋值 ,那么就会根据if else的优先级,always块会一直执行计数代码,忽略下面count==10的判断,而该分支没有对start进行处理,则start一直保持上一个周期的高电平
因此针对这段代码可以将count==10的判断优先级提高,先进行清零的判断,再进行计数。
always @(posedge clk or negedge reset_n) begin
if (~reset_n) begin
start <= 1'd0;
finish <= 1'd0;
count <= 8'd0;
end
else if (start_en) begin
start <= 1'd1;
end else if (count==10) begin
start <= 1'd0;
finish <= 1'd1;
count <= 8'd0;
end else if (start) begin
count <= count + 1'd1;
end else begin
start <= 1'd0;
finish <= 1'd0;
count <= 8'd0;
end
end
五、组合逻辑中的latch
锁存器(Latch)是一种电平触发的存储单元,数据存储的动作取决于输入时钟(或者使能)信号的电平值,即当其使能端端口有效时将输入传递给输出、当其使能端口无效时输出保持不变。
5.1 为什么会产生latch
在FPGA中构建组合逻辑电路,但此时就可能由于代码的不规范导致编译器综合出了带有锁存器的组合逻辑电路,产生的原因就是verilog代码中存在保持不变的情况,而组合逻辑时没有记忆功能的,为了记录当前的状态,就会引入锁存器来实现保持不变。
那么反映在verilog代码中,如果没有为每个状态都提供完整的赋值,系统将使用上一个状态的值,这可能导致电路中的某些元素在某些条件下保持其前一个状态,从而形成Latch;又或者在条件语句中没有覆盖所有可能的输入条件,那么系统将使用未覆盖条件下的上一个状态,从而引入Latch。
always @(*)
begin
if (condition1)
// 处理condition1
// 缺少对condition2的处理
end
5.2 为什么要避免latch
信号由于经由不同路径传输达到某一汇合点的时间有先有后的现象,就称之为竞争;由于竞争现象所引起的电路输出发生瞬间错误的现象,就称之为冒险。有竞争不一定有冒险,但出现了冒险就一定存在竞争。发生冒险时往往会出现一些不正确的尖峰信号,这些尖峰信号就是“毛刺”。
而latch的输出与使能信号有关, 当Latch的使能信号有效时,使得输出完全输入,输入状态可能多次变化,容易产生毛刺,增加了下一级电路的不确定性。如果毛刺刚好被采样到,那么电路的逻辑就有可能出现错误。
其次在FPGA中只有查找表LUT和触发器FF,没有锁存器LATCH资源,因此需要用LUT去模拟LATCH,导致占用更多的逻辑资源;并且锁存器不能异步复位,上电后处于不定态;锁存器没有时钟信号参与信号传递,使得静态时序分析变得更加复杂。
5.3 如何避免产生latch
5.3.1 使用完整的if-else语句
在组合逻辑中,不完整的if-else语句会产生latch。如下代码所示,当condition1为真时,执行q=d;但condition1不为真,系统会默认else分支下的q保持不变,导致产生latch。
always @(*)begin
if (condition1) begin
q = d;
end
end
如图所示:
在最开始,当d=0、condition=0时, q处于未知态(因为没有赋初值);
当d=1、condition=1时,执行q=d,此时q=1;
当d=0、condition=0时,由于代码中没有condition=0的分支,系统不对q进行赋值,保持q=1,此时就产生了latch。
从仿真结果可知这样的latch有两种避免方法:1.补全if-else语句;2.对输出赋初值
//方法1
always @(*)begin
if (condition1) begin
q = d;
end else
q = 0;
end
//方法2
always @(*)begin
q = 0;
if (condition1) begin
q = d;
end
end
此外当有多个信号时,需要将每个信号在每个分支都写出赋值,否则也会因if-else结构不完整导致产生latch。
//因if-else不完整产生latch
always @(*)begin
if (condition1) begin
q1 = d1;
end else
q2 = d2;
end
//将每个信号在每个分支都赋值
always @(*)begin
//q1 = 0; q2 = 0; //方法1
if (condition1) begin
q1 = d1;
q2 = 0;
end else //方法2
q1 = 0;
q2 = d2
end
5.3.2 使用完整的case语句
case语句产生的原因同if-else语句相同,如下所示,还有一种2'b11的情况没有写出
always @(*)begin
case(sel)
2'b00 : q = 4'b0001;
2'b01 : q = 4'b0010;
2'b10 : q = 4'b0100;
endcase
end
因此case语句同样有两种方法避免latch:1.将case语句选项补全;2.用default关键字来代替其他选项结果。
always @(*)begin
case(sel)
2'b00 : q = 4'b0001;
2'b01 : q = 4'b0010;
2'b10 : q = 4'b0100;
2'b11 : q = 4'b1000;
endcase
end
always @(*)begin
case(sel)
2'b00 : q = 4'b0001;
2'b01 : q = 4'b0010;
2'b10 : q = 4'b0100;
default : q = 4'b1000;
endcase
end
5.3.3 输出变量不能赋值给自己
在组合逻辑中,如果一个信号的赋值源头包含该信号本身,或者判断条件中有其信号本身的逻辑,则也会产生 Latch。因为此时信号具有存储功能,但是没有时钟驱动。这种产生latch的方式在 if 语句、case 语句、三元表达式中都可能出现,所以要避免这种写法。
always @(*)begin
case(sel)
2'b00 : q = q; //产生latch
2'b01 : q = 4'b0010;
2'b10 : q = 4'b0100;
default : q = 4'b1000;
endcase
end
always @(*) begin
if (q & sel) q = 1'b1 ; //产生latch
else q = 1'b0 ;
end
assign q = (sel && q) ? 1'b0 : 1'b1 ; //产生latch
5.3.4 使用完整的敏感信号列表
有时使用always@(a,b,c,d),但敏感列表没有列全,那么还是会保存之前的输出结果,产生latch。因此需要把敏感信号补全或者直接用 always@(*)。