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,每次更新都要多等几个周期,那延迟就上去了。
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
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,防止死锁。
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没有硬件缓存一致性协议。
我常用的方法有三种:
- 写直达(Write-Through): 每次写都同时更新缓存和主存。简单,但带宽浪费。
- 写回(Write-Back): 只在缓存满时才写回主存。高效,但需要脏数据标记。
- 无效化(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 多端口访问冲突
多个模块同时访问同一个缓存行时,就会发生冲突。我建议用仲裁器,按优先级分配访问权。优先级高的模块(比如订单匹配引擎)可以抢占,优先级低的模块(比如日志记录)需要等待。
2.4 知识体系结构图
下面这张图,是我自己总结的内存层次结构。你可以看到,从BRAM到HBM,延迟越来越高,容量越来越大。关键是要找到平衡点。
这张图从左到右,从上到下,展示了数据从热到冷的流动路径。我个人习惯,把最常访问的数据放在BRAM,次常用的放URAM,历史数据放DDR4,实时流数据放HBM。