[PATCH v1 1/8] hw/i2c/aspeed_i2c: Support the AST2700 master buffer mode

Jamin Lin <[email protected]> Tue, 4 Aug 2026 08:19:57 +0000
Newsgroups org.nongnu.qemu-arm,org.nongnu.qemu-devel
Message-ID <[email protected]>
The AST2700 I2C controller can move master DMA payloads through its=0A=
internal SRAM pool rather than DRAM. The Linux driver calls this "buffer=0A=
mode" and selects it by default. Buffer mode reuses the master DMA=0A=
command bits (TX/RX_DMA_EN) and the DMA length registers, so the only=0A=
difference from a DRAM transfer is where the data comes from and goes=0A=
to: an offset into the pool programmed in I2CM_DMA_TX/RX_ADDR. The=0A=
I2CC_VERSION_CTRL FUNC_CFG_DMA_EN bit selects between the two.=0A=
=0A=
Implement I2CC_VERSION_CTRL and, when FUNC_CFG_DMA_EN is clear, move the=0A=
payload through the pool buffer instead of DRAM.=0A=
=0A=
I2CC_VERSION_CTRL resets to all ones, so guests that never program it=0A=
keep targeting DRAM and behave as before. The register sits above the=0A=
register window of the earlier SoCs, which are therefore unaffected.=0A=
=0A=
Signed-off-by: Jamin Lin <[email protected]>=0A=
---=0A=
 include/hw/i2c/aspeed_i2c.h |  2 +=0A=
 hw/i2c/aspeed_i2c.c         | 79 +++++++++++++++++++++++++++++++++++++=0A=
 2 files changed, 81 insertions(+)=0A=
=0A=
diff --git a/include/hw/i2c/aspeed_i2c.h b/include/hw/i2c/aspeed_i2c.h=0A=
index 156998e7c1..05937a7a0b 100644=0A=
--- a/include/hw/i2c/aspeed_i2c.h=0A=
+++ b/include/hw/i2c/aspeed_i2c.h=0A=
@@ -231,6 +231,8 @@ REG32(I2CS_DMA_TX_ADDR_HI, 0x68)=0A=
     FIELD(I2CS_DMA_TX_ADDR_HI, ADDR_HI, 0, 7)=0A=
 REG32(I2CS_DMA_RX_ADDR_HI, 0x6c)=0A=
     FIELD(I2CS_DMA_RX_ADDR_HI, ADDR_HI, 0, 7)=0A=
+REG32(I2CC_VERSION_CTRL, 0x94)=0A=
+    FIELD(I2CC_VERSION_CTRL, FUNC_CFG_DMA_EN, 2, 1)=0A=
 =0A=
 struct AspeedI2CState;=0A=
 =0A=
diff --git a/hw/i2c/aspeed_i2c.c b/hw/i2c/aspeed_i2c.c=0A=
index 27afcaecee..68bdcd0e25 100644=0A=
--- a/hw/i2c/aspeed_i2c.c=0A=
+++ b/hw/i2c/aspeed_i2c.c=0A=
@@ -159,6 +159,7 @@ static uint64_t aspeed_i2c_bus_new_read(AspeedI2CBus *b=
us, hwaddr offset,=0A=
     case A_I2CS_INTR_CTRL:=0A=
     case A_I2CS_DMA_LEN_STS:=0A=
     case A_I2CS_INTR_STS:=0A=
+    case A_I2CC_VERSION_CTRL:=0A=
         value =3D bus->regs[offset / sizeof(*bus->regs)];=0A=
         break;=0A=
     case A_I2CC_DMA_ADDR:=0A=
@@ -295,6 +296,65 @@ static int aspeed_i2c_dma_read(AspeedI2CBus *bus, uint=
8_t *data)=0A=
     return 0;=0A=
 }=0A=
 =0A=
+/*=0A=
+ * In AST2700 buffer mode the master DMA command bits (TX/RX_DMA_EN) and t=
he=0A=
+ * DMA length registers are reused, but data is moved through the controll=
er=0A=
+ * internal SRAM pool at the offset programmed in I2CM_DMA_TX/RX_ADDR inst=
ead=0A=
+ * of DRAM. FUNC_CFG_DMA_EN selects between the two (set =3D DRAM).=0A=
+ */=0A=
+static bool aspeed_i2c_bus_dma_to_pool(AspeedI2CBus *bus)=0A=
+{=0A=
+    return aspeed_i2c_is_new_mode(bus->controller) &&=0A=
+           !ARRAY_FIELD_EX32(bus->regs, I2CC_VERSION_CTRL, FUNC_CFG_DMA_EN=
);=0A=
+}=0A=
+=0A=
+static int aspeed_i2c_bus_send_dma_pool(AspeedI2CBus *bus)=0A=
+{=0A=
+    AspeedI2CClass *aic =3D ASPEED_I2C_GET_CLASS(bus->controller);=0A=
+    uint32_t reg_dma_len =3D aspeed_i2c_bus_dma_len_offset(bus);=0A=
+    uint32_t reg_cmd =3D aspeed_i2c_bus_cmd_offset(bus);=0A=
+    uint32_t offset =3D bus->regs[R_I2CM_DMA_TX_ADDR];=0A=
+    uint8_t *pool_base =3D aic->bus_pool_base(bus);=0A=
+    int ret =3D -1;=0A=
+    int i;=0A=
+=0A=
+    ARRAY_FIELD_DP32(bus->regs, I2CM_DMA_LEN_STS, TX_LEN, 0);=0A=
+    for (i =3D 0; bus->regs[reg_dma_len] &&=0A=
+                offset + i < ASPEED_I2C_BUS_POOL_SIZE; i++) {=0A=
+        trace_aspeed_i2c_bus_send("BUFF", i + 1, bus->regs[reg_dma_len],=
=0A=
+                                  pool_base[offset + i]);=0A=
+        ret =3D i2c_send(bus->bus, pool_base[offset + i]);=0A=
+        bus->regs[reg_dma_len]--;=0A=
+        ARRAY_FIELD_DP32(bus->regs, I2CM_DMA_LEN_STS, TX_LEN, i + 1);=0A=
+        if (ret) {=0A=
+            break;=0A=
+        }=0A=
+    }=0A=
+    SHARED_ARRAY_FIELD_DP32(bus->regs, reg_cmd, TX_DMA_EN, 0);=0A=
+    return ret;=0A=
+}=0A=
+=0A=
+static void aspeed_i2c_bus_recv_dma_pool(AspeedI2CBus *bus)=0A=
+{=0A=
+    AspeedI2CClass *aic =3D ASPEED_I2C_GET_CLASS(bus->controller);=0A=
+    uint32_t reg_dma_len =3D aspeed_i2c_bus_dma_len_offset(bus);=0A=
+    uint32_t reg_cmd =3D aspeed_i2c_bus_cmd_offset(bus);=0A=
+    uint32_t offset =3D bus->regs[R_I2CM_DMA_RX_ADDR];=0A=
+    uint8_t *pool_base =3D aic->bus_pool_base(bus);=0A=
+    int i;=0A=
+=0A=
+    ARRAY_FIELD_DP32(bus->regs, I2CM_DMA_LEN_STS, RX_LEN, 0);=0A=
+    for (i =3D 0; bus->regs[reg_dma_len] &&=0A=
+                offset + i < ASPEED_I2C_BUS_POOL_SIZE; i++) {=0A=
+        pool_base[offset + i] =3D i2c_recv(bus->bus);=0A=
+        trace_aspeed_i2c_bus_recv("BUFF", i + 1, bus->regs[reg_dma_len],=
=0A=
+                                  pool_base[offset + i]);=0A=
+        bus->regs[reg_dma_len]--;=0A=
+        ARRAY_FIELD_DP32(bus->regs, I2CM_DMA_LEN_STS, RX_LEN, i + 1);=0A=
+    }=0A=
+    SHARED_ARRAY_FIELD_DP32(bus->regs, reg_cmd, RX_DMA_EN, 0);=0A=
+}=0A=
+=0A=
 static int aspeed_i2c_bus_send(AspeedI2CBus *bus)=0A=
 {=0A=
     AspeedI2CClass *aic =3D ASPEED_I2C_GET_CLASS(bus->controller);=0A=
@@ -320,6 +380,10 @@ static int aspeed_i2c_bus_send(AspeedI2CBus *bus)=0A=
         }=0A=
         SHARED_ARRAY_FIELD_DP32(bus->regs, reg_cmd, TX_BUFF_EN, 0);=0A=
     } else if (SHARED_ARRAY_FIELD_EX32(bus->regs, reg_cmd, TX_DMA_EN)) {=
=0A=
+        /* In buffer mode the DMA moves data through the pool, not DRAM */=
=0A=
+        if (aspeed_i2c_bus_dma_to_pool(bus)) {=0A=
+            return aspeed_i2c_bus_send_dma_pool(bus);=0A=
+        }=0A=
         /* In new mode, clear how many bytes we TXed */=0A=
         if (aspeed_i2c_is_new_mode(bus->controller)) {=0A=
             ARRAY_FIELD_DP32(bus->regs, I2CM_DMA_LEN_STS, TX_LEN, 0);=0A=
@@ -385,6 +449,11 @@ static void aspeed_i2c_bus_recv(AspeedI2CBus *bus)=0A=
         SHARED_ARRAY_FIELD_DP32(bus->regs, reg_pool_ctrl, RX_COUNT, i & 0x=
ff);=0A=
         SHARED_ARRAY_FIELD_DP32(bus->regs, reg_cmd, RX_BUFF_EN, 0);=0A=
     } else if (SHARED_ARRAY_FIELD_EX32(bus->regs, reg_cmd, RX_DMA_EN)) {=
=0A=
+        /* In buffer mode the DMA moves data through the pool, not DRAM */=
=0A=
+        if (aspeed_i2c_bus_dma_to_pool(bus)) {=0A=
+            aspeed_i2c_bus_recv_dma_pool(bus);=0A=
+            return;=0A=
+        }=0A=
         /* In new mode, clear how many bytes we RXed */=0A=
         if (aspeed_i2c_is_new_mode(bus->controller)) {=0A=
             ARRAY_FIELD_DP32(bus->regs, I2CM_DMA_LEN_STS, RX_LEN, 0);=0A=
@@ -854,6 +923,9 @@ static void aspeed_i2c_bus_new_write(AspeedI2CBus *bus,=
 hwaddr offset,=0A=
                                                       I2CS_DMA_RX_ADDR_HI,=
=0A=
                                                       ADDR_HI);=0A=
         break;=0A=
+    case A_I2CC_VERSION_CTRL:=0A=
+        bus->regs[R_I2CC_VERSION_CTRL] =3D value;=0A=
+        break;=0A=
     default:=0A=
         qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset 0x%" HWADDR_PRIx "\=
n",=0A=
                       __func__, offset);=0A=
@@ -1497,6 +1569,13 @@ static void aspeed_i2c_bus_reset_hold(Object *obj, R=
esetType type)=0A=
     memset(s->regs, 0, sizeof(s->regs));=0A=
     s->pending_intr_sts =3D 0;=0A=
     i2c_end_transfer(s->bus);=0A=
+    /*=0A=
+     * I2CC_VERSION_CTRL resets to all-ones. FUNC_CFG_DMA_EN is therefore =
set,=0A=
+     * so master DMA targets DRAM unless the guest clears it to select buf=
fer=0A=
+     * mode. Guests unaware of buffer mode never touch this register and k=
eep=0A=
+     * doing DRAM DMA.=0A=
+     */=0A=
+    s->regs[R_I2CC_VERSION_CTRL] =3D 0xffffffff;=0A=
 }=0A=
 =0A=
 static void aspeed_i2c_bus_realize(DeviceState *dev, Error **errp)=0A=
-- =0A=
2.43.0=0A=