FPGA与Verilog入门

FPGA  ·  2025-08-25

个人理解CPU与FPGA的区别

  • CPU 可以看作是一个功能固定、专门优化过的专用逻辑电路,而 FPGA 是一个可以重新配置的逻辑电路平台。理论上你可以用 FPGA 搭一个 CPU
  • 以计算加法为例:

    • CPU:在计算过程中,厂商已经帮你造好了通用的“硬件计算器”,你用汇编指令告诉它怎么用。

      add:
          mov eax, [esp+4] ; 取第一个参数 a
          add eax, [esp+8] ; 加第二个参数 b
          ret              ; 返回结果(在eax中)
  • FPGA:你自己用逻辑搭出一个“硬件计算器”,然后数据用二进制电平进去就能直接出结果。

    module adder (
        input  [3:0] a,   // 输入a,4位宽
        input  [3:0] b,   // 输入b,4位宽
        output [4:0] sum  // 输出sum,5位宽
    );
        assign sum = a + b;
    endmodule
  • 也就是说CPU有专门的计算单元,相当于封装好的FPGA硬件电路计算器,就是 ALU(算术逻辑单元),汇编指令将数据放进去,内部把数据翻译成二进制实现和FPGA一样的门电路运算,写回寄存器就能拿到结果。
  • FPGA是生成CPU内部那个硬件电路计算器、算术逻辑单元去计算数据,写的 Verilog/VHDL 会被综合器翻译成你需要的硬件电路(可能是加法器、乘法器、滤波器等等),这些电路是用 LUT、触发器、布线等资源“搭”出来的,编译完成后,这些电路就像 CPU 里的 ALU 一样是固定的硬件,但它是你自己定义的数据也用电平组成二进制表示

数学在硬件上的描述

  • 另一种理解2的n次方方式,当不为0的时候。把2当作电平的高低变化,即1条线有两种状态

那么2的1次方就是2,一条线有两种变化

2的2次方就是4种变化关系

线10101
线20011

2的3次方就是8种变化关系

线101001011
线200101101
线300010111

通过这个硬件线电平变化关系,我们也能看出来2的平方变化,不禁让人感叹数学和硬件的奇妙

数字与运算符

在Verilog 中的数字表示方式,最常用的格式是:<位宽>’<基数><数值>,如4’b1011

  • 位宽:描述常量所含位数的十进制整数,是可选项。例如4’b1011中的4就是位宽,通俗理解
    就是4根线。如果没有这一项可以通过常量的值进行推断。例如’b1011可知位宽是4,而’b10010可
    推断出位宽为5。
  • 基数:表示数值是多少进制。可以是b,B,d,D,o,O,h或者H,分别表示二进制、十进制、
    八进制和十六进制。如果没有此项,则缺省默认为十进制数。例如,二进制的4’b1011可以写成十进
    制的4’d11,也可以写成十六进制的4’hb或者八进制的4’o13,还可以不写基数直接写成11。综上所
    述,只要二进数相同,无论写成十进制、八进制和十六进制都是同样的数字。
  • 数值:是由基数所决定的表示常量真实值的一串ASCII码。如果基数定义为 b或B,数值可以
    是0,1,x,X,z或Z。如果基数定义为 o或O,数值可以是2,3,4,5,6,7。如果基数定义为
    h 或H,数值可以是8,9,a,b,c,d,e,f,A,B,C,D,E,F。对于基数为d或者D的情况,
    数值符可以是任意的十进制数:0到9,但不可以是x或z。例如,4’b12是错误的,因为b表示二进
    制,数值只能是0、1、x或者z,不包含2。32’h12等同于32’h00000012,即数值未写完整时,高
    位补0。
二进制
4'b1011   // 4位宽,二进制,数值为 1011(十进制11)
'b10010   // 自动推断为5位宽,二进制,数值为 10010(十进制18)
8'b00001111 // 8位宽,二进制,数值为 00001111(十进制15)

十进制
4'd11     // 4位宽,十进制,数值为 11(即二进制1011)
'd25      // 自动推断所需位宽,十进制,数值为25(二进制11001,5位)

十六进制
8'hFF     // 8位宽,十六进制,数值为 FF(十进制255,二进制11111111)
4'hB      // 4位宽,十六进制,数值为 B(十进制11,二进制1011)
16'h1A2F  // 16位宽,十六进制,数值为 1A2F

15        // 默认十进制,数值为15

Verilog简介

Verilog是硬件描述语言,顾名思义,就是用代码的形式描述硬件的功能,最终在硬件电路上实现该功能。在Verilog描述出硬件功能后需要使用综合器对Verilog代码进行解释并将代码转化成实际的电路来表示,最终产生实际的电路,也被称为网表。这种将Verilog代码转成网表的工具就是综合器。

​ 这个综合器就是各家FPGA给出的 开发工具软件,比如ISEVIVADO

Verilog模块

Verilog模块例程

​ 模块(module)是Verilog 的基本描述单位,是用于描述某个设计的功能或结构及与其他模块通信的外部端口。

​ 模块有五个主要部分:端口定义、参数定义(可选)、 I/O说明、内部信号声明、功能定义。 模块总是以关键词module开始,以关键词endmodule结尾。它的一般语法结构如下所示:

module moduleName (
  clk,
  RST_N,
  DOUT,
  INPUT_IO,
  A,
  B
);

  parameter Data_W = 7;

  input clk;
  input RST_N;        
  output [Data_W:0] DOUT;
  input [Data_W:0] INPUT_IO;

  reg A;
  reg [Data_W:0] dout;

  wire [Data_W:0] INPUT_cnt;
  assign DOUT = dout;

  always @(posedge clk or negedge RST_N) begin
    if (!RST_N) begin
      dout <= 0;
    end else begin
      dout <= dout + 1;
    end

  always @( A or B ) begin
      A = 0
    end


    endmodule

Verilog例程分析

模块

module moduleName (
  clk,
  RST_N,
  DOUT,
  INPUT_IO,
  A,
  B
);

如上述代码所示,模块是以module开始,以endmodule结束。模块名是模块唯一的标识符,一般建议模块名 尽量用能够描述其功能的名字来命名,并且模块名和文件名相同。 模块的端口表示的是模块的输入和输出口名,也是其与其他模块联系端口的标识。

参数

parameter Data_W = 7;

参数定义是将常量用符号代替以增加代码可读性和可修改性。这是一个可选择的语句, 用不到的情况下可以省略

接口定义

  input clk;
  input RST_N;        
  output [Data_W:0] DOUT;
  input [Data_W:0] INPUT_IO;

模块的端口可以是输入端口、输出端口或双向端口

信号类型

  reg A;
  reg [Data_W:0] dout;

  wire [Data_W:0] INPUT_cnt;

reg是最常用的寄存器类型,寄存器类型通常用于对存储单元的描述。线网类型用于对结构化器件之间的物理连线的建模,如器件的管脚,芯片内部器件如与门的输出等。

​ 实际上,reg表示一个存储单元,用来储存数据。wire表示连线,用来将module内部的接线端口连接到外部。

regwire类型定义语法如下

- reg A;                                        // A为1位寄存器
  • reg [Data_W:0] dout; // dout为7位寄存器
  • wire [Data_W:0] INPUT_cnt; // INPUT_cnt为8线型信号

程序语句

assign

assign语句是连续赋值语句,一般是将一个变量的值不间断地赋值给另一变量,两个变量之间
就类似于被导线连在了一起,习惯上当做连线用。

assign 语句的功能属于组合逻辑的范畴,应用范围可以概括为一下几点

  • 持续赋值
  • 连线
  • wire型变量赋值,wire是线网,相当于实际的连接线,如果要用assign直接连接,就用wire 型变量,wire型变量的值随时发生变化。

[!tip]

比如wire,通常用assign进 行赋值,如assign A = B^C

always

always 语句是条件循环语句,执行机制是通过对一个称为敏感变量表的事件驱动来实现的。always 是“一直、总是”的意思,@后面跟着事件。整个always的意思是:当敏感事件的条件 满足时,就执行一次“程序语句”。敏感事件每满足一次,就执行“程序语句”一次。

[!TIP]

需要注意,组合逻辑器件的赋值采用阻塞赋值=,时序逻辑器件的赋值语句采用非阻塞赋值<=

always @(敏感事件)begin
    程序语句
end
always @(posedge clk or negedge RST_N) begin
    if (!RST_N) begin
      dout <= 0;
    end else begin
      dout <= dout + 1;
end

always @( A or B ) begin
      A = 0
end
    
always @( * ) begin
    if (!A) begin
      B <= 0;
    end
end

​ 第一段always的意思是当clk时钟变化,或者RST_N复位信号产生时,检测如果RST_N复位信号等于低电平(代表用户按下复位按键),执行一些操作。

​ 第二段always的意思是当A或者B信号变化,就让A等于0

​ 第三段always的意思的前面定义的信号任一发生变化,都会检测A信号是否变化,若变化将B置0

if与case
  • if语句和case语句是Verilog里两个非常重要的语句,if和case语句有一定的相关性,也有一定的区别。相同的地方在于两者几乎可以实现一样的功能。
  • 不同的是,if语句每个分支之间是有优先级的,综合得到的电路是类似级联的结构。case语句每个分支是平等的,综合得到的电路则是一个多路选择器。因此,多个if else-if语句综合得到的逻辑电路延时有可能会比case语句稍大。
if
module if_case(
    input [1:0] en,
    input [1:0] a,
    output reg q
 );
 always@(*)begin
    if(en[0]==1)
        q=a[0];
    else if(en[1]==1)
        q=a[1];
    else
        q = 0;
 end

 endmodule
  • 其综合后的RTL视图如下所示

image-20250825164849646.png

​ 从上图中所示的RTL图可以看到,此电路中含有两个二选一多路选择器,并且右边的优先级要高于左边(因为q的值是直接与右边的二选一选择器连接),当en[0]不为1时才会继续判断en[1]。 也就是说,在if语句下综合出的电路含有优先级。

case
 always @(*)begin
    case(en)
        2'b01: q = a[0];
        2'b10: q = a[1];
        default:q = 0;
    endcase
 end
  • 其综合后的RTL视图如下所示:

image-20250825165008971.png

​ 可以看出,使用case语句编写的逻辑代码综合出的电路是并行的,没有优先级,不影响电路的运行速度。

所以

  • If 语句具有优先级,当if下的条件不满足时才执行else后面的部分。
  • 而case语句是并行的,没有优先级,这在两者综合出来的RTL视图中可以明显的观察出来。
  • 但由于现在的仿真和综合工具已经足够强大,最后综合后的结果if..else...与case...语句其实并无不同,只不过是两种不同的实现方式而已,因此基本上不用考虑这两者间的区别。在不影响功能的前提下设计师不需要做局部的优化工作, 例如不需要考虑if/case语句的资源耗费差异、不需要考虑优化电路。只有在影响功能(即时序约束报错)的前提下,根据提示对电路进行优化。

时序逻辑

:warning:需要注意,组合逻辑器件的赋值采用阻塞赋值=,时序逻辑器件的赋值语句采用非阻塞赋值<=
同步/异步复位的时序逻辑
  • 时序逻辑的代码一般有两种:同步复位的时序逻辑和异步复位的时序逻辑。
  • 在同步复位的时序逻辑中复位不是立即有效,而在时钟上升沿时复位才有效。其代码结构如下:
always@(posedge clk) begin 
    if(rst_n==1’b0) 
        代码语句; 
    else begin 
        代码语句; 
    end 
end 
  • 在异步复位的时序逻辑中复位立即有效,与时钟无关。其代码结构如下:
always@(posedge clk or negedge rst_n) begin 
    if(rst_n==1’b0) 
        代码语句; 
    else begin 
        代码语句; 
    end 
end

D触发器

  • 数字电路中介绍了多种触发器,如JK触发器、D触发器、RS触发器、T触发器等。在FPGA中使用的是最简单的触发器——D触发器。
D触发器结构

image-20250825180215773.png

  • 可以将D触发器视为一个芯片,该芯片拥有4个管脚:
    时钟clk、复位rst_n、输入信号d,输出管脚q。
  • 该芯片的功能如下:

    • 当给管脚rst_n给低电平(复位有效),即赋值为0时,输出管脚q处于低电平状态。
    • 如果管脚rst_n为高电平,则观察管脚clk的状态,当clk信号由0变1即处于上升沿的时候,将此时d的值赋给q。若d是低电平,则q也是低电平;若d是高电平,则q也是高电平。
D触发器波形与代码

image-20250825180449968.png

always @(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
        q <= 0;
    end
    else begin
        q <= d;
    end
end
  • 从语法上分析该段代码的功能为:

    • 在“时钟clk上升沿或者复位rst_n下降沿”的 时候执行一次。
    • 具体执行方式如下:

        1. 如果复位rst_n=0,则q的值为0;
        1. 如果复位rst_n=1,则将d的值赋给q
评论
LJ` Blog . All Rights Reserved. Theme Jasmine by Kent Liao.
冀ICP备2025127925号 冀公网安备13082402000074号