root/drivers/net/ethernet/amd/pds_core/dev.c
// 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 (!running) {
                dev_err(dev, "DEVCMD %d %s fw not running\n",
                        opcode, pdsc_devcmd_str(opcode));
                pdsc_devcmd_clean(pdsc);
                return -ENXIO;
        }

        if (!done || timeout) {
                dev_err(dev, "DEVCMD %d %s timeout, done %d timeout %d max_seconds=%d\n",
                        opcode, pdsc_devcmd_str(opcode), done, timeout,
                        max_seconds);
                pdsc_devcmd_clean(pdsc);
                return -ETIMEDOUT;
        }

        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 = kzalloc_objs(*pdsc->intr_info, nintrs);
        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;
}