遇到了一个坑,顺便就梳理了下ARM单片机的启动流程.
简单的介绍开发环境:
-
目标板:STM32F412xx. 512K的FLASH(0x08000000,0x080080000)和256K的RAM(0x02000000,0x020040000)
-
开发环境:keil + JTAG
应用具有升级功能,存在两个固件BL(bootloader),AP(application).
空间分配
-
BL使用的空间是(0x08000000,0x08040000),分了256K,因为我的BL中放了很多的字符和图片资源.
-
AP使用的空间是(0x08040400,0x08080000) ,0x08040000 ~ 0x080403FF 存放了AP的签名
启动跳转
上电后,BL获取控制权,对AP的签名信息和固件进行检查.检查通过后,跳入到AP的入口地址处(0x08040400).
跳入的代码如下
void jump_ap(uint32_t addr)
{
__set_MSP(*(uint32_t*)addr);
( (void(*)(void)) (*((int*)(addr+4))) )();
}
/*不是很好懂,可以写成这样*/
typedef void (*pv_call_t) (void);
void jump_ap(uint32_t addr)
{
int *p_sp = (int*)addr;
int *p_pc = (int*)(addr + 4)
//set msp
__set_MSP(*p_sp);
//set pc
(pv_call_t)(*p_pc)();
}
/*还是用汇编写比较容易看*/
static __asm void jump_ap(uint32_t addr)
{
LDR r1,[r0] ; r1 = *addr
MSR MSP,r1 ; msp = r1
LDR pc,[r0,#4] ; pc = *(addr + 4)
}
在BL中做一些检查工作,检查完毕,直接调用 jump_ap(0x08040400) 进入AP.
预警,坑出现
在我的ap里,有一个比较大的字符串结构体表,代码类似这样.
struct __string_table_st {
int res_id;
const char *str_en;
const char *str_cn;
};
struct __string_talbe_st string_tbl[] = { //string_tbl的大小大概在80左右.
{RES1,"hello world","你好世界"},
...
};
我用JTAG跟踪AP,在BL执行完毕后,断点停在了AP的main()函数入口处.ap的main类似如下
int main()
{ //[x] 断点处
SCB->VTOR = 0x08040400; //设置中断向量入口
HAL_Init();
xx_Init();
...
xx_Init();
...
work();
}
断住后,我去查看string_tbl的值,发现有数组的第55组数据竟然被更改,如下图:
感觉头顶悬了一颗地雷,一下子紧张起来,这里数据被更改,意味着内存数据有踩踏的地方.危险大发了.
观察和分析:
- 其他变量都是正常的,唯独第数组第55组中文字符串部分出错
- 因为这个现象,误导我以为是中文的错误,花了不少时间排查,最终发现与中文无关,与在内存位置有关
- 如果将 string_tbl修改为const,这样所有的数据都放在flash里,肯定是正常的.其实也应该将这个数组放在flash里,不过,很感谢当初我忘记了加const. 不然真的非常难暴露这个问题.
- 基本确定是内存被踩踏了
- 考虑到ap是从bl跳过来,如果直接运行ap呢? 于是我将ap编译到0x08000000的位置上.证明是没问题的.
- 基本确定是bl跳转到ap的过程中出问题的. (至此,我并没有在代码中暴露太多的细节,有经验的老手,应该猜出几种可能了)
启动内存加载流程
既然问题出现在跳转和加载过程,我们就来分析一下固件的加载流程。
BL和AP的加载流程是一样的,无非是PC指针从哪里开始的区别.这里使用AP做例子.
如图,AP固件编译后的并下载到flash的布局如图左边部分,Vectors里前两个地址里存放的内容是SP指针和ResetHander函数,从BL到AP跳转过程,就是将SP指针赋值给MSP,将ResetHander赋值给PC指针(见,启动跳转).于是PC从ResetHander开始执行,ResetHander大致如下:
Reset_Handler PROC
EXPORT Reset_Handler
IMPORT SystemInit
IMPORT __main
LDR R0,=SystemInit
BLX R0,
LDR R0,=__main
BX R0
ENDP
SystemInit函数,初始化体系相关的寄存器,这里关键是–main 函数. 这个不是我们实现的函数,我们实现的main().–main函数是keil提供的库里实现的,存在的目的是帮助我们实现将
- 将.data从flash里拷贝到内存里(上图中箭头标识)
- 将.bss端清0
- 调用main函数
回到我们的BUG里,string_tbl是属于.data段的,我把断点设置在了main的入口处,而在此之前已经由–main完成了string_tbl的加载和初始化. 所以是在这个过程中,string_tbl的第55个元素的所在内存被修改了.原因是发生了中断,而且在这个中断里对内存进行了修改,这个中断处理函数在BL里,在BL固件里我们执行了某些驱动的初始化,配置了某些中断,而在跳转的时候,有的中断被触发了,于是在BL里的中断服务函数被执行了.在我的应用里,是系统定时器被触发了,而且定时器的全局变量恰好与string_tbl的内存重叠.进而修改了该全局变量所在内存,而该内存恰好就是string_tbl的第55个元素的位置!!
总结
知识点很重要,不按规则做,看似正常的代码,往往隐藏很大的坑,这也是代码拿来主义最大的弊端。以此文为例,好在修改的是我需要显示的内容,比较容易发现问题。否则,极难找到问题,你的内存某个地方被修改,可能是无关紧要的部分,也可能是非常重要的部分;调试的时候,能调试到你怀疑人生。
知道问题时,解决就很简单. 跳转前,关闭所有中断响应,具体指令看你的固件手册吧.