第二章: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内部结构示意。你看一眼,心里就有数了。
我的经验:刚开始学的时候,别纠结每个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这些东西搞明白。后面写代码的时候,你会感谢今天认真看这一章的自己。