DFU-загрузчик STM32

Может показаться, что создать DFU-загрузчик просто — статей об этом предостаточно. Проблема в том, что многие из них не работают. Поэтому я решил разобрать здесь критичные моменты.

Проект загрузчика

Скорее всего, вам не хочется добавлять загрузчик прямо в свою прошивку. Так что давайте создадим отдельный проект загрузчика в CubeMX. Разумеется, можно использовать STM32CubeIDE, и процесс будет примерно таким же, но пока будем считать, что вы работаете в STM32CubeMX.

Общая настройка

Создайте в CubeMX пустой проект для своего микроконтроллера. Я использую STM32F103RCT6; вы можете взять любой другой процессор. Задайте RCC | High Speed Clock (HSE) = Crystal/Ceramic Resonator:

Задайте SYS | Debug = Serial Wire (если отлаживаете через SWD):

Настройка, специфичная для загрузчика

Включите Connectivity | USB | Device (FS). Оставьте свойства по умолчанию:

В Middleware | USB_DEVICE:

  • Задайте Class for FS IP = Download Firmware Update Class (DFU)
  • Задайте Parameter Settings | USBD_DFU_XFER_SIZE = 32768. Это максимальный размер передачи, и он работает. Если у вас не заработает, поставьте 1024. Эти настройки должны работать без проблем.
  • Задайте Parameter Settings | USBD_DFU_MEDIA Interface = @Internal Flash /0x08000000/02*016Ka,02*016Kg,01*064Kg,03*128Kg. Тщательно проверьте этот параметр — он задаёт разметку флеш-памяти. По умолчанию STM32Cube генерирует разметку, которая требует 0x0800C000 для USBD_DFU_APP_DEFAULT_ADD (стартовый адрес вашей прошивки). Она работает, но ни к чему отдавать простому загрузчику столько места, поэтому мы используем альтернативную разметку, которая работает только при USBD_DFU_APP_DEFAULT_ADD, равном 0x08008000.
  • Задайте Parameter Settings | USBD_DFU_APP_DEFAULT_ADD = 0x08008000. Учтите, что этот адрес будет работать только с разметкой флеш-памяти выше.

Все настройки:

Также найдите в даташите свой вывод BOOT1 и настройте его как вход. Это не строго обязательно — просто один из способов решить, загружать загрузчик или основную прошивку. Вы можете реализовать любое другое условие в main(), но я использую BOOT1, потому что на моей плате есть перемычка.

Отлично! Теперь можно сгенерировать проект и открыть его в какой-нибудь среде разработки.

Код загрузчика

main.c (отсюда нужна только функция main(); в противном случае можете столкнуться с различиями в конфигурации тактирования):

/* 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****/

Примечание: в некоторых статьях стартовый и конечный адреса флеш-памяти процессора выносят в отдельные переменные. На самом деле незачем это делать. Стартовый адрес всегда USBD_DFU_APP_DEFAULT_ADD. Конечный адрес всегда FLASH_BANK1_END или FLASH_BANK2_END в зависимости от вашего микроконтроллера.

Следующий файл, который нужно изменить, — usbd_dfu_if.c (можно просто скопировать; он на 100% корректный):

/* 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;

    /* Unlock flash */
    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;

    /* Lock flash */
    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****/

Вот и всё, что нужно для загрузчика. Прошейте его, установите BOOT1 в 0 (если вы вообще его используете) и переходите к изменениям целевой прошивки.

Целевая прошивка

Здесь нужно изменить следующее:

  • Найдите свой файл компоновщика; в моём случае это STM32F103RCTX_FLASH.ld. Замените FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K на FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 224K (вычтите из общего размера флеш-памяти зарезервированные под приложение 32К, здесь это 0x08008000 - 0x08000000).
  • Направьте таблицу векторов на ту же базу (в проектах STM32F1 HAL это обычно USER_VECT_TAB_ADDRESS / VECT_TAB_OFFSET в system_stm32f1xx.c; в других семействах имена символов другие, но идея та же).

Коротко говоря, этими двумя шагами вы добиваетесь, чтобы прошивка была скомпонована там, где её ждёт загрузчик, и чтобы прерывания использовали перемещённую таблицу векторов. После этого изменения вы сможете отлаживать прошивку как раньше. Установите BOOT1 в 1 и прошейте только что собранную прошивку вашим любимым GDB-сервером и средой разработки. Если вы всё сделали правильно, отладка должна работать. В противном случае не пытайтесь залить её через утилиту DFU — ничего не выйдет.

Совет: если вы делаете это с DFU впервые, настоятельно рекомендую начать экспериментировать с очень простым проектом с миганием светодиодом. Почему? Потому что мой сложный проект после этого изменения работал плохо — аппаратные исключения (hard fault), странные ошибки и так далее. Это не проблема загрузчика; это что-то в вашей прошивке, даже если раньше она работала. Если всё просто заработало, вам повезло. Если нет — начните с простого проекта, который работает. Убедитесь, что загрузчик успешно прошивает его через утилиту DFU и запускает. Затем возьмите пустой проект и постепенно добавляйте свой код, чтобы найти, в чём дело. В моём случае пришлось заново воссоздать всю структуру проекта с нуля. Это оказалось самым быстрым способом решить проблему, а не бесконечно в ней копаться.

Если вы думаете, что дело в FreeRTOS или в C++ вместо C, и нужно что-то ещё с перемещением памяти — нет. Для этого загрузчика больше ничего менять не нужно. Проверьте всё ещё раз.

Утилита DFuSe и dfu-util

Разные руководства советуют утилиту DFuSe от ST под Windows. Коротко: если вы хотите работать с устаревшим виндовым драйвером, постоянно конвертировать свои .hex-файлы в .dfu, чтобы проверять, работает ли загрузчик, и сидеть в среде только под Windows — можете её использовать.

Но я не рекомендую, потому что dfu-util намного проще.

Windows

Если у вас установлен драйвер STM32, работающий с DFuSe, он не заработает с dfu-util. Вместо этого сделайте так:

  • Удалите устройство и его драйвер через диспетчер устройств Windows
  • Установите драйвер на основе libusb с помощью Zadig

После этого dfu-util должен заработать.

macOS

У меня dfu-util не работал на macOS, пока не был применён этот патч. У вас он, возможно, заработает без проблем.

Как залить прошивку

Выполните dfu-util -l. Найдите устройство DFU. Вывод должен быть примерно таким:

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"

Если у вас что-то вроде этого:

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

нужно перезагрузить устройство DFU (отключить и снова подключить).

Как только устройство DFU определяется корректно, выполните следующую строку, чтобы залить прошивку:

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

Здесь -a должно равняться параметру alt из вывода dfu-util -l, -s равно значению USBD_DFU_APP_DEFAULT_ADD. При желании добавьте -v -v, чтобы получить подробный вывод на случай проблем. Эта команда должна успешно залить прошивку. Если вы на macOS и применили этот патч, добавьте -T 10 или задайте тайм-аут побольше.

Сброс USB на macOS

Если вам надоело отключать устройство вручную, используйте эту утилиту или ту же утилиту в готовом виде в приложении USB Prober. Она просто работает.

Бонус: видеоурок от STM32, который действительно работает