首页
/ Linux PCI Endpoint Framework(PCI EP)实战:EPC/EPF 双库 API 全解析与 configfs 绑定机制

Linux PCI Endpoint Framework(PCI EP)实战:EPC/EPF 双库 API 全解析与 configfs 绑定机制

2026-09-06 17:19:52作者:侯霆垣

本文基于内核文档 pci-endpoint.rst 及其对应实现源码,系统讲解 Linux PCI Endpoint Framework 的三层架构(EPC 库、EPF 库、configfs 层)。读完本文,你将能够独立编写一个 Endpoint Controller 驱动和一个 Endpoint Function 驱动,理解 pci_epc_set_bar() 等关键 API 的底层校验逻辑,并掌握通过 configfs 将功能驱动绑定到控制器、最终让主机侧 lspci 枚举出该 PCIe 设备的完整操作流程。

1. 背景:为什么 Linux 需要 Endpoint 模式

Linux 拥有成熟的 PCI 子系统,它面向工作在 Root Complex(RC)模式的 PCI 控制器:扫描 PCI 总线、分配内存与 IRQ 资源、按 vendor ID / device ID 加载 PCI 驱动,并提供热插拔、电源管理、高级错误报告、虚拟通道等服务。

然而,部分 SoC 集成的 PCI 控制器 IP 既可以工作在 Root Complex 模式,也可以工作在 Endpoint 模式。PCI Endpoint Framework 为 Linux 补充了 endpoint 模式支持,使得 Linux 可以运行在"EP 系统"中,覆盖测试验证、协处理器加速器等多种用例。该框架的核心由三个组件构成:

  1. Endpoint Controller(EPC)库:为可工作在 endpoint 模式的控制器提供 API,同时也为功能驱动实现特定 endpoint 功能提供 API;
  2. Endpoint Function(EPF)库:为功能驱动和 EPC 库提供 endpoint 模式功能 API;
  3. configfs 层:负责把 endpoint function 与 endpoint controller 绑定在一起。

实现代码集中在 drivers/pci/endpoint/ 目录:

文件 职责
pci-epc-core.c EPC 库核心,EPC 设备类、各 API 的实现
pci-epc-mem.c EPC 地址空间分配管理
pci-epf-core.c EPF 库核心,EPF 总线、BAR 寄存器空间管理
pci-ep-cfs.c configfs 接口,完成 function 与 controller 的绑定
functions/ 内置功能驱动(pci-epf-testpci-epf-ntb 等)

头文件方面,EPC 侧接口声明在 pci-epc.h,EPF 侧接口声明在 pci-epf.h

2. EPC 库 API(一):控制器驱动视角

2.1 创建与销毁 EPC 设备:devm_pci_epc_create() / pci_epc_create()

PCI 控制器驱动需要先实现一组回调 ops,然后调用 devm_pci_epc_create()pci_epc_create() 创建 EPC 设备。文档中列出的基础 ops 为:

  • write_header:填充配置空间头(configuration space header);
  • set_bar:配置 BAR;
  • clear_bar:复位 BAR;
  • alloc_addr_space:在 PCI 控制器地址空间中分配地址;
  • free_addr_space:释放已分配的地址空间;
  • raise_irq:触发 Legacy、MSI 或 MSI-X 中断;
  • start:启动 PCI link;
  • stop:停止 PCI link。

对照 pci-epc.h 中的 struct pci_epc_ops 定义,当前内核的完整 ops 集还包括:align_addr(返回映射 RC PCI 地址所需的控制器内存窗口地址、大小与偏移)、map_addr/unmap_addr(CPU 地址与 PCI 地址互映射)、set_msi/get_msiset_msix/get_msix(向 MSI/MSI-X capability 寄存器设置或读取中断数量)、map_msi_irq(把物理地址映射为 MSI 地址并返回 MSI data,主要用于 NTB 的 doorbell 场景)、get_features(按 func_no/vfunc_no 返回 struct pci_epc_features)、get_aux_resources_count/get_aux_resources(查询控制器自带的辅助资源,如 DMA 引擎寄存器窗口),以及记录模块属主的 owner 字段。

从源码结构看,__pci_epc_create()(见 pci-epc-core.c)会为 EPC 分配 struct pci_epc,将其挂入名为 pci_epc 的设备类(class),设备名取父设备名(即 /sys/class/pci_epc/ 下看到的名称,如 51000000.pcie_ep),并通过 pci_ep_cfs_add_epc_group() 在 configfs 中自动注册一个同名 controller 组。devm_pci_epc_create() 的 devres 变体(L1123-L1143)则把 pci_epc_destroy() 注册为 devres 释放回调,驱动 detach 时自动销毁。

struct pci_epc_features 是理解各 API 能力位的关键,它包含:

  • linkup_notifier:EPC 能否在 link up 时通知 EPF 驱动;
  • dynamic_inbound_mapping:是否支持在不先 pci_epc_clear_bar() 的情况下对已配置 BAR 再次调用 pci_epc_set_bar() 以更新 inbound 地址翻译;
  • subrange_mapping:是否支持对单个 BAR 建立多个入站子区映射(依赖 dynamic_inbound_mapping);
  • msi_capable / msix_capable / intx_capable:中断能力位;
  • bar[6]:每个 BAR 的硬件描述(enum pci_epc_bar_typeBAR_PROGRAMMABLEBAR_FIXEDBAR_RESIZABLEBAR_RESERVEDBAR_DISABLED,以及 only_64bit 约束);
  • align:BAR 缓冲区分配要求的对齐大小。

2.2 销毁与 link 通知

  • pci_epc_destroy():销毁 EPC 设备,源码(L1031-L1039)中依次执行移除 configfs 组、释放 PCI domain、device_unregister()
  • pci_epc_linkup():当 EPC 与 host 建立链路后,控制器驱动调用它通知所有已链接的功能设备。实现(L881-L897)遍历 epc->pci_epf 链表,对每个 EPF 调用其 event_ops->link_up 回调;与之配套的还有 pci_epc_linkdown()pci_epc_init_notify()pci_epc_bus_master_enable_notify() 等通知函数,对应 EPF 侧 struct pci_epc_event_ops 中的 epc_initepc_deinitlink_uplink_downbus_master_enable 五个回调(定义见 pci-epf.h)。

2.3 EPC 地址空间管理

  • pci_epc_mem_init():初始化 struct pci_epc_mem(含 struct pci_epc_mem_window 地址窗口 + bitmap 分页管理),用于后续 EPC 地址空间分配;pci_epc_mem_exit() 为其清理函数。多窗口控制器可使用 pci_epc_multi_mem_init()
  • 该地址空间即控制器 inbound 窗口中可映射给 RC PCI 地址的区域,pci_epc_mem_alloc_addr() 基于其中的 bitmap 按页分配。

3. EPC 库 API(二):功能驱动视角

3.1 配置空间头:pci_epc_write_header()

功能驱动用 pci_epc_write_header() 把标准配置头写入 endpoint controller。对应的数据结构是 struct pci_epf_headerpci-epf.h):vendoriddeviceidrevidprogif_codesubclass_codebaseclass_codecache_line_sizesubsys_vendor_idsubsys_idinterrupt_pin

实现(L756-L777)会先校验功能号合法性,并且只允许 vfunc_no <= 1 的函数写入 deviceID("Only Virtual Function #1 has deviceID"),这与 PCIe SRIOV 规范一致。

3.2 BAR 配置:pci_epc_set_bar() / pci_epc_clear_bar()

功能驱动用 pci_epc_set_bar() 配置 Base Address Register,以便 host 在枚举时分配 PCI 地址空间——功能驱动的寄存器空间通常就是经由此 API 配置的。

文档特别强调了一个进阶流程:BAR 子区映射(subrange mapping)。当 pci_epf_bar.num_submap 非零时,功能驱动是在请求基于 pci_epf_bar.submap 数组的 BAR 子区间映射,这要求 EPC 通过 subrange_mapping 能力位声明支持。由于子区映射要求 host 在枚举时先给 BAR 编程基址,因此 EPF 驱动需要对同一 BAR 两次调用 pci_epc_set_bar()(需要 dynamic_inbound_mapping 能力):第一次 num_submap 置 0、只配置 BAR 大小;第二次在 PCIe link up 且 host 完成 BAR 基址编程后,将 num_submap 置为非零。且两次调用之间不得调用 pci_epc_clear_bar(),因为清空 BAR 可能清掉/禁用 endpoint 侧仍被 host 认为有效的 BAR 地址解码。

struct pci_epf_barpci-epf.h)携带这些字段:phys_addraddrsizemem_size(为满足 iATU 对齐实际分配的大小)、barnoflags,以及可选的 num_submap/submapstruct pci_epf_bar_submap 数组,按顺序铺满整个 BAR,submap[0] 位于偏移 0)。

内核在 pci_epc_set_bar() 中对 epf_bar 做了一组硬性校验(L668-L714),写功能驱动时值得对照:

  • num_submap 非零时 submap 必须非空,且 EPC 必须同时具备 dynamic_inbound_mappingsubrange_mapping 能力,否则返回 -EINVAL
  • BAR_RESIZABLE 类型 BAR 大小必须在 1 MB ~ 128 TB 之间(对应 PCIe r6.0 §7.8.6.2 对 Resizable BAR 的最小/最大尺寸限制);
  • BAR_FIXED 类型 BAR 请求大小必须等于 fixed_size
  • size 必须是 2 的幂;
  • BAR_5 不能配置为 64 位(64 位 BAR 占用相邻两个 BAR),IO 类型不能与内存地址掩码位混用,超过 4 GB 的大 BAR 必须声明 64 位。

pci_epc_bar_size_to_rebar_cap()L725-L742)则负责把 BAR 尺寸换算成 Resizable BAR Capability 寄存器的编码(BIT(ilog2(size) - ilog2(1M) + 4)),供 BAR_RESIZABLE 控制器使用。pci_epc_clear_bar() 用于复位 BAR,源码中对 "BAR_5 + 64 位" 的组合直接跳过(该组合本就非法)。

3.3 中断:pci_epc_raise_irq()

功能驱动用 pci_epc_raise_irq(epc, func_no, vfunc_no, type, interrupt_num) 触发 Legacy(INTx)、MSI 或 MSI-X 中断。实现(L290-L307)先做功能号合法性检查,再进入控制器 ops;若控制器未实现 raise_irq 则静默返回 0。与之配套的还有 pci_epc_set_msi()(请求 1~32 个 MSI 中断号)、pci_epc_get_msi()(读取 RC 实际分配的 MSI 中断数)、pci_epc_set_msix()(1~2048 个,需指定 MSI-X 表所在 BAR 与偏移)与 pci_epc_get_msix()

3.4 地址映射:从 alloc_addrmem_map

  • pci_epc_mem_alloc_addr():从 EPC 地址空间分配内存地址,这是访问 RC 侧缓冲区的必备地址;pci_epc_mem_free_addr() 用于释放;
  • pci_epc_map_addr():把 pci_epc_mem_alloc_addr() 得到的本地 CPU 物理地址映射为一个 RC PCI 地址;pci_epc_unmap_addr() 解除映射;
  • pci_epc_mem_map():一些 endpoint controller 对可映射的 RC PCI 地址存在约束。该函数让功能驱动在考虑这些约束的前提下"分配 + 映射"控制器内存,它会返回实际映射成功的 PCI 地址范围大小(可能小于请求值)以及应使用的本地内存偏移。实现(L553-L603)的核心逻辑:若控制器实现了 ops->align_addr,先由它算出对齐后的 map_pci_addrmap_sizemap_offset;随后按 map_size 调用 pci_epc_mem_alloc_addr() 分配本地内存,最后 pci_epc_map_addr() 建立翻译;失败则回滚释放。输出结构 struct pci_epc_map 同时给出 pci_addr/pci_size(请求范围)、map_pci_addr/map_size(实际映射范围与所需内存)、phys_base/virt_basephys_addr/virt_addr(对齐偏移后的实际访问点)。pci_epc_mem_unmap() 完成对应的解除映射与释放。

4. EPC 库"其他 API":EPF 与 EPC 的绑定

文档指出以下 API 用于把 EPF 设备绑定到 EPC 设备,可参考 pci-ep-cfs.c 的用法:

  • pci_epc_get(epc_name):按设备名获取 EPC 引用。实现(L51-L68)通过 class_find_device_by_name()pci_epc 类中查找,并 try_module_get 防止控制器模块被卸载;pci_epc_put() 释放该引用;
  • pci_epc_add_epf():把一个 PCI endpoint function 加到 EPC 上。按 PCIe 规范,一个 PCIe 设备最多可有 8 个 function。实现(L790-L836)用 function_num_map 位图分配空闲 func_no,并受 epc->max_functions 限制;该 API 还接受 enum pci_epc_interface_typePRIMARY_INTERFACE/SECONDARY_INTERFACE),以支持 NTB 这类 EPF 同时挂到两个 EPC(主/从接口)的场景;
  • pci_epc_remove_epf():把 EPF 从 EPC 移除,清位并从链表摘除;
  • pci_epc_start():功能驱动完成 endpoint function 配置后调用以启动 PCI link(未实现 ops->start 时返回 0);
  • pci_epc_stop():停止 PCI link。

5. EPF 库 API

5.1 注册功能驱动:pci_epf_register_driver()

功能驱动需要实现以下 ops(struct pci_epf_opspci-epf.h):

  • bind:EPC 设备与 EPF 设备建立绑定时的操作;
  • unbind:绑定关系解除时的操作;
  • add_cfs:可选,创建功能特定的 configfs 属性。

随后用 pci_epf_register_driver() 宏(展开为 __pci_epf_register_driver(driver, THIS_MODULE))注册,pci_epf_unregister_driver() 反注册。

从源码结构看,EPF 体系是一条名为 pci-epf 的自定义总线(pci-epf-core.c):EPF 设备挂在这条总线上,EPF 驱动通过 pci_epf_match_id() 按名称匹配——优先用驱动的 id_tablepci_epf_device_id 列表)做精确匹配,否则回退到驱动名与设备名比较(L625-L646)。struct pci_epf_driver 还提供 probe/remove 回调,probe 时传入匹配到的设备 ID。注册时 __pci_epf_register_driver() 强制要求 ops->bindops->unbind 均非空(L487-L508),并会遍历 id_table 为每个函数名在 configfs functions/ 下创建目录。

5.2 BAR 寄存器空间:pci_epf_alloc_space() / pci_epf_free_space()

功能驱动用 pci_epf_alloc_space() 为指定 BAR 分配寄存器空间,用 pci_epf_free_space() 释放。实现细节(L305-L347)值得注意:

  • 内部 pci_epf_get_required_bar_size() 先把请求大小取整:最小 128 字节;BAR_RESIZABLE 时最小 1 MB;BAR_FIXED 时不得超过固定尺寸且最终采用固定尺寸;否则向上取 2 的幂;
  • 使用 dma_alloc_coherent() 在 EPC 父设备上分配内存(mem_size 会按 epc_features->align 对齐),保证 host 通过 BAR 读写的地址对本地 CPU 也是相干可访问的;
  • 若请求尺寸超过 4 GB 或该 BAR 是 only_64bit,自动置 PCI_BASE_ADDRESS_MEM_TYPE_64,否则置 32 位。

此外还有两个配套 API:pci_epf_assign_bar_space() 用于控制器侧已经给定 BAR 基址的场景(如 NTB),pci_epf_align_inbound_addr() 用于按 BAR 对齐要求对齐外设寄存器空间等非 pci_epf_alloc_space() 分配的入站地址。

5.3 供 EPC 库调用:pci_epf_linkup()

当 EPC 设备与 host 建立连接时,EPC 库调用 pci_epf_linkup(),最终触达功能驱动的 event_ops->link_up 回调——这是功能驱动感知 "host 已上线" 的标准入口。

5.4 其他 EPF API

  • pci_epf_create(name):按名称创建 EPF 设备,该名称用于与 EPF 驱动匹配。实现(L531-L573)会把名称按第一个 . 截断存储(configfs 创建的设备实际名为 <funcname>.<index>),并将 VF 号位图初值置 1(VF 编号从 1 开始);
  • pci_epf_destroy():销毁 EPF 设备;
  • pci_epf_bind():EPF 与 EPC 绑定成功时调用,会先绑定所有挂在该 PF 下的虚拟功能(校验 epc->max_vfs),再调用驱动 ops->bind
  • pci_epf_unbind():绑定丢失时调用,反向通知所有 VF 与 PF。

6. configfs 层:把功能驱动绑定到控制器驱动

6.1 挂载与目录结构

PCI Endpoint Core 在 configfs 下注册 pci_ep 子系统(pci-ep-cfs.c),包含两个根目录:

/sys/kernel/config/pci_ep/
    .. controllers/     # 每个 EPC 设备一个条目
    .. functions/       # 每个 EPF 驱动一个条目

挂载命令:

# mount -t configfs none /sys/kernel/config

查看系统中可用的 endpoint 控制器与功能驱动:

# ls /sys/class/pci_epc/
  51000000.pcie_ep

# ls /sys/kernel/config/pci_ep/controllers
  51000000.pcie_ep

# ls /sys/bus/pci-epf/drivers
  pci_epf_test

# ls /sys/kernel/config/pci_ep/functions
  pci_epf_test

6.2 创建 EPF 设备

functions/<EPF 驱动名>/mkdir 即创建一个 EPF 设备,它随后会被同名 EPF 驱动 probe:

# cd /sys/kernel/config/pci_ep/
# mkdir functions/pci_epf_test/func1

源码侧对应 pci_epf_make()L599-L659):为设备分配 IDR 序号,生成 <funcname>.<index> 名称调用 pci_epf_create(),并自动创建 primary/secondary/ 两个子组(对应主/次 EPC 接口),若驱动实现了 add_cfs 还会追加功能特定的属性组。

每个 EPF 设备目录会填充一组标准配置头属性(由 L418-L475 的属性表生成):vendoriddeviceidrevidprogif_codesubclass_codebaseclass_codecache_line_sizesubsys_vendor_idsubsys_idinterrupt_pin,以及 msi_interruptsmsix_interrupts。EPF 驱动在 bind 时填充默认值,例如 pci-epf-test 驱动把 vendorid 设为 0xffffinterrupt_pin 设为 0x0001

# cat functions/pci_epf_test/func1/vendorid
  0xffff
# cat functions/pci_epf_test/func1/interrupt_pin
  0x0001

用户可在绑定前覆盖这些值:

# echo 0x104c > functions/pci_epf_test/func1/vendorid
# echo 0xb500 > functions/pci_epf_test/func1/deviceid
# echo 32 > functions/pci_epf_test/func1/msi_interrupts
# echo 2048 > functions/pci_epf_test/func1/msix_interrupts

pci-epf-test 为例,其默认 BAR 大小为 bar0~bar4 各 128 KB(131072)、bar5 为 1 MB(1048576),定义见 pci-epf-test.c;绑定前可通过 bar?_size 覆盖(例如 # echo 1048576 > functions/pci_epf_test/func1/pci_epf_test.0/bar1_size)。注意:具有固定尺寸或保留 BAR 的控制器会忽略 configfs 中对应的 BAR 大小设置。

6.3 绑定与启动 link

要让 endpoint function 发挥作用,必须把它绑定到某个 PCI endpoint 控制器。做法是在 controllers 目录下建立符号链接:

# ln -s functions/pci_epf_test/func1 controllers/51000000.pcie_ep/

该符号链接触发 pci_primary_epc_epf_link()L110-L133),内核依次执行 pci_epc_add_epf()pci_epf_bind()pci_epc_notify_pending_init()(补发可能早于 bind 发生的 EPC 初始化完成事件)。对于需要连接两个 EPC 的场景(如 Non-transparent bridge),主接口 controller 的符号链接放在 primary/ 子目录、次接口放在 secondary/ 子目录。此外,把某个 EPF 设备符号链接到另一个 EPF 设备表示 VF 与 PF 的关联,一旦 EPF 被链接为 VF,就不能再链接到 EPC。

绑定完成后,向 controller 的 start 字段写 1 即可建立与 host 的链路:

# echo 1 > controllers/51000000.pcie_ep/start

start 属性的 store 回调(L173-L204)解析布尔值后调用 pci_epc_start()/pci_epc_stop(),并在成功启动时置位 epc_group->start;unlink 时若 link 处于启动状态会触发 WARN_ON_ONCE——即必须先停 link(写 0)再解除绑定

6.4 主机侧验证

host 枚举后 lspci 应显示 EP 侧配置的值(对应 6.2 节写入的 vendorid/deviceid):

00:00.0 PCI bridge: Texas Instruments Device 8888 (rev 01)
01:00.0 Unassigned class [ff00]: Texas Instruments Device b500

内核自带 Kselftest 可跑通完整的 BAR、中断与数据拷贝测试,详见 pci-test-howto.rst

# cd <kernel-dir>
# make -C tools/testing/selftests/pci_endpoint
# pci_endpoint_test
TAP version 13
1..16
ok 1 pci_ep_bar.BAR0.BAR_TEST
...
ok 8 pci_ep_basic.LEGACY_IRQ_TEST
ok 9 pci_ep_basic.MSI_TEST
ok 10 pci_ep_basic.MSIX_TEST
ok 14 pci_ep_data_transfer.dma.READ_TEST
...
# PASSED: 16 / 16 tests passed.

其中 pci_ep_data_transfer.dma.COPY_TEST 在大多数 DMA 控制器上会因缺少 MEMCPY over DMA 而失败,可用 pci_endpoint_test -f pci_ep_bar -f pci_ep_basic -v memcpy -T COPY_TEST -v dma 跳过。doorbell 用例则用 pci_endpoint_test -f pcie_ep_doorbell

7. 小结:一次完整的 EP 上线时序

把三层串起来,一个最小 endpoint 系统的完整时序为:

  1. 控制器驱动 probe:实现 pci_epc_opswrite_headerset_barclear_barraise_irqstart/stop 等),调用 devm_pci_epc_create() 创建 EPC,pci_epc_mem_init() 初始化 inbound 地址窗口,并在 /sys/class/pci_epc/configfs/controllers/ 出现同名条目;
  2. 功能驱动 通过 pci_epf_register_driver() 注册,其 id_table 中的函数名出现在 configfs/functions/ 下;
  3. 用户在 configfs 中 mkdir 创建 EPF 设备、写入 vendorid/deviceid 等配置头字段(驱动 bind 回调中通常执行 pci_epf_alloc_space() 分配寄存器空间、pci_epc_set_bar() 编程 BAR、pci_epc_write_header() 写配置头);
  4. 用户 ln -s 把 EPF 链接到 controller(触发 pci_epc_add_epf() + pci_epf_bind()),echo 1 > .../start 拉起 link(触发 ops->start,随后控制器收到 link up 时调用 pci_epc_linkup() 通知功能驱动);
  5. host 侧 lspci 枚举出该设备,主机驱动加载后通过 BAR 读写寄存器、经中断与 DMA 通路交互,即可用 Kselftest 完成回归验证。

框架的其余文档——pci-endpoint-cfs.rst(configfs 目录结构细节)、pci-ntb-howto.rstpci-vntb-howto.rst(NTB 双接口桥接)、pci-nvme-function.rst(NVMe 功能)——可在此骨架上进一步深入具体功能驱动的实现。

登录后查看全文
热门项目推荐
相关项目推荐