硬件描述语言,简称HDL,是一种电子系统硬件行为描述、结构描述、数据流描述的语言,可以用来表示逻辑电路图,逻辑表达式,复杂数字逻辑系统等;并且可以从顶层到底层逐层描述自己的设计思想,分层次的模块表示复杂的数字系统。
文章目录
前言
由于最近要学习FPGA ,所以写一个笔记来给自己记录比较常用的函数和数据类型,方便之后翻阅。
提示:以下是本篇文章正文内容,下面案例可供参考
一、Verilog 的基础结构
基本单元——module
在数字电路中,我们常常会把某些固定功能的电路作为一个模块封装起来,便于后续使用,因此这个模块的封装我们只需要注意:
- 模块的输入是什么?
- 模块的输出是什么?
- 输入输出是如何对应的?
在我们使用verilog 进行编程时,就是写好很多基本的module,然后描述他们的连接方式,从而组合成为一个系统。那么一个module需要什么呢?主要分为以下五个部分:
- 模块名 (让开发者快速了解模块的功能)
- 端口定义
- I/O口说明
- 内部信号的声明
- 模块的功能实现 (主要是代码部分)
//module 模块名 (端口1,端口2,端口3);
module FreDevider (
Clock,
rst,
Clkout
);
//I/O说明
input Clock;
input rst;
output Clkout;
//内部信号说明
reg Clkout;
//模块功能实现
always@(posedge Clock or posedge rst)
begin
if(rst)
Clkout<=0;
else
Clkout<=~Clkout;
end
endmodule
1.1 基本单元module的使用
上述已经写好了我们的module代码,后续使用的时候可以直接调用。
方法是: 模块名 + 实例名 + 端口声明 + 信号声明
//已经定义过一个叫FreDevider的module
FreDevider uut1( //模块名:FreDevider 实例名:uut1
.Clock(clock_signal), // 端口声明: .端口名 信号声明: (信号名)
.rst(rst_signal),
.Clkout(clkout_signal)
);
1.2 I/O 口的说明
I/O口的类型有三种:input, output, inout
其中,input和output分别是输入和输出、
inout 是双向端口,具有双向传输的能力,节约端口。在使用中应该避免两个方向需要同时传输的情况,在实际应用中,使用的不多。
I/O口的说明可以放在端口定义之后,如:
module FreDevider (
input Clock,
input rst,
output Clkout
);
也可以在端口定义的同时说明信号的位数。
module FreDevider (
input Clock,
input rst,
input [15:0] x; //16位输入
output [31:0] y; //32位输出
);
1.3 内部信号的声明
信号的属性一般有reg (寄存器类型)、wire(线网类型)。
寄存器类型用于表示可以存储值的变量,而线网类型用于表示信号和连接。
// 寄存器类型举例\
// 表示寄存器或存储单元,是一种有记忆元件的数据存储器
reg [3:0] my_reg; // 定义一个4位宽的寄存器类型变量my_reg
// 线网类型举例
// 用于表示信号或线网,是一种无记忆元件的数据传输线
wire [3:0] my_wire; // 定义一个4位宽的线网类型变量my_wire
每个信号都要定义其属性,但是对于模块的输入信号,其属性必须不是reg型,一般为wire型。
对于没有声明的信号,其默认为wire型,因此在定义时,我们只需要定义输出信号的类型和中间变量的类型即可。
1.4 模块功能的实现
在一个模块功能的实现方法中,通常有三种类型:
- 用assign声明语句
用于驱动线网型的变量,声明语句右边的变量是敏感信号。
右边的值发生变化时,立刻计算左边的结果,进行表达。
当输入变化时,输出也随之变化。属于组合逻辑电路。
assign a_not=~a;
assign c=a&b;
-
采用实例化的原件
采用IP核的形式实现。
-
采用always语句块
always语句块既能描述组合逻辑电路,又能描述时序逻辑电路。与assign不同的是,always语句后面的触发条件是持续敏感,也就是每时每刻都在执行或者判断的。
//生成时钟信号
always #5 clk=~clk;
//组合逻辑电路:二选一多路器
reg c_out;
always @(a_in or b_in or sel)
if(sel)
c_out=a_in;
else
c_out=b_in;
//时序逻辑电路:二分频模块
reg d;
always @(posedge clk or posedge rst)
begin
if(rst)
d<=1'b0;
else
d=~d;
二、Verilog 的数据类型
在Verilog中,有多种数据类型可供使用,包括位向量类型、整数类型、实数类型、布尔型、时间类型和字符串类型等。
整型和实数型用于表示数字,布尔型用于表示逻辑值。向量型用于表示多位数据。
下文将着重介绍:
1.整数型
integer 类型用于表示整数值,常用语计数器、延时器等电路中表示整数。占用存储空间是32位。
1.1 数制
- 十进制: d/D
- 二进制: b/B
- 八进制: o/O
- 十六进制: h/H
一般来说,我们使用以下的方式表示一个整数:
<位宽> + <'> <进制> + <数字>
8'd23 //位宽8 十进制 数字23
8'b00010111 //位宽8 二进制 数字23
8'o27 //位宽8 八进制 数字23
8'h17 //位宽8 十六进制 数字23
当数字较多时,使用下划线来辅助表达,但是要注意下划线不能出现在位宽和进制中,也不能出现在数字的第一位。
· 16'b0100_1011_1101_0111 //正确
· 16'b_1110_1101_0101_0101 //错误
1.2 实数和字符型
实数型或者字符型的数据可以在verilog语言中出现,但是却不能通过硬件表达出来。而是常常出现在命令、显示等非硬件参与的操作中。
这与上面出现的整数的表达方式和意义是不一样的。
(1)实数
可以用十进制来表示,也可以使用科学计数法来表示。
12 //表示12
23e-3 //表示0.0023
2.3e-2 //表示0.0023
23E4 //表示230000
在实际的仿真过程中,所有的科学计数的表达例如23e-3或者23E4,在硬件中都是0,而硬件中也不能直接表达小数。
在verilog中写出来的小数,会被硬件识别为0。
在程序中可以使用real类型表示实数,通常用于浮点数的计算。
2. 逻辑值 (X和Z状态)
对于Verilog语言来说,存在 X 和 Z 两种独立于0 、1之外的状态,分别是未知态和高阻态。常用语仿真和综合。
未知态X是:无法确定此时信号的状态是1还是0,但是能确定信号是有状态的,不是1就是0,且这个状态是能够影响到与其相连的后续电路的。
当我们用电表测量时,其值可能是1,可能是0,取决于被测时硬件电路的当前状态
高阻态Z是指,当前的信号状态既不是1,也不是0,而是没有状态,或者可以认为是断开,即此时信号的状态已经无法再影响到后续的电路
而在实际电路中,某一时刻只有1、 0、高阻态三种。
尽管X和Z表示的状态不是传统的1和0,但是X和Z也能参与到二进制的逻辑运算中来。在逻辑运算中,X和Z满足如下的规律:
0 && X = 0 ;
1 && X = X ;
0 || X = X ;
1 || X = 1 ;
0 && Z = 0 ;
1 && Z = X ;
0 || Z = X ;
1 || Z = 1 ;
3. 其他
其他更详细的函数、定义等可以参考另一个博主的内容,非常齐全,我就不做多余的工作了:
三、控制结构
Verilog HDL提供了多种控制结构,包括顺序块和并行块。顺序块按照代码的顺序执行,而并行块则可以同时执行多个操作。
3.1 顺序块
顺序块的语句会按照他们出现的顺序执行。
- 通常用于时序逻辑,如触发器、寄存器等。
- 执行顺序从上到下,从左到右。
// 顺序块示例
always @(posedge clk) begin
a <= b;
c <= d;
end
3.2 并行块
并行块中的语句可以同时执行。
- 通常用于组合逻辑。如加法器、多路选择器等
- 语句执行不依赖顺序
// 并行块示例
assign e = f & g;
assign h = i | j;
此外,Verilog 还提供了always块和initial块,用于描述硬件的行为。if、case和for控制结构则可用于实现条件控制和循环操作
3.3 always块
常用于描述组合逻辑和时序逻辑。
- 它的行为取决于其敏感列表。敏感列表是一个或多个信号,当这些信号变化时,always块内的语句会重新执行。
// always块示例(时序逻辑)
always @(posedge clk or negedge reset) begin
if (!reset)
q <= 0;
else
q <= p;
end
3.4 initial块
一般用于在仿真开始时执行一次的代码。
- 常用于生成和初始化信号,以及仿真一开始的一次性计算。
- initial 块的敏感列表中可以包含时钟信号或延迟时间。
// initial块示例(仿真初始化)
initial begin
int i = 0;
while (i < 10) begin
i = i + 1;
$display("Counter: %d", i);
end
end
3.5 if 控制结构
- 用于在某个条件下执行一段代码。
- 如果条件为真,则执行一段代码;如果条件为假,则执行另一段代码(可选)。
// if控制结构示例
reg [1:0] sel;
if (sel == 2'b00) begin
$display("Option A");
end else if (sel == 2'b01) begin
$display("Option B");
end else if (sel == 2'b10) begin
$display("Option C");
end else begin
$display("Invalid option");
end
3.6 case 控制结构:
- 用于根据多个可能的条件执行不同的代码段。
- 与switch-case结构类似,根据输入信号的某个值选择执行相应的代码段。
// case控制结构示例
reg [1:0] mode;
case (mode)
2'b00: $display("Mode A");
2'b01: $display("Mode B");
2'b10: $display("Mode C");
default: $display("Invalid mode");
endcase
3.7 for 控制结构
- 用于重复执行一段代码多次。
- 需要指定循环变量、初始值、每次循环后的增量和终止条件。
// for控制结构示例(仿真中)
initial begin
for (int i = 0; i < 10; i = i + 1) begin
$display("Counter: %d", i);
end
end
四、任务和函数
任务一般用于执行特定的操作,而函数则用于计算并返回一个值。他们可以用来简化代码和提高代码的可用性。
1. 任务的定义和使用
任务(task)是一种预定义的子程序,可以在模块内部或模块之间调用。任务用于封装重复的逻辑,以提高代码的可重用性和可维护性。
定义:
task task_name;
// 任务内容
endtask
使用:
task my_task;
// 任务内容
endtask
initial begin
my_task; // 调用任务
end
在上面的例子中,定义了一个名为my_task的任务,并在initial块中调用了该任务。
2. 函数的定义和使用
函数(function)是一种特殊的任务,返回一个值,可以在模块内部或模块之间调用。
定义:
function function_name;
// 函数内容
return value; // 返回值
endfunction
使用:
function add;
input [7:0] a, b; // 输入8位宽的信号a和b
reg [7:0] sum; // 输出8位宽的信号sum
sum = a + b; // 计算和并返回结果
return sum; // 返回结果作为函数的返回值
endfunction
initial begin
$display("%d", add(8'b0000_0001, 8'b0000_0010)); // 调用函数并显示结果
end
在本例中,定义了一个名为 add 的函数,用于计算两个8位宽的信号的和。在 initial 块中调用了该函数,并使用 $display 语句显示结果。