3、Stream Table 与 Stream ID 映射:从硬件表项到设备路由

好,咱们今天聊聊 SMMU 里一个非常核心的部件——Stream Table。说白了,它就是一张“设备路由表”。你想想看,系统里几十上百个设备,每个设备发起 DMA 请求时,SMMU 怎么知道该用哪个上下文(Context Bank)去翻译地址?靠的就是这张表。

我个人习惯把 Stream Table 理解成“门牌号系统”。每个设备有一个唯一的 Stream ID,就像门牌号。SMMU 拿到这个门牌号,去 Stream Table 里一查,就知道该带它去哪个房间(Context Bank)做地址翻译。

3.1 Stream Table 结构解析

Stream Table 在硬件上是一块连续的内存区域。它的基地址由 SMMU 的 SMMU_STRTAB_BASE 寄存器指定。每个表项(STE,Stream Table Entry)的大小是固定的,通常是 64 字节。

一个 STE 里到底放了什么?我挑几个关键字段说说:

字段 位宽 说明
Config 2 bits STE 的配置状态:0b00 表示无效(abort),0b01 表示直通(bypass),0b10 表示需要翻译(translate)
S1ContextPtr 52 bits 指向 Stage 1 的 Context Descriptor 的指针。说白了,就是指向具体那个 Context Bank 的基地址
S2ContextPtr 52 bits 指向 Stage 2 的 Context Descriptor 的指针。虚拟化场景下才用
STRW 2 bits 安全/非安全空间访问权限控制。0b00 表示只允许非安全访问,0b01 表示只允许安全访问
MTCFG 2 bits 内存类型配置,控制 cache 策略和一致性

关键点:STE 的 Config 字段决定了这个设备请求的命运。如果配成 0b00,SMMU 直接返回错误,DMA 请求被丢弃。如果配成 0b01,SMMU 不做任何翻译,直接放行。只有配成 0b10,才会走完整的地址翻译流程。

嗯,这里要注意。STE 的布局在不同 SMMU 版本里略有差异。ARM SMMUv2 和 SMMUv3 的 STE 结构就不完全一样。我当年在调试一个 FPGA 原型时,就踩过这个坑——固件里按 v2 的格式配了 STE,结果芯片是 v3 的核,SMMU 死活不认。后来查了一个下午才发现是版本不匹配。

3.2 Stream ID 到 Context Bank 的映射规则

设备发起 DMA 时,会带上自己的 Stream ID。SMMU 拿到这个 ID 后,怎么找到对应的 STE?

映射规则其实很简单:

STE 基地址 = SMMU_STRTAB_BASE + (Stream ID × STE 大小)

举个例子。假设 SMMU_STRTAB_BASE 是 0x1000_0000,STE 大小是 64 字节。那么:

  • Stream ID = 0 的设备,它的 STE 在 0x1000_0000
  • Stream ID = 1 的设备,它的 STE 在 0x1000_0040
  • Stream ID = 2 的设备,它的 STE 在 0x1000_0080

说白了,这就是一个线性数组。SMMU 硬件直接拿 Stream ID 做索引,去内存里取 STE。所以 Stream ID 的分配必须连续且紧凑,否则会浪费大量的 STE 空间。

我曾经踩过的坑:有一次,我把设备的 Stream ID 配成了 0x1000,结果 SMMU 去算 STE 地址时,直接越界到了 0x1000_0000 + 0x1000 × 64 = 0x1004_0000。那片内存区域根本没初始化,SMMU 读回来的全是 0,Config 字段是 0b00,所有 DMA 请求都被 abort 了。设备驱动报错说“DMA timeout”,我查了三天才定位到是 Stream ID 配大了。

所以,我建议你在设计固件时,给 Stream ID 分配一个明确的规划表。比如:

Stream ID 范围 用途 备注
0x0000 - 0x00FF 系统保留(SMMU 自身、中断控制器等) 通常 bypass 或 abort
0x0100 - 0x01FF 高速外设(GPU、Display、Camera) 需要独立 Context Bank
0x0200 - 0x02FF 低速外设(UART、I2C、SPI) 可以共享 Context Bank
0x0300 - 0x03FF 虚拟化设备(VM 直通设备) 需要 Stage 2 翻译

3.3 多设备共享 Context Bank 场景

你可能会问:每个设备都配一个独立的 Context Bank,是不是太浪费了?没错,Context Bank 的数量是有限的。SMMUv3 最多支持 64 个 Context Bank,但系统里的设备可能有上百个。怎么办?共享。

多设备共享 Context Bank,说白了就是多个 Stream ID 的 STE 指向同一个 S1ContextPtr。这样,这些设备共用同一套页表、同一个 ASID(Address Space ID)。

什么场景下适合共享?我总结了几种:

  • 同类型设备:比如多个 UART 控制器,它们访问的内存区域相同,权限也一样。共享一个 Context Bank 完全够用。
  • 非安全设备:不需要隔离的设备,比如一些调试接口、状态监控模块。
  • 低带宽设备:TLB 缺失频率低,共享 Context Bank 不会造成严重的 TLB 竞争。

我的建议:共享 Context Bank 时,一定要确保这些设备的地址空间不冲突。我曾经见过一个案例,两个设备共享同一个 Context Bank,结果它们的 DMA 地址范围有重叠。一个设备写数据时,把另一个设备的数据给覆盖了。排查了整整一周才发现是地址空间冲突。

下面这张图展示了 Stream ID 到 Context Bank 的完整映射流程:

Stream ID 到 Context Bank 映射流程 Stream ID 到 Context Bank 映射流程 设备 A Stream ID = 0x0100 设备 B Stream ID = 0x0101 设备 C Stream ID = 0x0200 Stream Table STE[0x0100] → CB #1 STE[0x0101] → CB #1 STE[0x0200] → CB #2 STE[0x0201] → Invalid Context Bank CB #1 设备 A + B 共享 CB #2 设备 C 独享 CB #3 未使用 说明: 设备 A 和设备 B 的 STE 都指向 CB #1,实现共享。设备 C 独享 CB #2。STE[0x0201] 配置为 Invalid,设备 D 的 DMA 请求会被 SMMU 直接 abort。

从这张图里你能看到,设备 A 和设备 B 的 Stream ID 不同,但它们的 STE 都指向 Context Bank #1。这就是共享。而设备 C 的 STE 指向 Context Bank #2,独享一个 bank。

共享 Context Bank 的好处是节省硬件资源。但代价是什么?TLB 竞争。多个设备共享同一个 Context Bank,意味着它们共用同一个 TLB。如果一个设备频繁触发 TLB 缺失,会把另一个设备的 TLB 项给挤出去。我在项目中就遇到过这种情况:GPU 和 Display 共享 Context Bank,结果 GPU 在做大量内存访问时,Display 的 TLB 项被频繁驱逐,导致显示画面出现撕裂。

避坑指南:如果你要共享 Context Bank,尽量把带宽需求相近、访问模式相似的设备放在一起。比如,把几个 UART 共享一个 bank,把几个 SPI 共享另一个 bank。别把高带宽设备和低带宽设备混在一起,否则 TLB 缺失率会飙升。

好了,Stream Table 和 Stream ID 映射这块,核心就是这些。记住:Stream Table 是 SMMU 的“门牌号系统”,Stream ID 是门牌号,Context Bank 是房间。配好这张表,设备才能找到正确的翻译路径。