前言

所谓HDL的语法结构,也就是对编写HDL时所要遵循的一些规则,为良好的代码风格修炼的关键一步。
本文节选自《FPGA之道》,其总结字字玑珠,可谓肺腑之言,下面一起来学习之。

语法结构

HDL的语法是比较灵活的,而语法结构又是HDL代码的基本组成元素,因此这也导致了我们写出的HDL代码具有多样化的特性。那么,为了养成良好的代码风格,在使用相关语法结构的时候也要遵循一定的原则。

省略与否请一致

这点主要是针对Verilog语言来说的,由于在Verilog中,很多语句结构在子语句数量仅为一句的时候,可以省略begin-end关键字。不过在这里还是建议大家不要省略为好,因为begin-end关键字天生就是语句段落的区分标志,能够很方便的辅助人眼识别出语句段落功能集群,并且便于代码的修改。因此建议大家不要为了图一时的省事而省略begin-end关键字。例如:

// Verilog example
always@(*)
	if(sel == 1’b1)
		a = in1;
	else
	begin
		a = in2;
		b = in1;
end

上述代码有两处省略。第一处是always程序块,由于只有一句条件语句,所以省略的begin-end关键字。第二处是if条件语句的第一个分支,由于此分支仅有一条语句,因此省略begin-end关键字。由于这两处省略,使代码看起来稍显凌乱,并且如果要对if条件语句的第一个分支进行修改的话,如果添加了子语句,又必须重新添加begin-end关键字,如果日后又删除了一个子语句,那么又可以删掉begin-end关键字,非常麻烦;对于always程序块的修改也是一样。因此建议大家在HDL代码的编写中不要省略begin-end关键字的书写,例如:

// Verilog example
always@(*)
begin
	if(sel == 1’b1)
	begin
		a = in1;
	end
	else
	begin
		a = in2;
		b = in1;
end
	end

范围方向请一致

在HDL代码中,对应逻辑向量或者数组等的时候,常常涉及到范围的书写。而范围的书写方式又分为两种:一种是从低到高,一种是从高到低。一般来说,这两种写法并没有优劣之分,但是建议大家为了代码的统一性与易读性,尽量只采用一种方式来表示范围。通常建议大家使用从高到低的范围表示法,因为它和数的二进制表示形式是一致的。例如:

	-- VHDL example 
	signal data : std_logic_vector(31 downto 0); 

// Verilog example
reg[31:0] data; 

端口声明请一致

这点也是主要针对Verilog语法来说的,因为Verilog语法对于module端口的定义有很多种变化。那么,为了统一起见,请选定一种端口声明方式作为你的编写风格。本书建议这样的Verilog端口声明形式:

// Verilog example
Module xxx(
	input clk,
		output reg data,
		……
);
……

顺便一提,HDL中,对于同样类型的端口或者变量,可以分开来声明也可以合并成一条语句同时声明。不过对于端口的声明,并不建议这么做,因为无论从可读性还是易修改性来说,合并端口的声明都没有好处。例如:

-- VHDL example 
entity xxx is
port (
   a, b : in std_logic_vector(7 downto 0);
   c : in std_logic_vector(5 downto 0);
   d : out std_logic_vector(4 downto 0)
);

// Verilog example
module(
	input [7:0] a,b, 
	input [5:0] c,
output [4:0] d
);

写成这样会更好:

-- VHDL example 
entity xxx is
port (
   a : in std_logic_vector(7 downto 0);
   b : in std_logic_vector(7 downto 0); 
   c : in std_logic_vector(5 downto 0);
   d : out std_logic_vector(4 downto 0)
);

// Verilog example
module(
	input [7:0] a,
input [7:0] b, 
	input [5:0] c,
output [4:0] d
);

参数声明请一致

这一点又主要是针对Verilog语法来说的,这也从一个侧面反映出Verilog的语法更加灵活一些,而VHDL的语法较严谨一些。由于在Verilog中,参数parameter声明和修改的位置都比较灵活,因此,为了代码的统一性,也请大家固定好自己的参数使用风格,切忌混用。在这里,本书推荐大家采用在module端口声明之前声明参数,在module例化语句的最开始修改参数。例如:

// Verilog example
module xxx
#(parameter WIDTH = 8)
(
input a,
output b
);
……
xxx #(.WIDTH(4))
	m0 (.a (a),
		.b (b));

映射方式请一致

这里的映射方式主要指例化时的映射方式,通过之前HDL语法的学习我们可以了解到,例化时端口的赋值都是采用映射赋值的形式,而映射赋值有显式的也有隐式的。隐式映射一般是指按位置映射,这种映射方式书写简便,但是不利于代码的阅读和修改,再加上代码风格对统一性的要求,因此这里推荐大家仅采用显式的方式来进行映射。例如:

	-- VHDL example 
m0: xxx
PORT MAP(
		a => a,
		b => b,
		c => c
	);

// Verilog example
xxx m0(.a (a),
		  .b (b) ,
		  .c (c));

代码缩进请一致

没有缩进的代码对于阅读者来说是个噩梦,例如:

	-- VHDL example 
if(a = '1')then
if(b = '1')then
c <= "11";
else
c <= "10";
end if;
else 
c <= "00";
end if;

// Verilog example
	if(a == 1'b1)
	if(b == 1'b1)
	c = 2'b11;
	else
	c = 2'b10;
	else
	c = 2'b00;

而乱缩进的代码对于阅读者来说更是误导,例如:

	-- VHDL example 
if (a =1)then
if(b =1)then
c <=11;
else
c <=10;
end if;
else 
c <=00;
end if;

// Verilog example
		if(a == 1’b1)
	if(b == 1’b1)
		c = 2’b11;
		else
			c = 2’b10;
	else
			c = 2’b00;

一般来说,保证子语句比主语句多缩进一个制表符,就可以达到比较好的效果,例如:

	-- VHDL example 
if (a =1)then
if(b =1)then
c <=11;
else
c <=10;
end if;
else 
c <=00;
end if;

// Verilog example
	if(a == 1’b1)
		if(b == 1’b1)
			c = 2’b11;
		else
			c = 2’b10;
	else
		c = 2’b00;

空格空行

适当的应用空格与空行,可以让代码的脉络显得更加清晰;如果缺少空格与空行的修饰,那么整个代码揉在一起会给人一种非常混杂的感觉,并且表达式也会显得头重脚轻。例如:

-- VHDL example 
signal a:std_logic;
signal b:std_logic;
process(a,b)
begin
	c<=a and b;
end
d<=a or b;

// Verilog example
reg a;
reg b;
always@(*)
	c<=a&b;
assign d=a|b;	

如果写成下面这样,感观上就会清晰许多:

-- VHDL example 
signal a : std_logic;
signal b : std_logic;

process(a, b)
begin
	c <= a and b;
end

d <= a or b;

// Verilog example
reg a;
reg b;

always@(*)
	c<=a&b;

assign d=a|b;	

不过也请注意,太多的使用空格和空行,会让代码变得非常松散,反而不容易看懂,因此空格与空行的使用也要掌握一个“度”的问题,要明白过犹不及的道理。

注释编写

注释主要有三大作用:解释说明、段落分隔和代码保留,分别介绍如下:

解释说明

无论命名是多么的有意义,无论语法是多么的统一,无论段落是多么的清晰,代码就是代码,相比于人类的思维,它更适合于机器阅读。那人类最容易理解的语言是什么?没错,是自然语言,例如汉语之于中国,英语之于欧美。那么为了进一步增强代码的可读性,我们可能需要在相应的代码段中加入一些自然语言来做解释说明之用。由于HDL代码本身不支持非语法元素的自然语言,因此,这时候注释语法就派上了用场。例如:

-- VHDL example 	
process(clk)
begin
	if(clk'event and clk = '1')then
		delay0 <= dataIn;
		delay1 <= delay0; 
		delay2 <= delay1; 
		delay3 <= delay2; 
		tmp <= delay0 + delay1 + delay2 + delay3;
		dataOut <= tmp srl 2;
	end if;
end

// Verilog example
always@(posedge clk)
begin
	delay0 <= dataIn;
	delay1 <= delay0; 
	delay2 <= delay1; 
	delay3 <= delay2; 
	tmp <= delay0 + delay1 + delay2 + delay3;
	dataOut <= tmp >> 2;
end

上述例子中由于没有注释,所以我们不太容易能看出它到底完成了什么功能,可是如果加上注释后,效果就大不一样了:

-- VHDL example 	
-- 这段代码描述了一个低通FIR滤波器的功能,滤波器的公式为:
-- y[n] = (x[n] + x[n-1] + x[n-2] + x[n-3]) / 4;
process(clk)
begin
	if(clk'event and clk = '1')then
		delay0 <= dataIn;
		delay1 <= delay0; 
		delay2 <= delay1; 
		delay3 <= delay2; 
		tmp <= delay0 + delay1 + delay2 + delay3;
		dataOut <= tmp srl 2;
	end if;
end

// Verilog example
// 这段代码描述了一个低通FIR滤波器的功能,滤波器的公式为:
// y[n] = (x[n] + x[n-1] + x[n-2] + x[n-3]) / 4;
always@(posedge clk)
begin
	delay0 <= dataIn;
	delay1 <= delay0; 
	delay2 <= delay1; 
	delay3 <= delay2; 
	tmp <= delay0 + delay1 + delay2 + delay3;
	dataOut <= tmp >> 2;
end

段落分隔

除了空格、空行外,注释也能实现对代码段落的分隔,而且还更加形象、更加多样化,尤其是Verilog支持段落的注释。在这里大家可以充分发挥自己的想象力和主观能动性,但是也请注意一个“度”的问题,把代码修饰的太花哨或者太杂乱,反而看起来更加费劲。关于分隔的应用可参考下例:

-- VHDL example 	
……
-- *********** 华丽的分隔线 ***********
……

// Verilog example
……
// *********** 华丽的分隔线 ***********
……

代码保留

注释的第三个用处就是功能保留,这主要是针对代码修改时的一些不确定因素决定的,例如你之前写了一个加法,可是后来修改为一个减法,但是也许因为一些上层的原理性问题,以后此处还是需要调整,那么就可以利用注释来保留一个本次用不到的功能,这样下次修改的时候就不需要重新输入代码了。例如:

-- VHDL example 	
a <= b + c;
-- a <= b - c;

// Verilog example
assign a = b + c;
-- assign a = b - c;

模块设计

模块设计其实是一个方案层的问题,不过,为了能够用代码将方案表达的更加的清晰、合理和生动,我们就需要在代码风格上面下些功夫。

确定好端口的顺序

VHDL中的entity,Verilog中的module,往往都会包含若干个输入、输出甚至双向端口,名称也许五花八门,那么这些端口该怎么排列呢?按端口名称的字母顺序排列显然不是一个好方法,因为字母顺序并不含有任何功能方面的意义,它几乎和随机排序一样,并不能带给我们更多的信息量,也不能辅助我们更好的理解和编写代码。因此,我们推荐大家结合功能和端口方向来对模块端口进行合理的顺序安排。

先方向后功能

这种方法的思想是将端口的声明先按方向分为两到三个大的部分(一般来说是先输入,再输出,最后双向),然后对于每一个部分,再按照功能分为若干个小块。例如:

-- VHDL example 
add0 : in std_logic_vector(7 downto 0);
add1 : in std_logic_vector(7 downto 0);

mul0 : in std_logic_vector(7 downto 0);
mul1 : in std_logic_vector(7 downto 0);

-- 按方向的分隔线

addResult : out std_logic_vector(8 downto 0);

mulResult : out std_logic_vector(15 downto 0);

// Verilog example
input [7:0] add0,
input [7:0] add1,

input [7:0] mul0,
input [7:0] mul1,

// 按方向的分隔线

output [8:0] addResult,
 
output [15:0] mulResult,

先功能后方向

这种方法的思想是将端口的声明先按功能分为若干个的部分,然后对于每一个部分,再按照端口方向(仍然是先输入,再输出,最后双向)分为两或三个小块。例如:

-- VHDL example 
add0 : in std_logic_vector(7 downto 0);
add1 : in std_logic_vector(7 downto 0);

addResult : out std_logic_vector(8 downto 0);

-- 按功能的分隔线

mul0 : in std_logic_vector(7 downto 0);
mul1 : in std_logic_vector(7 downto 0);

mulResult : out std_logic_vector(15 downto 0);

// Verilog example
input [7:0] add0,
input [7:0] add1,

output [8:0] addResult,

// 按功能的分隔线

input [7:0] mul0,
input [7:0] mul1,
 
output [15:0] mulResult,

从个人角度出发,我更推荐此种模块端口的划分方法。

时钟和复位

注意,如果模块内部不仅仅包含组合逻辑的话——实际上大部分情况下都是时序逻辑——那么模块的端口声明中肯定少不了时钟信号,同时也往往少不了复位信号。一般来说,时钟和复位信号的应用是贯穿整个模块的,而不是局限于模块内部某部分逻辑功能的,因此,一般在端口声明的时候,都会将时钟和复位信号放在最前面,至于两者之间的顺序,就看个人习惯了。例如:

-- VHDL example 
port (
	aReset : in std_logic; --异步复位
	clk : in std_logic; 
	rst : in std_logic; -- 同步复位
	……

// Verilog example
module xxx(
	input aReset, // 异步复位
	input clk,
	input rst,//同步复位
	……

多时钟模块

对于有多个时钟驱动的模块,建议先按照时钟域将端口分为若干个部分,然后每一部分再套用前面介绍的三点对端口进行排序即可。例如:

-- VHDL example 
clk100MHz : in std_logic;
fastDataIn : in std_logic_vector(7 downto 0);

fastDataOut : out std_logic_vector(7 downto 0);

-- 按时钟域的分隔线

clk100KHz : in std_logic;
slowDataIn : in std_logic_vector(7 downto 0);

slowDataOut : out std_logic_vector(7 downto 0);

// Verilog example
input clk100MHz,
input [7:0] fastDataIn,

output [7:0] fastDataOut,

// 按时钟域的分隔线

input clk100KHz,
input [7:0] slowDataIn,

output [7:0] slowDataOut,

统一端口的名称

我们的FPGA设计,一般都不止包含一个模块,大多数情况,一个中等规模的设计可能由几十甚至上百个子模块来实现。那么模块多了,自然端口也就更多了,对于那些功能、含义相同的端口,建议尽量在各个模块中统一它们的名称。例如时钟信号端口和复位信号端口就是最典型的例子,如果名称不同,会让人产生混淆,并且实例化的时候也容易引起笔误。例如:

-- VHDL example 
-- entity A
clk : in std_logic;
rst :in std_logic;

-- entity B
clock : in std_logic;
reset :in std_logic;

// Verilog example
// module A
input clk,
input rst,

// module B
input clock,
input reset,

上述代码造成混淆的原因就是因为端口名称不统一,而写成如下形式则会好很多:

-- VHDL example 
-- entity A
clk : in std_logic;
rst :in std_logic;

-- entity B
clk : in std_logic;
rst :in std_logic;

// Verilog example
// module A
input clk,
input rst,

// module B
input clk,
input rst,

文件结构

从代码风格上来讲,建议一个VHDL文件内部只描述一个entity,一个Verilog文件内部只描述一个module。除此以外,请让*.vhdl的文件名与它所描述的entity名一致,让*.v的文件名与它所描述的module名一致。例如:

-- VHDL example 
-- file name : XXXX.vhd	
entity XXXX is
……

// Verilog example
// file name : YYYY.v
module YYYY
……