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的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 -> 地址转换表 -> 远端内存”这条主线,剩下的就是细节问题了。动手配置时,多看看芯片手册里的寄存器描述,每个位域的含义都要搞清楚。