| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright(c) 2023 Advanced Micro Devices, Inc */ |
| |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/pci.h> |
| |
| #include <linux/pds/pds_common.h> |
| |
| #include "core.h" |
| |
| MODULE_DESCRIPTION(PDSC_DRV_DESCRIPTION); |
| MODULE_AUTHOR("Advanced Micro Devices, Inc"); |
| MODULE_LICENSE("GPL"); |
| |
| /* Supported devices */ |
| static const struct pci_device_id pdsc_id_table[] = { |
| { PCI_VDEVICE(PENSANDO, PCI_DEVICE_ID_PENSANDO_CORE_PF) }, |
| { PCI_VDEVICE(PENSANDO, PCI_DEVICE_ID_PENSANDO_VDPA_VF) }, |
| { 0, } /* end of table */ |
| }; |
| MODULE_DEVICE_TABLE(pci, pdsc_id_table); |
| |
| static void pdsc_wdtimer_cb(struct timer_list *t) |
| { |
| struct pdsc *pdsc = from_timer(pdsc, t, wdtimer); |
| |
| dev_dbg(pdsc->dev, "%s: jiffies %ld\n", __func__, jiffies); |
| mod_timer(&pdsc->wdtimer, |
| round_jiffies(jiffies + pdsc->wdtimer_period)); |
| |
| queue_work(pdsc->wq, &pdsc->health_work); |
| } |
| |
| static void pdsc_unmap_bars(struct pdsc *pdsc) |
| { |
| struct pdsc_dev_bar *bars = pdsc->bars; |
| unsigned int i; |
| |
| pdsc->info_regs = NULL; |
| pdsc->cmd_regs = NULL; |
| pdsc->intr_status = NULL; |
| pdsc->intr_ctrl = NULL; |
| |
| for (i = 0; i < PDS_CORE_BARS_MAX; i++) { |
| if (bars[i].vaddr) |
| pci_iounmap(pdsc->pdev, bars[i].vaddr); |
| bars[i].vaddr = NULL; |
| } |
| } |
| |
| static int pdsc_map_bars(struct pdsc *pdsc) |
| { |
| struct pdsc_dev_bar *bar = pdsc->bars; |
| struct pci_dev *pdev = pdsc->pdev; |
| struct device *dev = pdsc->dev; |
| struct pdsc_dev_bar *bars; |
| unsigned int i, j; |
| int num_bars = 0; |
| int err; |
| u32 sig; |
| |
| bars = pdsc->bars; |
| |
| /* Since the PCI interface in the hardware is configurable, |
| * we need to poke into all the bars to find the set we're |
| * expecting. |
| */ |
| for (i = 0, j = 0; i < PDS_CORE_BARS_MAX; i++) { |
| if (!(pci_resource_flags(pdev, i) & IORESOURCE_MEM)) |
| continue; |
| |
| bars[j].len = pci_resource_len(pdev, i); |
| bars[j].bus_addr = pci_resource_start(pdev, i); |
| bars[j].res_index = i; |
| |
| /* only map the whole bar 0 */ |
| if (j > 0) { |
| bars[j].vaddr = NULL; |
| } else { |
| bars[j].vaddr = pci_iomap(pdev, i, bars[j].len); |
| if (!bars[j].vaddr) { |
| dev_err(dev, "Cannot map BAR %d, aborting\n", i); |
| return -ENODEV; |
| } |
| } |
| |
| j++; |
| } |
| num_bars = j; |
| |
| /* BAR0: dev_cmd and interrupts */ |
| if (num_bars < 1) { |
| dev_err(dev, "No bars found\n"); |
| err = -EFAULT; |
| goto err_out; |
| } |
| |
| if (bar->len < PDS_CORE_BAR0_SIZE) { |
| dev_err(dev, "Resource bar size %lu too small\n", bar->len); |
| err = -EFAULT; |
| goto err_out; |
| } |
| |
| pdsc->info_regs = bar->vaddr + PDS_CORE_BAR0_DEV_INFO_REGS_OFFSET; |
| pdsc->cmd_regs = bar->vaddr + PDS_CORE_BAR0_DEV_CMD_REGS_OFFSET; |
| pdsc->intr_status = bar->vaddr + PDS_CORE_BAR0_INTR_STATUS_OFFSET; |
| pdsc->intr_ctrl = bar->vaddr + PDS_CORE_BAR0_INTR_CTRL_OFFSET; |
| |
| sig = ioread32(&pdsc->info_regs->signature); |
| if (sig != PDS_CORE_DEV_INFO_SIGNATURE) { |
| dev_err(dev, "Incompatible firmware signature %x", sig); |
| err = -EFAULT; |
| goto err_out; |
| } |
| |
| /* BAR1: doorbells */ |
| bar++; |
| if (num_bars < 2) { |
| dev_err(dev, "Doorbell bar missing\n"); |
| err = -EFAULT; |
| goto err_out; |
| } |
| |
| pdsc->db_pages = bar->vaddr; |
| pdsc->phy_db_pages = bar->bus_addr; |
| |
| return 0; |
| |
| err_out: |
| pdsc_unmap_bars(pdsc); |
| return err; |
| } |
| |
| void __iomem *pdsc_map_dbpage(struct pdsc *pdsc, int page_num) |
| { |
| return pci_iomap_range(pdsc->pdev, |
| pdsc->bars[PDS_CORE_PCI_BAR_DBELL].res_index, |
| (u64)page_num << PAGE_SHIFT, PAGE_SIZE); |
| } |
| |
| static int pdsc_sriov_configure(struct pci_dev *pdev, int num_vfs) |
| { |
| struct pdsc *pdsc = pci_get_drvdata(pdev); |
| struct device *dev = pdsc->dev; |
| int ret = 0; |
| |
| if (num_vfs > 0) { |
| pdsc->vfs = kcalloc(num_vfs, sizeof(struct pdsc_vf), |
| GFP_KERNEL); |
| if (!pdsc->vfs) |
| return -ENOMEM; |
| pdsc->num_vfs = num_vfs; |
| |
| ret = pci_enable_sriov(pdev, num_vfs); |
| if (ret) { |
| dev_err(dev, "Cannot enable SRIOV: %pe\n", |
| ERR_PTR(ret)); |
| goto no_vfs; |
| } |
| |
| return num_vfs; |
| } |
| |
| no_vfs: |
| pci_disable_sriov(pdev); |
| |
| kfree(pdsc->vfs); |
| pdsc->vfs = NULL; |
| pdsc->num_vfs = 0; |
| |
| return ret; |
| } |
| |
| static int pdsc_init_vf(struct pdsc *vf) |
| { |
| struct devlink *dl; |
| struct pdsc *pf; |
| int err; |
| |
| pf = pdsc_get_pf_struct(vf->pdev); |
| if (IS_ERR_OR_NULL(pf)) |
| return PTR_ERR(pf) ?: -1; |
| |
| vf->vf_id = pci_iov_vf_id(vf->pdev); |
| |
| dl = priv_to_devlink(vf); |
| devl_lock(dl); |
| devl_register(dl); |
| devl_unlock(dl); |
| |
| pf->vfs[vf->vf_id].vf = vf; |
| err = pdsc_auxbus_dev_add(vf, pf); |
| if (err) { |
| devl_lock(dl); |
| devl_unregister(dl); |
| devl_unlock(dl); |
| } |
| |
| return err; |
| } |
| |
| static const struct devlink_health_reporter_ops pdsc_fw_reporter_ops = { |
| .name = "fw", |
| .diagnose = pdsc_fw_reporter_diagnose, |
| }; |
| |
| static const struct devlink_param pdsc_dl_params[] = { |
| DEVLINK_PARAM_GENERIC(ENABLE_VNET, |
| BIT(DEVLINK_PARAM_CMODE_RUNTIME), |
| pdsc_dl_enable_get, |
| pdsc_dl_enable_set, |
| pdsc_dl_enable_validate), |
| }; |
| |
| #define PDSC_WQ_NAME_LEN 24 |
| |
| static int pdsc_init_pf(struct pdsc *pdsc) |
| { |
| struct devlink_health_reporter *hr; |
| char wq_name[PDSC_WQ_NAME_LEN]; |
| struct devlink *dl; |
| int err; |
| |
| pcie_print_link_status(pdsc->pdev); |
| |
| err = pci_request_regions(pdsc->pdev, PDS_CORE_DRV_NAME); |
| if (err) { |
| dev_err(pdsc->dev, "Cannot request PCI regions: %pe\n", |
| ERR_PTR(err)); |
| return err; |
| } |
| |
| err = pdsc_map_bars(pdsc); |
| if (err) |
| goto err_out_release_regions; |
| |
| /* General workqueue and timer, but don't start timer yet */ |
| snprintf(wq_name, sizeof(wq_name), "%s.%d", PDS_CORE_DRV_NAME, pdsc->uid); |
| pdsc->wq = create_singlethread_workqueue(wq_name); |
| INIT_WORK(&pdsc->health_work, pdsc_health_thread); |
| INIT_WORK(&pdsc->pci_reset_work, pdsc_pci_reset_thread); |
| timer_setup(&pdsc->wdtimer, pdsc_wdtimer_cb, 0); |
| pdsc->wdtimer_period = PDSC_WATCHDOG_SECS * HZ; |
| |
| mutex_init(&pdsc->devcmd_lock); |
| mutex_init(&pdsc->config_lock); |
| spin_lock_init(&pdsc->adminq_lock); |
| |
| mutex_lock(&pdsc->config_lock); |
| set_bit(PDSC_S_FW_DEAD, &pdsc->state); |
| |
| err = pdsc_setup(pdsc, PDSC_SETUP_INIT); |
| if (err) { |
| mutex_unlock(&pdsc->config_lock); |
| goto err_out_unmap_bars; |
| } |
| |
| err = pdsc_start(pdsc); |
| if (err) { |
| mutex_unlock(&pdsc->config_lock); |
| goto err_out_teardown; |
| } |
| |
| mutex_unlock(&pdsc->config_lock); |
| |
| dl = priv_to_devlink(pdsc); |
| devl_lock(dl); |
| err = devl_params_register(dl, pdsc_dl_params, |
| ARRAY_SIZE(pdsc_dl_params)); |
| if (err) { |
| devl_unlock(dl); |
| dev_warn(pdsc->dev, "Failed to register devlink params: %pe\n", |
| ERR_PTR(err)); |
| goto err_out_stop; |
| } |
| |
| hr = devl_health_reporter_create(dl, &pdsc_fw_reporter_ops, 0, pdsc); |
| if (IS_ERR(hr)) { |
| devl_unlock(dl); |
| dev_warn(pdsc->dev, "Failed to create fw reporter: %pe\n", hr); |
| err = PTR_ERR(hr); |
| goto err_out_unreg_params; |
| } |
| pdsc->fw_reporter = hr; |
| |
| devl_register(dl); |
| devl_unlock(dl); |
| |
| /* Lastly, start the health check timer */ |
| mod_timer(&pdsc->wdtimer, round_jiffies(jiffies + pdsc->wdtimer_period)); |
| |
| return 0; |
| |
| err_out_unreg_params: |
| devlink_params_unregister(dl, pdsc_dl_params, |
| ARRAY_SIZE(pdsc_dl_params)); |
| err_out_stop: |
| pdsc_stop(pdsc); |
| err_out_teardown: |
| pdsc_teardown(pdsc, PDSC_TEARDOWN_REMOVING); |
| err_out_unmap_bars: |
| timer_shutdown_sync(&pdsc->wdtimer); |
| if (pdsc->wq) |
| destroy_workqueue(pdsc->wq); |
| mutex_destroy(&pdsc->config_lock); |
| mutex_destroy(&pdsc->devcmd_lock); |
| pci_free_irq_vectors(pdsc->pdev); |
| pdsc_unmap_bars(pdsc); |
| err_out_release_regions: |
| pci_release_regions(pdsc->pdev); |
| |
| return err; |
| } |
| |
| static const struct devlink_ops pdsc_dl_ops = { |
| .info_get = pdsc_dl_info_get, |
| .flash_update = pdsc_dl_flash_update, |
| }; |
| |
| static const struct devlink_ops pdsc_dl_vf_ops = { |
| }; |
| |
| static DEFINE_IDA(pdsc_ida); |
| |
| static int pdsc_probe(struct pci_dev *pdev, const struct pci_device_id *ent) |
| { |
| struct device *dev = &pdev->dev; |
| const struct devlink_ops *ops; |
| struct devlink *dl; |
| struct pdsc *pdsc; |
| bool is_pf; |
| int err; |
| |
| is_pf = !pdev->is_virtfn; |
| ops = is_pf ? &pdsc_dl_ops : &pdsc_dl_vf_ops; |
| dl = devlink_alloc(ops, sizeof(struct pdsc), dev); |
| if (!dl) |
| return -ENOMEM; |
| pdsc = devlink_priv(dl); |
| |
| pdsc->pdev = pdev; |
| pdsc->dev = &pdev->dev; |
| set_bit(PDSC_S_INITING_DRIVER, &pdsc->state); |
| pci_set_drvdata(pdev, pdsc); |
| pdsc_debugfs_add_dev(pdsc); |
| |
| err = ida_alloc(&pdsc_ida, GFP_KERNEL); |
| if (err < 0) { |
| dev_err(pdsc->dev, "%s: id alloc failed: %pe\n", |
| __func__, ERR_PTR(err)); |
| goto err_out_free_devlink; |
| } |
| pdsc->uid = err; |
| |
| /* Query system for DMA addressing limitation for the device. */ |
| err = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(PDS_CORE_ADDR_LEN)); |
| if (err) { |
| dev_err(dev, "Unable to obtain 64-bit DMA for consistent allocations, aborting: %pe\n", |
| ERR_PTR(err)); |
| goto err_out_free_ida; |
| } |
| |
| err = pci_enable_device(pdev); |
| if (err) { |
| dev_err(dev, "Cannot enable PCI device: %pe\n", ERR_PTR(err)); |
| goto err_out_free_ida; |
| } |
| pci_set_master(pdev); |
| |
| if (is_pf) |
| err = pdsc_init_pf(pdsc); |
| else |
| err = pdsc_init_vf(pdsc); |
| if (err) { |
| dev_err(dev, "Cannot init device: %pe\n", ERR_PTR(err)); |
| goto err_out_disable_device; |
| } |
| |
| clear_bit(PDSC_S_INITING_DRIVER, &pdsc->state); |
| return 0; |
| |
| err_out_disable_device: |
| pci_disable_device(pdev); |
| err_out_free_ida: |
| ida_free(&pdsc_ida, pdsc->uid); |
| err_out_free_devlink: |
| pdsc_debugfs_del_dev(pdsc); |
| devlink_free(dl); |
| |
| return err; |
| } |
| |
| static void pdsc_remove(struct pci_dev *pdev) |
| { |
| struct pdsc *pdsc = pci_get_drvdata(pdev); |
| struct devlink *dl; |
| |
| /* Unhook the registrations first to be sure there |
| * are no requests while we're stopping. |
| */ |
| dl = priv_to_devlink(pdsc); |
| devl_lock(dl); |
| devl_unregister(dl); |
| if (!pdev->is_virtfn) { |
| if (pdsc->fw_reporter) { |
| devl_health_reporter_destroy(pdsc->fw_reporter); |
| pdsc->fw_reporter = NULL; |
| } |
| devl_params_unregister(dl, pdsc_dl_params, |
| ARRAY_SIZE(pdsc_dl_params)); |
| } |
| devl_unlock(dl); |
| |
| if (pdev->is_virtfn) { |
| struct pdsc *pf; |
| |
| pf = pdsc_get_pf_struct(pdsc->pdev); |
| if (!IS_ERR(pf)) { |
| pdsc_auxbus_dev_del(pdsc, pf); |
| pf->vfs[pdsc->vf_id].vf = NULL; |
| } |
| } else { |
| /* Remove the VFs and their aux_bus connections before other |
| * cleanup so that the clients can use the AdminQ to cleanly |
| * shut themselves down. |
| */ |
| pdsc_sriov_configure(pdev, 0); |
| |
| timer_shutdown_sync(&pdsc->wdtimer); |
| if (pdsc->wq) |
| destroy_workqueue(pdsc->wq); |
| |
| mutex_lock(&pdsc->config_lock); |
| set_bit(PDSC_S_STOPPING_DRIVER, &pdsc->state); |
| |
| pdsc_stop(pdsc); |
| pdsc_teardown(pdsc, PDSC_TEARDOWN_REMOVING); |
| mutex_unlock(&pdsc->config_lock); |
| mutex_destroy(&pdsc->config_lock); |
| mutex_destroy(&pdsc->devcmd_lock); |
| |
| pdsc_unmap_bars(pdsc); |
| pci_release_regions(pdev); |
| } |
| |
| pci_disable_device(pdev); |
| |
| ida_free(&pdsc_ida, pdsc->uid); |
| pdsc_debugfs_del_dev(pdsc); |
| devlink_free(dl); |
| } |
| |
| static void pdsc_stop_health_thread(struct pdsc *pdsc) |
| { |
| if (pdsc->pdev->is_virtfn) |
| return; |
| |
| timer_shutdown_sync(&pdsc->wdtimer); |
| if (pdsc->health_work.func) |
| cancel_work_sync(&pdsc->health_work); |
| } |
| |
| static void pdsc_restart_health_thread(struct pdsc *pdsc) |
| { |
| if (pdsc->pdev->is_virtfn) |
| return; |
| |
| timer_setup(&pdsc->wdtimer, pdsc_wdtimer_cb, 0); |
| mod_timer(&pdsc->wdtimer, jiffies + 1); |
| } |
| |
| static void pdsc_reset_prepare(struct pci_dev *pdev) |
| { |
| struct pdsc *pdsc = pci_get_drvdata(pdev); |
| |
| pdsc_stop_health_thread(pdsc); |
| pdsc_fw_down(pdsc); |
| |
| if (pdev->is_virtfn) { |
| struct pdsc *pf; |
| |
| pf = pdsc_get_pf_struct(pdsc->pdev); |
| if (!IS_ERR(pf)) |
| pdsc_auxbus_dev_del(pdsc, pf); |
| } |
| |
| pdsc_unmap_bars(pdsc); |
| pci_release_regions(pdev); |
| if (pci_is_enabled(pdev)) |
| pci_disable_device(pdev); |
| } |
| |
| static void pdsc_reset_done(struct pci_dev *pdev) |
| { |
| struct pdsc *pdsc = pci_get_drvdata(pdev); |
| struct device *dev = pdsc->dev; |
| int err; |
| |
| err = pci_enable_device(pdev); |
| if (err) { |
| dev_err(dev, "Cannot enable PCI device: %pe\n", ERR_PTR(err)); |
| return; |
| } |
| pci_set_master(pdev); |
| |
| if (!pdev->is_virtfn) { |
| pcie_print_link_status(pdsc->pdev); |
| |
| err = pci_request_regions(pdsc->pdev, PDS_CORE_DRV_NAME); |
| if (err) { |
| dev_err(pdsc->dev, "Cannot request PCI regions: %pe\n", |
| ERR_PTR(err)); |
| return; |
| } |
| |
| err = pdsc_map_bars(pdsc); |
| if (err) |
| return; |
| } |
| |
| pdsc_fw_up(pdsc); |
| pdsc_restart_health_thread(pdsc); |
| |
| if (pdev->is_virtfn) { |
| struct pdsc *pf; |
| |
| pf = pdsc_get_pf_struct(pdsc->pdev); |
| if (!IS_ERR(pf)) |
| pdsc_auxbus_dev_add(pdsc, pf); |
| } |
| } |
| |
| static pci_ers_result_t pdsc_pci_error_detected(struct pci_dev *pdev, |
| pci_channel_state_t error) |
| { |
| if (error == pci_channel_io_frozen) { |
| pdsc_reset_prepare(pdev); |
| return PCI_ERS_RESULT_NEED_RESET; |
| } |
| |
| return PCI_ERS_RESULT_NONE; |
| } |
| |
| static void pdsc_pci_error_resume(struct pci_dev *pdev) |
| { |
| struct pdsc *pdsc = pci_get_drvdata(pdev); |
| |
| if (test_bit(PDSC_S_FW_DEAD, &pdsc->state)) |
| pci_reset_function_locked(pdev); |
| } |
| |
| static const struct pci_error_handlers pdsc_err_handler = { |
| /* FLR handling */ |
| .reset_prepare = pdsc_reset_prepare, |
| .reset_done = pdsc_reset_done, |
| |
| /* AER handling */ |
| .error_detected = pdsc_pci_error_detected, |
| .resume = pdsc_pci_error_resume, |
| }; |
| |
| static struct pci_driver pdsc_driver = { |
| .name = PDS_CORE_DRV_NAME, |
| .id_table = pdsc_id_table, |
| .probe = pdsc_probe, |
| .remove = pdsc_remove, |
| .sriov_configure = pdsc_sriov_configure, |
| .err_handler = &pdsc_err_handler, |
| }; |
| |
| void *pdsc_get_pf_struct(struct pci_dev *vf_pdev) |
| { |
| return pci_iov_get_pf_drvdata(vf_pdev, &pdsc_driver); |
| } |
| EXPORT_SYMBOL_GPL(pdsc_get_pf_struct); |
| |
| static int __init pdsc_init_module(void) |
| { |
| if (strcmp(KBUILD_MODNAME, PDS_CORE_DRV_NAME)) |
| return -EINVAL; |
| |
| pdsc_debugfs_create(); |
| return pci_register_driver(&pdsc_driver); |
| } |
| |
| static void __exit pdsc_cleanup_module(void) |
| { |
| pci_unregister_driver(&pdsc_driver); |
| pdsc_debugfs_destroy(); |
| } |
| |
| module_init(pdsc_init_module); |
| module_exit(pdsc_cleanup_module); |