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:
-
Wrap DR byte accesses:
stmqspi_dr_read8()
stmqspi_dr_write8()
-
For OCTOSPI DR accesses:
- use 32-bit target accesses instead of byte accesses
- read/write only the low 8 bits
-
Before DR read:
-
In stmqspi_abort():
- do not force
ABORT when BUSY == 0
-
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.
Environment
OpenOCD version tested:
Target:
dapdirect_swdI found two problems in
src/flash/nor/stmqspi.con this target.After applying the patch below, the board works correctly in my setup.
Problem 1: 8-bit access to
OCTOSPI_DRis unreliableObserved behavior
On STM32H7R7 / XSPI, direct 8-bit accesses to
OCTOSPI_DR(offset0x50) are not reliable in my setup.The current code uses
target_read_u8()/target_write_u8()onSPI_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=1whileFTF=0, so waiting only forFTFis not sufficient before draining DR.Proposed fix
Introduce wrappers for DR byte access:
stmqspi_dr_read8()stmqspi_dr_write8()Behavior:
target_read_u8()/target_write_u8()target_read_u32()/target_write_u32()onOCTOSPI_DR, and only consume / drive the low 8 bitsFTForTCFis setThis fixes the problem on my STM32H7R7 board.
Problem 2: unconditional
ABORTcan wedge BUSY when controller is already idleObserved behavior
In some STM32H7R7 states, calling
stmqspi_abort()while the controller is already idle can causeBUSYto get stuck.Current behavior:
stmqspi_abort()always writesABORTThis appears unsafe on H7RS in at least some OCTOSPI states.
Proposed fix
In
stmqspi_abort():SRfirst (QSPI_SRorOCTOSPI_SR)BUSY == 0, returnERROR_OKimmediatelyABORTwhenBUSY == 1This avoids wedging the controller in my setup.
Patch summary
Main changes in
src/flash/nor/stmqspi.c:Wrap DR byte accesses:
stmqspi_dr_read8()stmqspi_dr_write8()For OCTOSPI DR accesses:
Before DR read:
FTForTCFIn
stmqspi_abort():ABORTwhenBUSY == 0Minor correctness fix:
stmqspi_auto_probe()should returnstmqspi_probe(bank)result instead of always returningERROR_OKDiff
rt-thread stm32h7r board config used for validation
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:
OCTOSPI_DRare problematicABORTwhile idle is unsafeThe patch keeps the original path for non-OCTOSPI users.
If needed, I can also help split this into separate commits / MRs.