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 <steven_lee@aspeedtech.com>
---
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
© 2016 - 2026 Red Hat, Inc.