4、FPGA基础与Verilog入门:FPGA内部结构、模块化设计、组合逻辑与时序逻辑

各位同学,欢迎来到第四章。这一章咱们要啃点硬骨头——FPGA的内部结构,还有Verilog的模块化设计思路。说实话,这部分内容我当年自学的时候也绕了不少弯路。但别担心,我会把那些坑一个个指出来。

先问大家一个问题:FPGA到底是个什么东西?说白了,它就是一盒可以随意拼装的数字电路积木。你买回来的时候,里面全是空的逻辑单元。你想让它干什么,就得用硬件描述语言(比如Verilog)去“画”电路图。嗯,就是这么回事。

4.1 FPGA内部结构:LUT、FF、BRAM

FPGA内部最核心的三个东西,我总结为:LUT(查找表)、FF(触发器)、BRAM(块RAM)。这三样东西,你搞懂了,FPGA就算入门了。

4.1.1 LUT(查找表)—— 组合逻辑的“万能钥匙”

LUT是什么?你可以把它想象成一个“真值表”。输入几个信号,输出就对应一个结果。比如一个4输入的LUT,它内部其实就是一个16×1的SRAM。你提前把真值表写进去,输入来了,直接查表输出。

我在项目中遇到过一件事:有个同事用LUT实现了一个复杂的加法器,结果资源爆了。为什么?因为LUT本质上只能做“查表”,做不了复杂的算术运算。你想想看,一个32位加法器如果用LUT硬拼,那得消耗多少资源?所以,LUT适合做逻辑判断、译码、多路选择,不适合做大量运算。

核心要点:
  • LUT本质是SRAM + 多路选择器
  • 常见有4输入、5输入、6输入LUT
  • 一个LUT可以实现任意组合逻辑函数
  • 资源有限,别拿LUT当计算器用

4.1.2 FF(触发器)—— 时序逻辑的“记忆细胞”

FF是FPGA里最基础的存储单元。它只有一个功能:在时钟上升沿(或下降沿)到来时,把输入的值“锁住”,然后输出。说白了,就是一个1位的寄存器。

我刚开始学的时候,总搞不清LUT和FF的区别。后来想通了:LUT负责“算”,FF负责“存”。你写Verilog的时候,always @(posedge clk) 这种写法,综合出来就是FF。如果你写的是 assign 或者 always @(*),那综合出来就是LUT。

我的小技巧: 看代码的时候,先找时钟沿。看到 posedge clk 或 negedge clk,就知道这里会生成FF。没有时钟沿的,全是组合逻辑。

4.1.3 BRAM(块RAM)—— 大容量存储的“仓库”

BRAM是FPGA内部专用的存储模块。一个BRAM通常有18Kb或36Kb的容量。它可以配置成单口RAM、双口RAM、FIFO、ROM等等。我个人的习惯是:只要需要存储超过几百比特的数据,就用BRAM。用LUT搭出来的寄存器堆,太浪费资源了。

我曾经在一个项目中,需要用FPGA缓存一帧图像数据。一帧720p的图像,大概需要1.5MB的存储空间。如果用FF搭,那FPGA得买航母那么大的。但用BRAM,几块就够了。这就是BRAM的价值。

资源类型 用途 容量 速度
LUT 组合逻辑、译码、选择 小(几比特) 快
FF 寄存器、状态机、流水线 极小(1比特) 极快
BRAM 数据缓存、FIFO、ROM 大(18Kb~36Kb) 中等

4.2 Verilog模块化设计

写Verilog和写C语言不一样。C语言讲究函数调用,Verilog讲究模块实例化。说白了,就是把一个大电路拆成若干个小电路,然后像搭积木一样拼起来。

我见过很多新手,一个模块写几千行。结果仿真跑不动,综合报错也看不懂。我的建议是:一个模块不要超过200行。超过200行,就拆。

4.2.1 模块的基本结构

一个标准的Verilog模块长这样:

module counter (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [7:0] cnt
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 8'd0;
        else if (en)
            cnt <= cnt + 1'b1;
    end

endmodule

注意看:module 和 endmodule 是成对出现的。端口列表里,input 和 output 要写清楚。我个人习惯把时钟和复位放在最前面,这样别人一看就知道这个模块的“心跳”是什么。

4.2.2 模块实例化——搭积木的艺术

模块写好了,怎么用?实例化。比如上面那个计数器,我想用两个:

module top (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en1,
    input  wire       en2,
    output wire [7:0] cnt1,
    output wire [7:0] cnt2
);

    counter u_counter1 (
        .clk  (clk),
        .rst_n(rst_n),
        .en   (en1),
        .cnt  (cnt1)
    );

    counter u_counter2 (
        .clk  (clk),
        .rst_n(rst_n),
        .en   (en2),
        .cnt  (cnt2)
    );

endmodule

这里 u_counter1 和 u_counter2 就是实例名。我习惯用 u_ 开头,这样一眼就能看出是实例化的模块。你想想看,如果代码里有几十个实例,没有命名规范,那调试起来得多痛苦?

注意: 实例化时,端口连接有两种方式:按位置连接和按名称连接。我强烈建议用按名称连接(就是上面那种 .端口名(信号) 的写法)。按位置连接一旦端口顺序变了,整个代码就废了。我曾经吃过这个亏,调了三天才发现是端口顺序搞反了。

4.3 组合逻辑与时序逻辑

这是Verilog里最容易混淆的两个概念。我直接说结论:组合逻辑没有记忆,时序逻辑有记忆。

4.3.1 组合逻辑——输入变,输出立刻变

组合逻辑的特点是:输出只取决于当前的输入。没有时钟,没有存储。比如一个与门:

assign y = a & b;

只要 a 或 b 变了,y 立刻变。这就是组合逻辑。在FPGA里,组合逻辑用LUT实现。

写组合逻辑时,我常用的写法有两种:

  • assign 语句:适合简单的赋值
  • always @(*) 块:适合复杂的逻辑判断

举个例子:

always @(*) begin
    case (sel)
        2'b00: data_out = data_in0;
        2'b01: data_out = data_in1;
        2'b10: data_out = data_in2;
        2'b11: data_out = data_in3;
    endcase
end

这就是一个4选1多路选择器。注意,always @(*) 里的 * 表示“所有输入信号”。只要输入变了,这个块就会重新计算。

4.3.2 时序逻辑——时钟沿来了,才变

时序逻辑的特点是:输出在时钟沿到来时才更新。比如一个D触发器:

always @(posedge clk) begin
    q <= d;
end

这里 q 只在 clk 的上升沿才等于 d。其他时间,q 保持不变。这就是“记忆”。在FPGA里,时序逻辑用FF实现。

我刚开始学的时候,总搞不清 = 和 <= 的区别。后来明白了:组合逻辑用 =(阻塞赋值),时序逻辑用 <=(非阻塞赋值)。这是铁律,别搞混。

避坑指南:
  • 组合逻辑里用 =,时序逻辑里用 <=
  • 不要在同一个 always 块里混用两种赋值
  • 时序逻辑的敏感列表里一定要有时钟沿
  • 组合逻辑的敏感列表里用 * 最安全

4.4 本章知识体系总览

说了这么多,我画了一张图,把本章的核心逻辑串起来。你看完这张图,再回头看前面的内容,会清晰很多。

FPGA基础与Verilog入门 - 知识体系 FPGA内部结构 LUT(查找表) 组合逻辑实现 FF(触发器) 时序逻辑存储 BRAM(块RAM) 大容量存储 Verilog模块化设计 模块定义(module/endmodule) 模块实例化(搭积木) 组合逻辑 vs 时序逻辑 组合逻辑:assign / always @(*) 时序逻辑:always @(posedge clk)

这张图把本章的三个核心内容串起来了:FPGA内部结构是硬件基础,Verilog模块化设计是编程方法,组合逻辑与时序逻辑是设计思想。三者缺一不可。

好了,这一章的内容就到这里。记住:LUT、FF、BRAM是FPGA的三大基石;模块化设计是写Verilog的基本功;组合逻辑和时序逻辑的区别,是区分“真·硬件工程师”和“伪·硬件工程师”的分水岭。多写代码,多仿真,慢慢就熟了。

最后送大家一句话: 学FPGA没有捷径,但有一条近路——多动手。光看书是学不会的,一定要上板子跑一跑。哪怕只是点亮一个LED,也比看十页书强。

可可精品资料库