第4章:SMMU页表配置:STE与CD配置实战
好,咱们进入正题。上一章我们把SMMU的地址空间和页表格式理清了,这一章要解决一个更实际的问题——怎么让SMMU知道“这个设备用哪个页表”?
答案就在两个关键数据结构里:STE(Stream Table Entry)和CD(Context Descriptor)。说白了,STE是设备到配置的“门牌号”,CD是具体的“房间布局”。
我刚开始调SMMU时,总觉得STE和CD是软件随便填的。直到有一次,一个PCIe设备死活访问不了内存,我查了三天,最后发现是STE里的S1DID字段配错了。嗯,从那以后我再也不敢小看这两个结构体了。
4.1 STE:设备的“身份证”
每个连接到SMMU的设备,都有一个对应的STE。设备发起事务时,SMMU根据StreamID找到STE,然后从STE里读出后续的配置信息。
STE的结构很丰富,但咱们重点关注这几个字段:
| 字段 | 位域 | 说明 |
|---|---|---|
| Valid | [0] | STE是否有效。无效时SMMU直接报错 |
| Config | [1:0] | 配置模式:00=禁止,01=Stage1,10=Stage2,11=Stage1+Stage2 |
| S1DID | [11:2] | Stage1的SubstreamID,用于选择CD |
| S1ContextPtr | [63:12] | 指向CD表的基地址,必须4KB对齐 |
| S2TTB | [63:12] | Stage2页表基地址,仅Stage2模式使用 |
4.2 CD:页表的“入口”
CD是STE指向的下一级结构。每个CD对应一个独立的地址空间,也就是一个进程或一个虚拟机的页表配置。
CD里最重要的字段是:
- ASID:地址空间ID,TLB查找时用来区分不同进程
- TTB0/TTB1:页表基地址,分别对应0地址空间和1地址空间
- TG0/TG1:页面粒度,支持4KB、16KB、64KB
- SH0/SH1:共享属性,决定页表是否被多核共享
- ORGN0/ORGN1:外设缓存策略
- IRGN0/IRGN1:内部缓存策略
你想想看,一个CD就定义了一个完整的地址翻译路径。从页表基地址到页面粒度,再到缓存策略,全在这里了。
4.3 配置流程:从设备到页表
实际配置时,我一般按这个步骤走:
- 分配STE表:在内存中分配一块连续区域,大小取决于StreamID的位数
- 分配CD表:每个STE指向一个CD表,CD表大小取决于SubstreamID的位数
- 填充CD:把页表基地址、ASID、粒度等信息写入CD
- 填充STE:把CD表基地址、配置模式写入STE,最后置位Valid
- 刷新TLB:通知SMMU配置已更新,让硬件重新加载
关键点:配置顺序很重要!必须先配CD,再配STE。如果STE先生效,但CD还没准备好,SMMU读到无效的CD会直接触发异常。
4.4 代码示例:STE与CD配置
下面是一个简化的配置代码,我用C语言风格写,方便你理解:
// 定义STE结构体
typedef struct {
uint32_t word0; // Valid, Config, S1DID等
uint32_t word1; // S1ContextPtr低32位
uint32_t word2; // S1ContextPtr高32位
uint32_t word3; // S2TTB等
} ste_t;
// 定义CD结构体
typedef struct {
uint32_t word0; // ASID, TTB0低32位
uint32_t word1; // TTB0高32位
uint32_t word2; // TG0, SH0, ORGN0, IRGN0
uint32_t word3; // TTB1, TG1等
} cd_t;
// 配置CD
void configure_cd(cd_t *cd, uint64_t ttb0, uint16_t asid) {
cd->word0 = (asid & 0xFFFF) | ((ttb0 & 0xFFFFFFFF) << 16);
cd->word1 = (ttb0 >> 32) & 0xFFFFFFFF;
cd->word2 = (TG_4KB << 0) | (SH_ISH << 2) | (CACHE_WB << 4);
// 注意:这里省略了其他字段的配置
}
// 配置STE
void configure_ste(ste_t *ste, uint64_t cd_base, uint8_t config) {
ste->word0 = (1 << 0) | // Valid
((config & 0x3) << 1); // Config
ste->word1 = cd_base & 0xFFFFFFFF;
ste->word2 = (cd_base >> 32) & 0xFFFFFFFF;
// 注意:S1DID等字段需要根据实际情况配置
}
4.5 配置验证:怎么确认配对了?
配完STE和CD后,怎么知道对不对?我一般用这几个方法:
- 读回验证:配置完后,把STE和CD的内容读回来,跟预期值比对
- 触发事务:让设备发起一次DMA,观察SMMU是否正常翻译
- 检查错误寄存器:如果配置有误,SMMU的GERROR或EVT寄存器会记录错误信息
我个人习惯在配置完成后,先读一次STE的Valid位,确认硬件已经识别到新配置。然后再让设备做一次小规模的DMA,比如读一个4KB的数据。如果成功了,说明配置没问题。
4.6 知识结构图:STE与CD的关系
下面这张图展示了从设备到页表的完整路径:
从图里可以清楚看到:设备通过StreamID找到STE,STE通过S1ContextPtr找到CD,CD再通过TTB0找到页表。每一层都是指针关系,环环相扣。
4.7 避坑指南
最后,分享几个我实际调试中遇到的坑:
- 对齐问题:STE和CD的基地址必须4KB对齐,否则SMMU会忽略低12位。我见过有人配了非对齐地址,结果SMMU读到的全是错的
- 大小端:ARM默认小端,但有些SoC会改。配置前先确认SMMU的字节序,否则字段会错位
- 缓存一致性:如果STE或CD在缓存里,SMMU可能读到旧数据。配置完后记得做cache clean
- 多设备共享:多个设备可以共享同一个STE,但要注意StreamID的分配不能冲突