| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Copyright (C) 2025-2026, Intel Corporation. All rights reserved. | |
| * Intel Management Engine Interface (Intel MEI) Linux driver | |
| * for CSC platforms. | |
| */ | |
| #include <linux/cleanup.h> | |
| #include <linux/device.h> | |
| #include <linux/dma-mapping.h> | |
| #include <linux/err.h> | |
| #include <linux/errno.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/module.h> | |
| #include <linux/mutex.h> | |
| #include <linux/pci.h> | |
| #include <linux/pm_runtime.h> | |
| #include <linux/sched.h> | |
| #include <linux/types.h> | |
| #include "client.h" | |
| #include "hw-me-regs.h" | |
| #include "hw-me.h" | |
| #include "mei_dev.h" | |
| #include "mei-trace.h" | |
| #define MEI_CSC_HECI2_OFFSET 0x1000 | |
| static int mei_csc_read_fws(const struct mei_device *mdev, int where, const char *name, u32 *val) | |
| { | |
| struct mei_me_hw *hw = to_me_hw(mdev); | |
| *val = ioread32(hw->mem_addr + where + 0xC00); | |
| trace_mei_reg_read(&mdev->dev, name, where, *val); | |
| return 0; | |
| } | |
| static int mei_csc_probe(struct pci_dev *pdev, const struct pci_device_id *ent) | |
| { | |
| struct device *dev = &pdev->dev; | |
| const struct mei_cfg *cfg; | |
| char __iomem *registers; | |
| struct mei_device *mdev; | |
| struct mei_me_hw *hw; | |
| int err; | |
| cfg = mei_me_get_cfg(ent->driver_data); | |
| if (!cfg) | |
| return -ENODEV; | |
| err = pcim_enable_device(pdev); | |
| if (err) | |
| return dev_err_probe(dev, err, "Failed to enable PCI device.\n"); | |
| pci_set_master(pdev); | |
| registers = pcim_iomap_region(pdev, 0, KBUILD_MODNAME); | |
| if (IS_ERR(registers)) | |
| return dev_err_probe(dev, PTR_ERR(registers), "Failed to get PCI region.\n"); | |
| err = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); | |
| if (err) | |
| return dev_err_probe(dev, err, "No usable DMA configuration.\n"); | |
| /* allocates and initializes the mei dev structure */ | |
| mdev = mei_me_dev_init(dev, cfg, false); | |
| if (!mdev) | |
| return -ENOMEM; | |
| mdev->read_fws_need_resume = true; | |
| hw = to_me_hw(mdev); | |
| /* | |
| * Both HECI1 and HECI2 are on this device, but only HECI2 is supported. | |
| */ | |
| hw->mem_addr = registers + MEI_CSC_HECI2_OFFSET; | |
| hw->read_fws = mei_csc_read_fws; | |
| /* | |
| * mei_register() assumes ownership of mdev. | |
| * No need to release it explicitly in error path. | |
| */ | |
| err = mei_register(mdev, dev); | |
| if (err) | |
| return err; | |
| pci_set_drvdata(pdev, mdev); | |
| err = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX | PCI_IRQ_MSI); | |
| if (err < 0) { | |
| dev_err_probe(dev, err, "Failed to allocate IRQ.\n"); | |
| goto err_mei_unreg; | |
| } | |
| hw->irq = pci_irq_vector(pdev, 0); | |
| /* request and enable interrupt */ | |
| err = request_threaded_irq(hw->irq, | |
| mei_me_irq_quick_handler, mei_me_irq_thread_handler, | |
| IRQF_SHARED | IRQF_ONESHOT, KBUILD_MODNAME, mdev); | |
| if (err) | |
| goto err_free_irq_vectors; | |
| /* | |
| * Continue to char device setup in spite of firmware handshake failure. | |
| * In order to provide access to the firmware status registers to the user | |
| * space via sysfs. The firmware status registers required to understand | |
| * firmware error state and possible recovery flow. | |
| */ | |
| if (mei_start(mdev)) | |
| dev_warn(dev, "Failed to initialize HECI hardware.\n"); | |
| pm_runtime_set_autosuspend_delay(dev, MEI_ME_RPM_TIMEOUT); | |
| pm_runtime_use_autosuspend(dev); | |
| /* | |
| * MEI requires to resume from runtime suspend mode | |
| * in order to perform link reset flow upon system suspend. | |
| */ | |
| dev_pm_set_driver_flags(dev, DPM_FLAG_NO_DIRECT_COMPLETE); | |
| pm_runtime_allow(dev); | |
| pm_runtime_put_noidle(dev); | |
| return 0; | |
| err_free_irq_vectors: | |
| pci_free_irq_vectors(pdev); | |
| err_mei_unreg: | |
| mei_deregister(mdev); | |
| return err; | |
| } | |
| static void mei_csc_shutdown(struct pci_dev *pdev) | |
| { | |
| struct mei_device *mdev = pci_get_drvdata(pdev); | |
| struct mei_me_hw *hw = to_me_hw(mdev); | |
| pm_runtime_get_noresume(&pdev->dev); | |
| mei_stop(mdev); | |
| mei_disable_interrupts(mdev); | |
| free_irq(hw->irq, mdev); | |
| pci_free_irq_vectors(pdev); | |
| } | |
| static void mei_csc_remove(struct pci_dev *pdev) | |
| { | |
| struct mei_device *mdev = pci_get_drvdata(pdev); | |
| mei_csc_shutdown(pdev); | |
| mei_deregister(mdev); | |
| } | |
| static int mei_csc_pci_prepare(struct device *dev) | |
| { | |
| pm_runtime_resume(dev); | |
| return 0; | |
| } | |
| static int mei_csc_pci_suspend(struct device *dev) | |
| { | |
| struct mei_device *mdev = dev_get_drvdata(dev); | |
| mei_stop(mdev); | |
| mei_disable_interrupts(mdev); | |
| return 0; | |
| } | |
| static int mei_csc_pci_resume(struct device *dev) | |
| { | |
| struct mei_device *mdev = dev_get_drvdata(dev); | |
| int err; | |
| err = mei_restart(mdev); | |
| if (err) | |
| return err; | |
| /* Start timer if stopped in suspend */ | |
| schedule_delayed_work(&mdev->timer_work, HZ); | |
| return 0; | |
| } | |
| static void mei_csc_pci_complete(struct device *dev) | |
| { | |
| pm_runtime_suspend(dev); | |
| } | |
| static int mei_csc_pm_runtime_idle(struct device *dev) | |
| { | |
| struct mei_device *mdev = dev_get_drvdata(dev); | |
| return mei_write_is_idle(mdev) ? 0 : -EBUSY; | |
| } | |
| static int mei_csc_pm_runtime_suspend(struct device *dev) | |
| { | |
| struct mei_device *mdev = dev_get_drvdata(dev); | |
| struct mei_me_hw *hw = to_me_hw(mdev); | |
| guard(mutex)(&mdev->device_lock); | |
| if (!mei_write_is_idle(mdev)) | |
| return -EAGAIN; | |
| hw->pg_state = MEI_PG_ON; | |
| return 0; | |
| } | |
| static int mei_csc_pm_runtime_resume(struct device *dev) | |
| { | |
| struct mei_device *mdev = dev_get_drvdata(dev); | |
| struct mei_me_hw *hw = to_me_hw(mdev); | |
| irqreturn_t irq_ret; | |
| scoped_guard(mutex, &mdev->device_lock) | |
| hw->pg_state = MEI_PG_OFF; | |
| /* Process all queues that wait for resume */ | |
| irq_ret = mei_me_irq_thread_handler(1, mdev); | |
| if (irq_ret != IRQ_HANDLED) | |
| dev_err(dev, "thread handler fail %d\n", irq_ret); | |
| return 0; | |
| } | |
| static const struct dev_pm_ops mei_csc_pm_ops = { | |
| .prepare = pm_sleep_ptr(mei_csc_pci_prepare), | |
| .complete = pm_sleep_ptr(mei_csc_pci_complete), | |
| SYSTEM_SLEEP_PM_OPS(mei_csc_pci_suspend, mei_csc_pci_resume) | |
| RUNTIME_PM_OPS(mei_csc_pm_runtime_suspend, | |
| mei_csc_pm_runtime_resume, mei_csc_pm_runtime_idle) | |
| }; | |
| static const struct pci_device_id mei_csc_pci_tbl[] = { | |
| { PCI_DEVICE_DATA(INTEL, MEI_CRI, MEI_ME_CSC_CFG) }, | |
| { } | |
| }; | |
| MODULE_DEVICE_TABLE(pci, mei_csc_pci_tbl); | |
| static struct pci_driver mei_csc_driver = { | |
| .name = KBUILD_MODNAME, | |
| .id_table = mei_csc_pci_tbl, | |
| .probe = mei_csc_probe, | |
| .remove = mei_csc_remove, | |
| .shutdown = mei_csc_shutdown, | |
| .driver = { | |
| .pm = &mei_csc_pm_ops, | |
| .probe_type = PROBE_PREFER_ASYNCHRONOUS, | |
| } | |
| }; | |
| module_pci_driver(mei_csc_driver); | |
| MODULE_DESCRIPTION("Intel(R) Management Engine Interface for discrete graphics (CSC)"); | |
| MODULE_LICENSE("GPL"); |