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 IPtoDownload Firmware Update Class (DFU) - Set
Parameter Settings | USBD_DFU_XFER_SIZEto 32768. That’s the maximum transfer size and it works. If it doesn’t in your case, set it to1024. Those settings should work without problems. - Set
Parameter Settings | USBD_DFU_MEDIA Interfaceto@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 requires0x0800C000forUSBD_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 withUSBD_DFU_APP_DEFAULT_ADDset to0x08008000. - Set
Parameter Settings | USBD_DFU_APP_DEFAULT_ADDto0x08008000. 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>© 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>© 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. ChangeFLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256KtoFLASH (rx) : ORIGIN = 0x08008000, LENGTH = 256K, e.g. set the start address of the flash. - Change
FLASH_BASEto0x08008000UL
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.
