[IOMMU]基于 AMD IOMMU(AMD‑Vi/IOMMUv2)的系统化总结与落地方案
基于 AMD IOMMU(AMD‑Vi/IOMMUv2)的系统化总结与落地方案
摘要:基于 AMD IOMMU(AMD‑Vi/IOMMUv2)的系统化总结与落地方案,覆盖架构要点、SoC 验证方法(功能/异常/并发/性能/虚拟化/SVM)、覆盖指标,并给出一个可直接使用的 C 参考模型(AMD 风格:RID→DTE→x86‑64 I/O 页表,含 1G/2M/4K、RW/NX、可选 PASID、IOTLB、TLBI API)。模型可通过 DPI‑C 或 TLM 轻松接入 UVM 记分牌。
一、AMD IOMMU(AMD‑Vi/IOMMUv2)核心要点(验证所需视角)
-
目标与能力
- 隔离与保护:把设备 DMA 的 IOVA 翻译成物理地址(PA),并按 R/W/X 权限、内存属性执行访问控制。
- 进阶特性:
- IOMMUv2/SVM:设备与进程共享虚地址(PASID 标识进程);设备缺页通过 PPR(Page Request)上报,主机完成映射后重试。
- ATS/PRI:设备侧地址转换缓存(ATC)与缺页请求能力(PRI),减小缺页/行走延迟。
- 中断重映射:MSI/MSI‑X 重定向与隔离(SoC 上可映射到平台 GIC/PLIC 等)。
-
关键数据结构与硬件模块
- Device Table(设备表):内存中按 RID(B:D.F)索引的 DTE(Device Table Entry)。
- 常用字段(验证必测):V(有效)、TV(翻译使能)、DomainID(保护域标识)、PT Root(I/O 页表根地址,PML4 风格)、NX 控制、(v2)PASID 表基址等。
- PASID 表(IOMMUv2):按 PASID 查找每个进程上下文的 I/O 页表根或进程页表根(简化为根指针)。
- I/O 页表:x86‑64 风格 4 级(PML4→PDPT→PD→PT),叶子大小 1G/2M/4K。
- 位语义(精简):P(present)、RW(写使能)、PS(大页,在 PD/PT 上下一级条目)、NX(不可执行)。
- IOTLB/ATC:IOMMU 侧转换缓存与设备侧 ATS 缓存;需要一致性/失效协同。
- 命令/事件
- Command Ring:失效类命令(按域/按地址/全局)、同步(Completion Wait/CMD_SYNC)等。
- Event Log:翻译/权限/表项错误、队列错误等事件。
- PPR Log(v2):设备页请求/响应日志。
- Device Table(设备表):内存中按 RID(B:D.F)索引的 DTE(Device Table Entry)。
-
典型翻译路径(简化)
- 输入:RID、可选 PASID、IOVA、访问类型(读/写/执行标志)。
- 步骤:RID→DTE;若 v2 且 PASID!=0,用 PASID 表覆盖页表根;按 I/O 页表行走至叶子(1G/2M/4K);检查 RW/NX 权限;返回 PA 和“到页边界”的映射长度;缓存入 IOTLB。
-
失效与同步
- OS/驱动修改页表或域映射后,通过命令环发 IOTLB/ATC 失效命令;CMD_SYNC 之后才能观察到新映射(顺序性需在验证中断言)。
-
错误与上报(需覆盖)
- DTE invalid/TV=0、PTE not present、权限拒绝(RW/NX)、表项对齐/保留位错误、命令环溢出/门铃错误、事件/PPR 日志满与背压。
二、SoC 上的 IOMMU 验证方法(环境、要点、覆盖)
-
环境与观测点
- 事务源:PCIe/NOC/AXI 主设备 Agent,支持并发流(多 RID/SSID/PASID)、可选 ATS/PRI。
- 内存模型:可编程 DRAM/缓存一致性内存,暴露 IOMMU 设备表/页表/队列所在区域。
- IOMMU 寄存器/队列:MMIO Monitor(命令环、队列基址、门铃写)、内存写监控(Event/PPR 入队)。
- Scoreboard:基于 C 参考模型做期望翻译与错误预测;比对 IOMMU 出侧 PA 总线事务和事件日志。
-
验证要点(功能/异常/并发/性能)
-
翻译正确性
- 页大小:4K/2M/1G;跨页/边界对齐/非对齐突发;长突发分页切分。
- 权限:R/W/X 组合;NX 生效(执行请求被拒绝)。
- 域/上下文:不同 RID→不同域;同 RID 不同 PASID(v2)→不同页表根。
-
异常/错误路径
- DTE 未有效/TV=0、PASID miss、PTE not present、RW/NX 权限拒绝。
- 表项异常:未对齐、保留位错误、越界索引。
- 命令/事件:队列满、指针越界、门铃写丢、事件类型与字段编码正确。
-
失效与同步
- 修改页表后发 IOTLB 失效(按域/按地址/全局),在 CMD_SYNC 前后 DUT 行为差异(允许窗口 vs. 强一致)。
- 与设备 ATS/ATC 的协同失效(若实现):ATC invalidate 广播及时到达,IOMMU/IOTLB 一致。
-
并发/压力
- 多 RID/多 PASID 并发;翻译/失效/事件入队并行;PRIQ(页请求)与命令/事件同时高压。
- 复位与电源门控:IOMMU 局部复位、设备 FLR 后上下文恢复。
-
虚拟化/SVM(IOMMUv2)
- PASID 绑定/解绑/切换;设备缺页→PPR→主机映射→TLBI→重试成功。
-
性能/QoS
- IOTLB 命中率、页行走带宽、命令/事件队列利用率与背压;1G/2M 大页收益。
-
-
覆盖建议
- 结构覆盖:页表层级(PML4/PDPT/PD/PT)各层命中/穿透、PS 位大页覆盖、不同权限组合。
- 错误覆盖:各类 fault 码、事件子类型、PPR 场景、队列满/几近满。
- 并发覆盖:多流/多域/多 PASID 的交叉、失效与翻译并发交叠。
- 时序覆盖:CMD_SYNC 前后可见性、ATS/ATC 失效窗口。
-
记分牌工作流
- 入侧 Monitor 抽取 {RID,PASID,IOVA,len,is_write,need_exec}。
- 调用参考模型 translate 得到 {PA,len_mapped,fault};将长突发按 len_mapped 分段循环。
- 无 fault:与出侧 PA 事务一一比对;有 fault:核对 Event Log/PPR 条目与阻断行为。
- 捕获命令环失效:调用参考模型 TLBI API 保持一致。
三、参考 C 模型(AMD 风格,通用接口,可接 DPI/TLM)
3.1 说明
- 适用于验证记分牌作为黄金参考:RID→DTE→(可选 PASID)→x86‑64 I/O 页表行走(1G/2M/4K),RW/NX 权限检查;返回“至页边界”的映射长度;内置简易 IOTLB 与 TLBI 接口。
- 简化且贴近 AMD 语义:DTE 32B 条目(示例),包含 V/TV/PT Root/NX 与 PASID base;PTE 使用 x86‑64 常见位(P/RW/PS/NX);PASID 表条目 8B,提供 per‑PASID 根指针。
- 通过回调读取内存(read64),从而与 DUT 共享“同一份”表与队列内存镜像。
- 注意:具体位定义/对齐在不同版本 SoC/RTL 可能有所差异,务必按实际设计调整 read_dte()/read_pasid_root() 的位段。
// SPDX-License-Identifier: MIT
// AMD-like IOMMU reference C model for SoC verification (C99)
// Features:
// - RID->DTE lookup (32B entry), optional PASID table (8B entry)
// - x86-64 style 4-level I/O page tables: 1G/2M/4K
// - RW/NX permissions; returns partial length to page boundary
// - Tiny IOTLB + TLBI APIs
// - Pluggable 64-bit little-endian memory read callback#include <stdint.h>
#include <stdbool.h>
#include <string.h>// ------------ Memory read callback ------------
typedef bool (*iommu_mem_rd64_fn)(uint64_t addr, uint64_t* data, void* user);// ------------ Fault codes ------------
typedef enum {AMD_IOMMU_FAULT_NONE = 0,AMD_IOMMU_FAULT_CTX_INVALID = 1, // DTE invalid / TV=0 / PASID missAMD_IOMMU_FAULT_PTE_NOTPRESENT = 2,AMD_IOMMU_FAULT_PERMISSION = 3,AMD_IOMMU_FAULT_MEM_READ = 4,AMD_IOMMU_FAULT_ADDR_SIZE = 5
} amd_iommu_fault_e;// ------------ Inputs / Outputs ------------
typedef struct {uint16_t rid; // Requestor ID (B:D.F)uint32_t pasid; // 0 if noneuint64_t iova; // IO virtual addressuint32_t len; // request length (bytes)uint8_t is_write; // 1=write, 0=readuint8_t need_exec; // 1 if execute (X) required
} amd_iommu_req_t;typedef struct {uint64_t pa; // translated physical address (first chunk)uint32_t len_mapped; // bytes mapped until page boundary or req enduint32_t attrs; // opaque attributes (not modeled here)amd_iommu_fault_e fault;uint64_t fault_addr; // iova that faulted (best effort)uint8_t leaf_lvl; // 0:4K, 1:2M, 2:1G
} amd_iommu_resp_t;// ------------ Config / State ------------
typedef struct {uint64_t devtbl_base; // Device Table base (byte address)uint8_t levels; // 4 for 48-bit VAuint8_t v2_enable; // PASID table enableduint8_t nx_enable; // Enforce NX bit
} amd_iommu_cfg_t;typedef struct {amd_iommu_cfg_t cfg;iommu_mem_rd64_fn mem_rd;void* mem_user;
} amd_iommu_env_t;// ------------ Simplified DTE/PASID formats ------------
typedef struct {uint8_t valid; // Vuint8_t tv; // Translation validuint8_t nxe; // NX enforcement flag (optional)uint64_t pt_root; // I/O page table root (PML4)uint64_t pasid_base; // PASID table base (optional)
} amd_dte_t;static bool read_dte(const amd_iommu_env_t* env, uint16_t rid, amd_dte_t* dte) {const uint64_t addr = env->cfg.devtbl_base + ((uint64_t)rid << 5); // 32B/entryuint64_t lo=0, hi=0, ex0=0, ex1=0;if (!env->mem_rd(addr + 0, &lo, env->mem_user)) return false;if (!env->mem_rd(addr + 8, &hi, env->mem_user)) return false;if (!env->mem_rd(addr +16, &ex0, env->mem_user)) return false;if (!env->mem_rd(addr +24, &ex1, env->mem_user)) return false;dte->valid = (uint8_t)(lo & 0x1);dte->tv = (uint8_t)((lo >> 1) & 0x1);dte->pt_root = (lo & 0x000FFFFFFFFFF000ULL); // [51:12]dte->nxe = (uint8_t)((hi >> 31) & 0x1); // example bit usagedte->pasid_base = env->cfg.v2_enable ? (ex0 & 0x000FFFFFFFFFF000ULL) : 0ULL;return true;
}static bool read_pasid_root(const amd_iommu_env_t* env, uint64_t pasid_base, uint32_t pasid, uint64_t* root_out) {const uint64_t addr = pasid_base + ((uint64_t)pasid << 3); // 8B/entryuint64_t e=0;if (!env->mem_rd(addr, &e, env->mem_user)) return false;if ((e & 0x1ULL) == 0) return false; // invalid entry*root_out = (e & 0x000FFFFFFFFFF000ULL);return true;
}// ------------ x86-64 style PTE bits ------------
#define PTE_P (1ULL<<0) // Present
#define PTE_RW (1ULL<<1) // Write enable
#define PTE_PS (1ULL<<7) // Large page
#define PTE_NX (1ULL<<63) // No-Execute
static inline uint64_t pte_addr(uint64_t e) { return e & 0x000FFFFFFFFFF000ULL; }// ------------ IOTLB (tiny) ------------
#define IOTLB_SZ 1024u
typedef struct {uint8_t valid;uint16_t rid;uint32_t pasid;uint64_t va_tag; // page-aligned IOVAuint64_t pa_base; // page-aligned PAuint8_t leaf_lvl; // 0/1/2uint8_t perms; // bit0 R, bit1 W, bit2 Xuint32_t attrs;
} tlb_ent_t;typedef struct {amd_iommu_env_t env;tlb_ent_t tlb[IOTLB_SZ];
} amd_iommu_model_t;static inline uint32_t hash_key(uint16_t rid, uint32_t pasid, uint64_t va_tag) {uint64_t h = ((uint64_t)rid << 32) ^ ((uint64_t)pasid << 16) ^ (va_tag >> 12);h ^= (h >> 33) * 0xff51afd7ed558ccdULL;return (uint32_t)(h ^ (h >> 32));
}
static inline uint64_t lvl_pg_sz(uint8_t lvl) { return (lvl==2)?(1ULL<<30): (lvl==1)?(1ULL<<21):(1ULL<<12); }// ------------ Public APIs ------------
static inline void amd_iommu_init(amd_iommu_model_t* m,const amd_iommu_cfg_t* cfg,iommu_mem_rd64_fn mem_rd,void* mem_user) {memset(m, 0, sizeof(*m));m->env.cfg = *cfg;m->env.mem_rd = mem_rd;m->env.mem_user = mem_user;
}static inline void amd_iommu_tlbi_all(amd_iommu_model_t* m) {memset(m->tlb, 0, sizeof(m->tlb));
}
static inline void amd_iommu_tlbi_rid(amd_iommu_model_t* m, uint16_t rid) {for (unsigned i = 0; i < IOTLB_SZ; ++i)if (m->tlb[i].valid && m->tlb[i].rid == rid) m->tlb[i].valid = 0;
}
static inline void amd_iommu_tlbi_addr(amd_iommu_model_t* m, uint16_t rid, uint32_t pasid, uint64_t iova) {for (unsigned i = 0; i < IOTLB_SZ; ++i) {if (!m->tlb[i].valid) continue;if (m->tlb[i].rid == rid && m->tlb[i].pasid == pasid) {uint64_t pg_sz = lvl_pg_sz(m->tlb[i].leaf_lvl);uint64_t mask = pg_sz - 1ULL;if ((m->tlb[i].va_tag & ~mask) == (iova & ~mask)) m->tlb[i].valid = 0;}}
}// Translate one request; returns true if call succeeded; resp->fault indicates OK/fault
static inline bool amd_iommu_translate(amd_iommu_model_t* m,const amd_iommu_req_t* req,amd_iommu_resp_t* resp) {memset(resp, 0, sizeof(*resp));if (req->len == 0) return true;// IOTLB lookupconst uint64_t va = req->iova;const uint64_t va4k = va & ~0xFFFULL;uint32_t h = hash_key(req->rid, req->pasid, va4k);for (unsigned probe = 0; probe < 4; ++probe) {tlb_ent_t* e = &m->tlb[(h + probe) & (IOTLB_SZ-1)];if (e->valid && e->rid == req->rid && e->pasid == req->pasid) {uint64_t pg_sz = lvl_pg_sz(e->leaf_lvl);uint64_t mask = pg_sz - 1ULL;if ((va & ~mask) == e->va_tag) {if ((req->is_write && !(e->perms & 0x2)) ||(req->need_exec && !(e->perms & 0x4))) {resp->fault = AMD_IOMMU_FAULT_PERMISSION;resp->fault_addr = va;return true;}uint64_t off = va & mask;resp->pa = e->pa_base + off;uint32_t remain = (uint32_t)(pg_sz - off);resp->len_mapped = (req->len < remain) ? req->len : remain;resp->attrs = e->attrs;resp->leaf_lvl = e->leaf_lvl;resp->fault = AMD_IOMMU_FAULT_NONE;return true;}}}// DTE lookupamd_dte_t dte;if (!read_dte(&m->env, req->rid, &dte)) {resp->fault = AMD_IOMMU_FAULT_MEM_READ; resp->fault_addr = m->env.cfg.devtbl_base; return false;}if (!dte.valid || !dte.tv) {resp->fault = AMD_IOMMU_FAULT_CTX_INVALID; resp->fault_addr = va; return true;}// Resolve root (PASID override if enabled)uint64_t root = dte.pt_root;if (m->env.cfg.v2_enable && req->pasid != 0) {uint64_t pr = 0;if (!read_pasid_root(&m->env, dte.pasid_base, req->pasid, &pr)) {resp->fault = AMD_IOMMU_FAULT_CTX_INVALID; resp->fault_addr = va; return true;}root = pr;}// Page walk (up to 4 levels): PML4(3)->PDPT(2)->PD(1)->PT(0)const int top_lvl = (m->env.cfg.levels >= 4) ? 3 : (m->env.cfg.levels - 1);uint64_t idx[4] = {(va >> 12) & 0x1FFULL, // PT(va >> 21) & 0x1FFULL, // PD(va >> 30) & 0x1FFULL, // PDPT(va >> 39) & 0x1FFULL // PML4};uint64_t table = root;uint8_t leaf_lvl = 0;uint64_t leaf_base = 0;uint8_t perms = 0;uint32_t oattrs = 0;for (int lvl = top_lvl; lvl >= 0; --lvl) {uint64_t eaddr = table + (idx[lvl] << 3);uint64_t e = 0;if (!m->env.mem_rd(eaddr, &e, m->env.mem_user)) {resp->fault = AMD_IOMMU_FAULT_MEM_READ; resp->fault_addr = eaddr; return false;}if ((e & PTE_P) == 0) {resp->fault = AMD_IOMMU_FAULT_PTE_NOTPRESENT; resp->fault_addr = va; return true;}// Large page (PDPT/PD)if ((lvl == 2 || lvl == 1) && (e & PTE_PS)) {leaf_lvl = (lvl == 2) ? 2 : 1; // 2:1G, 1:2Mleaf_base = pte_addr(e);perms = 0x1; // Rif (e & PTE_RW) perms |= 0x2; // Wif (m->env.cfg.nx_enable) {if (!(e & PTE_NX)) perms |= 0x4; // X if NX==0} else perms |= 0x4; // X ignored -> allowbreak;}// Leaf at PTif (lvl == 0) {leaf_lvl = 0; // 4Kleaf_base = pte_addr(e);perms = 0x1;if (e & PTE_RW) perms |= 0x2;if (m->env.cfg.nx_enable) {if (!(e & PTE_NX)) perms |= 0x4;} else perms |= 0x4;break;}// Next leveltable = pte_addr(e);if ((table & 0xFFFULL) != 0) {resp->fault = AMD_IOMMU_FAULT_ADDR_SIZE; resp->fault_addr = va; return true;}}// Permissionif ((req->is_write && !(perms & 0x2)) || (req->need_exec && !(perms & 0x4))) {resp->fault = AMD_IOMMU_FAULT_PERMISSION; resp->fault_addr = va; return true;}// Compose responseconst uint64_t pg_sz = lvl_pg_sz(leaf_lvl);const uint64_t mask = pg_sz - 1ULL;const uint64_t off = va & mask;resp->pa = leaf_base + off;const uint32_t remain = (uint32_t)(pg_sz - off);resp->len_mapped = (req->len < remain) ? req->len : remain;resp->attrs = oattrs;resp->leaf_lvl = leaf_lvl;resp->fault = AMD_IOMMU_FAULT_NONE;// Fill IOTLBfor (unsigned probe = 0; probe < 4; ++probe) {tlb_ent_t* te = &m->tlb[(h + probe) & (IOTLB_SZ-1)];if (!te->valid) {te->valid = 1;te->rid = req->rid;te->pasid = req->pasid;te->va_tag = va & ~mask;te->pa_base = leaf_base;te->leaf_lvl= leaf_lvl;te->perms = perms;te->attrs = oattrs;break;}}return true;
}// ------------ Example RAM-backed memory provider (optional for unit tests) ------------
typedef struct {const uint8_t* base;uint64_t size;uint64_t offset; // map memory window base address
} amd_mem_blob_t;static bool amd_blob_rd64(uint64_t addr, uint64_t* data, void* user) {const amd_mem_blob_t* b = (const amd_mem_blob_t*)user;if (addr < b->offset || addr + 8 > b->offset + b->size) return false;uint64_t off = addr - b->offset;// little-endianconst uint8_t* p = b->base + off;*data = (uint64_t)p[0] | ((uint64_t)p[1]<<8) | ((uint64_t)p[2]<<16) | ((uint64_t)p[3]<<24) |((uint64_t)p[4]<<32) | ((uin