FPGA——Verilog 语法最基础部分(一)

硬件描述语言,简称HDL,是一种电子系统硬件行为描述、结构描述、数据流描述的语言,可以用来表示逻辑电路图,逻辑表达式,复杂数字逻辑系统等;并且可以从顶层到底层逐层描述自己的设计思想,分层次的模块表示复杂的数字系统。



前言

由于最近要学习FPGA ,所以写一个笔记来给自己记录比较常用的函数和数据类型,方便之后翻阅。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Verilog 的基础结构

基本单元——module

在数字电路中,我们常常会把某些固定功能的电路作为一个模块封装起来,便于后续使用,因此这个模块的封装我们只需要注意:

  1. 模块的输入是什么?
  2. 模块的输出是什么?
  3. 输入输出是如何对应的?

在我们使用verilog 进行编程时,就是写好很多基本的module,然后描述他们的连接方式,从而组合成为一个系统。那么一个module需要什么呢?主要分为以下五个部分:

  1. 模块名 (让开发者快速了解模块的功能)
  2. 端口定义
  3. I/O口说明
  4. 内部信号的声明
  5. 模块的功能实现 (主要是代码部分)
//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 模块功能的实现

在一个模块功能的实现方法中,通常有三种类型:

  1. 用assign声明语句

    用于驱动线网型的变量,声明语句右边的变量是敏感信号
    右边的值发生变化时,立刻计算左边的结果,进行表达。
    当输入变化时,输出也随之变化。属于组合逻辑电路。

assign   a_not=~a;
assign   c=a&b;
  1. 采用实例化的原件

    采用IP核的形式实现。

  2. 采用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 语句显示结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值