/*
 * Nuvoton NPCM8xx PCS Module
 *
 * Copyright 2022 Google LLC
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 */

/*
 * Disclaimer:
 * Currently we only implemented the default values of the registers and
 * the soft reset feature. These are required to boot up the GMAC module
 * in Linux kernel for NPCM845 boards. Other functionalities are not modeled.
 */

#include "qemu/osdep.h"

#include "exec/hwaddr.h"
#include "hw/registerfields.h"
#include "hw/net/npcm_pcs.h"
#include "migration/vmstate.h"
#include "qemu/log.h"
#include "qemu/units.h"
#include "trace.h"

#define NPCM_PCS_IND_AC_BA      0x1fe
#define NPCM_PCS_IND_SR_CTL     0x1e00
#define NPCM_PCS_IND_SR_MII     0x1f00
#define NPCM_PCS_IND_SR_TIM     0x1f07
#define NPCM_PCS_IND_VR_MII     0x1f80

REG16(NPCM_PCS_SR_CTL_ID1, 0x08)
REG16(NPCM_PCS_SR_CTL_ID2, 0x0a)
REG16(NPCM_PCS_SR_CTL_STS, 0x10)

REG16(NPCM_PCS_SR_MII_CTRL, 0x00)
REG16(NPCM_PCS_SR_MII_STS, 0x02)
REG16(NPCM_PCS_SR_MII_DEV_ID1, 0x04)
REG16(NPCM_PCS_SR_MII_DEV_ID2, 0x06)
REG16(NPCM_PCS_SR_MII_AN_ADV, 0x08)
REG16(NPCM_PCS_SR_MII_LP_BABL, 0x0a)
REG16(NPCM_PCS_SR_MII_AN_EXPN, 0x0c)
REG16(NPCM_PCS_SR_MII_EXT_STS, 0x1e)

REG16(NPCM_PCS_SR_TIM_SYNC_ABL, 0x10)
REG16(NPCM_PCS_SR_TIM_SYNC_TX_MAX_DLY_LWR, 0x12)
REG16(NPCM_PCS_SR_TIM_SYNC_TX_MAX_DLY_UPR, 0x14)
REG16(NPCM_PCS_SR_TIM_SYNC_TX_MIN_DLY_LWR, 0x16)
REG16(NPCM_PCS_SR_TIM_SYNC_TX_MIN_DLY_UPR, 0x18)
REG16(NPCM_PCS_SR_TIM_SYNC_RX_MAX_DLY_LWR, 0x1a)
REG16(NPCM_PCS_SR_TIM_SYNC_RX_MAX_DLY_UPR, 0x1c)
REG16(NPCM_PCS_SR_TIM_SYNC_RX_MIN_DLY_LWR, 0x1e)
REG16(NPCM_PCS_SR_TIM_SYNC_RX_MIN_DLY_UPR, 0x20)

REG16(NPCM_PCS_VR_MII_MMD_DIG_CTRL1, 0x000)
REG16(NPCM_PCS_VR_MII_AN_CTRL, 0x002)
REG16(NPCM_PCS_VR_MII_AN_INTR_STS, 0x004)
REG16(NPCM_PCS_VR_MII_TC, 0x006)
REG16(NPCM_PCS_VR_MII_DBG_CTRL, 0x00a)
REG16(NPCM_PCS_VR_MII_EEE_MCTRL0, 0x00c)
REG16(NPCM_PCS_VR_MII_EEE_TXTIMER, 0x010)
REG16(NPCM_PCS_VR_MII_EEE_RXTIMER, 0x012)
REG16(NPCM_PCS_VR_MII_LINK_TIMER_CTRL, 0x014)
REG16(NPCM_PCS_VR_MII_EEE_MCTRL1, 0x016)
REG16(NPCM_PCS_VR_MII_DIG_STS, 0x020)
REG16(NPCM_PCS_VR_MII_ICG_ERRCNT1, 0x022)
REG16(NPCM_PCS_VR_MII_MISC_STS, 0x030)
REG16(NPCM_PCS_VR_MII_RX_LSTS, 0x040)
REG16(NPCM_PCS_VR_MII_MP_TX_BSTCTRL0, 0x070)
REG16(NPCM_PCS_VR_MII_MP_TX_LVLCTRL0, 0x074)
REG16(NPCM_PCS_VR_MII_MP_TX_GENCTRL0, 0x07a)
REG16(NPCM_PCS_VR_MII_MP_TX_GENCTRL1, 0x07c)
REG16(NPCM_PCS_VR_MII_MP_TX_STS, 0x090)
REG16(NPCM_PCS_VR_MII_MP_RX_GENCTRL0, 0x0b0)
REG16(NPCM_PCS_VR_MII_MP_RX_GENCTRL1, 0x0b2)
REG16(NPCM_PCS_VR_MII_MP_RX_LOS_CTRL0, 0x0ba)
REG16(NPCM_PCS_VR_MII_MP_MPLL_CTRL0, 0x0f0)
REG16(NPCM_PCS_VR_MII_MP_MPLL_CTRL1, 0x0f2)
REG16(NPCM_PCS_VR_MII_MP_MPLL_STS, 0x110)
REG16(NPCM_PCS_VR_MII_MP_MISC_CTRL2, 0x126)
REG16(NPCM_PCS_VR_MII_MP_LVL_CTRL, 0x130)
REG16(NPCM_PCS_VR_MII_MP_MISC_CTRL0, 0x132)
REG16(NPCM_PCS_VR_MII_MP_MISC_CTRL1, 0x134)
REG16(NPCM_PCS_VR_MII_DIG_CTRL2, 0x1c2)
REG16(NPCM_PCS_VR_MII_DIG_ERRCNT_SEL, 0x1c4)

/* Register Fields */
#define NPCM_PCS_SR_MII_CTRL_RST            BIT(15)

static const uint16_t npcm_pcs_sr_ctl_cold_reset_values[NPCM_PCS_NR_SR_CTLS] = {
    [R_NPCM_PCS_SR_CTL_ID1]                 = 0x699e,
    [R_NPCM_PCS_SR_CTL_STS]                 = 0x8000,
};

static const uint16_t npcm_pcs_sr_mii_cold_reset_values[NPCM_PCS_NR_SR_MIIS] = {
    [R_NPCM_PCS_SR_MII_CTRL]                = 0x1140,
    [R_NPCM_PCS_SR_MII_STS]                 = 0x0109,
    [R_NPCM_PCS_SR_MII_DEV_ID1]             = 0x699e,
    [R_NPCM_PCS_SR_MII_DEV_ID2]             = 0xced0,
    [R_NPCM_PCS_SR_MII_AN_ADV]              = 0x0020,
    [R_NPCM_PCS_SR_MII_EXT_STS]             = 0xc000,
};

static const uint16_t npcm_pcs_sr_tim_cold_reset_values[NPCM_PCS_NR_SR_TIMS] = {
    [R_NPCM_PCS_SR_TIM_SYNC_ABL]            = 0x0003,
    [R_NPCM_PCS_SR_TIM_SYNC_TX_MAX_DLY_LWR] = 0x0038,
    [R_NPCM_PCS_SR_TIM_SYNC_TX_MIN_DLY_LWR] = 0x0038,
    [R_NPCM_PCS_SR_TIM_SYNC_RX_MAX_DLY_LWR] = 0x0058,
    [R_NPCM_PCS_SR_TIM_SYNC_RX_MIN_DLY_LWR] = 0x0048,
};

static const uint16_t npcm_pcs_vr_mii_cold_reset_values[NPCM_PCS_NR_VR_MIIS] = {
    [R_NPCM_PCS_VR_MII_MMD_DIG_CTRL1]         = 0x2400,
    [R_NPCM_PCS_VR_MII_AN_INTR_STS]           = 0x000a,
    [R_NPCM_PCS_VR_MII_EEE_MCTRL0]            = 0x899c,
    [R_NPCM_PCS_VR_MII_DIG_STS]               = 0x0010,
    [R_NPCM_PCS_VR_MII_MP_TX_BSTCTRL0]        = 0x000a,
    [R_NPCM_PCS_VR_MII_MP_TX_LVLCTRL0]        = 0x007f,
    [R_NPCM_PCS_VR_MII_MP_TX_GENCTRL0]        = 0x0001,
    [R_NPCM_PCS_VR_MII_MP_RX_GENCTRL0]        = 0x0100,
    [R_NPCM_PCS_VR_MII_MP_RX_GENCTRL1]        = 0x1100,
    [R_NPCM_PCS_VR_MII_MP_RX_LOS_CTRL0]       = 0x000e,
    [R_NPCM_PCS_VR_MII_MP_MPLL_CTRL0]         = 0x0100,
    [R_NPCM_PCS_VR_MII_MP_MPLL_CTRL1]         = 0x0032,
    [R_NPCM_PCS_VR_MII_MP_MPLL_STS]           = 0x0001,
    [R_NPCM_PCS_VR_MII_MP_LVL_CTRL]           = 0x0019,
};

static void npcm_pcs_soft_reset(NPCMPCSState *s)
{
    memcpy(s->sr_ctl, npcm_pcs_sr_ctl_cold_reset_values,
           NPCM_PCS_NR_SR_CTLS * sizeof(uint16_t));
    memcpy(s->sr_mii, npcm_pcs_sr_mii_cold_reset_values,
           NPCM_PCS_NR_SR_MIIS * sizeof(uint16_t));
    memcpy(s->sr_tim, npcm_pcs_sr_tim_cold_reset_values,
           NPCM_PCS_NR_SR_TIMS * sizeof(uint16_t));
    memcpy(s->vr_mii, npcm_pcs_vr_mii_cold_reset_values,
           NPCM_PCS_NR_VR_MIIS * sizeof(uint16_t));
}

static uint16_t npcm_pcs_read_sr_ctl(NPCMPCSState *s, hwaddr offset)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_SR_CTLS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: SR_CTL read offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return 0;
    }

    return s->sr_ctl[regno];
}

static uint16_t npcm_pcs_read_sr_mii(NPCMPCSState *s, hwaddr offset)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_SR_MIIS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: SR_MII read offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return 0;
    }

    return s->sr_mii[regno];
}

static uint16_t npcm_pcs_read_sr_tim(NPCMPCSState *s, hwaddr offset)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_SR_TIMS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: SR_TIM read offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return 0;
    }

    return s->sr_tim[regno];
}

static uint16_t npcm_pcs_read_vr_mii(NPCMPCSState *s, hwaddr offset)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_VR_MIIS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: VR_MII read offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return 0;
    }

    return s->vr_mii[regno];
}

static void npcm_pcs_write_sr_ctl(NPCMPCSState *s, hwaddr offset, uint16_t v)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_SR_CTLS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: SR_CTL write offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return;
    }

    s->sr_ctl[regno] = v;
}

static void npcm_pcs_write_sr_mii(NPCMPCSState *s, hwaddr offset, uint16_t v)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_SR_MIIS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: SR_MII write offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return;
    }

    s->sr_mii[regno] = v;

    if ((offset == A_NPCM_PCS_SR_MII_CTRL) && (v & NPCM_PCS_SR_MII_CTRL_RST)) {
        /* Trigger a soft reset */
        npcm_pcs_soft_reset(s);
    }
}

static void npcm_pcs_write_sr_tim(NPCMPCSState *s, hwaddr offset, uint16_t v)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_SR_TIMS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: SR_TIM write offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return;
    }

    s->sr_tim[regno] = v;
}

static void npcm_pcs_write_vr_mii(NPCMPCSState *s, hwaddr offset, uint16_t v)
{
    hwaddr regno = offset / sizeof(uint16_t);

    if (regno >= NPCM_PCS_NR_VR_MIIS) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: VR_MII write offset 0x%04" HWADDR_PRIx
                      " is out of range.\n",
                      DEVICE(s)->canonical_path, offset);
        return;
    }

    s->vr_mii[regno] = v;
}

static uint64_t npcm_pcs_read(void *opaque, hwaddr offset, unsigned size)
{
    NPCMPCSState *s = opaque;
    uint16_t v = 0;

    if (offset == NPCM_PCS_IND_AC_BA) {
        v = s->indirect_access_base;
    } else {
        switch (s->indirect_access_base) {
        case NPCM_PCS_IND_SR_CTL:
            v = npcm_pcs_read_sr_ctl(s, offset);
            break;

        case NPCM_PCS_IND_SR_MII:
            v = npcm_pcs_read_sr_mii(s, offset);
            break;

        case NPCM_PCS_IND_SR_TIM:
            v = npcm_pcs_read_sr_tim(s, offset);
            break;

        case NPCM_PCS_IND_VR_MII:
            v = npcm_pcs_read_vr_mii(s, offset);
            break;

        default:
            qemu_log_mask(LOG_GUEST_ERROR,
                          "%s: Read with invalid indirect address base: 0x%"
                          PRIx16 "\n", DEVICE(s)->canonical_path,
                          s->indirect_access_base);
        }
    }

    trace_npcm_pcs_reg_read(DEVICE(s)->canonical_path, s->indirect_access_base,
                            offset, v);
    return v;
}

static void npcm_pcs_write(void *opaque, hwaddr offset,
                              uint64_t v, unsigned size)
{
    NPCMPCSState *s = opaque;

    trace_npcm_pcs_reg_write(DEVICE(s)->canonical_path, s->indirect_access_base,
                             offset, v);
    if (offset == NPCM_PCS_IND_AC_BA) {
        s->indirect_access_base = v;
    } else {
        switch (s->indirect_access_base) {
        case NPCM_PCS_IND_SR_CTL:
            npcm_pcs_write_sr_ctl(s, offset, v);
            break;

        case NPCM_PCS_IND_SR_MII:
            npcm_pcs_write_sr_mii(s, offset, v);
            break;

        case NPCM_PCS_IND_SR_TIM:
            npcm_pcs_write_sr_tim(s, offset, v);
            break;

        case NPCM_PCS_IND_VR_MII:
            npcm_pcs_write_vr_mii(s, offset, v);
            break;

        default:
            qemu_log_mask(LOG_GUEST_ERROR,
                          "%s: Write with invalid indirect address base: 0x%02"
                          PRIx16 "\n", DEVICE(s)->canonical_path,
                          s->indirect_access_base);
        }
    }
}

static void npcm_pcs_enter_reset(Object *obj, ResetType type)
{
    NPCMPCSState *s = NPCM_PCS(obj);

    npcm_pcs_soft_reset(s);
}

static const struct MemoryRegionOps npcm_pcs_ops = {
    .read = npcm_pcs_read,
    .write = npcm_pcs_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 2,
        .max_access_size = 2,
        .unaligned = false,
    },
};

static void npcm_pcs_realize(DeviceState *dev, Error **errp)
{
    NPCMPCSState *pcs = NPCM_PCS(dev);
    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);

    memory_region_init_io(&pcs->iomem, OBJECT(pcs), &npcm_pcs_ops, pcs,
                          TYPE_NPCM_PCS, 8 * KiB);
    sysbus_init_mmio(sbd, &pcs->iomem);
}

static const VMStateDescription vmstate_npcm_pcs = {
    .name = TYPE_NPCM_PCS,
    .version_id = 0,
    .minimum_version_id = 0,
    .fields = (VMStateField[]) {
        VMSTATE_UINT16(indirect_access_base, NPCMPCSState),
        VMSTATE_UINT16_ARRAY(sr_ctl, NPCMPCSState, NPCM_PCS_NR_SR_CTLS),
        VMSTATE_UINT16_ARRAY(sr_mii, NPCMPCSState, NPCM_PCS_NR_SR_MIIS),
        VMSTATE_UINT16_ARRAY(sr_tim, NPCMPCSState, NPCM_PCS_NR_SR_TIMS),
        VMSTATE_UINT16_ARRAY(vr_mii, NPCMPCSState, NPCM_PCS_NR_VR_MIIS),
        VMSTATE_END_OF_LIST(),
    },
};

static void npcm_pcs_class_init(ObjectClass *klass, const void *data)
{
    ResettableClass *rc = RESETTABLE_CLASS(klass);
    DeviceClass *dc = DEVICE_CLASS(klass);

    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    dc->desc = "NPCM PCS Controller";
    dc->realize = npcm_pcs_realize;
    dc->vmsd = &vmstate_npcm_pcs;
    rc->phases.enter = npcm_pcs_enter_reset;
}

static const TypeInfo npcm_pcs_types[] = {
    {
        .name = TYPE_NPCM_PCS,
        .parent = TYPE_SYS_BUS_DEVICE,
        .instance_size = sizeof(NPCMPCSState),
        .class_init = npcm_pcs_class_init,
    },
};
DEFINE_TYPES(npcm_pcs_types)
