lspci output on iMX95EVK as PCIe RCiMX95EVK作为PCIe RC时的lspci输出
请仔细观察上述片段。它取自iMX95的控制台,在对特定PCIe设备[iMX8MM即PCIe EP]执行“lspci”后,此设备已枚举为BDF[总线设备功能]01:00.0。
本博客试图揭开围绕lspci输出内存地址信息的神秘面纱。
我们将探讨这一地址的定义、用途及其在PCIe领域的重要性。本博客将重点讨论以下议题:-
1. Linux设备树中的PCIe对应关系
2. 什么是CPU和PCIe地址空间,以及地址空间转换的必要性?
3. 在Linux中为PCIe设备分配资源
4. 如何在Linux PCI子系统中进行地址空间转换?
Linux设备树中的PCIe对应关系
在Linux设备树中,PCIe对应关系可定义PCIe根复合体和端点在系统中的位置。设备树中的PCIe父节点代表PCIe控制器(根复合体/主机桥)。
参考imx95设备树源中的PCIe节点:-
pcie@4c300000 {
compatible = "fsl,imx95-pcie";
reg = <0x00 0x4c300000 0x00 0x10000 0x00 0x4c360000 0x00 0x20000 0x00 0x60100000 0x00 0xfe00000>;
reg-names = "dbi\0atu\0config";
#address-cells = <0x03>
…
}
pcie@4c300000代表Designware PCIe控制器根复合体,它将作为与其连接的设备/桥的父节点。
-- “compatible”属性则用于标识特定的PCIe控制器。其对应的驱动程序位于drivers/pci/controller/dwc/pci-imx6.c
-- “reg”属性可指定PCIe控制器的内存映射寄存器。
PCIe RC下的子节点代表PCIe总线上的设备。它们可以是固定功能设备,例如Wi-fi、以太网、NVMe,也可以是PCIe桥,后者可进一步连接设备。
参考“arch/arm64/boot/dts/freescale/imx95.dtsi”
pcie_4ca00000: pcie@4ca00000 {
compatible = "pci-host-ecam-generic";
reg = <0x0 0x4ca00000 0x0 0x100000>;
/* 必须为3。*/
…
…
enetc_port0: ethernet@0,0 {
兼容 = "fsl,imx95-enetc";
reg = <0x000000 0 0 0 0>;
clocks = <&scmi_clk IMX95_CLK_ENET>,
<&scmi_clk IMX95_CLK_ENETREF>;
clock-names = "ipg_clk", "enet_ref_clk";
nvmem-cells = <ð_mac0>;
nvmem-cell-names = "mac-address";
状态 = "disabled";
};
}
ethernet@0,0是位于总线0、设备0、功能0的PCIe设备。它是PCIe RC的子设备,内存映射位置为0x4ca00000
这些子设备/桥既可以借助PCI枚举动态发现,也可以在设备树中静态描述,如上文中设备树片段所示,其中“ethernet@0,0”条目以静态方式向RC声明与其连接的以太网子设备。如上例所示,这些子节点嵌套在设备树的PCI父节点中。
什么是CPU和PCIe地址空间,以及地址空间转换的必要性?
CPU地址空间是处理器所能查看的系统物理内存映射。
Cortex-A55在iMX95上查看的CPU物理地址空间示例:
起始地址 结束地址 模块
0x48000000 0x4812FFFF GIC编程寄存器
0x4AA00000 0x4AAFFFFF Neutron SRAM
0x4AC10000 0x4AC1FFFF 摄像头域块控制
0x4E080000 0x4E08FFFF DDR控制器
此地址空间是由系统固件/操作系统管理的全局系统视图。这些地址均由硬件设计固定。
另一方面,PCIe地址空间是PCI总线的本地空间,由PCIe子系统管理。该空间中的地址采用动态分配模式。
PCIe地址空间的示例如下:-
0x00000000 - 0x0FFFFFFF
0x10000000-0x1FFFFFFF
0x20000000 - 0x2FFFFFFF
由上文说明可知,CPU和PCIe地址空间运行在各自独立的地址域中。因此,除非部署转换机制,否则CPU无法访问PCIe设备的地址空间。我们将在后续章节中讨论这一内容,但请花几分钟时间来思考以下问题:
问:为什么CPU和PCIe需要独立的地址空间?
答:主要考量因素之一是模块化。我们采用独立的空间设计,确保PCIe设备独立于CPU架构进行设计。同一张卡可以在不同的系统中运行。CPU可以根据需要随时重新映射PCIe空间,从而始终保持灵活性。
此外,不同的地址空间会阻止设备访问任意系统内存。
根据本节的讨论内容,PCIe地址空间显然与CPU地址空间存在本质差异,而事实上——这种差异恰恰赋予其独特优势。因此,我们需要一个能在地址空间之间实现双向转换的实体。
接下来,我们将介绍“iATU”,即内部地址转换单元。在iMX SoC中,这些硬件单元负责执行地址转换。它们是Synopsys DesignWare PCIe控制器的组成部分,可为入站和出站事务提供可编程的地址转换窗口。
对于不熟悉PCIe入站和出站事务的读者,请花些时间阅读这篇技术博客 -
通过用例了解PCIe出站/入站窗口 - 恩智浦网络社区
注意:地址转换仅确保CPU可访问PCIe设备的内存,反之亦然。
到目前为止,读者应对PCIe地址转换形成基本认知。在深入研究Linux PCI子系统中的转换操作之前,让我们先讨论一下如何为PCIe设备分配资源。
在Linux中为PCIe设备分配资源
PCIe设备并未配置直接的CPU指令接口,因此其通过内存映射区域进行通信。设备需要内存来执行DMA操作或MSI/MSIX中断。不同的设备有不同的需求,因此PCIe中的资源可以是映射设备寄存器的MMIO,也可以是DMA传输所需的内存区域。
在Linux中,PCI子系统的pci_assign_resource函数负责在系统初始化期间为PCIe设备分配IO和内存资源,此操作将在枚举PCIe设备后执行。PCI总线上的所有设备均可调用此函数,并根据PCI设备的资源需求进行分配。
但是,Linux中的PCI子系统如何确定PCIe设备需要什么资源?
- 每台PCIe设备都设有由PCIe规范定义的配置空间。这包括
BAR[基地址寄存器] - 用于指示设备所需资源的类型[IO/Mem]和规模。
功能 - 用于广播设备功能,例如MSI中断、ASPM低功耗状态等。
通过从PCIe设备读取BAR,我们可以了解设备所需资源的类型和规模。
// 从PCIe设备的BAR中确定资源规模:-
步骤-1:将所有1写入目标BAR寄存器。
步骤-2:读回该值并清除低4位(对于内存BAR)或2位(对于I/O BAR),因为这些状态位并不参与大小的计算
步骤-3:对该值执行按位取反后加1。
步骤-4:返回值表示大小。
举例说明:-
假设在读回上述步骤-2中的值后,BAR返回0xFFFFF000。低4位已清零。
步骤-3:对该值执行按位取反 -> ~(0xFFFFF000) = 0x00000FFF
加1:0x00000FFF + 1 = 0x00001000
得到的值0x1000 = 4096字节表示大小,即BAR需要4KB的内存区域。
// 从PCIe设备的BAR中确定资源类型:-
PCI 配置空间中的基地址寄存器(BAR):
解释值0xFFFFF000,我们得到:
pci_read_bases [drivers/pci/probe.c]在Linux PCI子系统中负责确定设备枚举期间的BAR内存大小和类型需求。
毋庸置疑,上述向端点的BAR写入数据并确定资源类型与规模的操作流程,均在PCIe RC上执行。我们的设置如下:-
iMX95<------>
iMX8MM[RC] [EP]
在PCIe RC确定所需的BAR大小后,pci_assign_resource函数会分配一个内存范围,然后设置从该内存范围向PCIe地址空间的转换。
我们以一个lspci日志片段为切入点来撰写这篇博客文章,具体片段如下:-
综上所述,请注意RC驱动程序已分配以下资源:
0x910100000 - 0x910110000作为不可预取的内存地址范围,大小=64KB
上述内存地址范围位于iMX95 SoC上映射的PCIe 1出站空间内存中:-
范围0x910100000 - 0x910110000将映射到端点的PCIe地址空间。这实际上意味着,如果CPU生成此范围内的任何地址[包括起始和结束地址],PCIe控制器则会在RC上向总线中的端点发送PCIe TLP。它可能是对端点内存的读取/写入操作。写入/读取地址将根据地址空间转换来决定。
我们将在下一节详细探讨如何在Linux内核中实现这一转换。
在 Linux PCI 子系统中如何进行地址空间转换?
我们先从部分关键问题入手:-
如何指定0x910100000-0x910110000的范围?内核如何确认其必须映射PCIe 1出站空间,而非PCIe2出站空间或其他地址空间?
-- 正如Linux 中的所有优秀设计,这同样始于“设备树二进制”。Uboot会将dtb传递给内核,以便其获取我们的板硬件描述。由于我们使用Torradex的Verdin iMX95 EVK板作为根复合体,因此我们使用的dtb为
imx95-19x19-verdin-adv7535.dtb
我会附上可用的dtb,供读者按需使用。
该dtb包括 -arch/arm64/boot/dts/freescale/imx95.dtsi
让我们重点关注特定的pcie节点:-
“ranges”属性正是本节先前所提问题的答案。
- 此属性可定义父地址空间和子PCI地址空间之间的地址转换规则。
注意:此博客仅关注“ranges”属性,因为它与我们的讨论内容相关。因此,如果读者想了解PCIe节点的其他设备树属性,建议查阅其他资料。
让我们解码“ranges”属性:
具体格式如下:-
3个单元 2个单元 2个单元
因此,一个条目将配置7个单元。我们的dtsi设有2个条目。第一个条目用于IO空间转换,第二个条目则用于内存空间转换。请参考第二个条目:-
0x82000000 0x0 0x10000000 0x9 0x10000000 0 0x10000000
|------PCI地址---------------| |-CPU地址-| |---PCI大小---|
根据上文,我们获得以下信息:-
内存空间可预取
<
0x82000000 0x00 0x10000000 // PCIe 地址:0x10000000
0x09 0x10000000 // CPU/系统地址:0x910000000
0x00 0x10000000 // 大小:256MB
>;
0x82000000 = 1000 0010 0000 0000 0000 0000 0000 0000
因此,0x82000000表示:
注意:- 对于想了解lspci输出为何显示[size=64K]而dts256 MB的人,这是因为256MB是PCIe设备可用的最大地址空间。端点设备根据自身所需地址空间的大小,向系统申请分配。
同样,IO空间转换也是根据“ranges”中的第一个条目创建的:-
<
0x81000000 0x00 0x00 → PCI I/O 地址:0x00000000
0x00 0x6ff00000 → CPU/系统地址:0x6ff00000
0x00 0x100000 → 大小:1MB
>;
我们在iMX95 Verdin EVK Linux控制台的dmesg输出中也观察到类似情况:-
因此,内存空间映射已从
CPU地址0x910000000 - 0x091fffffff转换为PCIe地址0x10000000 - 0x1fffffff
我们有必要说明使用“ranges”属性的驱动程序。“ranges”属性在“pci_parse_request_of_pci_ranges -> devm_of_pci_get_host_bridge_resources”中进行解析,具体路径是“drivers/pci/of.c”
每个范围中的devm_of_pci_get_host_bridge_resources会自动管理这些资源的内存分配。它能确保在设备断开连接或驱动程序移除时释放这些资源。
我们已确定CPU和PCI地址范围的定义和原因。但是在lspci中,您观察到的是0x910100000,而非0x910000000,后者才是根据dtb指定的起始范围。这是为什么呢?
要回答这个问题,我们需要回溯PCIe设备枚举过程。
如上文所述,在PCIe枚举过程中,Linux PCI驱动程序会确定BAR资源,随后PCI核心驱动程序可能会遵循对齐要求分配地址。正因如此,系统会为EP的BAR0分配PCI总线地址0x10100000,且与0x10000000存在1MB[0x100000]的偏移量。
同时,您还需关注设备树PCI转换窗口:-
0x10100000转换为0x910100000
这一转换并非自动完成。设备树二进制仅指定转换窗口的具体信息,例如目标CPU地址空间和源PCI 地址空间之间的转换关系。实际转换是由iATU完成的。
此操作可在drivers/pci/controller/dwc/pcie-designware-host.c的dw_pcie_iatu_setup函数中完成,具体方式是使用dw_pcie_prog_outbound_atu函数创建出站窗口。
RC上的转换已成功配置,但部分环节仍需完善。
..
..
入站窗口!!
如果端点(如iMX8MM)上并未配置入站窗口,对0x910100000的读取/写入操作将毫无意义。我们在iMX8MM上使用的是PCI端点测试驱动程序,该驱动程序是Linux社区中的热门选择。如果读者需要了解更多信息,建议访问此页面:- 9. PCI端点框架 — Linux内核文档
pci_epc_map_addr函数(位于drivers/pci/endpoint/pci-epc-core.c中)通过将PCI地址[0x10100000]映射到EP内存中的物理地址来创建入站窗口。读取和写入操作就是通过这种方式进行的。
如果未配置入站窗口,进行读取操作时将出现以下情况:-
现在,一切准备就绪。转换窗口已在PCI驱动程序中配置,且处于Linux控制台环境中。当CPU发起内存读取请求时,将按以下顺序执行:-
对于内存写入操作:-
端点上会出现以下情况:-
最妙的是,整个转换过程由硬件以透明的方式完成 - 您的驱动程序只需读取/写入CPU地址,PCI主机控制器就会自动处理所有转换!
—— 我们如何测试地址映射?
要测试读取和写入操作,我们可以针对驱动程序本身作出一些改动,或者使用devmem5用户空间二进制。我们将对iMX8MM进行驱动程序方面的细微修改,并在RC上使用devmem5。
iMX8MM是PCIe端点,我们使用端点测试驱动程序对其进行相应配置。如果您需要实现相同配置,请参照此博客的指引操作 -
在iMX95 torradex板和iMX8MM EVK上启用PCIe端点框架 - 恩智浦网络社区
相反,如果您想将iMX95设为RC并将iMX8MM设为端点,请随时关注此博客 - 如何将iMX95EVK配置为PCIe端点并使用PCIe端点测试框架进行测试 - 恩智浦网络社区
接下来,我们需要完成两个任务:-
1. 在iMX8MM EP中,我们将写入部分随机值
在drivers/pci/endpoint/pci-epf-core.c中,对pci_epf_alloc_space函数进行以下修改:-
“space”是虚拟地址,“phys_addr”是连续的物理地址。
请注意,这是测试转换的初步方式,还有更合适的方案。
在修改后编译内核并用其启动开发板。使用PCI端点测试框架将iMX8MM设为端点。
2. 在iMX95 Verdin EVK [PCIe RC]上,我们将使用devmem5读取地址0x910100000,以验证能否在RC上观测到相同的数据。
以上就是本期的全部内容。此博客篇幅较长,如果您对其中的细节感到困惑,请随时通过私信或评论留言,我将尽力为您解答。
下次再见!
Gaurav Sharma