掌握FPGA核心:Veilog HDL语法与高效框架全解析
摘要 :Verilog HDL硬件描述语言是在用途最广泛的C语言的基础上发展起来的一种硬件描述语言,具有灵活性高、易学易用等特点。Verilog HDL可以在较短的时间内学习和掌握,FPGA的Veilog HDL基础语法总结,看完这些,FPGA的基本语法应该就没啥问题了!
一、基础知识
1、逻辑值
逻辑0:表示低电平,也就对应我们电路
GND
;
逻辑1:表示高电平,也就是对应我们电路的
VCC
;
逻辑X:表示未知,有可能是高电平,也有可能是低电平;
逻辑Z:表示高阻态,外部没有激励信号,是一个
悬空状态
。
2、进制格式
Verilog数字进制格式包括二进制、八进制、十进制和十六进制。
一般常用的为二进制、十进制和十六进制。
二进制表示如下:4b0101表示4位二进制数字0101
十进制表示如下:4'd2表示4位十进制数字2(二进制0010)
十六进制表示如下:4ha表示4位十六进制数字a(二进制1010)
16'b1001 1010 1010 1001=16'h9AA9
3、标识符
标识符(identifier)用于定义模块名、端口名、信号名等。
标识符可以是任意一组字母、数字、$符号和(下划线)符号的组合;
但标识符的第一个字符必须是字母或者下划线;
标识符是区分大小写的;
4、标识符推荐写法
不建议大小写混合使用;
普通内部信号建议全部小写;
信号命名最好体现信号的含义,简洁、清晰、易懂;
以下是一些推荐的写法:
-
1、用有意义的有效的名字如
sum
、cpu_addr
等。 -
2、用下划线区分词,如
cpu addr
。 -
3、采用一些前缀或后缀,比如时钟采用clk前缀:
clk_50
,clk_cpu
;
二、数据类型
在Verilog 语言中,主要有三大类数据类型。
寄存器数据类型、线网数据类型和参数数据类型 。
从名称中,我们可以看出,真正在数字电路中起作用的数据类型应该是 寄存器数据类型 和 线网数据类型 。
1、寄存器类型
寄存器表示一个抽象的数据存储单元,通过赋值语句可以改变寄存器储存的值寄存器数据类型的关键字是reg,reg类型数据的默认初始值为不定值x。
reg类型的数据只能在
always语句
和
initial语句
中被赋值。
如果该过程语句描述的是 时序逻辑 , 即always语句带有时钟信号,则该寄存器变量对应为触发器 ;
如果该过程语句描述的是 组合逻辑 , 即always语句不带有时钟信号,则该寄存器变量对应为硬件连线 ;
//计数器对系统时钟计数,计时0.2秒
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
counter <= 24'd0;
else if (counter < 24'd999_9999)
counter <= counter + 1'b1;
else
counter <= 24'd0;
end
//通过移位寄存器控制IO口的高低电平,从而改变LED的显示状态
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
led <= 4'b0001;
else if(counter == 24'd999_9999)
led[3:0] <= {led[2:0],led[3]};
else
led <= led;
end
2、线网类型
线网数据类型表示 结构实体(例如门)之间的物理连线 。
线网类型的变量不能储存值 ,它的值是由驱动它的元件所决定的。驱动线网类型变量的元件有 门 、 连续赋值语句 、 assign 等。
如果没有驱动元件连接到线网类型的变量上,则该变量就是高阻的,即其值为z。
线网数据类型包括
wire型
和
tri型
,其中最常用的就是wire类型。
3、参数类型
参数其实就是一个常量,在Verilog HDL中用parameter定义常量。
我们可以一次定义多个参数,参数与参数之间需要用 逗号隔开 。
每个参数定义的右边必须是一个常数表达式 。
参数型数据常用于 定义状态机的状态 、 数据位宽 和 延迟大小 等。
采用标识符来代表一个常量 可以提高程序的可读性和可维护性 。
在模块调用时,可通过参数传递来 改变被调用模块中已定义的参数 。
三、运算符
1、算数运算符
2、关系运算符
3、逻辑运算符
4、条件操作符
result=(a>=b)?a:b;
5、位运算符
6、移位运算符
两种移位运算都用0来填补移出的空位。
左移时,位宽增加;右移时,位宽不变。
4b1001 <<2 = 6'b100100;
4b1001 >>1 = 4b0100;
7、拼接运算符
c={a,b[3:0];
8、优先级运算符
四、模块结构
Verilog的基本设计单元是“模块"(block)。
一个模块是由两部分组成的,一部分描述接口,另一部分描述逻辑功能。
使用quartusii软件编写出上图左边的硬件描述代码,通过软件编译,就能生成最右边组合逻辑电路图来。每个Verilog程序包括4个主要的部分: 端口定义、I0说明、内部信号声明、功能定义 。
上图时流水灯的代码,第一个always块代码的意思:
如果
时钟信号的上升沿
或者
复位信号的下降沿到
来,就执行
begin
与
end
之间的代码。
如果产生了复位信号(低电平),计数器清0,如果计数器的值小于10000000,计数器的值就+1,如果没有产生复位信号和计数值不小于10000000,计数器的值就为0。在这个always块中,逻辑是顺序执行的。
第二个always块代码的意思:
如果
时钟信号的上升沿
或者
复位信号的下降沿到
来,就执行
begin
与
end
之间的代码。如果产生了复位信号(低电平),led0点亮,如果计数器的值小于10000000,led0-3顺序点亮,如果没有产生复位信号和计数值不小于10000000,led灯状态保持不变。在这个always块中,逻辑是顺序执行的。
但是这个always块代码是并行执行的,也就是说时钟信号一直在产生。
功能定义部分有三种方法:
-
1、assign语句描述组合逻辑
-
2、always语句描述组合/时序逻辑
-
3、例化实例元件
上述三种逻辑功能是并行执行的。
五、结构语句
1、initial和always语句
initial语句它在模块中只执行一次。
它常用于测试文件的编写,用来产生仿真测试信号(激励信号),或者用于对存储器变量赋初值。
always 语句一直在不断地重复活动。但是只有和一定的时间控制结合在一起才有作用。
一般
initial语句
常用于测试文件,在测试文件中初始化使用。比如上面的代码首先始终信号初始化为0,之后在always语句中让其10个时钟周期翻转一次,就达到了时钟的要求。
复位信号最开始为低电平,然后延时20个时钟周期就拉高。触摸按键信号最开始为低电平,延时10和时钟周期后拉高,再延时30个时钟周期再拉低,延时110个时钟周期再拉高,再延时30个时钟周期再拉低。
always的时间控制可以是 沿触发 ,也可以是 电平触发 ;可以是 单个信号 ,也可以是 多个信号 , 多个信号中间要用关键字or连 接。always 语句后紧跟的过程块是否运行,要看它的触发条件是否满足。
沿触发的always块常常描述时序逻辑行为 。由 关键词or 连接的多个事件名或信号名组成的列表称为“ 敏感列表 ”。
电平触发的always块常常描述组合逻辑行为。
2、组合逻辑和时序逻辑电路
根据逻辑功能的不同特点,可以将数字电路分成两大类:
组合逻辑电路和时序逻辑电路。
-
组合逻辑电路中 ,任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。
-
时序逻辑电路中 ,任时刻的输出不仅取决于当时的输入信号,而且还取决于电路原来的状态。或者说还与以前的输入有关,因此时序逻辑必须具备记忆功能。
3、赋值语句
Verilog HDL 语言中,信号有两种赋值方式
1、阻塞赋值(blocking),如b=a
2、非阻塞赋值(Non_Blocking),如b<=a
3.1、阻塞赋值
阻塞赋值可以认为只有一个步骤的操作:即计算RHS(左侧)并更新LHS(右侧)。
所谓阻塞的概念是指,在同一个always块中,后面的赋值语句是在前一句赋值语句结束后才开始赋值的。
module block_nonblock(Clk,Rst_n,a,b,c,out)
input Clk;
input Rst_n;
input a;
input b;
input c;
output reg [1:0] out;
// out a + b + c;最大值为3,所以应该定义为2位的位宽
// d = a+b;
// out = d+c;
reg [1:0]d;//定义一个中间变量
always @(posedge Clk or negedge Rst_n)
if (!Rst_n)
out = 2'b0;
else begin
d = a+b;
out = d+c;
end
endmodule
现在我们改变一下d= a+b;out = d+c;的顺序,就会发现综合出来的电路是完全不同的。
module block_nonblock(Clk,Rst_n,a,b,c,out)
input Clk;
input Rst_n;
input a;
input b;
input c;
output reg [1:0] out;
reg [1:0]d;//定义一个中间变量
always @(posedge Clk or negedge Rst_n)
if (!Rst_n)
out = 2'b0;
else begin
out = d+c;
d = a+b;
end
endmodule
3.2、非阻塞赋值
非阻塞赋值的操作过程可以看作两个步骤
(1)赋值开始的时候,计算RHS(左侧);
(2)赋值结束的时候,更新LHS(右侧)。
所谓非阻塞的概念是指,在计算非阻塞赋值的RHS以及更新LHS期间,允许其他的非阻塞赋值语句同时计算RHS和更新LHS。
非阻塞赋值只能用于对寄存器类型的变量进行赋值,因此只能用在initial块和always块等过程块中 。
还是用上面的例子
module block_nonblock(Clk,Rst_n,a,b,c,out)
input Clk;
input Rst_n;
input a;
input b;
input c;
output reg [1:0] out;
reg [1:0]d;//定义一个中间变量
always @(posedge Clk or negedge Rst_n)
if (!Rst_n)
out = 2'b0;
else begin
d <= a+b;
out <= d+c;
end
endmodule
生成效果如下:
现在我们改变一下d= a+b;out = d+c;的顺序,就会发现综合出来的电路是完全相同的。这里由于采用的非阻塞赋值,因此交换语句的前后顺序并不会对最终生成的逻辑电路有实际影响。
module block_nonblock(Clk,Rst_n,a,b,c,out)
input Clk;
input Rst_n;
input a;
input b;
input c;
output reg [1:0] out;
reg [1:0]d;//定义一个中间变量
always @(posedge Clk or negedge Rst_n)
if (!Rst_n)
out = 2'b0;
else begin
out <= d+c;
d <= a+b;
end
endmodule
1、在描述
组合逻辑
(电平触发)的always 块中用
阻塞赋值=
,综合成组合逻辑的电路结构;这种电路结构只与输入电平的变化有关系。
2、在描述
时序逻辑
(沿触发)的always 块中用
非阻塞赋值=
,综合成时序逻辑的电路结构;这种电路结构往往与触发沿有关系,只有在触发沿时才可能发生赋值的变化。
“注意 :在同一个always块中不要既用非阻塞赋值又用阻塞赋值 不充许在多个always块中对同一个变量进行赋值!因为在多个always块中代码时并行执行的。
”
一般在设计中掌握以下六个原则,可解决在综合后仿真中出现绝大多数的冒险竞争问题。
1)时序电路(沿触发的always块)建模时,用非阻塞赋值;
2)锁存器电路建模时,用非阻塞赋值;
3)用always块建立组合逻辑(电平触发的always块)模型时,用阻塞赋值;
4)在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值:
5)在同一个always块中不要既用非阻塞赋值又用阻塞赋值;
6)不要在一个以上的always块中为同一个变量赋值。
4、条件语句
条件语句必须在 过程块 中使用。 过程块语 句是指 由initial语句和always语句引导的块语句 。
4.1 if_else语句
1、允许一定形式的简写,如:
if(a) 等同于if(a==1)
if(la)等同于if(a!=1)
2、
if语句
对表达式的值进行判断,若为0,x,z,则按假处理;若为1,按真处理。
3、
if和else
后面的操作语句可以用
begin和end
包含多个语句。
4、允许if语句的
嵌套
。
4.1 case语句
case语句(多分支选择语句)
1、分支表达式的值互不相同;
2、所有表达式的
位宽必须相等
;不能用
’bx
来代替
n'bx
3、casez比较时,不考虑表达式中的 高阻值
4、casex不考虑 高阻值z 和 不定值x
注意
if_else
需要配对,一个if语句就应该必须有一个else语句。好处是避免latch产生。
latch
是一个锁存器,在数字电路中latch是一个电平触发的存储器,触发器是一个边沿触发的存储器。在编写veilog语句中应避免产生无畏锁存器,锁存器只在组合逻辑电路中产成,而锁存器会导致电路生成的毛刺比较多,还会影响我们对整个电路的时序分析。
什么样的情况下会产生这个锁存器呢?
首先在组合逻辑电路中,如果我们有
if语句
但是没有相应的
else语句
,他就有可能产生锁存器。第二点,比如case语句,如果我们的case语句没有给完全,没有列举完所有应该的产生的case语句,就应该写一个
default
,否则也会生成一个锁存器。