RC控制器驱动概述

RC控制器驱动运行在 Root Complex 所在的系统中,负责初始化PCIe控制器硬件、枚举设备、提供配置空间访问接口。这是PCIe子系统的"基础设施"——没有它,设备驱动无法看到任何PCIe设备。

本页定位

本页介绍 RC控制器驱动开发——属于RC硬件的软件栈,负责初始化RC硬件、枚举设备、提供配置空间访问接口。这与 PCIe设备驱动 是完全不同的方向。如果你在开发 EP控制器驱动,请访问对应页面。

典型场景

  • SoC厂商为新芯片开发PCIe RC支持
  • 移植Linux内核到新硬件平台
  • 调试PCIe链路训练失败问题

RC驱动与设备驱动的关系:RC驱动构建好PCI总线的"骨架"(枚举设备、分配资源),设备驱动在这个骨架上绑定具体设备。两者的交互通过Linux PCI子系统的标准API完成。

关键数据结构

pci_host_bridge

pci_host_bridge 是RC控制器在内核中的抽象,表示一个PCI主机桥。每个RC控制器对应一个 pci_host_bridge 实例。

/*
 * pci_host_bridge - 表示一个PCI主机桥
 * 是RC控制器在内核中的抽象
 */
struct pci_host_bridge {
    struct device dev;
    struct pci_bus *bus;           /* 根总线 */
    struct list_head windows;      /* 资源窗口 */
    void *sysdata;                 /* 平台私有数据 */
    
    /* 回调函数 */
    int (*map_irq)(const struct pci_dev *, u8, u8);
    void (*swizzle_irq)(struct pci_dev *, u8 *, u8 *);
};

pci_ops

pci_ops 定义了配置空间访问操作。RC驱动必须实现这些操作,它们是内核PCI子系统与硬件之间的接口。

/*
 * pci_ops - 配置空间访问操作
 * RC驱动必须实现这些操作
 */
struct pci_ops {
    void __iomem *(*map_bus)(struct pci_bus *bus, unsigned int devfn, int where);
    int (*read)(struct pci_bus *bus, unsigned int devfn, int where, int size, u32 *val);
    int (*write)(struct pci_bus *bus, unsigned int devfn, int where, int size, u32 *val);
};

ECAM配置访问实现

ECAM(Enhanced Configuration Access Mechanism)是PCIe规范定义的标准配置空间访问方法,通过MMIO访问完整的4KB配置空间。

地址计算公式:Base + (Bus << 20) + (Device << 15) + (Function << 12) + Offset

/*
 * ECAM配置空间映射
 * 地址计算:Base + (Bus << 20) + (Device << 15) + (Function << 12) + Offset
 */
static void __iomem *ecam_map_bus(struct pci_bus *bus, unsigned int devfn, int where)
{
    struct my_rc_data *data = bus->sysdata;
    void __iomem *base = data->ecam_base;
    unsigned int busn = bus->number;
    unsigned int dev = PCI_SLOT(devfn);
    unsigned int fn = PCI_FUNC(devfn);
    
    u32 offset = (busn << 20) | (dev << 15) | (fn << 12) | (where & ~3);
    
    return base + offset;
}

static int ecam_read(struct pci_bus *bus, unsigned int devfn, 
                     int where, int size, u32 *val)
{
    void __iomem *addr = ecam_map_bus(bus, devfn, where);
    
    if (!addr) {
        *val = ~0;
        return PCIBIOS_DEVICE_NOT_FOUND;
    }
    
    *val = readl(addr);
    
    /* 根据size调整返回值 */
    if (size == 1)
        *val = (*val >> ((where & 3) * 8)) & 0xff;
    else if (size == 2)
        *val = (*val >> ((where & 3) * 8)) & 0xffff;
    
    return PCIBIOS_SUCCESSFUL;
}

static struct pci_ops ecam_ops = {
    .map_bus = ecam_map_bus,
    .read = ecam_read,
    .write = ecam_write,  /* 类似实现 */
};

RC控制器初始化流程

RC驱动的 probe 函数负责完整的初始化流程:分配host bridge、映射资源、初始化硬件、注册到PCI子系统。

/*
 * RC驱动probe函数示例
 */
static int my_rc_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct pci_host_bridge *bridge;
    struct resource *cfg_res, *io_res, *mem_res;
    struct my_rc_data *data;
    int ret;
    
    /* 1. 分配host bridge结构 */
    bridge = devm_pci_alloc_host_bridge(dev, sizeof(*data));
    if (!bridge)
        return -ENOMEM;
    
    data = pci_host_bridge_priv(bridge);
    
    /* 2. 获取资源(从设备树或ACPI) */
    cfg_res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config");
    data->ecam_base = devm_ioremap_resource(dev, cfg_res);
    if (IS_ERR(data->ecam_base))
        return PTR_ERR(data->ecam_base);
    
    /* 3. 初始化硬件 */
    ret = my_rc_hw_init(pdev, data);
    if (ret)
        return ret;
    
    /* 4. 设置配置空间操作 */
    bridge->ops = &ecam_ops;
    bridge->sysdata = data;
    
    /* 5. 解析设备树 ranges → 资源窗口(I/O / MEM / DMA,填入 bridge->windows) */
    ret = devm_of_pci_get_host_bridge_resources(dev, 0, 0xff, &bridge->windows, &bridge->dma_ranges);
    if (ret)
        return ret;

    /* 6. 扫描根总线并注册 host bridge(枚举 + 资源分配 + 驱动绑定的统一入口) */
    ret = pci_host_probe(bridge);
    if (ret)
        return ret;

    return 0;
}

static const struct of_device_id my_rc_of_match[] = {
    { .compatible = "vendor,pcie-rc", },
    { }
};

static struct platform_driver my_rc_driver = {
    .probe = my_rc_probe,
    .driver = {
        .name = "my-pcie-rc",
        .of_match_table = my_rc_of_match,
    },
};
builtin_platform_driver(my_rc_driver);

设备树配置示例

ARM/ARM64平台通过设备树(Device Tree)描述PCIe控制器硬件信息。RC驱动通过 of_match_table 匹配设备树节点。

/* ARM平台设备树示例 */
pcie@40000000 {
    compatible = "vendor,pcie-rc";
    reg = <0x40000000 0x01000000>,    /* 配置空间 */
          <0x41000000 0x00100000>;    /* RC寄存器 */
    reg-names = "config", "ctrl";
    
    #address-cells = <3>;
    #size-cells = <2>;
    ranges = <0x02000000 0x0 0x80000000 0x80000000 0x0 0x20000000>;
    
    bus-range = <0x00 0x0f>;   /* 16MB ECAM 窗口:1MB/总线 × 16 条总线(bus 0x00–0x0f)
                                  若需 256 条总线,config 空间需扩到 256MB(如 4KB/设备 × 8函数 × 32设备/总线) */
    
    interrupts = ,
                 ;
    interrupt-names = "intx", "msi";
    
    #interrupt-cells = <1>;
    interrupt-map-mask = <0 0 0 7>;
    interrupt-map = <0 0 0 1 &gic GIC_SPI 120 ...>;
    
    status = "okay";
};

枚举流程深挖:从 pci_host_probe 到驱动 probe

上一节的 pci_host_probe(bridge) 之后再无你的代码参与——但总线上所有设备就是从这里"长"出来的。这条路径全部位于 drivers/pci/probe.cdrivers/pci/setup-bus.c,是 RC 驱动开发必须吃透的内核机制。

/* 枚举主链路(简化) */
pci_host_probe(bridge)
  └─ pci_scan_root_bus_bridge(bridge)
       └─ pci_scan_child_bus(bus)          /* DFS 扫描 */
            └─ pci_scan_slot / pci_scan_single_device
                 ├─ pci_scan_device()      /* 读 Vendor ID 探测设备 */
                 ├─ pci_setup_device()     /* 读 Header、Class、BAR */
                 │    └─ pci_read_bases()  /* BAR sizing(write-all-1s) */
                 └─ pci_scan_bridge()      /* Type 1 桥 → 递归扫描二级总线 */
  └─ pci_bus_add_devices(bridge->bus)      /* 驱动绑定 */
       └─ device_attach() → bus->match() → driver->probe()

1. 设备存在性判定:Vendor ID 读两次

DFS 扫描对 (bus, dev, func) 的每个槽位先读 Vendor ID:0xFFFF 表示无设备。为避免主桥/死设备返回假值,内核会读两次确认(pci_bus_read_dev_vendor_id()),不一致则认为设备不可靠。随后检查 Header Type 寄存器的多功能位(bit 7)——置位则把该设备的 8 个 Function 全部扫一遍。这也是为什么配置访问通道(ECAM / iATU CFG 窗口,见 iATU 地址翻译)必须先就绪:枚举的第一条配置读走的就是它。

2. BAR sizing:write-all-1s

读出 BAR 后,内核要确定它需要多大空间。方法是对每个 BAR 做一次"写 1 探测":

/* BAR sizing 的硬件语义(pci_read_bases() 背后的规则) */
u32 orig, sz;
pci_read_config_dword(dev, bar_pos, &orig);       /* 1. 保存原值 */
pci_write_config_dword(dev, bar_pos, ~0u);        /* 2. 写全 1 */
pci_read_config_dword(dev, bar_pos, &sz);         /* 3. 回读:实现位保留,地址位被清 */
pci_write_config_dword(dev, bar_pos, orig);       /* 4. 恢复原值 */

/* 5. 解码:低 4 位(MEM)/低 2 位(IO)是类型标志位,屏蔽后取反加一 */
sz &= (sz & PCI_BASE_ADDRESS_SPACE) ? 0xFFFFFFFC : 0xFFFFFFF0;
size = (~sz + 1);                                 /* 例如回读 0xFFF00000 → 1MB */

要点:设备对"写全 1 后回读"的实现方式是地址译码器只锁存高位地址位,低地址位读回恒为 0——这正是 BAR 计算器对回读值取 ~mask + 1 求大小的依据。64 位 BAR 占用两个连续 BAR 槽位(PCI_BASE_ADDRESS_MEM_TYPE_64),对低 DW 写全 1 后还须对高 DW 再做一轮探测。

3. Bus Number 分配:Type 1 桥的递归

扫到 Type 1 头(PCI-to-PCI 桥 / Switch 口)时,pci_scan_bridge()递归下探

  • 从全局下一个可用总线号分配 secondary(二级总线号),先把 subordinate 临时置为 0xFF 以便下游探测期间 TLP 能透传;
  • 对二级总线调 pci_scan_child_bus() 递归扫描,返回该子树消耗的最大总线号,再回写桥的 subordinate 上界;
  • 设备树/ACPI 给的 bus-range 是分配上限——这也是 iATU CFG 窗口大小必须与 bus-range 匹配的原因。

4. 资源分配与驱动绑定

  • 资源窗口:桥设备的 pci_scan_bridge() 同时声明其 MEM/Prefetch/IO 窗口资源;pci_bus_size_bridges() 自底向上估算窗口大小,pci_assign_unassigned_bus_resources() 自顶向下把具体的总线地址写回各设备 BAR——这一步完成后 lspci -vv 里的 Memory at ... 才有真值。
  • 绑定顺序pci_bus_add_devices() 逐设备 device_attach(),经 pci_bus_match() 用驱动 id_table(Vendor/Device/Class 码)匹配,命中即调用 driver->probe()。注意设备先于驱动存在:内核先扫完硬件再谈绑定,新插入模块时是反向走一遍(driver_attach())。
  • 常见现场问题:设备"扫不到"大概率死在前半程(配置通道/时钟/复位/链路未 up),"扫到了但 probe 不执行"则查 id_tablepci_driver.driver.name

相关内核源码

学习RC驱动开发的最佳方式是阅读Linux内核源码。以下是关键文件路径:

文件路径 内容
drivers/pci/controller/ 各种RC控制器驱动
drivers/pci/controller/pcie-xilinx.c Xilinx PCIe RC驱动示例
drivers/pci/controller/dwc/ Synopsys DesignWare PCIe控制器
drivers/pci/probe.c 设备枚举逻辑
drivers/pci/setup-bus.c 总线资源分配

学习建议

  • drivers/pci/controller/pci-ecam-generic.c 开始,它是ECAM的通用实现
  • 阅读 drivers/pci/probe.c 理解设备枚举流程
  • 参考具体SoC厂商的RC驱动(如 pci-host-common.c
  • 使用 lspci -t 查看总线树形结构验证你的驱动

RC驱动调试

验证设备枚举

# 查看PCI总线树形结构
lspci -t

# 查看所有设备详细信息
lspci -vvv

# 查看设备资源分配
lspci -vvv -s 00:00.0

# 查看内核PCI子系统日志
dmesg | grep -i pci

# 查看设备树中的PCIe节点
ls /proc/device-tree/ | grep pcie

# 启用PCI核心调试日志(动态调试,无需重启)
echo 'func pci_bus_read_dev_vendor_id +p; file drivers/pci/* +p' > /sys/kernel/debug/dynamic_debug/control

# 或编译期打开 CONFIG_PCI_DEBUG,并以 pci=kernel 参数细粒度控制
lspci -t && dmesg | grep "PCI: "

常见问题排查

# 1. 设备未被枚举 → 检查ECAM映射
#    确认ecam_base映射的物理地址正确
#    确认设备树ranges属性配置正确

# 2. 配置空间读取全0或全1 → 检查ecam_map_bus
#    确认bus/device/function编码正确
#    确认ECAM窗口大小足够覆盖所有总线

# 3. 链路训练失败 → 检查PHY和时钟
#    确认PCIe REFCLK已正确配置
#    确认PHY初始化序列正确
#    查看Link Status寄存器

# 4. MSI中断不工作 → 检查中断控制器映射
#    确认interrupt-map配置正确
#    确认GIC中断号映射正确
#    检查MSI控制器驱动是否加载