跳转到主要内容

FPGA初学者最容易踩的40个坑:从Verilog到上板一次讲透

作者:硬件老实说

刚开始学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,最后会变成什么样的电路,以及它为什么能——或者为什么不能——在真实芯片里稳定跑起来。

文章来源:瑞苏盈科