3. FPGA基础与Verilog入门:FPGA内部结构(LUT、FF、BRAM、DSP)、Verilog语法基础(模块、assign、always)、组合逻辑与时序逻辑
好,咱们正式开始啃FPGA这块硬骨头。很多初学者一上来就被各种术语吓住了,什么LUT、FF、BRAM,听着像天书。其实没那么玄乎。你想想看,FPGA说白了就是一大块可以随意配置的“数字积木”。你想搭什么电路,就用这些积木拼什么电路。
3.1 FPGA内部到底长啥样?
我第一次接触FPGA内部结构图时,感觉像在看一座城市的规划图。有街区(逻辑单元)、有仓库(BRAM)、有高速路(布线资源),还有专门的“数学运算加速器”(DSP)。咱们一个一个来看。
3.1.1 查找表(LUT)—— 最灵活的积木
LUT的全称是Look-Up Table,查找表。说白了,它就是一个“真值表”的硬件实现。比如你想实现一个与门:Y = A & B。传统数字电路需要搭晶体管,但在FPGA里,LUT直接把所有可能的输入组合对应的输出结果存起来。
一个4输入LUT,内部就是一个16x1的SRAM。输入A、B、C、D组合成4位地址,直接“查表”得到输出。我在项目中遇到过一个问题:用LUT实现复杂逻辑时,综合器会报“LUT输入不够”。这时候就得拆成多个LUT级联,或者换用更高级的器件。
3.1.2 触发器(FF)—— 记住状态的家伙
触发器(Flip-Flop)是时序逻辑的基石。它能在时钟沿到来时,把输入数据“锁住”并保持到下一个时钟沿。没有FF,你就没法做计数器、状态机、流水线这些好东西。
每个FPGA的逻辑单元(Slice)里,通常包含4-8个FF。我建议你养成一个好习惯:写代码时,组合逻辑和时序逻辑要分开写。别把FF和LUT混在一个always块里,否则后期调试会哭的。
cnt_reg、data_valid_d1。别用 reg1、reg2 这种名字,三个月后你自己都看不懂。
3.1.3 块RAM(BRAM)—— 片内存储利器
BRAM是FPGA内部的专用存储资源。一个BRAM通常是36Kb大小,可以配置成各种位宽和深度。比如你可以把它配成512x72、1Kx36、2Kx18等等。
为什么要用BRAM而不是LUT搭存储?因为BRAM是专用的硬核,面积小、速度快、功耗低。用LUT搭一个大FIFO,那资源消耗会让你怀疑人生。我曾经在一个项目中,为了省事用LUT搭了个4K深度的FIFO,结果逻辑资源直接爆了。后来换成BRAM,资源占用降了80%。
| 存储方式 | 容量 | 速度 | 资源消耗 | 适用场景 |
|---|---|---|---|---|
| LUT RAM(分布式) | 小(几十到几百bit) | 快 | 高 | 小FIFO、寄存器堆 |
| BRAM(块RAM) | 大(几十Kb) | 快 | 低 | 大FIFO、帧缓存、查找表 |
3.1.4 DSP单元—— 算数加速器
DSP(Digital Signal Processing)单元是专门做乘加运算的硬核。一个DSP48E2(Xilinx 7系列)可以完成25x18的乘法,然后累加。如果你要做FIR滤波器、FFT、矩阵乘法,DSP是绕不开的。
嗯,这里要注意:不要用LUT搭乘法器。除非你只做1位乘法,否则资源消耗和时序都会崩。我见过有人用Verilog的 * 运算符直接写乘法,综合器会把它映射到DSP上。但如果你写的是 a * b 且a和b都是变量,综合器会推断出DSP。如果a是常数,那综合器会用LUT+移位来实现,效率反而更高。
a * b 写了一个32位乘法,结果综合后告诉我DSP不够用。后来发现,其实可以用流水线拆成多个16位乘法,再拼接结果。这样既节省了DSP,又跑到了更高频率。
3.2 Verilog语法基础—— 从Hello World开始
Verilog是硬件描述语言,不是编程语言。这个区别很重要。你写的是“电路”,不是“程序”。
3.2.1 模块(Module)—— 电路的黑盒子
每个Verilog设计都以 module 开头,以 endmodule 结尾。模块定义了电路的输入输出端口和内部逻辑。
module led_controller (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg [3:0] led // 4位LED输出
);
// 内部逻辑写在这里
endmodule
我个人习惯把端口声明和内部信号分开写。端口用 input、output、inout 声明,内部信号用 wire、reg 声明。这样代码结构清晰,别人接手也容易看懂。
3.2.2 assign语句—— 组合逻辑的直通车
assign 用于描述组合逻辑。它相当于把右边的表达式“硬连线”到左边的信号上。只要右边输入变化,左边立刻更新。
assign led[0] = (counter > 100) ? 1'b1 : 1'b0;
assign led[1] = button_pressed & ~timer_done;
注意:assign 左边必须是 wire 类型,不能是 reg。这是新手最容易犯的错误。
3.2.3 always块—— 时序逻辑的根据地
always 块可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表。
- 组合逻辑:
always @(*)或always @(a or b or c) - 时序逻辑:
always @(posedge clk or negedge rst_n)
// 时序逻辑示例:带同步复位的计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 8'd0;
else if (counter == 8'd255)
counter <= 8'd0;
else
counter <= counter + 1'b1;
end
这里有个关键点:时序逻辑用 <=(非阻塞赋值),组合逻辑用 =(阻塞赋值)。混用的话,仿真结果和实际电路可能不一致。我见过一个同事,在always块里把 <= 和 = 混着用,结果仿真通过了,上板子就挂。查了两天才发现是赋值方式的问题。
3.3 组合逻辑 vs 时序逻辑—— 本质区别
这个问题我每次面试都会问。很多人背得滚瓜烂熟,但一写代码就乱套。
- 组合逻辑: 输出只取决于当前输入,没有记忆功能。比如加法器、多路选择器、译码器。
- 时序逻辑: 输出不仅取决于当前输入,还取决于之前的状态。比如计数器、状态机、寄存器。
你想想看,为什么要有时序逻辑?因为现实世界的信号是连续的,但数字电路只能在时钟沿采样。时序逻辑就是用来“记住”上一个时钟周期的状态。
3.4 本章知识体系图
下面这张图展示了FPGA内部结构、Verilog语法和逻辑类型之间的关系。你可以把它当作本章的“地图”。
这张图把本章的三个核心内容串起来了。FPGA内部结构是“硬件资源”,Verilog语法是“描述语言”,组合逻辑和时序逻辑是“设计思想”。三者缺一不可。
always @(posedge clk) 就是在说“每个时钟上升沿,把数据存到FF里”。
好了,这一章的内容就到这里。记住,FPGA设计不是写软件,是在画电路。多想想你写的每一行代码会综合成什么电路,慢慢就有感觉了。