本文为 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 语言中的大括号替换为 begin
和 end
。
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 中的一个重要的概念,需要我们多编程才能理解透彻。