向RAM中加载程序并运行

STM32启动分析。回到最初的起点,一切其实很简单!

2020-04-15 09:31·数学之声

一、时钟是芯片的心跳,第一声心跳由HSI提供

HSI = High Speed Internal Clock内部高速时钟,不依赖外部电路,有电就能工作的时钟。如果HSI坏了芯片就坏了,就算有外部时钟也不行,因为上电启动只能用HSI。STM32F1系列HSI频率8M,F4系列16M,精度都是1%,对于执行慢速,精度要求不高的应用已经足够,比如串口收发信息,读取传感器信息,控制开关等。

如果精度要求较高,就需要切换为高精度的外部时钟HSE(High Speed External Clock,这个切换过程是在HSI驱动下完成的。时钟切换成HSE后,可关闭HSI时钟,能够节省少量功耗。当HSE出现故障时,硬件会自动打开HSI,并将时钟系统切换到HSI;如果开启了时钟安全功能,还会产生NMI中断。

时钟框图


二、芯片自动从0地址读取32位整数,设置MSP主堆栈指针

问1:为什么用硬件设置MSP,不让软件来做这件事?

答1:因为芯片在执行一条指令的时候就有可能发生中断,处理中断要使用堆栈指针MSP。如果用软件来设置MSP,就有可能在第一个中断产生时还没有设置正确的MSP,导致堆栈错误。

问2:为什么只设置MSP不设置PSP?

答2:中断处理分两个部分,1是现场的保存和恢复,2是中断自身的处理。第1部分既可以用MSP又可以用PSP,第2部分只能用MSP。如果被中断的程序正在使用PSP,那么环境保存恢复就用PSP;如果被中的程序正在使用MSP,那么环境保存恢复就用MSP。假如芯片启动时默认使用PSP堆栈,那么在第一个中断产生前,硬件就必须设置好MSP和PSP两个堆栈,就需要在0地址和4地址各放一个32位整数。CORTEX M4内核启动默认使用MSP堆栈,因此只用设置MSP即可。

问3:STM32F4复位后所有中断都是关闭的,为什么还要硬件设置MSP?

答3:STM32F4启动后,所有的中断都是关闭的,只要小心的写启动代码,各种FAULT也是完全可以避免的。另外那个无法关闭的NMI中断,它的唯一中断源CSS(时钟安全系统)启动时也是关闭的,因此也不会产生NMI中断。所以对于STM32F4来说,是可以在0地址随便写个值,然后用软件初始化MSP,但这么做没什么意义,还浪费空间。


三、芯片自动从4地址读取32位整数,并跳转到该地址执行

内核:特指CORTEX-M3或者CORTEX-M4内核。

内核上电时,会从0x00000000地址读取堆栈指针,再从0x00000004地址读取第一条指令的地址并跳转到该地址执行。

注意:这个复位地址是无法更改的。上电复位只能从0地址开始。PC只能指向0x00000000。

网上很多其他资料都说:

更改BOOT设置后,PC复位地址可以指向FLASH的起始0x0800_0000,或者指向SRAM的起始地址0x2000_0000,或者指向芯片内置BOOT程序的起始地址0x1FFF_0000。

这个说法是完全错误的。上电复位后,PC只会指向0地址。

我们看看STM32的实际情况:

启动空间映射图

如上图所示,0地址开始的128M空间为启动空间,该空间不存在任何物理存储器。STM32通过2个BOOT引脚可将3个物理存储器映射到启动空间(上图蓝色箭头),包括:

  1. 片上FLASH存储器 ,起始地址0x0800_0000
  2. 内置BOOT程序,起始地址0x1FFF_0000,大小30KB
  3. 片上SRAM存储器,起始地址0x2000_0000

另外,软件还可配置 SYSCFG_MEMRMP 寄存器,将外部SRAM(上图红色箭头)映射到启动空间。注意:寄存器可配置全部4种映射,且会覆盖BOOT引脚的配置。

启动空间,片上FLASH,内置BOOT这3块地址空间的读写使用的是I-CODE和D-CODE总线。片上SRAM和外部SRAM则用的是SYSTEM总线。

举个例子,将片上SRAM(起始地址0x20000000)映射到启动空间后,对0地址的读写就是对0x20000000地址的读写。但是用0地址读写SRAM时,用的是I-CODE或D-CODE总线。而用0x20000000地址读写SRAM时,用的是SYSTEM总线。使用的总线不同,访问速度和总线冲突的情况就不同。我在另外一篇文章中详细说明了这两者的区别。

将FLASH映射到启动空间的情况:由于FLASH本来就是用I-CODE和D-CODE总线访问的,因此对0地址的读写和对0x08000000的读写是完全相同的,没有任何区别。

为什么STM32不将片上FLASH直接放到0地址,让内核固定从FLASH启动?而要做成用多种启动方式呢?各位可自己做个简单的实验,将芯片配置成FLASH启动,然后将下面的代码放到复位处理函数Reset_Handler的前两行,看看会发生什么事情。

CPSID    F

B           {PC}


四、芯片按顺序执行指令

上电跳转到第一条指令后,芯片就会按照顺序依次执行指令。当遇到跳转指令时,就会跳转到指定的地址执行。我在另外两篇文章中分析了指令的执行时间和功耗,这里讨论一下指令的存储地址执行地址

使用MDK编译程序时,MDK会为程序的每条指令规定好存储地址和执行地址。存储地址就是MDK将程序下载到STM32芯片时写入的地址,一般都是片上FLASH的地址0x0800_0000。而执行地址则是程序运行的地址。对于STM32来说,绝大部分情况下,存储地址和执行地址都指向片上FLASH,因为片上FLASH即可用来存储程序又可用来执行程序。

某些情况下,我们需要将程序的存储地址和执行地址设置为不同的值。这种情况下,需要先把程序从存储地址拷贝到执行地址后才能正常运行。比如一段程序,设置他的存储地址是0x0800_1000,设置他的执行地址是0x2000_2000。那么如果要正确运行这段程序, 就需要先将这段程序从0x0800_1000拷贝到0x2000_2000,然后让PC跳转到0x2000_2000执行。如果让PC直接跳转到0x0800_1000地址执行,则很可能会出错。比如函数function1,它的存放地址是0x0800_1100,它的执行地址是0x2000_2100,使用BLX指令调用这个函数,代码如下:

LDR    R0, =function1

BLX    R0

这里R0的值就是0x2000_2100,PC指针就会跳转到0x2000_2100执行。如果事先没有将程序拷贝到0x2000_2000地址,那么0x2000_2100的地址是没有function1函数代码的,只有一些随机的值,这样程序就跑飞了。使用绝对地址读写ROM时也会出现类似问题。但是如果一段程序只使用相对跳转和相对加载,那么这段程序可以在任何地址正确运行。

MDK的图形界面无法将执行地址和存储地址设置为不同的值,需要修改分散加载文件才能实现。下图是分散加载文件的内容,实现了上面描述的情况。本文最后会用实例进行想详细的说明。

分散加载文件


五、芯片要正确处理各种中断

内核总共支持256个中断(设置MSP也算成一个中断),前16个是固有的系统中断,只要使用CORTEX-M3和M4就必须包含这16个中断并正确处理。后面240个是可选的外部中断,可由芯片制造商自主决定如何使用,但至少要使用1个。也就是说,最少会有17个中断,最多有256个中断。

中断向量表记录了每个中断处理函数的起始地址,每个地址占用4个字节。如果芯片使用了全部256个中断,则中断向量表大小是256 × 4 = 1024字节。如果芯片用了17个中断,中断向量表的大小就是 17 × 4 = 68 字节。

内核复位后,只会0地址开始读取中断向量表,上电后可通过软件修改中断向量表的位置。所以MSP和复位中断必须放在0地址和4地址,而其他中断可放到另外的地址。通过VTOR寄存器修改中断向量表的起始位置:

VTOR = Vector Table Offset Register(地址0xE000ED08)

中断向量表的起始位置有特殊的要求。比如当前使用17个中断,由于17不是2的指数,先找到大于17的2的指数,也就是32,再用32×4=128,则中断向量表的起始地址必须是128的整数倍。同理,如果当前使用256个中断,则中断向量表的起始位置必须1024字节对齐。由于中断向量表至少是128字节对齐,所以VTOR寄存器的低7位是保留位必须是0。


六、MDK4环境下新建一个比较复杂的STM32F429工程

满足如下要求:

  1. 含引导程序BOOT,大小4K以内。在FLASH中执行
  2. 含APP程序,大小32K以内。在0x2000_2000开始的内存空间里运行
  3. 启动时由BOOT将APP加载到内存中运行
  4. BOOT和APP在同一个工程中
  5. 使用I-CODE和D-CODE总线运行APP
  6. APP使用CCM内存

1、新建工程,选择芯片型号为STM32F427IG。由于MDK4没有STM32F429,所以用STM32F427代替,或者选择CORTEX-M4的工程也可以。

STM32F427

2、不用系统提供的启动文件,我们从STM32F429的标准库中获取或者自己写。

不用MDK4提供的启动文件

3、新建Boot和App两个组,分别用来存Boot和APP相关代码。并建立boot.s,other_boot_file.c;app_startup.s,main.c四个空文件,放到对应组中。

工程结构

4、在MDK中用图形界面配置存储空间和内存空间。

BOOT大小4K,存放在FLASH的开头,起始地址0x0800_0000,大小0x1000

APP大小32K,存放在BOOT之后的FLASH空间,起始地址是0x0800_1000,大小0x8000

BOOT使用内存SRAM1,起始地址是0x2000_0000,大小4K字节0x1000

APP使用CCM内存,起始地址是0x1000_0000,大小是64K字节0x10000

按照下面的两张截图设置好

空间分配

空间分配

5、分别为boot组和app组分配ROM和RAM

6、编译工程让MDK生成默认的SCT分散加载文件

注意,链接时会报错,提示没有RESET段,先不管它。此时已经生成了默认的SCT文件BootApp.sct,文件如下图:

MDK自动生成的分散加载文件

可以看到,MDK在分散加载文件中对每个文件都进行了设置。如果工程中新增了文件,MDK会自动将新的文件设置到分散加载文件中。同样,如果删除了某个文件,MDK也会自动处理好。

如果自己写分散加载文件,就需要正确处理每个文件,还要处理好新增和删减文件的情况。我们新建一个分散加载文件BootAppCustom.sct,并让MDK使用这个自定义的分散加载文件:

使用自定义分散加载文件

7、修改分散加载文件BootAppCustom.sct的内容

为了能自动处理新增和删减文件的情况,我们规定所有BOOT组的文件名必须包含小写的“boot”字符串。所有APP组的文件名都不能包含boot字符串。在更庞大的工程里,“boot”这个字符串可换成更加复杂的形式,比如“
Company_Name_Boot_Loader_Code”,以保证APP组中的文件名肯定不会出现这个字符串。修改的最终结果如下图所示:

自定义分散加载文件

用 *boot*.o 匹配所有带boot字符串的文件。

APP存放在0x0800_1000,需要拷贝到0x2000_2000地址执行。还需要用I-CODE和D-CODE总线执行APP。因此要将SRAM1映射到启动空间,APP在启动空间执行,那么APP的执行地址就是0x0000_2000

APP_RESET, +First 用来指定App中断向量表的位置,保证中断向量表放在开头的位置。

*(InRoot$$Sections) 是__main函数使用的。__main函数不是我们常说的那个main函数,是MDK自动生成的一个函数,作用就是初始化ARM标准C库,初始化 ZI 和 RW ,初始化浮点运算单元,初始化堆栈等等。

.ANY 将所有非boot的文件放到APP空间。

8、编写boot.s启动文件和other_boot_file.c文件

boot.s先关中断,再将APP从FLASH拷贝到SRAM,然后设置中断向量表偏移量,再将SRAM1映射到启动空间,最后跳转到APP执行。代码中有详细注释,可下载后自行查看。

other_boot_file.c中写了NMI中断处理函数。

9、编写app_startup.s和main.c

app_startup.s就用STM32F429标准库提供的启动文件startup_stm32f429_439xx.s,去掉其中SystemInit的调用即可。官方启动的详细介绍网上有很多,可自行搜索。

main.c就是一个死循环。

10、编译工程,解决L6202E链接报错

完成代码编写后,编译工程,报错如下:

针对L6202E错误查询MDK的帮助文档:

L6202E

<objname>(<secname>) cannot be assigned to non-root region '<regionname>'

A root region is a region that has an execution address the same as its load address. The region does not therefore require moving or copying by the scatter-load initialization code.

Certain sections must be placed in root region in the image. __main.o and the linker-generated table (Region$$Table) must be in a root region. If not, the linker reports, for example:

Region$$Table cannot be assigned to a non-root region.

Scatter-loading (__scatter*.o) and decompressor (__dc*.o) objects from the library must be placed in a root region. These can all be placed together using InRoot$$Sections:

__main和一些特定的函数必须放在root region中,而root region的特点就是存放地址和执行地址相同。这是MDK为了保证启动过程正确所作的限制。在有Boot引导程序的情况下,这个限制其实是没有必要的,但目前也没有方法可解除该限制,只有更改分散加载文件来适应这个情况。新的分散加载文件如下:

将存放地址从0x0800_1000改成0x0000_2000,这样存储地址和执行地址就一致了,LR_IROM2也就变成了root region。用MDK下载APP到芯片时,会向地址0x0000_2000写入数据,此时是FLASH映射到启动空间的,因此也就是向FLASH下载数据。而运行时,将程序从FLASH拷贝到内存0x2000_2000后,修改了启动空间映射,将SRAM1映射到启动空间了,此时0x0000_2000的运行地址是对应的0x2000_2000内存空间。

上图中,LR_IROM1LR_IROM2两个存储地址处在不同的空间。为了能够正确的下载FLASH,需要在MKD的FLASH设置里面将这两个地址段都添加进去:

11、调试程序,解决Hard Fault异常的问题

上述工程编译通过后,进行调试运行,发现代码无法运行到main函数中。停止后发现芯片进入了Hard Fault异常,我们来处理这个异常。在MDK调试界面的Register栏,可以看到MSP的值:

MSP的值是0x10000428,我们再看看带浮点的CORTEX-M4的中断处理压栈的过程:

中断处理入栈情况

产生中断后,内核会将寄存器按照上图的顺序保存到堆栈中,入栈完成后,MSP指向存放R0的内存地址,而存放返回地址的指针比MSP要大24个字节,返回地址就是产生异常的指令地址。用MSP+24 = 0x10000428 + 24 = 0x10000440,再查看0x10000440地址存放的数据:

可以看到,存放的是0x0000234A(小端模式),这就是产生异常的指令地址。在反汇编窗口查找该地址对应的指令:

产生异常的指令是VMSR FPSCR, R0,功能向FPSCR寄存器写入R0,就是写FPSCR寄存器导致的异常。再看CORTEX-M4相关说明:

可以看到,要访问FPSCR寄存器,必须先打开CPACR寄存器的相关值,否则就会产生异常。

从下图中可以看到,在SystemInit函数中对CPACR进行了设置,使能了FPSCR的访问。但我们没有调用SystemInit所以导致了对FPSCR寄存的访问异常:

现在有两种解决方式,1是打开CPACR,2是关闭FPU功能。两种方法都可解决异常的问题。FPU的开关在MDK中设置如下:

我们选择第1中方式打开FPU,重新编译工程,然后进行调试,程序可正常进入main函数

注意:不用FPU的时候一定要将其关闭。

关闭FPU后,中断处理的环境保存和恢复只涉及到8个寄存器32个字节。

打开FPU后,中断处理的环境保存和恢复涉及到了8+17=25个寄存器,还会涉及到8字节对齐问题,中断处理会慢很多。


工程下载地址

链接:
https://pan.baidu.com/s/1WHAGWKyQRIpLjhWcoiABDw

提取码:nq1h

思考:

如果按照下面的分散加载文件,如果将Boot的存储地址和执行地址都设置为0是否有问题?

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值