驱动开发,为什么需要映射?
核心概念:为什么需要映射?
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。
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); // 只映射前4KBif (!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驱动开发中访问硬件寄存器的基石,正确使用它们对于编写稳定可靠的驱动程序至关重要。