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
toDownload 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 to1024
. 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 requires0x0800C000
value forUSBD_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 withUSBD_DFU_APP_DEFAULT_ADD
set to0x08008000
. - Set
Parameter Settings | USBD_DFU_APP_DEFAULT_ADD
to0x08008000
. 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>© 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>© 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
. ChangeFLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
toFLASH (rx) : ORIGIN = 0x08008000, LENGTH = 256K
, e.g. set the start address of the flash. - Change
FLASH_BASE
to0x08008000UL
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.