stm32cubeMX學習、USB DFU(Download Firmware Update)固件更新
本程序編寫基于秉火霸道STM32F103ZET6運行環(huán)境。
最近疫情期間,特地將自己大部分硬件資源全部用熱膠搶焊到了一起,以便以后自己復習和學習,當然還有很多,弄不上來了,只能等以后有機會再重新搞一塊!我還是非常舍得花錢買設備的!哈哈!這是一個STM32+Linux+51的大雜燴開發(fā)平臺!
1、產生問題
公司的產品,每次生產燒寫程序都得把機器拆開,然后插上串行線或者ST-Link進行燒寫,產品量產的情況下數(shù)量很多,所以生產每次都需要花費很長去時間去給機器燒程序(這里我們用野火的開發(fā)板來模擬)。
2、現(xiàn)有的硬件接口
現(xiàn)在的產品(野火的STM32F103ZET6開發(fā)板)有一個USB接口,硬件連接圖如下:
如上圖所示,當PD3為低電平的時候,USB接口供電,即可用,這一點在上一篇文章已經講解了,我們在STM32CubeMX把這個管腳默認拉低即可。
3、分析問題
STM32CubeMX支持了與USB相關的諸多配置功能,請看如下:
于我們需要使用USB接口來更新程序,所以我們需要在配置USB設備模式的時候給它選擇Download Firmware Update Class(DFU)。
1、USB燒寫原理及流程分析
1.1 燒寫原理
這點與IAP升級是大同小異的,只不過這里我們使用了USB來燒寫,之前寫過類似的一篇文章:帶串口屏顯示的BootLoader程序開發(fā) 在這篇文章里面也介紹了相應的原理,這里就不再重復描述,我們負責把這篇文章里提到的幾點實現(xiàn)就可以了。
1.2 程序存儲分區(qū)
STM32F103ZET6的FLASH容量一共有512KB。所以,我給BootLoader的大小是64K,也就是0x10000,具體是怎么算的呢?
0x10000轉十進制為65536,65536/1024 = 64K
把剩下的空間全部分配給APP,也就是0x70000,具體是怎么算的呢?
0x70000轉十進制為458752,458752/1024 = 448K
4、解決問題
4.1 配置編寫B(tài)ootLoader程序的CubeMX工程
4.1.1 配置RCC時鐘
4.1.2 配置串行調試接口
4.1.3 配置按鍵、調試燈、調試串口、USB使能管腳
調試燈選擇的是PB1,低電平點亮,具體可以看原理圖:
USB使能管腳默認為低電平。
選用USART2作為調試打印輸出。
4.1.4 配置USB相關的選項
配置的基本參數(shù)默認即可,不需要改變。
在中斷設置這里,將USB優(yōu)先級調低,可以避免一些默認其妙不穩(wěn)定的現(xiàn)象。接下來配置USB設備相關的選項。
類參數(shù)有一個字段比較重要:
@Internal Flash /0x08000000/03*016Ka,01*016Kg,01*064Kg,07*128Kg,04*016Kg,01*064Kg,07*128Kg
這個參數(shù)的具體含義描述如下:
-
@:檢測到這是一個特殊的映射描述符(避免解碼標準描述符)
-
/:用于區(qū)域之間的分隔符
-
每個地址以“ 0x”開頭的最大8位數(shù)字
-
/:用于區(qū)域之間的分隔符
-
扇區(qū)數(shù)的最大2位數(shù)字
-
*:用于扇區(qū)數(shù)和扇區(qū)大小之間的分隔符
-
扇區(qū)大小在0到999之間的最大3位
-
扇區(qū)大小乘數(shù)的1位數(shù)字。有效條目為:B(字節(jié)),K(千),M(兆)
-
扇區(qū)類型的1位數(shù)字,如下所示:
– a(0x41):可讀
– b(0x42):可擦除
– c(0x43):可讀和可擦除
(0x44):可寫
– e(0x45):可讀寫
–f(0x46):可擦除和可寫
–g(0x47):可讀寫,可寫
4.1.5 生成工程
這里默認不讓它自動生成main函數(shù),main函數(shù)我們自己寫。在配置USB設備參數(shù)里,USBD_DFU_XFER_SIZE參數(shù):USB數(shù)據pack大小,越大配置速度越快。默認配置1024Bytes. 1024Bytes使用的是堆空間,故堆空間要大于1024Bytes. 原因:代碼如下。
#define USBD_malloc malloc
/* Allocate Audio structure */
pdev->pClassData = USBD_malloc(sizeof (USBD_DFU_HandleTypeDef));
所以這里的堆我把它配置成0x1000。(個人習慣)
4.2 編寫B(tài)ootLoader程序
4.2.1 實現(xiàn)usbd_dfu_if.c中相關的接口
宏定義一些參數(shù)
//FLASH的擦寫實現(xiàn)
#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
實現(xiàn)如下接口:
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 獲取閃存狀態(tài),返回寫入或擦除操作所需的時間。
閃存初始化,解鎖內部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存放代碼的最后一個區(qū)域的地址為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 */
}
獲取閃存狀態(tài),返回寫入或擦除操作所需的時間。
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 實現(xiàn)main.c
定義調試打印接口,這里我用的是USART2
int fputc(int ch, FILE* FILE)
{
HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
跳轉到APP的代碼實現(xiàn):
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區(qū)域存放有數(shù)據,我們不希望去啟動USB,在剛開始的時候我們可以把USB的功能給失能掉,如果檢測到APP區(qū)域沒有數(shù)據,則再初始化USB功能,所以在這里編寫一個USB的失能函數(shù)。
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函數(shù)實現(xiàn)
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
USB_GPIO_DeInit();
MX_USART2_UART_Init();
/*如果沒有按下按鍵,則自動跳轉到APP區(qū),如果跳轉不過去,則代表區(qū)域無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);
}
}
實現(xiàn)完畢,接下來可以編譯程序,下載到開發(fā)板,由于沒有APP,所以開發(fā)板上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到開發(fā)板的設備端口到PC
看到沒有識別DFU
我們需要手動給它更新下驅動程序,直接就是剛剛下載的DfuSe安裝的目錄下找對應系統(tǒng)版本的驅動就好了。
最后可以看到該模式被識別了:
接下來打開DfuSeDemo這個軟件,可以看到開發(fā)板現(xiàn)在已經被識別了。
接下來將剛剛生成的APP_TEST1.dfu加載進來。
點擊Upgrade進行升級。
升級成功!接下來點擊Leave DFU mode,程序則會自動開始執(zhí)行。
這時候APP已經跑起來了,燈在以500ms的頻率不斷閃爍。
Bootloader代碼以及APP代碼在這里下載:
鏈接:https://pan.baidu.com/s/1zRv7j4E8SXgCV5F6RbSo1Q
提取碼:5539
如果有興趣的話,還可以把我之前寫的串口屏BootLoader那個程序繼續(xù)升級一下!
帶串口屏顯示的Bootloader
往期精彩
帶串口屏顯示的Bootloader
為Linux應用構造有限狀態(tài)機
編程修養(yǎng)(精品文,建議認真品讀并實踐)
嵌入式C語言代碼優(yōu)化方案(深度好文,建議花時間研讀并收藏)
若覺得本次分享的文章對您有幫助,隨手點[在看]
并轉發(fā)分享,也是對我的支持。
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!