4. pci_set_dma_mask() API 详解:函数原型、参数含义、返回值处理、典型调用示例
pci_set_dma_mask() 是 PCI/PCIe 设备驱动程序中最核心的 DMA 配置接口之一。它决定了设备能够访问的主存物理地址范围,直接影响 DMA 缓冲区的分配策略和系统内存映射的兼容性。本节从函数原型出发,逐层解析参数语义、返回值处理逻辑,并通过典型示例展示其在驱动中的标准用法。
4.1 函数原型与参数含义
pci_set_dma_mask() 定义在 <linux/pci.h> 中,其原型如下:
int pci_set_dma_mask(struct pci_dev *dev, u64 mask);
| 参数 | 类型 | 含义 | 典型值 |
|---|---|---|---|
| dev | struct pci_dev * |
指向目标 PCI 设备的设备结构体指针,在 probe() 中由 pci_get_device() 或 pci_dev_get() 获得 |
— |
| mask | u64 |
DMA 地址掩码,表示设备能够寻址的物理地址范围。掩码中为 1 的位表示设备可以使用的地址位 | DMA_BIT_MASK(32) = 0xFFFFFFFFDMA_BIT_MASK(64) = 0xFFFFFFFFFFFFFFFF |
关键理解:掩码 mask 并非限制 DMA 地址必须小于等于该值,而是定义设备硬件能够解码的地址位宽。例如 DMA_BIT_MASK(32) 表示设备只能使用低 32 位地址线,无法访问高于 4GB 的物理内存。
4.2 返回值处理与错误语义
函数返回 int 类型,遵循 Linux 内核标准的 0 成功 / 负错误码约定:
| 返回值 | 宏定义 | 含义 | 驱动处理策略 |
|---|---|---|---|
| 0 | — | 成功设置 DMA 掩码,设备可以按指定地址范围进行 DMA 操作 | 继续 probe 流程,可调用 pci_set_consistent_dma_mask() 设置一致性掩码 |
| -EIO | #define -EIO 5 |
设备不支持该掩码对应的地址宽度。常见原因:设备硬件地址线不足、PCI 桥限制、IOMMU 限制 | 尝试降级到更小的掩码(如从 64-bit 降级到 32-bit),若仍失败则返回错误退出 probe |
| -EINVAL | #define -EINVAL 22 |
参数无效,通常因为 dev 为 NULL 或 mask 为 0 |
检查参数传递逻辑,修复驱动 bug |
常见陷阱:许多驱动在 pci_set_dma_mask() 返回非零时直接返回 -ENODEV,这是不严谨的做法。正确的做法是区分 -EIO 和 -EINVAL,前者可以尝试降级,后者应视为驱动 bug。
4.3 典型调用示例:64-bit 优先降级策略
以下代码展示了工业级驱动中标准的 DMA 掩码配置模式——优先尝试 64-bit,若失败则降级到 32-bit:
static int my_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
int ret;
/* 步骤 1:启用 PCI 设备 */
ret = pci_enable_device(dev);
if (ret)
return ret;
/* 步骤 2:尝试设置 64-bit DMA 掩码 */
ret = pci_set_dma_mask(dev, DMA_BIT_MASK(64));
if (ret) {
/* 64-bit 失败,降级到 32-bit */
ret = pci_set_dma_mask(dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(&dev->dev, "无法设置 DMA 掩码 (32-bit 也失败)\n");
pci_disable_device(dev);
return ret;
}
dev_info(&dev->dev, "使用 32-bit DMA 寻址\n");
} else {
dev_info(&dev->dev, "使用 64-bit DMA 寻址\n");
}
/* 步骤 3:设置一致性 DMA 掩码(通常与流式掩码一致) */
ret = pci_set_consistent_dma_mask(dev, DMA_BIT_MASK(64));
if (ret) {
ret = pci_set_consistent_dma_mask(dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(&dev->dev, "无法设置一致性 DMA 掩码\n");
pci_disable_device(dev);
return ret;
}
}
/* 步骤 4:分配 DMA 缓冲区并继续初始化 */
// ...
return 0;
}
4.4 调用顺序与依赖关系
pci_set_dma_mask() 的调用必须遵循严格的时序依赖:
- 必须先调用
pci_enable_device():该函数初始化 PCI 配置空间,使能设备的总线主控能力,是 DMA 操作的前提。 - 必须在分配任何 DMA 缓冲区之前调用:DMA 掩码影响
dma_alloc_coherent()和pci_alloc_consistent()的地址分配策略。 - 一致性掩码应在流式掩码之后设置:
pci_set_consistent_dma_mask()依赖于pci_set_dma_mask()的成功状态。 - 若设备支持多个功能,每个功能需独立设置:多功能 PCI 设备每个 function 有独立的 DMA 地址能力。
4.5 常见错误模式与调试建议
| 错误模式 | 现象 | 根因 | 调试方法 |
|---|---|---|---|
| 掩码设置成功但 DMA 失败 | DMA 传输超时或数据损坏 | 设备实际地址线少于掩码声称的宽度;PCI 桥或 IOMMU 限制 | 检查 /sys/bus/pci/devices/.../dma_mask 和 lspci -vv 的 Address 字段 |
| 64-bit 掩码设置失败 | 返回 -EIO | 设备不支持 64-bit 寻址;系统没有 64-bit DMA 地址空间 | 降级到 32-bit;检查内核日志 dmesg 中的 DMA 相关消息 |
| 一致性掩码设置失败 | 返回 -EIO | 设备或系统不支持一致性 DMA 的地址范围 | 尝试使用 dma_set_coherent_mask() 替代;检查 IOMMU 配置 |
忘记调用 pci_enable_device() |
内核 oops 或返回 -EINVAL | 设备未使能,PCI 配置空间不可访问 | 确保 probe 中第一行调用 pci_enable_device() |
调试技巧:在 /sys/kernel/debug/dma-api/ 中可以查看当前系统的 DMA 掩码使用情况。使用 echo 1 > /sys/kernel/debug/dma-api/dump 可以输出所有设备的 DMA 掩码信息。
4.6 与 dma_set_mask() 的关系
pci_set_dma_mask() 是 dma_set_mask() 的 PCI 封装版本。两者关系如下:
| 函数 | 所属层 | 参数差异 | 推荐使用场景 |
|---|---|---|---|
pci_set_dma_mask() |
PCI 子系统 | 第一个参数为 struct pci_dev * |
PCI/PCIe 设备驱动(传统方式) |
dma_set_mask() |
DMA API 通用层 | 第一个参数为 struct device * |
平台设备、USB 设备等非 PCI 设备;新驱动推荐使用 |
在 PCI 驱动中,两者可以互换使用:
/* 等价调用 */
pci_set_dma_mask(pdev, DMA_BIT_MASK(64));
dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
内核演进趋势:Linux 内核社区推荐新驱动使用 dma_set_mask() 和 dma_set_coherent_mask() 通用接口,以实现设备无关性。但 pci_set_dma_mask() 在现有驱动中仍广泛使用,且与 PCI 错误处理逻辑结合更紧密。