背景
一下内容是在ubuntu使用gcc自己编译stm32得出的自己的总结,使用keil的时候很多东西,都已经由keil做好了,包括启动文件就是不同的,链接脚本是不需要自己管的。只是为了更加深入的理解整个系统。
链接脚本
编译生成的各个静态库文件.a文件。需要使用链接脚本一起,才可以,链接(重新组织出)出一个,完整有序的,包含所有信息的bin文件。
以下以stm32f407的链接脚本为例
1.设置入口ENTRY
stm32设置的入口即是,中断向量表里的系统复位向量。
32 /* Entry Point */
33 ENTRY(Reset_Handler)
34
35 /* Highest address of the user mode stack */
36 _estack = 0x2001FFFF; /* end of RAM */
37 /* Generate a link error if heap and stack don't fit into RAM */
38 _Min_Heap_Size = 0x200;; /* required amount of heap */
39 _Min_Stack_Size = 0x400;; /* required amount of stack */
stm32上电时会做两件事,将0x080000000这个栈顶地址放到,msp寄存器。将第二个地址Reset_Handler赋给,pc指针。这样我们上电debug的时候会发现,单片机第一步就会停留在启动文件里的Reset_Handler哪里。
2.设置MEMORY
37 /* Generate a link error if heap and stack don't fit into RAM */
38 _Min_Heap_Size = 0x200;; /* required amount of heap */
39 _Min_Stack_Size = 0x400;; /* required amount of stack */
40
41 /* Specify the memory areas */
42 MEMORY
43 {
44 FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
45 RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
46 CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
47 }
链接脚本基本就分为两个部分,memory和sections。其中memory规定了单片机rom和ram的地址和大小。每种芯片的这一部分的内容应该都是不一样的。
3.设置SECTIONS
接下来主要是看sections这个部分,通过这个部分我们就可以看到整个映像的各个部分的内容是放在哪里的。
52 /* The startup code goes first into FLASH */
53 .isr_vector :
54 {
55 . = ALIGN(4);
56 KEEP(*(.isr_vector)) /* Startup code */
57 . = ALIGN(4);
58 } >FLASH
第一部分当然是启动文件,其中最重要的就是中断向量表。启动文件里,规定了有哪些中断函数,发生中断时间时,会自动跳转到对应的中断函数,不同的芯片定义的函数是不一样的。
需要注意的是bin文件里面,包含了,代码段,只读数据段,数据段,bss段(未初始化的全局变量,只有bss段的地址大小描述信息,并不真正改变bin文件的大小),堆栈(同样不会真正占用bin文件的大小)等全部信息。这只是加载的镜像文件,真正在执行的时候,并不是所有的文件都是在rom里面执行的。链接脚本会将数据段等,从rom里面拷贝到ram里面执行。
- 放到rom里面的内容
60 /* The program code and other data goes into FLASH */
61 .text :
62 {
63 . = ALIGN(4);
64 *(.text) /* .text sections (code) */
65 *(.text*) /* .text* sections (code) */
66 *(.glue_7) /* glue arm to thumb code */
67 *(.glue_7t) /* glue thumb to arm code */
68 *(.eh_frame)
69
70 KEEP (*(.init))
71 KEEP (*(.fini))
72
73 . = ALIGN(4);
74 _etext = .; /* define a global symbols at end of code */
75 } >FLASH
77 /* Constant data goes into FLASH */
78 .rodata :
79 {
80 . = ALIGN(4);
81 *(.rodata) /* .rodata sections (constants, strings, etc.) */
82 *(.rodata*) /* .rodata* sections (constants, strings, etc.) */
83 . = ALIGN(4);
84 } >FLASH
可以看出放到rom里面的是代码段,和只读的数据段,例如字符串。
- 放到ram里面的内容
117 /* Initialized data sections goes into RAM, load LMA copy after code */
118 .data :
119 {
120 . = ALIGN(4);
121 _sdata = .; /* create a global symbol at data start */
122 *(.data) /* .data sections */
123 *(.data*) /* .data* sections */
124
125 . = ALIGN(4);
126 _edata = .; /* define a global symbol at data end */
127 } >RAM AT> FLASH
151 .bss :
152 {
153 /* This is used by the startup in order to initialize the .bss s ecion */
154 _sbss = .; /* define a global symbol at bss start */
155 __bss_start__ = _sbss;
156 *(.bss)
157 *(.bss*)
158 *(COMMON)
159
160 . = ALIGN(4);
161 _ebss = .; /* define a global symbol at bss end */
162 __bss_end__ = _ebss;
163 } >RAM
可以看出来,数据段和bss段最终都是放到ram里面的。
注意:其中>RAM AT>FLASH这个命令告诉连接器,这段内容是需要搬运的,下载地址和实际运行的地址是不一样的。需要从flash复制到ram里面才可以跑的。
以上就是最终编译链接出的bin文件,是如何在单片机里面跑的。整个bin文件都会放到rom里面,包含了许多的信息,最终跑起来的时候会将,数据段从rom里面,拷贝到ram里面去执行。根据bin文件里的信息,也会知道,bss段,堆栈的地址和大小,这样整个系统就可以跑起来了。(切记不是直接放到ram里面的,因为整个bin文件都是放到rom里面的,要在ram里面使用的时候,是需要拷贝的。也就是说加载的和执行的是不一样的)
启动文件
在单片机跑到mian函数,应用程序之前的代码就是启动文件的代码。
下面我们来具体分析一下这个里面包含的内容:
143 g_pfnVectors:
144 .word _estack
145 .word Reset_Handler
146 .word NMI_Handler
147 .word HardFault_Handler
148 .word MemManage_Handler
149 .word BusFault_Handler
150 .word UsageFault_Handler
151 .word 0
152 .word 0
153 .word 0
154 .word 0
155 .word SVC_Handler
156 .word DebugMon_Handler
157 .word 0
158 .word PendSV_Handler
159 .word SysTick_Handler
160
161 /* External Interrupts */
162 .word WWDG_IRQHandler /* Window WatchDog */
163 .word PVD_IRQHandler /* PVD through EXTI Line detection */
164 .word TAMP_STAMP_IRQHandler
...
以上可以看出来,flash最开头的地方就是,中断向量表,从地址0x0800000,开始,第一个就是栈顶指针,第二个就是,Reset_Handler,地址0x08000004。系统复位进入Reset_Handler对应的中断函数,接下来我们看一下,中断函数里面有什么。
79 Reset_Handler:
80 ldr sp, =_estack /* set stack pointer */
81
82 /* Copy the data segment initializers from flash to SRAM */
83 movs r1, #0
84 b LoopCopyDataInit
85
86 CopyDataInit:
87 ldr r3, =_sidata
88 ldr r3, [r3, r1]
89 str r3, [r0, r1]
90 adds r1, r1, #4
91
92 LoopCopyDataInit:
93 ldr r0, =_sdata
94 ldr r3, =_edata
95 adds r2, r0, r1
96 cmp r2, r3
97 bcc CopyDataInit
98 ldr r2, =_sbss
99 b LoopFillZerobss
100 /* Zero fill the bss segment. */
101 FillZerobss:
102 movs r3, #0
103 str r3, [r2], #4
104
105 LoopFillZerobss:
106 ldr r3, = _ebss
107 cmp r2, r3
108 bcc FillZerobss
109
110 /* Call the clock system intitialization function.*/
111 bl SystemInit
112 /* Call static constructors */
113 // bl __libc_init_array
114 /* Call the application's entry point.*/
115 bl main
116 bx lr
117 .size Reset_Handler, .-Reset_Handler
怎么样,片子怎么跑起来的是不是非常清晰。
- 首先设置栈顶指针
- 拷贝数据段
- 清bss段
- 执行函数SystemInit ,初始化系统时钟等等。
- 跳转到main函数
IAP远程升级
中断向量表的重定位(摘自cotex3权威指南):
有了以上的背景知识,其实远程升级是一件很简单的事情。具体思路如下:
- 有一份iap代码,中断向量表在0x08000000,主要工作就是对升级数据包即app程序的代码,进行接收(也可以在app代码里面接收),校验。并将校验通过的代码写进0x08000000之后的app的地址,最后跳转到该地址。
- 重新设置app中断向量表地址,设置的地方就在SystemInit 里设置向量偏移寄存器就可以了。这样APP的代码就可以跑起来了。iap的代码不变,可以不断地升级app的代码。
值得注意的是:工程里正在使用的只有一份中断向量表,在app中就不会使用iap的中断向量表。系统复位之后,设置的中断向量表没用了。因此每次复位之后都会,执行iap的代码,最终跳转到app。
实际应用举例
stm32的map文件
某个工程的文件编译出的文件
对应的map文件
可以看出来: 使用的ram的大小 ram = 7728 + 408 = 8136;
根据map文件,计算栈顶指针为:20001ac8 + 1280 = 0x20001fc8; 而 1fc8的大小刚好是8136。
所以实际的运行视图为,依次为:
内存起始地址
全局变量
静态变量
堆
堆内容
栈内容
栈
内存结束地址
可以看出来,堆的生长方向为,由小达大
栈的生长方向为,由大到小。