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>© 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>© 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. Она просто работает.