核心概念:为什么需要映射?
PCI 设备通过 BAR(基地址寄存器)声明自己需要的内存或 I/O 空间。这些地址是物理地址 ,CPU 无法直接访问。必须先将这些物理地址映射到内核的虚拟地址空间,驱动程序才能通过指针访问硬件寄存器。
pci_iomap
和 pci_ioremap_bar
就是完成这个映射工作的函数。
pci_iomap
函数原型
#include <linux/pci.h>
void __iomem *pci_iomap(struct pci_dev *dev, int bar, unsigned long maxlen);
参数详解
-
struct pci_dev *dev
- 指向要映射的PCI设备的结构体指针。
-
int bar
- 指定要映射的BAR的索引(0-5)。例如,
bar=0
表示映射BAR0。
- 指定要映射的BAR的索引(0-5)。例如,
-
unsigned long maxlen
-
要映射的最大长度(字节数)。
-
如果设为0,会映射整个BAR区域。
-
如果指定了长度,可以只映射BAR的一部分。
-
返回值
-
成功 :返回指向映射区域的
void __iomem *
类型的指针。 -
失败 :返回
NULL
。
特点与注意事项
-
不检查BAR类型 :
pci_iomap
会映射无论是内存还是I/O空间的BAR。对于I/O端口的BAR,通常不应该使用内存映射方式访问。 -
需要手动判断BAR类型:调用者需要自己检查BAR是内存空间还是I/O空间:
if (pci_resource_flags(pdev, bar) & IORESOURCE_IO) { // 这是I/O端口,应该用pci_iomap_range()或in/out指令 } else { // 这是内存空间,可以用pci_iomap }
-
灵活性 :可以通过
maxlen
参数控制映射的长度。
使用示例
struct pci_dev *pdev;
void __iomem *regs;
/* 映射BAR1的全部区域 */
regs = pci_iomap(pdev, 1, 0);
if (!regs) {
dev_err(&pdev->dev, "Failed to map BAR1\n");
return -ENOMEM;
}
/* 使用映射的地址访问硬件 */
writel(0x12345678, regs + REG_CONTROL); // 写入控制寄存器
u32 status = readl(regs + REG_STATUS); // 读取状态寄存器
pci_ioremap_bar
函数原型
#include <linux/pci.h>
void __iomem *pci_ioremap_bar(struct pci_dev *pdev, int bar);
参数详解
-
struct pci_dev *pdev
- 指向要映射的PCI设备的结构体指针。
-
int bar
- 指定要映射的BAR的索引(0-5)。
返回值
-
成功 :返回指向映射区域的
void __iomem *
类型的指针。 -
失败 :返回
NULL
。
特点与优势
-
自动类型检查 :
pci_ioremap_bar
会自动检查BAR的类型 。如果BAR是I/O端口空间(IORESOURCE_IO
),它不会进行映射并返回NULL。 -
映射整个BAR :总是映射整个BAR区域,不能指定部分映射。
-
更安全:由于自动类型检查,减少了错误映射I/O端口的风险。
-
便捷性:接口更简洁,不需要指定长度参数。
使用示例
struct pci_dev *pdev;
void __iomem *regs;
/* 映射BAR0 - 自动检查是否为内存空间 */
regs = pci_ioremap_bar(pdev, 0);
if (!regs) {
// 失败原因可能是:内存不足、BAR是I/O端口、BAR未启用等
dev_err(&pdev->dev, "Failed to map BAR0\n");
return -ENOMEM;
}
/* 访问硬件寄存器 */
iowrite32(0x55AA, regs + OFFSET_CONFIG);
u32 data = ioread32(regs + OFFSET_DATA);
两个函数的对比
特性 | pci_iomap |
pci_ioremap_bar |
---|---|---|
BAR类型检查 | ❌ 不检查,可能错误映射I/O空间 | ✅ 自动检查,只映射内存空间 |
映射范围 | 可指定长度(0=全部) | 总是映射整个BAR |
安全性 | 较低,需要手动检查类型 | 较高,内置安全检查 |
使用场景 | 需要部分映射时;明确知道BAR类型时 | 大多数情况下的首选 |
接口简洁性 | 需要三个参数 | 只需要两个参数 |
完整驱动代码示例
#include <linux/pci.h>
#include <linux/io.h>
struct my_device {
void __iomem *bar0_mem;
void __iomem *bar2_mem;
};
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_device *dev;
int ret;
/* 启用PCI设备 */
ret = pci_enable_device(pdev);
if (ret) return ret;
/* 申请资源区域 */
ret = pci_request_regions(pdev, "my_driver");
if (ret) goto err_disable;
/* 映射内存空间的BAR - 推荐使用pci_ioremap_bar */
dev->bar0_mem = pci_ioremap_bar(pdev, 0);
if (!dev->bar0_mem) {
ret = -ENOMEM;
goto err_release;
}
/* 如果需要部分映射,使用pci_iomap */
dev->bar2_mem = pci_iomap(pdev, 2, 0x1000); // 只映射前4KB
if (!dev->bar2_mem) {
ret = -ENOMEM;
goto err_unmap_bar0;
}
/* 设备初始化... */
return 0;
err_unmap_bar0:
pci_iounmap(pdev, dev->bar0_mem);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
static void my_remove(struct pci_dev *pdev)
{
struct my_device *dev = pci_get_drvdata(pdev);
/* 取消映射 */
if (dev->bar2_mem)
pci_iounmap(pdev, dev->bar2_mem);
if (dev->bar0_mem)
pci_iounmap(pdev, dev->bar0_mem);
/* 释放资源 */
pci_release_regions(pdev);
pci_disable_device(pdev);
}
重要注意事项
-
必须先调用
pci_request_regions
:在映射之前必须成功申请资源所有权。 -
使用正确的访问函数:映射后必须使用专门的I/O访问函数:
// 对于内存映射I/O(MMIO) ioread8(), ioread16(), ioread32() iowrite8(), iowrite16(), iowrite32() // 或者传统函数(取决于架构) readb(), readw(), readl() writeb(), writew(), writel()
-
不要直接使用指针 :绝对不能直接解引用
void __iomem *
指针:// 错误! u32 value = *regs; // 正确! u32 value = readl(regs);
-
配对使用
pci_iounmap
:在驱动卸载时必须调用pci_iounmap
来取消映射。
总结
-
pci_ioremap_bar
是首选:在大多数情况下,由于它的安全性和简洁性,应该优先使用。 -
pci_iomap
用于特殊情况:当需要部分映射或对BAR类型有明确控制时使用。 -
遵循正确的调用顺序 :
pci_enable_device
→pci_request_regions
→ 映射函数。 -
使用正确的I/O访问函数:映射后必须使用专门的读写函数访问硬件。
这两个函数是PCI驱动开发中访问硬件寄存器的基石,正确使用它们对于编写稳定可靠的驱动程序至关重要。