The HCP is the network complex of the Axiado AX3000 SoC. It exposes
three MMIO regions:
- EIP-197: an Inside Secure SafeXcel packet engine. It is the bus
master for all packet I/O, driven by four command/result descriptor
ring pairs (CDR/RDR). Packets are routed to and from one of five
MACs by an "application ID" tag carried in the EIP-96 input and
output tokens.
- SHIM: MAC configuration, per-MAC MDIO and statistics. Configuration
only; no packet data flows through it.
- PHY CSR: PCS/SerDes configuration.
The EIP-197 cryptographic transforms are not modelled; descriptors are
treated as pass-through, which is what the guest asks for when the
block is used purely as a network interface.
The MACs are exposed as "netdev0" to "netdev4" and "mac0" to "mac4"
properties, each with its own virtual PHY on that MAC's MDIO bus.
Signed-off-by: Ratan Lal Dondi <[email protected]>
---
hw/net/Kconfig | 3 +
hw/net/axiado_hcp.c | 1372 +++++++++++++++++++++++++++++++++++
hw/net/meson.build | 2 +
hw/net/trace-events | 6 +
include/hw/net/axiado_hcp.h | 172 +++++
5 files changed, 1555 insertions(+)
create mode 100644 hw/net/axiado_hcp.c
create mode 100644 include/hw/net/axiado_hcp.h
diff --git a/hw/net/Kconfig b/hw/net/Kconfig
index b56a173eed..a0b91bea03 100644
--- a/hw/net/Kconfig
+++ b/hw/net/Kconfig
@@ -162,3 +162,6 @@ config CAN_FLEXCAN
bool
depends on IMX
select CAN_BUS
+
+config AXIADO_HCP
+ bool
diff --git a/hw/net/axiado_hcp.c b/hw/net/axiado_hcp.c
new file mode 100644
index 0000000000..566e4ca780
--- /dev/null
+++ b/hw/net/axiado_hcp.c
@@ -0,0 +1,1372 @@
+/*
+ * Axiado HCP (Header & Crypto Processing) block - QEMU device model
+ *
+ * The HCP is the network complex of the Axiado AX3000 SoC. It exposes three
+ * MMIO regions:
+ *
+ * - EIP-197: an Inside Secure SafeXcel packet engine. It is the bus master
+ * for all packet I/O, driven by four command/result descriptor ring pairs
+ * (CDR/RDR). Packets are routed to and from one of five MACs by an
+ * "application ID" tag carried in the EIP-96 input/output tokens.
+ * - SHIM: MAC configuration, per-MAC MDIO and statistics. Config only; no
+ * packet data flows through it.
+ * - PHY CSR: PCS/SerDes configuration.
+ *
+ * Only the packet datapath is modelled. The EIP-197's cryptographic
+ * transforms are not implemented: descriptors are treated as plain
+ * pass-through, which is what the guest driver requests for network traffic.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (C) 2026 Ratan Lal Dondi <[email protected]>
+ */
+
+#include "qemu/osdep.h"
+#include "hw/net/axiado_hcp.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/qdev-properties-system.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "system/dma.h"
+#include "system/address-spaces.h"
+#include "trace.h"
+
+/*
+ * MAC configuration region, within the SHIM MMIO area
+ */
+
+#define MAC_R_REVISION 0x000
+#define MAC_R_SCRATCH 0x004
+#define MAC_R_COMMAND_CONFIG 0x008
+#define MAC_R_MAC_0 0x00c
+#define MAC_R_MAC_1 0x010
+#define MAC_R_FRM_LENGTH 0x014
+#define MAC_R_RX_FIFO_SECTIONS 0x01c
+#define MAC_R_TX_FIFO_SECTIONS 0x020
+#define MAC_R_MDIO_CFG_STATUS 0x030
+#define MAC_R_MDIO_COMMAND 0x034
+#define MAC_R_MDIO_DATA 0x038
+#define MAC_R_MDIO_REGADDR 0x03c
+#define MAC_R_STATUS 0x040
+#define MAC_R_IF_MODE 0x300
+#define MAC_R_PCS_STATUS 0x304
+
+#define MAC_REVISION_VAL 0x10136
+
+#define MII_BMCR 0x00
+#define MII_BMSR 0x01
+
+#define MDIO_COMMAND_READ_FLAG BIT(15)
+
+#define SHIM_REG_FIFO_BASE 0x040
+#define SHIM_REG_STATS_BASE 0x060
+
+#define FIFO_BIT_RX_RST 0
+#define FIFO_BIT_RX_EMPTY 2
+#define FIFO_BIT_RX_OVFLOW 3
+#define FIFO_BIT_TX_RST 4
+#define FIFO_BIT_TX_EMPTY 6
+#define FIFO_BIT_TX_OVFLOW 7
+
+/*
+ * EIP-207 input classification engine, at offsets from the packet engine base
+ * within the EIP-197 region.
+ */
+#define PE_ICE_SCRATCH_RAM 0x00800
+#define PE_ICE_PUE_CTRL 0x00c80
+#define PE_ICE_FPP_CTRL 0x00d80
+
+/*
+ * Administration RAM, at byte offsets from the ICE scratch RAM base. The
+ * classification firmware publishes its version here and sets bit 0 of the
+ * matching control word to say it has done so.
+ */
+#define ICE_ADMIN_IPUE_VERSION 0x00
+#define ICE_ADMIN_IFPP_VERSION 0x08
+#define ICE_ADMIN_IPUE_CTRL 0x14
+#define ICE_ADMIN_IFPP_CTRL 0x18
+#define ICE_ADMIN_VERSION_UPDATED (1u << 0)
+
+/* Administration RAM byte offset to an index into the packet engine window. */
+#define ICE_ADMIN_WORD(off) ((PE_ICE_SCRATCH_RAM + (off)) / 4)
+
+/* Version word: major in bits 11..8, minor in 7..4, patch level in 3..0. */
+#define ICE_FW_VERSION_FIELD 0xf
+#define ICE_FW_VERSION_MAJOR_SH 8
+#define ICE_FW_VERSION_MINOR_SH 4
+#define ICE_FW_VERSION_PATCH_SH 0
+
+#define ICE_FW_VERSION(maj, min, patch) \
+ ((((maj) & ICE_FW_VERSION_FIELD) << ICE_FW_VERSION_MAJOR_SH) | \
+ (((min) & ICE_FW_VERSION_FIELD) << ICE_FW_VERSION_MINOR_SH) | \
+ (((patch) & ICE_FW_VERSION_FIELD) << ICE_FW_VERSION_PATCH_SH))
+
+/*
+ * The version reported to the guest, which compares it against the version of
+ * the classification firmware image it has just downloaded. The two have to
+ * agree: report anything else and classification init fails outright. Update
+ * this when the guest ships a different classification firmware.
+ */
+#define ICE_FIRMWARE_VERSION ICE_FW_VERSION(3, 3, 1)
+
+static void mac_mdio_execute(HCPMac *m)
+{
+ uint32_t cmd = m->mdio_command;
+ uint8_t reg = m->mdio_regaddr ? (m->mdio_regaddr & 0x1f)
+ : (cmd & 0x1f);
+
+ if (cmd & MDIO_COMMAND_READ_FLAG) {
+ m->mdio_data = m->mii_regs[reg];
+ } else {
+ m->mii_regs[reg] = m->mdio_data & 0xffff;
+ }
+}
+
+static uint64_t mac_block_read(HCPMac *m, hwaddr off)
+{
+ switch (off) {
+ case MAC_R_REVISION: return MAC_REVISION_VAL;
+ case MAC_R_COMMAND_CONFIG: return m->command_config;
+ case MAC_R_MAC_0: return m->mac_addr_lo;
+ case MAC_R_MAC_1: return m->mac_addr_hi;
+ case MAC_R_FRM_LENGTH: return m->frm_length;
+ case MAC_R_RX_FIFO_SECTIONS: return m->rx_fifo_sections;
+ case MAC_R_TX_FIFO_SECTIONS: return m->tx_fifo_sections;
+ case MAC_R_MDIO_CFG_STATUS: return 0;
+ case MAC_R_MDIO_COMMAND: return m->mdio_command;
+ case MAC_R_MDIO_DATA: return m->mdio_data;
+ case MAC_R_MDIO_REGADDR: return m->mdio_regaddr;
+ case MAC_R_STATUS: return 0;
+ case MAC_R_IF_MODE: return m->if_mode;
+ case MAC_R_PCS_STATUS: return 0x0084;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: MAC reg read offset 0x%" HWADDR_PRIx "\n",
+ off);
+ return 0;
+ }
+}
+
+static void mac_block_write(HCPMac *m, hwaddr off, uint64_t val)
+{
+ switch (off) {
+ case MAC_R_COMMAND_CONFIG:
+ m->command_config = val;
+ break;
+ case MAC_R_MAC_0:
+ m->mac_addr_lo = val;
+ break;
+ case MAC_R_MAC_1:
+ m->mac_addr_hi = val;
+ break;
+ case MAC_R_FRM_LENGTH:
+ m->frm_length = val;
+ break;
+ case MAC_R_RX_FIFO_SECTIONS:
+ m->rx_fifo_sections = val;
+ break;
+ case MAC_R_TX_FIFO_SECTIONS:
+ m->tx_fifo_sections = val;
+ break;
+ case MAC_R_IF_MODE:
+ m->if_mode = val;
+ break;
+ case MAC_R_MDIO_COMMAND:
+ m->mdio_command = val;
+ mac_mdio_execute(m);
+ break;
+ case MAC_R_MDIO_DATA:
+ m->mdio_data = val & 0xffff;
+ break;
+ case MAC_R_MDIO_REGADDR:
+ m->mdio_regaddr = val;
+ break;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: MAC reg write offset 0x%" HWADDR_PRIx
+ " = 0x%" PRIx64 "\n", off, val);
+ break;
+ }
+}
+
+static uint64_t shim_global_read(AXIADOHCPState *s, hwaddr addr)
+{
+ if (addr >= SHIM_REG_FIFO_BASE &&
+ addr < SHIM_REG_FIFO_BASE + HCP_NUM_MACS * 4) {
+ return (1U << FIFO_BIT_TX_EMPTY) | (1U << FIFO_BIT_RX_EMPTY);
+ }
+ if (addr >= SHIM_REG_STATS_BASE &&
+ addr < SHIM_REG_STATS_BASE + HCP_NUM_MACS * 4) {
+ return 0;
+ }
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: shim global read 0x%" HWADDR_PRIx "\n", addr);
+ return 0;
+}
+
+static void shim_global_write(AXIADOHCPState *s, hwaddr addr, uint64_t val)
+{
+ if (addr >= SHIM_REG_FIFO_BASE &&
+ addr < SHIM_REG_FIFO_BASE + HCP_NUM_MACS * 4) {
+ return;
+ }
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: shim global write 0x%" HWADDR_PRIx
+ " = 0x%" PRIx64 "\n", addr, val);
+}
+
+static int shim_mac_index(hwaddr addr, hwaddr *off_out)
+{
+ if (addr < HCP_SHIM_MAC_BASE_OFFSET) {
+ return -1;
+ }
+ hwaddr rel = addr - HCP_SHIM_MAC_BASE_OFFSET;
+ int idx = rel / HCP_SHIM_MAC_STRIDE;
+ if (idx >= HCP_NUM_MACS) {
+ return -1;
+ }
+ *off_out = rel % HCP_SHIM_MAC_STRIDE;
+ return idx;
+}
+
+static uint64_t axiado_shim_read(void *opaque, hwaddr addr, unsigned size)
+{
+ AXIADOHCPState *s = AXIADO_HCP(opaque);
+ hwaddr off;
+ int idx = shim_mac_index(addr, &off);
+
+ if (idx >= 0) {
+ return mac_block_read(&s->macs[idx], off);
+ }
+ return shim_global_read(s, addr);
+}
+
+static void axiado_shim_write(void *opaque, hwaddr addr, uint64_t val,
+ unsigned size)
+{
+ AXIADOHCPState *s = AXIADO_HCP(opaque);
+ hwaddr off;
+ int idx = shim_mac_index(addr, &off);
+
+ if (idx >= 0) {
+ mac_block_write(&s->macs[idx], off, val);
+ return;
+ }
+ shim_global_write(s, addr, val);
+}
+
+static const MemoryRegionOps shim_ops = {
+ .read = axiado_shim_read,
+ .write = axiado_shim_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .valid.min_access_size = 4,
+ .valid.max_access_size = 4,
+ .impl.min_access_size = 4,
+ .impl.max_access_size = 4,
+};
+
+/*
+ * EIP-197 region, SafeXcel register layout
+ */
+
+#define XDR_BASE_LO 0x00
+#define XDR_BASE_HI 0x04
+#define XDR_SIZE 0x18
+#define XDR_DESC_SIZE 0x1c
+#define XDR_CFG 0x20
+#define XDR_DMA_CFG 0x24
+#define XDR_THRESH 0x28
+#define XDR_PREP_COUNT 0x2c
+#define XDR_PROC_COUNT 0x30
+#define XDR_PREP_PNTR 0x34
+#define XDR_PROC_PNTR 0x38
+#define XDR_STAT 0x3c
+
+#define XDR_STAT_PROC_THRESH_IRQ 0x10
+
+/*
+ * Bits 16..27 of xDR_STAT report the descriptor FIFO size in 32-bit words.
+ * The guest reads it at ring init to check that the descriptor size, fetch
+ * size and threshold it wants to program all fit. 256 words leaves headroom
+ * well past any combination the guest asks for.
+ */
+#define XDR_STAT_FIFO_SIZE_WORDS 256u
+#define XDR_STAT_FIFO_SIZE_BITS ((XDR_STAT_FIFO_SIZE_WORDS & 0xFFFu) << 16)
+
+#define EIP_HIA_XDR_BASE 0x80000
+#define EIP_HIA_XDR_STRIDE 0x1000
+#define EIP_HIA_RDR_OFF 0x800
+
+#define EIP_HIA_AIC_R_RING0_BASE 0x9E000
+#define EIP_AIC_R_CDR_ENABLE_CTRL 0x008
+#define EIP_AIC_R_CDR_ENABLED 0x010
+#define EIP_AIC_R_CDR_ENABLE_CLR 0x014
+#define EIP_AIC_R_RDR_ENABLE_CTRL 0x808
+#define EIP_AIC_R_RDR_ENABLED 0x810
+#define EIP_AIC_R_RDR_ENABLE_CLR 0x814
+
+#define EIP_HIA_AIC_G_BASE 0x9F000
+#define EIP_AIC_G_ENABLE_CTRL 0x808
+#define EIP_AIC_G_ENABLED 0x810
+#define EIP_AIC_G_ENABLE_CLR 0x814
+
+/* SafeXcel command descriptor - field offsets used here (little-endian). */
+#define CMD_DESC_FLAGS_OFF 0 /* particle_size:17, ..., flags */
+#define CMD_DESC_DATA_LO_OFF 8
+#define CMD_DESC_DATA_HI_OFF 12
+#define CMD_DESC_APP_ID_OFF 28 /* EIP-96 input token, app_id << 9 */
+
+/* SafeXcel result descriptor word-0 bit layout */
+#define RES_DESC_FLAGS_OFF 0
+#define RES_DESC_PARTICLE_MASK 0x1FFFFu
+#define RES_DESC_LAST_SEG_BIT (1u << 22)
+#define RES_DESC_FIRST_SEG_BIT (1u << 23)
+#define RES_DESC_DATA_LO_OFF 8
+#define RES_DESC_DATA_HI_OFF 12
+
+#define MAX_PKT_LEN 9600
+
+/*
+ * DESC_SIZE register encoding:
+ * bits 0..7 = dscr_word_count (data payload size, words)
+ * bits 16..23 = dscr_offs_word_count (per-slot stride, words)
+ * bits 29..31 = flags (atp_to_token / atp / 64-bit)
+ * The stride between descriptor slots in ring memory is the OFFSET field, not
+ * the payload size. Returns 0 if the guest has not programmed the register,
+ * which every caller checks before using it as a divisor.
+ */
+static uint32_t xdr_stride_bytes(HCPxDR *x)
+{
+ uint32_t offs_words = (x->desc_size >> 16) & 0xFFu;
+ return offs_words * 4u;
+}
+
+/*
+ * Drive the per-ring IRQ line from (STAT & AIC mask). The line is
+ * level-triggered, so it must be recomputed - and deasserted - as soon as the
+ * guest acks a STAT bit by writing it back, otherwise the interrupt
+ * controller re-fires forever.
+ */
+static void hcp_update_ring_irq(AXIADOHCPState *s, int r)
+{
+ bool level = false;
+
+ if (s->rings[r].aic_rdr_mask &&
+ (s->rings[r].rdr.stat & XDR_STAT_PROC_THRESH_IRQ)) {
+ level = true;
+ }
+ if (s->rings[r].aic_cdr_mask &&
+ (s->rings[r].cdr.stat & XDR_STAT_PROC_THRESH_IRQ)) {
+ level = true;
+ }
+ qemu_set_irq(s->irqs[HCP_IRQ_RING_BASE + r], level);
+}
+
+static int app_id_to_mac_index(AXIADOHCPState *s, uint16_t app_id)
+{
+ for (int i = 0; i < HCP_NUM_MACS; i++) {
+ if (s->macs[i].app_id == app_id) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/*
+ * Process all newly-prepared CDR descriptors for ring r. For each: DMA-read
+ * the command descriptor, extract the routing app_id, DMA-read the packet,
+ * and emit it on the matching MAC's NIC backend.
+ *
+ * No result descriptor is posted for a transmit. The engine and the guest
+ * share one RDR per ring for both transmit completions and receive results;
+ * the guest tracks transmit buffer reuse from the CDR consumed count alone,
+ * so writing transmit completions would only corrupt receive slots.
+ */
+static void hcp_process_tx_ring(AXIADOHCPState *s, int r)
+{
+ HCPxDR *cdr = &s->rings[r].cdr;
+ uint32_t stride = xdr_stride_bytes(cdr);
+
+ if (stride == 0 || cdr->size == 0 || cdr->base == 0) {
+ return;
+ }
+ uint32_t ring_descs = cdr->size / stride;
+ if (ring_descs == 0) {
+ return;
+ }
+
+ while (cdr->proc_count < cdr->prep_count) {
+ uint32_t idx = (cdr->proc_count / stride) % ring_descs;
+ hwaddr desc_addr = cdr->base + (hwaddr)idx * stride;
+
+ uint8_t desc[64];
+ size_t read_bytes = MIN(stride, (uint32_t)sizeof(desc));
+ if (dma_memory_read(&address_space_memory, desc_addr, desc, read_bytes,
+ MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "axiado-hcp: CDR DMA read failed at 0x%" HWADDR_PRIx
+ "\n", desc_addr);
+ break;
+ }
+
+ uint32_t flags = ldl_le_p(&desc[CMD_DESC_FLAGS_OFF]);
+ uint32_t pkt_len = flags & 0x1FFFFu;
+ uint64_t data_addr =
+ (uint64_t)ldl_le_p(&desc[CMD_DESC_DATA_LO_OFF])
+ | ((uint64_t)ldl_le_p(&desc[CMD_DESC_DATA_HI_OFF]) << 32);
+ /*
+ * The routing app_id is not a plain descriptor field: it lives in the
+ * EIP-96 input token as (app_id << 9).
+ */
+ uint16_t raw_token = lduw_le_p(&desc[CMD_DESC_APP_ID_OFF]);
+ uint16_t app_id = (raw_token >> 9) & 0x7Fu;
+
+ int mac_idx = app_id_to_mac_index(s, app_id);
+ if (mac_idx < 0 || pkt_len == 0 || pkt_len > MAX_PKT_LEN) {
+ /*
+ * An all-zero descriptor is an unused slot, not an error. Only
+ * report slots that carry data we could not route.
+ */
+ if (pkt_len != 0 || raw_token != 0) {
+ trace_axiado_hcp_tx_unroutable(r, app_id, pkt_len, data_addr);
+ }
+ } else {
+ g_autofree uint8_t *pkt = g_malloc(pkt_len);
+ if (dma_memory_read(&address_space_memory, data_addr, pkt, pkt_len,
+ MEMTXATTRS_UNSPECIFIED) == MEMTX_OK) {
+ HCPMac *mac = &s->macs[mac_idx];
+
+ trace_axiado_hcp_tx_packet(r, app_id, mac_idx, pkt_len);
+ if (mac->nic) {
+ qemu_send_packet(qemu_get_queue(mac->nic), pkt, pkt_len);
+ }
+ } else {
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "axiado-hcp: packet DMA read failed at 0x%"
+ PRIx64 "\n", data_addr);
+ }
+ }
+
+ cdr->proc_count += stride;
+ cdr->proc_pntr = (cdr->proc_pntr + stride) % cdr->size;
+ }
+}
+
+static uint64_t eip_xdr_read(HCPxDR *x, hwaddr off)
+{
+ switch (off) {
+ case XDR_BASE_LO: return x->base & 0xFFFFFFFFu;
+ case XDR_BASE_HI: return (x->base >> 32) & 0xFFFFFFFFu;
+ case XDR_SIZE: return x->size;
+ case XDR_DESC_SIZE: return x->desc_size;
+ case XDR_CFG: return x->cfg;
+ case XDR_DMA_CFG: return x->dma_cfg;
+ case XDR_THRESH: return x->thresh;
+ case XDR_PREP_COUNT:
+ /*
+ * This register is asymmetric. A write means "N more descriptors have
+ * been prepared", but a read returns the ring's current fill level:
+ * the guest recovers a descriptor count as
+ *
+ * ((reg >> 2) & 0x3fffff) / dscr_offs_word_count
+ *
+ * and uses it to decide how much room is left in the ring. Reporting
+ * the cumulative prepared count instead would make that fill level
+ * grow without bound, and the guest would eventually see the ring as
+ * permanently full and stop transmitting for good.
+ *
+ * Both counters are byte cursors and the guest's >> 2 undoes the << 2
+ * applied on write, so the outstanding byte count is the register
+ * value directly. Transmit descriptors are consumed synchronously, so
+ * this reads back as empty once a transmit has been processed.
+ */
+ return x->prep_count > x->proc_count
+ ? x->prep_count - x->proc_count : 0;
+ case XDR_PROC_COUNT: return x->proc_count;
+ case XDR_PREP_PNTR: return x->prep_pntr;
+ case XDR_PROC_PNTR: return x->proc_pntr;
+ case XDR_STAT: return x->stat | XDR_STAT_FIFO_SIZE_BITS;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip xdr read offset 0x%" HWADDR_PRIx "\n",
+ off);
+ return 0;
+ }
+}
+
+static int eip_xdr_ring(hwaddr addr, hwaddr *off_out, bool *is_rdr_out)
+{
+ if (addr < EIP_HIA_XDR_BASE ||
+ addr >= EIP_HIA_XDR_BASE + HCP_NUM_RINGS * EIP_HIA_XDR_STRIDE) {
+ return -1;
+ }
+ hwaddr rel = addr - EIP_HIA_XDR_BASE;
+ int r = rel / EIP_HIA_XDR_STRIDE;
+ hwaddr intra = rel % EIP_HIA_XDR_STRIDE;
+ *is_rdr_out = (intra >= EIP_HIA_RDR_OFF);
+ *off_out = (*is_rdr_out ? (intra - EIP_HIA_RDR_OFF) : intra) & 0x3F;
+ return r;
+}
+
+/*
+ * xDR write. Returns true if this write was a CDR PREP_COUNT bump that needs
+ * follow-up TX processing (caller invokes hcp_process_tx_ring afterward).
+ */
+static bool eip_xdr_write(HCPxDR *x, hwaddr off, uint64_t val)
+{
+ switch (off) {
+ case XDR_BASE_LO:
+ x->base = (x->base & ~0xFFFFFFFFULL) | (val & 0xFFFFFFFFu);
+ return false;
+ case XDR_BASE_HI:
+ x->base = (x->base & 0xFFFFFFFFULL) | ((val & 0xFFFFFFFFu) << 32);
+ return false;
+ case XDR_SIZE:
+ x->size = val;
+ return false;
+ case XDR_DESC_SIZE:
+ x->desc_size = val;
+ return false;
+ case XDR_CFG:
+ x->cfg = val;
+ return false;
+ case XDR_DMA_CFG:
+ x->dma_cfg = val;
+ return false;
+ case XDR_THRESH:
+ x->thresh = val;
+ return false;
+ case XDR_PREP_COUNT: {
+ /*
+ * Write layout:
+ * bit 31 = clear-count flag, reset rather than add
+ * bits 2.. = added descriptor word count, i.e. a byte count, since
+ * the per-slot stride is dscr_offs_word_count * 4
+ * The guest issues a clear at ring init; treating that as an increment
+ * would leave the ring permanently reporting millions of pending
+ * descriptors.
+ */
+ uint32_t add = val & 0x7FFFFFFFu;
+ if (val & 0x80000000u) {
+ x->prep_count = add;
+ x->prep_pntr = (x->size > 0) ? (add % x->size) : 0;
+ } else {
+ x->prep_count += add;
+ if (x->size > 0) {
+ x->prep_pntr = (x->prep_pntr + add) % x->size;
+ }
+ }
+ return true;
+ }
+ case XDR_PROC_COUNT:
+ /*
+ * In ownership-word mode this write only acknowledges results the
+ * guest has already read; the ownership word is the real handshake.
+ * It must not clobber proc_count, which is the engine-side slot
+ * cursor and only advances when a slot is actually consumed or
+ * produced.
+ */
+ return false;
+ case XDR_PREP_PNTR:
+ x->prep_pntr = val;
+ return false;
+ case XDR_PROC_PNTR:
+ x->proc_pntr = val;
+ return false;
+ case XDR_STAT:
+ if (val & XDR_STAT_PROC_THRESH_IRQ) {
+ x->stat &= ~XDR_STAT_PROC_THRESH_IRQ;
+ }
+ return false;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip xdr write offset 0x%" HWADDR_PRIx
+ " = 0x%" PRIx64 "\n", off, val);
+ return false;
+ }
+}
+
+/*
+ * Complete the classification firmware version handshake.
+ *
+ * Having downloaded the firmware, the guest starts a micro-engine in debug
+ * mode at a fixed program counter and polls the administration RAM for a
+ * version word that the running microcode would have written. No microcode
+ * executes here, so the model publishes the version itself: a write to either
+ * engine's control register makes that engine's version word and its updated
+ * flag appear, and the guest's poll succeeds on its first read.
+ *
+ * Without this the guest cannot complete classification init, and the only
+ * other way past it is to disable the check in the guest - which a device
+ * model should never require.
+ */
+static void hcp_ice_publish_fw_version(AXIADOHCPState *s, hwaddr ctrl_off)
+{
+ unsigned version, ctrl;
+
+ QEMU_BUILD_BUG_ON(ICE_ADMIN_WORD(ICE_ADMIN_IFPP_CTRL) >= HCP_EIP_PE_REGS);
+
+ if (ctrl_off == PE_ICE_FPP_CTRL) {
+ version = ICE_ADMIN_WORD(ICE_ADMIN_IFPP_VERSION);
+ ctrl = ICE_ADMIN_WORD(ICE_ADMIN_IFPP_CTRL);
+ } else {
+ version = ICE_ADMIN_WORD(ICE_ADMIN_IPUE_VERSION);
+ ctrl = ICE_ADMIN_WORD(ICE_ADMIN_IPUE_CTRL);
+ }
+
+ s->pe_regs[version] = ICE_FIRMWARE_VERSION;
+ s->pe_regs[ctrl] |= ICE_ADMIN_VERSION_UPDATED;
+}
+
+/*
+ * Locate the backing word for a register inside one of the windows the guest
+ * writes and reads back, or NULL if this address is not in any of them.
+ * Registers outside the windows are either write-only or read-only status,
+ * and need no storage.
+ */
+static uint32_t *eip_reg_slot(AXIADOHCPState *s, hwaddr addr)
+{
+ struct {
+ hwaddr base;
+ unsigned regs;
+ uint32_t *store;
+ } const windows[] = {
+ { HCP_EIP_FLUE_FHT_BASE, HCP_EIP_FLUE_FHT_REGS, s->flue_fht },
+ { HCP_EIP_PE_BASE, HCP_EIP_PE_REGS, s->pe_regs },
+ { HCP_EIP_TRC_RAM_BASE, HCP_EIP_TRC_RAM_REGS, s->trc_ram },
+ { HCP_EIP_CLS_BASE, HCP_EIP_CLS_REGS, s->cls_regs },
+ { HCP_EIP_FLOW_BASE, HCP_EIP_FLOW_REGS, s->flow_regs },
+ { HCP_EIP_CACHE_BASE, HCP_EIP_CACHE_REGS, s->cache_regs },
+ };
+
+ for (int i = 0; i < ARRAY_SIZE(windows); i++) {
+ hwaddr base = windows[i].base;
+
+ if (addr >= base && addr < base + windows[i].regs * 4) {
+ return &windows[i].store[(addr - base) / 4];
+ }
+ }
+ return NULL;
+}
+
+static int eip_aic_r_ring(hwaddr addr, hwaddr *off_out)
+{
+ hwaddr lo = EIP_HIA_AIC_R_RING0_BASE -
+ (HCP_NUM_RINGS - 1) * EIP_HIA_XDR_STRIDE;
+ if (addr < lo || addr > EIP_HIA_AIC_R_RING0_BASE + 0xFFF) {
+ return -1;
+ }
+ hwaddr block_base = addr & ~0xFFFULL;
+ int r = (EIP_HIA_AIC_R_RING0_BASE - block_base) / EIP_HIA_XDR_STRIDE;
+ if (r < 0 || r >= HCP_NUM_RINGS) {
+ return -1;
+ }
+ *off_out = addr & 0xFFF;
+ return r;
+}
+
+static uint64_t axiado_eip197_read(void *opaque, hwaddr addr, unsigned size)
+{
+ AXIADOHCPState *s = AXIADO_HCP(opaque);
+ uint32_t *slot;
+ hwaddr off;
+ bool is_rdr;
+ int r;
+
+ /*
+ * EIP-2xx identification registers, matched before the AIC range dispatch
+ * so they take precedence over it. Each sub-block has a version register
+ * whose low 16 bits hold a fixed signature and whose high 16 bits encode
+ * the revision, plus one or two options registers describing the
+ * capabilities the guest may use.
+ */
+ switch (addr) {
+ /*
+ * EIP-202 host interface adapter, signature 0x35ca.
+ * 0x9fff0 = OPTIONS2: lookaside interfaces (0..3), inline interfaces
+ * (4..7), AXI write channels (16..19), read clusters (20..27)
+ * 0x9fff4 = MST_CTRL, answered from the scratchpad
+ * 0x9fff8 = OPTIONS: rings (0..3), packet engines (4..8), command and
+ * result FIFO sizes (9..14), host interface (16..19), DMA
+ * length (20..24), host data width (25..27), target alignment
+ * (28..30), 64-bit addressing (31)
+ * 0x9fffc = VERSION
+ */
+ case 0x9FFF0:
+ return 0x000400CAu;
+ case 0x9FFF8:
+ /* Four rings, one packet engine, 64-bit addressing. */
+ return 0x80000014u;
+ case 0x9FFFC:
+ return 0x010135CAu;
+
+ /*
+ * EIP-207 classification sub-block, signature 0x30cf.
+ * 0xf7ff0 = OPTIONS2, unused by the minimum init path
+ * 0xf7ff8 = OPTIONS: bits 1..2 number of cache sets, bits 28..30
+ * number of lookup tables. One of each is the minimum the
+ * guest accepts; all other capability bits stay clear.
+ * 0xf7ffc = VERSION
+ */
+ case 0xF7FF0:
+ return 0;
+ case 0xF7FF8:
+ return (1u << 28) | (1u << 1);
+ case 0xF7FFC:
+ return 0x010130CFu;
+
+ /* EIP-207s flow look-up engine, same 0x30cf signature. */
+ case 0x01FF8:
+ return 0;
+ case 0x01FFC:
+ return 0x010130CFu;
+
+ /*
+ * EIP-96 packet engine, signature 0x9f60. Both the PE + 0x13fc and the
+ * PE + 0xfffc placements are answered, as the guest probes either
+ * depending on the variant it believes it is talking to.
+ */
+ case 0xA13FC:
+ case 0xA0FFC:
+ return 0x01019F60u;
+ case 0xA13F8:
+ case 0xA0FF0:
+ case 0xA0FF8:
+ return 0;
+
+ /* EIP-201 interrupt controller / HIA xDR block, signature 0x36c9. */
+ case 0x8FFF0:
+ case 0x8FFFC:
+ return 0x010136C9u;
+ case 0x8FFF8:
+ return 0;
+
+ /* EIP-197 global, signature 0x3ac5. */
+ case 0xFFFF0:
+ case 0xFFFFC:
+ return 0x01013AC5u;
+ case 0xFFFF8:
+ return 0;
+ }
+
+ r = eip_xdr_ring(addr, &off, &is_rdr);
+ if (r >= 0) {
+ HCPxDR *x = is_rdr ? &s->rings[r].rdr : &s->rings[r].cdr;
+ return eip_xdr_read(x, off);
+ }
+
+ r = eip_aic_r_ring(addr, &off);
+ if (r >= 0) {
+ HCPRing *ring = &s->rings[r];
+ switch (off) {
+ case EIP_AIC_R_CDR_ENABLE_CTRL: return ring->aic_cdr_mask;
+ case EIP_AIC_R_CDR_ENABLE_CLR: return ring->aic_cdr_mask;
+ case EIP_AIC_R_CDR_ENABLED: return 0;
+ case EIP_AIC_R_RDR_ENABLE_CTRL: return ring->aic_rdr_mask;
+ case EIP_AIC_R_RDR_ENABLE_CLR: return ring->aic_rdr_mask;
+ case EIP_AIC_R_RDR_ENABLED: return 0;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip aic_r[%d] read 0x%" HWADDR_PRIx
"\n",
+ r, off);
+ return 0;
+ }
+ }
+
+ if (addr >= EIP_HIA_AIC_G_BASE && addr < EIP_HIA_AIC_G_BASE + 0x1000) {
+ hwaddr g_off = addr - EIP_HIA_AIC_G_BASE;
+ switch (g_off) {
+ case EIP_AIC_G_ENABLE_CTRL: return s->aic_g_mask;
+ case EIP_AIC_G_ENABLE_CLR: return s->aic_g_mask;
+ case EIP_AIC_G_ENABLED: return 0;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip aic_g read 0x%" HWADDR_PRIx "\n",
+ g_off);
+ return 0;
+ }
+ }
+
+ slot = eip_reg_slot(s, addr);
+ if (slot) {
+ return *slot;
+ }
+
+ /* Write-only or read-only-status register: reads as zero. */
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip197 read 0x%" HWADDR_PRIx "\n", addr);
+ return 0;
+}
+
+static void axiado_eip197_write(void *opaque, hwaddr addr, uint64_t val,
+ unsigned size)
+{
+ AXIADOHCPState *s = AXIADO_HCP(opaque);
+ uint32_t *slot;
+ hwaddr off;
+ bool is_rdr;
+ int r;
+
+ r = eip_xdr_ring(addr, &off, &is_rdr);
+ if (r >= 0) {
+ HCPxDR *x = is_rdr ? &s->rings[r].rdr : &s->rings[r].cdr;
+ bool kick = eip_xdr_write(x, off, val);
+
+ /*
+ * A STAT write that cleared the threshold bit must immediately
+ * deassert the level-triggered ring interrupt, so the line is
+ * reconciled after any write to that register.
+ */
+ if (off == XDR_STAT) {
+ hcp_update_ring_irq(s, r);
+ }
+ if (kick) {
+ if (!is_rdr) {
+ /* Transmit command descriptors were posted on this ring. */
+ hcp_process_tx_ring(s, r);
+ } else {
+ /*
+ * Fresh receive buffers were posted, so retry anything that
+ * was back-pressured earlier.
+ */
+ for (int i = 0; i < HCP_NUM_MACS; i++) {
+ NICState *nic = s->macs[i].nic;
+
+ if (nic) {
+ qemu_flush_queued_packets(qemu_get_queue(nic));
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ r = eip_aic_r_ring(addr, &off);
+ if (r >= 0) {
+ HCPRing *ring = &s->rings[r];
+ switch (off) {
+ case EIP_AIC_R_CDR_ENABLE_CTRL:
+ ring->aic_cdr_mask |= val;
+ hcp_update_ring_irq(s, r);
+ return;
+ case EIP_AIC_R_CDR_ENABLE_CLR:
+ ring->aic_cdr_mask &= ~val;
+ hcp_update_ring_irq(s, r);
+ return;
+ case EIP_AIC_R_RDR_ENABLE_CTRL:
+ ring->aic_rdr_mask |= val;
+ hcp_update_ring_irq(s, r);
+ return;
+ case EIP_AIC_R_RDR_ENABLE_CLR:
+ ring->aic_rdr_mask &= ~val;
+ hcp_update_ring_irq(s, r);
+ return;
+ case EIP_AIC_R_CDR_ENABLED:
+ case EIP_AIC_R_RDR_ENABLED:
+ return;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip aic_r[%d] write 0x%" HWADDR_PRIx
+ " = 0x%" PRIx64 "\n", r, off, val);
+ return;
+ }
+ }
+
+ if (addr >= EIP_HIA_AIC_G_BASE && addr < EIP_HIA_AIC_G_BASE + 0x1000) {
+ hwaddr g_off = addr - EIP_HIA_AIC_G_BASE;
+ switch (g_off) {
+ case EIP_AIC_G_ENABLE_CTRL:
+ s->aic_g_mask |= val;
+ return;
+ case EIP_AIC_G_ENABLE_CLR:
+ s->aic_g_mask &= ~val;
+ return;
+ case EIP_AIC_G_ENABLED:
+ return;
+ default:
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip aic_g write 0x%" HWADDR_PRIx
+ " = 0x%" PRIx64 "\n", g_off, val);
+ return;
+ }
+ }
+
+ slot = eip_reg_slot(s, addr);
+ if (slot) {
+ *slot = val;
+
+ if (addr == HCP_EIP_PE_BASE + PE_ICE_FPP_CTRL ||
+ addr == HCP_EIP_PE_BASE + PE_ICE_PUE_CTRL) {
+ hcp_ice_publish_fw_version(s, addr - HCP_EIP_PE_BASE);
+ }
+ return;
+ }
+
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: eip197 write 0x%" HWADDR_PRIx " = 0x%" PRIx64
+ "\n", addr, val);
+}
+
+static const MemoryRegionOps eip197_ops = {
+ .read = axiado_eip197_read,
+ .write = axiado_eip197_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .valid.min_access_size = 4,
+ .valid.max_access_size = 4,
+ .impl.min_access_size = 4,
+ .impl.max_access_size = 4,
+};
+
+/*
+ * PHY CSR region
+ */
+
+/*
+ * PCS/SerDes configuration. The guest programs two lane blocks here, at
+ * 0x30000 and 0x34000, and never reads any of it back: the link is always up
+ * in this model, so none of the calibration or lock status it would poll for
+ * is needed. Writes are therefore accepted and discarded, and reads return
+ * zero. Both are logged, so a guest that does start reading shows up as
+ * LOG_UNIMP output rather than as silent wrong behaviour.
+ */
+static uint64_t axiado_phy_csr_read(void *opaque, hwaddr addr, unsigned size)
+{
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: phy csr read 0x%" HWADDR_PRIx "\n", addr);
+ return 0;
+}
+
+static void axiado_phy_csr_write(void *opaque, hwaddr addr, uint64_t val,
+ unsigned size)
+{
+ qemu_log_mask(LOG_UNIMP,
+ "axiado-hcp: phy csr write 0x%" HWADDR_PRIx " = 0x%" PRIx64
+ "\n", addr, val);
+}
+
+static const MemoryRegionOps phy_csr_ops = {
+ .read = axiado_phy_csr_read,
+ .write = axiado_phy_csr_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .valid.min_access_size = 4,
+ .valid.max_access_size = 4,
+ .impl.min_access_size = 4,
+ .impl.max_access_size = 4,
+};
+
+/*
+ * NIC backends and receive datapath
+ */
+
+/*
+ * 64-bit result descriptor plus EIP-96 output token, as the guest reads them
+ * back with ownership words enabled and a host data width of zero:
+ *
+ * word 0 : control word (segment byte count | FIRST_SEG | LAST_SEG)
+ * word 1 : extended length (not read by the driver)
+ * word 2/3: destination packet address (lo/hi) -> RES_DESC_DATA_LO/HI_OFF
+ * word 4..: EIP-96 output token
+ * token word 0, byte 16: packet byte count and error bits
+ * token word 2, byte 24: app_id << 9
+ * last word of the slot (stride-4): ownership word = 0xAAAAAAAA
+ *
+ * A packet is ready purely by virtue of the ownership word; the guest takes
+ * the length and app_id from the output token, not from a fixed result
+ * descriptor field.
+ */
+#define RES_TOKEN_WORD_OFF 4
+/* Byte offsets of the length/error word and the app_id word in the token. */
+#define RES_TOKEN_HDR_OFF (RES_TOKEN_WORD_OFF * 4)
+#define RES_TOKEN_APP_ID_OFF ((RES_TOKEN_WORD_OFF + 2) * 4)
+#define RES_TOKEN_LEN_MASK 0x1FFFFu
+#define RES_OWNERSHIP_PATTERN 0xAAAAAAAAu
+
+/*
+ * Ring that carries received packets. Ring 0 is the default interface and
+ * posts no receive buffers; rings 1 to 3 run in continuous-scatter mode,
+ * where each result descriptor slot is pre-loaded with the address of a
+ * guest receive buffer at offsets +8 and +12. Any of them will do, and all
+ * five MACs are demultiplexed from the app_id, so one ring serves them all.
+ */
+#define HCP_RX_RING 1
+
+/* Map a NetClientState back to the MAC index that owns it. */
+static int hcp_find_mac_for_nc(AXIADOHCPState *s, NetClientState *nc)
+{
+ for (int i = 0; i < HCP_NUM_MACS; i++) {
+ if (s->macs[i].nic && qemu_get_queue(s->macs[i].nic) == nc) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+static bool hcp_rdr_has_slot(HCPxDR *rdr)
+{
+ uint32_t stride = xdr_stride_bytes(rdr);
+ if (stride == 0 || rdr->size == 0 || rdr->base == 0) {
+ return false;
+ }
+ if (rdr->size / stride == 0) {
+ return false;
+ }
+ return rdr->prep_count > rdr->proc_count;
+}
+
+/*
+ * Deliver one inbound packet through ring r's result descriptor ring.
+ *
+ * 1. Read the current slot. The guest pre-loaded it with the address of a
+ * receive buffer at +8 and +12.
+ * 2. DMA-write the packet into that buffer. The buffer lives elsewhere in
+ * guest RAM, not in the descriptor.
+ * 3. Rebuild the slot as a result descriptor plus output token, and stamp
+ * the ownership word last.
+ * 4. Advance the receive cursor and latch the threshold interrupt.
+ *
+ * Returns false without consuming the packet if no slot is available, so the
+ * caller can apply back-pressure.
+ */
+static bool hcp_rdr_deliver(AXIADOHCPState *s, int r, int mac_idx,
+ const uint8_t *buf, size_t len)
+{
+ HCPxDR *rdr = &s->rings[r].rdr;
+
+ uint32_t rdr_stride = xdr_stride_bytes(rdr);
+ if (rdr_stride == 0 || rdr->size == 0 || rdr->base == 0) {
+ return false;
+ }
+ uint32_t rdr_descs = rdr->size / rdr_stride;
+ if (rdr_descs == 0) {
+ return false;
+ }
+
+ /*
+ * Step 1: locate the slot and read the receive buffer the guest posted in
+ * it. The command ring is the transmit ring and takes no part in receive.
+ */
+ uint32_t rdr_idx = s->rings[r].rx_idx % rdr_descs;
+ hwaddr rdr_desc_addr = rdr->base + (hwaddr)rdr_idx * rdr_stride;
+
+ uint8_t res_desc[256];
+ size_t rdr_io = MIN(rdr_stride, (uint32_t)sizeof(res_desc));
+ if (dma_memory_read(&address_space_memory, rdr_desc_addr, res_desc, rdr_io,
+ MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
+ return false;
+ }
+
+ uint32_t orig_own = (rdr_io >= rdr_stride && rdr_stride >= 4)
+ ? ldl_le_p(&res_desc[rdr_stride - 4]) : 0;
+ uint64_t buf_addr =
+ (uint64_t)ldl_le_p(&res_desc[RES_DESC_DATA_LO_OFF])
+ | ((uint64_t)ldl_le_p(&res_desc[RES_DESC_DATA_HI_OFF]) << 32);
+
+ if (buf_addr == 0) {
+ /* No buffer posted for this slot yet. */
+ trace_axiado_hcp_rx_backpressure(r, rdr_idx);
+ return false;
+ }
+
+ /*
+ * Only fill a slot the guest has freshly prepared. An ownership word still
+ * holding the pattern means this slot was filled earlier and has not been
+ * consumed, so the ring is full: leave the packet queued rather than
+ * overwrite a buffer that is still in use.
+ */
+ if (orig_own == RES_OWNERSHIP_PATTERN) {
+ trace_axiado_hcp_rx_backpressure(r, rdr_idx);
+ return false;
+ }
+
+ /* Step 2: DMA-write the packet into the guest's receive buffer. */
+ if (dma_memory_write(&address_space_memory, buf_addr, buf, len,
+ MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
+ return false;
+ }
+
+ /*
+ * Step 3: build the result descriptor and output token in this slot. The
+ * slot is rebuilt from zero because the guest clears only the ownership
+ * word, so stale token bytes would otherwise survive.
+ */
+ memset(res_desc, 0, rdr_io);
+
+ /* word 0: control word — segment byte count + first/last segment */
+ uint32_t ctrl = ((uint32_t)len & RES_DESC_PARTICLE_MASK)
+ | RES_DESC_FIRST_SEG_BIT
+ | RES_DESC_LAST_SEG_BIT;
+ stl_le_p(&res_desc[RES_DESC_FLAGS_OFF], ctrl);
+
+ /* words 2/3: destination packet address (64-bit device) */
+ stl_le_p(&res_desc[RES_DESC_DATA_LO_OFF], (uint32_t)buf_addr);
+ stl_le_p(&res_desc[RES_DESC_DATA_HI_OFF], (uint32_t)(buf_addr >> 32));
+
+ /* EIP-96 output token: length with a zero error code, then app_id << 9. */
+ if (rdr_io >= RES_TOKEN_APP_ID_OFF + 4) {
+ stl_le_p(&res_desc[RES_TOKEN_HDR_OFF],
+ (uint32_t)len & RES_TOKEN_LEN_MASK);
+ stl_le_p(&res_desc[RES_TOKEN_APP_ID_OFF],
+ ((uint32_t)s->macs[mac_idx].app_id & 0x7Fu) << 9);
+ }
+
+ /*
+ * The ownership word in the last word of the slot is the packet-ready
+ * handshake, so it is written after everything else the guest will read.
+ */
+ if (rdr_io >= rdr_stride && rdr_stride >= 4) {
+ stl_le_p(&res_desc[rdr_stride - 4], RES_OWNERSHIP_PATTERN);
+ }
+
+ dma_memory_write(&address_space_memory, rdr_desc_addr, res_desc, rdr_io,
+ MEMTXATTRS_UNSPECIFIED);
+
+ /*
+ * Step 4: advance the receive cursor. The command ring counters belong to
+ * the transmit path and must not move here.
+ */
+ s->rings[r].rx_idx++;
+ rdr->proc_count += rdr_stride;
+ rdr->proc_pntr = (rdr->proc_pntr + rdr_stride) % rdr->size;
+
+ trace_axiado_hcp_rx_packet(r, mac_idx, len);
+
+ rdr->stat |= XDR_STAT_PROC_THRESH_IRQ;
+ hcp_update_ring_irq(s, r);
+
+ return true;
+}
+
+static bool axiado_nic_can_receive(NetClientState *nc)
+{
+ AXIADOHCPState *s = qemu_get_nic_opaque(nc);
+
+ return hcp_rdr_has_slot(&s->rings[HCP_RX_RING].rdr);
+}
+
+static ssize_t axiado_nic_receive(NetClientState *nc, const uint8_t *buf,
+ size_t size)
+{
+ AXIADOHCPState *s = qemu_get_nic_opaque(nc);
+ int mac_idx = hcp_find_mac_for_nc(s, nc);
+
+ if (mac_idx < 0 || size == 0 || size > MAX_PKT_LEN) {
+ return size;
+ }
+
+ if (!hcp_rdr_deliver(s, HCP_RX_RING, mac_idx, buf, size)) {
+ /*
+ * No slot is free. Returning zero keeps the packet queued so it is
+ * redelivered from qemu_flush_queued_packets() once the guest posts
+ * more buffers; returning size would silently drop it and stall the
+ * guest's transport layer under load.
+ */
+ return 0;
+ }
+ return size;
+}
+
+static NetClientInfo axiado_nic_info = {
+ .type = NET_CLIENT_DRIVER_NIC,
+ .size = sizeof(NICState),
+ .can_receive = axiado_nic_can_receive,
+ .receive = axiado_nic_receive,
+};
+
+/*
+ * QOM lifecycle
+ */
+
+static void axiado_hcp_realize(DeviceState *dev, Error **errp)
+{
+ AXIADOHCPState *s = AXIADO_HCP(dev);
+ SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+
+ memory_region_init_io(&s->mmio_eip197, OBJECT(s), &eip197_ops, s,
+ "axiado-hcp.eip197", HCP_EIP197_SIZE);
+ sysbus_init_mmio(sbd, &s->mmio_eip197);
+
+ memory_region_init_io(&s->mmio_shim, OBJECT(s), &shim_ops, s,
+ "axiado-hcp.shim", HCP_SHIM_SIZE);
+ sysbus_init_mmio(sbd, &s->mmio_shim);
+
+ memory_region_init_io(&s->mmio_phy_csr, OBJECT(s), &phy_csr_ops, s,
+ "axiado-hcp.phy-csr", HCP_PHY_CSR_SIZE);
+ sysbus_init_mmio(sbd, &s->mmio_phy_csr);
+
+ for (int i = 0; i < HCP_NUM_IRQS; i++) {
+ sysbus_init_irq(sbd, &s->irqs[i]);
+ }
+
+ for (int i = 0; i < HCP_NUM_MACS; i++) {
+ HCPMac *m = &s->macs[i];
+ char nic_name[16];
+
+ snprintf(nic_name, sizeof(nic_name), "axiado-mac%d", i);
+ qemu_macaddr_default_if_unset(&m->conf.macaddr);
+ m->nic = qemu_new_nic(&axiado_nic_info, &m->conf,
+ object_get_typename(OBJECT(dev)),
+ nic_name, &dev->mem_reentrancy_guard, s);
+ qemu_format_nic_info_str(qemu_get_queue(m->nic), m->conf.macaddr.a);
+ }
+}
+
+static void axiado_hcp_unrealize(DeviceState *dev)
+{
+ AXIADOHCPState *s = AXIADO_HCP(dev);
+
+ for (int i = 0; i < HCP_NUM_MACS; i++) {
+ if (s->macs[i].nic) {
+ qemu_del_nic(s->macs[i].nic);
+ s->macs[i].nic = NULL;
+ }
+ }
+}
+
+static void axiado_hcp_reset(DeviceState *dev)
+{
+ AXIADOHCPState *s = AXIADO_HCP(dev);
+ static const uint8_t mac_app_ids[HCP_NUM_MACS] = { 5, 1, 2, 3, 4 };
+
+ for (int i = 0; i < HCP_NUM_MACS; i++) {
+ HCPMac *m = &s->macs[i];
+ /* Preserve nic + conf across reset */
+ NICState *saved_nic = m->nic;
+ NICConf saved_conf = m->conf;
+
+ memset(m, 0, sizeof(*m));
+ m->nic = saved_nic;
+ m->conf = saved_conf;
+ m->app_id = mac_app_ids[i];
+ m->mii_regs[MII_BMCR] = 0x1140;
+ m->mii_regs[MII_BMSR] = 0x002d;
+ }
+
+ for (int i = 0; i < HCP_NUM_RINGS; i++) {
+ memset(&s->rings[i], 0, sizeof(s->rings[i]));
+ }
+ s->aic_g_mask = 0;
+
+ memset(s->flue_fht, 0, sizeof(s->flue_fht));
+ memset(s->pe_regs, 0, sizeof(s->pe_regs));
+ memset(s->cls_regs, 0, sizeof(s->cls_regs));
+ memset(s->flow_regs, 0, sizeof(s->flow_regs));
+ memset(s->cache_regs, 0, sizeof(s->cache_regs));
+ memset(s->trc_ram, 0, sizeof(s->trc_ram));
+}
+
+static const VMStateDescription vmstate_axiado_hcp_mac = {
+ .name = "axiado-hcp/mac",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT32(command_config, HCPMac),
+ VMSTATE_UINT32(mac_addr_lo, HCPMac),
+ VMSTATE_UINT32(mac_addr_hi, HCPMac),
+ VMSTATE_UINT32(frm_length, HCPMac),
+ VMSTATE_UINT32(rx_fifo_sections, HCPMac),
+ VMSTATE_UINT32(tx_fifo_sections, HCPMac),
+ VMSTATE_UINT32(if_mode, HCPMac),
+ VMSTATE_UINT32(mdio_command, HCPMac),
+ VMSTATE_UINT32(mdio_data, HCPMac),
+ VMSTATE_UINT32(mdio_regaddr, HCPMac),
+ VMSTATE_UINT16_ARRAY(mii_regs, HCPMac, 32),
+ VMSTATE_UINT8(app_id, HCPMac),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static const VMStateDescription vmstate_axiado_hcp_xdr = {
+ .name = "axiado-hcp/xdr",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT64(base, HCPxDR),
+ VMSTATE_UINT32(size, HCPxDR),
+ VMSTATE_UINT32(desc_size, HCPxDR),
+ VMSTATE_UINT32(cfg, HCPxDR),
+ VMSTATE_UINT32(dma_cfg, HCPxDR),
+ VMSTATE_UINT32(thresh, HCPxDR),
+ VMSTATE_UINT32(prep_count, HCPxDR),
+ VMSTATE_UINT32(proc_count, HCPxDR),
+ VMSTATE_UINT32(prep_pntr, HCPxDR),
+ VMSTATE_UINT32(proc_pntr, HCPxDR),
+ VMSTATE_UINT32(stat, HCPxDR),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static const VMStateDescription vmstate_axiado_hcp_ring = {
+ .name = "axiado-hcp/ring",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_STRUCT(cdr, HCPRing, 1, vmstate_axiado_hcp_xdr, HCPxDR),
+ VMSTATE_STRUCT(rdr, HCPRing, 1, vmstate_axiado_hcp_xdr, HCPxDR),
+ VMSTATE_UINT32(aic_cdr_mask, HCPRing),
+ VMSTATE_UINT32(aic_rdr_mask, HCPRing),
+ VMSTATE_UINT32(rx_idx, HCPRing),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static const VMStateDescription vmstate_axiado_hcp = {
+ .name = TYPE_AXIADO_HCP,
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_STRUCT_ARRAY(macs, AXIADOHCPState, HCP_NUM_MACS, 1,
+ vmstate_axiado_hcp_mac, HCPMac),
+ VMSTATE_STRUCT_ARRAY(rings, AXIADOHCPState, HCP_NUM_RINGS, 1,
+ vmstate_axiado_hcp_ring, HCPRing),
+ VMSTATE_UINT32(aic_g_mask, AXIADOHCPState),
+ VMSTATE_UINT32_ARRAY(flue_fht, AXIADOHCPState,
+ HCP_EIP_FLUE_FHT_REGS),
+ VMSTATE_UINT32_ARRAY(pe_regs, AXIADOHCPState, HCP_EIP_PE_REGS),
+ VMSTATE_UINT32_ARRAY(cls_regs, AXIADOHCPState, HCP_EIP_CLS_REGS),
+ VMSTATE_UINT32_ARRAY(flow_regs, AXIADOHCPState, HCP_EIP_FLOW_REGS),
+ VMSTATE_UINT32_ARRAY(cache_regs, AXIADOHCPState, HCP_EIP_CACHE_REGS),
+ VMSTATE_UINT32_ARRAY(trc_ram, AXIADOHCPState, HCP_EIP_TRC_RAM_REGS),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static const Property axiado_hcp_properties[] = {
+ DEFINE_PROP_MACADDR("mac0", AXIADOHCPState, macs[0].conf.macaddr),
+ DEFINE_PROP_MACADDR("mac1", AXIADOHCPState, macs[1].conf.macaddr),
+ DEFINE_PROP_MACADDR("mac2", AXIADOHCPState, macs[2].conf.macaddr),
+ DEFINE_PROP_MACADDR("mac3", AXIADOHCPState, macs[3].conf.macaddr),
+ DEFINE_PROP_MACADDR("mac4", AXIADOHCPState, macs[4].conf.macaddr),
+ DEFINE_PROP_NETDEV("netdev0", AXIADOHCPState, macs[0].conf.peers),
+ DEFINE_PROP_NETDEV("netdev1", AXIADOHCPState, macs[1].conf.peers),
+ DEFINE_PROP_NETDEV("netdev2", AXIADOHCPState, macs[2].conf.peers),
+ DEFINE_PROP_NETDEV("netdev3", AXIADOHCPState, macs[3].conf.peers),
+ DEFINE_PROP_NETDEV("netdev4", AXIADOHCPState, macs[4].conf.peers),
+};
+
+static void axiado_hcp_class_init(ObjectClass *klass, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+
+ dc->desc = "Axiado HCP network complex";
+ dc->realize = axiado_hcp_realize;
+ dc->unrealize = axiado_hcp_unrealize;
+ dc->vmsd = &vmstate_axiado_hcp;
+ device_class_set_legacy_reset(dc, axiado_hcp_reset);
+ device_class_set_props(dc, axiado_hcp_properties);
+ set_bit(DEVICE_CATEGORY_NETWORK, dc->categories);
+}
+
+static const TypeInfo axiado_hcp_info = {
+ .name = TYPE_AXIADO_HCP,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(AXIADOHCPState),
+ .class_init = axiado_hcp_class_init,
+};
+
+static void axiado_hcp_register_types(void)
+{
+ type_register_static(&axiado_hcp_info);
+}
+type_init(axiado_hcp_register_types)
diff --git a/hw/net/meson.build b/hw/net/meson.build
index 63b0e96245..56dae1266e 100644
--- a/hw/net/meson.build
+++ b/hw/net/meson.build
@@ -70,4 +70,6 @@ if have_hmp
system_ss.add(files('rocker/rocker-hmp-cmds.c'))
endif
+system_ss.add(when: 'CONFIG_AXIADO_HCP', if_true: files('axiado_hcp.c'))
+
subdir('can')
diff --git a/hw/net/trace-events b/hw/net/trace-events
index 001a20b0e2..db9815931a 100644
--- a/hw/net/trace-events
+++ b/hw/net/trace-events
@@ -528,3 +528,9 @@ xen_netdev_rx(int dev, int idx, int status, int flags)
"vif%u idx %d status %d f
ethlite_pkt_lost(uint32_t rx_ctrl) "rx_ctrl:0x%" PRIx32
ethlite_pkt_size_too_big(uint64_t size) "size:0x%" PRIx64
ethlite_pkt_tx_size_too_big(uint64_t size) "size:0x%" PRIx64
+
+# axiado_hcp.c
+axiado_hcp_tx_packet(int ring, unsigned app_id, int mac, unsigned len) "ring
%d app_id %u mac %d len %u"
+axiado_hcp_tx_unroutable(int ring, unsigned app_id, unsigned len, uint64_t
addr) "ring %d app_id %u len %u data 0x%" PRIx64
+axiado_hcp_rx_packet(int ring, int mac, size_t len) "ring %d mac %d len %zu"
+axiado_hcp_rx_backpressure(int ring, unsigned slot) "ring %d slot %u"
diff --git a/include/hw/net/axiado_hcp.h b/include/hw/net/axiado_hcp.h
new file mode 100644
index 0000000000..89069ea456
--- /dev/null
+++ b/include/hw/net/axiado_hcp.h
@@ -0,0 +1,172 @@
+/*
+ * Axiado HCP (Header & Crypto Processing) block - QEMU device model
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (C) 2026 Ratan Lal Dondi <[email protected]>
+ */
+
+#ifndef HW_NET_AXIADO_HCP_H
+#define HW_NET_AXIADO_HCP_H
+
+#include "hw/core/sysbus.h"
+#include "net/net.h"
+
+#define TYPE_AXIADO_HCP "axiado-hcp"
+OBJECT_DECLARE_SIMPLE_TYPE(AXIADOHCPState, AXIADO_HCP)
+
+/* MMIO region sizes, matching the SoC base map */
+#define HCP_EIP197_SIZE 0x100000 /* EIP-197 registers and ring control */
+#define HCP_SHIM_SIZE 0x4000 /* MAC config, MDIO and statistics */
+#define HCP_PHY_CSR_SIZE 0x50000 /* PCS / PHY CSR */
+
+/*
+ * IRQ topology:
+ * index 0 = EIP-197 global
+ * indices 1..4 = ring interfaces
+ * indices 5..9 = per-MAC IRQs, PHY and link events only
+ */
+#define HCP_NUM_RINGS 4
+#define HCP_NUM_MACS 5
+#define HCP_IRQ_EIP197 0
+#define HCP_IRQ_RING_BASE 1
+#define HCP_IRQ_MAC_BASE (HCP_IRQ_RING_BASE + HCP_NUM_RINGS)
+#define HCP_NUM_IRQS (HCP_IRQ_MAC_BASE + HCP_NUM_MACS)
+
+/* MAC block layout within the SHIM region */
+#define HCP_SHIM_MAC_BASE_OFFSET 0x400
+#define HCP_SHIM_MAC_STRIDE 0x400
+
+/* Per-MAC state. Five instances per device: one 10G XGMII and four 1G SGMII.
*/
+typedef struct HCPMac {
+ /* MAC configuration registers */
+ uint32_t command_config; /* R_COMMAND_CONFIG 0x008 */
+ uint32_t mac_addr_lo; /* R_MAC_0 0x00c */
+ uint32_t mac_addr_hi; /* R_MAC_1 0x010 */
+ uint32_t frm_length; /* R_FRM_LENGTH 0x014 */
+ uint32_t rx_fifo_sections; /* R_RX_FIFO_SECT 0x01c */
+ uint32_t tx_fifo_sections; /* R_TX_FIFO_SECT 0x020 */
+ uint32_t if_mode; /* R_IF_MODE 0x300 */
+
+ /* Per-MAC MDIO controller (talks to this MAC's virtual PHY) */
+ uint32_t mdio_command; /* R_MDIO_COMMAND 0x034 */
+ uint32_t mdio_data; /* R_MDIO_DATA 0x038 */
+ uint32_t mdio_regaddr; /* R_MDIO_REGADDR 0x03c (Clause 45) */
+
+ /* Virtual PHY: Clause 22 register file accessed via per-MAC MDIO */
+ uint16_t mii_regs[32];
+
+ /* Axiado IOTOKEN routing tag. MAC 0 (10G) = 5; MAC 1..4 (1G) = 1..4. */
+ uint8_t app_id;
+
+ /* QEMU NIC backend for this MAC */
+ NICConf conf;
+ NICState *nic;
+} HCPMac;
+
+/*
+ * SafeXcel xDR — Command/Result Descriptor Ring control registers.
+ * One instance is a CDR; another (same layout) is the paired RDR.
+ */
+typedef struct HCPxDR {
+ uint64_t base; /* +0x00 BASE_LO / +0x04 BASE_HI */
+ uint32_t size; /* +0x18 RING_SIZE (bytes) */
+ uint32_t desc_size; /* +0x1c DESC_SIZE */
+ uint32_t cfg; /* +0x20 CFG */
+ uint32_t dma_cfg; /* +0x24 DMA_CFG */
+ uint32_t thresh; /* +0x28 THRESH */
+ /*
+ * Byte cursors, not register images. Descriptors prepared by the guest
+ * and descriptors consumed by the engine; the difference is the fill
+ * level reported when +0x2c is read.
+ */
+ uint32_t prep_count; /* +0x2c PREP_COUNT */
+ uint32_t proc_count; /* +0x30 PROC_COUNT */
+ uint32_t prep_pntr; /* +0x34 PREP_PNTR */
+ uint32_t proc_pntr; /* +0x38 PROC_PNTR */
+ uint32_t stat; /* +0x3c STAT (latched IRQ; bit 4 = THRESH IRQ) */
+} HCPxDR;
+
+/*
+ * Backed register windows inside the EIP-197 region, as base address and
+ * count of 32-bit registers. Each covers a block the guest writes during
+ * setup and reads back later:
+ *
+ * FLUE_FHT flow look-up engine, flow hash table configuration
+ * PE packet engine: buffer thresholds, ICE scratch RAM and control,
+ * and the EIP-96 token, context and seed registers
+ * CLS classification engines, three instances at a 0x800 stride
+ * FLOW flow control
+ * CACHE record cache control
+ * TRC_RAM classification record cache, actual on-chip RAM
+ *
+ * The ring status window at 0xffb00 is deliberately absent: the guest only
+ * ever reads it, so it needs no storage and reads as zero.
+ */
+#define HCP_EIP_FLUE_FHT_BASE 0x00000
+#define HCP_EIP_FLUE_FHT_REGS 8
+#define HCP_EIP_PE_BASE 0xa0000
+#define HCP_EIP_PE_REGS 2048
+#define HCP_EIP_TRC_RAM_BASE 0xe0000
+#define HCP_EIP_TRC_RAM_REGS 16384
+#define HCP_EIP_CLS_BASE 0xf0000
+#define HCP_EIP_CLS_REGS 2048
+#define HCP_EIP_FLOW_BASE 0xf6000
+#define HCP_EIP_FLOW_REGS 1024
+#define HCP_EIP_CACHE_BASE 0xf7000
+#define HCP_EIP_CACHE_REGS 1024
+
+/* Per-ring state: a CDR/RDR pair plus its interrupt enable masks. */
+typedef struct HCPRing {
+ HCPxDR cdr;
+ HCPxDR rdr;
+ uint32_t aic_cdr_mask;
+ uint32_t aic_rdr_mask;
+
+ /*
+ * Receive slot cursor, in descriptors. The guest's own read pointer is a
+ * plain counter starting at zero and advancing one descriptor per packet,
+ * so the byte counters above cannot drive the slot index: they carry
+ * clear-bit and word-shift encodings from the register writes. Keeping a
+ * separate cursor lines slot zero up with the guest's first read.
+ */
+ uint32_t rx_idx;
+} HCPRing;
+
+struct AXIADOHCPState {
+ SysBusDevice parent_obj;
+
+ MemoryRegion mmio_eip197;
+ MemoryRegion mmio_shim;
+ MemoryRegion mmio_phy_csr;
+
+ qemu_irq irqs[HCP_NUM_IRQS];
+
+ HCPMac macs[HCP_NUM_MACS];
+ HCPRing rings[HCP_NUM_RINGS];
+
+ /* Global AIC enable mask (per-ring bits) */
+ uint32_t aic_g_mask;
+
+ /*
+ * Register windows within the EIP-197 region that hold guest-written
+ * values. The engine's behaviour does not depend on them - the datapath
+ * is driven entirely by the descriptor rings - but the guest writes them
+ * during setup and reads them back, so the values have to persist.
+ * Anything outside these windows reads as zero.
+ */
+ uint32_t flue_fht[HCP_EIP_FLUE_FHT_REGS];
+ uint32_t pe_regs[HCP_EIP_PE_REGS];
+ uint32_t cls_regs[HCP_EIP_CLS_REGS];
+ uint32_t flow_regs[HCP_EIP_FLOW_REGS];
+ uint32_t cache_regs[HCP_EIP_CACHE_REGS];
+
+ /*
+ * Classification record cache. Real on-chip RAM: at init the guest walks
+ * it writing each record's own index, then reads the records back to
+ * discover how large the cache is.
+ */
+ uint32_t trc_ram[HCP_EIP_TRC_RAM_REGS];
+};
+
+#endif /* HW_NET_AXIADO_HCP_H */
--
2.34.1