FPGA基础回顾:从结构到流程,一次讲透
说实话,每次带新人做项目,我都要先问一句:「你搞清楚FPGA里面到底长什么样了吗?」很多人上来就写代码,结果综合出来资源不够用,或者时序跑不过去,回头才发现——哦,原来LUT和FF是干这个用的。
这一章,咱们就把FPGA的底裤扒开看看。不搞那些虚的,全是实战中天天要打交道的玩意儿。
FPGA内部结构:你手里的积木盒
FPGA说白了就是一大盒可编程的积木。你想想看,它不像CPU那样指令一条条跑,也不像ASIC那样焊死了功能。它的牛逼之处在于——你可以把电路「画」进去,然后让它真的变成硬件。
那这盒积木里到底装了啥?我按重要程度排个序:
1. LUT(查找表)—— 最基础的逻辑单元
LUT的全称是Look-Up Table,翻译过来就是「查表」。它本质上是一个小型的RAM,输入几个信号,输出一个结果。比如一个4输入的LUT,可以实现任意4输入的组合逻辑。
我在项目中遇到过一个坑:有人用LUT实现了16位加法器,结果资源爆了。为什么?因为加法器用LUT做太浪费了,应该用DSP或者进位链。嗯,这里要注意——LUT适合做随机逻辑,不适合做算术运算。
2. FF(触发器)—— 记住状态的家伙
FF就是Flip-Flop,触发器。它的作用很简单:在时钟沿到来的时候,把输入的值「锁住」并输出。没有FF,你的电路就没法做流水线,没法做状态机。
我个人的习惯是:写代码时先想清楚哪些信号需要寄存器打一拍。比如跨时钟域的信号,必须用两级FF同步,否则亚稳态会教你做人。
3. BRAM(块RAM)—— 存数据的仓库
BRAM是FPGA内部的专用存储资源。它不像LUT搭出来的分布式RAM那么零碎,而是一整块一整块的。常见的BRAM大小是18Kb或36Kb,可以配置成单口、双口、真双口等模式。
我曾经在做一个图像处理项目时,需要缓存一行1920像素的数据。如果用FF搭,资源直接爆炸。换成BRAM,轻轻松松搞定。所以记住:大块数据存储,优先用BRAM。
| 存储方式 | 适用场景 | 资源消耗 |
|---|---|---|
| FF(寄存器) | 少量数据、频繁读写 | 高 |
| 分布式RAM(LUT) | 小容量、深度浅 | 中等 |
| BRAM | 大容量、块状存储 | 低 |
4. DSP(数字信号处理单元)—— 算数的利器
DSP slice是FPGA里专门做乘加运算的硬核。一个DSP可以完成一次乘法+一次加法,而且一个时钟周期就能出结果。如果你用LUT搭乘法器,不仅慢,还费资源。
我建议:只要涉及乘法、乘加、乘累加,直接例化DSP。别自己手写乘法器,除非你想体验时序收敛的痛苦。
Verilog基础语法:够用就行
Verilog这东西,说难不难,说简单也不简单。我见过有人写了三年Verilog,还是分不清阻塞赋值和非阻塞赋值的区别。咱们不搞学院派那套,直接说实战中怎么用。
模块化设计
每个模块就是一个功能块。比如一个计数器模块,一个FIFO模块,一个状态机模块。模块之间通过端口连接。
module counter #(
parameter WIDTH = 8
)(
input clk,
input rst_n,
input en,
output reg [WIDTH-1:0] cnt
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 0;
else if (en)
cnt <= cnt + 1;
end
endmodule
你看,这个模块有参数、有输入输出、有时序逻辑。写模块时我习惯把参数放在最前面,这样别人调用时一目了然。
阻塞 vs 非阻塞
一句话总结:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。别问为什么,这是硬件描述语言的铁律。我曾经在面试时问过这个问题,十个人里有六个答不上来。
状态机写法
状态机是FPGA设计的灵魂。我推荐三段式写法:第一段描述状态转移,第二段描述次态逻辑,第三段描述输出。这样代码清晰,综合出来也漂亮。
// 三段式状态机示例
reg [1:0] state, next_state;
// 第一段:状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else state <= next_state;
end
// 第二段:次态逻辑
always @(*) begin
case (state)
IDLE: if (start) next_state = WORK;
else next_state = IDLE;
WORK: if (done) next_state = DONE;
else next_state = WORK;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(*) begin
case (state)
IDLE: out = 0;
WORK: out = 1;
DONE: out = 0;
endcase
end
开发流程:从代码到比特流
写完了代码,怎么让它跑在FPGA上?这中间有三步:综合、实现、下载。每一步都有坑,我一个个说。
1. 综合(Synthesis)
综合就是把Verilog代码翻译成网表——也就是LUT、FF、BRAM这些基本单元的连接关系。说白了,就是把你的「想法」变成「电路蓝图」。
我遇到过一个情况:代码写得很漂亮,但综合出来资源爆了。后来发现是某个循环被综合成了巨大的查找表。所以综合完后一定要看报告,看看资源用了多少,有没有警告。
2. 实现(Implementation)
实现又分三步:翻译(Translate)、映射(Map)、布局布线(Place & Route)。
- 翻译: 把网表和约束文件合并成一个整体
- 映射: 把逻辑单元映射到具体的LUT、FF上
- 布局布线: 决定每个单元放在芯片的哪个位置,以及怎么连线
这一步最耗时间,也最容易出问题。时序不满足?多半是布局布线没做好。我个人的经验是:先跑一次看看资源分布,如果某个区域特别密集,手动加个区域约束把它分散开。
3. 下载(Download)
最后一步,把生成的比特流文件(.bit)烧录到FPGA里。下载方式有两种:JTAG和SPI Flash。JTAG用于调试,掉电就丢;SPI Flash用于固化,上电自动加载。
避坑: 下载前一定要检查FPGA的供电电压和配置模式。我曾经因为跳线帽没插对,折腾了整整一个下午。
本章知识体系
下面这张图,是我自己画的FPGA基础回顾的知识结构。你看一眼,心里就有数了。
好了,这一章的内容就到这儿。FPGA基础回顾这块,说白了就是三件事:搞清楚芯片里有什么,学会怎么描述电路,走通从代码到硬件的流程。这三件事搞定了,后面的项目实战你就能游刃有余。