数字逻辑实践4->面向硬件电路的设计思维--FPGA设计总述
本文是对实验课上讲解的“面向硬件电路的设计思维”的总结,结合数字逻辑课本,进行提炼和整理。
主要来源是课件与本人整理,部分参考了网络大佬的博客。
本文主要介绍不同于之前软件设计思维的硬件设计思维,从非阻塞赋值、并行、面积速度转换、同步电路设计原则、模块划分设计、if-case对比等方面进行整理。
内容太多,我整理了好几天,在浩如烟海的网络前有点无力,想想是自己的实践不够,有一些问题没有亲身体验;也不能一蹴而就,得久久为功。所以这篇文章就当作一个Verilog学习与FPGA设计的总述性文章,后续继续学习我会加深对这些知识的理解。
00 阻塞赋值和非阻塞赋值
概念
回忆一下课本上的相关内容。
-
阻塞赋值:
-
"="
-
Verilog编译器按照这些语句在always块中的先后顺序依次执行。
-
如果一个变量通过阻塞赋值语句赋值,则这个新赋的值会在这个block中的后续语句中使用。
-
相当于串行
-
-
非阻塞赋值:
-
"<="
-
always块中所有非阻塞赋值的语句在求值时所用的值是最初进入always时各个变量已经具有的值。
-
换一个角度讲,"<="左侧的被赋值变量,只在always结束时统一被更新。
-
相当于并行
-
时序电路实例
阻塞赋值
先来看一看这段代码:
1 module example5_5 (x1, x2, x3, Clock, f, g); 2 input x1, x2, x3, Clock; 3 output reg f, g; 4 always @(posedge Clock) 5 begin 6 f = x1 & x2; 7 g=f | x3; 8 end 9 endmodule
可以看到关键点是g的表达式,由于是阻塞赋值,所以相当于:
g=(x1 & x2) | x3;
综合出的电路如图:
非阻塞赋值
1 module example5_6 (x1, x2, x3, Clock, f, g); 2 input x1, x2, x3, Clock; 3 output reg f, g; 4 always @(posedge Clock) 5 begin 6 f <= x1 & x2; 7 g <= f | x3; 8 end 9 endmodule
这个区别之处就在于这个g,是x3与前一个 f 进行"|"运算。
思考
将阻塞赋值的示例代码中的两个执行语句互换位置,会发生什么情况?
可以料见影响比较大。
组合逻辑实例
相反的,如果我们要实现 f = a1a0 + a2a1这样一个函数;
阻塞赋值
1 always @(A) 2 begin 3 f = A[1] & A[0]; 4 f=f | (A[2] & A[1]); 5 end
非阻塞赋值
1 always @(A) 2 begin 3 f <= A[1] & A[0]; 4 f <= f | (A[2] & A[1]); 5 end
可见这段代码有两个特征;
-
非阻塞赋值的结果在always结束后才可以看到
-
多次赋值时,后覆盖前。
这两个特征使得第二个f语句出现问题,因为第二个语句
f <= f | (A[2] & A[1]);
右侧的f的值是不可见的。
总结
-
对组合逻辑建模采用阻塞赋值
-
对时序逻辑建模采用非阻塞赋值
-
用多个always块分别对组合和时序逻辑建模
-
尽量不要在一个always块里面混合使用阻塞赋值和非阻塞赋值。如果在同一个块即为组合逻辑又为时序逻辑,应使用“非阻塞赋值”
01 程序是并行执行的
这里给的例子没怎么看懂。自己查了一下。
先说结论:
-
各个always块是并行执行的,
-
always块和initial块之间是并行执行的,
-
begin-end块内是顺序执行的,
-
但是非阻塞赋值(<=)是并行执行的,阻塞赋值(=)是顺序执行的,这条优先。且硬件思想的集中体现就是前面提到过的非阻塞赋值带来的并行执行语句。
再回去看例子:
1 module test ( clk, reset, a, b ); 2 input clk; 3 input reset; 4 input [ 3:0 ] a; 5 output [ 3:0 ] b; 6 7 reg [ 3:0 ] tempa1, tempa2, b; 8 9 always @ ( posedge clk ) begin 10 if ( ! reset ) begin 11 tempa1<=0; 12 tempa2<=0; 13 b<=0; 14 end 15 else begin 16 tempa1<= a + 1’b1; 17 tempa2<= tempa1 + 1’b1; 18 b<= tempa2 + 1’b1; 19 end 20 end 21 endmodule
可以料见实现的电路:
下面借鉴了
波形图:
可以看到强调的还是上面说过的非阻塞赋值的特点。
02 程序的可综合性
实验任务里总有这么一句:“用可综合的代码......”
什么是可综合性,什么又是不可综合性呢?
下面学习了
这篇文章讲的挺多,但是我现在这个RTL级还没搞明白的菜鸟用不到这么多,基本筛选如下:
不可综合的Verilog语句:
-
initial
只能在test bench中使用,不能综合。
-
assign 和deassign
不支持对reg 数据类型的assign或deassign进行综合,支持对wire数据类型的assign或deassign进行综合。
-
fork join 不可综合,可以使用非块语句达到同样的效果。
这个块是并行执行的,但是不可综合。
敏感列表里同时带有posedge和negedge 如:(现在也碰不到
1 always @(posedge clk or negedge clk) 2 begin 3 ... 4 end
03 面积和速度的转换
这里我们说的面积:设计所占用的FPGA逻辑资源数目,一般用所消耗的触发器和查找表(还没学)来衡量。
速度:是指在芯片上可以稳定运行时能达到的最高频率
两者性能上的调配方法:
-
模块复用
-
串并变换
可以看到这种串并转换,用更大的面积(即多个子模块并行),达到高频率的效果。这一点后面还会再提到。
-
流水线
04 同步电路的设计原则
同步设计的优点:
-
可以有效避免毛刺的影响,提高设计的可靠性
-
可以简化时序分析过程
-
可以减少工作环境对设计的影响
设计原则:
(由于应用还不多,对这些体会还不深刻,简单记录:
-
单时钟
全局时钟网络的时钟是性能最优,最便于预测的时钟,具有最强的驱动能力
-
单时钟沿
混合时钟会使时序分析复杂、电路工作频率降低
-
避免使用门控时钟
-
即时钟不要与组合逻辑再进行组合,如下:
-
-
可能引起毛刺、偏移
-
-
在模块内部不要再产生时钟了。
05 模块划分的设计原则
封装复用
这有点像C++的封装。
上一层模块只负责下一层模块的依据(即原材料),而具体行为互不相关。
这样就保证了各个模块的相对独立性和内部结构的合理性,便于维护,也使得相同逻辑可以复用同一模块。
同步时序模块的寄存器划分原则
在设计时,应尽量将模块中的同步时序逻辑输出信号以寄存器的形式送出,以便于综合工具区分时序和组合逻辑;
并且时序输出的寄存器应符合流水线设计思路,能工作在更高的频率,以极大地提高模块吞吐量。
流水线设计思路是什么?
就是将组合逻辑系统地分割,并在各个部分(分级)之间插入寄存器,并暂存中间数据的方法。 目的是将一个大操作分解成若干的小操作,每一步小操作的时间较小,所以能提高频率,各小操作能并行执行,所以能提高数据吞吐率(提高处理速度)。
06 通用代码风格
逻辑复用与逻辑复制
对于我这个初学者来说,逻辑复用和逻辑复制十分相似,了解后就知道确实不同,主要是涉及性能衡量尺度:速度和面积的统筹。
-
逻辑复用是通过提高工作频率来节省面积的优化方法,经常用于存在多个资源可共享单元的设计中。
-
PS:相当于为了节省人力,而让一个人干三个人的活。
-
10MHZ乘法器
-
两个5MHZ乘法器
-
-
逻辑复制——面积换速度
-
逻辑复用是通过增加面积而改善设计时序的优化方法,经常用于调整信号的扇出。
-
举例就类似于上面的面积换速度
-
-
逻辑结构
-
链状结构
-
树状结构
if和case语句的使用原则
-
If语句指定了一个有优先级的编码逻辑,
而case语句生成的逻辑是并行的,不具有优先级。
这里的if的优先级就引起一些其他问题:
1 always@(in0,in1,in2,in3) 2 begin 3 sel = 2’b00 ; 4 if(in0) sel = 2’b00 ; 5 else if(in1) sel = 2’b01 ; 6 else if(in2) sel = 2’b10 ; 7 else if(in3) sel = 2’b11 ; 8 end 9 //当这里in0和in1都==1时,se1=2'b00而不是2'b01; 10 //这就是if-else的优先逻辑。
而case是并行的,没有优先级。
-
if语句可以包含一系列的表达式;/有时甚至一个else就可以是一个二路选择器。
而case语句比较的是一个公共的控制表达式。整个语句块一起构成了一个多路选择器。
这个很好理解。
-
通常if-else结构速度较慢,但占用的面积小;
case语句结构速度较快,但占用的面积较大。
-
嵌套的if语句如果使用不当,就会导致设计的更长延时。
-
如果想利用if语句来实现那些对延时要求苛刻的路径,应将最高优先级给最迟到达的关键信号。(最小生成树思想)。
-
有时为了兼顾面积和速度,可以将if和case语句合用。
举例
1 //if-else实现四选一 2 module sdata_if (clk, reset, x, s, y); 3 input clk; 4 input reset; 5 input [3:0] x; 6 input [1:0] s; 7 output y; 8 9 reg y; 10 always@(posedge clk)begin 11 if(!reset)begin 12 y<=0; 13 end 14 else begin 15 if(s==2'b00) 16 y<=x[0]; 17 else if (s==2'b01) 18 y<=x[1]; 19 else if (s==2'b10) 20 y<=x[2]; 21 else 22 y<=x[3]; 23 end 24 end 25 endmodule
想象一下是什么电路?
就是一个四选一,需要两位的控制信号,这个控制信号就正好是s。
1 //case语句 2 module sdata_if (clk, reset, x, s, y); 3 input clk; 4 input reset; 5 input [3:0] x; 6 input [1:0] s; 7 output y; 8 9 reg y; 10 always@(posedge clk)begin 11 if(!reset)begin 12 y<=0; 13 end 14 else begin 15 case(s) 16 2'b00: y<=x[0]; 17 2'b01: y<=x[1]; 18 2'b10: y<=x[2]; 19 2'b11: y<=x[3]; 20 end 21 end 22 endmodule 23 24
实现电路也是类似上面的电路。
避免意外锁存器
锁存器在课本里学过,是用于存储一位数据的元件,电平触发。
其特点是:锁存器在不锁存数据时,输出随输入变化;但一旦数据锁存时,输入对输出不产生任何影响。
而我们在设计电路时,应该避免无意之间产生这种锁存器,否则会导致一些逻辑上的错误。
引起意外锁存器的原因
翻阅了很多网上的总结。有一些规则比较复杂,考虑到我现在的水平,我只记录对初入门水平够用且好理解的方面。后续继续学习可以深入了解更多。
-
if……else……结构中缺少else
-
case结构中的分支没有包含所有情况且没有default语句。
建议
-
如果使用if语句,最好写上else分支;
-
如果使用case语句,最好写上default语句。
-
即使需要锁存器,也通过else分支或default分支来显式说明。而不要利用语言特性触发生成(因为不可控)
内容真的好多,一时间难以完全消化...
上传于2021年11月25日23时