第二章 FPGA基础与Verilog语法速通: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的配置。

为了让你更直观地理解,我画了一张结构图。你看一眼就明白了:

FPGA 内部结构示意图 可编程逻辑单元阵列 LUT+FF LUT+FF LUT+FF LUT+FF LUT+FF LUT+FF 块RAM Block RAM DSP单元 DSP Slice 可编程互连资源(Routing) I/O 引脚(IOB)

个人经验:我在做图像缩放项目时,一开始把行缓存数据全放在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视图,慢慢你就找到感觉了。

可可精品资料库