root/drivers/net/phy/as21xxx.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Aeonsemi AS21XXxX PHY Driver
 *
 * Author: Christian Marangi <ansuelsmth@gmail.com>
 */

#include <linux/bitfield.h>
#include <linux/firmware.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/phy.h>

#define VEND1_GLB_REG_CPU_RESET_ADDR_LO_BASEADDR 0x3
#define VEND1_GLB_REG_CPU_RESET_ADDR_HI_BASEADDR 0x4

#define VEND1_GLB_REG_CPU_CTRL          0xe
#define   VEND1_GLB_CPU_CTRL_MASK       GENMASK(4, 0)
#define   VEND1_GLB_CPU_CTRL_LED_POLARITY_MASK GENMASK(12, 8)
#define   VEND1_GLB_CPU_CTRL_LED_POLARITY(_n) FIELD_PREP(VEND1_GLB_CPU_CTRL_LED_POLARITY_MASK, \
                                                         BIT(_n))

#define VEND1_FW_START_ADDR             0x100

#define VEND1_GLB_REG_MDIO_INDIRECT_ADDRCMD 0x101
#define VEND1_GLB_REG_MDIO_INDIRECT_LOAD 0x102

#define VEND1_GLB_REG_MDIO_INDIRECT_STATUS 0x103

#define VEND1_PTP_CLK                   0x142
#define   VEND1_PTP_CLK_EN              BIT(6)

/* 5 LED at step of 0x20
 * FE: Fast-Ethernet (10/100)
 * GE: Gigabit-Ethernet (1000)
 * NG: New-Generation (2500/5000/10000)
 */
#define VEND1_LED_REG(_n)               (0x1800 + ((_n) * 0x10))
#define   VEND1_LED_REG_A_EVENT         GENMASK(15, 11)
#define VEND1_LED_CONF                  0x1881
#define   VEND1_LED_CONFG_BLINK         GENMASK(7, 0)

#define VEND1_SPEED_STATUS              0x4002
#define   VEND1_SPEED_MASK              GENMASK(7, 0)
#define   VEND1_SPEED_10000             FIELD_PREP_CONST(VEND1_SPEED_MASK, 0x3)
#define   VEND1_SPEED_5000              FIELD_PREP_CONST(VEND1_SPEED_MASK, 0x5)
#define   VEND1_SPEED_2500              FIELD_PREP_CONST(VEND1_SPEED_MASK, 0x9)
#define   VEND1_SPEED_1000              FIELD_PREP_CONST(VEND1_SPEED_MASK, 0x10)
#define   VEND1_SPEED_100               FIELD_PREP_CONST(VEND1_SPEED_MASK, 0x20)
#define   VEND1_SPEED_10                FIELD_PREP_CONST(VEND1_SPEED_MASK, 0x0)

#define VEND1_IPC_CMD                   0x5801
#define   AEON_IPC_CMD_PARITY           BIT(15)
#define   AEON_IPC_CMD_SIZE             GENMASK(10, 6)
#define   AEON_IPC_CMD_OPCODE           GENMASK(5, 0)

#define IPC_CMD_NOOP                    0x0  /* Do nothing */
#define IPC_CMD_INFO                    0x1  /* Get Firmware Version */
#define IPC_CMD_SYS_CPU                 0x2  /* SYS_CPU */
#define IPC_CMD_BULK_DATA               0xa  /* Pass bulk data in ipc registers. */
#define IPC_CMD_BULK_WRITE              0xc  /* Write bulk data to memory */
#define IPC_CMD_CFG_PARAM               0x1a /* Write config parameters to memory */
#define IPC_CMD_NG_TESTMODE             0x1b /* Set NG test mode and tone */
#define IPC_CMD_TEMP_MON                0x15 /* Temperature monitoring function */
#define IPC_CMD_SET_LED                 0x23 /* Set led */

#define VEND1_IPC_STS                   0x5802
#define   AEON_IPC_STS_PARITY           BIT(15)
#define   AEON_IPC_STS_SIZE             GENMASK(14, 10)
#define   AEON_IPC_STS_OPCODE           GENMASK(9, 4)
#define   AEON_IPC_STS_STATUS           GENMASK(3, 0)
#define   AEON_IPC_STS_STATUS_RCVD      FIELD_PREP_CONST(AEON_IPC_STS_STATUS, 0x1)
#define   AEON_IPC_STS_STATUS_PROCESS   FIELD_PREP_CONST(AEON_IPC_STS_STATUS, 0x2)
#define   AEON_IPC_STS_STATUS_SUCCESS   FIELD_PREP_CONST(AEON_IPC_STS_STATUS, 0x4)
#define   AEON_IPC_STS_STATUS_ERROR     FIELD_PREP_CONST(AEON_IPC_STS_STATUS, 0x8)
#define   AEON_IPC_STS_STATUS_BUSY      FIELD_PREP_CONST(AEON_IPC_STS_STATUS, 0xe)
#define   AEON_IPC_STS_STATUS_READY     FIELD_PREP_CONST(AEON_IPC_STS_STATUS, 0xf)

#define VEND1_IPC_DATA0                 0x5808
#define VEND1_IPC_DATA1                 0x5809
#define VEND1_IPC_DATA2                 0x580a
#define VEND1_IPC_DATA3                 0x580b
#define VEND1_IPC_DATA4                 0x580c
#define VEND1_IPC_DATA5                 0x580d
#define VEND1_IPC_DATA6                 0x580e
#define VEND1_IPC_DATA7                 0x580f
#define VEND1_IPC_DATA(_n)              (VEND1_IPC_DATA0 + (_n))

/* Sub command of CMD_INFO */
#define IPC_INFO_VERSION                0x1

/* Sub command of CMD_SYS_CPU */
#define IPC_SYS_CPU_REBOOT              0x3
#define IPC_SYS_CPU_IMAGE_OFST          0x4
#define IPC_SYS_CPU_IMAGE_CHECK         0x5
#define IPC_SYS_CPU_PHY_ENABLE          0x6

/* Sub command of CMD_CFG_PARAM */
#define IPC_CFG_PARAM_DIRECT            0x4

/* CFG DIRECT sub command */
#define IPC_CFG_PARAM_DIRECT_NG_PHYCTRL 0x1
#define IPC_CFG_PARAM_DIRECT_CU_AN      0x2
#define IPC_CFG_PARAM_DIRECT_SDS_PCS    0x3
#define IPC_CFG_PARAM_DIRECT_AUTO_EEE   0x4
#define IPC_CFG_PARAM_DIRECT_SDS_PMA    0x5
#define IPC_CFG_PARAM_DIRECT_DPC_RA     0x6
#define IPC_CFG_PARAM_DIRECT_DPC_PKT_CHK 0x7
#define IPC_CFG_PARAM_DIRECT_DPC_SDS_WAIT_ETH 0x8
#define IPC_CFG_PARAM_DIRECT_WDT        0x9
#define IPC_CFG_PARAM_DIRECT_SDS_RESTART_AN 0x10
#define IPC_CFG_PARAM_DIRECT_TEMP_MON   0x11
#define IPC_CFG_PARAM_DIRECT_WOL        0x12

/* Sub command of CMD_TEMP_MON */
#define IPC_CMD_TEMP_MON_GET            0x4

#define AS21XXX_MDIO_AN_C22             0xffe0

#define PHY_ID_AS21XXX                  0x75009410
/* AS21xxx ID Legend
 * AS21x1xxB1
 *     ^ ^^
 *     | |J: Supports SyncE/PTP
 *     | |P: No SyncE/PTP support
 *     | 1: Supports 2nd Serdes
 *     | 2: Not 2nd Serdes support
 *     0: 10G, 5G, 2.5G
 *     5: 5G, 2.5G
 *     2: 2.5G
 */
#define PHY_ID_AS21011JB1               0x75009402
#define PHY_ID_AS21011PB1               0x75009412
#define PHY_ID_AS21010JB1               0x75009422
#define PHY_ID_AS21010PB1               0x75009432
#define PHY_ID_AS21511JB1               0x75009442
#define PHY_ID_AS21511PB1               0x75009452
#define PHY_ID_AS21510JB1               0x75009462
#define PHY_ID_AS21510PB1               0x75009472
#define PHY_ID_AS21210JB1               0x75009482
#define PHY_ID_AS21210PB1               0x75009492
#define PHY_VENDOR_AEONSEMI             0x75009400

#define AEON_MAX_LEDS                   5
#define AEON_IPC_DELAY                  10000
#define AEON_IPC_TIMEOUT                (AEON_IPC_DELAY * 100)
#define AEON_IPC_DATA_NUM_REGISTERS     8
#define AEON_IPC_DATA_MAX               (AEON_IPC_DATA_NUM_REGISTERS * sizeof(u16))

#define AEON_BOOT_ADDR                  0x1000
#define AEON_CPU_BOOT_ADDR              0x2000
#define AEON_CPU_CTRL_FW_LOAD           (BIT(4) | BIT(2) | BIT(1) | BIT(0))
#define AEON_CPU_CTRL_FW_START          BIT(0)

enum as21xxx_led_event {
        VEND1_LED_REG_A_EVENT_ON_10 = 0x0,
        VEND1_LED_REG_A_EVENT_ON_100,
        VEND1_LED_REG_A_EVENT_ON_1000,
        VEND1_LED_REG_A_EVENT_ON_2500,
        VEND1_LED_REG_A_EVENT_ON_5000,
        VEND1_LED_REG_A_EVENT_ON_10000,
        VEND1_LED_REG_A_EVENT_ON_FE_GE,
        VEND1_LED_REG_A_EVENT_ON_NG,
        VEND1_LED_REG_A_EVENT_ON_FULL_DUPLEX,
        VEND1_LED_REG_A_EVENT_ON_COLLISION,
        VEND1_LED_REG_A_EVENT_BLINK_TX,
        VEND1_LED_REG_A_EVENT_BLINK_RX,
        VEND1_LED_REG_A_EVENT_BLINK_ACT,
        VEND1_LED_REG_A_EVENT_ON_LINK,
        VEND1_LED_REG_A_EVENT_ON_LINK_BLINK_ACT,
        VEND1_LED_REG_A_EVENT_ON_LINK_BLINK_RX,
        VEND1_LED_REG_A_EVENT_ON_FE_GE_BLINK_ACT,
        VEND1_LED_REG_A_EVENT_ON_NG_BLINK_ACT,
        VEND1_LED_REG_A_EVENT_ON_NG_BLINK_FE_GE,
        VEND1_LED_REG_A_EVENT_ON_FD_BLINK_COLLISION,
        VEND1_LED_REG_A_EVENT_ON,
        VEND1_LED_REG_A_EVENT_OFF,
};

struct as21xxx_led_pattern_info {
        unsigned int pattern;
        u16 val;
};

struct as21xxx_priv {
        bool parity_status;
        /* Protect concurrent IPC access */
        struct mutex ipc_lock;
};

static struct as21xxx_led_pattern_info as21xxx_led_supported_pattern[] = {
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10),
                .val = VEND1_LED_REG_A_EVENT_ON_10
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_100),
                .val = VEND1_LED_REG_A_EVENT_ON_100
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_1000),
                .val = VEND1_LED_REG_A_EVENT_ON_1000
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_2500),
                .val = VEND1_LED_REG_A_EVENT_ON_2500
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_5000),
                .val = VEND1_LED_REG_A_EVENT_ON_5000
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10000),
                .val = VEND1_LED_REG_A_EVENT_ON_10000
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK),
                .val = VEND1_LED_REG_A_EVENT_ON_LINK
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10) |
                           BIT(TRIGGER_NETDEV_LINK_100) |
                           BIT(TRIGGER_NETDEV_LINK_1000),
                .val = VEND1_LED_REG_A_EVENT_ON_FE_GE
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_2500) |
                           BIT(TRIGGER_NETDEV_LINK_5000) |
                           BIT(TRIGGER_NETDEV_LINK_10000),
                .val = VEND1_LED_REG_A_EVENT_ON_NG
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_FULL_DUPLEX),
                .val = VEND1_LED_REG_A_EVENT_ON_FULL_DUPLEX
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_TX),
                .val = VEND1_LED_REG_A_EVENT_BLINK_TX
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_RX),
                .val = VEND1_LED_REG_A_EVENT_BLINK_RX
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_TX) |
                           BIT(TRIGGER_NETDEV_RX),
                .val = VEND1_LED_REG_A_EVENT_BLINK_ACT
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10) |
                           BIT(TRIGGER_NETDEV_LINK_100) |
                           BIT(TRIGGER_NETDEV_LINK_1000) |
                           BIT(TRIGGER_NETDEV_LINK_2500) |
                           BIT(TRIGGER_NETDEV_LINK_5000) |
                           BIT(TRIGGER_NETDEV_LINK_10000),
                .val = VEND1_LED_REG_A_EVENT_ON_LINK
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10) |
                           BIT(TRIGGER_NETDEV_LINK_100) |
                           BIT(TRIGGER_NETDEV_LINK_1000) |
                           BIT(TRIGGER_NETDEV_LINK_2500) |
                           BIT(TRIGGER_NETDEV_LINK_5000) |
                           BIT(TRIGGER_NETDEV_LINK_10000) |
                           BIT(TRIGGER_NETDEV_TX) |
                           BIT(TRIGGER_NETDEV_RX),
                .val = VEND1_LED_REG_A_EVENT_ON_LINK_BLINK_ACT
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10) |
                           BIT(TRIGGER_NETDEV_LINK_100) |
                           BIT(TRIGGER_NETDEV_LINK_1000) |
                           BIT(TRIGGER_NETDEV_LINK_2500) |
                           BIT(TRIGGER_NETDEV_LINK_5000) |
                           BIT(TRIGGER_NETDEV_LINK_10000) |
                           BIT(TRIGGER_NETDEV_RX),
                .val = VEND1_LED_REG_A_EVENT_ON_LINK_BLINK_RX
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_10) |
                           BIT(TRIGGER_NETDEV_LINK_100) |
                           BIT(TRIGGER_NETDEV_LINK_1000) |
                           BIT(TRIGGER_NETDEV_TX) |
                           BIT(TRIGGER_NETDEV_RX),
                .val = VEND1_LED_REG_A_EVENT_ON_FE_GE_BLINK_ACT
        },
        {
                .pattern = BIT(TRIGGER_NETDEV_LINK_2500) |
                           BIT(TRIGGER_NETDEV_LINK_5000) |
                           BIT(TRIGGER_NETDEV_LINK_10000) |
                           BIT(TRIGGER_NETDEV_TX) |
                           BIT(TRIGGER_NETDEV_RX),
                .val = VEND1_LED_REG_A_EVENT_ON_NG_BLINK_ACT
        }
};

static int aeon_firmware_boot(struct phy_device *phydev, const u8 *data,
                              size_t size)
{
        int i, ret;
        u16 val;

        ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLB_REG_CPU_CTRL,
                             VEND1_GLB_CPU_CTRL_MASK, AEON_CPU_CTRL_FW_LOAD);
        if (ret)
                return ret;

        ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_FW_START_ADDR,
                            AEON_BOOT_ADDR);
        if (ret)
                return ret;

        ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1,
                             VEND1_GLB_REG_MDIO_INDIRECT_ADDRCMD,
                             0x3ffc, 0xc000);
        if (ret)
                return ret;

        val = phy_read_mmd(phydev, MDIO_MMD_VEND1,
                           VEND1_GLB_REG_MDIO_INDIRECT_STATUS);
        if (val > 1) {
                phydev_err(phydev, "wrong origin mdio_indirect_status: %x\n", val);
                return -EINVAL;
        }

        /* Firmware is always aligned to u16 */
        for (i = 0; i < size; i += 2) {
                val = data[i + 1] << 8 | data[i];

                ret = phy_write_mmd(phydev, MDIO_MMD_VEND1,
                                    VEND1_GLB_REG_MDIO_INDIRECT_LOAD, val);
                if (ret)
                        return ret;
        }

        ret = phy_write_mmd(phydev, MDIO_MMD_VEND1,
                            VEND1_GLB_REG_CPU_RESET_ADDR_LO_BASEADDR,
                            lower_16_bits(AEON_CPU_BOOT_ADDR));
        if (ret)
                return ret;

        ret = phy_write_mmd(phydev, MDIO_MMD_VEND1,
                            VEND1_GLB_REG_CPU_RESET_ADDR_HI_BASEADDR,
                            upper_16_bits(AEON_CPU_BOOT_ADDR));
        if (ret)
                return ret;

        return phy_modify_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLB_REG_CPU_CTRL,
                              VEND1_GLB_CPU_CTRL_MASK, AEON_CPU_CTRL_FW_START);
}

static int aeon_firmware_load(struct phy_device *phydev)
{
        struct device *dev = &phydev->mdio.dev;
        const struct firmware *fw;
        const char *fw_name;
        int ret;

        ret = of_property_read_string(dev->of_node, "firmware-name",
                                      &fw_name);
        if (ret)
                return ret;

        ret = request_firmware(&fw, fw_name, dev);
        if (ret) {
                phydev_err(phydev, "failed to find FW file %s (%d)\n",
                           fw_name, ret);
                return ret;
        }

        ret = aeon_firmware_boot(phydev, fw->data, fw->size);

        release_firmware(fw);

        return ret;
}

static bool aeon_ipc_ready(u16 val, bool parity_status)
{
        u16 status;

        if (FIELD_GET(AEON_IPC_STS_PARITY, val) != parity_status)
                return false;

        status = val & AEON_IPC_STS_STATUS;

        return status != AEON_IPC_STS_STATUS_RCVD &&
               status != AEON_IPC_STS_STATUS_PROCESS &&
               status != AEON_IPC_STS_STATUS_BUSY;
}

static int aeon_ipc_wait_cmd(struct phy_device *phydev, bool parity_status)
{
        u16 val;

        /* Exit condition logic:
         * - Wait for parity bit equal
         * - Wait for status success, error OR ready
         */
        return phy_read_mmd_poll_timeout(phydev, MDIO_MMD_VEND1, VEND1_IPC_STS, val,
                                         aeon_ipc_ready(val, parity_status),
                                         AEON_IPC_DELAY, AEON_IPC_TIMEOUT, false);
}

static int aeon_ipc_send_cmd(struct phy_device *phydev,
                             struct as21xxx_priv *priv,
                             u16 cmd, u16 *ret_sts)
{
        bool curr_parity;
        int ret;

        /* The IPC sync by using a single parity bit.
         * Each CMD have alternately this bit set or clear
         * to understand correct flow and packet order.
         */
        curr_parity = priv->parity_status;
        if (priv->parity_status)
                cmd |= AEON_IPC_CMD_PARITY;

        /* Always update parity for next packet */
        priv->parity_status = !priv->parity_status;

        ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_CMD, cmd);
        if (ret)
                return ret;

        /* Wait for packet to be processed */
        usleep_range(AEON_IPC_DELAY, AEON_IPC_DELAY + 5000);

        /* With no ret_sts, ignore waiting for packet completion
         * (ipc parity bit sync)
         */
        if (!ret_sts)
                return 0;

        ret = aeon_ipc_wait_cmd(phydev, curr_parity);
        if (ret)
                return ret;

        ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_STS);
        if (ret < 0)
                return ret;

        *ret_sts = ret;
        if ((*ret_sts & AEON_IPC_STS_STATUS) != AEON_IPC_STS_STATUS_SUCCESS)
                return -EINVAL;

        return 0;
}

/* If data is NULL, return 0 or negative error.
 * If data not NULL, return number of Bytes received from IPC or
 * a negative error.
 */
static int aeon_ipc_send_msg(struct phy_device *phydev,
                             u16 opcode, u16 *data, unsigned int data_len,
                             u16 *ret_data)
{
        struct as21xxx_priv *priv = phydev->priv;
        unsigned int ret_size;
        u16 cmd, ret_sts;
        int ret;
        int i;

        /* IPC have a max of 8 register to transfer data,
         * make sure we never exceed this.
         */
        if (data_len > AEON_IPC_DATA_MAX)
                return -EINVAL;

        for (i = 0; i < data_len / sizeof(u16); i++)
                phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_DATA(i),
                              data[i]);

        cmd = FIELD_PREP(AEON_IPC_CMD_SIZE, data_len) |
              FIELD_PREP(AEON_IPC_CMD_OPCODE, opcode);

        mutex_lock(&priv->ipc_lock);

        ret = aeon_ipc_send_cmd(phydev, priv, cmd, &ret_sts);
        if (ret) {
                phydev_err(phydev, "failed to send ipc msg for %x: %d\n",
                           opcode, ret);
                goto out;
        }

        if (!data)
                goto out;

        if ((ret_sts & AEON_IPC_STS_STATUS) == AEON_IPC_STS_STATUS_ERROR) {
                ret = -EINVAL;
                goto out;
        }

        /* Prevent IPC from stack smashing the kernel.
         * We can't trust IPC to return a good value and we always
         * preallocate space for 16 Bytes.
         */
        ret_size = FIELD_GET(AEON_IPC_STS_SIZE, ret_sts);
        if (ret_size > AEON_IPC_DATA_MAX) {
                ret = -EINVAL;
                goto out;
        }

        /* Read data from IPC data register for ret_size value from IPC */
        for (i = 0; i < DIV_ROUND_UP(ret_size, sizeof(u16)); i++) {
                ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_IPC_DATA(i));
                if (ret < 0)
                        goto out;

                ret_data[i] = ret;
        }

        ret = ret_size;

out:
        mutex_unlock(&priv->ipc_lock);

        return ret;
}

static int aeon_ipc_noop(struct phy_device *phydev,
                         struct as21xxx_priv *priv, u16 *ret_sts)
{
        u16 cmd;

        cmd = FIELD_PREP(AEON_IPC_CMD_SIZE, 0) |
              FIELD_PREP(AEON_IPC_CMD_OPCODE, IPC_CMD_NOOP);

        return aeon_ipc_send_cmd(phydev, priv, cmd, ret_sts);
}

/* Logic to sync parity bit with IPC.
 * We send 2 NOP cmd with same partity and we wait for IPC
 * to handle the packet only for the second one. This way
 * we make sure we are sync for every next cmd.
 */
static int aeon_ipc_sync_parity(struct phy_device *phydev,
                                struct as21xxx_priv *priv)
{
        u16 ret_sts;
        int ret;

        mutex_lock(&priv->ipc_lock);

        /* Send NOP with no parity */
        aeon_ipc_noop(phydev, priv, NULL);

        /* Reset packet parity */
        priv->parity_status = false;

        /* Send second NOP with no parity */
        ret = aeon_ipc_noop(phydev, priv, &ret_sts);

        mutex_unlock(&priv->ipc_lock);

        /* We expect to return -EINVAL */
        if (ret != -EINVAL)
                return ret;

        if ((ret_sts & AEON_IPC_STS_STATUS) != AEON_IPC_STS_STATUS_READY) {
                phydev_err(phydev, "Invalid IPC status on sync parity: %x\n",
                           ret_sts);
                return -EINVAL;
        }

        return 0;
}

static int aeon_ipc_get_fw_version(struct phy_device *phydev)
{
        u16 ret_data[AEON_IPC_DATA_NUM_REGISTERS], data[1];
        char fw_version[AEON_IPC_DATA_MAX + 1];
        int ret;

        data[0] = IPC_INFO_VERSION;

        ret = aeon_ipc_send_msg(phydev, IPC_CMD_INFO, data,
                                sizeof(data), ret_data);
        if (ret < 0)
                return ret;

        /* Make sure FW version is NULL terminated */
        memcpy(fw_version, ret_data, ret);
        fw_version[ret] = '\0';

        phydev_info(phydev, "Firmware Version: %s\n", fw_version);

        return 0;
}

static int aeon_dpc_ra_enable(struct phy_device *phydev)
{
        u16 data[2];

        data[0] = IPC_CFG_PARAM_DIRECT;
        data[1] = IPC_CFG_PARAM_DIRECT_DPC_RA;

        return aeon_ipc_send_msg(phydev, IPC_CMD_CFG_PARAM, data,
                                 sizeof(data), NULL);
}

static int as21xxx_probe(struct phy_device *phydev)
{
        struct as21xxx_priv *priv;
        int ret;

        priv = devm_kzalloc(&phydev->mdio.dev,
                            sizeof(*priv), GFP_KERNEL);
        if (!priv)
                return -ENOMEM;
        phydev->priv = priv;

        ret = devm_mutex_init(&phydev->mdio.dev,
                              &priv->ipc_lock);
        if (ret)
                return ret;

        ret = aeon_ipc_sync_parity(phydev, priv);
        if (ret)
                return ret;

        ret = aeon_ipc_get_fw_version(phydev);
        if (ret)
                return ret;

        /* Enable PTP clk if not already Enabled */
        ret = phy_set_bits_mmd(phydev, MDIO_MMD_VEND1, VEND1_PTP_CLK,
                               VEND1_PTP_CLK_EN);
        if (ret)
                return ret;

        return aeon_dpc_ra_enable(phydev);
}

static int as21xxx_read_link(struct phy_device *phydev, int *bmcr)
{
        int status;

        /* Normal C22 BMCR report inconsistent data, use
         * the mapped C22 in C45 to have more consistent link info.
         */
        *bmcr = phy_read_mmd(phydev, MDIO_MMD_AN,
                             AS21XXX_MDIO_AN_C22 + MII_BMCR);
        if (*bmcr < 0)
                return *bmcr;

        /* Autoneg is being started, therefore disregard current
         * link status and report link as down.
         */
        if (*bmcr & BMCR_ANRESTART) {
                phydev->link = 0;
                return 0;
        }

        status = phy_read_mmd(phydev, MDIO_MMD_AN, MDIO_STAT1);
        if (status < 0)
                return status;

        phydev->link = !!(status & MDIO_STAT1_LSTATUS);

        return 0;
}

static int as21xxx_read_c22_lpa(struct phy_device *phydev)
{
        int lpagb;

        /* MII_STAT1000 are only filled in the mapped C22
         * in C45, use that to fill lpagb values and check.
         */
        lpagb = phy_read_mmd(phydev, MDIO_MMD_AN,
                             AS21XXX_MDIO_AN_C22 + MII_STAT1000);
        if (lpagb < 0)
                return lpagb;

        if (lpagb & LPA_1000MSFAIL) {
                int adv = phy_read_mmd(phydev, MDIO_MMD_AN,
                                       AS21XXX_MDIO_AN_C22 + MII_CTRL1000);

                if (adv < 0)
                        return adv;

                if (adv & CTL1000_ENABLE_MASTER)
                        phydev_err(phydev, "Master/Slave resolution failed, maybe conflicting manual settings?\n");
                else
                        phydev_err(phydev, "Master/Slave resolution failed\n");
                return -ENOLINK;
        }

        mii_stat1000_mod_linkmode_lpa_t(phydev->lp_advertising,
                                        lpagb);

        return 0;
}

static int as21xxx_read_status(struct phy_device *phydev)
{
        int bmcr, old_link = phydev->link;
        int ret;

        ret = as21xxx_read_link(phydev, &bmcr);
        if (ret)
                return ret;

        /* why bother the PHY if nothing can have changed */
        if (phydev->autoneg == AUTONEG_ENABLE && old_link && phydev->link)
                return 0;

        phydev->speed = SPEED_UNKNOWN;
        phydev->duplex = DUPLEX_UNKNOWN;
        phydev->pause = 0;
        phydev->asym_pause = 0;

        if (phydev->autoneg == AUTONEG_ENABLE) {
                ret = genphy_c45_read_lpa(phydev);
                if (ret)
                        return ret;

                ret = as21xxx_read_c22_lpa(phydev);
                if (ret)
                        return ret;

                phy_resolve_aneg_linkmode(phydev);
        } else {
                int speed;

                linkmode_zero(phydev->lp_advertising);

                speed = phy_read_mmd(phydev, MDIO_MMD_VEND1,
                                     VEND1_SPEED_STATUS);
                if (speed < 0)
                        return speed;

                switch (speed & VEND1_SPEED_STATUS) {
                case VEND1_SPEED_10000:
                        phydev->speed = SPEED_10000;
                        phydev->duplex = DUPLEX_FULL;
                        break;
                case VEND1_SPEED_5000:
                        phydev->speed = SPEED_5000;
                        phydev->duplex = DUPLEX_FULL;
                        break;
                case VEND1_SPEED_2500:
                        phydev->speed = SPEED_2500;
                        phydev->duplex = DUPLEX_FULL;
                        break;
                case VEND1_SPEED_1000:
                        phydev->speed = SPEED_1000;
                        if (bmcr & BMCR_FULLDPLX)
                                phydev->duplex = DUPLEX_FULL;
                        else
                                phydev->duplex = DUPLEX_HALF;
                        break;
                case VEND1_SPEED_100:
                        phydev->speed = SPEED_100;
                        phydev->duplex = DUPLEX_FULL;
                        break;
                case VEND1_SPEED_10:
                        phydev->speed = SPEED_10;
                        phydev->duplex = DUPLEX_FULL;
                        break;
                default:
                        return -EINVAL;
                }
        }

        return 0;
}

static int as21xxx_led_brightness_set(struct phy_device *phydev,
                                      u8 index, enum led_brightness value)
{
        u16 val = VEND1_LED_REG_A_EVENT_OFF;

        if (index > AEON_MAX_LEDS)
                return -EINVAL;

        if (value)
                val = VEND1_LED_REG_A_EVENT_ON;

        return phy_modify_mmd(phydev, MDIO_MMD_VEND1,
                              VEND1_LED_REG(index),
                              VEND1_LED_REG_A_EVENT,
                              FIELD_PREP(VEND1_LED_REG_A_EVENT, val));
}

static int as21xxx_led_hw_is_supported(struct phy_device *phydev, u8 index,
                                       unsigned long rules)
{
        int i;

        if (index > AEON_MAX_LEDS)
                return -EINVAL;

        for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++)
                if (rules == as21xxx_led_supported_pattern[i].pattern)
                        return 0;

        return -EOPNOTSUPP;
}

static int as21xxx_led_hw_control_get(struct phy_device *phydev, u8 index,
                                      unsigned long *rules)
{
        int i, val;

        if (index > AEON_MAX_LEDS)
                return -EINVAL;

        val = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_LED_REG(index));
        if (val < 0)
                return val;

        val = FIELD_GET(VEND1_LED_REG_A_EVENT, val);
        for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++)
                if (val == as21xxx_led_supported_pattern[i].val) {
                        *rules = as21xxx_led_supported_pattern[i].pattern;
                        return 0;
                }

        /* Should be impossible */
        return -EINVAL;
}

static int as21xxx_led_hw_control_set(struct phy_device *phydev, u8 index,
                                      unsigned long rules)
{
        u16 val = 0;
        int i;

        if (index > AEON_MAX_LEDS)
                return -EINVAL;

        for (i = 0; i < ARRAY_SIZE(as21xxx_led_supported_pattern); i++)
                if (rules == as21xxx_led_supported_pattern[i].pattern) {
                        val = as21xxx_led_supported_pattern[i].val;
                        break;
                }

        return phy_modify_mmd(phydev, MDIO_MMD_VEND1,
                              VEND1_LED_REG(index),
                              VEND1_LED_REG_A_EVENT,
                              FIELD_PREP(VEND1_LED_REG_A_EVENT, val));
}

static int as21xxx_led_polarity_set(struct phy_device *phydev, int index,
                                    unsigned long modes)
{
        bool led_active_low = false;
        u16 mask, val = 0;
        u32 mode;

        if (index > AEON_MAX_LEDS)
                return -EINVAL;

        for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
                switch (mode) {
                case PHY_LED_ACTIVE_LOW:
                        led_active_low = true;
                        break;
                case PHY_LED_ACTIVE_HIGH: /* default mode */
                        led_active_low = false;
                        break;
                default:
                        return -EINVAL;
                }
        }

        mask = VEND1_GLB_CPU_CTRL_LED_POLARITY(index);
        if (led_active_low)
                val = VEND1_GLB_CPU_CTRL_LED_POLARITY(index);

        return phy_modify_mmd(phydev, MDIO_MMD_VEND1,
                              VEND1_GLB_REG_CPU_CTRL,
                              mask, val);
}

static int as21xxx_match_phy_device(struct phy_device *phydev,
                                    const struct phy_driver *phydrv)
{
        struct as21xxx_priv *priv;
        u16 ret_sts;
        u32 phy_id;
        int ret;

        /* Skip PHY that are not AS21xxx */
        if (!phy_id_compare_vendor(phydev->c45_ids.device_ids[MDIO_MMD_PCS],
                                   PHY_VENDOR_AEONSEMI))
                return genphy_match_phy_device(phydev, phydrv);

        /* Read PHY ID to handle firmware loaded or HW reset */
        ret = phy_read_mmd(phydev, MDIO_MMD_PCS, MII_PHYSID1);
        if (ret < 0)
                return ret;
        phy_id = ret << 16;

        ret = phy_read_mmd(phydev, MDIO_MMD_PCS, MII_PHYSID2);
        if (ret < 0)
                return ret;
        phy_id |= ret;

        /* With PHY ID not the generic AS21xxx one assume
         * the firmware just loaded
         */
        if (phy_id != PHY_ID_AS21XXX)
                return phy_id == phydrv->phy_id;

        /* Allocate temp priv and load the firmware */
        priv = kzalloc_obj(*priv);
        if (!priv)
                return -ENOMEM;

        mutex_init(&priv->ipc_lock);

        ret = aeon_firmware_load(phydev);
        if (ret)
                goto out;

        /* Sync parity... */
        ret = aeon_ipc_sync_parity(phydev, priv);
        if (ret)
                goto out;

        /* ...and send a third NOOP cmd to wait for firmware finish loading */
        ret = aeon_ipc_noop(phydev, priv, &ret_sts);
        if (ret)
                goto out;

out:
        mutex_destroy(&priv->ipc_lock);
        kfree(priv);

        /* Return can either be 0 or a negative error code.
         * Returning 0 here means THIS is NOT a suitable PHY.
         *
         * For the specific case of the generic Aeonsemi PHY ID that
         * needs the firmware the be loaded first to have a correct PHY ID,
         * this is OK as a matching PHY ID will be found right after.
         * This relies on the driver probe order where the first PHY driver
         * probed is the generic one.
         */
        return ret;
}

static struct phy_driver as21xxx_drivers[] = {
        {
                /* PHY expose in C45 as 0x7500 0x9410
                 * before firmware is loaded.
                 * This driver entry must be attempted first to load
                 * the firmware and thus update the ID registers.
                 */
                PHY_ID_MATCH_EXACT(PHY_ID_AS21XXX),
                .name           = "Aeonsemi AS21xxx",
                .match_phy_device = as21xxx_match_phy_device,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21011JB1),
                .name           = "Aeonsemi AS21011JB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21011PB1),
                .name           = "Aeonsemi AS21011PB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21010PB1),
                .name           = "Aeonsemi AS21010PB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21010JB1),
                .name           = "Aeonsemi AS21010JB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21210PB1),
                .name           = "Aeonsemi AS21210PB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21510JB1),
                .name           = "Aeonsemi AS21510JB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21510PB1),
                .name           = "Aeonsemi AS21510PB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21511JB1),
                .name           = "Aeonsemi AS21511JB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21210JB1),
                .name           = "Aeonsemi AS21210JB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
        {
                PHY_ID_MATCH_EXACT(PHY_ID_AS21511PB1),
                .name           = "Aeonsemi AS21511PB1",
                .probe          = as21xxx_probe,
                .match_phy_device = as21xxx_match_phy_device,
                .read_status    = as21xxx_read_status,
                .led_brightness_set = as21xxx_led_brightness_set,
                .led_hw_is_supported = as21xxx_led_hw_is_supported,
                .led_hw_control_set = as21xxx_led_hw_control_set,
                .led_hw_control_get = as21xxx_led_hw_control_get,
                .led_polarity_set = as21xxx_led_polarity_set,
        },
};
module_phy_driver(as21xxx_drivers);

static struct mdio_device_id __maybe_unused as21xxx_tbl[] = {
        { PHY_ID_MATCH_VENDOR(PHY_VENDOR_AEONSEMI) },
        { }
};
MODULE_DEVICE_TABLE(mdio, as21xxx_tbl);

MODULE_DESCRIPTION("Aeonsemi AS21xxx PHY driver");
MODULE_AUTHOR("Christian Marangi <ansuelsmth@gmail.com>");
MODULE_LICENSE("GPL");