目录
五、第二种:MMIO 空间(Memory-Mapped I/O)
1️⃣ MMIO 是什么?
2️⃣ MMIO 和配置空间的本质区别
3️⃣ MMIO 从哪里来?
4️⃣ CPU 眼中的 MMIO
5️⃣ Linux 驱动如何访问 MMIO?
✅ 第一步:获取 BAR 物理地址
✅ 第二步:建立内核虚拟映射
✅ 第三步:访问设备寄存器
6️⃣ 为什么 MMIO 不能是普通指针?
① 内存序问题(Ordering)
② 端序问题(Endianness)
③ MMIO 没有“内存语义”
7️⃣ Non-Prefetchable vs Prefetchable(再强调)
✅ Non-Prefetchable(控制寄存器)
✅ Prefetchable(数据缓冲区)
8️⃣ 一个真实驱动示例(RTL8169 简化版)
9️⃣ MMIO 常见坑点(工程师必看)
❌ 坑 1:忘了开 Memory Space
❌ 坑 2:用普通指针访问 MMIO
❌ 坑 3:把 Prefetchable 当成性能优化
🔟 一句话总结(本节约核心)
六、工程师小贴士(强烈建议加框)
七、本篇结构回顾(第 7 篇)
五、第二种:MMIO 空间(Memory-Mapped I/O)
1️⃣ MMIO 是什么?
MMIO = Memory-Mapped I/O(内存映射 I/O)
一句话定义:
MMIO 是把“设备内部寄存器”映射到 CPU 物理地址空间的一种机制。
对 CPU 来说:
MMIO 地址看起来和普通内存一模一样
可以用
load / store指令访问编译器、Cache、流水线都“以为”它在访问内存
但实际上:
❗这不是内存,这是设备
📌MMIO = “伪装成内存的设备寄存器”
2️⃣ MMIO 和配置空间的本质区别
对比项 | 配置空间 | MMIO |
|---|---|---|
目的 | 管理设备 | 操作设备 |
访问方式 | 专用机制(CF8/CFC / ECAM) | CPU load/store |
是否在 CPU 地址空间 | 否(ECAM 除外) | ✅ 是 |
是否可被 C 语言直接访问 | ❌ | ✅ |
驱动核心路径 | 否 | ✅ |
是否可预取 | ❌ | 部分可 |
是否高频访问 | ❌ | ✅ |
📌配置空间是“行政楼”,MMIO 是“生产车间”
3️⃣ MMIO 从哪里来?
答案只有两个字:
BAR
我们在第 6 篇讲过:
设备通过 BAR 向系统“申请地址窗口”
系统批准后,把这段地址写入 BAR
这段地址,就是 MMIO 空间
BAR0 = 0xF7100000 ↓ CPU 访问 0xF7100000 ↓ RC 转换为 PCIe TLP ↓ Endpoint BAR Hit ↓ 访问设备寄存器📌MMIO = BAR 映射后的 CPU 地址
4️⃣ CPU 眼中的 MMIO
从 CPU 视角看:
volatile uint32_t *reg = (uint32_t *)0xF7100000; uint32_t val = reg[0]; reg[1] = 0x12345678;但内核里不能这么干,必须用内核 API。
5️⃣ Linux 驱动如何访问 MMIO?
✅ 第一步:获取 BAR 物理地址
resource_size_t bar_phy = pci_resource_start(pdev, 0); // BAR0 resource_size_t bar_len = pci_resource_len(pdev, 0);✅ 第二步:建立内核虚拟映射
void __iomem *regs; regs = pci_iomap(pdev, 0, 0); if (!regs) return -ENOMEM;📌pci_iomap()内部会调用ioremap()
✅ 第三步:访问设备寄存器
u32 val; val = readl(regs + 0x10); // 读寄存器 writel(0x1, regs + 0x10); // 写寄存器⚠️ 严禁:
*((u32 *)(regs + 0x10)) = 0x1; // ❌ 错误📌必须使用readl() / writel(),保证内存序和端序
6️⃣ 为什么 MMIO 不能是普通指针?
原因有三:
① 内存序问题(Ordering)
CPU 可能乱序执行
编译器可能重排访问
PCIe 对访问顺序敏感
readl() / writel()会插入屏障:
mb();② 端序问题(Endianness)
PCIe 是小端
某些 CPU 是大端
访问接口会处理转换
③ MMIO 没有“内存语义”
读可能改变状态
写可能有副作用
不能被 Cache / 预测执行
📌MMIO 是“有副作用的 I/O”,不是数据
7️⃣ Non-Prefetchable vs Prefetchable(再强调)
✅ Non-Prefetchable(控制寄存器)
每次访问必须真实发生
禁止预取、合并、重排
所有控制寄存器都必须用这个
Region 2: Memory at f7100000 (non-prefetchable)✅ Prefetchable(数据缓冲区)
可预读
可合并写
适合 DMA / Framebuffer
Region 4: Memory at f7000000 (prefetchable)📌Prefetchable ≠ 更快,而是“更安全的数据访问模型”
8️⃣ 一个真实驱动示例(RTL8169 简化版)
struct rtl8169_priv { void __iomem *mmio_addr; }; static int rtl8169_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct rtl8169_priv *priv; int ret; pci_enable_device(pdev); priv = kzalloc(sizeof(*priv), GFP_KERNEL); priv->mmio_addr = pci_iomap(pdev, 2, 0); // BAR2 /* 写 MAC 地址 */ writel(0x12345678, priv->mmio_addr + MAC_ADDR_REG); /* 使能 DMA */ writel(DMA_EN, priv->mmio_addr + DMA_CTRL); return 0; }📌所有“设备操作”,都发生在 MMIO 空间
9️⃣ MMIO 常见坑点(工程师必看)
❌ 坑 1:忘了开 Memory Space
Control: Mem- BusMaster+结果:
BAR 映射成功
访问直接异常
✅ 解决:
setpci -s xx:xx.x COMMAND=0x06❌ 坑 2:用普通指针访问 MMIO
结果:
偶发错误
多平台不兼容
被编译器优化掉
✅ 解决:
永远用
readl() / writel()
❌ 坑 3:把 Prefetchable 当成性能优化
结果:
控制寄存器用 Prefetchable
设备行为异常
✅ 解决:
控制寄存器 = Non-Prefetchable
数据区 = Prefetchable
🔟 一句话总结(本节约核心)
**MMIO 是 PCIe 设备寄存器的“主战场”,
它通过 BAR 映射到 CPU 地址空间,
驱动通过
readl() / writel()与设备对话。**
六、工程师小贴士(强烈建议加框)
⚠️ 调试 PCIe 驱动时,请记住这条铁律:
**“所有‘设备为什么不工作’的问题,
先确认 MMIO 能不能稳定读写。”**
方法:
devmem
lspci -vv
dmesg | grep ioremap
七、本篇结构回顾(第 7 篇)
一、三种地址空间总览 二、配置空间(管理面) ↓ 三、MMIO 空间(数据面 / 本节) ↓ 四、I/O 空间(历史包袱 / 下节)