| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * PCI Endpoint *Controller* (EPC) MSI library |
| * |
| * Copyright (C) 2025 NXP |
| * Author: Frank Li <Frank.Li@nxp.com> |
| */ |
| |
| #include <linux/align.h> |
| #include <linux/cleanup.h> |
| #include <linux/device.h> |
| #include <linux/export.h> |
| #include <linux/interrupt.h> |
| #include <linux/irqdomain.h> |
| #include <linux/module.h> |
| #include <linux/msi.h> |
| #include <linux/of_irq.h> |
| #include <linux/pci-epc.h> |
| #include <linux/pci-epf.h> |
| #include <linux/pci-ep-cfs.h> |
| #include <linux/pci-ep-msi.h> |
| #include <linux/slab.h> |
| |
| static void pci_epf_write_msi_msg(struct msi_desc *desc, struct msi_msg *msg) |
| { |
| struct pci_epc *epc; |
| struct pci_epf *epf; |
| |
| epc = pci_epc_get(dev_name(msi_desc_to_dev(desc))); |
| if (IS_ERR(epc)) |
| return; |
| |
| epf = list_first_entry_or_null(&epc->pci_epf, struct pci_epf, list); |
| |
| if (epf && epf->db_msg && desc->msi_index < epf->num_db) |
| memcpy(&epf->db_msg[desc->msi_index].msg, msg, sizeof(*msg)); |
| |
| pci_epc_put(epc); |
| } |
| |
| static int pci_epf_alloc_doorbell_embedded(struct pci_epf *epf, u16 num_db) |
| { |
| const struct pci_epc_aux_resource *doorbell = NULL; |
| struct pci_epf_doorbell_msg *msg; |
| struct pci_epc *epc = epf->epc; |
| size_t map_size = 0, off = 0; |
| dma_addr_t iova_base = 0; |
| phys_addr_t phys_base; |
| int count, ret, i; |
| u64 addr; |
| |
| count = pci_epc_get_aux_resources_count(epc, epf->func_no, |
| epf->vfunc_no); |
| if (count < 0) |
| return count; |
| if (!count) |
| return -ENODEV; |
| |
| struct pci_epc_aux_resource *res __free(kfree) = |
| kzalloc_objs(*res, count); |
| if (!res) |
| return -ENOMEM; |
| |
| ret = pci_epc_get_aux_resources(epc, epf->func_no, epf->vfunc_no, |
| res, count); |
| if (ret) |
| return ret; |
| |
| /* TODO: Support multiple DOORBELL_MMIO resources per EPC. */ |
| for (i = 0; i < count; i++) { |
| if (res[i].type != PCI_EPC_AUX_DOORBELL_MMIO) |
| continue; |
| |
| doorbell = &res[i]; |
| break; |
| } |
| if (!doorbell) |
| return -ENODEV; |
| addr = doorbell->phys_addr; |
| if (!IS_ALIGNED(addr, sizeof(u32))) |
| return -EINVAL; |
| |
| /* |
| * Reuse the pre-exposed BAR window if available. Otherwise map the MMIO |
| * doorbell resource here. Any required IOMMU mapping is handled |
| * internally, matching the MSI doorbell semantics. |
| */ |
| if (doorbell->bar == NO_BAR) { |
| phys_base = addr & PAGE_MASK; |
| off = addr - phys_base; |
| map_size = PAGE_ALIGN(off + sizeof(u32)); |
| |
| iova_base = dma_map_resource(epc->dev.parent, phys_base, |
| map_size, DMA_FROM_DEVICE, 0); |
| if (dma_mapping_error(epc->dev.parent, iova_base)) |
| return -EIO; |
| |
| addr = iova_base + off; |
| } |
| |
| msg = kzalloc_objs(*msg, num_db); |
| if (!msg) { |
| ret = -ENOMEM; |
| goto err_unmap; |
| } |
| |
| /* |
| * Embedded doorbell backends (e.g. DesignWare eDMA interrupt emulation) |
| * typically provide a single IRQ and do not offer per-doorbell |
| * distinguishable address/data pairs. The EPC aux resource therefore |
| * exposes one DOORBELL_MMIO entry (u.db_mmio.irq). |
| * |
| * Still, pci_epf_alloc_doorbell() allows requesting multiple doorbells. |
| * For such backends we replicate the same address/data for each entry |
| * and mark the IRQ as shared (IRQF_SHARED). Consumers must treat them |
| * as equivalent "kick" doorbells. |
| */ |
| for (i = 0; i < num_db; i++) |
| msg[i] = (struct pci_epf_doorbell_msg) { |
| .msg.address_lo = (u32)addr, |
| .msg.address_hi = (u32)(addr >> 32), |
| .msg.data = doorbell->u.db_mmio.data, |
| .virq = doorbell->u.db_mmio.irq, |
| .irq_flags = IRQF_SHARED, |
| .type = PCI_EPF_DOORBELL_EMBEDDED, |
| .bar = doorbell->bar, |
| .offset = (doorbell->bar == NO_BAR) ? 0 : |
| doorbell->bar_offset, |
| .iova_base = iova_base, |
| .iova_size = map_size, |
| }; |
| |
| epf->num_db = num_db; |
| epf->db_msg = msg; |
| return 0; |
| |
| err_unmap: |
| if (map_size) |
| dma_unmap_resource(epc->dev.parent, iova_base, map_size, |
| DMA_FROM_DEVICE, 0); |
| return ret; |
| } |
| |
| static int pci_epf_alloc_doorbell_msi(struct pci_epf *epf, u16 num_db) |
| { |
| struct pci_epf_doorbell_msg *msg; |
| struct device *dev = &epf->dev; |
| struct pci_epc *epc = epf->epc; |
| struct irq_domain *domain; |
| int ret, i; |
| |
| domain = of_msi_map_get_device_domain(epc->dev.parent, 0, |
| DOMAIN_BUS_PLATFORM_MSI); |
| if (!domain) { |
| dev_err(dev, "Can't find MSI domain for EPC\n"); |
| return -ENODEV; |
| } |
| |
| if (!irq_domain_is_msi_parent(domain)) |
| return -ENODEV; |
| |
| if (!irq_domain_is_msi_immutable(domain)) { |
| dev_err(dev, "Mutable MSI controller not supported\n"); |
| return -ENODEV; |
| } |
| |
| dev_set_msi_domain(epc->dev.parent, domain); |
| |
| msg = kzalloc_objs(struct pci_epf_doorbell_msg, num_db); |
| if (!msg) |
| return -ENOMEM; |
| |
| for (i = 0; i < num_db; i++) |
| msg[i] = (struct pci_epf_doorbell_msg) { |
| .type = PCI_EPF_DOORBELL_MSI, |
| .bar = NO_BAR, |
| }; |
| |
| epf->num_db = num_db; |
| epf->db_msg = msg; |
| |
| ret = platform_device_msi_init_and_alloc_irqs(epc->dev.parent, num_db, |
| pci_epf_write_msi_msg); |
| if (ret) { |
| dev_err(dev, "Failed to allocate MSI\n"); |
| kfree(msg); |
| epf->db_msg = NULL; |
| epf->num_db = 0; |
| return ret; |
| } |
| |
| for (i = 0; i < num_db; i++) |
| epf->db_msg[i].virq = msi_get_virq(epc->dev.parent, i); |
| |
| return 0; |
| } |
| |
| int pci_epf_alloc_doorbell(struct pci_epf *epf, u16 num_db) |
| { |
| struct pci_epc *epc = epf->epc; |
| struct device *dev = &epf->dev; |
| int ret; |
| |
| /* TODO: Multi-EPF support */ |
| if (list_first_entry_or_null(&epc->pci_epf, struct pci_epf, list) != epf) { |
| dev_err(dev, "Doorbell doesn't support multiple EPF\n"); |
| return -EINVAL; |
| } |
| |
| if (epf->db_msg) |
| return -EBUSY; |
| |
| ret = pci_epf_alloc_doorbell_msi(epf, num_db); |
| if (!ret) |
| return 0; |
| |
| /* |
| * Fall back to embedded doorbell only when platform MSI is unavailable |
| * for this EPC. |
| */ |
| if (ret != -ENODEV) |
| return ret; |
| |
| ret = pci_epf_alloc_doorbell_embedded(epf, num_db); |
| if (ret) { |
| dev_err(dev, "Failed to allocate doorbell: %d\n", ret); |
| return ret; |
| } |
| |
| dev_info(dev, "Using embedded (DMA) doorbell fallback\n"); |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(pci_epf_alloc_doorbell); |
| |
| void pci_epf_free_doorbell(struct pci_epf *epf) |
| { |
| struct pci_epf_doorbell_msg *msg0; |
| struct pci_epc *epc = epf->epc; |
| |
| if (!epf->db_msg) |
| return; |
| |
| msg0 = &epf->db_msg[0]; |
| if (msg0->type == PCI_EPF_DOORBELL_MSI) |
| platform_device_msi_free_irqs_all(epf->epc->dev.parent); |
| else if (msg0->type == PCI_EPF_DOORBELL_EMBEDDED && msg0->iova_size) |
| dma_unmap_resource(epc->dev.parent, msg0->iova_base, |
| msg0->iova_size, DMA_FROM_DEVICE, 0); |
| |
| kfree(epf->db_msg); |
| epf->db_msg = NULL; |
| epf->num_db = 0; |
| } |
| EXPORT_SYMBOL_GPL(pci_epf_free_doorbell); |