2. 内存层次结构基础:BRAM、URAM与外部DDR4/HBM

大家好,我是老张。今天我们来聊聊交易系统里最绕不开的话题——内存层次结构。说白了,就是你的数据到底该放哪儿,怎么放才能让FPGA跑得又快又稳。

我刚开始做交易系统那会儿,总觉得FPGA内部资源那么多,随便用用就行。结果有一次,一个简单的订单簿更新逻辑,因为BRAM用得太满,布局布线直接炸了。嗯,从那以后,我对内存层次结构就再也不敢马虎了。

2.1 FPGA内部存储:BRAM与URAM

FPGA内部最常用的存储资源,就是BRAM和URAM。它们俩的区别,我习惯用一个比喻:BRAM是便利店,URAM是大型超市。

  • BRAM(Block RAM):每个块18Kb或36Kb,读写速度快,延迟低。适合存小批量、高频访问的数据,比如订单簿的头部信息。
  • URAM(UltraRAM):每个块288Kb,容量大,但延迟稍高。适合存大批量、访问频率稍低的数据,比如历史行情快照。

我个人习惯,把最热的数据放BRAM,次热的数据放URAM。你想想看,如果订单簿的买卖盘口数据都放URAM,每次更新都要多等几个周期,那延迟就上去了。

核心原则: 热数据放BRAM,温数据放URAM,冷数据放外部DDR。

2.1.1 BRAM的使用技巧

BRAM支持真双口模式,可以同时读写。我在项目中遇到过一个问题:两个模块同时读同一个BRAM地址,结果读到了旧数据。后来我加了一个乒乓缓冲,才彻底解决。

// 真双口BRAM示例
module bram_dual_port (
    input clk,
    input we_a, we_b,
    input [9:0] addr_a, addr_b,
    input [31:0] din_a, din_b,
    output reg [31:0] dout_a, dout_b
);
    reg [31:0] mem [0:1023];
    
    always @(posedge clk) begin
        if (we_a) mem[addr_a] <= din_a;
        dout_a <= mem[addr_a];
    end
    
    always @(posedge clk) begin
        if (we_b) mem[addr_b] <= din_b;
        dout_b <= mem[addr_b];
    end
endmodule
避坑指南: 我曾经在BRAM的写使能信号上犯过错误——写使能只拉高了一个周期,但数据还没稳定。后来我加了一级寄存器延迟,问题就解决了。

2.1.2 URAM的使用场景

URAM的容量是BRAM的8倍,但延迟也多了1-2个周期。我建议用它来存深度订单簿的快照数据。比如,你要存1000档的买卖盘口,每档64字节,用BRAM得几百个块,用URAM十几个块就够了。

资源类型 容量 延迟 典型用途
BRAM 18Kb / 36Kb 1-2周期 订单簿头部、缓存行
URAM 288Kb 2-3周期 深度快照、历史数据

2.2 外部DDR4与HBM接口

当内部存储不够用时,就得用外部DDR4或HBM。DDR4延迟高,但容量大;HBM带宽高,但成本也高。

我做过一个项目,需要实时处理每秒100万笔订单。DDR4的带宽根本扛不住,后来换成了HBM,带宽直接翻了4倍。嗯,这里要注意:HBM的控制器设计比DDR4复杂得多,建议直接用Xilinx的IP核。

2.2.1 DDR4接口设计要点

  • 突发长度: 建议用8或16,减少地址切换开销。
  • Bank分组: 不同数据流用不同Bank,避免冲突。
  • 读写分离: 读和写用不同的FIFO,防止死锁。
警告: 我曾经在DDR4的时序约束上吃过亏——没加足够的时钟相位偏移,结果数据采样不稳定。建议用Vivado的时序分析工具跑一遍,确保setup/hold时间都满足。

2.2.2 HBM的高带宽优势

HBM的带宽可以达到DDR4的4-8倍,但它的伪通道设计需要特别注意。每个伪通道独立工作,但地址空间是连续的。我建议把不同数据流映射到不同伪通道,最大化并行度。

// HBM伪通道映射示例
// 伪通道0: 订单簿数据
// 伪通道1: 行情快照
// 伪通道2: 日志数据
// 伪通道3: 备用

assign hbm_addr = {pseudo_channel, row_addr, col_addr};

2.3 缓存一致性基础

缓存一致性,说白了就是多个模块看到的数据要一样。在FPGA里,这个问题比CPU更棘手,因为FPGA没有硬件缓存一致性协议。

我常用的方法有三种:

  1. 写直达(Write-Through): 每次写都同时更新缓存和主存。简单,但带宽浪费。
  2. 写回(Write-Back): 只在缓存满时才写回主存。高效,但需要脏数据标记。
  3. 无效化(Invalidation): 其他模块写数据时,通知缓存失效。复杂,但最灵活。
我的经验: 在交易系统里,我一般用写回+无效化的组合。写回减少带宽,无效化保证一致性。但要注意,无效化信号必须在一个时钟周期内广播到所有缓存。

2.3.1 缓存行对齐

缓存行的大小,我建议用64字节或128字节。太小了,缓存命中率低;太大了,带宽浪费。我习惯把订单簿的每个条目对齐到缓存行边界,这样一次突发读就能拿到完整数据。

// 缓存行对齐示例
typedef struct packed {
    logic [63:0] price;
    logic [31:0] volume;
    logic [31:0] order_id;
} order_entry_t;

// 每个条目64字节,对齐到缓存行
localparam CACHE_LINE_SIZE = 64;
localparam NUM_ENTRIES = 1024;

order_entry_t order_book [0:NUM_ENTRIES-1];

2.3.2 多端口访问冲突

多个模块同时访问同一个缓存行时,就会发生冲突。我建议用仲裁器,按优先级分配访问权。优先级高的模块(比如订单匹配引擎)可以抢占,优先级低的模块(比如日志记录)需要等待。

小技巧: 我曾经用Round-Robin仲裁器,结果低优先级模块一直抢不到资源。后来改成加权轮询,给每个模块分配固定带宽,问题就解决了。

2.4 知识体系结构图

下面这张图,是我自己总结的内存层次结构。你可以看到,从BRAM到HBM,延迟越来越高,容量越来越大。关键是要找到平衡点。

FPGA内存层次结构 BRAM (Block RAM) 容量: 18Kb-36Kb/块 | 延迟: 1-2周期 | 用途: 热数据、订单簿头部 URAM (UltraRAM) 容量: 288Kb/块 | 延迟: 2-3周期 | 用途: 温数据、深度快照 DDR4 SDRAM 容量: 4GB-16GB | 延迟: 50-100周期 | 用途: 冷数据、历史行情 HBM (High Bandwidth Memory) 容量: 4GB-8GB | 延迟: 30-50周期 | 用途: 高带宽、实时数据 延迟↑ 容量↑

这张图从左到右,从上到下,展示了数据从热到冷的流动路径。我个人习惯,把最常访问的数据放在BRAM,次常用的放URAM,历史数据放DDR4,实时流数据放HBM。

总结一下: 内存层次结构的关键,就是让数据待在它该待的地方。热数据放BRAM,温数据放URAM,冷数据放DDR4,高带宽需求放HBM。缓存一致性,用写回+无效化组合最稳妥。

专注资料整理