/*
 * i.MX USB PHY
 *
 * Copyright (c) 2020 Guenter Roeck <linux@roeck-us.net>
 *
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
 * See the COPYING file in the top-level directory.
 *
 * We need to implement basic reset control in the PHY control register.
 * For everything else, it is sufficient to set whatever is written.
 */

#include "qemu/osdep.h"
#include "hw/usb/imx-usb-phy.h"
#include "migration/vmstate.h"
#include "qemu/log.h"
#include "qemu/module.h"

static const VMStateDescription vmstate_imx_usbphy = {
    .name = TYPE_IMX_USBPHY,
    .version_id = 1,
    .minimum_version_id = 1,
    .fields = (VMStateField[]) {
        VMSTATE_UINT32_ARRAY(usbphy, IMXUSBPHYState, USBPHY_MAX),
        VMSTATE_END_OF_LIST()
    },
};

static void imx_usbphy_softreset(IMXUSBPHYState *s)
{
    s->usbphy[USBPHY_PWD] = 0x001e1c00;
    s->usbphy[USBPHY_TX] = 0x10060607;
    s->usbphy[USBPHY_RX] = 0x00000000;
    s->usbphy[USBPHY_CTRL] = 0xc0200000;
}

static void imx_usbphy_reset(DeviceState *dev)
{
    IMXUSBPHYState *s = IMX_USBPHY(dev);

    s->usbphy[USBPHY_STATUS] = 0x00000000;
    s->usbphy[USBPHY_DEBUG] = 0x7f180000;
    s->usbphy[USBPHY_DEBUG0_STATUS] = 0x00000000;
    s->usbphy[USBPHY_DEBUG1] = 0x00001000;
    s->usbphy[USBPHY_VERSION] = 0x04020000;

    imx_usbphy_softreset(s);
}

static uint64_t imx_usbphy_read(void *opaque, hwaddr offset, unsigned size)
{
    IMXUSBPHYState *s = (IMXUSBPHYState *)opaque;
    uint32_t index = offset >> 2;
    uint32_t value;

    switch (index) {
    case USBPHY_PWD_SET:
    case USBPHY_TX_SET:
    case USBPHY_RX_SET:
    case USBPHY_CTRL_SET:
    case USBPHY_DEBUG_SET:
    case USBPHY_DEBUG1_SET:
        /*
         * All REG_NAME_SET register access are in fact targeting the
         * REG_NAME register.
         */
        value = s->usbphy[index - 1];
        break;
    case USBPHY_PWD_CLR:
    case USBPHY_TX_CLR:
    case USBPHY_RX_CLR:
    case USBPHY_CTRL_CLR:
    case USBPHY_DEBUG_CLR:
    case USBPHY_DEBUG1_CLR:
        /*
         * All REG_NAME_CLR register access are in fact targeting the
         * REG_NAME register.
         */
        value = s->usbphy[index - 2];
        break;
    case USBPHY_PWD_TOG:
    case USBPHY_TX_TOG:
    case USBPHY_RX_TOG:
    case USBPHY_CTRL_TOG:
    case USBPHY_DEBUG_TOG:
    case USBPHY_DEBUG1_TOG:
        /*
         * All REG_NAME_TOG register access are in fact targeting the
         * REG_NAME register.
         */
        value = s->usbphy[index - 3];
        break;
    default:
        if (index < USBPHY_MAX) {
            value = s->usbphy[index];
        } else {
            qemu_log_mask(LOG_GUEST_ERROR,
                          "%s: Read from non-existing USB PHY register 0x%"
                          HWADDR_PRIx "\n",
                          __func__, offset);
            value = 0;
        }
        break;
    }
    return (uint64_t)value;
}

static void imx_usbphy_write(void *opaque, hwaddr offset, uint64_t value,
                             unsigned size)
{
    IMXUSBPHYState *s = (IMXUSBPHYState *)opaque;
    uint32_t index = offset >> 2;

    switch (index) {
    case USBPHY_CTRL:
        s->usbphy[index] = value;
        if (value & USBPHY_CTRL_SFTRST) {
            imx_usbphy_softreset(s);
        }
        break;
    case USBPHY_PWD:
    case USBPHY_TX:
    case USBPHY_RX:
    case USBPHY_STATUS:
    case USBPHY_DEBUG:
    case USBPHY_DEBUG1:
        s->usbphy[index] = value;
        break;
    case USBPHY_CTRL_SET:
        s->usbphy[index - 1] |= value;
        if (value & USBPHY_CTRL_SFTRST) {
            imx_usbphy_softreset(s);
        }
        break;
    case USBPHY_PWD_SET:
    case USBPHY_TX_SET:
    case USBPHY_RX_SET:
    case USBPHY_DEBUG_SET:
    case USBPHY_DEBUG1_SET:
        /*
         * All REG_NAME_SET register access are in fact targeting the
         * REG_NAME register. So we change the value of the REG_NAME
         * register, setting bits passed in the value.
         */
        s->usbphy[index - 1] |= value;
        break;
    case USBPHY_PWD_CLR:
    case USBPHY_TX_CLR:
    case USBPHY_RX_CLR:
    case USBPHY_CTRL_CLR:
    case USBPHY_DEBUG_CLR:
    case USBPHY_DEBUG1_CLR:
        /*
         * All REG_NAME_CLR register access are in fact targeting the
         * REG_NAME register. So we change the value of the REG_NAME
         * register, unsetting bits passed in the value.
         */
        s->usbphy[index - 2] &= ~value;
        break;
    case USBPHY_CTRL_TOG:
        s->usbphy[index - 3] ^= value;
        if ((value & USBPHY_CTRL_SFTRST) &&
            (s->usbphy[index - 3] & USBPHY_CTRL_SFTRST)) {
            imx_usbphy_softreset(s);
        }
        break;
    case USBPHY_PWD_TOG:
    case USBPHY_TX_TOG:
    case USBPHY_RX_TOG:
    case USBPHY_DEBUG_TOG:
    case USBPHY_DEBUG1_TOG:
        /*
         * All REG_NAME_TOG register access are in fact targeting the
         * REG_NAME register. So we change the value of the REG_NAME
         * register, toggling bits passed in the value.
         */
        s->usbphy[index - 3] ^= value;
        break;
    default:
        /* Other registers are read-only or do not exist */
        qemu_log_mask(LOG_GUEST_ERROR,
                      "%s: Write to %s USB PHY register 0x%"
                      HWADDR_PRIx "\n",
                      __func__,
                      index >= USBPHY_MAX ? "non-existing" : "read-only",
                      offset);
        break;
    }
}

static const struct MemoryRegionOps imx_usbphy_ops = {
    .read = imx_usbphy_read,
    .write = imx_usbphy_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = {
        /*
         * Our device would not work correctly if the guest was doing
         * unaligned access. This might not be a limitation on the real
         * device but in practice there is no reason for a guest to access
         * this device unaligned.
         */
        .min_access_size = 4,
        .max_access_size = 4,
        .unaligned = false,
    },
};

static void imx_usbphy_realize(DeviceState *dev, Error **errp)
{
    IMXUSBPHYState *s = IMX_USBPHY(dev);

    memory_region_init_io(&s->iomem, OBJECT(s), &imx_usbphy_ops, s,
                          "imx-usbphy", 0x1000);
    sysbus_init_mmio(SYS_BUS_DEVICE(s), &s->iomem);
}

static void imx_usbphy_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->reset = imx_usbphy_reset;
    dc->vmsd = &vmstate_imx_usbphy;
    dc->desc = "i.MX USB PHY Module";
    dc->realize = imx_usbphy_realize;
}

static const TypeInfo imx_usbphy_info = {
    .name          = TYPE_IMX_USBPHY,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(IMXUSBPHYState),
    .class_init    = imx_usbphy_class_init,
};

static void imx_usbphy_register_types(void)
{
    type_register_static(&imx_usbphy_info);
}

type_init(imx_usbphy_register_types)
