4. BAR空间映射与访问:ioremap、pci_ioremap_bar、读写BAR寄存器、内存屏障
PCIe设备的基地址寄存器(BAR,Base Address Register)是CPU访问设备内部寄存器、FIFO或本地内存的唯一窗口。驱动必须将BAR所指示的物理地址映射到内核虚拟地址空间,才能通过指针直接读写。本章深入讲解BAR映射机制、标准API的使用、寄存器访问方法以及内存屏障的必要性。
核心要点:BAR空间映射的本质是“物理地址 → 内核虚拟地址”的转换,配合内存屏障保证访问顺序与硬件行为一致。
4.1 BAR空间与物理地址获取
每个PCIe功能最多支持6个BAR(Type 0)或2个BAR(Type 1)。BAR在设备枚举阶段由BIOS或内核分配物理地址,驱动通过pci_resource_start()和pci_resource_len()获取地址与长度。
| API函数 | 功能 | 返回值 |
|---|---|---|
pci_resource_start(pdev, bar) |
获取BAR的物理起始地址 | resource_size_t(物理地址) |
pci_resource_len(pdev, bar) |
获取BAR的地址空间长度 | resource_size_t(字节数) |
pci_resource_flags(pdev, bar) |
获取BAR的类型标志(IO/MEM/64位) | unsigned long |
判断BAR类型:使用
if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) 判断是否为内存空间;IORESOURCE_IO 表示IO空间。现代PCIe设备几乎全部使用内存空间映射。
4.2 ioremap 与 pci_ioremap_bar
获取物理地址后,必须通过ioremap系列函数建立页表映射,使CPU能够访问设备内存。内核提供两种方式:
4.2.1 传统 ioremap
void __iomem *base;
unsigned long phys_addr = pci_resource_start(pdev, 0);
unsigned long size = pci_resource_len(pdev, 0);
base = ioremap(phys_addr, size);
if (!base) {
dev_err(&pdev->dev, "ioremap failed\n");
return -ENOMEM;
}
4.2.2 推荐 pci_ioremap_bar
void __iomem *base;
base = pci_ioremap_bar(pdev, 0); // 映射BAR0
if (!base) {
dev_err(&pdev->dev, "pci_ioremap_bar failed\n");
return -ENOMEM;
}
pci_ioremap_bar 优势:内部自动调用
pci_resource_start 和 ioremap,并检查BAR类型是否为MEM空间,代码更简洁、更安全。
4.3 BAR空间映射流程(SVG架构图)
4.4 读写BAR寄存器
映射完成后,使用内核提供的访问函数读写设备寄存器。这些函数确保正确的字节序和访问宽度。
| 函数 | 宽度 | 示例 |
|---|---|---|
readb() / writeb() |
8位 | val = readb(base + offset); |
readw() / writew() |
16位 | writew(0x1234, base + 0x10); |
readl() / writel() |
32位 | writel(cmd, base + REG_CTRL); |
readq() / writeq() |
64位 | val = readq(base + 0x20); |
// 典型寄存器访问示例
#define REG_STATUS 0x00
#define REG_CTRL 0x04
#define REG_DATA 0x10
void __iomem *bar0_base;
// 读状态寄存器
u32 status = readl(bar0_base + REG_STATUS);
// 写控制寄存器:启动DMA传输
writel(0x1, bar0_base + REG_CTRL);
// 写数据寄存器
writel(tx_data, bar0_base + REG_DATA);
注意:不要直接对
__iomem * 指针使用C语言的 * 解引用或结构体强制转换。必须使用 readl/writel 系列函数,这些函数在ARM/x86/RISC-V等架构上会插入必要的内存屏障。
4.5 内存屏障(Memory Barrier)
CPU和编译器可能对内存访问进行重排序,导致设备看到的访问顺序与代码顺序不一致。内存屏障强制保证访问顺序。
4.5.1 为什么需要内存屏障
- 编译器优化:编译器可能将连续的writel重新排序
- CPU乱序执行:现代CPU可能乱序发出内存事务
- 写缓冲合并:CPU可能将多次写操作合并为一次
- PCIe桥缓冲:桥接器可能重新排序TLP
4.5.2 内核提供的屏障函数
| 屏障函数 | 作用范围 | 说明 |
|---|---|---|
wmb() |
写操作 | 保证之前的所有写操作在之后的写操作之前完成 |
rmb() |
读操作 | 保证之前的所有读操作在之后的读操作之前完成 |
mb() |
读写操作 | 同时保证读写顺序 |
mmiowb() |
MMIO写 | 保证MMIO写操作顺序(某些架构需要) |
4.5.3 典型使用场景
// 场景:先写数据到FIFO,再写启动寄存器
writel(data0, bar0_base + FIFO_DATA);
writel(data1, bar0_base + FIFO_DATA);
writel(data2, bar0_base + FIFO_DATA);
// 必须保证所有数据写入完成后再启动DMA
wmb(); // 写内存屏障
writel(0x1, bar0_base + DMA_START); // 启动DMA
// 读状态前确保之前的写操作已到达设备
writel(CMD_QUERY, bar0_base + REG_CMD);
wmb(); // 确保命令写完成
status = readl(bar0_base + REG_STATUS);
最佳实践:在每次
writel 启动硬件操作(如DMA、中断使能)之前,插入 wmb()。在读取设备状态寄存器之前,如果之前有写操作,也需要 wmb() 或 mb()。
4.6 解除映射与错误处理
在驱动卸载或错误路径中,必须使用iounmap()解除映射,否则会导致内核虚拟地址泄漏。
static void my_pci_remove(struct pci_dev *pdev)
{
// 1. 停止硬件操作
writel(0x0, bar0_base + REG_CTRL);
wmb();
// 2. 解除BAR映射
if (bar0_base) {
iounmap(bar0_base);
bar0_base = NULL;
}
// 3. 释放PCI资源
pci_release_region(pdev, 0);
pci_disable_device(pdev);
}
static int my_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
int ret;
ret = pci_enable_device(pdev);
if (ret) return ret;
ret = pci_request_region(pdev, 0, "mydev_bar0");
if (ret) goto err_disable;
bar0_base = pci_ioremap_bar(pdev, 0);
if (!bar0_base) {
ret = -ENOMEM;
goto err_release;
}
// ... 初始化硬件 ...
return 0;
err_release:
pci_release_region(pdev, 0);
err_disable:
pci_disable_device(pdev);
return ret;
}
4.7 常见错误与避坑指南
| 错误场景 | 后果 | 正确做法 |
|---|---|---|
直接解引用 __iomem * |
Sparse警告,ARM上可能触发异常 | 始终使用 readl/writel |
| 忘记内存屏障 | 设备读到旧数据或顺序错乱 | 在关键写操作后加 wmb() |
| 映射后未检查返回值 | NULL指针解引用导致内核崩溃 | 始终检查 ioremap 返回值 |
| 解除映射顺序错误 | 设备仍在访问已释放的内存 | 先停止硬件,再iounmap |
使用 ioremap 映射IO空间 |
映射失败或行为未定义 | 使用 pci_ioremap_bar 自动判断 |
调试技巧:使用
sparse 工具(make C=1)检查 __iomem 注解是否正确。使用 devmem 工具在用户空间验证寄存器值是否与驱动读取一致。
4.8 本章小结
- BAR映射三步曲:获取物理地址 → ioremap → 使用readl/writel访问
- 推荐API:
pci_ioremap_bar()比手动ioremap更安全简洁 - 内存屏障:在启动硬件操作的写操作后必须加
wmb() - 错误路径:确保所有映射在remove或错误时被正确解除
- 访问函数:禁止直接解引用
__iomem指针