No magic
Stanislav Dombrovsky
Mobile Developer in the past. Former product manager in AppCode. Engineer.
· 13 min read

STM32 DFU Bootloader How-To

You may think creating a DFU bootloader is simple — there are plenty of articles about it. The problem is that many of them don’t work. That’s why I decided to cover the critical aspects here.

Bootloader project

Most likely you don’t want to add the bootloader directly to your firmware. So let’s create a separate bootloader project in CubeMX. Of course, you can use STM32CubeStudio and the process will be more or less the same, but for now we’ll assume you’re using STM32Cube for that.

General configuration

Start a blank project with CubeMX for your MCU. I use STM32F103RCT6; you can use any other processor. Set RCC | High Speed Clock (HSE) to Crystal/Ceramic Resonator:

Set SYS | Debug to Serial Wire (if you’re debugging via SWD):

Bootloader-specific configuration

Enable Connectivity | USB | Device (FS). Leave the default properties:

In Middleware | USB_DEVICE:

  • Set Class for FS IP to Download Firmware Update Class (DFU)
  • Set Parameter Settings | USBD_DFU_XFER_SIZE to 32768. That’s the maximum transfer size and it works. If it doesn’t in your case, set it to 1024. Those settings should work without problems.
  • Set Parameter Settings | USBD_DFU_MEDIA Interface to @Internal Flash /0x08000000/02*016Ka,02*016Kg,01*064Kg,03*128Kg. Double‑check this parameter — it defines the flash layout. By default, STM32Cube generates a layout that requires 0x0800C000 for USBD_DFU_APP_DEFAULT_ADD (the start address for your firmware). It works, but there’s no reason to give a simple bootloader that much space, so we use an alternative layout that works only with USBD_DFU_APP_DEFAULT_ADD set to 0x08008000.
  • Set Parameter Settings | USBD_DFU_APP_DEFAULT_ADD to 0x08008000. Note that this address will work only with the flash layout above.

All settings:

Also, locate your BOOT1 pin in the datasheet and initialize it as an input. It’s not strictly required — it’s just one way to decide whether to boot into the bootloader or the main firmware. You can implement any other condition in main(), but I use BOOT1 because I have a jumper on my board.

Cool! Now we can generate the project and open it in some IDE.

Bootloader code

main.c (you need only the main() function from here; otherwise you may run into differences in clock configuration):

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2020 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under Ultimate Liberty license
  * SLA0044, the "License"; You may not use this file except in compliance with
  * the License. You may obtain a copy of the License at:
  *                             www.st.com/SLA0044
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usb_device.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
typedef  void (*pFunction)(void);

pFunction JumpToApplication;
uint32_t JumpAddress;

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */
/* Initialize all configured peripherals */
    MX_GPIO_Init();

    if(HAL_GPIO_ReadPin(boot1_GPIO_Port, boot1_Pin ) == GPIO_PIN_SET)
    {
        /* Test if user code is programmed starting from address 0x08008000 */
        if (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0x2FFC0000) == 0x20000000)
        {

            /* Jump to user application */
            JumpAddress = *(__IO uint32_t *) (USBD_DFU_APP_DEFAULT_ADD + 4);
            JumpToApplication = (pFunction) JumpAddress;

            /* Initialize user application's Stack Pointer */
            __set_MSP(*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD);
            JumpToApplication();
        }
    }
  /* USER CODE END SysInit */



  MX_USB_DEVICE_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USB;
  PeriphClkInit.UsbClockSelection = RCC_USBCLKSOURCE_PLL_DIV1_5;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

Note: some articles define the start and end addresses of the processor flash as separate variables. In fact, there’s no reason to do that. The start address is always USBD_DFU_APP_DEFAULT_ADD. The end address is always FLASH_BANK1_END or FLASH_BANK2_END depending on your MCU.

The next file you need to modify is usbd_dfu_if.c (here just copy-paste it, it 100% correct):

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : usbd_dfu_if.c
  * @brief          : Usb device for Download Firmware Update.
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2020 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under Ultimate Liberty license
  * SLA0044, the "License"; You may not use this file except in compliance with
  * the License. You may obtain a copy of the License at:
  *                             www.st.com/SLA0044
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "usbd_dfu_if.h"

/* USER CODE BEGIN INCLUDE */

/* USER CODE END INCLUDE */

/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/

/* USER CODE END PV */

/** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY
  * @brief Usb device.
  * @{
  */

/** @defgroup USBD_DFU
  * @brief Usb DFU device module.
  * @{
  */

/** @defgroup USBD_DFU_Private_TypesDefinitions
  * @brief Private types.
  * @{
  */

/* USER CODE BEGIN PRIVATE_TYPES */

/* USER CODE END PRIVATE_TYPES */

/**
  * @}
  */

/** @defgroup USBD_DFU_Private_Defines
  * @brief Private defines.
  * @{
  */

#define FLASH_DESC_STR      "@Internal Flash   /0x08000000/02*016Ka,02*016Kg,01*064Kg,03*128Kg"

/* USER CODE BEGIN PRIVATE_DEFINES */

/* USER CODE END PRIVATE_DEFINES */

/**
  * @}
  */

/** @defgroup USBD_DFU_Private_Macros
  * @brief Private macros.
  * @{
  */

/* USER CODE BEGIN PRIVATE_MACRO */

/* USER CODE END PRIVATE_MACRO */

/**
  * @}
  */

/** @defgroup USBD_DFU_Private_Variables
  * @brief Private variables.
  * @{
  */

/* USER CODE BEGIN PRIVATE_VARIABLES */

/* USER CODE END PRIVATE_VARIABLES */

/**
  * @}
  */

/** @defgroup USBD_DFU_Exported_Variables
  * @brief Public variables.
  * @{
  */

extern USBD_HandleTypeDef hUsbDeviceFS;

/* USER CODE BEGIN EXPORTED_VARIABLES */
#define FLASH_ERASE_TIME    (uint16_t)50
#define FLASH_PROGRAM_TIME  (uint16_t)50
/* USER CODE END EXPORTED_VARIABLES */

/**
  * @}
  */

/** @defgroup USBD_DFU_Private_FunctionPrototypes
  * @brief Private functions declaration.
  * @{
  */

static uint16_t MEM_If_Init_FS(void);
static uint16_t MEM_If_Erase_FS(uint32_t Add);
static uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len);
static uint8_t *MEM_If_Read_FS(uint8_t *src, uint8_t *dest, uint32_t Len);
static uint16_t MEM_If_DeInit_FS(void);
static uint16_t MEM_If_GetStatus_FS(uint32_t Add, uint8_t Cmd, uint8_t *buffer);

/* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION */

/* USER CODE END PRIVATE_FUNCTIONS_DECLARATION */

/**
  * @}
  */

#if defined ( __ICCARM__ ) /* IAR Compiler */
  #pragma data_alignment=4
#endif
__ALIGN_BEGIN USBD_DFU_MediaTypeDef USBD_DFU_fops_FS __ALIGN_END =
{
   (uint8_t*)FLASH_DESC_STR,
    MEM_If_Init_FS,
    MEM_If_DeInit_FS,
    MEM_If_Erase_FS,
    MEM_If_Write_FS,
    MEM_If_Read_FS,
    MEM_If_GetStatus_FS
};

/* Private functions ---------------------------------------------------------*/
/**
  * @brief  Memory initialization routine.
  * @retval USBD_OK if operation is successful, MAL_FAIL else.
  */
uint16_t MEM_If_Init_FS(void)
{
  /* USER CODE BEGIN 0 */

    HAL_StatusTypeDef flash_ok = HAL_ERROR;

    //Делаем память открытой
    while(flash_ok != HAL_OK){
        flash_ok = HAL_FLASH_Unlock();
    }
    return (USBD_OK);
  /* USER CODE END 0 */
}

/**
  * @brief  De-Initializes Memory
  * @retval USBD_OK if operation is successful, MAL_FAIL else
  */
uint16_t MEM_If_DeInit_FS(void)
{
  /* USER CODE BEGIN 1 */

    HAL_StatusTypeDef flash_ok = HAL_ERROR;

    //Закрываем память
    flash_ok = HAL_ERROR;
    while(flash_ok != HAL_OK){
        flash_ok = HAL_FLASH_Lock();
    }
    return (USBD_OK);
  /* USER CODE END 1 */
}

/**
  * @brief  Erase sector.
  * @param  Add: Address of sector to be erased.
  * @retval 0 if operation is successful, MAL_FAIL else.
  */
uint16_t MEM_If_Erase_FS(uint32_t Add)
{
  /* USER CODE BEGIN 2 */


    uint32_t NbOfPages = 0;
    uint32_t PageError = 0;
    /* Variable contains Flash operation status */
    HAL_StatusTypeDef status;
    FLASH_EraseInitTypeDef eraseinitstruct;

    /* Get the number of sector to erase from 1st sector*/
    uint32_t flashEnd = 0;
    #if defined(FLASH_BANK2_END)
       flashEnd = FLASH_BANK2_END;
    #else
       flashEnd = FLASH_BANK1_END;
    #endif

    NbOfPages = ((flashEnd - USBD_DFU_APP_DEFAULT_ADD) / FLASH_PAGE_SIZE) + 1;
    eraseinitstruct.TypeErase = FLASH_TYPEERASE_PAGES;
    eraseinitstruct.PageAddress = USBD_DFU_APP_DEFAULT_ADD;
    eraseinitstruct.NbPages = NbOfPages;
    status = HAL_FLASHEx_Erase(&eraseinitstruct, &PageError);

    if (status != HAL_OK)
    {
        return (!USBD_OK);
    }
    return (USBD_OK);
  /* USER CODE END 2 */
}

/**
  * @brief  Memory write routine.
  * @param  src: Pointer to the source buffer. Address to be written to.
  * @param  dest: Pointer to the destination buffer.
  * @param  Len: Number of data to be written (in bytes).
  * @retval USBD_OK if operation is successful, MAL_FAIL else.
  */
uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* USER CODE BEGIN 3 */

    uint32_t i = 0;

    for(i = 0; i < Len; i+=4)
    {
        /* Device voltage range supposed to be [2.7V to 3.6V], the operation will
           be done by byte */
        if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, (uint32_t)(dest+i), *(uint32_t*)(src+i)) == HAL_OK)
        {
            /* Check the written value */
            if(*(uint32_t *)(src + i) != *(uint32_t*)(dest+i))
            {
                /* Flash content doesn't match SRAM content */
                return 2;
            }
        }
        else
        {

            /* Error occurred while writing data in Flash memory */
            return 1;
        }
    }

    return (USBD_OK);
  /* USER CODE END 3 */
}

/**
  * @brief  Memory read routine.
  * @param  src: Pointer to the source buffer. Address to be written to.
  * @param  dest: Pointer to the destination buffer.
  * @param  Len: Number of data to be read (in bytes).
  * @retval Pointer to the physical address where data should be read.
  */
uint8_t *MEM_If_Read_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* Return a valid address to avoid HardFault */
  /* USER CODE BEGIN 4 */
    uint32_t i = 0;
    uint8_t *psrc = src;

    for (i = 0; i < Len; i++)
    {
        dest[i] = *psrc++;
    }
    return (uint8_t*)(dest);
  /* USER CODE END 4 */
}

/**
  * @brief  Get status routine
  * @param  Add: Address to be read from
  * @param  Cmd: Number of data to be read (in bytes)
  * @param  buffer: used for returning the time necessary for a program or an erase operation
  * @retval USBD_OK if operation is successful
  */
uint16_t MEM_If_GetStatus_FS(uint32_t Add, uint8_t Cmd, uint8_t *buffer)
{
  /* USER CODE BEGIN 5 */

    switch (Cmd)
    {
        case DFU_MEDIA_PROGRAM:
            buffer[1] = (uint8_t)FLASH_PROGRAM_TIME;
            buffer[2] = (uint8_t)(FLASH_PROGRAM_TIME << 8);
            buffer[3] = 0;
            break;

        case DFU_MEDIA_ERASE:
        default:
            buffer[1] = (uint8_t)FLASH_ERASE_TIME;
            buffer[2] = (uint8_t)(FLASH_ERASE_TIME << 8);
            buffer[3] = 0;
            break;
    }
    return  (USBD_OK);
  /* USER CODE END 5 */
}

/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */

/* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */

/**
  * @}
  */

/**
  * @}
  */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

That’s all you need for the bootloader. Flash it, set BOOT1 to 0 (if you’re using it at all), and continue to the target firmware modifications.

Target firmware

Here you need to modify the following:

  • Find your linker file, in my case it’s STM32F103RCTX_FLASH.ld. Change FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K to FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 256K, e.g. set the start address of the flash.
  • Change FLASH_BASE to 0x08008000UL

In short, with these two steps you ensure your firmware runs from the address the bootloader expects and that the vector table is relocated. After this modification, you should be able to debug your firmware as before. Set BOOT1 to 1 and flash the newly built firmware with your favorite GDB server and IDE. If you’ve done everything correctly, you should be able to debug it. Otherwise, don’t try to load it using the DFU utility — it won’t work.

Tip: if you’re doing this DFU thing for the first time, I strongly recommend having a very simple blink project to experiment with. Why? Because my complex project didn’t work well after this modification — hard faults, strange errors, etc. That’s not a bootloader problem; it’s something in your firmware, even if it worked before. If it just works, you’re lucky. If it doesn’t, start with a simple project that works. Verify that your bootloader can successfully flash it via the DFU utility and boot it. Next, start with a blank project and incrementally add your code to find what’s wrong. In my case, I had to recreate the whole project structure from scratch. It was the fastest way to solve the problem instead of digging forever.

If you think it’s FreeRTOS causing issues, or C++ instead of C, and you need extra memory relocation — no. For this bootloader you don’t need to change anything else. Check everything again.

DFuSe Utility and dfu-util

Various guides recommend the DFuSe utility from ST on Windows. In short, if you want to work with an obsolete Windows driver and always convert your .hex files to .dfu to check whether your bootloader works — and stick to a Windows‑only environment — you can use it.

But I do not recommend it just because using the dfu-util is much more straightforward.

Windows

If you’ve installed the STM32 driver that works with DFuSe, it won’t work with dfu-util. Do this instead:

  • Delete the device and its driver from Windows Device Manager
  • Install libusb-based driver using Zadig

After that, dfu-util should work.

macOS

For me dfu-util didn’t work on macOS until this patch was applied. For you it may work without a problem.

How to upload the firmware

Execute dfu-util -l. Find the DFU device. The output should be like this:

Found DFU: [0483:df11] ver=0200, devnum=9, cfg=1, intf=0, path="20-4.2", alt=0, name="@Internal Flash   /0x08000000/02*016Ka,02*016Kg,01*064Kg,03*128Kg", serial="5CE4826C3236"

If you have something like this:

Found DFU: [0483:df11] ver=0200, devnum=9, cfg=1, intf=0, path="20-4.2", alt=0, name="UNKNOWN", serial="UNKNOWN"

you need to reset the DFU device (unplug - plug in).

As soon as you have the DFU device correctly identified, execute the following line to upload the firmware:

dfu-util -a 0 -s 0x08008000 -D <path to .bin file>

Here -a should be equal to alt parameter from dfu-util -l output, -s equals the USBD_DFU_APP_DEFAULT_ADD. Optionally, add -v -v to have a verbose output in case of problems. This command should successfully upload the firmware. If you’re on macOS and applied this patch, add -T 10 or set the longer timeout.

Resetting USB on macOS

If you are tired from unplugging your device, use this utility or the same utility in the precompiled form USB Prober app. It just works.

Bonus: video guide from STM32 that actually works