第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模式使用
我的习惯:初始化时把所有STE的Valid位先清0,等设备真正使能时再逐个配置。这样能避免未初始化的STE被误命中,导致SMMU异常。

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 配置流程:从设备到页表

实际配置时,我一般按这个步骤走:

  1. 分配STE表:在内存中分配一块连续区域,大小取决于StreamID的位数
  2. 分配CD表:每个STE指向一个CD表,CD表大小取决于SubstreamID的位数
  3. 填充CD:把页表基地址、ASID、粒度等信息写入CD
  4. 填充STE:把CD表基地址、配置模式写入STE,最后置位Valid
  5. 刷新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等字段需要根据实际情况配置
}
我曾经踩过的坑:配置完STE后,一定要做一次TLB invalidation。否则SMMU可能还在用旧的缓存数据,新配置不会生效。我当时就是忘了这步,结果设备一直用旧页表访问内存,数据全乱了。

4.5 配置验证:怎么确认配对了?

配完STE和CD后,怎么知道对不对?我一般用这几个方法:

  • 读回验证:配置完后,把STE和CD的内容读回来,跟预期值比对
  • 触发事务:让设备发起一次DMA,观察SMMU是否正常翻译
  • 检查错误寄存器:如果配置有误,SMMU的GERROR或EVT寄存器会记录错误信息

我个人习惯在配置完成后,先读一次STE的Valid位,确认硬件已经识别到新配置。然后再让设备做一次小规模的DMA,比如读一个4KB的数据。如果成功了,说明配置没问题。

4.6 知识结构图:STE与CD的关系

下面这张图展示了从设备到页表的完整路径:

STE与CD配置关系图 PCIe设备 StreamID STE S1ContextPtr CD TTB0 页表 关键字段 • Valid: STE是否有效 • Config: 翻译模式 • S1ContextPtr: CD基地址 • ASID: 地址空间ID • TTB0: 页表基地址

从图里可以清楚看到:设备通过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的分配不能冲突
我的调试技巧:如果设备访问异常,先读STE的Valid位和Config位,确认硬件是否识别到配置。然后再读CD的ASID和TTB0,看页表地址对不对。这两步能解决80%的问题。