Skip to content

stmqspi: OCTOSPI DR 8-bit access is unreliable on STM32H7R7/XSPI, and unconditional ABORT may wedge BUSY #50

@nickfox-taterli

Description

@nickfox-taterli

Environment

OpenOCD version tested:

commit 0a084c29345576911fb8d10fcbd158d0361b6186 (HEAD -> openocd-cubeide-r7, tag: openocd-cubeide-v1.19.0, origin/openocd-cubeide-r7, origin/HEAD)
Author: Antonio Borneo <borneo.antonio@gmail.com>
Date:   Wed Nov 5 14:45:29 2025 +0100

Target:

  • STM32H7R7
  • XSPI / OCTOSPI external NOR flash
  • Board: RT-Thread STM32H7R7 minimal system
  • Probe: ST-LINK
  • Transport: dapdirect_swd

I found two problems in src/flash/nor/stmqspi.c on this target.
After applying the patch below, the board works correctly in my setup.


Problem 1: 8-bit access to OCTOSPI_DR is unreliable

Observed behavior

On STM32H7R7 / XSPI, direct 8-bit accesses to OCTOSPI_DR (offset 0x50) are not reliable in my setup.

The current code uses target_read_u8() / target_write_u8() on SPI_DR.
This causes incorrect behavior when reading status / SFDP / flash ID or when exchanging command data through DR on OCTOSPI.

I also observed a state where a 1-byte read completed with TCF=1 while FTF=0, so waiting only for FTF is not sufficient before draining DR.

Proposed fix

Introduce wrappers for DR byte access:

  • stmqspi_dr_read8()
  • stmqspi_dr_write8()

Behavior:

  • For non-OCTOSPI: keep the original target_read_u8() / target_write_u8()
  • For OCTOSPI: use target_read_u32() / target_write_u32() on OCTOSPI_DR, and only consume / drive the low 8 bits
  • Before read, wait until either FTF or TCF is set

This fixes the problem on my STM32H7R7 board.


Problem 2: unconditional ABORT can wedge BUSY when controller is already idle

Observed behavior

In some STM32H7R7 states, calling stmqspi_abort() while the controller is already idle can cause BUSY to get stuck.

Current behavior:

  • stmqspi_abort() always writes ABORT

This appears unsafe on H7RS in at least some OCTOSPI states.

Proposed fix

In stmqspi_abort():

  • read SR first (QSPI_SR or OCTOSPI_SR)
  • if BUSY == 0, return ERROR_OK immediately
  • only assert ABORT when BUSY == 1

This avoids wedging the controller in my setup.


Patch summary

Main changes in src/flash/nor/stmqspi.c:

  1. Wrap DR byte accesses:

    • stmqspi_dr_read8()
    • stmqspi_dr_write8()
  2. For OCTOSPI DR accesses:

    • use 32-bit target accesses instead of byte accesses
    • read/write only the low 8 bits
  3. Before DR read:

    • wait for FTF or TCF
  4. In stmqspi_abort():

    • do not force ABORT when BUSY == 0
  5. Minor correctness fix:

    • stmqspi_auto_probe() should return stmqspi_probe(bank) result instead of always returning ERROR_OK

Diff

diff --git a/src/flash/nor/stmqspi.c b/src/flash/nor/stmqspi.c
index a1e1d3411..e5fda2a31 100644
--- a/src/flash/nor/stmqspi.c
+++ b/src/flash/nor/stmqspi.c
@@ -173,6 +173,64 @@ struct stmqspi_flash_bank {
        unsigned int sfdp_dummy2;       /* number of dummy bytes for SFDP read for flash2 */
 };
 
+/*
+ * Some probe/targets are unreliable for byte accesses on OCTOSPI_DR (offset 0x50).
+ * Use 32-bit accesses for OCTOSPI and extract/inject low byte.
+ */
+static inline int stmqspi_dr_read8(struct flash_bank *bank, uint8_t *data)
+{
+       struct target *target = bank->target;
+       const struct stmqspi_flash_bank *stmqspi_info = bank->driver_priv;
+       const uint32_t io_base = stmqspi_info->io_base;
+       long long endtime = timeval_ms() + SPI_CMD_TIMEOUT;
+       bool ready = false;
+       uint32_t last_sr = 0;
+
+       /* Wait until RX FIFO has at least one byte. */
+       do {
+               uint32_t sr;
+               int retval = target_read_u32(target, io_base + (stmqspi_info->octo ? OCTOSPI_SR : QSPI_SR), &sr);
+               if (retval != ERROR_OK)
+                       return retval;
+               last_sr = sr;
+               /* On some STM32H7R/XSPI states, 1-byte reads complete with TCF set
+                * while FTF stays clear. Accept either flag before draining DR. */
+               if (sr & (BIT(SPI_FTF) | BIT(SPI_TCF))) {
+                       ready = true;
+                       break;
+               }
+               alive_sleep(1);
+       } while (timeval_ms() < endtime);
+
+       if (!ready) {
+               LOG_DEBUG("timeout waiting DR data (SR=0x%08" PRIx32 ")", last_sr);
+               return ERROR_TIMEOUT_REACHED;
+       }
+
+       if (stmqspi_info->octo) {
+               uint32_t v;
+               int retval = target_read_u32(target, io_base + OCTOSPI_DR, &v);
+               if (retval != ERROR_OK)
+                       return retval;
+               *data = (uint8_t)(v & 0xFFU);
+               return ERROR_OK;
+       }
+
+       return target_read_u8(target, io_base + QSPI_DR, data);
+}
+
+static inline int stmqspi_dr_write8(struct flash_bank *bank, uint8_t data)
+{
+       struct target *target = bank->target;
+       const struct stmqspi_flash_bank *stmqspi_info = bank->driver_priv;
+       const uint32_t io_base = stmqspi_info->io_base;
+
+       if (stmqspi_info->octo)
+               return target_write_u32(target, io_base + OCTOSPI_DR, (uint32_t)data);
+
+       return target_write_u8(target, io_base + QSPI_DR, data);
+}
+
 static inline int octospi_cmd(struct flash_bank *bank, uint32_t mode,
                uint32_t ccr, uint32_t ir)
 {
@@ -264,12 +322,21 @@ static int stmqspi_abort(struct flash_bank *bank)
        const struct stmqspi_flash_bank *stmqspi_info = bank->driver_priv;
        const uint32_t io_base = stmqspi_info->io_base;
        uint32_t cr;
+       uint32_t sr;
+       int retval;
 
-       int retval = target_read_u32(target, io_base + SPI_CR, &cr);
+       retval = target_read_u32(target, io_base + SPI_CR, &cr);
 
        if (retval != ERROR_OK)
                cr = 0;
 
+       /* H7RS: avoid asserting ABORT while controller is already idle.
+        * Forcing ABORT on idle can wedge BUSY in some states. */
+       retval = target_read_u32(target,
+               io_base + (stmqspi_info->octo ? OCTOSPI_SR : QSPI_SR), &sr);
+       if (retval == ERROR_OK && (sr & BIT(SPI_BUSY)) == 0)
+               return ERROR_OK;
+
        return target_write_u32(target, io_base + SPI_CR, cr | BIT(SPI_ABORT));
 }
 
@@ -370,7 +437,7 @@ static int read_status_reg(struct flash_bank *bank, uint16_t *status)
                if ((stmqspi_info->saved_cr & (BIT(SPI_DUAL_FLASH) | BIT(SPI_FSEL_FLASH)))
                        != BIT(SPI_FSEL_FLASH)) {
                        /* get status of flash 1 in dual mode or flash 1 only mode */
-                       retval = target_read_u8(target, io_base + SPI_DR, &data);
+                       retval = stmqspi_dr_read8(bank, &data);
                        if (retval != ERROR_OK)
                                goto err;
                        *status |= data;
@@ -378,7 +445,7 @@ static int read_status_reg(struct flash_bank *bank, uint16_t *status)
 
                if ((stmqspi_info->saved_cr & (BIT(SPI_DUAL_FLASH) | BIT(SPI_FSEL_FLASH))) != 0) {
                        /* get status of flash 2 in dual mode or flash 2 only mode */
-                       retval = target_read_u8(target, io_base + SPI_DR, &data);
+                       retval = stmqspi_dr_read8(bank, &data);
                        if (retval != ERROR_OK)
                                goto err;
                        *status |= ((uint16_t)data) << 8;
@@ -862,7 +929,7 @@ COMMAND_HANDLER(stmqspi_handle_cmd)
                for (count = 3; count < CMD_ARGC; count++) {
                        COMMAND_PARSE_NUMBER(u8, CMD_ARGV[count], data);
                        snprintf(temp, sizeof(temp), "%02" PRIx8 " ", data);
-                       retval = target_write_u8(target, io_base + SPI_DR, data);
+                       retval = stmqspi_dr_write8(bank, data);
                        if (retval != ERROR_OK)
                                goto err;
                        strncat(output, temp, sizeof(output) - strlen(output) - 1);
@@ -905,7 +972,7 @@ COMMAND_HANDLER(stmqspi_handle_cmd)
 
                /* read response bytes */
                for ( ; num_read > 0; num_read--) {
-                       retval = target_read_u8(target, io_base + SPI_DR, &data);
+                       retval = stmqspi_dr_read8(bank, &data);
                        if (retval != ERROR_OK)
                                goto err;
                        snprintf(temp, sizeof(temp), "%02" PRIx8 " ", data);
@@ -1769,12 +1836,12 @@ static int find_sfdp_dummy(struct flash_bank *bank, int len)
        for (count = 0 ; count < max_bytes; count++) {
                if ((dual != 0) && !flash1) {
                        /* discard even byte in dual flash-mode if flash2 */
-                       retval = target_read_u8(target, io_base + SPI_DR, &data);
+                       retval = stmqspi_dr_read8(bank, &data);
                        if (retval != ERROR_OK)
                                goto err;
                }
 
-               retval = target_read_u8(target, io_base + SPI_DR, &data);
+               retval = stmqspi_dr_read8(bank, &data);
                if (retval != ERROR_OK)
                        goto err;
 
@@ -1790,7 +1857,7 @@ static int find_sfdp_dummy(struct flash_bank *bank, int len)
 
                if ((dual != 0) && flash1) {
                        /* discard odd byte in dual flash-mode if flash1 */
-                       retval = target_read_u8(target, io_base + SPI_DR, &data);
+                       retval = stmqspi_dr_read8(bank, &data);
                        if (retval != ERROR_OK)
                                goto err;
                }
@@ -1888,7 +1955,7 @@ static int read_sfdp_block(struct flash_bank *bank, uint32_t addr,
 
        /* dummy clocks */
        for (count = *dummy << dual; count > 0; --count) {
-               retval = target_read_u8(target, io_base + SPI_DR, (uint8_t *)buffer);
+               retval = stmqspi_dr_read8(bank, (uint8_t *)buffer);
                if (retval != ERROR_OK)
                        goto err;
        }
@@ -2016,7 +2083,7 @@ static int read_flash_id(struct flash_bank *bank, uint32_t *id1, uint32_t *id2)
                for (len1 = 0, len2 = 0; count > 0; --count) {
                        if ((stmqspi_info->saved_cr & (BIT(SPI_DUAL_FLASH) |
                                BIT(SPI_FSEL_FLASH))) != BIT(SPI_FSEL_FLASH)) {
-                               retval = target_read_u8(target, io_base + SPI_DR, &byte);
+                               retval = stmqspi_dr_read8(bank, &byte);
                                if (retval != ERROR_OK)
                                        goto err;
                                /* collect 3 bytes without continuation codes */
@@ -2027,7 +2094,7 @@ static int read_flash_id(struct flash_bank *bank, uint32_t *id1, uint32_t *id2)
                        }
                        if ((stmqspi_info->saved_cr & (BIT(SPI_DUAL_FLASH) |
                                BIT(SPI_FSEL_FLASH))) != 0) {
-                               retval = target_read_u8(target, io_base + SPI_DR, &byte);
+                               retval = stmqspi_dr_read8(bank, &byte);
                                if (retval != ERROR_OK)
                                        goto err;
                                /* collect 3 bytes without continuation codes */
@@ -2369,8 +2436,8 @@ static int stmqspi_auto_probe(struct flash_bank *bank)
 
        if (stmqspi_info->probed)
                return ERROR_OK;
-       stmqspi_probe(bank);
-       return ERROR_OK;
+
+       return stmqspi_probe(bank);
 }

rt-thread stm32h7r board config used for validation

source [find interface/stlink-dap.cfg]
transport select dapdirect_swd

set CHIPNAME stm32h7r7
set WORKAREASIZE 0x20000

source [find target/stm32h7rx.cfg]

flash bank ${CHIPNAME}.xspi2 stmqspi 0x70000000 0 0 0 ${CHIPNAME}.cpu0 0x5200A000

proc qspi_init { } {
    mmw 0x5802480c 0x00008000 0x0
    mmw 0x58024534 0x00005000 0x0
    mmw 0x58024540 0x00002000 0x0
    mmw 0x58024554 0x00000002 0x0
    mmw 0x58000510 0x00040010 0x0

    mww 0x5200B400 0x00000010

    mmw 0x58023400 0x00AA2AAA 0x00FF3FFF
    mmw 0x58023408 0x00FF3FFF 0x00FF3FFF
    mmw 0x5802340C 0x00000000 0x00FF3FFF
    mmw 0x58023420 0x09999999 0x0FFFFFFF
    mmw 0x58023424 0x00009999 0x0000FFFF

    mww 0x5200A130 0x00001000
    mww 0x5200A008 0x01190200
    mww 0x5200A00C 0x00000004
    mww 0x5200A108 0x00000000
    mww 0x5200A100 0x01003101
    mww 0x5200A110 0x00000013
    mww 0x5200A000 0x30400009

    flash probe 1
    stmqspi set 1 w35t51nwtbie 0x4000000 0x100 0x13 0x0c 0x12 0x60 0x10000 0xdc
}

${CHIPNAME}.cpu0 configure -event reset-init {
    qspi_init
}

Notes

  • The patch is validated on my STM32H7R7 + XSPI setup.

  • I cannot guarantee yet that all STM32 OCTOSPI variants behave the same way.

  • But at least on this target:

    • byte accesses to OCTOSPI_DR are problematic
    • unconditional ABORT while idle is unsafe
  • The patch keeps the original path for non-OCTOSPI users.

If needed, I can also help split this into separate commits / MRs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions