作为一个从嵌软跑来学 FPGA 的菜鸟,笔者每次动手写 verilog 代码的时候都有一种很拧巴的感觉,总觉的自己似乎还是有些地方的理解有问题。
网上看了无数篇讲硬件思维的文章,大家无一例外都是告诉你写 verilog 代码的时候最重要的就是要心中有电路,但是怎么才能心中有电路呢?数字电路的设计和软件系统设计的思维究竟有什么不同?
于是怀着这份疑问笔者打开了 Xilinx 的这篇官方文档:UltraFast Design Methodology Guide for Xilinx FPGAs and SoCs,希望能从中找到答案吧。
叠甲:以下内容均来自Xilinx 官方文档 ug949 以及笔者自己对文档内容的理解。
如有错误,还望斧正。
本文参考自 ug949-Ch .3 使用 RTL 进行设计
中的 RTL 编码指导
小节,本篇为其中的控制信号与控制集小节,大致探讨了 RTL 设计中的复位、时钟使能和控制使能这几个控制信号在综合时的行为和编码建议。
关于复位信号
数字电路设计中,复位的重要性不言而喻。而在 FPGA 设计中,不当的复位设计可能会导致综合器推断出不合适的器件。虽然在功能上可能没有问题,但是对于面积和硬件资源都是一种浪费。
不当复位有啥问题?
同步电路代码可能推断出一下几种器件:
- 查找表
- 寄存器
- 移位寄存器
- BlockRAM 或者 LUT RAM
- DSP48 寄存器
由于不同器件的复位方式不同,所以会导致使用不同的复位方式时综合出不同的器件。手册中为我们举了个典型的例子:在乘法器的输入或输出处描述的异步复位会导致寄存器被综合到 Slice 中而不是 DSP 块中。
什么时候应该复位?
鉴于 Xilinx 器件中有 GSR(全局置复位信号)的存在,在片子启动后,内部所有器件的初始状态其实都是已知的。因此没有必要单纯为了上电时初始化设备而编写复位代码。
关于应该如何进行复位设计,手册中是这么说的:
Xilinx highly recommends that you take special care in deciding when the design requires a reset, and when it does not. In many situations, resets might be required on the control path logic for proper operation. However, resets are generally less necessary on the data path logic.
即:很多时候,对控制路径的复位是必要的,而对数据路径的复位是不必要的。
Evaluate each synchronous block, and attempt to determine whether a reset is required for proper operation. Do not code the reset by default without ascertaining its real need.
在评估每一个同步块以后在决定是否要为其添加复位功能,不要起手就是一个复位同步块模板贴上去。为了便于评估每个模块是否确实需要复位功能,可以使用功能仿真来进行确认。
(PS:笔者自己就很喜欢贴复位同步块模板😭)
同步复位 or 异步复位?
首先,Xilinx 官方推荐我们使用同步复位方式。
对于大部分厂商而言,片子中的器件是具有异步复位的接口而不具有同步复位的接口的,所以异步复位相对于同步复位而言其实是更加节省资源的,这是异步复位的优点。
不过异步复位的缺点更是一堆:不仅不方便布线,还可能破坏 RAM 和移位寄存器中存储的内容。另外对于 DSP 48 和 BRAM 这种只有同步复位接口的器件而言,使用异步复位甚至会导致综合器不使用 DSP 和 BRAM 中的器件来实现你所写的功能,即前文所提到的不当的复位方式会导致的问题之一。
虽说同步复位挺好,不过同步复位需要复位信号的宽度至少要达到一个时钟周期才能成功复位的这点也很恼人。
Verilog HDL 同步复位和异步复位这篇文章中对复位信号进行了详细的探讨,其中提到了复位推荐使用的异步复位同步释放的方法其实在手册中也有提到:
When using asynchronous resets, remember to synchronize the deassertion of the asynchronous reset. Although the relative timing between clock and reset can be ignored during reset assertion, the reset release must be synchronized to the clock.
重构复位代码时的问题
👌,现在我已经完全掌握复位了💪,并且打算重构自己的复位代码打算大展拳脚!
于是我把自己代码中不需要复位的地方注释掉了:
always @(posedge clk or posedge rst) begin
if (rst) begin
//din dly1 <= 16'b0;
//din_dly2 <= 16'b0;
dout = 16'b0;
end else begin
din_dly1 <= din;
din_dly2 <= din_dly1;
dout <= din_dly2;
end
end
然而这样修改的结果是:din_dly2
和 din_dly1
的信号仍然会被异步复位。
显然这两个信号仍然处在异步复位的逻辑当中,而复位的 if 里面没有为这两个信号赋值看起来其实更像是你忘记把他们写进去了,而不是你不想对这两个信号进行复位,这时"聪明的"综合器就会一脸无奈的帮你把你忘记添加的东西补上去。
要想综合器能够正确的了解到你的意图,就必须摒弃上面这种“暧昧”的代码,转而使用更加清晰的表述:
always @(posedge clk) begin
din_dly1 <= din;
din_dly2 <= din_dly1;
end
always @(posedge clk or posedge rst) begin
if (rst) dout <= 16'd0;
else dout <= din_dly2;
end
另:当使用复位时,一定要确保设计中的所有寄存器都被复位。
关于时钟使能信号
如果你和笔者一样做过单片机的话,应该对低功耗设计有所耳闻。在低功耗设计中,降低功耗且不影响其他功能最有效的方法就是降低时钟频率,而在低功耗型号的单片机中,更是拥有直接将时钟暂时关闭的深度睡眠模式。在 FPGA 中时钟自然也是影响一个系统功耗的重要因素,因此通过关闭时钟使能可以做到在不影响系统功能的情况下显著降低系统功耗。然而,如果不能正确的使用时钟使能信号,甚至可能导致功耗不降反增。
时钟使能信号从何而来?
时钟信号从何而来?这其实是笔者看到时钟使能信号这个词语侯的第一反应。上节说的复位代码尚且是我们自己手写出来的,但是这个时钟使能信号笔者可以说是闻所未闻,所以还是很有必要了解一下这个信号到底是怎么来的。
手册上说,如果你在时序电路逻辑中写了一条分支不完整的条件语句(通俗来说就是 if 没写 else 分支),那么为了保证在 else 的情况中寄存器的值不发生变化,综合器就会综合出一个时钟使能信号。换句话说:时序电路中寄存器的值只有在时钟边沿才会更新,那我把时钟关了,就不会更新了(doge
不得不说这部分内容对笔者来说有点超纲,笔者一直以为没有写的 else 会被补上一句 reg_name <=reg_name
,没想到这两者其实还是不一样的。当然我们也不能听风就是雨,我们整一段简单的小代码来试试。
先写一段 if 分支完整,且在的代码:
module test (
input clk,
input a,
input b,
output reg c
);
always @(posedge clk) begin
if (a) begin
c <= b;
end else begin
c <= 1;
end
end
endmodule // test
显然这段代码应该是一个双端口选择器的结构,两个输入引脚分别接到 b 和 1,选择端口是 a,而输出则接到一个 D 触发器上构成一个时序电路,查看电路的详细设计和我们的构想是一致的:
那么我们来看看实际综合出来的原理图是什么样的:
抛开输入输出缓存用的 buff 不谈,实际的电路结构和我们所构思的电路结构是类似的, FDRE(单 D 触发器)虽然有时钟使能引脚,但是时钟使能是直接拉高的,并没有对我们的控制逻辑有任何影响。
那么我们再来对比一下没有 else 的代码:
module test (
input clk,
input a,
input b,
output reg c
);
always @(posedge clk) begin
if (a) begin
c <= b;
end
end
endmodule // test
详细设计显然是一个 D 触发器的逻辑:
而实际综合出来的原理图确实如手册所说,将输入信号连接到了时钟使能:
不过一个比较令人困惑的点是当寄存器的值在 else 分支中没有发生变化时,无论是否省略 else 分支都会将输入连接到时钟使能上,不过大致的行为也算是与手册上的说法一致。