Files
josh-blog-source/source/_posts/2020-12-05-josh-verilog-part-5.md
T

1095 lines
86 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Josh 的学习笔记之 Verilog(Part 5 —— RTL 设计与编码指导)
mathjax: true
comments: true
copyright: true
sticky: false
toc:
enable: true
number: false
wrap: true
expand_all: true
max_depth: 4
tags:
- Verilog
- HDL
- RTL
- 同步
- 多时钟
- 速度
- 面积
categories:
- - Verilog
- - Josh 的学习笔记
- Verilog
abbrlink: 53b8b42e
date: 2020-12-05 00:46:00
---
在[《Part 4 —— RTL 概念与常用 RTL 建模》](https://josh-gao.top/posts/ecb88422.html)中,通过具体的 RTL 建模建立了一些对 RTL 级描述的一些感性认识,本文将较深入地探讨一些 RTL 设计的基本规律。RTL 设计规律与方法是一个非常大的论题,在此不可能面面俱到,希望通过本文的介绍,引起大家的注意。如果大家能在日后的工作实践中,不断积累,有意识地积累基本设计原则、设计思想,将取得事半功倍的效果!
# 1. 一般性指导原则
RTL 级设计的评判标准有很多,如时序性能、所占面积、可测试性、可重要性、功耗、时钟域的分配、复位信号设计、是否与所用 EDA 工具匹配等。如果设计目标是在 FPGA 或 CPLD 等可编程逻辑器件上实现,则还需考虑是否能发挥这些 PLD 的结构特点等。根据以上所述这些目标的组合和优先级可以派生出很多不同的设计原则。这里仅仅讨论一般意义的指导原则。
这里抛砖引玉地提出 4 个基本设计原则,这些指导原则范畴非常广,不仅仅是要学习它们,更重要的是理解,并在今后的工作实践中充实、完善。
1. **面积和速度的平衡与互换原则**
面积和速度的平衡与互换原则提出了 RTL 设计的两个基本目标,并探讨了这两个目标对立统一的矛盾关系。
2. **硬件原则**
硬件原则重点在于提醒读者转化软件设计的思路,理解 HDL 语言设计的本质。
3. **系统原则**
系统原则希望读者能够通过全局、整体上把握设计,从而提高设计质址,优化设计效果。
4. **同步设计原则**
同步设计原则是设计时序稳定的基本要求,也是高速 RTL 设计的通用法则。
<!-- more -->
## 1.1 面积和速度的平衡与互换原则
这里“面积”是指一个设计所消耗的目标器件(如 FPGA/CPLD/ASIC 等)的硬件资源数量:
- 对于 FPGA,可以用所消耗的触发器(FF)和查找表(LUT)来衡扯;
- 对于 CPLD,常用宏单元(MC)衡量;
- 对于 ASIC 可以用设计的系统门衡量。
“速度”指设计在芯片上稳定运行时所能够达到的最高频率,这个频率由设计的时序状况决定,与设计满足的时钟周期、PAD to PAD Time、Clock Setup Time、Clock Hold Time 和 Clock-to-Output Delay 等众多时序特征量密切相关。
{% note %}
面积(Area)和速度(Speed)这两个指标贯穿着 RTL 设计的始终,是设计质量评价的终极标准。
{% endnote %}
这里就讨论一下设计中关于面积和速度的基本原则:{% label primary @面积和速度的平衡与互换 %}。
面积和速度是一对对立统一的矛盾体。要求一个设计同时具备设计面积最小,运行频率最高,这是不现实的。科学的设计目标应该是在满足设计时序要求(包含对设计最高频率的要求)的前提下,占用最小的芯片面积,或者在所规定的面积下,使设计的时序余量更大、频率更高。这两种目标充分体现了面积和速度的平衡思想。关于面积和速度的要求,不应该简单地理解为工程师水平的提高和设计完美性的追求,而应该认识到它们是和产品的质量、成本直接相关的。**如果设计的时序余量比较大,运行的频率比较高,则意味着设计的健壮性更强,整个系统的质量更有保证;另一方面,设计所消耗的面积更小,则意味着在单位芯片上实现的功能模块更多,需要的芯片数量更少,整个系统的成本也随之大幅度削减。**
作为矛盾的两个组成部分,面积和速度的地位是不一样的。相比之下,满足时序、工作频率的要求更重要一些,当两者冲突时,采用**速度优先**的准则。
面积和速度的互换是 RTL 设计的一个重要思想。从理论上讲,一个设计如果时序余量较大,所能跑的频率远远高于设计要求,那么就能通过功能模块复用减少整个设计消耗的芯片面积,这就是**用速度的优势换面积的节约**;反之,如果一个设计的时序要求很高,普通方法达不到设计频率,那么一般可以通过将数据流串/并转换,并行复制多个操作模块,对整个设计采取“乒乓操作”和“串/并转换”的思想进行处理,在芯片输出模块处再对数据进行“并/串转换”。从宏观上看,整个芯片满足了处理速度的要求,这相当于**用面积复制换取速度的提高**。面积和速度互换的具体操技巧很多,比如模块复用、“乒乓操作”、“串/并转换”等,需要在工作中不断积累。下面举例说明如何使用“速度换面积”和“面积换速度”。
### 1.1.1 “用速度的优势换面积的节约”举例
WCDMA(宽带码分多址)系统中使用到了快速哈达码(Fast Hadamard Transform, FHT)运算, FHT 由 4 步相同的算法完成,如[图 5-1](#图5-1) 所示。
<a name="图5-1"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-010-FastHadamardTransform.png!sign" alt="FHT 原理图"/>
</div>
FHT 单步算法如下:
$$
\begin{aligned}
&\mathrm{Out}[2i]=\mathrm{In}[2i] + \mathrm{In}[2i+8],&i=0\sim7\\
&\mathrm{Out}[2i+1]=\mathrm{In}[2i+1] - \mathrm{In}[2i+1+8],&i=0\sim7
\end{aligned}
$$
考虑流水线式数据处理的要求,最自然的设计方法就是设计不同端口宽度的 4 个单步 FHT,并用将这 4 个单步模块串联起来,从而完成数据流的流水线处理。该 FHT 实现方式的代码如下:
```verilog fhtpart.v
/******************************
该模块是 FHT 的顶层,调用 4 个不同端口宽度的单步 FHT 模块,完成整个 FHT 算法
******************************/
module fhtpart(
Clk,Reset,FhtStarOne,FhtStarTwo,FhtStarThree,FhtStarFour,
I0,I1,I2,I3,I4,I5,I6,I7,I8,
I9,I10,I11,I12,I13,I14,I15,
Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7,Out8,
Out9,Out10,Out11,Out12,Out13,Out14,Out15
);
input Clk; // 设计的主时钟
input Reset; // 异步复位
input FhtStarOne,FhtStarTwo,FhtStarThree,FhtStarFour;
// 4 个单步算法的时序控制信号
// FHT 的 16 个输入
input [11:0] I0,I1,I2,I3,I4,I5,I6,I7,I8;
input [11:0] I9,I10,I11,I12,I13,I14,I15;
// FHT 的 16 个输出
output [15:0] Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7;
output [15:0] Out8,Out9,Out10,Out11,Out12,Out13,Out14,Out15;
// 第 1 次 FHT 单步运算的输出
wire [12:0] m0,m1,m2,m3,m4,m5,m6,m7,m8,m9;
wire [12:0] m10,m11,m12,m13,m14,m15;
// 第 2 次 FHT 单步运算的输出
wire [13:0] mm0,mm1,mm2,mm3,mm4,mm5,mm6,mm7,mm8,mm9;
wire [13:0] mm10,mm11,mm12,mm13,mm14,mm15;
// 第 3 次 FHT 单步运算的输出
wire [14:0] mmm0,mmm1,mmm2,mmm3,mmm4,mmm5,mmm6,mmm7,mmm8,mmm9;
wire [14:0] mmm10,mmm11,mmm12,mmm13,mmm14,mmm15;
// 第 4 次 FHT 单步运算的输出
wire [15:0] Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7,Out8,Out9;
wire [15:0] Out10,Out11,Out12,Out13,Out14,Out15;
// 第 1 次 FHT 单步运算
fht_unit1 fht_unit1(
Clk,Reset,FhtStarOne,
I0,I1,I2,I3,I4,I5,I6,I7,I8,I9,I10,I11,I12,
I13,I14,I15,
m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,
m13,m14,m15
);
// 第 2 次 FHT 单步运算
fht_unit2 fht_unit2(
Clk,Reset,FhtStarTwo,
m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,
m13,m14,m15,
mm0,mm1,mm2,mm3,mm4,mm5,mm6,mm7,mm8,mm9,mm10,mm11,mm12,
mm13,mm14,mm15
);
// 第 3 次 FHT 单步运算
fht_unit3 fht_unit3(
Clk,Reset,FhtStarThree,
mm0,mm1,mm2,mm3,mm4,mm5,mm6,mm7,mm8,mm9,mm10,mm11,mm12,
mm13,mm14,mm15,
mmm0,mmm1,mmm2,mmm3,mmm4,mmm5,mmm6,mmm7,mmm8,mmm9,mmm10,mmm11,mmm12,
mmm13,mmm14,mmm15
);
// 第 4 次 FHT 单步运算
fht_unit4 fht_unit4(
Clk,Reset,FhtStarFour,
mmm0,mmm1,mmm2,mmm3,mmm4,mmm5,mmm6,mmm7,mmm8,mmm9,mmm10,mmm11,mmm12,
mmm13,mmm14,mmm15,
Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7,Out8,Out9,Out10,Out11,Out12,
Out13,Out14,Out15
);
endmodule
```
单步 FHT 运算举例如下(仅举例第 1 步的模块):
```verilog fht_unit1.v
/******************************
第 1 次 FHT 单步运算
******************************/
module fht_unit1(
Clk,Reset,FhtStar,
In0,In1,In2,In3,In4,In5,In6,In7,In8,In9,In10,In11,In12,
In13,In14,In15,
Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7,Out8,Out9,Out10,Out11,Out12,
Out13,Out14,Out15
);
input Clk; // 设计的主时钟
input Reset; // 异步复位
input FhtStar; // 单步 FHT 运算控制信号
// 单步 FT运算输入
input [11:0] In0,In1,In2,In3,In4,In5,In6,In7,In8,In9;
input [11:0] In10,In11,In12,In13,In14,In15;
// 单步 FT运算输出
output [12:0] Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7,Out8,Out9;
output [12:0] Out10,Out11,Out12,Out13,Out14,Out15;
reg [12:0] Out0,Out1,Out2,Out3,Out4,Out5;
reg [12:0] Out6,Out7,Out8,Out9,Out10,Out11;
reg [12:0] Out12,Out13,Out14,Out15;
// 补码运算
wire [11:0] In8Co =~ In8+1;
wire [11:0] In9Co =~ In9+1;
wire [11:0] In10Co =~ In10+1;
wire [11:0] In11Co =~ In11+1;
wire [11:0] In12Co =~ In12+1;
wire [11:0] In13Co =~ In13+1;
wire [11:0] In14Co =~ In14+1;
wire [11:0] In15Co =~ In15+1;
always @(posedge Clk or negedge Reset) begin
if(!Reset) begin
Out0<=0 ; Out1<=0 ; Out2<= 0; Out3<= 0;
Out4<=0 ; Out5<=0 ; Out6<= 0; Out7<= 0;
Out8<=0 ; Out9<=0 ; Out10<=0; Out11<=0;
Out12<=0; Out13<=0; Out14<=0; Out15<=0;
end
end
else begin
if(FhtStar) begin
Out0 <= {In0[11],In0 } + {In8[11],In8 };
Out1 <= {In0[11],In0 } + {In8Co[11],In8Co };
Out2 <= {In1[11],In1 } + {In9[11],In9 };
Out3 <= {In1[11],In1 } + {In9Co[11],In9Co };
Out4 <= {In2[11],In2 } + {In10[11],In10 };
Out5 <= {In2[11],In2 } + {In10Co[11],In10Co };
Out6 <= {In3[11],In3 } + {In11[11],In11 };
Out7 <= {In3[11],In3 } + {In11Co[11],In11Co };
Out8 <= {In4[11],In4 } + {In12[11],In12 };
Out9 <= {In4[11],In4 } + {In12Co[11],In12Co };
Out10 <= {In5[11],In5 } + {In13[11],In13 };
Out11 <= {In5[11],In5 } + {In13Co[11],In13Co };
Out12 <= {In6[11],In6 } + {In14[11],In14 };
Out13 <= {In6[11],In6 } + {In14Co[11],In14Co };
Out14 <= {In7[11],In7 } + {In15[11],In15 };
Out15 <= {In7[11],In7 } + {In15Co[11],In15Co };
end
end
end
endmodule
```
评估一下系统的流水线时间余量后,发现整个流水线有 16 个时钟周期,而 FHT 模块的频率很高,加法本身仅消耗 1 个时钟周期,加上数据的选择和分配所消耗时间,也完全能满足系统频率要求,所以**将单步 FHT 运算复用 4 次**,就能大幅度节约所消耗的资源。这种复用单步算法的FHT 实现框图如[图 5-2](#图5-2) 所示,由输入选择寄存、单步FHT 模块、输出选择寄存和计数器构成。
<a name="图5-2"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-020-FHTOperationReuseStructureDiagram.png!sign" width="500px" alt="FHT 运算复用结构图"/>
</div>
代码如下:
```verilog wch_fht.v
/******************************
优化后的 FHT 运算
******************************/
module wch_fht(
Clk,Reset,
PreFhtStar,
In0,In1,In2,In3,In4,In5,In6,In7,
In8,In9,In10,In11,In12,In13,In14,In15,
Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7,Out8,
Out9,Out10,Out11,Out12,Out13,Out14,Out15
);
input Clk; // 设计的主时钟
input Reset; // 异步复位信号
input PreFhtStar; // FHT 运算指示信号, 和上级模块运算关联
// FHT 的 16 个输入
input [11:0] In0,In1,In2,In3,In4,In5,In6,In7;
input [11:0] In8,In9,In10,In11,In12,In13,In14,In15;
// FHT 的 16 个输出
output [15:0] Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7;
output [15:0] Out8,Out9,Out10,Out11,Out12,Out13,Out14,Out15;
// FHT 输出寄存信号
reg [15:0] Out0,Out1,Out2,Out3,Out4,Out5,Out6,Out7;
reg [15:0] Out8,Out9,Out10,Out11,Out12,Out13,Out14,Out15;
// FHT 的中间结果
wire [15:0] Temp0,Temp1,Temp2,Temp3,Temp4,Temp5,Temp6,Temp7;
wire [15:0] Temp8,Temp9,Temp10,Temp11,Temp12,Temp13,Temp14,Temp15;
// FHT 运算控制计数器,和前一级流水线模块配合
reg [2:0] Cnt3; //count from 0 to 4,when Reset Cnt3=7;
reg FhtEn; //Enable fht culculate
always @(posedge Clk or negedge Reset) begin
if (!Reset)
Cnt3<= #1 3'b111;
else begin
if (PreFhtStar)
Cnt3<= #1 3'b100;
else
Cnt3<= #1 Cnt3-1;
end
end
always @(posedge Clk or negedge Reset)
if (!Reset)
FhtEn<= #1 0;
else begin
if (PreFhtStar)
FhtEn<= #1 1;
if (Cnt3==1)
FhtEn<= #1 0;
end
// 补码运算, 复制符号位
assign Temp0 = (Cnt3 == 4) ? {In0[11],In0[11],In0[11],In0[11],In0} :Out0;
assign Temp1 = (Cnt3 == 4) ? {In1[11],In1[11],In1[11],In1[11],In1} :Out1;
assign Temp2 = (Cnt3 == 4) ? {In2[11],In2[11],In2[11],In2[11],In2} :Out2;
assign Temp3 = (Cnt3 == 4) ? {In3[11],In3[11],In3[11],In3[11],In3} :Out3;
assign Temp4 = (Cnt3 == 4) ? {In4[11],In4[11],In4[11],In4[11],In4} :Out4;
assign Temp5 = (Cnt3 == 4) ? {In5[11],In5[11],In5[11],In5[11],In5} :Out5;
assign Temp6 = (Cnt3 == 4) ? {In6[11],In6[11],In6[11],In6[11],In6} :Out6;
assign Temp7 = (Cnt3 == 4) ? {In7[11],In7[11],In7[11],In7[11],In7} :Out7;
assign Temp8 = (Cnt3 == 4) ? {In8[11],In8[11],In8[11],In8[11],In8} :Out8;
assign Temp9 = (Cnt3 == 4) ? {In9[11],In9[11],In9[11],In9[11],In9} :Out9;
assign Temp10 = (Cnt3 == 4) ? {In10[11],In10[11],In10[11],In10[11],In10}:Out10;
assign Temp11 = (Cnt3 == 4) ? {In11[11],In11[11],In11[11],In11[11],In11}:Out11;
assign Temp12 = (Cnt3 == 4) ? {In12[11],In12[11],In12[11],In12[11],In12}:Out12;
assign Temp13 = (Cnt3 == 4) ? {In13[11],In13[11],In13[11],In13[11],In13}:Out13;
assign Temp14 = (Cnt3 == 4) ? {In14[11],In14[11],In14[11],In14[11],In14}:Out14;
assign Temp15 = (Cnt3 == 4) ? {In15[11],In15[11],In15[11],In15[11],In15}:Out15;
always @(posedge Clk or negedge Reset) begin
if (!Reset) begin
Out0<=0;Out1<=0;Out2<=0;Out3<=0;Out4<=0;Out5<=0;Out6<=0;Out7<=0;
Out8<=0;Out9<=0;Out10<=0;Out11<=0;Out12<=0;Out13<=0;Out14<=0;Out15<=0;
end
else begin
if ((Cnt3<=4) && Cnt3>=0 && FhtEn) begin
Out0[15:0] <= #1 Temp0[15:0] + Temp8[15:0];
Out1[15:0] <= #1 Temp0[15:0] - Temp8[15:0];
Out2[15:0] <= #1 Temp1[15:0] + Temp9[15:0];
Out3[15:0] <= #1 Temp1[15:0] - Temp9[15:0];
Out4[15:0] <= #1 Temp2[15:0] + Temp10[15:0];
Out5[15:0] <= #1 Temp2[15:0] - Temp10[15:0];
Out6[15:0] <= #1 Temp3[15:0] + Temp11[15:0];
Out7[15:0] <= #1 Temp3[15:0] - Temp11[15:0];
Out8[15:0] <= #1 Temp4[15:0] + Temp12[15:0];
Out9[15:0] <= #1 Temp4[15:0] - Temp12[15:0];
Out10[15:0] <= #1 Temp5[15:0] + Temp13[15:0];
Out11[15:0] <= #1 Temp5[15:0] - Temp13[15:0];
Out12[15:0] <= #1 Temp6[15:0] + Temp14[15:0];
Out13[15:0] <= #1 Temp6[15:0] - Temp14[15:0];
Out14[15:0] <= #1 Temp7[15:0] + Temp15[15:0];
Out15[15:0] <= #1 Temp7[15:0] - Temp15[15:0];
end
end
end
endmodule
```
为了便于对比两种实现方式的资源消耗,在 Synplify Pro 中对两种实现方法分别做了综合。两次综合选用的参数都完全一致,所以仅考察设计所消耗的寄存器和逻辑资源,选 中Disable I/O In sertion 选项,不插入 I/O,取消 Synplify Pro 中诸如 FSM Compiler、FSM Explorer、Resource Sharing、Retiming、Pipelining等综合优化选项。两次综合的结果如[图 5-3](#图5-3) 和[图 5-4](#图5-4) 所示。
<a name="图5-3"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-030-FHTPARTResource.bmp!sign" alt="未采用复用方案的“fhtpart”模块综合所消耗的资源"/>
</div>
<a name="图5-4"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-040-WCHFHTResource.bmp!sign" alt="采用复用方案的“wch_fht”模块综合所消耗的资源"/>
</div>
通过对比可以清晰地观察到,采用复用实现方案所占面积约为原方案的 1/4,而得到这个好处的代价是,完成整个 FHT 运算的周期为原来的 4 倍。这个例子通过运算周期的加长,换取了消耗芯片面积的减少,是前面所述的用频率换面积的一种体现。本例所述“频率换面积”的前提是,FHT 模块频率较高,运算周期的余量较大,采用4 步复用后.仍然能够满足系统流水线设计的要求。其实,如果流水线时序允许,FHT 运算甚至可以采用 1 bit 串行方案实现,该方案所消耗的芯片面积资源更少!
### 1.1.2 “用面积复制换取速度的提高”举例
举一个通过复制模块,并行处理达到高处理带宽的例子(可实际应用于无线系统、有线接入、路由器、视频系统等)。假设输入数据流的速率是 800 Mbit/s,而 FPGA 上设计的数据处理模块的处理速度最大为 200 Mbit/s,由于处理模块的数据吞吐量满足不了要求,直接在 FPGA 上实现这个设计是一个“impossible mission”!
这种情况下。就应该利用“面积换速度”的思想,至少复制 4 个处理模块,**首先将输入数据进行串/并转换**,然后利用这 4 个模块并行处理分配的数据,**最后将处理结果并/串变换**,完成数据速率的要求。在整个处理模块的两端看,数据速率仍然是 800 Mbit/s,而在 FPGA 的内部看,每个子模块处理的数据速率仅为 200 Mbit/s,其实整个数据的吞吐扯的保障是依赖于 4 个子模块并行处理完成的,也就是说利用了占用更多的芯片面积,实现了高速处理,通过使用“面积的复制换取处理速度的提高”的思想实现了设计。设计的示意框图如[图 5-5](#图5-5) 所示。
<a name="图5-5"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-050-MoreAreaforSpeed.png!sign" width="700px" alt="“面积换速度”示意图"/>
</div>
“面积复制换取速度提高”或者说“面积复制换取数据吞吐量的提高”是目前 FPGA 设计的常用技巧之一。现代高速FPGA 基本都有 DDR/DDR2/DDR3 的 I/O 硬件电路,通过这种 I/O 硬件电路,可以实现输入数据流的 1:2 和 1:4 的串/并变换和并/串变换,很多中高端 FPGA 在 I/O BANK 附近还专门设计了硬件的 DLL 或PLL,配合 DDR/DDR2/DDR3 的 I/O 硬件电路完成对应的采样时钟的降速和倍速。调用这些 I/O 的硬件模块,可以方便、可靠地实现数据流的并行化(输入端)和串行化(输出端),从而更利于实现“面积复制换取速度提高”,或“面积复制换取数据吞吐量的提高”的设计思想。
上面仅仅是对“面积换速度“思想的一个简单的举例,其实具体操作过程中还涉及很多的方法和技巧,例如,对高速数据流进行串/并转换,采用“乒乓操作”方法提高数据处理速率等。希望读者通过平时的应用进一步积累经验。
## 1.2 硬件原则
硬件原则主要针对 HDL 代码编写而言。首先应该明确 FPGA/CPLD、ASIC 的逻辑设计所采用的硬件描述语言(HDL) 同软件语言(如 C、C++ 等)是有本质区别的!以 Verilog 语言为例,虽然 Verilog 很多语法规则和 C 语言相似,但是 Verilog 作为硬件描述语言,它的本质作用在于描述硬件,应该认识到 Verilog 是采用了 C 语言形式的硬件的抽象,它的**最终实现结果是芯片内部的实际电路**。
所以评判一段 HDL 代码优劣的最终标准是其描述并实现的硬件电路的性能(包括面积和速度两个方面)。评价一个设计的代码水平较高,仅仅是说**这个设计由硬件向 HDL 代码这种表现形式转换得更流畅、合理**。而一个设计的最终性能,在更大程度上取决于设计工程师所构想的硬件实现方案的效率以及合理性。
初学者,特别是由软件转行的初学者,片面追求代码的整洁、简短,这是错误的,是与评价 HDL 的标准背道而驰的!正确的编码方法是,首先要做到对所需实现的硬件电路“胸有成竹”,对该部分硬件的结构与连接十分清晰,然后再用适当的 HDL 语句表达出来即可。
前面已经讨论过 HDL 语言与 C 语言等软件语言相比的最显著区别在于:HDL 语言便于描述“互联”、”并发”、”时间”这 3 个硬件设计的基本概念。
- **互连(connectivity)**: 互连是硬件电路的一个基本要素,在 C 语言中,并没有直接可以用来表示模块间互连的变扯;而 HDL 的网线型变最则专为模块互连而设计,描述电路连接清晰明确。如 Verilog 的 `wire` 型变量配合一些驱动结构就能有效地描述各个模块直接的端口与网线连接关系。
- **并发(concurrency)** : C 语言天生是**串行**的,不能描述硬件之间并发的特性, C 语言编译后.其机器指令在 CPU 的高速缓冲队列中基本是顺序执行;而 Verilog 可以有效地描述并行的硬件系统,硬件系统比软件系统速度快、实时性高的一个重要原因就是硬件系统中**各个单元的运算是独立的,信号流是并行的**。所以在使用 HDL 建模时,应该充分理解硬件系统的并行处理特点,合理安排数据流的时序,提高整个设计的效率。
- **时间(time)**:C 程序运行的时候,没有一个严格的时间概念,程序运行的时间长短,取决于处理器本身的性能;而 HDL 语言定义了绝对和相对的时间度量,在仿真时可以通过时间度扯与周期关系描述信号直接的时间关系。
Verilog 作为一种 HDL 语言,对系统行为的建模方式是分层次的。比较重要的层次有系统级(System)、算法级(Algorithm)、寄存器传输级(RTL)、逻辑级(Logic)、门级(Gate)和电路开关级(Switch)等。系统级和算法级与 C 语言更相似,可用的语法和表现形式也更丰富。自 RTL 级以后,HDL 语言的功能就越来越侧重于硬件电路的描述,可用的语法和表现形式的局限性也越大。相比之下, C 语言与系统级和算法级 Verilog 描述更相近一些,而与 RTL 级、Gate 级、Switch 级描述从描述目标和表现形式上都有较大的差异。
下举例说明 RTL 级 Verilog 描述语法和 C 语言描述语法的区别。
在C 语言的描述中,为了使代码执行效率高,表述简洁,经常用到下面的 `for` 循环语句:
```c
for (i = O; i < l6; i++)
DoSomething();
```
但是在实际工作中,除了描述仿真测试激励(testbench)时使用 `for` 循环语句外,RTL 级编码中**必须要慎用 `for` 循环**。其原因是 **`for` 循环会被综合器展开为所有变量情况的执行语句,每个变量独立占用寄存器资源,有些情况下不能有效地复用硬件逻辑资源,造成资源浪费**。在 RTL 硬件描述中,遇到类似算法,常用的方式是先搞清楚设计的时序要求,做一个 `reg` 型计数器,在每个时钟沿累加,并在每个时钟沿判断
计数器情况,做相应的处理,能复用的处理模块尽量复用,即使所有操作不易复用,也可以采用 `case` 语句展开描述。代码如下:
```verilog
reg [3:0] counter;
always @(posedge clk)
if (syn_rst)
counter <= 4'b0;
else
counter <= counter + 1;
always @(posedge clk) begin
case (counter)
4'b0000:
4'b0001:
...
default:
endcase
end
```
另外,在 C 描述中有 `if ... else` 和 `switch` 条件判断语句,其语法如下:
```c
if (flag) // 表示flag为真
...
else
...
```
```c
switch (variable) {
case value1: ...; break;
case value2: ...; break;
...
default: ...; break;
}
```
两者之间的区别主要在于 `switch` 是多分支选择语句,而 `if` 语句只有两个分支可供选择。虽然可以用嵌套的 `if` 语句来实现多分支选择,但那样的程序冗长难读。
对应 Verilog 也有 `if ... else` 语句和 `case` 语句,`if` 语句的语法相似,`case` 语句的语法如下:
```verilog
case (var)
var_value1: ...;
var_value2: ...;
...
default: ...;
```
通过 [《Part 4 —— RTL 概念与常用 RTL 建模》的 3.10 小节](https://josh-gao.top/posts/ecb88422.html#3-10-%E7%94%A8-case-%E5%92%8C-if-else-%E5%BB%BA%E6%A8%A1)的学习,我们会发现 `case` 语句和 `if ... else if ... else if ...` 语句以及 `if ... if ... if ...` 语句建模时可以建立无优先级和带有优先级的判断结构,使用 HDL 语言建模时,关键在于其综合实现结果的硬件结构。
这里进一步引申讨论一下 Verilog 的 `for` 循环。前面讲过 Verilog 语言是分层次的,即使同一个语法关键字在不同的应用层次也有不同的理解,for 循环就是一个非常好的例子。
- `for` 循环在行为级描述测试激励时的应用:前面介绍过,推荐使用 Behavior 级方式描述测试激励,在描述测试激励时,推荐使用 `for` 循环。好处主要有两个,一是描述简单,代码清晰;二是仿真器会对 `for` 循环开放一片内存,提供代码执行效率,加快仿真进程。
[comment]:<> (如7. 2. 1 小节就论述了如何使用for 语句实现遍历测试。)
- `for` 循环在 RTL 级描述硬件电路时的应用:在 RTL 级描述硬件时,一定要慎用 `for` 循环。前面已经介绍过,`for` 循环在硬件实现时会被综合器展开,不利于硬件资源复用,如果应用不当,还会造成资源浪费。但是任何问题不是绝对的,如果用户非常清晰 `for` 循环会被综合器展开这一基本原则,则可以逆向思维,对某些硬件上无法复用的展开结构抽象为 `for` 循环描述,大大提高代码的可读性。
[comment]:<> (例如8. 6. 2" 案例分析“小节就分别列举了使用展开结构和使用for 循环语句对某"冒泡排序“寻找最小值电路的建模方法,请读者仔细琢磨。)
## 1.3 系统原则
系统原则包含两个层次的含义:一是实现的目标器件本身可以看作一个系统,需要充分有效地发挥该系统的每个单元的功效。如果设计的实现目标为 FPGA,因为当代 FPGA 内嵌了很多固有的硬件资源(如:可编程输入/输出单元、基本可编程逻辑单元、嵌入式块 RAM 、丰富的布线资源、底层嵌入功能单元和内嵌专用硬核等),如何合理地使用这些硬件资源,对设计的全局有个宏观上的合理安排,比如合理安排时钟域、模块复用、约束、面积和速度等问题就显得至关重要。如果实现目标是 SoC,则需要分析什么样的算法和功能适合放在硬件系统里面实现,什么样的算法和功能适合放在微处理器系统(如 DSP、CPU 等)里面实现,并进一步合理划分软硬件之间的数据交换。
从更高层面上看,对于任何一个硬件系统,如何进行模块划分与任务分配,什么样的算法和功能适合放在可编程逻辑器件或 ASIC 里面实现,什么样的算法和功能适合放在 DSP、CPU 等微处理器实现,如何划分软硬件功能,安排模块接口设计等问题都非常重要。要知道**在系统上复用模块节省的面积远比在代码上小打小闹来得实惠多。**
[图 5-6](#图5-6) 描述了FPGA 设计的系统规划流程。我们可以发现**对设计从整体上进行模块复用应该在系统功能定义后就充分考虑**,并指导模块的具体划分。模块划分非常重要,除了关系到是否最大程度上发挥项目成员的协同设计能力,而且直接决定着设计的综合、实现效果和相关的操作时间,模块划分的具体方法请参考 4.2 小节中关于模块划分技巧的论述。
<a name="图5-6"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-060-SystemPlanningProc.png!sign" width="700px" alt="系统规划的简化流程"/>
</div>
对系统原则做一点引申,简单谈谈模块化设计方法。模块化设计是系统原则的一个很好的体现,它不仅是一种设计工具,它更是一种设计思路、设计方法,它是由顶向下、模块划分、分工协作设计思路的集中体现,是当代大型复杂系统的推荐设计方法。目前很多的 EDA 厂商都提高了模块化设计工具,通过这类工具划分每个模块的设计区域,然后单独设计和优化每个模块,最后将每个模块融合到顶层设计中,从而实现了团队协作、并行设计的模块化设计方法。合理使用模块化设计方法,能在最大程度上继承以往设计成果,并行分工协作,有效利用开发资源,缩短开发周期。
下举例说明如何在系统层次复用模块
利用“可编程匹配滤波器“实现 WCDMA 基站的方案,其核心是在合理规划系统的基础上,合理划分模块并安排操作时序,提高单元模块的复用率令从而大大降低硬件消耗,其设计思想是系统原则的集中体现。可编程匹配滤波器原理框图如[图 5-7](#图5-7) 所示。
<a name="图5-7"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-070-ProgrammableMatchFilter.png!sign" width="500px" alt="系统规划的简化流程"/>
</div>
其设计思想是:利用信道固有特点(如信道 pilot 导频、信道结构等),应用现代可编程数字信号处理的技术(如 DSP、FPGA 等),采取反馈与控制匹配滤波方式,实现对某信道的已扩信息的自动解扩解扰。该可编程 MF 的主要组成部分为本地码发生器、可编程信号 MF(S MF)、帧匹配滤波器(FRAME MF)和控制器。本地码发生器可生成各种所需的扩频、加扰序列,可接收控制器的指示脉冲,产生规定的本地解扩、解扰序列,作为 S MF 的参考序列;S MF 是完成匹配滤波的主体,可接收控制器的指示脉冲,将自己的匹配状态切换到下一匹配状态;F MF 完成对导频信号等特殊信号(信息位待选集有限)的检测,生成指示相关峰,通知控制器将 S MF 切换到下一匹配状态;控制器统一协调各部分工作。这种可编程滤波器可以在如越区切换、同步方面、CPCH 收发信机等多方面应用,如果适当安排时序流程,可以在较大程度上节约硬件资源。
# 2. 同步设计原则和多时钟处理
## 2.1 同步设计原则<a name="0201"></a>
同步设计是 PLD 和 ASIC 设计的最重要原则。本小节首先阐释为什么在 PLD 设计中要采用同步时序设计,然后重点论述同步时序设计的要点。
### 2.1.1 异步时序设计与同步时序设计
简单比较一下异步电路和同步电路的异同。
| 电路类型 | <center>特点</center> |
|:--:|:--|
| 异步电路 | · 电路的核心逻辑用组合电路实现,比如异步的 FIFO/RAM 读/写信号、地址译码等电路。<br>· 电路的主要信号、输出信号等并不依赖于任何一个时钟性信号,不是由时钟信号驱动触发器(FF)产生的。<br>· 异步时序电路的最大缺点是容易产生毛刺。在布局布线后仿真和用高分辨率逻辑分析仪观测实际信号时,这种毛刺尤其明显。<br>· 不利于器件移植,这包括 FPGA 器件族之间的移植和从 FPGA 向结构化 ASIC 的移植。<br>· 不利于静态时序分析(STA)、验证设计时序性能。|
| 同步电路 | · 电路的核心逻辑用各种各样的触发器实现。<br>· 电路的主要信号、输出信号等都是由某个时钟沿驱动触发器产生的。<br>· 同步时序电路可以很好地避免毛刺。布局布线后仿真和用高速逻辑分析仪采样实际工作信号皆无毛刺。<br>· 利于器件移植,这包括 FPGA 器件族之间的移植和从 FPGA 向结构化 ASIC 的移植。<br>· 有利于静态时序分析(STA)、验证设计时序性能。
早期 PLD 设计经常使用行波计数器(Ripple Counters)或者异步脉冲生成器等典型的异步逻辑设计方式以节约设计所消耗的面积资源。但是**异步逻辑设计的时序正确性完全依赖于每个逻辑元件和布线的延迟**,所以其时序约束相对繁杂而困难,并且**极易产生亚稳态、毛刺等,造成设计的稳定性下降和设计频率不高**。随着数字逻辑的不断经济化,器件资源已经不再成为设计的主要矛盾,而同步时序电路对全面提高设计的频率和稳定性至关重要,从这个层面上讲,尽量使用同步时序电路更加重要。
另一方面,随着 PLD 和 ASIC 的逻辑规模不断扩大,在芯片中完成复杂且质量优良的异步时序设计**过于费时费力**,其所需调整的时序路径和需要附加的相关约束相当繁琐,**异步时序方法是和可编程设计理念背道而驰的**。
随着EDA 工具的发展,大规模设计的综合、实现的优化效果越来越强。但是目前大多数综合、实现等EDA 工具都是基于时序驱动(Timing Driven)优化策略的。异步时序电路增加了时序分析的难度,需要确定最佳时序路径所需的计算量超出想象,所需时序约束相当繁琐,而且**对于异步电路很多综合、实现工具的编译会带来歧义**。而对于同步时序设计则恰恰相反,其**时序路径清晰,相关时序约束简单明了,综合、实现优化容易,布局布线计算量小**。所以目前可编程逻辑的 EDA 工具都推荐使用同步时序设计。
综上所述,**现代数字芯片设计推荐采用同步时序设计方式!**
### 2.1.2 同步时序设计
同步时序设计的基本原则是**使用时钟沿触发所有的操作**。如果所有寄存器的时序要求(Setup、Hold 时间等指标)都能够满足,则同步时序设计与异步时序设计相比,在不同的 PVT(工艺、电压、温度)条件下能获得更佳的系统稳定性与可靠性。
同步设计中,稳定可靠的数据采样必须遵从以下两个基本原则:
- 在有效时钟沿到达前,数据输入至少已经稳定了采样寄存器的 Setup 时间之久,这条原则简称**满足 Setup 时间原则**;
- 在有效时钟沿到达后,数据输入至少还将稳定保持采样寄存器的 Hold 时间之久,这条原则简称**满足 Hold 时间原则**。
同步时序设计有以下几个注意事项:
- 异步时钟域的数据转换。详见 [2.3 小节“异步时钟域数据同步”](#0203)。
- 组合逻辑电路的设计方法。详见[第 5 节“组合逻辑的注意事项”](#05)。
- 同步时序电路的时钟设计。 详见[第 6 节“时钟设计的注意事项”](#06)。
- 同步时序电路的延迟。同步时序设计中电路延迟最常用的设计方法是用分频或倍频的时钟或者同步计数器完成所需延迟。换句话说,同步时序电路的延时被当作一个电路逻辑来设计。对于**比较大的和特殊定时要求的延时,一般用高速时钟产生一个计数器,根据计数器的计数,控制延时**;对于**比较小的延时,可以用 D 触发器打一下**,这种做法**不仅使信号延时了一个时钟周期,而且完成了信号与时钟的初次同步**,在输入信号采样或增加时序约束余量时使用。另外许多初学者用行为级(Behavioral Level)方法描述延时,如 `#5 a<= 4'b0101;` 这种行为级描述方法常用于仿真测试激励,但是**在电路综合时会被忽略、并不能起到延时作用**。
## 2.2 亚稳态
在后续笔记中会介绍异步时钟域转换的问题,在谈这个话题之前,先分析一下什么是亚稳态,以及如何避免。
异步时钟域的转换的核心就是要保证下级时钟对上级数据采样的 Setup 时间和 Hold 时间。如果触发器的Setup 时间或者 Hold 时间不满足,就可能产生亚稳态,**此时触发器输出端 Q 在有效时钟沿之后比较长的一段时间内处于不确定的状态,在这段时间内 Q 端产生毛刺并不断振荡,最终固定在某一电压值,此电压值并不一定等于原来数据输入端 D 的数值**,这段时间称为决断时间(Resolution time)。经过 Resolution time 之后 Q 端将稳定到 0 或1, 但是究竟是 0 还是1,这是随机的,与输入没有必然的关系,其产生示意图如[图 5-8](#图5-8) 所示。
<a name="图5-8"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-080-MetastableGenerate.png!sign" width="700px" alt="亚稳态产生示意图"/>
</div>
**亚稳态的危害主要体现在破坏系统的稳定性**。由于输出在稳定下来之前可能是毛刺、振荡、固定的某一电压值,因此亚稳态将**导致逻辑误判**,严重情况下输出 0~1 之间的中间电压值还会**使下一级产生亚稳态,即导致亚稳态的传播**。逻辑误判导致功能性错误,而亚稳态的传播则扩大了故障面。另外,在亚稳态状态下,任何诸如环境噪声、电源于扰等细微扰动都将导致更恶劣的状态不稳定。这时这个系统的传输延迟增大,状态输出错误,在某些情况下甚至会使寄存器在两个有效判定门限(V~oL~ 、V~oH~)之间长时间的振荡。
**只要系统中有异步元件,亚稳态就无法避免**,因此设计的电路**首先要减少亚稳态导致的错误**,**其次要使系统对产生的错误不敏感**。前者要靠同步设计来实现,而后者则根据不同的设计应用不同的处理办法。
**使用两级以上寄存器采样可以有效地减少亚稳态继续传播的概率**。在[图 5-9](#图5-9) 中,左边为异步输入端,经过两级触发器采样,在右边的输出与 `bclk` 同步,而且该输出基本不存在亚稳态。其原理是使第一个触发器的输出端存在亚稳态,经过一个 clk 周期后,第二个触发器 D 端的电平仍未稳定的概率非常小,因此第二个触发器 Q 端基本不会产生亚稳态。理论上如果再添加一级寄存器,使同步采样达到 3 级,则末级输出为亚稳态的概率几乎为 0。
<a name="图5-9"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-090-TwoStageRegisterSampling.png!sign" width="700px" alt="两级寄存器采样降低亚稳态概率"/>
</div>
使用[图 5-9](#图5-9) 两级寄存器采样仅能降低亚稳态的概率,但是并不能保证第二级输出的稳态电平就是正确电平。前面说过经过 Resolution time 之后寄存器输出的电平是一个不确定的稳态值,也就是说**这种处理方法并不能排除采样错误的产生**,这时就**要求所设计的系统对采样错误有一定的容忍度**。有些应用本身就对采样错误不敏感,如一帧图像编码,一段话音编码等。而有些系统对错误采样比较敏感。这类由于亚稳态造成的采样是一些突发的错误,所以可以采用一些纠错编码手段完成错误的纠正。
## 2.3 异步时钟域数据同步<a name="0203"></a>
异步时钟域数据同步是芯片设计的一个常见问题,既是一个重点也是一个难点问题。很多设计工作时的不稳定都是源于异步时钟域数据同步不稳定。
### 2.3.1 两类异步时钟域同步的表现形式
异步时钟域数据同步,也被称为数据接口同步,顾名思义,是指**如何在两个时钟不同步的数据域之间可靠地进行数据交换**。数据的时钟域不同步主要有两种情况:
- 两个域的时钟频率相同,但是相位差不固定,或者相差固定但是不可测,简称为同频异相问题;
- 两个时钟域频率根本不同,简称为异频问题。
### 2.3.2 两种不推荐的异步时钟域操作方法
首先讨论两种在设计中不推荐的异步时钟域转换方法:一种是通过增加 Buffer 或者其他门延时调整采样;另一种是盲目使用时钟正负沿调整数据采样。
1. 通过 Buffer 等组合逻辑延迟线调整采样时间
在早期逻辑电路图设计阶段,有一些设计者养成了手工加入 Buffer 或者非门调整数据延迟的习惯,从而保证本级模块的时钟对上级模块数据的建立、保持时间的要求。这些做法目前主要应用的场合有两种:一是使用分立逻辑元件(如 74 系列)搭建数字逻辑电路;另一种是在 ASIC 设计领域。**目前使用分立逻辑元件搭建数字逻辑电路的场合一般为系统复杂度相对较低,系统灵活性要求不高的场合。**在上述场合使用分立逻辑器件设计数字逻辑电路,由于可以使用的调整延时的手段相对有限,而且采用插入 Buffer 、数字延迟逻辑甚至两个非门等手段调整采样的 Setup 和 Hold 时间是可以接受的。而 ASIC 设计领域采用这种方法是以严格的仿真和约束条件作为强力支持的。
正如 [2.1 同步设计原则](#0201)所述,**使用组合逻辑方法产生延迟,容易产生毛刺**,而且这种设计方法的**时序余量较差**,一旦外界条件变换(环境试验,特别是高低温试验),采样时序就有可能完全紊乱,造成电路瘫痪。另外,一旦芯片更新换代,或者移植到其他器件族的芯片上,采样时延必须重新调整,电路的可维护性和继承性都很差。
2. 盲目使用时钟正负沿调整数据采样
很多初学者习惯随意使用时钟的正负沿调整采样,甚至产生一系列不同相位或不同占空比的时钟,使用其正负沿调整数据。这种做法是不推荐的,原因如下:
1. 如果在一个时钟周期内,使用时钟的双沿同时操作,则使用该时钟的同相倍频时钟也能实现相同的功能。换句话说,一个时钟周期内,使用时钟的双沿同时操作,相当于使用了一个同相的倍频时钟。此时因为设计的时钟频率提升,所有相关的使用约束都会变得更紧,不利于可靠实现。
2. 芯片中的 PLL 和 DLL 一般都能较好地保证某个时钟沿的 Jitter、Skew 和占空比等各种参数指标,而对于另一个时钟沿的指标控制并不是那么严格。特别对于综合、实现等 EDA 的软件,如果没有明确对另外一个沿进行相关,这个沿的时序分析不一定完善,其综合或实现结果就不一定能严格满足用户期望的时序要求(比如 Setup、Hold 时间等),往往造成在该沿操作不稳定的结果。
总结这两点,如果设计者并不十分清楚同时使用上下沿的方法,不如直接使用同相倍频时钟更加简单、明确、可靠。但是如果设计者十分清楚同一周期使用双沿的注意事项,附加了相应的约束,这种做法并非不可。
针对使用两个时钟沿,在这里还想补充以下两点:
- 使用者虽然使用了同一个时钟的两个沿,但是**保证不在同一个周期内同时使用双沿,则不会增加时钟频率**。
- DDR 、QDR 本身就是利用了上下沿采样的原理,随着存储器件高速发展,时钟速度已经成为存取器件的瓶颈,所以可用时钟上下沿操作缓解对单沿 RAM 时钟振荡器的要求。但是必须清楚,硬件的 DDR、QDR 电路(包括 ASIC 的 DDR、QDR 与 FPGA 内嵌的 DDR、QDR 电路)是专用高速设计电路,对时钟的正沿、负沿的 Jitter 、Skew 和占空比等指标都有详细和明确的要求,这一点是和普通逻辑设计,特别是实现在 FPGA 中设计的情况截然不同,希望读者加以区分。
### 2.3.3 异步时钟域数据同步常用方法
下面分别介绍 [2.3 小节](#0203)提出的两大类异步时钟域数据同步问题的解决方法。
1. 同频异相问题
同频异相问题的简单解决方法是**用后级时钟对前级数据采样两次**,即通常所述的**用寄存器打两次**。数据同步如[图 5-10](#图5-10) 所示,这样的做法是有效地减少了亚稳态的传播,使后级电路数据都是有效电平值。但是这种做法并不能保证两级寄存器采样后的数据是正确的电平值,因为一旦 Setup 或 Hold 时间不满足,采样发生亚稳态,则经判决时间(Resolution Time)后,还是可能判决到错误电平值。所以这种方法仅仅适用于对少量错误不敏感的功能单元。
<a name="图5-10"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-100-DataSynchronization.png!sign" width="700px" alt="数据同步"/>
</div>
可靠的做法是用 DPRAM 、FIFO 或者一段寄存器 Buffer 完成异步时钟域的数据转换。把数据存放在 DPRAM 或FIFO 的方法如下:将上级芯片提供的数据随路时钟作为写信号,将数据写入 DPRAM 或者FIFO, 然后使用本级的采样时钟( 一般是数据处理的主时钟),将数据读出即可。由于时钟频率相同,所以 DPRAM 或 FIFO 两端的数据吞吐率一致,实现起来相对简单。
2. 异频问题
可靠完成异频问题的解决方法就是使用 DPRAM 或 FIFO 。其实现思路与前面所述一致,用上级随路时钟写上级数据,然后用本级时钟读出数据。但是由于时钟频率不同,所以两个端口的数据吞吐率不一致,设计时一定要开好缓冲区,并通过监控(Full、Half、Empty 等指示)确保数据流不会溢出。
# 3. 代码风格
代码风格,即 Coding Style,其分为一般性 Coding Style 和针对综合工具、实现工具、器件类型的 Coding Style。
## 3.1 Coding Style 的分类
所谓一般性的 Coding Style,是指不依赖于综合、实现工具和器件类型的代码风格。不同的综合实现工具对一些语法细节的阐释略有不同,特别是那些关于优先级实现的先后顺序等,所以不同的综合工具在个别细节上对 Coding Style 的解释有一定的差异。本篇笔记所述的 Coding Style,如果没有特别声明,都是不依赖于具体 EDA 工具, 一般意义上的 Coding Style,适用于不同厂商的综合实现工具和不同目标器件。
还有一类 Coding Style 是针对某种综合工具或者特定器件结构的,根据器件硬件结构,正确地实例化底层单元模块,合理地使用其内嵌的硬件电路,以达到最优化的设计效果。另外需要特别声明一点,一般 ASIC 设计的 Coding Style 和 PLD(主要指 FPGA 和 CPLD)设计的 Coding Style 有明显差异。这主要是因为PLD 设计是基于固有的硬件结构(如:逻辑单元、块 RAM、PLL/DLL、时钟资源等)。而 ASIC 设计结构灵活,目标多样,特别是在功耗、速度、时序等要求上,与 PLD 设计有明显差异。例如, ASIC 设计中根据要求会有意识地采样某些组合逻辑、门控时钟等降低功耗或提高速度。这里讲述的 Coding Style 和基本原则如不特殊声明,均基于 PLD 设计要求。
另外本篇笔记所述 Coding Style 主要是基于 RTL(寄存器传输级)而言,并非其他描述层次。所以诸如业界非常热门的结构化设计方法(Architectural-based Design)的代码风格的原则和方法与本章无关,甚至有很多原则和方法是与本章所述背道而驰的。
## 3.2 Coding Style 的重要性
当代的可编程逻辑设计日趋复杂,其系统复杂度和设计频率要求与 5 年前不可同日而语。良好的 Coding Style 对设计的工作频率,所消耗的芯片面积,甚至整个系统的稳定性都非常重要,良好规范的Coding Style 便于设计移植。
随着 EDA 技术的不断发展,综合、实现工具的优化能力越来越强大,可以自动完成许多复杂设计的面积和时序方面的优化,并且其优化效果日趋先进。但是如果盲目依赖综合、实现等 EDA 工具的优化,而忽略自己设计的代码风格,就大错特错了。因为 Coding Style 对综合、实现等 EDA 工具的优化结果的影响可以用这样一句话来概括:**好的Coding Style 会使综合、实现等优化事半功倍,达到最优化的结果;不良的Coding Style 会使综合、实现优化南辕北辙,甚至产生错误的结果**。所以必须明确:综合、实现等 EDA 工具的优化能力和正确性最终取决于设计的 Coding Style 的优劣。
在后面的几节中,将详细谈谈设计和代码风格。
# 4. 结构层次和模块划分
## 4.1 结构层次化编码
结构层次化编码(Hierarchical Coding)是模块化设计思想的一种体现。目前大型设计中必须采用结构层次化编码风格,以提高代码的可读性,易于模块划分,易于分工协作,易于设计仿真测试激励。最基本的结构化层次是由一个顶层模块和若干个子模块构成,每个子模块根据需要还可以包含自己的子模块。结构层次化编码结构如[图 5-11](#图5-11) 所示。
<a name="图5-11"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-110-StructuredCoding.png!sign" width="500px" alt="结构层次化编码示意图"/>
</div>
结构层次化编码有如下注意事项:
- **结构的层次不易太深,一般为 3~5 层即可**。在综合时, 一般综合工具为了获得更好的综合效果,特别是为了使综合结果所占用的面积更小,会默认将 RTL 代码的层次打平(Flatten)。而有时为了在综合后仿真和布局布线后的时序仿真中较方便地找出一些中间信号,比如子模块之间的接口信号等,可以在综合工具中设置保留结构层次,以便于仿真信号的查找和观察。
- **顶层模块最好仅包含对所有模块的组织和调用,而不应该完成比较复杂的逻辑功能**。较为合理的顶层模块由输入/输出管脚声明、模块的调用与实例化、全局时钟资源、全局置位/复位、三态缓冲和一些简单的组合逻辑等构成。
- **所有的 I/O 信号,如输入、输出、双向信号等的描述在顶层模块完成。**
- **子模块之间也可以有接口,但是最好不要建立子模块间跨层次的接口**,例如,上图中模块 `A1` 到模块 `B1` 之间不宜直接连接,两者需要交换的信号可以通过模块 `A` 模块 `B` 的接口传递。这样做的好处是增加了设计的可读性和可维护性。
- 子模块的合理划分非常重要,应该综合考虑子模块的功能、结构、时序、复杂度等多方面因素。
结构层次化编码的本质不应该简单地理解为一种具体的设计手段,而应该认识到它更是**一种系统层次的设计方法**。很多初学者都有这样一个疑问:一个设计完全可以在同一个模块内完整描述,为什么还要将其中的时序逻辑和组合逻辑、不同优化目标、不同功能的电路分成多级层次,使用多个模块描述呢?模块划分增加了内部接口描述的工作量,这不是“自讨苦吃“么?
虽然理论上任何设计都可以在同一个模块中完成,但是如果将不同功能、不同层次、不同类型的电路混淆在同一个模块中,这不是一种好的系统设计方法,对于比较复杂的设计,将会导致整个设计杂乱无章,不利于设计的阅读与维护,也会给综合和实现过程带来许多麻烦。
## 4.2 模块划分的技巧
结构层次化设计方法的第一个要点就是模块划分,模块划分非常重要,关系到能否最大程度上发挥项目成员协同设计的能力,更重要的是它直接决定着设计的综合、实现的耗时与效率。模块划分的基本原则介绍如下:
1. **对每个同步时序设计的子模块的输出使用寄存器**
这也被称为用寄存器分割同步时序模块的原则。使用寄存器分割同步时序单元的好处是:便于综合工具权衡所分割的子模块中的组合电路部分和同步时序电路部分,从而达到更好的时序优化效果,这种模块划分符合时序约束的习惯,便于利用约束属性进行时序约束。
2. **将相关的逻辑或者可以复用的逻辑划分在同一模块内**
这个做法有时被称为**呼应系统原则**。这样做的好处有:一方面将相关的逻辑和可以复用的逻辑划分在同一模块,可在最大程度上复用资源,减少设计所消耗的面积;另一方面更利于综合工具优化某个具体功能的时序关键路径。其原因是,传统的综合工具只能同时优化某一部分的逻辑,而所能同时优化的逻辑的从本单元就是模块,所以将相关功能划分在同一模块将在时序和面积上获得更好的综合优化效果。
3. **将不同优化目标的逻辑分开**
在介绍速度、面积平衡与互换原则时,合理的设计目标应该综合考虑面积最小和频率最高两个指标。好的设计,在规划阶段设计者就应该初步规划了设计的规模和时序关键路径,并对设计的优化目标有一个整体上的把握。对于时序紧张的部分.应该独立划分为一个模块.其优化目标为“Speed”,这种划分方法便于设计者进行时序约束,也便于综合和实现工具进行优化。目前很多综合与实现工具都支持物理区域位置约束,以模块为单元进行物理区域约束,从而优化关键路径时序,以达到更高的系统工作频率就更为方便有效。另一类情况是,设计的矛盾主要集中在芯片的资源消耗上。这时应该将资源消耗过大的部分划分为独立的模块,这类模块的优化目标应该定为“Area” 。同理,将它们规划到一起,更有利于区域布局与约束。这种根据优化目标进行优化的方法最大好处是,对于某个模块综合器仅需要考虑一种优化目标和策略,从而比较容易达到较好的优化效果。相反,如果同时考虑两种优化目标,会使综合器陷入互相制约的困境,耗费巨大的综合优化时间也得不到令人满意的综合优化结果。
4. **将时序约束较松的逻辑归到同一模块**
有些逻辑的时序非常宽松,不需要较高的时序约束,可以将这类逻辑归入同一模块,如多周期路径(Multi-cycle Path)等。将这些模块归类,并指定宽松约束,则可以让综合器尽量节省面积资源。
5. **将存储逻辑独立划分成模块**
RAM、ROM、CAM 和 FIFO 等存储单元应该独立划分模块。这样做的好处是便于利用综合约束属性显化指定这些存储单元的结构和所使用的资源类型,也便于综合器将这些存储单元自动类推为指定器件的硬件原语。另一个好处是在仿真时消耗的内存也会减少,便于提高仿真速度。这是因为大多数仿真器对大面积的RAM都有独特的内存管理方式,以提高仿真效率。
6. **合适的模块规模**
从理论上讲,模块的规模越大,越利于模块资源共享(Resource Sharing)。但是庞大的模块,将要求对综合器同时处理更多的逻辑结构.这将对综合器的处理能力和计算机的配置提出了较高的要求。另外,庞大的模块划分,不利于发挥目前非常流行的增拭综合与实现技术的优势。
# 5. 组合逻辑的注意事项<a name="05"></a>
相对复杂一些的设计都是由两部分组成的,分别为时序逻辑(Sequential Logic)和组合逻辑(Combination Logic)。同步时序设计系统中并不是不包含组合逻辑,而是要更加合理地设计、划分组合逻辑。在以下几小节中将介绍组合逻辑设计的一些问题。
## 5.1 `always` 组合逻辑信号敏感表
几乎所有的编码指导手册都有关于信号敏感表的论述。时序逻辑的信号敏感表比较好写,在信号敏感表中写明时钟信号的正负触发沿即可,关于信号敏感表的主要问题集中在组合逻辑的信号敏感表的写法。在此,仅强调组合逻辑的信号敏感表的相关要点:
- 正确的信号敏感表设计方法是将 `always` 模块中**使用到的所有输入信号和条件判断信号都列在信号敏感表中**。
- 希望通过信号敏感表的增减完成某项逻辑功能是不可能的。
- 不完整的信号敏感表会造成前仿真结果和综合、实现后仿真结果不一致。
- 一般综合工具对于不完整的信号敏感表的默认做法是,将处理进程中用到的所有输入和判断条件信号都默认添加到综合结果的信号敏感表中,并对原设计代码敏感表中遗漏的信号报警告(warning)信息。
有些初学者发现在信号敏感表中增减一些信号,会得到不同的仿真结果,于是企图依靠修改信号敏感表,而方便地完成某些逻辑的设计,这种做法是不可能的。其实一般综合工具的默认操作都是将 `always` 模块中使用到的所有输入信号和条件判断信号都当作触发信号,综合到信号敏感表中,所以增减信号敏感表,其实得到的综合结果完全一致。而增减信号敏感表,得到的仿真结果不同是因为仿真器的工作机制决定的,大多数仿真器是数据流和时钟周期驱动的,如果信号敏感表中没有某个信号,则无法触发和该信号相关的仿真进程,从而得到的仿真结果不同。
## 5.2 组合逻辑环路
**组合逻辑反馈环路是数字同步逻辑设计的大忌**,它最容易因振荡、毛刺、时序违规等引起整个系统的不稳定和不可靠。
典型的组合逻辑反馈环路如[图 5-12](#图5-12) 所示,寄存器的 Q 端输出直接通过组合逻辑反馈到寄存器的异步复位端,如果 Q 输出为 0 时,经组合逻辑运算后为异步复位端有效,则电路进入不断清零的死循环。
<a name="图5-12"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-120-CombinatorialLogicFeedbackLoop.png!sign" width="400px" alt="组合逻辑反馈环路示意图"/>
</div>
组合逻辑反馈环路是一种**高风险**设计方式,主要原因如下:
- 组合反馈环的逻辑功能完全依赖于其反馈环路上组合逻辑的门延迟和布线延迟等,如果这些传播延迟有任何改变,则该组合反馈环单元的整体逻辑功能将彻底改变,而且改变后的逻辑功能很难确定。
- **组合反馈环的时序分析是无穷循环的时序计算**,综合、实现等 EDA 工具迫不得已必须主动割断其时序路径,以完成相关的时序计算,而不同的 EDA 工具对组合反馈环的处理方法各不相同,所以组合反馈环的最终实现结果有很多不确定因素。
同步时序系统中应该避免使用组合逻辑反馈环路,注意事项主要有以下两点:
- 牢记**任何反馈环路必须包含寄存器**。
- 检查综合、实现报告的 Warning 信息,发现 Combinational Loops 后进行相应修改。
## 5.3 脉冲产生器
在异步时序设计中,常用延时链(Delay Chains)产生脉冲,常用的异步脉冲产生方法如[图 5-13](#图5-13) 所示。
<a name="图5-13"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-130-AsynchronousPulseGeneration.png!sign" width="500px" alt="常用的异步脉冲产生方法示意图"/>
</div>
这类异步方法设计的脉冲产生电路的脉冲宽度取决于 Delay Chains 的门延迟和线延迟,而在 FPGA/CPLD 中,大多数 Timing Driven 的综合、布线工具无法保证其布线延迟恒定。另外,PLD 器件本身在不同的 PVT(工艺、电压、温度)环境下其延时参数也有微小波动,所以脉冲宽度无法准确确定。而且 STA 工具也无法准确分析脉冲的特性,为时序仿真和验证带来了很多的不确定性。
异步脉冲序列产生电路(Multi-Vibrators)也被称为毛刺生成器(Glitch Generator),**利用组合反馈环路振荡而不断产生毛刺**。正如前面所述,组合反馈环是同步时序必须避免的.这类基于组合反馈环的 Multi-Vibrator 也会给设计带来稳定性、可靠性等方面的问题,必须避免使用。
同步时序设计脉冲电路的常用方法如[图 5-14](#图5-14) 所示。该设计的脉冲宽度不因器件或设计移植而改变,恒等于时钟周期,而且避免了异步设计的诸多不确定因素,其时序路径便于计算、STA 分析和仿真验证。
<a name="图5-14"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-140-SynchronousPulseGeneration.png!sign" width="500px" alt="常用的同步脉冲产生方法示意图"/>
</div>
## 5.4 慎用锁存器
同步时序设计要尽量慎用锁存器(Latch), 特别要注意综合出与设计意图不一致的 Latch 导致仿真和设计的错误。对于某些特定设计一定要使用 Latch 时,设计者必须明确该 Latch 是否为有意设计的。综合出与设计意图不吻合的Latch 结构的主要原因在于:在设计组合逻辑时,使用不完全的条件判断语句。如:有 `if` 而没有 `else`,或不完整的 `case` 语句等(这仅是一种可能,并不一定生成 Latch);另外一种情况是设计中有组合逻辑的反馈环路(Combinatorial Feedback Loops)等异步逻辑。
```verilog latch.v
/******************************
因if语句不完整而产生Latch的典型情况
******************************/
module latch (cond_1, data_in, data_out);
input cond_1;
input data_in;
output data_out;
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
reg data_out;
always @(cond_1 or data_in) begin
if (cond_1)
data_out <= data_in;
end
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
endmodule
```
上述描述,由于未指定在条件 `cond_1` 等于 `0` 时的动作,一般情况下会生成 Latch 结构如[图 5-15](#图5-15) 所示。
<a name="图5-15"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-150-LatchRTL.png!sign" width="400px" alt="Latch 的 RTL 示意图"/>
</div>
防止产生非目的性的Latch 的措施如下:
- 使用完备的 `if ... else` 语句。
- 检查设计中是否含有组合逻辑反馈环路。
- 为每个输入条件设计输出操作,对 `case` 语句设置 `default` 操作。特别是在状态机设计中,最好有一个 `default` 的状态转移,而且每个状态最好也有一个 `default` 的操作。
- 如果使用 `case` 语句时,特别是在设计状态机时,尽量附加综合约束属性,综合为完全条件 `case` 语句(full case)。目前,大多数综合工具支持 full case 的综合约束属性,具体的语法请参考综合工具的约束属性指南。
> 仔细检查综合器的综合报告,目前大多数的综合器对所综合出的 Latch 都会报“warning”, 通过综合报告可以较为方便地找出无意中生成的 Latch。
# 6. 时钟设计的注意事项<a name="06"></a>
时钟是同步设计的基础,在同步设计中,所有操作都是基于时钟沿触发的,所以时钟的设计对同步时序电路而言非常重要。对于 PLD 设计,通常推荐使用 FPGA 内嵌的 PLL 或 DLL 完成时钟的频率与相位变化,并用全局时钟和专用时钟选择器进行时钟布线。对于 ASIC 设计,常会用到各种各样的组合逻辑产生的时钟,但是这些设计如果直接移植到同步时序电路中会带来各种各样的问题,本节旨在辨析一些常用的时钟电路的优劣。
## 6.1 内部逻辑产生的时钟
**如果需要使用内部逻辑产生时钟,必须要在组合逻辑产生的时钟后插入寄存器**,如[图 5-16](#图5-16) 所示。如果直接使用组合逻辑产生的信号作为时钟信号或者异步置位/复位信号,会使设计不稳定。这是因为组合逻辑难免产生毛刺,这些毛刺到达一般数据路径,在经过寄存器采用后一般影响不大,但是如果作为时钟信号或者异步置位/复位信号时,如果毛刺的宽度足以驱动寄存器的时钟端或者异步置位/复位端,则必将产生错误的逻辑操作;即使毛刺的宽度不足以驱动时钟端或异步置位/复位端,也会带来寄存器的不稳定,甚至激发寄存器产生亚稳态。所以对于时钟路径,必须插入寄存器以过滤毛刺。
<a name="图5-16"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-160-InternalClockDesignwithRegister.png!sign" width="800px" alt="内部时钟设计必须插入寄存器"/>
</div>
另一方面,组合逻辑产生的时钟还会带来另外一个问题,组合逻辑电路的 Jitter 和 Skew 都比较大,如果时钟产生逻辑的延迟比数据路径的延迟更大,会带来负的 Skew,负的 Skew 同步逻辑设计是灾难性的。所以**使用组合逻辑产生内部时钟仅适用于时钟频率较低、时钟精度要求不高的情况**。另外,**这类时钟应该使用快速布线资源布线,而且需要对组合逻辑电路附加一定的约束条件,以确保时钟质质量**。
## 6.2 Ripple Counter
所谓 Ripple Counter,即行波计数器,其结构为:一组寄存器级连,每个寄存器的输出端接到下一级寄存器的时钟管脚,这种计数器常常用于异步分频电路。早期的 PLD 设计经常使用 Ripple Counter 以节约芯片资源。但是由于 Ripple Counter 是一种典型的异步时序逻辑,正如前面“[同步设计原则](#0201)”所述,异步时序逻辑会带来各种各样的时序问题,**在同步时序电路设计中必须严格避免使用 Ripple Counter**。
## 6.3 时钟选择
在通信系统中,为了适应不同的数据速率要求,经常要进行时钟切换。有时为了节约功耗,也会把高速时钟切换到低速时钟,或者进行时钟休眠操作。时钟切换的最佳途径是使用芯片内部的专用 Clock MUX 。这些 MUX 的反应速度快,锁定时间短,切换瞬间带来的冲击和抖动小。
如果所需器件没有专用的 Clock MUX,应该尽量满足如下几点:
- 时钟切换控制逻辑在配置后将不再改变。
- 时钟切换后,对所有相关电路复位,以保证所有寄存器、状态机和 RAM 等电路的状态不会锁死或进入死循环。
- 所设计系统对时钟切换过程发生的短暂错误不敏感。
## 6.4 门控时钟
门控时钟即 Gated Clock,如[图 5-17](#图5-17) 所示,是 IC 设计的一种常用减少功耗的手段。通过 Gating Logic 的控制,可以控制门后端的所有寄存器不再翻转,从而非常有效地节约功耗。
<a name="图5-17"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-170-GatedClock.png!sign" width="800px" alt="门控时钟"/>
</div>
但是 Gated Clock 不是同步时序电路,其 Gating Logic(门控逻辑)会污染 Clock 的质量,通过控制门后会产生毛刺并使时钟的 Skew(偏斜)、Jitter(延时)等指标恶化。正如“[同步设计原则](#0201)” 一节所述,在同步时序电路中,应该尽量不使用 Gated Clock 。
虽然有些材料指出当功耗成为主要矛盾时,可以使用门控时钟改进电路如[图 5-18](#图5-18) 所示的电路完成类似门控时钟的功能,但是笔者仍强烈建议在 PLD 设计中不要使用该图所示的门控时钟改进电路。虽然这个改进电路已经在较大程度上解决了门控电路产生的毛刺,但是请读者注意,这个电路工作的前提是时钟源Clock 的占空比(Duty Cycle)是非常理想的 50%,如果时钟的占空比不能保证 50%,则会产生许多有规律的毛刺信号。另外这个电路还有一个前提,那就是 Clock 与 Enable 信号的不布线 Skew 为 0,否则也会造成宽度为 Skew 的毛刺。
<a name="图5-18"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-180-ImprovedGatedClock.png!sign" width="800px" alt="门控时钟改进电路"/>
</div>
如果功耗真的成为了 PLD 设计的首要问题,建议采用其他方法减少功耗。如果最近发展起来的低核电压芯片(Core 电压为 1.0V),芯片休眠功能,Clock MUX 等新技术器件能有效地节约芯片功耗。
## 6.5 时钟同步使能端
大多数像寄存器这样的同步单元,都支持时钟的同步使能(Synchronous Clock Enable)。需要注意的是,虽然使能无效时这些单元无输出,但是这种方法并不能如 Gated Clock 一样减少功耗。但 Synchronous Clock Enable 却能够非常方便地完成一些逻辑功能,通过使用同步时钟使能端完成某些逻辑功能,有时可以节约芯片面积并提高设计频率。
同步使能端如[图 5-19](#图5-19) 所示,上半部分是同步使能功能,在目前大多数的器件上可以直接将使能信号连接到芯片的同步使能端实现,如[图 5-19](#图5-19) 的下半部分所示。
<a name="图5-19"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-190-EnableSynchronization.png!sign" width="500px" alt="同步使能端"/>
</div>
# 7. RTL 代码优化技巧
## 7.1 使用 Pipelining 技术优化时序
Pipelining,即流水线时序优化方法,其本质是调整一个较长的组合逻辑路径中的寄存器位置,用寄存器合理分割该组合逻辑路径,从而降低了对路径的 Output 和 Setup 等时间参数的要求,达到提高设计频率的目的。但是必须要注意的是,使用 Pipelining 优化技术**只能合理地调整寄存器位置,而不应该凭空增加寄存器级数**,所以 Pipelining 有时也被称为 Register Balance。
目前一些先进的综合工具能根据用户参数配置,自动运用 Pipelining 技术,通过用寄存器平衡设计中的较长组合路径(Register Balance),在一定程度上提高设计的工作频率。这种时序优化手段对乘法器、ROM等单元效果显著。
## 7.2 模块复用与 Resource Sharing
本小节的模块复用和Resource Sharing 主要站在微观的角度观察节约面积的问题。为了便与理解,首先看两个例子。
这是一个补码平方器的例子,输入是 8 位补码,求其平方和。由于输入是补码,所以当最高位是 1 时,表示原值是负数,需要按位取反,加 1 后再平方;当最高位是 0 时,表示原值是正数,直接求平方。
第一种实现方式:
```verilog resource_share1.v
module resource_share1 (data_in,square);
input [ 7:0] data_in; // 输入是补码
output [15:0] square;
wire [ 7:0] data_bar;
assign data_bar = ~data_in + 1;
assign square=(data_in[7]) ? (data_bar*data_bar) : (data_in*data_in);
endmodule
```
第二种实现方式:
```verilog resource_share2
module resource_share2 (data_in,square);
input [ 7:0] data_in; // 输入是补码
output [15:0] square;
wire [ 7:0] data_tmp;
assign data_tmp = (data_in[7]) ? (~data_in + 1) : data_in;
assign square = data_tmp * data_tmp;
endmodule
```
仔细观察一下可以发现:第一种实现方式需要两个 16 位乘法器,同时平方,然后根据输入补码的符号选择输出结果,其关键在于使用了两个乘法器,选择器在乘法器之后;第二种实现方法,首先根据输入补码的符号,换算为正数,然后平方,其关键在于选择器在乘法器之前,仅使用了一个乘法器,节约了资源。第二种实现方式与第一种实现方式相比节约的资源有两部分:第一部分,节约了一个 16 bit 乘法器;第二部分,后者的选择器是 1 bit 判断 8 bit 输出,而前者是 1 bit 判断 16 bit 输出。
两种代码的硬件结构示意图如[图 5-20](#图5-20) 和图5 - 21 所示。
<a name="图5-20"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-200-TwoMultipliedwithoutResourceSharing.png!sign" width="" alt="未Resource Sharing,2个乘法器的实现方案"/>
</div>
<a name="图5-21"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-210-OneMultipliedwithResourceSharing.png!sign" width="" alt="Resource Sharing,1个乘法器的实现方案"/>
</div>
对于综合后的 RTL 视图,这里需要强调以下几点:
1. 上例资源共享的单元是乘法器,通过 Resource Sharing,节省了一个乘法器和一些选择器占用的资源。其实如果拓展一下思维,将乘法器换成加法器、除法器等,甚至推广到**任何一个普通的模块**,后续结构含有选择器,都可以使用本例的设计思想,通过 Resource Sharing 成倍地节省前级模块所消耗的资源。
2. 不同的综合工具、同一综合工具的不同版本、不同的优化参数、不同厂商的目标器件、同一厂商的不同器件族等因素都可能造成不同的综合结果。
3. 目前很多综合工具都有“Resource Sharing”之类的优化参数,选择该参数,综合工具会自动考察设计中是否有可以资源共享的单元,在保证逻辑功能不变的情况下,进行 Resource Sharing,以**获得面积更小的综合结果**。
4. 最后需要强调的是,**不能因为综合工具的优化能力增强,而片面依靠综合工具放松对自己 Coding Style 的要求**。这是因为:第一,综合工具的优化力度毕竟有限,很多情况下不能智能地发现需要 Resource Sharing 的逻辑;第二,前面已经说过,“不同的综合工具、同一综合工具的不同版本、不同的优化参数、不同厂商的目标器件、同一厂商的不同器件族等因素"都会直接影响综合工具的优化能力和效果,所以依靠综合工具的优化能力十分不可靠;第三,在 ASIC 设计中,综合工具非常忠于用户意图,这时 Coding Style 更加重要。所以逻辑工程师必须注意自己 Coding Style 方面的修养并不断提高。
## 7.3 逻辑复制
逻辑复制是一种通过增加面积而改善时序条件的优化手段。逻辑复制最常使用的场合是调整信号的扇出。如果某个信号需要驱动后级的很多单元,换句话说,也就是其扇出非常大,那么为了增加这个信号的驱动能力,必须插入很多级 Buffer,这样就在一定程度上增加了这个信号路径的延时。这时可以复制生成这个信号的逻辑,使多路同频同相的信号驱动后续电路,平均到每路的扇出变低,不需要加 Buffer 也能满足驱动能力的要求,这样就节约了该信号的路径时延。用逻辑复制改善扇头电路如[图 5-22](#图5-22) 所示。
<a name="图5-22"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-220-DuplicationtoImproveFanOut.png!sign" width="" alt="用逻辑复制改善扇出"/>
</div>
需要说明的是,现在很多综合工具都可以自动设置最大扇出值(Max Fanout),如果某个信号的扇出值大于最大扇出,则该信号自动被综合工具复制。最大扇出值和器件的工艺密切相关,其合理值应该根据器件手册的声明和工程经验设置。这里举例用逻辑复制手段调整扇出,达到优化路径时延仅仅是为了讲述逻辑复制的概念,其实逻辑复制还有其他很多形式。例如,香农扩展(Shannon Expansion) 等时序优化技术,香农扩展在后面将会有详细的介绍。
有的读者会有疑问,逻辑复制和资源共享是两个矛盾的概念,既然使用了资源共享优化技术,为什么还要做逻辑复制呢?
其实这个问题的本质,还是面积与速度的平衡,**逻辑复制与前面的 Resource Sharing 是两个对立统一的概念**。**Resource Sharing 的目的是为了节省面积资源,而逻辑复制的目的是为了提高工作频率**。当使用逻辑复制手段提高工作频率的时候,必然会增加面积资源,这是与资源共享相对立的;但是正如前面介绍的面积与速度的对立统一一样,**逻辑复制和资源共享都是到达设计目标的两种手段**,一个侧重于速度目标,一个侧重于面积目标,两者存在一种转换与平衡的关系,所以两者又是统一的。
首先看下面的一个加法器资源共享的例子。
这个例子和前面乘法器的例子非常相似,只是将平方器换成了加法器。实现这个加法器也有两种代码写法,对应两种不同的硬件结构,资源共享如[图 5-23](#图5-23) 所示。
<a name="图5-23"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-230-ResourceSharingExample.png!sign" width="700px" alt="资源共享示例"/>
</div>
第一种写法,对应左边的 RTL 结构示意图:
```verilog mod_copy1.v
module mod_copy1 (sel, a, b, c, d, data_out);
input sel, a, b, c, d;
output data_out;
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
assign data_out= (sel)? (a+b) : (c+d) ;
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
endmodule
```
第二种写法,对应右边的 RTL 结构示意图:
```verilog mod_copy2.v
module mod_copy2 (sel, a, b, c, d, data_out);
input sel, a, b, c, d;
output data_out;
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
wire temp1, temp2;
assign temp1 = (sel) ? (a) : (c) ;
assign temp2 = (sel) ? (b) : (d) ;
assign data_out = temp1 + temp2;
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
endmodule
```
第一种写法比第二种省了一个加法器。而且第一种写法比第二种写法耗时略少一些,这在本例还不算十分明显,当运算模块不是加法器而是一些较复杂的逻辑时,会比较明显。当**设计的时序满足要求,或者设计的面积紧张时,一般会采用资源共享的优化方法**,将第一种设计转换为第二种设计,绝大多数情况如是;但是**在某些特殊情况下,时序非常紧张,就会反其道而行之,将第二种设计转换为第一种设计**,从而便于调整组合逻辑信号的到达时间,提高这个加法选择器的工作频率。
## 7.4 香农扩展
前面已经讲到,香农扩展(Shannon Expansion)也是一种逻辑复制、增加面积、提高频率的时序优化手段。
其定义如下,布尔逻辑可以做如下扩展:
$$
F\left(a,b,c\right) = aF\left(a,b,c\right) +\bar{a}F\left(a,b,c\right)
$$
从上面的定义可以看到,香农扩展即布尔逻辑扩展,是卡诺逻辑化简反向运算,香农扩展相当于逻辑复制,提高频率;而卡诺逻辑化简相当于资源共享,节约面积。
香农扩展通过增加 MUX,从而缩短了某个优先级,但是组合路径长的信号路径时延,从而提高了该关键路径的工作频率。通过下面的例子,会对香农扩展有更全面的理解。
设所需的运算逻辑表达式为:
```verilog
F= ((({8{late}} | in0) + in1) == in2) & en;
```
其中信号 `in0` 、`in1` 和 ``in2 都是 8 bit 的数据,信号 `late` 和信号 `en` 是控制信号,信号 `late` 是本逻辑运算的关键路径信号,延时最大。
使用香农扩展。
```verilog
F = late.F(late = 1) + ~late.F(late = 0)
= late.[((({8{1'b1}} | i0)+in1) == in2) & en] + ~late.[((({8{11b0}} | in0) + in1) == in2) & en]
= late.[(8'b1 + inl) == in2) & en] + ~late[((in0 + in1) == in2) & en]
```
这相当于一个以 `late` 为选择信号,以 `[(8'b1 + in1) == in2) & en]` 和 `[((in0 + in1) == in2) & en]` 为两个输入信号的 2 选 1 的MUX 。因此, `late` 信号的优先级被提高,其信号路径的延时降低,但是其代价是设计的面积增加了,并且需要两个比较运算符。
未使用香农扩展的 Verilog 代码描述如下:
```verilog un_shannon.v
module un_shannon (in0, in1, in2, late, en, out);
input [7:0] in0, in1, in2;
input late, en;
output out;
assign out = ((({8{late}} | in0) + in1) == in2) & en;
endmodule
```
<a name="图5-24"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-240-BeforeShannonExtension.png!sign" width="" alt="未使用香农扩展前的逻辑表达式对应的RTL视图"/>
</div>
从图中可以清晰地看到,未使用香农扩展时,从输入 PAD late 到输出 PAD out 之间共有4 个逻辑单元, 5 段路径。其综合结果使用了 8 个 2 输入或门,1 个 8 bit 输入加法器, 1 个 8 bit 比较器, 1 个 2 输入与门。
使用香农扩展的 Verilog 代码如下:
```verilog shannon_fast.v
module shannon_fast (in0, in1, in2, late, en, out);
input [7:0] in0, in1, in2;
input late, en;
output out;
wire late_eq_0, late_eq_1;
assign late_eq_0 = ((in0+in1) == in2) & en;
//assign late_eq_0 = ((({8{1'b0}} | in0) + in1) == in2) & en;
assign late_eq_1 = ((8'b1+in1) == in2) & en;
//assign late_eq_1 = ((({8{1'b1}} | in0) + in1) == in2) & en;
assign out = (late) ? late_eq_1 : late_eq_0;
endmodule
```
<a name="图5-25"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-250-AfterShannonExtension.png!sign" width="" alt="香农扩展后的逻辑表达式对应的RTL视图"/>
</div>
在图中可以清晰地看到,使用香农扩展后,从输入 PAD late 到输出 PAD out 之间共有 1 个逻辑单元,2 段路径。其综合结果使用了 2 个 8 bit 输入加法器, 2 个 8 bit 比较器,2 个输入与门和一个 2 输入选择器。
从RTL 视图可以清晰地看到,采用香农扩展后,对于 `late` 信号这一关键路径,消除了 3 个逻辑层次,从而在一定程度上提高了设计的工作频率。作为提高工作频率的代价,是多用 1 个加法器和选择器,消耗了更多的面积。由于本例十分简单,所以多消耗的 LUT 和缩短的路径时延都不十分显著,如果在复杂设计中运用香农扩展,就会取得更加显著的效果。
正如前面反复强调的面积和速度的平衡关系所述,**是否使用香衣扩展时序优化手段,关键要看被优化对象的优化目标是面积还是路径**。
# 8. 小结
[图 5-26](#图5-26) 罗列了 RTL 编码的全面设计目标,它们是:
- 时序(Timing):要求设计满足预期的时序约束条件,满足预期频率要求。
- 面积(Areag):要求设计所消耗的资源满足所规划的面积要求。
- 时钟(Clocksg):要求设计中的时钟质量满足规划要求。
- 验证(Verificationg):要求设计通过仿真验证的测试。
- 可测试性(DFT, Design For Testg):要求设计时充分考量设计的可测试性。
- 可重用性(Reuseg):希望设计便于被重用,便千继承设计成果。
- 功耗(Powerg):要求设计的最大功耗在规定范围之内。
- Vendor Policies(原厂的要求):这里主要指 EDA 工具的优化测量和 PLD 或 ASIC 厂方的设计规格与软硬件需求。
<a name="图5-26"></a>
<div align="center">
<img src="https://josh-blog-1257563604.cos.ap-beijing.myqcloud.com/img/2020-12-05-josh-verilog-part-5/2020-12-05-josh-verilog-part-5-260-GoalsofRTLCode.png!sign" width="600px" alt="RTL代码的设计目标"/>
</div>
对一个 RTL 设计求全责备地要求它同时满足上述所有目标是非常困难的,应该根据这个RTL 设计的实现载体和具体应用,分析上述要求对本设计的重要性,然后综合考虑以上因素。