Add a Caliptra mailbox frontend device with SRAM and CSR regions for the 
AST1040 MCI mailbox window.

Model the mailbox lock, command registers, execute flow and completion path 
through a pluggable peer interface so backends can service commands 
synchronously or asynchronously.

Signed-off-by: Steven Lee <[email protected]>
---
 include/hw/misc/aspeed_cptra_mbox.h | 133 ++++++++++
 hw/misc/aspeed_cptra_mbox.c         | 380 ++++++++++++++++++++++++++++
 hw/misc/meson.build                 |   1 +
 hw/misc/trace-events                |   4 +
 4 files changed, 518 insertions(+)
 create mode 100644 include/hw/misc/aspeed_cptra_mbox.h
 create mode 100644 hw/misc/aspeed_cptra_mbox.c

diff --git a/include/hw/misc/aspeed_cptra_mbox.h 
b/include/hw/misc/aspeed_cptra_mbox.h
new file mode 100644
index 0000000000..470e91597f
--- /dev/null
+++ b/include/hw/misc/aspeed_cptra_mbox.h
@@ -0,0 +1,133 @@
+/*
+ * ASPEED Caliptra mailbox model (Caliptra 2.x subsystem mode)
+ *
+ * Copyright (C) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_MISC_ASPEED_CPTRA_MBOX_H
+#define HW_MISC_ASPEED_CPTRA_MBOX_H
+
+#include "qom/object.h"
+#include "hw/core/sysbus.h"
+
+typedef struct Error Error;
+
+/* SRAM size: 2 MiB (matches MCU_MAILBOX0_SRAM_SIZE in caliptra-mcu-sw) */
+#define CPTRA_MBOX0_SRAM_SIZE    (2u * 1024u * 1024u)
+#define CPTRA_MBOX0_SRAM_WORDS   (CPTRA_MBOX0_SRAM_SIZE / 4)
+
+/* CSR window: 4 KiB (covers all defined registers) */
+#define CPTRA_MBOX0_CSR_SIZE     0x1000u
+
+/* CSR register offsets (relative to CSR BAR) */
+#define CPTRA_MBOX0_LOCK_OFF            0x000
+#define CPTRA_MBOX0_USER_OFF            0x004
+#define CPTRA_MBOX0_TARGET_USER_OFF     0x008
+#define CPTRA_MBOX0_TARGET_USER_VAL_OFF 0x00C
+#define CPTRA_MBOX0_CMD_OFF             0x010
+#define CPTRA_MBOX0_DLEN_OFF            0x014
+#define CPTRA_MBOX0_EXECUTE_OFF         0x018
+#define CPTRA_MBOX0_TARGET_STATUS_OFF   0x01C
+#define CPTRA_MBOX0_CMD_STATUS_OFF      0x020
+#define CPTRA_MBOX0_HW_STATUS_OFF       0x024
+
+/* CMD_STATUS values */
+#define CPTRA_MBOX0_STATUS_BUSY        0
+#define CPTRA_MBOX0_STATUS_DATA_READY  1
+#define CPTRA_MBOX0_STATUS_COMPLETE    2
+#define CPTRA_MBOX0_STATUS_CMD_FAILURE 3
+
+/* SoC agent ID reported in the USER register when the lock is acquired */
+#define CPTRA_MBOX0_SOC_USER_ID    1u
+
+/*
+ * Caliptra mailbox interface (frontend), implemented as a QOM interface so
+ * that backends can deliver an asynchronous response without depending on the
+ * concrete frontend device type.
+ */
+#define TYPE_CPTRA_MBOX_IF "cptra-mbox-if"
+typedef struct CptraMboxIfClass CptraMboxIfClass;
+DECLARE_CLASS_CHECKERS(CptraMboxIfClass, CPTRA_MBOX_IF, TYPE_CPTRA_MBOX_IF)
+typedef struct CptraMboxIf CptraMboxIf;
+#define CPTRA_MBOX_IF(obj) \
+    INTERFACE_CHECK(CptraMboxIf, (obj), TYPE_CPTRA_MBOX_IF)
+
+struct CptraMboxIfClass {
+    InterfaceClass parent;
+
+    /*
+     * Called by the peer when a command submitted via handle_execute() has
+     * completed. @status is a CPTRA_MBOX0_STATUS_* value; @data/@len carry the
+     * response payload to be written back into the mailbox SRAM (@len bytes,
+     * @dlen is the reported DLEN).
+     */
+    void (*complete)(CptraMboxIf *s, uint32_t status, uint32_t dlen,
+                     const uint8_t *data, uint32_t len);
+};
+
+/*
+ * Caliptra mailbox peer (backend) base class.
+ */
+#define TYPE_CPTRA_MBOX_PEER "cptra-mbox-peer"
+OBJECT_DECLARE_TYPE(CptraMboxPeer, CptraMboxPeerClass, CPTRA_MBOX_PEER)
+
+struct CptraMboxPeer {
+    DeviceState parent;
+
+    /* Set by the frontend when this peer is linked to it. */
+    CptraMboxIf *intf;
+};
+
+struct CptraMboxPeerClass {
+    DeviceClass parent;
+
+    /*
+     * Process a command. @data/@len is a copy of the request payload from the
+     * mailbox SRAM. The peer must eventually report completion by calling the
+     * interface's complete() method (synchronously or asynchronously).
+     */
+    void (*handle_execute)(CptraMboxPeer *p, uint32_t cmd, uint32_t dlen,
+                           const uint8_t *data, uint32_t len);
+
+    /* Optional: notify the peer of a mailbox reset. */
+    void (*handle_reset)(CptraMboxPeer *p);
+};
+
+/* Concrete frontend device type. */
+#define TYPE_ASPEED_CPTRA_MBOX "aspeed-cptra-mbox"
+OBJECT_DECLARE_SIMPLE_TYPE(AspeedCptraMboxState, ASPEED_CPTRA_MBOX)
+
+struct AspeedCptraMboxState {
+    SysBusDevice parent_obj;
+
+    /* Linked backend; NULL means no Caliptra peer is present. */
+    CptraMboxPeer *peer;
+
+    bool locked;
+    bool command_pending;
+    bool release_pending;
+    uint32_t user;
+    uint32_t target_user;
+    uint32_t target_user_valid;
+    uint32_t cmd;
+    uint32_t dlen;
+    uint32_t execute;
+    uint32_t target_status;
+    uint32_t cmd_status;
+    uint32_t hw_status;
+
+    uint32_t sram[CPTRA_MBOX0_SRAM_WORDS];
+
+    MemoryRegion sram_mr;
+    MemoryRegion csr_mr;
+};
+
+bool aspeed_cptra_mbox_set_peer(AspeedCptraMboxState *s, CptraMboxPeer *peer,
+                                Error **errp);
+
+/* Concrete external (chardev) peer type. */
+#define TYPE_CPTRA_MBOX_PEER_EXTERN "cptra-mbox-peer-extern"
+
+#endif /* HW_MISC_ASPEED_CPTRA_MBOX_H */
diff --git a/hw/misc/aspeed_cptra_mbox.c b/hw/misc/aspeed_cptra_mbox.c
new file mode 100644
index 0000000000..8c1faf69df
--- /dev/null
+++ b/hw/misc/aspeed_cptra_mbox.c
@@ -0,0 +1,380 @@
+/*
+ * ASPEED Caliptra mailbox host interface (frontend) and peer base class.
+ *
+ * Copyright (C) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/misc/aspeed_cptra_mbox.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "qemu/log.h"
+#include "system/memory.h"
+#include "trace.h"
+
+static size_t cptra_mbox_padded_len(uint32_t dlen)
+{
+    return (size_t)((dlen + 3) / 4) * 4;
+}
+
+static void cptra_mbox_clear(AspeedCptraMboxState *s)
+{
+    s->locked = false;
+    s->release_pending = false;
+    s->user = 0;
+    s->target_user = 0;
+    s->target_user_valid = 0;
+    s->cmd = 0;
+    s->dlen = 0;
+    s->execute = 0;
+    s->target_status = 0;
+    s->cmd_status = 0;
+    s->hw_status = 0;
+    memset(s->sram, 0, sizeof(s->sram));
+}
+
+static void cptra_mbox_submit(AspeedCptraMboxState *s)
+{
+    CptraMboxPeerClass *pc;
+    g_autofree uint8_t *data = NULL;
+    size_t len;
+
+    if (s->dlen > CPTRA_MBOX0_SRAM_SIZE) {
+        s->cmd_status = CPTRA_MBOX0_STATUS_CMD_FAILURE;
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: DLEN 0x%x exceeds SRAM size\n", __func__, s->dlen);
+        return;
+    }
+
+    if (!s->peer) {
+        /* No Caliptra peer present: the command cannot be serviced. */
+        s->cmd_status = CPTRA_MBOX0_STATUS_CMD_FAILURE;
+        return;
+    }
+
+    len = cptra_mbox_padded_len(s->dlen);
+    if (len) {
+        data = g_malloc(len);
+        for (size_t i = 0; i < len / 4; i++) {
+            stl_le_p(data + i * 4, s->sram[i]);
+        }
+    }
+
+    s->command_pending = true;
+    s->release_pending = false;
+    trace_cptra_mbox_execute(s->cmd, s->dlen);
+
+    pc = CPTRA_MBOX_PEER_GET_CLASS(s->peer);
+    pc->handle_execute(s->peer, s->cmd, s->dlen, data, len);
+}
+
+/* CptraMboxIf::complete - called by the peer when a command finishes. */
+static void cptra_mbox_complete(CptraMboxIf *iface, uint32_t status,
+                                uint32_t dlen, const uint8_t *data,
+                                uint32_t len)
+{
+    AspeedCptraMboxState *s = ASPEED_CPTRA_MBOX(iface);
+
+    s->command_pending = false;
+    trace_cptra_mbox_complete(status, dlen);
+
+    if (s->release_pending) {
+        cptra_mbox_clear(s);
+        return;
+    }
+
+    /*
+     * Report the peer's status verbatim and write back whatever response
+     * payload it returned. The peer signals a transport/command failure with
+     * CPTRA_MBOX0_STATUS_CMD_FAILURE and no data.
+     */
+    s->cmd_status = status;
+    s->dlen = dlen;
+    if (data && len) {
+        size_t words = MIN(len, CPTRA_MBOX0_SRAM_SIZE) / 4;
+
+        for (size_t i = 0; i < words; i++) {
+            s->sram[i] = ldl_le_p(data + i * 4);
+        }
+    }
+}
+
+static uint64_t cptra_mbox_sram_read(void *opaque, hwaddr offset, unsigned 
size)
+{
+    AspeedCptraMboxState *s = opaque;
+    uint32_t idx = offset / 4;
+
+    if (idx >= CPTRA_MBOX0_SRAM_WORDS) {
+        return 0;
+    }
+    return s->sram[idx];
+}
+
+static void cptra_mbox_sram_write(void *opaque, hwaddr offset,
+                                  uint64_t value, unsigned size)
+{
+    AspeedCptraMboxState *s = opaque;
+    uint32_t idx = offset / 4;
+
+    if (idx < CPTRA_MBOX0_SRAM_WORDS) {
+        s->sram[idx] = (uint32_t)value;
+    }
+}
+
+static const MemoryRegionOps cptra_mbox_sram_ops = {
+    .read = cptra_mbox_sram_read,
+    .write = cptra_mbox_sram_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid.min_access_size = 4,
+    .valid.max_access_size = 4,
+};
+
+static uint64_t cptra_mbox_csr_read(void *opaque, hwaddr offset, unsigned size)
+{
+    AspeedCptraMboxState *s = opaque;
+
+    switch (offset) {
+    case CPTRA_MBOX0_LOCK_OFF:
+        if (!s->locked) {
+            s->locked = true;
+            s->user = CPTRA_MBOX0_SOC_USER_ID;
+            return 0;
+        }
+        return 1;
+    case CPTRA_MBOX0_USER_OFF:
+        return s->user;
+    case CPTRA_MBOX0_TARGET_USER_OFF:
+        return s->target_user;
+    case CPTRA_MBOX0_TARGET_USER_VAL_OFF:
+        return s->target_user_valid;
+    case CPTRA_MBOX0_CMD_OFF:
+        return s->cmd;
+    case CPTRA_MBOX0_DLEN_OFF:
+        return s->dlen;
+    case CPTRA_MBOX0_EXECUTE_OFF:
+        return s->execute;
+    case CPTRA_MBOX0_TARGET_STATUS_OFF:
+        return s->target_status;
+    case CPTRA_MBOX0_CMD_STATUS_OFF:
+        return s->cmd_status;
+    case CPTRA_MBOX0_HW_STATUS_OFF:
+        return s->hw_status;
+    default:
+        return 0;
+    }
+}
+
+static void cptra_mbox_csr_write(void *opaque, hwaddr offset,
+                                 uint64_t value, unsigned size)
+{
+    AspeedCptraMboxState *s = opaque;
+    uint32_t val = (uint32_t)value;
+
+    switch (offset) {
+    case CPTRA_MBOX0_LOCK_OFF:
+    case CPTRA_MBOX0_USER_OFF:
+    case CPTRA_MBOX0_HW_STATUS_OFF:
+        break;
+    case CPTRA_MBOX0_TARGET_USER_OFF:
+        s->target_user = val;
+        break;
+    case CPTRA_MBOX0_TARGET_USER_VAL_OFF:
+        s->target_user_valid = val;
+        break;
+    case CPTRA_MBOX0_CMD_OFF:
+        s->cmd = val;
+        break;
+    case CPTRA_MBOX0_DLEN_OFF:
+        if (val > CPTRA_MBOX0_SRAM_SIZE) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: DLEN 0x%x exceeds SRAM size, clamped\n",
+                          __func__, val);
+            val = CPTRA_MBOX0_SRAM_SIZE;
+        }
+        s->dlen = val;
+        break;
+    case CPTRA_MBOX0_EXECUTE_OFF:
+        if (val == 1 && s->execute != 1 && !s->command_pending) {
+            s->execute = 1;
+            s->cmd_status = CPTRA_MBOX0_STATUS_BUSY;
+            cptra_mbox_submit(s);
+        } else if (val == 0 && s->execute != 0) {
+            if (s->command_pending) {
+                s->release_pending = true;
+            } else {
+                cptra_mbox_clear(s);
+            }
+        }
+        break;
+    case CPTRA_MBOX0_TARGET_STATUS_OFF:
+        s->target_status = val;
+        break;
+    case CPTRA_MBOX0_CMD_STATUS_OFF:
+        s->cmd_status = val;
+        break;
+    default:
+        break;
+    }
+}
+
+static const MemoryRegionOps cptra_mbox_csr_ops = {
+    .read = cptra_mbox_csr_read,
+    .write = cptra_mbox_csr_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid.min_access_size = 4,
+    .valid.max_access_size = 4,
+};
+
+static void cptra_mbox_reset_hold(Object *obj, ResetType type)
+{
+    AspeedCptraMboxState *s = ASPEED_CPTRA_MBOX(obj);
+
+    if (s->command_pending) {
+        /* Defer the clear until the in-flight command completes. */
+        s->release_pending = true;
+        return;
+    }
+    cptra_mbox_clear(s);
+
+    if (s->peer) {
+        CptraMboxPeerClass *pc = CPTRA_MBOX_PEER_GET_CLASS(s->peer);
+        if (pc->handle_reset) {
+            pc->handle_reset(s->peer);
+        }
+    }
+}
+
+static void cptra_mbox_peer_check(const Object *obj, const char *name,
+                                  Object *val, Error **errp)
+{
+    CptraMboxPeer *peer;
+
+    if (!val) {
+        return;
+    }
+
+    peer = CPTRA_MBOX_PEER(val);
+    if (peer->intf) {
+        error_setg(errp, "Caliptra mailbox peer is already in use");
+    }
+}
+
+bool aspeed_cptra_mbox_set_peer(AspeedCptraMboxState *s, CptraMboxPeer *peer,
+                                Error **errp)
+{
+    CptraMboxPeer *old_peer = s->peer;
+
+    if (old_peer == peer) {
+        if (s->peer) {
+            s->peer->intf = CPTRA_MBOX_IF(s);
+        }
+        return true;
+    }
+    if (!object_property_set_link(OBJECT(s), "peer",
+                                  peer ? OBJECT(peer) : NULL, errp)) {
+        return false;
+    }
+    if (old_peer) {
+        old_peer->intf = NULL;
+    }
+    if (s->peer) {
+        s->peer->intf = CPTRA_MBOX_IF(s);
+    }
+    return true;
+}
+
+static void cptra_mbox_init(Object *obj)
+{
+    AspeedCptraMboxState *s = ASPEED_CPTRA_MBOX(obj);
+
+    object_property_add_link(obj, "peer", TYPE_CPTRA_MBOX_PEER,
+                             (Object **)&s->peer, cptra_mbox_peer_check,
+                             OBJ_PROP_LINK_STRONG);
+}
+
+static void cptra_mbox_realize(DeviceState *dev, Error **errp)
+{
+    AspeedCptraMboxState *s = ASPEED_CPTRA_MBOX(dev);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+
+    if (s->peer) {
+        if (!aspeed_cptra_mbox_set_peer(s, s->peer, errp)) {
+            return;
+        }
+    }
+
+    memory_region_init_io(&s->sram_mr, OBJECT(s), &cptra_mbox_sram_ops, s,
+                          "cptra-mbox.sram", CPTRA_MBOX0_SRAM_SIZE);
+    sysbus_init_mmio(sbd, &s->sram_mr);
+
+    memory_region_init_io(&s->csr_mr, OBJECT(s), &cptra_mbox_csr_ops, s,
+                          "cptra-mbox.csr", CPTRA_MBOX0_CSR_SIZE);
+    sysbus_init_mmio(sbd, &s->csr_mr);
+}
+
+static const VMStateDescription vmstate_cptra_mbox = {
+    .name = TYPE_ASPEED_CPTRA_MBOX,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_BOOL(locked, AspeedCptraMboxState),
+        VMSTATE_UINT32(user, AspeedCptraMboxState),
+        VMSTATE_UINT32(target_user, AspeedCptraMboxState),
+        VMSTATE_UINT32(target_user_valid, AspeedCptraMboxState),
+        VMSTATE_UINT32(cmd, AspeedCptraMboxState),
+        VMSTATE_UINT32(dlen, AspeedCptraMboxState),
+        VMSTATE_UINT32(execute, AspeedCptraMboxState),
+        VMSTATE_UINT32(target_status, AspeedCptraMboxState),
+        VMSTATE_UINT32(cmd_status, AspeedCptraMboxState),
+        VMSTATE_UINT32(hw_status, AspeedCptraMboxState),
+        VMSTATE_UINT32_ARRAY(sram, AspeedCptraMboxState,
+                             CPTRA_MBOX0_SRAM_WORDS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void cptra_mbox_class_init(ObjectClass *oc, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(oc);
+    ResettableClass *rc = RESETTABLE_CLASS(oc);
+    CptraMboxIfClass *ic = CPTRA_MBOX_IF_CLASS(oc);
+
+    dc->desc = "Caliptra mailbox host interface";
+    dc->realize = cptra_mbox_realize;
+    dc->vmsd = &vmstate_cptra_mbox;
+    rc->phases.hold = cptra_mbox_reset_hold;
+    ic->complete = cptra_mbox_complete;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+}
+
+static const TypeInfo cptra_mbox_types[] = {
+    {
+        .name          = TYPE_CPTRA_MBOX_IF,
+        .parent        = TYPE_INTERFACE,
+        .class_size    = sizeof(CptraMboxIfClass),
+    },
+    {
+        .name          = TYPE_CPTRA_MBOX_PEER,
+        .parent        = TYPE_DEVICE,
+        .instance_size = sizeof(CptraMboxPeer),
+        .class_size    = sizeof(CptraMboxPeerClass),
+        .abstract      = true,
+    },
+    {
+        .name          = TYPE_ASPEED_CPTRA_MBOX,
+        .parent        = TYPE_SYS_BUS_DEVICE,
+        .instance_size = sizeof(AspeedCptraMboxState),
+        .instance_init = cptra_mbox_init,
+        .class_init    = cptra_mbox_class_init,
+        .interfaces    = (const InterfaceInfo[]) {
+            { TYPE_CPTRA_MBOX_IF },
+            { }
+        },
+    },
+};
+
+DEFINE_TYPES(cptra_mbox_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index fa6a961ac9..d23045c4b5 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -135,6 +135,7 @@ system_ss.add(when: 'CONFIG_PVPANIC_PCI', if_true: 
files('pvpanic-pci.c'))
 system_ss.add(when: 'CONFIG_PVPANIC_MMIO', if_true: files('pvpanic-mmio.c'))
 system_ss.add(when: 'CONFIG_AUX', if_true: files('auxbus.c'))
 system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files(
+  'aspeed_cptra_mbox.c',
   'aspeed_hace.c',
   'aspeed_lpc.c',
   'aspeed_ltpi.c',
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index 82ddbf4d15..9fe2d27fef 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -106,6 +106,10 @@ aspeed_ast2700_scuio_read(uint64_t offset, unsigned size, 
uint32_t data) "To 0x%
 aspeed_ast1040_scu_write(uint64_t offset, unsigned size, uint32_t data) "To 
0x%" PRIx64 " of size %u: 0x%" PRIx32
 aspeed_ast1040_scu_read(uint64_t offset, unsigned size, uint32_t data) "To 
0x%" PRIx64 " of size %u: 0x%" PRIx32
 
+# aspeed_cptra_mbox.c
+cptra_mbox_execute(uint32_t cmd, uint32_t dlen) "EXECUTE cmd 0x%" PRIx32 " 
dlen 0x%" PRIx32
+cptra_mbox_complete(uint32_t status, uint32_t dlen) "complete status %u dlen 
0x%" PRIx32
+
 # mps2-scc.c
 mps2_scc_read(uint64_t offset, uint64_t data, unsigned size) "MPS2 SCC read: 
offset 0x%" PRIx64 " data 0x%" PRIx64 " size %u"
 mps2_scc_write(uint64_t offset, uint64_t data, unsigned size) "MPS2 SCC write: 
offset 0x%" PRIx64 " data 0x%" PRIx64 " size %u"
-- 
2.43.0


Reply via email to