FPGA基础回顾:从LUT到时序收敛

各位同学,咱们开始第一节课。说实话,每次讲FPGA基础,我都觉得特别重要。你想想看,如果地基没打好,后面盖再高的楼也悬。我在低延迟交易系统里踩过的坑,十有八九都是基础不牢导致的。

今天咱们就聊聊FPGA内部到底长什么样,Verilog怎么写才靠谱,以及那个让无数人头疼的时序收敛问题。

FPGA内部结构:不只是逻辑门

很多人以为FPGA就是一堆门电路拼在一起。其实没那么简单。FPGA内部是一个高度结构化的阵列,我习惯把它想象成一个「乐高城市」——有住宅区、商业区、交通枢纽。

FPGA内部结构示意图 LUT 查找表 6输入 / 1输出 FF 触发器 D型 / 可复位 BRAM 块RAM 36Kb / 双端口 DSP48 数字信号处理 乘加器 / 25x18 SerDes 高速串行收发器 GTH/GTY / 最高56Gbps 可编程互联 Switch Matrix 行列布线 / 延迟可控

LUT(查找表)—— 逻辑的灵魂

LUT说白了就是一个「查字典」的电路。你给它几个输入,它直接输出对应的结果。现在的FPGA主流是6输入LUT,能实现任意6输入布尔函数。

我在做行情解析时,经常用LUT来做协议解码。比如以太网帧头的类型字段,用LUT查一下就知道是UDP还是TCP,比用逻辑门拼快多了。

小技巧: 写代码时尽量用case语句,综合工具会自动映射到LUT。比用if-else嵌套效率高不少。

FF(触发器)—— 时序的基石

触发器就是FPGA里的「记忆单元」。每个LUT后面基本都跟着一个FF,它们俩是黄金搭档。

我记得刚入行时,有个同事写代码忘了加always块的敏感列表,结果综合出来一堆透明锁存器。那板子调了三天才找到问题。嗯,从此我养成了写完整敏感列表的习惯。

BRAM(块RAM)—— 数据缓存专家

BRAM是FPGA里真正的「内存」。每个BRAM有36Kb,可以配置成单端口、双端口、甚至FIFO。

在交易系统里,我用BRAM做订单簿的深度缓存。双端口特性特别适合——一个端口写数据,另一个端口读数据,互不干扰。

BRAM配置位宽深度典型用途
单端口1-36位1024-32768参数存储
真双端口1-36位1024-32768跨时钟域
简单双端口1-72位512-16384FIFO

DSP48(数字信号处理单元)—— 算力担当

DSP48是FPGA里的「计算器」。一个DSP48可以完成25x18位的乘法,还能级联做乘加运算。

做低延迟交易时,计算订单价格、风险敞口这些,我全用DSP48。比用LUT拼乘法器快一个数量级。

SerDes(串行收发器)—— 高速接口

SerDes是FPGA和外界通信的「高速公路」。现在的GTY收发器能跑到56Gbps,延迟只有几纳秒。

我曾经在项目中用SerDes直接连接交易所的10G网络,绕过了CPU和网卡,延迟从微秒级降到了纳秒级。这就是硬件加速的魅力。

Verilog基础语法:够用就行

我不喜欢讲太多语法细节。你想想看,写FPGA又不是写软件,核心就三个东西:组合逻辑、时序逻辑、状态机。

组合逻辑:assign和always@(*)

// 组合逻辑:用assign
assign sum = a + b;

// 组合逻辑:用always块
always @(*) begin
    if (sel)
        out = in1;
    else
        out = in2;
end
注意: always@(*)里必须把所有输入信号都列在敏感列表里。漏一个,综合出来就是锁存器。我吃过这个亏。

时序逻辑:always@(posedge clk)

// 时序逻辑:带同步复位
always @(posedge clk) begin
    if (rst)
        count <= 0;
    else
        count <= count + 1;
end

这里有个关键点:非阻塞赋值 <= 和阻塞赋值 = 的区别。我建议时序逻辑全用 <=,组合逻辑全用 =。混着用容易出问题。

状态机:三段式写法

// 状态编码
localparam IDLE = 2'b00;
localparam READ = 2'b01;
localparam WRITE = 2'b10;

// 第一段:状态转移
always @(posedge clk) begin
    if (rst) state <= IDLE;
    else state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
    case (state)
        IDLE: next_state = start ? READ : IDLE;
        READ: next_state = done ? IDLE : READ;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
assign busy = (state != IDLE);

三段式状态机是我最推荐的写法。可读性强,时序好收敛,综合工具也喜欢。

FPGA开发流程:从代码到比特流

整个流程说白了就三步:写代码、跑工具、调时序。但每一步都有坑。

  1. 综合(Synthesis):把Verilog转成网表。我习惯先看综合报告,检查有没有意外生成的锁存器。
  2. 布局布线(Place & Route):把网表映射到FPGA物理资源上。这一步最耗时,也最考验耐心。
  3. 时序收敛(Timing Closure):检查所有路径是否满足时序要求。不满足?回去改代码。

时序收敛:硬着头皮也要过

时序收敛是FPGA开发里最磨人的环节。说白了就是检查你的设计能不能跑在目标频率上。

我刚开始做设计时,总觉得时序分析可有可无。直到有一次,板子调通了但跑着跑着就出错。查了三天,发现是一条关键路径的建立时间违例了。嗯,从那以后我再也不敢跳过时序分析了。

核心概念:
  • 建立时间(Setup Time):数据必须在时钟沿之前稳定。违例了?降频或者加流水线。
  • 保持时间(Hold Time):数据必须在时钟沿之后保持稳定。违例了?加延迟或者调整布局。
  • 时钟偏斜(Clock Skew):时钟到达不同FF的时间差。用全局时钟资源可以缓解。

我的经验是:先看最差路径(Worst Negative Slack),从那里下手。通常加一级流水线就能解决大部分问题。

避坑指南:我踩过的那些雷

  • 时钟域跨越:不同时钟域的信号一定要同步。我见过有人直接连,结果数据全乱了。
  • 复位策略:尽量用同步复位,异步复位容易出毛刺。
  • 资源浪费:别把BRAM当寄存器用,也别用LUT拼乘法器。用对资源,事半功倍。

好了,FPGA基础就回顾到这里。这些内容看着简单,但每个点都能展开讲一节课。咱们后面会反复用到这些知识,到时候再细聊。

我的建议: 找个开发板,把今天讲的LUT、FF、BRAM、DSP都实际用一遍。光看不练,永远学不会。
专注资料整理