NTB硬件架构:从逻辑框图到实战细节
各位同学,今天我们来啃一块硬骨头——NTB的硬件架构。说实话,我第一次接触非透明桥时,也被那一堆地址映射和寄存器搞得头大。但别怕,咱们一步步拆解,你会发现它其实挺有章法的。
一、NTB内部逻辑框图
先看整体结构。NTB本质上是一颗特殊的PCIe Switch,但它内部多了一套“翻译官”逻辑。
这张图我画得比较简洁,但核心模块都在了。你看,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。配置步骤如下:
- 在Host A侧配置Local Memory Window:把本地一段物理地址(比如0xA000_0000~0xA000_FFFF)映射到NTB的Local Window。
- 在Host B侧配置Remote Memory Window:把NTB的Remote Window映射到Host B的物理地址空间(比如0xB000_0000~0xB000_FFFF)。
- 建立映射关系: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传输流程
- 配置源地址:Local侧的内存地址
- 配置目的地址:Remote侧的内存地址(经过地址映射)
- 配置传输长度:要传多少字节
- 启动传输:写DMA控制寄存器
- 等待完成:轮询状态寄存器或等中断
// 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