本文主要是分析一下CM3中程序跳转原理以及需要注意的坑,CM4中跳转原理也是一样的。
一 应用场景
有时我们希望CM3上电后先运行一段bootloader程序,用于做一些前序的处理和控制,然后再跳转到APP代码去运行。
例如产品交付客户后发现需要升级一些功能,这个时候又不能打开产品外壳去重新下载程序,那么就可以通过这段bootloader程序来进行更新,那么这个bootloader就需要具有和外部通信的功能(例如使用CAN或者串口),可以接收传入的代码,并可以读写flash。
前序操作结束后,就需要跳转到APP代码去。总共需要2个Keil工程,一个bootloader的,一个APP的。
二 跳转代码
本文使用STM32F103RCT6,Keil 5.25来做的测试,期望的Flash代码结构如下,
跳转代码网上很多,本文也是从别的博客上copy过来的,如下(经过验证是OK的),
#include <stdint.h>
const uint32_t APP_FLASH_ADDRESS = 0X08008000; // APP代码在Flash上的起始地址
typedef void (*pFunction)(void); // 定义一个函数指针类型pFunction
pFunction Jump_To_Application; // 定义一个函数指针变量
void JumpToApp(uint32_t target_address)
{
__disable_irq(); // 关闭总中断
// 代码的flash起始地址加4,就是Reset Handler的函数地址
uint32_t JumpAddress = *(__IO uint32_t*)(target_address+4);
Jump_To_Application = (pFunction)JumpAddress; // 把这个地址转为函数指针
__set_MSP(*(__IO uint32_t*)target_address); // 设置栈顶地址
Jump_To_Application(); // 开始运行APP的Reset Handler
}
int main(void)
{
JumpToApp(APP_FLASH_ADDRESS); // 执行跳转命令
return 0;
}
其对应的Keil配置如下,
编译好直接烧录就行了。
APP的Keil配置如下,
APP跳转回Bootloader,其操作和Bootloader跳转到APP是一样的,只要改变跳转的Flash地址就行了。
三 遇到的坑和解决办法
遇到最多的坑是跳转过去后不运行,而单独测试APP是没问题的(就是不要Bootloader,直接把APP烧到0x8000000地址上),这是为什么呢?90%的原因是中断的问题,这个问题又分为2种,
- 中断向量表的重定向
- 外设中断的关闭
下面来分析一下这2个问题
1. 中断向量表的重定向
由前面的描述知道,要实现跳转就需要2个Keil工程,而每个Keil工程都会有自己的中断向量表,所以运行指定的Keil工程代码就需要使用对应的中断向量表。
在STM32启动时会去执行SystemInit()函数,这个是在startup_stm32f10x_hd.s里定义的(不同的芯片或keil版本这个汇编文件的名字可能不一样,调用函数都是一样的),
可以看到这是属于Reset_Handler里的代码(上电后或按reset键都会执行这段代码),先执行SystemInit()后,再去执行main()函数。在SystemInit()里会对中断向量表进行定位,如下,
因为没有定义VECT_TAB_SRAM,所以会执行黄色标记部分,右边的2个宏定义如下,
可以看到SCB->VTOR会被设置为0x08000000,这样APP的中断向量表也是被设置成这个值,因为SystemInit()函数是Keil提供的,是固定的,这样就会导致跳转后运行失败。
解决办法:在APP的main()函数开始的第一行,把中断向量表重定向一下,即设置为APP代码的Flash起始地址,
SCB->VTOR = 0x08008000;
可能有的人没有去重定向中断向量表也没问题,那是因为APP代码比较简单,没有用到中断向量表,这样APP使用Bootloader的中断向量表就不会有问题。
2. 外设中断的关闭
这个要先说下原理,为什么跳转前要关闭已经开启的外设中断。如下图
现在芯片里有2套程序,大家都要使用外设中断寄存器来做中断相关的操作(配置,开启等),但是外设中断寄存器只有一套。
如果Bootloader配置了一个定时器中断寄存器并开启,也写好了中断处理函数,然后没有disable它就跳转到APP去,而APP代码里没有使用定时器中断,也没有写中断处理函数,那么就会导致崩溃。因为配置好的定时器中断,到了定时时间要去执行中断处理函数,而中断向量表已经重定向了,现在的中断向量表里没有对应的中断处理函数。
看过前面的跳转代码的人可能会问:不是已经使用了__disable_irq()了吗,为什么还不行?
因为这个语句是关的总中断,当跳到APP后需要重新开启总中断,那么到时候它下面的那些外设中断又重新开始运行了(外设中断寄存器的值没有被清除掉)
就跟家里的水阀和水龙头一个道理,水阀是总阀,水阀关了,即使水龙头开着也不会出水,但是如果水阀开着,那么水龙头出不出水就取决于水龙头的开关情况了。
所以在__disable_irq()语句之后,我们要使用NVIC_DisableIRQ()来关闭外设中断,参数就是某个指定的中断,如下,
如果有多个中断开着,就多次调用这个函数,如下,
NVIC_DisableIRQ(WWDG_IRQn);
NVIC_DisableIRQ(RTC_IRQn);
NVIC_DisableIRQ(DMA1_Channel1_IRQn);
四 总结
本文简单分析了程序跳转的原理以及相关问题的解决办法,只要知道了原理,就可以很容易解决问题。写这篇文章时也参阅了很多其它网友的博客。另外,本文使用的是STM32单片机,对于其它公司的单片机也解决思路也都是类似的。
如果有写的不对的地方,请留言指正,谢谢。