2. FPGA基础与Verilog语法速通
各位同学,欢迎来到第二章。这一章咱们要啃的,是FPGA开发的“地基”。说实话,很多初学者一上来就急着写代码,结果遇到时序问题、资源不够用,完全摸不着头脑。我当年也踩过这个坑——代码写得飞起,综合出来一堆错误,最后发现是对FPGA内部结构理解不够。
所以,这一章我们稳扎稳打。先搞清楚FPGA肚子里到底装了些什么,再聊聊并行计算到底是个什么鬼,最后把Verilog最核心的语法过一遍。嗯,准备好了吗?
2.1 FPGA内部结构:你手里的“乐高”积木
FPGA,说白了就是一块可以无限次重新配置的芯片。它不像ASIC那样流片后就焊死了,你可以反复擦写,像搭乐高一样搭建你的数字电路。
它的内部主要由这几大块构成:
- 可编程逻辑单元(Logic Cell / LE):这是最基础的积木块,通常由一个查找表(LUT)和一个触发器(FF)组成。LUT负责实现组合逻辑,FF负责存储状态。
- 查找表(LUT):你可以把它理解成一个“真值表生成器”。输入几个信号,输出一个结果。比如4输入LUT,就能实现任意4输入的组合逻辑。
- 触发器(Flip-Flop):用来寄存数据,是时序逻辑的核心。每个时钟沿,它把输入的数据锁存到输出。
- 块RAM(Block RAM):芯片内部的小型存储器,用来存数据、做FIFO、或者当ROM用。速度极快,但容量有限。
- 数字信号处理单元(DSP Slice):专门用来做乘法、加法、乘累加运算的硬核模块。做图像滤波、卷积运算时,全靠它提速。
- 可编程互连资源(Routing):这些密密麻麻的走线,把上面所有的模块连接起来。布线好不好,直接决定了你的设计能不能跑到目标频率。
- I/O引脚(IOB):芯片和外界通信的接口,支持各种电平标准(LVCMOS、LVDS等)。
核心理解:FPGA不是用“指令”在跑程序,而是用“硬件连线”在实现电路。你写的Verilog代码,最终会被映射成LUT里的真值表、FF的连线、以及DSP的配置。
为了让你更直观地理解,我画了一张结构图。你看一眼就明白了:
个人经验:我在做图像缩放项目时,一开始把行缓存数据全放在LUT里,结果资源爆了。后来换成块RAM,资源占用直接降了70%。记住:能用BRAM就别用LUT搭大存储器,这是血的教训。
2.2 并行计算概念:FPGA的“三头六臂”
CPU是串行执行的,一条指令接一条指令。FPGA不一样,它天生就是并行的。你想想看,你写了一个always块,里面处理像素A;再写另一个always块,同时处理像素B。这两个块在硬件上是同时运行的,互不干扰。
为什么会这样?因为FPGA里的每个逻辑单元、每个DSP、每个BRAM,都是独立的硬件资源。你写的代码,最终会映射成多个独立的硬件电路。这些电路可以同时工作,这就是并行。
举个例子,做3x3的卷积运算:
- CPU做法:循环9次,每次取一个像素,乘系数,累加。耗时9个时钟周期(假设单周期乘法)。
- FPGA做法:用9个乘法器同时算,1个时钟周期就出结果。
这就是FPGA在图像处理上的核心优势——流水线并行和数据并行。
关键点:并行不是“快”,而是“吞吐率高”。单个像素的处理延迟可能没变,但每秒钟能处理的像素数量,FPGA可以甩CPU几条街。
2.3 Verilog基础语法:模块、assign、always
好了,理论讲完,咱们开始写代码。Verilog语法其实不多,核心就三个东西:模块、assign、always。把这几个搞明白,你就能写出大部分电路了。
2.3.1 模块(Module)—— 你的“黑盒子”
模块是Verilog的基本单元。每个模块就是一个硬件电路,有输入、有输出、有内部逻辑。我习惯把模块想象成一个芯片:引脚(端口)在外面,电路在里面。
// 一个简单的D触发器模块
module d_flip_flop (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire d, // 数据输入
output reg q // 数据输出
);
// 时序逻辑:每个时钟上升沿,把d赋值给q
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0; // 复位时输出0
else
q <= d; // 正常工作时,跟随输入
end
endmodule
你看,模块名、端口列表、内部逻辑,清清楚楚。这就是一个完整的硬件描述。
2.3.2 assign语句—— 组合逻辑的“连线”
assign用来描述组合逻辑。说白了,就是一根导线,输入变了,输出立刻变,没有时钟参与。
// 组合逻辑:与门
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b; // 连续赋值,a和b一变,y立刻变
endmodule
assign的右边可以是复杂的表达式,比如加法、比较、移位。但记住:assign不能用来描述寄存器(reg)的赋值,那是always的事。
避坑指南:我曾经犯过一个错误,在assign里给reg类型赋值,结果综合报错。记住:assign只能驱动wire类型,always块里才能驱动reg类型。这是语法红线,别踩。
2.3.3 always语句—— 时序逻辑的“心脏”
always块是Verilog里最灵活、也最容易出错的地方。它既可以描述时序逻辑(带时钟),也可以描述组合逻辑(不带时钟)。
常见的写法有两种:
// 写法1:时序逻辑(带时钟和复位)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 8'd0;
else
counter <= counter + 1'b1;
end
// 写法2:组合逻辑(敏感列表里没有时钟)
always @(*) begin
if (sel)
mux_out = a;
else
mux_out = b;
end
注意看区别:
- 时序逻辑里,赋值用
<=(非阻塞赋值)。它表示“在时钟沿到来时,同时更新所有寄存器”。 - 组合逻辑里,赋值用
=(阻塞赋值)。它表示“按顺序执行,立即更新”。
这个区别非常重要。我见过太多新手把这两个搞混,导致仿真结果和实际电路不一致。
我的习惯:写时序逻辑时,一律用非阻塞赋值 <=。写组合逻辑时,一律用阻塞赋值 =。这个习惯帮我避免了很多莫名其妙的bug。
2.4 Testbench编写:给你的设计“体检”
写完了代码,怎么知道它对不对?靠仿真。Testbench就是用来给设计施加激励、观察输出的“测试平台”。
Testbench本身也是一个模块,但它没有输入输出端口。它内部实例化你的设计,然后产生时钟、复位、数据,最后打印结果。
// 测试上面的D触发器
module tb_d_flip_flop;
reg clk;
reg rst_n;
reg d;
wire q;
// 实例化被测试模块
d_flip_flop u_dff (
.clk (clk),
.rst_n(rst_n),
.d (d),
.q (q)
);
// 产生时钟:周期10ns
initial begin
clk = 0;
forever #5 clk = ~clk; // 每5ns翻转一次
end
// 产生激励
initial begin
// 复位
rst_n = 0;
d = 0;
#20;
rst_n = 1; // 释放复位
// 输入数据
#10 d = 1;
#10 d = 0;
#10 d = 1;
#10 d = 1;
#10 d = 0;
// 结束仿真
#50 $finish;
end
// 监控输出
initial begin
$monitor("Time=%0t, clk=%b, rst_n=%b, d=%b, q=%b", $time, clk, rst_n, d, q);
end
endmodule
你看,Testbench里用了initial块来产生激励。initial块只执行一次,适合做初始化。forever循环用来产生连续的时钟。
仿真要点:写Testbench时,一定要模拟真实场景。比如复位时间够不够长?数据变化是否跨时钟域?我见过有人仿真时复位只给了1ns,结果实际板子上复位不稳定,电路直接跑飞。
好了,这一章的内容就到这里。FPGA内部结构、并行计算、Verilog基础语法、Testbench编写,这四个点你吃透了,后面的课程就会轻松很多。记住:硬件思维和软件思维完全不同,多写代码、多仿真、多看看综合后的RTL视图,慢慢你就找到感觉了。
可可精品资料库