从用户代码调用系统存储器内Bootloader的方法探讨
前言
我们知道,任何STM32芯片内都包含有一块系统存储器(SystemMemory),里边存储着内部的启动代码Bootloader。不同的STM32型号所支持的用于升级代码的通讯口不尽相同,需要参考应用笔记AN2606。但是,有一个问题避免不了,那就是如何进入System Memory去执行Bootloader?通常的办法都是将BOOT1和BOOT0进行配置:BOOT0拉高,BOOT1拉低(注意:有些型号的BOOT1由选项字节nBOOT1控制)。可是在一些产品中,由于外观的要求,往往不方便在外边开口去放置按键或跳线来改变BOOT脚的电平。而且,用户并不想自己写IAP代码,觉得麻烦。特别是一些产品,需要使用USBDFU来进行代码升级的,而在产品功能中USB又没用到,用户就会觉得自己为了一个通过USB进行代码升级的功能,去写IAP的话,需要去熟悉USB的代码,觉得麻烦,而且这些USB的代码还占用了用户的程序空间。对于这些用户来讲,他们很希望能在不管BOOT脚的情况下能够去调用STM32中System Memory的Bootloader,完成代码升级功能。
问题
某客户在其产品的设计中,使用了STM32F411。由于产品外观的要求,无法在外部对BOOT脚进行控制,而且外观上只有USB接口是留在外边的,需要使用USB DFU进行升级。而且USB接口只用于代码升级,没有其他功能,所以客户不想去碰USB代码,希望能够直接使用System Memory中的Bootloader进行代码升级。
调研
1.判断其可行性
首先,打开应用笔记AN2606《STM32 microcontroller system memory bootmode》,翻到3.1 Bootloader activation一节的最后,可以看到如下信息:
这里的意思就是说,用户可以通过从用户代码跳转到系统存储器去执行Bootloader。但是,在跳转到Bootloader之前,有几个事情必须要做好:
1) 关闭所有外设的时钟
2) 关闭使用的PLL
3) 禁用所有中断
4) 清除所有挂起的中断标志位
最后,去掉Bootloader的激活条件并产生一个硬件复位去执行用户代码,也可以直接使用Go命令去执行用户代码。
那么,如何从用户代码跳转到System Memory中去呢?这个其实并不难,如果写过IAP,或者看过关于IAP的应用笔记中的参考代码的话,比如应用笔记AN3965“STM32F40x/STM32F41x in-application programming using theUSART”及其参考代码STSW-STM32067,都应该知道,IAP的启动代码通过重新设置栈指针并跳转到用户代码来执行用户代码的。同样的道理,只要知道SystemMemory的地址,一样可以从用户代码通过重新设置主堆栈指针并跳转到System Memory来执行Bootloader。而System Memory地址可以从参考手册来获得。比如,查看STM32F411的参考手册RM0383,可以找到如下的表格:
可以知道STM32F411的System memory地址从0x1FFF0000开始。
那很多人又会问了,我的代码很复杂,用了很多外设,开了很多中断,可是要跳转到System Memory中的Bootloader,需要关所有外设的时钟,需要关PLL,需要关闭所有中断,需要禁用所有的中断,清除所有挂起的中断。这可是一项非常庞大的的任务啊!所以,在这里,我们需要一个更简单的事情来完成这项庞大的任务。其实真的就有这么简单的一个方法——复位!通过软件复位来实现这一目的。但是,复位后,又怎么知道还记得我们要去做代码升级呢?这又要用到STM32另一个特性了,那就是后备数据寄存器Backup Data Registers在软件复位后会保留其值,这样给了我们在复位前后做一个标志的机会。
这样,考证下来,客户的需求是具备可行性的。接下来需要做的是理清思路。
2.软件流程
这里使用32F411EDISCOVERY板来设计一个参考例程:设计一个用户程序,让LED3进行闪烁;当用户按键被按下,产生EXTI中断,在中断中选择后备数据寄存器,写入值0x32F2,然后产生软件复位;软件复位后,在运行代码的最前面对进行判断,如果其值不是0x32F2则直接去运行用户代码,如果其值为0x32F2则是需要跳转到Bootloader去进行代码升级,并在跳转前将RTC_BKP0R清零。这样,在进入Bootloader后,客户进行USB DFU升级后,将来不会因为不需要升级代码的复位而误入Bootloader。
来看软件流程图,先来看主程序的流程图:
再来看EXTI中断的流程图:
3.主要代码
使用STM32F4Cube库来开发这个例程。先来看位于main.c中的main函数:
int main(void)
{
/* STM32F4xx HALlibrary initialization:
- Configure theFlash prefetch, Flash preread and Buffer caches
- Systick timer isconfigured by default as source of time base, but user can eventually implement his proper time base source (a general purpose timer for example or other time source), keeping in mind that Time base durationshould be kept 1ms since PPP_TIMEOUT_VALUEs are defined and handled inmilliseconds basis.
- Low LevelInitialization */
HAL_Init();
/* Configure the Systemclock to have a frequency of 100 MHz */
SystemClock_Config();
/* Configure LED3,LED4, LED5 and LED6 */
BSP_LED_Init(LED3);
BSP_LED_Init(LED4);
BSP_LED_Init(LED5);
BSP_LED_Init(LED6);
/* Configure EXTI Line0(connected to PA0 pin) in interrupt mode */
EXTILine0_Config();
/* Infinite loop */
while (1)
{
}
}
Main函数很简单,配置系统时钟,对使用的LED进行初始化,然后配置了用户按键的EXTI中断,然后就进入主循环了。前面说到,要实现用户的功能程序为LED3闪烁,在主循环我们没看到,是因为在Cube库中,会使用SysTick,所以把LED3的闪烁放到SysTick的中断代码中了,查看stm32f4xx_it.c,如下:
void SysTick_Handler(void)
{
HAL_IncTick();
// LED3 Toggle
led_toggle_counter++;
if (led_toggle_counter>= 500)
{
led_toggle_counter =0;
BSP_LED_Toggle(LED3);
}
}
从main函数最开始的那段注释中知道,跳入main函数前,在startup_stm32f411xe.s中早已经先调用执行了位于system_stm32f4xx.c中的SystemInit函数。SystemInit函数主要执行初始化FPU、复位RCC时钟寄存器、配置向量表等功能。由于我们希望在最原始的状态下进入SystemMemory,所以我们将跳转到System Memory放在这个函数的最前头,如下:
void SystemInit(void)
{
/* Check if need to gointo bootloader before configure clock*/
RtcHandle.Instance =RTC;
if(HAL_RTCEx_BKUPRead(&RtcHandle, ) ==0x32F2)
{
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_HSE_DIV2);
__HAL_RCC_RTC_ENABLE();
HAL_RTCEx_BKUPWrite(&RtcHandle, ,0x0);
__HAL_RCC_RTC_DISABLE();
HAL_PWR_DisableBkUpAccess();
__HAL_RCC_PWR_CLK_DISABLE();
__set_MSP(*(__IO uint32_t*) 0x1FFF0000);
SysMemBootJump = (void (*)(void)) (*((uint32_t *) 0x1FFF0004));
SysMemBootJump();
while (1);
}
/* FPU settings------------------------------------------------------------*/
#if (__FPU_PRESENT ==1) && (__FPU_USED == 1)
SCB->CPACR |=((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */
#endif
/* Reset the RCC clockconfiguration to the default reset state ------------*/
/* Set HSION bit */
RCC->CR |=(uint32_t)0x00000001;
/* Reset CFGR register*/
RCC->CFGR =0x00000000;
/* Reset HSEON, CSSONand PLLON bits */
RCC->CR &=(uint32_t)0xFEF6FFFF;
/* Reset PLLCFGRregister */
RCC->PLLCFGR =0x24003010;
/* Reset HSEBYP bit */
RCC->CR &=(uint32_t)0xFFFBFFFF;
/* Disable allinterrupts */
RCC->CIR =0x00000000;
/* Configure the VectorTable location add offset address ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR =SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
SCB->VTOR =FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif
}
可以看到,在函数的最前面对RTC_BKP_DR0进行了判断,如果其值为0x32F2的话,则,如RM0383中5.1.2 Battery backup domain所描述的:
主堆栈指针MSP的初始值位于向量表偏移量为0x00的位置,复位Reset的值则位于向量表偏移量为0x04的位置。对于STM32F411来说,当执行System Memeory中的Bootloader时,MSP的初始值位于0x1FFF0000,而Reset向量则位于0x1FFF0004。所以在程序中,使用__set_MSP(*(__IO uint32_t*) 0x1FFF0000);来重新设置主堆栈指针,而后再跳转到0x1FFF0004所指向的程序地址去执行Bootloader。
再来看位于stm32f4xx_it.c中的EXTI中断程序:
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY_BUTTON_PIN);
}
及其位于main.c中的Callback函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin ==KEY_BUTTON_PIN)
{
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_HSE_DIV2);
__HAL_RCC_RTC_ENABLE();
RtcHandle.Instance =RTC;
HAL_RTCEx_BKUPWrite(&RtcHandle, ,0x32F2);
if(HAL_RTCEx_BKUPRead(&RtcHandle, RTC_BKP_DR0) != 0x32F2)
{
// Write backupdata memory failed
BSP_LED_On(LED5);
while (1) ; // Error
}
else
{
// Write backupdata memory succeeded.
BSP_LED_On(LED6);
(); // Software reset for going into bootloader
while (1) ;
}
}
}
当判断到用户按键按下,需要进行用户代码升级时,先启动备份域的访问时序,将RTC_BKP_DR0的值写为0x32F2。再读回来判断是否写入成功,以方便调试。如果写入成功后,则就调用HAL_NVIC_SystemReset()进行软件复位。重新复位后,就可以进入System Memory了。
4.实验
使用32F411EDISCOVERY来做实验。
1) 先将程序编译,下载到32F411EDISCOVERY板,可以看到LED3在进行闪烁。
2) 按下User按键,LED3熄灭,已经进入System Memory中的Bootloader。
3) 打开DfuSeDemo软件,此时AvailableDFU Devices中没有任何显示。
4) 将一根USB Micro连接线插入32F411EDISCOVERY板的CN5,可见LED7亮起,USB已连接。
5) 驱动完成后,可以再查看一下DfuSeDemo,AvailableDFU Devices已经显示
为“STM Device in DFU Mode”,代表已经成功驱动并正常工作了。
6) 之后就是正常的升级代码的流程了,点“Choose”按钮选择要更新的代码,这里准备好了一个32F411EDISCOVERY板的Demo程序经过Dfu file manager软件生成的32f411ediscovery.dfu的文件,导入!
7) 点Upgrade”按钮进行升级,弹出的对话框选Yes就可以了,之后就升级成功了。
8) 再点一下“Leave DFU mode”,进度条显示“Successfullyleft DFU mode!”,就可以进入更新后的用户代码了,可以看到4个LED灯正常欢快的滚动和闪烁着……
注意
此例程仅为验证其可行性,在实际应用中,有不尽完善的地方请用户自行完善。另外,有几个需要注意的地方:
1) 此Demo代码基于STM32Cube_FW_F4_V1.11.0撰写,解压缩后,可将其放入\STM32Cube_FW_F4_V1.11.0\Projects\STM32F411E-Discovery\Templates替换掉原来的源代码文件,即可编译运行。
2) 此程序使用按键按下作为条件来触发软件代码升级,用户可以根据自己的情况修改触发条件,如多个按键同时按下,等等。
3) 当用户应用中使用了RTC的话,RTC时钟源一旦被选择后是无法修改的,除非备份域被复位。在RM0383关于RCC_BDCR的描述中有提及:
4) 关于如何使用Dfu file manager生成.dfu文件,请参考UM0412“Getting started withDfuSe USB device firmware upgrade”或者实战经验“利用USB DFU实现IAP功能”。
5) 关于Bootloader中所使用的USB DFU协议,请参考AN3156“USB DFU protocol used inthe STM32 bootloader”。
==================================
扫描或长按二维码可关注公众号