引言
一般工程师都怕研究MCU的startup过程,其原因可能有:1.觉得没有必要,startup的过程和启动代码在新建工程时,并且已经默认加入并配置好,能够保证MCU正常工作,只要关系main()函数开始的用户程序就好(这其实对于大部分工程师来说确实如此);2. Startup过程往往需要一定的MCU内核CPU汇编指令知识,很多对内核寄存器/堆栈指针的初始化、I/D-cache的初始化过程往往需要使用专门的汇编指令,所以让很多工程师望而怯步; 但是如果你需要对RAM初始化进行个性化定义,想要搞清楚main()函数之前的所以准备工作,想要真正理解并自己开发一款嵌入式MCU的BootLoader,想成为真正的嵌入式MCU专家,就十分有必要搞清楚这个过程了。
嵌入式MCU的复位源
嵌入式MCU在硬件复位或者系统复位后,都是从复位向量所指向的复位中断ISR开始运行的,因此复位中断ISR一般也是整个嵌入式应用工程的入口(_EntryPoint)函数/启动(startup/boot)函数。
通常导致嵌入式MCU复位的复位源如下:
虽然我们用户编程一般不涉及MCU的软件启动过程,用户代码一般都是从main()函数开始,完成系统的时钟,工作模式和外设初始化,全局变量/数据结构的初始化,最后打开内核CPU全局中断,进入主程序(主循环状态机),用户的程序设计跟标准C语言程序设计无异。但了解清楚嵌入式MCU的startup过程对于深入理解嵌入式系统软件还是大有裨益的。
Startup相关的几个问题:
我们先来思考几个问题:
a. 我们都知道,C语言中的全局变量分为有初始化值(.data段变量)和无初始化值(.bss段变量—初始化值为0的全局变量和.common段变量—无初始化值的全局变量)两大类。我们在main()函数中,用户代码并没有对全局变量进行初始化,但使用全局变量时其初始化值已经确定--.data段变量已为其定义时的初始化值,而 .bss/.common段变量全为零,这是如何做到的呢 (要知道,嵌入式MCU中这些全局变量都是被分配到RAM中的,其每次上电之后的值时随机的。) ?如果我们想要某些全局变量在只在POR上电复位时被初始化而其他系统复位时不初始化,有该如何实现呢?
b. 在汽车MCU中,为了保证RAM数据的可靠,其RAM也常带有ECC功能(比如Qorivva MPC56xx/57xx系列MCU和S32K系列MCU),而每次上电之后RAM中的数据是随机的,如果不对其进行初始化就去读取其数据,就极容易产生ECC错误,从而进入系统异常,那么对RAM的初始化又是如何完成的呢?
c. C语言正常运行所需要的堆栈(stack)是如何指向RAM中用户指定的地址空间的呢?
d. 我们的用户程序都是从main()开始运行的,那么可不可以是其他的函数呢?main()函数一定是必须运行的吗?
嵌入式MCU Startup过程详解
你知道到吗?以上准备工作几乎全是在startup的过程中完成的,且看以下嵌入式MCU的一般startup流程:
具体每一步完成的初始化工作如下:
初始化CPU寄存器/关闭全局中断:每次POR上电或者复位之后,CPU寄存器除了PC寄存器有意见复位逻辑赋以存储在默认复位向量中的复位函数ISR(即_EntryPoint()/startup()函数) 地址外,其余的CPU寄存器值都是随机的,所以有必要对其进行初始化,然后再使用;除此之外,为了保证整个startup过程不受外设中断的影响,需要将内核CPU全局中断关闭(disable);
初始化看门狗:为了保证整个startup初始化过程正常进行,有必要对MCU内部看门狗进行初始化,关闭或者初始化一个溢出周期并在初始化过程中进行喂狗;(这个过程一般由编译器预编译变量控制,在工程属性编译器设置,对于有些MCU,比如S12系列MCU,其片上COP看门狗在正常模式(normal single chip mode)下只能配置一次,这样如果在startup的过程将其关闭了,在用户程序中就无法重新使能和配置了);
初始化RAM ECC:如果使用的MCU其RAM带有ECC功能(比如Qorivva MPC56x/57xx系列MCU),必须在使用前对其进行初始化(其过程就是往RAM中写出初始化数据已产生确定的ECC结果,一般是将之前的CPU寄存器值循环写入整个RAM空间,当然,对其赋值零也是可以的,之所以使用CPU寄存器是因为其访问速度快,而且有专门的单指令多数据(SIMD)支持将多个CPU寄存器写入RAM),否则会造成ECC错误,进入系统异常(比如PowerPC e200Z0内核的IVOR1—machine check/IVOR2—data storge exception);
下表列出了Freescale/NXP的汽车MCU存储器ECC功能:
配置存储器控制器:对于很多32位MCU来说,由于其内核CPU运行速度比较快(100~300MHz)而存储器的工作时钟频率往往较低,所以一般器存储器控制器都设计有buffer来控制访问效率;
比如Qorivva MPC564xA系列MCU通过BIUCR寄存器的WWSC和RWSC位来抽空读写片上P-Flash时的总线等待周期,当内核系统时钟较高时就需要减小等待周期以保证MCU正常工作:
另外,如果所使用的MCU内核CPU有片上指令/数据缓存(I-cache和D-cache,比如S32K14x和MPC57xx系列MCU),还需要对其进行初始化—Flush操作;
初始化C语言堆栈(stack):从链接结果中,读取栈顶地址将其写入CPU寄存器的SP寄存器,从而完成堆栈(stack)的初始化工作,这之后内核CPU就可以正常运行C语言代码了。
初始化RAM(copydown):在对RAM初始化时,内核CPU会读取编译链接结果中的启动结构体(链接器自定义,不同的编译器其结构和形式可能不同),从中获得RAM的初始化信息,其包括如下信息:
1. .data段全局变量初始化值在Flash中的存储地址和在其RAM中的运行时地址,以及长度(单位为字节);
2. .bss/.common段全局变量在RAM中的运行时地址,以及长度(单位为字节);
根据这些信息,内核CPU就可以对RAM进行初始化了:
(注意:编译器默认都是把所有相同段的数据放在连续的地址空间以提高初始化效率)
.data段全局变量区:从Flash中读取初始化值并将其写到对应的RAM地址空间;
.bss/.common段全局变量区:对其RAM地址写入零一完成初始化;
(从以上分析可知,.bss/.common段全局变量是不占Flash空间的,即在编译结果S19文件中也没有其初始化值)
下面是一个S12G128的具体工程编译链接结果:
启动结构体—startupData存储在地址0xC044(P-Flash地址),占用6个字节,包含RAM初始化信息:
在工程map信息中可以看到工程链接后系统栈顶(__SEG_END_SSTACK, stack结束地址+1)结果,期用于startup工程中对SP寄存器进行初始化:
该工程中的全局变量定义以及其默认编译链接结果:
初始化系统时钟:可以将MCU的时钟初始化放在main()函数之前,以缩短startup的时间。根据不同的编译器,其为可选配置。
跳转至main()函数: 在完成以上startup过程之后,在startup函数的最后就是调用main()函数,跳转至用户应用程序执行;(PS: 这里,其实我们可以修改让内核CPU跳转到自定义的任何函数,而非默认的main()函数)
以上介绍的时嵌入式MCU startup的一般流程,不同的MCU其片上资源和特性存在差异,所以上述初始化步骤也可能有所差异。
这就是今天先跟大家分享的内容,希望能够对大家有所帮助。
如果你喜欢本公众号的文章,请点击文章最开始的公众号关注或直接扫描识别下方二维码关注,你也可以在微信添加朋友-->公众号-->输入"汽车电子expert成长之路"搜索-->点击关注。若对本文观点有任何意见和建议也欢迎留言指出。您的关注、点赞、转发分享是对我辛勤写作的最大肯定。
胡恩伟
NXP汽车电子FAE
2017年7月23日于山城·重庆
转自
https://mp.weixin.qq.com/s?__biz=MzI0MDk0ODcxMw==&mid=2247483698&idx=1&sn=715d0992591757d3f6ab8d59d0163ff5&chksm=e91245b4de65cca2f7d16c605956d0acd7db3522437ceccfc73129e87553aec5d44c540d6984&scene=21#wechat_redirect