FPGA——Verilog 基础

本文为 FPGA 学习总结,欢迎分享交流。

运行环境

  • windows10
  • Vivado 2018.3
  • Modelsim 10.7

FPGA 技术背景

FPGA 在加速算法领域很热门,C/C++ 仅在算法领域较为高效。FPGA 可以并行执行,理论上,只要 FPGA 资源多,可以同时处理任意多条指令及任务。但 FPGA 在低速控制逻辑领域的使用则较为困难。

FPGA 逻辑控制有两种方法,一种是状态机设计,另一种是可以在 FPGA 中运行 CPU。

我们来看一小段代码:

u32sum(a,b){
	a = a + 1;
	b = b + 1;
	c = a + b;
	return c;
}
alway@(posedgeclk)begin
	a = a + 1
	b = b + 1
	c = a + b
end

在 C 语言中需要逐条执行指令,读一条执行一条,耗费的 CPU 周期较长;而在 Verilog 中,以上指令在一个时钟上升沿便可以全部被执行。

基础语法

Verilog 和 C 在语法上有很多相似的地方,有了 C 语言的基础,Verilog 看起来就并不陌生。

C 语言和 Verilog 的关键词和结构对比:

C Verilog
sub-function module, function, task
if-then-else if-then-else
Case Case
{,} begin, end
For For
While While
Break Disable
Define Define
Int Int
Printf monitor, display, strobe

C 语言和 Verilog 运算符用法完全相同。

关键词

module

module
…
endmodule

代表一个模块,代码写在这个两个关键字中间。

input output

  • input:定义模块的输入信号;

  • output:定义模块的输出信号,比如 output[3:0]Led 是一组输出信号。其中 [3:0] 表示 0~3 共 4 路信号。

  • inout:模块输入输出双向信号。

  • wire:导线,在 FPGA 中以连线的方式实现。

  • reg:寄存器,在 always 中可被赋值,经常用于时序逻辑。比如 reg[3:0]Led 表示了一组寄存器。

always

always@() 括号里面是敏感信号。敏感信号触发时,就执行一次 always 中的语句。

assign

assign 可以理解为导线,即硬的电线。

if…else…

仅将 C 语言中的大括号替换为 beginend

case…endcase

case…endcase 作用域用于状态机的编写。

begin…and

begin … end 作用域范围,类似于 C 的大括号

parameter

parameter 为定义常量和符号。

Verilog 中数值表示的方式

如果我们要表示一个十进制是 180 的数值,在 Verilog 中的表示方法如下:

二进制:8’b1011_0100; // 其中“_”是为了容易观察位数,可有可无。

十进制:8’d180;

16 进制:8’HB4;

根据实际情况选择进制。

阻塞赋值和非阻塞赋值详解

阻塞赋值和非阻塞赋值是 FPGA 中很重要的一个概念。首先来说非阻塞赋值:

reg A;
reg B;
always @(posedge clk)
begin
	A <= 1'b1; //在 FPGA 中这两行代码会同时输出
	B <= 1'b1;
	/***或者**
	B <= 1'b1;
	A <= 1'b1;
	*********/
end

当把两条语句拆成两个 always,也是同时输出:

reg A;
reg B;
always @(posedge clk)
begin
	A <= 1'b1;
end
always @(posedge clk)
begin
	B <= 1'b1;
end

但如果将 A 赋值改为阻塞赋值,B 改为非阻塞赋值,A 赋值语句会早于 B 在 FPGA 中表示出来,但在时序逻辑中表示出来还是同时输出的。

always @(posedge clk)
begin
	A = 1'b1;
	B <= 1'b1;
end

例程

在时序逻辑中要求优先使用非阻塞赋值,不推荐将两个写在一起。接下来我们软件仿真讲解这个重要的概念。

// tb_test.v
`timescale 1ns / 1ps

module tb_test();

reg clk_i;
reg rst_n_i;
wire[4:0]result1_o,result2_o;

// 模块的调用
 unblock unblcok_inst
(
    .clk_i(clk_i),
    .rst_n_i(rst_n_i),
    .result_o(result1_o)
 );
 
 block blcok_inst
(
    .clk_i(clk_i),
    .rst_n_i(rst_n_i),
    .result_o(result2_o)
 );    
    // 复位初始化
    initial begin
     clk_i =0;
     rst_n_i =0;
     #20; // 延时20个时间单位,这里单位时间为纳秒
     rst_n_i =1;
     #20; 
   	end

    always #10 clk_i = ~clk_i; // 产生50MHz的时钟
endmodule

// unblock.v
`timescale 1ns / 1ps
module unblock
(
input clk_i,
input rst_n_i,
output reg [4:0]result_o
    );
    reg [3:0]A;
    reg [3:0]B;
    reg [4:0]C;
 always @(posedge clk_i )
    if(!rst_n_i) // 复位,信号初始化
    begin
    #2 
     A <= 4'd4; // 十进制表示方法
     B <= 4'd12;
     C <= 5'd0;
     result_o = 5'd0;
    end 
    else  begin
    #2 
     C <= A + B;
        result_o <= (C >> 1); // 此时C为0,下个时钟才为16
    end 
endmodule

// block.v
module block(
input clk_i,
input rst_n_i,
output reg [4:0]result_o
    );
    reg [3:0]A;
    reg [3:0]B;
    reg [4:0]C;

always @(posedge clk_i)
    if(!rst_n_i)
    begin
     #2   A = 4'd4;
     #0.2 B = 4'd12;
     #0.2 C = 5'd0;
     #0.2 result_o = 5'd0;
    end 
    else  begin
    #2   C = A + B;
        #0.2 result_o = (C >> 1); // 此时C为16
    end
 
endmodule

我们点击左侧的 SIMULATION 执行代码,得到结果:

在这里插入图片描述

我们可以看到,在复位完成的第一个时钟时,阻塞赋值已经完成,而非阻塞赋值晚一个周期。我们通过调试进一步理解。

首先在 block.v 和 unblock.v 中的 always 前都加断点,并点击 Restart 再点击 Run for 10ms,此时为阻塞赋值,将 Objects 窗口中的 A、B、C 变量加入窗口。再点击 Run for 10ms,此时为非阻塞赋值,将 Objects 窗口中的 A、B、C 变量加入窗口,然后删除断点。继续点击 Run for 10ms,右键窗口中的变量,点击 Radix->Unsigned Decimal 显示为十进制方便查看。

在这里插入图片描述

对于非阻塞赋值,C 先等于 16,然后 result 在下一个时钟才会被赋值为 8;而在阻塞赋值中,C 的赋值和 result 赋值是同时产生的。因此非阻塞赋值更方便时序逻辑的控制,可以设置同步性,而阻塞赋值不小心就会出错。

阻塞赋值和非阻塞赋值是 FPGA 中的一个重要的概念,需要我们多编程才能理解透彻。

猜你喜欢

转载自blog.csdn.net/weixin_44413191/article/details/107516326