RC 控制器驱动开发
PCIe 子系统的"基础设施"层
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.c 与 drivers/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_table与pci_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控制器驱动是否加载