查看原文
其他

stm32cubeMX学习、USB DFU(Download Firmware Update)固件更新

杨源鑫 嵌入式云IOT技术圈 2021-01-31


本程序编写基于秉火霸道STM32F103ZET6运行环境。

最近疫情期间,特地将自己大部分硬件资源全部用热胶抢焊到了一起,以便以后自己复习和学习,当然还有很多,弄不上来了,只能等以后有机会再重新搞一块!我还是非常舍得花钱买设备的!哈哈!这是一个STM32+Linux+51的大杂烩开发平台!

1、产生问题

公司的产品,每次生产烧写程序都得把机器拆开,然后插上串行线或者ST-Link进行烧写,产品量产的情况下数量很多,所以生产每次都需要花费很长去时间去给机器烧程序(这里我们用野火的开发板来模拟)。

2、现有的硬件接口

现在的产品(野火的STM32F103ZET6开发板)有一个USB接口,硬件连接图如下:

如上图所示,当PD3为低电平的时候,USB接口供电,即可用,这一点在上一篇文章已经讲解了,我们在STM32CubeMX把这个管脚默认拉低即可。

3、分析问题

STM32CubeMX支持了与USB相关的诸多配置功能,请看如下:

于我们需要使用USB接口来更新程序,所以我们需要在配置USB设备模式的时候给它选择Download Firmware Update Class(DFU)。

1、USB烧写原理及流程分析

1.1 烧写原理

这点与IAP升级是大同小异的,只不过这里我们使用了USB来烧写,之前写过类似的一篇文章:带串口屏显示的BootLoader程序开发 在这篇文章里面也介绍了相应的原理,这里就不再重复描述,我们负责把这篇文章里提到的几点实现就可以了。

1.2 程序存储分区

STM32F103ZET6的FLASH容量一共有512KB。所以,我给BootLoader的大小是64K,也就是0x10000,具体是怎么算的呢?

0x10000转十进制为65536,65536/1024 = 64K

把剩下的空间全部分配给APP,也就是0x70000,具体是怎么算的呢?

0x70000转十进制为458752,458752/1024 = 448K

4、解决问题

4.1 配置编写BootLoader程序的CubeMX工程

4.1.1 配置RCC时钟

4.1.2 配置串行调试接口

4.1.3 配置按键、调试灯、调试串口、USB使能管脚

调试灯选择的是PB1,低电平点亮,具体可以看原理图:

USB使能管脚默认为低电平。

选用USART2作为调试打印输出。

4.1.4 配置USB相关的选项

配置的基本参数默认即可,不需要改变。

在中断设置这里,将USB优先级调低,可以避免一些默认其妙不稳定的现象。接下来配置USB设备相关的选项。

类参数有一个字段比较重要:

@Internal Flash   /0x08000000/03*016Ka,01*016Kg,01*064Kg,07*128Kg,04*016Kg,01*064Kg,07*128Kg

这个参数的具体含义描述如下:

  • @:检测到这是一个特殊的映射描述符(避免解码标准描述符)

  • /:用于区域之间的分隔符

  • 每个地址以“ 0x”开头的最大8位数字

  • /:用于区域之间的分隔符

  • 扇区数的最大2位数字

  • *:用于扇区数和扇区大小之间的分隔符

  • 扇区大小在0到999之间的最大3位

  • 扇区大小乘数的1位数字。有效条目为:B(字节),K(千),M(兆)

  • 扇区类型的1位数字,如下所示:

– a(0x41):可读
– b(0x42):可擦除
– c(0x43):可读和可擦除
(0x44):可写
– e(0x45):可读写
–f(0x46):可擦除和可写
–g(0x47):可读写,可写

4.1.5 生成工程

这里默认不让它自动生成main函数,main函数我们自己写。在配置USB设备参数里,USBD_DFU_XFER_SIZE参数:USB数据pack大小,越大配置速度越快。默认配置1024Bytes. 1024Bytes使用的是堆空间,故堆空间要大于1024Bytes. 原因:代码如下。

#define USBD_malloc malloc
/* Allocate Audio structure */
pdev->pClassData = USBD_malloc(sizeof (USBD_DFU_HandleTypeDef));

所以这里的堆我把它配置成0x1000。(个人习惯)

4.2 编写BootLoader程序

4.2.1 实现usbd_dfu_if.c中相关的接口

宏定义一些参数

//FLASH的擦写实现
#define FLASH_ERASE_TIME (uint16_t)50
#define FLASH_PROGRAM_TIME (uint16_t)50
//APP存放的结束地址
#define USBD_DFU_APP_END_ADD 0x08080000
//FLASH页大小
#define FLASH_PAGE_SIZE 0x800U //2K

实现如下接口:

MEM_If_Init_FS, 闪存初始化,解锁内部flash。
MEM_If_DeInit_FS, 闪存反(取消)初始化,上锁内部flash。
MEM_If_Erase_FS, 闪存擦除。
MEM_If_Write_FS, 闪存写入。
MEM_If_Read_FS, 闪存读取。
MEM_If_GetStatus_FS 获取闪存状态,返回写入或擦除操作所需的时间。

闪存初始化,解锁内部flash。

uint16_t MEM_If_Init_FS(void)
{
/* USER CODE BEGIN 0 */
//解锁内部FLASH
HAL_FLASH_Unlock();
//清除FLASH的一些标志,可以避免一些莫名其妙的问题
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGERR);
return (USBD_OK);
/* USER CODE END 0 */
}

闪存反(取消)初始化,上锁内部flash。

uint16_t MEM_If_DeInit_FS(void)
{
/* USER CODE BEGIN 1 */
//给FLASH上锁
HAL_FLASH_Lock();
return (USBD_OK);
/* USER CODE END 1 */
}

闪存擦除。

uint16_t MEM_If_Erase_FS(uint32_t Add)
{
/* USER CODE BEGIN 2 */
/*擦除整个APP程序存放的空间,即是0x08080000-0x08010000*/
/*
因为起始地址是0x8000000,而Size是0x80000,所以MCU存放代码的最后一个区域的地址为0x8080000。
而DFU占了其中的0x10000的空间。
*/
uint32_t NbOfPages = 0 ;
uint32_t PageError = 0 ;
FLASH_EraseInitTypeDef pEraseInit ;
NbOfPages = (USBD_DFU_APP_END_ADD - USBD_DFU_APP_DEFAULT_ADD)/FLASH_PAGE_SIZE ;
pEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
pEraseInit.PageAddress = USBD_DFU_APP_DEFAULT_ADD;
pEraseInit.NbPages = NbOfPages; //erase all pages of APP
if(HAL_FLASHEx_Erase(&pEraseInit,&PageError)!= HAL_OK)
return USBD_FAIL ;
return (USBD_OK);
/* USER CODE END 2 */
}

闪存写入。

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)
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,(uint32_t)(dest+i),*(uint32_t*)(src+i))== HAL_OK)
{
if(*(uint32_t*)(src+i) != *(uint32_t*)(dest+i))
return USBD_FAIL;
}
else
{
return USBD_FAIL;
}
}
return (USBD_OK);
/* USER CODE END 3 */
}

闪存读取

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

获取闪存状态,返回写入或擦除操作所需的时间。

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:
buffer[1] = (uint8_t)FLASH_ERASE_TIME;
buffer[2] = (uint8_t)(FLASH_ERASE_TIME << 8);
buffer[3] = 0;
break ;
default:

break;
}

return (USBD_OK);
/* USER CODE END 5 */
}

4.2.1 实现main.c

定义调试打印接口,这里我用的是USART2

int fputc(int ch, FILE* FILE)
{
HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}

跳转到APP的代码实现:

static void JumpToApp(void)
{
typedef void (*pFunction)(void);
static pFunction JumpToApplication;
static uint32_t JumpAddress;

/* Test if user code is programmed starting from USBD_DFU_APP_DEFAULT_ADD * address */
if (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0x2FFE0000) == 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();
}
}

在正常启动过程中,如果APP区域存放有数据,我们不希望去启动USB,在刚开始的时候我们可以把USB的功能给失能掉,如果检测到APP区域没有数据,则再初始化USB功能,所以在这里编写一个USB的失能函数。

static void USB_GPIO_DeInit(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};

/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();

/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11 | GPIO_PIN_12, GPIO_PIN_RESET);

/*Configure GPIO pin*/
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_Delay(500);
}

main函数实现

int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
USB_GPIO_DeInit();
MX_USART2_UART_Init();
/*如果没有按下按键,则自动跳转到APP区,如果跳转不过去,则代表区域无APP*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) != GPIO_PIN_SET)
{
JumpToApp();
printf("跳转失败,开始进入DFU模式\r\n");
}
//进入DFU模式
MX_USB_DEVICE_Init();
printf("Bruce.Yang DFU\n");
//调试灯常亮,代表此时在DFU模式
HAL_GPIO_WritePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin,GPIO_PIN_RESET);
while(1)
{
HAL_Delay(1000);
}
}

实现完毕,接下来可以编译程序,下载到开发板,由于没有APP,所以开发板上PB1的灯常亮。

4.2.2 编写APP程序

APP程序很简单,就让PB1灯以500ms的频率进行翻转吧。

配置过程(略)太简单了,应用APP的核心代码如下:

while (1)
{
/* USER CODE END WHILE */
HAL_GPIO_TogglePin(BLUE_LED_GPIO_Port,BLUE_LED_Pin);
HAL_Delay(500);
/* USER CODE BEGIN 3 */
}

接下来主要是在工程里做一些设置。

1、点击魔术棒设置APP启动的地址

2、更改中断向量表偏移

接下来编译生成APP_TEST.hex文件,我们用一个工具来将它烧写到板子上。

安装DFU烧录软件:DfuSe_Demo

官网下载链接:

https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-programmers/stsw-stm32080.html#resource

默认安装即可。

安装成功后得到两个软件


Dfu file manager是把bin文件或者hex文件生成 .dfu后缀的文件, .dfu后缀的文件就是我们的固件。DfuSe_Demo是烧录 文件后缀 .dfu 软件。

烧录步骤:

1、将.hex文件转化成.dfu后缀的文件

生成后可以看到效果:

2、连接USB到开发板的设备端口到PC

看到没有识别DFU

我们需要手动给它更新下驱动程序,直接就是刚刚下载的DfuSe安装的目录下找对应系统版本的驱动就好了。

最后可以看到该模式被识别了:

接下来打开DfuSeDemo这个软件,可以看到开发板现在已经被识别了。

接下来将刚刚生成的APP_TEST1.dfu加载进来。

点击Upgrade进行升级。

升级成功!接下来点击Leave DFU mode,程序则会自动开始执行。


这时候APP已经跑起来了,灯在以500ms的频率不断闪烁。

至此USB DFU固件成功!

Bootloader代码以及APP代码在这里下载:

链接:https://pan.baidu.com/s/1zRv7j4E8SXgCV5F6RbSo1Q
提取码:5539

如果有兴趣的话,还可以把我之前写的串口屏BootLoader那个程序继续升级一下!

带串口屏显示的Bootloader

往期精彩


带串口屏显示的Bootloader

为Linux应用构造有限状态机

编程修养(精品文,建议认真品读并实践)

嵌入式C语言代码优化方案(深度好文,建议花时间研读并收藏)

若觉得本次分享的文章对您有帮助,随手点[在看]并转发分享,也是对我的支持。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存