第三章 FPGA基础与开发环境:FPGA架构简介、Vivado开发流程、硬件描述语言基础

各位同学,欢迎来到第三章。这一章咱们要啃的,是FPGA开发最底层的硬骨头——芯片长什么样、工具怎么用、代码怎么写。

说实话,我当年刚接触FPGA时,觉得这东西就是个黑盒子。后来踩了不少坑,才慢慢摸清门道。今天我把这些经验揉碎了讲给你听。

3.1 FPGA架构简介:别把它当CPU用

FPGA的全称是现场可编程门阵列。名字很长,但核心就一句话:它是一块可以让你自己定义逻辑功能的芯片

CPU是固定的,你只能写软件去调用它的指令。FPGA不一样,你可以把它的内部电路“焊”成你想要的样子。说白了,你是在设计硬件,而不是写软件。

3.1.1 基本组成单元

一个典型的FPGA芯片,内部主要由这几部分组成:

  • 可配置逻辑块(CLB):这是FPGA的“肌肉”。每个CLB里包含查找表(LUT)和触发器(FF)。LUT负责实现组合逻辑,FF负责存储状态。
  • 输入输出块(IOB):芯片和外界通信的接口。你可以配置成输入、输出或双向。
  • 互连资源:把CLB和IOB连起来的“高速公路”。布线就是靠这些。
  • 块RAM(BRAM):片上的存储资源。我习惯用它做FIFO或小容量缓存。
  • 数字信号处理单元(DSP48):专门做乘加运算的硬核。做AI加速时,这东西是宝贝。

核心概念:FPGA的“可编程”本质,是通过改变LUT的查找表内容和互连开关的通断来实现的。你写的Verilog代码,最终会被映射成这些配置。

3.1.2 查找表(LUT)的工作原理

LUT本质上是一个小型的RAM。比如一个4输入LUT,它有16个存储位。你输入4位地址,它输出对应的1位数据。

举个例子:你想实现一个与门(Y = A & B)。

// 真值表
A B | Y
0 0 | 0
0 1 | 0
1 0 | 0
1 1 | 1

这个真值表会被烧录到LUT的存储单元里。当A=1, B=1时,LUT输出1。其他情况输出0。

嗯,这里要注意:LUT的输入越多,能实现的逻辑越复杂,但面积也越大。我在项目中遇到过,为了省资源,把一个大LUT拆成多个小LUT用。

3.2 Vivado开发流程:从代码到比特流

Xilinx的Vivado是目前主流的FPGA开发工具。它的流程很清晰,但每一步都有坑。我带你走一遍。

3.2.1 标准流程五步走

  1. 设计输入:写Verilog/VHDL代码,或者用Block Design画图。
  2. 综合(Synthesis):把RTL代码转换成门级网表。说白了,就是把你的“与或非”逻辑,映射到LUT和触发器上。
  3. 实现(Implementation):包括翻译、映射、布局布线。这一步最耗时。Vivado会把网表里的逻辑单元,放到芯片的具体位置上,并连好线。
  4. 时序分析:检查你的设计能不能跑在目标频率上。我刚开始做设计时总觉得这步可有可无,直到有一次流片回来芯片死活上不了高频……嗯,从那以后我再也不敢跳过时序分析了。
  5. 生成比特流:最后一步,生成烧录文件。下载到板子上就能跑了。

我的习惯:每次综合完,先看一眼资源利用率报告。如果LUT或BRAM用超了90%,后面布局布线大概率会出问题。提前改代码,比最后跑不动再回头强。

3.2.2 常见坑与避坑指南

  • 综合时间太长:我曾经一个设计综合了3小时。后来发现是代码里写了个巨大的case语句。拆成多个小模块后,时间降到20分钟。
  • 时序不收敛:说白了就是路径太长了。加流水线寄存器,或者优化逻辑级数。
  • 资源不够用:检查是不是有冗余逻辑。比如你写了个always块,但有些分支没赋值,综合器会给你生成锁存器,白白浪费资源。

警告:不要在Vivado里同时跑多个大工程。它会吃光你的内存。我建议每个工程单独开一个Vivado实例,或者用Tcl脚本批量跑。

3.3 硬件描述语言基础:Verilog入门

FPGA开发离不开硬件描述语言。主流是Verilog和VHDL。我个人更推荐Verilog,语法简洁,上手快。咱们今天只讲最核心的。

3.3.1 模块结构

一个Verilog程序,就是一个模块(module)。模块有输入输出端口,内部实现逻辑。

module led_blink (
    input  wire clk,      // 时钟
    input  wire rst_n,    // 复位,低有效
    output reg  led       // LED输出
);

// 计数器
reg [23:0] cnt;

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

// LED取反
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        led <= 1'b0;
    else if (cnt == 24'd5000000)
        led <= ~led;
end

endmodule

这段代码实现了一个LED闪烁。每5M个时钟周期,LED翻转一次。如果时钟是50MHz,那就是每秒闪5次。

3.3.2 组合逻辑 vs 时序逻辑

这是新手最容易搞混的地方。

  • 组合逻辑:输出只取决于当前输入。用assignalways @(*)实现。
  • 时序逻辑:输出还取决于之前的状态。用always @(posedge clk)实现。

你想想看,如果你把时序逻辑写成了组合逻辑,仿真可能没问题,但上板子后信号会乱跳。我吃过这个亏,查了三天才找到原因。

3.3.3 阻塞赋值与非阻塞赋值

这是Verilog里最经典的坑。

  • 阻塞赋值(=):顺序执行。用于组合逻辑。
  • 非阻塞赋值(<=):并行执行。用于时序逻辑。

记住一条铁律:在always块里,描述组合逻辑用阻塞赋值,描述时序逻辑用非阻塞赋值。混用的话,仿真和综合结果可能不一致。

避坑指南:我曾经在一个always块里同时用了阻塞和非阻塞赋值。Vivado没报错,但仿真结果就是不对。后来老老实实分开写,问题就解决了。

3.4 总结与练习

这一章我们讲了FPGA的架构、Vivado的流程、Verilog的基础。这些都是基本功,就像练武要先扎马步一样。

给你留个小作业:

  1. 用Verilog写一个4位计数器,从0数到15,每数一次LED亮一次。
  2. 在Vivado里跑一遍综合和实现,看看资源利用率。
  3. 试着把计数器改成8位,看看资源增加了多少。

做完这些,你对FPGA的感性认识会上一个台阶。下一章,咱们开始真正接触FINN的编译流程。


公众号:蓝海资料掘金营,微信deep3321