diff --git a/include/hw/arm/aspeed_soc.h b/include/hw/arm/aspeed_soc.h
index d9161d26d645..f0c36b8f7d35 100644
--- a/include/hw/arm/aspeed_soc.h
+++ b/include/hw/arm/aspeed_soc.h
@@ -30,6 +30,7 @@
 #include "hw/usb/hcd-ehci.h"
 #include "qom/object.h"
 #include "hw/misc/aspeed_lpc.h"
+#include "hw/misc/aspeed_ibt.h"
 
 #define ASPEED_SPIS_NUM  2
 #define ASPEED_EHCIS_NUM 2
@@ -65,6 +66,7 @@ struct AspeedSoCState {
     AspeedSDHCIState sdhci;
     AspeedSDHCIState emmc;
     AspeedLPCState lpc;
+    AspeedIBTState ibt;
 };
 
 #define TYPE_ASPEED_SOC "aspeed-soc"
diff --git a/include/hw/misc/aspeed_ibt.h b/include/hw/misc/aspeed_ibt.h
new file mode 100644
index 000000000000..a02a57df9ff8
--- /dev/null
+++ b/include/hw/misc/aspeed_ibt.h
@@ -0,0 +1,47 @@
+/*
+ * ASPEED iBT Device
+ *
+ * Copyright 2016 IBM Corp.
+ *
+ * This code is licensed under the GPL version 2 or later.  See
+ * the COPYING file in the top-level directory.
+ */
+#ifndef ASPEED_IBT_H
+#define ASPEED_IBT_H
+
+#include "hw/sysbus.h"
+#include "chardev/char-fe.h"
+
+#define TYPE_ASPEED_IBT "aspeed.ibt"
+#define ASPEED_IBT(obj) OBJECT_CHECK(AspeedIBTState, (obj), TYPE_ASPEED_IBT)
+
+#define ASPEED_IBT_NR_REGS (0x1C >> 2)
+
+#define ASPEED_IBT_BUFFER_SIZE 64
+
+typedef struct AspeedIBTState {
+    /*< private >*/
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+    CharBackend chr;
+    bool connected;
+
+    uint8_t recv_msg[ASPEED_IBT_BUFFER_SIZE];
+    uint8_t recv_msg_len;
+    int recv_msg_index;
+    int recv_msg_too_many;
+    bool recv_waiting;
+    int in_escape;
+
+    uint8_t send_msg[ASPEED_IBT_BUFFER_SIZE];
+    uint8_t send_msg_len;
+
+    MemoryRegion iomem;
+    qemu_irq irq;
+
+    uint32_t regs[ASPEED_IBT_NR_REGS];
+
+} AspeedIBTState;
+
+#endif /* ASPEED_IBT_H */
diff --git a/hw/arm/aspeed_ast2600.c b/hw/arm/aspeed_ast2600.c
index fc81c0d8df06..c30d0f320c2a 100644
--- a/hw/arm/aspeed_ast2600.c
+++ b/hw/arm/aspeed_ast2600.c
@@ -219,6 +219,8 @@ static void aspeed_soc_ast2600_init(Object *obj)
 
     snprintf(typename, sizeof(typename), "aspeed.hace-%s", socname);
     object_initialize_child(obj, "hace", &s->hace, typename);
+
+    object_initialize_child(obj, "ibt", &s->ibt, TYPE_ASPEED_IBT);
 }
 
 /*
@@ -510,6 +512,16 @@ static void aspeed_soc_ast2600_realize(DeviceState *dev, Error **errp)
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->hace), 0, sc->memmap[ASPEED_DEV_HACE]);
     sysbus_connect_irq(SYS_BUS_DEVICE(&s->hace), 0,
                        aspeed_soc_get_irq(s, ASPEED_DEV_HACE));
+
+    /* iBT */
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->ibt), errp)) {
+        return;
+    }
+    memory_region_add_subregion(&s->lpc.iomem,
+                   sc->memmap[ASPEED_DEV_IBT] - sc->memmap[ASPEED_DEV_LPC],
+                   &s->ibt.iomem);
+    sysbus_connect_irq(SYS_BUS_DEVICE(&s->ibt), 0,
+                       aspeed_soc_get_irq(s, ASPEED_DEV_IBT));
 }
 
 static void aspeed_soc_ast2600_class_init(ObjectClass *oc, void *data)
diff --git a/hw/arm/aspeed_soc.c b/hw/arm/aspeed_soc.c
index 4a95d27d9d63..5ab4cefc7e8b 100644
--- a/hw/arm/aspeed_soc.c
+++ b/hw/arm/aspeed_soc.c
@@ -219,6 +219,8 @@ static void aspeed_soc_init(Object *obj)
 
     snprintf(typename, sizeof(typename), "aspeed.hace-%s", socname);
     object_initialize_child(obj, "hace", &s->hace, typename);
+
+    object_initialize_child(obj, "ibt", &s->ibt, TYPE_ASPEED_IBT);
 }
 
 static void aspeed_soc_realize(DeviceState *dev, Error **errp)
@@ -438,6 +440,16 @@ static void aspeed_soc_realize(DeviceState *dev, Error **errp)
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->hace), 0, sc->memmap[ASPEED_DEV_HACE]);
     sysbus_connect_irq(SYS_BUS_DEVICE(&s->hace), 0,
                        aspeed_soc_get_irq(s, ASPEED_DEV_HACE));
+
+    /* iBT */
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->ibt), errp)) {
+        return;
+    }
+    memory_region_add_subregion(&s->lpc.iomem,
+                   sc->memmap[ASPEED_DEV_IBT] - sc->memmap[ASPEED_DEV_LPC],
+                   &s->ibt.iomem);
+    sysbus_connect_irq(SYS_BUS_DEVICE(&s->lpc), 1 + aspeed_lpc_ibt,
+                       qdev_get_gpio_in(DEVICE(&s->lpc), aspeed_lpc_ibt));
 }
 static Property aspeed_soc_properties[] = {
     DEFINE_PROP_LINK("dram", AspeedSoCState, dram_mr, TYPE_MEMORY_REGION,
diff --git a/hw/misc/aspeed_ibt.c b/hw/misc/aspeed_ibt.c
new file mode 100644
index 000000000000..69a2096ccb00
--- /dev/null
+++ b/hw/misc/aspeed_ibt.c
@@ -0,0 +1,596 @@
+/*
+ * ASPEED iBT Device
+ *
+ * Copyright (c) 2016-2021 Cédric Le Goater, IBM Corporation.
+ *
+ * This code is licensed under the GPL version 2 or later.  See
+ * the COPYING file in the top-level directory.
+ */
+
+#include "qemu/osdep.h"
+#include "hw/sysbus.h"
+#include "sysemu/qtest.h"
+#include "sysemu/sysemu.h"
+#include "qemu/log.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "hw/irq.h"
+#include "hw/qdev-properties.h"
+#include "hw/qdev-properties-system.h"
+#include "migration/vmstate.h"
+#include "hw/misc/aspeed_ibt.h"
+#include "trace.h"
+
+#define BT_IO_REGION_SIZE 0x1C
+
+#define TO_REG(o) (o >> 2)
+
+#define BT_CR0                0x0   /* iBT config */
+#define   BT_CR0_IO_BASE         16
+#define   BT_CR0_IRQ             12
+#define   BT_CR0_EN_CLR_SLV_RDP  0x8
+#define   BT_CR0_EN_CLR_SLV_WRP  0x4
+#define   BT_CR0_ENABLE_IBT      0x1
+#define BT_CR1                0x4  /* interrupt enable */
+#define   BT_CR1_IRQ_H2B         0x01
+#define   BT_CR1_IRQ_HBUSY       0x40
+#define BT_CR2                0x8  /* interrupt status */
+#define   BT_CR2_IRQ_H2B         0x01
+#define   BT_CR2_IRQ_HBUSY       0x40
+#define BT_CR3                0xc /* unused */
+#define BT_CTRL                  0x10
+#define   BT_CTRL_B_BUSY         0x80
+#define   BT_CTRL_H_BUSY         0x40
+#define   BT_CTRL_OEM0           0x20
+#define   BT_CTRL_SMS_ATN        0x10
+#define   BT_CTRL_B2H_ATN        0x08
+#define   BT_CTRL_H2B_ATN        0x04
+#define   BT_CTRL_CLR_RD_PTR     0x02
+#define   BT_CTRL_CLR_WR_PTR     0x01
+#define BT_BMC2HOST            0x14
+#define BT_INTMASK             0x18
+#define   BT_INTMASK_B2H_IRQEN   0x01
+#define   BT_INTMASK_B2H_IRQ     0x02
+#define   BT_INTMASK_BMC_HWRST   0x80
+
+/*
+ * VM IPMI defines
+ */
+#define VM_MSG_CHAR        0xA0 /* Marks end of message */
+#define VM_CMD_CHAR        0xA1 /* Marks end of a command */
+#define VM_ESCAPE_CHAR     0xAA /* Set bit 4 from the next byte to 0 */
+
+#define VM_PROTOCOL_VERSION        1
+#define VM_CMD_VERSION             0xff /* A version number byte follows */
+#define VM_CMD_NOATTN              0x00
+#define VM_CMD_ATTN                0x01
+#define VM_CMD_ATTN_IRQ            0x02
+#define VM_CMD_POWEROFF            0x03
+#define VM_CMD_RESET               0x04
+#define VM_CMD_ENABLE_IRQ          0x05 /* Enable/disable the messaging irq */
+#define VM_CMD_DISABLE_IRQ         0x06
+#define VM_CMD_SEND_NMI            0x07
+#define VM_CMD_CAPABILITIES        0x08
+#define   VM_CAPABILITIES_POWER    0x01
+#define   VM_CAPABILITIES_RESET    0x02
+#define   VM_CAPABILITIES_IRQ      0x04
+#define   VM_CAPABILITIES_NMI      0x08
+#define   VM_CAPABILITIES_ATTN     0x10
+#define   VM_CAPABILITIES_GRACEFUL_SHUTDOWN 0x20
+#define VM_CMD_GRACEFUL_SHUTDOWN   0x09
+
+/*
+ * These routines are inspired by the 'ipmi-bmc-extern' model and by
+ * the lanserv simulator of OpenIPMI. See :
+ *    https://github.com/cminyard/openipmi/blob/master/lanserv/serial_ipmi.c
+ */
+static unsigned char ipmb_checksum(const unsigned char *data, int size,
+                                   unsigned char start)
+{
+        unsigned char csum = start;
+
+        for (; size > 0; size--, data++) {
+                csum += *data;
+        }
+        return csum;
+}
+
+static void vm_add_char(unsigned char ch, unsigned char *c, unsigned int *pos)
+{
+    switch (ch) {
+    case VM_MSG_CHAR:
+    case VM_CMD_CHAR:
+    case VM_ESCAPE_CHAR:
+        c[(*pos)++] = VM_ESCAPE_CHAR;
+        c[(*pos)++] = ch | 0x10;
+        break;
+
+    default:
+        c[(*pos)++] = ch;
+    }
+}
+
+static void aspeed_ibt_dump_msg(const char *func, unsigned char *msg,
+                                unsigned int len)
+{
+    if (trace_event_get_state_backends(TRACE_ASPEED_IBT_CHR_DUMP_MSG)) {
+        int size = len * 3 + 1;
+        char tmp[size];
+        int i, n = 0;
+
+        for (i = 0; i < len; i++) {
+            n += snprintf(tmp + n, size - n, "%02x:", msg[i]);
+        }
+        tmp[size - 1] = 0;
+
+        trace_aspeed_ibt_chr_dump_msg(func, tmp, len);
+    }
+}
+
+static void aspeed_ibt_chr_write(AspeedIBTState *ibt, const uint8_t *buf,
+                                 int len)
+{
+    int i;
+
+    if (!qemu_chr_fe_get_driver(&ibt->chr)) {
+        return;
+    }
+
+    aspeed_ibt_dump_msg(__func__, ibt->recv_msg, ibt->recv_msg_len);
+
+    for (i = 0; i < len; i++) {
+        qemu_chr_fe_write(&ibt->chr, &buf[i], 1);
+    }
+}
+
+static void vm_send(AspeedIBTState *ibt)
+{
+    unsigned int i;
+    unsigned int len = 0;
+    unsigned char c[(ibt->send_msg_len + 7) * 2];
+    uint8_t netfn;
+
+    /*
+     * The VM IPMI message format does not follow the IPMI BT
+     * interface format. The sequence and the netfn bytes need to be
+     * swapped.
+     */
+    netfn = ibt->send_msg[1];
+    ibt->send_msg[1] = ibt->send_msg[2];
+    ibt->send_msg[2] = netfn;
+
+    /* No length byte in the VM IPMI message format. trim it */
+    for (i = 1; i < ibt->send_msg_len; i++) {
+        vm_add_char(ibt->send_msg[i], c, &len);
+    }
+
+    vm_add_char(-ipmb_checksum(&ibt->send_msg[1], ibt->send_msg_len - 1, 0),
+                c, &len);
+    c[len++] = VM_MSG_CHAR;
+
+    aspeed_ibt_chr_write(ibt, c, len);
+}
+
+static void aspeed_ibt_update_irq(AspeedIBTState *ibt)
+{
+    bool raise = false;
+
+    /* H2B rising */
+    if ((ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_H2B_ATN) &&
+        ((ibt->regs[TO_REG(BT_CR1)] & BT_CR1_IRQ_H2B) == BT_CR1_IRQ_H2B)) {
+        ibt->regs[TO_REG(BT_CR2)] |= BT_CR2_IRQ_H2B;
+
+        /*
+         * Also flag the fact that we are waiting for the guest/driver
+         * to read a received message
+         */
+        ibt->recv_waiting = true;
+        raise = true;
+    }
+
+    /* H_BUSY falling (not supported) */
+    if ((ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_H_BUSY) &&
+        ((ibt->regs[TO_REG(BT_CR1)] & BT_CR1_IRQ_HBUSY) == BT_CR1_IRQ_HBUSY)) {
+        ibt->regs[TO_REG(BT_CR2)] |= BT_CR2_IRQ_HBUSY;
+
+        raise = true;
+    }
+
+    if (raise) {
+        qemu_irq_raise(ibt->irq);
+    }
+}
+
+static void vm_handle_msg(AspeedIBTState *ibt, unsigned char *msg,
+                          unsigned int len)
+{
+    uint8_t seq;
+
+    aspeed_ibt_dump_msg(__func__, ibt->recv_msg, ibt->recv_msg_len);
+
+    if (len < 4) {
+        qemu_log_mask(LOG_GUEST_ERROR, " %s: Message too short\n", __func__);
+        return;
+    }
+
+    if (ipmb_checksum(ibt->recv_msg, ibt->recv_msg_len, 0) != 0) {
+        qemu_log_mask(LOG_GUEST_ERROR, " %s: Message checksum failure\n",
+                      __func__);
+        return;
+    }
+
+    /* Trim the checksum byte */
+    ibt->recv_msg_len--;
+
+    /*
+     * The VM IPMI message format does not follow the IPMI BT
+     * interface format. The sequence and the netfn bytes need to be
+     * swapped.
+     */
+    seq = ibt->recv_msg[0];
+    ibt->recv_msg[0] = ibt->recv_msg[1];
+    ibt->recv_msg[1] = seq;
+
+    aspeed_ibt_update_irq(ibt);
+}
+
+/* TODO: handle commands */
+static void vm_handle_cmd(AspeedIBTState *ibt, unsigned char *msg,
+                          unsigned int len)
+{
+    aspeed_ibt_dump_msg(__func__, ibt->recv_msg, ibt->recv_msg_len);
+
+    if (len < 1) {
+        qemu_log_mask(LOG_GUEST_ERROR, " %s: Command too short\n", __func__);
+        return;
+    }
+
+    switch (msg[0]) {
+    case VM_CMD_VERSION:
+        break;
+
+    case VM_CMD_CAPABILITIES:
+        if (len < 2) {
+            return;
+        }
+        break;
+
+    case VM_CMD_RESET:
+        break;
+    }
+}
+
+static void vm_handle_char(AspeedIBTState *ibt, unsigned char ch)
+{
+    unsigned int len = ibt->recv_msg_len;
+
+    switch (ch) {
+    case VM_MSG_CHAR:
+    case VM_CMD_CHAR:
+        if (ibt->in_escape) {
+            qemu_log_mask(LOG_GUEST_ERROR, " %s: Message ended in escape\n",
+                          __func__);
+        } else if (ibt->recv_msg_too_many) {
+            qemu_log_mask(LOG_GUEST_ERROR, " %s: Message too long\n", __func__);
+        } else if (ibt->recv_msg_len == 0) {
+            /* Nothing to do */
+        } else if (ch == VM_MSG_CHAR) {
+            /* Last byte of message. Signal BMC as the host would do */
+            ibt->regs[TO_REG(BT_CTRL)] |= BT_CTRL_H2B_ATN;
+
+            vm_handle_msg(ibt, ibt->recv_msg, ibt->recv_msg_len);
+
+            /* Message is only handled when read by BMC (!B_BUSY) */
+        } else if (ch == VM_CMD_CHAR) {
+            vm_handle_cmd(ibt, ibt->recv_msg, ibt->recv_msg_len);
+
+            /* Command is now handled. reset receive state */
+            ibt->in_escape = 0;
+            ibt->recv_msg_len = 0;
+            ibt->recv_msg_too_many = 0;
+        }
+        break;
+
+    case VM_ESCAPE_CHAR:
+        if (!ibt->recv_msg_too_many) {
+            ibt->in_escape = 1;
+        }
+        break;
+
+    default:
+        if (ibt->in_escape) {
+            ibt->in_escape = 0;
+            ch &= ~0x10;
+        }
+
+        if (!ibt->recv_msg_too_many) {
+            if (len >= sizeof(ibt->recv_msg)) {
+                ibt->recv_msg_too_many = 1;
+                break;
+            }
+
+            ibt->recv_msg[len] = ch;
+            ibt->recv_msg_len++;
+        }
+        break;
+    }
+}
+
+static void vm_connected(AspeedIBTState *ibt)
+{
+    unsigned int len = 0;
+    unsigned char c[5];
+
+    vm_add_char(VM_CMD_VERSION, c, &len);
+    vm_add_char(VM_PROTOCOL_VERSION, c, &len);
+    c[len++] = VM_CMD_CHAR;
+
+    aspeed_ibt_chr_write(ibt, c, len);
+}
+
+static void aspeed_ibt_chr_event(void *opaque, QEMUChrEvent event)
+{
+    AspeedIBTState *ibt = ASPEED_IBT(opaque);
+
+    switch (event) {
+    case CHR_EVENT_OPENED:
+        vm_connected(ibt);
+        ibt->connected = true;
+        break;
+
+    case CHR_EVENT_CLOSED:
+        if (!ibt->connected) {
+            return;
+        }
+        ibt->connected = false;
+        break;
+    case CHR_EVENT_BREAK:
+    case CHR_EVENT_MUX_IN:
+    case CHR_EVENT_MUX_OUT:
+        /* Ignore */
+        break;
+   }
+    trace_aspeed_ibt_chr_event(ibt->connected);
+}
+
+static int aspeed_ibt_chr_can_receive(void *opaque)
+{
+    AspeedIBTState *ibt = ASPEED_IBT(opaque);
+
+    return !ibt->recv_waiting && !(ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_B_BUSY);
+}
+
+static void aspeed_ibt_chr_receive(void *opaque, const uint8_t *buf,
+                                   int size)
+{
+    AspeedIBTState *ibt = ASPEED_IBT(opaque);
+    int i;
+
+    if (!ibt->connected) {
+        qemu_log_mask(LOG_GUEST_ERROR, " %s: not connected !?\n", __func__);
+        return;
+    }
+
+    for (i = 0; i < size; i++) {
+        vm_handle_char(ibt, buf[i]);
+    }
+}
+
+static void aspeed_ibt_write(void *opaque, hwaddr offset, uint64_t data,
+                             unsigned size)
+{
+    AspeedIBTState *ibt = ASPEED_IBT(opaque);
+
+    trace_aspeed_ibt_write(offset, data);
+
+    switch (offset) {
+    case BT_CTRL:
+        /* CLR_WR_PTR: cleared before a message is written */
+        if (data & BT_CTRL_CLR_WR_PTR) {
+            memset(ibt->send_msg, 0, sizeof(ibt->send_msg));
+            ibt->send_msg_len = 0;
+            trace_aspeed_ibt_event("CLR_WR_PTR");
+        }
+
+        /* CLR_RD_PTR: cleared before a message is read */
+        else if (data & BT_CTRL_CLR_RD_PTR) {
+            ibt->recv_msg_index = -1;
+            trace_aspeed_ibt_event("CLR_RD_PTR");
+        }
+
+        /*
+         * H2B_ATN: raised by host to end message, cleared by BMC
+         * before reading message
+         */
+        else if (data & BT_CTRL_H2B_ATN) {
+            ibt->regs[TO_REG(BT_CTRL)] &= ~BT_CTRL_H2B_ATN;
+            trace_aspeed_ibt_event("H2B_ATN");
+        }
+
+        /* B_BUSY: raised and cleared by BMC when message is read */
+        else if (data & BT_CTRL_B_BUSY) {
+            ibt->regs[TO_REG(BT_CTRL)] ^= BT_CTRL_B_BUSY;
+            trace_aspeed_ibt_event("B_BUSY");
+        }
+
+        /*
+         * B2H_ATN: raised by BMC and cleared by host
+         *
+         * Also simulate the host busy bit which is set while the host
+         * is reading the message from the BMC
+         */
+        else if (data & BT_CTRL_B2H_ATN) {
+            trace_aspeed_ibt_event("B2H_ATN");
+            ibt->regs[TO_REG(BT_CTRL)] |= (BT_CTRL_B2H_ATN | BT_CTRL_H_BUSY);
+
+            vm_send(ibt);
+
+            ibt->regs[TO_REG(BT_CTRL)] &= ~(BT_CTRL_B2H_ATN | BT_CTRL_H_BUSY);
+
+            /* signal H_BUSY falling but that's a bit useless */
+            aspeed_ibt_update_irq(ibt);
+        }
+
+        /* Anything else is unexpected */
+        else {
+            qemu_log_mask(LOG_GUEST_ERROR, "%s: unexpected CTRL setting\n",
+                          __func__);
+        }
+
+        /* Message was read by BMC. we can reset the receive state */
+        if (!(ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_B_BUSY)) {
+            trace_aspeed_ibt_event("B_BUSY cleared");
+            ibt->recv_waiting = false;
+            ibt->in_escape = 0;
+            ibt->recv_msg_len = 0;
+            ibt->recv_msg_too_many = 0;
+        }
+        break;
+
+    case BT_BMC2HOST:
+        if (ibt->send_msg_len < sizeof(ibt->send_msg)) {
+            trace_aspeed_ibt_event("BMC2HOST");
+            ibt->send_msg[ibt->send_msg_len++] = data & 0xff;
+        }
+        break;
+
+    case BT_CR0: /* TODO: iBT config */
+    case BT_CR1: /* interrupt enable */
+    case BT_CR3: /* unused */
+    case BT_INTMASK:
+        ibt->regs[TO_REG(offset)] = (uint32_t) data;
+        break;
+    case BT_CR2: /* interrupt status. writing 1 clears. */
+        ibt->regs[TO_REG(offset)] ^= (uint32_t) data;
+        qemu_irq_lower(ibt->irq);
+        break;
+
+    default:
+        qemu_log_mask(LOG_UNIMP, "%s: not implemented 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        break;
+    }
+}
+
+static uint64_t aspeed_ibt_read(void *opaque, hwaddr offset, unsigned size)
+{
+    AspeedIBTState *ibt = ASPEED_IBT(opaque);
+    uint64_t val = 0;
+
+    switch (offset) {
+    case BT_BMC2HOST:
+        trace_aspeed_ibt_event("BMC2HOST");
+        /*
+         * The IPMI BT interface requires the first byte to be the
+         * length of the message
+         */
+        if (ibt->recv_msg_index == -1) {
+            val = ibt->recv_msg_len;
+            ibt->recv_msg_index++;
+        } else if (ibt->recv_msg_index < ibt->recv_msg_len) {
+            val = ibt->recv_msg[ibt->recv_msg_index++];
+        }
+        break;
+
+    case BT_CR0:
+    case BT_CR1:
+    case BT_CR2:
+    case BT_CR3:
+    case BT_CTRL:
+    case BT_INTMASK:
+        return ibt->regs[TO_REG(offset)];
+    default:
+        qemu_log_mask(LOG_UNIMP, "%s: not implemented 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        return 0;
+    }
+
+    trace_aspeed_ibt_read(offset, val);
+    return val;
+}
+
+static const MemoryRegionOps aspeed_ibt_ops = {
+    .read = aspeed_ibt_read,
+    .write = aspeed_ibt_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+    },
+};
+
+static void aspeed_ibt_reset(DeviceState *dev)
+{
+    AspeedIBTState *ibt = ASPEED_IBT(dev);
+
+    memset(ibt->regs, 0, sizeof(ibt->regs));
+
+    memset(ibt->recv_msg, 0, sizeof(ibt->recv_msg));
+    ibt->recv_msg_len = 0;
+    ibt->recv_msg_index = -1;
+    ibt->recv_msg_too_many = 0;
+    ibt->recv_waiting = false;
+    ibt->in_escape = 0;
+
+    memset(ibt->send_msg, 0, sizeof(ibt->send_msg));
+    ibt->send_msg_len = 0;
+}
+
+static void aspeed_ibt_realize(DeviceState *dev, Error **errp)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+    AspeedIBTState *ibt = ASPEED_IBT(dev);
+
+    if (!qemu_chr_fe_get_driver(&ibt->chr) && !qtest_enabled()) {
+        warn_report("Aspeed iBT has no chardev backend");
+    } else {
+        qemu_chr_fe_set_handlers(&ibt->chr, aspeed_ibt_chr_can_receive,
+                                 aspeed_ibt_chr_receive, aspeed_ibt_chr_event,
+                                 NULL, ibt, NULL, true);
+    }
+
+    sysbus_init_irq(sbd, &ibt->irq);
+    memory_region_init_io(&ibt->iomem, OBJECT(ibt), &aspeed_ibt_ops, ibt,
+                          TYPE_ASPEED_IBT, BT_IO_REGION_SIZE);
+
+    sysbus_init_mmio(sbd, &ibt->iomem);
+}
+
+static Property aspeed_ibt_props[] = {
+    DEFINE_PROP_CHR("chardev", AspeedIBTState, chr),
+    DEFINE_PROP_END_OF_LIST(),
+};
+
+static const VMStateDescription vmstate_aspeed_ibt = {
+    .name = "aspeed.bt",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, AspeedIBTState, ASPEED_IBT_NR_REGS),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void aspeed_ibt_class_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    dc->realize = aspeed_ibt_realize;
+    dc->reset = aspeed_ibt_reset;
+    dc->desc = "ASPEED iBT Device";
+    dc->vmsd = &vmstate_aspeed_ibt;
+    device_class_set_props(dc, aspeed_ibt_props);
+}
+
+static const TypeInfo aspeed_ibt_info = {
+    .name = TYPE_ASPEED_IBT,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(AspeedIBTState),
+    .class_init = aspeed_ibt_class_init,
+};
+
+static void aspeed_ibt_register_types(void)
+{
+    type_register_static(&aspeed_ibt_info);
+}
+
+type_init(aspeed_ibt_register_types);
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 1deef25764da..30cb61ec0e31 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -111,6 +111,7 @@ softmmu_ss.add(when: 'CONFIG_PVPANIC_PCI', if_true: files('pvpanic-pci.c'))
 softmmu_ss.add(when: 'CONFIG_AUX', if_true: files('auxbus.c'))
 softmmu_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files(
   'aspeed_hace.c',
+  'aspeed_ibt.c',
   'aspeed_lpc.c',
   'aspeed_scu.c',
   'aspeed_sdmc.c',
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index d0a89eb05964..e8fcacdfd9e9 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -19,6 +19,13 @@ allwinner_h3_dramphy_write(uint64_t offset, uint64_t data, unsigned size) "write
 allwinner_sid_read(uint64_t offset, uint64_t data, unsigned size) "offset 0x%" PRIx64 " data 0x%" PRIx64 " size %" PRIu32
 allwinner_sid_write(uint64_t offset, uint64_t data, unsigned size) "offset 0x%" PRIx64 " data 0x%" PRIx64 " size %" PRIu32
 
+# aspeed_ibt.c
+aspeed_ibt_chr_dump_msg(const char *func, const char *buf, uint32_t len) "%s: %s #%d bytes"
+aspeed_ibt_chr_event(bool connected) "connected:%d"
+aspeed_ibt_read(uint64_t offset, uint64_t value) "offset:0x%" PRIx64 " value:0x%" PRIx64
+aspeed_ibt_write(uint64_t offset, uint64_t value) "offset:0x%" PRIx64 " value:0x%" PRIx64
+aspeed_ibt_event(const char* event) "%s"
+
 # avr_power.c
 avr_power_read(uint8_t value) "power_reduc read value:%u"
 avr_power_write(uint8_t value) "power_reduc write value:%u"
