* [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash
2026-11-12 15:03 [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support ~mohammadodeh
@ 2026-09-27 7:08 ` ~mohammadodeh
2026-11-14 10:20 ` Samuel Tardieu
2026-09-27 7:09 ` [PATCH qemu 2/3] hw/ssi: Add STM32L4x5 QuadSPI controller ~mohammadodeh
` (2 subsequent siblings)
3 siblings, 1 reply; 6+ messages in thread
From: ~mohammadodeh @ 2026-09-27 7:08 UTC (permalink / raw)
To: qemu-devel
Cc: Alexandre Iooss, Pierrick Bouvier, Peter Maydell, Paolo Bonzini,
Samuel Tardieu, Alistair Francis, Kevin Wolf, Hanna Reitz,
Fabiano Rosas, Laurent Vivier, Philippe Mathieu-Daudé,
Markus Armbruster, Cédric Le Goater, Bernhard Beschow,
Gaurav Sharma, Jan Kiszka, Bin Meng, Shameer Kolothum, Jamin Lin,
Marc-André Lureau, qemu-arm, qemu-block
From: Mohammad Odeh <mohammad.odeh@exalt.ps>
Add the 64-Mbit Macronix MX25R6435F serial NOR flash model, its
SFDP data, and the 1-4-4 quad page-program command used by the
device.
Signed-off-by: Mohammad Odeh <zephyr@exalt.ps>
---
hw/block/m25p80.c | 5 +++++
hw/block/m25p80_sfdp.c | 20 ++++++++++++++++++++
hw/block/m25p80_sfdp.h | 1 +
3 files changed, 26 insertions(+)
diff --git a/hw/block/m25p80.c b/hw/block/m25p80.c
index baf798c6b5..9b931f5861 100644
--- a/hw/block/m25p80.c
+++ b/hw/block/m25p80.c
@@ -243,6 +243,8 @@ static const FlashPartInfo known_devices[] = {
{ INFO6("mx25l25635f", 0xc22019, 0xc22019, 64 << 10, 512,
ER_4K | ER_32K), .sfdp_read = m25p80_sfdp_mx25l25635f },
{ INFO("mx25l25655e", 0xc22619, 0, 64 << 10, 512, 0) },
+ { INFO("mx25r6435", 0xc22817, 0, 64 << 10, 128, ER_4K | ER_32K),
+ .sfdp_read = m25p80_sfdp_mx25r6435 },
{ INFO("mx66l51235f", 0xc2201a, 0, 64 << 10, 1024, ER_4K | ER_32K) },
{ INFO("mx66u51235f", 0xc2253a, 0, 64 << 10, 1024, ER_4K | ER_32K) },
{ INFO("mx66u1g45g", 0xc2253b, 0, 64 << 10, 2048, ER_4K | ER_32K) },
@@ -405,6 +407,7 @@ typedef enum {
PP = 0x02,
PP4 = 0x12,
PP4_4 = 0x3e,
+ QPP_1_4_4 = 0x38,
DPP = 0xa2,
QPP = 0x32,
QPP_4 = 0x34,
@@ -759,6 +762,7 @@ static void complete_collecting_data(Flash *s)
switch (s->cmd_in_progress) {
case DPP:
+ case QPP_1_4_4:
case QPP:
case QPP_4:
case PP:
@@ -1255,6 +1259,7 @@ static void decode_new_cmd(Flash *s, uint32_t value)
"QIO mode\n", s->cmd_in_progress);
}
break;
+ case QPP_1_4_4:
case QPP:
case QPP_4:
case PP4_4:
diff --git a/hw/block/m25p80_sfdp.c b/hw/block/m25p80_sfdp.c
index 8ac603e212..29fc6aa5af 100644
--- a/hw/block/m25p80_sfdp.c
+++ b/hw/block/m25p80_sfdp.c
@@ -135,6 +135,26 @@ define_sfdp_read(mt35xu02g);
* Matronix
*/
+static const uint8_t sfdp_mx25r6435[] = {
+ 0x53, 0x46, 0x44, 0x50, 0x06, 0x01, 0x00, 0xff,
+ 0x00, 0x06, 0x01, 0x10, 0x10, 0x00, 0x00, 0xff,
+ 0xe5, 0x20, 0xf1, 0xff, 0xff, 0xff, 0xff, 0x03,
+ 0x44, 0xeb, 0x08, 0x6b, 0x08, 0x3b, 0x04, 0xbb,
+ 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff,
+ 0xff, 0xff, 0x00, 0xff, 0x0c, 0x20, 0x0f, 0x52,
+ 0x10, 0xd8, 0x00, 0xff, 0x23, 0x72, 0xf5, 0x00,
+ 0x82, 0xed, 0x04, 0xcc, 0x44, 0x83, 0x68, 0x44,
+ 0x30, 0xb0, 0x30, 0xb0, 0xf7, 0xc4, 0xd5, 0x5c,
+ 0x00, 0xbe, 0x29, 0xff, 0xf0, 0xd0, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+};
+define_sfdp_read(mx25r6435)
+
/* mx25l25635e. No 4B opcodes */
static const uint8_t sfdp_mx25l25635e[] = {
0x53, 0x46, 0x44, 0x50, 0x00, 0x01, 0x01, 0xff,
diff --git a/hw/block/m25p80_sfdp.h b/hw/block/m25p80_sfdp.h
index 61544fb7cc..da3e7d74bc 100644
--- a/hw/block/m25p80_sfdp.h
+++ b/hw/block/m25p80_sfdp.h
@@ -19,6 +19,7 @@ uint8_t m25p80_sfdp_n25q256a(uint32_t addr);
uint8_t m25p80_sfdp_mt35xu01g(uint32_t addr);
uint8_t m25p80_sfdp_mt35xu02g(uint32_t addr);
+uint8_t m25p80_sfdp_mx25r6435(uint32_t addr);
uint8_t m25p80_sfdp_mx25l25635e(uint32_t addr);
uint8_t m25p80_sfdp_mx25l25635f(uint32_t addr);
uint8_t m25p80_sfdp_mx66l1g45g(uint32_t addr);
--
2.49.1
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH qemu 2/3] hw/ssi: Add STM32L4x5 QuadSPI controller
2026-11-12 15:03 [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support ~mohammadodeh
2026-09-27 7:08 ` [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash ~mohammadodeh
@ 2026-09-27 7:09 ` ~mohammadodeh
2026-09-27 7:32 ` [PATCH qemu 3/3] hw/arm: Add STM32L496G-DISCO machine ~mohammadodeh
2026-11-14 10:22 ` [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support Samuel Tardieu
3 siblings, 0 replies; 6+ messages in thread
From: ~mohammadodeh @ 2026-09-27 7:09 UTC (permalink / raw)
To: qemu-devel
Cc: Alexandre Iooss, Pierrick Bouvier, Peter Maydell, Paolo Bonzini,
Samuel Tardieu, Alistair Francis, Kevin Wolf, Hanna Reitz,
Fabiano Rosas, Laurent Vivier, Philippe Mathieu-Daudé,
Markus Armbruster, Cédric Le Goater, Bernhard Beschow,
Gaurav Sharma, Jan Kiszka, Bin Meng, Shameer Kolothum, Jamin Lin,
Marc-André Lureau, qemu-arm, qemu-block
From: Mohammad Odeh <mohammad.odeh@exalt.ps>
Model the STM32L4x5 QuadSPI controller indirect read and write modes.
Wire its clock, interrupt, and SSI bus into the STM32L4x5 SoC.
Automatic polling, memory-mapped mode, and DMA are not implemented.
Signed-off-by: Mohammad Odeh <zephyr@exalt.ps>
---
MAINTAINERS | 1 +
hw/arm/Kconfig | 1 +
hw/arm/stm32l4x5_soc.c | 17 +
hw/ssi/Kconfig | 4 +
hw/ssi/meson.build | 1 +
hw/ssi/stm32l4x5_quadspi.c | 680 +++++++++++++++++++++++++++++
include/hw/arm/stm32l4x5_soc.h | 2 +
include/hw/ssi/stm32l4x5_quadspi.h | 51 +++
8 files changed, 757 insertions(+)
create mode 100644 hw/ssi/stm32l4x5_quadspi.c
create mode 100644 include/hw/ssi/stm32l4x5_quadspi.h
diff --git a/MAINTAINERS b/MAINTAINERS
index fa0c6d0c9b..7a820703f8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1119,6 +1119,7 @@ F: hw/misc/stm32l4x5_exti.c
F: hw/misc/stm32l4x5_syscfg.c
F: hw/misc/stm32l4x5_rcc.c
F: hw/gpio/stm32l4x5_gpio.c
+F: hw/ssi/stm32l4x5_quadspi.c
F: include/hw/*/stm32l4x5_*.h
F: tests/qtest/stm32l4x5*
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index e9f796f718..94a01c412b 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -398,6 +398,7 @@ config STM32L4X5_SOC
select STM32L4X5_RCC
select STM32L4X5_GPIO
select STM32L4X5_USART
+ select STM32L4X5_QUADSPI
config XLNX_ZYNQMP_ARM
bool
diff --git a/hw/arm/stm32l4x5_soc.c b/hw/arm/stm32l4x5_soc.c
index 39b8cdf97d..d91d18e88b 100644
--- a/hw/arm/stm32l4x5_soc.c
+++ b/hw/arm/stm32l4x5_soc.c
@@ -81,6 +81,9 @@ static const int exti_irq[NUM_EXTI_IRQ] = {
#define RCC_BASE_ADDRESS 0x40021000
#define RCC_IRQ 5
+#define QUADSPI_BASE_ADDRESS 0xa0001000
+#define QUADSPI_IRQ 71
+
#define EXTI_USART1_IRQ 26
#define EXTI_UART4_IRQ 29
#define EXTI_LPUART1_IRQ 31
@@ -144,6 +147,8 @@ static void stm32l4x5_soc_initfn(Object *obj)
}
object_initialize_child(obj, "syscfg", &s->syscfg, TYPE_STM32L4X5_SYSCFG);
object_initialize_child(obj, "rcc", &s->rcc, TYPE_STM32L4X5_RCC);
+ object_initialize_child(obj, "quadspi", &s->quadspi,
+ TYPE_STM32L4X5_QUADSPI);
for (unsigned i = 0; i < NUM_GPIOS; i++) {
g_autofree char *name = g_strdup_printf("gpio%c", 'a' + i);
@@ -313,6 +318,18 @@ static void stm32l4x5_soc_realize(DeviceState *dev_soc, Error **errp)
sysbus_mmio_map(busdev, 0, RCC_BASE_ADDRESS);
sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, RCC_IRQ));
+ /* QuadSPI controller */
+ dev = DEVICE(&s->quadspi);
+ qdev_connect_clock_in(dev, "clk",
+ qdev_get_clock_out(DEVICE(&s->rcc), "qspi-out"));
+ busdev = SYS_BUS_DEVICE(dev);
+ if (!sysbus_realize(busdev, errp)) {
+ return;
+ }
+ sysbus_mmio_map(busdev, 0, QUADSPI_BASE_ADDRESS);
+ sysbus_connect_irq(busdev, 0,
+ qdev_get_gpio_in(armv7m, QUADSPI_IRQ));
+
/* USART devices */
for (int i = 0; i < STM_NUM_USARTS; i++) {
g_autofree char *name = g_strdup_printf("usart%d-out", i + 1);
diff --git a/hw/ssi/Kconfig b/hw/ssi/Kconfig
index d477d20e9b..1b79da6d90 100644
--- a/hw/ssi/Kconfig
+++ b/hw/ssi/Kconfig
@@ -21,6 +21,10 @@ config STM32F2XX_SPI
bool
select SSI
+config STM32L4X5_QUADSPI
+ bool
+ select SSI
+
config BCM2835_SPI
bool
select SSI
diff --git a/hw/ssi/meson.build b/hw/ssi/meson.build
index f334e48e2f..ce9511b5df 100644
--- a/hw/ssi/meson.build
+++ b/hw/ssi/meson.build
@@ -6,6 +6,7 @@ system_ss.add(when: 'CONFIG_PL022', if_true: files('pl022.c'))
system_ss.add(when: 'CONFIG_SIFIVE_SPI', if_true: files('sifive_spi.c'))
system_ss.add(when: 'CONFIG_SSI', if_true: files('ssi.c'))
system_ss.add(when: 'CONFIG_STM32F2XX_SPI', if_true: files('stm32f2xx_spi.c'))
+system_ss.add(when: 'CONFIG_STM32L4X5_QUADSPI', if_true: files('stm32l4x5_quadspi.c'))
system_ss.add(when: 'CONFIG_XILINX_SPI', if_true: files('xilinx_spi.c'))
system_ss.add(when: 'CONFIG_XILINX_SPIPS', if_true: files('xilinx_spips.c'))
system_ss.add(when: 'CONFIG_XLNX_VERSAL', if_true: files('xlnx-versal-ospi.c'))
diff --git a/hw/ssi/stm32l4x5_quadspi.c b/hw/ssi/stm32l4x5_quadspi.c
new file mode 100644
index 0000000000..cb3e7eb665
--- /dev/null
+++ b/hw/ssi/stm32l4x5_quadspi.c
@@ -0,0 +1,680 @@
+/*
+ * STM32L4x5 QuadSPI controller
+ *
+ * Copyright (c) 2026 EXALT Technologies
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+/*
+ * The reference used is the STMicroelectronics RM0351 reference manual for
+ * STM32L47xxx, STM32L48xxx, STM32L49xxx and STM32L4Axxx MCUs.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/bitops.h"
+#include "qemu/log.h"
+#include "qapi/error.h"
+#include "hw/core/qdev-clock.h"
+#include "hw/core/irq.h"
+#include "hw/ssi/stm32l4x5_quadspi.h"
+#include "migration/vmstate.h"
+
+#define QUADSPI_MMIO_SIZE 0x400
+#define QUADSPI_FIFO_SIZE 16
+
+#define QUADSPI_CR 0x00
+#define QUADSPI_DCR 0x04
+#define QUADSPI_SR 0x08
+#define QUADSPI_FCR 0x0c
+#define QUADSPI_DLR 0x10
+#define QUADSPI_CCR 0x14
+#define QUADSPI_AR 0x18
+#define QUADSPI_ABR 0x1c
+#define QUADSPI_DR 0x20
+#define QUADSPI_PSMKR 0x24
+#define QUADSPI_PSMAR 0x28
+#define QUADSPI_PIR 0x2c
+#define QUADSPI_LPTR 0x30
+
+#define CR_EN BIT(0)
+#define CR_ABORT BIT(1)
+#define CR_DFM BIT(6)
+#define CR_FTHRES_SHIFT 8
+#define CR_FTHRES MAKE_64BIT_MASK(8, 4)
+#define CR_TEIE BIT(16)
+#define CR_TCIE BIT(17)
+#define CR_FTIE BIT(18)
+#define CR_SMIE BIT(19)
+#define CR_TOIE BIT(20)
+
+#define SR_TEF BIT(0)
+#define SR_TCF BIT(1)
+#define SR_FTF BIT(2)
+#define SR_SMF BIT(3)
+#define SR_TOF BIT(4)
+#define SR_BUSY BIT(5)
+#define SR_FLEVEL MAKE_64BIT_MASK(8, 5)
+#define SR_FLEVEL_SHIFT 8
+
+#define CCR_INSTRUCTION MAKE_64BIT_MASK(0, 8)
+#define CCR_IMODE MAKE_64BIT_MASK(8, 2)
+#define CCR_ADMODE MAKE_64BIT_MASK(10, 2)
+#define CCR_ADSIZE MAKE_64BIT_MASK(12, 2)
+#define CCR_ABMODE MAKE_64BIT_MASK(14, 2)
+#define CCR_ABSIZE MAKE_64BIT_MASK(16, 2)
+#define CCR_DCYC MAKE_64BIT_MASK(18, 5)
+#define CCR_DMODE MAKE_64BIT_MASK(24, 2)
+#define CCR_FMODE MAKE_64BIT_MASK(26, 2)
+#define CCR_SIOO BIT(28)
+#define CCR_DDRM BIT(31)
+
+#define CCR_IMODE_SHIFT 8
+#define CCR_ADMODE_SHIFT 10
+#define CCR_ADSIZE_SHIFT 12
+#define CCR_ABMODE_SHIFT 14
+#define CCR_ABSIZE_SHIFT 16
+#define CCR_DCYC_SHIFT 18
+#define CCR_DMODE_SHIFT 24
+#define CCR_FMODE_SHIFT 26
+
+enum QuadSpiFunctionalMode {
+ QUADSPI_INDIRECT_WRITE,
+ QUADSPI_INDIRECT_READ,
+ QUADSPI_AUTO_POLLING,
+ QUADSPI_MEMORY_MAPPED,
+};
+
+static unsigned int stm32l4x5_quadspi_mode_width(uint32_t mode)
+{
+ static const unsigned int widths[] = { 0, 1, 2, 4 };
+
+ return widths[mode & 3];
+}
+
+static unsigned int stm32l4x5_quadspi_fmode(Stm32l4x5QuadSpiState *s)
+{
+ return (s->ccr & CCR_FMODE) >> CCR_FMODE_SHIFT;
+}
+
+static void stm32l4x5_quadspi_update_irq(Stm32l4x5QuadSpiState *s)
+{
+ bool level = ((s->sr & SR_TEF) && (s->cr & CR_TEIE)) ||
+ ((s->sr & SR_TCF) && (s->cr & CR_TCIE)) ||
+ ((s->sr & SR_FTF) && (s->cr & CR_FTIE)) ||
+ ((s->sr & SR_SMF) && (s->cr & CR_SMIE)) ||
+ ((s->sr & SR_TOF) && (s->cr & CR_TOIE));
+
+ qemu_set_irq(s->irq, level);
+}
+
+static void stm32l4x5_quadspi_update_fifo(Stm32l4x5QuadSpiState *s)
+{
+ unsigned int level = fifo8_num_used(&s->fifo);
+ unsigned int threshold = ((s->cr & CR_FTHRES) >> CR_FTHRES_SHIFT) + 1;
+ unsigned int mode = stm32l4x5_quadspi_fmode(s);
+
+ s->sr &= ~(SR_FLEVEL | SR_FTF | SR_BUSY);
+ s->sr |= level << SR_FLEVEL_SHIFT;
+ if (s->transaction_active ||
+ (mode == QUADSPI_INDIRECT_READ && level)) {
+ s->sr |= SR_BUSY;
+ }
+ if (s->cr & CR_EN) {
+ if (mode == QUADSPI_INDIRECT_READ &&
+ (level >= threshold ||
+ (!s->remaining && !s->indefinite && level))) {
+ s->sr |= SR_FTF;
+ } else if (mode == QUADSPI_INDIRECT_WRITE &&
+ fifo8_num_free(&s->fifo) >= threshold) {
+ s->sr |= SR_FTF;
+ }
+ }
+ stm32l4x5_quadspi_update_irq(s);
+}
+
+static void stm32l4x5_quadspi_select(Stm32l4x5QuadSpiState *s)
+{
+ if (!s->transaction_active) {
+ qemu_set_irq(s->cs, 0);
+ s->transaction_active = true;
+ s->sr |= SR_BUSY;
+ }
+}
+
+static void stm32l4x5_quadspi_deselect(Stm32l4x5QuadSpiState *s)
+{
+ if (s->transaction_active) {
+ qemu_set_irq(s->cs, 1);
+ s->transaction_active = false;
+ }
+ s->remaining = 0;
+ s->indefinite = false;
+ s->write_pending = false;
+}
+
+static void stm32l4x5_quadspi_transfer_value(Stm32l4x5QuadSpiState *s,
+ uint32_t value,
+ unsigned int count)
+{
+ for (int i = count - 1; i >= 0; i--) {
+ ssi_transfer(s->ssi, extract32(value, i * 8, 8));
+ }
+}
+
+static void stm32l4x5_quadspi_send_prefix(Stm32l4x5QuadSpiState *s)
+{
+ unsigned int imode = (s->ccr & CCR_IMODE) >> CCR_IMODE_SHIFT;
+ unsigned int admode = (s->ccr & CCR_ADMODE) >> CCR_ADMODE_SHIFT;
+ unsigned int abmode = (s->ccr & CCR_ABMODE) >> CCR_ABMODE_SHIFT;
+ unsigned int dmode = (s->ccr & CCR_DMODE) >> CCR_DMODE_SHIFT;
+ unsigned int dummy_cycles = (s->ccr & CCR_DCYC) >> CCR_DCYC_SHIFT;
+ unsigned int dummy_width;
+ unsigned int dummy_bytes;
+
+ stm32l4x5_quadspi_select(s);
+
+ if (imode) {
+ ssi_transfer(s->ssi, s->ccr & CCR_INSTRUCTION);
+ }
+ if (admode) {
+ unsigned int address_bytes =
+ ((s->ccr & CCR_ADSIZE) >> CCR_ADSIZE_SHIFT) + 1;
+
+ stm32l4x5_quadspi_transfer_value(s, s->ar, address_bytes);
+ }
+ if (abmode) {
+ unsigned int alternate_bytes =
+ ((s->ccr & CCR_ABSIZE) >> CCR_ABSIZE_SHIFT) + 1;
+
+ stm32l4x5_quadspi_transfer_value(s, s->abr, alternate_bytes);
+ }
+
+ dummy_width = stm32l4x5_quadspi_mode_width(admode ? admode : dmode);
+ dummy_bytes = DIV_ROUND_UP(dummy_cycles * dummy_width, 8);
+ for (unsigned int i = 0; i < dummy_bytes; i++) {
+ ssi_transfer(s->ssi, 0);
+ }
+}
+
+static void stm32l4x5_quadspi_complete(Stm32l4x5QuadSpiState *s)
+{
+ stm32l4x5_quadspi_deselect(s);
+ s->rx_stalled = false;
+ if (stm32l4x5_quadspi_fmode(s) == QUADSPI_INDIRECT_WRITE) {
+ fifo8_reset(&s->fifo);
+ }
+ s->sr |= SR_TCF;
+ stm32l4x5_quadspi_update_fifo(s);
+}
+
+static bool stm32l4x5_quadspi_prepare_data(Stm32l4x5QuadSpiState *s)
+{
+ unsigned int fsize = extract32(s->dcr, 16, 5);
+ uint64_t flash_size = 1ULL << (fsize + 1);
+ uint64_t address = (s->ccr & CCR_ADMODE) ? s->ar : 0;
+ bool has_data = s->ccr & CCR_DMODE;
+
+ fifo8_reset(&s->fifo);
+ s->rx_stalled = false;
+ s->write_pending = false;
+ s->indefinite = has_data && s->dlr == UINT32_MAX && fsize == 31;
+ s->remaining = has_data ? (uint64_t)s->dlr + 1 : 0;
+ s->sr &= ~SR_TCF;
+ if ((s->ccr & CCR_ADMODE) &&
+ (address >= flash_size ||
+ (has_data && s->dlr != UINT32_MAX &&
+ s->remaining > flash_size - address))) {
+ s->remaining = 0;
+ s->indefinite = false;
+ s->sr |= SR_TEF;
+ stm32l4x5_quadspi_update_fifo(s);
+ return false;
+ }
+ if (has_data && s->dlr == UINT32_MAX) {
+ s->remaining = s->indefinite ? 0 : flash_size - address;
+ }
+ stm32l4x5_quadspi_update_fifo(s);
+ return true;
+}
+
+static bool stm32l4x5_quadspi_indirect(Stm32l4x5QuadSpiState *s)
+{
+ if (stm32l4x5_quadspi_fmode(s) >= QUADSPI_AUTO_POLLING ||
+ (s->ccr & (CCR_SIOO | CCR_DDRM)) || (s->cr & CR_DFM)) {
+ qemu_log_mask(LOG_UNIMP,
+ "%s: polling, mapped, SIOO, DDR, or dual flash mode\n",
+ TYPE_STM32L4X5_QUADSPI);
+ return false;
+ }
+ return true;
+}
+
+static void stm32l4x5_quadspi_fill_fifo(Stm32l4x5QuadSpiState *s)
+{
+ if (!(s->cr & CR_EN) || !s->transaction_active) {
+ return;
+ }
+ /* After a full FIFO, reception resumes when FLEVEL is at most 11. */
+ if (s->rx_stalled && fifo8_num_used(&s->fifo) > 11) {
+ return;
+ }
+ s->rx_stalled = false;
+ while ((s->remaining || s->indefinite) && !fifo8_is_full(&s->fifo)) {
+ fifo8_push(&s->fifo, ssi_transfer(s->ssi, 0));
+ if (!s->indefinite) {
+ s->remaining--;
+ }
+ }
+ if (!s->remaining && !s->indefinite) {
+ stm32l4x5_quadspi_complete(s);
+ } else {
+ s->rx_stalled = true;
+ stm32l4x5_quadspi_update_fifo(s);
+ }
+}
+
+static void stm32l4x5_quadspi_start_read(Stm32l4x5QuadSpiState *s)
+{
+ if (!stm32l4x5_quadspi_prepare_data(s)) {
+ return;
+ }
+ stm32l4x5_quadspi_send_prefix(s);
+ stm32l4x5_quadspi_fill_fifo(s);
+}
+
+static void stm32l4x5_quadspi_execute_command(Stm32l4x5QuadSpiState *s)
+{
+ if (!stm32l4x5_quadspi_prepare_data(s)) {
+ return;
+ }
+ stm32l4x5_quadspi_send_prefix(s);
+ stm32l4x5_quadspi_complete(s);
+}
+
+static uint64_t stm32l4x5_quadspi_read_register(void *opaque, hwaddr addr,
+ unsigned int size)
+{
+ Stm32l4x5QuadSpiState *s = opaque;
+ uint32_t value = 0;
+
+ switch (addr) {
+ case QUADSPI_CR:
+ return s->cr;
+ case QUADSPI_DCR:
+ return s->dcr;
+ case QUADSPI_SR:
+ return s->sr;
+ case QUADSPI_FCR:
+ return 0;
+ case QUADSPI_DLR:
+ return s->dlr;
+ case QUADSPI_CCR:
+ return s->ccr;
+ case QUADSPI_AR:
+ return s->ar;
+ case QUADSPI_ABR:
+ return s->abr;
+ case QUADSPI_DR:
+ if (!(s->cr & CR_EN) ||
+ stm32l4x5_quadspi_fmode(s) != QUADSPI_INDIRECT_READ) {
+ return 0;
+ }
+ for (unsigned int i = 0; i < size && !fifo8_is_empty(&s->fifo); i++) {
+ value |= (uint32_t)fifo8_pop(&s->fifo) << (i * 8);
+ }
+ s->dr = value;
+ stm32l4x5_quadspi_fill_fifo(s);
+ stm32l4x5_quadspi_update_fifo(s);
+ return value;
+ case QUADSPI_PSMKR:
+ return s->psmkr;
+ case QUADSPI_PSMAR:
+ return s->psmar;
+ case QUADSPI_PIR:
+ return s->pir;
+ case QUADSPI_LPTR:
+ return s->lptr;
+ default:
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "%s: bad read offset 0x%" HWADDR_PRIx "\n",
+ TYPE_STM32L4X5_QUADSPI, addr);
+ return 0;
+ }
+}
+
+static void stm32l4x5_quadspi_write_data(Stm32l4x5QuadSpiState *s,
+ uint32_t value,
+ unsigned int size)
+{
+ if (!(s->cr & CR_EN) ||
+ stm32l4x5_quadspi_fmode(s) != QUADSPI_INDIRECT_WRITE ||
+ (!s->write_pending && !s->remaining && !s->indefinite)) {
+ return;
+ }
+ if (s->write_pending) {
+ if (!stm32l4x5_quadspi_indirect(s) ||
+ !stm32l4x5_quadspi_prepare_data(s)) {
+ s->write_pending = false;
+ return;
+ }
+ }
+ if (fifo8_num_free(&s->fifo) < size) {
+ qemu_log_mask(LOG_GUEST_ERROR, "%s: write to full FIFO\n",
+ TYPE_STM32L4X5_QUADSPI);
+ return;
+ }
+ for (unsigned int i = 0; i < size; i++) {
+ fifo8_push(&s->fifo, extract32(value, i * 8, 8));
+ }
+ s->dr = value;
+ if (!s->transaction_active) {
+ stm32l4x5_quadspi_send_prefix(s);
+ }
+
+ while ((s->remaining || s->indefinite) && !fifo8_is_empty(&s->fifo)) {
+ ssi_transfer(s->ssi, fifo8_pop(&s->fifo));
+ if (!s->indefinite) {
+ s->remaining--;
+ }
+ }
+
+ if (!s->remaining && !s->indefinite) {
+ stm32l4x5_quadspi_complete(s);
+ } else {
+ stm32l4x5_quadspi_update_fifo(s);
+ }
+}
+
+static void stm32l4x5_quadspi_abort(Stm32l4x5QuadSpiState *s)
+{
+ stm32l4x5_quadspi_deselect(s);
+ fifo8_reset(&s->fifo);
+ s->rx_stalled = false;
+ s->cr &= ~CR_ABORT;
+ s->sr |= SR_TCF;
+ stm32l4x5_quadspi_update_fifo(s);
+}
+
+static void stm32l4x5_quadspi_write_register(void *opaque, hwaddr addr,
+ uint64_t value, unsigned int size)
+{
+ Stm32l4x5QuadSpiState *s = opaque;
+
+ switch (addr) {
+ case QUADSPI_CR:
+ if (s->sr & SR_BUSY) {
+ uint32_t mutable = CR_EN | CR_ABORT | CR_TEIE | CR_TCIE |
+ CR_FTIE | CR_SMIE | CR_TOIE;
+ value = (s->cr & ~mutable) | (value & mutable);
+ }
+ s->cr = value;
+ if (s->cr & CR_ABORT) {
+ stm32l4x5_quadspi_abort(s);
+ } else {
+ if (!(s->cr & CR_EN)) {
+ /* Disabling is not a reset or a transfer-complete event. */
+ stm32l4x5_quadspi_deselect(s);
+ fifo8_reset(&s->fifo);
+ s->rx_stalled = false;
+ }
+ stm32l4x5_quadspi_update_fifo(s);
+ }
+ return;
+ case QUADSPI_DCR:
+ s->dcr = value;
+ return;
+ case QUADSPI_SR:
+ return;
+ case QUADSPI_FCR:
+ s->sr &= ~(value & (SR_TEF | SR_TCF | SR_SMF | SR_TOF));
+ stm32l4x5_quadspi_update_irq(s);
+ return;
+ case QUADSPI_DLR:
+ s->dlr = value;
+ return;
+ case QUADSPI_CCR:
+ s->ccr = value;
+ s->write_pending = false;
+ if (!(s->cr & CR_EN) || !stm32l4x5_quadspi_indirect(s)) {
+ return;
+ }
+ if (((s->ccr & CCR_DMODE) == 0) &&
+ ((s->ccr & CCR_ADMODE) == 0) &&
+ ((s->ccr & CCR_IMODE) != 0)) {
+ stm32l4x5_quadspi_execute_command(s);
+ } else if ((s->ccr & CCR_DMODE) &&
+ stm32l4x5_quadspi_fmode(s) == QUADSPI_INDIRECT_WRITE) {
+ s->write_pending = true;
+ stm32l4x5_quadspi_update_fifo(s);
+ } else if ((s->ccr & CCR_DMODE) &&
+ !(s->ccr & CCR_ADMODE) &&
+ stm32l4x5_quadspi_fmode(s) == QUADSPI_INDIRECT_READ) {
+ stm32l4x5_quadspi_start_read(s);
+ }
+ return;
+ case QUADSPI_AR:
+ s->ar = value;
+ if (!(s->cr & CR_EN) || !stm32l4x5_quadspi_indirect(s)) {
+ return;
+ }
+ if ((s->ccr & CCR_DMODE) && (s->ccr & CCR_ADMODE) &&
+ stm32l4x5_quadspi_fmode(s) == QUADSPI_INDIRECT_READ) {
+ stm32l4x5_quadspi_start_read(s);
+ } else if (((s->ccr & CCR_DMODE) == 0) &&
+ ((s->ccr & CCR_ADMODE) != 0)) {
+ stm32l4x5_quadspi_execute_command(s);
+ }
+ return;
+ case QUADSPI_ABR:
+ s->abr = value;
+ return;
+ case QUADSPI_DR:
+ stm32l4x5_quadspi_write_data(s, value, size);
+ return;
+ case QUADSPI_PSMKR:
+ s->psmkr = value;
+ return;
+ case QUADSPI_PSMAR:
+ s->psmar = value;
+ return;
+ case QUADSPI_PIR:
+ s->pir = value;
+ return;
+ case QUADSPI_LPTR:
+ s->lptr = value;
+ return;
+ default:
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "%s: bad write offset 0x%" HWADDR_PRIx "\n",
+ TYPE_STM32L4X5_QUADSPI, addr);
+ }
+}
+
+static uint64_t stm32l4x5_quadspi_read(void *opaque, hwaddr addr,
+ unsigned int size)
+{
+ uint64_t value;
+
+ if ((addr & ~3ULL) == QUADSPI_DR) {
+ /* DR access size determines the number of FIFO bytes consumed. */
+ return stm32l4x5_quadspi_read_register(opaque, addr, size);
+ }
+ value = stm32l4x5_quadspi_read_register(opaque, addr & ~3ULL, 4);
+ return extract32(value, (addr & 3) * 8, size * 8);
+}
+
+static void stm32l4x5_quadspi_write(void *opaque, hwaddr addr,
+ uint64_t value, unsigned int size)
+{
+ Stm32l4x5QuadSpiState *s = opaque;
+ unsigned int shift = (addr & 3) * 8;
+ hwaddr reg = addr & ~3ULL;
+
+ if ((s->sr & SR_BUSY) && reg != QUADSPI_CR &&
+ reg != QUADSPI_FCR && reg != QUADSPI_SR && reg != QUADSPI_DR) {
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "%s: configuration write while busy\n",
+ TYPE_STM32L4X5_QUADSPI);
+ return;
+ }
+ if (reg == QUADSPI_FCR) {
+ value <<= shift;
+ } else if (reg != QUADSPI_DR && size != 4) {
+ uint32_t mask = MAKE_64BIT_MASK(shift, size * 8);
+ uint32_t old = stm32l4x5_quadspi_read_register(s, reg, 4);
+
+ value = (old & ~mask) | ((value << shift) & mask);
+ }
+ stm32l4x5_quadspi_write_register(s, reg, value, size);
+}
+
+static bool stm32l4x5_quadspi_accepts(void *opaque, hwaddr addr,
+ unsigned int size, bool is_write,
+ MemTxAttrs attrs)
+{
+ return (size == 1 || size == 2 || size == 4) &&
+ ((addr & ~3ULL) != QUADSPI_DR || addr == QUADSPI_DR);
+}
+
+static const MemoryRegionOps stm32l4x5_quadspi_ops = {
+ .read = stm32l4x5_quadspi_read,
+ .write = stm32l4x5_quadspi_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .impl.min_access_size = 1,
+ .impl.max_access_size = 4,
+ .valid.min_access_size = 1,
+ .valid.max_access_size = 4,
+ .valid.unaligned = false,
+ .valid.accepts = stm32l4x5_quadspi_accepts,
+};
+
+static void stm32l4x5_quadspi_hold_reset(Object *obj, ResetType type)
+{
+ Stm32l4x5QuadSpiState *s = STM32L4X5_QUADSPI(obj);
+
+ s->cr = 0;
+ s->dcr = 0;
+ s->sr = 0;
+ s->dlr = 0;
+ s->ccr = 0;
+ s->ar = 0;
+ s->abr = 0;
+ s->dr = 0;
+ s->psmkr = 0;
+ s->psmar = 0;
+ s->pir = 0;
+ s->lptr = 0;
+ s->remaining = 0;
+ s->transaction_active = false;
+ s->rx_stalled = false;
+ s->indefinite = false;
+ s->write_pending = false;
+ fifo8_reset(&s->fifo);
+ qemu_set_irq(s->cs, 1);
+ qemu_irq_lower(s->irq);
+}
+
+static void stm32l4x5_quadspi_init(Object *obj)
+{
+ Stm32l4x5QuadSpiState *s = STM32L4X5_QUADSPI(obj);
+ DeviceState *dev = DEVICE(obj);
+
+ fifo8_create(&s->fifo, QUADSPI_FIFO_SIZE);
+ memory_region_init_io(&s->mmio, obj, &stm32l4x5_quadspi_ops, s,
+ TYPE_STM32L4X5_QUADSPI, QUADSPI_MMIO_SIZE);
+ sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
+ sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);
+ qdev_init_gpio_out_named(dev, &s->cs, "cs", 1);
+ s->ssi = ssi_create_bus(dev, "ssi");
+ s->clk = qdev_init_clock_in(dev, "clk", NULL, s, 0);
+}
+
+static void stm32l4x5_quadspi_finalize(Object *obj)
+{
+ Stm32l4x5QuadSpiState *s = STM32L4X5_QUADSPI(obj);
+
+ fifo8_destroy(&s->fifo);
+}
+
+static void stm32l4x5_quadspi_realize(DeviceState *dev, Error **errp)
+{
+ Stm32l4x5QuadSpiState *s = STM32L4X5_QUADSPI(dev);
+
+ if (!clock_has_source(s->clk)) {
+ error_setg(errp, "%s: clk input must be connected",
+ TYPE_STM32L4X5_QUADSPI);
+ }
+}
+
+static int stm32l4x5_quadspi_post_load(void *opaque, int version_id)
+{
+ Stm32l4x5QuadSpiState *s = opaque;
+
+ if (s->fifo.head >= QUADSPI_FIFO_SIZE ||
+ fifo8_num_used(&s->fifo) > QUADSPI_FIFO_SIZE) {
+ return -EINVAL;
+ }
+ if ((s->transaction_active && !(s->cr & CR_EN)) ||
+ (s->indefinite && s->remaining)) {
+ return -EINVAL;
+ }
+ qemu_set_irq(s->cs, !s->transaction_active);
+ stm32l4x5_quadspi_update_fifo(s);
+ return 0;
+}
+
+static const VMStateDescription vmstate_stm32l4x5_quadspi = {
+ .name = TYPE_STM32L4X5_QUADSPI,
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .post_load = stm32l4x5_quadspi_post_load,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT32(cr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(dcr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(sr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(dlr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(ccr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(ar, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(abr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(dr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(psmkr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(psmar, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(pir, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT32(lptr, Stm32l4x5QuadSpiState),
+ VMSTATE_UINT64(remaining, Stm32l4x5QuadSpiState),
+ VMSTATE_BOOL(transaction_active, Stm32l4x5QuadSpiState),
+ VMSTATE_FIFO8(fifo, Stm32l4x5QuadSpiState),
+ VMSTATE_BOOL(rx_stalled, Stm32l4x5QuadSpiState),
+ VMSTATE_BOOL(indefinite, Stm32l4x5QuadSpiState),
+ VMSTATE_BOOL(write_pending, Stm32l4x5QuadSpiState),
+ VMSTATE_CLOCK(clk, Stm32l4x5QuadSpiState),
+ VMSTATE_END_OF_LIST()
+ },
+};
+
+static void stm32l4x5_quadspi_class_init(ObjectClass *klass, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+ dc->realize = stm32l4x5_quadspi_realize;
+ dc->vmsd = &vmstate_stm32l4x5_quadspi;
+ rc->phases.hold = stm32l4x5_quadspi_hold_reset;
+}
+
+static const TypeInfo stm32l4x5_quadspi_info = {
+ .name = TYPE_STM32L4X5_QUADSPI,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(Stm32l4x5QuadSpiState),
+ .instance_init = stm32l4x5_quadspi_init,
+ .instance_finalize = stm32l4x5_quadspi_finalize,
+ .class_init = stm32l4x5_quadspi_class_init,
+};
+
+static void stm32l4x5_quadspi_register_types(void)
+{
+ type_register_static(&stm32l4x5_quadspi_info);
+}
+
+type_init(stm32l4x5_quadspi_register_types)
diff --git a/include/hw/arm/stm32l4x5_soc.h b/include/hw/arm/stm32l4x5_soc.h
index 9c3d662bca..5b5e817ee7 100644
--- a/include/hw/arm/stm32l4x5_soc.h
+++ b/include/hw/arm/stm32l4x5_soc.h
@@ -32,6 +32,7 @@
#include "hw/misc/stm32l4x5_rcc.h"
#include "hw/gpio/stm32l4x5_gpio.h"
#include "hw/char/stm32l4x5_usart.h"
+#include "hw/ssi/stm32l4x5_quadspi.h"
#include "qom/object.h"
#define TYPE_STM32L4X5_SOC "stm32l4x5-soc"
@@ -56,6 +57,7 @@ struct Stm32l4x5SocState {
Stm32l4x5RccState rcc;
Stm32l4x5GpioState gpio[NUM_GPIOS];
Stm32l4x5UsartBaseState usart[STM_NUM_USARTS];
+ Stm32l4x5QuadSpiState quadspi;
Stm32l4x5UsartBaseState uart[STM_NUM_UARTS];
Stm32l4x5UsartBaseState lpuart;
diff --git a/include/hw/ssi/stm32l4x5_quadspi.h b/include/hw/ssi/stm32l4x5_quadspi.h
new file mode 100644
index 0000000000..e9882dd996
--- /dev/null
+++ b/include/hw/ssi/stm32l4x5_quadspi.h
@@ -0,0 +1,51 @@
+/*
+ * STM32L4x5 QuadSPI controller
+ *
+ * Copyright (c) 2026 EXALT Technologies
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_STM32L4X5_QUADSPI_H
+#define HW_STM32L4X5_QUADSPI_H
+
+#include "hw/core/clock.h"
+#include "hw/core/sysbus.h"
+#include "hw/ssi/ssi.h"
+#include "qemu/fifo8.h"
+#include "qom/object.h"
+
+#define TYPE_STM32L4X5_QUADSPI "stm32l4x5-quadspi"
+OBJECT_DECLARE_SIMPLE_TYPE(Stm32l4x5QuadSpiState, STM32L4X5_QUADSPI)
+
+struct Stm32l4x5QuadSpiState {
+ SysBusDevice parent_obj;
+
+ MemoryRegion mmio;
+ Clock *clk;
+ qemu_irq irq;
+ qemu_irq cs;
+ SSIBus *ssi;
+
+ uint32_t cr;
+ uint32_t dcr;
+ uint32_t sr;
+ uint32_t dlr;
+ uint32_t ccr;
+ uint32_t ar;
+ uint32_t abr;
+ uint32_t dr;
+ uint32_t psmkr;
+ uint32_t psmar;
+ uint32_t pir;
+ uint32_t lptr;
+
+ Fifo8 fifo;
+ uint64_t remaining;
+ bool transaction_active;
+ bool rx_stalled;
+ bool indefinite;
+ bool write_pending;
+};
+
+#endif /* HW_STM32L4X5_QUADSPI_H */
--
2.49.1
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH qemu 3/3] hw/arm: Add STM32L496G-DISCO machine
2026-11-12 15:03 [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support ~mohammadodeh
2026-09-27 7:08 ` [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash ~mohammadodeh
2026-09-27 7:09 ` [PATCH qemu 2/3] hw/ssi: Add STM32L4x5 QuadSPI controller ~mohammadodeh
@ 2026-09-27 7:32 ` ~mohammadodeh
2026-11-14 10:22 ` [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support Samuel Tardieu
3 siblings, 0 replies; 6+ messages in thread
From: ~mohammadodeh @ 2026-09-27 7:32 UTC (permalink / raw)
To: qemu-devel
Cc: Alexandre Iooss, Pierrick Bouvier, Peter Maydell, Paolo Bonzini,
Samuel Tardieu, Alistair Francis, Kevin Wolf, Hanna Reitz,
Fabiano Rosas, Laurent Vivier, Philippe Mathieu-Daudé,
Markus Armbruster, Cédric Le Goater, Bernhard Beschow,
Gaurav Sharma, Jan Kiszka, Bin Meng, Shameer Kolothum, Jamin Lin,
Marc-André Lureau, qemu-arm, qemu-block
From: Mohammad Odeh <mohammad.odeh@exalt.ps>
Add a machine model for the STM32L496G-DISCO Discovery kit.
Introduce the STM32L496xG SoC variant with its flash and SRAM sizes.
Attach the on-board MX25R6435F flash to the QuadSPI controller and add
qtests for identification, SFDP access, and data transfer.
Signed-off-by: Mohammad Odeh <zephyr@exalt.ps>
---
MAINTAINERS | 8 +
docs/system/arm/stm32.rst | 1 +
docs/system/arm/stm32l496g-disco.rst | 43 ++
docs/system/target-arm.rst | 1 +
hw/arm/Kconfig | 7 +
hw/arm/meson.build | 1 +
hw/arm/stm32l496g-disco.c | 95 ++++
hw/arm/stm32l4x5_soc.c | 25 +-
include/hw/arm/stm32l4x5_soc.h | 3 +
tests/qtest/meson.build | 1 +
tests/qtest/stm32l496g-disco-test.c | 693 +++++++++++++++++++++++++++
11 files changed, 874 insertions(+), 4 deletions(-)
create mode 100644 docs/system/arm/stm32l496g-disco.rst
create mode 100644 hw/arm/stm32l496g-disco.c
create mode 100644 tests/qtest/stm32l496g-disco-test.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 7a820703f8..cb1cad7f5c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1123,6 +1123,14 @@ F: hw/ssi/stm32l4x5_quadspi.c
F: include/hw/*/stm32l4x5_*.h
F: tests/qtest/stm32l4x5*
+STM32L496G-DISCO
+M: Mohammad Odeh <zephyr@exalt.ps>
+L: qemu-arm@nongnu.org
+S: Maintained
+F: hw/arm/stm32l496g-disco.c
+F: docs/system/arm/stm32l496g-disco.rst
+F: tests/qtest/stm32l496g-disco-test.c
+
STM32VLDISCOVERY
M: Alexandre Iooss <erdnaxe@crans.org>
L: qemu-arm@nongnu.org
diff --git a/docs/system/arm/stm32.rst b/docs/system/arm/stm32.rst
index 23b03f9029..70746f8e79 100644
--- a/docs/system/arm/stm32.rst
+++ b/docs/system/arm/stm32.rst
@@ -23,6 +23,7 @@ The following machines are based on this Arm Cortex-M4F chip :
- ``netduinoplus2`` Netduino Plus 2 board with STM32F405RGT6 microcontroller
- ``olimex-stm32-h405`` Olimex STM32 H405 board with STM32F405RGT6 microcontroller
- ``b-l475e-iot01a`` :doc:`B-L475E-IOT01A IoT Node </system/arm/b-l475e-iot01a>` board with STM32L475VG microcontroller
+- ``stm32l496g-disco`` :doc:`STM32L496G-DISCO Discovery Kit </system/arm/stm32l496g-disco>` board with STM32L496AG microcontroller
There are many other STM32 series that are currently not supported by QEMU.
diff --git a/docs/system/arm/stm32l496g-disco.rst b/docs/system/arm/stm32l496g-disco.rst
new file mode 100644
index 0000000000..732f98b202
--- /dev/null
+++ b/docs/system/arm/stm32l496g-disco.rst
@@ -0,0 +1,43 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+STM32L496G-DISCO Discovery Kit (``stm32l496g-disco``)
+=====================================================
+
+The STM32L496G-DISCO Discovery Kit uses an STM32L496AG microcontroller with
+an Arm Cortex-M4F core, 1 MiB of internal flash, 256 KiB of SRAM1, and 64 KiB
+of SRAM2.
+
+Supported devices
+-----------------
+
+The machine initially supports the devices shared with the STM32L4x5 model:
+
+- Cortex-M4F processor
+- Internal flash and SRAM
+- Reset and clock control
+- EXTI and system configuration controllers
+- GPIO ports A through H
+- USART1, USART2, USART3, UART4, UART5, and LPUART1
+- QuadSPI controller in indirect mode
+- MX25R6435F QuadSPI flash
+
+Missing devices
+---------------
+
+The following board features are not currently modeled:
+
+- GPIO port I
+- Timers, RTC, watchdogs, ADC, and DAC
+- I2C, SPI, SDMMC, USB, and FMC controllers
+- On-board display, joystick, and sensors
+
+Boot options
+------------
+
+USART2 is the console used by the Zephyr board configuration. It is QEMU's
+second serial backend, so start a Zephyr image with:
+
+.. code-block:: bash
+
+ qemu-system-arm -M stm32l496g-disco -kernel zephyr.elf \
+ -nographic -serial null -serial stdio
diff --git a/docs/system/target-arm.rst b/docs/system/target-arm.rst
index 260cd7e4d6..c7d9947655 100644
--- a/docs/system/target-arm.rst
+++ b/docs/system/target-arm.rst
@@ -82,6 +82,7 @@ Board-specific documentation
arm/aspeed
arm/bananapi_m2u.rst
arm/b-l475e-iot01a.rst
+ arm/stm32l496g-disco.rst
arm/sabrelite
arm/digic
arm/cubieboard
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 94a01c412b..5faa5d4ee1 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -389,6 +389,13 @@ config B_L475E_IOT01A
select STM32L4X5_SOC
imply DM163
+config STM32L496G_DISCO
+ bool
+ default y
+ depends on TCG && ARM
+ select STM32L4X5_SOC
+ select SSI_M25P80
+
config STM32L4X5_SOC
bool
select ARM_V7M
diff --git a/hw/arm/meson.build b/hw/arm/meson.build
index f3c1b0e00f..bc6d8ff86b 100644
--- a/hw/arm/meson.build
+++ b/hw/arm/meson.build
@@ -35,6 +35,7 @@ arm_common_ss.add(when: 'CONFIG_STM32F100_SOC', if_true: files('stm32f100_soc.c'
arm_common_ss.add(when: 'CONFIG_STM32F205_SOC', if_true: files('stm32f205_soc.c'))
arm_common_ss.add(when: 'CONFIG_STM32F405_SOC', if_true: files('stm32f405_soc.c'))
arm_common_ss.add(when: 'CONFIG_B_L475E_IOT01A', if_true: files('b-l475e-iot01a.c'))
+arm_common_ss.add(when: 'CONFIG_STM32L496G_DISCO', if_true: files('stm32l496g-disco.c'))
arm_common_ss.add(when: 'CONFIG_STM32L4X5_SOC', if_true: files('stm32l4x5_soc.c'))
arm_common_ss.add(when: 'CONFIG_XLNX_ZYNQMP_ARM', if_true: files('xlnx-zynqmp.c', 'xlnx-zcu102.c'))
arm_common_ss.add(when: 'CONFIG_XLNX_VERSAL', if_true: files('xlnx-versal.c', 'xlnx-versal-virt.c'))
diff --git a/hw/arm/stm32l496g-disco.c b/hw/arm/stm32l496g-disco.c
new file mode 100644
index 0000000000..9f3ad6e043
--- /dev/null
+++ b/hw/arm/stm32l496g-disco.c
@@ -0,0 +1,95 @@
+/*
+ * STM32L496G-DISCO Discovery Kit machine
+ *
+ * Copyright (c) 2026 EXALT Technologies
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+/*
+ * The reference used is the STMicroelectronics UM2160 user manual for the
+ * Discovery kit with STM32L496AG MCU and the RM0351 reference manual.
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "hw/arm/armv7m.h"
+#include "hw/arm/boot.h"
+#include "hw/arm/stm32l4x5_soc.h"
+#include "hw/core/boards.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/irq.h"
+#include "hw/ssi/ssi.h"
+#include "system/blockdev.h"
+
+#define TYPE_STM32L496G_DISCO MACHINE_TYPE_NAME("stm32l496g-disco")
+OBJECT_DECLARE_SIMPLE_TYPE(Stm32l496gDiscoMachineState,
+ STM32L496G_DISCO)
+
+typedef struct Stm32l496gDiscoMachineState {
+ MachineState parent_obj;
+
+ Stm32l4x5SocState soc;
+} Stm32l496gDiscoMachineState;
+
+static void stm32l496g_disco_init(MachineState *machine)
+{
+ Stm32l496gDiscoMachineState *s = STM32L496G_DISCO(machine);
+ const Stm32l4x5SocClass *sc;
+ DeviceState *flash;
+ DriveInfo *dinfo;
+ BusState *bus;
+ qemu_irq flash_cs;
+
+ object_initialize_child(OBJECT(machine), "soc", &s->soc,
+ TYPE_STM32L496XG_SOC);
+ sysbus_realize(SYS_BUS_DEVICE(&s->soc), &error_fatal);
+
+ bus = qdev_get_child_bus(DEVICE(&s->soc.quadspi), "ssi");
+ flash = qdev_new("mx25r6435");
+ dinfo = drive_get(IF_MTD, 0, 0);
+ if (dinfo) {
+ qdev_prop_set_drive_err(flash, "drive", blk_by_legacy_dinfo(dinfo),
+ &error_fatal);
+ }
+ qdev_realize_and_unref(flash, bus, &error_fatal);
+
+ flash_cs = qdev_get_gpio_in_named(flash, SSI_GPIO_CS, 0);
+ qdev_connect_gpio_out_named(DEVICE(&s->soc.quadspi), "cs", 0,
+ flash_cs);
+ qemu_set_irq(flash_cs, 1);
+
+ sc = STM32L4X5_SOC_GET_CLASS(&s->soc);
+ armv7m_load_kernel(s->soc.armv7m.cpu, machine->kernel_filename, 0,
+ sc->flash_size);
+}
+
+static void stm32l496g_disco_machine_init(ObjectClass *oc, const void *data)
+{
+ MachineClass *mc = MACHINE_CLASS(oc);
+ static const char *machine_valid_cpu_types[] = {
+ ARM_CPU_TYPE_NAME("cortex-m4"),
+ NULL
+ };
+
+ mc->desc = "STM32L496G-DISCO Discovery Kit (Cortex-M4)";
+ mc->init = stm32l496g_disco_init;
+ mc->valid_cpu_types = machine_valid_cpu_types;
+
+ /* SRAM is allocated by the SoC. */
+ mc->default_ram_size = 0;
+}
+
+static void stm32l496g_disco_machine_register_types(void)
+{
+ static const TypeInfo machine_type = {
+ .name = TYPE_STM32L496G_DISCO,
+ .parent = TYPE_MACHINE,
+ .instance_size = sizeof(Stm32l496gDiscoMachineState),
+ .class_init = stm32l496g_disco_machine_init,
+ };
+
+ type_register_static(&machine_type);
+}
+
+type_init(stm32l496g_disco_machine_register_types)
diff --git a/hw/arm/stm32l4x5_soc.c b/hw/arm/stm32l4x5_soc.c
index d91d18e88b..bd05a837fa 100644
--- a/hw/arm/stm32l4x5_soc.c
+++ b/hw/arm/stm32l4x5_soc.c
@@ -35,9 +35,7 @@
#define FLASH_BASE_ADDRESS 0x08000000
#define SRAM1_BASE_ADDRESS 0x20000000
-#define SRAM1_SIZE (96 * KiB)
#define SRAM2_BASE_ADDRESS 0x10000000
-#define SRAM2_SIZE (32 * KiB)
#define EXTI_ADDR 0x40010400
#define SYSCFG_ADDR 0x40010000
@@ -189,13 +187,15 @@ static void stm32l4x5_soc_realize(DeviceState *dev_soc, Error **errp)
memory_region_add_subregion(system_memory, FLASH_BASE_ADDRESS, &s->flash);
memory_region_add_subregion(system_memory, 0, &s->flash_alias);
- if (!memory_region_init_ram(&s->sram1, OBJECT(dev_soc), "SRAM1", SRAM1_SIZE,
+ if (!memory_region_init_ram(&s->sram1, OBJECT(dev_soc), "SRAM1",
+ sc->sram1_size,
errp)) {
return;
}
memory_region_add_subregion(system_memory, SRAM1_BASE_ADDRESS, &s->sram1);
- if (!memory_region_init_ram(&s->sram2, OBJECT(dev_soc), "SRAM2", SRAM2_SIZE,
+ if (!memory_region_init_ram(&s->sram2, OBJECT(dev_soc), "SRAM2",
+ sc->sram2_size,
errp)) {
return;
}
@@ -456,11 +456,15 @@ static void stm32l4x5_soc_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
+ Stm32l4x5SocClass *ssc = STM32L4X5_SOC_CLASS(klass);
dc->realize = stm32l4x5_soc_realize;
/* Reason: Mapped at fixed location on the system bus */
dc->user_creatable = false;
/* No vmstate or reset required: device has no internal state */
+
+ ssc->sram1_size = 96 * KiB;
+ ssc->sram2_size = 32 * KiB;
}
static void stm32l4x5xc_soc_class_init(ObjectClass *oc, const void *data)
@@ -484,6 +488,15 @@ static void stm32l4x5xg_soc_class_init(ObjectClass *oc, const void *data)
ssc->flash_size = 1 * MiB;
}
+static void stm32l496xg_soc_class_init(ObjectClass *oc, const void *data)
+{
+ Stm32l4x5SocClass *ssc = STM32L4X5_SOC_CLASS(oc);
+
+ ssc->flash_size = 1 * MiB;
+ ssc->sram1_size = 256 * KiB;
+ ssc->sram2_size = 64 * KiB;
+}
+
static const TypeInfo stm32l4x5_soc_types[] = {
{
.name = TYPE_STM32L4X5XC_SOC,
@@ -497,6 +510,10 @@ static const TypeInfo stm32l4x5_soc_types[] = {
.name = TYPE_STM32L4X5XG_SOC,
.parent = TYPE_STM32L4X5_SOC,
.class_init = stm32l4x5xg_soc_class_init,
+ }, {
+ .name = TYPE_STM32L496XG_SOC,
+ .parent = TYPE_STM32L4X5_SOC,
+ .class_init = stm32l496xg_soc_class_init,
}, {
.name = TYPE_STM32L4X5_SOC,
.parent = TYPE_SYS_BUS_DEVICE,
diff --git a/include/hw/arm/stm32l4x5_soc.h b/include/hw/arm/stm32l4x5_soc.h
index 5b5e817ee7..a10bc10cb4 100644
--- a/include/hw/arm/stm32l4x5_soc.h
+++ b/include/hw/arm/stm32l4x5_soc.h
@@ -39,6 +39,7 @@
#define TYPE_STM32L4X5XC_SOC "stm32l4x5xc-soc"
#define TYPE_STM32L4X5XE_SOC "stm32l4x5xe-soc"
#define TYPE_STM32L4X5XG_SOC "stm32l4x5xg-soc"
+#define TYPE_STM32L496XG_SOC "stm32l496xg-soc"
OBJECT_DECLARE_TYPE(Stm32l4x5SocState, Stm32l4x5SocClass, STM32L4X5_SOC)
#define NUM_EXTI_OR_GATES 4
@@ -71,6 +72,8 @@ struct Stm32l4x5SocClass {
SysBusDeviceClass parent_class;
size_t flash_size;
+ size_t sram1_size;
+ size_t sram2_size;
};
#endif
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 7994696086..29febd1a08 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -271,6 +271,7 @@ qtests_arm = \
(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_STM32L4X5_SOC') ? qtests_stm32l4x5 : []) + \
+ (config_all_devices.has_key('CONFIG_STM32L496G_DISCO') ? ['stm32l496g-disco-test'] : []) + \
(config_all_devices.has_key('CONFIG_FSI_APB2OPB_ASPEED') ? ['aspeed_fsi-test'] : []) + \
(config_all_devices.has_key('CONFIG_CAN_FLEXCAN') ? ['flexcan-test'] : []) + \
(config_all_devices.has_key('CONFIG_STM32L4X5_SOC') and
diff --git a/tests/qtest/stm32l496g-disco-test.c b/tests/qtest/stm32l496g-disco-test.c
new file mode 100644
index 0000000000..9529618fa7
--- /dev/null
+++ b/tests/qtest/stm32l496g-disco-test.c
@@ -0,0 +1,693 @@
+/*
+ * QTest testcase for the STM32L496G-DISCO machine
+ *
+ * Copyright (c) 2026 EXALT Technologies
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/bitops.h"
+#include "libqtest.h"
+#include "qobject/qdict.h"
+
+#define QUADSPI_BASE 0xa0001000
+
+#define QUADSPI_CR (QUADSPI_BASE + 0x00)
+#define QUADSPI_DCR (QUADSPI_BASE + 0x04)
+#define QUADSPI_SR (QUADSPI_BASE + 0x08)
+#define QUADSPI_FCR (QUADSPI_BASE + 0x0c)
+#define QUADSPI_DLR (QUADSPI_BASE + 0x10)
+#define QUADSPI_CCR (QUADSPI_BASE + 0x14)
+#define QUADSPI_AR (QUADSPI_BASE + 0x18)
+#define QUADSPI_DR (QUADSPI_BASE + 0x20)
+#define QUADSPI_ABR (QUADSPI_BASE + 0x1c)
+#define DCR_FSIZE(bits) (((bits) - 1) << 16)
+
+#define CR_EN BIT(0)
+#define CR_ABORT BIT(1)
+#define CR_TEIE BIT(16)
+#define CR_FTHRES(bytes) (((bytes) - 1) << 8)
+#define CR_TCIE BIT(17)
+#define CR_FTIE BIT(18)
+
+#define SR_TCF BIT(1)
+#define SR_TEF BIT(0)
+#define SR_FTF BIT(2)
+#define SR_BUSY BIT(5)
+#define SR_FLEVEL(sr) extract32((sr), 8, 5)
+
+#define FCR_CTCF BIT(1)
+
+#define CCR_IMODE_SINGLE (1 << 8)
+#define CCR_ADMODE_SINGLE (1 << 10)
+#define CCR_ADSIZE_24BIT (2 << 12)
+#define CCR_DCYC(cycles) ((cycles) << 18)
+#define CCR_DMODE_SINGLE (1 << 24)
+#define CCR_FMODE_INDIRECT_READ (1 << 26)
+#define CCR_ADMODE_DUAL (2 << 10)
+#define CCR_ADMODE_QUAD (3 << 10)
+#define CCR_DMODE_DUAL (2 << 24)
+#define CCR_DMODE_QUAD (3 << 24)
+#define CCR_ABMODE_QUAD (3 << 14)
+#define FCR_ALL 0x1b
+
+#define CMD_WRITE_ENABLE 0x06
+#define CMD_WRITE_DISABLE 0x04
+#define CMD_READ_STATUS 0x05
+#define CMD_READ 0x03
+#define CMD_PAGE_PROGRAM 0x02
+#define CMD_READ_ID 0x9f
+#define CMD_READ_SFDP 0x5a
+
+static QTestState *qts;
+
+static void qspi_command(uint8_t command)
+{
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ qtest_writel(qts, QUADSPI_CCR, CCR_IMODE_SINGLE | command);
+}
+
+static void qspi_start_read(uint8_t command, uint32_t address,
+ bool address_phase, unsigned int dummy_cycles,
+ unsigned int length)
+{
+ uint32_t ccr = CCR_IMODE_SINGLE | CCR_DMODE_SINGLE |
+ CCR_FMODE_INDIRECT_READ | command;
+
+ if (address_phase) {
+ ccr |= CCR_ADMODE_SINGLE | CCR_ADSIZE_24BIT;
+ }
+ ccr |= CCR_DCYC(dummy_cycles);
+
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ qtest_writel(qts, QUADSPI_CCR, 0);
+ qtest_writel(qts, QUADSPI_DLR, length - 1);
+ qtest_writel(qts, QUADSPI_CCR, ccr);
+ if (address_phase) {
+ qtest_writel(qts, QUADSPI_AR, address);
+ }
+}
+
+static uint32_t qspi_read(uint8_t command, uint32_t address,
+ bool address_phase, unsigned int dummy_cycles,
+ unsigned int length)
+{
+ qspi_start_read(command, address, address_phase, dummy_cycles, length);
+ return qtest_readl(qts, QUADSPI_DR);
+}
+
+static void qspi_start_write(uint32_t address, unsigned int length)
+{
+ uint32_t ccr = CMD_PAGE_PROGRAM | CCR_IMODE_SINGLE |
+ CCR_ADMODE_SINGLE | CCR_ADSIZE_24BIT |
+ CCR_DMODE_SINGLE;
+
+ qspi_command(CMD_WRITE_ENABLE);
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ qtest_writel(qts, QUADSPI_CCR, 0);
+ qtest_writel(qts, QUADSPI_AR, address);
+ qtest_writel(qts, QUADSPI_DLR, length - 1);
+ qtest_writel(qts, QUADSPI_CCR, ccr);
+}
+
+static void qspi_page_program(uint32_t address, uint32_t value)
+{
+ qspi_start_write(address, sizeof(value));
+ qtest_writel(qts, QUADSPI_DR, value);
+}
+
+static void test_quadspi_identification(void)
+{
+ uint32_t id = qspi_read(CMD_READ_ID, 0, false, 0, 3);
+
+ g_assert_cmphex(id & 0x00ffffff, ==, 0x001728c2);
+}
+
+static void test_quadspi_sfdp(void)
+{
+ uint32_t signature = qspi_read(CMD_READ_SFDP, 0, true, 8, 4);
+
+ g_assert_cmphex(signature, ==, 0x50444653);
+}
+
+static void test_quadspi_program_read(void)
+{
+ const uint32_t address = 0x100;
+ const uint32_t value = 0x5aa55aa5;
+ uint32_t actual;
+
+ qspi_page_program(address, value);
+ actual = qspi_read(CMD_READ, address, true, 0, sizeof(value));
+
+ g_assert_cmphex(actual, ==, value);
+}
+
+/* Verify FIFO capacity, RX backpressure, threshold IRQs, and byte ordering. */
+static void test_quadspi_fifo_refill(void)
+{
+ const uint32_t address = 0x200;
+ const unsigned int length = 40;
+ uint32_t sr;
+
+ qspi_start_write(address, length);
+ for (unsigned int i = 0; i < length; i++) {
+ qtest_writeb(qts, QUADSPI_DR, i);
+ }
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_FTHRES(16) | CR_FTIE);
+ qspi_start_read(CMD_READ, address, true, 0, length);
+ sr = qtest_readl(qts, QUADSPI_SR);
+ g_assert_cmpuint(SR_FLEVEL(sr), ==, 16);
+ g_assert_true(sr & SR_BUSY);
+ g_assert_true(sr & SR_FTF);
+ g_assert_false(sr & SR_TCF);
+ g_assert_true(qtest_get_irq(qts, 0));
+
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DR), ==, 0x03020100);
+ sr = qtest_readl(qts, QUADSPI_SR);
+ g_assert_cmpuint(SR_FLEVEL(sr), ==, 12);
+ g_assert_false(sr & SR_FTF);
+ g_assert_false(qtest_get_irq(qts, 0));
+
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, 4);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 16);
+ g_assert_true(qtest_get_irq(qts, 0));
+ for (unsigned int i = 5; i < length; i++) {
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, i);
+ sr = qtest_readl(qts, QUADSPI_SR);
+ g_assert_cmpuint(SR_FLEVEL(sr), <=, 16);
+ if (i + 1 < length) {
+ g_assert_true(sr & SR_BUSY);
+ }
+ }
+ g_assert_cmpuint(SR_FLEVEL(sr), ==, 0);
+ g_assert_true(sr & SR_TCF);
+ g_assert_false(sr & (SR_BUSY | SR_FTF));
+ g_assert_false(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+}
+
+/* A short final RX fragment must remain readable below the FIFO threshold. */
+static void test_quadspi_fifo_tail(void)
+{
+ uint32_t sr;
+
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_FTHRES(16) | CR_FTIE);
+ qspi_start_read(CMD_READ_ID, 0, false, 0, 3);
+ sr = qtest_readl(qts, QUADSPI_SR);
+ g_assert_cmpuint(SR_FLEVEL(sr), ==, 3);
+ g_assert_true(sr & SR_TCF);
+ g_assert_true(sr & SR_BUSY);
+ g_assert_true(sr & SR_FTF);
+ g_assert_true(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ g_assert_cmphex(qtest_readw(qts, QUADSPI_DR), ==, 0x28c2);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 1);
+ g_assert_true(qtest_get_irq(qts, 0));
+ g_assert_cmphex(qtest_readb(qts, QUADSPI_DR), ==, 0x17);
+ sr = qtest_readl(qts, QUADSPI_SR);
+ g_assert_false(sr & (SR_BUSY | SR_FTF | SR_TCF));
+ g_assert_false(qtest_get_irq(qts, 0));
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, 0);
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+}
+
+/* Byte, halfword, and word accesses must preserve the FIFO byte sequence. */
+static void test_quadspi_fifo_mixed_access(void)
+{
+ qspi_start_write(0x300, 7);
+ qtest_writeb(qts, QUADSPI_DR, 0xa1);
+ qtest_writew(qts, QUADSPI_DR, 0xc3b2);
+ qtest_writel(qts, QUADSPI_DR, 0x07f6e5d4);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 0);
+ qspi_start_read(CMD_READ, 0x300, true, 0, 7);
+ g_assert_cmphex(qtest_readb(qts, QUADSPI_DR), ==, 0xa1);
+ g_assert_cmphex(qtest_readw(qts, QUADSPI_DR), ==, 0xc3b2);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DR), ==, 0x07f6e5d4);
+}
+
+/* Extra bytes in a final word write must not be programmed beyond DLR. */
+static void test_quadspi_fifo_write_excess(void)
+{
+ qspi_start_write(0x400, 3);
+ qtest_writel(qts, QUADSPI_DR, 0x44332211);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 0);
+ g_assert_cmphex(qspi_read(CMD_READ, 0x400, true, 0, 4), ==, 0xff332211);
+}
+
+/* Aborting a paused RX transfer must discard its FIFO and permit a new read. */
+static void test_quadspi_fifo_abort(void)
+{
+ uint32_t sr;
+
+ qspi_start_read(CMD_READ, 0x200, true, 0, 40);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 16);
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_ABORT);
+ sr = qtest_readl(qts, QUADSPI_SR);
+ g_assert_cmpuint(SR_FLEVEL(sr), ==, 0);
+ g_assert_false(sr & (SR_BUSY | SR_FTF));
+ g_assert_true(sr & SR_TCF);
+ g_assert_false(qtest_readl(qts, QUADSPI_CR) & CR_ABORT);
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, 0);
+ g_assert_cmphex(qspi_read(CMD_READ_ID, 0, false, 0, 3), ==, 0x001728c2);
+}
+
+/* Reset must discard buffered RX data and deassert the threshold interrupt. */
+static void test_quadspi_fifo_reset(void)
+{
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_FTIE);
+ qspi_start_read(CMD_READ, 0x200, true, 0, 40);
+ g_assert_true(qtest_get_irq(qts, 0));
+ qtest_system_reset(qts);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CR), ==, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DLR), ==, 0);
+ g_assert_false(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(23));
+ g_assert_cmphex(qspi_read(CMD_READ_ID, 0, false, 0, 3), ==, 0x001728c2);
+}
+
+/* Disabled command writes must not set the flash's write-enable latch. */
+static void test_quadspi_disabled_command(void)
+{
+ qspi_command(CMD_WRITE_DISABLE);
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ qtest_writel(qts, QUADSPI_CR, CR_TCIE | CR_FTIE);
+ qspi_command(CMD_WRITE_ENABLE);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CCR), ==,
+ CCR_IMODE_SINGLE | CMD_WRITE_ENABLE);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ g_assert_false(qtest_get_irq(qts, 0));
+
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmphex(qspi_read(CMD_READ_STATUS, 0, false, 0, 1) & 2, ==, 0);
+ qspi_command(CMD_WRITE_ENABLE);
+ g_assert_cmphex(qspi_read(CMD_READ_STATUS, 0, false, 0, 1) & 2, ==, 2);
+ qspi_command(CMD_WRITE_DISABLE);
+}
+
+/* CCR, AR, and DR accesses must not exchange flash data while disabled. */
+static void test_quadspi_disabled_data(void)
+{
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ qtest_writel(qts, QUADSPI_CR, CR_TCIE | CR_FTIE);
+ qspi_start_read(CMD_READ_ID, 0, false, 0, 3);
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ qspi_start_read(CMD_READ, 0x500, true, 0, 4);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_AR), ==, 0x500);
+ g_assert_cmpuint(qtest_readl(qts, QUADSPI_DLR), ==, 3);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DR), ==, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+
+ qspi_start_write(0x500, 4);
+ qtest_writel(qts, QUADSPI_DR, 0x12345678);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ g_assert_false(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ qtest_writel(qts, QUADSPI_DR, 0x12345678);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmphex(qspi_read(CMD_READ, 0x500, true, 0, 4), ==, 0xffffffff);
+ g_assert_cmphex(qspi_read(CMD_READ_ID, 0, false, 0, 3), ==, 0x001728c2);
+}
+
+/* Disable clears RX state without synthesizing completion or replaying CCR. */
+static void test_quadspi_disable_read(void)
+{
+ uint32_t ccr;
+
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_FTIE | CR_TCIE);
+ qspi_start_read(CMD_READ, 0, true, 0, 40);
+ ccr = qtest_readl(qts, QUADSPI_CCR);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 16);
+ g_assert_true(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_CR, CR_FTIE | CR_TCIE);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ g_assert_false(qtest_get_irq(qts, 0));
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CCR), ==, ccr);
+ g_assert_cmpuint(qtest_readl(qts, QUADSPI_DLR), ==, 39);
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, 0);
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_FTIE | CR_TCIE);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ g_assert_false(qtest_get_irq(qts, 0));
+
+ qspi_start_read(CMD_READ_ID, 0, false, 0, 3);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ qtest_writel(qts, QUADSPI_CR, CR_TCIE);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, SR_TCF);
+ g_assert_true(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_FCR, FCR_CTCF);
+ g_assert_false(qtest_get_irq(qts, 0));
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+}
+
+/* Disable stops a partial write without replaying data on re-enable. */
+static void test_quadspi_disable_write(void)
+{
+ uint32_t ccr;
+
+ qspi_start_write(0x600, 4);
+ qtest_writeb(qts, QUADSPI_DR, 0xa5);
+ ccr = qtest_readl(qts, QUADSPI_CCR);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_BUSY);
+ qtest_writel(qts, QUADSPI_CR, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_SR), ==, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CCR), ==, ccr);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_AR), ==, 0x600);
+ qtest_writel(qts, QUADSPI_DR, 0);
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ qtest_writel(qts, QUADSPI_DR, 0);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmphex(qspi_read(CMD_READ, 0x600, true, 0, 4), ==, 0xffffffa5);
+}
+
+/* Reject out-of-range reads/writes before exchanging any flash payload. */
+static void test_quadspi_address_bounds(void)
+{
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(12));
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_TEIE);
+ qspi_start_read(CMD_READ, 0x1000, true, 0, 1);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TEF);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & (SR_BUSY | SR_TCF));
+ g_assert_true(qtest_get_irq(qts, 0));
+ qtest_writeb(qts, QUADSPI_FCR, SR_TEF);
+ g_assert_false(qtest_get_irq(qts, 0));
+ qspi_start_read(CMD_READ, 0xfff, true, 0, 2);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TEF);
+ qtest_writel(qts, QUADSPI_FCR, FCR_ALL);
+ qspi_start_write(0xfff, 2);
+ qtest_writew(qts, QUADSPI_DR, 0x1234);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TEF);
+ qtest_writel(qts, QUADSPI_FCR, FCR_ALL);
+ g_assert_cmphex(qspi_read(CMD_READ, 0xffc, true, 0, 4), ==, 0xffffffff);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(32));
+ qspi_start_read(0x13, UINT32_MAX, true, 0, 2);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TEF);
+ qtest_writel(qts, QUADSPI_FCR, FCR_ALL);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(23));
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+}
+
+/* Undefined length ends at FSIZE, or continues until abort for FSIZE = 31. */
+static void test_quadspi_undefined_length(void)
+{
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(12));
+ qspi_start_read(CMD_READ, 0xffc, true, 0, 0);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmpuint(SR_FLEVEL(qtest_readl(qts, QUADSPI_SR)), ==, 4);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DR), ==, 0xffffffff);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_BUSY);
+ qspi_start_write(0xffc, 0);
+ qtest_writel(qts, QUADSPI_DR, 0x78563412);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmphex(qspi_read(CMD_READ, 0xffc, true, 0, 4), ==, 0x78563412);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(32));
+ qspi_start_read(CMD_READ, 0, true, 0, 0);
+ for (unsigned int i = 0; i < 12; i++) {
+ qtest_readl(qts, QUADSPI_DR);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_BUSY);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ }
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_ABORT);
+ qspi_start_write(0x900, 0);
+ qtest_writel(qts, QUADSPI_DR, 0x12345678);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_BUSY);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_ABORT);
+ g_assert_cmphex(qspi_read(CMD_READ, 0x900, true, 0, 4), ==, 0x12345678);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(23));
+}
+
+/* Subword register accesses preserve adjacent bytes and write-one clears. */
+static void test_quadspi_partial_registers(void)
+{
+ qtest_writel(qts, QUADSPI_ABR, 0x12345678);
+ qtest_writeb(qts, QUADSPI_ABR + 1, 0xab);
+ qtest_writew(qts, QUADSPI_ABR + 2, 0xcdef);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_ABR), ==, 0xcdefab78);
+ g_assert_cmphex(qtest_readb(qts, QUADSPI_ABR + 1), ==, 0xab);
+ g_assert_cmphex(qtest_readw(qts, QUADSPI_ABR + 2), ==, 0xcdef);
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_TCIE);
+ qtest_writeb(qts, QUADSPI_CR + 1, 0x03);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CR), ==,
+ CR_EN | CR_TCIE | CR_FTHRES(4));
+ qspi_command(CMD_WRITE_DISABLE);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ qtest_writeb(qts, QUADSPI_FCR + 1, 0xff);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ qtest_writeb(qts, QUADSPI_FCR, FCR_CTCF);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+}
+
+/* A busy controller must not reinterpret its active command configuration. */
+static void test_quadspi_busy_registers(void)
+{
+ uint32_t ccr;
+
+ qspi_start_read(CMD_READ, 0, true, 0, 40);
+ ccr = qtest_readl(qts, QUADSPI_CCR);
+ qtest_writel(qts, QUADSPI_CCR, CCR_IMODE_SINGLE | CMD_WRITE_ENABLE);
+ qtest_writel(qts, QUADSPI_DLR, 1);
+ qtest_writel(qts, QUADSPI_AR, 0x800);
+ qtest_writel(qts, QUADSPI_DCR, 0);
+ qtest_writel(qts, QUADSPI_ABR, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CCR), ==, ccr);
+ g_assert_cmpuint(qtest_readl(qts, QUADSPI_DLR), ==, 39);
+ g_assert_cmpuint(qtest_readl(qts, QUADSPI_AR), ==, 0);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DCR), ==, DCR_FSIZE(23));
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_FTHRES(16) | CR_FTIE);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_CR), ==, CR_EN | CR_FTIE);
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_ABORT);
+}
+
+/* Deferred modes must never issue an accidental WREN command. */
+static void test_quadspi_unsupported_modes(void)
+{
+ static const uint32_t configs[] = {
+ (2 << 26), (3 << 26), BIT(28), BIT(31),
+ };
+
+ qspi_command(CMD_WRITE_DISABLE);
+ for (unsigned int i = 0; i < ARRAY_SIZE(configs); i++) {
+ qtest_writel(qts, QUADSPI_FCR, FCR_ALL);
+ qtest_writel(qts, QUADSPI_CCR,
+ configs[i] | CCR_IMODE_SINGLE | CMD_WRITE_ENABLE);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & (SR_TCF | SR_BUSY));
+ g_assert_cmphex(qspi_read(CMD_READ_STATUS, 0, false, 0, 1) & 2, ==, 0);
+ }
+ qtest_writel(qts, QUADSPI_CR, CR_EN | BIT(6));
+ qtest_writel(qts, QUADSPI_FCR, FCR_ALL);
+ qtest_writel(qts, QUADSPI_CCR, CCR_IMODE_SINGLE | CMD_WRITE_ENABLE);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & (SR_TCF | SR_BUSY));
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ g_assert_cmphex(qspi_read(CMD_READ_STATUS, 0, false, 0, 1) & 2, ==, 0);
+}
+
+/* Exercise fast-read opcodes and single/dual/quad byte prefixes. */
+static void test_quadspi_lane_modes(void)
+{
+ static const struct {
+ uint8_t opcode;
+ uint32_t admode;
+ uint32_t dmode;
+ unsigned int dummy;
+ bool alternate;
+ } modes[] = {
+ { 0x0b, CCR_ADMODE_SINGLE, CCR_DMODE_SINGLE, 8, false },
+ { 0x3b, CCR_ADMODE_SINGLE, CCR_DMODE_DUAL, 8, false },
+ { 0x6b, CCR_ADMODE_SINGLE, CCR_DMODE_QUAD, 8, false },
+ { 0xbb, CCR_ADMODE_DUAL, CCR_DMODE_DUAL, 4, false },
+ { 0xeb, CCR_ADMODE_QUAD, CCR_DMODE_QUAD, 6, false },
+ { 0xeb, CCR_ADMODE_QUAD, CCR_DMODE_QUAD, 4, true },
+ };
+
+ qspi_page_program(0x800, 0x76543210);
+ for (unsigned int i = 0; i < ARRAY_SIZE(modes); i++) {
+ uint32_t ccr = CCR_IMODE_SINGLE | CCR_ADSIZE_24BIT |
+ CCR_FMODE_INDIRECT_READ | modes[i].admode |
+ modes[i].dmode | CCR_DCYC(modes[i].dummy) |
+ modes[i].opcode;
+
+ qtest_writel(qts, QUADSPI_ABR, 0);
+ if (modes[i].alternate) {
+ ccr |= CCR_ABMODE_QUAD;
+ }
+ qtest_writel(qts, QUADSPI_DLR, 3);
+ qtest_writel(qts, QUADSPI_CCR, ccr);
+ qtest_writel(qts, QUADSPI_AR, 0x800);
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DR), ==, 0x76543210);
+ }
+}
+
+static void qspi_wait_migration(QTestState *state)
+{
+ int64_t deadline = g_get_monotonic_time() + 10 * G_TIME_SPAN_SECOND;
+
+ do {
+ QDict *response = qtest_qmp(state, "{'execute': 'query-migrate'}");
+ QDict *result = qdict_get_qdict(response, "return");
+ const char *status = qdict_get_str(result, "status");
+ bool complete = !strcmp(status, "completed");
+
+ if (!strcmp(status, "failed")) {
+ g_error("Migration failed: %s",
+ qdict_get_try_str(result, "error-desc"));
+ }
+ qobject_unref(response);
+ if (complete) {
+ return;
+ }
+ g_usleep(1000);
+ } while (g_get_monotonic_time() < deadline);
+ g_error("Migration did not complete within 10 seconds");
+}
+
+static void qspi_migrate(void)
+{
+ char *path = NULL;
+ int fd = g_file_open_tmp("qspi-migration-XXXXXX", &path, NULL);
+ g_autofree char *uri = g_strdup_printf("file:%s", path);
+ QTestState *source = qts;
+
+ g_assert_cmpint(fd, >=, 0);
+ close(fd);
+ qtest_qmp_assert_success(source,
+ "{'execute': 'migrate', 'arguments': {'uri': %s}}", uri);
+ qspi_wait_migration(source);
+ qts = qtest_init("-machine stm32l496g-disco -incoming defer");
+ qtest_qmp_assert_success(qts,
+ "{'execute': 'migrate-incoming', 'arguments': {'uri': %s}}", uri);
+ qspi_wait_migration(qts);
+ qtest_quit(source);
+ unlink(path);
+ g_free(path);
+}
+
+/* SFDP data exposes lost FIFO bytes or a reset flash-parser cursor. */
+static void test_quadspi_migration_read(void)
+{
+ QTestState *original = qts;
+ uint8_t expected[40];
+
+ qts = qtest_init("-machine stm32l496g-disco");
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(23));
+ qspi_start_read(CMD_READ_SFDP, 0, true, 8, sizeof(expected));
+ for (unsigned int i = 0; i < sizeof(expected); i++) {
+ expected[i] = qtest_readb(qts, QUADSPI_DR);
+ }
+ qspi_start_read(CMD_READ_SFDP, 0, true, 8, sizeof(expected));
+ for (unsigned int i = 0; i < 3; i++) {
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, expected[i]);
+ }
+ qspi_migrate();
+ for (unsigned int i = 3; i < sizeof(expected); i++) {
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, expected[i]);
+ }
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_BUSY);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(32));
+ qspi_start_read(CMD_READ_SFDP, 0, true, 8, 0);
+ for (unsigned int i = 0; i < 3; i++) {
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, expected[i]);
+ }
+ qspi_migrate();
+ for (unsigned int i = 3; i < sizeof(expected); i++) {
+ g_assert_cmpuint(qtest_readb(qts, QUADSPI_DR), ==, expected[i]);
+ }
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_BUSY);
+ g_assert_false(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ qtest_writel(qts, QUADSPI_CR, CR_EN | CR_ABORT);
+ qtest_quit(qts);
+ qts = original;
+}
+
+/* Restore an unread final fragment and an in-flight register write. */
+static void test_quadspi_migration_write(void)
+{
+ QTestState *original = qts;
+
+ qts = qtest_init("-machine stm32l496g-disco");
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(23));
+ qspi_start_read(CMD_READ_ID, 0, false, 0, 3);
+ qspi_migrate();
+ g_assert_cmphex(qtest_readl(qts, QUADSPI_DR), ==, 0x001728c2);
+ qspi_command(CMD_WRITE_ENABLE);
+ qtest_writel(qts, QUADSPI_DLR, 1);
+ qtest_writel(qts, QUADSPI_CCR,
+ CCR_IMODE_SINGLE | CCR_DMODE_SINGLE | 0x01);
+ qspi_migrate();
+ qtest_writeb(qts, QUADSPI_DR, 0x40);
+ qspi_migrate();
+ qtest_writeb(qts, QUADSPI_DR, 0);
+ g_assert_true(qtest_readl(qts, QUADSPI_SR) & SR_TCF);
+ g_assert_cmphex(qspi_read(CMD_READ_STATUS, 0, false, 0, 1) & 0x40,
+ ==, 0x40);
+ qtest_quit(qts);
+ qts = original;
+}
+
+int main(int argc, char **argv)
+{
+ int ret;
+
+ g_test_init(&argc, &argv, NULL);
+ g_test_set_nonfatal_assertions();
+
+ qts = qtest_init("-machine stm32l496g-disco");
+ qtest_irq_intercept_out_named(qts, "/machine/soc/quadspi",
+ "sysbus-irq");
+ qtest_writel(qts, QUADSPI_CR, CR_EN);
+ qtest_writel(qts, QUADSPI_DCR, DCR_FSIZE(23));
+
+ qtest_add_func("stm32l496g-disco/quadspi/identification",
+ test_quadspi_identification);
+ qtest_add_func("stm32l496g-disco/quadspi/sfdp", test_quadspi_sfdp);
+ qtest_add_func("stm32l496g-disco/quadspi/program-read",
+ test_quadspi_program_read);
+ qtest_add_func("stm32l496g-disco/quadspi/fifo-refill",
+ test_quadspi_fifo_refill);
+ qtest_add_func("stm32l496g-disco/quadspi/fifo-tail",
+ test_quadspi_fifo_tail);
+ qtest_add_func("stm32l496g-disco/quadspi/fifo-mixed-access",
+ test_quadspi_fifo_mixed_access);
+ qtest_add_func("stm32l496g-disco/quadspi/fifo-write-excess",
+ test_quadspi_fifo_write_excess);
+ qtest_add_func("stm32l496g-disco/quadspi/fifo-abort",
+ test_quadspi_fifo_abort);
+ qtest_add_func("stm32l496g-disco/quadspi/fifo-reset",
+ test_quadspi_fifo_reset);
+ qtest_add_func("stm32l496g-disco/quadspi/disabled-command",
+ test_quadspi_disabled_command);
+ qtest_add_func("stm32l496g-disco/quadspi/disabled-data",
+ test_quadspi_disabled_data);
+ qtest_add_func("stm32l496g-disco/quadspi/disable-read",
+ test_quadspi_disable_read);
+ qtest_add_func("stm32l496g-disco/quadspi/disable-write",
+ test_quadspi_disable_write);
+ qtest_add_func("stm32l496g-disco/quadspi/address-bounds",
+ test_quadspi_address_bounds);
+ qtest_add_func("stm32l496g-disco/quadspi/undefined-length",
+ test_quadspi_undefined_length);
+ qtest_add_func("stm32l496g-disco/quadspi/partial-registers",
+ test_quadspi_partial_registers);
+ qtest_add_func("stm32l496g-disco/quadspi/busy-registers",
+ test_quadspi_busy_registers);
+ qtest_add_func("stm32l496g-disco/quadspi/unsupported-modes",
+ test_quadspi_unsupported_modes);
+ qtest_add_func("stm32l496g-disco/quadspi/lane-modes",
+ test_quadspi_lane_modes);
+ qtest_add_func("stm32l496g-disco/quadspi/migration-read",
+ test_quadspi_migration_read);
+ qtest_add_func("stm32l496g-disco/quadspi/migration-write",
+ test_quadspi_migration_write);
+
+ ret = g_test_run();
+ qtest_quit(qts);
+
+ return ret;
+}
--
2.49.1
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support
@ 2026-11-12 15:03 ~mohammadodeh
2026-09-27 7:08 ` [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash ~mohammadodeh
` (3 more replies)
0 siblings, 4 replies; 6+ messages in thread
From: ~mohammadodeh @ 2026-11-12 15:03 UTC (permalink / raw)
To: qemu-devel
Cc: Alexandre Iooss, Pierrick Bouvier, Peter Maydell, Paolo Bonzini,
Samuel Tardieu, Alistair Francis, Kevin Wolf, Hanna Reitz,
Fabiano Rosas, Laurent Vivier, Philippe Mathieu-Daudé,
Markus Armbruster, Cédric Le Goater, Bernhard Beschow,
Gaurav Sharma, Jan Kiszka, Bin Meng, Shameer Kolothum, Jamin Lin,
Marc-André Lureau, qemu-arm, qemu-block
Add initial QEMU support for the STM32L496G-DISCO Discovery kit and
its MX25R6435F QuadSPI flash.
The series adds the flash model, the shared STM32L4x5 QuadSPI
controller, and the STM32L496xG SoC and board definitions.
The controller supports indirect reads and writes with a 16-byte FIFO,
enable and abort handling, address bounds checks, undefined-length
transfers, partial register accesses, and migration state restoration.
The board qtest has 21 passing cases covering identification, SFDP,
program/read transfers, FIFO behavior, single/dual/quad read frames,
error handling, and migration.
Transfer timing, automatic polling, memory-mapped mode, DMA, DDR,
SIOO, and dual-flash operation are not implemented.
Mohammad Odeh (3):
hw/block: Add Macronix MX25R6435F flash
hw/ssi: Add STM32L4x5 QuadSPI controller
hw/arm: Add STM32L496G-DISCO machine
MAINTAINERS | 9 +
docs/system/arm/stm32.rst | 1 +
docs/system/arm/stm32l496g-disco.rst | 43 ++
docs/system/target-arm.rst | 1 +
hw/arm/Kconfig | 8 +
hw/arm/meson.build | 1 +
hw/arm/stm32l496g-disco.c | 95 ++++
hw/arm/stm32l4x5_soc.c | 42 +-
hw/block/m25p80.c | 5 +
hw/block/m25p80_sfdp.c | 20 +
hw/block/m25p80_sfdp.h | 1 +
hw/ssi/Kconfig | 4 +
hw/ssi/meson.build | 1 +
hw/ssi/stm32l4x5_quadspi.c | 680 ++++++++++++++++++++++++++
include/hw/arm/stm32l4x5_soc.h | 5 +
include/hw/ssi/stm32l4x5_quadspi.h | 51 ++
tests/qtest/meson.build | 1 +
tests/qtest/stm32l496g-disco-test.c | 693 +++++++++++++++++++++++++++
18 files changed, 1657 insertions(+), 4 deletions(-)
create mode 100644 docs/system/arm/stm32l496g-disco.rst
create mode 100644 hw/arm/stm32l496g-disco.c
create mode 100644 hw/ssi/stm32l4x5_quadspi.c
create mode 100644 include/hw/ssi/stm32l4x5_quadspi.h
create mode 100644 tests/qtest/stm32l496g-disco-test.c
--
2.49.1
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash
2026-09-27 7:08 ` [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash ~mohammadodeh
@ 2026-11-14 10:20 ` Samuel Tardieu
0 siblings, 0 replies; 6+ messages in thread
From: Samuel Tardieu @ 2026-11-14 10:20 UTC (permalink / raw)
To: ~mohammadodeh
Cc: qemu-devel, Alexandre Iooss, Pierrick Bouvier, Peter Maydell,
Paolo Bonzini, Alistair Francis, Kevin Wolf, Hanna Reitz,
Fabiano Rosas, Laurent Vivier, Philippe Mathieu-Daudé,
Markus Armbruster, Cédric Le Goater, Bernhard Beschow,
Gaurav Sharma, Jan Kiszka, Bin Meng, Shameer Kolothum, Jamin Lin,
Marc-André Lureau, qemu-arm, qemu-block
~mohammadodeh <mohammadodeh@git.sr.ht> writes:
> { INFO("mx25l25655e", 0xc22619, 0, 64 << 10, 512, 0) },
> + { INFO("mx25r6435", 0xc22817, 0, 64 << 10, 128, ER_4K | ER_32K),
> + .sfdp_read = m25p80_sfdp_mx25r6435 },
> { INFO("mx66l51235f", 0xc2201a, 0, 64 << 10, 1024, ER_4K | ER_32K) },
By looking at the surrounding lines, it looks like the part suffix ("f" here) is usually included. Shouldn't it be "mx25r6435f" instead of "mx25r6435"?
> PP = 0x02,
> PP4 = 0x12,
> PP4_4 = 0x3e,
> + QPP_1_4_4 = 0x38,
> DPP = 0xa2,
> QPP = 0x32,
> QPP_4 = 0x34,
I would have put the `QPP_1_4_4` definition after the `QPP_4` to group the `QPP` instances.
> @@ -759,6 +762,7 @@ static void complete_collecting_data(Flash *s)
>
> switch (s->cmd_in_progress) {
> case DPP:
> + case QPP_1_4_4:
> case QPP:
> case QPP_4:
> case PP:
Same here.
Also, you haven't added a case to the switch in `get_addr_length()`. Is that because it matches the default case, or is that an oversight?
Best.
Sam
--
Samuel Tardieu
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support
2026-11-12 15:03 [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support ~mohammadodeh
` (2 preceding siblings ...)
2026-09-27 7:32 ` [PATCH qemu 3/3] hw/arm: Add STM32L496G-DISCO machine ~mohammadodeh
@ 2026-11-14 10:22 ` Samuel Tardieu
3 siblings, 0 replies; 6+ messages in thread
From: Samuel Tardieu @ 2026-11-14 10:22 UTC (permalink / raw)
To: ~mohammadodeh
Cc: qemu-devel, Alexandre Iooss, Pierrick Bouvier, Peter Maydell,
Paolo Bonzini, Alistair Francis, Kevin Wolf, Hanna Reitz,
Fabiano Rosas, Laurent Vivier, Philippe Mathieu-Daudé,
Markus Armbruster, Cédric Le Goater, Bernhard Beschow,
Gaurav Sharma, Jan Kiszka, Bin Meng, Shameer Kolothum, Jamin Lin,
Marc-André Lureau, qemu-arm, qemu-block
~mohammadodeh <mohammadodeh@git.sr.ht> writes:
> Add initial QEMU support for the STM32L496G-DISCO Discovery kit and
> its MX25R6435F QuadSPI flash.
This flash is also present on the B-L475E-IOT01A board which is supported by QEMU. You should probably also wire it there as well.
Best.
Sam
--
Samuel Tardieu
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-14 10:23 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12 15:03 [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support ~mohammadodeh
2026-09-27 7:08 ` [PATCH qemu 1/3] hw/block: Add Macronix MX25R6435F flash ~mohammadodeh
2026-11-14 10:20 ` Samuel Tardieu
2026-09-27 7:09 ` [PATCH qemu 2/3] hw/ssi: Add STM32L4x5 QuadSPI controller ~mohammadodeh
2026-09-27 7:32 ` [PATCH qemu 3/3] hw/arm: Add STM32L496G-DISCO machine ~mohammadodeh
2026-11-14 10:22 ` [PATCH qemu 0/3] hw/arm: Add STM32L496G-DISCO QuadSPI support Samuel Tardieu
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.