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模块化设计是编程方法,组合逻辑与时序逻辑是设计思想。三者缺一不可。
好了,这一章的内容就到这里。记住:LUT、FF、BRAM是FPGA的三大基石;模块化设计是写Verilog的基本功;组合逻辑和时序逻辑的区别,是区分“真·硬件工程师”和“伪·硬件工程师”的分水岭。多写代码,多仿真,慢慢就熟了。
可可精品资料库