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体系结构导读》,内容详细全面,一本书读透理论知识就齐全了。
我们常说的X1,X2,X4是指PCIE连接的通道数(Lane),最多32条通道,一条通道叫做1lane,包括TX和RX两根线,如下图:
PCIE是与PCI不同,PCIE使用端到端连接方式,在PCIE两端只能连接一个设备,这两个设备互为数据发送端和数据接收端,多个PCIE设备扩展需要 SWITCH,就像USB一样,一个口只能插一个USB设备,接口不够用,则需要USB HUB进行扩展,物理链路如下图:
物理连接线包括:可查找一个PCIE卡原理图进行参考,下图是网上下载的PCIE RC侧X1原理图,可供参考:
这一部分内容很多,涉及PCIE总线事务层、数据链路层、物理层等,而这一部分对于处理器使用者来说,是透明的,我们在使用处理器EP模式的时候,是不需要关系TLP是如何组包路由的,只需要建立相应的映射即可,这方面内容有兴趣的读者可以去阅读推荐书籍去理解内在机制,本节就根据自己的理解,抛去复杂的定义以及TLP路由等内容,通俗地解释一下。
参考上图进行理解,也可以先不用去管CPU域、DRAM域,通俗的讲,存储器域就是我们的处理器所能访问到的所有物理地址范围,比如一个32位处理器,可以访问0x0000_0000 - 0xFFFF_FFFF的物理地址空间;存储器域也不是字面意思的理解的内存地址空间范围,而是包括CPU域、主存以及外部设备域(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命令包不同,这一部分一般不需要驱动开发者去关注,毕竟芯片集成功能很全面简洁了。
LS1046A是一款高性能的64位ARM四核处理器。LS1046A处理器将四个64位ARM Cortex-A72内核与数据包处理加速和高速外设相集成。CoreMarks®测试高达45000分,可与10Gb以太网、第三代PCIe、SATA 3.0、USB 3.0和QSPI接口配对,是一系列企业和服务提供商联网、存储、安全和工业应用的完美产品组合。
主机开发系统:UBUNTU16.04
LS1046A Linux内核版本: Linux5.8 (使用高版本是因为其中 LS1046A 的EP驱动已经存在了)
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";
};
// 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存在于内核中,还没有使用;
这也是一个驱动模块,不过是 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测试的时候,按照如下步骤, 这不是本文重点,略提及,以后再补充:
# 控制器操作函数结构体,这一部分对于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;
}
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;
}
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;
在上面提到,主机侧设备驱动为 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相当于对共享内存进行读写操作,只不过是一块或多块跨系统的共享内存,所以要进行相应的互斥操作,具体怎么做,请查看其他资料,我是用两个中断进行乒乓互斥。当前环境下,无论是硬件寄存器还是内核驱动开发接口,相对来说已经很方便了。
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 物理内存的地址分布,后面我想再整理写一下。
《PCIE体系结构导读》
《PCIE规范详细解析》