一、操作环境
IDE:keil 5.25版本(或者更高版本)
板子:STM32F767核心板(SDIO接口用于读写SD,片外32M的SDRAM,USB slave)
二、BootLoader的的实现和作用
在Linux操作系统中一般BootLoader用于引导内核启动,先多板上要用到的外设进行初始化,接着创建一个很小的文件系统,用于引导内核以及根文件系统。在STM32F4系列以及跟高的性能cpu中存在FMC接口,用于拓展片外的SDRAM或者Flash设备。要实现引导,首先需要一套文件系统,这里我们使用FatFs,用于将SD的APP的bin文件读取到SDRAM,并且为了以后更新APP的操作方便我们也要通过BootLoader来更新新的APP程序,于是BootLoader就暂时定义两项功能,一是将SD内的程序进行加载并且执行,二是将SD通过USB挂载到电脑上直接通过USB将SD转化为U盘模式,这样就可以将需要更新的APP直接复制进SD卡便可以了。也就是说我们需要在BootLoader需要实现:FatFs、USB读取SD、将APP加载三个主要的动作,前面两个存在现成的库,只要将其移植便可,主要是后面通过文件系统将SD卡读取到SDRAM,并且跳转。
三、将SDRAM映射到可以自行代码的区域
STM32的FMC将SDRAM映射到从0xC000 0000~0xCFFF FFFF的SDRAM bank1或者映射到0xD000 0000~0xDFFF FFFF的SDRAM bank2,但是这两段区域是不能执行代码的,也就是说,即使将APP代码搬运到这里,也执行不了,所以需要将SDRAM重新映射到另一个区域才能执行代码,查阅对应的参考手册:
需要在SDRAM执行代码时需要将SYSCFG_MEMRMP寄存器的bit10~11进行以上设置,这个remap的意思并不是说FMC将SDRAM放在0x6000 0000开始的区域里面,而是映射关系,也就是说映射处理后操作0xC000 0000的数据在0x6000 0000处也会同步改变。这个过程可能需要一定的操作时间,在执行操作后适当添加点延时。
相关的代码很简单:
SYSCFG->MEMRMP |= (1<<10); // 将sdram remap到0x60000000位置才可以使得cpu去执行
delay_ms(1000);// 适当延时使得映射完毕
四、MSP与PSP以及启动流程
MSP是复位后默认使用的栈指针,PSP是只能用于线程模式的栈指针。在没有使用OS的情况下只是用到了MSP,也就是说PSP的存在是能够支持OS的,也就是说在存在OS的情况下,线程(任务)使用的栈指针为PSP,OS内核以及中断使用的为MSP,这样将其分离使得对栈空间的使用得到了优化。
存储器存放的起始两个字的数据依次是MSP的初始化值以及.S汇编文件的复位函数的地址复位向量,如对应的汇编代码:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
.....
存储器开始存放的数据是一个名为__Vectors的向量表,程序运行之前会以存储器的第一个字作为MSP的初始化值,接着将第二个字给PC,从而开始执行代码,这个操作完成后会跳转到Reset_Handler函数去执行代码,这样一来就从汇编开始执行到main。在程序编译好了的bin文件中,起始的两个字依次表示MSP的初始值和复位函数的函数地址,接着这些后续的字依次是中断的向量,也就是说存放着各个中断函数的中断服务函数地址,当触发中断时,自动根据向量表的值跳转到对应的中断ISR。
五、模仿复位操作来跳转到APP
根据上一点分析可知,在对应的跳转区域可以执行代码的前提下,只要将新的程序的第一个字和第二个字分别赋值到MSP和PC便可,于是接下来的操作便是读取文件(APP固件)到SDRAM的一个固定地址ADDR,接着读取ADDR开始的第一个字到MSP,再读取第二个字到PC。
先来解决MSP的赋值,涉及到对寄存器的操作一般采用汇编程序,在MDK环境下汇编的引用格式可以如下:
__asm int function_name(uint32_t parameter)
{
....
....
....
BX r14
}
传入的参数依次会给R0开始的寄存器,例如假设有三个参数para1-3,这三个参数依次会传递给R0-R2,存在返回值时默认存放在R0中。以下是对MSP赋值的一个函数:
__asm void MSR_MSP(uint32_t addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
MSR将R0的值写入到MSP中,也就是将addr的值写入到MSP,可以单步调试来验证函数的执行效果。
接下来的任务就是对PC指针的操作了,才C语言的环境中有个函数指针的操作就是跳转到指定的程序地址执行代码 ,所以只要将第二个字的地址强制转换为一个函数指针,然后在执行这个函数就可以实现跳转了,相关操作如下:
JumpAddress = *(uint32_t *)(SDRAM_ADDR_SWP+4); //app的入口地址
Jump_To_Application = (iapfun) JumpAddress; //强制转化为指针
其中Jump_To_Application是一个函数指针,通过JumpAddress强制转化为函数地址来对其赋值,也就是说在后文通过执行:
Jump_To_Application();
便可以实现跳转。
六、APP的keil工程配制
必须设置MSP和PC指针的位置,在keil工程里面没有开启SCT文件的情况下代码个内存分配的设置有工具里面设置决定:
这里程序读取到0x6000 0000位置所以代码的位置为这个地址,之后便是运行内存为地址,这里如果不设置的话,是默认在片内的0x800 0000地址,使用时无法跳转不过去,所以这里按照自己跳转的具体位置进行设置。
由于从ROM跳转到SDRAM后中断向量表没有跳转过来,会导致在APP使用中断时会使得程序跑飞,为了解决这个问题,我们在跳转到APP后将中断向量表重新映射到跳转的起始位置,具体操作在APP程序的SystemInit函数修改如下:
#ifdef VECT_TAB_SRAM
SCB->VTOR = RAMDTCM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
//SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
SCB->VTOR = SDRAM_BASE | VECT_TAB_OFFSET; // 映射到SDRAM_BASE + VECT_TAB_OFFSET
#endif
其中SDRAM_BASE为一个宏,其值为0x6000 0000,这样在跳转后便可以正常使用中断。
为了防止跳转过程中中断的干扰,需要在BootLoader里面跳转之前对中断进行屏蔽,并且在进入APP后对中断进行打开,完整的BootLoader跳转部分代码如下:
// 将sdram remap到0x60000000位置才可以使得cpu去执行
SYSCFG->MEMRMP |= (1<<10);
delay_ms(1000);// 适当延时使得映射完毕
__asm("CPSID I"); // 关闭中断
JumpAddress = *(uint32_t *)(SDRAM_ADDR_SWP+4); //app的入口地址
Jump_To_Application = (iapfun) JumpAddress; //强制转化为指针
MSR_MSP(*(uint32_t*) SDRAM_ADDR_SWP); //MSP赋值 main_SP
Jump_To_Application();
相关源码置顶可下载。