NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发


前言

RC : Root Complex,为CPU代言,CPU通过它控制其他设备,你可以先把它认为是台式机;
EP : EndPoint,PCIE终端设备,常见的PCIE网卡,显卡等;

PCIE 设备驱动RC端开发资料比较多,EP端开发网上资料相对较少。本文以 NXP LS1046A 处理器(主要)以及 飞腾新四核FT2004 处理器(次)为例,详细介绍 PCIE EP 端 LINUX 设备驱动开发,也会略微涉及硬件方面的知识,以及开发过程中遇到的困难。注意,本文并不会涉及太过细致的PCIE软硬件知识,只是会根据开发过程中遇到问题进行相应扩展,适合LS1046APCIE EP驱动开发、没有PCIE相关开发知识需要迅速开发、以及其他寻找解决相关问题的人。随心情想到什么写什么,有不清晰的地方,请留言讨论,我会不断完善。

主要实现的功能是RC,EP互相进行内存映射,从而RC端可以访问EP BAR地址空间,EP端可以访问主机全部物理内存。其他功能均可在其基础上实现, 能力有限,如有错误,欢迎指正。

PCIE相关理论知识可参考机械工业出版社王 齐编著的《PCIE体系结构导读》,内容详细全面,一本书读透理论知识就齐全了。


一、PCIE 硬件简介

PCIE 速度对比,PCIE设备与RC端进行底层协商,根据速度选择使用1.0、2.0或者3.0,前提是两边都支持,就像网卡百兆、千兆一样,虽然支持,但是如果协商信号有问题,千兆网卡也会协商出百兆速率:

在这里插入图片描述

我们常说的X1,X2,X4是指PCIE连接的通道数(Lane),最多32条通道,一条通道叫做1lane,包括TX和RX两根线,如下图:

在这里插入图片描述

PCIE是与PCI不同,PCIE使用端到端连接方式,在PCIE两端只能连接一个设备,这两个设备互为数据发送端和数据接收端,多个PCIE设备扩展需要 SWITCH,就像USB一样,一个口只能插一个USB设备,接口不够用,则需要USB HUB进行扩展,物理链路如下图:

在这里插入图片描述

物理连接线包括:
  1. PREST#信号,该信号为全局复位信号,由处理器系统控制,用于复位PCIE卡;
  2. WAKE#信号,当PCIE设备进入休眠状态时,PCIE设备可使用该信号向处理器系统提交唤醒需求,使处理器系统重新为设备供电;
  3. SMCLK和SMDATA信号,主要与X86处理器的SMBus有关;
  4. JTAG(Joint Test Action Group)是一种国际标准测试协议,与IEEE 1149.1兼容,主要用于芯片内部测试。目前绝大多数器件都支持 JTAG 测试标准。JTAG 信号由 TRST#、TCK、 TDI、TDO 和TMS信号组成。其中TRST#为复位信号;TCK 为时钟信号;TDI和TDO 分别与数据输入和数据输出对应; 而 TMS 信号为模式选择。/font>
  5. PRSNTI#和 PRSNT2#信号与 PCle 设备的热插拔相关。在基于 PCIe 总线的 Add-In 卡中, PRSNTI#和 PRSNT2#信号直接相连,而在处理器主板中,PRSNTI#信号接地,PRSNT2#信号通过上拉电阻接为高。当 Add-In 卡没有插入时,处理器主板的 PRSNT2#信号由上拉电阻接为高,而当 Add-In卡插入时主板的 PRSNT2#信号将与PRSNTI#信号通过 Add-In 卡连通,此时 PRSNT2#信号为低。处理器主板的热插拔控制逻辑将捕获这个"低电平",得知 Add-In 卡已经插入,从而触发系统软件进行相应处理。
    在这里插入图片描述
  6. 在一个处理器系统中,可能含有许多 PCle设备,这些设备可以作为 Add-In 卡与 PCle插槽连接,也可以作为内置模块,与处理器系统提供的 PCle 链路直接相连,而不需要经过 PCle 插槽。PCle设备与 PCIe插槽都具有REFCLK+和REFCLK-信号,其中 PCle插槽使用这组信号与处理器系统同步。在一个处理器系统中,通常采用专用逻辑向 PCle 插槽提供 REFCLK+和 REFCLK-信号,其中100 MHz的时钟源由晶振提供,并经过一个"一推多"的差分时钟驱动器生成多个同相位的时钟源,与 PCIe 插槽一一对应连接。

    在这里插入图片描述

    PCle插槽需要使用参考时钟,其频率范围为 100 MHz±300pm。当PCle 设备作为 Add-In 卡连接在 PCle插槽时,可以直接使用 PCle 插槽提供的 REFCLK+和 REFCLK -信号,也可以使用独立的参考时钟,只要这个参考时钟在 100 MHz±300ppm范围内即可。内置的 PCle设备与Add-In 卡在处理REFCLK+和 REFCLK-信号时使用的方法类似,但是 PCle设备可以使用独立的参考时钟,而不使用REFCIK+和 REFCLK-信号。

    在 PCle 设备配置空间的 Link Control Register 中,含有一个"Common Clock Configura- tion"位。当该位为1时,表示该设备与PCIe链路的对端设备使用"同相位"的参考时钟;如果为0,表示该设备与 PCle链路的对端设备使用的参考时钟是异步的。

    PCle 总线物理链路间的数据传送使用基于时钟的同步传送机制,但是在物理链路上并没有时钟线,PCle 总线的接收端含有时钟恢复模块 CDR(Clock Data Recovery),CDR 将从接收报文中提取接收时钟,从而进行同步数据传递,PCle 设备进行链路训练时将完成时钟的提取工作

    时钟线是我再调试硬件过程中遇到的难点,我们是开发板飞线连接X86台式机的,时钟信号线一开始未连接,也就是说EP端使用的独立的参考时钟,寄存器配置正确,但是协商不成功,导致主机发现不了设备,lspci的时候没有看到设备。所以时钟线改为直连,使用主机提供的时钟,解决了该问题。
硬件最小连接:因为是开发板飞线调试,电源开发板供电,最少连接线包括:
  1. 时钟线 : REFCLK+ / REFCLK-
  2. X1 发送差分信号线 : TX_P / TX_N 可交叉连接,寄存器可配置
  3. X1 接收差分信号线 : RX_P / RX_N 可交叉连接,寄存器可配置
  4. 地线

可查找一个PCIE卡原理图进行参考,下图是网上下载的PCIE RC侧X1原理图,可供参考:
在这里插入图片描述


二、PCIE EP地址映射原理介绍

这一部分内容很多,涉及PCIE总线事务层、数据链路层、物理层等,而这一部分对于处理器使用者来说,是透明的,我们在使用处理器EP模式的时候,是不需要关系TLP是如何组包路由的,只需要建立相应的映射即可,这方面内容有兴趣的读者可以去阅读推荐书籍去理解内在机制,本节就根据自己的理解,抛去复杂的定义以及TLP路由等内容,通俗地解释一下。

1. PCI总线的各种域(存储器域、PCI总线域)

在这里插入图片描述
参考上图进行理解,也可以先不用去管CPU域、DRAM域,通俗的讲,存储器域就是我们的处理器所能访问到的所有物理地址范围,比如一个32位处理器,可以访问0x0000_0000 - 0xFFFF_FFFF的物理地址空间;存储器域也不是字面意思的理解的内存地址空间范围,而是包括CPU域、主存以及外部设备空间(X86处理器,外部设备空间与PCI 总线域地址空间等效);

那么什么是PCI总线域呢?
在这里插入图片描述
我们可以将PCI总线域认为是RC与EP两个处理器之间一个虚拟的地址空间范围0x0000_0000 - 0xFFFF_FFFF,用于对两个处理器进行地址映射。
很少有系统会像上图一样进行映射,大部分都是简单等效映射,即如果一个处理器访问一个物理地址0x1234_5678, 并且已经进行好了映射,PCI控制器就会自动将这个地址翻译成PCI域地址0x1234_5678,这样就连同了两个处理器之间的地址映射。
一个处理器映射需要两个方向,本地映射到PCI域是OutBound,PCI域映射到本地是InBound;
如下图,主机RC端将一块物理地址0x7890_2000 OutBound映射到PCI域, 相对应的EP端处理器将相应的PCI域地址InBound映射到本地0x7890_1234;0x7890_2000是一个RC处理器可以访问到的物理地址,注意,它并不是内存RAM的地址,EP端InBouond到本地的地址0x7890_1234才是内存RAM地址;当映射完毕之后,RC端往0x7890_2000地址写1,PCI控制器就会进行地址转换,组TLP包,最终访问到PCI域0x7890_1234,然后EP端PCI控制器解码TLP包,地址范围匹配,就对0x7890_1234进行写操作,那样,EP端如果读物理地址0x7890_1234内存值时,就会读到1。RC读操作也是一样,只不过是TLP命令包不同,这一部分一般不需要驱动开发者去关注,毕竟芯片集成功能很全面简洁了。

在这里插入图片描述

2. 开发EP设备驱动要做的事

  1. 设置头部信息寄存器,配置头部信息,包括VID、PID 、 CLASS等;
  2. 分配BAR空间内存,建立 inbount 映射, 从而,RC端建立完OutBound映射完成后,就可以正常访问BAR空间了,此处EP端BAR空间内存,可任意指定分配EP存储域地址,就像EP端设备开了一个内存窗口给RC端一样;
  3. 如果需要实现EP端访问RC端地址功能,则还需要申请虚拟地址内存空间,建立 OutBond 映射。

三、NXP LS1046A PCIE EP端驱动

1. LS1046A处理器简介

LS1046A是一款高性能的64位ARM四核处理器。LS1046A处理器将四个64位ARM Cortex-A72内核与数据包处理加速和高速外设相集成。CoreMarks®测试高达45000分,可与10Gb以太网、第三代PCIe、SATA 3.0、USB 3.0和QSPI接口配对,是一系列企业和服务提供商联网、存储、安全和工业应用的完美产品组合。

2. 开发环境介绍

主机开发系统:UBUNTU16.04
LS1046A Linux内核版本: Linux5.8 (使用高版本是因为其中 LS1046A 的EP驱动已经存在了)

3. 驱动源码介绍

  1. LINUX 5.8中包含的关于LS1046A 处理器 的PCIE EP驱 设备树信息, 注意你最终配置的PCIE控制器,因为处理器有三个PCIE控制器,我选择的是PCIE1,也要注意status 设置成 enable :
    	pcie_ep@3400000 {
    		compatible = "fsl,ls1046a-pcie-ep","fsl,ls-pcie-ep";
    		reg = <0x00 0x03400000 0x0 0x00100000
    			0x40 0x00000000 0x8 0x00000000>;
    		reg-names = "regs", "addr_space";
    		num-ib-windows = <6>;
    		num-ob-windows = <8>;
    		status = "disabled";
    	};
    
  2. 根据设备树 compatible 信息寻找对应驱动程序, LINUX 5.8中包含的关于LS1046A 处理器 的PCIE EP驱动文件有 :
    (1) pci-layerscape-ep.c drivers\pci\controller\dwc (ls1046a ep驱动)
    (2) pcie-designware-ep.c drivers\pci\controller\dwc (desigware ep架构)
    (3)pci-epc-core.c drivers\pci\endpoint (ep控制器核心层)
    (4)pci-epf-test.c drivers\pci\endpoint\functions (EP侧驱动测试程序)
    (5)pci_endpoint_test.c drivers\misc (RC主机侧驱动测试程序)

3.1. 源码概览

// LS1046A EP驱动 probe 函数
static int __init ls_pcie_ep_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct dw_pcie *pci;
	struct ls_pcie_ep *pcie;
	struct resource *dbi_base;
	int ret;
	struct device_node *np = dev->of_node;
	struct device_node *msi_node;
	
    # 获取 MSI 设备树节点, 这个是我自己添加的,为了实现RC端触发EP中断,可忽略
	msi_node = of_parse_phandle(np, "msi-parent", 0);
	if (!msi_node) {
		dev_err(dev, "GP failed to find msi-parent\n");
		return -EINVAL;
	}
	# 获取 MSI 中断 ,可忽略
	ret = irq_of_parse_and_map(msi_node, 0);
	if(ret > 0)
	{
	    // 设置中断函数, 借鉴LS1046A RC功能时操作,可忽略
		__irq_set_handler(ret, test_pcie_msi_isr, 1, NULL);
		printk("GP Request IRQ ret = %d\n", ret);
	}
    # 以上我自己添加的,可忽略
    
    # 为结构体分配内存,此接口自动释放内存
	pcie = devm_kzalloc(dev, sizeof(*pcie), GFP_KERNEL);
	if (!pcie)
		return -ENOMEM;
	pci = devm_kzalloc(dev, sizeof(*pci), GFP_KERNEL);
	if (!pci)
		return -ENOMEM;
    # 硬件寄存器信息,并建立映射
	dbi_base = platform_get_resource_byname(pdev, IORESOURCE_MEM, "regs");
	pci->dbi_base = devm_pci_remap_cfg_resource(dev, dbi_base);
	if (IS_ERR(pci->dbi_base))
		return PTR_ERR(pci->dbi_base);

	pci->dbi_base2 = pci->dbi_base + PCIE_DBI2_OFFSET;
	pci->dev = dev;
	# 操作函数,当PCIE卡与主机连接时,调用其 start_link 函数
	pci->ops = &ls_pcie_ep_ops;
	pcie->pci = pci;
    
	platform_set_drvdata(pdev, pcie); 
    # 添加 EP 控制器, 重点 !!!
	ret = ls_add_pcie_ep(pcie, pdev);
	
	return ret;
}

static int __init ls_add_pcie_ep(struct ls_pcie_ep *pcie,
					struct platform_device *pdev)
{
	struct dw_pcie *pci = pcie->pci;
	struct device *dev = pci->dev;
	struct dw_pcie_ep *ep;
	struct resource *res;
	int ret;

	ep = &pci->ep;
	
	# pcie_ep_ops 操作函数,包含 ep_init 、 raise_irq 、 get_features
	# 会在相应部分进行调用,可先记录
	# ep_init  主要是吧 6个 BAR 空间清0
	# raise_irq 产生中断,EP端触发RC端的三种方式
	# get_features 记录一下PCI控制器的特征,后面利用这些特性去做相应的操作
	# 比如 msi_capable 决定是否使用MSI
	
	ep->ops = &pcie_ep_ops;
    # 设备树信息,6个inbount 窗口 8个outbound 窗口
	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "addr_space");
	if (!res)
		return -EINVAL;

	ep->phys_base = res->start;
	ep->addr_size = resource_size(res);
    # 重点, ep PCI控制器初始化
	ret = dw_pcie_ep_init(ep);
	if (ret) {
		dev_err(dev, "failed to initialize endpoint\n");
		return ret;
	}

	return 0;
}

int dw_pcie_ep_init(struct dw_pcie_ep *ep)
{
	int ret;
	void *addr;
	struct pci_epc *epc;
	struct dw_pcie *pci = to_dw_pcie_from_ep(ep);
	struct device *dev = pci->dev;
	struct device_node *np = dev->of_node;
	const struct pci_epc_features *epc_features;
  	
  	# 参数的判断
	if (!pci->dbi_base || !pci->dbi_base2) {
		dev_err(dev, "dbi_base/dbi_base2 is not populated\n");
		return -EINVAL;
	}
    # 获取硬件信息,inbound windows 数量 6
	ret = of_property_read_u32(np, "num-ib-windows", &ep->num_ib_windows);
	if (ret < 0) {
		dev_err(dev, "Unable to read *num-ib-windows* property\n");
		return ret;
	}
	if (ep->num_ib_windows > MAX_IATU_IN) {
		dev_err(dev, "Invalid *num-ib-windows*\n");
		return -EINVAL;
	}
	
	# outbound windows 数量 8
	ret = of_property_read_u32(np, "num-ob-windows", &ep->num_ob_windows);
	if (ret < 0) {
		dev_err(dev, "Unable to read *num-ob-windows* property\n");
		return ret;
	}
	if (ep->num_ob_windows > MAX_IATU_OUT) {
		dev_err(dev, "Invalid *num-ob-windows*\n");
		return -EINVAL;
	}
    # 窗口映射控制字段内存分配,LS1046A 有6个Inbound窗口和8个OutBound窗口
	ep->ib_window_map = devm_kcalloc(dev,
					 BITS_TO_LONGS(ep->num_ib_windows),
					 sizeof(long),
					 GFP_KERNEL);
	if (!ep->ib_window_map)
		return -ENOMEM;
    # Outbound 窗口控制结构体内存分配
	ep->ob_window_map = devm_kcalloc(dev,
					 BITS_TO_LONGS(ep->num_ob_windows),
					 sizeof(long),
					 GFP_KERNEL);
	if (!ep->ob_window_map)
		return -ENOMEM;

	addr = devm_kcalloc(dev, ep->num_ob_windows, sizeof(phys_addr_t),
			    GFP_KERNEL);
	if (!addr)
		return -ENOMEM;
	ep->outbound_addr = addr;
	
    # 创建控制器结构体, epc_ops 是控制器操作函数
    # 包括设置头部、设备BAR、设置MSI、建立outbound内存映射等
	epc = devm_pci_epc_create(dev, &epc_ops);
	if (IS_ERR(epc)) {
		dev_err(dev, "Failed to create epc device\n");
		return PTR_ERR(epc);
	}

	ep->epc = epc;
	epc_set_drvdata(epc, ep);
    # 调用ep 初始化函数,上文提过的 ep_init 函数
	if (ep->ops->ep_init)
		ep->ops->ep_init(ep);

	ret = of_property_read_u8(np, "max-functions", &epc->max_functions);
	if (ret < 0)
		epc->max_functions = 1;
    # 分配EP控制器所使用的物理内存,并初始化结构体,所使用的内存大小区域是设备树指定的
	ret = pci_epc_mem_init(epc, ep->phys_base, ep->addr_size,
			       ep->page_size);
	if (ret < 0) {
		dev_err(dev, "Failed to initialize address space\n");
		return ret;
	}
    # allocate memory address from EPC addr space
	ep->msi_mem = pci_epc_mem_alloc_addr(epc, &ep->msi_mem_phys,
					     epc->mem->window.page_size);
	if (!ep->msi_mem) {
		dev_err(dev, "Failed to reserve memory for MSI/MSI-X\n");
		return -ENOMEM;
	}
    # 获取LS1046A驱动中设置的 features,包括是否支持msi、msix等,变量: ls_pcie_epc_features
	if (ep->ops->get_features) {
		epc_features = ep->ops->get_features(ep);
		if (epc_features->core_init_notifier)
			return 0;
	}

	return dw_pcie_ep_init_complete(ep);
}

以上,LS1046A EP控制器驱动配置大体流程,控制器抽象结构体已经在内核中存在;驱动加载完毕,你还是看不到任何相关信息,因为现在只是使EP存在于内核中,还没有使用;

3.2. EP测试程序 pci-epf-test.c

这也是一个驱动模块,不过是 PCI TEST ENDPOINT FUNCTION;重点是ops 的bind函数 pci_epf_test_bind
它相对应的主机侧驱动为 pci_endpoint_test.c;
相关文档 :
pci-test.txt Documentation\PCI\endpoint\function\binding
pci-endpoint-test.txt Documentation\misc-devices

static struct pci_epf_ops ops = {
    # 解绑的时候调用
	.unbind	= pci_epf_test_unbind,
	# 绑定的时候调用
	.bind	= pci_epf_test_bind,
};

static struct pci_epf_driver test_driver = {
	.driver.name	= "pci_epf_test",
	.probe		= pci_epf_test_probe,
	.id_table	= pci_epf_test_ids,
	.ops		= &ops,
	.owner		= THIS_MODULE,
};

static int __init pci_epf_test_init(void)
{
	int ret;

	kpcitest_workqueue = alloc_workqueue("kpcitest",
					     WQ_MEM_RECLAIM | WQ_HIGHPRI, 0);
	if (!kpcitest_workqueue) {
		pr_err("Failed to allocate the kpcitest work queue\n");
		return -ENOMEM;
	}
    #注册一个驱动
	ret = pci_epf_register_driver(&test_driver);
	if (ret) {
		pr_err("Failed to register pci epf test driver --> %d\n", ret);
		return ret;
	}

	return 0;
}


static int pci_epf_test_bind(struct pci_epf *epf)
{
	int ret;
	struct pci_epf_test *epf_test = epf_get_drvdata(epf);
	struct pci_epf_header *header = epf->header;
	struct pci_epc *epc = epf->epc;
	struct device *dev = &epf->dev;

	if (WARN_ON_ONCE(!epc))
		return -EINVAL;
		
    # 在这就是根据前文提到的结构体 ls_pcie_epc_features 里面的标志进行不同的设置
    # 此处 linkup_notifier  = false
	if (epc->features & EPC_FEATURE_NO_LINKUP_NOTIFIER)
		epf_test->linkup_notifier = false;
	else
		epf_test->linkup_notifier = true;
	#  msix_available = false
	epf_test->msix_available = epc->features & EPC_FEATURE_MSIX_AVAILABLE;
	# 这里使用了 bar0 用于测试空间
	epf_test->test_reg_bar = EPC_FEATURE_GET_BAR(epc->features);
    
    # 向硬件寄存器里写设备头部信息,比如PID VID,最终调用的是 epc->ops->write_heade 
    # 这是上文提到的 epc_ops->write_heade 内的函数
	ret = pci_epc_write_header(epc, epf->func_no, header);
	if (ret) {
		dev_err(dev, "Co
		epf_test->msix_availnfiguration header write failed\n");
		return ret;
	}

    # 在这里EP端,只是申请了内存,用于BAR空间,大小在 bar_size[] 数组中定义
    # 并且物理地址保存在 epf->bar[bar].phys_addr 中,epf->bar[bar].addr 保存相应的虚拟地址;
    # 调用了 pci_epf_alloc_space,这个函数中,dma_alloc_coherent 申请DMA一致性内存,物理地址连续
    # 并且初始化bar空间的结构体,记录一下BAR空间的物理地址,虚拟地址,大小以及FLAGS
    # 在这里只是记录,后面会调用 pci_epf_test_set_bar 去寄存去设置。
    # 这里理解了很重要,此程序申请的是DRAM内存用于BAR空间的地址范围
    # 比如申请了0x1000 - 0x2000 作为BAR0空间,这一段是内部存储器的访问地址;
    # 此外,此外,此外,我们也可以定义非存储器的地址,理论上EP端的存储器域地址都可以设置
    # 比如物理地址0,比如IIC某个寄存器的物理地址等等等,看业务需求,前提当然是IIC控制器属于一个PCI设备。物理地址给RC端看
    # 映射为虚拟地址给本地kernel去操作,我们都知道,内核不能直接操作物理地址。
	ret = pci_epf_test_alloc_space(epf);
	if (ret)
		return ret;
		
    # 这里进行了 BAR 空间的硬件设置,设置完即生效了; 本质是调用了 epc->ops->set_bar
    # 同样是 epc_ops->set_bar 的操作函数,后面细讲 
	ret = pci_epf_test_set_bar(epf);
	if (ret)
		return ret;
		
    # 设置 MIS 中断 调用 epc->ops->set_msi,即 epc_ops->set_msi
	ret = pci_epc_set_msi(epc, epf->func_no, epf->msi_interrupts);
	if (ret) {
		dev_err(dev, "MSI configuration failed\n");
		return ret;
	}
    # 此处,暂时不需要,设置 MSIX
	if (epf_test->msix_available) {
		ret = pci_epc_set_msix(epc, epf->func_no, epf->msix_interrupts);
		if (ret) {
			dev_err(dev, "MSI-X configuration failed\n");
			return ret;
		}
	}

    # 创建一个工作队列, 循环调用 pci_epf_test_cmd_handler 函数,接收 RC 端数据,进行相应的操作
    # 这就是业务,PCIE 作为传输,数据具体做什么用,就是你说了算;
    # 当然,这种查询标志位的方式消耗性能大,常用的是使用 MSI 中断方式。
	if (!epf_test->linkup_notifier)
		queue_work(kpcitest_workqueue, &epf_test->cmd_handler.work);

	return 0;
}
这一部分其他内容也比较多,在我们进行PCIE测试的时候,按照如下步骤, 这不是本文重点,略提及,以后再补充:
  1. 在EP端,cd /sys/kernel/config/pci_ep/
  2. mkdir functions/pci_epf_test/func1
  3. echo 0x1957 > functions/pci_epf_test/func1/vendorid #设置VID
  4. echo 0x81c0 > functions/pci_epf_test/func1/deviceid #设置PID
  5. ln -s functions/pci_epf_test/func1 controllers/3500000.pcie/
  6. 在RC端,重新扫描PCIE总线,比如:
    echo 1 > /sys/class/pci_bus/0000:01/device/remove
    echo 1 > /sys/bus/pci/rescan
    设备是最开始发现的卡的设备,重新扫描之后 lspci 就会发现VID、PID与设置一样的PCIE设备了;

3.3. EP端设备配置空间寄存器

EP设备配置空间

3.4. EP端头部信息设置

在我们按上述创建设备文件,并进行绑定之后,系统就会调用 pci_epf_test_bind 这个函数,这个函数对EP端基本信息进行了设置,其中包括头部信息设置、不仅仅涉及BAR地址空间的设置, 上一节对这个函数进行了简单的讲解。后期如果做驱动开发,可以在这个函数的基础上改造进行设备初始化。 在这里你要明白,这一部分配置已经涉及业务的操作了,我的意思是,头部信息的配置、BAR空间的配置,已经是项目需求的配置了,比如你是要做一个显卡还是网卡,PID,VID就得相应地设置了,需要使用几个BAR空间,及其大小,也要定下来,等等等等。 上一节提到,对头部信息的设置是调用 pci_epc_write_heade(),最终会调用 epc_ops->write_heade,接下来我们看看这个函数怎么操作的。
# 控制器操作函数结构体,这一部分对于LS1046来说不用修改,我们在此基础上直接使用即可
# 对于其他处理器,我们仅作参考
static const struct pci_epc_ops epc_ops = {
	.write_header		= dw_pcie_ep_write_header,
	.set_bar		= dw_pcie_ep_set_bar,
	.clear_bar		= dw_pcie_ep_clear_bar,
	.map_addr		= dw_pcie_ep_map_addr,
	.unmap_addr		= dw_pcie_ep_unmap_addr,
	.set_msi		= dw_pcie_ep_set_msi,
	.get_msi		= dw_pcie_ep_get_msi,
	.set_msix		= dw_pcie_ep_set_msix,
	.get_msix		= dw_pcie_ep_get_msix,
	.raise_irq		= dw_pcie_ep_raise_irq,
	.start			= dw_pcie_ep_start,
	.stop			= dw_pcie_ep_stop,
	.get_features		= dw_pcie_ep_get_features,
};

# 使用此函数,只要构造变量 struct pci_epf_header *hdr,自定义变量,传入即可
static int dw_pcie_ep_write_header(struct pci_epc *epc, u8 func_no, 
									struct pci_epf_header *hdr)
{
	struct dw_pcie_ep *ep = epc_get_drvdata(epc);
	struct dw_pcie *pci = to_dw_pcie_from_ep(ep);
    # 使能 dbi 读写,写相应寄存器,具体查看芯片手册
	dw_pcie_dbi_ro_wr_en(pci);
	# 写 VID 进相应寄存器 0x00
	dw_pcie_writew_dbi(pci, PCI_VENDOR_ID, hdr->vendorid);
	# 写 PID 0x02
	dw_pcie_writew_dbi(pci, PCI_DEVICE_ID, hdr->deviceid);
	# 版本号 0x08
	dw_pcie_writeb_dbi(pci, PCI_REVISION_ID, hdr->revid);
	# CLASS
	dw_pcie_writeb_dbi(pci, PCI_CLASS_PROG, hdr->progif_code);
	dw_pcie_writew_dbi(pci, PCI_CLASS_DEVICE,
			   hdr->subclass_code | hdr->baseclass_code << 8);
	# CACHE LINE SIZE
	dw_pcie_writeb_dbi(pci, PCI_CACHE_LINE_SIZE,
			   hdr->cache_line_size);
	dw_pcie_writew_dbi(pci, PCI_SUBSYSTEM_VENDOR_ID,
			   hdr->subsys_vendor_id);
	dw_pcie_writew_dbi(pci, PCI_SUBSYSTEM_ID, hdr->subsys_id);
	dw_pcie_writeb_dbi(pci, PCI_INTERRUPT_PIN,
			   hdr->interrupt_pin);
    # 失能 读写位
	dw_pcie_dbi_ro_wr_dis(pci);

	return 0;
}

3.5. EP端BAR空间设置

BAR空间设置,主要是对上文中获取的物理地址以及大小进行inbound映射,并设置BAR相关寄存器,首地址及大小,上文提到,获取信息之后,调用 dw_pcie_ep_set_bar 来进行硬件寄存器设置生效。
static int dw_pcie_ep_set_bar(struct pci_epc *epc, u8 func_no,
			      struct pci_epf_bar *epf_bar)
{
	int ret;
	struct dw_pcie_ep *ep = epc_get_drvdata(epc);
	struct dw_pcie *pci = to_dw_pcie_from_ep(ep);
	enum pci_barno bar = epf_bar->barno;
	size_t size = epf_bar->size;
	int flags = epf_bar->flags;
	enum dw_pcie_as_type as_type;
	# BAR空间从首地址 0x10 开始, 6个BAR
	u32 reg = PCI_BASE_ADDRESS_0 + (4 * bar);

	# 我们使用 MEM 映射
	if (!(flags & PCI_BASE_ADDRESS_SPACE))
		as_type = DW_PCIE_AS_MEM;
	else
		as_type = DW_PCIE_AS_IO;

	# 这个函数就是将 BAR 空间对应的物理地址建立 inbound 映射
	# 这样就实现了EP端PCI域到EP存储域的映射函数
	# 如果 RC端已经进行了 outbound 映射,那么本函数运行完之后,RC端就可以直接访问这段EP端内存了
	# bar 表示第几个 BAR 
	# epf_bar->phys_addr 我们之前申请的内存空间的物理地址; as_type = DW_PCIE_AS_MEM
	ret = dw_pcie_ep_inbound_atu(ep, bar, epf_bar->phys_addr, as_type);
	if (ret)
		return ret;
    
    # 以下是设置 BAR 空间的大小
	dw_pcie_dbi_ro_wr_en(pci);

	dw_pcie_writel_dbi2(pci, reg, lower_32_bits(size - 1));
	dw_pcie_writel_dbi(pci, reg, flags);

	if (flags & PCI_BASE_ADDRESS_MEM_TYPE_64) {
	    # 如果使用64位地址,再设置高位
		dw_pcie_writel_dbi2(pci, reg + 4, upper_32_bits(size - 1));
		dw_pcie_writel_dbi(pci, reg + 4, 0);
	}

	ep->epf_bar[bar] = epf_bar;
	dw_pcie_dbi_ro_wr_dis(pci);

	return 0;
}

3.6. EP端物理地址映射

EP 端物理地址的映射包括 inbound 映射以及 outbound 映射。inbound 映射完毕之后,PCI域中的其他设备就可以访问这个地址范围了,比如RC端就可以访问 EP 端我们申请设置好的BAR空间;outbound 映射完毕之后,EP端就可以通过访问相应的本地虚拟地址,间接访问映射成功的PCI域地址,比如EP端就可以访问主机物理内存了; 你可能会疑问,outbound 之后,为什么说EP端可以访问主机物理内存。是这样的,这句话的前提是我们RC端是X86主板的硬件环境下,默认情况下, x86系列cpu地址空间和pci地址空间是重合的,即为同一空间;而非x86 cpu的cpu地址空间和pci地址空间为两个独立的空间, 在PCI域你可以和X86 CPU一样,看到所有的物理地址,所以当EP端建立了OUTBOUND映射,他就可以访问映射到PCI域的地址了。具体参考这一片博客:

https://blog.csdn.net/pwl999/article/details/78212508

# 映射inbound
static int dw_pcie_ep_inbound_atu(struct dw_pcie_ep *ep, enum pci_barno bar,
				  dma_addr_t cpu_addr,
				  enum dw_pcie_as_type as_type)
{
	int ret;
	u32 free_win;
	struct dw_pcie *pci = to_dw_pcie_from_ep(ep);

    # 获取 6 个inbound windows 中空闲的一个,只是一个管理数组
	free_win = find_first_zero_bit(ep->ib_window_map, ep->num_ib_windows);
	if (free_win >= ep->num_ib_windows) {
		dev_err(pci->dev, "No free inbound window\n");
		return -EINVAL;
	}
	# 具体的映射函数
	ret = dw_pcie_prog_inbound_atu(pci, free_win, bar, cpu_addr,
				       as_type);
	if (ret < 0) {
		dev_err(pci->dev, "Failed to program IB window\n");
		return ret;
	}

	ep->bar_to_atu[bar] = free_win;
	# 设置一下标志位,表示正在使用这个窗口
	set_bit(free_win, ep->ib_window_map);

	return 0;
}

# 具体的 Inbound 硬件寄存器设置函数
int dw_pcie_prog_inbound_atu(struct dw_pcie *pci, int index, int bar,
			     u64 cpu_addr, enum dw_pcie_as_type as_type)
{
	int type;
	u32 retries, val;
	
	# 这个根据硬件版本,进行设置的, iatu_unroll_enabled = false
	if (pci->iatu_unroll_enabled)
		return dw_pcie_prog_inbound_atu_unroll(pci, index, bar,
						       cpu_addr, as_type);

	dw_pcie_writel_dbi(pci, PCIE_ATU_VIEWPORT, PCIE_ATU_REGION_INBOUND |
			   index);
	
	# cpu_addr 即为EP本地存储域地址,即BAR空间的物理首地址,写入寄存器
	dw_pcie_writel_dbi(pci, PCIE_ATU_LOWER_TARGET, lower_32_bits(cpu_addr));
	dw_pcie_writel_dbi(pci, PCIE_ATU_UPPER_TARGET, upper_32_bits(cpu_addr));

	switch (as_type) {
	case DW_PCIE_AS_MEM:
		type = PCIE_ATU_TYPE_MEM;
		break;
	case DW_PCIE_AS_IO:
		type = PCIE_ATU_TYPE_IO;
		break;
	default:
		return -EINVAL;
	}

	dw_pcie_writel_dbi(pci, PCIE_ATU_CR1, type);
	dw_pcie_writel_dbi(pci, PCIE_ATU_CR2, PCIE_ATU_ENABLE
			   | PCIE_ATU_BAR_MODE_ENABLE | (bar << 8));

	/*
	 * Make sure ATU enable takes effect before any subsequent config
	 * and I/O accesses.
	 */
	# 确保设置生效
	for (retries = 0; retries < LINK_WAIT_MAX_IATU_RETRIES; retries++) {
		val = dw_pcie_readl_dbi(pci, PCIE_ATU_CR2);
		if (val & PCIE_ATU_ENABLE)
			return 0;

		mdelay(LINK_WAIT_IATU);
	}
	dev_err(pci->dev, "Inbound iATU is not being enabled\n");

	return -EBUSY;
}

详细的 LS1046A 寄存器含义,后面补上。
下面这一段代码是我自己参照上面的程序写,作用是在ep端申请一块内存用于BAR空间,用于实现RC端主机发送数据到卡一侧的功能,即将BAR空间当作数据缓冲区,而不是一般使用的那种配置寄存器,其本质都是一样的;主机往首地址写个1,EP端这边就能在相应的首地址读到1,也即主机直接读写EP端的内存。
    # 申请EP本地内存,大小 bar_size数组指定,base 为相应的虚拟地址
    # 物理地址保存在 epf->bar[pc_to_ep_bar].phys_addr 中
    base = pci_epf_alloc_space(epf, bar_size[pc_to_ep_bar], pc_to_ep_bar,
                               epc_features->align);
    if (!base)
    {
        dev_err(dev, "Failed to allocated register space\n");
        return -ENOMEM;
    }
    epf_test->reg[pc_to_ep_bar] = base;

	# 全局变量保存这一段空间的虚拟地址
    buf_start = base;
    # 全局变量保存这一段空间的物理地址
	buf_start_phy = epf->bar[pc_to_ep_bar].phys_addr;
    memset(buf_start, 0, RINGBUF_START_OFFSET);
   
    # 同时,你也可以不申请空间
    # 前面我们说过,inbound 的空间可以是本地的所有物理地址,不局限于DRAM空间
    # 以下则是,直接构造结构体,把物理地址 0x15A3000 直接映射进去,大小128字节
    # 这个地址是LS1046处理器产生中断的寄存器,我用做RC端触发EP端中断的功能
    epf_test->reg[ep_irq_bar] = NULL;
	epf->bar[ep_irq_bar].phys_addr = 0x15A3000;
	epf->bar[ep_irq_bar].addr = NULL;
	epf->bar[ep_irq_bar].size = 128;
	epf->bar[ep_irq_bar].barno = ep_irq_bar;
	epf->bar[ep_irq_bar].flags |= PCI_BASE_ADDRESS_MEM_TYPE_32;

3.7. RC端访问BAR地址空间

当EP端配置好了BAR空间以及头部信息,并且绑定之后,在RC端重新扫描之后,也即运行完 3.2 节最后的步骤之后, lspci 查看设备,第一个设备即为我的设备,同样,在系统设备文件里能查看其bar空间,路径为 /sys/class/bus/pci/devices/相应的设备/resource ,我的环境没了,没有截图:

在这里插入图片描述

在上面提到,主机侧设备驱动为 pci_endpoint_test.c ,支持的设备ID为 :

static const struct pci_device_id pci_endpoint_test_tbl[] = {
	{ PCI_DEVICE(PCI_VENDOR_ID_TI, PCI_DEVICE_ID_TI_DRA74x),
	  .driver_data = (kernel_ulong_t)&default_data,
	},
	{ PCI_DEVICE(PCI_VENDOR_ID_TI, PCI_DEVICE_ID_TI_DRA72x),
	  .driver_data = (kernel_ulong_t)&default_data,
	},
	# 我的设备, 飞思卡尔 ,0x81c0, 没有自行添加
	{ PCI_DEVICE(PCI_VENDOR_ID_FREESCALE, 0x81c0) },
	{ PCI_DEVICE_DATA(SYNOPSYS, EDDA, NULL) },
	{ PCI_DEVICE(PCI_VENDOR_ID_TI, PCI_DEVICE_ID_TI_AM654),
	  .driver_data = (kernel_ulong_t)&am654_data
	},
	{ PCI_DEVICE(PCI_VENDOR_ID_RENESAS, PCI_DEVICE_ID_RENESAS_R8A774C0),
	},
	{ }
};
编译加载驱动之后,就会调用 pci_endpoint_test_probe 函数,这个函数就是主机侧PCI驱动的初始化函数,下面来分析一下。
static int pci_endpoint_test_probe(struct pci_dev *pdev,
				   const struct pci_device_id *ent)
{
	int err;
	int id;
	char name[24];
	enum pci_barno bar;
	void __iomem *base;
	struct device *dev = &pdev->dev;
	struct pci_endpoint_test *test;
	struct pci_endpoint_test_data *data;
	enum pci_barno test_reg_bar = BAR_0;
	struct miscdevice *misc_device;
	
	# 我们的不是 PCI 桥
	if (pci_is_bridge(pdev))
		return -ENODEV;
	# 申请结构体空间
	test = devm_kzalloc(dev, sizeof(*test), GFP_KERNEL);
	if (!test)
		return -ENOMEM;
	# 测试空间 BAR0
	test->test_reg_bar = 0;
	test->alignment = 0;
	test->pdev = pdev;
	test->irq_type = IRQ_TYPE_UNDEFINED;

	if (no_msi)
		irq_type = IRQ_TYPE_LEGACY;
	# 获取驱动数据
	data = (struct pci_endpoint_test_data *)ent->driver_data;
	if (data) {
		test_reg_bar = data->test_reg_bar;
		test->test_reg_bar = test_reg_bar;
		test->alignment = data->alignment;
		irq_type = data->irq_type;
	}
	# 初始化一些变量,完成量与锁
	init_completion(&test->irq_raised);
	mutex_init(&test->mutex);
	# 设置DMA掩码
	if ((dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(48)) != 0) &&
	    dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)) != 0) {
		dev_err(dev, "Cannot set DMA mask\n");
		return -EINVAL;
	}
	# 使能设备,一般流程
	err = pci_enable_device(pdev);
	if (err) {
		dev_err(dev, "Cannot enable PCI device\n");
		return err;
	}
	# 请求资源IO
	err = pci_request_regions(pdev, DRV_MODULE_NAME);
	if (err) {
		dev_err(dev, "Cannot obtain PCI resources\n");
		goto err_disable_pdev;
	}
	# 设定设备工作在总线主设备模式
	pci_set_master(pdev);
	# 申请中断向量号,里面调用内核接口 pci_alloc_irq_vectors 申请
	if (!pci_endpoint_test_alloc_irq_vectors(test, irq_type))
		goto err_disable_irq;
	# 遍历 EP 设备透漏出来的 BAR 空间
	for (bar = 0; bar < PCI_STD_NUM_BARS; bar++) {
	    # 判断 FLAGS
		if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) {
			# 进行映射,系统看到的是物理地址,映射为虚拟地址,便于内核进行操作
			# 比如 bar == 0的时候,我们得到base 地址, 往 *base = 1,在EP端则可以读到 BAR0 首地址为 1
			# 当然,要做好中断触发的互斥机制,测试程序EP端是查询方式,我后来修改为中断方式
		
			base = pci_ioremap_bar(pdev, bar);
			if (!base) {
				dev_err(dev, "Failed to read BAR%d\n", bar);
				WARN_ON(bar == test_reg_bar);
			}
			# 记录下来
			test->bar[bar] = base;
		}
	}

	test->base = test->bar[test_reg_bar];
	if (!test->base) {
		err = -ENOMEM;
		dev_err(dev, "Cannot perform PCI test without BAR%d\n",
			test_reg_bar);
		goto err_iounmap;
	}
	# 驱动相关接口,设置驱动数据
	pci_set_drvdata(pdev, test);
	
	# 申请唯一ID
	id = ida_simple_get(&pci_endpoint_test_ida, 0, 0, GFP_KERNEL);
	if (id < 0) {
		err = id;
		dev_err(dev, "Unable to get id\n");
		goto err_iounmap;
	}

	snprintf(name, sizeof(name), DRV_MODULE_NAME ".%d", id);
	test->name = kstrdup(name, GFP_KERNEL);
	if (!test->name) {
		err = -ENOMEM;
		goto err_ida_remove;
	}
	# 申请中断,中断处理函数  pci_endpoint_test_irqhandler
	if (!pci_endpoint_test_request_irq(test))
		goto err_kfree_test_name;

	misc_device = &test->miscdev;
	misc_device->minor = MISC_DYNAMIC_MINOR;
	misc_device->name = kstrdup(name, GFP_KERNEL);
	if (!misc_device->name) {
		err = -ENOMEM;
		goto err_release_irq;
	}
	# 设备文件操作函数,根据自己的业务去处理读写函数
	misc_device->fops = &pci_endpoint_test_fops,
	
	#  创建杂项设备文件
	err = misc_register(misc_device);
	if (err) {
		dev_err(dev, "Failed to register device\n");
		goto err_kfree_name;
	}

	return 0;

err_kfree_name:
	kfree(misc_device->name);

err_release_irq:
	pci_endpoint_test_release_irq(test);

err_kfree_test_name:
	kfree(test->name);

err_ida_remove:
	ida_simple_remove(&pci_endpoint_test_ida, id);

err_iounmap:
	for (bar = 0; bar < PCI_STD_NUM_BARS; bar++) {
		if (test->bar[bar])
			pci_iounmap(pdev, test->bar[bar]);
	}

err_disable_irq:
	pci_endpoint_test_free_irq_vectors(test);
	pci_release_regions(pdev);

err_disable_pdev:
	pci_disable_device(pdev);

	return err;
}
在上面的基础上,进行修改,就可以实现自己的RC侧PCI设备驱动函数,重点就是进行BAR空间的映射,映射完毕之后,对BAR空间的虚拟地址进行相应操作就可以实现RC和EP的通信功能。至于具体的数据用来做什么,就是业务逻辑的事情了。 下面程序是我根据上面的官方程序修改的一个测试驱动 probe 函数:
static int pci_endpoint_test_probe(struct pci_dev *pdev,
                                   const struct pci_device_id *ent)
{
    int err;
    int id;
    char name[24];
    void __iomem *base;
    struct device *dev = &pdev->dev;
    struct miscdevice *misc_device;

	# 所有到的完成量,锁等变量初始化
    init_completion(&ep_write_app_cp);
    init_completion(&ep_read_app_cp);
    init_completion(&ep_write_to_rc_kernel_cp);

    mutex_init(&app_pc_read_lock);
    mutex_init(&app_pc_write_lock);

	# 1. DMA 掩码设置 
    if ((dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(48)) != 0) &&
            dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)) != 0)
    {
        dev_err(dev, "Cannot set DMA mask\n");
        return -EINVAL;
    }
	
	# 2. 使能设备
    err = pci_enable_device(pdev);
    if (err)
    {
        dev_err(dev, "Cannot enable PCI device\n");
        return err;
    }
	# 3. 请求资源
    err = pci_request_regions(pdev, DRV_MODULE_NAME);
    if (err)
    {
        dev_err(dev, "Cannot obtain PCI resources\n");
        goto err_disable_pdev;
    }
	# 4. 设置主设备模式
    pci_set_master(pdev);

    /* Set up a single MSI interrupt */
    # 5. 使能MSI中断, 这一步根据你的硬件去设置,使用 MSI 方式
    #    如果你是用其他的方式,比如 MSIX 或者IO中断,就进行其他设置 
    if (pci_enable_msi(pdev))
    {
        dev_err(dev,
                "Failed to enable MSI interrupts. Aborting.\n");
        err = -ENODEV;
        goto err_disable_irq;
    }

	# 6. 申请中断,设置中断处理函数
    err = request_irq(pdev->irq, pci_endpoint_irqhandler, 0, "PCIE_EP", dev);
    if (err)
    {
        goto err_req_irq;
    }

    // Get Bar0 Space
    # 7. 获取 BAR0 空间并进行映射
    if (pci_resource_flags(pdev, 0) & IORESOURCE_MEM)
    {
        base = pci_ioremap_bar(pdev, 0);
        if (!base)
        {
            dev_err(&pdev->dev, "Failed to read BAR%d\n", 0);
            goto err_ioremap0;
        }
		# 保存BAR0 空间的虚拟地址,以便后续进行通信
        buf_start = (char *)base;
    }

    // Get Bar2 Space
    # 获取 BAR2 空间
    if (pci_resource_flags(pdev, 2) & IORESOURCE_MEM)
    {
        base = pci_ioremap_bar(pdev, 2);
        if (!base)
        {
            dev_err(&pdev->dev, "Failed to read BAR%d\n", 0);
            goto err_ioremap2;
        }

        bar2_s = (unsigned int *)base;
    }
	
	# 空间清零
    memset(buf_start, 0, RINGBUF_START_OFFSET);

	# 这是我的业务,创建了一个接受线程;BAR 空间作为两个系统的共享内存空间进行数据通信
    l_taskstr = kthread_run(loop_rv_thread,
                            NULL,
                            "Pice_Module_Rev");
    if (IS_ERR(l_taskstr))
    {
        err = PTR_ERR(l_taskstr);
        goto err_iounmap2;
    }

	# 以下创建设备文件,以便应用层进行设备操作
    id = 0;
    snprintf(name, sizeof(name), DRV_MODULE_NAME ".%d", id);

    misc_device = &mmisc;
    misc_device->minor = MISC_DYNAMIC_MINOR;
    misc_device->name = kstrdup(name, GFP_KERNEL);
    if (!misc_device->name)
    {
        err = -ENOMEM;
        goto err_kfree_name;
    }
    # 设备文件处理函数,根据自己的业务去处理
    misc_device->fops = &pci_endpoint_test_fops,

    err = misc_register(misc_device);
    if (err)
    {
        dev_err(dev, "Failed to register device\n");
        goto err_stop_thread;
    }

    return 0;

err_stop_thread:
    if (l_taskstr)
        kthread_stop(l_taskstr);

err_kfree_name:
    kfree(misc_device->name);

err_iounmap2:
    pci_iounmap(pdev, bar2_s);

err_ioremap2:
    pci_iounmap(pdev, buf_start);

err_ioremap0:
    free_irq(pdev->irq, dev);

err_req_irq:
    pci_disable_msi(pdev);

err_disable_irq:
    pci_release_regions(pdev);

err_disable_pdev:
    pci_disable_device(pdev);

    return err;
}
如果明白以上的流程,就会发现其实PCIE驱动没有特别复杂,其本质很简单,RC与EP相当于对共享内存进行读写操作,只不过是一块或多块跨系统的共享内存,所以要进行相应的互斥操作,具体怎么做,请查看其他资料,我是用两个中断进行乒乓互斥。当前环境下,无论是硬件寄存器还是内核驱动开发接口,相对来说已经很方便了。

3.8. EP端访问主机全部物理内存设置

3.7 是RC端访问EP侧地址,这一节则是EP端访问RC侧地址,应用则是,EP端访问主机侧全部物理内存。3.6 节我也说过,在X86 架构下,主机RC侧硬件架构下,其cpu地址空间和pci地址空间是重合的,RC侧 inbound默认已经是映射完成, 我们只需要在EP侧进行outbound 映射,就能访问RC侧物理地址了。
static const struct pci_epc_ops epc_ops = {
	...
	# 这就是 ep 侧 outbound 映射函数
	.map_addr		= dw_pcie_ep_map_addr,
	.unmap_addr		= dw_pcie_ep_unmap_addr,
	...
};

static int dw_pcie_ep_map_addr(struct pci_epc *epc, u8 func_no,
			       phys_addr_t addr,
			       u64 pci_addr, size_t size)
{
	int ret;
	struct dw_pcie_ep *ep = epc_get_drvdata(epc);
	struct dw_pcie *pci = to_dw_pcie_from_ep(ep);
	
	# 对 dw_pcie_ep_outbound_atu 进行了一次封装调用
	ret = dw_pcie_ep_outbound_atu(ep, addr, pci_addr, size);
	if (ret) {
		dev_err(pci->dev, "Failed to enable address\n");
		return ret;
	}

	return 0;
}


static int dw_pcie_ep_outbound_atu(struct dw_pcie_ep *ep, phys_addr_t phys_addr,
				   u64 pci_addr, size_t size)
{
	u32 free_win;
	struct dw_pcie *pci = to_dw_pcie_from_ep(ep);
	# 这个是一个窗口管理,变量的某一位代表这个窗口是否在用,不在使用就申请使用并置为 1
	free_win = find_first_zero_bit(ep->ob_window_map, ep->num_ob_windows);
	if (free_win >= ep->num_ob_windows) {
		dev_err(pci->dev, "No free outbound window\n");
		return -EINVAL;
	}

	# 具体的映射函数
	dw_pcie_prog_outbound_atu(pci, free_win, PCIE_ATU_TYPE_MEM,
				  phys_addr, pci_addr, size);

	# 置位,表示这个窗口在使用中
	set_bit(free_win, ep->ob_window_map);
	ep->outbound_addr[free_win] = phys_addr;
	return 0;
}

# 具体的硬件寄存器的操作,进行 Outbound 映射
# pci 本地对象结构体
#  index 窗口选择,一共有8个窗口,每个窗口映射一个策略
#  type  MEM 映射还是 IO 映射, PCI 的两种映射方式
#  cpu_addr 本地的CPU可以访问的物理地址,我们使用一个空闲的物理地址即可,即没有使用的
#            注意,这个地址不是DRAM地址,而是SOC不在使用的可以访问的物理地址
#            比如,64位系统,假设内存,外设等使用了前32G物理地址
#            那么剩下的2^48 - 32G 的可寻址的物理地址都可以用,假设能寻址48#   pci_addr 这个是要映射的PCI域地址,一般就从 0 开始映射32G就够用了,除非你知道你要访问的RC具体物理地址范围
#   size  映射大小
void dw_pcie_prog_outbound_atu(struct dw_pcie *pci, int index, int type,
			       u64 cpu_addr, u64 pci_addr, u32 size)
{
	u32 retries, val;
	# 不关心
	if (pci->ops->cpu_addr_fixup)
		cpu_addr = pci->ops->cpu_addr_fixup(pci, cpu_addr);
	# 不关心
	if (pci->iatu_unroll_enabled) {
		dw_pcie_prog_outbound_atu_unroll(pci, index, type, cpu_addr,
						 pci_addr, size);
		return;
	}
	# 硬件寄存器设置 LS1046A
	# 选择窗口
	dw_pcie_writel_dbi(pci, PCIE_ATU_VIEWPORT,
			   PCIE_ATU_REGION_OUTBOUND | index);
	# 设置本地地址低地址
	dw_pcie_writel_dbi(pci, PCIE_ATU_LOWER_BASE,
			   lower_32_bits(cpu_addr));
	# 设置本地地址高地址
	dw_pcie_writel_dbi(pci, PCIE_ATU_UPPER_BASE,
			   upper_32_bits(cpu_addr));
	# 设置映射大小
	dw_pcie_writel_dbi(pci, PCIE_ATU_LIMIT,
			   lower_32_bits(cpu_addr + size - 1));
	# 设置目标地址低地址
	dw_pcie_writel_dbi(pci, PCIE_ATU_LOWER_TARGET,
			   lower_32_bits(pci_addr));
	# 设置目标地址高地址
	dw_pcie_writel_dbi(pci, PCIE_ATU_UPPER_TARGET,
			   upper_32_bits(pci_addr));
	# 设置映射类型
	dw_pcie_writel_dbi(pci, PCIE_ATU_CR1, type);
	# 设置使能位
	dw_pcie_writel_dbi(pci, PCIE_ATU_CR2, PCIE_ATU_ENABLE);

	/*
	 * Make sure ATU enable takes effect before any subsequent config
	 * and I/O accesses.
	 */
	 
	# 确保地址转换单元生效
	for (retries = 0; retries < LINK_WAIT_MAX_IATU_RETRIES; retries++) {
		val = dw_pcie_readl_dbi(pci, PCIE_ATU_CR2);
		if (val & PCIE_ATU_ENABLE)
			return;

		mdelay(LINK_WAIT_IATU);
	}
	dev_err(pci->dev, "Outbound iATU is not being enabled\n");
}

调用上面的接口,就映射成功了,假设我们映射了本地物理地址0x10_0000_0000到 PCI域0 ,大小32G范围,那么将 物理地址 0x10_0000_0000 映射为本地虚拟地址 addr ,之后就可以直接进行读写操作了。比如读四个字节, 内核里执行 printk("%d \n", *(int *) addr),就能读取主机物理地址0的内容,当然,这可能不是主机DRAM内存的首地址。 如果在EP端应用层使用 mmap()库函数将这个物理地址0x10_0000_0000 映射到应用层进程虚拟地址空间,那么你就可以在EP侧应用层像操作自己本地内存一样直接操作RC侧的物理内存了,这一方面属于业务功能实现了,比如需要分析主机物理地址内容等。 对于主机 LINUX 物理内存的地址分布,后面我想再整理写一下。

四、飞腾新四核 FT2004 PCIE EP端驱动

1. 处理器简介

FT-2000/4 是一款面向桌面应用的高性能国产通用 4 核处理器。每 2 个核构成 1 个处理器核簇(Cluster,并共享 L2 Cache。处理器核通过片内高速互联网络及相关控制器与存储系统、I/O 系统相连。

在这里插入图片描述

一共包括五个控制器,控制器本身也是一个PCI设备。

在这里插入图片描述

2. 开发环境介绍

开发环境基本与 LS1046 一样。 操作系统 : ubuntu16 / ubuntu18 交叉编译器 : aarch64-linux-gnu-

3. 驱动开发

通过以上的讲解,我想你应该多多少少有点明白了PCIE的EP端以及RC端驱动开发,飞腾处理器的PCIE使用,根据 《FT-2000/4 软件编程手册》进行寄存器配置即可。

在这里插入图片描述

飞腾的EP驱动代码,基本就是按照文档去配置即可。如果需要源码,请自行去与飞腾技术人员联系索取。此处就不贴了。我就简单提一下一些注意的地方:
  1. 由于飞腾资料不全,以及架构不一样,所以不能自定义 BAR 空间的大小,可以使用配置的只有 BAR0 和 BAR2 空间,默认均为4M,首地址只能是文档里写的 0x23_00000000,我自己申请的内存进行配置,不生效,并不清楚为什么;
  2. 作为一个64位处理器,EP端访问RC空间的地址,设置映射,每次最多竟然只有4G,官方技术人员回复说是写死的,改变不了,这个就比较不专业了,估计是这个功能还没完善或者还没做。但是,EP向RC发送MSI中断,所写的地址是在0 - 4 G,所以,如果你在映射并操作 4 - 8G的RC内存时。如果想要向RC发送MSI中断,还需要进行重新映射,也就是说软件进行映射管理。而作为LS1046,直接映射32G,只需要知道首地址,直接操作所有内存就可以了。这一点很不爽。如果有人知道解决方案,可以沟通交流。当然,可以使用DMA读写主机内存,DMA的地址可以填写任何物理地址,也可以实现RC物理地址的访问,并且经过测试,速度更快。
  3. 就像前文提到的,我们可以使用BAR空间直接进行数据传输,主机写数据直接到4M的BAR空间,或者EP写到BAR空间,主机去读,从而进行了双向传输。但是这种方法,很慢。印象中,写100K可能需要100多us, 所以,进行传输时应尽可能使用DMA,主机分配一块连续内存,卡内分配一段连续内存,使用卡上的DMA来回搬移,速度会提升很大,10US左右吧,如果没记错的话,基本接近理论最大速率;
  4. 如果你正在使用飞腾FT2004处理器作为PCI EP设备或者RC,使用飞腾处理器的DMA作为传输工具,比如,使用FT2004作为EP端设备,RC侧与FT2004侧内核层都申请了连续的一致性的物理地址内存,使用DMA进行数据搬移,无论任何方向,有时候发现,仍然有数据不对的情况,就好像缓存不一致的情况没有避免,这种情况有一种可能是,FT2004的DMA需要进行配置,在发送PCI协议组TLP包时的配置,具体如何配置,这个需要你去看芯片手册,我只给一个方向。
  5. 还有一个BUG,在使用FT2004处理器的时候,因为固件时官方给的,官方的UEFI固件没有实现EP功能,硬件信息不能传给内核,所以我们使用的是带有UBOOT的固件,根据官方手册,配置好相应PCI控制器的设备树之后,正常使用。但是在FT2004有大内存操作的时候,作为EP设备的BAR空间里面的内容被莫名修改了,就好像相应物理地址被覆盖了,如果你的PCI EP驱动运行一段时间崩溃了,你可以检查一下BAR空间中的内容是不是正确的,如果不正确,可能就是我提到的这种情况。正如上面的截图可以看到,BAR0是被映射到0X230000_0000首地址空间的,这段地址空间在内核驱动中应该已经被申请占用了,不应该出现内存被其他进程申请覆盖的情况。通过UBOOT与内核源码,我没看到PCI BAR空间本地内存的申请与配置,也可能我没仔细看,我可以肯定的是,BAR空间的内存的申请与使用,不是上面的本地 memory 地址0X230000_0000,而是其他地址,但是没有其他办法知道是哪的地址,怎么办呢?我是通过/dev/mem文件接口,应用层变成访问全部物理内存,访问这个接口需要内核重新配置编译,然后BAR空间写一个数,从0地址开始遍历比较,就能找到BAR空间实际映射使用的物理memory地址。最后,在设备树中将这个地址区域保留下来,具体操作请去自行搜索。说到底,还是资料太少,技术支持不够,也只能这么做了。

五、总结

PCIE驱动开发,以具体SOC为基础,根据自己的业务需求,进行RC或者EP端开发,配置相应的信息,即可。

六、参考

《PCIE体系结构导读》
《PCIE规范详细解析》

  • 13
    点赞
  • 123
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Happy_Enger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值