qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/3] Add STM32 DMA model
@ 2025-03-24 10:05 Nikita Shubin
  2025-03-24 10:05 ` [PATCH 1/3] hw/dma: Add STM32 platfrom DMA controller emulation Nikita Shubin
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Nikita Shubin @ 2025-03-24 10:05 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Alistair Francis, Peter Maydell, qemu-arm,
	Alexandre Iooss, Fabiano Rosas, Laurent Vivier, Ilya Chichkov,
	Nikita Shubin

From: Nikita Shubin <n.shubin@yadro.com>

Add STM32 DMA model and include it in STM32F100 SoC.

The model is fully compatible with GD32F30x DMA.

To: qemu-devel@nongnu.org
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: Alistair Francis <alistair@alistair23.me>
Cc: Peter Maydell <peter.maydell@linaro.org>
Cc: qemu-arm@nongnu.org
Cc: Alexandre Iooss <erdnaxe@crans.org>
Cc: Fabiano Rosas <farosas@suse.de>
Cc: Laurent Vivier <lvivier@redhat.com>
Cc: Nikita Shubin <nikita.shubin@maquefel.me>
Cc: Ilya Chichkov <i.chichkov@yadro.com>
Signed-off-by: Nikita Shubin <n.shubin@yadro.com>

---

b4 doesn't allow sending through relay, may be this should be addressed.

--- b4-submit-tracking ---
# This section is used internally by b4 prep for tracking purposes.
{
  "series": {
    "revision": 1,
    "change-id": "20250314-stm32_dma-24e7a75cec64",
    "prefixes": []
  }
}
-- 
2.48.1



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/3] hw/dma: Add STM32 platfrom DMA controller emulation
  2025-03-24 10:05 [PATCH 0/3] Add STM32 DMA model Nikita Shubin
@ 2025-03-24 10:05 ` Nikita Shubin
  2025-03-24 10:05 ` [PATCH 2/3] hw/arm/stm32f100: Add DMA support for stm32f100 Nikita Shubin
  2025-03-24 10:05 ` [PATCH 3/3] tests/qtest: add qtests for STM32 DMA Nikita Shubin
  2 siblings, 0 replies; 5+ messages in thread
From: Nikita Shubin @ 2025-03-24 10:05 UTC (permalink / raw)
  To: qemu-devel
  Cc: Ilya Chichkov, Nikita Shubin, Paolo Bonzini, Alistair Francis,
	Peter Maydell, qemu-arm

From: Nikita Shubin <n.shubin@yadro.com>

STMicroelectronics STM32 SoCs integrate DMA engine that supports:

* Independent concurrent DMA transfers using 7/5 DMA channels
* Generation of interrupts on various conditions during execution
* PERIPH to MEMORY transactions, invoked by peripheral device models
* MEMORY to MEMORY transactions, invoked by software

This model is compatible with STM32F1xxxx and GD32F30x.

Co-developed-by: Ilya Chichkov <i.chichkov@yadro.com>
Signed-off-by: Ilya Chichkov <i.chichkov@yadro.com>
Signed-off-by: Nikita Shubin <n.shubin@yadro.com>
---
 hw/dma/Kconfig             |   4 +
 hw/dma/meson.build         |   1 +
 hw/dma/stm32_dma.c         | 564 +++++++++++++++++++++++++++++++++++++
 hw/dma/trace-events        |  15 +
 include/hw/dma/stm32_dma.h |  46 +++
 5 files changed, 630 insertions(+)
 create mode 100644 hw/dma/stm32_dma.c
 create mode 100644 include/hw/dma/stm32_dma.h

diff --git a/hw/dma/Kconfig b/hw/dma/Kconfig
index 98fbb1bb04..f1483a1ce5 100644
--- a/hw/dma/Kconfig
+++ b/hw/dma/Kconfig
@@ -30,3 +30,7 @@ config SIFIVE_PDMA
 config XLNX_CSU_DMA
     bool
     select REGISTER
+
+config STM32_DMA
+    bool
+    select REGISTER
\ No newline at end of file
diff --git a/hw/dma/meson.build b/hw/dma/meson.build
index cc7810beb8..4db1aca955 100644
--- a/hw/dma/meson.build
+++ b/hw/dma/meson.build
@@ -12,3 +12,4 @@ system_ss.add(when: 'CONFIG_OMAP', if_true: files('omap_dma.c', 'soc_dma.c'))
 system_ss.add(when: 'CONFIG_RASPI', if_true: files('bcm2835_dma.c'))
 system_ss.add(when: 'CONFIG_SIFIVE_PDMA', if_true: files('sifive_pdma.c'))
 system_ss.add(when: 'CONFIG_XLNX_CSU_DMA', if_true: files('xlnx_csu_dma.c'))
+system_ss.add(when: 'CONFIG_STM32_DMA', if_true: files('stm32_dma.c'))
diff --git a/hw/dma/stm32_dma.c b/hw/dma/stm32_dma.c
new file mode 100644
index 0000000000..b4d588664d
--- /dev/null
+++ b/hw/dma/stm32_dma.c
@@ -0,0 +1,564 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * QEMU STM32 Direct memory access controller (DMA).
+ *
+ * This includes STM32F1xxxx, STM32F2xxxx and GD32F30x
+ *
+ * Author: 2025 Nikita Shubin <n.shubin@yadro.com>
+ */
+#include "qemu/osdep.h"
+
+#include "qapi/error.h"
+#include "hw/qdev-properties.h"
+#include "hw/qdev-properties-system.h"
+#include "hw/sysbus.h"
+#include "migration/vmstate.h"
+#include "hw/irq.h"
+#include "qemu/error-report.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/register.h"
+#include "system/dma.h"
+
+#include "trace.h"
+
+#include "hw/dma/stm32_dma.h"
+
+#define STM32_DMA_APERTURE_SIZE 0x400
+
+/* Global interrupt flag */
+#define DMA_ISR_GIF    BIT(0)
+/* Full transfer finish */
+#define DMA_ISR_TCIF  BIT(1)
+/* Half transfer finish */
+#define DMA_ISR_HTIF  BIT(2)
+/* Transfer error */
+#define DMA_ISR_TEIF  BIT(3)
+
+/* Interrupt flag register (DMA_ISR) */
+REG32(DMA_ISR, 0x00)
+    FIELD(DMA_ISR, CHAN0,     0,  4)
+    FIELD(DMA_ISR, CHAN1,     4,  4)
+    FIELD(DMA_ISR, CHAN2,     8,  4)
+    FIELD(DMA_ISR, CHAN3,     12,  4)
+    FIELD(DMA_ISR, CHAN4,     16,  4)
+    FIELD(DMA_ISR, CHAN5,     20,  4)
+    FIELD(DMA_ISR, CHAN6,     24,  4)
+    FIELD(DMA_ISR, RSVD,      28,  4)
+
+/* Interrupt flag clear register (DMA_IFCR) */
+REG32(DMA_IFCR, 0x04)
+    FIELD(DMA_IFCR, CHAN0,     0,  4)
+    FIELD(DMA_IFCR, CHAN1,     4,  4)
+    FIELD(DMA_IFCR, CHAN2,     8,  4)
+    FIELD(DMA_IFCR, CHAN3,     12,  4)
+    FIELD(DMA_IFCR, CHAN4,     16,  4)
+    FIELD(DMA_IFCR, CHAN5,     20,  4)
+    FIELD(DMA_IFCR, CHAN6,     24,  4)
+    FIELD(DMA_IFCR, RSVD,      28,  4)
+
+/* Channel x control register (DMA_CCRx) */
+/* Address offset: 0x08 + 0x14 * x */
+REG32(DMA_CCR, 0x08)
+    FIELD(DMA_CCR, EN,      0,  1)
+    FIELD(DMA_CCR, TCIE,    1,  1)
+    FIELD(DMA_CCR, HTIE,    2,  1)
+    FIELD(DMA_CCR, TEIE,    3,  1)
+
+    /* all below RO if chan enabled */
+    FIELD(DMA_CCR, DIR,     4,  1)
+    FIELD(DMA_CCR, CIRC,    5,  1)
+    FIELD(DMA_CCR, PINC,    6,  1)
+    FIELD(DMA_CCR, MINC,    7,  1)
+    FIELD(DMA_CCR, PSIZE,   8,  2)
+    FIELD(DMA_CCR, MSIZE,   10, 2)
+    FIELD(DMA_CCR, PL,      12, 2)
+    FIELD(DMA_CCR, M2M,     14, 1)
+    FIELD(DMA_CCR, RSVD,    15, 17)
+
+#define DMA_CCR_RO_MASK   \
+    (R_DMA_CCR_DIR_MASK \
+    | R_DMA_CCR_CIRC_MASK \
+    | R_DMA_CCR_PINC_MASK \
+    | R_DMA_CCR_MINC_MASK \
+    | R_DMA_CCR_PSIZE_MASK \
+    | R_DMA_CCR_MSIZE_MASK \
+    | R_DMA_CCR_PL_MASK \
+    | R_DMA_CCR_M2M_MASK)
+
+/* Channel x counter register (DMA_CNDTRx) */
+/* Address offset: 0x0C + 0x14 * x */
+REG32(DMA_CNDTR, 0x0c)
+    FIELD(DMA_CNDTR, NDT,      0,  16)
+    FIELD(DMA_CNDTR, RSVD,     16, 16)
+
+/* Channel x peripheral base address register (DMA_CPARx) */
+/* Address offset: 0x10 + 0x14 * x */
+REG32(DMA_CPAR, 0x10)
+    FIELD(DMA_CPAR, PA,      0,  32)
+
+/* Channel x memory base address register (DMA_CMARx) */
+/* 0x14 + 0x14 * x */
+REG32(DMA_CMAR, 0x14)
+    FIELD(DMA_CMAR, MA,      0,  32)
+
+#define A_DMA_CCR0     0x08
+#define A_DMA_CMAR7    0xa0
+
+static void stm32_dma_chan_update_intr(STM32DmaState *s, uint8_t idx)
+{
+    if (extract32(s->intf, idx * 4, 4)) {
+        /* set GIFCx */
+        set_bit32(idx * 4, &s->intf);
+        qemu_irq_raise(s->output[idx]);
+    }
+}
+
+static MemTxResult stm32_dma_transfer_one(STM32DmaState *s, uint8_t idx)
+{
+    STM32DmaChannel *chan = &s->chan[idx];
+    uint8_t pwidth = 1 << FIELD_EX32(chan->chctl, DMA_CCR, PSIZE);
+    uint8_t mwidth = 1 << FIELD_EX32(chan->chctl, DMA_CCR, MSIZE);
+    hwaddr paddr, maddr;
+    MemTxResult res = MEMTX_OK;
+    uint32_t data = 0;
+
+    paddr = chan->chpaddr;
+    if (FIELD_EX32(chan->chctl, DMA_CCR, PINC)) {
+        /* increment algorithm enabled */
+        uint32_t incr = chan->chcnt_shadow - chan->chcnt;
+
+        paddr += pwidth * incr;
+    }
+
+    maddr = chan->chmaddr;
+    if (FIELD_EX32(chan->chctl, DMA_CCR, MINC)) {
+        /* increment algorithm enabled */
+        uint32_t incr = chan->chcnt_shadow - chan->chcnt;
+
+        maddr += mwidth * incr;
+    }
+
+    /* issue transaction */
+    if (FIELD_EX32(chan->chctl, DMA_CCR, DIR)) {
+        /* FROM memory TO peripheral */
+        res = dma_memory_read(&address_space_memory, maddr, &data,
+                              pwidth, MEMTXATTRS_UNSPECIFIED);
+        if (res) {
+            goto fail_rw;
+        }
+
+        res = dma_memory_write(&address_space_memory, paddr, &data,
+                               mwidth, MEMTXATTRS_UNSPECIFIED);
+        if (res) {
+            goto fail_rw;
+        }
+
+        trace_stm32_dma_transfer(idx, maddr, mwidth,
+                                 paddr, pwidth, data);
+    } else {
+        /* FROM peripheral TO memory */
+        res = dma_memory_read(&address_space_memory, paddr, &data,
+                              pwidth, MEMTXATTRS_UNSPECIFIED);
+        if (res) {
+            goto fail_rw;
+        }
+
+        res = dma_memory_write(&address_space_memory, maddr, &data,
+                               mwidth, MEMTXATTRS_UNSPECIFIED);
+        if (res) {
+            goto fail_rw;
+        }
+
+        trace_stm32_dma_transfer(idx, paddr, pwidth,
+                                 maddr, mwidth, data);
+    }
+
+    if (FIELD_EX32(chan->chctl, DMA_CCR, HTIE)) {
+        /* Issue completed half transfer interrupt */
+        trace_stm32_dma_raise(idx, "HTIE");
+        set_bit(idx * 4 + 2, (unsigned long *)&s->intf);
+    }
+
+    return res;
+
+fail_rw:
+    if (FIELD_EX32(chan->chctl, DMA_CCR, TEIE)) {
+        trace_stm32_dma_raise(idx, "TEIE");
+        set_bit(idx * 4 + 3, (unsigned long *)&s->intf);
+    }
+
+    trace_stm32_dma_transfer_fail(idx, paddr, maddr);
+    return res;
+}
+
+static void stm32_dma_transfer(STM32DmaState *s, uint8_t idx, bool m2m)
+{
+    STM32DmaChannel *chan = &s->chan[idx];
+    MemTxResult res = 0;
+
+    if (!chan->enabled || chan->chcnt == 0) {
+        trace_stm32_dma_disabled(idx, chan->enabled, chan->chcnt);
+        return;
+    }
+
+    do {
+        res = stm32_dma_transfer_one(s, idx);
+        if (res) {
+            goto fail_rw;
+        }
+
+        chan->chcnt--;
+    } while (chan->chcnt && m2m);
+
+    /* rearm counter */
+    if (chan->chcnt == 0) {
+        if (FIELD_EX32(chan->chctl, DMA_CCR, TCIE)) {
+            /* Issue completed full transfer interrupt */
+            trace_stm32_dma_raise(idx, "TCIE");
+            set_bit(idx * 4 + 1, (unsigned long *)&s->intf);
+        }
+
+        /* M2M mode can't be circular */
+        if (!FIELD_EX32(chan->chctl, DMA_CCR, M2M) &&
+            FIELD_EX32(chan->chctl, DMA_CCR, CIRC)) {
+            chan->chcnt = chan->chcnt_shadow;
+            trace_stm32_dma_cmen(idx, chan->chcnt);
+        }
+    }
+
+fail_rw:
+    stm32_dma_chan_update_intr(s, idx);
+    return;
+}
+
+static uint32_t stm32_dma_chan_read(STM32DmaState *s, hwaddr addr)
+{
+    uint8_t idx = (addr - A_DMA_CCR0) / 0x14 /* STRIDE */;
+    uint8_t reg = (addr - 0x14 /* STRIDE */ * idx);
+    uint32_t val = 0;
+
+    if (idx > s->nr_chans) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: chan_idx %d exceed %d number of channels\n",
+                      __func__, idx, s->nr_chans);
+        return val;
+    }
+
+    switch (reg) {
+    case A_DMA_CCR:
+        val = s->chan[idx].chctl;
+        break;
+    case A_DMA_CNDTR:
+        val = s->chan[idx].chcnt;
+        break;
+    case A_DMA_CPAR:
+        val = s->chan[idx].chpaddr;
+        break;
+    case A_DMA_CMAR:
+        val = s->chan[idx].chmaddr;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unknown reg 0x%x\n",
+                      __func__, reg);
+        break;
+    }
+
+    trace_stm32_dma_chan_read(addr, idx, reg, val);
+
+    return val;
+}
+
+static uint64_t stm32_dma_read(void *opaque, hwaddr addr, unsigned int size)
+{
+    STM32DmaState *s = STM32_DMA(opaque);
+    uint32_t val = 0;
+
+    switch (addr) {
+    case A_DMA_ISR:
+        val = s->intf;
+        break;
+    case A_DMA_CCR0 ... A_DMA_CMAR7:
+        val = stm32_dma_chan_read(s, addr);
+        break;
+    /* write-only */
+    case A_DMA_IFCR:
+    default:
+        /*
+         * TODO: WARN_ONCE ? If left as is produces spam, because many
+         *       people use '|=' on write-only registers.
+         * qemu_log_mask(LOG_GUEST_ERROR,
+         * "%s:  read to unimplemented register " \
+         * "at address: 0x%" PRIx64 " size %d\n",
+         * __func__, addr, size);
+         */
+        break;
+    };
+
+    trace_stm32_dma_read(addr);
+
+    return val;
+}
+
+static void stm32_dma_update_chan_ctrl(STM32DmaState *s, uint8_t idx,
+                                      uint32_t val)
+{
+    int is_enabled, was_enabled;
+
+    was_enabled = !!FIELD_EX32(s->chan[idx].chctl, DMA_CCR, EN);
+    is_enabled = !!FIELD_EX32(val, DMA_CCR, EN);
+
+    if (was_enabled && is_enabled) {
+        uint32_t protected = (s->chan[idx].chctl ^ val) & DMA_CCR_RO_MASK;
+        if (protected) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: attempt to write enabled chan_idx %d settings "
+                          "with val 0x%x\n", __func__, idx, val);
+        }
+
+        val &= ~DMA_CCR_RO_MASK;
+        val |= DMA_CCR_RO_MASK & s->chan[idx].chctl;
+    }
+
+    s->chan[idx].chctl = val;
+    s->chan[idx].enabled = !!FIELD_EX32(s->chan[idx].chctl, DMA_CCR, EN);
+
+    if (!was_enabled && is_enabled) {
+        if (FIELD_EX32(s->chan[idx].chctl, DMA_CCR, M2M)) {
+            stm32_dma_transfer(s, idx, true);
+        }
+    }
+}
+
+static void stm32_dma_chan_write(STM32DmaState *s, hwaddr addr,
+                                uint64_t val)
+{
+    uint8_t idx = (addr - A_DMA_CCR0) / 0x14 /* STRIDE */;
+    uint8_t reg = (addr - 0x14 /* STRIDE */ * idx);
+
+    if (idx > s->nr_chans) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: chan_idx %d exceed %d number of channels\n",
+                      __func__, idx, s->nr_chans);
+        return;
+    }
+
+    trace_stm32_dma_chan_write(addr, idx, reg, val);
+
+    if (reg == A_DMA_CCR) {
+        stm32_dma_update_chan_ctrl(s, idx, val);
+        return;
+    }
+
+    if (s->chan[idx].enabled) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: attempt to changed enabled chan_idx %d settings\n",
+                      __func__, idx);
+        return;
+    }
+
+    switch (reg) {
+    case A_DMA_CNDTR:
+        s->chan[idx].chcnt = FIELD_EX32(val, DMA_CNDTR, NDT);
+        s->chan[idx].chcnt_shadow = s->chan[idx].chcnt;
+        break;
+    case A_DMA_CPAR:
+        s->chan[idx].chpaddr = val;
+        break;
+    case A_DMA_CMAR:
+        s->chan[idx].chmaddr = val;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: unknown reg 0x%x\n",
+                      __func__, reg);
+        break;
+    }
+}
+
+static void stm32_dma_intr_ack(STM32DmaState *s, uint32_t val)
+{
+    int i, j;
+    uint32_t changed = val & s->intf;
+
+    if (!changed) {
+        return;
+    }
+
+    for (i = 0, j = 0; i < R_DMA_IFCR_RSVD_SHIFT; i += 4, j++) {
+        uint8_t bits_changed = extract32(changed, i, 4);
+        if (bits_changed) {
+            /* clear bits */
+            uint8_t bits = 0;
+
+            /* Clear global interrupt flag of channel */
+            if (!(bits_changed & BIT(0))) {
+                bits = extract32(s->intf, i, 4) & ~bits_changed;
+            }
+
+            s->intf = deposit32(s->intf, i , 4, bits);
+            if (!bits) {
+                trace_stm32_dma_lower(j);
+                qemu_irq_lower(s->output[j]);
+            }
+        }
+    }
+}
+
+static void stm32_dma_write(void *opaque, hwaddr addr,
+                              uint64_t val, unsigned int size)
+{
+    STM32DmaState *s = STM32_DMA(opaque);
+
+    switch (addr) {
+    case A_DMA_IFCR:
+        stm32_dma_intr_ack(s, val);
+        break;
+    case A_DMA_CCR0 ... A_DMA_CMAR7:
+        stm32_dma_chan_write(s, addr, val);
+        break;
+    /* read-only */
+    case A_DMA_ISR:
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "%s:  write to unimplemented register " \
+                      "at address: 0x%" PRIx64 " size=%d val=0x%" PRIx64 "\n",
+                      __func__, addr, size, val);
+        break;
+    };
+
+    trace_stm32_dma_write(addr, val);
+}
+
+static const MemoryRegionOps dma_ops = {
+    .read =  stm32_dma_read,
+    .write = stm32_dma_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+static void stm32_dma_reset_enter(Object *obj, ResetType type)
+{
+    STM32DmaState *s = STM32_DMA(obj);
+
+    s->intf = 0x0;
+
+    for (int i = 0; i < s->nr_chans; i++) {
+        s->chan[i].chctl = 0;
+        s->chan[i].chcnt = 0;
+        s->chan[i].chpaddr = 0;
+        s->chan[i].chmaddr = 0;
+
+        s->chan[i].enabled = false;
+    }
+
+    trace_stm32_dma_reset("reset_enter");
+}
+
+static void stm32_dma_reset_hold(Object *obj, ResetType type)
+{
+    STM32DmaState *s = STM32_DMA(obj);
+
+    for (int i = 0; i < s->nr_chans; i++) {
+        qemu_irq_lower(s->output[i]);
+    }
+
+    trace_stm32_dma_reset("reset_hold");
+}
+
+/* irq from peripheral */
+static void stm32_dma_set(void *opaque, int line, int value)
+{
+    STM32DmaState *s = STM32_DMA(opaque);
+
+    if (line > s->nr_chans) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s:  requested non-existant line %d > %d\n",
+                      __func__, line, s->nr_chans);
+        return;
+    }
+
+    /* start dma transfer */
+    stm32_dma_transfer(s, line, false);
+
+    trace_stm32_dma_set(line, value);
+}
+
+static void stm32_dma_realize(DeviceState *dev, Error **errp)
+{
+    STM32DmaState *s = STM32_DMA(dev);
+
+    memory_region_init_io(&s->mmio, OBJECT(dev), &dma_ops, s,
+            TYPE_STM32_DMA, STM32_DMA_APERTURE_SIZE);
+
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio);
+
+    qdev_init_gpio_in(DEVICE(s), stm32_dma_set, s->nr_chans);
+    for (int i = 0; i < s->nr_chans; i++) {
+        sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->output[i]);
+    }
+}
+
+static const VMStateDescription vmstate_stm32_dma_channel = {
+    .name = "stm32_dma_channel",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_BOOL(enabled, STM32DmaChannel),
+        VMSTATE_UINT32(chctl, STM32DmaChannel),
+        VMSTATE_UINT32(chcnt, STM32DmaChannel),
+        VMSTATE_UINT32(chpaddr, STM32DmaChannel),
+        VMSTATE_UINT32(chmaddr, STM32DmaChannel),
+        VMSTATE_UINT32(chcnt_shadow, STM32DmaChannel),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static const VMStateDescription vmstate_stm32_dma = {
+    .name = TYPE_STM32_DMA,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT8(nr_chans, STM32DmaState),
+        VMSTATE_UINT32(intf,    STM32DmaState),
+        VMSTATE_STRUCT_ARRAY(chan, STM32DmaState, STM32_DMA_CHAN_NUMBER,
+                             1, vmstate_stm32_dma_channel, STM32DmaChannel),
+        VMSTATE_END_OF_LIST(),
+    }
+};
+
+static const Property stm32_dma_properties[] = {
+    DEFINE_PROP_UINT8("nchans", STM32DmaState, nr_chans, STM32_DMA_CHAN_NUMBER),
+};
+
+static void stm32_dma_class_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    rc->phases.enter = stm32_dma_reset_enter;
+    rc->phases.hold = stm32_dma_reset_hold;
+
+    dc->vmsd = &vmstate_stm32_dma;
+    dc->realize = stm32_dma_realize;
+    dc->desc = "STM32 DMA";
+
+    device_class_set_props(dc, stm32_dma_properties);
+}
+
+static const TypeInfo stm32_dma_info = {
+    .name = TYPE_STM32_DMA,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(STM32DmaState),
+    .class_init = stm32_dma_class_init,
+};
+
+static void stm32_dma_register_types(void)
+{
+    type_register_static(&stm32_dma_info);
+}
+
+type_init(stm32_dma_register_types)
diff --git a/hw/dma/trace-events b/hw/dma/trace-events
index 4c09790f9a..8d70fea582 100644
--- a/hw/dma/trace-events
+++ b/hw/dma/trace-events
@@ -47,3 +47,18 @@ pl330_iomem_read(uint32_t addr, uint32_t data) "addr: 0x%08"PRIx32" data: 0x%08"
 
 # xilinx_axidma.c
 xilinx_axidma_loading_desc_fail(uint32_t res) "error:%u"
+
+# stm32_dma.c
+stm32_dma_reset(const char *type) "%s"
+stm32_dma_set(int line, int value) "line %d, value %d"
+stm32_dma_transfer(uint8_t idx, uint64_t src, uint8_t swidth, uint64_t dst, uint8_t dwidth, uint32_t data) "idx: %d 0x%08" PRIx64"[%u] -> 0x%08" PRIx64 "[%u] 0x%x"
+stm32_dma_read(uint64_t addr) "address: 0x%08"PRIx64
+stm32_dma_write(uint64_t addr, uint64_t val) "address: 0x%08"PRIx64" data: 0x%08"PRIx64
+stm32_dma_chan_read(uint64_t addr, uint8_t idx, uint8_t reg, uint64_t val) "address: 0x%08"PRIx64" idx: %u reg: %x data: 0x%08"PRIx64
+stm32_dma_chan_write(uint64_t addr, uint8_t idx, uint8_t reg, uint64_t val) "address: 0x%08"PRIx64" idx: %u reg: %x data: 0x%08"PRIx64
+stm32_dma_transfer_fail(uint8_t idx, uint64_t src, uint64_t dst) "idx: %d 0x%08" PRIx64" -> 0x%08" PRIx64
+stm32_dma_raise(uint8_t idx, const char* type) "idx %d: %s"
+stm32_dma_lower(uint8_t idx) "idx %d"
+stm32_dma_cmen(uint8_t idx, uint32_t cnt) "idx %d cnt %u"
+stm32_dma_disabled(uint8_t idx, bool enabled, uint32_t cnt) "idx %d enabled %u cnt %u"
+
diff --git a/include/hw/dma/stm32_dma.h b/include/hw/dma/stm32_dma.h
new file mode 100644
index 0000000000..4777037a71
--- /dev/null
+++ b/include/hw/dma/stm32_dma.h
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * QEMU STM32 Direct memory access controller (DMA).
+ *
+ * This includes STM32F1xxxx, STM32F2xxxx and STM32F30x
+ *
+ * Author: 2025 Nikita Shubin <n.shubin@yadro.com>
+ */
+#ifndef HW_STM32_DMA_H
+#define HW_STM32_DMA_H
+
+#include "hw/sysbus.h"
+#include "qom/object.h"
+
+#define STM32_DMA_CHAN_NUMBER    7
+
+#define TYPE_STM32_DMA "stm32-dma"
+OBJECT_DECLARE_SIMPLE_TYPE(STM32DmaState, STM32_DMA)
+
+typedef struct STM32DmaChannel {
+    bool enabled;
+
+    uint32_t chctl;
+    uint32_t chcnt;
+    uint32_t chpaddr;
+    uint32_t chmaddr;
+
+    uint32_t chcnt_shadow;
+} STM32DmaChannel;
+
+typedef struct STM32DmaState {
+    /*< private >*/
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+    MemoryRegion mmio;
+    uint8_t nr_chans;
+
+    uint32_t intf;
+
+    STM32DmaChannel chan[STM32_DMA_CHAN_NUMBER];
+
+    qemu_irq output[STM32_DMA_CHAN_NUMBER];
+} STM32DmaState;
+
+#endif /* HW_STM32_DMA_H */
-- 
2.48.1



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 2/3] hw/arm/stm32f100: Add DMA support for stm32f100
  2025-03-24 10:05 [PATCH 0/3] Add STM32 DMA model Nikita Shubin
  2025-03-24 10:05 ` [PATCH 1/3] hw/dma: Add STM32 platfrom DMA controller emulation Nikita Shubin
@ 2025-03-24 10:05 ` Nikita Shubin
  2025-03-24 10:05 ` [PATCH 3/3] tests/qtest: add qtests for STM32 DMA Nikita Shubin
  2 siblings, 0 replies; 5+ messages in thread
From: Nikita Shubin @ 2025-03-24 10:05 UTC (permalink / raw)
  To: qemu-devel
  Cc: Nikita Shubin, Paolo Bonzini, Peter Maydell, Alexandre Iooss,
	Alistair Francis, qemu-arm

From: Nikita Shubin <n.shubin@yadro.com>

Add STM32 DMA support for stm32f100 SoC.

Signals from periphery to DMA are not connected, as no STM32 periphery
currently supports DMA.

Signed-off-by: Nikita Shubin <n.shubin@yadro.com>
---
 hw/arm/Kconfig                 |  1 +
 hw/arm/stm32f100_soc.c         | 51 ++++++++++++++++++++++++++++++++++
 include/hw/arm/stm32f100_soc.h |  3 ++
 3 files changed, 55 insertions(+)

diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 15200a2d7e..a51a8b8b9a 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -383,6 +383,7 @@ config STM32F100_SOC
     select ARM_V7M
     select STM32F2XX_USART
     select STM32F2XX_SPI
+    select STM32_DMA
 
 config STM32F205_SOC
     bool
diff --git a/hw/arm/stm32f100_soc.c b/hw/arm/stm32f100_soc.c
index 53b5636452..dc864ef403 100644
--- a/hw/arm/stm32f100_soc.c
+++ b/hw/arm/stm32f100_soc.c
@@ -39,9 +39,29 @@
 static const uint32_t usart_addr[STM_NUM_USARTS] = { 0x40013800, 0x40004400,
     0x40004800 };
 static const uint32_t spi_addr[STM_NUM_SPIS] = { 0x40013000, 0x40003800 };
+static const uint32_t dma_addr[STM_NUM_DMA] = { 0x40020000, 0x40020400 };
 
 static const int usart_irq[STM_NUM_USARTS] = {37, 38, 39};
 static const int spi_irq[STM_NUM_SPIS] = {35, 36};
+static const uint8_t dma1_irq[] = {
+    11, /* DMA1 channel0 global interrupt */
+    12, /* DMA1 channel1 global interrupt */
+    13, /* DMA1 channel2 global interrupt */
+    14, /* DMA1 channel3 global interrupt */
+    15, /* DMA1 channel4 global interrupt */
+    16, /* DMA1 channel5 global interrupt */
+    17, /* DMA1 channel6 global interrupt */
+};
+
+static const uint8_t dma2_irq[] = {
+    56, /* DMA2 channel0 global interrupt */
+    57, /* DMA2 channel1 global interrupt */
+    58, /* DMA2 channel2 global interrupt */
+    59, /* DMA2 channel3 global interrupt */
+    59, /* DMA2 channel4/5 global interrupt */
+};
+
+static const uint8_t dma_chan_num[STM_NUM_DMA] = { 7, 6 };
 
 static void stm32f100_soc_initfn(Object *obj)
 {
@@ -59,6 +79,10 @@ static void stm32f100_soc_initfn(Object *obj)
         object_initialize_child(obj, "spi[*]", &s->spi[i], TYPE_STM32F2XX_SPI);
     }
 
+    for (i = 0; i < STM_NUM_DMA; i++) {
+        object_initialize_child(obj, "dma[*]", &s->dma[i], TYPE_STM32_DMA);
+    }
+
     s->sysclk = qdev_init_clock_in(DEVICE(s), "sysclk", NULL, NULL, 0);
     s->refclk = qdev_init_clock_in(DEVICE(s), "refclk", NULL, NULL, 0);
 }
@@ -126,6 +150,33 @@ static void stm32f100_soc_realize(DeviceState *dev_soc, Error **errp)
         return;
     }
 
+    /* DMA 1 */
+    dev = DEVICE(&(s->dma[0]));
+    qdev_prop_set_uint8(dev, "nchans", dma_chan_num[0]);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->dma[0]), errp)) {
+        return;
+    }
+    busdev = SYS_BUS_DEVICE(dev);
+    sysbus_mmio_map(SYS_BUS_DEVICE(dev), 0, dma_addr[0]);
+    for (i = 0; i < ARRAY_SIZE(dma1_irq); i++) {
+        sysbus_connect_irq(busdev, i, qdev_get_gpio_in(armv7m, dma1_irq[i]));
+    }
+
+    /* DMA 2 */
+    dev = DEVICE(&(s->dma[1]));
+    qdev_prop_set_uint8(dev, "nchans", dma_chan_num[1]);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->dma[1]), errp)) {
+        return;
+    }
+    busdev = SYS_BUS_DEVICE(dev);
+    sysbus_mmio_map(SYS_BUS_DEVICE(dev), 0, dma_addr[1]);
+    for (i = 0; i < ARRAY_SIZE(dma2_irq); i++) {
+        sysbus_connect_irq(busdev, i, qdev_get_gpio_in(armv7m, dma2_irq[i]));
+    }
+
+    /* DMA channel 4 and 5 have shared irq */
+    sysbus_connect_irq(busdev, i, qdev_get_gpio_in(armv7m, dma2_irq[i - 1]));
+
     /* Attach UART (uses USART registers) and USART controllers */
     for (i = 0; i < STM_NUM_USARTS; i++) {
         dev = DEVICE(&(s->usart[i]));
diff --git a/include/hw/arm/stm32f100_soc.h b/include/hw/arm/stm32f100_soc.h
index a74d7b369c..0ee02e157c 100644
--- a/include/hw/arm/stm32f100_soc.h
+++ b/include/hw/arm/stm32f100_soc.h
@@ -26,6 +26,7 @@
 #define HW_ARM_STM32F100_SOC_H
 
 #include "hw/char/stm32f2xx_usart.h"
+#include "hw/dma/stm32_dma.h"
 #include "hw/ssi/stm32f2xx_spi.h"
 #include "hw/arm/armv7m.h"
 #include "qom/object.h"
@@ -36,6 +37,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(STM32F100State, STM32F100_SOC)
 
 #define STM_NUM_USARTS 3
 #define STM_NUM_SPIS 2
+#define STM_NUM_DMA 2
 
 #define FLASH_BASE_ADDRESS 0x08000000
 #define FLASH_SIZE (128 * 1024)
@@ -49,6 +51,7 @@ struct STM32F100State {
 
     STM32F2XXUsartState usart[STM_NUM_USARTS];
     STM32F2XXSPIState spi[STM_NUM_SPIS];
+    STM32DmaState dma[STM_NUM_DMA];
 
     MemoryRegion sram;
     MemoryRegion flash;
-- 
2.48.1



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 3/3] tests/qtest: add qtests for STM32 DMA
  2025-03-24 10:05 [PATCH 0/3] Add STM32 DMA model Nikita Shubin
  2025-03-24 10:05 ` [PATCH 1/3] hw/dma: Add STM32 platfrom DMA controller emulation Nikita Shubin
  2025-03-24 10:05 ` [PATCH 2/3] hw/arm/stm32f100: Add DMA support for stm32f100 Nikita Shubin
@ 2025-03-24 10:05 ` Nikita Shubin
  2025-04-01 14:11   ` Fabiano Rosas
  2 siblings, 1 reply; 5+ messages in thread
From: Nikita Shubin @ 2025-03-24 10:05 UTC (permalink / raw)
  To: qemu-devel; +Cc: Nikita Shubin, Fabiano Rosas, Laurent Vivier, Paolo Bonzini

From: Nikita Shubin <n.shubin@yadro.com>

Signed-off-by: Nikita Shubin <n.shubin@yadro.com>
---
 tests/qtest/meson.build      |   1 +
 tests/qtest/stm32-dma-test.c | 415 +++++++++++++++++++++++++++++++++++
 2 files changed, 416 insertions(+)
 create mode 100644 tests/qtest/stm32-dma-test.c

diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 5a8c1f102c..6c45692f9d 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -240,6 +240,7 @@ qtests_arm = \
   (config_all_devices.has_key('CONFIG_TPM_TIS_I2C') ? ['tpm-tis-i2c-test'] : []) + \
   (config_all_devices.has_key('CONFIG_VEXPRESS') ? ['test-arm-mptimer'] : []) + \
   (config_all_devices.has_key('CONFIG_MICROBIT') ? ['microbit-test'] : []) + \
+  (config_all_devices.has_key('CONFIG_STM32F100_SOC') ? ['stm32-dma-test'] : []) + \
   (config_all_devices.has_key('CONFIG_STM32L4X5_SOC') ? qtests_stm32l4x5 : []) + \
   (config_all_devices.has_key('CONFIG_FSI_APB2OPB_ASPEED') ? ['aspeed_fsi-test'] : []) + \
   (config_all_devices.has_key('CONFIG_STM32L4X5_SOC') and
diff --git a/tests/qtest/stm32-dma-test.c b/tests/qtest/stm32-dma-test.c
new file mode 100644
index 0000000000..74b81fa434
--- /dev/null
+++ b/tests/qtest/stm32-dma-test.c
@@ -0,0 +1,415 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * QTest testcase for STM32 DMA engine.
+ *
+ * This includes STM32F1xxxx, STM32F2xxxx and GD32F30x
+ *
+ * Author: 2025 Nikita Shubin <n.shubin@yadro.com>
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/bitops.h"
+#include "libqtest-single.h"
+#include "libqos/libqos.h"
+
+/* Offsets in stm32vldiscovery platform: */
+#define DMA_BASE    0x40020000
+#define SRAM_BASE   0x20000000
+
+/* Global interrupt flag */
+#define DMA_ISR_GIF   BIT(0)
+/* Full transfer finish */
+#define DMA_ISR_TCIF  BIT(1)
+/* Half transfer finish */
+#define DMA_ISR_HTIF  BIT(2)
+/* Transfer error */
+#define DMA_ISR_TEIF  BIT(3)
+
+/* Used register/fields definitions */
+#define DMA_CCR(idx)     (0x08 + 0x14 * idx)
+#define DMA_CNDTR(idx)   (0x0C + 0x14 * idx)
+#define DMA_CPAR(idx)    (0x10 + 0x14 * idx)
+#define DMA_CMAR(idx)    (0x14 + 0x14 * idx)
+
+#define DMA_MAX_CHAN    7
+
+/* Register offsets for a dma chan0 within a dma block. */
+#define DMA_CHAN(_idx, _irq)  \
+    { \
+        .ccr = DMA_CCR(_idx), \
+        .cndrt = DMA_CNDTR(_idx), \
+        .cpar = DMA_CPAR(_idx), \
+        .cmar = DMA_CMAR(_idx), \
+        .irq_line = _irq,\
+    }
+
+typedef struct DMAChan {
+    uint32_t ccr;
+    uint32_t cndrt;
+    uint32_t cpar;
+    uint32_t cmar;
+    uint8_t irq_line;
+} DMAChan;
+
+const DMAChan dma_chans[] = {
+    DMA_CHAN(0, 11),
+    DMA_CHAN(1, 12),
+    DMA_CHAN(2, 12),
+    DMA_CHAN(3, 13),
+    DMA_CHAN(4, 14),
+    DMA_CHAN(5, 16),
+    DMA_CHAN(6, 17),
+};
+
+/* Register offsets for a dma within a dma block. */
+typedef struct DMA {
+    uint32_t base_addr;
+    uint32_t isr;
+    uint32_t ofcr;
+} DMA;
+
+const DMA dma = {
+    .base_addr = DMA_BASE,
+    .isr = 0x00,
+    .ofcr = 0x04,
+};
+
+typedef struct TestData {
+    QTestState *qts;
+    const DMA *dma;
+    const DMAChan *chans;
+} TestData;
+
+#define NVIC_ISER 0xE000E100
+#define NVIC_ISPR 0xE000E200
+#define NVIC_ICPR 0xE000E280
+
+static void enable_nvic_irq(unsigned int n)
+{
+    writel(NVIC_ISER, 1 << n);
+}
+
+static void unpend_nvic_irq(unsigned int n)
+{
+    writel(NVIC_ICPR, 1 << n);
+}
+
+static bool check_nvic_pending(unsigned int n)
+{
+    return readl(NVIC_ISPR) & (1 << n);
+}
+
+static uint32_t dma_read(const TestData *td, uint32_t offset)
+{
+    return qtest_readl(td->qts, td->dma->base_addr + offset);
+}
+
+static void dma_write(const TestData *td, uint32_t offset, uint32_t value)
+{
+    qtest_writel(td->qts, td->dma->base_addr + offset, value);
+}
+
+static void dma_write_ofcr(const TestData *td, uint32_t value)
+{
+    return dma_write(td, td->dma->ofcr, value);
+}
+
+static uint32_t dma_read_isr(const TestData *td)
+{
+    return dma_read(td, td->dma->isr);
+}
+
+static void dma_write_ccr(const TestData *td, uint8_t idx, uint32_t value)
+{
+    dma_write(td, td->chans[idx].ccr, value);
+}
+
+static uint32_t dma_read_ccr(const TestData *td, uint8_t idx)
+{
+    return dma_read(td, td->chans[idx].ccr);
+}
+
+static void dma_write_cndrt(const TestData *td, uint8_t idx, uint32_t value)
+{
+    dma_write(td, td->chans[idx].cndrt, value);
+}
+
+static void dma_write_cpar(const TestData *td, uint8_t idx, uint32_t value)
+{
+    dma_write(td, td->chans[idx].cpar, value);
+}
+
+static void dma_write_cmar(const TestData *td, uint8_t idx, uint32_t value)
+{
+    dma_write(td, td->chans[idx].cmar, value);
+}
+
+static void test_m2m(gconstpointer test_data)
+{
+    const TestData *td = test_data;
+    QTestState *s = td->qts;
+    const uint32_t patt_len = 0xff;
+    char *pattern_check = g_malloc(patt_len);
+    char *pattern = g_malloc(patt_len);
+    uint8_t idx = 0;
+    uint32_t val;
+
+    enable_nvic_irq(td->chans[idx].irq_line);
+    qtest_irq_intercept_in(global_qtest, "/machine/soc/dma[0]");
+
+    /* write addr */
+    dma_write_cpar(td, idx, SRAM_BASE);
+    dma_write_cmar(td, idx, SRAM_BASE + patt_len);
+
+    /* enable increment and M2M */
+    val = dma_read_ccr(td, idx);
+    val |= BIT(1); /* TCIE */
+    val |= BIT(6); /* PINC */
+    val |= BIT(7); /* MINC */
+    val |= BIT(14); /* M2M */
+    dma_write_ccr(td, idx, val);
+
+    generate_pattern(pattern, patt_len, patt_len);
+    qtest_memwrite(s, SRAM_BASE, pattern, patt_len);
+
+    dma_write_cndrt(td, idx, patt_len);
+
+    val |= BIT(0); /* enable channel */
+    dma_write_ccr(td, idx, val);
+
+    qtest_memread(s, SRAM_BASE + patt_len, pattern_check, patt_len);
+
+    g_assert(memcmp(pattern, pattern_check, patt_len) == 0);
+
+    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
+}
+
+typedef struct width_pattern {
+    uint32_t src;
+    uint8_t swidth;
+    uint32_t dst;
+    uint8_t dwidth;
+} width_pattern;
+
+static void test_width(gconstpointer test_data)
+{
+    const width_pattern patterns[] = {
+        { 0xb0,       1, 0xb0,       1 },
+        { 0xb0,       1, 0x00b0,     2 },
+        { 0xb0,       1, 0x000000b0, 4 },
+        { 0xb1b0,     2, 0xb0,       1 },
+        { 0xb1b0,     2, 0xb1b0,     2 },
+        { 0xb1b0,     2, 0x0000b1b0, 4 },
+        { 0xb3b2b1b0, 4, 0xb0,       1 },
+        { 0xb3b2b1b0, 4, 0xb1b0,     2 },
+        { 0xb3b2b1b0, 4, 0xb3b2b1b0, 4 },
+    };
+
+    const TestData *td = test_data;
+    QTestState *s = td->qts;
+    const uint32_t patt = 0xffffffff;
+    const uint32_t patt_len = 4;
+    uint32_t dst;
+    uint8_t idx = 0;
+    uint32_t val;
+
+    qmp("{'execute':'system_reset' }");
+
+    /* write addr */
+    dma_write_cpar(td, idx, SRAM_BASE);
+    dma_write_cmar(td, idx, SRAM_BASE + patt_len);
+
+    /* enable increment and M2M */
+    val = dma_read_ccr(td, idx);
+    val |= BIT(6); /* PINC */
+    val |= BIT(7); /* MINC */
+    val |= BIT(14); /* M2M */
+    dma_write_ccr(td, idx, val);
+
+    for (int i = 0; i < ARRAY_SIZE(patterns); i++) {
+        /* fill destination and source with pattern */
+        qtest_memwrite(s, SRAM_BASE, &patt, patt_len);
+        qtest_memwrite(s, SRAM_BASE + patt_len, &patt, patt_len);
+
+        qtest_memwrite(s, SRAM_BASE, &patterns[i].src, patterns[i].swidth);
+
+        dma_write_cndrt(td, idx, 1);
+        val |= BIT(0); /* enable channel */
+        val = deposit32(val, 8, 2, patterns[i].swidth >> 1);
+        val = deposit32(val, 10, 2, patterns[i].dwidth >> 1);
+        dma_write_ccr(td, idx, val);
+
+        qtest_memread(s, SRAM_BASE + patt_len, &dst, patterns[i].dwidth);
+
+        g_assert(memcmp(&dst, &patterns[i].dst, patterns[i].dwidth) == 0);
+
+        /* disable chan */
+        val &= ~BIT(0);
+        dma_write_ccr(td, idx, val);
+    }
+}
+
+static void dma_set_irq(unsigned int idx, int num, int level)
+{
+    g_autofree char *name = g_strdup_printf("/machine/soc/dma[%u]",
+                                            idx);
+    qtest_set_irq_in(global_qtest, name, NULL, num, level);
+}
+
+static void test_triggers(gconstpointer test_data)
+{
+    const TestData *td = test_data;
+    QTestState *s = td->qts;
+    const uint32_t patt = 0xffffffff;
+    const uint32_t patt_len = 4;
+    uint32_t dst;
+    uint32_t val;
+
+    qmp("{'execute':'system_reset' }");
+
+    for (int i = 0; i < ARRAY_SIZE(dma_chans); i++) {
+        qtest_memset(s, SRAM_BASE, 0, patt_len * 2);
+        qtest_memwrite(s, SRAM_BASE, &patt, patt_len);
+
+        /* write addr */
+        dma_write_cpar(td, i, SRAM_BASE);
+        dma_write_cmar(td, i, SRAM_BASE + patt_len);
+
+        val = dma_read_ccr(td, i);
+
+        dma_write_cndrt(td, i, 1);
+        val |= BIT(0); /* enable channel */
+        val = deposit32(val, 8, 2, patt_len >> 1);
+        val = deposit32(val, 10, 2, patt_len >> 1);
+        dma_write_ccr(td, i, val);
+
+        dma_set_irq(0, i, 1);
+
+        qtest_memread(s, SRAM_BASE + patt_len, &dst, patt_len);
+
+        g_assert(memcmp(&dst, &patt, patt_len) == 0);
+
+        /* disable chan */
+        val &= ~BIT(0);
+        dma_write_ccr(td, i, val);
+    }
+}
+
+static void test_interrupts(gconstpointer test_data)
+{
+    const TestData *td = test_data;
+    const uint32_t patt_len = 1024;
+    uint8_t idx = 0;
+    uint32_t val;
+
+    qmp("{'execute':'system_reset' }");
+
+    enable_nvic_irq(td->chans[idx].irq_line);
+
+    /* write addr */
+    dma_write_cpar(td, idx, SRAM_BASE);
+    dma_write_cmar(td, idx, SRAM_BASE + patt_len);
+
+    /* write counter */
+    dma_write_cndrt(td, idx, 2);
+
+    /* enable increment and M2M */
+    val = dma_read_ccr(td, idx);
+    val |= BIT(0); /* EN */
+    val |= BIT(1); /* TCIE */
+    val |= BIT(2); /* HTIE */
+    val |= BIT(3); /* TEIE */
+    val |= BIT(6); /* PINC */
+    val |= BIT(7); /* MINC */
+    dma_write_ccr(td, idx, val);
+
+    /* Half-transfer */
+    dma_set_irq(0, idx, 1);
+    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
+    val = dma_read_isr(td);
+
+    g_assert_true(val & DMA_ISR_GIF);
+    g_assert_true(val & DMA_ISR_HTIF);
+    unpend_nvic_irq(td->chans[idx].irq_line);
+
+    dma_write_ofcr(td, 0xffffffff);
+    val = dma_read_isr(td);
+    g_assert_false(val & DMA_ISR_GIF);
+    g_assert_false(val & DMA_ISR_HTIF);
+
+    /* Full-transfer */
+    dma_set_irq(0, idx, 1);
+    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
+    val = dma_read_isr(td);
+
+    g_assert_true(val & DMA_ISR_GIF);
+    g_assert_true(val & DMA_ISR_HTIF);
+    g_assert_true(val & DMA_ISR_TCIF);
+    unpend_nvic_irq(td->chans[idx].irq_line);
+
+    dma_write_ofcr(td, 0xffffffff);
+    val = dma_read_isr(td);
+    g_assert_false(val & DMA_ISR_GIF);
+    g_assert_false(val & DMA_ISR_HTIF);
+    g_assert_false(val & DMA_ISR_TCIF);
+
+    /* Error-on-transfer */
+    val = dma_read_ccr(td, idx);
+    val &= ~BIT(0);
+    dma_write_ccr(td, idx, val);
+
+    dma_write_cndrt(td, idx, 1);
+    dma_write_cpar(td, idx, 0xffffffff);
+
+    val |= BIT(0);
+    dma_write_ccr(td, idx, val);
+
+    dma_set_irq(0, idx, 1);
+    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
+    val = dma_read_isr(td);
+
+    g_assert_true(val & DMA_ISR_GIF);
+    g_assert_true(val & DMA_ISR_TEIF);
+    unpend_nvic_irq(td->chans[idx].irq_line);
+
+    dma_write_ofcr(td, 0xffffffff);
+    val = dma_read_isr(td);
+    g_assert_false(val & DMA_ISR_GIF);
+    g_assert_false(val & DMA_ISR_TEIF);
+}
+
+static void stm32_add_test(const char *name, const TestData *td,
+                           GTestDataFunc fn)
+{
+    g_autofree char *full_name = g_strdup_printf(
+        "stm32_dma/%s", name);
+    qtest_add_data_func(full_name, td, fn);
+}
+
+/* Convenience macro for adding a test with a predictable function name. */
+#define add_test(name, td) stm32_add_test(#name, td, test_##name)
+
+int main(int argc, char **argv)
+{
+    TestData testdata;
+    QTestState *s;
+    int ret;
+
+    g_test_init(&argc, &argv, NULL);
+    s = qtest_start("-machine stm32vldiscovery");
+    g_test_set_nonfatal_assertions();
+
+    TestData *td = &testdata;
+    td->qts = s;
+    td->dma = &dma;
+    td->chans = dma_chans;
+    add_test(m2m, td);
+    add_test(width, td);
+    add_test(triggers, td);
+    add_test(interrupts, td);
+
+    ret = g_test_run();
+    qtest_end();
+
+    return ret;
+}
-- 
2.48.1



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH 3/3] tests/qtest: add qtests for STM32 DMA
  2025-03-24 10:05 ` [PATCH 3/3] tests/qtest: add qtests for STM32 DMA Nikita Shubin
@ 2025-04-01 14:11   ` Fabiano Rosas
  0 siblings, 0 replies; 5+ messages in thread
From: Fabiano Rosas @ 2025-04-01 14:11 UTC (permalink / raw)
  To: Nikita Shubin, qemu-devel; +Cc: Nikita Shubin, Laurent Vivier, Paolo Bonzini

Nikita Shubin <nikita.shubin@maquefel.me> writes:

> From: Nikita Shubin <n.shubin@yadro.com>
>
> Signed-off-by: Nikita Shubin <n.shubin@yadro.com>
> ---
>  tests/qtest/meson.build      |   1 +
>  tests/qtest/stm32-dma-test.c | 415 +++++++++++++++++++++++++++++++++++
>  2 files changed, 416 insertions(+)
>  create mode 100644 tests/qtest/stm32-dma-test.c
>
> diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
> index 5a8c1f102c..6c45692f9d 100644
> --- a/tests/qtest/meson.build
> +++ b/tests/qtest/meson.build
> @@ -240,6 +240,7 @@ qtests_arm = \
>    (config_all_devices.has_key('CONFIG_TPM_TIS_I2C') ? ['tpm-tis-i2c-test'] : []) + \
>    (config_all_devices.has_key('CONFIG_VEXPRESS') ? ['test-arm-mptimer'] : []) + \
>    (config_all_devices.has_key('CONFIG_MICROBIT') ? ['microbit-test'] : []) + \
> +  (config_all_devices.has_key('CONFIG_STM32F100_SOC') ? ['stm32-dma-test'] : []) + \
>    (config_all_devices.has_key('CONFIG_STM32L4X5_SOC') ? qtests_stm32l4x5 : []) + \
>    (config_all_devices.has_key('CONFIG_FSI_APB2OPB_ASPEED') ? ['aspeed_fsi-test'] : []) + \
>    (config_all_devices.has_key('CONFIG_STM32L4X5_SOC') and
> diff --git a/tests/qtest/stm32-dma-test.c b/tests/qtest/stm32-dma-test.c
> new file mode 100644
> index 0000000000..74b81fa434
> --- /dev/null
> +++ b/tests/qtest/stm32-dma-test.c
> @@ -0,0 +1,415 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * QTest testcase for STM32 DMA engine.
> + *
> + * This includes STM32F1xxxx, STM32F2xxxx and GD32F30x
> + *
> + * Author: 2025 Nikita Shubin <n.shubin@yadro.com>
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qemu/bitops.h"
> +#include "libqtest-single.h"
> +#include "libqos/libqos.h"
> +
> +/* Offsets in stm32vldiscovery platform: */
> +#define DMA_BASE    0x40020000
> +#define SRAM_BASE   0x20000000
> +
> +/* Global interrupt flag */
> +#define DMA_ISR_GIF   BIT(0)
> +/* Full transfer finish */
> +#define DMA_ISR_TCIF  BIT(1)
> +/* Half transfer finish */
> +#define DMA_ISR_HTIF  BIT(2)
> +/* Transfer error */
> +#define DMA_ISR_TEIF  BIT(3)
> +
> +/* Used register/fields definitions */
> +#define DMA_CCR(idx)     (0x08 + 0x14 * idx)
> +#define DMA_CNDTR(idx)   (0x0C + 0x14 * idx)
> +#define DMA_CPAR(idx)    (0x10 + 0x14 * idx)
> +#define DMA_CMAR(idx)    (0x14 + 0x14 * idx)
> +
> +#define DMA_MAX_CHAN    7
> +
> +/* Register offsets for a dma chan0 within a dma block. */
> +#define DMA_CHAN(_idx, _irq)  \
> +    { \
> +        .ccr = DMA_CCR(_idx), \
> +        .cndrt = DMA_CNDTR(_idx), \
> +        .cpar = DMA_CPAR(_idx), \
> +        .cmar = DMA_CMAR(_idx), \
> +        .irq_line = _irq,\
> +    }
> +
> +typedef struct DMAChan {
> +    uint32_t ccr;
> +    uint32_t cndrt;
> +    uint32_t cpar;
> +    uint32_t cmar;
> +    uint8_t irq_line;
> +} DMAChan;
> +
> +const DMAChan dma_chans[] = {
> +    DMA_CHAN(0, 11),
> +    DMA_CHAN(1, 12),
> +    DMA_CHAN(2, 12),
> +    DMA_CHAN(3, 13),
> +    DMA_CHAN(4, 14),
> +    DMA_CHAN(5, 16),
> +    DMA_CHAN(6, 17),
> +};
> +
> +/* Register offsets for a dma within a dma block. */
> +typedef struct DMA {
> +    uint32_t base_addr;
> +    uint32_t isr;
> +    uint32_t ofcr;
> +} DMA;
> +
> +const DMA dma = {
> +    .base_addr = DMA_BASE,
> +    .isr = 0x00,
> +    .ofcr = 0x04,
> +};
> +
> +typedef struct TestData {
> +    QTestState *qts;
> +    const DMA *dma;
> +    const DMAChan *chans;
> +} TestData;
> +
> +#define NVIC_ISER 0xE000E100
> +#define NVIC_ISPR 0xE000E200
> +#define NVIC_ICPR 0xE000E280
> +
> +static void enable_nvic_irq(unsigned int n)
> +{
> +    writel(NVIC_ISER, 1 << n);
> +}
> +
> +static void unpend_nvic_irq(unsigned int n)
> +{
> +    writel(NVIC_ICPR, 1 << n);
> +}
> +
> +static bool check_nvic_pending(unsigned int n)
> +{
> +    return readl(NVIC_ISPR) & (1 << n);
> +}
> +
> +static uint32_t dma_read(const TestData *td, uint32_t offset)
> +{
> +    return qtest_readl(td->qts, td->dma->base_addr + offset);
> +}
> +
> +static void dma_write(const TestData *td, uint32_t offset, uint32_t value)
> +{
> +    qtest_writel(td->qts, td->dma->base_addr + offset, value);
> +}
> +
> +static void dma_write_ofcr(const TestData *td, uint32_t value)
> +{
> +    return dma_write(td, td->dma->ofcr, value);
> +}
> +
> +static uint32_t dma_read_isr(const TestData *td)
> +{
> +    return dma_read(td, td->dma->isr);
> +}
> +
> +static void dma_write_ccr(const TestData *td, uint8_t idx, uint32_t value)
> +{
> +    dma_write(td, td->chans[idx].ccr, value);
> +}
> +
> +static uint32_t dma_read_ccr(const TestData *td, uint8_t idx)
> +{
> +    return dma_read(td, td->chans[idx].ccr);
> +}
> +
> +static void dma_write_cndrt(const TestData *td, uint8_t idx, uint32_t value)
> +{
> +    dma_write(td, td->chans[idx].cndrt, value);
> +}
> +
> +static void dma_write_cpar(const TestData *td, uint8_t idx, uint32_t value)
> +{
> +    dma_write(td, td->chans[idx].cpar, value);
> +}
> +
> +static void dma_write_cmar(const TestData *td, uint8_t idx, uint32_t value)
> +{
> +    dma_write(td, td->chans[idx].cmar, value);
> +}
> +
> +static void test_m2m(gconstpointer test_data)
> +{
> +    const TestData *td = test_data;
> +    QTestState *s = td->qts;
> +    const uint32_t patt_len = 0xff;
> +    char *pattern_check = g_malloc(patt_len);
> +    char *pattern = g_malloc(patt_len);

These^ two should be g_autofree.

> +    uint8_t idx = 0;
> +    uint32_t val;
> +
> +    enable_nvic_irq(td->chans[idx].irq_line);
> +    qtest_irq_intercept_in(global_qtest, "/machine/soc/dma[0]");
> +
> +    /* write addr */
> +    dma_write_cpar(td, idx, SRAM_BASE);
> +    dma_write_cmar(td, idx, SRAM_BASE + patt_len);
> +
> +    /* enable increment and M2M */
> +    val = dma_read_ccr(td, idx);
> +    val |= BIT(1); /* TCIE */
> +    val |= BIT(6); /* PINC */
> +    val |= BIT(7); /* MINC */
> +    val |= BIT(14); /* M2M */
> +    dma_write_ccr(td, idx, val);
> +
> +    generate_pattern(pattern, patt_len, patt_len);
> +    qtest_memwrite(s, SRAM_BASE, pattern, patt_len);
> +
> +    dma_write_cndrt(td, idx, patt_len);
> +
> +    val |= BIT(0); /* enable channel */
> +    dma_write_ccr(td, idx, val);
> +
> +    qtest_memread(s, SRAM_BASE + patt_len, pattern_check, patt_len);
> +
> +    g_assert(memcmp(pattern, pattern_check, patt_len) == 0);
> +
> +    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
> +}
> +
> +typedef struct width_pattern {
> +    uint32_t src;
> +    uint8_t swidth;
> +    uint32_t dst;
> +    uint8_t dwidth;
> +} width_pattern;
> +
> +static void test_width(gconstpointer test_data)
> +{
> +    const width_pattern patterns[] = {
> +        { 0xb0,       1, 0xb0,       1 },
> +        { 0xb0,       1, 0x00b0,     2 },
> +        { 0xb0,       1, 0x000000b0, 4 },
> +        { 0xb1b0,     2, 0xb0,       1 },
> +        { 0xb1b0,     2, 0xb1b0,     2 },
> +        { 0xb1b0,     2, 0x0000b1b0, 4 },
> +        { 0xb3b2b1b0, 4, 0xb0,       1 },
> +        { 0xb3b2b1b0, 4, 0xb1b0,     2 },
> +        { 0xb3b2b1b0, 4, 0xb3b2b1b0, 4 },
> +    };
> +
> +    const TestData *td = test_data;
> +    QTestState *s = td->qts;
> +    const uint32_t patt = 0xffffffff;
> +    const uint32_t patt_len = 4;
> +    uint32_t dst;
> +    uint8_t idx = 0;
> +    uint32_t val;
> +
> +    qmp("{'execute':'system_reset' }");

These calls to qmp() all leak the response dict. You need to catch the
return value and unref.

    QDict *response;

    response = qmp("{'execute':'system_reset' }");
    qobject_unref(response);

> +
> +    /* write addr */
> +    dma_write_cpar(td, idx, SRAM_BASE);
> +    dma_write_cmar(td, idx, SRAM_BASE + patt_len);
> +
> +    /* enable increment and M2M */
> +    val = dma_read_ccr(td, idx);
> +    val |= BIT(6); /* PINC */
> +    val |= BIT(7); /* MINC */
> +    val |= BIT(14); /* M2M */
> +    dma_write_ccr(td, idx, val);
> +
> +    for (int i = 0; i < ARRAY_SIZE(patterns); i++) {
> +        /* fill destination and source with pattern */
> +        qtest_memwrite(s, SRAM_BASE, &patt, patt_len);
> +        qtest_memwrite(s, SRAM_BASE + patt_len, &patt, patt_len);
> +
> +        qtest_memwrite(s, SRAM_BASE, &patterns[i].src, patterns[i].swidth);
> +
> +        dma_write_cndrt(td, idx, 1);
> +        val |= BIT(0); /* enable channel */
> +        val = deposit32(val, 8, 2, patterns[i].swidth >> 1);
> +        val = deposit32(val, 10, 2, patterns[i].dwidth >> 1);
> +        dma_write_ccr(td, idx, val);
> +
> +        qtest_memread(s, SRAM_BASE + patt_len, &dst, patterns[i].dwidth);
> +
> +        g_assert(memcmp(&dst, &patterns[i].dst, patterns[i].dwidth) == 0);
> +
> +        /* disable chan */
> +        val &= ~BIT(0);
> +        dma_write_ccr(td, idx, val);
> +    }
> +}
> +
> +static void dma_set_irq(unsigned int idx, int num, int level)
> +{
> +    g_autofree char *name = g_strdup_printf("/machine/soc/dma[%u]",
> +                                            idx);
> +    qtest_set_irq_in(global_qtest, name, NULL, num, level);
> +}
> +
> +static void test_triggers(gconstpointer test_data)
> +{
> +    const TestData *td = test_data;
> +    QTestState *s = td->qts;
> +    const uint32_t patt = 0xffffffff;
> +    const uint32_t patt_len = 4;
> +    uint32_t dst;
> +    uint32_t val;
> +
> +    qmp("{'execute':'system_reset' }");
> +
> +    for (int i = 0; i < ARRAY_SIZE(dma_chans); i++) {
> +        qtest_memset(s, SRAM_BASE, 0, patt_len * 2);
> +        qtest_memwrite(s, SRAM_BASE, &patt, patt_len);
> +
> +        /* write addr */
> +        dma_write_cpar(td, i, SRAM_BASE);
> +        dma_write_cmar(td, i, SRAM_BASE + patt_len);
> +
> +        val = dma_read_ccr(td, i);
> +
> +        dma_write_cndrt(td, i, 1);
> +        val |= BIT(0); /* enable channel */
> +        val = deposit32(val, 8, 2, patt_len >> 1);
> +        val = deposit32(val, 10, 2, patt_len >> 1);
> +        dma_write_ccr(td, i, val);
> +
> +        dma_set_irq(0, i, 1);
> +
> +        qtest_memread(s, SRAM_BASE + patt_len, &dst, patt_len);
> +
> +        g_assert(memcmp(&dst, &patt, patt_len) == 0);
> +
> +        /* disable chan */
> +        val &= ~BIT(0);
> +        dma_write_ccr(td, i, val);
> +    }
> +}
> +
> +static void test_interrupts(gconstpointer test_data)
> +{
> +    const TestData *td = test_data;
> +    const uint32_t patt_len = 1024;
> +    uint8_t idx = 0;
> +    uint32_t val;
> +
> +    qmp("{'execute':'system_reset' }");
> +
> +    enable_nvic_irq(td->chans[idx].irq_line);
> +
> +    /* write addr */
> +    dma_write_cpar(td, idx, SRAM_BASE);
> +    dma_write_cmar(td, idx, SRAM_BASE + patt_len);
> +
> +    /* write counter */
> +    dma_write_cndrt(td, idx, 2);
> +
> +    /* enable increment and M2M */
> +    val = dma_read_ccr(td, idx);
> +    val |= BIT(0); /* EN */
> +    val |= BIT(1); /* TCIE */
> +    val |= BIT(2); /* HTIE */
> +    val |= BIT(3); /* TEIE */
> +    val |= BIT(6); /* PINC */
> +    val |= BIT(7); /* MINC */
> +    dma_write_ccr(td, idx, val);
> +
> +    /* Half-transfer */
> +    dma_set_irq(0, idx, 1);
> +    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
> +    val = dma_read_isr(td);
> +
> +    g_assert_true(val & DMA_ISR_GIF);
> +    g_assert_true(val & DMA_ISR_HTIF);
> +    unpend_nvic_irq(td->chans[idx].irq_line);
> +
> +    dma_write_ofcr(td, 0xffffffff);
> +    val = dma_read_isr(td);
> +    g_assert_false(val & DMA_ISR_GIF);
> +    g_assert_false(val & DMA_ISR_HTIF);
> +
> +    /* Full-transfer */
> +    dma_set_irq(0, idx, 1);
> +    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
> +    val = dma_read_isr(td);
> +
> +    g_assert_true(val & DMA_ISR_GIF);
> +    g_assert_true(val & DMA_ISR_HTIF);
> +    g_assert_true(val & DMA_ISR_TCIF);
> +    unpend_nvic_irq(td->chans[idx].irq_line);
> +
> +    dma_write_ofcr(td, 0xffffffff);
> +    val = dma_read_isr(td);
> +    g_assert_false(val & DMA_ISR_GIF);
> +    g_assert_false(val & DMA_ISR_HTIF);
> +    g_assert_false(val & DMA_ISR_TCIF);
> +
> +    /* Error-on-transfer */
> +    val = dma_read_ccr(td, idx);
> +    val &= ~BIT(0);
> +    dma_write_ccr(td, idx, val);
> +
> +    dma_write_cndrt(td, idx, 1);
> +    dma_write_cpar(td, idx, 0xffffffff);
> +
> +    val |= BIT(0);
> +    dma_write_ccr(td, idx, val);
> +
> +    dma_set_irq(0, idx, 1);
> +    g_assert_true(check_nvic_pending(td->chans[idx].irq_line));
> +    val = dma_read_isr(td);
> +
> +    g_assert_true(val & DMA_ISR_GIF);
> +    g_assert_true(val & DMA_ISR_TEIF);
> +    unpend_nvic_irq(td->chans[idx].irq_line);
> +
> +    dma_write_ofcr(td, 0xffffffff);
> +    val = dma_read_isr(td);
> +    g_assert_false(val & DMA_ISR_GIF);
> +    g_assert_false(val & DMA_ISR_TEIF);
> +}
> +
> +static void stm32_add_test(const char *name, const TestData *td,
> +                           GTestDataFunc fn)
> +{
> +    g_autofree char *full_name = g_strdup_printf(
> +        "stm32_dma/%s", name);
> +    qtest_add_data_func(full_name, td, fn);
> +}
> +
> +/* Convenience macro for adding a test with a predictable function name. */
> +#define add_test(name, td) stm32_add_test(#name, td, test_##name)
> +
> +int main(int argc, char **argv)
> +{
> +    TestData testdata;
> +    QTestState *s;
> +    int ret;
> +
> +    g_test_init(&argc, &argv, NULL);
> +    s = qtest_start("-machine stm32vldiscovery");
> +    g_test_set_nonfatal_assertions();
> +
> +    TestData *td = &testdata;
> +    td->qts = s;
> +    td->dma = &dma;
> +    td->chans = dma_chans;
> +    add_test(m2m, td);
> +    add_test(width, td);
> +    add_test(triggers, td);
> +    add_test(interrupts, td);
> +
> +    ret = g_test_run();
> +    qtest_end();
> +
> +    return ret;
> +}


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2025-04-01 14:12 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-03-24 10:05 [PATCH 0/3] Add STM32 DMA model Nikita Shubin
2025-03-24 10:05 ` [PATCH 1/3] hw/dma: Add STM32 platfrom DMA controller emulation Nikita Shubin
2025-03-24 10:05 ` [PATCH 2/3] hw/arm/stm32f100: Add DMA support for stm32f100 Nikita Shubin
2025-03-24 10:05 ` [PATCH 3/3] tests/qtest: add qtests for STM32 DMA Nikita Shubin
2025-04-01 14:11   ` Fabiano Rosas

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).