第二章:FPGA基础与Verilog语法入门

各位同学,欢迎来到第二章。这一章咱们要啃的,是FPGA设计的“内功心法”。说白了,就是搞清楚FPGA肚子里到底装了些什么,以及我们怎么用Verilog这门语言跟它对话。

我刚开始学FPGA的时候,总觉得它像个黑盒子。写代码、烧进去、看结果,中间发生了什么?完全不知道。后来踩了不少坑才明白——不理解内部结构,你写出来的代码就是“玄学”。好,咱们开始。

2.1 FPGA内部结构:你手里的“乐高积木”

FPGA为什么叫“现场可编程门阵列”?因为它本质上是一堆可以随意连接的基本单元。我个人习惯把它想象成一套乐高积木——你手里有几种固定的零件,但你可以用它们拼出任何你想要的东西。

FPGA内部主要有这么几样东西:

  • LUT(查找表):这是FPGA最核心的“计算单元”。说白了,它就是一个可以配置的真值表。你给它几个输入,它查一下表,输出结果。一个4输入的LUT,可以实现任意4输入的逻辑函数。
  • FF(触发器):用来存储状态的。时钟沿来了,它就把输入“拍”到输出上。没有它,时序逻辑就是空谈。
  • BRAM(块RAM):FPGA内部的存储资源。你可以把它配置成FIFO、单口RAM、双口RAM等等。我做过一个项目,需要缓存一帧图像,就是用BRAM实现的。
  • DSP(数字信号处理单元):专门用来做乘加运算的硬核。做图像处理、滤波、卷积的时候,这东西是神器。

核心理解:LUT做逻辑,FF做存储,BRAM做大容量缓存,DSP做数学运算。这四个东西,就是FPGA的“四大金刚”。

下面这张图,是我自己画的FPGA内部结构示意。你看一眼,心里就有数了。

FPGA内部结构示意图 可编程互联线 LUT 查找表 4输入/6输入 FF 触发器 D → Q BRAM 块RAM 36Kb/块 DSP 乘加 每个CLB(可配置逻辑块)包含多个LUT+FF对 BRAM和DSP分布在芯片四周的列上 逻辑运算 状态存储 数据缓存 数学计算 I/O 引脚(可配置为输入/输出/双向)

我的经验:刚开始学的时候,别纠结每个LUT里有多少个晶体管。你只需要知道:LUT做组合逻辑,FF做时序逻辑。这两个东西经常成对出现,叫“逻辑单元”或“Slice”。

2.2 Verilog模块结构:你的第一个“零件”

Verilog里最基本的东西叫“模块”(module)。一个模块就是一个功能块,有输入、有输出、有内部逻辑。你想想看,这跟FPGA的“乐高积木”是不是一个道理?

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

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg  [7:0] count     // 8位计数器输出
);

    // 时序逻辑:每个时钟上升沿计数
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else
            count <= count + 1'b1;
    end

endmodule

注意看,模块以 module 开头,以 endmodule 结尾。端口列表里声明了输入输出。内部用 always 块描述行为。就这么简单。

注意:我曾经见过新手把 endmodule 写成 end,结果编译报错半天找不到原因。记住,模块结束是 endmodule,不是 end。

2.3 数据类型:wire 和 reg 的区别

这是新手最容易搞混的地方。我刚开始也迷糊了很久。其实很简单:

  • wire:代表“连线”。它本身不存储值,只是把两个点连起来。用 assign 赋值。
  • reg:代表“寄存器”。它可以在 always 块里被赋值,并且会保持这个值直到下一次赋值。

但是!这里有个坑:reg 不一定会被综合成触发器。如果你在 always 块里写的是组合逻辑,那 reg 综合出来可能只是一根线。嗯,这里要注意。

特性 wire reg
默认值 高阻态 (z) 不定态 (x)
赋值方式 assign 连续赋值 always 块内赋值
综合结果 组合逻辑(连线) 组合逻辑 或 时序逻辑
使用场景 模块间连线、组合逻辑输出 always 块中的变量

一句话总结:在 always 块里用 reg,在 assign 里用 wire。别想太多,先记住这个规则。

2.4 always块与assign语句:两种描述方式

Verilog里描述逻辑有两种主要方式:assign 和 always。

assign 是连续赋值。说白了就是“一直连着的”。输入变了,输出立刻变。适合描述组合逻辑:

assign sum = a + b;
assign flag = (a > b) ? 1'b1 : 1'b0;

always 是过程赋值。它只有在敏感列表里的信号变化时才会执行。适合描述时序逻辑,也可以描述组合逻辑:

// 时序逻辑:时钟沿触发
always @(posedge clk) begin
    q <= d;
end

// 组合逻辑:电平触发
always @(*) begin
    if (sel)
        y = a;
    else
        y = b;
end

注意看,时序逻辑我用 <=(非阻塞赋值),组合逻辑我用 =(阻塞赋值)。这是有讲究的。我建议你一开始就养成这个习惯,否则后面写复杂逻辑时会出大问题。

避坑指南:我曾经在一个项目里,把时序逻辑的 <= 写成了 =,结果仿真没问题,上板子就跑飞了。查了两天才发现是赋值方式的问题。记住:时序逻辑用 <=,组合逻辑用 =。

2.5 组合逻辑与时序逻辑:本质区别

这两个概念,是数字电路设计的基石。我见过太多人在这上面栽跟头。

组合逻辑:输出只取决于当前输入。没有记忆功能。比如加法器、多路选择器。用 assign 或 always @(*) 描述。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。有记忆功能。比如计数器、状态机。用 always @(posedge clk) 描述。

你想想看,图像处理里的大部分算法,比如边缘检测、滤波,本质上都是组合逻辑。但你要把一帧图像存下来、逐行处理,那就需要时序逻辑来控制节奏。

// 组合逻辑示例:2选1多路选择器
module mux2 (
    input  wire a, b, sel,
    output wire y
);
    assign y = sel ? a : b;
endmodule

// 时序逻辑示例:D触发器
module dff (
    input  wire clk, d,
    output reg  q
);
    always @(posedge clk) begin
        q <= d;
    end
endmodule

核心理解:组合逻辑是“现在的输入决定现在的输出”,时序逻辑是“过去的输入影响现在的输出”。做图像采集时,像素数据流是组合逻辑,但帧同步、行同步这些控制信号,一定是时序逻辑。

好了,第二章的内容就到这里。这些基础概念,是后面所有章节的根基。别急着往下赶,先把LUT、FF、wire、reg、always、assign这些东西搞明白。后面写代码的时候,你会感谢今天认真看这一章的自己。