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 的完整映射流程:
从这张图里你能看到,设备 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 是房间。配好这张表,设备才能找到正确的翻译路径。