/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Marvell MV88W8618 / Freecom MusicPal emulation.
 *
 * Copyright (c) 2008 Jan Kiszka
 */

#include "qemu/osdep.h"
#include "qapi/error.h"
#include "hw/qdev-properties.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "hw/net/mv88w8618_eth.h"
#include "migration/vmstate.h"
#include "sysemu/dma.h"
#include "net/net.h"

#define MP_ETH_SIZE             0x00001000

/* Ethernet register offsets */
#define MP_ETH_SMIR             0x010
#define MP_ETH_PCXR             0x408
#define MP_ETH_SDCMR            0x448
#define MP_ETH_ICR              0x450
#define MP_ETH_IMR              0x458
#define MP_ETH_FRDP0            0x480
#define MP_ETH_FRDP1            0x484
#define MP_ETH_FRDP2            0x488
#define MP_ETH_FRDP3            0x48C
#define MP_ETH_CRDP0            0x4A0
#define MP_ETH_CRDP1            0x4A4
#define MP_ETH_CRDP2            0x4A8
#define MP_ETH_CRDP3            0x4AC
#define MP_ETH_CTDP0            0x4E0
#define MP_ETH_CTDP1            0x4E4

/* MII PHY access */
#define MP_ETH_SMIR_DATA        0x0000FFFF
#define MP_ETH_SMIR_ADDR        0x03FF0000
#define MP_ETH_SMIR_OPCODE      (1 << 26) /* Read value */
#define MP_ETH_SMIR_RDVALID     (1 << 27)

/* PHY registers */
#define MP_ETH_PHY1_BMSR        0x00210000
#define MP_ETH_PHY1_PHYSID1     0x00410000
#define MP_ETH_PHY1_PHYSID2     0x00610000

#define MP_PHY_BMSR_LINK        0x0004
#define MP_PHY_BMSR_AUTONEG     0x0008

#define MP_PHY_88E3015          0x01410E20

/* TX descriptor status */
#define MP_ETH_TX_OWN           (1U << 31)

/* RX descriptor status */
#define MP_ETH_RX_OWN           (1U << 31)

/* Interrupt cause/mask bits */
#define MP_ETH_IRQ_RX_BIT       0
#define MP_ETH_IRQ_RX           (1 << MP_ETH_IRQ_RX_BIT)
#define MP_ETH_IRQ_TXHI_BIT     2
#define MP_ETH_IRQ_TXLO_BIT     3

/* Port config bits */
#define MP_ETH_PCXR_2BSM_BIT    28 /* 2-byte incoming suffix */

/* SDMA command bits */
#define MP_ETH_CMD_TXHI         (1 << 23)
#define MP_ETH_CMD_TXLO         (1 << 22)

typedef struct mv88w8618_tx_desc {
    uint32_t cmdstat;
    uint16_t res;
    uint16_t bytes;
    uint32_t buffer;
    uint32_t next;
} mv88w8618_tx_desc;

typedef struct mv88w8618_rx_desc {
    uint32_t cmdstat;
    uint16_t bytes;
    uint16_t buffer_size;
    uint32_t buffer;
    uint32_t next;
} mv88w8618_rx_desc;

OBJECT_DECLARE_SIMPLE_TYPE(mv88w8618_eth_state, MV88W8618_ETH)

struct mv88w8618_eth_state {
    /*< private >*/
    SysBusDevice parent_obj;
    /*< public >*/

    MemoryRegion iomem;
    qemu_irq irq;
    MemoryRegion *dma_mr;
    AddressSpace dma_as;
    uint32_t smir;
    uint32_t icr;
    uint32_t imr;
    int mmio_index;
    uint32_t vlan_header;
    uint32_t tx_queue[2];
    uint32_t rx_queue[4];
    uint32_t frx_queue[4];
    uint32_t cur_rx[4];
    NICState *nic;
    NICConf conf;
};

static void eth_rx_desc_put(AddressSpace *dma_as, uint32_t addr,
                            mv88w8618_rx_desc *desc)
{
    cpu_to_le32s(&desc->cmdstat);
    cpu_to_le16s(&desc->bytes);
    cpu_to_le16s(&desc->buffer_size);
    cpu_to_le32s(&desc->buffer);
    cpu_to_le32s(&desc->next);
    dma_memory_write(dma_as, addr, desc, sizeof(*desc), MEMTXATTRS_UNSPECIFIED);
}

static void eth_rx_desc_get(AddressSpace *dma_as, uint32_t addr,
                            mv88w8618_rx_desc *desc)
{
    dma_memory_read(dma_as, addr, desc, sizeof(*desc), MEMTXATTRS_UNSPECIFIED);
    le32_to_cpus(&desc->cmdstat);
    le16_to_cpus(&desc->bytes);
    le16_to_cpus(&desc->buffer_size);
    le32_to_cpus(&desc->buffer);
    le32_to_cpus(&desc->next);
}

static ssize_t eth_receive(NetClientState *nc, const uint8_t *buf, size_t size)
{
    mv88w8618_eth_state *s = qemu_get_nic_opaque(nc);
    uint32_t desc_addr;
    mv88w8618_rx_desc desc;
    int i;

    for (i = 0; i < 4; i++) {
        desc_addr = s->cur_rx[i];
        if (!desc_addr) {
            continue;
        }
        do {
            eth_rx_desc_get(&s->dma_as, desc_addr, &desc);
            if ((desc.cmdstat & MP_ETH_RX_OWN) && desc.buffer_size >= size) {
                dma_memory_write(&s->dma_as, desc.buffer + s->vlan_header,
                                 buf, size, MEMTXATTRS_UNSPECIFIED);
                desc.bytes = size + s->vlan_header;
                desc.cmdstat &= ~MP_ETH_RX_OWN;
                s->cur_rx[i] = desc.next;

                s->icr |= MP_ETH_IRQ_RX;
                if (s->icr & s->imr) {
                    qemu_irq_raise(s->irq);
                }
                eth_rx_desc_put(&s->dma_as, desc_addr, &desc);
                return size;
            }
            desc_addr = desc.next;
        } while (desc_addr != s->rx_queue[i]);
    }
    return size;
}

static void eth_tx_desc_put(AddressSpace *dma_as, uint32_t addr,
                            mv88w8618_tx_desc *desc)
{
    cpu_to_le32s(&desc->cmdstat);
    cpu_to_le16s(&desc->res);
    cpu_to_le16s(&desc->bytes);
    cpu_to_le32s(&desc->buffer);
    cpu_to_le32s(&desc->next);
    dma_memory_write(dma_as, addr, desc, sizeof(*desc), MEMTXATTRS_UNSPECIFIED);
}

static void eth_tx_desc_get(AddressSpace *dma_as, uint32_t addr,
                            mv88w8618_tx_desc *desc)
{
    dma_memory_read(dma_as, addr, desc, sizeof(*desc), MEMTXATTRS_UNSPECIFIED);
    le32_to_cpus(&desc->cmdstat);
    le16_to_cpus(&desc->res);
    le16_to_cpus(&desc->bytes);
    le32_to_cpus(&desc->buffer);
    le32_to_cpus(&desc->next);
}

static void eth_send(mv88w8618_eth_state *s, int queue_index)
{
    uint32_t desc_addr = s->tx_queue[queue_index];
    mv88w8618_tx_desc desc;
    uint32_t next_desc;
    uint8_t buf[2048];
    int len;

    do {
        eth_tx_desc_get(&s->dma_as, desc_addr, &desc);
        next_desc = desc.next;
        if (desc.cmdstat & MP_ETH_TX_OWN) {
            len = desc.bytes;
            if (len < 2048) {
                dma_memory_read(&s->dma_as, desc.buffer, buf, len,
                                MEMTXATTRS_UNSPECIFIED);
                qemu_send_packet(qemu_get_queue(s->nic), buf, len);
            }
            desc.cmdstat &= ~MP_ETH_TX_OWN;
            s->icr |= 1 << (MP_ETH_IRQ_TXLO_BIT - queue_index);
            eth_tx_desc_put(&s->dma_as, desc_addr, &desc);
        }
        desc_addr = next_desc;
    } while (desc_addr != s->tx_queue[queue_index]);
}

static uint64_t mv88w8618_eth_read(void *opaque, hwaddr offset,
                                   unsigned size)
{
    mv88w8618_eth_state *s = opaque;

    switch (offset) {
    case MP_ETH_SMIR:
        if (s->smir & MP_ETH_SMIR_OPCODE) {
            switch (s->smir & MP_ETH_SMIR_ADDR) {
            case MP_ETH_PHY1_BMSR:
                return MP_PHY_BMSR_LINK | MP_PHY_BMSR_AUTONEG |
                       MP_ETH_SMIR_RDVALID;
            case MP_ETH_PHY1_PHYSID1:
                return (MP_PHY_88E3015 >> 16) | MP_ETH_SMIR_RDVALID;
            case MP_ETH_PHY1_PHYSID2:
                return (MP_PHY_88E3015 & 0xFFFF) | MP_ETH_SMIR_RDVALID;
            default:
                return MP_ETH_SMIR_RDVALID;
            }
        }
        return 0;

    case MP_ETH_ICR:
        return s->icr;

    case MP_ETH_IMR:
        return s->imr;

    case MP_ETH_FRDP0 ... MP_ETH_FRDP3:
        return s->frx_queue[(offset - MP_ETH_FRDP0) / 4];

    case MP_ETH_CRDP0 ... MP_ETH_CRDP3:
        return s->rx_queue[(offset - MP_ETH_CRDP0) / 4];

    case MP_ETH_CTDP0 ... MP_ETH_CTDP1:
        return s->tx_queue[(offset - MP_ETH_CTDP0) / 4];

    default:
        return 0;
    }
}

static void mv88w8618_eth_write(void *opaque, hwaddr offset,
                                uint64_t value, unsigned size)
{
    mv88w8618_eth_state *s = opaque;

    switch (offset) {
    case MP_ETH_SMIR:
        s->smir = value;
        break;

    case MP_ETH_PCXR:
        s->vlan_header = ((value >> MP_ETH_PCXR_2BSM_BIT) & 1) * 2;
        break;

    case MP_ETH_SDCMR:
        if (value & MP_ETH_CMD_TXHI) {
            eth_send(s, 1);
        }
        if (value & MP_ETH_CMD_TXLO) {
            eth_send(s, 0);
        }
        if (value & (MP_ETH_CMD_TXHI | MP_ETH_CMD_TXLO) && s->icr & s->imr) {
            qemu_irq_raise(s->irq);
        }
        break;

    case MP_ETH_ICR:
        s->icr &= value;
        break;

    case MP_ETH_IMR:
        s->imr = value;
        if (s->icr & s->imr) {
            qemu_irq_raise(s->irq);
        }
        break;

    case MP_ETH_FRDP0 ... MP_ETH_FRDP3:
        s->frx_queue[(offset - MP_ETH_FRDP0) / 4] = value;
        break;

    case MP_ETH_CRDP0 ... MP_ETH_CRDP3:
        s->rx_queue[(offset - MP_ETH_CRDP0) / 4] =
            s->cur_rx[(offset - MP_ETH_CRDP0) / 4] = value;
        break;

    case MP_ETH_CTDP0 ... MP_ETH_CTDP1:
        s->tx_queue[(offset - MP_ETH_CTDP0) / 4] = value;
        break;
    }
}

static const MemoryRegionOps mv88w8618_eth_ops = {
    .read = mv88w8618_eth_read,
    .write = mv88w8618_eth_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
};

static void eth_cleanup(NetClientState *nc)
{
    mv88w8618_eth_state *s = qemu_get_nic_opaque(nc);

    s->nic = NULL;
}

static NetClientInfo net_mv88w8618_info = {
    .type = NET_CLIENT_DRIVER_NIC,
    .size = sizeof(NICState),
    .receive = eth_receive,
    .cleanup = eth_cleanup,
};

static void mv88w8618_eth_init(Object *obj)
{
    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
    DeviceState *dev = DEVICE(sbd);
    mv88w8618_eth_state *s = MV88W8618_ETH(dev);

    sysbus_init_irq(sbd, &s->irq);
    memory_region_init_io(&s->iomem, obj, &mv88w8618_eth_ops, s,
                          "mv88w8618-eth", MP_ETH_SIZE);
    sysbus_init_mmio(sbd, &s->iomem);
}

static void mv88w8618_eth_realize(DeviceState *dev, Error **errp)
{
    mv88w8618_eth_state *s = MV88W8618_ETH(dev);

    if (!s->dma_mr) {
        error_setg(errp, TYPE_MV88W8618_ETH " 'dma-memory' link not set");
        return;
    }

    address_space_init(&s->dma_as, s->dma_mr, "emac-dma");
    s->nic = qemu_new_nic(&net_mv88w8618_info, &s->conf,
                          object_get_typename(OBJECT(dev)), dev->id,
                          &dev->mem_reentrancy_guard, s);
}

static const VMStateDescription mv88w8618_eth_vmsd = {
    .name = "mv88w8618_eth",
    .version_id = 1,
    .minimum_version_id = 1,
    .fields = (const VMStateField[]) {
        VMSTATE_UINT32(smir, mv88w8618_eth_state),
        VMSTATE_UINT32(icr, mv88w8618_eth_state),
        VMSTATE_UINT32(imr, mv88w8618_eth_state),
        VMSTATE_UINT32(vlan_header, mv88w8618_eth_state),
        VMSTATE_UINT32_ARRAY(tx_queue, mv88w8618_eth_state, 2),
        VMSTATE_UINT32_ARRAY(rx_queue, mv88w8618_eth_state, 4),
        VMSTATE_UINT32_ARRAY(frx_queue, mv88w8618_eth_state, 4),
        VMSTATE_UINT32_ARRAY(cur_rx, mv88w8618_eth_state, 4),
        VMSTATE_END_OF_LIST()
    }
};

static Property mv88w8618_eth_properties[] = {
    DEFINE_NIC_PROPERTIES(mv88w8618_eth_state, conf),
    DEFINE_PROP_LINK("dma-memory", mv88w8618_eth_state, dma_mr,
                     TYPE_MEMORY_REGION, MemoryRegion *),
    DEFINE_PROP_END_OF_LIST(),
};

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

    dc->vmsd = &mv88w8618_eth_vmsd;
    device_class_set_props(dc, mv88w8618_eth_properties);
    dc->realize = mv88w8618_eth_realize;
}

static const TypeInfo mv88w8618_eth_info = {
    .name          = TYPE_MV88W8618_ETH,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(mv88w8618_eth_state),
    .instance_init = mv88w8618_eth_init,
    .class_init    = mv88w8618_eth_class_init,
};

static void musicpal_register_types(void)
{
    type_register_static(&mv88w8618_eth_info);
}

type_init(musicpal_register_types)