[Solved/workaround] GIGA R1 DFU uploads fail (exit status 74) after a 100/0 sketch grows past 768 KiB

I am posting this in case it saves someone else from a few hours of digging around with the Giga, or replacing a healthy board

Full disclosure I've used genAI for a lot of the trial and error debugging process, as well as the recovery sketch. Not my proudest moment, but it's saved me days of head scratching (or even more likely just chucking the board thinking it was dead) While it has worked well for me and is reproducible, take everything here with a healthy pinch of salt

Symptoms

My GIGA uploaded normally for months using the 2 MB M7 + M4 in SDRAM (100_0) flash split. After the M7 program grew beyond roughly 768 KiB, a normal Arduino upload failed with:

Device stuck after special command request
Failed uploading: uploading error: exit status 74

After that, even uploading Blink through the Arduino IDE failed. Re-flashing the bootloader worked, but didn't clear the upload stall. The board was still visible in STM32 system DFU mode, and STM32CubeProgrammer could erase and program it manually.

I initially suspected worn flash, but a brand-new board developed the same fault after several uploads of the same large project.

Likely explanation

With 100_0, the M7 application is allowed to grow into internal Flash Bank 2. On both affected boards, the persistent M4 automatic-boot setting was still enabled and pointed to the beginning of Bank 2 (0x08100000), exactly 768 KiB into an M7 application starting at 0x08040000.

Once the M7 program crossed that address, the M4 could treat M7 program data as its own startup information. The values we read there were not a valid M4 startup table. This appears to interfere with flash operations and leave the Arduino bootloader permanently busy during an upload.

That last part is a well-supported explanation, not a fully traced proof. What is proven is that clearing Bank 2 and disabling M4 automatic boot restored normal uploads on two boards.

Before starting

Important: This procedure is for an M7-only project using the
100_0 split
. Do not use it unchanged if your project intentionally runs
code on the M4.

You need:

  • Windows PowerShell
  • STM32CubeProgrammer;
  • Arduino IDE with Arduino Mbed OS Giga Boards installed; and
  • no other STM32 boards connected in DFU mode.

Data-loss warning: This procedure deliberately erases internal Flash
Bank 2, from 0x08100000 through 0x081FFFFF.

That removes the existing application tail or M4 image in that range. It
does not touch the Arduino bootloader at 0x08000000 or external QSPI
storage.

If your bootloader has already been erased, restore it first using Arduino's GIGA bootloader guide.

Step 1: Enter STM32 system DFU

  1. Disconnect USB power.
  2. Hold BOOT0.
  3. Reconnect USB or press and release RST while holding BOOT0.
  4. Release BOOT0.

Open PowerShell and set the CubeProgrammer path:

$cli = "C:\Program Files\STMicroelectronics\STM32Cube\STM32CubeProgrammer\bin\STM32_Programmer_CLI.exe"
$port = "USB1"
& $cli -c "port=$port"

If your board is listed as USB2, change $port accordingly.

Stop if the output does not identify:

Product ID : DFU in FS Mode
Device ID  : 0x450
Device CPU : Cortex-M7
NVM size   : 2 MBytes

Display the normal option bytes:

& $cli -c "port=$port" -ob displ

For the boards recovered here, read protection was Level 0 (RDP = 0xAA), security and bank swap were disabled, and no sectors were write-protected. Stop and investigate rather than continuing if your board is protected or has bank swap enabled.

Step 2: Record the original option state

Read the current and staged option words:

& $cli -c "port=$port" -r32 0x5200201C 0x08

Save the output. The first word is OPTSR_CUR; the second is OPTSR_PRG. They should match before continuing.

On my affected boards they were:

OPTSR_CUR = 0x0BC6AAF0
OPTSR_PRG = 0x0BC6AAF0

Do not write that example value directly to your board: other option bits may legitimately differ. The recovery sketch below changes only the BCM4 bit.

You can also inspect the first 32 bytes of Bank 2:

& $cli -c "port=$port" -r32 0x08100000 0x20

Step 3: Create an exact 1 MiB blank image

Run this in the same PowerShell window:

$blank = Join-Path $env:TEMP "giga-bank2-blank.bin"
$bytes = New-Object byte[] 0x100000
for ($i = 0; $i -lt $bytes.Length; $i++) {
    $bytes[$i] = 0xFF
}
[IO.File]::WriteAllBytes($blank, $bytes)
(Get-Item $blank).Length

The final command must print:

1048576

Step 4: Erase only Bank 2 and verify every byte

Writing the all-0xFF file at the exact Bank 2 address makes CubeProgrammer erase only the sectors covered by that range:

& $cli -c "port=$port" -w $blank 0x08100000 -v

Do not continue if CubeProgrammer reports an error.

Now upload the complete bank and verify that every byte is 0xFF:

$readback = Join-Path $env:TEMP "giga-bank2-readback.bin"
& $cli -c "port=$port" --upload 0x08100000 0x100000 $readback

$data = [IO.File]::ReadAllBytes($readback)
$badByte = $false
foreach ($value in $data) {
    if ($value -ne 0xFF) {
        $badByte = $true
        break
    }
}

if ($data.Length -ne 0x100000 -or $badByte) {
    throw "Bank 2 did not verify as completely blank."
}
"Bank 2 verified blank."

Only continue if it prints Bank 2 verified blank.

Step 5: Prove normal Arduino uploading works

  1. Disconnect USB power.
  2. Leave BOOT0 in its normal state.
  3. Reconnect the board and double-tap RST.
  4. Upload the normal Arduino Blink example to the M7 core using the standard Arduino IDE uploader.

Blink should now upload and run normally. This proved on both affected boards that removing the invalid Bank 2 contents was enough to release the upload failure.

Step 6: Disable persistent M4 automatic boot

Create a new Arduino sketch, paste the following code, select the GIGA R1
M7 core, and upload it normally.

Recovery sketch — click to expand
#include <Arduino.h>
#include "stm32h7xx_hal.h"

static void hexValue(const char *name, uint32_t value)
{
    Serial.print(name);
    Serial.print(": 0x");
    Serial.println(value, HEX);
}

static void lockFlash()
{
    HAL_FLASH_Lock();
    HAL_FLASH_OB_Lock();
}

void setup()
{
    Serial.begin(115200);
    const uint32_t started = millis();
    while (!Serial && millis() - started < 5000U) {}

    Serial.println("GIGA M4 auto-boot recovery");
    hexValue("Initial SYSCFG->UR1", SYSCFG->UR1);
    hexValue("Initial OPTSR_CUR", FLASH->OPTSR_CUR);
    hexValue("Initial OPTSR_PRG", FLASH->OPTSR_PRG);
    Serial.print("Effective BCM4 state: ");
    Serial.println((SYSCFG->UR1 & SYSCFG_UR1_BCM4) ? 1 : 0);

    if ((SYSCFG->UR1 & SYSCFG_UR1_BCM4) == 0U) {
        Serial.println("BCM4 is already disabled; recovery complete.");
        return;
    }

    const uint32_t stableMask =
        ~(FLASH_OPTSR_OPT_BUSY | FLASH_OPTSR_OPTCHANGEERR);

    if ((FLASH->OPTSR_CUR &
            (FLASH_OPTSR_OPT_BUSY | FLASH_OPTSR_OPTCHANGEERR)) != 0U ||
        (FLASH->OPTSR_CUR & stableMask) !=
            (FLASH->OPTSR_PRG & stableMask) ||
        FLASH->PRAR_CUR1 != FLASH->PRAR_PRG1 ||
        FLASH->SCAR_CUR1 != FLASH->SCAR_PRG1 ||
        FLASH->WPSN_CUR1 != FLASH->WPSN_PRG1 ||
        FLASH->BOOT7_CUR != FLASH->BOOT7_PRG ||
        FLASH->BOOT4_CUR != FLASH->BOOT4_PRG ||
        FLASH->PRAR_CUR2 != FLASH->PRAR_PRG2 ||
        FLASH->SCAR_CUR2 != FLASH->SCAR_PRG2 ||
        FLASH->WPSN_CUR2 != FLASH->WPSN_PRG2) {
        Serial.println("Option state is busy or has a staged change; aborting.");
        return;
    }

    __HAL_FLASH_CLEAR_FLAG_BANK1(FLASH_FLAG_PGSERR_BANK1);
    delay(10);
    if ((FLASH->SR1 & FLASH_SR_PGSERR) != 0U) {
        Serial.println("PGSERR reappeared; aborting without changing options.");
        return;
    }

    FLASH_OBProgramInitTypeDef ob = {};
    ob.Banks = FLASH_BANK_1;
    HAL_FLASHEx_OBGetConfig(&ob);

    const uint32_t optsrBefore = FLASH->OPTSR_PRG;
    const uint32_t prar1 = FLASH->PRAR_PRG1;
    const uint32_t scar1 = FLASH->SCAR_PRG1;
    const uint32_t wpsn1 = FLASH->WPSN_PRG1;
    const uint32_t boot7 = FLASH->BOOT7_PRG;
    const uint32_t boot4 = FLASH->BOOT4_PRG;
    const uint32_t prar2 = FLASH->PRAR_PRG2;
    const uint32_t scar2 = FLASH->SCAR_PRG2;
    const uint32_t wpsn2 = FLASH->WPSN_PRG2;

    if (HAL_FLASH_OB_Unlock() != HAL_OK ||
        HAL_FLASH_Unlock() != HAL_OK) {
        Serial.println("Flash unlock failed; aborting.");
        lockFlash();
        return;
    }

    ob.OptionType = OPTIONBYTE_USER;
    ob.USERType = OB_USER_BCM4;
    ob.USERConfig = OB_BCM4_DISABLE;

    const HAL_StatusTypeDef status = HAL_FLASHEx_OBProgram(&ob);
    Serial.print("HAL_FLASHEx_OBProgram: ");
    Serial.println(static_cast<unsigned int>(status));
    hexValue("HAL flash error", HAL_FLASH_GetError());

    if (status != HAL_OK ||
        FLASH->OPTSR_PRG !=
            (optsrBefore & ~FLASH_OPTSR_BCM4) ||
        FLASH->PRAR_PRG1 != prar1 ||
        FLASH->SCAR_PRG1 != scar1 ||
        FLASH->WPSN_PRG1 != wpsn1 ||
        FLASH->BOOT7_PRG != boot7 ||
        FLASH->BOOT4_PRG != boot4 ||
        FLASH->PRAR_PRG2 != prar2 ||
        FLASH->SCAR_PRG2 != scar2 ||
        FLASH->WPSN_PRG2 != wpsn2) {
        Serial.println("Programming failed or another option changed.");
        Serial.println("Option reload blocked.");
        lockFlash();
        return;
    }

    Serial.println("Only BCM4 is staged to change.");
    Serial.println("Launching option reload; the board should reset.");
    Serial.flush();
    delay(250);
    HAL_FLASH_OB_Launch();
}

void loop()
{
    delay(1000);
}

Open Serial Monitor at 115200 baud. A successful first run should report:

HAL_FLASHEx_OBProgram: 0
HAL flash error: 0x0
Only BCM4 is staged to change.
Launching option reload; the board should reset.

After the automatic reset, the sketch should report:

Initial SYSCFG->UR1: 0x10000
Effective BCM4 state: 0
BCM4 is already disabled; recovery complete.

If it reports an abort or a non-zero HAL result, stop rather than
repeatedly trying option programming.

Step 7: Independently check the changed option bit

Re-enter STM32 system DFU mode and read the option words again:

& $cli -c "port=$port" -r32 0x5200201C 0x08

Both words should again match. The only change from the values saved in Step 2 should be bit 0x00400000 becoming clear.

For example, my board changed from:

before: 0x0BC6AAF0
after:  0x0B86AAF0
XOR:    0x00400000

Again, compare against your own saved value rather than assuming the example value is correct for every board.

Step 8: Upload the large application twice

Return BOOT0 to normal, fully power-cycle, double-tap RST and upload your 100_0 M7 application using the normal Arduino uploader.

Then immediately upload it a second time. Both uploads should complete normally even if the application occupies Bank 2.

That final repeat upload is important: it demonstrates that the original failure does not return once the M4 automatic-boot setting is disabled.

1 Like