FPGA

assign 关键字用于对线网(wire)或变量(var)进行连续赋值。连续赋值意味着一旦右侧的表达式发生变化,赋值就会立即更新左侧的值,这与过程赋值(在always块中)不同,后者在某种事件或条件发生时才更新值。

assign temp2 = {32{1’b0}}; 是什么意思?

在Verilog中,assign temp2 = {32{1'b0}}; 这行代码声明了一个连续赋值,将 temp2 这个线网的值设置为一个32位的全0值。

这里的 {32{1'b0}} 是一个重复拼接操作,含义如下:

  • 1'b0 是一个二进制数,表示一个位宽为1的数值,值为0。
  • {32{1'b0}} 表示将 1'b0 这个值重复32次。
Verilog 数据类型

Verilog 最常用的 2 种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两种数据类型的扩展或辅助。

整数(integer) reg 型变量为无符号数,而 integer 型变量为有符号数

实数(real)

在Verilog中,realinteger 是数据类型关键字,分别用于声明实数类型和整数类型的变量。

txt
real data1;
integer temp;
initial begin
    data1 = 2e3;
    data1 = 3.75;
end

initial begin
    temp = data1; //temp 值的大小为3
end

这段代码包含两个 initial 块,它们在仿真开始时执行一次。

第一个 initial 块中:

  1. data1 被初始化为实数类型 real
  2. data1 被赋值为 2e3,这意味着 data1 现在的值是2000.0。
  3. 随后,data1 被更新为 3.75

第二个 initial 块中:

  1. temp 被初始化为整数类型 integer
  2. temp 被赋值为 data1 的值。由于 data1 当前是 3.75,这个赋值会将实数转换为整数。在Verilog中,实数赋值给整数时,会进行取整操作,保留数值的整数部分,忽略小数部分。因此,temp 的值将是3。

需要注意的是,您的注释 //temp 值的大小为3 是正确的,因为 data1 的值 3.75 在赋值给 temp 时会被取整为3。

时间(time)

Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。例如:

txt
time current_time;
initial begin
    #100;
    current_time = $time; //current_time 的大小为 100
end

这段代码包含一个 initial 块,它在仿真开始时执行一次。

initial 块中:

  1. current_time 被初始化为时间类型 time
  2. #100; 是一个延迟语句,它会使仿真暂停100个时间单位。在Verilog中,# 后面跟一个数字表示延迟的时间量。

数组

存储器

参数 参数用来表示常量,用关键字 parameter 声明,只能赋值一次

parameter data_width = 10’d32 ;

字符串

字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。

字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。例如,为存储字符串 “run.runoob.com”, 需要 14*8bit 的存储单元:

reg [0: 14*8-1] str ;
initial begin
str = “run.runoob.com”;
end

IMG_20240424_115210

IMG_20240424_115158

IMG_20240424_115542

IMG_20240424_115525

IMG_20240424_115506

2.4 Verilog 表达式

表达式由操作符和操作数构成,其目的是根据操作符的意义得到一个计算结果。

a^b ; //a与b进行异或操作
address[9:0] + 10’b1 ; //地址累加
flag1 && flag2 ; //逻辑与操作

always块里赋值对象不能是wire型

同类型操作符之间,除条件操作符从右往左关联,其余操作符都是自左向右关联。圆括号内表达式优先执行

txt
//自右向左关联,两种写法等价
A+B-C ;
(A+B)-C ;

//自右向左关联,两种写法等价,结果为 B、D 或 F
A ? B : C ? D : F ;
A ? B : (C ? D : F) ;

求幂(**)、取模(%)

txt
b = 4'b100x;

x 是一个表示未知或不可确定状态的字符。它用于在仿真中表示一个位的值是未知的,这通常发生在综合过程中,当某些逻辑路径没有被明确赋值时,或者在设计中的某些部分还没有完全定义时。

无符号数乘法时,结果变量位宽应该为 2 个操作数位宽之和

reg [3:0] mula ;
reg [1:0] mulb;
reg [5:0] res ;
mula = 4’he ;
mulb = 2’h3 ;
res = mula * mulb ; //结果为res=6’h2a, 数据结果没有丢失位数

逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)

按位操作符包括:取反(),与(&),或(|),异或(^),同或(^)

按位操作符对 2 个操作数的每 1bit 数据进行按位操作,如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。

归约操作符

归约操作符包括:归约与(&),归约与非(&),归约或(|),归约或非(|),归约异或(^),归约同或(~^)。

归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。

逻辑操作符、按位操作符和归约操作符都使用相同的符号表示,因此有时候容易混淆。区分这些操作符的关键是分清操作数的数目,和计算结果的规则。

txt
A = 4'b1010 ;
&A ;      //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1
~|A ;     //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0
^A ;      //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0

移位操作符

移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。

移位操作符是双目操作符,两个操作数分别表示要进行移位的向量信号(操作符左侧)与移动的位数(操作符右侧)。

算术左移和逻辑左移时,右边低位会补 0。

逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。

实例

A = 4’b1100 ;
B = 4’b0010 ;
A = A >> 2 ; //结果为 4’b0011
A = A << 1; *//结果为 4’b1000*
A = A <<< 1 ; *//结果为 4’b1000*
C = B + (A>>>2); //结果为 2 + (-4/4) = 1, 4’b0001

define, undef

在编译阶段,`define 用于文本替换,类似于 C 语言中的 #define

`undef 用来取消之前的宏定义

txt
`ifdef       MCU51
    parameter DATA_DW = 8   ;
`elsif       WINDOW
    parameter DATA_DW = 64  ;
`else
    parameter DATA_DW = 32  ;
`endif

`include

使用 `include 可以在编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中,作用类似于 C 语言中的 #include 结构。

timescale

在 Verilog 模型中,时延有具体的单位时间表述,并用 `timescale 编译指令将时间单位与实际时间相关联。

该指令用于定义时延、仿真的单位和精度,格式为:

txt
`timescale      time_unit / time_precision

time_unit 表示时间单位,time_precision 表示时间精度,它们均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成。时间精度可以和时间单位一样,但是时间精度大小不能超过时间单位大小,例如下面例子中,输出端 Z 会延迟 5.21ns 输出 A&B 的结果。

实例

timescale 1ns/100ps *//时间单位为1ns,精度为100ps,合法* *//timescale 100ps/1ns //不合法*
module AndFunc(Z, A, B);
output Z;
input A, B ;
assign #5.207 Z = A & B
endmodule

在编译过程中,timescale 指令会影响后面所有模块中的时延值,直至遇到另一个 timescale 指令或 `resetall 指令。

由于在 Verilog 中没有默认的 timescale,如果没有指定 timescale,Verilog 模块就有会继承前面编译模块的 `timescale 参数。有可能导致设计出错。

如果一个设计中的多个模块都带有 `timescale 时,模拟器总是定位在所有模块的最小时延精度上,并且所有时延都相应地换算为最小时延精度

`default_nettype

该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型。

txt
`default_nettype wand 

该实例定义的缺省的线网为线与类型。因此,如果在此指令后面的任何模块中的连线没有说明,那么该线网被假定为线与类型。

txt
`default_nettype none

该实例定义后,将不再自动产生 wire 型变量。

celldefine, endcelldefine

这两个程序指令用于将模块标记为单元模块,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。

实例

celldefine **module** ( **input** clk, **input** rst, **output** clk_pll, **output** flag); …… **endmodule** endcelldefine

unconnected_drive, nounconnected_drive

在模块实例化中,出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态。

txt
assign

用于对 wire 型变量进行赋值,不对寄存器赋值

进位输出(Carry out,通常表示为Co或Cout)是全加器的一个输出,它表示在两个二进制位相加时是否产生了进位。在二进制加法中,当两个加数位(A和B)的和大于或等于2时,就会产生进位,因为二进制中的每一位只能表示0或1。进位输出就是用来表示这个进位的。

module full_adder1(
input Ai, Bi, Ci,
output So, Co);

assign So = Ai ^ Bi ^ Ci ;
assign Co = (Ai & Bi) | (Ci & (Ai | Bi));
endmodule

更简单的:

txt
module full_adder1(
    input Ai, Bi, Ci,
    output So, Co);
    
    assign {Co, So} = Ai + Bi + Ci;
endmodule

//普通时延,A&B计算结果延时10个时间单位赋值给Z
wire Z, A, B ;
assign #10 Z = A & B ;

//隐式时延,声明一个wire型变量时对其进行包含一定时延的连续赋值。
wire A, B;
wire #10 Z = A & B;

//声明时延,声明一个wire型变量是指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z ;
assign Z =A & B

Verilog 过程结构

过程结构语句有 2 种,initial 与 always 语句

一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。

但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。

initial语句

initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。

如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。

如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。

initial 理论上来讲是不可综合的,多用于初始化、信号检测等。

这些语句在模块间并行执行,与其在模块的前后顺序没有关系

always 语句

与 initial 语句相反,always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。

由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。

parameter 关键字用于定义模块的参数。参数是一种可以在模块实例化时或在模块内部使用,但不一定要在模块的所有复制中传递的常数。简单地说,参数类似于函数或算法中的变量,它们在模块的复制品之间共享。

连续性赋值使用assign语句,而过程性赋值使用always块。

阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。

阻塞赋值语句使用等号 = 作为赋值符。

非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。

非阻塞赋值语句使用小于等于号 <= 作为赋值符。

如下所示,2 个 always 块中语句并行执行,赋值操作右端操作数使用的是上一个时钟周期的旧值,此时 a<=b 与 b<=a 就可以相互不干扰的执行,达到交换寄存器值的目的。

实例

always @(posedge clk) begin
a <= b ;
end

always @(posedge clk) begin
b <= a;
end

Verilog 时序控制

Verilog 提供了 2 大类时序控制方法:时延控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制

时延控制:根据在表达式中的位置差异,时延控制又可以分为常规时延与内嵌时延。

常规时延

txt
reg  value_test ;
reg  value_general ;
#10  value_general    = value_test ;

或:

txt
#10 ;
value_ single         = value_test ;

内嵌时延

遇到内嵌延时时,该语句先将计算结果保存,然后等待一定的时间后赋值给目标信号。

内嵌时延控制加在赋值号之后。例如:

txt
reg  value_test ;
reg  value_embed ;
value_embed        = #10 value_test ;

需要说明的是,这 2 种时延控制方式的效果是有所不同的。

当延时语句的赋值符号右端是常量时,2 种时延控制都能达到相同的延时赋值效果。

当延时语句的赋值符号右端是变量时,2 种时延控制可能会产生不同的延时赋值效果。

边沿触发事件控制

在 Verilog 中,事件是指某一个 reg 或 wire 型变量发生了值的变化。事件控制用符号 @ 表示。

设计:根据需求编写硬件描述语言(如Verilog或VHDL)代码来描述设计的功能和行为

synthesize综合,合成:综合代码,检查语法是否有错误,将高级的逻辑描述代码转换为逻辑门级别的网表或等效的门级电路

FloorPlanner 是 FPGA 设计流程中的一个重要工具,用于执行布局(Place)阶段的子任务,即对设计中的逻辑电路进行布局安置。在 FPGA 设计流程中,FloorPlanner 的地位如下:

  1. 布局规划:FloorPlanner 负责规划 FPGA 芯片上各个逻辑模块的布局位置,以最大程度地满足设计的性能和资源利用率要求。它会考虑逻辑模块之间的布线延迟、信号传输路径长度等因素,以优化整体的布局结构。
  2. 资源分配:FloorPlanner 还负责将设计中的逻辑模块分配到 FPGA 芯片的不同区域,并且合理利用芯片上的资源(如片上存储器、DSP模块等),以满足设计对资源的需求。
  3. 时序约束:在布局过程中,FloorPlanner 还会考虑时序约束,确保设计中的时序要求能够得到满足。它会尽可能减少逻辑模块之间的传输延迟,以确保时序性能。
  4. 优化布局:FloorPlanner 通过对设计进行优化布局,以降低布线延迟、减少时序问题和功耗等方面的优化。这可以提高设计的性能、可靠性和功耗效率。

在 FPGA 设计流程中,FloorPlanner 位于布局(Place)阶段之前,它为后续的布线(Route)阶段提供了优化的布局结果,从而帮助实现设计的最终映射和部署。

Place&Route

开发流程中的 Place & Route 是指在将设计映射到 FPGA 芯片时的一个重要步骤。下面解释一下它的含义和作用:

  1. Place(放置):Place 指的是将设计中的逻辑元素(如逻辑门、寄存器等)放置到 FPGA 芯片的物理位置上。这一步骤考虑了芯片内部的布局和连接资源,以尽可能地优化性能和资源利用率。放置的目标是最小化延迟、最大化时序性能,并且尽量减少芯片内的布线冲突。
  2. Route(布线):Route 是指将设计中的逻辑元素之间的连接关系转化为芯片内部的实际物理连线。这一步骤考虑了芯片内部的连线资源、信号传输延迟等因素,以确保逻辑元素之间的连接能够有效地建立并满足时序要求。布线的目标是尽可能地降低信号传输延迟、最小化信号干扰,同时满足设计的时序约束。

在 Verilog 中,reg 类型通常用于表示存储元素(如寄存器),而不是直接连接到模块的输出端口。输出端口通常使用 outputinout 声明,并且通常需要与 wire 类型一起使用。

reg 类型在 Verilog 中表示的是寄存器类型,它在 always 块中使用,存储状态或信号。而 output 端口应该使用 wire 类型来表示,因为它们不会存储状态,只是将信号传递给其他部件。

因此,你在模块顶层中使用 output reg 是不符合常规的 Verilog 设计习惯的,通常应该使用 output wire

image-20240427142422285

半加器(Half Adder)和全加器(Full Adder)是数字电路中用于执行二进制加法的基本组件。它们的主要区别在于它们处理的输入数量和功能。

半加器: 半加器是一个组合逻辑电路,它接受两个二进制位作为输入,并产生两个输出:和(Sum)和进位(Carry)。半加器只处理两个输入位的加法,不考虑来自较低位的进位。半加器的输出进位只能表示当前两个输入位相加是否产生了进位。

半加器的逻辑可以表示为:

  • 和(Sum) = A XOR B

  • 进位(Carry) = A AND B

其中,A和B是两个输入位,XOR表示异或门,AND表示与门。

全加器: 全加器也是一个组合逻辑电路,它接受三个二进制位作为输入,并产生两个输出:和(Sum)和进位(Carry)。全加器的三个输入包括两个加数位(A和B)以及来自较低位的进位(Carry-in)。全加器能够处理包括进位在内的三个位的加法。

全加器的逻辑可以表示为:

  • 和(Sum) = (A XOR B) XOR Carry-in

  • 进位(Carry) = (A AND B) OR (Carry-in AND (A XOR B))

其中,Carry-in是来自较低位的进位,OR表示或门。

区别:

输入数量:半加器有两个输入,全加器有三个输入。

功能:半加器只计算两个输入位的和和进位,而全加器计算三个输入位(包括来自较低位的进位)的和和进位。

应用:半加器通常用于构建更复杂的加法器电路,如全加器。全加器则用于实现多位二进制数的加法,因为它能够处理进位。

在实际的数字电路设计中,全加器更为常用,因为它可以级联(Cascade)起来构成多位加法器,如4位、8位、16位等,从而实现更复杂的算术运算。

在Verilog中,parameter是一个关键字,用于定义模块(module)的参数

parameter定义的值在模块的整个实例化过程中都是固定的,不能被重新赋值。这意味着一旦定义了parameter,它的值在整个模块中都是不变的。

Assignment 赋值

SP代表single port单端口模式,即同一时钟控制读写,同一时间只能写或者度

在您提供的 Verilog 代码片段中,ram_inst 是一个实例化语句,用于创建一个 RAM 模块的实例。这个实例使用了多个引脚(pins),这些引脚定义了模块的输入和输出接口。下面是每个引脚的详细解释:

  • .dout(dout_o):这是 RAM 模块的输出引脚,名为 dout,它是一个 8 位宽度的输出信号。在这个实例中,它被连接到了一个名为 dout_o 的信号。

  • .clk(clk):这是 RAM 模块的时钟输入引脚,用于同步数据写入和读取操作。它被连接到了一个名为 clk 的信号。

  • .oce(oce_i):这是 RAM 模块的输出使能输入引脚,用于控制 dout 信号是否输出数据。它被连接到了一个名为 oce_i 的信号。

  • .ce(ce_i):这是 RAM 模块的芯片使能输入引脚,用于控制 RAM 是否可以进行读写操作。它被连接到了一个名为 ce_i 的信号。

  • .reset(reset_i):这是 RAM 模块的复位输入引脚,用于将 RAM 恢复到初始状态。它被连接到了一个名为 reset_i 的信号。

  • .wre(wre_i):这是 RAM 模块的写使能输入引脚,用于控制是否可以写入数据到 RAM。它被连接到了一个名为 wre_i 的信号。

  • .ad(addr):这是 RAM 模块的地址输入引脚,用于指定要读取或写入的 RAM 单元的地址。它被连接到了一个名为 addr 的信号,该信号是 11 位宽度的。

  • .din(data_i):这是 RAM 模块的数据输入引脚,用于写入数据到 RAM。它被连接到了一个名为 data_i 的信号,该信号是 8 位宽度的。

每个引脚都是 RAM 模块与其外部接口之间的连接点,它们定义了模块如何与外部信号交互。在实际的设计中,您需要确保这些引脚被正确地连接到相应的信号,并且信号的类型和宽度与 RAM 模块的要求相匹配。

FPGA中BRAM和DRAM的区别

FPGA(现场可编程门阵列)中的BRAM(块RAM)和DRAM(动态RAM)是两种不同类型的存储器,它们在设计和使用上有着显著的区别:

  1. 类型和用途
  • BRAM:是静态RAM(SRAM)的一种形式,通常集成在FPGA芯片内部。它提供快速的存储解决方案,适用于需要高速、小容量存储的应用,如缓存、缓冲区或FPGA内部的数据存储。
  • DRAM:是一种动态RAM,与FPGA芯片外部连接。它具有更高的存储密度,但速度较BRAM慢。DRAM适用于需要大容量存储的应用,如图像处理、视频缓冲和大量数据存储。
  1. 存储机制
  • BRAM:作为静态RAM,它不需要刷新电路来维持数据。每个存储单元都使用六晶体管(6T)的SRAM细胞结构,这意味着它可以无限期地保持数据,直到被写入新数据。
  • DRAM:作为动态RAM,它需要定期刷新来维持数据。每个存储单元通常由一个电容器和一个晶体管组成,因此它的密度可以更高,但速度较慢,并且需要更复杂的控制逻辑。
  1. 性能特点
  • BRAM:提供单周期访问时间,这意味着访问数据几乎立即完成,适用于要求严格实时性能的应用。
  • DRAM:由于其刷新要求,访问速度较慢,通常需要多个时钟周期来访问数据。
  1. 集成度
  • BRAM:在FPGA芯片内部,与逻辑元素紧密集成,可以提供非常低的延迟访问。
  • DRAM:通常作为外部组件连接到FPGA,通过内存接口(如DDR)进行通信。
  1. 功耗
  • BRAM:由于其简单性和快速访问能力,通常功耗较低。
  • DRAM:由于需要刷新和复杂的控制逻辑,功耗通常更高。

在选择使用BRAM还是DRAM时,设计者需要根据应用需求、性能要求、成本考虑和功耗限制来做出决策。对于需要高速、小容量存储的应用,BRAM通常是更好的选择;而对于需要大容量存储的应用,DRAM可能是更合适的选择。

在FPGA中,BRAM(块RAM)可以被配置为单端口模式或双端口模式,这两种模式在数据访问方式上有所不同:

  1. 单端口模式
  • 在单端口模式下,BRAM有一个数据访问端口,即地址和数据线是共用的。
  • 在任何给定的时间,单端口BRAM只能进行一次读操作或写操作。如果在一个时钟周期内同时尝试进行读和写操作,通常会发生冲突,除非特定的FPGA具有特殊的管理机制。
  • 单端口模式适用于那些不需要同时进行读写操作的应用场景,或者那些可以接受顺序访问的应用场景。
  1. 双端口模式
  • 双端口模式允许BRAM同时通过两个独立的端口进行访问,每个端口都有自己的地址线、数据线和控制线。
  • 这意味着双端口BRAM可以在同一时钟周期内进行一次读操作和一次写操作,或者同时进行两次读操作,访问不同的地址。
  • 双端口模式适用于需要同时或并行访问存储器中不同位置的应用场景,如图像处理、缓存和乒乓缓冲等。

在某些FPGA中,BRAM还可以配置为更高级的端口模式,如四端口模式,这允许更多的并行访问。设计者根据具体应用的需求来选择最合适的端口模式,以优化性能和资源利用。

十六进制数系统中的每个数字代表4位二进制数

在Verilog中,defparam是一个编译器指令,用于在模块实例化时重定义参数的值。这条指令可以用来改变模块实例化时参数的默认值。

在Verilog中,localparam关键字用于声明一个模块内部的参数,这个参数在模块的整个作用域内都是常量。localparam声明的参数是不可变的,它们的值在编译时就已经确定了。