This is a model for the lowrisc_ethernet block, a simple
network driver found in the CVA6 CoreV APU.

Signed-off-by: Ben Dooks <ben.do...@codethink.co.uk>
---
note, squashed in "hw/net: lowrisc cleanups"
---
 hw/net/Kconfig               |   3 +
 hw/net/lowrisc.c             | 474 +++++++++++++++++++++++++++++++++++
 hw/net/meson.build           |   1 +
 hw/net/trace-events          |  10 +
 include/hw/net/lowrisc_eth.h |  54 ++++
 5 files changed, 542 insertions(+)
 create mode 100644 hw/net/lowrisc.c
 create mode 100644 include/hw/net/lowrisc_eth.h

diff --git a/hw/net/Kconfig b/hw/net/Kconfig
index 7f80218d10..790fe1ce60 100644
--- a/hw/net/Kconfig
+++ b/hw/net/Kconfig
@@ -109,6 +109,9 @@ config LASI_82596
     bool
     select I82596_COMMON
 
+config LOWRISC_ETH
+    bool
+
 config SUNHME
     bool
 
diff --git a/hw/net/lowrisc.c b/hw/net/lowrisc.c
new file mode 100644
index 0000000000..98177793e6
--- /dev/null
+++ b/hw/net/lowrisc.c
@@ -0,0 +1,474 @@
+/*
+ * QEMU LowRISC ethernet emulation
+ *
+ * Ben Dooks <ben.do...@codethink.co.uk>
+ * Copyright (c) 2025 Codethink Ltd,
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+
+#include "hw/irq.h"
+#include "hw/registerfields.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "net/eth.h"
+
+#include "hw/net/lowrisc_eth.h"
+#include "trace.h"
+
+/* address space is roughtly
+ * 0x0000..0x07ff - nothing here
+ * 0x8000..0x0880 - control and status registers
+ * 0x1000..0x4000 - transmission buffer(s) ?
+ * 0x4000..0x8000 - recive buffers
+ *
+ * Registers are bottom 32bits of each 64bit address, and the SRAMs for the
+ * transmit and receive buffers seem to be 64bit capable even if the code says
+ * they have 32bit data ports.
+ */
+REG32(MACLO, 0x800)
+
+REG32(MACHI, 0x808)
+    FIELD(MACHI, RX_LOOPBACK, 17, 1)
+    FIELD(MACHI, RX_ALL, 22, 1)
+    FIELD(MACHI, IRQ_EN, 23, 1)
+
+REG32(TPLR, 0x810)
+    FIELD(TPLR, FRAME_ADDR, 16, 12)
+    FIELD(TPLR, PACKET_LEN, 0, 12)
+    FIELD(TPLR, BUSY, 31, 1)
+
+REG32(TFCS, 0x0818)
+
+REG32(MDIOCTRL, 0x0820)
+    FIELD(MDIOCTRL, M_CLK, 0, 1)
+    FIELD(MDIOCTRL, M_DO, 1, 1)
+    FIELD(MDIOCTRL, M_OE, 2, 1) /* 0 = in, 1 = out */
+    FIELD(MDIOCTRL, M_DI, 3, 1)
+
+REG32(RFCS, 0x0828)
+
+REG32(RSR, 0x830)
+    FIELD(RSR, RECV_FIRST, 0, 4)
+    FIELD(RSR, RECV_NEXT, 4, 4)
+    FIELD(RSR, RECV_LAST, 8, 4)
+    FIELD(RSR, AVAIL, 12, 1)
+    FIELD(RSR, IRQ,  13, 1)
+
+REG32(RBAD, 0x0838)
+
+#define R_RPLR          (0x0840)        /* array of up to 16 registers */
+#define R_RPLR_END      (R_RPLR + ((NR_RPLR-1) * 8))
+
+#define R_TXBUFF        (0x1000)
+#define R_RXBUFF        (0x4000)
+
+#define R_TXBUFF_END    (R_TXBUFF + TX_BUFF_SZ - 1)
+#define R_RXBUFF_END    (R_RXBUFF + RX_BUFF_SZ - 1)
+
+static uint32_t *find_register(LowriscEthState *s, hwaddr offset)
+{
+    if (offset >= R_RPLR && offset <= R_RPLR_END) {
+        offset -= R_RPLR;
+        offset /= 8;
+
+        return &s->r_rplr[offset];
+    }
+
+    switch (offset >> 2) {
+    case R_MACLO:
+        return &s->r_maclo;
+    case R_MACHI:
+        return &s->r_machi;
+    case R_MDIOCTRL:
+        return &s->r_mdioctrl;
+    case R_TPLR:
+        return &s->r_tplr;
+    case R_RFCS:
+        return &s->r_rfcs;
+    case R_RSR:
+        return &s->r_rsr;
+
+    default:
+        fprintf(stderr, "failed to find register for offset 0x%04x\n",
+                (unsigned)offset);
+        return NULL;
+    }
+
+    return NULL;
+}
+
+static void lowrisc_eth_update_irq(LowriscEthState *s)
+{
+    unsigned next, first;
+    uint32_t rsr = s->r_rsr;
+    bool set = false;
+
+    next = FIELD_EX32(rsr, RSR, RECV_NEXT);
+    first = FIELD_EX32(rsr, RSR, RECV_FIRST);
+
+    if (FIELD_EX32(s->r_machi, MACHI, IRQ_EN)) {
+        if (next != first) {
+            set = true;
+        }
+    }
+
+    /* update rsr for availability and irq-signalled state */
+    rsr = FIELD_DP32(rsr, RSR, AVAIL, (next != first) ? 1 : 0);
+    s->r_rsr = FIELD_DP32(rsr, RSR, IRQ, set ? 1 : 0);
+
+    trace_lowrisc_eth_irq(set, first, next,
+                          FIELD_EX32(s->r_machi, MACHI, IRQ_EN));
+    qemu_set_irq(s->irq, set);
+}
+
+static ssize_t lowrisc_eth_receive(NetClientState *nc,
+                                   const uint8_t *buf, size_t size)
+{
+    LowriscEthState *s = qemu_get_nic_opaque(nc);
+    unsigned next, first, last;
+    unsigned index;
+    uint32_t rsr = s->r_rsr;
+    uint8_t *rxb;
+
+    last = FIELD_EX32(rsr, RSR, RECV_LAST);
+    next = FIELD_EX32(rsr, RSR, RECV_NEXT);
+    first = FIELD_EX32(rsr, RSR, RECV_FIRST);
+
+    trace_lowrisc_eth_rx((unsigned)size, first, next, last);
+
+    if (next == ((first + last) & 15)) {
+        /* we should not really get here, we're already full */
+        return -1;
+    }
+
+    if (is_multicast_ether_addr(buf) || is_broadcast_ether_addr(buf)) {
+        /* we're good for this packet */
+    } else if (FIELD_EX32(s->r_machi, MACHI, RX_ALL)) {
+        /* accepting everytihng, good here */
+    } else if (FIELD_EX32(s->r_machi, MACHI, RX_LOOPBACK)) {
+        /* should probably accept loopback packets...? */
+    } else {
+        /* check for destination being our MAC */
+        if (memcmp(buf, s->conf.macaddr.a, sizeof(s->conf.macaddr.a)) != 0) {
+            return size;
+        }
+    }
+
+    /* accepting the packet, work out which slot to put it in */
+    index = next & 7;
+    rxb = s->rx_buff + (index * RX_SZ);
+
+    trace_lowrisc_eth_rx_good((unsigned)size, index);
+
+    memcpy(rxb, buf, size);
+    //todo - add an actual FCS as it expects it in the rx buffer
+    s->r_rplr[index] = size + 4;
+
+    next = (next + 1) & 15;
+    s->r_rsr = FIELD_DP32(rsr, RSR, RECV_NEXT, next);
+
+    trace_lowrisc_eth_rx_upd_rsr(s->r_rsr);
+    lowrisc_eth_update_irq(s);
+
+    return size;
+}
+
+static bool lowrisc_eth_can_receive(NetClientState *nc)
+{
+    LowriscEthState *s = qemu_get_nic_opaque(nc);
+    unsigned next, first, last;
+    uint32_t rsr = s->r_rsr;
+    bool ok;
+
+    last = FIELD_EX32(rsr, RSR, RECV_LAST);
+    next = FIELD_EX32(rsr, RSR, RECV_NEXT);
+    first = FIELD_EX32(rsr, RSR, RECV_FIRST);
+    ok = next != ((first + last) & 15);
+
+    trace_lowrisc_eth_rx_check(first, next, first, ok);
+    return ok;
+}
+
+#define make_mac(__m, __b) (((uint32_t)(__m)) << (__b))
+
+static void lowrisc_eth_init_registers(LowriscEthState *s)
+{
+    const uint8_t *mac;
+
+    /* general register init */
+
+    s->r_tplr = 0;
+    s->r_rfcs = 0;
+    s->r_tplr = 0;
+    s->r_rsr = 0;
+    s->r_mdioctrl = FIELD_DP32(0x0, MDIOCTRL, M_DI, 1);
+    memset(&s->r_rplr, 0, sizeof(s->r_rplr));
+
+    /* init mac registers */
+
+    mac = &s->conf.macaddr.a[0];
+    s->r_maclo = make_mac(mac[5], 0) | make_mac(mac[4], 8) | make_mac(mac[3], 
16) | make_mac(mac[2], 24);
+    s->r_machi = make_mac(mac[1], 0) | make_mac(mac[0], 8);
+
+    /* init the rx and tx buffres for now */
+    memset(&s->rx_buff, 0x44, sizeof(s->rx_buff));
+    memset(&s->tx_buff, 0x55, sizeof(s->tx_buff));
+}
+
+static void lowrisc_eth_reset(DeviceState *d)
+{
+    LowriscEthState *s = LOWRISC_ETH(d);
+
+    lowrisc_eth_init_registers(s);
+    lowrisc_eth_update_irq(s);
+}
+
+static uint64_t lowrisc_eth_read(void *opaque, hwaddr offset, unsigned size)
+{
+    LowriscEthState *s = opaque;
+    uint64_t retval = 0;
+
+    if (offset >= R_TXBUFF && offset <= R_TXBUFF_END) {
+        uint64_t *ptr = (uint64_t *)(s->tx_buff + offset - R_TXBUFF);
+        retval = *ptr;
+    } else if (offset >= R_RXBUFF && offset <= R_RXBUFF_END) {
+        uint64_t *ptr = (uint64_t *)(s->rx_buff + offset - R_RXBUFF);
+        retval = *ptr;
+    } else {
+        uint32_t *reg = find_register(s, offset);
+
+        if (reg) {
+            retval = *reg;
+        } else {
+            retval = ~(uint64_t)0x0;
+        }
+    }
+
+    /* note, there's nothing in the read path that would need updating
+     * the irq state, so no need to re-sync interrupts */
+
+    trace_lowrisc_eth_io_read(offset, retval);
+    return retval;
+}
+
+
+static void lowrisc_eth_update_mdioctrl(LowriscEthState *s, uint32_t val)
+{
+    /* since we're not implementing any sort of bit-banged MDIO, we just
+     * return the data input as high, which seems to be enough to allow
+     * the PHY link checks to work
+     */
+
+    s->r_mdioctrl = FIELD_DP32(s->r_mdioctrl, MDIOCTRL, M_DI, 1);
+}
+
+/* update tplr register, assume we're transmitting a packet */
+static void lowrisc_eth_update_tplr(LowriscEthState *s, uint32_t val)
+{
+    unsigned len = FIELD_EX32(val, TPLR, PACKET_LEN);
+
+    s->r_tplr = val | R_TPLR_BUSY_MASK;
+
+    trace_lowrisc_eth_tx(len);
+
+    if (FIELD_EX32(s->r_machi, MACHI, RX_LOOPBACK)) {
+        lowrisc_eth_receive(qemu_get_queue(s->nic),  s->tx_buff, len);
+    } else {
+        qemu_send_packet(qemu_get_queue(s->nic), s->tx_buff, len);
+    }
+
+    /* clear busy as we are done now, no irq (oversight?) to be raised */
+    s->r_tplr &= ~R_TPLR_BUSY_MASK;
+}
+
+static void lowrisc_eth_update_mac(LowriscEthState *s)
+{
+    MACAddr addr;
+
+    /* if the maclo or machi registers change, then change qemu config */
+
+    addr.a[5] = s->r_maclo;
+    addr.a[4] = s->r_maclo >> 8;
+    addr.a[3] = s->r_maclo >> 16;
+    addr.a[2] = s->r_maclo >> 24;
+    addr.a[1] = s->r_machi;
+    addr.a[0] = s->r_machi >> 8;
+
+    if (memcmp(&addr, &s->conf.macaddr, sizeof(s->conf.macaddr)) != 0) {
+        s->conf.macaddr = addr;
+        qemu_format_nic_info_str(qemu_get_queue(s->nic), s->conf.macaddr.a);
+    }
+}
+
+#define update_rsr(__s, __field, __val) \
+    do { __s->r_rsr= FIELD_DP32(__s->r_rsr, RSR, __field, __val); } while(0)
+
+static void lowrisc_eth_write(void *opaque, hwaddr offset, uint64_t val,
+                              unsigned size)
+{
+    LowriscEthState *s = (LowriscEthState *)opaque;
+
+    trace_lowrisc_eth_io_write(offset, val);
+
+    if (offset >= R_TXBUFF && offset <= R_TXBUFF_END) {
+        uint8_t *ptr = s->tx_buff;
+
+        offset -= R_TXBUFF;
+        size = MIN(size, sizeof(s->tx_buff)-offset);
+        memcpy(ptr + offset, &val, size);
+    } else if (offset >= R_RXBUFF && offset <= R_RXBUFF_END) {
+        uint8_t *ptr = s->rx_buff;
+
+        offset -= R_RXBUFF;
+        size = MIN(size, sizeof(s->rx_buff)-offset);
+        memcpy(ptr + offset, &val, size);
+    } else {
+        uint32_t *reg = find_register(s, offset);
+
+        /* the core in cva6 may not fully check byte enables
+         * so just assume we're writing to the registers in full */
+
+        if (reg) {
+            switch (offset >> 2) {
+            case R_MACLO:
+                *reg = val;
+                lowrisc_eth_update_mac(s);
+                break;
+
+            case R_MACHI:
+                *reg = val;
+                lowrisc_eth_update_mac(s);
+                lowrisc_eth_update_irq(s);
+                break;
+
+            case R_RSR:
+                /* bits 3:0 of this write to the firstbuff field */
+                update_rsr(s, RECV_FIRST, val & 15);
+                lowrisc_eth_update_irq(s);
+                break;
+
+            case R_RFCS:
+                /* bits 3:0 of this write to the lastbuff field */
+                update_rsr(s, RECV_LAST, val & 15);
+                lowrisc_eth_update_irq(s);
+                break;
+
+            case R_TPLR:
+                *reg = val;
+                lowrisc_eth_update_tplr(s, val);
+                break;
+
+            case R_MDIOCTRL:
+                *reg = val;
+                lowrisc_eth_update_mdioctrl(s, val);
+                break;
+
+            default:
+                /* for now just assume anything else is just writable */
+                *reg = val;
+            }
+
+            return;
+        }
+    }
+}
+
+static const MemoryRegionOps lowrisc_eth_ops = {
+    .read = lowrisc_eth_read,
+    .write = lowrisc_eth_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    /* set max access size to 64bit, for any register it is only 64bit
+     * and tx/rx memory might be able to sub-write
+     */
+    .impl.max_access_size = 8,
+};
+
+static NetClientInfo net_lowrisc_eth_info = {
+    .type = NET_CLIENT_DRIVER_NIC,
+    .size = sizeof(NICState),
+    .can_receive = lowrisc_eth_can_receive,
+    .receive = lowrisc_eth_receive,
+    /* note, we do not currently have any way of signaling link status */
+};
+
+static void lowrisc_eth_realize(DeviceState *dev, Error **errp)
+{
+    LowriscEthState *s = LOWRISC_ETH(dev);
+
+    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
+
+    qemu_macaddr_default_if_unset(&s->conf.macaddr);
+
+    s->nic = qemu_new_nic(&net_lowrisc_eth_info, &s->conf,
+                          object_get_typename(OBJECT(dev)), dev->id,
+                          &dev->mem_reentrancy_guard, s);
+}
+
+static void lowrisc_eth_init(Object *obj)
+{
+    LowriscEthState *s = LOWRISC_ETH(obj);
+    DeviceState *dev = DEVICE(obj);
+
+    lowrisc_eth_init_registers(s);
+    lowrisc_eth_update_irq(s);
+
+    memory_region_init_io(&s->iomem, OBJECT(s), &lowrisc_eth_ops, s,
+                          "net", R_RXBUFF_END);
+
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
+}
+
+static const VMStateDescription vmstate_lowrisc_eth = {
+    .name = "lowrisc_eth",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(r_maclo, LowriscEthState),
+        VMSTATE_UINT32(r_machi,  LowriscEthState),
+        VMSTATE_UINT32(r_mdioctrl,  LowriscEthState),
+        VMSTATE_UINT32(r_rfcs,  LowriscEthState),
+        VMSTATE_UINT32(r_tplr,  LowriscEthState),
+        VMSTATE_UINT32(r_rsr, LowriscEthState),
+        VMSTATE_UINT32_ARRAY(r_rplr, LowriscEthState, NR_RPLR),
+
+        /* might be overkill, but store rx and tx buffers */
+        VMSTATE_UINT8_ARRAY(tx_buff, LowriscEthState, TX_BUFF_SZ),
+        VMSTATE_UINT8_ARRAY(rx_buff, LowriscEthState, RX_BUFF_SZ),
+
+        VMSTATE_END_OF_LIST(),
+    },
+};
+
+static const Property lowrisc_eth_properties[] = {
+    DEFINE_NIC_PROPERTIES(LowriscEthState, conf),
+};
+
+static void lowrisc_eth_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = lowrisc_eth_realize;
+    device_class_set_props(dc, lowrisc_eth_properties);
+    dc->vmsd = &vmstate_lowrisc_eth;
+    device_class_set_legacy_reset(dc, lowrisc_eth_reset);
+}
+
+static const TypeInfo lowrisc_eth_info = {
+    .name  = TYPE_LOWRISC_ETH,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(LowriscEthState),
+    .instance_init = lowrisc_eth_init,
+    .class_init = lowrisc_eth_class_init,
+};
+
+static void lowrisc_eth_register_types(void)
+{
+    type_register_static(&lowrisc_eth_info);
+}
+
+type_init(lowrisc_eth_register_types)
diff --git a/hw/net/meson.build b/hw/net/meson.build
index e6759e26ca..79a65850fc 100644
--- a/hw/net/meson.build
+++ b/hw/net/meson.build
@@ -21,6 +21,7 @@ system_ss.add(when: 'CONFIG_SMC91C111', if_true: 
files('smc91c111.c'))
 system_ss.add(when: 'CONFIG_LAN9118', if_true: files('lan9118.c'))
 system_ss.add(when: 'CONFIG_LAN9118_PHY', if_true: files('lan9118_phy.c'))
 system_ss.add(when: 'CONFIG_NE2000_ISA', if_true: files('ne2000-isa.c'))
+system_ss.add(when: 'CONFIG_LOWRISC_ETH', if_true: files('lowrisc.c'))
 system_ss.add(when: 'CONFIG_OPENCORES_ETH', if_true: files('opencores_eth.c'))
 system_ss.add(when: 'CONFIG_XGMAC', if_true: files('xgmac.c'))
 system_ss.add(when: 'CONFIG_MIPSNET', if_true: files('mipsnet.c'))
diff --git a/hw/net/trace-events b/hw/net/trace-events
index 72b69c4a8b..acf1851eb4 100644
--- a/hw/net/trace-events
+++ b/hw/net/trace-events
@@ -20,6 +20,16 @@ lan9118_phy_reset(void) ""
 lance_mem_readw(uint64_t addr, uint32_t ret) "addr=0x%"PRIx64"val=0x%04x"
 lance_mem_writew(uint64_t addr, uint32_t val) "addr=0x%"PRIx64"val=0x%04x"
 
+# lowrisc.c
+lowrisc_eth_io_read(uint64_t addr, uint64_t val) "addr=0x%04"PRIx64" 
val=0x%016"PRIx64
+lowrisc_eth_io_write(uint64_t addr, uint64_t val) "addr=0x%04"PRIx64" 
val=0x%016"PRIx64
+lowrisc_eth_tx(unsigned length) "transmitting packet (%u bytes)"
+lowrisc_eth_irq(bool set, unsigned first, unsigned next, unsigned irqen) 
"set=%d first=%u next=%u irqen=%u"
+lowrisc_eth_rx(unsigned length, unsigned first, unsigned next, unsigned last) 
"rx length %u (%u,%u,%u)"
+lowrisc_eth_rx_good(unsigned length, unsigned index) "received %u bytes to 
index %u"
+lowrisc_eth_rx_check(unsigned first, unsigned next, unsigned last, bool ok) 
"%u,%u,%u -> %u"
+lowrisc_eth_rx_upd_rsr(uint64_t val) "RSR 0x%"PRIx64
+
 # mipsnet.c
 mipsnet_send(uint32_t size) "sending len=%u"
 mipsnet_receive(uint32_t size) "receiving len=%u"
diff --git a/include/hw/net/lowrisc_eth.h b/include/hw/net/lowrisc_eth.h
new file mode 100644
index 0000000000..1f27d92ca8
--- /dev/null
+++ b/include/hw/net/lowrisc_eth.h
@@ -0,0 +1,54 @@
+/*
+ * QEMU LowRISC ethernet emulation
+ *
+  * Ben Dooks <ben.do...@codethink.co.uk>
+  * Copyright (c) 2025 Codethink Ltd,
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+
+#ifndef LOWRISC_ETH_H
+#define LOWRISC_ETH_H
+#include "qom/object.h"
+
+#define TYPE_LOWRISC_ETH "lowrisc_eth"
+OBJECT_DECLARE_SIMPLE_TYPE(LowriscEthState, LOWRISC_ETH)
+
+#include "net/net.h"
+#include "hw/sysbus.h"
+
+#define RX_SZ           (2048)
+#define NR_RX_BUFFS     (8)
+
+#define RX_BUFF_SZ (NR_RX_BUFFS * RX_SZ)
+#define TX_BUFF_SZ (0x1000)
+
+/* whilst the rx pointers are all 4 bit, the core only uses 3 bits for buffer 
index */
+#define NR_RPLR (8)
+
+struct LowriscEthState {
+    /*< private >*/
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+    MemoryRegion iomem;
+    NICState *nic;
+    NICConf conf;
+    qemu_irq irq;
+
+    /* register states */
+    uint32_t r_maclo;
+    uint32_t r_machi;
+    uint32_t r_mdioctrl;
+    uint32_t r_rfcs;
+    uint32_t r_tplr;
+    uint32_t r_rsr;
+    uint32_t r_rplr[NR_RPLR];
+
+    /* packet buffers */
+    uint8_t rx_buff[RX_BUFF_SZ];
+    uint8_t tx_buff[TX_BUFF_SZ];
+};
+
+#endif /* LOWRISC_ETH_H */
-- 
2.37.2.352.g3c44437643


Reply via email to