| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright(c) 2023 Advanced Micro Devices, Inc */ |
| |
| #include <linux/errno.h> |
| #include <linux/pci.h> |
| #include <linux/utsname.h> |
| |
| #include "core.h" |
| |
| int pdsc_err_to_errno(enum pds_core_status_code code) |
| { |
| switch (code) { |
| case PDS_RC_SUCCESS: |
| return 0; |
| case PDS_RC_EVERSION: |
| case PDS_RC_EQTYPE: |
| case PDS_RC_EQID: |
| case PDS_RC_EINVAL: |
| case PDS_RC_ENOSUPP: |
| return -EINVAL; |
| case PDS_RC_EPERM: |
| return -EPERM; |
| case PDS_RC_ENOENT: |
| return -ENOENT; |
| case PDS_RC_EAGAIN: |
| return -EAGAIN; |
| case PDS_RC_ENOMEM: |
| return -ENOMEM; |
| case PDS_RC_EFAULT: |
| return -EFAULT; |
| case PDS_RC_EBUSY: |
| return -EBUSY; |
| case PDS_RC_EEXIST: |
| return -EEXIST; |
| case PDS_RC_EVFID: |
| return -ENODEV; |
| case PDS_RC_ECLIENT: |
| return -ECHILD; |
| case PDS_RC_ENOSPC: |
| return -ENOSPC; |
| case PDS_RC_ERANGE: |
| return -ERANGE; |
| case PDS_RC_BAD_ADDR: |
| return -EFAULT; |
| case PDS_RC_BAD_PCI: |
| return -ENXIO; |
| case PDS_RC_EOPCODE: |
| case PDS_RC_EINTR: |
| case PDS_RC_DEV_CMD: |
| case PDS_RC_ERROR: |
| case PDS_RC_ERDMA: |
| case PDS_RC_EIO: |
| default: |
| return -EIO; |
| } |
| } |
| |
| bool pdsc_is_fw_running(struct pdsc *pdsc) |
| { |
| if (!pdsc->info_regs) |
| return false; |
| |
| pdsc->fw_status = ioread8(&pdsc->info_regs->fw_status); |
| pdsc->last_fw_time = jiffies; |
| pdsc->last_hb = ioread32(&pdsc->info_regs->fw_heartbeat); |
| |
| /* Firmware is useful only if the running bit is set and |
| * fw_status != 0xff (bad PCI read) |
| */ |
| return (pdsc->fw_status != PDS_RC_BAD_PCI) && |
| (pdsc->fw_status & PDS_CORE_FW_STS_F_RUNNING); |
| } |
| |
| bool pdsc_is_fw_good(struct pdsc *pdsc) |
| { |
| bool fw_running = pdsc_is_fw_running(pdsc); |
| u8 gen; |
| |
| /* Make sure to update the cached fw_status by calling |
| * pdsc_is_fw_running() before getting the generation |
| */ |
| gen = pdsc->fw_status & PDS_CORE_FW_STS_F_GENERATION; |
| |
| return fw_running && gen == pdsc->fw_generation; |
| } |
| |
| static u8 pdsc_devcmd_status(struct pdsc *pdsc) |
| { |
| return ioread8(&pdsc->cmd_regs->comp.status); |
| } |
| |
| static bool pdsc_devcmd_done(struct pdsc *pdsc) |
| { |
| return ioread32(&pdsc->cmd_regs->done) & PDS_CORE_DEV_CMD_DONE; |
| } |
| |
| static void pdsc_devcmd_dbell(struct pdsc *pdsc) |
| { |
| iowrite32(0, &pdsc->cmd_regs->done); |
| iowrite32(1, &pdsc->cmd_regs->doorbell); |
| } |
| |
| static void pdsc_devcmd_clean(struct pdsc *pdsc) |
| { |
| iowrite32(0, &pdsc->cmd_regs->doorbell); |
| memset_io(&pdsc->cmd_regs->cmd, 0, sizeof(pdsc->cmd_regs->cmd)); |
| } |
| |
| static const char *pdsc_devcmd_str(int opcode) |
| { |
| switch (opcode) { |
| case PDS_CORE_CMD_NOP: |
| return "PDS_CORE_CMD_NOP"; |
| case PDS_CORE_CMD_IDENTIFY: |
| return "PDS_CORE_CMD_IDENTIFY"; |
| case PDS_CORE_CMD_RESET: |
| return "PDS_CORE_CMD_RESET"; |
| case PDS_CORE_CMD_INIT: |
| return "PDS_CORE_CMD_INIT"; |
| case PDS_CORE_CMD_FW_DOWNLOAD: |
| return "PDS_CORE_CMD_FW_DOWNLOAD"; |
| case PDS_CORE_CMD_FW_CONTROL: |
| return "PDS_CORE_CMD_FW_CONTROL"; |
| default: |
| return "PDS_CORE_CMD_UNKNOWN"; |
| } |
| } |
| |
| static int pdsc_devcmd_wait(struct pdsc *pdsc, u8 opcode, int max_seconds) |
| { |
| struct device *dev = pdsc->dev; |
| unsigned long start_time; |
| unsigned long max_wait; |
| unsigned long duration; |
| int timeout = 0; |
| bool running; |
| int done = 0; |
| int err = 0; |
| int status; |
| |
| start_time = jiffies; |
| max_wait = start_time + (max_seconds * HZ); |
| |
| while (!done && !timeout) { |
| running = pdsc_is_fw_running(pdsc); |
| if (!running) |
| break; |
| |
| done = pdsc_devcmd_done(pdsc); |
| if (done) |
| break; |
| |
| timeout = time_after(jiffies, max_wait); |
| if (timeout) |
| break; |
| |
| usleep_range(100, 200); |
| } |
| duration = jiffies - start_time; |
| |
| if (done && duration > HZ) |
| dev_dbg(dev, "DEVCMD %d %s after %ld secs\n", |
| opcode, pdsc_devcmd_str(opcode), duration / HZ); |
| |
| if ((!done || timeout) && running) { |
| dev_err(dev, "DEVCMD %d %s timeout, done %d timeout %d max_seconds=%d\n", |
| opcode, pdsc_devcmd_str(opcode), done, timeout, |
| max_seconds); |
| err = -ETIMEDOUT; |
| pdsc_devcmd_clean(pdsc); |
| } |
| |
| status = pdsc_devcmd_status(pdsc); |
| err = pdsc_err_to_errno(status); |
| if (err && err != -EAGAIN) |
| dev_err(dev, "DEVCMD %d %s failed, status=%d err %d %pe\n", |
| opcode, pdsc_devcmd_str(opcode), status, err, |
| ERR_PTR(err)); |
| |
| return err; |
| } |
| |
| int pdsc_devcmd_locked(struct pdsc *pdsc, union pds_core_dev_cmd *cmd, |
| union pds_core_dev_comp *comp, int max_seconds) |
| { |
| int err; |
| |
| if (!pdsc->cmd_regs) |
| return -ENXIO; |
| |
| memcpy_toio(&pdsc->cmd_regs->cmd, cmd, sizeof(*cmd)); |
| pdsc_devcmd_dbell(pdsc); |
| err = pdsc_devcmd_wait(pdsc, cmd->opcode, max_seconds); |
| |
| if ((err == -ENXIO || err == -ETIMEDOUT) && pdsc->wq) |
| queue_work(pdsc->wq, &pdsc->health_work); |
| else |
| memcpy_fromio(comp, &pdsc->cmd_regs->comp, sizeof(*comp)); |
| |
| return err; |
| } |
| |
| int pdsc_devcmd(struct pdsc *pdsc, union pds_core_dev_cmd *cmd, |
| union pds_core_dev_comp *comp, int max_seconds) |
| { |
| int err; |
| |
| mutex_lock(&pdsc->devcmd_lock); |
| err = pdsc_devcmd_locked(pdsc, cmd, comp, max_seconds); |
| mutex_unlock(&pdsc->devcmd_lock); |
| |
| return err; |
| } |
| |
| int pdsc_devcmd_init(struct pdsc *pdsc) |
| { |
| union pds_core_dev_comp comp = {}; |
| union pds_core_dev_cmd cmd = { |
| .opcode = PDS_CORE_CMD_INIT, |
| }; |
| |
| return pdsc_devcmd(pdsc, &cmd, &comp, pdsc->devcmd_timeout); |
| } |
| |
| int pdsc_devcmd_reset(struct pdsc *pdsc) |
| { |
| union pds_core_dev_comp comp = {}; |
| union pds_core_dev_cmd cmd = { |
| .reset.opcode = PDS_CORE_CMD_RESET, |
| }; |
| |
| if (!pdsc_is_fw_running(pdsc)) |
| return 0; |
| |
| return pdsc_devcmd(pdsc, &cmd, &comp, pdsc->devcmd_timeout); |
| } |
| |
| static int pdsc_devcmd_identify_locked(struct pdsc *pdsc) |
| { |
| union pds_core_dev_comp comp = {}; |
| union pds_core_dev_cmd cmd = { |
| .identify.opcode = PDS_CORE_CMD_IDENTIFY, |
| .identify.ver = PDS_CORE_IDENTITY_VERSION_1, |
| }; |
| |
| return pdsc_devcmd_locked(pdsc, &cmd, &comp, pdsc->devcmd_timeout); |
| } |
| |
| static void pdsc_init_devinfo(struct pdsc *pdsc) |
| { |
| pdsc->dev_info.asic_type = ioread8(&pdsc->info_regs->asic_type); |
| pdsc->dev_info.asic_rev = ioread8(&pdsc->info_regs->asic_rev); |
| pdsc->fw_generation = PDS_CORE_FW_STS_F_GENERATION & |
| ioread8(&pdsc->info_regs->fw_status); |
| |
| memcpy_fromio(pdsc->dev_info.fw_version, |
| pdsc->info_regs->fw_version, |
| PDS_CORE_DEVINFO_FWVERS_BUFLEN); |
| pdsc->dev_info.fw_version[PDS_CORE_DEVINFO_FWVERS_BUFLEN] = 0; |
| |
| memcpy_fromio(pdsc->dev_info.serial_num, |
| pdsc->info_regs->serial_num, |
| PDS_CORE_DEVINFO_SERIAL_BUFLEN); |
| pdsc->dev_info.serial_num[PDS_CORE_DEVINFO_SERIAL_BUFLEN] = 0; |
| |
| dev_dbg(pdsc->dev, "fw_version %s\n", pdsc->dev_info.fw_version); |
| } |
| |
| static int pdsc_identify(struct pdsc *pdsc) |
| { |
| struct pds_core_drv_identity drv = {}; |
| size_t sz; |
| int err; |
| int n; |
| |
| drv.drv_type = cpu_to_le32(PDS_DRIVER_LINUX); |
| /* Catching the return quiets a Wformat-truncation complaint */ |
| n = snprintf(drv.driver_ver_str, sizeof(drv.driver_ver_str), |
| "%s %s", PDS_CORE_DRV_NAME, utsname()->release); |
| if (n > sizeof(drv.driver_ver_str)) |
| dev_dbg(pdsc->dev, "release name truncated, don't care\n"); |
| |
| /* Next let's get some info about the device |
| * We use the devcmd_lock at this level in order to |
| * get safe access to the cmd_regs->data before anyone |
| * else can mess it up |
| */ |
| mutex_lock(&pdsc->devcmd_lock); |
| |
| sz = min_t(size_t, sizeof(drv), sizeof(pdsc->cmd_regs->data)); |
| memcpy_toio(&pdsc->cmd_regs->data, &drv, sz); |
| |
| err = pdsc_devcmd_identify_locked(pdsc); |
| if (!err) { |
| sz = min_t(size_t, sizeof(pdsc->dev_ident), |
| sizeof(pdsc->cmd_regs->data)); |
| memcpy_fromio(&pdsc->dev_ident, &pdsc->cmd_regs->data, sz); |
| } |
| mutex_unlock(&pdsc->devcmd_lock); |
| |
| if (err) { |
| dev_err(pdsc->dev, "Cannot identify device: %pe\n", |
| ERR_PTR(err)); |
| return err; |
| } |
| |
| if (isprint(pdsc->dev_info.fw_version[0]) && |
| isascii(pdsc->dev_info.fw_version[0])) |
| dev_info(pdsc->dev, "FW: %.*s\n", |
| (int)(sizeof(pdsc->dev_info.fw_version) - 1), |
| pdsc->dev_info.fw_version); |
| else |
| dev_info(pdsc->dev, "FW: (invalid string) 0x%02x 0x%02x 0x%02x 0x%02x ...\n", |
| (u8)pdsc->dev_info.fw_version[0], |
| (u8)pdsc->dev_info.fw_version[1], |
| (u8)pdsc->dev_info.fw_version[2], |
| (u8)pdsc->dev_info.fw_version[3]); |
| |
| return 0; |
| } |
| |
| void pdsc_dev_uninit(struct pdsc *pdsc) |
| { |
| if (pdsc->intr_info) { |
| int i; |
| |
| for (i = 0; i < pdsc->nintrs; i++) |
| pdsc_intr_free(pdsc, i); |
| |
| kfree(pdsc->intr_info); |
| pdsc->intr_info = NULL; |
| pdsc->nintrs = 0; |
| } |
| |
| pci_free_irq_vectors(pdsc->pdev); |
| } |
| |
| int pdsc_dev_init(struct pdsc *pdsc) |
| { |
| unsigned int nintrs; |
| int err; |
| |
| /* Initial init and reset of device */ |
| pdsc_init_devinfo(pdsc); |
| pdsc->devcmd_timeout = PDS_CORE_DEVCMD_TIMEOUT; |
| |
| err = pdsc_devcmd_reset(pdsc); |
| if (err) |
| return err; |
| |
| err = pdsc_identify(pdsc); |
| if (err) |
| return err; |
| |
| pdsc_debugfs_add_ident(pdsc); |
| |
| /* Now we can reserve interrupts */ |
| nintrs = le32_to_cpu(pdsc->dev_ident.nintrs); |
| nintrs = min_t(unsigned int, num_online_cpus(), nintrs); |
| |
| /* Get intr_info struct array for tracking */ |
| pdsc->intr_info = kcalloc(nintrs, sizeof(*pdsc->intr_info), GFP_KERNEL); |
| if (!pdsc->intr_info) |
| return -ENOMEM; |
| |
| err = pci_alloc_irq_vectors(pdsc->pdev, nintrs, nintrs, PCI_IRQ_MSIX); |
| if (err != nintrs) { |
| dev_err(pdsc->dev, "Can't get %d intrs from OS: %pe\n", |
| nintrs, ERR_PTR(err)); |
| err = -ENOSPC; |
| goto err_out; |
| } |
| pdsc->nintrs = nintrs; |
| |
| return 0; |
| |
| err_out: |
| kfree(pdsc->intr_info); |
| pdsc->intr_info = NULL; |
| |
| return err; |
| } |