作者:硬件老实说
刚开始学FPGA的时候,很多人都有一个错觉:只要把Verilog学会,再把Vivado或者Quartus用熟,FPGA基本就入门了。
真做过几个项目以后才发现,事情没这么简单。
代码能综合,不代表逻辑没问题;仿真能跑,不代表上板一定正常;波形看起来没问题,也不代表时序就能收敛。
更麻烦的是,FPGA很多坑一开始根本看不出来。
比如一个if没写完整,综合器给你推了个Latch;一个跨时钟域信号直接连过去,仿真可能一切正常,上板之后却开始“随机抽风”;为了降频随手拿逻辑做了个分频时钟,最后整个设计的时序约束都变得一团糟。
所以,FPGA入门真正需要掌握的,不只是40个知识点,更是这40个知识点背后的工程思维。
下面这40个坑,基本覆盖了从数字电路、Verilog,到仿真、时序、FPGA架构,再到真正上板开发的全过程。
一、第一关就容易翻车:数字电路基础没打牢
很多人不太愿意花时间学数字电路。
“我就是来写Verilog的,逻辑门这些东西以后再说。”
这其实是第一个坑。
Verilog只是描述硬件的语言。如果不知道自己描述出来的东西最终是什么电路,后面遇到问题很容易两眼一抹黑。
坑1:二进制、十六进制都不熟
FPGA工程里,十六进制几乎天天见。
寄存器地址:0x80000000
数据:0xA5
状态寄存器:0x00000003
如果二进制、十六进制之间还需要拿计算器转换,调试效率会非常低。
至少要熟练掌握:二进制 ↔ 十六进制 ↔ 十进制
另外,BCD码也要知道它是怎么回事,尤其是做显示、计时器之类的设计时。
坑2:只知道“与或非”,不知道异或到底干什么
AND、OR、NOT是最基础的。
但工程里真正经常碰到的还有:
XOR
XNOR
比如奇偶校验、CRC、比较逻辑、加法器,都大量使用异或。
看到:assign y = a ^ b;
不要只知道“这是异或”。
最好能马上想到:这个东西最终会变成什么逻辑资源?
这才开始进入硬件思维。
坑3:搞不清组合逻辑和时序逻辑
这是必须跨过去的一道坎。
组合逻辑:输入变,输出跟着变。
时序逻辑:要靠时钟和寄存器保存状态。
例如:
assign y = a & b;
这是组合逻辑。
而:
always @(posedge clk)
q <= d;
描述的是时序逻辑。
简单粗暴地记:组合逻辑负责算,寄存器负责记。
后面的FSM、Pipeline、FIFO,本质上都能从这里一路追过去。
坑4:看到MUX还不知道是什么
MUX,也就是多路选择器。
A ─┐
B ─┼──→ MUX ──→ Y
C ─┘
↑
SEL
Verilog里一个简单的:
assign y = sel ? a : b;
背后就是选择逻辑。
复杂的数据通路里,MUX会非常多。
所以不要小看这个东西。很多时候,FPGA频率上不去,问题就藏在一大堆选择逻辑里面。
坑5:把Latch和Flip-Flop当成一回事
Latch是电平触发。
Flip-Flop是边沿触发。
初学者真正容易踩的是——你可能没打算写Latch,却让综合器给你推出来了。
例如:
always @(*) begin
if (en)
q = d;
end
en=0的时候,q怎么办?
代码没有说。
综合器只能理解成:
那就把之前的值保持住。
于是Latch出来了。
看到:
Latch inferred
别急着怪Vivado。
先检查自己的组合逻辑是不是没有完整赋值。
坑6:加法器只会写+,不知道背后的硬件
写:assign sum = a + b;
当然没问题。
但要知道,FPGA并不是运行了一条“加法指令”。
综合工具会把它映射到芯片内部的专用进位链等资源。
理解半加器、全加器之后,再去理解Carry Chain、DSP等资源,会顺很多。
二、Verilog最容易出现的误区:把它当成C语言
这是第二个大坑。
Verilog看起来有变量、if、case、always,确实很像软件语言。
但它描述的是硬件并行行为。
这个区别一定要建立起来。
坑7:module和端口概念混乱
一个基本模块:
module led_ctrl(
input wire clk,
input wire rst_n,
output wire led
);
endmodule
这里的module可以理解成一个硬件模块。
端口就是实际的硬件接口。
不是函数,也不是软件里的参数列表。
坑8:认为reg就一定是寄存器
这是一个流传很广的误解。
例如:
reg a;
always @(*) begin
a = b & c;
end
这里的a虽然声明成reg,但综合以后完全可能只是组合逻辑。
真正决定是不是寄存器的,不是reg这个关键词,而是:你的RTL代码描述了什么硬件结构。
这一点非常重要。
坑9:位宽随便写
比如:
8'hFF
16'd255
32'h000000FF
它们的数值可能看起来一样,但位宽并不一样。
再比如:
reg [7:0] a;
reg [15:0] b;
assign c = a + b;
这种运算到底怎么扩展?有没有符号位问题?
大型工程里,位宽错误是很常见的Bug来源。
尤其涉及:
- 加法
- 乘法
- 移位
- 有符号数
- AXI接口
- ADC数据
更要小心。
坑10:总线、位选、部分位选不会用
例如:
wire [7:0] data;
访问:
data[0]
或者:
data[7:4]
这些都是基础操作。
真正做项目之后你会发现,寄存器映射、协议解析、数据拼接,天天都在干这个。
坑11:把三种Verilog描述方式混在一起
Verilog常见的三种描述方式:结构描述
模块之间互相连接。
数据流描述
assign y = a & b;
行为描述
always @(posedge clk)
q <= d;
不用死记概念。
你只需要知道:
我现在是在描述连线、组合逻辑,还是描述一个时序过程?
写代码的时候脑子里有这个判断,已经够用了。
坑12:阻塞赋值、非阻塞赋值乱用
这是Verilog初学者必踩坑。
组合逻辑通常:
=
时序逻辑通常:
<=
例如:
always @(posedge clk) begin
a <= b;
b <= a;
end
两个寄存器会在同一个时钟沿完成数据交换。
如果换成阻塞赋值:
always @(posedge clk) begin
a = b;
b = a;
end
代码行为就不一样了。
一句工程习惯:
时序逻辑优先<=,组合逻辑优先=。
先这么写,能少掉很多麻烦。
坑13:always敏感列表写错
常见:
always @(posedge clk)
表示时序逻辑。
组合逻辑:
always @(*)
如果是SystemVerilog,则可以进一步使用:
always_ff
always_comb
这些写法的价值在于,让代码意图更清楚,也更容易被工具检查。
坑14:if和case随便选
简单条件:
if
多个状态:
case
尤其是状态机:
case(state)
IDLE: ...
RUN: ...
DONE: ...
endcase
需要注意的是,组合逻辑中的条件如果没有覆盖完整,很容易推断出Latch。
所以写完if之后,别忘了问一句:
每一种情况下,这个输出到底有没有确定的值?
三、不会仿真,上板就是“碰运气”
有些新人特别喜欢直接下载。
代码写完:
“跑一下看看。”
这不是工程开发,这是买彩票。
坑15:Testbench不会写
Testbench的作用很简单:
给你的设计制造输入,然后观察输出。
基本结构:
Testbench
│
↓
DUT
│
├── clk
├── reset
├── data
└── output
刚开始不要追求复杂。
一个计数器写一个Testbench,一个FSM写一个Testbench。
慢慢就会形成验证习惯。
坑16:只看代码,不看波形
Verilog代码几十行的时候,靠肉眼还能看。
几千行、几万行之后,靠看代码猜问题,基本就是折磨自己。
仿真波形里重点看:
- Clock
- Reset
- State
- Valid
- Ready
- Data
- Enable
尤其是状态机和握手协议,波形非常重要。
坑17:把#10当成FPGA里的真实延时
例如:
#10;
这是仿真时间控制。
它不是:
“让FPGA真的延时10ns。”
FPGA里面真正的延时控制通常靠:
- 时钟
- 计数器
- Clock Enable
- PLL/MMCM
- 时序约束
所以看到Testbench里的#10,不要和真实硬件延时混为一谈。
坑18:只会看波形,不会用$display
比如:
$display("data = %h", data);
还有:
$monitor
$finish
$dumpfile
这些都是验证和调试的工具。
项目越复杂,越应该学会利用自动化检查,而不是盯着波形一点一点找。
坑19:认为RTL仿真通过就万事大吉
这是一个非常危险的想法。
RTL仿真解决的是:功能逻辑有没有问题?
但综合、布局布线以后,还会出现:
- Setup违例
- Hold违例
- Clock问题
- Routing问题
- CDC问题
所以:RTL Simulation Pass,不等于工程完成。
四、你写的代码,最后到底变成了什么?
学到这里,必须开始建立FPGA内部架构概念。
坑20:不知道LUT是什么
LUT是FPGA实现组合逻辑的重要基本资源。
你写:
assign y = a & b;
综合工具会把它映射到FPGA内部逻辑资源。
所以RTL代码不是“执行”出来的。
而是:综合 → 映射 → 布局布线 → 变成真实硬件结构。
坑21:不知道Flip-Flop对应什么
例如:
always @(posedge clk)
q <= d;
这就是典型的寄存器描述。
最终会映射到FPGA内部的FF资源。
所以你应该逐渐建立这样的对应关系:
RTL
↓
LUT
FF
BRAM
DSP
Clock
IO
↓
真实FPGA硬件
这一步想通以后,很多优化才有意义。
坑22:BRAM和Distributed RAM乱用
FPGA内部的RAM并不是只有一种。
常见有:
- BRAM
- Distributed RAM
小容量、局部存储,有时适合Distributed RAM。
较大的Buffer、FIFO、数据缓存,则通常考虑BRAM等专用存储资源。
千万别一上来看到一个数组,就想当然地认为:
“反正综合器会自动处理。”
大型项目里,资源映射是需要关注的。
坑23:PLL/MMCM只会“点一下IP”
很多初学者会调用PLL,却不知道它解决什么问题。
PLL/MMCM通常用于:
- 倍频
- 分频
- 相位调整
- 时钟管理
例如:
100MHz
↓
PLL/MMCM
↓
200MHz
但要记住:
时钟是FPGA里最特殊的信号之一。
不能像普通Data一样随便处理。
坑24:拿普通逻辑线传时钟
比如为了图方便:
assign clk_new = clk & enable;
看起来很简单。
实际上可能给整个设计埋雷。
因为时钟需要低skew的专用时钟网络。
所以工程上通常更推荐:
Clock
↓
Clock Enable
↓
普通逻辑
而不是:
Clock
↓
逻辑门
↓
新Clock
能不用逻辑造时钟,就尽量别造。
坑25:忘记管脚和IO约束
RTL写了:
input clk;
FPGA并不知道这个clk接哪个物理Pin。
还需要约束:
- Pin
- I/O Standard
- Clock
- Timing
Xilinx/AMD平台通常使用.xdc。
约束不是“编译前顺手加一下”。
它本身就是设计的一部分。
五、FPGA真正开始难起来:时序
如果前面的东西是“把电路做出来”,那么时序就是:
让这个电路稳定地跑起来。
坑26:不理解posedge/negedge
:
posedge clk
是上升沿。
negedge clk
是下降沿。
大多数同步设计会选择统一的时钟边沿。
不要一个模块posedge,一个模块negedge,最后自己把自己绕进去。
除非你真的知道为什么这么做。
坑27:同步复位、异步复位傻傻分不清
异步复位:
always @(posedge clk or negedge rst_n)
同步复位:
always @(posedge clk)
两种都很常见。
关键不是背“哪个最好”。
而是要考虑整个系统:
复位从哪里来?什么时候拉低?什么时候释放?跨不跨时钟域?
尤其复杂SoC/FPGA系统里,复位也是需要认真设计的。
坑28:Setup/Hold不知道是什么意思
可以简单理解:
Setup:
时钟边沿到来之前,数据要提前稳定。
Hold:
时钟边沿过去之后,数据还要继续保持一小段时间。
如果不满足,就可能出现时序问题。
所以FPGA里经常会看到:
WNS
TNS
Setup
Hold
Slack
这些东西不是“高级报告”。
它们直接告诉你:这个设计能不能稳定跑。
坑29:跨时钟域直接连信号
这是工程中非常经典的坑。
假设:
Clock A
↓
DATA
↓
Clock B
如果A域的数据刚好在B域采样边沿附近变化,就可能发生亚稳态。
单bit控制信号常见处理:
FF1 → FF2
也就是双级同步器。
但这里必须强调:
双级同步器不是所有CDC问题都能解决。
多bit数据不能简单地每一位都过两个FF就完事。
这时候可能需要:
- 握手
- Async FIFO
- Gray Code
- 数据保持协议
这是FPGA从“入门”走向“工程”的一道分水岭。
坑30:不遵守同步设计原则
几个非常实用的原则:
不要随便门控时钟。
不要用组合逻辑制造时钟。
不要写奇怪的组合反馈。
尽量形成:
Register
↓
Combinational Logic
↓
Register
↓
Combinational Logic
↓
Register
这套结构虽然看起来朴素,但真的很耐用。
六、把这5个模块写熟,很多项目就能拆开了
FPGA入门不需要一开始就做超级复杂的项目。
先把几个基本模块写到熟。
坑31:分频器直接生成新时钟
计数器当然可以实现“慢下来”的效果。
但工程里更推荐把它作为Clock Enable:
Clock
↓
Counter
↓
Enable
↓
Logic
而不是:
Clock
↓
Counter
↓
New Clock
如果真的需要新的时钟频率,优先考虑专用时钟资源。
坑32:FSM只会照着模板抄
FSM是FPGA工程里非常常见的东西。
例如:
IDLE
↓
START
↓
RUN
↓
DONE
↓
IDLE
协议控制、UART、SPI、I²C、数据处理,都可以看到FSM的影子。
但不要只记模板。
真正要搞清楚:
什么情况下跳状态?
状态保存什么信息?
输出什么时候变化?
坑33:不知道FSM三段式到底在干什么
经典三段式:
① 状态寄存器
② 次态逻辑
③ 输出逻辑
也就是:
state
↓
next_state
↓
output
它的好处不是“老师要求这么写”。
而是结构比较清楚,调试起来舒服。
当然,实际工程中也有人使用两段式甚至其他写法。
重点还是:状态、组合逻辑、时序逻辑边界要清楚。
坑34:计数器写得不严谨
计数器看起来很简单:
counter <= counter + 1'b1;
但实际项目里要考虑:
- 初值
- 最大值
- 溢出
- 模N
- 复位
- Enable
- 位宽
比如模10计数:
0 → 1 → 2 → ... → 9 → 0
不要简单依赖自然溢出。
坑35:按键直接接FPGA逻辑
按键看起来是:
按下 → 1
松开 → 0
实际却不是这么干净。
机械触点会抖。
所以基本流程应该是:
按键
↓
同步
↓
消抖
↓
稳定信号
↓
边沿检测
↓
事件
这个小实验非常适合新手。
因为一下就能把:
CDC、计数器、FSM、边沿检测
这些知识串起来。
七、工具不会用,前面的东西也很难落地
FPGA开发工具很多,但不要把它理解成“点几个按钮就完事”。
坑36:不知道完整开发流程
一个基本流程应该是:
需求
↓
RTL
↓
Testbench
↓
仿真
↓
综合
↓
布局布线
↓
时序分析
↓
Bitstream
↓
下载
↓
板级调试
任何一步出问题,都可能导致最后“板子不工作”。
所以遇到Bug,第一件事不是疯狂改代码。
先判断:问题到底在哪一层?
坑37:只会Vivado里的“Run”
Xilinx/AMD FPGA常用Vivado。
Altera FPGA常用Quartus Prime。
初学阶段至少要能独立完成:
- 建工程
- 加RTL
- 加约束
- 综合
- Implementation
- 查看资源
- 查看Timing
- 生成Bitstream
- 下载
能独立跑通一次完整流程,比看十篇工具教程都有用。
坑38:Warning全部点掉
这个习惯一定要改。
看到:
WARNING
然后:
“没事,能生成bit就行。”
以后项目大了,很可能会被自己坑回来。
尤其关注:
Latch inferred
Multiple drivers
Width mismatch
Unconnected port
Clock not constrained
Timing violation
不要追求“Warning数量变成0”这么简单。
真正应该问:这个Warning为什么出现?它会不会影响硬件?
八、最后两个坑,决定你能不能从“入门”走向工程
坑39:频率不够,只知道“让工具多跑几次”
这是很多初学者遇到Timing Fail后的真实状态。
重新Implementation。
不行。
再跑一次。
还是不行。
其实真正应该看的是关键路径:
Register
↓
Combinational Logic
↓
Register
如果中间逻辑太深,就考虑Pipeline。
例如:
原来:
REG
↓
Logic A
↓
Logic B
↓
Logic C
↓
REG
改成:
REG
↓
Logic A
↓
REG
↓
Logic B
↓
REG
↓
Logic C
↓
REG
路径短了,频率通常更容易提升。
但代价也来了:延迟增加、寄存器增加、控制更复杂。
所以Pipeline不是万能药。
真正的目标是:在频率、吞吐率、延迟和资源之间做平衡。
坑40:IP会调用,但不知道IP解决了什么问题
PLL、FIFO、RAM、DDR、PCIe、Ethernet……
这些东西没必要全部从零开始造轮子。
成熟IP可以大幅提高开发效率。
但这里还有一个坑:不要变成“IP调用工程师”。
比如调用Async FIFO之前,至少应该知道:
- 为什么要FIFO?
- 为什么有两个时钟?
- Full怎么来的?
- Empty怎么来的?
- CDC怎么处理?
- 数据什么时候真正安全?
否则IP一旦出问题,就只剩一句:
“重新生成一下试试?”
那就比较尴尬了。
最后:真正值得初学者记住的,不是40个坑,而是这条路线
把这40个知识点全部学完,并不意味着你已经成为FPGA工程师。
它更像是拿到了一张地图。
真正建议的学习顺序是:
数字电路
↓
Verilog
↓
组合逻辑 / 时序逻辑
↓
Testbench
↓
RTL仿真
↓
LUT / FF / BRAM / DSP
↓
时钟与复位
↓
Setup / Hold
↓
CDC
↓
FSM / Counter / FIFO
↓
Timing Closure
↓
IP
↓
完整FPGA项目
不要试图一次把所有东西吃完。
更实际的方式是——
学一个,写一个;写一个,仿真一个;仿真通过,再上板。
比如第一周只做LED和计数器。
第二周做按键消抖。
第三周做UART。
再往后做FIFO、SPI、I²C。
等你真正经历过几次:“仿真没问题 → 上板不工作 → 查波形 → 查约束 → 查时序 → 查CDC → 最后发现是自己代码写错了”
FPGA才算真正入门。
因为FPGA工程最有意思、也最折磨人的地方就在这里:你最终面对的不是Verilog代码,而是一块真的在跑的硬件。
代码只是入口。
真正的能力,是知道自己写下的每一行RTL,最后会变成什么样的电路,以及它为什么能——或者为什么不能——在真实芯片里稳定跑起来。
文章来源:瑞苏盈科