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


文章目录

  • 前言
  • 一、PCIE 硬件简介
  • 二、PCIE EP地址映射原理介绍
    • 1. PCI总线的各种域(存储器域、PCI总线域)
    • 2. 开发EP设备驱动要做的事
  • 三、NXP LS1046A PCIE EP端驱动
    • 1. LS1046A处理器简介
    • 2. 开发环境介绍
    • 3. 驱动源码介绍
      • 3.1. 源码概览
      • 3.2. EP测试程序 pci-epf-test.c
      • 3.3. EP端设备配置空间寄存器
      • 3.4. EP端头部信息设置
      • 3.5. EP端BAR空间设置
      • 3.6. EP端物理地址映射
      • 3.7. RC端访问BAR地址空间
      • 3.8. EP端访问主机全部物理内存设置
  • 四、飞腾新四核 FT2004 PCIE EP端驱动
    • 1. 处理器简介
    • 2. 开发环境介绍
    • 3. 驱动开发
  • 五、总结
  • 六、参考

前言 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,前提是两边都支持,就像网卡百兆、千兆一样,虽然支持,但是如果协商信号有问题,千兆网卡也会协商出百兆速率: linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片


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

linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

PCIE是与PCI不同,PCIE使用端到端连接方式,在PCIE两端只能连接一个设备,这两个设备互为数据发送端和数据接收端,多个PCIE设备扩展需要 SWITCH,就像USB一样,一个口只能插一个USB设备,接口不够用,则需要USB HUB进行扩展,物理链路如下图:
linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

物理连接线包括:
  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 卡已经插入,从而触发系统软件进行相应处理。
    linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
    文章图片
  6. 在一个处理器系统中,可能含有许多 PCle设备,这些设备可以作为 Add-In 卡与 PCle插槽连接,也可以作为内置模块,与处理器系统提供的 PCle 链路直接相连,而不需要经过 PCle 插槽。PCle设备与 PCIe插槽都具有REFCLK+和REFCLK-信号,其中 PCle插槽使用这组信号与处理器系统同步。在一个处理器系统中,通常采用专用逻辑向 PCle 插槽提供 REFCLK+和 REFCLK-信号,其中100 MHz的时钟源由晶振提供,并经过一个"一推多"的差分时钟驱动器生成多个同相位的时钟源,与 PCIe 插槽一一对应连接。

    linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
    文章图片


    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原理图,可供参考:
linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

二、PCIE EP地址映射原理介绍 这一部分内容很多,涉及PCIE总线事务层、数据链路层、物理层等,而这一部分对于处理器使用者来说,是透明的,我们在使用处理器EP模式的时候,是不需要关系TLP是如何组包路由的,只需要建立相应的映射即可,这方面内容有兴趣的读者可以去阅读推荐书籍去理解内在机制,本节就根据自己的理解,抛去复杂的定义以及TLP路由等内容,通俗地解释一下。
1. PCI总线的各种域(存储器域、PCI总线域) linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

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

那么什么是PCI总线域呢?
linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

我们可以将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命令包不同,这一部分一般不需要驱动开发者去关注,毕竟芯片集成功能很全面简洁了。

linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

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端设备配置空间寄存器
linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

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 ,我的环境没了,没有截图: linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片


在上面提到,主机侧设备驱动为 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 = https://www.it610.com/article/(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 = https://www.it610.com/article/(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个窗口,每个窗口映射一个策略 #typeMEM 映射还是 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 系统相连。 linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片


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

linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

2. 开发环境介绍 开发环境基本与 LS1046 一样。 操作系统 : ubuntu16 / ubuntu18 交叉编译器 : aarch64-linux-gnu- 3. 驱动开发 通过以上的讲解,我想你应该多多少少有点明白了PCIE的EP端以及RC端驱动开发,飞腾处理器的PCIE使用,根据 《FT-2000/4 软件编程手册》进行寄存器配置即可。 linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发
文章图片

飞腾的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端开发,配置相应的信息,即可。 六、参考 【linux-驱动|NXP LS1046A及飞腾新四核 FT2004 PCIE EP端LINUX设备驱动开发】《PCIE体系结构导读》
《PCIE规范详细解析》

    推荐阅读