本帖最后由 DKENNY 于 2025-2-15 15:49 编辑
#申请原创# #技术资源# @21小跑堂
前言
想象一下,当你按下电源按钮,嵌入式设备瞬间苏醒,背后的秘密是什么?在今天的嵌入式开发中,理解 APM32 的启动流程不仅是工程师的基本功,更是解决复杂问题的关键。本文将带你一步步揭开 APM32 从上电到进入 main 函数的神秘面纱。
Cortex-M启动流程详细剖析(GCC环境)
我们这次的开发环境情况是这样的:用的处理器是 APM32F103 ,GCC 的版本呢,是 10.3.1 。
平常咱们开发桌面操作系统上的应用程序时,一般不太用操心系统初始化相关的事儿。为啥这么说呢?因为大多数应用程序都是在操作系统启动并稳定运行之后才开始运作的,操作系统早就给咱们准备好了适宜的运行环境。
然而,嵌入式设备可就大不一样喽!设备一通电,所有的设置工作都得咱们开发者亲自动手。刚上电那会儿,处理器没有堆栈,中断功能也没有开启,外围设备更是处于未配置的状态,这些工作都得依靠软件来进行设定。而且不同类型的 CPU 、不同容量的内存以及不同种类的外设,它们各自的初始化工作都存在差异。
今天我就以 APMF103(基于 Cortex - M3 架构)为例,给大伙详细说道说道。
下面咱们具体瞧瞧从 Flash 启动 APM32 的过程,重点讲讲从上电复位一直到进入 main 函数这段历程。主要有这么几个关键步骤:
- 首先要做的是初始化栈顶指针 sp 。为啥必须得这么做呢?这是因为在进入 C 程序之前,得先设定好栈地址。毕竟咱们是通过函数调用的方式进入 C 程序的,这个过程中是要用到栈空间的。
- 接着,要设置 PC 指针。
- 之后呢,要把 Flash 里的 data 段复制到 RAM 当中。
- 再然后,要对系统时钟进行配置。
- 最后,调用 C 库函数 _libc_init_array 来初始化用户堆栈,完成这些操作后,就可以进入 main 函数啦。
在正式详细展开讲这些步骤之前,咱们还得先了解一下 APM32 的启动模式 。
1 APM32 启动模式
咱先来讲讲 APM32 的启动模式哈,为啥要先讲这个呢?因为启动模式能决定向量表的位置。APM32 一共有三种启动模式:
1)主闪存存储器(Main Flash)启动:这种模式是从 APM32 内置的 Flash 启动,地址范围是 0x08000000 - 0x0807FFFF 。平常咱们用 JTAG 或者 SWD 模式往里面下载程序,下载好之后,重启时程序就直接从这儿启动。
给大家举个例子哈,就拿 0x08000000 对应的内存来说,这块内存既可以通过 0x00000000 这个地址来操作,也能通过 0x08000000 来操作,而且不管用哪个地址,操作的都是同一块内存哦。
2)系统存储器(System Memory)启动:这里说的系统储存器其实就是 APM32 的内置 ROM 。要是选了这个启动模式,内置 ROM 的起始地址就会被重映射到 0x00000000 这个地址,代码就从这儿开始运行。ROM 里有一段出厂就预置好的代码,这段代码可重要啦,它就像一座桥,能让外部通过 UART、CAN 或者 USB 这些方式,把代码写到 APM32 的内置 Flash 里。这段代码也叫 ISP(In System Programing)代码,用这种方式烧录代码就叫 ISP 烧录。
一般啥时候会选这种启动模式呢?就是咱们想从串口下载程序的时候。为啥呢?因为厂家提供的 ISP 程序里,有串口下载程序的固件,通过这个 ISP 程序就能把用户程序下载到系统的 Flash 里。
再举个内存地址的例子,像 0x1FFFFFF0 对应的内存,它既可以通过 0x00000000 操作,也能通过 0x1FFFFFF0 操作,操作的同样是同一块内存。
3)片上 SRAM 启动:这个模式是从内置 SRAM 启动,地址范围在 0x20000000 - 0x3FFFFFFF 。大家都知道 SRAM 嘛,它本身没有存储程序的能力,所以这个模式一般是在调试程序的时候用。而且 SRAM 只能通过 0x20000000 来操作,和前面那两种启动模式不太一样。从 SRAM 启动的时候,得在应用程序的初始化代码里重新设置向量表的位置。选了这个启动模式后,内置 SRAM 的起始地址会被重映射到 0x00000000 地址,代码就在这儿开始运行。这种模式有个好处,就是烧录程序的时候不用擦写 Flash ,速度比较快,很适合调试,不过一旦掉电,数据就没啦。
那怎么选择启动模式呢?用户可以通过设置 BOOT0 和 BOOT1 这两个引脚的电平状态,来决定复位后的启动模式。具体情况如下表所示。
这里大家要注意哈,启动模式只是决定程序烧录的地方,程序加载完之后会有一个重映射(映射到 0x00000000 这个地址位置);而真正产生复位信号的时候,CPU 还是从开始的位置执行程序。
还有个重要的点,APM32 上电复位以后,代码区都是从 0x00000000 开始的,这三种启动模式其实就是把各自存储空间的地址映射到 0x00000000 这个地址上。
2 APM32 启动文件分析
咱得知道,APM32 的启动过程大多得靠汇编来完成,所以启动相关的很多内容都在启动文件里。我用的启动文件是 startup_apm32f10x_hd.S 。除了这个,还有个链接文件,链接文件主要是规定了入口函数、堆栈大小,以及数据段的整体布局这些内容。这里要注意一下哈,要是用 MDK 的话,它有相应的内存管理相关操作,也就是 sct 分段加载,MDK 把这事儿给处理好了。
启动文件主要包含三个部分:定义各个内存地址、Reset_Handler 函数,还有中断向量表。
先看看这段代码:
- .syntax unified
- .cpu cortex - m3
- .fpusoftvfp
- .thumb
这里面,.cpu cortex - m3 的作用是明确 CPU 的类型,这里用的 CPU 核就是 Cortex - M 。.fpusoftvfp 呢,是说明了浮点运算的类型,因为 Cortex - M 没有硬件 FPU 单元,所以这里采用的是软件 FPU 。.thumb 则是指定了指令类型为 thumb 。
再看下面这段代码:
- .word _start_address_init_data
- .word _start_address_data
- .word _end_address_data
- .word _start_address_bss
- .word _end_address_bss
这里面这些名字都有各自的含义哈。_start_address_init_data 是 data 段地址相关的变量。_start_address_data 代表 data 段的起始地址。_end_address_data 是 data 段的结束地址。_start_address_bss 是 bss 段的起始地址。_end_address_bss 就是 bss 段的结束地址。
接着看这个代码块:- .section .text.Reset_Handler
- .weak Reset_Handler
- .type Reset_Handler, %function
- // Reset handler routine
- Reset_Handler:
- ldr r0, =_start_address_data
- ldr r1, =_end_address_data
- ldr r2, =_start_address_init_data
- movs r3, #0
- b L_loop0_0
.section.text.Reset_Handler 和 .weak Reset_Handler 这两句,是定义了一个新的代码段,并且把它申明为 weak 函数。.typeReset_Handler, %function 这句呢,是把 Reset_Handler 声明成函数。ldr r0, =_start_address_data、ldr r1, =_end_address_data、ldr r2, =_start_address_init_data 这三句,是在设置 data 段、bss 段的地址。b L_loop0_0 这句的作用是把 data 段复制到 RAM 中。
再往后看这部分代码:
- L_loop0:
- ldr r4, [r2, r3]
- str r4, [r0, r3]
- adds r3, r3, #4
- L_loop0_0:
- adds r4, r0, r3
- cmp r4, r1
- bcc L_loop0
-
- ldr r2, =_start_address_bss
- ldr r4, =_end_address_bss
- movs r3, #0
- b L_loop1
- L_loop2:
- str r3, [r2]
- adds r2, r2, #4
- L_loop1:
- cmp r2, r4
- bcc L_loop2
上面这一大段代码,就是把 Flash 里的 data 段完整地复制到 RAM 的整个过程,另外还包含了 bss 段的清零工作。
接下来,在进入 C 空间之前,还有这么一些准备工作:
- bl SystemInit
- bl __libc_init_array
- bl main
- bx lr
- .size Reset_Handler, .-Reset_Handler
这里面,bl SystemInit 是初始化系统时钟。bl __libc_init_array 是初始化 lib 库。bl main 就是跳转到 main 函数。
最后就是中断向量表的内容啦:
|