| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2020-21 Intel Corporation. |
| */ |
| |
| #include <linux/acpi.h> |
| #include <linux/bitfield.h> |
| #include <linux/module.h> |
| #include <net/rtnetlink.h> |
| |
| #include "iosm_ipc_imem.h" |
| #include "iosm_ipc_pcie.h" |
| #include "iosm_ipc_protocol.h" |
| |
| MODULE_DESCRIPTION("IOSM Driver"); |
| MODULE_LICENSE("GPL v2"); |
| |
| /* WWAN GUID */ |
| static guid_t wwan_acpi_guid = GUID_INIT(0xbad01b75, 0x22a8, 0x4f48, 0x87, 0x92, |
| 0xbd, 0xde, 0x94, 0x67, 0x74, 0x7d); |
| |
| static void ipc_pcie_resources_release(struct iosm_pcie *ipc_pcie) |
| { |
| /* Free the MSI resources. */ |
| ipc_release_irq(ipc_pcie); |
| |
| /* Free mapped doorbell scratchpad bus memory into CPU space. */ |
| iounmap(ipc_pcie->scratchpad); |
| |
| /* Free mapped IPC_REGS bus memory into CPU space. */ |
| iounmap(ipc_pcie->ipc_regs); |
| |
| /* Releases all PCI I/O and memory resources previously reserved by a |
| * successful call to pci_request_regions. Call this function only |
| * after all use of the PCI regions has ceased. |
| */ |
| pci_release_regions(ipc_pcie->pci); |
| } |
| |
| static void ipc_pcie_cleanup(struct iosm_pcie *ipc_pcie) |
| { |
| /* Free the shared memory resources. */ |
| ipc_imem_cleanup(ipc_pcie->imem); |
| |
| ipc_pcie_resources_release(ipc_pcie); |
| |
| /* Signal to the system that the PCI device is not in use. */ |
| pci_disable_device(ipc_pcie->pci); |
| } |
| |
| static void ipc_pcie_deinit(struct iosm_pcie *ipc_pcie) |
| { |
| kfree(ipc_pcie->imem); |
| kfree(ipc_pcie); |
| } |
| |
| static void ipc_pcie_remove(struct pci_dev *pci) |
| { |
| struct iosm_pcie *ipc_pcie = pci_get_drvdata(pci); |
| |
| ipc_pcie_cleanup(ipc_pcie); |
| |
| ipc_pcie_deinit(ipc_pcie); |
| } |
| |
| static int ipc_pcie_resources_request(struct iosm_pcie *ipc_pcie) |
| { |
| struct pci_dev *pci = ipc_pcie->pci; |
| u32 cap = 0; |
| u32 ret; |
| |
| /* Reserved PCI I/O and memory resources. |
| * Mark all PCI regions associated with PCI device pci as |
| * being reserved by owner IOSM_IPC. |
| */ |
| ret = pci_request_regions(pci, "IOSM_IPC"); |
| if (ret) { |
| dev_err(ipc_pcie->dev, "failed pci request regions"); |
| goto pci_request_region_fail; |
| } |
| |
| /* Reserve the doorbell IPC REGS memory resources. |
| * Remap the memory into CPU space. Arrange for the physical address |
| * (BAR) to be visible from this driver. |
| * pci_ioremap_bar() ensures that the memory is marked uncachable. |
| */ |
| ipc_pcie->ipc_regs = pci_ioremap_bar(pci, ipc_pcie->ipc_regs_bar_nr); |
| |
| if (!ipc_pcie->ipc_regs) { |
| dev_err(ipc_pcie->dev, "IPC REGS ioremap error"); |
| ret = -EBUSY; |
| goto ipc_regs_remap_fail; |
| } |
| |
| /* Reserve the MMIO scratchpad memory resources. |
| * Remap the memory into CPU space. Arrange for the physical address |
| * (BAR) to be visible from this driver. |
| * pci_ioremap_bar() ensures that the memory is marked uncachable. |
| */ |
| ipc_pcie->scratchpad = |
| pci_ioremap_bar(pci, ipc_pcie->scratchpad_bar_nr); |
| |
| if (!ipc_pcie->scratchpad) { |
| dev_err(ipc_pcie->dev, "doorbell scratchpad ioremap error"); |
| ret = -EBUSY; |
| goto scratch_remap_fail; |
| } |
| |
| /* Install the irq handler triggered by CP. */ |
| ret = ipc_acquire_irq(ipc_pcie); |
| if (ret) { |
| dev_err(ipc_pcie->dev, "acquiring MSI irq failed!"); |
| goto irq_acquire_fail; |
| } |
| |
| /* Enable bus-mastering for the IOSM IPC device. */ |
| pci_set_master(pci); |
| |
| /* Enable LTR if possible |
| * This is needed for L1.2! |
| */ |
| pcie_capability_read_dword(ipc_pcie->pci, PCI_EXP_DEVCAP2, &cap); |
| if (cap & PCI_EXP_DEVCAP2_LTR) |
| pcie_capability_set_word(ipc_pcie->pci, PCI_EXP_DEVCTL2, |
| PCI_EXP_DEVCTL2_LTR_EN); |
| |
| dev_dbg(ipc_pcie->dev, "link between AP and CP is fully on"); |
| |
| return ret; |
| |
| irq_acquire_fail: |
| iounmap(ipc_pcie->scratchpad); |
| scratch_remap_fail: |
| iounmap(ipc_pcie->ipc_regs); |
| ipc_regs_remap_fail: |
| pci_release_regions(pci); |
| pci_request_region_fail: |
| return ret; |
| } |
| |
| bool ipc_pcie_check_aspm_enabled(struct iosm_pcie *ipc_pcie, |
| bool parent) |
| { |
| struct pci_dev *pdev; |
| u16 value = 0; |
| u32 enabled; |
| |
| if (parent) |
| pdev = ipc_pcie->pci->bus->self; |
| else |
| pdev = ipc_pcie->pci; |
| |
| pcie_capability_read_word(pdev, PCI_EXP_LNKCTL, &value); |
| enabled = value & PCI_EXP_LNKCTL_ASPMC; |
| dev_dbg(ipc_pcie->dev, "ASPM L1: 0x%04X 0x%03X", pdev->device, value); |
| |
| return (enabled == PCI_EXP_LNKCTL_ASPM_L1 || |
| enabled == PCI_EXP_LNKCTL_ASPMC); |
| } |
| |
| bool ipc_pcie_check_data_link_active(struct iosm_pcie *ipc_pcie) |
| { |
| struct pci_dev *parent; |
| u16 link_status = 0; |
| |
| if (!ipc_pcie->pci->bus || !ipc_pcie->pci->bus->self) { |
| dev_err(ipc_pcie->dev, "root port not found"); |
| return false; |
| } |
| |
| parent = ipc_pcie->pci->bus->self; |
| |
| pcie_capability_read_word(parent, PCI_EXP_LNKSTA, &link_status); |
| dev_dbg(ipc_pcie->dev, "Link status: 0x%04X", link_status); |
| |
| return link_status & PCI_EXP_LNKSTA_DLLLA; |
| } |
| |
| static bool ipc_pcie_check_aspm_supported(struct iosm_pcie *ipc_pcie, |
| bool parent) |
| { |
| struct pci_dev *pdev; |
| u32 support; |
| u32 cap = 0; |
| |
| if (parent) |
| pdev = ipc_pcie->pci->bus->self; |
| else |
| pdev = ipc_pcie->pci; |
| pcie_capability_read_dword(pdev, PCI_EXP_LNKCAP, &cap); |
| support = u32_get_bits(cap, PCI_EXP_LNKCAP_ASPMS); |
| if (support < PCI_EXP_LNKCTL_ASPM_L1) { |
| dev_dbg(ipc_pcie->dev, "ASPM L1 not supported: 0x%04X", |
| pdev->device); |
| return false; |
| } |
| return true; |
| } |
| |
| void ipc_pcie_config_aspm(struct iosm_pcie *ipc_pcie) |
| { |
| bool parent_aspm_enabled, dev_aspm_enabled; |
| |
| /* check if both root port and child supports ASPM L1 */ |
| if (!ipc_pcie_check_aspm_supported(ipc_pcie, true) || |
| !ipc_pcie_check_aspm_supported(ipc_pcie, false)) |
| return; |
| |
| parent_aspm_enabled = ipc_pcie_check_aspm_enabled(ipc_pcie, true); |
| dev_aspm_enabled = ipc_pcie_check_aspm_enabled(ipc_pcie, false); |
| |
| dev_dbg(ipc_pcie->dev, "ASPM parent: %s device: %s", |
| parent_aspm_enabled ? "Enabled" : "Disabled", |
| dev_aspm_enabled ? "Enabled" : "Disabled"); |
| } |
| |
| /* Initializes PCIe endpoint configuration */ |
| static void ipc_pcie_config_init(struct iosm_pcie *ipc_pcie) |
| { |
| /* BAR0 is used for doorbell */ |
| ipc_pcie->ipc_regs_bar_nr = IPC_DOORBELL_BAR0; |
| |
| /* update HW configuration */ |
| ipc_pcie->scratchpad_bar_nr = IPC_SCRATCHPAD_BAR2; |
| ipc_pcie->doorbell_reg_offset = IPC_DOORBELL_CH_OFFSET; |
| ipc_pcie->doorbell_write = IPC_WRITE_PTR_REG_0; |
| ipc_pcie->doorbell_capture = IPC_CAPTURE_PTR_REG_0; |
| } |
| |
| /* This will read the BIOS WWAN RTD3 settings: |
| * D0L1.2/D3L2/Disabled |
| */ |
| static enum ipc_pcie_sleep_state ipc_pcie_read_bios_cfg(struct device *dev) |
| { |
| enum ipc_pcie_sleep_state sleep_state = IPC_PCIE_D0L12; |
| union acpi_object *object; |
| acpi_handle handle_acpi; |
| |
| handle_acpi = ACPI_HANDLE(dev); |
| if (!handle_acpi) { |
| pr_debug("pci device is NOT ACPI supporting device\n"); |
| goto default_ret; |
| } |
| |
| object = acpi_evaluate_dsm(handle_acpi, &wwan_acpi_guid, 0, 3, NULL); |
| if (!object) |
| goto default_ret; |
| |
| if (object->integer.value == 3) |
| sleep_state = IPC_PCIE_D3L2; |
| |
| ACPI_FREE(object); |
| |
| default_ret: |
| return sleep_state; |
| } |
| |
| static int ipc_pcie_probe(struct pci_dev *pci, |
| const struct pci_device_id *pci_id) |
| { |
| struct iosm_pcie *ipc_pcie = kzalloc(sizeof(*ipc_pcie), GFP_KERNEL); |
| int ret; |
| |
| pr_debug("Probing device 0x%X from the vendor 0x%X", pci_id->device, |
| pci_id->vendor); |
| |
| if (!ipc_pcie) |
| goto ret_fail; |
| |
| /* Initialize ipc dbg component for the PCIe device */ |
| ipc_pcie->dev = &pci->dev; |
| |
| /* Set the driver specific data. */ |
| pci_set_drvdata(pci, ipc_pcie); |
| |
| /* Save the address of the PCI device configuration. */ |
| ipc_pcie->pci = pci; |
| |
| /* Update platform configuration */ |
| ipc_pcie_config_init(ipc_pcie); |
| |
| /* Initialize the device before it is used. Ask low-level code |
| * to enable I/O and memory. Wake up the device if it was suspended. |
| */ |
| if (pci_enable_device(pci)) { |
| dev_err(ipc_pcie->dev, "failed to enable the AP PCIe device"); |
| /* If enable of PCIe device has failed then calling |
| * ipc_pcie_cleanup will panic the system. More over |
| * ipc_pcie_cleanup() is required to be called after |
| * ipc_imem_mount() |
| */ |
| goto pci_enable_fail; |
| } |
| |
| ret = dma_set_mask(ipc_pcie->dev, DMA_BIT_MASK(64)); |
| if (ret) { |
| dev_err(ipc_pcie->dev, "Could not set PCI DMA mask: %d", ret); |
| goto set_mask_fail; |
| } |
| |
| ipc_pcie_config_aspm(ipc_pcie); |
| dev_dbg(ipc_pcie->dev, "PCIe device enabled."); |
| |
| /* Read WWAN RTD3 BIOS Setting |
| */ |
| ipc_pcie->d3l2_support = ipc_pcie_read_bios_cfg(&pci->dev); |
| |
| ipc_pcie->suspend = 0; |
| |
| if (ipc_pcie_resources_request(ipc_pcie)) |
| goto resources_req_fail; |
| |
| /* Establish the link to the imem layer. */ |
| ipc_pcie->imem = ipc_imem_init(ipc_pcie, pci->device, |
| ipc_pcie->scratchpad, ipc_pcie->dev); |
| if (!ipc_pcie->imem) { |
| dev_err(ipc_pcie->dev, "failed to init imem"); |
| goto imem_init_fail; |
| } |
| |
| return 0; |
| |
| imem_init_fail: |
| ipc_pcie_resources_release(ipc_pcie); |
| resources_req_fail: |
| set_mask_fail: |
| pci_disable_device(pci); |
| pci_enable_fail: |
| kfree(ipc_pcie); |
| ret_fail: |
| return -EIO; |
| } |
| |
| static const struct pci_device_id iosm_ipc_ids[] = { |
| { PCI_DEVICE(PCI_VENDOR_ID_INTEL, INTEL_CP_DEVICE_7560_ID) }, |
| { PCI_DEVICE(PCI_VENDOR_ID_INTEL, INTEL_CP_DEVICE_7360_ID) }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(pci, iosm_ipc_ids); |
| |
| /* Enter sleep in s2idle case |
| */ |
| static int __maybe_unused ipc_pcie_suspend_s2idle(struct iosm_pcie *ipc_pcie) |
| { |
| ipc_cp_irq_sleep_control(ipc_pcie, IPC_MEM_DEV_PM_FORCE_SLEEP); |
| |
| /* Complete all memory stores before setting bit */ |
| smp_mb__before_atomic(); |
| |
| set_bit(0, &ipc_pcie->suspend); |
| |
| /* Complete all memory stores after setting bit */ |
| smp_mb__after_atomic(); |
| |
| ipc_imem_pm_s2idle_sleep(ipc_pcie->imem, true); |
| |
| return 0; |
| } |
| |
| /* Resume from sleep in s2idle case |
| */ |
| static int __maybe_unused ipc_pcie_resume_s2idle(struct iosm_pcie *ipc_pcie) |
| { |
| ipc_cp_irq_sleep_control(ipc_pcie, IPC_MEM_DEV_PM_FORCE_ACTIVE); |
| |
| ipc_imem_pm_s2idle_sleep(ipc_pcie->imem, false); |
| |
| /* Complete all memory stores before clearing bit. */ |
| smp_mb__before_atomic(); |
| |
| clear_bit(0, &ipc_pcie->suspend); |
| |
| /* Complete all memory stores after clearing bit. */ |
| smp_mb__after_atomic(); |
| return 0; |
| } |
| |
| int __maybe_unused ipc_pcie_suspend(struct iosm_pcie *ipc_pcie) |
| { |
| /* The HAL shall ask the shared memory layer whether D3 is allowed. */ |
| ipc_imem_pm_suspend(ipc_pcie->imem); |
| |
| dev_dbg(ipc_pcie->dev, "SUSPEND done"); |
| return 0; |
| } |
| |
| int __maybe_unused ipc_pcie_resume(struct iosm_pcie *ipc_pcie) |
| { |
| /* The HAL shall inform the shared memory layer that the device is |
| * active. |
| */ |
| ipc_imem_pm_resume(ipc_pcie->imem); |
| |
| dev_dbg(ipc_pcie->dev, "RESUME done"); |
| return 0; |
| } |
| |
| static int __maybe_unused ipc_pcie_suspend_cb(struct device *dev) |
| { |
| struct iosm_pcie *ipc_pcie; |
| struct pci_dev *pdev; |
| |
| pdev = to_pci_dev(dev); |
| |
| ipc_pcie = pci_get_drvdata(pdev); |
| |
| switch (ipc_pcie->d3l2_support) { |
| case IPC_PCIE_D0L12: |
| ipc_pcie_suspend_s2idle(ipc_pcie); |
| break; |
| case IPC_PCIE_D3L2: |
| ipc_pcie_suspend(ipc_pcie); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int __maybe_unused ipc_pcie_resume_cb(struct device *dev) |
| { |
| struct iosm_pcie *ipc_pcie; |
| struct pci_dev *pdev; |
| |
| pdev = to_pci_dev(dev); |
| |
| ipc_pcie = pci_get_drvdata(pdev); |
| |
| switch (ipc_pcie->d3l2_support) { |
| case IPC_PCIE_D0L12: |
| ipc_pcie_resume_s2idle(ipc_pcie); |
| break; |
| case IPC_PCIE_D3L2: |
| ipc_pcie_resume(ipc_pcie); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static SIMPLE_DEV_PM_OPS(iosm_ipc_pm, ipc_pcie_suspend_cb, ipc_pcie_resume_cb); |
| |
| static struct pci_driver iosm_ipc_driver = { |
| .name = KBUILD_MODNAME, |
| .probe = ipc_pcie_probe, |
| .remove = ipc_pcie_remove, |
| .driver = { |
| .pm = &iosm_ipc_pm, |
| }, |
| .id_table = iosm_ipc_ids, |
| }; |
| module_pci_driver(iosm_ipc_driver); |
| |
| int ipc_pcie_addr_map(struct iosm_pcie *ipc_pcie, unsigned char *data, |
| size_t size, dma_addr_t *mapping, int direction) |
| { |
| if (ipc_pcie->pci) { |
| *mapping = dma_map_single(&ipc_pcie->pci->dev, data, size, |
| direction); |
| if (dma_mapping_error(&ipc_pcie->pci->dev, *mapping)) { |
| dev_err(ipc_pcie->dev, "dma mapping failed"); |
| return -EINVAL; |
| } |
| } |
| return 0; |
| } |
| |
| void ipc_pcie_addr_unmap(struct iosm_pcie *ipc_pcie, size_t size, |
| dma_addr_t mapping, int direction) |
| { |
| if (!mapping) |
| return; |
| if (ipc_pcie->pci) |
| dma_unmap_single(&ipc_pcie->pci->dev, mapping, size, direction); |
| } |
| |
| struct sk_buff *ipc_pcie_alloc_local_skb(struct iosm_pcie *ipc_pcie, |
| gfp_t flags, size_t size) |
| { |
| struct sk_buff *skb; |
| |
| if (!ipc_pcie || !size) { |
| pr_err("invalid pcie object or size"); |
| return NULL; |
| } |
| |
| skb = __netdev_alloc_skb(NULL, size, flags); |
| if (!skb) |
| return NULL; |
| |
| IPC_CB(skb)->op_type = (u8)UL_DEFAULT; |
| IPC_CB(skb)->mapping = 0; |
| |
| return skb; |
| } |
| |
| struct sk_buff *ipc_pcie_alloc_skb(struct iosm_pcie *ipc_pcie, size_t size, |
| gfp_t flags, dma_addr_t *mapping, |
| int direction, size_t headroom) |
| { |
| struct sk_buff *skb = ipc_pcie_alloc_local_skb(ipc_pcie, flags, |
| size + headroom); |
| if (!skb) |
| return NULL; |
| |
| if (headroom) |
| skb_reserve(skb, headroom); |
| |
| if (ipc_pcie_addr_map(ipc_pcie, skb->data, size, mapping, direction)) { |
| dev_kfree_skb(skb); |
| return NULL; |
| } |
| |
| BUILD_BUG_ON(sizeof(*IPC_CB(skb)) > sizeof(skb->cb)); |
| |
| /* Store the mapping address in skb scratch pad for later usage */ |
| IPC_CB(skb)->mapping = *mapping; |
| IPC_CB(skb)->direction = direction; |
| IPC_CB(skb)->len = size; |
| |
| return skb; |
| } |
| |
| void ipc_pcie_kfree_skb(struct iosm_pcie *ipc_pcie, struct sk_buff *skb) |
| { |
| if (!skb) |
| return; |
| |
| ipc_pcie_addr_unmap(ipc_pcie, IPC_CB(skb)->len, IPC_CB(skb)->mapping, |
| IPC_CB(skb)->direction); |
| IPC_CB(skb)->mapping = 0; |
| dev_kfree_skb(skb); |
| } |