4、NTB配置与地址映射:NTB BAR空间配置、地址转换机制、内存窗口设置

好,咱们进入NTB最核心的部分——地址映射。说实话,很多工程师在PCIe Switch项目里栽跟头,十有八九都是这里没搞明白。NTB的地址映射,说白了就是让两个独立的主机域,通过Switch的NTB模块,互相看到对方的内存。

我刚开始接触NTB时,总觉得这东西很玄乎。后来亲手调过几个项目,踩过坑,才慢慢摸清门道。今天我把这些经验掰开揉碎了讲给你听。

4.1 NTB BAR空间配置——第一道关卡

NTB模块在PCIe枚举时,会向主机暴露一组BAR空间。这组BAR空间,就是主机访问NTB的“大门”。

每个NTB端口通常有2~4个BAR。我个人习惯把BAR0和BAR1留给控制寄存器,BAR2和BAR3留给数据窗口。为什么这么分?因为控制寄存器需要小空间、高频率访问,而数据窗口需要大块连续空间。

关键点:NTB的BAR空间大小,必须在硬件设计阶段就定好。一旦BIOS枚举完成,BAR大小就固定了。你没法在驱动里动态改。

举个例子,我做过一个项目,BAR2配置为1MB。结果对方主机需要映射2MB的DMA缓冲区。这就尴尬了——要么改硬件,要么拆成两个窗口。后来我学乖了,BAR空间尽量往大了配,反正PCIe地址空间够用。

BAR编号 典型用途 推荐大小 地址空间类型
BAR0 控制/状态寄存器 4KB ~ 64KB Memory-Mapped IO
BAR1 中断/门铃寄存器 4KB Memory-Mapped IO
BAR2 数据窗口0 1MB ~ 256MB Prefetchable Memory
BAR3 数据窗口1 1MB ~ 256MB Prefetchable Memory

小技巧:BAR空间尽量用Prefetchable类型。这样CPU可以缓存读取,性能会好不少。我见过有人用Non-Prefetchable,结果DMA吞吐量直接腰斩。

4.2 地址转换机制——NTB的灵魂

地址转换,是NTB最核心的机制。你想想看,主机A的物理地址0x1000,和主机B的物理地址0x1000,完全是两码事。NTB必须在这两个地址空间之间搭一座桥。

这座桥,就是地址转换表(Address Translation Table, ATT)。

NTB内部有一组硬件寄存器,用来描述“本地BAR地址”和“远端内存地址”之间的映射关系。当主机A往自己的NTB BAR里写数据时,NTB硬件自动把地址翻译成主机B的物理地址,然后把数据送过去。

这个转换过程是硬件完成的,零延迟。嗯,这里要注意——我说的零延迟是指没有软件开销,但PCIe总线本身的传输延迟还是有的。

我曾经在一个项目中,发现NTB传输总是不对。查了两天,最后发现是地址转换表配置错了——我把本地BAR的偏移地址当成了绝对地址。这个坑,我印象特别深。

警告:地址转换时,务必注意对齐要求。NTB通常要求本地地址和远端地址的偏移量对齐到4KB边界。如果不对齐,硬件会直接报错,或者静默地丢弃数据。

4.3 内存窗口设置——实战配置

内存窗口,就是NTB BAR里被映射到远端的那一块区域。每个NTB端口可以设置多个窗口,每个窗口对应一段连续的远端内存。

配置窗口时,你需要填写三个关键参数:

  • 本地基地址:NTB BAR空间内的偏移地址
  • 远端基地址:对方主机的物理地址
  • 窗口大小:映射的内存长度

下面是一个典型的配置流程。我习惯用伪代码来表示,这样更清晰:

// 步骤1:读取NTB能力寄存器,获取窗口数量
uint32_t cap = read_register(NTB_CAP_OFFSET);
int num_windows = (cap >> 16) & 0xFF;

// 步骤2:配置窗口0
// 本地BAR2的基地址为0x100000,映射到远端0x80000000
write_register(NTB_WIN0_LBASE, 0x100000);   // 本地偏移
write_register(NTB_WIN0_RBASE, 0x80000000); // 远端物理地址
write_register(NTB_WIN0_SIZE,  0x100000);   // 1MB窗口

// 步骤3:使能窗口
write_register(NTB_WIN0_CTRL, 0x1);         // 使能位

这里有个细节——远端基地址必须是对方主机实际可用的物理地址。我见过有人直接把0x0作为远端地址,结果触发了一堆PCIe错误。因为0x0通常是系统保留区,不能随便用。

我个人建议,远端内存窗口最好分配在对方主机的DMA缓冲区里。这样数据可以直接被DMA引擎搬走,不需要CPU介入。

4.4 地址映射的完整流程

为了让你看得更明白,我画了一张图,展示NTB地址映射的完整数据流:

主机A 主机A内存 物理地址: 0x1000~0x1FFF NTB BAR2 本地偏移: 0x0000~0xFFFF DMA引擎 PCIe Switch (NTB) 地址转换表 (ATT) 本地偏移 -> 远端物理地址 0x0000 -> 0x80000000 0x1000 -> 0x80001000 内部交叉互联 主机B NTB BAR2 本地偏移: 0x0000~0xFFFF 主机B内存 物理地址: 0x80000000~0x8000FFFF DMA引擎 数据流方向: 主机A -> 主机B 主机A写NTB BAR -> ATT转换 -> 内部互联 -> 主机B内存

这张图里,数据从主机A的DMA引擎出发,写入主机A的NTB BAR2。NTB硬件根据地址转换表,把本地偏移翻译成主机B的物理地址0x80000000。然后数据通过Switch内部互联,直接写入主机B的内存。整个过程不需要主机B的CPU参与。

避坑指南:我曾经遇到一个情况,主机A写NTB BAR后,主机B读到的数据全是乱的。查了半天,发现是地址转换表里的窗口大小配置错了——我配了1MB,但实际DMA传输了2MB。数据写到窗口边界外,就乱套了。记住:窗口大小必须大于等于实际传输的数据量。

4.5 多窗口与地址重叠

有些NTB芯片支持多个窗口。你可以把窗口0映射到主机B的DMA缓冲区,窗口1映射到主机B的控制结构体。这样隔离性好,不容易出错。

但要注意地址重叠问题。如果两个窗口的本地地址范围有重叠,NTB硬件的行为是未定义的。我见过一个芯片,重叠时直接死锁,整个Switch都挂了。所以配置窗口时,一定要检查地址范围是否冲突。

嗯,这里再补充一点——窗口的使能顺序也很重要。我建议先配置好所有窗口的基地址和大小,最后再统一使能。如果边配置边使能,中间状态可能会导致错误的地址转换。

好了,NTB的地址映射就讲到这里。这部分内容比较绕,但只要你理解了“本地BAR -> 地址转换表 -> 远端内存”这条主线,剩下的就是细节问题了。动手配置时,多看看芯片手册里的寄存器描述,每个位域的含义都要搞清楚。

专注资料整理