NTB硬件架构:从逻辑框图到实战细节

各位同学,今天我们来啃一块硬骨头——NTB的硬件架构。说实话,我第一次接触非透明桥时,也被那一堆地址映射和寄存器搞得头大。但别怕,咱们一步步拆解,你会发现它其实挺有章法的。

一、NTB内部逻辑框图

先看整体结构。NTB本质上是一颗特殊的PCIe Switch,但它内部多了一套“翻译官”逻辑。

Host A RC Host B RC NTB Switch Local Memory Window Remote Memory Window Doorbell Registers Scratchpad Registers DMA Engine PCIe PCIe

这张图我画得比较简洁,但核心模块都在了。你看,NTB内部主要分四块:地址映射窗口、Doorbell/Scratchpad寄存器、DMA引擎,以及连接它们的内部总线。我个人习惯把NTB想象成一个“智能路由器”——它不光转发数据,还能翻译地址、发送中断、搬运数据。

二、地址映射机制:Local与Remote Memory Window

这是NTB最核心的机制,也是很多人栽跟头的地方。

2.1 为什么需要地址映射?

你想想看,Host A和Host B各自有独立的物理地址空间。Host A的地址0x1000,和Host B的地址0x1000,根本就不是同一个地方。NTB要做的,就是让Host A能访问到Host B的内存,但地址得“翻译”一下。

核心概念:Local Memory Window是本地看到的窗口,Remote Memory Window是远端看到的窗口。两者通过NTB内部的地址转换逻辑一一对应。

2.2 地址映射实战配置

我在项目中遇到过这样一个场景:Host A想把一段数据发给Host B。配置步骤如下:

  1. 在Host A侧配置Local Memory Window:把本地一段物理地址(比如0xA000_0000~0xA000_FFFF)映射到NTB的Local Window。
  2. 在Host B侧配置Remote Memory Window:把NTB的Remote Window映射到Host B的物理地址空间(比如0xB000_0000~0xB000_FFFF)。
  3. 建立映射关系:NTB内部把Local Window和Remote Window绑定。Host A写0xA000_0000,数据就出现在Host B的0xB000_0000。
// 伪代码示例:配置地址映射
// Host A侧
ntb_local_window_setup(BASE_ADDR_A, 0xA000_0000, SIZE_64KB);
// Host B侧
ntb_remote_window_setup(BASE_ADDR_B, 0xB000_0000, SIZE_64KB);
// NTB内部绑定
ntb_bind_windows(BASE_ADDR_A, BASE_ADDR_B);

避坑指南:我曾经因为窗口大小没对齐,导致数据写到一半就卡死了。记住:Local和Remote Window的大小必须完全一致,而且起始地址要对齐到窗口大小的边界上。

三、Doorbell与Scratchpad寄存器

这两个寄存器组是NTB的“通信兵”。

3.1 Doorbell寄存器

Doorbell说白了就是“门铃”。Host A按一下门铃(写Doorbell寄存器),Host B就会收到一个中断。我习惯用Doorbell来做事件通知——比如“数据准备好了”、“缓冲区空了”之类的。

寄存器 偏移地址 位宽 描述
Doorbell Set 0x00 32位 写1触发中断,写0无效
Doorbell Clear 0x04 32位 写1清除中断状态
Doorbell Status 0x08 32位 读当前中断状态
Doorbell Mask 0x0C 32位 屏蔽某些位的中断

实战技巧:我一般把Doorbell的每个bit分配给不同的用途。比如bit0表示“数据就绪”,bit1表示“错误通知”,bit2表示“心跳检测”。这样通过读Status寄存器,一眼就能看出发生了什么事件。

3.2 Scratchpad寄存器

Scratchpad就是“便签纸”。Host A写一个值进去,Host B能读到。反过来也行。它不像Doorbell那样触发中断,纯粹是共享几个寄存器用来传小数据。

我记得有一次调试,两个CPU之间需要交换一个很小的配置参数。用DMA太浪费,用Doorbell又不需要中断。最后就是用Scratchpad搞定的——Host A把参数写进Scratchpad,Host B轮询读取,简单高效。

// Host A写入Scratchpad
ntb_scratchpad_write(0, 0x12345678);  // 写入Scratchpad[0]
ntb_scratchpad_write(1, 0xDEADBEEF);  // 写入Scratchpad[1]

// Host B读取Scratchpad
uint32_t val0 = ntb_scratchpad_read(0);  // 读到0x12345678
uint32_t val1 = ntb_scratchpad_read(1);  // 读到0xDEADBEEF

四、NTB的DMA引擎

终于讲到DMA了。说实话,没有DMA的NTB就像没有轮子的汽车——能跑,但效率太低。

4.1 DMA引擎的作用

NTB内置的DMA引擎,可以在不需要CPU干预的情况下,在Local和Remote之间搬运数据。你想想看,如果没有DMA,Host A要发1MB数据给Host B,CPU得一个一个字节地写,那得等到猴年马月?

4.2 DMA传输流程

  1. 配置源地址:Local侧的内存地址
  2. 配置目的地址:Remote侧的内存地址(经过地址映射)
  3. 配置传输长度:要传多少字节
  4. 启动传输:写DMA控制寄存器
  5. 等待完成:轮询状态寄存器或等中断
// DMA传输配置示例
dma_channel_t ch;
ch.src_addr = 0xA000_0000;      // Local地址
ch.dst_addr = 0xB000_0000;      // Remote地址(经过映射)
ch.length   = 1024 * 1024;      // 1MB
ch.dir      = DMA_LOCAL_TO_REMOTE;

ntb_dma_start(&ch);
while(!ntb_dma_is_done(&ch));   // 等待完成

性能要点:NTB的DMA引擎通常支持链式传输(descriptor chain),可以一次提交多个传输任务。我建议你尽量用链式传输,减少CPU的干预次数。另外,注意对齐——源地址、目的地址、长度最好都对齐到64字节,否则性能会打折扣。

4.3 DMA的两种模式

模式 描述 适用场景
Block模式 一次传输一整块数据 大块数据搬运,如文件传输
Scatter-Gather模式 不连续的物理地址拼成一次传输 网络包处理、虚拟化场景

我个人更常用Scatter-Gather模式。为什么呢?因为在实际系统中,内存往往是碎片化的。你申请一个1MB的缓冲区,物理上可能分散在好几个不连续的页里。Scatter-Gather模式正好解决这个问题——它用描述符链表把分散的物理地址串起来,DMA引擎自己会挨个处理。

曾经踩过的坑:有一次我配置DMA描述符时,忘了把最后一个描述符的“链结束”标志置位。结果DMA引擎一直等下一个描述符,整个传输就挂死了。嗯,这种低级错误犯过一次就再也不会忘了。

五、总结

NTB的硬件架构,说白了就是四个模块的协同工作:地址映射窗口负责翻译地址,Doorbell负责发中断通知,Scratchpad负责传小数据,DMA引擎负责搬大块数据。搞懂了这四块,NTB的基本原理你就掌握了八成。

下一节我们会深入地址映射的细节,包括如何配置多个窗口、如何处理地址冲突。但今天的内容已经够消化一阵子了——建议你找个开发板,亲手配一下Doorbell和Scratchpad,感受一下NTB的通信过程。


公众号:蓝海资料掘金营,微信deep3321