本程序编写基于秉火霸道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那个程序继续升级一下!