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级联,或者换用更高级的器件。

核心要点: LUT是实现组合逻辑的基本单元。现代FPGA的LUT通常是6输入,可以灵活配置成两个5输入LUT使用。

3.1.2 触发器(FF)—— 记住状态的家伙

触发器(Flip-Flop)是时序逻辑的基石。它能在时钟沿到来时,把输入数据“锁住”并保持到下一个时钟沿。没有FF,你就没法做计数器、状态机、流水线这些好东西。

每个FPGA的逻辑单元(Slice)里,通常包含4-8个FF。我建议你养成一个好习惯:写代码时,组合逻辑和时序逻辑要分开写。别把FF和LUT混在一个always块里,否则后期调试会哭的。

我的小技巧: 在代码里给每个FF取个有意义的名字,比如 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入门 - 知识体系 FPGA内部结构 LUT(查找表) FF(触发器) BRAM(块RAM) DSP(数字信号处理) 组合逻辑 + 存储 + 运算 = 可编程数字系统 Verilog语法基础 module(模块) assign(连续赋值) always(过程块) wire vs reg 阻塞赋值 = vs 非阻塞赋值 <= 敏感列表 @(posedge clk) 逻辑类型 组合逻辑 输出只取决于当前输入 时序逻辑 输出取决于当前输入+历史状态 组合逻辑:assign、always @(*) 时序逻辑:always @(posedge clk) 描述 实现 用Verilog语法描述逻辑,综合工具映射到FPGA内部结构上

这张图把本章的三个核心内容串起来了。FPGA内部结构是“硬件资源”,Verilog语法是“描述语言”,组合逻辑和时序逻辑是“设计思想”。三者缺一不可。

学习建议: 别急着背语法。先理解LUT、FF、BRAM、DSP是干什么的,再去看Verilog代码,你会发现很多语法其实就是在描述这些硬件的行为。比如 always @(posedge clk) 就是在说“每个时钟上升沿,把数据存到FF里”。

好了,这一章的内容就到这里。记住,FPGA设计不是写软件,是在画电路。多想想你写的每一行代码会综合成什么电路,慢慢就有感觉了。

可可精品资料库