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

STM32 DFU Bootloader How-To

You may think that creating a DFU bootloader is a simple process - there are many various articles about it. The problem is that most of them do not work. That’s why I’ve decided to cover various aspects here.

Bootloader project

Most probably you don’t want to add the bootloader directly into 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 the moment - let’s just assume you’re using the STM32Cube for that.

General configuration

Start the blank project with CubeMX for your processor. 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 (of course if you’re debugging using SWD):

Bootloader-specific configuration

Enable Connectivity | USB | Device (FS). Leave all 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 speed of transfer and it works. If it does not in your case, simply set it to 1024. Such settings should work without any problems.
  • Set Parameter Settings | USBD_DFU_MEDIA Inteface 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 the flash layout that requires 0x0800C000 value for USBD_DFU_APP_DEFAULT_ADD (and so the start address for your firmware). It works, but there is no reason for giving the simple bootloader such a lot of space, so we use another 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 particular address will work only with the flash layout above.

All settings:

Also, locate your BOOT1 pin in the datasheet and init it as input. In fact, it’s not required - it’s just for one condition that allows to understand if we boot into the bootloader or main firmware. You can configure any other way in the main() function to have such a condition, but I will use BOOT1 simply 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 (of course, you need only the main() function from here, otherwise you may have problems with clock config that might be different from mine):

/* 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 of the articles define here start and the end address of processor flash as a separate variable. In fact, there is 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 processor.

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, put the set BOOT1 to 0 (if you’re using it at all) and continue to the target firmware modification

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, by these two steps you make sure that you run your firmware from the address that bootloader expects, and you relocate the vector table. After this modification you must be able to debug your firmware in the same way as before. Just set the BOOT1 to 1 and flash the newly built firmware with you favorite GDB server and IDE. If you’ve done everything correctly, you must be able to debug it. Otherwise don’t even try to load it using the DFU utility - it won’t work.

Fun fact: if you’re doing this DFU think for the first time, I strongly recommend having a very simple blink project for your device to experiment with. Why: because, for example, my complex project didn’t worked well after this modification. Hard faults, strange errors etc. But that’s not the problem with the bootloader itself, 100% that’s something wrong with your firmware even though it worked before. If it just works, you’re lucky one. If it doesn’t - try starting with a simple project that just works. Check that your bootloader can succesfully flash it using the DFU utility and actually boot it after that. Next, start with a blank project and incrementally add your project parts to understand, what’s wrong. In my case it helped a lot - yep, I’ve needed to recreate the whole project structure from scratch in order to have a working project. But now it works and I think in this case it’s the fastest way to solve the problem instead of digging into the problem.

If you think that it’s FreeRTOS causing such problems in your code, or C++ code instead of pure C and you need some additional relocation of the memory region - NO. For this particular bootloader you don’t need to change anything else, dot. Check everything again.

DFuSe Utility and dfu-util

Various guides recommend DFuSe Utility from STM32 on Windows. In short, if you want to work with an obsolete driver for Windows and always convert your .hex files to .dfu to be able to check if you bootloader is working, and stick to 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 have installed STM32 driver that works with DFuSe Utility, it won’t work with dfu-util. What you need to do is:

  • Delete the device and it’s 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